n2n-nexus 0.4.2 → 0.6.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +74 -117
  3. package/assets/n2n-nexus-banner.jpg +0 -0
  4. package/assets/n2n-nexus-icon.png +0 -0
  5. package/assets/n2n-nexus-logo.png +0 -0
  6. package/build/client/nexus-client.js +30 -14
  7. package/build/config/cli.js +0 -3
  8. package/build/config/index.js +0 -10
  9. package/build/constants.js +7 -11
  10. package/build/daemon/index.js +35 -19
  11. package/build/daemon/rate-limit.js +62 -0
  12. package/build/daemon/server.js +349 -542
  13. package/build/daemon/tool-outputs.js +155 -0
  14. package/build/daemon/tools.js +271 -0
  15. package/build/index.js +1 -1
  16. package/build/server/instance-id.js +19 -0
  17. package/build/server/nexus.js +152 -43
  18. package/build/storage/docs.js +14 -7
  19. package/build/storage/events.js +31 -0
  20. package/build/storage/fs-utils.js +42 -0
  21. package/build/storage/global-messages.js +44 -0
  22. package/build/storage/index.js +44 -41
  23. package/build/storage/migrations.js +79 -0
  24. package/build/storage/paths.js +0 -3
  25. package/build/storage/projects.js +4 -5
  26. package/build/storage/registry.js +67 -25
  27. package/build/storage/sqlite-meeting.js +35 -23
  28. package/build/storage/sqlite.js +41 -24
  29. package/build/storage/tasks.js +29 -103
  30. package/build/types.js +1 -1
  31. package/docs/ARCHITECTURE.md +29 -20
  32. package/docs/ARCHITECTURE_zh.md +29 -20
  33. package/docs/ASSISTANT_GUIDE.md +70 -67
  34. package/docs/README_zh.md +25 -15
  35. package/llms.txt +11 -1
  36. package/package.json +24 -14
  37. package/build/storage/logs.js +0 -60
  38. package/build/storage/meetings.js +0 -276
  39. package/build/storage/store.js +0 -153
