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.
Files changed (43) hide show
  1. package/README.md +101 -3
  2. package/bin/pantheon-init.mjs +79 -0
  3. package/commands/pantheon-vision.md +67 -0
  4. package/opencode.json +13 -9
  5. package/package.json +12 -5
  6. package/scripts/_pantheon_paths.py +0 -2
  7. package/scripts/doctor.mjs +4 -2
  8. package/scripts/install/health-check.mjs +101 -1
  9. package/scripts/install/model-picker.mjs +118 -0
  10. package/scripts/install/opencode.mjs +102 -19
  11. package/scripts/install/venv.mjs +25 -2
  12. package/scripts/install-mcp.mjs +98 -13
  13. package/scripts/mcp_persistence_server.py +1 -1
  14. package/scripts/secret-scan.mjs +88 -0
  15. package/scripts/uninstall.mjs +3 -3
  16. package/scripts/validate-routing.mjs +15 -0
  17. package/src/agents/hermes.md +2 -2
  18. package/src/agents/mnemosyne.md +1 -1
  19. package/src/agents/zeus.md +1 -1
  20. package/src/mcp/_pantheon_paths.py +0 -2
  21. package/src/mcp/install-mcp.mjs +6 -913
  22. package/src/mcp/mcp_persistence_server.py +2 -1
  23. package/src/mcp/pantheon_vision_server.py +495 -0
  24. package/src/mcp/requirements-vision.txt +3 -0
  25. package/src/pantheon/auto-wake.ts +91 -0
  26. package/src/pantheon/background-job-board.ts +568 -0
  27. package/src/pantheon/file-persistence.ts +95 -0
  28. package/src/pantheon/presets.d.mts +108 -0
  29. package/src/pantheon/presets.mjs +554 -0
  30. package/src/pantheon/vision.ts +1437 -0
  31. package/src/plugin.ts +69 -25
  32. package/src/plugins/tui/package.json +3 -3
  33. package/src/plugins/tui/src/index.tsx +866 -121
  34. package/src/routing.yml +148 -0
  35. package/src/skills/context-compression/SKILL.md +1 -1
  36. package/src/skills/loop-engineering/SKILL.md +1 -1
  37. package/src/skills/orchestration-workflow/SKILL.md +11 -11
  38. package/scripts/scrub_secrets.py +0 -281
  39. package/src/plugins/tui/dist/tui.d.ts +0 -8
  40. package/src/plugins/tui/dist/tui.d.ts.map +0 -1
  41. package/src/plugins/tui/dist/tui.js +0 -609
  42. package/src/plugins/tui/dist/tui.js.map +0 -1
  43. 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 { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
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 { colors, configure, section, step, success, warning, error, info, bullet, spinner, printSummary } from './cli-ui.mjs'
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
- import { installPlugin, registerPlugin, unregisterPlugin } from './plugin.mjs'
25
- import { resolveComponents } from './component.mjs'
26
-
27
- const COMPONENT_NAMES = ['agents', 'skills', 'instructions', 'prompts', 'commands', 'plugins', 'runtime']
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) { cfg.subagent_depth = 2; changed = true }
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) + '\n')
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 pythonPath = process.platform === 'win32' ? 'python' : 'python3'
609
- const venvPython = join(runtimeTarget, '.venv', 'bin', 'python3')
610
- const memoryPython = existsSync(venvPython) ? venvPython : pythonPath
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 // Fatal — abort installation
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(` ${colors.dim('\u2500'.repeat(Math.min(process.stdout.columns || 60, 60)))}\n`)
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) process.stdout.write(` ${colors.dim('\u2014')} ${skipped} already up-to-date\n`)
724
- if (errors > 0) process.stdout.write(` ${colors.yellow('\u26a0')} ${errors} error(s) encountered\n`)
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(` ${colors.dim('\u2022')} Run 'opencode' to start using Pantheon agents\n`)
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
  }
@@ -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 = join(venvPath, 'bin', 'python3')
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
- 'Try: PIP_USER=0 ' + pip + ' install -r ' + reqFile + ' --break-system-packages'
116
+ 'Try: PIP_USER=0 ' +
117
+ pip +
118
+ ' install -r ' +
119
+ reqFile +
120
+ ' --break-system-packages',
98
121
  )
99
122
  }
100
123
  }
@@ -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: { type: 'local', command: join(ROOT, '.venv/bin/python3'), args: [join(ROOT, 'scripts/mcp_resources_server.py')] },
175
- vscode: { type: 'stdio', command: join(ROOT, '.venv/bin/python3'), args: [join(ROOT, 'scripts/mcp_resources_server.py')] },
176
- cursor: { type: 'local', command: join(ROOT, '.venv/bin/python3'), args: [join(ROOT, 'scripts/mcp_resources_server.py')] },
177
- claude: { type: 'local', command: join(ROOT, '.venv/bin/python3'), args: [join(ROOT, 'scripts/mcp_resources_server.py')] },
178
- windsurf: { type: 'local', command: join(ROOT, '.venv/bin/python3'), args: [join(ROOT, 'scripts/mcp_resources_server.py')] },
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: { type: 'local', command: join(ROOT, '.venv/bin/python3'), args: [join(ROOT, 'scripts/code_mode_server.py')] },
195
- vscode: { type: 'stdio', command: join(ROOT, '.venv/bin/python3'), args: [join(ROOT, 'scripts/code_mode_server.py')] },
196
- cursor: { type: 'local', command: join(ROOT, '.venv/bin/python3'), args: [join(ROOT, 'scripts/code_mode_server.py')] },
197
- claude: { type: 'local', command: join(ROOT, '.venv/bin/python3'), args: [join(ROOT, 'scripts/code_mode_server.py')] },
198
- windsurf: { type: 'local', command: join(ROOT, '.venv/bin/python3'), args: [join(ROOT, 'scripts/code_mode_server.py')] },
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
- main()
1004
+ if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
1005
+ main()
1006
+ }
@@ -233,7 +233,7 @@ async def kv_store(
233
233
 
234
234
  # Opportunistic auto-purge: if namespace has >500 entries, purge expired
235
235
  _opportunistic_auto_purge(conn, namespace)
236
-
236
+
237
237
  return {"status": "stored", "namespace": namespace, "key": key}
238
238
 
239
239
 
@@ -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
+ }
@@ -36,14 +36,14 @@ const PANTHEON_AGENT_NAMES = [
36
36
  'themis',
37
37
  'prometheus',
38
38
  'hephaestus',
39
-
40
-
39
+
40
+
41
41
  'nyx',
42
42
  'gaia',
43
43
  'iris',
44
44
  'mnemosyne',
45
45
  'talos',
46
-
46
+
47
47
  ]
48
48
 
49
49
  const PLATFORM_LABELS = {
@@ -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)`