n2n-nexus 0.4.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +114 -0
- package/README.md +77 -117
- package/assets/n2n-nexus-banner.jpg +0 -0
- package/assets/n2n-nexus-icon.png +0 -0
- package/assets/n2n-nexus-logo.png +0 -0
- package/build/client/nexus-client.js +44 -21
- package/build/config/cli.js +0 -3
- package/build/config/index.js +0 -10
- package/build/constants.js +7 -11
- package/build/daemon/index.js +35 -19
- package/build/daemon/rate-limit.js +62 -0
- package/build/daemon/server.js +422 -545
- package/build/daemon/tool-outputs.js +177 -0
- package/build/daemon/tools.js +321 -0
- package/build/index.js +1 -1
- package/build/server/instance-id.js +19 -0
- package/build/server/nexus.js +243 -47
- package/build/storage/direct-messages.js +89 -0
- package/build/storage/docs.js +14 -7
- package/build/storage/events.js +31 -0
- package/build/storage/fs-utils.js +42 -0
- package/build/storage/global-messages.js +44 -0
- package/build/storage/index.js +47 -41
- package/build/storage/migrations.js +86 -0
- package/build/storage/paths.js +0 -3
- package/build/storage/projects.js +4 -5
- package/build/storage/registry.js +67 -25
- package/build/storage/sessions.js +77 -0
- package/build/storage/sqlite-meeting.js +35 -23
- package/build/storage/sqlite.js +73 -24
- package/build/storage/tasks.js +29 -103
- package/build/types.js +1 -1
- package/docs/ARCHITECTURE.md +36 -25
- package/docs/ARCHITECTURE_zh.md +37 -26
- package/docs/ASSISTANT_GUIDE.md +77 -65
- package/docs/README_zh.md +34 -19
- package/llms.txt +12 -2
- package/package.json +24 -14
- package/build/storage/logs.js +0 -60
- package/build/storage/meetings.js +0 -276
- package/build/storage/store.js +0 -153
package/build/storage/sqlite.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import Database from "better-sqlite3";
|
|
2
|
-
import { createRequire } from "module";
|
|
3
2
|
import { NexusPaths } from "./paths.js";
|
|
4
3
|
let db = null;
|
|
5
4
|
/**
|
|
@@ -50,6 +49,46 @@ CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON messages(timestamp);
|
|
|
50
49
|
CREATE INDEX IF NOT EXISTS idx_decisions_meeting ON decisions(meeting_id);
|
|
51
50
|
CREATE INDEX IF NOT EXISTS idx_meetings_status ON meetings(status);
|
|
52
51
|
|
|
52
|
+
-- Global (non-meeting) chat stream
|
|
53
|
+
CREATE TABLE IF NOT EXISTS global_messages (
|
|
54
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
55
|
+
sender TEXT NOT NULL,
|
|
56
|
+
text TEXT NOT NULL,
|
|
57
|
+
category TEXT CHECK(category IN ('MEETING_START', 'PROPOSAL', 'DECISION', 'UPDATE', 'CHAT')),
|
|
58
|
+
timestamp TEXT NOT NULL
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
-- Audit trail of state changes made through the daemon
|
|
62
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
63
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
64
|
+
timestamp TEXT NOT NULL,
|
|
65
|
+
actor TEXT NOT NULL,
|
|
66
|
+
action TEXT NOT NULL,
|
|
67
|
+
target TEXT,
|
|
68
|
+
detail TEXT
|
|
69
|
+
);
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_events_action ON events(action);
|
|
71
|
+
|
|
72
|
+
-- Tasks managed by assistants
|
|
73
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
74
|
+
id TEXT PRIMARY KEY,
|
|
75
|
+
status TEXT NOT NULL CHECK(status IN ('pending', 'running', 'completed', 'failed', 'cancelled')) DEFAULT 'pending',
|
|
76
|
+
progress REAL DEFAULT 0.0 CHECK(progress >= 0.0 AND progress <= 1.0),
|
|
77
|
+
source_meeting_id TEXT,
|
|
78
|
+
metadata TEXT,
|
|
79
|
+
result_uri TEXT,
|
|
80
|
+
error_message TEXT,
|
|
81
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
82
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
83
|
+
ttl INTEGER,
|
|
84
|
+
FOREIGN KEY (source_meeting_id) REFERENCES meetings(id) ON DELETE SET NULL
|
|
85
|
+
);
|
|
86
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
|
87
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_meeting ON tasks(source_meeting_id);
|
|
88
|
+
-- updateTask sets updated_at itself in ISO 8601; the old trigger overwrote it
|
|
89
|
+
-- with datetime('now') in a different format, breaking timestamp comparisons.
|
|
90
|
+
DROP TRIGGER IF EXISTS tasks_updated_at;
|
|
91
|
+
|
|
53
92
|
-- Read cursors table (tracks each IDE's last read message per context)
|
|
54
93
|
CREATE TABLE IF NOT EXISTS read_cursors (
|
|
55
94
|
instance_id TEXT NOT NULL,
|
|
@@ -59,6 +98,38 @@ CREATE TABLE IF NOT EXISTS read_cursors (
|
|
|
59
98
|
updated_at TEXT,
|
|
60
99
|
PRIMARY KEY (instance_id, context_type, context_id)
|
|
61
100
|
);
|
|
101
|
+
|
|
102
|
+
-- Live MCP connections have separate identities even within one IDE/project.
|
|
103
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
104
|
+
session_id TEXT PRIMARY KEY,
|
|
105
|
+
instance_id TEXT NOT NULL,
|
|
106
|
+
name TEXT NOT NULL,
|
|
107
|
+
client_name TEXT NOT NULL,
|
|
108
|
+
workspace TEXT NOT NULL,
|
|
109
|
+
project_id TEXT,
|
|
110
|
+
inbound TEXT NOT NULL CHECK(inbound IN ('accept', 'refuse')) DEFAULT 'accept',
|
|
111
|
+
connected INTEGER NOT NULL DEFAULT 1,
|
|
112
|
+
created_at TEXT NOT NULL,
|
|
113
|
+
last_seen_at TEXT NOT NULL
|
|
114
|
+
);
|
|
115
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
|
|
116
|
+
|
|
117
|
+
CREATE TABLE IF NOT EXISTS direct_messages (
|
|
118
|
+
message_id TEXT PRIMARY KEY,
|
|
119
|
+
from_session_id TEXT NOT NULL REFERENCES sessions(session_id),
|
|
120
|
+
to_session_id TEXT NOT NULL REFERENCES sessions(session_id),
|
|
121
|
+
text TEXT NOT NULL,
|
|
122
|
+
category TEXT CHECK(category IN ('MEETING_START', 'PROPOSAL', 'DECISION', 'UPDATE', 'CHAT')),
|
|
123
|
+
reply_to TEXT REFERENCES direct_messages(message_id),
|
|
124
|
+
status TEXT NOT NULL CHECK(status IN ('accepted', 'fetched', 'acknowledged', 'replied', 'refused', 'expired')),
|
|
125
|
+
created_at TEXT NOT NULL,
|
|
126
|
+
expires_at TEXT NOT NULL,
|
|
127
|
+
fetched_at TEXT,
|
|
128
|
+
acknowledged_at TEXT,
|
|
129
|
+
replied_at TEXT,
|
|
130
|
+
reason TEXT
|
|
131
|
+
);
|
|
132
|
+
CREATE INDEX IF NOT EXISTS idx_direct_messages_inbox ON direct_messages(to_session_id, status);
|
|
62
133
|
`;
|
|
63
134
|
/**
|
|
64
135
|
* Get the database file path
|
|
@@ -76,6 +147,7 @@ export function initDatabase() {
|
|
|
76
147
|
db = new Database(dbPath);
|
|
77
148
|
// Enable WAL mode for better concurrent access
|
|
78
149
|
db.pragma("journal_mode = WAL");
|
|
150
|
+
db.pragma("foreign_keys = ON");
|
|
79
151
|
// Initialize schema
|
|
80
152
|
db.exec(SCHEMA);
|
|
81
153
|
// Migration: Add initiator column if it doesn't exist (Upgrade from v0.1.7)
|
|
@@ -116,26 +188,3 @@ export function closeDatabase() {
|
|
|
116
188
|
console.error("[Nexus] SQLite database closed");
|
|
117
189
|
}
|
|
118
190
|
}
|
|
119
|
-
export function isSqliteAvailable() {
|
|
120
|
-
const require = createRequire(import.meta.url);
|
|
121
|
-
try {
|
|
122
|
-
// Try to load better-sqlite3
|
|
123
|
-
require("better-sqlite3");
|
|
124
|
-
return true;
|
|
125
|
-
}
|
|
126
|
-
catch {
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
// Cleanup on process exit
|
|
131
|
-
process.on("exit", () => {
|
|
132
|
-
closeDatabase();
|
|
133
|
-
});
|
|
134
|
-
process.on("SIGINT", () => {
|
|
135
|
-
closeDatabase();
|
|
136
|
-
process.exit(0);
|
|
137
|
-
});
|
|
138
|
-
process.on("SIGTERM", () => {
|
|
139
|
-
closeDatabase();
|
|
140
|
-
process.exit(0);
|
|
141
|
-
});
|
package/build/storage/tasks.js
CHANGED
|
@@ -6,59 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { getDatabase } from "./sqlite.js";
|
|
8
8
|
import { TASK_CLEANUP_MAX_AGE_MS } from "../constants.js";
|
|
9
|
+
export const TASK_STATUSES = ["pending", "running", "completed", "failed", "cancelled"];
|
|
9
10
|
// Generate unique task ID
|
|
10
11
|
function generateTaskId() {
|
|
11
12
|
const timestamp = new Date().toISOString().replace(/[-:T.Z]/g, "").slice(0, 14);
|
|
12
13
|
const random = Math.random().toString(36).substring(2, 6);
|
|
13
14
|
return `task_${timestamp}_${random}`;
|
|
14
15
|
}
|
|
15
|
-
let initialized = false;
|
|
16
|
-
/**
|
|
17
|
-
* Initialize the tasks table (run migrations)
|
|
18
|
-
*/
|
|
19
|
-
export function initTasksTable() {
|
|
20
|
-
if (initialized)
|
|
21
|
-
return;
|
|
22
|
-
const db = getDatabase();
|
|
23
|
-
const TASKS_SCHEMA = `
|
|
24
|
-
CREATE TABLE IF NOT EXISTS tasks (
|
|
25
|
-
id TEXT PRIMARY KEY,
|
|
26
|
-
status TEXT NOT NULL CHECK(status IN ('pending', 'running', 'completed', 'failed', 'cancelled')) DEFAULT 'pending',
|
|
27
|
-
progress REAL DEFAULT 0.0 CHECK(progress >= 0.0 AND progress <= 1.0),
|
|
28
|
-
source_meeting_id TEXT,
|
|
29
|
-
metadata TEXT,
|
|
30
|
-
result_uri TEXT,
|
|
31
|
-
error_message TEXT,
|
|
32
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
33
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
34
|
-
ttl INTEGER,
|
|
35
|
-
FOREIGN KEY (source_meeting_id) REFERENCES meetings(id) ON DELETE SET NULL
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
|
39
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_meeting ON tasks(source_meeting_id);
|
|
40
|
-
`;
|
|
41
|
-
db.exec(TASKS_SCHEMA);
|
|
42
|
-
// Add trigger for auto-updating updated_at (separate exec to handle IF NOT EXISTS)
|
|
43
|
-
try {
|
|
44
|
-
db.exec(`
|
|
45
|
-
CREATE TRIGGER IF NOT EXISTS tasks_updated_at
|
|
46
|
-
AFTER UPDATE ON tasks
|
|
47
|
-
FOR EACH ROW
|
|
48
|
-
BEGIN
|
|
49
|
-
UPDATE tasks SET updated_at = datetime('now') WHERE id = OLD.id;
|
|
50
|
-
END;
|
|
51
|
-
`);
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
// Trigger may already exist in older SQLite versions without IF NOT EXISTS support
|
|
55
|
-
}
|
|
56
|
-
console.error("[Nexus] Tasks table initialized");
|
|
57
|
-
initialized = true;
|
|
58
|
-
}
|
|
59
|
-
export function resetTasksInit() {
|
|
60
|
-
initialized = false;
|
|
61
|
-
}
|
|
62
16
|
/**
|
|
63
17
|
* Create a new task
|
|
64
18
|
*/
|
|
@@ -73,14 +27,7 @@ export function createTask(input = {}) {
|
|
|
73
27
|
stmt.run(id, input.source_meeting_id || null, input.metadata ? JSON.stringify(input.metadata) : null, now, now, input.ttl || null);
|
|
74
28
|
return getTask(id);
|
|
75
29
|
}
|
|
76
|
-
|
|
77
|
-
* Get a task by ID
|
|
78
|
-
*/
|
|
79
|
-
export function getTask(id) {
|
|
80
|
-
const db = getDatabase();
|
|
81
|
-
const row = db.prepare("SELECT * FROM tasks WHERE id = ?").get(id);
|
|
82
|
-
if (!row)
|
|
83
|
-
return null;
|
|
30
|
+
function rowToTask(row) {
|
|
84
31
|
return {
|
|
85
32
|
id: row.id,
|
|
86
33
|
status: row.status,
|
|
@@ -94,6 +41,13 @@ export function getTask(id) {
|
|
|
94
41
|
ttl: row.ttl || null
|
|
95
42
|
};
|
|
96
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Get a task by ID
|
|
46
|
+
*/
|
|
47
|
+
export function getTask(id) {
|
|
48
|
+
const row = getDatabase().prepare("SELECT * FROM tasks WHERE id = ?").get(id);
|
|
49
|
+
return row ? rowToTask(row) : null;
|
|
50
|
+
}
|
|
97
51
|
/**
|
|
98
52
|
* Update task status and progress
|
|
99
53
|
*/
|
|
@@ -141,39 +95,7 @@ export function listTasks(status, limit = 50) {
|
|
|
141
95
|
sql += " ORDER BY created_at DESC LIMIT ?";
|
|
142
96
|
params.push(limit);
|
|
143
97
|
const rows = db.prepare(sql).all(...params);
|
|
144
|
-
return rows.map(
|
|
145
|
-
id: row.id,
|
|
146
|
-
status: row.status,
|
|
147
|
-
progress: row.progress,
|
|
148
|
-
source_meeting_id: row.source_meeting_id || null,
|
|
149
|
-
metadata: row.metadata ? JSON.parse(row.metadata) : {},
|
|
150
|
-
result_uri: row.result_uri || null,
|
|
151
|
-
error_message: row.error_message || null,
|
|
152
|
-
created_at: row.created_at,
|
|
153
|
-
updated_at: row.updated_at,
|
|
154
|
-
ttl: row.ttl || null
|
|
155
|
-
}));
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* Get tasks by meeting ID
|
|
159
|
-
*/
|
|
160
|
-
export function getTasksByMeeting(meetingId) {
|
|
161
|
-
const db = getDatabase();
|
|
162
|
-
const rows = db.prepare(`
|
|
163
|
-
SELECT * FROM tasks WHERE source_meeting_id = ? ORDER BY created_at DESC
|
|
164
|
-
`).all(meetingId);
|
|
165
|
-
return rows.map(row => ({
|
|
166
|
-
id: row.id,
|
|
167
|
-
status: row.status,
|
|
168
|
-
progress: row.progress,
|
|
169
|
-
source_meeting_id: row.source_meeting_id || null,
|
|
170
|
-
metadata: row.metadata ? JSON.parse(row.metadata) : {},
|
|
171
|
-
result_uri: row.result_uri || null,
|
|
172
|
-
error_message: row.error_message || null,
|
|
173
|
-
created_at: row.created_at,
|
|
174
|
-
updated_at: row.updated_at,
|
|
175
|
-
ttl: row.ttl || null
|
|
176
|
-
}));
|
|
98
|
+
return rows.map(rowToTask);
|
|
177
99
|
}
|
|
178
100
|
/**
|
|
179
101
|
* Cancel a pending or running task
|
|
@@ -187,26 +109,30 @@ export function cancelTask(id) {
|
|
|
187
109
|
updateTask(id, { status: "cancelled" });
|
|
188
110
|
return true;
|
|
189
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Mark pending/running tasks whose TTL has elapsed as failed.
|
|
114
|
+
*/
|
|
115
|
+
export function expireTasks(now = Date.now()) {
|
|
116
|
+
return getDatabase().prepare(`
|
|
117
|
+
UPDATE tasks
|
|
118
|
+
SET status = 'failed', error_message = 'TTL expired', updated_at = ?
|
|
119
|
+
WHERE status IN ('pending', 'running')
|
|
120
|
+
AND ttl IS NOT NULL
|
|
121
|
+
AND ROUND(unixepoch(created_at, 'subsec') * 1000) + ttl < ?
|
|
122
|
+
`).run(new Date(now).toISOString(), now).changes;
|
|
123
|
+
}
|
|
190
124
|
/**
|
|
191
125
|
* Delete completed/failed/cancelled tasks older than specified age
|
|
192
126
|
*/
|
|
193
|
-
export function cleanupTasks(maxAgeMs = TASK_CLEANUP_MAX_AGE_MS) {
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
const result = db.prepare(`
|
|
127
|
+
export function cleanupTasks(maxAgeMs = TASK_CLEANUP_MAX_AGE_MS, now = Date.now()) {
|
|
128
|
+
const cutoff = new Date(now - maxAgeMs).toISOString();
|
|
129
|
+
return getDatabase().prepare(`
|
|
197
130
|
DELETE FROM tasks
|
|
198
131
|
WHERE status IN ('completed', 'failed', 'cancelled')
|
|
199
132
|
AND updated_at < ?
|
|
200
|
-
`).run(cutoff);
|
|
201
|
-
return result.changes;
|
|
133
|
+
`).run(cutoff).changes;
|
|
202
134
|
}
|
|
203
|
-
/**
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
export function getActiveTaskCount() {
|
|
207
|
-
const db = getDatabase();
|
|
208
|
-
const row = db.prepare(`
|
|
209
|
-
SELECT COUNT(*) as count FROM tasks WHERE status IN ('pending', 'running')
|
|
210
|
-
`).get();
|
|
211
|
-
return row.count;
|
|
135
|
+
/** Expire overdue tasks, then purge finished ones past the retention window. */
|
|
136
|
+
export function runTaskMaintenance(now = Date.now()) {
|
|
137
|
+
return { expired: expireTasks(now), removed: cleanupTasks(TASK_CLEANUP_MAX_AGE_MS, now) };
|
|
212
138
|
}
|
package/build/types.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export const MESSAGE_CATEGORIES = ["MEETING_START", "PROPOSAL", "DECISION", "UPDATE", "CHAT"];
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -20,36 +20,39 @@
|
|
|
20
20
|
▼ ▼ ▼
|
|
21
21
|
MCP-A MCP-B MCP-C
|
|
22
22
|
(Win) (WSL) (VM)
|
|
23
|
-
|
|
23
|
+
Lightweight protocol adapter, one per MCP connection
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
### Core Principles
|
|
27
27
|
|
|
28
28
|
1. **Daemon is the single source of truth** — all reads/writes, business logic, and tool definitions live in the daemon.
|
|
29
|
-
2. **MCP
|
|
30
|
-
3. **Daemon decides tool capability** — MCP fetches `GET /api/tools` at startup
|
|
31
|
-
4. **No local fallback** — if the daemon is unreachable, the MCP reports an error and retries every 3 s. No split-brain degradation.
|
|
32
|
-
5. **Plain HTTP between MCP and daemon** —
|
|
29
|
+
2. **MCP holds connection identity only** — no hardcoded tool names or local persistent data; sessions and inboxes live in the daemon.
|
|
30
|
+
3. **Daemon decides tool capability** — MCP fetches `GET /api/tools` at startup and reconnect. Session inboxes require both the session-aware adapter and daemon; legacy clients can continue using the existing shared-message APIs.
|
|
31
|
+
4. **No local fallback** — if the daemon is unreachable, the MCP reports an error and retries every 3 s. No split-brain degradation. A tool call the daemon rejects (validation, not found, permission) is returned to the IDE as an error result (`isError: true`) and does not affect the connection; an unknown tool name is a JSON-RPC `-32602` error.
|
|
32
|
+
5. **Plain HTTP between MCP and daemon** — inboxes are pulled explicitly; heartbeats maintain presence but do not wake the model.
|
|
33
33
|
|
|
34
34
|
### MCP Startup Flow
|
|
35
35
|
|
|
36
36
|
```
|
|
37
37
|
Read NEXUS_ENDPOINT (default: http://127.0.0.1:5688)
|
|
38
38
|
│
|
|
39
|
-
├─
|
|
39
|
+
├─ Serve stdio (MCP TypeScript SDK v2, serveStdio) and GET /api/tools once;
|
|
40
|
+
│ the first tools/list waits up to 1 s for that fetch
|
|
41
|
+
│ ├─ 2026-07-28 clients: server/discover, per-request _meta envelope,
|
|
42
|
+
│ │ list_changed delivered on subscriptions/listen
|
|
43
|
+
│ └─ 2025-era clients: initialize handshake, unsolicited list_changed
|
|
40
44
|
│
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
│
|
|
47
|
-
└─ GET /api/tools succeeds
|
|
48
|
-
→ cache tool list
|
|
49
|
-
→ send notifications/tools/list_changed
|
|
50
|
-
→ IDE re-fetches: tools appear
|
|
45
|
+
├─ If the daemon was down: retry loop (every 3 s)
|
|
46
|
+
│ └─ GET /api/tools succeeds → cache tool list
|
|
47
|
+
│ → notifications/tools/list_changed → IDE re-fetches
|
|
48
|
+
│
|
|
49
|
+
└─ stdin closes → stop retrying, close, exit
|
|
51
50
|
```
|
|
52
51
|
|
|
52
|
+
The adapter's instance ID (read cursors, meeting permissions) uses the first client name seen on the connection, from the request's `io.modelcontextprotocol/clientInfo` (2026-07-28) or from `initialize` (2025 era), unless `NEXUS_INSTANCE_ID` / `--id` pins it.
|
|
53
|
+
|
|
54
|
+
Each MCP connection also gets a random session ID, registered on its first tool request and refreshed every 20 seconds with a 60-second presence lease. Disconnect marks it offline; an adapter restart creates a new session, so pending messages for the old session are not transferred. Multiple host chats sharing a connection share its inbox. Project association is directory metadata, not an implicit target for project tools. Registration and disconnect use `POST /api/sessions/register` and `POST /api/sessions/disconnect`; direct tools receive session identity outside their model-supplied arguments. This remains a trusted local daemon, not an authentication boundary. See [tool reference](TOOLS_REFERENCE.md#session-directory-and-direct-inboxes).
|
|
55
|
+
|
|
53
56
|
---
|
|
54
57
|
|
|
55
58
|
## 💾 Data Persistence
|
|
@@ -60,7 +63,6 @@ All data lives under the daemon's storage root (default `~/.n2n-nexus`):
|
|
|
60
63
|
~/.n2n-nexus/
|
|
61
64
|
├── global/
|
|
62
65
|
│ ├── blueprint.md # Master strategy document
|
|
63
|
-
│ ├── discussion.json # Global chat (JSON fallback)
|
|
64
66
|
│ ├── docs_index.json # Global docs index
|
|
65
67
|
│ └── docs/ # Shared markdown documents
|
|
66
68
|
├── projects/
|
|
@@ -68,14 +70,17 @@ All data lives under the daemon's storage root (default `~/.n2n-nexus`):
|
|
|
68
70
|
│ ├── manifest.json # Project metadata
|
|
69
71
|
│ ├── internal_blueprint.md # Internal technical docs
|
|
70
72
|
│ └── assets/ # Binary assets (images, PDFs)
|
|
71
|
-
├── meetings/ # Meeting files (JSON fallback mode)
|
|
72
73
|
├── registry.json # Global project index
|
|
73
|
-
└── nexus.db # SQLite database (meetings, tasks, cursors)
|
|
74
|
+
└── nexus.db # SQLite database (meetings, global chat, events, tasks, cursors)
|
|
74
75
|
```
|
|
75
76
|
|
|
76
|
-
**SQLite WAL mode**: Only the daemon process
|
|
77
|
+
**SQLite WAL mode**: Only the daemon process opens the database. MCP adapters never touch it — they go through HTTP.
|
|
78
|
+
|
|
79
|
+
**File-store writes**: HTTP requests are handled concurrently, so every mutation of `registry.json`, manifests and the docs index is queued behind one daemon-wide lock and written atomically (temp file + rename).
|
|
77
80
|
|
|
78
|
-
**
|
|
81
|
+
**Activity log**: state changes made through the daemon (project sync/rename/delete, meeting start/end, doc sync, ...) are recorded as structured events in SQLite and read with `list_events` / `GET /api/events`; they are not mixed into chat streams.
|
|
82
|
+
|
|
83
|
+
**Self-healing**: `registry.json` is auto-repaired if corrupted or missing.
|
|
79
84
|
|
|
80
85
|
---
|
|
81
86
|
|
|
@@ -142,38 +147,40 @@ npx n2n-nexus daemon --root ~/.n2n-nexus --port 5688
|
|
|
142
147
|
### Tool capability (MCP-facing)
|
|
143
148
|
```
|
|
144
149
|
GET /api/tools Return full tool definition list (JSON Schema)
|
|
145
|
-
POST /api/tools/call Execute tool call { tool, args, instanceId }
|
|
150
|
+
POST /api/tools/call Execute tool call { tool, args, instanceId, sessionId? }
|
|
146
151
|
```
|
|
147
152
|
|
|
148
153
|
### System
|
|
149
154
|
```
|
|
150
155
|
GET /health Health status + version
|
|
151
|
-
GET /api/storage/info Storage mode and stats
|
|
152
156
|
```
|
|
153
157
|
|
|
154
|
-
###
|
|
158
|
+
### Projects
|
|
155
159
|
```
|
|
156
|
-
POST /api/session/register
|
|
157
160
|
POST /api/projects/sync
|
|
158
161
|
POST /api/projects/update
|
|
159
162
|
POST /api/projects/rename
|
|
160
163
|
POST /api/projects/delete
|
|
161
164
|
GET /api/projects/search
|
|
162
165
|
GET /api/projects/topology
|
|
166
|
+
GET /api/projects/:projectId
|
|
163
167
|
```
|
|
164
168
|
|
|
165
169
|
### Messages & Global
|
|
166
170
|
```
|
|
167
171
|
POST /api/messages/send
|
|
168
172
|
GET /api/messages/unread
|
|
173
|
+
GET /api/events
|
|
169
174
|
GET /api/global/docs
|
|
170
175
|
GET /api/global/docs/:docId
|
|
171
176
|
POST /api/global/docs/:docId
|
|
177
|
+
GET /api/global/strategy
|
|
172
178
|
POST /api/global/strategy
|
|
173
179
|
```
|
|
174
180
|
|
|
175
181
|
### Meetings
|
|
176
182
|
```
|
|
183
|
+
GET /api/meetings
|
|
177
184
|
POST /api/meetings/start
|
|
178
185
|
POST /api/meetings/end
|
|
179
186
|
POST /api/meetings/archive
|
|
@@ -197,6 +204,10 @@ POST /api/maintenance/logs
|
|
|
197
204
|
|
|
198
205
|
---
|
|
199
206
|
|
|
207
|
+
### Request guards
|
|
208
|
+
|
|
209
|
+
The daemon rejects browser-originated requests (`Origin` header or cross-site `Sec-Fetch-Site`: 403), `Host` names that are neither IP literals nor local/allow-listed names (403, prevents DNS rebinding), non-JSON POST bodies (415) and bodies above 16 MB (413). Tool calls are rate limited per instance and per client address (429 with `Retry-After`). See `SECURITY.md`.
|
|
210
|
+
|
|
200
211
|
## Scope Boundaries
|
|
201
212
|
|
|
202
213
|
- No cloud bridge by default.
|
package/docs/ARCHITECTURE_zh.md
CHANGED
|
@@ -20,36 +20,39 @@
|
|
|
20
20
|
▼ ▼ ▼
|
|
21
21
|
MCP-A MCP-B MCP-C
|
|
22
22
|
(Win) (WSL) (VM)
|
|
23
|
-
|
|
23
|
+
轻量协议适配器,每条 MCP 连接一个
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
### 核心原则
|
|
27
27
|
|
|
28
28
|
1. **Daemon 是唯一的真相来源** — 所有读写、业务逻辑和工具定义都在 daemon 侧。
|
|
29
|
-
2. **MCP
|
|
30
|
-
3. **Daemon 决定工具能力** — MCP
|
|
31
|
-
4. **无本地降级** — daemon 不可达时,MCP 每 3 秒重试并报告错误,不做本地 fallback(避免数据分裂)。
|
|
32
|
-
5. **MCP ↔ Daemon 全部走普通 HTTP** —
|
|
29
|
+
2. **MCP 只维护连接身份** — 不含 hardcode 工具名、不存本地持久化数据;会话目录和收件箱在 daemon。
|
|
30
|
+
3. **Daemon 决定工具能力** — MCP 启动和重连时拉取 `GET /api/tools`。个人收件箱需要适配器和 daemon 都支持会话身份;旧客户端仍可使用原有共享消息接口。
|
|
31
|
+
4. **无本地降级** — daemon 不可达时,MCP 每 3 秒重试并报告错误,不做本地 fallback(避免数据分裂)。daemon 拒绝的工具调用(校验失败、不存在、无权限)作为错误结果(`isError: true`)返回给 IDE,不影响连接;未知工具名返回 JSON-RPC `-32602` 错误。
|
|
32
|
+
5. **MCP ↔ Daemon 全部走普通 HTTP** — 收件箱由助手主动拉取;心跳只维护在线状态,不唤醒模型。
|
|
33
33
|
|
|
34
34
|
### MCP 启动流程
|
|
35
35
|
|
|
36
36
|
```
|
|
37
37
|
读取 NEXUS_ENDPOINT(默认 http://127.0.0.1:5688)
|
|
38
38
|
│
|
|
39
|
-
├─
|
|
39
|
+
├─ 开始 stdio 服务(MCP TypeScript SDK v2,serveStdio),同时拉一次 GET /api/tools;
|
|
40
|
+
│ 第一次 tools/list 最多等这次拉取 1 秒
|
|
41
|
+
│ ├─ 2026-07-28 客户端:server/discover、每请求 _meta 信封,
|
|
42
|
+
│ │ list_changed 经 subscriptions/listen 下发
|
|
43
|
+
│ └─ 2025 代客户端:initialize 握手,主动下发 list_changed
|
|
40
44
|
│
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
│
|
|
47
|
-
└─ GET /api/tools 成功
|
|
48
|
-
→ 缓存工具列表
|
|
49
|
-
→ 发送 notifications/tools/list_changed
|
|
50
|
-
→ IDE 重新拉取,工具出现
|
|
45
|
+
├─ 若 daemon 不在线:重试循环(每 3 秒)
|
|
46
|
+
│ └─ GET /api/tools 成功 → 缓存工具列表
|
|
47
|
+
│ → notifications/tools/list_changed → IDE 重新拉取
|
|
48
|
+
│
|
|
49
|
+
└─ stdin 关闭 → 停止重试、关闭、退出
|
|
51
50
|
```
|
|
52
51
|
|
|
52
|
+
适配器的实例 ID(未读游标、会议权限以它为键)取自该连接上第一次见到的客户端名,来源是请求 `_meta` 中的 `io.modelcontextprotocol/clientInfo`(2026-07-28)或 `initialize`(2025 代),`NEXUS_INSTANCE_ID` / `--id` 可固定。
|
|
53
|
+
|
|
54
|
+
每条 MCP 连接还会生成随机会话 ID,在首次工具请求时注册,每 20 秒刷新一次心跳,在线租约为 60 秒。断连后标记离线;适配器重启会创建新会话,旧会话的待收消息不会转移。宿主多个聊天共用连接时也共用收件箱。项目关联仅用于目录筛选,不作为项目工具的隐式操作目标。注册和断连分别使用 `POST /api/sessions/register` 与 `POST /api/sessions/disconnect`,工具请求的会话身份放在模型参数之外。系统仍采用可信本地 daemon 边界,这不是身份认证机制。精确契约见[工具参考](TOOLS_REFERENCE.md#session-directory-and-direct-inboxes)。
|
|
55
|
+
|
|
53
56
|
---
|
|
54
57
|
|
|
55
58
|
## 💾 数据持久化
|
|
@@ -60,7 +63,6 @@ MCP-A MCP-B MCP-C
|
|
|
60
63
|
~/.n2n-nexus/
|
|
61
64
|
├── global/
|
|
62
65
|
│ ├── blueprint.md # 主战略文档
|
|
63
|
-
│ ├── discussion.json # 全局聊天(JSON 回退)
|
|
64
66
|
│ ├── docs_index.json # 全局文档索引
|
|
65
67
|
│ └── docs/ # 共享 Markdown 文档
|
|
66
68
|
├── projects/
|
|
@@ -68,14 +70,17 @@ MCP-A MCP-B MCP-C
|
|
|
68
70
|
│ ├── manifest.json # 项目元数据
|
|
69
71
|
│ ├── internal_blueprint.md # 内部技术文档
|
|
70
72
|
│ └── assets/ # 二进制资产(图片、PDF)
|
|
71
|
-
├── meetings/ # 会议文件(JSON 回退模式)
|
|
72
73
|
├── registry.json # 全局项目索引
|
|
73
|
-
└── nexus.db # SQLite
|
|
74
|
+
└── nexus.db # SQLite 数据库(会议、全局聊天、事件、任务、游标)
|
|
74
75
|
```
|
|
75
76
|
|
|
76
|
-
**SQLite WAL 模式**:只有 daemon
|
|
77
|
+
**SQLite WAL 模式**:只有 daemon 进程打开数据库。MCP 适配器不直接访问,全部经由 HTTP。
|
|
78
|
+
|
|
79
|
+
**文件存储写入**:HTTP 请求是并发处理的,因此对 `registry.json`、manifest 和文档索引的每次修改都排在一把 daemon 级写锁后面,并以原子方式写入(临时文件 + rename)。
|
|
77
80
|
|
|
78
|
-
|
|
81
|
+
**活动日志**:经由 daemon 发生的状态变更(项目同步/重命名/删除、会议开始/结束、文档同步等)以结构化事件记录在 SQLite 中,通过 `list_events` / `GET /api/events` 读取,不混入聊天流。
|
|
82
|
+
|
|
83
|
+
**自我修复**:`registry.json` 在损坏或丢失时自动重建初始状态。
|
|
79
84
|
|
|
80
85
|
---
|
|
81
86
|
|
|
@@ -110,7 +115,7 @@ n2n-nexus
|
|
|
110
115
|
│
|
|
111
116
|
├─ n2n-nexus daemon 用户手动启动一次,持续运行,持有所有数据。
|
|
112
117
|
│
|
|
113
|
-
└─ n2n-nexus mcp IDE 通过 npx
|
|
118
|
+
└─ n2n-nexus mcp IDE 通过 npx 自动启动,维护连接身份。
|
|
114
119
|
```
|
|
115
120
|
|
|
116
121
|
**Daemon**(用户手动启动):
|
|
@@ -142,38 +147,40 @@ npx n2n-nexus daemon --root ~/.n2n-nexus --port 5688
|
|
|
142
147
|
### 工具能力(MCP 对接)
|
|
143
148
|
```
|
|
144
149
|
GET /api/tools 返回完整工具定义列表(JSON Schema)
|
|
145
|
-
POST /api/tools/call 执行工具调用 { tool, args, instanceId }
|
|
150
|
+
POST /api/tools/call 执行工具调用 { tool, args, instanceId, sessionId? }
|
|
146
151
|
```
|
|
147
152
|
|
|
148
153
|
### 系统
|
|
149
154
|
```
|
|
150
155
|
GET /health 健康状态 + 版本信息
|
|
151
|
-
GET /api/storage/info 存储模式和统计
|
|
152
156
|
```
|
|
153
157
|
|
|
154
|
-
###
|
|
158
|
+
### 项目
|
|
155
159
|
```
|
|
156
|
-
POST /api/session/register
|
|
157
160
|
POST /api/projects/sync
|
|
158
161
|
POST /api/projects/update
|
|
159
162
|
POST /api/projects/rename
|
|
160
163
|
POST /api/projects/delete
|
|
161
164
|
GET /api/projects/search
|
|
162
165
|
GET /api/projects/topology
|
|
166
|
+
GET /api/projects/:projectId
|
|
163
167
|
```
|
|
164
168
|
|
|
165
169
|
### 消息与全局协作
|
|
166
170
|
```
|
|
167
171
|
POST /api/messages/send
|
|
168
172
|
GET /api/messages/unread
|
|
173
|
+
GET /api/events
|
|
169
174
|
GET /api/global/docs
|
|
170
175
|
GET /api/global/docs/:docId
|
|
171
176
|
POST /api/global/docs/:docId
|
|
177
|
+
GET /api/global/strategy
|
|
172
178
|
POST /api/global/strategy
|
|
173
179
|
```
|
|
174
180
|
|
|
175
181
|
### 会议
|
|
176
182
|
```
|
|
183
|
+
GET /api/meetings
|
|
177
184
|
POST /api/meetings/start
|
|
178
185
|
POST /api/meetings/end
|
|
179
186
|
POST /api/meetings/archive
|
|
@@ -197,6 +204,10 @@ POST /api/maintenance/logs
|
|
|
197
204
|
|
|
198
205
|
---
|
|
199
206
|
|
|
207
|
+
### 请求防护
|
|
208
|
+
|
|
209
|
+
daemon 拒绝来自浏览器的请求(带 `Origin` 头或跨站 `Sec-Fetch-Site`:403)、既非 IP 也不在本地/允许列表中的 `Host`(403,防 DNS rebinding)、非 JSON 的 POST 请求体(415)以及超过 16 MB 的请求体(413)。工具调用按实例和客户端地址限流(429,带 `Retry-After`)。详见 `SECURITY.md`。
|
|
210
|
+
|
|
200
211
|
## 边界说明
|
|
201
212
|
|
|
202
213
|
- 不默认提供云端桥接。
|