Skill: udaymanish6/discord-output-metrics-footer (version 0.1.0)

--- name: discord-output-metrics-footer description: Install, configure, maintain, or troubleshoot a compact Discord output footer for OpenClaw that shows live context usage, output tokens, Codex quota remaining, model used, and optional subagent token aggregate under each Discord response. --- # Discord Output Metrics Footer Use this skill when an OpenClaw workspace should show compact runtime metrics under Discord outputs. Default footer: ```text _🟢 ↑54k ↓157 · 21%ctx · 5h 89% · kimi-k2.6:cloud_ ``` With subagent aggregate: ```text _🟢 ↑54k ↓157 · 21%ctx · 5h 89% · openai-codex/gpt-5.5 · sub ↑31k ↓4k_ ``` ## What it does - Adds a Discord delivery-time footer using OpenClaw plugin hooks. - Uses `llm_output` for actual model/token metrics. - Uses `message_sending` to append the footer without adding prompt tokens. - Uses live/cached OpenAI Codex OAuth usage for `5h 89%`. - Uses color status from context usage and quota remaining. - Optionally aggregates nearby subagent LLM usage. ## Install into an OpenClaw workspace Copy the bundled extension template: ```bash mkdir -p ~/.openclaw/extensions/discord-output-metrics-footer cp -R assets/extension-template/* ~/.openclaw/extensions/discord-output-metrics-footer/ ``` Add this plugin config to `~/.openclaw/openclaw.json`: ```json { "plugins": { "allow": ["discord-output-metrics-footer"], "load": { "paths": ["~/.openclaw/extensions/discord-output-metrics-footer"] }, "entries": { "discord-output-metrics-footer": { "enabled": true, "config": { "cacheMs": 120000, "quotaCacheMs": 60000, "contextReserveTokens": 40000, "appendSubagents": true, "disabledConversations": [] } } } } } ``` Then validate and restart: ```bash openclaw config validate openclaw gateway restart ``` ## Channel deployment The plugin can run for any Discord channel. To disable specific Discord channel IDs, add them to `disabledConversations`. Recommended defaults: - Enable in active work channels like `#general`, `#coding`, `#reasoning`, and audit channels. - Disable in status-only channels like `#feed` and incident channels like `#system` if noise matters. ## Configuration Fields: - `cacheMs`: window for matching recent LLM output and subagent output. - `quotaCacheMs`: minimum interval between Codex quota refreshes. - `contextReserveTokens`: safety reserve subtracted from usable context calculations. - `appendSubagents`: append `sub ↑x ↓y` when nearby subagent usage is detected. - `disabledConversations`: Discord channel IDs where no footer should be appended. ## Color status - `🟢`: context under 50% and quota over 50%. - `🟡`: context 50-80% or quota 20-50%. - `🔴`: context over 80% or quota under 20%. If context and quota disagree, show the worse status. ## Guardrails - Do not put footer instructions in agent prompts. - Do not call an LLM to calculate footer metrics. - Do not fetch Codex quota on every message; use cache. - Do not show emails, OAuth profile names, token values, API key prefixes, or auth file paths. - Do not add the footer when it would exceed Discord's message limit. Read `references/implementation.md` before changing the extension code.

path: README.md
content: # Discord Output Metrics Footer

Compact runtime telemetry for OpenClaw Discord responses.

This skill installs an OpenClaw extension that appends a one-line footer under Discord outputs showing context usage, output tokens, live/cached Codex quota, model used, and optional subagent token aggregate.

```text
_🟢 ↑54k ↓157 · 21%ctx · 5h 89% · kimi-k2.6:cloud_
```

With subagents:

```text
_🟢 ↑54k ↓157 · 21%ctx · 5h 89% · openai-codex/gpt-5.5 · sub ↑31k ↓4k_
```

## What it shows

