diff --git a/docs/cli/gateway.md b/docs/cli/gateway.md index 4613e5718f2..b224bb769d6 100644 --- a/docs/cli/gateway.md +++ b/docs/cli/gateway.md @@ -181,12 +181,20 @@ Fetch usage-cost summaries from session logs. ```bash openclaw gateway usage-cost openclaw gateway usage-cost --days 7 +openclaw gateway usage-cost --agent work --json +openclaw gateway usage-cost --all-agents openclaw gateway usage-cost --json ``` Number of days to include. + + Scope the cost summary to one configured agent id. + + + Aggregate the cost summary across all configured agents. Cannot be combined with `--agent`. + ### `gateway stability` diff --git a/src/cli/gateway-cli.coverage.test.ts b/src/cli/gateway-cli.coverage.test.ts index 139d6bd021a..d74b5f42cfb 100644 --- a/src/cli/gateway-cli.coverage.test.ts +++ b/src/cli/gateway-cli.coverage.test.ts @@ -213,6 +213,55 @@ describe("gateway-cli coverage", () => { }); }); + it("scopes usage-cost to a specific agent via --agent", async () => { + callGateway.mockClear(); + + await runGatewayCommand(["gateway", "usage-cost", "--agent", "alpha", "--days", "7", "--json"]); + + expect(callGateway).toHaveBeenCalledTimes(1); + const costCall = firstMockArg(callGateway) as { method?: string; params?: unknown }; + expect(costCall?.method).toBe("usage.cost"); + expect(costCall?.params).toEqual({ days: 7, agentId: "alpha" }); + }); + + it("omits agentId from usage-cost when --agent is absent or blank", async () => { + callGateway.mockClear(); + + await runGatewayCommand(["gateway", "usage-cost", "--agent", " ", "--days", "7", "--json"]); + + expect(callGateway).toHaveBeenCalledTimes(1); + const costCall = firstMockArg(callGateway) as { method?: string; params?: unknown }; + expect(costCall?.method).toBe("usage.cost"); + expect(costCall?.params).toEqual({ days: 7 }); + }); + + it("aggregates usage-cost across agents via --all-agents", async () => { + callGateway.mockClear(); + + await runGatewayCommand(["gateway", "usage-cost", "--all-agents", "--days", "7", "--json"]); + + expect(callGateway).toHaveBeenCalledTimes(1); + const costCall = firstMockArg(callGateway) as { method?: string; params?: unknown }; + expect(costCall?.method).toBe("usage.cost"); + expect(costCall?.params).toEqual({ days: 7, agentScope: "all" }); + }); + + it("rejects combining --agent with --all-agents for usage-cost", async () => { + callGateway.mockClear(); + + await expectGatewayExit([ + "gateway", + "usage-cost", + "--agent", + "alpha", + "--all-agents", + "--json", + ]); + + expect(callGateway).not.toHaveBeenCalled(); + expect(runtimeErrors.join("\n")).toContain("Use --agent or --all-agents, not both"); + }); + it("writes JSON for gateway health transport failures in JSON mode", async () => { const error = new Error("gateway closed (1006)"); const payload = { diff --git a/src/cli/gateway-cli/register.ts b/src/cli/gateway-cli/register.ts index a80504d8cfd..e3d7d0ea679 100644 --- a/src/cli/gateway-cli/register.ts +++ b/src/cli/gateway-cli/register.ts @@ -556,12 +556,24 @@ export function registerGatewayCli(program: Command) { .command("usage-cost") .description("Fetch usage cost summary from session logs") .option("--days ", "Number of days to include", "30") + .option("--agent ", "Scope the cost summary to a specific agent id") + .option("--all-agents", "Aggregate the cost summary across all agents", false) .action(async (opts, command) => { await runGatewayCommand( async () => { const rpcOpts = resolveGatewayRpcOptions(opts, command); const days = parseDaysOption(opts.days); - const result = await callGatewayCli("usage.cost", rpcOpts, { days }); + const agentId = typeof opts.agent === "string" ? opts.agent.trim() : undefined; + // The gateway honors agentScope only when no agentId is set, so reject the + // ambiguous combination here instead of silently dropping --all-agents. + if (agentId && opts.allAgents) { + throw new Error("Use --agent or --all-agents, not both"); + } + const result = await callGatewayCli("usage.cost", rpcOpts, { + days, + ...(agentId ? { agentId } : {}), + ...(opts.allAgents ? { agentScope: "all" } : {}), + }); if (rpcOpts.json) { defaultRuntime.writeJson(result); return;