n2n-nexus 0.4.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +74 -117
  3. package/assets/n2n-nexus-banner.jpg +0 -0
  4. package/assets/n2n-nexus-icon.png +0 -0
  5. package/assets/n2n-nexus-logo.png +0 -0
  6. package/build/client/nexus-client.js +30 -14
  7. package/build/config/cli.js +0 -3
  8. package/build/config/index.js +0 -10
  9. package/build/constants.js +7 -11
  10. package/build/daemon/index.js +35 -19
  11. package/build/daemon/rate-limit.js +62 -0
  12. package/build/daemon/server.js +349 -542
  13. package/build/daemon/tool-outputs.js +155 -0
  14. package/build/daemon/tools.js +271 -0
  15. package/build/index.js +1 -1
  16. package/build/server/instance-id.js +19 -0
  17. package/build/server/nexus.js +152 -43
  18. package/build/storage/docs.js +14 -7
  19. package/build/storage/events.js +31 -0
  20. package/build/storage/fs-utils.js +42 -0
  21. package/build/storage/global-messages.js +44 -0
  22. package/build/storage/index.js +44 -41
  23. package/build/storage/migrations.js +79 -0
  24. package/build/storage/paths.js +0 -3
  25. package/build/storage/projects.js +4 -5
  26. package/build/storage/registry.js +67 -25
  27. package/build/storage/sqlite-meeting.js +35 -23
  28. package/build/storage/sqlite.js +41 -24
  29. package/build/storage/tasks.js +29 -103
  30. package/build/types.js +1 -1
  31. package/docs/ARCHITECTURE.md +29 -20
  32. package/docs/ARCHITECTURE_zh.md +29 -20
  33. package/docs/ASSISTANT_GUIDE.md +70 -67
  34. package/docs/README_zh.md +25 -15
  35. package/llms.txt +11 -1
  36. package/package.json +24 -14
  37. package/build/storage/logs.js +0 -60
  38. package/build/storage/meetings.js +0 -276
  39. package/build/storage/store.js +0 -153
