n2n-nexus 0.8.2 → 0.9.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 (53) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/CONTRIBUTING.md +49 -0
  3. package/README.md +157 -77
  4. package/SECURITY.md +37 -0
  5. package/build/cli/client-command.js +58 -0
  6. package/build/cli/doctor.js +58 -8
  7. package/build/cli/entry.js +5 -0
  8. package/build/cli/hook.js +104 -16
  9. package/build/cli/install.js +37 -13
  10. package/build/cli/mcp-config.js +16 -12
  11. package/build/client/discovery.js +115 -0
  12. package/build/client/nexus-client.js +8 -2
  13. package/build/contract.js +69 -0
  14. package/build/index.js +12 -18
  15. package/build/server/nexus.js +63 -35
  16. package/contract/errors.json +52 -0
  17. package/contract/hook.json +156 -0
  18. package/contract/tools.json +2812 -0
  19. package/docs/ARCHITECTURE.md +142 -108
  20. package/docs/ARCHITECTURE_zh.md +144 -110
  21. package/docs/ASSISTANT_GUIDE.md +5 -4
  22. package/docs/ASSISTANT_GUIDE_zh.md +134 -0
  23. package/docs/MEETING_MINUTES_2025-12-29.md +166 -0
  24. package/docs/README_zh.md +159 -79
  25. package/docs/TOOLS_REFERENCE.md +404 -0
  26. package/docs/discussion_2025-12-29.md +727 -0
  27. package/llms.txt +16 -5
  28. package/package.json +12 -8
  29. package/build/cli/autostart.js +0 -64
  30. package/build/config/cli.js +0 -8
  31. package/build/config/paths.js +0 -38
  32. package/build/constants.js +0 -18
  33. package/build/daemon/hook-contract.js +0 -68
  34. package/build/daemon/index.js +0 -57
  35. package/build/daemon/rate-limit.js +0 -62
  36. package/build/daemon/server.js +0 -862
  37. package/build/daemon/tool-outputs.js +0 -180
  38. package/build/daemon/tools.js +0 -326
  39. package/build/storage/agents.js +0 -227
  40. package/build/storage/docs.js +0 -81
  41. package/build/storage/events.js +0 -31
  42. package/build/storage/fs-utils.js +0 -88
  43. package/build/storage/global-messages.js +0 -44
  44. package/build/storage/index.js +0 -141
  45. package/build/storage/migrations.js +0 -87
  46. package/build/storage/paths.js +0 -23
  47. package/build/storage/projects.js +0 -53
  48. package/build/storage/registry.js +0 -355
  49. package/build/storage/sqlite-meeting.js +0 -323
  50. package/build/storage/sqlite.js +0 -275
  51. package/build/storage/tasks.js +0 -138
  52. package/build/types.js +0 -1
  53. package/build/utils/async-rwlock.js +0 -64
