@datafrog-io/n2n-nexus 0.3.2 → 0.3.4
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/README.md +14 -4
- package/build/config.js +72 -31
- package/build/index.js +11 -1
- package/build/storage/index.js +7 -0
- package/build/storage/tasks.js +7 -0
- package/docs/CHANGELOG_zh.md +18 -0
- package/docs/README_zh.md +14 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -115,15 +115,25 @@ Add to your MCP config file (e.g., `claude_desktop_config.json` or Cursor MCP se
|
|
|
115
115
|
|
|
116
116
|
> **Zero-Config**: No `--id` or `--host` needed. Just run and collaborate!
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
```json
|
|
120
|
-
"args": ["-y", "@datafrog-io/n2n-nexus", "--root", "/path/to/storage"]
|
|
118
|
+
"args": ["-y", "@datafrog-io/n2n-nexus"]
|
|
121
119
|
```
|
|
122
120
|
|
|
121
|
+
### 💾 Data Persistence (Zero-Config)
|
|
122
|
+
|
|
123
|
+
Nexus now automatically stores data in your system's standard **User Data Directory** (XDG Base Directory). This ensures your meeting history and projects persist across IDE restarts, `npx` cache clears, and updates.
|
|
124
|
+
|
|
125
|
+
- **Linux / WSL**: `~/.local/share/n2n-nexus`
|
|
126
|
+
- **Windows**: `%APPDATA%\n2n-nexus`
|
|
127
|
+
- **macOS**: `~/Library/Application Support/n2n-nexus`
|
|
128
|
+
|
|
129
|
+
> **Note for WSL Users**: To maximize I/O performance, WSL instances store data in the Linux file system (`~/.local/share`), while Windows instances use `%APPDATA%`. Data is **isolated** between environments to prevent database corruption and performance degradation.
|
|
130
|
+
|
|
123
131
|
### CLI Arguments
|
|
124
132
|
| Argument | Description | Default |
|
|
125
133
|
|----------|-------------|---------|
|
|
126
|
-
|
|
|
134
|
+
| Argument | Description | Default |
|
|
135
|
+
|----------|-------------|---------|
|
|
136
|
+
| `--root` | Override storage path (advanced use only) | System User Data Dir |
|
|
127
137
|
|
|
128
138
|
> **Note:** Host identity and Instance ID are determined automatically based on the project folder name and startup order.
|
|
129
139
|
|
package/build/config.js
CHANGED
|
@@ -15,7 +15,7 @@ const getArg = (k) => {
|
|
|
15
15
|
const hasFlag = (k) => args.includes(k) || args.includes(k.charAt(1) === "-" ? k : k.substring(0, 2));
|
|
16
16
|
// --- CLI Commands Handlers ---
|
|
17
17
|
if (hasFlag("--help") || hasFlag("-h")) {
|
|
18
|
-
console.
|
|
18
|
+
console.error(`
|
|
19
19
|
n2ns Nexus 🚀 - Local Digital Asset Hub (MCP Server) v${pkg.version}
|
|
20
20
|
|
|
21
21
|
USAGE:
|
|
@@ -46,13 +46,13 @@ ENVIRONMENT VARIABLES:
|
|
|
46
46
|
process.exit(0);
|
|
47
47
|
}
|
|
48
48
|
if (hasFlag("--version") || hasFlag("-v")) {
|
|
49
|
-
console.
|
|
49
|
+
console.error(pkg.version);
|
|
50
50
|
process.exit(0);
|
|
51
51
|
}
|
|
52
52
|
// --- Path Normalization Logic ---
|
|
53
53
|
function normalizeRootPath(inputPath) {
|
|
54
|
-
// 1. Priority: CLI --root > ENV NEXUS_ROOT > Default
|
|
55
|
-
let root = inputPath || process.env.NEXUS_ROOT ||
|
|
54
|
+
// 1. Priority: CLI --root > ENV NEXUS_ROOT > System Default (XDG/AppData)
|
|
55
|
+
let root = inputPath || process.env.NEXUS_ROOT || getDefaultDataDir();
|
|
56
56
|
// 2. Resolve ~ to home directory
|
|
57
57
|
if (root.startsWith("~")) {
|
|
58
58
|
root = path.join(os.homedir(), root.slice(1));
|
|
@@ -65,18 +65,48 @@ function normalizeRootPath(inputPath) {
|
|
|
65
65
|
}
|
|
66
66
|
return path.resolve(root);
|
|
67
67
|
}
|
|
68
|
+
function getDefaultDataDir() {
|
|
69
|
+
const home = os.homedir();
|
|
70
|
+
const appName = "n2n-nexus";
|
|
71
|
+
switch (process.platform) {
|
|
72
|
+
case "win32":
|
|
73
|
+
return path.join(process.env.APPDATA || path.join(home, "AppData", "Roaming"), appName);
|
|
74
|
+
case "darwin":
|
|
75
|
+
return path.join(home, "Library", "Application Support", appName);
|
|
76
|
+
default: // linux, wsl, etc.
|
|
77
|
+
return path.join(process.env.XDG_DATA_HOME || path.join(home, ".local", "share"), appName);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
68
80
|
/**
|
|
69
81
|
* Probe a port to see if it's a Nexus Host
|
|
70
82
|
*/
|
|
71
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Probe a port to see if it's a Nexus Host using the Custom Handshake Protocol
|
|
85
|
+
*/
|
|
86
|
+
async function probeHost(port, myId) {
|
|
72
87
|
return new Promise((resolve) => {
|
|
73
|
-
const
|
|
88
|
+
const postData = JSON.stringify({
|
|
89
|
+
clientVersion: pkg.version,
|
|
90
|
+
instanceId: myId
|
|
91
|
+
});
|
|
92
|
+
const req = http.request({
|
|
93
|
+
hostname: "127.0.0.1",
|
|
94
|
+
port: port,
|
|
95
|
+
path: "/nexus/handshake",
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers: {
|
|
98
|
+
"Content-Type": "application/json",
|
|
99
|
+
"Content-Length": Buffer.byteLength(postData)
|
|
100
|
+
},
|
|
101
|
+
timeout: 500
|
|
102
|
+
}, (res) => {
|
|
74
103
|
let data = "";
|
|
75
104
|
res.on("data", (chunk) => data += chunk);
|
|
76
105
|
res.on("end", () => {
|
|
77
106
|
try {
|
|
78
107
|
const info = JSON.parse(data);
|
|
79
108
|
if (info.service === "n2n-nexus" && info.role === "host") {
|
|
109
|
+
// console.error(`[Nexus Handshake] Connected to Host v${info.serverVersion} (Protocol ${info.protocol})`);
|
|
80
110
|
resolve({ isNexus: true, rootStorage: info.rootStorage });
|
|
81
111
|
}
|
|
82
112
|
else {
|
|
@@ -93,6 +123,8 @@ async function probeHost(port) {
|
|
|
93
123
|
req.destroy();
|
|
94
124
|
resolve({ isNexus: false });
|
|
95
125
|
});
|
|
126
|
+
req.write(postData);
|
|
127
|
+
req.end();
|
|
96
128
|
});
|
|
97
129
|
}
|
|
98
130
|
/**
|
|
@@ -112,11 +144,12 @@ async function isHostAutoElection(root) {
|
|
|
112
144
|
while (true) {
|
|
113
145
|
// Phase 1: Probe-First - Check if any Host already exists (Concurrent Batch Scan)
|
|
114
146
|
const BATCH_SIZE = 20;
|
|
147
|
+
const myId = getArg("--id") || `candidate-${Math.random().toString(36).substring(2, 6)}`;
|
|
115
148
|
for (let batchStart = startPort; batchStart <= endPort; batchStart += BATCH_SIZE) {
|
|
116
149
|
const batchEnd = Math.min(batchStart + BATCH_SIZE - 1, endPort);
|
|
117
150
|
const promises = [];
|
|
118
151
|
for (let port = batchStart; port <= batchEnd; port++) {
|
|
119
|
-
promises.push(probeHost(port).then(res => ({ port, ...res })));
|
|
152
|
+
promises.push(probeHost(port, myId).then(res => ({ port, ...res })));
|
|
120
153
|
}
|
|
121
154
|
const results = await Promise.all(promises);
|
|
122
155
|
const found = results.find(r => r.isNexus);
|
|
@@ -128,28 +161,35 @@ async function isHostAutoElection(root) {
|
|
|
128
161
|
for (let port = startPort; port <= endPort; port++) {
|
|
129
162
|
const result = await new Promise((resolve) => {
|
|
130
163
|
const server = http.createServer((req, res) => {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
164
|
+
// HANDSHAKE ENDPOINT
|
|
165
|
+
if (req.method === "POST" && req.url === "/nexus/handshake") {
|
|
166
|
+
let body = "";
|
|
167
|
+
req.on("data", chunk => body += chunk);
|
|
168
|
+
req.on("end", () => {
|
|
169
|
+
try {
|
|
170
|
+
const _clientInfo = JSON.parse(body);
|
|
171
|
+
// console.error(`[Nexus Handshake] Client connected: ${_clientInfo.instanceId} (v${_clientInfo.clientVersion})`);
|
|
172
|
+
}
|
|
173
|
+
catch { /* ignore malformed */ }
|
|
174
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
175
|
+
res.end(JSON.stringify({
|
|
176
|
+
service: "n2n-nexus",
|
|
177
|
+
protocol: "v1", // Nexus Handshake Protocol v1
|
|
178
|
+
role: "host",
|
|
179
|
+
serverVersion: pkg.version,
|
|
180
|
+
rootStorage: root,
|
|
181
|
+
status: "ready"
|
|
182
|
+
}));
|
|
183
|
+
});
|
|
139
184
|
return;
|
|
140
185
|
}
|
|
141
186
|
res.writeHead(404);
|
|
142
187
|
res.end();
|
|
143
188
|
});
|
|
144
|
-
server.on("error", (
|
|
145
|
-
|
|
146
|
-
resolve({ isHost: false });
|
|
147
|
-
}
|
|
148
|
-
else {
|
|
149
|
-
resolve({ isHost: false });
|
|
150
|
-
}
|
|
189
|
+
server.on("error", (_err) => {
|
|
190
|
+
resolve({ isHost: false });
|
|
151
191
|
});
|
|
152
|
-
server.listen(port, "
|
|
192
|
+
server.listen(port, "0.0.0.0", () => {
|
|
153
193
|
resolve({ isHost: true, server });
|
|
154
194
|
});
|
|
155
195
|
});
|
|
@@ -157,18 +197,16 @@ async function isHostAutoElection(root) {
|
|
|
157
197
|
return { isHost: true, port, server: result.server };
|
|
158
198
|
}
|
|
159
199
|
// Phase 3: Bind failed - another Guest won. Wait then join winner.
|
|
160
|
-
await new Promise(r => setTimeout(r,
|
|
161
|
-
const probe = await probeHost(port);
|
|
200
|
+
await new Promise(r => setTimeout(r, 2000)); // Short wait for winner to stabilize
|
|
201
|
+
const probe = await probeHost(port, myId);
|
|
162
202
|
if (probe.isNexus) {
|
|
163
203
|
return { isHost: false, port, rootStorage: probe.rootStorage };
|
|
164
204
|
}
|
|
165
|
-
// If still not Nexus, try next port
|
|
205
|
+
// If still not Nexus, try next port
|
|
166
206
|
}
|
|
167
207
|
// Fallback: All ports occupied - progressive backoff retry
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const intervalStr = retryCount < 5 ? "1 minute" : "2 minutes";
|
|
171
|
-
console.error(`[Nexus] All ports ${startPort}-${endPort} occupied. Retry #${retryCount + 1} in ${intervalStr}...`);
|
|
208
|
+
const waitTime = retryCount < 5 ? 5000 : 30000;
|
|
209
|
+
console.error(`[Nexus] All ports ${startPort}-${endPort} occupied. Retry #${retryCount + 1} in ${waitTime / 1000}s...`);
|
|
172
210
|
await new Promise(r => setTimeout(r, waitTime));
|
|
173
211
|
retryCount++;
|
|
174
212
|
}
|
|
@@ -186,7 +224,10 @@ function getAutoProjectName() {
|
|
|
186
224
|
}
|
|
187
225
|
}
|
|
188
226
|
catch { /* ignore */ }
|
|
189
|
-
|
|
227
|
+
const base = path.basename(process.cwd()) || "Assistant";
|
|
228
|
+
// Append random suffix to prevent collisions when multiple IDEs open empty/same folders
|
|
229
|
+
const suffix = Math.random().toString(36).substring(2, 6);
|
|
230
|
+
return `${base}-${suffix}`;
|
|
190
231
|
}
|
|
191
232
|
const rootPath = normalizeRootPath(getArg("--root"));
|
|
192
233
|
const election = await isHostAutoElection(rootPath);
|
package/build/index.js
CHANGED
|
@@ -141,6 +141,16 @@ class NexusServer {
|
|
|
141
141
|
catch { /* ignore */ }
|
|
142
142
|
process.exit(0);
|
|
143
143
|
};
|
|
144
|
+
// Global Error Handlers to prevent process exit on background errors
|
|
145
|
+
process.on("uncaughtException", (err) => {
|
|
146
|
+
console.error("[Nexus CRITICAL] Uncaught Exception:", err);
|
|
147
|
+
// Attempt to log to disk if possible, but keep process alive if safe
|
|
148
|
+
// For a Hub, staying alive is often preferred over crashing
|
|
149
|
+
});
|
|
150
|
+
process.on("unhandledRejection", (reason, promise) => {
|
|
151
|
+
console.error("[Nexus WARNING] Unhandled Rejection at:", promise, "reason:", reason);
|
|
152
|
+
// Do not exit. Background tasks (like file sync) often trigger this.
|
|
153
|
+
});
|
|
144
154
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
145
155
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
146
156
|
if (CONFIG.isHost && hostServer) {
|
|
@@ -262,7 +272,7 @@ class NexusServer {
|
|
|
262
272
|
try {
|
|
263
273
|
process.stdout.write(dataLine.substring(6) + "\n");
|
|
264
274
|
}
|
|
265
|
-
catch { }
|
|
275
|
+
catch { /* ignore stdout errors */ }
|
|
266
276
|
}
|
|
267
277
|
}
|
|
268
278
|
});
|
package/build/storage/index.js
CHANGED
|
@@ -13,7 +13,10 @@ export class StorageManager {
|
|
|
13
13
|
static get projectsRoot() { return path.join(CONFIG.rootStorage, "projects"); }
|
|
14
14
|
static get registryFile() { return path.join(CONFIG.rootStorage, "registry.json"); }
|
|
15
15
|
static get archivesDir() { return path.join(CONFIG.rootStorage, "archives"); }
|
|
16
|
+
static initialized = false;
|
|
16
17
|
static async init() {
|
|
18
|
+
if (this.initialized)
|
|
19
|
+
return;
|
|
17
20
|
await fs.mkdir(CONFIG.rootStorage, { recursive: true });
|
|
18
21
|
await fs.mkdir(this.globalDir, { recursive: true });
|
|
19
22
|
await fs.mkdir(this.projectsRoot, { recursive: true });
|
|
@@ -32,6 +35,10 @@ export class StorageManager {
|
|
|
32
35
|
catch {
|
|
33
36
|
// SQLite may not be available or database not ready - will be initialized on first use
|
|
34
37
|
}
|
|
38
|
+
this.initialized = true;
|
|
39
|
+
}
|
|
40
|
+
static resetInit() {
|
|
41
|
+
this.initialized = false;
|
|
35
42
|
}
|
|
36
43
|
/**
|
|
37
44
|
* Proactively reads and validates JSON.
|
package/build/storage/tasks.js
CHANGED
|
@@ -11,10 +11,13 @@ function generateTaskId() {
|
|
|
11
11
|
const random = Math.random().toString(36).substring(2, 6);
|
|
12
12
|
return `task_${timestamp}_${random}`;
|
|
13
13
|
}
|
|
14
|
+
let initialized = false;
|
|
14
15
|
/**
|
|
15
16
|
* Initialize the tasks table (run migrations)
|
|
16
17
|
*/
|
|
17
18
|
export function initTasksTable() {
|
|
19
|
+
if (initialized)
|
|
20
|
+
return;
|
|
18
21
|
const db = getDatabase();
|
|
19
22
|
const TASKS_SCHEMA = `
|
|
20
23
|
CREATE TABLE IF NOT EXISTS tasks (
|
|
@@ -50,6 +53,10 @@ export function initTasksTable() {
|
|
|
50
53
|
// Trigger may already exist in older SQLite versions without IF NOT EXISTS support
|
|
51
54
|
}
|
|
52
55
|
console.error("[Nexus] Tasks table initialized");
|
|
56
|
+
initialized = true;
|
|
57
|
+
}
|
|
58
|
+
export function resetTasksInit() {
|
|
59
|
+
initialized = false;
|
|
53
60
|
}
|
|
54
61
|
/**
|
|
55
62
|
* Create a new task
|
package/docs/CHANGELOG_zh.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
本项目的所有重大变更都将记录在此文件中。
|
|
4
4
|
|
|
5
|
+
## [0.3.4] - 2026-01-10
|
|
6
|
+
### 协议与稳定性
|
|
7
|
+
- **新握手协议**: 引入 `POST /nexus/handshake` 替代旧版 `/hello`。支持严格的客户端/服务端版本校验和稳健的 Host 探测。
|
|
8
|
+
- **全局错误安全网**: 增加了 `uncaughtException` 和 `unhandledRejection` 处理器,防止后台任务错误导致进程退出,确保 Hub 的高可用性。
|
|
9
|
+
- **修复 (EOF 错误)**: 解决了由于 SQLite 非幂等初始化导致的“连接关闭:EOF”崩溃问题。
|
|
10
|
+
- **修复 (僵尸 Host)**: 改进了 Guest 的 Host检测逻辑,消除了死循环重试。
|
|
11
|
+
- **测试覆盖**: 新增 `guest_connection.test.ts` 验证 Guest-Host SSE 集成。
|
|
12
|
+
|
|
13
|
+
## [0.3.3] - 2026-01-10
|
|
14
|
+
### 🔄 零配置持久化 (Zero-Config Persistence)
|
|
15
|
+
- **支持 XDG Base Directory**: 将存储位置从不稳定的 `node_modules` 迁移至系统标准的 User Data 路径:
|
|
16
|
+
- **Linux/WSL**: `~/.local/share/n2n-nexus`
|
|
17
|
+
- **Windows**: `%APPDATA%\n2n-nexus`
|
|
18
|
+
- **macOS**: `~/Library/Application Support/n2n-nexus`
|
|
19
|
+
- **数据持久化**: 数据现在可以从 `npx` 缓存清理、项目删除和重新安装中幸存。
|
|
20
|
+
- **监听地址**: 默认监听地址更改为 `0.0.0.0` 以支持 WSL 镜像模式网络。
|
|
21
|
+
- **身份安全**: 默认的 "Assistant" ID 现在会自动追加随机后缀(例如 `Assistant-x9a2`),以防止多个空 IDE 连接时发生冲突。
|
|
22
|
+
|
|
5
23
|
## [v0.3.0] - 2026-01-08
|
|
6
24
|
|
|
7
25
|
### 🌐 全局 Hub 架构 (零配置多 IDE 协作)
|
package/docs/README_zh.md
CHANGED
|
@@ -115,15 +115,25 @@
|
|
|
115
115
|
|
|
116
116
|
> **零配置**: 无需 `--id` 或 `--host`。直接运行即可协作!
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
```json
|
|
120
|
-
"args": ["-y", "@datafrog-io/n2n-nexus", "--root", "/path/to/storage"]
|
|
118
|
+
"args": ["-y", "@datafrog-io/n2n-nexus"]
|
|
121
119
|
```
|
|
122
120
|
|
|
121
|
+
### 💾 数据持久化 (零配置)
|
|
122
|
+
|
|
123
|
+
Nexus 现在自动将数据存储在系统标准的 **用户数据目录** (XDG Base Directory) 中。这确保了会议历史和项目数据在 IDE 重启、`npx` 缓存清理和更新后依然存在。
|
|
124
|
+
|
|
125
|
+
- **Linux / WSL**: `~/.local/share/n2n-nexus`
|
|
126
|
+
- **Windows**: `%APPDATA%\n2n-nexus`
|
|
127
|
+
- **macOS**: `~/Library/Application Support/n2n-nexus`
|
|
128
|
+
|
|
129
|
+
> **WSL 用户注意**: 为了获得最佳 I/O 性能,WSL 实例将数据存储在 Linux 文件系统中 (`~/.local/share`),而 Windows 实例使用 `%APPDATA%`。数据在两个环境之间是 **隔离** 的,以防止数据库损坏和性能下降。
|
|
130
|
+
|
|
123
131
|
### 命令行参数
|
|
124
132
|
| 参数 | 说明 | 默认值 |
|
|
125
133
|
|------|------|--------|
|
|
126
|
-
|
|
|
134
|
+
| 参数 | 说明 | 默认值 |
|
|
135
|
+
|------|------|--------|
|
|
136
|
+
| `--root` | 覆盖存储路径 (仅限高级用途) | 系统用户数据目录 |
|
|
127
137
|
|
|
128
138
|
> **注意:** 实例 ID(默认为当前项目文件夹名称)和 Host 身份将根据启动顺序自动生成。
|
|
129
139
|
|