opencode-resolve 0.1.13 → 0.1.16

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.
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "node:child_process"
2
2
  import { constants } from "node:fs"
3
- import { access, mkdir, readFile, writeFile } from "node:fs/promises"
3
+ import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises"
4
4
  import { homedir } from "node:os"
5
5
  import { dirname, join, resolve } from "node:path"
6
6
  import { createInterface } from "node:readline/promises"
@@ -9,14 +9,21 @@ import { fileURLToPath } from "node:url"
9
9
  const packageName = "opencode-resolve"
10
10
  const root = resolve(dirname(fileURLToPath(import.meta.url)), "..")
11
11
  const configDir = process.env.OPENCODE_CONFIG_HOME || join(homedir(), ".config", "opencode")
12
+ const cacheDir = process.env.OPENCODE_CACHE_HOME || join(homedir(), ".cache", "opencode")
12
13
  const opencodeConfigPath = join(configDir, "opencode.json")
13
14
  const resolveConfigPath = join(configDir, "resolve.json")
14
15
  const exampleConfigPath = join(root, "opencode-resolve.example.json")
16
+ const selfPluginCachePath = join(cacheDir, "packages", `${packageName}@latest`)
17
+ const selfPluginCachedPackageJson = join(selfPluginCachePath, "node_modules", packageName, "package.json")
15
18
 
16
19
  const ADDITIVE_DEFAULTS = {
17
20
  autoApprove: true,
18
21
  }
19
22
 
23
+ const DEFAULT_ENABLED_AGENTS = ["coder", "resolver", "explorer", "reviewer", "deep-reviewer", "planner"]
24
+ const GPT_ENABLED_AGENTS = ["coder", "resolver", "gpt", "explorer", "reviewer", "deep-reviewer", "planner"]
25
+ const GLM_ENABLED_AGENTS = ["coder", "resolver", "glm", "explorer", "reviewer", "planner"]
26
+
20
27
  // ZAI local MCP server bootstrap for GLM users.
21
28
  // Do not copy provider secrets from OpenCode's auth store into opencode.json.
22
29
  // The MCP process should receive credentials from the user's runtime environment.
