pantheon-opencode 1.2.0 → 1.2.2

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 (63) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +112 -15
  3. package/bin/pantheon-init.mjs +93 -0
  4. package/commands/pantheon-vision.md +67 -0
  5. package/opencode.json +14 -10
  6. package/package.json +20 -4
  7. package/scripts/_pantheon_paths.py +0 -2
  8. package/scripts/changelog-extract.mjs +112 -0
  9. package/scripts/doctor.mjs +307 -15
  10. package/scripts/hooks/audit-imports.sh +43 -0
  11. package/scripts/hooks/format-multi-language.sh +96 -0
  12. package/scripts/hooks/log-session-start.sh +24 -0
  13. package/scripts/hooks/on-subagent-delegation-start.sh +91 -0
  14. package/scripts/hooks/on-subagent-delegation-stop.sh +37 -0
  15. package/scripts/hooks/run-type-check.sh +21 -0
  16. package/scripts/hooks/scan-secrets.sh +55 -0
  17. package/scripts/hooks/validate-post-conditions.sh +21 -0
  18. package/scripts/hooks/validate-talos-scope.sh +43 -0
  19. package/scripts/hooks/validate-tool-safety.sh +46 -0
  20. package/scripts/install/health-check.mjs +101 -1
  21. package/scripts/install/model-picker.mjs +118 -0
  22. package/scripts/install/opencode.mjs +155 -43
  23. package/scripts/install/plugin.mjs +4 -2
  24. package/scripts/install/venv.mjs +25 -2
  25. package/scripts/install-mcp.mjs +170 -37
  26. package/scripts/mcp_persistence_server.py +2 -1
  27. package/scripts/prune.mjs +359 -0
  28. package/scripts/secret-scan.mjs +88 -0
  29. package/scripts/uninstall.mjs +3 -3
  30. package/scripts/validate-routing.mjs +15 -0
  31. package/scripts/version-check.mjs +146 -0
  32. package/scripts/versioning.mjs +13 -14
  33. package/src/agents/hermes.md +2 -2
  34. package/src/agents/mnemosyne.md +1 -1
  35. package/src/agents/zeus.md +1 -1
  36. package/src/mcp/_pantheon_paths.py +0 -2
  37. package/src/mcp/install-mcp.mjs +6 -913
  38. package/src/mcp/mcp_persistence_server.py +2 -1
  39. package/src/mcp/pantheon_vision_server.py +495 -0
  40. package/src/mcp/requirements-mcp.txt +8 -5
  41. package/src/mcp/requirements-vision.txt +3 -0
  42. package/src/pantheon/auto-wake.ts +91 -0
  43. package/src/pantheon/background-job-board.ts +568 -0
  44. package/src/pantheon/file-persistence.ts +95 -0
  45. package/src/pantheon/presets.d.mts +108 -0
  46. package/src/pantheon/presets.mjs +554 -0
  47. package/src/pantheon/vision.ts +1437 -0
  48. package/src/plugin.ts +69 -25
  49. package/src/plugins/hook-runner.ts +186 -0
  50. package/src/plugins/pantheon-hooks.ts +153 -0
  51. package/src/plugins/tui/dist/tui.d.ts +1 -1
  52. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  53. package/src/plugins/tui/dist/tui.js +812 -223
  54. package/src/plugins/tui/dist/tui.js.map +1 -1
  55. package/src/plugins/tui/dist/tui.tsx +881 -121
  56. package/src/plugins/tui/package.json +3 -3
  57. package/src/plugins/tui/src/index.tsx +881 -121
  58. package/src/routing.yml +148 -0
  59. package/src/skills/context-compression/SKILL.md +1 -1
  60. package/src/skills/loop-engineering/SKILL.md +1 -1
  61. package/src/skills/orchestration-workflow/SKILL.md +11 -11
  62. package/scripts/scrub_secrets.py +0 -281
  63. package/src/mcp/requirements-mcp-core.txt +0 -5
@@ -1,921 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * install-mcp.mjs — Multi-platform MCP server installer for Pantheon
3
+ * Package entry point for the canonical MCP installer.
4
4
  *
5
- * Detects the user's platform (OpenCode, VS Code, Cursor, Claude, Windsurf),
6
- * installs MCP servers (Essential Tier 1, optional Domain Tier 2), writes
7
- * platform-specific config files, and validates each MCP responds.
8
- *
9
- * Usage:
10
- * node scripts/install-mcp.mjs
11
- * node scripts/install-mcp.mjs --platform opencode
12
- * node scripts/install-mcp.mjs --tier 2
13
- * node scripts/install-mcp.mjs --mcp context7
14
- * node scripts/install-mcp.mjs --list
15
- * node scripts/install-mcp.mjs --dry-run
16
- * node scripts/install-mcp.mjs --force
17
- * node scripts/install-mcp.mjs --help
5
+ * The implementation lives in scripts/install-mcp.mjs. Keeping this file as
6
+ * a launcher prevents the two historical catalog copies from diverging.
18
7
  */
