pi-link 0.1.5 → 0.1.7

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
@@ -2,7 +2,31 @@
2
2
 
3
3
  All notable changes to pi-link are documented here.
4
4
 
5
- This changelog is based on the git history from `2026-03-21` through `2026-04-02` (current). Versions correspond to npm publishes.
5
+ This changelog is based on the git history from `2026-03-21` through `2026-04-03` (current). Versions correspond to npm publishes.
6
+
7
+ ---
8
+
9
+ ## 0.1.7 — 2026-04-09
10
+
11
+ ### Added
12
+
13
+ - **Bundled `pi-link-coordination` skill.** The coordination guide is now shipped with the package via `pi.skills` manifest entry. Installing pi-link now auto-loads the skill — no manual copy required. The skill provides on-demand guidance for agents delegating work across terminals: tool selection (`link_prompt` vs `link_send`), the golden rule (no sync-after-async on same target), callback contracts, and coordination modes.
14
+
15
+ ---
16
+
17
+ ## 0.1.6 — 2026-04-03
18
+
19
+ **Pi 0.65.0 migration.** Pi removed `session_switch` and `session_fork` events. All session transitions (startup, reload, `/new`, `/resume`, `/fork`) now fire `session_start` with `event.reason`. Each transition tears down the old extension runtime via `session_shutdown` before creating a fresh one — so there is no live connection to update in-place across sessions.
20
+
21
+ ### Added
22
+
23
+ - **Persistent connection intent.** `/link-connect` and `/link-disconnect` now save their state to the session via `pi.appendEntry("link-active", ...)`. On `session_start`, the saved preference is checked before falling back to `--link`. Connect once and it stays connected across session resumes without needing the flag. Explicit user intent (`link-active`) takes precedence over the `--link` flag default.
24
+
25
+ ### Removed
26
+
27
+ - **`cwd_update` message type.** With the old `session_switch` gone, mid-session cwd changes have no trigger. Working directories are now only reported on connect (via `register`/`welcome`). Protocol returns to 9 message types.
28
+
29
+ - **`session_switch` handler.** The 77-line in-place mutation matrix (hub rename, cwd diffing, client reconnect) is dead under the new lifecycle. Replaced by a unified `session_start` handler + `shouldConnect()` helper.
6
30
 
7
31
  ---
8
32
 
package/README.md CHANGED
@@ -191,7 +191,7 @@ Send a prompt to a remote terminal and **wait** for the LLM's response (synchron
191
191
 
192
192
  Lists all connected terminals with role info, live agent status, working directory, and self-identification. Takes no parameters.
193
193
 
194
- Each terminal reports its current working directory on connect and on session switch. `link_list` shows the full absolute path so agents can choose the right target, use explicit paths when terminals differ, and catch wrong-project mistakes early.
194
+ Each terminal reports its current working directory on connect. `link_list` shows the full absolute path so agents can choose the right target, use explicit paths when terminals differ, and catch wrong-project mistakes early.
195
195
 
196
196
  Each terminal's status is derived automatically from Pi lifecycle events — agents can't set it manually. Three states:
197
197
 
@@ -397,7 +397,7 @@ The `pi.extensions` field tells Pi which files to load as extensions. Here it po
397
397
 
398
398
  ### Protocol
399
399
 
400
- The wire protocol consists of **10 message types**, all serialized as JSON over WebSocket frames. New cwd-related fields are optional for backward compatibility.
400
+ The wire protocol consists of **9 message types**, all serialized as JSON over WebSocket frames. Cwd-related fields are optional for backward compatibility.
401
401
 
402
402
  | Type | Direction | Purpose |
403
403
  | ----------------- | --------------- | ----------------------------------------------------------------------- |
@@ -409,7 +409,6 @@ The wire protocol consists of **10 message types**, all serialized as JSON over
409
409
  | `prompt_request` | Any → Any | Request a remote terminal to execute a prompt |
410
410
  | `prompt_response` | Any → Any | Response carrying the remote prompt result |
