@jeffreyjyz/opencode-command-code 0.5.3 → 0.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.d.ts CHANGED
@@ -1,5 +1,3 @@
1
- export declare function authHeaders(key: string): Record<string, string>;
2
- export declare function getJson(path: string, key: string, timeoutMs?: number): Promise<unknown>;
3
1
  export type Whoami = {
4
2
  success: boolean;
5
3
  user?: {
@@ -30,13 +28,6 @@ export type Credits = {
30
28
  weekly?: Window;
31
29
  };
32
30
  };
33
- export type Summary = {
34
- totalCount?: number;
35
- totalCost?: number;
36
- successRate?: number;
37
- totalTokensIn?: number;
38
- totalTokensOut?: number;
39
- };
40
31
  export type ProviderModelsResp = {
41
32
  data: Array<{
42
33
  id: string;
@@ -48,5 +39,4 @@ export type ProviderModelsResp = {
48
39
  export declare function whoami(key: string): Promise<Whoami>;
49
40
  export declare function subscriptions(key: string): Promise<SubData>;
50
41
  export declare function credits(key: string): Promise<Credits>;
51
- export declare function usageSummary(key: string): Promise<Summary>;
52
42
  export declare function providerModels(key: string): Promise<ProviderModelsResp>;
package/dist/cli.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { spawn } from "node:child_process";
1
2
  /** Fallback lookup paths: the opencode background service's PATH often lacks
2
3
  * the homebrew prefix, so the bare name alone is not enough. `CMDUSE_BIN` wins
3
4
  * when set — the dev-build hook the sibling cmduse consumers (mpc) honour too. */
@@ -6,6 +7,9 @@ export declare function cmduseCandidates(env?: Record<string, string | undefined
6
7
  * wins when set (the `bun link` dev hook oc-cmd-compare installs),
7
8
  * then the bun/homebrew fallbacks. */
8
9
  export declare function mpcCandidates(env?: Record<string, string | undefined>): string[];
10
+ /** Same lookup for ocuse, the OpenCode Go/Zen reader shipped in the cmd-usage
11
+ * crate: `OCUSE_BIN` wins when set, then the homebrew fallbacks. */
12
+ export declare function ocuseCandidates(env?: Record<string, string | undefined>): string[];
9
13
  /** Shell-words split: whitespace-separated, with single/double quoting and
10
14
  * backslash escapes ("--tz +05:30", arg='a b'). Unterminated quotes close at
11
15
  * end of input. */
@@ -18,20 +22,39 @@ export declare function buildCmduseArgs(arg: string): string[];
18
22
  * string is split and passed straight through; an empty arg means
19
23
  * the fewest-columns view (see MPC_DEFAULT_ARGS). */
20
24
  export declare function buildMpcArgs(arg: string): string[];
25
+ /**
26
+ * The arg the ocuse_usage tool and the /ocuse-usage dialog pass on: the
27
+ * agent's (or slash) args when given, else `-1`. A bare ocuse is the live
28
+ * watch loop and would never return, so empty input never passes through.
29
+ */
30
+ export declare function ocuseArgs(arg: string | undefined): string;
21
31
  export type RunOptions = {
22
32
  signal?: AbortSignal;
23
33
  /** Extra env for the child (CMD_API_KEY when the plugin resolved one). */
24
34
  env?: Record<string, string>;
25
35
  /** Overridable for tests. */
26
36
  candidates?: readonly string[];
37
+ /** Extra spawn options. Excludes `stdio`: the callbacks below assume stdout
38
+ * and stderr are piped (the default here), so callers may only add fields
39
+ * that leave that contract intact — the sidebar passes `detached` (see
40
+ * constants/cli.ts; its own stdio equals the default). */
41
+ spawnOptions?: Omit<NonNullable<Parameters<typeof spawn>[2]>, "stdio">;
42
+ /** Kill the child after this many ms. The sidebar bounds each poll; the
43
+ * tool spawns leave it unset. */
44
+ timeoutMs?: number;
27
45
  };
28
46
  /** Run cmduse and return its plain-text stdout. Tries PATH, then common
29
47
  * homebrew locations; every candidate failing with ENOENT yields a single
30
48
  * install-hint error. */
31
- export declare function runCmduse(arg: string, opts?: RunOptions): Promise<string>;
49
+ export declare function runCmduse(arg: string | readonly string[], opts?: RunOptions): Promise<string>;
32
50
  /** Run mpc and return its plain-text stdout (the per-model price
33
51
  * comparison). Same contract as runCmduse: `MPC_BIN` wins, then the
34
52
  * fallback paths; every candidate failing with ENOENT yields a
35
53
  * single install-hint error, and any other failure (a non-zero exit
36
54
  * included) surfaces immediately with mpc's last stderr lines. */
37
- export declare function runMpc(arg: string, opts?: RunOptions): Promise<string>;
55
+ export declare function runMpc(arg: string | readonly string[], opts?: RunOptions): Promise<string>;
56
+ /** Run ocuse and return its plain-text stdout (OpenCode Go/Zen usage). Same
57
+ * contract as runCmduse; ocuse ships in the cmd-usage crate, so its install
58
+ * hint is the same brew formula. A string arg is shell-split, an array used
59
+ * as-is. */
60
+ export declare function runOcuse(arg: string | readonly string[], opts?: RunOptions): Promise<string>;
@@ -13,12 +13,20 @@ export declare const ZEN_PROVIDER_ID = "opencode";
13
13
  export declare const ZEN_PROVIDER_PREFIX = "opencode-zen";
14
14
  /** TUI plugin id (stable contract; test/tui.test.ts pins it). */
15
15
  export declare const TUI_ID = "command-code.tui";
16
+ /** The v2 /cmd-usage slash command id (registered and bound in tui.tsx). */
17
+ export declare const CMD_USAGE_COMMAND_ID = "command-code.cmd-usage";
18
+ /** The v2 /ocuse-usage slash command id (registered and bound in tui.tsx). */
19
+ export declare const OCUSE_USAGE_COMMAND_ID = "command-code.ocuse-usage";
16
20
  /** The cmd_usage tool description, shared by the v1 and v2 halves. */
17
21
  export declare const TOOL_DESCRIPTION = "Fetch live Command Code plan/usage: plan name, monthly credits, 5-hour & weekly windows, billing-period summary. Pass arg='plans' for the plan comparison table only, or extra cmduse flags (e.g. '--tz +05:30 daily').";
18
22
  /** The mpc_prices tool description, shared by the v1 and v2 halves. */
19
23
  export declare const MPC_TOOL_DESCRIPTION = "Per-model price comparison: what a model costs per request ($/1M input, output and cache rates) and per month (request allowance) on each Command Code (cc) and OpenCode Go (oc) plan side, plus ability, Tok/s, the cheaper-side verdict and running deals. Pass '--model <regex>' to filter rows, '--minimal' | '--medium' | '--detail' for table width, '--json' for the full structured rows, '--cc-plan' / '--oc-plan' to pick a plan, or '--usage' to project real usage.";
20
24
  /** The mpc_prices `arg` description, shared by the v1 and v2 halves. */
21
25
  export declare const MPC_TOOL_ARG_DESCRIPTION = "Optional mpc flags: '--model <regex>' filters rows, '--minimal' | '--medium' | '--detail' set table width, '--json' for full structured rows, '--cc-plan'/'--oc-plan' pick a plan, '--usage' projects real usage";
26
+ /** The ocuse_usage tool description, shared by the v1 and v2 halves. */
27
+ export declare const OCUSE_TOOL_DESCRIPTION = "Fetch live OpenCode Go/Zen usage: the account's window meters, per-model spend against limits, 5-hour & weekly windows, billing-period summary. Defaults to the one-shot dashboard; pass extra ocuse flags for more (e.g. '--plan go-plus', 'daily', '--json').";
28
+ /** The ocuse_usage `arg` description, shared by the v1 and v2 halves. */
29
+ export declare const OCUSE_TOOL_ARG_DESCRIPTION = "Optional: extra ocuse flags (e.g. '--plan go-plus', 'daily', '--json'); empty means the one-shot dashboard";
22
30
  export type Lane = {
23
31
  id: "command-code-anthropic" | "command-code-openai";
24
32
  name: string;
@@ -11,8 +11,9 @@ export declare const ROW_WIDTH = 37;
11
11
  export declare const TIER_DISPLAY: Readonly<Record<Category, string>>;
12
12
  /**
13
13
  * OpenCode Go plans the panel can render: the same token rates, different
14
- * per-model monthly limits. Which one a user is on is not discoverable (Go has
15
- * no usage API and the store records no subscription), so it is a setting.
14
+ * per-model monthly limits. Which one a user is on is not discoverable even with
15
+ * the usage endpoint in hand — it answers percentages, not a plan id, and the
16
+ * store records no subscription — so it is a setting.
16
17
  */
17
18
  export declare const OC_PLANS: readonly ["go", "go-plus"];
18
19
  export type OcPlan = (typeof OC_PLANS)[number];
package/dist/index.js CHANGED
@@ -2414,7 +2414,7 @@ function superRefine(fn) {
2414
2414
  // package.json
2415
2415
  var package_default = {
2416
2416
  name: "@jeffreyjyz/opencode-command-code",
2417
- version: "0.5.3",
2417
+ version: "0.5.7",
2418
2418
  description: "Command Code provider + usage dashboard for opencode: two providers (Anthropic wire for Claude, OpenAI wire for everything else) against api.commandcode.ai, live model list, plan gating, /cmd-usage. Requires the cmduse CLI for usage rendering.",
2419
2419
  type: "module",
2420
2420
  main: "dist/index.js",
@@ -3230,6 +3230,11 @@ var MPC_BIN_CANDIDATES = [
3230
3230
  join(homedir(), ".bun/bin/mpc"),
3231
3231
  "/opt/homebrew/bin/mpc"
3232
3232
  ];
3233
+ var OCUSE_BIN_CANDIDATES = [
3234
+ "ocuse",
3235
+ "/opt/homebrew/bin/ocuse",
3236
+ "/usr/local/bin/ocuse"
3237
+ ];
3233
3238
 
3234
3239
  // src/constants/cli.ts
3235
3240
  var DEFAULT_ARGS = ["-1", "--plain"];
@@ -3256,6 +3261,11 @@ function mpcCandidates(env = process.env) {
3256
3261
  return candidates.filter((bin) => Boolean(bin));
3257
3262
  }
3258
3263
  var MPC_CANDIDATES = mpcCandidates();
3264
+ function ocuseCandidates(env = process.env) {
3265
+ const candidates = [env.OCUSE_BIN, ...OCUSE_BIN_CANDIDATES];
3266
+ return candidates.filter((bin) => Boolean(bin));
3267
+ }
3268
+ var OCUSE_CANDIDATES = ocuseCandidates();
3259
3269
  function splitCliArgs(input) {
3260
3270
  const out = [];
3261
3271
  let cur = "";
@@ -3308,6 +3318,9 @@ function buildMpcArgs(arg) {
3308
3318
  return [...MPC_DEFAULT_ARGS];
3309
3319
  return tokens;
3310
3320
  }
3321
+ function ocuseArgs(arg) {
3322
+ return arg !== undefined && arg.trim() !== "" ? arg : "-1";
3323
+ }
3311
3324
  function stderrTail(text) {
3312
3325
  const lines = text.trimEnd().split(`
3313
3326
  `);
@@ -3318,36 +3331,53 @@ function runOnce(bin, args, opts, name = "cmduse") {
3318
3331
  return new Promise((resolve, reject) => {
3319
3332
  const child = spawn(bin, args, {
3320
3333
  env: { ...process.env, ...opts.env ?? {} },
3321
- stdio: ["ignore", "pipe", "pipe"]
3334
+ stdio: ["ignore", "pipe", "pipe"],
3335
+ ...opts.spawnOptions
3322
3336
  });
3323
3337
  const out = [];
3324
3338
  const err = [];
3339
+ let settled = false;
3340
+ const timer = opts.timeoutMs === undefined ? undefined : setTimeout(() => {
3341
+ if (settled)
3342
+ return;
3343
+ settled = true;
3344
+ child.kill();
3345
+ reject(new Error(`${name} ${args.join(" ")}: timed out after ${opts.timeoutMs}ms`));
3346
+ }, opts.timeoutMs);
3325
3347
  const onAbort = () => child.kill("SIGTERM");
3326
3348
  opts.signal?.addEventListener("abort", onAbort, { once: true });
3349
+ const settle = (fn) => {
3350
+ if (settled)
3351
+ return;
3352
+ settled = true;
3353
+ if (timer)
3354
+ clearTimeout(timer);
3355
+ opts.signal?.removeEventListener("abort", onAbort);
3356
+ fn();
3357
+ };
3327
3358
  child.stdout.on("data", (d) => out.push(d));
3328
3359
  child.stderr.on("data", (d) => err.push(d));
3329
- child.on("error", (e) => {
3330
- opts.signal?.removeEventListener("abort", onAbort);
3331
- reject(e);
3332
- });
3360
+ child.on("error", (e) => settle(() => reject(e)));
3333
3361
  child.on("close", (code, signal) => {
3334
- opts.signal?.removeEventListener("abort", onAbort);
3335
3362
  const stdout = Buffer.concat(out).toString("utf8");
3336
3363
  if (opts.signal?.aborted) {
3337
- reject(new Error(`${name} invocation aborted`));
3364
+ settle(() => reject(new Error(`${name} invocation aborted`)));
3338
3365
  } else if (code === 0) {
3339
- resolve(stdout);
3366
+ settle(() => resolve(stdout));
3340
3367
  } else {
3341
3368
  const why = signal ? `killed by ${signal}` : `exit ${code}`;
3342
3369
  const tail = stderrTail(Buffer.concat(err).toString("utf8"));
3343
- reject(new Error(`${name} ${args.join(" ")}: ${why}${tail ? `
3344
- ${tail}` : ""}`));
3370
+ settle(() => reject(new Error(`${name} ${args.join(" ")}: ${why}${tail ? `
3371
+ ${tail}` : ""}`)));
3345
3372
  }
3346
3373
  });
3347
3374
  });
3348
3375
  }
3376
+ function resolveArgs(arg, build) {
3377
+ return typeof arg === "string" ? build(arg) : [...arg];
3378
+ }
3349
3379
  async function runCmduse(arg, opts = {}) {
3350
- const args = buildCmduseArgs(arg);
3380
+ const args = resolveArgs(arg, buildCmduseArgs);
3351
3381
  const candidates = opts.candidates ?? CANDIDATES;
3352
3382
  let last;
3353
3383
  for (const bin of candidates) {
@@ -3362,7 +3392,7 @@ async function runCmduse(arg, opts = {}) {
3362
3392
  throw new Error(`cmduse binary not found (install: brew install JeffreyJYZ/tap/cmduse)${last instanceof Error && last.message !== last.name ? ` \u2014 ${last.message}` : ""}`, { cause: last });
3363
3393
  }
3364
3394
  async function runMpc(arg, opts = {}) {
3365
- const args = buildMpcArgs(arg);
3395
+ const args = resolveArgs(arg, buildMpcArgs);
3366
3396
  const candidates = opts.candidates ?? MPC_CANDIDATES;
3367
3397
  let last;
3368
3398
  for (const bin of candidates) {
@@ -3376,6 +3406,21 @@ async function runMpc(arg, opts = {}) {
3376
3406
  }
3377
3407
  throw new Error(`mpc binary not found (install: bun add -g @jeffreyjyz/mpc)${last instanceof Error && last.message !== last.name ? ` \u2014 ${last.message}` : ""}`, { cause: last });
3378
3408
  }
3409
+ async function runOcuse(arg, opts = {}) {
3410
+ const args = resolveArgs(arg, splitCliArgs);
3411
+ const candidates = opts.candidates ?? OCUSE_CANDIDATES;
3412
+ let last;
3413
+ for (const bin of candidates) {
3414
+ try {
3415
+ return await runOnce(bin, args, opts, "ocuse");
3416
+ } catch (e) {
3417
+ last = e;
3418
+ if (e?.code !== "ENOENT")
3419
+ throw e;
3420
+ }
3421
+ }
3422
+ throw new Error(`ocuse binary not found (install: brew install JeffreyJYZ/tap/cmduse)${last instanceof Error && last.message !== last.name ? ` \u2014 ${last.message}` : ""}`, { cause: last });
3423
+ }
3379
3424
 
3380
3425
  // src/constants/models.ts
3381
3426
  var MODEL_CAPABILITIES = {
@@ -3404,6 +3449,8 @@ var INTEGRATION_NAME = "Command Code";
3404
3449
  var TOOL_DESCRIPTION = "Fetch live Command Code plan/usage: plan name, monthly credits, 5-hour & weekly windows, billing-period summary. Pass arg='plans' for the plan comparison table only, or extra cmduse flags (e.g. '--tz +05:30 daily').";
3405
3450
  var MPC_TOOL_DESCRIPTION = "Per-model price comparison: what a model costs per request ($/1M input, output and cache rates) and per month (request allowance) on each Command Code (cc) and OpenCode Go (oc) plan side, plus ability, Tok/s, the cheaper-side verdict and running deals. Pass '--model <regex>' to filter rows, '--minimal' | '--medium' | '--detail' for table width, '--json' for the full structured rows, '--cc-plan' / '--oc-plan' to pick a plan, or '--usage' to project real usage.";
3406
3451
  var MPC_TOOL_ARG_DESCRIPTION = "Optional mpc flags: '--model <regex>' filters rows, '--minimal' | '--medium' | '--detail' set table width, '--json' for full structured rows, '--cc-plan'/'--oc-plan' pick a plan, '--usage' projects real usage";
3452
+ var OCUSE_TOOL_DESCRIPTION = "Fetch live OpenCode Go/Zen usage: the account's window meters, per-model spend against limits, 5-hour & weekly windows, billing-period summary. Defaults to the one-shot dashboard; pass extra ocuse flags for more (e.g. '--plan go-plus', 'daily', '--json').";
3453
+ var OCUSE_TOOL_ARG_DESCRIPTION = "Optional: extra ocuse flags (e.g. '--plan go-plus', 'daily', '--json'); empty means the one-shot dashboard";
3407
3454
  var LANES = [
3408
3455
  {
3409
3456
  id: "command-code-anthropic",
@@ -4941,6 +4988,26 @@ var commandCodeV2 = {
4941
4988
  };
4942
4989
  }
4943
4990
  });
4991
+ editor.add({
4992
+ name: "ocuse_usage",
4993
+ description: OCUSE_TOOL_DESCRIPTION,
4994
+ input: {
4995
+ type: "object",
4996
+ properties: {
4997
+ arg: {
4998
+ type: "string",
4999
+ description: OCUSE_TOOL_ARG_DESCRIPTION
5000
+ }
5001
+ },
5002
+ additionalProperties: false
5003
+ },
5004
+ async execute(input) {
5005
+ const arg = typeof input?.arg === "string" ? input.arg : undefined;
5006
+ return {
5007
+ content: await runOcuse(ocuseArgs(arg))
5008
+ };
5009
+ }
5010
+ });
4944
5011
  });
4945
5012
  let hasConnection = false;
4946
5013
  let connectionMs = 0;
@@ -5025,6 +5092,7 @@ var commandCodeV2 = {
5025
5092
 
5026
5093
  // src/index.ts
5027
5094
  var PROVIDER_NPM = `${package_default.name}@${package_default.version}`;
5095
+ var [ANTHROPIC, OPENAI] = LANES;
5028
5096
  function tool(input) {
5029
5097
  return input;
5030
5098
  }
@@ -5071,7 +5139,7 @@ function toModelDefs(models, providerID, npm, interleaved = false) {
5071
5139
  var CommandCodePlugin = async (_input) => {
5072
5140
  return {
5073
5141
  auth: {
5074
- provider: "command-code-anthropic",
5142
+ provider: ANTHROPIC.id,
5075
5143
  loader: async (getAuth) => {
5076
5144
  const auth = await getAuth();
5077
5145
  if (auth?.type !== "api") {
@@ -5101,7 +5169,7 @@ var CommandCodePlugin = async (_input) => {
5101
5169
  return {
5102
5170
  type: "success",
5103
5171
  key,
5104
- provider: "command-code-anthropic"
5172
+ provider: ANTHROPIC.id
5105
5173
  };
5106
5174
  } catch {
5107
5175
  return { type: "failed" };
@@ -5125,19 +5193,19 @@ var CommandCodePlugin = async (_input) => {
5125
5193
  console.warn("[command-code] model list unavailable:", e);
5126
5194
  }
5127
5195
  }
5128
- const claudeDefs = split ? toModelDefs(split.claude, "command-code-anthropic", PROVIDER_NPM) : {};
5129
- const openDefs = split ? toModelDefs(split.open, "command-code-openai", PROVIDER_NPM, {
5196
+ const claudeDefs = split ? toModelDefs(split.claude, ANTHROPIC.id, PROVIDER_NPM) : {};
5197
+ const openDefs = split ? toModelDefs(split.open, OPENAI.id, PROVIDER_NPM, {
5130
5198
  field: "reasoning_content"
5131
5199
  }) : {};
5132
- const userAnthropic = existing("command-code-anthropic");
5133
- cfg.provider["command-code-anthropic"] = {
5200
+ const userAnthropic = existing(ANTHROPIC.id);
5201
+ cfg.provider[ANTHROPIC.id] = {
5134
5202
  npm: userAnthropic.npm ?? PROVIDER_NPM,
5135
5203
  name: userAnthropic.name ?? "Command Code (Anthropic)",
5136
5204
  options: { baseURL: PROVIDER_BASE, ...userAnthropic.options },
5137
5205
  models: { ...claudeDefs, ...userAnthropic.models }
5138
5206
  };
5139
- const userOpenai = existing("command-code-openai");
5140
- cfg.provider["command-code-openai"] = {
5207
+ const userOpenai = existing(OPENAI.id);
5208
+ cfg.provider[OPENAI.id] = {
5141
5209
  npm: userOpenai.npm ?? PROVIDER_NPM,
5142
5210
  name: userOpenai.name ?? "Command Code (OpenAI)",
5143
5211
  options: {
@@ -5152,16 +5220,20 @@ var CommandCodePlugin = async (_input) => {
5152
5220
  description: "Show Command Code plan, credits, and usage windows",
5153
5221
  template: "Call the cmd_usage tool and present its markdown output verbatim to the user. If the tool errors, tell the user to run /connect (Command Code (Anthropic)) and retry. $ARGUMENTS"
5154
5222
  };
5223
+ cfg.command["ocuse-usage"] = {
5224
+ description: "Show OpenCode Go/Zen usage: account meters and per-model spend",
5225
+ template: "Call the ocuse_usage tool and present its markdown output verbatim to the user. If the tool errors, the Go key is missing or has no subscription \u2014 usage still shows where the store has it. $ARGUMENTS"
5226
+ };
5155
5227
  },
5156
5228
  provider: {
5157
- id: "command-code-anthropic",
5229
+ id: ANTHROPIC.id,
5158
5230
  models: async (_provider, ctx) => {
5159
5231
  const key = await resolveKey(async () => {
5160
5232
  const a = ctx.auth;
5161
5233
  return a?.type === "api" && a.key ? { key: a.key } : undefined;
5162
5234
  });
5163
5235
  const split = await loadModels(key);
5164
- return toModelDefs(split.claude, "command-code-anthropic", PROVIDER_NPM);
5236
+ return toModelDefs(split.claude, ANTHROPIC.id, PROVIDER_NPM);
5165
5237
  }
5166
5238
  },
5167
5239
  tool: {
@@ -5188,6 +5260,15 @@ var CommandCodePlugin = async (_input) => {
5188
5260
  env: { CMD_API_KEY: key }
5189
5261
  });
5190
5262
  }
5263
+ }),
5264
+ ocuse_usage: tool({
5265
+ description: OCUSE_TOOL_DESCRIPTION,
5266
+ args: {
5267
+ arg: string2().optional().describe(OCUSE_TOOL_ARG_DESCRIPTION)
5268
+ },
5269
+ async execute(args) {
5270
+ return runOcuse(ocuseArgs(args.arg));
5271
+ }
5191
5272
  })
5192
5273
  }
5193
5274
  };
@@ -33,8 +33,9 @@ export declare function loadMeta(plan?: OcPlan): Promise<Map<string, ModelMeta>>
33
33
  * unreachable (see `requireSnapshot`). */
34
34
  export declare function loadUsage(): Promise<Usage>;
35
35
  /**
36
- * One OpenCode Go / Zen snapshot from ocuse, which reads opencode's own store
37
- * (those providers have no usage API). Throws when ocuse is missing or fails,
38
- * and the caller then omits the panel.
36
+ * One OpenCode Go / Zen snapshot from ocuse: opencode's own store for the
37
+ * per-model totals, plus OpenCode Go's usage endpoint for the account meters
38
+ * when the account has a Go key. Throws when ocuse is missing or fails, and the
39
+ * caller then omits the panel.
39
40
  */
40
41
  export declare function loadZen(plan?: OcPlan): Promise<ZenUsage>;
@@ -87,6 +87,15 @@ export interface WindowUsage {
87
87
  resetAt?: number;
88
88
  /** The account API's own over-cap flag (cmduse passes it through). */
89
89
  exceeded?: boolean;
90
+ /**
91
+ * The account's own share of the limit, when its endpoint answered. That
92
+ * figure is the one the server gates on and prints on its console, so it —
93
+ * not `used / cap`, which this machine computes and which sits below the
94
+ * server's meter — sets the row's tone and reads as the share. The money
95
+ * pair stays this store's own, which is why the two need not divide to each
96
+ * other.
97
+ */
98
+ sharePct?: number;
90
99
  }
91
100
  /** One model's usage for the account's current billing period. */
92
101
  export interface ModelUsage {
@@ -140,7 +149,13 @@ export declare function until(epoch: number | undefined, now?: number): string;
140
149
  * the OpenCode Go/Zen side adds the window's request count as `extra`, so
141
150
  * `LIMIT EXCEEDED`, the tone, `cap in …`, `elapsed` and `resets` all have one
142
151
  * implementation. */
143
- export declare function windowRows(label: string, w: WindowUsage | undefined, now: number, durSecs: number, extra?: string): SidebarRow[];
152
+ export declare function windowRows(label: string, w: WindowUsage | undefined, now: number, durSecs: number, extra?: string,
153
+ /** How the use line reads. The default is money (`$0.55/$14 (3.9%)`); the
154
+ * account's own meters publish their own share of the limit, which the
155
+ * formatter receives as `share` — the money pair beside it stays local, so
156
+ * the tone, `LIMIT EXCEEDED`, `cap in …`, `elapsed` and `resets` stay this
157
+ * one implementation for both panels. */
158
+ use?: (used: number, cap: number, share: number) => string): SidebarRow[];
144
159
  /**
145
160
  * The two store-backed rows for the active model — its requests under `Usage
146
161
  * (this model)`, then this conversation's own totals under `Session`. They need
@@ -27,9 +27,9 @@ export declare function loadModelUsage(modelID: string, sinceMs: number, path?:
27
27
  /**
28
28
  * The oldest in-window assistant turn for one model, as epoch ms — the start of
29
29
  * the rolling window the Go/Zen panel needs. A rolling 5-hour/weekly window has
30
- * no service-reported reset (Go has no usage API), but its clock does have a
31
- * real start: the oldest request still inside it ages out at `start + window`,
32
- * which is that window's honest reset. Null when the model has no turn in the
30
+ * no reset recorded locally (the store carries none for these providers), but
31
+ * its clock does have a real start: the oldest request still inside it ages out
32
+ * at `start + window`, which is that window's honest reset. Null when the model has no turn in the
33
33
  * window, or the store is missing/unreadable, so callers then omit the
34
34
  * continuation exactly as before.
35
35
  *
@@ -13,6 +13,25 @@ export interface ZenModel {
13
13
  weekly: ZenTotals;
14
14
  month: ZenTotals;
15
15
  }
16
+ /** One of the account's own meters (`ocuse`'s `account` block). */
17
+ export interface ZenMeter {
18
+ /** The server's own word; anything but "ok" reads as over the limit. */
19
+ status: string;
20
+ /** Percent of the window's limit already used — the console's number. */
21
+ percent: number;
22
+ /** Reset instant, epoch ms. */
23
+ resetsAt?: number;
24
+ }
25
+ /** The account's three meters, under the endpoint's own names: `rolling` is the
26
+ * 5-hour window. Only present when ocuse reached OpenCode Go's usage endpoint,
27
+ * and then these — not the per-model figures — are what opencode.ai/console
28
+ * shows, because only the server knows its own window anchors and price
29
+ * basis. */
30
+ export interface ZenAccount {
31
+ rolling: ZenMeter;
32
+ weekly: ZenMeter;
33
+ monthly: ZenMeter;
34
+ }
16
35
  export interface ZenUsage {
17
36
  /**
18
37
  * Billing period start, epoch ms: `periodStart` on current ocuse, or
@@ -29,6 +48,15 @@ export interface ZenUsage {
29
48
  weekly: ZenTotals;
30
49
  month: ZenTotals;
31
50
  };
51
+ /** The account's own meters, when ocuse could ask for them. */
52
+ account?: ZenAccount;
53
+ /**
54
+ * Why `account` is absent, when ocuse could name it: `HTTP 401
55
+ * api_key_expired`, `no Go key`, a transport fault (`ocuse` 0.7.14+). The
56
+ * panel shows it, because "estimated locally" alone cannot tell a missing key
57
+ * from an expired one — and only the expired one is something to act on.
58
+ */
59
+ accountError?: string;
32
60
  /** Keyed by canonical model key, the same normalisation the CC side matches on. */
33
61
  models: Map<string, ZenModel>;
34
62
  }
package/dist/tui.js CHANGED
@@ -14,6 +14,7 @@ var OCUSE_BIN_CANDIDATES = ["ocuse", "/opt/homebrew/bin/ocuse", "/usr/local/bin/
14
14
 
15
15
  // src/constants/cli.ts
16
16
  var DEFAULT_ARGS = ["-1", "--plain"];
17
+ var MPC_DEFAULT_ARGS = ["--minimal"];
17
18
  var SUBCOMMANDS = new Set(["plans", "models", "daily", "hourly", "model", "session", "statusline", "config"]);
18
19
  var SPAWN_OPTIONS = {
19
20
  stdio: ["ignore", "pipe", "pipe"],
@@ -31,6 +32,11 @@ function mpcCandidates(env = process.env) {
31
32
  return candidates.filter((bin) => Boolean(bin));
32
33
  }
33
34
  var MPC_CANDIDATES = mpcCandidates();
35
+ function ocuseCandidates(env = process.env) {
36
+ const candidates = [env.OCUSE_BIN, ...OCUSE_BIN_CANDIDATES];
37
+ return candidates.filter((bin) => Boolean(bin));
38
+ }
39
+ var OCUSE_CANDIDATES = ocuseCandidates();
34
40
  function splitCliArgs(input) {
35
41
  const out = [];
36
42
  let cur = "";
@@ -77,6 +83,15 @@ function buildCmduseArgs(arg) {
77
83
  return tokens;
78
84
  return [...DEFAULT_ARGS, ...tokens];
79
85
  }
86
+ function buildMpcArgs(arg) {
87
+ const tokens = splitCliArgs(arg);
88
+ if (!tokens.length)
89
+ return [...MPC_DEFAULT_ARGS];
90
+ return tokens;
91
+ }
92
+ function ocuseArgs(arg) {
93
+ return arg !== undefined && arg.trim() !== "" ? arg : "-1";
94
+ }
80
95
  function stderrTail(text) {
81
96
  const lines = text.trimEnd().split(`
82
97
  `);
@@ -90,38 +105,55 @@ function runOnce(bin, args, opts, name = "cmduse") {
90
105
  ...process.env,
91
106
  ...opts.env ?? {}
92
107
  },
93
- stdio: ["ignore", "pipe", "pipe"]
108
+ stdio: ["ignore", "pipe", "pipe"],
109
+ ...opts.spawnOptions
94
110
  });
95
111
  const out = [];
96
112
  const err = [];
113
+ let settled = false;
114
+ const timer = opts.timeoutMs === undefined ? undefined : setTimeout(() => {
115
+ if (settled)
116
+ return;
117
+ settled = true;
118
+ child.kill();
119
+ reject(new Error(`${name} ${args.join(" ")}: timed out after ${opts.timeoutMs}ms`));
120
+ }, opts.timeoutMs);
97
121
  const onAbort = () => child.kill("SIGTERM");
98
122
  opts.signal?.addEventListener("abort", onAbort, {
99
123
  once: true
100
124
  });
125
+ const settle = (fn) => {
126
+ if (settled)
127
+ return;
128
+ settled = true;
129
+ if (timer)
130
+ clearTimeout(timer);
131
+ opts.signal?.removeEventListener("abort", onAbort);
132
+ fn();
133
+ };
101
134
  child.stdout.on("data", (d) => out.push(d));
102
135
  child.stderr.on("data", (d) => err.push(d));
103
- child.on("error", (e) => {
104
- opts.signal?.removeEventListener("abort", onAbort);
105
- reject(e);
106
- });
136
+ child.on("error", (e) => settle(() => reject(e)));
107
137
  child.on("close", (code, signal) => {
108
- opts.signal?.removeEventListener("abort", onAbort);
109
138
  const stdout = Buffer.concat(out).toString("utf8");
110
139
  if (opts.signal?.aborted) {
111
- reject(new Error(`${name} invocation aborted`));
140
+ settle(() => reject(new Error(`${name} invocation aborted`)));
112
141
  } else if (code === 0) {
113
- resolve(stdout);
142
+ settle(() => resolve(stdout));
114
143
  } else {
115
144
  const why = signal ? `killed by ${signal}` : `exit ${code}`;
116
145
  const tail = stderrTail(Buffer.concat(err).toString("utf8"));
117
- reject(new Error(`${name} ${args.join(" ")}: ${why}${tail ? `
118
- ${tail}` : ""}`));
146
+ settle(() => reject(new Error(`${name} ${args.join(" ")}: ${why}${tail ? `
147
+ ${tail}` : ""}`)));
119
148
  }
120
149
  });
121
150
  });
122
151
  }
152
+ function resolveArgs(arg, build) {
153
+ return typeof arg === "string" ? build(arg) : [...arg];
154
+ }
123
155
  async function runCmduse(arg, opts = {}) {
124
- const args = buildCmduseArgs(arg);
156
+ const args = resolveArgs(arg, buildCmduseArgs);
125
157
  const candidates = opts.candidates ?? CANDIDATES;
126
158
  let last;
127
159
  for (const bin of candidates) {
@@ -137,6 +169,40 @@ async function runCmduse(arg, opts = {}) {
137
169
  cause: last
138
170
  });
139
171
  }
172
+ async function runMpc(arg, opts = {}) {
173
+ const args = resolveArgs(arg, buildMpcArgs);
174
+ const candidates = opts.candidates ?? MPC_CANDIDATES;
175
+ let last;
176
+ for (const bin of candidates) {
177
+ try {
178
+ return await runOnce(bin, args, opts, "mpc");
179
+ } catch (e) {
180
+ last = e;
181
+ if (e?.code !== "ENOENT")
182
+ throw e;
183
+ }
184
+ }
185
+ throw new Error(`mpc binary not found (install: bun add -g @jeffreyjyz/mpc)${last instanceof Error && last.message !== last.name ? ` \u2014 ${last.message}` : ""}`, {
186
+ cause: last
187
+ });
188
+ }
189
+ async function runOcuse(arg, opts = {}) {
190
+ const args = resolveArgs(arg, splitCliArgs);
191
+ const candidates = opts.candidates ?? OCUSE_CANDIDATES;
192
+ let last;
193
+ for (const bin of candidates) {
194
+ try {
195
+ return await runOnce(bin, args, opts, "ocuse");
196
+ } catch (e) {
197
+ last = e;
198
+ if (e?.code !== "ENOENT")
199
+ throw e;
200
+ }
201
+ }
202
+ throw new Error(`ocuse binary not found (install: brew install JeffreyJYZ/tap/cmduse)${last instanceof Error && last.message !== last.name ? ` \u2014 ${last.message}` : ""}`, {
203
+ cause: last
204
+ });
205
+ }
140
206
 
141
207
  // src/constants/providers.ts
142
208
  var PROVIDER_ID_PREFIX = "command-code";
@@ -144,6 +210,8 @@ var GO_PROVIDER_ID = "opencode-go";
144
210
  var ZEN_PROVIDER_ID = "opencode";
145
211
  var ZEN_PROVIDER_PREFIX = "opencode-zen";
146
212
  var TUI_ID = "command-code.tui";
213
+ var CMD_USAGE_COMMAND_ID = "command-code.cmd-usage";
214
+ var OCUSE_USAGE_COMMAND_ID = "command-code.ocuse-usage";
147
215
 
148
216
  // src/sidebar/panel.tsx
149
217
  import { createComponent as _$createComponent } from "@opentui/solid";
@@ -621,18 +689,19 @@ function until(epoch, now = Date.now()) {
621
689
  const days = Math.floor(hours / 24);
622
690
  return hours % 24 ? `${days}d ${hours % 24}h` : `${days}d`;
623
691
  }
624
- function windowRows(label, w, now, durSecs, extra) {
692
+ function windowRows(label, w, now, durSecs, extra, use) {
625
693
  if (!w || typeof w.cap !== "number" || w.cap <= 0)
626
694
  return [];
627
695
  const used = typeof w.used === "number" ? w.used : 0;
628
696
  const pct = used / w.cap * 100;
697
+ const share = typeof w.sharePct === "number" ? w.sharePct : pct;
629
698
  const nowSecs = Math.floor(now / 1000);
630
699
  const elapsed = elapsedLabel(w.resetAt, durSecs, nowSecs);
631
700
  const eta = paceEtaSecs(w.resetAt, durSecs, used, w.cap, nowSecs);
632
701
  const reset = until(w.resetAt, now);
633
702
  const lead = w.exceeded ? "LIMIT EXCEEDED" : eta === undefined ? elapsed === undefined ? "" : `${elapsed} elapsed` : `cap in ${until(now + eta * 1000, now)}`;
634
703
  const detail = [lead, reset ? `resets ${reset}` : ""].filter(Boolean).join(" \xB7 ");
635
- return [[label, `${money(used)}/${money(w.cap)} (${pct.toFixed(1)}%)`, w.exceeded ? "crit" : pctTone(pct)], ...detail ? [["", detail, "muted"]] : [], ...extra ? [["", extra, "muted"]] : []];
704
+ return [[label, use ? use(used, w.cap, share) : `${money(used)}/${money(w.cap)} (${pct.toFixed(1)}%)`, w.exceeded ? "crit" : pctTone(share)], ...detail ? [["", detail, "muted"]] : [], ...extra ? [["", extra, "muted"]] : []];
636
705
  }
637
706
  function localUsageRows(usage, session) {
638
707
  const rows = [];
@@ -2027,7 +2096,6 @@ function minPlan(id) {
2027
2096
  }
2028
2097
 
2029
2098
  // src/sidebar/data.ts
2030
- import { spawn as spawn2 } from "child_process";
2031
2099
  import { mkdir, readFile, writeFile } from "fs/promises";
2032
2100
  import { homedir as homedir3 } from "os";
2033
2101
  import { dirname, join as join3 } from "path";
@@ -2042,6 +2110,14 @@ function totals(value) {
2042
2110
  tokens: num("tokens")
2043
2111
  };
2044
2112
  }
2113
+ function meter(value) {
2114
+ const v = value ?? {};
2115
+ return {
2116
+ status: typeof v.status === "string" ? v.status : "ok",
2117
+ percent: typeof v.percent === "number" ? v.percent : 0,
2118
+ resetsAt: typeof v.resetsAt === "number" ? v.resetsAt : undefined
2119
+ };
2120
+ }
2045
2121
  function parseZenJson(text) {
2046
2122
  let body;
2047
2123
  try {
@@ -2067,6 +2143,12 @@ function parseZenJson(text) {
2067
2143
  periodStart: typeof body.periodStartAt === "number" ? body.periodStartAt : typeof body.periodStart === "number" ? body.periodStart : undefined,
2068
2144
  periodEndAt: typeof body.periodEndAt === "number" ? body.periodEndAt : undefined,
2069
2145
  source: typeof body.source === "string" ? body.source : undefined,
2146
+ account: body.account ? {
2147
+ rolling: meter(body.account.rolling),
2148
+ weekly: meter(body.account.weekly),
2149
+ monthly: meter(body.account.monthly)
2150
+ } : undefined,
2151
+ accountError: typeof body.accountError === "string" && body.accountError ? body.accountError : undefined,
2070
2152
  totals: {
2071
2153
  fiveHour: totals(top.fiveHour),
2072
2154
  weekly: totals(top.weekly),
@@ -2107,19 +2189,41 @@ function zenWindow(label, spent, limit, share, now, durSecs, resetAt) {
2107
2189
  exceeded: spent.cost >= cap
2108
2190
  }, now, durSecs, spent.requests > 0 ? `${count(spent.requests)} req` : undefined);
2109
2191
  }
2192
+ function zenMeterRows(label, meter, now, durSecs, local) {
2193
+ return windowRows(label, {
2194
+ cap: local?.cap ?? 100,
2195
+ used: local?.spent ?? meter.percent,
2196
+ resetAt: meter.resetsAt,
2197
+ sharePct: meter.percent,
2198
+ exceeded: meter.percent >= 100 || meter.status !== "ok"
2199
+ }, now, durSecs, undefined, (used, cap, share) => local ? `${money(used)}/${money(cap)} (${share.toFixed(1)}% of limit)` : `${share.toFixed(1)}% of limit`);
2200
+ }
2110
2201
  function zenRows(usage, meta, modelUsage, session, fallbackName, windows) {
2111
2202
  if (!usage)
2112
2203
  return [];
2113
2204
  const now = Date.now();
2114
2205
  const oc = meta?.oc;
2115
2206
  const rows = [["Plan", `${oc?.plan ?? "Go"} \xB7 limits per model`, "base"]];
2207
+ if (usage.accountError) {
2208
+ rows.push(["", clip(`usage API: ${usage.accountError}`, valueWidth("")), "warn"]);
2209
+ }
2116
2210
  const mine = usage.models.get(modelKey(fallbackName ?? "")) ?? (meta ? usage.models.get(modelKey(meta.name)) : undefined);
2117
2211
  const limit = oc?.allowance ?? mine?.limit;
2118
2212
  const cap = mine ? limit : undefined;
2119
- rows.push(...zenWindow("5-hour", mine?.fiveHour ?? usage.totals.fiveHour, cap, GO_WINDOW_SHARE.fiveHour, now, FIVE_HOUR_SECS, rollingReset(windows?.fiveHour, FIVE_HOUR_SECS)));
2120
- rows.push(...zenWindow("Weekly", mine?.weekly ?? usage.totals.weekly, cap, GO_WINDOW_SHARE.weekly, now, WEEKLY_SECS, rollingReset(windows?.weekly, WEEKLY_SECS)));
2213
+ const windowMoney = (spent, share) => mine && typeof limit === "number" && limit > 0 ? {
2214
+ spent: spent.cost,
2215
+ cap: limit * share
2216
+ } : undefined;
2121
2217
  const period = monthPeriod(usage);
2122
- rows.push(...zenWindow("Monthly", mine?.month ?? usage.totals.month, cap, GO_WINDOW_SHARE.month, now, period?.durSecs ?? 0, period?.resetAt));
2218
+ if (usage.account) {
2219
+ rows.push(...zenMeterRows("5-hour", usage.account.rolling, now, FIVE_HOUR_SECS, mine ? windowMoney(mine.fiveHour, GO_WINDOW_SHARE.fiveHour) : undefined));
2220
+ rows.push(...zenMeterRows("Weekly", usage.account.weekly, now, WEEKLY_SECS, mine ? windowMoney(mine.weekly, GO_WINDOW_SHARE.weekly) : undefined));
2221
+ rows.push(...zenMeterRows("Monthly", usage.account.monthly, now, period?.durSecs ?? 0, mine ? windowMoney(mine.month, GO_WINDOW_SHARE.month) : undefined));
2222
+ } else {
2223
+ rows.push(...zenWindow("5-hour", mine?.fiveHour ?? usage.totals.fiveHour, cap, GO_WINDOW_SHARE.fiveHour, now, FIVE_HOUR_SECS, rollingReset(windows?.fiveHour, FIVE_HOUR_SECS)));
2224
+ rows.push(...zenWindow("Weekly", mine?.weekly ?? usage.totals.weekly, cap, GO_WINDOW_SHARE.weekly, now, WEEKLY_SECS, rollingReset(windows?.weekly, WEEKLY_SECS)));
2225
+ rows.push(...zenWindow("Monthly", mine?.month ?? usage.totals.month, cap, GO_WINDOW_SHARE.month, now, period?.durSecs ?? 0, period?.resetAt));
2226
+ }
2123
2227
  rows.push(["Period", `${count(usage.totals.month.requests)} requests \xB7 ${money(usage.totals.month.cost)}`, "base"]);
2124
2228
  rows.push(separator());
2125
2229
  const name = meta?.name ?? fallbackName;
@@ -2149,48 +2253,10 @@ function zenRows(usage, meta, modelUsage, session, fallbackName, windows) {
2149
2253
  }
2150
2254
 
2151
2255
  // src/sidebar/data.ts
2152
- var CMDUSE = cmduseCandidates();
2153
- var MPC = MPC_BIN_CANDIDATES;
2154
- var OCUSE = [process.env.OCUSE_BIN, ...OCUSE_BIN_CANDIDATES].filter((bin) => Boolean(bin));
2155
- function run(bin, args) {
2156
- return new Promise((resolve, reject) => {
2157
- const child = spawn2(bin, args, SPAWN_OPTIONS);
2158
- const out = [];
2159
- const err = [];
2160
- let settled = false;
2161
- const timer = setTimeout(() => {
2162
- if (settled)
2163
- return;
2164
- settled = true;
2165
- child.kill();
2166
- reject(new Error(`${bin} ${args.join(" ")}: timed out after ${CHILD_TIMEOUT_MS}ms`));
2167
- }, CHILD_TIMEOUT_MS);
2168
- const done = (fn) => {
2169
- if (settled)
2170
- return;
2171
- settled = true;
2172
- clearTimeout(timer);
2173
- fn();
2174
- };
2175
- child.stdout?.on("data", (d) => out.push(d));
2176
- child.stderr?.on("data", (d) => err.push(d));
2177
- child.on("error", (e) => done(() => reject(e)));
2178
- child.on("close", (code) => done(() => code === 0 ? resolve(Buffer.concat(out).toString("utf8")) : reject(new Error(`${bin} ${args.join(" ")}: exit ${code} ${Buffer.concat(err).toString("utf8").slice(-200)}`))));
2179
- });
2180
- }
2181
- async function runFirst(candidates, args) {
2182
- let last;
2183
- for (const bin of candidates) {
2184
- try {
2185
- return await run(bin, args);
2186
- } catch (e) {
2187
- last = e;
2188
- if (e?.code !== "ENOENT")
2189
- throw e;
2190
- }
2191
- }
2192
- throw last instanceof Error ? last : new Error(`${candidates[0]} not found`);
2193
- }
2256
+ var SIDEBAR_OPTS = {
2257
+ spawnOptions: SPAWN_OPTIONS,
2258
+ timeoutMs: CHILD_TIMEOUT_MS
2259
+ };
2194
2260
  function parseUsageJson(text) {
2195
2261
  const raw = JSON.parse(text);
2196
2262
  const win = (v) => {
@@ -2276,7 +2342,7 @@ async function loadMeta(plan = OC_PLAN_DEFAULT) {
2276
2342
  return new Map(cached.rows ?? []);
2277
2343
  }
2278
2344
  } catch {}
2279
- const meta = parseMpcJson(await runFirst(MPC, metaArgs(plan)));
2345
+ const meta = parseMpcJson(await runMpc(metaArgs(plan), SIDEBAR_OPTS));
2280
2346
  try {
2281
2347
  const path = cachePath(plan);
2282
2348
  await mkdir(dirname(path), {
@@ -2290,10 +2356,10 @@ async function loadMeta(plan = OC_PLAN_DEFAULT) {
2290
2356
  return meta;
2291
2357
  }
2292
2358
  async function loadUsage() {
2293
- return requireSnapshot(parseUsageJson(await runFirst(CMDUSE, ["-1", "--json", "--plain"])));
2359
+ return requireSnapshot(parseUsageJson(await runCmduse(["-1", "--json", "--plain"], SIDEBAR_OPTS)));
2294
2360
  }
2295
2361
  async function loadZen(plan = OC_PLAN_DEFAULT) {
2296
- return parseZenJson(await runFirst(OCUSE, zenArgs(plan)));
2362
+ return parseZenJson(await runOcuse(zenArgs(plan), SIDEBAR_OPTS));
2297
2363
  }
2298
2364
 
2299
2365
  // src/sidebar/usageDb.ts
@@ -2697,7 +2763,7 @@ var commandCodeTui = {
2697
2763
  mode: "global",
2698
2764
  priority: 10,
2699
2765
  commands: [{
2700
- id: "command-code.cmd-usage",
2766
+ id: CMD_USAGE_COMMAND_ID,
2701
2767
  title: "Command Code usage",
2702
2768
  group: "Command Code",
2703
2769
  slash: {
@@ -2723,8 +2789,35 @@ var commandCodeTui = {
2723
2789
  message: text.trimEnd()
2724
2790
  });
2725
2791
  }
2792
+ }, {
2793
+ id: OCUSE_USAGE_COMMAND_ID,
2794
+ title: "OpenCode Go/Zen usage",
2795
+ group: "Command Code",
2796
+ slash: {
2797
+ name: "ocuse-usage",
2798
+ arguments: true
2799
+ },
2800
+ enabled: () => true,
2801
+ suggested: true,
2802
+ run: async (input) => {
2803
+ let text;
2804
+ try {
2805
+ text = await runOcuse(ocuseArgs(input));
2806
+ } catch (error) {
2807
+ ctx.ui.toast.show({
2808
+ title: "ocuse-usage failed",
2809
+ message: error instanceof Error ? error.message : String(error),
2810
+ variant: "error"
2811
+ });
2812
+ return;
2813
+ }
2814
+ await ctx.ui.dialog.alert({
2815
+ title: "OpenCode Go/Zen usage",
2816
+ message: text.trimEnd()
2817
+ });
2818
+ }
2726
2819
  }],
2727
- bindings: ["command-code.cmd-usage"]
2820
+ bindings: [CMD_USAGE_COMMAND_ID, OCUSE_USAGE_COMMAND_ID]
2728
2821
  }));
2729
2822
  }
2730
2823
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeffreyjyz/opencode-command-code",
3
- "version": "0.5.3",
3
+ "version": "0.5.7",
4
4
  "description": "Command Code provider + usage dashboard for opencode: two providers (Anthropic wire for Claude, OpenAI wire for everything else) against api.commandcode.ai, live model list, plan gating, /cmd-usage. Requires the cmduse CLI for usage rendering.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",