opencode-resolve 0.1.6 → 0.1.8
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/README.ko.md +98 -53
- package/README.md +98 -53
- package/dist/agents.d.ts +26 -0
- package/dist/agents.js +355 -0
- package/dist/config.d.ts +29 -0
- package/dist/config.js +398 -0
- package/dist/hooks/index.d.ts +18 -0
- package/dist/hooks/index.js +493 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +11 -702
- package/dist/state.d.ts +33 -0
- package/dist/state.js +20 -0
- package/dist/tools/index.d.ts +252 -0
- package/dist/tools/index.js +1209 -0
- package/dist/types.d.ts +56 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +33 -0
- package/dist/utils.js +371 -0
- package/opencode-resolve.example.json +5 -2
- package/opencode-resolve.reference.jsonc +78 -19
- package/package.json +6 -2
- package/scripts/postinstall.mjs +311 -31
package/scripts/postinstall.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { spawn } from "node:child_process"
|
|
1
2
|
import { constants } from "node:fs"
|
|
2
3
|
import { access, mkdir, readFile, writeFile } from "node:fs/promises"
|
|
3
4
|
import { homedir } from "node:os"
|
|
4
5
|
import { dirname, join, resolve } from "node:path"
|
|
6
|
+
import { createInterface } from "node:readline/promises"
|
|
5
7
|
import { fileURLToPath } from "node:url"
|
|
6
8
|
|
|
7
9
|
const packageName = "opencode-resolve"
|
|
@@ -15,6 +17,30 @@ const ADDITIVE_DEFAULTS = {
|
|
|
15
17
|
autoApprove: true,
|
|
16
18
|
}
|
|
17
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
|
+
|
|
33
|
+
const COMPANION_PLUGINS = [
|
|
34
|
+
{
|
|
35
|
+
pkg: "@tarquinen/opencode-dcp",
|
|
36
|
+
desc: "Dynamic Context Pruning — strips obsolete tool outputs so long resolver loops cost fewer tokens",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
pkg: "@slkiser/opencode-quota",
|
|
40
|
+
desc: "Live token/quota usage indicator — supports GLM coding-plan, OpenAI Plus/Pro, Qwen, and more",
|
|
41
|
+
},
|
|
42
|
+
]
|
|
43
|
+
|
|
18
44
|
if (process.env.OPENCODE_RESOLVE_SKIP_POSTINSTALL === "1") {
|
|
19
45
|
process.exit(0)
|
|
20
46
|
}
|
|
@@ -24,6 +50,7 @@ console.log(`[${packageName}] installing v${pluginVersion}`)
|
|
|
24
50
|
|
|
25
51
|
try {
|
|
26
52
|
await registerPlugin()
|
|
53
|
+
await offerCompanionPlugins()
|
|
27
54
|
console.log(`[${packageName}] v${pluginVersion} install complete — restart OpenCode to load the plugin`)
|
|
28
55
|
} catch (error) {
|
|
29
56
|
console.warn(`[${packageName}] automatic OpenCode registration skipped: ${formatError(error)}`)
|
|
@@ -44,11 +71,19 @@ async function registerPlugin() {
|
|
|
44
71
|
await mkdir(configDir, { recursive: true })
|
|
45
72
|
|
|
46
73
|
const config = await readOpenCodeConfig()
|
|
47
|
-
|
|
74
|
+
let configChanged = addPlugin(config)
|
|
75
|
+
|
|
76
|
+
// Inject ZAI MCP servers when GLM is detected
|
|
77
|
+
const allModels = detectAllModels(config)
|
|
78
|
+
const hasGLM = allModels.some((m) => isGLMModel(m))
|
|
79
|
+
if (hasGLM) {
|
|
80
|
+
const mcpChanged = await injectZAIMCPs(config)
|
|
81
|
+
configChanged = configChanged || mcpChanged
|
|
82
|
+
}
|
|
48
83
|
|
|
49
|
-
if (
|
|
84
|
+
if (configChanged) {
|
|
50
85
|
await writeFile(opencodeConfigPath, `${JSON.stringify(config, null, 2)}\n`)
|
|
51
|
-
console.log(`[${packageName}]
|
|
86
|
+
console.log(`[${packageName}] updated ${opencodeConfigPath}`)
|
|
52
87
|
} else {
|
|
53
88
|
console.log(`[${packageName}] already registered in ${opencodeConfigPath}`)
|
|
54
89
|
}
|
|
@@ -67,9 +102,27 @@ async function createAdaptiveResolveConfig(opencodeConfig) {
|
|
|
67
102
|
const example = JSON.parse(raw)
|
|
68
103
|
|
|
69
104
|
const currentModel = detectOpenCodeModel(opencodeConfig)
|
|
70
|
-
const
|
|
71
|
-
|
|
105
|
+
const allModels = detectAllModels(opencodeConfig)
|
|
72
106
|
const resolveConfig = { ...example }
|
|
107
|
+
|
|
108
|
+
// Profile selection based on detected providers
|
|
109
|
+
const hasGLM = allModels.some((m) => isGLMModel(m))
|
|
110
|
+
const hasGPT = allModels.some((m) => isGPTModel(m))
|
|
111
|
+
const preset = buildModelPreset(currentModel, allModels)
|
|
112
|
+
|
|
113
|
+
if (hasGLM && !hasGPT) {
|
|
114
|
+
// GLM only → GLM profile, silver tier (token-efficient, no deep-reviewer)
|
|
115
|
+
resolveConfig.profile = "glm"
|
|
116
|
+
resolveConfig.tier = "silver"
|
|
117
|
+
} else if (hasGPT && !hasGLM) {
|
|
118
|
+
// GPT only → GPT profile, gold tier (full power)
|
|
119
|
+
resolveConfig.profile = "gpt"
|
|
120
|
+
resolveConfig.tier = "gold"
|
|
121
|
+
} else {
|
|
122
|
+
// Mixed or unknown provider → explicit mix profile. No tier: use DEFAULT_ENABLED.
|
|
123
|
+
resolveConfig.profile = "mix"
|
|
124
|
+
}
|
|
125
|
+
|
|
73
126
|
if (preset && Object.keys(preset).length > 0) {
|
|
74
127
|
resolveConfig.models = preset
|
|
75
128
|
}
|
|
@@ -107,54 +160,155 @@ function detectOpenCodeModel(config) {
|
|
|
107
160
|
return null
|
|
108
161
|
}
|
|
109
162
|
|
|
110
|
-
function buildModelPreset(currentModel) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const lower = currentModel.toLowerCase()
|
|
163
|
+
function buildModelPreset(currentModel, allModels = []) {
|
|
164
|
+
const glmModel = allModels.find((m) => isGLMModel(m))
|
|
165
|
+
const gptModel = allModels.find((m) => isGPTModel(m))
|
|
114
166
|
|
|
115
|
-
|
|
116
|
-
if (lower.includes("glm") || lower.includes("zai")) {
|
|
167
|
+
if (glmModel && gptModel) {
|
|
117
168
|
return {
|
|
118
|
-
|
|
119
|
-
|
|
169
|
+
mix: "gpt",
|
|
170
|
+
glm: glmModel,
|
|
171
|
+
gpt: gptModel,
|
|
172
|
+
bronze: "glm",
|
|
173
|
+
silver: "glm",
|
|
174
|
+
gold: "gpt",
|
|
120
175
|
fast: "glm",
|
|
121
176
|
strong: "gpt",
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
177
|
+
mini: "glm",
|
|
178
|
+
codex: "gpt",
|
|
179
|
+
explorer: "bronze",
|
|
180
|
+
coder: "silver",
|
|
181
|
+
resolver: "gold",
|
|
182
|
+
reviewer: "gold",
|
|
183
|
+
"deep-reviewer": "gold",
|
|
184
|
+
planner: "gold",
|
|
127
185
|
}
|
|
128
186
|
}
|
|
129
187
|
|
|
188
|
+
if (!currentModel) {
|
|
189
|
+
if (glmModel) return buildGLMOnlyPreset(glmModel)
|
|
190
|
+
if (gptModel) return buildGPTOnlyPreset(gptModel)
|
|
191
|
+
return {}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const lower = currentModel.toLowerCase()
|
|
195
|
+
|
|
196
|
+
// GLM / ZAI — GLM-only preset (no GPT dependency, avoids token-exhaustion errors)
|
|
197
|
+
if (lower.includes("glm") || lower.includes("zai")) {
|
|
198
|
+
return buildGLMOnlyPreset(currentModel)
|
|
199
|
+
}
|
|
200
|
+
|
|
130
201
|
// OpenAI / GPT single-provider preset
|
|
131
202
|
if (lower.includes("openai/") || lower.includes("gpt")) {
|
|
132
|
-
return
|
|
133
|
-
gpt: currentModel,
|
|
134
|
-
fast: "gpt",
|
|
135
|
-
strong: "gpt",
|
|
136
|
-
mini: "gpt",
|
|
137
|
-
codex: "gpt",
|
|
138
|
-
coder: "gpt",
|
|
139
|
-
resolver: "gpt",
|
|
140
|
-
explorer: "gpt",
|
|
141
|
-
reviewer: "gpt",
|
|
142
|
-
"deep-reviewer": "gpt",
|
|
143
|
-
}
|
|
203
|
+
return buildGPTOnlyPreset(currentModel)
|
|
144
204
|
}
|
|
145
205
|
|
|
146
206
|
// Unknown provider — keep model-neutral
|
|
147
207
|
return {}
|
|
148
208
|
}
|
|
149
209
|
|
|
210
|
+
function buildGLMOnlyPreset(model) {
|
|
211
|
+
return {
|
|
212
|
+
glm: model,
|
|
213
|
+
fast: "glm",
|
|
214
|
+
strong: "glm",
|
|
215
|
+
coder: "glm",
|
|
216
|
+
resolver: "glm",
|
|
217
|
+
reviewer: "glm",
|
|
218
|
+
"deep-reviewer": "glm",
|
|
219
|
+
explorer: "fast",
|
|
220
|
+
planner: "glm",
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function buildGPTOnlyPreset(model) {
|
|
225
|
+
return {
|
|
226
|
+
gpt: model,
|
|
227
|
+
fast: "gpt",
|
|
228
|
+
strong: "gpt",
|
|
229
|
+
mini: "gpt",
|
|
230
|
+
codex: "gpt",
|
|
231
|
+
coder: "gpt",
|
|
232
|
+
resolver: "gpt",
|
|
233
|
+
explorer: "gpt",
|
|
234
|
+
reviewer: "gpt",
|
|
235
|
+
"deep-reviewer": "gpt",
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
150
239
|
function getPresetLabel(currentModel) {
|
|
151
240
|
if (!currentModel) return "inherited"
|
|
152
241
|
const lower = currentModel.toLowerCase()
|
|
153
|
-
if (lower.includes("glm") || lower.includes("zai")) return "glm
|
|
242
|
+
if (lower.includes("glm") || lower.includes("zai")) return "glm-only"
|
|
154
243
|
if (lower.includes("openai/") || lower.includes("gpt")) return "gpt-only"
|
|
155
244
|
return "inherited"
|
|
156
245
|
}
|
|
157
246
|
|
|
247
|
+
function isGLMModel(currentModel) {
|
|
248
|
+
if (!currentModel) return false
|
|
249
|
+
const lower = currentModel.toLowerCase()
|
|
250
|
+
return lower.includes("glm") || lower.includes("zai")
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function isGPTModel(currentModel) {
|
|
254
|
+
if (!currentModel) return false
|
|
255
|
+
const lower = currentModel.toLowerCase()
|
|
256
|
+
return lower.includes("openai/") || lower.includes("gpt")
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function detectAllModels(config) {
|
|
260
|
+
const models = new Set()
|
|
261
|
+
|
|
262
|
+
// Top-level model
|
|
263
|
+
if (typeof config.model === "string" && config.model.length > 0) {
|
|
264
|
+
models.add(config.model)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Top-level models object values
|
|
268
|
+
if (isObject(config.models)) {
|
|
269
|
+
for (const value of Object.values(config.models)) {
|
|
270
|
+
if (typeof value === "string" && value.length > 0) {
|
|
271
|
+
models.add(value)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Provider model lists
|
|
277
|
+
if (isObject(config.provider)) {
|
|
278
|
+
for (const [providerId, providerConfig] of Object.entries(config.provider)) {
|
|
279
|
+
if (isObject(providerConfig) && isObject(providerConfig.models)) {
|
|
280
|
+
for (const [modelKey, modelEntry] of Object.entries(providerConfig.models)) {
|
|
281
|
+
if (typeof modelKey === "string" && modelKey.length > 0) {
|
|
282
|
+
models.add(qualifyModelId(providerId, modelKey))
|
|
283
|
+
}
|
|
284
|
+
if (typeof modelEntry === "string") {
|
|
285
|
+
models.add(qualifyModelId(providerId, modelEntry))
|
|
286
|
+
} else if (isObject(modelEntry) && typeof modelEntry.id === "string") {
|
|
287
|
+
models.add(qualifyModelId(providerId, modelEntry.id))
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Agent model values
|
|
295
|
+
if (isObject(config.agent)) {
|
|
296
|
+
for (const agentConfig of Object.values(config.agent)) {
|
|
297
|
+
if (isObject(agentConfig) && typeof agentConfig.model === "string" && agentConfig.model.length > 0) {
|
|
298
|
+
models.add(agentConfig.model)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return [...models]
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function qualifyModelId(providerId, modelId) {
|
|
307
|
+
if (typeof modelId !== "string" || modelId.length === 0) return modelId
|
|
308
|
+
if (modelId.includes("/")) return modelId
|
|
309
|
+
return `${providerId}/${modelId}`
|
|
310
|
+
}
|
|
311
|
+
|
|
158
312
|
async function migrateResolveConfig() {
|
|
159
313
|
let raw
|
|
160
314
|
try {
|
|
@@ -225,6 +379,28 @@ function addPlugin(config) {
|
|
|
225
379
|
return true
|
|
226
380
|
}
|
|
227
381
|
|
|
382
|
+
async function injectZAIMCPs(config) {
|
|
383
|
+
config.mcp ??= {}
|
|
384
|
+
if (!isObject(config.mcp)) return false
|
|
385
|
+
|
|
386
|
+
const added = []
|
|
387
|
+
for (const [name, mcpConfig] of Object.entries(ZAI_MCP_SERVERS)) {
|
|
388
|
+
if (config.mcp[name] === undefined) {
|
|
389
|
+
config.mcp[name] = mcpConfig
|
|
390
|
+
added.push(name)
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (added.length > 0) {
|
|
395
|
+
console.log(`[${packageName}] injected ZAI MCP server config: ${added.join(", ")}`)
|
|
396
|
+
console.log(`[${packageName}] note: API keys are not copied into opencode.json; export Z_AI_API_KEY if the MCP server requires it.`)
|
|
397
|
+
return true
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
console.log(`[${packageName}] ZAI MCP server already present — skipping`)
|
|
401
|
+
return false
|
|
402
|
+
}
|
|
403
|
+
|
|
228
404
|
function isRegisteredPluginEntry(entry) {
|
|
229
405
|
if (typeof entry === "string") return isResolvePluginName(entry)
|
|
230
406
|
if (Array.isArray(entry) && typeof entry[0] === "string") return isResolvePluginName(entry[0])
|
|
@@ -261,3 +437,107 @@ function isObject(value) {
|
|
|
261
437
|
function formatError(error) {
|
|
262
438
|
return error instanceof Error ? error.message : String(error)
|
|
263
439
|
}
|
|
440
|
+
|
|
441
|
+
async function offerCompanionPlugins() {
|
|
442
|
+
if (process.env.OPENCODE_RESOLVE_SKIP_COMPANIONS === "1") return
|
|
443
|
+
|
|
444
|
+
const config = await readOpenCodeConfig()
|
|
445
|
+
const existing = collectPluginBaseNames(config.plugin ?? [])
|
|
446
|
+
const missing = COMPANION_PLUGINS.filter((c) => !existing.has(c.pkg))
|
|
447
|
+
|
|
448
|
+
if (missing.length === 0) {
|
|
449
|
+
console.log(`[${packageName}] recommended companion plugins already present — skipping`)
|
|
450
|
+
return
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY)
|
|
454
|
+
|
|
455
|
+
if (!isInteractive) {
|
|
456
|
+
console.log(`[${packageName}] recommended companion plugins not detected:`)
|
|
457
|
+
for (const c of missing) {
|
|
458
|
+
console.log(` - ${c.pkg} — ${c.desc}`)
|
|
459
|
+
console.log(` install: opencode plugin ${c.pkg}@latest --global --force`)
|
|
460
|
+
}
|
|
461
|
+
return
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
465
|
+
try {
|
|
466
|
+
for (const c of missing) {
|
|
467
|
+
console.log("")
|
|
468
|
+
console.log(`[${packageName}] recommended companion: ${c.pkg}`)
|
|
469
|
+
console.log(` ${c.desc}`)
|
|
470
|
+
const raw = await rl.question(" install now? [Y/n] ")
|
|
471
|
+
const answer = raw.trim().toLowerCase()
|
|
472
|
+
const accepted = answer === "" || answer === "y" || answer === "yes"
|
|
473
|
+
if (!accepted) {
|
|
474
|
+
console.log(` skipped — install later via: opencode plugin ${c.pkg}@latest --global --force`)
|
|
475
|
+
continue
|
|
476
|
+
}
|
|
477
|
+
const installed = await installCompanion(c.pkg)
|
|
478
|
+
if (!installed) {
|
|
479
|
+
console.warn(` ${c.pkg} install command failed — leave plugin list untouched, retry manually`)
|
|
480
|
+
continue
|
|
481
|
+
}
|
|
482
|
+
await addCompanionToOpenCodeConfig(`${c.pkg}@latest`)
|
|
483
|
+
console.log(` ${c.pkg} cached and registered — restart OpenCode to activate`)
|
|
484
|
+
}
|
|
485
|
+
} finally {
|
|
486
|
+
rl.close()
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function collectPluginBaseNames(plugins) {
|
|
491
|
+
const names = new Set()
|
|
492
|
+
for (const entry of plugins) {
|
|
493
|
+
const raw = typeof entry === "string"
|
|
494
|
+
? entry
|
|
495
|
+
: Array.isArray(entry) && typeof entry[0] === "string"
|
|
496
|
+
? entry[0]
|
|
497
|
+
: null
|
|
498
|
+
if (!raw) continue
|
|
499
|
+
names.add(stripVersionSuffix(raw))
|
|
500
|
+
}
|
|
501
|
+
return names
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function stripVersionSuffix(name) {
|
|
505
|
+
if (name.startsWith("@")) {
|
|
506
|
+
const slashIndex = name.indexOf("/")
|
|
507
|
+
if (slashIndex === -1) return name
|
|
508
|
+
const scope = name.slice(0, slashIndex)
|
|
509
|
+
const rest = name.slice(slashIndex + 1)
|
|
510
|
+
return `${scope}/${rest.split("@")[0]}`
|
|
511
|
+
}
|
|
512
|
+
return name.split("@")[0]
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async function installCompanion(pkg) {
|
|
516
|
+
return new Promise((resolveSpawn) => {
|
|
517
|
+
const child = spawn("opencode", ["plugin", `${pkg}@latest`, "--global", "--force"], {
|
|
518
|
+
stdio: "inherit",
|
|
519
|
+
})
|
|
520
|
+
child.on("exit", (code) => resolveSpawn(code === 0))
|
|
521
|
+
child.on("error", () => resolveSpawn(false))
|
|
522
|
+
})
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async function addCompanionToOpenCodeConfig(pluginEntry) {
|
|
526
|
+
const config = await readOpenCodeConfig()
|
|
527
|
+
config.plugin ??= []
|
|
528
|
+
if (!Array.isArray(config.plugin)) {
|
|
529
|
+
throw new Error(`${opencodeConfigPath}.plugin must be an array`)
|
|
530
|
+
}
|
|
531
|
+
const baseName = stripVersionSuffix(pluginEntry)
|
|
532
|
+
const alreadyPresent = config.plugin.some((entry) => {
|
|
533
|
+
const raw = typeof entry === "string"
|
|
534
|
+
? entry
|
|
535
|
+
: Array.isArray(entry) && typeof entry[0] === "string"
|
|
536
|
+
? entry[0]
|
|
537
|
+
: null
|
|
538
|
+
return raw !== null && stripVersionSuffix(raw) === baseName
|
|
539
|
+
})
|
|
540
|
+
if (alreadyPresent) return
|
|
541
|
+
config.plugin.push(pluginEntry)
|
|
542
|
+
await writeFile(opencodeConfigPath, `${JSON.stringify(config, null, 2)}\n`)
|
|
543
|
+
}
|