@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 +21 -0
- package/README.md +325 -0
- package/bin/rly.js +9 -0
- package/docs/AGENT.md +317 -0
- package/package.json +46 -0
- package/skills/relay/SKILL.md +156 -0
- package/skills/relay/examples/blocks-showcase.json +118 -0
- package/skills/relay/examples/feature-feedback.json +27 -0
- package/skills/relay/examples/prototype-review.json +25 -0
- package/src/cli.js +629 -0
- package/src/open.js +15 -0
- package/src/server.js +371 -0
- package/src/spec.js +435 -0
- package/src/store.js +147 -0
- package/src/ui/annotate.css +151 -0
- package/src/ui/annotate.js +440 -0
- package/src/ui/app.js +605 -0
- package/src/ui/blocks.css +170 -0
- package/src/ui/blocks.js +719 -0
- package/src/ui/index.html +21 -0
- package/src/ui/kit.js +413 -0
- package/src/ui/style.css +231 -0
- package/src/util.js +19 -0
- package/vendor/VERSIONS.json +5 -0
- package/vendor/chart.umd.js +14 -0
- package/vendor/mermaid.min.js +3405 -0
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`.
|