@jeffreyjyz/opencode-command-code 0.1.6 → 0.2.0

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 ADDED
@@ -0,0 +1,19 @@
1
+ /** Shell-words split: whitespace-separated, with single/double quoting and
2
+ * backslash escapes ("--tz +05:30", arg='a b'). Unterminated quotes close at
3
+ * end of input. */
4
+ export declare function splitCliArgs(input: string): string[];
5
+ /** argv for a cmduse invocation: the default one-shot dashboard plus any
6
+ * pass-through flags, or a subcommand invocation ("plans",
7
+ * "--tz +05:30 daily") passed through untouched. */
8
+ export declare function buildCmduseArgs(arg: string): string[];
9
+ export type RunOptions = {
10
+ signal?: AbortSignal;
11
+ /** Extra env for the child (CMD_API_KEY when the plugin resolved one). */
12
+ env?: Record<string, string>;
13
+ /** Overridable for tests. */
14
+ candidates?: readonly string[];
15
+ };
16
+ /** Run cmduse and return its plain-text stdout. Tries PATH, then common
17
+ * homebrew locations; every candidate failing with ENOENT yields a single
18
+ * install-hint error. */
19
+ export declare function runCmduse(arg: string, opts?: RunOptions): Promise<string>;
package/dist/gating.d.ts CHANGED
@@ -10,6 +10,9 @@ export declare const PLAN_RULES: Record<string, {
10
10
  * category rules. Empirically probed — the CLI bundle misses these because it
11
11
  * tracks category via serving lane, not per-model plan entitlements. */
12
12
  export declare const HARD_BLOCKED: Record<string, string[]>;
13
+ /** canonical known model ids (lowercase compare) */
14
+ declare const KNOWN_MODELS: string[];
15
+ export { KNOWN_MODELS };
13
16
  /** ISO timestamp the snapshot was extracted (absent on pre-metadata files). */
14
17
  export declare const GATE_EXTRACTED_AT: string | undefined;
15
18
  /** Command Code CLI version the snapshot was scraped from. */
package/dist/index.d.ts CHANGED
@@ -1,3 +1,9 @@
1
- import type { Plugin } from "@opencode-ai/plugin";
2
- export declare const CommandCodePlugin: Plugin;
3
- export default CommandCodePlugin;
1
+ import type { Plugin as PluginV1 } from "@opencode-ai/plugin";
2
+ export declare const CommandCodePlugin: PluginV1;
3
+ declare const _default: {
4
+ /** v1 entrypoint (opencode >=1.18.29 object entrypoints). */
5
+ server: PluginV1;
6
+ id: string;
7
+ setup: (context: import("@opencode/plugin/promise/plugin").Context) => Promise<import("@opencode/plugin/promise/plugin").Cleanup | void> | import("@opencode/plugin/promise/plugin").Cleanup | void;
8
+ };
9
+ export default _default;
package/dist/index.js CHANGED
@@ -42,13 +42,124 @@ async function credits(key) {
42
42
  windowLimits: r.windowLimits
43
43
  };
44
44
  }
45
- async function usageSummary(key) {
46
- return getJson("/alpha/usage/summary", key);
47
- }
48
45
  async function providerModels(key) {
49
46
  return getJson("/provider/v1/models", key, 15000);
50
47
  }
51
48
 
