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.
Files changed (41) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/README.md +77 -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 +44 -21
  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 +422 -545
  13. package/build/daemon/tool-outputs.js +177 -0
  14. package/build/daemon/tools.js +321 -0
  15. package/build/index.js +1 -1
  16. package/build/server/instance-id.js +19 -0
  17. package/build/server/nexus.js +243 -47
  18. package/build/storage/direct-messages.js +89 -0
  19. package/build/storage/docs.js +14 -7
  20. package/build/storage/events.js +31 -0
  21. package/build/storage/fs-utils.js +42 -0
  22. package/build/storage/global-messages.js +44 -0
  23. package/build/storage/index.js +47 -41
  24. package/build/storage/migrations.js +86 -0
  25. package/build/storage/paths.js +0 -3
  26. package/build/storage/projects.js +4 -5
  27. package/build/storage/registry.js +67 -25
  28. package/build/storage/sessions.js +77 -0
  29. package/build/storage/sqlite-meeting.js +35 -23
  30. package/build/storage/sqlite.js +73 -24
  31. package/build/storage/tasks.js +29 -103
  32. package/build/types.js +1 -1
  33. package/docs/ARCHITECTURE.md +36 -25
  34. package/docs/ARCHITECTURE_zh.md +37 -26
  35. package/docs/ASSISTANT_GUIDE.md +77 -65
  36. package/docs/README_zh.md +34 -19
  37. package/llms.txt +12 -2
  38. package/package.json +24 -14
  39. package/build/storage/logs.js +0 -60
  40. package/build/storage/meetings.js +0 -276
  41. package/build/storage/store.js +0 -153
@@ -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 { 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 { 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
- initTasksTable();
9
- const info = await UnifiedMeetingStore.getStorageInfo();
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
- 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." };
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
- 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.");
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(session.currentProject, sanitized, buf);
311
- return { ok: true, message: `Asset '${sanitized}' saved to project '${session.currentProject}'.` };
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
- 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." };
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 (!args.oldId || !args.newId)
335
- throw new Error("oldId and newId are required.");
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
- 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." };
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 || 10;
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 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;
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
- await UnifiedMeetingStore.addMessage(meetingId, msg);
104
+ SqliteMeetingStore.addMessage(meetingId, msg);
387
105
  return { ok: true, sentTo: "meeting", meetingId, message: `Message sent to meeting ${meetingId}.` };
388
106
  }
389
- await StorageManager.addGlobalLog(instanceId, args.message, args.category);
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 || 10;
111
+ const count = args.count;
394
112
  if (args.meetingId) {
395
- const meeting = await UnifiedMeetingStore.getMeeting(args.meetingId);
113
+ const meeting = SqliteMeetingStore.getMeeting(args.meetingId);
396
114
  if (!meeting)
397
115
  throw new Error(`Meeting '${args.meetingId}' not found.`);
398
- const messages = await UnifiedMeetingStore.getRecentMessages(count, args.meetingId, instanceId);
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 logs = await StorageManager.getRecentLogs(count);
402
- return { source: "global", messages: logs, count: logs.length };
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
- if (!args.content)
406
- throw new Error("content is required.");
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
- if (!args.topic)
418
- throw new Error("topic is required.");
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
- let id = args.meetingId;
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 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 };
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 = await UnifiedMeetingStore.getMeeting(args.meetingId);
166
+ const meeting = SqliteMeetingStore.getMeeting(args.meetingId);
439
167
  if (!meeting)
440
168
  throw new Error(`Meeting '${args.meetingId}' not found.`);
441
- await UnifiedMeetingStore.archiveMeeting(args.meetingId, instanceId);
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 = await UnifiedMeetingStore.getMeeting(args.meetingId);
174
+ const meeting = SqliteMeetingStore.getMeeting(args.meetingId);
446
175
  if (!meeting)
447
176
  throw new Error(`Meeting '${args.meetingId}' not found.`);
448
- await UnifiedMeetingStore.reopenMeeting(args.meetingId, instanceId);
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 || 50);
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
- 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.` };
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
- 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." };
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, args, instanceId) {
503
- const u = args;
226
+ async function dispatchTool(name, rawArgs, instanceId, sessionId) {
504
227
  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}'`);
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, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload) });
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
- async function parseBody(req) {
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
- let data = "";
541
- req.on("data", chunk => { data += chunk; });
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 (!data)
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
- resolve(JSON.parse(data));
373
+ parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
547
374
  }
548
375
  catch {
549
- reject(new Error("Invalid JSON"));
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
- const storageInfo = await initializeStorage();
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 url = new URL(req.url || "/", `http://${req.headers.host || "127.0.0.1"}`);
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.instanceId || "unknown";
516
+ const instanceId = actor(body);
588
517
  if (!toolName)
589
518
  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 });
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
- 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);
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 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);
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
- 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 });
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
- 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 });
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
- 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 });
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
- 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 });
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
- 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);
551
+ return jsonResponse(res, 200, await call("search_projects", queryArgs(url, ["limit"]), UNKNOWN_INSTANCE));
661
552
  }
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);
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
- // 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);
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
- 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);
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
- 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);
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
- 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);
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
- const result = await toolEndMeeting({ meetingId: body.meetingId, summary: body.summary }, body.instanceId || "unknown");
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
- 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);
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
- 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);
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 = await UnifiedMeetingStore.getMeeting(meetingId);
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
- const result = await toolCreateTask(body);
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
- 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);
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
- const result = await toolGetTask({ taskId });
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 task = getTask(taskId);
764
- if (!task)
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
- const updated = updateTask(taskId, body);
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
- const result = await toolCancelTask({ taskId });
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
- 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);
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, storageInfo };
667
+ return { server };
791
668
  }