@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.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
- };