opencode-resolve 0.1.7 → 0.1.9

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.
@@ -17,6 +17,19 @@ const ADDITIVE_DEFAULTS = {
17
17
  autoApprove: true,
18
18
  }
19
19
 
20
+ // ZAI local MCP server bootstrap for GLM users.
21
+ // Do not copy provider secrets from OpenCode's auth store into opencode.json.
22
+ // The MCP process should receive credentials from the user's runtime environment.
23
+ const ZAI_MCP_SERVERS = {
24
+ "zai-mcp-server": {
25
+ type: "local",
26
+ command: ["npx", "-y", "@z_ai/mcp-server"],
27
+ environment: {
28
+ Z_AI_MODE: "ZAI",
29
+ },
30
+ },
31
+ }
32
+
20
33
  const COMPANION_PLUGINS = [
21
34
  {
22
35
  pkg: "@tarquinen/opencode-dcp",
@@ -28,6 +41,26 @@ const COMPANION_PLUGINS = [
28
41
  },
29
42
  ]
30
43
 
44
+ const OPENAI_MODEL_HINTS = [
45
+ "openai/gpt-5.5",
46
+ "openai/gpt-5.4",
47
+ "openai/gpt-5.3-codex",
48
+ "openai/gpt-5.3-codex-spark",
49
+ "openai/gpt-5.2",
50
+ "openai/gpt-5-mini",
51
+ "openai/gpt-4o-mini",
52
+ ]
53
+
54
+ const GLM_MODEL_HINTS = [
55
+ "zai-coding-plan/glm-5.1",
56
+ "zai/glm-5.1",
57
+ "zai/glm-5",
58
+ "zai/glm-4.7-flash",
59
+ "zai/glm-4.5-flash",
60
+ "zai/glm-4.5",
61
+ "zai/glm-4.5-air",
62
+ ]
63
+
31
64
  if (process.env.OPENCODE_RESOLVE_SKIP_POSTINSTALL === "1") {
32
65
  process.exit(0)
33
66
  }
@@ -56,34 +89,162 @@ async function readOwnVersion() {
56
89
 
57
90
  async function registerPlugin() {
58
91
  await mkdir(configDir, { recursive: true })
92
+ const scriptedAnswers = await readScriptedAnswersIfNeeded()
59
93
 
60
94
  const config = await readOpenCodeConfig()
61
- const changed = addPlugin(config)
95
+ let configChanged = addPlugin(config)
96
+
97
+ // Inject ZAI MCP servers when GLM is detected
98
+ const allModels = detectAllModels(config)
99
+ const hasGLM = allModels.some((m) => isGLMModel(m))
100
+ if (hasGLM) {
101
+ const mcpChanged = await injectZAIMCPs(config)
102
+ configChanged = configChanged || mcpChanged
103
+ }
62
104
 
63
- if (changed) {
105
+ if (configChanged) {
64
106
  await writeFile(opencodeConfigPath, `${JSON.stringify(config, null, 2)}\n`)
65
- console.log(`[${packageName}] registered in ${opencodeConfigPath}`)
107
+ console.log(`[${packageName}] updated ${opencodeConfigPath}`)
66
108
  } else {
67
109
  console.log(`[${packageName}] already registered in ${opencodeConfigPath}`)
68
110
  }
69
111
 
70
112
  if (!(await exists(resolveConfigPath))) {
71
- await createAdaptiveResolveConfig(config)
113
+ await createAdaptiveResolveConfig(config, scriptedAnswers)
114
+ return
115
+ }
116
+
117
+ await handleExistingResolveConfig(config, scriptedAnswers)
118
+ }
119
+
120
+ async function handleExistingResolveConfig(opencodeConfig, scriptedAnswers) {
121
+ const action = await chooseExistingResolveConfigAction(scriptedAnswers)
122
+ if (action === "fresh") {
123
+ await backupResolveConfig()
124
+ await createAdaptiveResolveConfig(opencodeConfig, scriptedAnswers)
72
125
  return
73
126
  }
74
127
 
75
128
  await migrateResolveConfig()
76
129
  }
77
130
 
78
- async function createAdaptiveResolveConfig(opencodeConfig) {
131
+ async function chooseExistingResolveConfigAction(scriptedAnswers) {
132
+ const requested = (process.env.OPENCODE_RESOLVE_REINSTALL ?? "").trim().toLowerCase()
133
+ if (["fresh", "reset", "recreate", "new"].includes(requested)) return "fresh"
134
+ if (["update", "keep", "migrate", "preserve"].includes(requested)) return "update"
135
+ if (requested) {
136
+ console.warn(`[${packageName}] ignoring unknown OPENCODE_RESOLVE_REINSTALL=${JSON.stringify(requested)}; use "fresh" or "update".`)
137
+ }
138
+
139
+ const forcePrompt = process.env.OPENCODE_RESOLVE_FORCE_PROMPT === "1"
140
+ const canPrompt = Boolean((process.stdin.isTTY && process.stdout.isTTY) || forcePrompt)
141
+ if (!canPrompt) {
142
+ console.log(`[${packageName}] existing ${resolveConfigPath} found; preserving it and applying additive updates.`)
143
+ console.log(`[${packageName}] for a fresh reinstall, rerun in a TTY or set OPENCODE_RESOLVE_REINSTALL=fresh.`)
144
+ return "update"
145
+ }
146
+
147
+ const rl = createPromptInterface(scriptedAnswers)
148
+ try {
149
+ console.log("")
150
+ console.log(`[${packageName}] Existing resolve config found: ${resolveConfigPath}`)
151
+ console.log(" 1. update existing config — preserve your settings and add missing defaults")
152
+ console.log(" 2. fresh reinstall — back up resolve.json and run setup again")
153
+ const answer = await askChoice(rl, "Existing config [1=update, 2=fresh reinstall, default 1]: ", ["1", "2"], "1")
154
+ return answer === "2" ? "fresh" : "update"
155
+ } finally {
156
+ rl.close()
157
+ }
158
+ }
159
+
160
+ async function backupResolveConfig() {
161
+ const raw = await readFile(resolveConfigPath, "utf8")
162
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-")
163
+ const backupPath = `${resolveConfigPath}.bak.${stamp}`
164
+ await writeFile(backupPath, raw)
165
+ console.log(`[${packageName}] backed up existing resolve config to ${backupPath}`)
166
+ }
167
+
168
+ async function createAdaptiveResolveConfig(opencodeConfig, scriptedAnswers) {
79
169
  await assertReadable(exampleConfigPath)
80
170
  const raw = await readFile(exampleConfigPath, "utf8")
81
171
  const example = JSON.parse(raw)
82
172
 
83
173
  const currentModel = detectOpenCodeModel(opencodeConfig)
84
- const preset = buildModelPreset(currentModel)
85
-
174
+ const allModels = detectAllModels(opencodeConfig)
86
175
  const resolveConfig = { ...example }
176
+ const forcePrompt = process.env.OPENCODE_RESOLVE_FORCE_PROMPT === "1"
177
+ const canPrompt = Boolean(
178
+ (process.stdin.isTTY && process.stdout.isTTY) || forcePrompt,
179
+ )
180
+ const interactivePreset = canPrompt
181
+ ? await buildInteractivePreset(currentModel, allModels, scriptedAnswers)
182
+ : undefined
183
+
184
+ if (interactivePreset) {
185
+ resolveConfig.profile = interactivePreset.profile
186
+ if (interactivePreset.tier) resolveConfig.tier = interactivePreset.tier
187
+ else delete resolveConfig.tier
188
+ if (interactivePreset.enabled) resolveConfig.enabled = interactivePreset.enabled
189
+ resolveConfig.models = interactivePreset.models
190
+ resolveConfig.agents = {
191
+ ...resolveConfig.agents,
192
+ ...(interactivePreset.agents ?? {}),
193
+ }
194
+ await writeFile(resolveConfigPath, `${JSON.stringify(resolveConfig, null, 2)}\n`)
195
+ console.log(`[${packageName}] created ${resolveConfigPath} (preset: ${interactivePreset.label})`)
196
+ return
197
+ }
198
+
199
+ if (process.env.OPENCODE_RESOLVE_AUTO_PRESET !== "1") {
200
+ resolveConfig.profile = "mix"
201
+ delete resolveConfig.tier
202
+ resolveConfig.models = {}
203
+ resolveConfig.agents = {
204
+ ...resolveConfig.agents,
205
+ codex: { ...(resolveConfig.agents?.codex ?? {}), enabled: true },
206
+ glm: { ...(resolveConfig.agents?.glm ?? {}), enabled: true },
207
+ }
208
+ await writeFile(resolveConfigPath, `${JSON.stringify(resolveConfig, null, 2)}\n`)
209
+ console.log(`[${packageName}] created ${resolveConfigPath} (preset: prompt-required)`)
210
+ console.log(`[${packageName}] model pinning was not guessed because no interactive prompt was available.`)
211
+ console.log(`[${packageName}] rerun setup in a TTY or edit ${resolveConfigPath} to choose GPT/GLM three-tier models.`)
212
+ return
213
+ }
214
+
215
+ // Profile selection based on detected providers
216
+ const hasGLM = allModels.some((m) => isGLMModel(m))
217
+ const hasGPT = allModels.some((m) => isGPTModel(m))
218
+ const preset = buildModelPreset(currentModel, allModels)
219
+
220
+ if (hasGLM && !hasGPT) {
221
+ // GLM only → GLM profile, silver tier (token-efficient, no deep-reviewer)
222
+ resolveConfig.profile = "glm"
223
+ resolveConfig.tier = "silver"
224
+ resolveConfig.agents = {
225
+ ...resolveConfig.agents,
226
+ glm: { ...(resolveConfig.agents?.glm ?? {}), enabled: true },
227
+ }
228
+ } else if (hasGPT && !hasGLM) {
229
+ // GPT only → GPT profile, gold tier (full power)
230
+ resolveConfig.profile = "gpt"
231
+ resolveConfig.tier = "gold"
232
+ resolveConfig.agents = {
233
+ ...resolveConfig.agents,
234
+ codex: { ...(resolveConfig.agents?.codex ?? {}), enabled: true },
235
+ }
236
+ } else {
237
+ // Mixed or unknown provider → explicit mix profile. No tier: use DEFAULT_ENABLED.
238
+ resolveConfig.profile = "mix"
239
+ if (hasGLM && hasGPT) {
240
+ resolveConfig.agents = {
241
+ ...resolveConfig.agents,
242
+ codex: { ...(resolveConfig.agents?.codex ?? {}), enabled: true },
243
+ glm: { ...(resolveConfig.agents?.glm ?? {}), enabled: true },
244
+ }
245
+ }
246
+ }
247
+
87
248
  if (preset && Object.keys(preset).length > 0) {
88
249
  resolveConfig.models = preset
89
250
  }
@@ -121,54 +282,389 @@ function detectOpenCodeModel(config) {
121
282
  return null
122
283
  }
123
284
 
124
- function buildModelPreset(currentModel) {
125
- if (!currentModel) return {}
285
+ function buildModelPreset(currentModel, allModels = []) {
286
+ const detectedGLMModels = allModels.filter(isGLMModel)
287
+ const detectedGPTModels = allModels.filter(isGPTModel)
288
+ const glmModels = detectedGLMModels.length > 0 ? collectModelChoices(detectedGLMModels, isGLMModel, GLM_MODEL_HINTS, false) : []
289
+ const gptModels = detectedGPTModels.length > 0 ? collectModelChoices(detectedGPTModels, isGPTModel, OPENAI_MODEL_HINTS, false) : []
290
+ const glmModel = glmModels[0]
291
+ const gptModel = gptModels[0]
292
+
293
+ if (glmModel && gptModel) {
294
+ const glmTiers = chooseThreeTier(glmModels, "glm", false)
295
+ const gptTiers = chooseThreeTier(gptModels, "gpt", false)
296
+ return {
297
+ mix: "gpt",
298
+ gpt: gptTiers.gold,
299
+ bronze: glmTiers.bronze,
300
+ silver: glmTiers.silver,
301
+ gold: gptTiers.gold,
302
+ "glm-bronze": glmTiers.bronze,
303
+ "glm-silver": glmTiers.silver,
304
+ "glm-gold": glmTiers.gold,
305
+ "gpt-bronze": gptTiers.bronze,
306
+ "gpt-silver": gptTiers.silver,
307
+ "gpt-gold": gptTiers.gold,
308
+ fast: "bronze",
309
+ strong: "gold",
310
+ mini: "bronze",
311
+ codex: "gpt-gold",
312
+ glm: glmTiers.gold,
313
+ explorer: "bronze",
314
+ coder: "silver",
315
+ resolver: "gold",
316
+ reviewer: "gold",
317
+ "deep-reviewer": "gold",
318
+ planner: "gold",
319
+ }
320
+ }
321
+
322
+ if (!currentModel) {
323
+ return {}
324
+ }
126
325
 
127
326
  const lower = currentModel.toLowerCase()
128
327
 
129
- // GLM / ZAI mixed preset
328
+ // GLM / ZAI GLM-only preset (no GPT dependency, avoids token-exhaustion errors)
130
329
  if (lower.includes("glm") || lower.includes("zai")) {
131
- return {
132
- glm: "zai-coding-plan/glm-5.1",
133
- gpt: "openai/gpt-5.5",
134
- fast: "glm",
135
- strong: "gpt",
136
- coder: "glm",
137
- resolver: "gpt",
138
- reviewer: "gpt",
139
- "deep-reviewer": "gpt",
140
- explorer: "fast",
141
- }
330
+ return buildGLMOnlyPreset(currentModel)
142
331
  }
143
332
 
144
333
  // OpenAI / GPT single-provider preset
145
334
  if (lower.includes("openai/") || lower.includes("gpt")) {
146
- return {
147
- gpt: currentModel,
148
- fast: "gpt",
149
- strong: "gpt",
150
- mini: "gpt",
151
- codex: "gpt",
152
- coder: "gpt",
153
- resolver: "gpt",
154
- explorer: "gpt",
155
- reviewer: "gpt",
156
- "deep-reviewer": "gpt",
157
- }
335
+ return buildGPTOnlyPreset(currentModel)
158
336
  }
159
337
 
160
338
  // Unknown provider — keep model-neutral
161
339
  return {}
162
340
  }
163
341
 
342
+ function buildGLMOnlyPreset(model) {
343
+ const tiers = chooseThreeTier(collectModelChoices([model], isGLMModel, GLM_MODEL_HINTS, false), "glm", false)
344
+ return {
345
+ glm: tiers.gold,
346
+ bronze: tiers.bronze,
347
+ silver: tiers.silver,
348
+ gold: tiers.gold,
349
+ fast: "bronze",
350
+ strong: "gold",
351
+ mini: "bronze",
352
+ coder: "silver",
353
+ resolver: "gold",
354
+ reviewer: "gold",
355
+ "deep-reviewer": "gold",
356
+ explorer: "bronze",
357
+ planner: "gold",
358
+ }
359
+ }
360
+
361
+ function buildGPTOnlyPreset(model) {
362
+ const tiers = chooseThreeTier(collectModelChoices([model], isGPTModel, OPENAI_MODEL_HINTS, false), "gpt", false)
363
+ return {
364
+ gpt: tiers.gold,
365
+ bronze: tiers.bronze,
366
+ silver: tiers.silver,
367
+ gold: tiers.gold,
368
+ fast: "bronze",
369
+ strong: "gold",
370
+ mini: "bronze",
371
+ codex: "gold",
372
+ coder: "silver",
373
+ resolver: "gold",
374
+ explorer: "bronze",
375
+ reviewer: "gold",
376
+ "deep-reviewer": "gold",
377
+ }
378
+ }
379
+
380
+ async function buildInteractivePreset(currentModel, allModels, scriptedAnswers) {
381
+ const choices = {
382
+ gpt: collectModelChoices(allModels, isGPTModel, OPENAI_MODEL_HINTS),
383
+ glm: collectModelChoices(allModels, isGLMModel, GLM_MODEL_HINTS),
384
+ }
385
+ if (currentModel) {
386
+ if (isGPTModel(currentModel)) choices.gpt = unique([currentModel, ...choices.gpt])
387
+ if (isGLMModel(currentModel)) choices.glm = unique([currentModel, ...choices.glm])
388
+ }
389
+
390
+ const rl = createPromptInterface(scriptedAnswers)
391
+ try {
392
+ console.log("")
393
+ console.log(`[${packageName}] Choose resolve profile:`)
394
+ console.log(" 1. mix — neutral resolver plus optional Codex and GLM primary agents")
395
+ console.log(" 2. gpt — GPT/Codex-only, three-tier")
396
+ console.log(" 3. glm — GLM-only, three-tier")
397
+ const profileAnswer = await askChoice(rl, "Profile [1=mix, 2=gpt, 3=glm, default 1]: ", ["1", "2", "3"], "1")
398
+ const profile = profileAnswer === "2" ? "gpt" : profileAnswer === "3" ? "glm" : "mix"
399
+
400
+ if (profile === "gpt") {
401
+ const tiers = await askThreeTier(rl, "GPT/Codex", choices.gpt)
402
+ return {
403
+ label: "gpt-three-tier",
404
+ profile: "gpt",
405
+ tier: "gold",
406
+ models: buildGPTThreeTierModels(tiers),
407
+ agents: { codex: { enabled: true } },
408
+ }
409
+ }
410
+
411
+ if (profile === "glm") {
412
+ const tiers = await askThreeTier(rl, "GLM", choices.glm)
413
+ return {
414
+ label: "glm-three-tier",
415
+ profile: "glm",
416
+ tier: "gold",
417
+ models: buildGLMThreeTierModels(tiers),
418
+ agents: { glm: { enabled: true } },
419
+ }
420
+ }
421
+
422
+ const useCodex = await askYesNo(rl, "Enable dedicated Codex primary agent too? [Y/n]: ", true)
423
+ const useGLM = await askYesNo(rl, "Enable dedicated GLM primary agent too? [Y/n]: ", true)
424
+ const gptTiers = await askThreeTier(rl, "Codex/GPT", choices.gpt)
425
+ const glmTiers = await askThreeTier(rl, "GLM", choices.glm)
426
+ return {
427
+ label: "mix-three-tier",
428
+ profile: "mix",
429
+ models: buildMixedThreeTierModels(gptTiers, glmTiers),
430
+ agents: {
431
+ codex: { enabled: useCodex },
432
+ glm: { enabled: useGLM },
433
+ },
434
+ }
435
+ } finally {
436
+ rl.close()
437
+ }
438
+ }
439
+
440
+ function createPromptInterface(scriptedAnswers) {
441
+ return scriptedAnswers
442
+ ? {
443
+ async question(prompt) {
444
+ const answer = scriptedAnswers.length > 0 ? scriptedAnswers.shift() ?? "" : ""
445
+ process.stdout.write(prompt)
446
+ process.stdout.write(`${answer}\n`)
447
+ return answer
448
+ },
449
+ close() {},
450
+ }
451
+ : createInterface({ input: process.stdin, output: process.stdout })
452
+ }
453
+
454
+ async function askThreeTier(rl, label, models) {
455
+ const choices = models.length > 0 ? models : (label.toLowerCase().includes("glm") ? GLM_MODEL_HINTS : OPENAI_MODEL_HINTS)
456
+ console.log("")
457
+ console.log(`[${packageName}] ${label} model choices:`)
458
+ choices.forEach((model, index) => console.log(` ${index + 1}. ${model}`))
459
+ const defaults = chooseThreeTier(choices, label.toLowerCase().includes("glm") ? "glm" : "gpt")
460
+ const bronze = await askModel(rl, choices, `Pick ${label} bronze/scout [default ${defaults.bronze}]: `, defaults.bronze)
461
+ const silver = await askModel(rl, choices, `Pick ${label} silver/coder [default ${defaults.silver}]: `, defaults.silver)
462
+ const gold = await askModel(rl, choices, `Pick ${label} gold/reasoner [default ${defaults.gold}]: `, defaults.gold)
463
+ return { bronze, silver, gold }
464
+ }
465
+
466
+ async function askModel(rl, choices, question, defaultValue) {
467
+ const answer = (await rl.question(question)).trim()
468
+ if (!answer) return defaultValue
469
+ const index = Number.parseInt(answer, 10)
470
+ if (Number.isInteger(index) && index >= 1 && index <= choices.length) return choices[index - 1]
471
+ return answer
472
+ }
473
+
474
+ async function askChoice(rl, question, valid, defaultValue) {
475
+ const answer = (await rl.question(question)).trim().toLowerCase()
476
+ if (!answer) return defaultValue
477
+ return valid.includes(answer) ? answer : defaultValue
478
+ }
479
+
480
+ async function askYesNo(rl, question, defaultValue) {
481
+ const answer = (await rl.question(question)).trim().toLowerCase()
482
+ if (!answer) return defaultValue
483
+ return answer === "y" || answer === "yes"
484
+ }
485
+
486
+ function buildMixedThreeTierModels(gptTiers, glmTiers) {
487
+ return {
488
+ mix: "gpt-gold",
489
+ gpt: "gpt-gold",
490
+ glm: "glm-gold",
491
+ bronze: "glm-bronze",
492
+ silver: "glm-silver",
493
+ gold: "gpt-gold",
494
+ "gpt-bronze": gptTiers.bronze,
495
+ "gpt-silver": gptTiers.silver,
496
+ "gpt-gold": gptTiers.gold,
497
+ "glm-bronze": glmTiers.bronze,
498
+ "glm-silver": glmTiers.silver,
499
+ "glm-gold": glmTiers.gold,
500
+ fast: "bronze",
501
+ strong: "gold",
502
+ mini: "bronze",
503
+ codex: "gpt-gold",
504
+ explorer: "bronze",
505
+ coder: "silver",
506
+ resolver: "gold",
507
+ reviewer: "gold",
508
+ "deep-reviewer": "gold",
509
+ planner: "gold",
510
+ }
511
+ }
512
+
513
+ function buildGPTThreeTierModels(tiers) {
514
+ return {
515
+ gpt: "gold",
516
+ bronze: tiers.bronze,
517
+ silver: tiers.silver,
518
+ gold: tiers.gold,
519
+ fast: "bronze",
520
+ strong: "gold",
521
+ mini: "bronze",
522
+ codex: "gold",
523
+ explorer: "bronze",
524
+ coder: "silver",
525
+ resolver: "gold",
526
+ reviewer: "gold",
527
+ "deep-reviewer": "gold",
528
+ planner: "gold",
529
+ }
530
+ }
531
+
532
+ function buildGLMThreeTierModels(tiers) {
533
+ return {
534
+ glm: "gold",
535
+ bronze: tiers.bronze,
536
+ silver: tiers.silver,
537
+ gold: tiers.gold,
538
+ fast: "bronze",
539
+ strong: "gold",
540
+ mini: "bronze",
541
+ explorer: "bronze",
542
+ coder: "silver",
543
+ resolver: "gold",
544
+ reviewer: "gold",
545
+ "deep-reviewer": "gold",
546
+ planner: "gold",
547
+ }
548
+ }
549
+
550
+ function collectModelChoices(allModels, predicate, hints, includeFallbackHints = true) {
551
+ const detected = allModels.filter(predicate)
552
+ const providerIds = new Set(detected.map((model) => model.split("/")[0]).filter(Boolean))
553
+ const matchingHints = includeFallbackHints
554
+ ? hints.filter((model) => providerIds.size === 0 || providerIds.has(model.split("/")[0]) || detected.length < 3)
555
+ : []
556
+ return unique([...detected, ...matchingHints])
557
+ }
558
+
559
+ function chooseThreeTier(models, family, includeFallbackHints = true) {
560
+ const fallback = family === "glm" ? GLM_MODEL_HINTS : OPENAI_MODEL_HINTS
561
+ const choices = unique(includeFallbackHints ? [...models, ...fallback] : models)
562
+ return {
563
+ bronze: preferModel(choices, family === "glm" ? ["flash", "air", "mini"] : ["spark", "mini", "4o-mini"], choices[0]),
564
+ silver: preferModel(choices, family === "glm" ? ["5.1", "4.5"] : ["codex", "5.3", "5.2"], choices[1] ?? choices[0]),
565
+ gold: preferModel(choices, family === "glm" ? ["5.1", "5", "4.5"] : ["5.5", "5.4", "gpt-5.3-codex"], choices[2] ?? choices[1] ?? choices[0]),
566
+ }
567
+ }
568
+
569
+ function preferModel(models, needles, fallback) {
570
+ return models.find((model) => {
571
+ const lower = model.toLowerCase()
572
+ return needles.some((needle) => lower.includes(needle))
573
+ }) ?? fallback
574
+ }
575
+
576
+ function unique(values) {
577
+ return [...new Set(values.filter((value) => typeof value === "string" && value.length > 0))]
578
+ }
579
+
580
+ async function readAllStdin() {
581
+ let raw = ""
582
+ for await (const chunk of process.stdin) {
583
+ raw += chunk
584
+ }
585
+ return raw
586
+ }
587
+
588
+ async function readScriptedAnswersIfNeeded() {
589
+ if (process.env.OPENCODE_RESOLVE_FORCE_PROMPT === "1" && !process.stdin.isTTY) {
590
+ return (await readAllStdin()).split(/\r?\n/)
591
+ }
592
+ return undefined
593
+ }
594
+
164
595
  function getPresetLabel(currentModel) {
165
596
  if (!currentModel) return "inherited"
166
597
  const lower = currentModel.toLowerCase()
167
- if (lower.includes("glm") || lower.includes("zai")) return "glm+gpt"
598
+ if (lower.includes("glm") || lower.includes("zai")) return "glm-only"
168
599
  if (lower.includes("openai/") || lower.includes("gpt")) return "gpt-only"
169
600
  return "inherited"
170
601
  }
171
602
 
603
+ function isGLMModel(currentModel) {
604
+ if (!currentModel) return false
605
+ const lower = currentModel.toLowerCase()
606
+ return lower.includes("glm") || lower.includes("zai")
607
+ }
608
+
609
+ function isGPTModel(currentModel) {
610
+ if (!currentModel) return false
611
+ const lower = currentModel.toLowerCase()
612
+ return lower.includes("openai/") || lower.includes("gpt")
613
+ }
614
+
615
+ function detectAllModels(config) {
616
+ const models = new Set()
617
+
618
+ // Top-level model
619
+ if (typeof config.model === "string" && config.model.length > 0) {
620
+ models.add(config.model)
621
+ }
622
+
623
+ // Top-level models object values
624
+ if (isObject(config.models)) {
625
+ for (const value of Object.values(config.models)) {
626
+ if (typeof value === "string" && value.length > 0) {
627
+ models.add(value)
628
+ }
629
+ }
630
+ }
631
+
632
+ // Provider model lists
633
+ if (isObject(config.provider)) {
634
+ for (const [providerId, providerConfig] of Object.entries(config.provider)) {
635
+ if (isObject(providerConfig) && isObject(providerConfig.models)) {
636
+ for (const [modelKey, modelEntry] of Object.entries(providerConfig.models)) {
637
+ if (typeof modelKey === "string" && modelKey.length > 0) {
638
+ models.add(qualifyModelId(providerId, modelKey))
639
+ }
640
+ if (typeof modelEntry === "string") {
641
+ models.add(qualifyModelId(providerId, modelEntry))
642
+ } else if (isObject(modelEntry) && typeof modelEntry.id === "string") {
643
+ models.add(qualifyModelId(providerId, modelEntry.id))
644
+ }
645
+ }
646
+ }
647
+ }
648
+ }
649
+
650
+ // Agent model values
651
+ if (isObject(config.agent)) {
652
+ for (const agentConfig of Object.values(config.agent)) {
653
+ if (isObject(agentConfig) && typeof agentConfig.model === "string" && agentConfig.model.length > 0) {
654
+ models.add(agentConfig.model)
655
+ }
656
+ }
657
+ }
658
+
659
+ return [...models]
660
+ }
661
+
662
+ function qualifyModelId(providerId, modelId) {
663
+ if (typeof modelId !== "string" || modelId.length === 0) return modelId
664
+ if (modelId.includes("/")) return modelId
665
+ return `${providerId}/${modelId}`
666
+ }
667
+
172
668
  async function migrateResolveConfig() {
173
669
  let raw
174
670
  try {
@@ -239,6 +735,28 @@ function addPlugin(config) {
239
735
  return true
240
736
  }
241
737
 
738
+ async function injectZAIMCPs(config) {
739
+ config.mcp ??= {}
740
+ if (!isObject(config.mcp)) return false
741
+
742
+ const added = []
743
+ for (const [name, mcpConfig] of Object.entries(ZAI_MCP_SERVERS)) {
744
+ if (config.mcp[name] === undefined) {
745
+ config.mcp[name] = mcpConfig
746
+ added.push(name)
747
+ }
748
+ }
749
+
750
+ if (added.length > 0) {
751
+ console.log(`[${packageName}] injected ZAI MCP server config: ${added.join(", ")}`)
752
+ console.log(`[${packageName}] note: API keys are not copied into opencode.json; export Z_AI_API_KEY if the MCP server requires it.`)
753
+ return true
754
+ }
755
+
756
+ console.log(`[${packageName}] ZAI MCP server already present — skipping`)
757
+ return false
758
+ }
759
+
242
760
  function isRegisteredPluginEntry(entry) {
243
761
  if (typeof entry === "string") return isResolvePluginName(entry)
244
762
  if (Array.isArray(entry) && typeof entry[0] === "string") return isResolvePluginName(entry[0])