@gururea/opencode-commandcode-provider 0.2.1 → 0.3.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/README.md CHANGED
@@ -34,7 +34,7 @@ Restart opencode, then:
34
34
  - **`commandcode_status` tool / `/commandcode-status`.** Shows plugin diagnostics: catalog source (live/cache/fallback), model count, last fetch time, endpoint, zero-data-retention setting, and version.
35
35
  - Command Code API keys (`user_...`) do not expire, so they are stored once and reused.
36
36
 
37
- > The plugin installs the `commandcode-quota.md` and `commandcode-status.md` slash-command files into your project's `.opencode/command/` directory on startup, so the `/commandcode-quota` and `/commandcode-status` commands appear in the TUI autocomplete. The commands invoke the corresponding tools registered by the plugin.
37
+ > On startup the plugin installs `commandcode-quota.md` and `commandcode-status.md` into your project's `.opencode/command/` directory, so the commands appear in the TUI autocomplete. Each command uses a shell block that runs a bundled CLI (`node <package>/dist/cli.js …`), so the output is produced deterministically — it does not rely on the model to call a tool.
38
38
 
39
39
  ## Environment variables
40
40
 
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,763 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { readFileSync as readFileSync2 } from "fs";
5
+ import { homedir } from "os";
6
+ import { dirname as dirname2, join } from "path";
7
+ import { fileURLToPath } from "url";
8
+
9
+ // src/api-key.ts
10
+ import { existsSync, readFileSync } from "fs";
11
+ var PROVIDER_ID = "commandcode";
12
+ function isRecord(value) {
13
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14
+ }
15
+ function authKey(record) {
16
+ const type = record.type;
17
+ if (type === "api") return typeof record.key === "string" ? record.key : void 0;
18
+ if (type === "oauth") return typeof record.access === "string" ? record.access : void 0;
19
+ if (type === "wellknown") {
20
+ return typeof record.token === "string" ? record.token : typeof record.key === "string" ? record.key : void 0;
21
+ }
22
+ return void 0;
23
+ }
24
+ function credentialFromAuthJson(raw) {
25
+ try {
26
+ const parsed = JSON.parse(raw);
27
+ if (!isRecord(parsed)) return void 0;
28
+ const entry = parsed[PROVIDER_ID];
29
+ if (!isRecord(entry)) return void 0;
30
+ return authKey(entry);
31
+ } catch {
32
+ return void 0;
33
+ }
34
+ }
35
+ function defaultAuthPath() {
36
+ return `${process.env.HOME ?? ""}/.local/share/opencode/auth.json`;
37
+ }
38
+ function readCommandCodeKey(options = {}) {
39
+ const env = options.env ?? process.env;
40
+ if (env.COMMAND_CODE_API_KEY) return env.COMMAND_CODE_API_KEY;
41
+ if (env.COMMANDCODE_API_KEY) return env.COMMANDCODE_API_KEY;
42
+ const authContent = options.authContent ?? env.OPENCODE_AUTH_CONTENT;
43
+ if (authContent) {
44
+ const fromContent = credentialFromAuthJson(authContent);
45
+ if (fromContent) return fromContent;
46
+ }
47
+ const authPath = options.authPath ?? defaultAuthPath();
48
+ try {
49
+ if (!existsSync(authPath)) return void 0;
50
+ const raw = readFileSync(authPath, "utf-8");
51
+ return credentialFromAuthJson(raw);
52
+ } catch {
53
+ return void 0;
54
+ }
55
+ }
56
+
57
+ // src/catalog.ts
58
+ var FALLBACK_MODELS = [
59
+ { id: "claude-sonnet-5", name: "Claude Sonnet 5", contextLength: 1e6 },
60
+ { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", contextLength: 1e6 },
61
+ { id: "claude-fable-5", name: "Claude Fable 5", contextLength: 1e6 },
62
+ { id: "claude-opus-5", name: "Claude Opus 5", contextLength: 1e6 },
63
+ { id: "claude-opus-4-8", name: "Claude Opus 4.8", contextLength: 1e6 },
64
+ { id: "claude-opus-4-7", name: "Claude Opus 4.7", contextLength: 1e6 },
65
+ { id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5", contextLength: 2e5 },
66
+ { id: "gpt-5.6-sol", name: "GPT-5.6 Sol", contextLength: 105e4 },
67
+ { id: "gpt-5.6-terra", name: "GPT-5.6 Terra", contextLength: 105e4 },
68
+ { id: "gpt-5.6-luna", name: "GPT-5.6 Luna", contextLength: 105e4 },
69
+ { id: "gpt-5.5", name: "GPT-5.5", contextLength: 4e5 },
70
+ { id: "gpt-5.4", name: "GPT-5.4", contextLength: 4e5 },
71
+ { id: "gpt-5.3-codex", name: "GPT-5.3 Codex", contextLength: 4e5 },
72
+ { id: "gpt-5.4-mini", name: "GPT-5.4 Mini", contextLength: 4e5 },
73
+ { id: "deepseek/deepseek-v4-pro", name: "DeepSeek V4 Pro (latest)", contextLength: 1e6 },
74
+ { id: "deepseek/deepseek-v4-flash", name: "DeepSeek V4 Flash (latest)", contextLength: 1e6 },
75
+ { id: "deepseek/deepseek-v4-flash-vision-exp", name: "DeepSeek V4 Flash Vision (exp)", contextLength: 1e6 },
76
+ { id: "moonshotai/Kimi-K3", name: "Kimi K3", contextLength: 1e6 },
77
+ { id: "moonshotai/Kimi-K2.7-Code", name: "Kimi K2.7 Code", contextLength: 256e3 },
78
+ { id: "moonshotai/Kimi-K2.7-Code-Highspeed", name: "Kimi K2.7 Code HighSpeed", contextLength: 262e3 },
79
+ { id: "moonshotai/Kimi-K2.6", name: "Kimi K2.6", contextLength: 256e3 },
80
+ { id: "moonshotai/Kimi-K2.5", name: "Kimi K2.5", contextLength: 256e3 },
81
+ { id: "z-ai/glm-5.3-flash", name: "GLM-5.3 Flash", contextLength: 1048576 },
82
+ { id: "zai-org/GLM-5.3", name: "GLM-5.3", contextLength: 1e6 },
83
+ { id: "zai-org/GLM-5.2", name: "GLM-5.2", contextLength: 1e6 },
84
+ { id: "zai-org/GLM-5.2-Fast", name: "GLM-5.2 Fast", contextLength: 1e6 },
85
+ { id: "zai-org/GLM-5.1", name: "GLM-5.1", contextLength: 2e5 },
86
+ { id: "zai-org/GLM-5", name: "GLM-5", contextLength: 2e5 },
87
+ { id: "MiniMaxAI/MiniMax-M3", name: "MiniMax M3", contextLength: 1e6 },
88
+ { id: "MiniMaxAI/MiniMax-M2.7", name: "MiniMax M2.7", contextLength: 2e5 },
89
+ { id: "minimax/minimax-m3-free", name: "MiniMax M3", contextLength: 1e6 },
90
+ { id: "minimax/minimax-m2.7-free", name: "MiniMax M2.7", contextLength: 197e3 },
91
+ { id: "MiniMaxAI/MiniMax-M2.5", name: "MiniMax M2.5", contextLength: 2e5 },
92
+ { id: "xiaomi/mimo-v2.5-pro", name: "MiMo V2.5 Pro", contextLength: 1e6 },
93
+ { id: "xiaomi/mimo-v2.5", name: "MiMo V2.5", contextLength: 1e6 },
94
+ { id: "Qwen/Qwen3.8-Max", name: "Qwen 3.8 Max", contextLength: 1e6 },
95
+ { id: "Qwen/Qwen3.8-27B", name: "Qwen 3.8 27B", contextLength: 262144 },
96
+ { id: "Qwen/Qwen3.8-Flash", name: "Qwen 3.8 Flash", contextLength: 1e6 },
97
+ { id: "Qwen/Qwen3.7-Max", name: "Qwen 3.7 Max", contextLength: 1e6 },
98
+ { id: "Qwen/Qwen3.7-Plus", name: "Qwen 3.7 Plus", contextLength: 1e6 },
99
+ { id: "Qwen/Qwen3.7-Flash", name: "Qwen 3.7 Flash", contextLength: 1e6 },
100
+ { id: "Qwen/Qwen3.6-Max-Preview", name: "Qwen 3.6 Max Preview", contextLength: 2e5 },
101
+ { id: "Qwen/Qwen3.6-Plus", name: "Qwen 3.6 Plus", contextLength: 2e5 },
102
+ { id: "stepfun/Step-3.7-Flash", name: "Step 3.7 Flash", contextLength: 256e3 },
103
+ { id: "stepfun/Step-3.5-Flash", name: "Step 3.5 Flash", contextLength: 1e6 },
104
+ { id: "tencent/hy3-paid", name: "Tencent Hy3", contextLength: 262144 },
105
+ { id: "tencent/hy4-preview", name: "Tencent Hy4 Preview", contextLength: 1048576 },
106
+ { id: "google/gemini-3.7-flash", name: "Gemini 3.7 Flash", contextLength: 1048576 },
107
+ { id: "google/gemini-3.6-flash", name: "Gemini 3.6 Flash", contextLength: 1e6 },
108
+ { id: "google/gemini-3.5-flash", name: "Gemini 3.5 Flash", contextLength: 1e6 },
109
+ { id: "google/gemini-3.5-flash-lite", name: "Gemini 3.5 Flash Lite", contextLength: 1e6 },
110
+ { id: "google/gemini-3.1-flash-lite", name: "Gemini 3.1 Flash Lite", contextLength: 1e6 },
111
+ { id: "sakana/fugu-ultra", name: "Fugu Ultra", contextLength: 1e6 },
112
+ { id: "nvidia/nemotron-3-ultra-550b-a55b", name: "Nemotron 3 Ultra", contextLength: 1e6 },
113
+ { id: "thinkingmachines/inkling-small", name: "Inkling Small", contextLength: 1e6 },
114
+ { id: "poolside/laguna-s-2.1-free", name: "Laguna S 2.1", contextLength: 256e3 },
115
+ { id: "meta/muse-spark-1.1", name: "Muse Spark 1.1", contextLength: 1048576 },
116
+ { id: "meta/muse-spark-1.2", name: "Muse Spark 1.2", contextLength: 1048576 },
117
+ { id: "meta/muse-spark-1.2-contributor", name: "Muse Spark 1.2 Contributor", contextLength: 1048576 },
118
+ { id: "xai/grok-4.5", name: "Grok 4.5", contextLength: 5e5 },
119
+ { id: "xai/grok-4.6", name: "Grok 4.6", contextLength: 5e5 }
120
+ ];
121
+
122
+ // src/models.ts
123
+ import { mkdir, readFile, rename, rm, writeFile } from "fs/promises";
124
+ import { dirname } from "path";
125
+ var DEFAULT_PROVIDER_API_BASE = "https://api.commandcode.ai/provider/v1";
126
+ var DEFAULT_MODELS_URL = `${DEFAULT_PROVIDER_API_BASE}/models`;
127
+ var DEFAULT_MODELS_TIMEOUT_MS = 1e4;
128
+ var DEFAULT_MODELS_TTL_MS = 24 * 60 * 60 * 1e3;
129
+ var MODEL_CACHE_VERSION = 1;
130
+ function isRecord2(value) {
131
+ return typeof value === "object" && value !== null && !Array.isArray(value);
132
+ }
133
+ function stringField(record, key) {
134
+ const value = record[key];
135
+ if (typeof value !== "string" || value.length === 0) {
136
+ throw new Error(`Expected ${key} to be a non-empty string`);
137
+ }
138
+ return value;
139
+ }
140
+ function positiveNumberField(record, key) {
141
+ const value = record[key];
142
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
143
+ throw new Error(`Expected ${key} to be a positive number`);
144
+ }
145
+ return value;
146
+ }
147
+ function commandCodeModelsFromApiResponse(value) {
148
+ if (!isRecord2(value)) throw new Error("Expected models response to be an object");
149
+ if (value.object !== "list") throw new Error("Expected models response object to be 'list'");
150
+ if (!Array.isArray(value.data)) throw new Error("Expected models response data to be an array");
151
+ const models = value.data.map((entry) => {
152
+ if (!isRecord2(entry)) throw new Error("Expected model entry to be an object");
153
+ return {
154
+ id: stringField(entry, "id"),
155
+ name: stringField(entry, "name"),
156
+ contextLength: positiveNumberField(entry, "context_length")
157
+ };
158
+ });
159
+ if (models.length === 0) throw new Error("Command Code returned an empty model catalog");
160
+ return models;
161
+ }
162
+ function errorMessage(error) {
163
+ return error instanceof Error ? error.message : String(error);
164
+ }
165
+ async function runWithTimeout(operation, timeoutMs, externalSignal) {
166
+ const controller = new AbortController();
167
+ let timer;
168
+ let onExternalAbort;
169
+ const cleanup = () => {
170
+ if (timer !== void 0) clearTimeout(timer);
171
+ if (externalSignal && onExternalAbort) {
172
+ externalSignal.removeEventListener("abort", onExternalAbort);
173
+ }
174
+ };
175
+ if (externalSignal?.aborted) {
176
+ throw externalSignal.reason ?? new Error("The operation was aborted");
177
+ }
178
+ onExternalAbort = () => controller.abort(externalSignal?.reason);
179
+ if (externalSignal && onExternalAbort) {
180
+ externalSignal.addEventListener("abort", onExternalAbort, { once: true });
181
+ }
182
+ timer = setTimeout(() => controller.abort(new Error(`Model discovery timed out after ${timeoutMs}ms`)), timeoutMs);
183
+ try {
184
+ return await operation(controller.signal);
185
+ } finally {
186
+ cleanup();
187
+ }
188
+ }
189
+ async function fetchCommandCodeModels(options = {}) {
190
+ const url = options.url ?? DEFAULT_MODELS_URL;
191
+ const fetchImpl = options.fetchImpl ?? fetch;
192
+ const timeoutMs = options.timeoutMs !== void 0 && Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : DEFAULT_MODELS_TIMEOUT_MS;
193
+ const body = await runWithTimeout(
194
+ async (signal) => {
195
+ const response = await fetchImpl(url, {
196
+ headers: { accept: "application/json" },
197
+ signal
198
+ });
199
+ if (!response.ok) {
200
+ throw new Error(
201
+ `Failed to fetch Command Code models: ${response.status} ${response.statusText}`
202
+ );
203
+ }
204
+ return await response.json();
205
+ },
206
+ timeoutMs,
207
+ options.signal
208
+ );
209
+ return commandCodeModelsFromApiResponse(body);
210
+ }
211
+ function cacheIsFresh(cachedAt, now, ttlMs) {
212
+ return now - cachedAt < ttlMs;
213
+ }
214
+ function commandCodeModelsFromCache(value) {
215
+ if (!isRecord2(value)) throw new Error("Expected model cache to be an object");
216
+ if (value.version !== MODEL_CACHE_VERSION) {
217
+ throw new Error(`Expected model cache version ${MODEL_CACHE_VERSION}`);
218
+ }
219
+ if (!Array.isArray(value.models)) throw new Error("Expected cached models to be an array");
220
+ const models = value.models.map((entry) => {
221
+ if (!isRecord2(entry)) throw new Error("Expected cached model entry to be an object");
222
+ return {
223
+ id: stringField(entry, "id"),
224
+ name: stringField(entry, "name"),
225
+ contextLength: positiveNumberField(entry, "contextLength")
226
+ };
227
+ });
228
+ if (models.length === 0) throw new Error("Expected cached models to be non-empty");
229
+ return models;
230
+ }
231
+ async function readCommandCodeModelsCache(cachePath) {
232
+ const contents = await readFile(cachePath, "utf-8");
233
+ return commandCodeModelsFromCache(JSON.parse(contents));
234
+ }
235
+ async function writeCommandCodeModelsCache(cachePath, models, fetchedAt = Date.now()) {
236
+ await mkdir(dirname(cachePath), { recursive: true });
237
+ const temporaryPath = `${cachePath}.${process.pid}.tmp`;
238
+ try {
239
+ await writeFile(
240
+ temporaryPath,
241
+ `${JSON.stringify({ version: MODEL_CACHE_VERSION, fetchedAt, models }, null, 2)}
242
+ `,
243
+ { encoding: "utf-8", mode: 384 }
244
+ );
245
+ await rename(temporaryPath, cachePath);
246
+ } finally {
247
+ try {
248
+ await rm(temporaryPath, { force: true });
249
+ } catch {
250
+ }
251
+ }
252
+ }
253
+ async function loadCommandCodeModels(options) {
254
+ const cachePath = options.cachePath;
255
+ const ttlMs = options.ttlMs ?? DEFAULT_MODELS_TTL_MS;
256
+ const now = options.now ?? Date.now;
257
+ try {
258
+ const models = await fetchCommandCodeModels(options);
259
+ const fetchedAt = now();
260
+ try {
261
+ await writeCommandCodeModelsCache(cachePath, models, fetchedAt);
262
+ return { models, source: "live", fetchedAt };
263
+ } catch (error) {
264
+ return {
265
+ models,
266
+ source: "live",
267
+ fetchedAt,
268
+ warning: `Loaded the live Command Code model catalog but could not write the cache at ${cachePath}: ${errorMessage(error)}`
269
+ };
270
+ }
271
+ } catch (liveError) {
272
+ if (options.signal?.aborted) throw liveError;
273
+ try {
274
+ const cachedAt = await cachedTimestamp(cachePath);
275
+ const models = await readCommandCodeModelsCache(cachePath);
276
+ const stale = cachedAt !== null && !cacheIsFresh(cachedAt, now(), ttlMs);
277
+ return {
278
+ models,
279
+ source: "cache",
280
+ fetchedAt: cachedAt ?? void 0,
281
+ warning: stale ? `Could not refresh the Command Code model catalog (${errorMessage(liveError)}). Using a stale cached catalog from ${cachePath}.` : `Could not refresh the Command Code model catalog (${errorMessage(liveError)}). Using the cached catalog from ${cachePath}.`
282
+ };
283
+ } catch (cacheError) {
284
+ const fallback = options.fallbackModels ?? [];
285
+ return {
286
+ models: fallback,
287
+ source: "fallback",
288
+ warning: `Could not refresh the Command Code model catalog (${errorMessage(liveError)}) and no valid cached catalog is available at ${cachePath} (${errorMessage(cacheError)}). Using the built-in fallback catalog.`
289
+ };
290
+ }
291
+ }
292
+ }
293
+ async function cachedTimestamp(cachePath) {
294
+ try {
295
+ const contents = await readFile(cachePath, "utf-8");
296
+ const parsed = JSON.parse(contents);
297
+ if (!isRecord2(parsed)) return null;
298
+ return typeof parsed.fetchedAt === "number" && Number.isFinite(parsed.fetchedAt) ? parsed.fetchedAt : null;
299
+ } catch {
300
+ return null;
301
+ }
302
+ }
303
+
304
+ // src/redact.ts
305
+ function redactBearer(value) {
306
+ return value.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [redacted]");
307
+ }
308
+ function redactCredentials(value) {
309
+ return value.replace(
310
+ /\b(?:api[-_ ]?key|apikey|access[-_ ]?token|refresh[-_ ]?token|token|secret|password|authorization)\s*[=:]\s*[^\s,;&)]+/gi,
311
+ (match) => {
312
+ const separatorIndex = match.search(/[=:]/);
313
+ return separatorIndex < 0 ? "[redacted]" : `${match.slice(0, separatorIndex + 1)}[redacted]`;
314
+ }
315
+ );
316
+ }
317
+ function redactUserTokens(value) {
318
+ return value.replace(/\b(?:user|cc)_[A-Za-z0-9_-]{8,}\b/gi, "[redacted]");
319
+ }
320
+ function redactQuerySecrets(value) {
321
+ return value.replace(
322
+ /([?&](?:api[-_ ]?key|apikey|access_token|refresh_token|token|secret|password)=)[^&#\s]+/gi,
323
+ "$1[redacted]"
324
+ );
325
+ }
326
+ function redactStandaloneSecrets(value) {
327
+ return value.replace(
328
+ /\b(?:sk|rk|ghp|github_pat|xox[baprs])[-_A-Za-z0-9]{16,}\b|\beyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g,
329
+ "[redacted]"
330
+ );
331
+ }
332
+ function redactCommandCodeErrorText(value) {
333
+ return redactStandaloneSecrets(
334
+ redactQuerySecrets(redactUserTokens(redactCredentials(redactBearer(value))))
335
+ );
336
+ }
337
+
338
+ // src/quota.ts
339
+ var DEFAULT_API_BASE = "https://api.commandcode.ai";
340
+ var QUOTA_TIMEOUT_MS = 15e3;
341
+ function isRecord3(value) {
342
+ return typeof value === "object" && value !== null && !Array.isArray(value);
343
+ }
344
+ function numberValue(value) {
345
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
346
+ }
347
+ function stringValue(value) {
348
+ return typeof value === "string" && value.length > 0 ? value : void 0;
349
+ }
350
+ function errorMessage2(error) {
351
+ return error instanceof Error ? error.message : String(error);
352
+ }
353
+ function normalizeResetAt(value) {
354
+ let timestamp;
355
+ if (typeof value === "number" && Number.isFinite(value)) timestamp = value;
356
+ if (typeof value === "string" && value.length > 0) {
357
+ const trimmed = value.trim();
358
+ timestamp = /^\d+$/.test(trimmed) ? Number(trimmed) : Date.parse(trimmed);
359
+ }
360
+ if (timestamp === void 0 || !Number.isFinite(timestamp) || timestamp < 0) return null;
361
+ return timestamp >= 1e12 ? Math.round(timestamp / 1e3) : timestamp;
362
+ }
363
+ function windowLimitsFromCredits(value) {
364
+ if (!isRecord3(value)) return [];
365
+ const limits = [];
366
+ for (const [window, entry] of [
367
+ ["fiveHour", value.fiveHour],
368
+ ["weekly", value.weekly]
369
+ ]) {
370
+ if (!isRecord3(entry)) continue;
371
+ const used = numberValue(entry.used);
372
+ const cap = numberValue(entry.cap);
373
+ if (used === void 0 || cap === void 0 || used === 0 && cap === 0) continue;
374
+ limits.push({ window, used, cap, resetAt: normalizeResetAt(entry.resetAt) });
375
+ }
376
+ return limits;
377
+ }
378
+ function parseCredits(value) {
379
+ if (!isRecord3(value) || !isRecord3(value.credits)) return null;
380
+ const credits = value.credits;
381
+ const monthlyCredits = numberValue(credits.monthlyCredits);
382
+ const purchasedCredits = numberValue(credits.purchasedCredits);
383
+ const freeCredits = numberValue(credits.freeCredits);
384
+ if (monthlyCredits === void 0 && purchasedCredits === void 0 && freeCredits === void 0) {
385
+ return null;
386
+ }
387
+ const monthly = monthlyCredits ?? 0;
388
+ const purchased = purchasedCredits ?? 0;
389
+ const free = freeCredits ?? 0;
390
+ return {
391
+ monthlyCredits: monthly,
392
+ purchasedCredits: purchased,
393
+ freeCredits: free,
394
+ remainingCredits: monthly + purchased + free,
395
+ windowLimits: windowLimitsFromCredits(value.windowLimits)
396
+ };
397
+ }
398
+ function parseSubscription(value) {
399
+ if (!isRecord3(value) || !isRecord3(value.data)) return null;
400
+ const data = value.data;
401
+ const planId = stringValue(data.planId);
402
+ const status = stringValue(data.status);
403
+ const currentPeriodStart = stringValue(data.currentPeriodStart);
404
+ const currentPeriodEnd = stringValue(data.currentPeriodEnd);
405
+ if (!planId && !status && !currentPeriodStart && !currentPeriodEnd) return null;
406
+ return {
407
+ planId: planId ?? null,
408
+ status: status ?? null,
409
+ currentPeriodStart: currentPeriodStart ?? null,
410
+ currentPeriodEnd: currentPeriodEnd ?? null
411
+ };
412
+ }
413
+ function parseSummary(value) {
414
+ if (!isRecord3(value)) return null;
415
+ const totalCost = numberValue(value.totalCost);
416
+ const totalCount = numberValue(value.totalCount);
417
+ if (totalCost === void 0 || totalCount === void 0) return null;
418
+ const totalTokens = numberValue(value.totalTokens) ?? numberValue(value.tokens);
419
+ return { totalCost, totalCount, ...totalTokens === void 0 ? {} : { totalTokens } };
420
+ }
421
+ function parseWhoami(value) {
422
+ if (!isRecord3(value)) return null;
423
+ const org = isRecord3(value.org) ? value.org : void 0;
424
+ const user = isRecord3(value.user) ? value.user : void 0;
425
+ const login = (org ? stringValue(org.login) : void 0) ?? (user ? stringValue(user.userName) ?? stringValue(user.name) : void 0);
426
+ if (!login) return null;
427
+ const orgId = org ? stringValue(org.id) : void 0;
428
+ const keyName = user ? stringValue(user.keyName) ?? stringValue(user.displayName) : void 0;
429
+ return { login, orgId: orgId ?? null, ...keyName ? { keyName } : {} };
430
+ }
431
+ function buildUrl(path, params) {
432
+ const search = new URLSearchParams();
433
+ for (const [key, value] of Object.entries(params)) {
434
+ if (value) search.set(key, value);
435
+ }
436
+ const query = search.toString();
437
+ return `${path}${query ? `?${query}` : ""}`;
438
+ }
439
+ function isHttpError(value) {
440
+ return isRecord3(value) && value.__httpError === true && typeof value.message === "string" && typeof value.status === "number" && typeof value.body === "string";
441
+ }
442
+ function isQuotaError(value) {
443
+ return isRecord3(value) && value.__quotaError === true && (value.kind === "timeout" || value.kind === "network");
444
+ }
445
+ function isBlockingHttpError(error) {
446
+ return error.status === 401 || error.status === 403;
447
+ }
448
+ function httpFailure(error, context) {
449
+ const detail = error.body.trim().slice(0, 200);
450
+ return {
451
+ ok: false,
452
+ error: {
453
+ kind: "http",
454
+ message: redactCommandCodeErrorText(
455
+ `${context} request failed (${error.status}): ${detail || error.message}`
456
+ )
457
+ }
458
+ };
459
+ }
460
+ var QuotaTimeoutError = class extends Error {
461
+ };
462
+ async function fetchCommandCodeQuota(options) {
463
+ if (!options.apiKey) {
464
+ return { ok: false, error: { message: "No Command Code API key found", kind: "config" } };
465
+ }
466
+ const baseUrl = options.baseUrl ?? DEFAULT_API_BASE;
467
+ const fetchImpl = options.fetchImpl ?? fetch;
468
+ const timeoutMs = options.timeoutMs ?? QUOTA_TIMEOUT_MS;
469
+ const overallController = new AbortController();
470
+ const overallTimer = setTimeout(() => overallController.abort(), timeoutMs);
471
+ const headers = {
472
+ accept: "application/json",
473
+ Authorization: `Bearer ${options.apiKey}`,
474
+ ...options.extraHeaders
475
+ };
476
+ const request = async (path) => {
477
+ if (overallController.signal.aborted) throw new QuotaTimeoutError();
478
+ try {
479
+ const response = await fetchImpl(`${baseUrl}${path}`, {
480
+ method: "GET",
481
+ headers,
482
+ signal: overallController.signal
483
+ });
484
+ if (!response.ok) {
485
+ return {
486
+ __httpError: true,
487
+ message: response.status === 401 || response.status === 403 ? "Command Code rejected the API key" : response.statusText,
488
+ status: response.status,
489
+ body: await response.text().catch(() => "")
490
+ };
491
+ }
492
+ return await response.json();
493
+ } catch (error) {
494
+ if (overallController.signal.aborted) throw new QuotaTimeoutError();
495
+ throw error;
496
+ }
497
+ };
498
+ const safeRequest = async (path) => {
499
+ try {
500
+ return await request(path);
501
+ } catch (error) {
502
+ return {
503
+ __quotaError: true,
504
+ kind: error instanceof QuotaTimeoutError ? "timeout" : "network"
505
+ };
506
+ }
507
+ };
508
+ try {
509
+ const whoamiRaw = await request("/alpha/whoami");
510
+ if (isHttpError(whoamiRaw)) return httpFailure(whoamiRaw, "whoami");
511
+ const account = parseWhoami(whoamiRaw);
512
+ if (!account) {
513
+ return {
514
+ ok: false,
515
+ error: { kind: "http", message: "Command Code returned an unrecognized account response" }
516
+ };
517
+ }
518
+ const orgId = account.orgId ?? void 0;
519
+ const [creditsRaw, subscriptionRaw] = await Promise.all([
520
+ safeRequest(buildUrl("/alpha/billing/credits", { orgId })),
521
+ safeRequest(buildUrl("/alpha/billing/subscriptions", { orgId }))
522
+ ]);
523
+ if (isHttpError(creditsRaw) && isBlockingHttpError(creditsRaw)) {
524
+ return httpFailure(creditsRaw, "credits");
525
+ }
526
+ if (isHttpError(subscriptionRaw) && isBlockingHttpError(subscriptionRaw)) {
527
+ return httpFailure(subscriptionRaw, "subscription");
528
+ }
529
+ const unavailable = [];
530
+ const credits = isHttpError(creditsRaw) || isQuotaError(creditsRaw) ? null : parseCredits(creditsRaw);
531
+ if (!credits) unavailable.push("credits");
532
+ const subscription = isHttpError(subscriptionRaw) || isQuotaError(subscriptionRaw) ? null : parseSubscription(subscriptionRaw);
533
+ if (!subscription) unavailable.push("subscription");
534
+ const summaryRaw = await safeRequest(
535
+ buildUrl("/alpha/usage/summary", {
536
+ orgId,
537
+ since: subscription?.currentPeriodStart ?? void 0
538
+ })
539
+ );
540
+ if (isHttpError(summaryRaw) && isBlockingHttpError(summaryRaw)) {
541
+ return httpFailure(summaryRaw, "summary");
542
+ }
543
+ const summary = isHttpError(summaryRaw) || isQuotaError(summaryRaw) ? null : parseSummary(summaryRaw);
544
+ if (!summary) unavailable.push("usage");
545
+ if (!credits && !subscription && !summary) {
546
+ return {
547
+ ok: false,
548
+ error: {
549
+ kind: overallController.signal.aborted ? "timeout" : "http",
550
+ message: overallController.signal.aborted ? "Command Code quota request timed out" : "Command Code returned no recognized usage data for the account"
551
+ }
552
+ };
553
+ }
554
+ return {
555
+ ok: true,
556
+ quota: {
557
+ account,
558
+ credits,
559
+ subscription,
560
+ summary,
561
+ ...unavailable.length > 0 ? { unavailable } : {}
562
+ }
563
+ };
564
+ } catch (error) {
565
+ if (error instanceof QuotaTimeoutError || overallController.signal.aborted) {
566
+ return {
567
+ ok: false,
568
+ error: { message: "Command Code quota request timed out", kind: "timeout" }
569
+ };
570
+ }
571
+ return {
572
+ ok: false,
573
+ error: {
574
+ message: redactCommandCodeErrorText(
575
+ `Failed to fetch Command Code quota: ${errorMessage2(error)}`
576
+ ),
577
+ kind: "network"
578
+ }
579
+ };
580
+ } finally {
581
+ clearTimeout(overallTimer);
582
+ }
583
+ }
584
+
585
+ // src/quota-format.ts
586
+ function formatWindowLimits(limits, now = Date.now) {
587
+ const labels = {
588
+ fiveHour: "5-hour",
589
+ weekly: "Weekly"
590
+ };
591
+ return limits.map((limit) => {
592
+ const used = limit.used.toFixed(2);
593
+ const cap = limit.cap.toFixed(2);
594
+ const percent = limit.cap > 0 ? Math.round(limit.used / limit.cap * 100) : 0;
595
+ const reset = limit.resetAt === null ? "" : ` (resets ${formatResetClock(limit.resetAt, now)})`;
596
+ return `${labels[limit.window]}: ${used} / ${cap} credits (${percent}% used)${reset}`;
597
+ });
598
+ }
599
+ function formatResetClock(resetAtSeconds, now) {
600
+ const date = new Date(resetAtSeconds * 1e3);
601
+ if (Number.isNaN(date.getTime())) return "unknown";
602
+ const diffMs = date.getTime() - now();
603
+ if (diffMs <= 0) return "soon";
604
+ const minutes = Math.ceil(diffMs / 6e4);
605
+ if (minutes < 60) return `in ${minutes}m`;
606
+ const hours = Math.floor(minutes / 60);
607
+ const remainingMinutes = minutes % 60;
608
+ if (hours < 24) {
609
+ return remainingMinutes > 0 ? `in ${hours}h ${remainingMinutes}m` : `in ${hours}h`;
610
+ }
611
+ const days = Math.floor(hours / 24);
612
+ return days === 1 ? "in 1 day" : `in ${days} days`;
613
+ }
614
+ function creditsDetail(credits) {
615
+ if (!credits) return void 0;
616
+ const parts = [
617
+ `monthly $${credits.monthlyCredits.toFixed(2)}`,
618
+ `purchased $${credits.purchasedCredits.toFixed(2)}`
619
+ ];
620
+ if (credits.freeCredits > 0) parts.push(`free $${credits.freeCredits.toFixed(2)}`);
621
+ return `Sources: ${parts.join(" / ")}`;
622
+ }
623
+ function subscriptionLine(subscription) {
624
+ const plan = (subscription.planId ?? "Unknown").replace(/[_-]+/g, " ").trim();
625
+ const status = subscription.status ? ` (${subscription.status})` : "";
626
+ return `Plan: ${plan}${status}`;
627
+ }
628
+ function formatTokens(tokens) {
629
+ if (tokens >= 1e9) return `${(tokens / 1e9).toFixed(1)}B`;
630
+ if (tokens >= 1e6) return `${(tokens / 1e6).toFixed(1)}M`;
631
+ if (tokens >= 1e3) return `${(tokens / 1e3).toFixed(1)}k`;
632
+ return String(tokens);
633
+ }
634
+ function formatQuota(quota, now = Date.now) {
635
+ const lines = [];
636
+ const remaining = quota.credits?.remainingCredits ?? 0;
637
+ const spent = quota.summary?.totalCost ?? 0;
638
+ const pool = remaining + spent;
639
+ if (quota.credits || quota.summary) {
640
+ lines.push("Credits");
641
+ lines.push(` Remaining: $${remaining.toFixed(2)} of $${pool.toFixed(2)}`);
642
+ lines.push(` Used: $${spent.toFixed(2)}`);
643
+ lines.push(` ${pool > 0 ? Math.round(spent / pool * 100) : 0}% used`);
644
+ }
645
+ const detail = creditsDetail(quota.credits);
646
+ if (detail) lines.push(detail);
647
+ if (quota.subscription) lines.push(subscriptionLine(quota.subscription));
648
+ if (quota.summary) {
649
+ lines.push("");
650
+ lines.push(quota.subscription?.currentPeriodStart ? "Usage (billing period)" : "Usage");
651
+ lines.push(` Cost: $${quota.summary.totalCost.toFixed(2)}`);
652
+ lines.push(` Requests: ${quota.summary.totalCount.toLocaleString("en-US")}`);
653
+ if (quota.summary.totalTokens !== void 0) {
654
+ lines.push(` Tokens: ${formatTokens(quota.summary.totalTokens)}`);
655
+ }
656
+ }
657
+ lines.push("");
658
+ lines.push("Account");
659
+ lines.push(` ${quota.account.keyName ?? quota.account.login}`);
660
+ const limits = quota.credits?.windowLimits ?? [];
661
+ if (limits.length > 0) {
662
+ lines.push("");
663
+ lines.push("Usage windows:");
664
+ lines.push(...formatWindowLimits(limits, now).map((line) => ` ${line}`));
665
+ }
666
+ if ((quota.unavailable?.length ?? 0) > 0) {
667
+ lines.push("");
668
+ lines.push(`Unavailable: ${quota.unavailable?.join(", ")}`);
669
+ }
670
+ lines.push("");
671
+ lines.push("Full detail: https://commandcode.ai/usage");
672
+ return lines.join("\n");
673
+ }
674
+
675
+ // src/status.ts
676
+ function formatStatus(input) {
677
+ const lines = [
678
+ "Command Code plugin status",
679
+ ` Source: ${input.source}`,
680
+ ` Models: ${input.modelCount}`,
681
+ ` Fetched: ${input.fetchedAt === void 0 ? "n/a" : new Date(input.fetchedAt).toISOString()}`,
682
+ ` Endpoint: ${input.apiBase}`,
683
+ ` Zero-data-retention: ${input.zdr ? "on" : "off"}`,
684
+ ` Version: ${input.version}`
685
+ ];
686
+ return lines.join("\n");
687
+ }
688
+
689
+ // src/cli.ts
690
+ function zdrEnabled() {
691
+ return process.env.CMD_ZDR === "1" || process.env.COMMANDCODE_ZDR === "1";
692
+ }
693
+ function modelsCachePath() {
694
+ const configured = process.env.COMMANDCODE_MODELS_CACHE;
695
+ if (configured && configured.length > 0) return configured;
696
+ return join(homedir(), ".cache", "opencode", "commandcode-models.json");
697
+ }
698
+ function pluginVersion() {
699
+ try {
700
+ const here = fileURLToPath(import.meta.url);
701
+ const pkg = JSON.parse(readFileSync2(join(dirname2(here), "..", "package.json"), "utf-8"));
702
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
703
+ } catch {
704
+ return "unknown";
705
+ }
706
+ }
707
+ function modelsTimeoutMs() {
708
+ const raw = process.env.COMMANDCODE_MODELS_TIMEOUT_MS;
709
+ if (!raw) return DEFAULT_MODELS_TIMEOUT_MS;
710
+ const parsed = Number(raw);
711
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_MODELS_TIMEOUT_MS;
712
+ }
713
+ async function runQuota() {
714
+ const apiKey = readCommandCodeKey();
715
+ if (!apiKey) {
716
+ process.stdout.write(
717
+ "No Command Code API key found. Run /connect and select Command Code.\n"
718
+ );
719
+ return;
720
+ }
721
+ const baseUrl = process.env.COMMANDCODE_API_BASE?.replace(/\/provider\/v1\/?$/, "") ?? "https://api.commandcode.ai";
722
+ const extraHeaders = zdrEnabled() ? { "x-cmd-zdr": "1" } : void 0;
723
+ const result = await fetchCommandCodeQuota({ apiKey, baseUrl, extraHeaders });
724
+ if (!result.ok) {
725
+ process.stdout.write(`${result.error.message}
726
+ `);
727
+ return;
728
+ }
729
+ process.stdout.write(`${formatQuota(result.quota)}
730
+ `);
731
+ }
732
+ async function runStatus() {
733
+ const apiBase = process.env.COMMANDCODE_API_BASE ?? DEFAULT_PROVIDER_API_BASE;
734
+ const modelsUrl = process.env.COMMANDCODE_MODELS_URL ?? `${apiBase}/models`;
735
+ const cachePath = modelsCachePath();
736
+ const result = await loadCommandCodeModels({
737
+ url: modelsUrl,
738
+ cachePath,
739
+ fallbackModels: FALLBACK_MODELS,
740
+ timeoutMs: modelsTimeoutMs()
741
+ });
742
+ const status = {
743
+ source: result.source,
744
+ modelCount: result.models.length,
745
+ fetchedAt: result.fetchedAt,
746
+ apiBase,
747
+ zdr: zdrEnabled(),
748
+ version: pluginVersion()
749
+ };
750
+ process.stdout.write(`${formatStatus(status)}
751
+ `);
752
+ }
753
+ async function main() {
754
+ const cmd = process.argv[2];
755
+ if (cmd === "quota") return runQuota();
756
+ if (cmd === "status") return runStatus();
757
+ process.stdout.write("Usage: opencode-commandcode <quota|status>\n");
758
+ }
759
+ main().catch((error) => {
760
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}
761
+ `);
762
+ process.exitCode = 1;
763
+ });
package/dist/index.js CHANGED
@@ -237,30 +237,105 @@ function createAuthHook(options = {}) {
237
237
  };
238
238
  }
239
239
 
240
+ // src/catalog.ts
241
+ var FALLBACK_MODELS = [
242
+ { id: "claude-sonnet-5", name: "Claude Sonnet 5", contextLength: 1e6 },
243
+ { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", contextLength: 1e6 },
244
+ { id: "claude-fable-5", name: "Claude Fable 5", contextLength: 1e6 },
245
+ { id: "claude-opus-5", name: "Claude Opus 5", contextLength: 1e6 },
246
+ { id: "claude-opus-4-8", name: "Claude Opus 4.8", contextLength: 1e6 },
247
+ { id: "claude-opus-4-7", name: "Claude Opus 4.7", contextLength: 1e6 },
248
+ { id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5", contextLength: 2e5 },
249
+ { id: "gpt-5.6-sol", name: "GPT-5.6 Sol", contextLength: 105e4 },
250
+ { id: "gpt-5.6-terra", name: "GPT-5.6 Terra", contextLength: 105e4 },
251
+ { id: "gpt-5.6-luna", name: "GPT-5.6 Luna", contextLength: 105e4 },
252
+ { id: "gpt-5.5", name: "GPT-5.5", contextLength: 4e5 },
253
+ { id: "gpt-5.4", name: "GPT-5.4", contextLength: 4e5 },
254
+ { id: "gpt-5.3-codex", name: "GPT-5.3 Codex", contextLength: 4e5 },
255
+ { id: "gpt-5.4-mini", name: "GPT-5.4 Mini", contextLength: 4e5 },
256
+ { id: "deepseek/deepseek-v4-pro", name: "DeepSeek V4 Pro (latest)", contextLength: 1e6 },
257
+ { id: "deepseek/deepseek-v4-flash", name: "DeepSeek V4 Flash (latest)", contextLength: 1e6 },
258
+ { id: "deepseek/deepseek-v4-flash-vision-exp", name: "DeepSeek V4 Flash Vision (exp)", contextLength: 1e6 },
259
+ { id: "moonshotai/Kimi-K3", name: "Kimi K3", contextLength: 1e6 },
260
+ { id: "moonshotai/Kimi-K2.7-Code", name: "Kimi K2.7 Code", contextLength: 256e3 },
261
+ { id: "moonshotai/Kimi-K2.7-Code-Highspeed", name: "Kimi K2.7 Code HighSpeed", contextLength: 262e3 },
262
+ { id: "moonshotai/Kimi-K2.6", name: "Kimi K2.6", contextLength: 256e3 },
263
+ { id: "moonshotai/Kimi-K2.5", name: "Kimi K2.5", contextLength: 256e3 },
264
+ { id: "z-ai/glm-5.3-flash", name: "GLM-5.3 Flash", contextLength: 1048576 },
265
+ { id: "zai-org/GLM-5.3", name: "GLM-5.3", contextLength: 1e6 },
266
+ { id: "zai-org/GLM-5.2", name: "GLM-5.2", contextLength: 1e6 },
267
+ { id: "zai-org/GLM-5.2-Fast", name: "GLM-5.2 Fast", contextLength: 1e6 },
268
+ { id: "zai-org/GLM-5.1", name: "GLM-5.1", contextLength: 2e5 },
269
+ { id: "zai-org/GLM-5", name: "GLM-5", contextLength: 2e5 },
270
+ { id: "MiniMaxAI/MiniMax-M3", name: "MiniMax M3", contextLength: 1e6 },
271
+ { id: "MiniMaxAI/MiniMax-M2.7", name: "MiniMax M2.7", contextLength: 2e5 },
272
+ { id: "minimax/minimax-m3-free", name: "MiniMax M3", contextLength: 1e6 },
273
+ { id: "minimax/minimax-m2.7-free", name: "MiniMax M2.7", contextLength: 197e3 },
274
+ { id: "MiniMaxAI/MiniMax-M2.5", name: "MiniMax M2.5", contextLength: 2e5 },
275
+ { id: "xiaomi/mimo-v2.5-pro", name: "MiMo V2.5 Pro", contextLength: 1e6 },
276
+ { id: "xiaomi/mimo-v2.5", name: "MiMo V2.5", contextLength: 1e6 },
277
+ { id: "Qwen/Qwen3.8-Max", name: "Qwen 3.8 Max", contextLength: 1e6 },
278
+ { id: "Qwen/Qwen3.8-27B", name: "Qwen 3.8 27B", contextLength: 262144 },
279
+ { id: "Qwen/Qwen3.8-Flash", name: "Qwen 3.8 Flash", contextLength: 1e6 },
280
+ { id: "Qwen/Qwen3.7-Max", name: "Qwen 3.7 Max", contextLength: 1e6 },
281
+ { id: "Qwen/Qwen3.7-Plus", name: "Qwen 3.7 Plus", contextLength: 1e6 },
282
+ { id: "Qwen/Qwen3.7-Flash", name: "Qwen 3.7 Flash", contextLength: 1e6 },
283
+ { id: "Qwen/Qwen3.6-Max-Preview", name: "Qwen 3.6 Max Preview", contextLength: 2e5 },
284
+ { id: "Qwen/Qwen3.6-Plus", name: "Qwen 3.6 Plus", contextLength: 2e5 },
285
+ { id: "stepfun/Step-3.7-Flash", name: "Step 3.7 Flash", contextLength: 256e3 },
286
+ { id: "stepfun/Step-3.5-Flash", name: "Step 3.5 Flash", contextLength: 1e6 },
287
+ { id: "tencent/hy3-paid", name: "Tencent Hy3", contextLength: 262144 },
288
+ { id: "tencent/hy4-preview", name: "Tencent Hy4 Preview", contextLength: 1048576 },
289
+ { id: "google/gemini-3.7-flash", name: "Gemini 3.7 Flash", contextLength: 1048576 },
290
+ { id: "google/gemini-3.6-flash", name: "Gemini 3.6 Flash", contextLength: 1e6 },
291
+ { id: "google/gemini-3.5-flash", name: "Gemini 3.5 Flash", contextLength: 1e6 },
292
+ { id: "google/gemini-3.5-flash-lite", name: "Gemini 3.5 Flash Lite", contextLength: 1e6 },
293
+ { id: "google/gemini-3.1-flash-lite", name: "Gemini 3.1 Flash Lite", contextLength: 1e6 },
294
+ { id: "sakana/fugu-ultra", name: "Fugu Ultra", contextLength: 1e6 },
295
+ { id: "nvidia/nemotron-3-ultra-550b-a55b", name: "Nemotron 3 Ultra", contextLength: 1e6 },
296
+ { id: "thinkingmachines/inkling-small", name: "Inkling Small", contextLength: 1e6 },
297
+ { id: "poolside/laguna-s-2.1-free", name: "Laguna S 2.1", contextLength: 256e3 },
298
+ { id: "meta/muse-spark-1.1", name: "Muse Spark 1.1", contextLength: 1048576 },
299
+ { id: "meta/muse-spark-1.2", name: "Muse Spark 1.2", contextLength: 1048576 },
300
+ { id: "meta/muse-spark-1.2-contributor", name: "Muse Spark 1.2 Contributor", contextLength: 1048576 },
301
+ { id: "xai/grok-4.5", name: "Grok 4.5", contextLength: 5e5 },
302
+ { id: "xai/grok-4.6", name: "Grok 4.6", contextLength: 5e5 }
303
+ ];
304
+
305
+ // src/cli-path.ts
306
+ import { fileURLToPath } from "url";
307
+ function cliPath() {
308
+ return fileURLToPath(new URL("./cli.js", import.meta.url));
309
+ }
310
+
240
311
  // src/commands.ts
241
312
  import { mkdir, readFile, writeFile } from "fs/promises";
242
313
  import { join } from "path";
243
- var QUOTA_TEMPLATE = `---
314
+ function quotaTemplate(cliPath2) {
315
+ return `---
244
316
  description: Show Command Code account usage and quota (credits, plan, usage)
245
317
  ---
246
318
 
247
- Use the **commandcode_quota** tool to fetch and report the user's Command Code account usage and quota. Summarize the credits remaining and used, the plan, the billing-period usage (cost, requests, tokens), and the 5-hour and weekly usage windows. If no API key is configured, tell the user to run \`/connect\` and select Command Code.
319
+ ! \`node "${cliPath2}" quota\`
248
320
  `;
249
- var STATUS_TEMPLATE = `---
321
+ }
322
+ function statusTemplate(cliPath2) {
323
+ return `---
250
324
  description: Show Command Code plugin diagnostics (catalog source, model count, endpoint)
251
325
  ---
252
326
 
253
- Use the **commandcode_status** tool to fetch and report the Command Code plugin diagnostics: the model catalog source (live/cache/fallback), model count, when the catalog was last fetched, the API endpoint, the zero-data-retention setting, and the plugin version.
327
+ ! \`node "${cliPath2}" status\`
254
328
  `;
255
- var COMMANDS = [
256
- { name: "commandcode-quota.md", content: QUOTA_TEMPLATE },
257
- { name: "commandcode-status.md", content: STATUS_TEMPLATE }
258
- ];
259
- async function installCommandFiles(directory) {
329
+ }
330
+ async function installCommandFiles(directory, cliPath2) {
260
331
  if (!directory) return;
261
332
  const commandDir = join(directory, ".opencode", "command");
262
333
  await mkdir(commandDir, { recursive: true });
263
- for (const command of COMMANDS) {
334
+ const commands = [
335
+ { name: "commandcode-quota.md", content: quotaTemplate(cliPath2) },
336
+ { name: "commandcode-status.md", content: statusTemplate(cliPath2) }
337
+ ];
338
+ for (const command of commands) {
264
339
  const target = join(commandDir, command.name);
265
340
  try {
266
341
  const existing = await readFile(target, "utf-8");
@@ -1079,71 +1154,7 @@ async function cachedTimestamp(cachePath) {
1079
1154
 
1080
1155
  // src/index.ts
1081
1156
  var PROVIDER_ID2 = "commandcode";
1082
- var PLUGIN_VERSION = "0.2.0";
1083
- var FALLBACK_MODELS = [
1084
- { id: "claude-sonnet-5", name: "Claude Sonnet 5", contextLength: 1e6 },
1085
- { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", contextLength: 1e6 },
1086
- { id: "claude-fable-5", name: "Claude Fable 5", contextLength: 1e6 },
1087
- { id: "claude-opus-5", name: "Claude Opus 5", contextLength: 1e6 },
1088
- { id: "claude-opus-4-8", name: "Claude Opus 4.8", contextLength: 1e6 },
1089
- { id: "claude-opus-4-7", name: "Claude Opus 4.7", contextLength: 1e6 },
1090
- { id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5", contextLength: 2e5 },
1091
- { id: "gpt-5.6-sol", name: "GPT-5.6 Sol", contextLength: 105e4 },
1092
- { id: "gpt-5.6-terra", name: "GPT-5.6 Terra", contextLength: 105e4 },
1093
- { id: "gpt-5.6-luna", name: "GPT-5.6 Luna", contextLength: 105e4 },
1094
- { id: "gpt-5.5", name: "GPT-5.5", contextLength: 4e5 },
1095
- { id: "gpt-5.4", name: "GPT-5.4", contextLength: 4e5 },
1096
- { id: "gpt-5.3-codex", name: "GPT-5.3 Codex", contextLength: 4e5 },
1097
- { id: "gpt-5.4-mini", name: "GPT-5.4 Mini", contextLength: 4e5 },
1098
- { id: "deepseek/deepseek-v4-pro", name: "DeepSeek V4 Pro (latest)", contextLength: 1e6 },
1099
- { id: "deepseek/deepseek-v4-flash", name: "DeepSeek V4 Flash (latest)", contextLength: 1e6 },
1100
- { id: "deepseek/deepseek-v4-flash-vision-exp", name: "DeepSeek V4 Flash Vision (exp)", contextLength: 1e6 },
1101
- { id: "moonshotai/Kimi-K3", name: "Kimi K3", contextLength: 1e6 },
1102
- { id: "moonshotai/Kimi-K2.7-Code", name: "Kimi K2.7 Code", contextLength: 256e3 },
1103
- { id: "moonshotai/Kimi-K2.7-Code-Highspeed", name: "Kimi K2.7 Code HighSpeed", contextLength: 262e3 },
1104
- { id: "moonshotai/Kimi-K2.6", name: "Kimi K2.6", contextLength: 256e3 },
1105
- { id: "moonshotai/Kimi-K2.5", name: "Kimi K2.5", contextLength: 256e3 },
1106
- { id: "z-ai/glm-5.3-flash", name: "GLM-5.3 Flash", contextLength: 1048576 },
1107
- { id: "zai-org/GLM-5.3", name: "GLM-5.3", contextLength: 1e6 },
1108
- { id: "zai-org/GLM-5.2", name: "GLM-5.2", contextLength: 1e6 },
1109
- { id: "zai-org/GLM-5.2-Fast", name: "GLM-5.2 Fast", contextLength: 1e6 },
1110
- { id: "zai-org/GLM-5.1", name: "GLM-5.1", contextLength: 2e5 },
1111
- { id: "zai-org/GLM-5", name: "GLM-5", contextLength: 2e5 },
1112
- { id: "MiniMaxAI/MiniMax-M3", name: "MiniMax M3", contextLength: 1e6 },
1113
- { id: "MiniMaxAI/MiniMax-M2.7", name: "MiniMax M2.7", contextLength: 2e5 },
1114
- { id: "minimax/minimax-m3-free", name: "MiniMax M3", contextLength: 1e6 },
1115
- { id: "minimax/minimax-m2.7-free", name: "MiniMax M2.7", contextLength: 197e3 },
1116
- { id: "MiniMaxAI/MiniMax-M2.5", name: "MiniMax M2.5", contextLength: 2e5 },
1117
- { id: "xiaomi/mimo-v2.5-pro", name: "MiMo V2.5 Pro", contextLength: 1e6 },
1118
- { id: "xiaomi/mimo-v2.5", name: "MiMo V2.5", contextLength: 1e6 },
1119
- { id: "Qwen/Qwen3.8-Max", name: "Qwen 3.8 Max", contextLength: 1e6 },
1120
- { id: "Qwen/Qwen3.8-27B", name: "Qwen 3.8 27B", contextLength: 262144 },
1121
- { id: "Qwen/Qwen3.8-Flash", name: "Qwen 3.8 Flash", contextLength: 1e6 },
1122
- { id: "Qwen/Qwen3.7-Max", name: "Qwen 3.7 Max", contextLength: 1e6 },
1123
- { id: "Qwen/Qwen3.7-Plus", name: "Qwen 3.7 Plus", contextLength: 1e6 },
1124
- { id: "Qwen/Qwen3.7-Flash", name: "Qwen 3.7 Flash", contextLength: 1e6 },
1125
- { id: "Qwen/Qwen3.6-Max-Preview", name: "Qwen 3.6 Max Preview", contextLength: 2e5 },
1126
- { id: "Qwen/Qwen3.6-Plus", name: "Qwen 3.6 Plus", contextLength: 2e5 },
1127
- { id: "stepfun/Step-3.7-Flash", name: "Step 3.7 Flash", contextLength: 256e3 },
1128
- { id: "stepfun/Step-3.5-Flash", name: "Step 3.5 Flash", contextLength: 1e6 },
1129
- { id: "tencent/hy3-paid", name: "Tencent Hy3", contextLength: 262144 },
1130
- { id: "tencent/hy4-preview", name: "Tencent Hy4 Preview", contextLength: 1048576 },
1131
- { id: "google/gemini-3.7-flash", name: "Gemini 3.7 Flash", contextLength: 1048576 },
1132
- { id: "google/gemini-3.6-flash", name: "Gemini 3.6 Flash", contextLength: 1e6 },
1133
- { id: "google/gemini-3.5-flash", name: "Gemini 3.5 Flash", contextLength: 1e6 },
1134
- { id: "google/gemini-3.5-flash-lite", name: "Gemini 3.5 Flash Lite", contextLength: 1e6 },
1135
- { id: "google/gemini-3.1-flash-lite", name: "Gemini 3.1 Flash Lite", contextLength: 1e6 },
1136
- { id: "sakana/fugu-ultra", name: "Fugu Ultra", contextLength: 1e6 },
1137
- { id: "nvidia/nemotron-3-ultra-550b-a55b", name: "Nemotron 3 Ultra", contextLength: 1e6 },
1138
- { id: "thinkingmachines/inkling", name: "Inkling", contextLength: 256e3 },
1139
- { id: "thinkingmachines/inkling-small", name: "Inkling Small", contextLength: 1e6 },
1140
- { id: "poolside/laguna-s-2.1-free", name: "Laguna S 2.1", contextLength: 256e3 },
1141
- { id: "meta/muse-spark-1.1", name: "Muse Spark 1.1", contextLength: 1048576 },
1142
- { id: "meta/muse-spark-1.2", name: "Muse Spark 1.2", contextLength: 1048576 },
1143
- { id: "meta/muse-spark-1.2-contributor", name: "Muse Spark 1.2 Contributor", contextLength: 1048576 },
1144
- { id: "xai/grok-4.5", name: "Grok 4.5", contextLength: 5e5 },
1145
- { id: "xai/grok-4.6", name: "Grok 4.6", contextLength: 5e5 }
1146
- ];
1157
+ var PLUGIN_VERSION = "0.3.0";
1147
1158
  function zeroDataRetentionHeaders() {
1148
1159
  if (process.env.CMD_ZDR === "1" || process.env.COMMANDCODE_ZDR === "1") {
1149
1160
  return { "x-cmd-zdr": "1" };
@@ -1188,7 +1199,7 @@ var CommandCodePlugin = async ({ client, directory }) => {
1188
1199
  const apiBase = providerBaseUrl();
1189
1200
  const modelsUrl = process.env.COMMANDCODE_MODELS_URL ?? `${apiBase}/models`;
1190
1201
  const cachePath = modelsCachePath();
1191
- installCommandFiles(directory).catch(() => {
1202
+ installCommandFiles(directory, cliPath()).catch(() => {
1192
1203
  });
1193
1204
  const { modelMap: models, modelsInfo } = await loadProviderModels(
1194
1205
  modelsUrl,
package/package.json CHANGED
@@ -1,24 +1,29 @@
1
1
  {
2
2
  "name": "@gururea/opencode-commandcode-provider",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "OpenCode plugin that adds the Command Code (commandcode.ai) provider and its authentication to the interface.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
8
+ "bin": {
9
+ "opencode-commandcode": "./dist/cli.js"
10
+ },
8
11
  "exports": {
9
12
  ".": {
10
13
  "types": "./dist/index.d.ts",
11
14
  "import": "./dist/index.js"
15
+ },
16
+ "./cli": {
17
+ "import": "./dist/cli.js"
12
18
  }
13
19
  },
14
20
  "files": [
15
21
  "dist",
16
- "command",
17
22
  "README.md",
18
23
  "LICENSE"
19
24
  ],
20
25
  "scripts": {
21
- "build": "tsup src/index.ts --format esm --dts --clean --out-dir dist --external @opencode-ai/plugin --external zod",
26
+ "build": "tsup src/index.ts src/cli.ts --format esm --dts --clean --no-splitting --out-dir dist --external @opencode-ai/plugin --external zod",
22
27
  "test": "node --import tsx --test tests/*.test.ts",
23
28
  "typecheck": "tsc --noEmit",
24
29
  "prepare": "npm run build"
@@ -1,5 +0,0 @@
1
- ---
2
- description: Show Command Code account usage and quota (credits, plan, usage)
3
- ---
4
-
5
- Use the **commandcode_quota** tool to fetch and report the user's Command Code account usage and quota. Summarize the credits remaining and used, the plan, the billing-period usage (cost, requests, tokens), and the 5-hour and weekly usage windows. If no API key is configured, tell the user to run `/connect` and select Command Code.
@@ -1,5 +0,0 @@
1
- ---
2
- description: Show Command Code plugin diagnostics (catalog source, model count, endpoint)
3
- ---
4
-
5
- Use the **commandcode_status** tool to fetch and report the Command Code plugin diagnostics: the model catalog source (live/cache/fallback), model count, when the catalog was last fetched, the API endpoint, the zero-data-retention setting, and the plugin version.