49
+ // src/cli.ts
50
+ import { spawn } from "child_process";
51
+ var CANDIDATES = ["cmduse", "/opt/homebrew/bin/cmduse", "/usr/local/bin/cmduse"];
52
+ var DEFAULT_ARGS = ["-1", "--plain"];
53
+ var SUBCOMMANDS = new Set([
54
+ "plans",
55
+ "models",
56
+ "daily",
57
+ "hourly",
58
+ "model",
59
+ "session",
60
+ "statusline",
61
+ "config"
62
+ ]);
63
+ function splitCliArgs(input) {
64
+ const out = [];
65
+ let cur = "";
66
+ let has = false;
67
+ let quote;
68
+ for (let i = 0;i < input.length; i++) {
69
+ const ch = input[i] ?? "";
70
+ if (quote) {
71
+ if (ch === quote) {
72
+ quote = undefined;
73
+ } else if (ch === "\\" && quote === '"' && i + 1 < input.length) {
74
+ cur += input[++i];
75
+ } else {
76
+ cur += ch;
77
+ }
78
+ continue;
79
+ }
80
+ if (ch === '"' || ch === "'") {
81
+ quote = ch;
82
+ has = true;
83
+ } else if (ch === "\\") {
84
+ if (i + 1 < input.length)
85
+ cur += input[++i];
86
+ has = true;
87
+ } else if (/\s/.test(ch)) {
88
+ if (has)
89
+ out.push(cur);
90
+ cur = "";
91
+ has = false;
92
+ } else {
93
+ cur += ch;
94
+ has = true;
95
+ }
96
+ }
97
+ if (has)
98
+ out.push(cur);
99
+ return out;
100
+ }
101
+ function buildCmduseArgs(arg) {
102
+ const tokens = splitCliArgs(arg);
103
+ if (!tokens.length)
104
+ return DEFAULT_ARGS;
105
+ if (tokens.some((t) => SUBCOMMANDS.has(t)))
106
+ return tokens;
107
+ return [...DEFAULT_ARGS, ...tokens];
108
+ }
109
+ function stderrTail(text) {
110
+ const lines = text.trimEnd().split(`
111
+ `);
112
+ return lines.slice(-3).join(`
113
+ `).slice(-400);
114
+ }
115
+ function runOnce(bin, args, opts) {
116
+ return new Promise((resolve, reject) => {
117
+ const child = spawn(bin, args, {
118
+ env: { ...process.env, ...opts.env ?? {} },
119
+ stdio: ["ignore", "pipe", "pipe"]
120
+ });
121
+ const out = [];
122
+ const err = [];
123
+ const onAbort = () => child.kill("SIGTERM");
124
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
125
+ child.stdout.on("data", (d) => out.push(d));
126
+ child.stderr.on("data", (d) => err.push(d));
127
+ child.on("error", (e) => {
128
+ opts.signal?.removeEventListener("abort", onAbort);
129
+ reject(e);
130
+ });
131
+ child.on("close", (code, signal) => {
132
+ opts.signal?.removeEventListener("abort", onAbort);
133
+ const stdout = Buffer.concat(out).toString("utf8");
134
+ if (opts.signal?.aborted) {
135
+ reject(new Error("cmduse invocation aborted"));
136
+ } else if (code === 0) {
137
+ resolve(stdout);
138
+ } else {
139
+ const why = signal ? `killed by ${signal}` : `exit ${code}`;
140
+ const tail = stderrTail(Buffer.concat(err).toString("utf8"));
141
+ reject(new Error(`cmduse ${args.join(" ")}: ${why}${tail ? `
142
+ ${tail}` : ""}`));
143
+ }
144
+ });
145
+ });
146
+ }
147
+ async function runCmduse(arg, opts = {}) {
148
+ const args = buildCmduseArgs(arg);
149
+ const candidates = opts.candidates ?? CANDIDATES;
150
+ let last;
151
+ for (const bin of candidates) {
152
+ try {
153
+ return await runOnce(bin, args, opts);
154
+ } catch (e) {
155
+ last = e;
156
+ if (e?.code !== "ENOENT")
157
+ throw e;
158
+ }
159
+ }
160
+ 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 });
161
+ }
162
+
52
163
  // src/key.ts
53
164
  import { readFile } from "fs/promises";
54
165
  import { homedir } from "os";
