@darthph0enix/commandcode-go-opencode-provider 0.5.1 → 0.6.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/package.json +1 -1
- package/plugin.ts +12 -1
- package/src/model-sync.ts +101 -18
package/package.json
CHANGED
package/plugin.ts
CHANGED
|
@@ -21,7 +21,18 @@ export default async function commandcodePlugin() {
|
|
|
21
21
|
if (!cc.env) cc.env = ["COMMANDCODE_API_KEY"]
|
|
22
22
|
|
|
23
23
|
if (!cc.models) {
|
|
24
|
-
const
|
|
24
|
+
const options = (cc.options as Record<string, unknown> | undefined) ?? {}
|
|
25
|
+
const baseURL = typeof options.baseURL === "string" ? options.baseURL.replace(/\/+$/, "") : undefined
|
|
26
|
+
const catalogUrl =
|
|
27
|
+
typeof options.catalogUrl === "string"
|
|
28
|
+
? options.catalogUrl
|
|
29
|
+
: baseURL
|
|
30
|
+
? `${baseURL}/catalog.json`
|
|
31
|
+
: undefined
|
|
32
|
+
const { models, source } = await loadSyncedModels({ catalogUrl })
|
|
33
|
+
if (process.env.COMMANDCODE_DEBUG) {
|
|
34
|
+
console.warn(`[commandcode] injected ${models.length} models from ${source}`)
|
|
35
|
+
}
|
|
25
36
|
const modelsObj: Record<string, unknown> = {}
|
|
26
37
|
for (const entry of models) {
|
|
27
38
|
const key = toConfigKey(entry.id)
|
package/src/model-sync.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync, realpathSync, renameSync, writeFileSync } from "fs"
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, realpathSync, renameSync, writeFileSync } from "fs"
|
|
2
2
|
import { dirname, join } from "path"
|
|
3
3
|
import { fileURLToPath } from "url"
|
|
4
4
|
import { resolveApiKey } from "./auth.js"
|
|
@@ -55,6 +55,11 @@ function writeAtomic(file: string, data: string) {
|
|
|
55
55
|
// Locate the model catalog bundled with the installed Command Code CLI.
|
|
56
56
|
// The catalog is the vendor-maintained list for the same API endpoint this
|
|
57
57
|
// provider talks to, so it is the most accurate metadata source available.
|
|
58
|
+
function bundledMdIn(root: string): string | null {
|
|
59
|
+
const candidate = join(root, "command-code", "dist", "bundled", "command-code-knowledge", "reference", "models.md")
|
|
60
|
+
return existsSync(candidate) ? candidate : null
|
|
61
|
+
}
|
|
62
|
+
|
|
58
63
|
function findModelsMd(): string | null {
|
|
59
64
|
const explicit = process.env.COMMANDCODE_MODELS_MD
|
|
60
65
|
if (explicit && existsSync(explicit)) return explicit
|
|
@@ -80,6 +85,28 @@ function findModelsMd(): string | null {
|
|
|
80
85
|
}
|
|
81
86
|
}
|
|
82
87
|
}
|
|
88
|
+
|
|
89
|
+
// Common global npm roots — PATH-independent so background services
|
|
90
|
+
// (OpenChamber, launchd/systemd) resolve the catalog even when the npm
|
|
91
|
+
// global bin dir is not exported into their environment.
|
|
92
|
+
const roots: string[] = []
|
|
93
|
+
const home = process.env.HOME
|
|
94
|
+
if (home) {
|
|
95
|
+
for (const base of [join(home, ".local", "share", "nvm"), join(home, ".nvm", "versions", "node"), join(home, ".npm-global", "lib")]) {
|
|
96
|
+
try {
|
|
97
|
+
for (const version of readdirSync(base)) {
|
|
98
|
+
roots.push(join(base, version, "lib", "node_modules"))
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
// directory absent — skip
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
roots.push("/usr/local/lib/node_modules", "/usr/lib/node_modules", "/opt/homebrew/lib/node_modules")
|
|
106
|
+
for (const root of roots) {
|
|
107
|
+
const found = bundledMdIn(root)
|
|
108
|
+
if (found) return found
|
|
109
|
+
}
|
|
83
110
|
return null
|
|
84
111
|
}
|
|
85
112
|
|
|
@@ -247,6 +274,34 @@ export function parseModelsMd(text: string, hints: Map<string, ModelEntry>): Mod
|
|
|
247
274
|
return entries
|
|
248
275
|
}
|
|
249
276
|
|
|
277
|
+
// Server-managed master catalog. The server (keypool) serves the same
|
|
278
|
+
// enriched model list this provider builds locally, so clients fetch it at
|
|
279
|
+
// startup and always see current models/capabilities — no plugin update and
|
|
280
|
+
// no `op pull` needed when the catalog changes.
|
|
281
|
+
export async function fetchRemoteCatalog(url: string, timeoutMs = 1500): Promise<ModelEntry[] | null> {
|
|
282
|
+
const controller = new AbortController()
|
|
283
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
284
|
+
try {
|
|
285
|
+
const response = await fetch(url, { signal: controller.signal, headers: { accept: "application/json" } })
|
|
286
|
+
if (!response.ok) return null
|
|
287
|
+
const payload = (await response.json()) as unknown
|
|
288
|
+
const list = Array.isArray(payload)
|
|
289
|
+
? payload
|
|
290
|
+
: payload && typeof payload === "object" && Array.isArray((payload as { models?: unknown }).models)
|
|
291
|
+
? (payload as { models: unknown[] }).models
|
|
292
|
+
: null
|
|
293
|
+
if (!list) return null
|
|
294
|
+
const entries = list.filter(
|
|
295
|
+
(entry): entry is ModelEntry => typeof entry === "object" && entry !== null && typeof (entry as { id?: unknown }).id === "string",
|
|
296
|
+
)
|
|
297
|
+
return entries.length >= 5 ? entries : null
|
|
298
|
+
} catch {
|
|
299
|
+
return null
|
|
300
|
+
} finally {
|
|
301
|
+
clearTimeout(timer)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
250
305
|
async function fetchApiModels(): Promise<ApiModel[] | null> {
|
|
251
306
|
const apiKey = resolveApiKey({})
|
|
252
307
|
const controller = new AbortController()
|
|
@@ -311,44 +366,68 @@ function mergeApiModels(entries: ModelEntry[], api: ApiModel[], hints?: Map<stri
|
|
|
311
366
|
}
|
|
312
367
|
|
|
313
368
|
// Build the model list fresh on every opencode instance start:
|
|
314
|
-
// 1.
|
|
315
|
-
//
|
|
316
|
-
//
|
|
317
|
-
|
|
369
|
+
// 1. REMOTE master catalog (server-managed; always current — primary source
|
|
370
|
+
// so client devices never depend on a local CLI install for metadata)
|
|
371
|
+
// 2. local CLI-bundled catalog + Command Code's public model endpoint
|
|
372
|
+
// (used to append models newer than the server catalog, and as the
|
|
373
|
+
// offline fallback when the server is unreachable)
|
|
374
|
+
// 3. last synced cache, then the bundled snapshot as a final fallback.
|
|
375
|
+
export async function loadSyncedModels(options?: { catalogUrl?: string }): Promise<SyncResult> {
|
|
318
376
|
const bundled = loadCachedModels(BUNDLED_MODELS)
|
|
319
377
|
const cached = loadCachedModels(CACHE_MODELS)
|
|
320
378
|
const hints = new Map<string, ModelEntry>()
|
|
321
379
|
for (const entry of [...(cached ?? []), ...(bundled ?? [])]) hints.set(entry.id.toLowerCase(), entry)
|
|
322
380
|
|
|
323
|
-
let
|
|
381
|
+
let localModels: ModelEntry[] | null = null
|
|
382
|
+
let localSource = ""
|
|
324
383
|
try {
|
|
325
384
|
const modelsMd = findModelsMd()
|
|
326
385
|
if (modelsMd) {
|
|
327
386
|
const parsed = parseModelsMd(readFileSync(modelsMd, "utf-8"), hints)
|
|
328
|
-
if (parsed.length >= 5)
|
|
387
|
+
if (parsed.length >= 5) {
|
|
388
|
+
const apiModels = await fetchApiModels()
|
|
389
|
+
localModels = mergeApiModels(parsed, apiModels ?? [], hints)
|
|
390
|
+
localSource = apiModels?.length ? "cli-catalog+api" : "cli-catalog"
|
|
391
|
+
}
|
|
329
392
|
}
|
|
330
393
|
} catch {
|
|
331
|
-
// intentionally silent: fall through to the API or
|
|
394
|
+
// intentionally silent: fall through to the API or remote catalog
|
|
395
|
+
}
|
|
396
|
+
if (!localModels?.length) {
|
|
397
|
+
const apiModels = await fetchApiModels()
|
|
398
|
+
if (apiModels?.length) {
|
|
399
|
+
localModels = mergeApiModels([], apiModels, hints)
|
|
400
|
+
localSource = "api"
|
|
401
|
+
}
|
|
332
402
|
}
|
|
333
|
-
|
|
334
|
-
const apiModels = await fetchApiModels()
|
|
335
403
|
|
|
336
404
|
let models: ModelEntry[] | null = null
|
|
337
405
|
let source = ""
|
|
338
|
-
if (
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
406
|
+
if (options?.catalogUrl) {
|
|
407
|
+
const remote = await fetchRemoteCatalog(options.catalogUrl)
|
|
408
|
+
if (remote) {
|
|
409
|
+
models = remote
|
|
410
|
+
source = "remote"
|
|
411
|
+
if (localModels?.length) {
|
|
412
|
+
const seen = new Set(remote.map((entry) => entry.id.toLowerCase()))
|
|
413
|
+
const extra = localModels.filter((entry) => !seen.has(entry.id.toLowerCase()))
|
|
414
|
+
if (extra.length) {
|
|
415
|
+
models = [...remote, ...extra]
|
|
416
|
+
source = "remote+local"
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (!models?.length && localModels?.length) {
|
|
422
|
+
models = localModels
|
|
423
|
+
source = localSource
|
|
344
424
|
}
|
|
345
|
-
|
|
346
425
|
if (!models?.length) {
|
|
347
426
|
models = cached ?? bundled ?? []
|
|
348
427
|
source = cached ? "cache" : "bundled"
|
|
349
428
|
}
|
|
350
429
|
|
|
351
|
-
if (!models.length) throw new Error("Command Code model catalog unavailable (no CLI, API, cache, or bundled list)")
|
|
430
|
+
if (!models.length) throw new Error("Command Code model catalog unavailable (no remote, CLI, API, cache, or bundled list)")
|
|
352
431
|
|
|
353
432
|
if (source !== "cache" && source !== "bundled") {
|
|
354
433
|
try {
|
|
@@ -358,5 +437,9 @@ export async function loadSyncedModels(): Promise<SyncResult> {
|
|
|
358
437
|
}
|
|
359
438
|
}
|
|
360
439
|
|
|
440
|
+
if (process.env.COMMANDCODE_DEBUG) {
|
|
441
|
+
console.warn(`[commandcode] catalog source=${source} models=${models.length}`)
|
|
442
|
+
}
|
|
443
|
+
|
|
361
444
|
return { models, source }
|
|
362
445
|
}
|