agent-enderun 0.9.4 → 0.9.5

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.
Files changed (38) hide show
  1. package/.enderun/ENDERUN.md +1 -1
  2. package/.enderun/STATUS.md +2 -2
  3. package/.enderun/config.json +1 -1
  4. package/.enderun/memory-graph/shared-facts.json +1 -1
  5. package/framework-mcp/dist/index.js +42 -240
  6. package/framework-mcp/dist/tools/definitions.js +109 -0
  7. package/framework-mcp/dist/tools/file_system/patch_file.js +18 -0
  8. package/framework-mcp/dist/tools/file_system/read_file.js +7 -0
  9. package/framework-mcp/dist/tools/file_system/replace_text.js +14 -0
  10. package/framework-mcp/dist/tools/file_system/write_file.js +9 -0
  11. package/framework-mcp/dist/tools/framework/get_status.js +5 -0
  12. package/framework-mcp/dist/tools/framework/orchestrate.js +5 -0
  13. package/framework-mcp/dist/tools/framework/update_contract_hash.js +5 -0
  14. package/framework-mcp/dist/tools/framework/update_memory.js +8 -0
  15. package/framework-mcp/dist/tools/index.js +25 -0
  16. package/framework-mcp/dist/tools/messaging/log_action.js +22 -0
  17. package/framework-mcp/dist/tools/messaging/send_message.js +21 -0
  18. package/framework-mcp/dist/tools/types.js +1 -0
  19. package/framework-mcp/dist/utils/cli.js +20 -0
  20. package/framework-mcp/dist/utils/security.js +35 -0
  21. package/framework-mcp/src/declarations.d.ts +17 -3
  22. package/framework-mcp/src/index.ts +49 -277
  23. package/framework-mcp/src/tools/definitions.ts +111 -0
  24. package/framework-mcp/src/tools/file_system/patch_file.ts +23 -0
  25. package/framework-mcp/src/tools/file_system/read_file.ts +9 -0
  26. package/framework-mcp/src/tools/file_system/replace_text.ts +18 -0
  27. package/framework-mcp/src/tools/file_system/write_file.ts +11 -0
  28. package/framework-mcp/src/tools/framework/get_status.ts +7 -0
  29. package/framework-mcp/src/tools/framework/orchestrate.ts +7 -0
  30. package/framework-mcp/src/tools/framework/update_contract_hash.ts +7 -0
  31. package/framework-mcp/src/tools/framework/update_memory.ts +10 -0
  32. package/framework-mcp/src/tools/index.ts +31 -0
  33. package/framework-mcp/src/tools/messaging/log_action.ts +28 -0
  34. package/framework-mcp/src/tools/messaging/send_message.ts +26 -0
  35. package/framework-mcp/src/tools/types.ts +40 -0
  36. package/framework-mcp/src/utils/cli.ts +20 -0
  37. package/framework-mcp/src/utils/security.ts +41 -0
  38. package/package.json +2 -2
@@ -0,0 +1,35 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+ /**
4
+ * Validates and resolves a user-provided path to prevent path traversal attacks.
5
+ * Ensures the resolved path stays within the project root boundary.
6
+ */
7
+ export function safePath(projectRoot, userPath) {
8
+ const resolved = path.resolve(projectRoot, userPath);
9
+ const normalizedRoot = path.resolve(projectRoot);
10
+ if (!resolved.startsWith(normalizedRoot + path.sep) && resolved !== normalizedRoot) {
11
+ throw new Error(`Access denied: path "${userPath}" escapes project root.`);
12
+ }
13
+ return resolved;
14
+ }
15
+ /**
16
+ * Resolves the active framework directory by scanning known candidates.
17
+ */
18
+ export function resolveFrameworkDir(projectRoot) {
19
+ const candidates = [
20
+ ".gemini/antigravity",
21
+ ".gemini/antigravity-cli",
22
+ ".gemini",
23
+ ".claude",
24
+ ".grok",
25
+ ".agent",
26
+ ".enderun",
27
+ ];
28
+ for (const candidate of candidates) {
29
+ const candidatePath = path.join(projectRoot, candidate);
30
+ if (fs.existsSync(candidatePath)) {
31
+ return candidate;
32
+ }
33
+ }
34
+ return ".enderun";
35
+ }
@@ -1,3 +1,17 @@
1
- declare module "@modelcontextprotocol/sdk/server/index.js";
2
- declare module "@modelcontextprotocol/sdk/server/stdio.js";
3
- declare module "@modelcontextprotocol/sdk/types.js";
1
+ declare module "@modelcontextprotocol/sdk/server/index.js" {
2
+ export class Server {
3
+ constructor(info: { name: string; version: string }, options: { capabilities: { tools: Record<string, never> } });
4
+ setRequestHandler(schema: unknown, handler: (request: unknown) => Promise<unknown>): void;
5
+ connect(transport: unknown): Promise<void>;
6
+ close(): Promise<void>;
7
+ }
8
+ }
9
+
10
+ declare module "@modelcontextprotocol/sdk/server/stdio.js" {
11
+ export class StdioServerTransport {}
12
+ }
13
+
14
+ declare module "@modelcontextprotocol/sdk/types.js" {
15
+ export const ListToolsRequestSchema: unknown;
16
+ export const CallToolRequestSchema: unknown;
17
+ }
@@ -4,9 +4,12 @@ import {
4
4
  CallToolRequestSchema,
5
5
  ListToolsRequestSchema,
6
6
  } from "@modelcontextprotocol/sdk/types.js";
