@jeffreyjyz/opencode-command-code 0.2.4 → 0.2.5

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/index.js CHANGED
@@ -544,65 +544,6 @@ async function loadModels(key) {
544
544
  return { claude, open };
545
545
  }
546
546
 
547
- // src/usagelog.ts
548
- import { appendFileSync, mkdirSync } from "fs";
549
- import { homedir as homedir2 } from "os";
550
- import { dirname, join } from "path";
551
- function usageLogPath() {
552
- if (process.env.MPC_USAGE_LOG)
553
- return process.env.MPC_USAGE_LOG;
554
- const base = process.env.XDG_CACHE_HOME ?? join(homedir2(), ".cache");
555
- return join(base, "mpc", "usage.jsonl");
556
- }
557
- var logged = new Set;
558
- var MAX_SEEN = 5000;
559
- function numberOr(value) {
560
- return typeof value === "number" && Number.isFinite(value) ? value : 0;
561
- }
562
- function logUsage(info) {
563
- try {
564
- if (!info || info.role !== "assistant" || !info.tokens)
565
- return;
566
- const completed = info.time?.completed;
567
- if (!completed)
568
- return;
569
- if (info.id) {
570
- if (logged.has(info.id))
571
- return;
572
- if (logged.size >= MAX_SEEN)
573
- logged.clear();
574
- logged.add(info.id);
575
- }
576
- const input = numberOr(info.tokens.input);
577
- const output = numberOr(info.tokens.output);
578
- const cacheRead = numberOr(info.tokens.cache?.read);
579
- const cacheWrite = numberOr(info.tokens.cache?.write);
580
- if (input + output + cacheRead + cacheWrite === 0)
581
- return;
582
- const line = JSON.stringify({
583
- ts: new Date(completed).toISOString(),
584
- provider: info.providerID,
585
- model: info.modelID,
586
- input,
587
- cacheRead,
588
- cacheWrite,
589
- output,
590
- costUsd: numberOr(info.cost),
591
- messageID: info.id
592
- });
593
- const path = usageLogPath();
594
- mkdirSync(dirname(path), { recursive: true });
595
- appendFileSync(path, `${line}
596
- `);
597
- } catch {}
598
- }
599
- function logEvent(event) {
600
- const e = event;
601
- if (e?.type !== "message.updated")
602
- return;
603
- logUsage(e.properties?.info);
604
- }
605
-
606
547
  // src/v2.ts
607
548
  import { Plugin } from "@opencode/plugin";
608
549
  var PLUGIN_ID = "command-code";