- `↑54k`: input/context tokens used for the turn.
- `↓157`: assistant output tokens for the turn.
- `21%ctx`: current context window usage.
- `5h 89%`: live/cached OpenAI Codex short-window quota remaining.
- `kimi-k2.6:cloud`: model used for the turn.
- `sub ↑31k ↓4k`: optional best-effort aggregate of nearby subagent usage.

## Color status

- `🟢`: context under 50% and quota over 50%.
- `🟡`: context 50-80% or quota 20-50%.
- `🔴`: context over 80% or quota under 20%.

If context and quota disagree, the footer shows the worse status.

## Install

Install this skill into your OpenClaw workspace, then copy the bundled extension template:

```bash
mkdir -p ~/.openclaw/extensions/discord-output-metrics-footer
cp -R assets/extension-template/* ~/.openclaw/extensions/discord-output-metrics-footer/
```

Add the plugin to `~/.openclaw/openclaw.json`:

```json
{
  "plugins": {
    "allow": ["discord-output-metrics-footer"],
    "load": {
      "paths": ["~/.openclaw/extensions/discord-output-metrics-footer"]
    },
    "entries": {
      "discord-output-metrics-footer": {
        "enabled": true,
        "config": {
          "cacheMs": 120000,
          "quotaCacheMs": 60000,
          "contextReserveTokens": 40000,
          "appendSubagents": true,
          "disabledConversations": []
        }
      }
    }
  }
}
```

Validate and restart:

```bash
openclaw config validate
openclaw gateway restart
```

## Channel deployment

The extension works for any Discord channel. Use `disabledConversations` to suppress the footer in specific Discord channel IDs.

Recommended:

- Enable in work channels such as `#general`, `#coding`, `#reasoning`, and audit channels.
- Disable in status-only or incident channels such as `#feed` and `#system` if you want less noise.

## Configuration

| Field | Default | Purpose |
| --- | --- | --- |
| `cacheMs` | `120000` | Window for matching recent LLM output and subagent output. |
| `quotaCacheMs` | `60000` | Minimum interval between Codex quota refreshes. |
| `contextReserveTokens` | `40000` | Safety reserve for context calculations. |
| `appendSubagents` | `true` | Append `sub ↑x ↓y` when nearby subagent usage is detected. |
| `disabledConversations` | `[]` | Discord channel IDs where no footer should be appended. |

## Privacy

The extension must never print:

- OAuth tokens.
- API keys.
- Email addresses.
- Auth profile names.
- Local auth file paths.

If Codex quota cannot be fetched, the footer omits the quota field rather than blocking delivery.

## Files

```text
discord-output-metrics-footer/
├── SKILL.md
├── README.md
├── assets/
│   └── extension-template/
│       ├── index.ts
│       ├── openclaw.plugin.json
│       └── package.json
└── references/
    └── implementation.md
```

## Publish to ClawHub

```bash
clawhub publish . --slug discord-output-metrics-footer --name "Discord Output Metrics Footer" --version 0.1.0 --changelog "Initial skill with bundled OpenClaw extension template"
```

## License

MIT



path: assets/extension-template/index.ts
content: import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";

type Usage = {
  provider?: string;
  model?: string;
  input?: number;
  output?: number;
  total?: number;
  ts: number;
  sessionKey?: string;
};

type Quota = {
  percent?: number;
  weeklyPercent?: number;
  resetLabel?: string;
  ts: number;
};

type Config = {
  enabled?: boolean;
  disabledConversations?: string[];
  disabledChannels?: string[];
  cacheMs?: number;
  quotaCacheMs?: number;
  contextReserveTokens?: number;
  appendSubagents?: boolean;
};

const recentBySession = new Map<string, Usage>();
const recentOutputs: Usage[] = [];
let quotaCache: Quota | null = null;
let quotaFetchInFlight: Promise<Quota | null> | null = null;

