agent-enderun 0.5.0 → 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.
@@ -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 internal knowledge base for architectural patterns, troubleshooting guides, and FAQs.",
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 topic" },
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 in the Academy's internal knowledge base.",
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 of the entry" },
24
- content: { type: "string", description: "The technical content or guide" },
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
- if (content.toLowerCase().includes(parsed.data.query.toLowerCase()) || file.toLowerCase().includes(parsed.data.query.toLowerCase())) {
43
- return `### ${file.replace(".md", "")}\n\n${content.slice(0, 300)}...`;
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 base entries found." }] };
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 topic or content." }] };
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
- fs.writeFileSync(path.join(kbDir, parsed.data.topic.replace(/[^a-z0-9]/gi, "_").toLowerCase() + ".md"), parsed.data.content);
63
- return { content: [{ type: "text", text: `Knowledge base updated: ${parsed.data.topic}` }] };
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. Useful for collaboration and delegation.",
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
  },
@@ -41,12 +55,20 @@ export const messageHandlers = {
41
55
  fs.mkdirSync(messagesDir, { recursive: true });
42
56
  const messagePath = path.join(messagesDir, `${recipient}.json`);
43
57
  const messages = (fs.existsSync(messagePath) ? JSON.parse(fs.readFileSync(messagePath, "utf-8")) : []);
44
- messages.push({ timestamp: new Date().toISOString(), from: "manager", traceId: parsed.data.traceId, content: parsed.data.message, read: false });
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
- return { content: [{ type: "text", text: unread.length === 0 ? "No new messages." : `### INBOX: @${agentName}\n\n` + unread.map((m) => `- **From:** ${m.from}\n **Message:** ${m.content}`).join("\n\n") }] };
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
  };
@@ -1,6 +1,6 @@
1
1
  import path from "path";
2
2
  import fs from "fs";
3
- export const FRAMEWORK_VERSION = "0.5.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) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-enderun-mcp",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Enterprise-grade MCP Server for AI Agent Framework",
5
5
  "author": "Yusuf BEKAR",
6
6
  "license": "MIT",
@@ -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
+ });
@@ -1,10 +1,12 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
+ import { execSync } from "child_process";
3
4
  import {
4
5
  getFrameworkDir,
5
6
  collectMarkdownArtifacts,
6
7
  FRAMEWORK_VERSION
7
8
  } from "../utils.js";
9
+ import { VERIFY_FRAMEWORK_HEALTH_ARGS_SCHEMA } from "../schemas.js";
8
10
 
