@jeffreyjyz/opencode-command-code 0.5.0 → 0.5.3

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/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.0",
2417
+ version: "0.5.3",
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",
@@ -130,6 +130,24 @@ export declare function rate(value: number): string;
130
130
  export declare function count(value: number): string;
131
131
  /** "3h 12m" / "1d 20h" / "<1m" for a reset epoch. */
132
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[];
133
151
  /** Plan + rolling windows + period totals, from a cmduse snapshot. */
134
152
  export declare function usageRows(usage: Usage | undefined, now?: number): SidebarRow[];
135
153
  /** Active model's allowance, rates and benchmarks, from mpc's catalog; its
@@ -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 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
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
@@ -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;
@@ -26,8 +34,26 @@ export interface ZenUsage {
26
34
  }
27
35
  /** Parse `ocuse -1 --json`. Anything malformed reads as "no usage". */
28
36
  export declare function parseZenJson(text: string): ZenUsage;
37
+ /**
38
+ * The starts of the active model's rolling windows, epoch ms: the oldest request
39
+ * still inside each one, from opencode's own store. The Go/Zen panel turns each
40
+ * into a reset via `rollingReset`; absent when no such request exists.
41
+ */
42
+ export interface ZenWindowStarts {
43
+ fiveHour?: number;
44
+ weekly?: number;
45
+ }
46
+ /**
47
+ * A rolling window's reset, derived from the oldest request still inside it:
48
+ * that request leaves the window at `oldest + durSecs`, which is an honest reset
49
+ * epoch — unlike `now + durSecs`, a constant that would be noise in a usage
50
+ * panel. Undefined when there is no oldest request (an empty window), so the row
51
+ * keeps the same spend-only degrade it had before.
52
+ */
53
+ export declare function rollingReset(startMs: number | undefined, durSecs: number): number | undefined;
29
54
  /** The OpenCode Go / Zen panel: period windows, a rule, then the active model.
30
55
  * 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[];
56
+ * — and the local `Usage (this model)` / `Session` figures, which need no network
57
+ * — on screen when mpc's catalog is unreachable; only the catalog-derived rows
58
+ * then drop out. */
59
+ export declare function zenRows(usage: ZenUsage | undefined, meta: ModelMeta | undefined, modelUsage?: ModelUsage, session?: ModelUsage, fallbackName?: string, windows?: ZenWindowStarts): SidebarRow[];
package/dist/tui.js CHANGED
@@ -621,7 +621,7 @@ function until(epoch, now = Date.now()) {
621
621
  const days = Math.floor(hours / 24);
622
622
  return hours % 24 ? `${days}d ${hours % 24}h` : `${days}d`;
623
623
  }
