@khanglvm/relay 0.13.1 → 0.13.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khanglvm/relay",
3
- "version": "0.13.1",
3
+ "version": "0.13.2",
4
4
  "description": "Question boards with rich blocks (markdown, charts, mermaid, tables, code, diffs, video, sandboxed HTML), clickable local file-links, and element-level annotations for AI coding agents (Claude Code, Codex, …): ask users structured questions, present interactive visuals, collect inline comments, read answers as JSON — in a local browser board OR rendered INLINE inside the Claude & Codex apps as an MCP App (SEP-1865).",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -417,6 +417,18 @@ the matching `answers` value (e.g. `answers.approach = "a"` but
417
417
  `notes.approach = "actually B"` → the user means B). Reconcile them before
418
418
  generating output. The same channels appear under `draft` on timeout/cancel.
419
419
 
420
+ **Don't let your shell truncate the result.** A board with several annotations
421
+ prints a large JSON blob, and most agent shell tools cap stdout — so you silently
422
+ get only the first few annotations and miss the rest. Two rules:
423
+
424
+ - **Never pipe `rly wait`/`rly result` through `head`/`tail`/`sed`** (or any
425
+ output cap). That's exactly how annotations get dropped.
426
+ - The full result is **always written to a file**, surfaced as the FIRST field
427
+ of the output: `"resultFile": "~/.relay/boards/<id>.result.json"`. If the
428
+ output looks cut off (or to be safe on any board with annotations), **read
429
+ that file with your file tool** instead of trusting stdout — it's the complete,
430
+ untruncated payload.
431
+
420
432
  ### Reply to annotations (agent → user conversation)
421
433
 