package/CHANGELOG.md CHANGED
@@ -2,6 +2,45 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.9.0] - 2026-10-23
6
+
7
+ Version 0.9.0 of both programs: the host is now `n2n-nexus-host`, a separate Go program, and the npm package `n2n-nexus` is the client only.
8
+
9
+ ### Added
10
+
11
+ - `n2n-nexus-host`, the host rewritten in Go as one executable for Windows, macOS and Linux (amd64 and arm64). Without arguments it runs as a tray app: status, address and online sessions, open the log and data folders, LAN access on / off (on Windows it adds the TCP and UDP firewall rules after a UAC prompt; elsewhere it names the ports to allow), start at login (turned on the first time the tray runs), restart, quit; Chinese texts on a Chinese system, English otherwise. `serve` runs it in the foreground; `service install|uninstall|status` registers a boot service that runs as the installing user and restarts the host 5 s after it exits (Windows scheduled task, macOS LaunchDaemon, Linux systemd unit), and refuses to install while the tray runs the host; `autostart on|off` starts the tray at login. When a host already answers on the port (the service), the tray only controls it. One host runs per data directory.
12
+ - Installation through Homebrew (`brew install n2ns/tap/n2n-nexus-host`, built from source), Scoop (`scoop bucket add n2ns https://github.com/n2ns/scoop-bucket`, `scoop install n2ns/n2n-nexus-host`) and archives on GitHub Releases (tags `host-v*`) with `SHA256SUMS` and build provenance (`gh attestation verify`). The binaries are not code-signed yet.
13
+ - `update_task`: update a task's `status`, `progress` (0–1), `result_uri`, `error_message` or `metadata`; returns the task.
14
+ - `contract/`: the protocol between host and client in JSON (`tools.json`, `hook.json`, `errors.json`), shipped in the npm package. `GET /health` reports the host's `contractVersion`, and `n2n-nexus doctor` checks that the host answers with the client's.
15
+ - LAN discovery: `NEXUS_ENDPOINT=discover` (or `--endpoint discover`) makes the MCP adapter and the hooks find a host by UDP broadcast on `NEXUS_DISCOVERY_PORT` (default 5688), cache it in `~/.n2n-nexus/discovered-endpoint` and look again when it cannot be reached. A host listening on a non-loopback address answers on its `discoveryPort`. When none answers, clients use the local endpoint `http://127.0.0.1:5688`.
16
+ - The SessionStart hook tells a new conversation its own session name and ID and which other sessions are not offline, so it can message them without being asked to look first.
17
+ - The MCP adapter sends server instructions describing how to find and message other AI sessions.
18
+ - Idle conversations are woken: `install` adds a second Stop hook (`n2n-nexus hook <client> --wait`) that waits up to an hour after a turn for new messages or notices and then starts a turn with them, through an `asyncRewake` hook in Claude Code and `codex queue` in Codex (`NEXUS_CODEX_COMMAND` overrides the command). The session shows as `listening` meanwhile; any new activity ends the wait. Run `n2n-nexus install <client>` again to add the hook. An unattended Claude session (`claude -p`, `CLAUDE_CODE_SESSION_ATTENDED=0`) skips the wait, so it exits as soon as it answers.
19
+ - Automatic turn limit: wakes, listening Stops and Stops held open for messages count as automatic turns, and after `maxAutoTurns` (host `settings.json`, default 8; `0` = no limit) since the user's last prompt, messages are stored but no longer wake or hold the conversation, so two AI sessions cannot keep waking each other. The announcement says `Automatic turn n of N`; the prompt that starts a woken turn does not reset the count.
20
+
21
+ ### Changed
22
+
23
+ - **Breaking:** the npm package `n2n-nexus` is the client only (MCP adapter, `hook`, `install` / `uninstall`, `doctor`); `n2n-nexus daemon` is removed and the package no longer depends on `better-sqlite3`. Run the host with `n2n-nexus-host`.
24
+ - **Breaking:** storage is a new SQLite database, `nexus.db` in the host's data directory (`%LOCALAPPDATA%\n2n-nexus`, `~/Library/Application Support/n2n-nexus`, `$XDG_DATA_HOME/n2n-nexus` with default `~/.local/share/n2n-nexus`; `--root` or `NEXUS_ROOT` overrides it). Projects, assets and global docs are tables in it instead of files, every write is one transaction, and the schema is managed by numbered migrations. The data of earlier versions in `~/.n2n-nexus` is not imported and is left untouched.
25
+ - **Breaking:** the host has only four endpoints: `GET /health`, `GET /api/tools`, `POST /api/tools/call` and `POST /api/hook`. The project, message, global, meeting, task, event and maintenance REST endpoints are removed; every one of them has a tool of the same purpose.
26
+ - **Breaking:** `read_messages` needs an `instanceId` (the MCP adapter always sends one); calls without one no longer share an `unknown` read cursor.
27
+ - **Breaking:** host settings live in `settings.json` in the data directory (`host`, `port`, `discoveryPort`, `allowedHosts`, `rateLimit`, `rateBurst`, `maxAutoTurns`), overridden by command-line flags; the host reads no environment variable except `NEXUS_ROOT`. `NEXUS_HOST`, `NEXUS_DAEMON_PORT`, `NEXUS_ALLOWED_HOSTS`, `NEXUS_RATE_LIMIT`, `NEXUS_RATE_BURST` and `NEXUS_MAX_AUTO_TURNS` are gone; `NEXUS_DISCOVERY_PORT` remains for clients using `discover`.
28
+ - Hooks are rate limited per conversation and per client address, in buckets separate from tool calls.
29
+ - The host keeps data for a bounded time: events, read or expired direct messages and delivered notices for 30 days, finished tasks for 7 days; `host.log` rotates at 10 MB with 3 old files kept (the tray's own lines go to `tray.log`).
30
+ - The MCP adapter and the hooks never start a host. When none answers at a local endpoint, tool calls fail with a message saying to install and start `n2n-nexus-host`; at a remote endpoint, that it must be started on that machine. The hooks skip silently.
31
+ - `peers` lists only the sessions that are not offline; `includeOffline: true` adds the offline ones active in the last 24 hours, as before.
32
+ - An `idle` or `working` session without activity for 30 minutes is marked `offline`, since clients do not always send SessionEnd (finished or closed Codex threads stayed "online" forever); its next event brings it back. `peers` and the SessionStart introduction say that only `listening` sessions answer right away.
33
+
34
+ ### Fixed
35
+
36
+ - A background wait counts as activity, so a session waiting longer than 30 minutes is not marked `offline` between two waits. A session with `listen` on no longer stays `listening` after its waits have ended, and a host restart resets sessions left `listening`, so they go `offline` after 30 minutes without activity like any other session.
37
+ - Starting, ending and reopening a meeting each update the meeting and the active meeting in one transaction.
38
+ - The event filter `action` matches the exact name or the exact `<action>.` prefix; it no longer ignores case or treats `_` and `%` as wildcards.
39
+ - A tool call the host does not answer within the request timeout fails on its own; the MCP adapter no longer drops the tool list and reconnects, which led clients to retry calls the host was still completing.
40
+ - Windows: `install <claude|codex>` finds `claude.cmd` / `codex.cmd` installed by npm, and the `--wait` hook wakes Codex installed that way. The script behind npm's shim runs with Node directly, never through cmd.exe, so a message text cannot run commands and keeps its line breaks; installing from an npx cache path is refused as on other systems.
41
+ - The `create_task` description no longer points to a tool that does not exist.
42
+ - The npm package includes the documents its README and guides link to (tools reference, security, contributing, Chinese assistant guide, 2025-12-29 meeting notes).
43
+
5
44
  ## [0.8.2] - 2026-10-22
6
45
 
7
46
  ### Fixed
@@ -0,0 +1,49 @@
1
+ # Contributing
2
+
3
+ Thanks for helping improve n2n-nexus.
4
+
5
+ ## Development Setup
6
+
7
+ Use Node.js 24, matching CI and the lint toolchain, and the Go version in `go.mod` for the host (`npm test` builds the Go test host too). The published client requires Node.js 22.14+ on the 22.x release line, or Node.js 23.6+.
8
+
9
+ ```bash
10
+ npm install
11
+ npm run build
12
+ npm test
13
+ go vet ./...
14
+ go test ./host/...
15
+ npm run host:build-test && npm run test:conformance
16
+ ```
17
+
18
+ Pre-commit runs lint. Pre-push runs `go vet`, `go test ./host/...`, build, `npm test`, the test-host build and the conformance tests via lefthook.
19
+
20
+ For a full local run:
21
+
22
+ ```bash
23
+ # Start a host
24
+ go run ./host/cmd/n2n-nexus-host serve --root /tmp/nexus-test --port 5799
25
+
26
+ # Start MCP adapter (separate terminal)
27
+ NEXUS_ENDPOINT=http://127.0.0.1:5799 node build/index.js mcp
28
+ ```
29
+
30
+ ## Pull Request Guidelines
31
+
32
+ - Keep changes focused and easy to review.
33
+ - Add or update tests for behavior changes: Go tests next to the host code, conformance tests in `tests/conformance/` for anything a client can observe over HTTP, client tests in `tests/`.
34
+ - Update `docs/ARCHITECTURE.md` before changing the HTTP API, the database schema, or the protocol in `contract/`.
35
+ - Update `docs/ASSISTANT_GUIDE.md` and `docs/ASSISTANT_GUIDE_zh.md` when tool behavior visible to AI assistants changes.
36
+ - Run `npm run build && npm test`, `go test ./host/...` and `npm run test:conformance` before opening a PR.
37
+
38
+ ## Commit Hygiene
39
+
40
+ - Do not commit host data directories, `tests/tmp/`, `host/bin/`, or generated `node_modules/`.
41
+ - Explain user-visible behavior changes in the PR description.
42
+
43
+ ## Architecture Notes
44
+
45
+ The host (`n2n-nexus-host`, Go, `host/`) is the single source of truth. The MCP adapter (`n2n-nexus`, TypeScript, `src/`) is a stateless proxy — it has no tool definitions and no local data. Tool definitions live in `contract/tools.json`, their handlers in `host/internal/core` and `host/internal/agents`. See `docs/ARCHITECTURE.md` for the full system design and `host/README.md` for building and releasing the host.
46
+
47
+ ## Changelog
48
+
49
+ User-visible changes should be recorded in `CHANGELOG.md`.
package/README.md CHANGED
@@ -16,7 +16,7 @@ Local-first MCP coordination hub from N2NS Lab for multi-AI assistant collaborat
16
16
 
17
17
  ---
18
18
 
19
- > **One daemon. Many assistants. Shared project state.**
19
+ > **One host. Many assistants. Shared project state.**
20
20
 
21
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.
22
22
 
@@ -36,20 +36,20 @@ n2n-nexus is an open-source Model Context Protocol (MCP) coordination server for
36
36
 
37
37
  ## 💡 What is n2n-nexus?
38
38
 
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.
39
+ 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
40
 
41
41
  - Coordinate multiple AI assistants working on the same project.
42
42
  - Share project manifests, decisions, and assets across IDEs and machines.
43
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.
44
+ - Run the host once; connect adapters from Windows, WSL, SSH hosts, or multiple editors.
45
45
  - Avoid giving each assistant broad filesystem or backend access.
46
46
 
47
47
  ## 🏗️ Architecture
48
48
 
49
49
  ```text
50
50
  ┌──────────────────────────────────────┐
51
- │ n2n-nexus daemon │
52
- │ Standalone HTTP server · always on │
51
+ │ n2n-nexus-host │
52
+ │ Tray app or background service │
53
53
  │ Owns data, tools, tasks, messages │
54
54
  └──────────────┬───────────────────────┘
55
55
  │ HTTP (NEXUS_ENDPOINT)
@@ -57,25 +57,62 @@ n2n-nexus gives AI assistants a shared coordination workspace. A long-running lo
57
57
  ▼ ▼ ▼
58
58
  MCP-A MCP-B MCP-C
59
59
  (Win) (WSL) (SSH/VM)
60
- Stateless adapter per IDE
60
+ n2n-nexus adapter per IDE
61
61
  ```
62
62
 
63
- - **Daemon is the source of truth**: start it once and keep it running.
64
- - **MCP adapters are stateless**: each IDE starts an adapter, which forwards tool calls to the daemon.
65
- - **Every Claude Code and Codex conversation has its own identity**: client hooks register the conversation with the daemon and notify it of new direct messages; each tool call names its conversation through the request metadata the client attaches, never through model-supplied arguments.
66
- - **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.
63
+ - **The host is the source of truth**: install it once and keep it running, as a tray app or as a background service.
64
+ - **MCP adapters are stateless**: each IDE starts an adapter (`n2n-nexus mcp`), which forwards tool calls to the host.
65
+ - **Every Claude Code and Codex conversation has its own identity**: client hooks register the conversation with the host and notify it of new direct messages; each tool call names its conversation through the request metadata the client attaches, never through model-supplied arguments.
66
+ - **Cross-environment by design**: point `NEXUS_ENDPOINT` at the same host from different machines or shells. The host listens on `127.0.0.1` by default; turn on LAN access only on a trusted network.
67
67
 
68
68
  ## 🚀 Quick start
69
69
 
70
- Requires Node.js 22.14+ on the 22.x release line, or Node.js 23.6+.
70
+ ### 1. Install the host
71
+
72
+ | Platform | Install |
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
+ - 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.
83
+ - 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.
84
+ - On Linux the tray needs a desktop with StatusNotifierItem (AppIndicator) support; on a server, use `service install`.
85
+
86
+ Verify a download against the release's `SHA256SUMS` and its build provenance:
87
+
88
+ ```bash
89
+ sha256sum -c SHA256SUMS --ignore-missing
90
+ gh attestation verify n2n-nexus-host_<version>_<os>_<arch>.tar.gz --repo n2ns/n2n-nexus
91
+ ```
71
92
 
72
- ### 1. Start the daemon
93
+ ### 2. Install the client and connect Claude Code or Codex
94
+
95
+ Requires Node.js 22.14+ on the 22.x release line, or Node.js 23.6+.
73
96
 
74
97
  ```bash
75
- npx n2n-nexus daemon --port 5688
98
+ npm i -g n2n-nexus
99
+ n2n-nexus install claude # ~/.claude/settings.json (or $CLAUDE_CONFIG_DIR) + claude mcp add --scope user
100
+ n2n-nexus install codex # ~/.codex/hooks.json (or $CODEX_HOME) + codex mcp add
101
+ n2n-nexus doctor # check the installed commands and that the host answers
76
102
  ```
77
103
 
78
- ### 2. Configure an MCP client
104
+ `install` writes the hooks and the MCP server into the client's user configuration.
105
+
106
+ - Codex runs new hooks only after you trust them: open Codex and run `/hooks` once.
107
+ - 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.
108
+ - 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.
109
+ - 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>`.
110
+ - 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.
111
+ - 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.
112
+ - `n2n-nexus uninstall <claude|codex>` removes the hooks and the MCP server that `install` added and leaves the rest of the configuration alone.
113
+ - Supported on Linux, WSL and macOS; Codex hooks also support Windows PowerShell. Other MCP clients can use every tool except direct messages.
114
+
115
+ ### Other MCP clients
79
116
 
80
117
  ```json
81
118
  {
@@ -91,40 +128,36 @@ npx n2n-nexus daemon --port 5688
91
128
  }
92
129
  ```
93
130
 
94
- The adapter can start before the daemon. It loads the daemon tool list once the daemon becomes reachable. When nothing listens at a local endpoint (`127.0.0.1` or `localhost`), the adapter and the hooks start a daemon in the background (log: `~/.n2n-nexus/daemon.log`).
131
+ 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.
95
132
 
96
- ### 3. Direct messages between Claude Code and Codex conversations
133
+ ### Direct messages between Claude Code and Codex conversations
97
134
 
98
- Install n2n-nexus globally, then let it write the hooks and the MCP server into the client's user configuration:
135
+ 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.
99
136
 
100
- ```bash
101
- npm i -g n2n-nexus
102
- n2n-nexus install claude # ~/.claude/settings.json (or $CLAUDE_CONFIG_DIR) + claude mcp add --scope user
103
- n2n-nexus install codex # ~/.codex/hooks.json (or $CODEX_HOME) + codex mcp add
104
- ```
137
+ ### LAN access
105
138
 
106
- - Codex runs new hooks only after you trust them: open Codex and run `/hooks` once.
107
- - Pass `--endpoint <url>` to both commands when the daemon is not at `http://127.0.0.1:5688`.
108
- - 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>`.
109
- - 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.
110
- - 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 exits with code 1 when they do not.
111
- - `n2n-nexus uninstall <claude|codex>` removes the hooks and the MCP server that `install` added and leaves the rest of the configuration alone.
112
- - Supported on Linux, WSL and macOS. Other MCP clients can use every tool except direct messages.
139
+ By default the host listens on `127.0.0.1` only. To make it the host for other machines on a trusted network:
140
+
141
+ - 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.
142
+ - 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.
113
143
 
114
- Each conversation then 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`. 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.
144
+ A host that listens on a non-loopback address answers LAN discovery on UDP `discoveryPort`.
115
145
 
116
146
  ### Cross-environment endpoint examples
117
147
 
118
148
  | Scenario | `NEXUS_ENDPOINT` |
119
149
  | --- | --- |
120
150
  | Same machine | `http://127.0.0.1:5688` |
121
- | WSL IDE to Windows daemon | `http://host.docker.internal:5688` |
122
- | Windows IDE to WSL daemon | `http://<WSL-IP>:5688` |
151
+ | WSL IDE to Windows host | `http://host.docker.internal:5688` |
152
+ | Windows IDE to WSL host | `http://<WSL-IP>:5688` |
123
153
  | Remote machine | `http://<Server-IP>:5688` |
154
+ | Find a host on the LAN | `discover` |
155
+
156
+ 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.
124
157
 
125
158
  ## 🛠️ Toolset
126
159
 
127
- The daemon exposes 32 tools, grouped by purpose:
160
+ The host exposes 33 tools, grouped by purpose:
128
161
 
129
162
  | Group | Tools | Purpose |
130
163
  | --- | --- | --- |
@@ -134,7 +167,7 @@ The daemon exposes 32 tools, grouped by purpose:
134
167
  | **Messaging** | `send_message`, `read_messages`, `list_events` | Post meeting or global messages; read them and the activity log. |
135
168
  | **Direct messages** | `peers`, `send`, `read`, `reply`, `listen`, `set_profile` | Find other Claude Code / Codex conversations, message them, read and answer what arrives, wait for messages, and name yourself. |
136
169
  | **Meetings** | `list_meetings`, `start_meeting`, `end_meeting`, `archive_meeting`, `reopen_meeting` | List, open, close, archive, and reopen meeting sessions. |
137
- | **Tasks** | `create_task`, `get_task`, `list_tasks`, `cancel_task` | Create and track tasks that assistants manage themselves. |
170
+ | **Tasks** | `create_task`, `get_task`, `list_tasks`, `update_task`, `cancel_task` | Create, update and track tasks that assistants manage themselves. |
138
171
  | **Maintenance** | `host_maintenance`, `host_delete_project` | Prune or clear the global chat stream; delete a project. |
139
172
 
140
173
  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).
