@jeffreyjyz/opencode-command-code 0.1.7 → 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 +19 -0
- package/dist/gating.d.ts +3 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.js +250 -254
- package/dist/v2.d.ts +23 -0
- package/package.json +5 -4
- package/dist/usage.d.ts +0 -42
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:
|
|
3
|
-
|
|
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
|
-
|
|
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,253 +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/
|
|
470
|
-
|
|
471
|
-
var
|
|
472
|
-
var
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
if (n >= 1000)
|
|
481
|
-
return `${round1(n / 1000).toFixed(1)}K`;
|
|
482
|
-
return `${n}`;
|
|
483
|
-
}
|
|
484
|
-
function bar(used, cap) {
|
|
485
|
-
const pct = cap > 0 ? Math.min(Math.max(used / cap, 0), 1) : 0;
|
|
486
|
-
const filled = Math.round(pct * BAR_WIDTH);
|
|
487
|
-
return "\u2588".repeat(filled) + "\u2591".repeat(BAR_WIDTH - filled);
|
|
488
|
-
}
|
|
489
|
-
function pctStr(used, cap) {
|
|
490
|
-
return cap > 0 ? `${Math.round(used / cap * 100)}%` : "\u2014";
|
|
491
|
-
}
|
|
492
|
-
function relTime(resetAtMs, nowSecs) {
|
|
493
|
-
if (resetAtMs === undefined)
|
|
494
|
-
return "unknown";
|
|
495
|
-
const resetSecs = Math.floor(resetAtMs / 1000);
|
|
496
|
-
if (resetSecs <= nowSecs)
|
|
497
|
-
return "resetting\u2026";
|
|
498
|
-
const diff = resetSecs - nowSecs;
|
|
499
|
-
const d = Math.floor(diff / 86400);
|
|
500
|
-
const h = Math.floor(diff % 86400 / 3600);
|
|
501
|
-
const m = Math.floor(diff % 3600 / 60);
|
|
502
|
-
if (d > 0)
|
|
503
|
-
return `${d}d ${h}h`;
|
|
504
|
-
if (h > 0)
|
|
505
|
-
return `${h}h ${m}m`;
|
|
506
|
-
if (m > 0)
|
|
507
|
-
return `${m}m`;
|
|
508
|
-
return "<1m";
|
|
509
|
-
}
|
|
510
|
-
function duration(secs) {
|
|
511
|
-
const d = Math.floor(secs / 86400);
|
|
512
|
-
const h = Math.floor(secs % 86400 / 3600);
|
|
513
|
-
const m = Math.floor(secs % 3600 / 60);
|
|
514
|
-
if (d > 0)
|
|
515
|
-
return `${d}d ${h}h`;
|
|
516
|
-
if (h > 0)
|
|
517
|
-
return `${h}h ${m}m`;
|
|
518
|
-
if (m > 0)
|
|
519
|
-
return `${m}m`;
|
|
520
|
-
return "<1m";
|
|
521
|
-
}
|
|
522
|
-
function elapsedPct(resetAtMs, durSecs, nowSecs) {
|
|
523
|
-
if (resetAtMs === undefined)
|
|
524
|
-
return;
|
|
525
|
-
const resetSecs = Math.floor(resetAtMs / 1000);
|
|
526
|
-
if (resetSecs < durSecs)
|
|
527
|
-
return;
|
|
528
|
-
const start = resetSecs - durSecs;
|
|
529
|
-
if (nowSecs < start)
|
|
530
|
-
return;
|
|
531
|
-
const pct = Math.min(Math.max((nowSecs - start) / durSecs * 100, 0), 100);
|
|
532
|
-
return Math.round(pct);
|
|
533
|
-
}
|
|
534
|
-
function paceEta(resetAtMs, durSecs, used, cap, nowSecs) {
|
|
535
|
-
if (resetAtMs === undefined)
|
|
536
|
-
return;
|
|
537
|
-
const resetSecs = Math.floor(resetAtMs / 1000);
|
|
538
|
-
if (resetSecs < durSecs)
|
|
539
|
-
return;
|
|
540
|
-
const start = resetSecs - durSecs;
|
|
541
|
-
if (nowSecs <= start || nowSecs >= resetSecs)
|
|
542
|
-
return;
|
|
543
|
-
const elapsed = nowSecs - start;
|
|
544
|
-
if (elapsed / durSecs < 0.1)
|
|
545
|
-
return;
|
|
546
|
-
const rate = used / elapsed;
|
|
547
|
-
if (rate <= 0 || used >= cap)
|
|
548
|
-
return;
|
|
549
|
-
const secsToCap = (cap - used) / rate;
|
|
550
|
-
if (secsToCap >= resetSecs - nowSecs)
|
|
551
|
-
return;
|
|
552
|
-
return secsToCap;
|
|
553
|
-
}
|
|
554
|
-
function parseIsoUtc(s) {
|
|
555
|
-
const m = s.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d+)?)(Z|[+-]\d{2}:?\d{2})?$/);
|
|
556
|
-
if (!m)
|
|
557
|
-
return;
|
|
558
|
-
const [, y, mo, d, h, mi, sec, off] = m;
|
|
559
|
-
let offsetMs = 0;
|
|
560
|
-
if (off && off !== "Z") {
|
|
561
|
-
const sign = off.startsWith("-") ? -1 : 1;
|
|
562
|
-
const digits = off.slice(1).replace(":", "");
|
|
563
|
-
const hh = Number(digits.slice(0, 2));
|
|
564
|
-
const mm = Number(digits.slice(2, 4) || "0");
|
|
565
|
-
offsetMs = sign * (hh * 60 + mm) * 60000;
|
|
566
|
-
}
|
|
567
|
-
return Date.UTC(+y, +mo - 1, +d, +h, +mi, Math.floor(+sec)) - offsetMs;
|
|
568
|
-
}
|
|
569
|
-
function monthlyWindow(cap, remaining, periodStart, periodEnd) {
|
|
570
|
-
const used = Math.min(Math.max(cap - remaining, 0), cap);
|
|
571
|
-
const resetAt = periodEnd ? parseIsoUtc(periodEnd) : undefined;
|
|
572
|
-
const start = periodStart ? parseIsoUtc(periodStart) : undefined;
|
|
573
|
-
const durSecs = start !== undefined && resetAt !== undefined ? Math.max(Math.floor((resetAt - start) / 1000), 1) : undefined;
|
|
574
|
-
return { window: { used, cap, resetAt }, durSecs };
|
|
575
|
-
}
|
|
576
|
-
var NAME_RULES = plans_default.nameRules;
|
|
577
|
-
var DEFAULT_NAME = plans_default.defaultName;
|
|
578
|
-
var CAPS = plans_default.caps;
|
|
579
|
-
var PLANS = plans_default.plans;
|
|
580
|
-
function planMonthlyCap(planId) {
|
|
581
|
-
return CAPS[planName(planId)] ?? undefined;
|
|
582
|
-
}
|
|
583
|
-
function planRuleMatched(planId) {
|
|
584
|
-
const id = planId.toLowerCase();
|
|
585
|
-
return NAME_RULES.some((rule) => rule.needles.every((n) => id.includes(n)));
|
|
586
|
-
}
|
|
587
|
-
var unknownPlanWarned = false;
|
|
588
|
-
function warnUnknownPlanOnce(planId) {
|
|
589
|
-
if (unknownPlanWarned)
|
|
590
|
-
return;
|
|
591
|
-
unknownPlanWarned = true;
|
|
592
|
-
console.warn(`[command-code] unknown plan id '${planId}' \u2014 showing Free; update core/plans.json`);
|
|
593
|
-
}
|
|
594
|
-
function planName(planId) {
|
|
595
|
-
const id = planId.toLowerCase();
|
|
596
|
-
for (const rule of NAME_RULES) {
|
|
597
|
-
if (rule.needles.every((n) => id.includes(n)))
|
|
598
|
-
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"
|
|
599
558
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
function
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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;
|
|
610
577
|
}
|
|
611
|
-
function
|
|
612
|
-
const
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
for (const
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
`;
|
|
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;
|
|
620
586
|
}
|
|
621
|
-
|
|
622
|
-
return out;
|
|
587
|
+
return seed;
|
|
623
588
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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)
|
|
630
656
|
return;
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
} else {
|
|
647
|
-
lines.push(`**Credits remaining:** ${money(cr.credits.monthlyCredits)} monthly \xB7 ${money(cr.credits.purchasedCredits ?? 0)} purchased \xB7 ${money(cr.credits.freeCredits ?? 0)} free
|
|
648
|
-
`);
|
|
649
|
-
}
|
|
650
|
-
lines.push("### Usage windows");
|
|
651
|
-
if (cap) {
|
|
652
|
-
const { window, durSecs } = monthlyWindow(cap, cr.credits.monthlyCredits, sub.currentPeriodStart, sub.currentPeriodEnd);
|
|
653
|
-
lines.push(windowLine("Monthly", { ...window, exceeded: false }, nowSecs, durSecs));
|
|
654
|
-
}
|
|
655
|
-
const five = cr.windowLimits?.fiveHour;
|
|
656
|
-
const weekly = cr.windowLimits?.weekly;
|
|
657
|
-
if (five)
|
|
658
|
-
lines.push(windowLine("5-hour", five, nowSecs, FIVE_HOUR_SECS));
|
|
659
|
-
if (weekly)
|
|
660
|
-
lines.push(windowLine("Weekly", weekly, nowSecs, WEEKLY_SECS));
|
|
661
|
-
if (!five && !weekly)
|
|
662
|
-
lines.push(`No rolling windows on this plan (pay-as-you-go credits only).
|
|
663
|
-
`);
|
|
664
|
-
if (summary) {
|
|
665
|
-
lines.push("### This billing period");
|
|
666
|
-
lines.push("| Metric | Value |", "|---|---|");
|
|
667
|
-
lines.push(`| Requests | ${summary.totalCount ?? "\u2014"} |`);
|
|
668
|
-
lines.push(`| Cost | ${summary.totalCost !== undefined ? money(summary.totalCost) : "\u2014"} |`);
|
|
669
|
-
lines.push(`| Tokens in / out | ${summary.totalTokensIn !== undefined ? compact(summary.totalTokensIn) : "\u2014"} / ${summary.totalTokensOut !== undefined ? compact(summary.totalTokensOut) : "\u2014"} |`);
|
|
670
|
-
lines.push(`| Success rate | ${summary.successRate !== undefined ? `${Math.round(summary.successRate)}%` : "\u2014"} |`);
|
|
671
|
-
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();
|
|
672
672
|
}
|
|
673
|
-
|
|
674
|
-
lines.push(plansTable(sub.planId));
|
|
675
|
-
return lines.join(`
|
|
676
|
-
`);
|
|
677
|
-
}
|
|
673
|
+
});
|
|
678
674
|
|
|
679
675
|
// src/index.ts
|
|
680
|
-
var PROVIDER_BASE = `${API_BASE}/provider/v1`;
|
|
681
676
|
var MODEL_CAPABILITIES = {
|
|
682
677
|
temperature: true,
|
|
683
678
|
reasoning: true,
|
|
@@ -762,15 +757,15 @@ var CommandCodePlugin = async (_input) => {
|
|
|
762
757
|
const openDefs = split ? toModelDefs(split.open, "command-code-openai", "@ai-sdk/openai-compatible", { field: "reasoning_content" }) : {};
|
|
763
758
|
const userAnthropic = existing("command-code-anthropic");
|
|
764
759
|
cfg.provider["command-code-anthropic"] = {
|
|
765
|
-
npm: userAnthropic.
|
|
766
|
-
name: "Command Code (Anthropic)",
|
|
760
|
+
npm: userAnthropic.npm ?? "@ai-sdk/anthropic",
|
|
761
|
+
name: userAnthropic.name ?? "Command Code (Anthropic)",
|
|
767
762
|
options: { baseURL: PROVIDER_BASE, ...userAnthropic.options },
|
|
768
763
|
models: { ...claudeDefs, ...userAnthropic.models }
|
|
769
764
|
};
|
|
770
765
|
const userOpenai = existing("command-code-openai");
|
|
771
766
|
cfg.provider["command-code-openai"] = {
|
|
772
|
-
npm: userOpenai.
|
|
773
|
-
name: userOpenai.
|
|
767
|
+
npm: userOpenai.npm ?? "@ai-sdk/openai-compatible",
|
|
768
|
+
name: userOpenai.name ?? "Command Code (OpenAI)",
|
|
774
769
|
options: {
|
|
775
770
|
baseURL: PROVIDER_BASE,
|
|
776
771
|
...openKey ? { apiKey: openKey } : {},
|
|
@@ -799,19 +794,20 @@ var CommandCodePlugin = async (_input) => {
|
|
|
799
794
|
cmd_usage: tool({
|
|
800
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.",
|
|
801
796
|
args: {
|
|
802
|
-
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")
|
|
803
798
|
},
|
|
804
799
|
async execute(args) {
|
|
805
|
-
if (args.arg === "plans")
|
|
806
|
-
return plansTable("");
|
|
807
800
|
const key = await resolveKey();
|
|
808
|
-
return
|
|
801
|
+
return runCmduse(args.arg ?? "", { env: { CMD_API_KEY: key } });
|
|
809
802
|
}
|
|
810
803
|
})
|
|
811
804
|
}
|
|
812
805
|
};
|
|
813
806
|
};
|
|
814
|
-
var src_default =
|
|
807
|
+
var src_default = {
|
|
808
|
+
...commandCodeV2,
|
|
809
|
+
server: CommandCodePlugin
|
|
810
|
+
};
|
|
815
811
|
export {
|
|
816
812
|
CommandCodePlugin,
|
|
817
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.
|
|
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,42 +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
|
-
/** Mirrors cmduse_core::compact. `toFixed` alone disagreed with Rust's `{:.1}`
|
|
6
|
-
* at `.x5` ties; round half-away-from-zero explicitly so both ports match. */
|
|
7
|
-
export declare function compact(n: number): string;
|
|
8
|
-
export declare function bar(used: number, cap: number): string;
|
|
9
|
-
export declare function pctStr(used: number, cap: number): string;
|
|
10
|
-
export declare function relTime(resetAtMs: number | undefined, nowSecs: number): string;
|
|
11
|
-
/** Compact "Xh Ym" / "Xd Yh" for a span of seconds. Pure duration — use this
|
|
12
|
-
* (not relTime) for ETAs/countdowns. Mirrors cmduse_core::duration. */
|
|
13
|
-
export declare function duration(secs: number): string;
|
|
14
|
-
/** % of a rolling window elapsed; window length = durSecs, ends at resetAtMs.
|
|
15
|
-
* undefined when the window hasn't started or the reset is unknown. */
|
|
16
|
-
export declare function elapsedPct(resetAtMs: number | undefined, durSecs: number, nowSecs: number): number | undefined;
|
|
17
|
-
/** Seconds until spend hits cap at the current rate, if before the reset.
|
|
18
|
-
* Suppressed before 10% of the window has elapsed (flat-rate projection is
|
|
19
|
-
* unreliable that early). undefined = no warning. */
|
|
20
|
-
export declare function paceEta(resetAtMs: number | undefined, durSecs: number, used: number, cap: number, nowSecs: number): number | undefined;
|
|
21
|
-
/** ISO 8601 → epoch ms. Handles trailing Z or a ±HH:MM / ±HHMM offset. */
|
|
22
|
-
export declare function parseIsoUtc(s: string): number | undefined;
|
|
23
|
-
/** Assemble the monthly window from the plan cap + subscription period:
|
|
24
|
-
* used = cap − remaining (clamped), reset = period end, durSecs = period
|
|
25
|
-
* length. Mirrors cmduse_core::monthly_window (conformance-pinned). */
|
|
26
|
-
export declare function monthlyWindow(cap: number, remaining: number, periodStart?: string, periodEnd?: string): {
|
|
27
|
-
window: {
|
|
28
|
-
used: number;
|
|
29
|
-
cap: number;
|
|
30
|
-
resetAt?: number;
|
|
31
|
-
};
|
|
32
|
-
durSecs?: number;
|
|
33
|
-
};
|
|
34
|
-
export declare function planMonthlyCap(planId: string): number | undefined;
|
|
35
|
-
/** True when `planId` matched a NAME_RULES entry (not the DEFAULT_NAME
|
|
36
|
-
* fallback). Mirrors cmduse_core::plan_rule_matched. */
|
|
37
|
-
export declare function planRuleMatched(planId: string): boolean;
|
|
38
|
-
export declare function planName(planId: string): string;
|
|
39
|
-
export declare function windowLine(label: string, w: Window, nowSecs: number, durSecs?: number): string;
|
|
40
|
-
export declare function plansTable(current: string): string;
|
|
41
|
-
/** Fetch billing data in parallel and render the markdown dashboard. */
|
|
42
|
-
export declare function renderUsage(key: string): Promise<string>;
|