@datafrog-io/n2n-nexus 0.2.0 → 0.3.0

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
@@ -10,73 +10,14 @@
10
10
 
11
11
  > **Works with:** Claude Code · Claude Desktop · VS Code · Cursor · Windsurf · Zed · JetBrains · Theia · Google Antigravity
12
12
 
13
- ## 🏛️ Architecture
14
-
15
- 1. **Nexus Room (Discussion)**: Unified public channel for all IDE assistants to coordinate across projects.
16
- 2. **Asset Vault (Archives)**:
17
- - **Manifest**: Technical details, billing, topology, and API specs for each project.
18
- - **Internal Docs**: Detailed technical implementation plans.
19
- - **Assets**: Local physical assets (Logos, UI screenshots, etc.).
20
- 3. **Global Knowledge**:
21
- - **Master Strategy**: Top-level strategic blueprint.
22
- - **Global Docs**: Cross-project common documents (e.g., Coding Standards, Roadmaps).
23
- 4. **Topology Engine**: Automated dependency graph analysis.
24
-
25
- ## 💾 Data Persistence
26
-
27
- Nexus stores all data in the local file system (customizable path), ensuring complete data sovereignty.
28
-
29
- **Directory Structure Example**:
30
- ```text
31
- Nexus_Storage/
32
- ├── global/
33
- │ ├── blueprint.md # Master Strategy
34
- │ ├── discussion.json # Chat History
35
- │ ├── docs_index.json # Global Docs Metadata
36
- │ └── docs/ # Global Markdown Docs
37
- │ ├── coding-standards.md
38
- │ └── deployment-flow.md
39
- ├── projects/
40
- │ ├── my-app/
41
- │ │ ├── manifest.json # Project Metadata
42
- │ │ ├── internal_blueprint.md
43
- │ │ └── assets/ # Binary Assets
44
- │ └── ...
45
- ├── registry.json # Global Project Index
46
- └── archives/ # (Reserved for backups)
47
- ```
48
-
49
- **Self-healing**: Core data files (e.g., `registry.json`, `discussion.json`) include automatic detection and repair mechanisms. If files are corrupted or missing, the system automatically rebuilds the initial state to ensure uninterrupted service.
50
-
51
- **Concurrency Safety**: All write operations to shared files (`discussion.json`, `registry.json`) are protected by an `AsyncMutex` lock, preventing race conditions when multiple AI agents communicate simultaneously.
52
-
53
- ## 🏷️ Project ID Conventions (Naming Standard)
54
-
55
- To ensure clarity and prevent collisions in the flat local namespace, all Project IDs MUST follow the **Prefix Dictionary** format: `[prefix]_[project-name]`.
13
+ 📖 **Documentation:** [CHANGELOG](CHANGELOG.md) | [TODO](TODO.md) | [中文文档](docs/README_zh.md) | [AI Assistant Guide](docs/ASSISTANT_GUIDE.md) | [Architecture](docs/ARCHITECTURE.md)
56
14
 
57
- | Prefix | Category | Example |
58
- | :--- | :--- | :--- |
59
- | `web_` | Websites, landing pages, domain-based projects | `web_datafrog.io` |
60
- | `api_` | Backend services, REST/gRPC APIs | `api_user-auth` |
61
- | `chrome_` | Chrome extensions | `chrome_evisa-helper` |
62
- | `vscode_` | VSCode extensions | `vscode_super-theme` |
63
- | `mcp_` | MCP Servers and MCP-related tools | `mcp_github-repo` |
64
- | `android_` | Native Android projects (Kotlin/Java) | `android_client-app` |
65
- | `ios_` | Native iOS projects (Swift/ObjC) | `ios_client-app` |
66
- | `flutter_` | **Mobile Cross-platform Special Case** | `flutter_unified-app` |
67
- | `desktop_` | General desktop apps (Tauri, Electron, etc.) | `desktop_main-hub` |
68
- | `lib_` | Shared libraries, SDKs, NPM/Python packages | `lib_crypto-core` |
69
- | `bot_` | Bots (Discord, Slack, DingTalk, etc.) | `bot_auto-moderator` |
70
- | `infra_` | Infrastructure as Code, CI/CD, DevOps scripts | `infra_k8s-config` |
71
- | `doc_` | Pure technical handbooks, strategies, roadmaps | `doc_coding-guide` |
72
-
73
- ---
74
15
 
