daemora 1.0.3 → 1.0.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 (121) hide show
  1. package/LICENSE +663 -0
  2. package/README.md +69 -19
  3. package/SOUL.md +25 -24
  4. package/daemora-ui/README.md +11 -0
  5. package/package.json +12 -2
  6. package/skills/api-development.md +35 -0
  7. package/skills/artifacts-builder/SKILL.md +74 -0
  8. package/skills/artifacts-builder/scripts/bundle-artifact.sh +54 -0
  9. package/skills/artifacts-builder/scripts/init-artifact.sh +322 -0
  10. package/skills/artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
  11. package/skills/brand-guidelines.md +73 -0
  12. package/skills/browser.md +77 -0
  13. package/skills/changelog-generator.md +104 -0
  14. package/skills/coding.md +26 -10
  15. package/skills/content-research-writer.md +538 -0
  16. package/skills/data-analysis.md +27 -0
  17. package/skills/debugging.md +33 -0
  18. package/skills/devops.md +37 -0
  19. package/skills/document-docx.md +197 -0
  20. package/skills/document-pdf.md +294 -0
  21. package/skills/document-pptx.md +484 -0
  22. package/skills/document-xlsx.md +289 -0
  23. package/skills/domain-name-brainstormer.md +212 -0
  24. package/skills/file-organizer.md +433 -0
  25. package/skills/frontend-design.md +42 -0
  26. package/skills/image-enhancer.md +99 -0
  27. package/skills/invoice-organizer.md +446 -0
  28. package/skills/lead-research-assistant.md +199 -0
  29. package/skills/mcp-builder/SKILL.md +328 -0
  30. package/skills/mcp-builder/reference/evaluation.md +602 -0
  31. package/skills/mcp-builder/reference/mcp_best_practices.md +915 -0
  32. package/skills/mcp-builder/reference/node_mcp_server.md +916 -0
  33. package/skills/mcp-builder/reference/python_mcp_server.md +752 -0
  34. package/skills/mcp-builder/scripts/connections.py +151 -0
  35. package/skills/mcp-builder/scripts/evaluation.py +373 -0
  36. package/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
  37. package/skills/mcp-builder/scripts/requirements.txt +2 -0
  38. package/skills/meeting-insights-analyzer.md +327 -0
  39. package/skills/orchestration.md +93 -0
  40. package/skills/raffle-winner-picker.md +159 -0
  41. package/skills/slack-gif-creator/SKILL.md +646 -0
  42. package/skills/slack-gif-creator/core/color_palettes.py +302 -0
  43. package/skills/slack-gif-creator/core/easing.py +230 -0
  44. package/skills/slack-gif-creator/core/frame_composer.py +469 -0
  45. package/skills/slack-gif-creator/core/gif_builder.py +246 -0
  46. package/skills/slack-gif-creator/core/typography.py +357 -0
  47. package/skills/slack-gif-creator/core/validators.py +264 -0
  48. package/skills/slack-gif-creator/core/visual_effects.py +494 -0
  49. package/skills/slack-gif-creator/requirements.txt +4 -0
  50. package/skills/slack-gif-creator/templates/bounce.py +106 -0
  51. package/skills/slack-gif-creator/templates/explode.py +331 -0
  52. package/skills/slack-gif-creator/templates/fade.py +329 -0
  53. package/skills/slack-gif-creator/templates/flip.py +291 -0
  54. package/skills/slack-gif-creator/templates/kaleidoscope.py +211 -0
  55. package/skills/slack-gif-creator/templates/morph.py +329 -0
  56. package/skills/slack-gif-creator/templates/move.py +293 -0
  57. package/skills/slack-gif-creator/templates/pulse.py +268 -0
  58. package/skills/slack-gif-creator/templates/shake.py +127 -0
  59. package/skills/slack-gif-creator/templates/slide.py +291 -0
  60. package/skills/slack-gif-creator/templates/spin.py +269 -0
  61. package/skills/slack-gif-creator/templates/wiggle.py +300 -0
  62. package/skills/slack-gif-creator/templates/zoom.py +312 -0
  63. package/skills/system-admin.md +44 -0
  64. package/skills/tailored-resume-generator.md +345 -0
  65. package/skills/theme-factory/SKILL.md +59 -0
  66. package/skills/theme-factory/theme-showcase.pdf +0 -0
  67. package/skills/theme-factory/themes/arctic-frost.md +19 -0
  68. package/skills/theme-factory/themes/botanical-garden.md +19 -0
  69. package/skills/theme-factory/themes/desert-rose.md +19 -0
  70. package/skills/theme-factory/themes/forest-canopy.md +19 -0
  71. package/skills/theme-factory/themes/golden-hour.md +19 -0
  72. package/skills/theme-factory/themes/midnight-galaxy.md +19 -0
  73. package/skills/theme-factory/themes/modern-minimalist.md +19 -0
  74. package/skills/theme-factory/themes/ocean-depths.md +19 -0
  75. package/skills/theme-factory/themes/sunset-boulevard.md +19 -0
  76. package/skills/theme-factory/themes/tech-innovation.md +19 -0
  77. package/skills/video-downloader.md +99 -0
  78. package/skills/web-development.md +32 -0
  79. package/skills/webapp-testing/SKILL.md +96 -0
  80. package/skills/webapp-testing/examples/console_logging.py +35 -0
  81. package/skills/webapp-testing/examples/element_discovery.py +40 -0
  82. package/skills/webapp-testing/examples/static_html_automation.py +33 -0
  83. package/skills/webapp-testing/scripts/with_server.py +106 -0
  84. package/src/agents/SubAgentManager.js +57 -12
  85. package/src/api/openai-compat.js +212 -0
  86. package/src/channels/TelegramChannel.js +5 -2
  87. package/src/channels/index.js +7 -10
  88. package/src/cli.js +129 -50
  89. package/src/config/agentProfiles.js +1 -0
  90. package/src/config/default.js +10 -0
  91. package/src/config/models.js +317 -71
  92. package/src/config/permissions.js +12 -0
  93. package/src/core/AgentLoop.js +70 -50
  94. package/src/core/Compaction.js +84 -2
  95. package/src/core/MessageQueue.js +90 -0
  96. package/src/core/Task.js +13 -0
  97. package/src/core/TaskQueue.js +1 -1
  98. package/src/core/TaskRunner.js +80 -5
  99. package/src/index.js +328 -48
  100. package/src/mcp/MCPAgentRunner.js +48 -11
  101. package/src/mcp/MCPManager.js +40 -2
  102. package/src/models/ModelRouter.js +67 -1
  103. package/src/safety/DockerSandbox.js +212 -0
  104. package/src/safety/ExecApproval.js +118 -0
  105. package/src/scheduler/Heartbeat.js +56 -21
  106. package/src/services/cleanup.js +106 -0
  107. package/src/services/sessions.js +39 -1
  108. package/src/setup/wizard.js +75 -4
  109. package/src/skills/SkillLoader.js +104 -17
  110. package/src/storage/TaskStore.js +19 -1
  111. package/src/systemPrompt.js +171 -328
  112. package/src/tools/browserAutomation.js +615 -104
  113. package/src/tools/executeCommand.js +19 -1
  114. package/src/tools/index.js +6 -0
  115. package/src/tools/manageAgents.js +55 -4
  116. package/src/tools/replyWithFile.js +62 -0
  117. package/src/tools/screenCapture.js +12 -1
  118. package/src/tools/taskManager.js +164 -0
  119. package/src/tools/useMCP.js +3 -1
  120. package/src/utils/Embeddings.js +157 -10
  121. package/src/webhooks/WebhookHandler.js +107 -0