422
434
  ```sh
package/src/cli.js CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  loadRunning,
16
16
  removeRunning,
17
17
  isAlive,
18
+ saveResultFile,
18
19
  HOME,
19
20
  } from './store.js';
20
21
  import { runBoard } from './server.js';
@@ -76,6 +77,17 @@ function printJson(obj) {
76
77
  process.stdout.write(JSON.stringify(obj, null, 2) + '\n');
77
78
  }
78
79
 
80
+ // Print a terminal result. A board with many annotations produces a large JSON
81
+ // payload, and an agent's shell harness commonly truncates long stdout — so the
82
+ // agent silently misses annotations past the cut. We write the FULL result to a
83
+ // sidecar file and surface its path FIRST (`resultFile`), so even a truncated
84
+ // stdout shows where the complete payload is; the agent reads that file with its
85
+ // file tool. Falls back to a plain print if the sidecar can't be written.
86
+ function printResult(result) {
87
+ const resultFile = result && result.boardId ? saveResultFile(result.boardId, result) : null;
88
+ printJson(resultFile ? { resultFile, ...result } : result);
89
+ }
90
+
79
91
  // Push-wake for `rly wait --notify-cmd`: run the agent's local shell command
80
92
  // once a TERMINAL result lands. Result JSON goes to the command's stdin;
81
93
  // RLY_BOARD_ID / RLY_STATUS / RLY_URL are exported. Same shape as the server's
@@ -225,7 +237,7 @@ async function runOrDetach(record, args) {
225
237
 
226
238
  const { done } = await runBoard({ id: record.id, port, open, timeoutSec });
227
239
  const result = await done;
228
- printJson(result);
240
+ printResult(result);
229
241
  return exitCodeFor(result.status);
230
242
  }
231
243
 
@@ -490,7 +502,7 @@ async function cmdWait(args) {
490
502
  // Push-wake: run the agent's --notify-cmd after a TERMINAL result, then print.
491
503
  const finishResult = (result) => {
492
504
  if (notifyCmd) runNotifyCmd(notifyCmd, result);
493
- printJson(result);
505
+ printResult(result);
494
506
  return exitCodeFor(result.status);
495
507
  };
496
508
 
@@ -506,7 +518,7 @@ async function cmdWait(args) {
506
518
  if (again?.result?.finishedAt) {
507
519
  return finishResult(again.result);
508
520
  }
509
- printJson({
521
+ printResult({
510
522
  status: 'lost',
511
523
  boardId: id,
512
524
  draft: again?.draft ?? null,
@@ -552,7 +564,7 @@ async function cmdWaitLoop(id, deadline, opts) {
552
564
  const { whileActive, idleGrace, notifyCmd } = opts;
553
565
  const finishResult = (result) => {
554
566
  if (notifyCmd) runNotifyCmd(notifyCmd, result);
555
- printJson(result);
567
+ printResult(result);
556
568
  return exitCodeFor(result.status);
557
569
  };
558
570
  while (Date.now() < deadline) {
@@ -567,7 +579,7 @@ async function cmdWaitLoop(id, deadline, opts) {
567
579
  if (again?.result?.finishedAt) {
568
580
  return finishResult(again.result);
569
581
  }
570
- printJson({
582
+ printResult({
571
583
  status: 'lost',
572
584
  boardId: id,
573
585
  draft: again?.draft ?? null,
@@ -603,7 +615,7 @@ async function cmdWaitLoop(id, deadline, opts) {
603
615
  async function cmdResult(args) {
604
616
  const record = mustLoad(args._[0]);
605
617
  if (record.result && record.result.finishedAt) {
606
- printJson(record.result);
618
+ printResult(record.result);
607
619
  return exitCodeFor(record.result.status);
608
620
  }
609
621
  const running = loadRunning(record.id);
@@ -613,10 +625,10 @@ async function cmdResult(args) {
613
625
  const out = { status: 'open', boardId: record.id, url: running.url, draft: record.draft ?? null };
614
626
  const presence = await fetchPresence(running.url);
615
627
  if (presence) out.presence = presence;
616
- printJson(out);
628
+ printResult(out); // draft can hold many annotations — sidecar it too
617
629
  return 0;
618
630
  }
619
- printJson({ status: 'lost', boardId: record.id, draft: record.draft ?? null });
631
+ printResult({ status: 'lost', boardId: record.id, draft: record.draft ?? null });
620
632
  return 5;
621
633
  }
622
634
 
@@ -1380,6 +1392,8 @@ USAGE
1380
1392
  --while-active [--idle-grace 180]: keep waiting past the deadline
1381
1393
  while the user is still viewing/focused & recently active
1382
1394
  --notify-cmd "<cmd>": run <cmd> on a terminal result (JSON on stdin)
1395
+ NB: also writes the FULL result to "resultFile" (first field) —
1396
+ read that file if your shell truncates stdout; never pipe to head/tail
1383
1397
  rly result <id> result/status now (includes live autosaved draft + presence while open)
1384
1398
  rly list [--json] running boards
1385
1399
  rly open [id] re-open the browser tab of a running board
package/src/store.js CHANGED
@@ -53,9 +53,25 @@ export function loadBoard(id) {
53
53
  }
54
54
  }
55
55
 
56
+ // Write the full result to its own file (just the result, not the whole board
57
+ // record with its bulky spec). An agent's shell may truncate `rly wait`/`result`
58
+ // stdout for a board with many annotations — this file is the complete payload
59
+ // it can read with a file tool instead. Returns the path (null on failure).
60
+ export function saveResultFile(id, result) {
61
+ try {
62
+ ensureDirs();
63
+ const p = path.join(BOARDS_DIR, `${id}.result.json`);
64
+ fs.writeFileSync(p, JSON.stringify(result, null, 2));
65
+ return p;
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+
56
71
  export function deleteBoard(id) {
57
72
  try {
58
73
  fs.unlinkSync(boardPath(id));
74
+ try { fs.unlinkSync(path.join(BOARDS_DIR, `${id}.result.json`)); } catch { /* no sidecar */ }
59
75
  return true;
60
76
  } catch {
61
77
  return false;
@@ -66,7 +82,7 @@ export function listBoards(limit = 20) {
66
82
  ensureDirs();
67
83
  const records = [];
68
84
  for (const f of fs.readdirSync(BOARDS_DIR)) {
69
- if (!f.endsWith('.json')) continue;
85
+ if (!f.endsWith('.json') || f.endsWith('.result.json')) continue; // skip result sidecars
70
86
  try {
71
87
  records.push(JSON.parse(fs.readFileSync(path.join(BOARDS_DIR, f), 'utf8')));
72
88
  } catch {