@jeffreyjyz/opencode-command-code 0.2.8 → 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.
@@ -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
@@ -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")
@@ -593,6 +597,72 @@ async function loadUsage() {
593
597
  return parseUsageJson(await runFirst(CMDUSE, ["-1", "--json", "--plain"]));
594
598
  }
595
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
+
596
666
  // src/tui.tsx
597
667
  var ID = "command-code.tui";
598
668
  var POLL_MS = 30000;
@@ -628,10 +698,14 @@ function Panel(props) {
628
698
  function useRows(activeModelId, active) {
629
699
  const [usage, setUsage] = createSignal([]);
630
700
  const [meta, setMeta] = createSignal(new Map);
701
+ const [modelUsage, setModelUsage] = createSignal();
631
702
  loadMeta().then(setMeta).catch(() => {});
632
703
  const refresh = async () => {
633
704
  try {
634
- 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);
635
709
  } catch {}
636
710
  };
637
711
  createEffect(() => {
@@ -650,7 +724,7 @@ function useRows(activeModelId, active) {
650
724
  ...found,
651
725
  tier: found.tier ?? (id ? tierFor(id) : undefined)
652
726
  } : undefined;
653
- return [...usage(), ...modelRows(model)];
727
+ return [...usage(), ...modelRows(model, modelUsage())];
654
728
  });
655
729
  }
656
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.8",
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",