19
8
 
20
- // ---------------------------------------------------------------------------
21
- // Imports
22
- // ---------------------------------------------------------------------------
23
- import { execSync } from 'node:child_process'
24
- import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
25
- import { homedir } from 'node:os'
26
- import { dirname, join } from 'node:path'
27
- import { createInterface } from 'node:readline'
28
9
  import { fileURLToPath } from 'node:url'
10
+ import { main } from '../../scripts/install-mcp.mjs'
29
11
 
30
- const __dirname = dirname(fileURLToPath(import.meta.url))
31
- const ROOT = join(__dirname, '..')
32
-
33
- // ---------------------------------------------------------------------------
34
- // MCP Definitions
35
- // ---------------------------------------------------------------------------
36
-
37
- /**
38
- * All supported MCP servers keyed by canonical name.
39
- *
40
- * Each MCP has:
41
- * tier - 1 (Essential), 2 (Domain), 3 (Project)
42
- * name - Human-readable label
43
- * description- Short description
44
- * platforms - Per-platform config; each entry has:
45
- * type - 'remote' | 'local' | 'stdio' | 'http'
46
- * url - (remote/http) endpoint URL
47
- * command - (local/stdio) executable
48
- * args - (local/stdio) command arguments
49
- * env - Required environment variable names
50
- * validate - Async function returning { ok, message }
51
- */
52
- const MCPS = {
53
- github: {
54
- tier: 1,
55
- name: 'GitHub MCP',
56
- description: 'Repository access, PR/issue management via GitHub API',
57
- platforms: {
58
- opencode: { type: 'remote', url: 'https://api.githubcopilot.com/mcp/' },
59
- vscode: { type: 'http', url: 'https://api.githubcopilot.com/mcp/' },
60
- cursor: { type: 'http', url: 'https://api.githubcopilot.com/mcp/' },
61
- claude: { type: 'remote', url: 'https://api.githubcopilot.com/mcp/' },
62
- windsurf: { type: 'http', url: 'https://api.githubcopilot.com/mcp/' },
63
- },
64
- env: [],
65
- validate: async () => ({ ok: true, message: 'remote (OAuth handled by platform)' }),
66
- },
67
- context7: {
68
- tier: 1,
69
- name: 'Context7',
70
- description: 'Up-to-date documentation for libraries and frameworks',
71
- platforms: {
72
- opencode: { type: 'local', command: 'npx', args: ['-y', '@upstash/context7-mcp'] },
73
- vscode: { type: 'stdio', command: 'npx', args: ['-y', '@upstash/context7-mcp'] },
74
- cursor: { type: 'local', command: 'npx', args: ['-y', '@upstash/context7-mcp'] },
75
- claude: { type: 'local', command: 'npx', args: ['-y', '@upstash/context7-mcp'] },
76
- windsurf: { type: 'local', command: 'npx', args: ['-y', '@upstash/context7-mcp'] },
77
- },
78
- env: [],
79
- validate: async () => {
80
- try {
81
- execSync('npx --yes @upstash/context7-mcp --help 2>&1', {
82
- stdio: 'pipe',
83
- timeout: 15000,
84
- })
85
- return { ok: true }
86
- } catch {
87
- return { ok: true, message: 'package resolved (npx)' }
88
- }
89
- },
90
- },
91
- playwright: {
92
- tier: 2,
93
- name: 'Playwright',
94
- description: 'Browser automation and E2E testing via Playwright',
95
- platforms: {
96
- opencode: { type: 'local', command: 'npx', args: ['-y', '@playwright/mcp@latest'] },
97
- vscode: { type: 'stdio', command: 'npx', args: ['-y', '@playwright/mcp@latest'] },
98
- cursor: { type: 'local', command: 'npx', args: ['-y', '@playwright/mcp@latest'] },
99
- claude: { type: 'local', command: 'npx', args: ['-y', '@playwright/mcp@latest'] },
100
- windsurf: { type: 'local', command: 'npx', args: ['-y', '@playwright/mcp@latest'] },
101
- },
102
- env: [],
103
- validate: async () => {
104
- try {
105
- execSync('npx --yes @playwright/mcp@latest --help 2>&1', {
106
- stdio: 'pipe',
107
- timeout: 15000,
108
- })
109
- return { ok: true }
110
- } catch {
111
- return { ok: true, message: 'package resolved (npx)' }
112
- }
113
- },
114
- },
115
- postgresql: {
116
- tier: 2,
117
- name: 'PostgreSQL',
118
- description: 'Database schema exploration and SQL query execution',
119
- platforms: {
120
- opencode: { type: 'local', command: 'npx', args: ['-y', '@anthropic/postgres-mcp'] },
121
- vscode: { type: 'stdio', command: 'npx', args: ['-y', '@anthropic/postgres-mcp'] },
122
- cursor: { type: 'local', command: 'npx', args: ['-y', '@anthropic/postgres-mcp'] },
123
- claude: { type: 'local', command: 'npx', args: ['-y', '@anthropic/postgres-mcp'] },
124
- windsurf: { type: 'local', command: 'npx', args: ['-y', '@anthropic/postgres-mcp'] },
125
- },
126
- env: ['DATABASE_URL'],
127
- validate: async () => {
128
- if (!process.env.DATABASE_URL) {
129
- return { ok: false, message: 'DATABASE_URL not set' }
130
- }
131
- try {
132
- execSync('npx --yes @anthropic/postgres-mcp --help 2>&1', {
133
- stdio: 'pipe',
134
- timeout: 15000,
135
- })
136
- return { ok: true, message: 'package resolved (requires DATABASE_URL at runtime)' }
137
- } catch {
138
- return { ok: true, message: 'package resolved (npx)' }
139
- }
140
- },
141
- },
142
- 'brave-search': {
143
- tier: 2,
144
- name: 'Brave Search',
145
- description: 'Real-time web search via Brave Search API',
146
- platforms: {
147
- opencode: { type: 'local', command: 'npx', args: ['-y', '@anthropic/brave-search-mcp'] },
148
- vscode: { type: 'stdio', command: 'npx', args: ['-y', '@anthropic/brave-search-mcp'] },
149
- cursor: { type: 'local', command: 'npx', args: ['-y', '@anthropic/brave-search-mcp'] },
150
- claude: { type: 'local', command: 'npx', args: ['-y', '@anthropic/brave-search-mcp'] },
151
- windsurf: { type: 'local', command: 'npx', args: ['-y', '@anthropic/brave-search-mcp'] },
152
- },
153
- env: ['BRAVE_API_KEY'],
154
- validate: async () => {
155
- if (!process.env.BRAVE_API_KEY) {
156
- return { ok: false, message: 'BRAVE_API_KEY not set' }
157
- }
158
- try {
159
- execSync('npx --yes @anthropic/brave-search-mcp --help 2>&1', {
160
- stdio: 'pipe',
161
- timeout: 15000,
162
- })
163
- return { ok: true, message: 'package resolved (requires BRAVE_API_KEY at runtime)' }
164
- } catch {
165
- return { ok: true, message: 'package resolved (npx)' }
166
- }
167
- },
168
- },
169
- 'pantheon-resources': {
170
- tier: 1,
171
- name: 'Pantheon Resources',
172
- description: 'Pantheon framework resources — agents, skills, routing, deepwork, memory bank',
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')] },
179
- },
180
- env: [],
181
- validate: async () => {
182
- const scriptPath = join(ROOT, 'scripts', 'mcp_resources_server.py')
183
- if (existsSync(scriptPath)) {
184
- return { ok: true, message: 'server script present' }
185
- }
186
- return { ok: false, message: 'scripts/mcp_resources_server.py not found' }
187
- },
188
- },
189
- 'pantheon-code-mode': {
190
- tier: 2,
191
- name: 'Pantheon Code Mode',
192
- description: 'Execute orchestration scripts from .pantheon/code-mode/ directory',
193
- 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')] },
199
- },
200
- env: [],
201
- validate: async () => {
202
- const scriptPath = join(ROOT, 'scripts', 'code_mode_server.py')
203
- if (existsSync(scriptPath)) {
204
- return { ok: true, message: 'server script present' }
205
- }
206
- return { ok: false, message: 'scripts/code_mode_server.py not found' }
207
- },
208
- },
209
- 'pantheon-memory': {
210
- tier: 1,
211
- name: 'Pantheon Memory',
212
- description:
213
- 'Pantheon Memory MCP Server — persistent memory with semantic search, recall, knowledge graph, compression, and verification',
214
- platforms: {
215
- opencode: {
216
- type: 'local',
217
- command: join(ROOT, '.venv/bin/python3'),
218
- args: [join(ROOT, 'scripts/memory_mcp_server.py')],
219
- },
220
- claude: {
221
- type: 'local',
222
- command: join(ROOT, '.venv/bin/python3'),
223
- args: [join(ROOT, 'scripts/memory_mcp_server.py')],
224
- },
225
- cursor: {
226
- type: 'local',
227
- command: join(ROOT, '.venv/bin/python3'),
228
- args: [join(ROOT, 'scripts/memory_mcp_server.py')],
229
- },
230
- windsurf: {
231
- type: 'local',
232
- command: join(ROOT, '.venv/bin/python3'),
233
- args: [join(ROOT, 'scripts/memory_mcp_server.py')],
234
- },
235
- },
236
- env: [],
237
- validate: async () => {
238
- const scriptPath = join(ROOT, 'scripts', 'memory_mcp_server.py')
239
- if (existsSync(scriptPath)) {
240
- return { ok: true, message: 'server script present' }
241
- }
242
- return { ok: false, message: 'scripts/memory_mcp_server.py not found' }
243
- },
244
- },
245
- 'pantheon-persistence': {
246
- tier: 1,
247
- name: 'Pantheon Persistence',
248
- description:
249
- 'Pantheon Persistence MCP Server — key-value store with FTS5 full-text search, TTL-based expiration, and namespace isolation',
250
- platforms: {
251
- opencode: {
252
- type: 'local',
253
- command: join(ROOT, '.venv/bin/python3'),
254
- args: [join(ROOT, 'scripts/mcp_persistence_server.py')],
255
- },
256
- claude: {
257
- type: 'local',
258
- command: join(ROOT, '.venv/bin/python3'),
259
- args: [join(ROOT, 'scripts/mcp_persistence_server.py')],
260
- },
261
- cursor: {
262
- type: 'local',
263
- command: join(ROOT, '.venv/bin/python3'),
264
- args: [join(ROOT, 'scripts/mcp_persistence_server.py')],
265
- },
266
- windsurf: {
267
- type: 'local',
268
- command: join(ROOT, '.venv/bin/python3'),
269
- args: [join(ROOT, 'scripts/mcp_persistence_server.py')],
270
- },
271
- },
272
- env: [],
273
- validate: async () => {
274
- const scriptPath = join(ROOT, 'scripts', 'mcp_persistence_server.py')
275
- if (existsSync(scriptPath)) {
276
- return { ok: true, message: 'server script present' }
277
- }
278
- return { ok: false, message: 'scripts/mcp_persistence_server.py not found' }
279
- },
280
- },
281
- docker: {
282
- tier: 2,
283
- name: 'Docker MCP',
284
- description: 'Container lifecycle management via Docker CLI',
285
- platforms: {
286
- opencode: { type: 'local', command: 'docker' },
287
- vscode: { type: 'stdio', command: 'docker' },
288
- cursor: { type: 'local', command: 'docker' },
289
- claude: { type: 'local', command: 'docker' },
290
- windsurf: { type: 'local', command: 'docker' },
291
- },
292
- env: [],
293
- validate: async () => {
294
- try {
295
- execSync('docker info --format "{{.ServerVersion}}" 2>&1', {
296
- stdio: 'pipe',
297
- timeout: 10000,
298
- })
299
- return { ok: true, message: 'Docker daemon reachable' }
300
- } catch {
301
- return { ok: false, message: 'Docker daemon not reachable or not installed' }
302
- }
303
- },
304
- },
305
- }
306
-
307
- // ---------------------------------------------------------------------------
308
- // Platform Metadata
309
- // ---------------------------------------------------------------------------
310
-
311
- const PLATFORMS = {
312
- opencode: {
313
- label: 'OpenCode',
314
- configFile: 'opencode.json',
315
- configPaths: [
316
- join(ROOT, 'opencode.json'),
317
- join(process.cwd(), 'opencode.json'),
318
- join(homedir(), '.config', 'opencode', 'opencode.json'),
319
- ],
320
- // OpenCode uses "mcp" key for servers (not "mcpServers")
321
- configKey: 'mcp',
322
- detect: () =>
323
- existsSync(join(ROOT, 'opencode.json')) ||
324
- existsSync(join(process.cwd(), 'opencode.json')) ||
325
- existsSync(join(homedir(), '.config', 'opencode', 'opencode.json')),
326
- },
327
- vscode: {
328
- label: 'VS Code',
329
- configFile: '.vscode/mcp.json',
330
- configPaths: [join(ROOT, '.vscode', 'mcp.json'), join(process.cwd(), '.vscode', 'mcp.json')],
331
- // VS Code uses "servers" key
332
- configKey: 'servers',
333
- detect: () =>
334
- existsSync(join(ROOT, '.vscode', 'mcp.json')) ||
335
- existsSync(join(process.cwd(), '.vscode', 'mcp.json')),
336
- },
337
- cursor: {
338
- label: 'Cursor',
339
- configFile: '.cursor/mcp.json',
340
- configPaths: [join(ROOT, '.cursor', 'mcp.json'), join(process.cwd(), '.cursor', 'mcp.json')],
341
- configKey: 'mcpServers',
342
- detect: () =>
343
- existsSync(join(ROOT, '.cursor', 'mcp.json')) ||
344
- existsSync(join(process.cwd(), '.cursor', 'mcp.json')),
345
- },
346
- claude: {
347
- label: 'Claude',
348
- configFile: '.mcp.json',
349
- configPaths: [
350
- join(ROOT, '.mcp.json'),
351
- join(process.cwd(), '.mcp.json'),
352
- join(homedir(), '.claude', 'settings.json'),
353
- ],
354
- configKey: 'mcpServers',
355
- detect: () =>
356
- existsSync(join(ROOT, '.mcp.json')) ||
357
- existsSync(join(process.cwd(), '.mcp.json')) ||
358
- existsSync(join(homedir(), '.claude', 'settings.json')),
359
- },
360
- windsurf: {
361
- label: 'Windsurf',
362
- configFile: '~/.codeium/windsurf/mcp_config.json',
363
- configPaths: [join(homedir(), '.codeium', 'windsurf', 'mcp_config.json')],
364
- configKey: 'mcpServers',
365
- detect: () => existsSync(join(homedir(), '.codeium', 'windsurf', 'mcp_config.json')),
366
- },
367
- }
368
-
369
- // ---------------------------------------------------------------------------
370
- // Platform Config Writers
371
- // ---------------------------------------------------------------------------
372
-
373
- /**
374
- * Build the MCP server entry for a given platform.
375
- * Each platform has a slightly different JSON shape.
376
- *
377
- * @param {string} mcpKey - Canonical MCP key (e.g. 'github', 'context7')
378
- * @param {string} platformName - Platform key (e.g. 'opencode', 'vscode')
379
- * @returns {object|null} - The config entry, or null if unsupported
380
- */
381
- function buildMcpEntry(mcpKey, platformName) {
382
- const mcp = MCPS[mcpKey]
383
- if (!mcp) return null
384
- const platform = mcp.platforms[platformName]
385
- if (!platform) return null
386
-
387
- // Collect env vars that are set for inclusion in config
388
- const env = {}
389
- for (const key of mcp.env) {
390
- const val = process.env[key]
391
- if (val) {
392
- env[key] = `\${${key}}`
393
- }
394
- }
395
-
396
- switch (platform.type) {
397
- case 'remote':
398
- case 'http': {
399
- // OpenCode uses "mcp" key with { type, url, enabled }
400
- if (platformName === 'opencode') {
401
- return {
402
- type: 'remote',
403
- url: platform.url,
404
- enabled: true,
405
- }
406
- }
407
- // VS Code http format: { type: "http", url }
408
- if (platformName === 'vscode') {
409
- return { type: 'http', url: platform.url }
410
- }
411
- // Cursor/Claude/Windsurf http format
412
- return { type: 'http', url: platform.url }
413
- }
414
-
415
- case 'local': {
416
- // OpenCode uses "mcp" key with { type: "local", command: [...], enabled }
417
- if (platformName === 'opencode') {
418
- const entry = {
419
- type: 'local',
420
- command: [platform.command, ...(platform.args || [])],
421
- enabled: true,
422
- }
423
- if (Object.keys(env).length > 0) {
424
- entry.environment = env
425
- }
426
- return entry
427
- }
428
- // Cursor/Claude/Windsurf format: { command, args?, env? }
429
- const entry = {
430
- command: platform.command,
431
- ...(platform.args ? { args: platform.args } : {}),
432
- }
433
- if (Object.keys(env).length > 0) {
434
- entry.env = env
435
- }
436
- return entry
437
- }
438
-
439
- case 'stdio': {
440
- // VS Code stdio format: { type: "stdio", command, args? }
441
- return {
442
- type: 'stdio',
443
- command: platform.command,
444
- ...(platform.args ? { args: platform.args } : {}),
445
- }
446
- }
447
-
448
- default:
449
- return null
450
- }
451
- }
452
-
453
- /**
454
- * Read a JSON config file, returning the parsed object or {}.
455
- */
456
- function readConfig(filePath) {
457
- try {
458
- if (existsSync(filePath)) {
459
- const raw = readFileSync(filePath, 'utf8')
460
- return JSON.parse(raw)
461
- }
462
- } catch (err) {
463
- console.warn(` ⚠️ Could not parse ${filePath}: ${err.message}`)
464
- }
465
- return {}
466
- }
467
-
468
- /**
469
- * Write the MCP server entry to the platform config file.
470
- *
471
- * @param {string} platformName - Platform key
472
- * @param {string} mcpKey - MCP canonical key
473
- * @param {boolean} dryRun - If true, don't write anything
474
- * @param {boolean} force - Overwrite existing entry
475
- * @returns {{ status: string, path?: string, reason?: string, entry?: object }}
476
- */
477
- function writeMcpConfig(platformName, mcpKey, dryRun, force) {
478
- const platform = PLATFORMS[platformName]
479
- const mcp = MCPS[mcpKey]
480
- if (!platform || !mcp) {
481
- return { status: 'error', reason: 'unknown platform or mcp' }
482
- }
483
-
484
- // Find the best config path (first existing, or first fallback)
485
- let configPath = null
486
- for (const p of platform.configPaths) {
487
- if (existsSync(p)) {
488
- configPath = p
489
- break
490
- }
491
- }
492
- if (!configPath) {
493
- configPath = platform.configPaths[0]
494
- }
495
-
496
- // Read existing config
497
- const config = readConfig(configPath)
498
- const configExisted = Object.keys(config).length > 0
499
-
500
- // The key under which MCP servers are stored (e.g. 'mcp', 'servers', 'mcpServers')
501
- const key = platform.configKey
502
-
503
- // Ensure the section exists
504
- if (!config[key]) {
505
- config[key] = {}
506
- }
507
-
508
- // Check if the MCP already has an entry
509
- if (config[key][mcpKey] && !force) {
510
- return {
511
- status: 'skipped',
512
- reason: `Already exists in ${platform.configFile} (use --force to overwrite)`,
513
- }
514
- }
515
-
516
- // Build the platform-specific entry
517
- const entry = buildMcpEntry(mcpKey, platformName)
518
- if (!entry) {
519
- return {
520
- status: 'error',
521
- reason: `${mcp.name} does not support ${platform.label}`,
522
- }
523
- }
524
-
525
- if (dryRun) {
526
- return { status: 'dry-run', entry }
527
- }
528
-
529
- // Backup existing config before modifying
530
- if (configExisted) {
531
- const backupPath = `${configPath}.backup`
532
- if (!existsSync(backupPath)) {
533
- try {
534
- copyFileSync(configPath, backupPath)
535
- } catch {
536
- // Non-fatal: continue even if backup fails
537
- }
538
- }
539
- }
540
-
541
- // Write the MCP entry
542
- config[key][mcpKey] = entry
543
-
544
- // Ensure directory exists
545
- const dir = dirname(configPath)
546
- if (!existsSync(dir)) {
547
- mkdirSync(dir, { recursive: true })
548
- }
549
-
550
- writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8')
551
-
552
- return { status: 'installed', path: configPath }
553
- }
554
-
555
- // ---------------------------------------------------------------------------
556
- // CLI Argument Parsing
557
- // ---------------------------------------------------------------------------
558
-
559
- function parseArgs(argv) {
560
- const args = {
561
- platforms: [],
562
- tier: 1,
563
- mcp: null,
564
- list: false,
565
- dryRun: false,
566
- force: false,
567
- help: false,
568
- }
569
-
570
- for (let i = 2; i < argv.length; i++) {
571
- switch (argv[i]) {
572
- case '--platform':
573
- args.platforms = argv[++i].split(',').map((s) => s.trim().toLowerCase())
574
- break
575
- case '--tier':
576
- args.tier = parseInt(argv[++i], 10)
577
- break
578
- case '--mcp':
579
- args.mcp = argv[++i].toLowerCase()
580
- break
581
- case '--list':
582
- args.list = true
583
- break
584
- case '--dry-run':
585
- args.dryRun = true
586
- break
587
- case '--force':
588
- args.force = true
589
- break
590
- case '--help':
591
- case '-h':
592
- args.help = true
593
- break
594
- default:
595
- console.warn(`⚠️ Unknown option: ${argv[i]}`)
596
- break
597
- }
598
- }
599
-
600
- // If no platforms specified and not --list, auto-detect
601
- if (args.platforms.length === 0 && !args.list && !args.help) {
602
- const detected = detectPlatforms()
603
- if (detected.length > 0) {
604
- args.platforms = detected
605
- } else {
606
- // Default to all platforms if none detected
607
- args.platforms = Object.keys(PLATFORMS)
608
- }
609
- }
610
-
611
- return args
612
- }
613
-
614
- function detectPlatforms() {
615
- const detected = []
616
- for (const [name, platform] of Object.entries(PLATFORMS)) {
617
- if (platform.detect()) {
618
- detected.push(name)
619
- }
620
- }
621
- return detected
622
- }
623
-
624
- // ---------------------------------------------------------------------------
625
- // Interactive Prompt
626
- // ---------------------------------------------------------------------------
627
-
628
- function askQuestion(query) {
629
- return new Promise((resolve) => {
630
- const rl = createInterface({ input: process.stdin, output: process.stdout })
631
- rl.question(query, (answer) => {
632
- rl.close()
633
- resolve(answer.trim().toLowerCase())
634
- })
635
- })
636
- }
637
-
638
- async function promptForTier2Mcps() {
639
- const selected = []
640
- const tier2Mcps = Object.entries(MCPS).filter(([, mcp]) => mcp.tier === 2)
641
-
642
- console.log('\nDomain (Tier 2) — ? [y/N]:')
643
-
644
- for (const [key, mcp] of tier2Mcps) {
645
- const envHints = mcp.env.length > 0 ? ` (needs: ${mcp.env.join(', ')})` : ''
646
- const answer = await askQuestion(` ${mcp.name}${envHints}? [y/N] `)
647
- if (answer === 'y' || answer === 'yes') {
648
- selected.push(key)
649
- }
650
- }
651
-
652
- return selected
653
- }
654
-
655
- // ---------------------------------------------------------------------------
656
- // Validation
657
- // ---------------------------------------------------------------------------
658
-
659
- async function validateMcp(mcpKey) {
660
- const mcp = MCPS[mcpKey]
661
- if (!mcp) return { ok: false, message: 'unknown MCP' }
662
- try {
663
- return await mcp.validate()
664
- } catch (err) {
665
- return { ok: false, message: err.message }
666
- }
667
- }
668
-
669
- // ---------------------------------------------------------------------------
670
- // List Available MCPs
671
- // ---------------------------------------------------------------------------
672
-
673
- function listMcps() {
674
- console.log('\nAvailable MCP Servers:\n')
675
- console.log('Essential (Tier 1):')
676
- for (const [key, mcp] of Object.entries(MCPS)) {
677
- if (mcp.tier === 1) {
678
- console.log(` ${key.padEnd(16)} ${mcp.name.padEnd(20)} ${mcp.description}`)
679
- }
680
- }
681
- console.log('\nDomain (Tier 2):')
682
- for (const [key, mcp] of Object.entries(MCPS)) {
683
- if (mcp.tier === 2) {
684
- const envInfo = mcp.env.length > 0 ? ` [env: ${mcp.env.join(', ')}]` : ''
685
- console.log(` ${key.padEnd(16)} ${mcp.name.padEnd(20)} ${mcp.description}${envInfo}`)
686
- }
687
- }
688
- console.log(`\nPlatforms supported: ${Object.keys(PLATFORMS).join(', ')}`)
689
- console.log('')
690
- }
691
-
692
- // ---------------------------------------------------------------------------
693
- // Help
694
- // ---------------------------------------------------------------------------
695
-
696
- function showHelp() {
697
- console.log(`
698
- install-mcp.mjs — Multi-platform MCP server installer for Pantheon
699
-
700
- Usage:
701
- node scripts/install-mcp.mjs Auto-detect platform, install Tier 1
702
- node scripts/install-mcp.mjs --platform opencode Install for specific platform
703
- node scripts/install-mcp.mjs --tier 2 Install Tier 2 (interactive prompt)
704
- node scripts/install-mcp.mjs --mcp context7 Install a specific MCP only
705
- node scripts/install-mcp.mjs --list List available MCPs
706
- node scripts/install-mcp.mjs --dry-run Preview changes without writing
707
- node scripts/install-mcp.mjs --force Overwrite existing MCP entries
708
- node scripts/install-mcp.mjs --platform opencode --mcp github,context7
709
-
710
- Options:
711
- --platform <name> Target platform (opencode|vscode|cursor|claude|windsurf).
712
- Comma-separated for multiple. Auto-detects if omitted.
713
- --tier <n> 1=Essential (default), 2=Domain, 3=Project
714
- --mcp <name> Install specific MCP(s) only. Comma-separated.
715
- --list List available MCPs and exit
716
- --dry-run Show what would be installed without writing
717
- --force Overwrite existing MCP entries
718
- --help, -h Show this help
719
-
720
- Platforms:
721
- opencode → opencode.json (mcp key)
722
- vscode → .vscode/mcp.json (servers key)
723
- cursor → .cursor/mcp.json (mcpServers key)
724
- claude → .mcp.json or ~/.claude/settings.json (mcpServers key)
725
- windsurf → ~/.codeium/windsurf/mcp_config.json (mcpServers key)
726
-
727
- Environment Variables:
728
- DATABASE_URL Required for PostgreSQL MCP (postgresql+ssl://...)
729
- BRAVE_API_KEY Required for Brave Search MCP
730
-
731
- Examples:
732
- node scripts/install-mcp.mjs Auto-detect + install essentials
733
- node scripts/install-mcp.mjs --platform opencode --tier 2 OpenCode + interactive Tier 2
734
- node scripts/install-mcp.mjs --mcp docker Install only Docker MCP
735
- node scripts/install-mcp.mjs --platform vscode --dry-run Preview VS Code changes
736
- `)
12
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
13
+ main()
737
14
  }
