n2n-nexus 0.9.2 → 0.9.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/CHANGELOG.md +45 -0
- package/CONTRIBUTING.md +3 -1
- package/README.md +61 -241
- package/SECURITY.md +4 -1
- package/build/cli/doctor.js +8 -7
- package/build/cli/hook.js +46 -18
- package/build/cli/install.js +87 -14
- package/build/cli/title.js +126 -0
- package/build/contract.js +22 -2
- package/build/index.js +4 -3
- package/build/server/instance-id.js +2 -2
- package/build/server/nexus.js +6 -2
- package/contract/errors.json +2 -2
- package/contract/hook.json +20 -3
- package/contract/tools.json +565 -17
- package/docs/ARCHITECTURE.md +16 -66
- package/docs/ARCHITECTURE_zh.md +18 -68
- package/docs/ASSISTANT_GUIDE.md +41 -13
- package/docs/ASSISTANT_GUIDE_zh.md +40 -14
- package/docs/CHANGELOG_zh.md +626 -0
- package/docs/CONTRIBUTING_zh.md +51 -0
- package/docs/INSTALL.md +146 -0
- package/docs/INSTALL_zh.md +146 -0
- package/docs/README_zh.md +71 -310
- package/docs/SECURITY_zh.md +40 -0
- package/docs/TOOLS_REFERENCE.md +91 -30
- package/docs/TOOLS_REFERENCE_zh.md +480 -0
- package/docs/USER_GUIDE.md +142 -0
- package/docs/USER_GUIDE_zh.md +142 -0
- package/llms.txt +3 -33
- package/package.json +10 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,52 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
[中文版](docs/CHANGELOG_zh.md)
|
|
4
|
+
|
|
3
5
|
All notable changes to this project will be documented in this file.
|
|
4
6
|
|
|
7
|
+
## [0.9.4] - 2026-10-26
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Contract version 2: the npm client and the host must be upgraded together (`n2n-nexus doctor` reports a mismatch).
|
|
12
|
+
- Chat and meetings identify the conversation, not the MCP adapter instance, when the n2n-nexus hooks are installed. Several conversations of one IDE in one workspace share an instance; they now keep their own unread cursors, and their messages, a meeting's `initiator` and `participants` and the `actor` of `meeting.*` events carry their session id (`claude:<id>` / `codex:<id>`), which works with `send`. Clients without the hooks keep using the instance. If an instance has had a conversation identified within the last 30 minutes and a call of it cannot be identified, the call fails with a retry message instead of reading through the shared cursor. Results say which identity was used (`identity`).
|
|
13
|
+
- `read_messages` no longer returns your own messages in unread mode; a conversation's first unread read of global chat starts at the newest message. New history mode: `latest: true` or `afterId` read without moving a cursor and include your own messages. Each message has `senderName`, and `category` is always present (null without one).
|
|
14
|
+
- Meeting messages are numbered per meeting, from 1: `id` in `read_messages`, `messageId` from `send_message` (now returned for meetings too) and `afterId` in a meeting use that number, so a new meeting no longer continues the numbers of earlier ones.
|
|
15
|
+
|
|
16
|
+
- Meetings have members. `join_meeting(meetingId, mode?, approver?, since?)` and `leave_meeting(meetingId)`; the initiator joins its meeting. News of a meeting reaches its members through the hooks as one line per meeting: active members are woken (or their turn continues), async members see it at their next prompt or tool call, and nobody is told about their own messages. Meeting news is not an automatic turn and still arrives past `maxAutoTurns`, so a meeting runs until it is ended; ending it gives each other member one notice. `list_meetings` shows the members and how many wakes the meeting caused.
|
|
17
|
+
- Decisions by consensus. `send_message(..., draft: true)` publishes a meeting's current draft and fixes its required approvers (members that joined as `required` through the hooks); `stance(meetingId, messageId, endorse | object | withdraw, note?)` takes a stance on it, an objection with its reason. When at least two required approvers endorsed the current draft and nobody objects, the meeting closes by itself with the draft as a `DECISION` (`endReason: consensus`; `end_meeting` records `manual`). Ordinary proposals do not reset stances; a leaving or re-joining approver changes the list, which is posted to the meeting. Required approvers that have not taken a stance after a quiet `stanceReminderMinutes` (new `settings.json` field, default 10) are reminded once. `list_meetings` shows each meeting's `endReason` and current draft with its stances.
|
|
18
|
+
- Direct messages say what they ask: `send` and `reply` take `expectsReply` (false: the notification says no reply is needed and does not ask for one) and `wake` (false: the message never wakes the recipient or holds its turn, and waits for its next prompt or tool call). `read` returns `expectsReply`.
|
|
19
|
+
- Sessions have a readable `title`: the conversation's title as Claude Code or Codex shows it, read by the hooks and shown next to the name in `peers`, introductions, notifications and meeting members. `set_profile({ title })` sets your own (`""` returns to the client's). The name stays the handle for `send`.
|
|
20
|
+
- A direct message is listed in full once; while it stays unread, later notifications only count it.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- A failed `codex queue` no longer loses the wake: the Codex background wait reports the result and the host undoes a wake that was not delivered, and the wait tries again after a delay (1 s, doubling up to 5 minutes), so the same messages and notices wake the conversation again; the failed wake is not counted as an automatic turn. The session no longer stays `working` until it goes stale.
|
|
25
|
+
- A session that goes offline after 30 minutes without activity now tells its `notifyWhenIdle` subscribers, as a session that ends does, also when they only wait in the background.
|
|
26
|
+
- The prompt that starts a woken turn no longer announces again what the wake announced.
|
|
27
|
+
- The Codex SessionEnd hook fits Codex's 3-second limit: `n2n-nexus install codex` writes timeout 3 (run it again to update an existing installation, which removes Codex's "clamping SessionEnd hook timeout" warning), and the SessionEnd request gives up after 1.5 s without a second host lookup.
|
|
28
|
+
- `n2n-nexus uninstall <client>` no longer needs the client on PATH: it removes the hooks from the client's config file anyway. Without the client it skips `<client> mcp remove` and, when the MCP server is still registered, says so and how to remove it (run that command later, or delete the `n2n-nexus` entry from `~/.claude.json` / `config.toml`). `install` still requires the client.
|
|
29
|
+
- Tool calls are rate limited per conversation when the hooks identify it, so conversations that share an adapter instance (one client in one directory) no longer use up each other's budget; without the hooks calls are still limited per instance. The per-address limit is unchanged.
|
|
30
|
+
|
|
31
|
+
## [0.9.3] - 2026-10-24
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- The MCP server updates itself: `n2n-nexus install` registers it as `"<node>" "<npx>" -y n2n-nexus@latest mcp` (npm's npx next to the chosen Node; `PATH` set to a folder whose `node` is that Node, then the system folders), so every new conversation starts the latest published version. The hooks still run the global installation; update them with `npm i -g n2n-nexus@latest`. Run `n2n-nexus install claude` / `n2n-nexus install codex` once to switch an existing installation; the hook commands stay the same, so Codex does not ask to trust them again. `n2n-nexus doctor` checks the new form and reports an MCP command in the old one.
|
|
36
|
+
|
|
37
|
+
- n2n-nexus is for sessions giving each other work: direct message notices, the `read` tool and the assistant guide now tell the receiving model to carry out what a message asks and answer with `reply`, instead of only when its own user asked for it.
|
|
38
|
+
- No limit on automatic turns by default: `maxAutoTurns` in the host's `settings.json` now defaults to `0`, so sessions keep waking each other and carrying out each other's requests without anyone typing. Set a number to cap it again; a `settings.json` that sets `maxAutoTurns` keeps its value.
|
|
39
|
+
- Any MCP adapter instance can end, archive or reopen a meeting, not only the one that started it.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- The Codex background wait loads what a wake needs before it starts waiting, so an upgrade of the package (or a cleared npx cache) during the wait no longer makes the wake fail silently.
|
|
44
|
+
|
|
45
|
+
### Documentation
|
|
46
|
+
|
|
47
|
+
- An installation guide (`docs/INSTALL.md`) covers installing, upgrading and removing the host and the client, including the one-time Codex hook trust; a user guide (`docs/USER_GUIDE.md`) covers using them and holds the command, environment variable, `settings.json` and data directory reference. The README now gives an overview and links to them; each document covers its own topic instead of repeating the others.
|
|
48
|
+
- Every document has a Chinese version with the same sections: new Chinese installation guide, user guide, tools reference, host README, contributing and security docs, and the Chinese changelog now lists every version.
|
|
49
|
+
|
|
5
50
|
## [0.9.2] - 2026-10-24
|
|
6
51
|
|
|
7
52
|
### Fixed
|
package/CONTRIBUTING.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Contributing
|
|
2
2
|
|
|
3
|
+
[中文版](docs/CONTRIBUTING_zh.md)
|
|
4
|
+
|
|
3
5
|
Thanks for helping improve n2n-nexus.
|
|
4
6
|
|
|
5
7
|
## Development Setup
|
|
@@ -42,7 +44,7 @@ NEXUS_ENDPOINT=http://127.0.0.1:5799 node build/index.js mcp
|
|
|
42
44
|
|
|
43
45
|
## Architecture Notes
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
See `docs/ARCHITECTURE.md` for the system design and `host/README.md` for building and releasing the host.
|
|
46
48
|
|
|
47
49
|
## Changelog
|
|
48
50
|
|
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ Local-first MCP coordination hub from N2NS Lab for multi-AI assistant collaborat
|
|
|
11
11
|
[](https://github.com/n2ns/n2n-nexus/blob/main/LICENSE)
|
|
12
12
|
[](https://modelcontextprotocol.io)
|
|
13
13
|
[](https://nodejs.org)
|
|
14
|
+
[](https://datafrog.io)
|
|
14
15
|
|
|
15
16
|
[中文版](./docs/README_zh.md)
|
|
16
17
|
|
|
@@ -22,22 +23,44 @@ n2n-nexus is an open-source Model Context Protocol (MCP) coordination server for
|
|
|
22
23
|
|
|
23
24
|
## 📚 Contents
|
|
24
25
|
|
|
26
|
+
- [Looking for](#-looking-for)
|
|
25
27
|
- [What is n2n-nexus?](#-what-is-n2n-nexus)
|
|
28
|
+
- [Why use it?](#-why-use-it)
|
|
26
29
|
- [Architecture](#️-architecture)
|
|
27
30
|
- [Quick start](#-quick-start)
|
|
28
31
|
- [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
32
|
- [Real-world example](#-real-world-example)
|
|
34
|
-
- [
|
|
35
|
-
- [
|
|
33
|
+
- [FAQ](#-faq)
|
|
34
|
+
- [Documentation](#-documentation)
|
|
35
|
+
|
|
36
|
+
## 🔎 Looking for
|
|
37
|
+
|
|
38
|
+
If you are searching for:
|
|
39
|
+
|
|
40
|
+
- multi-AI assistant collaboration over MCP
|
|
41
|
+
- a local multi-agent collaboration server
|
|
42
|
+
- a local MCP coordination hub
|
|
43
|
+
- shared project context for AI coding assistants
|
|
44
|
+
- a cross-IDE MCP collaboration tool
|
|
45
|
+
- a project registry for AI assistants
|
|
46
|
+
- meetings and task coordination for coding agents
|
|
47
|
+
|
|
48
|
+
n2n-nexus is designed for these cases: shared host state, lightweight MCP adapters, project-aware collaboration, local storage and cross-environment IDE workflows.
|
|
36
49
|
|
|
37
50
|
## 💡 What is n2n-nexus?
|
|
38
51
|
|
|
39
52
|
n2n-nexus gives AI assistants a shared coordination workspace. A long-running local host, `n2n-nexus-host`, owns project state, meetings, messages, tasks, shared docs, and project assets. Lightweight MCP adapters from the `n2n-nexus` npm package connect each IDE or assistant to the same host.
|
|
40
53
|
|
|
54
|
+
**Quick summary**
|
|
55
|
+
|
|
56
|
+
- **Install**: the host `n2n-nexus-host` with Scoop / Homebrew / a Releases archive; the client with `npm i -g n2n-nexus@latest`, then `n2n-nexus install <claude|codex>`
|
|
57
|
+
- **Protocol**: Model Context Protocol (MCP); the adapters reach the host over HTTP
|
|
58
|
+
- **Storage**: one SQLite database in the host's data directory
|
|
59
|
+
- **Good for**: multi-assistant coding sessions, project handoffs, shared decisions, meeting records, task tracking, project manifests
|
|
60
|
+
- **Not for**: cloud team chat, public project management SaaS, source code indexing, vector search or remote database hosting
|
|
61
|
+
|
|
62
|
+
## 🎯 Why use it?
|
|
63
|
+
|
|
41
64
|
- Coordinate multiple AI assistants working on the same project.
|
|
42
65
|
- Share project manifests, decisions, and assets across IDEs and machines.
|
|
43
66
|
- Keep decisions, proposals, and updates in a local meeting and message log.
|
|
@@ -67,94 +90,11 @@ n2n-nexus gives AI assistants a shared coordination workspace. A long-running lo
|
|
|
67
90
|
|
|
68
91
|
## 🚀 Quick start
|
|
69
92
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
| --- | --- |
|
|
74
|
-
| Windows (Scoop) | `scoop bucket add n2ns https://github.com/n2ns/scoop-bucket`, then `scoop install n2ns/n2n-nexus-host` |
|
|
75
|
-
| Windows (winget) | coming soon |
|
|
76
|
-
| macOS (Homebrew) | `brew install n2ns/tap/n2n-nexus-host` |
|
|
77
|
-
| Linux | download `n2n-nexus-host_<version>_linux_<amd64\|arm64>.tar.gz` from [Releases](https://github.com/n2ns/n2n-nexus/releases), extract it and put `n2n-nexus-host` on your `PATH` |
|
|
78
|
-
|
|
79
|
-
[Releases](https://github.com/n2ns/n2n-nexus/releases) (tags `host-v<version>`) has the archives for every platform: `n2n-nexus-host_<version>_<os>_<arch>.zip` for Windows and `.tar.gz` for macOS and Linux, amd64 and arm64.
|
|
80
|
-
|
|
81
|
-
- The binaries are not code-signed yet. On Windows, the first start of `n2n-nexus-host.exe` or `n2n-nexus-host-tray.exe` may show a SmartScreen warning: click **More info → Run anyway**. Homebrew builds the host from source on your Mac, so there is no Gatekeeper prompt.
|
|
82
|
-
- On Windows the package has two programs: `n2n-nexus-host.exe` for commands, and `n2n-nexus-host-tray.exe`, the tray, which opens no console window. `n2n-nexus-host` without arguments starts the tray program and returns.
|
|
83
|
-
- Run `n2n-nexus-host` without arguments to start it as a tray app. The tray menu shows the address it listens on and the number of online sessions, opens the log and data folders, switches LAN access on and off, restarts the host and turns **Start at login** on or off (`n2n-nexus-host autostart on|off` does the same). The first time the tray runs it turns **Start at login** on; after that it stays as you set it. The menu is in Chinese on a Chinese system and in English otherwise.
|
|
84
|
-
- Or run it as a background service that starts at boot, without a tray: `n2n-nexus-host service install` from an elevated terminal (Windows) or with `sudo` (macOS, Linux). The service runs as the user who installed it. Quit the tray first: `service install` refuses while the tray runs the host. While the service runs, a tray started later only controls it.
|
|
85
|
-
- On Linux the tray needs a desktop with StatusNotifierItem (AppIndicator) support; on a server, use `service install`.
|
|
86
|
-
|
|
87
|
-
Verify a download against the release's `SHA256SUMS` and its build provenance:
|
|
88
|
-
|
|
89
|
-
```bash
|
|
90
|
-
sha256sum -c SHA256SUMS --ignore-missing
|
|
91
|
-
gh attestation verify n2n-nexus-host_<version>_<os>_<arch>.tar.gz --repo n2ns/n2n-nexus
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
### 2. Install the client and connect Claude Code or Codex
|
|
95
|
-
|
|
96
|
-
Requires Node.js 22.14+ on the 22.x release line, or Node.js 23.6+.
|
|
97
|
-
|
|
98
|
-
```bash
|
|
99
|
-
npm i -g n2n-nexus
|
|
100
|
-
n2n-nexus install claude # ~/.claude/settings.json (or $CLAUDE_CONFIG_DIR) + claude mcp add --scope user
|
|
101
|
-
n2n-nexus install codex # ~/.codex/hooks.json (or $CODEX_HOME) + codex mcp add
|
|
102
|
-
n2n-nexus doctor # check the installed commands and that the host answers
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
`install` writes the hooks and the MCP server into the client's user configuration.
|
|
106
|
-
|
|
107
|
-
- Codex runs new hooks only after you trust them: open Codex and run `/hooks` once.
|
|
108
|
-
- Pass `--endpoint <url>` to both commands when the host is not at `http://127.0.0.1:5688`, or `--endpoint discover` to find a host on the LAN.
|
|
109
|
-
- On Windows, run `npm i -g` and `install` from a regular terminal, not from the terminal of a packaged app such as Codex: that terminal installs into the app's private copy of `%APPDATA%`, which hides the global install from the app.
|
|
110
|
-
- The hooks and the MCP server command use an absolute Node path, because GUI clients started from the Dock (such as Claude Desktop) do not read your shell's `PATH`. By default `install` writes the `node` found on `PATH` when it is the same file as the Node running `install` (for example `/usr/local/bin/node`, `/opt/homebrew/bin/node`, `~/.local/bin/node` or a version manager's stable entry point), and otherwise the real path of the running Node. `install` prints the path it wrote as `Node: <path>`.
|
|
111
|
-
- Pass `--node <path>` to choose the Node path yourself. It must exist, be executable and satisfy the `engines` range in package.json (`^22.14.0 || >=23.6.0`); otherwise `install` exits with code 1 and writes nothing.
|
|
112
|
-
- After upgrading Node: if `install` warned that the Node path contains a version directory (Homebrew `Cellar`, nvm, fnm, Volta, asdf, `node-v<version>`), run `n2n-nexus install <claude|codex>` again after each upgrade, or pass `--node` with a stable path. `n2n-nexus doctor [claude|codex]` checks that the Node path and the entry script in the installed hook and MCP commands still exist and run, and that the host they talk to answers with the client's contract version; it exits with code 1 when a check fails.
|
|
113
|
-
- `n2n-nexus uninstall <claude|codex>` removes the hooks and the MCP server that `install` added and leaves the rest of the configuration alone.
|
|
114
|
-
- Supported on Linux, WSL and macOS; Codex hooks also support Windows PowerShell. Other MCP clients can use every tool except direct messages.
|
|
115
|
-
|
|
116
|
-
### Other MCP clients
|
|
117
|
-
|
|
118
|
-
```json
|
|
119
|
-
{
|
|
120
|
-
"mcpServers": {
|
|
121
|
-
"n2n-nexus": {
|
|
122
|
-
"command": "npx",
|
|
123
|
-
"args": ["-y", "n2n-nexus", "mcp"],
|
|
124
|
-
"env": {
|
|
125
|
-
"NEXUS_ENDPOINT": "http://127.0.0.1:5688"
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
The adapter can start before the host. It loads the host's tool list once the host becomes reachable. The adapter and the hooks never start a host themselves: when none answers, tool calls fail with a message saying how to start it (or, for a remote endpoint, that it must be started on that machine), and the hooks skip silently. A call the host does not answer in time fails on its own; the host may still complete it.
|
|
133
|
-
|
|
134
|
-
### Direct messages between Claude Code and Codex conversations
|
|
135
|
-
|
|
136
|
-
Each conversation appears in `peers`. A new direct message is announced to the recipient automatically: when the user sends a prompt, after each tool call, and once before the turn ends. The recipient reads the text with `read`. An idle conversation is woken too: when a turn ends, a background hook waits up to an hour for anything new and then starts a turn with it (Claude Code through an `asyncRewake` hook, Codex through `codex queue`), so AI sessions can answer each other without anyone typing. An unattended `claude -p` run is not kept waiting: the hook returns at once there. A Codex conversation is woken only while it is loaded in the running Codex app, and not again after Codex restarts until it ends a turn; see [Architecture](docs/ARCHITECTURE.md#sessions-presence-and-waking-idle-conversations) for the limits. With `listen` on, a conversation that finished its turn keeps waiting for up to about 10 minutes and continues when a message arrives; while it waits, input you type is queued until you press Esc.
|
|
137
|
-
|
|
138
|
-
### LAN access
|
|
93
|
+
1. Install and start the host, `n2n-nexus-host`, with Scoop, Homebrew or an archive from [Releases](https://github.com/n2ns/n2n-nexus/releases): [Installation guide, steps 1–2](docs/INSTALL.md#1-install-the-host).
|
|
94
|
+
2. Install the client and connect Claude Code or Codex (`npm i -g n2n-nexus@latest`, then `n2n-nexus install claude` or `n2n-nexus install codex`): [step 3](docs/INSTALL.md#3-install-the-client-for-claude-code-and-codex).
|
|
95
|
+
3. Codex only: trust the n2n-nexus hooks once with `/hooks`: [step 4](docs/INSTALL.md#4-turn-on-the-codex-hooks-one-time-required).
|
|
139
96
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
- Tray: check **Allow LAN access**. The tray writes `"host": "0.0.0.0"` to `settings.json` and restarts the host. On Windows it adds inbound firewall rules for the TCP port and the UDP discovery port (5688 and 5688 by default) after a UAC prompt; on macOS and Linux it tells you which ports to allow in your firewall.
|
|
143
|
-
- Service: `n2n-nexus-host service install --host 0.0.0.0` writes the address to `settings.json`; allow TCP 5688 and UDP 5688 in the firewall yourself.
|
|
144
|
-
|
|
145
|
-
A host that listens on a non-loopback address answers LAN discovery on UDP `discoveryPort`.
|
|
146
|
-
|
|
147
|
-
### Cross-environment endpoint examples
|
|
148
|
-
|
|
149
|
-
| Scenario | `NEXUS_ENDPOINT` |
|
|
150
|
-
| --- | --- |
|
|
151
|
-
| Same machine | `http://127.0.0.1:5688` |
|
|
152
|
-
| WSL IDE to Windows host | `http://host.docker.internal:5688` |
|
|
153
|
-
| Windows IDE to WSL host | `http://<WSL-IP>:5688` |
|
|
154
|
-
| Remote machine | `http://<Server-IP>:5688` |
|
|
155
|
-
| Find a host on the LAN | `discover` |
|
|
156
|
-
|
|
157
|
-
With `discover`, the adapter and the hooks broadcast a query on UDP `NEXUS_DISCOVERY_PORT` and use the first host that answers; the result is cached in `~/.n2n-nexus/discovered-endpoint` and looked up again when that host cannot be reached. A host answers only when it listens on a non-loopback address (LAN access on), so allow UDP and TCP 5688 through its firewall for the trusted network. When none answers, the client uses `http://127.0.0.1:5688` (a host on this machine) and looks again after a minute.
|
|
97
|
+
Other MCP clients and several machines: see the [Installation guide](docs/INSTALL.md). Using it, troubleshooting and configuration: see the [User guide](docs/USER_GUIDE.md).
|
|
158
98
|
|
|
159
99
|
## 🛠️ Toolset
|
|
160
100
|
|
|
@@ -171,140 +111,8 @@ The host exposes 33 tools, grouped by purpose:
|
|
|
171
111
|
| **Tasks** | `create_task`, `get_task`, `list_tasks`, `update_task`, `cancel_task` | Create, update and track tasks that assistants manage themselves. |
|
|
172
112
|
| **Maintenance** | `host_maintenance`, `host_delete_project` | Prune or clear the global chat stream; delete a project. |
|
|
173
113
|
|
|
174
|
-
Direct messages need the hooks from `n2n-nexus install`. Notifications carry only the sender and the first line; the text arrives through `read`, as tool output, so a message from another AI session never reaches the model as a system instruction. Unread messages expire after 24 hours and the sender is told. See the [assistant workflow](docs/ASSISTANT_GUIDE.md#direct-conversations).
|
|
175
|
-
|
|
176
114
|
See [Tools reference](./docs/TOOLS_REFERENCE.md) for parameter schemas and call examples.
|
|
177
115
|
|
|
178
|
-
## 💾 Data storage
|
|
179
|
-
|
|
180
|
-
The host keeps all coordination data in one SQLite database in its data directory:
|
|
181
|
-
|
|
182
|
-
| Platform | Data directory | Log directory |
|
|
183
|
-
| --- | --- | --- |
|
|
184
|
-
| Windows | `%LOCALAPPDATA%\n2n-nexus` | `%LOCALAPPDATA%\n2n-nexus\logs` |
|
|
185
|
-
| macOS | `~/Library/Application Support/n2n-nexus` | `~/Library/Logs/n2n-nexus` |
|
|
186
|
-
| Linux | `$XDG_DATA_HOME/n2n-nexus` (default `~/.local/share/n2n-nexus`) | `$XDG_STATE_HOME/n2n-nexus` (default `~/.local/state/n2n-nexus`) |
|
|
187
|
-
|
|
188
|
-
Override the data directory with `--root <path>` or `NEXUS_ROOT`.
|
|
189
|
-
|
|
190
|
-
```text
|
|
191
|
-
<data directory>/
|
|
192
|
-
├── nexus.db # projects, assets, docs, meetings, global chat, events, tasks, read cursors, conversations, direct messages
|
|
193
|
-
├── settings.json # host settings (see below)
|
|
194
|
-
└── host.lock # one host per data directory
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
The background service, and the tray while it runs the host itself, log to `host.log` in the log directory (10 MB per file, 3 old files kept); the tray's own lines go to `tray.log` there; `n2n-nexus-host serve` logs to standard error. Every hour the host deletes events, read or expired direct messages and delivered notices older than 30 days, and tasks 7 days after they finished.
|
|
198
|
-
|
|
199
|
-
Data of n2n-nexus 0.x (`~/.n2n-nexus`) is not imported: the host does not read it and leaves it in place.
|
|
200
|
-
|
|
201
|
-
### settings.json
|
|
202
|
-
|
|
203
|
-
Read when the host starts; command-line flags override it. A missing file or field uses the default.
|
|
204
|
-
|
|
205
|
-
| Field | Description | Default |
|
|
206
|
-
| --- | --- | --- |
|
|
207
|
-
| `host` | Address to listen on (an IPv4 address listens on IPv4 only, an IPv6 address on IPv6 only); `0.0.0.0` makes the host reachable from the LAN | `"127.0.0.1"` |
|
|
208
|
-
| `port` | TCP port | `5688` |
|
|
209
|
-
| `discoveryPort` | UDP port of LAN discovery, answered only when `host` is not a loopback address | `5688` |
|
|
210
|
-
| `allowedHosts` | Extra DNS names accepted in the `Host` header (IP literals, `localhost` and `host.docker.internal` are always accepted) | `[]` |
|
|
211
|
-
| `rateLimit` | Tool calls per second per instance; each client address gets 5x. Hooks have their own budget at the same rate per conversation and 5x per client address. `0` disables rate limiting | `20` |
|
|
212
|
-
| `rateBurst` | Calls an instance may make at once before the per-second limit applies (minimum 1) | `60` |
|
|
213
|
-
| `maxAutoTurns` | Automatic turns a conversation may take between its user's prompts (wakes, listening and Stops held open for messages); past it, messages wait until the user prompts. `0` disables the limit | `8` |
|
|
214
|
-
|
|
215
|
-
After editing the file, restart the host (tray **Restart**, or restart `serve` or the service).
|
|
216
|
-
|
|
217
|
-
## 🏷️ Project ID conventions
|
|
218
|
-
|
|
219
|
-
Project IDs follow `[prefix]_[name]`.
|
|
220
|
-
|
|
221
|
-
| Prefix | Category | Example |
|
|
222
|
-
| --- | --- | --- |
|
|
223
|
-
| `web_` | Websites | `web_datafrog.io` |
|
|
224
|
-
| `api_` | Backend services | `api_user-auth` |
|
|
225
|
-
| `mcp_` | MCP servers | `mcp_nexus` |
|
|
226
|
-
| `lib_` | Libraries / SDKs | `lib_crypto-core` |
|
|
227
|
-
| `chrome_` | Chrome extensions | `chrome_evisa-helper` |
|
|
228
|
-
| `vscode_` | VS Code extensions | `vscode_super-theme` |
|
|
229
|
-
| `desktop_` | Desktop apps | `desktop_main-hub` |
|
|
230
|
-
| `infra_` | Infrastructure / DevOps | `infra_k8s-config` |
|
|
231
|
-
| `doc_` | Documentation | `doc_coding-guide` |
|
|
232
|
-
|
|
233
|
-
## 💻 CLI reference
|
|
234
|
-
|
|
235
|
-
### n2n-nexus-host
|
|
236
|
-
|
|
237
|
-
```bash
|
|
238
|
-
# Tray app (default)
|
|
239
|
-
n2n-nexus-host
|
|
240
|
-
|
|
241
|
-
# Run in the foreground without a tray; flags override settings.json (--port 0 picks a free port)
|
|
242
|
-
n2n-nexus-host serve [--host 127.0.0.1] [--port 5688] [--root <dir>]
|
|
243
|
-
|
|
244
|
-
# Background service that starts at boot (elevated terminal on Windows, sudo on macOS / Linux).
|
|
245
|
-
# install refuses while the tray runs the host (quit it first; reopened, it controls the service),
|
|
246
|
-
# writes --host / --port to settings.json, stops a host running for the data directory,
|
|
247
|
-
# registers and starts the service, and reports what answers on the port.
|
|
248
|
-
# uninstall removes the service and stops its host; a tray or a foreground serve keeps running.
|
|
249
|
-
n2n-nexus-host service install [--host <addr>] [--port <port>] [--root <dir>]
|
|
250
|
-
n2n-nexus-host service uninstall [--root <dir>]
|
|
251
|
-
n2n-nexus-host service status [--host <addr>] [--port <port>] [--root <dir>]
|
|
252
|
-
|
|
253
|
-
# Start the tray at login, or stop doing so
|
|
254
|
-
n2n-nexus-host autostart on|off
|
|
255
|
-
|
|
256
|
-
n2n-nexus-host version
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
What `service install` registers, and where `autostart on` registers the tray:
|
|
260
|
-
|
|
261
|
-
| Platform | Service (starts at boot, runs as the installing user, restarts the host 5 s after it exits) | Tray at login |
|
|
262
|
-
| --- | --- | --- |
|
|
263
|
-
| Windows | Scheduled task `n2n-nexus-host` | `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, value `n2n-nexus-host` |
|
|
264
|
-
| macOS | `/Library/LaunchDaemons/io.n2ns.n2n-nexus-host.plist` | `~/Library/LaunchAgents/io.n2ns.n2n-nexus-host.tray.plist` |
|
|
265
|
-
| Linux | `/etc/systemd/system/n2n-nexus-host.service` | `~/.config/autostart/n2n-nexus-host.desktop` |
|
|
266
|
-
|
|
267
|
-
Under `sudo`, the service uses the default data and log directories of the user who ran `sudo`, unless `--root` names another data directory. Only one host runs per data directory: a second one exits with "another n2n-nexus-host is running".
|
|
268
|
-
|
|
269
|
-
### n2n-nexus (client)
|
|
270
|
-
|
|
271
|
-
```bash
|
|
272
|
-
# Start the MCP adapter (launched by the IDE)
|
|
273
|
-
NEXUS_ENDPOINT=http://127.0.0.1:5688 n2n-nexus mcp [--id <instance id>]
|
|
274
|
-
|
|
275
|
-
# Install / remove the hooks and MCP server for Claude Code or Codex
|
|
276
|
-
n2n-nexus install <claude|codex> [--endpoint <url>] [--node <path>]
|
|
277
|
-
n2n-nexus uninstall <claude|codex>
|
|
278
|
-
|
|
279
|
-
# Check that the installed hook and MCP commands still run and that the host answers
|
|
280
|
-
# with the client's contract version (exit 1 when not)
|
|
281
|
-
n2n-nexus doctor [claude|codex]
|
|
282
|
-
|
|
283
|
-
# Hook entry point (written into the client configuration by install)
|
|
284
|
-
n2n-nexus hook <claude|codex> [--endpoint <url>] [--wait]
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
### Environment variables
|
|
288
|
-
|
|
289
|
-
Client (`n2n-nexus`):
|
|
290
|
-
|
|
291
|
-
| Variable | Description | Default |
|
|
292
|
-
| --- | --- | --- |
|
|
293
|
-
| `NEXUS_ENDPOINT` | Host URL for the MCP adapter, the hooks and `doctor`, or `discover` to find one on the LAN | `http://127.0.0.1:5688` |
|
|
294
|
-
| `NEXUS_DISCOVERY_PORT` | UDP port the client queries with `discover` | `5688` |
|
|
295
|
-
| `NEXUS_INSTANCE_ID` | Override the MCP instance ID (read cursors and meeting permissions are keyed on it) | `mcp-<hostname>-<hash of working directory>-<IDE name>` |
|
|
296
|
-
| `NEXUS_CODEX_COMMAND` | Command the Codex background wait runs as `<command> queue ...` | `codex` |
|
|
297
|
-
|
|
298
|
-
Host (`n2n-nexus-host`): only `NEXUS_ROOT`, the data directory (`--root` overrides it). Every other host setting is in `settings.json`, which the tray, the service and the command line share.
|
|
299
|
-
|
|
300
|
-
## 🔐 Security and governance notes
|
|
301
|
-
|
|
302
|
-
- n2n-nexus stores coordination data locally by default.
|
|
303
|
-
- Do not put secrets, credentials, customer data, or private tokens in meeting messages or project assets unless your local policy allows it.
|
|
304
|
-
- Destructive tools such as project deletion should be used through explicit review workflows.
|
|
305
|
-
- Exposing the host beyond localhost is an operational choice; restrict network access when using remote endpoints.
|
|
306
|
-
- Treat project manifests and internal docs as potentially sensitive implementation data.
|
|
307
|
-
|
|
308
116
|
## 🌐 Real-world example
|
|
309
117
|
|
|
310
118
|
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 host to coordinate in real time.
|
|
@@ -322,33 +130,45 @@ What to look for in the session:
|
|
|
322
130
|
| [Discussion Log](docs/discussion_2025-12-29.md) | The full chronological stream: proposals, debug exchanges, decisions, and SYSTEM tool entries |
|
|
323
131
|
| [Meeting Minutes](docs/MEETING_MINUTES_2025-12-29.md) | The structured output: participants, key decisions, test results, and action items |
|
|
324
132
|
|
|
325
|
-
##
|
|
133
|
+
## ❓ FAQ
|
|
326
134
|
|
|
327
|
-
|
|
135
|
+
### Is n2n-nexus a project management SaaS?
|
|
328
136
|
|
|
329
|
-
|
|
330
|
-
git clone https://github.com/n2ns/n2n-nexus.git
|
|
331
|
-
cd n2n-nexus
|
|
332
|
-
npm install
|
|
333
|
-
npm run build
|
|
137
|
+
No. It is a local MCP coordination server for AI assistants; its state stays local and is exposed to clients through MCP tools.
|
|
334
138
|
|
|
335
|
-
|
|
336
|
-
go run ./host/cmd/n2n-nexus-host serve --port 5799 --root /tmp/nexus-test
|
|
139
|
+
### Does it replace n2n-memory?
|
|
337
140
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
141
|
+
No. `n2n-memory` is a local memory tool for a single repository. `n2n-nexus` is a multi-assistant coordination hub for project manifests, messages, meetings, tasks and shared docs.
|
|
142
|
+
|
|
143
|
+
### Does it support Claude Desktop, Cursor, VS Code and other clients?
|
|
144
|
+
|
|
145
|
+
Yes, as long as the client can run a local MCP command server. The IDE starts the adapter, and the adapter reaches the local host through `NEXUS_ENDPOINT`. In the Claude desktop app, register it for Claude Code (`n2n-nexus install claude`) only, not in `claude_desktop_config.json`: the app would bridge it into the Code tab and drop the conversation identity ([User guide, section 2](docs/USER_GUIDE.md#2-claude-desktop-app)).
|
|
146
|
+
|
|
147
|
+
### Can Windows and WSL work together?
|
|
148
|
+
|
|
149
|
+
Yes. Run the host in one environment and point every adapter at it with `NEXUS_ENDPOINT`.
|
|
150
|
+
|
|
151
|
+
### Does it send data to the cloud?
|
|
152
|
+
|
|
153
|
+
No cloud service is needed. Data stays in the configured local directory. Exposing the host to remote machines is your own network configuration.
|
|
154
|
+
|
|
155
|
+
### Is it a vector database or a source code indexer?
|
|
341
156
|
|
|
342
|
-
|
|
157
|
+
No. n2n-nexus coordinates project metadata, messages, meetings, docs, tasks and assets; it does no semantic code search.
|
|
343
158
|
|
|
344
|
-
## 📖
|
|
159
|
+
## 📖 Documentation
|
|
345
160
|
|
|
161
|
+
- **[Installation guide](./docs/INSTALL.md)**: Installing, upgrading and removing the host and the client; Codex hook trust, other MCP clients, several machines.
|
|
162
|
+
- **[User guide](./docs/USER_GUIDE.md)**: Using n2n-nexus: the tray, direct messages, troubleshooting; commands, environment variables, `settings.json`, data and logs.
|
|
346
163
|
- **[Architecture](./docs/ARCHITECTURE.md)**: System design and host-client separation explained.
|
|
347
164
|
- **[Tools reference](./docs/TOOLS_REFERENCE.md)**: Full tool list and parameters.
|
|
348
165
|
- **[AI Assistant Guide](./docs/ASSISTANT_GUIDE.md)**: How AI assistants should call tools and stay in project context.
|
|
166
|
+
- **[Host development](./host/README.md)**: Building, testing and releasing the host.
|
|
167
|
+
- **[Contributing](./CONTRIBUTING.md)**: Development setup, and how to report issues and contribute.
|
|
349
168
|
- **[Changelog](./CHANGELOG.md)**: Version history and release notes.
|
|
350
|
-
- **[
|
|
351
|
-
- **[
|
|
169
|
+
- **[Security](./SECURITY.md)**: Security model, and how to report vulnerabilities.
|
|
170
|
+
- **[Chinese README](./docs/README_zh.md)**: This page in Chinese; every document above has a Chinese version.
|
|
171
|
+
- **[llms.txt](./llms.txt)**: Index of the docs for AI models.
|
|
352
172
|
|
|
353
173
|
## 📄 License
|
|
354
174
|
|
package/SECURITY.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Security Policy
|
|
2
2
|
|
|
3
|
+
[中文版](docs/SECURITY_zh.md)
|
|
4
|
+
|
|
3
5
|
## Supported Versions
|
|
4
6
|
|
|
5
7
|
Security fixes target the latest published versions of n2n-nexus (npm client) and n2n-nexus-host.
|
|
@@ -30,8 +32,9 @@ Key boundaries to be aware of:
|
|
|
30
32
|
|
|
31
33
|
- **Host network exposure**: The host binds to `127.0.0.1` by default. LAN access (the tray's **Allow LAN access**, `"host": "0.0.0.0"` in `settings.json`, or `--host 0.0.0.0`) exposes it to your local network, and it then answers LAN discovery on UDP `discoveryPort`. There is no authentication layer in the open-source baseline — only expose the host beyond localhost on networks you control.
|
|
32
34
|
- **Browser requests**: The host has no authentication, so it refuses requests that carry a browser `Origin` header or a cross-site `Sec-Fetch-Site` header (403), requires `Content-Type: application/json` on POST (415) and caps request bodies at 16 MB (413). It also rejects any `Host` header that is not an IP literal, `localhost`, `host.docker.internal` or a name listed in `allowedHosts` in `settings.json`, which closes DNS-rebinding attacks where a page reaches the host as a same-origin request under the attacker's domain. MCP adapters and scripts are unaffected.
|
|
33
|
-
- **Rate limiting**: Tool calls are limited per instance ID (default 20/s, burst 60, set with `rateLimit` / `rateBurst` in `settings.json`) and, because instance IDs are self-declared, per client address at five times that. Tool calls that name
|
|
35
|
+
- **Rate limiting**: Tool calls are limited per conversation, or per instance ID when no conversation is identified (default 20/s, burst 60, set with `rateLimit` / `rateBurst` in `settings.json`) and, because instance and session IDs are self-declared, per client address at five times that. Tool calls that name neither a conversation nor an instance ID are limited only per address. Hooks (`POST /api/hook`) are limited per conversation and per client address in buckets of their own. Over the limit the host answers `429` with `Retry-After`; `/health` and `GET /api/tools` are exempt.
|
|
34
36
|
- **Malformed requests**: Invalid request URLs or Host headers return 400 without terminating the host. Validly formed requests still pass through the browser and Host checks above.
|
|
35
37
|
- **Sensitive content in coordination data**: Project manifests, internal docs, meeting messages, and global documents are stored in plaintext in the host's local database. Do not store secrets, credentials, or customer data in these fields.
|
|
38
|
+
- **Destructive tools**: Destructive tools such as project deletion should be used through explicit review workflows.
|
|
36
39
|
- **Project assets**: Binary assets uploaded via `upload_project_asset` are stored in the host's database in its data directory. Treat them as local files with the same access controls.
|
|
37
40
|
- **API key for MCP**: The MCP adapter connects to the host over plain HTTP. If you expose the host to other machines, consider adding a reverse proxy with TLS in front of it.
|
package/build/cli/doctor.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `n2n-nexus doctor [claude|codex]`: check that the Node path and the entry
|
|
3
|
-
* script in the hook commands and
|
|
4
|
-
*
|
|
3
|
+
* script in the hook commands, and the Node path and npm's npx script in the
|
|
4
|
+
* MCP server command (`"<node>" "<npx>" -y n2n-nexus@latest mcp`), that
|
|
5
|
+
* `install` wrote still exist and run, that the host those commands talk to answers
|
|
5
6
|
* GET /health as n2n-nexus-host with this client's contract version, and say
|
|
6
7
|
* how to fix what does not. Without a client both are checked; a client with
|
|
7
8
|
* nothing installed is only reported when it was named. Exit code 1 when a
|
|
@@ -13,7 +14,7 @@ import { CONTRACT_VERSION, HOOK_CLIENTS, MCP_SERVER_NAME, isHookClient, noDaemon
|
|
|
13
14
|
import { NexusClient, NexusToolError } from "../client/nexus-client.js";
|
|
14
15
|
import { LOCAL_ENDPOINT, resolveEndpoint } from "../client/discovery.js";
|
|
15
16
|
import { pkg } from "../config/index.js";
|
|
16
|
-
import { VERSIONED_NODE_DIR, configFile, isOurs, parseVersion, readConfig, satisfiesEngines } from "./install.js";
|
|
17
|
+
import { MCP_PACKAGE, VERSIONED_NODE_DIR, configFile, isOurs, parseVersion, readConfig, satisfiesEngines } from "./install.js";
|
|
17
18
|
import { mcpConfigFile, readMcpCommand } from "./mcp-config.js";
|
|
18
19
|
const ok = (text) => ({ ok: true, text });
|
|
19
20
|
const bad = (text) => ({ ok: false, text });
|
|
@@ -131,11 +132,11 @@ function doctorClient(client, endpoints, defaultEndpoint) {
|
|
|
131
132
|
}
|
|
132
133
|
else {
|
|
133
134
|
report(`MCP Node ${mcp.command}`, checkNode(mcp.command));
|
|
134
|
-
const
|
|
135
|
-
if (
|
|
136
|
-
report(`MCP
|
|
135
|
+
const [npx, ...rest] = mcp.args;
|
|
136
|
+
if (npx && rest.join(" ") === `-y ${MCP_PACKAGE} mcp`)
|
|
137
|
+
report(`MCP npx ${npx}`, checkEntry(npx));
|
|
137
138
|
else
|
|
138
|
-
report(`MCP command in ${mcpFile}`, bad("
|
|
139
|
+
report(`MCP command in ${mcpFile}`, bad(`not in the form "<node>" "<npx>" -y ${MCP_PACKAGE} mcp`));
|
|
139
140
|
}
|
|
140
141
|
if (failed) {
|
|
141
142
|
lines.push(` fix: run "n2n-nexus install ${client}" again (add --node <path> to use a stable Node path such as /usr/local/bin/node).`);
|
package/build/cli/hook.js
CHANGED
|
@@ -2,15 +2,19 @@
|
|
|
2
2
|
* `n2n-nexus hook <claude|codex> [--endpoint <url>] [--wait]`: forward one client
|
|
3
3
|
* hook event to the daemon and print its output, or with `--wait` run the
|
|
4
4
|
* background wait that wakes an idle conversation. Runs on every tool call, so
|
|
5
|
-
* it loads only node built-ins (no MCP SDK
|
|
5
|
+
* it loads only node built-ins (no MCP SDK or zod; node:sqlite only to read a
|
|
6
|
+
* Codex conversation's title, see title.ts), and every failure
|
|
6
7
|
* is silent: exit 0 with nothing on stdout, so the client carries on unaffected.
|
|
7
8
|
*/
|
|
8
9
|
import { spawn } from "node:child_process";
|
|
9
|
-
import { BACKGROUND_WAIT_EVENT, HOOK_INPUT_FIELDS, HOOK_REQUEST_TIMEOUT_MS, HOOK_STOP_REQUEST_TIMEOUT_MS, WAIT_HOOK_TIMEOUT_SEC, noDaemonHint, isHookClient } from "../contract.js";
|
|
10
|
+
import { BACKGROUND_WAIT_EVENT, WAKE_RESULT_EVENT, HOOK_INPUT_FIELDS, HOOK_REQUEST_TIMEOUT_MS, HOOK_STOP_REQUEST_TIMEOUT_MS, SESSION_END_REQUEST_TIMEOUT_MS, WAIT_HOOK_TIMEOUT_SEC, WAKE_RETRY_FIRST_DELAY_MS, WAKE_RETRY_MAX_DELAY_MS, noDaemonHint, isHookClient } from "../contract.js";
|
|
10
11
|
import { NexusClient, NexusTransportError, REQUEST_TIMEOUT_CODE } from "../client/nexus-client.js";
|
|
11
12
|
import { DISCOVER_ENDPOINT, LOCAL_ENDPOINT, resolveEndpoint } from "../client/discovery.js";
|
|
13
|
+
import { readTitle } from "./title.js";
|
|
12
14
|
/** Errors after which a discovered daemon is looked up again: it was never reached, so resending is safe. */
|
|
13
15
|
const UNREACHABLE_CODES = new Set(["ECONNREFUSED", "EHOSTUNREACH", "ENETUNREACH", "ETIMEDOUT", "EADDRNOTAVAIL"]);
|
|
16
|
+
/** Events whose forwarded input carries the conversation's title. */
|
|
17
|
+
const TITLE_EVENTS = new Set(["SessionStart", "UserPromptSubmit", "Stop"]);
|
|
14
18
|
function isPlainObject(value) {
|
|
15
19
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16
20
|
}
|
|
@@ -38,7 +42,15 @@ async function forward(argv) {
|
|
|
38
42
|
if (Object.prototype.hasOwnProperty.call(stdin, field))
|
|
39
43
|
input[field] = stdin[field];
|
|
40
44
|
}
|
|
41
|
-
|
|
45
|
+
// The title is read only where the host updates it, not on the frequent tool events.
|
|
46
|
+
if (typeof stdin.hook_event_name === "string" && TITLE_EVENTS.has(stdin.hook_event_name)) {
|
|
47
|
+
const title = await readTitle(client, stdin);
|
|
48
|
+
if (title !== undefined)
|
|
49
|
+
input.title = title;
|
|
50
|
+
}
|
|
51
|
+
// SessionEnd must finish within Codex's 3 s hook limit (discovery included): a short budget and a single attempt.
|
|
52
|
+
const sessionEnd = stdin.hook_event_name === "SessionEnd";
|
|
53
|
+
const timeoutMs = stdin.hook_event_name === "Stop" ? HOOK_STOP_REQUEST_TIMEOUT_MS : sessionEnd ? SESSION_END_REQUEST_TIMEOUT_MS : HOOK_REQUEST_TIMEOUT_MS;
|
|
42
54
|
/** Send to `endpoint`; nothing listening there is reported with what to do about it. */
|
|
43
55
|
const send = async (endpoint) => {
|
|
44
56
|
try {
|
|
@@ -60,7 +72,7 @@ async function forward(argv) {
|
|
|
60
72
|
// A timed-out Stop may have been waiting for messages on purpose, so only other events count a timeout.
|
|
61
73
|
const code = error instanceof NexusTransportError ? error.code ?? "" : "";
|
|
62
74
|
const unreachable = UNREACHABLE_CODES.has(code) || (code === REQUEST_TIMEOUT_CODE && stdin.hook_event_name !== "Stop");
|
|
63
|
-
if (configured !== DISCOVER_ENDPOINT || !unreachable)
|
|
75
|
+
if (configured !== DISCOVER_ENDPOINT || !unreachable || sessionEnd)
|
|
64
76
|
throw error;
|
|
65
77
|
const found = await resolveEndpoint(configured, { fresh: true });
|
|
66
78
|
if (found === endpoint)
|
|
@@ -79,8 +91,8 @@ async function forward(argv) {
|
|
|
79
91
|
/** The background wait ends before the client's hook timeout would kill it during a request. */
|
|
80
92
|
const WAIT_DEADLINE_MS = (WAIT_HOOK_TIMEOUT_SEC - 10) * 1000;
|
|
81
93
|
/**
|
|
82
|
-
* Ask the daemon until something arrives for the session (the
|
|
83
|
-
*
|
|
94
|
+
* Ask the daemon until something arrives for the session (the wake), the
|
|
95
|
+
* session becomes active again or the deadline passes (null).
|
|
84
96
|
*/
|
|
85
97
|
export async function waitForWake(client, sessionId, endpoint, deadlineMs = WAIT_DEADLINE_MS) {
|
|
86
98
|
const nexus = new NexusClient({ endpoint });
|
|
@@ -89,8 +101,8 @@ export async function waitForWake(client, sessionId, endpoint, deadlineMs = WAIT
|
|
|
89
101
|
while (Date.now() < deadline) {
|
|
90
102
|
const response = await nexus.hook({ client, input: { hook_event_name: BACKGROUND_WAIT_EVENT, session_id: sessionId } }, HOOK_STOP_REQUEST_TIMEOUT_MS);
|
|
91
103
|
const output = isPlainObject(response) ? response.output : null;
|
|
92
|
-
if (isPlainObject(output) && typeof output.wake === "string")
|
|
93
|
-
return output.wake;
|
|
104
|
+
if (isPlainObject(output) && typeof output.wake === "string" && typeof output.dispatchId === "string")
|
|
105
|
+
return { text: output.wake, dispatchId: output.dispatchId };
|
|
94
106
|
if (!(isPlainObject(output) && output.timeout === true))
|
|
95
107
|
return null;
|
|
96
108
|
}
|
|
@@ -101,7 +113,11 @@ export async function waitForWake(client, sessionId, endpoint, deadlineMs = WAIT
|
|
|
101
113
|
* asyncRewake hook: exit code 2 with the notification on stderr wakes the
|
|
102
114
|
* conversation. Codex has no such hook, so its Stop hook starts a detached copy
|
|
103
115
|
* (`--session <id>`) and returns at once; the copy hands the notification to
|
|
104
|
-
* `codex queue` (NEXUS_CODEX_COMMAND, default `codex`), which starts a turn
|
|
116
|
+
* `codex queue` (NEXUS_CODEX_COMMAND, default `codex`), which starts a turn,
|
|
117
|
+
* and reports to the host whether it did (WAKE_RESULT_EVENT): after a failed
|
|
118
|
+
* queue the host undoes the wake and the copy waits again after a delay that
|
|
119
|
+
* doubles on each failure, so the same messages wake the session on a later
|
|
120
|
+
* try without retrying a broken `codex` in a loop.
|
|
105
121
|
*/
|
|
106
122
|
async function runWait(argv) {
|
|
107
123
|
const client = argv[0];
|
|
@@ -124,17 +140,29 @@ async function runWait(argv) {
|
|
|
124
140
|
return 0;
|
|
125
141
|
}
|
|
126
142
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
143
|
+
// Load what a wake needs before waiting: the wait lasts up to an hour, and an
|
|
144
|
+
// upgrade or a cleared npx cache may remove the package's files meanwhile.
|
|
145
|
+
const { spawnClientSync } = client === "codex" ? await import("./client-command.js") : { spawnClientSync: undefined };
|
|
146
|
+
const endpoint = await resolveEndpoint(configured);
|
|
147
|
+
const deadline = Date.now() + WAIT_DEADLINE_MS;
|
|
148
|
+
for (let delay = WAKE_RETRY_FIRST_DELAY_MS;; delay = Math.min(delay * 2, WAKE_RETRY_MAX_DELAY_MS)) {
|
|
149
|
+
const wake = await waitForWake(client, sessionId, endpoint, deadline - Date.now());
|
|
150
|
+
if (wake === null)
|
|
151
|
+
return 0;
|
|
152
|
+
if (!spawnClientSync) {
|
|
153
|
+
await new Promise((resolve) => process.stderr.write(wake.text + "\n", () => resolve()));
|
|
154
|
+
return 2;
|
|
155
|
+
}
|
|
131
156
|
// Never through cmd.exe: the text comes from another session and spans several lines.
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
157
|
+
const queued = spawnClientSync(process.env.NEXUS_CODEX_COMMAND || "codex", ["queue", "--thread", sessionId, "--message", wake.text], { stdio: "ignore", windowsHide: true });
|
|
158
|
+
const ok = !queued.error && queued.status === 0;
|
|
159
|
+
const input = { hook_event_name: WAKE_RESULT_EVENT, session_id: sessionId, dispatch_id: wake.dispatchId, ok };
|
|
160
|
+
const reported = await new NexusClient({ endpoint }).hook({ client, input }, HOOK_REQUEST_TIMEOUT_MS).then(() => true, () => false);
|
|
161
|
+
// Unknown result (no report): the host keeps the wake, and waiting again would deliver nothing.
|
|
162
|
+
if (ok || !reported || Date.now() + delay >= deadline)
|
|
163
|
+
return 0;
|
|
164
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
135
165
|
}
|
|
136
|
-
await new Promise((resolve) => process.stderr.write(text + "\n", () => resolve()));
|
|
137
|
-
return 2;
|
|
138
166
|
}
|
|
139
167
|
/** Run the hook command; resolves to exit code 0, or 2 when a background wait wakes a Claude conversation. */
|
|
140
168
|
export async function runHook(argv) {
|