pi-link 0.1.2 → 0.1.3

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 (3) hide show
  1. package/README.md +12 -8
  2. package/index.ts +106 -9
  3. package/package.json +30 -30
package/README.md CHANGED
@@ -209,13 +209,13 @@ link (hub) 3 terminal(s)
209
209
 
210
210
  ## Slash Commands
211
211
 
212
- | Command | Purpose |
213
- | ----------------------- | -------------------------------------------------------------------------------------------------------- |
214
- | `/link` | Show link status (name, role, online count, agent status per terminal) |
215
- | `/link-name [name]` | Rename this terminal. With no argument, adopts the current Pi session name if available. Collision-safe. |
216
- | `/link-broadcast <msg>` | Broadcast a chat message to all other terminals |
217
- | `/link-connect` | Connect to Pi Link (works anytime, with or without `--link`) |
218
- | `/link-disconnect` | Disconnect from Pi Link and suppress auto-reconnect (overrides `--link`) |
212
+ | Command | Purpose |
213
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------ |
214
+ | `/link` | Show link status (name, role, online count, agent status per terminal) |
215
+ | `/link-name [name]` | Rename and save as this session's preferred link name. With no argument, adopts the Pi session name. Restored on resume. |
216
+ | `/link-broadcast <msg>` | Broadcast a chat message to all other terminals |
217
+ | `/link-connect` | Connect to Pi Link (works anytime, with or without `--link`) |
218
+ | `/link-disconnect` | Disconnect from Pi Link and suppress auto-reconnect (overrides `--link`) |
219
219
 
220
220
  ### Examples
221
221
 
@@ -243,6 +243,8 @@ link (hub) 3 terminal(s)
243
243
  ✓ Pi Link hub started on :9900 as "orchestrator" ... if no hub exists
244
244
  ```
245
245
 
246
+ **Name persistence:** `/link-name` saves your preferred name to the session. Resume that session later and your name is restored automatically. If the name is taken, the hub assigns a variant (e.g., `"builder-2"`), but your preferred name stays saved — the next reconnect retries it. Both `/link-name builder` and `/link-name` (no args) count as explicit saves; hub-assigned variants like `"builder-2"` are never persisted.
247
+
246
248
  See [Configuration](#configuration) for details on `--link`, `/link-connect`, and `/link-disconnect` behavior.
247
249
 
248
250
  ---
@@ -439,12 +441,14 @@ Client A Hub Client B
439
441
  |<-----------------| |
440
442
  ```
441
443
 
442
- ### Name Uniqueness
444
+ ### Name Uniqueness & Persistence
443
445
 
444
446
  The hub enforces unique terminal names via a `uniqueName()` function. If `"builder"` is already taken, the next terminal requesting that name is assigned `"builder-2"`, then `"builder-3"`, and so on.
445
447
 
446
448
  Default names are random 4-character hex IDs: `t-a1b2`, `t-c3d4`, etc.
447
449
 
450
+ **Persistence:** `/link-name` saves the preferred name to the session via `pi.appendEntry("link-name", { name })`. On session resume, the saved name is restored and requested from the hub. Only explicit `/link-name` calls persist — hub-assigned variants like `"builder-2"` are not saved. On reconnect, the terminal always requests the preferred name, not the last runtime name.
451
+
448
452
  **Rename guards:**
449
453
 
450
454
  - If you're already using the requested name, `/link-name` returns early (`"Already using..."`).
