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.
- package/README.md +12 -8
- package/index.ts +106 -9
- 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
|
|
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(
|
|
544
|
+
function connectAsClient(): Promise<boolean> {
|
|
544
545
|
return new Promise((resolve) => {
|
|
545
|
-
const socket = new WebSocket(`ws://127.0.0.1:${
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|