package/CHANGELOG.md CHANGED
@@ -2,6 +2,93 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.6.0] - 2026-10-16
6
+
7
+ ### Added
8
+
9
+ - Rate limiting, which the MCP tools specification requires of servers: a token bucket per instance ID (default 20 calls/s, burst 60; `NEXUS_RATE_LIMIT`, `NEXUS_RATE_BURST`, `0` disables) plus one per client address at five times that, since instance IDs are self-declared. Requests without an instance ID (most REST reads) are limited per address only. Over the limit the daemon answers `429` with `Retry-After`; the MCP adapter reports it to the model as a tool error and keeps the connection. `/health` and `GET /api/tools` are exempt.
10
+
11
+ - Structured tool results: every successful call returns `structuredContent` alongside the JSON text, and the eleven data-returning tools publish an `outputSchema`. Result shapes are zod contracts in `src/daemon/tool-outputs.ts` that type the handlers; a test calls all tools through the adapter for both protocol eras and checks each result against its contract. Publishing schemas for write tools as well would have grown `tools/list` from about 14 KB to 27 KB for a one-line acknowledgement, so they are left out (22 KB with the read schemas).
12
+
13
+ - A display `title` on every tool (for example "Delete Project"), which MCP clients show instead of the raw tool name.
14
+
15
+ - MCP tool annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`) on every tool, so clients can tell read-only tools from ones that overwrite or delete data and ask the user before the latter.
16
+
17
+ - `list_meetings` tool and `GET /api/meetings`: list meetings with status, initiator, participants, message and decision counts and summary. Assistants could only reach a meeting whose id they already knew.
18
+
19
+ - `get_project` tool and `GET /api/projects/:projectId`: read a project's full manifest and internal docs. Assistants could sync them but had no tool to read them back.
20
+
21
+ - `get_global_strategy`, `list_global_docs` and `get_global_doc` tools. Assistants could write the strategy and shared docs but had no tool to read them back; only the REST API could.
22
+
23
+ - `list_events` tool and `GET /api/events`: a structured activity log (actor, action, target, detail) of every state change made through the daemon.
24
+
25
+ ### Removed
26
+
27
+ - `register_session_context` and `POST /api/session/register`. The daemon kept an in-memory "current project" per instance that only `upload_project_asset` used; MCP 2026-07-28 makes protocol sessions go away and recommends passing cross-call state as explicit tool arguments. `upload_project_asset` now takes `projectId` and checks that the project exists.
28
+
29
+ - Leftovers from the pre-0.5 election architecture and other dead code: unused constants (`PORT_RANGE_*`, `HANDSHAKE_TIMEOUT`, `HEARTBEAT_INTERVAL`), `hasFlag`, `HubConfig`/`ToolContext`/`MeetingState` types, `isSqliteAvailable`, `getTasksByMeeting`, `getActiveTaskCount`, `NexusClient.health`, the never-used `archives/` directory and `tasks.json` path, `suggestedSyncTargets` (parsed a `name@project` instance-id format that no longer exists), the undocumented `NEXUS_PORT` alias of `NEXUS_DAEMON_PORT`, and `migrations/003_add_tasks_table.sql` (the schema lives in `sqlite.ts`). The two `getRootPath` implementations are merged into `config/paths.ts`, so `--root ./relative` and `~` are resolved consistently. `billing` is now preserved on read and patch instead of being dropped.
30
+
31
+ - JSON meeting-store fallback (`store.ts`, `meetings.ts`) and the `/api/storage/info` endpoint. SQLite is required; the daemon no longer reports a "degraded" storage mode.
32
+
33
+ ### Fixed
34
+
35
+ - `NEXUS_RATE_BURST` between 0 and 1 made every call fail with 429 forever, and `NEXUS_RATE_BURST=0` silently disabled rate limiting. The burst is now at least 1, and only `NEXUS_RATE_LIMIT=0` disables limiting.
36
+
37
+ - Manifests written by earlier releases (whose `update_project` stored patches unvalidated) could hold nested relations, endpoints or API specs with missing or extra fields, which made `get_project` and `get_global_topology` fail outputSchema validation on 2026-07-28 clients. Reads now normalize stored manifests: invalid entries are dropped, missing strings become empty, an unknown relation type reads as `related`. The files are not rewritten.
38
+
39
+ - Result contracts were only enforced by TypeScript, which ignores extra fields on returned variables, so a new field in stored data could reach clients as structuredContent that violates the published `outputSchema`. Every result now passes through its zod contract on the way out: undeclared fields are stripped, and a result that cannot meet the contract becomes a server error.
40
+
41
+ - If a tool call lost the daemon while a reconnect attempt that had just succeeded was still notifying the client, the attempt then stopped the retry loop although the adapter was disconnected again; every later call answered "Daemon is not ready yet" until the IDE restarted. The loop now stops only if the connection is still up.
42
+
43
+ - The adapter's reconnect loop used a 3-second `setInterval` while each attempt could take up to the 5-second request timeout, so a hung daemon accumulated overlapping requests. Each attempt is now scheduled only after the previous one finishes.
44
+
45
+ - `rename_project`, `cancel_task`, `end_meeting`, `archive_meeting` and `reopen_meeting` were annotated `destructiveHint: false`, which MCP reserves for purely additive tools; clients could run them without asking the user. They are now marked destructive.
46
+
47
+ - When the daemon reported a tool as unknown (for example after an upgrade the adapter did not notice), the adapter returned `-32602` but kept offering the tool in `tools/list`. It now re-fetches the tool list and sends `list_changed` if it differs.
48
+
49
+ - The adapter fetched the daemon tool list before it began reading stdin. A daemon that accepted connections but never answered held `server/discover` and `initialize` for the 5-second client timeout, long enough for probing clients to misclassify the server as legacy, and delayed exit after stdin closed. The adapter now serves immediately and fetches in the background.
50
+
51
+ - On 2026-07-28 connections, a request without `io.modelcontextprotocol/clientInfo` (only a SHOULD in the spec) got an instance ID without the IDE name, so one client could appear as two instances with split unread cursors and meeting permissions. The adapter now keeps the first client name seen on the connection.
52
+
53
+ - Calling a tool the adapter does not know returned a tool result with `isError: true`. The MCP specification classifies an unknown tool as a protocol error; the adapter now answers with JSON-RPC error `-32602` (Invalid Params), also when the daemon reports the tool as unknown.
54
+
55
+ - The MCP adapter did not exit when the IDE closed its stdin: while the daemon was unreachable the 3-second reconnect timer kept the process alive indefinitely, leaving orphaned adapters. The adapter now shuts down and exits when stdin ends, as the stdio transport specification requires.
56
+
57
+ - Project and document ids that match `Object.prototype` keys were mishandled: `register_session_context({ projectId: "constructor" })` reported success for a project that did not exist, and a project or doc named `__proto__` was silently lost because assigning it rewrote the map's prototype. The registry and docs index are now prototype-free maps.
58
+
59
+ - `sync_project_assets` wrote the manifest and the internal docs as two separately locked steps, so a concurrent `host_delete_project` could run between them and leave a project directory on disk that the registry no longer knew about. Both writes now happen in one critical section.
60
+
61
+ - `npm run build` now clears `build/` first. `tsc` alone left compiled files of deleted sources behind (for example `build/storage/store.js`), and `npm publish` would have shipped them.
62
+
63
+ - `ttl` on tasks was stored but never enforced and `cleanupTasks` was never scheduled, so the tasks table grew forever. The daemon now runs task maintenance at startup and hourly: pending/running tasks past their TTL are marked `failed` (`TTL expired`) and finished tasks older than 7 days are purged. The `tasks_updated_at` trigger is dropped: it overwrote the ISO 8601 `updated_at` written by `updateTask` with `datetime('now')` in a different format, so age comparisons mixed two timestamp formats.
64
+
65
+ - `update_global_strategy` wrote `global/docs/strategy.md` while the documentation described `global/blueprint.md` as the master strategy document, and two initializers created `blueprint.md` with different placeholder text. The tool now writes `blueprint.md`; `GET /api/global/strategy` reads it. On first start an existing `strategy` global doc is moved into `blueprint.md` (unless a non-placeholder blueprint is already there) and removed from the docs index.
66
+
67
+ - `sqlite.ts` registered SIGINT/SIGTERM handlers at import time that called `process.exit(0)` immediately, so the daemon's graceful `server.close` never ran. Signal handling now lives in the daemon entry point: stop accepting connections, finish in-flight requests, close the database, exit.
68
+
69
+ - Tool arguments were never validated: the daemon cast whatever arrived and let SQLite or the filesystem fail later (for example `category: "SYSTEM"` surfaced as a `CHECK constraint failed` 500). Every tool now has a zod contract in `src/daemon/tools.ts`; the JSON Schema served to MCP clients is generated from it and the same contract validates tool calls and REST endpoints, returning 400 with the failing field. `patch` for `update_project` rejects unknown fields, `create_task` checks that `source_meeting_id` exists, and SQLite foreign keys are enforced.
70
+
71
+ - The MCP adapter generated a random instance ID on every start, so an IDE restart reset unread cursors and left meetings that no one could end or archive (only the initiator may). The default is now `mcp-<hostname>-<hash of working directory>-<IDE name>` (IDE name from the MCP handshake), stable per machine, workspace and IDE. `reopen_meeting` applies the same initiator check as `end_meeting` and `archive_meeting`.
72
+
73
+ - The daemon accepted any HTTP request, so a web page open in the user's browser could call `host_delete_project` on the local daemon with a cross-site `text/plain` POST. Requests carrying a browser `Origin` or cross-site `Sec-Fetch-Site` header are refused (403), POST requires `Content-Type: application/json` (415), and request bodies are capped at 16 MB (413) instead of being buffered without limit. The `Host` header must be an IP literal, `localhost`, `host.docker.internal` or a name in `NEXUS_ALLOWED_HOSTS`, which blocks DNS-rebinding pages that would otherwise reach the daemon as same-origin GETs.
74
+
75
+ - The MCP adapter treated every failed tool call (unknown task, closed meeting, permission denied, ...) as the daemon going away: it cleared the tool list, notified the IDE and entered the reconnect loop. Transport failures and tool failures are now distinct (`NexusTransportError` vs `NexusToolError`); only the former triggers reconnection, and tool failures are returned as `isError` results. `POST /api/tools/call` answers 404 for an unknown tool and 400 when the tool rejects the call.
76
+
77
+ ### Changed
78
+
79
+ - The MCP adapter moved from `@modelcontextprotocol/sdk` 1.x (protocol 2025-11-25) to the v2 SDK (`@modelcontextprotocol/server` 2.3) and serves both the stateless 2026-07-28 revision and 2025-era clients over stdio via `serveStdio`. 2026-07-28 clients get `server/discover`, the per-request `_meta` envelope and list-changed notifications on `subscriptions/listen`; the instance ID reads the client name from the request envelope. The first `tools/list` waits up to one second for the startup daemon fetch, so it is complete whenever a local daemon is up, while `server/discover` and `initialize` are answered immediately even if the daemon hangs.
80
+
81
+ - Tools without parameters (`get_global_strategy`, `list_global_docs`) publish `{ "type": "object", "properties": {}, "additionalProperties": false }`, the form the MCP specification recommends, and reject unexpected arguments.
82
+
83
+ - `send_message` routing no longer depends on `register_session_context`: a message goes to `meetingId` when given, else to the active meeting, else to global chat. `POST /api/messages/send` uses the same rule (its `source` parameter is gone) instead of a separate code path with different defaults.
84
+
85
+ - Global chat moved from `global/discussion.json` into SQLite. `read_messages` without `meetingId` is now truly incremental per instance (the tool description had promised this), appends no longer rewrite the whole file, and `host_maintenance` reports how many messages were removed. On first start the daemon imports an existing `discussion.json` (categories outside the five known values become unset) and renames it to `discussion.json.migrated`.
86
+ - Daemon state changes (session register, project sync/rename/delete, meeting end, ...) are recorded as events instead of `SYSTEM` lines in the global chat.
87
+
88
+ - `sync_project_assets`, `update_project`, `rename_project`, and `host_delete_project` now run synchronously and return their result directly instead of a `task_id` to poll. The operations take milliseconds; the background-task wrapper only added a polling round-trip and let the work race with other requests.
89
+
90
+ - Require Node.js 22.14+ on the 22.x release line, or Node.js 23.6+ and update MCP SDK, SQLite, and test dependencies to their latest compatible stable releases.
91
+
5
92
  ## [0.5.0] - 2026-06-14
6
93
 
7
94
  ### 🏗️ Daemon + MCP Architecture (Breaking Refactor)
package/README.md CHANGED
@@ -1,3 +1,7 @@
1
+ <p align="center">
2
+ <img src="./assets/n2n-nexus-logo.png" width="128" alt="n2n-nexus logo">
3
+ </p>
4
+
1
5
  # n2n-nexus
2
6
 
3
7
  Local-first MCP coordination hub from N2NS Lab for multi-AI assistant collaboration across IDEs, machines, and projects.
@@ -7,7 +11,6 @@ Local-first MCP coordination hub from N2NS Lab for multi-AI assistant collaborat
7
11
  [![license](https://img.shields.io/github/license/n2ns/n2n-nexus)](https://github.com/n2ns/n2n-nexus/blob/main/LICENSE)
8
12
  [![MCP Protocol](https://img.shields.io/badge/MCP-Protocol-blue)](https://modelcontextprotocol.io)
9
13
  [![node version](https://img.shields.io/node/v/n2n-nexus)](https://nodejs.org)
10
- [![DataFrog.io](https://datafrog.io/badges/datafrog.svg)](https://datafrog.io)
11
14
 
12
15
  [中文版](./docs/README_zh.md)
13
16
 
@@ -15,45 +18,33 @@ Local-first MCP coordination hub from N2NS Lab for multi-AI assistant collaborat
15
18
 
16
19
  > **One daemon. Many assistants. Shared project state.**
17
20
 
18
- n2n-nexus is an open-source Model Context Protocol (MCP) coordination server for teams and developers who use multiple AI coding assistants. A long-running local daemon owns project state, meetings, messages, tasks, shared docs, and project assets. Lightweight MCP adapters connect each IDE or assistant to the same daemon.
19
-
20
- Use it when Claude Desktop, Claude Code, Cursor, VS Code, Windsurf, JetBrains, or other MCP-enabled clients need a shared local coordination layer instead of isolated per-chat context.
21
-
22
- ## Search-friendly positioning
23
-
24
- If you are searching for:
25
-
26
- - multi-agent MCP coordination
27
- - multi-AI assistant collaboration
28
- - local MCP coordination hub
29
- - shared project context for AI coding agents
30
- - cross-IDE MCP collaboration server
31
- - local-first project registry for AI assistants
32
- - meeting and task coordination for coding agents
21
+ n2n-nexus is an open-source Model Context Protocol (MCP) coordination server for developers and teams who use multiple AI coding assistants across IDEs, machines, and projects. Use it when Claude Desktop, Claude Code, Cursor, VS Code, Windsurf, JetBrains, or other MCP-enabled clients need a shared local coordination layer instead of isolated per-chat context.
33
22
 
34
- n2n-nexus is designed for these goals: shared daemon state, stateless MCP adapters, project-aware collaboration, local storage, and cross-environment IDE workflows.
23
+ ## 📚 Contents
35
24
 
36
- ## What Is n2n-nexus?
25
+ - [What is n2n-nexus?](#-what-is-n2n-nexus)
26
+ - [Architecture](#️-architecture)
27
+ - [Quick start](#-quick-start)
28
+ - [Toolset](#️-toolset)
29
+ - [Data storage](#-data-storage)
30
+ - [Project ID conventions](#️-project-id-conventions)
31
+ - [CLI reference](#-cli-reference)
32
+ - [Security and governance notes](#-security-and-governance-notes)
33
+ - [Real-world example](#-real-world-example)
34
+ - [Local development](#-local-development)
35
+ - [Related docs](#-related-docs)
37
36
 
38
- n2n-nexus gives AI assistants a shared coordination workspace. Instead of each assistant keeping its own isolated conversation state, all connected MCP clients can read and write to the same local daemon.
37
+ ## 💡 What is n2n-nexus?
39
38
 
40
- **TL;DR**
41
-
42
- - **Install**: `npx n2n-nexus daemon --port 5688`
43
- - **Protocol**: Model Context Protocol (MCP), adapter-to-daemon HTTP bridge
44
- - **Storage**: local filesystem plus SQLite under `~/.n2n-nexus` by default
45
- - **Best for**: multi-assistant coding sessions, project handoffs, shared decisions, meeting notes, async tasks, project manifests
46
- - **Not for**: cloud team chat, public project management SaaS, source code indexing, vector search, or remote database hosting
47
-
48
- ## Why Use It?
39
+ n2n-nexus gives AI assistants a shared coordination workspace. A long-running local daemon owns project state, meetings, messages, tasks, shared docs, and project assets. Lightweight MCP adapters connect each IDE or assistant to the same daemon.
49
40
 
50
41
  - Coordinate multiple AI assistants working on the same project.
51
- - Share project manifests, internal notes, assets, and topology across IDEs.
52
- - Keep decisions, proposals, and updates in a local meeting/message log.
53
- - Run the daemon once and connect adapters from Windows, WSL, SSH hosts, VMs, or multiple editors.
54
- - Avoid giving each assistant broad filesystem or backend access just to exchange context.
42
+ - Share project manifests, decisions, and assets across IDEs and machines.
43
+ - Keep decisions, proposals, and updates in a local meeting and message log.
44
+ - Run the daemon once; connect adapters from Windows, WSL, SSH hosts, or multiple editors.
45
+ - Avoid giving each assistant broad filesystem or backend access.
55
46
 
56
- ## Architecture
47
+ ## 🏗️ Architecture
57
48
 
58
49
  ```text
