pi-link 0.1.16 → 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 +39 -0
- package/README.md +1 -3
- package/bin/pi-link.mjs +61 -87
- package/index.ts +33 -8
- package/package.json +1 -1
- package/skills/pi-link-coordination/SKILL.md +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,45 @@ 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
|
+
|
|
29
|
+
## 0.1.17 — 2026-07-01
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- **`link_send` now rejects self-targets.** Sending to your own link name returns an immediate `self_target` error instead of delivering the message back to yourself, matching the existing `link_prompt` and `link_compact` behavior.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- **Disconnected link tool calls are now marked as errors.** `notConnectedResult()` tags `error: "not_connected"`, so callers and renderers treat a disconnected call as a failure instead of a success-looking result.
|
|
38
|
+
- **Hub ignores duplicate `register` messages.** A second `register` on an already-registered client socket is now dropped instead of re-running the join flow (which could reassign the name and emit a spurious re-join).
|
|
39
|
+
- **`link_prompt` and `link_compact` honor an already-aborted signal.** Both tools now check `signal.aborted` at the top of `execute` and return an `error: "aborted"` result immediately, rather than starting work that was cancelled before it began.
|
|
40
|
+
|
|
41
|
+
### Internal
|
|
42
|
+
|
|
43
|
+
- Removed the unused `ExtensionContext` parameter from `shouldConnect()`.
|
|
44
|
+
- Clarified the keepalive comment: it presumes `sendUserMessage()` starts a run, with the sender's 30-minute hard ceiling as the backstop if it ever doesn't.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
9
48
|
## 0.1.16 — 2026-06-09
|
|
10
49
|
|
|
11
50
|
### Added
|
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
|
|
@@ -255,7 +253,7 @@ Note: `triggerTurn` does **not** cause the response to come back to the caller -
|
|
|
255
253
|
|
|
256
254
|
> **Broadcast note:** Sending to `"*"` delivers to **all other terminals** - the sender is excluded.
|
|
257
255
|
|
|
258
|
-
Pre-validates the target name against the local terminal list before sending, catching typos early. See [Message Routing](#message-routing--error-handling) for delivery semantics.
|
|
256
|
+
Pre-validates the target name against the local terminal list before sending, catching typos early. See [Message Routing](#message-routing--error-handling) for delivery semantics. **Self-target rejection** - sending to yourself (`to` equals your own name) returns an immediate error.
|
|
259
257
|
|
|
260
258
|
### `link_prompt`
|
|
261
259
|
|
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
|
@@ -433,7 +433,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
433
433
|
|
|
434
434
|
// ── Connection intent ──────────────────────────────────────────────────
|
|
435
435
|
|
|
436
|
-
function shouldConnect(
|
|
436
|
+
function shouldConnect(): boolean {
|
|
437
437
|
const data = latestCustomData("link-active") as
|
|
438
438
|
| { active?: boolean }
|
|
439
439
|
| undefined;
|
|
@@ -775,7 +775,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
775
775
|
});
|
|
776
776
|
} else {
|
|
777
777
|
pendingRemotePrompt = { id: msg.id, from: msg.from };
|
|
778
|
-
// Keepalive: periodic status push so sender knows we're alive
|
|
778
|
+
// Keepalive: periodic status push so sender knows we're alive.
|
|
779
|
+
// Keepalive presumes sendUserMessage() starts a run (platform contract);
|
|
780
|
+
// if it ever doesn't, the sender's 30 min hard ceiling is the backstop.
|
|
779
781
|
if (keepaliveTimer) clearInterval(keepaliveTimer);
|
|
780
782
|
keepaliveTimer = setInterval(
|
|
781
783
|
() => pushStatus(true),
|
|
@@ -848,6 +850,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
848
850
|
|
|
849
851
|
// First message must be register
|
|
850
852
|
if (msg.type === "register") {
|
|
853
|
+
if (clientName) return; // already registered — ignore duplicate
|
|
851
854
|
clientName = uniqueName(msg.name);
|
|
852
855
|
hubClients.set(clientWs, clientName);
|
|
853
856
|
if (msg.cwd) hubTerminalCwds.set(clientName, msg.cwd);
|
|
@@ -1225,9 +1228,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
1225
1228
|
if (fromEnv && !pi.getSessionName()) pi.setSessionName(flagName);
|
|
1226
1229
|
} else {
|
|
1227
1230
|
const saved = latestCustomData("link-name") as
|
|
1228
|
-
| { name?:
|
|
1231
|
+
| { name?: unknown }
|
|
1229
1232
|
| undefined;
|
|
1230
|
-
const savedName = normalizeName(
|
|
1233
|
+
const savedName = normalizeName(
|
|
1234
|
+
typeof saved?.name === "string" ? saved.name : undefined,
|
|
1235
|
+
);
|
|
1231
1236
|
if (savedName) {
|
|
1232
1237
|
preferredName = savedName;
|
|
1233
1238
|
terminalName = preferredName;
|
|
@@ -1237,7 +1242,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1237
1242
|
}
|
|
1238
1243
|
}
|
|
1239
1244
|
|
|
1240
|
-
if (flagName || shouldConnect(
|
|
1245
|
+
if (flagName || shouldConnect()) scheduleStartupConnect();
|
|
1241
1246
|
});
|
|
1242
1247
|
|
|
1243
1248
|
pi.on("session_shutdown", async () => {
|
|
@@ -1317,7 +1322,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1317
1322
|
}
|
|
1318
1323
|
|
|
1319
1324
|
function notConnectedResult() {
|
|
1320
|
-
return textResult("Not connected to link");
|
|
1325
|
+
return textResult("Not connected to link", { error: "not_connected" });
|
|
1321
1326
|
}
|
|
1322
1327
|
|
|
1323
1328
|
function truncatePreview(text: string, max = 60) {
|
|
@@ -1377,6 +1382,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
1377
1382
|
|
|
1378
1383
|
// Pre-validate target exists locally (best-effort, catches typos and definitely-absent names)
|
|
1379
1384
|
if (params.to !== "*") {
|
|
1385
|
+
if (params.to === terminalName) {
|
|
1386
|
+
return textResult("Cannot send to yourself", {
|
|
1387
|
+
to: params.to,
|
|
1388
|
+
error: "self_target",
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1380
1391
|
const miss = targetNotFound(params.to);
|
|
1381
1392
|
if (miss) return miss;
|
|
1382
1393
|
}
|
|
@@ -1439,6 +1450,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1439
1450
|
}),
|
|
1440
1451
|
|
|
1441
1452
|
async execute(_toolCallId, params, signal) {
|
|
1453
|
+
if (signal?.aborted) {
|
|
1454
|
+
return textResult("Compact request aborted", {
|
|
1455
|
+
to: params.to,
|
|
1456
|
+
error: "aborted",
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1442
1460
|
if (role === "disconnected") return notConnectedResult();
|
|
1443
1461
|
|
|
1444
1462
|
if (params.to === terminalName) {
|
|
@@ -1496,7 +1514,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1496
1514
|
instructions: params.instructions,
|
|
1497
1515
|
});
|
|
1498
1516
|
|
|
1499
|
-
if (!delivered
|
|
1517
|
+
if (!delivered) {
|
|
1500
1518
|
const pending = cleanupPendingCompact(requestId);
|
|
1501
1519
|
if (pending) {
|
|
1502
1520
|
pending.resolve(
|
|
@@ -1537,6 +1555,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1537
1555
|
}),
|
|
1538
1556
|
|
|
1539
1557
|
async execute(_toolCallId, params, signal) {
|
|
1558
|
+
if (signal?.aborted) {
|
|
1559
|
+
return textResult("Prompt request aborted", {
|
|
1560
|
+
to: params.to,
|
|
1561
|
+
error: "aborted",
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1540
1565
|
if (role === "disconnected") return notConnectedResult();
|
|
1541
1566
|
|
|
1542
1567
|
if (params.to === terminalName) {
|
|
@@ -1598,7 +1623,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1598
1623
|
prompt: params.prompt,
|
|
1599
1624
|
});
|
|
1600
1625
|
|
|
1601
|
-
if (!delivered
|
|
1626
|
+
if (!delivered) {
|
|
1602
1627
|
const pending = cleanupPending(requestId);
|
|
1603
1628
|
if (pending) {
|
|
1604
1629
|
pending.resolve(
|
package/package.json
CHANGED
|
@@ -48,7 +48,7 @@ Synchronous RPC. Send a prompt, wait for the response.
|
|
|
48
48
|
|
|
49
49
|
### `link_send`
|
|
50
50
|
|
|
51
|
-
Fire-and-forget. Send to one terminal or `to: "*"` to broadcast to every other connected terminal; there is no exclusion filter.
|
|
51
|
+
Fire-and-forget. Send to one terminal or `to: "*"` to broadcast to every other connected terminal; there is no exclusion filter. Sending to yourself is rejected. A send to a name that isn't connected fails with an error listing who _is_ connected — delivery failure is visible to the sender, not silent.
|
|
52
52
|
|
|
53
53
|
Set `triggerTurn: true` to queue async work on the receiver. The sender does **not** get an automatic response back.
|
|
54
54
|
|
|
@@ -69,7 +69,7 @@ Use `triggerTurn: false` for fire-and-forget status notifications only — when
|
|
|
69
69
|
|
|
70
70
|
### `link_compact`
|
|
71
71
|
|
|
72
|
-
Blocks until the target finishes compacting, then returns. Ask another terminal to compact its context window into a summary, freeing space. `link_list` exposes context usage; `link_compact` is the lever when you decide a terminal should free context — and because the call only returns once compaction is done, you can immediately hand it more work with `link_send`/`link_prompt` (no sleep, no busy-bounce). Busy targets (mid-turn or already compacting) decline; retry when `link_list` shows them idle. You decide the threshold; pi-link just runs the compaction you ask for. Its optional `instructions` add an extra focus to that summary.
|
|
72
|
+
Blocks until the target finishes compacting, then returns (3 min ceiling — the call then errors for you, though the target may still finish compacting on its own; check it before assuming failure). Ask another terminal to compact its context window into a summary, freeing space. `link_list` exposes context usage; `link_compact` is the lever when you decide a terminal should free context — and because the call only returns once compaction is done, you can immediately hand it more work with `link_send`/`link_prompt` (no sleep, no busy-bounce). Busy targets (mid-turn or already compacting) decline; retry when `link_list` shows them idle. You decide the threshold; pi-link just runs the compaction you ask for. Its optional `instructions` add an extra focus to that summary.
|
|
73
73
|
|
|
74
74
|
---
|
|
75
75
|
|