daemora 1.0.0

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 (115) hide show
  1. package/README.md +666 -0
  2. package/SOUL.md +104 -0
  3. package/config/hooks.json +14 -0
  4. package/config/mcp.json +145 -0
  5. package/package.json +86 -0
  6. package/skills/.gitkeep +0 -0
  7. package/skills/apple-notes.md +193 -0
  8. package/skills/apple-reminders.md +189 -0
  9. package/skills/camsnap.md +162 -0
  10. package/skills/coding.md +14 -0
  11. package/skills/documents.md +13 -0
  12. package/skills/email.md +13 -0
  13. package/skills/gif-search.md +196 -0
  14. package/skills/healthcheck.md +225 -0
  15. package/skills/image-gen.md +147 -0
  16. package/skills/model-usage.md +182 -0
  17. package/skills/obsidian.md +207 -0
  18. package/skills/pdf.md +211 -0
  19. package/skills/research.md +13 -0
  20. package/skills/skill-creator.md +142 -0
  21. package/skills/spotify.md +149 -0
  22. package/skills/summarize.md +230 -0
  23. package/skills/things.md +199 -0
  24. package/skills/tmux.md +204 -0
  25. package/skills/trello.md +183 -0
  26. package/skills/video-frames.md +202 -0
  27. package/skills/weather.md +127 -0
  28. package/src/a2a/A2AClient.js +136 -0
  29. package/src/a2a/A2AServer.js +316 -0
  30. package/src/a2a/AgentCard.js +79 -0
  31. package/src/agents/SubAgentManager.js +369 -0
  32. package/src/agents/Supervisor.js +192 -0
  33. package/src/channels/BaseChannel.js +104 -0
  34. package/src/channels/DiscordChannel.js +288 -0
  35. package/src/channels/EmailChannel.js +172 -0
  36. package/src/channels/GoogleChatChannel.js +316 -0
  37. package/src/channels/HttpChannel.js +26 -0
  38. package/src/channels/LineChannel.js +168 -0
  39. package/src/channels/SignalChannel.js +186 -0
  40. package/src/channels/SlackChannel.js +329 -0
  41. package/src/channels/TeamsChannel.js +272 -0
  42. package/src/channels/TelegramChannel.js +347 -0
  43. package/src/channels/WhatsAppChannel.js +219 -0
  44. package/src/channels/index.js +198 -0
  45. package/src/cli.js +1267 -0
  46. package/src/config/agentProfiles.js +120 -0
  47. package/src/config/channels.js +32 -0
  48. package/src/config/default.js +206 -0
  49. package/src/config/models.js +123 -0
  50. package/src/config/permissions.js +167 -0
  51. package/src/core/AgentLoop.js +446 -0
  52. package/src/core/Compaction.js +143 -0
  53. package/src/core/CostTracker.js +116 -0
  54. package/src/core/EventBus.js +46 -0
  55. package/src/core/Task.js +67 -0
  56. package/src/core/TaskQueue.js +206 -0
  57. package/src/core/TaskRunner.js +226 -0
  58. package/src/daemon/DaemonManager.js +301 -0
  59. package/src/hooks/HookRunner.js +230 -0
  60. package/src/index.js +482 -0
  61. package/src/mcp/MCPAgentRunner.js +112 -0
  62. package/src/mcp/MCPClient.js +186 -0
  63. package/src/mcp/MCPManager.js +412 -0
  64. package/src/models/ModelRouter.js +180 -0
  65. package/src/safety/AuditLog.js +135 -0
  66. package/src/safety/CircuitBreaker.js +126 -0
  67. package/src/safety/FilesystemGuard.js +169 -0
  68. package/src/safety/GitRollback.js +139 -0
  69. package/src/safety/HumanApproval.js +156 -0
  70. package/src/safety/InputSanitizer.js +72 -0
  71. package/src/safety/PermissionGuard.js +83 -0
  72. package/src/safety/Sandbox.js +70 -0
  73. package/src/safety/SecretScanner.js +100 -0
  74. package/src/safety/SecretVault.js +250 -0
  75. package/src/scheduler/Heartbeat.js +115 -0
  76. package/src/scheduler/Scheduler.js +228 -0
  77. package/src/services/models/outputSchema.js +15 -0
  78. package/src/services/openai.js +25 -0
  79. package/src/services/sessions.js +65 -0
  80. package/src/setup/theme.js +110 -0
  81. package/src/setup/wizard.js +788 -0
  82. package/src/skills/SkillLoader.js +168 -0
  83. package/src/storage/TaskStore.js +69 -0
  84. package/src/systemPrompt.js +526 -0
  85. package/src/tenants/TenantContext.js +19 -0
  86. package/src/tenants/TenantManager.js +379 -0
  87. package/src/tools/ToolRegistry.js +141 -0
  88. package/src/tools/applyPatch.js +144 -0
  89. package/src/tools/browserAutomation.js +223 -0
  90. package/src/tools/createDocument.js +265 -0
  91. package/src/tools/cronTool.js +105 -0
  92. package/src/tools/editFile.js +139 -0
  93. package/src/tools/executeCommand.js +123 -0
  94. package/src/tools/glob.js +67 -0
  95. package/src/tools/grep.js +121 -0
  96. package/src/tools/imageAnalysis.js +120 -0
  97. package/src/tools/index.js +173 -0
  98. package/src/tools/listDirectory.js +47 -0
  99. package/src/tools/manageAgents.js +47 -0
  100. package/src/tools/manageMCP.js +159 -0
  101. package/src/tools/memory.js +478 -0
  102. package/src/tools/messageChannel.js +45 -0
  103. package/src/tools/projectTracker.js +259 -0
  104. package/src/tools/readFile.js +52 -0
  105. package/src/tools/screenCapture.js +112 -0
  106. package/src/tools/searchContent.js +76 -0
  107. package/src/tools/searchFiles.js +75 -0
  108. package/src/tools/sendEmail.js +118 -0
  109. package/src/tools/sendFile.js +63 -0
  110. package/src/tools/textToSpeech.js +161 -0
  111. package/src/tools/transcribeAudio.js +82 -0
  112. package/src/tools/useMCP.js +29 -0
  113. package/src/tools/webFetch.js +150 -0
  114. package/src/tools/webSearch.js +134 -0
  115. package/src/tools/writeFile.js +26 -0