@@ -143,31 +176,43 @@ See [Tools reference](./docs/TOOLS_REFERENCE.md) for parameter schemas and call
143
176
 
144
177
  ## 💾 Data storage
145
178
 
146
- Default storage root:
179
+ The host keeps all coordination data in one SQLite database in its data directory:
147
180
 
148
- | Platform | Path |
149
- | --- | --- |
150
- | Linux / WSL | `~/.n2n-nexus` |
151
- | Windows | `%USERPROFILE%\.n2n-nexus` |
152
- | macOS | `~/.n2n-nexus` |
181
+ | Platform | Data directory | Log directory |
182
+ | --- | --- | --- |
183
+ | Windows | `%LOCALAPPDATA%\n2n-nexus` | `%LOCALAPPDATA%\n2n-nexus\logs` |
184
+ | macOS | `~/Library/Application Support/n2n-nexus` | `~/Library/Logs/n2n-nexus` |
185
+ | Linux | `$XDG_DATA_HOME/n2n-nexus` (default `~/.local/share/n2n-nexus`) | `$XDG_STATE_HOME/n2n-nexus` (default `~/.local/state/n2n-nexus`) |
153
186
 
154
- Override with `--root <path>` or `NEXUS_ROOT`.
187
+ Override the data directory with `--root <path>` or `NEXUS_ROOT`.
155
188
 