const MODEL_CONTEXT: Record<string, number> = {
  "openai-codex/gpt-5.5": 391000,
  "gpt-5.5": 391000,
  "openai-codex/gpt-5.4-mini": 266000,
  "gpt-5.4-mini": 266000,
  "ollama/kimi-k2.6:cloud": 250000,
  "kimi-k2.6:cloud": 250000,
  "ollama/qwen3.5:397b-cloud": 250000,
  "qwen3.5:397b-cloud": 250000
};

function n(v: unknown): number | undefined {
  return typeof v === "number" && Number.isFinite(v) ? v : undefined;
}

function fmt(v?: number): string {
  if (!Number.isFinite(v)) return "n/a";
  const x = Number(v);
  if (x >= 1_000_000) return `${Math.round(x / 100_000) / 10}m`;
  if (x >= 10_000) return `${Math.round(x / 1000)}k`;
  if (x >= 1000) return `${Math.round(x / 100) / 10}k`;
  return String(Math.round(x));
}

function colorForUsage(percent?: number): string {
  if (!Number.isFinite(percent)) return "⚪";
  if (Number(percent) >= 80) return "🔴";
  if (Number(percent) >= 50) return "🟡";
  return "🟢";
}

function colorForRemaining(percent?: number): string {
  if (!Number.isFinite(percent)) return "⚪";
  if (Number(percent) < 20) return "🔴";
  if (Number(percent) <= 50) return "🟡";
  return "🟢";
}

function worst(...colors: string[]): string {
  if (colors.includes("🔴")) return "🔴";
  if (colors.includes("🟡")) return "🟡";
  if (colors.includes("🟢")) return "🟢";
  return "⚪";
}

function modelLabel(provider?: string, model?: string): string {
  const m = model || "model";
  if (!provider || m.includes("/")) return m;
  return `${provider}/${m}`;
}

function contextWindow(model?: string, provider?: string): number | undefined {
  const label = modelLabel(provider, model);
  return MODEL_CONTEXT[label] ?? MODEL_CONTEXT[model || ""] ?? undefined;
}

function latestUsage(sessionKey?: string): Usage | undefined {
  if (sessionKey && recentBySession.has(sessionKey)) return recentBySession.get(sessionKey);
  return recentOutputs.at(-1);
}

function aggregateSubagents(root?: Usage, cacheMs = 120000): { input: number; output: number } | null {
  if (!root) return null;
  const cutoff = Date.now() - cacheMs;
  let input = 0;
  let output = 0;
  for (const u of recentOutputs) {
    if (u === root || u.ts < cutoff) continue;
    if (Math.abs(root.ts - u.ts) > cacheMs) continue;
    if (u.sessionKey && root.sessionKey && u.sessionKey === root.sessionKey) continue;
    input += u.input ?? 0;
    output += u.output ?? 0;
  }
  return input || output ? { input, output } : null;
}

function authProfilesPath(): string {
  return path.join(os.homedir(), ".openclaw", "agents", "main", "agent", "auth-profiles.json");
}

function findCodexAccessToken(): string | null {
  try {
    const raw = JSON.parse(fs.readFileSync(authProfilesPath(), "utf8"));
    for (const profile of Object.values(raw.profiles ?? {}) as any[]) {
      if (profile?.provider !== "openai-codex" || profile?.type !== "oauth") continue;
      if (typeof profile.access === "string") return profile.access;
      if (typeof profile.access?.accessToken === "string") return profile.access.accessToken;
    }
  } catch {
    return null;
  }
  return null;
}

function resetLabelFromNow(resetAt?: unknown): string | undefined {
  if (typeof resetAt !== "number") return undefined;
  const ms = resetAt > 10_000_000_000 ? resetAt - Date.now() : resetAt * 1000 - Date.now();
  if (!Number.isFinite(ms) || ms <= 0) return undefined;
  const h = Math.max(1, Math.round(ms / 3600000));
  return `${h}h`;
}