package/src/index.js ADDED
@@ -0,0 +1,482 @@
1
+ import express from "express";
2
+ import { mkdirSync } from "fs";
3
+ import { toolFunctions } from "./tools/index.js";
4
+ import { getSession } from "./services/sessions.js";
5
+ import { config } from "./config/default.js";
6
+ import { listAvailableModels } from "./models/ModelRouter.js";
7
+ import taskQueue from "./core/TaskQueue.js";
8
+ import taskRunner from "./core/TaskRunner.js";
9
+ import { loadTask, listTasks } from "./storage/TaskStore.js";
10
+ import { getTodayCost } from "./core/CostTracker.js";
11
+ import supervisor from "./agents/Supervisor.js";
12
+ import { getActiveSubAgentCount } from "./agents/SubAgentManager.js";
13
+ import channelRegistry from "./channels/index.js";
14
+ import skillLoader from "./skills/SkillLoader.js";
15
+ import mcpManager from "./mcp/MCPManager.js";
16
+ import auditLog from "./safety/AuditLog.js";
17
+ import scheduler from "./scheduler/Scheduler.js";
18
+ import heartbeat from "./scheduler/Heartbeat.js";
19
+ import { mountAgentCard } from "./a2a/AgentCard.js";
20
+ import { mountA2AServer } from "./a2a/A2AServer.js";
21
+ import daemonManager from "./daemon/DaemonManager.js";
22
+ import secretVault from "./safety/SecretVault.js";
23
+ import tenantManager from "./tenants/TenantManager.js";
24
+
25
+ // Ensure all data directories exist
26
+ const dirs = [
27
+ config.dataDir,
28
+ config.sessionsDir,
29
+ config.tasksDir,
30
+ config.memoryDir,
31
+ config.auditDir,
32
+ config.costsDir,
33
+ config.skillsDir,
34
+ ];
35
+ for (const dir of dirs) {
36
+ mkdirSync(dir, { recursive: true });
37
+ }
38
+
39
+ // Initialize task system
40
+ taskQueue.init();
41
+ taskRunner.start();
42
+ supervisor.start();
43
+ auditLog.start();
44
+ scheduler.start();
45
+ heartbeat.start();
46
+
47
+ const app = express();
48
+ app.use(express.json());
49
+
50
+ // Mount A2A protocol endpoints
51
+ mountAgentCard(app);
52
+ mountA2AServer(app);
53
+
54
+ // --- Health check ---
55
+ app.get("/health", (req, res) => {
56
+ res.json({
57
+ status: "ok",
58
+ uptime: process.uptime(),
59
+ timestamp: new Date().toISOString(),
60
+ tools: Object.keys(toolFunctions).length,
61
+ model: config.defaultModel,
62
+ permissionTier: config.permissionTier,
63
+ queue: taskQueue.stats(),
64
+ todayCost: getTodayCost(),
65
+ });
66
+ });
67
+
68
+ // --- Chat endpoint (DISABLED — no authentication, anyone on the network could submit tasks) ---
69
+ // Uncomment and add authentication middleware before re-enabling.
70
+ // app.post("/chat", async (req, res) => { ... });
71
+
72
+ // --- Task submit endpoint (DISABLED — same reason, unauthenticated) ---
73
+ // app.post("/tasks", (req, res) => { ... });
74
+
75
+ app.get("/tasks/:id", (req, res) => {
76
+ const task = loadTask(req.params.id);
77
+ if (!task) {
78
+ return res.status(404).json({ error: "Task not found" });
79
+ }
80
+ res.json(task);
81
+ });
82
+
83
+ app.get("/tasks", (req, res) => {
84
+ const { limit, status } = req.query;
85
+ const tasks = listTasks({
86
+ limit: limit ? parseInt(limit, 10) : 20,
87
+ status: status || null,
88
+ });
89
+ res.json({
90
+ tasks: tasks.map((t) => ({
91
+ id: t.id,
92
+ status: t.status,
93
+ channel: t.channel,
94
+ input: t.input.slice(0, 100),
95
+ cost: t.cost,
96
+ createdAt: t.createdAt,
97
+ completedAt: t.completedAt,
98
+ })),
99
+ queue: taskQueue.stats(),
100
+ });
101
+ });
102
+
103
+ // --- Session endpoint ---
104
+ app.get("/sessions/:id", (req, res) => {
105
+ const session = getSession(req.params.id);
106
+ if (!session) {
107
+ return res.status(404).json({ error: "Session not found" });
108
+ }
109
+ res.json(session);
110
+ });
111
+
112
+ // --- Config endpoint ---
113
+ app.get("/config", (req, res) => {
114
+ res.json({
115
+ defaultModel: config.defaultModel,
116
+ permissionTier: config.permissionTier,
117
+ maxCostPerTask: config.maxCostPerTask,
118
+ maxDailyCost: config.maxDailyCost,
119
+ daemonMode: config.daemonMode,
120
+ heartbeatIntervalMinutes: config.heartbeatIntervalMinutes,
121
+ channels: Object.fromEntries(
122
+ Object.entries(config.channels).map(([k, v]) => [k, { enabled: v.enabled }])
123
+ ),
124
+ });
125
+ });
126
+
127
+ // --- Models endpoint ---
128
+ app.get("/models", (req, res) => {
129
+ res.json({
130
+ default: config.defaultModel,
131
+ available: listAvailableModels(),
132
+ });
133
+ });
134
+
135
+ // --- Supervisor endpoint ---
136
+ app.get("/supervisor", (req, res) => {
137
+ res.json({
138
+ warnings: supervisor.getWarnings(),
139
+ activeSubAgents: getActiveSubAgentCount(),
140
+ });
141
+ });
142
+
143
+ // --- WhatsApp webhook ---
144
+ app.post("/webhooks/whatsapp", async (req, res) => {
145
+ const whatsapp = channelRegistry.get("whatsapp");
146
+ if (!whatsapp || !whatsapp.running) {
147
+ return res.status(503).json({ error: "WhatsApp channel not active" });
148
+ }
149
+ try {
150
+ await whatsapp.handleWebhook(req.body);
151
+ res.status(200).send("OK");
152
+ } catch (error) {
153
+ console.error("[WhatsApp Webhook] Error:", error.message);
154
+ res.status(500).send("Error");
155
+ }
156
+ });
157
+
158
+ // --- Microsoft Teams webhook ---
159
+ app.post("/webhooks/teams", async (req, res) => {
160
+ const teams = channelRegistry.get("teams");
161
+ if (!teams || !teams.running) {
162
+ return res.status(503).json({ error: "Teams channel not active" });
163
+ }
164
+ try {
165
+ await teams.handleWebhook(req, res);
166
+ } catch (error) {
167
+ console.error("[Teams Webhook] Error:", error.message);
168
+ if (!res.headersSent) res.status(500).send("Error");
169
+ }
170
+ });
171
+
172
+ // --- Google Chat webhook ---
173
+ app.post("/webhooks/googlechat", async (req, res) => {
174
+ const gc = channelRegistry.get("googlechat");
175
+ if (!gc || !gc.running) {
176
+ return res.status(503).json({ error: "Google Chat channel not active" });
177
+ }
178
+ try {
179
+ await gc.handleWebhook(req, res);
180
+ } catch (error) {
181
+ console.error("[GoogleChat Webhook] Error:", error.message);
182
+ if (!res.headersSent) res.status(500).send("Error");
183
+ }
184
+ });
185
+
186
+ // --- LINE webhook (needs raw body for HMAC-SHA256 signature validation) ---
187
+ app.post("/webhooks/line", express.raw({ type: "application/json" }), async (req, res) => {
188
+ const line = channelRegistry.get("line");
189
+ if (!line || !line.running) {
190
+ return res.status(503).json({ error: "LINE channel not active" });
191
+ }
192
+ try {
193
+ const rawBody = req.body; // Buffer (express.raw)
194
+ const signature = req.headers["x-line-signature"];
195
+ const body = JSON.parse(rawBody.toString("utf8"));
196
+ const result = await line.handleWebhook(rawBody, signature, body);
197
+ if (result.error) {
198
+ return res.status(401).json(result);
199
+ }
200
+ res.status(200).json(result);
201
+ } catch (error) {
202
+ console.error("[LINE Webhook] Error:", error.message);
203
+ res.status(500).send("Error");
204
+ }
205
+ });
206
+
207
+ // --- Channels endpoint ---
208
+ app.get("/channels", (req, res) => {
209
+ res.json({ channels: channelRegistry.list() });
210
+ });
211
+
212
+ // --- Skills endpoint ---
213
+ app.get("/skills", (req, res) => {
214
+ res.json({ skills: skillLoader.list() });
215
+ });
216
+
217
+ app.post("/skills/reload", (req, res) => {
218
+ skillLoader.reload();
219
+ res.json({ message: "Skills reloaded", skills: skillLoader.list() });
220
+ });
221
+
222
+ // --- Schedule endpoints ---
223
+ app.post("/schedules", (req, res) => {
224
+ try {
225
+ const { cronExpression, taskInput, channel, model, name } = req.body;
226
+ if (!cronExpression || !taskInput) {
227
+ return res.status(400).json({ error: "cronExpression and taskInput are required" });
228
+ }
229
+ const schedule = scheduler.create({ cronExpression, taskInput, channel, model, name });
230
+ res.status(201).json(schedule);
231
+ } catch (error) {
232
+ res.status(400).json({ error: error.message });
233
+ }
234
+ });
235
+
236
+ app.get("/schedules", (req, res) => {
237
+ res.json({ schedules: scheduler.list() });
238
+ });
239
+
240
+ app.delete("/schedules/:id", (req, res) => {
241
+ scheduler.delete(req.params.id);
242
+ res.json({ message: "Schedule deleted" });
243
+ });
244
+
245
+ // --- Audit endpoint ---
246
+ app.get("/audit", (req, res) => {
247
+ res.json(auditLog.stats());
248
+ });
249
+
250
+ // --- MCP endpoints ---
251
+ app.get("/mcp", (req, res) => {
252
+ const cfg = mcpManager.readConfig().mcpServers || {};
253
+ const live = mcpManager.list();
254
+ const servers = Object.entries(cfg)
255
+ .filter(([k]) => !k.startsWith("_comment"))
256
+ .map(([name, serverCfg]) => {
257
+ const liveEntry = live.find(s => s.name === name);
258
+ return {
259
+ name,
260
+ enabled: serverCfg.enabled !== false,
261
+ connected: liveEntry?.connected ?? false,
262
+ tools: liveEntry?.tools ?? [],
263
+ type: serverCfg.command ? "stdio" : (serverCfg.transport || "http"),
264
+ command: serverCfg.command || null,
265
+ url: serverCfg.url || null,
266
+ };
267
+ });
268
+ res.json({ servers });
269
+ });
270
+
271
+ // Add a new MCP server
272
+ app.post("/mcp", async (req, res) => {
273
+ const { name, command, args, url, transport, env } = req.body;
274
+ if (!name) return res.status(400).json({ error: "name is required" });
275
+ if (!command && !url) return res.status(400).json({ error: "command (stdio) or url (http/sse) required" });
276
+
277
+ const serverConfig = command
278
+ ? { command, args: args || [], env: env || {} }
279
+ : { url, transport: transport || undefined, env: env || {} };
280
+
281
+ try {
282
+ const result = await mcpManager.addServer(name, serverConfig);
283
+ res.status(201).json({ message: result, name });
284
+ } catch (err) {
285
+ res.status(400).json({ error: err.message });
286
+ }
287
+ });
288
+
289
+ // Remove an MCP server
290
+ app.delete("/mcp/:name", async (req, res) => {
291
+ try {
292
+ const result = await mcpManager.removeServer(req.params.name);
293
+ res.json({ message: result });
294
+ } catch (err) {
295
+ res.status(400).json({ error: err.message });
296
+ }
297
+ });
298
+
299
+ // Enable / disable / reload an MCP server
300
+ app.post("/mcp/:name/:action", async (req, res) => {
301
+ const { name, action } = req.params;
302
+ try {
303
+ let result;
304
+ if (action === "enable") result = await mcpManager.setEnabled(name, true);
305
+ else if (action === "disable") result = await mcpManager.setEnabled(name, false);
306
+ else if (action === "reload") result = await mcpManager.reloadServer(name);
307
+ else return res.status(400).json({ error: `Unknown action: ${action}. Valid: enable, disable, reload` });
308
+ res.json({ message: result });
309
+ } catch (err) {
310
+ res.status(400).json({ error: err.message });
311
+ }
312
+ });
313
+
314
+ // --- Daemon endpoints ---
315
+ app.get("/daemon/status", (req, res) => {
316
+ res.json(daemonManager.status());
317
+ });
318
+
319
+ app.post("/daemon/:action", (req, res) => {
320
+ const { action } = req.params;
321
+ try {
322
+ switch (action) {
323
+ case "install":
324
+ daemonManager.install();
325
+ res.json({ message: "Daemon installed. Will auto-start on boot." });
326
+ break;
327
+ case "uninstall":
328
+ daemonManager.uninstall();
329
+ res.json({ message: "Daemon uninstalled." });
330
+ break;
331
+ case "start":
332
+ daemonManager.start();
333
+ res.json({ message: "Daemon started." });
334
+ break;
335
+ case "stop":
336
+ daemonManager.stop();
337
+ res.json({ message: "Daemon stopped." });
338
+ break;
339
+ case "restart":
340
+ daemonManager.restart();
341
+ res.json({ message: "Daemon restarted." });
342
+ break;
343
+ default:
344
+ res.status(400).json({ error: `Unknown action: ${action}` });
345
+ }
346
+ } catch (error) {
347
+ res.status(500).json({ error: error.message });
348
+ }
349
+ });
350
+
351
+ // --- Vault endpoints ---
352
+ app.get("/vault/status", (req, res) => {
353
+ res.json({
354
+ exists: secretVault.exists(),
355
+ unlocked: secretVault.isUnlocked(),
356
+ });
357
+ });
358
+
359
+ app.post("/vault/unlock", (req, res) => {
360
+ try {
361
+ const { passphrase } = req.body;
362
+ if (!passphrase) return res.status(400).json({ error: "passphrase is required" });
363
+ secretVault.unlock(passphrase);
364
+ // Inject vault secrets into process.env for model adapters
365
+ const secrets = secretVault.getAsEnv();
366
+ for (const [key, value] of Object.entries(secrets)) {
367
+ process.env[key] = value;
368
+ }
369
+ res.json({ message: "Vault unlocked", secretCount: Object.keys(secrets).length });
370
+ } catch (error) {
371
+ res.status(400).json({ error: error.message });
372
+ }
373
+ });
374
+
375
+ app.post("/vault/lock", (req, res) => {
376
+ secretVault.lock();
377
+ res.json({ message: "Vault locked" });
378
+ });
379
+
380
+ // --- Tenant endpoints ---
381
+ app.get("/tenants", (req, res) => {
382
+ const tenants = tenantManager.list();
383
+ res.json({ tenants, stats: tenantManager.stats() });
384
+ });
385
+
386
+ app.get("/tenants/:id", (req, res) => {
387
+ const tenant = tenantManager.get(decodeURIComponent(req.params.id));
388
+ if (!tenant) return res.status(404).json({ error: "Tenant not found" });
389
+ res.json(tenant);
390
+ });
391
+
392
+ app.patch("/tenants/:id", (req, res) => {
393
+ const id = decodeURIComponent(req.params.id);
394
+ const updated = tenantManager.set(id, req.body);
395
+ res.json(updated);
396
+ });
397
+
398
+ app.post("/tenants/:id/suspend", (req, res) => {
399
+ const id = decodeURIComponent(req.params.id);
400
+ const { reason } = req.body;
401
+ const updated = tenantManager.suspend(id, reason || "");
402
+ if (!updated) return res.status(404).json({ error: "Tenant not found" });
403
+ res.json(updated);
404
+ });
405
+
406
+ app.post("/tenants/:id/unsuspend", (req, res) => {
407
+ const id = decodeURIComponent(req.params.id);
408
+ const updated = tenantManager.unsuspend(id);
409
+ if (!updated) return res.status(404).json({ error: "Tenant not found" });
410
+ res.json(updated);
411
+ });
412
+
413
+ app.post("/tenants/:id/reset", (req, res) => {
414
+ const id = decodeURIComponent(req.params.id);
415
+ const updated = tenantManager.reset(id);
416
+ if (!updated) return res.status(404).json({ error: "Tenant not found" });
417
+ res.json(updated);
418
+ });
419
+
420
+ app.delete("/tenants/:id", (req, res) => {
421
+ const id = decodeURIComponent(req.params.id);
422
+ const deleted = tenantManager.delete(id);
423
+ if (!deleted) return res.status(404).json({ error: "Tenant not found" });
424
+ res.json({ message: "Tenant deleted" });
425
+ });
426
+
427
+ // --- Costs endpoint ---
428
+ app.get("/costs/today", (req, res) => {
429
+ res.json({
430
+ date: new Date().toISOString().split("T")[0],
431
+ totalCost: getTodayCost(),
432
+ dailyLimit: config.maxDailyCost,
433
+ remaining: Math.max(0, config.maxDailyCost - getTodayCost()),
434
+ });
435
+ });
436
+
437
+ // --- Start server ---
438
+ app.listen(config.port, async () => {
439
+ console.log("\n--- Daemora Server ---");
440
+ console.log(`Running on http://localhost:${config.port}`);
441
+ console.log(`Model: ${config.defaultModel}`);
442
+ console.log(`Permission tier: ${config.permissionTier}`);
443
+ console.log(`Tools loaded: ${Object.keys(toolFunctions).join(", ")}`);
444
+ console.log(`Total tools: ${Object.keys(toolFunctions).length}`);
445
+ console.log(`Data dir: ${config.dataDir}`);
446
+ console.log(`Daemon mode: ${config.daemonMode}`);
447
+ console.log(`Task runner: active (concurrency: 2)`);
448
+
449
+ // Initialize MCP in background
450
+ mcpManager.init().catch((e) => console.log(`[MCPManager] Init error: ${e.message}`));
451
+
452
+ // Start channels (await so we see results before the blank line)
453
+ try {
454
+ await channelRegistry.startAll();
455
+ } catch (e) {
456
+ console.log(`[ChannelRegistry] Start error: ${e.message}`);
457
+ }
458
+ console.log("");
459
+ });
460
+
461
+ // Graceful shutdown
462
+ process.on("SIGTERM", () => {
463
+ console.log("\n[Shutdown] SIGTERM received. Stopping...");
464
+ scheduler.stop();
465
+ heartbeat.stop();
466
+ taskRunner.stop();
467
+ supervisor.stop();
468
+ mcpManager.shutdown().then(() =>
469
+ channelRegistry.stopAll().then(() => process.exit(0))
470
+ );
471
+ });
472
+
473
+ process.on("SIGINT", () => {
474
+ console.log("\n[Shutdown] SIGINT received. Stopping...");
475
+ scheduler.stop();
476
+ heartbeat.stop();
477
+ taskRunner.stop();
478
+ supervisor.stop();
479
+ mcpManager.shutdown().then(() =>
480
+ channelRegistry.stopAll().then(() => process.exit(0))
481
+ );
482
+ });
@@ -0,0 +1,112 @@
1
+ import { spawnSubAgent } from "../agents/SubAgentManager.js";
2
+ import mcpManager from "./MCPManager.js";
3
+
4
+ /**
5
+ * MCP Agent Runner — spawns specialist sub-agents for individual MCP servers.
6
+ *
7
+ * Each specialist gets:
8
+ * - ONLY the tools from its assigned MCP server (no built-in tools)
9
+ * - A minimal system prompt focused solely on its server's capabilities
10
+ * - No SOUL.md, no memory, no unrelated tool docs
11
+ *
12
+ * This keeps context lean: a GitHub specialist sees only GitHub tools.
13
+ * A Notion specialist sees only Notion tools. No confusion, no wasted tokens.
14
+ */
15
+
16
+ /**
17
+ * Build a focused system prompt for an MCP specialist agent.
18
+ * @param {string} serverName - The MCP server name
19
+ * @param {string} toolDocs - Formatted tool documentation for this server's tools
20
+ * @returns {{ role: "system", content: string }}
21
+ */
22
+ function buildMCPAgentSystemPrompt(serverName, toolDocs) {
23
+ return {
24
+ role: "system",
25
+ content: `You are a specialist agent for the "${serverName}" MCP server. You have been delegated a specific task by the main agent. Complete it fully and autonomously using the tools below.
26
+
27
+ # Response Format
28
+
29
+ Respond with a JSON object on every turn:
30
+ \`\`\`
31
+ {
32
+ "type": "tool_call" | "text",
33
+ "tool_call": { "tool_name": "string", "params": ["string", ...] } | null,
34
+ "text_content": "string" | null,
35
+ "finalResponse": boolean
36
+ }
37
+ \`\`\`
38
+
39
+ - type "tool_call": set tool_call, set text_content to null, finalResponse to false
40
+ - type "text": set text_content with a clear summary of what you did, set tool_call to null, finalResponse to true
41
+
42
+ # Available Tools
43
+
44
+ ${toolDocs}
45
+
46
+ # How to Call MCP Tools
47
+
48
+ All MCP tool params must be passed as a single JSON string (the first and only argument):
49
+ tool_name: "mcp__${serverName}__someToolName"
50
+ params: ['{"param1":"value1","param2":"value2"}']
51
+
52
+ # Rules — You Own This Task
53
+
54
+ - **Do the work, don't describe it.** Your first response must be a tool_call, not a plan.
55
+ - **Chain calls until fully done.** After each tool result, decide: need more tools? Call another. Only set finalResponse true when the task is genuinely complete.
56
+ - **Never ask for clarification.** You have everything you need in the task description. Make reasonable decisions and proceed.
57
+ - **Handle errors yourself.** If a tool call fails, read the error, adjust your approach, try again. Do not give up and report failure unless you have exhausted all approaches.
58
+ - **Be thorough.** If the task says "update all tasks in a project", update all of them. If it says "research X", gather enough detail to be useful. Don't do a half job.
59
+ - **End with a useful summary.** When done, set finalResponse true and write a clear summary: what was done, what was created/updated/found, and any important details the main agent needs.`,
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Run a specialist MCP agent for the given server.
65
+ *
66
+ * @param {string} serverName - MCP server name (e.g. "github", "notion")
67
+ * @param {string} taskDescription - Full task description (no other context available)
68
+ * @param {object} options - Forwarded to spawnSubAgent (parentTaskId, channelMeta, approvalMode, timeout, model)
69
+ * @returns {Promise<string>} - Agent's final response
70
+ */
71
+ export async function runMCPAgent(serverName, taskDescription, options = {}) {
72
+ // Get only this server's tool functions
73
+ const serverTools = mcpManager.getServerTools(serverName);
74
+
75
+ if (Object.keys(serverTools).length === 0) {
76
+ const available = mcpManager.list().map((s) => s.name);
77
+ if (available.length === 0) {
78
+ return `No MCP servers are connected. Check config/mcp.json to enable servers.`;
79
+ }
80
+ return `MCP server "${serverName}" not found or has no tools. Available servers: ${available.join(", ")}`;
81
+ }
82
+
83
+ // Build tool docs for this server's system prompt
84
+ const toolDocs = Object.keys(serverTools)
85
+ .map((fullName) => {
86
+ const entry = mcpManager.toolMap.get(fullName);
87
+ if (!entry) return `### ${fullName}(argsJson: string)`;
88
+ const schema = entry.inputSchema?.properties || {};
89
+ const required = entry.inputSchema?.required || [];
90
+ const params = Object.entries(schema)
91
+ .map(([k, v]) => `${k}${required.includes(k) ? "" : "?"}: ${v.type || "any"}`)
92
+ .join(", ");
93
+ const desc = entry.description || entry.toolName;
94
+ const paramLine = params ? `\n- argsJson: \`{${params}}\`` : "";
95
+ return `### ${fullName}(argsJson: string)\n${desc}${paramLine}`;
96
+ })
97
+ .join("\n\n");
98
+
99
+ const systemPromptOverride = buildMCPAgentSystemPrompt(serverName, toolDocs);
100
+
101
+ console.log(
102
+ `[MCPAgentRunner] Spawning specialist for "${serverName}" (${Object.keys(serverTools).length} tools)`
103
+ );
104
+
105
+ return spawnSubAgent(taskDescription, {
106
+ ...options,
107
+ toolOverride: serverTools,
108
+ systemPromptOverride,
109
+ // MCP agents are always depth 1 — they don't spawn further sub-agents
110
+ depth: 1,
111
+ });
112
+ }