agent-enderun 0.0.1

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/bin/cli.js ADDED
@@ -0,0 +1,863 @@
1
+ #!/usr/bin/env node
2
+ console.log("🤖 Agent Enderun CLI — Initializing...");
3
+
4
+ import fs from "fs";
5
+ import path from "path";
6
+ import { fileURLToPath } from "url";
7
+ import crypto from "crypto";
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+ const sourceDir = path.join(__dirname, "..");
12
+ const targetDir = process.cwd();
13
+
14
+ // --- CONSTANTS ---
15
+ const FRAMEWORK_VERSION = getPackageVersion();
16
+
17
+ // --- HELPER FUNCTIONS ---
18
+
19
+ function getPackageVersion() {
20
+ const pkg = JSON.parse(fs.readFileSync(path.join(sourceDir, "package.json"), "utf8"));
21
+ return pkg.version;
22
+ }
23
+
24
+ function getFrameworkDir() {
25
+ const adapters = [".gemini", ".claude", ".cursor", ".codex", ".enderun"];
26
+ for (const adp of adapters) {
27
+ const fullPath = path.join(targetDir, adp);
28
+ if (fs.existsSync(fullPath) && fs.lstatSync(fullPath).isDirectory()) {
29
+ return adp;
30
+ }
31
+ }
32
+ return ".enderun";
33
+ }
34
+
35
+ function getMemoryPath() {
36
+ return path.join(targetDir, getFrameworkDir(), "PROJECT_MEMORY.md");
37
+ }
38
+
39
+ function generateULID(seedTime = Date.now()) {
40
+ const ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
41
+ const ENCODING_LEN = ENCODING.length;
42
+ let time = seedTime;
43
+ const timeChars = new Array(10);
44
+ for (let i = 9; i >= 0; i--) {
45
+ timeChars[i] = ENCODING.charAt(time % ENCODING_LEN);
46
+ time = Math.floor(time / ENCODING_LEN);
47
+ }
48
+ const randomChars = new Array(16);
49
+ for (let i = 0; i < 16; i++) {
50
+ randomChars[i] = ENCODING.charAt(Math.floor(Math.random() * ENCODING_LEN));
51
+ }
52
+ return timeChars.join("") + randomChars.join("");
53
+ }
54
+
55
+ function sleep(ms) {
56
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
57
+ }
58
+
59
+ function acquireMemoryLock(lockPath, maxRetries = 5) {
60
+ for (let attempt = 0; attempt < maxRetries; attempt += 1) {
61
+ try {
62
+ const fd = fs.openSync(lockPath, "wx");
63
+ fs.closeSync(fd);
64
+ return true;
65
+ } catch (error) {
66
+ if (error?.code !== "EEXIST") throw error;
67
+ if (attempt < maxRetries - 1) sleep(1000);
68
+ }
69
+ }
70
+ return false;
71
+ }
72
+
73
+ function releaseMemoryLock(lockPath) {
74
+ if (fs.existsSync(lockPath)) fs.unlinkSync(lockPath);
75
+ }
76
+
77
+ function insertTaskRow(memoryContent, row) {
78
+ const sectionHeader = "## ACTIVE TASKS";
79
+ const tableDivider = "| :--- | :--- | :--- | :--- | :--- |";
80
+ const sectionIndex = memoryContent.indexOf(sectionHeader);
81
+ if (sectionIndex === -1) return null;
82
+ const dividerIndex = memoryContent.indexOf(tableDivider, sectionIndex);
83
+ if (dividerIndex === -1) return null;
84
+ const dividerLineEnd = memoryContent.indexOf("\n", dividerIndex);
85
+ if (dividerLineEnd === -1) return null;
86
+
87
+ return (
88
+ memoryContent.slice(0, dividerLineEnd + 1) +
89
+ `${row}\n` +
90
+ memoryContent.slice(dividerLineEnd + 1)
91
+ );
92
+ }
93
+
94
+ function sanitizeTableCell(value) {
95
+ return String(value).replace(/\|/g, "\\|").replace(/\r?\n/g, " ").trim();
96
+ }
97
+
98
+ function normalizeAgentName(agent) {
99
+ return String(agent || "manager").replace(/^@+/, "").trim() || "manager";
100
+ }
101
+
102
+ function normalizePriority(priority) {
103
+ const normalized = String(priority || "P2").toUpperCase().trim();
104
+ return /^P[0-3]$/.test(normalized) ? normalized : "P2";
105
+ }
106
+
107
+ function mergePackageJson(targetPath, sourcePath) {
108
+ let targetPkg = {};
109
+ if (fs.existsSync(targetPath)) {
110
+ try {
111
+ targetPkg = JSON.parse(fs.readFileSync(targetPath, "utf8"));
112
+ } catch (e) {
113
+ console.warn("⚠️ Could not parse existing package.json, creating a new one.");
114
+ }
115
+ }
116
+
117
+ const sourcePkg = JSON.parse(fs.readFileSync(sourcePath, "utf8"));
118
+
119
+ const sanitizeDeps = (deps) => {
120
+ if (!deps) return deps;
121
+ const cleaned = {};
122
+ for (const [name, version] of Object.entries(deps)) {
123
+ cleaned[name] = (typeof version === "string" && version.startsWith("workspace:")) ? "*" : version;
124
+ }
125
+ return cleaned;
126
+ };
127
+
128
+ targetPkg.dependencies = sanitizeDeps({
129
+ ...targetPkg.dependencies,
130
+ ...sourcePkg.dependencies
131
+ });
132
+
133
+ // Merge scripts
134
+ targetPkg.scripts = {
135
+ ...targetPkg.scripts,
136
+ "enderun:status": "agent-enderun status",
137
+ "enderun:trace": "agent-enderun trace:new",
138
+ "enderun:verify": "agent-enderun verify-contract",
139
+ "enderun:build": "npm run build --prefix packages/shared-types && npm run build --prefix packages/framework-mcp",
140
+ };
141
+
142
+ targetPkg.devDependencies = sanitizeDeps({
143
+ ...targetPkg.devDependencies,
144
+ "@modelcontextprotocol/sdk": sourcePkg.devDependencies["@modelcontextprotocol/sdk"],
145
+ "zod": sourcePkg.devDependencies["zod"],
146
+ "ts-morph": sourcePkg.devDependencies["ts-morph"],
147
+ "typescript": sourcePkg.devDependencies["typescript"],
148
+ "@types/node": sourcePkg.devDependencies["@types/node"],
149
+ "tsx": sourcePkg.devDependencies["tsx"]
150
+ });
151
+
152
+ if (targetPkg.peerDependencies) targetPkg.peerDependencies = sanitizeDeps(targetPkg.peerDependencies);
153
+ if (targetPkg.optionalDependencies) targetPkg.optionalDependencies = sanitizeDeps(targetPkg.optionalDependencies);
154
+
155
+ // Ensure basic fields
156
+ if (!targetPkg.name) targetPkg.name = path.basename(process.cwd());
157
+ if (!targetPkg.version) targetPkg.version = "0.1.0";
158
+ if (!targetPkg.type) targetPkg.type = "module";
159
+
160
+ // Add workspaces if it's a new project or doesn't have them
161
+ if (!targetPkg.workspaces && !fs.existsSync(path.join(process.cwd(), "pnpm-workspace.yaml"))) {
162
+ targetPkg.workspaces = ["packages/*"];
163
+ }
164
+
165
+ // Add metadata
166
+ targetPkg.enderun = {
167
+ version: sourcePkg.version,
168
+ initializedAt: new Date().toISOString(),
169
+ };
170
+
171
+ fs.writeFileSync(targetPath, JSON.stringify(targetPkg, null, 2));
172
+ console.log("✅ package.json updated with Enderun scripts and dependencies.");
173
+ }
174
+
175
+ function updateGitIgnore(targetPath, frameworkDir = ".enderun") {
176
+ const IGNORE_LINES = [
177
+ "# AI-Enderun",
178
+ ".gemini/logs/*.json",
179
+ ".claude/logs/*.json",
180
+ ".cursor/logs/*.json",
181
+ ".codex/logs/*.json",
182
+ ".enderun/logs/*.json",
183
+ ".gemini/*.lock",
184
+ ".claude/*.lock",
185
+ ".cursor/*.lock",
186
+ ".codex/*.lock",
187
+ ".enderun/*.lock",
188
+ ".env",
189
+ ".DS_Store"
190
+ ];
191
+
192
+ let content = "";
193
+ if (fs.existsSync(targetPath)) {
194
+ content = fs.readFileSync(targetPath, "utf8");
195
+ }
196
+
197
+ const lines = content.split("\n").map((l) => l.trim());
198
+ let added = false;
199
+
200
+ for (const line of IGNORE_LINES) {
201
+ if (!lines.includes(line)) {
202
+ content += (content.endsWith("\n") || content === "" ? "" : "\n") + line + "\n";
203
+ added = true;
204
+ }
205
+ }
206
+
207
+ if (added) {
208
+ fs.writeFileSync(targetPath, content);
209
+ console.log("✅ .gitignore updated.");
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Create initial PROJECT_MEMORY.md if missing.
215
+ */
216
+ function initializeMemory(memoryPath, targetBase) {
217
+ if (fs.existsSync(memoryPath)) return;
218
+
219
+ const traceId = generateULID();
220
+ const date = new Date().toISOString().split("T")[0];
221
+ const template = `# PROJECT MEMORY — Agent Enderun
222
+
223
+ This file is the Single Source of Truth (SSOT) and the persistent memory of the project.
224
+
225
+ ## CURRENT STATUS
226
+
227
+ | Active Phase | Profile | Last Update | Active Trace ID | Blockers |
228
+ | :----------- | :------ | :---------- | :-------------- | :------- |
229
+ | PHASE_0 | Lightweight | ${date} | ${traceId} | NONE |
230
+
231
+ ## PROJECT DEFINITION
232
+
233
+ | Field | Value |
234
+ | :--- | :--- |
235
+ | Project Name | ${path.basename(process.cwd())} |
236
+ | Platform | Not defined |
237
+ | Frontend | React 19 + Vite + Panda CSS |
238
+ | Backend | Node.js 20+ + Fastify |
239
+ | DB | PostgreSQL |
240
+
241
+ ## DOD STATUS
242
+
243
+ | Phase | Status | Note |
244
+ | :--- | :--- | :--- |
245
+ | PHASE_0 | IN_PROGRESS | Initializing project structure |
246
+ | PHASE_1 | PENDING | |
247
+ | PHASE_2 | PENDING | |
248
+ | PHASE_3 | PENDING | |
249
+ | PHASE_4 | PENDING | |
250
+
251
+ ## CRITICAL DECISIONS
252
+
253
+ | Date | Decision | Rationale | Agent |
254
+ | :--- | :--- | :--- | :--- |
255
+ | ${date} | Project Initialized | Framework setup via CLI | @manager |
256
+
257
+ ## DELIVERABLES
258
+
259
+ | Module | Status | Agent | Date |
260
+ | :--- | :--- | :--- | :--- |
261
+
262
+ ## ACTIVE TASKS
263
+
264
+ | Trace ID | Task | Agent | Priority | Status |
265
+ | :--- | :--- | :--- | :--- | :--- |
266
+ | ${traceId} | Framework setup and architecture alignment | @manager | P1 | IN_PROGRESS |
267
+
268
+ ## HISTORY (Persistent Memory)
269
+
270
+ ### ${date} — Framework Initialization
271
+
272
+ - **Agent:** @manager
273
+ - **Trace ID:** ${traceId}
274
+ - **Action:** Initialized Agent Enderun framework and project structure.
275
+ `;
276
+
277
+ const finalTemplate = template.replace(/\{\{FRAMEWORK_DIR\}\}/g, targetBase);
278
+ fs.writeFileSync(memoryPath, finalTemplate);
279
+ console.log(`✅ PROJECT_MEMORY.md initialized in ${targetBase}`);
280
+ }
281
+
282
+ // --- COMMANDS ---
283
+
284
+ function getPackageManager() {
285
+ const override = process.env.AGENT_ENDERUN_PACKAGE_MANAGER || process.env.AGENT_ENDERUN_PM || process.env.AI_ENDERUN_PACKAGE_MANAGER || process.env.AI_ENDERUN_PM;
286
+ if (override) return override.toLowerCase();
287
+
288
+ const userAgent = process.env.npm_config_user_agent || "";
289
+ if (userAgent.includes("pnpm")) return "pnpm";
290
+ if (userAgent.includes("yarn")) return "yarn";
291
+ if (userAgent.includes("npm")) return "npm";
292
+
293
+ const npmExecPath = process.env.npm_execpath || "";
294
+ if (npmExecPath.includes("pnpm")) return "pnpm";
295
+ if (npmExecPath.includes("yarn")) return "yarn";
296
+ if (npmExecPath.includes("npm")) return "npm";
297
+
298
+ if (fs.existsSync(path.join(process.cwd(), "pnpm-lock.yaml"))) return "pnpm";
299
+ if (fs.existsSync(path.join(process.cwd(), "yarn.lock")) || fs.existsSync(path.join(process.cwd(), "yarn.lock.json"))) return "yarn";
300
+ return "npm";
301
+ }
302
+
303
+ /**
304
+ * Scaffold the framework into the target project.
305
+ */
306
+ async function initCommand(selectedAdapter) {
307
+ const ADAPTERS = {
308
+ gemini: ["GEMINI.md", "gemini-extension.json"],
309
+ claude: ["CLAUDE.md"],
310
+ cursor: ["CURSOR.md"],
311
+ codex: ["CODEX.md"],
312
+ };
313
+
314
+ const targetBase = selectedAdapter ? `.${selectedAdapter}` : ".enderun";
315
+ const targetFrameworkDir = path.join(targetDir, targetBase);
316
+
317
+ const CORE_FILES = [
318
+ ".enderun",
319
+ "docs",
320
+ "mcp.json",
321
+ ".env.example",
322
+ "ENDERUN.md",
323
+ "packages/framework-mcp",
324
+ "packages/shared-types",
325
+ ];
326
+
327
+ const DIRS_TO_CREATE = [
328
+ `${targetBase}/agents`,
329
+ `${targetBase}/docs/api`,
330
+ `${targetBase}/logs`,
331
+ "apps/web",
332
+ "apps/backend",
333
+ "docs",
334
+ "packages/shared-types",
335
+ "packages/framework-mcp",
336
+ ];
337
+
338
+ console.log("🚀 Installing Agent Enderun (Smart Mode)...");
339
+
340
+ // Ensure target framework base exists
341
+ if (!fs.existsSync(targetFrameworkDir)) {
342
+ fs.mkdirSync(targetFrameworkDir, { recursive: true });
343
+ }
344
+
345
+ // Create subdirectories
346
+ for (const dir of DIRS_TO_CREATE) {
347
+ const fullPath = path.join(targetDir, dir);
348
+ if (!fs.existsSync(fullPath)) {
349
+ fs.mkdirSync(fullPath, { recursive: true });
350
+ console.log(`📂 Created directory: ${dir}`);
351
+ }
352
+ }
353
+
354
+ let filesToProcess = [...CORE_FILES];
355
+
356
+ if (selectedAdapter) {
357
+ if (!ADAPTERS[selectedAdapter]) {
358
+ console.error(`❌ Invalid adapter: ${selectedAdapter}. Available: gemini, claude, cursor, codex`);
359
+ process.exit(1);
360
+ }
361
+ filesToProcess = [...CORE_FILES, ...ADAPTERS[selectedAdapter]];
362
+ } else {
363
+ Object.values(ADAPTERS).forEach(list => filesToProcess.push(...list));
364
+ }
365
+
366
+ for (const item of filesToProcess) {
367
+ const src = path.join(sourceDir, item);
368
+ let dest = path.join(targetDir, item);
369
+
370
+ // Remap core framework files to targetBase
371
+ if (item === ".enderun") dest = targetFrameworkDir;
372
+ if (item === "ENDERUN.md") dest = path.join(targetFrameworkDir, "ENDERUN.md");
373
+ if (ADAPTERS[selectedAdapter]?.includes(item)) {
374
+ dest = path.join(targetDir, item); // Keep adapter linker files in root
375
+ }
376
+
377
+ if (fs.existsSync(src)) {
378
+ if (fs.lstatSync(src).isDirectory()) {
379
+ // When copying framework dir, skip logs and project-specific state
380
+ const skipFiles = (item === ".enderun") ? ["logs", "PROJECT_MEMORY.md", "BRAIN_DASHBOARD.md", "PROJECT_MEMORY.lock"] : [];
381
+ const isDocs = item === "docs";
382
+ copyDir(src, dest, new Set(skipFiles), isDocs, targetBase);
383
+ } else {
384
+ // Special files handling
385
+ if (item === "package.json") continue;
386
+ if (item === "ENDERUN.md" && fs.existsSync(dest)) {
387
+ console.log(`ℹ️ Skipping ENDERUN.md (already exists in ${targetBase}).`);
388
+ continue;
389
+ }
390
+
391
+ if (fs.existsSync(dest)) {
392
+ console.log(`ℹ️ Skipping existing file: ${item}`);
393
+ continue;
394
+ }
395
+
396
+ const ext = path.extname(item);
397
+ const textExtensions = [".md", ".json", ".js", ".ts", ".txt", ""];
398
+ if (textExtensions.includes(ext)) {
399
+ let content = fs.readFileSync(src, "utf8");
400
+ content = content.replace(/\{\{FRAMEWORK_DIR\}\}/g, targetBase);
401
+ fs.writeFileSync(dest, content);
402
+ } else {
403
+ fs.copyFileSync(src, dest);
404
+ }
405
+ }
406
+ console.log(`✅ ${item} processed -> ${path.relative(targetDir, dest)}`);
407
+ }
408
+ }
409
+
410
+ // Smart setup
411
+ mergePackageJson(path.join(targetDir, "package.json"), path.join(sourceDir, "package.json"));
412
+ updateGitIgnore(path.join(targetDir, ".gitignore"), targetBase);
413
+
414
+ const finalMemoryPath = path.join(targetDir, targetBase, "PROJECT_MEMORY.md");
415
+ initializeMemory(finalMemoryPath, targetBase);
416
+
417
+ // Initialize git if missing
418
+ if (!fs.existsSync(path.join(targetDir, ".git"))) {
419
+ try {
420
+ const { execSync } = await import("child_process");
421
+ execSync("git init", { cwd: targetDir, stdio: "ignore" });
422
+ console.log("✅ Git repository initialized.");
423
+ } catch (e) {
424
+ console.warn("⚠️ Could not initialize git automatically. Please run 'git init' manually.");
425
+ }
426
+ }
427
+
428
+ // --- Post-Install Hooks (Smart Setup) ---
429
+
430
+ console.log("\n🛠️ Running smart configuration for adapters...");
431
+
432
+ // Universal Gemini Compatibility (Symlink)
433
+ try {
434
+ const geminiDir = path.join(targetDir, ".gemini");
435
+ const geminiAgentsDir = path.join(geminiDir, "agents");
436
+ const frameworkAgentsDir = path.join(targetDir, targetBase, "agents");
437
+
438
+ if (!fs.existsSync(geminiDir)) {
439
+ fs.mkdirSync(geminiDir, { recursive: true });
440
+ }
441
+
442
+ if (targetBase !== ".gemini" && !fs.existsSync(geminiAgentsDir)) {
443
+ const relativePath = path.relative(geminiDir, frameworkAgentsDir);
444
+ fs.symlinkSync(relativePath, geminiAgentsDir, "dir");
445
+ console.log(`🔗 Omni-Agent: Created symlink from .gemini/agents to ${targetBase}/agents`);
446
+ }
447
+ } catch (err) {
448
+ // Silently ignore if symlink fails (e.g. on non-compatible OS)
449
+ }
450
+
451
+ if (selectedAdapter === "claude" || !selectedAdapter) {
452
+ const mcpPath = path.join(targetDir, "packages/framework-mcp/src/index.ts");
453
+ console.log("\n📝 Claude Code Setup:");
454
+ console.log("To enable Agent Enderun tools in Claude Code, run this command:");
455
+ console.log(`\x1b[36mclaude config add framework-mcp npx tsx ${mcpPath}\x1b[0m`);
456
+ }
457
+
458
+ if (selectedAdapter === "cursor" || !selectedAdapter) {
459
+ console.log("✨ Cursor: Adapter CLAUDE.md and ENDERUN.md are ready to guide your AI.");
460
+ }
461
+
462
+ const pkgMgr = getPackageManager();
463
+ console.log(`\nℹ️ Detected package manager: ${pkgMgr}`);
464
+ const installCmd = pkgMgr === "npm" ? "npm install" : `${pkgMgr} install`;
465
+ const buildCmd = pkgMgr === "npm" ? "npm run enderun:build" : `${pkgMgr} run enderun:build`;
466
+
467
+ console.log("\n✨ Framework scaffolded! (v" + FRAMEWORK_VERSION + ")");
468
+
469
+ try {
470
+ const { execSync } = await import("child_process");
471
+
472
+ console.log(`\n📦 Step 1/3: Running '${installCmd}'...`);
473
+ execSync(installCmd, { stdio: "inherit" });
474
+
475
+ console.log(`\n🏗️ Step 2/3: Running '${buildCmd}'...`);
476
+ execSync(buildCmd, { stdio: "inherit" });
477
+
478
+ console.log(`\n🔍 Step 3/3: Running 'agent-enderun check'...`);
479
+ // Run the health check logic directly
480
+ await checkHealth();
481
+
482
+ console.log("\n🚀 Agent Enderun is fully installed, built and verified!");
483
+ } catch (error) {
484
+ console.error("\n❌ Automatic installation failed. Please run manually:");
485
+ console.log(`👉 ${installCmd} && ${buildCmd}`);
486
+ }
487
+ }
488
+
489
+ /**
490
+ * Check framework health and MCP status.
491
+ */
492
+ function checkCommand() {
493
+ console.log(`🔍 Checking Agent Enderun Health (v${FRAMEWORK_VERSION})...`);
494
+ let issues = 0;
495
+
496
+ const frameworkDir = getFrameworkDir();
497
+ const checks = [
498
+ { name: "Constitution (ENDERUN.md)", path: path.join(frameworkDir, "ENDERUN.md") },
499
+ { name: "Memory (PROJECT_MEMORY.md)", path: path.join(frameworkDir, "PROJECT_MEMORY.md") },
500
+ { name: "Shared Types", path: "packages/shared-types/package.json" },
501
+ { name: "MCP Server", path: "packages/framework-mcp/package.json" },
502
+ { name: "Tech Stack", path: "docs/tech-stack.md" },
503
+ { name: "Requirements", path: "docs/project-docs.md" },
504
+ { name: "API Registry", path: path.join(frameworkDir, "docs/api/README.md") },
505
+ ];
506
+
507
+ for (const check of checks) {
508
+ if (fs.existsSync(path.join(process.cwd(), check.path))) {
509
+ console.log(`✅ ${check.name} found.`);
510
+ } else {
511
+ console.log(`❌ ${check.name} MISSING! (${check.path})`);
512
+ issues++;
513
+ }
514
+ }
515
+
516
+ // Dependency Check
517
+ const mcpNodeModules = path.join(process.cwd(), "packages/framework-mcp/node_modules");
518
+ const rootNodeModules = path.join(process.cwd(), "node_modules");
519
+ if (!fs.existsSync(mcpNodeModules) && !fs.existsSync(rootNodeModules)) {
520
+ console.log("❌ Dependencies MISSING! (Run 'npm install')");
521
+ issues++;
522
+ } else {
523
+ console.log("✅ Dependencies found.");
524
+ }
525
+
526
+ // MCP Build Check
527
+ const mcpPath = path.join(process.cwd(), "packages/framework-mcp/dist/index.js");
528
+ if (!fs.existsSync(mcpPath)) {
529
+ console.log("❌ MCP Build MISSING! (Run 'npm run enderun:build')");
530
+ issues++;
531
+ } else {
532
+ console.log("✅ MCP Build found.");
533
+ console.log("⏳ Testing MCP Server syntax...");
534
+ try {
535
+ execSync(`node --check ${mcpPath}`, { stdio: "pipe" });
536
+ console.log("✅ MCP Server syntax valid.");
537
+ } catch (e) {
538
+ // If --check fails on ESM, we might skip it or use a better check
539
+ console.log("⚠️ MCP Syntax check skipped (ESM/Environment).");
540
+ }
541
+ }
542
+
543
+ if (issues === 0) {
544
+ console.log("\n🚀 All systems green! Agent Enderun is ready for orchestration.");
545
+ } else {
546
+ console.log(`\n⚠️ Found ${issues} issues. Please fix them before starting.`);
547
+ }
548
+ }
549
+
550
+ function copyDir(src, dest, skipSet = new Set(), nonDestructive = false, frameworkDir = ".enderun") {
551
+ const DEFAULT_SKIP = new Set(["node_modules", ".git", ".DS_Store"]);
552
+ const actualSkip = new Set([...DEFAULT_SKIP, ...skipSet]);
553
+
554
+ if (!fs.existsSync(dest)) {
555
+ fs.mkdirSync(dest, { recursive: true });
556
+ }
557
+
558
+ fs.readdirSync(src, { withFileTypes: true }).forEach(entry => {
559
+ if (actualSkip.has(entry.name)) return;
560
+
561
+ const srcPath = path.join(src, entry.name);
562
+ const destPath = path.join(dest, entry.name);
563
+
564
+ if (entry.isDirectory()) {
565
+ copyDir(srcPath, destPath, skipSet, nonDestructive, frameworkDir);
566
+ } else {
567
+ if (nonDestructive && fs.existsSync(destPath)) {
568
+ return;
569
+ }
570
+
571
+ const ext = path.extname(entry.name);
572
+ const textExtensions = [".md", ".json", ".js", ".ts", ".txt", ""];
573
+
574
+ if (textExtensions.includes(ext)) {
575
+ let content = fs.readFileSync(srcPath, "utf8");
576
+ content = content.replace(/\{\{FRAMEWORK_DIR\}\}/g, frameworkDir);
577
+ fs.writeFileSync(destPath, content);
578
+ } else {
579
+ fs.copyFileSync(srcPath, destPath);
580
+ }
581
+ }
582
+ });
583
+ }
584
+
585
+ /**
586
+ * Print the current framework status.
587
+ */
588
+ function statusCommand() {
589
+ const memoryPath = getMemoryPath();
590
+ const frameworkDir = getFrameworkDir();
591
+ if (!fs.existsSync(memoryPath)) {
592
+ console.error(`❌ Error: ${frameworkDir}/PROJECT_MEMORY.md not found. Please run 'init' first.`);
593
+ return;
594
+ }
595
+
596
+ const content = fs.readFileSync(memoryPath, "utf8");
597
+ const statusMatch = content.match(/\| Active Phase \| Profile \| Last Update \| Active Trace ID \| Blockers \|\n\| :----------- \| :------ \| :---------- \| :-------------- \| :------- \|\n\| (.*?) \| (.*?) \| (.*?) \| (.*?) \| (.*?) \|/);
598
+
599
+ console.log("\n📊 --- PROJECT STATUS ---");
600
+ if (statusMatch) {
601
+ console.log(`🔹 Phase: ${statusMatch[1].trim()}`);
602
+ console.log(`🧭 Profile: ${statusMatch[2].trim()}`);
603
+ console.log(`📅 Update: ${statusMatch[3].trim()}`);
604
+ console.log(`🆔 Trace ID: ${statusMatch[4].trim()}`);
605
+ console.log(`⛔ Blockers: ${statusMatch[5].trim()}`);
606
+ }
607
+
608
+ const tasksSection = content.match(/## ACTIVE TASKS\n\n([\s\S]*?)\n\n##/);
609
+ if (tasksSection) {
610
+ console.log("\n📋 Active Tasks:");
611
+ console.log(tasksSection[1].trim());
612
+ }
613
+
614
+ console.log("\n-----------------------\n");
615
+ }
616
+
617
+ /**
618
+ * Generate a new Trace ID and add it to project memory.
619
+ */
620
+ function traceNewCommand(description, agent = "manager", priority = "P2") {
621
+ const memoryPath = getMemoryPath();
622
+ if (!fs.existsSync(memoryPath)) {
623
+ console.error("❌ Error: PROJECT_MEMORY.md not found.");
624
+ return;
625
+ }
626
+
627
+ const traceId = generateULID();
628
+ const safeDescription = sanitizeTableCell(description);
629
+ const safeAgent = normalizeAgentName(agent);
630
+ const safePriority = normalizePriority(priority);
631
+ const newTask = `| ${traceId} | ${safeDescription} | @${safeAgent} | ${safePriority} | IN_PROGRESS |`;
632
+ const lockPath = `${memoryPath}.lock`;
633
+
634
+ if (!acquireMemoryLock(lockPath)) {
635
+ console.error("❌ Error: Memory lock timeout (5 retries).");
636
+ return;
637
+ }
638
+
639
+ try {
640
+ const content = fs.readFileSync(memoryPath, "utf8");
641
+ const updated = insertTaskRow(content, newTask);
642
+ if (!updated) {
643
+ console.error("❌ Error: ACTIVE TASKS table not found, task could not be added.");
644
+ return;
645
+ }
646
+
647
+ fs.writeFileSync(memoryPath, updated);
648
+ console.log(`\n✅ New Trace ID created: ${traceId}`);
649
+ console.log(`📝 Added to task list: ${description}\n`);
650
+ } finally {
651
+ releaseMemoryLock(lockPath);
652
+ }
653
+ }
654
+
655
+ /**
656
+ * Verify the shared-types contract hash.
657
+ */
658
+ function verifyContractCommand() {
659
+ const sharedDir = path.join(targetDir, "packages/shared-types/src");
660
+ const contractPath = path.join(targetDir, "packages/shared-types/contract.version.json");
661
+
662
+ if (!fs.existsSync(sharedDir) || !fs.existsSync(contractPath)) {
663
+ console.error("❌ Error: Shared types or contract.version.json not found.");
664
+ return;
665
+ }
666
+
667
+ const walk = (d) => fs.readdirSync(d, { withFileTypes: true }).flatMap((e) => {
668
+ const p = path.join(d, e.name);
669
+ return e.isDirectory() ? walk(p) : (p.endsWith(".ts") ? [p] : []);
670
+ });
671
+
672
+ const files = walk(sharedDir).sort();
673
+ const h = crypto.createHash("sha256");
674
+ for (const f of files) {
675
+ h.update(fs.readFileSync(f));
676
+ }
677
+ const currentHash = h.digest("hex");
678
+
679
+ try {
680
+ const stored = JSON.parse(fs.readFileSync(contractPath, "utf8")).contract_hash;
681
+ if (currentHash === stored) {
682
+ console.log("✅ Contract hash verified! (MATCH)");
683
+ } else {
684
+ console.error(`❌ HASH MISMATCH!\nExpected: ${stored}\nActual: ${currentHash}`);
685
+ process.exit(1);
686
+ }
687
+ } catch (err) {
688
+ console.error("❌ Error reading contract.version.json");
689
+ process.exit(1);
690
+ }
691
+ }
692
+
693
+ function logAgentActionCommand(data) {
694
+ const frameworkDir = getFrameworkDir();
695
+ const logsDir = path.join(targetDir, frameworkDir, "logs");
696
+ if (!fs.existsSync(logsDir)) {
697
+ fs.mkdirSync(logsDir, { recursive: true });
698
+ }
699
+
700
+ const agent = normalizeAgentName(data.agent);
701
+ const logPath = path.join(logsDir, `${agent}.json`);
702
+ let logs = [];
703
+
704
+ if (fs.existsSync(logPath)) {
705
+ try {
706
+ logs = JSON.parse(fs.readFileSync(logPath, "utf8"));
707
+ if (!Array.isArray(logs)) logs = [];
708
+ } catch {
709
+ logs = [];
710
+ }
711
+ }
712
+
713
+ const newEntry = {
714
+ timestamp: new Date().toISOString(),
715
+ ...data,
716
+ };
717
+
718
+ logs.push(newEntry);
719
+ fs.writeFileSync(logPath, JSON.stringify(logs, null, 2));
720
+ console.log(`✅ Logged action to ${frameworkDir}/logs/${agent}.json`);
721
+ }
722
+
723
+ function updateProjectMemoryCommand(section, content) {
724
+ const memoryPath = getMemoryPath();
725
+ if (!fs.existsSync(memoryPath)) {
726
+ console.error("❌ Error: PROJECT_MEMORY.md not found.");
727
+ return;
728
+ }
729
+
730
+ const lockPath = `${memoryPath}.lock`;
731
+ if (!acquireMemoryLock(lockPath)) {
732
+ console.error("❌ Error: Memory lock timeout.");
733
+ return;
734
+ }
735
+
736
+ try {
737
+ let memoryContent = fs.readFileSync(memoryPath, "utf8");
738
+
739
+ if (section === "HISTORY") {
740
+ const headers = ["## HISTORY (Persistent Memory)", "## HISTORY"];
741
+ let sectionIndex = -1;
742
+ let headerUsed = "";
743
+ for (const h of headers) {
744
+ sectionIndex = memoryContent.indexOf(h);
745
+ if (sectionIndex !== -1) {
746
+ headerUsed = h;
747
+ break;
748
+ }
749
+ }
750
+
751
+ if (sectionIndex === -1) {
752
+ console.error("❌ Error: HISTORY section not found.");
753
+ return;
754
+ }
755
+ const headerEnd = memoryContent.indexOf("\n", sectionIndex) + 1;
756
+ memoryContent = memoryContent.slice(0, headerEnd) + "\n" + content.trim() + "\n" + memoryContent.slice(headerEnd);
757
+ } else {
758
+ const escaped = section.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
759
+ const sectionRegex = new RegExp(`## ${escaped}[\\s\\S]*?(?=\\n## |$)`, "m");
760
+ if (!sectionRegex.test(memoryContent)) {
761
+ console.error(`❌ Error: Section not found: ${section}`);
762
+ return;
763
+ }
764
+ memoryContent = memoryContent.replace(sectionRegex, `## ${section}\n\n${content.trim()}\n`);
765
+ }
766
+
767
+ fs.writeFileSync(memoryPath, memoryContent);
768
+ console.log(`✅ Section ${section} updated in PROJECT_MEMORY.md`);
769
+ } finally {
770
+ releaseMemoryLock(lockPath);
771
+ }
772
+ }
773
+
774
+ // --- MAIN DISPATCHER ---
775
+
776
+ async function main() {
777
+ const [command, ...args] = process.argv.slice(2);
778
+
779
+ switch (command) {
780
+ case "init":
781
+ await initCommand(args[0]);
782
+ break;
783
+ case "status":
784
+ statusCommand();
785
+ break;
786
+ case "trace:new":
787
+ if (!args[0]) {
788
+ console.error("❌ Usage: agent-enderun trace:new <description> [agent] [priority]");
789
+ } else {
790
+ traceNewCommand(args[0], args[1], args[2]);
791
+ }
792
+ break;
793
+ case "verify-contract":
794
+ verifyContractCommand();
795
+ break;
796
+ case "log_agent_action": {
797
+ // Handle both structured JSON and positional args
798
+ let data = {};
799
+ try {
800
+ if (args[0] && args[0].startsWith("{")) {
801
+ data = JSON.parse(args.join(" "));
802
+ } else {
803
+ data = {
804
+ agent: args[0],
805
+ action: args[1],
806
+ requestId: args[2],
807
+ status: args[3] || "SUCCESS",
808
+ summary: args[4] || "",
809
+ };
810
+ }
811
+ } catch (e) {
812
+ console.error("❌ Error parsing arguments for log_agent_action");
813
+ process.exit(1);
814
+ }
815
+ logAgentActionCommand(data);
816
+ break;
817
+ }
818
+ case "update_project_memory": {
819
+ let section, content;
820
+ try {
821
+ if (args[0] && args[0].startsWith("{")) {
822
+ const data = JSON.parse(args.join(" "));
823
+ section = data.section;
824
+ content = data.content;
825
+ } else {
826
+ section = args[0];
827
+ content = args.slice(1).join(" ");
828
+ }
829
+ } catch (e) {
830
+ console.error("❌ Error parsing arguments for update_project_memory");
831
+ process.exit(1);
832
+ }
833
+ updateProjectMemoryCommand(section, content);
834
+ break;
835
+ }
836
+ case "check":
837
+ checkCommand();
838
+ break;
839
+ case "version":
840
+ case "-v":
841
+ case "--version":
842
+ console.log(`v${FRAMEWORK_VERSION}`);
843
+ break;
844
+ default:
845
+ console.log(`
846
+ 🤖 Agent Enderun CLI (v${FRAMEWORK_VERSION})
847
+
848
+ Available Commands:
849
+ init [adapter] Initialize the framework (gemini, claude, cursor, codex)
850
+ check Verify framework health and MCP server status
851
+ status Show current phase and task status
852
+ trace:new <desc> Generate a new Trace ID and add the task to memory
853
+ verify-contract Check if shared types match the stored hash
854
+ version Show version information
855
+
856
+ Example:
857
+ agent-enderun trace:new "Auth module design" backend P1
858
+ `);
859
+ break;
860
+ }
861
+ }
862
+
863
+ main().catch(console.error);