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.
- package/AGENTS.md +37 -0
- package/LICENSE +21 -0
- package/README.md +1013 -0
- package/bin/pantheon-init.mjs +183 -0
- package/commands/pantheon-audit.md +25 -0
- package/commands/pantheon-bg.md +10 -0
- package/commands/pantheon-consolidate.md +11 -0
- package/commands/pantheon-deepwork.md +128 -0
- package/commands/pantheon-doc.md +10 -0
- package/commands/pantheon-focus.md +9 -0
- package/commands/pantheon-forget.md +58 -0
- package/commands/pantheon-hash.md +11 -0
- package/commands/pantheon-optimize.md +79 -0
- package/commands/pantheon-remember.md +44 -0
- package/commands/pantheon-search.md +48 -0
- package/commands/pantheon-status.md +71 -0
- package/commands/pantheon-todo.md +11 -0
- package/commands/pantheon.md +49 -0
- package/docs/AGENT-MCP.md +194 -0
- package/docs/ARCHITECTURE.md +384 -0
- package/docs/BRANCH-PROTECTION.md +142 -0
- package/docs/INDEX.md +81 -0
- package/docs/INSTALLATION.md +217 -0
- package/docs/MCP.md +238 -0
- package/docs/MEMORY.md +471 -0
- package/docs/MIGRATION-MEMORY-BANK.md +139 -0
- package/docs/PLATFORMS.md +5 -0
- package/docs/QUICKSTART.md +49 -0
- package/docs/README.md +18 -0
- package/docs/RELEASING.md +256 -0
- package/docs/SETUP.md +5 -0
- package/docs/UPGRADING.md +41 -0
- package/docs/mcp-recommendations.md +439 -0
- package/docs/mcp-tools.md +156 -0
- package/docs/mcp-user-guide.md +204 -0
- package/docs/persistence-mcp.md +111 -0
- package/package.json +72 -0
- package/pantheon.schema.json +158 -0
- package/scripts/__init__.py +0 -0
- package/scripts/_pantheon_paths.py +68 -0
- package/scripts/check-version-consistency.sh +23 -0
- package/scripts/code_mode_server.py +202 -0
- package/scripts/doctor.mjs +763 -0
- package/scripts/generate-prompts.sh +222 -0
- package/scripts/generate-routing-docs.mjs +104 -0
- package/scripts/hash_verify.py +192 -0
- package/scripts/init-pantheon-mcp.sh +118 -0
- package/scripts/install/agents-md.mjs +214 -0
- package/scripts/install/health-check.mjs +196 -0
- package/scripts/install/migrate.mjs +209 -0
- package/scripts/install/opencode.mjs +645 -0
- package/scripts/install/shared.mjs +655 -0
- package/scripts/install/venv.mjs +116 -0
- package/scripts/install-mcp.mjs +885 -0
- package/scripts/install.mjs +26 -0
- package/scripts/manifest.mjs +622 -0
- package/scripts/mcp_persistence_server.py +459 -0
- package/scripts/mcp_resources_server.py +206 -0
- package/scripts/memory_cache.py +78 -0
- package/scripts/memory_mcp_server.py +605 -0
- package/scripts/paths.py +64 -0
- package/scripts/prune_context.py +72 -0
- package/scripts/release-bundle.mjs +109 -0
- package/scripts/scrub-secrets.py +282 -0
- package/scripts/scrub_secrets.py +281 -0
- package/scripts/test-context-compression.sh +166 -0
- package/scripts/themis_heuristic_scan.py +287 -0
- package/scripts/todo_enforcer.py +242 -0
- package/scripts/uninstall.mjs +1057 -0
- package/scripts/validate-routing.mjs +160 -0
- package/scripts/validate_agent_frontmatter.py +226 -0
- package/scripts/versioning.mjs +254 -0
- package/skills-lock.json +16 -0
- package/src/agents/aphrodite.md +162 -0
- package/src/agents/apollo.md +109 -0
- package/src/agents/athena.md +226 -0
- package/src/agents/demeter.md +146 -0
- package/src/agents/gaia.md +82 -0
- package/src/agents/hephaestus.md +105 -0
- package/src/agents/hermes.md +302 -0
- package/src/agents/iris.md +99 -0
- package/src/agents/mnemosyne.md +226 -0
- package/src/agents/nyx.md +87 -0
- package/src/agents/prometheus.md +199 -0
- package/src/agents/talos.md +93 -0
- package/src/agents/themis.md +187 -0
- package/src/agents/zeus.md +209 -0
- package/src/instructions/agent-return-format.instructions.md +26 -0
- package/src/instructions/backend-standards.instructions.md +45 -0
- package/src/instructions/documentation-standards.instructions.md +53 -0
- package/src/instructions/frontend-standards.instructions.md +46 -0
- package/src/instructions/memory-protocol.instructions.md +67 -0
- package/src/instructions/yagni.instructions.md +21 -0
- package/src/instructions/zeus-anti-stall.instructions.md +72 -0
- package/src/instructions/zeus-communication-rules.instructions.md +15 -0
- package/src/instructions/zeus-council-synthesis.instructions.md +105 -0
- package/src/instructions/zeus-timeout-retry.instructions.md +127 -0
- package/src/mcp/_pantheon_paths.py +67 -0
- package/src/mcp/code_mode_server.py +202 -0
- package/src/mcp/init-pantheon-mcp.sh +118 -0
- package/src/mcp/install-mcp.mjs +885 -0
- package/src/mcp/mcp_persistence_server.py +458 -0
- package/src/mcp/mcp_resources_server.py +205 -0
- package/src/mcp/memory_mcp_server.py +604 -0
- package/src/mcp/requirements-mcp-core.txt +5 -0
- package/src/mcp/requirements-mcp.txt +5 -0
- package/src/plugin.ts +19 -0
- package/src/plugins/tui/dist/tui.js +143 -0
- package/src/plugins/tui/dist/tui.tsx +144 -0
- package/src/plugins/tui/package.json +32 -0
- package/src/plugins/tui/src/index.tsx +144 -0
- package/src/plugins/tui/tsdown.config.ts +22 -0
- package/src/routing.yml +499 -0
- package/src/skills/README.md +230 -0
- package/src/skills/agent-coordination/SKILL.md +95 -0
- package/src/skills/artifact-management/SKILL.md +118 -0
- package/src/skills/auto-continue/SKILL.md +280 -0
- package/src/skills/code-review-checklist/SKILL.md +139 -0
- package/src/skills/context-compression/SKILL.md +861 -0
- package/src/skills/git-workflow-and-versioning/SKILL.md +32 -0
- package/src/skills/incremental-implementation/SKILL.md +27 -0
- package/src/skills/memory-bank/SKILL.md +165 -0
- package/src/skills/orchestration-workflow/SKILL.md +311 -0
- package/src/skills/security-hardening/SKILL.md +36 -0
- package/src/skills/session-goal/SKILL.md +138 -0
- package/src/skills/spec-driven-development/SKILL.md +23 -0
- package/src/skills/tdd-with-agents/SKILL.md +170 -0
- 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
|
+
}
|