624
- function windowRows(label, w, now, durSecs) {
624
+ function windowRows(label, w, now, durSecs, extra) {
625
625
  if (!w || typeof w.cap !== "number" || w.cap <= 0)
626
626
  return [];
627
627
  const used = typeof w.used === "number" ? w.used : 0;
@@ -632,7 +632,23 @@ function windowRows(label, w, now, durSecs) {
632
632
  const reset = until(w.resetAt, now);
633
633
  const lead = w.exceeded ? "LIMIT EXCEEDED" : eta === undefined ? elapsed === undefined ? "" : `${elapsed} elapsed` : `cap in ${until(now + eta * 1000, now)}`;
634
634
  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"]] : []];
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;
636
652
  }
637
653
  function monthlyDetail(usage, now) {
638
654
  const {
@@ -681,19 +697,7 @@ function modelRows(meta, usage, session, fallbackName) {
681
697
  const name = meta?.name ?? fallbackName;
682
698
  if (!name)
683
699
  return [];
684
- const rows = [["Model", clip(name, valueWidth("Model")), "strong", true]];
685
- if (usage) {
686
- rows.push(["Usage (this model)", `${count(usage.requests)} req`, "base"]);
687
- const spend = usage.cost > 0 ? money(usage.cost) : "";
688
- const share = usage.cost > 0 && session && session.cost > 0 ? `${Math.min(100, Math.round(usage.cost / session.cost * 100))}% of session` : "";
689
- const detail = [spend, share].filter(Boolean).join(" \xB7 ");
690
- if (detail)
691
- rows.push(["", detail, "muted"]);
692
- }
693
- if (session) {
694
- const spent = session.cost > 0 ? ` \xB7 ${money(session.cost)}` : "";
695
- rows.push(["Session", `${count(session.requests)} req${spent}`, "base"]);
696
- }
700
+ const rows = [["Model", clip(name, valueWidth("Model")), "strong", true], ...localUsageRows(usage, session)];
697
701
  if (!meta)
698
702
  return rows;
699
703
  if (meta.tier)
@@ -2060,7 +2064,8 @@ function parseZenJson(text) {
2060
2064
  }
2061
2065
  const top = body.totals ?? {};
2062
2066
  return {
2063
- 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,
2064
2069
  source: typeof body.source === "string" ? body.source : undefined,
2065
2070
  totals: {
2066
2071
  fiveHour: totals(top.fiveHour),
@@ -2070,43 +2075,63 @@ function parseZenJson(text) {
2070
2075
  models
2071
2076
  };
2072
2077
  }
2073
- function windowRows2(label, spent, limit, share) {
2074
- 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 rollingReset(startMs, durSecs) {
2094
+ if (typeof startMs !== "number" || !Number.isFinite(startMs))
2095
+ return;
2096
+ return startMs + durSecs * 1000;
2097
+ }
2098
+ function zenWindow(label, spent, limit, share, now, durSecs, resetAt) {
2099
+ const cap = typeof limit === "number" && limit > 0 ? limit * share : undefined;
2100
+ if (cap === undefined) {
2075
2101
  return [[label, `${money(spent.cost)} \xB7 ${count(spent.requests)} req`, "base"]];
2076
2102
  }
2077
- const cap = limit * share;
2078
- const pct = cap > 0 ? spent.cost / cap * 100 : 0;
2079
- const rows = [[label, `${money(spent.cost)}/${money(cap)} (${pct.toFixed(1)}%)`, pctTone(pct)]];
2080
- if (spent.requests > 0)
2081
- rows.push(["", `${count(spent.requests)} req`, "muted"]);
2082
- return rows;
2083
- }
2084
- function zenRows(usage, meta, session, fallbackName) {
2103
+ return windowRows(label, {
2104
+ cap,
2105
+ used: spent.cost,
2106
+ resetAt,
2107
+ exceeded: spent.cost >= cap
2108
+ }, now, durSecs, spent.requests > 0 ? `${count(spent.requests)} req` : undefined);
2109
+ }
2110
+ function zenRows(usage, meta, modelUsage, session, fallbackName, windows) {
2085
2111
  if (!usage)
2086
2112
  return [];
2113
+ const now = Date.now();
2087
2114
  const oc = meta?.oc;
2088
2115
  const rows = [["Plan", `${oc?.plan ?? "Go"} \xB7 limits per model`, "base"]];
2089
- const mine = meta ? usage.models.get(modelKey(meta.name)) : undefined;
2116
+ const mine = usage.models.get(modelKey(fallbackName ?? "")) ?? (meta ? usage.models.get(modelKey(meta.name)) : undefined);
2090
2117
  const limit = oc?.allowance ?? mine?.limit;
2091
2118
  const cap = mine ? limit : undefined;
2092
- rows.push(...windowRows2("5-hour", mine?.fiveHour ?? usage.totals.fiveHour, cap, GO_WINDOW_SHARE.fiveHour));
2093
- rows.push(...windowRows2("Weekly", mine?.weekly ?? usage.totals.weekly, cap, GO_WINDOW_SHARE.weekly));
2094
- rows.push(...windowRows2("Month", mine?.month ?? usage.totals.month, cap, GO_WINDOW_SHARE.month));
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)));
2121
+ 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));
2095
2123
  rows.push(["Period", `${count(usage.totals.month.requests)} requests \xB7 ${money(usage.totals.month.cost)}`, "base"]);
2096
2124
  rows.push(separator());
2097
2125
  const name = meta?.name ?? fallbackName;
2098
2126
  if (!name)
2099
2127
  return rows;
2100
2128
  rows.push(["Model", clip(name, valueWidth("Model")), "strong", true]);
2101
- if (session) {
2102
- const spent = session.cost > 0 ? ` \xB7 ${money(session.cost)}` : "";
2103
- rows.push(["Session", `${count(session.requests)} req${spent}`, "base"]);
2104
- }
2105
- if (!meta)
2106
- return rows;
2129
+ rows.push(...localUsageRows(modelUsage, session));
2107
2130
  if (typeof limit === "number") {
2108
2131
  rows.push(["Allowance", allowanceLabel(limit, oc?.free), "base"]);
2109
2132
  }
2133
+ if (!meta)
2134
+ return rows;
2110
2135
  if (oc?.rates) {
2111
2136
  const cache = [`cache read ${rate(oc.rates.cacheRead)}`];
2112
2137
  if (typeof oc.rates.cacheWrite === "number" && oc.rates.cacheWrite > 0) {
@@ -2355,6 +2380,53 @@ function loadModelUsage(modelID, sinceMs, path = usageDbPath(), sessionID) {
2355
2380
  db.close();
2356
2381
  }
2357
2382
  }
2383
+ function loadModelWindowStart(modelID, sinceMs, path = usageDbPath()) {
2384
+ let db;
2385
+ try {
2386
+ db = new Database(path, {
2387
+ readonly: true
2388
+ });
2389
+ } catch {
2390
+ return null;
2391
+ }
2392
+ try {
2393
+ const key = modelKey(modelID);
2394
+ if (hasV2(db)) {
2395
+ const rows = db.query(`SELECT json_extract(data, '$.model.id') AS model,
2396
+ min(time_created) AS start
2397
+ FROM session_message
2398
+ WHERE type = 'assistant' AND time_created >= ?
2399
+ GROUP BY model`).all(sinceMs);
2400
+ for (const row of rows) {
2401
+ if (row.model && modelKey(row.model) === key)
2402
+ return row.start;
2403
+ }
2404
+ return null;
2405
+ }
2406
+ const rows = db.query(`SELECT data, time_created FROM message
2407
+ WHERE data LIKE '%"tokens"%' AND time_created >= ?`).all(sinceMs);
2408
+ let oldest = null;
2409
+ for (const row of rows) {
2410
+ let data;
2411
+ try {
2412
+ data = JSON.parse(row.data);
2413
+ } catch {
2414
+ continue;
2415
+ }
2416
+ if (data.role !== "assistant" || !data.tokens || !data.modelID)
2417
+ continue;
2418
+ if (modelKey(data.modelID) !== key)
2419
+ continue;
2420
+ if (oldest === null || row.time_created < oldest)
2421
+ oldest = row.time_created;
2422
+ }
2423
+ return oldest;
2424
+ } catch {
2425
+ return null;
2426
+ } finally {
2427
+ db.close();
2428
+ }
2429
+ }
2358
2430
  function loadSessionUsage(sessionID, path = usageDbPath()) {
2359
2431
  let db;
2360
2432
  try {
@@ -2465,6 +2537,7 @@ function useRows(activeModelId, providerID, sessionID) {
2465
2537
  const [meta, setMeta] = createSignal(seededMeta());
2466
2538
  const [modelUsage, setModelUsage] = createSignal(lastModelUsage);
2467
2539
  const [sessionUsage, setSessionUsage] = createSignal();
2540
+ const [modelWindows, setModelWindows] = createSignal();
2468
2541
  const [wasOurs, setWasOurs] = createSignal(false);
2469
2542
  const refreshMeta = () => {
2470
2543
  loadMeta(loadPrefs().ocPlan).then((next) => {
@@ -2485,6 +2558,15 @@ function useRows(activeModelId, providerID, sessionID) {
2485
2558
  rememberModelUsage(id, fresh, session);
2486
2559
  setModelUsage(lastModelUsage);
2487
2560
  };
2561
+ const refreshModelWindows = (id) => {
2562
+ if (!id)
2563
+ return;
2564
+ const now = Date.now();
2565
+ setModelWindows({
2566
+ fiveHour: loadModelWindowStart(id, now - FIVE_HOUR_SECS * 1000) ?? undefined,
2567
+ weekly: loadModelWindowStart(id, now - WEEKLY_SECS * 1000) ?? undefined
2568
+ });
2569
+ };
2488
2570
  let inFlight = false;
2489
2571
  const refresh = async () => {
2490
2572
  if (inFlight)
@@ -2503,6 +2585,8 @@ function useRows(activeModelId, providerID, sessionID) {
2503
2585
  refreshModelUsage(activeModelId());
2504
2586
  } else if (kind === "go" || kind === "zen") {
2505
2587
  setZen(await loadZen(loadPrefs().ocPlan));
2588
+ refreshModelUsage(activeModelId());
2589
+ refreshModelWindows(activeModelId());
2506
2590
  }
2507
2591
  } catch {} finally {
2508
2592
  inFlight = false;
@@ -2520,7 +2604,12 @@ function useRows(activeModelId, providerID, sessionID) {
2520
2604
  createEffect(() => {
2521
2605
  const id = activeModelId();
2522
2606
  const session = sessionID();
2523
- if (sessionKind(providerID()) !== "ours" || !id || !session)
2607
+ const kind = sessionKind(providerID());
2608
+ if (!shows(kind) || !id)
2609
+ return;
2610
+ if (kind === "go" || kind === "zen")
2611
+ refreshModelWindows(id);
2612
+ if (!session)
2524
2613
  return;
2525
2614
  refreshModelUsage(id);
2526
2615
  });
@@ -2540,7 +2629,7 @@ function useRows(activeModelId, providerID, sessionID) {
2540
2629
  const found = id ? meta().get(modelKey(id)) ?? meta().get(id) : undefined;
2541
2630
  const fallbackName = id ? id.split("/").pop() ?? id : undefined;
2542
2631
  if (kind === "go" || kind === "zen") {
2543
- return zenRows(zen(), found, sessionUsage(), fallbackName);
2632
+ return zenRows(zen(), found, usageFor(id), sessionUsage(), fallbackName, modelWindows());
2544
2633
  }
2545
2634
  const model = found ? {
2546
2635
  ...found,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeffreyjyz/opencode-command-code",
3
- "version": "0.5.0",
3
+ "version": "0.5.3",
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",