@jeffreyjyz/opencode-command-code 0.3.15 → 0.3.17

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.3.15",
2417
+ version: "0.3.17",
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",
@@ -9,6 +9,14 @@ import { type ZenUsage } from "./zen";
9
9
  export declare const SPAWN_OPTIONS: Parameters<typeof spawn>[2];
10
10
  /** Parse `cmduse -1 --json` into the usage snapshot. */
11
11
  export declare function parseUsageJson(text: string): Usage;
12
+ /**
13
+ * A snapshot is only usable when cmduse actually reached the account API.
14
+ * `-1 --json` exits 0 on failure and then carries the defaults (plan "Free", no
15
+ * windows) beside an `error`; painting that over a good snapshot is what made
16
+ * the Monthly row flash during an outage. Throw so the caller keeps the last
17
+ * snapshot, exactly as it does when the CLI is missing.
18
+ */
19
+ export declare function requireSnapshot(usage: Usage): Usage;
12
20
  /**
13
21
  * Parse `mpc --json` into per-model meta: the CommandCode fields at the top
14
22
  * level and the OpenCode Go / Zen side under `oc`, so one lookup serves either
@@ -17,7 +25,9 @@ export declare function parseUsageJson(text: string): Usage;
17
25
  export declare function parseMpcJson(text: string): Map<string, ModelMeta>;
18
26
  /** mpc's catalog, cached on disk: it scrapes live docs over the network. */
19
27
  export declare function loadMeta(): Promise<Map<string, ModelMeta>>;
20
- /** One usage snapshot from cmduse. Throws when cmduse is missing or fails. */
28
+ /** One usage snapshot from cmduse. Throws when cmduse is missing, fails, or
29
+ * returns the errored defaults its JSON carries when the account API is
30
+ * unreachable (see `requireSnapshot`). */
21
31
  export declare function loadUsage(): Promise<Usage>;
22
32
  /**
23
33
  * One OpenCode Go / Zen snapshot from ocuse, which reads opencode's own store
@@ -101,6 +101,14 @@ export interface Usage {
101
101
  periodEnd?: string;
102
102
  requests?: number;
103
103
  cost?: number;
104
+ /**
105
+ * cmduse's own fetch error. `cmduse -1 --json` still exits 0 when the account
106
+ * API is unreachable, and then publishes the *defaults* (plan "Free", no
107
+ * windows) beside this string — the human renderer hides that fake frame, and
108
+ * a consumer must too, or it paints "Free" with no Monthly row over a good
109
+ * snapshot. See `loadUsage`.
110
+ */
111
+ error?: string;
104
112
  }
105
113
  /** Money at credit scale: cents, but "$60" not "$60.00". */
106
114
  export declare function money(value: number): string;
@@ -113,8 +121,14 @@ export declare function until(epoch: number | undefined, now?: number): string;
113
121
  export declare function usageRows(usage: Usage | undefined, now?: number): SidebarRow[];
114
122
  /** Active model's allowance, rates and benchmarks, from mpc's catalog; its
115
123
  * period usage (when the store has it) rides directly under the model name, and
116
- * this session's own totals under that (0.4.0). */
117
- export declare function modelRows(meta: ModelMeta | undefined, usage?: ModelUsage, session?: ModelUsage): SidebarRow[];
124
+ * this session's own totals under that (0.4.0).
125
+ *
126
+ * `fallbackName` keeps the block on screen when the catalog is unavailable: the
127
+ * `Usage (this model)`/`Session` rows come from the local store and need no
128
+ * network, so an outage must not hide them behind a failed mpc spawn. The name
129
+ * is the only thing borrowed, and it is the session's own id when no catalog
130
+ * row names the model. */
131
+ export declare function modelRows(meta: ModelMeta | undefined, usage?: ModelUsage, session?: ModelUsage, fallbackName?: string): SidebarRow[];
118
132
  /**
119
133
  * Canonical key for a model id, mirroring mpc's normalizeKey: lowercase, drop
120
134
  * the vendor prefix and any parenthetical, strip punctuation. Used to match the
@@ -1,4 +1,4 @@
1
- import { type ModelUsage, type SidebarRow, type Usage } from "./rows";
1
+ import { type ModelMeta, type ModelUsage, type SidebarRow, type Usage } from "./rows";
2
2
  /**
3
3
  * Which family a session's provider belongs to, as far as this panel is
4
4
  * concerned. `unknown` is the frame during a session switch, before the host has
@@ -24,4 +24,10 @@ export declare function rememberModelUsage(id: string, usage: ModelUsage): void;
24
24
  * beats blanking it, the same call `wasOurs` makes for the account block.
25
25
  */
