/** * Kimi Quota Command (kb #62) — registers `/quota` on Adolf's Matrix channel. * * OpenClaw's native-command dispatch (`api.registerCommand`) runs a * `/`-prefixed command BEFORE the agent turn: no model is invoked, so this * never spends a Kimi turn (unlike asking Adolf in prose "what's my quota"). * It hits adolf-llm's own GET /usage route (server.js, kb #62 piece 1), which * itself talks straight to Kimi's managed-usage API — no LLM anywhere in the * path. * * Gating: `requireAuth: true` (the registerCommand default) restricts the * command to `ctx.isAuthorizedSender`, i.e. the same Matrix DM allowlist * (`channels.matrix.dm.allowFrom` in openclaw.json) that already gates every * other interaction with Adolf. No separate owner-only tier is needed here — * it's a read-only status line, not a privileged action. */ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; // adolf-llm is a sibling service on the same `openai` compose network — // reached by service name, not localhost/host.docker.internal. const USAGE_URL = "http://adolf-llm:8010/usage"; const FETCH_TIMEOUT_MS = 5000; function pct(row) { return row && typeof row.pct === "number" ? `${row.pct}%` : "n/a"; } async function fetchUsage() { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); try { const res = await fetch(USAGE_URL, { signal: controller.signal }); const body = await res.json().catch(() => ({})); if (!res.ok) throw new Error(body?.error || `adolf-llm /usage HTTP ${res.status}`); return body; } finally { clearTimeout(timer); } } export default definePluginEntry({ id: "quota-command", name: "Codex Quota Command", description: "LLM-free /quota command: reads Adolf's Codex usage from adolf-llm:8010/usage and replies with a compact readout.", register(api) { api.registerCommand({ name: "quota", description: "Show Codex quota usage for the plan's rate-limit windows — no model call.", acceptsArgs: false, requireAuth: true, handler: async () => { try { const usage = await fetchUsage(); // Codex reports up to two plan-defined windows rather than Kimi's // fixed 5h/weekly/7d buckets; render whichever exist, shortest // first, labelled from the data itself. const rows = [usage.secondary, usage.primary] .filter((r) => r && typeof r.pct === "number") .map((r) => (r.window_label ? `${r.window_label} ${r.pct}%` : pct(r))); let line = rows.length ? `Codex: ${rows.join(" · ")}` : "Codex: no rate-limit windows reported"; if (usage.plan) line += ` (${usage.plan} plan)`; if (usage.limit_reached) line += " ⚠ limit reached"; if (usage.stale) line += ` — stale, ${usage.age_s}s old`; return { text: line, suppressReply: true }; } catch (e) { api.logger?.warn?.(`quota-command: fetch failed (${e?.message || e})`); return { text: `Codex quota unavailable: ${e?.message || e}`, suppressReply: true }; } }, }); }, });