agent-enderun 0.4.7 → 0.5.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/PROJECT_MEMORY.md +15 -1
- package/.enderun/docs/tech-stack.md +38 -10
- package/.enderun/knowledge/branded_types_pattern.md +7 -0
- package/.enderun/knowledge/hermes_protocol.md +59 -0
- package/.enderun/knowledge/zero_ui_library_policy.md +8 -4
- package/ENDERUN.md +11 -6
- package/README.md +62 -51
- package/bin/cli.js +8 -10
- package/bin/update-contract.js +44 -0
- package/claude.md +4 -1
- package/codex.md +4 -1
- package/cursor.md +4 -1
- package/gemini.md +4 -1
- package/package.json +10 -8
- package/packages/framework-mcp/dist/index.js +2 -1
- package/packages/framework-mcp/dist/schemas.js +14 -0
- package/packages/framework-mcp/dist/tools/framework.js +25 -0
- package/packages/framework-mcp/dist/tools/knowledge.js +75 -11
- package/packages/framework-mcp/dist/tools/memory.js +3 -1
- package/packages/framework-mcp/dist/tools/messages.js +61 -8
- package/packages/framework-mcp/dist/tools/repository.js +6 -2
- package/packages/framework-mcp/dist/utils.js +3 -3
- package/packages/framework-mcp/package.json +1 -1
- package/packages/framework-mcp/src/index.ts +2 -1
- package/packages/framework-mcp/src/schemas.ts +18 -0
- package/packages/framework-mcp/src/tools/academy.ts +17 -11
- package/packages/framework-mcp/src/tools/codebase.ts +5 -5
- package/packages/framework-mcp/src/tools/contract.ts +3 -3
- package/packages/framework-mcp/src/tools/database.ts +1 -1
- package/packages/framework-mcp/src/tools/framework.ts +29 -5
- package/packages/framework-mcp/src/tools/git.ts +1 -1
- package/packages/framework-mcp/src/tools/index.ts +1 -1
- package/packages/framework-mcp/src/tools/knowledge.ts +88 -14
- package/packages/framework-mcp/src/tools/memory.ts +5 -3
- package/packages/framework-mcp/src/tools/messages.ts +81 -13
- package/packages/framework-mcp/src/tools/repository.ts +8 -3
- package/packages/framework-mcp/src/tools/security.ts +2 -2
- package/packages/framework-mcp/src/utils.ts +3 -3
- package/packages/shared-types/contract.version.json +7 -4
- package/packages/shared-types/package.json +1 -1
- package/.enderun/ENDERUN.md +0 -205
package/codex.md
CHANGED
|
@@ -4,4 +4,7 @@ This file is the entry point for Codex. The project's "Supreme Law" and all inst
|
|
|
4
4
|
|
|
5
5
|
👉 **[{{FRAMEWORK_DIR}}/ENDERUN.md]({{FRAMEWORK_DIR}}/ENDERUN.md)**
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
### 🤖 AGENT DIRECTIVE
|
|
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.
|
|
9
|
+
|
|
10
|
+
Please read the Supreme Law thoroughly before taking any action.
|
package/cursor.md
CHANGED
|
@@ -4,4 +4,7 @@ This file is the entry point for Cursor. The project's "Supreme Law" and all ins
|
|
|
4
4
|
|
|
5
5
|
👉 **[{{FRAMEWORK_DIR}}/ENDERUN.md]({{FRAMEWORK_DIR}}/ENDERUN.md)**
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
### 🤖 AGENT DIRECTIVE
|
|
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.
|
|
9
|
+
|
|
10
|
+
Please read the Supreme Law thoroughly before taking any action.
|
package/gemini.md
CHANGED
|
@@ -4,4 +4,7 @@ This file is the entry point for Gemini. The project's "Supreme Law" and all ins
|
|
|
4
4
|
|
|
5
5
|
👉 **[{{FRAMEWORK_DIR}}/ENDERUN.md]({{FRAMEWORK_DIR}}/ENDERUN.md)**
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
### 🤖 AGENT DIRECTIVE
|
|
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.
|
|
9
|
+
|
|
10
|
+
Please read the Supreme Law thoroughly before taking any action.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-enderun",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "The Supreme AI Governance & Orchestration Framework for Enterprise Development",
|
|
5
5
|
"author": "Yusuf BEKAR",
|
|
6
6
|
"license": "MIT",
|
|
@@ -61,22 +61,24 @@
|
|
|
61
61
|
"enderun:trace": "agent-enderun trace:new",
|
|
62
62
|
"enderun:verify": "agent-enderun verify-contract",
|
|
63
63
|
"enderun:check": "agent-enderun check",
|
|
64
|
+
"lint": "eslint .",
|
|
64
65
|
"clean": "rm -rf node_modules packages/*/node_modules apps/*/node_modules packages/*/dist apps/*/dist"
|
|
65
66
|
},
|
|
66
67
|
"publishConfig": {
|
|
67
68
|
"access": "public"
|
|
68
69
|
},
|
|
69
|
-
"dependencies": {},
|
|
70
70
|
"devDependencies": {
|
|
71
|
+
"@eslint/js": "^10.0.1",
|
|
71
72
|
"@types/node": "^22.13.4",
|
|
72
|
-
"
|
|
73
|
-
"tsx": "^4.19.4",
|
|
74
|
-
"vitest": "^3.0.5",
|
|
73
|
+
"concurrently": "^9.1.2",
|
|
75
74
|
"eslint": "^9.20.1",
|
|
76
|
-
"
|
|
75
|
+
"tsx": "^4.19.4",
|
|
76
|
+
"typescript": "^5.9.3",
|
|
77
|
+
"typescript-eslint": "^8.59.3",
|
|
78
|
+
"vitest": "^3.0.5"
|
|
77
79
|
},
|
|
78
80
|
"enderun": {
|
|
79
|
-
"version": "0.
|
|
81
|
+
"version": "0.5.1",
|
|
80
82
|
"initializedAt": "2026-05-09T13:24:27.472Z"
|
|
81
83
|
}
|
|
82
|
-
}
|
|
84
|
+
}
|
|
@@ -2,9 +2,10 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
3
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
4
4
|
import { allTools, allHandlers } from "./tools/index.js";
|
|
5
|
+
import { FRAMEWORK_VERSION } from "./utils.js";
|
|
5
6
|
const server = new Server({
|
|
6
7
|
name: "ai-enderun-mcp",
|
|
7
|
-
version:
|
|
8
|
+
version: FRAMEWORK_VERSION,
|
|
8
9
|
}, {
|
|
9
10
|
capabilities: {
|
|
10
11
|
tools: {},
|
|
@@ -29,6 +29,8 @@ export const SEND_AGENT_MESSAGE_ARGS_SCHEMA = z.object({
|
|
|
29
29
|
to: z.string().min(1),
|
|
30
30
|
message: z.string().min(1),
|
|
31
31
|
traceId: z.string().min(1),
|
|
32
|
+
category: z.enum(["ACTION", "DELEGATION", "INFO", "ALERT"]).default("INFO"),
|
|
33
|
+
priority: z.enum(["LOW", "MEDIUM", "HIGH", "URGENT"]).default("MEDIUM"),
|
|
32
34
|
});
|
|
33
35
|
export const SEARCH_KNOWLEDGE_BASE_ARGS_SCHEMA = z.object({
|
|
34
36
|
query: z.string().min(1),
|
|
@@ -82,3 +84,15 @@ export const GET_AGENT_AUDIT_REPORT_ARGS_SCHEMA = z.object({
|
|
|
82
84
|
agent: z.string().min(1),
|
|
83
85
|
days: z.number().default(7),
|
|
84
86
|
});
|
|
87
|
+
export const GET_AGENT_INBOX_STATS_ARGS_SCHEMA = z.object({
|
|
88
|
+
agent: z.string().min(1),
|
|
89
|
+
});
|
|
90
|
+
export const GET_KNOWLEDGE_GRAPH_ARGS_SCHEMA = z.object({
|
|
91
|
+
tag: z.string().optional(),
|
|
92
|
+
});
|
|
93
|
+
export const SYNC_CONTRACT_HASH_ARGS_SCHEMA = z.object({
|
|
94
|
+
force: z.boolean().default(false),
|
|
95
|
+
});
|
|
96
|
+
export const VERIFY_FRAMEWORK_HEALTH_ARGS_SCHEMA = z.object({
|
|
97
|
+
detailed: z.boolean().default(false),
|
|
98
|
+
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import { execSync } from "child_process";
|
|
3
4
|
import { getFrameworkDir, collectMarkdownArtifacts, FRAMEWORK_VERSION } from "../utils.js";
|
|
5
|
+
import { VERIFY_FRAMEWORK_HEALTH_ARGS_SCHEMA } from "../schemas.js";
|
|
4
6
|
export const frameworkTools = [
|
|
5
7
|
{
|
|
6
8
|
name: "get_framework_status",
|
|
@@ -37,6 +39,16 @@ export const frameworkTools = [
|
|
|
37
39
|
description: "Analyzes a pre-existing codebase to automatically generate the initial project memory, learning its tech stack and architecture.",
|
|
38
40
|
inputSchema: { type: "object", properties: {} },
|
|
39
41
|
},
|
|
42
|
+
{
|
|
43
|
+
name: "verify_framework_health",
|
|
44
|
+
description: "Runs the framework's internal 'check' command and reports system health status via MCP.",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: {
|
|
48
|
+
detailed: { type: "boolean", default: false, description: "Include detailed issues if any." },
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
40
52
|
];
|
|
41
53
|
export const frameworkHandlers = {
|
|
42
54
|
bootstrap_legacy_memory: async (args, projectRoot) => {
|
|
@@ -158,4 +170,17 @@ export const frameworkHandlers = {
|
|
|
158
170
|
get_system_time: async () => {
|
|
159
171
|
return { content: [{ type: "text", text: new Date().toISOString() }] };
|
|
160
172
|
},
|
|
173
|
+
verify_framework_health: async (args, projectRoot) => {
|
|
174
|
+
const parsed = VERIFY_FRAMEWORK_HEALTH_ARGS_SCHEMA.safeParse(args ?? {});
|
|
175
|
+
try {
|
|
176
|
+
// Run the internal CLI check command via tsx to ensure we test current state
|
|
177
|
+
// If the CLI is not yet linked, we might need a relative path
|
|
178
|
+
const cliPath = path.join(projectRoot, "bin/cli.js");
|
|
179
|
+
const output = execSync(`node ${cliPath} check`, { cwd: projectRoot, encoding: "utf-8", stdio: "pipe" });
|
|
180
|
+
return { content: [{ type: "text", text: `### FRAMEWORK HEALTH CHECK\n\n${output}` }] };
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
return { content: [{ type: "text", text: `### FRAMEWORK HEALTH CHECK (FAILED)\n\n${error.stdout || error.message}` }] };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
161
186
|
};
|
|
@@ -5,28 +5,52 @@ import { SEARCH_KNOWLEDGE_BASE_ARGS_SCHEMA, UPDATE_KNOWLEDGE_BASE_ARGS_SCHEMA }
|
|
|
5
5
|
export const knowledgeTools = [
|
|
6
6
|
{
|
|
7
7
|
name: "search_knowledge_base",
|
|
8
|
-
description: "Searches the Academy's
|
|
8
|
+
description: "Searches the Academy's Obsidian-style knowledge base using keywords or tags.",
|
|
9
9
|
inputSchema: {
|
|
10
10
|
type: "object",
|
|
11
11
|
properties: {
|
|
12
|
-
query: { type: "string", description: "Search query or
|
|
12
|
+
query: { type: "string", description: "Search query, topic, or tag (e.g. #security)" },
|
|
13
13
|
},
|
|
14
14
|
required: ["query"],
|
|
15
15
|
},
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
name: "update_knowledge_base",
|
|
19
|
-
description: "Adds or updates an entry
|
|
19
|
+
description: "Adds or updates an entry with mandatory YAML frontmatter for Obsidian compatibility.",
|
|
20
20
|
inputSchema: {
|
|
21
21
|
type: "object",
|
|
22
22
|
properties: {
|
|
23
|
-
topic: { type: "string", description: "The topic or title
|
|
24
|
-
content: { type: "string", description: "
|
|
23
|
+
topic: { type: "string", description: "The topic or title" },
|
|
24
|
+
content: { type: "string", description: "Content (YAML frontmatter will be auto-generated if missing)" },
|
|
25
|
+
tags: { type: "array", items: { type: "string" }, description: "Tags for the entry" },
|
|
25
26
|
},
|
|
26
27
|
required: ["topic", "content"],
|
|
27
28
|
},
|
|
28
29
|
},
|
|
30
|
+
{
|
|
31
|
+
name: "get_knowledge_graph",
|
|
32
|
+
description: "Generates a Mermaid diagram showing relationships between knowledge entries based on 'related' metadata.",
|
|
33
|
+
inputSchema: {
|
|
34
|
+
type: "object",
|
|
35
|
+
properties: {
|
|
36
|
+
tag: { type: "string", description: "Filter by tag" },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
29
40
|
];
|
|
41
|
+
function parseFrontmatter(content) {
|
|
42
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
43
|
+
if (!match)
|
|
44
|
+
return { metadata: {}, body: content };
|
|
45
|
+
const yaml = match[1];
|
|
46
|
+
const metadata = {};
|
|
47
|
+
yaml.split("\n").forEach(line => {
|
|
48
|
+
const [key, ...val] = line.split(":");
|
|
49
|
+
if (key && val.length > 0)
|
|
50
|
+
metadata[key.trim()] = val.join(":").trim();
|
|
51
|
+
});
|
|
52
|
+
return { metadata, body: content.replace(match[0], "").trim() };
|
|
53
|
+
}
|
|
30
54
|
export const knowledgeHandlers = {
|
|
31
55
|
search_knowledge_base: async (args, projectRoot) => {
|
|
32
56
|
const parsed = SEARCH_KNOWLEDGE_BASE_ARGS_SCHEMA.safeParse(args ?? {});
|
|
@@ -37,14 +61,20 @@ export const knowledgeHandlers = {
|
|
|
37
61
|
const kbDir = path.join(projectRoot, frameworkDir, "knowledge");
|
|
38
62
|
if (!fs.existsSync(kbDir))
|
|
39
63
|
return { content: [{ type: "text", text: "Knowledge base is empty." }] };
|
|
64
|
+
const query = parsed.data.query.toLowerCase();
|
|
40
65
|
const results = fs.readdirSync(kbDir).filter(f => f.endsWith(".md")).map(file => {
|
|
41
66
|
const content = fs.readFileSync(path.join(kbDir, file), "utf-8");
|
|
42
|
-
|
|
43
|
-
|
|
67
|
+
const { metadata, body } = parseFrontmatter(content);
|
|
68
|
+
const matchesQuery = body.toLowerCase().includes(query) ||
|
|
69
|
+
file.toLowerCase().includes(query) ||
|
|
70
|
+
(metadata.tags && metadata.tags.toLowerCase().includes(query)) ||
|
|
71
|
+
(metadata.title && metadata.title.toLowerCase().includes(query));
|
|
72
|
+
if (matchesQuery) {
|
|
73
|
+
return `### ${metadata.title || file.replace(".md", "")}\n**Tags:** ${metadata.tags || "None"}\n\n${body.slice(0, 300)}...`;
|
|
44
74
|
}
|
|
45
75
|
return null;
|
|
46
76
|
}).filter(Boolean);
|
|
47
|
-
return { content: [{ type: "text", text: results.length > 0 ? `### KNOWLEDGE BASE SEARCH RESULTS\n\n${results.join("\n\n---\n\n")}` : "No matching knowledge
|
|
77
|
+
return { content: [{ type: "text", text: results.length > 0 ? `### KNOWLEDGE BASE SEARCH RESULTS\n\n${results.join("\n\n---\n\n")}` : "No matching knowledge entries found." }] };
|
|
48
78
|
}
|
|
49
79
|
catch (error) {
|
|
50
80
|
return { content: [{ type: "text", text: "Knowledge base search failed." }] };
|
|
@@ -53,17 +83,51 @@ export const knowledgeHandlers = {
|
|
|
53
83
|
update_knowledge_base: async (args, projectRoot) => {
|
|
54
84
|
const parsed = UPDATE_KNOWLEDGE_BASE_ARGS_SCHEMA.safeParse(args ?? {});
|
|
55
85
|
if (!parsed.success)
|
|
56
|
-
return { content: [{ type: "text", text: "Invalid
|
|
86
|
+
return { content: [{ type: "text", text: "Invalid arguments." }] };
|
|
57
87
|
try {
|
|
58
88
|
const frameworkDir = getFrameworkDir(projectRoot);
|
|
59
89
|
const kbDir = path.join(projectRoot, frameworkDir, "knowledge");
|
|
60
90
|
if (!fs.existsSync(kbDir))
|
|
61
91
|
fs.mkdirSync(kbDir, { recursive: true });
|
|
62
|
-
|
|
63
|
-
|
|
92
|
+
const fileName = parsed.data.topic.replace(/[^a-z0-9]/gi, "_").toLowerCase() + ".md";
|
|
93
|
+
const tags = parsed.data.tags || [];
|
|
94
|
+
let finalContent = parsed.data.content;
|
|
95
|
+
if (!finalContent.startsWith("---")) {
|
|
96
|
+
const frontmatter = `---\ntitle: ${parsed.data.topic}\ntags: [${tags.join(", ")}]\nlast_updated: ${new Date().toISOString()}\n---\n\n`;
|
|
97
|
+
finalContent = frontmatter + finalContent;
|
|
98
|
+
}
|
|
99
|
+
fs.writeFileSync(path.join(kbDir, fileName), finalContent);
|
|
100
|
+
return { content: [{ type: "text", text: `Obsidian Wiki updated: ${parsed.data.topic}` }] };
|
|
64
101
|
}
|
|
65
102
|
catch (error) {
|
|
66
103
|
return { content: [{ type: "text", text: "Failed to update knowledge base." }] };
|
|
67
104
|
}
|
|
68
105
|
},
|
|
106
|
+
get_knowledge_graph: async (args, projectRoot) => {
|
|
107
|
+
try {
|
|
108
|
+
const frameworkDir = getFrameworkDir(projectRoot);
|
|
109
|
+
const kbDir = path.join(projectRoot, frameworkDir, "knowledge");
|
|
110
|
+
if (!fs.existsSync(kbDir))
|
|
111
|
+
return { content: [{ type: "text", text: "Knowledge base empty." }] };
|
|
112
|
+
const files = fs.readdirSync(kbDir).filter(f => f.endsWith(".md"));
|
|
113
|
+
let mermaid = "graph TD\n";
|
|
114
|
+
files.forEach(file => {
|
|
115
|
+
const content = fs.readFileSync(path.join(kbDir, file), "utf-8");
|
|
116
|
+
const { metadata } = parseFrontmatter(content);
|
|
117
|
+
const id = file.replace(".md", "");
|
|
118
|
+
const label = metadata.title || id;
|
|
119
|
+
mermaid += ` ${id}["${label}"]\n`;
|
|
120
|
+
if (metadata.related) {
|
|
121
|
+
const related = metadata.related.replace(/[\[\]]/g, "").split(",");
|
|
122
|
+
related.forEach((r) => {
|
|
123
|
+
mermaid += ` ${id} --> ${r.trim().replace(".md", "")}\n`;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
return { content: [{ type: "text", text: `### KNOWLEDGE GRAPH (Mermaid)\n\n\`\`\`mermaid\n${mermaid}\n\`\`\`` }] };
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
return { content: [{ type: "text", text: "Failed to generate knowledge graph." }] };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
69
133
|
};
|
|
@@ -1,28 +1,42 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { getFrameworkDir } from "../utils.js";
|
|
4
|
-
import { SEND_AGENT_MESSAGE_ARGS_SCHEMA, READ_AGENT_MESSAGES_ARGS_SCHEMA } from "../schemas.js";
|
|
4
|
+
import { SEND_AGENT_MESSAGE_ARGS_SCHEMA, READ_AGENT_MESSAGES_ARGS_SCHEMA, GET_AGENT_INBOX_STATS_ARGS_SCHEMA } from "../schemas.js";
|
|
5
5
|
export const messageTools = [
|
|
6
6
|
{
|
|
7
7
|
name: "send_agent_message",
|
|
8
|
-
description: "Sends a message to another specialized agent
|
|
8
|
+
description: "Sends a message to another specialized agent following the Hermes Protocol.",
|
|
9
9
|
inputSchema: {
|
|
10
10
|
type: "object",
|
|
11
11
|
properties: {
|
|
12
12
|
to: { type: "string", description: "Recipient agent name" },
|
|
13
13
|
message: { type: "string", description: "The message content" },
|
|
14
14
|
traceId: { type: "string", description: "The active Trace ID" },
|
|
15
|
+
category: { type: "string", enum: ["ACTION", "DELEGATION", "INFO", "ALERT"], default: "INFO" },
|
|
16
|
+
priority: { type: "string", enum: ["LOW", "MEDIUM", "HIGH", "URGENT"], default: "MEDIUM" },
|
|
15
17
|
},
|
|
16
18
|
required: ["to", "message", "traceId"],
|
|
17
19
|
},
|
|
18
20
|
},
|
|
19
21
|
{
|
|
20
22
|
name: "read_agent_messages",
|
|
21
|
-
description: "Reads messages sent to the current agent.",
|
|
23
|
+
description: "Reads messages sent to the current agent. Filters by Trace ID if provided.",
|
|
22
24
|
inputSchema: {
|
|
23
25
|
type: "object",
|
|
24
26
|
properties: {
|
|
25
27
|
agent: { type: "string", description: "Current agent name reading their messages" },
|
|
28
|
+
traceId: { type: "string", description: "Optional Trace ID to filter messages" },
|
|
29
|
+
},
|
|
30
|
+
required: ["agent"],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "get_agent_inbox_stats",
|
|
35
|
+
description: "Returns statistics about the agent's inbox (total, unread, priority distribution).",
|
|
36
|
+
inputSchema: {
|
|
37
|
+
type: "object",
|
|
38
|
+
properties: {
|
|
39
|
+
agent: { type: "string", description: "Agent name" },
|
|
26
40
|
},
|
|
27
41
|
required: ["agent"],
|
|
28
42
|
},
|
|
@@ -40,13 +54,21 @@ export const messageHandlers = {
|
|
|
40
54
|
if (!fs.existsSync(messagesDir))
|
|
41
55
|
fs.mkdirSync(messagesDir, { recursive: true });
|
|
42
56
|
const messagePath = path.join(messagesDir, `${recipient}.json`);
|
|
43
|
-
|
|
44
|
-
messages.push({
|
|
57
|
+
const messages = (fs.existsSync(messagePath) ? JSON.parse(fs.readFileSync(messagePath, "utf-8")) : []);
|
|
58
|
+
messages.push({
|
|
59
|
+
timestamp: new Date().toISOString(),
|
|
60
|
+
from: "manager",
|
|
61
|
+
traceId: parsed.data.traceId,
|
|
62
|
+
category: parsed.data.category,
|
|
63
|
+
priority: parsed.data.priority,
|
|
64
|
+
content: parsed.data.message,
|
|
65
|
+
read: false
|
|
66
|
+
});
|
|
45
67
|
fs.writeFileSync(messagePath, JSON.stringify(messages, null, 2));
|
|
46
|
-
return { content: [{ type: "text", text: `Message sent to @${recipient}.` }] };
|
|
68
|
+
return { content: [{ type: "text", text: `Hermes: Message sent to @${recipient} [${parsed.data.category} | ${parsed.data.priority}].` }] };
|
|
47
69
|
}
|
|
48
70
|
catch (error) {
|
|
49
|
-
return { content: [{ type: "text", text: "Failed to send message." }] };
|
|
71
|
+
return { content: [{ type: "text", text: "Failed to send message via Hermes." }] };
|
|
50
72
|
}
|
|
51
73
|
},
|
|
52
74
|
read_agent_messages: async (args, projectRoot) => {
|
|
@@ -61,11 +83,42 @@ export const messageHandlers = {
|
|
|
61
83
|
return { content: [{ type: "text", text: "No messages found." }] };
|
|
62
84
|
const messages = JSON.parse(fs.readFileSync(messagePath, "utf-8"));
|
|
63
85
|
const unread = messages.filter((m) => !m.read);
|
|
86
|
+
// Mark as read
|
|
64
87
|
fs.writeFileSync(messagePath, JSON.stringify(messages.map((m) => ({ ...m, read: true })), null, 2));
|
|
65
|
-
|
|
88
|
+
if (unread.length === 0)
|
|
89
|
+
return { content: [{ type: "text", text: "No new messages." }] };
|
|
90
|
+
const formatted = unread.map((m) => `- [${m.priority}] **${m.from}** (${m.category}): ${m.content} *(Trace: ${m.traceId})*`).join("\n");
|
|
91
|
+
return { content: [{ type: "text", text: `### HERMES INBOX: @${agentName}\n\n${formatted}` }] };
|
|
66
92
|
}
|
|
67
93
|
catch (error) {
|
|
68
94
|
return { content: [{ type: "text", text: "Failed to read messages." }] };
|
|
69
95
|
}
|
|
70
96
|
},
|
|
97
|
+
get_agent_inbox_stats: async (args, projectRoot) => {
|
|
98
|
+
const parsed = GET_AGENT_INBOX_STATS_ARGS_SCHEMA.safeParse(args ?? {});
|
|
99
|
+
if (!parsed.success)
|
|
100
|
+
return { content: [{ type: "text", text: "Invalid agent name." }] };
|
|
101
|
+
try {
|
|
102
|
+
const frameworkDir = getFrameworkDir(projectRoot);
|
|
103
|
+
const agentName = parsed.data.agent.replace(/^@/, "");
|
|
104
|
+
const messagePath = path.join(projectRoot, frameworkDir, "messages", `${agentName}.json`);
|
|
105
|
+
if (!fs.existsSync(messagePath))
|
|
106
|
+
return { content: [{ type: "text", text: "Inbox empty." }] };
|
|
107
|
+
const messages = JSON.parse(fs.readFileSync(messagePath, "utf-8"));
|
|
108
|
+
const unread = messages.filter(m => !m.read);
|
|
109
|
+
const priorityDist = messages.reduce((acc, m) => {
|
|
110
|
+
acc[m.priority] = (acc[m.priority] || 0) + 1;
|
|
111
|
+
return acc;
|
|
112
|
+
}, {});
|
|
113
|
+
return {
|
|
114
|
+
content: [{
|
|
115
|
+
type: "text",
|
|
116
|
+
text: `### INBOX STATS: @${agentName}\n- Total: ${messages.length}\n- Unread: ${unread.length}\n- Priority Distribution: ${JSON.stringify(priorityDist)}`
|
|
117
|
+
}]
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
return { content: [{ type: "text", text: "Failed to get inbox stats." }] };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
71
124
|
};
|
|
@@ -62,8 +62,12 @@ export const repositoryHandlers = {
|
|
|
62
62
|
if (relativePath.includes("node_modules") || relativePath.includes("dist"))
|
|
63
63
|
continue;
|
|
64
64
|
sourceFile.getExportedDeclarations().forEach((declarations, name) => {
|
|
65
|
-
declarations.forEach(decl => {
|
|
66
|
-
|
|
65
|
+
declarations.forEach(decl => {
|
|
66
|
+
const d = decl;
|
|
67
|
+
if (typeof d.getJsDocs === "function" && d.getJsDocs().length === 0) {
|
|
68
|
+
missingJSDoc.push(`${relativePath} -> ${name}`);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
67
71
|
});
|
|
68
72
|
}
|
|
69
73
|
const majorDirs = ["apps/backend", "apps/web", "packages/shared-types", "packages/framework-mcp"], missingREADME = majorDirs.filter(dir => fs.existsSync(path.join(projectRoot, dir)) && !fs.existsSync(path.join(projectRoot, dir, "README.md")));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fs from "fs";
|
|
3
|
-
export const FRAMEWORK_VERSION = "0.
|
|
3
|
+
export const FRAMEWORK_VERSION = "0.5.1";
|
|
4
4
|
export function getFrameworkDir(projectRoot) {
|
|
5
5
|
const adapters = [".gemini", ".claude", ".cursor", ".codex", ".enderun"];
|
|
6
6
|
for (const adp of adapters) {
|
|
@@ -71,7 +71,7 @@ export function collectMarkdownArtifacts(projectRoot) {
|
|
|
71
71
|
}
|
|
72
72
|
export function replaceSectionContent(markdown, sectionTitle, newBody) {
|
|
73
73
|
const escaped = sectionTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
74
|
-
const sectionRegex = new RegExp(`## ${escaped}[\\s\\S]*?(?=\\n## |$)
|
|
74
|
+
const sectionRegex = new RegExp(`## ${escaped}[\\s\\S]*?(?=\\n## |$)`);
|
|
75
75
|
if (!sectionRegex.test(markdown)) {
|
|
76
76
|
throw new Error(`Section not found: ${sectionTitle}`);
|
|
77
77
|
}
|
|
@@ -79,7 +79,7 @@ export function replaceSectionContent(markdown, sectionTitle, newBody) {
|
|
|
79
79
|
}
|
|
80
80
|
export function prependToSection(markdown, sectionTitle, contentToPrepend) {
|
|
81
81
|
const escaped = sectionTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
82
|
-
const sectionRegex = new RegExp(`(## ${escaped}\\n)([\\s\\S]*?)(?=\\n## |$)
|
|
82
|
+
const sectionRegex = new RegExp(`(## ${escaped}\\n)([\\s\\S]*?)(?=\\n## |$)`);
|
|
83
83
|
const match = markdown.match(sectionRegex);
|
|
84
84
|
if (!match) {
|
|
85
85
|
throw new Error(`Section not found: ${sectionTitle}`);
|
|
@@ -2,10 +2,11 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
3
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
4
4
|
import { allTools, allHandlers } from "./tools/index.js";
|
|
5
|
+
import { FRAMEWORK_VERSION } from "./utils.js";
|
|
5
6
|
|
|
6
7
|
const server = new Server({
|
|
7
8
|
name: "ai-enderun-mcp",
|
|
8
|
-
version:
|
|
9
|
+
version: FRAMEWORK_VERSION,
|
|
9
10
|
}, {
|
|
10
11
|
capabilities: {
|
|
11
12
|
tools: {},
|
|
@@ -35,6 +35,8 @@ export const SEND_AGENT_MESSAGE_ARGS_SCHEMA = z.object({
|
|
|
35
35
|
to: z.string().min(1),
|
|
36
36
|
message: z.string().min(1),
|
|
37
37
|
traceId: z.string().min(1),
|
|
38
|
+
category: z.enum(["ACTION", "DELEGATION", "INFO", "ALERT"]).default("INFO"),
|
|
39
|
+
priority: z.enum(["LOW", "MEDIUM", "HIGH", "URGENT"]).default("MEDIUM"),
|
|
38
40
|
});
|
|
39
41
|
|
|
40
42
|
export const SEARCH_KNOWLEDGE_BASE_ARGS_SCHEMA = z.object({
|
|
@@ -104,3 +106,19 @@ export const GET_AGENT_AUDIT_REPORT_ARGS_SCHEMA = z.object({
|
|
|
104
106
|
agent: z.string().min(1),
|
|
105
107
|
days: z.number().default(7),
|
|
106
108
|
});
|
|
109
|
+
|
|
110
|
+
export const GET_AGENT_INBOX_STATS_ARGS_SCHEMA = z.object({
|
|
111
|
+
agent: z.string().min(1),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
export const GET_KNOWLEDGE_GRAPH_ARGS_SCHEMA = z.object({
|
|
115
|
+
tag: z.string().optional(),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
export const SYNC_CONTRACT_HASH_ARGS_SCHEMA = z.object({
|
|
119
|
+
force: z.boolean().default(false),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
export const VERIFY_FRAMEWORK_HEALTH_ARGS_SCHEMA = z.object({
|
|
123
|
+
detailed: z.boolean().default(false),
|
|
124
|
+
});
|
|
@@ -89,8 +89,14 @@ export const academyTools = [
|
|
|
89
89
|
},
|
|
90
90
|
];
|
|
91
91
|
|
|
92
|
+
interface AgentLog {
|
|
93
|
+
timestamp: string;
|
|
94
|
+
status: string;
|
|
95
|
+
[key: string]: unknown;
|
|
96
|
+
}
|
|
97
|
+
|
|
92
98
|
export const academyHandlers = {
|
|
93
|
-
get_academy_performance: async (args:
|
|
99
|
+
get_academy_performance: async (args: unknown, projectRoot: string) => {
|
|
94
100
|
const parsed = GET_ACADEMY_PERFORMANCE_ARGS_SCHEMA.safeParse(args ?? {});
|
|
95
101
|
const days = parsed.success ? parsed.data.periodDays : 30;
|
|
96
102
|
try {
|
|
@@ -100,8 +106,8 @@ export const academyHandlers = {
|
|
|
100
106
|
const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
|
101
107
|
let totalActions = 0, totalSuccess = 0;
|
|
102
108
|
const agentStats = fs.readdirSync(logsDir).filter(f => f.endsWith(".json")).map(file => {
|
|
103
|
-
const logs = JSON.parse(fs.readFileSync(path.join(logsDir, file), "utf-8")).filter((l
|
|
104
|
-
const success = logs.filter((l
|
|
109
|
+
const logs = (JSON.parse(fs.readFileSync(path.join(logsDir, file), "utf-8")) as AgentLog[]).filter((l) => new Date(l.timestamp) >= cutoff);
|
|
110
|
+
const success = logs.filter((l) => l.status === "SUCCESS").length;
|
|
105
111
|
totalActions += logs.length; totalSuccess += success;
|
|
106
112
|
return { agent: file.replace(".json", ""), actions: logs.length, successRate: (success / (logs.length || 1)) * 100 };
|
|
107
113
|
}).filter(s => s.actions > 0);
|
|
@@ -110,15 +116,15 @@ export const academyHandlers = {
|
|
|
110
116
|
return { content: [{ type: "text", text: "Failed to generate global performance report." }] };
|
|
111
117
|
}
|
|
112
118
|
},
|
|
113
|
-
generate_strategic_briefing: async (args:
|
|
119
|
+
generate_strategic_briefing: async (args: unknown, projectRoot: string) => {
|
|
114
120
|
try {
|
|
115
121
|
const frameworkDir = getFrameworkDir(projectRoot);
|
|
116
122
|
const logsDir = path.join(projectRoot, frameworkDir, "logs");
|
|
117
123
|
let totalActions = 0, successCount = 0;
|
|
118
124
|
if (fs.existsSync(logsDir)) {
|
|
119
125
|
fs.readdirSync(logsDir).filter(f => f.endsWith(".json")).forEach(file => {
|
|
120
|
-
const logs = JSON.parse(fs.readFileSync(path.join(logsDir, file), "utf-8"));
|
|
121
|
-
totalActions += logs.length; successCount += logs.filter((l
|
|
126
|
+
const logs = JSON.parse(fs.readFileSync(path.join(logsDir, file), "utf-8")) as AgentLog[];
|
|
127
|
+
totalActions += logs.length; successCount += logs.filter((l) => l.status === "SUCCESS").length;
|
|
122
128
|
});
|
|
123
129
|
}
|
|
124
130
|
const successRate = (successCount / (totalActions || 1)) * 100;
|
|
@@ -127,7 +133,7 @@ export const academyHandlers = {
|
|
|
127
133
|
return { content: [{ type: "text", text: "Strategic briefing generation failed." }] };
|
|
128
134
|
}
|
|
129
135
|
},
|
|
130
|
-
generate_academy_progress_report: async (args:
|
|
136
|
+
generate_academy_progress_report: async (args: unknown, projectRoot: string) => {
|
|
131
137
|
const parsed = GENERATE_ACADEMY_PROGRESS_REPORT_ARGS_SCHEMA.safeParse(args ?? {});
|
|
132
138
|
const days = parsed.success ? parsed.data.days : 7;
|
|
133
139
|
try {
|
|
@@ -141,7 +147,7 @@ export const academyHandlers = {
|
|
|
141
147
|
return { content: [{ type: "text", text: "Failed to generate progress report." }] };
|
|
142
148
|
}
|
|
143
149
|
},
|
|
144
|
-
get_agent_audit_report: async (args:
|
|
150
|
+
get_agent_audit_report: async (args: unknown, projectRoot: string) => {
|
|
145
151
|
const parsed = GET_AGENT_AUDIT_REPORT_ARGS_SCHEMA.safeParse(args ?? {});
|
|
146
152
|
if (!parsed.success) return { content: [{ type: "text", text: "Invalid agent or days argument." }] };
|
|
147
153
|
try {
|
|
@@ -149,14 +155,14 @@ export const academyHandlers = {
|
|
|
149
155
|
const logPath = path.join(projectRoot, frameworkDir, "logs", `${parsed.data.agent}.json`);
|
|
150
156
|
if (!fs.existsSync(logPath)) return { content: [{ type: "text", text: `No logs found for agent: ${parsed.data.agent}` }] };
|
|
151
157
|
const cutoff = new Date(Date.now() - parsed.data.days * 24 * 60 * 60 * 1000);
|
|
152
|
-
const logs = JSON.parse(fs.readFileSync(logPath, "utf-8")).filter((l
|
|
153
|
-
const successCount = logs.filter((l
|
|
158
|
+
const logs = (JSON.parse(fs.readFileSync(logPath, "utf-8")) as AgentLog[]).filter((l) => new Date(l.timestamp) >= cutoff);
|
|
159
|
+
const successCount = logs.filter((l) => l.status === "SUCCESS").length;
|
|
154
160
|
return { content: [{ type: "text", text: `### AGENT AUDIT REPORT: ${parsed.data.agent.toUpperCase()}\n\n- **Total Actions:** ${logs.length}\n- **Success Rate:** ${((successCount / (logs.length || 1)) * 100).toFixed(1)}%` }] };
|
|
155
161
|
} catch (error) {
|
|
156
162
|
return { content: [{ type: "text", text: "Failed to generate audit report." }] };
|
|
157
163
|
}
|
|
158
164
|
},
|
|
159
|
-
log_agent_action: async (args:
|
|
165
|
+
log_agent_action: async (args: unknown, projectRoot: string) => {
|
|
160
166
|
const parsed = LOG_AGENT_ACTION_ARGS_SCHEMA.safeParse(args ?? {});
|
|
161
167
|
if (!parsed.success) return { content: [{ type: "text", text: "Invalid arguments for log_agent_action." }] };
|
|
162
168
|
try {
|
|
@@ -134,7 +134,7 @@ export const codebaseTools = [
|
|
|
134
134
|
];
|
|
135
135
|
|
|
136
136
|
export const codebaseHandlers = {
|
|
137
|
-
search_codebase: async (args:
|
|
137
|
+
search_codebase: async (args: unknown, projectRoot: string) => {
|
|
138
138
|
const parsed = SEARCH_CODEBASE_ARGS_SCHEMA.safeParse(args);
|
|
139
139
|
if (!parsed.success) {
|
|
140
140
|
return { content: [{ type: "text", text: "Invalid query/extension argument." }] };
|
|
@@ -168,7 +168,7 @@ export const codebaseHandlers = {
|
|
|
168
168
|
return { content: [{ type: "text", text: "Search failed." }] };
|
|
169
169
|
}
|
|
170
170
|
},
|
|
171
|
-
analyze_dependencies: async (args:
|
|
171
|
+
analyze_dependencies: async (args: unknown, projectRoot: string) => {
|
|
172
172
|
const parsed = ANALYZE_DEPENDENCIES_ARGS_SCHEMA.safeParse(args);
|
|
173
173
|
if (!parsed.success) {
|
|
174
174
|
return { content: [{ type: "text", text: "Invalid path argument." }] };
|
|
@@ -198,7 +198,7 @@ export const codebaseHandlers = {
|
|
|
198
198
|
return { content: [{ type: "text", text: "Analysis failed: " + (error instanceof Error ? error.message : String(error)) }] };
|
|
199
199
|
}
|
|
200
200
|
},
|
|
201
|
-
analyze_codebase_intelligence: async (args:
|
|
201
|
+
analyze_codebase_intelligence: async (args: unknown, projectRoot: string) => {
|
|
202
202
|
const parsed = ANALYZE_CODEBASE_INTELLIGENCE_ARGS_SCHEMA.safeParse(args);
|
|
203
203
|
const targetPath = parsed.success ? parsed.data.path : ".";
|
|
204
204
|
try {
|
|
@@ -227,7 +227,7 @@ export const codebaseHandlers = {
|
|
|
227
227
|
return { content: [{ type: "text", text: "Intelligence scan failed." }] };
|
|
228
228
|
}
|
|
229
229
|
},
|
|
230
|
-
analyze_procedural_continuity: async (args:
|
|
230
|
+
analyze_procedural_continuity: async (args: unknown, projectRoot: string) => {
|
|
231
231
|
const parsed = ANALYZE_PROCEDURAL_CONTINUITY_ARGS_SCHEMA.safeParse(args);
|
|
232
232
|
if (!parsed.success) return { content: [{ type: "text", text: "Invalid targetPath or referencePath." }] };
|
|
233
233
|
try {
|
|
@@ -251,7 +251,7 @@ export const codebaseHandlers = {
|
|
|
251
251
|
return { content: [{ type: "text", text: "Continuity analysis failed." }] };
|
|
252
252
|
}
|
|
253
253
|
},
|
|
254
|
-
generate_dependency_graph: async (args:
|
|
254
|
+
generate_dependency_graph: async (args: unknown, projectRoot: string) => {
|
|
255
255
|
const parsed = GENERATE_DEPENDENCY_GRAPH_ARGS_SCHEMA.safeParse(args);
|
|
256
256
|
const targetPath = parsed.success ? parsed.data.path : "src";
|
|
257
257
|
const format = parsed.success ? parsed.data.format : "mermaid";
|