@gururea/opencode-commandcode-provider 0.2.1 → 0.4.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 +1 -9
- package/dist/index.js +117 -681
- package/package.json +1 -2
- package/command/commandcode-quota.md +0 -5
- package/command/commandcode-status.md +0 -5
package/README.md
CHANGED
|
@@ -25,17 +25,13 @@ Restart opencode, then:
|
|
|
25
25
|
|
|
26
26
|
## What the plugin does
|
|
27
27
|
|
|
28
|
-
- **Registers the provider.** The plugin injects a `commandcode` custom provider into the opencode config (`@ai-sdk/openai-compatible`, base URL `https://api.commandcode.ai/provider/v1`).
|
|
28
|
+
- **Registers the provider.** The plugin injects a `commandcode` custom provider into the opencode config (`@ai-sdk/openai-compatible`, base URL `https://api.commandcode.ai/provider/v1`). Claude models are routed to Command Code's Anthropic-compatible endpoint via the native `@ai-sdk/anthropic` SDK.
|
|
29
29
|
- **Live model discovery with caching.** The catalog is fetched from the Provider API at every startup. A successful fetch is cached to disk (`~/.cache/opencode/commandcode-models.json`) so the next start is fast and works offline; if both the endpoint and the cache are unavailable, a built-in fallback catalog keeps the provider visible.
|
|
30
30
|
- **Adds authentication.** The `/connect` dialog gets two methods via the plugin `auth` hook:
|
|
31
31
|
- **Browser login:** opens `commandcode.ai/studio/auth/cli` and receives the API key through a local callback server (fall back to pasting the key if the transfer fails).
|
|
32
32
|
- **API key:** the standard key-paste dialog.
|
|
33
|
-
- **`commandcode_quota` tool / `/commandcode-quota`.** Reports your account usage and quota: remaining and used credits, plan, billing-period usage (cost, requests, tokens), and the 5-hour and weekly usage windows.
|
|
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
33
|
- Command Code API keys (`user_...`) do not expire, so they are stored once and reused.
|
|
36
34
|
|
|
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.
|
|
38
|
-
|
|
39
35
|
## Environment variables
|
|
40
36
|
|
|
41
37
|
| Variable | Purpose |
|
|
@@ -47,10 +43,6 @@ Restart opencode, then:
|
|
|
47
43
|
| `COMMANDCODE_MODELS_TIMEOUT_MS` | Model catalog fetch timeout (default `10000` ms) |
|
|
48
44
|
| `CMD_ZDR=1` / `COMMANDCODE_ZDR=1` | Send the `x-cmd-zdr: 1` zero-data-retention header |
|
|
49
45
|
|
|
50
|
-
## Scope
|
|
51
|
-
|
|
52
|
-
Claude models (`claude-*`) are excluded: they are served through Command Code's Anthropic-compatible endpoint, which is outside the OpenAI-compatible scope of this MVP.
|
|
53
|
-
|
|
54
46
|
## Development
|
|
55
47
|
|
|
56
48
|
```sh
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { homedir } from "os";
|
|
3
|
-
import { join
|
|
3
|
+
import { join } from "path";
|
|
4
4
|
|
|
5
5
|
// src/auth.ts
|
|
6
6
|
import { randomBytes } from "crypto";
|
|
@@ -237,548 +237,73 @@ function createAuthHook(options = {}) {
|
|
|
237
237
|
};
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
// src/
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
{
|
|
257
|
-
{
|
|
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 }
|
|
258
303
|
];
|
|
259
|
-
async function installCommandFiles(directory) {
|
|
260
|
-
if (!directory) return;
|
|
261
|
-
const commandDir = join(directory, ".opencode", "command");
|
|
262
|
-
await mkdir(commandDir, { recursive: true });
|
|
263
|
-
for (const command of COMMANDS) {
|
|
264
|
-
const target = join(commandDir, command.name);
|
|
265
|
-
try {
|
|
266
|
-
const existing = await readFile(target, "utf-8");
|
|
267
|
-
if (existing === command.content) continue;
|
|
268
|
-
return;
|
|
269
|
-
} catch {
|
|
270
|
-
}
|
|
271
|
-
await writeFile(target, command.content, "utf-8");
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// src/quota-tool.ts
|
|
276
|
-
import { tool } from "@opencode-ai/plugin";
|
|
277
|
-
|
|
278
|
-
// src/api-key.ts
|
|
279
|
-
import { existsSync, readFileSync } from "fs";
|
|
280
|
-
var PROVIDER_ID = "commandcode";
|
|
281
|
-
function isRecord(value) {
|
|
282
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
283
|
-
}
|
|
284
|
-
function authKey(record) {
|
|
285
|
-
const type = record.type;
|
|
286
|
-
if (type === "api") return typeof record.key === "string" ? record.key : void 0;
|
|
287
|
-
if (type === "oauth") return typeof record.access === "string" ? record.access : void 0;
|
|
288
|
-
if (type === "wellknown") {
|
|
289
|
-
return typeof record.token === "string" ? record.token : typeof record.key === "string" ? record.key : void 0;
|
|
290
|
-
}
|
|
291
|
-
return void 0;
|
|
292
|
-
}
|
|
293
|
-
function credentialFromAuthJson(raw) {
|
|
294
|
-
try {
|
|
295
|
-
const parsed = JSON.parse(raw);
|
|
296
|
-
if (!isRecord(parsed)) return void 0;
|
|
297
|
-
const entry = parsed[PROVIDER_ID];
|
|
298
|
-
if (!isRecord(entry)) return void 0;
|
|
299
|
-
return authKey(entry);
|
|
300
|
-
} catch {
|
|
301
|
-
return void 0;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
function defaultAuthPath() {
|
|
305
|
-
return `${process.env.HOME ?? ""}/.local/share/opencode/auth.json`;
|
|
306
|
-
}
|
|
307
|
-
function readCommandCodeKey(options = {}) {
|
|
308
|
-
const env = options.env ?? process.env;
|
|
309
|
-
if (env.COMMAND_CODE_API_KEY) return env.COMMAND_CODE_API_KEY;
|
|
310
|
-
if (env.COMMANDCODE_API_KEY) return env.COMMANDCODE_API_KEY;
|
|
311
|
-
const authContent = options.authContent ?? env.OPENCODE_AUTH_CONTENT;
|
|
312
|
-
if (authContent) {
|
|
313
|
-
const fromContent = credentialFromAuthJson(authContent);
|
|
314
|
-
if (fromContent) return fromContent;
|
|
315
|
-
}
|
|
316
|
-
const authPath = options.authPath ?? defaultAuthPath();
|
|
317
|
-
try {
|
|
318
|
-
if (!existsSync(authPath)) return void 0;
|
|
319
|
-
const raw = readFileSync(authPath, "utf-8");
|
|
320
|
-
return credentialFromAuthJson(raw);
|
|
321
|
-
} catch {
|
|
322
|
-
return void 0;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// src/redact.ts
|
|
327
|
-
function redactBearer(value) {
|
|
328
|
-
return value.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [redacted]");
|
|
329
|
-
}
|
|
330
|
-
function redactCredentials(value) {
|
|
331
|
-
return value.replace(
|
|
332
|
-
/\b(?:api[-_ ]?key|apikey|access[-_ ]?token|refresh[-_ ]?token|token|secret|password|authorization)\s*[=:]\s*[^\s,;&)]+/gi,
|
|
333
|
-
(match) => {
|
|
334
|
-
const separatorIndex = match.search(/[=:]/);
|
|
335
|
-
return separatorIndex < 0 ? "[redacted]" : `${match.slice(0, separatorIndex + 1)}[redacted]`;
|
|
336
|
-
}
|
|
337
|
-
);
|
|
338
|
-
}
|
|
339
|
-
function redactUserTokens(value) {
|
|
340
|
-
return value.replace(/\b(?:user|cc)_[A-Za-z0-9_-]{8,}\b/gi, "[redacted]");
|
|
341
|
-
}
|
|
342
|
-
function redactQuerySecrets(value) {
|
|
343
|
-
return value.replace(
|
|
344
|
-
/([?&](?:api[-_ ]?key|apikey|access_token|refresh_token|token|secret|password)=)[^&#\s]+/gi,
|
|
345
|
-
"$1[redacted]"
|
|
346
|
-
);
|
|
347
|
-
}
|
|
348
|
-
function redactStandaloneSecrets(value) {
|
|
349
|
-
return value.replace(
|
|
350
|
-
/\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,
|
|
351
|
-
"[redacted]"
|
|
352
|
-
);
|
|
353
|
-
}
|
|
354
|
-
function redactCommandCodeErrorText(value) {
|
|
355
|
-
return redactStandaloneSecrets(
|
|
356
|
-
redactQuerySecrets(redactUserTokens(redactCredentials(redactBearer(value))))
|
|
357
|
-
);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// src/quota.ts
|
|
361
|
-
var DEFAULT_API_BASE = "https://api.commandcode.ai";
|
|
362
|
-
var QUOTA_TIMEOUT_MS = 15e3;
|
|
363
|
-
function isRecord2(value) {
|
|
364
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
365
|
-
}
|
|
366
|
-
function numberValue(value) {
|
|
367
|
-
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
368
|
-
}
|
|
369
|
-
function stringValue(value) {
|
|
370
|
-
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
371
|
-
}
|
|
372
|
-
function errorMessage(error) {
|
|
373
|
-
return error instanceof Error ? error.message : String(error);
|
|
374
|
-
}
|
|
375
|
-
function normalizeResetAt(value) {
|
|
376
|
-
let timestamp;
|
|
377
|
-
if (typeof value === "number" && Number.isFinite(value)) timestamp = value;
|
|
378
|
-
if (typeof value === "string" && value.length > 0) {
|
|
379
|
-
const trimmed = value.trim();
|
|
380
|
-
timestamp = /^\d+$/.test(trimmed) ? Number(trimmed) : Date.parse(trimmed);
|
|
381
|
-
}
|
|
382
|
-
if (timestamp === void 0 || !Number.isFinite(timestamp) || timestamp < 0) return null;
|
|
383
|
-
return timestamp >= 1e12 ? Math.round(timestamp / 1e3) : timestamp;
|
|
384
|
-
}
|
|
385
|
-
function windowLimitsFromCredits(value) {
|
|
386
|
-
if (!isRecord2(value)) return [];
|
|
387
|
-
const limits = [];
|
|
388
|
-
for (const [window, entry] of [
|
|
389
|
-
["fiveHour", value.fiveHour],
|
|
390
|
-
["weekly", value.weekly]
|
|
391
|
-
]) {
|
|
392
|
-
if (!isRecord2(entry)) continue;
|
|
393
|
-
const used = numberValue(entry.used);
|
|
394
|
-
const cap = numberValue(entry.cap);
|
|
395
|
-
if (used === void 0 || cap === void 0 || used === 0 && cap === 0) continue;
|
|
396
|
-
limits.push({ window, used, cap, resetAt: normalizeResetAt(entry.resetAt) });
|
|
397
|
-
}
|
|
398
|
-
return limits;
|
|
399
|
-
}
|
|
400
|
-
function parseCredits(value) {
|
|
401
|
-
if (!isRecord2(value) || !isRecord2(value.credits)) return null;
|
|
402
|
-
const credits = value.credits;
|
|
403
|
-
const monthlyCredits = numberValue(credits.monthlyCredits);
|
|
404
|
-
const purchasedCredits = numberValue(credits.purchasedCredits);
|
|
405
|
-
const freeCredits = numberValue(credits.freeCredits);
|
|
406
|
-
if (monthlyCredits === void 0 && purchasedCredits === void 0 && freeCredits === void 0) {
|
|
407
|
-
return null;
|
|
408
|
-
}
|
|
409
|
-
const monthly = monthlyCredits ?? 0;
|
|
410
|
-
const purchased = purchasedCredits ?? 0;
|
|
411
|
-
const free = freeCredits ?? 0;
|
|
412
|
-
return {
|
|
413
|
-
monthlyCredits: monthly,
|
|
414
|
-
purchasedCredits: purchased,
|
|
415
|
-
freeCredits: free,
|
|
416
|
-
remainingCredits: monthly + purchased + free,
|
|
417
|
-
windowLimits: windowLimitsFromCredits(value.windowLimits)
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
|
-
function parseSubscription(value) {
|
|
421
|
-
if (!isRecord2(value) || !isRecord2(value.data)) return null;
|
|
422
|
-
const data = value.data;
|
|
423
|
-
const planId = stringValue(data.planId);
|
|
424
|
-
const status = stringValue(data.status);
|
|
425
|
-
const currentPeriodStart = stringValue(data.currentPeriodStart);
|
|
426
|
-
const currentPeriodEnd = stringValue(data.currentPeriodEnd);
|
|
427
|
-
if (!planId && !status && !currentPeriodStart && !currentPeriodEnd) return null;
|
|
428
|
-
return {
|
|
429
|
-
planId: planId ?? null,
|
|
430
|
-
status: status ?? null,
|
|
431
|
-
currentPeriodStart: currentPeriodStart ?? null,
|
|
432
|
-
currentPeriodEnd: currentPeriodEnd ?? null
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
function parseSummary(value) {
|
|
436
|
-
if (!isRecord2(value)) return null;
|
|
437
|
-
const totalCost = numberValue(value.totalCost);
|
|
438
|
-
const totalCount = numberValue(value.totalCount);
|
|
439
|
-
if (totalCost === void 0 || totalCount === void 0) return null;
|
|
440
|
-
const totalTokens = numberValue(value.totalTokens) ?? numberValue(value.tokens);
|
|
441
|
-
return { totalCost, totalCount, ...totalTokens === void 0 ? {} : { totalTokens } };
|
|
442
|
-
}
|
|
443
|
-
function parseWhoami(value) {
|
|
444
|
-
if (!isRecord2(value)) return null;
|
|
445
|
-
const org = isRecord2(value.org) ? value.org : void 0;
|
|
446
|
-
const user = isRecord2(value.user) ? value.user : void 0;
|
|
447
|
-
const login = (org ? stringValue(org.login) : void 0) ?? (user ? stringValue(user.userName) ?? stringValue(user.name) : void 0);
|
|
448
|
-
if (!login) return null;
|
|
449
|
-
const orgId = org ? stringValue(org.id) : void 0;
|
|
450
|
-
const keyName = user ? stringValue(user.keyName) ?? stringValue(user.displayName) : void 0;
|
|
451
|
-
return { login, orgId: orgId ?? null, ...keyName ? { keyName } : {} };
|
|
452
|
-
}
|
|
453
|
-
function buildUrl(path, params) {
|
|
454
|
-
const search = new URLSearchParams();
|
|
455
|
-
for (const [key, value] of Object.entries(params)) {
|
|
456
|
-
if (value) search.set(key, value);
|
|
457
|
-
}
|
|
458
|
-
const query = search.toString();
|
|
459
|
-
return `${path}${query ? `?${query}` : ""}`;
|
|
460
|
-
}
|
|
461
|
-
function isHttpError(value) {
|
|
462
|
-
return isRecord2(value) && value.__httpError === true && typeof value.message === "string" && typeof value.status === "number" && typeof value.body === "string";
|
|
463
|
-
}
|
|
464
|
-
function isQuotaError(value) {
|
|
465
|
-
return isRecord2(value) && value.__quotaError === true && (value.kind === "timeout" || value.kind === "network");
|
|
466
|
-
}
|
|
467
|
-
function isBlockingHttpError(error) {
|
|
468
|
-
return error.status === 401 || error.status === 403;
|
|
469
|
-
}
|
|
470
|
-
function httpFailure(error, context) {
|
|
471
|
-
const detail = error.body.trim().slice(0, 200);
|
|
472
|
-
return {
|
|
473
|
-
ok: false,
|
|
474
|
-
error: {
|
|
475
|
-
kind: "http",
|
|
476
|
-
message: redactCommandCodeErrorText(
|
|
477
|
-
`${context} request failed (${error.status}): ${detail || error.message}`
|
|
478
|
-
)
|
|
479
|
-
}
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
var QuotaTimeoutError = class extends Error {
|
|
483
|
-
};
|
|
484
|
-
async function fetchCommandCodeQuota(options) {
|
|
485
|
-
if (!options.apiKey) {
|
|
486
|
-
return { ok: false, error: { message: "No Command Code API key found", kind: "config" } };
|
|
487
|
-
}
|
|
488
|
-
const baseUrl = options.baseUrl ?? DEFAULT_API_BASE;
|
|
489
|
-
const fetchImpl = options.fetchImpl ?? fetch;
|
|
490
|
-
const timeoutMs = options.timeoutMs ?? QUOTA_TIMEOUT_MS;
|
|
491
|
-
const overallController = new AbortController();
|
|
492
|
-
const overallTimer = setTimeout(() => overallController.abort(), timeoutMs);
|
|
493
|
-
const headers = {
|
|
494
|
-
accept: "application/json",
|
|
495
|
-
Authorization: `Bearer ${options.apiKey}`,
|
|
496
|
-
...options.extraHeaders
|
|
497
|
-
};
|
|
498
|
-
const request = async (path) => {
|
|
499
|
-
if (overallController.signal.aborted) throw new QuotaTimeoutError();
|
|
500
|
-
try {
|
|
501
|
-
const response = await fetchImpl(`${baseUrl}${path}`, {
|
|
502
|
-
method: "GET",
|
|
503
|
-
headers,
|
|
504
|
-
signal: overallController.signal
|
|
505
|
-
});
|
|
506
|
-
if (!response.ok) {
|
|
507
|
-
return {
|
|
508
|
-
__httpError: true,
|
|
509
|
-
message: response.status === 401 || response.status === 403 ? "Command Code rejected the API key" : response.statusText,
|
|
510
|
-
status: response.status,
|
|
511
|
-
body: await response.text().catch(() => "")
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
|
-
return await response.json();
|
|
515
|
-
} catch (error) {
|
|
516
|
-
if (overallController.signal.aborted) throw new QuotaTimeoutError();
|
|
517
|
-
throw error;
|
|
518
|
-
}
|
|
519
|
-
};
|
|
520
|
-
const safeRequest = async (path) => {
|
|
521
|
-
try {
|
|
522
|
-
return await request(path);
|
|
523
|
-
} catch (error) {
|
|
524
|
-
return {
|
|
525
|
-
__quotaError: true,
|
|
526
|
-
kind: error instanceof QuotaTimeoutError ? "timeout" : "network"
|
|
527
|
-
};
|
|
528
|
-
}
|
|
529
|
-
};
|
|
530
|
-
try {
|
|
531
|
-
const whoamiRaw = await request("/alpha/whoami");
|
|
532
|
-
if (isHttpError(whoamiRaw)) return httpFailure(whoamiRaw, "whoami");
|
|
533
|
-
const account = parseWhoami(whoamiRaw);
|
|
534
|
-
if (!account) {
|
|
535
|
-
return {
|
|
536
|
-
ok: false,
|
|
537
|
-
error: { kind: "http", message: "Command Code returned an unrecognized account response" }
|
|
538
|
-
};
|
|
539
|
-
}
|
|
540
|
-
const orgId = account.orgId ?? void 0;
|
|
541
|
-
const [creditsRaw, subscriptionRaw] = await Promise.all([
|
|
542
|
-
safeRequest(buildUrl("/alpha/billing/credits", { orgId })),
|
|
543
|
-
safeRequest(buildUrl("/alpha/billing/subscriptions", { orgId }))
|
|
544
|
-
]);
|
|
545
|
-
if (isHttpError(creditsRaw) && isBlockingHttpError(creditsRaw)) {
|
|
546
|
-
return httpFailure(creditsRaw, "credits");
|
|
547
|
-
}
|
|
548
|
-
if (isHttpError(subscriptionRaw) && isBlockingHttpError(subscriptionRaw)) {
|
|
549
|
-
return httpFailure(subscriptionRaw, "subscription");
|
|
550
|
-
}
|
|
551
|
-
const unavailable = [];
|
|
552
|
-
const credits = isHttpError(creditsRaw) || isQuotaError(creditsRaw) ? null : parseCredits(creditsRaw);
|
|
553
|
-
if (!credits) unavailable.push("credits");
|
|
554
|
-
const subscription = isHttpError(subscriptionRaw) || isQuotaError(subscriptionRaw) ? null : parseSubscription(subscriptionRaw);
|
|
555
|
-
if (!subscription) unavailable.push("subscription");
|
|
556
|
-
const summaryRaw = await safeRequest(
|
|
557
|
-
buildUrl("/alpha/usage/summary", {
|
|
558
|
-
orgId,
|
|
559
|
-
since: subscription?.currentPeriodStart ?? void 0
|
|
560
|
-
})
|
|
561
|
-
);
|
|
562
|
-
if (isHttpError(summaryRaw) && isBlockingHttpError(summaryRaw)) {
|
|
563
|
-
return httpFailure(summaryRaw, "summary");
|
|
564
|
-
}
|
|
565
|
-
const summary = isHttpError(summaryRaw) || isQuotaError(summaryRaw) ? null : parseSummary(summaryRaw);
|
|
566
|
-
if (!summary) unavailable.push("usage");
|
|
567
|
-
if (!credits && !subscription && !summary) {
|
|
568
|
-
return {
|
|
569
|
-
ok: false,
|
|
570
|
-
error: {
|
|
571
|
-
kind: overallController.signal.aborted ? "timeout" : "http",
|
|
572
|
-
message: overallController.signal.aborted ? "Command Code quota request timed out" : "Command Code returned no recognized usage data for the account"
|
|
573
|
-
}
|
|
574
|
-
};
|
|
575
|
-
}
|
|
576
|
-
return {
|
|
577
|
-
ok: true,
|
|
578
|
-
quota: {
|
|
579
|
-
account,
|
|
580
|
-
credits,
|
|
581
|
-
subscription,
|
|
582
|
-
summary,
|
|
583
|
-
...unavailable.length > 0 ? { unavailable } : {}
|
|
584
|
-
}
|
|
585
|
-
};
|
|
586
|
-
} catch (error) {
|
|
587
|
-
if (error instanceof QuotaTimeoutError || overallController.signal.aborted) {
|
|
588
|
-
return {
|
|
589
|
-
ok: false,
|
|
590
|
-
error: { message: "Command Code quota request timed out", kind: "timeout" }
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
return {
|
|
594
|
-
ok: false,
|
|
595
|
-
error: {
|
|
596
|
-
message: redactCommandCodeErrorText(
|
|
597
|
-
`Failed to fetch Command Code quota: ${errorMessage(error)}`
|
|
598
|
-
),
|
|
599
|
-
kind: "network"
|
|
600
|
-
}
|
|
601
|
-
};
|
|
602
|
-
} finally {
|
|
603
|
-
clearTimeout(overallTimer);
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// src/quota-format.ts
|
|
608
|
-
function formatWindowLimits(limits, now = Date.now) {
|
|
609
|
-
const labels = {
|
|
610
|
-
fiveHour: "5-hour",
|
|
611
|
-
weekly: "Weekly"
|
|
612
|
-
};
|
|
613
|
-
return limits.map((limit) => {
|
|
614
|
-
const used = limit.used.toFixed(2);
|
|
615
|
-
const cap = limit.cap.toFixed(2);
|
|
616
|
-
const percent = limit.cap > 0 ? Math.round(limit.used / limit.cap * 100) : 0;
|
|
617
|
-
const reset = limit.resetAt === null ? "" : ` (resets ${formatResetClock(limit.resetAt, now)})`;
|
|
618
|
-
return `${labels[limit.window]}: ${used} / ${cap} credits (${percent}% used)${reset}`;
|
|
619
|
-
});
|
|
620
|
-
}
|
|
621
|
-
function formatResetClock(resetAtSeconds, now) {
|
|
622
|
-
const date = new Date(resetAtSeconds * 1e3);
|
|
623
|
-
if (Number.isNaN(date.getTime())) return "unknown";
|
|
624
|
-
const diffMs = date.getTime() - now();
|
|
625
|
-
if (diffMs <= 0) return "soon";
|
|
626
|
-
const minutes = Math.ceil(diffMs / 6e4);
|
|
627
|
-
if (minutes < 60) return `in ${minutes}m`;
|
|
628
|
-
const hours = Math.floor(minutes / 60);
|
|
629
|
-
const remainingMinutes = minutes % 60;
|
|
630
|
-
if (hours < 24) {
|
|
631
|
-
return remainingMinutes > 0 ? `in ${hours}h ${remainingMinutes}m` : `in ${hours}h`;
|
|
632
|
-
}
|
|
633
|
-
const days = Math.floor(hours / 24);
|
|
634
|
-
return days === 1 ? "in 1 day" : `in ${days} days`;
|
|
635
|
-
}
|
|
636
|
-
function creditsDetail(credits) {
|
|
637
|
-
if (!credits) return void 0;
|
|
638
|
-
const parts = [
|
|
639
|
-
`monthly $${credits.monthlyCredits.toFixed(2)}`,
|
|
640
|
-
`purchased $${credits.purchasedCredits.toFixed(2)}`
|
|
641
|
-
];
|
|
642
|
-
if (credits.freeCredits > 0) parts.push(`free $${credits.freeCredits.toFixed(2)}`);
|
|
643
|
-
return `Sources: ${parts.join(" / ")}`;
|
|
644
|
-
}
|
|
645
|
-
function subscriptionLine(subscription) {
|
|
646
|
-
const plan = (subscription.planId ?? "Unknown").replace(/[_-]+/g, " ").trim();
|
|
647
|
-
const status = subscription.status ? ` (${subscription.status})` : "";
|
|
648
|
-
return `Plan: ${plan}${status}`;
|
|
649
|
-
}
|
|
650
|
-
function formatTokens(tokens) {
|
|
651
|
-
if (tokens >= 1e9) return `${(tokens / 1e9).toFixed(1)}B`;
|
|
652
|
-
if (tokens >= 1e6) return `${(tokens / 1e6).toFixed(1)}M`;
|
|
653
|
-
if (tokens >= 1e3) return `${(tokens / 1e3).toFixed(1)}k`;
|
|
654
|
-
return String(tokens);
|
|
655
|
-
}
|
|
656
|
-
function formatQuota(quota, now = Date.now) {
|
|
657
|
-
const lines = [];
|
|
658
|
-
const remaining = quota.credits?.remainingCredits ?? 0;
|
|
659
|
-
const spent = quota.summary?.totalCost ?? 0;
|
|
660
|
-
const pool = remaining + spent;
|
|
661
|
-
if (quota.credits || quota.summary) {
|
|
662
|
-
lines.push("Credits");
|
|
663
|
-
lines.push(` Remaining: $${remaining.toFixed(2)} of $${pool.toFixed(2)}`);
|
|
664
|
-
lines.push(` Used: $${spent.toFixed(2)}`);
|
|
665
|
-
lines.push(` ${pool > 0 ? Math.round(spent / pool * 100) : 0}% used`);
|
|
666
|
-
}
|
|
667
|
-
const detail = creditsDetail(quota.credits);
|
|
668
|
-
if (detail) lines.push(detail);
|
|
669
|
-
if (quota.subscription) lines.push(subscriptionLine(quota.subscription));
|
|
670
|
-
if (quota.summary) {
|
|
671
|
-
lines.push("");
|
|
672
|
-
lines.push(quota.subscription?.currentPeriodStart ? "Usage (billing period)" : "Usage");
|
|
673
|
-
lines.push(` Cost: $${quota.summary.totalCost.toFixed(2)}`);
|
|
674
|
-
lines.push(` Requests: ${quota.summary.totalCount.toLocaleString("en-US")}`);
|
|
675
|
-
if (quota.summary.totalTokens !== void 0) {
|
|
676
|
-
lines.push(` Tokens: ${formatTokens(quota.summary.totalTokens)}`);
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
lines.push("");
|
|
680
|
-
lines.push("Account");
|
|
681
|
-
lines.push(` ${quota.account.keyName ?? quota.account.login}`);
|
|
682
|
-
const limits = quota.credits?.windowLimits ?? [];
|
|
683
|
-
if (limits.length > 0) {
|
|
684
|
-
lines.push("");
|
|
685
|
-
lines.push("Usage windows:");
|
|
686
|
-
lines.push(...formatWindowLimits(limits, now).map((line) => ` ${line}`));
|
|
687
|
-
}
|
|
688
|
-
if ((quota.unavailable?.length ?? 0) > 0) {
|
|
689
|
-
lines.push("");
|
|
690
|
-
lines.push(`Unavailable: ${quota.unavailable?.join(", ")}`);
|
|
691
|
-
}
|
|
692
|
-
lines.push("");
|
|
693
|
-
lines.push("Full detail: https://commandcode.ai/usage");
|
|
694
|
-
return lines.join("\n");
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
// src/quota-tool.ts
|
|
698
|
-
function createQuotaTool({ apiBase, extraHeaders, client, readKey }) {
|
|
699
|
-
const resolveKey = readKey ?? readCommandCodeKey;
|
|
700
|
-
const showToast = (message, variant) => {
|
|
701
|
-
client.tui.showToast({ body: { message, variant } }).catch(() => {
|
|
702
|
-
});
|
|
703
|
-
};
|
|
704
|
-
return {
|
|
705
|
-
commandcode_quota: tool({
|
|
706
|
-
description: "Show the user's Command Code account usage and quota: remaining and used credits, plan, billing-period usage, and usage windows.",
|
|
707
|
-
args: {},
|
|
708
|
-
async execute() {
|
|
709
|
-
const apiKey = resolveKey();
|
|
710
|
-
if (!apiKey) {
|
|
711
|
-
showToast(
|
|
712
|
-
"Command Code quota needs an API key. Run /connect and select Command Code.",
|
|
713
|
-
"error"
|
|
714
|
-
);
|
|
715
|
-
return {
|
|
716
|
-
title: "Command Code quota",
|
|
717
|
-
output: "No Command Code API key found. Run /connect and authenticate first."
|
|
718
|
-
};
|
|
719
|
-
}
|
|
720
|
-
const result = await fetchCommandCodeQuota({
|
|
721
|
-
apiKey,
|
|
722
|
-
baseUrl: apiBase,
|
|
723
|
-
extraHeaders
|
|
724
|
-
});
|
|
725
|
-
if (!result.ok) {
|
|
726
|
-
showToast(result.error.message, "error");
|
|
727
|
-
return { title: "Command Code quota", output: result.error.message };
|
|
728
|
-
}
|
|
729
|
-
const output = formatQuota(result.quota);
|
|
730
|
-
showToast("Command Code quota loaded.", "success");
|
|
731
|
-
return { title: "Command Code quota", output };
|
|
732
|
-
}
|
|
733
|
-
})
|
|
734
|
-
};
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
// src/status-tool.ts
|
|
738
|
-
import { tool as tool2 } from "@opencode-ai/plugin";
|
|
739
|
-
|
|
740
|
-
// src/status.ts
|
|
741
|
-
function formatStatus(input) {
|
|
742
|
-
const lines = [
|
|
743
|
-
"Command Code plugin status",
|
|
744
|
-
` Source: ${input.source}`,
|
|
745
|
-
` Models: ${input.modelCount}`,
|
|
746
|
-
` Fetched: ${input.fetchedAt === void 0 ? "n/a" : new Date(input.fetchedAt).toISOString()}`,
|
|
747
|
-
` Endpoint: ${input.apiBase}`,
|
|
748
|
-
` Zero-data-retention: ${input.zdr ? "on" : "off"}`,
|
|
749
|
-
` Version: ${input.version}`
|
|
750
|
-
];
|
|
751
|
-
return lines.join("\n");
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
// src/status-tool.ts
|
|
755
|
-
function createStatusTool({
|
|
756
|
-
apiBase,
|
|
757
|
-
zdr,
|
|
758
|
-
version,
|
|
759
|
-
modelsInfo
|
|
760
|
-
}) {
|
|
761
|
-
return {
|
|
762
|
-
commandcode_status: tool2({
|
|
763
|
-
description: "Show Command Code plugin diagnostics: the model catalog source (live/cache/fallback), model count, cache time, endpoint, and zero-data-retention setting.",
|
|
764
|
-
args: {},
|
|
765
|
-
async execute() {
|
|
766
|
-
const output = formatStatus({
|
|
767
|
-
source: modelsInfo.source,
|
|
768
|
-
modelCount: modelsInfo.modelCount,
|
|
769
|
-
fetchedAt: modelsInfo.fetchedAt,
|
|
770
|
-
apiBase,
|
|
771
|
-
zdr,
|
|
772
|
-
version
|
|
773
|
-
});
|
|
774
|
-
return { title: "Command Code status", output };
|
|
775
|
-
}
|
|
776
|
-
})
|
|
777
|
-
};
|
|
778
|
-
}
|
|
779
304
|
|
|
780
305
|
// src/models.ts
|
|
781
|
-
import { mkdir
|
|
306
|
+
import { mkdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
782
307
|
import { dirname } from "path";
|
|
783
308
|
var DEFAULT_PROVIDER_API_BASE = "https://api.commandcode.ai/provider/v1";
|
|
784
309
|
var DEFAULT_MODELS_URL = `${DEFAULT_PROVIDER_API_BASE}/models`;
|
|
@@ -878,7 +403,7 @@ function modelCost(id) {
|
|
|
878
403
|
function isAnthropicModel(id) {
|
|
879
404
|
return id.startsWith("claude-");
|
|
880
405
|
}
|
|
881
|
-
function
|
|
406
|
+
function isRecord(value) {
|
|
882
407
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
883
408
|
}
|
|
884
409
|
function stringField(record, key) {
|
|
@@ -896,11 +421,11 @@ function positiveNumberField(record, key) {
|
|
|
896
421
|
return value;
|
|
897
422
|
}
|
|
898
423
|
function commandCodeModelsFromApiResponse(value) {
|
|
899
|
-
if (!
|
|
424
|
+
if (!isRecord(value)) throw new Error("Expected models response to be an object");
|
|
900
425
|
if (value.object !== "list") throw new Error("Expected models response object to be 'list'");
|
|
901
426
|
if (!Array.isArray(value.data)) throw new Error("Expected models response data to be an array");
|
|
902
427
|
const models = value.data.map((entry) => {
|
|
903
|
-
if (!
|
|
428
|
+
if (!isRecord(entry)) throw new Error("Expected model entry to be an object");
|
|
904
429
|
return {
|
|
905
430
|
id: stringField(entry, "id"),
|
|
906
431
|
name: stringField(entry, "name"),
|
|
@@ -935,7 +460,7 @@ function toProviderModelMap(models, providerBaseUrl2 = DEFAULT_PROVIDER_API_BASE
|
|
|
935
460
|
}
|
|
936
461
|
return map;
|
|
937
462
|
}
|
|
938
|
-
function
|
|
463
|
+
function errorMessage(error) {
|
|
939
464
|
return error instanceof Error ? error.message : String(error);
|
|
940
465
|
}
|
|
941
466
|
async function runWithTimeout(operation, timeoutMs, externalSignal) {
|
|
@@ -988,13 +513,13 @@ function cacheIsFresh(cachedAt, now, ttlMs) {
|
|
|
988
513
|
return now - cachedAt < ttlMs;
|
|
989
514
|
}
|
|
990
515
|
function commandCodeModelsFromCache(value) {
|
|
991
|
-
if (!
|
|
516
|
+
if (!isRecord(value)) throw new Error("Expected model cache to be an object");
|
|
992
517
|
if (value.version !== MODEL_CACHE_VERSION) {
|
|
993
518
|
throw new Error(`Expected model cache version ${MODEL_CACHE_VERSION}`);
|
|
994
519
|
}
|
|
995
520
|
if (!Array.isArray(value.models)) throw new Error("Expected cached models to be an array");
|
|
996
521
|
const models = value.models.map((entry) => {
|
|
997
|
-
if (!
|
|
522
|
+
if (!isRecord(entry)) throw new Error("Expected cached model entry to be an object");
|
|
998
523
|
return {
|
|
999
524
|
id: stringField(entry, "id"),
|
|
1000
525
|
name: stringField(entry, "name"),
|
|
@@ -1005,14 +530,14 @@ function commandCodeModelsFromCache(value) {
|
|
|
1005
530
|
return models;
|
|
1006
531
|
}
|
|
1007
532
|
async function readCommandCodeModelsCache(cachePath) {
|
|
1008
|
-
const contents = await
|
|
533
|
+
const contents = await readFile(cachePath, "utf-8");
|
|
1009
534
|
return commandCodeModelsFromCache(JSON.parse(contents));
|
|
1010
535
|
}
|
|
1011
536
|
async function writeCommandCodeModelsCache(cachePath, models, fetchedAt = Date.now()) {
|
|
1012
|
-
await
|
|
537
|
+
await mkdir(dirname(cachePath), { recursive: true });
|
|
1013
538
|
const temporaryPath = `${cachePath}.${process.pid}.tmp`;
|
|
1014
539
|
try {
|
|
1015
|
-
await
|
|
540
|
+
await writeFile(
|
|
1016
541
|
temporaryPath,
|
|
1017
542
|
`${JSON.stringify({ version: MODEL_CACHE_VERSION, fetchedAt, models }, null, 2)}
|
|
1018
543
|
`,
|
|
@@ -1041,7 +566,7 @@ async function loadCommandCodeModels(options) {
|
|
|
1041
566
|
models,
|
|
1042
567
|
source: "live",
|
|
1043
568
|
fetchedAt,
|
|
1044
|
-
warning: `Loaded the live Command Code model catalog but could not write the cache at ${cachePath}: ${
|
|
569
|
+
warning: `Loaded the live Command Code model catalog but could not write the cache at ${cachePath}: ${errorMessage(error)}`
|
|
1045
570
|
};
|
|
1046
571
|
}
|
|
1047
572
|
} catch (liveError) {
|
|
@@ -1054,23 +579,23 @@ async function loadCommandCodeModels(options) {
|
|
|
1054
579
|
models,
|
|
1055
580
|
source: "cache",
|
|
1056
581
|
fetchedAt: cachedAt ?? void 0,
|
|
1057
|
-
warning: stale ? `Could not refresh the Command Code model catalog (${
|
|
582
|
+
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}.`
|
|
1058
583
|
};
|
|
1059
584
|
} catch (cacheError) {
|
|
1060
585
|
const fallback = options.fallbackModels ?? [];
|
|
1061
586
|
return {
|
|
1062
587
|
models: fallback,
|
|
1063
588
|
source: "fallback",
|
|
1064
|
-
warning: `Could not refresh the Command Code model catalog (${
|
|
589
|
+
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.`
|
|
1065
590
|
};
|
|
1066
591
|
}
|
|
1067
592
|
}
|
|
1068
593
|
}
|
|
1069
594
|
async function cachedTimestamp(cachePath) {
|
|
1070
595
|
try {
|
|
1071
|
-
const contents = await
|
|
596
|
+
const contents = await readFile(cachePath, "utf-8");
|
|
1072
597
|
const parsed = JSON.parse(contents);
|
|
1073
|
-
if (!
|
|
598
|
+
if (!isRecord(parsed)) return null;
|
|
1074
599
|
return typeof parsed.fetchedAt === "number" && Number.isFinite(parsed.fetchedAt) ? parsed.fetchedAt : null;
|
|
1075
600
|
} catch {
|
|
1076
601
|
return null;
|
|
@@ -1078,72 +603,7 @@ async function cachedTimestamp(cachePath) {
|
|
|
1078
603
|
}
|
|
1079
604
|
|
|
1080
605
|
// src/index.ts
|
|
1081
|
-
var
|
|
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
|
-
];
|
|
606
|
+
var PROVIDER_ID = "commandcode";
|
|
1147
607
|
function zeroDataRetentionHeaders() {
|
|
1148
608
|
if (process.env.CMD_ZDR === "1" || process.env.COMMANDCODE_ZDR === "1") {
|
|
1149
609
|
return { "x-cmd-zdr": "1" };
|
|
@@ -1158,55 +618,57 @@ function providerBaseUrl() {
|
|
|
1158
618
|
function modelsCachePath() {
|
|
1159
619
|
const configured = process.env.COMMANDCODE_MODELS_CACHE;
|
|
1160
620
|
if (configured && configured.length > 0) return configured;
|
|
1161
|
-
return
|
|
621
|
+
return join(homedir(), ".cache", "opencode", "commandcode-models.json");
|
|
1162
622
|
}
|
|
1163
|
-
|
|
1164
|
-
const result = await loadCommandCodeModels({
|
|
1165
|
-
url: modelsUrl,
|
|
1166
|
-
cachePath,
|
|
1167
|
-
fallbackModels: FALLBACK_MODELS,
|
|
1168
|
-
timeoutMs: getModelsTimeoutMs()
|
|
1169
|
-
});
|
|
1170
|
-
if (result.warning) logWarn(result.warning);
|
|
1171
|
-
else logInfo(`Loaded ${result.models.length} Command Code models from ${result.source}`);
|
|
1172
|
-
return {
|
|
1173
|
-
modelMap: toProviderModelMap(result.models, providerBaseUrl2),
|
|
1174
|
-
modelsInfo: {
|
|
1175
|
-
source: result.source,
|
|
1176
|
-
modelCount: result.models.length,
|
|
1177
|
-
fetchedAt: result.fetchedAt
|
|
1178
|
-
}
|
|
1179
|
-
};
|
|
1180
|
-
}
|
|
1181
|
-
function getModelsTimeoutMs() {
|
|
623
|
+
function modelsTimeoutMs() {
|
|
1182
624
|
const raw = process.env.COMMANDCODE_MODELS_TIMEOUT_MS;
|
|
1183
625
|
if (!raw) return DEFAULT_MODELS_TIMEOUT_MS;
|
|
1184
626
|
const parsed = Number(raw);
|
|
1185
627
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_MODELS_TIMEOUT_MS;
|
|
1186
628
|
}
|
|
1187
|
-
var CommandCodePlugin = async ({ client
|
|
629
|
+
var CommandCodePlugin = async ({ client }) => {
|
|
1188
630
|
const apiBase = providerBaseUrl();
|
|
1189
631
|
const modelsUrl = process.env.COMMANDCODE_MODELS_URL ?? `${apiBase}/models`;
|
|
1190
632
|
const cachePath = modelsCachePath();
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
633
|
+
let models = {};
|
|
634
|
+
try {
|
|
635
|
+
const result = await loadCommandCodeModels({
|
|
636
|
+
url: modelsUrl,
|
|
637
|
+
cachePath,
|
|
638
|
+
fallbackModels: FALLBACK_MODELS,
|
|
639
|
+
timeoutMs: modelsTimeoutMs()
|
|
640
|
+
});
|
|
641
|
+
if (result.warning) {
|
|
642
|
+
client.app.log({
|
|
643
|
+
body: { service: "opencode-commandcode", level: "warn", message: result.warning }
|
|
644
|
+
}).catch(() => {
|
|
1199
645
|
});
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
|
|
646
|
+
} else {
|
|
647
|
+
client.app.log({
|
|
648
|
+
body: {
|
|
649
|
+
service: "opencode-commandcode",
|
|
650
|
+
level: "info",
|
|
651
|
+
message: `Loaded ${result.models.length} Command Code models from ${result.source}`
|
|
652
|
+
}
|
|
653
|
+
}).catch(() => {
|
|
1203
654
|
});
|
|
1204
655
|
}
|
|
1205
|
-
|
|
656
|
+
models = toProviderModelMap(result.models, apiBase);
|
|
657
|
+
} catch (error) {
|
|
658
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
659
|
+
client.app.log({
|
|
660
|
+
body: {
|
|
661
|
+
service: "opencode-commandcode",
|
|
662
|
+
level: "error",
|
|
663
|
+
message: `Failed to load Command Code models (${message})`
|
|
664
|
+
}
|
|
665
|
+
}).catch(() => {
|
|
666
|
+
});
|
|
667
|
+
}
|
|
1206
668
|
return {
|
|
1207
669
|
config: async (cfg) => {
|
|
1208
670
|
cfg.provider ??= {};
|
|
1209
|
-
cfg.provider[
|
|
671
|
+
cfg.provider[PROVIDER_ID] = {
|
|
1210
672
|
npm: "@ai-sdk/openai-compatible",
|
|
1211
673
|
name: "Command Code",
|
|
1212
674
|
env: ["COMMAND_CODE_API_KEY", "COMMANDCODE_API_KEY"],
|
|
@@ -1216,34 +678,8 @@ var CommandCodePlugin = async ({ client, directory }) => {
|
|
|
1216
678
|
},
|
|
1217
679
|
models
|
|
1218
680
|
};
|
|
1219
|
-
cfg.command ??= {};
|
|
1220
|
-
cfg.command["commandcode-quota"] = {
|
|
1221
|
-
description: "Show Command Code account usage and quota (credits, plan, usage)",
|
|
1222
|
-
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."
|
|
1223
|
-
};
|
|
1224
|
-
cfg.command["commandcode-status"] = {
|
|
1225
|
-
description: "Show Command Code plugin diagnostics (catalog source, model count, endpoint)",
|
|
1226
|
-
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."
|
|
1227
|
-
};
|
|
1228
|
-
client.app.log({
|
|
1229
|
-
body: {
|
|
1230
|
-
service: "opencode-commandcode",
|
|
1231
|
-
level: "info",
|
|
1232
|
-
message: `Registered Command Code provider with ${Object.keys(models).length} models (source: ${modelsInfo.source})`
|
|
1233
|
-
}
|
|
1234
|
-
}).catch(() => {
|
|
1235
|
-
});
|
|
1236
681
|
},
|
|
1237
|
-
auth: createAuthHook()
|
|
1238
|
-
tool: {
|
|
1239
|
-
...createQuotaTool({ apiBase, extraHeaders: zeroDataRetentionHeaders(), client }),
|
|
1240
|
-
...createStatusTool({
|
|
1241
|
-
apiBase,
|
|
1242
|
-
zdr: zeroDataRetentionHeaders() !== void 0,
|
|
1243
|
-
version: PLUGIN_VERSION,
|
|
1244
|
-
modelsInfo
|
|
1245
|
-
})
|
|
1246
|
-
}
|
|
682
|
+
auth: createAuthHook()
|
|
1247
683
|
};
|
|
1248
684
|
};
|
|
1249
685
|
var index_default = CommandCodePlugin;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gururea/opencode-commandcode-provider",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"dist",
|
|
16
|
-
"command",
|
|
17
16
|
"README.md",
|
|
18
17
|
"LICENSE"
|
|
19
18
|
],
|
|
@@ -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.
|