n2n-nexus 0.4.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +114 -0
- package/README.md +77 -117
- package/assets/n2n-nexus-banner.jpg +0 -0
- package/assets/n2n-nexus-icon.png +0 -0
- package/assets/n2n-nexus-logo.png +0 -0
- package/build/client/nexus-client.js +44 -21
- package/build/config/cli.js +0 -3
- package/build/config/index.js +0 -10
- package/build/constants.js +7 -11
- package/build/daemon/index.js +35 -19
- package/build/daemon/rate-limit.js +62 -0
- package/build/daemon/server.js +422 -545
- package/build/daemon/tool-outputs.js +177 -0
- package/build/daemon/tools.js +321 -0
- package/build/index.js +1 -1
- package/build/server/instance-id.js +19 -0
- package/build/server/nexus.js +243 -47
- package/build/storage/direct-messages.js +89 -0
- package/build/storage/docs.js +14 -7
- package/build/storage/events.js +31 -0
- package/build/storage/fs-utils.js +42 -0
- package/build/storage/global-messages.js +44 -0
- package/build/storage/index.js +47 -41
- package/build/storage/migrations.js +86 -0
- package/build/storage/paths.js +0 -3
- package/build/storage/projects.js +4 -5
- package/build/storage/registry.js +67 -25
- package/build/storage/sessions.js +77 -0
- package/build/storage/sqlite-meeting.js +35 -23
- package/build/storage/sqlite.js +73 -24
- package/build/storage/tasks.js +29 -103
- package/build/types.js +1 -1
- package/docs/ARCHITECTURE.md +36 -25
- package/docs/ARCHITECTURE_zh.md +37 -26
- package/docs/ASSISTANT_GUIDE.md +77 -65
- package/docs/README_zh.md +34 -19
- package/llms.txt +12 -2
- package/package.json +24 -14
- package/build/storage/logs.js +0 -60
- package/build/storage/meetings.js +0 -276
- package/build/storage/store.js +0 -153
package/build/daemon/server.js
CHANGED
|
@@ -1,364 +1,65 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
|
+
import net from "node:net";
|
|
2
3
|
import { URL } from "node:url";
|
|
3
4
|
import { StorageManager } from "../storage/index.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
5
|
+
import { SqliteMeetingStore } from "../storage/sqlite-meeting.js";
|
|
6
|
+
import { GlobalMessageStore } from "../storage/global-messages.js";
|
|
7
|
+
import { SessionStore } from "../storage/sessions.js";
|
|
8
|
+
import { DirectMessageStore } from "../storage/direct-messages.js";
|
|
9
|
+
import { EventStore } from "../storage/events.js";
|
|
10
|
+
import { runMigrations } from "../storage/migrations.js";
|
|
11
|
+
import { createTask, updateTask, getTask, listTasks, cancelTask } from "../storage/tasks.js";
|
|
12
|
+
import { MAX_BODY_BYTES } from "../constants.js";
|
|
13
|
+
import { ADDRESS_LIMIT_FACTOR, RateLimiter, rateLimitFromEnv } from "./rate-limit.js";
|
|
14
|
+
import { ResultContractError, finalizeResult } from "./tool-outputs.js";
|
|
15
|
+
import { TOOL_DEFINITIONS, isToolName, parseToolArgs, taskUpdateSchema, sessionRegistrationSchema, sessionDisconnectSchema, ToolValidationError } from "./tools.js";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Storage bootstrap
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
6
20
|
async function initializeStorage() {
|
|
7
21
|
await StorageManager.init();
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
return {
|
|
11
|
-
ready: true,
|
|
12
|
-
storageMode: info.storage_mode,
|
|
13
|
-
isDegraded: info.is_degraded
|
|
14
|
-
};
|
|
22
|
+
SqliteMeetingStore.init();
|
|
23
|
+
await runMigrations();
|
|
15
24
|
}
|
|
16
|
-
const sessions = new Map();
|
|
17
|
-
function getSession(instanceId) {
|
|
18
|
-
if (!sessions.has(instanceId))
|
|
19
|
-
sessions.set(instanceId, { currentProject: null });
|
|
20
|
-
return sessions.get(instanceId);
|
|
21
|
-
}
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
// Tool definitions (served via GET /api/tools)
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
const TOOL_DEFINITIONS = [
|
|
26
|
-
// Session
|
|
27
|
-
{
|
|
28
|
-
name: "register_session_context",
|
|
29
|
-
description: "Declare active project. Format: [prefix]_[name] (e.g. 'web_example.com', 'mcp_nexus').",
|
|
30
|
-
inputSchema: {
|
|
31
|
-
type: "object",
|
|
32
|
-
properties: { projectId: { type: "string", description: "Project ID with prefix" } },
|
|
33
|
-
required: ["projectId"]
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
// Project assets
|
|
37
|
-
{
|
|
38
|
-
name: "sync_project_assets",
|
|
39
|
-
description: "[ASYNC] Sync full project manifest + internal docs. Returns taskId.",
|
|
40
|
-
inputSchema: {
|
|
41
|
-
type: "object",
|
|
42
|
-
properties: {
|
|
43
|
-
manifest: {
|
|
44
|
-
type: "object",
|
|
45
|
-
description: "Project metadata",
|
|
46
|
-
properties: {
|
|
47
|
-
id: { type: "string" }, name: { type: "string" },
|
|
48
|
-
description: { type: "string" },
|
|
49
|
-
techStack: { type: "array", items: { type: "string" } },
|
|
50
|
-
relations: { type: "array", items: { type: "object" } },
|
|
51
|
-
repositoryUrl: { type: "string" }, localPath: { type: "string" },
|
|
52
|
-
endpoints: { type: "array", items: { type: "object" } },
|
|
53
|
-
apiSpec: { type: "array", items: { type: "object" } }
|
|
54
|
-
},
|
|
55
|
-
required: ["id", "name", "description", "techStack", "relations", "repositoryUrl", "localPath", "endpoints", "apiSpec"]
|
|
56
|
-
},
|
|
57
|
-
internalDocs: { type: "string", description: "Markdown implementation guide" }
|
|
58
|
-
},
|
|
59
|
-
required: ["manifest", "internalDocs"]
|
|
60
|
-
}
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
name: "upload_project_asset",
|
|
64
|
-
description: "Upload binary file (base64) to active project's asset folder.",
|
|
65
|
-
inputSchema: {
|
|
66
|
-
type: "object",
|
|
67
|
-
properties: {
|
|
68
|
-
fileName: { type: "string", description: "Safe filename (no path traversal)" },
|
|
69
|
-
base64Content: { type: "string" }
|
|
70
|
-
},
|
|
71
|
-
required: ["fileName", "base64Content"]
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
name: "update_project",
|
|
76
|
-
description: "Patch project manifest fields (partial update).",
|
|
77
|
-
inputSchema: {
|
|
78
|
-
type: "object",
|
|
79
|
-
properties: {
|
|
80
|
-
projectId: { type: "string" },
|
|
81
|
-
patch: { type: "object", description: "Fields to update" }
|
|
82
|
-
},
|
|
83
|
-
required: ["projectId", "patch"]
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
name: "rename_project",
|
|
88
|
-
description: "[ASYNC] Rename project ID with cascading relation updates. Returns taskId.",
|
|
89
|
-
inputSchema: {
|
|
90
|
-
type: "object",
|
|
91
|
-
properties: { oldId: { type: "string" }, newId: { type: "string" } },
|
|
92
|
-
required: ["oldId", "newId"]
|
|
93
|
-
}
|
|
94
|
-
},
|
|
95
|
-
// Global collaboration
|
|
96
|
-
{
|
|
97
|
-
name: "search_projects",
|
|
98
|
-
description: "Search project registry by name or description.",
|
|
99
|
-
inputSchema: {
|
|
100
|
-
type: "object",
|
|
101
|
-
properties: {
|
|
102
|
-
query: { type: "string" },
|
|
103
|
-
limit: { type: "integer", default: 10 }
|
|
104
|
-
},
|
|
105
|
-
required: ["query"]
|
|
106
|
-
}
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
name: "get_global_topology",
|
|
110
|
-
description: "Default: project list + stats. With projectId: detailed subgraph.",
|
|
111
|
-
inputSchema: {
|
|
112
|
-
type: "object",
|
|
113
|
-
properties: { projectId: { type: "string", description: "Focus on specific project (optional)" } }
|
|
114
|
-
}
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
name: "send_message",
|
|
118
|
-
description: "Post message to active meeting or global chat.",
|
|
119
|
-
inputSchema: {
|
|
120
|
-
type: "object",
|
|
121
|
-
properties: {
|
|
122
|
-
message: { type: "string" },
|
|
123
|
-
category: { type: "string", enum: ["MEETING_START", "PROPOSAL", "DECISION", "UPDATE", "CHAT"] }
|
|
124
|
-
},
|
|
125
|
-
required: ["message"]
|
|
126
|
-
}
|
|
127
|
-
},
|
|
128
|
-
{
|
|
129
|
-
name: "read_messages",
|
|
130
|
-
description: "Read unread messages (auto-incremental per instance).",
|
|
131
|
-
inputSchema: {
|
|
132
|
-
type: "object",
|
|
133
|
-
properties: {
|
|
134
|
-
count: { type: "integer", default: 10 },
|
|
135
|
-
meetingId: { type: "string" }
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
},
|
|
139
|
-
{
|
|
140
|
-
name: "update_global_strategy",
|
|
141
|
-
description: "Overwrite master strategy document.",
|
|
142
|
-
inputSchema: {
|
|
143
|
-
type: "object",
|
|
144
|
-
properties: { content: { type: "string" } },
|
|
145
|
-
required: ["content"]
|
|
146
|
-
}
|
|
147
|
-
},
|
|
148
|
-
{
|
|
149
|
-
name: "sync_global_doc",
|
|
150
|
-
description: "Create/update a global shared document.",
|
|
151
|
-
inputSchema: {
|
|
152
|
-
type: "object",
|
|
153
|
-
properties: {
|
|
154
|
-
docId: { type: "string" },
|
|
155
|
-
title: { type: "string" },
|
|
156
|
-
content: { type: "string" }
|
|
157
|
-
},
|
|
158
|
-
required: ["docId", "title", "content"]
|
|
159
|
-
}
|
|
160
|
-
},
|
|
161
|
-
// Meeting management
|
|
162
|
-
{
|
|
163
|
-
name: "start_meeting",
|
|
164
|
-
description: "Start new meeting session. Returns meeting ID.",
|
|
165
|
-
inputSchema: {
|
|
166
|
-
type: "object",
|
|
167
|
-
properties: { topic: { type: "string" } },
|
|
168
|
-
required: ["topic"]
|
|
169
|
-
}
|
|
170
|
-
},
|
|
171
|
-
{
|
|
172
|
-
name: "end_meeting",
|
|
173
|
-
description: "End active meeting. Locks history.",
|
|
174
|
-
inputSchema: {
|
|
175
|
-
type: "object",
|
|
176
|
-
properties: {
|
|
177
|
-
meetingId: { type: "string" },
|
|
178
|
-
summary: { type: "string" }
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
},
|
|
182
|
-
{
|
|
183
|
-
name: "archive_meeting",
|
|
184
|
-
description: "Archive closed meeting. Read-only after.",
|
|
185
|
-
inputSchema: {
|
|
186
|
-
type: "object",
|
|
187
|
-
properties: { meetingId: { type: "string" } },
|
|
188
|
-
required: ["meetingId"]
|
|
189
|
-
}
|
|
190
|
-
},
|
|
191
|
-
{
|
|
192
|
-
name: "reopen_meeting",
|
|
193
|
-
description: "Reopen closed/archived meeting.",
|
|
194
|
-
inputSchema: {
|
|
195
|
-
type: "object",
|
|
196
|
-
properties: { meetingId: { type: "string" } },
|
|
197
|
-
required: ["meetingId"]
|
|
198
|
-
}
|
|
199
|
-
},
|
|
200
|
-
// Task management
|
|
201
|
-
{
|
|
202
|
-
name: "create_task",
|
|
203
|
-
description: "[ASYNC] Create background task. Returns taskId for polling.",
|
|
204
|
-
inputSchema: {
|
|
205
|
-
type: "object",
|
|
206
|
-
properties: {
|
|
207
|
-
source_meeting_id: { type: "string", description: "Link to meeting for traceability" },
|
|
208
|
-
metadata: { type: "object" },
|
|
209
|
-
ttl: { type: "integer", description: "TTL in milliseconds" }
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
},
|
|
213
|
-
{
|
|
214
|
-
name: "get_task",
|
|
215
|
-
description: "Get task status and progress by ID.",
|
|
216
|
-
inputSchema: {
|
|
217
|
-
type: "object",
|
|
218
|
-
properties: { taskId: { type: "string" } },
|
|
219
|
-
required: ["taskId"]
|
|
220
|
-
}
|
|
221
|
-
},
|
|
222
|
-
{
|
|
223
|
-
name: "list_tasks",
|
|
224
|
-
description: "List tasks with optional status filter.",
|
|
225
|
-
inputSchema: {
|
|
226
|
-
type: "object",
|
|
227
|
-
properties: {
|
|
228
|
-
status: { type: "string", enum: ["pending", "running", "completed", "failed", "cancelled"] },
|
|
229
|
-
limit: { type: "integer", default: 50 }
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
},
|
|
233
|
-
{
|
|
234
|
-
name: "cancel_task",
|
|
235
|
-
description: "Cancel pending/running task.",
|
|
236
|
-
inputSchema: {
|
|
237
|
-
type: "object",
|
|
238
|
-
properties: { taskId: { type: "string" } },
|
|
239
|
-
required: ["taskId"]
|
|
240
|
-
}
|
|
241
|
-
},
|
|
242
|
-
// Maintenance
|
|
243
|
-
{
|
|
244
|
-
name: "host_maintenance",
|
|
245
|
-
description: "Manage logs: 'prune' oldest N or 'clear' all.",
|
|
246
|
-
inputSchema: {
|
|
247
|
-
type: "object",
|
|
248
|
-
properties: {
|
|
249
|
-
action: { type: "string", enum: ["prune", "clear"] },
|
|
250
|
-
count: { type: "integer", minimum: 0 }
|
|
251
|
-
},
|
|
252
|
-
required: ["action", "count"]
|
|
253
|
-
}
|
|
254
|
-
},
|
|
255
|
-
{
|
|
256
|
-
name: "host_delete_project",
|
|
257
|
-
description: "[ASYNC] Delete project. Irreversible. Returns taskId.",
|
|
258
|
-
inputSchema: {
|
|
259
|
-
type: "object",
|
|
260
|
-
properties: { projectId: { type: "string" } },
|
|
261
|
-
required: ["projectId"]
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
];
|
|
265
25
|
// ---------------------------------------------------------------------------
|
|
266
26
|
// Tool handlers
|
|
267
27
|
// ---------------------------------------------------------------------------
|
|
268
|
-
async function toolRegisterSessionContext(args, instanceId) {
|
|
269
|
-
if (!args.projectId)
|
|
270
|
-
throw new Error("projectId is required.");
|
|
271
|
-
const manifest = await StorageManager.getProjectManifest(args.projectId);
|
|
272
|
-
if (!manifest) {
|
|
273
|
-
return { ok: false, message: `Project '${args.projectId}' not found in registry.` };
|
|
274
|
-
}
|
|
275
|
-
getSession(instanceId).currentProject = args.projectId;
|
|
276
|
-
await StorageManager.addGlobalLog("SYSTEM", `Session Context set to Project: ${args.projectId} (instance: ${instanceId})`);
|
|
277
|
-
return { ok: true, message: `Session context registered for project: ${args.projectId}.` };
|
|
278
|
-
}
|
|
279
28
|
async function toolSyncProjectAssets(args, instanceId) {
|
|
280
|
-
const m = args.manifest;
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
metadata: { operation: "sync_project_assets", projectId: m.id, initiator: instanceId }
|
|
285
|
-
});
|
|
286
|
-
setImmediate(async () => {
|
|
287
|
-
try {
|
|
288
|
-
updateTask(task.id, { status: "running", progress: 0.1 });
|
|
289
|
-
await StorageManager.saveProjectManifest(m);
|
|
290
|
-
updateTask(task.id, { progress: 0.5 });
|
|
291
|
-
await StorageManager.saveProjectDocs(m.id, args.internalDocs);
|
|
292
|
-
updateTask(task.id, { progress: 0.9 });
|
|
293
|
-
await StorageManager.addGlobalLog("SYSTEM", `[${instanceId}@${m.id}] Asset Sync completed.`);
|
|
294
|
-
updateTask(task.id, { status: "completed", progress: 1.0, result_uri: `nexus://projects/${m.id}/manifest` });
|
|
295
|
-
}
|
|
296
|
-
catch (e) {
|
|
297
|
-
updateTask(task.id, { status: "failed", error_message: String(e) });
|
|
298
|
-
}
|
|
299
|
-
});
|
|
300
|
-
return { task_id: task.id, status: "pending", message: "Sync task created. Use get_task to poll for completion." };
|
|
29
|
+
const m = { ...args.manifest, lastUpdated: args.manifest.lastUpdated ?? new Date().toISOString() };
|
|
30
|
+
await StorageManager.saveProject(m, args.internalDocs);
|
|
31
|
+
EventStore.record(instanceId, "project.sync", m.id);
|
|
32
|
+
return { ok: true, projectId: m.id, message: `Project '${m.id}' manifest and docs synced.` };
|
|
301
33
|
}
|
|
302
34
|
async function toolUploadAsset(args, instanceId) {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
throw new Error("No active project. Call register_session_context first.");
|
|
306
|
-
if (!args.fileName || !args.base64Content)
|
|
307
|
-
throw new Error("fileName and base64Content are required.");
|
|
35
|
+
if (!await StorageManager.getProjectManifest(args.projectId))
|
|
36
|
+
throw new Error(`Project '${args.projectId}' not found.`);
|
|
308
37
|
const sanitized = args.fileName.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
309
38
|
const buf = Buffer.from(args.base64Content, "base64");
|
|
310
|
-
await StorageManager.saveAsset(
|
|
311
|
-
|
|
39
|
+
await StorageManager.saveAsset(args.projectId, sanitized, buf);
|
|
40
|
+
EventStore.record(instanceId, "project.asset", args.projectId, { fileName: sanitized, bytes: buf.length });
|
|
41
|
+
return { ok: true, projectId: args.projectId, fileName: sanitized, message: `Asset '${sanitized}' saved to project '${args.projectId}'.` };
|
|
312
42
|
}
|
|
313
43
|
async function toolUpdateProject(args, instanceId) {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
throw new Error("Cannot change 'id' via patch. Use rename_project instead.");
|
|
318
|
-
const task = createTask({
|
|
319
|
-
metadata: { operation: "update_project", projectId: args.projectId, initiator: instanceId }
|
|
320
|
-
});
|
|
321
|
-
setImmediate(async () => {
|
|
322
|
-
try {
|
|
323
|
-
updateTask(task.id, { status: "running", progress: 0.2 });
|
|
324
|
-
await StorageManager.patchProjectManifest(args.projectId, args.patch);
|
|
325
|
-
updateTask(task.id, { status: "completed", progress: 1.0, result_uri: `nexus://projects/${args.projectId}/manifest` });
|
|
326
|
-
}
|
|
327
|
-
catch (e) {
|
|
328
|
-
updateTask(task.id, { status: "failed", error_message: String(e) });
|
|
329
|
-
}
|
|
330
|
-
});
|
|
331
|
-
return { task_id: task.id, status: "pending", message: "Update task created." };
|
|
44
|
+
await StorageManager.patchProjectManifest(args.projectId, args.patch);
|
|
45
|
+
EventStore.record(instanceId, "project.update", args.projectId, { fields: Object.keys(args.patch) });
|
|
46
|
+
return { ok: true, projectId: args.projectId, message: `Project '${args.projectId}' updated.` };
|
|
332
47
|
}
|
|
333
48
|
async function toolRenameProject(args, instanceId) {
|
|
334
|
-
if (
|
|
335
|
-
throw new Error("oldId and newId are
|
|
49
|
+
if (args.oldId === args.newId)
|
|
50
|
+
throw new Error("oldId and newId are identical.");
|
|
336
51
|
const exists = await StorageManager.getProjectManifest(args.oldId);
|
|
337
52
|
if (!exists)
|
|
338
53
|
throw new Error(`Project '${args.oldId}' not found.`);
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
}
|
|
342
|
-
setImmediate(async () => {
|
|
343
|
-
try {
|
|
344
|
-
updateTask(task.id, { status: "running", progress: 0.2 });
|
|
345
|
-
const count = await StorageManager.renameProject(args.oldId, args.newId);
|
|
346
|
-
await StorageManager.addGlobalLog("SYSTEM", `Project renamed '${args.oldId}' → '${args.newId}' (${count} cascading updates).`);
|
|
347
|
-
updateTask(task.id, { status: "completed", progress: 1.0, result_uri: `nexus://projects/${args.newId}/manifest` });
|
|
348
|
-
}
|
|
349
|
-
catch (e) {
|
|
350
|
-
updateTask(task.id, { status: "failed", error_message: String(e) });
|
|
351
|
-
}
|
|
352
|
-
});
|
|
353
|
-
return { task_id: task.id, status: "pending", message: "Rename task created." };
|
|
54
|
+
await StorageManager.renameProject(args.oldId, args.newId);
|
|
55
|
+
EventStore.record(instanceId, "project.rename", args.newId, { oldId: args.oldId });
|
|
56
|
+
return { ok: true, oldId: args.oldId, newId: args.newId, message: `Project renamed '${args.oldId}' → '${args.newId}'.` };
|
|
354
57
|
}
|
|
355
58
|
async function toolSearchProjects(args) {
|
|
356
|
-
if (!args.query)
|
|
357
|
-
throw new Error("query is required.");
|
|
358
59
|
const registry = await StorageManager.listRegistry();
|
|
359
60
|
const projects = registry.projects;
|
|
360
61
|
const q = args.query.toLowerCase();
|
|
361
|
-
const limit = args.limit
|
|
62
|
+
const limit = args.limit;
|
|
362
63
|
const results = Object.entries(projects)
|
|
363
64
|
.filter(([id, p]) => id.toLowerCase().includes(q) ||
|
|
364
65
|
(p.name && p.name.toLowerCase().includes(q)) ||
|
|
@@ -370,12 +71,29 @@ async function toolSearchProjects(args) {
|
|
|
370
71
|
async function toolGetGlobalTopology(args) {
|
|
371
72
|
return StorageManager.calculateTopology(args.projectId);
|
|
372
73
|
}
|
|
373
|
-
async function
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
74
|
+
async function toolGetProject(args) {
|
|
75
|
+
const manifest = await StorageManager.getProjectManifest(args.projectId);
|
|
76
|
+
if (!manifest)
|
|
77
|
+
throw new Error(`Project '${args.projectId}' not found.`);
|
|
78
|
+
return args.includeDocs
|
|
79
|
+
? { manifest, internalDocs: await StorageManager.getProjectDocs(args.projectId) }
|
|
80
|
+
: { manifest };
|
|
81
|
+
}
|
|
82
|
+
function requireSession(sessionId, instanceId) {
|
|
83
|
+
if (!sessionId)
|
|
84
|
+
throw new Error("A registered sessionId is required");
|
|
85
|
+
return SessionStore.require(sessionId, instanceId);
|
|
86
|
+
}
|
|
87
|
+
async function toolSendMessage(args, instanceId, sessionId) {
|
|
88
|
+
if (args.to !== undefined) {
|
|
89
|
+
const sender = requireSession(sessionId, instanceId);
|
|
90
|
+
const saved = DirectMessageStore.send(sender.sessionId, { ...args, to: args.to });
|
|
91
|
+
return {
|
|
92
|
+
ok: true, sentTo: "session", messageId: saved.messageId, status: saved.status,
|
|
93
|
+
to: saved.to, replyTo: saved.replyTo, message: `Direct message ${saved.status}.`
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const meetingId = args.meetingId ?? SqliteMeetingStore.getActiveMeetingId();
|
|
379
97
|
if (meetingId) {
|
|
380
98
|
const msg = {
|
|
381
99
|
from: instanceId,
|
|
@@ -383,72 +101,87 @@ async function toolSendMessage(args, instanceId) {
|
|
|
383
101
|
category: args.category,
|
|
384
102
|
timestamp: new Date().toISOString()
|
|
385
103
|
};
|
|
386
|
-
|
|
104
|
+
SqliteMeetingStore.addMessage(meetingId, msg);
|
|
387
105
|
return { ok: true, sentTo: "meeting", meetingId, message: `Message sent to meeting ${meetingId}.` };
|
|
388
106
|
}
|
|
389
|
-
|
|
390
|
-
return { ok: true, sentTo: "global", message: "Message sent to global chat." };
|
|
107
|
+
const saved = GlobalMessageStore.addMessage(instanceId, args.message, args.category);
|
|
108
|
+
return { ok: true, sentTo: "global", messageId: saved.id, message: "Message sent to global chat." };
|
|
391
109
|
}
|
|
392
110
|
async function toolReadMessages(args, instanceId) {
|
|
393
|
-
const count = args.count
|
|
111
|
+
const count = args.count;
|
|
394
112
|
if (args.meetingId) {
|
|
395
|
-
const meeting =
|
|
113
|
+
const meeting = SqliteMeetingStore.getMeeting(args.meetingId);
|
|
396
114
|
if (!meeting)
|
|
397
115
|
throw new Error(`Meeting '${args.meetingId}' not found.`);
|
|
398
|
-
const messages =
|
|
116
|
+
const messages = SqliteMeetingStore.getRecentMessages(count, args.meetingId, instanceId);
|
|
399
117
|
return { source: "meeting", meetingId: args.meetingId, messages, count: messages.length };
|
|
400
118
|
}
|
|
401
|
-
const
|
|
402
|
-
return { source: "global", messages
|
|
119
|
+
const messages = GlobalMessageStore.readUnread(instanceId, count);
|
|
120
|
+
return { source: "global", messages, count: messages.length };
|
|
403
121
|
}
|
|
404
122
|
async function toolUpdateGlobalStrategy(args, instanceId) {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
await StorageManager.saveGlobalDoc("strategy", "Global Collaboration Strategy", args.content, instanceId);
|
|
123
|
+
await StorageManager.saveGlobalBlueprint(args.content);
|
|
124
|
+
EventStore.record(instanceId, "strategy.update");
|
|
408
125
|
return { ok: true, message: "Global strategy updated." };
|
|
409
126
|
}
|
|
127
|
+
async function toolGetGlobalStrategy() {
|
|
128
|
+
return { content: await StorageManager.getGlobalBlueprint() };
|
|
129
|
+
}
|
|
130
|
+
async function toolListGlobalDocs() {
|
|
131
|
+
const index = await StorageManager.listGlobalDocs();
|
|
132
|
+
const docs = Object.entries(index)
|
|
133
|
+
.map(([docId, entry]) => ({ docId, ...entry }))
|
|
134
|
+
.sort((a, b) => b.lastUpdated.localeCompare(a.lastUpdated));
|
|
135
|
+
return { count: docs.length, docs };
|
|
136
|
+
}
|
|
137
|
+
async function toolGetGlobalDoc(args) {
|
|
138
|
+
const entry = (await StorageManager.listGlobalDocs())[args.docId];
|
|
139
|
+
if (!entry)
|
|
140
|
+
throw new Error(`Document '${args.docId}' not found.`);
|
|
141
|
+
return { docId: args.docId, ...entry, content: await StorageManager.getGlobalDoc(args.docId) };
|
|
142
|
+
}
|
|
410
143
|
async function toolSyncGlobalDoc(args, instanceId) {
|
|
411
|
-
if (!args.docId || !args.title || !args.content)
|
|
412
|
-
throw new Error("docId, title, and content are required.");
|
|
413
144
|
await StorageManager.saveGlobalDoc(args.docId, args.title, args.content, instanceId);
|
|
145
|
+
EventStore.record(instanceId, "doc.sync", args.docId, { title: args.title });
|
|
414
146
|
return { ok: true, message: `Global document '${args.title}' (${args.docId}) synchronized.` };
|
|
415
147
|
}
|
|
148
|
+
async function toolListMeetings(args) {
|
|
149
|
+
const meetings = SqliteMeetingStore.listMeetings(args.status, args.limit);
|
|
150
|
+
return { count: meetings.length, activeMeetingId: SqliteMeetingStore.getActiveMeetingId(), meetings };
|
|
151
|
+
}
|
|
416
152
|
async function toolStartMeeting(args, instanceId) {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
const meeting = await UnifiedMeetingStore.startMeeting(args.topic, instanceId);
|
|
153
|
+
const meeting = SqliteMeetingStore.startMeeting(args.topic, instanceId);
|
|
154
|
+
EventStore.record(instanceId, "meeting.start", meeting.id, { topic: meeting.topic });
|
|
420
155
|
return { ok: true, meetingId: meeting.id, topic: meeting.topic, status: meeting.status };
|
|
421
156
|
}
|
|
422
157
|
async function toolEndMeeting(args, instanceId) {
|
|
423
|
-
|
|
424
|
-
if (!id) {
|
|
425
|
-
const active = await UnifiedMeetingStore.getActiveMeeting();
|
|
426
|
-
if (active)
|
|
427
|
-
id = active.id;
|
|
428
|
-
}
|
|
158
|
+
const id = args.meetingId ?? SqliteMeetingStore.getActiveMeetingId();
|
|
429
159
|
if (!id)
|
|
430
160
|
throw new Error("No active meeting found to end.");
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
}
|
|
435
|
-
return { ok: true, meetingId: id, status: "closed", summary: result.meeting.summary };
|
|
161
|
+
const meeting = SqliteMeetingStore.endMeeting(id, args.summary, instanceId);
|
|
162
|
+
EventStore.record(instanceId, "meeting.end", id, { topic: meeting.topic, summary: args.summary });
|
|
163
|
+
return { ok: true, meetingId: id, status: "closed", summary: meeting.summary };
|
|
436
164
|
}
|
|
437
165
|
async function toolArchiveMeeting(args, instanceId) {
|
|
438
|
-
const meeting =
|
|
166
|
+
const meeting = SqliteMeetingStore.getMeeting(args.meetingId);
|
|
439
167
|
if (!meeting)
|
|
440
168
|
throw new Error(`Meeting '${args.meetingId}' not found.`);
|
|
441
|
-
|
|
169
|
+
SqliteMeetingStore.archiveMeeting(args.meetingId, instanceId);
|
|
170
|
+
EventStore.record(instanceId, "meeting.archive", args.meetingId);
|
|
442
171
|
return { ok: true, meetingId: args.meetingId, status: "archived" };
|
|
443
172
|
}
|
|
444
173
|
async function toolReopenMeeting(args, instanceId) {
|
|
445
|
-
const meeting =
|
|
174
|
+
const meeting = SqliteMeetingStore.getMeeting(args.meetingId);
|
|
446
175
|
if (!meeting)
|
|
447
176
|
throw new Error(`Meeting '${args.meetingId}' not found.`);
|
|
448
|
-
|
|
177
|
+
SqliteMeetingStore.reopenMeeting(args.meetingId, instanceId);
|
|
178
|
+
EventStore.record(instanceId, "meeting.reopen", args.meetingId);
|
|
449
179
|
return { ok: true, meetingId: args.meetingId, status: "active" };
|
|
450
180
|
}
|
|
451
181
|
async function toolCreateTask(args) {
|
|
182
|
+
if (args.source_meeting_id && !SqliteMeetingStore.getMeeting(args.source_meeting_id)) {
|
|
183
|
+
throw new Error(`Meeting '${args.source_meeting_id}' not found.`);
|
|
184
|
+
}
|
|
452
185
|
const task = createTask({ source_meeting_id: args.source_meeting_id, metadata: args.metadata, ttl: args.ttl });
|
|
453
186
|
return { task_id: task.id, status: task.status, message: "Task created.", task };
|
|
454
187
|
}
|
|
@@ -459,7 +192,7 @@ async function toolGetTask(args) {
|
|
|
459
192
|
return task;
|
|
460
193
|
}
|
|
461
194
|
async function toolListTasks(args) {
|
|
462
|
-
const tasks = listTasks(args.status, args.limit
|
|
195
|
+
const tasks = listTasks(args.status, args.limit);
|
|
463
196
|
return { count: tasks.length, tasks };
|
|
464
197
|
}
|
|
465
198
|
async function toolCancelTask(args) {
|
|
@@ -468,209 +201,362 @@ async function toolCancelTask(args) {
|
|
|
468
201
|
throw new Error(`Task '${args.taskId}' not found or cannot be cancelled.`);
|
|
469
202
|
return { ok: true, taskId: args.taskId, message: "Task cancelled." };
|
|
470
203
|
}
|
|
471
|
-
async function toolHostMaintenance(args) {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
|
|
204
|
+
async function toolHostMaintenance(args, instanceId) {
|
|
205
|
+
const removed = args.action === "clear"
|
|
206
|
+
? GlobalMessageStore.clear()
|
|
207
|
+
: GlobalMessageStore.pruneOldest(args.count);
|
|
208
|
+
EventStore.record(instanceId, `maintenance.${args.action}`, undefined, { removed });
|
|
209
|
+
return { ok: true, removed, remaining: GlobalMessageStore.count(), message: `Removed ${removed} global messages.` };
|
|
210
|
+
}
|
|
211
|
+
async function toolListEvents(args) {
|
|
212
|
+
const events = EventStore.list(args.limit, args.action);
|
|
213
|
+
return { count: events.length, events };
|
|
478
214
|
}
|
|
479
215
|
async function toolHostDeleteProject(args, instanceId) {
|
|
480
216
|
const exists = await StorageManager.getProjectManifest(args.projectId);
|
|
481
217
|
if (!exists)
|
|
482
218
|
throw new Error(`Project '${args.projectId}' not found.`);
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
}
|
|
486
|
-
setImmediate(async () => {
|
|
487
|
-
try {
|
|
488
|
-
updateTask(task.id, { status: "running", progress: 0.3 });
|
|
489
|
-
await StorageManager.deleteProject(args.projectId);
|
|
490
|
-
await StorageManager.addGlobalLog("SYSTEM", `Project deleted: '${args.projectId}' by ${instanceId}.`);
|
|
491
|
-
updateTask(task.id, { status: "completed", progress: 1.0 });
|
|
492
|
-
}
|
|
493
|
-
catch (e) {
|
|
494
|
-
updateTask(task.id, { status: "failed", error_message: String(e) });
|
|
495
|
-
}
|
|
496
|
-
});
|
|
497
|
-
return { task_id: task.id, status: "pending", message: "Delete task created." };
|
|
219
|
+
await StorageManager.deleteProject(args.projectId);
|
|
220
|
+
EventStore.record(instanceId, "project.delete", args.projectId);
|
|
221
|
+
return { ok: true, projectId: args.projectId, message: `Project '${args.projectId}' deleted.` };
|
|
498
222
|
}
|
|
499
223
|
// ---------------------------------------------------------------------------
|
|
500
224
|
// Tool dispatcher
|
|
501
225
|
// ---------------------------------------------------------------------------
|
|
502
|
-
async function dispatchTool(name,
|
|
503
|
-
const u = args;
|
|
226
|
+
async function dispatchTool(name, rawArgs, instanceId, sessionId) {
|
|
504
227
|
switch (name) {
|
|
505
|
-
case "
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
case "
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
case "
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
case "
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
case "
|
|
526
|
-
|
|
527
|
-
|
|
228
|
+
case "list_sessions": {
|
|
229
|
+
const args = parseToolArgs(name, rawArgs);
|
|
230
|
+
const self = sessionId ? requireSession(sessionId, instanceId) : null;
|
|
231
|
+
const sessions = SessionStore.list(args);
|
|
232
|
+
return { self, sessions, count: sessions.length };
|
|
233
|
+
}
|
|
234
|
+
case "update_session": {
|
|
235
|
+
const args = parseToolArgs(name, rawArgs);
|
|
236
|
+
return { session: SessionStore.update(requireSession(sessionId, instanceId).sessionId, args) };
|
|
237
|
+
}
|
|
238
|
+
case "read_inbox": {
|
|
239
|
+
const args = parseToolArgs(name, rawArgs);
|
|
240
|
+
const self = requireSession(sessionId, instanceId);
|
|
241
|
+
const messages = DirectMessageStore.readInbox(self.sessionId, args.count);
|
|
242
|
+
return { sessionId: self.sessionId, messages, count: messages.length };
|
|
243
|
+
}
|
|
244
|
+
case "get_message": {
|
|
245
|
+
const args = parseToolArgs(name, rawArgs);
|
|
246
|
+
return { message: DirectMessageStore.get(args.messageId, requireSession(sessionId, instanceId).sessionId) };
|
|
247
|
+
}
|
|
248
|
+
case "acknowledge_message": {
|
|
249
|
+
const args = parseToolArgs(name, rawArgs);
|
|
250
|
+
return { message: DirectMessageStore.acknowledge(args.messageId, requireSession(sessionId, instanceId).sessionId, args.outcome) };
|
|
251
|
+
}
|
|
252
|
+
case "sync_project_assets": return toolSyncProjectAssets(parseToolArgs(name, rawArgs), instanceId);
|
|
253
|
+
case "upload_project_asset": return toolUploadAsset(parseToolArgs(name, rawArgs), instanceId);
|
|
254
|
+
case "update_project": return toolUpdateProject(parseToolArgs(name, rawArgs), instanceId);
|
|
255
|
+
case "rename_project": return toolRenameProject(parseToolArgs(name, rawArgs), instanceId);
|
|
256
|
+
case "search_projects": return toolSearchProjects(parseToolArgs(name, rawArgs));
|
|
257
|
+
case "get_global_topology": return toolGetGlobalTopology(parseToolArgs(name, rawArgs));
|
|
258
|
+
case "get_project": return toolGetProject(parseToolArgs(name, rawArgs));
|
|
259
|
+
case "send_message": return toolSendMessage(parseToolArgs(name, rawArgs), instanceId, sessionId);
|
|
260
|
+
case "read_messages": return toolReadMessages(parseToolArgs(name, rawArgs), instanceId);
|
|
261
|
+
case "list_events": return toolListEvents(parseToolArgs(name, rawArgs));
|
|
262
|
+
case "get_global_strategy":
|
|
263
|
+
parseToolArgs(name, rawArgs);
|
|
264
|
+
return toolGetGlobalStrategy();
|
|
265
|
+
case "update_global_strategy": return toolUpdateGlobalStrategy(parseToolArgs(name, rawArgs), instanceId);
|
|
266
|
+
case "list_global_docs":
|
|
267
|
+
parseToolArgs(name, rawArgs);
|
|
268
|
+
return toolListGlobalDocs();
|
|
269
|
+
case "get_global_doc": return toolGetGlobalDoc(parseToolArgs(name, rawArgs));
|
|
270
|
+
case "sync_global_doc": return toolSyncGlobalDoc(parseToolArgs(name, rawArgs), instanceId);
|
|
271
|
+
case "list_meetings": return toolListMeetings(parseToolArgs(name, rawArgs));
|
|
272
|
+
case "start_meeting": return toolStartMeeting(parseToolArgs(name, rawArgs), instanceId);
|
|
273
|
+
case "end_meeting": return toolEndMeeting(parseToolArgs(name, rawArgs), instanceId);
|
|
274
|
+
case "archive_meeting": return toolArchiveMeeting(parseToolArgs(name, rawArgs), instanceId);
|
|
275
|
+
case "reopen_meeting": return toolReopenMeeting(parseToolArgs(name, rawArgs), instanceId);
|
|
276
|
+
case "create_task": return toolCreateTask(parseToolArgs(name, rawArgs));
|
|
277
|
+
case "get_task": return toolGetTask(parseToolArgs(name, rawArgs));
|
|
278
|
+
case "list_tasks": return toolListTasks(parseToolArgs(name, rawArgs));
|
|
279
|
+
case "cancel_task": return toolCancelTask(parseToolArgs(name, rawArgs));
|
|
280
|
+
case "host_maintenance": return toolHostMaintenance(parseToolArgs(name, rawArgs), instanceId);
|
|
281
|
+
case "host_delete_project": return toolHostDeleteProject(parseToolArgs(name, rawArgs), instanceId);
|
|
528
282
|
}
|
|
529
283
|
}
|
|
530
284
|
// ---------------------------------------------------------------------------
|
|
531
285
|
// HTTP helpers
|
|
532
286
|
// ---------------------------------------------------------------------------
|
|
533
|
-
function jsonResponse(res, status, body) {
|
|
287
|
+
function jsonResponse(res, status, body, headers = {}) {
|
|
534
288
|
const payload = JSON.stringify(body);
|
|
535
|
-
res.writeHead(status, {
|
|
289
|
+
res.writeHead(status, {
|
|
290
|
+
"Content-Type": "application/json",
|
|
291
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
292
|
+
...(status === 413 ? { Connection: "close" } : {}),
|
|
293
|
+
...headers
|
|
294
|
+
});
|
|
536
295
|
res.end(payload);
|
|
537
296
|
}
|
|
538
|
-
|
|
297
|
+
class HttpError extends Error {
|
|
298
|
+
status;
|
|
299
|
+
headers;
|
|
300
|
+
constructor(status, message, headers = {}) {
|
|
301
|
+
super(message);
|
|
302
|
+
this.status = status;
|
|
303
|
+
this.headers = headers;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function rateLimited(retryAfterSec, scope) {
|
|
307
|
+
return new HttpError(429, `Rate limit exceeded for ${scope}; retry after ${retryAfterSec}s.`, { "Retry-After": String(retryAfterSec) });
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Run a tool for an HTTP request. Validation failures and handler rejections
|
|
311
|
+
* are the caller's fault and surface as 400, not as daemon errors.
|
|
312
|
+
*/
|
|
313
|
+
async function callTool(name, args, instanceId, sessionId) {
|
|
314
|
+
try {
|
|
315
|
+
return finalizeResult(name, await dispatchTool(name, args, instanceId, sessionId));
|
|
316
|
+
}
|
|
317
|
+
catch (e) {
|
|
318
|
+
if (e instanceof HttpError)
|
|
319
|
+
throw e;
|
|
320
|
+
if (e instanceof ResultContractError) {
|
|
321
|
+
// A daemon-side defect (or unreadable stored data), not the caller's fault.
|
|
322
|
+
console.error("[n2n-nexus daemon]", e.message);
|
|
323
|
+
throw new HttpError(500, e.message);
|
|
324
|
+
}
|
|
325
|
+
throw new HttpError(400, e instanceof Error ? e.message : String(e));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/** Instance id recorded for requests that do not name one. */
|
|
329
|
+
const UNKNOWN_INSTANCE = "unknown";
|
|
330
|
+
function actor(body) {
|
|
331
|
+
return typeof body.instanceId === "string" && body.instanceId ? body.instanceId : UNKNOWN_INSTANCE;
|
|
332
|
+
}
|
|
333
|
+
function sessionActor(body) {
|
|
334
|
+
return z.string().min(1).optional().parse(body.sessionId);
|
|
335
|
+
}
|
|
336
|
+
/** Query-string parameters as tool arguments; `numeric` names are converted to numbers. */
|
|
337
|
+
function queryArgs(url, numeric) {
|
|
338
|
+
const args = {};
|
|
339
|
+
for (const [key, value] of url.searchParams) {
|
|
340
|
+
if (key === "instanceId")
|
|
341
|
+
continue;
|
|
342
|
+
args[key] = numeric.includes(key) ? Number(value) : value;
|
|
343
|
+
}
|
|
344
|
+
return args;
|
|
345
|
+
}
|
|
346
|
+
async function parseRequestBody(req, maxBytes) {
|
|
347
|
+
const declared = Number(req.headers["content-length"]);
|
|
348
|
+
if (declared > maxBytes)
|
|
349
|
+
throw new HttpError(413, `Request body exceeds ${maxBytes} bytes`);
|
|
539
350
|
return new Promise((resolve, reject) => {
|
|
540
|
-
|
|
541
|
-
|
|
351
|
+
const chunks = [];
|
|
352
|
+
let received = 0;
|
|
353
|
+
let tooLarge = false;
|
|
354
|
+
req.on("data", (chunk) => {
|
|
355
|
+
if (tooLarge)
|
|
356
|
+
return;
|
|
357
|
+
received += chunk.length;
|
|
358
|
+
if (received > maxBytes) {
|
|
359
|
+
// Keep draining so the 413 can still be written; the response closes the connection.
|
|
360
|
+
tooLarge = true;
|
|
361
|
+
chunks.length = 0;
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
chunks.push(chunk);
|
|
365
|
+
});
|
|
542
366
|
req.on("end", () => {
|
|
543
|
-
if (
|
|
367
|
+
if (tooLarge)
|
|
368
|
+
return reject(new HttpError(413, `Request body exceeds ${maxBytes} bytes`));
|
|
369
|
+
if (received === 0)
|
|
544
370
|
return resolve({});
|
|
371
|
+
let parsed;
|
|
545
372
|
try {
|
|
546
|
-
|
|
373
|
+
parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
547
374
|
}
|
|
548
375
|
catch {
|
|
549
|
-
reject(new
|
|
376
|
+
return reject(new HttpError(400, "Invalid JSON"));
|
|
377
|
+
}
|
|
378
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
379
|
+
return reject(new HttpError(400, "Request body must be a JSON object"));
|
|
550
380
|
}
|
|
381
|
+
resolve(parsed);
|
|
551
382
|
});
|
|
552
383
|
req.on("error", reject);
|
|
553
384
|
});
|
|
554
385
|
}
|
|
386
|
+
/** Host names (without port) that may address the daemon besides IP literals. */
|
|
387
|
+
const DEFAULT_ALLOWED_HOSTS = ["localhost", "host.docker.internal"];
|
|
388
|
+
export function allowedHostsFromEnv(env = process.env) {
|
|
389
|
+
const extra = (env.NEXUS_ALLOWED_HOSTS || "").split(",").map(h => h.trim().toLowerCase()).filter(Boolean);
|
|
390
|
+
return [...DEFAULT_ALLOWED_HOSTS, ...extra];
|
|
391
|
+
}
|
|
392
|
+
/** Hostname part of a Host header: strips the port and IPv6 brackets. */
|
|
393
|
+
function hostName(header) {
|
|
394
|
+
const m = header.match(/^\[([^\]]+)\](?::\d+)?$/);
|
|
395
|
+
if (m)
|
|
396
|
+
return m[1].toLowerCase();
|
|
397
|
+
return header.replace(/:\d+$/, "").toLowerCase();
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* The daemon has no authentication, so it must not be callable from a web
|
|
401
|
+
* page the user happens to have open. Browsers attach Origin / Sec-Fetch-*
|
|
402
|
+
* headers to cross-site requests and only send JSON bodies after a CORS
|
|
403
|
+
* preflight, which the daemon never grants; MCP adapters and scripts send
|
|
404
|
+
* neither header and use application/json. A DNS-rebinding page reaches the
|
|
405
|
+
* daemon as a same-origin request under its own host name, so the Host
|
|
406
|
+
* header must be an IP literal or a known local name.
|
|
407
|
+
*/
|
|
408
|
+
function rejectBrowserRequest(req, allowedHosts) {
|
|
409
|
+
const host = req.headers.host;
|
|
410
|
+
if (!host)
|
|
411
|
+
return new HttpError(400, "Host header is required");
|
|
412
|
+
const name = hostName(host);
|
|
413
|
+
if (!net.isIP(name) && !allowedHosts.includes(name)) {
|
|
414
|
+
return new HttpError(403, `Host '${name}' is not allowed; use an IP address or add it to NEXUS_ALLOWED_HOSTS`);
|
|
415
|
+
}
|
|
416
|
+
if (req.headers.origin !== undefined) {
|
|
417
|
+
return new HttpError(403, "Requests from browser origins are not allowed");
|
|
418
|
+
}
|
|
419
|
+
const site = req.headers["sec-fetch-site"];
|
|
420
|
+
if (typeof site === "string" && site !== "none" && site !== "same-origin") {
|
|
421
|
+
return new HttpError(403, "Cross-site browser requests are not allowed");
|
|
422
|
+
}
|
|
423
|
+
if (req.method === "POST") {
|
|
424
|
+
const type = String(req.headers["content-type"] || "").split(";")[0].trim().toLowerCase();
|
|
425
|
+
if (type !== "application/json") {
|
|
426
|
+
return new HttpError(415, "Content-Type must be application/json");
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
555
431
|
// ---------------------------------------------------------------------------
|
|
556
432
|
// Server factory
|
|
557
433
|
// ---------------------------------------------------------------------------
|
|
558
434
|
export async function createDaemonServer(options) {
|
|
559
|
-
|
|
435
|
+
await initializeStorage();
|
|
436
|
+
const maxBodyBytes = options.maxBodyBytes ?? MAX_BODY_BYTES;
|
|
437
|
+
const allowedHosts = options.allowedHosts ?? allowedHostsFromEnv();
|
|
438
|
+
const limit = options.rateLimit ?? rateLimitFromEnv();
|
|
439
|
+
const instanceLimiter = new RateLimiter(limit.perSecond, limit.burst);
|
|
440
|
+
const addressLimiter = new RateLimiter(limit.perSecond * ADDRESS_LIMIT_FACTOR, limit.burst * ADDRESS_LIMIT_FACTOR);
|
|
441
|
+
/**
|
|
442
|
+
* Tool calls from HTTP routes, counted against the calling instance's budget.
|
|
443
|
+
* Requests that name no instance ("unknown") are only limited per address:
|
|
444
|
+
* pooling them in one shared bucket let any client exhaust it for all others.
|
|
445
|
+
*/
|
|
446
|
+
const call = (name, args, instanceId, sessionId) => {
|
|
447
|
+
if (instanceId !== UNKNOWN_INSTANCE) {
|
|
448
|
+
const wait = instanceLimiter.take(`instance:${instanceId}`);
|
|
449
|
+
if (wait)
|
|
450
|
+
throw rateLimited(wait, `instance '${instanceId}'`);
|
|
451
|
+
}
|
|
452
|
+
return callTool(name, args, instanceId, sessionId);
|
|
453
|
+
};
|
|
560
454
|
const server = http.createServer(async (req, res) => {
|
|
561
455
|
const method = req.method || "GET";
|
|
562
|
-
const
|
|
563
|
-
const path = url.pathname;
|
|
456
|
+
const parseBody = (r) => parseRequestBody(r, maxBodyBytes);
|
|
564
457
|
try {
|
|
458
|
+
let url;
|
|
459
|
+
try {
|
|
460
|
+
url = new URL(req.url || "/", `http://${req.headers.host || "127.0.0.1"}`);
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
req.resume();
|
|
464
|
+
throw new HttpError(400, "Invalid request URL or Host header");
|
|
465
|
+
}
|
|
466
|
+
const path = url.pathname;
|
|
467
|
+
const guard = rejectBrowserRequest(req, allowedHosts);
|
|
468
|
+
if (guard) {
|
|
469
|
+
req.resume();
|
|
470
|
+
return jsonResponse(res, guard.status, { ok: false, error: guard.message });
|
|
471
|
+
}
|
|
472
|
+
// Health and the tool list stay reachable so adapters can tell "busy" from "down".
|
|
473
|
+
const exempt = method === "GET" && (path === "/health" || path === "/api/tools");
|
|
474
|
+
if (!exempt) {
|
|
475
|
+
const wait = addressLimiter.take(`address:${req.socket.remoteAddress ?? "unknown"}`);
|
|
476
|
+
if (wait) {
|
|
477
|
+
req.resume();
|
|
478
|
+
const e = rateLimited(wait, "this client address");
|
|
479
|
+
return jsonResponse(res, e.status, { ok: false, error: e.message }, e.headers);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
565
482
|
// Health
|
|
566
483
|
if (method === "GET" && path === "/health") {
|
|
567
|
-
return jsonResponse(res, 200, {
|
|
568
|
-
ok: true, version: options.version,
|
|
569
|
-
storageMode: storageInfo.storageMode,
|
|
570
|
-
isDegraded: storageInfo.isDegraded,
|
|
571
|
-
ready: storageInfo.ready
|
|
572
|
-
});
|
|
573
|
-
}
|
|
574
|
-
// Storage info
|
|
575
|
-
if (method === "GET" && path === "/api/storage/info") {
|
|
576
|
-
return jsonResponse(res, 200, storageInfo);
|
|
484
|
+
return jsonResponse(res, 200, { ok: true, version: options.version });
|
|
577
485
|
}
|
|
578
486
|
// Tool definitions
|
|
579
487
|
if (method === "GET" && path === "/api/tools") {
|
|
580
488
|
return jsonResponse(res, 200, { tools: TOOL_DEFINITIONS });
|
|
581
489
|
}
|
|
490
|
+
if (method === "POST" && path === "/api/sessions/register") {
|
|
491
|
+
const input = sessionRegistrationSchema.parse(await parseBody(req));
|
|
492
|
+
let session;
|
|
493
|
+
try {
|
|
494
|
+
session = SessionStore.register(input);
|
|
495
|
+
}
|
|
496
|
+
catch (e) {
|
|
497
|
+
throw new HttpError(400, e instanceof Error ? e.message : String(e));
|
|
498
|
+
}
|
|
499
|
+
return jsonResponse(res, 200, { ok: true, session });
|
|
500
|
+
}
|
|
501
|
+
if (method === "POST" && path === "/api/sessions/disconnect") {
|
|
502
|
+
const input = sessionDisconnectSchema.parse(await parseBody(req));
|
|
503
|
+
try {
|
|
504
|
+
SessionStore.disconnect(input.sessionId, input.instanceId);
|
|
505
|
+
}
|
|
506
|
+
catch (e) {
|
|
507
|
+
throw new HttpError(400, e instanceof Error ? e.message : String(e));
|
|
508
|
+
}
|
|
509
|
+
return jsonResponse(res, 200, { ok: true });
|
|
510
|
+
}
|
|
582
511
|
// Tool call dispatcher ← core endpoint
|
|
583
512
|
if (method === "POST" && path === "/api/tools/call") {
|
|
584
513
|
const body = await parseBody(req);
|
|
585
514
|
const toolName = body.tool;
|
|
586
515
|
const args = (body.args || {});
|
|
587
|
-
const instanceId = body
|
|
516
|
+
const instanceId = actor(body);
|
|
588
517
|
if (!toolName)
|
|
589
518
|
return jsonResponse(res, 400, { ok: false, error: "tool is required" });
|
|
590
|
-
|
|
591
|
-
|
|
519
|
+
if (!isToolName(toolName)) {
|
|
520
|
+
return jsonResponse(res, 404, { ok: false, error: `Unknown tool: '${toolName}'` });
|
|
521
|
+
}
|
|
522
|
+
return jsonResponse(res, 200, { ok: true, result: await call(toolName, args, instanceId, sessionActor(body)) });
|
|
592
523
|
}
|
|
593
524
|
// Messages
|
|
594
525
|
if (method === "POST" && path === "/api/messages/send") {
|
|
595
526
|
const body = await parseBody(req);
|
|
596
|
-
|
|
597
|
-
return jsonResponse(res, 400, { ok: false, error: "message is required" });
|
|
598
|
-
const instanceId = body.instanceId || "unknown";
|
|
599
|
-
const meetingId = body.meetingId;
|
|
600
|
-
const source = body.source;
|
|
601
|
-
// Direct meetingId routing (bypasses session currentProject check)
|
|
602
|
-
if (meetingId || source === "meeting") {
|
|
603
|
-
const targetMeetingId = meetingId || (await UnifiedMeetingStore.getActiveMeeting())?.id || null;
|
|
604
|
-
if (!targetMeetingId) {
|
|
605
|
-
return jsonResponse(res, 200, { ok: false, sentTo: "global", message: "No active meeting found." });
|
|
606
|
-
}
|
|
607
|
-
const msg = {
|
|
608
|
-
from: instanceId, text: body.message,
|
|
609
|
-
category: body.category || "CHAT",
|
|
610
|
-
timestamp: new Date().toISOString()
|
|
611
|
-
};
|
|
612
|
-
await UnifiedMeetingStore.addMessage(targetMeetingId, msg);
|
|
613
|
-
return jsonResponse(res, 200, { ok: true, sentTo: "meeting", meetingId: targetMeetingId, message: `Message sent to meeting ${targetMeetingId}.` });
|
|
614
|
-
}
|
|
615
|
-
const result = await toolSendMessage({ message: body.message, category: body.category }, instanceId);
|
|
616
|
-
return jsonResponse(res, 200, result);
|
|
527
|
+
return jsonResponse(res, 200, await call("send_message", body, actor(body), sessionActor(body)));
|
|
617
528
|
}
|
|
618
529
|
if (method === "GET" && path === "/api/messages/unread") {
|
|
619
|
-
const
|
|
620
|
-
const meetingId = url.searchParams.get("meetingId") || undefined;
|
|
621
|
-
const instanceId = url.searchParams.get("instanceId") || "unknown";
|
|
622
|
-
const result = await toolReadMessages({ count, meetingId }, instanceId);
|
|
530
|
+
const result = await call("read_messages", queryArgs(url, ["count"]), url.searchParams.get("instanceId") || UNKNOWN_INSTANCE);
|
|
623
531
|
return jsonResponse(res, 200, result);
|
|
624
532
|
}
|
|
625
533
|
// Projects
|
|
626
534
|
if (method === "POST" && path === "/api/projects/sync") {
|
|
627
535
|
const body = await parseBody(req);
|
|
628
|
-
|
|
629
|
-
return jsonResponse(res, 400, { ok: false, error: "manifest is required" });
|
|
630
|
-
const result = await toolSyncProjectAssets({ manifest: body.manifest, internalDocs: body.internalDocs || "" }, body.instanceId || "unknown");
|
|
631
|
-
return jsonResponse(res, 200, { ok: true, ...result });
|
|
536
|
+
return jsonResponse(res, 200, await call("sync_project_assets", { internalDocs: "", ...body }, actor(body)));
|
|
632
537
|
}
|
|
633
538
|
if (method === "POST" && path === "/api/projects/update") {
|
|
634
539
|
const body = await parseBody(req);
|
|
635
|
-
|
|
636
|
-
return jsonResponse(res, 400, { ok: false, error: "projectId and patch are required" });
|
|
637
|
-
const result = await toolUpdateProject({ projectId: body.projectId, patch: body.patch }, body.instanceId || "unknown");
|
|
638
|
-
return jsonResponse(res, 200, { ok: true, ...result });
|
|
540
|
+
return jsonResponse(res, 200, await call("update_project", body, actor(body)));
|
|
639
541
|
}
|
|
640
542
|
if (method === "POST" && path === "/api/projects/rename") {
|
|
641
543
|
const body = await parseBody(req);
|
|
642
|
-
|
|
643
|
-
return jsonResponse(res, 400, { ok: false, error: "oldId and newId are required" });
|
|
644
|
-
const result = await toolRenameProject({ oldId: body.oldId, newId: body.newId }, body.instanceId || "unknown");
|
|
645
|
-
return jsonResponse(res, 200, { ok: true, ...result });
|
|
544
|
+
return jsonResponse(res, 200, await call("rename_project", body, actor(body)));
|
|
646
545
|
}
|
|
647
546
|
if (method === "POST" && path === "/api/projects/delete") {
|
|
648
547
|
const body = await parseBody(req);
|
|
649
|
-
|
|
650
|
-
return jsonResponse(res, 400, { ok: false, error: "projectId is required" });
|
|
651
|
-
const result = await toolHostDeleteProject({ projectId: body.projectId }, body.instanceId || "unknown");
|
|
652
|
-
return jsonResponse(res, 200, { ok: true, ...result });
|
|
548
|
+
return jsonResponse(res, 200, await call("host_delete_project", body, actor(body)));
|
|
653
549
|
}
|
|
654
550
|
if (method === "GET" && path === "/api/projects/search") {
|
|
655
|
-
|
|
656
|
-
const limit = Number(url.searchParams.get("limit")) || 10;
|
|
657
|
-
if (!query)
|
|
658
|
-
return jsonResponse(res, 400, { ok: false, error: "query is required" });
|
|
659
|
-
const result = await toolSearchProjects({ query, limit });
|
|
660
|
-
return jsonResponse(res, 200, result);
|
|
551
|
+
return jsonResponse(res, 200, await call("search_projects", queryArgs(url, ["limit"]), UNKNOWN_INSTANCE));
|
|
661
552
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
const
|
|
665
|
-
return jsonResponse(res, 200,
|
|
553
|
+
const projectMatch = path.match(/^\/api\/projects\/([^/]+)$/);
|
|
554
|
+
if (method === "GET" && projectMatch && !["search", "topology"].includes(projectMatch[1])) {
|
|
555
|
+
const includeDocs = url.searchParams.get("includeDocs") !== "false";
|
|
556
|
+
return jsonResponse(res, 200, await call("get_project", { projectId: decodeURIComponent(projectMatch[1]), includeDocs }, UNKNOWN_INSTANCE));
|
|
666
557
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
const body = await parseBody(req);
|
|
670
|
-
if (!body.projectId)
|
|
671
|
-
return jsonResponse(res, 400, { ok: false, error: "projectId is required" });
|
|
672
|
-
const result = await toolRegisterSessionContext({ projectId: body.projectId }, body.instanceId || "unknown");
|
|
673
|
-
return jsonResponse(res, result.ok ? 200 : 404, result);
|
|
558
|
+
if (method === "GET" && path === "/api/projects/topology") {
|
|
559
|
+
return jsonResponse(res, 200, await call("get_global_topology", queryArgs(url, []), UNKNOWN_INSTANCE));
|
|
674
560
|
}
|
|
675
561
|
// Global docs
|
|
676
562
|
if (method === "GET" && path === "/api/global/docs") {
|
|
@@ -688,52 +574,41 @@ export async function createDaemonServer(options) {
|
|
|
688
574
|
}
|
|
689
575
|
if (method === "POST") {
|
|
690
576
|
const body = await parseBody(req);
|
|
691
|
-
|
|
692
|
-
return jsonResponse(res, 400, { ok: false, error: "title and content are required" });
|
|
693
|
-
const instanceId = body.instanceId || "unknown";
|
|
694
|
-
await StorageManager.saveGlobalDoc(docId, body.title, body.content, instanceId);
|
|
577
|
+
await call("sync_global_doc", { ...body, docId }, actor(body));
|
|
695
578
|
return jsonResponse(res, 200, { ok: true, docId });
|
|
696
579
|
}
|
|
697
580
|
}
|
|
581
|
+
if (method === "GET" && path === "/api/global/strategy") {
|
|
582
|
+
return jsonResponse(res, 200, { content: await StorageManager.getGlobalBlueprint() });
|
|
583
|
+
}
|
|
698
584
|
if (method === "POST" && path === "/api/global/strategy") {
|
|
699
585
|
const body = await parseBody(req);
|
|
700
|
-
|
|
701
|
-
return jsonResponse(res, 400, { ok: false, error: "content is required" });
|
|
702
|
-
const instanceId = body.instanceId || "unknown";
|
|
703
|
-
const result = await toolUpdateGlobalStrategy({ content: body.content }, instanceId);
|
|
704
|
-
return jsonResponse(res, 200, result);
|
|
586
|
+
return jsonResponse(res, 200, await call("update_global_strategy", body, actor(body)));
|
|
705
587
|
}
|
|
706
588
|
// Meetings
|
|
589
|
+
if (method === "GET" && path === "/api/meetings") {
|
|
590
|
+
return jsonResponse(res, 200, await call("list_meetings", queryArgs(url, ["limit"]), UNKNOWN_INSTANCE));
|
|
591
|
+
}
|
|
707
592
|
if (method === "POST" && path === "/api/meetings/start") {
|
|
708
593
|
const body = await parseBody(req);
|
|
709
|
-
|
|
710
|
-
return jsonResponse(res, 400, { ok: false, error: "topic is required" });
|
|
711
|
-
const result = await toolStartMeeting({ topic: body.topic }, body.instanceId || "unknown");
|
|
712
|
-
return jsonResponse(res, 200, result);
|
|
594
|
+
return jsonResponse(res, 200, await call("start_meeting", body, actor(body)));
|
|
713
595
|
}
|
|
714
596
|
if (method === "POST" && path === "/api/meetings/end") {
|
|
715
597
|
const body = await parseBody(req);
|
|
716
|
-
|
|
717
|
-
return jsonResponse(res, 200, result);
|
|
598
|
+
return jsonResponse(res, 200, await call("end_meeting", body, actor(body)));
|
|
718
599
|
}
|
|
719
600
|
if (method === "POST" && path === "/api/meetings/archive") {
|
|
720
601
|
const body = await parseBody(req);
|
|
721
|
-
|
|
722
|
-
return jsonResponse(res, 400, { ok: false, error: "meetingId is required" });
|
|
723
|
-
const result = await toolArchiveMeeting({ meetingId: body.meetingId }, body.instanceId || "unknown");
|
|
724
|
-
return jsonResponse(res, 200, result);
|
|
602
|
+
return jsonResponse(res, 200, await call("archive_meeting", body, actor(body)));
|
|
725
603
|
}
|
|
726
604
|
if (method === "POST" && path === "/api/meetings/reopen") {
|
|
727
605
|
const body = await parseBody(req);
|
|
728
|
-
|
|
729
|
-
return jsonResponse(res, 400, { ok: false, error: "meetingId is required" });
|
|
730
|
-
const result = await toolReopenMeeting({ meetingId: body.meetingId }, body.instanceId || "unknown");
|
|
731
|
-
return jsonResponse(res, 200, result);
|
|
606
|
+
return jsonResponse(res, 200, await call("reopen_meeting", body, actor(body)));
|
|
732
607
|
}
|
|
733
608
|
const meetingMatch = path.match(/^\/api\/meetings\/(.+)$/);
|
|
734
609
|
if (meetingMatch && method === "GET") {
|
|
735
610
|
const meetingId = decodeURIComponent(meetingMatch[1]);
|
|
736
|
-
const meeting =
|
|
611
|
+
const meeting = SqliteMeetingStore.getMeeting(meetingId);
|
|
737
612
|
if (!meeting)
|
|
738
613
|
return jsonResponse(res, 404, { ok: false, error: `Meeting '${meetingId}' not found.` });
|
|
739
614
|
return jsonResponse(res, 200, meeting);
|
|
@@ -741,51 +616,53 @@ export async function createDaemonServer(options) {
|
|
|
741
616
|
// Tasks
|
|
742
617
|
if (method === "POST" && path === "/api/tasks") {
|
|
743
618
|
const body = await parseBody(req);
|
|
744
|
-
|
|
745
|
-
return jsonResponse(res, 200, result);
|
|
619
|
+
return jsonResponse(res, 200, await call("create_task", body, actor(body)));
|
|
746
620
|
}
|
|
747
621
|
if (method === "GET" && path === "/api/tasks") {
|
|
748
|
-
|
|
749
|
-
const limit = Number(url.searchParams.get("limit")) || 50;
|
|
750
|
-
const result = await toolListTasks({ status: status || undefined, limit });
|
|
751
|
-
return jsonResponse(res, 200, result);
|
|
622
|
+
return jsonResponse(res, 200, await call("list_tasks", queryArgs(url, ["limit"]), UNKNOWN_INSTANCE));
|
|
752
623
|
}
|
|
753
624
|
const taskMatch = path.match(/^\/api\/tasks\/([^/]+)(\/(.+))?$/);
|
|
754
625
|
if (taskMatch) {
|
|
755
626
|
const taskId = decodeURIComponent(taskMatch[1]);
|
|
756
627
|
const action = taskMatch[3];
|
|
757
628
|
if (method === "GET" && !action) {
|
|
758
|
-
|
|
759
|
-
return jsonResponse(res, 200, result);
|
|
629
|
+
return jsonResponse(res, 200, await call("get_task", { taskId }, UNKNOWN_INSTANCE));
|
|
760
630
|
}
|
|
761
631
|
if (method === "POST" && action === "update") {
|
|
762
632
|
const body = await parseBody(req);
|
|
763
|
-
const
|
|
764
|
-
if (!
|
|
633
|
+
const update = taskUpdateSchema.safeParse(body);
|
|
634
|
+
if (!update.success)
|
|
635
|
+
return jsonResponse(res, 400, { ok: false, error: `Invalid task update:\n${z.prettifyError(update.error)}` });
|
|
636
|
+
if (!getTask(taskId))
|
|
765
637
|
return jsonResponse(res, 404, { ok: false, error: `Task '${taskId}' not found.` });
|
|
766
|
-
|
|
767
|
-
return jsonResponse(res, 200, { ok: true, task: updated });
|
|
638
|
+
return jsonResponse(res, 200, { ok: true, task: updateTask(taskId, update.data) });
|
|
768
639
|
}
|
|
769
640
|
if (method === "POST" && action === "cancel") {
|
|
770
|
-
|
|
771
|
-
return jsonResponse(res, 200, result);
|
|
641
|
+
return jsonResponse(res, 200, await call("cancel_task", { taskId }, UNKNOWN_INSTANCE));
|
|
772
642
|
}
|
|
773
643
|
}
|
|
774
644
|
// Maintenance
|
|
775
645
|
if (method === "POST" && path === "/api/maintenance/logs") {
|
|
776
646
|
const body = await parseBody(req);
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
647
|
+
return jsonResponse(res, 200, await call("host_maintenance", body, actor(body)));
|
|
648
|
+
}
|
|
649
|
+
// Events
|
|
650
|
+
if (method === "GET" && path === "/api/events") {
|
|
651
|
+
return jsonResponse(res, 200, await call("list_events", queryArgs(url, ["limit"]), UNKNOWN_INSTANCE));
|
|
781
652
|
}
|
|
782
653
|
jsonResponse(res, 404, { ok: false, error: "Not found" });
|
|
783
654
|
}
|
|
784
655
|
catch (e) {
|
|
785
656
|
const message = e instanceof Error ? e.message : String(e);
|
|
657
|
+
if (e instanceof HttpError) {
|
|
658
|
+
return jsonResponse(res, e.status, { ok: false, error: message }, e.headers);
|
|
659
|
+
}
|
|
660
|
+
if (e instanceof ToolValidationError || e instanceof z.ZodError) {
|
|
661
|
+
return jsonResponse(res, 400, { ok: false, error: e instanceof z.ZodError ? z.prettifyError(e) : message });
|
|
662
|
+
}
|
|
786
663
|
console.error("[n2n-nexus daemon] Error:", message);
|
|
787
664
|
jsonResponse(res, 500, { ok: false, error: message });
|
|
788
665
|
}
|
|
789
666
|
});
|
|
790
|
-
return { server
|
|
667
|
+
return { server };
|
|
791
668
|
}
|