738
-
739
- // ---------------------------------------------------------------------------
740
- // Progress / Logging Helpers
741
- // ---------------------------------------------------------------------------
742
-
743
- const progress = {
744
- installed: [],
745
- skipped: [],
746
- errors: [],
747
- validations: {},
748
- }
749
-
750
- function logResult(platformLabel, mcpKey, result) {
751
- const mcp = MCPS[mcpKey]
752
- const label = mcp ? mcp.name : mcpKey
753
- switch (result.status) {
754
- case 'installed':
755
- console.log(` ✅ ${label} — config written to ${result.path}`)
756
- progress.installed.push({ platform: platformLabel, mcp: mcpKey })
757
- break
758
- case 'skipped':
759
- console.log(` ⏭️ ${label} — ${result.reason}`)
760
- progress.skipped.push({ platform: platformLabel, mcp: mcpKey, reason: result.reason })
761
- break
762
- case 'dry-run': {
763
- const desc =
764
- result.entry?.type === 'remote' || result.entry?.type === 'http'
765
- ? `remote → ${result.entry.url}`
766
- : `local → ${result.entry.command} ${(result.entry.args || []).join(' ')}`
767
- console.log(` 📋 ${label} — would install (${desc})`)
768
- break
769
- }
770
- default:
771
- console.log(` ❌ ${label} — ${result.reason}`)
772
- progress.errors.push({ platform: platformLabel, mcp: mcpKey, reason: result.reason })
773
- }
774
- }
775
-
776
- function logValidation(mcpKey, result) {
777
- const mcp = MCPS[mcpKey]
778
- const label = mcp ? mcp.name : mcpKey
779
- if (result.ok) {
780
- const extra = result.message ? ` — ${result.message}` : ''
781
- console.log(` ✅ ${label}${extra}`)
782
- } else {
783
- const extra = result.message ? ` — ${result.message}` : ''
784
- console.log(` ❌ ${label}${extra}`)
785
- }
786
- progress.validations[mcpKey] = result
787
- }
788
-
789
- // ---------------------------------------------------------------------------
790
- // Main
791
- // ---------------------------------------------------------------------------
792
-
793
- async function main() {
794
- const args = parseArgs(process.argv)
795
-
796
- if (args.help) {
797
- showHelp()
798
- process.exit(0)
799
- }
800
-
801
- if (args.list) {
802
- listMcps()
803
- process.exit(0)
804
- }
805
-
806
- // Determine which MCPs to install
807
- let mcpKeys = []
808
-
809
- if (args.mcp) {
810
- // Specific MCP(s) requested
811
- mcpKeys = args.mcp.split(',').map((s) => s.trim().toLowerCase())
812
- for (const key of mcpKeys) {
813
- if (!MCPS[key]) {
814
- console.error(`❌ Unknown MCP: "${key}". Use --list to see available MCPs.`)
815
- process.exit(1)
816
- }
817
- }
818
- } else {
819
- // Tier 1 — always included
820
- const tier1Mcps = Object.entries(MCPS)
821
- .filter(([, mcp]) => mcp.tier === 1)
822
- .map(([key]) => key)
823
- mcpKeys.push(...tier1Mcps)
824
-
825
- // Tier 2 — interactive prompt
826
- if (args.tier >= 2) {
827
- const tier2Selected = await promptForTier2Mcps()
828
- mcpKeys.push(...tier2Selected)
829
- }
830
- }
831
-
832
- if (mcpKeys.length === 0) {
833
- console.log('No MCPs selected. Nothing to install.\n')
834
- process.exit(0)
835
- }
836
-
837
- // Determine target platforms
838
- const platforms = args.platforms
839
- const platformLabels = platforms.map((p) => PLATFORMS[p]?.label || p)
840
-
841
- if (args.dryRun) {
842
- console.log('\n🔍 Dry-run mode — no files will be written\n')
843
- }
844
-
845
- console.log(
846
- `\n🔍 ${platforms.length === 1 ? 'Platform' : 'Platforms'}: ${platformLabels.join(', ')}`,
847
- )
848
- console.log(
849
- `📦 Installing ${mcpKeys.length} MCP(s): ${mcpKeys.map((k) => MCPS[k]?.name || k).join(', ')}\n`,
850
- )
851
-
852
- // Install per platform
853
- for (const platformName of platforms) {
854
- const platform = PLATFORMS[platformName]
855
- if (!platform) {
856
- console.warn(` ⚠️ Unknown platform: ${platformName} — skipping`)
857
- continue
858
- }
859
-
860
- console.log(`🔧 ${platform.label} (${platform.configFile}):`)
861
-
862
- for (const mcpKey of mcpKeys) {
863
- const result = writeMcpConfig(platformName, mcpKey, args.dryRun, args.force)
864
- logResult(platform.label, mcpKey, result)
865
- }
866
-
867
- console.log('')
868
- }
869
-
870
- // Validation
871
- if (!args.dryRun) {
872
- console.log('🔍 Validating MCPs...\n')
873
- for (const key of mcpKeys) {
874
- const result = await validateMcp(key)
875
- logValidation(key, result)
876
- }
877
- console.log('')
878
- } else {
879
- console.log('🔍 Validation skipped in dry-run mode\n')
880
- }
881
-
882
- // Summary
883
- console.log('='.repeat(60))
884
- console.log('📋 Installation Summary')
885
- console.log('='.repeat(60))
886
- console.log(` Platforms: ${platformLabels.join(', ')}`)
887
- console.log(` MCPs: ${mcpKeys.length} requested`)
888
- console.log(` Installed: ${progress.installed.length}`)
889
- console.log(` Skipped: ${progress.skipped.length}`)
890
- console.log(` Errors: ${progress.errors.length}`)
891
-
892
- if (progress.installed.length > 0) {
893
- console.log('\n ✅ Installed:')
894
- for (const item of progress.installed) {
895
- console.log(` - ${item.mcp} → ${item.platform}`)
896
- }
897
- }
898
-
899
- if (progress.skipped.length > 0 && !args.dryRun) {
900
- console.log('\n ⏭️ Skipped:')
901
- for (const item of progress.skipped) {
902
- console.log(` - ${item.mcp} (${item.platform}): ${item.reason}`)
903
- }
904
- }
905
-
906
- if (progress.errors.length > 0) {
907
- console.log('\n ❌ Errors:')
908
- for (const item of progress.errors) {
909
- console.log(` - ${item.mcp} (${item.platform}): ${item.reason}`)
910
- }
911
- }
912
-
913
- console.log('\n⚠️ Reminder: Restart your editor/CLI tool for MCP changes to take effect.')
914
- console.log(' For VS Code: Cmd+Shift+P → "Developer: Reload Window"')
915
- console.log(' For OpenCode: Just restart the session')
916
- console.log('')
917
-
918
- process.exit(progress.errors.length > 0 ? 1 : 0)
919
- }
920
-
921
- main()