411
411
  | `status_update` | Any → Hub → All | Terminal broadcasts its agent status change |
412
- | `cwd_update` | Any → Hub → All | Terminal broadcasts a cwd change |
413
412
  | `error` | Hub → Client | Error notification |
414
413
 
415
414
  ### Message Flow Examples
@@ -429,7 +428,7 @@ Client Hub
429
428
  | |
430
429
  ```
431
430
 
432
- Hub then broadcasts `terminal_joined` to the other connected terminals. The `welcome` message includes status and cwd snapshots for all connected terminals (fields omitted above for brevity). `terminal_joined` also includes the new terminal's optional cwd, and mid-session cwd changes are distributed via `cwd_update`.
431
+ Hub then broadcasts `terminal_joined` to the other connected terminals. The `welcome` message includes status and cwd snapshots for all connected terminals (fields omitted above for brevity). `terminal_joined` also includes the new terminal's optional cwd.
433
432
 
434
433
  **Sending a chat message:**
435
434
 
@@ -482,7 +481,7 @@ Default names are random 4-character hex IDs: `t-a1b2`, `t-c3d4`, etc.
482
481
  | `agentRunning` | `boolean` | Whether an agent run is active; blocks incoming remote prompts |
483
482
  | `activeToolName` | `string \| null` | Name of the currently executing tool (drives `tool:<name>` status) |
484
483
  | `stateSince` | `number` | Timestamp of last status change (used for duration display) |
485
- | `currentCwd` | `string` | Current working directory reported to peers on connect and session switch |
484
+ | `currentCwd` | `string` | Current working directory reported to peers on connect |
486
485
  | `manuallyDisconnected` | `boolean` | Set by `/link-disconnect`; suppresses auto-reconnect |
487
486
  | `pendingRemotePrompt` | `object \| null` | Tracks the single in-flight remote prompt execution |
488
487
  | `pendingPromptResponses` | `Map` | Outstanding prompt RPCs awaiting responses (includes inactivity + ceiling timers per entry) |
package/index.ts CHANGED
@@ -49,11 +49,6 @@ interface TerminalJoinedMsg {
49
49
  terminals: string[];
50
50
  cwd?: string;
51
51
  }
52
- interface CwdUpdateMsg {
53
- type: "cwd_update";
54
- name: string;
55
- cwd: string;
56
- }
57
52
  interface TerminalLeftMsg {
58
53
  type: "terminal_left";
59
54
  name: string;
@@ -105,7 +100,6 @@ type LinkMessage =
105
100
  | PromptRequestMsg
106
101
  | PromptResponseMsg
107
102
  | StatusUpdateMsg
108
- | CwdUpdateMsg
109
103
  | ErrorMsg;
110
104
 
111
105
  // ─── Extension ───────────────────────────────────────────────────────────────
@@ -240,17 +234,18 @@ export default function (pi: ExtensionAPI) {
240
234
  return normalized;
241
235
  }
242
236
 
243
- function pushCwdUpdate() {
244
- const msg: CwdUpdateMsg = {
245
- type: "cwd_update",
246
- name: terminalName,
247
- cwd: currentCwd,
248
- };
249
- if (role === "hub") {
250
- hubBroadcast(msg, terminalName);
251
- } else if (role === "client" && ws?.readyState === WebSocket.OPEN) {
252
- ws.send(JSON.stringify(msg));
253
- }
237
+ // ── Connection intent ──────────────────────────────────────────────────
238
+
239
+ function shouldConnect(_ctx: ExtensionContext): boolean {
240
+ const saved = _ctx.sessionManager
241
+ .getEntries()
242
+ .filter(
243
+ (e: { type: string; customType?: string }) =>
244
+ e.type === "custom" && e.customType === "link-active",
245
+ )
246
+ .pop() as { data?: { active?: boolean } } | undefined;
247
+ if (saved?.data?.active !== undefined) return saved.data.active;
248
+ return pi.getFlag("link") === true;
254
249
  }
255
250
 
256
251
  // ── Pending prompt helpers ───────────────────────────────────────────────
@@ -452,10 +447,6 @@ export default function (pi: ExtensionAPI) {
452
447
  resetInactivityFor(msg.name);
453
448
  break;
454
449
 
455
- case "cwd_update":
456
- terminalCwds.set(msg.name, msg.cwd);
457
- break;
458
-
459
450
  // ── Chat message ──
460
451
  case "chat":
461
452
  pi.sendMessage(
@@ -588,21 +579,6 @@ export default function (pi: ExtensionAPI) {
588
579
  return;
589
580
  }
590
581
 
591
- // Cwd update — store and relay to other clients only
592
- if (msg.type === "cwd_update") {
593
- hubTerminalCwds.set(clientName, msg.cwd);
594
- const normalized: CwdUpdateMsg = {
595
- type: "cwd_update",
596
- name: clientName,
597
- cwd: msg.cwd,
598
- };
599
- const json = JSON.stringify(normalized);
600
- for (const [otherWs, name] of hubClients) {
601
- if (name !== clientName) otherWs.send(json);
602
- }
603
- return;
604
- }
605
-
606
582
  // Route chat / prompt messages
607
583
  if (
608
584
  msg.type === "chat" ||
@@ -817,90 +793,13 @@ export default function (pi: ExtensionAPI) {
817
793
  terminalName = preferredName;
818
794
  }
819
795
 
820
- if (pi.getFlag("link") === true) await initialize();
796
+ if (shouldConnect(_ctx)) await initialize();
821
797
  });
822
798
 
823
799
  pi.on("session_shutdown", async () => {
824
800
  cleanup();
825
801
  });
826
802
 
827
- pi.on("session_switch", async (_event, _ctx) => {
828
- ctx = _ctx;
829
-
830
- // 1. Cwd change detection (always, before any name logic)
831
- const newCwd = _ctx.cwd;
832
- const cwdChanged = newCwd !== currentCwd;
833
- if (cwdChanged) currentCwd = newCwd;
834
-
835
- // 2. Restore preferred name from the new session
836
- const saved = _ctx.sessionManager
837
- .getEntries()
838
- .filter(
839
- (e: { type: string; customType?: string }) =>
840
- e.type === "custom" && e.customType === "link-name",
841
- )
842
- .pop() as { data?: { name?: string } } | undefined;
843
-
844
- preferredName = saved?.data?.name ?? null;
845
- const desiredName = preferredName ?? `t-${crypto.randomUUID().slice(0, 4)}`;
846
- const nameChanged = desiredName !== terminalName;
847
-
848
- if (!nameChanged && !cwdChanged) return; // nothing to do
849
-
850
- if (!nameChanged) {
851
- // Name stayed the same, but cwd changed — push cwd update
852
- pushCwdUpdate();
853
- return;
854
- }
855
-
856
- // Name changed (cwd may or may not have changed too)
857
- if (role === "hub") {
858
- // Hub rename in-place — avoid tearing down the server
859
- const takenByOther = Array.from(hubClients.values()).includes(
860
- desiredName,
861
- );
862
- if (takenByOther) {
863
- // Can't use preferred name — keep current identity
864
- ctx?.ui.notify(
865
- `Session preferred name "${desiredName}" is taken, keeping "${terminalName}"`,
866
- "warning",
867
- );
868
- // Still push cwd update under current name if cwd changed
869
- if (cwdChanged) pushCwdUpdate();
870
- return;
871
- }
872
- const old = terminalName;
873
- terminalName = desiredName;
874
- const list = terminalList();
875
- connectedTerminals = list;
876
- updateStatus();
877
- // Notify clients only — hub already updated local state
878
- hubBroadcast(
879
- { type: "terminal_left", name: old, terminals: list },
880
- terminalName,
881
- );
882
- hubBroadcast(
883
- {
884
- type: "terminal_joined",
885
- name: desiredName,
886
- terminals: list,
887
- cwd: currentCwd,
888
- },
889
- terminalName,
890
- );
891
- pushStatus(true);
892
- } else if (role === "client") {
893
- // Client — disconnect and reconnect with new name (register includes cwd)
894
- terminalName = desiredName;
895
- disconnect();
896
- manuallyDisconnected = false;
897
- await initialize();
898
- } else {
899
- // Disconnected — just update local name
900
- terminalName = desiredName;
901
- }
902
- });
903
-
904
803
  pi.on("agent_start", async () => {
905
804
  agentRunning = true;
906
805
  activeToolName = null;
@@ -1384,18 +1283,23 @@ export default function (pi: ExtensionAPI) {
1384
1283
  pi.registerCommand("link-disconnect", {
1385
1284
  description: "Disconnect from the link",
1386
1285
  handler: async (_args, _ctx) => {
1286
+ pi.appendEntry("link-active", { active: false });
1287
+ manuallyDisconnected = true;
1387
1288
  if (role === "disconnected") {
1388
- _ctx.ui.notify("Already disconnected", "info");
1289
+ if (reconnectTimer) {
1290
+ clearTimeout(reconnectTimer);
1291
+ reconnectTimer = null;
1292
+ }
1293
+ _ctx.ui.notify("Link disconnected", "info");
1389
1294
  return;
1390
1295
  }
1391
- manuallyDisconnected = true;
1392
1296
  disconnect();
1393
1297
  _ctx.ui.notify("Disconnected from link", "info");
1394
1298
  },
1395
1299
  });
1396
1300
 
1397
1301
  pi.registerCommand("link-connect", {
1398
- description: "Connect to the link (after manual disconnect)",
1302
+ description: "Connect to the link",
1399
1303
  handler: async (_args, _ctx) => {
1400
1304
  if (role !== "disconnected") {
1401
1305
  _ctx.ui.notify(
@@ -1404,6 +1308,7 @@ export default function (pi: ExtensionAPI) {
1404
1308
  );
1405
1309
  return;
1406
1310
  }
1311
+ pi.appendEntry("link-active", { active: true });
1407
1312
  manuallyDisconnected = false;
1408
1313
  await initialize();
1409
1314
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-link",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "WebSocket-based inter-terminal communication for Pi. Connect multiple Pi terminals over a local link network.",
5
5
  "author": "alvivar",
6
6
  "license": "MIT",
@@ -25,6 +25,9 @@
25
25
  "pi": {
26
26
  "extensions": [
27
27
  "./index.ts"
28
+ ],
29
+ "skills": [
30
+ "./skills"
28
31
  ]
29
32
  }
30
33
  }
@@ -0,0 +1,114 @@
1
+ ---
2
+ name: pi-link-coordination
3
+ description: Guidance for coordinating work across Pi terminals using pi-link. Use when delegating tasks, choosing between link_prompt and link_send, planning async vs sync work, batching parallel jobs, or avoiding busy/conflict patterns.
4
+ ---
5
+
6
+ # Pi-Link Coordination
7
+
8
+ How to coordinate work across Pi terminals via pi-link.
9
+
10
+ ---
11
+
12
+ ## Tool Selection Rule
13
+
14
+ - Need the answer back now? → `link_prompt`
15
+ - Need autonomous work done? → `link_send(triggerTurn: true)`
16
+ - Need to notify only? → `link_send(triggerTurn: false)`
17
+
18
+ ---
19
+
20
+ ## The Golden Rule
21
+
22
+ > After `link_send(triggerTurn: true)` to terminal X, do not `link_prompt` X until X sends a completion callback.
23
+
24
+ Pick one mode per terminal per task. Mixing sync and async on the same terminal is the most common coordination failure.
25
+
26
+ ---
27
+
28
+ ## The Tools
29
+
30
+ ### `link_list`
31
+
32
+ Returns connected terminals with names, live status (`idle`, `thinking`, `tool:<name>`), and working directory (cwd). Use before delegating when availability or path context is uncertain.
33
+
34
+ ### `link_prompt`
35
+
36
+ Synchronous RPC. Send a prompt, wait for the response.
37
+
38
+ - Fails immediately if target is missing, self, disconnects, or busy (local work or another remote prompt)
39
+ - 90s inactivity timeout, 30min hard ceiling
40
+ - Remote agent doesn't share your context — prompts must be self-contained
41
+ - Include: goal, scope, constraints, output format, done condition
42
+
43
+ ### `link_send`
44
+
45
+ Fire-and-forget. Send to one terminal or `to: "*"` to broadcast (excludes sender).
46
+
47
+ Set `triggerTurn: true` to activate the receiver's LLM. The sender does **not** get the response back.
48
+
49
+ **Callback contract for `triggerTurn: true`:** ask the receiver to reply via `link_send` with:
50
+
51
+ - `DONE` signal
52
+ - Output paths / artifacts created
53
+ - Blockers or open questions
54
+
55
+ ---
56
+
57
+ ## Operating Constraints
58
+
59
+ - **One remote prompt at a time per target.** Concurrent requests rejected as busy.
60
+ - **No shared context.** Every remote prompt must be self-contained.
61
+ - **Messages are ephemeral.** Offline terminals lose messages.
62
+ - **Localhost only.** Same machine.
63
+ - **Cwd is a hint, not proof.** Same cwd ≠ same workspace/branch/access. Use explicit paths; absolute when cwds differ or shared-root assumptions are unclear.
64
+ - **Naming:** `role@domain` (e.g., `builder@pi-link`). Only talk to your own domain unless told otherwise.
65
+
66
+ ---
67
+
68
+ ## Coordination Modes
69
+
70
+ ### Sync ask — `link_prompt`
71
+
72
+ For answers, review, analysis you need back now. One terminal at a time. Keep scope focused to avoid timeout.
73
+
74
+ ### Async delegate — `link_send(triggerTurn: true)`
75
+
76
+ For autonomous work. Require the callback contract (DONE + paths + blockers). Do your own work in parallel. Don't `link_prompt` the target until the callback arrives.
77
+
78
+ ### Parallel batch — async to multiple terminals
79
+
80
+ Distribute independent tasks. Use explicit paths (absolute if cwds differ). Wait for all callbacks, then synthesize. Don't prompt any dispatched terminal until its callback arrives.
81
+
82
+ ---
83
+
84
+ ## Anti-Patterns
85
+
86
+ **❌ Mixing async and sync on the same terminal**
87
+ Dispatched with `link_send(triggerTurn: true)` then sent a `link_prompt` → rejected as busy. See Golden Rule.
88
+
89
+ **❌ Using `link_send` when you need the response**
90
+ Result disappears. Use `link_prompt`.
91
+
92
+ **❌ Vague prompts**
93
+ "Fix the bug" is useless. Include file, line, root cause, expected fix.
94
+
95
+ **❌ No completion callback on async work**
96
+ Always require DONE + artifact paths + blockers.
97
+
98
+ **❌ Circular delegation**
99
+ A → B → C → A = deadlock. Maintain clear hierarchy.
100
+
101
+ **❌ Skipping `link_list` before retrying a busy target**
102
+ Check status before re-sending.
103
+
104
+ ---
105
+
106
+ ## Quick Reference
107
+
108
+ | I need to... | Tool | Mode |
109
+ | -------------------------------- | ------------------------------- | --------------- |
110
+ | See who's available | `link_list` | — |
111
+ | Get an answer from another agent | `link_prompt` | Synchronous |
112
+ | Delegate autonomous work | `link_send(triggerTurn: true)` | Asynchronous |
113
+ | Notify without activating | `link_send(triggerTurn: false)` | Fire-and-forget |
114
+ | Broadcast to all | `link_send(to: "*")` | Broadcast |
package/sync.ffs_db CHANGED
Binary file