opencode-resolve 0.1.20 → 0.3.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.
@@ -20,48 +20,17 @@ const ADDITIVE_DEFAULTS = {
20
20
  autoApprove: true,
21
21
  }
22
22
 
23
- // Heuristic strength scoring for an arbitrary provider/model id.
24
- // Higher = "stronger" / more expensive in expected behavior. Heuristic-only.
25
- const STRENGTH_BOOSTS = [
26
- { re: /\b(mini|flash|nano|lite|haiku|air|small)\b/i, score: -3 },
27
- { re: /\b(claude-3|gpt-3|gpt-4o-mini|o1-mini|o3-mini|4\.5)\b/i, score: -1 },
28
- { re: /\b(gpt-4o|opus|sonnet|gemini-1\.5)\b/i, score: 1 },
29
- { re: /\b(o1|o3|o4|reason|reasoning|pro|max|ultra|gold|sota)\b/i, score: 2 },
30
- { re: /\b(5\.5|5\.4|5\.3|5\.2|5\.1|sonnet-4|opus-4|opus-5|max-5|next|2026)\b/i, score: 2 },
31
- { re: /\b(codex|coder|code|coding)\b/i, score: 1 },
32
- ]
23
+ // Tier classification by name keywords. Returns 0=bronze (cheap scout),
24
+ // 1=silver (workhorse coder), 2=gold (flagship reasoner). Within the same
25
+ // tier we tiebreak by numeric version so 5.1 ranks above 4.5.
26
+ const TIER_BRONZE_RE = /\b(mini|flash|nano|lite|haiku|air|small|gpt-3\.5)\b/i
27
+ const TIER_GOLD_RE = /\b(5\.5|5\.4|opus|o1-pro|o1|o3|o4|max|ultra|sota|reasoning|gpt-4-turbo)\b/i
33
28
 
34
29
  // Provider-neutral agents — safe to enable for everyone (no profile-specific model needed).
35
30
  const DEFAULT_ENABLED_AGENTS = [
36
31
  "coder", "resolver", "explorer", "reviewer", "deep-reviewer", "planner",
37
32
  "architect", "debugger", "researcher",
38
33
  ]
39
- const GPT_ENABLED_AGENTS = [...DEFAULT_ENABLED_AGENTS, "gpt", "gpt-coder", "codex"]
40
- const GLM_ENABLED_AGENTS = [...DEFAULT_ENABLED_AGENTS, "glm"]
41
-
42
- // ZAI local MCP server bootstrap for GLM users.
43
- // Do not copy provider secrets from OpenCode's auth store into opencode.json.
44
- // The MCP process should receive credentials from the user's runtime environment.
45
- const ZAI_MCP_SERVERS = {
46
- "zai-mcp-server": {
47
- type: "local",
48
- command: ["npx", "-y", "@z_ai/mcp-server"],
49
- environment: {
50
- Z_AI_MODE: "ZAI",
51
- },
52
- },
53
- }
54
-
55
- const COMPANION_PLUGINS = [
56
- {
57
- pkg: "@tarquinen/opencode-dcp",
58
- desc: "Dynamic Context Pruning — strips obsolete tool outputs so long resolver loops cost fewer tokens",
59
- },
60
- {
61
- pkg: "@slkiser/opencode-quota",
62
- desc: "Live token/quota usage indicator — supports GLM coding-plan, OpenAI Plus/Pro, Qwen, and more",
63
- },
64
- ]
65
34
 