59
50
  ┌──────────────────────────────────────┐
@@ -73,7 +64,9 @@ n2n-nexus gives AI assistants a shared coordination workspace. Instead of each a
73
64
  - **MCP adapters are stateless**: each IDE starts an adapter through `npx`, then forwards tool calls to the daemon.
74
65
  - **Cross-environment by design**: point `NEXUS_ENDPOINT` at the same daemon from different machines or shells. The daemon binds to localhost by default; use `--host 0.0.0.0` only on a trusted network.
75
66
 
76
- ## Quick Start
67
+ ## 🚀 Quick start
68
+
69
+ Requires Node.js 22.14+ on the 22.x release line, or Node.js 23.6+.
77
70
 
78
71
  ### 1. Start the daemon
79
72
 
@@ -108,48 +101,23 @@ The adapter can start before the daemon. It loads the daemon tool list once the
108
101
  | Windows IDE to WSL daemon | `http://<WSL-IP>:5688` |
109
102
  | Remote machine | `http://<Server-IP>:5688` |
110
103
 
111
- ## Toolset
112
-
113
- ### Session and context
114
-
115
- - `register_session_context`: declare the active project ID.
116
-
117
- ### Project asset management
118
-
119
- - `sync_project_assets`: submit a project manifest and internal docs.
120
- - `update_project`: patch a project manifest.
121
- - `rename_project`: rename a project ID and update relations.
122
- - `upload_project_asset`: upload binary or text assets.
123
- - `search_projects`: search the project registry.
124
- - `get_global_topology`: inspect project topology and dependencies.
125
-
126
- ### Messaging and collaboration
127
-
128
- - `send_message`: post meeting or global messages.
129
- - `read_messages`: read unread messages incrementally.
130
- - `update_global_strategy`: update the master strategy document.
131
- - `sync_global_doc`: create or update shared docs.
132
-
133
- ### Meeting management
134
-
135
- - `start_meeting`: open a meeting session.
136
- - `end_meeting`: close and lock a meeting.
137
- - `archive_meeting`: move a closed meeting to archive.
138
- - `reopen_meeting`: reactivate a closed or archived meeting.
139
-
140
- ### Async task management
104
+ ## 🛠️ Toolset
141
105
 