function parseQuota(data: any): Quota {
  const candidates = [
    data?.codex,
    data?.plus,
    data?.pro,
    data?.usage,
    data
  ];
  let percent: number | undefined;
  let weeklyPercent: number | undefined;
  let resetLabel: string | undefined;
  for (const c of candidates) {
    if (!c || typeof c !== "object") continue;
    percent ??= n(c.remaining_percent) ?? n(c.remainingPercent) ?? n(c.percent_remaining) ?? n(c.percentRemaining);
    weeklyPercent ??= n(c.weekly_remaining_percent) ?? n(c.weeklyRemainingPercent);
    resetLabel ??= resetLabelFromNow(c.reset_at ?? c.resetAt);
    if (percent == null && Number.isFinite(c.requests_used) && Number.isFinite(c.requests_limit)) {
      percent = Math.round(((c.requests_limit - c.requests_used) / c.requests_limit) * 100);
    }
  }
  return { percent, weeklyPercent, resetLabel: resetLabel ?? "5h", ts: Date.now() };
}

async function fetchCodexQuota(cacheMs: number): Promise<Quota | null> {
  if (quotaCache && Date.now() - quotaCache.ts < cacheMs) return quotaCache;
  if (quotaFetchInFlight) return quotaFetchInFlight;
  quotaFetchInFlight = (async () => {
    const [REDACTED_SECRET]);
    if (!token) return quotaCache;
    try {
      const res = await fetch("https://chatgpt.com/backend-api/wham/usage", {
        headers: {
          Authorization: `Bearer ${token}`,
          Accept: "application/json"
        },
        signal: AbortSignal.timeout(4000)
      });
      if (!res.ok) return quotaCache;
      quotaCache = parseQuota(await res.json());
      return quotaCache;
    } catch {
      return quotaCache;
    } finally {
      quotaFetchInFlight = null;
    }
  })();
  return quotaFetchInFlight;
}

function appendFooter(content: string, footer: string): string {
  const line = `\n\n_${footer}_`;
  if (content.includes("↑") && content.includes("%ctx")) return content;
  if (content.length + line.length > 1990) return content;
  return `${content}${line}`;
}

export default definePluginEntry({
  id: "discord-output-metrics-footer",
  name: "Discord Output Metrics Footer",
  description: "Append compact context/token/quota metrics to Discord outputs.",
  register(api) {
    const cfg = (api.pluginConfig ?? {}) as Config;
    if (cfg.enabled === false) return;
    const cacheMs = cfg.cacheMs ?? 120000;
    const quotaCacheMs = cfg.quotaCacheMs ?? 60000;
    const reserve = cfg.contextReserveTokens ?? 40000;

    api.on("llm_output", async (event: any, ctx: any) => {
      const usage: Usage = {
        provider: event.provider ?? ctx.modelProviderId,
        model: event.model ?? ctx.modelId,
        input: n(event.usage?.input),
        output: n(event.usage?.output),
        total: n(event.usage?.total),
        ts: Date.now(),
        sessionKey: ctx.sessionKey
      };
      recentOutputs.push(usage);
      if (usage.sessionKey) recentBySession.set(usage.sessionKey, usage);
      while (recentOutputs.length > 80) recentOutputs.shift();
    }, { name: "discord-output-metrics-footer-llm-output" });

    api.on("message_sending", async (event: any, ctx: any) => {
      if (ctx.channelId !== "discord") return;
      if ((cfg.disabledChannels ?? []).includes(ctx.channelId)) return;
      if ((cfg.disabledConversations ?? []).includes(ctx.conversationId)) return;
      if (!event.content || typeof event.content !== "string") return;

      const usage = latestUsage();
      const quota = await fetchCodexQuota(quotaCacheMs);
      const label = modelLabel(usage?.provider, usage?.model);
      const win = contextWindow(usage?.model, usage?.provider);
      const total = usage?.total ?? ((usage?.input ?? 0) + (usage?.output ?? 0));
      const ctxPct = win ? Math.round((total / win) * 100) : undefined;
      const status = worst(colorForUsage(ctxPct), colorForRemaining(quota?.percent));
      const parts = [
        `${status} ↑${fmt(usage?.input)} ↓${fmt(usage?.output)}`,
        `${Number.isFinite(ctxPct) ? ctxPct : "n/a"}%ctx`
      ];
      if (quota?.percent != null) parts.push(`${quota.resetLabel ?? "5h"} ${quota.percent}%`);
      parts.push(label);
      const sub = cfg.appendSubagents === false ? null : aggregateSubagents(usage, cacheMs);
      if (sub) parts.push(`sub ↑${fmt(sub.input)} ↓${fmt(sub.output)}`);
      return { content: appendFooter(event.content, parts.join(" · ")) };
    }, { name: "discord-output-metrics-footer-message-sending" });
  }
});


