pi-link 0.1.17 → 0.2.0

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,26 @@ This changelog is based on the git history from `2026-03-21` (initial commit) th
6
6
 
7
7
  ---
8
8
 
9
+ ## 0.2.0 — 2026-07-17
10
+
11
+ ### Added
12
+
13
+ - **`pi-link --version` prints the installed package version.** It writes the bare semver to stdout and exits successfully, so users can identify the installed build during testing.
14
+
15
+ ### Breaking
16
+
17
+ - **Removed the deprecated `pi-link list` and `pi-link resolve <name>` command forms.** They now error with migration guidance; use `pi-link --list` and `pi-link --resolve <name>` instead. The canonical flag forms are unchanged.
18
+
19
+ ### Changed
20
+
21
+ - **The wrapper no longer reserves legacy `--all` / `-a` flags.** Before a session name, they receive the normal unknown-argument error; after a session name, they pass through to Pi unchanged. Use `--global` / `-g` for pi-link's cross-directory session lookup.
22
+
23
+ ### Fixed
24
+
25
+ - **Malformed saved link names no longer crash startup.** A non-string `link-name` session entry is ignored and naming falls through to the session name or random default; well-formed name precedence is unchanged.
26
+
27
+ ---
28
+
9
29
  ## 0.1.17 — 2026-07-01
10
30
 
11
31
  ### Changed
package/README.md CHANGED
@@ -220,8 +220,6 @@ Resume: pi-link <name>
220
220
 
221
221
  For scripting, `pi-link --resolve <name>` prints just the session path (machine-readable, no other output). Exit codes: `0` on single match, `1` if ambiguous (multiple matches printed to stderr), `2` if not found.
222
222
 
223
- > The legacy subcommand forms `pi-link list` and `pi-link resolve <name>` still work but print a stderr deprecation warning and will be removed in a future release. Switch to the flag forms above.
224
-
225
223
  ---
226
224
 
227
225
  ## LLM Tools
package/bin/pi-link.mjs CHANGED
@@ -8,9 +8,7 @@
8
8
  // pi-link --list [--global|-g] List pi-link sessions in current cwd (or everywhere).
9
9
  // pi-link --resolve <name> [--global|-g]
10
10
  // Print just the session path (machine-readable).
11
- //
12
- // Deprecated subcommand forms `pi-link list` / `pi-link resolve <name>` still
13
- // work for one release with a stderr warning.
11
+ // pi-link --version Print the installed pi-link version.
14
12
 
15
13
  import { readdir, stat } from "fs/promises";
16
14
  import { createReadStream, existsSync, readFileSync } from "fs";
@@ -19,6 +17,12 @@ import { join } from "path";
19
17
  import { homedir } from "os";
20
18
  import { spawn } from "child_process";
21
19
 
20
+ // Canonicalize a link/session name: trim + collapse internal whitespace.
21
+ // Must match the extension's normalizeName (index.ts).
22
+ function normalizeName(s) {
23
+ return s.trim().replace(/\s+/g, " ");
24
+ }
25
+
22
26
  // ── Pi config resolution ───────────────────────────────────────────────────
23
27
  // Match Pi's session-dir lookup order so list/resolve/<name> see what Pi sees.
24
28
  // Custom sessionDir → flat layout; default → <agentDir>/sessions/<encoded-cwd>.
