@telsoncc/opencode-commandcode 1.0.5 → 1.1.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/CHANGELOG.md +4 -0
- package/_version.txt +1 -1
- package/manifest.json +4 -4
- package/models.json +4 -4
- package/package.json +1 -1
- package/plugin-opencode2.ts +47 -2
- package/src/opencode2.ts +129 -2
- package/src/startup.ts +3 -1
package/CHANGELOG.md
CHANGED
package/_version.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.65.
|
|
1
|
+
1.65.2
|
package/manifest.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-09-
|
|
4
|
-
"pluginVersion": "1.0
|
|
5
|
-
"commandCodeVersion": "1.65.
|
|
6
|
-
"commandCodeTarball": "https://registry.npmjs.org/command-code/-/command-code-1.65.
|
|
3
|
+
"generatedAt": "2026-09-24T21:10:50.082Z",
|
|
4
|
+
"pluginVersion": "1.1.0",
|
|
5
|
+
"commandCodeVersion": "1.65.2",
|
|
6
|
+
"commandCodeTarball": "https://registry.npmjs.org/command-code/-/command-code-1.65.2.tgz",
|
|
7
7
|
"modelCount": 81,
|
|
8
8
|
"reasoningModelCount": 68,
|
|
9
9
|
"extraction": {
|
package/models.json
CHANGED
|
@@ -671,9 +671,9 @@
|
|
|
671
671
|
],
|
|
672
672
|
"tool_call": true,
|
|
673
673
|
"cost": {
|
|
674
|
-
"input": 0.
|
|
675
|
-
"output":
|
|
676
|
-
"cache_read": 0.
|
|
674
|
+
"input": 0.15,
|
|
675
|
+
"output": 0.6,
|
|
676
|
+
"cache_read": 0.003
|
|
677
677
|
},
|
|
678
678
|
"limit": {
|
|
679
679
|
"context": 1000000,
|
|
@@ -2349,7 +2349,7 @@
|
|
|
2349
2349
|
"cache_read": 0.02
|
|
2350
2350
|
},
|
|
2351
2351
|
"limit": {
|
|
2352
|
-
"context":
|
|
2352
|
+
"context": 262144,
|
|
2353
2353
|
"output": 131072
|
|
2354
2354
|
},
|
|
2355
2355
|
"attachment": false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telsoncc/opencode-commandcode",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Command Code API provider for opencode — use Claude, GPT, Gemini, DeepSeek, Qwen, Kimi, GLM, MiniMax, and Step models via Command Code",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
package/plugin-opencode2.ts
CHANGED
|
@@ -3,7 +3,13 @@ import {
|
|
|
3
3
|
COMMANDCODE_INTEGRATION_ID,
|
|
4
4
|
applyIntegration,
|
|
5
5
|
applyProviderInventory,
|
|
6
|
+
attachRemoteRefreshRunner,
|
|
7
|
+
isRemoteSyncDisabled,
|
|
6
8
|
loadCatalogForV2,
|
|
9
|
+
persistRefreshedModels,
|
|
10
|
+
refreshCatalogFromRemote,
|
|
11
|
+
writeRemoteRefreshSummary,
|
|
12
|
+
type RemoteRefreshResult,
|
|
7
13
|
type V2SetupContext,
|
|
8
14
|
} from "./src/opencode2.js";
|
|
9
15
|
import type { ModelEntry } from "./src/catalog.js";
|
|
@@ -15,7 +21,9 @@ import type { ModelEntry } from "./src/catalog.js";
|
|
|
15
21
|
* Models register in memory via `ctx.provider.transform` + `editor.add` +
|
|
16
22
|
* `reload()` — nothing is written into `opencode.json`. Auth registers a
|
|
17
23
|
* `commandcode` integration (API key via `/connect`, `COMMANDCODE_API_KEY`
|
|
18
|
-
* env fallback).
|
|
24
|
+
* env fallback). After startup the inventory refreshes itself from the live
|
|
25
|
+
* availability API every 6h (opt out with `disableModelSync` in
|
|
26
|
+
* `~/.config/opencode/opencode-commandcode.json`).
|
|
19
27
|
*
|
|
20
28
|
* Dual export: `{ id, setup, server }`. OpenCode 2.0 loads this module and
|
|
21
29
|
* calls `setup()`; OpenCode 1.18.29+ prefers `exports["./server"]` and calls
|
|
@@ -39,7 +47,9 @@ export async function setup(ctx: V2SetupContext): Promise<() => Promise<void>> {
|
|
|
39
47
|
);
|
|
40
48
|
|
|
41
49
|
const catalog = loadCatalogForV2();
|
|
42
|
-
|
|
50
|
+
// Reassigned by the remote refresh below; the provider transform closure
|
|
51
|
+
// replays against the current value on every reload().
|
|
52
|
+
let inventory: ModelEntry[] = catalog.models;
|
|
43
53
|
let sourceConnection: unknown;
|
|
44
54
|
const refreshConnection = async (): Promise<void> => {
|
|
45
55
|
try {
|
|
@@ -61,6 +71,40 @@ export async function setup(ctx: V2SetupContext): Promise<() => Promise<void>> {
|
|
|
61
71
|
// Keep the last-good inventory when the reload fails.
|
|
62
72
|
}
|
|
63
73
|
|
|
74
|
+
// Live availability refresh: prune ids the API retired and surface new
|
|
75
|
+
// ones without waiting for an npm publish. The bundled catalog stays as
|
|
76
|
+
// the offline fallback; models unknown locally are reported, not
|
|
77
|
+
// published, until a catalog sync extracts their cost data.
|
|
78
|
+
const sameInventory = (next: ModelEntry[]): boolean =>
|
|
79
|
+
next.length === inventory.length && next.every((m, i) => m.id === inventory[i]?.id);
|
|
80
|
+
|
|
81
|
+
const syncFromRemote = async (): Promise<void> => {
|
|
82
|
+
if (isRemoteSyncDisabled()) return;
|
|
83
|
+
let result: RemoteRefreshResult;
|
|
84
|
+
try {
|
|
85
|
+
result = await refreshCatalogFromRemote(loadCatalogForV2().models);
|
|
86
|
+
} catch {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
writeRemoteRefreshSummary(result, catalog.commandCodeVersion);
|
|
90
|
+
if (result.models.length === 0 || sameInventory(result.models)) return;
|
|
91
|
+
inventory = result.models;
|
|
92
|
+
persistRefreshedModels(inventory);
|
|
93
|
+
try {
|
|
94
|
+
await ctx.provider.reload();
|
|
95
|
+
} catch {
|
|
96
|
+
// Keep serving the last-good inventory; the next tick retries.
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
let detachRefresh: (() => void) | null = null;
|
|
101
|
+
if (!isRemoteSyncDisabled()) {
|
|
102
|
+
detachRefresh = attachRemoteRefreshRunner(() => syncFromRemote());
|
|
103
|
+
void syncFromRemote().catch(() => {
|
|
104
|
+
// Startup already published the bundled inventory.
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
64
108
|
// Rebind the inventory when credentials switch; the transform closure reads
|
|
65
109
|
// the current `inventory`/`sourceConnection` on every replay.
|
|
66
110
|
let stopped = false;
|
|
@@ -88,6 +132,7 @@ export async function setup(ctx: V2SetupContext): Promise<() => Promise<void>> {
|
|
|
88
132
|
|
|
89
133
|
return async () => {
|
|
90
134
|
stopped = true;
|
|
135
|
+
detachRefresh?.();
|
|
91
136
|
for (const registration of registrations.reverse()) {
|
|
92
137
|
try {
|
|
93
138
|
await registration.dispose();
|
package/src/opencode2.ts
CHANGED
|
@@ -2,7 +2,14 @@ import { readFileSync, existsSync } from "fs";
|
|
|
2
2
|
import { homedir } from "os";
|
|
3
3
|
import { join, dirname } from "path";
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
MODELS_API_URL,
|
|
7
|
+
filterCatalogByAvailability,
|
|
8
|
+
parseAvailabilityIds,
|
|
9
|
+
toConfigKey,
|
|
10
|
+
loadCatalogFromLocalCommandCode,
|
|
11
|
+
type ModelEntry,
|
|
12
|
+
} from "./catalog.js";
|
|
6
13
|
import {
|
|
7
14
|
readCatalogCache,
|
|
8
15
|
writeCatalogCache,
|
|
@@ -241,7 +248,7 @@ function readBundledVersion(): string | null {
|
|
|
241
248
|
}
|
|
242
249
|
}
|
|
243
250
|
|
|
244
|
-
export type V2CatalogSource = "bundled" | "cache" | "opt-in-local";
|
|
251
|
+
export type V2CatalogSource = "bundled" | "cache" | "opt-in-local" | "remote";
|
|
245
252
|
|
|
246
253
|
export interface LoadedV2Catalog {
|
|
247
254
|
models: ModelEntry[];
|
|
@@ -334,3 +341,123 @@ export function loadCatalogForV2(): LoadedV2Catalog {
|
|
|
334
341
|
|
|
335
342
|
return { models, source, commandCodeVersion, degraded, degradedReason };
|
|
336
343
|
}
|
|
344
|
+
|
|
345
|
+
export const REMOTE_SYNC_TIMEOUT_MS = 15_000;
|
|
346
|
+
export const REMOTE_SYNC_INTERVAL_MS = 6 * 3600_000;
|
|
347
|
+
|
|
348
|
+
export interface RemoteRefreshResult {
|
|
349
|
+
models: ModelEntry[];
|
|
350
|
+
unavailable: string[];
|
|
351
|
+
pendingNew: string[];
|
|
352
|
+
remoteCount: number;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Live availability ids from the Provider API. No auth required. */
|
|
356
|
+
export async function fetchRemoteAvailability(
|
|
357
|
+
timeoutMs = REMOTE_SYNC_TIMEOUT_MS,
|
|
358
|
+
): Promise<string[]> {
|
|
359
|
+
const response = await fetch(MODELS_API_URL, {
|
|
360
|
+
headers: { accept: "application/json" },
|
|
361
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
362
|
+
});
|
|
363
|
+
if (!response.ok) {
|
|
364
|
+
throw new Error(`availability request failed: HTTP ${response.status}`);
|
|
365
|
+
}
|
|
366
|
+
return parseAvailabilityIds(await response.json());
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Reconcile a local catalog with live availability: keep the ids the API
|
|
371
|
+
* still serves, report retired ones, and list remote ids missing locally.
|
|
372
|
+
* Missing ids are *not* published — they carry no cost/limit data until a
|
|
373
|
+
* catalog sync extracts them.
|
|
374
|
+
*/
|
|
375
|
+
export function applyRemoteAvailability(
|
|
376
|
+
base: ModelEntry[],
|
|
377
|
+
remoteIds: string[],
|
|
378
|
+
): RemoteRefreshResult {
|
|
379
|
+
const { retained, unavailable } = filterCatalogByAvailability(base, remoteIds);
|
|
380
|
+
const known = new Set(base.map((m) => m.id));
|
|
381
|
+
const pendingNew = [...new Set(remoteIds)].filter((id) => !known.has(id)).sort();
|
|
382
|
+
return { models: retained, unavailable, pendingNew, remoteCount: remoteIds.length };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export async function refreshCatalogFromRemote(
|
|
386
|
+
base: ModelEntry[],
|
|
387
|
+
timeoutMs = REMOTE_SYNC_TIMEOUT_MS,
|
|
388
|
+
): Promise<RemoteRefreshResult> {
|
|
389
|
+
return applyRemoteAvailability(base, await fetchRemoteAvailability(timeoutMs));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export function isRemoteSyncDisabled(): boolean {
|
|
393
|
+
return loadPluginFileConfig().disableModelSync === true;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Overwrite the last-good disk cache with a refreshed inventory. */
|
|
397
|
+
export function persistRefreshedModels(models: ModelEntry[]): void {
|
|
398
|
+
if (models.length === 0) return;
|
|
399
|
+
try {
|
|
400
|
+
writeCatalogCache(pluginStateDir(), models);
|
|
401
|
+
} catch {
|
|
402
|
+
// ignore cache write
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function writeRemoteRefreshSummary(
|
|
407
|
+
result: RemoteRefreshResult,
|
|
408
|
+
commandCodeVersion: string | null,
|
|
409
|
+
): void {
|
|
410
|
+
const summary: StartupSummary = {
|
|
411
|
+
catalogSource: "remote",
|
|
412
|
+
commandCodeVersion,
|
|
413
|
+
modelCount: result.models.length,
|
|
414
|
+
reasoningModelCount: result.models.filter((m) => m.reasoning).length,
|
|
415
|
+
degraded: result.models.length === 0,
|
|
416
|
+
degradedReason:
|
|
417
|
+
result.models.length === 0 ? "remote availability matched no local models" : null,
|
|
418
|
+
pendingNewCount: result.pendingNew.length,
|
|
419
|
+
};
|
|
420
|
+
try {
|
|
421
|
+
writeStartupSummary(pluginStateDir(), summary);
|
|
422
|
+
} catch {
|
|
423
|
+
// ignore
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const remoteRefreshRunners = new Set<() => Promise<void>>();
|
|
428
|
+
let remoteRefreshTimer: ReturnType<typeof setInterval> | null = null;
|
|
429
|
+
|
|
430
|
+
export function remoteRefreshRunnerCount(): number {
|
|
431
|
+
return remoteRefreshRunners.size;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Share one interval across setups in the process. Returns a detach
|
|
436
|
+
* function; the timer stops after the last runner detaches.
|
|
437
|
+
*/
|
|
438
|
+
export function attachRemoteRefreshRunner(
|
|
439
|
+
run: () => Promise<void>,
|
|
440
|
+
intervalMs = REMOTE_SYNC_INTERVAL_MS,
|
|
441
|
+
): () => void {
|
|
442
|
+
remoteRefreshRunners.add(run);
|
|
443
|
+
if (!remoteRefreshTimer) {
|
|
444
|
+
remoteRefreshTimer = setInterval(() => {
|
|
445
|
+
for (const runner of remoteRefreshRunners) {
|
|
446
|
+
void runner().catch(() => {
|
|
447
|
+
// A failed refresh keeps the last inventory; the next tick retries.
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
}, intervalMs);
|
|
451
|
+
remoteRefreshTimer.unref?.();
|
|
452
|
+
}
|
|
453
|
+
let detached = false;
|
|
454
|
+
return () => {
|
|
455
|
+
if (detached) return;
|
|
456
|
+
detached = true;
|
|
457
|
+
remoteRefreshRunners.delete(run);
|
|
458
|
+
if (remoteRefreshRunners.size === 0 && remoteRefreshTimer) {
|
|
459
|
+
clearInterval(remoteRefreshTimer);
|
|
460
|
+
remoteRefreshTimer = null;
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
}
|
package/src/startup.ts
CHANGED
|
@@ -6,12 +6,14 @@ import type { ModelEntry } from "./catalog.js";
|
|
|
6
6
|
export type { ModelEntry } from "./catalog.js";
|
|
7
7
|
|
|
8
8
|
export type StartupSummary = {
|
|
9
|
-
catalogSource: "bundled" | "cache" | "opt-in-local";
|
|
9
|
+
catalogSource: "bundled" | "cache" | "opt-in-local" | "remote";
|
|
10
10
|
commandCodeVersion: string | null;
|
|
11
11
|
modelCount: number;
|
|
12
12
|
reasoningModelCount: number;
|
|
13
13
|
degraded: boolean;
|
|
14
14
|
degradedReason: string | null;
|
|
15
|
+
/** Remote ids with no local cost data yet — next catalog sync picks them up. */
|
|
16
|
+
pendingNewCount?: number;
|
|
15
17
|
};
|
|
16
18
|
|
|
17
19
|
export function pluginStateDir(): string {
|