n2n-nexus 0.12.6 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,14 @@
4
4
 
5
5
  All notable changes to this project will be documented in this file.
6
6
 
7
+ ## [0.13.0] - 2026-11-15
8
+
9
+ Upgrade the host and the client together for pi (the contract version stays 5: an older host answers a pi hook with 400, which the hook ignores, and Claude Code and Codex are not affected). The database is upgraded in place: keep `nexus.db`. An older host refuses the upgraded database.
10
+
11
+ ### Added
12
+
13
+ - pi as a client: `n2n-nexus install pi` writes a pi extension (`~/.pi/agent/extensions/n2n-nexus.ts`, or under `$PI_CODING_AGENT_DIR`) that runs `n2n-nexus hook pi` on pi's session start, prompt, tool result, end of run and shutdown, continues a run when messages arrive during it and wakes an idle pi session through the background wait, and registers the MCP server with `pi mcp add`; `uninstall pi` removes both and `doctor pi` checks them. pi's MCP tool calls carry no session, so the MCP server started by pi names the processes above it (pi starts it through npx and a shell) and its machine, and the host attributes the call to the session the nearest pi process among them on that machine reported last.
14
+
7
15
  ## [0.12.6] - 2026-11-10
8
16
 
9
17
  Upgrade the host and the client together (contract version 5), then run `n2n-nexus install <client>` again so the background wait hook gets its new timeout. The database is upgraded in place: keep `nexus.db`. An older host refuses the upgraded database.
package/README.md CHANGED
@@ -53,7 +53,7 @@ n2n-nexus gives AI assistants a shared coordination workspace. A long-running lo
53
53
 
54
54
  **Quick summary**
55
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>`
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|pi>`
57
57
  - **Protocol**: Model Context Protocol (MCP); the adapters reach the host over HTTP
58
58
  - **Storage**: one SQLite database in the host's data directory
59
59
  - **Good for**: multi-assistant coding sessions, project handoffs, shared decisions, meeting records, task tracking, project manifests
@@ -62,7 +62,7 @@ n2n-nexus gives AI assistants a shared coordination workspace. A long-running lo
62
62
  ## 🎯 Why use it?
63
63
 
64
64
  - Coordinate multiple AI assistants working on the same project.
65
- - Let Claude Code and Codex conversations message each other directly: a message arrives whole through the client's hooks, wakes a waiting conversation, and is answered with `reply`; each session shows the machine and environment it runs on.
65
+ - Let Claude Code, Codex and pi conversations message each other directly: a message arrives whole through the client's hooks, wakes a waiting conversation, and is answered with `reply`; each session shows the machine and environment it runs on.
66
66
  - Hold meetings among assistants and keep their decisions. When you take part, your conversation holds the user's seat: you decide each draft and end the meeting, and the assistants' consensus is only reported.
67
67
  - Share project manifests, decisions, and assets across IDEs and machines.
68
68
  - Keep decisions, proposals, and updates in a local meeting and message log; old data is cleaned up after the retention you set.
@@ -87,13 +87,13 @@ n2n-nexus gives AI assistants a shared coordination workspace. A long-running lo
87
87
 
88
88
  - **The host is the source of truth**: install it once and keep it running, as a tray app or as a background service.
89
89
  - **MCP adapters are stateless**: each IDE starts an adapter (`n2n-nexus mcp`), which forwards tool calls to the host.
90
- - **Every Claude Code and Codex conversation has its own identity**: client hooks register the conversation with the host and deliver new direct messages and meeting messages to it; each tool call names its conversation through the request metadata the client attaches, never through model-supplied arguments.
90
+ - **Every Claude Code, Codex and pi conversation has its own identity**: client hooks (for pi, an extension) register the conversation with the host and deliver new direct messages and meeting messages to it; each tool call names its conversation through the request metadata the client attaches (for pi, the pi process above the MCP server), never through model-supplied arguments.
91
91
  - **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.
92
92
 
93
93
  ## 🚀 Quick start
94
94
 
95
95
  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).
