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.
- package/LICENSE +663 -0
- package/README.md +69 -19
- package/SOUL.md +25 -24
- package/daemora-ui/README.md +11 -0
- package/package.json +12 -2
- package/skills/api-development.md +35 -0
- package/skills/artifacts-builder/SKILL.md +74 -0
- package/skills/artifacts-builder/scripts/bundle-artifact.sh +54 -0
- package/skills/artifacts-builder/scripts/init-artifact.sh +322 -0
- package/skills/artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
- package/skills/brand-guidelines.md +73 -0
- package/skills/browser.md +77 -0
- package/skills/changelog-generator.md +104 -0
- package/skills/coding.md +26 -10
- package/skills/content-research-writer.md +538 -0
- package/skills/data-analysis.md +27 -0
- package/skills/debugging.md +33 -0
- package/skills/devops.md +37 -0
- package/skills/document-docx.md +197 -0
- package/skills/document-pdf.md +294 -0
- package/skills/document-pptx.md +484 -0
- package/skills/document-xlsx.md +289 -0
- package/skills/domain-name-brainstormer.md +212 -0
- package/skills/file-organizer.md +433 -0
- package/skills/frontend-design.md +42 -0
- package/skills/image-enhancer.md +99 -0
- package/skills/invoice-organizer.md +446 -0
- package/skills/lead-research-assistant.md +199 -0
- package/skills/mcp-builder/SKILL.md +328 -0
- package/skills/mcp-builder/reference/evaluation.md +602 -0
- package/skills/mcp-builder/reference/mcp_best_practices.md +915 -0
- package/skills/mcp-builder/reference/node_mcp_server.md +916 -0
- package/skills/mcp-builder/reference/python_mcp_server.md +752 -0
- package/skills/mcp-builder/scripts/connections.py +151 -0
- package/skills/mcp-builder/scripts/evaluation.py +373 -0
- package/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
- package/skills/mcp-builder/scripts/requirements.txt +2 -0
- package/skills/meeting-insights-analyzer.md +327 -0
- package/skills/orchestration.md +93 -0
- package/skills/raffle-winner-picker.md +159 -0
- package/skills/slack-gif-creator/SKILL.md +646 -0
- package/skills/slack-gif-creator/core/color_palettes.py +302 -0
- package/skills/slack-gif-creator/core/easing.py +230 -0
- package/skills/slack-gif-creator/core/frame_composer.py +469 -0
- package/skills/slack-gif-creator/core/gif_builder.py +246 -0
- package/skills/slack-gif-creator/core/typography.py +357 -0
- package/skills/slack-gif-creator/core/validators.py +264 -0
- package/skills/slack-gif-creator/core/visual_effects.py +494 -0
- package/skills/slack-gif-creator/requirements.txt +4 -0
- package/skills/slack-gif-creator/templates/bounce.py +106 -0
- package/skills/slack-gif-creator/templates/explode.py +331 -0
- package/skills/slack-gif-creator/templates/fade.py +329 -0
- package/skills/slack-gif-creator/templates/flip.py +291 -0
- package/skills/slack-gif-creator/templates/kaleidoscope.py +211 -0
- package/skills/slack-gif-creator/templates/morph.py +329 -0
- package/skills/slack-gif-creator/templates/move.py +293 -0
- package/skills/slack-gif-creator/templates/pulse.py +268 -0
- package/skills/slack-gif-creator/templates/shake.py +127 -0
- package/skills/slack-gif-creator/templates/slide.py +291 -0
- package/skills/slack-gif-creator/templates/spin.py +269 -0
- package/skills/slack-gif-creator/templates/wiggle.py +300 -0
- package/skills/slack-gif-creator/templates/zoom.py +312 -0
- package/skills/system-admin.md +44 -0
- package/skills/tailored-resume-generator.md +345 -0
- package/skills/theme-factory/SKILL.md +59 -0
- package/skills/theme-factory/theme-showcase.pdf +0 -0
- package/skills/theme-factory/themes/arctic-frost.md +19 -0
- package/skills/theme-factory/themes/botanical-garden.md +19 -0
- package/skills/theme-factory/themes/desert-rose.md +19 -0
- package/skills/theme-factory/themes/forest-canopy.md +19 -0
- package/skills/theme-factory/themes/golden-hour.md +19 -0
- package/skills/theme-factory/themes/midnight-galaxy.md +19 -0
- package/skills/theme-factory/themes/modern-minimalist.md +19 -0
- package/skills/theme-factory/themes/ocean-depths.md +19 -0
- package/skills/theme-factory/themes/sunset-boulevard.md +19 -0
- package/skills/theme-factory/themes/tech-innovation.md +19 -0
- package/skills/video-downloader.md +99 -0
- package/skills/web-development.md +32 -0
- package/skills/webapp-testing/SKILL.md +96 -0
- package/skills/webapp-testing/examples/console_logging.py +35 -0
- package/skills/webapp-testing/examples/element_discovery.py +40 -0
- package/skills/webapp-testing/examples/static_html_automation.py +33 -0
- package/skills/webapp-testing/scripts/with_server.py +106 -0
- package/src/agents/SubAgentManager.js +57 -12
- package/src/api/openai-compat.js +212 -0
- package/src/channels/TelegramChannel.js +5 -2
- package/src/channels/index.js +7 -10
- package/src/cli.js +129 -50
- package/src/config/agentProfiles.js +1 -0
- package/src/config/default.js +10 -0
- package/src/config/models.js +317 -71
- package/src/config/permissions.js +12 -0
- package/src/core/AgentLoop.js +70 -50
- package/src/core/Compaction.js +84 -2
- package/src/core/MessageQueue.js +90 -0
- package/src/core/Task.js +13 -0
- package/src/core/TaskQueue.js +1 -1
- package/src/core/TaskRunner.js +80 -5
- package/src/index.js +328 -48
- package/src/mcp/MCPAgentRunner.js +48 -11
- package/src/mcp/MCPManager.js +40 -2
- package/src/models/ModelRouter.js +67 -1
- package/src/safety/DockerSandbox.js +212 -0
- package/src/safety/ExecApproval.js +118 -0
- package/src/scheduler/Heartbeat.js +56 -21
- package/src/services/cleanup.js +106 -0
- package/src/services/sessions.js +39 -1
- package/src/setup/wizard.js +75 -4
- package/src/skills/SkillLoader.js +104 -17
- package/src/storage/TaskStore.js +19 -1
- package/src/systemPrompt.js +171 -328
- package/src/tools/browserAutomation.js +615 -104
- package/src/tools/executeCommand.js +19 -1
- package/src/tools/index.js +6 -0
- package/src/tools/manageAgents.js +55 -4
- package/src/tools/replyWithFile.js +62 -0
- package/src/tools/screenCapture.js +12 -1
- package/src/tools/taskManager.js +164 -0
- package/src/tools/useMCP.js +3 -1
- package/src/utils/Embeddings.js +157 -10
- 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 (
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
// ---
|
|
108
|
-
app.get("/
|
|
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
|
-
|
|
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:
|
|
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
|
|
283
|
-
: { url, transport
|
|
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 ---");
|