@jeffreyjyz/opencode-command-code 0.2.8 → 0.2.11

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;
@@ -0,0 +1,10 @@
1
+ /** 5-hour window length in seconds. Mirrors cmduse_core::FIVE_HOUR_SECS. */
2
+ export declare const FIVE_HOUR_SECS: number;
3
+ /** Weekly window length in seconds. Mirrors cmduse_core::WEEKLY_SECS. */
4
+ export declare const WEEKLY_SECS: number;
5
+ /**
6
+ * Percentage of a rolling window that has elapsed, or undefined when the
7
+ * window has not started (or has no reset time). `nowSecs` is whole seconds —
8
+ * the same unit cmduse-core's vectors pin.
9
+ */
10
+ export declare function elapsedPct(resetAtMs: number | undefined, durSecs: number, nowSecs: number): number | undefined;
package/dist/tui.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
2
  import type { RGBA } from "@opentui/core";
3
- import { Plugin } from "@opencode/plugin/tui";
3
+ import type { Plugin as TuiPluginNs } from "@opencode/plugin/tui";
4
4
  interface V1Api {
5
5
  slots: {
6
6
  register: (config: {
@@ -31,10 +31,10 @@ interface V1Api {
31
31
  }
32
32
  /** v1 half — snake_case slot map registered through `api.slots`. */
33
33
  export declare const tui: (api: V1Api) => Promise<void>;
34
- export declare const commandCodeTui: Plugin.Definition;
34
+ export declare const commandCodeTui: TuiPluginNs.Definition;
35
35
  declare const _default: {
36
36
  tui: (api: V1Api) => Promise<void>;
37
37
  id: string;
38
- setup: (context: Plugin.Context) => Promise<Plugin.Cleanup | void> | Plugin.Cleanup | void;
38
+ setup: (context: TuiPluginNs.Context) => Promise<TuiPluginNs.Cleanup | void> | TuiPluginNs.Cleanup | void;
39
39
  };
40
40
  export default _default;
package/dist/tui.js CHANGED
@@ -8,7 +8,6 @@ import { createComponent as _$createComponent } from "@opentui/solid";
8
8
  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
- import { Plugin } from "@opencode/plugin/tui";
12
11
  import { For, Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js";
13
12
 
14
13
  // src/cli.ts
@@ -389,6 +388,22 @@ function canonicalizeModelId(model) {
389
388
  return findKnown(stripDate(model)) ?? model;
390
389
  }
391
390
 
391
+ // src/sidebar/windows.ts
392
+ var FIVE_HOUR_SECS = 5 * 3600;
393
+ var WEEKLY_SECS = 7 * 86400;
394
+ function elapsedPct(resetAtMs, durSecs, nowSecs) {
395
+ if (typeof resetAtMs !== "number" || !Number.isFinite(resetAtMs))
396
+ return;
397
+ const resetSecs = Math.floor(resetAtMs / 1000);
398
+ if (resetSecs < durSecs)
399
+ return;
400
+ const start = resetSecs - durSecs;
401
+ if (nowSecs < start)
402
+ return;
403
+ const pct = (nowSecs - start) / durSecs * 100;
404
+ return Math.round(Math.min(100, Math.max(0, pct)));
405
+ }
406
+
392
407
  // src/sidebar/rows.ts
393
408
  var TIER_DISPLAY = {
394
409
  opensource: "open source",
@@ -425,13 +440,15 @@ function until(epoch, now = Date.now()) {
425
440
  const days = Math.floor(hours / 24);
426
441
  return hours % 24 ? `${days}d ${hours % 24}h` : `${days}d`;
427
442
  }
428
- function windowRow(label, w, now) {
443
+ function windowRow(label, w, now, durSecs) {
429
444
  if (!w || typeof w.cap !== "number" || w.cap <= 0)
430
445
  return;
431
446
  const used = typeof w.used === "number" ? w.used : 0;
432
447
  const pct = Math.round(used / w.cap * 100);
448
+ const elapsed = elapsedPct(w.resetAt, durSecs, Math.floor(now / 1000));
433
449
  const reset = until(w.resetAt, now);
434
- return [label, `${money(used)} / ${money(w.cap)} (${pct}%)${reset ? ` \xB7 resets ${reset}` : ""}`];
450
+ const parts = [`${money(used)} / ${money(w.cap)} (${pct}%)`, ...elapsed === undefined ? [] : [`${elapsed}% elapsed`], ...reset ? [`resets ${reset}`] : []];
451
+ return [label, parts.join(" \xB7 ")];
435
452
  }
436
453
  function usageRows(usage, now = Date.now()) {
437
454
  if (!usage)
@@ -446,10 +463,10 @@ function usageRows(usage, now = Date.now()) {
446
463
  const pct = usage.monthlyCap > 0 ? Math.round(used / usage.monthlyCap * 100) : 0;
447
464
  rows.push(["Monthly", `${money(used)} / ${money(usage.monthlyCap)} (${pct}%)`]);
448
465
  }
449
- const five = windowRow("5-hour", usage.fiveHour, now);
466
+ const five = windowRow("5-hour", usage.fiveHour, now, FIVE_HOUR_SECS);
450
467
  if (five)
451
468
  rows.push(five);
452
- const week = windowRow("Weekly", usage.weekly, now);
469
+ const week = windowRow("Weekly", usage.weekly, now, WEEKLY_SECS);
453
470
  if (week)
454
471
  rows.push(week);
455
472
  if (typeof usage.requests === "number" || typeof usage.cost === "number") {
@@ -462,10 +479,14 @@ function usageRows(usage, now = Date.now()) {
462
479
  }
463
480
  return rows;
464
481
  }
465
- function modelRows(meta) {
482
+ function modelRows(meta, usage) {
466
483
  if (!meta)
467
484
  return [];
468
485
  const rows = [["Model", meta.name]];
486
+ if (usage) {
487
+ const spent = usage.cost > 0 ? ` \xB7 ${money(usage.cost)}` : "";
488
+ rows.push(["Usage (this model)", `${count(usage.requests)} req${spent}`]);
489
+ }
469
490
  if (meta.tier)
470
491
  rows.push(["Tier", TIER_DISPLAY[meta.tier]]);
471
492
  if (typeof meta.allowance === "number")
@@ -593,6 +614,72 @@ async function loadUsage() {
593
614
  return parseUsageJson(await runFirst(CMDUSE, ["-1", "--json", "--plain"]));
594
615
  }
595
616
 
617
+ // src/sidebar/usageDb.ts
618
+ import { Database } from "bun:sqlite";
619
+ import { homedir as homedir2 } from "os";
620
+ import { join as join2 } from "path";
621
+ function usageDbPath() {
622
+ if (process.env.OPENCODE_DB)
623
+ return process.env.OPENCODE_DB;
624
+ const base = process.env.XDG_DATA_HOME ?? join2(homedir2(), ".local", "share");
625
+ return join2(base, "opencode", "opencode.db");
626
+ }
627
+ function periodStart(periodEnd, now = Date.now()) {
628
+ const end = periodEnd ? Date.parse(`${periodEnd}T23:59:59`) : Number.NaN;
629
+ if (!Number.isFinite(end))
630
+ return now - 30 * 24 * 60 * 60 * 1000;
631
+ const start = new Date(end);
632
+ start.setMonth(start.getMonth() - 1);
633
+ return start.getTime();
634
+ }
635
+ function usage(value) {
636
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
637
+ }
638
+ function loadModelUsage(modelID, sinceMs, path = usageDbPath()) {
639
+ let db;
640
+ try {
641
+ db = new Database(path, {
642
+ readonly: true
643
+ });
644
+ } catch {
645
+ return null;
646
+ }
647
+ try {
648
+ const rows = db.query(`SELECT data, time_created FROM message
649
+ WHERE data LIKE '%"tokens"%' AND time_created >= ?`).all(sinceMs);
650
+ const key = modelKey(modelID);
651
+ const seen = new Set;
652
+ let requests = 0;
653
+ let cost = 0;
654
+ for (const row of rows) {
655
+ let data;
656
+ try {
657
+ data = JSON.parse(row.data);
658
+ } catch {
659
+ continue;
660
+ }
661
+ if (data.role !== "assistant" || !data.modelID || !data.tokens)
662
+ continue;
663
+ if (modelKey(data.modelID) !== key)
664
+ continue;
665
+ const id = `${data.modelID}@${row.time_created}`;
666
+ if (seen.has(id))
667
+ continue;
668
+ seen.add(id);
669
+ requests += 1;
670
+ cost += usage(data.cost);
671
+ }
672
+ return {
673
+ requests,
674
+ cost
675
+ };
676
+ } catch {
677
+ return null;
678
+ } finally {
679
+ db.close();
680
+ }
681
+ }
682
+
596
683
  // src/tui.tsx
597
684
  var ID = "command-code.tui";
598
685
  var POLL_MS = 30000;
@@ -628,10 +715,14 @@ function Panel(props) {
628
715
  function useRows(activeModelId, active) {
629
716
  const [usage, setUsage] = createSignal([]);
630
717
  const [meta, setMeta] = createSignal(new Map);
718
+ const [modelUsage, setModelUsage] = createSignal();
631
719
  loadMeta().then(setMeta).catch(() => {});
632
720
  const refresh = async () => {
633
721
  try {
634
- setUsage(usageRows(await loadUsage()));
722
+ const snapshot = await loadUsage();
723
+ setUsage(usageRows(snapshot));
724
+ const id = activeModelId();
725
+ setModelUsage(id ? loadModelUsage(id, periodStart(snapshot.periodEnd)) ?? undefined : undefined);
635
726
  } catch {}
636
727
  };
637
728
  createEffect(() => {
@@ -650,7 +741,7 @@ function useRows(activeModelId, active) {
650
741
  ...found,
651
742
  tier: found.tier ?? (id ? tierFor(id) : undefined)
652
743
  } : undefined;
653
- return [...usage(), ...modelRows(model)];
744
+ return [...usage(), ...modelRows(model, modelUsage())];
654
745
  });
655
746
  }
656
747
  var isOurs = (providerID) => Boolean(providerID?.startsWith("command-code"));
@@ -690,7 +781,7 @@ function PanelV2(props) {
690
781
  muted: () => ctx.theme.text.subdued
691
782
  });
692
783
  }
693
- var commandCodeTui = Plugin.define({
784
+ var commandCodeTui = {
694
785
  id: ID,
695
786
  setup(ctx) {
696
787
  ctx.ui.slot({
@@ -741,7 +832,7 @@ var commandCodeTui = Plugin.define({
741
832
  }
742
833
  });
743
834
  }
744
- });
835
+ };
745
836
  var tui_default = {
746
837
  ...commandCodeTui,
747
838
  tui
package/dist/v2.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Plugin } from "@opencode/plugin";
1
+ import type { Plugin as PluginNs } from "@opencode/plugin";
2
2
  import { type CmdModel } from "./models";
3
3
  export declare const PLUGIN_ID = "command-code";
4
4
  export declare const PROVIDER_BASE = "https://api.commandcode.ai/provider/v1";
@@ -15,9 +15,10 @@ type Lane = {
15
15
  reasoningField?: string;
16
16
  };
17
17
  export type { Lane };
18
- /** API/known model → v2 Model.Info. Every Command Code model shares
19
- * capabilities and pricing (subscription credits, not per-token); only the
20
- * open lane needs the reasoning round-trip field. */
18
+ export declare const TOOL_DESCRIPTION = "Fetch live Command Code plan/usage: plan name, monthly credits, 5-hour & weekly windows, billing-period summary. Pass arg='plans' for the plan comparison table only, or extra cmduse flags (e.g. '--tz +05:30 daily').";
19
+ /** API/known model → v2 Model.Info. Capabilities, variants and $/1M cost all
20
+ * come from the generated catalog (the listing API publishes none of them);
21
+ * only the open lane needs the reasoning round-trip field. */
21
22
  export declare function toV2Model(m: CmdModel, lane: Lane): unknown;
22
23
  /** Instant static seed from gating.json's known model ids, split by wire lane.
23
24
  * Replaced by the gated live list once the background fetch lands; also the
@@ -37,4 +38,10 @@ type CredentialContext = {
37
38
  * back to CMD_API_KEY / ~/.commandcode/auth.json. Resolved per call — a
38
39
  * /connect mid-session must be picked up. */
39
40
  export declare function credentialKey(ctx: CredentialContext): Promise<string | undefined>;
40
- export declare const commandCodeV2: Plugin.Plugin;
41
+ /**
42
+ * opencode validates the plugin *by decoding* the default export against its
43
+ * own `Plugin` interface (identity `define` on both sides), so the package is
44
+ * only needed for types. Keeps the runtime dependency surface to opencode's
45
+ * own rewrite list (@opentui/*, solid-js).
46
+ */
47
+ export declare const commandCodeV2: PluginNs.Plugin;
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.11",
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",
@@ -13,18 +13,20 @@
13
13
  "build": "rm -rf dist && bun build src/index.ts --outdir dist --target bun --external @opencode-ai/plugin --external @opencode/plugin --external @opencode/plugin/tui && bun scripts/build-tui.ts && tsc -p tsconfig.build.json",
14
14
  "test": "bun test",
15
15
  "typecheck": "tsc --noEmit",
16
- "build:tui": "bun scripts/build-tui.ts"
16
+ "build:tui": "bun scripts/build-tui.ts",
17
+ "extract:catalog": "bun scripts/extract-catalog.ts"
17
18
  },
18
19
  "dependencies": {
19
- "@opencode-ai/plugin": "*",
20
- "@opencode/plugin": "^2.0.11",
21
20
  "@opentui/core": "^0.5.4",
22
21
  "@opentui/solid": "^0.5.4",
23
- "solid-js": "1.9.12"
22
+ "solid-js": "1.9.12",
23
+ "zod": "4.1.8"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/bun": "latest",
27
- "typescript": "^5"
27
+ "typescript": "^5",
28
+ "@opencode-ai/plugin": "*",
29
+ "@opencode/plugin": "^2.0.11"
28
30
  },
29
31
  "files": [
30
32
  "dist",