daemora 1.0.4 → 1.0.6

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 (123) hide show
  1. package/LICENSE +663 -0
  2. package/README.md +69 -19
  3. package/SOUL.md +29 -26
  4. package/config/mcp.json +126 -66
  5. package/daemora-ui/README.md +11 -0
  6. package/package.json +12 -2
  7. package/skills/api-development.md +35 -0
  8. package/skills/artifacts-builder/SKILL.md +74 -0
  9. package/skills/artifacts-builder/scripts/bundle-artifact.sh +54 -0
  10. package/skills/artifacts-builder/scripts/init-artifact.sh +322 -0
  11. package/skills/artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
  12. package/skills/brand-guidelines.md +73 -0
  13. package/skills/browser.md +77 -0
  14. package/skills/changelog-generator.md +104 -0
  15. package/skills/coding.md +26 -10
  16. package/skills/content-research-writer.md +538 -0
  17. package/skills/data-analysis.md +27 -0
  18. package/skills/debugging.md +33 -0
  19. package/skills/devops.md +37 -0
  20. package/skills/document-docx.md +197 -0
  21. package/skills/document-pdf.md +294 -0
  22. package/skills/document-pptx.md +484 -0
  23. package/skills/document-xlsx.md +289 -0
  24. package/skills/domain-name-brainstormer.md +212 -0
  25. package/skills/file-organizer.md +433 -0
  26. package/skills/frontend-design.md +42 -0
  27. package/skills/image-enhancer.md +99 -0
  28. package/skills/invoice-organizer.md +446 -0
  29. package/skills/lead-research-assistant.md +199 -0
  30. package/skills/mcp-builder/SKILL.md +328 -0
  31. package/skills/mcp-builder/reference/evaluation.md +602 -0
  32. package/skills/mcp-builder/reference/mcp_best_practices.md +915 -0
  33. package/skills/mcp-builder/reference/node_mcp_server.md +916 -0
  34. package/skills/mcp-builder/reference/python_mcp_server.md +752 -0
  35. package/skills/mcp-builder/scripts/connections.py +151 -0
  36. package/skills/mcp-builder/scripts/evaluation.py +373 -0
  37. package/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
  38. package/skills/mcp-builder/scripts/requirements.txt +2 -0
  39. package/skills/meeting-insights-analyzer.md +327 -0
  40. package/skills/orchestration.md +93 -0
  41. package/skills/raffle-winner-picker.md +159 -0
  42. package/skills/slack-gif-creator/SKILL.md +646 -0
  43. package/skills/slack-gif-creator/core/color_palettes.py +302 -0
  44. package/skills/slack-gif-creator/core/easing.py +230 -0
  45. package/skills/slack-gif-creator/core/frame_composer.py +469 -0
  46. package/skills/slack-gif-creator/core/gif_builder.py +246 -0
  47. package/skills/slack-gif-creator/core/typography.py +357 -0
  48. package/skills/slack-gif-creator/core/validators.py +264 -0
  49. package/skills/slack-gif-creator/core/visual_effects.py +494 -0
  50. package/skills/slack-gif-creator/requirements.txt +4 -0
  51. package/skills/slack-gif-creator/templates/bounce.py +106 -0
  52. package/skills/slack-gif-creator/templates/explode.py +331 -0
  53. package/skills/slack-gif-creator/templates/fade.py +329 -0
  54. package/skills/slack-gif-creator/templates/flip.py +291 -0
  55. package/skills/slack-gif-creator/templates/kaleidoscope.py +211 -0
  56. package/skills/slack-gif-creator/templates/morph.py +329 -0
  57. package/skills/slack-gif-creator/templates/move.py +293 -0
  58. package/skills/slack-gif-creator/templates/pulse.py +268 -0
  59. package/skills/slack-gif-creator/templates/shake.py +127 -0
  60. package/skills/slack-gif-creator/templates/slide.py +291 -0
  61. package/skills/slack-gif-creator/templates/spin.py +269 -0
  62. package/skills/slack-gif-creator/templates/wiggle.py +300 -0
  63. package/skills/slack-gif-creator/templates/zoom.py +312 -0
  64. package/skills/system-admin.md +44 -0
  65. package/skills/tailored-resume-generator.md +345 -0
  66. package/skills/theme-factory/SKILL.md +59 -0
  67. package/skills/theme-factory/theme-showcase.pdf +0 -0
  68. package/skills/theme-factory/themes/arctic-frost.md +19 -0
  69. package/skills/theme-factory/themes/botanical-garden.md +19 -0
  70. package/skills/theme-factory/themes/desert-rose.md +19 -0
  71. package/skills/theme-factory/themes/forest-canopy.md +19 -0
  72. package/skills/theme-factory/themes/golden-hour.md +19 -0
  73. package/skills/theme-factory/themes/midnight-galaxy.md +19 -0
  74. package/skills/theme-factory/themes/modern-minimalist.md +19 -0
  75. package/skills/theme-factory/themes/ocean-depths.md +19 -0
  76. package/skills/theme-factory/themes/sunset-boulevard.md +19 -0
  77. package/skills/theme-factory/themes/tech-innovation.md +19 -0
  78. package/skills/video-downloader.md +99 -0
  79. package/skills/web-development.md +32 -0
  80. package/skills/webapp-testing/SKILL.md +96 -0
  81. package/skills/webapp-testing/examples/console_logging.py +35 -0
  82. package/skills/webapp-testing/examples/element_discovery.py +40 -0
  83. package/skills/webapp-testing/examples/static_html_automation.py +33 -0
  84. package/skills/webapp-testing/scripts/with_server.py +106 -0
  85. package/src/agents/SubAgentManager.js +134 -16
  86. package/src/agents/systemPrompt.js +427 -0
  87. package/src/api/openai-compat.js +212 -0
  88. package/src/channels/TelegramChannel.js +5 -2
  89. package/src/channels/index.js +7 -10
  90. package/src/cli.js +281 -55
  91. package/src/config/agentProfiles.js +1 -0
  92. package/src/config/default.js +15 -1
  93. package/src/config/models.js +314 -78
  94. package/src/config/permissions.js +12 -0
  95. package/src/core/AgentLoop.js +70 -50
  96. package/src/core/Compaction.js +111 -11
  97. package/src/core/MessageQueue.js +90 -0
  98. package/src/core/Task.js +13 -0
  99. package/src/core/TaskQueue.js +1 -1
  100. package/src/core/TaskRunner.js +81 -6
  101. package/src/index.js +725 -59
  102. package/src/mcp/MCPAgentRunner.js +48 -11
  103. package/src/mcp/MCPManager.js +40 -2
  104. package/src/models/ModelRouter.js +74 -4
  105. package/src/safety/DockerSandbox.js +212 -0
  106. package/src/safety/ExecApproval.js +118 -0
  107. package/src/scheduler/Heartbeat.js +56 -21
  108. package/src/services/cleanup.js +106 -0
  109. package/src/services/sessions.js +39 -1
  110. package/src/setup/wizard.js +125 -75
  111. package/src/skills/SkillLoader.js +132 -17
  112. package/src/storage/TaskStore.js +19 -1
  113. package/src/tools/browserAutomation.js +615 -104
  114. package/src/tools/executeCommand.js +19 -1
  115. package/src/tools/index.js +7 -1
  116. package/src/tools/manageAgents.js +55 -4
  117. package/src/tools/replyWithFile.js +62 -0
  118. package/src/tools/screenCapture.js +12 -1
  119. package/src/tools/taskManager.js +164 -0
  120. package/src/tools/useMCP.js +3 -1
  121. package/src/utils/Embeddings.js +236 -12
  122. package/src/webhooks/WebhookHandler.js +107 -0
  123. package/src/systemPrompt.js +0 -528