156
189
  ```text
157
- ~/.n2n-nexus/
158
- ├── global/
159
- │ ├── blueprint.md
160
- │ ├── docs_index.json
161
- │ └── docs/
162
- ├── projects/
163
- │ └── {project-id}/
164
- │ ├── manifest.json
165
- │ ├── internal_blueprint.md
166
- │ └── assets/
167
- ├── registry.json
168
- └── nexus.db # meetings, global chat, events, tasks, read cursors, conversations and direct messages
190
+ <data directory>/
191
+ ├── nexus.db # projects, assets, docs, meetings, global chat, events, tasks, read cursors, conversations, direct messages
192
+ ├── settings.json # host settings (see below)
193
+ └── host.lock # one host per data directory
169
194
  ```
170
195
 
196
+ 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.
197
+
198
+ Data of n2n-nexus 0.x (`~/.n2n-nexus`) is not imported: the host does not read it and leaves it in place.
199
+
200
+ ### settings.json
201
+
202
+ Read when the host starts; command-line flags override it. A missing file or field uses the default.
203
+
204
+ | Field | Description | Default |
205
+ | --- | --- | --- |
206
+ | `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"` |
207
+ | `port` | TCP port | `5688` |
208
+ | `discoveryPort` | UDP port of LAN discovery, answered only when `host` is not a loopback address | `5688` |
209
+ | `allowedHosts` | Extra DNS names accepted in the `Host` header (IP literals, `localhost` and `host.docker.internal` are always accepted) | `[]` |
210
+ | `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` |
211
+ | `rateBurst` | Calls an instance may make at once before the per-second limit applies (minimum 1) | `60` |
212
+ | `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` |
213
+
214
+ After editing the file, restart the host (tray **Restart**, or restart `serve` or the service).
215
+
171
216
  ## 🏷️ Project ID conventions
