remote-pi 0.4.0 → 0.4.2

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.
@@ -23,8 +23,8 @@
23
23
  import type { ClientMessage, ServerMessage, WireModel } from "../protocol/types.js";
24
24
  /**
25
25
  * Structural subset of the SDK's `Model<Api>` interface (defined in
26
- * `@mariozechner/pi-ai`, which is a transitive dep — not re-exported by
27
- * `@mariozechner/pi-coding-agent`'s main entry). Capturing just the
26
+ * `@earendil-works/pi-ai`, which is a transitive dep — not re-exported by
27
+ * `@earendil-works/pi-coding-agent`'s main entry). Capturing just the
28
28
  * fields we touch keeps the handler decoupled from the SDK's full Model
29
29
  * surface and avoids a direct dep on `pi-ai`.
30
30
  */
@@ -14,7 +14,7 @@
14
14
  * imports, no internal-state coupling — see the probe note in
15
15
  * `plan/28-pi-commands.md` Wave 0.
16
16
  */
17
- import { ModelRegistry } from "@mariozechner/pi-coding-agent";
17
+ import { ModelRegistry } from "@earendil-works/pi-coding-agent";
18
18
  /**
19
19
  * Lazily instantiate the shared `ModelRegistry`. Subsequent calls return
20
20
  * the same instance — keep it cached so `refresh()` cycles are cheap and
@@ -14,7 +14,7 @@
14
14
  * imports, no internal-state coupling — see the probe note in
15
15
  * `plan/28-pi-commands.md` Wave 0.
16
16
  */
17
- import { ModelRegistry, AuthStorage } from "@mariozechner/pi-coding-agent";
17
+ import { ModelRegistry, AuthStorage } from "@earendil-works/pi-coding-agent";
18
18
  let _registry = null;
19
19
  /**
20
20
  * Lazily instantiate the shared `ModelRegistry`. Subsequent calls return
@@ -1 +1 @@
1
- {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/actions/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAE3E,IAAI,SAAS,GAAyB,IAAI,CAAC;AAE3C;;;;GAIG;AACH,MAAM,UAAU,mBAAmB;IACjC,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,SAAS,GAAG,aAAa,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,EAAE,CAAC,CAAC;IACzD,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,2BAA2B;IACzC,SAAS,GAAG,IAAI,CAAC;AACnB,CAAC"}
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/actions/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAE7E,IAAI,SAAS,GAAyB,IAAI,CAAC;AAE3C;;;;GAIG;AACH,MAAM,UAAU,mBAAmB;IACjC,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,SAAS,GAAG,aAAa,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,EAAE,CAAC,CAAC;IACzD,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,2BAA2B;IACzC,SAAS,GAAG,IAAI,CAAC;AACnB,CAAC"}
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * Plan/26 W2. The Pi RPC protocol (`pi --mode rpc`) used by the daemon
10
10
  * children themselves is a separate contract — see
11
- * `node_modules/@mariozechner/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts`.
11
+ * `node_modules/@earendil-works/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts`.
12
12
  * This file is strictly the supervisor's own control plane.
13
13
  */
14
14
  /** Per-daemon runtime state observable through the supervisor. */
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * Plan/26 W2. The Pi RPC protocol (`pi --mode rpc`) used by the daemon
10
10
  * children themselves is a separate contract — see
11
- * `node_modules/@mariozechner/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts`.
11
+ * `node_modules/@earendil-works/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts`.
12
12
  * This file is strictly the supervisor's own control plane.
13
13
  */
14
14
  // ── Serialization helpers ────────────────────────────────────────────────────
package/dist/index.d.ts CHANGED
@@ -29,7 +29,7 @@
29
29
  * AgentBridge (src/session/agent_bridge.ts) remains the tested, mockable unit
30
30
  * for integration tests.
31
31
  */
32
- import type { ExtensionContext, ExtensionFactory } from "@mariozechner/pi-coding-agent";
32
+ import type { ExtensionContext, ExtensionFactory } from "@earendil-works/pi-coding-agent";
33
33
  import type { ClientMessage, SessionHistoryEvent } from "./protocol/types.js";
34
34
  import { PlainPeerChannel } from "./transport/peer_channel.js";
35
35
  export type RemoteState = "idle" | "started";
package/dist/index.js CHANGED
@@ -30,7 +30,7 @@
30
30
  * for integration tests.
31
31
  */
32
32
  import { randomUUID } from "node:crypto";
33
- import { SettingsManager } from "@mariozechner/pi-coding-agent";
33
+ import { SettingsManager } from "@earendil-works/pi-coding-agent";
34
34
  import { buildQRUri, qrSession, renderQRAscii } from "./pairing/qr.js";
