@jeffreyjyz/opencode-command-code 0.5.19 → 0.5.20

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/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # @jeffreyjyz/opencode-command-code
2
+
3
+ Command Code providers, a live gated model list and a usage sidebar for opencode, with all usage rendering delegated to the `cmduse` CLI.
4
+
5
+ [![npm](https://img.shields.io/npm/v/@jeffreyjyz/opencode-command-code?style=flat-square)](https://www.npmjs.com/package/@jeffreyjyz/opencode-command-code) [![CI](https://img.shields.io/github/actions/workflow/status/JeffreyJYZ/cmdcode-tools/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/JeffreyJYZ/cmdcode-tools/actions/workflows/ci.yml) [![license](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE)
6
+
7
+ ## What it is
8
+
9
+ One package, both opencode plugin APIs: v1 hosts call the default export's `server()` hook map, v2 hosts decode `{ id, setup }` and call `setup(context)`. The two halves are independent implementations sharing one CLI layer, and neither reimplements usage math — every window figure comes from the `cmduse` binaries.
10
+
11
+ It registers two provider lanes against `api.commandcode.ai`:
12
+
13
+ - `command-code-anthropic` — Claude models on the Anthropic wire.
14
+ - `command-code-openai` — everything else on the OpenAI-compatible wire, where the reasoning text is round-tripped through `reasoning_content` so a compaction or model switch doesn't get rejected.
15
+
16
+ The model list is fetched live from Command Code's listing API, split by wire, and gated to your plan (`core/gating.json` in the repo is the source of truth). The picker never waits on the network: a static seed from the known-model table, then the last live list cached on disk, paint instantly and the live fetch merges over them — union by model id, so a gated or partial response can never make a model vanish. On v2 hosts the fetch then repeats on an interval, and a tick only touches the provider registry when the model ids actually changed.
17
+
18
+ Each model is registered with Command Code's published $/1M rates, so opencode's own cost display works for a subscription provider instead of showing $0. Vision support and reasoning-effort variants come from a generated catalog, because the listing API publishes no capabilities.
19
+
20
+ Credentials resolve in one order everywhere: `CMD_API_KEY`, then the key `/connect` stored in opencode's credential store, then `~/.commandcode/auth.json` from `cmd login`.
21
+
22
+ ## Install
23
+
24
+ Add the plugin to `opencode.json` (v2 hosts use `plugins`; v1's singular `plugin` is auto-normalized):
25
+
26
+ ```json
27
+ {
28
+ "plugins": ["@jeffreyjyz/opencode-command-code"]
29
+ }
30
+ ```
31
+
32
+ Usage rendering needs the `cmduse` CLI on `PATH` — it owns the plan table, the window math and the formatting:
33
+
34
+ ```sh
35
+ brew install JeffreyJYZ/tap/cmduse
36
+ ```
37
+
38
+ The sidebar's per-model catalog (tier, rates, allowance, deals) comes from `mpc`, which is optional: without it the section keeps the model name and the store-backed rows and simply omits the catalog rows.
39
+
40
+ ```sh
41
+ bun add -g @jeffreyjyz/mpc
42
+ ```
43
+
44
+ ## Usage
45
+
46
+ Run `/connect`, choose **Command Code** and paste an API key (create one at commandcode.ai/settings/keys), or use the `CMD_API_KEY` env method, or log in once with `cmd login`. With no credential at all the providers register as `auto`, so a later `/connect` lights them up without a restart.
47
+
48
+ One caveat on the v1 half: its `/connect` entry is bound to the Anthropic lane (opencode's plugin API allows one auth hook per provider id), so the OpenAI lane shares the key through the resolved `options.apiKey` — a `/connect`-only v1 setup needs `cmd login` or `CMD_API_KEY` for that lane.
49
+
50
+ While a session uses a Command Code, OpenCode Go/Zen or Volcengine ARK model, the sidebar grows one section for that family — plain text unless you ask for colour. Write `~/.config/opencode/command-code.json`:
51
+
52
+ ```json
53
+ { "colors": true, "ocPlan": "go-plus" }
54
+ ```
55
+
56
+ `CMD_COLORS=1` (or `0`) and `CMD_OC_PLAN=go|go-plus` override it for a single run. `ocPlan` only matters on the Go side: the two Go plans share token prices but grant different per-model limits and nothing local records which one you have. The file is re-read every few seconds, so a flip lands on the next poll.
57
+
58
+ What the section shows:
59
+
60
+ - plan, price, monthly credits used, and the 5-hour / weekly windows with used / cap, percent used, elapsed share, reset countdown and a `cap in …` pace estimate; an over-cap window is flagged `LIMIT EXCEEDED`
61
+ - this period's requests and spend, the active model's tier, allowance and $/M rates, its `Usage (this model)` share, any running `Deal`, and this session's own totals
62
+ - on OpenCode Go/Zen and ARK sessions, the same shape from `ocuse` and `arkuse` instead — account meters where the provider publishes them, per-model spend from opencode's own store
63
+
64
+ Usage comes from `cmduse` polled every 5 seconds, read from opencode's message store for the per-model and per-session rows, and cached from `mpc` for the catalog. Every block remembers its last reading, so a remount, a session switch or a network outage repaints instead of blanking; the model figure is keyed by model and session, so switching models never shows the previous model's numbers.
65
+
66
+ ## Commands
67
+
68
+ Each command passes its arguments through to the CLI that owns it, and each has an agent-facing tool of the same name, so the model can fetch the same text without you typing a slash command.
69
+
70
+ | Command | Tool | Spawns |
71
+ |---|---|---|
72
+ | `/cmd-usage` | `cmd_usage` | `cmduse` — plan, credits, windows, billing period; `plans` for the plan table |
73
+ | `/ocuse-usage` | `ocuse_usage` | `ocuse` — OpenCode Go/Zen meters and per-model spend; empty args mean `-1` |
74
+ | `/arkuse-usage` | `arkuse_usage` | `arkuse` — Volcengine ARK Agent Plan meters in AFP; empty args mean `-1` |
75
+ | — | `mpc_prices` | `mpc` — per-model price comparison across Command Code and OpenCode Go plans |
76
+
77
+ A bare `ocuse` or `arkuse` is a live watch loop that would never return to the dialog, so an empty argument means the one-shot dashboard.
78
+
79
+ ## Links
80
+
81
+ - [Source (GitHub)](https://github.com/JeffreyJYZ/cmdcode-tools/tree/main/opencode-plugin)
82
+ - [npm](https://www.npmjs.com/package/@jeffreyjyz/opencode-command-code)
83
+ - [`cmduse` CLI](https://github.com/JeffreyJYZ/cmdcode-tools/tree/main/cmduse) — the usage renderer this plugin spawns
84
+ - [MIT license](LICENSE)
package/dist/index.js CHANGED
@@ -2414,7 +2414,7 @@ function superRefine(fn) {
2414
2414
  // package.json
2415
2415
  var package_default = {
2416
2416
  name: "@jeffreyjyz/opencode-command-code",
2417
- version: "0.5.19",
2417
+ version: "0.5.20",
2418
2418
  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.",
2419
2419
  type: "module",
2420
2420
  main: "dist/index.js",
@@ -30,6 +30,17 @@ export declare function pctTone(pct: number): Tone;
30
30
  export declare function separator(): SidebarRow;
31
31
  /** Truncate to `width`, ellipsis included. */
32
32
  export declare function clip(text: string, width?: number): string;
33
+ /**
34
+ * Whether a usage-API reason is worth a row in the panel.
35
+ *
36
+ * Only a reason that names something the user can fix is: a missing or expired
37
+ * key, a missing CLI/profile, a rejected request. Everything else — a dropped
38
+ * connection, a DNS blip, a timeout — is transport, heals on the next poll, and
39
+ * used to make an `usage API: …` line pop in and out of the sidebar every few
40
+ * seconds. The pattern list is a whitelist on purpose: a new transport wording
41
+ * must stay quiet by default.
42
+ */
43
+ export declare function actionableAccountError(reason: string | undefined): boolean;
33
44
  /** Value budget for a labelled row: the panel draws `label: value`. */
34
45
  export declare function valueWidth(label: string): number;
35
46
  /** How the panel splits a row: a muted label (or banner) and a toned value. */
@@ -45,4 +45,19 @@ export declare function seededZen(): ZenUsage | undefined;
45
45
  export declare function rememberArk(usage: ArkUsage): void;
46
46
  /** The remembered ARK snapshot, or undefined on the first panel. */
47
47
  export declare function seededArk(): ArkUsage | undefined;
48
+ /** The account half both panels share. */
49
+ export type Accounted = {
50
+ account?: unknown;
51
+ accountError?: string;
52
+ };
53
+ /**
54
+ * Keep the previous account meters when this poll only failed on the wire.
55
+ *
56
+ * ocuse/arkuse answer with `accountError` and no `account` for *any* failure, so
57
+ * one dropped request blinked the whole meter block to the local estimate and
58
+ * back 5s later. Only a reason the user can act on may drop the old numbers;
59
+ * a transport fault keeps them, unlabelled, because the next poll restores them
60
+ * anyway.
61
+ */
62
+ export declare function keepAccount<T extends Accounted>(next: T, last: Accounted | undefined): T;
48
63
  export declare function useRows(activeModelId: () => string | undefined, providerID: () => string | undefined, sessionID: () => string | undefined): import("solid-js").Accessor<SidebarRow[]>;
package/dist/tui.js CHANGED
@@ -675,6 +675,11 @@ function separator() {
675
675
  function clip(text, width = ROW_WIDTH) {
676
676
  return text.length <= width ? text : `${text.slice(0, width - 1)}\u2026`;
677
677
  }
678
+ function actionableAccountError(reason) {
679
+ if (!reason)
680
+ return false;
681
+ return /api_key_expired|no Go key|no agent-plan|not found on PATH|HTTP 40[13]|invalid.*key|unauthori[sz]ed/i.test(reason);
682
+ }
678
683
  function valueWidth(label) {
679
684
  return ROW_WIDTH - label.length - 2;
680
685
  }
@@ -2270,7 +2275,7 @@ function arkRows(usage, fallbackName) {
2270
2275
  return [];
2271
2276
  const now = Date.now();
2272
2277
  const rows = [["Plan", clip(`${usage.plan} \xB7 AFP plan${usage.profile ? ` \xB7 ${usage.profile}` : ""}`, valueWidth("Plan")), "base", true]];
2273
- if (usage.accountError) {
2278
+ if (usage.accountError && actionableAccountError(usage.accountError)) {
2274
2279
  rows.push(["", clip(`usage API: ${usage.accountError}`, valueWidth("")), "warn"]);
2275
2280
  }
2276
2281
  const period = monthPeriod(usage);
@@ -2419,7 +2424,7 @@ function zenRows(usage, meta, modelUsage, session, fallbackName, windows) {
2419
2424
  const now = Date.now();
2420
2425
  const oc = meta?.oc;
2421
2426
  const rows = [["Plan", `${oc?.plan ?? "Go"} \xB7 limits per model`, "base"]];
2422
- if (usage.accountError) {
2427
+ if (usage.accountError && actionableAccountError(usage.accountError)) {
2423
2428
  rows.push(["", clip(`usage API: ${usage.accountError}`, valueWidth("")), "warn"]);
2424
2429
  }
2425
2430
  const mine = usage.models.get(modelKey(fallbackName ?? "")) ?? (meta ? usage.models.get(modelKey(meta.name)) : undefined);
@@ -2849,6 +2854,17 @@ function rememberArk(usage) {
2849
2854
  function seededArk() {
2850
2855
  return lastArk;
2851
2856
  }
2857
+ function keepAccount(next, last) {
2858
+ if (next.account || !last?.account)
2859
+ return next;
2860
+ if (actionableAccountError(next.accountError))
2861
+ return next;
2862
+ return {
2863
+ ...next,
2864
+ account: last.account,
2865
+ accountError: undefined
2866
+ };
2867
+ }
2852
2868
  function useRows(activeModelId, providerID, sessionID) {
2853
2869
  const [usage, setUsage] = createSignal(seededRows());
2854
2870
  const [zen, setZen] = createSignal(seededZen());
@@ -2903,13 +2919,13 @@ function useRows(activeModelId, providerID, sessionID) {
2903
2919
  setUsage(usageRows(snapshot));
2904
2920
  refreshModelUsage(activeModelId());
2905
2921
  } else if (kind === "go" || kind === "zen") {
2906
- const snapshot = await loadZen(loadPrefs().ocPlan);
2922
+ const snapshot = keepAccount(await loadZen(loadPrefs().ocPlan), lastZen);
2907
2923
  rememberZen(snapshot);
2908
2924
  setZen(snapshot);
2909
2925
  refreshModelUsage(activeModelId());
2910
2926
  refreshModelWindows(activeModelId());
2911
2927
  } else if (kind === "ark") {
2912
- const snapshot = await loadArk();
2928
+ const snapshot = keepAccount(await loadArk(), lastArk);
2913
2929
  rememberArk(snapshot);
2914
2930
  setArk(snapshot);
2915
2931
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeffreyjyz/opencode-command-code",
3
- "version": "0.5.19",
3
+ "version": "0.5.20",
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",