142
- - `create_task`: create a background task.
143
- - `get_task`: poll task status and result.
144
- - `list_tasks`: list tasks by status.
145
- - `cancel_task`: cancel a pending or running task.
106
+ The daemon exposes 26 tools, grouped by purpose:
146
107
 
147
- ### Maintenance
108
+ | Group | Tools | Purpose |
109
+ | --- | --- | --- |
110
+ | **Topology** | `get_global_topology` | Read the project list or a detailed per-project subgraph. |
111
+ | **Projects & assets** | `search_projects`, `get_project`, `update_project`, `rename_project`, `sync_project_assets`, `upload_project_asset` | Find, read, update, and rename projects; sync manifests and internal docs; upload binary assets. |
112
+ | **Global docs** | `list_global_docs`, `get_global_doc`, `sync_global_doc`, `get_global_strategy`, `update_global_strategy` | List, read, create and update shared global documents and the master strategy document. |
113
+ | **Messaging** | `send_message`, `read_messages`, `list_events` | Post and read meeting and global messages; read the structured activity log. |
114
+ | **Meetings** | `list_meetings`, `start_meeting`, `end_meeting`, `archive_meeting`, `reopen_meeting` | List, open, close, archive, and reopen meeting sessions. |
115
+ | **Tasks** | `create_task`, `get_task`, `list_tasks`, `cancel_task` | Create and track tasks that assistants manage themselves. |
116
+ | **Maintenance** | `host_maintenance`, `host_delete_project` | Prune or clear the global chat stream; delete a project. |
148
117
 