package/src/index.js CHANGED
@@ -1,15 +1,18 @@
1
1
  import express from "express";
2
- import { mkdirSync } from "fs";
2
+ import { mkdirSync, existsSync } from "fs";
3
+ import { join, dirname } from "path";
4
+ import { fileURLToPath } from "url";
3
5
  import { toolFunctions } from "./tools/index.js";
4
- import { getSession } from "./services/sessions.js";
6
+ import { getSession, listSessions, createSession, clearSession } from "./services/sessions.js";
5
7
  import { config } from "./config/default.js";
6
8
  import { listAvailableModels } from "./models/ModelRouter.js";
7
9
  import taskQueue from "./core/TaskQueue.js";
8
10
  import taskRunner from "./core/TaskRunner.js";
9
- import { loadTask, listTasks } from "./storage/TaskStore.js";
11
+ import { loadTask, listTasks, listChildTasks } from "./storage/TaskStore.js";
10
12
  import { getTodayCost } from "./core/CostTracker.js";
11
13
  import supervisor from "./agents/Supervisor.js";
12
- import { getActiveSubAgentCount } from "./agents/SubAgentManager.js";
14
+ import { getActiveSubAgentCount, listActiveAgents } from "./agents/SubAgentManager.js";
15
+ import eventBus from "./core/EventBus.js";
13
16
  import channelRegistry from "./channels/index.js";
