pi-link 0.1.8 → 0.1.9
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 +10 -0
- package/README.md +51 -18
- package/bin/pi-link.mjs +154 -0
- package/index.ts +32 -17
- package/package.json +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,16 @@ This changelog is based on the git history from `2026-03-21` (initial commit) th
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## 0.1.9 — 2026-04-22
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`--link-name <name>` flag.** Connect to link with a chosen terminal name on startup. Implies `--link` (no need for both). Persists the name and sets the Pi session name if currently unnamed. Name precedence: `--link-name` > saved `/link-name` > session name > random `t-xxxx`.
|
|
14
|
+
|
|
15
|
+
- **`pi-link start` CLI.** New bin script (`bin/pi-link.mjs`) for session-by-name resume. `pi-link start worker-1` scans `~/.pi/agent/sessions/` for a matching session name — one match resumes it, no match creates a new session, multiple matches prints candidates and exits. Extra Pi flags pass through: `pi-link start worker-1 --model sonnet --thinking high`. Local-cwd sessions prioritized.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
9
19
|
## 0.1.8 — 2026-04-16
|
|
10
20
|
|
|
11
21
|
### Added
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A WebSocket-based inter-terminal communication system that creates a local network between multiple Pi coding agent terminals. Enables terminals to discover each other, exchange messages, and orchestrate work across agents - all automatically on `localhost`.
|
|
4
4
|
|
|
5
|
-
> Self-contained TypeScript in a single `index.ts` file. Start Pi with `--link` to enable.
|
|
5
|
+
> Self-contained TypeScript in a single `index.ts` file. Start Pi with `--link` or `--link-name <name>` to enable.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -57,16 +57,23 @@ pi uninstall npm:pi-link
|
|
|
57
57
|
|
|
58
58
|
### Usage
|
|
59
59
|
|
|
60
|
-
Link is **off by default**. Start Pi with
|
|
60
|
+
Link is **off by default**. Start Pi with `--link-name` to connect with a meaningful name:
|
|
61
61
|
|
|
62
62
|
```
|
|
63
63
|
Terminal 1 Terminal 2
|
|
64
64
|
---------- ----------
|
|
65
|
-
$ pi --link
|
|
66
|
-
✓ Link hub started on :9900 as "
|
|
65
|
+
$ pi --link-name builder $ pi --link-name reviewer
|
|
66
|
+
✓ Link hub started on :9900 as "builder" ✓ Joined link as "reviewer" (2 online)
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
Or use `pi-link start` to resume an existing session by name (or create one):
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pi-link start worker-1 # resume or create session "worker-1"
|
|
73
|
+
pi-link start worker-1 --model sonnet # with extra Pi flags
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`pi --link` also works (connects with an auto-generated name). Already in a session without either flag? Connect mid-session with `/link-connect`.
|
|
70
77
|
|
|
71
78
|
Use `/link` in any terminal to check status, or let the LLM tools handle cross-terminal coordination.
|
|
72
79
|
|
|
@@ -124,18 +131,41 @@ Every other terminal sees:
|
|
|
124
131
|
|
|
125
132
|
## Configuration
|
|
126
133
|
|
|
127
|
-
Link is **off by default**. Without `--link`, the extension is completely silent
|
|
134
|
+
Link is **off by default**. Without `--link` or `--link-name`, the extension is completely silent — no status bar, no connections, no warnings.
|
|
128
135
|
|
|
129
|
-
| Method
|
|
130
|
-
|
|
|
131
|
-
| `pi --link
|
|
132
|
-
|
|
|
133
|
-
|
|
|
136
|
+
| Method | When | Auto-reconnect? |
|
|
137
|
+
| ----------------------- | ----------------------------------- | -------------------------------- |
|
|
138
|
+
| `pi --link-name <name>` | Connect on startup with a name | Yes |
|
|
139
|
+
| `pi --link` | Connect on startup (random name) | Yes |
|
|
140
|
+
| `pi-link start <name>` | Resume/create session, connect | Yes |
|
|
141
|
+
| `/link-connect` | Opt-in mid-session (no flag needed) | Yes |
|
|
142
|
+
| `/link-disconnect` | Opt-out mid-session | Suppressed until `/link-connect` |
|
|
134
143
|
|
|
135
|
-
|
|
144
|
+
`--link-name` implies `--link` — no need for both. It also persists the name and sets the Pi session name if the session is currently unnamed.
|
|
145
|
+
|
|
146
|
+
**Name precedence:** `--link-name` flag > saved `/link-name` > Pi session name > random `t-xxxx`.
|
|
147
|
+
|
|
148
|
+
`/link-connect` and `/link-disconnect` save their intent to the session — resume later and the connection state is restored without needing the flag. Explicit user intent takes precedence over `--link`.
|
|
136
149
|
|
|
137
150
|
Once connected, terminals discover each other on `127.0.0.1:9900`. See [Limitations](#limitations--design-decisions) for the hardcoded port.
|
|
138
151
|
|
|
152
|
+
### `pi-link start`
|
|
153
|
+
|
|
154
|
+
The `pi-link` CLI resolves sessions by display name:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
pi-link start <name> [pi-flags...]
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
- Scans `~/.pi/agent/sessions/` for sessions with a matching name
|
|
161
|
+
- **One match** → resumes that session with `--link-name <name>`
|
|
162
|
+
- **No match** → starts a new session with `--link-name <name>`
|
|
163
|
+
- **Multiple matches** → prints candidates (cwd, modified date, path) and exits
|
|
164
|
+
- Extra Pi flags pass through: `pi-link start worker-1 --model sonnet --thinking high`
|
|
165
|
+
- `--session` and `--link-name` cannot be passed as extra flags (managed by `pi-link start`)
|
|
166
|
+
|
|
167
|
+
Sessions in the current working directory are prioritized when sorting candidates.
|
|
168
|
+
|
|
139
169
|
---
|
|
140
170
|
|
|
141
171
|
## LLM Tools
|
|
@@ -293,7 +323,7 @@ The network topology is **hub-spoke (star)**:
|
|
|
293
323
|
|
|
294
324
|
### Auto-Discovery Protocol
|
|
295
325
|
|
|
296
|
-
The discovery sequence runs on startup (with `--link`) or when `/link-connect` is used. See [Configuration](#configuration) for details.
|
|
326
|
+
The discovery sequence runs on startup (with `--link` or `--link-name`) or when `/link-connect` is used. See [Configuration](#configuration) for details.
|
|
297
327
|
|
|
298
328
|
The sequence is a simple fallback:
|
|
299
329
|
|
|
@@ -377,6 +407,9 @@ When the hub goes down and a client promotes itself, terminal names and in-fligh
|
|
|
377
407
|
```json
|
|
378
408
|
{
|
|
379
409
|
"name": "pi-link",
|
|
410
|
+
"bin": {
|
|
411
|
+
"pi-link": "./bin/pi-link.mjs"
|
|
412
|
+
},
|
|
380
413
|
"dependencies": {
|
|
381
414
|
"ws": "^8.20.0"
|
|
382
415
|
},
|
|
@@ -390,7 +423,7 @@ When the hub goes down and a client promotes itself, terminal names and in-fligh
|
|
|
390
423
|
}
|
|
391
424
|
```
|
|
392
425
|
|
|
393
|
-
|
|
426
|
+
`pi.extensions` tells Pi which files to load as extensions. `pi.skills` registers bundled skill directories. `bin` exposes the `pi-link` CLI (see [Configuration](#configuration)).
|
|
394
427
|
|
|
395
428
|
---
|
|
396
429
|
|
|
@@ -529,11 +562,11 @@ The flush pipeline:
|
|
|
529
562
|
|
|
530
563
|
1. **Debounce** - `scheduleFlush(FLUSH_DELAY_MS)` coalesces burst arrivals (200ms window).
|
|
531
564
|
2. **Idle gate** - `flushInbox()` checks `ctx.isIdle()`. If busy, retries every 500ms.
|
|
532
|
-
3. **Batch**
|
|
533
|
-
4. **Deliver**
|
|
534
|
-
5. **Drain**
|
|
565
|
+
3. **Batch** - up to 20 messages or ~16 000 chars per delivery (soft cap - the first item is always included even if oversized).
|
|
566
|
+
4. **Deliver** - one `pi.sendMessage({ triggerTurn: true })` call with a `[Link: N message(s) received]` block.
|
|
567
|
+
5. **Drain** - if the inbox still has items, reschedule.
|
|
535
568
|
|
|
536
|
-
On `agent_end`, the inbox flush is kicked via `scheduleFlush(0)`
|
|
569
|
+
On `agent_end`, the inbox flush is kicked via `scheduleFlush(0)` - deferred to the next macrotask, by which time `ctx.isIdle()` returns `true`.
|
|
537
570
|
|
|
538
571
|
| Constant | Value | Purpose |
|
|
539
572
|
| ----------------- | ------ | ---------------------------------------- |
|
package/bin/pi-link.mjs
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// pi-link CLI — resolve session by name and launch Pi with --link-name
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// pi-link start <name> [pi-flags...]
|
|
7
|
+
//
|
|
8
|
+
// If a session named <name> exists, resumes it.
|
|
9
|
+
// If not, creates a new session.
|
|
10
|
+
// Always connects to the link as <name>.
|
|
11
|
+
|
|
12
|
+
import { readdir, stat } from "fs/promises";
|
|
13
|
+
import { createReadStream } from "fs";
|
|
14
|
+
import { createInterface } from "readline";
|
|
15
|
+
import { join } from "path";
|
|
16
|
+
import { homedir } from "os";
|
|
17
|
+
import { spawn } from "child_process";
|
|
18
|
+
|
|
19
|
+
const SESSIONS_DIR = join(homedir(), ".pi", "agent", "sessions");
|
|
20
|
+
|
|
21
|
+
// ── Session scanning ───────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
async function getSessionName(filePath) {
|
|
24
|
+
let name;
|
|
25
|
+
let cwd;
|
|
26
|
+
const rl = createInterface({ input: createReadStream(filePath, "utf-8"), crlfDelay: Infinity });
|
|
27
|
+
for await (const line of rl) {
|
|
28
|
+
if (!line) continue;
|
|
29
|
+
try {
|
|
30
|
+
const entry = JSON.parse(line);
|
|
31
|
+
if (entry.type === "session" && entry.cwd) cwd = entry.cwd;
|
|
32
|
+
if (entry.type === "session_info" && entry.name !== undefined) {
|
|
33
|
+
name = entry.name?.trim() || undefined;
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
// skip malformed lines
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return { name, cwd };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function findSessionsByName(targetName) {
|
|
43
|
+
let cwdDirs;
|
|
44
|
+
try {
|
|
45
|
+
cwdDirs = await readdir(SESSIONS_DIR, { withFileTypes: true });
|
|
46
|
+
} catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const matches = [];
|
|
51
|
+
|
|
52
|
+
for (const dir of cwdDirs) {
|
|
53
|
+
if (!dir.isDirectory()) continue;
|
|
54
|
+
const dirPath = join(SESSIONS_DIR, dir.name);
|
|
55
|
+
|
|
56
|
+
let files;
|
|
57
|
+
try {
|
|
58
|
+
files = await readdir(dirPath);
|
|
59
|
+
} catch {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
if (!file.endsWith(".jsonl")) continue;
|
|
65
|
+
const filePath = join(dirPath, file);
|
|
66
|
+
try {
|
|
67
|
+
const { name, cwd } = await getSessionName(filePath);
|
|
68
|
+
if (name === targetName) {
|
|
69
|
+
const stats = await stat(filePath);
|
|
70
|
+
matches.push({ path: filePath, cwd: cwd || "?", modified: stats.mtime });
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Local-first: current cwd matches before others, then by modified time
|
|
79
|
+
const localCwd = process.cwd();
|
|
80
|
+
matches.sort((a, b) => {
|
|
81
|
+
const aLocal = a.cwd === localCwd ? 1 : 0;
|
|
82
|
+
const bLocal = b.cwd === localCwd ? 1 : 0;
|
|
83
|
+
if (aLocal !== bLocal) return bLocal - aLocal;
|
|
84
|
+
return b.modified.getTime() - a.modified.getTime();
|
|
85
|
+
});
|
|
86
|
+
return matches;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── CLI ────────────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
const args = process.argv.slice(2);
|
|
92
|
+
const command = args[0];
|
|
93
|
+
|
|
94
|
+
if (command !== "start" || args.length < 2) {
|
|
95
|
+
console.log(`Usage: pi-link start <name> [pi-flags...]
|
|
96
|
+
|
|
97
|
+
Start Pi connected to the link as <name>.
|
|
98
|
+
Resumes a session named <name> if one exists, otherwise creates a new session.
|
|
99
|
+
|
|
100
|
+
Examples:
|
|
101
|
+
pi-link start worker-1
|
|
102
|
+
pi-link start worker-1 --model sonnet
|
|
103
|
+
pi-link start worker-1 --model sonnet --thinking high`);
|
|
104
|
+
process.exit(command === "start" ? 1 : 0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const name = args[1].trim().replace(/\s+/g, " ");
|
|
108
|
+
if (!name) {
|
|
109
|
+
console.error("Error: name cannot be empty.");
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const extraFlags = args.slice(2);
|
|
114
|
+
for (const flag of ["--session", "--link-name"]) {
|
|
115
|
+
if (extraFlags.includes(flag)) {
|
|
116
|
+
console.error(`Error: ${flag} is managed by pi-link start. Remove it.`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log(`Searching for session "${name}"...`);
|
|
122
|
+
const matches = await findSessionsByName(name);
|
|
123
|
+
|
|
124
|
+
const piArgs = [];
|
|
125
|
+
|
|
126
|
+
if (matches.length === 1) {
|
|
127
|
+
console.log(`Resuming session: ${matches[0].path}`);
|
|
128
|
+
piArgs.push("--session", matches[0].path);
|
|
129
|
+
} else if (matches.length > 1) {
|
|
130
|
+
console.error(`\nMultiple sessions named "${name}":\n`);
|
|
131
|
+
for (const m of matches) {
|
|
132
|
+
console.error(` ${m.modified.toISOString().slice(0, 19)} cwd: ${m.cwd}`);
|
|
133
|
+
console.error(` ${m.path}\n`);
|
|
134
|
+
}
|
|
135
|
+
console.error(`Use pi --session <path> --link-name ${name} to pick one.`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
} else {
|
|
138
|
+
console.log("No existing session found. Starting new session.");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
piArgs.push("--link-name", name, ...extraFlags);
|
|
142
|
+
|
|
143
|
+
// On Windows, resolve 'pi' through the shell so .cmd/.ps1 shims work
|
|
144
|
+
const isWin = process.platform === "win32";
|
|
145
|
+
const cmd = isWin ? "cmd" : "pi";
|
|
146
|
+
const cmdArgs = isWin ? ["/c", "pi", ...piArgs] : piArgs;
|
|
147
|
+
|
|
148
|
+
const child = spawn(cmd, cmdArgs, { stdio: "inherit" });
|
|
149
|
+
|
|
150
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
151
|
+
child.on("error", (err) => {
|
|
152
|
+
console.error(`Failed to start pi: ${err.message}`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
});
|
package/index.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Pi Link — WebSocket-based inter-terminal communication
|
|
3
3
|
*
|
|
4
4
|
* Connects multiple Pi terminals over a local WebSocket link.
|
|
5
|
-
* Opt-in via --link flag or /link-connect command.
|
|
5
|
+
* Opt-in via --link / --link-name flag or /link-connect command.
|
|
6
6
|
* First terminal to connect becomes the hub; others join as clients.
|
|
7
7
|
* Hub loss triggers automatic promotion of a surviving client.
|
|
8
8
|
*
|
|
@@ -115,6 +115,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
115
115
|
default: false,
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
+
pi.registerFlag("link-name", {
|
|
119
|
+
description: "Connect to link with this terminal name",
|
|
120
|
+
type: "string",
|
|
121
|
+
});
|
|
122
|
+
|
|
118
123
|
// ── State ────────────────────────────────────────────────────────────────
|
|
119
124
|
|
|
120
125
|
let role: "hub" | "client" | "disconnected" = "disconnected";
|
|
@@ -853,25 +858,35 @@ export default function (pi: ExtensionAPI) {
|
|
|
853
858
|
ctx = _ctx;
|
|
854
859
|
currentCwd = _ctx.cwd;
|
|
855
860
|
|
|
856
|
-
//
|
|
857
|
-
const
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
(
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
861
|
+
// Resolve terminal name: --link-name flag > saved link-name > session name > random
|
|
862
|
+
const rawLinkName = pi.getFlag("link-name");
|
|
863
|
+
const flagName =
|
|
864
|
+
typeof rawLinkName === "string"
|
|
865
|
+
? rawLinkName.trim().replace(/\s+/g, " ") || undefined
|
|
866
|
+
: undefined;
|
|
867
|
+
if (flagName) {
|
|
868
|
+
preferredName = flagName;
|
|
869
|
+
terminalName = flagName;
|
|
870
|
+
pi.appendEntry("link-name", { name: flagName });
|
|
871
|
+
if (!pi.getSessionName()) pi.setSessionName(flagName);
|
|
867
872
|
} else {
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
873
|
+
const saved = _ctx.sessionManager
|
|
874
|
+
.getEntries()
|
|
875
|
+
.filter(
|
|
876
|
+
(e: { type: string; customType?: string }) =>
|
|
877
|
+
e.type === "custom" && e.customType === "link-name",
|
|
878
|
+
)
|
|
879
|
+
.pop() as { data?: { name?: string } } | undefined;
|
|
880
|
+
if (saved?.data?.name) {
|
|
881
|
+
preferredName = saved.data.name;
|
|
882
|
+
terminalName = preferredName;
|
|
883
|
+
} else {
|
|
884
|
+
const sessionName = pi.getSessionName()?.trim().replace(/\s+/g, " ");
|
|
885
|
+
if (sessionName) terminalName = sessionName;
|
|
886
|
+
}
|
|
872
887
|
}
|
|
873
888
|
|
|
874
|
-
if (shouldConnect(_ctx)) await initialize();
|
|
889
|
+
if (flagName || shouldConnect(_ctx)) await initialize();
|
|
875
890
|
});
|
|
876
891
|
|
|
877
892
|
pi.on("session_shutdown", async () => {
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-link",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
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",
|
|
7
|
+
"bin": {
|
|
8
|
+
"pi-link": "./bin/pi-link.mjs"
|
|
9
|
+
},
|
|
7
10
|
"repository": {
|
|
8
11
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/alvivar/pi-link"
|
|
12
|
+
"url": "git+https://github.com/alvivar/pi-link.git"
|
|
10
13
|
},
|
|
11
14
|
"keywords": [
|
|
12
15
|
"pi-package",
|