@@ -351,7 +462,7 @@ function evaluateModelAccess(model, plan) {
351
462
  return { allowed: true };
352
463
  if (!plan.planId)
353
464
  return { allowed: true };
354
- const canonical = canonicalizeModelId(model);
465
+ const canonical = canonicalizeModelId(bareModel(model));
355
466
  if ((HARD_BLOCKED[plan.planId] ?? []).some((m) => m.toLowerCase() === canonical.toLowerCase())) {
356
467
  return { allowed: false };
357
468
  }
@@ -399,7 +510,8 @@ function splitModels(models) {
399
510
  async function loadModels(key) {
400
511
  warnIfGatingStale();
401
512
  let models;
402
- if (cache && Date.now() - cache.at < CACHE_TTL_MS) {
513
+ const fresh = cache && cache.key === key && Date.now() - cache.at < CACHE_TTL_MS;
514
+ if (fresh) {
403
515
  models = cache.models;
404
516
  } else {
405
517
  try {
@@ -409,9 +521,9 @@ async function loadModels(key) {
409
521
  name: m.name ?? m.id,
410
522
  contextLength: m.context_length ?? 0
411
523
  }));
412
- cache = { at: Date.now(), models };
524
+ cache = { key, at: Date.now(), models };
413
525
  } catch (e) {
414
- if (cache) {
526
+ if (cache && cache.key === key) {
415
527
  models = cache.models;
416
528
  } else {
417
529
  throw new Error("Could not fetch Command Code model list (https://api.commandcode.ai/provider/v1/models). Check network/API key.", { cause: e });
@@ -431,252 +543,136 @@ async function loadModels(key) {
431
543
  const { claude, open } = splitModels(allowed);
432
544
  return { claude, open };
433
545
  }
434
- // ../core/plans.json
435
- var plans_default = {
436
- nameRules: [
437
- { needles: ["enterprise"], name: "Enterprise" },
438
- { needles: ["provider"], name: "Provider" },
439
- { needles: ["team"], name: "Team Pro" },
440
- { needles: ["max", "20"], name: "Max 20x" },
441
- { needles: ["max"], name: "Max 10x" },
442
- { needles: ["goat"], name: "GOAT" },
443
- { needles: ["pro"], name: "Pro" },
444
- { needles: ["go"], name: "Go" }
445
- ],
446
- defaultName: "Free",
447
- caps: {
448
- Enterprise: null,
449
- Provider: null,
450
- "Team Pro": 40,
451
- "Max 20x": 300,
452
- "Max 10x": 150,
453
- GOAT: 70,
454
- Pro: 80,
455
- Go: 10,
456
- Free: null
457
- },
458
- plans: [
459
- { name: "Go", price: "$1", monthly: "$10", fiveHour: "$3", weekly: "$6" },
460
- { name: "GOAT", price: "$10", monthly: "$70", fiveHour: "$14", weekly: "$35" },
461
- { name: "Pro", price: "$20", monthly: "$80", fiveHour: "$16", weekly: "$40" },
462
- { name: "Provider", price: "$15", monthly: "PAYG", fiveHour: "\u2014", weekly: "\u2014" },
463
- { name: "Max 10x", price: "$100", monthly: "$150", fiveHour: "$45", weekly: "$90" },
464
- { name: "Max 20x", price: "$200", monthly: "$300", fiveHour: "$90", weekly: "$180" },
465
- { name: "Team Pro", price: "$40", monthly: "$40", fiveHour: "$12", weekly: "$24" }
466
- ]
467
- };
468
546
 
469
- // src/usage.ts
470
- var BAR_WIDTH = 12;
471
- var FIVE_HOUR_SECS = 5 * 3600;
472
- var WEEKLY_SECS = 7 * 86400;
473
- function money(v) {
474
- return `$${v.toFixed(2)}`;
475
- }
476
- function compact(n) {
477
- if (n >= 1e6)
478
- return `${(n / 1e6).toFixed(1)}M`;
479
- if (n >= 1000)
480
- return `${(n / 1000).toFixed(1)}K`;
481
- return `${n}`;
482
- }
483
- function bar(used, cap) {
484
- const pct = cap > 0 ? Math.min(Math.max(used / cap, 0), 1) : 0;
485
- const filled = Math.round(pct * BAR_WIDTH);
486
- return "\u2588".repeat(filled) + "\u2591".repeat(BAR_WIDTH - filled);
487
- }
488
- function pctStr(used, cap) {
489
- return cap > 0 ? `${Math.round(used / cap * 100)}%` : "\u2014";
490
- }
491
- function relTime(resetAtMs, nowSecs) {
492
- if (resetAtMs === undefined)
493
- return "unknown";
494
- const resetSecs = Math.floor(resetAtMs / 1000);
495
- if (resetSecs <= nowSecs)
496
- return "resetting\u2026";
497
- const diff = resetSecs - nowSecs;
498
- const d = Math.floor(diff / 86400);
499
- const h = Math.floor(diff % 86400 / 3600);
500
- const m = Math.floor(diff % 3600 / 60);
501
- if (d > 0)
502
- return `${d}d ${h}h`;
503
- if (h > 0)
504
- return `${h}h ${m}m`;
505
- if (m > 0)
506
- return `${m}m`;
507
- return "<1m";
508
- }
509
- function duration(secs) {
510
- const d = Math.floor(secs / 86400);
511
- const h = Math.floor(secs % 86400 / 3600);
512
- const m = Math.floor(secs % 3600 / 60);
513
- if (d > 0)
514
- return `${d}d ${h}h`;
515
- if (h > 0)
516
- return `${h}h ${m}m`;
517
- if (m > 0)
518
- return `${m}m`;
519
- return "<1m";
520
- }
521
- function elapsedPct(resetAtMs, durSecs, nowSecs) {
522
- if (resetAtMs === undefined)
523
- return;
524
- const resetSecs = Math.floor(resetAtMs / 1000);
525
- if (resetSecs < durSecs)
526
- return;
527
- const start = resetSecs - durSecs;
528
- if (nowSecs < start)
529
- return;
530
- const pct = Math.min(Math.max((nowSecs - start) / durSecs * 100, 0), 100);
531
- return Math.round(pct);
532
- }
533
- function paceEta(resetAtMs, durSecs, used, cap, nowSecs) {
534
- if (resetAtMs === undefined)
535
- return;
536
- const resetSecs = Math.floor(resetAtMs / 1000);
537
- if (resetSecs < durSecs)
538
- return;
539
- const start = resetSecs - durSecs;
540
- if (nowSecs <= start || nowSecs >= resetSecs)
541
- return;
542
- const elapsed = nowSecs - start;
543
- if (elapsed / durSecs < 0.1)
544
- return;
545
- const rate = used / elapsed;
546
- if (rate <= 0 || used >= cap)
547
- return;
548
- const secsToCap = (cap - used) / rate;
549
- if (secsToCap >= resetSecs - nowSecs)
550
- return;
551
- return secsToCap;
552
- }
553
- function parseIsoUtc(s) {
554
- const m = s.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d+)?)(Z|[+-]\d{2}:?\d{2})?$/);
555
- if (!m)
556
- return;
557
- const [, y, mo, d, h, mi, sec, off] = m;
558
- let offsetMs = 0;
559
- if (off && off !== "Z") {
560
- const sign = off.startsWith("-") ? -1 : 1;
561
- const digits = off.slice(1).replace(":", "");
562
- const hh = Number(digits.slice(0, 2));
563
- const mm = Number(digits.slice(2, 4) || "0");
564
- offsetMs = sign * (hh * 60 + mm) * 60000;
565
- }
566
- return Date.UTC(+y, +mo - 1, +d, +h, +mi, Math.floor(+sec)) - offsetMs;
567
- }
568
- function monthlyWindow(cap, remaining, periodStart, periodEnd) {
569
- const used = Math.min(Math.max(cap - remaining, 0), cap);
570
- const resetAt = periodEnd ? parseIsoUtc(periodEnd) : undefined;
571
- const start = periodStart ? parseIsoUtc(periodStart) : undefined;
572
- const durSecs = start !== undefined && resetAt !== undefined ? Math.max(Math.floor((resetAt - start) / 1000), 1) : undefined;
573
- return { window: { used, cap, resetAt }, durSecs };
574
- }
575
- var NAME_RULES = plans_default.nameRules;
576
- var DEFAULT_NAME = plans_default.defaultName;
577
- var CAPS = plans_default.caps;
578
- var PLANS = plans_default.plans;
579
- function planMonthlyCap(planId) {
580
- return CAPS[planName(planId)] ?? undefined;
581
- }
582
- function planRuleMatched(planId) {
583
- const id = planId.toLowerCase();
584
- return NAME_RULES.some((rule) => rule.needles.every((n) => id.includes(n)));
585
- }
586
- var unknownPlanWarned = false;
587
- function warnUnknownPlanOnce(planId) {
588
- if (unknownPlanWarned)
589
- return;
590
- unknownPlanWarned = true;
591
- console.warn(`[command-code] unknown plan id '${planId}' \u2014 showing Free; update core/plans.json`);
592
- }
593
- function planName(planId) {
594
- const id = planId.toLowerCase();
595
- for (const rule of NAME_RULES) {
596
- if (rule.needles.every((n) => id.includes(n)))
597
- return rule.name;
547
+ // src/v2.ts
548
+ import { Plugin } from "@opencode/plugin";
549
+ var PLUGIN_ID = "command-code";
550
+ var PROVIDER_BASE = "https://api.commandcode.ai/provider/v1";
551
+ var LANES = [
552
+ { id: "command-code-anthropic", name: "Command Code (Anthropic)", pkg: "@opencode/ai/providers/anthropic" },
553
+ {
554
+ id: "command-code-openai",
555
+ name: "Command Code (OpenAI)",
556
+ pkg: "@opencode/ai/providers/openai-compatible",
557
+ reasoningField: "reasoning_content"
598
558
  }
599
- return DEFAULT_NAME;
600
- }
601
- function windowLine(label, w, nowSecs, durSecs) {
602
- const status = w.exceeded ? " \xB7 **LIMIT EXCEEDED**" : "";
603
- const elapsed = durSecs !== undefined ? elapsedPct(w.resetAt, durSecs, nowSecs) : undefined;
604
- const thru = elapsed !== undefined ? ` \xB7 window ${elapsed}% elapsed` : "";
605
- const eta = durSecs !== undefined ? paceEta(w.resetAt, durSecs, w.used, w.cap, nowSecs) : undefined;
606
- const pace = eta !== undefined ? ` \xB7 **on pace to hit cap in ${duration(eta)}**` : "";
607
- return `**${label}** \`${bar(w.used, w.cap)}\` ${money(w.used)} of ${money(w.cap)} (${pctStr(w.used, w.cap)}) \xB7 resets in ${relTime(w.resetAt, nowSecs)}${thru}${pace}${status}
608
- `;
559
+ ];
560
+ var TOOL_DESCRIPTION = "Fetch live Command Code plan/usage: plan name, monthly credits, 5-hour & weekly windows, billing-period summary. Pass arg='plans' for the plan comparison table only, or extra cmduse flags (e.g. '--tz +05:30 daily').";
561
+ function toV2Model(m, lane) {
562
+ const info = {
563
+ id: m.id,
564
+ modelID: m.id,
565
+ providerID: lane.id,
566
+ name: m.name,
567
+ capabilities: { tools: true, input: ["text"], output: ["text"] },
568
+ variants: [],
569
+ time: { released: 0 },
570
+ cost: [],
571
+ status: "active",
572
+ enabled: true,
573
+ limit: { context: m.contextLength || 128000, output: 32000 },
574
+ ...lane.reasoningField ? { compatibility: { reasoningField: lane.reasoningField } } : {}
575
+ };
576
+ return info;
609
577
  }
610
- function plansTable(current) {
611
- const mine = planName(current);
612
- let out = `| Plan | Price | Credits/mo | 5-hour | Weekly |
613
- |---|---|---|---|---|
614
- `;
615
- for (const { name, price, monthly, fiveHour, weekly } of PLANS) {
616
- const mark = name === mine ? "**" : "";
617
- out += `| ${mark}${name}${mark} | ${price}/mo | ${monthly} | ${fiveHour} | ${weekly} |
618
- `;
578
+ function staticSeedModels() {
579
+ const seed = {
580
+ "command-code-anthropic": [],
581
+ "command-code-openai": []
582
+ };
583
+ for (const lane of LANES) {
584
+ const models = KNOWN_MODELS.filter((id) => isClaude(id) === (lane.reasoningField === undefined)).map((id) => toV2Model({ id, name: id, contextLength: 0 }, lane));
585
+ seed[lane.id] = models;
619
586
  }
620
- out += "\nWindows throttle only included monthly credits; on-demand (`/extra`) credits are never throttled.\n";
621
- return out;
587
+ return seed;
622
588
  }
623
- async function renderUsage(key) {
624
- const nowSecs = Math.floor(Date.now() / 1000);
625
- const [sub, cr, summary] = await Promise.all([
626
- subscriptions(key),
627
- credits(key),
628
- usageSummary(key).catch(() => {
589
+ var commandCodeV2 = Plugin.define({
590
+ id: PLUGIN_ID,
591
+ async setup(ctx) {
592
+ let key;
593
+ try {
594
+ key = await resolveKey();
595
+ } catch {}
596
+ if (!key) {
597
+ console.warn("[command-code] no API key found \u2014 providers registered disabled. `cmd login` (~/.commandcode/auth.json), CMD_API_KEY, or providers.command-code-*.settings.apiKey in opencode.jsonc, then restart.");
598
+ }
599
+ const seed = staticSeedModels();
600
+ await ctx.provider.transform((editor) => {
601
+ for (const lane of LANES) {
602
+ editor.update(lane.id, (provider) => {
603
+ if (provider.name === provider.id)
604
+ provider.name = lane.name;
605
+ if (provider.package === "")
606
+ provider.package = lane.pkg;
607
+ provider.activation = key ? "enabled" : "disabled";
608
+ provider.settings = { ...provider.settings, baseURL: PROVIDER_BASE };
609
+ if (key && provider.settings.apiKey === undefined)
610
+ provider.settings.apiKey = key;
611
+ });
612
+ editor.models.set(lane.id, seed[lane.id]);
613
+ }
614
+ });
615
+ await ctx.command.transform((editor) => {
616
+ editor.add({
617
+ name: "cmd-usage",
618
+ description: "Show Command Code plan, credits, and usage windows",
619
+ execute: async ({ sessionID, prompt }) => {
620
+ const arg = typeof prompt?.text === "string" ? prompt.text : "";
621
+ let text;
622
+ try {
623
+ text = await runCmduse(arg, { env: key ? { CMD_API_KEY: key } : undefined });
624
+ } catch (e) {
625
+ text = `cmd-usage failed: ${e instanceof Error ? e.message : String(e)}`;
626
+ }
627
+ await ctx.session.synthetic({ sessionID, text: `**/cmd-usage**
628
+
629
+ ${text}` });
630
+ }
631
+ });
632
+ });
633
+ await ctx.tool.transform((editor) => {
634
+ editor.add({
635
+ name: "cmd_usage",
636
+ description: TOOL_DESCRIPTION,
637
+ input: {
638
+ type: "object",
639
+ properties: {
640
+ arg: {
641
+ type: "string",
642
+ description: "Optional: 'plans' for the plan table only, or extra cmduse flags (e.g. '--tz +05:30 daily')"
643
+ }
644
+ },
645
+ additionalProperties: false
646
+ },
647
+ async execute(input) {
648
+ const arg = typeof input?.arg === "string" ? input.arg : "";
649
+ return {
650
+ content: await runCmduse(arg, { env: key ? { CMD_API_KEY: key } : undefined })
651
+ };
652
+ }
653
+ });
654
+ });
655
+ if (!key)
629
656
  return;
630
- })
631
- ]);
632
- if (sub.planId && sub.planId !== "free" && !planRuleMatched(sub.planId)) {
633
- warnUnknownPlanOnce(sub.planId);
634
- }
635
- const lines = [];
636
- lines.push(`## Command Code \u2014 ${planName(sub.planId)} (${sub.status})
637
- `);
638
- if (sub.currentPeriodEnd)
639
- lines.push(`Billing period ends \`${sub.currentPeriodEnd.slice(0, 10)}\`
640
- `);
641
- const cap = planMonthlyCap(sub.planId);
642
- if (cap) {
643
- lines.push(`**Credits:** ${money(cr.credits.monthlyCredits)} / ${money(cap)} monthly \xB7 ${money(cr.credits.purchasedCredits ?? 0)} purchased \xB7 ${money(cr.credits.freeCredits ?? 0)} free
644
- `);
645
- } else {
646
- lines.push(`**Credits remaining:** ${money(cr.credits.monthlyCredits)} monthly \xB7 ${money(cr.credits.purchasedCredits ?? 0)} purchased \xB7 ${money(cr.credits.freeCredits ?? 0)} free
647
- `);
648
- }
649
- lines.push("### Usage windows");
650
- if (cap) {
651
- const { window, durSecs } = monthlyWindow(cap, cr.credits.monthlyCredits, sub.currentPeriodStart, sub.currentPeriodEnd);
652
- lines.push(windowLine("Monthly", { ...window, exceeded: false }, nowSecs, durSecs));
653
- }
654
- const five = cr.windowLimits?.fiveHour;
655
- const weekly = cr.windowLimits?.weekly;
656
- if (five)
657
- lines.push(windowLine("5-hour", five, nowSecs, FIVE_HOUR_SECS));
658
- if (weekly)
659
- lines.push(windowLine("Weekly", weekly, nowSecs, WEEKLY_SECS));
660
- if (!five && !weekly)
661
- lines.push(`No rolling windows on this plan (pay-as-you-go credits only).
662
- `);
663
- if (summary) {
664
- lines.push("### This billing period");
665
- lines.push("| Metric | Value |", "|---|---|");
666
- lines.push(`| Requests | ${summary.totalCount ?? "\u2014"} |`);
667
- lines.push(`| Cost | ${summary.totalCost !== undefined ? money(summary.totalCost) : "\u2014"} |`);
668
- lines.push(`| Tokens in / out | ${summary.totalTokensIn !== undefined ? compact(summary.totalTokensIn) : "\u2014"} / ${summary.totalTokensOut !== undefined ? compact(summary.totalTokensOut) : "\u2014"} |`);
669
- lines.push(`| Success rate | ${summary.successRate !== undefined ? `${Math.round(summary.successRate)}%` : "\u2014"} |`);
670
- lines.push("");
657
+ const controller = new AbortController;
658
+ (async () => {
659
+ try {
660
+ const split = await loadModels(key);
661
+ if (controller.signal.aborted)
662
+ return;
663
+ await ctx.provider.transform((editor) => {
664
+ editor.models.set("command-code-anthropic", split.claude.map((m) => toV2Model(m, LANES[0])));
665
+ editor.models.set("command-code-openai", split.open.map((m) => toV2Model(m, LANES[1])));
666
+ });
667
+ } catch (e) {
668
+ console.warn("[command-code] live model list unavailable, keeping static seed:", e);
669
+ }
670
+ })();
671
+ return () => controller.abort();
671
672
  }
672
- lines.push("### All plans");
673
- lines.push(plansTable(sub.planId));
674
- return lines.join(`
675
- `);
676
- }
673
+ });
677
674
 
678
675
  // src/index.ts
679
- var PROVIDER_BASE = `${API_BASE}/provider/v1`;
680
676
  var MODEL_CAPABILITIES = {
681
677
  temperature: true,
682
678
  reasoning: true,
@@ -761,15 +757,15 @@ var CommandCodePlugin = async (_input) => {
761
757
  const openDefs = split ? toModelDefs(split.open, "command-code-openai", "@ai-sdk/openai-compatible", { field: "reasoning_content" }) : {};
762
758
  const userAnthropic = existing("command-code-anthropic");
763
759
  cfg.provider["command-code-anthropic"] = {
764
- npm: userAnthropic.options ? undefined : "@ai-sdk/anthropic",
765
- name: "Command Code (Anthropic)",
760
+ npm: userAnthropic.npm ?? "@ai-sdk/anthropic",
761
+ name: userAnthropic.name ?? "Command Code (Anthropic)",
766
762
  options: { baseURL: PROVIDER_BASE, ...userAnthropic.options },
767
763
  models: { ...claudeDefs, ...userAnthropic.models }
768
764
  };
769
765
  const userOpenai = existing("command-code-openai");
770
766
  cfg.provider["command-code-openai"] = {
771
- npm: userOpenai.options ? undefined : "@ai-sdk/openai-compatible",
772
- name: userOpenai.options ? undefined : "Command Code (OpenAI)",
767
+ npm: userOpenai.npm ?? "@ai-sdk/openai-compatible",
768
+ name: userOpenai.name ?? "Command Code (OpenAI)",
773
769
  options: {
774
770
  baseURL: PROVIDER_BASE,
775
771
  ...openKey ? { apiKey: openKey } : {},
@@ -798,19 +794,20 @@ var CommandCodePlugin = async (_input) => {
798
794
  cmd_usage: tool({
799
795
  description: "Fetch live Command Code plan/usage: plan name, monthly credits, 5-hour & weekly windows, billing-period summary. Pass arg=plans for the plan comparison table only.",
800
796
  args: {
801
- arg: tool.schema.string().optional().describe("Optional: 'plans' for the plan table only")
797
+ arg: tool.schema.string().optional().describe("Optional: 'plans' for the plan table only, or extra cmduse flags")
802
798
  },
803
799
  async execute(args) {
804
- if (args.arg === "plans")
805
- return plansTable("");
806
800
  const key = await resolveKey();
807
- return renderUsage(key);
801
+ return runCmduse(args.arg ?? "", { env: { CMD_API_KEY: key } });
808
802
  }
809
803
  })
810
804
  }
811
805
  };
812
806
  };
813
- var src_default = CommandCodePlugin;
807
+ var src_default = {
808
+ ...commandCodeV2,
809
+ server: CommandCodePlugin
810
+ };
814
811
  export {
815
812
  CommandCodePlugin,
816
813
  src_default as default
package/dist/v2.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ import { Plugin } from "@opencode/plugin";
2
+ import { type CmdModel } from "./models";
3
+ export declare const PLUGIN_ID = "command-code";
4
+ export declare const PROVIDER_BASE = "https://api.commandcode.ai/provider/v1";
5
+ type Lane = {
6
+ id: "command-code-anthropic" | "command-code-openai";
7
+ name: string;
8
+ /** v2 runtime package (first-party, distinct from the v1 `npm` value). */
9
+ pkg: string;
10
+ /** Wire field reasoning must round-trip through on the openai-compatible
11
+ * lane; v2 spells the old `interleaved: {field}` as `compatibility`. */
12
+ reasoningField?: string;
13
+ };
14
+ export type { Lane };
15
+ /** API/known model → v2 Model.Info. Every Command Code model shares
16
+ * capabilities and pricing (subscription credits, not per-token); only the
17
+ * open lane needs the reasoning round-trip field. */
18
+ export declare function toV2Model(m: CmdModel, lane: Lane): unknown;
19
+ /** Instant static seed from gating.json's known model ids, split by wire lane.
20
+ * Replaced by the gated live list once the background fetch lands; also the
21
+ * fallback shown when the live fetch fails. */
22
+ export declare function staticSeedModels(): Record<Lane["id"], unknown[]>;
23
+ export declare const commandCodeV2: Plugin.Plugin;
package/package.json CHANGED
@@ -1,17 +1,18 @@
1
1
  {
2
2
  "name": "@jeffreyjyz/opencode-command-code",
3
- "version": "0.1.6",
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.",
3
+ "version": "0.2.0",
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",
7
7
  "types": "dist/index.d.ts",
8
8
  "scripts": {
9
- "build": "bun build src/index.ts --outdir dist --target bun --external @opencode-ai/plugin && tsc -p tsconfig.build.json",
9
+ "build": "rm -rf dist && bun build src/index.ts --outdir dist --target bun --external @opencode-ai/plugin --external @opencode/plugin && tsc -p tsconfig.build.json",
10
10
  "test": "bun test",
11
11
  "typecheck": "tsc --noEmit"
12
12
  },
13
13
  "dependencies": {
14
- "@opencode-ai/plugin": "*"
14
+ "@opencode-ai/plugin": "*",
15
+ "@opencode/plugin": "^2.0.11"
15
16
  },
16
17
  "devDependencies": {
17
18
  "@types/bun": "latest",
package/dist/usage.d.ts DELETED
@@ -1,40 +0,0 @@
1
- import { type Window } from "./api";
2
- export declare const FIVE_HOUR_SECS: number;
3
- export declare const WEEKLY_SECS: number;
4
- export declare function money(v: number): string;
5
- export declare function compact(n: number): string;
6
- export declare function bar(used: number, cap: number): string;
7
- export declare function pctStr(used: number, cap: number): string;
8
- export declare function relTime(resetAtMs: number | undefined, nowSecs: number): string;
9
- /** Compact "Xh Ym" / "Xd Yh" for a span of seconds. Pure duration — use this
10
- * (not relTime) for ETAs/countdowns. Mirrors cmduse_core::duration. */
11
- export declare function duration(secs: number): string;
12
- /** % of a rolling window elapsed; window length = durSecs, ends at resetAtMs.
13
- * undefined when the window hasn't started or the reset is unknown. */
14
- export declare function elapsedPct(resetAtMs: number | undefined, durSecs: number, nowSecs: number): number | undefined;
15
- /** Seconds until spend hits cap at the current rate, if before the reset.
16
- * Suppressed before 10% of the window has elapsed (flat-rate projection is
17
- * unreliable that early). undefined = no warning. */
18
- export declare function paceEta(resetAtMs: number | undefined, durSecs: number, used: number, cap: number, nowSecs: number): number | undefined;
19
- /** ISO 8601 → epoch ms. Handles trailing Z or a ±HH:MM / ±HHMM offset. */
20
- export declare function parseIsoUtc(s: string): number | undefined;
21
- /** Assemble the monthly window from the plan cap + subscription period:
22
- * used = cap − remaining (clamped), reset = period end, durSecs = period
23
- * length. Mirrors cmduse_core::monthly_window (conformance-pinned). */
24
- export declare function monthlyWindow(cap: number, remaining: number, periodStart?: string, periodEnd?: string): {
25
- window: {
26
- used: number;
27
- cap: number;
28
- resetAt?: number;
29
- };
30
- durSecs?: number;
31
- };
32
- export declare function planMonthlyCap(planId: string): number | undefined;
33
- /** True when `planId` matched a NAME_RULES entry (not the DEFAULT_NAME
34
- * fallback). Mirrors cmduse_core::plan_rule_matched. */
35
- export declare function planRuleMatched(planId: string): boolean;
36
- export declare function planName(planId: string): string;
37
- export declare function windowLine(label: string, w: Window, nowSecs: number, durSecs?: number): string;
38
- export declare function plansTable(current: string): string;
39
- /** Fetch billing data in parallel and render the markdown dashboard. */
40
- export declare function renderUsage(key: string): Promise<string>;