149
- - `host_maintenance`: prune or clear system logs.
150
- - `host_delete_project`: delete a project and assets.
118
+ See [Tools reference](./docs/TOOLS_REFERENCE.md) for parameter schemas and call examples.
151
119
 
152
- ## Data Storage
120
+ ## 💾 Data storage
153
121
 
154
122
  Default storage root:
155
123
 
@@ -173,10 +141,10 @@ Override with `--root <path>` or `NEXUS_ROOT`.
173
141
  │ ├── internal_blueprint.md
174
142
  │ └── assets/
175
143
  ├── registry.json
176
- └── nexus.db
144
+ └── nexus.db # meetings, global chat, events, tasks, read cursors
177
145
  ```
178
146
 
179
- ## Project ID Conventions
147
+ ## 🏷️ Project ID conventions
180
148
 
181
149
  Project IDs follow `[prefix]_[name]`.
182
150
 
@@ -192,7 +160,7 @@ Project IDs follow `[prefix]_[name]`.
192
160
  | `infra_` | Infrastructure / DevOps | `infra_k8s-config` |
193
161
  | `doc_` | Documentation | `doc_coding-guide` |
194
162
 
195
- ## CLI Reference
163
+ ## 💻 CLI reference
196
164
 
197
165
  ```bash
198
166
  # Start daemon
@@ -209,9 +177,13 @@ NEXUS_ENDPOINT=http://127.0.0.1:5688 n2n-nexus mcp
209
177
  | `NEXUS_ENDPOINT` | Daemon URL for MCP adapter | `http://127.0.0.1:5688` |
210
178
  | `NEXUS_ROOT` | Storage root for daemon | `~/.n2n-nexus` |
211
179
  | `NEXUS_HOST` | Daemon bind host | `127.0.0.1` |
212
- | `NEXUS_INSTANCE_ID` | Override MCP instance ID | auto-generated |
180
+ | `NEXUS_DAEMON_PORT` | Daemon port (overridden by `--port`) | `5688` |
181
+ | `NEXUS_RATE_LIMIT` | Tool calls per second per instance; each client address gets 5x. `0` disables rate limiting | `20` |
182
+ | `NEXUS_RATE_BURST` | Calls an instance may make at once before the per-second limit applies (minimum 1) | `60` |
183
+ | `NEXUS_ALLOWED_HOSTS` | Extra DNS names accepted in the `Host` header (IP literals, `localhost`, `host.docker.internal` are always accepted) | empty |
184
+ | `NEXUS_INSTANCE_ID` | Override MCP instance ID (read cursors and meeting permissions are keyed on it) | `mcp-<hostname>-<hash of working directory>-<IDE name>` |
213
185
 
214
- ## Security and governance notes
186
+ ## 🔐 Security and governance notes
215
187
 
216
188
  - n2n-nexus stores coordination data locally by default.
