@khanglvm/relay 0.14.2 → 0.16.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/README.md +18 -3
- package/docs/AGENT.md +66 -23
- package/package.json +1 -1
- package/skills/relay/SKILL.md +48 -12
- package/src/cli.js +42 -21
- package/src/mcp-ui/board.js +46 -8
- package/src/mcp.js +9 -3
- package/src/server.js +310 -95
- package/src/spec.js +5 -0
- package/src/store.js +15 -0
- package/src/ui/annotate.js +73 -26
- package/src/ui/app.js +100 -36
- package/src/ui/blocks.css +63 -11
- package/src/ui/blocks.js +353 -48
package/README.md
CHANGED
|
@@ -78,7 +78,7 @@ unchanged; pick whichever surface fits.
|
|
|
78
78
|
| "Here's the diff — paste it in your editor" | Side-by-side **diff** blocks, syntax-highlighted **code**, **video** walkthroughs, and **file paths you click to open** in the default app |
|
|
79
79
|
| "Which commits should I pick? Resolve this conflict manually." | `rly git pick` / `rly git cherry-pick` boards and color-coded `git-conflict` resolvers with ours/theirs/custom hunk choices |
|
|
80
80
|
| "Type *done* when finished reviewing" | A Submit button; answers, notes, and inline comments returned as JSON |
|
|
81
|
-
| Feedback = another wall of text | Click any chart point, diagram node, table cell, or sentence to
|
|
81
|
+
| Feedback = another wall of text | Click any chart point, diagram node, table cell, or sentence; hold+drag an image area to attach a local crop — the agent replies and the thread grows on the board |
|
|
82
82
|
|
|
83
83
|
Everything autosaves in real time, detached board links keep serving after
|
|
84
84
|
agent timeouts, multiple boards run at once, and the package has **zero npm
|
|
@@ -93,8 +93,9 @@ actually needs them.
|
|
|
93
93
|
| `rly help` | every command at a glance |
|
|
94
94
|
| `rly git pick` / `rly git cherry-pick` | choose commit actions and rank the order directly on a board; add `--code` to cherry-pick with split code review and per-hunk Apply/Skip/Hold |
|
|
95
95
|
| `rly git conflict [files…]` | auto-detect unmerged conflict files, or open specific local paths, and return resolved content in `result.blockEdits` |
|
|
96
|
-
| `rly share <board-id>` | activate/list/revoke same-Wi-Fi reviewer or
|
|
96
|
+
| `rly share <board-id>` | activate/list/revoke same-Wi-Fi collaborator, reference-only reviewer, or read-only links for a running browser board |
|
|
97
97
|
| `rly view <file.md> …` | open a quick read-only board that renders local markdown file(s), data files, or PDFs — library-free; great for plans, READMEs, reports, quotes |
|
|
98
|
+
| `rly show --file spec.json --display-only` | present a browser board and continue immediately — no comment/note or acknowledgement required; inline `relay_show` uses this mode by default |
|
|
98
99
|
| `rly install --target <agent>` | write relay's rules into an agent's instruction file — `claude` `codex` `cursor` `copilot` `kiro` `windsurf` `cline` `gemini` `opencode` `droid` `agents`; `--all`, `--scope`, `--print`, `--list` |
|
|
99
100
|
| `rly upgrade` | update the CLI **and** refresh the skill in one step (safe around open boards; `--dry-run`, `--cli-only`, `--skill-only`) |
|
|
100
101
|
| `rly mcp` | run relay as an MCP App server so boards render **inline** in the chat — **stdio** for local desktop hosts (Claude Desktop, Codex), or `rly mcp --http` (Streamable HTTP) for web/mobile/remote; `rly mcp config` / `rly mcp install --target claude\|codex` to register it |
|
|
@@ -110,6 +111,19 @@ npm test # smoke tests with no external services (spawns real servers, fake-
|
|
|
110
111
|
|
|
111
112
|
## Changelog
|
|
112
113
|
|
|
114
|
+
### 0.16.0 — native viewer chrome, area comments, and durable agent waits
|
|
115
|
+
|
|
116
|
+
- Viewer toolbars now use browser-native sticky positioning instead of a
|
|
117
|
+
scroll-event counter-translation; open comment composers re-anchor on scroll
|
|
118
|
+
and preserve unsaved per-target drafts.
|
|
119
|
+
- Hold then drag on an image to comment on an exact rectangular area. Relay
|
|
120
|
+
saves the crop beside the browser-board record and returns its path; comparison
|
|
121
|
+
crops also identify and capture the selected Before/After source.
|
|
122
|
+
- Browser boards support `responseRequired:false` / `--display-only`; inline
|
|
123
|
+
`relay_show` is display-only by default. Board and waiter deadlines now default
|
|
124
|
+
to one day (`--timeout 0` removes Relay's deadline), with client-specific
|
|
125
|
+
background/polling guidance in the agent docs.
|
|
126
|
+
|
|
113
127
|
### 0.11.0 — render inline inside the Claude & Codex apps (MCP App)
|
|
114
128
|
- **`rly mcp` — relay as an MCP App** ([SEP-1865](https://modelcontextprotocol.io/seps/1865-mcp-apps-interactive-user-interfaces-for-mcp),
|
|
115
129
|
extension `io.modelcontextprotocol/ui`). A stdio MCP server with no npm runtime dependencies
|
|
@@ -177,7 +191,8 @@ npm test # smoke tests with no external services (spawns real servers, fake-
|
|
|
177
191
|
whose connection drops blocks further input instead of losing it, and
|
|
178
192
|
`rly rescue <id>` re-serves on the same port so an open tab reconnects. Active
|
|
179
193
|
same-Wi-Fi share links stay tied to the board and survive a same-port re-serve
|
|
180
|
-
until revoked.
|
|
194
|
+
until revoked. Reviewer submissions are isolated, reference-only side reviews
|
|
195
|
+
that never finish the owner's board; read-only links cannot mutate feedback.
|
|
181
196
|
- Still **zero npm runtime dependencies**, offline, and cross-platform. Vendored
|
|
182
197
|
browser libraries are loaded only for boards that need them.
|
|
183
198
|
|
package/docs/AGENT.md
CHANGED
|
@@ -54,7 +54,7 @@ stderr. Exit codes: `0` submitted/acknowledged · `2` timeout · `3` cancelled
|
|
|
54
54
|
prints the result JSON:
|
|
55
55
|
|
|
56
56
|
```sh
|
|
57
|
-
rly ask --file spec.json --timeout
|
|
57
|
+
rly ask --file spec.json --timeout 86400 # one day; 0 = no Relay deadline
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
**2. Detached (recommended when your shell tool has an execution time limit).**
|
|
@@ -62,7 +62,7 @@ Returns immediately with the board URL; collect later:
|
|
|
62
62
|
|
|
63
63
|
```sh
|
|
64
64
|
rly ask --file spec.json --detach # → {"status":"open","boardId":"b-…","url":"…"}
|
|
65
|
-
rly wait b-xxxxx --timeout
|
|
65
|
+
rly wait b-xxxxx --timeout 86400 # default: one day; pass 0 for no deadline
|
|
66
66
|
rly result b-xxxxx # non-blocking peek; while open it includes
|
|
67
67
|
# the live autosaved draft of the user's answers
|
|
68
68
|
```
|
|
@@ -79,19 +79,24 @@ click the topbar **Share** icon, or you can manage the same links for the user:
|
|
|
79
79
|
|
|
80
80
|
```sh
|
|
81
81
|
rly share b-xxxxx # list active share roles
|
|
82
|
-
rly share b-xxxxx --role review # reviewer:
|
|
83
|
-
rly share b-xxxxx --role collab # collaborator: edit/comment/submit
|
|
82
|
+
rly share b-xxxxx --role review # reviewer: answer/comment + submit a side review
|
|
83
|
+
rly share b-xxxxx --role collab # collaborator: edit/comment/final-submit as owner-authorized
|
|
84
|
+
rly share b-xxxxx --role read # read-only viewer: no answers/comments/submit
|
|
84
85
|
rly share b-xxxxx --role review --revoke # revoke one role
|
|
85
86
|
rly share b-xxxxx --revoke --all # revoke all active links
|
|
86
87
|
```
|
|
87
88
|
|
|
88
|
-
Use `review`
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
submit
|
|
93
|
-
|
|
94
|
-
|
|
89
|
+
Use `review` for independent feedback: each browser gets an isolated review
|
|
90
|
+
draft and may answer, comment, and submit, but the submission is stored as a
|
|
91
|
+
**reference-only side review**. It does not finish the board, wake the agent, or
|
|
92
|
+
complete `rly wait`; the owner (or an owner-authorized collaborator) must still
|
|
93
|
+
submit the final answer. `rly result <id>` exposes current side-review drafts and
|
|
94
|
+
submissions on demand under `sideReviews`, clearly marked `referenceOnly:true`.
|
|
95
|
+
Use `read` when the other device should only view the board and existing
|
|
96
|
+
feedback. Read-only viewers cannot answer, comment, edit blocks, open local file
|
|
97
|
+
links, or submit. Collaborators retain owner-authorized edit/comment/final-submit
|
|
98
|
+
permission. Active links keep the same token across a same-port
|
|
99
|
+
`rly reopen`/`rly rescue` until revoked.
|
|
95
100
|
|
|
96
101
|
## Inline mode — relay as an MCP App (Claude & Codex apps)
|
|
97
102
|
|
|
@@ -135,8 +140,9 @@ Both take **the exact same board spec** documented below (the tool `inputSchema`
|
|
|
135
140
|
fills it in, and their answers come back to you (answers, per-question notes,
|
|
136
141
|
comment) as a user-message turn, with structured context synced when the host
|
|
137
142
|
supports it — read them just as you would the CLI's result JSON. There is **no
|
|
138
|
-
`--detach`/`rly wait` dance, no stdout parsing, and no timeout** in this mode
|
|
139
|
-
|
|
143
|
+
`--detach`/`rly wait` dance, no stdout parsing, and no timeout** in this mode.
|
|
144
|
+
`relay_ask` stays live until the user submits and the host delivers the result;
|
|
145
|
+
`relay_show` is display-only by default and the agent continues immediately.
|
|
140
146
|
|
|
141
147
|
**Near-full parity with the browser board.** Local `codeFile` / `htmlFile` /
|
|
142
148
|
`diffFile` and local **image** files work inline too — the server inlines them
|
|
@@ -161,12 +167,25 @@ rly ask -q "Deploy to prod now?::yesno" -q "!Environment::single::dev,staging,pr
|
|
|
161
167
|
# label::type::comma,separated,options leading "!" = required
|
|
162
168
|
```
|
|
163
169
|
|
|
164
|
-
Visualization-only
|
|
170
|
+
Visualization-only, with acknowledgement/feedback:
|
|
165
171
|
|
|
166
172
|
```sh
|
|
167
173
|
rly show --html-file prototype.html --title "Dashboard concept" --height 600
|
|
168
174
|
```
|
|
169
175
|
|
|
176
|
+
Display-only (the agent continues immediately; no note, comments, or
|
|
177
|
+
Submit/Acknowledge action):
|
|
178
|
+
|
|
179
|
+
```sh
|
|
180
|
+
rly show --file spec.json --display-only
|
|
181
|
+
# equivalent spec field: {"responseRequired": false, "blocks": [...]}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`--display-only` automatically detaches the browser board. In MCP App mode,
|
|
185
|
+
`relay_show` is display-only by default; set `responseRequired:true` only when
|
|
186
|
+
the user really needs to acknowledge or leave feedback. `relay_ask` remains the
|
|
187
|
+
response-bearing tool.
|
|
188
|
+
|
|
170
189
|
Read a markdown file (no questions; library-free renderer; submit reads "Done"):
|
|
171
190
|
|
|
172
191
|
```sh
|
|
@@ -214,6 +233,7 @@ choices plus the full resolved file in `result.blockEdits[blockId]`:
|
|
|
214
233
|
"allowPartial": true, // default true: user may submit with gaps -> "skipped"
|
|
215
234
|
"note": true, // default true: optional free-text box -> result "comment"
|
|
216
235
|
"autoClose": true, // default true: tab tries to close itself after submit
|
|
236
|
+
"responseRequired": true, // false = display-only; no feedback/action, agent continues
|
|
217
237
|
"submitLabel": "Submit",
|
|
218
238
|
"questions": [
|
|
219
239
|
{ "id": "approach", "type": "single", "label": "Which approach?", "required": true,
|
|
@@ -516,11 +536,11 @@ the question-types section above.
|
|
|
516
536
|
| `diff` | proposed code changes / before-after — a unified diff rendered as a colored git-style comparison (no git needed) |
|
|
517
537
|
| `video` | demos, screen recordings, walkthroughs — YouTube/Vimeo embeds, a media URL, or a local video file (streamed) |
|
|
518
538
|
| `pdf` | quotes, reports, exports, forms — local `.pdf` files or PDF URLs rendered inline; local files stream from the board server |
|
|
519
|
-
| `image` | screenshots, mockup exports, photos — local files embed and work offline |
|
|
539
|
+
| `image` | screenshots, mockup exports, photos — local files embed and work offline; hold then drag to comment on an exact area |
|
|
520
540
|
| `palette` | color palettes / themes — swatch cards with hover-hex + click-to-copy; pair with a `color` question to let the user pick |
|
|
521
541
|
| `kpi` | big-number metric cards (`items:[{label,value,delta?,dir?,sub?}]`) with up/down/flat-tinted deltas — at-a-glance numbers without a chart |
|
|
522
542
|
| `typography` | type specimens (`specimens:[{label?,size?,weight?,font?,text?}]`) — react to type choices like a palette |
|
|
523
|
-
| `compare` | before/after images with a draggable divider (`before`/`after` = url/path/`{src,label}`)
|
|
543
|
+
| `compare` | before/after images with a draggable divider (`before`/`after` = url/path/`{src,label}`); area comments preserve which side was selected |
|
|
524
544
|
| `html` | anything else — pixel-perfect mockups, custom widgets, embeds |
|
|
525
545
|
|
|
526
546
|
Two cross-block fields work on **any** block: `"ref":"name"` makes it openable in
|
|
@@ -529,6 +549,14 @@ shown earlier — no scrolling); `image` blocks also take `"pins":true` for
|
|
|
529
549
|
click-to-drop coordinate pin-comments. `table` blocks take `"rowsFile"` (load
|
|
530
550
|
rows from .csv/.tsv/.json), `"filterable"`, and `"exportable"` (CSV download).
|
|
531
551
|
|
|
552
|
+
Every interactive `image` and `compare` block also supports area feedback:
|
|
553
|
+
hold briefly, then drag a rectangle. Relay saves a pixel crop beside the board
|
|
554
|
+
record and returns its local path with the annotation, so the agent can open the
|
|
555
|
+
exact pixels instead of inferring from a whole-image comment. In a comparison,
|
|
556
|
+
the side visible where the hold began is recorded as `before` or `after` and the
|
|
557
|
+
crop comes from that source image. Remote images that prevent browser canvas
|
|
558
|
+
access still return coordinates and a `cropUnavailable` marker.
|
|
559
|
+
|
|
532
560
|
### Height rules
|
|
533
561
|
|
|
534
562
|
- `markdown`, `code`: natural flow (no fixed height).
|
|
@@ -628,6 +656,8 @@ intro. Annotations are autosaved with the draft and returned in the final result
|
|
|
628
656
|
| `text` | `quote`, `prefix` (≤30 chars before), `suffix` (≤30 after) | selecting text in a markdown block |
|
|
629
657
|
| `html-element` | `ref`, `label`, `detail?` | hovering any element in a custom-HTML block (auto), or a `data-relay-annotate` / `relayKit.commentable()` element |
|
|
630
658
|
| `image` | `label` | clicking a PlantUML diagram or an image block |
|
|
659
|
+
| `image-point` | `x`, `y`, `label` | clicking an image authored with `pins:true` |
|
|
660
|
+
| `image-region` | `x`, `y`, `w`, `h`, `label`, optional `side`, `crop` | holding then dragging on an image or comparison; `crop.path` is the local image artifact in browser mode |
|
|
631
661
|
|
|
632
662
|
Read annotations as first-class feedback — they often carry the sharpest insight
|
|
633
663
|
(e.g. a user circling the one data point that concerns them, or quoting the exact
|
|
@@ -658,14 +688,15 @@ time). Use it instead of guessing timeouts:
|
|
|
658
688
|
```sh
|
|
659
689
|
rly result b-xxxxx # open board → includes "presence":
|
|
660
690
|
# {open, seen, visible, focused, secondsSinceActivity, secondsSincePing}
|
|
661
|
-
rly wait b-xxxxx --timeout
|
|
691
|
+
rly wait b-xxxxx --timeout 86400 --while-active --idle-grace 3600
|
|
662
692
|
```
|
|
663
693
|
|
|
664
694
|
`--while-active` keeps extending the wait as long as the user is demonstrably
|
|
665
695
|
active (page visible/focused and interaction within `--idle-grace` seconds,
|
|
666
|
-
default 180); once they
|
|
667
|
-
with `presence` attached
|
|
668
|
-
|
|
696
|
+
default 180); once the deadline is reached and they are idle it returns the
|
|
697
|
+
normal `wait-timeout` JSON, with `presence` attached. Relay now defaults both a
|
|
698
|
+
board and `rly wait` to 86,400 seconds (one day); pass `--timeout 0` for no Relay
|
|
699
|
+
deadline when the host can safely own a long-running waiter.
|
|
669
700
|
|
|
670
701
|
### A `timeout` on a detached board is NOT the end
|
|
671
702
|
|
|
@@ -680,7 +711,7 @@ tells them you stopped waiting and to prompt you afterward. So if you got a
|
|
|
680
711
|
so a late submit push-wakes you. A blocking `rly ask` (no `--detach`) still ends
|
|
681
712
|
hard on timeout, since there's no separate waiter to hand back to.
|
|
682
713
|
|
|
683
|
-
##
|
|
714
|
+
## Waiting and push-wake across agent clients
|
|
684
715
|
|
|
685
716
|
```sh
|
|
686
717
|
rly ask --file spec.json --detach --on-result 'curl -s -X POST localhost:9999/wake -d @-'
|
|
@@ -692,7 +723,19 @@ the board reaches a terminal status — submitted, acknowledged, timeout, or
|
|
|
692
723
|
cancelled — with the full result JSON piped to stdin and `RLY_BOARD_ID`,
|
|
693
724
|
`RLY_STATUS`, `RLY_URL` in the environment. `--notify-cmd` does the same from
|
|
694
725
|
a `wait` that obtains a terminal result. Write a file your harness watches,
|
|
695
|
-
hit a webhook — whatever wakes you.
|
|
726
|
+
hit a webhook — whatever wakes you. This is passive only when that client or
|
|
727
|
+
harness has an actual inbound wake mechanism; Relay does not pretend that a
|
|
728
|
+
file write alone starts a new model turn.
|
|
729
|
+
|
|
730
|
+
| Client/surface | Recommended Relay return path |
|
|
731
|
+
|---|---|
|
|
732
|
+
| MCP App (`relay_ask`) | No CLI wait. The inline board stays live and sends `ui/message` on submit, which starts the user-return turn. |
|
|
733
|
+
| MCP App (`relay_show`) | Display-only by default. Present it and continue immediately; no acknowledgement is requested. |
|
|
734
|
+
| Claude Code | Run `rly wait <id> --timeout 0` as a [background Bash task](https://docs.anthropic.com/en/docs/claude-code/interactive-mode#background-bash-commands); retain its task id/output rather than opening a second board. |
|
|
735
|
+
| Gemini CLI | Run the waiter in the background and set [`tools.shell.backgroundCompletionBehavior` to `"inject"`](https://geminicli.com/docs/reference/configuration/) so completion is returned to the agent; `"notify"` only informs the chat UI. |
|
|
736
|
+
| Codex | Keep one foreground waiter. Codex background terminals use explicit `write_stdin` polling (the [poll window defaults to five minutes](https://developers.openai.com/codex/config-reference)); the process can outlive each poll, but no portable browser-submit API wakes a stopped turn. |
|
|
737
|
+
| OpenCode | Core Bash is foreground. Its [ecosystem](https://opencode.ai/docs/ecosystem/) lists background-PTY extensions; use one only when installed, otherwise keep the waiter foreground. |
|
|
738
|
+
| Pi | Core intentionally has [no background Bash](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/usage.md); keep the waiter foreground or use an explicitly installed background-task extension. |
|
|
696
739
|
|
|
697
740
|
### Codex browser-board pattern
|
|
698
741
|
|
|
@@ -704,7 +747,7 @@ submits:
|
|
|
704
747
|
|
|
705
748
|
```sh
|
|
706
749
|
rly ask --file spec.json --detach
|
|
707
|
-
rly wait b-xxxxx --timeout
|
|
750
|
+
rly wait b-xxxxx --timeout 86400 --while-active --idle-grace 3600
|
|
708
751
|
```
|
|
709
752
|
|
|
710
753
|
If `rly wait` exits with `wait-timeout`, immediately run `rly result <boardId>`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khanglvm/relay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
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
|
@@ -85,8 +85,8 @@ priority call in a `rank` question — not in paragraphs. Unsure which exists? R
|
|
|
85
85
|
| `code` | source / config / command output — highlighted, line numbers, hover-a-line to comment |
|
|
86
86
|
| `diff` | code changes — colored unified/split, multi-file (`rly diff` builds the whole board) |
|
|
87
87
|
| `git-conflict` | conflict-marker files — side-by-side ours/theirs/base with hunk choices; returns resolved content in `result.blockEdits[blockId]` |
|
|
88
|
-
| `image` | screenshots / mockups / renders — zoom+pan; `pins:true` →
|
|
89
|
-
| `compare` | a before/after pair — draggable divider |
|
|
88
|
+
| `image` | screenshots / mockups / renders — zoom+pan; hold+drag → area crop comment; `pins:true` → point comments |
|
|
89
|
+
| `compare` | a before/after pair — draggable divider; area comments retain the selected side |
|
|
90
90
|
| `video` | a demo / screen recording / walkthrough |
|
|
91
91
|
| `pdf` | a quote / report / exported document that should render inline |
|
|
92
92
|
| `palette` | color schemes — swatch cards, hover-hex, click-to-copy |
|
|
@@ -130,7 +130,7 @@ blocking `rly ask` that gets killed cancels the board. Always prefer:
|
|
|
130
130
|
|
|
131
131
|
```sh
|
|
132
132
|
rly ask --file spec.json --detach # returns {"boardId":"b-…","url":…} immediately
|
|
133
|
-
rly wait b-xxxxx --timeout
|
|
133
|
+
rly wait b-xxxxx --timeout 86400 # default: one day; 0 = no Relay deadline
|
|
134
134
|
# (on exit 2 "wait-timeout" just run wait again)
|
|
135
135
|
rly result b-xxxxx # non-blocking peek (includes live draft)
|
|
136
136
|
```
|
|
@@ -142,7 +142,7 @@ create a new board just because a wait timed out.
|
|
|
142
142
|
For long waits prefer presence-aware waiting over a huge --timeout:
|
|
143
143
|
|
|
144
144
|
```sh
|
|
145
|
-
rly wait b-xxxxx --timeout
|
|
145
|
+
rly wait b-xxxxx --timeout 86400 --while-active --idle-grace 3600
|
|
146
146
|
# keeps extending while the user is demonstrably viewing/typing on the board;
|
|
147
147
|
# returns wait-timeout promptly once they are idle/gone (presence included)
|
|
148
148
|
rly result b-xxxxx # while open also shows presence {visible, focused, secondsSinceActivity}
|
|
@@ -159,7 +159,7 @@ So in Codex, keep the waiter in the foreground until the user submits:
|
|
|
159
159
|
|
|
160
160
|
```sh
|
|
161
161
|
rly ask --file spec.json --detach
|
|
162
|
-
rly wait b-xxxxx --timeout
|
|
162
|
+
rly wait b-xxxxx --timeout 86400 --while-active --idle-grace 3600
|
|
163
163
|
```
|
|
164
164
|
|
|
165
165
|
If `rly wait` exits with `wait-timeout`, immediately run `rly result <boardId>`.
|
|
@@ -168,9 +168,30 @@ If it is still open and the user may continue, run `rly wait` again. Do not use
|
|
|
168
168
|
webhooks, but normal Codex CLI sessions do not expose a portable inbound API
|
|
169
169
|
that wakes the current agent turn.
|
|
170
170
|
|
|
171
|
-
Blocking mode (`rly ask --file spec.json --timeout
|
|
171
|
+
Blocking mode (`rly ask --file spec.json --timeout 86400`, no `--detach`) is fine
|
|
172
172
|
ONLY when your shell tool has no execution time limit.
|
|
173
173
|
|
|
174
|
+
Relay defaults board and waiter deadlines to one day; use `--timeout 0` when the
|
|
175
|
+
client can safely own an indefinite process. Choose the return path by host:
|
|
176
|
+
|
|
177
|
+
- Inline MCP `relay_ask`: no CLI wait; its `ui/message` submission wakes the
|
|
178
|
+
return turn. Inline `relay_show` is display-only by default and does not wait.
|
|
179
|
+
- Claude Code: background `rly wait <id> --timeout 0` as a background Bash task.
|
|
180
|
+
- Gemini CLI: background the waiter and use
|
|
181
|
+
`tools.shell.backgroundCompletionBehavior: "inject"`.
|
|
182
|
+
- Codex: keep one foreground waiter; background terminals require explicit
|
|
183
|
+
polling and do not expose a portable browser-submit wake API.
|
|
184
|
+
- OpenCode/Pi: keep it foreground unless a background-task extension is
|
|
185
|
+
explicitly installed (Pi core has no background Bash).
|
|
186
|
+
|
|
187
|
+
Use `--on-result`/`--notify-cmd` only when the surrounding harness exposes a real
|
|
188
|
+
webhook, file watcher, or inbound wake path.
|
|
189
|
+
|
|
190
|
+
To present without asking for any response, use
|
|
191
|
+
`rly show --file spec.json --display-only` (equivalent to
|
|
192
|
+
`"responseRequired":false`). It returns immediately and shows no note,
|
|
193
|
+
comments, or Submit/Acknowledge action.
|
|
194
|
+
|
|
174
195
|
Useful flags: `--no-open` (don't auto-open the browser — for tests/CI; real
|
|
175
196
|
users need the tab, so omit it normally) · `--title` · `--timeout <sec>`.
|
|
176
197
|
|
|
@@ -189,16 +210,22 @@ with `rly share`:
|
|
|
189
210
|
rly share b-xxxxx # list active share roles
|
|
190
211
|
rly share b-xxxxx --role review # activate a reviewer link
|
|
191
212
|
rly share b-xxxxx --role collab # activate a collaborator link
|
|
213
|
+
rly share b-xxxxx --role read # activate a read-only link
|
|
192
214
|
rly share b-xxxxx --role review --revoke # revoke one role
|
|
193
215
|
rly share b-xxxxx --revoke --all # revoke all active share links
|
|
194
216
|
```
|
|
195
217
|
|
|
196
|
-
Use `review`
|
|
197
|
-
|
|
198
|
-
|
|
218
|
+
Use `review` for independent feedback. Each reviewer browser gets an isolated
|
|
219
|
+
draft and can answer, comment, and submit a **reference-only side review**. That
|
|
220
|
+
submission never finishes the board or wakes/completes the waiting agent; the
|
|
221
|
+
owner (or owner-authorized collaborator) must still provide the final answer.
|
|
222
|
+
When the owner asks for current reviewer input, run `rly result <id>` and inspect
|
|
223
|
+
`sideReviews.submissions` plus `sideReviews.drafts`; both are explicitly marked
|
|
224
|
+
reference-only. Use `read` for a view-only link with no answers, comments, block
|
|
225
|
+
edits, local-file opening, or submit. Use `collab` only when the user explicitly
|
|
199
226
|
wants that device to act as the owner: collaborators can edit answers, comment,
|
|
200
|
-
open allowed local file links, and submit. Shared viewers refresh from the
|
|
201
|
-
draft when another viewer
|
|
227
|
+
open allowed local file links, and final-submit. Shared viewers refresh from the
|
|
228
|
+
relevant live draft when another viewer in the same role/session saves.
|
|
202
229
|
Active share links are durable too: they keep the same token across a same-port
|
|
203
230
|
`rly reopen`/`rly rescue` until `rly share --revoke` disables them.
|
|
204
231
|
|
|
@@ -412,6 +439,15 @@ Add `"pins": true` to an `image` block: the user clicks any point on the image t
|
|
|
412
439
|
drop a comment anchored to that exact spot (Figma-style), returned as an
|
|
413
440
|
`{kind:"image-point", x, y}` annotation. Ideal for design/mockup review.
|
|
414
441
|
|
|
442
|
+
### Comment on an exact image area (local crop)
|
|
443
|
+
|
|
444
|
+
Every interactive `image` and `compare` block supports hold-then-drag area
|
|
445
|
+
feedback without another spec flag. Relay returns an `image-region` target with
|
|
446
|
+
normalized `x`, `y`, `w`, and `h`; browser boards also save the selected pixels
|
|
447
|
+
beside the board and return `target.crop.path`, which the agent should open with
|
|
448
|
+
its image viewer. Comparison targets include `side:"before"|"after"`, and the
|
|
449
|
+
crop comes from that source image rather than the composited slider view.
|
|
450
|
+
|
|
415
451
|
### Show a git diff in one step — `rly diff`
|
|
416
452
|
|
|
417
453
|
`rly diff [git args…]` runs `git diff` and opens the result as a diff board —
|
|
@@ -443,7 +479,7 @@ mention annotation in the board intro.
|
|
|
443
479
|
"id": "a1",
|
|
444
480
|
"questionId": "q-id or null",
|
|
445
481
|
"blockId": "b2",
|
|
446
|
-
"target": { "kind": "chart-element | mermaid-node | graphviz-node | table-cell | text | html-element | image | image-point | code-line", "..." },
|
|
482
|
+
"target": { "kind": "chart-element | mermaid-node | graphviz-node | table-cell | text | html-element | image | image-point | image-region | code-line", "..." },
|
|
447
483
|
"text": "user comment",
|
|
448
484
|
"author": "user",
|
|
449
485
|
"createdAt": "ISO",
|
package/src/cli.js
CHANGED
|
@@ -28,6 +28,8 @@ const BIN = path.join(PKG_ROOT, 'bin', 'rly.js');
|
|
|
28
28
|
const PKG_JSON = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8'));
|
|
29
29
|
const VERSION = PKG_JSON.version;
|
|
30
30
|
const PKG_NAME = PKG_JSON.name; // e.g. "@khanglvm/relay" — the global package to upgrade
|
|
31
|
+
const DEFAULT_BOARD_TIMEOUT_SEC = 86400;
|
|
32
|
+
const DEFAULT_WAIT_TIMEOUT_SEC = 86400;
|
|
31
33
|
|
|
32
34
|
const VALUED_FLAGS = new Set([
|
|
33
35
|
'file', 'html', 'html-file', 'title', 'intro', 'timeout', 'port',
|
|
@@ -171,6 +173,7 @@ async function resolveSpecInput(args, mode) {
|
|
|
171
173
|
if (args.htmlFile) raw.htmlFile = args.htmlFile;
|
|
172
174
|
if (args.height) raw.htmlHeight = args.height;
|
|
173
175
|
if (args.submitLabel) raw.submitLabel = args.submitLabel;
|
|
176
|
+
if (args.displayOnly === true) raw.responseRequired = false;
|
|
174
177
|
if (args.q.length) {
|
|
175
178
|
raw.questions = [...(raw.questions || []), ...args.q.map((s, i) => questionFromInline(s, i))];
|
|
176
179
|
}
|
|
@@ -198,7 +201,7 @@ function readFileOrThrow(p) {
|
|
|
198
201
|
|
|
199
202
|
async function runOrDetach(record, args) {
|
|
200
203
|
await assertSpecReady(record.spec);
|
|
201
|
-
const timeoutSec = args.timeout !== undefined ? Math.max(0, Number.parseInt(args.timeout, 10) || 0) :
|
|
204
|
+
const timeoutSec = args.timeout !== undefined ? Math.max(0, Number.parseInt(args.timeout, 10) || 0) : DEFAULT_BOARD_TIMEOUT_SEC;
|
|
202
205
|
const port = args.port !== undefined ? Number.parseInt(args.port, 10) || 0 : 0;
|
|
203
206
|
const open = args.open !== false;
|
|
204
207
|
|
|
@@ -231,7 +234,9 @@ async function runOrDetach(record, args) {
|
|
|
231
234
|
boardId: record.id,
|
|
232
235
|
url: info.url,
|
|
233
236
|
port: info.port,
|
|
234
|
-
hint:
|
|
237
|
+
hint: record.spec.responseRequired === false
|
|
238
|
+
? 'display-only board opened; no response will be collected'
|
|
239
|
+
: `block for the answers with: rly wait ${record.id}`,
|
|
235
240
|
});
|
|
236
241
|
return 0;
|
|
237
242
|
}
|
|
@@ -247,7 +252,7 @@ async function cmdAsk(args, mode) {
|
|
|
247
252
|
const spec = normalizeSpec(raw);
|
|
248
253
|
await assertSpecReady(spec);
|
|
249
254
|
const record = createBoard(spec);
|
|
250
|
-
return runOrDetach(record, args);
|
|
255
|
+
return runOrDetach(record, spec.responseRequired === false ? { ...args, detach: true } : args);
|
|
251
256
|
}
|
|
252
257
|
|
|
253
258
|
// `rly diff [git args…]` — run `git diff` for the user and open the result as a
|
|
@@ -639,11 +644,11 @@ async function fetchPresence(url) {
|
|
|
639
644
|
async function cmdWait(args) {
|
|
640
645
|
const id = args._[0];
|
|
641
646
|
if (!id) throw new CliError('usage: rly wait <board-id> [--timeout <sec>] [--while-active] [--idle-grace <sec>] [--notify-cmd <cmd>]');
|
|
642
|
-
const timeoutSec = args.timeout !== undefined ? Math.max(
|
|
647
|
+
const timeoutSec = args.timeout !== undefined ? Math.max(0, Number.parseInt(args.timeout, 10) || 0) : DEFAULT_WAIT_TIMEOUT_SEC;
|
|
643
648
|
const whileActive = args.whileActive === true;
|
|
644
649
|
const idleGrace = args.idleGrace !== undefined ? Math.max(0, Number.parseInt(args.idleGrace, 10) || 0) : 180;
|
|
645
650
|
const notifyCmd = typeof args.notifyCmd === 'string' && args.notifyCmd.trim() ? args.notifyCmd : null;
|
|
646
|
-
let deadline = Date.now() + timeoutSec * 1000;
|
|
651
|
+
let deadline = timeoutSec === 0 ? Number.POSITIVE_INFINITY : Date.now() + timeoutSec * 1000;
|
|
647
652
|
mustLoad(id);
|
|
648
653
|
|
|
649
654
|
// Push-wake: run the agent's --notify-cmd after a TERMINAL result, then print.
|
|
@@ -761,21 +766,27 @@ async function cmdWaitLoop(id, deadline, opts) {
|
|
|
761
766
|
|
|
762
767
|
async function cmdResult(args) {
|
|
763
768
|
const record = mustLoad(args._[0]);
|
|
769
|
+
const sideReviews = {
|
|
770
|
+
referenceOnly: true,
|
|
771
|
+
note: 'Side reviews are reference only. Wait for the board owner (or owner-authorized collaborator) to submit the final answer.',
|
|
772
|
+
submissions: Array.isArray(record.sideReviews) ? record.sideReviews : [],
|
|
773
|
+
drafts: record.reviewDrafts && typeof record.reviewDrafts === 'object' ? record.reviewDrafts : {},
|
|
774
|
+
};
|
|
764
775
|
if (record.result && record.result.finishedAt) {
|
|
765
|
-
printResult(record.result);
|
|
776
|
+
printResult({ ...record.result, sideReviews });
|
|
766
777
|
return exitCodeFor(record.result.status);
|
|
767
778
|
}
|
|
768
779
|
const running = loadRunning(record.id);
|
|
769
780
|
if (running && isAlive(running.pid)) {
|
|
770
781
|
// While open, expose the real-time autosaved draft so agents can peek, plus
|
|
771
782
|
// best-effort presence (whether the user is still viewing/focused/active).
|
|
772
|
-
const out = { status: 'open', boardId: record.id, url: running.url, draft: record.draft ?? null };
|
|
783
|
+
const out = { status: 'open', boardId: record.id, url: running.url, draft: record.draft ?? null, sideReviews };
|
|
773
784
|
const presence = await fetchPresence(running.url);
|
|
774
785
|
if (presence) out.presence = presence;
|
|
775
786
|
printResult(out); // draft can hold many annotations — sidecar it too
|
|
776
787
|
return 0;
|
|
777
788
|
}
|
|
778
|
-
printResult({ status: 'lost', boardId: record.id, draft: record.draft ?? null });
|
|
789
|
+
printResult({ status: 'lost', boardId: record.id, draft: record.draft ?? null, sideReviews });
|
|
779
790
|
return 5;
|
|
780
791
|
}
|
|
781
792
|
|
|
@@ -785,9 +796,10 @@ async function cmdShare(args) {
|
|
|
785
796
|
if (!running || !isAlive(running.pid)) {
|
|
786
797
|
throw new CliError(`board "${record.id}" is not running. Use \`rly reopen ${record.id} --detach\` first.`, 5);
|
|
787
798
|
}
|
|
788
|
-
|
|
789
|
-
if (role
|
|
790
|
-
|
|
799
|
+
let role = args.all ? 'all' : args.role;
|
|
800
|
+
if (role === 'readonly' || role === 'read-only') role = 'read';
|
|
801
|
+
if (role !== undefined && !['collab', 'review', 'read', 'all'].includes(role)) {
|
|
802
|
+
throw new CliError('--role must be collab, review, or read.', 4);
|
|
791
803
|
}
|
|
792
804
|
const shareUrl = new URL('/api/share', running.url);
|
|
793
805
|
let res;
|
|
@@ -1079,7 +1091,7 @@ the user should view — put it in relay instead of printing it.**
|
|
|
1079
1091
|
user-facing "wake this agent turn from a browser submit" command. In Codex,
|
|
1080
1092
|
keep the waiter in the foreground until the user submits:
|
|
1081
1093
|
\`rly ask --file spec.json --detach\` then
|
|
1082
|
-
\`rly wait <boardId> --timeout
|
|
1094
|
+
\`rly wait <boardId> --timeout 86400 --while-active --idle-grace 3600\`.
|
|
1083
1095
|
If wait exits with \`wait-timeout\`, immediately run \`rly result <boardId>\`;
|
|
1084
1096
|
if it is still open and the user may continue, run \`rly wait\` again. Do not
|
|
1085
1097
|
use \`--on-result\` as the primary Codex return path; normal Codex CLI sessions
|
|
@@ -1087,6 +1099,10 @@ the user should view — put it in relay instead of printing it.**
|
|
|
1087
1099
|
Detached boards are durable: a board timeout hands the agent a \`timeout\`
|
|
1088
1100
|
result but keeps the same URL/port serving until Submit or \`rly stop\`. Do
|
|
1089
1101
|
not create a new board just because a wait timed out.
|
|
1102
|
+
- **No response needed** → \`rly show --file spec.json --display-only\`. It
|
|
1103
|
+
returns immediately and shows no note, comments, or Submit/Acknowledge action.
|
|
1104
|
+
In MCP App mode, \`relay_show\` is display-only by default; \`relay_ask\` is the
|
|
1105
|
+
response-bearing tool.
|
|
1090
1106
|
- **A plan, structure, architecture, data, or prototype** → a relay board with
|
|
1091
1107
|
diagram/chart/table/code/image/html blocks — never ASCII diagrams or walls of prose.
|
|
1092
1108
|
- **"Show me the diff / git diff / these changes"** → \`rly diff\` (runs git diff →
|
|
@@ -1100,14 +1116,16 @@ the user should view — put it in relay instead of printing it.**
|
|
|
1100
1116
|
option its own visual (\`options[].blocks\`) so the user picks by looking.
|
|
1101
1117
|
- Same-Wi-Fi sharing is locked by default. When the user wants another device to
|
|
1102
1118
|
review or co-fill a running board, manage explicit links with \`rly share <id>\`:
|
|
1103
|
-
\`--role review\` creates
|
|
1104
|
-
|
|
1119
|
+
\`--role review\` creates an answer/comment link whose submissions are reference-only
|
|
1120
|
+
side reviews (\`rly result\` exposes them but they never complete \`rly wait\`),
|
|
1121
|
+
\`--role collab\` creates an owner-authorized edit/comment/final-submit link,
|
|
1122
|
+
\`--role read\` creates a view-only link, and \`--revoke\` disables active links. Active
|
|
1105
1123
|
share links are durable across same-port \`rly reopen\`/\`rly rescue\` until
|
|
1106
1124
|
revoked.
|
|
1107
1125
|
- **There's a purpose-built component for most content — use the MOST SPECIFIC one,
|
|
1108
1126
|
never plain prose when a block fits.** Blocks: \`table\` (sortable/filterable/CSV,
|
|
1109
1127
|
load from .csv/.json), \`chart\`, \`kpi\` (stat cards), \`mermaid\`/\`graphviz\`/\`plantuml\`,
|
|
1110
|
-
\`code\`, \`diff\`, \`git-conflict\`, \`image\` (+\`pins\`), \`compare\` (before/after), \`video\`, \`pdf\`, \`palette\`,
|
|
1128
|
+
\`code\`, \`diff\`, \`git-conflict\`, \`image\` (hold+drag area crops; +\`pins\` for points), \`compare\` (before/after with side-aware area crops), \`video\`, \`pdf\`, \`palette\`,
|
|
1111
1129
|
\`typography\`, \`html\`. Question types: \`single\`/\`multi\`/\`yesno\`/\`scale\`/\`color\`/
|
|
1112
1130
|
\`text\`/\`textarea\` plus \`rank\` (prioritize), \`allocate\` (split a budget), \`checklist\`
|
|
1113
1131
|
(per-item sign-off). For a business user, reach for \`kpi\`+\`chart\`+\`table\` and
|
|
@@ -1571,7 +1589,7 @@ async function cmdServeInternal(args) {
|
|
|
1571
1589
|
id,
|
|
1572
1590
|
port: args.port !== undefined ? Number.parseInt(args.port, 10) || 0 : 0,
|
|
1573
1591
|
open: args.open !== false,
|
|
1574
|
-
timeoutSec: args.timeout !== undefined ? Math.max(0, Number.parseInt(args.timeout, 10) || 0) :
|
|
1592
|
+
timeoutSec: args.timeout !== undefined ? Math.max(0, Number.parseInt(args.timeout, 10) || 0) : DEFAULT_BOARD_TIMEOUT_SEC,
|
|
1575
1593
|
quiet: true,
|
|
1576
1594
|
// Detached board: timeout hands back to the agent but keeps the same
|
|
1577
1595
|
// URL/port serving until Submit or explicit stop.
|
|
@@ -1591,7 +1609,9 @@ USAGE
|
|
|
1591
1609
|
quick inline questions ("!" = required, label::type::options)
|
|
1592
1610
|
rly ask ... --detach no blocking: prints {boardId,url} now; collect via \`rly wait <id>\`
|
|
1593
1611
|
rly ask ... --on-result "<cmd>" push-wake: run <cmd> when the board finishes (result JSON on stdin)
|
|
1594
|
-
rly show --html-file viz.html visualization
|
|
1612
|
+
rly show --html-file viz.html visualization board (submit button = acknowledge)
|
|
1613
|
+
rly show --file spec.json --display-only
|
|
1614
|
+
present without comments/acknowledgement; returns immediately
|
|
1595
1615
|
rly view <file.md> [more.md …] quick read-only board rendering markdown file(s) (no lib)
|
|
1596
1616
|
(.csv/.tsv/.json render as a filterable, sortable table;
|
|
1597
1617
|
.pdf streams in an inline PDF viewer)
|
|
@@ -1602,7 +1622,7 @@ USAGE
|
|
|
1602
1622
|
add --code for split code view with per-hunk Apply/Skip/Hold
|
|
1603
1623
|
rly git conflict [files…] board for resolving conflict-marker files; with no files, auto-detects
|
|
1604
1624
|
unmerged git files and returns resolved content in result.blockEdits
|
|
1605
|
-
rly wait <id> [--timeout
|
|
1625
|
+
rly wait <id> [--timeout 86400] block up to one day until the board finishes (0 = no deadline)
|
|
1606
1626
|
--while-active [--idle-grace 180]: keep waiting past the deadline
|
|
1607
1627
|
while the user is still viewing/focused & recently active
|
|
1608
1628
|
--notify-cmd "<cmd>": run <cmd> on a terminal result (JSON on stdin)
|
|
@@ -1610,8 +1630,9 @@ USAGE
|
|
|
1610
1630
|
read that file if your shell truncates stdout; never pipe to head/tail
|
|
1611
1631
|
rly result <id> result/status now (includes live autosaved draft + presence while open)
|
|
1612
1632
|
rly share <id> list active same-Wi-Fi share links for a running board
|
|
1613
|
-
rly share <id> --role review
|
|
1614
|
-
rly share <id> --role collab
|
|
1633
|
+
rly share <id> --role review reviewer: answer/comment/submit a reference-only side review
|
|
1634
|
+
rly share <id> --role collab collaborator: edit/comment/final-submit as owner-authorized
|
|
1635
|
+
rly share <id> --role read activate a read-only viewer link
|
|
1615
1636
|
rly share <id> --role review --revoke
|
|
1616
1637
|
revoke one share role; use --revoke --all to disable all roles
|
|
1617
1638
|
active links survive same-port reopen/rescue until revoked
|
|
@@ -1645,7 +1666,7 @@ USAGE
|
|
|
1645
1666
|
|
|
1646
1667
|
COMMON FLAGS
|
|
1647
1668
|
--title <s> --intro <s> --html-file <f> --height <px> --submit-label <s>
|
|
1648
|
-
--timeout <sec> (default
|
|
1669
|
+
--timeout <sec> (default 86400; 0 = none) --port <n> --no-open --detach --display-only
|
|
1649
1670
|
|
|
1650
1671
|
EXIT CODES 0 submitted/acknowledged · 2 timeout · 3 cancelled · 4 usage · 5 not found
|
|
1651
1672
|
|