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 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 the `--link` flag to auto-connect on startup:
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 $ pi --link
66
- ✓ Link hub started on :9900 as "t-a1b2" ✓ Joined link as "t-c3d4" (2 online)
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
- Already in a session without `--link`? You can connect mid-session with `/link-connect`.
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 - no status bar, no connections, no warnings.
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 | When | Auto-reconnect? |
130
- | ------------------ | ----------------------------------- | -------------------------------- |
131
- | `pi --link` | Auto-connect on startup | Yes |
132
- | `/link-connect` | Opt-in mid-session (no flag needed) | Yes |
133
- | `/link-disconnect` | Opt-out mid-session | Suppressed until `/link-connect` |
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
- `/link-connect` enables full participation in Pi Link regardless of whether `--link` was passed. Both `/link-connect` and `/link-disconnect` save their intent to the session - resume that session later and the connection state is restored without needing the flag. Explicit user intent takes precedence over `--link`.
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
- The `pi.extensions` field tells Pi which files to load as extensions. `pi.skills` registers bundled skill directories - the `pi-link-coordination` skill is loaded automatically on install.
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** up to 20 messages or ~16 000 chars per delivery (soft cap the first item is always included even if oversized).
533
- 4. **Deliver** one `pi.sendMessage({ triggerTurn: true })` call with a `[Link: N message(s) received]` block.
534
- 5. **Drain** if the inbox still has items, reschedule.
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)` deferred to the next macrotask, by which time `ctx.isIdle()` returns `true`.
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
  | ----------------- | ------ | ---------------------------------------- |
@@ -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
- // Restore preferred link name from session
857
- const saved = _ctx.sessionManager
858
- .getEntries()
859
- .filter(
860
- (e: { type: string; customType?: string }) =>
861
- e.type === "custom" && e.customType === "link-name",
862
- )
863
- .pop() as { data?: { name?: string } } | undefined;
864
- if (saved?.data?.name) {
865
- preferredName = saved.data.name;
866
- terminalName = preferredName;
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
- // No explicit link-name: fall back to session name as a better default than t-xxxx
869
- const sessionName = pi.getSessionName()?.trim().replace(/\s+/g, " ");
870
- if (sessionName) terminalName = sessionName;
871
- // NOT saved as preferredName only /link-name persists
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.8",
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",