package/index.ts CHANGED
@@ -118,6 +118,7 @@ export default function (pi: ExtensionAPI) {
118
118
 
119
119
  let role: "hub" | "client" | "disconnected" = "disconnected";
120
120
  let terminalName = `t-${crypto.randomUUID().slice(0, 4)}`;
121
+ let preferredName: string | null = null;
121
122
  let connectedTerminals: string[] = [];
122
123
  let ctx: ExtensionContext | undefined;
123
124
  let disposed = false;
@@ -540,20 +541,20 @@ export default function (pi: ExtensionAPI) {
540
541
 
541
542
  // ── Connect as client ────────────────────────────────────────────────────
542
543
 
543
- function connectAsClient(port: number): Promise<boolean> {
544
+ function connectAsClient(): Promise<boolean> {
544
545
  return new Promise((resolve) => {
545
- const socket = new WebSocket(`ws://127.0.0.1:${port}`);
546
+ const socket = new WebSocket(`ws://127.0.0.1:${DEFAULT_PORT}`);
546
547
  let resolved = false;
547
548
 
548
549
  socket.on("open", () => {
549
550
  ws = socket;
550
551
  role = "client";
551
552
  resolved = true;
552
- // Register with the hub
553
+ // Register with preferred name if available, otherwise current name
553
554
  socket.send(
554
555
  JSON.stringify({
555
556
  type: "register",
556
- name: terminalName,
557
+ name: preferredName ?? terminalName,
557
558
  } satisfies RegisterMsg),
558
559
  );
559
560
  resolve(true);
@@ -594,7 +595,7 @@ export default function (pi: ExtensionAPI) {
594
595
  if (disposed) return;
595
596
 
596
597
  // Try connecting to an existing hub
597
- if (await connectAsClient(DEFAULT_PORT)) return;
598
+ if (await connectAsClient()) return;
598
599
 
599
600
  // No hub found — become the hub
600
601
  if (await startHub()) return;
@@ -663,6 +664,20 @@ export default function (pi: ExtensionAPI) {
663
664
 
664
665
  pi.on("session_start", async (_event, _ctx) => {
665
666
  ctx = _ctx;
667
+
668
+ // Restore preferred link name from session
669
+ const saved = _ctx.sessionManager
670
+ .getEntries()
671
+ .filter(
672
+ (e: { type: string; customType?: string }) =>
673
+ e.type === "custom" && e.customType === "link-name",
674
+ )
675
+ .pop() as { data?: { name?: string } } | undefined;
676
+ if (saved?.data?.name) {
677
+ preferredName = saved.data.name;
678
+ terminalName = preferredName;
679
+ }
680
+
666
681
  if (pi.getFlag("link") === true) await initialize();
667
682
  });
668
683
 
@@ -670,6 +685,63 @@ export default function (pi: ExtensionAPI) {
670
685
  cleanup();
671
686
  });
672
687
 
688
+ pi.on("session_switch", async (_event, _ctx) => {
689
+ ctx = _ctx;
690
+
691
+ // Restore preferred name from the new session
692
+ const saved = _ctx.sessionManager
693
+ .getEntries()
694
+ .filter(
695
+ (e: { type: string; customType?: string }) =>
696
+ e.type === "custom" && e.customType === "link-name",
697
+ )
698
+ .pop() as { data?: { name?: string } } | undefined;
699
+
700
+ preferredName = saved?.data?.name ?? null;
701
+ const desiredName = preferredName ?? `t-${crypto.randomUUID().slice(0, 4)}`;
702
+
703
+ if (desiredName === terminalName) return; // no identity change needed
704
+
705
+ if (role === "hub") {
706
+ // Hub rename in-place — avoid tearing down the server
707
+ const takenByOther = Array.from(hubClients.values()).includes(
708
+ desiredName,
709
+ );
710
+ if (takenByOther) {
711
+ // Can't use preferred name — keep current identity
712
+ ctx?.ui.notify(
713
+ `Session preferred name "${desiredName}" is taken, keeping "${terminalName}"`,
714
+ "warning",
715
+ );
716
+ return;
717
+ }
718
+ const old = terminalName;
719
+ terminalName = desiredName;
720
+ const list = terminalList();
721
+ connectedTerminals = list;
722
+ updateStatus();
723
+ // Notify clients only — hub already updated local state
724
+ hubBroadcast(
725
+ { type: "terminal_left", name: old, terminals: list },
726
+ terminalName,
727
+ );
728
+ hubBroadcast(
729
+ { type: "terminal_joined", name: desiredName, terminals: list },
730
+ terminalName,
731
+ );
732
+ pushStatus(true);
733
+ } else if (role === "client") {
734
+ // Client — disconnect and reconnect with new name
735
+ terminalName = desiredName;
736
+ disconnect();
737
+ manuallyDisconnected = false;
738
+ await initialize();
739
+ } else {
740
+ // Disconnected — just update local name
741
+ terminalName = desiredName;
742
+ }
743
+ });
744
+
673
745
  pi.on("agent_start", async () => {
674
746
  agentRunning = true;
675
747
  activeToolName = null;
@@ -836,6 +908,13 @@ export default function (pi: ExtensionAPI) {
836
908
  async execute(_toolCallId, params, signal) {
837
909
  if (role === "disconnected") return notConnectedResult();
838
910
 
911
+ if (!connectedTerminals.includes(params.to)) {
912
+ return textResult(
913
+ `Terminal "${params.to}" not found. Connected: ${connectedTerminals.join(", ")}`,
914
+ { to: params.to, error: "not_found" },
915
+ );
916
+ }
917
+
839
918
  const requestId = crypto.randomUUID();
840
919
 
841
920
  return new Promise((resolve) => {
@@ -1019,12 +1098,23 @@ export default function (pi: ExtensionAPI) {
1019
1098
  }
1020
1099
  }
1021
1100
 
1022
- if (newName === terminalName) {
1101
+ if (newName === terminalName && newName === preferredName) {
1023
1102
  _ctx.ui.notify(`Already using "${newName}"`, "info");
1024
1103
  return;
1025
1104
  }
1026
1105
 
1027
- // If we're the hub, check uniqueness before renaming
1106
+ function savePreference() {
1107
+ preferredName = newName;
1108
+ pi.appendEntry("link-name", { name: preferredName });
1109
+ }
1110
+
1111
+ if (newName === terminalName) {
1112
+ savePreference();
1113
+ _ctx.ui.notify(`Saved "${newName}" as preferred link name`, "info");
1114
+ return;
1115
+ }
1116
+
1117
+ // If we're the hub, check uniqueness before persisting
1028
1118
  if (role === "hub") {
1029
1119
  // Check if name is taken by another terminal
1030
1120
  const takenByOther = Array.from(hubClients.values()).includes(newName);
@@ -1040,15 +1130,21 @@ export default function (pi: ExtensionAPI) {
1040
1130
  const list = terminalList();
1041
1131
  connectedTerminals = list;
1042
1132
  updateStatus();
1043
- hubBroadcast({ type: "terminal_left", name: old, terminals: list });
1133
+ // Notify clients only hub already updated local state
1134
+ hubBroadcast(
1135
+ { type: "terminal_left", name: old, terminals: list },
1136
+ terminalName,
1137
+ );
1044
1138
  hubBroadcast(
1045
1139
  { type: "terminal_joined", name: newName, terminals: list },
1046
- newName,
1140
+ terminalName,
1047
1141
  );
1048
1142
  pushStatus(true);
1143
+ savePreference();
1049
1144
  _ctx.ui.notify(`Renamed to "${newName}"`, "info");
1050
1145
  } else if (role === "client") {
1051
1146
  // Reconnect with new name — hub will enforce uniqueness via register
1147
+ savePreference();
1052
1148
  terminalName = newName;
1053
1149
  ws?.close();
1054
1150
  // Reconnect will happen via the onClose handler → scheduleReconnect
@@ -1057,6 +1153,7 @@ export default function (pi: ExtensionAPI) {
1057
1153
  "info",
1058
1154
  );
1059
1155
  } else {
1156
+ savePreference();
1060
1157
  terminalName = newName;
1061
1158
  _ctx.ui.notify(`Name set to "${newName}" (not connected)`, "info");
1062
1159
  }
package/package.json CHANGED
@@ -1,30 +1,30 @@
1
- {
2
- "name": "pi-link",
3
- "version": "0.1.2",
4
- "description": "WebSocket-based inter-terminal communication for Pi. Connect multiple Pi terminals over a local link network.",
5
- "author": "alvivar",
6
- "license": "MIT",
7
- "repository": {
8
- "type": "git",
9
- "url": "https://github.com/alvivar/pi-link"
10
- },
11
- "keywords": [
12
- "pi-package",
13
- "pi",
14
- "link",
15
- "websocket",
16
- "terminal",
17
- "multi-agent"
18
- ],
19
- "dependencies": {
20
- "ws": "^8.20.0"
21
- },
22
- "devDependencies": {
23
- "@types/ws": "^8.18.1"
24
- },
25
- "pi": {
26
- "extensions": [
27
- "./index.ts"
28
- ]
29
- }
30
- }
1
+ {
2
+ "name": "pi-link",
3
+ "version": "0.1.3",
4
+ "description": "WebSocket-based inter-terminal communication for Pi. Connect multiple Pi terminals over a local link network.",
5
+ "author": "alvivar",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/alvivar/pi-link"
10
+ },
11
+ "keywords": [
12
+ "pi-package",
13
+ "pi",
14
+ "link",
15
+ "websocket",
16
+ "terminal",
17
+ "multi-agent"
18
+ ],
19
+ "dependencies": {
20
+ "ws": "^8.20.0"
21
+ },
22
+ "devDependencies": {
23
+ "@types/ws": "^8.18.1"
24
+ },
25
+ "pi": {
26
+ "extensions": [
27
+ "./index.ts"
28
+ ]
29
+ }
30
+ }