26
26
  export declare function seededModelUsage(id: string | undefined): ModelUsage | undefined;
27
+ /** Keep a catalog for the next panel that needs it. An empty map is never
28
+ * remembered: it cannot be distinguished from "mpc returned nothing" and would
29
+ * wipe a good catalog. */
30
+ export declare function rememberMeta(meta: Map<string, ModelMeta>): void;
31
+ /** Rows to paint before mpc's catalog is read again. */
32
+ export declare function seededMeta(): Map<string, ModelMeta>;
27
33
  export declare function useRows(activeModelId: () => string | undefined, providerID: () => string | undefined, sessionID: () => string | undefined): import("solid-js").Accessor<SidebarRow[]>;
@@ -32,5 +32,8 @@ export interface ZenUsage {
32
32
  }
33
33
  /** Parse `ocuse -1 --json`. Anything malformed reads as "no usage". */
34
34
  export declare function parseZenJson(text: string): ZenUsage;
35
- /** The OpenCode Go / Zen panel: period windows, a rule, then the active model. */
36
- export declare function zenRows(usage: ZenUsage | undefined, meta: ModelMeta | undefined, session?: ModelUsage): SidebarRow[];
35
+ /** The OpenCode Go / Zen panel: period windows, a rule, then the active model.
36
+ * Like `modelRows`, `fallbackName` (the session's own id) keeps the model block
37
+ * — and the local `Session` figure — on screen when mpc's catalog is
38
+ * unreachable; only the catalog-derived rows then drop out. */
39
+ export declare function zenRows(usage: ZenUsage | undefined, meta: ModelMeta | undefined, session?: ModelUsage, fallbackName?: string): SidebarRow[];
package/dist/tui.js CHANGED
@@ -603,10 +603,11 @@ function usageRows(usage, now = Date.now()) {
603
603
  }
604
604
  return rows;
605
605
  }
