pantheon-opencode 1.4.2 → 1.5.0-beta.1
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/.pantheon/code-mode/.gitkeep +0 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-1.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-10.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-11.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-2.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-3.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-4.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-5.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-6.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-7.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-8.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/checkpoint-9.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/heartbeat.json +8 -0
- package/.pantheon/code-mode/.pantheon/deepwork/initial-token-40k/session.json +11 -0
- package/.pantheon/code-mode/README.md +93 -0
- package/.pantheon/code-mode/checkpoint-session.py +7 -0
- package/.pantheon/code-mode/checkpoint-session.sh +11 -0
- package/.pantheon/code-mode/checkpoint_session.py +481 -0
- package/.pantheon/code-mode/compress-inline.py +450 -0
- package/.pantheon/code-mode/eval-llm-judge.py +255 -0
- package/.pantheon/code-mode/eval-monte-carlo.py +368 -0
- package/.pantheon/code-mode/eval-run.py +197 -0
- package/.pantheon/code-mode/eval-static.py +321 -0
- package/.pantheon/code-mode/example-sync.sh +5 -0
- package/.pantheon/code-mode/session-end-save.py +186 -0
- package/.pantheon/code-mode/session-end-save.sh +18 -0
- package/.pantheon/code-mode/test-allowed.sh +5 -0
- package/.pantheon/code-mode/test-auto-continue-checkpoint.sh +179 -0
- package/.pantheon/code-mode/test-env.sh +7 -0
- package/.pantheon/code-mode/test-prlimit.sh +9 -0
- package/.pantheon/code-mode/test-timeout.sh +6 -0
- package/README.md +158 -1189
- package/README.pt-BR.md +222 -0
- package/bin/pantheon-init.mjs +30 -11
- package/opencode.json +1 -0
- package/package.json +14 -3
- package/plugin.json +15 -0
- package/scripts/_pantheon_paths.py +48 -7
- package/scripts/ci-validate-yaml.py +7 -2
- package/scripts/code_mode_server.py +33 -2
- package/scripts/doctor.mjs +135 -127
- package/scripts/generate-preset-docs.mjs +127 -66
- package/scripts/generate-prompts.sh +2 -2
- package/scripts/install/cli-ui.mjs +8 -1
- package/scripts/install/config-migration.mjs +401 -0
- package/scripts/install/health-check.mjs +3 -3
- package/scripts/install/model-picker.mjs +72 -23
- package/scripts/install/opencode-version.mjs +47 -0
- package/scripts/install/opencode.mjs +261 -88
- package/scripts/install/plugin.mjs +112 -42
- package/scripts/install/shared.mjs +13 -158
- package/scripts/install-mcp.mjs +44 -232
- package/scripts/mcp_persistence_server.py +344 -15
- package/scripts/mcp_resources_server.py +144 -40
- package/scripts/memory_mcp_server.py +2 -2
- package/scripts/recover-zenodo-deposition.mjs +146 -0
- package/scripts/sync-tui.mjs +32 -7
- package/scripts/test-opencode-v1-v2-sandbox.sh +591 -0
- package/scripts/uninstall.mjs +298 -954
- package/scripts/validate-zenodo-release.mjs +154 -0
- package/scripts/zenodo-artifact.mjs +21 -0
- package/scripts/zenodo-release-state.mjs +53 -0
- package/src/mcp/_pantheon_paths.py +48 -7
- package/src/mcp/code_mode_server.py +33 -2
- package/src/mcp/eval_store.py +1 -1
- package/src/mcp/mcp_persistence_server.py +344 -15
- package/src/mcp/mcp_resources_server.py +144 -40
- package/src/mcp/pantheon_vision_server.py +1 -1
- package/src/mcp/toon_codec.py +491 -0
- package/src/pantheon/background-job-board.ts +67 -1
- package/src/pantheon/cost-command.ts +6 -2
- package/src/pantheon/delegate-manager.ts +384 -0
- package/src/pantheon/delegate-task-adapter.ts +444 -0
- package/src/pantheon/delegation-notify.ts +35 -20
- package/src/pantheon/model-command.ts +86 -32
- package/src/pantheon/token-opt.ts +555 -0
- package/src/pantheon/toon-codec.ts +481 -0
- package/src/pantheon/tui-version-check.ts +1 -3
- package/src/pantheon/v2-bridge.ts +129 -0
- package/src/pantheon/v2-events.ts +131 -0
- package/src/pantheon/v2-hooks.ts +287 -0
- package/src/pantheon/v2-tool-definitions.ts +331 -0
- package/src/plugin-v2/index.ts +21 -0
- package/src/plugin-v2.ts +836 -0
- package/src/plugin.ts +57 -10
- package/src/plugins/tui/dist/server.d.ts +1 -1
- package/src/plugins/tui/dist/server.d.ts.map +1 -1
- package/src/plugins/tui/dist/server.js +1 -1
- package/src/plugins/tui/dist/server.js.map +1 -1
- package/src/plugins/tui/dist/tui.d.ts +10 -7
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +11 -8
- package/src/plugins/tui/dist/tui.js.map +1 -1
- package/src/plugins/tui/dist/tui.tsx +22 -16
- package/src/plugins/tui/package-lock.json +51 -51
- package/src/plugins/tui/package.json +7 -7
- package/src/plugins/tui/src/index.tsx +22 -16
- package/src/skills/README.md +0 -2
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config-migration.mjs — Bidirectional V1↔V2 config format migration
|
|
3
|
+
*
|
|
4
|
+
* Converts opencode.json between:
|
|
5
|
+
* - V1 (object-based): `permission`, `agent`, `provider`, `mcp` with flat keys
|
|
6
|
+
* - V2 (array-based): `permissions`, `agents`, `providers`; MCP remains the
|
|
7
|
+
* named top-level server map because OpenCode 1.18.x validates that shape for
|
|
8
|
+
* both the V1 and V2 installer paths.
|
|
9
|
+
*
|
|
10
|
+
* Rules:
|
|
11
|
+
* 1. Deep clone before mutating — never modifies input
|
|
12
|
+
* 2. Unknown fields pass through untouched
|
|
13
|
+
* 3. console.warn for ambiguities
|
|
14
|
+
* 4. Handles nested objects recursively
|
|
15
|
+
*
|
|
16
|
+
* @module config-migration
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Helpers
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/** Deep clone a JSON-serializable value */
|
|
24
|
+
function deepClone(obj) {
|
|
25
|
+
return JSON.parse(JSON.stringify(obj))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// V1 → V2 helpers
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
/** Action name mapping for permissions (V1 key → V2 action) */
|
|
33
|
+
const V1_PERMISSION_TO_ACTION = {
|
|
34
|
+
bash: 'shell',
|
|
35
|
+
skill: 'skill',
|
|
36
|
+
edit: 'edit',
|
|
37
|
+
websearch: 'websearch',
|
|
38
|
+
// passthrough unknowns
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Convert V1 permission object to V2 permissions array.
|
|
43
|
+
*
|
|
44
|
+
* V1 shape:
|
|
45
|
+
* { bash: { "git *": "allow" }, edit: "allow", skill: { "*": "allow" }, websearch: "deny" }
|
|
46
|
+
*
|
|
47
|
+
* V2 shape:
|
|
48
|
+
* [ { action: "shell", resource: "git *", effect: "allow" }, ... ]
|
|
49
|
+
*/
|
|
50
|
+
function convertPermissionsV1toV2(permObj) {
|
|
51
|
+
const result = []
|
|
52
|
+
for (const [key, value] of Object.entries(permObj)) {
|
|
53
|
+
const action = V1_PERMISSION_TO_ACTION[key] || key
|
|
54
|
+
if (typeof value === 'string') {
|
|
55
|
+
// Simple: { edit: "allow" } → [{ action: "edit", resource: "*", effect: "allow" }]
|
|
56
|
+
result.push({ action, resource: '*', effect: value })
|
|
57
|
+
} else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
58
|
+
// Nested: { bash: { "git *": "allow" } } → [{ action: "shell", resource: "git *", effect: "allow" }]
|
|
59
|
+
for (const [resource, effect] of Object.entries(value)) {
|
|
60
|
+
result.push({ action, resource, effect })
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
console.warn(`[config-migration] Unexpected permission value for "${key}":`, value)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return result
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Convert V2 permissions array back to V1 permission object.
|
|
71
|
+
*/
|
|
72
|
+
function convertPermissionsV2toV1(permArray) {
|
|
73
|
+
const result = {}
|
|
74
|
+
// Reverse action mapping: V2 action → V1 key
|
|
75
|
+
const ACTION_TO_V1 = Object.fromEntries(
|
|
76
|
+
Object.entries(V1_PERMISSION_TO_ACTION).map(([k, v]) => [v, k])
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
for (const { action, resource, effect } of permArray) {
|
|
80
|
+
const v1Key = ACTION_TO_V1[action] || action
|
|
81
|
+
if (resource === '*') {
|
|
82
|
+
result[v1Key] = effect
|
|
83
|
+
} else {
|
|
84
|
+
if (!result[v1Key] || typeof result[v1Key] === 'string') {
|
|
85
|
+
// Convert scalar to object
|
|
86
|
+
if (typeof result[v1Key] === 'string') {
|
|
87
|
+
const prevEffect = result[v1Key]
|
|
88
|
+
result[v1Key] = { '*': prevEffect }
|
|
89
|
+
} else {
|
|
90
|
+
result[v1Key] = {}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
result[v1Key][resource] = effect
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return result
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Provider name consolidation: V1 → V2 */
|
|
100
|
+
const PROVIDER_RENAME = {
|
|
101
|
+
'azure-cognitive-services': 'azure',
|
|
102
|
+
'google-vertex-anthropic': 'google-vertex',
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Reverse: V2 → V1 */
|
|
106
|
+
const PROVIDER_RENAME_REVERSE = Object.fromEntries(
|
|
107
|
+
Object.entries(PROVIDER_RENAME).map(([k, v]) => [v, k])
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Convert a single V1 provider config to V2.
|
|
112
|
+
*
|
|
113
|
+
* V1 shape:
|
|
114
|
+
* { npm: "ai-sdk-openai", api: "https://...", options: { apiKey: "..." }, models: { ... } }
|
|
115
|
+
*
|
|
116
|
+
* V2 shape:
|
|
117
|
+
* { package: "aisdk:ai-sdk-openai", settings: { baseURL: "...", apiKey: "..." }, models: { ... } }
|
|
118
|
+
*/
|
|
119
|
+
function convertProviderV1toV2(name, config) {
|
|
120
|
+
const result = { ...config }
|
|
121
|
+
|
|
122
|
+
// npm → package (with aisdk: prefix)
|
|
123
|
+
if (result.npm !== undefined) {
|
|
124
|
+
const pkg = result.npm.startsWith('aisdk:') ? result.npm : `aisdk:${result.npm}`
|
|
125
|
+
result.package = pkg
|
|
126
|
+
delete result.npm
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// api → settings.baseURL
|
|
130
|
+
if (result.api !== undefined) {
|
|
131
|
+
if (!result.settings) result.settings = {}
|
|
132
|
+
result.settings.baseURL = result.api
|
|
133
|
+
delete result.api
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// options.apiKey → settings.apiKey
|
|
137
|
+
if (result.options?.apiKey !== undefined) {
|
|
138
|
+
if (!result.settings) result.settings = {}
|
|
139
|
+
result.settings.apiKey = result.options.apiKey
|
|
140
|
+
const { apiKey, ...restOptions } = result.options
|
|
141
|
+
if (Object.keys(restOptions).length > 0) {
|
|
142
|
+
result.options = restOptions
|
|
143
|
+
} else {
|
|
144
|
+
delete result.options
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Model variant: { variant: "high" } → key suffix #high
|
|
149
|
+
// Also rename model-level "attachment" → "media"
|
|
150
|
+
if (result.models) {
|
|
151
|
+
const newModels = {}
|
|
152
|
+
for (const [modelKey, modelConfig] of Object.entries(result.models)) {
|
|
153
|
+
const { variant, attachment, ...restConfig } = modelConfig || {}
|
|
154
|
+
const finalConfig = { ...restConfig }
|
|
155
|
+
if (attachment !== undefined) {
|
|
156
|
+
finalConfig.media = attachment
|
|
157
|
+
}
|
|
158
|
+
if (variant) {
|
|
159
|
+
const newKey = `${modelKey}#${variant}`
|
|
160
|
+
newModels[newKey] = finalConfig
|
|
161
|
+
} else {
|
|
162
|
+
newModels[modelKey] = finalConfig
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
result.models = newModels
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return result
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Convert a single V2 provider config back to V1.
|
|
173
|
+
*/
|
|
174
|
+
function convertProviderV2toV1(name, config) {
|
|
175
|
+
const result = { ...config }
|
|
176
|
+
|
|
177
|
+
// package → npm (strip aisdk: prefix)
|
|
178
|
+
if (result.package !== undefined) {
|
|
179
|
+
result.npm = result.package.startsWith('aisdk:')
|
|
180
|
+
? result.package.slice(6)
|
|
181
|
+
: result.package
|
|
182
|
+
delete result.package
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// settings.baseURL → api
|
|
186
|
+
if (result.settings?.baseURL !== undefined) {
|
|
187
|
+
result.api = result.settings.baseURL
|
|
188
|
+
const { baseURL, ...restSettings } = result.settings
|
|
189
|
+
if (Object.keys(restSettings).length > 0) {
|
|
190
|
+
result.settings = restSettings
|
|
191
|
+
} else {
|
|
192
|
+
delete result.settings
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// settings.apiKey → options.apiKey
|
|
197
|
+
if (result.settings?.apiKey !== undefined) {
|
|
198
|
+
if (!result.options) result.options = {}
|
|
199
|
+
result.options.apiKey = result.settings.apiKey
|
|
200
|
+
const { apiKey, ...restSettings } = result.settings || {}
|
|
201
|
+
if (Object.keys(restSettings).length > 0) {
|
|
202
|
+
result.settings = restSettings
|
|
203
|
+
} else {
|
|
204
|
+
delete result.settings
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Model key with #suffix → variant field
|
|
209
|
+
// Also rename model-level "media" → "attachment"
|
|
210
|
+
if (result.models) {
|
|
211
|
+
const newModels = {}
|
|
212
|
+
for (const [modelKey, modelConfig] of Object.entries(result.models)) {
|
|
213
|
+
const { media, ...restConfig } = modelConfig || {}
|
|
214
|
+
const finalConfig = { ...restConfig }
|
|
215
|
+
if (media !== undefined) {
|
|
216
|
+
finalConfig.attachment = media
|
|
217
|
+
}
|
|
218
|
+
if (modelKey.includes('#')) {
|
|
219
|
+
const [baseName, variant] = modelKey.split('#', 2)
|
|
220
|
+
newModels[baseName] = { ...finalConfig, variant }
|
|
221
|
+
} else {
|
|
222
|
+
newModels[modelKey] = finalConfig
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
result.models = newModels
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return result
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Convert V1 MCP config to V2.
|
|
233
|
+
*
|
|
234
|
+
* V1: { bifrost: { type: "remote", url: "...", enabled: true, timeout: 30000 } }
|
|
235
|
+
* V2 installer output: { bifrost: { type: "remote", url: "...", enabled: true } }
|
|
236
|
+
*
|
|
237
|
+
* OpenCode 1.18.18 does not have an `mcp.servers` wrapper. It interprets
|
|
238
|
+
* `servers` as the name of an MCP server and then rejects it with
|
|
239
|
+
* `Missing key mcp.servers.enabled`. Keep MCP in the documented shape and
|
|
240
|
+
* normalize old generated V2 entries back to it.
|
|
241
|
+
*/
|
|
242
|
+
function convertMcpV1toV2(mcpConfig) {
|
|
243
|
+
if (!mcpConfig || typeof mcpConfig !== 'object') return mcpConfig
|
|
244
|
+
|
|
245
|
+
const servers = {}
|
|
246
|
+
const source =
|
|
247
|
+
mcpConfig.servers && typeof mcpConfig.servers === 'object' && !Array.isArray(mcpConfig.servers)
|
|
248
|
+
? { ...mcpConfig, ...mcpConfig.servers }
|
|
249
|
+
: mcpConfig
|
|
250
|
+
|
|
251
|
+
for (const [name, serverConfig] of Object.entries(source)) {
|
|
252
|
+
if (name === 'servers') continue
|
|
253
|
+
// Installer versions that seeded the legacy wrapper also wrote a boolean
|
|
254
|
+
// feature toggle (`servers: { enabled: true }`). After unwrapping, that
|
|
255
|
+
// toggle surfaces as a non-object entry and is not a server; drop it.
|
|
256
|
+
if (serverConfig === null || typeof serverConfig !== 'object') continue
|
|
257
|
+
const srv = { ...serverConfig }
|
|
258
|
+
|
|
259
|
+
// Normalize legacy V2's disabled flag to OpenCode 1.18.x's enabled flag.
|
|
260
|
+
if ('disabled' in srv) {
|
|
261
|
+
srv.enabled = !srv.disabled
|
|
262
|
+
delete srv.disabled
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Normalize legacy V2's split timeout to the V1/V2-compatible number.
|
|
266
|
+
if (srv.timeout !== undefined && srv.timeout !== null) {
|
|
267
|
+
if (typeof srv.timeout === 'object') {
|
|
268
|
+
srv.timeout = srv.timeout.execution || srv.timeout.catalog || 30000
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// A shorthand inherited-server override is valid only when named directly.
|
|
273
|
+
// Runtime MCP entries always have a complete config, so leave other values
|
|
274
|
+
// untouched and avoid dropping user-owned servers.
|
|
275
|
+
servers[name] = srv
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return servers
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Convert V2 MCP config back to V1.
|
|
283
|
+
*/
|
|
284
|
+
function convertMcpV2toV1(mcpConfig) {
|
|
285
|
+
if (!mcpConfig || typeof mcpConfig !== 'object') return mcpConfig
|
|
286
|
+
|
|
287
|
+
return convertMcpV1toV2(mcpConfig)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
// Top-level key mappings
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
|
|
294
|
+
const V1_TO_V2_RENAMES = {
|
|
295
|
+
provider: 'providers',
|
|
296
|
+
agent: 'agents',
|
|
297
|
+
command: 'commands',
|
|
298
|
+
reference: 'references',
|
|
299
|
+
snapshot: 'snapshots',
|
|
300
|
+
attachment: 'media',
|
|
301
|
+
permission: 'permissions',
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Build reverse map */
|
|
305
|
+
const V2_TO_V1_RENAMES = Object.fromEntries(
|
|
306
|
+
Object.entries(V1_TO_V2_RENAMES).map(([k, v]) => [v, k])
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
// Public API
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Migrate a V1 opencode.json config to V2 format.
|
|
315
|
+
*
|
|
316
|
+
* @param {object} config - V1 config object (not mutated)
|
|
317
|
+
* @returns {object} V2 config object
|
|
318
|
+
*/
|
|
319
|
+
export function migrateV1toV2(config) {
|
|
320
|
+
const c = deepClone(config)
|
|
321
|
+
const result = {}
|
|
322
|
+
|
|
323
|
+
for (const [key, value] of Object.entries(c)) {
|
|
324
|
+
const v2Key = V1_TO_V2_RENAMES[key] || key
|
|
325
|
+
|
|
326
|
+
if (key === 'permission' && typeof value === 'object' && value !== null) {
|
|
327
|
+
result.permissions = convertPermissionsV1toV2(value)
|
|
328
|
+
} else if (key === 'agent' && typeof value === 'object' && value !== null) {
|
|
329
|
+
const agents = {}
|
|
330
|
+
for (const [agentName, agentConfig] of Object.entries(value)) {
|
|
331
|
+
const ac = { ...agentConfig }
|
|
332
|
+
if (ac.permission && typeof ac.permission === 'object') {
|
|
333
|
+
ac.permissions = convertPermissionsV1toV2(ac.permission)
|
|
334
|
+
delete ac.permission
|
|
335
|
+
}
|
|
336
|
+
agents[agentName] = ac
|
|
337
|
+
}
|
|
338
|
+
result.agents = agents
|
|
339
|
+
} else if (key === 'provider' && typeof value === 'object' && value !== null) {
|
|
340
|
+
const providers = {}
|
|
341
|
+
for (const [provName, provConfig] of Object.entries(value)) {
|
|
342
|
+
const renamed = PROVIDER_RENAME[provName] || provName
|
|
343
|
+
providers[renamed] = convertProviderV1toV2(provName, provConfig)
|
|
344
|
+
}
|
|
345
|
+
result.providers = providers
|
|
346
|
+
} else if (key === 'mcp' && typeof value === 'object' && value !== null) {
|
|
347
|
+
result.mcp = convertMcpV1toV2(value)
|
|
348
|
+
} else if (v2Key !== key) {
|
|
349
|
+
result[v2Key] = value
|
|
350
|
+
} else {
|
|
351
|
+
result[key] = value
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return result
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Migrate a V2 opencode.json config back to V1 format.
|
|
360
|
+
*
|
|
361
|
+
* @param {object} config - V2 config object (not mutated)
|
|
362
|
+
* @returns {object} V1 config object
|
|
363
|
+
*/
|
|
364
|
+
export function migrateV2toV1(config) {
|
|
365
|
+
const c = deepClone(config)
|
|
366
|
+
const result = {}
|
|
367
|
+
|
|
368
|
+
for (const [key, value] of Object.entries(c)) {
|
|
369
|
+
const v1Key = V2_TO_V1_RENAMES[key] || key
|
|
370
|
+
|
|
371
|
+
if (key === 'permissions' && Array.isArray(value)) {
|
|
372
|
+
result.permission = convertPermissionsV2toV1(value)
|
|
373
|
+
} else if (key === 'agents' && typeof value === 'object' && value !== null) {
|
|
374
|
+
const agents = {}
|
|
375
|
+
for (const [agentName, agentConfig] of Object.entries(value)) {
|
|
376
|
+
const ac = { ...agentConfig }
|
|
377
|
+
if (Array.isArray(ac.permissions)) {
|
|
378
|
+
ac.permission = convertPermissionsV2toV1(ac.permissions)
|
|
379
|
+
delete ac.permissions
|
|
380
|
+
}
|
|
381
|
+
agents[agentName] = ac
|
|
382
|
+
}
|
|
383
|
+
result.agent = agents
|
|
384
|
+
} else if (key === 'providers' && typeof value === 'object' && value !== null) {
|
|
385
|
+
const providers = {}
|
|
386
|
+
for (const [provName, provConfig] of Object.entries(value)) {
|
|
387
|
+
const renamed = PROVIDER_RENAME_REVERSE[provName] || provName
|
|
388
|
+
providers[renamed] = convertProviderV2toV1(provName, provConfig)
|
|
389
|
+
}
|
|
390
|
+
result.provider = providers
|
|
391
|
+
} else if (key === 'mcp' && typeof value === 'object' && value !== null) {
|
|
392
|
+
result.mcp = convertMcpV2toV1(value)
|
|
393
|
+
} else if (v1Key !== key) {
|
|
394
|
+
result[v1Key] = value
|
|
395
|
+
} else {
|
|
396
|
+
result[key] = value
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return result
|
|
401
|
+
}
|
|
@@ -18,12 +18,12 @@ import { join } from 'node:path'
|
|
|
18
18
|
* NON-FATAL — all checks are reported, none throw.
|
|
19
19
|
*
|
|
20
20
|
* @param {string} target - Installation directory
|
|
21
|
-
* @param {{ dryRun?: boolean }} [options]
|
|
21
|
+
* @param {{ dryRun?: boolean; pythonTarget?: string }} [options]
|
|
22
22
|
* @returns {{ passed: Array<{check: string, detail: string}>, failed: Array<{check: string, detail: string}>, warnings: Array<{check: string, detail: string}> }}
|
|
23
23
|
*/
|
|
24
|
-
export function healthCheck(target, { dryRun = false } = {}) {
|
|
24
|
+
export function healthCheck(target, { dryRun = false, pythonTarget = target } = {}) {
|
|
25
25
|
const results = { passed: [], failed: [], warnings: [] }
|
|
26
|
-
const python = findPython(
|
|
26
|
+
const python = findPython(pythonTarget)
|
|
27
27
|
const visionServer = resolveVisionServer(target)
|
|
28
28
|
|
|
29
29
|
// Check 1: Critical runtime scripts exist
|
|
@@ -14,7 +14,14 @@
|
|
|
14
14
|
* "herdar do chat" (inherit) é o default da Q1: não grava active-preset.json,
|
|
15
15
|
* então os delegates herdam o modelo do chat pai (herança nativa, sem preset).
|
|
16
16
|
*/
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
copyFileSync,
|
|
19
|
+
existsSync,
|
|
20
|
+
mkdirSync,
|
|
21
|
+
readFileSync,
|
|
22
|
+
renameSync,
|
|
23
|
+
writeFileSync,
|
|
24
|
+
} from 'node:fs'
|
|
18
25
|
import { homedir } from 'node:os'
|
|
19
26
|
import { join } from 'node:path'
|
|
20
27
|
import { createInterface } from 'node:readline/promises'
|
|
@@ -22,10 +29,22 @@ import { loadPresetDefs } from '../../src/pantheon/presets.mjs'
|
|
|
22
29
|
|
|
23
30
|
/** Preset pricing table (2026 verified). */
|
|
24
31
|
export const PRESET_PRICE = {
|
|
25
|
-
'go-free': {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
'go-free': {
|
|
33
|
+
label: 'Gratuito',
|
|
34
|
+
detail: 'Zen free – sem custo, quota limitada, só modelos -free',
|
|
35
|
+
},
|
|
36
|
+
'go-fast': {
|
|
37
|
+
label: 'Baixo custo',
|
|
38
|
+
detail: 'Go gateway – baixa latência, kimi-k2.7-code / glm-5.3-flash',
|
|
39
|
+
},
|
|
40
|
+
'go-premium': {
|
|
41
|
+
label: 'Premium',
|
|
42
|
+
detail: 'Go gateway – melhor qualidade, gpt-5.6-sol / qwen3.8-max / deepseek-v4-pro',
|
|
43
|
+
},
|
|
44
|
+
openai: {
|
|
45
|
+
label: 'Pago (OpenAI)',
|
|
46
|
+
detail: 'Direto OpenAI – https://api.openai.com/v1, gpt-5.6 family',
|
|
47
|
+
},
|
|
29
48
|
}
|
|
30
49
|
|
|
31
50
|
/**
|
|
@@ -37,7 +56,9 @@ export const PRESET_PRICE = {
|
|
|
37
56
|
export function requiredKeyEnvForPreset(name, def) {
|
|
38
57
|
if (name === 'openai') return 'OPENAI_API_KEY'
|
|
39
58
|
// all Go presets use PANTHEON_OPENCODE_API_KEY (alias OPENCODE_GO_API_KEY)
|
|
40
|
-
const providerEnvs = Object.values(def?.providers ?? {})
|
|
59
|
+
const providerEnvs = Object.values(def?.providers ?? {})
|
|
60
|
+
.map((p) => p.apiKeyEnv)
|
|
61
|
+
.filter(Boolean)
|
|
41
62
|
if (providerEnvs.includes('PANTHEON_OPENCODE_API_KEY')) return 'PANTHEON_OPENCODE_API_KEY'
|
|
42
63
|
if (providerEnvs.includes('OPENAI_API_KEY')) return 'OPENAI_API_KEY'
|
|
43
64
|
return 'PANTHEON_OPENCODE_API_KEY'
|
|
@@ -134,8 +155,14 @@ export function buildInitQuestions(presetDefs) {
|
|
|
134
155
|
header: 'Escopo',
|
|
135
156
|
question: 'Onde salvar o preset ativo?',
|
|
136
157
|
options: [
|
|
137
|
-
{
|
|
138
|
-
|
|
158
|
+
{
|
|
159
|
+
label: 'project',
|
|
160
|
+
description: './.pantheon/active-preset.json (escopo do projeto, seguro)',
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
label: 'global',
|
|
164
|
+
description: '~/.config/opencode/.pantheon/active-preset.json (global)',
|
|
165
|
+
},
|
|
139
166
|
],
|
|
140
167
|
multiSelect: false,
|
|
141
168
|
},
|
|
@@ -238,7 +265,10 @@ export async function runModelPicker({
|
|
|
238
265
|
}
|
|
239
266
|
|
|
240
267
|
const list = names
|
|
241
|
-
.map(
|
|
268
|
+
.map(
|
|
269
|
+
(n, i) =>
|
|
270
|
+
` ${i + 1}) ${n} — ${defs[n].description ?? ''} | ${buildPresetTableRow(n, defs[n])}`,
|
|
271
|
+
)
|
|
242
272
|
.join('\n')
|
|
243
273
|
const rli = rl ?? createInterface({ input: process.stdin, output: process.stdout })
|
|
244
274
|
try {
|
|
@@ -319,7 +349,7 @@ export async function runInitWizard({
|
|
|
319
349
|
} else {
|
|
320
350
|
// Fallback: readline 3-step wizard
|
|
321
351
|
const rli = rl ?? createInterface({ input: process.stdin, output: process.stdout })
|
|
322
|
-
|
|
352
|
+
const closeAfter = !rl
|
|
323
353
|
try {
|
|
324
354
|
// Q1: perfil (0 = herdar do chat, default)
|
|
325
355
|
const list = [
|
|
@@ -353,20 +383,22 @@ export async function runInitWizard({
|
|
|
353
383
|
// Q2: key masked
|
|
354
384
|
const requiredEnv = requiredKeyEnvForPreset(presetName, defs[presetName])
|
|
355
385
|
const aliasNote =
|
|
356
|
-
requiredEnv === 'PANTHEON_OPENCODE_API_KEY'
|
|
357
|
-
? ' (alias OPENCODE_GO_API_KEY aceito)'
|
|
358
|
-
: ''
|
|
386
|
+
requiredEnv === 'PANTHEON_OPENCODE_API_KEY' ? ' (alias OPENCODE_GO_API_KEY aceito)' : ''
|
|
359
387
|
const envConfigured = isKeyConfiguredForPreset(env, presetName, defs[presetName])
|
|
360
388
|
let keyAnswer = ''
|
|
361
389
|
if (envConfigured) {
|
|
362
|
-
logger.log?.(
|
|
390
|
+
logger.log?.(
|
|
391
|
+
`Chave ${requiredEnv}${aliasNote} já configurada no ambiente (mascarada: ${maskKey(env[requiredEnv] ?? env.OPENCODE_GO_API_KEY ?? '')}).`,
|
|
392
|
+
)
|
|
363
393
|
} else {
|
|
364
394
|
// masked collection
|
|
365
395
|
const prompt = `[2/3] Informe ${requiredEnv}${aliasNote} (entrada mascarada, não escreve .env): `
|
|
366
396
|
// Use questionMasked for tests; real TTY would mask
|
|
367
397
|
keyAnswer = (await questionMasked(rli, prompt)).trim()
|
|
368
398
|
if (keyAnswer === '') {
|
|
369
|
-
logger.warn?.(
|
|
399
|
+
logger.warn?.(
|
|
400
|
+
`Chave vazia para ${requiredEnv} — wizard canceled. Preencha env ${requiredEnv} antes de continuar.`,
|
|
401
|
+
)
|
|
370
402
|
return null
|
|
371
403
|
}
|
|
372
404
|
// validate non-empty already; optionally check length
|
|
@@ -374,7 +406,9 @@ export async function runInitWizard({
|
|
|
374
406
|
// For health-check, set in current env (not .env file) for this session
|
|
375
407
|
// Prefer primary var
|
|
376
408
|
env[requiredEnv] = keyAnswer
|
|
377
|
-
logger.log?.(
|
|
409
|
+
logger.log?.(
|
|
410
|
+
`Chave coletada (mascarada: ${maskKey(keyAnswer)}). Defina export ${requiredEnv}='${maskKey(keyAnswer)}' no seu shell para persistir (não escrevemos .env).`,
|
|
411
|
+
)
|
|
378
412
|
}
|
|
379
413
|
}
|
|
380
414
|
|
|
@@ -398,8 +432,14 @@ export async function runInitWizard({
|
|
|
398
432
|
})()
|
|
399
433
|
const dir = scope === 'global' ? globalDir : projectDir
|
|
400
434
|
if (!dryRun) {
|
|
401
|
-
const result = writeActivePreset(dir, presetName, {
|
|
402
|
-
|
|
435
|
+
const result = writeActivePreset(dir, presetName, {
|
|
436
|
+
dryRun: false,
|
|
437
|
+
source: 'interactive',
|
|
438
|
+
logger,
|
|
439
|
+
})
|
|
440
|
+
logger.log?.(
|
|
441
|
+
`Preset ${presetName} salvo em ${scope} (${result.path})${result.backupPath ? ` backup: ${result.backupPath}` : ''}`,
|
|
442
|
+
)
|
|
403
443
|
// health-check: verify file
|
|
404
444
|
try {
|
|
405
445
|
const raw = readFileSync(result.path, 'utf8')
|
|
@@ -423,15 +463,18 @@ export async function runInitWizard({
|
|
|
423
463
|
const a2 = answers[2]?.answer ?? answers[2]
|
|
424
464
|
presetName = a0 && typeof a0 === 'object' && 'label' in a0 ? a0.label : a0
|
|
425
465
|
keyAnswer = a1 && typeof a1 === 'object' && 'answer' in a1 ? a1.answer : a1
|
|
426
|
-
if (keyAnswer && typeof keyAnswer === 'object' && 'label' in keyAnswer)
|
|
466
|
+
if (keyAnswer && typeof keyAnswer === 'object' && 'label' in keyAnswer)
|
|
467
|
+
keyAnswer = keyAnswer.label
|
|
427
468
|
scope = a2 && typeof a2 === 'object' && 'label' in a2 ? a2.label : a2
|
|
428
469
|
if (scope && typeof scope === 'object' && 'answer' in scope) scope = scope.answer
|
|
429
470
|
} else if (answers && typeof answers === 'object') {
|
|
430
471
|
presetName = answers.preset ?? answers['0'] ?? answers.q1 ?? answers.presetName
|
|
431
472
|
keyAnswer = answers.key ?? answers['1'] ?? answers.q2
|
|
432
473
|
scope = answers.scope ?? answers['2'] ?? answers.q3
|
|
433
|
-
if (presetName && typeof presetName === 'object' && 'label' in presetName)
|
|
434
|
-
|
|
474
|
+
if (presetName && typeof presetName === 'object' && 'label' in presetName)
|
|
475
|
+
presetName = presetName.label
|
|
476
|
+
if (presetName && typeof presetName === 'object' && 'answer' in presetName)
|
|
477
|
+
presetName = presetName.answer
|
|
435
478
|
if (scope && typeof scope === 'object' && 'label' in scope) scope = scope.label
|
|
436
479
|
if (scope && typeof scope === 'object' && 'answer' in scope) scope = scope.answer
|
|
437
480
|
}
|
|
@@ -459,7 +502,9 @@ export async function runInitWizard({
|
|
|
459
502
|
}
|
|
460
503
|
if (keyAnswer && !dryRun) {
|
|
461
504
|
env[requiredEnv] = keyAnswer
|
|
462
|
-
logger.log?.(
|
|
505
|
+
logger.log?.(
|
|
506
|
+
`Chave coletada (mascarada: ${maskKey(String(keyAnswer))}). Não escrevemos .env; export ${requiredEnv} no shell para persistir.`,
|
|
507
|
+
)
|
|
463
508
|
}
|
|
464
509
|
const finalScope = scope === 'global' ? 'global' : 'project'
|
|
465
510
|
const projectDir = presetDir ?? target ?? process.cwd()
|
|
@@ -470,7 +515,11 @@ export async function runInitWizard({
|
|
|
470
515
|
})()
|
|
471
516
|
const dir = finalScope === 'global' ? globalDir : projectDir
|
|
472
517
|
if (!dryRun) {
|
|
473
|
-
const result = writeActivePreset(dir, presetName, {
|
|
518
|
+
const result = writeActivePreset(dir, presetName, {
|
|
519
|
+
dryRun: false,
|
|
520
|
+
source: 'interactive',
|
|
521
|
+
logger,
|
|
522
|
+
})
|
|
474
523
|
logger.log?.(`Preset ${presetName} salvo em ${finalScope} (${result.path})`)
|
|
475
524
|
try {
|
|
476
525
|
const raw = readFileSync(result.path, 'utf8')
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** OpenCode major-version selection shared by the CLI and installer. */
|
|
2
|
+
|
|
3
|
+
const VALID_VERSIONS = new Set(['v1', 'v2', 'auto'])
|
|
4
|
+
|
|
5
|
+
/** @param {string[]} args @returns {'v1'|'v2'|'auto'|null} */
|
|
6
|
+
export function parseOpenCodeVersion(args) {
|
|
7
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
8
|
+
const arg = args[index]
|
|
9
|
+
if (arg.startsWith('--opencode-version=')) return normalize(arg.split('=', 2)[1])
|
|
10
|
+
if (arg === '--opencode-version') return normalize(args[index + 1])
|
|
11
|
+
// --version v1|v2 was supported by 1.4.x. Bare --version remains the
|
|
12
|
+
// package version command and is deliberately not interpreted here.
|
|
13
|
+
if (arg.startsWith('--version=')) return normalize(arg.split('=', 2)[1])
|
|
14
|
+
if (arg === '--version' && args[index + 1]?.startsWith('v')) {
|
|
15
|
+
// Preserve bare --version/package-version compatibility, while making
|
|
16
|
+
// unsupported OpenCode selectors (for example v3) fail loudly.
|
|
17
|
+
return normalize(args[index + 1])
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalize(value) {
|
|
24
|
+
if (!VALID_VERSIONS.has(value)) {
|
|
25
|
+
throw new Error(`Invalid OpenCode version "${value}" — expected v1, v2, or auto`)
|
|
26
|
+
}
|
|
27
|
+
return value
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Resolve auto conservatively: only an explicit hint selects V2.
|
|
32
|
+
* @param {'v1'|'v2'|'auto'|null|undefined} requested
|
|
33
|
+
* @param {{env?: Record<string, string|undefined>, binary?: string}} [options]
|
|
34
|
+
* @returns {'v1'|'v2'}
|
|
35
|
+
*/
|
|
36
|
+
export function resolveOpenCodeVersion(requested = 'v1', options = {}) {
|
|
37
|
+
if (!VALID_VERSIONS.has(requested)) {
|
|
38
|
+
throw new Error(`Invalid OpenCode version "${requested}" — expected v1, v2, or auto`)
|
|
39
|
+
}
|
|
40
|
+
if (requested === 'v1' || requested === 'v2') return requested
|
|
41
|
+
const env = options.env ?? process.env
|
|
42
|
+
if (env.OPENCODE_VERSION === 'v1' || env.OPENCODE_VERSION === 'v2') {
|
|
43
|
+
return env.OPENCODE_VERSION
|
|
44
|
+
}
|
|
45
|
+
const binary = options.binary ?? env.OPENCODE_BIN ?? ''
|
|
46
|
+
return /(?:^|[\\/])opencode2(?:\.exe)?$/i.test(binary) ? 'v2' : 'v1'
|
|
47
|
+
}
|