path: assets/extension-template/openclaw.plugin.json
content: {
  "id": "discord-output-metrics-footer",
  "name": "Discord Output Metrics Footer",
  "version": "0.1.0",
  "kind": "code-plugin",
  "configSchema": {
    "type": "object",
    "additionalProperties": true
  },
  "extensions": [
    "./index.ts"
  ]
}


path: assets/extension-template/package.json
content: {
  "name": "discord-output-metrics-footer",
  "version": "0.1.0",
  "type": "module",
  "description": "Append compact context/token/quota metrics to OpenClaw Discord outputs.",
  "main": "index.ts",
  "openclaw": {
    "extensions": [
      "./index.ts"
    ],
    "compat": {
      "pluginApi": ">=2026.3.24-beta.2",
      "minGatewayVersion": "2026.4.23-beta.5"
    }
  }
}


path: references/implementation.md
content: # Implementation

Bundled extension template:

```text
assets/extension-template/
```

Expected deployed extension path:

```text
~/.openclaw/extensions/discord-output-metrics-footer/
```

## Hooks

The extension registers:

```text
llm_output
message_sending
```

`llm_output` records recent provider/model/token usage.

`message_sending` appends the footer only for Discord delivery.

This keeps the footer out of model prompts and memory context.

## Footer format

```text
_🟢 ↑54k ↓157 · 21%ctx · 5h 89% · kimi-k2.6:cloud_
```

Fields:

- `↑54k`: input/context tokens for the turn.
- `↓157`: assistant output tokens for the turn.
- `21%ctx`: current context usage percentage.
- `5h 89%`: live/cached Codex short-window quota remaining.
- `kimi-k2.6:cloud`: model used for the turn.

Subagent aggregate:

```text
_🟢 ↑54k ↓157 · 21%ctx · 5h 89% · openai-codex/gpt-5.5 · sub ↑31k ↓4k_
```

Subagent accounting is best effort. The current extension aggregates nearby LLM outputs from other sessions within the cache window. If OpenClaw exposes parent/root run IDs later, replace this heuristic with exact parent-run matching.

## Codex quota

The extension reads the local OpenClaw auth profile store to find an OpenAI Codex OAuth access token, then fetches:

```text
https://chatgpt.com/backend-api/wham/usage
```

Privacy rules:

- Never print the token.
- Never print the email.
- Never print the auth profile ID.
- Never print the auth profile path in Discord.

If quota fetch fails, omit the quota field rather than blocking delivery.

## Model context

The extension includes a small context-window map for common configured models:

- `openai-codex/gpt-5.5`: `391k`
- `openai-codex/gpt-5.4-mini`: `266k`
- `ollama/kimi-k2.6:cloud`: `250k`
- `ollama/qwen3.5:397b-cloud`: `250k`

If a model is unknown, show `n/a%ctx` instead of guessing.

## ClawHub publish

From the skill folder:

```bash
clawhub publish . --slug discord-output-metrics-footer --name "Discord Output Metrics Footer" --version 0.1.0 --changelog "Initial skill with bundled OpenClaw extension template"
```

Run `clawhub login` first if not authenticated.



Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)