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.
- package/LICENSE +663 -0
- package/README.md +69 -19
- package/SOUL.md +29 -26
- package/config/mcp.json +126 -66
- 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 +134 -16
- package/src/agents/systemPrompt.js +427 -0
- 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 +281 -55
- package/src/config/agentProfiles.js +1 -0
- package/src/config/default.js +15 -1
- package/src/config/models.js +314 -78
- package/src/config/permissions.js +12 -0
- package/src/core/AgentLoop.js +70 -50
- package/src/core/Compaction.js +111 -11
- 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 +81 -6
- package/src/index.js +725 -59
- package/src/mcp/MCPAgentRunner.js +48 -11
- package/src/mcp/MCPManager.js +40 -2
- package/src/models/ModelRouter.js +74 -4
- 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 +125 -75
- package/src/skills/SkillLoader.js +132 -17
- package/src/storage/TaskStore.js +19 -1
- package/src/tools/browserAutomation.js +615 -104
- package/src/tools/executeCommand.js +19 -1
- package/src/tools/index.js +7 -1
- 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 +236 -12
- package/src/webhooks/WebhookHandler.js +107 -0
- 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
|
-
//
|
|
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 (
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
// ---
|
|
108
|
-
app.get("/
|
|
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
|
-
|
|
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:
|
|
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
|
|
283
|
-
: { url, transport
|
|
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
|
-
//
|
|
454
|
-
|
|
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
|
-
//
|
|
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(`[
|
|
1120
|
+
console.log(`[Startup] Channel start error: ${e.message}`);
|
|
461
1121
|
}
|
|
462
|
-
|
|
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
|