pantheon-opencode 1.6.0-beta.7 → 1.6.0-beta.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.
- package/AGENTS.md +58 -15
- package/CHANGELOG.md +61 -94
- package/README.md +81 -103
- package/README.pt-BR.md +19 -37
- package/commands/pantheon-audit.md +7 -1
- package/commands/pantheon-consolidate.md +1 -1
- package/commands/pantheon-focus.md +17 -0
- package/package.json +1 -2
- package/plugin.json +2 -2
- package/pyproject.toml +1 -1
- package/scripts/build-agents-md.mjs +2 -3
- package/scripts/doctor.mjs +22 -104
- package/scripts/hooks/scan-secrets.sh +97 -0
- package/scripts/install/config-migration.mjs +9 -42
- package/scripts/install/interactive.mjs +1 -1
- package/scripts/install/opencode.mjs +174 -77
- package/scripts/install/plugin.mjs +43 -9
- package/scripts/install/strings.mjs +0 -8
- package/scripts/probe-pantheon-cost.mjs +3 -16
- package/scripts/sync-artifacts.mjs +1 -0
- package/scripts/sync-tui.mjs +24 -3
- package/scripts/test-opencode-v2-sandbox.sh +203 -786
- package/scripts/validate-sandbox-install.mjs +138 -0
- package/skills-lock.json +4 -5
- package/src/agents/aphrodite.md +2 -9
- package/src/agents/demeter.md +0 -9
- package/src/agents/gaia.md +1 -0
- package/src/agents/hephaestus.md +0 -9
- package/src/agents/hermes.md +2 -9
- package/src/agents/mnemosyne.md +18 -53
- package/src/agents/prometheus.md +0 -9
- package/src/agents/themis.md +0 -1
- package/src/agents/zeus.md +180 -19
- package/src/instructions/agent-return-format.instructions.md +48 -3
- package/src/instructions/memory-protocol.instructions.md +8 -9
- package/src/instructions/zeus-anti-stall.instructions.md +54 -6
- package/src/instructions/zeus-council-synthesis.instructions.md +71 -0
- package/src/instructions/zeus-memory-operations.instructions.md +4 -4
- package/src/instructions/zeus-timeout-retry.instructions.md +81 -7
- package/src/pantheon/v2-delegation-enforce.ts +2 -1
- package/src/plugin-v2.ts +251 -168
- package/src/plugins/pantheon-hooks.ts +35 -82
- package/src/plugins/tui/dist/tui-v2.d.ts +9 -0
- package/src/plugins/tui/dist/tui-v2.d.ts.map +1 -0
- package/src/plugins/tui/dist/tui-v2.js +151 -0
- package/src/plugins/tui/dist/tui-v2.js.map +1 -0
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +29 -53
- package/src/plugins/tui/index-v2.ts +6 -0
- package/src/plugins/tui/package-lock.json +5411 -1292
- package/src/plugins/tui/package.json +10 -7
- package/src/plugins/tui/src/index-v2.tsx +108 -0
- package/src/plugins/tui/src/index.tsx +28 -63
- package/src/plugins/tui/src/opencode-v2.d.ts +13 -0
- package/src/plugins/tui/tsdown.config.ts +2 -2
- package/src/plugins/tui/tui-v2.ts +2 -0
- package/src/plugins/tui/tui.ts +3 -0
- package/src/routing.yml +4 -2
- package/src/skills/README.md +7 -12
- package/src/skills/agent-coordination/SKILL.md +83 -13
- package/src/skills/auto-continue/SKILL.md +13 -14
- package/src/skills/context-compression/SKILL.md +26 -879
- package/src/skills/session-goal/SKILL.md +14 -5
- package/src/pantheon/secret-scanner.ts +0 -489
- package/src/plugins/tui/README.md +0 -14
- package/src/skills/council-synthesis/SKILL.md +0 -29
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
writeConfigWithBackup,
|
|
57
57
|
writeIfChanged,
|
|
58
58
|
} from './shared.mjs'
|
|
59
|
+
import { createInitialState, hasState, readState, setComponentState, writeState } from './state.mjs'
|
|
59
60
|
import { strings } from './strings.mjs'
|
|
60
61
|
|
|
61
62
|
import { setupVenv, venvPythonPath } from './venv.mjs'
|
|
@@ -135,6 +136,74 @@ export const MANAGED_FIELDS = Object.freeze([
|
|
|
135
136
|
'disable_model_invocation',
|
|
136
137
|
])
|
|
137
138
|
|
|
139
|
+
const PANTHEON_MCP_SERVERS = Object.freeze([
|
|
140
|
+
'pantheon-resources',
|
|
141
|
+
'pantheon-memory',
|
|
142
|
+
'pantheon-code-mode',
|
|
143
|
+
'pantheon-persistence',
|
|
144
|
+
'pantheon-vision',
|
|
145
|
+
])
|
|
146
|
+
|
|
147
|
+
function normalizeMcpPermissionSegment(value) {
|
|
148
|
+
return value.replace(/[^a-zA-Z0-9_]/g, '_')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Translate an agent's `mcp_tools` frontmatter into per-tool native
|
|
153
|
+
* permissions. V1 matches the original server-tool names; V2 normalizes
|
|
154
|
+
* punctuation to underscores. Emitting both spellings keeps the same source
|
|
155
|
+
* metadata valid for either host. Unlisted bundled Pantheon servers default to
|
|
156
|
+
* deny; unrelated user MCP servers are not touched.
|
|
157
|
+
*
|
|
158
|
+
* @param {Record<string, 'all' | string[]>} mcpTools
|
|
159
|
+
* @returns {Record<string, 'allow' | 'deny'>}
|
|
160
|
+
*/
|
|
161
|
+
export function deriveNativeMcpPermissions(mcpTools) {
|
|
162
|
+
if (mcpTools === null || typeof mcpTools !== 'object' || Array.isArray(mcpTools)) {
|
|
163
|
+
throw new TypeError('mcp_tools must be an object keyed by MCP server name')
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const servers = new Set([...PANTHEON_MCP_SERVERS, ...Object.keys(mcpTools)])
|
|
167
|
+
const permissions = {}
|
|
168
|
+
|
|
169
|
+
for (const server of servers) {
|
|
170
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(server)) {
|
|
171
|
+
throw new TypeError(`mcp_tools server name is invalid: ${server}`)
|
|
172
|
+
}
|
|
173
|
+
const tools = Object.hasOwn(mcpTools, server) ? mcpTools[server] : []
|
|
174
|
+
if (
|
|
175
|
+
tools !== 'all' &&
|
|
176
|
+
(!Array.isArray(tools) ||
|
|
177
|
+
tools.some((tool) => typeof tool !== 'string' || !/^[a-zA-Z0-9._-]+$/.test(tool)))
|
|
178
|
+
) {
|
|
179
|
+
throw new TypeError(`mcp_tools.${server} must be "all" or an array of tool names`)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const serverAliases = new Set([server, normalizeMcpPermissionSegment(server)])
|
|
183
|
+
for (const alias of serverAliases) permissions[`${alias}_*`] = 'deny'
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for (const server of servers) {
|
|
187
|
+
const tools = Object.hasOwn(mcpTools, server) ? mcpTools[server] : []
|
|
188
|
+
const serverAliases = new Set([server, normalizeMcpPermissionSegment(server)])
|
|
189
|
+
if (tools === 'all') {
|
|
190
|
+
for (const alias of serverAliases) permissions[`${alias}_*`] = 'allow'
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
for (const tool of tools) {
|
|
195
|
+
const toolAliases = new Set([tool, normalizeMcpPermissionSegment(tool)])
|
|
196
|
+
for (const serverAlias of serverAliases) {
|
|
197
|
+
for (const toolAlias of toolAliases) {
|
|
198
|
+
permissions[`${serverAlias}_${toolAlias}`] = 'allow'
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return permissions
|
|
205
|
+
}
|
|
206
|
+
|
|
138
207
|
function deepClone(value) {
|
|
139
208
|
return JSON.parse(JSON.stringify(value))
|
|
140
209
|
}
|
|
@@ -226,26 +295,23 @@ function mergeMissing(target, source) {
|
|
|
226
295
|
}
|
|
227
296
|
|
|
228
297
|
/**
|
|
229
|
-
* Resolve a plugin ref from the repository-root opencode.json to
|
|
230
|
-
* installed
|
|
298
|
+
* Resolve a plugin ref from the repository-root opencode.json to a path INSIDE
|
|
299
|
+
* the installed package.
|
|
231
300
|
*
|
|
232
301
|
* Only exact Pantheon plugin identities are resolved into the installed
|
|
233
302
|
* package. In particular, a third-party absolute path ending in
|
|
234
303
|
* `src/plugin.ts` or `src/plugins/pantheon-hooks.ts` is not evidence that the
|
|
235
|
-
* plugin belongs to Pantheon and must be preserved verbatim.
|
|
236
|
-
* of package-local paths; V2 uses an exact npm version so it is independent of
|
|
237
|
-
* the installer's global prefix or transient npx cache.
|
|
304
|
+
* plugin belongs to Pantheon and must be preserved verbatim.
|
|
238
305
|
*/
|
|
239
306
|
export function resolveInstalledPlugin(plugin) {
|
|
240
307
|
if (typeof plugin !== 'string') return plugin
|
|
241
308
|
const normalized = normalizePluginRef(plugin)
|
|
242
|
-
if (normalized === PANTHEON_V2_PLUGIN || normalized === PANTHEON_V2_LEGACY_FILE) {
|
|
243
|
-
return PANTHEON_V2_PACKAGE_SPEC
|
|
244
|
-
}
|
|
245
309
|
if (PANTHEON_PLUGIN_IDENTITIES.has(normalized)) return join(ROOT, normalized)
|
|
310
|
+
// Legacy V2 file ref (pre-directory-contract): migrate to the installed
|
|
311
|
+
// plugin directory so the beta loader accepts the entry.
|
|
312
|
+
if (normalized === PANTHEON_V2_LEGACY_FILE) return join(ROOT, PANTHEON_V2_PLUGIN)
|
|
246
313
|
const identity = managedPluginIdentity(plugin)
|
|
247
314
|
if (identity !== null && isAbsolute(normalized)) {
|
|
248
|
-
if (identity === PANTHEON_V2_PLUGIN) return PANTHEON_V2_PACKAGE_SPEC
|
|
249
315
|
return INSTALLED_PANTHEON_PLUGIN_PATHS.get(identity) ?? plugin
|
|
250
316
|
}
|
|
251
317
|
return plugin
|
|
@@ -267,14 +333,7 @@ const PANTHEON_V2_PLUGIN = 'src/plugin-v2'
|
|
|
267
333
|
const PANTHEON_V2_LEGACY_FILE = 'src/plugin-v2.ts'
|
|
268
334
|
const PANTHEON_V1_EXPORT = 'pantheon-opencode/plugin'
|
|
269
335
|
const PANTHEON_V2_EXPORT = 'pantheon-opencode/plugin-v2'
|
|
270
|
-
const
|
|
271
|
-
const PANTHEON_V2_PACKAGE_SPEC = `${PACKAGE_MANIFEST.name}@${PACKAGE_MANIFEST.version}`
|
|
272
|
-
// Installation origin probe: npm copies (global prefix, npx cache) always live
|
|
273
|
-
// under a `node_modules` directory; a repo checkout does not. From a checkout —
|
|
274
|
-
// or between a version bump and its publish — the V2 pin above names a version
|
|
275
|
-
// that may not exist on the npm registry yet, and the host cannot resolve it
|
|
276
|
-
// until publish. Advisory only: the install itself is structurally correct.
|
|
277
|
-
const RUNNING_FROM_NPM_INSTALL = import.meta.url.includes('/node_modules/')
|
|
336
|
+
const PANTHEON_PACKAGE_REF = /^pantheon-opencode(?:@[^/]+)?(?:\/(plugin|plugin-v2))?$/
|
|
278
337
|
const PANTHEON_PLUGIN_IDENTITIES = new Set([
|
|
279
338
|
PANTHEON_V1_PLUGIN,
|
|
280
339
|
PANTHEON_V1_HOOKS,
|
|
@@ -303,14 +362,22 @@ function managedPluginIdentity(ref) {
|
|
|
303
362
|
const normalized = normalizePluginRef(ref)
|
|
304
363
|
if (typeof normalized !== 'string') return null
|
|
305
364
|
|
|
365
|
+
// Older installs registered the package root itself (often pinned to a
|
|
366
|
+
// beta version) in OpenCode's plural `plugins` list. Treat all exact npm
|
|
367
|
+
// specs for this package as managed so an update removes the old cached
|
|
368
|
+
// copy before registering the current package path. Do not match similarly
|
|
369
|
+
// named third-party packages such as `pantheon-opencode-fork`.
|
|
370
|
+
const packageRef = normalized.match(PANTHEON_PACKAGE_REF)
|
|
371
|
+
if (packageRef !== null) {
|
|
372
|
+
if (packageRef[1] === 'plugin') return PANTHEON_V1_PLUGIN
|
|
373
|
+
return PANTHEON_V2_PLUGIN
|
|
374
|
+
}
|
|
375
|
+
|
|
306
376
|
if (PANTHEON_PLUGIN_IDENTITIES.has(normalized)) return normalized
|
|
307
377
|
if (normalized === PANTHEON_V2_LEGACY_FILE) return PANTHEON_V2_PLUGIN
|
|
308
378
|
if (normalized === PANTHEON_V1_EXPORT || normalized.startsWith(`${PANTHEON_V1_EXPORT}@`)) {
|
|
309
379
|
return PANTHEON_V1_PLUGIN
|
|
310
380
|
}
|
|
311
|
-
if (normalized === PACKAGE_MANIFEST.name || normalized.startsWith(`${PACKAGE_MANIFEST.name}@`)) {
|
|
312
|
-
return PANTHEON_V2_PLUGIN
|
|
313
|
-
}
|
|
314
381
|
if (normalized === PANTHEON_V2_EXPORT || normalized.startsWith(`${PANTHEON_V2_EXPORT}@`)) {
|
|
315
382
|
return PANTHEON_V2_PLUGIN
|
|
316
383
|
}
|
|
@@ -475,15 +542,14 @@ export function isGlobalConfigDir(target) {
|
|
|
475
542
|
* package; the loader must NEVER be pointed at that in-package dir (a package
|
|
476
543
|
* reinstall/upgrade replaces the dir under a live registration and leaves
|
|
477
544
|
* diverging copies loading). Instead the installer always copies it into the
|
|
478
|
-
* config dir's plugins/ dir and
|
|
545
|
+
* config dir's plugins/ dir and uses THAT copy:
|
|
479
546
|
*
|
|
480
|
-
* <configDir>/plugins/pantheon-tui (dist/ + package
|
|
547
|
+
* <configDir>/plugins/pantheon-tui (tui.ts + dist/ + package manifests)
|
|
481
548
|
*
|
|
482
|
-
*
|
|
483
|
-
*
|
|
484
|
-
* exports["./server"] → dist/server.js (no-op server() stub)
|
|
549
|
+
* V1 registers that copy through tui.json. V2 discovers the local plugin's
|
|
550
|
+
* index.ts + tui.ts pair; the V2 shim re-exports dist/tui-v2.js.
|
|
485
551
|
*
|
|
486
|
-
* @param {string} configDir -
|
|
552
|
+
* @param {string} configDir - OpenCode config directory (target for global
|
|
487
553
|
* installs, <target>/.opencode for project installs)
|
|
488
554
|
* @returns {string} absolute path of the copied plugin directory
|
|
489
555
|
*/
|
|
@@ -509,23 +575,20 @@ export function tuiConfigLocations(target) {
|
|
|
509
575
|
}
|
|
510
576
|
|
|
511
577
|
/**
|
|
512
|
-
* Sync the Pantheon TUI plugin
|
|
513
|
-
*
|
|
578
|
+
* Sync the Pantheon TUI plugin payload and V1 registration. V2 discovers the
|
|
579
|
+
* copied local-plugin entrypoints and does not use a project tui.json.
|
|
514
580
|
*
|
|
515
581
|
* Contract (single source of truth):
|
|
516
|
-
* 1. ALWAYS copy src/plugins/tui → <configDir>/plugins/pantheon-tui
|
|
517
|
-
*
|
|
518
|
-
* directory that exists and carries the loader contract (exports →
|
|
519
|
-
* dist/tui.js, dist/server.js). The copy is idempotent: a second run is
|
|
520
|
-
* byte-identical.
|
|
582
|
+
* 1. ALWAYS copy src/plugins/tui → <configDir>/plugins/pantheon-tui with the
|
|
583
|
+
* selected host entrypoints and compiled bundle. The copy is idempotent.
|
|
521
584
|
* 2. Clean stale Pantheon TUI refs from ALL tui.json locations OpenCode
|
|
522
585
|
* loads — ~/.opencode, $XDG_CONFIG_HOME/opencode and
|
|
523
586
|
* <target>/.opencode — covering every old reference shape: dist file
|
|
524
587
|
* paths, in-package src/plugins/tui dirs, bare plugins/pantheon-tui
|
|
525
588
|
* copies, and npx paths. A stale ref left in the non-target location
|
|
526
589
|
* would still be loaded and break the TUI.
|
|
527
|
-
* 3.
|
|
528
|
-
*
|
|
590
|
+
* 3. V1 registers the copied directory in the selected target. V2 relies on
|
|
591
|
+
* local-plugin discovery and does not write a project tui.json entry.
|
|
529
592
|
*
|
|
530
593
|
* @param {string} target - install target directory
|
|
531
594
|
* @param {object} [opts]
|
|
@@ -533,18 +596,19 @@ export function tuiConfigLocations(target) {
|
|
|
533
596
|
* @param {boolean} [opts.dryRun=false]
|
|
534
597
|
* @returns {'created'|'skipped'} status for the target registration
|
|
535
598
|
*/
|
|
536
|
-
export function syncTuiRegistration(
|
|
599
|
+
export function syncTuiRegistration(
|
|
600
|
+
target,
|
|
601
|
+
{ isGlobal = false, dryRun = false, version = 'v1' } = {},
|
|
602
|
+
) {
|
|
537
603
|
const configDir = isGlobal ? target : join(target, '.opencode')
|
|
538
604
|
const targetTuiConfigPath = join(configDir, 'tui.json')
|
|
539
605
|
|
|
540
|
-
// 1.
|
|
541
|
-
// plugins/pantheon-tui.
|
|
542
|
-
// in-package src/plugins/tui) keeps installs hermetic and idempotent:
|
|
543
|
-
// the ref points at a dir that exists and has dist/tui.js.
|
|
606
|
+
// 1. Copy the host-specific plugin into the target config's
|
|
607
|
+
// plugins/pantheon-tui. This keeps the installed entry hermetic.
|
|
544
608
|
const tuiSrcDir = join(ROOT, 'src', 'plugins', 'tui')
|
|
545
609
|
const tuiCopyDir = resolveTuiCopyTarget(configDir)
|
|
546
610
|
if (existsSync(tuiSrcDir)) {
|
|
547
|
-
copyPluginFiles(tuiSrcDir, tuiCopyDir, { dryRun })
|
|
611
|
+
copyPluginFiles(tuiSrcDir, tuiCopyDir, { dryRun, version })
|
|
548
612
|
}
|
|
549
613
|
|
|
550
614
|
// 2. Clean stale Pantheon TUI refs from every location OpenCode reads.
|
|
@@ -560,6 +624,11 @@ export function syncTuiRegistration(target, { isGlobal = false, dryRun = false }
|
|
|
560
624
|
})
|
|
561
625
|
}
|
|
562
626
|
|
|
627
|
+
// OpenCode V2 discovers local plugins from the config plugins directories;
|
|
628
|
+
// its TUI plugin API is exposed by the sibling tui.ts entrypoint. V1 still
|
|
629
|
+
// requires the explicit tui.json registration.
|
|
630
|
+
if (version === 'v2') return 'skipped'
|
|
631
|
+
|
|
563
632
|
// 3. Register the copied directory in the selected target only.
|
|
564
633
|
return registerPlugin(targetTuiConfigPath, tuiCopyDir, { dryRun })
|
|
565
634
|
}
|
|
@@ -796,14 +865,13 @@ export async function installOpenCode(
|
|
|
796
865
|
}
|
|
797
866
|
}
|
|
798
867
|
|
|
799
|
-
//
|
|
800
|
-
//
|
|
801
|
-
//
|
|
802
|
-
//
|
|
803
|
-
// .opencode/agents/ .opencode/skills/ .opencode/commands/
|
|
868
|
+
// V1 project installs use the legacy .opencode/agents layout. OpenCode V2's
|
|
869
|
+
// active config directory is the project root and its agent loader scans
|
|
870
|
+
// <config-dir>/{agent,agents}/**, so V2 project agents must be at ./agents.
|
|
871
|
+
// Global installs are flat for both versions.
|
|
804
872
|
const isGlobal = isGlobalConfigDir(target)
|
|
805
873
|
const _subDir = isGlobal ? '' : '.opencode'
|
|
806
|
-
const agentPrefix = isGlobal ? 'agents' : '.opencode/agents'
|
|
874
|
+
const agentPrefix = isGlobal || version === 'v2' ? 'agents' : '.opencode/agents'
|
|
807
875
|
|
|
808
876
|
configure({ dryRun })
|
|
809
877
|
|
|
@@ -822,7 +890,7 @@ export async function installOpenCode(
|
|
|
822
890
|
stats.errors++
|
|
823
891
|
} else {
|
|
824
892
|
const agentStep = step(S.installingAgents)
|
|
825
|
-
const dstDir =
|
|
893
|
+
const dstDir = join(target, agentPrefix)
|
|
826
894
|
if (!dryRun) mkdirSync(dstDir, { recursive: true })
|
|
827
895
|
if (clean && existsSync(dstDir) && !dryRun) {
|
|
828
896
|
const existing = readdirSync(dstDir)
|
|
@@ -902,9 +970,7 @@ export async function installOpenCode(
|
|
|
902
970
|
// already-installed Pantheon agents, but must not create agents the user
|
|
903
971
|
// did not install. Full agent installs do this in phase 1 above.
|
|
904
972
|
if (!componentSet.has('agents')) {
|
|
905
|
-
const installedAgents =
|
|
906
|
-
? join(target, 'agents')
|
|
907
|
-
: join(target, '.opencode', 'agents')
|
|
973
|
+
const installedAgents = join(target, agentPrefix)
|
|
908
974
|
const sourceAgents = join(ROOT, 'src', 'agents')
|
|
909
975
|
if (existsSync(installedAgents) && existsSync(sourceAgents)) {
|
|
910
976
|
const refreshed = copyAgentPrompts(sourceAgents, installedAgents, dryRun, true)
|
|
@@ -934,7 +1000,11 @@ export async function installOpenCode(
|
|
|
934
1000
|
if (componentSet.has('commands')) {
|
|
935
1001
|
section('\u26A1 Commands')
|
|
936
1002
|
const srcCmds = join(ROOT, 'commands')
|
|
937
|
-
|
|
1003
|
+
// V2 project sandboxes use OPENCODE_CONFIG_DIR=<project root>, so OpenCode
|
|
1004
|
+
// discovers custom commands from <target>/commands. V1 project installs
|
|
1005
|
+
// keep the standard .opencode/commands layout; global installs are flat.
|
|
1006
|
+
const commandDir = isGlobal || version === 'v2' ? 'commands' : '.opencode/commands'
|
|
1007
|
+
const dstCmds = join(target, commandDir)
|
|
938
1008
|
if (existsSync(srcCmds)) {
|
|
939
1009
|
const cmdResult = syncDir(srcCmds, dstCmds, dryRun, clean, (f) => f.endsWith('.md'))
|
|
940
1010
|
stats.created += cmdResult.created
|
|
@@ -959,7 +1029,7 @@ export async function installOpenCode(
|
|
|
959
1029
|
const dstPluginDir = isGlobal
|
|
960
1030
|
? join(target, 'plugins', 'pantheon-tui')
|
|
961
1031
|
: join(target, '.opencode', 'plugins', 'pantheon-tui')
|
|
962
|
-
const pluginStats = installPlugin(srcPluginDir, dstPluginDir, { dryRun, clean })
|
|
1032
|
+
const pluginStats = installPlugin(srcPluginDir, dstPluginDir, { dryRun, clean, version })
|
|
963
1033
|
stats.created += pluginStats.created
|
|
964
1034
|
stats.skipped += pluginStats.skipped
|
|
965
1035
|
stats.errors += pluginStats.errors
|
|
@@ -971,7 +1041,7 @@ export async function installOpenCode(
|
|
|
971
1041
|
if (componentSet.has('plugins')) {
|
|
972
1042
|
let tuiStatus
|
|
973
1043
|
try {
|
|
974
|
-
tuiStatus = syncTuiRegistration(target, { isGlobal, dryRun })
|
|
1044
|
+
tuiStatus = syncTuiRegistration(target, { isGlobal, dryRun, version })
|
|
975
1045
|
} catch (cause) {
|
|
976
1046
|
error(`TUI plugin setup failed: ${cause.message}`)
|
|
977
1047
|
stats.errors++
|
|
@@ -1000,6 +1070,7 @@ export async function installOpenCode(
|
|
|
1000
1070
|
'mcp_persistence.py',
|
|
1001
1071
|
'pantheon_vision.py',
|
|
1002
1072
|
'eval_store.py',
|
|
1073
|
+
'themis_heuristic_scan.py',
|
|
1003
1074
|
]
|
|
1004
1075
|
const srcScriptsDir = join(ROOT, 'scripts')
|
|
1005
1076
|
// Canonical MCP server sources live in src/mcp/. Map them explicitly so the
|
|
@@ -1154,6 +1225,12 @@ export async function installOpenCode(
|
|
|
1154
1225
|
if (fm.permission) {
|
|
1155
1226
|
agent.permission = JSON.parse(JSON.stringify(fm.permission))
|
|
1156
1227
|
}
|
|
1228
|
+
if (fm.mcp_tools !== undefined) {
|
|
1229
|
+
agent.permission = {
|
|
1230
|
+
...(agent.permission ?? {}),
|
|
1231
|
+
...deriveNativeMcpPermissions(fm.mcp_tools),
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1157
1234
|
|
|
1158
1235
|
config[name] = agent
|
|
1159
1236
|
}
|
|
@@ -1266,12 +1343,16 @@ export async function installOpenCode(
|
|
|
1266
1343
|
// --------------------------------------------------------------------
|
|
1267
1344
|
// B.5.1 Hermetic plugin resolution (packaging fix)
|
|
1268
1345
|
// --------------------------------------------------------------------
|
|
1269
|
-
// A developer running from a checkout
|
|
1270
|
-
//
|
|
1271
|
-
//
|
|
1272
|
-
//
|
|
1273
|
-
//
|
|
1274
|
-
//
|
|
1346
|
+
// A developer running from a checkout has a repository-root opencode.json
|
|
1347
|
+
// that may reference plugins via developer-machine absolute paths (e.g.
|
|
1348
|
+
// <dev>/pantheon/.../src/plugin.ts or
|
|
1349
|
+
// <dev>/pantheon/.../src/plugins/pantheon-hooks.ts). Copying those
|
|
1350
|
+
// verbatim into the user's config would break every global install on any
|
|
1351
|
+
// other machine. resolveInstalledPlugin() rewrites each entry to the plugin
|
|
1352
|
+
// file inside the INSTALLED package (derived from ROOT — never hardcoded),
|
|
1353
|
+
// so both dev and global installs resolve to a path that actually exists.
|
|
1354
|
+
// The source opencode.json keeps its dev path for local development; the
|
|
1355
|
+
// transform happens at install/sync time only.
|
|
1275
1356
|
|
|
1276
1357
|
if (version === 'v1') {
|
|
1277
1358
|
// V1 loads the singular `plugin` list. Remove every Pantheon V2 entry from
|
|
@@ -1332,13 +1413,18 @@ export async function installOpenCode(
|
|
|
1332
1413
|
}
|
|
1333
1414
|
}
|
|
1334
1415
|
}
|
|
1335
|
-
//
|
|
1336
|
-
//
|
|
1337
|
-
//
|
|
1338
|
-
//
|
|
1339
|
-
//
|
|
1340
|
-
// directory
|
|
1341
|
-
//
|
|
1416
|
+
// The beta resolves every `plugins` entry through npm install, so an
|
|
1417
|
+
// `a/b` shorthand (e.g. the published `pantheon-opencode/plugin-v2`
|
|
1418
|
+
// export) is treated as a GitHub repo and dies with NpmInstallFailedError
|
|
1419
|
+
// (beta log evidence). The beta loader additionally requires each entry
|
|
1420
|
+
// to be a DIRECTORY with a real index.js/index.ts — a file path warns
|
|
1421
|
+
// "must be a directory" and a symlink is ignored (sandbox probe
|
|
1422
|
+
// evidence). Register the V2 plugin DIRECTORY inside the INSTALLED
|
|
1423
|
+
// package — exactly like V1's ensurePantheonPlugin, but pointing at
|
|
1424
|
+
// src/plugin-v2 whose index.ts shim re-exports the real entry.
|
|
1425
|
+
// Legacy refs (npm shorthand, the old src/plugin-v2.ts file path) are
|
|
1426
|
+
// recognized via managedPluginIdentity and already stripped above, so
|
|
1427
|
+
// they migrate here.
|
|
1342
1428
|
const resolvedV2 = resolveInstalledPlugin(PANTHEON_V2_PLUGIN)
|
|
1343
1429
|
config.plugins = config.plugins.filter(
|
|
1344
1430
|
(plugin) => pluginReferenceIdentity(plugin) !== pluginReferenceIdentity(resolvedV2),
|
|
@@ -1522,16 +1608,6 @@ export async function installOpenCode(
|
|
|
1522
1608
|
configToWrite.plugins,
|
|
1523
1609
|
config.context_sandbox,
|
|
1524
1610
|
)
|
|
1525
|
-
|
|
1526
|
-
// Bootstrap chicken-and-egg: the pin names the CURRENT package version,
|
|
1527
|
-
// which exists on the registry only after publish. From an npm install
|
|
1528
|
-
// (global prefix / npx cache) that is the published version by
|
|
1529
|
-
// construction; from a repo checkout the pin may point at an unpublished
|
|
1530
|
-
// version the host cannot resolve. Warn instead of failing — the config is
|
|
1531
|
-
// structurally correct and the pin realigns once the version is out.
|
|
1532
|
-
if (!RUNNING_FROM_NPM_INSTALL) {
|
|
1533
|
-
warning(S.v2UnpublishedPin(PANTHEON_V2_PACKAGE_SPEC))
|
|
1534
|
-
}
|
|
1535
1611
|
}
|
|
1536
1612
|
|
|
1537
1613
|
const configContent = `${JSON.stringify(configToWrite, null, 2)}\n`
|
|
@@ -1635,4 +1711,25 @@ export async function installOpenCode(
|
|
|
1635
1711
|
if (stats.errors > 0) {
|
|
1636
1712
|
throw new Error(`installation completed with ${stats.errors} error(s); see output above`)
|
|
1637
1713
|
}
|
|
1714
|
+
|
|
1715
|
+
// Record the component state only after the install has completed. The
|
|
1716
|
+
// postinstall marker can predate `init` and start with every component
|
|
1717
|
+
// pending; leaving it untouched made a healthy beta install look incomplete
|
|
1718
|
+
// to `doctor` forever. Unselected components retain their previous state.
|
|
1719
|
+
if (!dryRun) {
|
|
1720
|
+
const packageVersion = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')).version
|
|
1721
|
+
let state = readState(target)
|
|
1722
|
+
if (!state && hasState(target)) {
|
|
1723
|
+
throw new Error('Installation state exists but cannot be read; refusing to replace it')
|
|
1724
|
+
}
|
|
1725
|
+
state ??= createInitialState(packageVersion)
|
|
1726
|
+
state.pantheon_version = packageVersion
|
|
1727
|
+
for (const component of ['agents', 'skills', 'plugins', 'runtime']) {
|
|
1728
|
+
if (!componentSet.has(component)) continue
|
|
1729
|
+
const status = component === 'runtime' && !runtimeHealthy ? 'failed' : 'installed'
|
|
1730
|
+
const componentVersion = state.components?.[component]?.version ?? 1
|
|
1731
|
+
setComponentState(state, component, componentVersion, status)
|
|
1732
|
+
}
|
|
1733
|
+
writeState(target, state)
|
|
1734
|
+
}
|
|
1638
1735
|
}
|
|
@@ -8,24 +8,24 @@ import { isAbsolute, join, resolve } from 'node:path'
|
|
|
8
8
|
import { ROOT, writeIfChanged } from './shared.mjs'
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Copy a plugin's runtime payload (the bundled dist/*,
|
|
12
|
-
* package-lock.json) from source to destination.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* Copy a plugin's runtime payload (the root TUI loader, bundled dist/*,
|
|
12
|
+
* package.json and package-lock.json) from source to destination. The raw TSX
|
|
13
|
+
* implementation is never shipped to the destination; tui.ts only re-exports
|
|
14
|
+
* the host-specific bundle so the local plugin stays hermetic.
|
|
15
15
|
* @param {string} srcDir - Source plugin directory (e.g. ROOT/src/plugins/tui)
|
|
16
16
|
* @param {string} dstDir - Destination plugin directory
|
|
17
17
|
* @param {object} [options]
|
|
18
18
|
* @param {boolean} [options.dryRun=false] - If true, don't write files
|
|
19
|
+
* @param {'v1'|'v2'} [options.version='v1'] - Host plugin generation
|
|
19
20
|
* @returns {{ created: number, skipped: number }}
|
|
20
21
|
*/
|
|
21
|
-
export function copyPluginFiles(srcDir, dstDir, { dryRun = false } = {}) {
|
|
22
|
+
export function copyPluginFiles(srcDir, dstDir, { dryRun = false, version = 'v1' } = {}) {
|
|
22
23
|
const result = { created: 0, skipped: 0 }
|
|
23
24
|
|
|
24
25
|
if (!dryRun) mkdirSync(dstDir, { recursive: true })
|
|
25
26
|
if (!dryRun) mkdirSync(join(dstDir, 'dist'), { recursive: true })
|
|
26
27
|
|
|
27
28
|
// Drop legacy raw copies from previous installs (root index.tsx + dist/tui.tsx).
|
|
28
|
-
// The loader consumes only the bundle, so stale raws must not linger.
|
|
29
29
|
if (!dryRun) {
|
|
30
30
|
rmSync(join(dstDir, 'index.tsx'), { force: true })
|
|
31
31
|
rmSync(join(dstDir, 'dist', 'tui.tsx'), { force: true })
|
|
@@ -45,15 +45,45 @@ export function copyPluginFiles(srcDir, dstDir, { dryRun = false } = {}) {
|
|
|
45
45
|
// Copy package.json
|
|
46
46
|
const pkgSrc = join(srcDir, 'package.json')
|
|
47
47
|
if (existsSync(pkgSrc)) {
|
|
48
|
+
const packageJson = JSON.parse(readFileSync(pkgSrc, 'utf8'))
|
|
49
|
+
if (version === 'v2' && packageJson.exports?.['./tui-v2']) {
|
|
50
|
+
packageJson.exports['./tui'] = packageJson.exports['./tui-v2']
|
|
51
|
+
}
|
|
48
52
|
const pkgStatus = writeIfChanged(
|
|
49
53
|
join(dstDir, 'package.json'),
|
|
50
|
-
|
|
54
|
+
`${JSON.stringify(packageJson, null, 2)}\n`,
|
|
51
55
|
dryRun,
|
|
52
56
|
)
|
|
53
57
|
if (pkgStatus === 'created') result.created++
|
|
54
58
|
else result.skipped++
|
|
55
59
|
}
|
|
56
60
|
|
|
61
|
+
// Each host generation receives its own root loader and compiled bundle.
|
|
62
|
+
const tuiEntrySrc = join(srcDir, version === 'v2' ? 'tui-v2.ts' : 'tui.ts')
|
|
63
|
+
if (existsSync(tuiEntrySrc)) {
|
|
64
|
+
const entryStatus = writeIfChanged(
|
|
65
|
+
join(dstDir, 'tui.ts'),
|
|
66
|
+
readFileSync(tuiEntrySrc, 'utf8'),
|
|
67
|
+
dryRun,
|
|
68
|
+
)
|
|
69
|
+
if (entryStatus === 'created') result.created++
|
|
70
|
+
else result.skipped++
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const v2ServerEntry = join(srcDir, 'index-v2.ts')
|
|
74
|
+
const v2ServerDestination = join(dstDir, 'index.ts')
|
|
75
|
+
if (version === 'v2' && existsSync(v2ServerEntry)) {
|
|
76
|
+
const entryStatus = writeIfChanged(
|
|
77
|
+
v2ServerDestination,
|
|
78
|
+
readFileSync(v2ServerEntry, 'utf8'),
|
|
79
|
+
dryRun,
|
|
80
|
+
)
|
|
81
|
+
if (entryStatus === 'created') result.created++
|
|
82
|
+
else result.skipped++
|
|
83
|
+
} else if (!dryRun) {
|
|
84
|
+
rmSync(v2ServerDestination, { force: true })
|
|
85
|
+
}
|
|
86
|
+
|
|
57
87
|
// Copy the lockfile so dependency installation is reproducible. A missing
|
|
58
88
|
// lockfile is intentionally not synthesized by the installer.
|
|
59
89
|
const lockSrc = join(srcDir, 'package-lock.json')
|
|
@@ -79,14 +109,18 @@ export function copyPluginFiles(srcDir, dstDir, { dryRun = false } = {}) {
|
|
|
79
109
|
* @param {boolean} [options.clean=false] - If true, remove existing before install
|
|
80
110
|
* @returns {{ created: number, skipped: number, errors: number }}
|
|
81
111
|
*/
|
|
82
|
-
export function installPlugin(
|
|
112
|
+
export function installPlugin(
|
|
113
|
+
srcDir,
|
|
114
|
+
dstDir,
|
|
115
|
+
{ dryRun = false, clean = false, version = 'v1' } = {},
|
|
116
|
+
) {
|
|
83
117
|
const result = { created: 0, skipped: 0, errors: 0 }
|
|
84
118
|
|
|
85
119
|
if (clean && existsSync(dstDir) && !dryRun) {
|
|
86
120
|
rmSync(dstDir, { recursive: true, force: true })
|
|
87
121
|
}
|
|
88
122
|
|
|
89
|
-
const copyResult = copyPluginFiles(srcDir, dstDir, { dryRun })
|
|
123
|
+
const copyResult = copyPluginFiles(srcDir, dstDir, { dryRun, version })
|
|
90
124
|
result.created += copyResult.created
|
|
91
125
|
result.skipped += copyResult.skipped
|
|
92
126
|
|
|
@@ -47,10 +47,6 @@ const TABLE = {
|
|
|
47
47
|
prereqMissing:
|
|
48
48
|
'runtime prerequisites missing — install the tools above, or re-run with --no-mcp to install without Python MCP servers',
|
|
49
49
|
v2Migrating: 'Migrating config to V2 native format...',
|
|
50
|
-
v2UnpublishedPin: (spec) =>
|
|
51
|
-
`V2 plugin is pinned to ${spec}, but this installer is running from a source checkout — ` +
|
|
52
|
-
'that version may not be published to npm yet, so OpenCode cannot resolve the pin until it is. ' +
|
|
53
|
-
'Run the installer from the published package (npx pantheon-opencode init) to avoid this.',
|
|
54
50
|
// summary (printSummary)
|
|
55
51
|
summaryInstalled: (target) => `OpenCode installed in ${target}`,
|
|
56
52
|
summaryWithErrors: (target) => `Installation finished WITH ERRORS in ${target}`,
|
|
@@ -99,10 +95,6 @@ const TABLE = {
|
|
|
99
95
|
prereqMissing:
|
|
100
96
|
'pré-requisitos do runtime ausentes — instale as ferramentas acima, ou rode de novo com --no-mcp para instalar sem os MCPs Python',
|
|
101
97
|
v2Migrating: 'Migrando config para o formato nativo V2...',
|
|
102
|
-
v2UnpublishedPin: (spec) =>
|
|
103
|
-
`Plugin V2 fixado em ${spec}, mas este installer está rodando de um checkout do código-fonte — ` +
|
|
104
|
-
'essa versão pode ainda não estar publicada no npm, então o OpenCode não resolve o pin até a publicação. ' +
|
|
105
|
-
'Rode o installer pelo pacote publicado (npx pantheon-opencode init) para evitar isso.',
|
|
106
98
|
// summary (printSummary)
|
|
107
99
|
summaryInstalled: (target) => `OpenCode instalado em ${target}`,
|
|
108
100
|
summaryWithErrors: (target) => `Instalação concluída COM ERROS em ${target}`,
|
|
@@ -8,10 +8,9 @@
|
|
|
8
8
|
* temporary synthetic opencode database. A missing fixture runtime is also
|
|
9
9
|
* AMBIENTAL rather than a false PASS.
|
|
10
10
|
*/
|
|
11
|
-
import {
|
|
11
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs'
|
|
12
12
|
import { tmpdir } from 'node:os'
|
|
13
13
|
import { join } from 'node:path'
|
|
14
|
-
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
15
14
|
|
|
16
15
|
const requestedVersion = valueFor('--version')
|
|
17
16
|
const jsonOutput = process.argv.includes('--json')
|
|
@@ -152,27 +151,19 @@ async function main() {
|
|
|
152
151
|
return
|
|
153
152
|
}
|
|
154
153
|
|
|
155
|
-
const root = mkdtempSync(join(tmpdir(), 'pantheon-cost-probe-'))
|
|
156
154
|
let DatabaseSync
|
|
157
155
|
let createCostCommand
|
|
158
156
|
try {
|
|
159
157
|
;({ DatabaseSync } = await import('node:sqlite'))
|
|
160
|
-
|
|
161
|
-
// node_modules. Exercise the shipped command from an isolated temp copy
|
|
162
|
-
// while resolving its runtime dependencies through the installed package.
|
|
163
|
-
const runtimeSource = join(root, 'cost-command.ts')
|
|
164
|
-
const packageNodeModules = fileURLToPath(new URL('../node_modules/', import.meta.url))
|
|
165
|
-
copyFileSync(fileURLToPath(source), runtimeSource)
|
|
166
|
-
symlinkSync(packageNodeModules, join(root, 'node_modules'), 'dir')
|
|
167
|
-
;({ createCostCommand } = await import(pathToFileURL(runtimeSource).href))
|
|
158
|
+
;({ createCostCommand } = await import(source.href))
|
|
168
159
|
} catch (error) {
|
|
169
|
-
rmSync(root, { recursive: true, force: true })
|
|
170
160
|
ambiental(
|
|
171
161
|
`pantheon_cost fixture runtime unavailable: ${error instanceof Error ? error.message : String(error)}`,
|
|
172
162
|
)
|
|
173
163
|
return
|
|
174
164
|
}
|
|
175
165
|
|
|
166
|
+
const root = mkdtempSync(join(tmpdir(), 'pantheon-cost-probe-'))
|
|
176
167
|
const dataHome = join(root, 'data')
|
|
177
168
|
const opencodeDir = join(dataHome, 'opencode')
|
|
178
169
|
// Every real installation writes `opencode.db`; the v1/v2 distinction is the
|
|
@@ -263,7 +254,6 @@ async function main() {
|
|
|
263
254
|
|
|
264
255
|
const restore = setEnvironment({
|
|
265
256
|
XDG_DATA_HOME: dataHome,
|
|
266
|
-
OPENCODE_DB: undefined,
|
|
267
257
|
PANTHEON_OPENCODE_VERSION: requestedVersion,
|
|
268
258
|
PANTHEON_COST_DB: undefined,
|
|
269
259
|
})
|
|
@@ -294,7 +284,6 @@ async function main() {
|
|
|
294
284
|
|
|
295
285
|
const envRestore = setEnvironment({
|
|
296
286
|
XDG_DATA_HOME: dataHome,
|
|
297
|
-
OPENCODE_DB: undefined,
|
|
298
287
|
PANTHEON_OPENCODE_VERSION: requestedVersion,
|
|
299
288
|
PANTHEON_COST_DB: envDb,
|
|
300
289
|
})
|
|
@@ -319,7 +308,6 @@ async function main() {
|
|
|
319
308
|
|
|
320
309
|
const explicitRestore = setEnvironment({
|
|
321
310
|
XDG_DATA_HOME: dataHome,
|
|
322
|
-
OPENCODE_DB: undefined,
|
|
323
311
|
PANTHEON_OPENCODE_VERSION: requestedVersion,
|
|
324
312
|
PANTHEON_COST_DB: envDb,
|
|
325
313
|
})
|
|
@@ -341,7 +329,6 @@ async function main() {
|
|
|
341
329
|
|
|
342
330
|
const missingRestore = setEnvironment({
|
|
343
331
|
XDG_DATA_HOME: join(root, 'missing-data'),
|
|
344
|
-
OPENCODE_DB: undefined,
|
|
345
332
|
PANTHEON_OPENCODE_VERSION: requestedVersion,
|
|
346
333
|
PANTHEON_COST_DB: undefined,
|
|
347
334
|
})
|
|
@@ -40,6 +40,7 @@ const MCP_SCRIPTS = {
|
|
|
40
40
|
'pantheon_vision.py': () => join(ROOT, 'src', 'mcp', 'pantheon_vision.py'),
|
|
41
41
|
'eval_store.py': () => join(ROOT, 'src', 'mcp', 'eval_store.py'),
|
|
42
42
|
'scrub-secrets.py': () => join(ROOT, 'scripts', 'scrub-secrets.py'),
|
|
43
|
+
'themis_heuristic_scan.py': () => join(ROOT, 'scripts', 'themis_heuristic_scan.py'),
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
/**
|