75
16
  ## 🛠️ Toolset
76
17
 
77
18
  ### A. Session & Context
78
19
  - `register_session_context`: Declare the project ID currently active in the IDE to unlock write permissions.
79
- - `mcp://nexus/session`: View current identity, role (Moderator/Regular), and active project.
20
+ - `mcp://nexus/session`: View current identity, role (Host/Regular), and active project.
80
21
 
81
22
  ### B. Project Asset Management
82
23
  - `sync_project_assets`: **[Core/ASYNC]** Submit full Project Manifest and Internal Docs. Returns `taskId`.
@@ -89,16 +30,16 @@ To ensure clarity and prevent collisions in the flat local namespace, all Projec
89
30
 
90
31
  ### C. Global Collaboration
91
32
  - `send_message`: Post a message to the team (Auto-routes to active meeting).
92
- - `read_messages`: Retrieve latest logs from active meeting or global registry.
33
+ - `read_messages`: **[Incremental]** Returns only unread messages per IDE instance. Server tracks read cursor automatically.
93
34
  - `update_global_strategy`: Update the core strategic blueprint (`# Master Plan`).
94
- - `get_global_topology`: Retrieve the network-wide project dependency graph.
35
+ - `get_global_topology`: **[Progressive]** Default: summary list. With `projectId`: detailed subgraph.
95
36
  - `sync_global_doc`: Create or update a shared cross-project document.
96
37
 
97
38
  ### D. Meeting Management
98
39
  - `start_meeting`: Start a new tactical session for focused collaboration.
99
40
  - `reopen_meeting`: Reactivate a `closed` or `archived` session to continue discussion.
100
- - `end_meeting`: Conclude a meeting, lock history (**Moderator only**).
101
- - `archive_meeting`: Move closed meetings to cold storage (**Moderator only**).
41
+ - `end_meeting`: Conclude a meeting, lock history (**Host only**).
42
+ - `archive_meeting`: Move closed meetings to cold storage (**Host only**).
102
43
 
103
44
  ### E. Task Management (Phase 2 - ASYNC)
104
45
  - `create_task`: Create a new background task. Link to meeting for traceability.
@@ -107,20 +48,53 @@ To ensure clarity and prevent collisions in the flat local namespace, all Projec
107
48
  - `update_task`: Update progress or result (typically for workers).
108
49
  - `cancel_task`: Cancel a pending or running task.
109
50
 
110
- ### F. Admin (Moderator Only)
111
- - `moderator_maintenance`: Prune or clear system logs.
112
- - `moderator_delete_project`: Completely remove a project and its assets.
51
+ ### F. Host (Host Only)
52
+ - `host_maintenance`: Prune or clear system logs.
53
+ - `host_delete_project`: Completely remove a project and its assets.
113
54
 
114
55
  ## 📄 Resources (URI)
115
56
 
57
+ **Core Resources (Static):**
116
58
  - `mcp://nexus/chat/global`: Real-time conversation history.
117
- - `mcp://nexus/hub/registry`: Global project registry overview.
59
+ - `mcp://nexus/hub/registry`: Global project registry - **read this first to discover project IDs**.
118
60
  - `mcp://nexus/docs/global-strategy`: Strategic blueprint.
61
+ - `mcp://nexus/docs/list`: Index of shared documents.
62
+ - `mcp://nexus/meetings/list`: List of active and closed meetings.
119
63
  - `mcp://nexus/session`: Current session status and identity.
120
64
  - `mcp://nexus/status`: System operational status and storage mode.
121
65
  - `mcp://nexus/active-meeting`: Real-time transcript of the current active meeting.
