@gururea/opencode-commandcode-provider 0.2.0 → 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 `/commandcode-quota` and `/commandcode-status` **tools** are registered by the plugin. The corresponding **slash commands** are provided as `.opencode/command/*.md` files in the package, which you can copy into your project's `.opencode/command/` directory if your opencode setup does not auto-discover package commands.
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
@@ -1,6 +1,6 @@
1
1
  // src/index.ts
2
2
  import { homedir } from "os";
3
- import { join } from "path";
3
+ import { join as join2 } from "path";
4
4
 
5
5
  // src/auth.ts
6
6
  import { randomBytes } from "crypto";
@@ -237,6 +237,116 @@ 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
+
311
+ // src/commands.ts
312
+ import { mkdir, readFile, writeFile } from "fs/promises";
313
+ import { join } from "path";
314
+ function quotaTemplate(cliPath2) {
315
+ return `---
316
+ description: Show Command Code account usage and quota (credits, plan, usage)
317
+ ---
318
+
319
+ ! \`node "${cliPath2}" quota\`
320
+ `;
321
+ }
322
+ function statusTemplate(cliPath2) {
323
+ return `---
324
+ description: Show Command Code plugin diagnostics (catalog source, model count, endpoint)
325
+ ---
326
+
327
+ ! \`node "${cliPath2}" status\`
328
+ `;
329
+ }
330
+ async function installCommandFiles(directory, cliPath2) {
331
+ if (!directory) return;
332
+ const commandDir = join(directory, ".opencode", "command");
333
+ await mkdir(commandDir, { recursive: true });
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) {
339
+ const target = join(commandDir, command.name);
340
+ try {
341
+ const existing = await readFile(target, "utf-8");
342
+ if (existing === command.content) continue;
343
+ return;
344
+ } catch {
345
+ }
346
+ await writeFile(target, command.content, "utf-8");
347
+ }
348
+ }
349
+
240
350
  // src/quota-tool.ts
241
351
  import { tool } from "@opencode-ai/plugin";
242
352
 
