pantheon-opencode 1.0.0

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 (128) hide show
  1. package/AGENTS.md +37 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1013 -0
  4. package/bin/pantheon-init.mjs +183 -0
  5. package/commands/pantheon-audit.md +25 -0
  6. package/commands/pantheon-bg.md +10 -0
  7. package/commands/pantheon-consolidate.md +11 -0
  8. package/commands/pantheon-deepwork.md +128 -0
  9. package/commands/pantheon-doc.md +10 -0
  10. package/commands/pantheon-focus.md +9 -0
  11. package/commands/pantheon-forget.md +58 -0
  12. package/commands/pantheon-hash.md +11 -0
  13. package/commands/pantheon-optimize.md +79 -0
  14. package/commands/pantheon-remember.md +44 -0
  15. package/commands/pantheon-search.md +48 -0
  16. package/commands/pantheon-status.md +71 -0
  17. package/commands/pantheon-todo.md +11 -0
  18. package/commands/pantheon.md +49 -0
  19. package/docs/AGENT-MCP.md +194 -0
  20. package/docs/ARCHITECTURE.md +384 -0
  21. package/docs/BRANCH-PROTECTION.md +142 -0
  22. package/docs/INDEX.md +81 -0
  23. package/docs/INSTALLATION.md +217 -0
  24. package/docs/MCP.md +238 -0
  25. package/docs/MEMORY.md +471 -0
  26. package/docs/MIGRATION-MEMORY-BANK.md +139 -0
  27. package/docs/PLATFORMS.md +5 -0
  28. package/docs/QUICKSTART.md +49 -0
  29. package/docs/README.md +18 -0
  30. package/docs/RELEASING.md +256 -0
  31. package/docs/SETUP.md +5 -0
  32. package/docs/UPGRADING.md +41 -0
  33. package/docs/mcp-recommendations.md +439 -0
  34. package/docs/mcp-tools.md +156 -0
  35. package/docs/mcp-user-guide.md +204 -0
  36. package/docs/persistence-mcp.md +111 -0
  37. package/package.json +72 -0
  38. package/pantheon.schema.json +158 -0
  39. package/scripts/__init__.py +0 -0
  40. package/scripts/_pantheon_paths.py +68 -0
  41. package/scripts/check-version-consistency.sh +23 -0
  42. package/scripts/code_mode_server.py +202 -0
  43. package/scripts/doctor.mjs +763 -0
  44. package/scripts/generate-prompts.sh +222 -0
  45. package/scripts/generate-routing-docs.mjs +104 -0
  46. package/scripts/hash_verify.py +192 -0
  47. package/scripts/init-pantheon-mcp.sh +118 -0
  48. package/scripts/install/agents-md.mjs +214 -0
  49. package/scripts/install/health-check.mjs +196 -0
  50. package/scripts/install/migrate.mjs +209 -0
  51. package/scripts/install/opencode.mjs +645 -0
  52. package/scripts/install/shared.mjs +655 -0
  53. package/scripts/install/venv.mjs +116 -0
  54. package/scripts/install-mcp.mjs +885 -0
  55. package/scripts/install.mjs +26 -0
  56. package/scripts/manifest.mjs +622 -0
  57. package/scripts/mcp_persistence_server.py +459 -0
  58. package/scripts/mcp_resources_server.py +206 -0
  59. package/scripts/memory_cache.py +78 -0
  60. package/scripts/memory_mcp_server.py +605 -0
  61. package/scripts/paths.py +64 -0
  62. package/scripts/prune_context.py +72 -0
  63. package/scripts/release-bundle.mjs +109 -0
  64. package/scripts/scrub-secrets.py +282 -0
  65. package/scripts/scrub_secrets.py +281 -0
  66. package/scripts/test-context-compression.sh +166 -0
  67. package/scripts/themis_heuristic_scan.py +287 -0
  68. package/scripts/todo_enforcer.py +242 -0
  69. package/scripts/uninstall.mjs +1057 -0
  70. package/scripts/validate-routing.mjs +160 -0
  71. package/scripts/validate_agent_frontmatter.py +226 -0
  72. package/scripts/versioning.mjs +254 -0
  73. package/skills-lock.json +16 -0
  74. package/src/agents/aphrodite.md +162 -0
  75. package/src/agents/apollo.md +109 -0
  76. package/src/agents/athena.md +226 -0
  77. package/src/agents/demeter.md +146 -0
  78. package/src/agents/gaia.md +82 -0
  79. package/src/agents/hephaestus.md +105 -0
  80. package/src/agents/hermes.md +302 -0
  81. package/src/agents/iris.md +99 -0
  82. package/src/agents/mnemosyne.md +226 -0
  83. package/src/agents/nyx.md +87 -0
  84. package/src/agents/prometheus.md +199 -0
  85. package/src/agents/talos.md +93 -0
  86. package/src/agents/themis.md +187 -0
  87. package/src/agents/zeus.md +209 -0
  88. package/src/instructions/agent-return-format.instructions.md +26 -0
  89. package/src/instructions/backend-standards.instructions.md +45 -0
  90. package/src/instructions/documentation-standards.instructions.md +53 -0
  91. package/src/instructions/frontend-standards.instructions.md +46 -0
  92. package/src/instructions/memory-protocol.instructions.md +67 -0
  93. package/src/instructions/yagni.instructions.md +21 -0
  94. package/src/instructions/zeus-anti-stall.instructions.md +72 -0
  95. package/src/instructions/zeus-communication-rules.instructions.md +15 -0
  96. package/src/instructions/zeus-council-synthesis.instructions.md +105 -0
  97. package/src/instructions/zeus-timeout-retry.instructions.md +127 -0
  98. package/src/mcp/_pantheon_paths.py +67 -0
  99. package/src/mcp/code_mode_server.py +202 -0
  100. package/src/mcp/init-pantheon-mcp.sh +118 -0
  101. package/src/mcp/install-mcp.mjs +885 -0
  102. package/src/mcp/mcp_persistence_server.py +458 -0
  103. package/src/mcp/mcp_resources_server.py +205 -0
  104. package/src/mcp/memory_mcp_server.py +604 -0
  105. package/src/mcp/requirements-mcp-core.txt +5 -0
  106. package/src/mcp/requirements-mcp.txt +5 -0
  107. package/src/plugin.ts +19 -0
  108. package/src/plugins/tui/dist/tui.js +143 -0
  109. package/src/plugins/tui/dist/tui.tsx +144 -0
  110. package/src/plugins/tui/package.json +32 -0
  111. package/src/plugins/tui/src/index.tsx +144 -0
  112. package/src/plugins/tui/tsdown.config.ts +22 -0
  113. package/src/routing.yml +499 -0
  114. package/src/skills/README.md +230 -0
  115. package/src/skills/agent-coordination/SKILL.md +95 -0
  116. package/src/skills/artifact-management/SKILL.md +118 -0
  117. package/src/skills/auto-continue/SKILL.md +280 -0
  118. package/src/skills/code-review-checklist/SKILL.md +139 -0
  119. package/src/skills/context-compression/SKILL.md +861 -0
  120. package/src/skills/git-workflow-and-versioning/SKILL.md +32 -0
  121. package/src/skills/incremental-implementation/SKILL.md +27 -0
  122. package/src/skills/memory-bank/SKILL.md +165 -0
  123. package/src/skills/orchestration-workflow/SKILL.md +311 -0
  124. package/src/skills/security-hardening/SKILL.md +36 -0
  125. package/src/skills/session-goal/SKILL.md +138 -0
  126. package/src/skills/spec-driven-development/SKILL.md +23 -0
  127. package/src/skills/tdd-with-agents/SKILL.md +170 -0
  128. package/src/skills/visual-review-pipeline/SKILL.md +200 -0