package/src/index.js CHANGED
@@ -1,15 +1,19 @@
1
1
  import express from "express";
2
- import { mkdirSync } from "fs";
2
+ import { mkdirSync, existsSync, readFileSync, writeFileSync, readdirSync, unlinkSync } from "fs";
3
+ import { join, dirname } from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { randomBytes } from "crypto";
3
6
  import { toolFunctions } from "./tools/index.js";
4
- import { getSession } from "./services/sessions.js";
7
+ import { getSession, listSessions, createSession, clearSession } from "./services/sessions.js";
5
8
  import { config } from "./config/default.js";
6
9
  import { listAvailableModels } from "./models/ModelRouter.js";
7
10
  import taskQueue from "./core/TaskQueue.js";
8
11
  import taskRunner from "./core/TaskRunner.js";
9
- import { loadTask, listTasks } from "./storage/TaskStore.js";
12
+ import { loadTask, listTasks, listChildTasks } from "./storage/TaskStore.js";
10
13
  import { getTodayCost } from "./core/CostTracker.js";
11
14
  import supervisor from "./agents/Supervisor.js";
12
- import { getActiveSubAgentCount } from "./agents/SubAgentManager.js";
15
+ import { getActiveSubAgentCount, listActiveAgents } from "./agents/SubAgentManager.js";
16
+ import eventBus from "./core/EventBus.js";
13
17
  import channelRegistry from "./channels/index.js";
14
18
  import skillLoader from "./skills/SkillLoader.js";
15
19
  import mcpManager from "./mcp/MCPManager.js";
@@ -22,6 +26,12 @@ import voiceWebhook from "./voice/VoiceWebhook.js";
22
26
  import daemonManager from "./daemon/DaemonManager.js";
23
27
  import secretVault from "./safety/SecretVault.js";
24
28
  import tenantManager from "./tenants/TenantManager.js";
29
+ import { runCleanup } from "./services/cleanup.js";
30
+ import webhookHandler from "./webhooks/WebhookHandler.js";
31
+ import execApproval from "./safety/ExecApproval.js";
32
+ import openaiCompat from "./api/openai-compat.js";
33
+
34
+ const __dirname = dirname(fileURLToPath(import.meta.url));
25
35
 
26
36
  // Ensure all data directories exist
27
37
  const dirs = [
@@ -37,9 +47,16 @@ for (const dir of dirs) {
37
47
  mkdirSync(dir, { recursive: true });
38
48
  }
39
49
 
40
- // Initialize task system
50
+ // Auto-cleanup old data on startup
51
+ if (config.cleanupAfterDays > 0) {
52
+ const cleaned = runCleanup(config.cleanupAfterDays);
53
+ if (cleaned.total > 0) {
54
+ 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})`);
55
+ }
56
+ }
57
+
58
+ // Initialize task system (TaskRunner starts after full init — see startup sequence below)
41
59
  taskQueue.init();
42
- taskRunner.start();
43
60
  supervisor.start();
44
61
  auditLog.start();
45
62
  scheduler.start();
@@ -55,10 +72,118 @@ 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
+
86
+ // Security headers on all responses
87
+ app.use((req, res, next) => {
88
+ res.setHeader("X-Content-Type-Options", "nosniff");
89
+ res.setHeader("X-Frame-Options", "DENY");
90
+ res.setHeader("X-XSS-Protection", "1; mode=block");
91
+ res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
92
+ res.setHeader("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
93
+ next();
94
+ });
95
+
96
+ // Localhost-only: reject non-local IP addresses
97
+ const localOnly = (req, res, next) => {
98
+ const remoteAddress = req.socket.remoteAddress;
99
+ if (remoteAddress === "127.0.0.1" || remoteAddress === "::ffff:127.0.0.1" || remoteAddress === "::1") {
100
+ next();
101
+ } else {
102
+ console.warn(`[Security] Blocked non-local request from ${remoteAddress}`);
103
+ res.status(403).json({ error: "Access denied. Only local requests are allowed." });
104
+ }
105
+ };
106
+
107
+ // Origin validation: block DNS rebinding and cross-origin browser attacks.
108
+ // Browsers always send Origin on cross-origin requests. A malicious page on
109
+ // evil.com making fetch("http://localhost:8081/api/...") will have Origin: https://evil.com
110
+ // which we reject. Same-origin requests from our UI have no Origin or matching localhost.
111
+ const originGuard = (req, res, next) => {
112
+ const origin = req.headers.origin;
113
+ if (!origin) return next(); // Same-origin requests (no Origin header) — safe
114
+
115
+ // Allow only our own localhost origins
116
+ const allowedOrigins = [
117
+ `http://localhost:${config.port}`,
118
+ `http://127.0.0.1:${config.port}`,
119
+ `http://[::1]:${config.port}`,
120
+ ];
121
+ // Also allow Vite dev server (common dev ports)
122
+ for (const devPort of [5173, 5174, 3000, 3001]) {
123
+ allowedOrigins.push(`http://localhost:${devPort}`);
124
+ allowedOrigins.push(`http://127.0.0.1:${devPort}`);
125
+ }
126
+
127
+ if (allowedOrigins.includes(origin)) {
128
+ res.setHeader("Access-Control-Allow-Origin", origin);
129
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS");
130
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
131
+ if (req.method === "OPTIONS") return res.sendStatus(204);
132
+ return next();
133
+ }
134
+
135
+ console.warn(`[Security] Blocked cross-origin request from ${origin}`);
136
+ res.status(403).json({ error: "Cross-origin request blocked." });
137
+ };
138
+
139
+ // --- API Token auth ---
140
+ // Auto-generated on first start, stored on disk. Required for all /api/* requests.
141
+ // The UI receives the token via server-injected <meta> tag (no login needed).
142
+ // Other local tools (curl, scripts) read it from data/auth-token or pass via header.
143
+ const AUTH_TOKEN_PATH = join(config.dataDir, "auth-token");
144
+
145
+ function getOrCreateAuthToken() {
146
+ if (existsSync(AUTH_TOKEN_PATH)) {
147
+ const token = readFileSync(AUTH_TOKEN_PATH, "utf-8").trim();
148
+ if (token.length >= 32) return token;
149
+ }
150
+ const token = randomBytes(32).toString("hex");
151
+ mkdirSync(dirname(AUTH_TOKEN_PATH), { recursive: true });
152
+ writeFileSync(AUTH_TOKEN_PATH, token, { mode: 0o600 });
153
+ console.log("[Security] Generated new API auth token");
154
+ return token;
155
+ }
156
+
157
+ const API_TOKEN = getOrCreateAuthToken();
158
+
159
+ const tokenAuth = (req, res, next) => {
160
+ // Health endpoint is public (monitoring/readiness probes)
161
+ if (req.path === "/api/health") return next();
162
+
163
+ // Check Authorization: Bearer <token> header
164
+ const authHeader = req.headers.authorization;
165
+ if (authHeader === `Bearer ${API_TOKEN}`) return next();
166
+
167
+ // Check X-Auth-Token header (simpler for scripts/curl)
168
+ if (req.headers["x-auth-token"] === API_TOKEN) return next();
169
+
170
+ // Check ?token= query param (for SSE/EventSource which can't set headers)
171
+ if (req.query.token === API_TOKEN) return next();
172
+
173
+ console.warn(`[Security] Rejected unauthenticated request: ${req.method} ${req.path}`);
174
+ res.status(401).json({ error: "Authentication required. Include Authorization: Bearer <token> header." });
175
+ };
176
+
177
+ // Apply security to all API routes: IP check → origin check → token auth
178
+ app.use("/api", localOnly);
179
+ app.use("/api", originGuard);
180
+ app.use("/api", tokenAuth);
181
+
58
182
  // --- Health check ---
