@jeffreyjyz/opencode-command-code 0.5.18 → 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 +84 -0
- package/dist/index.js +1 -1
- package/dist/sidebar/ark.d.ts +14 -0
- package/dist/sidebar/rows.d.ts +11 -0
- package/dist/sidebar/useRows.d.ts +15 -0
- package/dist/tui.js +55 -10
- package/package.json +1 -1
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
|
+
[](https://www.npmjs.com/package/@jeffreyjyz/opencode-command-code) [](https://github.com/JeffreyJYZ/cmdcode-tools/actions/workflows/ci.yml) [](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.
|
|
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",
|
package/dist/sidebar/ark.d.ts
CHANGED
|
@@ -22,6 +22,15 @@ export interface ArkModel {
|
|
|
22
22
|
weekly: ArkTotals;
|
|
23
23
|
month: ArkTotals;
|
|
24
24
|
}
|
|
25
|
+
/** This store's own view of one window (`arkuse`'s `windows` block): AFP against
|
|
26
|
+
* the plan's published quota, the percentage that yields, and the boundary the
|
|
27
|
+
* row counts down to. Present even when no meter answered, which is what keeps
|
|
28
|
+
* a panel row from degrading to AFP alone. */
|
|
29
|
+
export interface ArkWindow {
|
|
30
|
+
quota?: number;
|
|
31
|
+
percent?: number;
|
|
32
|
+
resetAt?: number;
|
|
33
|
+
}
|
|
25
34
|
export interface ArkUsage {
|
|
26
35
|
plan: string;
|
|
27
36
|
profile?: string;
|
|
@@ -34,6 +43,11 @@ export interface ArkUsage {
|
|
|
34
43
|
weekly: ArkTotals;
|
|
35
44
|
month: ArkTotals;
|
|
36
45
|
daily?: ArkTotals;
|
|
46
|
+
windows?: {
|
|
47
|
+
fiveHour?: ArkWindow;
|
|
48
|
+
weekly?: ArkWindow;
|
|
49
|
+
monthly?: ArkWindow;
|
|
50
|
+
};
|
|
37
51
|
account?: {
|
|
38
52
|
fiveHour: ArkMeter;
|
|
39
53
|
daily?: ArkMeter;
|
package/dist/sidebar/rows.d.ts
CHANGED
|
@@ -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
|
}
|
|
@@ -2146,6 +2151,16 @@ function meter(value) {
|
|
|
2146
2151
|
resetAt: typeof v.resetAt === "number" ? v.resetAt : undefined
|
|
2147
2152
|
};
|
|
2148
2153
|
}
|
|
2154
|
+
function storeWindow(value) {
|
|
2155
|
+
if (!value || typeof value !== "object")
|
|
2156
|
+
return;
|
|
2157
|
+
const v = value;
|
|
2158
|
+
return {
|
|
2159
|
+
quota: typeof v.quota === "number" ? v.quota : undefined,
|
|
2160
|
+
percent: typeof v.percent === "number" ? v.percent : undefined,
|
|
2161
|
+
resetAt: typeof v.resetAt === "number" ? v.resetAt : undefined
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2149
2164
|
function parseArkJson(text) {
|
|
2150
2165
|
let body;
|
|
2151
2166
|
try {
|
|
@@ -2167,6 +2182,7 @@ function parseArkJson(text) {
|
|
|
2167
2182
|
});
|
|
2168
2183
|
}
|
|
2169
2184
|
const summary = body.summary ?? {};
|
|
2185
|
+
const windows = body.windows ?? {};
|
|
2170
2186
|
return {
|
|
2171
2187
|
plan: typeof body.plan === "string" ? body.plan : "medium",
|
|
2172
2188
|
profile: typeof body.profile === "string" ? body.profile : undefined,
|
|
@@ -2180,6 +2196,11 @@ function parseArkJson(text) {
|
|
|
2180
2196
|
weekly: totals(body.weekly),
|
|
2181
2197
|
month: totals(body.monthly),
|
|
2182
2198
|
daily: body.daily ? totals(body.daily) : undefined,
|
|
2199
|
+
windows: body.windows ? {
|
|
2200
|
+
fiveHour: storeWindow(windows.fiveHour),
|
|
2201
|
+
weekly: storeWindow(windows.weekly),
|
|
2202
|
+
monthly: storeWindow(windows.monthly)
|
|
2203
|
+
} : undefined,
|
|
2183
2204
|
account: body.account ? {
|
|
2184
2205
|
fiveHour: meter(account.fiveHour),
|
|
2185
2206
|
daily: account.daily ? meter(account.daily) : undefined,
|
|
@@ -2233,16 +2254,28 @@ function meterRows(label, meter, local, now, durSecs) {
|
|
|
2233
2254
|
exceeded: pct >= 100 || meter.status !== "ok"
|
|
2234
2255
|
}, now, durSecs, `${afp(local.afp)} local${local.requests > 0 ? ` \xB7 ${count(local.requests)} req` : ""}`, (used, cap) => `${brief(used)}/${brief(cap)} (${pctText}%)`);
|
|
2235
2256
|
}
|
|
2236
|
-
function localRows(label, totals) {
|
|
2237
|
-
const
|
|
2238
|
-
|
|
2257
|
+
function localRows(label, totals, window, now, durSecs) {
|
|
2258
|
+
const quota = window?.quota;
|
|
2259
|
+
const percent = window?.percent;
|
|
2260
|
+
if (typeof quota !== "number" || quota <= 0 || typeof percent !== "number") {
|
|
2261
|
+
const detail = totals.requests > 0 ? ` \xB7 ${count(totals.requests)} req` : "";
|
|
2262
|
+
return [[label, `${afp(totals.afp)}${detail}`, "base"]];
|
|
2263
|
+
}
|
|
2264
|
+
const pctText = percent.toFixed(1);
|
|
2265
|
+
return windowRows(label, {
|
|
2266
|
+
cap: quota,
|
|
2267
|
+
used: totals.afp,
|
|
2268
|
+
resetAt: window?.resetAt,
|
|
2269
|
+
sharePct: percent,
|
|
2270
|
+
exceeded: percent >= 100
|
|
2271
|
+
}, now, durSecs, `${count(totals.requests)} req`, (used, cap) => `${brief(used)}/${brief(cap)} (${pctText}% est.)`);
|
|
2239
2272
|
}
|
|
2240
2273
|
function arkRows(usage, fallbackName) {
|
|
2241
2274
|
if (!usage)
|
|
2242
2275
|
return [];
|
|
2243
2276
|
const now = Date.now();
|
|
2244
2277
|
const rows = [["Plan", clip(`${usage.plan} \xB7 AFP plan${usage.profile ? ` \xB7 ${usage.profile}` : ""}`, valueWidth("Plan")), "base", true]];
|
|
2245
|
-
if (usage.accountError) {
|
|
2278
|
+
if (usage.accountError && actionableAccountError(usage.accountError)) {
|
|
2246
2279
|
rows.push(["", clip(`usage API: ${usage.accountError}`, valueWidth("")), "warn"]);
|
|
2247
2280
|
}
|
|
2248
2281
|
const period = monthPeriod(usage);
|
|
@@ -2255,9 +2288,10 @@ function arkRows(usage, fallbackName) {
|
|
|
2255
2288
|
rows.push(...meterRows("Weekly", account.weekly, usage.weekly, now, WEEKLY_SECS));
|
|
2256
2289
|
rows.push(...meterRows("Monthly", account.monthly, usage.month, now, period?.durSecs ?? 0));
|
|
2257
2290
|
} else {
|
|
2258
|
-
|
|
2259
|
-
rows.push(...localRows("
|
|
2260
|
-
rows.push(...localRows("
|
|
2291
|
+
const windows = usage.windows;
|
|
2292
|
+
rows.push(...localRows("5-hour", usage.fiveHour, windows?.fiveHour, now, FIVE_HOUR_SECS));
|
|
2293
|
+
rows.push(...localRows("Weekly", usage.weekly, windows?.weekly, now, WEEKLY_SECS));
|
|
2294
|
+
rows.push(...localRows("Monthly", usage.month, windows?.monthly, now, period?.durSecs ?? 0));
|
|
2261
2295
|
}
|
|
2262
2296
|
const yuan = typeof usage.summary.yuan === "number" ? ` (${money(usage.summary.yuan)})` : "";
|
|
2263
2297
|
rows.push(["Period", clip(`${count(usage.summary.requests)} requests \xB7 ${afp(usage.summary.afp)}${yuan}`, valueWidth("Period")), "base"]);
|
|
@@ -2390,7 +2424,7 @@ function zenRows(usage, meta, modelUsage, session, fallbackName, windows) {
|
|
|
2390
2424
|
const now = Date.now();
|
|
2391
2425
|
const oc = meta?.oc;
|
|
2392
2426
|
const rows = [["Plan", `${oc?.plan ?? "Go"} \xB7 limits per model`, "base"]];
|
|
2393
|
-
if (usage.accountError) {
|
|
2427
|
+
if (usage.accountError && actionableAccountError(usage.accountError)) {
|
|
2394
2428
|
rows.push(["", clip(`usage API: ${usage.accountError}`, valueWidth("")), "warn"]);
|
|
2395
2429
|
}
|
|
2396
2430
|
const mine = usage.models.get(modelKey(fallbackName ?? "")) ?? (meta ? usage.models.get(modelKey(meta.name)) : undefined);
|
|
@@ -2820,6 +2854,17 @@ function rememberArk(usage) {
|
|
|
2820
2854
|
function seededArk() {
|
|
2821
2855
|
return lastArk;
|
|
2822
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
|
+
}
|
|
2823
2868
|
function useRows(activeModelId, providerID, sessionID) {
|
|
2824
2869
|
const [usage, setUsage] = createSignal(seededRows());
|
|
2825
2870
|
const [zen, setZen] = createSignal(seededZen());
|
|
@@ -2874,13 +2919,13 @@ function useRows(activeModelId, providerID, sessionID) {
|
|
|
2874
2919
|
setUsage(usageRows(snapshot));
|
|
2875
2920
|
refreshModelUsage(activeModelId());
|
|
2876
2921
|
} else if (kind === "go" || kind === "zen") {
|
|
2877
|
-
const snapshot = await loadZen(loadPrefs().ocPlan);
|
|
2922
|
+
const snapshot = keepAccount(await loadZen(loadPrefs().ocPlan), lastZen);
|
|
2878
2923
|
rememberZen(snapshot);
|
|
2879
2924
|
setZen(snapshot);
|
|
2880
2925
|
refreshModelUsage(activeModelId());
|
|
2881
2926
|
refreshModelWindows(activeModelId());
|
|
2882
2927
|
} else if (kind === "ark") {
|
|
2883
|
-
const snapshot = await loadArk();
|
|
2928
|
+
const snapshot = keepAccount(await loadArk(), lastArk);
|
|
2884
2929
|
rememberArk(snapshot);
|
|
2885
2930
|
setArk(snapshot);
|
|
2886
2931
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jeffreyjyz/opencode-command-code",
|
|
3
|
-
"version": "0.5.
|
|
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",
|