172
217
 
173
218
  Project IDs follow `[prefix]_[name]`.
@@ -186,53 +231,86 @@ Project IDs follow `[prefix]_[name]`.
186
231
 
187
232
  ## 💻 CLI reference
188
233
 
234
+ ### n2n-nexus-host
235
+
189
236
  ```bash
190
- # Start daemon
191
- n2n-nexus daemon [--port 5688] [--root ~/.n2n-nexus] [--host 127.0.0.1]
237
+ # Tray app (default)
238
+ n2n-nexus-host
239
+
240
+ # Run in the foreground without a tray; flags override settings.json (--port 0 picks a free port)
241
+ n2n-nexus-host serve [--host 127.0.0.1] [--port 5688] [--root <dir>]
192
242
 
193
- # Start MCP adapter
194
- NEXUS_ENDPOINT=http://127.0.0.1:5688 n2n-nexus mcp
243
+ # Background service that starts at boot (elevated terminal on Windows, sudo on macOS / Linux).
244
+ # install refuses while the tray runs the host (quit it first; reopened, it controls the service),
245
+ # writes --host / --port to settings.json, stops a host running for the data directory,
246
+ # registers and starts the service, and reports what answers on the port.
247
+ # uninstall removes the service and stops its host; a tray or a foreground serve keeps running.
248
+ n2n-nexus-host service install [--host <addr>] [--port <port>] [--root <dir>]
249
+ n2n-nexus-host service uninstall [--root <dir>]
250
+ n2n-nexus-host service status [--host <addr>] [--port <port>] [--root <dir>]
251
+
252
+ # Start the tray at login, or stop doing so
253
+ n2n-nexus-host autostart on|off
254
+
255
+ n2n-nexus-host version
256
+ ```
257
+
258
+ What `service install` registers, and where `autostart on` registers the tray:
259
+
260
+ | Platform | Service (starts at boot, runs as the installing user, restarts the host 5 s after it exits) | Tray at login |
261
+ | --- | --- | --- |
262
+ | Windows | Scheduled task `n2n-nexus-host` | `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, value `n2n-nexus-host` |
263
+ | macOS | `/Library/LaunchDaemons/io.n2ns.n2n-nexus-host.plist` | `~/Library/LaunchAgents/io.n2ns.n2n-nexus-host.tray.plist` |
264
+ | Linux | `/etc/systemd/system/n2n-nexus-host.service` | `~/.config/autostart/n2n-nexus-host.desktop` |
265
+
266
+ 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".
267
+
268
+ ### n2n-nexus (client)
269
+
270
+ ```bash
271
+ # Start the MCP adapter (launched by the IDE)
272
+ NEXUS_ENDPOINT=http://127.0.0.1:5688 n2n-nexus mcp [--id <instance id>]
195
273
 
