@datafrog-io/n2n-nexus 0.3.4 → 0.3.6
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/cli.js +11 -0
- package/build/config/index.js +81 -0
- package/build/config/paths.js +38 -0
- package/build/constants.js +22 -0
- package/build/index.js +85 -212
- package/build/network/election.js +94 -0
- package/build/network/guest.js +91 -0
- package/build/network/host.js +79 -0
- package/build/network/index.js +6 -0
- package/build/resources/index.js +6 -5
- package/build/storage/index.js +15 -14
- package/build/storage/meetings.js +12 -11
- package/build/storage/sqlite.js +1 -1
- package/build/storage/tasks.js +2 -1
- package/build/tools/handlers.js +1 -1
- package/build/utils/auth.js +1 -1
- package/docs/CHANGELOG_zh.md +5 -0
- package/package.json +2 -2
- package/build/config.js +0 -243
package/build/config.js
DELETED
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import { fileURLToPath } from "url";
|
|
3
|
-
import os from "os";
|
|
4
|
-
import fs from "fs";
|
|
5
|
-
import http from "http";
|
|
6
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
const args = process.argv.slice(2);
|
|
8
|
-
// Load version from package.json
|
|
9
|
-
const pkgPath = path.resolve(__dirname, "../package.json");
|
|
10
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
11
|
-
const getArg = (k) => {
|
|
12
|
-
const i = args.indexOf(k);
|
|
13
|
-
return i !== -1 && args[i + 1] ? args[i + 1] : "";
|
|
14
|
-
};
|
|
15
|
-
const hasFlag = (k) => args.includes(k) || args.includes(k.charAt(1) === "-" ? k : k.substring(0, 2));
|
|
16
|
-
// --- CLI Commands Handlers ---
|
|
17
|
-
if (hasFlag("--help") || hasFlag("-h")) {
|
|
18
|
-
console.error(`
|
|
19
|
-
n2ns Nexus 🚀 - Local Digital Asset Hub (MCP Server) v${pkg.version}
|
|
20
|
-
|
|
21
|
-
USAGE:
|
|
22
|
-
npx -y @datafrog-io/n2n-nexus [options]
|
|
23
|
-
|
|
24
|
-
DESCRIPTION:
|
|
25
|
-
A local-first project management and collaboration hub designed for
|
|
26
|
-
multi-AI assistant coordination across different IDEs (Cursor, VS Code, etc.).
|
|
27
|
-
|
|
28
|
-
OPTIONS:
|
|
29
|
-
--root <path> Directory for data persistence. Default: ./storage
|
|
30
|
-
--version, -v Show version number.
|
|
31
|
-
--help, -h Show this message.
|
|
32
|
-
|
|
33
|
-
MCP CONFIG EXAMPLE (claude_desktop_config.json):
|
|
34
|
-
{
|
|
35
|
-
"mcpServers": {
|
|
36
|
-
"n2n-nexus": {
|
|
37
|
-
"command": "npx",
|
|
38
|
-
"args": ["-y", "@datafrog-io/n2n-nexus", "--root", "/path/to/storage"]
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
ENVIRONMENT VARIABLES:
|
|
44
|
-
NEXUS_ROOT Override default storage path.
|
|
45
|
-
`);
|
|
46
|
-
process.exit(0);
|
|
47
|
-
}
|
|
48
|
-
if (hasFlag("--version") || hasFlag("-v")) {
|
|
49
|
-
console.error(pkg.version);
|
|
50
|
-
process.exit(0);
|
|
51
|
-
}
|
|
52
|
-
// --- Path Normalization Logic ---
|
|
53
|
-
function normalizeRootPath(inputPath) {
|
|
54
|
-
// 1. Priority: CLI --root > ENV NEXUS_ROOT > System Default (XDG/AppData)
|
|
55
|
-
let root = inputPath || process.env.NEXUS_ROOT || getDefaultDataDir();
|
|
56
|
-
// 2. Resolve ~ to home directory
|
|
57
|
-
if (root.startsWith("~")) {
|
|
58
|
-
root = path.join(os.homedir(), root.slice(1));
|
|
59
|
-
}
|
|
60
|
-
// 3. Cross-platform adaptation (WSL <-> Windows)
|
|
61
|
-
// If running on Linux (WSL) but path looks like Windows (D:/ or C:\\)
|
|
62
|
-
if (process.platform === "linux" && /^[a-zA-Z]:[/\\]/.test(root)) {
|
|
63
|
-
const drive = root[0].toLowerCase();
|
|
64
|
-
root = `/mnt/${drive}${root.slice(2).replace(/\\/g, "/")}`;
|
|
65
|
-
}
|
|
66
|
-
return path.resolve(root);
|
|
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
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Probe a port to see if it's a Nexus Host
|
|
82
|
-
*/
|
|
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) {
|
|
87
|
-
return new Promise((resolve) => {
|
|
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) => {
|
|
103
|
-
let data = "";
|
|
104
|
-
res.on("data", (chunk) => data += chunk);
|
|
105
|
-
res.on("end", () => {
|
|
106
|
-
try {
|
|
107
|
-
const info = JSON.parse(data);
|
|
108
|
-
if (info.service === "n2n-nexus" && info.role === "host") {
|
|
109
|
-
// console.error(`[Nexus Handshake] Connected to Host v${info.serverVersion} (Protocol ${info.protocol})`);
|
|
110
|
-
resolve({ isNexus: true, rootStorage: info.rootStorage });
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
resolve({ isNexus: false });
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
catch {
|
|
117
|
-
resolve({ isNexus: false });
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
req.on("error", () => resolve({ isNexus: false }));
|
|
122
|
-
req.on("timeout", () => {
|
|
123
|
-
req.destroy();
|
|
124
|
-
resolve({ isNexus: false });
|
|
125
|
-
});
|
|
126
|
-
req.write(postData);
|
|
127
|
-
req.end();
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* Automatic Host Election (Port-Based 5688-5700)
|
|
132
|
-
* Strategy: Probe-First + Atomic Bind + Join Winner on Failure
|
|
133
|
-
*
|
|
134
|
-
* 1. First, scan all ports to find existing Host
|
|
135
|
-
* 2. If found, join it immediately
|
|
136
|
-
* 3. If not found, try to become Host
|
|
137
|
-
* 4. If bind fails, wait and re-probe (give winner time to start)
|
|
138
|
-
*/
|
|
139
|
-
async function isHostAutoElection(root) {
|
|
140
|
-
const startPort = 5688;
|
|
141
|
-
const endPort = 5800;
|
|
142
|
-
let retryCount = 0;
|
|
143
|
-
// eslint-disable-next-line no-constant-condition
|
|
144
|
-
while (true) {
|
|
145
|
-
// Phase 1: Probe-First - Check if any Host already exists (Concurrent Batch Scan)
|
|
146
|
-
const BATCH_SIZE = 20;
|
|
147
|
-
const myId = getArg("--id") || `candidate-${Math.random().toString(36).substring(2, 6)}`;
|
|
148
|
-
for (let batchStart = startPort; batchStart <= endPort; batchStart += BATCH_SIZE) {
|
|
149
|
-
const batchEnd = Math.min(batchStart + BATCH_SIZE - 1, endPort);
|
|
150
|
-
const promises = [];
|
|
151
|
-
for (let port = batchStart; port <= batchEnd; port++) {
|
|
152
|
-
promises.push(probeHost(port, myId).then(res => ({ port, ...res })));
|
|
153
|
-
}
|
|
154
|
-
const results = await Promise.all(promises);
|
|
155
|
-
const found = results.find(r => r.isNexus);
|
|
156
|
-
if (found) {
|
|
157
|
-
return { isHost: false, port: found.port, rootStorage: found.rootStorage };
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
// Phase 2: No Host found, attempt to become Host
|
|
161
|
-
for (let port = startPort; port <= endPort; port++) {
|
|
162
|
-
const result = await new Promise((resolve) => {
|
|
163
|
-
const server = http.createServer((req, res) => {
|
|
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
|
-
});
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
res.writeHead(404);
|
|
187
|
-
res.end();
|
|
188
|
-
});
|
|
189
|
-
server.on("error", (_err) => {
|
|
190
|
-
resolve({ isHost: false });
|
|
191
|
-
});
|
|
192
|
-
server.listen(port, "0.0.0.0", () => {
|
|
193
|
-
resolve({ isHost: true, server });
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
if (result.isHost) {
|
|
197
|
-
return { isHost: true, port, server: result.server };
|
|
198
|
-
}
|
|
199
|
-
// Phase 3: Bind failed - another Guest won. Wait then join winner.
|
|
200
|
-
await new Promise(r => setTimeout(r, 2000)); // Short wait for winner to stabilize
|
|
201
|
-
const probe = await probeHost(port, myId);
|
|
202
|
-
if (probe.isNexus) {
|
|
203
|
-
return { isHost: false, port, rootStorage: probe.rootStorage };
|
|
204
|
-
}
|
|
205
|
-
// If still not Nexus, try next port
|
|
206
|
-
}
|
|
207
|
-
// Fallback: All ports occupied - progressive backoff retry
|
|
208
|
-
const waitTime = retryCount < 5 ? 5000 : 30000;
|
|
209
|
-
console.error(`[Nexus] All ports ${startPort}-${endPort} occupied. Retry #${retryCount + 1} in ${waitTime / 1000}s...`);
|
|
210
|
-
await new Promise(r => setTimeout(r, waitTime));
|
|
211
|
-
retryCount++;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* Automatic Project Name Detection
|
|
216
|
-
*/
|
|
217
|
-
function getAutoProjectName() {
|
|
218
|
-
try {
|
|
219
|
-
const pkgPath = path.join(process.cwd(), "package.json");
|
|
220
|
-
if (fs.existsSync(pkgPath)) {
|
|
221
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
222
|
-
if (pkg.name)
|
|
223
|
-
return pkg.name.split("/").pop() || pkg.name;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
catch { /* ignore */ }
|
|
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}`;
|
|
231
|
-
}
|
|
232
|
-
const rootPath = normalizeRootPath(getArg("--root"));
|
|
233
|
-
const election = await isHostAutoElection(rootPath);
|
|
234
|
-
const projectName = getAutoProjectName();
|
|
235
|
-
export const hostServer = election.server;
|
|
236
|
-
export const CONFIG = {
|
|
237
|
-
// Priority: CLI --id > Auto-named (Project Name only)
|
|
238
|
-
instanceId: getArg("--id") || projectName,
|
|
239
|
-
isHost: election.isHost,
|
|
240
|
-
// Inherit storage path if Guest, otherwise use local resolved path
|
|
241
|
-
rootStorage: election.isHost ? rootPath : (election.rootStorage || rootPath),
|
|
242
|
-
port: election.port
|
|
243
|
-
};
|