122
- - `mcp://nexus/projects/{id}/manifest`: Full metadata for a specific project.
123
- - `mcp://nexus/projects/{id}/internal-docs`: Internal technical docs for a specific project.
66
+
67
+ **Resource Templates (Use registry to discover IDs):**
68
+ - `mcp://nexus/projects/{projectId}/manifest`: Full metadata for a specific project.
69
+ - `mcp://nexus/projects/{projectId}/internal-docs`: Internal technical docs for a project.
70
+ - `mcp://nexus/docs/{docId}`: Read a specific shared document.
71
+ - `mcp://nexus/meetings/{meetingId}`: Full transcript for a specific meeting.
72
+
73
+ ## 🌐 Global Hub Architecture
74
+
75
+ **v0.3.0** introduces a fully automatic, zero-configuration collaboration architecture:
76
+
77
+ ```
78
+ ┌─────────────────────────────────────────────────────────────┐
79
+ │ Global Nexus Hub │
80
+ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
81
+ │ │ Cursor │ │ VS Code │ │ Claude │ │ Zed │ │
82
+ │ │ (Guest) │ │ (Guest) │ │ (Host) │ │ (Guest) │ │
83
+ │ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │
84
+ │ │ │ │ │ │
85
+ │ └─────────────┴──────┬──────┴─────────────┘ │
86
+ │ │ SSE │
87
+ │ ┌───────▼───────┐ │
88
+ │ │ Port 5688 │ │
89
+ │ │ (Auto-Elected)│ │
90
+ │ └───────────────┘ │
91
+ └─────────────────────────────────────────────────────────────┘
92
+ ```
93
+
94
+ - **Zero Config**: Just run `npx @datafrog-io/n2n-nexus` - no `--id` or `--host` required.
95
+ - **Auto Election**: First instance binds port 5688 and becomes Host; others join as Guests.
96
+ - **Cross-Project Sync**: All IDEs share the same Hub, enabling real-time cross-project meetings.
97
+ - **Hot Failover**: If Host disconnects, a Guest automatically promotes within 10 seconds.
124
98
 
125
99
  ## 🚀 Quick Start
126
100
 
@@ -128,7 +102,7 @@ To ensure clarity and prevent collisions in the flat local namespace, all Projec
128
102
 
129
103
  Add to your MCP config file (e.g., `claude_desktop_config.json` or Cursor MCP settings):
130
104
 
131
- #### Moderator (Admin AI)
105
+ #### Leader AI
132
106
  ```json
133
107
  {
134
108
  "mcpServers": {
@@ -137,8 +111,6 @@ Add to your MCP config file (e.g., `claude_desktop_config.json` or Cursor MCP se
137
111
  "args": [
138
112
  "-y",
139
113
  "@datafrog-io/n2n-nexus",
140
- "--id", "Master-AI",
141
- "--moderator",
142
114
  "--root", "D:/DevSpace/Nexus_Storage"
143
115
  ]
144
116
  }
@@ -146,7 +118,7 @@ Add to your MCP config file (e.g., `claude_desktop_config.json` or Cursor MCP se
146
118
  }
147
119
  ```
148
120
 
149
- #### Regular AI
121
+ #### Collaborator AI
150
122
  ```json
151
123
  {
152
124
  "mcpServers": {
@@ -155,7 +127,6 @@ Add to your MCP config file (e.g., `claude_desktop_config.json` or Cursor MCP se
155
127
  "args": [
156
128
  "-y",
157
129
  "@datafrog-io/n2n-nexus",
158
- "--id", "Assistant-AI",
159
130
  "--root", "D:/DevSpace/Nexus_Storage"
160
131
  ]
161
132
  }
@@ -166,11 +137,9 @@ Add to your MCP config file (e.g., `claude_desktop_config.json` or Cursor MCP se
166
137
  ### CLI Arguments
167
138
  | Argument | Description | Default |
168
139
  |----------|-------------|---------|
169
- | `--id` | Instance identifier for this AI agent | `Assistant` |
170
- | `--moderator` | Grant admin privileges to this instance | `false` |
171
140
  | `--root` | Local storage path for all Nexus data | `./storage` |
172
141
 
173
- > **Note:** Only instances with `--moderator` flag can use admin tools (e.g., `moderator_maintenance`).
142
+ > **Note:** Host identity and Instance ID are determined automatically based on the project folder name and startup order.
174
143
 
175
144
  ### Local Development
176
145
  ```bash
