@khanglvm/relay 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Le Vu Minh Khang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,325 @@
1
+ # relay
2
+
3
+ **The relay between AI agents and humans — boards, blocks, and element-level comments.**
4
+
5
+ `rly` lets an AI agent (Claude Code, Codex, or anything that can run a CLI) ask
6
+ its user structured questions in a clean browser page — single/multi choice,
7
+ yes-no, free text, rating scales — and/or present rich content blocks (markdown,
8
+ charts, diagrams, tables, code, custom HTML), then **block until the user clicks
9
+ Submit** and read the answers as JSON. No more "type *done* in the terminal",
10
+ no more hand-rolled HTML + throwaway servers.
11
+
12
+ Users can **hover chart points, diagram nodes, table cells, or select text to
13
+ leave inline comments** — returned alongside answers as `result.annotations`.
14
+
15
+ - Zero runtime dependencies — plain Node ≥ 18, vanilla HTML/CSS/JS UI
16
+ - Light/dark theme (auto + manual toggle), responsive, content-focused
17
+ - Real-time answer autosave (drafts survive timeout/cancel)
18
+ - Auto-closes the tab after submit and unblocks the CLI
19
+ - Native content blocks: markdown, mermaid diagrams, Chart.js charts, tables, code, sandboxed HTML
20
+ - Chart.js and Mermaid are **vendored and lazy-loaded** only when a board uses them — the base board stays dependency-free and as fast as before
21
+ - Element-level annotations: users comment on chart points, diagram nodes, table cells, text, or custom HTML elements; returned as `result.annotations`
22
+ - Multiple boards at once, local history: reuse / modify / reopen / remove
23
+ - Agent-first: JSON on stdout, logs on stderr, `--detach` + `wait` for shell tools with execution time limits, built-in agent guide & skill
24
+
25
+ ## Install
26
+
27
+ ```sh
28
+ npm i -g @khanglvm/relay # provides `rly` (and `relay`)
29
+ # or per-invocation:
30
+ npx -y @khanglvm/relay help
31
+ ```
32
+
33
+ ## Quick start
34
+
35
+ ```sh
36
+ # Quick inline questions ("!" = required, label::type::options)
37
+ rly ask -q "Deploy to prod?::yesno" -q "!Environment::single::dev,staging,prod"
38
+
39
+ # Full board from a spec
40
+ rly ask --file spec.json --timeout 1800
41
+
42
+ # Visualization-only (prototype/idea); submit button = "Acknowledge"
43
+ rly show --html-file prototype.html --title "Dashboard concept" --height 600
44
+
45
+ # Non-blocking pattern (for agent tools with exec timeouts)
46
+ rly ask --file spec.json --detach # → {"boardId":"b-…","url":"http://127.0.0.1:…"}
47
+ rly wait b-xxxxx # blocks until submit, prints result JSON
48
+ ```
49
+
50
+ The browser opens automatically; the user answers and clicks **Submit**; the
51
+ CLI prints something like:
52
+
53
+ ```json
54
+ {
55
+ "status": "submitted",
56
+ "boardId": "b-k3x9q2",
57
+ "answers": { "q1": "yes", "q2": "staging" },
58
+ "skipped": [],
59
+ "comment": "ship it",
60
+ "annotations": [],
61
+ "durationMs": 23000
62
+ }
63
+ ```
64
+
65
+ ## Board spec
66
+
67
+ ```jsonc
68
+ {
69
+ "title": "Feature direction",
70
+ "intro": "Context shown under the title.",
71
+ "blocks": [
72
+ { "type": "markdown", "md": "## Background\nUse this context when deciding." }
73
+ ],
74
+ "allowPartial": true, // user may submit with gaps (returned in "skipped")
75
+ "note": true, // optional free-text box → result "comment"
76
+ "autoClose": true, // tab closes itself after submit
77
+ "questions": [
78
+ { "id": "approach", "type": "single", "label": "Which approach?", "required": true,
79
+ "options": [{ "value": "a", "label": "A", "description": "fast" }, "B"], "other": true },
80
+ { "id": "scope", "type": "multi", "label": "Include?", "options": ["api", "ui", "docs"] },
81
+ { "id": "ship", "type": "yesno", "label": "Ship this week?" },
82
+ { "id": "name", "type": "text", "label": "Codename?", "placeholder": "falcon" },
83
+ { "id": "notes", "type": "textarea", "label": "Constraints?" },
84
+ { "id": "conf", "type": "scale", "label": "Confidence?", "min": 1, "max": 5,
85
+ "minLabel": "low", "maxLabel": "high",
86
+ "blocks": [{ "type": "markdown", "md": "Rate your confidence in the chosen approach." }] }
87
+ ]
88
+ }
89
+ ```
90
+
91
+ `rly schema` prints the full JSON Schema; `rly agent` prints the complete
92
+ agent-oriented guide (answer shapes, block reference, annotation shape, patterns).
93
+
94
+ ## Content blocks
95
+
96
+ Blocks can appear at the board level (`"blocks": [...]` on the root) or per question
97
+ (`"blocks": [...]` on a question object). Legacy `"html"` / `"htmlFile"` fields are
98
+ still accepted and normalised into a single `html` block automatically.
99
+
100
+ ### Markdown
101
+
102
+ ```json
103
+ { "type": "markdown", "md": "## Section\nAny **CommonMark** prose." }
104
+ ```
105
+
106
+ Built-in mini renderer — no library loaded.
107
+
108
+ ### Mermaid diagram
109
+
110
+ ```json
111
+ { "type": "mermaid", "code": "graph TD; A-->B; B-->C", "height": 400 }
112
+ ```
113
+
114
+ Lazy-loads the vendored Mermaid bundle only when used. `height` clamps to
115
+ 100–2400 px (default: natural flow, max 1200 px with scroll).
116
+
117
+ ### Chart — shorthand
118
+
119
+ ```json
120
+ {
121
+ "type": "chart",
122
+ "kind": "bar",
123
+ "title": "Q1 velocity",
124
+ "labels": ["Jan", "Feb", "Mar"],
125
+ "series": [
126
+ { "label": "Shipped", "data": [12, 19, 14], "color": "#4d8a66" },
127
+ { "label": "Planned", "data": [15, 15, 15] }
128
+ ],
129
+ "height": 320
130
+ }
131
+ ```
132
+
133
+ `kind`: `bar` | `line` | `pie` | `doughnut` | `radar` | `scatter`.
134
+ Omit `color` to use the built-in palette.
135
+
136
+ ### Chart — full Chart.js config
137
+
138
+ ```json
139
+ {
140
+ "type": "chart",
141
+ "config": {
142
+ "type": "bar",
143
+ "data": { "labels": ["A", "B"], "datasets": [{ "label": "x", "data": [1, 2] }] },
144
+ "options": { "plugins": { "legend": { "display": false } } }
145
+ },
146
+ "height": 280
147
+ }
148
+ ```
149
+
150
+ Pass any valid Chart.js v4 config object to `config`. Lazy-loads the vendored
151
+ Chart.js bundle.
152
+
153
+ ### Table
154
+
155
+ ```json
156
+ {
157
+ "type": "table",
158
+ "columns": [
159
+ { "key": "name", "label": "Name" },
160
+ { "key": "status", "label": "Status", "align": "center" },
161
+ { "key": "score", "label": "Score", "align": "right" }
162
+ ],
163
+ "rows": [
164
+ { "name": "Alpha", "status": "done", "score": 92 },
165
+ { "name": "Beta", "status": "wip", "score": 71 }
166
+ ],
167
+ "sortable": true
168
+ }
169
+ ```
170
+
171
+ `columns` may also be a plain `["A", "B", "C"]` string array, with `rows` as
172
+ parallel arrays: `[[val, val, val], ...]`. Users can click column headers to
173
+ sort when `"sortable": true`.
174
+
175
+ ### Code
176
+
177
+ ```json
178
+ { "type": "code", "lang": "js", "code": "const x = 1 + 2;" }
179
+ ```
180
+
181
+ Rendered in a styled pre/code block. `lang` is optional.
182
+
183
+ ### HTML (sandboxed iframe)
184
+
185
+ ```json
186
+ { "type": "html", "html": "<h1>Hello</h1>", "height": 360 }
187
+ ```
188
+
189
+ or reference a file:
190
+
191
+ ```json
192
+ { "type": "html", "htmlFile": "viz.html", "height": 400 }
193
+ ```
194
+
195
+ Rendered in a **sandboxed iframe** (`allow-scripts`, no parent access).
196
+ Width: always 100% of the content column — ~820 px max on desktop, ~300 px min
197
+ on phones. Height: 100–2400 px, default 360. Fragments (no `<html>` tag) are
198
+ auto-wrapped to match the current theme; full documents receive a
199
+ `?theme=light|dark` query param.
200
+
201
+ ## Annotations
202
+
203
+ Users can leave inline comments on any annotatable element — chart data points,
204
+ mermaid nodes, table cells, text selections inside markdown, and labelled
205
+ elements inside custom HTML. A small pin icon appears on hover; clicking opens a
206
+ comment popover. Comments are autosaved with the draft and returned in the final
207
+ result.
208
+
209
+ ### result.annotations shape
210
+
211
+ ```json
212
+ {
213
+ "status": "submitted",
214
+ "boardId": "b-k3x9q2",
215
+ "answers": { "approach": "a" },
216
+ "annotations": [
217
+ {
218
+ "id": "a1",
219
+ "questionId": null,
220
+ "blockId": "b2",
221
+ "target": {
222
+ "kind": "chart-element",
223
+ "datasetIndex": 0,
224
+ "index": 1,
225
+ "label": "Feb",
226
+ "value": 19
227
+ },
228
+ "text": "Feb spike was due to the onboarding push — not repeatable.",
229
+ "createdAt": "2026-06-11T10:23:00.000Z"
230
+ }
231
+ ],
232
+ "durationMs": 58000
233
+ }
234
+ ```
235
+
236
+ ### Annotation target kinds
237
+
238
+ | kind | what the user clicked |
239
+ |---|---|
240
+ | `chart-element` | a bar, point, or pie slice — includes `datasetIndex`, `index`, `label`, `value` |
241
+ | `mermaid-node` | a node in a diagram — includes `nodeId`, `text` |
242
+ | `table-cell` | a cell — includes `row` (0-based), `col` (column key), `value` |
243
+ | `text` | a text selection inside a markdown block — includes `quote`, `prefix`, `suffix` |
244
+ | `html-element` | a labelled element inside custom HTML (via `kit.js`) — includes `label`, optional `detail` |
245
+
246
+ ### kit.js — annotatable custom HTML
247
+
248
+ Inside a custom HTML iframe, load `/kit.js` to make elements commentable:
249
+
250
+ ```html
251
+ <script src="/kit.js"></script>
252
+ <script>
253
+ // Make any element commentable — users see a hover outline + click to comment
254
+ relayKit.commentable(document.getElementById('revenue-chart'), 'Revenue chart', 'Q1 2026');
255
+ relayKit.commentable(document.getElementById('cta-button'), 'CTA button');
256
+ </script>
257
+ ```
258
+
259
+ `relayKit.commentable(el, label, detail?)` — outlines `el` on hover; clicking
260
+ opens the annotation popover in the parent page anchored to that element.
261
+ `label` is shown in the annotation summary; `detail` is optional extra context.
262
+
263
+ ## Commands
264
+
265
+ | Command | What it does |
266
+ |---|---|
267
+ | `rly ask [--file spec.json \| --file - \| -q "…"]` | Create board, open browser, block until submit, print answers JSON |
268
+ | `rly ask … --detach` | Don't block — print `{boardId,url}` immediately |
269
+ | `rly show --html-file viz.html` | Visualization-only board (acknowledge) |
270
+ | `rly wait <id> [--timeout s]` | Block until board finishes, print result |
271
+ | `rly result <id>` | Result/status now — includes **live autosaved draft** while open |
272
+ | `rly list [--json]` | Running boards |
273
+ | `rly open [id]` | Re-open the browser tab of a running board |
274
+ | `rly reopen <id>` | Serve a saved board again, **prefilled with saved answers** |
275
+ | `rly reuse <id> [--dump]` | Re-run a past board as a new one (blank) |
276
+ | `rly stop <id> \| --all` | Stop running board(s) — draft preserved |
277
+ | `rly history [--limit n] [--json]` | Saved boards |
278
+ | `rly spec <id>` | Print a saved spec (edit → `rly ask --file`) |
279
+ | `rly rm <id> \| --all` | Delete saved board(s) |
280
+ | `rly schema` | JSON Schema of the spec |
281
+ | `rly agent` | Full guide for AI agents |
282
+ | `rly skill [install\|path]` | Bundled universal agent skill |
283
+
284
+ Common flags: `--title --intro --html-file --height --submit-label
285
+ --timeout <sec> --port <n> --no-open --detach`.
286
+
287
+ Exit codes: `0` submitted/acknowledged · `2` timeout · `3` cancelled ·
288
+ `4` usage · `5` not found.
289
+
290
+ Storage: `~/.relay` (override with `RLY_HOME`). Boards bind to `127.0.0.1` only.
291
+
292
+ ## Agent skill (Claude Code, Codex, …)
293
+
294
+ A universal [SKILL.md](skills/relay/SKILL.md) is bundled:
295
+
296
+ ```sh
297
+ rly skill install # auto-installs into ~/.claude/skills and ~/.codex/skills
298
+ rly skill install --target claude # or codex | both | <custom dir>
299
+ npx skills add khanglvm/relay # via the skills installer, straight from this repo
300
+ ```
301
+
302
+ `rly help` and `rly agent` also point agents at the skill, so an agent that
303
+ merely has the CLI installed can discover and self-install it.
304
+
305
+ ## Development
306
+
307
+ ```sh
308
+ npm test # zero-dep smoke tests (spawns real servers, fake-submits)
309
+ ```
310
+
311
+ ## Migration from quest-board
312
+
313
+ relay was formerly published as `@khanglvm/quest-board` (CLI: `qbd`). That
314
+ package is deprecated; install `@khanglvm/relay` instead.
315
+
316
+ - Storage moved from `~/.quest-board` to `~/.relay`. Override with `RLY_HOME`.
317
+ There is no automatic migration — copy boards manually if needed.
318
+ - The old `QUEST_BOARD_HOME` env var is still read as a fallback during the
319
+ transition period.
320
+ - Legacy `"html"` / `"htmlFile"` / `"htmlHeight"` fields in specs continue to
321
+ work and are silently normalised into an html block.
322
+
323
+ ## License
324
+
325
+ [MIT](LICENSE) © Le Vu Minh Khang
package/bin/rly.js ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../src/cli.js';
3
+
4
+ const code = await main(process.argv.slice(2));
5
+ process.exitCode = code ?? 0;
6
+ // Safety net: if some handle lingers after the work is done (e.g. a browser
7
+ // keep-alive socket that survived server.close), force the exit. unref'd, so
8
+ // a clean run exits naturally before this fires.
9
+ setTimeout(() => process.exit(process.exitCode ?? 0), 3000).unref();
package/docs/AGENT.md ADDED
@@ -0,0 +1,317 @@
1
+ # relay (`rly`) — agent guide
2
+
3
+ Purpose: ask the human structured questions in a browser tab and/or show them
4
+ rich content blocks (markdown, charts, diagrams, tables, code, custom HTML),
5
+ then **wait for them to click Submit** and read the answers as JSON from stdout.
6
+ No "type 'done' in the terminal", no hand-rolled HTML+server.
7
+
8
+ **Tell the user** at the start of your intro text that they can hover chart
9
+ points, diagram nodes, and table cells to leave comments, and select text in
10
+ markdown blocks to annotate — their comments come back in `result.annotations`
11
+ alongside their answers. Treat annotations as first-class feedback.
12
+
13
+ Everything machine-relevant is on **stdout as JSON**; human-facing logs go to
14
+ stderr. Exit codes: `0` submitted/acknowledged · `2` timeout · `3` cancelled ·
15
+ `4` usage error · `5` not found.
16
+
17
+ ## Two execution patterns
18
+
19
+ **1. Blocking (simplest).** The command blocks until the user submits, then
20
+ prints the result JSON:
21
+
22
+ ```sh
23
+ rly ask --file spec.json --timeout 1800
24
+ ```
25
+
26
+ **2. Detached (recommended when your shell tool has an execution time limit).**
27
+ Returns immediately with the board URL; collect later:
28
+
29
+ ```sh
30
+ rly ask --file spec.json --detach # → {"status":"open","boardId":"b-…","url":"…"}
31
+ rly wait b-xxxxx --timeout 3500 # blocks until submit, prints result JSON
32
+ rly result b-xxxxx # non-blocking peek; while open it includes
33
+ # the live autosaved draft of the user's answers
34
+ ```
35
+
36
+ Answers **autosave in real time** as the user fills the board — a page reload
37
+ restores them, and a draft survives timeouts/cancellation (included in those
38
+ results), so partial input is never lost.
39
+
40
+ ## Creating boards
41
+
42
+ From a JSON spec file (`--file spec.json`), stdin (`--file -`), or quick
43
+ inline questions:
44
+
45
+ ```sh
46
+ rly ask -q "Deploy to prod now?::yesno" -q "!Environment::single::dev,staging,prod"
47
+ # label::type::comma,separated,options leading "!" = required
48
+ ```
49
+
50
+ Visualization-only (no questions; submit button reads "Acknowledge"):
51
+
52
+ ```sh
53
+ rly show --html-file prototype.html --title "Dashboard concept" --height 600
54
+ ```
55
+
56
+ ## Board spec (JSON)
57
+
58
+ ```jsonc
59
+ {
60
+ "title": "Feature direction",
61
+ "intro": "Pick what we build next. Lines are preserved.",
62
+ "blocks": [
63
+ { "type": "markdown", "md": "## Background\nContext here." }
64
+ ],
65
+ "allowPartial": true, // default true: user may submit with gaps -> "skipped"
66
+ "note": true, // default true: optional free-text box -> result "comment"
67
+ "autoClose": true, // default true: tab tries to close itself after submit
68
+ "submitLabel": "Submit",
69
+ "questions": [
70
+ { "id": "approach", "type": "single", "label": "Which approach?", "required": true,
71
+ "options": [
72
+ { "value": "a", "label": "Approach A", "description": "fast, less flexible" },
73
+ "Approach B" // plain strings work too
74
+ ],
75
+ "other": true }, // adds a free-text "Other" option
76
+ { "id": "scope", "type": "multi", "label": "Include which parts?", "options": ["api", "ui", "docs"],
77
+ "note": true }, // optional free-text under the question
78
+ // → returned as result.notes.scope
79
+ { "id": "ship", "type": "yesno", "label": "Ship this week?" },
80
+ { "id": "name", "type": "text", "label": "Project codename?", "placeholder": "e.g. falcon" },
81
+ { "id": "notes", "type": "textarea", "label": "Any constraints?" },
82
+ { "id": "confidence", "type": "scale", "label": "Confidence?", "min": 1, "max": 5,
83
+ "minLabel": "low", "maxLabel": "high" },
84
+ { "id": "layout", "type": "single", "label": "Which layout?", "options": ["left", "right"],
85
+ "blocks": [{ "type": "markdown", "md": "Compare the two options above." }] }
86
+ ]
87
+ }
88
+ ```
89
+
90
+ `rly schema` prints the full JSON Schema.
91
+
92
+ ### Question types → answer shapes
93
+
94
+ | type | answer value in result |
95
+ |------------|-------------------------------------|
96
+ | `single` | `"value"` (Other → its text verbatim) |
97
+ | `multi` | `["a","b"]` (Other text appended) |
98
+ | `yesno` | `"yes"` \| `"no"` |
99
+ | `text` | `"string"` |
100
+ | `textarea` | `"string"` |
101
+ | `scale` | number (`min`…`max`, default 1–5) |
102
+
103
+ Aliases accepted: radio/choice/select→single, checkbox→multi,
104
+ boolean/bool/yn→yesno, input→text, longtext→textarea, rating/likert→scale.
105
+
106
+ ### Result JSON (stdout)
107
+
108
+ ```json
109
+ {
110
+ "status": "submitted",
111
+ "boardId": "b-xxxxx",
112
+ "answers": { "approach": "a", "scope": ["api", "ui"], "ship": "yes", "confidence": 4 },
113
+ "skipped": ["name"],
114
+ "comment": "free-text note from the user",
115
+ "notes": { "scope": "docs can wait until the API settles" },
116
+ "annotations": [
117
+ {
118
+ "id": "a1",
119
+ "questionId": null,
120
+ "blockId": "b2",
121
+ "target": { "kind": "chart-element", "datasetIndex": 0, "index": 1, "label": "Feb", "value": 19 },
122
+ "text": "Feb spike was from the onboarding push — not repeatable.",
123
+ "createdAt": "2026-06-11T10:23:00.000Z"
124
+ }
125
+ ],
126
+ "finishedAt": "2026-06-11T03:00:00.000Z",
127
+ "durationMs": 42000
128
+ }
129
+ ```
130
+
131
+ Unanswered questions are absent from `answers` and listed in `skipped`.
132
+ Questions with `"note": true` show a small optional free-text field; non-empty
133
+ notes come back in `notes` keyed by question id. On `timeout`/`cancelled`, a
134
+ `draft` field carries the autosaved partial answers and any annotations written
135
+ so far.
136
+
137
+ ## Blocks
138
+
139
+ Blocks can appear at the board level (`"blocks": [...]` on the root object) or
140
+ per question (`"blocks": [...]` on a question object). Heights clamp to
141
+ 100–2400 px.
142
+
143
+ ### All block shapes
144
+
145
+ ```jsonc
146
+ // Markdown — built-in mini renderer, no library
147
+ { "type": "markdown", "md": "## Heading\nAny **CommonMark** prose." }
148
+
149
+ // Mermaid diagram — vendored, lazy-loaded; natural height, max 1200 px + scroll
150
+ { "type": "mermaid", "code": "graph TD; A-->B; B-->C", "height": 400 }
151
+
152
+ // Chart — shorthand (lazy-loads vendored Chart.js; default height 320)
153
+ {
154
+ "type": "chart",
155
+ "kind": "bar", // bar | line | pie | doughnut | radar | scatter
156
+ "title": "Velocity",
157
+ "labels": ["Jan", "Feb", "Mar"],
158
+ "series": [
159
+ { "label": "Shipped", "data": [12, 19, 14], "color": "#4d8a66" },
160
+ { "label": "Planned", "data": [15, 15, 15] }
161
+ ],
162
+ "height": 320
163
+ }
164
+
165
+ // Chart — full Chart.js v4 config
166
+ {
167
+ "type": "chart",
168
+ "config": {
169
+ "type": "bar",
170
+ "data": { "labels": ["A", "B"], "datasets": [{ "label": "x", "data": [1, 2] }] },
171
+ "options": { "plugins": { "legend": { "display": false } } }
172
+ },
173
+ "height": 280
174
+ }
175
+
176
+ // Table — sortable, annotatable cells
177
+ {
178
+ "type": "table",
179
+ "columns": [
180
+ { "key": "name", "label": "Name" },
181
+ { "key": "status", "label": "Status", "align": "center" },
182
+ { "key": "score", "label": "Score", "align": "right" }
183
+ ],
184
+ "rows": [
185
+ { "name": "Alpha", "status": "done", "score": 92 },
186
+ { "name": "Beta", "status": "wip", "score": 71 }
187
+ ],
188
+ "sortable": true
189
+ }
190
+ // columns may also be plain string array; rows may be parallel arrays [[val,val],...]
191
+
192
+ // Code — styled pre/code block
193
+ { "type": "code", "lang": "js", "code": "const x = 1 + 2;" }
194
+
195
+ // HTML — sandboxed iframe; default height 360
196
+ { "type": "html", "html": "<h1>Hello</h1>", "height": 360 }
197
+ { "type": "html", "htmlFile": "viz.html", "height": 400 }
198
+ ```
199
+
200
+ ### When to use which block
201
+
202
+ | Block | Best for |
203
+ |---|---|
204
+ | `mermaid` | flows, state machines, architecture overviews, sequence diagrams |
205
+ | `chart` | numbers, trends, comparisons, metrics |
206
+ | `table` | structured comparisons, option matrices, data grids |
207
+ | `markdown` | prose context, background, instructions, section headings |
208
+ | `code` | code snippets, config examples, command output |
209
+ | `html` | anything else — pixel-perfect mockups, custom widgets, embeds |
210
+
211
+ ### Height rules
212
+
213
+ - `markdown`, `code`: natural flow (no fixed height).
214
+ - `mermaid`: natural flow, max-height 1200 px with internal scroll. Override with `"height"`.
215
+ - `chart`: default 320 px. Override with `"height"`.
216
+ - `html`: default 360 px. Override with `"height"`.
217
+ - `table`: natural flow.
218
+ - All heights clamp to 100–2400 px.
219
+
220
+ ## Custom HTML sizing contract
221
+
222
+ - Rendered in a **sandboxed iframe** (`allow-scripts allow-forms allow-popups
223
+ allow-modals`, **no** same-origin/parent access). Ship a self-contained HTML
224
+ document: inline your CSS/JS; external CDN resources do load, but offline-safe
225
+ inline is better.
226
+ - **Width: always 100% of the content column — up to ~820 px on desktop, as
227
+ narrow as ~300 px on phones. Design responsively; don't assume fixed width.**
228
+ - **Height: fixed per block via `height` (px, 100–2400). Default 360.**
229
+ Content taller than that scrolls inside the iframe.
230
+ - **Fragments** (no `<html>` tag) are auto-wrapped in a minimal document whose
231
+ background/text match the user's current theme. **Full documents** are served
232
+ verbatim and receive a `?theme=light|dark` query param on theme toggle.
233
+
234
+ ### kit.js — make iframe elements annotatable
235
+
236
+ Load `/kit.js` inside your custom HTML iframe to let users comment on specific
237
+ elements:
238
+
239
+ ```html
240
+ <script src="/kit.js"></script>
241
+ <script>
242
+ relayKit.commentable(document.getElementById('chart'), 'Revenue chart', 'Q1 2026');
243
+ relayKit.commentable(document.getElementById('hero-cta'), 'CTA button');
244
+ </script>
245
+ ```
246
+
247
+ `relayKit.commentable(el, label, detail?)` — outlines `el` on hover; a click
248
+ opens the annotation popover in the parent page anchored to the element.
249
+ Annotations come back in `result.annotations` with `target.kind = "html-element"`,
250
+ `target.label`, and (if provided) `target.detail`.
251
+
252
+ ## Annotations
253
+
254
+ Users can comment on any annotatable element. Tell them about it in your board
255
+ intro. Annotations are autosaved with the draft and returned in the final result.
256
+
257
+ ### result.annotations shape
258
+
259
+ ```json
260
+ "annotations": [
261
+ {
262
+ "id": "a1",
263
+ "questionId": "q-id or null for board-level",
264
+ "blockId": "b2",
265
+ "target": { ... },
266
+ "text": "user comment text",
267
+ "createdAt": "2026-06-11T10:23:00.000Z"
268
+ }
269
+ ]
270
+ ```
271
+
272
+ ### All 5 target kinds
273
+
274
+ | kind | Fields | Triggered by |
275
+ |---|---|---|
276
+ | `chart-element` | `datasetIndex`, `index`, `label`, `value` | clicking a bar, point, or pie slice |
277
+ | `mermaid-node` | `nodeId`, `text` | clicking a diagram node |
278
+ | `table-cell` | `row` (0-based), `col` (column key), `value` | clicking a table cell |
279
+ | `text` | `quote`, `prefix` (≤30 chars before), `suffix` (≤30 after) | selecting text in a markdown block |
280
+ | `html-element` | `label`, `detail?` | clicking a `relayKit.commentable()` element |
281
+
282
+ Read annotations as first-class feedback — they often carry the sharpest insight
283
+ (e.g. a user circling the one data point that concerns them, or quoting the exact
284
+ sentence they disagree with).
285
+
286
+ ## Managing boards
287
+
288
+ ```sh
289
+ rly list [--json] # running boards (id, url, pid)
290
+ rly open [id] # re-open the browser tab of a running board
291
+ rly reopen <id> # serve a SAVED board again, prefilled with its saved
292
+ # answers/draft; user can edit and resubmit
293
+ rly reuse <id> # re-run a past board as a NEW board (blank answers)
294
+ rly spec <id> # print a saved spec — edit it, then `rly ask --file`
295
+ rly history [--json] # saved boards with statuses
296
+ rly stop <id> | --all # stop running board(s) → status "cancelled", draft kept
297
+ rly rm <id> | --all # delete saved board(s)
298
+ ```
299
+
300
+ Multiple boards can run at once (each gets its own port on 127.0.0.1).
301
+ Storage lives in `~/.relay` (override with `RLY_HOME`).
302
+
303
+ ## Tips for agents
304
+
305
+ - Prefer `--detach` + `rly wait` if your shell tool kills long commands.
306
+ - Don't pass `--no-open` for real users — the browser tab opening *is* the
307
+ notification. Use it only in tests.
308
+ - Quote JSON carefully; prefer writing a spec file or piping via `--file -`.
309
+ - Use stable `id`s on questions so your follow-up logic reads clean keys.
310
+ - `rly result <id>` while a board is open returns the live draft — useful to
311
+ check whether the user has started answering.
312
+ - In the board `intro`, tell users they can hover chart points / select text to
313
+ leave inline comments — they won't discover it otherwise.
314
+ - Check `result.annotations` before generating your next output; a comment on a
315
+ specific data point or a quoted sentence often overrides the checkbox answer.
316
+ - Bundled universal skill (Claude Code, Codex, any SKILL.md-aware agent):
317
+ `rly skill install` — or `npx skills add khanglvm/relay`.