@@ -53,16 +60,21 @@ const OPENAI_MODEL_HINTS = [
53
60
 
54
61
  const GLM_MODEL_HINTS = [
55
62
  "zai-coding-plan/glm-5.1",
63
+ "zai-coding-plan/glm-4.5",
64
+ "zai-coding-plan/glm-4.5-air",
56
65
  "zai-coding-plan/glm-5",
57
- "zai-coding-plan/glm-4.7-flash",
58
- "zai-coding-plan/glm-4.5-flash",
66
+ "zai-coding-plan/glm-4.7",
59
67
  "zai/glm-5.1",
68
+ "zai/glm-4.5",
69
+ "zai/glm-4.5-air",
60
70
  "zai/glm-5",
61
- "zai/glm-4.7-flash",
62
71
  "zai/glm-4.7",
72
+ "zai-coding-plan/glm-4.7-flashx",
73
+ "zai/glm-4.7-flashx",
74
+ "zai-coding-plan/glm-4.5-flash",
63
75
  "zai/glm-4.5-flash",
64
- "zai/glm-4.5",
65
- "zai/glm-4.5-air",
76
+ "zai-coding-plan/glm-4.7-flash",
77
+ "zai/glm-4.7-flash",
66
78
  ]
67
79
 
68
80
  if (process.env.OPENCODE_RESOLVE_SKIP_POSTINSTALL === "1") {
@@ -74,6 +86,7 @@ console.log(`[${packageName}] installing v${pluginVersion}`)
74
86
 
75
87
  try {
76
88
  await registerPlugin()
89
+ await refreshSelfPluginCache(pluginVersion)
77
90
  await offerCompanionPlugins()
78
91
  console.log(`[${packageName}] v${pluginVersion} install complete — restart OpenCode to load the plugin`)
79
92
  } catch (error) {
@@ -121,6 +134,68 @@ async function registerPlugin() {
121
134
  await handleExistingResolveConfig(probe, scriptedAnswers)
122
135
  }
123
136
 
137
+ async function refreshSelfPluginCache(expectedVersion) {
138
+ if (process.env.OPENCODE_RESOLVE_SKIP_CACHE_REFRESH === "1") return
139
+ if (process.env.OPENCODE_RESOLVE_REFRESHING_CACHE === "1") return
140
+
141
+ const forceRefresh = readInstallerOption("force_cache_refresh") === "1"
142
+ const cachedVersion = await readCachedSelfVersion()
143
+ if (!forceRefresh && cachedVersion === expectedVersion) {
144
+ console.log(`[${packageName}] OpenCode plugin cache already at v${expectedVersion}`)
145
+ return
146
+ }
147
+
148
+ if (forceRefresh && cachedVersion === expectedVersion) {
149
+ console.log(`[${packageName}] forcing OpenCode plugin cache refresh at v${expectedVersion}`)
150
+ } else if (cachedVersion) {
151
+ console.log(`[${packageName}] stale OpenCode plugin cache detected: v${cachedVersion} -> v${expectedVersion}`)
152
+ } else {
153
+ console.log(`[${packageName}] OpenCode plugin cache missing; refreshing cache`)
154
+ }
155
+
156
+ await rm(selfPluginCachePath, { recursive: true, force: true })
157
+ const refreshed = await runOpenCodePluginInstall()
158
+ if (!refreshed) {
159
+ console.warn(`[${packageName}] could not refresh OpenCode plugin cache automatically`)
160
+ console.warn(`[${packageName}] run manually: opencode plugin ${packageName} --global --force`)
161
+ return
162
+ }
163
+
164
+ const nextVersion = await readCachedSelfVersion()
165
+ if (nextVersion && nextVersion !== expectedVersion) {
166
+ console.warn(`[${packageName}] OpenCode plugin cache refreshed but still reports v${nextVersion}; expected v${expectedVersion}`)
167
+ return
168
+ }
169
+ console.log(`[${packageName}] OpenCode plugin cache refreshed to v${nextVersion ?? expectedVersion}`)
170
+ }
171
+
172
+ async function readCachedSelfVersion() {
173
+ try {
174
+ const raw = await readFile(selfPluginCachedPackageJson, "utf8")
175
+ const parsed = JSON.parse(raw)
176
+ return typeof parsed?.version === "string" ? parsed.version : undefined
177
+ } catch (error) {
178
+ if (isMissingFileError(error)) return undefined
179
+ return undefined
180
+ }
181
+ }
182
+
183
+ async function runOpenCodePluginInstall() {
184
+ return new Promise((resolveSpawn) => {
185
+ const child = spawn("opencode", ["plugin", packageName, "--global", "--force"], {
186
+ stdio: "ignore",
187
+ env: {
188
+ ...process.env,
189
+ OPENCODE_RESOLVE_REFRESHING_CACHE: "1",
190
+ OPENCODE_RESOLVE_SKIP_POSTINSTALL: "1",
191
+ OPENCODE_RESOLVE_SKIP_COMPANIONS: "1",
192
+ },
193
+ })
194
+ child.on("exit", (code) => resolveSpawn(code === 0))
195
+ child.on("error", () => resolveSpawn(false))
196
+ })
197
+ }
198
+
124
199
  function isPluginRegisteredIn(config) {
125
200
  return Array.isArray(config.plugin) && config.plugin.some(isRegisteredPluginEntry)
126
201
  }
@@ -149,8 +224,18 @@ function applyMCPPatches(config, names) {
149
224
  async function handleExistingResolveConfig(opencodeConfig, scriptedAnswers) {
150
225
  const action = await chooseExistingResolveConfigAction(scriptedAnswers)
151
226
  if (action === "fresh") {
227
+ const existing = await readExistingResolveConfig()
152
228
  await backupResolveConfig()
153
- await createAdaptiveResolveConfig(opencodeConfig, scriptedAnswers)
229
+ const preserveModels = readInstallerOption("reset_models") !== "1"
230
+ await createAdaptiveResolveConfig(opencodeConfig, scriptedAnswers, {
231
+ preservedModels: preserveModels ? existing?.models : undefined,
232
+ })
233
+ return
234
+ }
235
+
236
+ if (action === "models") {
237
+ await backupResolveConfig()
238
+ await reconfigureExistingModels(opencodeConfig, scriptedAnswers)
154
239
  return
155
240
  }
156
241
 
@@ -158,18 +243,20 @@ async function handleExistingResolveConfig(opencodeConfig, scriptedAnswers) {
158
243
  }
159
244
 
160
245
  async function chooseExistingResolveConfigAction(scriptedAnswers) {
161
- const requested = (process.env.OPENCODE_RESOLVE_REINSTALL ?? "").trim().toLowerCase()
246
+ if (readInstallerOption("configure_models") === "1") return "models"
247
+ const requested = readInstallerOption("reinstall").trim().toLowerCase()
162
248
  if (["fresh", "reset", "recreate", "new"].includes(requested)) return "fresh"
163
249
  if (["update", "keep", "migrate", "preserve"].includes(requested)) return "update"
164
250
  if (requested) {
165
- console.warn(`[${packageName}] ignoring unknown OPENCODE_RESOLVE_REINSTALL=${JSON.stringify(requested)}; use "fresh" or "update".`)
251
+ console.warn(`[${packageName}] ignoring unknown reinstall mode ${JSON.stringify(requested)}; use "fresh" or "update".`)
166
252
  }
167
253
 
168
- const forcePrompt = process.env.OPENCODE_RESOLVE_FORCE_PROMPT === "1"
254
+ const forcePrompt = readInstallerOption("force_prompt") === "1"
169
255
  const canPrompt = Boolean((process.stdin.isTTY && process.stdout.isTTY) || forcePrompt)
170
256
  if (!canPrompt) {
171
257
  console.log(`[${packageName}] existing ${resolveConfigPath} found; preserving it and applying additive updates.`)
172
- console.log(`[${packageName}] for a fresh reinstall, rerun in a TTY or set OPENCODE_RESOLVE_REINSTALL=fresh.`)
258
+ console.log(`[${packageName}] for model setup, run: ${packageName} setup --models`)
259
+ console.log(`[${packageName}] to force plugin cache reinstall without touching settings, run: ${packageName} setup --force-cache`)
173
260
  return "update"
174
261
  }
175
262
 
@@ -178,14 +265,26 @@ async function chooseExistingResolveConfigAction(scriptedAnswers) {
178
265
  console.log("")
179
266
  console.log(`[${packageName}] Existing resolve config found: ${resolveConfigPath}`)
180
267
  console.log(" 1. update existing config — preserve your settings and add missing defaults")
181
- console.log(" 2. fresh reinstallback up resolve.json and run setup again")
182
- const answer = await askChoice(rl, "Existing config [1=update, 2=fresh reinstall, default 1]: ", ["1", "2"], "1")
183
- return answer === "2" ? "fresh" : "update"
268
+ console.log(" 2. reconfigure modelspreserve the rest of resolve.json")
269
+ console.log(" 3. fresh reinstall back up resolve.json and run setup again, preserving model pins")
270
+ const answer = await askChoice(rl, "Existing config [1=update, 2=models, 3=fresh, default 1]: ", ["1", "2", "3"], "1")
271
+ if (answer === "2") return "models"
272
+ return answer === "3" ? "fresh" : "update"
184
273
  } finally {
185
274
  rl.close()
186
275
  }
187
276
  }
188
277
 
278
+ async function readExistingResolveConfig() {
279
+ try {
280
+ const raw = await readFile(resolveConfigPath, "utf8")
281
+ const parsed = JSON.parse(raw)
282
+ return isObject(parsed) ? parsed : undefined
283
+ } catch {
284
+ return undefined
285
+ }
286
+ }
287
+
189
288
  async function backupResolveConfig() {
190
289
  const raw = await readFile(resolveConfigPath, "utf8")
191
290
  const stamp = new Date().toISOString().replace(/[:.]/g, "-")
@@ -194,15 +293,16 @@ async function backupResolveConfig() {
194
293
  console.log(`[${packageName}] backed up existing resolve config to ${backupPath}`)
195
294
  }
196
295
 
197
- async function createAdaptiveResolveConfig(opencodeConfig, scriptedAnswers) {
296
+ async function createAdaptiveResolveConfig(opencodeConfig, scriptedAnswers, options = {}) {
198
297
  await assertReadable(exampleConfigPath)
199
298
  const raw = await readFile(exampleConfigPath, "utf8")
200
299
  const example = JSON.parse(raw)
300
+ const preservedModels = isObject(options.preservedModels) ? options.preservedModels : undefined
201
301
 
202
302
  const currentModel = detectOpenCodeModel(opencodeConfig)
203
303
  const allModels = detectAllModels(opencodeConfig)
204
304
  const resolveConfig = { ...example }
205
- const forcePrompt = process.env.OPENCODE_RESOLVE_FORCE_PROMPT === "1"
305
+ const forcePrompt = readInstallerOption("force_prompt") === "1"
206
306
  const canPrompt = Boolean(
207
307
  (process.stdin.isTTY && process.stdout.isTTY) || forcePrompt,
208
308
  )
@@ -215,7 +315,7 @@ async function createAdaptiveResolveConfig(opencodeConfig, scriptedAnswers) {
215
315
  if (interactivePreset.tier) resolveConfig.tier = interactivePreset.tier
216
316
  else delete resolveConfig.tier
217
317
  if (interactivePreset.enabled) resolveConfig.enabled = interactivePreset.enabled
218
- resolveConfig.models = interactivePreset.models
318
+ resolveConfig.models = mergePreservedModels(interactivePreset.models, preservedModels)
219
319
  resolveConfig.agents = {
220
320
  ...resolveConfig.agents,
221
321
  ...(interactivePreset.agents ?? {}),
@@ -255,7 +355,9 @@ async function createAdaptiveResolveConfig(opencodeConfig, scriptedAnswers) {
255
355
  }
256
356
 
257
357
  if (preset && Object.keys(preset).length > 0) {
258
- resolveConfig.models = preset
358
+ resolveConfig.models = mergePreservedModels(preset, preservedModels)
359
+ } else if (preservedModels) {
360
+ resolveConfig.models = { ...preservedModels }
259
361
  } else {
260
362
  console.log(`[${packageName}] no GPT/GLM models detected in opencode.json — agents inherit the top-level model`)
261
363
  console.log(`[${packageName}] to configure model pinning, rerun setup in a TTY or edit ${resolveConfigPath}`)
@@ -267,6 +369,59 @@ async function createAdaptiveResolveConfig(opencodeConfig, scriptedAnswers) {
267
369
  console.log(`[${packageName}] created ${resolveConfigPath} (preset: ${label})`)
268
370
  }
269
371
 
372
+ async function reconfigureExistingModels(opencodeConfig, scriptedAnswers) {
373
+ const existing = await readExistingResolveConfig()
374
+ if (!existing) {
375
+ await createAdaptiveResolveConfig(opencodeConfig, scriptedAnswers)
376
+ return
377
+ }
378
+
379
+ const currentModel = detectOpenCodeModel(opencodeConfig)
380
+ const allModels = detectAllModels(opencodeConfig)
381
+ const forcePrompt = readInstallerOption("force_prompt") === "1"
382
+ const canPrompt = Boolean((process.stdin.isTTY && process.stdout.isTTY) || forcePrompt)
383
+ const interactivePreset = canPrompt
384
+ ? await buildInteractivePreset(currentModel, allModels, scriptedAnswers)
385
+ : undefined
386
+ const preset = interactivePreset ?? {
387
+ label: getPresetLabel(currentModel),
388
+ profile: inferProfileFromModels(currentModel, allModels),
389
+ models: buildModelPreset(currentModel, allModels),
390
+ }
391
+
392
+ if (!preset.models || Object.keys(preset.models).length === 0) {
393
+ console.log(`[${packageName}] no GPT/GLM models detected; existing model pins preserved`)
394
+ return
395
+ }
396
+
397
+ const updated = { ...existing }
398
+ updated.profile = preset.profile
399
+ if (preset.tier) updated.tier = preset.tier
400
+ else delete updated.tier
401
+ if (preset.enabled) updated.enabled = preset.enabled
402
+ updated.models = preset.models
403
+ updated.agents = {
404
+ ...(updated.agents ?? {}),
405
+ ...(preset.agents ?? {}),
406
+ }
407
+
408
+ await writeFile(resolveConfigPath, `${JSON.stringify(updated, null, 2)}\n`)
409
+ console.log(`[${packageName}] updated model pins in ${resolveConfigPath} (preset: ${preset.label})`)
410
+ }
411
+
412
+ function mergePreservedModels(generated, preserved) {
413
+ if (!preserved) return generated
414
+ return { ...generated, ...preserved }
415
+ }
416
+
417
+ function inferProfileFromModels(currentModel, allModels) {
418
+ const hasGLM = allModels.some((m) => isGLMModel(m)) || isGLMModel(currentModel)
419
+ const hasGPT = allModels.some((m) => isGPTModel(m)) || isGPTModel(currentModel)
420
+ if (hasGLM && !hasGPT) return "glm"
421
+ if (hasGPT && !hasGLM) return "gpt"
422
+ return "mix"
423
+ }
424
+
270
425
  function detectOpenCodeModel(config) {
271
426
  // Prefer top-level `model` as primary signal
272
427
  if (typeof config.model === "string" && config.model.length > 0) {
@@ -421,6 +576,7 @@ async function buildInteractivePreset(currentModel, allModels, scriptedAnswers)
421
576
  label: "gpt-three-tier",
422
577
  profile: "gpt",
423
578
  tier: "gold",
579
+ enabled: GPT_ENABLED_AGENTS,
424
580
  models: buildGPTThreeTierModels(tiers),
425
581
  agents: { gpt: { enabled: true } },
426
582
  }
@@ -437,6 +593,7 @@ async function buildInteractivePreset(currentModel, allModels, scriptedAnswers)
437
593
  label: "glm-three-tier",
438
594
  profile: "glm",
439
595
  tier: "gold",
596
+ enabled: GLM_ENABLED_AGENTS,
440
597
  models: buildGLMThreeTierModels(tiers),
441
598
  agents: { glm: { enabled: true } },
442
599
  }
@@ -449,6 +606,11 @@ async function buildInteractivePreset(currentModel, allModels, scriptedAnswers)
449
606
  return {
450
607
  label: "mix-three-tier",
451
608
  profile: "mix",
609
+ enabled: unique([
610
+ ...DEFAULT_ENABLED_AGENTS,
611
+ ...(useGPT ? ["gpt"] : []),
612
+ ...(useGLM ? ["glm"] : []),
613
+ ]),
452
614
  models: buildMixedThreeTierModels(gptTiers, glmTiers),
453
615
  agents: {
454
616
  gpt: { enabled: useGPT },
@@ -576,19 +738,40 @@ function collectModelChoices(allModels, predicate, hints, includeFallbackHints =
576
738
  const matchingHints = includeFallbackHints
577
739
  ? hints.filter((model) => providerIds.size === 0 || providerIds.has(model.split("/")[0]) || detected.length < 3)
578
740
  : []
579
- return unique([...detected, ...matchingHints])
741
+ const choices = unique([...detected, ...matchingHints])
742
+ return predicate === isGLMModel ? sortGLMModelChoices(choices) : choices
580
743
  }
581
744
 
582
745
  function chooseThreeTier(models, family, includeFallbackHints = true) {
583
746
  const fallback = family === "glm" ? GLM_MODEL_HINTS : OPENAI_MODEL_HINTS
584
747
  const choices = unique(includeFallbackHints ? [...models, ...fallback] : models)
585
748
  return {
586
- bronze: preferModel(choices, family === "glm" ? ["flash", "air", "mini"] : ["spark", "mini", "4o-mini"], choices[0]),
587
- silver: preferModel(choices, family === "glm" ? ["4.7", "4.5", "5"] : ["codex", "5.3", "5.2"], choices[1] ?? choices[0]),
749
+ bronze: preferModel(choices, family === "glm" ? ["5.1", "4.5", "5"] : ["spark", "mini", "4o-mini"], choices[0]),
750
+ silver: preferModel(choices, family === "glm" ? ["5.1", "4.5", "5"] : ["codex", "5.3", "5.2"], choices[1] ?? choices[0]),
588
751
  gold: preferModel(choices, family === "glm" ? ["5.1", "5", "4.5"] : ["5.5", "5.4", "gpt-5.3-codex"], choices[2] ?? choices[1] ?? choices[0]),
589
752
  }
590
753
  }
591
754
 
755
+ function sortGLMModelChoices(models) {
756
+ return [...models].sort((a, b) => rankGLMModel(a) - rankGLMModel(b))
757
+ }
758
+
759
+ function rankGLMModel(model) {
760
+ const lower = model.toLowerCase()
761
+ if (lower.includes("5.1")) return 0
762
+ if (lower.includes("4.5") && !lower.includes("air") && !lower.includes("flash")) return 1
763
+ if (lower.includes("4.5-airx")) return 2
764
+ if (lower.includes("4.5-air")) return 3
765
+ if (/\bglm-5\b/.test(lower)) return 4
766
+ if (lower.includes("4.7-flashx")) return 5
767
+ if (lower.includes("4.7") && !lower.includes("flash")) return 6
768
+ if (lower.includes("4.6")) return 7
769
+ if (lower.includes("4.5-flash")) return 8
770
+ if (lower.includes("4.7-flash")) return 9
771
+ if (lower.includes("flash")) return 10
772
+ return 20
773
+ }
774
+
592
775
  function preferModel(models, needles, fallback) {
593
776
  return models.find((model) => {
594
777
  const lower = model.toLowerCase()
@@ -609,12 +792,22 @@ async function readAllStdin() {
609
792
  }
610
793
 
611
794
  async function readScriptedAnswersIfNeeded() {
612
- if (process.env.OPENCODE_RESOLVE_FORCE_PROMPT === "1" && !process.stdin.isTTY) {
795
+ if (readInstallerOption("force_prompt") === "1" && !process.stdin.isTTY) {
613
796
  return (await readAllStdin()).split(/\r?\n/)
614
797
  }
615
798
  return undefined
616
799
  }
617
800
 
801
+ function readInstallerOption(name) {
802
+ const normalized = name.toUpperCase().replace(/-/g, "_")
803
+ const npmName = name.toLowerCase().replace(/-/g, "_")
804
+ return (
805
+ process.env[`OPENCODE_RESOLVE_${normalized}`] ??
806
+ process.env[`npm_config_opencode_resolve_${npmName}`] ??
807
+ ""
808
+ )
809
+ }
810
+
618
811
  function getPresetLabel(currentModel) {
619
812
  if (!currentModel) return "inherited"
620
813
  const lower = currentModel.toLowerCase()