217
189
  - Do not put secrets, credentials, customer data, or private tokens in meeting messages or project assets unless your local policy allows it.
@@ -219,16 +191,26 @@ NEXUS_ENDPOINT=http://127.0.0.1:5688 n2n-nexus mcp
219
191
  - Exposing the daemon beyond localhost is an operational choice; restrict network access when using remote endpoints.
220
192
  - Treat project manifests and internal docs as potentially sensitive implementation data.
221
193
 
222
- ## Real-world example
194
+ ## 🌐 Real-world example
195
+
196
+ The docs include a recorded multi-agent session that shows n2n-nexus in action. Four AI assistants — running in different IDEs and assigned to different projects — used a shared n2n-nexus daemon to coordinate in real time.
197
+
198
+ What to look for in the session:
223
199
 
224
- The docs include a sample multi-agent session where several AI assistants collaborated on architecture and protocol decisions:
200
+ - **Shared coordination space**: each assistant registered its active project (with the `register_session_context` tool of that release; current versions pass `projectId` explicitly instead), then read and wrote to the same daemon throughout the session.
201
+ - **Structured message categories**: `PROPOSAL`, `DECISION`, `UPDATE`, and `MEETING_START` tags let assistants signal intent, not just post text.
202
+ - **SYSTEM entries**: lines like `[Augment] Synced global doc: edge-sync-protocol-v1` record actual tool calls. That version wrote them into the chat stream; current versions keep them out of the conversation and expose them as structured events through `list_events`.
203
+ - **Async debugging across instances**: when one assistant hit an auth error, others responded with fixes in real time, each from their own IDE context.
204
+ - **Two output formats**: the discussion log is the raw chronological stream; the meeting minutes are a structured summary derived from it, showing how a long session distills into decisions and action items.
225
205
 
226
- | File | Description |
206
+ | File | What it shows |
227
207
  | --- | --- |
228
- | [Meeting Minutes](docs/MEETING_MINUTES_2025-12-29.md) | Structured decisions and test notes |
229
- | [Discussion Log](docs/discussion_2025-12-29_en.md) | Human-readable discussion transcript |
208
+ | [Discussion Log](docs/discussion_2025-12-29.md) | The full chronological stream: proposals, debug exchanges, decisions, and SYSTEM tool entries |
209
+ | [Meeting Minutes](docs/MEETING_MINUTES_2025-12-29.md) | The structured output: participants, key decisions, test results, and action items |
230
210
 
231
- ## Local Development
211
+ ## 🔧 Local development
212
+
213
+ Use Node.js 24 for development, matching CI and the lint toolchain.
232
214
 
233
215
  ```bash
234
216
  git clone https://github.com/n2ns/n2n-nexus.git
@@ -243,44 +225,19 @@ node build/index.js daemon --root /tmp/nexus-test --port 5688
243
225
  NEXUS_ENDPOINT=http://127.0.0.1:5688 node build/index.js mcp
244
226
  ```
245
227
 
246
- ## FAQ
247
-
248
- ### Is n2n-nexus a project management SaaS?
249
-
250
- No. It is a local MCP coordination server for AI assistants. It stores state locally and exposes MCP tools to connected clients.
251
-
252
- ### Does it replace n2n-memory?
253
-
254
- No. `n2n-memory` is repository-local memory for one project. `n2n-nexus` is a shared coordination hub for multiple assistants, project manifests, messages, meetings, and tasks.
255
-
256
- ### Does it work with Claude Desktop, Cursor, VS Code, and other IDEs?
257
-
258
- Yes, when the client supports local MCP command servers. The adapter is started by the IDE and connects to the local daemon through `NEXUS_ENDPOINT`.
259
-
260
- ### Can it coordinate assistants across Windows and WSL?
261
-
262
- Yes. Run the daemon in one environment and point each adapter at it using `NEXUS_ENDPOINT`.
263
-
264
- ### Does it send data to the cloud?
265
-
266
- No cloud service is required. Data is stored under the configured local root. If you expose the daemon to a remote machine, that is your own network configuration.
267
-
268
- ### Is this a vector database or code indexer?
269
-
270
- No. n2n-nexus coordinates project metadata, messages, meetings, docs, tasks, and assets. It is not a semantic code search engine.
271
-
272
- ## Related docs
228
+ ## 📖 Related docs
273
229
 
274
- - [Architecture](./docs/ARCHITECTURE.md)
275
- - [AI Assistant Guide](./docs/ASSISTANT_GUIDE.md)
276
- - [Chinese README](./docs/README_zh.md)
277
- - [Changelog](./CHANGELOG.md)
278
- - [llms.txt](./llms.txt)
230
+ - **[Architecture](./docs/ARCHITECTURE.md)**: System design and daemon-adapter separation explained.
231
+ - **[Tools reference](./docs/TOOLS_REFERENCE.md)**: Full tool list and parameters.
232
+ - **[AI Assistant Guide](./docs/ASSISTANT_GUIDE.md)**: How AI assistants should call tools and stay in project context.
233
+ - **[Changelog](./CHANGELOG.md)**: Version history and release notes.
234
+ - **[Contributing](./CONTRIBUTING.md)**: How to report issues and contribute.
235
+ - **[Security](./SECURITY.md)**: How to report vulnerabilities.
279
236
 
280
- ## License
237
+ ## 📄 License
281
238
 