@@ -178,7 +147,7 @@ git clone https://github.com/n2ns/n2n-nexus.git
178
147
  cd n2n-nexus
179
148
  npm install
180
149
  npm run build
181
- npm start -- --id Master-AI --root ./my-storage
150
+ npm start -- --root ./my-storage
182
151
  ```
183
152
 
184
153
  ---
@@ -202,4 +171,13 @@ The following files demonstrate a real orchestration session where **4 AI agents
202
171
  > *This is what AI-native development looks like.*
203
172
 
204
173
  ---
205
- © 2025 datafrog.io. Built for Local-Only AI Workflows.
174
+
175
+ ## ⭐ Support This Project
176
+
177
+ If **n2ns Nexus** helps you build better AI workflows, consider giving it a star! Your support helps us improve and motivates continued development.
178
+
179
+ [![Star on GitHub](https://img.shields.io/github/stars/n2ns/n2n-nexus?style=social)](https://github.com/n2ns/n2n-nexus)
180
+
181
+ ---
182
+
183
+ © 2026 datafrog.io. Built for Local-Only AI Workflows.
package/build/config.js CHANGED
@@ -1,14 +1,186 @@
1
1
  import path from "path";
2
2
  import { fileURLToPath } from "url";
3
+ import os from "os";
4
+ import fs from "fs";
5
+ import http from "http";
3
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
4
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"));
5
11
  const getArg = (k) => {
6
12
  const i = args.indexOf(k);
7
13
  return i !== -1 && args[i + 1] ? args[i + 1] : "";
8
14
  };
9
- const hasFlag = (k) => args.includes(k);
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.log(`
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.log(pkg.version);
50
+ process.exit(0);
51
+ }
52
+ // --- Path Normalization Logic ---
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");
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
+ /**
69
+ * Probe a port to see if it's a Nexus Host
70
+ */
71
+ async function probeHost(port) {
72
+ return new Promise((resolve) => {
73
+ const req = http.get(`http://127.0.0.1:${port}/hello`, { timeout: 500 }, (res) => {
74
+ let data = "";
75
+ res.on("data", (chunk) => data += chunk);
76
+ res.on("end", () => {
77
+ try {
78
+ const info = JSON.parse(data);
79
+ if (info.service === "n2n-nexus" && info.role === "host") {
80
+ resolve({ isNexus: true, rootStorage: info.rootStorage });
81
+ }
82
+ else {
83
+ resolve({ isNexus: false });
84
+ }
85
+ }
86
+ catch {
87
+ resolve({ isNexus: false });
88
+ }
89
+ });
90
+ });
91
+ req.on("error", () => resolve({ isNexus: false }));
92
+ req.on("timeout", () => {
93
+ req.destroy();
94
+ resolve({ isNexus: false });
95
+ });
96
+ });
97
+ }
98
+ /**
99
+ * Automatic Host Election (Port-Based 5688-5700)
100
+ * Strategy: Probe-First + Atomic Bind + Join Winner on Failure
101
+ *
102
+ * 1. First, scan all ports to find existing Host
103
+ * 2. If found, join it immediately
104
+ * 3. If not found, try to become Host
105
+ * 4. If bind fails, wait and re-probe (give winner time to start)
106
+ */
107
+ async function isHostAutoElection(root) {
108
+ const startPort = 5688;
109
+ const endPort = 5700;
110
+ // Phase 1: Probe-First - Check if any Host already exists
111
+ for (let port = startPort; port <= endPort; port++) {
112
+ const probe = await probeHost(port);
113
+ if (probe.isNexus) {
114
+ return { isHost: false, port, rootStorage: probe.rootStorage };
115
+ }
116
+ }
117
+ // Phase 2: No Host found, attempt to become Host
118
+ for (let port = startPort; port <= endPort; port++) {
119
+ const result = await new Promise((resolve) => {
120
+ const server = http.createServer((req, res) => {
121
+ if (req.url === "/hello") {
122
+ res.writeHead(200, { "Content-Type": "application/json" });
123
+ res.end(JSON.stringify({
124
+ service: "n2n-nexus",
125
+ role: "host",
126
+ version: pkg.version,
127
+ rootStorage: root
128
+ }));
129
+ return;
130
+ }
131
+ res.writeHead(404);
132
+ res.end();
133
+ });
134
+ server.on("error", (err) => {
135
+ if (err.code === "EADDRINUSE") {
136
+ resolve({ isHost: false });
137
+ }
138
+ else {
139
+ resolve({ isHost: false });
140
+ }
141
+ });
142
+ server.listen(port, "127.0.0.1", () => {
143
+ resolve({ isHost: true, server });
144
+ });
145
+ });
146
+ if (result.isHost) {
147
+ return { isHost: true, port, server: result.server };
148
+ }
149
+ // Phase 3: Bind failed - another Guest won. Wait then join winner.
150
+ await new Promise(r => setTimeout(r, 10000)); // Give winner 10s to start /hello
151
+ const probe = await probeHost(port);
152
+ if (probe.isNexus) {
153
+ return { isHost: false, port, rootStorage: probe.rootStorage };
154
+ }
155
+ // If still not Nexus, try next port (occupied by non-Nexus service)
156
+ }
157
+ // Fallback: become Host on startPort (should rarely happen)
158
+ return { isHost: true, port: startPort };
159
+ }
160
+ /**
161
+ * Automatic Project Name Detection
162
+ */
163
+ function getAutoProjectName() {
164
+ try {
165
+ const pkgPath = path.join(process.cwd(), "package.json");
166
+ if (fs.existsSync(pkgPath)) {
167
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
168
+ if (pkg.name)
169
+ return pkg.name.split("/").pop() || pkg.name;
170
+ }
171
+ }
172
+ catch { /* ignore */ }
173
+ return path.basename(process.cwd()) || "Assistant";
174
+ }
175
+ const rootPath = normalizeRootPath(getArg("--root"));
176
+ const election = await isHostAutoElection(rootPath);
177
+ const projectName = getAutoProjectName();
178
+ export const hostServer = election.server;
10
179
  export const CONFIG = {
11
- instanceId: getArg("--id") || "Assistant",
12
- isModerator: hasFlag("--moderator"),
13
- rootStorage: path.resolve(getArg("--root") || path.join(__dirname, "../storage"))
180
+ // Priority: CLI --id > Auto-named (Project Name only)
181
+ instanceId: getArg("--id") || projectName,
182
+ isHost: election.isHost,
183
+ // Inherit storage path if Guest, otherwise use local resolved path
184
+ rootStorage: election.isHost ? rootPath : (election.rootStorage || rootPath),
185
+ port: election.port
14
186
  };
