@datafrog-io/n2n-nexus 0.3.3 → 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/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.log(`
18
+ console.error(`
19
19
  n2ns Nexus 🚀 - Local Digital Asset Hub (MCP Server) v${pkg.version}
20
20
 
21
21
  USAGE:
@@ -46,7 +46,7 @@ ENVIRONMENT VARIABLES:
46
46
  process.exit(0);
47
47
  }
48
48
  if (hasFlag("--version") || hasFlag("-v")) {
49
- console.log(pkg.version);
49
+ console.error(pkg.version);
50
50
  process.exit(0);
51
51
  }
52
52
  // --- Path Normalization Logic ---
@@ -80,15 +80,33 @@ function getDefaultDataDir() {
80
80
  /**
81
81
  * Probe a port to see if it's a Nexus Host
82
82
  */
83
- async function probeHost(port) {
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) {
84
87
  return new Promise((resolve) => {
85
- const req = http.get(`http://127.0.0.1:${port}/hello`, { timeout: 500 }, (res) => {
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) => {
86
103
  let data = "";
87
104
  res.on("data", (chunk) => data += chunk);
88
105
  res.on("end", () => {
89
106
  try {
90
107
  const info = JSON.parse(data);
91
108
  if (info.service === "n2n-nexus" && info.role === "host") {
109
+ // console.error(`[Nexus Handshake] Connected to Host v${info.serverVersion} (Protocol ${info.protocol})`);
92
110
  resolve({ isNexus: true, rootStorage: info.rootStorage });
93
111
  }
94
112
  else {
@@ -105,6 +123,8 @@ async function probeHost(port) {
105
123
  req.destroy();
106
124
  resolve({ isNexus: false });
107
125
  });
126
+ req.write(postData);
127
+ req.end();
108
128
  });
109
129
  }
110
130
  /**
@@ -124,11 +144,12 @@ async function isHostAutoElection(root) {
124
144
  while (true) {
125
145
  // Phase 1: Probe-First - Check if any Host already exists (Concurrent Batch Scan)
126
146
  const BATCH_SIZE = 20;
147
+ const myId = getArg("--id") || `candidate-${Math.random().toString(36).substring(2, 6)}`;
127
148
  for (let batchStart = startPort; batchStart <= endPort; batchStart += BATCH_SIZE) {
128
149
  const batchEnd = Math.min(batchStart + BATCH_SIZE - 1, endPort);
129
150
  const promises = [];
130
151
  for (let port = batchStart; port <= batchEnd; port++) {
131
- promises.push(probeHost(port).then(res => ({ port, ...res })));
152
+ promises.push(probeHost(port, myId).then(res => ({ port, ...res })));
132
153
  }
133
154
  const results = await Promise.all(promises);
134
155
  const found = results.find(r => r.isNexus);
@@ -140,26 +161,33 @@ async function isHostAutoElection(root) {
140
161
  for (let port = startPort; port <= endPort; port++) {
141
162
  const result = await new Promise((resolve) => {
142
163
  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
- }));
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
+ });
151
184
  return;
152
185
  }
153
186
  res.writeHead(404);
154
187
  res.end();
155
188
  });
156
- server.on("error", (err) => {
157
- if (err.code === "EADDRINUSE") {
158
- resolve({ isHost: false });
159
- }
160
- else {
161
- resolve({ isHost: false });
162
- }
189
+ server.on("error", (_err) => {
190
+ resolve({ isHost: false });
163
191
  });
164
192
  server.listen(port, "0.0.0.0", () => {
165
193
  resolve({ isHost: true, server });
@@ -169,18 +197,16 @@ async function isHostAutoElection(root) {
169
197
  return { isHost: true, port, server: result.server };
170
198
  }
171
199
  // 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);
200
+ await new Promise(r => setTimeout(r, 2000)); // Short wait for winner to stabilize
201
+ const probe = await probeHost(port, myId);
174
202
  if (probe.isNexus) {
175
203
  return { isHost: false, port, rootStorage: probe.rootStorage };
176
204
  }
177
- // If still not Nexus, try next port (occupied by non-Nexus service)
205
+ // If still not Nexus, try next port
178
206
  }
179
207
  // 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}...`);
208
+ const waitTime = retryCount < 5 ? 5000 : 30000;
209
+ console.error(`[Nexus] All ports ${startPort}-${endPort} occupied. Retry #${retryCount + 1} in ${waitTime / 1000}s...`);
184
210
  await new Promise(r => setTimeout(r, waitTime));
185
211
  retryCount++;
186
212
  }
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
  });
@@ -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.
@@ -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
@@ -2,6 +2,14 @@
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
+
5
13
  ## [0.3.3] - 2026-01-10
6
14
  ### 🔄 零配置持久化 (Zero-Config Persistence)
7
15
  - **支持 XDG Base Directory**: 将存储位置从不稳定的 `node_modules` 迁移至系统标准的 User Data 路径:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datafrog-io/n2n-nexus",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "Modular MCP Server for multi-AI assistant coordination",
5
5
  "main": "build/index.js",
6
6
  "type": "module",