606
- function modelRows(meta, usage, session) {
607
- if (!meta)
606
+ function modelRows(meta, usage, session, fallbackName) {
607
+ const name = meta?.name ?? fallbackName;
608
+ if (!name)
608
609
  return [];
609
- const rows = [["Model", clip(meta.name, valueWidth("Model")), "strong", true]];
610
+ const rows = [["Model", clip(name, valueWidth("Model")), "strong", true]];
610
611
  if (usage) {
611
612
  rows.push(["Usage (this model)", `${count(usage.requests)} req`, "base"]);
612
613
  const spend = usage.cost > 0 ? money(usage.cost) : "";
@@ -619,6 +620,8 @@ function modelRows(meta, usage, session) {
619
620
  const spent = session.cost > 0 ? ` \xB7 ${money(session.cost)}` : "";
620
621
  rows.push(["Session", `${count(session.requests)} req${spent}`, "base"]);
621
622
  }
623
+ if (!meta)
624
+ return rows;
622
625
  if (meta.tier)
623
626
  rows.push(["Tier", TIER_DISPLAY[meta.tier], "base"]);
624
627
  else if (meta.minPlan)
@@ -1927,7 +1930,7 @@ function windowRows2(label, spent, limit, share) {
1927
1930
  rows.push(["", `${count(spent.requests)} req`, "muted"]);
1928
1931
  return rows;
1929
1932
  }
1930
- function zenRows(usage, meta, session) {
1933
+ function zenRows(usage, meta, session, fallbackName) {
1931
1934
  if (!usage)
1932
1935
  return [];
1933
1936
  const oc = meta?.oc;
@@ -1939,13 +1942,16 @@ function zenRows(usage, meta, session) {
1939
1942
  rows.push(...windowRows2("Month", mine?.month ?? usage.totals.month, limit, GO_WINDOW_SHARE.month));
1940
1943
  rows.push(["Period", `${count(usage.totals.month.requests)} requests \xB7 ${money(usage.totals.month.cost)}`, "base"]);
1941
1944
  rows.push(separator());
1942
- if (!meta)
1945
+ const name = meta?.name ?? fallbackName;
1946
+ if (!name)
1943
1947
  return rows;
1944
- rows.push(["Model", clip(meta.name, valueWidth("Model")), "strong", true]);
1948
+ rows.push(["Model", clip(name, valueWidth("Model")), "strong", true]);
1945
1949
  if (session) {
1946
1950
  const spent = session.cost > 0 ? ` \xB7 ${money(session.cost)}` : "";
1947
1951
  rows.push(["Session", `${count(session.requests)} req${spent}`, "base"]);
1948
1952
  }
1953
+ if (!meta)
1954
+ return rows;
1949
1955
  if (typeof limit === "number") {
1950
1956
  rows.push(["Allowance", `${money(limit)}/mo`, "base"]);
1951
1957
  }
@@ -1974,15 +1980,31 @@ var SPAWN_OPTIONS = {
1974
1980
  stdio: ["ignore", "pipe", "pipe"],
1975
1981
  detached: true
1976
1982
  };
1983
+ var CHILD_TIMEOUT_MS = 30000;
1977
1984
  function run(bin, args) {
1978
1985
  return new Promise((resolve, reject) => {
1979
1986
  const child = spawn2(bin, args, SPAWN_OPTIONS);
1980
1987
  const out = [];
1981
1988
  const err = [];
1989
+ let settled = false;
1990
+ const timer = setTimeout(() => {
1991
+ if (settled)
1992
+ return;
1993
+ settled = true;
1994
+ child.kill();
1995
+ reject(new Error(`${bin} ${args.join(" ")}: timed out after ${CHILD_TIMEOUT_MS}ms`));
1996
+ }, CHILD_TIMEOUT_MS);
1997
+ const done = (fn) => {
1998
+ if (settled)
1999
+ return;
2000
+ settled = true;
2001
+ clearTimeout(timer);
2002
+ fn();
2003
+ };
1982
2004
  child.stdout?.on("data", (d) => out.push(d));
1983
2005
  child.stderr?.on("data", (d) => err.push(d));
1984
- child.on("error", reject);
1985
- child.on("close", (code) => code === 0 ? resolve(Buffer.concat(out).toString("utf8")) : reject(new Error(`${bin} ${args.join(" ")}: exit ${code} ${Buffer.concat(err).toString("utf8").slice(-200)}`)));
2006
+ child.on("error", (e) => done(() => reject(e)));
2007
+ 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)}`))));
1986
2008
  });
1987
2009
  }
1988
2010
  async function runFirst(candidates, args) {
@@ -2022,9 +2044,15 @@ function parseUsageJson(text) {
2022
2044
  periodStartAt: typeof raw.periodStartAt === "number" ? raw.periodStartAt : undefined,
2023
2045
  periodEndAt: typeof raw.periodEndAt === "number" ? raw.periodEndAt : undefined,
2024
2046
  requests: typeof summary.requests === "number" ? summary.requests : undefined,
2025
- cost: typeof summary.cost === "number" ? summary.cost : undefined
2047
+ cost: typeof summary.cost === "number" ? summary.cost : undefined,
2048
+ error: typeof raw.error === "string" && raw.error ? raw.error : undefined
2026
2049
  };
2027
2050
  }
2051
+ function requireSnapshot(usage) {
2052
+ if (usage.error)
2053
+ throw new Error(usage.error);
2054
+ return usage;
2055
+ }
2028
2056
  function parseMpcJson(text) {
2029
2057
  const body = JSON.parse(text);
2030
2058
  const meta = new Map;
@@ -2083,7 +2111,7 @@ async function loadMeta() {
2083
2111
  return meta;
2084
2112
  }
2085
2113
  async function loadUsage() {
2086
- return parseUsageJson(await runFirst(CMDUSE, ["-1", "--json", "--plain"]));
2114
+ return requireSnapshot(parseUsageJson(await runFirst(CMDUSE, ["-1", "--json", "--plain"])));
2087
2115
  }
2088
2116
  async function loadZen() {
2089
2117
  return parseZenJson(await runFirst(OCUSE, ["-1", "--json"]));
@@ -2266,15 +2294,27 @@ function seededModelUsage(id) {
2266
2294
  return lastModelUsage.usage;
2267
2295
  return lastModelUsage.key === modelKey(id) ? lastModelUsage.usage : undefined;
2268
2296
  }
2297
+ var lastMeta;
2298
+ function rememberMeta(meta) {
2299
+ if (meta.size > 0)
2300
+ lastMeta = meta;
2301
+ }
2302
+ function seededMeta() {
2303
+ return lastMeta ?? new Map;
2304
+ }
2269
2305
  function useRows(activeModelId, providerID, sessionID) {
2270
2306
  const [usage, setUsage] = createSignal(seededRows());
2271
2307
  const [zen, setZen] = createSignal();
2272
- const [meta, setMeta] = createSignal(new Map);
2308
+ const [meta, setMeta] = createSignal(seededMeta());
2273
2309
  const [modelUsage, setModelUsage] = createSignal(lastModelUsage);
2274
2310
  const [sessionUsage, setSessionUsage] = createSignal();
2275
2311
  const [wasOurs, setWasOurs] = createSignal(false);
2276
2312
  const refreshMeta = () => {
2277
- loadMeta().then(setMeta).catch(() => {});
2313
+ loadMeta().then((next) => {
2314
+ rememberMeta(next);
2315
+ if (next.size > 0)
2316
+ setMeta(next);
2317
+ }).catch(() => {});
2278
2318
  };
2279
2319
  refreshMeta();
2280
2320
  const refreshModelUsage = (id) => {
@@ -2285,13 +2325,17 @@ function useRows(activeModelId, providerID, sessionID) {
2285
2325
  rememberModelUsage(id, fresh);
2286
2326
  setModelUsage(lastModelUsage);
2287
2327
  };
2328
+ let inFlight = false;
2288
2329
  const refresh = async () => {
2289
- const kind = sessionKind(providerID());
2290
- const session = sessionID();
2291
- refreshMeta();
2292
- if (session)
2293
- setSessionUsage(loadSessionUsage(session) ?? undefined);
2330
+ if (inFlight)
2331
+ return;
2332
+ inFlight = true;
2294
2333
  try {
2334
+ const kind = sessionKind(providerID());
2335
+ const session = sessionID();
2336
+ refreshMeta();
2337
+ if (session)
2338
+ setSessionUsage(loadSessionUsage(session) ?? undefined);
2295
2339
  if (kind === "ours") {
2296
2340
  const snapshot = await loadUsage();
2297
2341
  rememberSnapshot(snapshot);
@@ -2300,7 +2344,9 @@ function useRows(activeModelId, providerID, sessionID) {
2300
2344
  } else if (kind === "go" || kind === "zen") {
2301
2345
  setZen(await loadZen());
2302
2346
  }
2303
- } catch {}
2347
+ } catch {} finally {
2348
+ inFlight = false;
2349
+ }
2304
2350
  };
2305
2351
  createEffect(() => {
2306
2352
  const kind = sessionKind(providerID());
@@ -2331,15 +2377,16 @@ function useRows(activeModelId, providerID, sessionID) {
2331
2377
  return [];
2332
2378
  const id = activeModelId();
2333
2379
  const found = id ? meta().get(modelKey(id)) ?? meta().get(id) : undefined;
2380
+ const fallbackName = id ? id.split("/").pop() ?? id : undefined;
2334
2381
  if (kind === "go" || kind === "zen") {
2335
- return zenRows(zen(), found, sessionUsage());
2382
+ return zenRows(zen(), found, sessionUsage(), fallbackName);
2336
2383
  }
2337
2384
  const model = found ? {
2338
2385
  ...found,
2339
2386
  tier: found.tier ?? (id ? tierFor(id) : undefined),
2340
2387
  minPlan: id ? minPlan(id) : undefined
2341
2388
  } : undefined;
2342
- return [...usage(), separator(), ...modelRows(model, usageFor(id), sessionUsage())];
2389
+ return [...usage(), separator(), ...modelRows(model, usageFor(id), sessionUsage(), fallbackName)];
2343
2390
  });
2344
2391
  }
2345
2392
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeffreyjyz/opencode-command-code",
3
- "version": "0.3.15",
3
+ "version": "0.3.17",
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",