@gururea/opencode-commandcode-provider 0.3.0 → 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 +54 -629
- package/package.json +2 -8
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +0 -763
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";
|
|
@@ -302,558 +302,8 @@ var FALLBACK_MODELS = [
|
|
|
302
302
|
{ id: "xai/grok-4.6", name: "Grok 4.6", contextLength: 5e5 }
|
|
303
303
|
];
|
|
304
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
|
-
|
|
350
|
-
// src/quota-tool.ts
|
|
351
|
-
import { tool } from "@opencode-ai/plugin";
|
|
352
|
-
|
|
353
|
-
// src/api-key.ts
|
|
354
|
-
import { existsSync, readFileSync } from "fs";
|
|
355
|
-
var PROVIDER_ID = "commandcode";
|
|
356
|
-
function isRecord(value) {
|
|
357
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
358
|
-
}
|
|
359
|
-
function authKey(record) {
|
|
360
|
-
const type = record.type;
|
|
361
|
-
if (type === "api") return typeof record.key === "string" ? record.key : void 0;
|
|
362
|
-
if (type === "oauth") return typeof record.access === "string" ? record.access : void 0;
|
|
363
|
-
if (type === "wellknown") {
|
|
364
|
-
return typeof record.token === "string" ? record.token : typeof record.key === "string" ? record.key : void 0;
|
|
365
|
-
}
|
|
366
|
-
return void 0;
|
|
367
|
-
}
|
|
368
|
-
function credentialFromAuthJson(raw) {
|
|
369
|
-
try {
|
|
370
|
-
const parsed = JSON.parse(raw);
|
|
371
|
-
if (!isRecord(parsed)) return void 0;
|
|
372
|
-
const entry = parsed[PROVIDER_ID];
|
|
373
|
-
if (!isRecord(entry)) return void 0;
|
|
374
|
-
return authKey(entry);
|
|
375
|
-
} catch {
|
|
376
|
-
return void 0;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
function defaultAuthPath() {
|
|
380
|
-
return `${process.env.HOME ?? ""}/.local/share/opencode/auth.json`;
|
|
381
|
-
}
|
|
382
|
-
function readCommandCodeKey(options = {}) {
|
|
383
|
-
const env = options.env ?? process.env;
|
|
384
|
-
if (env.COMMAND_CODE_API_KEY) return env.COMMAND_CODE_API_KEY;
|
|
385
|
-
if (env.COMMANDCODE_API_KEY) return env.COMMANDCODE_API_KEY;
|
|
386
|
-
const authContent = options.authContent ?? env.OPENCODE_AUTH_CONTENT;
|
|
387
|
-
if (authContent) {
|
|
388
|
-
const fromContent = credentialFromAuthJson(authContent);
|
|
389
|
-
if (fromContent) return fromContent;
|
|
390
|
-
}
|
|
391
|
-
const authPath = options.authPath ?? defaultAuthPath();
|
|
392
|
-
try {
|
|
393
|
-
if (!existsSync(authPath)) return void 0;
|
|
394
|
-
const raw = readFileSync(authPath, "utf-8");
|
|
395
|
-
return credentialFromAuthJson(raw);
|
|
396
|
-
} catch {
|
|
397
|
-
return void 0;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// src/redact.ts
|
|
402
|
-
function redactBearer(value) {
|
|
403
|
-
return value.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [redacted]");
|
|
404
|
-
}
|
|
405
|
-
function redactCredentials(value) {
|
|
406
|
-
return value.replace(
|
|
407
|
-
/\b(?:api[-_ ]?key|apikey|access[-_ ]?token|refresh[-_ ]?token|token|secret|password|authorization)\s*[=:]\s*[^\s,;&)]+/gi,
|
|
408
|
-
(match) => {
|
|
409
|
-
const separatorIndex = match.search(/[=:]/);
|
|
410
|
-
return separatorIndex < 0 ? "[redacted]" : `${match.slice(0, separatorIndex + 1)}[redacted]`;
|
|
411
|
-
}
|
|
412
|
-
);
|
|
413
|
-
}
|
|
414
|
-
function redactUserTokens(value) {
|
|
415
|
-
return value.replace(/\b(?:user|cc)_[A-Za-z0-9_-]{8,}\b/gi, "[redacted]");
|
|
416
|
-
}
|
|
417
|
-
function redactQuerySecrets(value) {
|
|
418
|
-
return value.replace(
|
|
419
|
-
/([?&](?:api[-_ ]?key|apikey|access_token|refresh_token|token|secret|password)=)[^&#\s]+/gi,
|
|
420
|
-
"$1[redacted]"
|
|
421
|
-
);
|
|
422
|
-
}
|
|
423
|
-
function redactStandaloneSecrets(value) {
|
|
424
|
-
return value.replace(
|
|
425
|
-
/\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,
|
|
426
|
-
"[redacted]"
|
|
427
|
-
);
|
|
428
|
-
}
|
|
429
|
-
function redactCommandCodeErrorText(value) {
|
|
430
|
-
return redactStandaloneSecrets(
|
|
431
|
-
redactQuerySecrets(redactUserTokens(redactCredentials(redactBearer(value))))
|
|
432
|
-
);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// src/quota.ts
|
|
436
|
-
var DEFAULT_API_BASE = "https://api.commandcode.ai";
|
|
437
|
-
var QUOTA_TIMEOUT_MS = 15e3;
|
|
438
|
-
function isRecord2(value) {
|
|
439
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
440
|
-
}
|
|
441
|
-
function numberValue(value) {
|
|
442
|
-
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
443
|
-
}
|
|
444
|
-
function stringValue(value) {
|
|
445
|
-
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
446
|
-
}
|
|
447
|
-
function errorMessage(error) {
|
|
448
|
-
return error instanceof Error ? error.message : String(error);
|
|
449
|
-
}
|
|
450
|
-
function normalizeResetAt(value) {
|
|
451
|
-
let timestamp;
|
|
452
|
-
if (typeof value === "number" && Number.isFinite(value)) timestamp = value;
|
|
453
|
-
if (typeof value === "string" && value.length > 0) {
|
|
454
|
-
const trimmed = value.trim();
|
|
455
|
-
timestamp = /^\d+$/.test(trimmed) ? Number(trimmed) : Date.parse(trimmed);
|
|
456
|
-
}
|
|
457
|
-
if (timestamp === void 0 || !Number.isFinite(timestamp) || timestamp < 0) return null;
|
|
458
|
-
return timestamp >= 1e12 ? Math.round(timestamp / 1e3) : timestamp;
|
|
459
|
-
}
|
|
460
|
-
function windowLimitsFromCredits(value) {
|
|
461
|
-
if (!isRecord2(value)) return [];
|
|
462
|
-
const limits = [];
|
|
463
|
-
for (const [window, entry] of [
|
|
464
|
-
["fiveHour", value.fiveHour],
|
|
465
|
-
["weekly", value.weekly]
|
|
466
|
-
]) {
|
|
467
|
-
if (!isRecord2(entry)) continue;
|
|
468
|
-
const used = numberValue(entry.used);
|
|
469
|
-
const cap = numberValue(entry.cap);
|
|
470
|
-
if (used === void 0 || cap === void 0 || used === 0 && cap === 0) continue;
|
|
471
|
-
limits.push({ window, used, cap, resetAt: normalizeResetAt(entry.resetAt) });
|
|
472
|
-
}
|
|
473
|
-
return limits;
|
|
474
|
-
}
|
|
475
|
-
function parseCredits(value) {
|
|
476
|
-
if (!isRecord2(value) || !isRecord2(value.credits)) return null;
|
|
477
|
-
const credits = value.credits;
|
|
478
|
-
const monthlyCredits = numberValue(credits.monthlyCredits);
|
|
479
|
-
const purchasedCredits = numberValue(credits.purchasedCredits);
|
|
480
|
-
const freeCredits = numberValue(credits.freeCredits);
|
|
481
|
-
if (monthlyCredits === void 0 && purchasedCredits === void 0 && freeCredits === void 0) {
|
|
482
|
-
return null;
|
|
483
|
-
}
|
|
484
|
-
const monthly = monthlyCredits ?? 0;
|
|
485
|
-
const purchased = purchasedCredits ?? 0;
|
|
486
|
-
const free = freeCredits ?? 0;
|
|
487
|
-
return {
|
|
488
|
-
monthlyCredits: monthly,
|
|
489
|
-
purchasedCredits: purchased,
|
|
490
|
-
freeCredits: free,
|
|
491
|
-
remainingCredits: monthly + purchased + free,
|
|
492
|
-
windowLimits: windowLimitsFromCredits(value.windowLimits)
|
|
493
|
-
};
|
|
494
|
-
}
|
|
495
|
-
function parseSubscription(value) {
|
|
496
|
-
if (!isRecord2(value) || !isRecord2(value.data)) return null;
|
|
497
|
-
const data = value.data;
|
|
498
|
-
const planId = stringValue(data.planId);
|
|
499
|
-
const status = stringValue(data.status);
|
|
500
|
-
const currentPeriodStart = stringValue(data.currentPeriodStart);
|
|
501
|
-
const currentPeriodEnd = stringValue(data.currentPeriodEnd);
|
|
502
|
-
if (!planId && !status && !currentPeriodStart && !currentPeriodEnd) return null;
|
|
503
|
-
return {
|
|
504
|
-
planId: planId ?? null,
|
|
505
|
-
status: status ?? null,
|
|
506
|
-
currentPeriodStart: currentPeriodStart ?? null,
|
|
507
|
-
currentPeriodEnd: currentPeriodEnd ?? null
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
function parseSummary(value) {
|
|
511
|
-
if (!isRecord2(value)) return null;
|
|
512
|
-
const totalCost = numberValue(value.totalCost);
|
|
513
|
-
const totalCount = numberValue(value.totalCount);
|
|
514
|
-
if (totalCost === void 0 || totalCount === void 0) return null;
|
|
515
|
-
const totalTokens = numberValue(value.totalTokens) ?? numberValue(value.tokens);
|
|
516
|
-
return { totalCost, totalCount, ...totalTokens === void 0 ? {} : { totalTokens } };
|
|
517
|
-
}
|
|
518
|
-
function parseWhoami(value) {
|
|
519
|
-
if (!isRecord2(value)) return null;
|
|
520
|
-
const org = isRecord2(value.org) ? value.org : void 0;
|
|
521
|
-
const user = isRecord2(value.user) ? value.user : void 0;
|
|
522
|
-
const login = (org ? stringValue(org.login) : void 0) ?? (user ? stringValue(user.userName) ?? stringValue(user.name) : void 0);
|
|
523
|
-
if (!login) return null;
|
|
524
|
-
const orgId = org ? stringValue(org.id) : void 0;
|
|
525
|
-
const keyName = user ? stringValue(user.keyName) ?? stringValue(user.displayName) : void 0;
|
|
526
|
-
return { login, orgId: orgId ?? null, ...keyName ? { keyName } : {} };
|
|
527
|
-
}
|
|
528
|
-
function buildUrl(path, params) {
|
|
529
|
-
const search = new URLSearchParams();
|
|
530
|
-
for (const [key, value] of Object.entries(params)) {
|
|
531
|
-
if (value) search.set(key, value);
|
|
532
|
-
}
|
|
533
|
-
const query = search.toString();
|
|
534
|
-
return `${path}${query ? `?${query}` : ""}`;
|
|
535
|
-
}
|
|
536
|
-
function isHttpError(value) {
|
|
537
|
-
return isRecord2(value) && value.__httpError === true && typeof value.message === "string" && typeof value.status === "number" && typeof value.body === "string";
|
|
538
|
-
}
|
|
539
|
-
function isQuotaError(value) {
|
|
540
|
-
return isRecord2(value) && value.__quotaError === true && (value.kind === "timeout" || value.kind === "network");
|
|
541
|
-
}
|
|
542
|
-
function isBlockingHttpError(error) {
|
|
543
|
-
return error.status === 401 || error.status === 403;
|
|
544
|
-
}
|
|
545
|
-
function httpFailure(error, context) {
|
|
546
|
-
const detail = error.body.trim().slice(0, 200);
|
|
547
|
-
return {
|
|
548
|
-
ok: false,
|
|
549
|
-
error: {
|
|
550
|
-
kind: "http",
|
|
551
|
-
message: redactCommandCodeErrorText(
|
|
552
|
-
`${context} request failed (${error.status}): ${detail || error.message}`
|
|
553
|
-
)
|
|
554
|
-
}
|
|
555
|
-
};
|
|
556
|
-
}
|
|
557
|
-
var QuotaTimeoutError = class extends Error {
|
|
558
|
-
};
|
|
559
|
-
async function fetchCommandCodeQuota(options) {
|
|
560
|
-
if (!options.apiKey) {
|
|
561
|
-
return { ok: false, error: { message: "No Command Code API key found", kind: "config" } };
|
|
562
|
-
}
|
|
563
|
-
const baseUrl = options.baseUrl ?? DEFAULT_API_BASE;
|
|
564
|
-
const fetchImpl = options.fetchImpl ?? fetch;
|
|
565
|
-
const timeoutMs = options.timeoutMs ?? QUOTA_TIMEOUT_MS;
|
|
566
|
-
const overallController = new AbortController();
|
|
567
|
-
const overallTimer = setTimeout(() => overallController.abort(), timeoutMs);
|
|
568
|
-
const headers = {
|
|
569
|
-
accept: "application/json",
|
|
570
|
-
Authorization: `Bearer ${options.apiKey}`,
|
|
571
|
-
...options.extraHeaders
|
|
572
|
-
};
|
|
573
|
-
const request = async (path) => {
|
|
574
|
-
if (overallController.signal.aborted) throw new QuotaTimeoutError();
|
|
575
|
-
try {
|
|
576
|
-
const response = await fetchImpl(`${baseUrl}${path}`, {
|
|
577
|
-
method: "GET",
|
|
578
|
-
headers,
|
|
579
|
-
signal: overallController.signal
|
|
580
|
-
});
|
|
581
|
-
if (!response.ok) {
|
|
582
|
-
return {
|
|
583
|
-
__httpError: true,
|
|
584
|
-
message: response.status === 401 || response.status === 403 ? "Command Code rejected the API key" : response.statusText,
|
|
585
|
-
status: response.status,
|
|
586
|
-
body: await response.text().catch(() => "")
|
|
587
|
-
};
|
|
588
|
-
}
|
|
589
|
-
return await response.json();
|
|
590
|
-
} catch (error) {
|
|
591
|
-
if (overallController.signal.aborted) throw new QuotaTimeoutError();
|
|
592
|
-
throw error;
|
|
593
|
-
}
|
|
594
|
-
};
|
|
595
|
-
const safeRequest = async (path) => {
|
|
596
|
-
try {
|
|
597
|
-
return await request(path);
|
|
598
|
-
} catch (error) {
|
|
599
|
-
return {
|
|
600
|
-
__quotaError: true,
|
|
601
|
-
kind: error instanceof QuotaTimeoutError ? "timeout" : "network"
|
|
602
|
-
};
|
|
603
|
-
}
|
|
604
|
-
};
|
|
605
|
-
try {
|
|
606
|
-
const whoamiRaw = await request("/alpha/whoami");
|
|
607
|
-
if (isHttpError(whoamiRaw)) return httpFailure(whoamiRaw, "whoami");
|
|
608
|
-
const account = parseWhoami(whoamiRaw);
|
|
609
|
-
if (!account) {
|
|
610
|
-
return {
|
|
611
|
-
ok: false,
|
|
612
|
-
error: { kind: "http", message: "Command Code returned an unrecognized account response" }
|
|
613
|
-
};
|
|
614
|
-
}
|
|
615
|
-
const orgId = account.orgId ?? void 0;
|
|
616
|
-
const [creditsRaw, subscriptionRaw] = await Promise.all([
|
|
617
|
-
safeRequest(buildUrl("/alpha/billing/credits", { orgId })),
|
|
618
|
-
safeRequest(buildUrl("/alpha/billing/subscriptions", { orgId }))
|
|
619
|
-
]);
|
|
620
|
-
if (isHttpError(creditsRaw) && isBlockingHttpError(creditsRaw)) {
|
|
621
|
-
return httpFailure(creditsRaw, "credits");
|
|
622
|
-
}
|
|
623
|
-
if (isHttpError(subscriptionRaw) && isBlockingHttpError(subscriptionRaw)) {
|
|
624
|
-
return httpFailure(subscriptionRaw, "subscription");
|
|
625
|
-
}
|
|
626
|
-
const unavailable = [];
|
|
627
|
-
const credits = isHttpError(creditsRaw) || isQuotaError(creditsRaw) ? null : parseCredits(creditsRaw);
|
|
628
|
-
if (!credits) unavailable.push("credits");
|
|
629
|
-
const subscription = isHttpError(subscriptionRaw) || isQuotaError(subscriptionRaw) ? null : parseSubscription(subscriptionRaw);
|
|
630
|
-
if (!subscription) unavailable.push("subscription");
|
|
631
|
-
const summaryRaw = await safeRequest(
|
|
632
|
-
buildUrl("/alpha/usage/summary", {
|
|
633
|
-
orgId,
|
|
634
|
-
since: subscription?.currentPeriodStart ?? void 0
|
|
635
|
-
})
|
|
636
|
-
);
|
|
637
|
-
if (isHttpError(summaryRaw) && isBlockingHttpError(summaryRaw)) {
|
|
638
|
-
return httpFailure(summaryRaw, "summary");
|
|
639
|
-
}
|
|
640
|
-
const summary = isHttpError(summaryRaw) || isQuotaError(summaryRaw) ? null : parseSummary(summaryRaw);
|
|
641
|
-
if (!summary) unavailable.push("usage");
|
|
642
|
-
if (!credits && !subscription && !summary) {
|
|
643
|
-
return {
|
|
644
|
-
ok: false,
|
|
645
|
-
error: {
|
|
646
|
-
kind: overallController.signal.aborted ? "timeout" : "http",
|
|
647
|
-
message: overallController.signal.aborted ? "Command Code quota request timed out" : "Command Code returned no recognized usage data for the account"
|
|
648
|
-
}
|
|
649
|
-
};
|
|
650
|
-
}
|
|
651
|
-
return {
|
|
652
|
-
ok: true,
|
|
653
|
-
quota: {
|
|
654
|
-
account,
|
|
655
|
-
credits,
|
|
656
|
-
subscription,
|
|
657
|
-
summary,
|
|
658
|
-
...unavailable.length > 0 ? { unavailable } : {}
|
|
659
|
-
}
|
|
660
|
-
};
|
|
661
|
-
} catch (error) {
|
|
662
|
-
if (error instanceof QuotaTimeoutError || overallController.signal.aborted) {
|
|
663
|
-
return {
|
|
664
|
-
ok: false,
|
|
665
|
-
error: { message: "Command Code quota request timed out", kind: "timeout" }
|
|
666
|
-
};
|
|
667
|
-
}
|
|
668
|
-
return {
|
|
669
|
-
ok: false,
|
|
670
|
-
error: {
|
|
671
|
-
message: redactCommandCodeErrorText(
|
|
672
|
-
`Failed to fetch Command Code quota: ${errorMessage(error)}`
|
|
673
|
-
),
|
|
674
|
-
kind: "network"
|
|
675
|
-
}
|
|
676
|
-
};
|
|
677
|
-
} finally {
|
|
678
|
-
clearTimeout(overallTimer);
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
// src/quota-format.ts
|
|
683
|
-
function formatWindowLimits(limits, now = Date.now) {
|
|
684
|
-
const labels = {
|
|
685
|
-
fiveHour: "5-hour",
|
|
686
|
-
weekly: "Weekly"
|
|
687
|
-
};
|
|
688
|
-
return limits.map((limit) => {
|
|
689
|
-
const used = limit.used.toFixed(2);
|
|
690
|
-
const cap = limit.cap.toFixed(2);
|
|
691
|
-
const percent = limit.cap > 0 ? Math.round(limit.used / limit.cap * 100) : 0;
|
|
692
|
-
const reset = limit.resetAt === null ? "" : ` (resets ${formatResetClock(limit.resetAt, now)})`;
|
|
693
|
-
return `${labels[limit.window]}: ${used} / ${cap} credits (${percent}% used)${reset}`;
|
|
694
|
-
});
|
|
695
|
-
}
|
|
696
|
-
function formatResetClock(resetAtSeconds, now) {
|
|
697
|
-
const date = new Date(resetAtSeconds * 1e3);
|
|
698
|
-
if (Number.isNaN(date.getTime())) return "unknown";
|
|
699
|
-
const diffMs = date.getTime() - now();
|
|
700
|
-
if (diffMs <= 0) return "soon";
|
|
701
|
-
const minutes = Math.ceil(diffMs / 6e4);
|
|
702
|
-
if (minutes < 60) return `in ${minutes}m`;
|
|
703
|
-
const hours = Math.floor(minutes / 60);
|
|
704
|
-
const remainingMinutes = minutes % 60;
|
|
705
|
-
if (hours < 24) {
|
|
706
|
-
return remainingMinutes > 0 ? `in ${hours}h ${remainingMinutes}m` : `in ${hours}h`;
|
|
707
|
-
}
|
|
708
|
-
const days = Math.floor(hours / 24);
|
|
709
|
-
return days === 1 ? "in 1 day" : `in ${days} days`;
|
|
710
|
-
}
|
|
711
|
-
function creditsDetail(credits) {
|
|
712
|
-
if (!credits) return void 0;
|
|
713
|
-
const parts = [
|
|
714
|
-
`monthly $${credits.monthlyCredits.toFixed(2)}`,
|
|
715
|
-
`purchased $${credits.purchasedCredits.toFixed(2)}`
|
|
716
|
-
];
|
|
717
|
-
if (credits.freeCredits > 0) parts.push(`free $${credits.freeCredits.toFixed(2)}`);
|
|
718
|
-
return `Sources: ${parts.join(" / ")}`;
|
|
719
|
-
}
|
|
720
|
-
function subscriptionLine(subscription) {
|
|
721
|
-
const plan = (subscription.planId ?? "Unknown").replace(/[_-]+/g, " ").trim();
|
|
722
|
-
const status = subscription.status ? ` (${subscription.status})` : "";
|
|
723
|
-
return `Plan: ${plan}${status}`;
|
|
724
|
-
}
|
|
725
|
-
function formatTokens(tokens) {
|
|
726
|
-
if (tokens >= 1e9) return `${(tokens / 1e9).toFixed(1)}B`;
|
|
727
|
-
if (tokens >= 1e6) return `${(tokens / 1e6).toFixed(1)}M`;
|
|
728
|
-
if (tokens >= 1e3) return `${(tokens / 1e3).toFixed(1)}k`;
|
|
729
|
-
return String(tokens);
|
|
730
|
-
}
|
|
731
|
-
function formatQuota(quota, now = Date.now) {
|
|
732
|
-
const lines = [];
|
|
733
|
-
const remaining = quota.credits?.remainingCredits ?? 0;
|
|
734
|
-
const spent = quota.summary?.totalCost ?? 0;
|
|
735
|
-
const pool = remaining + spent;
|
|
736
|
-
if (quota.credits || quota.summary) {
|
|
737
|
-
lines.push("Credits");
|
|
738
|
-
lines.push(` Remaining: $${remaining.toFixed(2)} of $${pool.toFixed(2)}`);
|
|
739
|
-
lines.push(` Used: $${spent.toFixed(2)}`);
|
|
740
|
-
lines.push(` ${pool > 0 ? Math.round(spent / pool * 100) : 0}% used`);
|
|
741
|
-
}
|
|
742
|
-
const detail = creditsDetail(quota.credits);
|
|
743
|
-
if (detail) lines.push(detail);
|
|
744
|
-
if (quota.subscription) lines.push(subscriptionLine(quota.subscription));
|
|
745
|
-
if (quota.summary) {
|
|
746
|
-
lines.push("");
|
|
747
|
-
lines.push(quota.subscription?.currentPeriodStart ? "Usage (billing period)" : "Usage");
|
|
748
|
-
lines.push(` Cost: $${quota.summary.totalCost.toFixed(2)}`);
|
|
749
|
-
lines.push(` Requests: ${quota.summary.totalCount.toLocaleString("en-US")}`);
|
|
750
|
-
if (quota.summary.totalTokens !== void 0) {
|
|
751
|
-
lines.push(` Tokens: ${formatTokens(quota.summary.totalTokens)}`);
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
lines.push("");
|
|
755
|
-
lines.push("Account");
|
|
756
|
-
lines.push(` ${quota.account.keyName ?? quota.account.login}`);
|
|
757
|
-
const limits = quota.credits?.windowLimits ?? [];
|
|
758
|
-
if (limits.length > 0) {
|
|
759
|
-
lines.push("");
|
|
760
|
-
lines.push("Usage windows:");
|
|
761
|
-
lines.push(...formatWindowLimits(limits, now).map((line) => ` ${line}`));
|
|
762
|
-
}
|
|
763
|
-
if ((quota.unavailable?.length ?? 0) > 0) {
|
|
764
|
-
lines.push("");
|
|
765
|
-
lines.push(`Unavailable: ${quota.unavailable?.join(", ")}`);
|
|
766
|
-
}
|
|
767
|
-
lines.push("");
|
|
768
|
-
lines.push("Full detail: https://commandcode.ai/usage");
|
|
769
|
-
return lines.join("\n");
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
// src/quota-tool.ts
|
|
773
|
-
function createQuotaTool({ apiBase, extraHeaders, client, readKey }) {
|
|
774
|
-
const resolveKey = readKey ?? readCommandCodeKey;
|
|
775
|
-
const showToast = (message, variant) => {
|
|
776
|
-
client.tui.showToast({ body: { message, variant } }).catch(() => {
|
|
777
|
-
});
|
|
778
|
-
};
|
|
779
|
-
return {
|
|
780
|
-
commandcode_quota: tool({
|
|
781
|
-
description: "Show the user's Command Code account usage and quota: remaining and used credits, plan, billing-period usage, and usage windows.",
|
|
782
|
-
args: {},
|
|
783
|
-
async execute() {
|
|
784
|
-
const apiKey = resolveKey();
|
|
785
|
-
if (!apiKey) {
|
|
786
|
-
showToast(
|
|
787
|
-
"Command Code quota needs an API key. Run /connect and select Command Code.",
|
|
788
|
-
"error"
|
|
789
|
-
);
|
|
790
|
-
return {
|
|
791
|
-
title: "Command Code quota",
|
|
792
|
-
output: "No Command Code API key found. Run /connect and authenticate first."
|
|
793
|
-
};
|
|
794
|
-
}
|
|
795
|
-
const result = await fetchCommandCodeQuota({
|
|
796
|
-
apiKey,
|
|
797
|
-
baseUrl: apiBase,
|
|
798
|
-
extraHeaders
|
|
799
|
-
});
|
|
800
|
-
if (!result.ok) {
|
|
801
|
-
showToast(result.error.message, "error");
|
|
802
|
-
return { title: "Command Code quota", output: result.error.message };
|
|
803
|
-
}
|
|
804
|
-
const output = formatQuota(result.quota);
|
|
805
|
-
showToast("Command Code quota loaded.", "success");
|
|
806
|
-
return { title: "Command Code quota", output };
|
|
807
|
-
}
|
|
808
|
-
})
|
|
809
|
-
};
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
// src/status-tool.ts
|
|
813
|
-
import { tool as tool2 } from "@opencode-ai/plugin";
|
|
814
|
-
|
|
815
|
-
// src/status.ts
|
|
816
|
-
function formatStatus(input) {
|
|
817
|
-
const lines = [
|
|
818
|
-
"Command Code plugin status",
|
|
819
|
-
` Source: ${input.source}`,
|
|
820
|
-
` Models: ${input.modelCount}`,
|
|
821
|
-
` Fetched: ${input.fetchedAt === void 0 ? "n/a" : new Date(input.fetchedAt).toISOString()}`,
|
|
822
|
-
` Endpoint: ${input.apiBase}`,
|
|
823
|
-
` Zero-data-retention: ${input.zdr ? "on" : "off"}`,
|
|
824
|
-
` Version: ${input.version}`
|
|
825
|
-
];
|
|
826
|
-
return lines.join("\n");
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
// src/status-tool.ts
|
|
830
|
-
function createStatusTool({
|
|
831
|
-
apiBase,
|
|
832
|
-
zdr,
|
|
833
|
-
version,
|
|
834
|
-
modelsInfo
|
|
835
|
-
}) {
|
|
836
|
-
return {
|
|
837
|
-
commandcode_status: tool2({
|
|
838
|
-
description: "Show Command Code plugin diagnostics: the model catalog source (live/cache/fallback), model count, cache time, endpoint, and zero-data-retention setting.",
|
|
839
|
-
args: {},
|
|
840
|
-
async execute() {
|
|
841
|
-
const output = formatStatus({
|
|
842
|
-
source: modelsInfo.source,
|
|
843
|
-
modelCount: modelsInfo.modelCount,
|
|
844
|
-
fetchedAt: modelsInfo.fetchedAt,
|
|
845
|
-
apiBase,
|
|
846
|
-
zdr,
|
|
847
|
-
version
|
|
848
|
-
});
|
|
849
|
-
return { title: "Command Code status", output };
|
|
850
|
-
}
|
|
851
|
-
})
|
|
852
|
-
};
|
|
853
|
-
}
|
|
854
|
-
|
|
855
305
|
// src/models.ts
|
|
856
|
-
import { mkdir
|
|
306
|
+
import { mkdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
857
307
|
import { dirname } from "path";
|
|
858
308
|
var DEFAULT_PROVIDER_API_BASE = "https://api.commandcode.ai/provider/v1";
|
|
859
309
|
var DEFAULT_MODELS_URL = `${DEFAULT_PROVIDER_API_BASE}/models`;
|
|
@@ -953,7 +403,7 @@ function modelCost(id) {
|
|
|
953
403
|
function isAnthropicModel(id) {
|
|
954
404
|
return id.startsWith("claude-");
|
|
955
405
|
}
|
|
956
|
-
function
|
|
406
|
+
function isRecord(value) {
|
|
957
407
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
958
408
|
}
|
|
959
409
|
function stringField(record, key) {
|
|
@@ -971,11 +421,11 @@ function positiveNumberField(record, key) {
|
|
|
971
421
|
return value;
|
|
972
422
|
}
|
|
973
423
|
function commandCodeModelsFromApiResponse(value) {
|
|
974
|
-
if (!
|
|
424
|
+
if (!isRecord(value)) throw new Error("Expected models response to be an object");
|
|
975
425
|
if (value.object !== "list") throw new Error("Expected models response object to be 'list'");
|
|
976
426
|
if (!Array.isArray(value.data)) throw new Error("Expected models response data to be an array");
|
|
977
427
|
const models = value.data.map((entry) => {
|
|
978
|
-
if (!
|
|
428
|
+
if (!isRecord(entry)) throw new Error("Expected model entry to be an object");
|
|
979
429
|
return {
|
|
980
430
|
id: stringField(entry, "id"),
|
|
981
431
|
name: stringField(entry, "name"),
|
|
@@ -1010,7 +460,7 @@ function toProviderModelMap(models, providerBaseUrl2 = DEFAULT_PROVIDER_API_BASE
|
|
|
1010
460
|
}
|
|
1011
461
|
return map;
|
|
1012
462
|
}
|
|
1013
|
-
function
|
|
463
|
+
function errorMessage(error) {
|
|
1014
464
|
return error instanceof Error ? error.message : String(error);
|
|
1015
465
|
}
|
|
1016
466
|
async function runWithTimeout(operation, timeoutMs, externalSignal) {
|
|
@@ -1063,13 +513,13 @@ function cacheIsFresh(cachedAt, now, ttlMs) {
|
|
|
1063
513
|
return now - cachedAt < ttlMs;
|
|
1064
514
|
}
|
|
1065
515
|
function commandCodeModelsFromCache(value) {
|
|
1066
|
-
if (!
|
|
516
|
+
if (!isRecord(value)) throw new Error("Expected model cache to be an object");
|
|
1067
517
|
if (value.version !== MODEL_CACHE_VERSION) {
|
|
1068
518
|
throw new Error(`Expected model cache version ${MODEL_CACHE_VERSION}`);
|
|
1069
519
|
}
|
|
1070
520
|
if (!Array.isArray(value.models)) throw new Error("Expected cached models to be an array");
|
|
1071
521
|
const models = value.models.map((entry) => {
|
|
1072
|
-
if (!
|
|
522
|
+
if (!isRecord(entry)) throw new Error("Expected cached model entry to be an object");
|
|
1073
523
|
return {
|
|
1074
524
|
id: stringField(entry, "id"),
|
|
1075
525
|
name: stringField(entry, "name"),
|
|
@@ -1080,14 +530,14 @@ function commandCodeModelsFromCache(value) {
|
|
|
1080
530
|
return models;
|
|
1081
531
|
}
|
|
1082
532
|
async function readCommandCodeModelsCache(cachePath) {
|
|
1083
|
-
const contents = await
|
|
533
|
+
const contents = await readFile(cachePath, "utf-8");
|
|
1084
534
|
return commandCodeModelsFromCache(JSON.parse(contents));
|
|
1085
535
|
}
|
|
1086
536
|
async function writeCommandCodeModelsCache(cachePath, models, fetchedAt = Date.now()) {
|
|
1087
|
-
await
|
|
537
|
+
await mkdir(dirname(cachePath), { recursive: true });
|
|
1088
538
|
const temporaryPath = `${cachePath}.${process.pid}.tmp`;
|
|
1089
539
|
try {
|
|
1090
|
-
await
|
|
540
|
+
await writeFile(
|
|
1091
541
|
temporaryPath,
|
|
1092
542
|
`${JSON.stringify({ version: MODEL_CACHE_VERSION, fetchedAt, models }, null, 2)}
|
|
1093
543
|
`,
|
|
@@ -1116,7 +566,7 @@ async function loadCommandCodeModels(options) {
|
|
|
1116
566
|
models,
|
|
1117
567
|
source: "live",
|
|
1118
568
|
fetchedAt,
|
|
1119
|
-
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)}`
|
|
1120
570
|
};
|
|
1121
571
|
}
|
|
1122
572
|
} catch (liveError) {
|
|
@@ -1129,23 +579,23 @@ async function loadCommandCodeModels(options) {
|
|
|
1129
579
|
models,
|
|
1130
580
|
source: "cache",
|
|
1131
581
|
fetchedAt: cachedAt ?? void 0,
|
|
1132
|
-
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}.`
|
|
1133
583
|
};
|
|
1134
584
|
} catch (cacheError) {
|
|
1135
585
|
const fallback = options.fallbackModels ?? [];
|
|
1136
586
|
return {
|
|
1137
587
|
models: fallback,
|
|
1138
588
|
source: "fallback",
|
|
1139
|
-
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.`
|
|
1140
590
|
};
|
|
1141
591
|
}
|
|
1142
592
|
}
|
|
1143
593
|
}
|
|
1144
594
|
async function cachedTimestamp(cachePath) {
|
|
1145
595
|
try {
|
|
1146
|
-
const contents = await
|
|
596
|
+
const contents = await readFile(cachePath, "utf-8");
|
|
1147
597
|
const parsed = JSON.parse(contents);
|
|
1148
|
-
if (!
|
|
598
|
+
if (!isRecord(parsed)) return null;
|
|
1149
599
|
return typeof parsed.fetchedAt === "number" && Number.isFinite(parsed.fetchedAt) ? parsed.fetchedAt : null;
|
|
1150
600
|
} catch {
|
|
1151
601
|
return null;
|
|
@@ -1153,8 +603,7 @@ async function cachedTimestamp(cachePath) {
|
|
|
1153
603
|
}
|
|
1154
604
|
|
|
1155
605
|
// src/index.ts
|
|
1156
|
-
var
|
|
1157
|
-
var PLUGIN_VERSION = "0.3.0";
|
|
606
|
+
var PROVIDER_ID = "commandcode";
|
|
1158
607
|
function zeroDataRetentionHeaders() {
|
|
1159
608
|
if (process.env.CMD_ZDR === "1" || process.env.COMMANDCODE_ZDR === "1") {
|
|
1160
609
|
return { "x-cmd-zdr": "1" };
|
|
@@ -1169,55 +618,57 @@ function providerBaseUrl() {
|
|
|
1169
618
|
function modelsCachePath() {
|
|
1170
619
|
const configured = process.env.COMMANDCODE_MODELS_CACHE;
|
|
1171
620
|
if (configured && configured.length > 0) return configured;
|
|
1172
|
-
return
|
|
1173
|
-
}
|
|
1174
|
-
async function loadProviderModels(modelsUrl, cachePath, providerBaseUrl2, logWarn, logInfo) {
|
|
1175
|
-
const result = await loadCommandCodeModels({
|
|
1176
|
-
url: modelsUrl,
|
|
1177
|
-
cachePath,
|
|
1178
|
-
fallbackModels: FALLBACK_MODELS,
|
|
1179
|
-
timeoutMs: getModelsTimeoutMs()
|
|
1180
|
-
});
|
|
1181
|
-
if (result.warning) logWarn(result.warning);
|
|
1182
|
-
else logInfo(`Loaded ${result.models.length} Command Code models from ${result.source}`);
|
|
1183
|
-
return {
|
|
1184
|
-
modelMap: toProviderModelMap(result.models, providerBaseUrl2),
|
|
1185
|
-
modelsInfo: {
|
|
1186
|
-
source: result.source,
|
|
1187
|
-
modelCount: result.models.length,
|
|
1188
|
-
fetchedAt: result.fetchedAt
|
|
1189
|
-
}
|
|
1190
|
-
};
|
|
621
|
+
return join(homedir(), ".cache", "opencode", "commandcode-models.json");
|
|
1191
622
|
}
|
|
1192
|
-
function
|
|
623
|
+
function modelsTimeoutMs() {
|
|
1193
624
|
const raw = process.env.COMMANDCODE_MODELS_TIMEOUT_MS;
|
|
1194
625
|
if (!raw) return DEFAULT_MODELS_TIMEOUT_MS;
|
|
1195
626
|
const parsed = Number(raw);
|
|
1196
627
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_MODELS_TIMEOUT_MS;
|
|
1197
628
|
}
|
|
1198
|
-
var CommandCodePlugin = async ({ client
|
|
629
|
+
var CommandCodePlugin = async ({ client }) => {
|
|
1199
630
|
const apiBase = providerBaseUrl();
|
|
1200
631
|
const modelsUrl = process.env.COMMANDCODE_MODELS_URL ?? `${apiBase}/models`;
|
|
1201
632
|
const cachePath = modelsCachePath();
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
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(() => {
|
|
1210
645
|
});
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
|
|
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(() => {
|
|
1214
654
|
});
|
|
1215
655
|
}
|
|
1216
|
-
|
|
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
|
+
}
|
|
1217
668
|
return {
|
|
1218
669
|
config: async (cfg) => {
|
|
1219
670
|
cfg.provider ??= {};
|
|
1220
|
-
cfg.provider[
|
|
671
|
+
cfg.provider[PROVIDER_ID] = {
|
|
1221
672
|
npm: "@ai-sdk/openai-compatible",
|
|
1222
673
|
name: "Command Code",
|
|
1223
674
|
env: ["COMMAND_CODE_API_KEY", "COMMANDCODE_API_KEY"],
|
|
@@ -1227,34 +678,8 @@ var CommandCodePlugin = async ({ client, directory }) => {
|
|
|
1227
678
|
},
|
|
1228
679
|
models
|
|
1229
680
|
};
|
|
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
|
-
};
|
|
1239
|
-
client.app.log({
|
|
1240
|
-
body: {
|
|
1241
|
-
service: "opencode-commandcode",
|
|
1242
|
-
level: "info",
|
|
1243
|
-
message: `Registered Command Code provider with ${Object.keys(models).length} models (source: ${modelsInfo.source})`
|
|
1244
|
-
}
|
|
1245
|
-
}).catch(() => {
|
|
1246
|
-
});
|
|
1247
681
|
},
|
|
1248
|
-
auth: createAuthHook()
|
|
1249
|
-
tool: {
|
|
1250
|
-
...createQuotaTool({ apiBase, extraHeaders: zeroDataRetentionHeaders(), client }),
|
|
1251
|
-
...createStatusTool({
|
|
1252
|
-
apiBase,
|
|
1253
|
-
zdr: zeroDataRetentionHeaders() !== void 0,
|
|
1254
|
-
version: PLUGIN_VERSION,
|
|
1255
|
-
modelsInfo
|
|
1256
|
-
})
|
|
1257
|
-
}
|
|
682
|
+
auth: createAuthHook()
|
|
1258
683
|
};
|
|
1259
684
|
};
|
|
1260
685
|
var index_default = CommandCodePlugin;
|