@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 +1 -1
- package/skills/relay/SKILL.md +12 -0
- package/src/cli.js +22 -8
- package/src/store.js +17 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khanglvm/relay",
|
|
3
|
-
"version": "0.13.
|
|
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",
|
package/skills/relay/SKILL.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
628
|
+
printResult(out); // draft can hold many annotations — sidecar it too
|
|
617
629
|
return 0;
|
|
618
630
|
}
|
|
619
|
-
|
|
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 {
|