9
11
  export const frameworkTools = [
10
12
  {
@@ -42,6 +44,16 @@ export const frameworkTools = [
42
44
  description: "Analyzes a pre-existing codebase to automatically generate the initial project memory, learning its tech stack and architecture.",
43
45
  inputSchema: { type: "object", properties: {} },
44
46
  },
47
+ {
48
+ name: "verify_framework_health",
49
+ description: "Runs the framework's internal 'check' command and reports system health status via MCP.",
50
+ inputSchema: {
51
+ type: "object",
52
+ properties: {
53
+ detailed: { type: "boolean", default: false, description: "Include detailed issues if any." },
54
+ },
55
+ },
56
+ },
45
57
  ];
46
58
 
47
59
  export const frameworkHandlers = {
@@ -154,4 +166,16 @@ export const frameworkHandlers = {
154
166
  get_system_time: async () => {
155
167
  return { content: [{ type: "text", text: new Date().toISOString() }] };
156
168
  },
169
+ verify_framework_health: async (args: unknown, projectRoot: string) => {
170
+ const parsed = VERIFY_FRAMEWORK_HEALTH_ARGS_SCHEMA.safeParse(args ?? {});
171
+ try {
172
+ // Run the internal CLI check command via tsx to ensure we test current state
173
+ // If the CLI is not yet linked, we might need a relative path
174
+ const cliPath = path.join(projectRoot, "bin/cli.js");
175
+ const output = execSync(`node ${cliPath} check`, { cwd: projectRoot, encoding: "utf-8", stdio: "pipe" });
176
+ return { content: [{ type: "text", text: `### FRAMEWORK HEALTH CHECK\n\n${output}` }] };
177
+ } catch (error: any) {
178
+ return { content: [{ type: "text", text: `### FRAMEWORK HEALTH CHECK (FAILED)\n\n${error.stdout || error.message}` }] };
179
+ }
180
+ }
157
181
  };
@@ -3,35 +3,59 @@ import path from "path";
3
3
  import { getFrameworkDir } from "../utils.js";
4
4
  import {
5
5
  SEARCH_KNOWLEDGE_BASE_ARGS_SCHEMA,
6
- UPDATE_KNOWLEDGE_BASE_ARGS_SCHEMA
6
+ UPDATE_KNOWLEDGE_BASE_ARGS_SCHEMA,
7
+ GET_KNOWLEDGE_GRAPH_ARGS_SCHEMA
7
8
  } from "../schemas.js";
8
9
 
9
10
  export const knowledgeTools = [
10
11
  {
11
12
  name: "search_knowledge_base",
12
- description: "Searches the Academy's internal knowledge base for architectural patterns, troubleshooting guides, and FAQs.",
13
+ description: "Searches the Academy's Obsidian-style knowledge base using keywords or tags.",
13
14
  inputSchema: {
14
15
  type: "object",
15
16
  properties: {
16
- query: { type: "string", description: "Search query or topic" },
17
+ query: { type: "string", description: "Search query, topic, or tag (e.g. #security)" },
17
18
  },
18
19
  required: ["query"],
19
20
  },
20
21
  },
21
22
  {
22
23
  name: "update_knowledge_base",
23
- description: "Adds or updates an entry in the Academy's internal knowledge base.",
24
+ description: "Adds or updates an entry with mandatory YAML frontmatter for Obsidian compatibility.",
24
25
  inputSchema: {
25
26
  type: "object",
26
27
  properties: {
27
- topic: { type: "string", description: "The topic or title of the entry" },
28
- content: { type: "string", description: "The technical content or guide" },
28
+ topic: { type: "string", description: "The topic or title" },
29
+ content: { type: "string", description: "Content (YAML frontmatter will be auto-generated if missing)" },
30
+ tags: { type: "array", items: { type: "string" }, description: "Tags for the entry" },
29
31
  },
30
32
  required: ["topic", "content"],
31
33
  },
32
34
  },
35
+ {
36
+ name: "get_knowledge_graph",
37
+ description: "Generates a Mermaid diagram showing relationships between knowledge entries based on 'related' metadata.",
38
+ inputSchema: {
39
+ type: "object",
40
+ properties: {
41
+ tag: { type: "string", description: "Filter by tag" },
42
+ },
43
+ },
44
+ },
33
45
  ];
34
46
 
47
+ function parseFrontmatter(content: string) {
48
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
49
+ if (!match) return { metadata: {}, body: content };
50
+ const yaml = match[1];
51
+ const metadata: Record<string, any> = {};
52
+ yaml.split("\n").forEach(line => {
53
+ const [key, ...val] = line.split(":");
54
+ if (key && val.length > 0) metadata[key.trim()] = val.join(":").trim();
55
+ });
56
+ return { metadata, body: content.replace(match[0], "").trim() };
57
+ }
58
+
35
59
  export const knowledgeHandlers = {
36
60
  search_knowledge_base: async (args: unknown, projectRoot: string) => {
37
61
  const parsed = SEARCH_KNOWLEDGE_BASE_ARGS_SCHEMA.safeParse(args ?? {});
@@ -40,29 +64,79 @@ export const knowledgeHandlers = {
40
64
  const frameworkDir = getFrameworkDir(projectRoot);
41
65
  const kbDir = path.join(projectRoot, frameworkDir, "knowledge");
42
66
  if (!fs.existsSync(kbDir)) return { content: [{ type: "text", text: "Knowledge base is empty." }] };
67
+
68
+ const query = parsed.data.query.toLowerCase();
43
69
  const results = fs.readdirSync(kbDir).filter(f => f.endsWith(".md")).map(file => {
44
70
  const content = fs.readFileSync(path.join(kbDir, file), "utf-8");
45
- if (content.toLowerCase().includes(parsed.data.query.toLowerCase()) || file.toLowerCase().includes(parsed.data.query.toLowerCase())) {
46
- return `### ${file.replace(".md", "")}\n\n${content.slice(0, 300)}...`;
71
+ const { metadata, body } = parseFrontmatter(content);
72
+
73
+ const matchesQuery = body.toLowerCase().includes(query) ||
74
+ file.toLowerCase().includes(query) ||
75
+ (metadata.tags && metadata.tags.toLowerCase().includes(query)) ||
76
+ (metadata.title && metadata.title.toLowerCase().includes(query));
77
+
78
+ if (matchesQuery) {
79
+ return `### ${metadata.title || file.replace(".md", "")}\n**Tags:** ${metadata.tags || "None"}\n\n${body.slice(0, 300)}...`;
47
80
  }
48
81
  return null;
49
82
  }).filter(Boolean);
50
- return { content: [{ type: "text", text: results.length > 0 ? `### KNOWLEDGE BASE SEARCH RESULTS\n\n${results.join("\n\n---\n\n")}` : "No matching knowledge base entries found." }] };
83
+
84
+ 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." }] };
51
85
  } catch (error) {
52
86
  return { content: [{ type: "text", text: "Knowledge base search failed." }] };
53
87
  }
54
88
  },
55
89
  update_knowledge_base: async (args: unknown, projectRoot: string) => {
56
90
  const parsed = UPDATE_KNOWLEDGE_BASE_ARGS_SCHEMA.safeParse(args ?? {});
57
- if (!parsed.success) return { content: [{ type: "text", text: "Invalid topic or content." }] };
91
+ if (!parsed.success) return { content: [{ type: "text", text: "Invalid arguments." }] };
58
92
  try {
59
93
  const frameworkDir = getFrameworkDir(projectRoot);
60
94
  const kbDir = path.join(projectRoot, frameworkDir, "knowledge");
61
95
  if (!fs.existsSync(kbDir)) fs.mkdirSync(kbDir, { recursive: true });
62
- fs.writeFileSync(path.join(kbDir, parsed.data.topic.replace(/[^a-z0-9]/gi, "_").toLowerCase() + ".md"), parsed.data.content);
63
- return { content: [{ type: "text", text: `Knowledge base updated: ${parsed.data.topic}` }] };
96
+
97
+ const fileName = parsed.data.topic.replace(/[^a-z0-9]/gi, "_").toLowerCase() + ".md";
98
+ const tags = (parsed as any).data.tags || [];
99
+
100
+ let finalContent = parsed.data.content;
101
+ if (!finalContent.startsWith("---")) {
102
+ const frontmatter = `---\ntitle: ${parsed.data.topic}\ntags: [${tags.join(", ")}]\nlast_updated: ${new Date().toISOString()}\n---\n\n`;
103
+ finalContent = frontmatter + finalContent;
104
+ }
105
+
106
+ fs.writeFileSync(path.join(kbDir, fileName), finalContent);
107
+ return { content: [{ type: "text", text: `Obsidian Wiki updated: ${parsed.data.topic}` }] };
64
108
  } catch (error) {
65
109
  return { content: [{ type: "text", text: "Failed to update knowledge base." }] };
66
110
  }
67
111
  },
112
+ get_knowledge_graph: async (args: unknown, projectRoot: string) => {
113
+ try {
114
+ const frameworkDir = getFrameworkDir(projectRoot);
115
+ const kbDir = path.join(projectRoot, frameworkDir, "knowledge");
116
+ if (!fs.existsSync(kbDir)) return { content: [{ type: "text", text: "Knowledge base empty." }] };
117
+
118
+ const files = fs.readdirSync(kbDir).filter(f => f.endsWith(".md"));
119
+ let mermaid = "graph TD\n";
120
+
121
+ files.forEach(file => {
122
+ const content = fs.readFileSync(path.join(kbDir, file), "utf-8");
123
+ const { metadata } = parseFrontmatter(content);
124
+ const id = file.replace(".md", "");
125
+ const label = metadata.title || id;
126
+
127
+ mermaid += ` ${id}["${label}"]\n`;
128
+
129
+ if (metadata.related) {
130
+ const related = metadata.related.replace(/[\[\]]/g, "").split(",");
131
+ related.forEach((r: string) => {
132
+ mermaid += ` ${id} --> ${r.trim().replace(".md", "")}\n`;
133
+ });
134
+ }
135
+ });
136
+
137
+ return { content: [{ type: "text", text: `### KNOWLEDGE GRAPH (Mermaid)\n\n\`\`\`mermaid\n${mermaid}\n\`\`\`` }] };
138
+ } catch (error) {
139
+ return { content: [{ type: "text", text: "Failed to generate knowledge graph." }] };
140
+ }
141
+ }
68
142
  };