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,655 @@
1
+ /**
2
+ * shared.mjs — Shared utilities for Pantheon platform installers
3
+ */
4
+
5
+ import {
6
+ cpSync,
7
+ existsSync,
8
+ mkdirSync,
9
+ readdirSync,
10
+ readFileSync,
11
+ rmSync,
12
+ statSync,
13
+ writeFileSync,
14
+ } from 'node:fs'
15
+ import { basename, dirname, join } from 'node:path'
16
+ import { fileURLToPath } from 'node:url'
17
+ import yaml from 'js-yaml'
18
+
19
+ export const __dirname = dirname(fileURLToPath(import.meta.url))
20
+ export const ROOT = join(__dirname, '..', '..')
21
+ export const AGENTS_DIR = join(ROOT, 'src', 'agents')
22
+ export const PLATFORM_DIR = join(ROOT, 'platform')
23
+
24
+ // Auto-detect agent names from agents/ directory
25
+ export function getAgentNames() {
26
+ if (!existsSync(AGENTS_DIR)) return []
27
+ return readdirSync(AGENTS_DIR)
28
+ .filter((f) => f.endsWith('.agent.md') || f.endsWith('.md'))
29
+ .map((f) => f.replace(/\.(agent\.)?md$/, ''))
30
+ .sort()
31
+ }
32
+
33
+ // Cached constant for backward compatibility
34
+ export const AGENT_NAMES = getAgentNames()
35
+
36
+ export const PLATFORM_DETECTORS = {
37
+ opencode: (target) =>
38
+ existsSync(join(target, 'opencode.json')) ||
39
+ existsSync(join(target, 'platform', 'opencode', 'opencode.json')),
40
+ claude: (target) => existsSync(join(target, '.claude')) || existsSync(join(target, 'CLAUDE.md')),
41
+ cursor: (target) =>
42
+ existsSync(join(target, '.cursor')) || existsSync(join(target, '.cursorrules')),
43
+ windsurf: (target) =>
44
+ existsSync(join(target, '.windsurf')) || existsSync(join(target, '.windsurfrules')),
45
+ copilot: (target) =>
46
+ existsSync(join(target, '.github', 'copilot-instructions.md')) ||
47
+ existsSync(join(target, '.vscode')),
48
+ continue: (target) =>
49
+ existsSync(join(target, '.continue', 'config.yaml')) || existsSync(join(target, '.continue')),
50
+ cline: (target) => existsSync(join(target, '.clinerules')),
51
+ }
52
+
53
+ export const summary = {
54
+ opencode: { created: 0, skipped: 0, errors: 0 },
55
+ claude: { created: 0, skipped: 0, errors: 0 },
56
+ cursor: { created: 0, skipped: 0, errors: 0 },
57
+ windsurf: { created: 0, skipped: 0, errors: 0 },
58
+ copilot: { created: 0, skipped: 0, errors: 0 },
59
+ continue: { created: 0, skipped: 0, errors: 0 },
60
+ cline: { created: 0, skipped: 0, errors: 0 },
61
+ }
62
+
63
+ export function showHelp() {
64
+ console.log(`
65
+ install.mjs — Multi-platform Pantheon agent installer
66
+
67
+ Usage:
68
+ node scripts/install.mjs auto-detect, cwd
69
+ node scripts/install.mjs --target /path/to/project auto-detect, target
70
+ node scripts/install.mjs --platforms opencode,claude specific platforms, cwd
71
+ node scripts/install.mjs --target /path --platforms all all platforms
72
+ node scripts/install.mjs --detect detect platforms without installing
73
+ node scripts/install.mjs --dry-run preview without writing
74
+ node scripts/install.mjs --backup create timestamped backup before writing
75
+ node scripts/install.mjs --clean wipe + fresh install (all components)
76
+ node scripts/install.mjs --clean --components agents,skills wipe only agents+skills, reinstall
77
+ node scripts/install.mjs --components agents install only agents (no skills/instructions)
78
+ node scripts/install.mjs --help show this help
79
+
80
+ Components (--components):
81
+ Comma-separated list of what to install. Default: agents,skills,instructions,commands,plugins
82
+ agents → agent .md files
83
+ skills → skill definitions (.opencode/skills/)
84
+ instructions → AGENTS.md + instructions/*.instructions.md
85
+ prompts → prompts/*.prompt.md (optional)
86
+ commands → .opencode/commands/*.md (OpenCode command shortcuts)
87
+
88
+ Clean mode (--clean):
89
+ Deletes ALL existing Pantheon files for selected components, then
90
+ re-installs fresh from source. Useful after removing/renaming agents or skills.
91
+ OFF by default — without --clean only copies new/changed files (never deletes).
92
+
93
+ Platforms:
94
+ opencode → .opencode/agents/ + opencode.json
95
+ claude → .claude/agents/ + CLAUDE.md + settings.json
96
+ cursor → .cursor/rules/ (renamed .mdc files)
97
+ windsurf → .windsurf/agents/ + .windsurfrules
98
+ copilot → .github/agents/ symlinks + .vscode/settings.json check
99
+ continue → .continue/rules/ + .continue/config.yaml
100
+ cline → .clinerules/ (no extension, plain markdown)
101
+ all → install every platform
102
+
103
+ When --platforms is omitted, the script auto-detects which platforms
104
+ the target project already supports (based on config files present).
105
+ If none are detected, ALL platforms are installed.
106
+ `)
107
+ }
108
+
109
+ export function parseArgs(argv) {
110
+ const args = {
111
+ target: null,
112
+ platforms: null,
113
+ components: null,
114
+ dryRun: false,
115
+ clean: false,
116
+ backup: false,
117
+ help: false,
118
+ detect: false,
119
+ }
120
+
121
+ for (let i = 2; i < argv.length; i++) {
122
+ switch (argv[i]) {
123
+ case '--target':
124
+ args.target = argv[++i]
125
+ break
126
+ case '--platforms':
127
+ args.platforms = argv[++i].split(',').map((s) => s.trim().toLowerCase())
128
+ break
129
+ case '--components':
130
+ args.components = argv[++i].split(',').map((s) => s.trim().toLowerCase())
131
+ break
132
+ case '--detect':
133
+ args.detect = true
134
+ break
135
+ case '--dry-run':
136
+ args.dryRun = true
137
+ break
138
+ case '--clean':
139
+ args.clean = true
140
+ break
141
+ case '--backup':
142
+ args.backup = true
143
+ break
144
+ case '--help':
145
+ args.help = true
146
+ break
147
+ default:
148
+ console.warn(`⚠️ Unknown option: ${argv[i]}`)
149
+ break
150
+ }
151
+ }
152
+
153
+ if (!args.target) {
154
+ args.target = process.cwd()
155
+ }
156
+
157
+ // Resolve to absolute path
158
+ args.target = resolveTarget(args.target)
159
+
160
+ return args
161
+ }
162
+
163
+ export function resolveTarget(target) {
164
+ // If it's already absolute, use it
165
+ if (target.startsWith('/')) return target
166
+ // If it's relative, resolve from cwd
167
+ return join(process.cwd(), target)
168
+ }
169
+
170
+ export function createBackup(target) {
171
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
172
+ const backupDir = join(target, '..', `.pantheon-bak-${basename(target)}-${timestamp}`)
173
+
174
+ console.log(`\n 💾 Creating backup: ${backupDir}`)
175
+ mkdirSync(backupDir, { recursive: true })
176
+
177
+ // Copy only relevant Pantheon directories/files
178
+ const items = ['agents', 'skills', 'commands', 'instructions', 'opencode.json', 'tui.json']
179
+ let count = 0
180
+ for (const item of items) {
181
+ const src = join(target, item)
182
+ try {
183
+ cpSync(src, join(backupDir, item), {
184
+ recursive: true,
185
+ errorOnExist: false,
186
+ })
187
+ count++
188
+ } catch {
189
+ // File or directory doesn't exist in target — skip silently
190
+ }
191
+ }
192
+
193
+ console.log(` ✅ Backed up ${count} items to: ${backupDir}`)
194
+ return backupDir
195
+ }
196
+
197
+ export function detectAndReport(target) {
198
+ const PLATFORM_LABELS = {
199
+ opencode: 'OpenCode',
200
+ claude: 'Claude Code',
201
+ cursor: 'Cursor',
202
+ windsurf: 'Windsurf',
203
+ copilot: 'VS Code / Copilot',
204
+ continue: 'Continue.dev',
205
+ cline: 'Cline',
206
+ }
207
+
208
+ const CONFIG_FILES = {
209
+ opencode: () => 'opencode.json',
210
+ claude: () => '.claude/ or CLAUDE.md',
211
+ cursor: () => '.cursor/ or .cursorrules',
212
+ windsurf: () => '.windsurf/ or .windsurfrules',
213
+ copilot: () => '.github/copilot-instructions.md or .vscode/',
214
+ continue: () => '.continue/config.yaml or .continue/',
215
+ cline: () => '.clinerules',
216
+ }
217
+
218
+ console.log(`\n 🔍 Pantheon Platform Detection`)
219
+ console.log(` Target: ${target}\n`)
220
+
221
+ const results = []
222
+ for (const [platform, detector] of Object.entries(PLATFORM_DETECTORS)) {
223
+ const found = detector(target)
224
+ results.push({ platform, found })
225
+ }
226
+
227
+ // Table header
228
+ console.log(` ${'Platform'.padEnd(12)} ${'Detected?'.padEnd(12)} Config file`)
229
+ console.log(` ${'─'.repeat(55)}`)
230
+
231
+ const detected = results.filter((r) => r.found)
232
+ for (const r of results) {
233
+ const status = r.found ? '✅ YES' : '❌ no'
234
+ const configFile = r.found ? CONFIG_FILES[r.platform]() : '(not found)'
235
+ console.log(` ${PLATFORM_LABELS[r.platform].padEnd(12)} ${status.padEnd(12)} ${configFile}`)
236
+ }
237
+
238
+ console.log(`\n 📊 ${detected.length} of ${results.length} platforms detected.`)
239
+ if (detected.length > 0) {
240
+ const names = detected.map((r) => r.platform).join(',')
241
+ console.log(` → Install with: node scripts/install.mjs --platforms ${names}`)
242
+ }
243
+
244
+ return detected.map((r) => r.platform)
245
+ }
246
+
247
+ export function detectPlatforms(target) {
248
+ const detected = []
249
+ for (const [platform, detector] of Object.entries(PLATFORM_DETECTORS)) {
250
+ if (detector(target)) {
251
+ detected.push(platform)
252
+ }
253
+ }
254
+ return detected
255
+ }
256
+
257
+ export function sourceDirValid(dir) {
258
+ if (!existsSync(dir)) return false
259
+ const entries = readdirSync(dir)
260
+ return entries.length > 0
261
+ }
262
+
263
+ export function copyFiles(srcDir, dstDir, dryRun, renameMap = null, clean = false) {
264
+ const entries = readdirSync(srcDir)
265
+ let created = 0
266
+ let skipped = 0
267
+
268
+ // Build set of expected destination filenames (after rename)
269
+ const dstNames = new Set()
270
+ for (const entry of entries) {
271
+ const srcFile = join(srcDir, entry)
272
+ if (!existsSync(srcFile)) continue
273
+ const dstName = renameMap ? (renameMap(entry) ?? entry) : entry
274
+ dstNames.add(dstName)
275
+
276
+ const dstFile = join(dstDir, dstName)
277
+
278
+ const content = readFileSync(srcFile, 'utf8')
279
+ const existing = existsSync(dstFile) ? readFileSync(dstFile, 'utf8') : null
280
+
281
+ if (existing !== null) {
282
+ if (existing === content) {
283
+ skipped++
284
+ continue
285
+ }
286
+ if (!dryRun) {
287
+ writeFileSync(dstFile, content, 'utf8')
288
+ }
289
+ created++
290
+ } else {
291
+ if (!dryRun) {
292
+ writeFileSync(dstFile, content, 'utf8')
293
+ }
294
+ created++
295
+ }
296
+ }
297
+
298
+ // Remove stale Pantheon agent files from dst (opt-in via clean flag)
299
+ // Safety: ONLY removes files whose name matches the agent file pattern
300
+ // (lowercase name + .md/.mdc extension), regardless of AGENT_NAMES membership.
301
+ // This handles deleted canonical agents (e.g., agora) whose source file is gone.
302
+ if (clean && existsSync(dstDir)) {
303
+ const dstEntries = readdirSync(dstDir)
304
+ const agentPattern = /^[a-z-]+\.(md|mdc)$/
305
+ for (const entry of dstEntries) {
306
+ if (dstNames.has(entry)) continue // still in source
307
+ const dstFile = join(dstDir, entry)
308
+ if (!statSync(dstFile).isFile()) continue
309
+ // Safety: only remove files that match agent file naming pattern
310
+ // (user custom files with different naming patterns are untouched)
311
+ if (!agentPattern.test(entry)) continue
312
+ if (!dryRun) {
313
+ rmSync(dstFile, { force: true })
314
+ }
315
+ created++
316
+ }
317
+ }
318
+
319
+ return { created, skipped }
320
+ }
321
+
322
+ export function writeIfChanged(filePath, content, dryRun) {
323
+ const existing = existsSync(filePath) ? readFileSync(filePath, 'utf8') : null
324
+ if (existing === content) {
325
+ return 'skipped'
326
+ }
327
+ if (!dryRun) {
328
+ writeFileSync(filePath, content, 'utf8')
329
+ }
330
+ return 'created'
331
+ }
332
+
333
+ export function collectSkillNames() {
334
+ const skillsDir = join(ROOT, 'src', 'skills')
335
+ if (!existsSync(skillsDir)) return []
336
+ return readdirSync(skillsDir)
337
+ .filter((entry) => {
338
+ const entryPath = join(skillsDir, entry)
339
+ return statSync(entryPath).isDirectory() && existsSync(join(entryPath, 'SKILL.md'))
340
+ })
341
+ .sort()
342
+ }
343
+
344
+ // ---------------------------------------------------------------------------
345
+ // YAML / frontmatter helpers
346
+ // ---------------------------------------------------------------------------
347
+
348
+ /**
349
+ * Parse ---frontmatter--- + body from a markdown file.
350
+ * Returns { fm: object, body: string } or null if no frontmatter.
351
+ */
352
+ export function parseFrontmatter(content) {
353
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/)
354
+ if (!match) return null
355
+ return {
356
+ fm: yaml.load(match[1]) ?? {},
357
+ body: match[2],
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Serialize a frontmatter object back to YAML.
363
+ * Uses long-line mode and avoids unnecessary quoting.
364
+ */
365
+ export function serializeFm(fm) {
366
+ return yaml.dump(fm, {
367
+ lineWidth: -1,
368
+ quotingType: '"',
369
+ forceQuotes: false,
370
+ noRefs: true,
371
+ })
372
+ }
373
+
374
+ export function installSkills(skills, target, dryRun, subDir = '.opencode') {
375
+ const srcSkillsDir = join(ROOT, 'src', 'skills')
376
+ const dstSkillsDir = join(target, subDir, 'skills')
377
+
378
+ let created = 0
379
+ let skipped = 0
380
+
381
+ for (const skill of skills) {
382
+ const src = join(srcSkillsDir, skill)
383
+ const dst = join(dstSkillsDir, skill)
384
+
385
+ if (!dryRun) mkdirSync(dst, { recursive: true })
386
+
387
+ function copyDirRecursive(from, to) {
388
+ const entries = readdirSync(from)
389
+ for (const entry of entries) {
390
+ const srcPath = join(from, entry)
391
+ const dstPath = join(to, entry)
392
+ if (statSync(srcPath).isDirectory()) {
393
+ if (!dryRun) mkdirSync(dstPath, { recursive: true })
394
+ copyDirRecursive(srcPath, dstPath)
395
+ } else {
396
+ const content = readFileSync(srcPath, 'utf8')
397
+ const existing = existsSync(dstPath) ? readFileSync(dstPath, 'utf8') : null
398
+ if (existing === content) {
399
+ skipped++
400
+ } else {
401
+ if (!dryRun) writeFileSync(dstPath, content, 'utf8')
402
+ created++
403
+ }
404
+ }
405
+ }
406
+ }
407
+
408
+ copyDirRecursive(src, dst)
409
+ }
410
+
411
+ return { created, skipped }
412
+ }
413
+
414
+ export function syncDir(src, dst, dryRun, clean = false, filter = null) {
415
+ if (!existsSync(src)) return { created: 0, skipped: 0 }
416
+ let created = 0
417
+ let skipped = 0
418
+
419
+ if (clean && existsSync(dst)) {
420
+ if (!dryRun) {
421
+ rmSync(dst, { recursive: true, force: true })
422
+ }
423
+ }
424
+
425
+ if (!dryRun) mkdirSync(dst, { recursive: true })
426
+
427
+ const entries = readdirSync(src)
428
+ for (const entry of entries) {
429
+ if (filter && !filter(entry)) continue
430
+ const srcPath = join(src, entry)
431
+ const dstPath = join(dst, entry)
432
+ if (statSync(srcPath).isDirectory()) {
433
+ const sub = syncDir(srcPath, dstPath, dryRun, false, filter)
434
+ created += sub.created
435
+ skipped += sub.skipped
436
+ } else {
437
+ const content = readFileSync(srcPath, 'utf8')
438
+ const existing = existsSync(dstPath) ? readFileSync(dstPath, 'utf8') : null
439
+ if (existing === content) {
440
+ skipped++
441
+ } else {
442
+ if (!dryRun) writeFileSync(dstPath, content, 'utf8')
443
+ created++
444
+ }
445
+ }
446
+ }
447
+ return { created, skipped }
448
+ }
449
+
450
+ // ---------------------------------------------------------------------------
451
+ // MCP Servers schema validation
452
+ // ---------------------------------------------------------------------------
453
+
454
+ /**
455
+ * Schema definition for mcpServers frontmatter field.
456
+ * Max 5 MCPs per agent, tools must reference tools in agent's tools: array.
457
+ */
458
+ export const MCP_SERVERS_SCHEMA = {
459
+ type: 'array',
460
+ maxItems: 5,
461
+ items: {
462
+ type: 'object',
463
+ required: ['name', 'tools', 'when'],
464
+ properties: {
465
+ name: {
466
+ type: 'string',
467
+ minLength: 1,
468
+ description: 'MCP server identifier',
469
+ },
470
+ tools: {
471
+ type: 'array',
472
+ minItems: 1,
473
+ items: {
474
+ type: 'string',
475
+ minLength: 1,
476
+ },
477
+ description: 'Tools this MCP provides (must exist in agent tools: array)',
478
+ },
479
+ when: {
480
+ type: 'string',
481
+ minLength: 1,
482
+ description: 'Activation condition (e.g., "always", "on-demand")',
483
+ },
484
+ },
485
+ additionalProperties: false,
486
+ },
487
+ }
488
+
489
+ /**
490
+ * Validate mcpServers against schema constraints.
491
+ * @param {Array} mcpServers - The mcpServers array from agent frontmatter
492
+ * @param {string[]} agentTools - The agent's tools: array for cross-reference validation (unused, MCP tools are external)
493
+ * @returns {{ valid: boolean, errors: string[] }}
494
+ */
495
+ export function validateMcpServers(mcpServers, _agentTools = []) {
496
+ const errors = []
497
+
498
+ if (!Array.isArray(mcpServers)) {
499
+ errors.push('mcpServers must be an array')
500
+ return { valid: false, errors }
501
+ }
502
+
503
+ if (mcpServers.length > 5) {
504
+ errors.push(`mcpServers exceeds maximum of 5 (got ${mcpServers.length})`)
505
+ }
506
+
507
+ for (let i = 0; i < mcpServers.length; i++) {
508
+ const mcp = mcpServers[i]
509
+ const prefix = `mcpServers[${i}]`
510
+
511
+ if (!mcp || typeof mcp !== 'object') {
512
+ errors.push(`${prefix}: must be an object`)
513
+ continue
514
+ }
515
+
516
+ // Required fields
517
+ if (!mcp.name || typeof mcp.name !== 'string' || mcp.name.trim() === '') {
518
+ errors.push(`${prefix}.name: required and must be non-empty string`)
519
+ }
520
+
521
+ if (!Array.isArray(mcp.tools) || mcp.tools.length === 0) {
522
+ errors.push(`${prefix}.tools: required and must be non-empty array`)
523
+ }
524
+
525
+ if (!mcp.when || typeof mcp.when !== 'string' || mcp.when.trim() === '') {
526
+ errors.push(`${prefix}.when: required and must be non-empty string`)
527
+ }
528
+
529
+ // Disallow additional properties
530
+ const allowedKeys = ['name', 'tools', 'when']
531
+ for (const key of Object.keys(mcp)) {
532
+ if (!allowedKeys.includes(key)) {
533
+ errors.push(`${prefix}: unexpected property "${key}"`)
534
+ }
535
+ }
536
+ }
537
+
538
+ return { valid: errors.length === 0, errors }
539
+ }
540
+
541
+ export function printSummary(target, platforms) {
542
+ const PLATFORM_LABELS = {
543
+ opencode: 'OpenCode',
544
+ claude: 'Claude Code',
545
+ cursor: 'Cursor',
546
+ windsurf: 'Windsurf',
547
+ copilot: 'VS Code / Copilot',
548
+ continue: 'Continue.dev',
549
+ cline: 'Cline',
550
+ }
551
+
552
+ console.log('')
553
+ console.log('='.repeat(60))
554
+ console.log('📋 Installation Summary')
555
+ console.log(` Target: ${target}`)
556
+ console.log('='.repeat(60))
557
+
558
+ let totalCreated = 0
559
+ let totalSkipped = 0
560
+ let totalErrors = 0
561
+
562
+ for (const platform of platforms) {
563
+ const label = PLATFORM_LABELS[platform] ?? platform
564
+ const stats = summary[platform]
565
+ totalCreated += stats.created
566
+ totalSkipped += stats.skipped
567
+ totalErrors += stats.errors
568
+
569
+ const status = stats.errors > 0 ? '⚠️' : '✅'
570
+ console.log(
571
+ ` ${status} ${label}: ${stats.created} created, ${stats.skipped} skipped${stats.errors > 0 ? `, ${stats.errors} errors` : ''}`,
572
+ )
573
+ }
574
+
575
+ console.log('-'.repeat(60))
576
+ console.log(
577
+ ` Total: ${totalCreated} files created, ${totalSkipped} files skipped, ${totalErrors} errors`,
578
+ )
579
+
580
+ if (totalErrors > 0) {
581
+ console.log(' ⚠️ Some platforms had errors — review warnings above.')
582
+ }
583
+
584
+ console.log('')
585
+ console.log('📖 Next Steps:')
586
+ console.log('')
587
+
588
+ if (platforms.includes('opencode')) {
589
+ console.log(' OpenCode:')
590
+ console.log(` - Run \`opencode\` in ${target}`)
591
+ console.log(' - Invoke agents with @agent-name in chat')
592
+ console.log(' - To customize models: edit opencode.json')
593
+ console.log(' - Skills are in .opencode/skills/ (auto-loaded)')
594
+ console.log('')
595
+ }
596
+
597
+ if (platforms.includes('claude')) {
598
+ console.log(' Claude Code:')
599
+ console.log(` - Run \`claude\` in ${target}`)
600
+ console.log(' - Agents in .claude/agents/')
601
+ console.log(' - Skills in .claude/skills/')
602
+ console.log(' - Settings in .claude/settings.json')
603
+ console.log('')
604
+ }
605
+
606
+ if (platforms.includes('cursor')) {
607
+ console.log(' Cursor:')
608
+ console.log(` - Rules in .cursor/rules/ (.mdc format)`)
609
+ console.log(' - Skills in .cursor/skills/')
610
+ console.log(' - AGENTS.md in project root')
611
+ console.log(' - Use @agent-name in Agent mode')
612
+ console.log('')
613
+ }
614
+
615
+ if (platforms.includes('windsurf')) {
616
+ console.log(' Windsurf (Cascade):')
617
+ console.log(` - Rules in .windsurf/rules/`)
618
+ console.log(' - Skills in .windsurf/skills/')
619
+ console.log(' - Workflows in .windsurf/workflows/ (/orchestrate, /code-review)')
620
+ console.log(' - AGENTS.md in project root')
621
+ console.log('')
622
+ }
623
+
624
+ if (platforms.includes('copilot')) {
625
+ console.log(' VS Code / Copilot:')
626
+ console.log(` - Agents in .github/agents/`)
627
+ console.log(' - Ensure .vscode/settings.json has plugin config')
628
+ console.log(' - Use @agent-name in VS Code Copilot Chat')
629
+ console.log(' - Plugin manifest in plugin.json')
630
+ console.log('')
631
+ }
632
+
633
+ if (platforms.includes('continue')) {
634
+ console.log(' Continue.dev:')
635
+ console.log(` - Rules in .continue/rules/`)
636
+ console.log(' - Config in .continue/config.yaml')
637
+ console.log(' - Rules are injected into system prompts (no @name invocation)')
638
+ console.log(' - Edit config.yaml to set API keys and models')
639
+ console.log(' - Use /reload to apply config changes')
640
+ console.log('')
641
+ }
642
+
643
+ if (platforms.includes('cline')) {
644
+ console.log(' Cline:')
645
+ console.log(` - Agent rules in .clinerules/ (no extension, plain markdown)`)
646
+ console.log(' - Skills in .clinerules/skills/')
647
+ console.log(' - Commands in .clinerules/commands/')
648
+ console.log(' - AGENTS.md in project root')
649
+ console.log(' - Invoke agents with @agent-name in Cline chat')
650
+ console.log('')
651
+ }
652
+
653
+ console.log(' 📚 Full documentation: https://github.com/ils15/pantheon')
654
+ console.log(' 🐛 Report issues: https://github.com/ils15/pantheon/issues')
655
+ }