@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darthph0enix/commandcode-go-opencode-provider",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "author": "Brent Weatherall",
5
5
  "repository": {
6
6
  "type": "git",
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 { models } = await loadSyncedModels()
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. CLI-bundled catalog (vendor-maintained, refreshed by `cmd update`)
315
- // 2. Command Code's public model endpoint (picks up brand-new models)
316
- // 3. last synced cache, then the bundled snapshot as an offline fallback.
317
- export async function loadSyncedModels(): Promise<SyncResult> {
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 docModels: ModelEntry[] | null = null
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) docModels = parsed
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 cache
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 (docModels?.length) {
339
- models = mergeApiModels(docModels, apiModels ?? [], hints)
340
- source = apiModels?.length ? "cli-catalog+api" : "cli-catalog"
341
- } else if (apiModels?.length) {
342
- models = mergeApiModels([], apiModels, hints)
343
- source = "api"
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
  }