282
239
  This project is licensed under the [Apache-2.0 License](./LICENSE).
283
240
 
284
241
  ---
285
242
 
286
- Built by N2NS Lab, short for Next-to-Native Systems Lab, Datafrog's open-source lab for practical AI developer tools.
243
+ Built by [N2NS Lab](https://n2ns.com), the open-source lab of [datafrog.io](https://datafrog.io) for practical AI developer tools.
Binary file
Binary file
Binary file
@@ -1,6 +1,22 @@
1
1
  import http from "node:http";
2
2
  import https from "node:https";
3
3
  import { URL } from "node:url";
4
+ /** The daemon could not be reached or answered with something that is not JSON. */
5
+ export class NexusTransportError extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = "NexusTransportError";
9
+ }
10
+ }
11
+ /** The daemon answered, but the tool call itself failed (unknown tool, validation, not found, ...). */
12
+ export class NexusToolError extends Error {
13
+ status;
14
+ constructor(message, status) {
15
+ super(message);
16
+ this.status = status;
17
+ this.name = "NexusToolError";
18
+ }
19
+ }
4
20
  function normalizeEndpoint(endpoint) {
5
21
  const raw = endpoint || process.env.NEXUS_ENDPOINT || "http://127.0.0.1:5688";
6
22
  if (/^https?:\/\//i.test(raw))
@@ -34,30 +50,30 @@ export class NexusClient {
34
50
  let data = "";
35
51
  res.on("data", chunk => { data += chunk; });
36
52
  res.on("end", () => {
53
+ let parsed;
37
54
  try {
38
- const parsed = data ? JSON.parse(data) : {};
39
- if (res.statusCode && res.statusCode >= 400) {
40
- reject(new Error(parsed.error || `HTTP ${res.statusCode}`));
41
- }
42
- else {
43
- resolve(parsed);
44
- }
55
+ parsed = data ? JSON.parse(data) : {};
45
56
  }
46
57
  catch {
47
- reject(new Error(`Invalid JSON response: ${data}`));
58
+ reject(new NexusTransportError(`Invalid JSON response from daemon: ${data.slice(0, 200)}`));
59
+ return;
60
+ }
61
+ const status = res.statusCode ?? 0;
62
+ if (status >= 400) {
63
+ reject(new NexusToolError(parsed.error || `HTTP ${status}`, status));
64
+ }
65
+ else {
66
+ resolve(parsed);
48
67
  }
49
68
  });
50
69
  });
51
- req.on("error", reject);
52
- req.on("timeout", () => req.destroy(new Error("Request timeout")));
70
+ req.on("error", (e) => reject(e instanceof NexusTransportError ? e : new NexusTransportError(e.message)));
71
+ req.on("timeout", () => req.destroy(new NexusTransportError(`Request to ${this.endpoint} timed out after ${this.timeoutMs} ms`)));
53
72
  if (payload)
54
73
  req.write(payload);
55
74
  req.end();
56
75
  });
57
76
  }
