@jeffreyjyz/opencode-command-code 0.3.7 → 0.3.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.
package/dist/cli.d.ts CHANGED
@@ -1,3 +1,7 @@
1
+ /** Fallback lookup paths: the opencode background service's PATH often lacks
2
+ * the homebrew prefix, so the bare name alone is not enough. `CMDUSE_BIN` wins
3
+ * when set — the dev-build hook the sibling cmduse consumers (mpc) honour too. */
4
+ export declare function cmduseCandidates(env?: Record<string, string | undefined>): string[];
1
5
  /** Shell-words split: whitespace-separated, with single/double quoting and
2
6
  * backslash escapes ("--tz +05:30", arg='a b'). Unterminated quotes close at
3
7
  * end of input. */
package/dist/index.js CHANGED
@@ -2457,7 +2457,16 @@ async function providerModels(key) {
2457
2457
 
2458
2458
  // src/cli.ts
2459
2459
  import { spawn } from "child_process";
2460
- var CANDIDATES = ["cmduse", "/opt/homebrew/bin/cmduse", "/usr/local/bin/cmduse"];
2460
+ function cmduseCandidates(env = process.env) {
2461
+ const candidates = [
2462
+ env.CMDUSE_BIN,
2463
+ "cmduse",
2464
+ "/opt/homebrew/bin/cmduse",
2465
+ "/usr/local/bin/cmduse"
2466
+ ];
2467
+ return candidates.filter((bin) => Boolean(bin));
2468
+ }
2469
+ var CANDIDATES = cmduseCandidates();
2461
2470
  var DEFAULT_ARGS = ["-1", "--plain"];
2462
2471
  var SUBCOMMANDS = new Set([
2463
2472
  "plans",
@@ -4054,7 +4063,7 @@ var commandCodeV2 = {
4054
4063
  // package.json
4055
4064
  var package_default = {
4056
4065
  name: "@jeffreyjyz/opencode-command-code",
4057
- version: "0.3.7",
4066
+ version: "0.3.9",
4058
4067
  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.",
4059
4068
  type: "module",
4060
4069
  main: "dist/index.js",
@@ -0,0 +1,50 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import { RGBA } from "@opentui/core";
3
+ import { type SidebarRow, type Tone } from "./rows";
4
+ /** One colour per row tone, plus the two label colours (plain rows, headlines). */
5
+ export type PanelColors = Readonly<Record<Tone, RGBA>> & {
6
+ readonly label: RGBA;
7
+ readonly headline: RGBA;
8
+ };
9
+ /**
10
+ * The slice of the v2 host theme the panel reads. `@opencode/theme` is not a
11
+ * dependency here (the host rewrites its own module instances in), so the shape
12
+ * is declared structurally — and read defensively, since the theme package has
13
+ * renamed tokens before (`text.default` -> `text.base`).
14
+ */
15
+ export interface HostTheme {
16
+ readonly text: {
17
+ readonly base: RGBA;
18
+ readonly muted: RGBA;
19
+ readonly feedback: Readonly<Record<"success" | "warning" | "error" | "info", {
20
+ readonly base: RGBA;
21
+ }>>;
22
+ };
23
+ readonly hue?: {
24
+ readonly accent?: Readonly<Record<number, RGBA>>;
25
+ };
26
+ }
27
+ /**
28
+ * v2 host theme -> panel colours. `mode` matters for one thing: the accent hue
29
+ * is a usable purple in dark themes (#9d7cd8) but a peach in light ones, so the
30
+ * headline shade is only taken from it in dark mode. A lighter cyan was tried
31
+ * first and failed: terminals fold one-step-apart cyans onto the same ANSI
32
+ * colour, so the headline rows read identical to the rest.
33
+ */
34
+ export declare function hostColors(theme: HostTheme, mode?: "dark" | "light"): PanelColors;
35
+ /** v1 hosts expose only a text/muted pair, so the semantic roles are literals. */
36
+ export declare function legacyColors(theme: {
37
+ text: RGBA;
38
+ textMuted: RGBA;
39
+ }): PanelColors;
40
+ /**
41
+ * Colours off: every tone renders as plain text, so only bold and dim carry
42
+ * structure. Headlines lose their accent, status rows their green/amber/red —
43
+ * which is the point of the default.
44
+ */
45
+ export declare function mono(colors: PanelColors): PanelColors;
46
+ /** Bold title, then one line per row. Renders nothing when there are no rows. */
47
+ export declare function Panel(props: {
48
+ rows: () => SidebarRow[];
49
+ colors: () => PanelColors;
50
+ }): import("solid-js").JSX.Element;
@@ -0,0 +1,10 @@
1
+ export interface Prefs {
2
+ colors: boolean;
3
+ }
4
+ export declare const DEFAULTS: Prefs;
5
+ /** Preferences live beside opencode's own config, XDG layout honoured. */
6
+ export declare function prefsPath(env?: NodeJS.ProcessEnv): string;
7
+ /** Only a literal `true` enables colour, so a typo cannot turn the panel on. */
8
+ export declare function parsePrefs(raw: unknown): Prefs;
9
+ /** Env override, then the file, then the default. Cached for `CACHE_MS`. */
10
+ export declare function loadPrefs(env?: NodeJS.ProcessEnv, now?: number): Prefs;
@@ -1,5 +1,41 @@
1
1
  import { type Category } from "../gating";
2
- export type SidebarRow = [label: string, value: string];
2
+ /**
3
+ * Colour role for a row's *value*, resolved against the host theme by the
4
+ * panel: `ok` / `warn` / `crit` are headroom (green / amber / red), `strong` is
5
+ * plain text in bold (the model name, the way mpc bolds its MODEL column),
6
+ * `muted` is for continuations and rules, `base` is plain text. Labels are
7
+ * coloured separately: the panel draws them in the theme's info hue.
8
+ */
9
+ export type Tone = "base" | "muted" | "strong" | "ok" | "warn" | "crit";
10
+ /**
11
+ * A row. `headline` marks the two rows whose label names the thing the panel is
12
+ * about (the plan, the active model); the panel colours their labels apart.
13
+ */
14
+ export type SidebarRow = [
15
+ label: string,
16
+ value: string,
17
+ tone?: Tone,
18
+ headline?: true
19
+ ];
20
+ /** Headroom severity: comfortable to 70%, tight to 90%, over after that. */
21
+ export declare function pctTone(pct: number): Tone;
22
+ /**
23
+ * The sidebar is 42 columns wide with 2+2 padding, and our panel keeps one more
24
+ * column clear on the right, so a rendered row must stay within this. Rows that
25
+ * cannot fit are split across an indented continuation (`""` label) rather than
26
+ * wrapping in the host's renderer, which is what made the panel look broken.
27
+ */
28
+ export declare const ROW_WIDTH = 37;
29
+ /** A horizontal rule between blocks. */
30
+ export declare function separator(): SidebarRow;
31
+ /** Truncate to `width`, ellipsis included. */
32
+ export declare function clip(text: string, width?: number): string;
33
+ /** Value budget for a labelled row: the panel draws `label: value`. */
34
+ export declare function valueWidth(label: string): number;
35
+ /** How the panel splits a row: a muted label (or banner) and a toned value. */
36
+ export declare function parts(row: SidebarRow): [label: string, value: string];
37
+ /** Rendered width of a row, as the panel draws it. */
38
+ export declare function rowWidth(row: SidebarRow): number;
3
39
  export interface ModelMeta {
4
40
  key: string;
5
41
  name: string;
@@ -30,6 +66,9 @@ export interface Usage {
30
66
  plan?: string;
31
67
  monthlyCap?: number;
32
68
  monthlyCredits?: number;
69
+ /** Billing-period bounds in epoch ms (cmduse 0.7.2+); absent on older builds. */
70
+ periodStartAt?: number;
71
+ periodEndAt?: number;
33
72
  fiveHour?: WindowUsage;
34
73
  weekly?: WindowUsage;
35
74
  periodEnd?: string;
@@ -3,10 +3,14 @@ import { type ModelUsage } from "./rows";
3
3
  export declare function usageDbPath(): string;
4
4
  /**
5
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.
6
+ * 0.7.2+ publishes `periodStartAt` directly; older builds publish only
7
+ * `periodEnd` (YYYY-MM-DD), so the start is that date one calendar month back.
8
+ * Falls back to the last 30 days when neither is usable.
8
9
  */
9
- export declare function periodStart(periodEnd: string | undefined, now?: number): number;
10
+ export declare function periodStart(usage: {
11
+ periodStartAt?: number;
12
+ periodEnd?: string;
13
+ }, now?: number): number;
10
14
  /**
11
15
  * Requests and cost for one model since `sinceMs`. Null when the store is
12
16
  * missing or unreadable, so callers can fall back to omitting the row.
@@ -0,0 +1 @@
1
+ export declare function useRows(activeModelId: () => string | undefined, active: () => boolean): import("solid-js").Accessor<import("./rows").SidebarRow[]>;
package/dist/tui.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
- import type { RGBA } from "@opentui/core";
3
2
  import type { Plugin as TuiPluginNs } from "@opencode/plugin/tui";
3
+ import type { RGBA } from "@opentui/core";
4
4
  interface V1Api {
5
5
  slots: {
6
6
  register: (config: {
package/dist/tui.js CHANGED
@@ -1,18 +1,14 @@
1
1
  // @bun
2
2
  // src/tui.tsx
3
- import { memo as _$memo } from "@opentui/solid";
4
- import { setProp as _$setProp } from "@opentui/solid";
5
- import { effect as _$effect } from "@opentui/solid";
6
- import { insert as _$insert } from "@opentui/solid";
7
- import { createComponent as _$createComponent } from "@opentui/solid";
8
- import { createTextNode as _$createTextNode } from "@opentui/solid";
9
- import { insertNode as _$insertNode } from "@opentui/solid";
10
- import { createElement as _$createElement } from "@opentui/solid";
11
- import { For, Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js";
3
+ import { createComponent as _$createComponent2 } from "@opentui/solid";
12
4
 
13
5
  // src/cli.ts
14
6
  import { spawn } from "child_process";
15
- var CANDIDATES = ["cmduse", "/opt/homebrew/bin/cmduse", "/usr/local/bin/cmduse"];
7
+ function cmduseCandidates(env = process.env) {
8
+ const candidates = [env.CMDUSE_BIN, "cmduse", "/opt/homebrew/bin/cmduse", "/usr/local/bin/cmduse"];
9
+ return candidates.filter((bin) => Boolean(bin));
10
+ }
11
+ var CANDIDATES = cmduseCandidates();
16
12
  var DEFAULT_ARGS = ["-1", "--plain"];
17
13
  var SUBCOMMANDS = new Set(["plans", "models", "daily", "hourly", "model", "session", "statusline", "config"]);
18
14
  function splitCliArgs(input) {
@@ -122,11 +118,18 @@ async function runCmduse(arg, opts = {}) {
122
118
  });
123
119
  }
124
120
 
125
- // src/sidebar/data.ts
126
- import { spawn as spawn2 } from "child_process";
127
- import { mkdir, readFile, writeFile } from "fs/promises";
128
- import { homedir } from "os";
129
- import { dirname, join } from "path";
121
+ // src/sidebar/panel.tsx
122
+ import { createComponent as _$createComponent } from "@opentui/solid";
123
+ import { createTextNode as _$createTextNode } from "@opentui/solid";
124
+ import { insertNode as _$insertNode } from "@opentui/solid";
125
+ import { spread as _$spread } from "@opentui/solid";
126
+ import { mergeProps as _$mergeProps } from "@opentui/solid";
127
+ import { setProp as _$setProp } from "@opentui/solid";
128
+ import { effect as _$effect } from "@opentui/solid";
129
+ import { insert as _$insert } from "@opentui/solid";
130
+ import { createElement as _$createElement } from "@opentui/solid";
131
+ import { RGBA } from "@opentui/core";
132
+ import { For, Show } from "solid-js";
130
133
  // ../core/gating.json
131
134
  var gating_default = {
132
135
  extractedAt: "2026-09-27T06:27:06.878Z",
@@ -468,6 +471,28 @@ function elapsedLabel(resetAtMs, durSecs, nowSecs) {
468
471
  }
469
472
 
470
473
  // src/sidebar/rows.ts
474
+ function pctTone(pct) {
475
+ if (pct >= 90)
476
+ return "crit";
477
+ if (pct >= 70)
478
+ return "warn";
479
+ return "ok";
480
+ }
481
+ var ROW_WIDTH = 37;
482
+ function separator() {
483
+ return ["\u2500".repeat(ROW_WIDTH), "", "muted"];
484
+ }
485
+ function clip(text, width = ROW_WIDTH) {
486
+ return text.length <= width ? text : `${text.slice(0, width - 1)}\u2026`;
487
+ }
488
+ function valueWidth(label) {
489
+ return ROW_WIDTH - label.length - 2;
490
+ }
491
+ function parts(row) {
492
+ if (row[0] === "")
493
+ return [` ${row[1]}`, ""];
494
+ return [row[1] ? `${row[0]}: ` : row[0], row[1]];
495
+ }
471
496
  var TIER_DISPLAY = {
472
497
  opensource: "open source",
473
498
  premium: "premium"
@@ -503,15 +528,30 @@ function until(epoch, now = Date.now()) {
503
528
  const days = Math.floor(hours / 24);
504
529
  return hours % 24 ? `${days}d ${hours % 24}h` : `${days}d`;
505
530
  }
506
- function windowRow(label, w, now, durSecs) {
531
+ function windowRows(label, w, now, durSecs) {
507
532
  if (!w || typeof w.cap !== "number" || w.cap <= 0)
508
- return;
533
+ return [];
509
534
  const used = typeof w.used === "number" ? w.used : 0;
510
535
  const pct = Math.round(used / w.cap * 100);
511
536
  const elapsed = elapsedLabel(w.resetAt, durSecs, Math.floor(now / 1000));
512
537
  const reset = until(w.resetAt, now);
513
- const parts = [`${money(used)} / ${money(w.cap)} (${pct}%)`, ...elapsed === undefined ? [] : [`${elapsed} elapsed`], ...reset ? [`resets ${reset}`] : []];
514
- return [label, parts.join(" \xB7 ")];
538
+ const detail = [...elapsed === undefined ? [] : [`${elapsed} elapsed`], ...reset ? [`resets ${reset}`] : []].join(" \xB7 ");
539
+ return [[label, `${money(used)}/${money(w.cap)} (${pct}%)`, pctTone(pct)], ...detail ? [["", detail, "muted"]] : []];
540
+ }
541
+ function monthlyDetail(usage, now) {
542
+ const {
543
+ periodStartAt,
544
+ periodEndAt
545
+ } = usage;
546
+ if (typeof periodStartAt !== "number" || typeof periodEndAt !== "number")
547
+ return;
548
+ const durSecs = Math.round((periodEndAt - periodStartAt) / 1000);
549
+ if (durSecs <= 0)
550
+ return;
551
+ const elapsed = elapsedLabel(periodEndAt, durSecs, Math.floor(now / 1000));
552
+ const reset = until(periodEndAt, now);
553
+ const parts = [...elapsed === undefined ? [] : [`${elapsed} elapsed`], ...reset ? [`resets ${reset}`] : []];
554
+ return parts.length ? parts.join(" \xB7 ") : undefined;
515
555
  }
516
556
  function usageRows(usage, now = Date.now()) {
517
557
  if (!usage)
@@ -519,54 +559,54 @@ function usageRows(usage, now = Date.now()) {
519
559
  const rows = [];
520
560
  if (usage.plan) {
521
561
  const credits = typeof usage.monthlyCap === "number" ? ` \xB7 $${usage.monthlyCap}/mo credits` : "";
522
- rows.push(["Plan", `${usage.plan}${credits}`]);
562
+ rows.push(["Plan", `${usage.plan}${credits}`, "base", true]);
523
563
  }
524
564
  if (typeof usage.monthlyCap === "number" && typeof usage.monthlyCredits === "number") {
525
- const used = usage.monthlyCap - usage.monthlyCredits;
565
+ const used = Math.min(usage.monthlyCap, Math.max(0, usage.monthlyCap - usage.monthlyCredits));
526
566
  const pct = usage.monthlyCap > 0 ? Math.round(used / usage.monthlyCap * 100) : 0;
527
- rows.push(["Monthly", `${money(used)} / ${money(usage.monthlyCap)} (${pct}%)`]);
567
+ rows.push(["Monthly", `${money(used)} / ${money(usage.monthlyCap)} (${pct}%)`, pctTone(pct)]);
568
+ const detail = monthlyDetail(usage, now);
569
+ if (detail)
570
+ rows.push(["", detail, "muted"]);
528
571
  }
529
- const five = windowRow("5-hour", usage.fiveHour, now, FIVE_HOUR_SECS);
530
- if (five)
531
- rows.push(five);
532
- const week = windowRow("Weekly", usage.weekly, now, WEEKLY_SECS);
533
- if (week)
534
- rows.push(week);
572
+ rows.push(...windowRows("5-hour", usage.fiveHour, now, FIVE_HOUR_SECS));
573
+ rows.push(...windowRows("Weekly", usage.weekly, now, WEEKLY_SECS));
535
574
  if (typeof usage.requests === "number" || typeof usage.cost === "number") {
536
575
  const parts = [];
537
576
  if (typeof usage.requests === "number")
538
577
  parts.push(`${count(usage.requests)} requests`);
539
578
  if (typeof usage.cost === "number")
540
579
  parts.push(money(usage.cost));
541
- rows.push(["Period", parts.join(" \xB7 ")]);
580
+ rows.push(["Period", parts.join(" \xB7 "), "base"]);
542
581
  }
543
582
  return rows;
544
583
  }
545
584
  function modelRows(meta, usage) {
546
585
  if (!meta)
547
586
  return [];
548
- const rows = [["Model", meta.name]];
587
+ const rows = [["Model", clip(meta.name, valueWidth("Model")), "strong", true]];
549
588
  if (usage) {
550
589
  const spent = usage.cost > 0 ? ` \xB7 ${money(usage.cost)}` : "";
551
- rows.push(["Usage (this model)", `${count(usage.requests)} req${spent}`]);
590
+ rows.push(["Usage (this model)", `${count(usage.requests)} req${spent}`, "base"]);
552
591
  }
553
592
  if (meta.tier)
554
- rows.push(["Tier", TIER_DISPLAY[meta.tier]]);
593
+ rows.push(["Tier", TIER_DISPLAY[meta.tier], "base"]);
555
594
  else if (meta.minPlan)
556
- rows.push(["Min plan", meta.minPlan]);
595
+ rows.push(["Min plan", meta.minPlan, "base"]);
557
596
  if (typeof meta.allowance === "number")
558
- rows.push(["Allowance", `${money(meta.allowance)}/mo`]);
597
+ rows.push(["Allowance", `${money(meta.allowance)}/mo`, "base"]);
559
598
  if (meta.rates) {
560
- const parts = [`${rate(meta.rates.input)}/${rate(meta.rates.output)} in/out`, `cache read ${rate(meta.rates.cacheRead)}`];
599
+ const cache = [`cache read ${rate(meta.rates.cacheRead)}`];
561
600
  if (typeof meta.rates.cacheWrite === "number" && meta.rates.cacheWrite > 0) {
562
- parts.push(`cache write ${rate(meta.rates.cacheWrite)}`);
601
+ cache.push(`write ${rate(meta.rates.cacheWrite)}`);
563
602
  }
564
- rows.push(["Rates", parts.join(" \xB7 ")]);
603
+ rows.push(["Rates", `${rate(meta.rates.input)}/${rate(meta.rates.output)} in/out`, "base"]);
604
+ rows.push(["", cache.join(" \xB7 "), "muted"]);
565
605
  }
566
606
  if (typeof meta.intelligence === "number")
567
- rows.push(["Intelligence", String(meta.intelligence)]);
607
+ rows.push(["Intelligence", String(meta.intelligence), "base"]);
568
608
  if (typeof meta.tps === "number")
569
- rows.push(["Tok/s", String(meta.tps)]);
609
+ rows.push(["Tok/s", String(meta.tps), "base"]);
570
610
  return rows;
571
611
  }
572
612
  function modelKey(modelId) {
@@ -578,110 +618,157 @@ function tierFor(modelId) {
578
618
  return MODEL_CATEGORIES[canonicalizeModelId(modelId)];
579
619
  }
580
620
 
581
- // src/sidebar/data.ts
582
- var CMDUSE = ["cmduse", "/opt/homebrew/bin/cmduse", "/usr/local/bin/cmduse"];
583
- var MPC = ["mpc", join(homedir(), ".bun/bin/mpc"), "/opt/homebrew/bin/mpc"];
584
- var CACHE_TTL_MS = 6 * 60 * 60 * 1000;
585
- var SPAWN_OPTIONS = {
586
- stdio: ["ignore", "pipe", "pipe"],
587
- detached: true
588
- };
589
- function run(bin, args) {
590
- return new Promise((resolve, reject) => {
591
- const child = spawn2(bin, args, SPAWN_OPTIONS);
592
- const out = [];
593
- const err = [];
594
- child.stdout?.on("data", (d) => out.push(d));
595
- child.stderr?.on("data", (d) => err.push(d));
596
- child.on("error", reject);
597
- 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)}`)));
598
- });
599
- }
600
- async function runFirst(candidates, args) {
601
- let last;
602
- for (const bin of candidates) {
603
- try {
604
- return await run(bin, args);
605
- } catch (e) {
606
- last = e;
607
- if (e?.code !== "ENOENT")
608
- throw e;
609
- }
610
- }
611
- throw last instanceof Error ? last : new Error(`${candidates[0]} not found`);
621
+ // src/sidebar/panel.tsx
622
+ function hostColors(theme, mode = "dark") {
623
+ return {
624
+ base: theme.text.base,
625
+ muted: theme.text.muted,
626
+ strong: theme.text.base,
627
+ label: theme.text.feedback.info.base,
628
+ headline: mode === "dark" ? theme.hue?.accent?.[200] ?? theme.text.feedback.info.base : theme.text.feedback.info.base,
629
+ ok: theme.text.feedback.success.base,
630
+ warn: theme.text.feedback.warning.base,
631
+ crit: theme.text.feedback.error.base
632
+ };
612
633
  }
613
- function parseUsageJson(text) {
614
- const raw = JSON.parse(text);
615
- const win = (v) => {
616
- if (!v || typeof v !== "object")
617
- return;
618
- const w = v;
619
- return {
620
- cap: typeof w.cap === "number" ? w.cap : undefined,
621
- used: typeof w.used === "number" ? w.used : undefined,
622
- resetAt: typeof w.resetAt === "number" ? w.resetAt : undefined
623
- };
634
+ function legacyColors(theme) {
635
+ return {
636
+ base: theme.text,
637
+ muted: theme.textMuted,
638
+ strong: theme.text,
639
+ label: RGBA.fromValues(0.34, 0.71, 0.76, 1),
640
+ headline: RGBA.fromValues(0.62, 0.49, 0.85, 1),
641
+ ok: RGBA.fromValues(0.5, 0.85, 0.56, 1),
642
+ warn: RGBA.fromValues(0.96, 0.65, 0.26, 1),
643
+ crit: RGBA.fromValues(0.88, 0.42, 0.46, 1)
624
644
  };
625
- const summary = raw.summary ?? {};
645
+ }
646
+ function mono(colors) {
626
647
  return {
627
- plan: typeof raw.plan === "string" ? raw.plan : undefined,
628
- monthlyCap: typeof raw.monthlyCap === "number" ? raw.monthlyCap : undefined,
629
- monthlyCredits: typeof raw.monthlyCredits === "number" ? raw.monthlyCredits : undefined,
630
- fiveHour: win(raw.fiveHour),
631
- weekly: win(raw.weekly),
632
- periodEnd: typeof raw.periodEnd === "string" ? raw.periodEnd : undefined,
633
- requests: typeof summary.requests === "number" ? summary.requests : undefined,
634
- cost: typeof summary.cost === "number" ? summary.cost : undefined
648
+ base: colors.base,
649
+ muted: colors.muted,
650
+ strong: colors.base,
651
+ label: colors.base,
652
+ headline: colors.base,
653
+ ok: colors.base,
654
+ warn: colors.base,
655
+ crit: colors.base
635
656
  };
636
657
  }
637
- function parseMpcJson(text) {
638
- const body = JSON.parse(text);
639
- const meta = new Map;
640
- for (const row of body.rows ?? []) {
641
- const cc = row.cc;
642
- if (!cc || !row.name)
643
- continue;
644
- const entry = {
645
- key: row.key ?? modelKey(row.name),
646
- name: row.name,
647
- allowance: cc.allowance,
648
- rates: cc.pricing,
649
- intelligence: cc.ability ?? undefined,
650
- tps: cc.tps ?? undefined
651
- };
652
- meta.set(entry.key, entry);
653
- meta.set(modelKey(row.name), entry);
658
+ var spanStyle = (fg) => ({
659
+ style: {
660
+ fg
654
661
  }
655
- return meta;
656
- }
657
- function cachePath() {
658
- const base = process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache");
659
- return join(base, "command-code", "cc-catalog.json");
662
+ });
663
+ var rowColors = (colors, row) => row[3] ? {
664
+ label: colors.headline,
665
+ value: colors.headline
666
+ } : {
667
+ label: colors.label,
668
+ value: colors[row[2] ?? "base"]
669
+ };
670
+ function Row(props) {
671
+ const [label, value] = parts(props.row);
672
+ const tone = props.row[2] ?? "base";
673
+ const fg = rowColors(props.colors, props.row);
674
+ if (!value) {
675
+ return (() => {
676
+ var _el$ = _$createElement("text");
677
+ _$insert(_el$, label);
678
+ _$effect((_$p) => _$setProp(_el$, "fg", props.colors.muted, _$p));
679
+ return _el$;
680
+ })();
681
+ }
682
+ return (() => {
683
+ var _el$2 = _$createElement("text"), _el$3 = _$createElement("span"), _el$4 = _$createElement("b"), _el$5 = _$createElement("span");
684
+ _$insertNode(_el$2, _el$3);
685
+ _$insertNode(_el$2, _el$5);
686
+ _$insertNode(_el$3, _el$4);
687
+ _$spread(_el$3, _$mergeProps(() => spanStyle(fg.label)), true);
688
+ _$insert(_el$4, label);
689
+ _$spread(_el$5, _$mergeProps(() => spanStyle(fg.value)), true);
690
+ _$insert(_el$5, tone === "strong" ? (() => {
691
+ var _el$6 = _$createElement("b");
692
+ _$insert(_el$6, value);
693
+ return _el$6;
694
+ })() : value);
695
+ return _el$2;
696
+ })();
660
697
  }
661
- async function loadMeta() {
662
- try {
663
- const cached = JSON.parse(await readFile(cachePath(), "utf8"));
664
- if (cached.fetchedAt && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
665
- return new Map(cached.rows ?? []);
698
+ function Panel(props) {
699
+ return _$createComponent(Show, {
700
+ get when() {
701
+ return props.rows().length > 0;
702
+ },
703
+ get children() {
704
+ var _el$7 = _$createElement("box"), _el$8 = _$createElement("text"), _el$9 = _$createElement("b");
705
+ _$insertNode(_el$7, _el$8);
706
+ _$insertNode(_el$8, _el$9);
707
+ _$insertNode(_el$9, _$createTextNode(`Command Code`));
708
+ _$insert(_el$7, _$createComponent(For, {
709
+ get each() {
710
+ return props.rows();
711
+ },
712
+ children: (row) => _$createComponent(Row, {
713
+ row,
714
+ get colors() {
715
+ return props.colors();
716
+ }
717
+ })
718
+ }), null);
719
+ _$effect((_$p) => _$setProp(_el$8, "fg", props.colors().base, _$p));
720
+ return _el$7;
666
721
  }
667
- } catch {}
668
- const meta = parseMpcJson(await runFirst(MPC, ["--json"]));
669
- try {
670
- const path = cachePath();
671
- await mkdir(dirname(path), {
672
- recursive: true
673
- });
674
- await writeFile(path, JSON.stringify({
675
- fetchedAt: Date.now(),
676
- rows: [...meta]
677
- }));
678
- } catch {}
679
- return meta;
722
+ });
680
723
  }
681
- async function loadUsage() {
682
- return parseUsageJson(await runFirst(CMDUSE, ["-1", "--json", "--plain"]));
724
+
725
+ // src/sidebar/prefs.ts
726
+ import { readFileSync } from "fs";
727
+ import { homedir } from "os";
728
+ import { join } from "path";
729
+ var DEFAULTS = {
730
+ colors: false
731
+ };
732
+ var CACHE_MS = 5000;
733
+ var cached;
734
+ function prefsPath(env = process.env) {
735
+ const base = env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
736
+ return join(base, "opencode", "command-code.json");
737
+ }
738
+ function parsePrefs(raw) {
739
+ const value = raw?.colors;
740
+ return {
741
+ colors: value === true
742
+ };
743
+ }
744
+ function loadPrefs(env = process.env, now = Date.now()) {
745
+ if (cached && now - cached.at < CACHE_MS)
746
+ return cached.prefs;
747
+ const flag = env.CMD_COLORS;
748
+ let prefs = DEFAULTS;
749
+ if (flag === "1" || flag === "true")
750
+ prefs = {
751
+ colors: true
752
+ };
753
+ else if (flag === "0" || flag === "false")
754
+ prefs = {
755
+ colors: false
756
+ };
757
+ else {
758
+ try {
759
+ prefs = parsePrefs(JSON.parse(readFileSync(prefsPath(env), "utf8")));
760
+ } catch {}
761
+ }
762
+ cached = {
763
+ at: now,
764
+ prefs
765
+ };
766
+ return prefs;
683
767
  }
684
768
 
769
+ // src/sidebar/useRows.ts
770
+ import { createEffect, createMemo, createSignal, onCleanup } from "solid-js";
771
+
685
772
  // src/catalog.ts
686
773
  var MODEL_CATALOG = {
687
774
  "claude-fable-5": {
@@ -1742,18 +1829,130 @@ function minPlan(id) {
1742
1829
  return MODEL_CATALOG[id]?.minPlan ?? null;
1743
1830
  }
1744
1831
 
1832
+ // src/sidebar/data.ts
1833
+ import { spawn as spawn2 } from "child_process";
1834
+ import { mkdir, readFile, writeFile } from "fs/promises";
1835
+ import { homedir as homedir2 } from "os";
1836
+ import { dirname, join as join2 } from "path";
1837
+ var CMDUSE = cmduseCandidates();
1838
+ var MPC = ["mpc", join2(homedir2(), ".bun/bin/mpc"), "/opt/homebrew/bin/mpc"];
1839
+ var CACHE_TTL_MS = 6 * 60 * 60 * 1000;
1840
+ var SPAWN_OPTIONS = {
1841
+ stdio: ["ignore", "pipe", "pipe"],
1842
+ detached: true
1843
+ };
1844
+ function run(bin, args) {
1845
+ return new Promise((resolve, reject) => {
1846
+ const child = spawn2(bin, args, SPAWN_OPTIONS);
1847
+ const out = [];
1848
+ const err = [];
1849
+ child.stdout?.on("data", (d) => out.push(d));
1850
+ child.stderr?.on("data", (d) => err.push(d));
1851
+ child.on("error", reject);
1852
+ 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)}`)));
1853
+ });
1854
+ }
1855
+ async function runFirst(candidates, args) {
1856
+ let last;
1857
+ for (const bin of candidates) {
1858
+ try {
1859
+ return await run(bin, args);
1860
+ } catch (e) {
1861
+ last = e;
1862
+ if (e?.code !== "ENOENT")
1863
+ throw e;
1864
+ }
1865
+ }
1866
+ throw last instanceof Error ? last : new Error(`${candidates[0]} not found`);
1867
+ }
1868
+ function parseUsageJson(text) {
1869
+ const raw = JSON.parse(text);
1870
+ const win = (v) => {
1871
+ if (!v || typeof v !== "object")
1872
+ return;
1873
+ const w = v;
1874
+ return {
1875
+ cap: typeof w.cap === "number" ? w.cap : undefined,
1876
+ used: typeof w.used === "number" ? w.used : undefined,
1877
+ resetAt: typeof w.resetAt === "number" ? w.resetAt : undefined
1878
+ };
1879
+ };
1880
+ const summary = raw.summary ?? {};
1881
+ return {
1882
+ plan: typeof raw.plan === "string" ? raw.plan : undefined,
1883
+ monthlyCap: typeof raw.monthlyCap === "number" ? raw.monthlyCap : undefined,
1884
+ monthlyCredits: typeof raw.monthlyCredits === "number" ? raw.monthlyCredits : undefined,
1885
+ fiveHour: win(raw.fiveHour),
1886
+ weekly: win(raw.weekly),
1887
+ periodEnd: typeof raw.periodEnd === "string" ? raw.periodEnd : undefined,
1888
+ periodStartAt: typeof raw.periodStartAt === "number" ? raw.periodStartAt : undefined,
1889
+ periodEndAt: typeof raw.periodEndAt === "number" ? raw.periodEndAt : undefined,
1890
+ requests: typeof summary.requests === "number" ? summary.requests : undefined,
1891
+ cost: typeof summary.cost === "number" ? summary.cost : undefined
1892
+ };
1893
+ }
1894
+ function parseMpcJson(text) {
1895
+ const body = JSON.parse(text);
1896
+ const meta = new Map;
1897
+ for (const row of body.rows ?? []) {
1898
+ const cc = row.cc;
1899
+ if (!cc || !row.name)
1900
+ continue;
1901
+ const entry = {
1902
+ key: row.key ?? modelKey(row.name),
1903
+ name: row.name,
1904
+ allowance: cc.allowance,
1905
+ rates: cc.pricing,
1906
+ intelligence: cc.ability ?? undefined,
1907
+ tps: cc.tps ?? undefined
1908
+ };
1909
+ meta.set(entry.key, entry);
1910
+ meta.set(modelKey(row.name), entry);
1911
+ }
1912
+ return meta;
1913
+ }
1914
+ function cachePath() {
1915
+ const base = process.env.XDG_CACHE_HOME ?? join2(homedir2(), ".cache");
1916
+ return join2(base, "command-code", "cc-catalog.json");
1917
+ }
1918
+ async function loadMeta() {
1919
+ try {
1920
+ const cached = JSON.parse(await readFile(cachePath(), "utf8"));
1921
+ if (cached.fetchedAt && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
1922
+ return new Map(cached.rows ?? []);
1923
+ }
1924
+ } catch {}
1925
+ const meta = parseMpcJson(await runFirst(MPC, ["--json"]));
1926
+ try {
1927
+ const path = cachePath();
1928
+ await mkdir(dirname(path), {
1929
+ recursive: true
1930
+ });
1931
+ await writeFile(path, JSON.stringify({
1932
+ fetchedAt: Date.now(),
1933
+ rows: [...meta]
1934
+ }));
1935
+ } catch {}
1936
+ return meta;
1937
+ }
1938
+ async function loadUsage() {
1939
+ return parseUsageJson(await runFirst(CMDUSE, ["-1", "--json", "--plain"]));
1940
+ }
1941
+
1745
1942
  // src/sidebar/usageDb.ts
1746
1943
  import { Database } from "bun:sqlite";
1747
- import { homedir as homedir2 } from "os";
1748
- import { join as join2 } from "path";
1944
+ import { homedir as homedir3 } from "os";
1945
+ import { join as join3 } from "path";
1749
1946
  function usageDbPath() {
1750
1947
  if (process.env.OPENCODE_DB)
1751
1948
  return process.env.OPENCODE_DB;
1752
- const base = process.env.XDG_DATA_HOME ?? join2(homedir2(), ".local", "share");
1753
- return join2(base, "opencode", "opencode.db");
1949
+ const base = process.env.XDG_DATA_HOME ?? join3(homedir3(), ".local", "share");
1950
+ return join3(base, "opencode", "opencode.db");
1754
1951
  }
1755
- function periodStart(periodEnd, now = Date.now()) {
1756
- const end = periodEnd ? Date.parse(`${periodEnd}T23:59:59`) : Number.NaN;
1952
+ function periodStart(usage, now = Date.now()) {
1953
+ if (typeof usage.periodStartAt === "number")
1954
+ return usage.periodStartAt;
1955
+ const end = usage.periodEnd ? Date.parse(`${usage.periodEnd}T23:59:59`) : Number.NaN;
1757
1956
  if (!Number.isFinite(end))
1758
1957
  return now - 30 * 24 * 60 * 60 * 1000;
1759
1958
  const start = new Date(end);
@@ -1808,38 +2007,8 @@ function loadModelUsage(modelID, sinceMs, path = usageDbPath()) {
1808
2007
  }
1809
2008
  }
1810
2009
 
1811
- // src/tui.tsx
1812
- var ID = "command-code.tui";
2010
+ // src/sidebar/useRows.ts
1813
2011
  var POLL_MS = 60000;
1814
- function Panel(props) {
1815
- return _$createComponent(Show, {
1816
- get when() {
1817
- return props.rows().length > 0;
1818
- },
1819
- get children() {
1820
- var _el$ = _$createElement("box"), _el$2 = _$createElement("text"), _el$3 = _$createElement("b");
1821
- _$insertNode(_el$, _el$2);
1822
- _$insertNode(_el$2, _el$3);
1823
- _$insertNode(_el$3, _$createTextNode(`Command Code`));
1824
- _$insert(_el$, _$createComponent(For, {
1825
- get each() {
1826
- return props.rows();
1827
- },
1828
- children: (row) => (() => {
1829
- var _el$5 = _$createElement("text");
1830
- _$insert(_el$5, (() => {
1831
- var _c$ = _$memo(() => !!row[1]);
1832
- return () => _c$() ? `${row[0]}: ${row[1]}` : row[0];
1833
- })());
1834
- _$effect((_$p) => _$setProp(_el$5, "fg", props.muted(), _$p));
1835
- return _el$5;
1836
- })()
1837
- }), null);
1838
- _$effect((_$p) => _$setProp(_el$2, "fg", props.text(), _$p));
1839
- return _el$;
1840
- }
1841
- });
1842
- }
1843
2012
  function useRows(activeModelId, active) {
1844
2013
  const [usage, setUsage] = createSignal([]);
1845
2014
  const [meta, setMeta] = createSignal(new Map);
@@ -1850,7 +2019,7 @@ function useRows(activeModelId, active) {
1850
2019
  const snapshot = await loadUsage();
1851
2020
  setUsage(usageRows(snapshot));
1852
2021
  const id = activeModelId();
1853
- setModelUsage(id ? loadModelUsage(id, periodStart(snapshot.periodEnd)) ?? undefined : undefined);
2022
+ setModelUsage(id ? loadModelUsage(id, periodStart(snapshot)) ?? undefined : undefined);
1854
2023
  } catch {}
1855
2024
  };
1856
2025
  createEffect(() => {
@@ -1870,17 +2039,20 @@ function useRows(activeModelId, active) {
1870
2039
  tier: found.tier ?? (id ? tierFor(id) : undefined),
1871
2040
  minPlan: id ? minPlan(id) : undefined
1872
2041
  } : undefined;
1873
- return [...usage(), ...modelRows(model, modelUsage())];
2042
+ return [...usage(), separator(), ...modelRows(model, modelUsage())];
1874
2043
  });
1875
2044
  }
2045
+
2046
+ // src/tui.tsx
2047
+ var ID = "command-code.tui";
1876
2048
  var isOurs = (providerID) => Boolean(providerID?.startsWith("command-code"));
2049
+ var themed = (colors) => loadPrefs().colors ? colors : mono(colors);
1877
2050
  function PanelV1(props) {
1878
2051
  const current = () => props.api.state.session.get(props.sessionID)?.model;
1879
2052
  const rows = useRows(() => current()?.id, () => isOurs(current()?.providerID));
1880
- return _$createComponent(Panel, {
2053
+ return _$createComponent2(Panel, {
1881
2054
  rows,
1882
- text: () => props.api.theme.current.text,
1883
- muted: () => props.api.theme.current.textMuted
2055
+ colors: () => themed(legacyColors(props.api.theme.current))
1884
2056
  });
1885
2057
  }
1886
2058
  var tui = async (api) => {
@@ -1888,7 +2060,7 @@ var tui = async (api) => {
1888
2060
  order: 200,
1889
2061
  slots: {
1890
2062
  sidebar_content(_ctx, props) {
1891
- return _$createComponent(PanelV1, {
2063
+ return _$createComponent2(PanelV1, {
1892
2064
  api,
1893
2065
  get sessionID() {
1894
2066
  return props.session_id;
@@ -1904,10 +2076,9 @@ function PanelV2(props) {
1904
2076
  } = props;
1905
2077
  const current = () => ctx.data.session.get(props.sessionID)?.model;
1906
2078
  const rows = useRows(() => current()?.id, () => isOurs(current()?.providerID));
1907
- return _$createComponent(Panel, {
2079
+ return _$createComponent2(Panel, {
1908
2080
  rows,
1909
- text: () => ctx.theme.text.default,
1910
- muted: () => ctx.theme.text.subdued
2081
+ colors: () => themed(hostColors(ctx.theme))
1911
2082
  });
1912
2083
  }
1913
2084
  var commandCodeTui = {
@@ -1915,7 +2086,7 @@ var commandCodeTui = {
1915
2086
  setup(ctx) {
1916
2087
  ctx.ui.slot({
1917
2088
  append: "sidebar.content",
1918
- render: (input) => _$createComponent(PanelV2, {
2089
+ render: (input) => _$createComponent2(PanelV2, {
1919
2090
  ctx,
1920
2091
  get sessionID() {
1921
2092
  return input.sessionID;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeffreyjyz/opencode-command-code",
3
- "version": "0.3.7",
3
+ "version": "0.3.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",