@jeffreyjyz/opencode-command-code 0.3.14 → 0.3.16

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.
@@ -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
@@ -51,6 +51,14 @@ export interface ModelMeta {
51
51
  };
52
52
  intelligence?: number;
53
53
  tps?: number;
54
+ /**
55
+ * Promotion CommandCode is running on this model (0.3.13): the badge from
56
+ * the docs row, with its expiry line when the page publishes one.
57
+ */
58
+ deal?: {
59
+ badge: string;
60
+ ends?: string;
61
+ };
54
62
  /**
55
63
  * The OpenCode Go / Zen side of the same mpc row (0.4.0): the CommandCode
56
64
  * fields above are the default, this is what a Go/Zen session reads.
@@ -93,6 +101,14 @@ export interface Usage {
93
101
  periodEnd?: string;
94
102
  requests?: number;
95
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;
96
112
  }
97
113
  /** Money at credit scale: cents, but "$60" not "$60.00". */
98
114
  export declare function money(value: number): string;
@@ -105,8 +121,14 @@ export declare function until(epoch: number | undefined, now?: number): string;
105
121
  export declare function usageRows(usage: Usage | undefined, now?: number): SidebarRow[];
106
122
  /** Active model's allowance, rates and benchmarks, from mpc's catalog; its
107
123
  * period usage (when the store has it) rides directly under the model name, and
108
- * this session's own totals under that (0.4.0). */
109
- 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[];
110
132
  /**
111
133
  * Canonical key for a model id, mirroring mpc's normalizeKey: lowercase, drop
112
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,24 +603,35 @@ 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
- const spent = usage.cost > 0 ? ` \xB7 ${money(usage.cost)}` : "";
612
- rows.push(["Usage (this model)", `${count(usage.requests)} req${spent}`, "base"]);
612
+ rows.push(["Usage (this model)", `${count(usage.requests)} req`, "base"]);
613
+ const spend = usage.cost > 0 ? money(usage.cost) : "";
614
+ const share = usage.cost > 0 && session && session.cost > 0 ? `${Math.round(usage.cost / session.cost * 100)}% of session` : "";
615
+ const detail = [spend, share].filter(Boolean).join(" \xB7 ");
616
+ if (detail)
617
+ rows.push(["", detail, "muted"]);
613
618
  }
614
619
  if (session) {
615
620
  const spent = session.cost > 0 ? ` \xB7 ${money(session.cost)}` : "";
616
621
  rows.push(["Session", `${count(session.requests)} req${spent}`, "base"]);
617
622
  }
623
+ if (!meta)
624
+ return rows;
618
625
  if (meta.tier)
619
626
  rows.push(["Tier", TIER_DISPLAY[meta.tier], "base"]);
620
627
  else if (meta.minPlan)
621
628
  rows.push(["Min plan", meta.minPlan, "base"]);
622
629
  if (typeof meta.allowance === "number")
623
630
  rows.push(["Allowance", `${money(meta.allowance)}/mo`, "base"]);
631
+ if (meta.deal) {
632
+ const ends = meta.deal.ends?.match(/([A-Z][a-z]{2})[a-z]*\s+(\d{1,2})/);
633
+ rows.push(["Deal", ends ? `${meta.deal.badge} \xB7 ends ${ends[1]} ${ends[2]}` : meta.deal.badge, "ok"]);
634
+ }
624
635
  if (meta.rates) {
625
636
  const cache = [`cache read ${rate(meta.rates.cacheRead)}`];
626
637
  if (typeof meta.rates.cacheWrite === "number" && meta.rates.cacheWrite > 0) {
@@ -1919,7 +1930,7 @@ function windowRows2(label, spent, limit, share) {
1919
1930
  rows.push(["", `${count(spent.requests)} req`, "muted"]);
1920
1931
  return rows;
1921
1932
  }
1922
- function zenRows(usage, meta, session) {
1933
+ function zenRows(usage, meta, session, fallbackName) {
1923
1934
  if (!usage)
1924
1935
  return [];
1925
1936
  const oc = meta?.oc;
@@ -1931,13 +1942,16 @@ function zenRows(usage, meta, session) {
1931
1942
  rows.push(...windowRows2("Month", mine?.month ?? usage.totals.month, limit, GO_WINDOW_SHARE.month));
1932
1943
  rows.push(["Period", `${count(usage.totals.month.requests)} requests \xB7 ${money(usage.totals.month.cost)}`, "base"]);
1933
1944
  rows.push(separator());
1934
- if (!meta)
1945
+ const name = meta?.name ?? fallbackName;
1946
+ if (!name)
1935
1947
  return rows;
1936
- rows.push(["Model", clip(meta.name, valueWidth("Model")), "strong", true]);
1948
+ rows.push(["Model", clip(name, valueWidth("Model")), "strong", true]);
1937
1949
  if (session) {
1938
1950
  const spent = session.cost > 0 ? ` \xB7 ${money(session.cost)}` : "";
1939
1951
  rows.push(["Session", `${count(session.requests)} req${spent}`, "base"]);
1940
1952
  }
1953
+ if (!meta)
1954
+ return rows;
1941
1955
  if (typeof limit === "number") {
1942
1956
  rows.push(["Allowance", `${money(limit)}/mo`, "base"]);
1943
1957
  }
@@ -1961,20 +1975,36 @@ function zenRows(usage, meta, session) {
1961
1975
  var CMDUSE = cmduseCandidates();
1962
1976
  var MPC = ["mpc", join2(homedir2(), ".bun/bin/mpc"), "/opt/homebrew/bin/mpc"];
1963
1977
  var OCUSE = [process.env.OCUSE_BIN, "ocuse", "/opt/homebrew/bin/ocuse", "/usr/local/bin/ocuse"].filter((bin) => Boolean(bin));
1964
- var CACHE_TTL_MS = 6 * 60 * 60 * 1000;
1978
+ var CACHE_TTL_MS = 60 * 60 * 1000;
1965
1979
  var SPAWN_OPTIONS = {
1966
1980
  stdio: ["ignore", "pipe", "pipe"],
1967
1981
  detached: true
1968
1982
  };
1983
+ var CHILD_TIMEOUT_MS = 30000;
1969
1984
  function run(bin, args) {
1970
1985
  return new Promise((resolve, reject) => {
1971
1986
  const child = spawn2(bin, args, SPAWN_OPTIONS);
1972
1987
  const out = [];
1973
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
+ };
1974
2004
  child.stdout?.on("data", (d) => out.push(d));
1975
2005
  child.stderr?.on("data", (d) => err.push(d));
1976
- child.on("error", reject);
1977
- 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)}`))));
1978
2008
  });
1979
2009
  }
1980
2010
  async function runFirst(candidates, args) {
@@ -2014,9 +2044,15 @@ function parseUsageJson(text) {
2014
2044
  periodStartAt: typeof raw.periodStartAt === "number" ? raw.periodStartAt : undefined,
2015
2045
  periodEndAt: typeof raw.periodEndAt === "number" ? raw.periodEndAt : undefined,
2016
2046
  requests: typeof summary.requests === "number" ? summary.requests : undefined,
2017
- 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
2018
2049
  };
2019
2050
  }
2051
+ function requireSnapshot(usage) {
2052
+ if (usage.error)
2053
+ throw new Error(usage.error);
2054
+ return usage;
2055
+ }
2020
2056
  function parseMpcJson(text) {
2021
2057
  const body = JSON.parse(text);
2022
2058
  const meta = new Map;
@@ -2032,6 +2068,10 @@ function parseMpcJson(text) {
2032
2068
  rates: cc?.pricing,
2033
2069
  intelligence: cc?.ability ?? undefined,
2034
2070
  tps: cc?.tps ?? undefined,
2071
+ deal: cc?.deal?.badge !== undefined ? {
2072
+ badge: cc.deal.badge,
2073
+ ends: cc.deal.ends
2074
+ } : undefined,
2035
2075
  oc: oc ? {
2036
2076
  provider: oc.provider,
2037
2077
  plan: oc.plan,
@@ -2071,7 +2111,7 @@ async function loadMeta() {
2071
2111
  return meta;
2072
2112
  }
2073
2113
  async function loadUsage() {
2074
- return parseUsageJson(await runFirst(CMDUSE, ["-1", "--json", "--plain"]));
2114
+ return requireSnapshot(parseUsageJson(await runFirst(CMDUSE, ["-1", "--json", "--plain"])));
2075
2115
  }
2076
2116
  async function loadZen() {
2077
2117
  return parseZenJson(await runFirst(OCUSE, ["-1", "--json"]));
@@ -2105,20 +2145,25 @@ function loadModelUsage(modelID, sinceMs, path = usageDbPath(), sessionID) {
2105
2145
  }
2106
2146
  try {
2107
2147
  if (hasV2(db)) {
2108
- const rows = db.query(`SELECT json_extract(data, '$.model.id') AS model, count(*) AS c
2148
+ const rows = db.query(`SELECT json_extract(data, '$.model.id') AS model,
2149
+ count(*) AS c,
2150
+ coalesce(sum(json_extract(data, '$.cost')), 0) AS cost
2109
2151
  FROM session_message
2110
2152
  WHERE type = 'assistant'
2111
2153
  ${sessionID ? "AND session_id = ?" : "AND time_created >= ?"}
2112
2154
  GROUP BY model`).all(sessionID ?? sinceMs);
2113
2155
  const key = modelKey(modelID);
2114
2156
  let requests = 0;
2157
+ let cost = 0;
2115
2158
  for (const row of rows) {
2116
- if (row.model && modelKey(row.model) === key)
2159
+ if (row.model && modelKey(row.model) === key) {
2117
2160
  requests += row.c;
2161
+ cost += row.cost;
2162
+ }
2118
2163
  }
2119
2164
  return {
2120
2165
  requests,
2121
- cost: 0
2166
+ cost
2122
2167
  };
2123
2168
  }
2124
2169
  const rows = db.query(`SELECT data, time_created FROM message
@@ -2206,7 +2251,7 @@ function loadSessionUsage(sessionID, path = usageDbPath()) {
2206
2251
  }
2207
2252
 
2208
2253
  // src/sidebar/useRows.ts
2209
- var POLL_MS = 20000;
2254
+ var POLL_MS = 5000;
2210
2255
  function sessionKind(providerID) {
2211
2256
  if (!providerID)
2212
2257
  return "unknown";
@@ -2249,14 +2294,29 @@ function seededModelUsage(id) {
2249
2294
  return lastModelUsage.usage;
2250
2295
  return lastModelUsage.key === modelKey(id) ? lastModelUsage.usage : undefined;
2251
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
+ }
2252
2305
  function useRows(activeModelId, providerID, sessionID) {
2253
2306
  const [usage, setUsage] = createSignal(seededRows());
2254
2307
  const [zen, setZen] = createSignal();
2255
- const [meta, setMeta] = createSignal(new Map);
2308
+ const [meta, setMeta] = createSignal(seededMeta());
2256
2309
  const [modelUsage, setModelUsage] = createSignal(lastModelUsage);
2257
2310
  const [sessionUsage, setSessionUsage] = createSignal();
2258
2311
  const [wasOurs, setWasOurs] = createSignal(false);
2259
- loadMeta().then(setMeta).catch(() => {});
2312
+ const refreshMeta = () => {
2313
+ loadMeta().then((next) => {
2314
+ rememberMeta(next);
2315
+ if (next.size > 0)
2316
+ setMeta(next);
2317
+ }).catch(() => {});
2318
+ };
2319
+ refreshMeta();
2260
2320
  const refreshModelUsage = (id) => {
2261
2321
  if (!id)
2262
2322
  return;
@@ -2265,12 +2325,17 @@ function useRows(activeModelId, providerID, sessionID) {
2265
2325
  rememberModelUsage(id, fresh);
2266
2326
  setModelUsage(lastModelUsage);
2267
2327
  };
2328
+ let inFlight = false;
2268
2329
  const refresh = async () => {
2269
- const kind = sessionKind(providerID());
2270
- const session = sessionID();
2271
- if (session)
2272
- setSessionUsage(loadSessionUsage(session) ?? undefined);
2330
+ if (inFlight)
2331
+ return;
2332
+ inFlight = true;
2273
2333
  try {
2334
+ const kind = sessionKind(providerID());
2335
+ const session = sessionID();
2336
+ refreshMeta();
2337
+ if (session)
2338
+ setSessionUsage(loadSessionUsage(session) ?? undefined);
2274
2339
  if (kind === "ours") {
2275
2340
  const snapshot = await loadUsage();
2276
2341
  rememberSnapshot(snapshot);
@@ -2279,7 +2344,9 @@ function useRows(activeModelId, providerID, sessionID) {
2279
2344
  } else if (kind === "go" || kind === "zen") {
2280
2345
  setZen(await loadZen());
2281
2346
  }
2282
- } catch {}
2347
+ } catch {} finally {
2348
+ inFlight = false;
2349
+ }
2283
2350
  };
2284
2351
  createEffect(() => {
2285
2352
  const kind = sessionKind(providerID());
@@ -2310,15 +2377,16 @@ function useRows(activeModelId, providerID, sessionID) {
2310
2377
  return [];
2311
2378
  const id = activeModelId();
2312
2379
  const found = id ? meta().get(modelKey(id)) ?? meta().get(id) : undefined;
2380
+ const fallbackName = id ? id.split("/").pop() ?? id : undefined;
2313
2381
  if (kind === "go" || kind === "zen") {
2314
- return zenRows(zen(), found, sessionUsage());
2382
+ return zenRows(zen(), found, sessionUsage(), fallbackName);
2315
2383
  }
2316
2384
  const model = found ? {
2317
2385
  ...found,
2318
2386
  tier: found.tier ?? (id ? tierFor(id) : undefined),
2319
2387
  minPlan: id ? minPlan(id) : undefined
2320
2388
  } : undefined;
2321
- return [...usage(), separator(), ...modelRows(model, usageFor(id), sessionUsage())];
2389
+ return [...usage(), separator(), ...modelRows(model, usageFor(id), sessionUsage(), fallbackName)];
2322
2390
  });
2323
2391
  }
2324
2392
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeffreyjyz/opencode-command-code",
3
- "version": "0.3.14",
3
+ "version": "0.3.16",
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",