open-xmen 0.1.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 (60) hide show
  1. package/.cerebro/.gitignore +27 -0
  2. package/.cerebro/cerebro-identity.md +76 -0
  3. package/.cerebro/docs/agent-mapping.md +54 -0
  4. package/.cerebro/docs/cerebro-workflow.md +115 -0
  5. package/.cerebro/docs/orchestration.md +28 -0
  6. package/.cerebro/docs/overview.md +44 -0
  7. package/.cerebro/docs/skill-policy.md +25 -0
  8. package/.cerebro/integrations/semble.md +30 -0
  9. package/.cerebro/opencode/model-routing.md +37 -0
  10. package/.cerebro/schemas/boulder.schema.json +143 -0
  11. package/.cerebro/schemas/team-run.schema.json +234 -0
  12. package/.cerebro/schemas/upgrade-manifest.schema.json +45 -0
  13. package/.cerebro/schemas/upgrade-state.schema.json +38 -0
  14. package/.cerebro/scripts/check-agent-teams-enabled.py +24 -0
  15. package/.cerebro/scripts/ensure-upgrade-cache-gitignored.py +27 -0
  16. package/.cerebro/scripts/fetch-upstream-ref.py +67 -0
  17. package/.cerebro/scripts/reset-runtime.py +125 -0
  18. package/.cerebro/scripts/setup-status.py +101 -0
  19. package/.cerebro/scripts/test-stop-hook.py +60 -0
  20. package/.cerebro/scripts/upgrade-latest-tag.py +34 -0
  21. package/.cerebro/scripts/validate-agent-frontmatter.py +87 -0
  22. package/.cerebro/scripts/validate-boulder.py +105 -0
  23. package/.cerebro/scripts/validate-opencode-runtime.py +94 -0
  24. package/.cerebro/scripts/validate-team-runs.py +310 -0
  25. package/.cerebro/scripts/validate-upgrade-metadata.py +104 -0
  26. package/.cerebro/scripts/write-upgrade-state.py +93 -0
  27. package/.cerebro/templates/customer-vision.md +58 -0
  28. package/.cerebro/templates/plan.md +35 -0
  29. package/.cerebro/templates/product-brief.md +110 -0
  30. package/.cerebro/templates/project-context.md +64 -0
  31. package/.cerebro/templates/requirements-brief.md +67 -0
  32. package/.cerebro/templates/team-run.json +22 -0
  33. package/.cerebro/upgrade-manifest.json +160 -0
  34. package/.opencode/.gitignore +5 -0
  35. package/.opencode/agents/beast.md +38 -0
  36. package/.opencode/agents/cerebro.md +22 -0
  37. package/.opencode/agents/cyclops.md +22 -0
  38. package/.opencode/agents/cypher.md +46 -0
  39. package/.opencode/agents/emma-frost.md +38 -0
  40. package/.opencode/agents/forge.md +22 -0
  41. package/.opencode/agents/legion.md +45 -0
  42. package/.opencode/agents/nightcrawler.md +22 -0
  43. package/.opencode/agents/professor-x.md +39 -0
  44. package/.opencode/agents/sage.md +22 -0
  45. package/.opencode/agents/storm.md +49 -0
  46. package/.opencode/agents/wolverine.md +22 -0
  47. package/.opencode/commands/cerebro-doctor.md +19 -0
  48. package/.opencode/commands/cerebro-index.md +22 -0
  49. package/.opencode/commands/cerebro-plan.md +21 -0
  50. package/.opencode/commands/cerebro-reset.md +20 -0
  51. package/.opencode/commands/cerebro-start-work.md +20 -0
  52. package/.opencode/commands/cerebro-upgrade.md +19 -0
  53. package/.opencode/commands/to-me-my-x-men.md +27 -0
  54. package/AGENTS.md +12 -0
  55. package/README.md +193 -0
  56. package/dist/cli.d.ts +2 -0
  57. package/dist/cli.js +597 -0
  58. package/dist/index.d.ts +4 -0
  59. package/dist/index.js +466 -0
  60. package/package.json +54 -0
