n2n-nexus 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +358 -0
- package/LICENSE +201 -0
- package/README.md +286 -0
- package/build/client/nexus-client.js +71 -0
- package/build/config/cli.js +11 -0
- package/build/config/index.js +16 -0
- package/build/config/paths.js +38 -0
- package/build/constants.js +22 -0
- package/build/daemon/index.js +41 -0
- package/build/daemon/server.js +791 -0
- package/build/index.js +47 -0
- package/build/server/nexus.js +98 -0
- package/build/storage/docs.js +74 -0
- package/build/storage/index.js +105 -0
- package/build/storage/logs.js +60 -0
- package/build/storage/meetings.js +276 -0
- package/build/storage/paths.js +26 -0
- package/build/storage/projects.js +75 -0
- package/build/storage/registry.js +230 -0
- package/build/storage/sqlite-meeting.js +311 -0
- package/build/storage/sqlite.js +141 -0
- package/build/storage/store.js +153 -0
- package/build/storage/tasks.js +212 -0
- package/build/types.js +1 -0
- package/build/utils/async-mutex.js +36 -0
- package/docs/ARCHITECTURE.md +205 -0
- package/docs/ARCHITECTURE_zh.md +205 -0
- package/docs/ASSISTANT_GUIDE.md +120 -0
- package/docs/README_zh.md +285 -0
- package/llms.txt +46 -0
- package/package.json +90 -0
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import { URL } from "node:url";
|
|
3
|
+
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";
|
|
6
|
+
async function initializeStorage() {
|
|
7
|
+
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);
|
|
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
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
// Tool handlers
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
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
|
+
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." };
|
|
301
|
+
}
|
|
302
|
+
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.");
|
|
308
|
+
const sanitized = args.fileName.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
309
|
+
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}'.` };
|
|
312
|
+
}
|
|
313
|
+
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." };
|
|
332
|
+
}
|
|
333
|
+
async function toolRenameProject(args, instanceId) {
|
|
334
|
+
if (!args.oldId || !args.newId)
|
|
335
|
+
throw new Error("oldId and newId are required.");
|
|
336
|
+
const exists = await StorageManager.getProjectManifest(args.oldId);
|
|
337
|
+
if (!exists)
|
|
338
|
+
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." };
|
|
354
|
+
}
|
|
355
|
+
async function toolSearchProjects(args) {
|
|
356
|
+
if (!args.query)
|
|
357
|
+
throw new Error("query is required.");
|
|
358
|
+
const registry = await StorageManager.listRegistry();
|
|
359
|
+
const projects = registry.projects;
|
|
360
|
+
const q = args.query.toLowerCase();
|
|
361
|
+
const limit = args.limit || 10;
|
|
362
|
+
const results = Object.entries(projects)
|
|
363
|
+
.filter(([id, p]) => id.toLowerCase().includes(q) ||
|
|
364
|
+
(p.name && p.name.toLowerCase().includes(q)) ||
|
|
365
|
+
(p.summary && p.summary.toLowerCase().includes(q)))
|
|
366
|
+
.slice(0, limit)
|
|
367
|
+
.map(([id, p]) => ({ id, name: p.name || id, description: p.summary }));
|
|
368
|
+
return { query: args.query, count: results.length, results };
|
|
369
|
+
}
|
|
370
|
+
async function toolGetGlobalTopology(args) {
|
|
371
|
+
return StorageManager.calculateTopology(args.projectId);
|
|
372
|
+
}
|
|
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;
|
|
379
|
+
if (meetingId) {
|
|
380
|
+
const msg = {
|
|
381
|
+
from: instanceId,
|
|
382
|
+
text: args.message,
|
|
383
|
+
category: args.category,
|
|
384
|
+
timestamp: new Date().toISOString()
|
|
385
|
+
};
|
|
386
|
+
await UnifiedMeetingStore.addMessage(meetingId, msg);
|
|
387
|
+
return { ok: true, sentTo: "meeting", meetingId, message: `Message sent to meeting ${meetingId}.` };
|
|
388
|
+
}
|
|
389
|
+
await StorageManager.addGlobalLog(instanceId, args.message, args.category);
|
|
390
|
+
return { ok: true, sentTo: "global", message: "Message sent to global chat." };
|
|
391
|
+
}
|
|
392
|
+
async function toolReadMessages(args, instanceId) {
|
|
393
|
+
const count = args.count || 10;
|
|
394
|
+
if (args.meetingId) {
|
|
395
|
+
const meeting = await UnifiedMeetingStore.getMeeting(args.meetingId);
|
|
396
|
+
if (!meeting)
|
|
397
|
+
throw new Error(`Meeting '${args.meetingId}' not found.`);
|
|
398
|
+
const messages = await UnifiedMeetingStore.getRecentMessages(count, args.meetingId, instanceId);
|
|
399
|
+
return { source: "meeting", meetingId: args.meetingId, messages, count: messages.length };
|
|
400
|
+
}
|
|
401
|
+
const logs = await StorageManager.getRecentLogs(count);
|
|
402
|
+
return { source: "global", messages: logs, count: logs.length };
|
|
403
|
+
}
|
|
404
|
+
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);
|
|
408
|
+
return { ok: true, message: "Global strategy updated." };
|
|
409
|
+
}
|
|
410
|
+
async function toolSyncGlobalDoc(args, instanceId) {
|
|
411
|
+
if (!args.docId || !args.title || !args.content)
|
|
412
|
+
throw new Error("docId, title, and content are required.");
|
|
413
|
+
await StorageManager.saveGlobalDoc(args.docId, args.title, args.content, instanceId);
|
|
414
|
+
return { ok: true, message: `Global document '${args.title}' (${args.docId}) synchronized.` };
|
|
415
|
+
}
|
|
416
|
+
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);
|
|
420
|
+
return { ok: true, meetingId: meeting.id, topic: meeting.topic, status: meeting.status };
|
|
421
|
+
}
|
|
422
|
+
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
|
+
}
|
|
429
|
+
if (!id)
|
|
430
|
+
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 };
|
|
436
|
+
}
|
|
437
|
+
async function toolArchiveMeeting(args, instanceId) {
|
|
438
|
+
const meeting = await UnifiedMeetingStore.getMeeting(args.meetingId);
|
|
439
|
+
if (!meeting)
|
|
440
|
+
throw new Error(`Meeting '${args.meetingId}' not found.`);
|
|
441
|
+
await UnifiedMeetingStore.archiveMeeting(args.meetingId, instanceId);
|
|
442
|
+
return { ok: true, meetingId: args.meetingId, status: "archived" };
|
|
443
|
+
}
|
|
444
|
+
async function toolReopenMeeting(args, instanceId) {
|
|
445
|
+
const meeting = await UnifiedMeetingStore.getMeeting(args.meetingId);
|
|
446
|
+
if (!meeting)
|
|
447
|
+
throw new Error(`Meeting '${args.meetingId}' not found.`);
|
|
448
|
+
await UnifiedMeetingStore.reopenMeeting(args.meetingId, instanceId);
|
|
449
|
+
return { ok: true, meetingId: args.meetingId, status: "active" };
|
|
450
|
+
}
|
|
451
|
+
async function toolCreateTask(args) {
|
|
452
|
+
const task = createTask({ source_meeting_id: args.source_meeting_id, metadata: args.metadata, ttl: args.ttl });
|
|
453
|
+
return { task_id: task.id, status: task.status, message: "Task created.", task };
|
|
454
|
+
}
|
|
455
|
+
async function toolGetTask(args) {
|
|
456
|
+
const task = getTask(args.taskId);
|
|
457
|
+
if (!task)
|
|
458
|
+
throw new Error(`Task '${args.taskId}' not found.`);
|
|
459
|
+
return task;
|
|
460
|
+
}
|
|
461
|
+
async function toolListTasks(args) {
|
|
462
|
+
const tasks = listTasks(args.status, args.limit || 50);
|
|
463
|
+
return { count: tasks.length, tasks };
|
|
464
|
+
}
|
|
465
|
+
async function toolCancelTask(args) {
|
|
466
|
+
const ok = cancelTask(args.taskId);
|
|
467
|
+
if (!ok)
|
|
468
|
+
throw new Error(`Task '${args.taskId}' not found or cannot be cancelled.`);
|
|
469
|
+
return { ok: true, taskId: args.taskId, message: "Task cancelled." };
|
|
470
|
+
}
|
|
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.` };
|
|
478
|
+
}
|
|
479
|
+
async function toolHostDeleteProject(args, instanceId) {
|
|
480
|
+
const exists = await StorageManager.getProjectManifest(args.projectId);
|
|
481
|
+
if (!exists)
|
|
482
|
+
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." };
|
|
498
|
+
}
|
|
499
|
+
// ---------------------------------------------------------------------------
|
|
500
|
+
// Tool dispatcher
|
|
501
|
+
// ---------------------------------------------------------------------------
|
|
502
|
+
async function dispatchTool(name, args, instanceId) {
|
|
503
|
+
const u = args;
|
|
504
|
+
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}'`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
// ---------------------------------------------------------------------------
|
|
531
|
+
// HTTP helpers
|
|
532
|
+
// ---------------------------------------------------------------------------
|
|
533
|
+
function jsonResponse(res, status, body) {
|
|
534
|
+
const payload = JSON.stringify(body);
|
|
535
|
+
res.writeHead(status, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload) });
|
|
536
|
+
res.end(payload);
|
|
537
|
+
}
|
|
538
|
+
async function parseBody(req) {
|
|
539
|
+
return new Promise((resolve, reject) => {
|
|
540
|
+
let data = "";
|
|
541
|
+
req.on("data", chunk => { data += chunk; });
|
|
542
|
+
req.on("end", () => {
|
|
543
|
+
if (!data)
|
|
544
|
+
return resolve({});
|
|
545
|
+
try {
|
|
546
|
+
resolve(JSON.parse(data));
|
|
547
|
+
}
|
|
548
|
+
catch {
|
|
549
|
+
reject(new Error("Invalid JSON"));
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
req.on("error", reject);
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
// ---------------------------------------------------------------------------
|
|
556
|
+
// Server factory
|
|
557
|
+
// ---------------------------------------------------------------------------
|
|
558
|
+
export async function createDaemonServer(options) {
|
|
559
|
+
const storageInfo = await initializeStorage();
|
|
560
|
+
const server = http.createServer(async (req, res) => {
|
|
561
|
+
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;
|
|
564
|
+
try {
|
|
565
|
+
// Health
|
|
566
|
+
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);
|
|
577
|
+
}
|
|
578
|
+
// Tool definitions
|
|
579
|
+
if (method === "GET" && path === "/api/tools") {
|
|
580
|
+
return jsonResponse(res, 200, { tools: TOOL_DEFINITIONS });
|
|
581
|
+
}
|
|
582
|
+
// Tool call dispatcher ← core endpoint
|
|
583
|
+
if (method === "POST" && path === "/api/tools/call") {
|
|
584
|
+
const body = await parseBody(req);
|
|
585
|
+
const toolName = body.tool;
|
|
586
|
+
const args = (body.args || {});
|
|
587
|
+
const instanceId = body.instanceId || "unknown";
|
|
588
|
+
if (!toolName)
|
|
589
|
+
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 });
|
|
592
|
+
}
|
|
593
|
+
// Messages
|
|
594
|
+
if (method === "POST" && path === "/api/messages/send") {
|
|
595
|
+
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);
|
|
617
|
+
}
|
|
618
|
+
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);
|
|
623
|
+
return jsonResponse(res, 200, result);
|
|
624
|
+
}
|
|
625
|
+
// Projects
|
|
626
|
+
if (method === "POST" && path === "/api/projects/sync") {
|
|
627
|
+
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 });
|
|
632
|
+
}
|
|
633
|
+
if (method === "POST" && path === "/api/projects/update") {
|
|
634
|
+
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 });
|
|
639
|
+
}
|
|
640
|
+
if (method === "POST" && path === "/api/projects/rename") {
|
|
641
|
+
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 });
|
|
646
|
+
}
|
|
647
|
+
if (method === "POST" && path === "/api/projects/delete") {
|
|
648
|
+
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 });
|
|
653
|
+
}
|
|
654
|
+
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);
|
|
661
|
+
}
|
|
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);
|
|
666
|
+
}
|
|
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);
|
|
674
|
+
}
|
|
675
|
+
// Global docs
|
|
676
|
+
if (method === "GET" && path === "/api/global/docs") {
|
|
677
|
+
const index = await StorageManager.listGlobalDocs();
|
|
678
|
+
return jsonResponse(res, 200, { docs: index });
|
|
679
|
+
}
|
|
680
|
+
const docMatch = path.match(/^\/api\/global\/docs\/(.+)$/);
|
|
681
|
+
if (docMatch) {
|
|
682
|
+
const docId = decodeURIComponent(docMatch[1]);
|
|
683
|
+
if (method === "GET") {
|
|
684
|
+
const doc = await StorageManager.getGlobalDoc(docId);
|
|
685
|
+
if (!doc)
|
|
686
|
+
return jsonResponse(res, 404, { ok: false, error: `Document '${docId}' not found.` });
|
|
687
|
+
return jsonResponse(res, 200, doc);
|
|
688
|
+
}
|
|
689
|
+
if (method === "POST") {
|
|
690
|
+
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);
|
|
695
|
+
return jsonResponse(res, 200, { ok: true, docId });
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (method === "POST" && path === "/api/global/strategy") {
|
|
699
|
+
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);
|
|
705
|
+
}
|
|
706
|
+
// Meetings
|
|
707
|
+
if (method === "POST" && path === "/api/meetings/start") {
|
|
708
|
+
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);
|
|
713
|
+
}
|
|
714
|
+
if (method === "POST" && path === "/api/meetings/end") {
|
|
715
|
+
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);
|
|
718
|
+
}
|
|
719
|
+
if (method === "POST" && path === "/api/meetings/archive") {
|
|
720
|
+
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);
|
|
725
|
+
}
|
|
726
|
+
if (method === "POST" && path === "/api/meetings/reopen") {
|
|
727
|
+
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);
|
|
732
|
+
}
|
|
733
|
+
const meetingMatch = path.match(/^\/api\/meetings\/(.+)$/);
|
|
734
|
+
if (meetingMatch && method === "GET") {
|
|
735
|
+
const meetingId = decodeURIComponent(meetingMatch[1]);
|
|
736
|
+
const meeting = await UnifiedMeetingStore.getMeeting(meetingId);
|
|
737
|
+
if (!meeting)
|
|
738
|
+
return jsonResponse(res, 404, { ok: false, error: `Meeting '${meetingId}' not found.` });
|
|
739
|
+
return jsonResponse(res, 200, meeting);
|
|
740
|
+
}
|
|
741
|
+
// Tasks
|
|
742
|
+
if (method === "POST" && path === "/api/tasks") {
|
|
743
|
+
const body = await parseBody(req);
|
|
744
|
+
const result = await toolCreateTask(body);
|
|
745
|
+
return jsonResponse(res, 200, result);
|
|
746
|
+
}
|
|
747
|
+
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);
|
|
752
|
+
}
|
|
753
|
+
const taskMatch = path.match(/^\/api\/tasks\/([^/]+)(\/(.+))?$/);
|
|
754
|
+
if (taskMatch) {
|
|
755
|
+
const taskId = decodeURIComponent(taskMatch[1]);
|
|
756
|
+
const action = taskMatch[3];
|
|
757
|
+
if (method === "GET" && !action) {
|
|
758
|
+
const result = await toolGetTask({ taskId });
|
|
759
|
+
return jsonResponse(res, 200, result);
|
|
760
|
+
}
|
|
761
|
+
if (method === "POST" && action === "update") {
|
|
762
|
+
const body = await parseBody(req);
|
|
763
|
+
const task = getTask(taskId);
|
|
764
|
+
if (!task)
|
|
765
|
+
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 });
|
|
768
|
+
}
|
|
769
|
+
if (method === "POST" && action === "cancel") {
|
|
770
|
+
const result = await toolCancelTask({ taskId });
|
|
771
|
+
return jsonResponse(res, 200, result);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
// Maintenance
|
|
775
|
+
if (method === "POST" && path === "/api/maintenance/logs") {
|
|
776
|
+
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);
|
|
781
|
+
}
|
|
782
|
+
jsonResponse(res, 404, { ok: false, error: "Not found" });
|
|
783
|
+
}
|
|
784
|
+
catch (e) {
|
|
785
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
786
|
+
console.error("[n2n-nexus daemon] Error:", message);
|
|
787
|
+
jsonResponse(res, 500, { ok: false, error: message });
|
|
788
|
+
}
|
|
789
|
+
});
|
|
790
|
+
return { server, storageInfo };
|
|
791
|
+
}
|