@@ -743,7 +853,7 @@ function createStatusTool({
743
853
  }
744
854
 
745
855
  // src/models.ts
746
- import { mkdir, readFile, rename, rm, writeFile } from "fs/promises";
856
+ import { mkdir as mkdir2, readFile as readFile2, rename, rm, writeFile as writeFile2 } from "fs/promises";
747
857
  import { dirname } from "path";
748
858
  var DEFAULT_PROVIDER_API_BASE = "https://api.commandcode.ai/provider/v1";
749
859
  var DEFAULT_MODELS_URL = `${DEFAULT_PROVIDER_API_BASE}/models`;
@@ -970,14 +1080,14 @@ function commandCodeModelsFromCache(value) {
970
1080
  return models;
971
1081
  }
972
1082
  async function readCommandCodeModelsCache(cachePath) {
973
- const contents = await readFile(cachePath, "utf-8");
1083
+ const contents = await readFile2(cachePath, "utf-8");
974
1084
  return commandCodeModelsFromCache(JSON.parse(contents));
975
1085
  }
976
1086
  async function writeCommandCodeModelsCache(cachePath, models, fetchedAt = Date.now()) {
977
- await mkdir(dirname(cachePath), { recursive: true });
1087
+ await mkdir2(dirname(cachePath), { recursive: true });
978
1088
  const temporaryPath = `${cachePath}.${process.pid}.tmp`;
979
1089
  try {
980
- await writeFile(
1090
+ await writeFile2(
981
1091
  temporaryPath,
982
1092
  `${JSON.stringify({ version: MODEL_CACHE_VERSION, fetchedAt, models }, null, 2)}
983
1093
  `,
@@ -1033,7 +1143,7 @@ async function loadCommandCodeModels(options) {
1033
1143
  }
1034
1144
  async function cachedTimestamp(cachePath) {
1035
1145
  try {
1036
- const contents = await readFile(cachePath, "utf-8");
1146
+ const contents = await readFile2(cachePath, "utf-8");
1037
1147
  const parsed = JSON.parse(contents);
1038
1148
  if (!isRecord3(parsed)) return null;
1039
1149
  return typeof parsed.fetchedAt === "number" && Number.isFinite(parsed.fetchedAt) ? parsed.fetchedAt : null;
@@ -1044,71 +1154,7 @@ async function cachedTimestamp(cachePath) {
1044
1154
 
1045
1155
  // src/index.ts
1046
1156
  var PROVIDER_ID2 = "commandcode";
1047
- var PLUGIN_VERSION = "0.2.0";
1048
- var FALLBACK_MODELS = [
1049
- { id: "claude-sonnet-5", name: "Claude Sonnet 5", contextLength: 1e6 },
1050
- { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6", contextLength: 1e6 },
1051
- { id: "claude-fable-5", name: "Claude Fable 5", contextLength: 1e6 },
1052
- { id: "claude-opus-5", name: "Claude Opus 5", contextLength: 1e6 },
1053
- { id: "claude-opus-4-8", name: "Claude Opus 4.8", contextLength: 1e6 },
1054
- { id: "claude-opus-4-7", name: "Claude Opus 4.7", contextLength: 1e6 },
1055
- { id: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5", contextLength: 2e5 },
1056
- { id: "gpt-5.6-sol", name: "GPT-5.6 Sol", contextLength: 105e4 },
1057
- { id: "gpt-5.6-terra", name: "GPT-5.6 Terra", contextLength: 105e4 },
1058
- { id: "gpt-5.6-luna", name: "GPT-5.6 Luna", contextLength: 105e4 },
1059
- { id: "gpt-5.5", name: "GPT-5.5", contextLength: 4e5 },
1060
- { id: "gpt-5.4", name: "GPT-5.4", contextLength: 4e5 },
1061
- { id: "gpt-5.3-codex", name: "GPT-5.3 Codex", contextLength: 4e5 },
1062
- { id: "gpt-5.4-mini", name: "GPT-5.4 Mini", contextLength: 4e5 },
1063
- { id: "deepseek/deepseek-v4-pro", name: "DeepSeek V4 Pro (latest)", contextLength: 1e6 },
1064
- { id: "deepseek/deepseek-v4-flash", name: "DeepSeek V4 Flash (latest)", contextLength: 1e6 },
1065
- { id: "deepseek/deepseek-v4-flash-vision-exp", name: "DeepSeek V4 Flash Vision (exp)", contextLength: 1e6 },
1066
- { id: "moonshotai/Kimi-K3", name: "Kimi K3", contextLength: 1e6 },
1067
- { id: "moonshotai/Kimi-K2.7-Code", name: "Kimi K2.7 Code", contextLength: 256e3 },
1068
- { id: "moonshotai/Kimi-K2.7-Code-Highspeed", name: "Kimi K2.7 Code HighSpeed", contextLength: 262e3 },
1069
- { id: "moonshotai/Kimi-K2.6", name: "Kimi K2.6", contextLength: 256e3 },
1070
- { id: "moonshotai/Kimi-K2.5", name: "Kimi K2.5", contextLength: 256e3 },
1071
- { id: "z-ai/glm-5.3-flash", name: "GLM-5.3 Flash", contextLength: 1048576 },
1072
- { id: "zai-org/GLM-5.3", name: "GLM-5.3", contextLength: 1e6 },
1073
- { id: "zai-org/GLM-5.2", name: "GLM-5.2", contextLength: 1e6 },
1074
- { id: "zai-org/GLM-5.2-Fast", name: "GLM-5.2 Fast", contextLength: 1e6 },
1075
- { id: "zai-org/GLM-5.1", name: "GLM-5.1", contextLength: 2e5 },
1076
- { id: "zai-org/GLM-5", name: "GLM-5", contextLength: 2e5 },
1077
- { id: "MiniMaxAI/MiniMax-M3", name: "MiniMax M3", contextLength: 1e6 },
1078
- { id: "MiniMaxAI/MiniMax-M2.7", name: "MiniMax M2.7", contextLength: 2e5 },
1079
- { id: "minimax/minimax-m3-free", name: "MiniMax M3", contextLength: 1e6 },
1080
- { id: "minimax/minimax-m2.7-free", name: "MiniMax M2.7", contextLength: 197e3 },
1081
- { id: "MiniMaxAI/MiniMax-M2.5", name: "MiniMax M2.5", contextLength: 2e5 },
1082
- { id: "xiaomi/mimo-v2.5-pro", name: "MiMo V2.5 Pro", contextLength: 1e6 },
1083
- { id: "xiaomi/mimo-v2.5", name: "MiMo V2.5", contextLength: 1e6 },
1084
- { id: "Qwen/Qwen3.8-Max", name: "Qwen 3.8 Max", contextLength: 1e6 },
1085
- { id: "Qwen/Qwen3.8-27B", name: "Qwen 3.8 27B", contextLength: 262144 },
1086
- { id: "Qwen/Qwen3.8-Flash", name: "Qwen 3.8 Flash", contextLength: 1e6 },
1087
- { id: "Qwen/Qwen3.7-Max", name: "Qwen 3.7 Max", contextLength: 1e6 },
1088
- { id: "Qwen/Qwen3.7-Plus", name: "Qwen 3.7 Plus", contextLength: 1e6 },
1089
- { id: "Qwen/Qwen3.7-Flash", name: "Qwen 3.7 Flash", contextLength: 1e6 },
1090
- { id: "Qwen/Qwen3.6-Max-Preview", name: "Qwen 3.6 Max Preview", contextLength: 2e5 },
1091
- { id: "Qwen/Qwen3.6-Plus", name: "Qwen 3.6 Plus", contextLength: 2e5 },
1092
- { id: "stepfun/Step-3.7-Flash", name: "Step 3.7 Flash", contextLength: 256e3 },
1093
- { id: "stepfun/Step-3.5-Flash", name: "Step 3.5 Flash", contextLength: 1e6 },
1094
- { id: "tencent/hy3-paid", name: "Tencent Hy3", contextLength: 262144 },
1095
- { id: "tencent/hy4-preview", name: "Tencent Hy4 Preview", contextLength: 1048576 },
1096
- { id: "google/gemini-3.7-flash", name: "Gemini 3.7 Flash", contextLength: 1048576 },
1097
- { id: "google/gemini-3.6-flash", name: "Gemini 3.6 Flash", contextLength: 1e6 },
1098
- { id: "google/gemini-3.5-flash", name: "Gemini 3.5 Flash", contextLength: 1e6 },
1099
- { id: "google/gemini-3.5-flash-lite", name: "Gemini 3.5 Flash Lite", contextLength: 1e6 },
1100
- { id: "google/gemini-3.1-flash-lite", name: "Gemini 3.1 Flash Lite", contextLength: 1e6 },
1101
- { id: "sakana/fugu-ultra", name: "Fugu Ultra", contextLength: 1e6 },
1102
- { id: "nvidia/nemotron-3-ultra-550b-a55b", name: "Nemotron 3 Ultra", contextLength: 1e6 },
1103
- { id: "thinkingmachines/inkling", name: "Inkling", contextLength: 256e3 },
1104
- { id: "thinkingmachines/inkling-small", name: "Inkling Small", contextLength: 1e6 },
1105
- { id: "poolside/laguna-s-2.1-free", name: "Laguna S 2.1", contextLength: 256e3 },
1106
- { id: "meta/muse-spark-1.1", name: "Muse Spark 1.1", contextLength: 1048576 },
1107
- { id: "meta/muse-spark-1.2", name: "Muse Spark 1.2", contextLength: 1048576 },
1108
- { id: "meta/muse-spark-1.2-contributor", name: "Muse Spark 1.2 Contributor", contextLength: 1048576 },
1109
- { id: "xai/grok-4.5", name: "Grok 4.5", contextLength: 5e5 },
1110
- { id: "xai/grok-4.6", name: "Grok 4.6", contextLength: 5e5 }
1111
- ];
1157
+ var PLUGIN_VERSION = "0.3.0";
1112
1158
  function zeroDataRetentionHeaders() {
1113
1159
  if (process.env.CMD_ZDR === "1" || process.env.COMMANDCODE_ZDR === "1") {
1114
1160
  return { "x-cmd-zdr": "1" };
@@ -1123,7 +1169,7 @@ function providerBaseUrl() {
1123
1169
  function modelsCachePath() {
1124
1170
  const configured = process.env.COMMANDCODE_MODELS_CACHE;
1125
1171
  if (configured && configured.length > 0) return configured;
1126
- return join(homedir(), ".cache", "opencode", "commandcode-models.json");
1172
+ return join2(homedir(), ".cache", "opencode", "commandcode-models.json");
1127
1173
  }
1128
1174
  async function loadProviderModels(modelsUrl, cachePath, providerBaseUrl2, logWarn, logInfo) {
1129
1175
  const result = await loadCommandCodeModels({
@@ -1149,10 +1195,12 @@ function getModelsTimeoutMs() {
1149
1195
  const parsed = Number(raw);
1150
1196
  return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_MODELS_TIMEOUT_MS;
1151
1197
  }
1152
- var CommandCodePlugin = async ({ client }) => {
1198
+ var CommandCodePlugin = async ({ client, directory }) => {
1153
1199
  const apiBase = providerBaseUrl();
1154
1200
  const modelsUrl = process.env.COMMANDCODE_MODELS_URL ?? `${apiBase}/models`;
1155
1201
  const cachePath = modelsCachePath();
1202
+ installCommandFiles(directory, cliPath()).catch(() => {
1203
+ });
1156
1204
  const { modelMap: models, modelsInfo } = await loadProviderModels(
1157
1205
  modelsUrl,
1158
1206
  cachePath,
@@ -1179,6 +1227,15 @@ var CommandCodePlugin = async ({ client }) => {
1179
1227
  },
1180
1228
  models
1181
1229
  };
1230
+ cfg.command ??= {};
1231
+ cfg.command["commandcode-quota"] = {
1232
+ description: "Show Command Code account usage and quota (credits, plan, usage)",
1233
+ template: "Use the commandcode_quota tool to fetch and report the user's Command Code account usage and quota. Summarize credits remaining and used, the plan, billing-period usage (cost, requests, tokens), and the 5-hour and weekly usage windows."
1234
+ };
1235
+ cfg.command["commandcode-status"] = {
1236
+ description: "Show Command Code plugin diagnostics (catalog source, model count, endpoint)",
1237
+ template: "Use the commandcode_status tool to fetch and report the Command Code plugin diagnostics: catalog source (live/cache/fallback), model count, last fetch time, API endpoint, zero-data-retention setting, and plugin version."
1238
+ };
1182
1239
  client.app.log({
1183
1240
  body: {
1184
1241
  service: "opencode-commandcode",
package/package.json CHANGED
@@ -1,24 +1,29 @@
1
1
  {
2
2
  "name": "@gururea/opencode-commandcode-provider",
3
- "version": "0.2.0",
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.