Usage: add cost summaries to /usage + mac menu

This commit is contained in:
Peter Steinberger
2026-01-19 00:04:58 +00:00
parent 1ea3ac0a1d
commit 3ce1ee84ac
14 changed files with 706 additions and 10 deletions

View File

@@ -59,14 +59,14 @@ describe("commands registry args", () => {
name: "mode",
description: "mode",
type: "string",
choices: ["off", "tokens", "full"],
choices: ["off", "tokens", "full", "cost"],
},
],
};
const menu = resolveCommandArgMenu({ command, args: undefined, cfg: {} as never });
expect(menu?.arg.name).toBe("mode");
expect(menu?.choices).toEqual(["off", "tokens", "full"]);
expect(menu?.choices).toEqual(["off", "tokens", "full", "cost"]);
});
it("does not show menus when arg already provided", () => {
@@ -82,7 +82,7 @@ describe("commands registry args", () => {
name: "mode",
description: "mode",
type: "string",
choices: ["off", "tokens", "full"],
choices: ["off", "tokens", "full", "cost"],
},
],
};
@@ -141,7 +141,7 @@ describe("commands registry args", () => {
name: "mode",
description: "on or off",
type: "string",
choices: ["off", "tokens", "full"],
choices: ["off", "tokens", "full", "cost"],
},
],
};

View File

@@ -233,14 +233,14 @@ function buildChatCommands(): ChatCommandDefinition[] {
defineChatCommand({
key: "usage",
nativeName: "usage",
description: "Toggle per-response usage line.",
description: "Usage footer or cost summary.",
textAlias: "/usage",
args: [
{
name: "mode",
description: "off, tokens, or full",
description: "off, tokens, full, or cost",
type: "string",
choices: ["off", "tokens", "full"],
choices: ["off", "tokens", "full", "cost"],
},
],
argsMenu: "auto",

View File

@@ -7,6 +7,8 @@ import { scheduleGatewaySigusr1Restart, triggerClawdbotRestart } from "../../inf
import { parseActivationCommand } from "../group-activation.js";
import { parseSendPolicyCommand } from "../send-policy.js";
import { normalizeUsageDisplay, resolveResponseUsageMode } from "../thinking.js";
import { loadCostUsageSummary, loadSessionCostSummary } from "../../infra/session-cost-usage.js";
import { formatTokenCount, formatUsd } from "../../utils/usage-format.js";
import {
formatAbortReplyText,
isAbortTrigger,
@@ -141,10 +143,48 @@ export const handleUsageCommand: CommandHandler = async (params, allowTextComman
const rawArgs = normalized === "/usage" ? "" : normalized.slice("/usage".length).trim();
const requested = rawArgs ? normalizeUsageDisplay(rawArgs) : undefined;
if (rawArgs.toLowerCase().startsWith("cost")) {
const sessionSummary = await loadSessionCostSummary({
sessionId: params.sessionEntry?.sessionId,
sessionEntry: params.sessionEntry,
sessionFile: params.sessionEntry?.sessionFile,
config: params.cfg,
});
const summary = await loadCostUsageSummary({ days: 30, config: params.cfg });
const sessionCost = formatUsd(sessionSummary?.totalCost);
const sessionTokens = sessionSummary?.totalTokens
? formatTokenCount(sessionSummary.totalTokens)
: undefined;
const sessionMissing = sessionSummary?.missingCostEntries ?? 0;
const sessionSuffix = sessionMissing > 0 ? " (partial)" : "";
const sessionLine =
sessionCost || sessionTokens
? `Session ${sessionCost ?? "n/a"}${sessionSuffix}${sessionTokens ? ` · ${sessionTokens} tokens` : ""}`
: "Session n/a";
const todayKey = new Date().toLocaleDateString("en-CA");
const todayEntry = summary.daily.find((entry) => entry.date === todayKey);
const todayCost = formatUsd(todayEntry?.totalCost);
const todayMissing = todayEntry?.missingCostEntries ?? 0;
const todaySuffix = todayMissing > 0 ? " (partial)" : "";
const todayLine = `Today ${todayCost ?? "n/a"}${todaySuffix}`;
const last30Cost = formatUsd(summary.totals.totalCost);
const last30Missing = summary.totals.missingCostEntries;
const last30Suffix = last30Missing > 0 ? " (partial)" : "";
const last30Line = `Last 30d ${last30Cost ?? "n/a"}${last30Suffix}`;
return {
shouldContinue: false,
reply: { text: `💸 Usage cost\n${sessionLine}\n${todayLine}\n${last30Line}` },
};
}
if (rawArgs && !requested) {
return {
shouldContinue: false,
reply: { text: "⚙️ Usage: /usage off|tokens|full" },
reply: { text: "⚙️ Usage: /usage off|tokens|full|cost" },
};
}