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