@@ -678,10 +619,6 @@ var commandCodeV2 = Plugin.define({
678
619
  if (!localKey && !hasConnection) {
679
620
  console.warn(`[command-code] no API key found. Run /connect and choose "${INTEGRATION_NAME}", or \`cmd login\` (~/.commandcode/auth.json), or set CMD_API_KEY.`);
680
621
  }
681
- try {
682
- const events = ctx.event;
683
- events?.subscribe?.((event) => logEvent(event));
684
- } catch {}
685
622
  await ctx.integration.transform((editor) => {
686
623
  editor.update(INTEGRATION_ID, (integration) => {
687
624
  if (integration.name === integration.id) {
@@ -836,9 +773,6 @@ var CommandCodePlugin = async (_input) => {
836
773
  }
837
774
  ]
838
775
  },
839
- event: async ({ event }) => {
840
- logEvent(event);
841
- },
842
776
  config: async (cfg) => {
843
777
  cfg.provider ??= {};
844
778
  const existing = (id) => cfg.provider?.[id] ?? {};
@@ -0,0 +1,9 @@
1
+ import { type ModelMeta, type Usage } from "./rows";
2
+ /** Parse `cmduse -1 --json` into the usage snapshot. */
3
+ export declare function parseUsageJson(text: string): Usage;
4
+ /** Parse `mpc --json` into per-model meta, CommandCode side only. */
5
+ export declare function parseMpcJson(text: string): Map<string, ModelMeta>;
6
+ /** mpc's catalog, cached on disk: it scrapes live docs over the network. */
7
+ export declare function loadMeta(): Promise<Map<string, ModelMeta>>;
8
+ /** One usage snapshot from cmduse. Throws when cmduse is missing or fails. */
9
+ export declare function loadUsage(): Promise<Usage>;
@@ -0,0 +1,50 @@
1
+ import { type Category } from "../gating";
2
+ export type SidebarRow = [label: string, value: string];
3
+ export interface ModelMeta {
4
+ key: string;
5
+ name: string;
6
+ tier?: Category;
7
+ allowance?: number;
8
+ rates?: {
9
+ input: number;
10
+ output: number;
11
+ cacheRead: number;
12
+ cacheWrite?: number | null;
13
+ };
14
+ intelligence?: number;
15
+ tps?: number;
16
+ }
17
+ export interface WindowUsage {
18
+ cap?: number;
19
+ used?: number;
20
+ resetAt?: number;
21
+ }
22
+ export interface Usage {
23
+ plan?: string;
24
+ monthlyCap?: number;
25
+ monthlyCredits?: number;
26
+ fiveHour?: WindowUsage;
27
+ weekly?: WindowUsage;
28
+ periodEnd?: string;
29
+ requests?: number;
30
+ cost?: number;
31
+ }
32
+ /** Money at credit scale: cents, but "$60" not "$60.00". */
33
+ export declare function money(value: number): string;
34
+ /** Rate scale needs more precision than credit scale ($0.0036). */
35
+ export declare function rate(value: number): string;
36
+ export declare function count(value: number): string;
37
+ /** "3h 12m" / "1d 20h" / "<1m" for a reset epoch. */
38
+ export declare function until(epoch: number | undefined, now?: number): string;
39
+ /** Plan + rolling windows + period totals, from a cmduse snapshot. */
40
+ 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[];
43
+ /**
44
+ * Canonical key for a model id, mirroring mpc's normalizeKey: lowercase, drop
45
+ * the vendor prefix and any parenthetical, strip punctuation. Used to match the
46
+ * session's active model against the mpc catalog.
47
+ */
48
+ export declare function modelKey(modelId: string): string;
49
+ /** Tier lookup for a model id, from the canonical gating snapshot. */
50
+ export declare function tierFor(modelId: string): Category | undefined;
package/dist/tui.d.ts CHANGED
@@ -1,3 +1,40 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import type { RGBA } from "@opentui/core";
1
3
  import { Plugin } from "@opencode/plugin/tui";
2
- declare const _default: Plugin.Definition;
4
+ interface V1Api {
5
+ slots: {
6
+ register: (config: {
7
+ order?: number;
8
+ slots: {
9
+ sidebar_content: (ctx: unknown, props: {
10
+ session_id: string;
11
+ }) => unknown;
12
+ };
13
+ }) => void;
14
+ };
15
+ state: {
16
+ session: {
17
+ get: (id: string) => {
18
+ model?: {
19
+ providerID: string;
20
+ id: string;
21
+ };
22
+ } | undefined;
23
+ };
24
+ };
25
+ theme: {
26
+ current: {
27
+ text: RGBA;
28
+ textMuted: RGBA;
29
+ };
30
+ };
31
+ }
32
+ /** v1 half — snake_case slot map registered through `api.slots`. */
33
+ export declare const tui: (api: V1Api) => Promise<void>;
34
+ export declare const commandCodeTui: Plugin.Definition;
35
+ declare const _default: {
36
+ tui: (api: V1Api) => Promise<void>;
37
+ id: string;
38
+ setup: (context: Plugin.Context) => Promise<Plugin.Cleanup | void> | Plugin.Cleanup | void;
39
+ };
3
40
  export default _default;
package/dist/tui.js CHANGED
@@ -1,21 +1,21 @@
1
1
  // @bun
2
- // src/tui.ts
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";
3
11
  import { Plugin } from "@opencode/plugin/tui";
12
+ import { For, Show, createMemo, createSignal, onCleanup } from "solid-js";
4
13
 
5
14
  // src/cli.ts
6
15
  import { spawn } from "child_process";
7
16
  var CANDIDATES = ["cmduse", "/opt/homebrew/bin/cmduse", "/usr/local/bin/cmduse"];
8
17
  var DEFAULT_ARGS = ["-1", "--plain"];
9
- var SUBCOMMANDS = new Set([
10
- "plans",
11
- "models",
12
- "daily",
13
- "hourly",
14
- "model",
15
- "session",
16
- "statusline",
17
- "config"
18
- ]);
18
+ var SUBCOMMANDS = new Set(["plans", "models", "daily", "hourly", "model", "session", "statusline", "config"]);
19
19
  function splitCliArgs(input) {
20
20
  const out = [];
21
21
  let cur = "";
@@ -71,13 +71,18 @@ function stderrTail(text) {
71
71
  function runOnce(bin, args, opts) {
72
72
  return new Promise((resolve, reject) => {
73
73
  const child = spawn(bin, args, {
74
- env: { ...process.env, ...opts.env ?? {} },
74
+ env: {
75
+ ...process.env,
76
+ ...opts.env ?? {}
77
+ },
75
78
  stdio: ["ignore", "pipe", "pipe"]
76
79
  });
77
80
  const out = [];
78
81
  const err = [];
79
82
  const onAbort = () => child.kill("SIGTERM");
80
- opts.signal?.addEventListener("abort", onAbort, { once: true });
83
+ opts.signal?.addEventListener("abort", onAbort, {
84
+ once: true
85
+ });
81
86
  child.stdout.on("data", (d) => out.push(d));
82
87
  child.stderr.on("data", (d) => err.push(d));
83
88
  child.on("error", (e) => {
@@ -113,46 +118,618 @@ async function runCmduse(arg, opts = {}) {
113
118
  throw e;
114
119
  }
115
120
  }
116
- throw new Error(`cmduse binary not found (install: brew install JeffreyJYZ/tap/cmduse)${last instanceof Error && last.message !== last.name ? ` \u2014 ${last.message}` : ""}`, { cause: last });
121
+ throw new Error(`cmduse binary not found (install: brew install JeffreyJYZ/tap/cmduse)${last instanceof Error && last.message !== last.name ? ` \u2014 ${last.message}` : ""}`, {
122
+ cause: last
123
+ });
124
+ }
125
+
126
+ // src/sidebar/data.ts
127
+ import { spawn as spawn2 } from "child_process";
128
+ import { mkdir, readFile, writeFile } from "fs/promises";
129
+ import { homedir } from "os";
130
+ import { dirname, join } from "path";
131
+ // ../core/gating.json
132
+ var gating_default = {
133
+ extractedAt: "2026-09-12T06:01:01.385Z",
134
+ cliVersion: "1.38.2",
135
+ categories: {
136
+ "claude-sonnet-5": "premium",
137
+ "claude-sonnet-4-6": "premium",
138
+ "claude-fable-5": "premium",
139
+ "claude-opus-5": "premium",
140
+ "claude-opus-4-8": "premium",
141
+ "claude-opus-4-7": "premium",
142
+ "claude-haiku-4-5-20251001": "premium",
143
+ "gpt-5.6-sol": "opensource",
144
+ "gpt-5.6-terra": "premium",
145
+ "gpt-5.6-luna": "opensource",
146
+ "gpt-5.5": "premium",
147
+ "gpt-5.4": "premium",
148
+ "gpt-5.3-codex": "premium",
149
+ "gpt-5.4-mini": "premium",
150
+ "google/gemini-3.5-flash": "premium",
151
+ "google/gemini-3.1-flash-lite": "premium",
152
+ "sakana/fugu-ultra": "premium",
153
+ "meta/muse-spark-1.1": "premium",
154
+ "meta/muse-spark-1.2": "opensource",
155
+ "meta/muse-spark-1.2-contributor": "opensource",
156
+ "xai/grok-4.5": "opensource",
157
+ "xai/grok-4.6": "opensource",
158
+ "google/gemini-3.7-flash": "opensource",
159
+ "z-ai/glm-5.3-flash": "opensource",
160
+ "tencent/hy4-preview": "opensource",
161
+ "tencent/hy3-paid": "opensource",
162
+ "tencent/Hy3": "opensource",
163
+ "MiniMaxAI/MiniMax-M3-Free": "opensource",
164
+ "MiniMaxAI/MiniMax-M3": "opensource",
165
+ "MiniMaxAI/MiniMax-M2.7": "opensource",
166
+ "minimax/minimax-m3-free": "opensource",
167
+ "minimax/minimax-m2.7-free": "opensource",
168
+ "MiniMaxAI/MiniMax-M2.5": "opensource",
169
+ "deepseek/deepseek-v4-pro": "opensource",
170
+ "deepseek/deepseek-v4-flash": "opensource",
171
+ "moonshotai/Kimi-K3": "opensource",
172
+ "moonshotai/Kimi-K2.7-Code": "opensource",
173
+ "moonshotai/Kimi-K2.7-Code-Highspeed": "opensource",
174
+ "moonshotai/Kimi-K2.6": "opensource",
175
+ "moonshotai/Kimi-K2.5": "opensource",
176
+ "zai-org/GLM-5.3": "opensource",
177
+ "zai-org/GLM-5.2": "opensource",
178
+ "zai-org/GLM-5.2-Fast": "opensource",
179
+ "zai-org/GLM-5.1": "opensource",
180
+ "zai-org/GLM-5": "opensource",
181
+ "xiaomi/mimo-v2.5-pro": "opensource",
182
+ "xiaomi/mimo-v2.5": "opensource",
183
+ "Qwen/Qwen3.6-Max-Preview": "opensource",
184
+ "Qwen/Qwen3.6-Plus": "opensource",
185
+ "Qwen/Qwen3.7-Max": "opensource",
186
+ "Qwen/Qwen3.7-Plus": "opensource",
187
+ "Qwen/Qwen3.8-Max": "opensource",
188
+ "Qwen/Qwen3.8-27B": "opensource",
189
+ "Qwen/Qwen3.8-Flash": "opensource",
190
+ "Qwen/Qwen3.7-Flash": "opensource",
191
+ "stepfun/Step-3.7-Flash": "opensource",
192
+ "stepfun/Step-3.5-Flash": "opensource",
193
+ "nvidia/nemotron-3-ultra-550b-a55b": "opensource",
194
+ "thinkingmachines/inkling": "opensource"
195
+ },
196
+ plans: {
197
+ "individual-go": {
198
+ allowedCategories: [
199
+ "opensource"
200
+ ],
201
+ blockedModels: [
202
+ "vercel-ai-gateway:meta/muse-spark-1.2",
203
+ "vercel-ai-gateway:xai/grok-4.6",
204
+ "openrouter:google/gemini-3.7-flash",
205
+ "vercel-ai-gateway:gpt-5.6-sol"
206
+ ]
207
+ },
208
+ "individual-goat": {
209
+ allowedCategories: [
210
+ "opensource"
211
+ ],
212
+ blockedModels: []
213
+ },
214
+ "individual-pro": {
215
+ allowedCategories: [
216
+ "premium",
217
+ "opensource"
218
+ ],
219
+ blockedModels: [
220
+ "anthropic:claude-fable-5",
221
+ "anthropic:claude-opus-5",
222
+ "anthropic:claude-opus-4-8",
223
+ "anthropic:claude-opus-4-7",
224
+ "anthropic:claude-opus-4-6",
225
+ "anthropic:claude-opus-4-5-20251101",
226
+ "vercel-ai-gateway:sakana/fugu-ultra"
227
+ ]
228
+ },
229
+ "individual-pro-v1": {
230
+ allowedCategories: [
231
+ "premium",
232
+ "opensource"
233
+ ],
234
+ blockedModels: [
235
+ "anthropic:claude-fable-5",
236
+ "anthropic:claude-opus-5",
237
+ "anthropic:claude-opus-4-8",
238
+ "anthropic:claude-opus-4-7",
239
+ "anthropic:claude-opus-4-6",
240
+ "anthropic:claude-opus-4-5-20251101",
241
+ "vercel-ai-gateway:sakana/fugu-ultra"
242
+ ]
243
+ },
244
+ "individual-provider": {
245
+ allowedCategories: [
246
+ "premium",
247
+ "opensource"
248
+ ],
249
+ blockedModels: []
250
+ },
251
+ "individual-max": {
252
+ allowedCategories: [
253
+ "premium",
254
+ "opensource"
255
+ ],
256
+ blockedModels: []
257
+ },
258
+ "individual-ultra": {
259
+ allowedCategories: [
260
+ "premium",
261
+ "opensource"
262
+ ],
263
+ blockedModels: []
264
+ },
265
+ "teams-pro": {
266
+ allowedCategories: [
267
+ "premium",
268
+ "opensource"
269
+ ],
270
+ blockedModels: []
271
+ }
272
+ },
273
+ knownModels: [
274
+ "claude-sonnet-4-6",
275
+ "claude-fable-5",
276
+ "claude-opus-5",
277
+ "claude-opus-4-8",
278
+ "claude-opus-4-7",
279
+ "claude-haiku-4-5-20251001",
280
+ "gpt-5.6-sol",
281
+ "gpt-5.6-terra",
282
+ "gpt-5.6-luna",
283
+ "gpt-5.5",
284
+ "gpt-5.4",
285
+ "gpt-5.3-codex",
286
+ "gpt-5.4-mini",
287
+ "moonshotai/Kimi-K3",
288
+ "thinkingmachines/inkling",
289
+ "thinkingmachines/inkling-small",
290
+ "deepseek/deepseek-v4-pro",
291
+ "deepseek/deepseek-v4-flash",
292
+ "deepseek/deepseek-v4-flash-vision-exp",
293
+ "moonshotai/Kimi-K2.7-Code",
294
+ "moonshotai/Kimi-K2.7-Code-Highspeed",
295
+ "moonshotai/Kimi-K2.6",
296
+ "moonshotai/Kimi-K2.5",
297
+ "zai-org/GLM-5.3",
298
+ "z-ai/glm-5.3-flash",
299
+ "zai-org/GLM-5.2",
300
+ "zai-org/GLM-5.2-Fast",
301
+ "zai-org/GLM-5.1",
302
+ "zai-org/GLM-5",
303
+ "xiaomi/mimo-v2.5-pro",
304
+ "xiaomi/mimo-v2.5",
305
+ "stepfun/Step-3.7-Flash",
306
+ "stepfun/Step-3.5-Flash",
307
+ "tencent/hy4-preview",
308
+ "tencent/hy3-paid",
309
+ "tencent/Hy3",
310
+ "google/gemini-3.7-flash",
311
+ "google/gemini-3.6-flash",
312
+ "google/gemini-3.5-flash",
313
+ "google/gemini-3.5-flash-lite",
314
+ "google/gemini-3.1-flash-lite",
315
+ "sakana/fugu-ultra",
316
+ "xai/grok-4.5",
317
+ "xai/grok-4.6",
318
+ "meta/muse-spark-1.1",
319
+ "meta/muse-spark-1.2",
320
+ "meta/muse-spark-1.2-contributor",
321
+ "nvidia/nemotron-3-ultra-550b-a55b",
322
+ "poolside/laguna-s-2.1-free",
323
+ "inclusionai/ling-3.0-flash-free",
324
+ "minimax/minimax-m3-free",
325
+ "minimax/minimax-m2.7-free",
326
+ "claude-sonnet-5",
327
+ "MiniMaxAI/MiniMax-M3-Free",
328
+ "MiniMaxAI/MiniMax-M3",
329
+ "MiniMaxAI/MiniMax-M2.7",
330
+ "MiniMaxAI/MiniMax-M2.5",
331
+ "Qwen/Qwen3.6-Max-Preview",
332
+ "Qwen/Qwen3.6-Plus",
333
+ "Qwen/Qwen3.7-Max",
334
+ "Qwen/Qwen3.7-Plus",
335
+ "Qwen/Qwen3.8-Max",
336
+ "Qwen/Qwen3.8-27B",
337
+ "Qwen/Qwen3.8-Flash",
338
+ "Qwen/Qwen3.7-Flash"
339
+ ],
340
+ aliases: {
341
+ "claude-sonnet-4-20250514": "claude-sonnet-4-6",
342
+ "claude-sonnet-4-5-20250929": "claude-sonnet-4-6",
343
+ "claude-opus-4-5-20251101": "claude-opus-4-7",
344
+ "claude-opus-4-6": "claude-opus-4-7",
345
+ "claude-haiku-4-5": "claude-haiku-4-5-20251001"
346
+ },
347
+ hardBlocked: {
348
+ "individual-goat": [
349
+ "meta/muse-spark-1.1",
350
+ "google/gemini-3.5-flash",
351
+ "google/gemini-3.6-flash",
352
+ "google/gemini-3.5-flash-lite",
353
+ "google/gemini-3.1-flash-lite"
354
+ ],
355
+ "individual-go": [
356
+ "meta/muse-spark-1.1",
357
+ "google/gemini-3.5-flash",
358
+ "google/gemini-3.6-flash",
359
+ "google/gemini-3.5-flash-lite",
360
+ "google/gemini-3.1-flash-lite",
361
+ "meta/muse-spark-1.2",
362
+ "meta/muse-spark-1.2-contributor",
363
+ "meta/muse-spark-1.3",
364
+ "meta/muse-spark-1.3-contributor"
365
+ ]
366
+ }
367
+ };
368
+
369
+ // src/gating.ts
370
+ var MODEL_CATEGORIES = gating_default.categories;
371
+ var PLAN_RULES = gating_default.plans;
372
+ var HARD_BLOCKED = gating_default.hardBlocked;
373
+ var KNOWN_MODELS = gating_default.knownModels;
374
+ var MODEL_ALIASES = gating_default.aliases;
375
+ var GATE_EXTRACTED_AT = gating_default.extractedAt;
376
+ var GATE_CLI_VERSION = gating_default.cliVersion;
377
+ function findKnown(s) {
378
+ const k = s.toLowerCase();
379
+ return KNOWN_MODELS.find((m) => m.toLowerCase() === k);
380
+ }
381
+ function canonicalizeModelId(model) {
382
+ const stripDate = (s) => s.replace(/[-@]\d{8}$/, "");
383
+ const direct = findKnown(model);
384
+ if (direct)
385
+ return direct;
386
+ const aliased = MODEL_ALIASES[model.toLowerCase()];
387
+ if (aliased)
388
+ return findKnown(aliased) ?? model;
389
+ return findKnown(stripDate(model)) ?? model;
117
390
  }
118
391
 
119
- // src/tui.ts
120
- var tui_default = Plugin.define({
121
- id: "command-code.tui",
122
- setup(context) {
123
- context.ui.slot({
392
+ // src/sidebar/rows.ts
393
+ var TIER_DISPLAY = {
394
+ opensource: "open source",
395
+ premium: "premium"
396
+ };
397
+ function money(value) {
398
+ const rounded = Math.round(value * 100) / 100;
399
+ return `$${Number.isInteger(rounded) ? rounded : rounded.toFixed(2)}`;
400
+ }
401
+ function rate(value) {
402
+ if (value === 0)
403
+ return "$0";
404
+ return `$${Number(value.toFixed(4))}`;
405
+ }
406
+ function count(value) {
407
+ if (value >= 1e6)
408
+ return `${(value / 1e6).toFixed(1)}M`;
409
+ if (value >= 1000)
410
+ return `${(value / 1000).toFixed(1)}K`;
411
+ return String(Math.round(value));
412
+ }
413
+ function until(epoch, now = Date.now()) {
414
+ if (!epoch)
415
+ return "";
416
+ const secs = Math.max(0, Math.round((epoch - now) / 1000));
417
+ if (secs < 60)
418
+ return "<1m";
419
+ const mins = Math.floor(secs / 60);
420
+ if (mins < 60)
421
+ return `${mins}m`;
422
+ const hours = Math.floor(mins / 60);
423
+ if (hours < 24)
424
+ return mins % 60 ? `${hours}h ${mins % 60}m` : `${hours}h`;
425
+ const days = Math.floor(hours / 24);
426
+ return hours % 24 ? `${days}d ${hours % 24}h` : `${days}d`;
427
+ }
428
+ function windowRow(label, w, now) {
429
+ if (!w || typeof w.cap !== "number" || w.cap <= 0)
430
+ return;
431
+ const used = typeof w.used === "number" ? w.used : 0;
432
+ const pct = Math.round(used / w.cap * 100);
433
+ const reset = until(w.resetAt, now);
434
+ return [label, `${money(used)} / ${money(w.cap)} (${pct}%)${reset ? ` \xB7 resets ${reset}` : ""}`];
435
+ }
436
+ function usageRows(usage, now = Date.now()) {
437
+ if (!usage)
438
+ return [];
439
+ const rows = [];
440
+ if (usage.plan) {
441
+ const credits = typeof usage.monthlyCap === "number" ? ` \xB7 $${usage.monthlyCap}/mo credits` : "";
442
+ rows.push(["Plan", `${usage.plan}${credits}`]);
443
+ }
444
+ if (typeof usage.monthlyCap === "number" && typeof usage.monthlyCredits === "number") {
445
+ const used = usage.monthlyCap - usage.monthlyCredits;
446
+ const pct = usage.monthlyCap > 0 ? Math.round(used / usage.monthlyCap * 100) : 0;
447
+ rows.push(["Monthly", `${money(used)} / ${money(usage.monthlyCap)} (${pct}%)`]);
448
+ }
449
+ const five = windowRow("5-hour", usage.fiveHour, now);
450
+ if (five)
451
+ rows.push(five);
452
+ const week = windowRow("Weekly", usage.weekly, now);
453
+ if (week)
454
+ rows.push(week);
455
+ if (typeof usage.requests === "number" || typeof usage.cost === "number") {
456
+ const parts = [];
457
+ if (typeof usage.requests === "number")
458
+ parts.push(`${count(usage.requests)} requests`);
459
+ if (typeof usage.cost === "number")
460
+ parts.push(money(usage.cost));
461
+ rows.push(["Period", parts.join(" \xB7 ")]);
462
+ }
463
+ return rows;
464
+ }
465
+ function modelRows(meta) {
466
+ if (!meta)
467
+ return [];
468
+ const rows = [["Model", meta.name]];
469
+ if (meta.tier)
470
+ rows.push(["Tier", TIER_DISPLAY[meta.tier]]);
471
+ if (typeof meta.allowance === "number")
472
+ rows.push(["Allowance", `${money(meta.allowance)}/mo`]);
473
+ if (meta.rates) {
474
+ rows.push(["Rates", `${rate(meta.rates.input)}/${rate(meta.rates.output)} in/out`]);
475
+ rows.push(["Cache read", rate(meta.rates.cacheRead)]);
476
+ }
477
+ if (typeof meta.intelligence === "number")
478
+ rows.push(["Intelligence", String(meta.intelligence)]);
479
+ if (typeof meta.tps === "number")
480
+ rows.push(["Tok/s", String(meta.tps)]);
481
+ return rows;
482
+ }
483
+ function modelKey(modelId) {
484
+ const bare = modelId.toLowerCase().replace(/\([^)]*\)/g, " ");
485
+ const tail = bare.split("/").pop() ?? bare;
486
+ return tail.replace(/[^a-z0-9]+/g, "");
487
+ }
488
+ function tierFor(modelId) {
489
+ return MODEL_CATEGORIES[canonicalizeModelId(modelId)];
490
+ }
491
+
492
+ // src/sidebar/data.ts
493
+ var CMDUSE = ["cmduse", "/opt/homebrew/bin/cmduse", "/usr/local/bin/cmduse"];
494
+ var MPC = ["mpc", join(homedir(), ".bun/bin/mpc"), "/opt/homebrew/bin/mpc"];
495
+ var CACHE_TTL_MS = 6 * 60 * 60 * 1000;
496
+ function run(bin, args) {
497
+ return new Promise((resolve, reject) => {
498
+ const child = spawn2(bin, args, {
499
+ stdio: ["ignore", "pipe", "pipe"]
500
+ });
501
+ const out = [];
502
+ const err = [];
503
+ child.stdout.on("data", (d) => out.push(d));
504
+ child.stderr.on("data", (d) => err.push(d));
505
+ child.on("error", reject);
506
+ 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)}`)));
507
+ });
508
+ }
509
+ async function runFirst(candidates, args) {
510
+ let last;
511
+ for (const bin of candidates) {
512
+ try {
513
+ return await run(bin, args);
514
+ } catch (e) {
515
+ last = e;
516
+ if (e?.code !== "ENOENT")
517
+ throw e;
518
+ }
519
+ }
520
+ throw last instanceof Error ? last : new Error(`${candidates[0]} not found`);
521
+ }
522
+ function parseUsageJson(text) {
523
+ const raw = JSON.parse(text);
524
+ const win = (v) => {
525
+ if (!v || typeof v !== "object")
526
+ return;
527
+ const w = v;
528
+ return {
529
+ cap: typeof w.cap === "number" ? w.cap : undefined,
530
+ used: typeof w.used === "number" ? w.used : undefined,
531
+ resetAt: typeof w.resetAt === "number" ? w.resetAt : undefined
532
+ };
533
+ };
534
+ const summary = raw.summary ?? {};
535
+ return {
536
+ plan: typeof raw.plan === "string" ? raw.plan : undefined,
537
+ monthlyCap: typeof raw.monthlyCap === "number" ? raw.monthlyCap : undefined,
538
+ monthlyCredits: typeof raw.monthlyCredits === "number" ? raw.monthlyCredits : undefined,
539
+ fiveHour: win(raw.fiveHour),
540
+ weekly: win(raw.weekly),
541
+ periodEnd: typeof raw.periodEnd === "string" ? raw.periodEnd : undefined,
542
+ requests: typeof summary.requests === "number" ? summary.requests : undefined,
543
+ cost: typeof summary.cost === "number" ? summary.cost : undefined
544
+ };
545
+ }
546
+ function parseMpcJson(text) {
547
+ const body = JSON.parse(text);
548
+ const meta = new Map;
549
+ for (const row of body.rows ?? []) {
550
+ const cc = row.cc;
551
+ if (!cc || !row.name)
552
+ continue;
553
+ const entry = {
554
+ key: row.key ?? modelKey(row.name),
555
+ name: row.name,
556
+ allowance: cc.allowance,
557
+ rates: cc.pricing,
558
+ intelligence: cc.ability ?? undefined,
559
+ tps: cc.tps ?? undefined
560
+ };
561
+ meta.set(entry.key, entry);
562
+ meta.set(modelKey(row.name), entry);
563
+ }
564
+ return meta;
565
+ }
566
+ function cachePath() {
567
+ const base = process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache");
568
+ return join(base, "command-code", "cc-catalog.json");
569
+ }
570
+ async function loadMeta() {
571
+ try {
572
+ const cached = JSON.parse(await readFile(cachePath(), "utf8"));
573
+ if (cached.fetchedAt && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
574
+ return new Map(cached.rows ?? []);
575
+ }
576
+ } catch {}
577
+ const meta = parseMpcJson(await runFirst(MPC, ["--json"]));
578
+ try {
579
+ const path = cachePath();
580
+ await mkdir(dirname(path), {
581
+ recursive: true
582
+ });
583
+ await writeFile(path, JSON.stringify({
584
+ fetchedAt: Date.now(),
585
+ rows: [...meta]
586
+ }));
587
+ } catch {}
588
+ return meta;
589
+ }
590
+ async function loadUsage() {
591
+ return parseUsageJson(await runFirst(CMDUSE, ["-1", "--json", "--plain"]));
592
+ }
593
+
594
+ // src/tui.tsx
595
+ var ID = "command-code.tui";
596
+ var POLL_MS = 30000;
597
+ function Panel(props) {
598
+ return _$createComponent(Show, {
599
+ get when() {
600
+ return props.rows().length > 0;
601
+ },
602
+ get children() {
603
+ var _el$ = _$createElement("box"), _el$2 = _$createElement("text"), _el$3 = _$createElement("b");
604
+ _$insertNode(_el$, _el$2);
605
+ _$insertNode(_el$2, _el$3);
606
+ _$insertNode(_el$3, _$createTextNode(`Command Code`));
607
+ _$insert(_el$, _$createComponent(For, {
608
+ get each() {
609
+ return props.rows();
610
+ },
611
+ children: (row) => (() => {
612
+ var _el$5 = _$createElement("text");
613
+ _$insert(_el$5, (() => {
614
+ var _c$ = _$memo(() => !!row[1]);
615
+ return () => _c$() ? `${row[0]}: ${row[1]}` : row[0];
616
+ })());
617
+ _$effect((_$p) => _$setProp(_el$5, "fg", props.muted(), _$p));
618
+ return _el$5;
619
+ })()
620
+ }), null);
621
+ _$effect((_$p) => _$setProp(_el$2, "fg", props.text(), _$p));
622
+ return _el$;
623
+ }
624
+ });
625
+ }
626
+ function useRows(activeModelId, active) {
627
+ const [usage, setUsage] = createSignal([]);
628
+ const [meta, setMeta] = createSignal(new Map);
629
+ loadMeta().then(setMeta).catch(() => {});
630
+ const refresh = async () => {
631
+ try {
632
+ setUsage(usageRows(await loadUsage()));
633
+ } catch {}
634
+ };
635
+ refresh();
636
+ const timer = setInterval(() => void refresh(), POLL_MS);
637
+ onCleanup(() => clearInterval(timer));
638
+ return createMemo(() => {
639
+ if (!active())
640
+ return [];
641
+ const id = activeModelId();
642
+ const found = id ? meta().get(modelKey(id)) ?? meta().get(id) : undefined;
643
+ const model = found ? {
644
+ ...found,
645
+ tier: found.tier ?? (id ? tierFor(id) : undefined)
646
+ } : undefined;
647
+ return [...usage(), ...modelRows(model)];
648
+ });
649
+ }
650
+ var isOurs = (providerID) => Boolean(providerID?.startsWith("command-code"));
651
+ function PanelV1(props) {
652
+ const current = () => props.api.state.session.get(props.sessionID)?.model;
653
+ const rows = useRows(() => current()?.id, () => isOurs(current()?.providerID));
654
+ return _$createComponent(Panel, {
655
+ rows,
656
+ text: () => props.api.theme.current.text,
657
+ muted: () => props.api.theme.current.textMuted
658
+ });
659
+ }
660
+ var tui = async (api) => {
661
+ api.slots.register({
662
+ order: 200,
663
+ slots: {
664
+ sidebar_content(_ctx, props) {
665
+ return _$createComponent(PanelV1, {
666
+ api,
667
+ get sessionID() {
668
+ return props.session_id;
669
+ }
670
+ });
671
+ }
672
+ }
673
+ });
674
+ };
675
+ function PanelV2(props) {
676
+ const {
677
+ ctx
678
+ } = props;
679
+ const current = () => ctx.data.session.get(props.sessionID)?.model;
680
+ const rows = useRows(() => current()?.id, () => isOurs(current()?.providerID));
681
+ return _$createComponent(Panel, {
682
+ rows,
683
+ text: () => ctx.theme.text.default,
684
+ muted: () => ctx.theme.text.subdued
685
+ });
686
+ }
687
+ var commandCodeTui = Plugin.define({
688
+ id: ID,
689
+ setup(ctx) {
690
+ ctx.ui.slot({
691
+ append: "sidebar.content",
692
+ render: (input) => _$createComponent(PanelV2, {
693
+ ctx,
694
+ get sessionID() {
695
+ return input.sessionID;
696
+ }
697
+ })
698
+ });
699
+ ctx.ui.slot({
124
700
  append: "app",
125
701
  render: () => {
126
- context.keymap.layer(() => ({
702
+ ctx.keymap.layer(() => ({
127
703
  mode: "global",
128
704
  priority: 10,
129
- commands: [
130
- {
131
- id: "command-code.cmd-usage",
132
- title: "Command Code usage",
133
- group: "Command Code",
134
- slash: { name: "cmd-usage", arguments: true },
135
- enabled: () => true,
136
- suggested: true,
137
- run: async (input) => {
138
- let text;
139
- try {
140
- text = await runCmduse(input ?? "");
141
- } catch (e) {
142
- context.ui.toast.show({
143
- title: "cmd-usage failed",
144
- message: e instanceof Error ? e.message : String(e),
145
- variant: "error"
146
- });
147
- return;
148
- }
149
- await context.ui.dialog.alert({
150
- title: "Command Code usage",
151
- message: text.trimEnd()
705
+ commands: [{
706
+ id: "command-code.cmd-usage",
707
+ title: "Command Code usage",
708
+ group: "Command Code",
709
+ slash: {
710
+ name: "cmd-usage",
711
+ arguments: true
712
+ },
713
+ enabled: () => true,
714
+ suggested: true,
715
+ run: async (input) => {
716
+ let text;
717
+ try {
718
+ text = await runCmduse(input ?? "");
719
+ } catch (error) {
720
+ ctx.ui.toast.show({
721
+ title: "cmd-usage failed",
722
+ message: error instanceof Error ? error.message : String(error),
723
+ variant: "error"
152
724
  });
725
+ return;
153
726
  }
727
+ await ctx.ui.dialog.alert({
728
+ title: "Command Code usage",
729
+ message: text.trimEnd()
730
+ });
154
731
  }
155
- ],
732
+ }],
156
733
  bindings: ["command-code.cmd-usage"]
157
734
  }));
158
735
  return null;
@@ -160,6 +737,12 @@ var tui_default = Plugin.define({
160
737
  });
161
738
  }
162
739
  });
740
+ var tui_default = {
741
+ ...commandCodeTui,
742
+ tui
743
+ };
163
744
  export {
164
- tui_default as default
745
+ commandCodeTui,
746
+ tui_default as default,
747
+ tui
165
748
  };
package/package.json CHANGED
@@ -1,32 +1,45 @@
1
1
  {
2
- "name": "@jeffreyjyz/opencode-command-code",
3
- "version": "0.2.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
- "type": "module",
6
- "main": "dist/index.js",
7
- "types": "dist/index.d.ts",
8
- "exports": {
9
- ".": "./dist/index.js",
10
- "./tui": "./dist/tui.js"
11
- },
12
- "scripts": {
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 build src/tui.ts --outdir dist --target bun --external @opencode-ai/plugin --external @opencode/plugin --external @opencode/plugin/tui && tsc -p tsconfig.build.json",
14
- "test": "bun test",
15
- "typecheck": "tsc --noEmit"
16
- },
17
- "dependencies": {
18
- "@opencode-ai/plugin": "*",
19
- "@opencode/plugin": "^2.0.11"
20
- },
21
- "devDependencies": {
22
- "@types/bun": "latest",
23
- "solid-js": "^1.9.0",
24
- "typescript": "^5"
25
- },
26
- "files": ["dist", "tui.js", "LICENSE"],
27
- "keywords": ["opencode", "command-code", "plugin", "provider", "ai-sdk"],
28
- "license": "MIT",
29
- "publishConfig": {
30
- "access": "public"
31
- }
2
+ "name": "@jeffreyjyz/opencode-command-code",
3
+ "version": "0.2.5",
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
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": "./dist/index.js",
10
+ "./tui": "./dist/tui.js"
11
+ },
12
+ "scripts": {
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
+ "test": "bun test",
15
+ "typecheck": "tsc --noEmit",
16
+ "build:tui": "bun scripts/build-tui.ts"
17
+ },
18
+ "dependencies": {
19
+ "@opencode-ai/plugin": "*",
20
+ "@opencode/plugin": "^2.0.11",
21
+ "@opentui/core": "^0.5.4",
22
+ "@opentui/solid": "^0.5.4",
23
+ "solid-js": "1.9.12"
24
+ },
25
+ "devDependencies": {
26
+ "@types/bun": "latest",
27
+ "typescript": "^5"
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "tui.js",
32
+ "LICENSE"
33
+ ],
34
+ "keywords": [
35
+ "opencode",
36
+ "command-code",
37
+ "plugin",
38
+ "provider",
39
+ "ai-sdk"
40
+ ],
41
+ "license": "MIT",
42
+ "publishConfig": {
43
+ "access": "public"
44
+ }
32
45
  }
@@ -1,25 +0,0 @@
1
- export interface UsageMessage {
2
- id?: string;
3
- role?: string;
4
- providerID?: string;
5
- modelID?: string;
6
- cost?: number;
7
- tokens?: {
8
- input?: number;
9
- output?: number;
10
- cache?: {
11
- read?: number;
12
- write?: number;
13
- };
14
- };
15
- time?: {
16
- created?: number;
17
- completed?: number;
18
- };
19
- }
20
- /** `$MPC_USAGE_LOG`, else `$XDG_CACHE_HOME/mpc/usage.jsonl`, else `~/.cache/…`. */
21
- export declare function usageLogPath(): string;
22
- /** Append one JSONL line for a finished assistant message. Never throws. */
23
- export declare function logUsage(info: UsageMessage | undefined): void;
24
- /** Narrow an opencode event down to an assistant message update. */
25
- export declare function logEvent(event: unknown): void;