35
35
  import { addPeer, getOrCreateEd25519Keypair, listOwnerPubkeys, listPeers, removePeer, } from "./pairing/storage.js";
36
36
  import { MeshClient } from "./mesh/client.js";
@@ -56,7 +56,7 @@ import { fileURLToPath } from "node:url";
56
56
  import { mkdirSync, copyFileSync, existsSync, unlinkSync, readFileSync, writeFileSync, realpathSync } from "node:fs";
57
57
  import { createInterface } from "node:readline";
58
58
  import { spawnSync } from "node:child_process";
59
- import { hostname, homedir } from "node:os";
59
+ import { hostname } from "node:os";
60
60
  import { kDefaultRelayUrl, resolveRelayUrl, saveConfig, isValidRelayUrl, isWebSocketScheme, toWebSocketUrl, } from "./config.js";
61
61
  let _state = "idle";
62
62
  let _relay = null;
@@ -930,11 +930,10 @@ const extension = (pi) => {
930
930
  _broadcastToActive({ type: "agent_done", in_reply_to: _currentTurnId });
931
931
  _currentTurnId = null;
932
932
  });
933
- // plan/25 Wave 0: notify the local broker of turn lifecycle so it can
934
- // ACK incoming agent-network envelopes as `busy` while this peer's LLM is
935
- // mid-turn (and `received` once idle). Fire-and-forgetif the broker
936
- // can't be reached, the worst case is a bad ACK answer; recovery is the
937
- // next turn boundary. Skip silently when no mesh session is joined.
933
+ // plan/34: the broker no longer gates delivery on busy state, so we no
934
+ // longer notify it of turn lifecycle. Working state is still published as
935
+ // room_meta over the relay (plan/32) belowthat's independent of the
936
+ // broker and drives the app's working indicator.
938
937
  pi.on("turn_start", (_event, ctx) => {
939
938
  // Late model hydration: if the model was still unknown at connect (resolved
940
939
  // lazily by the SDK), grab it on the first turn and fan it out — so a daemon
@@ -955,10 +954,6 @@ const extension = (pi) => {
955
954
  if (_relay && _myRoomId) {
956
955
  _relay.sendControl({ type: "room_meta_update", room_id: _myRoomId, meta: { working: true } });
957
956
  }
958
- if (!_meshNode)
959
- return;
960
- void _meshNode.send("broker", { type: "turn_state", busy: true })
961
- .catch(() => { });
962
957
  });
963
958
  pi.on("turn_end", () => {
964
959
  // Plan/32 Part B: publish working=false as room_meta (raw, no debounce).
@@ -967,10 +962,6 @@ const extension = (pi) => {
967
962
  if (_relay && _myRoomId) {
968
963
  _relay.sendControl({ type: "room_meta_update", room_id: _myRoomId, meta: { working: false } });
969
964
  }
970
- if (!_meshNode)
971
- return;
972
- void _meshNode.send("broker", { type: "turn_state", busy: false })
973
- .catch(() => { });
974
965
  });
975
966
  // Plan/32: compaction feedback. compact() doesn't run a turn, so bracket it
976
967
  // with working=true/false here. Returning void = no veto → default
@@ -2650,34 +2641,27 @@ if (_isDirectRun()) {
2650
2641
  }
2651
2642
  // ── `remote-pi claude` — launch Claude Code connected to the mesh ─────────────
2652
2643
  /**
2653
- * Deploy the agent-network skill into the user-global Claude skills dir
2654
- * (`~/.claude/skills/agent-network/SKILL.md`). The template ships in the
2655
- * package at `skills/claude-agent-network/SKILL.md` (sibling of `dist/`).
2656
- * Idempotent: overwrites on every launch so skill edits in the package
2657
- * propagate. Best-effort a failure here doesn't block the launch (the
2658
- * MCP `instructions` + tool descriptions still give Claude the basics).
2659
- *
2660
- * Mirrors the Pi extension's `_deployAgentNetworkSkill()` (which targets
2661
- * `~/.pi/remote/skills/`): same concept, each runtime's own skills dir.
2644
+ * Resolve the packaged agent-network skill path
2645
+ * (`<pkgRoot>/skills/agent-network/SKILL.md`). Single source of truth shared
2646
+ * by both runtimes: Pi discovers it via `resources_discover`, and the Claude
2647
+ * launcher injects it as a system prompt (see `_cmdClaudeCli`). Returns null
2648
+ * if the file is missing (e.g. running before `pnpm build`).
2662
2649
  */
2663
- function _deployClaudeMeshSkill() {
2664
- try {
2665
- const here = fileURLToPath(import.meta.url); // dist/index.js
2666
- const pkgRoot = dirname(dirname(here)); // package root (dist → ..)
2667
- const template = join(pkgRoot, "skills", "claude-agent-network", "SKILL.md");
2668
- if (!existsSync(template))
2669
- return;
2670
- const destDir = join(homedir(), ".claude", "skills", "agent-network");
2671
- mkdirSync(destDir, { recursive: true });
2672
- copyFileSync(template, join(destDir, "SKILL.md"));
2673
- }
2674
- catch {
2675
- /* best-effort — never block the launch on skill deploy */
2676
- }
2650
+ function _agentNetworkSkillPath() {
2651
+ const here = fileURLToPath(import.meta.url); // dist/index.js (or src/index.ts via tsx)
2652
+ const pkgRoot = dirname(dirname(here)); // package root (dist → ..; src → ..)
2653
+ const skill = join(pkgRoot, "skills", "agent-network", "SKILL.md");
2654
+ return existsSync(skill) ? skill : null;
2677
2655
  }
2678
2656
  async function _cmdClaudeCli(args) {
2679
- // Determine target cwd — first non-flag arg, or process.cwd()
2680
- const targetCwd = args.find((a) => !a.startsWith("--")) ?? process.cwd();
2657
+ // Contract: `remote-pi claude [cwd] [claude-flags...]`. The optional cwd is
2658
+ // ONLY the leading positional (first token, not a flag); everything after it
2659
+ // is forwarded verbatim to the `claude` binary (e.g. `--resume`, `-c`,
2660
+ // `-p "prompt"`). Restricting cwd to the leading token avoids mistaking a
2661
+ // flag's value (e.g. the id in `--resume <id>`) for the cwd.
2662
+ const hasCwdArg = args.length > 0 && !args[0].startsWith("-");
2663
+ const targetCwd = hasCwdArg ? args[0] : process.cwd();
2664
+ const passthroughArgs = hasCwdArg ? args.slice(1) : args;
2681
2665
  // Wizard when no local config exists
2682
2666
  if (!localConfigExists(targetCwd)) {
2683
2667
  const suggested = defaultAgentName(targetCwd);
@@ -2698,14 +2682,6 @@ async function _cmdClaudeCli(args) {
2698
2682
  }
2699
2683
  const absCwd = resolve(targetCwd);
2700
2684
  const SERVER_NAME = "remote-pi-mesh";
2701
- // Deploy the agent-network skill so Claude knows HOW to use the mesh tools
2702
- // (when to call get_messages, ACK statuses, cross-PC addressing, replies).
2703
- // Skills load only from disk — an MCP server can't inject one — so we write
2704
- // it to the user-global Claude skills dir, mirroring how the Pi extension
2705
- // deploys its own agent-network skill to ~/.pi/remote/skills/. The skill's
2706
- // `description` self-gates activation to mesh contexts, so a global deploy
2707
- // doesn't pollute unrelated Claude sessions.
2708
- _deployClaudeMeshSkill();
2709
2685
  // The channel feature (claude/channel push) only recognizes MCP servers
2710
2686
  // registered in one of the persistent scopes Claude Code enumerates:
2711
2687
  // enterprise / user / project / local. A server passed via `--mcp-config`
@@ -2713,30 +2689,42 @@ async function _cmdClaudeCli(args) {
2713
2689
  // scopes, so `--dangerously-load-development-channels server:<name>` can't
2714
2690
  // match it ("no MCP server configured with that name").
2715
2691
  //
2716
- // We therefore register in the **local** scope: per-project (only active in
2717
- // this cwd) and stored in `~/.claude.json` NOT written into the project
2718
- // dir, NOT committed to VCS, NOT global. Best of both: no file pollution
2719
- // AND channels work.
2692
+ // We therefore register in the **local** scope, stored in `~/.claude.json`
2693
+ // (NOT written into the project dir, NOT committed to VCS, NOT global) so the
2694
+ // dev-channel flag matches it. Caveat that drives the next decision: `-s
2695
+ // local` is keyed by the **git repo root** and shares ONE entry per server
2696
+ // name across every subdir of the repo — a same-name re-add overwrites it.
2720
2697
  //
2721
2698
  // remove-then-add makes it idempotent and refreshes the path if the
2722
2699
  // extension moved (Pi can reinstall to a new hash dir on upgrade).
2700
+ //
2701
+ // Crucially we do NOT bake `--cwd <absCwd>` into the registration. In a
2702
+ // monorepo (app/, relay/, site/, …) every subproject is its own agent, but
2703
+ // they'd all share this single repo-keyed entry — so whichever agent ran
2704
+ // `remote-pi claude` LAST would leak its cwd to every other session, and the
2705
+ // repo-root session would then spawn a peer that locks a subdir another agent
2706
+ // owns ("Failed to connect"). Instead, the server resolves its folder from
2707
+ // its own `process.cwd()`, which Claude sets to the directory each `claude`
2708
+ // session was launched in (verified empirically — NOT the git root, NOT
2709
+ // CLAUDE_PROJECT_DIR). We launch claude with `cwd: absCwd` below, so a single
2710
+ // shared registration self-identifies as the right agent in every session.
2723
2711
  spawnSync("claude", ["mcp", "remove", SERVER_NAME, "-s", "local"], {
2724
2712
  cwd: absCwd, stdio: "ignore", shell: false,
2725
2713
  });
2726
2714
  const add = spawnSync("claude", [
2727
2715
  "mcp", "add", SERVER_NAME, "-s", "local", "--",
2728
- process.execPath, meshServerPath, "--cwd", absCwd,
2716
+ process.execPath, meshServerPath,
2729
2717
  ], { cwd: absCwd, stdio: ["ignore", "pipe", "pipe"], shell: false, encoding: "utf8" });
2730
2718
  if (add.status !== 0) {
2731
2719
  console.log(`[remote-pi] failed to register MCP server: ${add.stderr || add.stdout}`);
2732
2720
  process.exit(1);
2733
2721
  }
2734
- const agentName = loadLocalConfig(targetCwd).agent_name ?? defaultAgentName(targetCwd);
2735
- process.stdout.write(`[remote-pi] Launching Claude as "${agentName}" in ${absCwd}\n`);
2736
- process.stdout.write(`[remote-pi] MCP server: ${meshServerPath} (local scope)\n`);
2737
- process.stdout.write(`[remote-pi] Tools: list_peers, agent_send, get_messages\n`);
2738
- process.stdout.write(`[remote-pi] Skill: agent-network (~/.claude/skills/)\n`);
2739
- process.stdout.write(`[remote-pi] Channel push enabled — incoming messages wake Claude\n\n`);
2722
+ // Inject the agent-network protocol as a system prompt instead of deploying a
2723
+ // skill file into ~/.claude. Anyone running `remote-pi claude` is here to use
2724
+ // the mesh, so load the protocol unconditionally — no lazy skill gating, no
2725
+ // global skills-dir pollution, and the packaged file is the single source of
2726
+ // truth shared with the Pi runtime. Skipped only if the file is missing.
2727
+ const skillPath = _agentNetworkSkillPath();
2740
2728
  // Launch flags:
2741
2729
  // --dangerously-load-development-channels TAG — enable claude/channel push
2742
2730
  // for our local (non-allowlisted) server, so incoming mesh messages
@@ -2745,9 +2733,23 @@ async function _cmdClaudeCli(args) {
2745
2733
  // (`plugin:<name>@<marketplace>` is the plugin form). Shows a one-time
2746
2734
  // confirmation dialog at startup.
2747
2735
  // --dangerously-skip-permissions — auto-approve tool calls
2736
+ // --append-system-prompt-file=<skill> — load the mesh protocol
2737
+ // `--append-system-prompt-file` uses the glued `--flag=value` form (a SINGLE
2738
+ // argv token) on purpose: tools that restore a session by capturing and
2739
+ // replaying the live process's argv (e.g. cmux) drop the TRAILING token,
2740
+ // which here was the skill path — leaving a dangling `--append-system-prompt-file`
2741
+ // → `claude` aborts with "argument missing" and the session never comes back.
2742
+ // As one token, the worst case is the whole flag being dropped: claude still
2743
+ // starts (just without the injected protocol), which is recoverable instead
2744
+ // of fatal. (The channels flag stays a separate pair — it's never last, so
2745
+ // it isn't affected, and we don't risk a parser that may not accept `=`.)
2746
+ // Any extra args the user passed (e.g. `--resume`, `-c`) are appended last so
2747
+ // they reach the claude binary; ours come first as sensible defaults.
2748
2748
  spawnSync("claude", [
2749
2749
  "--dangerously-load-development-channels", `server:${SERVER_NAME}`,
2750
2750
  "--dangerously-skip-permissions",
2751
+ ...(skillPath ? [`--append-system-prompt-file=${skillPath}`] : []),
2752
+ ...passthroughArgs,
2751
2753
  ], {
2752
2754
  cwd: absCwd,
2753
2755
  stdio: "inherit",