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.
- package/.enderun/ENDERUN.md +1 -1
- package/.enderun/STATUS.md +2 -2
- package/.enderun/config.json +1 -1
- package/.enderun/memory-graph/shared-facts.json +1 -1
- package/framework-mcp/dist/index.js +42 -240
- package/framework-mcp/dist/tools/definitions.js +109 -0
- package/framework-mcp/dist/tools/file_system/patch_file.js +18 -0
- package/framework-mcp/dist/tools/file_system/read_file.js +7 -0
- package/framework-mcp/dist/tools/file_system/replace_text.js +14 -0
- package/framework-mcp/dist/tools/file_system/write_file.js +9 -0
- package/framework-mcp/dist/tools/framework/get_status.js +5 -0
- package/framework-mcp/dist/tools/framework/orchestrate.js +5 -0
- package/framework-mcp/dist/tools/framework/update_contract_hash.js +5 -0
- package/framework-mcp/dist/tools/framework/update_memory.js +8 -0
- package/framework-mcp/dist/tools/index.js +25 -0
- package/framework-mcp/dist/tools/messaging/log_action.js +22 -0
- package/framework-mcp/dist/tools/messaging/send_message.js +21 -0
- package/framework-mcp/dist/tools/types.js +1 -0
- package/framework-mcp/dist/utils/cli.js +20 -0
- package/framework-mcp/dist/utils/security.js +35 -0
- package/framework-mcp/src/declarations.d.ts +17 -3
- package/framework-mcp/src/index.ts +49 -277
- package/framework-mcp/src/tools/definitions.ts +111 -0
- package/framework-mcp/src/tools/file_system/patch_file.ts +23 -0
- package/framework-mcp/src/tools/file_system/read_file.ts +9 -0
- package/framework-mcp/src/tools/file_system/replace_text.ts +18 -0
- package/framework-mcp/src/tools/file_system/write_file.ts +11 -0
- package/framework-mcp/src/tools/framework/get_status.ts +7 -0
- package/framework-mcp/src/tools/framework/orchestrate.ts +7 -0
- package/framework-mcp/src/tools/framework/update_contract_hash.ts +7 -0
- package/framework-mcp/src/tools/framework/update_memory.ts +10 -0
- package/framework-mcp/src/tools/index.ts +31 -0
- package/framework-mcp/src/tools/messaging/log_action.ts +28 -0
- package/framework-mcp/src/tools/messaging/send_message.ts +26 -0
- package/framework-mcp/src/tools/types.ts +40 -0
- package/framework-mcp/src/utils/cli.ts +20 -0
- package/framework-mcp/src/utils/security.ts +41 -0
- 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
|
-
|
|
3
|
-
|
|
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
|
-
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
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
|
-
|
|
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.
|
|
138
|
-
const
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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(
|
|
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 };
|