@datafrog-io/n2n-nexus 0.3.1 → 0.3.3

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 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
- **Optional**: Use `--root` to specify a custom storage path:
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
- | `--root` | Local storage path for all Nexus data | `./storage` |
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
@@ -51,8 +51,8 @@ if (hasFlag("--version") || hasFlag("-v")) {
51
51
  }
52
52
  // --- Path Normalization Logic ---
53
53
  function normalizeRootPath(inputPath) {
54
- // 1. Priority: CLI --root > ENV NEXUS_ROOT > Default ./storage
55
- let root = inputPath || process.env.NEXUS_ROOT || path.join(__dirname, "../storage");
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,6 +65,18 @@ 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
  */
@@ -104,70 +116,74 @@ async function probeHost(port) {
104
116
  * 3. If not found, try to become Host
105
117
  * 4. If bind fails, wait and re-probe (give winner time to start)
106
118
  */
107
- async function isHostAutoElection(root, retryCount = 0) {
119
+ async function isHostAutoElection(root) {
108
120
  const startPort = 5688;
109
121
  const endPort = 5800;
110
- // Phase 1: Probe-First - Check if any Host already exists (Concurrent Batch Scan)
111
- const BATCH_SIZE = 20;
112
- for (let batchStart = startPort; batchStart <= endPort; batchStart += BATCH_SIZE) {
113
- const batchEnd = Math.min(batchStart + BATCH_SIZE - 1, endPort);
114
- const promises = [];
115
- for (let port = batchStart; port <= batchEnd; port++) {
116
- promises.push(probeHost(port).then(res => ({ port, ...res })));
122
+ let retryCount = 0;
123
+ // eslint-disable-next-line no-constant-condition
124
+ while (true) {
125
+ // Phase 1: Probe-First - Check if any Host already exists (Concurrent Batch Scan)
126
+ const BATCH_SIZE = 20;
127
+ for (let batchStart = startPort; batchStart <= endPort; batchStart += BATCH_SIZE) {
128
+ const batchEnd = Math.min(batchStart + BATCH_SIZE - 1, endPort);
129
+ const promises = [];
130
+ for (let port = batchStart; port <= batchEnd; port++) {
131
+ promises.push(probeHost(port).then(res => ({ port, ...res })));
132
+ }
133
+ const results = await Promise.all(promises);
134
+ const found = results.find(r => r.isNexus);
135
+ if (found) {
136
+ return { isHost: false, port: found.port, rootStorage: found.rootStorage };
137
+ }
117
138
  }
118
- const results = await Promise.all(promises);
119
- const found = results.find(r => r.isNexus);
120
- if (found) {
121
- return { isHost: false, port: found.port, rootStorage: found.rootStorage };
122
- }
123
- }
124
- // Phase 2: No Host found, attempt to become Host
125
- for (let port = startPort; port <= endPort; port++) {
126
- const result = await new Promise((resolve) => {
127
- const server = http.createServer((req, res) => {
128
- if (req.url === "/hello") {
129
- res.writeHead(200, { "Content-Type": "application/json" });
130
- res.end(JSON.stringify({
131
- service: "n2n-nexus",
132
- role: "host",
133
- version: pkg.version,
134
- rootStorage: root
135
- }));
136
- return;
137
- }
138
- res.writeHead(404);
139
- res.end();
140
- });
141
- server.on("error", (err) => {
142
- if (err.code === "EADDRINUSE") {
143
- resolve({ isHost: false });
144
- }
145
- else {
146
- resolve({ isHost: false });
147
- }
148
- });
149
- server.listen(port, "127.0.0.1", () => {
150
- resolve({ isHost: true, server });
139
+ // Phase 2: No Host found, attempt to become Host
140
+ for (let port = startPort; port <= endPort; port++) {
141
+ const result = await new Promise((resolve) => {
142
+ const server = http.createServer((req, res) => {
143
+ if (req.url === "/hello") {
144
+ res.writeHead(200, { "Content-Type": "application/json" });
145
+ res.end(JSON.stringify({
146
+ service: "n2n-nexus",
147
+ role: "host",
148
+ version: pkg.version,
149
+ rootStorage: root
150
+ }));
151
+ return;
152
+ }
153
+ res.writeHead(404);
154
+ res.end();
155
+ });
156
+ server.on("error", (err) => {
157
+ if (err.code === "EADDRINUSE") {
158
+ resolve({ isHost: false });
159
+ }
160
+ else {
161
+ resolve({ isHost: false });
162
+ }
163
+ });
164
+ server.listen(port, "0.0.0.0", () => {
165
+ resolve({ isHost: true, server });
166
+ });
151
167
  });
152
- });
153
- if (result.isHost) {
154
- return { isHost: true, port, server: result.server };
155
- }
156
- // Phase 3: Bind failed - another Guest won. Wait then join winner.
157
- await new Promise(r => setTimeout(r, 10000)); // Give winner 10s to start /hello
158
- const probe = await probeHost(port);
159
- if (probe.isNexus) {
160
- return { isHost: false, port, rootStorage: probe.rootStorage };
168
+ if (result.isHost) {
169
+ return { isHost: true, port, server: result.server };
170
+ }
171
+ // Phase 3: Bind failed - another Guest won. Wait then join winner.
172
+ await new Promise(r => setTimeout(r, 10000)); // Give winner 10s to start /hello
173
+ const probe = await probeHost(port);
174
+ if (probe.isNexus) {
175
+ return { isHost: false, port, rootStorage: probe.rootStorage };
176
+ }
177
+ // If still not Nexus, try next port (occupied by non-Nexus service)
161
178
  }
162
- // If still not Nexus, try next port (occupied by non-Nexus service)
179
+ // Fallback: All ports occupied - progressive backoff retry
180
+ // First 5 attempts: 1 minute interval, then 2 minute interval
181
+ const waitTime = retryCount < 5 ? 60000 : 120000;
182
+ const intervalStr = retryCount < 5 ? "1 minute" : "2 minutes";
183
+ console.error(`[Nexus] All ports ${startPort}-${endPort} occupied. Retry #${retryCount + 1} in ${intervalStr}...`);
184
+ await new Promise(r => setTimeout(r, waitTime));
185
+ retryCount++;
163
186
  }
164
- // Fallback: All ports occupied - progressive backoff retry
165
- // First 5 attempts: 1 minute interval, then 2 minute interval
166
- const waitTime = retryCount < 5 ? 60000 : 120000;
167
- const intervalStr = retryCount < 5 ? "1 minute" : "2 minutes";
168
- console.error(`[Nexus] All ports ${startPort}-${endPort} occupied. Retry #${retryCount + 1} in ${intervalStr}...`);
169
- await new Promise(r => setTimeout(r, waitTime));
170
- return isHostAutoElection(root, retryCount + 1);
171
187
  }
172
188
  /**
173
189
  * Automatic Project Name Detection
@@ -182,7 +198,10 @@ function getAutoProjectName() {
182
198
  }
183
199
  }
184
200
  catch { /* ignore */ }
185
- return path.basename(process.cwd()) || "Assistant";
201
+ const base = path.basename(process.cwd()) || "Assistant";
202
+ // Append random suffix to prevent collisions when multiple IDEs open empty/same folders
203
+ const suffix = Math.random().toString(36).substring(2, 6);
204
+ return `${base}-${suffix}`;
186
205
  }
187
206
  const rootPath = normalizeRootPath(getArg("--root"));
188
207
  const election = await isHostAutoElection(rootPath);
package/build/index.js CHANGED
@@ -231,7 +231,7 @@ class NexusServer {
231
231
  const req = http.request({
232
232
  hostname: "127.0.0.1",
233
233
  port: CONFIG.port,
234
- path: `/mcp?sessionId=${sessionId}&id=${guestId}`,
234
+ path: `/mcp?sessionId=${sessionId}&id=${encodeURIComponent(guestId)}`,
235
235
  method: "POST",
236
236
  headers: { "Content-Type": "application/json" }
237
237
  });
@@ -243,7 +243,7 @@ class NexusServer {
243
243
  catch { /* suppress */ }
244
244
  };
245
245
  process.stdin.on("data", stdioHandler);
246
- http.get(`http://127.0.0.1:${CONFIG.port}/mcp?id=${guestId}`, (res) => {
246
+ http.get(`http://127.0.0.1:${CONFIG.port}/mcp?id=${encodeURIComponent(guestId)}`, (res) => {
247
247
  retryCount = 0; // Reset on successful connection
248
248
  let buffer = "";
249
249
  res.on("data", (chunk) => {
@@ -2,6 +2,16 @@
2
2
 
3
3
  本项目的所有重大变更都将记录在此文件中。
4
4
 
5
+ ## [0.3.3] - 2026-01-10
6
+ ### 🔄 零配置持久化 (Zero-Config Persistence)
7
+ - **支持 XDG Base Directory**: 将存储位置从不稳定的 `node_modules` 迁移至系统标准的 User Data 路径:
8
+ - **Linux/WSL**: `~/.local/share/n2n-nexus`
9
+ - **Windows**: `%APPDATA%\n2n-nexus`
10
+ - **macOS**: `~/Library/Application Support/n2n-nexus`
11
+ - **数据持久化**: 数据现在可以从 `npx` 缓存清理、项目删除和重新安装中幸存。
12
+ - **监听地址**: 默认监听地址更改为 `0.0.0.0` 以支持 WSL 镜像模式网络。
13
+ - **身份安全**: 默认的 "Assistant" ID 现在会自动追加随机后缀(例如 `Assistant-x9a2`),以防止多个空 IDE 连接时发生冲突。
14
+
5
15
  ## [v0.3.0] - 2026-01-08
6
16
 
7
17
  ### 🌐 全局 Hub 架构 (零配置多 IDE 协作)
package/docs/README_zh.md CHANGED
@@ -115,15 +115,25 @@
115
115
 
116
116
  > **零配置**: 无需 `--id` 或 `--host`。直接运行即可协作!
117
117
 
118
- **可选**: 使用 `--root` 指定自定义存储路径:
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
- | `--root` | 本地数据存储路径 | `./storage` |
134
+ | 参数 | 说明 | 默认值 |
135
+ |------|------|--------|
136
+ | `--root` | 覆盖存储路径 (仅限高级用途) | 系统用户数据目录 |
127
137
 
128
138
  > **注意:** 实例 ID(默认为当前项目文件夹名称)和 Host 身份将根据启动顺序自动生成。
129
139
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datafrog-io/n2n-nexus",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Modular MCP Server for multi-AI assistant coordination",
5
5
  "main": "build/index.js",
6
6
  "type": "module",