196
274
  # Install / remove the hooks and MCP server for Claude Code or Codex
197
- # --node <path>: the Node executable to write into the client configuration
198
275
  n2n-nexus install <claude|codex> [--endpoint <url>] [--node <path>]
199
276
  n2n-nexus uninstall <claude|codex>
200
277
 
201
- # Check that the installed hook and MCP commands still run (exit 1 when not)
278
+ # Check that the installed hook and MCP commands still run and that the host answers
279
+ # with the client's contract version (exit 1 when not)
202
280
  n2n-nexus doctor [claude|codex]
203
281
 
204
282
  # Hook entry point (written into the client configuration by install)
205
- n2n-nexus hook <claude|codex> [--endpoint <url>]
283
+ n2n-nexus hook <claude|codex> [--endpoint <url>] [--wait]
206
284
  ```
207
285
 
208
286
  ### Environment variables
209
287
 
288
+ Client (`n2n-nexus`):
289
+
210
290
  | Variable | Description | Default |
211
291
  | --- | --- | --- |
212
- | `NEXUS_ENDPOINT` | Daemon URL for the MCP adapter and the hooks | `http://127.0.0.1:5688` |
213
- | `NEXUS_ROOT` | Storage root for daemon | `~/.n2n-nexus` |
214
- | `NEXUS_HOST` | Daemon bind host | `127.0.0.1` |
215
- | `NEXUS_DAEMON_PORT` | Daemon port (overridden by `--port`) | `5688` |
216
- | `NEXUS_RATE_LIMIT` | Tool calls per second per instance; each client address gets 5x. `0` disables rate limiting | `20` |
217
- | `NEXUS_RATE_BURST` | Calls an instance may make at once before the per-second limit applies (minimum 1) | `60` |
218
- | `NEXUS_ALLOWED_HOSTS` | Extra DNS names accepted in the `Host` header (IP literals, `localhost`, `host.docker.internal` are always accepted) | empty |
219
- | `NEXUS_INSTANCE_ID` | Override MCP instance ID (read cursors and meeting permissions are keyed on it) | `mcp-<hostname>-<hash of working directory>-<IDE name>` |
292
+ | `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` |
293
+ | `NEXUS_DISCOVERY_PORT` | UDP port the client queries with `discover` | `5688` |
294
+ | `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>` |
295
+ | `NEXUS_CODEX_COMMAND` | Command the Codex background wait runs as `<command> queue ...` | `codex` |
296
+
297
+ 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.
220
298
 