package/build/index.js CHANGED
@@ -2,15 +2,17 @@
2
2
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js";
5
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
5
6
  import { readFileSync } from "fs";
6
7
  import { join } from "path";
7
8
  import { fileURLToPath } from "url";
8
- import { CONFIG } from "./config.js";
9
+ import http from "http";
10
+ import { CONFIG, hostServer } from "./config.js";
9
11
  import { StorageManager } from "./storage/index.js";
10
12
  import { TOOL_DEFINITIONS, handleToolCall } from "./tools/index.js";
11
13
  import { listResources, getResourceContent } from "./resources/index.js";
12
14
  import { sanitizeErrorMessage } from "./utils/error.js";
13
- import { checkModeratorPermission } from "./utils/auth.js";
15
+ import { checkHostPermission } from "./utils/auth.js";
14
16
  const __dirname = fileURLToPath(new URL(".", import.meta.url));
15
17
  const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
16
18
  /**
@@ -21,6 +23,7 @@ const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8")
21
23
  class NexusServer {
22
24
  server;
23
25
  currentProject = null;
26
+ sseTransports = new Map();
24
27
  constructor() {
25
28
  this.server = new Server({ name: "n2n-nexus", version: pkg.version }, { capabilities: { resources: {}, tools: {}, prompts: {} } });
26
29
  this.setupHandlers();
@@ -62,8 +65,8 @@ class NexusServer {
62
65
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
63
66
  const { name, arguments: toolArgs } = request.params;
64
67
  try {
65
- if (name.startsWith("moderator_"))
66
- checkModeratorPermission(name);
68
+ if (name.startsWith("host_"))
69
+ checkHostPermission(name);
67
70
  const result = await handleToolCall(name, toolArgs, {
68
71
  currentProject: this.currentProject,
69
72
  setCurrentProject: (id) => { this.currentProject = id; },
@@ -131,29 +134,143 @@ class NexusServer {
131
134
  const shutdown = async (signal) => {
132
135
  console.error(`\n[Nexus] Received ${signal}. Shutting down...`);
133
136
  try {
134
- // Post-departure log
135
137
  const msg = `Nexus Session Terminated (IDE Closed).`;
136
138
  await StorageManager.addGlobalLog(`SYSTEM:${CONFIG.instanceId}`, msg, "UPDATE");
137
139
  console.error(`[Nexus:${CONFIG.instanceId}] Goodbye!`);
138
140
  }
139
- catch {
140
- // Ignore if storage is already cleaned up
141
- }
141
+ catch { /* ignore */ }
142
142
  process.exit(0);