66
35
  const OPENAI_MODEL_HINTS = [
67
36
  "openai/gpt-5.5",
@@ -74,11 +43,13 @@ const OPENAI_MODEL_HINTS = [
74
43
  ]
75
44
 
76
45
  const GLM_MODEL_HINTS = [
46
+ "zai-coding-plan/glm-5.2",
77
47
  "zai-coding-plan/glm-5.1",
78
48
  "zai-coding-plan/glm-4.5",
79
49
  "zai-coding-plan/glm-4.5-air",
80
50
  "zai-coding-plan/glm-5",
81
51
  "zai-coding-plan/glm-4.7",
52
+ "zai/glm-5.2",
82
53
  "zai/glm-5.1",
83
54
  "zai/glm-4.5",
84
55
  "zai/glm-4.5-air",
@@ -105,7 +76,6 @@ async function printSummaryBanner(version) {
105
76
  const raw = await readFile(resolveConfigPath, "utf8")
106
77
  const cfg = JSON.parse(raw)
107
78
  const parts = []
108
- if (cfg.profile) parts.push(`profile=${cfg.profile}`)
109
79
  if (cfg.tier) parts.push(`tier=${cfg.tier}`)
110
80
  const enabled = Array.isArray(cfg.enabled) ? cfg.enabled.length : Object.keys(cfg.agents ?? {}).length
111
81
  if (enabled) parts.push(`${enabled} agents`)
@@ -131,7 +101,6 @@ async function printSummaryBanner(version) {
131
101
  try {
132
102
  await registerPlugin()
133
103
  await refreshSelfPluginCache(pluginVersion)
134
- await offerCompanionPlugins()
135
104
  console.log(`[${packageName}] v${pluginVersion} install complete — restart OpenCode to load the plugin`)
136
105
  await printSummaryBanner(pluginVersion)
137
106
  } catch (error) {
@@ -154,17 +123,11 @@ async function registerPlugin() {
154
123
  const scriptedAnswers = await readScriptedAnswersIfNeeded()
155
124
 
156
125
  const probe = await readOpenCodeConfig()
157
- const allModels = detectAllModels(probe)
158
- const hasGLM = allModels.some((m) => isGLMModel(m))
159
126
  const pluginNeeded = !isPluginRegisteredIn(probe)
160
- const missingMCPNames = hasGLM
161
- ? Object.keys(ZAI_MCP_SERVERS).filter((name) => probe.mcp?.[name] === undefined)
162
- : []
163
127
 
164
- if (pluginNeeded || missingMCPNames.length > 0) {
128
+ if (pluginNeeded) {
165
129
  const fresh = await readOpenCodeConfig()
166
- if (pluginNeeded) applyPluginPatch(fresh)
167
- if (missingMCPNames.length > 0) applyMCPPatches(fresh, missingMCPNames)
130
+ applyPluginPatch(fresh)
168
131
  await writeFile(opencodeConfigPath, `${JSON.stringify(fresh, null, 2)}\n`)
169
132
  console.log(`[${packageName}] updated ${opencodeConfigPath}`)
170
133
  } else {
@@ -233,7 +196,6 @@ async function runOpenCodePluginInstall() {
233
196
  ...process.env,
234
197
  OPENCODE_RESOLVE_REFRESHING_CACHE: "1",
235
198
  OPENCODE_RESOLVE_SKIP_POSTINSTALL: "1",
236
- OPENCODE_RESOLVE_SKIP_COMPANIONS: "1",
237
199
  },
238
200
  })
239
201
  child.on("exit", (code) => resolveSpawn(code === 0))
@@ -255,25 +217,13 @@ function applyPluginPatch(config) {
255
217
  }
256
218
  }
257
219
 
258
- function applyMCPPatches(config, names) {
259
- config.mcp ??= {}
260
- for (const name of names) {
261
- if (config.mcp[name] === undefined) {
262
- config.mcp[name] = ZAI_MCP_SERVERS[name]
263
- }
264
- }
265
- console.log(`[${packageName}] injected ZAI MCP server config: ${names.join(", ")}`)
266
- console.log(`[${packageName}] note: API keys are not copied into opencode.json; export Z_AI_API_KEY if the MCP server requires it.`)
267
- }
268
-
269
220
  async function handleExistingResolveConfig(opencodeConfig, scriptedAnswers) {
270
221
  const action = await chooseExistingResolveConfigAction(scriptedAnswers)
271
222
  if (action === "fresh") {
272
- const existing = await readExistingResolveConfig()
273
223
  await backupResolveConfig()
274
- const preserveModels = readInstallerOption("reset_models") !== "1"
224
+ console.log(`[${packageName}] resetting resolve.json — running setup from scratch (model pins wiped)`)
275
225
  await createAdaptiveResolveConfig(opencodeConfig, scriptedAnswers, {
276
- preservedModels: preserveModels ? existing?.models : undefined,
226
+ preservedModels: undefined,
277
227
  })
278
228
  return
279
229
  }
@@ -290,7 +240,7 @@ async function handleExistingResolveConfig(opencodeConfig, scriptedAnswers) {
290
240
  async function chooseExistingResolveConfigAction(scriptedAnswers) {
291
241
  if (readInstallerOption("configure_models") === "1") return "models"
292
242
  const requested = readInstallerOption("reinstall").trim().toLowerCase()
293
- if (["fresh", "reset", "recreate", "new"].includes(requested)) return "fresh"
243
+ if (["fresh", "reset", "recreate", "new", "nuke", "wipe", "full-reset", "scratch"].includes(requested)) return "fresh"
294
244
  if (["update", "keep", "migrate", "preserve"].includes(requested)) return "update"
295
245
  if (requested) {
296
246
  console.warn(`[${packageName}] ignoring unknown reinstall mode ${JSON.stringify(requested)}; use "fresh" or "update".`)
@@ -301,6 +251,7 @@ async function chooseExistingResolveConfigAction(scriptedAnswers) {
301
251
  if (!canPrompt) {
302
252
  console.log(`[${packageName}] existing ${resolveConfigPath} found; preserving it and applying additive updates.`)
303
253
  console.log(`[${packageName}] for model setup, run: ${packageName} setup --models`)
254
+ console.log(`[${packageName}] for a full reset (wipes the whole file incl. model pins), run: ${packageName} setup --reset`)
304
255
  console.log(`[${packageName}] to force plugin cache reinstall without touching settings, run: ${packageName} setup --force-cache`)
305
256
  return "update"
306
257
  }
@@ -316,10 +267,11 @@ async function chooseExistingResolveConfigAction(scriptedAnswers) {
316
267
  console.log("How do you want to handle it?")
317
268
  console.log(" 1. keep — preserve existing settings, only add missing defaults (recommended)")
318
269
  console.log(" 2. models — re-pick models only, keep the rest")
319
- console.log(" 3. reset — back up the current file and run setup from scratch (model pins preserved)")
270
+ console.log(" 3. reset — back up the file and wipe EVERYTHING (model pins included)")
320
271
  const answer = await askChoice(rl, "Choice [1=keep, 2=models, 3=reset, default 1]: ", ["1", "2", "3"], "1")
321
272
  if (answer === "2") return "models"
322
- return answer === "3" ? "fresh" : "update"
273
+ if (answer === "3") return "fresh"
274
+ return "update"
323
275
  } finally {
324
276
  rl.close()
325
277
  }
@@ -361,7 +313,6 @@ async function createAdaptiveResolveConfig(opencodeConfig, scriptedAnswers, opti
361
313
  : undefined
362
314
 
363
315
  if (interactivePreset) {
364
- resolveConfig.profile = interactivePreset.profile
365
316
  if (interactivePreset.tier) resolveConfig.tier = interactivePreset.tier
366
317
  else delete resolveConfig.tier
367
318
  if (interactivePreset.enabled) resolveConfig.enabled = interactivePreset.enabled
@@ -375,42 +326,17 @@ async function createAdaptiveResolveConfig(opencodeConfig, scriptedAnswers, opti
375
326
  return
376
327
  }
377
328
 
378
- const hasGLM = allModels.some((m) => isGLMModel(m))
379
- const hasGPT = allModels.some((m) => isGPTModel(m))
329
+ // Non-interactive: agents inherit the top-level OpenCode model. No model
330
+ // pinning unless the user runs setup in a TTY and picks one.
380
331
  const preset = buildModelPreset(currentModel, allModels)
381
332
 
382
- if (hasGLM && !hasGPT) {
383
- resolveConfig.profile = "glm"
384
- resolveConfig.tier = "silver"
385
- resolveConfig.agents = {
386
- ...resolveConfig.agents,
387
- glm: { ...(resolveConfig.agents?.glm ?? {}), enabled: true },
388
- }
389
- } else if (hasGPT && !hasGLM) {
390
- resolveConfig.profile = "gpt"
391
- resolveConfig.tier = "gold"
392
- resolveConfig.agents = {
393
- ...resolveConfig.agents,
394
- gpt: { ...(resolveConfig.agents?.gpt ?? {}), enabled: true },
395
- }
396
- } else {
397
- resolveConfig.profile = "mix"
398
- if (hasGLM && hasGPT) {
399
- resolveConfig.agents = {
400
- ...resolveConfig.agents,
401
- gpt: { ...(resolveConfig.agents?.gpt ?? {}), enabled: true },
402
- glm: { ...(resolveConfig.agents?.glm ?? {}), enabled: true },
403
- }
404
- }
405
- }
406
-
407
333
  if (preset && Object.keys(preset).length > 0) {
408
334
  resolveConfig.models = mergePreservedModels(preset, preservedModels)
409
335
  } else if (preservedModels) {
410
336
  resolveConfig.models = { ...preservedModels }
411
337
  } else {
412
338
  const providerHint = currentModel ? ` (top-level model: ${currentModel})` : ""
413
- console.log(`[${packageName}] no GPT/GLM models detected in opencode.json — agents inherit the top-level model${providerHint}`)
339
+ console.log(`[${packageName}] no models detected in opencode.json — agents inherit the top-level model${providerHint}`)
414
340
  console.log(`[${packageName}] to pin role-specific models, edit ${resolveConfigPath} ("models" section)`)
415
341
  console.log(`[${packageName}] or rerun setup in a TTY: ${packageName} setup --models`)
416
342
  }
@@ -437,17 +363,15 @@ async function reconfigureExistingModels(opencodeConfig, scriptedAnswers) {
437
363
  : undefined
438
364
  const preset = interactivePreset ?? {
439
365
  label: getPresetLabel(currentModel),
440
- profile: inferProfileFromModels(currentModel, allModels),
441
366
  models: buildModelPreset(currentModel, allModels),
442
367
  }
443
368
 
444
369
  if (!preset.models || Object.keys(preset.models).length === 0) {
445
- console.log(`[${packageName}] no GPT/GLM models detected; existing model pins preserved`)
370
+ console.log(`[${packageName}] no models detected; existing model pins preserved`)
446
371
  return
447
372
  }
448
373
 
449
374
  const updated = { ...existing }
450
- updated.profile = preset.profile
451
375
  if (preset.tier) updated.tier = preset.tier
452
376
  else delete updated.tier
453
377
  if (preset.enabled) updated.enabled = preset.enabled
@@ -466,12 +390,28 @@ function mergePreservedModels(generated, preserved) {
466
390
  return { ...generated, ...preserved }
467
391
  }
468
392
 
469
- function inferProfileFromModels(currentModel, allModels) {
470
- const hasGLM = allModels.some((m) => isGLMModel(m)) || isGLMModel(currentModel)
471
- const hasGPT = allModels.some((m) => isGPTModel(m)) || isGPTModel(currentModel)
472
- if (hasGLM && !hasGPT) return "glm"
473
- if (hasGPT && !hasGLM) return "gpt"
474
- return "mix"
393
+ function inferModelStrengthHint(currentModel, allModels = []) {
394
+ // Non-interactive fallback: pin a single detected model to every role so
395
+ // agents don't fall through to an unrelated default. Returns {} when nothing
396
+ // is detectable (agents then inherit the top-level OpenCode model).
397
+ if (!currentModel) return {}
398
+ return {
399
+ bronze: currentModel,
400
+ silver: currentModel,
401
+ gold: currentModel,
402
+ fast: currentModel,
403
+ strong: currentModel,
404
+ mini: currentModel,
405
+ explorer: "bronze",
406
+ coder: "gold",
407
+ resolver: "gold",
408
+ reviewer: "gold",
409
+ "deep-reviewer": "gold",
410
+ planner: "gold",
411
+ architect: "gold",
412
+ debugger: "gold",
413
+ researcher: "bronze",
414
+ }
475
415
  }
476
416
 
477
417
  function detectOpenCodeModel(config) {
@@ -502,195 +442,16 @@ function detectOpenCodeModel(config) {
502
442
  }
503
443
 
504
444
  function buildModelPreset(currentModel, allModels = []) {
505
- const detectedGLMModels = allModels.filter(isGLMModel)
506
- const detectedGPTModels = allModels.filter(isGPTModel)
507
- const glmModels = detectedGLMModels.length > 0 ? collectModelChoices(detectedGLMModels, isGLMModel, GLM_MODEL_HINTS, false) : []
508
- const gptModels = detectedGPTModels.length > 0 ? collectModelChoices(detectedGPTModels, isGPTModel, OPENAI_MODEL_HINTS, false) : []
509
- const glmModel = glmModels[0]
510
- const gptModel = gptModels[0]
511
-
512
- if (glmModel && gptModel) {
513
- const glmTiers = chooseThreeTier(glmModels, "glm", false)
514
- const gptTiers = chooseThreeTier(gptModels, "gpt", false)
515
- return {
516
- mix: "gpt",
517
- gpt: gptTiers.gold,
518
- bronze: glmTiers.bronze,
519
- silver: glmTiers.silver,
520
- gold: gptTiers.gold,
521
- "glm-bronze": glmTiers.bronze,
522
- "glm-silver": glmTiers.silver,
523
- "glm-gold": glmTiers.gold,
524
- "gpt-bronze": gptTiers.bronze,
525
- "gpt-silver": gptTiers.silver,
526
- "gpt-gold": gptTiers.gold,
527
- fast: "bronze",
528
- strong: "gold",
529
- mini: "bronze",
530
- codex: "gpt-gold",
531
- glm: glmTiers.gold,
532
- explorer: "bronze",
533
- coder: "silver",
534
- resolver: "gold",
535
- reviewer: "gold",
536
- "deep-reviewer": "gold",
537
- planner: "gold",
538
- }
539
- }
540
-
541
- if (!currentModel) {
542
- return {}
543
- }
544
-
545
- const lower = currentModel.toLowerCase()
546
-
547
- // GLM / ZAI — GLM-only preset (no GPT dependency, avoids token-exhaustion errors)
548
- if (lower.includes("glm") || lower.includes("zai")) {
549
- return buildGLMOnlyPreset(currentModel, glmModels)
550
- }
551
-
552
- if (lower.includes("openai/") || lower.includes("gpt")) {
553
- return buildGPTOnlyPreset(currentModel, gptModels)
554
- }
555
-
556
- // OpenAI / GPT single-provider preset
557
- if (lower.includes("openai/") || lower.includes("gpt")) {
558
- return buildGPTOnlyPreset(currentModel)
559
- }
560
-
561
- // Unknown provider — keep model-neutral
562
- return {}
563
- }
564
-
565
- function buildGLMOnlyPreset(model, glmModels) {
566
- const models = glmModels && glmModels.length > 0 ? glmModels : [model]
567
- const tiers = chooseThreeTier(models, "glm", false)
568
- return {
569
- glm: tiers.gold,
570
- bronze: tiers.bronze,
571
- silver: tiers.silver,
572
- gold: tiers.gold,
573
- fast: "bronze",
574
- strong: "gold",
575
- mini: "bronze",
576
- coder: "gold",
577
- resolver: "gold",
578
- reviewer: "gold",
579
- "deep-reviewer": "gold",
580
- explorer: "bronze",
581
- planner: "gold",
582
- }
583
- }
584
-
585
- function buildGPTOnlyPreset(model, gptModels) {
586
- const models = gptModels && gptModels.length > 0 ? gptModels : [model]
587
- const tiers = chooseThreeTier(models, "gpt", false)
588
- return {
589
- gpt: tiers.gold,
590
- bronze: tiers.bronze,
591
- silver: tiers.silver,
592
- gold: tiers.gold,
593
- fast: "bronze",
594
- strong: "gold",
595
- mini: "bronze",
596
- codex: "gold",
597
- coder: "silver",
598
- resolver: "gold",
599
- explorer: "bronze",
600
- reviewer: "gold",
601
- "deep-reviewer": "gold",
602
- }
445
+ // Non-interactive path: pin a single detected model across all roles.
446
+ // The interactive TTY flow (buildGenericInteractivePreset) handles tier
447
+ // splitting; this is just the headless fallback.
448
+ return inferModelStrengthHint(currentModel, allModels)
603
449
  }
604
450
 
605
451
  async function buildInteractivePreset(currentModel, allModels, scriptedAnswers, opencodeConfig) {
606
- const choices = {
607
- gpt: collectModelChoices(allModels, isGPTModel, OPENAI_MODEL_HINTS),
608
- glm: collectModelChoices(allModels, isGLMModel, GLM_MODEL_HINTS),
609
- }
610
- if (currentModel) {
611
- if (isGPTModel(currentModel)) choices.gpt = unique([currentModel, ...choices.gpt])
612
- if (isGLMModel(currentModel)) choices.glm = unique([currentModel, ...choices.glm])
613
- }
614
-
615
- const hasGPTOrGLM = allModels.some((m) => isGPTModel(m) || isGLMModel(m))
616
- const providerCount = opencodeConfig ? detectProvidersFromConfig(opencodeConfig).length : 0
617
- const offerAuto = !hasGPTOrGLM && providerCount > 0
618
- const defaultChoice = offerAuto ? "4" : "1"
619
-
620
- const rl = createPromptInterface(scriptedAnswers)
621
- try {
622
- console.log("")
623
- console.log("──────────────────────────────────────────────────────────────")
624
- console.log(` opencode-resolve setup`)
625
- console.log(` Press enter at any prompt to accept the default in [brackets].`)
626
- console.log("──────────────────────────────────────────────────────────────")
627
- console.log("")
628
- console.log(`[${packageName}] Step 1/2 — Choose resolve profile:`)
629
- console.log(` 1. mix — neutral resolver plus optional Codex and GLM primary agents${offerAuto ? "" : " (recommended)"}`)
630
- console.log(" 2. gpt — GPT/Codex-only, three-tier")
631
- console.log(" 3. glm — GLM-only, three-tier")
632
- if (offerAuto) {
633
- console.log(` 4. auto — provider-agnostic tier setup from detected providers (recommended)`)
634
- }
635
- const validChoices = offerAuto ? ["1", "2", "3", "4"] : ["1", "2", "3"]
636
- const profileAnswer = await askChoice(rl, `Profile [${validChoices.join(",")}, default ${defaultChoice}]: `, validChoices, defaultChoice)
637
-
638
- if (profileAnswer === "4" && offerAuto) {
639
- rl.close()
640
- const generic = await buildGenericInteractivePreset(currentModel, opencodeConfig, scriptedAnswers)
641
- if (generic) return generic
642
- // fall through to mix if generic returned nothing
643
- }
644
-
645
- const profile = profileAnswer === "2" ? "gpt" : profileAnswer === "3" ? "glm" : "mix"
646
-
647
- if (profile === "gpt") {
648
- const tiers = await askThreeTier(rl, "GPT/Codex", choices.gpt)
649
- return {
650
- label: "gpt-three-tier",
651
- profile: "gpt",
652
- tier: "gold",
653
- enabled: GPT_ENABLED_AGENTS,
654
- models: buildGPTThreeTierModels(tiers),
655
- agents: { gpt: { enabled: true } },
656
- }
657
- }
658
-
659
- if (profile === "glm") {
660
- // zai and zai-coding-plan are distinct providers — show whatever the user actually
661
- // has configured. No rewriting between them.
662
- const tiers = await askThreeTier(rl, "GLM", choices.glm)
663
- return {
664
- label: "glm-three-tier",
665
- profile: "glm",
666
- tier: "gold",
667
- enabled: GLM_ENABLED_AGENTS,
668
- models: buildGLMThreeTierModels(tiers),
669
- agents: { glm: { enabled: true } },
670
- }
671
- }
672
-
673
- const useGPT = await askYesNo(rl, "Enable dedicated GPT primary agent too? [Y/n]: ", true)
674
- const useGLM = await askYesNo(rl, "Enable dedicated GLM primary agent too? [Y/n]: ", true)
675
- const gptTiers = await askThreeTier(rl, "GPT", choices.gpt)
676
- const glmTiers = await askThreeTier(rl, "GLM", choices.glm)
677
- return {
678
- label: "mix-three-tier",
679
- profile: "mix",
680
- enabled: unique([
681
- ...DEFAULT_ENABLED_AGENTS,
682
- ...(useGPT ? ["gpt"] : []),
683
- ...(useGLM ? ["glm"] : []),
684
- ]),
685
- models: buildMixedThreeTierModels(gptTiers, glmTiers),
686
- agents: {
687
- gpt: { enabled: useGPT },
688
- glm: { enabled: useGLM },
689
- },
690
- }
691
- } finally {
692
- rl.close()
693
- }
452
+ // Single flow: detect providers from opencode.json, pick which API/model to
453
+ // pin, done. No profile selection — model-agnostic by design.
454
+ return buildGenericInteractivePreset(currentModel, opencodeConfig, scriptedAnswers)
694
455
  }
695
456
 
696
457
  function createPromptInterface(scriptedAnswers) {
@@ -712,8 +473,15 @@ async function askThreeTier(rl, label, models) {
712
473
  const defaults = chooseThreeTier(choices, label.toLowerCase().includes("glm") ? "glm" : "gpt")
713
474
  while (true) {
714
475
  console.log("")
715
- console.log(`[${packageName}] ${label} model choices:`)
716
- choices.forEach((model, index) => console.log(` ${index + 1}. ${model}`))
476
+ console.log(`[${packageName}] ${label} model choices (★ = recommended default for that tier):`)
477
+ choices.forEach((model, index) => {
478
+ const tags = []
479
+ if (model === defaults.bronze) tags.push("★ bronze")
480
+ if (model === defaults.silver) tags.push("★ silver")
481
+ if (model === defaults.gold) tags.push("★ gold")
482
+ const suffix = tags.length > 0 ? ` ${tags.join(" / ")}` : ""
483
+ console.log(` ${index + 1}. ${model}${suffix}`)
484
+ })
717
485
  const bronze = await askModel(rl, choices, `Pick ${label} bronze/scout [default ${defaults.bronze}]: `, defaults.bronze)
718
486
  const silver = await askModel(rl, choices, `Pick ${label} silver/coder [default ${defaults.silver}]: `, defaults.silver)
719
487
  const gold = await askModel(rl, choices, `Pick ${label} gold/reasoner [default ${defaults.gold}]: `, defaults.gold)
@@ -747,70 +515,6 @@ async function askYesNo(rl, question, defaultValue) {
747
515
  return answer === "y" || answer === "yes"
748
516
  }
749
517
 
750
- function buildMixedThreeTierModels(gptTiers, glmTiers) {
751
- return {
752
- mix: "gpt-gold",
753
- gpt: "gpt-gold",
754
- glm: "glm-gold",
755
- bronze: "glm-bronze",
756
- silver: "glm-silver",
757
- gold: "gpt-gold",
758
- "gpt-bronze": gptTiers.bronze,
759
- "gpt-silver": gptTiers.silver,
760
- "gpt-gold": gptTiers.gold,
761
- "glm-bronze": glmTiers.bronze,
762
- "glm-silver": glmTiers.silver,
763
- "glm-gold": glmTiers.gold,
764
- fast: "bronze",
765
- strong: "gold",
766
- mini: "bronze",
767
- codex: "gpt-gold",
768
- explorer: "bronze",
769
- coder: "silver",
770
- resolver: "gold",
771
- reviewer: "gold",
772
- "deep-reviewer": "gold",
773
- planner: "gold",
774
- }
775
- }
776
-
777
- function buildGPTThreeTierModels(tiers) {
778
- return {
779
- gpt: "gold",
780
- bronze: tiers.bronze,
781
- silver: tiers.silver,
782
- gold: tiers.gold,
783
- fast: "bronze",
784
- strong: "gold",
785
- mini: "bronze",
786
- codex: "gold",
787
- explorer: "bronze",
788
- coder: "silver",
789
- resolver: "gold",
790
- reviewer: "gold",
791
- "deep-reviewer": "gold",
792
- planner: "gold",
793
- }
794
- }
795
-
796
- function buildGLMThreeTierModels(tiers) {
797
- return {
798
- glm: "gold",
799
- bronze: tiers.bronze,
800
- silver: tiers.silver,
801
- gold: tiers.gold,
802
- fast: "bronze",
803
- strong: "gold",
804
- mini: "bronze",
805
- explorer: "bronze",
806
- coder: "gold",
807
- resolver: "gold",
808
- reviewer: "gold",
809
- "deep-reviewer": "gold",
810
- planner: "gold",
811
- }
812
- }
813
-
814
518
  function collectModelChoices(allModels, predicate, hints, includeFallbackHints = true) {
815
519
  const detected = allModels.filter(predicate)
816
520
  if (detected.length > 0) {
@@ -831,12 +535,28 @@ function chooseThreeTier(models, family) {
831
535
  const fallback = family === "glm" ? GLM_MODEL_HINTS : OPENAI_MODEL_HINTS
832
536
  return chooseThreeTier(fallback, family)
833
537
  }
834
- // Sort by inferred strength (weak strong) and pick bronze/silver/gold by position.
538
+ // Bucket by tier, then pick the STRONGEST within each bucket so defaults are
539
+ // the newest model of that class (e.g. gpt-5-mini beats gpt-4o-mini for bronze).
835
540
  const sorted = sortModelsByStrength(choices)
541
+ return pickFromBuckets(sorted)
542
+ }
543
+
544
+ function pickFromBuckets(sorted) {
545
+ // bronze = cheapest scout (WEAKEST of bronze bucket — e.g. gpt-4o-mini over gpt-5-mini)
546
+ // silver = strongest workhorse coder (STRONGEST of silver bucket — e.g. gpt-5.3-codex)
547
+ // gold = flagship reasoner (STRONGEST of gold bucket — e.g. gpt-5.5)
548
+ const buckets = [[], [], []]
549
+ for (const m of sorted) buckets[inferModelTier(m)].push(m)
550
+ const weakestOf = (bucket) => bucket.length > 0 ? bucket[0] : null
551
+ const strongestOf = (bucket) => bucket.length > 0 ? bucket[bucket.length - 1] : null
552
+ const n = sorted.length
553
+ const fallbackBronze = sorted[0]
554
+ const fallbackGold = sorted[n - 1]
555
+ const fallbackSilver = n >= 2 ? sorted[Math.floor(n / 2)] : sorted[0]
836
556
  return {
837
- bronze: sorted[0],
838
- silver: sorted[Math.min(1, sorted.length - 1)] ?? sorted[0],
839
- gold: sorted[sorted.length - 1],
557
+ bronze: weakestOf(buckets[0]) ?? fallbackBronze,
558
+ silver: strongestOf(buckets[1]) ?? fallbackSilver,
559
+ gold: strongestOf(buckets[2]) ?? fallbackGold,
840
560
  }
841
561
  }
842
562
 
@@ -846,6 +566,7 @@ function sortGLMModelChoices(models) {
846
566
 
847
567
  function rankGLMModel(model) {
848
568
  const lower = model.toLowerCase()
569
+ if (lower.includes("5.2")) return -1
849
570
  if (lower.includes("5.1")) return 0
850
571
  if (lower.includes("4.5") && !lower.includes("air") && !lower.includes("flash")) return 1
851
572
  if (lower.includes("4.5-airx")) return 2
@@ -897,24 +618,35 @@ function readInstallerOption(name) {
897
618
  }
898
619
 
899
620
  function getPresetLabel(currentModel) {
900
- if (!currentModel) return "inherited"
901
- const lower = currentModel.toLowerCase()
902
- if (lower.includes("glm") || lower.includes("zai")) return "glm-only"
903
- if (lower.includes("openai/") || lower.includes("gpt")) return "gpt-only"
904
- return "inherited"
621
+ return currentModel ? "configured" : "inherited"
905
622
  }
906
623
 
907
- function inferModelStrength(modelId) {
624
+ function inferModelTier(modelId) {
625
+ if (typeof modelId !== "string") return 1
626
+ if (TIER_BRONZE_RE.test(modelId)) return 0
627
+ if (TIER_GOLD_RE.test(modelId)) return 2
628
+ return 1
629
+ }
630
+
631
+ function extractModelVersion(modelId) {
908
632
  if (typeof modelId !== "string") return 0
909
- let score = 0
910
- for (const { re, score: s } of STRENGTH_BOOSTS) {
911
- if (re.test(modelId)) score += s
912
- }
913
- return score
633
+ const match = modelId.match(/\b(\d+(?:\.\d+)?)\b/)
634
+ return match ? Number.parseFloat(match[1]) : 0
635
+ }
636
+
637
+ function inferModelStrength(modelId) {
638
+ // Composite ordering: tier first, then version. Used for sortModelsByStrength.
639
+ return inferModelTier(modelId) * 100 + extractModelVersion(modelId)
914
640
  }
915
641
 
916
642
  function sortModelsByStrength(models) {
917
- return [...models].sort((a, b) => inferModelStrength(a) - inferModelStrength(b))
643
+ return [...models].sort((a, b) => {
644
+ const diff = inferModelStrength(a) - inferModelStrength(b)
645
+ if (diff !== 0) return diff
646
+ // Tie on strength: keep the canonical/shorter variant last so bucket "strongest"
647
+ // picks e.g. gpt-5.3-codex over gpt-5.3-codex-spark.
648
+ return b.length - a.length
649
+ })
918
650
  }
919
651
 
920
652
  function detectProvidersFromConfig(config) {
@@ -1057,23 +789,41 @@ async function buildGenericInteractivePreset(currentModel, opencodeConfig, scrip
1057
789
  const shapeAns = await askChoice(rl, `Tier shape [1,2,3, default ${shapeDefault}]: `, ["1", "2", "3"], shapeDefault)
1058
790
  const shape = shapeAns === "3" ? "three" : shapeAns === "2" ? "two" : "single"
1059
791
 
1060
- // 3. Pick models
1061
- const weakest = sorted[0]
1062
- const strongest = sorted[sorted.length - 1]
1063
- const middle = sorted[Math.floor((sorted.length - 1) / 2)]
792
+ // 3. Pick models — use the same bucket-aware picks as askThreeTier so the
793
+ // generic flow defaults line up with the GPT/GLM tier shortcuts.
794
+ const tierPicks = pickFromBuckets(sorted)
795
+ const n = sorted.length
796
+ const strongest = sorted[n - 1]
797
+ const bronzeDefault = tierPicks.bronze
798
+ const silverDefault = tierPicks.silver
799
+ const goldDefault = tierPicks.gold
1064
800
  console.log("")
1065
- console.log(`[${packageName}] Step 3/3 — Pick models (defaults inferred from name strength):`)
1066
- sorted.forEach((m, i) => console.log(` ${i + 1}. ${m}`))
801
+ console.log(`[${packageName}] Step 3/3 — Pick models ( = recommended default for that tier):`)
802
+ sorted.forEach((m, i) => {
803
+ const tags = []
804
+ if (shape === "three") {
805
+ if (m === bronzeDefault) tags.push("★ bronze")
806
+ if (m === silverDefault) tags.push("★ silver")
807
+ if (m === goldDefault) tags.push("★ gold")
808
+ } else if (shape === "two") {
809
+ if (m === bronzeDefault) tags.push("★ silver")
810
+ if (m === goldDefault) tags.push("★ gold")
811
+ } else if (m === strongest) {
812
+ tags.push("★ all roles")
813
+ }
814
+ const suffix = tags.length > 0 ? ` ${tags.join(" / ")}` : ""
815
+ console.log(` ${i + 1}. ${m}${suffix}`)
816
+ })
1067
817
 
1068
818
  while (true) {
1069
819
  const tiers = { shape }
1070
820
  if (shape === "three") {
1071
- tiers.bronze = await askModel(rl, sorted, `Bronze (scout for explorer/researcher) [default ${weakest}]: `, weakest)
1072
- tiers.silver = await askModel(rl, sorted, `Silver (coder/debugger) [default ${middle}]: `, middle)
1073
- tiers.gold = await askModel(rl, sorted, `Gold (resolver/reviewer/deep-reviewer/planner/architect) [default ${strongest}]: `, strongest)
821
+ tiers.bronze = await askModel(rl, sorted, `Bronze (scout for explorer/researcher) [default ${bronzeDefault}]: `, bronzeDefault)
822
+ tiers.silver = await askModel(rl, sorted, `Silver (coder/debugger) [default ${silverDefault}]: `, silverDefault)
823
+ tiers.gold = await askModel(rl, sorted, `Gold (resolver/reviewer/deep-reviewer/planner/architect) [default ${goldDefault}]: `, goldDefault)
1074
824
  } else if (shape === "two") {
1075
- tiers.silver = await askModel(rl, sorted, `Silver (fast: coder/explorer/debugger/researcher) [default ${weakest}]: `, weakest)
1076
- tiers.gold = await askModel(rl, sorted, `Gold (strong: resolver/reviewer/…/architect) [default ${strongest}]: `, strongest)
825
+ tiers.silver = await askModel(rl, sorted, `Silver (fast: coder/explorer/debugger/researcher) [default ${bronzeDefault}]: `, bronzeDefault)
826
+ tiers.gold = await askModel(rl, sorted, `Gold (strong: resolver/reviewer/…/architect) [default ${goldDefault}]: `, goldDefault)
1077
827
  } else {
1078
828
  tiers.gold = await askModel(rl, sorted, `Model for all roles [default ${strongest}]: `, strongest)
1079
829
  }
@@ -1085,8 +835,7 @@ async function buildGenericInteractivePreset(currentModel, opencodeConfig, scrip
1085
835
  const ok = await askYesNo(rl, `Confirm picks? [Y/n] (n re-asks all): `, true)
1086
836
  if (ok) {
1087
837
  return {
1088
- label: `generic-${shape}-tier`,
1089
- profile: "mix",
838
+ label: `${shape}-tier`,
1090
839
  enabled: DEFAULT_ENABLED_AGENTS,
1091
840
  models: buildGenericResolveModels(tiers),
1092
841
  agents: {},
@@ -1262,115 +1011,3 @@ function formatError(error) {
1262
1011
  return error instanceof Error ? error.message : String(error)
1263
1012
  }
1264
1013
 
1265
- async function offerCompanionPlugins() {
1266
- if (process.env.OPENCODE_RESOLVE_SKIP_COMPANIONS === "1") return
1267
-
1268
- const config = await readOpenCodeConfig()
1269
- const existing = collectPluginBaseNames(config.plugin ?? [])
1270
- const missing = COMPANION_PLUGINS.filter((c) => !existing.has(c.pkg))
1271
-
1272
- if (missing.length === 0) {
1273
- console.log(`[${packageName}] recommended companion plugins already present — skipping`)
1274
- return
1275
- }
1276
-
1277
- const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY)
1278
-
1279
- if (!isInteractive) {
1280
- console.log(`[${packageName}] recommended companion plugins not detected:`)
1281
- for (const c of missing) {
1282
- console.log(` - ${c.pkg} — ${c.desc}`)
1283
- console.log(` install: opencode plugin ${c.pkg}@latest --global --force`)
1284
- }
1285
- return
1286
- }
1287
-
1288
- const rl = createInterface({ input: process.stdin, output: process.stdout })
1289
- try {
1290
- for (const c of missing) {
1291
- console.log("")
1292
- console.log(`[${packageName}] recommended companion: ${c.pkg}`)
1293
- console.log(` ${c.desc}`)
1294
- const raw = await rl.question(" install now? [Y/n] ")
1295
- const answer = raw.trim().toLowerCase()
1296
- const accepted = answer === "" || answer === "y" || answer === "yes"
1297
- if (!accepted) {
1298
- console.log(` skipped — install later via: opencode plugin ${c.pkg}@latest --global --force`)
1299
- continue
1300
- }
1301
- const installed = await installCompanion(c.pkg)
1302
- if (!installed) {
1303
- console.warn(` ${c.pkg} install command failed — leave plugin list untouched, retry manually`)
1304
- continue
1305
- }
1306
- await addCompanionToOpenCodeConfig(`${c.pkg}@latest`)
1307
- console.log(` ${c.pkg} cached and registered — restart OpenCode to activate`)
1308
- }
1309
- } finally {
1310
- rl.close()
1311
- }
1312
- }
1313
-
1314
- function collectPluginBaseNames(plugins) {
1315
- const names = new Set()
1316
- for (const entry of plugins) {
1317
- const raw = typeof entry === "string"
1318
- ? entry
1319
- : Array.isArray(entry) && typeof entry[0] === "string"
1320
- ? entry[0]
1321
- : null
1322
- if (!raw) continue
1323
- names.add(stripVersionSuffix(raw))
1324
- }
1325
- return names
1326
- }
1327
-
1328
- function stripVersionSuffix(name) {
1329
- if (name.startsWith("@")) {
1330
- const slashIndex = name.indexOf("/")
1331
- if (slashIndex === -1) return name
1332
- const scope = name.slice(0, slashIndex)
1333
- const rest = name.slice(slashIndex + 1)
1334
- return `${scope}/${rest.split("@")[0]}`
1335
- }
1336
- return name.split("@")[0]
1337
- }
1338
-
1339
- async function installCompanion(pkg) {
1340
- return new Promise((resolveSpawn) => {
1341
- const child = spawn("opencode", ["plugin", `${pkg}@latest`, "--global", "--force"], {
1342
- stdio: "inherit",
1343
- })
1344
- child.on("exit", (code) => resolveSpawn(code === 0))
1345
- child.on("error", () => resolveSpawn(false))
1346
- })
1347
- }
1348
-
1349
- async function addCompanionToOpenCodeConfig(pluginEntry) {
1350
- const baseName = stripVersionSuffix(pluginEntry)
1351
- const probe = await readOpenCodeConfig()
1352
- const alreadyPresent = isCompanionPresent(probe, baseName)
1353
- if (alreadyPresent) return
1354
-
1355
- const fresh = await readOpenCodeConfig()
1356
- if (!isCompanionPresent(fresh, baseName)) {
1357
- fresh.plugin ??= []
1358
- if (!Array.isArray(fresh.plugin)) {
1359
- throw new Error(`${opencodeConfigPath}.plugin must be an array`)
1360
- }
1361
- fresh.plugin.push(pluginEntry)
1362
- await writeFile(opencodeConfigPath, `${JSON.stringify(fresh, null, 2)}\n`)
1363
- }
1364
- }
1365
-
1366
- function isCompanionPresent(config, baseName) {
1367
- if (!Array.isArray(config.plugin)) return false
1368
- return config.plugin.some((entry) => {
1369
- const raw = typeof entry === "string"
1370
- ? entry
1371
- : Array.isArray(entry) && typeof entry[0] === "string"
1372
- ? entry[0]
1373
- : null
1374
- return raw !== null && stripVersionSuffix(raw) === baseName
1375
- })
1376
- }