@jeffreyjyz/opencode-command-code 0.2.7 → 0.2.9

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.
@@ -1,4 +1,11 @@
1
+ import { spawn } from "node:child_process";
1
2
  import { type ModelMeta, type Usage } from "./rows";
3
+ /** `detached` is load-bearing, not tidiness: it starts the child in its own
4
+ * session, so it has no controlling terminal. cmduse's snapshot() paints a
5
+ * "fetching usage…" spinner straight to /dev/tty — piping stdout/stderr does
6
+ * not stop it — which would corrupt the opencode TUI this process inherits its
7
+ * tty from. Detaching makes that open fail; stdout/stderr stay piped. */
8
+ export declare const SPAWN_OPTIONS: Parameters<typeof spawn>[2];
2
9
  /** Parse `cmduse -1 --json` into the usage snapshot. */
3
10
  export declare function parseUsageJson(text: string): Usage;
4
11
  /** Parse `mpc --json` into per-model meta, CommandCode side only. */
@@ -19,6 +19,11 @@ export interface WindowUsage {
19
19
  used?: number;
20
20
  resetAt?: number;
21
21
  }
22
+ /** One model's usage for the account's current billing period. */
23
+ export interface ModelUsage {
24
+ requests: number;
25
+ cost: number;
26
+ }
22
27
  export interface Usage {
23
28
  plan?: string;
24
29
  monthlyCap?: number;
@@ -38,8 +43,9 @@ export declare function count(value: number): string;
38
43
  export declare function until(epoch: number | undefined, now?: number): string;
39
44
  /** Plan + rolling windows + period totals, from a cmduse snapshot. */
40
45
  export declare function usageRows(usage: Usage | undefined, now?: number): SidebarRow[];
41
- /** Active model's allowance, rates and benchmarks, from mpc's catalog. */
42
- export declare function modelRows(meta: ModelMeta | undefined): SidebarRow[];
46
+ /** Active model's allowance, rates and benchmarks, from mpc's catalog; its
47
+ * period usage (when the store has it) rides directly under the model name. */
48
+ export declare function modelRows(meta: ModelMeta | undefined, usage?: ModelUsage): SidebarRow[];
43
49
  /**
44
50
  * Canonical key for a model id, mirroring mpc's normalizeKey: lowercase, drop
45
51
  * the vendor prefix and any parenthetical, strip punctuation. Used to match the
@@ -0,0 +1,16 @@
1
+ import { type ModelUsage } from "./rows";
2
+ /** opencode's message store: `OPENCODE_DB`, else the XDG data dir. */
3
+ export declare function usageDbPath(): string;
4
+ /**
5
+ * Start of the billing period a cmduse snapshot reports, as epoch ms. cmduse
6
+ * publishes only `periodEnd` (YYYY-MM-DD), so the start is that date one
7
+ * calendar month back. Falls back to the last 30 days when it is missing.
8
+ */
9
+ export declare function periodStart(periodEnd: string | undefined, now?: number): number;
10
+ /**
11
+ * Requests and cost for one model since `sinceMs`. Null when the store is
12
+ * missing or unreadable, so callers can fall back to omitting the row.
13
+ * Matching is on the canonical key: the session id and the stored modelID can
14
+ * differ by vendor prefix/punctuation.
15
+ */
16
+ export declare function loadModelUsage(modelID: string, sinceMs: number, path?: string): ModelUsage | null;
package/dist/tui.js CHANGED
@@ -9,7 +9,7 @@ import { createTextNode as _$createTextNode } from "@opentui/solid";
9
9
  import { insertNode as _$insertNode } from "@opentui/solid";
10
10
  import { createElement as _$createElement } from "@opentui/solid";
11
11
  import { Plugin } from "@opencode/plugin/tui";
12
- import { For, Show, createMemo, createSignal, onCleanup } from "solid-js";
12
+ import { For, Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js";
13
13
 
14
14
  // src/cli.ts
15
15
  import { spawn } from "child_process";
@@ -462,10 +462,14 @@ function usageRows(usage, now = Date.now()) {
462
462
  }
463
463
  return rows;
464
464
  }
465
- function modelRows(meta) {
465
+ function modelRows(meta, usage) {
466
466
  if (!meta)
467
467
  return [];
468
468
  const rows = [["Model", meta.name]];
469
+ if (usage) {
470
+ const spent = usage.cost > 0 ? ` \xB7 ${money(usage.cost)}` : "";
471
+ rows.push(["Usage", `${count(usage.requests)} req${spent}`]);
472
+ }
469
473
  if (meta.tier)
470
474
  rows.push(["Tier", TIER_DISPLAY[meta.tier]]);
471
475
  if (typeof meta.allowance === "number")
@@ -493,15 +497,17 @@ function tierFor(modelId) {
493
497
  var CMDUSE = ["cmduse", "/opt/homebrew/bin/cmduse", "/usr/local/bin/cmduse"];
494
498
  var MPC = ["mpc", join(homedir(), ".bun/bin/mpc"), "/opt/homebrew/bin/mpc"];
495
499
  var CACHE_TTL_MS = 6 * 60 * 60 * 1000;
500
+ var SPAWN_OPTIONS = {
501
+ stdio: ["ignore", "pipe", "pipe"],
502
+ detached: true
503
+ };
496
504
  function run(bin, args) {
497
505
  return new Promise((resolve, reject) => {
498
- const child = spawn2(bin, args, {
499
- stdio: ["ignore", "pipe", "pipe"]
500
- });
506
+ const child = spawn2(bin, args, SPAWN_OPTIONS);
501
507
  const out = [];
502
508
  const err = [];
503
- child.stdout.on("data", (d) => out.push(d));
504
- child.stderr.on("data", (d) => err.push(d));
509
+ child.stdout?.on("data", (d) => out.push(d));
510
+ child.stderr?.on("data", (d) => err.push(d));
505
511
  child.on("error", reject);
506
512
  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)}`)));
507
513
  });
@@ -591,6 +597,72 @@ async function loadUsage() {
591
597
  return parseUsageJson(await runFirst(CMDUSE, ["-1", "--json", "--plain"]));
592
598
  }
593
599
 
600
+ // src/sidebar/usageDb.ts
601
+ import { Database } from "bun:sqlite";
602
+ import { homedir as homedir2 } from "os";
603
+ import { join as join2 } from "path";
604
+ function usageDbPath() {
605
+ if (process.env.OPENCODE_DB)
606
+ return process.env.OPENCODE_DB;
607
+ const base = process.env.XDG_DATA_HOME ?? join2(homedir2(), ".local", "share");
608
+ return join2(base, "opencode", "opencode.db");
609
+ }
610
+ function periodStart(periodEnd, now = Date.now()) {
611
+ const end = periodEnd ? Date.parse(`${periodEnd}T23:59:59`) : Number.NaN;
612
+ if (!Number.isFinite(end))
613
+ return now - 30 * 24 * 60 * 60 * 1000;
614
+ const start = new Date(end);
615
+ start.setMonth(start.getMonth() - 1);
616
+ return start.getTime();
617
+ }
618
+ function usage(value) {
619
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
620
+ }
621
+ function loadModelUsage(modelID, sinceMs, path = usageDbPath()) {
622
+ let db;
623
+ try {
624
+ db = new Database(path, {
625
+ readonly: true
626
+ });
627
+ } catch {
628
+ return null;
629
+ }
630
+ try {
631
+ const rows = db.query(`SELECT data, time_created FROM message
632
+ WHERE data LIKE '%"tokens"%' AND time_created >= ?`).all(sinceMs);
633
+ const key = modelKey(modelID);
634
+ const seen = new Set;
635
+ let requests = 0;
636
+ let cost = 0;
637
+ for (const row of rows) {
638
+ let data;
639
+ try {
640
+ data = JSON.parse(row.data);
641
+ } catch {
642
+ continue;
643
+ }
644
+ if (data.role !== "assistant" || !data.modelID || !data.tokens)
645
+ continue;
646
+ if (modelKey(data.modelID) !== key)
647
+ continue;
648
+ const id = `${data.modelID}@${row.time_created}`;
649
+ if (seen.has(id))
650
+ continue;
651
+ seen.add(id);
652
+ requests += 1;
653
+ cost += usage(data.cost);
654
+ }
655
+ return {
656
+ requests,
657
+ cost
658
+ };
659
+ } catch {
660
+ return null;
661
+ } finally {
662
+ db.close();
663
+ }
664
+ }
665
+
594
666
  // src/tui.tsx
595
667
  var ID = "command-code.tui";
596
668
  var POLL_MS = 30000;
@@ -626,15 +698,23 @@ function Panel(props) {
626
698
  function useRows(activeModelId, active) {
627
699
  const [usage, setUsage] = createSignal([]);
628
700
  const [meta, setMeta] = createSignal(new Map);
701
+ const [modelUsage, setModelUsage] = createSignal();
629
702
  loadMeta().then(setMeta).catch(() => {});
630
703
  const refresh = async () => {
631
704
  try {
632
- setUsage(usageRows(await loadUsage()));
705
+ const snapshot = await loadUsage();
706
+ setUsage(usageRows(snapshot));
707
+ const id = activeModelId();
708
+ setModelUsage(id ? loadModelUsage(id, periodStart(snapshot.periodEnd)) ?? undefined : undefined);
633
709
  } catch {}
634
710
  };
635
- refresh();
636
- const timer = setInterval(() => void refresh(), POLL_MS);
637
- onCleanup(() => clearInterval(timer));
711
+ createEffect(() => {
712
+ if (!active())
713
+ return;
714
+ refresh();
715
+ const timer = setInterval(() => void refresh(), POLL_MS);
716
+ onCleanup(() => clearInterval(timer));
717
+ });
638
718
  return createMemo(() => {
639
719
  if (!active())
640
720
  return [];
@@ -644,7 +724,7 @@ function useRows(activeModelId, active) {
644
724
  ...found,
645
725
  tier: found.tier ?? (id ? tierFor(id) : undefined)
646
726
  } : undefined;
647
- return [...usage(), ...modelRows(model)];
727
+ return [...usage(), ...modelRows(model, modelUsage())];
648
728
  });
649
729
  }
650
730
  var isOurs = (providerID) => Boolean(providerID?.startsWith("command-code"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeffreyjyz/opencode-command-code",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
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",