pantheon-opencode 1.2.0 → 1.2.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/README.md +101 -3
- package/bin/pantheon-init.mjs +79 -0
- package/commands/pantheon-vision.md +67 -0
- package/opencode.json +13 -9
- package/package.json +12 -5
- package/scripts/_pantheon_paths.py +0 -2
- package/scripts/doctor.mjs +4 -2
- package/scripts/install/health-check.mjs +101 -1
- package/scripts/install/model-picker.mjs +118 -0
- package/scripts/install/opencode.mjs +102 -19
- package/scripts/install/venv.mjs +25 -2
- package/scripts/install-mcp.mjs +98 -13
- package/scripts/mcp_persistence_server.py +1 -1
- package/scripts/secret-scan.mjs +88 -0
- package/scripts/uninstall.mjs +3 -3
- package/scripts/validate-routing.mjs +15 -0
- package/src/agents/hermes.md +2 -2
- package/src/agents/mnemosyne.md +1 -1
- package/src/agents/zeus.md +1 -1
- package/src/mcp/_pantheon_paths.py +0 -2
- package/src/mcp/install-mcp.mjs +6 -913
- package/src/mcp/mcp_persistence_server.py +2 -1
- package/src/mcp/pantheon_vision_server.py +495 -0
- package/src/mcp/requirements-vision.txt +3 -0
- package/src/pantheon/auto-wake.ts +91 -0
- package/src/pantheon/background-job-board.ts +568 -0
- package/src/pantheon/file-persistence.ts +95 -0
- package/src/pantheon/presets.d.mts +108 -0
- package/src/pantheon/presets.mjs +554 -0
- package/src/pantheon/vision.ts +1437 -0
- package/src/plugin.ts +69 -25
- package/src/plugins/tui/package.json +3 -3
- package/src/plugins/tui/src/index.tsx +866 -121
- package/src/routing.yml +148 -0
- package/src/skills/context-compression/SKILL.md +1 -1
- package/src/skills/loop-engineering/SKILL.md +1 -1
- package/src/skills/orchestration-workflow/SKILL.md +11 -11
- package/scripts/scrub_secrets.py +0 -281
- package/src/plugins/tui/dist/tui.d.ts +0 -8
- package/src/plugins/tui/dist/tui.d.ts.map +0 -1
- package/src/plugins/tui/dist/tui.js +0 -609
- package/src/plugins/tui/dist/tui.js.map +0 -1
- package/src/plugins/tui/dist/tui.tsx +0 -444
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* model-picker.mjs — interactive model preset selection + persistence
|
|
4
|
+
*
|
|
5
|
+
* Used by the installer (interactive prompt) and the `pantheon-opencode`
|
|
6
|
+
* CLI. Writes the active preset marker to <presetDir>/.pantheon/active-preset.json
|
|
7
|
+
* which the plugin (src/plugin.ts) reads on startup via resolveActivePreset().
|
|
8
|
+
*/
|
|
9
|
+
import { copyFileSync, existsSync, mkdirSync, renameSync, writeFileSync } from 'node:fs'
|
|
10
|
+
import { join } from 'node:path'
|
|
11
|
+
import { createInterface } from 'node:readline/promises'
|
|
12
|
+
import { loadPresetDefs } from '../../src/pantheon/presets.mjs'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Build the active-preset.json payload (version 1).
|
|
16
|
+
*
|
|
17
|
+
* @param {string} presetName
|
|
18
|
+
* @param {string} [source] 'cli' | 'interactive'
|
|
19
|
+
* @returns {{version: number, preset: string, source: string, updated_at: string}}
|
|
20
|
+
*/
|
|
21
|
+
export function buildActivePresetFile(presetName, source = 'cli') {
|
|
22
|
+
return {
|
|
23
|
+
version: 1,
|
|
24
|
+
preset: presetName,
|
|
25
|
+
source,
|
|
26
|
+
updated_at: new Date().toISOString(),
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Persist the active preset marker, backing up any previous file to .bak.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} presetDir install target (project root or global opencode dir)
|
|
34
|
+
* @param {string} presetName
|
|
35
|
+
* @param {{dryRun?: boolean, source?: string, logger?: object}} [opts]
|
|
36
|
+
* @returns {{written: boolean, path: string, backupPath: string|null}}
|
|
37
|
+
*/
|
|
38
|
+
export function writeActivePreset(
|
|
39
|
+
presetDir,
|
|
40
|
+
presetName,
|
|
41
|
+
{ dryRun = false, source = 'cli', logger = console } = {},
|
|
42
|
+
) {
|
|
43
|
+
const dir = join(presetDir, '.pantheon')
|
|
44
|
+
const filePath = join(dir, 'active-preset.json')
|
|
45
|
+
const backupPath = join(dir, 'active-preset.json.bak')
|
|
46
|
+
|
|
47
|
+
if (dryRun) {
|
|
48
|
+
logger.log?.(`[dry-run] Would write model preset "${presetName}" to ${filePath}`)
|
|
49
|
+
return { written: false, path: filePath, backupPath: null }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
mkdirSync(dir, { recursive: true })
|
|
53
|
+
let backupMade = false
|
|
54
|
+
if (existsSync(filePath)) {
|
|
55
|
+
copyFileSync(filePath, backupPath)
|
|
56
|
+
backupMade = true
|
|
57
|
+
}
|
|
58
|
+
writeFileSync(`${filePath}.tmp-${process.pid}`, `${JSON.stringify(buildActivePresetFile(presetName, source), null, 2)}\n`)
|
|
59
|
+
renameSync(`${filePath}.tmp-${process.pid}`, filePath)
|
|
60
|
+
return { written: true, path: filePath, backupPath: backupMade ? backupPath : null }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Interactive model preset picker. Asks the user to choose a preset; persists
|
|
65
|
+
* the choice unless dryRun. `rl` may be injected for tests; otherwise a
|
|
66
|
+
* readline/promises interface on stdin/stdout is created and closed.
|
|
67
|
+
*
|
|
68
|
+
* @param {{presetDir?: string, presets?: object, dryRun?: boolean, logger?: object, rl?: object}} [opts]
|
|
69
|
+
* @returns {Promise<string|null>} chosen preset name or null (skipped/unknown)
|
|
70
|
+
*/
|
|
71
|
+
export async function runModelPicker({
|
|
72
|
+
presetDir,
|
|
73
|
+
presets,
|
|
74
|
+
dryRun = false,
|
|
75
|
+
logger = console,
|
|
76
|
+
rl,
|
|
77
|
+
} = {}) {
|
|
78
|
+
const defs = presets ?? loadPresetDefs()
|
|
79
|
+
const names = Object.keys(defs)
|
|
80
|
+
if (names.length === 0) {
|
|
81
|
+
logger.warn?.('Model presets: no presets defined in routing.yml')
|
|
82
|
+
return null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const list = names.map((n, i) => ` ${i + 1}) ${n} — ${defs[n].description ?? ''}`).join('\n')
|
|
86
|
+
const rli = rl ?? createInterface({ input: process.stdin, output: process.stdout })
|
|
87
|
+
try {
|
|
88
|
+
const answer = await rli.question(
|
|
89
|
+
`\nModel presets (number or name; blank to skip):\n${list}\n> `,
|
|
90
|
+
)
|
|
91
|
+
const trimmed = (answer ?? '').trim()
|
|
92
|
+
if (trimmed === '' || trimmed.toLowerCase() === 'none') {
|
|
93
|
+
logger.log?.('Skipping model preset selection.')
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
let name = null
|
|
97
|
+
if (names.includes(trimmed)) {
|
|
98
|
+
name = trimmed
|
|
99
|
+
} else {
|
|
100
|
+
const idx = Number(trimmed)
|
|
101
|
+
if (Number.isInteger(idx) && idx >= 1 && idx <= names.length) {
|
|
102
|
+
name = names[idx - 1]
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (!name) {
|
|
106
|
+
logger.warn?.(`Unknown preset "${trimmed}" — skipping model preset selection.`)
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
if (!dryRun) {
|
|
110
|
+
writeActivePreset(presetDir, name, { dryRun: false, source: 'interactive', logger })
|
|
111
|
+
} else {
|
|
112
|
+
logger.log?.(`[dry-run] Would set model preset "${name}"`)
|
|
113
|
+
}
|
|
114
|
+
return name
|
|
115
|
+
} finally {
|
|
116
|
+
if (!rl) rli.close()
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -3,12 +3,33 @@
|
|
|
3
3
|
* opencode.mjs — OpenCode platform installer
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
chmodSync,
|
|
8
|
+
existsSync,
|
|
9
|
+
mkdirSync,
|
|
10
|
+
readdirSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
rmSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
} from 'node:fs'
|
|
7
15
|
import { homedir } from 'node:os'
|
|
8
16
|
import { join, resolve } from 'node:path'
|
|
17
|
+
import {
|
|
18
|
+
bullet,
|
|
19
|
+
colors,
|
|
20
|
+
configure,
|
|
21
|
+
error,
|
|
22
|
+
info,
|
|
23
|
+
printSummary,
|
|
24
|
+
section,
|
|
25
|
+
spinner,
|
|
26
|
+
step,
|
|
27
|
+
success,
|
|
28
|
+
warning,
|
|
29
|
+
} from './cli-ui.mjs'
|
|
9
30
|
import { healthCheck } from './health-check.mjs'
|
|
10
31
|
import { detectVersion, runMigrations } from './migrate.mjs'
|
|
11
|
-
import {
|
|
32
|
+
import { installPlugin, registerPlugin, unregisterPlugin } from './plugin.mjs'
|
|
12
33
|
import {
|
|
13
34
|
collectSkillNames,
|
|
14
35
|
copyFiles,
|
|
@@ -20,11 +41,17 @@ import {
|
|
|
20
41
|
syncDir,
|
|
21
42
|
writeIfChanged,
|
|
22
43
|
} from './shared.mjs'
|
|
23
|
-
import { setupVenv } from './venv.mjs'
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
44
|
+
import { setupVenv, venvPythonPath } from './venv.mjs'
|
|
45
|
+
|
|
46
|
+
const COMPONENT_NAMES = [
|
|
47
|
+
'agents',
|
|
48
|
+
'skills',
|
|
49
|
+
'instructions',
|
|
50
|
+
'prompts',
|
|
51
|
+
'commands',
|
|
52
|
+
'plugins',
|
|
53
|
+
'runtime',
|
|
54
|
+
]
|
|
28
55
|
|
|
29
56
|
function deepClone(value) {
|
|
30
57
|
return JSON.parse(JSON.stringify(value))
|
|
@@ -63,7 +90,7 @@ function mergeMissing(target, source) {
|
|
|
63
90
|
* Global installs use a flat layout: agents/ skills/ commands/ plugins/
|
|
64
91
|
* Project installs use the .opencode/ sub-directory layout.
|
|
65
92
|
*/
|
|
66
|
-
function isGlobalConfigDir(target) {
|
|
93
|
+
export function isGlobalConfigDir(target) {
|
|
67
94
|
// Primary: ~/.opencode (actual OpenCode installation)
|
|
68
95
|
const homeDir = resolve(join(homedir(), '.opencode'))
|
|
69
96
|
if (resolve(target) === homeDir) return true
|
|
@@ -261,9 +288,12 @@ export async function installOpenCode(
|
|
|
261
288
|
try {
|
|
262
289
|
const cfg = JSON.parse(readFileSync(cfgPath, 'utf8'))
|
|
263
290
|
let changed = false
|
|
264
|
-
if (cfg.subagent_depth === undefined) {
|
|
291
|
+
if (cfg.subagent_depth === undefined) {
|
|
292
|
+
cfg.subagent_depth = 2
|
|
293
|
+
changed = true
|
|
294
|
+
}
|
|
265
295
|
if (changed && !dryRun) {
|
|
266
|
-
writeFileSync(cfgPath, JSON.stringify(cfg, null, 2)
|
|
296
|
+
writeFileSync(cfgPath, `${JSON.stringify(cfg, null, 2)}\n`)
|
|
267
297
|
success('subagent_depth: 2 added to opencode.json')
|
|
268
298
|
}
|
|
269
299
|
} catch (e) {
|
|
@@ -314,12 +344,16 @@ export async function installOpenCode(
|
|
|
314
344
|
'scrub-secrets.py',
|
|
315
345
|
'_pantheon_paths.py',
|
|
316
346
|
'mcp_persistence_server.py',
|
|
347
|
+
'pantheon_vision_server.py',
|
|
317
348
|
]
|
|
318
349
|
const srcScriptsDir = join(ROOT, 'scripts')
|
|
350
|
+
const canonicalMcpScripts = {
|
|
351
|
+
'pantheon_vision_server.py': join(ROOT, 'src', 'mcp', 'pantheon_vision_server.py'),
|
|
352
|
+
}
|
|
319
353
|
const dstScriptsDir = join(runtimeTarget, 'scripts')
|
|
320
354
|
if (!dryRun) mkdirSync(dstScriptsDir, { recursive: true })
|
|
321
355
|
for (const script of mcpScripts) {
|
|
322
|
-
const src = join(srcScriptsDir, script)
|
|
356
|
+
const src = canonicalMcpScripts[script] || join(srcScriptsDir, script)
|
|
323
357
|
if (!existsSync(src)) {
|
|
324
358
|
warning(`Script not found: ${src}`)
|
|
325
359
|
stats.errors++
|
|
@@ -341,6 +375,18 @@ export async function installOpenCode(
|
|
|
341
375
|
} else stats.skipped++
|
|
342
376
|
}
|
|
343
377
|
|
|
378
|
+
// Keep Vision's dependency contract available to the installed runtime.
|
|
379
|
+
const visionRequirements = join(ROOT, 'src', 'mcp', 'requirements-vision.txt')
|
|
380
|
+
if (existsSync(visionRequirements)) {
|
|
381
|
+
const destination = join(runtimeTarget, 'requirements-vision.txt')
|
|
382
|
+
const status = writeIfChanged(destination, readFileSync(visionRequirements, 'utf8'), dryRun)
|
|
383
|
+
if (status === 'created') stats.created++
|
|
384
|
+
else stats.skipped++
|
|
385
|
+
} else {
|
|
386
|
+
warning(`Requirements file not found: ${visionRequirements}`)
|
|
387
|
+
stats.errors++
|
|
388
|
+
}
|
|
389
|
+
|
|
344
390
|
// ── Code-mode scripts ──
|
|
345
391
|
const srcCodeModeDir = join(ROOT, '.pantheon', 'code-mode')
|
|
346
392
|
const dstCodeModeDir = join(runtimeTarget, '.pantheon', 'code-mode')
|
|
@@ -604,10 +650,16 @@ export async function installOpenCode(
|
|
|
604
650
|
// --------------------------------------------------------------------
|
|
605
651
|
if (componentSet.has('runtime')) {
|
|
606
652
|
config.mcp = config.mcp || {}
|
|
653
|
+
// P1-3: derive the MCP python from the SAME venv layout setupVenv
|
|
654
|
+
// creates (<target>/.venv via venvPythonPath). For project installs the
|
|
655
|
+
// runtime payload lives under <target>/.opencode (runtimeTarget), but the
|
|
656
|
+
// venv is created at <target>/.venv — pointing MCP commands at
|
|
657
|
+
// runtimeTarget/.venv would reference an executable that never exists.
|
|
607
658
|
const runtimeTarget = isGlobal ? target : join(target, '.opencode')
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
659
|
+
const venvPython = venvPythonPath(target)
|
|
660
|
+
// Point the config at the venv even on a first install; setupVenv runs
|
|
661
|
+
// later in this function and creates this path before OpenCode starts.
|
|
662
|
+
const memoryPython = venvPython
|
|
611
663
|
|
|
612
664
|
// Only add if not already configured by user
|
|
613
665
|
if (!config.mcp['pantheon-resources']) {
|
|
@@ -642,6 +694,14 @@ export async function installOpenCode(
|
|
|
642
694
|
enabled: true,
|
|
643
695
|
}
|
|
644
696
|
}
|
|
697
|
+
if (!config.mcp['pantheon-vision']) {
|
|
698
|
+
config.mcp['pantheon-vision'] = {
|
|
699
|
+
type: 'local',
|
|
700
|
+
cwd: runtimeTarget,
|
|
701
|
+
command: [memoryPython, 'scripts/pantheon_vision_server.py'],
|
|
702
|
+
enabled: true,
|
|
703
|
+
}
|
|
704
|
+
}
|
|
645
705
|
|
|
646
706
|
// Default MCP permissions
|
|
647
707
|
config.permission = config.permission || {}
|
|
@@ -658,6 +718,9 @@ export async function installOpenCode(
|
|
|
658
718
|
if (!config.permission.mcp['pantheon-persistence']) {
|
|
659
719
|
config.permission.mcp['pantheon-persistence'] = 'allow'
|
|
660
720
|
}
|
|
721
|
+
if (!config.permission.mcp['pantheon-vision']) {
|
|
722
|
+
config.permission.mcp['pantheon-vision'] = 'ask'
|
|
723
|
+
}
|
|
661
724
|
}
|
|
662
725
|
|
|
663
726
|
const configContent = `${JSON.stringify(config, null, 2)}\n`
|
|
@@ -707,7 +770,7 @@ export async function installOpenCode(
|
|
|
707
770
|
}
|
|
708
771
|
} catch (err) {
|
|
709
772
|
error(`Setup failed: ${err.message}`)
|
|
710
|
-
throw err
|
|
773
|
+
throw err // Fatal — abort installation
|
|
711
774
|
}
|
|
712
775
|
}
|
|
713
776
|
|
|
@@ -718,18 +781,38 @@ export async function installOpenCode(
|
|
|
718
781
|
|
|
719
782
|
process.stdout.write('\n')
|
|
720
783
|
process.stdout.write(` ${colors.bold(colors.green('Installation complete'))}\n`)
|
|
721
|
-
process.stdout.write(
|
|
784
|
+
process.stdout.write(
|
|
785
|
+
` ${colors.dim('\u2500'.repeat(Math.min(process.stdout.columns || 60, 60)))}\n`,
|
|
786
|
+
)
|
|
722
787
|
process.stdout.write(` ${colors.green('\u2713')} ${created} component(s) installed\n`)
|
|
723
|
-
if (skipped > 0)
|
|
724
|
-
|
|
788
|
+
if (skipped > 0)
|
|
789
|
+
process.stdout.write(` ${colors.dim('\u2014')} ${skipped} already up-to-date\n`)
|
|
790
|
+
if (errors > 0)
|
|
791
|
+
process.stdout.write(` ${colors.yellow('\u26a0')} ${errors} error(s) encountered\n`)
|
|
725
792
|
process.stdout.write('\n')
|
|
726
793
|
process.stdout.write(` ${colors.bold('Next steps:')}\n`)
|
|
727
794
|
process.stdout.write(` ${colors.dim('\u2022')} Configure agents in opencode.json\n`)
|
|
728
795
|
process.stdout.write(` ${colors.dim('\u2022')} Add MCP servers in mcp.json\n`)
|
|
729
796
|
process.stdout.write(` ${colors.dim('\u2022')} Run 'opencode doctor' to verify\n`)
|
|
730
|
-
process.stdout.write(
|
|
797
|
+
process.stdout.write(
|
|
798
|
+
` ${colors.dim('\u2022')} Run 'opencode' to start using Pantheon agents\n`,
|
|
799
|
+
)
|
|
731
800
|
process.stdout.write('\n')
|
|
732
801
|
} else {
|
|
733
802
|
printSummary(target, ['opencode'], stats)
|
|
734
803
|
}
|
|
804
|
+
|
|
805
|
+
// -----------------------------------------------------------------------
|
|
806
|
+
// 5. Model preset selection (--components agents). Interactive picker runs
|
|
807
|
+
// unless autoYes (use defaults) or an explicit --preset was given.
|
|
808
|
+
// -----------------------------------------------------------------------
|
|
809
|
+
if (interactive && !dryRun && !autoYes && !opts.preset) {
|
|
810
|
+
const { runModelPicker } = await import('./model-picker.mjs')
|
|
811
|
+
await runModelPicker({ presetDir: target, logger: console })
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (opts.preset && !dryRun) {
|
|
815
|
+
const { writeActivePreset } = await import('./model-picker.mjs')
|
|
816
|
+
writeActivePreset(target, opts.preset, { source: 'cli' })
|
|
817
|
+
}
|
|
735
818
|
}
|
package/scripts/install/venv.mjs
CHANGED
|
@@ -17,6 +17,25 @@ import { fileURLToPath } from 'node:url'
|
|
|
17
17
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
18
18
|
const ROOT = join(__dirname, '..', '..')
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the python executable of the venv that setupVenv creates for a
|
|
22
|
+
* target. Single source of truth for the venv layout: the venv always lives
|
|
23
|
+
* at <target>/.venv — even for PROJECT installs, where the runtime payload
|
|
24
|
+
* goes to <target>/.opencode. MCP commands and health checks MUST derive
|
|
25
|
+
* their python from here (never from a runtime sub-directory), or a project
|
|
26
|
+
* install would point at <target>/.opencode/.venv/bin/python3, which
|
|
27
|
+
* setupVenv never creates (P1-3).
|
|
28
|
+
*
|
|
29
|
+
* @param {string} target - target installation directory
|
|
30
|
+
* @returns {string} absolute path to the venv python executable
|
|
31
|
+
*/
|
|
32
|
+
export function venvPythonPath(target) {
|
|
33
|
+
const venvPath = join(target, '.venv')
|
|
34
|
+
return process.platform === 'win32'
|
|
35
|
+
? join(venvPath, 'Scripts', 'python.exe')
|
|
36
|
+
: join(venvPath, 'bin', 'python3')
|
|
37
|
+
}
|
|
38
|
+
|
|
20
39
|
/**
|
|
21
40
|
* Set up a Python virtual environment and install MCP dependencies.
|
|
22
41
|
*
|
|
@@ -26,7 +45,7 @@ const ROOT = join(__dirname, '..', '..')
|
|
|
26
45
|
*/
|
|
27
46
|
export function setupVenv(target, { dryRun = false, skipInstall = false, force = false } = {}) {
|
|
28
47
|
const venvPath = join(target, '.venv')
|
|
29
|
-
const pythonBin =
|
|
48
|
+
const pythonBin = venvPythonPath(target)
|
|
30
49
|
|
|
31
50
|
// Step 1: Force recreate venv if --force
|
|
32
51
|
if (force && existsSync(venvPath)) {
|
|
@@ -94,7 +113,11 @@ export function setupVenv(target, { dryRun = false, skipInstall = false, force =
|
|
|
94
113
|
if (r2.status !== 0) {
|
|
95
114
|
throw new Error(
|
|
96
115
|
'Failed to install MCP dependencies. ' +
|
|
97
|
-
|
|
116
|
+
'Try: PIP_USER=0 ' +
|
|
117
|
+
pip +
|
|
118
|
+
' install -r ' +
|
|
119
|
+
reqFile +
|
|
120
|
+
' --break-system-packages',
|
|
98
121
|
)
|
|
99
122
|
}
|
|
100
123
|
}
|
package/scripts/install-mcp.mjs
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
import { execSync } from 'node:child_process'
|
|
24
24
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
25
25
|
import { homedir } from 'node:os'
|
|
26
|
-
import { dirname, join } from 'node:path'
|
|
26
|
+
import { dirname, join, resolve } from 'node:path'
|
|
27
27
|
import { createInterface } from 'node:readline'
|
|
28
28
|
import { fileURLToPath } from 'node:url'
|
|
29
29
|
|
|
@@ -171,11 +171,31 @@ const MCPS = {
|
|
|
171
171
|
name: 'Pantheon Resources',
|
|
172
172
|
description: 'Pantheon framework resources — agents, skills, routing, deepwork, memory bank',
|
|
173
173
|
platforms: {
|
|
174
|
-
opencode: {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
174
|
+
opencode: {
|
|
175
|
+
type: 'local',
|
|
176
|
+
command: join(ROOT, '.venv/bin/python3'),
|
|
177
|
+
args: [join(ROOT, 'scripts/mcp_resources_server.py')],
|
|
178
|
+
},
|
|
179
|
+
vscode: {
|
|
180
|
+
type: 'stdio',
|
|
181
|
+
command: join(ROOT, '.venv/bin/python3'),
|
|
182
|
+
args: [join(ROOT, 'scripts/mcp_resources_server.py')],
|
|
183
|
+
},
|
|
184
|
+
cursor: {
|
|
185
|
+
type: 'local',
|
|
186
|
+
command: join(ROOT, '.venv/bin/python3'),
|
|
187
|
+
args: [join(ROOT, 'scripts/mcp_resources_server.py')],
|
|
188
|
+
},
|
|
189
|
+
claude: {
|
|
190
|
+
type: 'local',
|
|
191
|
+
command: join(ROOT, '.venv/bin/python3'),
|
|
192
|
+
args: [join(ROOT, 'scripts/mcp_resources_server.py')],
|
|
193
|
+
},
|
|
194
|
+
windsurf: {
|
|
195
|
+
type: 'local',
|
|
196
|
+
command: join(ROOT, '.venv/bin/python3'),
|
|
197
|
+
args: [join(ROOT, 'scripts/mcp_resources_server.py')],
|
|
198
|
+
},
|
|
179
199
|
},
|
|
180
200
|
env: [],
|
|
181
201
|
validate: async () => {
|
|
@@ -191,11 +211,31 @@ const MCPS = {
|
|
|
191
211
|
name: 'Pantheon Code Mode',
|
|
192
212
|
description: 'Execute orchestration scripts from .pantheon/code-mode/ directory',
|
|
193
213
|
platforms: {
|
|
194
|
-
opencode: {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
214
|
+
opencode: {
|
|
215
|
+
type: 'local',
|
|
216
|
+
command: join(ROOT, '.venv/bin/python3'),
|
|
217
|
+
args: [join(ROOT, 'scripts/code_mode_server.py')],
|
|
218
|
+
},
|
|
219
|
+
vscode: {
|
|
220
|
+
type: 'stdio',
|
|
221
|
+
command: join(ROOT, '.venv/bin/python3'),
|
|
222
|
+
args: [join(ROOT, 'scripts/code_mode_server.py')],
|
|
223
|
+
},
|
|
224
|
+
cursor: {
|
|
225
|
+
type: 'local',
|
|
226
|
+
command: join(ROOT, '.venv/bin/python3'),
|
|
227
|
+
args: [join(ROOT, 'scripts/code_mode_server.py')],
|
|
228
|
+
},
|
|
229
|
+
claude: {
|
|
230
|
+
type: 'local',
|
|
231
|
+
command: join(ROOT, '.venv/bin/python3'),
|
|
232
|
+
args: [join(ROOT, 'scripts/code_mode_server.py')],
|
|
233
|
+
},
|
|
234
|
+
windsurf: {
|
|
235
|
+
type: 'local',
|
|
236
|
+
command: join(ROOT, '.venv/bin/python3'),
|
|
237
|
+
args: [join(ROOT, 'scripts/code_mode_server.py')],
|
|
238
|
+
},
|
|
199
239
|
},
|
|
200
240
|
env: [],
|
|
201
241
|
validate: async () => {
|
|
@@ -278,6 +318,49 @@ const MCPS = {
|
|
|
278
318
|
return { ok: false, message: 'scripts/mcp_persistence_server.py not found' }
|
|
279
319
|
},
|
|
280
320
|
},
|
|
321
|
+
'pantheon-vision': {
|
|
322
|
+
tier: 1,
|
|
323
|
+
name: 'Pantheon Vision',
|
|
324
|
+
description:
|
|
325
|
+
'Image description, OCR, and structured analysis through the OpenCode vision gateway',
|
|
326
|
+
requirements: join(ROOT, 'src', 'mcp', 'requirements-vision.txt'),
|
|
327
|
+
platforms: {
|
|
328
|
+
opencode: {
|
|
329
|
+
type: 'local',
|
|
330
|
+
command: join(ROOT, '.venv/bin/python3'),
|
|
331
|
+
args: [join(ROOT, 'src', 'mcp', 'pantheon_vision_server.py')],
|
|
332
|
+
},
|
|
333
|
+
vscode: {
|
|
334
|
+
type: 'stdio',
|
|
335
|
+
command: join(ROOT, '.venv/bin/python3'),
|
|
336
|
+
args: [join(ROOT, 'src', 'mcp', 'pantheon_vision_server.py')],
|
|
337
|
+
},
|
|
338
|
+
claude: {
|
|
339
|
+
type: 'local',
|
|
340
|
+
command: join(ROOT, '.venv/bin/python3'),
|
|
341
|
+
args: [join(ROOT, 'src', 'mcp', 'pantheon_vision_server.py')],
|
|
342
|
+
},
|
|
343
|
+
cursor: {
|
|
344
|
+
type: 'local',
|
|
345
|
+
command: join(ROOT, '.venv/bin/python3'),
|
|
346
|
+
args: [join(ROOT, 'src', 'mcp', 'pantheon_vision_server.py')],
|
|
347
|
+
},
|
|
348
|
+
windsurf: {
|
|
349
|
+
type: 'local',
|
|
350
|
+
command: join(ROOT, '.venv/bin/python3'),
|
|
351
|
+
args: [join(ROOT, 'src', 'mcp', 'pantheon_vision_server.py')],
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
env: [],
|
|
355
|
+
validate: async () => {
|
|
356
|
+
const sourcePath = join(ROOT, 'src', 'mcp', 'pantheon_vision_server.py')
|
|
357
|
+
const requirementsPath = join(ROOT, 'src', 'mcp', 'requirements-vision.txt')
|
|
358
|
+
if (existsSync(sourcePath) && existsSync(requirementsPath)) {
|
|
359
|
+
return { ok: true, message: 'canonical server source and isolated requirements present' }
|
|
360
|
+
}
|
|
361
|
+
return { ok: false, message: 'canonical vision source or requirements not found' }
|
|
362
|
+
},
|
|
363
|
+
},
|
|
281
364
|
docker: {
|
|
282
365
|
tier: 2,
|
|
283
366
|
name: 'Docker MCP',
|
|
@@ -790,7 +873,7 @@ function logValidation(mcpKey, result) {
|
|
|
790
873
|
// Main
|
|
791
874
|
// ---------------------------------------------------------------------------
|
|
792
875
|
|
|
793
|
-
async function main() {
|
|
876
|
+
export async function main() {
|
|
794
877
|
const args = parseArgs(process.argv)
|
|
795
878
|
|
|
796
879
|
if (args.help) {
|
|
@@ -918,4 +1001,6 @@ async function main() {
|
|
|
918
1001
|
process.exit(progress.errors.length > 0 ? 1 : 0)
|
|
919
1002
|
}
|
|
920
1003
|
|
|
921
|
-
|
|
1004
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
|
|
1005
|
+
main()
|
|
1006
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/** Scan versionable files without printing secret values. */
|
|
4
|
+
|
|
5
|
+
import { execFileSync } from 'node:child_process'
|
|
6
|
+
import { readFileSync } from 'node:fs'
|
|
7
|
+
import { resolve } from 'node:path'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
|
|
10
|
+
const bifrostHeader = ['x', '-bf-', 'vk'].join('')
|
|
11
|
+
const bifrostTokenPrefix = ['sk', '-bf-'].join('')
|
|
12
|
+
const apiKeyName = ['api', 'Key'].join('')
|
|
13
|
+
const authorizationName = ['Author', 'ization'].join('')
|
|
14
|
+
|
|
15
|
+
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
16
|
+
|
|
17
|
+
// Files that may reference credential PATTERN NAMES (not values): gitleaks
|
|
18
|
+
// config, security policy, scan tooling and its fixtures. Keep in sync with
|
|
19
|
+
// the [allowlist] paths in .gitleaks.toml.
|
|
20
|
+
export const allowlistedFiles = new Set([
|
|
21
|
+
'.gitleaks.toml',
|
|
22
|
+
'.gitleaksignore',
|
|
23
|
+
'SECURITY.md',
|
|
24
|
+
'scripts/secret-scan.mjs',
|
|
25
|
+
'tests/test_secret_scan.mjs',
|
|
26
|
+
])
|
|
27
|
+
|
|
28
|
+
const patterns = [
|
|
29
|
+
{ label: 'Bifrost credential header', regex: new RegExp(escapeRegExp(bifrostHeader), 'i') },
|
|
30
|
+
{
|
|
31
|
+
label: 'Bifrost credential value',
|
|
32
|
+
regex: new RegExp(`${escapeRegExp(bifrostTokenPrefix)}[A-Za-z0-9_-]{8,}`, 'i'),
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
label: 'literal API key value',
|
|
36
|
+
regex: new RegExp(
|
|
37
|
+
`["']${escapeRegExp(apiKeyName)}["']\\s*[:=]\\s*["'](?!\\$\\{|process\\.env|os\\.getenv|env:)[^"']{12,}["']`,
|
|
38
|
+
'i',
|
|
39
|
+
),
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
label: 'literal Authorization bearer value',
|
|
43
|
+
regex: new RegExp(
|
|
44
|
+
`["']${escapeRegExp(authorizationName)}["']\\s*[:=]\\s*["']Bearer\\s+[A-Za-z0-9._-]{12,}["']`,
|
|
45
|
+
'i',
|
|
46
|
+
),
|
|
47
|
+
},
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
export function scanText(text, file = '<text>') {
|
|
51
|
+
return patterns.filter(({ regex }) => regex.test(text)).map(({ label }) => ({ file, label }))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function versionableFiles() {
|
|
55
|
+
return execFileSync('git', ['ls-files', '-z', '--cached', '--others', '--exclude-standard'], {
|
|
56
|
+
encoding: 'buffer',
|
|
57
|
+
})
|
|
58
|
+
.toString()
|
|
59
|
+
.split('\0')
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function scanVersionableFiles() {
|
|
64
|
+
const findings = []
|
|
65
|
+
for (const file of versionableFiles()) {
|
|
66
|
+
if (allowlistedFiles.has(file)) continue
|
|
67
|
+
let text
|
|
68
|
+
try {
|
|
69
|
+
const content = readFileSync(file)
|
|
70
|
+
if (content.includes(0)) continue
|
|
71
|
+
text = content.toString('utf8')
|
|
72
|
+
} catch {
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
findings.push(...scanText(text, file))
|
|
76
|
+
}
|
|
77
|
+
return findings
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
|
|
81
|
+
const findings = scanVersionableFiles()
|
|
82
|
+
if (findings.length > 0) {
|
|
83
|
+
for (const finding of findings) console.error(`[SECRET SCAN] ${finding.file}: ${finding.label}`)
|
|
84
|
+
process.exitCode = 1
|
|
85
|
+
} else {
|
|
86
|
+
console.log('Secret scan passed: no versionable Bifrost/API credentials detected.')
|
|
87
|
+
}
|
|
88
|
+
}
|
package/scripts/uninstall.mjs
CHANGED
|
@@ -18,6 +18,7 @@ import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
|
18
18
|
import { dirname, join } from 'node:path'
|
|
19
19
|
import { fileURLToPath } from 'node:url'
|
|
20
20
|
import yaml from 'js-yaml'
|
|
21
|
+
import { validatePresetDefs } from '../src/pantheon/presets.mjs'
|
|
21
22
|
|
|
22
23
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
23
24
|
const ROOT = join(__dirname, '..')
|
|
@@ -150,6 +151,20 @@ for (const [name, info] of Object.entries(routing.agents || {})) {
|
|
|
150
151
|
}
|
|
151
152
|
}
|
|
152
153
|
|
|
154
|
+
// F. Model preset validation
|
|
155
|
+
const presetDefs = routing.presets || {}
|
|
156
|
+
console.log(`\n Model presets: ${Object.keys(presetDefs).length} defined`)
|
|
157
|
+
|
|
158
|
+
const presetKnownAgents = routingAgents.filter((a) => a !== 'zen' && a !== 'zeus_copilot')
|
|
159
|
+
const presetValidation = validatePresetDefs(presetDefs, { agents: presetKnownAgents })
|
|
160
|
+
for (const err of presetValidation.errors) {
|
|
161
|
+
check(false, `Preset ${err}`)
|
|
162
|
+
}
|
|
163
|
+
for (const w of presetValidation.warnings) {
|
|
164
|
+
warn(`Preset ${w}`)
|
|
165
|
+
}
|
|
166
|
+
console.log(` Presets defined: ${Object.keys(presetDefs).length}`)
|
|
167
|
+
|
|
153
168
|
// Summary
|
|
154
169
|
console.log(`\n${'='.repeat(50)}`)
|
|
155
170
|
const status = failures === 0 ? '✅ PASSED' : `❌ FAILED (${failures}/${checks} checks failed)`
|