221
299
  ## 🔐 Security and governance notes
222
300
 
223
301
  - n2n-nexus stores coordination data locally by default.
224
302
  - Do not put secrets, credentials, customer data, or private tokens in meeting messages or project assets unless your local policy allows it.
225
303
  - Destructive tools such as project deletion should be used through explicit review workflows.
226
- - Exposing the daemon beyond localhost is an operational choice; restrict network access when using remote endpoints.
304
+ - Exposing the host beyond localhost is an operational choice; restrict network access when using remote endpoints.
227
305
  - Treat project manifests and internal docs as potentially sensitive implementation data.
228
306
 
229
307
  ## 🌐 Real-world example
230
308
 
231
- 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.
309
+ 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.
232
310
 
233
311
  What to look for in the session:
234
312
 
235
- - **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.
313
+ - **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 host throughout the session.
236
314
  - **Structured message categories**: `PROPOSAL`, `DECISION`, `UPDATE`, and `MEETING_START` tags let assistants signal intent, not just post text.
237
315
  - **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`.
238
316
  - **Async debugging across instances**: when one assistant hit an auth error, others responded with fixes in real time, each from their own IDE context.
@@ -245,7 +323,7 @@ What to look for in the session:
245
323
 
246
324
  ## 🔧 Local development
247
325
 
248
- Use Node.js 24 for development, matching CI and the lint toolchain.
326
+ Use Node.js 24 for the client, matching CI and the lint toolchain, and the Go version in `go.mod` for the host.
249
327
 
250
328
  ```bash
251
329
  git clone https://github.com/n2ns/n2n-nexus.git
@@ -253,16 +331,18 @@ cd n2n-nexus
253
331
  npm install
254
332
  npm run build
255
333
 
256
- # Run daemon
257
- node build/index.js daemon --root /tmp/nexus-test --port 5688
334
+ # Run a host
335
+ go run ./host/cmd/n2n-nexus-host serve --port 5799 --root /tmp/nexus-test
258
336
 
