@jeffreyjyz/opencode-command-code 0.5.1 → 0.5.4

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. */
@@ -24,14 +28,27 @@ export type RunOptions = {
24
28
  env?: Record<string, string>;
25
29
  /** Overridable for tests. */
26
30
  candidates?: readonly string[];
31
+ /** Extra spawn options. Excludes `stdio`: the callbacks below assume stdout
32
+ * and stderr are piped (the default here), so callers may only add fields
33
+ * that leave that contract intact — the sidebar passes `detached` (see
34
+ * constants/cli.ts; its own stdio equals the default). */
35
+ spawnOptions?: Omit<NonNullable<Parameters<typeof spawn>[2]>, "stdio">;
36
+ /** Kill the child after this many ms. The sidebar bounds each poll; the
37
+ * tool spawns leave it unset. */
38
+ timeoutMs?: number;
27
39
  };
28
40
  /** Run cmduse and return its plain-text stdout. Tries PATH, then common
29
41
  * homebrew locations; every candidate failing with ENOENT yields a single
30
42
  * install-hint error. */
31
- export declare function runCmduse(arg: string, opts?: RunOptions): Promise<string>;
43
+ export declare function runCmduse(arg: string | readonly string[], opts?: RunOptions): Promise<string>;
32
44
  /** Run mpc and return its plain-text stdout (the per-model price
33
45
  * comparison). Same contract as runCmduse: `MPC_BIN` wins, then the
34
46
  * fallback paths; every candidate failing with ENOENT yields a
35
47
  * single install-hint error, and any other failure (a non-zero exit
36
48
  * included) surfaces immediately with mpc's last stderr lines. */
37
- export declare function runMpc(arg: string, opts?: RunOptions): Promise<string>;
49
+ export declare function runMpc(arg: string | readonly string[], opts?: RunOptions): Promise<string>;
50
+ /** Run ocuse and return its plain-text stdout (OpenCode Go/Zen usage). Same
51
+ * contract as runCmduse; ocuse ships in the cmd-usage crate, so its install
52
+ * hint is the same brew formula. A string arg is shell-split, an array used
53
+ * as-is. */
54
+ export declare function runOcuse(arg: string | readonly string[], opts?: RunOptions): Promise<string>;
@@ -13,6 +13,8 @@ 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";
16
18
  /** The cmd_usage tool description, shared by the v1 and v2 halves. */
17
19
  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
20
  /** The mpc_prices tool description, shared by the v1 and v2 halves. */
@@ -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.1",
2417
+ version: "0.5.4",
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 = "";
@@ -3318,36 +3328,53 @@ function runOnce(bin, args, opts, name = "cmduse") {
3318
3328
  return new Promise((resolve, reject) => {
3319
3329
  const child = spawn(bin, args, {
3320
3330
  env: { ...process.env, ...opts.env ?? {} },
3321
- stdio: ["ignore", "pipe", "pipe"]
3331
+ stdio: ["ignore", "pipe", "pipe"],
3332
+ ...opts.spawnOptions
3322
3333
  });
3323
3334
  const out = [];
3324
3335
  const err = [];
3336
+ let settled = false;
3337
+ const timer = opts.timeoutMs === undefined ? undefined : setTimeout(() => {
3338
+ if (settled)
3339
+ return;
3340
+ settled = true;
3341
+ child.kill();
3342
+ reject(new Error(`${name} ${args.join(" ")}: timed out after ${opts.timeoutMs}ms`));
3343
+ }, opts.timeoutMs);
3325
3344
  const onAbort = () => child.kill("SIGTERM");
3326
3345
  opts.signal?.addEventListener("abort", onAbort, { once: true });
3346
+ const settle = (fn) => {
3347
+ if (settled)
3348
+ return;
3349
+ settled = true;
3350
+ if (timer)
3351
+ clearTimeout(timer);
3352
+ opts.signal?.removeEventListener("abort", onAbort);
3353
+ fn();
3354
+ };
3327
3355
  child.stdout.on("data", (d) => out.push(d));
3328
3356
  child.stderr.on("data", (d) => err.push(d));
3329
- child.on("error", (e) => {
3330
- opts.signal?.removeEventListener("abort", onAbort);
3331
- reject(e);
3332
- });
3357
+ child.on("error", (e) => settle(() => reject(e)));
3333
3358
  child.on("close", (code, signal) => {
3334
- opts.signal?.removeEventListener("abort", onAbort);
3335
3359
  const stdout = Buffer.concat(out).toString("utf8");
3336
3360
  if (opts.signal?.aborted) {
3337
- reject(new Error(`${name} invocation aborted`));
3361
+ settle(() => reject(new Error(`${name} invocation aborted`)));
3338
3362
  } else if (code === 0) {
3339
- resolve(stdout);
3363
+ settle(() => resolve(stdout));
3340
3364
  } else {
3341
3365
  const why = signal ? `killed by ${signal}` : `exit ${code}`;
3342
3366
  const tail = stderrTail(Buffer.concat(err).toString("utf8"));
3343
- reject(new Error(`${name} ${args.join(" ")}: ${why}${tail ? `
3344
- ${tail}` : ""}`));
3367
+ settle(() => reject(new Error(`${name} ${args.join(" ")}: ${why}${tail ? `
3368
+ ${tail}` : ""}`)));
3345
3369
  }
3346
3370
  });
3347
3371
  });
3348
3372
  }
