pi-link 0.5.1 → 0.5.2

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,22 @@ This changelog is based on the git history from `2026-03-21` (initial commit) th
6
6
 
7
7
  ---
8
8
 
9
+ ## 0.5.2 — 2026-09-29
10
+
11
+ ### Fixed
12
+
13
+ - **Session-directory errors no longer look like an empty session history.** If the CLI cannot enumerate the session root for a reason other than a missing directory, it reports the error and exits with code `1`. The launcher no longer starts a new session on that false absence, `--list` no longer reports no sessions, and `--resolve` no longer reports the name as not found. A missing root still means no sessions yet; handling of individual session files and subdirectories is unchanged.
14
+
15
+ - **A terminal named `__proto__` keeps its status, cwd and context snapshots.** The name-keyed records in the initial welcome and `link_list` details now use dictionaries without a prototype, so that accepted name is copied into those records and serialized as an ordinary own property rather than lost through JavaScript's inherited setter. No names are newly reserved.
16
+
17
+ - **`pi-link --version` fails explicitly when package metadata cannot supply a version.** An unreadable or malformed `package.json`, or a missing version, now produces an error and a nonzero exit instead of printing `unknown` and exiting successfully. A healthy installation still prints only its version.
18
+
19
+ ### Changed
20
+
21
+ - **Internal cleanup reuses existing status and delivery helpers.** Status formatting uses the existing identity formatter, disconnect relies on the inbox scheduler's existing timer guard, and client status updates use the shared hub broadcast path instead of duplicating it. Those updates keep their authoritative sender name, recipients and context rules; the hub now stores them through self-delivery after sending to peers, as the shared path already does. Two unreachable CLI `break` statements after `process.exit(0)` were also removed.
22
+
23
+ ---
24
+
9
25
  ## 0.5.1 — 2026-09-22
10
26
 
11
27
  ### Fixed