259
- # Run MCP adapter
260
- NEXUS_ENDPOINT=http://127.0.0.1:5688 node build/index.js mcp
337
+ # Run the MCP adapter against it
338
+ NEXUS_ENDPOINT=http://127.0.0.1:5799 node build/index.js mcp
261
339
  ```
262
340
 
341
+ See [host/README.md](./host/README.md) for building, testing and releasing the host.
342
+
263
343
  ## 📖 Related docs
264
344
 
265
- - **[Architecture](./docs/ARCHITECTURE.md)**: System design and daemon-adapter separation explained.
345
+ - **[Architecture](./docs/ARCHITECTURE.md)**: System design and host-client separation explained.
266
346
  - **[Tools reference](./docs/TOOLS_REFERENCE.md)**: Full tool list and parameters.
267
347
  - **[AI Assistant Guide](./docs/ASSISTANT_GUIDE.md)**: How AI assistants should call tools and stay in project context.
268
348
  - **[Changelog](./CHANGELOG.md)**: Version history and release notes.
package/SECURITY.md ADDED
@@ -0,0 +1,37 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ Security fixes target the latest published versions of n2n-nexus (npm client) and n2n-nexus-host.
6
+
7
+ ## Reporting a Vulnerability
8
+
9
+ Please do not open a public issue for suspected vulnerabilities.
10
+
11
+ Report security concerns through GitHub private vulnerability reporting for this repository, or contact Datafrog through the security channel listed on the organization profile.
12
+
13
+ When reporting, include:
14
+
15
+ - A description of the vulnerability.
16
+ - Steps to reproduce or a proof of concept.
17
+ - Affected versions or commit hashes.
18
+ - Any known impact or mitigation.
19
+
20
+ We aim to acknowledge reports within 5 business days.
21
+
22
+ ## Security Model
23
+
24
+ n2n-nexus runs a local HTTP host, `n2n-nexus-host`, that stores coordination data in `nexus.db` in its data directory (`%LOCALAPPDATA%\n2n-nexus`, `~/Library/Application Support/n2n-nexus` or `~/.local/share/n2n-nexus` by default). It does not send project data to remote services.
25
+
26
+ Key boundaries to be aware of:
27
+
28
+ - **Unsigned binaries**: The `n2n-nexus-host` release binaries are not code-signed yet. Verify a download with the release's `SHA256SUMS` and `gh attestation verify <file> --repo n2ns/n2n-nexus` (build provenance), or install through Homebrew, which builds from source.
29
+ - **Local trust model**: Every process running as the same operating-system user is trusted. The host has no authentication; such a process can call it directly or read and write `nexus.db` and the client transcripts under `~/.claude` and `~/.codex`. Conversation IDs and Claude tool-use bindings route direct messages between conversations; they are not credentials, so an AI session with shell access can act as another conversation of the same user. Checks inside the host cannot prevent that while the data stays readable to that user. The barrier that matters is the client's permission to run commands, which the user controls. To isolate agents from each other, run them as different operating-system users, each with its own host and data directory.
30
+
31
+ - **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
+ - **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 no 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
+ - **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
+ - **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.
36
+ - **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
+ - **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.
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Run the `claude` / `codex` command without a shell. On Windows npm installs
3
+ * them as `.cmd` shims, which Node cannot start without cmd.exe, and cmd.exe
4
+ * would parse the arguments again: a message text from another session could
5
+ * end a quoted argument and run commands, and a newline ends the command line.
6
+ * So a shim written by npm is resolved to the script it runs, started with
7
+ * this Node; an `.exe` or `.com` runs directly; anything else is not run.
8
+ *
9
+ * Node built-ins only: the hook command imports this to run `codex queue`.
10
+ */
11
+ import { spawnSync } from "node:child_process";
12
+ import fs from "node:fs";
13
+ import path from "node:path";
14
+ /** The bin path in npm's cmd-shim launch line: `"%dp0%\<path>" %*`. */
15
+ const NPM_SHIM_TARGET = /"%~?dp0%?\\([^"]+)"\s*%\*/;
16
+ /** The program and leading arguments that run `name` on this platform, or null when it cannot be run without a shell. */
17
+ export function clientCommand(name) {
18
+ if (process.platform !== "win32")
19
+ return { command: name, args: [] };
20
+ const file = findOnPath(name);
21
+ if (!file)
22
+ return null;
23
+ const ext = path.extname(file).toLowerCase();
24
+ if (ext === ".exe" || ext === ".com")
25
+ return { command: file, args: [] };
26
+ if (ext !== ".cmd" && ext !== ".bat")
27
+ return null;
28
+ const target = NPM_SHIM_TARGET.exec(fs.readFileSync(file, "utf8"))?.[1];
29
+ if (!target)
30
+ return null;
31
+ const script = path.join(path.dirname(file), ...target.split(/[\\/]+/));
32
+ const scriptExt = path.extname(script).toLowerCase();
33
+ if (scriptExt === ".exe")
34
+ return { command: script, args: [] };
35
+ return [".js", ".cjs", ".mjs"].includes(scriptExt) || scriptExt === "" ? { command: process.execPath, args: [script] } : null;
36
+ }
37
+ /** `name` as given (a path) or the first PATH entry with a PATHEXT extension that exists. */
38
+ function findOnPath(name) {
39
+ const exts = (process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean);
40
+ const candidates = (base) => path.extname(base) ? [base] : exts.map((ext) => base + ext.toLowerCase());
41
+ const dirs = /[\\/]/.test(name) ? [""] : (process.env.PATH || "").split(path.delimiter).filter(Boolean);
42
+ for (const dir of dirs) {
43
+ for (const file of candidates(dir ? path.join(dir, name) : name)) {
44
+ if (fs.existsSync(file) && fs.statSync(file).isFile())
45
+ return file;
46
+ }
47
+ }
48
+ return null;
49
+ }
50
+ /** spawnSync for a client command; a command that cannot be run reports ENOENT like a missing one. */
51
+ export function spawnClientSync(name, args, options) {
52
+ const resolved = clientCommand(name);
53
+ if (!resolved) {
54
+ const error = Object.assign(new Error(`spawnSync ${name} ENOENT`), { code: "ENOENT" });
55
+ return { pid: 0, output: [], stdout: "", stderr: "", status: null, signal: null, error };
56
+ }
57
+ return spawnSync(resolved.command, [...resolved.args, ...args], options);
58
+ }