59
- app.get("/health", (req, res) => {
183
+ app.get("/api/health", (req, res) => {
60
184
  res.json({
61
- status: "ok",
185
+ status: _serverReady ? "ok" : "starting",
186
+ ready: _serverReady,
62
187
  uptime: process.uptime(),
63
188
  timestamp: new Date().toISOString(),
64
189
  tools: Object.keys(toolFunctions).length,
@@ -69,14 +194,57 @@ app.get("/health", (req, res) => {
69
194
  });
70
195
  });
71
196
 
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) => { ... });
197
+ // --- Chat endpoint (Async returns taskId, client uses SSE to stream) ---
198
+ app.post("/api/chat", (req, res) => {
199
+ try {
200
+ const { input, sessionId, model, priority, tenantId } = req.body;
201
+ if (!input) return res.status(400).json({ error: "input is required" });
202
+
203
+ const task = taskQueue.enqueue({
204
+ input,
205
+ channel: "http",
206
+ sessionId: sessionId || "local-user",
207
+ tenantId: tenantId || "http:local",
208
+ model,
209
+ priority: priority || 5,
210
+ type: "chat",
211
+ });
212
+
213
+ res.status(202).json({
214
+ taskId: task.id,
215
+ sessionId: task.sessionId,
216
+ status: "queued",
217
+ });
218
+ } catch (error) {
219
+ res.status(500).json({ error: error.message });
220
+ }
221
+ });
222
+
223
+ // --- Task submit endpoint (Async) ---
224
+ app.post("/api/tasks", (req, res) => {
225
+ try {
226
+ const { input, sessionId, model, priority } = req.body;
227
+ if (!input) return res.status(400).json({ error: "input is required" });
75
228
 
76
- // --- Task submit endpoint (DISABLED - same reason, unauthenticated) ---
77
- // app.post("/tasks", (req, res) => { ... });
229
+ const task = taskQueue.enqueue({
230
+ input,
231
+ channel: "http",
232
+ sessionId: sessionId || "local-user",
233
+ model,
234
+ priority: priority || 5,
235
+ });
78
236
 
79
- app.get("/tasks/:id", (req, res) => {
237
+ res.status(202).json({
238
+ message: "Task enqueued",
239
+ taskId: task.id,
240
+ status: task.status,
241
+ });
242
+ } catch (error) {
243
+ res.status(500).json({ error: error.message });
244
+ }
245
+ });
246
+
247
+ app.get("/api/tasks/:id", (req, res) => {
80
248
  const task = loadTask(req.params.id);
81
249
  if (!task) {
82
250
  return res.status(404).json({ error: "Task not found" });
@@ -84,19 +252,26 @@ app.get("/tasks/:id", (req, res) => {
84
252
  res.json(task);
85
253
  });
86
254
 
87
- app.get("/tasks", (req, res) => {
88
- const { limit, status } = req.query;
255
+ app.get("/api/tasks", (req, res) => {
256
+ const { limit, status, type } = req.query;
89
257
  const tasks = listTasks({
90
258
  limit: limit ? parseInt(limit, 10) : 20,
91
259
  status: status || null,
260
+ type: type || null,
92
261
  });
93
262
  res.json({
94
263
  tasks: tasks.map((t) => ({
95
264
  id: t.id,
96
265
  status: t.status,
266
+ type: t.type || "chat",
267
+ title: t.title || null,
97
268
  channel: t.channel,
98
- input: t.input.slice(0, 100),
269
+ input: t.input?.slice(0, 100) || "",
99
270
  cost: t.cost,
271
+ parentTaskId: t.parentTaskId || null,
272
+ agentId: t.agentId || null,
273
+ agentCreated: t.agentCreated || false,
274
+ subAgents: t.subAgents || null,
100
275
  createdAt: t.createdAt,
101
276
  completedAt: t.completedAt,
102
277
  })),
@@ -104,17 +279,79 @@ app.get("/tasks", (req, res) => {
104
279
  });
105
280
  });
106
281
 
107
- // --- Session endpoint ---
108
- app.get("/sessions/:id", (req, res) => {
282
+ // --- Child tasks endpoint ---
283
+ app.get("/api/tasks/:id/children", (req, res) => {
284
+ const children = listChildTasks(req.params.id);
285
+ res.json({
286
+ parentTaskId: req.params.id,
287
+ children: children.map((t) => ({
288
+ id: t.id,
289
+ status: t.status,
290
+ type: t.type || "chat",
291
+ title: t.title || null,
292
+ input: t.input?.slice(0, 100) || "",
293
+ agentId: t.agentId || null,
294
+ cost: t.cost,
295
+ createdAt: t.createdAt,
296
+ completedAt: t.completedAt,
297
+ })),
298
+ });
299
+ });
300
+
301
+ // --- Session endpoints ---
302
+ app.get("/api/sessions", (req, res) => {
303
+ const sessionIds = listSessions();
304
+ const sessionList = sessionIds.map(id => {
305
+ const s = getSession(id);
306
+ return {
307
+ sessionId: s.sessionId,
308
+ createdAt: s.createdAt,
309
+ lastMessage: s.messages.length > 0 ? s.messages[s.messages.length - 1].content.slice(0, 50) : "Empty chat",
310
+ messageCount: s.messages.length
311
+ };
312
+ }).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
313
+
314
+ res.json({ sessions: sessionList });
315
+ });
316
+
317
+ app.post("/api/sessions", (req, res) => {
318
+ const session = createSession();
319
+ res.status(201).json(session);
320
+ });
321
+
322
+ app.get("/api/sessions/:id", (req, res) => {
109
323
  const session = getSession(req.params.id);
110
324
  if (!session) {
111
325
  return res.status(404).json({ error: "Session not found" });
112
326
  }
113
- res.json(session);
327
+ // Filter out any leaked tool_call / tool_result messages
328
+ const cleanMessages = (session.messages || []).filter(msg => {
329
+ if (!msg.content || typeof msg.content !== "string") return false;
330
+ if (msg.role !== "user" && msg.role !== "assistant") return false;
331
+ const trimmed = msg.content.trimStart();
332
+ if (trimmed.startsWith("{")) {
333
+ try {
334
+ const parsed = JSON.parse(trimmed);
335
+ if (parsed.type === "tool_call" || parsed.tool_call) return false;
336
+ if (parsed.tool_name) return false;
337
+ if (parsed.type === "text" && parsed.finalResponse !== undefined) return false;
338
+ } catch { /* not JSON, keep it */ }
339
+ }
340
+ return true;
341
+ });
342
+ res.json({ ...session, messages: cleanMessages });
343
+ });
344
+
345
+ app.delete("/api/sessions/:id", (req, res) => {
346
+ const deleted = clearSession(req.params.id);
347
+ if (!deleted) {
348
+ return res.status(404).json({ error: "Session not found" });
349
+ }
350
+ res.json({ message: "Session deleted" });
114
351
  });
115
352
 
116
353
  // --- Config endpoint ---
117
- app.get("/config", (req, res) => {
354
+ app.get("/api/config", (req, res) => {
118
355
  res.json({
119
356
  defaultModel: config.defaultModel,
120
357
  permissionTier: config.permissionTier,
@@ -129,21 +366,115 @@ app.get("/config", (req, res) => {
129
366
  });
130
367
 
131
368
  // --- Models endpoint ---
132
- app.get("/models", (req, res) => {
369
+ app.get("/api/models", (req, res) => {
370
+ const available = listAvailableModels();
133
371
  res.json({
134
372
  default: config.defaultModel,
135
- available: listAvailableModels(),
373
+ available: available.map(m => ({
374
+ ...m,
375
+ pricingPerMTok: m.costPer1kInput > 0 ? {
376
+ input: `$${(m.costPer1kInput * 1000).toFixed(2)}`,
377
+ output: `$${(m.costPer1kOutput * 1000).toFixed(2)}`,
378
+ } : { input: "$0", output: "$0" },
379
+ })),
136
380
  });
137
381
  });
138
382
 
383
+ // --- Model switch endpoint ---
384
+ app.post("/api/model", (req, res) => {
385
+ const { model } = req.body;
386
+ if (!model) return res.status(400).json({ error: "model is required" });
387
+
388
+ const available = listAvailableModels();
389
+ const match = available.find(m => m.id === model);
390
+ if (!match) {
391
+ return res.status(400).json({
392
+ error: `Unknown or unavailable model: ${model}`,
393
+ available: available.map(m => m.id),
394
+ });
395
+ }
396
+
397
+ config.defaultModel = model;
398
+ res.json({ message: `Default model set to ${model}`, model });
399
+ });
400
+
139
401
  // --- Supervisor endpoint ---
140
- app.get("/supervisor", (req, res) => {
402
+ app.get("/api/supervisor", (req, res) => {
141
403
  res.json({
142
404
  warnings: supervisor.getWarnings(),
143
405
  activeSubAgents: getActiveSubAgentCount(),
144
406
  });
145
407
  });
146
408
 
409
+ // --- Sub-agents endpoint ---
410
+ app.get("/api/subagents", (req, res) => {
411
+ res.json({ agents: listActiveAgents() });
412
+ });
413
+
414
+ // --- SSE streaming endpoint for task events ---
415
+ app.get("/api/tasks/:id/stream", (req, res) => {
416
+ const taskId = req.params.id;
417
+ res.writeHead(200, {
418
+ "Content-Type": "text/event-stream",
419
+ "Cache-Control": "no-cache",
420
+ Connection: "keep-alive",
421
+ });
422
+
423
+ const send = (event, data) => {
424
+ res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
425
+ };
426
+
427
+ // Send current state immediately
428
+ const task = loadTask(taskId);
429
+ if (task) send("task:state", task);
430
+
431
+ const onTool = (evt) => {
432
+ if (evt.taskId === taskId) send("tool:after", evt);
433
+ };
434
+ const onModel = (evt) => {
435
+ if (evt.taskId === taskId || evt.taskId?.startsWith("subagent-")) send("model:called", evt);
436
+ };
437
+ const onAgentSpawn = (evt) => {
438
+ if (evt.parentTaskId === taskId) send("agent:spawned", evt);
439
+ };
440
+ const onAgentDone = (evt) => {
441
+ if (evt.parentTaskId === taskId) send("agent:finished", evt);
442
+ };
443
+ const onComplete = (evt) => {
444
+ if (evt.taskId === taskId) {
445
+ const finalTask = loadTask(taskId);
446
+ send("task:completed", finalTask || evt);
447
+ cleanup();
448
+ res.end();
449
+ }
450
+ };
451
+ const onFail = (evt) => {
452
+ if (evt.taskId === taskId) {
453
+ send("task:failed", evt);
454
+ cleanup();
455
+ res.end();
456
+ }
457
+ };
458
+
459
+ eventBus.on("tool:after", onTool);
460
+ eventBus.on("model:called", onModel);
461
+ eventBus.on("agent:spawned", onAgentSpawn);
462
+ eventBus.on("agent:finished", onAgentDone);
463
+ eventBus.on("task:completed", onComplete);
464
+ eventBus.on("task:failed", onFail);
465
+
466
+ const cleanup = () => {
467
+ eventBus.removeListener("tool:after", onTool);
468
+ eventBus.removeListener("model:called", onModel);
469
+ eventBus.removeListener("agent:spawned", onAgentSpawn);
470
+ eventBus.removeListener("agent:finished", onAgentDone);
471
+ eventBus.removeListener("task:completed", onComplete);
472
+ eventBus.removeListener("task:failed", onFail);
473
+ };
474
+
475
+ req.on("close", cleanup);
476
+ });
477
+
147
478
  // --- WhatsApp webhook ---
148
479
  app.post("/webhooks/whatsapp", async (req, res) => {
149
480
  const whatsapp = channelRegistry.get("whatsapp");
@@ -209,22 +540,22 @@ app.post("/webhooks/line", express.raw({ type: "application/json" }), async (req
209
540
  });
210
541
 
211
542
  // --- Channels endpoint ---
212
- app.get("/channels", (req, res) => {
543
+ app.get("/api/channels", (req, res) => {
213
544
  res.json({ channels: channelRegistry.list() });
214
545
  });
215
546
 
216
547
  // --- Skills endpoint ---
217
- app.get("/skills", (req, res) => {
548
+ app.get("/api/skills", (req, res) => {
218
549
  res.json({ skills: skillLoader.list() });
219
550
  });
220
551
 
221
- app.post("/skills/reload", (req, res) => {
552
+ app.post("/api/skills/reload", (req, res) => {
222
553
  skillLoader.reload();
223
554
  res.json({ message: "Skills reloaded", skills: skillLoader.list() });
224
555
  });
225
556
 
226
557
  // --- Schedule endpoints ---
227
- app.post("/schedules", (req, res) => {
558
+ app.post("/api/schedules", (req, res) => {
228
559
  try {
229
560
  const { cronExpression, taskInput, channel, model, name } = req.body;
230
561
  if (!cronExpression || !taskInput) {
@@ -237,28 +568,47 @@ app.post("/schedules", (req, res) => {
237
568
  }
238
569
  });
239
570
 
240
- app.get("/schedules", (req, res) => {
571
+ app.get("/api/schedules", (req, res) => {
241
572
  res.json({ schedules: scheduler.list() });
242
573
  });
243
574
 
244
- app.delete("/schedules/:id", (req, res) => {
575
+ app.delete("/api/schedules/:id", (req, res) => {
245
576
  scheduler.delete(req.params.id);
246
577
  res.json({ message: "Schedule deleted" });
247
578
  });
248
579
 
249
580
  // --- Audit endpoint ---
250
- app.get("/audit", (req, res) => {
581
+ app.get("/api/audit", (req, res) => {
251
582
  res.json(auditLog.stats());
252
583
  });
253
584
 
254
585
  // --- MCP endpoints ---
255
- app.get("/mcp", (req, res) => {
586
+ app.get("/api/mcp", (req, res) => {
256
587
  const cfg = mcpManager.readConfig().mcpServers || {};
257
588
  const live = mcpManager.list();
589
+ const isPlaceholder = (v) => !v || v.startsWith("YOUR_") || v === "" || v.startsWith("${");
590
+ // Detect placeholder patterns in command args (e.g. connection strings, paths with dummy values)
591
+ const isArgPlaceholder = (v) => {
592
+ if (typeof v !== "string") return false;
593
+ return /user:pass@/i.test(v) || /\/Users\/you\//i.test(v) || /YOUR_/i.test(v)
594
+ || /your-.*-here/i.test(v) || /example\.com/i.test(v) || /changeme/i.test(v)
595
+ || /placeholder/i.test(v) || /xxx/i.test(v);
596
+ };
258
597
  const servers = Object.entries(cfg)
259
598
  .filter(([k]) => !k.startsWith("_comment"))
260
599
  .map(([name, serverCfg]) => {
261
600
  const liveEntry = live.find(s => s.name === name);
601
+ // Check if any env/header values are unconfigured placeholders
602
+ const envEntries = serverCfg.env ? Object.entries(serverCfg.env) : [];
603
+ const headerEntries = serverCfg.headers ? Object.entries(serverCfg.headers) : [];
604
+ // Also check args for placeholder patterns
605
+ const args = serverCfg.args || [];
606
+ const placeholderArgs = args
607
+ .map((v, i) => isArgPlaceholder(v) ? { index: i, value: v } : null)
608
+ .filter(Boolean);
609
+ const needsConfig = envEntries.some(([, v]) => isPlaceholder(v))
610
+ || headerEntries.some(([, v]) => isPlaceholder(v))
611
+ || placeholderArgs.length > 0;
262
612
  return {
263
613
  name,
264
614
  enabled: serverCfg.enabled !== false,
@@ -267,20 +617,26 @@ app.get("/mcp", (req, res) => {
267
617
  type: serverCfg.command ? "stdio" : (serverCfg.transport || "http"),
268
618
  command: serverCfg.command || null,
269
619
  url: serverCfg.url || null,
620
+ description: serverCfg.description || null,
621
+ envKeys: serverCfg.env ? Object.keys(serverCfg.env) : [],
622
+ headerKeys: serverCfg.headers ? Object.keys(serverCfg.headers) : [],
623
+ placeholderArgs,
624
+ needsConfig,
270
625
  };
271
626
  });
272
627
  res.json({ servers });
273
628
  });
274
629
 
275
630
  // Add a new MCP server
276
- app.post("/mcp", async (req, res) => {
277
- const { name, command, args, url, transport, env } = req.body;
631
+ app.post("/api/mcp", async (req, res) => {
632
+ const { name, command, args, url, transport, env, headers, description } = req.body;
278
633
  if (!name) return res.status(400).json({ error: "name is required" });
279
634
  if (!command && !url) return res.status(400).json({ error: "command (stdio) or url (http/sse) required" });
280
635
 
281
636
  const serverConfig = command
282
- ? { command, args: args || [], env: env || {} }
283
- : { url, transport: transport || undefined, env: env || {} };
637
+ ? { command, args: args || [], ...(env && Object.keys(env).length > 0 ? { env } : {}) }
638
+ : { url, ...(transport ? { transport } : {}), ...(headers && Object.keys(headers).length > 0 ? { headers } : {}) };
639
+ if (description) serverConfig.description = description;
284
640
 
285
641
  try {
286
642
  const result = await mcpManager.addServer(name, serverConfig);
@@ -291,7 +647,7 @@ app.post("/mcp", async (req, res) => {
291
647
  });
292
648
 
293
649
  // Remove an MCP server
294
- app.delete("/mcp/:name", async (req, res) => {
650
+ app.delete("/api/mcp/:name", async (req, res) => {
295
651
  try {
296
652
  const result = await mcpManager.removeServer(req.params.name);
297
653
  res.json({ message: result });
@@ -300,8 +656,41 @@ app.delete("/mcp/:name", async (req, res) => {
300
656
  }
301
657
  });
302
658
 
659
+ // Update MCP server credentials (env vars or headers)
660
+ app.patch("/api/mcp/:name", async (req, res) => {
661
+ const { name } = req.params;
662
+ const { env, headers: hdrs, args: argUpdates } = req.body;
663
+ try {
664
+ const mcpConfig = mcpManager.readConfig();
665
+ const serverCfg = mcpConfig.mcpServers?.[name];
666
+ if (!serverCfg) return res.status(404).json({ error: `Server "${name}" not found` });
667
+
668
+ if (env && typeof env === "object") {
669
+ serverCfg.env = { ...(serverCfg.env || {}), ...env };
670
+ }
671
+ if (hdrs && typeof hdrs === "object") {
672
+ serverCfg.headers = { ...(serverCfg.headers || {}), ...hdrs };
673
+ }
674
+ // Support updating specific args by index (e.g. connection strings)
675
+ if (argUpdates && typeof argUpdates === "object") {
676
+ if (!serverCfg.args) serverCfg.args = [];
677
+ for (const [indexStr, value] of Object.entries(argUpdates)) {
678
+ const idx = parseInt(indexStr, 10);
679
+ if (!isNaN(idx) && idx >= 0 && idx < serverCfg.args.length) {
680
+ serverCfg.args[idx] = value;
681
+ }
682
+ }
683
+ }
684
+ mcpConfig.mcpServers[name] = serverCfg;
685
+ mcpManager.writeConfig(mcpConfig);
686
+ res.json({ message: `Credentials updated for "${name}"` });
687
+ } catch (err) {
688
+ res.status(400).json({ error: err.message });
689
+ }
690
+ });
691
+
303
692
  // Enable / disable / reload an MCP server
304
- app.post("/mcp/:name/:action", async (req, res) => {
693
+ app.post("/api/mcp/:name/:action", async (req, res) => {
305
694
  const { name, action } = req.params;
306
695
  try {
307
696
  let result;
@@ -316,11 +705,11 @@ app.post("/mcp/:name/:action", async (req, res) => {
316
705
  });
317
706
 
318
707
  // --- Daemon endpoints ---
319
- app.get("/daemon/status", (req, res) => {
708
+ app.get("/api/daemon/status", (req, res) => {
320
709
  res.json(daemonManager.status());
321
710
  });
322
711
 
323
- app.post("/daemon/:action", (req, res) => {
712
+ app.post("/api/daemon/:action", (req, res) => {
324
713
  const { action } = req.params;
325
714
  try {
326
715
  switch (action) {
@@ -353,14 +742,14 @@ app.post("/daemon/:action", (req, res) => {
353
742
  });
354
743
 
355
744
  // --- Vault endpoints ---
356
- app.get("/vault/status", (req, res) => {
745
+ app.get("/api/vault/status", (req, res) => {
357
746
  res.json({
358
747
  exists: secretVault.exists(),
359
748
  unlocked: secretVault.isUnlocked(),
360
749
  });
361
750
  });
362
751
 
363
- app.post("/vault/unlock", (req, res) => {
752
+ app.post("/api/vault/unlock", (req, res) => {
364
753
  try {
365
754
  const { passphrase } = req.body;
366
755
  if (!passphrase) return res.status(400).json({ error: "passphrase is required" });
@@ -376,30 +765,30 @@ app.post("/vault/unlock", (req, res) => {
376
765
  }
377
766
  });
378
767
 
379
- app.post("/vault/lock", (req, res) => {
768
+ app.post("/api/vault/lock", (req, res) => {
380
769
  secretVault.lock();
381
770
  res.json({ message: "Vault locked" });
382
771
  });
383
772
 
384
773
  // --- Tenant endpoints ---
385
- app.get("/tenants", (req, res) => {
774
+ app.get("/api/tenants", (req, res) => {
386
775
  const tenants = tenantManager.list();
387
776
  res.json({ tenants, stats: tenantManager.stats() });
388
777
  });
389
778
 
390
- app.get("/tenants/:id", (req, res) => {
779
+ app.get("/api/tenants/:id", (req, res) => {
391
780
  const tenant = tenantManager.get(decodeURIComponent(req.params.id));
392
781
  if (!tenant) return res.status(404).json({ error: "Tenant not found" });
393
782
  res.json(tenant);
394
783
  });
395
784
 
396
- app.patch("/tenants/:id", (req, res) => {
785
+ app.patch("/api/tenants/:id", (req, res) => {
397
786
  const id = decodeURIComponent(req.params.id);
398
787
  const updated = tenantManager.set(id, req.body);
399
788
  res.json(updated);
400
789
  });
401
790
 
402
- app.post("/tenants/:id/suspend", (req, res) => {
791
+ app.post("/api/tenants/:id/suspend", (req, res) => {
403
792
  const id = decodeURIComponent(req.params.id);
404
793
  const { reason } = req.body;
405
794
  const updated = tenantManager.suspend(id, reason || "");
@@ -407,29 +796,227 @@ app.post("/tenants/:id/suspend", (req, res) => {
407
796
  res.json(updated);
408
797
  });
409
798
 
410
- app.post("/tenants/:id/unsuspend", (req, res) => {
799
+ app.post("/api/tenants/:id/unsuspend", (req, res) => {
411
800
  const id = decodeURIComponent(req.params.id);
412
801
  const updated = tenantManager.unsuspend(id);
413
802
  if (!updated) return res.status(404).json({ error: "Tenant not found" });
414
803
  res.json(updated);
415
804
  });
416
805
 
417
- app.post("/tenants/:id/reset", (req, res) => {
806
+ app.post("/api/tenants/:id/reset", (req, res) => {
418
807
  const id = decodeURIComponent(req.params.id);
419
808
  const updated = tenantManager.reset(id);
420
809
  if (!updated) return res.status(404).json({ error: "Tenant not found" });
421
810
  res.json(updated);
422
811
  });
423
812
 
424
- app.delete("/tenants/:id", (req, res) => {
813
+ app.delete("/api/tenants/:id", (req, res) => {
425
814
  const id = decodeURIComponent(req.params.id);
426
815
  const deleted = tenantManager.delete(id);
427
816
  if (!deleted) return res.status(404).json({ error: "Tenant not found" });
428
817
  res.json({ message: "Tenant deleted" });
429
818
  });
430
819
 
820
+ // --- Exec approvals ---
821
+ app.get("/api/approvals", (req, res) => {
822
+ res.json({ approvals: execApproval.listPending(), mode: execApproval.mode });
823
+ });
824
+
825
+ app.post("/api/approvals/:id", (req, res) => {
826
+ const { decision } = req.body;
827
+ if (!["allow", "allow-once", "deny"].includes(decision)) {
828
+ return res.status(400).json({ error: 'decision must be "allow", "allow-once", or "deny"' });
829
+ }
830
+ const resolved = execApproval.resolveApproval(req.params.id, decision);
831
+ if (!resolved) return res.status(404).json({ error: "Approval not found or expired" });
832
+ res.json({ message: `Approval ${req.params.id} → ${decision}` });
833
+ });
834
+
835
+ // --- Settings endpoint (read/write .env vars) ---
836
+ app.get("/api/settings", (req, res) => {
837
+ const envPath = join(__dirname, "..", ".env");
838
+ const examplePath = join(__dirname, "..", ".env.example");
839
+
840
+ // Parse current .env
841
+ const envVars = {};
842
+ if (existsSync(envPath)) {
843
+ const lines = readFileSync(envPath, "utf-8").split("\n");
844
+ for (const line of lines) {
845
+ const trimmed = line.trim();
846
+ if (!trimmed || trimmed.startsWith("#")) continue;
847
+ const eqIdx = trimmed.indexOf("=");
848
+ if (eqIdx === -1) continue;
849
+ const key = trimmed.slice(0, eqIdx).trim();
850
+ const val = trimmed.slice(eqIdx + 1).trim();
851
+ envVars[key] = val;
852
+ }
853
+ }
854
+
855
+ // Parse .env.example for available vars with sections
856
+ const available = [];
857
+ if (existsSync(examplePath)) {
858
+ const lines = readFileSync(examplePath, "utf-8").split("\n");
859
+ let section = "General";
860
+ for (const line of lines) {
861
+ const trimmed = line.trim();
862
+ if (trimmed.startsWith("# ===")) {
863
+ section = trimmed.replace(/^# =+\s*/, "").replace(/\s*=+$/, "");
864
+ continue;
865
+ }
866
+ if (!trimmed || trimmed.startsWith("#")) continue;
867
+ const eqIdx = trimmed.indexOf("=");
868
+ if (eqIdx === -1) continue;
869
+ const key = trimmed.slice(0, eqIdx).trim();
870
+ available.push({ key, section });
871
+ }
872
+ }
873
+
874
+ // Mask values for security
875
+ const masked = {};
876
+ for (const [key, val] of Object.entries(envVars)) {
877
+ if (!val) { masked[key] = ""; continue; }
878
+ masked[key] = val.length <= 4 ? "****" : val.slice(0, 4) + "*".repeat(Math.min(val.length - 4, 20));
879
+ }
880
+
881
+ res.json({ vars: masked, available });
882
+ });
883
+
884
+ app.put("/api/settings", (req, res) => {
885
+ const { updates } = req.body; // { KEY: "value", KEY2: "value2" }
886
+ if (!updates || typeof updates !== "object") {
887
+ return res.status(400).json({ error: "updates object is required" });
888
+ }
889
+
890
+ const envPath = join(__dirname, "..", ".env");
891
+ let content = existsSync(envPath) ? readFileSync(envPath, "utf-8") : "";
892
+
893
+ for (const [key, value] of Object.entries(updates)) {
894
+ // Validate key format (alphanumeric + underscore only)
895
+ if (!/^[A-Z][A-Z0-9_]*$/.test(key)) continue;
896
+ const regex = new RegExp(`^${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}=.*$`, "m");
897
+ if (regex.test(content)) {
898
+ content = content.replace(regex, `${key}=${value}`);
899
+ } else {
900
+ content = content.trimEnd() + `\n${key}=${value}\n`;
901
+ }
902
+ // Also update process.env so changes take effect without restart
903
+ process.env[key] = value;
904
+ }
905
+
906
+ writeFileSync(envPath, content, "utf-8");
907
+
908
+ res.json({ message: `Updated ${Object.keys(updates).length} variable(s)`, updated: Object.keys(updates) });
909
+ });
910
+
911
+ // --- User Profile endpoints ---
912
+ app.get("/api/profile", (req, res) => {
913
+ const profilePath = join(config.dataDir, "user-profile.json");
914
+ if (!existsSync(profilePath)) return res.json({});
915
+ try {
916
+ const profile = JSON.parse(readFileSync(profilePath, "utf-8"));
917
+ res.json(profile);
918
+ } catch {
919
+ res.json({});
920
+ }
921
+ });
922
+
923
+ app.put("/api/profile", (req, res) => {
924
+ const { name, personality, tone, instructions, subAgentModel } = req.body;
925
+ const profilePath = join(config.dataDir, "user-profile.json");
926
+ const profile = { name: name || "", personality: personality || "", tone: tone || "", instructions: instructions || "", subAgentModel: subAgentModel || "" };
927
+ mkdirSync(dirname(profilePath), { recursive: true });
928
+ writeFileSync(profilePath, JSON.stringify(profile, null, 2), "utf-8");
929
+ // Apply sub-agent model to runtime so it takes effect immediately
930
+ if (subAgentModel) {
931
+ process.env.SUB_AGENT_MODEL = subAgentModel;
932
+ } else {
933
+ delete process.env.SUB_AGENT_MODEL;
934
+ }
935
+ res.json({ message: "Profile saved", profile });
936
+ });
937
+
938
+ // --- Custom Skills endpoints ---
939
+ app.get("/api/skills/custom", (req, res) => {
940
+ const customDir = join(config.skillsDir, "custom");
941
+ if (!existsSync(customDir)) return res.json({ skills: [] });
942
+ const files = [];
943
+ try {
944
+ const entries = readdirSync(customDir);
945
+ for (const f of entries) {
946
+ if (!f.endsWith(".md")) continue;
947
+ const content = readFileSync(join(customDir, f), "utf-8");
948
+ // Parse frontmatter
949
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
950
+ const meta = {};
951
+ if (fmMatch) {
952
+ for (const line of fmMatch[1].split("\n")) {
953
+ const idx = line.indexOf(":");
954
+ if (idx > 0) meta[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
955
+ }
956
+ }
957
+ files.push({
958
+ name: meta.name || f.replace(".md", ""),
959
+ description: meta.description || "",
960
+ triggers: meta.triggers || "",
961
+ filename: f,
962
+ content: fmMatch ? fmMatch[2].trim() : content,
963
+ });
964
+ }
965
+ } catch { /* ignore */ }
966
+ res.json({ skills: files });
967
+ });
968
+
969
+ app.post("/api/skills/custom", (req, res) => {
970
+ const { name, description, triggers, content } = req.body;
971
+ if (!name) return res.status(400).json({ error: "name is required" });
972
+ if (!content) return res.status(400).json({ error: "content is required" });
973
+
974
+ // Sanitize filename
975
+ const safeName = name.replace(/[^a-zA-Z0-9_-]/g, "-").toLowerCase();
976
+ const customDir = join(config.skillsDir, "custom");
977
+ mkdirSync(customDir, { recursive: true });
978
+
979
+ const filePath = join(customDir, `${safeName}.md`);
980
+ const frontmatter = `---\nname: ${safeName}\ndescription: ${description || ""}\n${triggers ? `triggers: ${triggers}\n` : ""}---\n\n`;
981
+ writeFileSync(filePath, frontmatter + content, "utf-8");
982
+
983
+ // Reload skills so new skill is discoverable
984
+ skillLoader.reload();
985
+
986
+ res.status(201).json({ message: "Custom skill created", name: safeName });
987
+ });
988
+
989
+ app.delete("/api/skills/custom/:name", (req, res) => {
990
+ const safeName = req.params.name.replace(/[^a-zA-Z0-9_-]/g, "-").toLowerCase();
991
+ const filePath = join(config.skillsDir, "custom", `${safeName}.md`);
992
+ if (!existsSync(filePath)) return res.status(404).json({ error: "Skill not found" });
993
+
994
+ unlinkSync(filePath);
995
+ skillLoader.reload();
996
+ res.json({ message: "Custom skill deleted" });
997
+ });
998
+
999
+ // --- Memory endpoints ---
1000
+ app.get("/api/memory", (req, res) => {
1001
+ const memoryPath = config.memoryPath;
1002
+ if (!existsSync(memoryPath)) return res.json({ content: "" });
1003
+ try {
1004
+ const content = readFileSync(memoryPath, "utf-8");
1005
+ res.json({ content });
1006
+ } catch {
1007
+ res.json({ content: "" });
1008
+ }
1009
+ });
1010
+
1011
+ app.put("/api/memory", (req, res) => {
1012
+ const { content } = req.body;
1013
+ if (content === undefined) return res.status(400).json({ error: "content is required" });
1014
+ writeFileSync(config.memoryPath, content, "utf-8");
1015
+ res.json({ message: "Memory saved" });
1016
+ });
1017
+
431
1018
  // --- Costs endpoint ---
432
- app.get("/costs/today", (req, res) => {
1019
+ app.get("/api/costs/today", (req, res) => {
433
1020
  res.json({
434
1021
  date: new Date().toISOString().split("T")[0],
435
1022
  totalCost: getTodayCost(),
@@ -438,28 +1025,107 @@ app.get("/costs/today", (req, res) => {
438
1025
  });
439
1026
  });
440
1027
 
1028
+ // --- Static UI (with auth token injection) ---
1029
+ const uiPath = join(__dirname, "..", "daemora-ui", "dist");
1030
+ if (existsSync(uiPath)) {
1031
+ const indexHtmlPath = join(uiPath, "index.html");
1032
+ let indexHtml = existsSync(indexHtmlPath) ? readFileSync(indexHtmlPath, "utf-8") : "";
1033
+
1034
+ // Inject auth token as a <meta> tag so the UI can read it without a login flow.
1035
+ // Safe because the HTML is only served to localhost (localOnly middleware).
1036
+ const tokenMeta = `<meta name="api-token" content="${API_TOKEN}" />`;
1037
+ if (indexHtml && !indexHtml.includes('name="api-token"')) {
1038
+ indexHtml = indexHtml.replace("</head>", ` ${tokenMeta}\n </head>`);
1039
+ }
1040
+
1041
+ // Serve static assets normally
1042
+ app.use(express.static(uiPath, { index: false })); // index:false so we handle index.html ourselves
1043
+
1044
+ // Serve token-injected index.html for all UI routes
1045
+ app.get(/.*/, (req, res, next) => {
1046
+ 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/")) {
1047
+ return next();
1048
+ }
1049
+ res.setHeader("Content-Type", "text/html");
1050
+ res.send(indexHtml);
1051
+ });
1052
+ console.log(`[Server] Serving UI from ${uiPath}`);
1053
+ }
1054
+
1055
+ // --- Load user profile settings on startup ---
1056
+ try {
1057
+ const profilePath = join(config.dataDir, "user-profile.json");
1058
+ if (existsSync(profilePath)) {
1059
+ const p = JSON.parse(readFileSync(profilePath, "utf-8"));
1060
+ if (p.subAgentModel && !process.env.SUB_AGENT_MODEL) {
1061
+ process.env.SUB_AGENT_MODEL = p.subAgentModel;
1062
+ }
1063
+ }
1064
+ } catch { /* ignore */ }
1065
+
1066
+ // --- Server readiness gate ---
1067
+ // The server must fully initialize before processing user messages.
1068
+ // Skills, MCP, embeddings, and channels all need to load first.
1069
+ // Requests that arrive before ready get a 503 with a clear message.
1070
+ let _serverReady = false;
1071
+
1072
+ // Gate message-processing endpoints until startup completes
1073
+ const readinessGate = (req, res, next) => {
1074
+ if (_serverReady) return next();
1075
+ res.status(503).json({ error: "Server is starting up — loading skills, MCP, and channels. Please wait a moment and retry." });
1076
+ };
1077
+ app.use("/api/chat", readinessGate);
1078
+ app.post("/api/tasks", readinessGate);
1079
+
441
1080
  // --- Start server ---
442
1081
  app.listen(config.port, async () => {
443
1082
  console.log("\n--- Daemora Server ---");
444
1083
  console.log(`Running on http://localhost:${config.port}`);
445
1084
  console.log(`Model: ${config.defaultModel}`);
1085
+ if (process.env.SUB_AGENT_MODEL) console.log(`Sub-agent model: ${process.env.SUB_AGENT_MODEL}`);
446
1086
  console.log(`Permission tier: ${config.permissionTier}`);
447
- console.log(`Tools loaded: ${Object.keys(toolFunctions).join(", ")}`);
448
- console.log(`Total tools: ${Object.keys(toolFunctions).length}`);
449
1087
  console.log(`Data dir: ${config.dataDir}`);
450
1088
  console.log(`Daemon mode: ${config.daemonMode}`);
451
- console.log(`Task runner: active (concurrency: 2)`);
452
1089
 
453
- // Initialize MCP in background
454
- mcpManager.init().catch((e) => console.log(`[MCPManager] Init error: ${e.message}`));
1090
+ // ── Phase 1: Load skills + embeddings (must complete before processing messages) ──
1091
+ console.log("[Startup] Loading skills...");
1092
+ skillLoader.load();
1093
+ console.log(`[Startup] Skills loaded: ${skillLoader.list().length}`);
1094
+
1095
+ console.log("[Startup] Initializing embeddings...");
1096
+ try {
1097
+ const { ensureOllamaEmbedModel } = await import("./utils/Embeddings.js");
1098
+ await ensureOllamaEmbedModel();
1099
+ } catch { /* non-fatal */ }
1100
+
1101
+ // Embed skills (uses whatever embedding provider is available)
1102
+ try {
1103
+ await skillLoader.embedSkills();
1104
+ console.log("[Startup] Skill embeddings ready");
1105
+ } catch { /* non-fatal — TF-IDF fallback always works */ }
1106
+
1107
+ // ── Phase 2: Connect MCP servers ──
1108
+ console.log("[Startup] Connecting MCP servers...");
1109
+ try {
1110
+ await mcpManager.init();
1111
+ } catch (e) {
1112
+ console.log(`[Startup] MCP init error (non-fatal): ${e.message}`);
1113
+ }
455
1114
 
456
- // Start channels (await so we see results before the blank line)
1115
+ // ── Phase 3: Start channels ──
1116
+ console.log("[Startup] Starting channels...");
457
1117
  try {
458
1118
  await channelRegistry.startAll();
459
1119
  } catch (e) {
460
- console.log(`[ChannelRegistry] Start error: ${e.message}`);
1120
+ console.log(`[Startup] Channel start error: ${e.message}`);
461
1121
  }
462
- console.log("");
1122
+
1123
+ // ── Ready — start processing messages ──
1124
+ taskRunner.start();
1125
+ console.log(`[Startup] Tools: ${Object.keys(toolFunctions).length}`);
1126
+ console.log(`[Startup] Task runner: active (concurrency: 2)`);
1127
+ _serverReady = true;
1128
+ console.log("[Startup] Server ready ✓\n");
463
1129
  });
464
1130
 
465
1131
  // Graceful shutdown