package/bin/pi-link.mjs CHANGED
@@ -183,14 +183,17 @@ async function loadSessionRecord(filePath, scopeCwd) {
183
183
  // Returns meta + mtime + path for every readable session in `dir`, or only
184
184
  // those from `scopeCwd` when it is given. Custom layout is flat
185
185
  // (<dir>/*.jsonl); default layout has one subdir level per encoded cwd
186
- // (<dir>/<sub>/*.jsonl). Errors on individual files/dirs are silently skipped
187
- // — active or partially-written sessions are tolerated.
186
+ // (<dir>/<sub>/*.jsonl). A missing `dir` means no sessions yet; any other
187
+ // failure to read it is fatal, since reporting no sessions would be a lie.
188
+ // Errors on individual files/dirs are silently skipped — active or
189
+ // partially-written sessions are tolerated.
188
190
  async function scanSessions(dir, isCustom, scopeCwd) {
189
191
  let entries;
190
192
  try {
191
193
  entries = await readdir(dir, { withFileTypes: true });
192
- } catch {
193
- return [];
194
+ } catch (err) {
195
+ if (err.code === "ENOENT") return [];
196
+ fail(`cannot read session directory: ${err.message}`);
194
197
  }
195
198
 
196
199
  const tasks = [];
@@ -315,14 +318,14 @@ function printHelp() {
315
318
  }
316
319
 
317
320
  function printVersion() {
321
+ let pkg;
318
322
  try {
319
- const pkg = JSON.parse(
320
- readFileSync(new URL("../package.json", import.meta.url), "utf-8"),
321
- );
322
- console.log(pkg.version ?? "unknown");
323
- } catch {
324
- console.log("unknown");
323
+ pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
324
+ } catch (err) {
325
+ fail(`cannot read pi-link's package.json: ${err.message}`);
325
326
  }
327
+ if (!pkg.version) fail("pi-link's package.json has no version");
328
+ console.log(pkg.version);
326
329
  }
327
330
 
328
331
  function describeMode(mode) {
@@ -515,11 +518,9 @@ switch (state.mode) {
515
518
  case "help":
516
519
  printHelp();
517
520
  process.exit(0);
518
- break; // unreachable; present to satisfy no-fallthrough lints
519
521
  case "version":
520
522
  printVersion();
521
523
  process.exit(0);
522
- break; // unreachable; present to satisfy no-fallthrough lints
523
524
  case "list":
524
525
  await runList(state);
525
526
  break;
package/index.ts CHANGED
@@ -421,9 +421,7 @@ export default function (pi: ExtensionAPI) {
421
421
  }
422
422
 
423
423
  function formatStatus(s: LinkStatus): string {
424
- const dur = formatDuration(s.since);
425
- if (s.kind === "tool") return `tool:${s.toolName} (${dur})`;
426
- return `${s.kind} (${dur})`;
424
+ return `${statusIdentity(s)} (${formatDuration(s.since)})`;
427
425
  }
428
426
 
429
427
  function formatTokens(n: number): string {
@@ -963,14 +961,15 @@ export default function (pi: ExtensionAPI) {
963
961
  hubClients.set(clientWs, clientName);
964
962
  const list = terminalList();
965
963
 
966
- // Confirm to the new client (with status, cwd and context snapshots)
967
- const statuses: Record<string, LinkStatus> = {};
964
+ // Confirm to the new client (with status, cwd and context snapshots).
965
+ // Keyed by terminal name, so no prototype: a plain {} would swallow "__proto__".
966
+ const statuses: Record<string, LinkStatus> = Object.create(null);
968
967
  statuses[terminalName] = deriveStatus(); // hub's own status
969
968
  for (const [name, status] of terminalStatuses) statuses[name] = status;
970
- const cwds: Record<string, string> = {};
969
+ const cwds: Record<string, string> = Object.create(null);
971
970
  if (currentCwd) cwds[terminalName] = currentCwd; // hub's own cwd
972
971
  for (const [name, cwd] of terminalCwds) cwds[name] = cwd;
973
- const contexts: Record<string, ContextSnapshot> = {};
972
+ const contexts: Record<string, ContextSnapshot> = Object.create(null);
974
973
  const hubContext = captureContext();
975
974
  if (hubContext) contexts[terminalName] = hubContext; // hub's own context
976
975
  for (const [name, c] of terminalContexts) contexts[name] = c;
@@ -1001,21 +1000,16 @@ export default function (pi: ExtensionAPI) {
1001
1000
  // Ignore messages from unregistered clients
1002
1001
  if (!clientName) return;
1003
1002
 
1004
- // Status update — store and fan out to other clients only (not back to hub)
1003
+ // Status update: rebuilt under the authoritative name, fanned out to the other
1004
+ // clients, and stored by the hub through its own self-delivery.
1005
1005
  if (msg.type === "status_update") {
1006
- terminalStatuses.set(clientName, msg.status);
1007
- if (msg.context) terminalContexts.set(clientName, msg.context);
1008
- else if (msg.context === null) terminalContexts.delete(clientName);
1009
1006
  const normalized: StatusUpdateMsg = {
1010
1007
  type: "status_update",
1011
1008
  name: clientName,
1012
1009
  status: msg.status,
1013
1010
  context: msg.context, // undefined omitted by JSON; null forwarded to clear
1014
1011
  };
1015
- const json = JSON.stringify(normalized);
1016
- for (const [otherWs, name] of hubClients) {
1017
- if (name !== clientName) otherWs.send(json);
1018
- }
1012
+ hubBroadcast(normalized, clientName);
1019
1013
  return;
1020
1014
  }
1021
1015
 
@@ -1340,7 +1334,7 @@ export default function (pi: ExtensionAPI) {
1340
1334
  updateStatus();
1341
1335
 
1342
1336
  // Inbox survives disconnect; flush unless a local /compact still gates it.
1343
- if (!flushTimer) releaseInbox();
1337
+ releaseInbox();
1344
1338
  }
1345
1339
 
1346
1340
  function cleanup() {
@@ -1765,9 +1759,10 @@ export default function (pi: ExtensionAPI) {
1765
1759
  async execute() {
1766
1760
  if (role === "disconnected") return notConnectedResult();
1767
1761
 
1768
- const statuses: Record<string, string> = {};
1769
- const cwds: Record<string, string> = {};
1770
- const contexts: Record<string, ContextSnapshot> = {};
1762
+ // Keyed by terminal name, so no prototype (see the welcome snapshots).
1763
+ const statuses: Record<string, string> = Object.create(null);
1764
+ const cwds: Record<string, string> = Object.create(null);
1765
+ const contexts: Record<string, ContextSnapshot> = Object.create(null);
1771
1766
  const visible = visibleTerminals();
1772
1767
  const list = visible
1773
1768
  .map((name) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-link",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
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",