@jeffreyjyz/opencode-command-code 0.4.0 → 0.5.1

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/cli.d.ts CHANGED
@@ -2,6 +2,10 @@
2
2
  * the homebrew prefix, so the bare name alone is not enough. `CMDUSE_BIN` wins
3
3
  * when set — the dev-build hook the sibling cmduse consumers (mpc) honour too. */
4
4
  export declare function cmduseCandidates(env?: Record<string, string | undefined>): string[];
5
+ /** Same lookup for mpc, the per-model price comparison: `MPC_BIN`
6
+ * wins when set (the `bun link` dev hook oc-cmd-compare installs),
7
+ * then the bun/homebrew fallbacks. */
8
+ export declare function mpcCandidates(env?: Record<string, string | undefined>): string[];
5
9
  /** Shell-words split: whitespace-separated, with single/double quoting and
6
10
  * backslash escapes ("--tz +05:30", arg='a b'). Unterminated quotes close at
7
11
  * end of input. */
@@ -10,6 +14,10 @@ export declare function splitCliArgs(input: string): string[];
10
14
  * pass-through flags, or a subcommand invocation ("plans",
11
15
  * "--tz +05:30 daily") passed through untouched. */
12
16
  export declare function buildCmduseArgs(arg: string): string[];
17
+ /** argv for an mpc invocation: mpc has no subcommands, so the arg
18
+ * string is split and passed straight through; an empty arg means
19
+ * the fewest-columns view (see MPC_DEFAULT_ARGS). */
20
+ export declare function buildMpcArgs(arg: string): string[];
13
21
  export type RunOptions = {
14
22
  signal?: AbortSignal;
15
23
  /** Extra env for the child (CMD_API_KEY when the plugin resolved one). */
@@ -21,3 +29,9 @@ export type RunOptions = {
21
29
  * homebrew locations; every candidate failing with ENOENT yields a single
22
30
  * install-hint error. */
23
31
  export declare function runCmduse(arg: string, opts?: RunOptions): Promise<string>;
32
+ /** Run mpc and return its plain-text stdout (the per-model price
33
+ * comparison). Same contract as runCmduse: `MPC_BIN` wins, then the
34
+ * fallback paths; every candidate failing with ENOENT yields a
35
+ * single install-hint error, and any other failure (a non-zero exit
36
+ * included) surfaces immediately with mpc's last stderr lines. */
37
+ export declare function runMpc(arg: string, opts?: RunOptions): Promise<string>;
@@ -1,6 +1,11 @@
1
1
  import type { spawn } from "node:child_process";
2
2
  /** Default invocation: one-shot dashboard, no colors / no live redraw. */
3
3
  export declare const DEFAULT_ARGS: readonly string[];
4
+ /** Default mpc invocation: the fewest-columns view. mpc has no
5
+ * subcommands to fall back on, so an empty arg must mean the
6
+ * narrowest table — a bare tool call cannot dump a wide comparison
7
+ * into the model's context. */
8
+ export declare const MPC_DEFAULT_ARGS: readonly string[];
4
9
  /** cmduse subcommands — when the arg starts with one, pass it through
5
10
  * untouched; otherwise it is extra flags for the one-shot dashboard. */
6
11
  export declare const SUBCOMMANDS: ReadonlySet<string>;
@@ -15,6 +15,10 @@ export declare const ZEN_PROVIDER_PREFIX = "opencode-zen";
15
15
  export declare const TUI_ID = "command-code.tui";
16
16
  /** The cmd_usage tool description, shared by the v1 and v2 halves. */
17
17
  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
+ /** The mpc_prices tool description, shared by the v1 and v2 halves. */
19
+ 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
+ /** The mpc_prices `arg` description, shared by the v1 and v2 halves. */
21
+ 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";
18
22
  export type Lane = {
19
23
  id: "command-code-anthropic" | "command-code-openai";
20
24
  name: string;
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.4.0",
2417
+ version: "0.5.1",
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",
@@ -3233,6 +3233,7 @@ var MPC_BIN_CANDIDATES = [
3233
3233
 
3234
3234
  // src/constants/cli.ts
3235
3235
  var DEFAULT_ARGS = ["-1", "--plain"];
3236
+ var MPC_DEFAULT_ARGS = ["--minimal"];
3236
3237
  var SUBCOMMANDS = new Set([
3237
3238
  "plans",
3238
3239
  "models",
@@ -3250,6 +3251,11 @@ function cmduseCandidates(env = process.env) {
3250
3251
  return candidates.filter((bin) => Boolean(bin));
3251
3252
  }
3252
3253
  var CANDIDATES = cmduseCandidates();
3254
+ function mpcCandidates(env = process.env) {
3255
+ const candidates = [env.MPC_BIN, ...MPC_BIN_CANDIDATES];
3256
+ return candidates.filter((bin) => Boolean(bin));
3257
+ }
3258
+ var MPC_CANDIDATES = mpcCandidates();
3253
3259
  function splitCliArgs(input) {
3254
3260
  const out = [];
3255
3261
  let cur = "";
@@ -3296,13 +3302,19 @@ function buildCmduseArgs(arg) {
3296
3302
  return tokens;
3297
3303
  return [...DEFAULT_ARGS, ...tokens];
3298
3304
  }
3305
+ function buildMpcArgs(arg) {
3306
+ const tokens = splitCliArgs(arg);
3307
+ if (!tokens.length)
3308
+ return [...MPC_DEFAULT_ARGS];
3309
+ return tokens;
3310
+ }
3299
3311
  function stderrTail(text) {
3300
3312
  const lines = text.trimEnd().split(`
3301
3313
  `);
3302
3314
  return lines.slice(-3).join(`
3303
3315
  `).slice(-400);
3304
3316
  }
3305
- function runOnce(bin, args, opts) {
3317
+ function runOnce(bin, args, opts, name = "cmduse") {
3306
3318
  return new Promise((resolve, reject) => {
3307
3319
  const child = spawn(bin, args, {
3308
3320
  env: { ...process.env, ...opts.env ?? {} },
@@ -3322,13 +3334,13 @@ function runOnce(bin, args, opts) {
3322
3334
  opts.signal?.removeEventListener("abort", onAbort);
3323
3335
  const stdout = Buffer.concat(out).toString("utf8");
3324
3336
  if (opts.signal?.aborted) {
3325
- reject(new Error("cmduse invocation aborted"));
3337
+ reject(new Error(`${name} invocation aborted`));
3326
3338
  } else if (code === 0) {
3327
3339
  resolve(stdout);
3328
3340
  } else {
3329
3341
  const why = signal ? `killed by ${signal}` : `exit ${code}`;
3330
3342
  const tail = stderrTail(Buffer.concat(err).toString("utf8"));
3331
- reject(new Error(`cmduse ${args.join(" ")}: ${why}${tail ? `
3343
+ reject(new Error(`${name} ${args.join(" ")}: ${why}${tail ? `
3332
3344
  ${tail}` : ""}`));
3333
3345
  }
3334
3346
  });
@@ -3349,6 +3361,21 @@ async function runCmduse(arg, opts = {}) {
3349
3361
  }
3350
3362
  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 });
3351
3363
  }
3364
+ async function runMpc(arg, opts = {}) {
3365
+ const args = buildMpcArgs(arg);
3366
+ const candidates = opts.candidates ?? MPC_CANDIDATES;
3367
+ let last;
3368
+ for (const bin of candidates) {
3369
+ try {
3370
+ return await runOnce(bin, args, opts, "mpc");
3371
+ } catch (e) {
3372
+ last = e;
3373
+ if (e?.code !== "ENOENT")
3374
+ throw e;
3375
+ }
3376
+ }
3377
+ 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
+ }
3352
3379
 
3353
3380
  // src/constants/models.ts
3354
3381
  var MODEL_CAPABILITIES = {
@@ -3375,6 +3402,8 @@ var PLUGIN_ID = PROVIDER_ID_PREFIX;
3375
3402
  var INTEGRATION_ID = PROVIDER_ID_PREFIX;
3376
3403
  var INTEGRATION_NAME = "Command Code";
3377
3404
  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
+ 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
+ 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";
3378
3407
  var LANES = [
3379
3408
  {
3380
3409
  id: "command-code-anthropic",
@@ -4889,6 +4918,29 @@ var commandCodeV2 = {
4889
4918
  };
4890
4919
  }
4891
4920
  });
4921
+ editor.add({
4922
+ name: "mpc_prices",
4923
+ description: MPC_TOOL_DESCRIPTION,
4924
+ input: {
4925
+ type: "object",
4926
+ properties: {
4927
+ arg: {
4928
+ type: "string",
4929
+ description: MPC_TOOL_ARG_DESCRIPTION
4930
+ }
4931
+ },
4932
+ additionalProperties: false
4933
+ },
4934
+ async execute(input) {
4935
+ const arg = typeof input?.arg === "string" ? input.arg : "";
4936
+ const key = await credentialKey(ctx);
4937
+ return {
4938
+ content: await runMpc(arg, {
4939
+ env: key ? { CMD_API_KEY: key } : undefined
4940
+ })
4941
+ };
4942
+ }
4943
+ });
4892
4944
  });
4893
4945
  let hasConnection = false;
4894
4946
  let connectionMs = 0;
@@ -5124,6 +5176,18 @@ var CommandCodePlugin = async (_input) => {
5124
5176
  env: { CMD_API_KEY: key }
5125
5177
  });
5126
5178
  }
5179
+ }),
5180
+ mpc_prices: tool({
5181
+ description: MPC_TOOL_DESCRIPTION,
5182
+ args: {
5183
+ arg: string2().optional().describe(MPC_TOOL_ARG_DESCRIPTION)
5184
+ },
5185
+ async execute(args) {
5186
+ const key = await resolveKey();
5187
+ return runMpc(args.arg ?? "", {
5188
+ env: { CMD_API_KEY: key }
5189
+ });
5190
+ }
5127
5191
  })
5128
5192
  }
5129
5193
  };
@@ -43,7 +43,7 @@ export interface ModelMeta {
43
43
  /** Cheapest plan that serves the model, from the generated catalog. */
44
44
  minPlan?: string | null;
45
45
  allowance?: number;
46
- /** Unbounded on the CommandCode side: `allowance` is 0 by convention. */
46
+ /** Unbounded on the CommandCode side: read `free`, not `allowance`, which may still carry a catalog figure. */
47
47
  free?: boolean;
48
48
  rates?: {
49
49
  input: number;
@@ -118,9 +118,11 @@ export interface Usage {
118
118
  export declare function money(value: number): string;
119
119
  /**
120
120
  * A monthly allowance as it appears on the panel. A free/unbounded model has no
121
- * budget to divide — mpc reports `free: true` and allowance 0 — so it reads `∞`
122
- * rather than the misleading `$0/mo` (0 is not a budget). Both sides call this,
123
- * so a free CommandCode model and a free OpenCode Go one render alike.
121
+ * budget to divide — mpc reports `free: true` — so it reads `∞` rather than the
122
+ * misleading `$0/mo` (0 is not a budget). `free` decides, so this holds whether
123
+ * the allowance is 0 (OpenCode Go's free rows) or a catalog figure
124
+ * (CommandCode's). Both sides call this, so a free CommandCode model and a free
125
+ * OpenCode Go one render alike.
124
126
  */
125
127
  export declare function allowanceLabel(limit: number, free?: boolean): string;
126
128
  /** Rate scale needs more precision than credit scale ($0.0036). */
@@ -128,6 +130,24 @@ export declare function rate(value: number): string;
128
130
  export declare function count(value: number): string;
129
131
  /** "3h 12m" / "1d 20h" / "<1m" for a reset epoch. */
130
132
  export declare function until(epoch: number | undefined, now?: number): string;
133
+ /** A rolling window as two or three rows: the use line, then resets plus whatever
134
+ * needs flagging, then an optional `extra` continuation. The lead segment follows
135
+ * cmduse's importance order — over-cap flag, then the cap ETA, then the
136
+ * informational elapsed share — because the 37-column budget fits exactly one of
137
+ * them beside the reset countdown.
138
+ *
139
+ * Shared by both panels: CommandCode passes four arguments (output unchanged),
140
+ * the OpenCode Go/Zen side adds the window's request count as `extra`, so
141
+ * `LIMIT EXCEEDED`, the tone, `cap in …`, `elapsed` and `resets` all have one
142
+ * implementation. */
143
+ export declare function windowRows(label: string, w: WindowUsage | undefined, now: number, durSecs: number, extra?: string): SidebarRow[];
144
+ /**
145
+ * The two store-backed rows for the active model — its requests under `Usage
146
+ * (this model)`, then this conversation's own totals under `Session`. They need
147
+ * no network, so both panels keep them when the catalog is missing, and both
148
+ * families build them here rather than in two places.
149
+ */
150
+ export declare function localUsageRows(usage?: ModelUsage, session?: ModelUsage): SidebarRow[];
131
151
  /** Plan + rolling windows + period totals, from a cmduse snapshot. */
132
152
  export declare function usageRows(usage: Usage | undefined, now?: number): SidebarRow[];
133
153
  /** Active model's allowance, rates and benchmarks, from mpc's catalog; its
@@ -14,7 +14,15 @@ export interface ZenModel {
14
14
  month: ZenTotals;
15
15
  }
16
16
  export interface ZenUsage {
17
+ /**
18
+ * Billing period start, epoch ms: `periodStart` on current ocuse, or
19
+ * `periodStartAt` once it publishes the bounds (the same instants cmduse
20
+ * 0.7.2+ carries).
21
+ */
17
22
  periodStart?: number;
23
+ /** Billing period end, epoch ms. Optional: older ocuse does not publish it
24
+ * and the Monthly row must then degrade to spend alone. */
25
+ periodEndAt?: number;
18
26
  source?: string;
19
27
  totals: {
20
28
  fiveHour: ZenTotals;
@@ -28,6 +36,7 @@ export interface ZenUsage {
28
36
  export declare function parseZenJson(text: string): ZenUsage;
29
37
  /** The OpenCode Go / Zen panel: period windows, a rule, then the active model.
30
38
  * Like `modelRows`, `fallbackName` (the session's own id) keeps the model block
31
- * — and the local `Session` figure — on screen when mpc's catalog is
32
- * unreachable; only the catalog-derived rows then drop out. */
33
- export declare function zenRows(usage: ZenUsage | undefined, meta: ModelMeta | undefined, session?: ModelUsage, fallbackName?: string): SidebarRow[];
39
+ * — and the local `Usage (this model)` / `Session` figures, which need no network
40
+ * — on screen when mpc's catalog is unreachable; only the catalog-derived rows
41
+ * then drop out. */
42
+ export declare function zenRows(usage: ZenUsage | undefined, meta: ModelMeta | undefined, modelUsage?: ModelUsage, session?: ModelUsage, fallbackName?: string): SidebarRow[];
package/dist/tui.js CHANGED
@@ -26,6 +26,11 @@ function cmduseCandidates(env = process.env) {
26
26
  return candidates.filter((bin) => Boolean(bin));
27
27
  }
28
28
  var CANDIDATES = cmduseCandidates();
29
+ function mpcCandidates(env = process.env) {
30
+ const candidates = [env.MPC_BIN, ...MPC_BIN_CANDIDATES];
31
+ return candidates.filter((bin) => Boolean(bin));
32
+ }
33
+ var MPC_CANDIDATES = mpcCandidates();
29
34
  function splitCliArgs(input) {
30
35
  const out = [];
31
36
  let cur = "";
@@ -78,7 +83,7 @@ function stderrTail(text) {
78
83
  return lines.slice(-3).join(`
79
84
  `).slice(-400);
80
85
  }
81
- function runOnce(bin, args, opts) {
86
+ function runOnce(bin, args, opts, name = "cmduse") {
82
87
  return new Promise((resolve, reject) => {
83
88
  const child = spawn(bin, args, {
84
89
  env: {
@@ -103,13 +108,13 @@ function runOnce(bin, args, opts) {
103
108
  opts.signal?.removeEventListener("abort", onAbort);
104
109
  const stdout = Buffer.concat(out).toString("utf8");
105
110
  if (opts.signal?.aborted) {
106
- reject(new Error("cmduse invocation aborted"));
111
+ reject(new Error(`${name} invocation aborted`));
107
112
  } else if (code === 0) {
108
113
  resolve(stdout);
109
114
  } else {
110
115
  const why = signal ? `killed by ${signal}` : `exit ${code}`;
111
116
  const tail = stderrTail(Buffer.concat(err).toString("utf8"));
112
- reject(new Error(`cmduse ${args.join(" ")}: ${why}${tail ? `
117
+ reject(new Error(`${name} ${args.join(" ")}: ${why}${tail ? `
113
118
  ${tail}` : ""}`));
114
119
  }
115
120
  });
@@ -616,7 +621,7 @@ function until(epoch, now = Date.now()) {
616
621
  const days = Math.floor(hours / 24);
617
622
  return hours % 24 ? `${days}d ${hours % 24}h` : `${days}d`;
618
623
  }
619
- function windowRows(label, w, now, durSecs) {
624
+ function windowRows(label, w, now, durSecs, extra) {
620
625
  if (!w || typeof w.cap !== "number" || w.cap <= 0)
621
626
  return [];
622
627
  const used = typeof w.used === "number" ? w.used : 0;
@@ -627,7 +632,23 @@ function windowRows(label, w, now, durSecs) {
627
632
  const reset = until(w.resetAt, now);
628
633
  const lead = w.exceeded ? "LIMIT EXCEEDED" : eta === undefined ? elapsed === undefined ? "" : `${elapsed} elapsed` : `cap in ${until(now + eta * 1000, now)}`;
629
634
  const detail = [lead, reset ? `resets ${reset}` : ""].filter(Boolean).join(" \xB7 ");
630
- return [[label, `${money(used)}/${money(w.cap)} (${pct.toFixed(1)}%)`, w.exceeded ? "crit" : pctTone(pct)], ...detail ? [["", detail, "muted"]] : []];
635
+ return [[label, `${money(used)}/${money(w.cap)} (${pct.toFixed(1)}%)`, w.exceeded ? "crit" : pctTone(pct)], ...detail ? [["", detail, "muted"]] : [], ...extra ? [["", extra, "muted"]] : []];
636
+ }
637
+ function localUsageRows(usage, session) {
638
+ const rows = [];
639
+ if (usage) {
640
+ rows.push(["Usage (this model)", `${count(usage.requests)} req`, "base"]);
641
+ const spend = usage.cost > 0 ? money(usage.cost) : "";
642
+ const share = usage.cost > 0 && session && session.cost > 0 ? `${Math.min(100, Math.round(usage.cost / session.cost * 100))}% of session` : "";
643
+ const detail = [spend, share].filter(Boolean).join(" \xB7 ");
644
+ if (detail)
645
+ rows.push(["", detail, "muted"]);
646
+ }
647
+ if (session) {
648
+ const spent = session.cost > 0 ? ` \xB7 ${money(session.cost)}` : "";
649
+ rows.push(["Session", `${count(session.requests)} req${spent}`, "base"]);
650
+ }
651
+ return rows;
631
652
  }
632
653
  function monthlyDetail(usage, now) {
633
654
  const {
@@ -676,19 +697,7 @@ function modelRows(meta, usage, session, fallbackName) {
676
697
  const name = meta?.name ?? fallbackName;
677
698
  if (!name)
678
699
  return [];
679
- const rows = [["Model", clip(name, valueWidth("Model")), "strong", true]];
680
- if (usage) {
681
- rows.push(["Usage (this model)", `${count(usage.requests)} req`, "base"]);
682
- const spend = usage.cost > 0 ? money(usage.cost) : "";
683
- const share = usage.cost > 0 && session && session.cost > 0 ? `${Math.min(100, Math.round(usage.cost / session.cost * 100))}% of session` : "";
684
- const detail = [spend, share].filter(Boolean).join(" \xB7 ");
685
- if (detail)
686
- rows.push(["", detail, "muted"]);
687
- }
688
- if (session) {
689
- const spent = session.cost > 0 ? ` \xB7 ${money(session.cost)}` : "";
690
- rows.push(["Session", `${count(session.requests)} req${spent}`, "base"]);
691
- }
700
+ const rows = [["Model", clip(name, valueWidth("Model")), "strong", true], ...localUsageRows(usage, session)];
692
701
  if (!meta)
693
702
  return rows;
694
703
  if (meta.tier)
@@ -2055,7 +2064,8 @@ function parseZenJson(text) {
2055
2064
  }
2056
2065
  const top = body.totals ?? {};
2057
2066
  return {
2058
- periodStart: typeof body.periodStart === "number" ? body.periodStart : undefined,
2067
+ periodStart: typeof body.periodStartAt === "number" ? body.periodStartAt : typeof body.periodStart === "number" ? body.periodStart : undefined,
2068
+ periodEndAt: typeof body.periodEndAt === "number" ? body.periodEndAt : undefined,
2059
2069
  source: typeof body.source === "string" ? body.source : undefined,
2060
2070
  totals: {
2061
2071
  fiveHour: totals(top.fiveHour),
@@ -2065,38 +2075,53 @@ function parseZenJson(text) {
2065
2075
  models
2066
2076
  };
2067
2077
  }
2068
- function windowRows2(label, spent, limit, share) {
2069
- if (typeof limit !== "number" || limit <= 0) {
2078
+ function monthPeriod(usage) {
2079
+ const {
2080
+ periodStart,
2081
+ periodEndAt
2082
+ } = usage;
2083
+ if (typeof periodStart !== "number" || typeof periodEndAt !== "number")
2084
+ return;
2085
+ const durSecs = Math.round((periodEndAt - periodStart) / 1000);
2086
+ if (durSecs <= 0)
2087
+ return;
2088
+ return {
2089
+ resetAt: periodEndAt,
2090
+ durSecs
2091
+ };
2092
+ }
2093
+ function zenWindow(label, spent, limit, share, now, durSecs, resetAt) {
2094
+ const cap = typeof limit === "number" && limit > 0 ? limit * share : undefined;
2095
+ if (cap === undefined) {
2070
2096
  return [[label, `${money(spent.cost)} \xB7 ${count(spent.requests)} req`, "base"]];
2071
2097
  }
2072
- const cap = limit * share;
2073
- const pct = cap > 0 ? spent.cost / cap * 100 : 0;
2074
- const rows = [[label, `${money(spent.cost)}/${money(cap)} (${pct.toFixed(1)}%)`, pctTone(pct)]];
2075
- if (spent.requests > 0)
2076
- rows.push(["", `${count(spent.requests)} req`, "muted"]);
2077
- return rows;
2078
- }
2079
- function zenRows(usage, meta, session, fallbackName) {
2098
+ return windowRows(label, {
2099
+ cap,
2100
+ used: spent.cost,
2101
+ resetAt,
2102
+ exceeded: spent.cost >= cap
2103
+ }, now, durSecs, spent.requests > 0 ? `${count(spent.requests)} req` : undefined);
2104
+ }
2105
+ function zenRows(usage, meta, modelUsage, session, fallbackName) {
2080
2106
  if (!usage)
2081
2107
  return [];
2108
+ const now = Date.now();
2082
2109
  const oc = meta?.oc;
2083
2110
  const rows = [["Plan", `${oc?.plan ?? "Go"} \xB7 limits per model`, "base"]];
2084
2111
  const mine = meta ? usage.models.get(modelKey(meta.name)) : undefined;
2085
2112
  const limit = oc?.allowance ?? mine?.limit;
2086
2113
  const cap = mine ? limit : undefined;
2087
- rows.push(...windowRows2("5-hour", mine?.fiveHour ?? usage.totals.fiveHour, cap, GO_WINDOW_SHARE.fiveHour));
2088
- rows.push(...windowRows2("Weekly", mine?.weekly ?? usage.totals.weekly, cap, GO_WINDOW_SHARE.weekly));
2089
- rows.push(...windowRows2("Month", mine?.month ?? usage.totals.month, cap, GO_WINDOW_SHARE.month));
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
+ 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));
2090
2118
  rows.push(["Period", `${count(usage.totals.month.requests)} requests \xB7 ${money(usage.totals.month.cost)}`, "base"]);
2091
2119
  rows.push(separator());
2092
2120
  const name = meta?.name ?? fallbackName;
2093
2121
  if (!name)
2094
2122
  return rows;
2095
2123
  rows.push(["Model", clip(name, valueWidth("Model")), "strong", true]);
2096
- if (session) {
2097
- const spent = session.cost > 0 ? ` \xB7 ${money(session.cost)}` : "";
2098
- rows.push(["Session", `${count(session.requests)} req${spent}`, "base"]);
2099
- }
2124
+ rows.push(...localUsageRows(modelUsage, session));
2100
2125
  if (!meta)
2101
2126
  return rows;
2102
2127
  if (typeof limit === "number") {
@@ -2498,6 +2523,7 @@ function useRows(activeModelId, providerID, sessionID) {
2498
2523
  refreshModelUsage(activeModelId());
2499
2524
  } else if (kind === "go" || kind === "zen") {
2500
2525
  setZen(await loadZen(loadPrefs().ocPlan));
2526
+ refreshModelUsage(activeModelId());
2501
2527
  }
2502
2528
  } catch {} finally {
2503
2529
  inFlight = false;
@@ -2515,7 +2541,7 @@ function useRows(activeModelId, providerID, sessionID) {
2515
2541
  createEffect(() => {
2516
2542
  const id = activeModelId();
2517
2543
  const session = sessionID();
2518
- if (sessionKind(providerID()) !== "ours" || !id || !session)
2544
+ if (!shows(sessionKind(providerID())) || !id || !session)
2519
2545
  return;
2520
2546
  refreshModelUsage(id);
2521
2547
  });
@@ -2535,7 +2561,7 @@ function useRows(activeModelId, providerID, sessionID) {
2535
2561
  const found = id ? meta().get(modelKey(id)) ?? meta().get(id) : undefined;
2536
2562
  const fallbackName = id ? id.split("/").pop() ?? id : undefined;
2537
2563
  if (kind === "go" || kind === "zen") {
2538
- return zenRows(zen(), found, sessionUsage(), fallbackName);
2564
+ return zenRows(zen(), found, usageFor(id), sessionUsage(), fallbackName);
2539
2565
  }
2540
2566
  const model = found ? {
2541
2567
  ...found,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeffreyjyz/opencode-command-code",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
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",