143
143
  };
144
144
  process.on("SIGINT", () => shutdown("SIGINT"));
145
145
  process.on("SIGTERM", () => shutdown("SIGTERM"));
146
- const transport = new StdioServerTransport();
147
- await this.server.connect(transport);
148
- // Announce presence
149
- try {
146
+ if (CONFIG.isHost && hostServer) {
147
+ // --- HOST MODE: Central Hub ---
150
148
  await StorageManager.init();
151
- const onlineMsg = `Nexus Session Active (IDE Opened). Role: ${CONFIG.isModerator ? "Moderator" : "Regular"}`;
149
+ hostServer.on("request", async (req, res) => {
150
+ const url = new URL(req.url || "", `http://${req.headers.host}`);
151
+ if (url.pathname === "/mcp") {
152
+ const guestId = url.searchParams.get("id") || "UnknownGuest";
153
+ if (req.method === "GET") {
154
+ const transport = new SSEServerTransport("/mcp", res);
155
+ this.sseTransports.set(transport.sessionId, transport);
156
+ const msg = `Guest Joined: ${guestId}`;
157
+ await StorageManager.addGlobalLog(`HOST:${CONFIG.instanceId}`, msg, "UPDATE");
158
+ console.error(`[Nexus Hub] ${msg} (Session: ${transport.sessionId})`);
159
+ // Heartbeat: keep connection alive
160
+ const heartbeat = setInterval(() => {
161
+ try {
162
+ res.write(": ping\n\n");
163
+ }
164
+ catch {
165
+ clearInterval(heartbeat);
166
+ }
167
+ }, 30000);
168
+ transport.onclose = () => {
169
+ this.sseTransports.delete(transport.sessionId);
170
+ clearInterval(heartbeat);
171
+ console.error(`[Nexus Hub] Guest Left: ${guestId}`);
172
+ };
173
+ await this.server.connect(transport);
174
+ return;
175
+ }
176
+ else if (req.method === "POST") {
177
+ const sessionId = url.searchParams.get("sessionId");
178
+ const transport = sessionId ? this.sseTransports.get(sessionId) : null;
179
+ if (transport) {
180
+ await transport.handlePostMessage(req, res);
181
+ }
182
+ else {
183
+ res.writeHead(404).end("Session unknown");
184
+ }
185
+ return;
186
+ }
187
+ }
188
+ });
189
+ // Support local stdio for the host's own IDE
190
+ const transport = new StdioServerTransport();
191
+ await this.server.connect(transport);
192
+ const onlineMsg = `Nexus Hub Active. Playing Host.`;
152
193
  await StorageManager.addGlobalLog(`SYSTEM:${CONFIG.instanceId}`, onlineMsg, "UPDATE");
153
- console.error(`[Nexus:${CONFIG.instanceId}] ${onlineMsg}`);
194
+ console.error(`[Nexus:${CONFIG.instanceId}] ${onlineMsg} (Port: ${CONFIG.port})`);
154
195
  }
155
- catch (e) {
156
- console.error("[Nexus] Failed to post online message:", e);
196
+ else {
197
+ // --- GUEST MODE: SSE Proxy ---
198
+ const guestId = CONFIG.instanceId;
199
+ // Random delay function to prevent thundering herd during re-election
200
+ const randomDelay = () => Math.floor(Math.random() * 3000);
201
+ const startProxy = () => {
202
+ // Clear any stale stdin listeners before starting
203
+ process.stdin.removeAllListeners("data");
204
+ console.error(`[Nexus:${guestId}] Global Hub detected at ${CONFIG.port}. Joining...`);
205
+ let sessionId = null;
206
+ let lastActivity = Date.now();
207
+ // Watchdog: trigger re-election if Host is silent for too long
208
+ const watchdog = setInterval(() => {
209
+ if (Date.now() - lastActivity > 60000) {
210
+ console.error("[Nexus Guest] Host stale. Triggering re-election...");
211
+ cleanup();
212
+ // Random delay to prevent all guests from racing for the port
213
+ setTimeout(() => this.run(), randomDelay());
214
+ }
215
+ }, 10000);
216
+ const cleanup = () => {
217
+ clearInterval(watchdog);
218
+ process.stdin.removeAllListeners("data");
219
+ };
220
+ const stdioHandler = (chunk) => {
221
+ if (!sessionId)
222
+ return;
223
+ try {
224
+ const req = http.request({
225
+ hostname: "127.0.0.1",
226
+ port: CONFIG.port,
227
+ path: `/mcp?sessionId=${sessionId}&id=${guestId}`,
228
+ method: "POST",
229
+ headers: { "Content-Type": "application/json" }
230
+ });
231
+ // Handle request errors to prevent unhandled exceptions
232
+ req.on("error", () => { });
233
+ req.write(chunk);
234
+ req.end();
235
+ }
236
+ catch { /* suppress */ }
237
+ };
238
+ process.stdin.on("data", stdioHandler);
239
+ http.get(`http://127.0.0.1:${CONFIG.port}/mcp?id=${guestId}`, (res) => {
240
+ let buffer = "";
241
+ res.on("data", (chunk) => {
242
+ lastActivity = Date.now();
243
+ const str = chunk.toString();
244
+ buffer += str;
245
+ if (!sessionId && buffer.includes("event: endpoint")) {
246
+ const match = buffer.match(/sessionId=([a-f0-9-]+)/);
247
+ if (match)
248
+ sessionId = match[1];
249
+ }
250
+ if (str.includes("event: message")) {
251
+ const lines = str.split("\n");
252
+ const dataLine = lines.find((l) => l.startsWith("data: "));
253
+ if (dataLine) {
254
+ try {
255
+ process.stdout.write(dataLine.substring(6) + "\n");
256
+ }
257
+ catch { }
258
+ }
259
+ }
260
+ });
261
+ res.on("end", () => {
262
+ console.error("[Nexus Guest] Lost connection to Host. Re-electing...");
263
+ cleanup();
264
+ // Random delay for re-election
265
+ setTimeout(() => this.run(), randomDelay());
266
+ });
267
+ }).on("error", () => {
268
+ console.error("[Nexus Guest] Proxy Receive Error. Retry with random delay...");
269
+ cleanup();
270
+ setTimeout(() => this.run(), 1000 + randomDelay());
271
+ });
272
+ };
273
+ startProxy();
157
274
  }
158
275
  }
159
276
  }