@@ -0,0 +1,885 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * install-mcp.mjs — Multi-platform MCP server installer for Pantheon
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
18
+ */
19
+
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
+ import { fileURLToPath } from 'node:url'
29
+
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: 'python', args: ['scripts/mcp_resources_server.py'] },
175
+ vscode: { type: 'stdio', command: 'python', args: ['scripts/mcp_resources_server.py'] },
176
+ cursor: { type: 'local', command: 'python', args: ['scripts/mcp_resources_server.py'] },
177
+ claude: { type: 'local', command: 'python', args: ['scripts/mcp_resources_server.py'] },
178
+ windsurf: { type: 'local', command: 'python', args: ['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: 'python', args: ['scripts/code_mode_server.py'] },
195
+ vscode: { type: 'stdio', command: 'python', args: ['scripts/code_mode_server.py'] },
196
+ cursor: { type: 'local', command: 'python', args: ['scripts/code_mode_server.py'] },
197
+ claude: { type: 'local', command: 'python', args: ['scripts/code_mode_server.py'] },
198
+ windsurf: { type: 'local', command: 'python', args: ['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: '.venv/bin/python3',
218
+ args: ['scripts/memory_mcp_server.py'],
219
+ },
220
+ claude: {
221
+ type: 'local',
222
+ command: '.venv/bin/python3',
223
+ args: ['scripts/memory_mcp_server.py'],
224
+ },
225
+ cursor: {
226
+ type: 'local',
227
+ command: '.venv/bin/python3',
228
+ args: ['scripts/memory_mcp_server.py'],
229
+ },
230
+ windsurf: {
231
+ type: 'local',
232
+ command: '.venv/bin/python3',
233
+ args: ['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
+ docker: {
246
+ tier: 2,
247
+ name: 'Docker MCP',
248
+ description: 'Container lifecycle management via Docker CLI',
249
+ platforms: {
250
+ opencode: { type: 'local', command: 'docker' },
251
+ vscode: { type: 'stdio', command: 'docker' },
252
+ cursor: { type: 'local', command: 'docker' },
253
+ claude: { type: 'local', command: 'docker' },
254
+ windsurf: { type: 'local', command: 'docker' },
255
+ },
256
+ env: [],
257
+ validate: async () => {
258
+ try {
259
+ execSync('docker info --format "{{.ServerVersion}}" 2>&1', {
260
+ stdio: 'pipe',
261
+ timeout: 10000,
262
+ })
263
+ return { ok: true, message: 'Docker daemon reachable' }
264
+ } catch {
265
+ return { ok: false, message: 'Docker daemon not reachable or not installed' }
266
+ }
267
+ },
268
+ },
269
+ }
270
+
271
+ // ---------------------------------------------------------------------------
272
+ // Platform Metadata
273
+ // ---------------------------------------------------------------------------
274
+
275
+ const PLATFORMS = {
276
+ opencode: {
277
+ label: 'OpenCode',
278
+ configFile: 'opencode.json',
279
+ configPaths: [
280
+ join(ROOT, 'opencode.json'),
281
+ join(process.cwd(), 'opencode.json'),
282
+ join(homedir(), '.config', 'opencode', 'opencode.json'),
283
+ ],
284
+ // OpenCode uses "mcp" key for servers (not "mcpServers")
285
+ configKey: 'mcp',
286
+ detect: () =>
287
+ existsSync(join(ROOT, 'opencode.json')) ||
288
+ existsSync(join(process.cwd(), 'opencode.json')) ||
289
+ existsSync(join(homedir(), '.config', 'opencode', 'opencode.json')),
290
+ },
291
+ vscode: {
292
+ label: 'VS Code',
293
+ configFile: '.vscode/mcp.json',
294
+ configPaths: [join(ROOT, '.vscode', 'mcp.json'), join(process.cwd(), '.vscode', 'mcp.json')],
295
+ // VS Code uses "servers" key
296
+ configKey: 'servers',
297
+ detect: () =>
298
+ existsSync(join(ROOT, '.vscode', 'mcp.json')) ||
299
+ existsSync(join(process.cwd(), '.vscode', 'mcp.json')),
300
+ },
301
+ cursor: {
302
+ label: 'Cursor',
303
+ configFile: '.cursor/mcp.json',
304
+ configPaths: [join(ROOT, '.cursor', 'mcp.json'), join(process.cwd(), '.cursor', 'mcp.json')],
305
+ configKey: 'mcpServers',
306
+ detect: () =>
307
+ existsSync(join(ROOT, '.cursor', 'mcp.json')) ||
308
+ existsSync(join(process.cwd(), '.cursor', 'mcp.json')),
309
+ },
310
+ claude: {
311
+ label: 'Claude',
312
+ configFile: '.mcp.json',
313
+ configPaths: [
314
+ join(ROOT, '.mcp.json'),
315
+ join(process.cwd(), '.mcp.json'),
316
+ join(homedir(), '.claude', 'settings.json'),
317
+ ],
318
+ configKey: 'mcpServers',
319
+ detect: () =>
320
+ existsSync(join(ROOT, '.mcp.json')) ||
321
+ existsSync(join(process.cwd(), '.mcp.json')) ||
322
+ existsSync(join(homedir(), '.claude', 'settings.json')),
323
+ },
324
+ windsurf: {
325
+ label: 'Windsurf',
326
+ configFile: '~/.codeium/windsurf/mcp_config.json',
327
+ configPaths: [join(homedir(), '.codeium', 'windsurf', 'mcp_config.json')],
328
+ configKey: 'mcpServers',
329
+ detect: () => existsSync(join(homedir(), '.codeium', 'windsurf', 'mcp_config.json')),
330
+ },
331
+ }
332
+
333
+ // ---------------------------------------------------------------------------
334
+ // Platform Config Writers
335
+ // ---------------------------------------------------------------------------
336
+
337
+ /**
338
+ * Build the MCP server entry for a given platform.
339
+ * Each platform has a slightly different JSON shape.
340
+ *
341
+ * @param {string} mcpKey - Canonical MCP key (e.g. 'github', 'context7')
342
+ * @param {string} platformName - Platform key (e.g. 'opencode', 'vscode')
343
+ * @returns {object|null} - The config entry, or null if unsupported
344
+ */
345
+ function buildMcpEntry(mcpKey, platformName) {
346
+ const mcp = MCPS[mcpKey]
347
+ if (!mcp) return null
348
+ const platform = mcp.platforms[platformName]
349
+ if (!platform) return null
350
+
351
+ // Collect env vars that are set for inclusion in config
352
+ const env = {}
353
+ for (const key of mcp.env) {
354
+ const val = process.env[key]
355
+ if (val) {
356
+ env[key] = `\${${key}}`
357
+ }
358
+ }
359
+
360
+ switch (platform.type) {
361
+ case 'remote':
362
+ case 'http': {
363
+ // OpenCode uses "mcp" key with { type, url, enabled }
364
+ if (platformName === 'opencode') {
365
+ return {
366
+ type: 'remote',
367
+ url: platform.url,
368
+ enabled: true,
369
+ }
370
+ }
371
+ // VS Code http format: { type: "http", url }
372
+ if (platformName === 'vscode') {
373
+ return { type: 'http', url: platform.url }
374
+ }
375
+ // Cursor/Claude/Windsurf http format
376
+ return { type: 'http', url: platform.url }
377
+ }
378
+
379
+ case 'local': {
380
+ // OpenCode uses "mcp" key with { type: "local", command: [...], enabled }
381
+ if (platformName === 'opencode') {
382
+ const entry = {
383
+ type: 'local',
384
+ command: [platform.command, ...(platform.args || [])],
385
+ enabled: true,
386
+ }
387
+ if (Object.keys(env).length > 0) {
388
+ entry.environment = env
389
+ }
390
+ return entry
391
+ }
392
+ // Cursor/Claude/Windsurf format: { command, args?, env? }
393
+ const entry = {
394
+ command: platform.command,
395
+ ...(platform.args ? { args: platform.args } : {}),
396
+ }
397
+ if (Object.keys(env).length > 0) {
398
+ entry.env = env
399
+ }
400
+ return entry
401
+ }
402
+
403
+ case 'stdio': {
404
+ // VS Code stdio format: { type: "stdio", command, args? }
405
+ return {
406
+ type: 'stdio',
407
+ command: platform.command,
408
+ ...(platform.args ? { args: platform.args } : {}),
409
+ }
410
+ }
411
+
412
+ default:
413
+ return null
414
+ }
415
+ }
416
+
417
+ /**
418
+ * Read a JSON config file, returning the parsed object or {}.
419
+ */
420
+ function readConfig(filePath) {
421
+ try {
422
+ if (existsSync(filePath)) {
423
+ const raw = readFileSync(filePath, 'utf8')
424
+ return JSON.parse(raw)
425
+ }
426
+ } catch (err) {
427
+ console.warn(` ⚠️ Could not parse ${filePath}: ${err.message}`)
428
+ }
429
+ return {}
430
+ }
431
+
432
+ /**
433
+ * Write the MCP server entry to the platform config file.
434
+ *
435
+ * @param {string} platformName - Platform key
436
+ * @param {string} mcpKey - MCP canonical key
437
+ * @param {boolean} dryRun - If true, don't write anything
438
+ * @param {boolean} force - Overwrite existing entry
439
+ * @returns {{ status: string, path?: string, reason?: string, entry?: object }}
440
+ */
441
+ function writeMcpConfig(platformName, mcpKey, dryRun, force) {
442
+ const platform = PLATFORMS[platformName]
443
+ const mcp = MCPS[mcpKey]
444
+ if (!platform || !mcp) {
445
+ return { status: 'error', reason: 'unknown platform or mcp' }
446
+ }
447
+
448
+ // Find the best config path (first existing, or first fallback)
449
+ let configPath = null
450
+ for (const p of platform.configPaths) {
451
+ if (existsSync(p)) {
452
+ configPath = p
453
+ break
454
+ }
455
+ }
456
+ if (!configPath) {
457
+ configPath = platform.configPaths[0]
458
+ }
459
+
460
+ // Read existing config
461
+ const config = readConfig(configPath)
462
+ const configExisted = Object.keys(config).length > 0
463
+
464
+ // The key under which MCP servers are stored (e.g. 'mcp', 'servers', 'mcpServers')
465
+ const key = platform.configKey
466
+
467
+ // Ensure the section exists
468
+ if (!config[key]) {
469
+ config[key] = {}
470
+ }
471
+
472
+ // Check if the MCP already has an entry
473
+ if (config[key][mcpKey] && !force) {
474
+ return {
475
+ status: 'skipped',
476
+ reason: `Already exists in ${platform.configFile} (use --force to overwrite)`,
477
+ }
478
+ }
479
+
480
+ // Build the platform-specific entry
481
+ const entry = buildMcpEntry(mcpKey, platformName)
482
+ if (!entry) {
483
+ return {
484
+ status: 'error',
485
+ reason: `${mcp.name} does not support ${platform.label}`,
486
+ }
487
+ }
488
+
489
+ if (dryRun) {
490
+ return { status: 'dry-run', entry }
491
+ }
492
+
493
+ // Backup existing config before modifying
494
+ if (configExisted) {
495
+ const backupPath = `${configPath}.backup`
496
+ if (!existsSync(backupPath)) {
497
+ try {
498
+ copyFileSync(configPath, backupPath)
499
+ } catch {
500
+ // Non-fatal: continue even if backup fails
501
+ }
502
+ }
503
+ }
504
+
505
+ // Write the MCP entry
506
+ config[key][mcpKey] = entry
507
+
508
+ // Ensure directory exists
509
+ const dir = dirname(configPath)
510
+ if (!existsSync(dir)) {
511
+ mkdirSync(dir, { recursive: true })
512
+ }
513
+
514
+ writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8')
515
+
516
+ return { status: 'installed', path: configPath }
517
+ }
518
+
519
+ // ---------------------------------------------------------------------------
520
+ // CLI Argument Parsing
521
+ // ---------------------------------------------------------------------------
522
+
523
+ function parseArgs(argv) {
524
+ const args = {
525
+ platforms: [],
526
+ tier: 1,
527
+ mcp: null,
528
+ list: false,
529
+ dryRun: false,
530
+ force: false,
531
+ help: false,
532
+ }
533
+
534
+ for (let i = 2; i < argv.length; i++) {
535
+ switch (argv[i]) {
536
+ case '--platform':
537
+ args.platforms = argv[++i].split(',').map((s) => s.trim().toLowerCase())
538
+ break
539
+ case '--tier':
540
+ args.tier = parseInt(argv[++i], 10)
541
+ break
542
+ case '--mcp':
543
+ args.mcp = argv[++i].toLowerCase()
544
+ break
545
+ case '--list':
546
+ args.list = true
547
+ break
548
+ case '--dry-run':
549
+ args.dryRun = true
550
+ break
551
+ case '--force':
552
+ args.force = true
553
+ break
554
+ case '--help':
555
+ case '-h':
556
+ args.help = true
557
+ break
558
+ default:
559
+ console.warn(`⚠️ Unknown option: ${argv[i]}`)
560
+ break
561
+ }
562
+ }
563
+
564
+ // If no platforms specified and not --list, auto-detect
565
+ if (args.platforms.length === 0 && !args.list && !args.help) {
566
+ const detected = detectPlatforms()
567
+ if (detected.length > 0) {
568
+ args.platforms = detected
569
+ } else {
570
+ // Default to all platforms if none detected
571
+ args.platforms = Object.keys(PLATFORMS)
572
+ }
573
+ }
574
+
575
+ return args
576
+ }
577
+
578
+ function detectPlatforms() {
579
+ const detected = []
580
+ for (const [name, platform] of Object.entries(PLATFORMS)) {
581
+ if (platform.detect()) {
582
+ detected.push(name)
583
+ }
584
+ }
585
+ return detected
586
+ }
587
+
588
+ // ---------------------------------------------------------------------------
589
+ // Interactive Prompt
590
+ // ---------------------------------------------------------------------------
591
+
592
+ function askQuestion(query) {
593
+ return new Promise((resolve) => {
594
+ const rl = createInterface({ input: process.stdin, output: process.stdout })
595
+ rl.question(query, (answer) => {
596
+ rl.close()
597
+ resolve(answer.trim().toLowerCase())
598
+ })
599
+ })
600
+ }
601
+
602
+ async function promptForTier2Mcps() {
603
+ const selected = []
604
+ const tier2Mcps = Object.entries(MCPS).filter(([, mcp]) => mcp.tier === 2)
605
+
606
+ console.log('\nDomain (Tier 2) — ? [y/N]:')
607
+
608
+ for (const [key, mcp] of tier2Mcps) {
609
+ const envHints = mcp.env.length > 0 ? ` (needs: ${mcp.env.join(', ')})` : ''
610
+ const answer = await askQuestion(` ${mcp.name}${envHints}? [y/N] `)
611
+ if (answer === 'y' || answer === 'yes') {
612
+ selected.push(key)
613
+ }
614
+ }
615
+
616
+ return selected
617
+ }
618
+
619
+ // ---------------------------------------------------------------------------
620
+ // Validation
621
+ // ---------------------------------------------------------------------------
622
+
623
+ async function validateMcp(mcpKey) {
624
+ const mcp = MCPS[mcpKey]
625
+ if (!mcp) return { ok: false, message: 'unknown MCP' }
626
+ try {
627
+ return await mcp.validate()
628
+ } catch (err) {
629
+ return { ok: false, message: err.message }
630
+ }
631
+ }
632
+
633
+ // ---------------------------------------------------------------------------
634
+ // List Available MCPs
635
+ // ---------------------------------------------------------------------------
636
+
637
+ function listMcps() {
638
+ console.log('\nAvailable MCP Servers:\n')
639
+ console.log('Essential (Tier 1):')
640
+ for (const [key, mcp] of Object.entries(MCPS)) {
641
+ if (mcp.tier === 1) {
642
+ console.log(` ${key.padEnd(16)} ${mcp.name.padEnd(20)} ${mcp.description}`)
643
+ }
644
+ }
645
+ console.log('\nDomain (Tier 2):')
646
+ for (const [key, mcp] of Object.entries(MCPS)) {
647
+ if (mcp.tier === 2) {
648
+ const envInfo = mcp.env.length > 0 ? ` [env: ${mcp.env.join(', ')}]` : ''
649
+ console.log(` ${key.padEnd(16)} ${mcp.name.padEnd(20)} ${mcp.description}${envInfo}`)
650
+ }
651
+ }
652
+ console.log(`\nPlatforms supported: ${Object.keys(PLATFORMS).join(', ')}`)
653
+ console.log('')
654
+ }
655
+
656
+ // ---------------------------------------------------------------------------
657
+ // Help
658
+ // ---------------------------------------------------------------------------
659
+
660
+ function showHelp() {
661
+ console.log(`
662
+ install-mcp.mjs — Multi-platform MCP server installer for Pantheon
663
+
664
+ Usage:
665
+ node scripts/install-mcp.mjs Auto-detect platform, install Tier 1
666
+ node scripts/install-mcp.mjs --platform opencode Install for specific platform
667
+ node scripts/install-mcp.mjs --tier 2 Install Tier 2 (interactive prompt)
668
+ node scripts/install-mcp.mjs --mcp context7 Install a specific MCP only
669
+ node scripts/install-mcp.mjs --list List available MCPs
670
+ node scripts/install-mcp.mjs --dry-run Preview changes without writing
671
+ node scripts/install-mcp.mjs --force Overwrite existing MCP entries
672
+ node scripts/install-mcp.mjs --platform opencode --mcp github,context7
673
+
674
+ Options:
675
+ --platform <name> Target platform (opencode|vscode|cursor|claude|windsurf).
676
+ Comma-separated for multiple. Auto-detects if omitted.
677
+ --tier <n> 1=Essential (default), 2=Domain, 3=Project
678
+ --mcp <name> Install specific MCP(s) only. Comma-separated.
679
+ --list List available MCPs and exit
680
+ --dry-run Show what would be installed without writing
681
+ --force Overwrite existing MCP entries
682
+ --help, -h Show this help
683
+
684
+ Platforms:
685
+ opencode → opencode.json (mcp key)
686
+ vscode → .vscode/mcp.json (servers key)
687
+ cursor → .cursor/mcp.json (mcpServers key)
688
+ claude → .mcp.json or ~/.claude/settings.json (mcpServers key)
689
+ windsurf → ~/.codeium/windsurf/mcp_config.json (mcpServers key)
690
+
691
+ Environment Variables:
692
+ DATABASE_URL Required for PostgreSQL MCP (postgresql+ssl://...)
693
+ BRAVE_API_KEY Required for Brave Search MCP
694
+
695
+ Examples:
696
+ node scripts/install-mcp.mjs Auto-detect + install essentials
697
+ node scripts/install-mcp.mjs --platform opencode --tier 2 OpenCode + interactive Tier 2
698
+ node scripts/install-mcp.mjs --mcp docker Install only Docker MCP
699
+ node scripts/install-mcp.mjs --platform vscode --dry-run Preview VS Code changes
700
+ `)
701
+ }
702
+
703
+ // ---------------------------------------------------------------------------
704
+ // Progress / Logging Helpers
705
+ // ---------------------------------------------------------------------------
706
+
707
+ const progress = {
708
+ installed: [],
709
+ skipped: [],
710
+ errors: [],
711
+ validations: {},
712
+ }
713
+
714
+ function logResult(platformLabel, mcpKey, result) {
715
+ const mcp = MCPS[mcpKey]
716
+ const label = mcp ? mcp.name : mcpKey
717
+ switch (result.status) {
718
+ case 'installed':
719
+ console.log(` ✅ ${label} — config written to ${result.path}`)
720
+ progress.installed.push({ platform: platformLabel, mcp: mcpKey })
721
+ break
722
+ case 'skipped':
723
+ console.log(` ⏭️ ${label} — ${result.reason}`)
724
+ progress.skipped.push({ platform: platformLabel, mcp: mcpKey, reason: result.reason })
725
+ break
726
+ case 'dry-run': {
727
+ const desc =
728
+ result.entry?.type === 'remote' || result.entry?.type === 'http'
729
+ ? `remote → ${result.entry.url}`
730
+ : `local → ${result.entry.command} ${(result.entry.args || []).join(' ')}`
731
+ console.log(` 📋 ${label} — would install (${desc})`)
732
+ break
733
+ }
734
+ default:
735
+ console.log(` ❌ ${label} — ${result.reason}`)
736
+ progress.errors.push({ platform: platformLabel, mcp: mcpKey, reason: result.reason })
737
+ }
738
+ }
739
+
740
+ function logValidation(mcpKey, result) {
741
+ const mcp = MCPS[mcpKey]
742
+ const label = mcp ? mcp.name : mcpKey
743
+ if (result.ok) {
744
+ const extra = result.message ? ` — ${result.message}` : ''
745
+ console.log(` ✅ ${label}${extra}`)
746
+ } else {
747
+ const extra = result.message ? ` — ${result.message}` : ''
748
+ console.log(` ❌ ${label}${extra}`)
749
+ }
750
+ progress.validations[mcpKey] = result
751
+ }
752
+
753
+ // ---------------------------------------------------------------------------
754
+ // Main
755
+ // ---------------------------------------------------------------------------
756
+
757
+ async function main() {
758
+ const args = parseArgs(process.argv)
759
+
760
+ if (args.help) {
761
+ showHelp()
762
+ process.exit(0)
763
+ }
764
+
765
+ if (args.list) {
766
+ listMcps()
767
+ process.exit(0)
768
+ }
769
+
770
+ // Determine which MCPs to install
771
+ let mcpKeys = []
772
+
773
+ if (args.mcp) {
774
+ // Specific MCP(s) requested
775
+ mcpKeys = args.mcp.split(',').map((s) => s.trim().toLowerCase())
776
+ for (const key of mcpKeys) {
777
+ if (!MCPS[key]) {
778
+ console.error(`❌ Unknown MCP: "${key}". Use --list to see available MCPs.`)
779
+ process.exit(1)
780
+ }
781
+ }
782
+ } else {
783
+ // Tier 1 — always included
784
+ const tier1Mcps = Object.entries(MCPS)
785
+ .filter(([, mcp]) => mcp.tier === 1)
786
+ .map(([key]) => key)
787
+ mcpKeys.push(...tier1Mcps)
788
+
789
+ // Tier 2 — interactive prompt
790
+ if (args.tier >= 2) {
791
+ const tier2Selected = await promptForTier2Mcps()
792
+ mcpKeys.push(...tier2Selected)
793
+ }
794
+ }
795
+
796
+ if (mcpKeys.length === 0) {
797
+ console.log('No MCPs selected. Nothing to install.\n')
798
+ process.exit(0)
799
+ }
800
+
801
+ // Determine target platforms
802
+ const platforms = args.platforms
803
+ const platformLabels = platforms.map((p) => PLATFORMS[p]?.label || p)
804
+
805
+ if (args.dryRun) {
806
+ console.log('\n🔍 Dry-run mode — no files will be written\n')
807
+ }
808
+
809
+ console.log(
810
+ `\n🔍 ${platforms.length === 1 ? 'Platform' : 'Platforms'}: ${platformLabels.join(', ')}`,
811
+ )
812
+ console.log(
813
+ `📦 Installing ${mcpKeys.length} MCP(s): ${mcpKeys.map((k) => MCPS[k]?.name || k).join(', ')}\n`,
814
+ )
815
+
816
+ // Install per platform
817
+ for (const platformName of platforms) {
818
+ const platform = PLATFORMS[platformName]
819
+ if (!platform) {
820
+ console.warn(` ⚠️ Unknown platform: ${platformName} — skipping`)
821
+ continue
822
+ }
823
+
824
+ console.log(`🔧 ${platform.label} (${platform.configFile}):`)
825
+
826
+ for (const mcpKey of mcpKeys) {
827
+ const result = writeMcpConfig(platformName, mcpKey, args.dryRun, args.force)
828
+ logResult(platform.label, mcpKey, result)
829
+ }
830
+
831
+ console.log('')
832
+ }
833
+
834
+ // Validation
835
+ if (!args.dryRun) {
836
+ console.log('🔍 Validating MCPs...\n')
837
+ for (const key of mcpKeys) {
838
+ const result = await validateMcp(key)
839
+ logValidation(key, result)
840
+ }
841
+ console.log('')
842
+ } else {
843
+ console.log('🔍 Validation skipped in dry-run mode\n')
844
+ }
845
+
846
+ // Summary
847
+ console.log('='.repeat(60))
848
+ console.log('📋 Installation Summary')
849
+ console.log('='.repeat(60))
850
+ console.log(` Platforms: ${platformLabels.join(', ')}`)
851
+ console.log(` MCPs: ${mcpKeys.length} requested`)
852
+ console.log(` Installed: ${progress.installed.length}`)
853
+ console.log(` Skipped: ${progress.skipped.length}`)
854
+ console.log(` Errors: ${progress.errors.length}`)
855
+
856
+ if (progress.installed.length > 0) {
857
+ console.log('\n ✅ Installed:')
858
+ for (const item of progress.installed) {
859
+ console.log(` - ${item.mcp} → ${item.platform}`)
860
+ }
861
+ }
862
+
863
+ if (progress.skipped.length > 0 && !args.dryRun) {
864
+ console.log('\n ⏭️ Skipped:')
865
+ for (const item of progress.skipped) {
866
+ console.log(` - ${item.mcp} (${item.platform}): ${item.reason}`)
867
+ }
868
+ }
869
+
870
+ if (progress.errors.length > 0) {
871
+ console.log('\n ❌ Errors:')
872
+ for (const item of progress.errors) {
873
+ console.log(` - ${item.mcp} (${item.platform}): ${item.reason}`)
874
+ }
875
+ }
876
+
877
+ console.log('\n⚠️ Reminder: Restart your editor/CLI tool for MCP changes to take effect.')
878
+ console.log(' For VS Code: Cmd+Shift+P → "Developer: Reload Window"')
879
+ console.log(' For OpenCode: Just restart the session')
880
+ console.log('')
881
+
882
+ process.exit(progress.errors.length > 0 ? 1 : 0)
883
+ }
884
+
885
+ main()