58
- async health() {
59
- return this.request("GET", "/health");
60
- }
61
77
  async fetchTools() {
62
78
  const res = await this.request("GET", "/api/tools");
63
79
  return res.tools;
@@ -65,7 +81,7 @@ export class NexusClient {
65
81
  async callTool(name, args, instanceId) {
66
82
  const res = await this.request("POST", "/api/tools/call", { tool: name, args: args || {}, instanceId: instanceId || "unknown" });
67
83
  if (!res.ok)
68
- throw new Error(res.error || "Tool call failed");
84
+ throw new NexusToolError(res.error || "Tool call failed", 200);
69
85
  return res.result;
70
86
  }
71
87
  }
@@ -6,6 +6,3 @@ export function getArg(k) {
6
6
  const i = args.indexOf(k);
7
7
  return i !== -1 && args[i + 1] ? args[i + 1] : "";
8
8
  }
9
- export function hasFlag(k) {
10
- return args.includes(k) || args.includes(k.charAt(1) === "-" ? k : k.substring(0, 2));
11
- }
@@ -4,13 +4,3 @@ import { fileURLToPath } from "url";
4
4
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
5
5
  const pkgPath = path.resolve(__dirname, "../../package.json");
6
6
  export const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
7
- export function getRootPath() {
8
- // Priority: --root CLI arg → NEXUS_ROOT env → ~/.n2n-nexus
9
- const argIndex = process.argv.indexOf("--root");
10
- if (argIndex !== -1 && process.argv[argIndex + 1]) {
11
- return process.argv[argIndex + 1];
12
- }
13
- if (process.env.NEXUS_ROOT)
14
- return process.env.NEXUS_ROOT;
15
- return path.join(process.env.HOME || process.env.USERPROFILE || ".", ".n2n-nexus");
16
- }
@@ -1,7 +1,5 @@
1
1
  /**
2
- * Nexus Network Constants
3
- *
4
- * Centralized configuration for network-related settings.
2
+ * Nexus constants
5
3
  */
6
4
  // Service identification
7
5
  export const SERVICE_NAME = "n2n-nexus";
@@ -9,14 +7,12 @@ export const SERVICE_NAME = "n2n-nexus";
9
7
  // Default to localhost because the daemon exposes unauthenticated local admin APIs.
10
8
  // Use "0.0.0.0" only when you intentionally expose it to another trusted environment.
11
9
  export const NEXUS_HOST = "127.0.0.1";
12
- // Port range for auto-election
13
- export const PORT_RANGE_START = 5688;
14
- export const PORT_RANGE_END = 5800;
15
- // Timeouts (milliseconds)
16
- export const HANDSHAKE_TIMEOUT = 200;
17
- export const HEARTBEAT_INTERVAL = 30000;
18
- // Task cleanup
10
+ // Default daemon port
11
+ export const NEXUS_PORT = 5688;
12
+ // HTTP request body limit for the daemon (base64 asset uploads are the largest payload)
13
+ export const MAX_BODY_BYTES = 16 * 1024 * 1024;
14
+ // Task lifecycle: finished tasks are purged after this age; maintenance runs at this interval
19
15
  export const TASK_CLEANUP_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
16
+ export const TASK_MAINTENANCE_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
20
17
  // File I/O
21
18
  export const FILE_ENCODING = "utf-8";
22
- export const PACKAGE_JSON = "package.json";
@@ -1,41 +1,57 @@
1
1
  import { createDaemonServer } from "./server.js";
2
- import { pkg, getRootPath } from "../config/index.js";
3
- import { NEXUS_HOST } from "../constants.js";
2
+ import { closeDatabase } from "../storage/sqlite.js";
3
+ import { runTaskMaintenance } from "../storage/tasks.js";
4
+ import { TASK_MAINTENANCE_INTERVAL_MS } from "../constants.js";
5
+ import { pkg } from "../config/index.js";
6
+ import { getArg } from "../config/cli.js";
7
+ import { getRootPath } from "../config/paths.js";
8
+ import { NEXUS_HOST, NEXUS_PORT } from "../constants.js";
4
9
  function parsePort() {
5
- const argIndex = process.argv.indexOf("--port");
6
- if (argIndex !== -1) {
7
- const parsed = parseInt(process.argv[argIndex + 1] || "", 10);
10
+ for (const candidate of [getArg("--port"), process.env.NEXUS_DAEMON_PORT]) {
11
+ const parsed = parseInt(candidate || "", 10);
8
12
  if (!isNaN(parsed) && parsed > 0)
9
13
  return parsed;
10
14
  }
11
- const envPort = parseInt(process.env.NEXUS_DAEMON_PORT || process.env.NEXUS_PORT || "", 10);
12
- if (!isNaN(envPort) && envPort > 0)
13
- return envPort;
14
- return 5688;
15
+ return NEXUS_PORT;
15
16
  }
16
17
  function parseHost() {
17
- const argIndex = process.argv.indexOf("--host");
18
- if (argIndex !== -1 && process.argv[argIndex + 1]) {
19
- return process.argv[argIndex + 1];
20
- }
21
- return process.env.NEXUS_HOST || NEXUS_HOST;
18
+ return getArg("--host") || process.env.NEXUS_HOST || NEXUS_HOST;
22
19
  }
23
20
  export async function runDaemon() {
24
21
  const port = parsePort();
25
22
  const host = parseHost();
26
23
  const root = getRootPath();
27
- // Inject root path into CONFIG via env so StorageManager picks it up
24
+ // Publish the resolved root so every storage module (which reads NEXUS_ROOT) agrees with the log line below
28
25
  process.env.NEXUS_ROOT = root;
29
- const { server, storageInfo } = await createDaemonServer({ port, host, version: pkg.version });
26
+ const { server } = await createDaemonServer({ port, host, version: pkg.version });
30
27
  await new Promise((resolve, reject) => {
31
28
  server.once("error", reject);
32
29
  server.listen(port, host, () => {
33
30
  console.error(`[n2n-nexus] Daemon v${pkg.version} listening on http://${host}:${port}`);
34
- console.error(`[n2n-nexus] Storage: ${root} (${storageInfo.storageMode}${storageInfo.isDegraded ? ", degraded" : ""})`);
31
+ console.error(`[n2n-nexus] Storage: ${root}`);
35
32
  resolve();
36
33
  });
37
34
  });
38
- process.once("SIGINT", () => { console.error("[n2n-nexus] Shutting down."); server.close(() => process.exit(0)); });
39
- process.once("SIGTERM", () => { server.close(() => process.exit(0)); });
35
+ // Expire overdue tasks and purge old finished ones at startup and then periodically
36
+ const maintain = () => {
37
+ const { expired, removed } = runTaskMaintenance();
38
+ if (expired || removed)
39
+ console.error(`[n2n-nexus] Task maintenance: ${expired} expired, ${removed} removed.`);
40
+ };
41
+ maintain();
42
+ setInterval(maintain, TASK_MAINTENANCE_INTERVAL_MS).unref();
43
+ // Graceful shutdown: stop accepting connections, let in-flight requests
44
+ // finish, then close SQLite. A second signal exits immediately.
45
+ const shutdown = (signal) => {
46
+ console.error(`[n2n-nexus] ${signal} received, shutting down.`);
47
+ server.close(() => {
48
+ closeDatabase();
49
+ process.exit(0);
50
+ });
51
+ process.once(signal, () => process.exit(1));
52
+ };
53
+ process.once("SIGINT", () => shutdown("SIGINT"));
54
+ process.once("SIGTERM", () => shutdown("SIGTERM"));
55
+ process.on("exit", () => closeDatabase());
40
56
  console.error("[n2n-nexus] Ready. Press Ctrl+C to stop.");
41
57
  }