package/dist/cli.js ADDED
@@ -0,0 +1,597 @@
1
+ #!/usr/bin/env node
2
+ import { copyFileSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
3
+ import { spawnSync } from "node:child_process";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ const REQUIRED_AGENTS = [
7
+ "cerebro",
8
+ "legion",
9
+ "cypher",
10
+ "professor-x",
11
+ "cyclops",
12
+ "wolverine",
13
+ "storm",
14
+ "forge",
15
+ "nightcrawler",
16
+ "sage",
17
+ "beast",
18
+ "emma-frost",
19
+ ];
20
+ const REQUIRED_COMMANDS = [
21
+ "cerebro-index",
22
+ "cerebro-plan",
23
+ "cerebro-start-work",
24
+ "cerebro-doctor",
25
+ "cerebro-reset",
26
+ "cerebro-upgrade",
27
+ "to-me-my-x-men",
28
+ ];
29
+ const OPEN_XMEN_AGENT_BLOCK_START = "<!-- OPEN-XMEN:START -->";
30
+ const OPEN_XMEN_AGENT_BLOCK_END = "<!-- OPEN-XMEN:END -->";
31
+ const PACKAGE_NAME = "open-xmen";
32
+ const OPENCODE_INSTRUCTIONS = ["AGENTS.md", ".cerebro/cerebro-identity.md", ".cerebro/opencode/model-routing.md"];
33
+ const SKIP_COPY_NAMES = new Set(["node_modules", "__pycache__"]);
34
+ const DENY_COPY_PATTERN = /(^|[/\\])(?:\.env(?:\.|$)|.*(?:secret|credential|token|private[-_]?key).*)/i;
35
+ function main() {
36
+ const args = process.argv.slice(2);
37
+ const command = args[0];
38
+ if (!command)
39
+ return install([]);
40
+ if (command === "install")
41
+ return install(args.slice(1));
42
+ if (command === "doctor")
43
+ return doctor(args.slice(1));
44
+ if (command === "models")
45
+ return models();
46
+ if (command === "--help" || command === "-h") {
47
+ printHelp();
48
+ return 0;
49
+ }
50
+ console.error(`Unknown command: ${command}`);
51
+ console.error("Run `open-xmen --help` for usage information.");
52
+ return 1;
53
+ }
54
+ function printHelp() {
55
+ console.log(`Open X-Men installer
56
+
57
+ Usage:
58
+ open-xmen [install] [OPTIONS]
59
+ open-xmen doctor [OPTIONS]
60
+ open-xmen models
61
+
62
+ Install options:
63
+ --dir <path> Project directory to install into (default: current directory)
64
+ --dry-run Print planned writes without changing files
65
+ --reset Refresh managed runtime/template files and replace existing Open X-Men blocks
66
+ --force Alias for overwrite behavior used by --reset
67
+ --no-deps Skip OpenCode plugin cache warm-up
68
+ -h, --help Show this help message
69
+
70
+ Doctor options:
71
+ --dir <path> Project directory to validate (default: current directory)
72
+ --json Print diagnostics as JSON
73
+ -h, --help Show this help message
74
+
75
+ Examples:
76
+ bunx open-xmen@latest install
77
+ bunx open-xmen@latest install --dir /path/to/project --dry-run
78
+ bunx open-xmen@latest install --reset --no-deps
79
+ open-xmen doctor --json
80
+ `);
81
+ }
82
+ function printInstallHelp() {
83
+ console.log(`Usage: open-xmen install [--dir <path>] [--dry-run] [--reset] [--force] [--no-deps]
84
+
85
+ By default, existing runtime/template files are skipped. Use --reset or --force to refresh them.
86
+ opencode.jsonc is updated atomically and an opencode.jsonc.bak backup is created when replacing an existing config.`);
87
+ }
88
+ function printDoctorHelp() {
89
+ console.log("Usage: open-xmen doctor [--dir <path>] [--json]");
90
+ }
91
+ function install(args) {
92
+ if (args.includes("--help") || args.includes("-h")) {
93
+ printInstallHelp();
94
+ return 0;
95
+ }
96
+ const unknown = args.find((arg, index) => {
97
+ if (!arg.startsWith("-"))
98
+ return false;
99
+ if (args[index - 1] === "--dir")
100
+ return false;
101
+ return !["--dir", "--dry-run", "--force", "--reset", "--no-deps"].includes(arg);
102
+ });
103
+ if (unknown) {
104
+ console.error(`Unknown install option: ${unknown}`);
105
+ return 1;
106
+ }
107
+ const targetArg = valueAfter(args, "--dir");
108
+ if (args.includes("--dir") && !targetArg) {
109
+ console.error("Missing value for --dir");
110
+ return 1;
111
+ }
112
+ const options = {
113
+ dryRun: args.includes("--dry-run"),
114
+ force: args.includes("--force"),
115
+ reset: args.includes("--reset"),
116
+ skipDeps: args.includes("--no-deps"),
117
+ target: path.resolve(targetArg || process.cwd()),
118
+ };
119
+ const overwrite = options.force || options.reset;
120
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
121
+ const planned = [];
122
+ const copyOptions = { dryRun: options.dryRun, overwrite, planned };
123
+ console.log(`Open X-Men ${options.dryRun ? "dry run" : "install"}`);
124
+ console.log(`Target: ${options.target}`);
125
+ if (overwrite)
126
+ console.log("Mode: refresh existing managed files (--reset/--force)");
127
+ else
128
+ console.log("Mode: safe install (existing files are skipped)");
129
+ if (!options.dryRun)
130
+ mkdirSync(options.target, { recursive: true });
131
+ else
132
+ planned.push(`ensure directory ${options.target}`);
133
+ copyAny(path.join(packageRoot, ".opencode/.gitignore"), path.join(options.target, ".opencode/.gitignore"), copyOptions);
134
+ copyTree(path.join(packageRoot, ".opencode/agents"), path.join(options.target, ".opencode/agents"), copyOptions);
135
+ copyTree(path.join(packageRoot, ".opencode/commands"), path.join(options.target, ".opencode/commands"), copyOptions);
136
+ copyCerebroRuntime(packageRoot, options.target, copyOptions);
137
+ installAgentInstructions(packageRoot, options.target, { dryRun: options.dryRun, overwrite, planned });
138
+ updateOpencodeConfig(options.target, { dryRun: options.dryRun, planned });
139
+ if (options.dryRun) {
140
+ console.log("\nPlanned actions:");
141
+ for (const action of planned)
142
+ console.log(`- ${action}`);
143
+ console.log("\nopen-xmen install: DRY RUN PASS");
144
+ return 0;
145
+ }
146
+ if (!options.skipDeps)
147
+ warmOpenCodePluginCache(packageRoot);
148
+ else
149
+ console.log("Skipped OpenCode plugin cache warm-up (--no-deps)");
150
+ console.log("open-xmen install: PASS");
151
+ console.log(`Installed into ${options.target}`);
152
+ console.log("Next: run `opencode .`, then use `/cerebro-index`, `/cerebro-plan`, or `/to-me-my-x-men`.");
153
+ return 0;
154
+ }
155
+ function doctor(args = []) {
156
+ if (args.includes("--help") || args.includes("-h")) {
157
+ printDoctorHelp();
158
+ return 0;
159
+ }
160
+ const json = args.includes("--json");
161
+ const targetArg = valueAfter(args, "--dir");
162
+ const unknown = args.find((arg, index) => {
163
+ if (!arg.startsWith("-"))
164
+ return false;
165
+ if (args[index - 1] === "--dir")
166
+ return false;
167
+ return !["--dir", "--json"].includes(arg);
168
+ });
169
+ if (unknown) {
170
+ if (json)
171
+ console.log(JSON.stringify({ ok: false, error: `Unknown doctor option: ${unknown}` }, null, 2));
172
+ else
173
+ console.error(`Unknown doctor option: ${unknown}`);
174
+ return 1;
175
+ }
176
+ if (args.includes("--dir") && !targetArg) {
177
+ if (json)
178
+ console.log(JSON.stringify({ ok: false, error: "Missing value for --dir" }, null, 2));
179
+ else
180
+ console.error("Missing value for --dir");
181
+ return 1;
182
+ }
183
+ const cwd = path.resolve(targetArg || process.cwd());
184
+ const result = runDoctor(cwd);
185
+ if (json) {
186
+ console.log(JSON.stringify(result, null, 2));
187
+ return result.ok ? 0 : 1;
188
+ }
189
+ if (!result.ok) {
190
+ console.error("Cerebro OpenCode doctor: FAIL");
191
+ for (const error of result.errors)
192
+ console.error(`- ${error}`);
193
+ return 1;
194
+ }
195
+ console.log("Cerebro OpenCode doctor: PASS");
196
+ console.log(`Agents: ${result.agents}`);
197
+ console.log(`Commands: ${result.commands}`);
198
+ return 0;
199
+ }
200
+ function runDoctor(cwd) {
201
+ const errors = [];
202
+ const exists = (file) => existsSync(path.join(cwd, file));
203
+ const read = (file) => readFileSync(path.join(cwd, file), "utf8");
204
+ for (const file of ["opencode.jsonc", "AGENTS.md", ".cerebro/cerebro-identity.md"]) {
205
+ if (!exists(file))
206
+ errors.push(`missing ${file}`);
207
+ }
208
+ if (!opencodeConfigHasOpenXmenPlugin(cwd)) {
209
+ errors.push("opencode.jsonc does not include the open-xmen plugin entry");
210
+ }
211
+ for (const name of REQUIRED_AGENTS) {
212
+ const file = `.opencode/agents/${name}.md`;
213
+ if (!exists(file))
214
+ errors.push(`missing ${file}`);
215
+ else if (!read(file).startsWith("---\n"))
216
+ errors.push(`${file} missing frontmatter`);
217
+ }
218
+ for (const name of REQUIRED_COMMANDS) {
219
+ const file = `.opencode/commands/${name}.md`;
220
+ if (!exists(file))
221
+ errors.push(`missing ${file}`);
222
+ else if (!read(file).includes("agent: cerebro"))
223
+ errors.push(`${file} should run with agent: cerebro`);
224
+ }
225
+ for (const dir of [".cerebro/plans", ".cerebro/notepads", ".cerebro/team-runs"]) {
226
+ if (!exists(dir))
227
+ errors.push(`missing ${dir}`);
228
+ }
229
+ const opencode = spawnSync("opencode", ["--version"], { encoding: "utf8" });
230
+ if (opencode.status !== 0)
231
+ errors.push("opencode executable not found or not working");
232
+ let agents = 0;
233
+ let commands = 0;
234
+ agents = countMarkdownFiles(path.join(cwd, ".opencode/agents"));
235
+ commands = countMarkdownFiles(path.join(cwd, ".opencode/commands"));
236
+ return { ok: errors.length === 0, cwd, errors, agents, commands };
237
+ }
238
+ function models() {
239
+ console.log(JSON.stringify({
240
+ frontier: process.env.CEREBRO_MODEL_FRONTIER || "openai/gpt-5.4",
241
+ strong: process.env.CEREBRO_MODEL_STRONG || "openai/gpt-5.4",
242
+ coding: process.env.CEREBRO_MODEL_CODING || "openai/gpt-5.3-codex",
243
+ spark: process.env.CEREBRO_MODEL_SPARK || "openai/gpt-5.3-codex-spark",
244
+ fast: process.env.CEREBRO_MODEL_FAST || "openai/gpt-5.4-mini",
245
+ image: process.env.CEREBRO_MODEL_IMAGE || "openai/gpt-image-2",
246
+ }, null, 2));
247
+ return 0;
248
+ }
249
+ function copyCerebroRuntime(packageRoot, target, opts) {
250
+ const root = path.join(packageRoot, ".cerebro");
251
+ const entries = [
252
+ ".gitignore",
253
+ "cerebro-identity.md",
254
+ "docs",
255
+ "integrations",
256
+ "opencode",
257
+ "schemas",
258
+ "scripts",
259
+ "templates",
260
+ "upgrade-manifest.json",
261
+ ];
262
+ for (const entry of entries) {
263
+ copyAny(path.join(root, entry), path.join(target, ".cerebro", entry), opts);
264
+ }
265
+ for (const dir of ["plans", "notepads", "team-runs", "pending-todos"]) {
266
+ const targetDir = path.join(target, ".cerebro", dir);
267
+ const gitkeep = path.join(targetDir, ".gitkeep");
268
+ if (opts.dryRun) {
269
+ opts.planned.push(`ensure directory ${targetDir}`);
270
+ if (!existsSync(gitkeep))
271
+ opts.planned.push(`create ${gitkeep}`);
272
+ continue;
273
+ }
274
+ mkdirSync(targetDir, { recursive: true });
275
+ if (!existsSync(gitkeep))
276
+ writeFileSync(gitkeep, "", "utf8");
277
+ }
278
+ }
279
+ function installAgentInstructions(packageRoot, target, opts) {
280
+ const source = readFileSync(path.join(packageRoot, "AGENTS.md"), "utf8").trim();
281
+ const block = `${OPEN_XMEN_AGENT_BLOCK_START}\n${source}\n${OPEN_XMEN_AGENT_BLOCK_END}`;
282
+ const destination = path.join(target, "AGENTS.md");
283
+ if (!existsSync(destination)) {
284
+ if (opts.dryRun)
285
+ opts.planned.push(`create ${destination}`);
286
+ else
287
+ writeFileSync(destination, `${block}\n`, "utf8");
288
+ return;
289
+ }
290
+ const current = readFileSync(destination, "utf8");
291
+ const start = current.indexOf(OPEN_XMEN_AGENT_BLOCK_START);
292
+ const end = current.indexOf(OPEN_XMEN_AGENT_BLOCK_END);
293
+ if (start !== -1 && end !== -1 && end > start) {
294
+ if (!opts.overwrite) {
295
+ opts.planned.push(`skip existing Open X-Men AGENTS.md block ${destination}`);
296
+ return;
297
+ }
298
+ const next = `${current.slice(0, start)}${block}${current.slice(end + OPEN_XMEN_AGENT_BLOCK_END.length)}`;
299
+ if (next === current || `${next}\n` === current) {
300
+ opts.planned.push(`unchanged ${destination}`);
301
+ return;
302
+ }
303
+ if (opts.dryRun)
304
+ opts.planned.push(`refresh Open X-Men AGENTS.md block ${destination}`);
305
+ else
306
+ writeFileSync(destination, next.endsWith("\n") ? next : `${next}\n`, "utf8");
307
+ return;
308
+ }
309
+ if (opts.dryRun)
310
+ opts.planned.push(`append Open X-Men AGENTS.md block ${destination}`);
311
+ else
312
+ writeFileSync(destination, `${current.trimEnd()}\n\n${block}\n`, "utf8");
313
+ }
314
+ function opencodeConfigHasOpenXmenPlugin(cwd = process.cwd()) {
315
+ const configPath = path.join(cwd, "opencode.jsonc");
316
+ if (!existsSync(configPath))
317
+ return false;
318
+ try {
319
+ const config = parseJsonc(readFileSync(configPath, "utf8"));
320
+ return asArray(config.plugin).some((entry) => {
321
+ const spec = Array.isArray(entry) ? entry[0] : entry;
322
+ return typeof spec === "string" && isOpenXmenPluginEntry(spec);
323
+ });
324
+ }
325
+ catch {
326
+ return false;
327
+ }
328
+ }
329
+ function getPluginEntry() {
330
+ const cliEntryPath = process.argv[1];
331
+ if (!cliEntryPath)
332
+ return PACKAGE_NAME;
333
+ const packageRoot = findPackageRoot(cliEntryPath);
334
+ if (!packageRoot || isPackageManagerInstall(packageRoot))
335
+ return PACKAGE_NAME;
336
+ return packageRoot;
337
+ }
338
+ function replaceOpenXmenPluginEntries(items, pluginEntry) {
339
+ return [...items.filter((entry) => {
340
+ const spec = Array.isArray(entry) ? entry[0] : entry;
341
+ return !(typeof spec === "string" && isOpenXmenPluginEntry(spec));
342
+ }), pluginEntry];
343
+ }
344
+ function isOpenXmenPluginEntry(entry) {
345
+ return entry === PACKAGE_NAME || entry.startsWith(`${PACKAGE_NAME}@`) || entry.endsWith(".opencode/plugins/open-xmen.ts") || isLocalPackageRootEntry(entry);
346
+ }
347
+ function findPackageRoot(startPath) {
348
+ let current = path.dirname(startPath);
349
+ while (true) {
350
+ const packageJsonPath = path.join(current, "package.json");
351
+ if (existsSync(packageJsonPath)) {
352
+ const packageJson = readJsonFile(packageJsonPath);
353
+ if (isRecord(packageJson) && packageJson.name === PACKAGE_NAME)
354
+ return current;
355
+ }
356
+ const parent = path.dirname(current);
357
+ if (parent === current)
358
+ return null;
359
+ current = parent;
360
+ }
361
+ }
362
+ function isLocalPackageRootEntry(entry) {
363
+ if (!entry || entry.startsWith("file://"))
364
+ return false;
365
+ const packageJsonPath = path.join(entry, "package.json");
366
+ if (!existsSync(packageJsonPath))
367
+ return false;
368
+ const packageJson = readJsonFile(packageJsonPath);
369
+ return isRecord(packageJson) && packageJson.name === PACKAGE_NAME;
370
+ }
371
+ function isPackageManagerInstall(packageRoot) {
372
+ return packageRoot.replaceAll("\\", "/").includes(`/node_modules/${PACKAGE_NAME}`);
373
+ }
374
+ function warmOpenCodePluginCache(packageRoot) {
375
+ if (!isPackageManagerInstall(packageRoot)) {
376
+ console.log("Local development install - cache warm-up not required");
377
+ return;
378
+ }
379
+ const home = process.env.HOME;
380
+ if (!home)
381
+ return;
382
+ let version = "latest";
383
+ const packageJson = readJsonFile(path.join(packageRoot, "package.json"));
384
+ if (isRecord(packageJson) && typeof packageJson.version === "string")
385
+ version = packageJson.version;
386
+ const cacheRoot = process.env.XDG_CACHE_HOME || path.join(home, ".cache");
387
+ const cacheDir = path.join(cacheRoot, "opencode", "packages", `${PACKAGE_NAME}@${version}`);
388
+ mkdirSync(cacheDir, { recursive: true });
389
+ writeFileSync(path.join(cacheDir, "package.json"), `${JSON.stringify({ name: `${PACKAGE_NAME}-cache`, private: true, dependencies: { [PACKAGE_NAME]: version } }, null, 2)}\n`, "utf8");
390
+ const bun = spawnSync("bun", ["install", "--ignore-scripts"], { cwd: cacheDir, stdio: "inherit" });
391
+ if (bun.status !== 0)
392
+ console.warn("open-xmen install: skipped OpenCode plugin cache warm-up; bun install failed");
393
+ else
394
+ console.log(`OpenCode plugin cache warmed: ${cacheDir}`);
395
+ }
396
+ function updateOpencodeConfig(target, opts) {
397
+ const destination = path.join(target, "opencode.jsonc");
398
+ const parsed = existsSync(destination) ? parseJsonc(readFileSync(destination, "utf8")) : {};
399
+ const config = isRecord(parsed) ? parsed : {};
400
+ config.$schema ||= "https://opencode.ai/config.json";
401
+ config.plugin = replaceOpenXmenPluginEntries(asArray(config.plugin), getPluginEntry());
402
+ config.instructions = appendUnique(asArray(config.instructions), ...OPENCODE_INSTRUCTIONS);
403
+ config.share ??= "disabled";
404
+ config.permission ??= { edit: "ask", bash: "ask", webfetch: "ask" };
405
+ const content = `${JSON.stringify(config, null, 2)}\n`;
406
+ if (opts.dryRun) {
407
+ if (existsSync(destination))
408
+ opts.planned.push(`atomically update ${destination} (backup ${destination}.bak)`);
409
+ else
410
+ opts.planned.push(`create ${destination}`);
411
+ return;
412
+ }
413
+ writeAtomicConfig(destination, content);
414
+ }
415
+ function writeAtomicConfig(destination, content) {
416
+ mkdirSync(path.dirname(destination), { recursive: true });
417
+ if (existsSync(destination)) {
418
+ const current = readFileSync(destination, "utf8");
419
+ if (current === content)
420
+ return;
421
+ copyFileSync(destination, `${destination}.bak`);
422
+ }
423
+ const temp = `${destination}.tmp`;
424
+ writeFileSync(temp, content, "utf8");
425
+ renameSync(temp, destination);
426
+ }
427
+ function copyTree(source, destination, opts) {
428
+ copyAny(source, destination, opts);
429
+ }
430
+ function copyAny(source, destination, opts) {
431
+ if (!existsSync(source))
432
+ return;
433
+ if (isDeniedRuntimePath(source) || isDeniedRuntimePath(destination))
434
+ return;
435
+ const link = lstatSync(source);
436
+ if (link.isSymbolicLink())
437
+ return;
438
+ const stat = statSync(source);
439
+ if (stat.isDirectory()) {
440
+ if (SKIP_COPY_NAMES.has(path.basename(source)))
441
+ return;
442
+ assertContained(source, path.join(source, "placeholder"));
443
+ if (opts.dryRun)
444
+ opts.planned.push(`ensure directory ${destination}`);
445
+ else
446
+ mkdirSync(destination, { recursive: true });
447
+ for (const item of readdirSync(source)) {
448
+ copyAny(path.join(source, item), path.join(destination, item), opts);
449
+ }
450
+ return;
451
+ }
452
+ if (!stat.isFile())
453
+ return;
454
+ if (source.endsWith(".pyc"))
455
+ return;
456
+ assertContained(path.dirname(source), source);
457
+ if (existsSync(destination) && !opts.overwrite) {
458
+ opts.planned.push(`skip existing ${destination}`);
459
+ return;
460
+ }
461
+ if (opts.dryRun) {
462
+ opts.planned.push(`${existsSync(destination) ? "overwrite" : "copy"} ${destination}`);
463
+ return;
464
+ }
465
+ mkdirSync(path.dirname(destination), { recursive: true });
466
+ writeFileSync(destination, readFileSync(source));
467
+ }
468
+ function countMarkdownFiles(directory) {
469
+ if (!existsSync(directory))
470
+ return 0;
471
+ return readdirSync(directory).filter((file) => file.endsWith(".md")).length;
472
+ }
473
+ function readJsonFile(file) {
474
+ if (!existsSync(file))
475
+ return undefined;
476
+ try {
477
+ return JSON.parse(readFileSync(file, "utf8"));
478
+ }
479
+ catch (error) {
480
+ if (error instanceof SyntaxError)
481
+ return undefined;
482
+ throw error;
483
+ }
484
+ }
485
+ function isRecord(value) {
486
+ return typeof value === "object" && value !== null && !Array.isArray(value);
487
+ }
488
+ function isDeniedRuntimePath(filePath) {
489
+ return DENY_COPY_PATTERN.test(filePath.replaceAll("\\", "/"));
490
+ }
491
+ function assertContained(root, candidate) {
492
+ const resolvedRoot = realpathSync(root);
493
+ const resolvedCandidate = existsSync(candidate) ? realpathSync(candidate) : path.resolve(candidate);
494
+ const relative = path.relative(resolvedRoot, resolvedCandidate);
495
+ if (relative.startsWith("..") || path.isAbsolute(relative))
496
+ throw new Error(`copy path escapes source root: ${candidate}`);
497
+ }
498
+ function parseJsonc(text) {
499
+ const stripped = removeTrailingCommas(stripJsonComments(text));
500
+ return stripped.trim() ? JSON.parse(stripped) : {};
501
+ }
502
+ function stripJsonComments(text) {
503
+ let out = "";
504
+ let inString = false;
505
+ let escape = false;
506
+ for (let i = 0; i < text.length; i++) {
507
+ const ch = text[i];
508
+ const next = text[i + 1];
509
+ if (inString) {
510
+ out += ch;
511
+ if (escape)
512
+ escape = false;
513
+ else if (ch === "\\")
514
+ escape = true;
515
+ else if (ch === '"')
516
+ inString = false;
517
+ continue;
518
+ }
519
+ if (ch === '"') {
520
+ inString = true;
521
+ out += ch;
522
+ continue;
523
+ }
524
+ if (ch === "/" && next === "/") {
525
+ while (i < text.length && text[i] !== "\n")
526
+ i++;
527
+ out += "\n";
528
+ continue;
529
+ }
530
+ if (ch === "/" && next === "*") {
531
+ i += 2;
532
+ while (i < text.length && !(text[i] === "*" && text[i + 1] === "/"))
533
+ i++;
534
+ i++;
535
+ continue;
536
+ }
537
+ out += ch;
538
+ }
539
+ return out;
540
+ }
541
+ function removeTrailingCommas(text) {
542
+ let out = "";
543
+ let inString = false;
544
+ let escape = false;
545
+ for (let i = 0; i < text.length; i++) {
546
+ const ch = text[i];
547
+ if (inString) {
548
+ out += ch;
549
+ if (escape)
550
+ escape = false;
551
+ else if (ch === "\\")
552
+ escape = true;
553
+ else if (ch === '"')
554
+ inString = false;
555
+ continue;
556
+ }
557
+ if (ch === '"') {
558
+ inString = true;
559
+ out += ch;
560
+ continue;
561
+ }
562
+ if (ch === ",") {
563
+ let j = i + 1;
564
+ while (/\s/.test(text[j] || ""))
565
+ j++;
566
+ if (text[j] === "}" || text[j] === "]")
567
+ continue;
568
+ }
569
+ out += ch;
570
+ }
571
+ return out;
572
+ }
573
+ function appendUnique(items, ...values) {
574
+ const result = [...items];
575
+ for (const value of values) {
576
+ if (!result.includes(value))
577
+ result.push(value);
578
+ }
579
+ return result;
580
+ }
581
+ function asArray(value) {
582
+ return Array.isArray(value) ? value.filter(isJsonValue) : [];
583
+ }
584
+ function isJsonValue(value) {
585
+ if (value === null)
586
+ return true;
587
+ if (["boolean", "number", "string"].includes(typeof value))
588
+ return true;
589
+ if (Array.isArray(value))
590
+ return value.every(isJsonValue);
591
+ return isRecord(value) && Object.values(value).every(isJsonValue);
592
+ }
593
+ function valueAfter(args, flag) {
594
+ const index = args.indexOf(flag);
595
+ return index === -1 ? undefined : args[index + 1];
596
+ }
597
+ process.exitCode = main();
@@ -0,0 +1,4 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ export declare const CerebroPlugin: Plugin;
3
+ export declare const server: Plugin;
4
+ export default CerebroPlugin;