@@ -88,11 +92,11 @@ async function getSessionMeta(filePath) {
88
92
  if (typeof entry.cwd === "string") cwd = entry.cwd;
89
93
  if (typeof entry.id === "string") id = entry.id;
90
94
  } else if (entry.type === "session_info" && typeof entry.name === "string") {
91
- sessionName = entry.name.trim().replace(/\s+/g, " ") || undefined;
95
+ sessionName = normalizeName(entry.name) || undefined;
92
96
  } else if (entry.type === "custom" && entry.customType === "link-name") {
93
97
  hasLinkName = true;
94
98
  if (entry.data && typeof entry.data.name === "string") {
95
- const n = entry.data.name.trim().replace(/\s+/g, " ");
99
+ const n = normalizeName(entry.data.name);
96
100
  if (n) linkName = n;
97
101
  }
98
102
  } else if (entry.type === "message" || entry.type === "user" || entry.type === "assistant") {
@@ -237,20 +241,9 @@ function renderTable(rows, columns) {
237
241
 
238
242
  const rawArgs = process.argv.slice(2);
239
243
 
240
- // Reject pi-link flags renamed in 0.1.12 with a clear pointer to the new name.
241
- // Same intent as `rejectManagedFlag` (specific message > generic "Unknown argument")
242
- // but for our own renames, not Pi-managed flags.
243
- function rejectRenamedFlag(token) {
244
- if (token === "--all" || token === "-a") {
245
- const replacement = token === "-a" ? "-g" : "--global";
246
- console.error(`Error: ${token} was renamed to ${replacement}.`);
247
- process.exit(1);
248
- }
249
- }
250
-
251
244
  // Reject Pi flags that pi-link manages, plus --link-name (which exists at the
252
245
  // `pi` level for link-only naming, but the wrapper's combined-mode contract
253
- // conflicts with it). Called from Phase 6 (mode entry) and Phase 7 (after
246
+ // conflicts with it). Called from Phase 4 (mode entry) and Phase 5 (after
254
247
  // launcher name), so it fires on both `pi-link --session foo` and
255
248
  // `pi-link foo --session bar` with the friendly message.
256
249
  function rejectManagedFlag(token) {
@@ -288,22 +281,27 @@ function printHelp() {
288
281
  console.error("Usage: pi-link <name> [--global|-g] [pi flags...]");
289
282
  console.error(" pi-link --list [--global|-g]");
290
283
  console.error(" pi-link --resolve <name> [--global|-g]");
284
+ console.error(" pi-link --version");
291
285
  console.error("");
292
286
  console.error("By default, name lookup is scoped to the current cwd.");
293
287
  console.error("--global / -g widens the search to sessions in any cwd.");
294
288
  }
295
289
 
296
- function printDeprecationWarning(form) {
297
- const canonical = form === "list" ? "--list" : "--resolve";
298
- console.error(
299
- `Warning: 'pi-link ${form}' is deprecated. Use 'pi-link ${canonical}' instead. ` +
300
- `(Subcommand form will be removed in a future release.)`,
301
- );
290
+ function printVersion() {
291
+ try {
292
+ const pkg = JSON.parse(
293
+ readFileSync(new URL("../package.json", import.meta.url), "utf-8"),
294
+ );
295
+ console.log(pkg.version ?? "unknown");
296
+ } catch {
297
+ console.log("unknown");
298
+ }
302
299
  }
303
300
 
304
301
  function describeMode(mode) {
305
302
  switch (mode) {
306
303
  case "help": return "--help";
304
+ case "version": return "--version";
307
305
  case "list": return "--list";
308
306
  case "resolve": return "--resolve";
309
307
  case "launcher": return "session name";
@@ -313,23 +311,19 @@ function describeMode(mode) {
313
311
 
314
312
  // ── Parser ─────────────────────────────────────────────────────────────────
315
313
  //
316
- // Single sequential pass populates `state`; dispatcher reads it. Phases (per
317
- // PLAN-cli-flags.md):
318
- // 1. Renamed-flag rejection (always-on for --all/-a)
319
- // 2. Global flags (--global, --help, --)
320
- // 3. Mode-selecting flags (--list, --resolve, --resolve=<name>)
321
- // 4. Deprecated subcommands (list, resolve <name>) — only at mode-null position
322
- // 5. Mode-specific extra-token rejection (with deprecated-resolve leniency)
323
- // 6. Launcher mode entry (mode null + bare positional)
324
- // 7. Launcher passthrough (mode launcher) with orphan-positional rejection
314
+ // Single sequential pass populates `state`; dispatcher reads it. Phases:
315
+ // 1. Global flags (--global, --help, --version, --)
316
+ // 2. Mode-selecting flags (--list, --resolve, --resolve=<name>)
317
+ // 3. Mode-specific extra-token rejection
318
+ // 4. Launcher mode entry (mode null + bare positional)
319
+ // 5. Launcher passthrough (mode launcher) with orphan-positional rejection
325
320
 
326
321
  const state = {
327
- mode: null, // null | "help" | "list" | "resolve" | "launcher"
322
+ mode: null, // null | "help" | "version" | "list" | "resolve" | "launcher"
328
323
  resolveName: null,
329
324
  launcherName: null,
330
325
  global: false,
331
326
  piPassthrough: [],
332
- deprecated: null, // null | "list" | "resolve"
333
327
  };
334
328
 
335
329
  function setMode(mode) {
@@ -344,10 +338,7 @@ let lastWasFlag = false;
344
338
  for (let i = 0; i < rawArgs.length; i++) {
345
339
  const a = rawArgs[i];
346
340
 
347
- // Phase 1: renamed-flag rejection.
348
- rejectRenamedFlag(a);
349
-
350
- // Phase 2: global flags / scope-affecting tokens.
341
+ // Phase 1: global flags / scope-affecting tokens.
351
342
  if (a === "--global" || a === "-g") {
352
343
  state.global = true;
353
344
  lastWasFlag = false;
@@ -357,6 +348,10 @@ for (let i = 0; i < rawArgs.length; i++) {
357
348
  setMode("help"); // errors if combined with another mode
358
349
  continue;
359
350
  }
351
+ if (a === "--version") {
352
+ setMode("version"); // errors if combined with another mode
353
+ continue;
354
+ }
360
355
  if (a === "--") {
361
356
  // `--` only meaningful in launcher mode (separates pi flags from positionals).
362
357
  if (state.mode !== "launcher") {
@@ -369,7 +364,7 @@ for (let i = 0; i < rawArgs.length; i++) {
369
364
  break;
370
365
  }
371
366
 
372
- // Phase 3: mode-selecting flags.
367
+ // Phase 2: mode-selecting flags.
373
368
  if (a === "--list") {
374
369
  setMode("list");
375
370
  continue;
@@ -392,57 +387,36 @@ for (let i = 0; i < rawArgs.length; i++) {
392
387
  continue;
393
388
  }
394
389
 
395
- // Phase 4: deprecated subcommands (only at mode-null position).
396
- if (state.mode === null && (a === "list" || a === "resolve")) {
397
- state.deprecated = a;
398
- if (a === "list") {
399
- setMode("list");
400
- } else {
401
- setMode("resolve");
402
- const next = rawArgs[i + 1];
403
- if (next !== undefined && !next.startsWith("-")) {
404
- state.resolveName = next;
405
- i++;
406
- }
407
- // else: leave null; Phase 5 leniency or post-parse validation handles it.
408
- }
409
- continue;
410
- }
411
-
412
- // Phase 5: mode-specific extra-token rejection.
390
+ // Phase 3: mode-specific extra-token rejection.
413
391
  if (state.mode === "help") {
414
392
  fail(`--help does not accept arguments: ${a}`);
415
393
  }
394
+ if (state.mode === "version") {
395
+ fail(`--version does not accept arguments: ${a}`);
396
+ }
416
397
  if (state.mode === "list") {
417
398
  fail(`--list does not accept argument: ${a}\n Usage: pi-link --list [--global|-g]`);
418
399
  }
419
400
  if (state.mode === "resolve") {
420
- // Deprecated-form leniency: `pi-link resolve --global foo` was order-independent;
421
- // if we entered via deprecated path and haven't bound a name yet, take this.
422
- if (
423
- state.deprecated === "resolve" &&
424
- state.resolveName === null &&
425
- !a.startsWith("-")
426
- ) {
427
- state.resolveName = a;
428
- continue;
429
- }
430
401
  fail(`--resolve accepts exactly one name; got extra: ${a}`);
431
402
  }
432
403
 
433
- // Phase 6: launcher mode entry. state.mode === null here, no name set yet.
434
- // (lastWasFlag is still false here — only Phase 7 sets it, and Phase 7 requires launcher mode.)
404
+ // Phase 4: launcher mode entry. state.mode === null here, no name set yet.
405
+ // (lastWasFlag is still false here — only Phase 5 sets it, and Phase 5 requires launcher mode.)
435
406
  if (state.mode === null) {
436
407
  rejectManagedFlag(a);
437
408
  if (a.startsWith("-")) {
438
409
  fail(`Unknown argument: ${a}\n Usage: pi-link <name> [--global|-g] [pi flags...]`);
439
410
  }
411
+ if (a === "list" || a === "resolve") {
412
+ fail(`'pi-link ${a}' was removed. Use 'pi-link --${a}'.`);
413
+ }
440
414
  state.mode = "launcher";
441
415
  state.launcherName = a;
442
416
  continue;
443
417
  }
444
418
 
445
- // Phase 7: launcher mode, name set. Tokens go to passthrough or get rejected.
419
+ // Phase 5: launcher mode, name set. Tokens go to passthrough or get rejected.
446
420
  rejectManagedFlag(a);
447
421
  if (a.startsWith("-")) {
448
422
  state.piPassthrough.push(a);
@@ -466,24 +440,20 @@ if (state.mode === "resolve") {
466
440
  if (state.resolveName === null) {
467
441
  fail(`--resolve requires a name argument.\n Usage: pi-link --resolve <name> [--global|-g]`);
468
442
  }
469
- const normalized = state.resolveName.trim().replace(/\s+/g, " ");
443
+ const normalized = normalizeName(state.resolveName);
470
444
  if (!normalized) {
471
445
  fail(`--resolve requires a non-empty name argument.\n Usage: pi-link --resolve <name> [--global|-g]`);
472
446
  }
473
447
  state.resolveName = normalized;
474
448
  }
475
449
  if (state.mode === "launcher") {
476
- const normalized = state.launcherName.trim().replace(/\s+/g, " ");
450
+ const normalized = normalizeName(state.launcherName);
477
451
  if (!normalized) {
478
452
  fail(`session name cannot be empty.\n Usage: pi-link <name> [--global|-g] [pi flags...]`);
479
453
  }
480
454
  state.launcherName = normalized;
481
455
  }
482
456
 
483
- if (state.deprecated) {
484
- printDeprecationWarning(state.deprecated);
485
- }
486
-
487
457
  // ── Dispatch ───────────────────────────────────────────────────────────────
488
458
 
489
459
  switch (state.mode) {
@@ -492,6 +462,10 @@ switch (state.mode) {
492
462
  printHelp();
493
463
  process.exit(0);
494
464
  break; // unreachable; present to satisfy no-fallthrough lints
465
+ case "version":
466
+ printVersion();
467
+ process.exit(0);
468
+ break; // unreachable; present to satisfy no-fallthrough lints
495
469
  case "list":
496
470
  await runList(state);
497
471
  break;
@@ -517,18 +491,18 @@ async function runList(state) {
517
491
  }
518
492
  const columns = state.global
519
493
  ? [
520
- { header: "NAME", get: (s) => s.name },
521
- { header: "CWD", get: (s) => displayPath(s.cwd) },
522
- { header: "MODIFIED", get: (s) => relTime(s.modified), dim: true },
523
- { header: "MESSAGES", get: (s) => s.messages, dim: true },
524
- { header: "ID", get: (s) => s.id, dim: true },
525
- ]
494
+ { header: "NAME", get: (s) => s.name },
495
+ { header: "CWD", get: (s) => displayPath(s.cwd) },
496
+ { header: "MODIFIED", get: (s) => relTime(s.modified), dim: true },
497
+ { header: "MESSAGES", get: (s) => s.messages, dim: true },
498
+ { header: "ID", get: (s) => s.id, dim: true },
499
+ ]
526
500
  : [
527
- { header: "NAME", get: (s) => s.name },
528
- { header: "MODIFIED", get: (s) => relTime(s.modified), dim: true },
529
- { header: "MESSAGES", get: (s) => s.messages, dim: true },
530
- { header: "ID", get: (s) => s.id, dim: true },
531
- ];
501
+ { header: "NAME", get: (s) => s.name },
502
+ { header: "MODIFIED", get: (s) => relTime(s.modified), dim: true },
503
+ { header: "MESSAGES", get: (s) => s.messages, dim: true },
504
+ { header: "ID", get: (s) => s.id, dim: true },
505
+ ];
532
506
  console.log(renderTable(sessions, columns));
533
507
  if (process.stdout.isTTY) {
534
508
  console.log("");
package/index.ts CHANGED
@@ -1228,9 +1228,11 @@ export default function (pi: ExtensionAPI) {
1228
1228
  if (fromEnv && !pi.getSessionName()) pi.setSessionName(flagName);
1229
1229
  } else {
1230
1230
  const saved = latestCustomData("link-name") as
1231
- | { name?: string }
1231
+ | { name?: unknown }
1232
1232
  | undefined;
1233
- const savedName = normalizeName(saved?.name);
1233
+ const savedName = normalizeName(
1234
+ typeof saved?.name === "string" ? saved.name : undefined,
1235
+ );
1234
1236
  if (savedName) {
1235
1237
  preferredName = savedName;
1236
1238
  terminalName = preferredName;
@@ -1512,7 +1514,7 @@ export default function (pi: ExtensionAPI) {
1512
1514
  instructions: params.instructions,
1513
1515
  });
1514
1516
 
1515
- if (!delivered && pendingCompactResponses.has(requestId)) {
1517
+ if (!delivered) {
1516
1518
  const pending = cleanupPendingCompact(requestId);
1517
1519
  if (pending) {
1518
1520
  pending.resolve(
@@ -1621,7 +1623,7 @@ export default function (pi: ExtensionAPI) {
1621
1623
  prompt: params.prompt,
1622
1624
  });
1623
1625
 
1624
- if (!delivered && pendingPromptResponses.has(requestId)) {
1626
+ if (!delivered) {
1625
1627
  const pending = cleanupPending(requestId);
1626
1628
  if (pending) {
1627
1629
  pending.resolve(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-link",
3
- "version": "0.1.17",
3
+ "version": "0.2.0",
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",