96
- 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).
96
+ 2. Install the client and connect Claude Code, Codex or pi (`npm i -g n2n-nexus@latest`, then `n2n-nexus install claude`, `n2n-nexus install codex` or `n2n-nexus install pi`): [step 3](docs/INSTALL.md#3-install-the-client-for-claude-code-codex-and-pi).
97
97
  3. Codex only: `install codex` trusts the n2n-nexus hooks in Codex; if it says it could not, trust them by hand: [step 4](docs/INSTALL.md#4-turn-on-the-codex-hooks-one-time-required).
98
98
 
99
99
  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).
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Where Claude Code and Codex keep their configuration. Node built-ins only:
2
+ * Where Claude Code, Codex and pi keep their configuration. Node built-ins only:
3
3
  * the hook command uses this to read a Codex conversation's title.
4
4
  */
5
5
  import os from "node:os";
@@ -10,3 +10,5 @@ export const claudeConfigDir = () => process.env.CLAUDE_CONFIG_DIR || path.join(
10
10
  export const claudeJsonDir = () => process.env.CLAUDE_CONFIG_DIR || os.homedir();
11
11
  /** Codex's hooks.json, config.toml and state databases: CODEX_HOME, else ~/.codex. */
12
12
  export const codexHome = () => process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
13
+ /** pi's agent directory (extensions, mcp.json, settings.json): PI_CODING_AGENT_DIR, else ~/.pi/agent. */
14
+ export const piAgentDir = () => process.env.PI_CODING_AGENT_DIR || path.join(os.homedir(), ".pi", "agent");
@@ -47,6 +47,31 @@ function describe(pid) {
47
47
  throw new Error(`no process ${pid}`);
48
48
  return { name: name.toLowerCase(), ppid };
49
49
  }
50
+ /** The most processes ancestorPids lists (contract/hook.json caller.clientPids maxItems). */
51
+ const MAX_ANCESTORS = 16;
52
+ /**
53
+ * This process's parent, its parent and so on up to (not including) init,
54
+ * nearest first; on Windows, or where a parent cannot be read, the list ends
55
+ * there. pi starts the MCP adapter through npx and a shell, so the pi process
56
+ * is one of them (the host finds it among them).
57
+ */
58
+ export function ancestorPids() {
59
+ const pids = [process.ppid];
60
+ if (process.platform === "win32")
61
+ return pids;
62
+ try {
63
+ while (pids.length < MAX_ANCESTORS) {
64
+ const { ppid } = describe(pids[pids.length - 1]);
65
+ if (ppid <= 1 || pids.includes(ppid))
66
+ break;
67
+ pids.push(ppid);
68
+ }
69
+ }
70
+ catch {
71
+ // Not readable: the processes found so far.
72
+ }
73
+ return pids;
74
+ }
50
75
  /**
51
76
  * The process that ran this hook, past the shells it went through: the
52
77
  * nearest ancestor that is not a shell; undefined when it cannot be told.
@@ -1,6 +1,6 @@
1
1
  /**
2
- * `n2n-nexus doctor [claude|codex]`: check that the Node path and the entry
3
- * script in the hook commands, and the Node path and npm's npx script in the
2
+ * `n2n-nexus doctor [claude|codex|pi]`: check that the Node path and the entry
3
+ * script in the hook commands (for pi, in its extension file), and the Node path and npm's npx script in the
4
4
  * MCP server command (`"<node>" "<npx>" -y n2n-nexus@latest mcp`), that
5
5
  * `install` wrote still exist and run, that the host those commands talk to answers
6
6
  * GET /health as n2n-nexus-host with this client's contract version, that
@@ -17,6 +17,7 @@ import { pkg } from "../config/index.js";
17
17
  import { hookEndpoint, isOurs, ourCommands, parseHookCommand } from "./hook-command.js";
18
18
  import { MCP_PACKAGE, VERSIONED_NODE_DIR, configFile, probeNode, readConfig } from "./install.js";
19
19
  import { mcpConfigFile, readMcpCommand } from "./mcp-config.js";
20
+ import { PI_EXTENSION_MARK, piExtensionFile, readPiExtension, readPiExtensionFile } from "./pi.js";
20
21
  const ok = (text) => ({ ok: true, text });
21
22
  const bad = (text) => ({ ok: false, text });
22
23
  const VERSIONED_NOTE = "in a version directory that an upgrade may remove";
@@ -92,7 +93,72 @@ async function doctorHosts(endpoints) {
92
93
  * Check one client; prints the report and returns whether everything passed, or undefined when nothing is installed.
93
94
  * Adds the endpoints its hook and MCP commands use to `endpoints`, falling back to defaultEndpoint.
94
95
  */
96
+ /** The MCP server registration of `client`: whether there is one (reported), and the endpoints it names. */
97
+ function doctorMcp(client, endpoints, defaultEndpoint, report, mcp) {
98
+ const mcpFile = mcpConfigFile(client);
99
+ if (!mcp) {
100
+ report(`MCP server ${MCP_SERVER_NAME} in ${mcpFile}`, bad("not registered"));
101
+ return;
102
+ }
103
+ endpoints.add(mcp.endpoint === undefined ? defaultEndpoint : mcp.endpoint || LOCAL_ENDPOINT);
104
+ report(`MCP Node ${mcp.command}`, checkNode(mcp.command));
105
+ const [npx, ...rest] = mcp.args;
106
+ if (npx && rest.join(" ") === `-y ${MCP_PACKAGE} mcp`)
107
+ report(`MCP npx ${npx}`, checkEntry(npx));
108
+ else
109
+ report(`MCP command in ${mcpFile}`, bad(`not in the form "<node>" "<npx>" -y ${MCP_PACKAGE} mcp`));
110
+ }
111
+ /** pi: the extension file that carries the hooks, and the MCP server. */
112
+ function doctorPi(endpoints, defaultEndpoint) {
113
+ const file = piExtensionFile();
114
+ const mcp = readMcpCommand("pi", mcpConfigFile("pi"));
115
+ const text = readPiExtensionFile(file);
116
+ if (text === undefined && !mcp)
117
+ return undefined;
118
+ const lines = [];
119
+ let failed = false;
120
+ let versioned = false;
121
+ const report = (label, check) => {
122
+ lines.push(` ${check.ok ? "ok " : "FAIL"} ${label}: ${check.text}`);
123
+ if (!check.ok)
124
+ failed = true;
125
+ if (check.text.includes(VERSIONED_NOTE))
126
+ versioned = true;
127
+ };
128
+ lines.push("pi");
129
+ if (text === undefined) {
130
+ report(`extension in ${file}`, bad("not installed"));
131
+ }
132
+ else if (!text.startsWith(PI_EXTENSION_MARK)) {
133
+ report(`extension in ${file}`, bad("not written by n2n-nexus"));
134
+ }
135
+ else {
136
+ const command = readPiExtension(text);
137
+ if (!command) {
138
+ report(`extension in ${file}`, bad("its Node path and entry script cannot be read"));
139
+ }
140
+ else {
141
+ if (command.endpoint)
142
+ endpoints.add(command.endpoint);
143
+ else
144
+ endpoints.add(defaultEndpoint);
145
+ report(`extension Node ${command.node}`, checkNode(command.node));
146
+ report(`extension entry ${command.entry}`, checkEntry(command.entry));
147
+ }
148
+ }
149
+ doctorMcp("pi", endpoints, defaultEndpoint, report, mcp);
150
+ if (failed) {
151
+ lines.push(` fix: run "n2n-nexus install pi" again (add --node <path> to use a stable Node path such as /usr/local/bin/node).`);
152
+ }
153
+ else if (versioned) {
154
+ lines.push(` note: after upgrading Node, run "n2n-nexus install pi" again, or install with --node <path> pointing at a stable entry such as /usr/local/bin/node.`);
155
+ }
156
+ process.stdout.write(lines.join("\n") + "\n");
157
+ return !failed;
158
+ }
95
159
  function doctorClient(client, endpoints, defaultEndpoint) {
160
+ if (client === "pi")
161
+ return doctorPi(endpoints, defaultEndpoint);
96
162
  const hooksFile = configFile(client);
97
163
  const mcpFile = mcpConfigFile(client);
98
164
  const lines = [];
@@ -126,18 +192,7 @@ function doctorClient(client, endpoints, defaultEndpoint) {
126
192
  }
127
193
  if (client === "claude" && commands.size > 0)
128
194
  report(`background wait hook in ${hooksFile}`, checkClaudeWait(config));
129
- if (!mcp) {
130
- report(`MCP server ${MCP_SERVER_NAME} in ${mcpFile}`, bad("not registered"));
131
- }
132
- else {
133
- endpoints.add(mcp.endpoint === undefined ? defaultEndpoint : mcp.endpoint || LOCAL_ENDPOINT);
134
- report(`MCP Node ${mcp.command}`, checkNode(mcp.command));
135
- const [npx, ...rest] = mcp.args;
136
- if (npx && rest.join(" ") === `-y ${MCP_PACKAGE} mcp`)
137
- report(`MCP npx ${npx}`, checkEntry(npx));
138
- else
139
- report(`MCP command in ${mcpFile}`, bad(`not in the form "<node>" "<npx>" -y ${MCP_PACKAGE} mcp`));
140
- }
195
+ doctorMcp(client, endpoints, defaultEndpoint, report, mcp);
141
196
  if (failed) {
142
197
  lines.push(` fix: run "n2n-nexus install ${client}" again (add --node <path> to use a stable Node path such as /usr/local/bin/node).`);
143
198
  }
@@ -178,11 +233,11 @@ async function doctorCodexTrust() {
178
233
  + ` fix: run "n2n-nexus install codex" again, or ${MANUAL_TRUST}.\n`);
179
234
  return false;
180
235
  }
181
- /** Parse `[claude|codex]`, run the checks and return the exit code. */
236
+ /** Parse `[claude|codex|pi]`, run the checks and return the exit code. */
182
237
  export async function runDoctor(argv) {
183
238
  const client = argv[0];
184
239
  if (client !== undefined && !isHookClient(client)) {
185
- process.stderr.write("Usage: n2n-nexus doctor [claude|codex]\n");
240
+ process.stderr.write("Usage: n2n-nexus doctor [claude|codex|pi]\n");
186
241
  return 1;
187
242
  }
188
243
  try {
package/build/cli/hook.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `n2n-nexus hook <claude|codex> [--endpoint <url>] [--wait]`: forward one client
2
+ * `n2n-nexus hook <claude|codex|pi> [--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
5
  * it loads only node built-ins (no MCP SDK or zod; node:sqlite only to read a
@@ -35,7 +35,7 @@ function argValue(argv, name) {
35
35
  function clientAndEndpoint(argv) {
36
36
  const client = argv[0];
37
37
  if (!isHookClient(client))
38
- throw new Error(`unknown client "${client ?? ""}" (expected claude or codex)`);
38
+ throw new Error(`unknown client "${client ?? ""}" (expected claude, codex or pi)`);
39
39
  return { client, configured: configuredEndpoint(argValue(argv, "--endpoint")) };
40
40
  }
41
41
  /** Returns the text to print on stdout ("" for nothing); throws on any failure. */
@@ -57,6 +57,12 @@ async function forward(argv) {
57
57
  }
58
58
  // Every event says where the conversation runs; the host refuses one without it.
59
59
  input.machine = machineInfo();
60
+ // A pi process names itself (--client-pid, its extension passes it): tool calls from it find the session.
61
+ if (client === "pi") {
62
+ const pid = parsePid(argValue(argv, "--client-pid"));
63
+ if (pid !== undefined)
64
+ input.client_pid = pid;
65
+ }
60
66
  // SessionEnd must finish within Codex's 3 s hook limit (discovery included): a short budget and a single attempt.
61
67
  const sessionEnd = stdin.hook_event_name === "SessionEnd";
62
68
  const timeoutMs = stdin.hook_event_name === "Stop" ? HOOK_STOP_REQUEST_TIMEOUT_MS : sessionEnd ? SESSION_END_REQUEST_TIMEOUT_MS : HOOK_REQUEST_TIMEOUT_MS;
@@ -1,9 +1,10 @@
1
1
  /**
2
- * `n2n-nexus install <claude|codex> [--endpoint <url>] [--node <path>]` and
3
- * `n2n-nexus uninstall <claude|codex>`: register the hooks and the MCP server
4
- * with Claude Code or Codex. Our hook entries are recognised by a command that
2
+ * `n2n-nexus install <claude|codex|pi> [--endpoint <url>] [--node <path>]` and
3
+ * `n2n-nexus uninstall <claude|codex|pi>`: register the hooks and the MCP server
4
+ * with Claude Code, Codex or pi. Our hook entries are recognised by a command that
5
5
  * contains both `n2n-nexus` and ` hook <client>`; everything else in the
6
- * configuration file is kept as it is.
6
+ * configuration file is kept as it is. pi has no hook configuration: its hooks
7
+ * are an extension file (pi.ts), which install writes and uninstall removes.
7
8
  *
8
9
  * For Claude Code, install also writes a marked block into the user's
9
10
  * CLAUDE.md that makes direct messages count as the user's own requests
@@ -28,9 +29,11 @@ import { claudeConfigDir, codexHome } from "./client-dirs.js";
28
29
  import { entryScript } from "./entry.js";
29
30
  import { hookCommand, isOurs, windowsHookCommand } from "./hook-command.js";
30
31
  import { mcpConfigFile, readMcpCommand } from "./mcp-config.js";
32
+ import { PI_EXTENSION_MARK, piExtensionFile, readPiExtensionFile, renderPiExtension, writePiExtension } from "./pi.js";
31
33
  const EVENTS = {
32
34
  claude: ["SessionStart", "SessionEnd", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"],
33
- codex: ["SessionStart", "SessionEnd", "UserPromptSubmit", "PostToolUse", "Stop"]
35
+ codex: ["SessionStart", "SessionEnd", "UserPromptSubmit", "PostToolUse", "Stop"],
36
+ pi: []
34
37
  };
35
38
  class InstallError extends Error {
36
39
  }
@@ -155,6 +158,8 @@ function checkNodePath(given) {
155
158
  return node;
156
159
  }
157
160
  export function configFile(client) {
161
+ if (client === "pi")
162
+ return piExtensionFile();
158
163
  return client === "claude" ? path.join(claudeConfigDir(), "settings.json") : path.join(codexHome(), "hooks.json");
159
164
  }
160
165
  export function readConfig(file) {
@@ -323,6 +328,30 @@ function mcpRemoveArgs(client) {
323
328
  ? ["mcp", "remove", "--scope", "user", MCP_SERVER_NAME]
324
329
  : ["mcp", "remove", MCP_SERVER_NAME];
325
330
  }
331
+ /**
332
+ * Write (install) or remove (uninstall) the pi extension that carries the hooks. An extension file that is not
333
+ * ours is never overwritten or removed. Returns whether an extension of ours was there before.
334
+ */
335
+ function piExtensionStep(action, command) {
336
+ const file = piExtensionFile();
337
+ const text = readPiExtensionFile(file);
338
+ const ours = text !== undefined && text.startsWith(PI_EXTENSION_MARK);
339
+ if (text !== undefined && !ours) {
340
+ if (action === "install")
341
+ throw new InstallError(`${file} exists and is not written by n2n-nexus; move it aside and run again.`);
342
+ process.stdout.write(`Left ${file} alone: it is not written by n2n-nexus.\n`);
343
+ return false;
344
+ }
345
+ if (action === "install") {
346
+ writePiExtension(file, renderPiExtension(command));
347
+ process.stdout.write(`Wrote the pi extension to ${file}\n`);
348
+ }
349
+ else if (ours) {
350
+ fs.rmSync(file, { force: true });
351
+ process.stdout.write(`Removed the pi extension from ${file}\n`);
352
+ }
353
+ return ours;
354
+ }
326
355
  /**
327
356
  * `<client> mcp add` for `"<node>" "<npx>" -y n2n-nexus@latest mcp`. PATH starts with `bin` (see npxFor): npx starts
328
357
  * the package's bin, `#!/usr/bin/env node` (Unix) or a .cmd shim running `node` (Windows), which looks Node up on
@@ -377,7 +406,7 @@ function removeMcpServer(client, action, hooksRemoved) {
377
406
  return;
378
407
  if (action === "uninstall" && error.code === "ENOENT") {
379
408
  const file = mcpConfigFile(client);
380
- const what = client === "claude" ? `the "${MCP_SERVER_NAME}" entry under "mcpServers"` : `the [mcp_servers.${MCP_SERVER_NAME}] table`;
409
+ const what = client === "codex" ? `the [mcp_servers.${MCP_SERVER_NAME}] table` : `the "${MCP_SERVER_NAME}" entry under "mcpServers"`;
381
410
  process.stdout.write(`Skipped "${client} ${removeArgs.join(" ")}": the "${client}" command could not be run (not on PATH, or not runnable without a shell). `
382
411
  + `The MCP server is still registered in ${file}; run that command once ${client} can be run, or delete ${what} from ${file}.\n`);
383
412
  return;
@@ -403,22 +432,28 @@ export function runInstall(options) {
403
432
  if (action === "install") {
404
433
  const probe = spawnClientSync(client, ["--version"], { stdio: "ignore" });
405
434
  if (probe.error || probe.status !== 0) {
406
- throw new InstallError(`The "${client}" command was not found on PATH; install ${client === "claude" ? "Claude Code" : "Codex"} first.`);
435
+ throw new InstallError(`The "${client}" command was not found on PATH; install ${CLIENT_NAMES[client]} first.`);
407
436
  }
408
437
  }
409
438
  const node = action === "install" ? (options.node ? checkNodePath(options.node) : stableNodePath()) : undefined;
410
439
  const npm = node ? npxFor(node) : undefined;
411
- const file = configFile(client);
412
- const config = readConfig(file);
413
- const removed = removeOurEntries(config, client, file);
414
- if (action === "install") {
415
- const command = hookCommand(node, entry, client, endpoint);
416
- const commandWindows = client === "codex" ? windowsHookCommand(node, entry, client, endpoint) : undefined;
417
- addOurEntries(config, client, command, commandWindows);
440
+ let removed;
441
+ if (client === "pi") {
442
+ removed = piExtensionStep(action, node ? { node, entry, endpoint } : undefined);
418
443
  }
419
- if (action === "install" || removed) {
420
- writeConfig(file, config);
421
- process.stdout.write(`${action === "install" ? "Wrote hooks to" : "Removed hooks from"} ${file}\n`);
444
+ else {
445
+ const file = configFile(client);
446
+ const config = readConfig(file);
447
+ removed = removeOurEntries(config, client, file);
448
+ if (action === "install") {
449
+ const command = hookCommand(node, entry, client, endpoint);
450
+ const commandWindows = client === "codex" ? windowsHookCommand(node, entry, client, endpoint) : undefined;
451
+ addOurEntries(config, client, command, commandWindows);
452
+ }
453
+ if (action === "install" || removed) {
454
+ writeConfig(file, config);
455
+ process.stdout.write(`${action === "install" ? "Wrote hooks to" : "Removed hooks from"} ${file}\n`);
456
+ }
422
457
  }
423
458
  if (client === "claude" && writeInstructions(action === "install")) {
424
459
  process.stdout.write(`${action === "install" ? "Wrote the n2n-nexus instructions to" : "Removed the n2n-nexus instructions from"} ${instructionsFile()}\n`);
@@ -440,11 +475,13 @@ export function runInstall(options) {
440
475
  return 1;
441
476
  }
442
477
  }
443
- /** Parse `<claude|codex> [--endpoint <url>] [--node <path>]` and run. */
478
+ /** The product names the installer's messages use. */
479
+ const CLIENT_NAMES = { claude: "Claude Code", codex: "Codex", pi: "pi" };
480
+ /** Parse `<claude|codex|pi> [--endpoint <url>] [--node <path>]` and run. */
444
481
  export async function runInstallCommand(action, argv) {
445
482
  const client = argv[0];
446
483
  if (!isHookClient(client)) {
447
- process.stderr.write(`Usage: n2n-nexus ${action} <claude|codex>${action === "install" ? " [--endpoint <url>] [--node <path>]" : ""}\n`);
484
+ process.stderr.write(`Usage: n2n-nexus ${action} <claude|codex|pi>${action === "install" ? " [--endpoint <url>] [--node <path>]" : ""}\n`);
448
485
  return 1;
449
486
  }
450
487
  const option = (name, what) => {
@@ -1,17 +1,21 @@
1
1
  /**
2
- * Where Claude Code and Codex keep their MCP server registrations, and how to
3
- * read the n2n-nexus entry: `~/.claude.json` (user scope) and
4
- * `$CODEX_HOME/config.toml`. `claude mcp get` is not used for this because it
2
+ * Where Claude Code, Codex and pi keep their MCP server registrations, and how to
3
+ * read the n2n-nexus entry: `~/.claude.json` (user scope), `$CODEX_HOME/config.toml`
4
+ * and pi's `mcp.json`. `claude mcp get` is not used for this because it
5
5
  * looks in every scope, not only the user scope that install writes to, and
6
6
  * it starts the server for a health check.
7
7
  */
8
8
  import fs from "node:fs";
9
9
  import path from "node:path";
10
10
  import { MCP_SERVER_NAME } from "../contract.js";
11
- import { claudeJsonDir, codexHome } from "./client-dirs.js";
11
+ import { claudeJsonDir, codexHome, piAgentDir } from "./client-dirs.js";
12
12
  /** Where the client keeps the MCP server command. */
13
13
  export function mcpConfigFile(client) {
14
- return client === "claude" ? path.join(claudeJsonDir(), ".claude.json") : path.join(codexHome(), "config.toml");
14
+ if (client === "claude")
15
+ return path.join(claudeJsonDir(), ".claude.json");
16
+ if (client === "pi")
17
+ return path.join(piAgentDir(), "mcp.json");
18
+ return path.join(codexHome(), "config.toml");
15
19
  }
16
20
  /** The MCP server command the client has for n2n-nexus, or undefined when not registered. */
17
21
  export function readMcpCommand(client, file) {
@@ -24,7 +28,7 @@ export function readMcpCommand(client, file) {
24
28
  return undefined;
25
29
  throw error;
26
30
  }
27
- if (client === "claude") {
31
+ if (client === "claude" || client === "pi") {
28
32
  let parsed;
29
33
  try {
30
34
  parsed = JSON.parse(text);