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 +20 -0
- package/README.md +0 -2
- package/bin/pi-link.mjs +61 -87
- package/index.ts +6 -4
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
434
|
-
// (lastWasFlag is still false here — only Phase
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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?:
|
|
1231
|
+
| { name?: unknown }
|
|
1232
1232
|
| undefined;
|
|
1233
|
-
const savedName = normalizeName(
|
|
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
|
|
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
|
|
1626
|
+
if (!delivered) {
|
|
1625
1627
|
const pending = cleanupPending(requestId);
|
|
1626
1628
|
if (pending) {
|
|
1627
1629
|
pending.resolve(
|
package/package.json
CHANGED