3373
+ function resolveArgs(arg, build) {
3374
+ return typeof arg === "string" ? build(arg) : [...arg];
3375
+ }
3349
3376
  async function runCmduse(arg, opts = {}) {
3350
- const args = buildCmduseArgs(arg);
3377
+ const args = resolveArgs(arg, buildCmduseArgs);
3351
3378
  const candidates = opts.candidates ?? CANDIDATES;
3352
3379
  let last;
3353
3380
  for (const bin of candidates) {
@@ -3362,7 +3389,7 @@ async function runCmduse(arg, opts = {}) {
3362
3389
  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
3390
  }
3364
3391
  async function runMpc(arg, opts = {}) {
3365
- const args = buildMpcArgs(arg);
3392
+ const args = resolveArgs(arg, buildMpcArgs);
3366
3393
  const candidates = opts.candidates ?? MPC_CANDIDATES;
3367
3394
  let last;
3368
3395
  for (const bin of candidates) {
@@ -5025,6 +5052,7 @@ var commandCodeV2 = {
5025
5052
 
5026
5053
  // src/index.ts
5027
5054
  var PROVIDER_NPM = `${package_default.name}@${package_default.version}`;
5055
+ var [ANTHROPIC, OPENAI] = LANES;
5028
5056
  function tool(input) {
5029
5057
  return input;
5030
5058
  }
@@ -5071,7 +5099,7 @@ function toModelDefs(models, providerID, npm, interleaved = false) {
5071
5099
  var CommandCodePlugin = async (_input) => {
5072
5100
  return {
5073
5101
  auth: {
5074
- provider: "command-code-anthropic",
5102
+ provider: ANTHROPIC.id,
5075
5103
  loader: async (getAuth) => {
5076
5104
  const auth = await getAuth();
5077
5105
  if (auth?.type !== "api") {
@@ -5101,7 +5129,7 @@ var CommandCodePlugin = async (_input) => {
5101
5129
  return {
5102
5130
  type: "success",
5103
5131
  key,
5104
- provider: "command-code-anthropic"
5132
+ provider: ANTHROPIC.id
5105
5133
  };
5106
5134
  } catch {
5107
5135
  return { type: "failed" };
@@ -5125,19 +5153,19 @@ var CommandCodePlugin = async (_input) => {
5125
5153
  console.warn("[command-code] model list unavailable:", e);
5126
5154
  }
5127
5155
  }
5128
- const claudeDefs = split ? toModelDefs(split.claude, "command-code-anthropic", PROVIDER_NPM) : {};
5129
- const openDefs = split ? toModelDefs(split.open, "command-code-openai", PROVIDER_NPM, {
5156
+ const claudeDefs = split ? toModelDefs(split.claude, ANTHROPIC.id, PROVIDER_NPM) : {};
5157
+ const openDefs = split ? toModelDefs(split.open, OPENAI.id, PROVIDER_NPM, {
5130
5158
  field: "reasoning_content"
5131
5159
  }) : {};
5132
- const userAnthropic = existing("command-code-anthropic");
5133
- cfg.provider["command-code-anthropic"] = {
5160
+ const userAnthropic = existing(ANTHROPIC.id);
5161
+ cfg.provider[ANTHROPIC.id] = {
5134
5162
  npm: userAnthropic.npm ?? PROVIDER_NPM,
5135
5163
  name: userAnthropic.name ?? "Command Code (Anthropic)",
5136
5164
  options: { baseURL: PROVIDER_BASE, ...userAnthropic.options },
5137
5165
  models: { ...claudeDefs, ...userAnthropic.models }
5138
5166
  };
5139
- const userOpenai = existing("command-code-openai");
5140
- cfg.provider["command-code-openai"] = {
5167
+ const userOpenai = existing(OPENAI.id);
5168
+ cfg.provider[OPENAI.id] = {
5141
5169
  npm: userOpenai.npm ?? PROVIDER_NPM,
5142
5170
  name: userOpenai.name ?? "Command Code (OpenAI)",
5143
5171
  options: {
@@ -5154,14 +5182,14 @@ var CommandCodePlugin = async (_input) => {
5154
5182
  };
5155
5183
  },
5156
5184
  provider: {
5157
- id: "command-code-anthropic",
5185
+ id: ANTHROPIC.id,
5158
5186
  models: async (_provider, ctx) => {
5159
5187
  const key = await resolveKey(async () => {
5160
5188
  const a = ctx.auth;
5161
5189
  return a?.type === "api" && a.key ? { key: a.key } : undefined;
5162
5190
  });
5163
5191
  const split = await loadModels(key);
5164
- return toModelDefs(split.claude, "command-code-anthropic", PROVIDER_NPM);
5192
+ return toModelDefs(split.claude, ANTHROPIC.id, PROVIDER_NPM);
5165
5193
  }
5166
5194
  },
5167
5195
  tool: {
@@ -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>;
@@ -140,7 +140,12 @@ export declare function until(epoch: number | undefined, now?: number): string;
140
140
  * the OpenCode Go/Zen side adds the window's request count as `extra`, so
141
141
  * `LIMIT EXCEEDED`, the tone, `cap in …`, `elapsed` and `resets` all have one
142
142
  * implementation. */
143
- export declare function windowRows(label: string, w: WindowUsage | undefined, now: number, durSecs: number, extra?: string): SidebarRow[];
143
+ export declare function windowRows(label: string, w: WindowUsage | undefined, now: number, durSecs: number, extra?: string,
144
+ /** How the use line reads. The default is money (`$0.55/$14 (3.9%)`); the
145
+ * account's own meters publish shares rather than dollars and pass a percent
146
+ * formatter, so the tone, `LIMIT EXCEEDED`, `cap in …`, `elapsed` and
147
+ * `resets` stay this one implementation for both. */
148
+ use?: (used: number, cap: number, pct: number) => string): SidebarRow[];
144
149
  /**
145
150
  * The two store-backed rows for the active model — its requests under `Usage
146
151
  * (this model)`, then this conversation's own totals under `Session`. They need
@@ -24,6 +24,21 @@ export declare function periodStart(usage: {
24
24
  * while the session said 2.7K).
25
25
  */
26
26
  export declare function loadModelUsage(modelID: string, sinceMs: number, path?: string, sessionID?: string): ModelUsage | null;
27
+ /**
28
+ * The oldest in-window assistant turn for one model, as epoch ms — the start of
29
+ * the rolling window the Go/Zen panel needs. A rolling 5-hour/weekly window has
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
+ * window, or the store is missing/unreadable, so callers then omit the
34
+ * continuation exactly as before.
35
+ *
36
+ * Account-wide like ocuse's per-model window totals, so it is *not* scoped to a
37
+ * session: a rolling window counts the model's spend across every conversation.
38
+ * Matching is on the canonical key, the same normalisation `loadModelUsage` uses
39
+ * (the session id and the stored `modelID` differ by vendor prefix/punctuation).
40
+ */
41
+ export declare function loadModelWindowStart(modelID: string, sinceMs: number, path?: string): number | null;
27
42
  /**
28
43
  * Requests and cost for one session, so the panel can show what this
29
44
  * conversation has spent rather than only the period-to-date figure. Null when
@@ -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,14 +48,33 @@ 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;
32
53
  /** Keyed by canonical model key, the same normalisation the CC side matches on. */
33
54
  models: Map<string, ZenModel>;
34
55
  }
35
56
  /** Parse `ocuse -1 --json`. Anything malformed reads as "no usage". */
36
57
  export declare function parseZenJson(text: string): ZenUsage;
58
+ /**
59
+ * The starts of the active model's rolling windows, epoch ms: the oldest request
60
+ * still inside each one, from opencode's own store. The Go/Zen panel turns each
61
+ * into a reset via `rollingReset`; absent when no such request exists.
62
+ */
63
+ export interface ZenWindowStarts {
64
+ fiveHour?: number;
65
+ weekly?: number;
66
+ }
67
+ /**
68
+ * A rolling window's reset, derived from the oldest request still inside it:
69
+ * that request leaves the window at `oldest + durSecs`, which is an honest reset
70
+ * epoch — unlike `now + durSecs`, a constant that would be noise in a usage
71
+ * panel. Undefined when there is no oldest request (an empty window), so the row
72
+ * keeps the same spend-only degrade it had before.
73
+ */
74
+ export declare function rollingReset(startMs: number | undefined, durSecs: number): number | undefined;
37
75
  /** The OpenCode Go / Zen panel: period windows, a rule, then the active model.
38
76
  * Like `modelRows`, `fallbackName` (the session's own id) keeps the model block
39
77
  * — and the local `Usage (this model)` / `Session` figures, which need no network
40
78
  * — on screen when mpc's catalog is unreachable; only the catalog-derived rows
41
79
  * then drop out. */
42
- export declare function zenRows(usage: ZenUsage | undefined, meta: ModelMeta | undefined, modelUsage?: ModelUsage, session?: ModelUsage, fallbackName?: string): SidebarRow[];
80
+ export declare function zenRows(usage: ZenUsage | undefined, meta: ModelMeta | undefined, modelUsage?: ModelUsage, session?: ModelUsage, fallbackName?: string, windows?: ZenWindowStarts): SidebarRow[];
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,12 @@ 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
+ }
80
92
  function stderrTail(text) {
81
93
  const lines = text.trimEnd().split(`
82
94
  `);
@@ -90,38 +102,55 @@ function runOnce(bin, args, opts, name = "cmduse") {
90
102
  ...process.env,
91
103
  ...opts.env ?? {}
92
104
  },
93
- stdio: ["ignore", "pipe", "pipe"]
105
+ stdio: ["ignore", "pipe", "pipe"],
106
+ ...opts.spawnOptions
94
107
  });
95
108
  const out = [];
96
109
  const err = [];
110
+ let settled = false;
111
+ const timer = opts.timeoutMs === undefined ? undefined : setTimeout(() => {
112
+ if (settled)
113
+ return;
114
+ settled = true;
115
+ child.kill();
116
+ reject(new Error(`${name} ${args.join(" ")}: timed out after ${opts.timeoutMs}ms`));
117
+ }, opts.timeoutMs);
97
118
  const onAbort = () => child.kill("SIGTERM");
98
119
  opts.signal?.addEventListener("abort", onAbort, {
99
120
  once: true
100
121
  });
122
+ const settle = (fn) => {
123
+ if (settled)
124
+ return;
125
+ settled = true;
126
+ if (timer)
127
+ clearTimeout(timer);
128
+ opts.signal?.removeEventListener("abort", onAbort);
129
+ fn();
130
+ };
101
131
  child.stdout.on("data", (d) => out.push(d));
102
132
  child.stderr.on("data", (d) => err.push(d));
103
- child.on("error", (e) => {
104
- opts.signal?.removeEventListener("abort", onAbort);
105
- reject(e);
106
- });
133
+ child.on("error", (e) => settle(() => reject(e)));
107
134
  child.on("close", (code, signal) => {
108
- opts.signal?.removeEventListener("abort", onAbort);
109
135
  const stdout = Buffer.concat(out).toString("utf8");
110
136
  if (opts.signal?.aborted) {
111
- reject(new Error(`${name} invocation aborted`));
137
+ settle(() => reject(new Error(`${name} invocation aborted`)));
112
138
  } else if (code === 0) {
113
- resolve(stdout);
139
+ settle(() => resolve(stdout));
114
140
  } else {
115
141
  const why = signal ? `killed by ${signal}` : `exit ${code}`;
116
142
  const tail = stderrTail(Buffer.concat(err).toString("utf8"));
117
- reject(new Error(`${name} ${args.join(" ")}: ${why}${tail ? `
118
- ${tail}` : ""}`));
143
+ settle(() => reject(new Error(`${name} ${args.join(" ")}: ${why}${tail ? `
144
+ ${tail}` : ""}`)));
119
145
  }
120
146
  });
121
147
  });
122
148
  }
149
+ function resolveArgs(arg, build) {
150
+ return typeof arg === "string" ? build(arg) : [...arg];
151
+ }
123
152
  async function runCmduse(arg, opts = {}) {
124
- const args = buildCmduseArgs(arg);
153
+ const args = resolveArgs(arg, buildCmduseArgs);
125
154
  const candidates = opts.candidates ?? CANDIDATES;
126
155
  let last;
127
156
  for (const bin of candidates) {
@@ -137,6 +166,40 @@ async function runCmduse(arg, opts = {}) {
137
166
  cause: last
138
167
  });
139
168
  }
169
+ async function runMpc(arg, opts = {}) {
170
+ const args = resolveArgs(arg, buildMpcArgs);
171
+ const candidates = opts.candidates ?? MPC_CANDIDATES;
172
+ let last;
173
+ for (const bin of candidates) {
174
+ try {
175
+ return await runOnce(bin, args, opts, "mpc");
176
+ } catch (e) {
177
+ last = e;
178
+ if (e?.code !== "ENOENT")
179
+ throw e;
180
+ }
181
+ }
182
+ throw new Error(`mpc binary not found (install: bun add -g @jeffreyjyz/mpc)${last instanceof Error && last.message !== last.name ? ` \u2014 ${last.message}` : ""}`, {
183
+ cause: last
184
+ });
185
+ }
186
+ async function runOcuse(arg, opts = {}) {
187
+ const args = resolveArgs(arg, splitCliArgs);
188
+ const candidates = opts.candidates ?? OCUSE_CANDIDATES;
189
+ let last;
190
+ for (const bin of candidates) {
191
+ try {
192
+ return await runOnce(bin, args, opts, "ocuse");
193
+ } catch (e) {
194
+ last = e;
195
+ if (e?.code !== "ENOENT")
196
+ throw e;
197
+ }
198
+ }
199
+ throw new Error(`ocuse binary not found (install: brew install JeffreyJYZ/tap/cmduse)${last instanceof Error && last.message !== last.name ? ` \u2014 ${last.message}` : ""}`, {
200
+ cause: last
201
+ });
202
+ }
140
203
 
141
204
  // src/constants/providers.ts
142
205
  var PROVIDER_ID_PREFIX = "command-code";
@@ -144,6 +207,7 @@ var GO_PROVIDER_ID = "opencode-go";
144
207
  var ZEN_PROVIDER_ID = "opencode";
145
208
  var ZEN_PROVIDER_PREFIX = "opencode-zen";
146
209
  var TUI_ID = "command-code.tui";
210
+ var CMD_USAGE_COMMAND_ID = "command-code.cmd-usage";
147
211
 
148
212
  // src/sidebar/panel.tsx
149
213
  import { createComponent as _$createComponent } from "@opentui/solid";
@@ -621,7 +685,7 @@ function until(epoch, now = Date.now()) {
621
685
  const days = Math.floor(hours / 24);
622
686
  return hours % 24 ? `${days}d ${hours % 24}h` : `${days}d`;
623
687
  }
624
- function windowRows(label, w, now, durSecs, extra) {
688
+ function windowRows(label, w, now, durSecs, extra, use) {
625
689
  if (!w || typeof w.cap !== "number" || w.cap <= 0)
626
690
  return [];
627
691
  const used = typeof w.used === "number" ? w.used : 0;
@@ -632,7 +696,7 @@ function windowRows(label, w, now, durSecs, extra) {
632
696
  const reset = until(w.resetAt, now);
633
697
  const lead = w.exceeded ? "LIMIT EXCEEDED" : eta === undefined ? elapsed === undefined ? "" : `${elapsed} elapsed` : `cap in ${until(now + eta * 1000, now)}`;
634
698
  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"]] : []];
699
+ return [[label, use ? use(used, w.cap, pct) : `${money(used)}/${money(w.cap)} (${pct.toFixed(1)}%)`, w.exceeded ? "crit" : pctTone(pct)], ...detail ? [["", detail, "muted"]] : [], ...extra ? [["", extra, "muted"]] : []];
636
700
  }
637
701
  function localUsageRows(usage, session) {
638
702
  const rows = [];
@@ -2027,7 +2091,6 @@ function minPlan(id) {
2027
2091
  }
2028
2092
 
2029
2093
  // src/sidebar/data.ts
2030
- import { spawn as spawn2 } from "child_process";
2031
2094
  import { mkdir, readFile, writeFile } from "fs/promises";
2032
2095
  import { homedir as homedir3 } from "os";
2033
2096
  import { dirname, join as join3 } from "path";
@@ -2042,6 +2105,14 @@ function totals(value) {
2042
2105
  tokens: num("tokens")
2043
2106
  };
2044
2107
  }
2108
+ function meter(value) {
2109
+ const v = value ?? {};
2110
+ return {
2111
+ status: typeof v.status === "string" ? v.status : "ok",
2112
+ percent: typeof v.percent === "number" ? v.percent : 0,
2113
+ resetsAt: typeof v.resetsAt === "number" ? v.resetsAt : undefined
2114
+ };
2115
+ }
2045
2116
  function parseZenJson(text) {
2046
2117
  let body;
2047
2118
  try {
@@ -2067,6 +2138,11 @@ function parseZenJson(text) {
2067
2138
  periodStart: typeof body.periodStartAt === "number" ? body.periodStartAt : typeof body.periodStart === "number" ? body.periodStart : undefined,
2068
2139
  periodEndAt: typeof body.periodEndAt === "number" ? body.periodEndAt : undefined,
2069
2140
  source: typeof body.source === "string" ? body.source : undefined,
2141
+ account: body.account ? {
2142
+ rolling: meter(body.account.rolling),
2143
+ weekly: meter(body.account.weekly),
2144
+ monthly: meter(body.account.monthly)
2145
+ } : undefined,
2070
2146
  totals: {
2071
2147
  fiveHour: totals(top.fiveHour),
2072
2148
  weekly: totals(top.weekly),
@@ -2090,6 +2166,11 @@ function monthPeriod(usage) {
2090
2166
  durSecs
2091
2167
  };
2092
2168
  }
2169
+ function rollingReset(startMs, durSecs) {
2170
+ if (typeof startMs !== "number" || !Number.isFinite(startMs))
2171
+ return;
2172
+ return startMs + durSecs * 1000;
2173
+ }
2093
2174
  function zenWindow(label, spent, limit, share, now, durSecs, resetAt) {
2094
2175
  const cap = typeof limit === "number" && limit > 0 ? limit * share : undefined;
2095
2176
  if (cap === undefined) {
@@ -2102,19 +2183,33 @@ function zenWindow(label, spent, limit, share, now, durSecs, resetAt) {
2102
2183
  exceeded: spent.cost >= cap
2103
2184
  }, now, durSecs, spent.requests > 0 ? `${count(spent.requests)} req` : undefined);
2104
2185
  }
2105
- function zenRows(usage, meta, modelUsage, session, fallbackName) {
2186
+ function zenMeterRows(label, meter, now, durSecs) {
2187
+ return windowRows(label, {
2188
+ cap: 100,
2189
+ used: meter.percent,
2190
+ resetAt: meter.resetsAt,
2191
+ exceeded: meter.percent >= 100 || meter.status !== "ok"
2192
+ }, now, durSecs, undefined, (_used, _cap, pct) => `${pct.toFixed(1)}% of limit`);
2193
+ }
2194
+ function zenRows(usage, meta, modelUsage, session, fallbackName, windows) {
2106
2195
  if (!usage)
2107
2196
  return [];
2108
2197
  const now = Date.now();
2109
2198
  const oc = meta?.oc;
2110
2199
  const rows = [["Plan", `${oc?.plan ?? "Go"} \xB7 limits per model`, "base"]];
2111
- const mine = meta ? usage.models.get(modelKey(meta.name)) : undefined;
2200
+ const mine = usage.models.get(modelKey(fallbackName ?? "")) ?? (meta ? usage.models.get(modelKey(meta.name)) : undefined);
2112
2201
  const limit = oc?.allowance ?? mine?.limit;
2113
2202
  const cap = mine ? limit : undefined;
2114
- rows.push(...zenWindow("5-hour", mine?.fiveHour ?? usage.totals.fiveHour, cap, GO_WINDOW_SHARE.fiveHour, now, FIVE_HOUR_SECS));
2115
- rows.push(...zenWindow("Weekly", mine?.weekly ?? usage.totals.weekly, cap, GO_WINDOW_SHARE.weekly, now, WEEKLY_SECS));
2116
2203
  const period = monthPeriod(usage);
2117
- rows.push(...zenWindow("Monthly", mine?.month ?? usage.totals.month, cap, GO_WINDOW_SHARE.month, now, period?.durSecs ?? 0, period?.resetAt));
2204
+ if (usage.account) {
2205
+ rows.push(...zenMeterRows("5-hour", usage.account.rolling, now, FIVE_HOUR_SECS));
2206
+ rows.push(...zenMeterRows("Weekly", usage.account.weekly, now, WEEKLY_SECS));
2207
+ rows.push(...zenMeterRows("Monthly", usage.account.monthly, now, period?.durSecs ?? 0));
2208
+ } else {
2209
+ 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)));
2210
+ rows.push(...zenWindow("Weekly", mine?.weekly ?? usage.totals.weekly, cap, GO_WINDOW_SHARE.weekly, now, WEEKLY_SECS, rollingReset(windows?.weekly, WEEKLY_SECS)));
2211
+ rows.push(...zenWindow("Monthly", mine?.month ?? usage.totals.month, cap, GO_WINDOW_SHARE.month, now, period?.durSecs ?? 0, period?.resetAt));
2212
+ }
2118
2213
  rows.push(["Period", `${count(usage.totals.month.requests)} requests \xB7 ${money(usage.totals.month.cost)}`, "base"]);
2119
2214
  rows.push(separator());
2120
2215
  const name = meta?.name ?? fallbackName;
@@ -2122,11 +2217,11 @@ function zenRows(usage, meta, modelUsage, session, fallbackName) {
2122
2217
  return rows;
2123
2218
  rows.push(["Model", clip(name, valueWidth("Model")), "strong", true]);
2124
2219
  rows.push(...localUsageRows(modelUsage, session));
2125
- if (!meta)
2126
- return rows;
2127
2220
  if (typeof limit === "number") {
2128
2221
  rows.push(["Allowance", allowanceLabel(limit, oc?.free), "base"]);
2129
2222
  }
2223
+ if (!meta)
2224
+ return rows;
2130
2225
  if (oc?.rates) {
2131
2226
  const cache = [`cache read ${rate(oc.rates.cacheRead)}`];
2132
2227
  if (typeof oc.rates.cacheWrite === "number" && oc.rates.cacheWrite > 0) {
@@ -2144,48 +2239,10 @@ function zenRows(usage, meta, modelUsage, session, fallbackName) {
2144
2239
  }
2145
2240
 
2146
2241
  // src/sidebar/data.ts
2147
- var CMDUSE = cmduseCandidates();
2148
- var MPC = MPC_BIN_CANDIDATES;
2149
- var OCUSE = [process.env.OCUSE_BIN, ...OCUSE_BIN_CANDIDATES].filter((bin) => Boolean(bin));
2150
- function run(bin, args) {
2151
- return new Promise((resolve, reject) => {
2152
- const child = spawn2(bin, args, SPAWN_OPTIONS);
2153
- const out = [];
2154
- const err = [];
2155
- let settled = false;
2156
- const timer = setTimeout(() => {
2157
- if (settled)
2158
- return;
2159
- settled = true;
2160
- child.kill();
2161
- reject(new Error(`${bin} ${args.join(" ")}: timed out after ${CHILD_TIMEOUT_MS}ms`));
2162
- }, CHILD_TIMEOUT_MS);
2163
- const done = (fn) => {
2164
- if (settled)
2165
- return;
2166
- settled = true;
2167
- clearTimeout(timer);
2168
- fn();
2169
- };
2170
- child.stdout?.on("data", (d) => out.push(d));
2171
- child.stderr?.on("data", (d) => err.push(d));
2172
- child.on("error", (e) => done(() => reject(e)));
2173
- 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)}`))));
2174
- });
2175
- }
2176
- async function runFirst(candidates, args) {
2177
- let last;
2178
- for (const bin of candidates) {
2179
- try {
2180
- return await run(bin, args);
2181
- } catch (e) {
2182
- last = e;
2183
- if (e?.code !== "ENOENT")
2184
- throw e;
2185
- }
2186
- }
2187
- throw last instanceof Error ? last : new Error(`${candidates[0]} not found`);
2188
- }
2242
+ var SIDEBAR_OPTS = {
2243
+ spawnOptions: SPAWN_OPTIONS,
2244
+ timeoutMs: CHILD_TIMEOUT_MS
2245
+ };
2189
2246
  function parseUsageJson(text) {
2190
2247
  const raw = JSON.parse(text);
2191
2248
  const win = (v) => {
@@ -2271,7 +2328,7 @@ async function loadMeta(plan = OC_PLAN_DEFAULT) {
2271
2328
  return new Map(cached.rows ?? []);
2272
2329
  }
2273
2330
  } catch {}
2274
- const meta = parseMpcJson(await runFirst(MPC, metaArgs(plan)));
2331
+ const meta = parseMpcJson(await runMpc(metaArgs(plan), SIDEBAR_OPTS));
2275
2332
  try {
2276
2333
  const path = cachePath(plan);
2277
2334
  await mkdir(dirname(path), {
@@ -2285,10 +2342,10 @@ async function loadMeta(plan = OC_PLAN_DEFAULT) {
2285
2342
  return meta;
2286
2343
  }
2287
2344
  async function loadUsage() {
2288
- return requireSnapshot(parseUsageJson(await runFirst(CMDUSE, ["-1", "--json", "--plain"])));
2345
+ return requireSnapshot(parseUsageJson(await runCmduse(["-1", "--json", "--plain"], SIDEBAR_OPTS)));
2289
2346
  }
2290
2347
  async function loadZen(plan = OC_PLAN_DEFAULT) {
2291
- return parseZenJson(await runFirst(OCUSE, zenArgs(plan)));
2348
+ return parseZenJson(await runOcuse(zenArgs(plan), SIDEBAR_OPTS));
2292
2349
  }
2293
2350
 
2294
2351
  // src/sidebar/usageDb.ts
@@ -2375,6 +2432,53 @@ function loadModelUsage(modelID, sinceMs, path = usageDbPath(), sessionID) {
2375
2432
  db.close();
2376
2433
  }
2377
2434
  }
2435
+ function loadModelWindowStart(modelID, sinceMs, path = usageDbPath()) {
2436
+ let db;
2437
+ try {
2438
+ db = new Database(path, {
2439
+ readonly: true
2440
+ });
2441
+ } catch {
2442
+ return null;
2443
+ }
2444
+ try {
2445
+ const key = modelKey(modelID);
2446
+ if (hasV2(db)) {
2447
+ const rows = db.query(`SELECT json_extract(data, '$.model.id') AS model,
2448
+ min(time_created) AS start
2449
+ FROM session_message
2450
+ WHERE type = 'assistant' AND time_created >= ?
2451
+ GROUP BY model`).all(sinceMs);
2452
+ for (const row of rows) {
2453
+ if (row.model && modelKey(row.model) === key)
2454
+ return row.start;
2455
+ }
2456
+ return null;
2457
+ }
2458
+ const rows = db.query(`SELECT data, time_created FROM message
2459
+ WHERE data LIKE '%"tokens"%' AND time_created >= ?`).all(sinceMs);
2460
+ let oldest = null;
2461
+ for (const row of rows) {
2462
+ let data;
2463
+ try {
2464
+ data = JSON.parse(row.data);
2465
+ } catch {
2466
+ continue;
2467
+ }
2468
+ if (data.role !== "assistant" || !data.tokens || !data.modelID)
2469
+ continue;
2470
+ if (modelKey(data.modelID) !== key)
2471
+ continue;
2472
+ if (oldest === null || row.time_created < oldest)
2473
+ oldest = row.time_created;
2474
+ }
2475
+ return oldest;
2476
+ } catch {
2477
+ return null;
2478
+ } finally {
2479
+ db.close();
2480
+ }
2481
+ }
2378
2482
  function loadSessionUsage(sessionID, path = usageDbPath()) {
2379
2483
  let db;
2380
2484
  try {
@@ -2485,6 +2589,7 @@ function useRows(activeModelId, providerID, sessionID) {
2485
2589
  const [meta, setMeta] = createSignal(seededMeta());
2486
2590
  const [modelUsage, setModelUsage] = createSignal(lastModelUsage);
2487
2591
  const [sessionUsage, setSessionUsage] = createSignal();
2592
+ const [modelWindows, setModelWindows] = createSignal();
2488
2593
  const [wasOurs, setWasOurs] = createSignal(false);
2489
2594
  const refreshMeta = () => {
2490
2595
  loadMeta(loadPrefs().ocPlan).then((next) => {
@@ -2505,6 +2610,15 @@ function useRows(activeModelId, providerID, sessionID) {
2505
2610
  rememberModelUsage(id, fresh, session);
2506
2611
  setModelUsage(lastModelUsage);
2507
2612
  };
2613
+ const refreshModelWindows = (id) => {
2614
+ if (!id)
2615
+ return;
2616
+ const now = Date.now();
2617
+ setModelWindows({
2618
+ fiveHour: loadModelWindowStart(id, now - FIVE_HOUR_SECS * 1000) ?? undefined,
2619
+ weekly: loadModelWindowStart(id, now - WEEKLY_SECS * 1000) ?? undefined
2620
+ });
2621
+ };
2508
2622
  let inFlight = false;
2509
2623
  const refresh = async () => {
2510
2624
  if (inFlight)
@@ -2524,6 +2638,7 @@ function useRows(activeModelId, providerID, sessionID) {
2524
2638
  } else if (kind === "go" || kind === "zen") {
2525
2639
  setZen(await loadZen(loadPrefs().ocPlan));
2526
2640
  refreshModelUsage(activeModelId());
2641
+ refreshModelWindows(activeModelId());
2527
2642
  }
2528
2643
  } catch {} finally {
2529
2644
  inFlight = false;
@@ -2541,7 +2656,12 @@ function useRows(activeModelId, providerID, sessionID) {
2541
2656
  createEffect(() => {
2542
2657
  const id = activeModelId();
2543
2658
  const session = sessionID();
2544
- if (!shows(sessionKind(providerID())) || !id || !session)
2659
+ const kind = sessionKind(providerID());
2660
+ if (!shows(kind) || !id)
2661
+ return;
2662
+ if (kind === "go" || kind === "zen")
2663
+ refreshModelWindows(id);
2664
+ if (!session)
2545
2665
  return;
2546
2666
  refreshModelUsage(id);
2547
2667
  });
@@ -2561,7 +2681,7 @@ function useRows(activeModelId, providerID, sessionID) {
2561
2681
  const found = id ? meta().get(modelKey(id)) ?? meta().get(id) : undefined;
2562
2682
  const fallbackName = id ? id.split("/").pop() ?? id : undefined;
2563
2683
  if (kind === "go" || kind === "zen") {
2564
- return zenRows(zen(), found, usageFor(id), sessionUsage(), fallbackName);
2684
+ return zenRows(zen(), found, usageFor(id), sessionUsage(), fallbackName, modelWindows());
2565
2685
  }
2566
2686
  const model = found ? {
2567
2687
  ...found,
@@ -2629,7 +2749,7 @@ var commandCodeTui = {
2629
2749
  mode: "global",
2630
2750
  priority: 10,
2631
2751
  commands: [{
2632
- id: "command-code.cmd-usage",
2752
+ id: CMD_USAGE_COMMAND_ID,
2633
2753
  title: "Command Code usage",
2634
2754
  group: "Command Code",
2635
2755
  slash: {
@@ -2656,7 +2776,7 @@ var commandCodeTui = {
2656
2776
  });
2657
2777
  }
2658
2778
  }],
2659
- bindings: ["command-code.cmd-usage"]
2779
+ bindings: [CMD_USAGE_COMMAND_ID]
2660
2780
  }));
2661
2781
  }
2662
2782
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeffreyjyz/opencode-command-code",
3
- "version": "0.5.1",
3
+ "version": "0.5.4",
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",