agent-enderun 0.9.0 → 0.9.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/.enderun/BRAIN_DASHBOARD.md +2 -2
- package/.enderun/PROJECT_MEMORY.md +26 -4
- package/.enderun/STATUS.md +24 -28
- package/.enderun/agents/backend.md +37 -8
- package/.enderun/agents/database.md +55 -0
- package/.enderun/agents/devops.md +28 -14
- package/.enderun/agents/explorer.md +1 -1
- package/.enderun/agents/frontend.md +21 -7
- package/.enderun/agents/git.md +1 -1
- package/.enderun/agents/manager.md +41 -20
- package/.enderun/agents/mobile.md +1 -1
- package/.enderun/agents/native.md +1 -1
- package/.enderun/agents/quality.md +122 -0
- package/.enderun/cli-commands.json +3 -3
- package/.enderun/config.json +7 -1
- package/.enderun/knowledge/context_boundary_rules.md +57 -0
- package/.enderun/knowledge/corporate-governance/high-risk-action-approval-flow.md +29 -0
- package/.enderun/knowledge/documentation_ownership.md +53 -0
- package/.enderun/knowledge/frontend_professionalization_guidelines.md +7 -7
- package/.enderun/knowledge/hermes_live_test_guidelines.md +5 -5
- package/.enderun/knowledge/lessons-learned.md +14 -0
- package/.enderun/knowledge/manager_authority_audit_enforcement.md +8 -8
- package/.enderun/knowledge/project_scaffold_guidelines.md +4 -4
- package/.enderun/knowledge/reference_application_guidelines.md +6 -6
- package/.enderun/memory-graph/agent-contexts/quality.json +1 -0
- package/.enderun/queue/README.md +13 -13
- package/README.md +204 -102
- package/bin/cli.js +1 -1
- package/bin/init-check.js +69 -0
- package/bin/update-contract.js +24 -2
- package/docs/README.md +21 -32
- package/docs/architecture/README.md +9 -0
- package/docs/architecture/standards/data-fetching-patterns.md +13 -0
- package/docs/architecture/standards/design-system.md +31 -0
- package/docs/architecture/standards/logging.md +7 -0
- package/docs/architecture/standards/tech-stack.md +9 -0
- package/docs/getting-started.md +9 -259
- package/docs/user/README.md +35 -0
- package/docs/user/action-plan-2026.md +9 -0
- package/docs/user/getting-started.md +13 -0
- package/docs/user/roadmap.md +13 -0
- package/eslint.config.js +1 -1
- package/framework-mcp/dist/index.js +7 -13
- package/package.json +3 -1
- package/src/cli/adapters.ts +208 -0
- package/src/cli/commands/app.ts +38 -0
- package/src/cli/commands/check.ts +87 -0
- package/src/cli/commands/compliance.ts +55 -0
- package/src/cli/commands/contract.ts +45 -0
- package/src/cli/commands/explorer.ts +45 -0
- package/src/cli/commands/git.ts +39 -0
- package/src/cli/commands/init.ts +272 -0
- package/src/cli/commands/knowledge.ts +44 -0
- package/src/cli/commands/lint.ts +25 -0
- package/src/cli/commands/log.ts +37 -0
- package/src/cli/commands/memory.ts +78 -0
- package/src/cli/commands/orchestrate.ts +111 -0
- package/src/cli/commands/script.ts +20 -0
- package/src/cli/commands/security.ts +38 -0
- package/src/cli/commands/status.ts +59 -0
- package/src/cli/commands/trace.ts +46 -0
- package/src/cli/index.ts +107 -0
- package/src/cli/utils/app.ts +764 -0
- package/src/cli/utils/claude.ts +56 -0
- package/src/cli/utils/fs.ts +139 -0
- package/src/cli/utils/memory.ts +141 -0
- package/src/cli/utils/pkg.ts +215 -0
- package/src/cli/utils/string.ts +48 -0
- package/src/cli/utils/time.ts +27 -0
- package/.enderun/agents/analyst.md +0 -236
- package/.enderun/agents/orchestrator.md +0 -172
- package/.enderun/agents/qa.md +0 -121
- package/.enderun/agents/security.md +0 -202
- package/.enderun/knowledge/api_design_rules.md +0 -6
- package/.enderun/knowledge/branded_types_pattern.md +0 -8
- package/.enderun/knowledge/code_review_checklist.md +0 -7
- package/.enderun/knowledge/contract_versioning.md +0 -7
- package/.enderun/knowledge/database_migration.md +0 -6
- package/.enderun/knowledge/deployment_checklist.md +0 -7
- package/.enderun/knowledge/git_commit_strategy.md +0 -10
- package/.enderun/knowledge/monitoring_setup.md +0 -5
- package/.enderun/knowledge/performance_guidelines.md +0 -11
- package/.enderun/knowledge/repository_patterns.md +0 -9
- package/.enderun/knowledge/security_scanning.md +0 -6
- package/.enderun/knowledge/testing_standards.md +0 -7
- package/.enderun/knowledge/troubleshooting_guide.md +0 -5
- package/.enderun/memory-graph/agent-contexts/analyst.json +0 -1
- package/.enderun/memory-graph/agent-contexts/orchestrator.json +0 -1
- package/.enderun/memory-graph/agent-contexts/qa.json +0 -1
- package/.enderun/memory-graph/agent-contexts/security.json +0 -1
- package/docs/action-plan-2026.md +0 -119
- package/docs/roadmap.md +0 -142
- package/docs/tech-stack.md +0 -30
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Finds the Claude config file path (if it exists).
|
|
6
|
+
*/
|
|
7
|
+
export function findClaudeConfigPath(): string | null {
|
|
8
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
9
|
+
if (!home) return null;
|
|
10
|
+
|
|
11
|
+
const possiblePaths = [
|
|
12
|
+
path.join(home, ".config", "claude", "config.json"),
|
|
13
|
+
path.join(home, ".claude", "config.json"),
|
|
14
|
+
path.join(home, "Library", "Application Support", "Claude", "config.json"), // macOS Claude Desktop
|
|
15
|
+
path.join(home, "Library", "Application Support", "Claude Code", "config.json"), // macOS Claude Code
|
|
16
|
+
path.join(home, ".config", "Claude", "config.json"), // some Linux setups
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
for (const p of possiblePaths) {
|
|
20
|
+
if (fs.existsSync(p)) {
|
|
21
|
+
return p;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Adds or updates an MCP server entry in Claude's config file.
|
|
29
|
+
*/
|
|
30
|
+
export function addMcpServerToClaude(configPath: string, serverName: string, serverConfig: Record<string, unknown>): boolean {
|
|
31
|
+
try {
|
|
32
|
+
let config: { mcpServers?: Record<string, Record<string, unknown>> } = { mcpServers: {} };
|
|
33
|
+
|
|
34
|
+
if (fs.existsSync(configPath)) {
|
|
35
|
+
const content = fs.readFileSync(configPath, "utf8");
|
|
36
|
+
config = JSON.parse(content);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!config.mcpServers) {
|
|
40
|
+
config.mcpServers = {};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
config.mcpServers[serverName] = serverConfig;
|
|
44
|
+
|
|
45
|
+
// Ensure parent directory exists
|
|
46
|
+
const dir = path.dirname(configPath);
|
|
47
|
+
if (!fs.existsSync(dir)) {
|
|
48
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
52
|
+
return true;
|
|
53
|
+
} catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
import { remapFrameworkContent } from "../adapters.js"; // Import from the new adapters.js
|
|
5
|
+
import type { AdapterId } from "../adapters.js";
|
|
6
|
+
|
|
7
|
+
export function updateGitIgnore(targetPath: string, frameworkDir = ".gemini"): void {
|
|
8
|
+
const IGNORE_LINES = [
|
|
9
|
+
"# AI-Enderun",
|
|
10
|
+
`${frameworkDir}/logs/*.json`,
|
|
11
|
+
`${frameworkDir}/*.lock`,
|
|
12
|
+
".env",
|
|
13
|
+
".DS_Store",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
let content = "";
|
|
17
|
+
if (fs.existsSync(targetPath)) {
|
|
18
|
+
content = fs.readFileSync(targetPath, "utf8");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const lines = content.split("\n").map((l) => l.trim());
|
|
22
|
+
let added = false;
|
|
23
|
+
|
|
24
|
+
for (const line of IGNORE_LINES) {
|
|
25
|
+
if (!lines.includes(line)) {
|
|
26
|
+
content += (content.endsWith("\n") || content === "" ? "" : "\n") + line + "\n";
|
|
27
|
+
added = true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (added) {
|
|
32
|
+
fs.writeFileSync(targetPath, content);
|
|
33
|
+
console.warn("✅ .gitignore updated.");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function ensureDir(dirPath: string): void {
|
|
38
|
+
if (!fs.existsSync(dirPath)) {
|
|
39
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function writeTextFile(filePath: string, content: string): void {
|
|
44
|
+
ensureDir(path.dirname(filePath));
|
|
45
|
+
// Use template literal for content handling to avoid issues with raw newlines
|
|
46
|
+
fs.writeFileSync(filePath, content.endsWith("\n") ? content : `${content}\n`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function writeJsonFile(filePath: string, value: unknown): void {
|
|
50
|
+
writeTextFile(filePath, JSON.stringify(value, null, 2));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface SanitizeJsonFunction {
|
|
54
|
+
(obj: any, targetScope?: string): any;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function copyDir(
|
|
58
|
+
src: string,
|
|
59
|
+
dest: string,
|
|
60
|
+
skipSet = new Set<string>(),
|
|
61
|
+
nonDestructive = false,
|
|
62
|
+
frameworkDir = ".gemini",
|
|
63
|
+
targetScope = "",
|
|
64
|
+
sanitizeJson: SanitizeJsonFunction,
|
|
65
|
+
adapterId: AdapterId = "gemini",
|
|
66
|
+
): void {
|
|
67
|
+
const DEFAULT_SKIP = new Set(["node_modules", ".git", ".DS_Store", "package-lock.json"]);
|
|
68
|
+
const actualSkip = new Set([...DEFAULT_SKIP, ...skipSet]);
|
|
69
|
+
|
|
70
|
+
if (!fs.existsSync(dest)) {
|
|
71
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fs.readdirSync(src, { withFileTypes: true }).forEach((entry) => {
|
|
75
|
+
if (actualSkip.has(entry.name)) return;
|
|
76
|
+
|
|
77
|
+
const srcPath = path.join(src, entry.name);
|
|
78
|
+
const destPath = path.join(dest, entry.name);
|
|
79
|
+
|
|
80
|
+
if (entry.isDirectory()) {
|
|
81
|
+
copyDir(
|
|
82
|
+
srcPath,
|
|
83
|
+
destPath,
|
|
84
|
+
skipSet,
|
|
85
|
+
nonDestructive,
|
|
86
|
+
frameworkDir,
|
|
87
|
+
targetScope,
|
|
88
|
+
sanitizeJson,
|
|
89
|
+
adapterId,
|
|
90
|
+
);
|
|
91
|
+
} else {
|
|
92
|
+
if (nonDestructive && fs.existsSync(destPath)) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const ext = path.extname(entry.name);
|
|
97
|
+
const textExtensions = [".md", ".json", ".js", ".ts", ".txt", ""];
|
|
98
|
+
|
|
99
|
+
if (textExtensions.includes(ext)) {
|
|
100
|
+
let content = fs.readFileSync(srcPath, "utf8");
|
|
101
|
+
content = remapFrameworkContent(content, frameworkDir, adapterId); // Use the new remap function
|
|
102
|
+
|
|
103
|
+
if (ext === ".json") {
|
|
104
|
+
try {
|
|
105
|
+
const json = JSON.parse(content);
|
|
106
|
+
content = JSON.stringify(sanitizeJson(json, targetScope), null, 2);
|
|
107
|
+
content = remapFrameworkContent(content, frameworkDir, adapterId); // Remap again after JSON.stringify for any placeholders
|
|
108
|
+
} catch {
|
|
109
|
+
content = content.replace(/workspace:[^"'\s]*/g, "*");
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
content = content.replace(/workspace:[^"'\s]*/g, "*");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fs.writeFileSync(destPath, content);
|
|
116
|
+
} else {
|
|
117
|
+
fs.copyFileSync(srcPath, destPath);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function collectFiles(dir: string, extensions: string[]): string[] {
|
|
124
|
+
let results: string[] = [];
|
|
125
|
+
if (!fs.existsSync(dir)) return results;
|
|
126
|
+
const list = fs.readdirSync(dir);
|
|
127
|
+
list.forEach((file) => {
|
|
128
|
+
file = path.join(dir, file);
|
|
129
|
+
const stat = fs.statSync(file);
|
|
130
|
+
if (stat && stat.isDirectory()) {
|
|
131
|
+
if (!file.includes("node_modules") && !file.includes(".git")) {
|
|
132
|
+
results = results.concat(collectFiles(file, extensions));
|
|
133
|
+
}
|
|
134
|
+
} else if (extensions.includes(path.extname(file))) {
|
|
135
|
+
results.push(file);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
return results;
|
|
139
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { sleep } from "./time.js";
|
|
4
|
+
import { generateULID } from "./time.js";
|
|
5
|
+
import { FRAMEWORK_DIR_CANDIDATES } from "../adapters.js";
|
|
6
|
+
|
|
7
|
+
const targetDir = process.cwd();
|
|
8
|
+
|
|
9
|
+
export function getFrameworkDir(): string {
|
|
10
|
+
for (const dir of FRAMEWORK_DIR_CANDIDATES) {
|
|
11
|
+
const dirPath = path.join(targetDir, dir);
|
|
12
|
+
if (!fs.existsSync(dirPath)) continue;
|
|
13
|
+
const hasMemory = fs.existsSync(path.join(dirPath, "PROJECT_MEMORY.md"));
|
|
14
|
+
const hasAgents = fs.existsSync(path.join(dirPath, "agents"));
|
|
15
|
+
if (hasMemory || hasAgents) {
|
|
16
|
+
return dir;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
for (const dir of FRAMEWORK_DIR_CANDIDATES) {
|
|
20
|
+
if (fs.existsSync(path.join(targetDir, dir))) {
|
|
21
|
+
return dir;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return ".gemini";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getMemoryPath() {
|
|
28
|
+
return path.join(targetDir, getFrameworkDir(), "PROJECT_MEMORY.md");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getConfiguredPaths(): { backend: string; frontend: string; docs: string; tests: string } {
|
|
32
|
+
let backend = "apps/backend";
|
|
33
|
+
let frontend = "apps/web";
|
|
34
|
+
let docs = "docs";
|
|
35
|
+
let tests = "tests";
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const frameworkDir = getFrameworkDir();
|
|
39
|
+
const configPath = path.join(targetDir, frameworkDir, "config.json");
|
|
40
|
+
if (fs.existsSync(configPath)) {
|
|
41
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
42
|
+
if (config.paths) {
|
|
43
|
+
if (config.paths.backend) backend = config.paths.backend;
|
|
44
|
+
if (config.paths.frontend) frontend = config.paths.frontend;
|
|
45
|
+
if (config.paths.docs) docs = config.paths.docs;
|
|
46
|
+
if (config.paths.tests) tests = config.paths.tests;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
// ignore
|
|
51
|
+
}
|
|
52
|
+
return { backend, frontend, docs, tests };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function acquireMemoryLock(lockPath: string, maxRetries = 5): boolean {
|
|
56
|
+
for (let attempt = 0; attempt < maxRetries; attempt += 1) {
|
|
57
|
+
try {
|
|
58
|
+
const fd = fs.openSync(lockPath, "wx");
|
|
59
|
+
fs.closeSync(fd);
|
|
60
|
+
return true;
|
|
61
|
+
} catch (error: unknown) {
|
|
62
|
+
const err = error as NodeJS.ErrnoException;
|
|
63
|
+
if (err?.code !== "EEXIST") throw error;
|
|
64
|
+
if (attempt < maxRetries - 1) sleep(1000);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function releaseMemoryLock(lockPath: string): void {
|
|
71
|
+
if (fs.existsSync(lockPath)) fs.unlinkSync(lockPath);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Create initial PROJECT_MEMORY.md if missing.
|
|
76
|
+
*/
|
|
77
|
+
export function initializeMemory(memoryPath: string, targetBase: string): void {
|
|
78
|
+
if (fs.existsSync(memoryPath)) return;
|
|
79
|
+
|
|
80
|
+
const traceId = generateULID();
|
|
81
|
+
const date = new Date().toISOString().split("T")[0];
|
|
82
|
+
const template = `# PROJECT MEMORY — Agent Enderun
|
|
83
|
+
|
|
84
|
+
This file is the Single Source of Truth (SSOT) and the persistent memory of the project.
|
|
85
|
+
|
|
86
|
+
## CURRENT STATUS
|
|
87
|
+
|
|
88
|
+
| Active Phase | Profile | Last Update | Active Trace ID | Blockers |
|
|
89
|
+
| :----------- | :------ | :---------- | :-------------- | :------- |
|
|
90
|
+
| PHASE_0 | Lightweight | ${date} | ${traceId} | NONE |
|
|
91
|
+
|
|
92
|
+
## PROJECT DEFINITION
|
|
93
|
+
|
|
94
|
+
| Field | Value |
|
|
95
|
+
| :--- | :--- |
|
|
96
|
+
| Project Name | ${path.basename(process.cwd())} |
|
|
97
|
+
| Platform | Not defined |
|
|
98
|
+
| Frontend | React 19 + Vite + Panda CSS |
|
|
99
|
+
| Backend | Node.js 20+ + Fastify |
|
|
100
|
+
| DB | PostgreSQL |
|
|
101
|
+
|
|
102
|
+
## DOD STATUS
|
|
103
|
+
|
|
104
|
+
| Phase | Status | Note |
|
|
105
|
+
| :--- | :--- | :--- |
|
|
106
|
+
| PHASE_0 | IN_PROGRESS | Initializing project structure |
|
|
107
|
+
| PHASE_1 | PENDING | |
|
|
108
|
+
| PHASE_2 | PENDING | |
|
|
109
|
+
| PHASE_3 | PENDING | |
|
|
110
|
+
| PHASE_4 | PENDING | |
|
|
111
|
+
|
|
112
|
+
## CRITICAL DECISIONS
|
|
113
|
+
|
|
114
|
+
| Date | Decision | Rationale | Agent |
|
|
115
|
+
| :--- | :--- | :--- | :--- |
|
|
116
|
+
| ${date} | Project Initialized | Framework setup via CLI | @manager |
|
|
117
|
+
|
|
118
|
+
## DELIVERABLES
|
|
119
|
+
|
|
120
|
+
| Module | Status | Agent | Date |
|
|
121
|
+
| :--- | :--- | :--- | :--- |
|
|
122
|
+
|
|
123
|
+
## ACTIVE TASKS
|
|
124
|
+
|
|
125
|
+
| Trace ID | Task | Agent | Priority | Status |
|
|
126
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
127
|
+
| ${traceId} | Framework setup and architecture alignment | @manager | P1 | IN_PROGRESS |
|
|
128
|
+
|
|
129
|
+
## HISTORY (Persistent Memory)
|
|
130
|
+
|
|
131
|
+
### ${date} — Framework Initialization
|
|
132
|
+
|
|
133
|
+
- **Agent:** @manager
|
|
134
|
+
- **Trace ID:** ${traceId}
|
|
135
|
+
- **Action:** Initialized Agent Enderun framework and project structure.
|
|
136
|
+
`;
|
|
137
|
+
|
|
138
|
+
const finalTemplate = template.replace(/\{\{FRAMEWORK_DIR\}\}/g, targetBase);
|
|
139
|
+
fs.writeFileSync(memoryPath, finalTemplate);
|
|
140
|
+
console.warn(`✅ PROJECT_MEMORY.md initialized in ${targetBase}`);
|
|
141
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
const __filename = new URL(import.meta.url).pathname;
|
|
5
|
+
const __dirname = path.dirname(__filename);
|
|
6
|
+
const sourceDir = path.join(__dirname, "../../.."); // Adjust path as necessary to reach project root
|
|
7
|
+
|
|
8
|
+
export function getPackageVersion() {
|
|
9
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(sourceDir, "package.json"), "utf8"));
|
|
10
|
+
return pkg.version;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getPackageManager() {
|
|
14
|
+
const override = process.env.AGENT_ENDERUN_PACKAGE_MANAGER || process.env.AGENT_ENDERUN_PM || process.env.AI_ENDERUN_PACKAGE_MANAGER || process.env.AI_ENDERUN_PM;
|
|
15
|
+
if (override) return override.toLowerCase();
|
|
16
|
+
|
|
17
|
+
const userAgent = process.env.npm_config_user_agent || "";
|
|
18
|
+
const npmExecPath = process.env.npm_execpath || "";
|
|
19
|
+
|
|
20
|
+
if (userAgent.includes("pnpm") || npmExecPath.includes("pnpm")) return "pnpm";
|
|
21
|
+
if (userAgent.includes("yarn") || npmExecPath.includes("yarn")) return "yarn";
|
|
22
|
+
|
|
23
|
+
// Check for lockfiles in target directory
|
|
24
|
+
if (fs.existsSync(path.join(process.cwd(), "pnpm-lock.yaml")) || fs.existsSync(path.join(process.cwd(), "pnpm-workspace.yaml"))) return "pnpm";
|
|
25
|
+
if (fs.existsSync(path.join(process.cwd(), "yarn.lock"))) return "yarn";
|
|
26
|
+
|
|
27
|
+
return "npm";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface PackageJson {
|
|
31
|
+
name?: string;
|
|
32
|
+
version?: string;
|
|
33
|
+
type?: string;
|
|
34
|
+
workspaces?: string[];
|
|
35
|
+
dependencies?: Record<string, string>;
|
|
36
|
+
devDependencies?: Record<string, string>;
|
|
37
|
+
peerDependencies?: Record<string, string>;
|
|
38
|
+
optionalDependencies?: Record<string, string>;
|
|
39
|
+
scripts?: Record<string, string>;
|
|
40
|
+
enderun?: Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function mergePackageJson(targetPath: string, sourcePath: string): void {
|
|
44
|
+
let targetPkg: PackageJson = {};
|
|
45
|
+
if (fs.existsSync(targetPath)) {
|
|
46
|
+
try {
|
|
47
|
+
targetPkg = JSON.parse(fs.readFileSync(targetPath, "utf8"));
|
|
48
|
+
} catch {
|
|
49
|
+
console.warn("⚠️ Could not parse existing package.json, creating a new one.");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const sourcePkg: PackageJson = JSON.parse(fs.readFileSync(sourcePath, "utf8"));
|
|
54
|
+
|
|
55
|
+
type PackageMap = Record<string, string>;
|
|
56
|
+
const sanitizeDeps = (deps: PackageMap | Record<string, unknown> | undefined): Record<string, string> | undefined => {
|
|
57
|
+
if (!deps) return deps as undefined;
|
|
58
|
+
const cleaned: Record<string, string> = {};
|
|
59
|
+
for (const [name, version] of Object.entries(deps)) {
|
|
60
|
+
cleaned[name] = (typeof version === "string" && version.startsWith("workspace:")) ? "*" : String(version || "");
|
|
61
|
+
}
|
|
62
|
+
return cleaned;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const actualTargetScope = targetPkg.name && targetPkg.name.startsWith("@")
|
|
66
|
+
? targetPkg.name.split("/")[0]
|
|
67
|
+
: (targetPkg.name ? `@${targetPkg.name}` : "");
|
|
68
|
+
|
|
69
|
+
// Cleanup potential leftovers from previous bugged runs where agent-enderun was renamed to target name
|
|
70
|
+
if (actualTargetScope) {
|
|
71
|
+
const scopeName = actualTargetScope.startsWith("@") ? actualTargetScope.slice(1) : actualTargetScope;
|
|
72
|
+
const cleanup = (obj: Record<string, string> | undefined): void => {
|
|
73
|
+
if (!obj) return;
|
|
74
|
+
delete obj[scopeName];
|
|
75
|
+
delete obj[actualTargetScope];
|
|
76
|
+
delete obj["agent-enderun"]; // Will be re-added correctly
|
|
77
|
+
};
|
|
78
|
+
cleanup(targetPkg.devDependencies);
|
|
79
|
+
cleanup(targetPkg.dependencies);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
targetPkg.dependencies = sanitizeDeps({
|
|
83
|
+
...targetPkg.dependencies,
|
|
84
|
+
...sourcePkg.dependencies
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Merge scripts
|
|
88
|
+
const pkgMgr = getPackageManager();
|
|
89
|
+
const runCmd = pkgMgr === "yarn" ? "yarn" : (pkgMgr === "pnpm" ? "pnpm" : "npm run");
|
|
90
|
+
|
|
91
|
+
targetPkg.scripts = {
|
|
92
|
+
...targetPkg.scripts,
|
|
93
|
+
"enderun:status": "agent-enderun status",
|
|
94
|
+
"enderun:trace": "agent-enderun trace:new",
|
|
95
|
+
"enderun:verify": "agent-enderun verify-contract",
|
|
96
|
+
"enderun:check": "agent-enderun check",
|
|
97
|
+
"enderun:test": "vitest run",
|
|
98
|
+
"enderun:test:watch": "vitest",
|
|
99
|
+
"enderun:build": `${runCmd} build --prefix framework-mcp`,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const sourceDevDeps = sourcePkg.devDependencies || {};
|
|
103
|
+
targetPkg.devDependencies = sanitizeDeps({
|
|
104
|
+
...targetPkg.devDependencies,
|
|
105
|
+
"agent-enderun": `^${sourcePkg.version}`,
|
|
106
|
+
...(sourceDevDeps["@modelcontextprotocol/sdk"] ? {"@modelcontextprotocol/sdk": sourceDevDeps["@modelcontextprotocol/sdk"]} : {}),
|
|
107
|
+
...(sourceDevDeps["zod"] ? {"zod": sourceDevDeps["zod"]} : {}),
|
|
108
|
+
...(sourceDevDeps["ts-morph"] ? {"ts-morph": sourceDevDeps["ts-morph"]} : {}),
|
|
109
|
+
...(sourceDevDeps["typescript"] ? {"typescript": sourceDevDeps["typescript"]} : {}),
|
|
110
|
+
...(sourceDevDeps["@types/node"] ? {"@types/node": sourceDevDeps["@types/node"]} : {}),
|
|
111
|
+
...(sourceDevDeps["tsx"] ? {"tsx": sourceDevDeps["tsx"]} : {}),
|
|
112
|
+
...(sourceDevDeps["vitest"] ? {"vitest": sourceDevDeps["vitest"]} : {}),
|
|
113
|
+
"@pandacss/dev": sourceDevDeps["@pandacss/dev"] || "^1.11.1"
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (targetPkg.peerDependencies) {
|
|
117
|
+
targetPkg.peerDependencies = sanitizeDeps(targetPkg.peerDependencies);
|
|
118
|
+
}
|
|
119
|
+
if (targetPkg.optionalDependencies) {
|
|
120
|
+
targetPkg.optionalDependencies = sanitizeDeps(targetPkg.optionalDependencies);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Ensure basic fields
|
|
124
|
+
if (!targetPkg.name) targetPkg.name = path.basename(process.cwd());
|
|
125
|
+
if (!targetPkg.version) targetPkg.version = "0.1.0";
|
|
126
|
+
if (!targetPkg.type) targetPkg.type = "module";
|
|
127
|
+
if (!targetPkg.workspaces) targetPkg.workspaces = ["apps/*", "framework-mcp"];
|
|
128
|
+
|
|
129
|
+
// Add metadata
|
|
130
|
+
targetPkg.enderun = {
|
|
131
|
+
version: sourcePkg.version || "0.0.0",
|
|
132
|
+
initializedAt: new Date().toISOString(),
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
fs.writeFileSync(targetPath, JSON.stringify(targetPkg, null, 2));
|
|
136
|
+
console.warn("✅ package.json updated with Enderun scripts and dependencies.");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function sanitizeJson(obj: Record<string, unknown>, targetScope = ""): Record<string, unknown> {
|
|
140
|
+
if (typeof obj !== "object" || obj === null) return obj;
|
|
141
|
+
if (Array.isArray(obj)) return obj.map(item => sanitizeJson(item as Record<string, unknown>, targetScope)) as unknown as Record<string, unknown>;
|
|
142
|
+
const cleaned: Record<string, unknown> = {};
|
|
143
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
144
|
+
let finalKey = key;
|
|
145
|
+
let finalValue = value;
|
|
146
|
+
|
|
147
|
+
// Remove UnoCSS related keys or values
|
|
148
|
+
if (typeof key === "string" && key.includes("unocss")) continue;
|
|
149
|
+
if (typeof value === "string" && value.includes("unocss")) {
|
|
150
|
+
continue; // Skip this script/field
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Replace scope if needed
|
|
154
|
+
if (targetScope) {
|
|
155
|
+
const scopeName = targetScope.startsWith("@") ? targetScope.slice(1) : targetScope;
|
|
156
|
+
|
|
157
|
+
// Handle scoped: @ai-enderun/foo -> @target/foo
|
|
158
|
+
if (typeof key === "string" && key.startsWith("@ai-enderun/")) {
|
|
159
|
+
finalKey = key.replace("@ai-enderun/", `${targetScope}/`);
|
|
160
|
+
}
|
|
161
|
+
if (typeof value === "string" && value.startsWith("@ai-enderun/")) {
|
|
162
|
+
finalValue = value.replace("@ai-enderun/", `${targetScope}/`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Handle unscoped: ai-enderun-foo -> target-foo
|
|
166
|
+
if (typeof key === "string" && key.startsWith("ai-enderun-")) {
|
|
167
|
+
finalKey = key.replace("ai-enderun-", `${scopeName}-`);
|
|
168
|
+
}
|
|
169
|
+
if (typeof value === "string" && value.startsWith("ai-enderun-")) {
|
|
170
|
+
finalValue = value.replace("ai-enderun-", `${scopeName}-`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Handle agent-enderun -> target (ONLY for the package name)
|
|
174
|
+
if (key === "name" && value === "agent-enderun") {
|
|
175
|
+
finalValue = scopeName;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Preserve agent-enderun in dependencies and bin
|
|
179
|
+
// (No action needed as finalKey/finalValue default to original)
|
|
180
|
+
|
|
181
|
+
if (typeof value === "string" && value.startsWith("workspace:")) {
|
|
182
|
+
finalValue = "*";
|
|
183
|
+
}
|
|
184
|
+
} else if (typeof value === "string" && value.startsWith("workspace:")) {
|
|
185
|
+
finalValue = "*";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
cleaned[finalKey] = (typeof finalValue === "object" && finalValue !== null) ? sanitizeJson(finalValue as Record<string, unknown>, targetScope) : finalValue;
|
|
189
|
+
}
|
|
190
|
+
return cleaned;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function deepCleanProtocols(dir: string, targetScope = ""): void {
|
|
194
|
+
if (!fs.existsSync(dir)) return;
|
|
195
|
+
|
|
196
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
197
|
+
for (const entry of entries) {
|
|
198
|
+
const fullPath = path.join(dir, entry.name);
|
|
199
|
+
if (entry.isDirectory()) {
|
|
200
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
201
|
+
deepCleanProtocols(fullPath, targetScope);
|
|
202
|
+
} else if (entry.name === "package.json") {
|
|
203
|
+
try {
|
|
204
|
+
const content = fs.readFileSync(fullPath, "utf8");
|
|
205
|
+
const json = JSON.parse(content);
|
|
206
|
+
const cleaned = JSON.stringify(sanitizeJson(json, targetScope), null, 2);
|
|
207
|
+
fs.writeFileSync(fullPath, cleaned);
|
|
208
|
+
} catch {
|
|
209
|
+
// ignore malformed json
|
|
210
|
+
}
|
|
211
|
+
} else if (entry.name === "package-lock.json") {
|
|
212
|
+
fs.unlinkSync(fullPath);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export function insertTaskRow(memoryContent: string, row: string): string | null {
|
|
2
|
+
const sectionHeader = "## ACTIVE TASKS";
|
|
3
|
+
const tableDivider = "| :--- | :--- | :--- | :--- | :--- |";
|
|
4
|
+
const sectionIndex = memoryContent.indexOf(sectionHeader);
|
|
5
|
+
if (sectionIndex === -1) return null;
|
|
6
|
+
const dividerIndex = memoryContent.indexOf(tableDivider, sectionIndex);
|
|
7
|
+
if (dividerIndex === -1) return null;
|
|
8
|
+
const dividerLineEnd = memoryContent.indexOf("\n", dividerIndex);
|
|
9
|
+
if (dividerLineEnd === -1) return null;
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
memoryContent.slice(0, dividerLineEnd + 1) +
|
|
13
|
+
`${row}\n` +
|
|
14
|
+
memoryContent.slice(dividerLineEnd + 1)
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function sanitizeTableCell(value: unknown): string {
|
|
19
|
+
return String(value)
|
|
20
|
+
.replace(/\|/g, "\\|")
|
|
21
|
+
.replace(/\r?\n/g, " ")
|
|
22
|
+
.trim();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function normalizeAgentName(agent?: unknown): string {
|
|
26
|
+
return String(agent || "manager").replace(/^@+/, "").trim() || "manager";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function normalizePriority(priority?: unknown): string {
|
|
30
|
+
const normalized = String(priority || "P2").toUpperCase().trim();
|
|
31
|
+
return /^P[0-3]$/.test(normalized) ? normalized : "P2";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function slugifyName(value: string): string {
|
|
35
|
+
const slug = String(value || "enderun-app")
|
|
36
|
+
.toLowerCase()
|
|
37
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
38
|
+
.replace(/^-+|-+$/g, "");
|
|
39
|
+
return slug || "enderun-app";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function titleCase(value: string): string {
|
|
43
|
+
return String(value || "Enderun App")
|
|
44
|
+
.replace(/[-_]+/g, " ")
|
|
45
|
+
.replace(/\s+/g, " ")
|
|
46
|
+
.trim()
|
|
47
|
+
.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
48
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function generateULID(seedTime = Date.now(), seed?: number) {
|
|
2
|
+
const ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
3
|
+
const ENCODING_LEN = ENCODING.length;
|
|
4
|
+
let time = seedTime;
|
|
5
|
+
const timeChars = new Array(10);
|
|
6
|
+
for (let i = 9; i >= 0; i--) {
|
|
7
|
+
timeChars[i] = ENCODING.charAt(time % ENCODING_LEN);
|
|
8
|
+
time = Math.floor(time / ENCODING_LEN);
|
|
9
|
+
}
|
|
10
|
+
const randomChars = new Array(16);
|
|
11
|
+
if (seed) {
|
|
12
|
+
let pseudoRandom = seed;
|
|
13
|
+
for (let i = 0; i < 16; i++) {
|
|
14
|
+
pseudoRandom = (pseudoRandom * 16807) % 2147483647;
|
|
15
|
+
randomChars[i] = ENCODING.charAt(pseudoRandom % ENCODING_LEN);
|
|
16
|
+
}
|
|
17
|
+
} else {
|
|
18
|
+
for (let i = 0; i < 16; i++) {
|
|
19
|
+
randomChars[i] = ENCODING.charAt(Math.floor(Math.random() * ENCODING_LEN));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return timeChars.join("") + randomChars.join("");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function sleep(ms: number): void {
|
|
26
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
27
|
+
}
|