agent-enderun 0.5.1 → 0.5.2
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 +0 -0
- package/.enderun/PROJECT_MEMORY.md +27 -2
- package/.enderun/STATUS.md +2 -0
- package/.enderun/agents/analyst.md +8 -8
- package/.enderun/agents/backend.md +11 -11
- package/.enderun/agents/explorer.md +4 -4
- package/.enderun/agents/frontend.md +7 -7
- package/.enderun/agents/git.md +5 -5
- package/.enderun/agents/manager.md +12 -12
- package/.enderun/agents/mobile.md +5 -5
- package/.enderun/agents/native.md +5 -5
- package/.enderun/benchmarks/.gitkeep +0 -0
- package/.enderun/cli-commands.json +0 -0
- package/.enderun/config.json +0 -0
- package/.enderun/docs/api/README.md +0 -0
- package/.enderun/docs/api/auth.md +0 -0
- package/.enderun/docs/api/errors.md +0 -0
- package/.enderun/docs/error-handling.md +0 -0
- package/.enderun/docs/privacy.md +0 -0
- package/.enderun/docs/project-docs.md +0 -0
- package/.enderun/docs/security.md +0 -0
- package/.enderun/docs/tech-stack.md +1 -1
- package/.enderun/docs/troubleshooting.md +0 -0
- package/.enderun/knowledge/api_design_rules.md +0 -0
- package/.enderun/knowledge/async_error_handling.md +0 -0
- package/.enderun/knowledge/branded_types_pattern.md +0 -0
- package/.enderun/knowledge/code_review_checklist.md +0 -0
- package/.enderun/knowledge/contract_versioning.md +0 -0
- package/.enderun/knowledge/database_migration.md +0 -0
- package/.enderun/knowledge/deployment_checklist.md +0 -0
- package/.enderun/knowledge/git_commit_strategy.md +0 -0
- package/.enderun/knowledge/hermes_protocol.md +0 -0
- package/.enderun/knowledge/legacy_onboarding.md +0 -0
- package/.enderun/knowledge/monitoring_setup.md +0 -0
- package/.enderun/knowledge/performance_guidelines.md +0 -0
- package/.enderun/knowledge/repository_patterns.md +0 -0
- package/.enderun/knowledge/responsive_design_standards.md +0 -0
- package/.enderun/knowledge/security_scanning.md +0 -0
- package/.enderun/knowledge/testing_standards.md +1 -1
- package/.enderun/knowledge/troubleshooting_guide.md +0 -0
- package/.enderun/knowledge/zero_ui_library_policy.md +0 -0
- package/.enderun/logs/.gitkeep +0 -0
- package/.enderun/messages/.gitkeep +0 -0
- package/.enderun/monitoring/.gitkeep +0 -0
- package/.env.example +0 -0
- package/ENDERUN.md +3 -3
- package/LICENSE +0 -0
- package/README.md +38 -1
- package/bin/cli.js +627 -3
- package/bin/update-contract.js +21 -2
- package/claude.md +1 -1
- package/codex.md +1 -1
- package/cursor.md +1 -1
- package/docs/README.md +23 -0
- package/gemini-extension.json +8 -2
- package/gemini.md +1 -1
- package/mcp.json +0 -0
- package/package.json +4 -3
- package/packages/framework-mcp/dist/index.js +0 -0
- package/packages/framework-mcp/dist/schemas.js +2 -0
- package/packages/framework-mcp/dist/tools/contract.js +13 -7
- package/packages/framework-mcp/dist/tools/knowledge.js +1 -1
- package/packages/framework-mcp/dist/tools/messages.js +15 -6
- package/packages/framework-mcp/dist/tools/repository.js +24 -3
- package/packages/framework-mcp/dist/utils.js +2 -2
- package/packages/framework-mcp/package.json +2 -2
- package/packages/framework-mcp/src/schemas.ts +2 -0
- package/packages/framework-mcp/src/tools/contract.ts +14 -7
- package/packages/framework-mcp/src/tools/knowledge.ts +1 -1
- package/packages/framework-mcp/src/tools/messages.ts +17 -8
- package/packages/framework-mcp/src/tools/repository.ts +24 -3
- package/packages/framework-mcp/src/utils.ts +2 -2
- package/packages/shared-types/README.md +0 -0
- package/packages/shared-types/contract.version.json +3 -3
- package/packages/shared-types/package.json +4 -4
- package/packages/shared-types/src/index.ts +0 -0
- package/packages/shared-types/tsconfig.json +0 -0
- package/panda.config.ts +5 -1
- package/tsconfig.json +0 -0
- package/packages/framework-mcp/dist/index.d.ts +0 -1
- package/packages/shared-types/dist/index.d.ts.map +0 -1
package/bin/update-contract.js
CHANGED
|
@@ -12,13 +12,32 @@ const projectRoot = path.join(__dirname, "..");
|
|
|
12
12
|
const contractPath = path.join(projectRoot, "packages/shared-types/contract.version.json");
|
|
13
13
|
const sharedTypesDir = path.join(projectRoot, "packages/shared-types/src");
|
|
14
14
|
|
|
15
|
+
function collectTypeFiles(dir) {
|
|
16
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
17
|
+
const files = [];
|
|
18
|
+
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
const fullPath = path.join(dir, entry.name);
|
|
21
|
+
if (entry.isDirectory()) {
|
|
22
|
+
files.push(...collectTypeFiles(fullPath));
|
|
23
|
+
} else if (entry.name.endsWith(".ts")) {
|
|
24
|
+
files.push(fullPath);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return files;
|
|
29
|
+
}
|
|
30
|
+
|
|
15
31
|
function calculateHash(dir) {
|
|
16
32
|
const hash = crypto.createHash("sha256");
|
|
17
|
-
const files =
|
|
33
|
+
const files = collectTypeFiles(dir).sort();
|
|
18
34
|
|
|
19
35
|
files.forEach(file => {
|
|
20
|
-
const content = fs.readFileSync(
|
|
36
|
+
const content = fs.readFileSync(file);
|
|
37
|
+
hash.update(path.relative(projectRoot, file));
|
|
38
|
+
hash.update("\0");
|
|
21
39
|
hash.update(content);
|
|
40
|
+
hash.update("\0");
|
|
22
41
|
});
|
|
23
42
|
|
|
24
43
|
return hash.digest("hex");
|
package/claude.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
This file is the entry point for Claude. The project's "Supreme Law" and all instructions are located in the following file:
|
|
4
4
|
|
|
5
|
-
👉 **[
|
|
5
|
+
👉 **[.claude/ENDERUN.md](.claude/ENDERUN.md)**
|
|
6
6
|
|
|
7
7
|
### 🤖 AGENT DIRECTIVE
|
|
8
8
|
You are ALWAYS operating as `@manager` (Team-Lead) by default — on every message, every turn. The user does NOT need to type `@manager`. Typing a specific agent (e.g. `@backend`) explicitly will override this and activate that specialist role directly. You are responsible for analysis and delegation.
|
package/codex.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
This file is the entry point for Codex. The project's "Supreme Law" and all instructions are located in the following file:
|
|
4
4
|
|
|
5
|
-
👉 **[
|
|
5
|
+
👉 **[.enderun/ENDERUN.md](.enderun/ENDERUN.md)**
|
|
6
6
|
|
|
7
7
|
### 🤖 AGENT DIRECTIVE
|
|
8
8
|
You are ALWAYS operating as `@manager` (Team-Lead) by default — on every message, every turn. The user does NOT need to type `@manager`. Typing a specific agent (e.g. `@backend`) explicitly will override this and activate that specialist role directly. You are responsible for analysis and delegation.
|
package/cursor.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
This file is the entry point for Cursor. The project's "Supreme Law" and all instructions are located in the following file:
|
|
4
4
|
|
|
5
|
-
👉 **[
|
|
5
|
+
👉 **[.cursor/ENDERUN.md](.cursor/ENDERUN.md)**
|
|
6
6
|
|
|
7
7
|
### 🤖 AGENT DIRECTIVE
|
|
8
8
|
You are ALWAYS operating as `@manager` (Team-Lead) by default — on every message, every turn. The user does NOT need to type `@manager`. Typing a specific agent (e.g. `@backend`) explicitly will override this and activate that specialist role directly. You are responsible for analysis and delegation.
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Project Documentation
|
|
2
|
+
|
|
3
|
+
This folder is managed by **Agent Enderun agents** and contains documentation
|
|
4
|
+
generated for **your project** — not the framework itself.
|
|
5
|
+
|
|
6
|
+
## What Goes Here
|
|
7
|
+
|
|
8
|
+
| File | Owner Agent | Description |
|
|
9
|
+
| :--- | :--- | :--- |
|
|
10
|
+
| `architecture.md` | @backend | System architecture and data flow diagrams |
|
|
11
|
+
| `api-reference.md` | @backend | Public API endpoints and usage examples |
|
|
12
|
+
| `components.md` | @frontend | UI component catalog and usage guidelines |
|
|
13
|
+
| `deployment.md` | @manager | Deployment steps and environment setup |
|
|
14
|
+
| `decisions.md` | @manager | Architecture Decision Records (ADRs) |
|
|
15
|
+
|
|
16
|
+
## Distinction from `.enderun/docs/`
|
|
17
|
+
|
|
18
|
+
| Path | Purpose |
|
|
19
|
+
| :--- | :--- |
|
|
20
|
+
| `/docs/` | **Your project** documentation — written by agents as features are built |
|
|
21
|
+
| `/.enderun/docs/` | **Framework** configuration — tech-stack, security policy, privacy policy |
|
|
22
|
+
|
|
23
|
+
> Agents will populate this folder automatically during **PHASE_2 (Core Development)**.
|
package/gemini-extension.json
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "Agent Enderun",
|
|
3
3
|
"description": "Enterprise-grade AI orchestration framework for software teams.",
|
|
4
|
-
"instructions": "
|
|
5
|
-
|
|
4
|
+
"instructions": ".gemini/ENDERUN.md",
|
|
5
|
+
"mcpServers": {
|
|
6
|
+
"agent-enderun": {
|
|
7
|
+
"command": "node",
|
|
8
|
+
"args": ["packages/framework-mcp/dist/index.js"]
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
package/gemini.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
This file is the entry point for Gemini. The project's "Supreme Law" and all instructions are located in the following file:
|
|
4
4
|
|
|
5
|
-
👉 **[
|
|
5
|
+
👉 **[.gemini/ENDERUN.md](.gemini/ENDERUN.md)**
|
|
6
6
|
|
|
7
7
|
### 🤖 AGENT DIRECTIVE
|
|
8
8
|
You are ALWAYS operating as `@manager` (Team-Lead) by default — on every message, every turn. The user does NOT need to type `@manager`. Typing a specific agent (e.g. `@backend`) explicitly will override this and activate that specialist role directly. You are responsible for analysis and delegation.
|
package/mcp.json
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-enderun",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "The Supreme AI Governance & Orchestration Framework for Enterprise Development",
|
|
5
5
|
"author": "Yusuf BEKAR",
|
|
6
6
|
"license": "MIT",
|
|
@@ -51,7 +51,8 @@
|
|
|
51
51
|
"gemini-extension.json",
|
|
52
52
|
"cursor.md",
|
|
53
53
|
"claude.md",
|
|
54
|
-
"codex.md"
|
|
54
|
+
"codex.md",
|
|
55
|
+
"docs"
|
|
55
56
|
],
|
|
56
57
|
"scripts": {
|
|
57
58
|
"enderun:build": "npm run build --workspaces --if-present",
|
|
@@ -78,7 +79,7 @@
|
|
|
78
79
|
"vitest": "^3.0.5"
|
|
79
80
|
},
|
|
80
81
|
"enderun": {
|
|
81
|
-
"version": "0.5.
|
|
82
|
+
"version": "0.5.2",
|
|
82
83
|
"initializedAt": "2026-05-09T13:24:27.472Z"
|
|
83
84
|
}
|
|
84
85
|
}
|
|
File without changes
|
|
@@ -38,6 +38,7 @@ export const SEARCH_KNOWLEDGE_BASE_ARGS_SCHEMA = z.object({
|
|
|
38
38
|
export const UPDATE_KNOWLEDGE_BASE_ARGS_SCHEMA = z.object({
|
|
39
39
|
topic: z.string().min(1),
|
|
40
40
|
content: z.string().min(1),
|
|
41
|
+
tags: z.array(z.string().min(1)).default([]),
|
|
41
42
|
});
|
|
42
43
|
export const ANALYZE_DATABASE_SCHEMA_ARGS_SCHEMA = z.object({
|
|
43
44
|
path: z.string().default("apps/backend"),
|
|
@@ -63,6 +64,7 @@ export const VALIDATE_REPOSITORY_HEALTH_ARGS_SCHEMA = z.object({
|
|
|
63
64
|
});
|
|
64
65
|
export const READ_AGENT_MESSAGES_ARGS_SCHEMA = z.object({
|
|
65
66
|
agent: z.string().min(1),
|
|
67
|
+
traceId: z.string().min(1).optional(),
|
|
66
68
|
});
|
|
67
69
|
export const VERIFY_CONTRACT_INTEGRITY_ARGS_SCHEMA = z.object({
|
|
68
70
|
domain: z.string().min(1),
|
|
@@ -4,6 +4,17 @@ import crypto from "crypto";
|
|
|
4
4
|
import { Project } from "ts-morph";
|
|
5
5
|
import { getFrameworkDir, collectFilesRecursively } from "../utils.js";
|
|
6
6
|
import { VERIFY_CONTRACT_INTEGRITY_ARGS_SCHEMA } from "../schemas.js";
|
|
7
|
+
function calculateContractHash(sharedTypesDir, projectRoot) {
|
|
8
|
+
const files = collectFilesRecursively(sharedTypesDir, new Set(["ts"])).sort();
|
|
9
|
+
const hash = crypto.createHash("sha256");
|
|
10
|
+
files.forEach((filePath) => {
|
|
11
|
+
hash.update(path.relative(projectRoot, filePath));
|
|
12
|
+
hash.update("\0");
|
|
13
|
+
hash.update(fs.readFileSync(filePath));
|
|
14
|
+
hash.update("\0");
|
|
15
|
+
});
|
|
16
|
+
return hash.digest("hex");
|
|
17
|
+
}
|
|
7
18
|
export const contractTools = [
|
|
8
19
|
{
|
|
9
20
|
name: "verify_api_contract",
|
|
@@ -37,10 +48,7 @@ export const contractHandlers = {
|
|
|
37
48
|
const contractJsonPath = path.join(projectRoot, "packages/shared-types/contract.version.json");
|
|
38
49
|
if (!fs.existsSync(sharedTypesDir) || !fs.existsSync(contractJsonPath))
|
|
39
50
|
return { content: [{ type: "text", text: "Missing shared-types directory or contract.version.json" }] };
|
|
40
|
-
const
|
|
41
|
-
const hash = crypto.createHash("sha256");
|
|
42
|
-
files.forEach(f => hash.update(fs.readFileSync(f)));
|
|
43
|
-
const currentHash = hash.digest("hex");
|
|
51
|
+
const currentHash = calculateContractHash(sharedTypesDir, projectRoot);
|
|
44
52
|
const storedHash = JSON.parse(fs.readFileSync(contractJsonPath, "utf-8")).contract_hash;
|
|
45
53
|
return { content: [{ type: "text", text: currentHash === storedHash ? "✅ MATCH: Contract is valid and synchronized." : `❌ MISMATCH: Current hash (${currentHash.slice(0, 8)}...) does not match stored hash (${storedHash.slice(0, 8)}...).` }] };
|
|
46
54
|
}
|
|
@@ -57,9 +65,7 @@ export const contractHandlers = {
|
|
|
57
65
|
const files = collectFilesRecursively(sharedTypesDir, new Set(["ts"])).sort();
|
|
58
66
|
if (files.length === 0)
|
|
59
67
|
return { content: [{ type: "text", text: "⚠️ WARNING: No TypeScript files found in shared-types/src. Hash not updated." }] };
|
|
60
|
-
const
|
|
61
|
-
files.forEach(f => hash.update(fs.readFileSync(f)));
|
|
62
|
-
const currentHash = hash.digest("hex");
|
|
68
|
+
const currentHash = calculateContractHash(sharedTypesDir, projectRoot);
|
|
63
69
|
const contractJson = fs.existsSync(contractJsonPath) ? JSON.parse(fs.readFileSync(contractJsonPath, "utf-8")) : {};
|
|
64
70
|
contractJson.contract_hash = currentHash;
|
|
65
71
|
contractJson.last_updated = new Date().toISOString();
|
|
@@ -90,7 +90,7 @@ export const knowledgeHandlers = {
|
|
|
90
90
|
if (!fs.existsSync(kbDir))
|
|
91
91
|
fs.mkdirSync(kbDir, { recursive: true });
|
|
92
92
|
const fileName = parsed.data.topic.replace(/[^a-z0-9]/gi, "_").toLowerCase() + ".md";
|
|
93
|
-
const tags = parsed.data.tags
|
|
93
|
+
const tags = parsed.data.tags;
|
|
94
94
|
let finalContent = parsed.data.content;
|
|
95
95
|
if (!finalContent.startsWith("---")) {
|
|
96
96
|
const frontmatter = `---\ntitle: ${parsed.data.topic}\ntags: [${tags.join(", ")}]\nlast_updated: ${new Date().toISOString()}\n---\n\n`;
|
|
@@ -42,6 +42,10 @@ export const messageTools = [
|
|
|
42
42
|
},
|
|
43
43
|
},
|
|
44
44
|
];
|
|
45
|
+
function normalizeAgentName(agent) {
|
|
46
|
+
const normalized = agent.replace(/^@/, "").trim();
|
|
47
|
+
return /^[a-z0-9_-]+$/i.test(normalized) ? normalized : null;
|
|
48
|
+
}
|
|
45
49
|
export const messageHandlers = {
|
|
46
50
|
send_agent_message: async (args, projectRoot) => {
|
|
47
51
|
const parsed = SEND_AGENT_MESSAGE_ARGS_SCHEMA.safeParse(args ?? {});
|
|
@@ -49,7 +53,9 @@ export const messageHandlers = {
|
|
|
49
53
|
return { content: [{ type: "text", text: "Invalid message arguments." }] };
|
|
50
54
|
try {
|
|
51
55
|
const frameworkDir = getFrameworkDir(projectRoot);
|
|
52
|
-
const recipient = parsed.data.to
|
|
56
|
+
const recipient = normalizeAgentName(parsed.data.to);
|
|
57
|
+
if (!recipient)
|
|
58
|
+
return { content: [{ type: "text", text: "Invalid recipient agent name." }] };
|
|
53
59
|
const messagesDir = path.join(projectRoot, frameworkDir, "messages");
|
|
54
60
|
if (!fs.existsSync(messagesDir))
|
|
55
61
|
fs.mkdirSync(messagesDir, { recursive: true });
|
|
@@ -77,14 +83,15 @@ export const messageHandlers = {
|
|
|
77
83
|
return { content: [{ type: "text", text: "Invalid agent name." }] };
|
|
78
84
|
try {
|
|
79
85
|
const frameworkDir = getFrameworkDir(projectRoot);
|
|
80
|
-
const agentName = parsed.data.agent
|
|
86
|
+
const agentName = normalizeAgentName(parsed.data.agent);
|
|
87
|
+
if (!agentName)
|
|
88
|
+
return { content: [{ type: "text", text: "Invalid agent name." }] };
|
|
81
89
|
const messagePath = path.join(projectRoot, frameworkDir, "messages", `${agentName}.json`);
|
|
82
90
|
if (!fs.existsSync(messagePath))
|
|
83
91
|
return { content: [{ type: "text", text: "No messages found." }] };
|
|
84
92
|
const messages = JSON.parse(fs.readFileSync(messagePath, "utf-8"));
|
|
85
|
-
const unread = messages.filter((m) => !m.read);
|
|
86
|
-
|
|
87
|
-
fs.writeFileSync(messagePath, JSON.stringify(messages.map((m) => ({ ...m, read: true })), null, 2));
|
|
93
|
+
const unread = messages.filter((m) => !m.read && (!parsed.data.traceId || m.traceId === parsed.data.traceId));
|
|
94
|
+
fs.writeFileSync(messagePath, JSON.stringify(messages.map((m) => (!m.read && (!parsed.data.traceId || m.traceId === parsed.data.traceId) ? { ...m, read: true } : m)), null, 2));
|
|
88
95
|
if (unread.length === 0)
|
|
89
96
|
return { content: [{ type: "text", text: "No new messages." }] };
|
|
90
97
|
const formatted = unread.map((m) => `- [${m.priority}] **${m.from}** (${m.category}): ${m.content} *(Trace: ${m.traceId})*`).join("\n");
|
|
@@ -100,7 +107,9 @@ export const messageHandlers = {
|
|
|
100
107
|
return { content: [{ type: "text", text: "Invalid agent name." }] };
|
|
101
108
|
try {
|
|
102
109
|
const frameworkDir = getFrameworkDir(projectRoot);
|
|
103
|
-
const agentName = parsed.data.agent
|
|
110
|
+
const agentName = normalizeAgentName(parsed.data.agent);
|
|
111
|
+
if (!agentName)
|
|
112
|
+
return { content: [{ type: "text", text: "Invalid agent name." }] };
|
|
104
113
|
const messagePath = path.join(projectRoot, frameworkDir, "messages", `${agentName}.json`);
|
|
105
114
|
if (!fs.existsSync(messagePath))
|
|
106
115
|
return { content: [{ type: "text", text: "Inbox empty." }] };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import {
|
|
3
|
+
import { spawnSync } from "child_process";
|
|
4
4
|
import { Project } from "ts-morph";
|
|
5
5
|
import { resolveSafePath } from "../utils.js";
|
|
6
6
|
import { VALIDATE_REPOSITORY_HEALTH_ARGS_SCHEMA, ANALYZE_DOCUMENTATION_DEBT_ARGS_SCHEMA } from "../schemas.js";
|
|
@@ -35,7 +35,28 @@ export const repositoryHandlers = {
|
|
|
35
35
|
if (!fs.existsSync(pkgPath))
|
|
36
36
|
return { content: [{ type: "text", text: "package.json not found." }] };
|
|
37
37
|
const scripts = JSON.parse(fs.readFileSync(pkgPath, "utf-8")).scripts || {};
|
|
38
|
-
const
|
|
38
|
+
const scriptCandidates = {
|
|
39
|
+
lint: ["lint", "enderun:lint"],
|
|
40
|
+
test: ["test", "enderun:test"],
|
|
41
|
+
build: ["build", "enderun:build"],
|
|
42
|
+
};
|
|
43
|
+
const runScript = (name) => {
|
|
44
|
+
const scriptName = scriptCandidates[name].find((candidate) => scripts[candidate]);
|
|
45
|
+
if (!scriptName)
|
|
46
|
+
return { name, script: "-", status: "SKIPPED", details: "No matching script found." };
|
|
47
|
+
const result = spawnSync("npm", ["run", scriptName], {
|
|
48
|
+
cwd: projectRoot,
|
|
49
|
+
encoding: "utf-8",
|
|
50
|
+
stdio: "pipe",
|
|
51
|
+
});
|
|
52
|
+
const output = `${result.stdout || ""}${result.stderr || ""}`.trim();
|
|
53
|
+
return {
|
|
54
|
+
name,
|
|
55
|
+
script: scriptName,
|
|
56
|
+
status: result.status === 0 ? "PASSED" : "FAILED",
|
|
57
|
+
details: output.split("\n").slice(-5).join("\n"),
|
|
58
|
+
};
|
|
59
|
+
};
|
|
39
60
|
const results = [];
|
|
40
61
|
if (scope === "full" || scope === "lint")
|
|
41
62
|
results.push(runScript("lint"));
|
|
@@ -43,7 +64,7 @@ export const repositoryHandlers = {
|
|
|
43
64
|
results.push(runScript("test"));
|
|
44
65
|
if (scope === "full" || scope === "build")
|
|
45
66
|
results.push(runScript("build"));
|
|
46
|
-
return { content: [{ type: "text", text: `### REPOSITORY HEALTH REPORT\n\n` + results.map(r => `- **${r.name.toUpperCase()}
|
|
67
|
+
return { content: [{ type: "text", text: `### REPOSITORY HEALTH REPORT\n\n` + results.map(r => `- **${r.name.toUpperCase()}** (${r.script}): ${r.status}${r.status === "FAILED" && r.details ? `\n ${r.details}` : ""}`).join("\n") }] };
|
|
47
68
|
}
|
|
48
69
|
catch (error) {
|
|
49
70
|
return { content: [{ type: "text", text: "Health validation failed." }] };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fs from "fs";
|
|
3
|
-
export const FRAMEWORK_VERSION = "0.5.
|
|
3
|
+
export const FRAMEWORK_VERSION = "0.5.2";
|
|
4
4
|
export function getFrameworkDir(projectRoot) {
|
|
5
|
-
const adapters = [".gemini", ".claude", ".cursor", ".
|
|
5
|
+
const adapters = [".gemini", ".claude", ".cursor", ".enderun", ".codex"];
|
|
6
6
|
for (const adp of adapters) {
|
|
7
7
|
const fullPath = path.join(projectRoot, adp);
|
|
8
8
|
if (fs.existsSync(fullPath) && fs.lstatSync(fullPath).isDirectory()) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-enderun-mcp",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Enterprise-grade MCP Server for AI Agent Framework",
|
|
5
5
|
"author": "Yusuf BEKAR",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"README.md"
|
|
27
27
|
],
|
|
28
28
|
"scripts": {
|
|
29
|
-
"build": "
|
|
29
|
+
"build": "tsc -p tsconfig.json",
|
|
30
30
|
"start": "node dist/index.js",
|
|
31
31
|
"dev": "tsx src/index.ts"
|
|
32
32
|
},
|
|
@@ -46,6 +46,7 @@ export const SEARCH_KNOWLEDGE_BASE_ARGS_SCHEMA = z.object({
|
|
|
46
46
|
export const UPDATE_KNOWLEDGE_BASE_ARGS_SCHEMA = z.object({
|
|
47
47
|
topic: z.string().min(1),
|
|
48
48
|
content: z.string().min(1),
|
|
49
|
+
tags: z.array(z.string().min(1)).default([]),
|
|
49
50
|
});
|
|
50
51
|
|
|
51
52
|
export const ANALYZE_DATABASE_SCHEMA_ARGS_SCHEMA = z.object({
|
|
@@ -79,6 +80,7 @@ export const VALIDATE_REPOSITORY_HEALTH_ARGS_SCHEMA = z.object({
|
|
|
79
80
|
|
|
80
81
|
export const READ_AGENT_MESSAGES_ARGS_SCHEMA = z.object({
|
|
81
82
|
agent: z.string().min(1),
|
|
83
|
+
traceId: z.string().min(1).optional(),
|
|
82
84
|
});
|
|
83
85
|
|
|
84
86
|
export const VERIFY_CONTRACT_INTEGRITY_ARGS_SCHEMA = z.object({
|
|
@@ -8,6 +8,18 @@ import {
|
|
|
8
8
|
} from "../utils.js";
|
|
9
9
|
import { VERIFY_CONTRACT_INTEGRITY_ARGS_SCHEMA } from "../schemas.js";
|
|
10
10
|
|
|
11
|
+
function calculateContractHash(sharedTypesDir: string, projectRoot: string): string {
|
|
12
|
+
const files = collectFilesRecursively(sharedTypesDir, new Set(["ts"])).sort();
|
|
13
|
+
const hash = crypto.createHash("sha256");
|
|
14
|
+
files.forEach((filePath) => {
|
|
15
|
+
hash.update(path.relative(projectRoot, filePath));
|
|
16
|
+
hash.update("\0");
|
|
17
|
+
hash.update(fs.readFileSync(filePath));
|
|
18
|
+
hash.update("\0");
|
|
19
|
+
});
|
|
20
|
+
return hash.digest("hex");
|
|
21
|
+
}
|
|
22
|
+
|
|
11
23
|
export const contractTools = [
|
|
12
24
|
{
|
|
13
25
|
name: "verify_api_contract",
|
|
@@ -41,10 +53,7 @@ export const contractHandlers = {
|
|
|
41
53
|
const sharedTypesDir = path.join(projectRoot, "packages/shared-types/src");
|
|
42
54
|
const contractJsonPath = path.join(projectRoot, "packages/shared-types/contract.version.json");
|
|
43
55
|
if (!fs.existsSync(sharedTypesDir) || !fs.existsSync(contractJsonPath)) return { content: [{ type: "text", text: "Missing shared-types directory or contract.version.json" }] };
|
|
44
|
-
const
|
|
45
|
-
const hash = crypto.createHash("sha256");
|
|
46
|
-
files.forEach(f => hash.update(fs.readFileSync(f)));
|
|
47
|
-
const currentHash = hash.digest("hex");
|
|
56
|
+
const currentHash = calculateContractHash(sharedTypesDir, projectRoot);
|
|
48
57
|
const storedHash = JSON.parse(fs.readFileSync(contractJsonPath, "utf-8")).contract_hash;
|
|
49
58
|
return { content: [{ type: "text", text: currentHash === storedHash ? "✅ MATCH: Contract is valid and synchronized." : `❌ MISMATCH: Current hash (${currentHash.slice(0, 8)}...) does not match stored hash (${storedHash.slice(0, 8)}...).` }] };
|
|
50
59
|
} catch (error) {
|
|
@@ -58,9 +67,7 @@ export const contractHandlers = {
|
|
|
58
67
|
if (!fs.existsSync(sharedTypesDir)) return { content: [{ type: "text", text: "Missing shared-types directory" }] };
|
|
59
68
|
const files = collectFilesRecursively(sharedTypesDir, new Set(["ts"])).sort();
|
|
60
69
|
if (files.length === 0) return { content: [{ type: "text", text: "⚠️ WARNING: No TypeScript files found in shared-types/src. Hash not updated." }] };
|
|
61
|
-
const
|
|
62
|
-
files.forEach(f => hash.update(fs.readFileSync(f)));
|
|
63
|
-
const currentHash = hash.digest("hex");
|
|
70
|
+
const currentHash = calculateContractHash(sharedTypesDir, projectRoot);
|
|
64
71
|
const contractJson = fs.existsSync(contractJsonPath) ? JSON.parse(fs.readFileSync(contractJsonPath, "utf-8")) : {};
|
|
65
72
|
contractJson.contract_hash = currentHash;
|
|
66
73
|
contractJson.last_updated = new Date().toISOString();
|
|
@@ -95,7 +95,7 @@ export const knowledgeHandlers = {
|
|
|
95
95
|
if (!fs.existsSync(kbDir)) fs.mkdirSync(kbDir, { recursive: true });
|
|
96
96
|
|
|
97
97
|
const fileName = parsed.data.topic.replace(/[^a-z0-9]/gi, "_").toLowerCase() + ".md";
|
|
98
|
-
const tags =
|
|
98
|
+
const tags = parsed.data.tags;
|
|
99
99
|
|
|
100
100
|
let finalContent = parsed.data.content;
|
|
101
101
|
if (!finalContent.startsWith("---")) {
|
|
@@ -58,13 +58,19 @@ interface Message {
|
|
|
58
58
|
read: boolean;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
function normalizeAgentName(agent: string): string | null {
|
|
62
|
+
const normalized = agent.replace(/^@/, "").trim();
|
|
63
|
+
return /^[a-z0-9_-]+$/i.test(normalized) ? normalized : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
61
66
|
export const messageHandlers = {
|
|
62
67
|
send_agent_message: async (args: unknown, projectRoot: string) => {
|
|
63
68
|
const parsed = SEND_AGENT_MESSAGE_ARGS_SCHEMA.safeParse(args ?? {});
|
|
64
69
|
if (!parsed.success) return { content: [{ type: "text", text: "Invalid message arguments." }] };
|
|
65
70
|
try {
|
|
66
71
|
const frameworkDir = getFrameworkDir(projectRoot);
|
|
67
|
-
const recipient = parsed.data.to
|
|
72
|
+
const recipient = normalizeAgentName(parsed.data.to);
|
|
73
|
+
if (!recipient) return { content: [{ type: "text", text: "Invalid recipient agent name." }] };
|
|
68
74
|
const messagesDir = path.join(projectRoot, frameworkDir, "messages");
|
|
69
75
|
if (!fs.existsSync(messagesDir)) fs.mkdirSync(messagesDir, { recursive: true });
|
|
70
76
|
const messagePath = path.join(messagesDir, `${recipient}.json`);
|
|
@@ -73,8 +79,8 @@ export const messageHandlers = {
|
|
|
73
79
|
timestamp: new Date().toISOString(),
|
|
74
80
|
from: "manager",
|
|
75
81
|
traceId: parsed.data.traceId,
|
|
76
|
-
category: parsed.data.category
|
|
77
|
-
priority: parsed.data.priority
|
|
82
|
+
category: parsed.data.category,
|
|
83
|
+
priority: parsed.data.priority,
|
|
78
84
|
content: parsed.data.message,
|
|
79
85
|
read: false
|
|
80
86
|
});
|
|
@@ -89,14 +95,16 @@ export const messageHandlers = {
|
|
|
89
95
|
if (!parsed.success) return { content: [{ type: "text", text: "Invalid agent name." }] };
|
|
90
96
|
try {
|
|
91
97
|
const frameworkDir = getFrameworkDir(projectRoot);
|
|
92
|
-
const agentName = parsed.data.agent
|
|
98
|
+
const agentName = normalizeAgentName(parsed.data.agent);
|
|
99
|
+
if (!agentName) return { content: [{ type: "text", text: "Invalid agent name." }] };
|
|
93
100
|
const messagePath = path.join(projectRoot, frameworkDir, "messages", `${agentName}.json`);
|
|
94
101
|
if (!fs.existsSync(messagePath)) return { content: [{ type: "text", text: "No messages found." }] };
|
|
95
102
|
const messages = JSON.parse(fs.readFileSync(messagePath, "utf-8")) as Message[];
|
|
96
|
-
const unread = messages.filter((m) => !m.read);
|
|
103
|
+
const unread = messages.filter((m) => !m.read && (!parsed.data.traceId || m.traceId === parsed.data.traceId));
|
|
97
104
|
|
|
98
|
-
|
|
99
|
-
|
|
105
|
+
fs.writeFileSync(messagePath, JSON.stringify(messages.map((m) => (
|
|
106
|
+
!m.read && (!parsed.data.traceId || m.traceId === parsed.data.traceId) ? { ...m, read: true } : m
|
|
107
|
+
)), null, 2));
|
|
100
108
|
|
|
101
109
|
if (unread.length === 0) return { content: [{ type: "text", text: "No new messages." }] };
|
|
102
110
|
|
|
@@ -114,7 +122,8 @@ export const messageHandlers = {
|
|
|
114
122
|
if (!parsed.success) return { content: [{ type: "text", text: "Invalid agent name." }] };
|
|
115
123
|
try {
|
|
116
124
|
const frameworkDir = getFrameworkDir(projectRoot);
|
|
117
|
-
const agentName = parsed.data.agent
|
|
125
|
+
const agentName = normalizeAgentName(parsed.data.agent);
|
|
126
|
+
if (!agentName) return { content: [{ type: "text", text: "Invalid agent name." }] };
|
|
118
127
|
const messagePath = path.join(projectRoot, frameworkDir, "messages", `${agentName}.json`);
|
|
119
128
|
if (!fs.existsSync(messagePath)) return { content: [{ type: "text", text: "Inbox empty." }] };
|
|
120
129
|
const messages = JSON.parse(fs.readFileSync(messagePath, "utf-8")) as Message[];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import {
|
|
3
|
+
import { spawnSync } from "child_process";
|
|
4
4
|
import { Project } from "ts-morph";
|
|
5
5
|
import {
|
|
6
6
|
resolveSafePath,
|
|
@@ -42,12 +42,33 @@ export const repositoryHandlers = {
|
|
|
42
42
|
const pkgPath = path.join(projectRoot, "package.json");
|
|
43
43
|
if (!fs.existsSync(pkgPath)) return { content: [{ type: "text", text: "package.json not found." }] };
|
|
44
44
|
const scripts = JSON.parse(fs.readFileSync(pkgPath, "utf-8")).scripts || {};
|
|
45
|
-
const
|
|
45
|
+
const scriptCandidates: Record<string, string[]> = {
|
|
46
|
+
lint: ["lint", "enderun:lint"],
|
|
47
|
+
test: ["test", "enderun:test"],
|
|
48
|
+
build: ["build", "enderun:build"],
|
|
49
|
+
};
|
|
50
|
+
const runScript = (name: string) => {
|
|
51
|
+
const scriptName = scriptCandidates[name].find((candidate) => scripts[candidate]);
|
|
52
|
+
if (!scriptName) return { name, script: "-", status: "SKIPPED", details: "No matching script found." };
|
|
53
|
+
|
|
54
|
+
const result = spawnSync("npm", ["run", scriptName], {
|
|
55
|
+
cwd: projectRoot,
|
|
56
|
+
encoding: "utf-8",
|
|
57
|
+
stdio: "pipe",
|
|
58
|
+
});
|
|
59
|
+
const output = `${result.stdout || ""}${result.stderr || ""}`.trim();
|
|
60
|
+
return {
|
|
61
|
+
name,
|
|
62
|
+
script: scriptName,
|
|
63
|
+
status: result.status === 0 ? "PASSED" : "FAILED",
|
|
64
|
+
details: output.split("\n").slice(-5).join("\n"),
|
|
65
|
+
};
|
|
66
|
+
};
|
|
46
67
|
const results = [];
|
|
47
68
|
if (scope === "full" || scope === "lint") results.push(runScript("lint"));
|
|
48
69
|
if (scope === "full" || scope === "test") results.push(runScript("test"));
|
|
49
70
|
if (scope === "full" || scope === "build") results.push(runScript("build"));
|
|
50
|
-
return { content: [{ type: "text", text: `### REPOSITORY HEALTH REPORT\n\n` + results.map(r => `- **${r.name.toUpperCase()}
|
|
71
|
+
return { content: [{ type: "text", text: `### REPOSITORY HEALTH REPORT\n\n` + results.map(r => `- **${r.name.toUpperCase()}** (${r.script}): ${r.status}${r.status === "FAILED" && r.details ? `\n ${r.details}` : ""}`).join("\n") }] };
|
|
51
72
|
} catch (error) {
|
|
52
73
|
return { content: [{ type: "text", text: "Health validation failed." }] };
|
|
53
74
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
|
|
4
|
-
export const FRAMEWORK_VERSION = "0.5.
|
|
4
|
+
export const FRAMEWORK_VERSION = "0.5.2";
|
|
5
5
|
|
|
6
6
|
export function getFrameworkDir(projectRoot: string): string {
|
|
7
|
-
const adapters = [".gemini", ".claude", ".cursor", ".
|
|
7
|
+
const adapters = [".gemini", ".claude", ".cursor", ".enderun", ".codex"];
|
|
8
8
|
for (const adp of adapters) {
|
|
9
9
|
const fullPath = path.join(projectRoot, adp);
|
|
10
10
|
if (fs.existsSync(fullPath) && fs.lstatSync(fullPath).isDirectory()) {
|
|
File without changes
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": "0.1.11",
|
|
3
|
-
"last_updated": "2026-05-
|
|
4
|
-
"contract_hash": "
|
|
3
|
+
"last_updated": "2026-05-16T16:47:16.000Z",
|
|
4
|
+
"contract_hash": "8574144a23e68351f48365f5ccb3d69dc11629af54ec9c1ed0fffd57f967b777",
|
|
5
5
|
"breaking_changes": [
|
|
6
6
|
{
|
|
7
7
|
"version": "0.1.11",
|
|
@@ -9,4 +9,4 @@
|
|
|
9
9
|
}
|
|
10
10
|
],
|
|
11
11
|
"deprecated_versions": []
|
|
12
|
-
}
|
|
12
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-enderun/shared-types",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Shared TypeScript types for AI-Enderun Framework. Ensures Contract-First synchronization between agents.",
|
|
5
5
|
"author": "Yusuf BEKAR",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
"README.md"
|
|
24
24
|
],
|
|
25
25
|
"scripts": {
|
|
26
|
-
"build": "
|
|
27
|
-
"build:watch": "
|
|
28
|
-
"typecheck": "
|
|
26
|
+
"build": "tsc -p tsconfig.json",
|
|
27
|
+
"build:watch": "tsc -p tsconfig.json --watch",
|
|
28
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
29
29
|
},
|
|
30
30
|
"license": "MIT",
|
|
31
31
|
"devDependencies": {
|
|
File without changes
|
|
File without changes
|
package/panda.config.ts
CHANGED
|
@@ -5,7 +5,11 @@ export default defineConfig({
|
|
|
5
5
|
preflight: true,
|
|
6
6
|
|
|
7
7
|
// Where to look for your css declarations
|
|
8
|
-
include: [
|
|
8
|
+
include: [
|
|
9
|
+
"./src/**/*.{js,jsx,ts,tsx}",
|
|
10
|
+
"./pages/**/*.{js,jsx,ts,tsx}",
|
|
11
|
+
"./apps/**/*.{js,jsx,ts,tsx}",
|
|
12
|
+
],
|
|
9
13
|
|
|
10
14
|
// Files to exclude
|
|
11
15
|
exclude: [],
|
package/tsconfig.json
CHANGED
|
File without changes
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|