@@ -1,60 +0,0 @@
1
- import { promises as fs } from "node:fs";
2
- import { NexusPaths } from "./paths.js";
3
- export class LogStorage {
4
- static async ensureLogFile() {
5
- try {
6
- await fs.access(NexusPaths.globalDiscussion);
7
- }
8
- catch {
9
- await fs.writeFile(NexusPaths.globalDiscussion, "[]", "utf-8");
10
- }
11
- }
12
- static async addLog(from, text, category) {
13
- await this.ensureLogFile();
14
- const logs = await this.getLogs();
15
- const payload = {
16
- timestamp: new Date().toISOString(),
17
- from,
18
- text,
19
- category: this.normalizeCategory(category)
20
- };
21
- logs.push(payload);
22
- await fs.writeFile(NexusPaths.globalDiscussion, JSON.stringify(logs, null, 2), "utf-8");
23
- }
24
- static normalizeCategory(category) {
25
- if (!category)
26
- return undefined;
27
- const allowed = ["MEETING_START", "PROPOSAL", "DECISION", "UPDATE", "CHAT", "message", "SYSTEM"];
28
- return allowed.includes(category) ? category : "UPDATE";
29
- }
30
- static async getLogs(limit = 10) {
31
- await this.ensureLogFile();
32
- const rows = await this.readRawLogs();
33
- if (limit <= 0)
34
- return rows;
35
- return rows.slice(-limit);
36
- }
37
- static async pruneLogs(count) {
38
- const current = await this.readRawLogs();
39
- if (count <= 0)
40
- return;
41
- if (current.length <= count)
42
- return;
43
- const remaining = current.slice(count);
44
- await fs.writeFile(NexusPaths.globalDiscussion, JSON.stringify(remaining, null, 2), "utf-8");
45
- }
46
- static async clearLogs() {
47
- await fs.writeFile(NexusPaths.globalDiscussion, "[]", "utf-8");
48
- }
49
- static async readRawLogs() {
50
- await this.ensureLogFile();
51
- try {
52
- const raw = await fs.readFile(NexusPaths.globalDiscussion, "utf-8");
53
- const parsed = JSON.parse(raw);
54
- return Array.isArray(parsed) ? parsed : [];
55
- }
56
- catch {
57
- return [];
58
- }
59
- }
60
- }
@@ -1,276 +0,0 @@
1
- import { promises as fs } from "fs";
2
- import path from "path";
3
- import { NexusPaths } from "./paths.js";
4
- import { AsyncMutex } from "../utils/async-mutex.js";
5
- import { FILE_ENCODING } from "../constants.js";
6
- /**
7
- * MeetingStore - Handles all meeting-related storage operations
8
- */
9
- export class MeetingStore {
10
- static meetingLock = new AsyncMutex();
11
- static stateLock = new AsyncMutex();
12
- // --- Path Definitions ---
13
- static get meetingsDir() { return path.join(NexusPaths.root, "meetings"); }
14
- static get stateFile() { return path.join(NexusPaths.root, "global", "meeting_state.json"); }
15
- /**
16
- * Initialize meeting storage directories
17
- */
18
- static async init() {
19
- await fs.mkdir(this.meetingsDir, { recursive: true });
20
- await this.loadStateSafe();
21
- }
22
- /**
23
- * Check if a path exists
24
- */
25
- static async exists(p) {
26
- try {
27
- await fs.access(p);
28
- return true;
29
- }
30
- catch {
31
- return false;
32
- }
33
- }
34
- /**
35
- * Load meeting state with self-healing
36
- */
37
- static async loadStateSafe() {
38
- const defaultState = { activeMeetings: [], defaultMeetingId: null };
39
- try {
40
- if (!await this.exists(this.stateFile)) {
41
- await fs.writeFile(this.stateFile, JSON.stringify(defaultState, null, 2), FILE_ENCODING);
42
- return defaultState;
43
- }
44
- const content = await fs.readFile(this.stateFile, FILE_ENCODING);
45
- const cleanContent = content.replace(/^\uFEFF/, '').trim();
46
- if (!cleanContent)
47
- throw new Error("Empty file");
48
- return JSON.parse(cleanContent);
49
- }
50
- catch (e) {
51
- console.warn(`[MeetingStore] Repairing corrupted state file. Error: ${e.message}`);
52
- await fs.writeFile(this.stateFile, JSON.stringify(defaultState, null, 2), FILE_ENCODING);
53
- return defaultState;
54
- }
55
- }
56
- /**
57
- * Get current meeting state
58
- */
59
- static async getState() {
60
- return this.loadStateSafe();
61
- }
62
- /**
63
- * Save meeting state
64
- */
65
- static async saveState(state) {
66
- await fs.writeFile(this.stateFile, JSON.stringify(state, null, 2), FILE_ENCODING);
67
- }
68
- /**
69
- * Generate a unique meeting ID
70
- */
71
- static generateMeetingId(topic) {
72
- const now = new Date();
73
- const timestamp = now.toISOString().replace(/[-:T]/g, '').substring(0, 14);
74
- // Create slug from topic, fallback to base64 hash for non-ASCII
75
- let slug = topic
76
- .toLowerCase()
77
- .replace(/[^a-z0-9]+/g, '-')
78
- .replace(/^-|-$/g, '')
79
- .substring(0, 30);
80
- // If slug is empty (e.g., Chinese topic), use base64 of topic
81
- if (!slug) {
82
- slug = Buffer.from(topic).toString('base64').replace(/[^a-zA-Z0-9]/g, '').substring(0, 8).toLowerCase();
83
- }
84
- // Add random suffix for uniqueness (prevents collision in same second)
85
- const suffix = Math.random().toString(36).substring(2, 6);
86
- return `${timestamp}-${slug || 'meeting'}-${suffix}`;
87
- }
88
- /**
89
- * Get the file path for a meeting
90
- */
91
- static getMeetingPath(id) {
92
- return path.join(this.meetingsDir, `${id}.json`);
93
- }
94
- /**
95
- * Start a new meeting
96
- */
97
- static async startMeeting(topic, initiator) {
98
- await this.init();
99
- return this.stateLock.withLock(async () => {
100
- const id = this.generateMeetingId(topic);
101
- const meeting = {
102
- id,
103
- topic,
104
- status: "active",
105
- startTime: new Date().toISOString(),
106
- initiator,
107
- participants: [initiator],
108
- messages: [],
109
- decisions: []
110
- };
111
- // Save meeting file
112
- await fs.writeFile(this.getMeetingPath(id), JSON.stringify(meeting, null, 2), FILE_ENCODING);
113
- // Update state
114
- const state = await this.loadStateSafe();
115
- state.activeMeetings.push(id);
116
- state.defaultMeetingId = id;
117
- await this.saveState(state);
118
- return meeting;
119
- });
120
- }
121
- /**
122
- * Get a meeting by ID
123
- */
124
- static async getMeeting(id) {
125
- const meetingPath = this.getMeetingPath(id);
126
- if (!await this.exists(meetingPath))
127
- return null;
128
- const content = await fs.readFile(meetingPath, FILE_ENCODING);
129
- return JSON.parse(content);
130
- }
131
- /**
132
- * Add a message to a meeting
133
- */
134
- static async addMessage(meetingId, message) {
135
- await this.meetingLock.withLock(async () => {
136
- const meeting = await this.getMeeting(meetingId);
137
- if (!meeting)
138
- throw new Error(`Meeting '${meetingId}' not found.`);
139
- if (meeting.status !== "active")
140
- throw new Error(`Meeting '${meetingId}' is ${meeting.status}, cannot add messages.`);
141
- // Add message
142
- meeting.messages.push(message);
143
- // Track participant
144
- if (!meeting.participants.includes(message.from)) {
145
- meeting.participants.push(message.from);
146
- }
147
- // Extract decisions
148
- if (message.category === "DECISION") {
149
- meeting.decisions.push(message.text);
150
- }
151
- await fs.writeFile(this.getMeetingPath(meetingId), JSON.stringify(meeting, null, 2), FILE_ENCODING);
152
- });
153
- }
154
- /**
155
- * End a meeting (close it)
156
- */
157
- static async endMeeting(meetingId, summary, callerId) {
158
- return this.stateLock.withLock(async () => {
159
- const meeting = await this.getMeeting(meetingId);
160
- if (!meeting)
161
- throw new Error(`Meeting '${meetingId}' not found.`);
162
- if (meeting.status !== "active")
163
- throw new Error(`Meeting '${meetingId}' is already ${meeting.status}.`);
164
- // Permission check: Only initiator can end
165
- if (callerId && meeting.initiator && meeting.initiator !== callerId) {
166
- throw new Error(`Permission denied: Only initiator (${meeting.initiator}) can end this meeting.`);
167
- }
168
- // Close the meeting
169
- meeting.status = "closed";
170
- meeting.endTime = new Date().toISOString();
171
- if (summary)
172
- meeting.summary = summary;
173
- await fs.writeFile(this.getMeetingPath(meetingId), JSON.stringify(meeting, null, 2), FILE_ENCODING);
174
- // Update state - remove from active meetings
175
- const state = await this.loadStateSafe();
176
- state.activeMeetings = state.activeMeetings.filter(id => id !== meetingId);
177
- state.defaultMeetingId = state.activeMeetings.length > 0
178
- ? state.activeMeetings[state.activeMeetings.length - 1]
179
- : null;
180
- await this.saveState(state);
181
- // Suggest sync targets based on participants (extract project IDs)
182
- const suggestedSyncTargets = meeting.participants
183
- .map(p => p.split('@')[1])
184
- .filter((v, i, a) => v && v !== "Global" && a.indexOf(v) === i);
185
- return { meeting, suggestedSyncTargets };
186
- });
187
- }
188
- /**
189
- * Archive a closed meeting
190
- */
191
- static async archiveMeeting(meetingId, callerId) {
192
- const meeting = await this.getMeeting(meetingId);
193
- if (!meeting)
194
- throw new Error(`Meeting '${meetingId}' not found.`);
195
- if (meeting.status === "active")
196
- throw new Error(`Meeting '${meetingId}' is still active. End it first.`);
197
- // Permission check: Only initiator can archive
198
- if (callerId && meeting.initiator && meeting.initiator !== callerId) {
199
- throw new Error(`Permission denied: Only initiator (${meeting.initiator}) can archive this meeting.`);
200
- }
201
- meeting.status = "archived";
202
- await fs.writeFile(this.getMeetingPath(meetingId), JSON.stringify(meeting, null, 2), FILE_ENCODING);
203
- }
204
- /**
205
- * Reopen a closed or archived meeting
206
- */
207
- static async reopenMeeting(meetingId, _callerId) {
208
- return this.stateLock.withLock(async () => {
209
- const meeting = await this.getMeeting(meetingId);
210
- if (!meeting)
211
- throw new Error(`Meeting '${meetingId}' not found.`);
212
- if (meeting.status === "active")
213
- throw new Error(`Meeting '${meetingId}' is already active.`);
214
- // Update status to active
215
- meeting.status = "active";
216
- meeting.endTime = undefined;
217
- await fs.writeFile(this.getMeetingPath(meetingId), JSON.stringify(meeting, null, 2), FILE_ENCODING);
218
- // Update state
219
- const state = await this.loadStateSafe();
220
- if (!state.activeMeetings.includes(meetingId)) {
221
- state.activeMeetings.push(meetingId);
222
- }
223
- state.defaultMeetingId = meetingId;
224
- await this.saveState(state);
225
- return meeting;
226
- });
227
- }
228
- /**
229
- * List all meetings with optional status filter
230
- */
231
- static async listMeetings(status) {
232
- await this.init();
233
- const files = await fs.readdir(this.meetingsDir);
234
- const meetings = [];
235
- for (const file of files) {
236
- if (!file.endsWith('.json'))
237
- continue;
238
- const id = file.replace('.json', '');
239
- const meeting = await this.getMeeting(id);
240
- if (!meeting)
241
- continue;
242
- if (status && meeting.status !== status)
243
- continue;
244
- meetings.push({
245
- id: meeting.id,
246
- topic: meeting.topic,
247
- status: meeting.status,
248
- startTime: meeting.startTime,
249
- participantCount: meeting.participants.length
250
- });
251
- }
252
- // Sort by startTime descending
253
- return meetings.sort((a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime());
254
- }
255
- /**
256
- * Get the current active meeting (for auto-routing)
257
- */
258
- static async getActiveMeeting() {
259
- const state = await this.getState();
260
- if (!state.defaultMeetingId)
261
- return null;
262
- return this.getMeeting(state.defaultMeetingId);
263
- }
264
- /**
265
- * Get recent messages from the active meeting
266
- */
267
- static async getRecentMessages(count = 10, meetingId) {
268
- const targetId = meetingId || (await this.getState()).defaultMeetingId;
269
- if (!targetId)
270
- return [];
271
- const meeting = await this.getMeeting(targetId);
272
- if (!meeting)
273
- return [];
274
- return meeting.messages.slice(-count);
275
- }
276
- }
@@ -1,153 +0,0 @@
1
- /**
2
- * Meeting Store Entry Point
3
- *
4
- * Provides a unified interface that automatically selects between:
5
- * - SQLite backend (preferred, for concurrent access safety)
6
- * - JSON backend (fallback, for environments without native module support)
7
- */
8
- // Lazy-loaded store implementation
9
- let storeType = null;
10
- let SqliteStore = null;
11
- let JsonStore = null;
12
- /**
13
- * Detect and initialize the appropriate store
14
- */
15
- async function getStore() {
16
- if (storeType === "sqlite" && SqliteStore) {
17
- return { type: "sqlite", store: SqliteStore };
18
- }
19
- if (storeType === "json" && JsonStore) {
20
- return { type: "json", store: JsonStore };
21
- }
22
- // Try SQLite first
23
- try {
24
- // Dynamic import to avoid bundling issues
25
- const sqliteModule = await import("./sqlite-meeting.js");
26
- SqliteStore = sqliteModule.SqliteMeetingStore;
27
- SqliteStore.init();
28
- storeType = "sqlite";
29
- console.error("[Nexus MeetingStore] Using SQLite backend");
30
- return { type: "sqlite", store: SqliteStore };
31
- }
32
- catch (e) {
33
- console.error("[Nexus MeetingStore] SQLite unavailable:", e.message);
34
- console.error("[Nexus MeetingStore] Falling back to JSON backend");
35
- console.warn("[Nexus MeetingStore] ⚠️ JSON mode is single-process only. For multi-IDE environments, install better-sqlite3.");
36
- // Fall back to JSON
37
- const jsonModule = await import("./meetings.js");
38
- JsonStore = jsonModule.MeetingStore;
39
- storeType = "json";
40
- return { type: "json", store: JsonStore };
41
- }
42
- }
43
- /**
44
- * Unified Meeting Store Interface
45
- */
46
- export const UnifiedMeetingStore = {
47
- /**
48
- * Start a new meeting
49
- */
50
- async startMeeting(topic, initiator) {
51
- const { store } = await getStore();
52
- return store.startMeeting(topic, initiator);
53
- },
54
- /**
55
- * Get a meeting by ID
56
- */
57
- async getMeeting(id) {
58
- const { store } = await getStore();
59
- return store.getMeeting(id);
60
- },
61
- /**
62
- * Add a message to a meeting
63
- */
64
- async addMessage(meetingId, message) {
65
- const { store } = await getStore();
66
- return store.addMessage(meetingId, message);
67
- },
68
- /**
69
- * End a meeting
70
- */
71
- async endMeeting(meetingId, summary, callerId) {
72
- const { store } = await getStore();
73
- return store.endMeeting(meetingId, summary, callerId);
74
- },
75
- /**
76
- * Archive a meeting
77
- */
78
- async archiveMeeting(meetingId, callerId) {
79
- const { store } = await getStore();
80
- return store.archiveMeeting(meetingId, callerId);
81
- },
82
- /**
83
- * Reopen a meeting
84
- */
85
- async reopenMeeting(meetingId, callerId) {
86
- const { store } = await getStore();
87
- return store.reopenMeeting(meetingId, callerId);
88
- },
89
- /**
90
- * List meetings
91
- */
92
- async listMeetings(status) {
93
- const { store } = await getStore();
94
- return store.listMeetings(status);
95
- },
96
- /**
97
- * Get the current active meeting
98
- */
99
- async getActiveMeeting() {
100
- const { store } = await getStore();
101
- return store.getActiveMeeting();
102
- },
103
- /**
104
- * Get recent messages (incremental for SQLite with instanceId)
105
- */
106
- async getRecentMessages(count, meetingId, instanceId) {
107
- const { type, store } = await getStore();
108
- if (type === "sqlite") {
109
- // SQLite supports incremental reads with cursor tracking
110
- return store.getRecentMessages(count || 10, meetingId, instanceId);
111
- }
112
- else {
113
- // JSON fallback - no incremental support
114
- return store.getRecentMessages(count || 10, meetingId);
115
- }
116
- },
117
- /**
118
- * Get the current backend type
119
- */
120
- async getBackendType() {
121
- const { type } = await getStore();
122
- return type;
123
- },
124
- /**
125
- * Get meeting state (SQLite only, returns empty for JSON)
126
- */
127
- async getState() {
128
- const { type, store } = await getStore();
129
- if (type === "sqlite") {
130
- const sqliteStore = store;
131
- const activeMeetings = JSON.parse(sqliteStore.getState("active_meetings") || "[]");
132
- const defaultMeetingId = sqliteStore.getState("default_meeting") || null;
133
- return { activeMeetings, defaultMeetingId };
134
- }
135
- else {
136
- // JSON backend
137
- const jsonStore = store;
138
- const state = await jsonStore.getState();
139
- return state;
140
- }
141
- },
142
- /**
143
- * Get storage info for status display
144
- * @returns storage_mode and is_degraded flag
145
- */
146
- async getStorageInfo() {
147
- const { type } = await getStore();
148
- return {
149
- storage_mode: type,
150
- is_degraded: type === "json" // JSON mode is considered degraded
151
- };
152
- }
153
- };