7
- import fs from "fs";
8
- import path from "path";
9
- import { execSync } from "child_process";
7
+
8
+ import { CallToolRequest, ToolResult } from "./tools/types.js";
9
+ import { TOOLS, toolHandlers } from "./tools/index.js";
10
+
11
+
12
+ // ─── Server Setup ─────────────────────────────────────────────────
10
13
 
11
14
  const server = new Server(
12
15
  {
@@ -20,297 +23,66 @@ const server = new Server(
20
23
  }
21
24
  );
22
25
 
23
- const TOOLS: any[] = [
24
- {
25
- name: "read_file",
26
- description: "Read the content of a file within the project.",
27
- inputSchema: {
28
- type: "object",
29
- properties: {
30
- path: { type: "string", description: "Path to the file relative to project root" },
31
- },
32
- required: ["path"],
33
- },
34
- },
35
- {
36
- name: "write_file",
37
- description: "Write content to a file. Creates directories if missing.",
38
- inputSchema: {
39
- type: "object",
40
- properties: {
41
- path: { type: "string", description: "Path to the file relative to project root" },
42
- content: { type: "string", description: "Complete content of the file" },
43
- },
44
- required: ["path", "content"],
45
- },
46
- },
47
- {
48
- name: "replace_text",
49
- description: "Surgically replace a string in a file with another string.",
50
- inputSchema: {
51
- type: "object",
52
- properties: {
53
- path: { type: "string", description: "Path to the file" },
54
- oldText: { type: "string", description: "The exact text to find" },
55
- newText: { type: "string", description: "The text to replace it with" },
56
- },
57
- required: ["path", "oldText", "newText"],
58
- },
59
- },
60
- {
61
- name: "patch_file",
62
- description: "Safely update a file by replacing a specific line range with new content.",
63
- inputSchema: {
64
- type: "object",
65
- properties: {
66
- path: { type: "string", description: "Path to the file" },
67
- startLine: { type: "number", description: "Starting line number (1-indexed)" },
68
- endLine: { type: "number", description: "Ending line number (inclusive)" },
69
- newContent: { type: "string", description: "The new lines to insert" },
70
- },
71
- required: ["path", "startLine", "endLine", "newContent"],
72
- },
73
- },
74
- {
75
- name: "get_framework_status",
76
- description: "Get the current project phase, active traces, and agent states.",
77
- inputSchema: { type: "object", properties: {} },
78
- },
79
- {
80
- name: "update_project_memory",
81
- description: "Update a specific section in PROJECT_MEMORY.md.",
82
- inputSchema: {
83
- type: "object",
84
- properties: {
85
- section: { type: "string", description: "Section name (e.g., HISTORY, ACTIVE TASKS)" },
86
- content: { type: "string", description: "Markdown content to append or set" },
87
- },
88
- required: ["section", "content"],
89
- },
90
- },
91
- {
92
- name: "orchestrate_loop",
93
- description: "Process the pending Hermes messages and trigger dynamic state transitions.",
94
- inputSchema: { type: "object", properties: {} },
95
- },
96
- {
97
- name: "send_agent_message",
98
- description: "Send a Hermes protocol message to another agent.",
99
- inputSchema: {
100
- type: "object",
101
- properties: {
102
- to: { type: "string", description: "Target agent (e.g., @backend, @qa)" },
103
- category: { type: "string", enum: ["ACTION", "DELEGATION", "INFO", "ALERT"] },
104
- content: { type: "string", description: "Message content" },
105
- traceId: { type: "string", description: "Active Trace ID" },
106
- },
107
- required: ["to", "category", "content", "traceId"],
108
- },
109
- },
110
- {
111
- name: "log_agent_action",
112
- description: "Log an agent action to the framework logs.",
113
- inputSchema: {
114
- type: "object",
115
- properties: {
116
- agent: { type: "string", description: "The agent name (e.g., @manager, @backend)" },
117
- action: { type: "string", description: "Action type or name" },
118
- traceId: { type: "string", description: "The active Trace ID" },
119
- status: { type: "string", enum: ["SUCCESS", "FAILURE"], description: "The status of the action" },
120
- summary: { type: "string", description: "Brief description of the action taken" },
121
- findings: { type: "array", items: { type: "string" }, description: "Optional findings or details" }
122
- },
123
- required: ["agent", "action", "traceId", "status", "summary"]
124
- }
125
- },
126
- {
127
- name: "update_contract_hash",
128
- description: "Re-generate and synchronize the backend contract SHA-256 hash.",
129
- inputSchema: { type: "object", properties: {} }
130
- }
131
- ];
132
-
133
- server.onRequest(ListToolsRequestSchema, async () => {
26
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
134
27
  return { tools: TOOLS };
135
28
  });
136
29
 
137
- server.onRequest(CallToolRequestSchema, async (request: any) => {
138
- const { name, arguments: args } = request.params as { name: string, arguments?: Record<string, unknown> };
139
-
140
- const projectRoot = process.cwd();
30
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
31
+ const typedRequest = request as CallToolRequest;
32
+ const { name, arguments: args } = typedRequest.params;
33
+ const projectRoot = process.env.ENDERUN_PROJECT_ROOT || process.cwd();
141
34
 
142
35
  try {
143
- switch (name) {
144
- case "read_file": {
145
- const filePath = path.join(projectRoot, args?.path as string);
146
- const content = fs.readFileSync(filePath, "utf8");
147
- return { content: [{ type: "text", text: content }] };
148
- }
149
-
150
- case "write_file": {
151
- const filePath = path.join(projectRoot, args?.path as string);
152
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
153
- fs.writeFileSync(filePath, args?.content as string);
154
- return { content: [{ type: "text", text: `✅ File written: ${args?.path}` }] };
155
- }
156
-
157
- case "replace_text": {
158
- const filePath = path.join(projectRoot, args?.path as string);
159
- let content = fs.readFileSync(filePath, "utf8");
160
- const oldText = args?.oldText as string;
161
- const newText = args?.newText as string;
162
-
163
- if (!content.includes(oldText)) {
164
- throw new Error(`Text not found in file: ${oldText}`);
165
- }
166
-
167
- content = content.replace(oldText, newText);
168
- fs.writeFileSync(filePath, content);
169
- return { content: [{ type: "text", text: `✅ Surgical edit successful in ${args?.path}` }] };
170
- }
171
-
172
- case "patch_file": {
173
- const filePath = path.join(projectRoot, args?.path as string);
174
- const lines = fs.readFileSync(filePath, "utf8").split("\n");
175
- const start = (args?.startLine as number) - 1;
176
- const end = (args?.endLine as number);
177
- const newContent = (args?.newContent as string).split("\n");
178
-
179
- if (start < 0 || start > lines.length) throw new Error("Invalid start line.");
180
-
181
- lines.splice(start, end - start, ...newContent);
182
- fs.writeFileSync(filePath, lines.join("\n"));
183
- return { content: [{ type: "text", text: `✅ File patched successfully: ${args?.path}` }] };
184
- }
185
-
186
- case "get_framework_status": {
187
- const output = execSync("npx agent-enderun status").toString();
188
- return { content: [{ type: "text", text: output }] };
189
- }
190
-
191
- case "update_project_memory": {
192
- const section = args?.section as string;
193
- const content = args?.content as string;
194
- // Escape quotes for shell
195
- const safeContent = content.replace(/"/g, "\\\"");
196
- execSync(`npx agent-enderun update_project_memory "${section}" "${safeContent}"`);
197
- return { content: [{ type: "text", text: `✅ Section ${section} updated.` }] };
198
- }
199
-
200
- case "orchestrate_loop": {
201
- const output = execSync("npx agent-enderun orchestrate").toString();
202
- return { content: [{ type: "text", text: output }] };
203
- }
204
-
205
- case "send_agent_message": {
206
- interface SendAgentMessageArgs {
207
- to: string;
208
- category: "ACTION" | "DELEGATION" | "INFO" | "ALERT";
209
- content: string;
210
- traceId: string;
211
- }
212
- const { to, category, content, traceId } = args as any as SendAgentMessageArgs;
213
- const candidates = [
214
- ".gemini/antigravity",
215
- ".gemini/antigravity-cli",
216
- ".gemini",
217
- ".claude",
218
- ".grok",
219
- ".agent",
220
- ".enderun",
221
- ];
222
- let frameworkDir = ".gemini";
223
- for (const c of candidates) {
224
- if (fs.existsSync(path.join(projectRoot, c))) {
225
- frameworkDir = c;
226
- break;
227
- }
228
- }
229
- const messagePath = path.join(projectRoot, frameworkDir, "messages", `${to.replace("@", "")}.json`);
230
-
231
- const message = {
232
- timestamp: new Date().toISOString(),
233
- from: "@mcp",
234
- to,
235
- category,
236
- traceId,
237
- content,
238
- status: "PENDING"
239
- };
240
-
241
- fs.mkdirSync(path.dirname(messagePath), { recursive: true });
242
- fs.appendFileSync(messagePath, JSON.stringify(message) + "\n");
243
-
244
- return { content: [{ type: "text", text: `✅ Message sent to ${to}` }] };
245
- }
246
-
247
- case "log_agent_action": {
248
- interface LogAgentActionArgs {
249
- agent: string;
250
- action: string;
251
- traceId: string;
252
- status: "SUCCESS" | "FAILURE";
253
- summary: string;
254
- findings?: string[];
255
- }
256
- const { agent, action, traceId, status, summary, findings } = args as any as LogAgentActionArgs;
257
-
258
- const candidates = [
259
- ".gemini/antigravity",
260
- ".gemini/antigravity-cli",
261
- ".gemini",
262
- ".claude",
263
- ".grok",
264
- ".agent",
265
- ".enderun",
266
- ];
267
- let frameworkDir = ".enderun";
268
- for (const c of candidates) {
269
- if (fs.existsSync(path.join(projectRoot, c))) {
270
- frameworkDir = c;
271
- break;
272
- }
273
- }
274
-
275
- const agentName = agent.replace("@", "");
276
- const logPath = path.join(projectRoot, frameworkDir, "logs", `${agentName}.json`);
277
-
278
- const logEntry = {
279
- timestamp: new Date().toISOString(),
280
- agent,
281
- action,
282
- requestId: traceId,
283
- status,
284
- summary,
285
- findings: findings || []
36
+ const handler = toolHandlers[name];
37
+ if (!handler) {
38
+ return {
39
+ isError: true,
40
+ content: [{ type: "text" as const, text: `Unknown tool: ${name}` }],
286
41
  };
287
-
288
- fs.mkdirSync(path.dirname(logPath), { recursive: true });
289
- fs.appendFileSync(logPath, JSON.stringify(logEntry) + "\n");
290
-
291
- return { content: [{ type: "text", text: `✅ Action logged for ${agent} to ${path.join(frameworkDir, "logs", `${agentName}.json`)}` }] };
292
- }
293
-
294
- case "update_contract_hash": {
295
- const output = execSync("npx agent-enderun update-contract").toString();
296
- return { content: [{ type: "text", text: output }] };
297
- }
298
-
299
- default:
300
- throw new Error(`Unknown tool: ${name}`);
301
42
  }
43
+ return handler(projectRoot, args || {});
302
44
  } catch (error: unknown) {
303
45
  const message = error instanceof Error ? error.message : "Unknown error occurred";
304
46
  return {
305
47
  isError: true,
306
- content: [{ type: "text", text: message }],
48
+ content: [{ type: "text" as const, text: message }],
307
49
  };
308
50
  }
309
51
  });
310
52
 
53
+ // ─── Graceful Startup & Shutdown ──────────────────────────────────
54
+
311
55
  async function run() {
312
56
  const transport = new StdioServerTransport();
57
+
58
+ // Prevent unhandled errors from crashing the MCP stream
59
+ process.on("uncaughtException", (error: Error) => {
60
+ process.stderr.write(`[agent-enderun-mcp] Uncaught exception: ${error.message}
61
+ `);
62
+ });
63
+ process.on("unhandledRejection", (reason: unknown) => {
64
+ const message = reason instanceof Error ? reason.message : String(reason);
65
+ process.stderr.write(`[agent-enderun-mcp] Unhandled rejection: ${message}
66
+ `);
67
+ });
68
+
69
+ // Graceful shutdown on SIGINT/SIGTERM
70
+ const shutdown = async () => {
71
+ try {
72
+ await server.close();
73
+ } catch {
74
+ // Already closed or failed — safe to ignore
75
+ }
76
+ process.exit(0);
77
+ };
78
+ process.on("SIGINT", shutdown);
79
+ process.on("SIGTERM", shutdown);
80
+
313
81
  await server.connect(transport);
314
82
  }
315
83
 
316
- run().catch(console.error);
84
+ run().catch((error: Error) => {
85
+ process.stderr.write(`[agent-enderun-mcp] Fatal startup error: ${error.message}
86
+ `);
87
+ process.exit(1);
88
+ });
@@ -0,0 +1,111 @@
1
+ import { ToolDefinition } from "../tools/types.js";
2
+
3
+ export const TOOLS: ToolDefinition[] = [
4
+ {
5
+ name: "read_file",
6
+ description: "Read the content of a file within the project.",
7
+ inputSchema: {
8
+ type: "object",
9
+ properties: {
10
+ path: { type: "string", description: "Path to the file relative to project root" },
11
+ },
12
+ required: ["path"],
13
+ },
14
+ },
15
+ {
16
+ name: "write_file",
17
+ description: "Write content to a file. Creates directories if missing.",
18
+ inputSchema: {
19
+ type: "object",
20
+ properties: {
21
+ path: { type: "string", description: "Path to the file relative to project root" },
22
+ content: { type: "string", description: "Complete content of the file" },
23
+ },
24
+ required: ["path", "content"],
25
+ },
26
+ },
27
+ {
28
+ name: "replace_text",
29
+ description: "Surgically replace a string in a file with another string.",
30
+ inputSchema: {
31
+ type: "object",
32
+ properties: {
33
+ path: { type: "string", description: "Path to the file" },
34
+ oldText: { type: "string", description: "The exact text to find" },
35
+ newText: { type: "string", description: "The text to replace it with" },
36
+ },
37
+ required: ["path", "oldText", "newText"],
38
+ },
39
+ },
40
+ {
41
+ name: "patch_file",
42
+ description: "Safely update a file by replacing a specific line range with new content.",
43
+ inputSchema: {
44
+ type: "object",
45
+ properties: {
46
+ path: { type: "string", description: "Path to the file" },
47
+ startLine: { type: "number", description: "Starting line number (1-indexed)" },
48
+ endLine: { type: "number", description: "Ending line number (inclusive)" },
49
+ newContent: { type: "string", description: "The new lines to insert" },
50
+ },
51
+ required: ["path", "startLine", "endLine", "newContent"],
52
+ },
53
+ },
54
+ {
55
+ name: "get_framework_status",
56
+ description: "Get the current project phase, active traces, and agent states.",
57
+ inputSchema: { type: "object", properties: {} },
58
+ },
59
+ {
60
+ name: "update_project_memory",
61
+ description: "Update a specific section in PROJECT_MEMORY.md.",
62
+ inputSchema: {
63
+ type: "object",
64
+ properties: {
65
+ section: { type: "string", description: "Section name (e.g., HISTORY, ACTIVE TASKS)" },
66
+ content: { type: "string", description: "Markdown content to append or set" },
67
+ },
68
+ required: ["section", "content"],
69
+ },
70
+ },
71
+ {
72
+ name: "orchestrate_loop",
73
+ description: "Process the pending Hermes messages and trigger dynamic state transitions.",
74
+ inputSchema: { type: "object", properties: {} },
75
+ },
76
+ {
77
+ name: "send_agent_message",
78
+ description: "Send a Hermes protocol message to another agent.",
79
+ inputSchema: {
80
+ type: "object",
81
+ properties: {
82
+ to: { type: "string", description: "Target agent (e.g., @backend, @qa)" },
83
+ category: { type: "string", enum: ["ACTION", "DELEGATION", "INFO", "ALERT"] },
84
+ content: { type: "string", description: "Message content" },
85
+ traceId: { type: "string", description: "Active Trace ID" },
86
+ },
87
+ required: ["to", "category", "content", "traceId"],
88
+ },
89
+ },
90
+ {
91
+ name: "log_agent_action",
92
+ description: "Log an agent action to the framework logs.",
93
+ inputSchema: {
94
+ type: "object",
95
+ properties: {
96
+ agent: { type: "string", description: "The agent name (e.g., @manager, @backend)" },
97
+ action: { type: "string", description: "Action type or name" },
98
+ traceId: { type: "string", description: "The active Trace ID" },
99
+ status: { type: "string", enum: ["SUCCESS", "FAILURE"], description: "The status of the action" },
100
+ summary: { type: "string", description: "Brief description of the action taken" },
101
+ findings: { type: "string", description: "Optional comma-separated findings or details" }
102
+ },
103
+ required: ["agent", "action", "traceId", "status", "summary"]
104
+ }
105
+ },
106
+ {
107
+ name: "update_contract_hash",
108
+ description: "Re-generate and synchronize the backend contract SHA-256 hash.",
109
+ inputSchema: { type: "object", properties: {} }
110
+ }
111
+ ];
@@ -0,0 +1,23 @@
1
+ import fs from "fs";
2
+ import { safePath } from "../../utils/security.js";
3
+ import { ToolArgs, ToolResult } from "../types.js";
4
+
5
+ export function handlePatchFile(projectRoot: string, args: ToolArgs): ToolResult {
6
+ const filePath = safePath(projectRoot, args.path as string);
7
+ const lines = fs.readFileSync(filePath, "utf8").split("\n");
8
+ const start = (args.startLine as number) - 1;
9
+ const end = args.endLine as number;
10
+ const newContent = (args.newContent as string).split("\n");
11
+
12
+ if (start < 0 || start > lines.length) {
13
+ throw new Error(`Invalid start line: ${start + 1}. File has ${lines.length} lines.`);
14
+ }
15
+ if (end < start + 1 || end > lines.length) {
16
+ throw new Error(`Invalid end line: ${end}. Must be between ${start + 1} and ${lines.length}.`);
17
+ }
18
+
19
+ lines.splice(start, end - start, ...newContent);
20
+ fs.writeFileSync(filePath, lines.join("\n"));
21
+ return { content: [{ type: "text", text: `✅ File patched successfully: ${args.path}` }] };
22
+ }
23
+
@@ -0,0 +1,9 @@
1
+ import fs from "fs";
2
+ import { safePath } from "../../utils/security.js";
3
+ import { ToolArgs, ToolResult } from "../types.js";
4
+
5
+ export function handleReadFile(projectRoot: string, args: ToolArgs): ToolResult {
6
+ const filePath = safePath(projectRoot, args.path as string);
7
+ const content = fs.readFileSync(filePath, "utf8");
8
+ return { content: [{ type: "text", text: content }] };
9
+ }
@@ -0,0 +1,18 @@
1
+ import fs from "fs";
2
+ import { safePath } from "../../utils/security.js";
3
+ import { ToolArgs, ToolResult } from "../types.js";
4
+
5
+ export function handleReplaceText(projectRoot: string, args: ToolArgs): ToolResult {
6
+ const filePath = safePath(projectRoot, args.path as string);
7
+ let content = fs.readFileSync(filePath, "utf8");
8
+ const oldText = args.oldText as string;
9
+ const newText = args.newText as string;
10
+
11
+ if (!content.includes(oldText)) {
12
+ throw new Error(`Text not found in file: ${oldText.slice(0, 100)}...`);
13
+ }
14
+
15
+ content = content.replace(oldText, newText);
16
+ fs.writeFileSync(filePath, content);
17
+ return { content: [{ type: "text", text: `✅ Surgical edit successful in ${args.path}` }] };
18
+ }
@@ -0,0 +1,11 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { safePath } from "../../utils/security.js";
4
+ import { ToolArgs, ToolResult } from "../types.js";
5
+
6
+ export function handleWriteFile(projectRoot: string, args: ToolArgs): ToolResult {
7
+ const filePath = safePath(projectRoot, args.path as string);
8
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
9
+ fs.writeFileSync(filePath, args.content as string);
10
+ return { content: [{ type: "text", text: `✅ File written: ${args.path}` }] };
11
+ }
@@ -0,0 +1,7 @@
1
+ import { safeExec } from "../../utils/cli.js";
2
+ import { ToolResult } from "../types.js";
3
+
4
+ export function handleGetFrameworkStatus(projectRoot: string): ToolResult {
5
+ const output = safeExec("npx", ["agent-enderun", "status"], projectRoot);
6
+ return { content: [{ type: "text", text: output }] };
7
+ }
@@ -0,0 +1,7 @@
1
+ import { safeExec } from "../../utils/cli.js";
2
+ import { ToolResult } from "../types.js";
3
+
4
+ export function handleOrchestrateLoop(projectRoot: string): ToolResult {
5
+ const output = safeExec("npx", ["agent-enderun", "orchestrate"], projectRoot);
6
+ return { content: [{ type: "text", text: output }] };
7
+ }
@@ -0,0 +1,7 @@
1
+ import { safeExec } from "../../utils/cli.js";
2
+ import { ToolResult } from "../types.js";
3
+
4
+ export function handleUpdateContractHash(projectRoot: string): ToolResult {
5
+ const output = safeExec("npx", ["agent-enderun", "update-contract"], projectRoot);
6
+ return { content: [{ type: "text", text: output }] };
7
+ }
@@ -0,0 +1,10 @@
1
+ import { safeExec } from "../../utils/cli.js";
2
+ import { ToolArgs, ToolResult } from "../types.js";
3
+
4
+ export function handleUpdateProjectMemory(projectRoot: string, args: ToolArgs): ToolResult {
5
+ const section = args.section as string;
6
+ const content = args.content as string;
7
+ // Using execFileSync with array args prevents command injection
8
+ safeExec("npx", ["agent-enderun", "update_project_memory", section, content], projectRoot);
9
+ return { content: [{ type: "text", text: `✅ Section ${section} updated.` }] };
10
+ }
@@ -0,0 +1,31 @@
1
+ import { TOOLS } from "./definitions.js";
2
+ import { handleReadFile } from "./file_system/read_file.js";
3
+ import { handleWriteFile } from "./file_system/write_file.js";
4
+ import { handleReplaceText } from "./file_system/replace_text.js";
5
+ import { handlePatchFile } from "./file_system/patch_file.js";
6
+ import { handleGetFrameworkStatus } from "./framework/get_status.js";
7
+ import { handleUpdateProjectMemory } from "./framework/update_memory.js";
8
+ import { handleOrchestrateLoop } from "./framework/orchestrate.js";
9
+ import { handleUpdateContractHash } from "./framework/update_contract_hash.js";
10
+ import { handleSendAgentMessage } from "./messaging/send_message.js";
11
+ import { handleLogAgentAction } from "./messaging/log_action.js";
12
+ import { ToolResult, ToolArgs } from "./types.js";
13
+
14
+ // Define a type for the tool handlers
15
+ export type ToolHandler = (projectRoot: string, args: ToolArgs) => ToolResult;
16
+
17
+ // Map of tool names to their handler functions
18
+ export const toolHandlers: Record<string, ToolHandler> = {
19
+ read_file: handleReadFile,
20
+ write_file: handleWriteFile,
21
+ replace_text: handleReplaceText,
22
+ patch_file: handlePatchFile,
23
+ get_framework_status: handleGetFrameworkStatus,
24
+ update_project_memory: handleUpdateProjectMemory,
25
+ orchestrate_loop: handleOrchestrateLoop,
26
+ send_agent_message: handleSendAgentMessage,
27
+ log_agent_action: handleLogAgentAction,
28
+ update_contract_hash: handleUpdateContractHash,
29
+ };
30
+
31
+ export { TOOLS };