pantheon-opencode 1.6.0-beta.4 → 1.6.0-beta.6
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 +42 -23
- package/CHANGELOG.md +41 -0
- package/README.md +114 -12
- package/README.pt-BR.md +15 -6
- package/bin/pantheon-init.mjs +26 -3
- package/package.json +5 -6
- package/plugin.json +1 -1
- package/pyproject.toml +6 -6
- package/scripts/{code_mode_server.py → code_mode.py} +2 -2
- package/scripts/doctor.mjs +151 -1
- package/scripts/install/config-migration.mjs +80 -2
- package/scripts/install/health-check.mjs +12 -17
- package/scripts/install/opencode-version.mjs +246 -4
- package/scripts/install/opencode.mjs +250 -119
- package/scripts/install-mcp.mjs +28 -9
- package/scripts/{mcp_persistence_server.py → mcp_persistence.py} +1 -1
- package/{src/mcp/mcp_resources_server.py → scripts/mcp_resources.py} +1 -1
- package/scripts/{memory_mcp_server.py → memory_mcp.py} +1 -1
- package/scripts/probe-context-rehydrate.mjs +2 -2
- package/scripts/run-node-tests.mjs +189 -0
- package/scripts/sync-artifacts.mjs +5 -5
- package/scripts/test-opencode-v2-sandbox.sh +21 -21
- package/skills-lock.json +2 -2
- package/src/agents/aphrodite.md +1 -1
- package/src/agents/apollo.md +7 -2
- package/src/agents/athena.md +1 -0
- package/src/agents/demeter.md +1 -1
- package/src/agents/hephaestus.md +1 -1
- package/src/agents/hermes.md +1 -1
- package/src/agents/iris.md +1 -0
- package/src/agents/nyx.md +1 -0
- package/src/agents/themis.md +1 -0
- package/src/agents/zeus.md +39 -10
- package/src/instructions/memory-protocol.instructions.md +21 -10
- package/src/instructions/zeus-anti-stall.instructions.md +19 -11
- package/src/instructions/zeus-council-synthesis.instructions.md +2 -2
- package/src/mcp/{code_mode_server.py → code_mode.py} +2 -2
- package/src/mcp/eval_store.py +2 -2
- package/src/mcp/{mcp_persistence_server.py → mcp_persistence.py} +1 -1
- package/{scripts/mcp_resources_server.py → src/mcp/mcp_resources.py} +1 -1
- package/src/mcp/{memory_mcp_server.py → memory_mcp.py} +1 -1
- package/src/pantheon/v2-unsupported.d.mts +11 -0
- package/src/pantheon/v2-unsupported.mjs +113 -0
- package/src/plugin-v2.ts +149 -53
- package/src/plugins/tui/dist/tui.js +1 -1
- package/src/plugins/tui/package-lock.json +2 -2
- package/src/plugins/tui/package.json +1 -1
- package/src/skills/auto-continue/SKILL.md +4 -4
- package/src/skills/context-compression/SKILL.md +1 -1
- package/opencode.json +0 -58
- /package/src/mcp/{pantheon_vision_server.py → pantheon_vision.py} +0 -0
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Dual-version install (Phase 3): V1 and V2 receive the schema each runtime
|
|
6
6
|
* actually consumes. V1 (`opencode` 1.18.x) uses the singular `plugin` key;
|
|
7
|
-
* V2 (`opencode2
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* V2 (`opencode2`, OpenCode 2.x — `opencode2 --version` prints
|
|
8
|
+
* "opencode v2.0.22") converts that list to `plugins` before
|
|
9
|
+
* writing its config. An early 2.x prerelease printed "0.0.0-next-17444"
|
|
10
|
+
* instead; that is history, not what hosts report today, and it must not be
|
|
11
|
+
* cited as an observation (see the precedence rationale in
|
|
12
|
+
* scripts/install/opencode-version.mjs). Known V2 beta gaps handled here:
|
|
13
|
+
* top-level `subagent_depth` is migrated to `experimental.subagent_depth`, and
|
|
14
|
+
* the instruction content is consolidated into AGENTS.md, which both versions
|
|
11
15
|
* load. Pass --version v2 to pantheon-init for an informational label; state
|
|
12
16
|
* isolation is handled at runtime via OPENCODE_DB.
|
|
13
17
|
*/
|
|
@@ -16,6 +20,7 @@ import { createHash } from 'node:crypto'
|
|
|
16
20
|
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs'
|
|
17
21
|
import { homedir } from 'node:os'
|
|
18
22
|
import { dirname, isAbsolute, join, resolve } from 'node:path'
|
|
23
|
+
import { listV2UnsupportedFeatures } from '../../src/pantheon/v2-unsupported.mjs'
|
|
19
24
|
import {
|
|
20
25
|
bullet,
|
|
21
26
|
colors,
|
|
@@ -29,7 +34,7 @@ import {
|
|
|
29
34
|
success,
|
|
30
35
|
warning,
|
|
31
36
|
} from './cli-ui.mjs'
|
|
32
|
-
import { migrateV1toV2 } from './config-migration.mjs'
|
|
37
|
+
import { migrateMcpEntrypointPaths, migrateV1toV2 } from './config-migration.mjs'
|
|
33
38
|
import { healthCheck } from './health-check.mjs'
|
|
34
39
|
import { detectVersion, runMigrations } from './migrate.mjs'
|
|
35
40
|
import { resolveOpenCodeVersion } from './opencode-version.mjs'
|
|
@@ -61,6 +66,44 @@ const COMPONENT_NAMES = [
|
|
|
61
66
|
'runtime',
|
|
62
67
|
]
|
|
63
68
|
|
|
69
|
+
/**
|
|
70
|
+
* The product defaults an install seeds into the user's `opencode.json`.
|
|
71
|
+
*
|
|
72
|
+
* They live HERE, in code, rather than in a template shipped in the npm
|
|
73
|
+
* tarball: that template also carried a maintainer's private MCP endpoint,
|
|
74
|
+
* provider models and per-developer permission overrides, and shipping it to
|
|
75
|
+
* every installer was the bug (#199). Reading defaults off it meant deleting it
|
|
76
|
+
* silently stopped the seeding, so the defaults are now stated next to the code
|
|
77
|
+
* that consumes them.
|
|
78
|
+
*
|
|
79
|
+
* PERSONAL CONFIGURATION MUST NEVER BE SEEDED HERE: this is the installer's
|
|
80
|
+
* contract with every user, and a value that is only right on one machine
|
|
81
|
+
* belongs in that machine's own `opencode.json`. Deliberately NOT carried over
|
|
82
|
+
* from the template, each because it was personal rather than a product
|
|
83
|
+
* default:
|
|
84
|
+
* - `provider` — pinned the maintainer's provider, model and modalities.
|
|
85
|
+
* - `mcp` — a private remote MCP endpoint.
|
|
86
|
+
* - `agent` — the maintainer's per-agent permission overrides.
|
|
87
|
+
* - `permission.websearch: "deny"` — a personal restriction; a user may rely
|
|
88
|
+
* on web search, so denying it is not ours to decide.
|
|
89
|
+
* - `permission.bash` — the template's dev allowlist (git, npm, ruff, …), a
|
|
90
|
+
* personal judgement about which commands to trust.
|
|
91
|
+
* Anything added here must be justifiable as "correct for a fresh install of
|
|
92
|
+
* the product, for anyone".
|
|
93
|
+
*
|
|
94
|
+
* Other seeding paths are untouched by this constant and still run: the config
|
|
95
|
+
* merge derives plugin refs, provider and compaction from a repository-root
|
|
96
|
+
* opencode.json when a contributor has one, and separately gates its own
|
|
97
|
+
* permission grants — `skill` on the skills component, `bash` unconditionally —
|
|
98
|
+
* on whether the user's own config already set them.
|
|
99
|
+
*
|
|
100
|
+
* Frozen, and copied only into a confirmed gap (never into a key the user
|
|
101
|
+
* already set), so nothing here can be mutated through a user's config.
|
|
102
|
+
*/
|
|
103
|
+
const PRODUCT_CONFIG_DEFAULTS = Object.freeze({
|
|
104
|
+
default_agent: 'zeus',
|
|
105
|
+
})
|
|
106
|
+
|
|
64
107
|
/**
|
|
65
108
|
* Per-agent fields owned by the framework: for an agent that already exists in
|
|
66
109
|
* the target config these are overwritten from the canonical source, and for a
|
|
@@ -158,8 +201,8 @@ function mergeMissing(target, source) {
|
|
|
158
201
|
}
|
|
159
202
|
|
|
160
203
|
/**
|
|
161
|
-
* Resolve a plugin ref from the
|
|
162
|
-
* installed package.
|
|
204
|
+
* Resolve a plugin ref from the repository-root opencode.json to a path INSIDE
|
|
205
|
+
* the installed package.
|
|
163
206
|
*
|
|
164
207
|
* Only exact Pantheon plugin identities are resolved into the installed
|
|
165
208
|
* package. In particular, a third-party absolute path ending in
|
|
@@ -456,6 +499,92 @@ export function setupRuntimePhase(target, { dryRun, clean, isGlobal }, stats, de
|
|
|
456
499
|
}
|
|
457
500
|
}
|
|
458
501
|
|
|
502
|
+
/**
|
|
503
|
+
* Write the five managed MCP server entries into `config`, healing any stale
|
|
504
|
+
* entrypoint paths first.
|
|
505
|
+
*
|
|
506
|
+
* The heal runs on EVERY install, not only when an entry is missing: the
|
|
507
|
+
* per-server guards below are add-only, so an existing install whose
|
|
508
|
+
* `opencode.json` still carries a pre-rename `*_server.py` path would never be
|
|
509
|
+
* repaired. Healing first means a re-run of the installer fixes an orphaned
|
|
510
|
+
* config in place — the migration that makes the rename non-breaking.
|
|
511
|
+
*
|
|
512
|
+
* @param {object} config - full config object (mutated in place)
|
|
513
|
+
* @param {{ runtimeTarget: string, venvPython: string }} ctx
|
|
514
|
+
* @returns {{ healed: number }} number of stale path strings rewritten
|
|
515
|
+
*/
|
|
516
|
+
export function applyMcpServerEntries(config, { runtimeTarget, venvPython }) {
|
|
517
|
+
config.mcp = config.mcp || {}
|
|
518
|
+
// MCP stays a flat named-server map for BOTH V1 and V2: OpenCode 1.18.x has
|
|
519
|
+
// no `mcp.servers` wrapper and rejects one with "Missing key
|
|
520
|
+
// mcp.servers.enabled". config-migration.mjs normalizes any legacy
|
|
521
|
+
// wrapper-shaped entries back to the flat map.
|
|
522
|
+
const healed = migrateMcpEntrypointPaths(config.mcp)
|
|
523
|
+
|
|
524
|
+
// Only add if not already configured by user
|
|
525
|
+
if (!config.mcp['pantheon-resources']) {
|
|
526
|
+
config.mcp['pantheon-resources'] = {
|
|
527
|
+
type: 'local',
|
|
528
|
+
cwd: runtimeTarget,
|
|
529
|
+
command: [venvPython, 'scripts/mcp_resources.py'],
|
|
530
|
+
enabled: true,
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (!config.mcp['pantheon-code-mode']) {
|
|
534
|
+
config.mcp['pantheon-code-mode'] = {
|
|
535
|
+
type: 'local',
|
|
536
|
+
cwd: runtimeTarget,
|
|
537
|
+
command: [venvPython, 'scripts/code_mode.py'],
|
|
538
|
+
enabled: true,
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (!config.mcp['pantheon-memory']) {
|
|
542
|
+
config.mcp['pantheon-memory'] = {
|
|
543
|
+
type: 'local',
|
|
544
|
+
cwd: runtimeTarget,
|
|
545
|
+
command: [venvPython, 'scripts/memory_mcp.py'],
|
|
546
|
+
enabled: true,
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (!config.mcp['pantheon-persistence']) {
|
|
550
|
+
config.mcp['pantheon-persistence'] = {
|
|
551
|
+
type: 'local',
|
|
552
|
+
cwd: runtimeTarget,
|
|
553
|
+
command: [venvPython, 'scripts/mcp_persistence.py'],
|
|
554
|
+
enabled: true,
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (!config.mcp['pantheon-vision']) {
|
|
558
|
+
config.mcp['pantheon-vision'] = {
|
|
559
|
+
type: 'local',
|
|
560
|
+
cwd: runtimeTarget,
|
|
561
|
+
command: [venvPython, 'scripts/pantheon_vision.py'],
|
|
562
|
+
enabled: true,
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Default MCP permissions
|
|
567
|
+
config.permission = config.permission || {}
|
|
568
|
+
config.permission.mcp = config.permission.mcp || {}
|
|
569
|
+
if (!config.permission.mcp['pantheon-resources']) {
|
|
570
|
+
config.permission.mcp['pantheon-resources'] = 'allow'
|
|
571
|
+
}
|
|
572
|
+
if (!config.permission.mcp['pantheon-code-mode']) {
|
|
573
|
+
config.permission.mcp['pantheon-code-mode'] = 'ask'
|
|
574
|
+
}
|
|
575
|
+
if (!config.permission.mcp['pantheon-memory']) {
|
|
576
|
+
config.permission.mcp['pantheon-memory'] = 'allow'
|
|
577
|
+
}
|
|
578
|
+
if (!config.permission.mcp['pantheon-persistence']) {
|
|
579
|
+
config.permission.mcp['pantheon-persistence'] = 'allow'
|
|
580
|
+
}
|
|
581
|
+
if (!config.permission.mcp['pantheon-vision']) {
|
|
582
|
+
config.permission.mcp['pantheon-vision'] = 'ask'
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return { healed }
|
|
586
|
+
}
|
|
587
|
+
|
|
459
588
|
export async function installOpenCode(
|
|
460
589
|
target,
|
|
461
590
|
dryRun = false,
|
|
@@ -477,12 +606,31 @@ export async function installOpenCode(
|
|
|
477
606
|
// --opencode-version=v2. V2 normalizes V1 fields in memory (no rewrite) and
|
|
478
607
|
// isolates its state DB via OPENCODE_DB (~/.local/share/opencode/opencode-v2.db)
|
|
479
608
|
// with a distinct service port (49375 vs V1's 49374).
|
|
480
|
-
|
|
609
|
+
// Default to 'auto' so a flagless run resolves against the actual host. A
|
|
610
|
+
// hardcoded 'v1' here (or in bin/pantheon-init.mjs) is what shipped the bug:
|
|
611
|
+
// the V1 branch writes .ts file paths that a 2.x host drops from the config.
|
|
612
|
+
// `opts` is forwarded as the resolver's options bag so a caller (and CI) can
|
|
613
|
+
// pin env/probe instead of spawning a real host binary.
|
|
614
|
+
const version = resolveOpenCodeVersion(opts.version ?? 'auto', opts)
|
|
481
615
|
if (version === 'v2') {
|
|
482
616
|
info(
|
|
483
617
|
'OpenCode V2 target — shared config; state isolated via OPENCODE_DB ' +
|
|
484
618
|
'(~/.local/share/opencode/opencode-v2.db), service port 49375',
|
|
485
619
|
)
|
|
620
|
+
// Say what the V2 contract does NOT give you, at the moment the user picks
|
|
621
|
+
// it. The V2 surface is 3 tools (hashline_edit, pantheon_cost,
|
|
622
|
+
// pantheon_model); V1's is 6 plus the BackgroundJobBoard, the three goal
|
|
623
|
+
// tools and the caller/target delegation matrix. This list was previously
|
|
624
|
+
// reachable only through `getUnsupportedFeatures()`, which no production
|
|
625
|
+
// code called — a user migrating hit the reduction with no warning at all.
|
|
626
|
+
//
|
|
627
|
+
// The markers are read from the same seed `getUnsupportedFeatures()` is
|
|
628
|
+
// built from (src/pantheon/v2-unsupported.mjs), so this notice cannot drift
|
|
629
|
+
// from what the plugin reports. Advisory: a V2 install is a supported
|
|
630
|
+
// configuration, not a failure.
|
|
631
|
+
info(
|
|
632
|
+
`V2 plugin generation — reduced surface. Unsupported features: ${listV2UnsupportedFeatures().join(', ')}`,
|
|
633
|
+
)
|
|
486
634
|
}
|
|
487
635
|
|
|
488
636
|
// Interactive mode handling
|
|
@@ -514,10 +662,24 @@ export async function installOpenCode(
|
|
|
514
662
|
}
|
|
515
663
|
}
|
|
516
664
|
|
|
517
|
-
|
|
665
|
+
// Product defaults live in code (PRODUCT_CONFIG_DEFAULTS). A repository-root
|
|
666
|
+
// opencode.json is a contributor's PERSONAL dev harness, not product data, so
|
|
667
|
+
// it must never be read just because it happens to exist: doing so silently
|
|
668
|
+
// injects the developer's provider/plugins/compaction into every config an
|
|
669
|
+
// install generates. The merge is therefore opt-in only, via
|
|
670
|
+
// --merge-dev-config <path>. Absent the flag, no root-level config is read.
|
|
671
|
+
const mergeDevConfigOpt = opts.mergeDevConfig ?? null
|
|
672
|
+
if (mergeDevConfigOpt !== null && typeof mergeDevConfigOpt !== 'string') {
|
|
673
|
+
throw new Error('mergeDevConfig requires a file path string')
|
|
674
|
+
}
|
|
675
|
+
const pantheonConfigPath =
|
|
676
|
+
typeof mergeDevConfigOpt === 'string' ? resolve(mergeDevConfigOpt) : null
|
|
518
677
|
const targetConfigPath = join(target, 'opencode.json')
|
|
519
678
|
const config = readJsonConfig(targetConfigPath)
|
|
520
|
-
const pantheonConfig =
|
|
679
|
+
const pantheonConfig =
|
|
680
|
+
pantheonConfigPath !== null && existsSync(pantheonConfigPath)
|
|
681
|
+
? readJsonConfig(pantheonConfigPath)
|
|
682
|
+
: null
|
|
521
683
|
|
|
522
684
|
// beta.5 preflight: fail BEFORE any files are written when the runtime
|
|
523
685
|
// component needs toolchain pieces that are missing. Nothing has been
|
|
@@ -710,30 +872,30 @@ export async function installOpenCode(
|
|
|
710
872
|
|
|
711
873
|
// ── MCP server scripts ──
|
|
712
874
|
const mcpScripts = [
|
|
713
|
-
'
|
|
714
|
-
'
|
|
715
|
-
'
|
|
875
|
+
'mcp_resources.py',
|
|
876
|
+
'code_mode.py',
|
|
877
|
+
'memory_mcp.py',
|
|
716
878
|
'scrub-secrets.py',
|
|
717
879
|
'_pantheon_paths.py',
|
|
718
880
|
'mcp_codemap_module.py',
|
|
719
|
-
'
|
|
720
|
-
'
|
|
881
|
+
'mcp_persistence.py',
|
|
882
|
+
'pantheon_vision.py',
|
|
721
883
|
'eval_store.py',
|
|
722
884
|
]
|
|
723
885
|
const srcScriptsDir = join(ROOT, 'scripts')
|
|
724
886
|
// Canonical MCP server sources live in src/mcp/. Map them explicitly so the
|
|
725
887
|
// sync always copies from the canonical source, never from the stale root
|
|
726
888
|
// scripts/ copies (which previously propagated bugs like the missing
|
|
727
|
-
// `import uuid` in
|
|
889
|
+
// `import uuid` in mcp_persistence.py). scrub-secrets.py is not in
|
|
728
890
|
// src/mcp/ and intentionally falls back to join(ROOT, 'scripts').
|
|
729
891
|
const canonicalMcpScripts = {
|
|
730
|
-
'
|
|
731
|
-
'
|
|
732
|
-
'
|
|
892
|
+
'mcp_resources.py': join(ROOT, 'src', 'mcp', 'mcp_resources.py'),
|
|
893
|
+
'code_mode.py': join(ROOT, 'src', 'mcp', 'code_mode.py'),
|
|
894
|
+
'memory_mcp.py': join(ROOT, 'src', 'mcp', 'memory_mcp.py'),
|
|
733
895
|
'_pantheon_paths.py': join(ROOT, 'src', 'mcp', '_pantheon_paths.py'),
|
|
734
896
|
'mcp_codemap_module.py': join(ROOT, 'src', 'mcp', 'mcp_codemap_module.py'),
|
|
735
|
-
'
|
|
736
|
-
'
|
|
897
|
+
'mcp_persistence.py': join(ROOT, 'src', 'mcp', 'mcp_persistence.py'),
|
|
898
|
+
'pantheon_vision.py': join(ROOT, 'src', 'mcp', 'pantheon_vision.py'),
|
|
737
899
|
'eval_store.py': join(ROOT, 'src', 'mcp', 'eval_store.py'),
|
|
738
900
|
}
|
|
739
901
|
const dstScriptsDir = join(runtimeTarget, 'scripts')
|
|
@@ -939,8 +1101,11 @@ export async function installOpenCode(
|
|
|
939
1101
|
// --------------------------------------------------------------------
|
|
940
1102
|
// B.5 Ensure critical top-level OpenCode config sections
|
|
941
1103
|
// --------------------------------------------------------------------
|
|
942
|
-
|
|
943
|
-
|
|
1104
|
+
// Seeded from code, not from a packaged template: see
|
|
1105
|
+
// PRODUCT_CONFIG_DEFAULTS. Only fills a gap — a user who already chose a
|
|
1106
|
+
// default agent keeps it.
|
|
1107
|
+
if (config.default_agent === undefined) {
|
|
1108
|
+
config.default_agent = PRODUCT_CONFIG_DEFAULTS.default_agent
|
|
944
1109
|
}
|
|
945
1110
|
// Apply only explicit top-level model overrides. Existing values remain
|
|
946
1111
|
// untouched and an install without either flag leaves both fields absent.
|
|
@@ -982,8 +1147,9 @@ export async function installOpenCode(
|
|
|
982
1147
|
// --------------------------------------------------------------------
|
|
983
1148
|
// B.5.1 Hermetic plugin resolution (packaging fix)
|
|
984
1149
|
// --------------------------------------------------------------------
|
|
985
|
-
//
|
|
986
|
-
// absolute paths (e.g.
|
|
1150
|
+
// A developer running from a checkout has a repository-root opencode.json
|
|
1151
|
+
// that may reference plugins via developer-machine absolute paths (e.g.
|
|
1152
|
+
// <dev>/pantheon/.../src/plugin.ts or
|
|
987
1153
|
// <dev>/pantheon/.../src/plugins/pantheon-hooks.ts). Copying those
|
|
988
1154
|
// verbatim into the user's config would break every global install on any
|
|
989
1155
|
// other machine. resolveInstalledPlugin() rewrites each entry to the plugin
|
|
@@ -1002,7 +1168,7 @@ export async function installOpenCode(
|
|
|
1002
1168
|
if (config.plugin === undefined) config.plugin = []
|
|
1003
1169
|
if (Array.isArray(config.plugin)) {
|
|
1004
1170
|
config.plugin = removePantheonPluginReferences(config.plugin)
|
|
1005
|
-
if (Array.isArray(pantheonConfig
|
|
1171
|
+
if (Array.isArray(pantheonConfig?.plugin)) {
|
|
1006
1172
|
for (const plugin of pantheonConfig.plugin) {
|
|
1007
1173
|
if (!isPantheonPluginReference(plugin) && !hasPluginReference(config.plugin, plugin)) {
|
|
1008
1174
|
config.plugin.push(deepClone(plugin))
|
|
@@ -1032,7 +1198,7 @@ export async function installOpenCode(
|
|
|
1032
1198
|
}
|
|
1033
1199
|
if (!Array.isArray(config.plugins)) config.plugins = []
|
|
1034
1200
|
config.plugins = deduplicatePluginReferences(removePantheonPluginReferences(config.plugins))
|
|
1035
|
-
if (Array.isArray(pantheonConfig
|
|
1201
|
+
if (Array.isArray(pantheonConfig?.plugins)) {
|
|
1036
1202
|
for (const plugin of pantheonConfig.plugins) {
|
|
1037
1203
|
if (!isPantheonPluginReference(plugin) && !hasPluginReference(config.plugins, plugin)) {
|
|
1038
1204
|
config.plugins.push(deepClone(plugin))
|
|
@@ -1058,33 +1224,68 @@ export async function installOpenCode(
|
|
|
1058
1224
|
config.plugins.push(resolvedV2)
|
|
1059
1225
|
}
|
|
1060
1226
|
|
|
1061
|
-
if (
|
|
1062
|
-
config.provider
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1227
|
+
if (pantheonConfig !== null) {
|
|
1228
|
+
if (config.provider === undefined && pantheonConfig.provider !== undefined) {
|
|
1229
|
+
config.provider = deepClone(pantheonConfig.provider)
|
|
1230
|
+
} else if (config.provider !== undefined && pantheonConfig.provider !== undefined) {
|
|
1231
|
+
mergeMissing(config.provider, pantheonConfig.provider)
|
|
1232
|
+
}
|
|
1066
1233
|
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1234
|
+
if (config.compaction === undefined && pantheonConfig.compaction !== undefined) {
|
|
1235
|
+
config.compaction = deepClone(pantheonConfig.compaction)
|
|
1236
|
+
} else if (config.compaction !== undefined && pantheonConfig.compaction !== undefined) {
|
|
1237
|
+
mergeMissing(config.compaction, pantheonConfig.compaction)
|
|
1238
|
+
}
|
|
1071
1239
|
}
|
|
1072
1240
|
|
|
1073
1241
|
// --------------------------------------------------------------------
|
|
1074
1242
|
// C. Merge permissions
|
|
1075
1243
|
// --------------------------------------------------------------------
|
|
1076
1244
|
if (config.permission === undefined) config.permission = {}
|
|
1077
|
-
if (pantheonConfig.permission !== undefined) {
|
|
1078
|
-
mergeMissing(config.permission, pantheonConfig.permission)
|
|
1079
|
-
}
|
|
1080
1245
|
if (
|
|
1081
1246
|
typeof config.permission === 'object' &&
|
|
1082
1247
|
config.permission !== null &&
|
|
1083
1248
|
!Array.isArray(config.permission)
|
|
1084
1249
|
) {
|
|
1085
|
-
if
|
|
1250
|
+
// Fill-if-absent, never overwrite. Pantheon's skills cannot be invoked
|
|
1251
|
+
// without this grant, so an install that just installed them seeds it —
|
|
1252
|
+
// but a user who deliberately set `permission.skill` (to `deny`, or to a
|
|
1253
|
+
// partial map of their own) keeps their choice. This was an unconditional
|
|
1254
|
+
// assignment nine lines below a comment that said "never overwrite", which
|
|
1255
|
+
// silently flipped an explicit deny to allow.
|
|
1256
|
+
//
|
|
1257
|
+
// Gated on the skills component AND absent-only, which is the whole point:
|
|
1258
|
+
// `skill` is a capability grant for something this install just wrote to
|
|
1259
|
+
// disk. A runtime-only or agents-only install has nothing to grant, so it
|
|
1260
|
+
// must not seed the key. It used to anyway, via the unconditional
|
|
1261
|
+
// PRODUCT_CONFIG_DEFAULTS.permission merge that sat above this block —
|
|
1262
|
+
// which also made this gate dead code.
|
|
1263
|
+
if (componentSet.has('skills') && config.permission.skill === undefined) {
|
|
1086
1264
|
config.permission.skill = { '*': 'allow' }
|
|
1087
1265
|
}
|
|
1266
|
+
// These five are the commands Pantheon's OWN agents must be able to run
|
|
1267
|
+
// unattended to do their job: version control, package execution, and the
|
|
1268
|
+
// test/lint pair every implementation agent reaches for. That is the whole
|
|
1269
|
+
// bar for a grant made on every install.
|
|
1270
|
+
//
|
|
1271
|
+
// Anything beyond it is the installing user's judgement about which
|
|
1272
|
+
// commands to trust on their machine, to be made in THEIR config — not ours
|
|
1273
|
+
// to grant silently on their behalf. The personal dev allowlist this grew
|
|
1274
|
+
// out of was the same category of decision, one layer further out of view.
|
|
1275
|
+
//
|
|
1276
|
+
// Specifically NOT granted, and why:
|
|
1277
|
+
// - `curl` — arbitrary network egress; the widest grant on the list and
|
|
1278
|
+
// not required to install or run anything here.
|
|
1279
|
+
// - `docker` — host/daemon access; a container runtime is not an
|
|
1280
|
+
// installation requirement.
|
|
1281
|
+
// - `gh` — third-party account access on someone's behalf; a GitHub CLI
|
|
1282
|
+
// login is never something an installer may assume.
|
|
1283
|
+
// - `pip`, `black`, `make` — ordinary toolchain commands with no Pantheon
|
|
1284
|
+
// agent dependency; users who want them can add them.
|
|
1285
|
+
//
|
|
1286
|
+
// NOTE: this grant is NOT gated on the skills component — unlike
|
|
1287
|
+
// permission.skill above, agents are installed by the agents component and
|
|
1288
|
+
// the test/lint commands are needed whichever components were selected.
|
|
1088
1289
|
if (config.permission.bash === undefined) {
|
|
1089
1290
|
config.permission.bash = {
|
|
1090
1291
|
'git *': 'allow',
|
|
@@ -1092,12 +1293,6 @@ export async function installOpenCode(
|
|
|
1092
1293
|
'npx *': 'allow',
|
|
1093
1294
|
'pytest *': 'allow',
|
|
1094
1295
|
'ruff *': 'allow',
|
|
1095
|
-
'black *': 'allow',
|
|
1096
|
-
'pip *': 'allow',
|
|
1097
|
-
'docker *': 'allow',
|
|
1098
|
-
'curl *': 'allow',
|
|
1099
|
-
'gh *': 'allow',
|
|
1100
|
-
'make *': 'allow',
|
|
1101
1296
|
}
|
|
1102
1297
|
}
|
|
1103
1298
|
}
|
|
@@ -1154,82 +1349,18 @@ export async function installOpenCode(
|
|
|
1154
1349
|
// at a venv that does not exist would make every MCP fail to launch.
|
|
1155
1350
|
// --------------------------------------------------------------------
|
|
1156
1351
|
if (componentSet.has('runtime') && runtimeHealthy) {
|
|
1157
|
-
|
|
1158
|
-
//
|
|
1159
|
-
//
|
|
1160
|
-
//
|
|
1161
|
-
// legacy wrapper-shaped entries back to the flat map.
|
|
1162
|
-
// P1-3: derive the MCP python from the SAME venv layout setupVenv
|
|
1163
|
-
// creates (<target>/.venv via venvPythonPath). For project installs the
|
|
1164
|
-
// runtime payload lives under <target>/.opencode (runtimeTarget), but the
|
|
1165
|
-
// venv is created at <target>/.venv — pointing MCP commands at
|
|
1352
|
+
// P1-3: derive the MCP python from the SAME venv layout setupVenv creates
|
|
1353
|
+
// (<target>/.venv via venvPythonPath). For project installs the runtime
|
|
1354
|
+
// payload lives under <target>/.opencode (runtimeTarget), but the venv is
|
|
1355
|
+
// created at <target>/.venv — pointing MCP commands at
|
|
1166
1356
|
// runtimeTarget/.venv would reference an executable that never exists.
|
|
1167
|
-
const runtimeTarget = isGlobal ? target : join(target, '.opencode')
|
|
1168
|
-
const venvPython = venvPythonPath(target)
|
|
1169
1357
|
// Point the config at the venv even on a first install; setupVenv runs
|
|
1170
1358
|
// later in this function and creates this path before OpenCode starts.
|
|
1171
|
-
const
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
type: 'local',
|
|
1177
|
-
cwd: runtimeTarget,
|
|
1178
|
-
command: [memoryPython, 'scripts/mcp_resources_server.py'],
|
|
1179
|
-
enabled: true,
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
if (!config.mcp['pantheon-code-mode']) {
|
|
1183
|
-
config.mcp['pantheon-code-mode'] = {
|
|
1184
|
-
type: 'local',
|
|
1185
|
-
cwd: runtimeTarget,
|
|
1186
|
-
command: [memoryPython, 'scripts/code_mode_server.py'],
|
|
1187
|
-
enabled: true,
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
if (!config.mcp['pantheon-memory']) {
|
|
1191
|
-
config.mcp['pantheon-memory'] = {
|
|
1192
|
-
type: 'local',
|
|
1193
|
-
cwd: runtimeTarget,
|
|
1194
|
-
command: [memoryPython, 'scripts/memory_mcp_server.py'],
|
|
1195
|
-
enabled: true,
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
if (!config.mcp['pantheon-persistence']) {
|
|
1199
|
-
config.mcp['pantheon-persistence'] = {
|
|
1200
|
-
type: 'local',
|
|
1201
|
-
cwd: runtimeTarget,
|
|
1202
|
-
command: [memoryPython, 'scripts/mcp_persistence_server.py'],
|
|
1203
|
-
enabled: true,
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
if (!config.mcp['pantheon-vision']) {
|
|
1207
|
-
config.mcp['pantheon-vision'] = {
|
|
1208
|
-
type: 'local',
|
|
1209
|
-
cwd: runtimeTarget,
|
|
1210
|
-
command: [memoryPython, 'scripts/pantheon_vision_server.py'],
|
|
1211
|
-
enabled: true,
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
// Default MCP permissions
|
|
1216
|
-
config.permission = config.permission || {}
|
|
1217
|
-
config.permission.mcp = config.permission.mcp || {}
|
|
1218
|
-
if (!config.permission.mcp['pantheon-resources']) {
|
|
1219
|
-
config.permission.mcp['pantheon-resources'] = 'allow'
|
|
1220
|
-
}
|
|
1221
|
-
if (!config.permission.mcp['pantheon-code-mode']) {
|
|
1222
|
-
config.permission.mcp['pantheon-code-mode'] = 'ask'
|
|
1223
|
-
}
|
|
1224
|
-
if (!config.permission.mcp['pantheon-memory']) {
|
|
1225
|
-
config.permission.mcp['pantheon-memory'] = 'allow'
|
|
1226
|
-
}
|
|
1227
|
-
if (!config.permission.mcp['pantheon-persistence']) {
|
|
1228
|
-
config.permission.mcp['pantheon-persistence'] = 'allow'
|
|
1229
|
-
}
|
|
1230
|
-
if (!config.permission.mcp['pantheon-vision']) {
|
|
1231
|
-
config.permission.mcp['pantheon-vision'] = 'ask'
|
|
1232
|
-
}
|
|
1359
|
+
const runtimeTarget = isGlobal ? target : join(target, '.opencode')
|
|
1360
|
+
applyMcpServerEntries(config, {
|
|
1361
|
+
runtimeTarget,
|
|
1362
|
+
venvPython: venvPythonPath(target),
|
|
1363
|
+
})
|
|
1233
1364
|
}
|
|
1234
1365
|
|
|
1235
1366
|
// ── V2 native migration ──
|
package/scripts/install-mcp.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import { homedir } from 'node:os'
|
|
|
24
24
|
import { basename, dirname, join, resolve } from 'node:path'
|
|
25
25
|
import { createInterface } from 'node:readline'
|
|
26
26
|
import { fileURLToPath } from 'node:url'
|
|
27
|
+
import { migrateMcpEntrypointPaths } from './install/config-migration.mjs'
|
|
27
28
|
|
|
28
29
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
29
30
|
const ROOT = join(__dirname, '..')
|
|
@@ -70,11 +71,11 @@ function resolveServerScript(relPath) {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
const VENV_PYTHON = resolveVenvPython()
|
|
73
|
-
const MCP_RESOURCES_SERVER = resolveServerScript('scripts/
|
|
74
|
-
const CODE_MODE_SERVER = resolveServerScript('scripts/
|
|
75
|
-
const MEMORY_MCP_SERVER = resolveServerScript('scripts/
|
|
76
|
-
const MCP_PERSISTENCE_SERVER = resolveServerScript('scripts/
|
|
77
|
-
const PANTHEON_VISION_SERVER = resolveServerScript('src/mcp/
|
|
74
|
+
const MCP_RESOURCES_SERVER = resolveServerScript('scripts/mcp_resources.py')
|
|
75
|
+
const CODE_MODE_SERVER = resolveServerScript('scripts/code_mode.py')
|
|
76
|
+
const MEMORY_MCP_SERVER = resolveServerScript('scripts/memory_mcp.py')
|
|
77
|
+
const MCP_PERSISTENCE_SERVER = resolveServerScript('scripts/mcp_persistence.py')
|
|
78
|
+
const PANTHEON_VISION_SERVER = resolveServerScript('src/mcp/pantheon_vision.py')
|
|
78
79
|
|
|
79
80
|
// ---------------------------------------------------------------------------
|
|
80
81
|
// MCP Definitions
|
|
@@ -209,7 +210,7 @@ export const MCPS = {
|
|
|
209
210
|
if (existsSync(scriptPath)) {
|
|
210
211
|
return { ok: true, message: 'server script present' }
|
|
211
212
|
}
|
|
212
|
-
return { ok: false, message: '
|
|
213
|
+
return { ok: false, message: 'mcp_resources.py not found at resolved path' }
|
|
213
214
|
},
|
|
214
215
|
},
|
|
215
216
|
'pantheon-code-mode': {
|
|
@@ -230,7 +231,7 @@ export const MCPS = {
|
|
|
230
231
|
if (existsSync(scriptPath)) {
|
|
231
232
|
return { ok: true, message: 'server script present' }
|
|
232
233
|
}
|
|
233
|
-
return { ok: false, message: '
|
|
234
|
+
return { ok: false, message: 'code_mode.py not found at resolved path' }
|
|
234
235
|
},
|
|
235
236
|
},
|
|
236
237
|
'pantheon-memory': {
|
|
@@ -251,7 +252,7 @@ export const MCPS = {
|
|
|
251
252
|
if (existsSync(scriptPath)) {
|
|
252
253
|
return { ok: true, message: 'server script present' }
|
|
253
254
|
}
|
|
254
|
-
return { ok: false, message: '
|
|
255
|
+
return { ok: false, message: 'memory_mcp.py not found at resolved path' }
|
|
255
256
|
},
|
|
256
257
|
},
|
|
257
258
|
'pantheon-persistence': {
|
|
@@ -272,7 +273,7 @@ export const MCPS = {
|
|
|
272
273
|
if (existsSync(scriptPath)) {
|
|
273
274
|
return { ok: true, message: 'server script present' }
|
|
274
275
|
}
|
|
275
|
-
return { ok: false, message: '
|
|
276
|
+
return { ok: false, message: 'mcp_persistence.py not found at resolved path' }
|
|
276
277
|
},
|
|
277
278
|
},
|
|
278
279
|
'pantheon-vision': {
|
|
@@ -471,8 +472,22 @@ function writeMcpConfig(platformName, mcpKey, dryRun, force) {
|
|
|
471
472
|
config[key] = {}
|
|
472
473
|
}
|
|
473
474
|
|
|
475
|
+
// Heal pre-rename MCP entrypoint paths before the skip check, so re-running
|
|
476
|
+
// the installer repairs an existing config whose `*_server.py` paths were
|
|
477
|
+
// orphaned by the entrypoint rename. Without this the rename is a silent
|
|
478
|
+
// break for every already-installed config (see config-migration.mjs).
|
|
479
|
+
const healed = migrateMcpEntrypointPaths(config[key])
|
|
480
|
+
|
|
474
481
|
// Check if the MCP already has an entry
|
|
475
482
|
if (config[key][mcpKey] && !force) {
|
|
483
|
+
if (healed > 0 && !dryRun) {
|
|
484
|
+
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8')
|
|
485
|
+
return {
|
|
486
|
+
status: 'healed',
|
|
487
|
+
path: configPath,
|
|
488
|
+
reason: `Updated ${healed} stale MCP entrypoint path(s) in ${platform.configFile}`,
|
|
489
|
+
}
|
|
490
|
+
}
|
|
476
491
|
return {
|
|
477
492
|
status: 'skipped',
|
|
478
493
|
reason: `Already exists in ${platform.configFile} (use --force to overwrite)`,
|
|
@@ -705,6 +720,10 @@ function logResult(platformLabel, mcpKey, result) {
|
|
|
705
720
|
console.log(` ⏭️ ${label} — ${result.reason}`)
|
|
706
721
|
progress.skipped.push({ platform: platformLabel, mcp: mcpKey, reason: result.reason })
|
|
707
722
|
break
|
|
723
|
+
case 'healed':
|
|
724
|
+
console.log(` 🩹 ${label} — ${result.reason}`)
|
|
725
|
+
progress.installed.push({ platform: platformLabel, mcp: mcpKey })
|
|
726
|
+
break
|
|
708
727
|
case 'dry-run': {
|
|
709
728
|
const desc =
|
|
710
729
|
result.entry?.type === 'remote' || result.entry?.type === 'http'
|
|
@@ -5,7 +5,7 @@ Key-Value store with FTS5 full-text search, TTL-based expiration,
|
|
|
5
5
|
and namespace isolation. Uses SQLite with zero external dependencies.
|
|
6
6
|
|
|
7
7
|
Usage:
|
|
8
|
-
python scripts/
|
|
8
|
+
python scripts/mcp_persistence.py
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
@@ -15,7 +15,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
15
15
|
|
|
16
16
|
const requestedVersion = valueFor('--version')
|
|
17
17
|
const jsonOutput = process.argv.includes('--json')
|
|
18
|
-
const runtime = fileURLToPath(new URL('./
|
|
18
|
+
const runtime = fileURLToPath(new URL('./mcp_persistence.py', import.meta.url))
|
|
19
19
|
|
|
20
20
|
function valueFor(name) {
|
|
21
21
|
const index = process.argv.indexOf(name)
|
|
@@ -192,7 +192,7 @@ async function main() {
|
|
|
192
192
|
return
|
|
193
193
|
}
|
|
194
194
|
if (!existsSync(runtime)) {
|
|
195
|
-
unavailable('installed package does not include
|
|
195
|
+
unavailable('installed package does not include mcp_persistence.py')
|
|
196
196
|
return
|
|
197
197
|
}
|
|
198
198
|
|