14
17
  import skillLoader from "./skills/SkillLoader.js";
15
18
  import mcpManager from "./mcp/MCPManager.js";
@@ -22,6 +25,12 @@ import voiceWebhook from "./voice/VoiceWebhook.js";
22
25
  import daemonManager from "./daemon/DaemonManager.js";
23
26
  import secretVault from "./safety/SecretVault.js";
24
27
  import tenantManager from "./tenants/TenantManager.js";
28
+ import { runCleanup } from "./services/cleanup.js";
29
+ import webhookHandler from "./webhooks/WebhookHandler.js";
30
+ import execApproval from "./safety/ExecApproval.js";
31
+ import openaiCompat from "./api/openai-compat.js";
32
+
33
+ const __dirname = dirname(fileURLToPath(import.meta.url));
25
34
 
26
35
  // Ensure all data directories exist
27
36
  const dirs = [
@@ -37,6 +46,14 @@ for (const dir of dirs) {
37
46
  mkdirSync(dir, { recursive: true });
38
47
  }
39
48
 
49
+ // Auto-cleanup old data on startup
50
+ if (config.cleanupAfterDays > 0) {
51
+ const cleaned = runCleanup(config.cleanupAfterDays);
52
+ if (cleaned.total > 0) {
53
+ console.log(`[Cleanup] Deleted ${cleaned.total} file(s) older than ${config.cleanupAfterDays} days (tasks: ${cleaned.tasks}, audit: ${cleaned.audit}, costs: ${cleaned.costs}, sessions: ${cleaned.sessions})`);
54
+ }
55
+ }
56
+
40
57
  // Initialize task system
41
58
  taskQueue.init();
42
59
  taskRunner.start();
@@ -55,8 +72,32 @@ mountA2AServer(app);
55
72
  // Mount voice call webhooks (Twilio callbacks during live calls)
56
73
  app.use("/voice", voiceWebhook);
57
74
 
75
+ // Mount webhook triggers (external integrations, CI/CD, GitHub webhooks)
76
+ app.use("/hooks", webhookHandler);
77
+
78
+ // Mount OpenAI-compatible API (gated by OPENAI_COMPAT_ENABLED)
79
+ if (process.env.OPENAI_COMPAT_ENABLED === "true") {
80
+ app.use("/v1", openaiCompat);
81
+ console.log("[Server] OpenAI-compatible API enabled at /v1/chat/completions");
82
+ }
83
+
84
+ // --- Security middleware ---
85
+ const localOnly = (req, res, next) => {
86
+ const remoteAddress = req.socket.remoteAddress;
87
+ // Support both IPv4 and IPv6 localhost
88
+ if (remoteAddress === "127.0.0.1" || remoteAddress === "::ffff:127.0.0.1" || remoteAddress === "::1") {
89
+ next();
90
+ } else {
91
+ console.warn(`[Security] Blocked non-local request from ${remoteAddress}`);
92
+ res.status(403).json({ error: "Access denied. Only local requests are allowed." });
93
+ }
94
+ };
95
+
96
+ // Apply local-only security to all API routes
97
+ app.use("/api", localOnly);
98
+
58
99
  // --- Health check ---
59
- app.get("/health", (req, res) => {
100
+ app.get("/api/health", (req, res) => {
60
101
  res.json({
61
102
  status: "ok",
62
103
  uptime: process.uptime(),
@@ -69,14 +110,57 @@ app.get("/health", (req, res) => {
69
110
  });
70
111
  });
71
112
 
72
- // --- Chat endpoint (DISABLED - no authentication, anyone on the network could submit tasks) ---
73
- // Uncomment and add authentication middleware before re-enabling.
74
- // app.post("/chat", async (req, res) => { ... });
113
+ // --- Chat endpoint (Async returns taskId, client uses SSE to stream) ---
114
+ app.post("/api/chat", (req, res) => {
115
+ try {
116
+ const { input, sessionId, model, priority, tenantId } = req.body;
117
+ if (!input) return res.status(400).json({ error: "input is required" });
118
+
119
+ const task = taskQueue.enqueue({
120
+ input,
121
+ channel: "http",
122
+ sessionId: sessionId || "local-user",
123
+ tenantId: tenantId || "http:local",
124
+ model,
125
+ priority: priority || 5,
126
+ type: "chat",
127
+ });
75
128
 
76
- // --- Task submit endpoint (DISABLED - same reason, unauthenticated) ---
77
- // app.post("/tasks", (req, res) => { ... });
129
+ res.status(202).json({
130
+ taskId: task.id,
131
+ sessionId: task.sessionId,
132
+ status: "queued",
133
+ });
134
+ } catch (error) {
135
+ res.status(500).json({ error: error.message });
136
+ }
137
+ });
78
138
 
79
- app.get("/tasks/:id", (req, res) => {
139
+ // --- Task submit endpoint (Async) ---
140
+ app.post("/api/tasks", (req, res) => {
141
+ try {
142
+ const { input, sessionId, model, priority } = req.body;
143
+ if (!input) return res.status(400).json({ error: "input is required" });
144
+
145
+ const task = taskQueue.enqueue({
146
+ input,
147
+ channel: "http",
148
+ sessionId: sessionId || "local-user",
149
+ model,
150
+ priority: priority || 5,
151
+ });
152
+
153
+ res.status(202).json({
154
+ message: "Task enqueued",
155
+ taskId: task.id,
156
+ status: task.status,
157
+ });
158
+ } catch (error) {
159
+ res.status(500).json({ error: error.message });
160
+ }
161
+ });
162
+
163
+ app.get("/api/tasks/:id", (req, res) => {
80
164
  const task = loadTask(req.params.id);
81
165
  if (!task) {
82
166
  return res.status(404).json({ error: "Task not found" });
@@ -84,19 +168,26 @@ app.get("/tasks/:id", (req, res) => {
84
168
  res.json(task);
85
169
  });
86
170
 
87
- app.get("/tasks", (req, res) => {
88
- const { limit, status } = req.query;
171
+ app.get("/api/tasks", (req, res) => {
172
+ const { limit, status, type } = req.query;
89
173
  const tasks = listTasks({
90
174
  limit: limit ? parseInt(limit, 10) : 20,
91
175
  status: status || null,
176
+ type: type || null,
92
177
  });
93
178
  res.json({
94
179
  tasks: tasks.map((t) => ({
95
180
  id: t.id,
96
181
  status: t.status,
182
+ type: t.type || "chat",
183
+ title: t.title || null,
97
184
  channel: t.channel,
98
- input: t.input.slice(0, 100),
185
+ input: t.input?.slice(0, 100) || "",
99
186
  cost: t.cost,
187
+ parentTaskId: t.parentTaskId || null,
188
+ agentId: t.agentId || null,
189
+ agentCreated: t.agentCreated || false,
190
+ subAgents: t.subAgents || null,
100
191
  createdAt: t.createdAt,
101
192
  completedAt: t.completedAt,
102
193
  })),
@@ -104,17 +195,79 @@ app.get("/tasks", (req, res) => {
104
195
  });
105
196
  });
106
197
 
107
- // --- Session endpoint ---
108
- app.get("/sessions/:id", (req, res) => {
198
+ // --- Child tasks endpoint ---
199
+ app.get("/api/tasks/:id/children", (req, res) => {
200
+ const children = listChildTasks(req.params.id);
201
+ res.json({
202
+ parentTaskId: req.params.id,
203
+ children: children.map((t) => ({
204
+ id: t.id,
205
+ status: t.status,
206
+ type: t.type || "chat",
207
+ title: t.title || null,
208
+ input: t.input?.slice(0, 100) || "",
209
+ agentId: t.agentId || null,
210
+ cost: t.cost,
211
+ createdAt: t.createdAt,
212
+ completedAt: t.completedAt,
213
+ })),
214
+ });
215
+ });
216
+
217
+ // --- Session endpoints ---
218
+ app.get("/api/sessions", (req, res) => {
219
+ const sessionIds = listSessions();
220
+ const sessionList = sessionIds.map(id => {
221
+ const s = getSession(id);
222
+ return {
223
+ sessionId: s.sessionId,
224
+ createdAt: s.createdAt,
225
+ lastMessage: s.messages.length > 0 ? s.messages[s.messages.length - 1].content.slice(0, 50) : "Empty chat",
226
+ messageCount: s.messages.length
227
+ };
228
+ }).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
229
+
230
+ res.json({ sessions: sessionList });
231
+ });
232
+
233
+ app.post("/api/sessions", (req, res) => {
234
+ const session = createSession();
235
+ res.status(201).json(session);
236
+ });
237
+
238
+ app.get("/api/sessions/:id", (req, res) => {
109
239
  const session = getSession(req.params.id);
110
240
  if (!session) {
111
241
  return res.status(404).json({ error: "Session not found" });
112
242
  }
113
- res.json(session);
243
+ // Filter out any leaked tool_call / tool_result messages
244
+ const cleanMessages = (session.messages || []).filter(msg => {
245
+ if (!msg.content || typeof msg.content !== "string") return false;
246
+ if (msg.role !== "user" && msg.role !== "assistant") return false;
247
+ const trimmed = msg.content.trimStart();
248
+ if (trimmed.startsWith("{")) {
249
+ try {
250
+ const parsed = JSON.parse(trimmed);
251
+ if (parsed.type === "tool_call" || parsed.tool_call) return false;
252
+ if (parsed.tool_name) return false;
253
+ if (parsed.type === "text" && parsed.finalResponse !== undefined) return false;
254
+ } catch { /* not JSON, keep it */ }
255
+ }
256
+ return true;
257
+ });
258
+ res.json({ ...session, messages: cleanMessages });
259
+ });
260
+
261
+ app.delete("/api/sessions/:id", (req, res) => {
262
+ const deleted = clearSession(req.params.id);
263
+ if (!deleted) {
264
+ return res.status(404).json({ error: "Session not found" });
265
+ }
266
+ res.json({ message: "Session deleted" });
114
267
  });
115
268
 
116
269
  // --- Config endpoint ---
117
- app.get("/config", (req, res) => {
270
+ app.get("/api/config", (req, res) => {
118
271
  res.json({
119
272
  defaultModel: config.defaultModel,
120
273
  permissionTier: config.permissionTier,
@@ -129,21 +282,115 @@ app.get("/config", (req, res) => {
129
282
  });
130
283
 
131
284
  // --- Models endpoint ---
132
- app.get("/models", (req, res) => {
285
+ app.get("/api/models", (req, res) => {
286
+ const available = listAvailableModels();
133
287
  res.json({
134
288
  default: config.defaultModel,
135
- available: listAvailableModels(),
289
+ available: available.map(m => ({
290
+ ...m,
291
+ pricingPerMTok: m.costPer1kInput > 0 ? {
292
+ input: `$${(m.costPer1kInput * 1000).toFixed(2)}`,
293
+ output: `$${(m.costPer1kOutput * 1000).toFixed(2)}`,
294
+ } : { input: "$0", output: "$0" },
295
+ })),
136
296
  });
137
297
  });
138
298
 
299
+ // --- Model switch endpoint ---
300
+ app.post("/api/model", (req, res) => {
301
+ const { model } = req.body;
302
+ if (!model) return res.status(400).json({ error: "model is required" });
303
+
304
+ const available = listAvailableModels();
305
+ const match = available.find(m => m.id === model);
306
+ if (!match) {
307
+ return res.status(400).json({
308
+ error: `Unknown or unavailable model: ${model}`,
309
+ available: available.map(m => m.id),
310
+ });
311
+ }
312
+
313
+ config.defaultModel = model;
314
+ res.json({ message: `Default model set to ${model}`, model });
315
+ });
316
+
139
317
  // --- Supervisor endpoint ---
140
- app.get("/supervisor", (req, res) => {
318
+ app.get("/api/supervisor", (req, res) => {
141
319
  res.json({
142
320
  warnings: supervisor.getWarnings(),
143
321
  activeSubAgents: getActiveSubAgentCount(),
144
322
  });
145
323
  });
146
324
 
325
+ // --- Sub-agents endpoint ---
326
+ app.get("/api/subagents", (req, res) => {
327
+ res.json({ agents: listActiveAgents() });
328
+ });
329
+
330
+ // --- SSE streaming endpoint for task events ---
331
+ app.get("/api/tasks/:id/stream", (req, res) => {
332
+ const taskId = req.params.id;
333
+ res.writeHead(200, {
334
+ "Content-Type": "text/event-stream",
335
+ "Cache-Control": "no-cache",
336
+ Connection: "keep-alive",
337
+ });
338
+
339
+ const send = (event, data) => {
340
+ res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
341
+ };
342
+
343
+ // Send current state immediately
344
+ const task = loadTask(taskId);
345
+ if (task) send("task:state", task);
346
+
347
+ const onTool = (evt) => {
348
+ if (evt.taskId === taskId) send("tool:after", evt);
349
+ };
350
+ const onModel = (evt) => {
351
+ if (evt.taskId === taskId || evt.taskId?.startsWith("subagent-")) send("model:called", evt);
352
+ };
353
+ const onAgentSpawn = (evt) => {
354
+ if (evt.parentTaskId === taskId) send("agent:spawned", evt);
355
+ };
356
+ const onAgentDone = (evt) => {
357
+ if (evt.parentTaskId === taskId) send("agent:finished", evt);
358
+ };
359
+ const onComplete = (evt) => {
360
+ if (evt.taskId === taskId) {
361
+ const finalTask = loadTask(taskId);
362
+ send("task:completed", finalTask || evt);
363
+ cleanup();
364
+ res.end();
365
+ }
366
+ };
367
+ const onFail = (evt) => {
368
+ if (evt.taskId === taskId) {
369
+ send("task:failed", evt);
370
+ cleanup();
371
+ res.end();
372
+ }
373
+ };
374
+
375
+ eventBus.on("tool:after", onTool);
376
+ eventBus.on("model:called", onModel);
377
+ eventBus.on("agent:spawned", onAgentSpawn);
378
+ eventBus.on("agent:finished", onAgentDone);
379
+ eventBus.on("task:completed", onComplete);
380
+ eventBus.on("task:failed", onFail);
381
+
382
+ const cleanup = () => {
383
+ eventBus.removeListener("tool:after", onTool);
384
+ eventBus.removeListener("model:called", onModel);
385
+ eventBus.removeListener("agent:spawned", onAgentSpawn);
386
+ eventBus.removeListener("agent:finished", onAgentDone);
387
+ eventBus.removeListener("task:completed", onComplete);
388
+ eventBus.removeListener("task:failed", onFail);
389
+ };
390
+
391
+ req.on("close", cleanup);
392
+ });
393
+
147
394
  // --- WhatsApp webhook ---
148
395
  app.post("/webhooks/whatsapp", async (req, res) => {
149
396
  const whatsapp = channelRegistry.get("whatsapp");
@@ -209,22 +456,22 @@ app.post("/webhooks/line", express.raw({ type: "application/json" }), async (req
209
456
  });
210
457
 
211
458
  // --- Channels endpoint ---
212
- app.get("/channels", (req, res) => {
459
+ app.get("/api/channels", (req, res) => {
213
460
  res.json({ channels: channelRegistry.list() });
214
461
  });
215
462
 
216
463
  // --- Skills endpoint ---
217
- app.get("/skills", (req, res) => {
464
+ app.get("/api/skills", (req, res) => {
218
465
  res.json({ skills: skillLoader.list() });
219
466
  });
220
467
 
221
- app.post("/skills/reload", (req, res) => {
468
+ app.post("/api/skills/reload", (req, res) => {
222
469
  skillLoader.reload();
223
470
  res.json({ message: "Skills reloaded", skills: skillLoader.list() });
224
471
  });
225
472
 
226
473
  // --- Schedule endpoints ---
227
- app.post("/schedules", (req, res) => {
474
+ app.post("/api/schedules", (req, res) => {
228
475
  try {
229
476
  const { cronExpression, taskInput, channel, model, name } = req.body;
230
477
  if (!cronExpression || !taskInput) {
@@ -237,22 +484,22 @@ app.post("/schedules", (req, res) => {
237
484
  }
238
485
  });
239
486
 
240
- app.get("/schedules", (req, res) => {
487
+ app.get("/api/schedules", (req, res) => {
241
488
  res.json({ schedules: scheduler.list() });
242
489
  });
243
490
 
244
- app.delete("/schedules/:id", (req, res) => {
491
+ app.delete("/api/schedules/:id", (req, res) => {
245
492
  scheduler.delete(req.params.id);
246
493
  res.json({ message: "Schedule deleted" });
247
494
  });
248
495
 
249
496
  // --- Audit endpoint ---
250
- app.get("/audit", (req, res) => {
497
+ app.get("/api/audit", (req, res) => {
251
498
  res.json(auditLog.stats());
252
499
  });
253
500
 
254
501
  // --- MCP endpoints ---
255
- app.get("/mcp", (req, res) => {
502
+ app.get("/api/mcp", (req, res) => {
256
503
  const cfg = mcpManager.readConfig().mcpServers || {};
257
504
  const live = mcpManager.list();
258
505
  const servers = Object.entries(cfg)
@@ -267,20 +514,24 @@ app.get("/mcp", (req, res) => {
267
514
  type: serverCfg.command ? "stdio" : (serverCfg.transport || "http"),
268
515
  command: serverCfg.command || null,
269
516
  url: serverCfg.url || null,
517
+ description: serverCfg.description || null,
518
+ envKeys: serverCfg.env ? Object.keys(serverCfg.env) : [],
519
+ headerKeys: serverCfg.headers ? Object.keys(serverCfg.headers) : [],
270
520
  };
271
521
  });
272
522
  res.json({ servers });
273
523
  });
274
524
 
275
525
  // Add a new MCP server
276
- app.post("/mcp", async (req, res) => {
277
- const { name, command, args, url, transport, env } = req.body;
526
+ app.post("/api/mcp", async (req, res) => {
527
+ const { name, command, args, url, transport, env, headers, description } = req.body;
278
528
  if (!name) return res.status(400).json({ error: "name is required" });
279
529
  if (!command && !url) return res.status(400).json({ error: "command (stdio) or url (http/sse) required" });
280
530
 
281
531
  const serverConfig = command
282
- ? { command, args: args || [], env: env || {} }
283
- : { url, transport: transport || undefined, env: env || {} };
532
+ ? { command, args: args || [], ...(env && Object.keys(env).length > 0 ? { env } : {}) }
533
+ : { url, ...(transport ? { transport } : {}), ...(headers && Object.keys(headers).length > 0 ? { headers } : {}) };
534
+ if (description) serverConfig.description = description;
284
535
 
285
536
  try {
286
537
  const result = await mcpManager.addServer(name, serverConfig);
@@ -291,7 +542,7 @@ app.post("/mcp", async (req, res) => {
291
542
  });
292
543
 
293
544
  // Remove an MCP server
294
- app.delete("/mcp/:name", async (req, res) => {
545
+ app.delete("/api/mcp/:name", async (req, res) => {
295
546
  try {
296
547
  const result = await mcpManager.removeServer(req.params.name);
297
548
  res.json({ message: result });
@@ -301,7 +552,7 @@ app.delete("/mcp/:name", async (req, res) => {
301
552
  });
302
553
 
303
554
  // Enable / disable / reload an MCP server
304
- app.post("/mcp/:name/:action", async (req, res) => {
555
+ app.post("/api/mcp/:name/:action", async (req, res) => {
305
556
  const { name, action } = req.params;
306
557
  try {
307
558
  let result;
@@ -316,11 +567,11 @@ app.post("/mcp/:name/:action", async (req, res) => {
316
567
  });
317
568
 
318
569
  // --- Daemon endpoints ---
319
- app.get("/daemon/status", (req, res) => {
570
+ app.get("/api/daemon/status", (req, res) => {
320
571
  res.json(daemonManager.status());
321
572
  });
322
573
 
323
- app.post("/daemon/:action", (req, res) => {
574
+ app.post("/api/daemon/:action", (req, res) => {
324
575
  const { action } = req.params;
325
576
  try {
326
577
  switch (action) {
@@ -353,14 +604,14 @@ app.post("/daemon/:action", (req, res) => {
353
604
  });
354
605
 
355
606
  // --- Vault endpoints ---
356
- app.get("/vault/status", (req, res) => {
607
+ app.get("/api/vault/status", (req, res) => {
357
608
  res.json({
358
609
  exists: secretVault.exists(),
359
610
  unlocked: secretVault.isUnlocked(),
360
611
  });
361
612
  });
362
613
 
363
- app.post("/vault/unlock", (req, res) => {
614
+ app.post("/api/vault/unlock", (req, res) => {
364
615
  try {
365
616
  const { passphrase } = req.body;
366
617
  if (!passphrase) return res.status(400).json({ error: "passphrase is required" });
@@ -376,30 +627,30 @@ app.post("/vault/unlock", (req, res) => {
376
627
  }
377
628
  });
378
629
 
379
- app.post("/vault/lock", (req, res) => {
630
+ app.post("/api/vault/lock", (req, res) => {
380
631
  secretVault.lock();
381
632
  res.json({ message: "Vault locked" });
382
633
  });
383
634
 
384
635
  // --- Tenant endpoints ---
385
- app.get("/tenants", (req, res) => {
636
+ app.get("/api/tenants", (req, res) => {
386
637
  const tenants = tenantManager.list();
387
638
  res.json({ tenants, stats: tenantManager.stats() });
388
639
  });
389
640
 
390
- app.get("/tenants/:id", (req, res) => {
641
+ app.get("/api/tenants/:id", (req, res) => {
391
642
  const tenant = tenantManager.get(decodeURIComponent(req.params.id));
392
643
  if (!tenant) return res.status(404).json({ error: "Tenant not found" });
393
644
  res.json(tenant);
394
645
  });
395
646
 
396
- app.patch("/tenants/:id", (req, res) => {
647
+ app.patch("/api/tenants/:id", (req, res) => {
397
648
  const id = decodeURIComponent(req.params.id);
398
649
  const updated = tenantManager.set(id, req.body);
399
650
  res.json(updated);
400
651
  });
401
652
 
402
- app.post("/tenants/:id/suspend", (req, res) => {
653
+ app.post("/api/tenants/:id/suspend", (req, res) => {
403
654
  const id = decodeURIComponent(req.params.id);
404
655
  const { reason } = req.body;
405
656
  const updated = tenantManager.suspend(id, reason || "");
@@ -407,29 +658,44 @@ app.post("/tenants/:id/suspend", (req, res) => {
407
658
  res.json(updated);
408
659
  });
409
660
 
410
- app.post("/tenants/:id/unsuspend", (req, res) => {
661
+ app.post("/api/tenants/:id/unsuspend", (req, res) => {
411
662
  const id = decodeURIComponent(req.params.id);
412
663
  const updated = tenantManager.unsuspend(id);
413
664
  if (!updated) return res.status(404).json({ error: "Tenant not found" });
414
665
  res.json(updated);
415
666
  });
416
667
 
417
- app.post("/tenants/:id/reset", (req, res) => {
668
+ app.post("/api/tenants/:id/reset", (req, res) => {
418
669
  const id = decodeURIComponent(req.params.id);
419
670
  const updated = tenantManager.reset(id);
420
671
  if (!updated) return res.status(404).json({ error: "Tenant not found" });
421
672
  res.json(updated);
422
673
  });
423
674
 
424
- app.delete("/tenants/:id", (req, res) => {
675
+ app.delete("/api/tenants/:id", (req, res) => {
425
676
  const id = decodeURIComponent(req.params.id);
426
677
  const deleted = tenantManager.delete(id);
427
678
  if (!deleted) return res.status(404).json({ error: "Tenant not found" });
428
679
  res.json({ message: "Tenant deleted" });
429
680
  });
430
681
 
682
+ // --- Exec approvals ---
683
+ app.get("/api/approvals", (req, res) => {
684
+ res.json({ approvals: execApproval.listPending(), mode: execApproval.mode });
685
+ });
686
+
687
+ app.post("/api/approvals/:id", (req, res) => {
688
+ const { decision } = req.body;
689
+ if (!["allow", "allow-once", "deny"].includes(decision)) {
690
+ return res.status(400).json({ error: 'decision must be "allow", "allow-once", or "deny"' });
691
+ }
692
+ const resolved = execApproval.resolveApproval(req.params.id, decision);
693
+ if (!resolved) return res.status(404).json({ error: "Approval not found or expired" });
694
+ res.json({ message: `Approval ${req.params.id} → ${decision}` });
695
+ });
696
+
431
697
  // --- Costs endpoint ---
432
- app.get("/costs/today", (req, res) => {
698
+ app.get("/api/costs/today", (req, res) => {
433
699
  res.json({
434
700
  date: new Date().toISOString().split("T")[0],
435
701
  totalCost: getTodayCost(),
@@ -438,6 +704,20 @@ app.get("/costs/today", (req, res) => {
438
704
  });
439
705
  });
440
706
 
707
+ // --- Static UI ---
708
+ const uiPath = join(__dirname, "..", "daemora-ui", "dist");
709
+ if (existsSync(uiPath)) {
710
+ app.use(express.static(uiPath));
711
+ // Serve index.html for all other routes (React Router support)
712
+ app.get(/.*/, (req, res, next) => {
713
+ if (req.path.startsWith("/api/") || req.path.startsWith("/webhooks/") || req.path.startsWith("/voice/") || req.path.startsWith("/a2a/") || req.path.startsWith("/hooks/") || req.path.startsWith("/v1/")) {
714
+ return next();
715
+ }
716
+ res.sendFile(join(uiPath, "index.html"));
717
+ });
718
+ console.log(`[Server] Serving UI from ${uiPath}`);
719
+ }
720
+
441
721
  // --- Start server ---
442
722
  app.listen(config.port, async () => {
443
723
  console.log("\n--- Daemora Server ---");