@khanglvm/relay 0.2.0 → 0.3.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 +72 -5
- package/docs/AGENT.md +70 -4
- package/docs/assets/annotations.png +0 -0
- package/docs/assets/board-dark.png +0 -0
- package/docs/assets/board-light.png +0 -0
- package/docs/assets/demo.gif +0 -0
- package/docs/assets/mobile.png +0 -0
- package/package.json +1 -1
- package/skills/relay/SKILL.md +58 -24
- package/skills/relay/examples/blocks-showcase.json +9 -0
- package/src/cli.js +115 -2
- package/src/server.js +50 -11
- package/src/spec.js +28 -3
- package/src/ui/annotate.css +57 -5
- package/src/ui/annotate.js +100 -4
- package/src/ui/app.js +44 -0
- package/src/ui/blocks.css +34 -0
- package/src/ui/blocks.js +136 -0
- package/src/ui/style.css +9 -0
- package/vendor/VERSIONS.json +2 -1
- package/vendor/viz-standalone.js +9 -0
package/README.md
CHANGED
|
@@ -5,23 +5,38 @@
|
|
|
5
5
|
`rly` lets an AI agent (Claude Code, Codex, or anything that can run a CLI) ask
|
|
6
6
|
its user structured questions in a clean browser page — single/multi choice,
|
|
7
7
|
yes-no, free text, rating scales — and/or present rich content blocks (markdown,
|
|
8
|
-
charts, diagrams, tables, code,
|
|
8
|
+
charts, diagrams, tables, code, sandboxed HTML, Graphviz and PlantUML diagrams), then **block until the user clicks
|
|
9
9
|
Submit** and read the answers as JSON. No more "type *done* in the terminal",
|
|
10
10
|
no more hand-rolled HTML + throwaway servers.
|
|
11
11
|
|
|
12
12
|
Users can **hover chart points, diagram nodes, table cells, or select text to
|
|
13
13
|
leave inline comments** — returned alongside answers as `result.annotations`.
|
|
14
|
+
Agents can reply to those comments and reopen the board as a conversation thread.
|
|
14
15
|
|
|
15
16
|
- Zero runtime dependencies — plain Node ≥ 18, vanilla HTML/CSS/JS UI
|
|
16
17
|
- Light/dark theme (auto + manual toggle), responsive, content-focused
|
|
17
18
|
- Real-time answer autosave (drafts survive timeout/cancel)
|
|
18
19
|
- 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
|
|
20
|
+
- Native content blocks: markdown, mermaid diagrams, Graphviz diagrams, PlantUML, Chart.js charts, tables, code, sandboxed HTML
|
|
21
|
+
- Chart.js, Mermaid, and Graphviz are **vendored and lazy-loaded** only when a board uses them — the base board stays dependency-free and as fast as before
|
|
21
22
|
- Element-level annotations: users comment on chart points, diagram nodes, table cells, text, or custom HTML elements; returned as `result.annotations`
|
|
23
|
+
- **Threaded annotation replies**: agents can reply to user comments via `rly reopen --replies`; conversations shown inline in the board
|
|
24
|
+
- **Live board mutation**: `rly update` lets an agent push a new spec to an already-open board; the page reloads and prefills answers from draft
|
|
22
25
|
- Multiple boards at once, local history: reuse / modify / reopen / remove
|
|
23
26
|
- Agent-first: JSON on stdout, logs on stderr, `--detach` + `wait` for shell tools with execution time limits, built-in agent guide & skill
|
|
24
27
|
|
|
28
|
+
## See it
|
|
29
|
+
|
|
30
|
+
| Light theme | Dark theme |
|
|
31
|
+
|---|---|
|
|
32
|
+
|  |  |
|
|
33
|
+
|
|
34
|
+
| Annotation popover | Mobile |
|
|
35
|
+
|---|---|
|
|
36
|
+
|  |  |
|
|
37
|
+
|
|
38
|
+

|
|
39
|
+
|
|
25
40
|
## Install
|
|
26
41
|
|
|
27
42
|
```sh
|
|
@@ -198,14 +213,55 @@ on phones. Height: 100–2400 px, default 360. Fragments (no `<html>` tag) are
|
|
|
198
213
|
auto-wrapped to match the current theme; full documents receive a
|
|
199
214
|
`?theme=light|dark` query param.
|
|
200
215
|
|
|
216
|
+
### Graphviz diagram
|
|
217
|
+
|
|
218
|
+
```json
|
|
219
|
+
{ "type": "graphviz", "dot": "digraph { a -> b -> c }", "height": 300 }
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Rendered entirely **offline** via the vendored `viz-standalone.js` (Graphviz compiled to
|
|
223
|
+
WebAssembly). Never upscaled past the diagram's natural width; shrinks on narrow screens;
|
|
224
|
+
container max-height 1200 px with scroll. Nodes (`g.node`) and edges (`g.edge`) are
|
|
225
|
+
individually annotatable. Use Graphviz for precise dependency graphs, call graphs, or
|
|
226
|
+
state machines where Mermaid's auto-layout doesn't give enough control.
|
|
227
|
+
|
|
228
|
+
### PlantUML diagram
|
|
229
|
+
|
|
230
|
+
```json
|
|
231
|
+
{ "type": "plantuml", "code": "@startuml\nA -> B: request\n@enduml", "height": 340 }
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Rendered via a **PlantUML server** (default: `https://www.plantuml.com/plantuml`). The
|
|
235
|
+
diagram source is deflate-encoded client-side and sent as a URL parameter — no server
|
|
236
|
+
round-trip for the page itself, just an `<img>` request. If the server is unreachable or
|
|
237
|
+
the browser lacks `CompressionStream`, a muted error card is shown.
|
|
238
|
+
|
|
239
|
+
**Privacy note:** diagram source is encoded and sent to the configured PlantUML server.
|
|
240
|
+
For sensitive diagrams, host your own server and pass `"server": "https://plantuml.example.com"`.
|
|
241
|
+
|
|
242
|
+
The rendered image is registered as a single annotatable element (target kind `image`).
|
|
243
|
+
Use PlantUML for UML sequence diagrams, class diagrams, and component diagrams.
|
|
244
|
+
|
|
201
245
|
## Annotations
|
|
202
246
|
|
|
203
247
|
Users can leave inline comments on any annotatable element — chart data points,
|
|
204
|
-
mermaid nodes, table cells, text selections inside markdown, and labelled
|
|
248
|
+
mermaid nodes, graphviz nodes and edges, table cells, text selections inside markdown, and labelled
|
|
205
249
|
elements inside custom HTML. A small pin icon appears on hover; clicking opens a
|
|
206
250
|
comment popover. Comments are autosaved with the draft and returned in the final
|
|
207
251
|
result.
|
|
208
252
|
|
|
253
|
+
**Threaded replies:** each annotation can have replies. Agents can read
|
|
254
|
+
`result.annotations`, compose replies, and reopen the board as a conversation:
|
|
255
|
+
|
|
256
|
+
```sh
|
|
257
|
+
# After reading result.annotations from a previous board:
|
|
258
|
+
rly reopen <id> --replies replies.json
|
|
259
|
+
# replies.json: [{"annotationId":"a1","text":"Good catch — fixed in the next sprint."}]
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
The board reopens with agent replies shown inline under each comment. Users can reply
|
|
263
|
+
back; the conversation grows with each `rly reopen --replies` cycle.
|
|
264
|
+
|
|
209
265
|
### result.annotations shape
|
|
210
266
|
|
|
211
267
|
```json
|
|
@@ -226,22 +282,31 @@ result.
|
|
|
226
282
|
"value": 19
|
|
227
283
|
},
|
|
228
284
|
"text": "Feb spike was due to the onboarding push — not repeatable.",
|
|
229
|
-
"
|
|
285
|
+
"author": "user",
|
|
286
|
+
"createdAt": "2026-06-11T10:23:00.000Z",
|
|
287
|
+
"replies": [
|
|
288
|
+
{ "author": "agent", "text": "Confirmed — excluded from the trend line.", "createdAt": "2026-06-11T11:00:00.000Z" }
|
|
289
|
+
]
|
|
230
290
|
}
|
|
231
291
|
],
|
|
232
292
|
"durationMs": 58000
|
|
233
293
|
}
|
|
234
294
|
```
|
|
235
295
|
|
|
296
|
+
Each annotation has an optional `author` (`"user"` | `"agent"`, default `"user"`) and
|
|
297
|
+
an optional `replies` array (capped at 50; each reply: `{author, text, createdAt}`).
|
|
298
|
+
|
|
236
299
|
### Annotation target kinds
|
|
237
300
|
|
|
238
301
|
| kind | what the user clicked |
|
|
239
302
|
|---|---|
|
|
240
303
|
| `chart-element` | a bar, point, or pie slice — includes `datasetIndex`, `index`, `label`, `value` |
|
|
241
304
|
| `mermaid-node` | a node in a diagram — includes `nodeId`, `text` |
|
|
305
|
+
| `graphviz-node` | a node or edge in a Graphviz diagram — includes `nodeId`, `text` |
|
|
242
306
|
| `table-cell` | a cell — includes `row` (0-based), `col` (column key), `value` |
|
|
243
307
|
| `text` | a text selection inside a markdown block — includes `quote`, `prefix`, `suffix` |
|
|
244
308
|
| `html-element` | a labelled element inside custom HTML (via `kit.js`) — includes `label`, optional `detail` |
|
|
309
|
+
| `image` | a PlantUML diagram image — includes `label` |
|
|
245
310
|
|
|
246
311
|
### kit.js — annotatable custom HTML
|
|
247
312
|
|
|
@@ -272,6 +337,8 @@ opens the annotation popover in the parent page anchored to that element.
|
|
|
272
337
|
| `rly list [--json]` | Running boards |
|
|
273
338
|
| `rly open [id]` | Re-open the browser tab of a running board |
|
|
274
339
|
| `rly reopen <id>` | Serve a saved board again, **prefilled with saved answers** |
|
|
340
|
+
| `rly reopen <id> --replies file.json` | Reopen with agent replies appended to matching annotations |
|
|
341
|
+
| `rly update <id> [--file spec.json \| --title T \| --intro I \| -q "…"]` | Push a new spec to a running board — page reloads, answers survive via draft |
|
|
275
342
|
| `rly reuse <id> [--dump]` | Re-run a past board as a new one (blank) |
|
|
276
343
|
| `rly stop <id> \| --all` | Stop running board(s) — draft preserved |
|
|
277
344
|
| `rly history [--limit n] [--json]` | Saved boards |
|
package/docs/AGENT.md
CHANGED
|
@@ -149,6 +149,16 @@ per question (`"blocks": [...]` on a question object). Heights clamp to
|
|
|
149
149
|
// Mermaid diagram — vendored, lazy-loaded; natural height, max 1200 px + scroll
|
|
150
150
|
{ "type": "mermaid", "code": "graph TD; A-->B; B-->C", "height": 400 }
|
|
151
151
|
|
|
152
|
+
// Graphviz diagram — vendored viz-standalone.js (Graphviz-WASM), fully offline
|
|
153
|
+
// Nodes (g.node) and edges (g.edge) are individually annotatable
|
|
154
|
+
{ "type": "graphviz", "dot": "digraph { a -> b -> c }", "height": 300 }
|
|
155
|
+
|
|
156
|
+
// PlantUML diagram — rendered via a PlantUML server (default: plantuml.com)
|
|
157
|
+
// Source is deflate-encoded client-side; only an img URL is sent to the server.
|
|
158
|
+
// Use "server" for a self-hosted instance to avoid leaking sensitive diagrams.
|
|
159
|
+
{ "type": "plantuml", "code": "@startuml\nA -> B: request\n@enduml", "height": 340 }
|
|
160
|
+
{ "type": "plantuml", "code": "...", "server": "https://plantuml.example.com", "height": 300 }
|
|
161
|
+
|
|
152
162
|
// Chart — shorthand (lazy-loads vendored Chart.js; default height 320)
|
|
153
163
|
{
|
|
154
164
|
"type": "chart",
|
|
@@ -202,6 +212,8 @@ per question (`"blocks": [...]` on a question object). Heights clamp to
|
|
|
202
212
|
| Block | Best for |
|
|
203
213
|
|---|---|
|
|
204
214
|
| `mermaid` | flows, state machines, architecture overviews, sequence diagrams |
|
|
215
|
+
| `graphviz` | precise dependency graphs, call graphs, state machines when Mermaid's auto-layout falls short; individually annotatable nodes and edges |
|
|
216
|
+
| `plantuml` | UML diagrams (sequence, class, component) via server rendering; great for detailed interface contracts |
|
|
205
217
|
| `chart` | numbers, trends, comparisons, metrics |
|
|
206
218
|
| `table` | structured comparisons, option matrices, data grids |
|
|
207
219
|
| `markdown` | prose context, background, instructions, section headings |
|
|
@@ -262,27 +274,53 @@ intro. Annotations are autosaved with the draft and returned in the final result
|
|
|
262
274
|
"id": "a1",
|
|
263
275
|
"questionId": "q-id or null for board-level",
|
|
264
276
|
"blockId": "b2",
|
|
265
|
-
"target": {
|
|
266
|
-
"text": "
|
|
267
|
-
"
|
|
277
|
+
"target": { "kind": "chart-element", "datasetIndex": 0, "index": 1, "label": "Feb", "value": 19 },
|
|
278
|
+
"text": "Feb spike was from the onboarding push — not repeatable.",
|
|
279
|
+
"author": "user",
|
|
280
|
+
"createdAt": "2026-06-11T10:23:00.000Z",
|
|
281
|
+
"replies": [
|
|
282
|
+
{ "author": "agent", "text": "Confirmed — excluded from the trend line.", "createdAt": "2026-06-11T11:00:00.000Z" }
|
|
283
|
+
]
|
|
268
284
|
}
|
|
269
285
|
]
|
|
270
286
|
```
|
|
271
287
|
|
|
272
|
-
|
|
288
|
+
`author` is `"user"` (default, when absent) or `"agent"`. `replies` is an array of
|
|
289
|
+
`{author, text, createdAt}` objects, capped at 50 per annotation.
|
|
290
|
+
|
|
291
|
+
### All target kinds
|
|
273
292
|
|
|
274
293
|
| kind | Fields | Triggered by |
|
|
275
294
|
|---|---|---|
|
|
276
295
|
| `chart-element` | `datasetIndex`, `index`, `label`, `value` | clicking a bar, point, or pie slice |
|
|
277
296
|
| `mermaid-node` | `nodeId`, `text` | clicking a diagram node |
|
|
297
|
+
| `graphviz-node` | `nodeId`, `text` | clicking a Graphviz node or edge |
|
|
278
298
|
| `table-cell` | `row` (0-based), `col` (column key), `value` | clicking a table cell |
|
|
279
299
|
| `text` | `quote`, `prefix` (≤30 chars before), `suffix` (≤30 after) | selecting text in a markdown block |
|
|
280
300
|
| `html-element` | `label`, `detail?` | clicking a `relayKit.commentable()` element |
|
|
301
|
+
| `image` | `label` | clicking the PlantUML diagram image |
|
|
281
302
|
|
|
282
303
|
Read annotations as first-class feedback — they often carry the sharpest insight
|
|
283
304
|
(e.g. a user circling the one data point that concerns them, or quoting the exact
|
|
284
305
|
sentence they disagree with).
|
|
285
306
|
|
|
307
|
+
### Threaded replies — `rly reopen --replies`
|
|
308
|
+
|
|
309
|
+
After reading `result.annotations`, an agent can reply to specific comments and
|
|
310
|
+
reopen the board as a conversation:
|
|
311
|
+
|
|
312
|
+
```sh
|
|
313
|
+
rly reopen <id> --replies replies.json
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
`replies.json` is an array of `{"annotationId": "a1", "text": "..."}` objects.
|
|
317
|
+
The server seeds the draft from the saved result, appends each reply with
|
|
318
|
+
`author: "agent"` and `createdAt: now`, then serves the board prefilled. Unknown
|
|
319
|
+
annotation IDs cause a `CliError` (exit 4) listing valid IDs.
|
|
320
|
+
|
|
321
|
+
The UI shows agent and user replies as a thread under each comment — agent replies
|
|
322
|
+
use an accent chip, user replies use a muted chip.
|
|
323
|
+
|
|
286
324
|
## Managing boards
|
|
287
325
|
|
|
288
326
|
```sh
|
|
@@ -290,6 +328,8 @@ rly list [--json] # running boards (id, url, pid)
|
|
|
290
328
|
rly open [id] # re-open the browser tab of a running board
|
|
291
329
|
rly reopen <id> # serve a SAVED board again, prefilled with its saved
|
|
292
330
|
# answers/draft; user can edit and resubmit
|
|
331
|
+
rly reopen <id> --replies replies.json
|
|
332
|
+
# reopen with agent replies (see Threaded replies above)
|
|
293
333
|
rly reuse <id> # re-run a past board as a NEW board (blank answers)
|
|
294
334
|
rly spec <id> # print a saved spec — edit it, then `rly ask --file`
|
|
295
335
|
rly history [--json] # saved boards with statuses
|
|
@@ -300,6 +340,26 @@ rly rm <id> | --all # delete saved board(s)
|
|
|
300
340
|
Multiple boards can run at once (each gets its own port on 127.0.0.1).
|
|
301
341
|
Storage lives in `~/.relay` (override with `RLY_HOME`).
|
|
302
342
|
|
|
343
|
+
## Live board mutation — `rly update`
|
|
344
|
+
|
|
345
|
+
Push a new spec to an already-open board without stopping it:
|
|
346
|
+
|
|
347
|
+
```sh
|
|
348
|
+
rly update <boardId> --file new-spec.json # replace the full spec
|
|
349
|
+
rly update <boardId> --title "New title" # patch just the title
|
|
350
|
+
rly update <boardId> --intro "New intro" # patch just the intro
|
|
351
|
+
rly update <boardId> -q "!Priority::single::p0,p1,p2" # append a question
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
The page **reloads** for the user and prefills their previous answers from the
|
|
355
|
+
autosaved draft — answers for question IDs that no longer exist are silently
|
|
356
|
+
ignored. A small toast "Board updated by the agent" appears for 4 seconds.
|
|
357
|
+
|
|
358
|
+
Stdout: `{"status":"updated","boardId":"…","rev":2,"url":"…"}`.
|
|
359
|
+
|
|
360
|
+
**Caution:** the page reloads for the user. Batch your changes into one `rly update`
|
|
361
|
+
call rather than calling it repeatedly in a loop.
|
|
362
|
+
|
|
303
363
|
## Tips for agents
|
|
304
364
|
|
|
305
365
|
- Prefer `--detach` + `rly wait` if your shell tool kills long commands.
|
|
@@ -313,5 +373,11 @@ Storage lives in `~/.relay` (override with `RLY_HOME`).
|
|
|
313
373
|
leave inline comments — they won't discover it otherwise.
|
|
314
374
|
- Check `result.annotations` before generating your next output; a comment on a
|
|
315
375
|
specific data point or a quoted sentence often overrides the checkbox answer.
|
|
376
|
+
- Use `rly reopen <id> --replies replies.json` to answer the user's element
|
|
377
|
+
comments and reopen the board as a conversation thread.
|
|
378
|
+
- Use `rly update <id>` to push spec changes to a running board — the page
|
|
379
|
+
reloads and answers survive via draft autosave. Batch updates; do not spam.
|
|
380
|
+
- For sensitive PlantUML diagrams, set `"server": "https://your-server"` to avoid
|
|
381
|
+
sending source to the public plantuml.com server.
|
|
316
382
|
- Bundled universal skill (Claude Code, Codex, any SKILL.md-aware agent):
|
|
317
383
|
`rly skill install` — or `npx skills add khanglvm/relay`.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khanglvm/relay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Browser-based question boards with rich blocks (markdown, charts, mermaid, tables, code, sandboxed HTML) and element-level annotations for AI coding agents (Claude Code, Codex, …): ask users structured questions, present interactive visuals, collect inline comments, wait for submit, read answers as JSON.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agents",
|
package/skills/relay/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: relay
|
|
3
|
-
description: Ask the user interactive questions in a browser board (single/multi choice, yes-no, free text, scale) and/or present rich content blocks (markdown, mermaid diagrams, charts, interactive tables, code, sandboxed HTML), then wait for Submit and read JSON answers plus element-level annotations. PROACTIVELY use whenever you would otherwise (a) call a native ask-user/question tool with 2+ questions or options that need explanation, (b) describe a UI/design/plan/architecture in prose that a visual would show better — draft diagrams (mermaid), charts, and interactive tables as native blocks, or (c) hand-roll an HTML file or local server to demo an idea - relay replaces all three. Triggers - clarify requirements before ambiguous work, choose between approaches, plan approval, design/UX feedback, mockup or prototype review, compare alternatives, survey, metrics review, "ask the user", "show the user", "which do you prefer", "get feedback", diagram, chart, data table, architecture overview, metrics. Skip only for a single trivial yes/no confirmation.
|
|
3
|
+
description: Ask the user interactive questions in a browser board (single/multi choice, yes-no, free text, scale) and/or present rich content blocks (markdown, mermaid diagrams, graphviz, plantuml, uml, charts, interactive tables, code, sandboxed HTML), then wait for Submit and read JSON answers plus element-level annotations. PROACTIVELY use whenever you would otherwise (a) call a native ask-user/question tool with 2+ questions or options that need explanation, (b) describe a UI/design/plan/architecture in prose that a visual would show better — draft diagrams (mermaid, graphviz, plantuml), charts, and interactive tables as native blocks, or (c) hand-roll an HTML file or local server to demo an idea - relay replaces all three. Triggers - clarify requirements before ambiguous work, choose between approaches, plan approval, design/UX feedback, mockup or prototype review, compare alternatives, survey, metrics review, "ask the user", "show the user", "which do you prefer", "get feedback", diagram, chart, data table, architecture overview, metrics, dependency graph, sequence diagram, class diagram, uml, graphviz, plantuml. Skip only for a single trivial yes/no confirmation.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# relay (`rly`)
|
|
@@ -35,20 +35,21 @@ questions into ONE board rather than opening several in a row.
|
|
|
35
35
|
|
|
36
36
|
## Choose a pattern
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
**DEFAULT: detached.** Most agent shell tools kill long-running commands, and a
|
|
39
|
+
blocking `rly ask` that gets killed cancels the board. Always prefer:
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
```sh
|
|
42
|
+
rly ask --file spec.json --detach # returns {"boardId":"b-…","url":…} immediately
|
|
43
|
+
rly wait b-xxxxx --timeout 550 # blocks until submit, prints result JSON
|
|
44
|
+
# (on exit 2 "wait-timeout" just run wait again)
|
|
45
|
+
rly result b-xxxxx # non-blocking peek (includes live draft)
|
|
46
|
+
```
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
Blocking mode (`rly ask --file spec.json --timeout 1800`, no --detach) is fine
|
|
49
|
+
ONLY when your shell tool has no execution time limit.
|
|
46
50
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
rly wait b-xxxxx --timeout 3500 # blocks until submit, prints result JSON
|
|
50
|
-
rly result b-xxxxx # non-blocking peek (includes live draft)
|
|
51
|
-
```
|
|
51
|
+
Useful flags: `--no-open` (don't auto-open the browser — for tests/CI; real
|
|
52
|
+
users need the tab, so omit it normally) · `--title` · `--timeout <sec>`.
|
|
52
53
|
|
|
53
54
|
Exit codes: 0 submitted · 2 timeout · 3 cancelled · 5 not found. On
|
|
54
55
|
timeout/cancel the result still contains the autosaved `draft` of partial
|
|
@@ -66,7 +67,8 @@ answers and any annotations written so far.
|
|
|
66
67
|
{ "id": "parts", "type": "multi", "label": "Include?", "options": ["api", "ui"], "note": true },
|
|
67
68
|
{ "id": "ship", "type": "yesno", "label": "Ship now?" },
|
|
68
69
|
{ "id": "why", "type": "textarea", "label": "Reasoning?" },
|
|
69
|
-
{ "id": "conf", "type": "scale", "label": "Confidence", "min": 1, "max": 5
|
|
70
|
+
{ "id": "conf", "type": "scale", "label": "Confidence", "min": 1, "max": 5,
|
|
71
|
+
"minLabel": "low", "maxLabel": "high" }
|
|
70
72
|
]
|
|
71
73
|
}
|
|
72
74
|
```
|
|
@@ -92,6 +94,9 @@ Add `"blocks": [...]` to the root or to any question.
|
|
|
92
94
|
```jsonc
|
|
93
95
|
{ "type": "markdown", "md": "## Section\n**prose**" }
|
|
94
96
|
{ "type": "mermaid", "code": "graph TD; A-->B", "height": 400 }
|
|
97
|
+
{ "type": "graphviz", "dot": "digraph { a -> b }", "height": 300 }
|
|
98
|
+
{ "type": "plantuml", "code": "@startuml\nA->B\n@enduml", "height": 300 }
|
|
99
|
+
{ "type": "plantuml", "code": "...", "server": "https://plantuml.example.com" }
|
|
95
100
|
{ "type": "chart", "kind": "bar", "title": "...",
|
|
96
101
|
"labels": ["Jan","Feb"], "series": [{"label":"x","data":[1,2]}], "height": 320 }
|
|
97
102
|
{ "type": "chart", "config": { /* full Chart.js v4 config */ }, "height": 300 }
|
|
@@ -101,14 +106,15 @@ Add `"blocks": [...]` to the root or to any question.
|
|
|
101
106
|
{ "type": "html", "htmlFile": "viz.html", "height": 400 }
|
|
102
107
|
```
|
|
103
108
|
|
|
104
|
-
Chart.js and
|
|
105
|
-
dependency-free.
|
|
106
|
-
|
|
109
|
+
Chart.js, Mermaid, and Graphviz are **vendored and lazy-loaded** — the base board
|
|
110
|
+
stays dependency-free. PlantUML uses the public plantuml.com server by default;
|
|
111
|
+
pass `"server"` for a self-hosted instance. Legacy `"html"` / `"htmlFile"` /
|
|
112
|
+
`"htmlHeight"` on root or questions are still accepted and normalised automatically.
|
|
107
113
|
|
|
108
114
|
## Annotations
|
|
109
115
|
|
|
110
|
-
Users can hover chart points, diagram nodes, table cells, or
|
|
111
|
-
markdown to leave inline comments. Always mention this in the board intro.
|
|
116
|
+
Users can hover chart points, diagram nodes (mermaid + graphviz), table cells, or
|
|
117
|
+
select text in markdown to leave inline comments. Always mention this in the board intro.
|
|
112
118
|
|
|
113
119
|
`result.annotations` is an array of:
|
|
114
120
|
|
|
@@ -117,18 +123,32 @@ markdown to leave inline comments. Always mention this in the board intro.
|
|
|
117
123
|
"id": "a1",
|
|
118
124
|
"questionId": "q-id or null",
|
|
119
125
|
"blockId": "b2",
|
|
120
|
-
"target": {
|
|
121
|
-
"kind": "chart-element | mermaid-node | table-cell | text | html-element",
|
|
122
|
-
"..."
|
|
123
|
-
},
|
|
126
|
+
"target": { "kind": "chart-element | mermaid-node | graphviz-node | table-cell | text | html-element | image", "..." },
|
|
124
127
|
"text": "user comment",
|
|
125
|
-
"
|
|
128
|
+
"author": "user",
|
|
129
|
+
"createdAt": "ISO",
|
|
130
|
+
"replies": [{ "author": "agent", "text": "acknowledged", "createdAt": "ISO" }]
|
|
126
131
|
}
|
|
127
132
|
```
|
|
128
133
|
|
|
129
134
|
Read annotations before generating your next output — a comment on a specific
|
|
130
135
|
data point often carries sharper signal than a checkbox answer.
|
|
131
136
|
|
|
137
|
+
### Reply to annotations (agent → user conversation)
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
# 1. Read result from a previous board
|
|
141
|
+
rly result <id> # or rly wait <id>
|
|
142
|
+
|
|
143
|
+
# 2. Build replies file
|
|
144
|
+
# replies.json: [{"annotationId":"a1","text":"Good catch — fixed."}]
|
|
145
|
+
|
|
146
|
+
# 3. Reopen as a conversation
|
|
147
|
+
rly reopen <id> --replies replies.json
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Unknown annotation IDs cause an error listing valid IDs (exit 4).
|
|
151
|
+
|
|
132
152
|
## Recipes
|
|
133
153
|
|
|
134
154
|
**Plan approval** — board-level `markdown` block rendering the plan, one `yesno`
|
|
@@ -152,5 +172,19 @@ and table cells to comment on specific values.
|
|
|
152
172
|
|
|
153
173
|
`rly history` (saved boards) · `rly spec <id>` (print spec to modify) ·
|
|
154
174
|
`rly reuse <id>` (re-run blank) · `rly reopen <id>` (re-open with saved
|
|
155
|
-
answers prefilled) · `rly
|
|
175
|
+
answers prefilled) · `rly reopen <id> --replies file.json` (add agent replies) ·
|
|
176
|
+
`rly list` / `rly open` / `rly stop <id>` · `rly rm <id>`.
|
|
156
177
|
Multiple boards can run concurrently.
|
|
178
|
+
|
|
179
|
+
### Live mutation — `rly update`
|
|
180
|
+
|
|
181
|
+
Push a new spec to a running board. The page reloads and prefills answers from the
|
|
182
|
+
autosaved draft — answers survive, the user sees a toast "Board updated by the agent".
|
|
183
|
+
|
|
184
|
+
```sh
|
|
185
|
+
rly update <boardId> --file new-spec.json # replace spec
|
|
186
|
+
rly update <boardId> --title T --intro I # patch fields
|
|
187
|
+
rly update <boardId> -q "!Priority::single::p0,p1" # append question
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Batch your changes into one call — the page reloads for the user on each update.
|
|
@@ -26,6 +26,15 @@
|
|
|
26
26
|
"code": "graph LR\n Client-->|HTTPS|Gateway\n Gateway-->AuthService\n Gateway-->APIService\n APIService-->DB[(Postgres)]\n APIService-->Cache[(Redis)]\n APIService-->Queue[[Job queue]]\n Queue-->Worker\n Worker-->DB",
|
|
27
27
|
"height": 300
|
|
28
28
|
},
|
|
29
|
+
{
|
|
30
|
+
"type": "markdown",
|
|
31
|
+
"md": "## Service dependency graph\nThe Graphviz diagram below shows precise call-time dependencies between services. Click any node or edge to leave a comment."
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"type": "graphviz",
|
|
35
|
+
"dot": "digraph services {\n rankdir=LR\n node [shape=box style=filled fontname=Helvetica fontsize=11]\n Gateway [fillcolor=\"#c8e6c9\"]\n APIService [fillcolor=\"#fff9c4\"]\n AuthService[fillcolor=\"#c8e6c9\"]\n DB [shape=cylinder fillcolor=\"#bbdefb\"]\n Cache [shape=cylinder fillcolor=\"#bbdefb\"]\n Worker [fillcolor=\"#fff9c4\"]\n Gateway -> APIService\n Gateway -> AuthService\n APIService -> DB\n APIService -> Cache\n APIService -> Worker [style=dashed label=\"async\"]\n Worker -> DB\n}",
|
|
36
|
+
"height": 260
|
|
37
|
+
},
|
|
29
38
|
{
|
|
30
39
|
"type": "markdown",
|
|
31
40
|
"md": "## Service health snapshot\nCurrent status as of the last deployment. Click any cell to comment."
|
package/src/cli.js
CHANGED
|
@@ -8,6 +8,7 @@ import { normalizeSpec, questionFromInline, SPEC_SCHEMA } from './spec.js';
|
|
|
8
8
|
import {
|
|
9
9
|
createBoard,
|
|
10
10
|
loadBoard,
|
|
11
|
+
saveBoard,
|
|
11
12
|
deleteBoard,
|
|
12
13
|
listBoards,
|
|
13
14
|
listRunning,
|
|
@@ -26,7 +27,7 @@ const VERSION = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'),
|
|
|
26
27
|
|
|
27
28
|
const VALUED_FLAGS = new Set([
|
|
28
29
|
'file', 'html', 'html-file', 'title', 'intro', 'timeout', 'port',
|
|
29
|
-
'submit-label', 'height', 'limit', 'target', 'id',
|
|
30
|
+
'submit-label', 'height', 'limit', 'target', 'id', 'replies',
|
|
30
31
|
]);
|
|
31
32
|
|
|
32
33
|
function camel(key) {
|
|
@@ -183,8 +184,58 @@ async function cmdAsk(args, mode) {
|
|
|
183
184
|
return runOrDetach(record, args);
|
|
184
185
|
}
|
|
185
186
|
|
|
187
|
+
// Seeds the draft from the last result (as runBoard would on reopen) and
|
|
188
|
+
// appends agent replies to the matching annotations, so an agent can ANSWER
|
|
189
|
+
// the user's element comments and re-open the board as a conversation.
|
|
190
|
+
// Persists the record with the result archived so runBoard doesn't re-seed.
|
|
191
|
+
function seedAgentReplies(record, replies) {
|
|
192
|
+
if (!Array.isArray(replies)) throw new CliError('--replies file must be a JSON array of {annotationId, text}.', 4);
|
|
193
|
+
// Mirror runBoard's reopen draft-seeding from the prior result.
|
|
194
|
+
if (record.result) {
|
|
195
|
+
if (record.result.answers) {
|
|
196
|
+
record.draft = {
|
|
197
|
+
answers: record.result.answers,
|
|
198
|
+
comment: record.result.comment || '',
|
|
199
|
+
notes: record.result.notes || {},
|
|
200
|
+
annotations: record.result.annotations || [],
|
|
201
|
+
updatedAt: new Date().toISOString(),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
record.pastResults = [...(record.pastResults || []), record.result].slice(-10);
|
|
205
|
+
record.result = null;
|
|
206
|
+
}
|
|
207
|
+
const annotations = (record.draft && Array.isArray(record.draft.annotations)) ? record.draft.annotations : [];
|
|
208
|
+
const validIds = annotations.map((a) => a && a.id).filter(Boolean);
|
|
209
|
+
const now = new Date().toISOString();
|
|
210
|
+
replies.forEach((r, i) => {
|
|
211
|
+
if (r === null || typeof r !== 'object' || Array.isArray(r)) {
|
|
212
|
+
throw new CliError(`--replies[${i}]: must be an object {annotationId, text}.`, 4);
|
|
213
|
+
}
|
|
214
|
+
const annotationId = typeof r.annotationId === 'string' ? r.annotationId : '';
|
|
215
|
+
const text = typeof r.text === 'string' ? r.text : '';
|
|
216
|
+
if (!annotationId) throw new CliError(`--replies[${i}]: missing "annotationId".`, 4);
|
|
217
|
+
if (!text.trim()) throw new CliError(`--replies[${i}]: missing "text".`, 4);
|
|
218
|
+
const ann = annotations.find((a) => a && a.id === annotationId);
|
|
219
|
+
if (!ann) {
|
|
220
|
+
throw new CliError(
|
|
221
|
+
`--replies[${i}]: unknown annotationId "${annotationId}". Valid ids: ${validIds.length ? validIds.join(', ') : '(none)'}.`,
|
|
222
|
+
4
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
if (!Array.isArray(ann.replies)) ann.replies = [];
|
|
226
|
+
ann.replies.push({ author: 'agent', text, createdAt: now });
|
|
227
|
+
});
|
|
228
|
+
if (!record.draft) record.draft = { answers: {}, comment: '', notes: {}, annotations, updatedAt: now };
|
|
229
|
+
else record.draft.annotations = annotations;
|
|
230
|
+
saveBoard(record);
|
|
231
|
+
}
|
|
232
|
+
|
|
186
233
|
async function cmdReopen(args) {
|
|
187
234
|
const record = mustLoad(args._[0]);
|
|
235
|
+
if (args.replies !== undefined) {
|
|
236
|
+
const replies = parseJson(readFileOrThrow(args.replies), args.replies);
|
|
237
|
+
seedAgentReplies(record, replies);
|
|
238
|
+
}
|
|
188
239
|
const running = loadRunning(record.id);
|
|
189
240
|
if (running && isAlive(running.pid)) {
|
|
190
241
|
openUrl(running.url);
|
|
@@ -204,6 +255,64 @@ async function cmdReuse(args) {
|
|
|
204
255
|
return runOrDetach(record, args);
|
|
205
256
|
}
|
|
206
257
|
|
|
258
|
+
// Live-mutates a running board: rebuild the spec (full replace via --file, or
|
|
259
|
+
// patch the current spec via --title/--intro/-q), then POST it (already
|
|
260
|
+
// normalized) to the board's /api/update with the per-board mutation token.
|
|
261
|
+
async function cmdUpdate(args) {
|
|
262
|
+
const id = args._[0];
|
|
263
|
+
if (!id) throw new CliError('usage: rly update <board-id> (--file new-spec.json | --title T | --intro I | -q "...")');
|
|
264
|
+
const record = loadBoard(id);
|
|
265
|
+
if (!record) throw new CliError(`board "${id}" not found. See \`rly history\`.`, 5);
|
|
266
|
+
const running = loadRunning(id);
|
|
267
|
+
if (!running || !isAlive(running.pid)) {
|
|
268
|
+
throw new CliError(`board "${id}" is not running — \`rly reopen ${id}\` to serve it, then update.`, 5);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
let spec;
|
|
272
|
+
if (args.file) {
|
|
273
|
+
const raw =
|
|
274
|
+
args.file === '-'
|
|
275
|
+
? parseJson(await readStdin(), 'stdin')
|
|
276
|
+
: parseJson(readFileOrThrow(args.file), args.file);
|
|
277
|
+
spec = normalizeSpec(raw);
|
|
278
|
+
} else if (args.title || args.intro || args.q.length) {
|
|
279
|
+
// Patch the CURRENT spec, then re-normalize so it's a clean normalized spec.
|
|
280
|
+
const raw = { ...record.spec };
|
|
281
|
+
if (args.title) raw.title = args.title;
|
|
282
|
+
if (args.intro) raw.intro = args.intro;
|
|
283
|
+
if (args.q.length) {
|
|
284
|
+
raw.questions = [...(raw.questions || []), ...args.q.map((s, i) => questionFromInline(s, i))];
|
|
285
|
+
}
|
|
286
|
+
spec = normalizeSpec(raw);
|
|
287
|
+
} else {
|
|
288
|
+
throw new CliError('update needs --file <spec.json>, --title, --intro, or -q "...".', 4);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
let res;
|
|
292
|
+
try {
|
|
293
|
+
res = await fetch(new URL('/api/update', running.url), {
|
|
294
|
+
method: 'POST',
|
|
295
|
+
headers: { 'content-type': 'application/json', 'x-relay-token': running.token || '' },
|
|
296
|
+
body: JSON.stringify({ spec }),
|
|
297
|
+
});
|
|
298
|
+
} catch (err) {
|
|
299
|
+
throw new CliError(`could not reach board "${id}" at ${running.url}: ${String((err && err.message) || err)}`, 5);
|
|
300
|
+
}
|
|
301
|
+
if (res.status === 403) throw new CliError(`board "${id}" rejected the update token (stale running file?).`, 5);
|
|
302
|
+
if (!res.ok) {
|
|
303
|
+
let detail = '';
|
|
304
|
+
try {
|
|
305
|
+
detail = (await res.json()).error || '';
|
|
306
|
+
} catch {
|
|
307
|
+
// non-JSON body
|
|
308
|
+
}
|
|
309
|
+
throw new CliError(`board "${id}" rejected the update${detail ? `: ${detail}` : ''}.`, 4);
|
|
310
|
+
}
|
|
311
|
+
const body = await res.json();
|
|
312
|
+
printJson({ status: 'updated', boardId: id, rev: body.rev, url: running.url });
|
|
313
|
+
return 0;
|
|
314
|
+
}
|
|
315
|
+
|
|
207
316
|
async function cmdWait(args) {
|
|
208
317
|
const id = args._[0];
|
|
209
318
|
if (!id) throw new CliError('usage: rly wait <board-id> [--timeout <sec>]');
|
|
@@ -539,8 +648,10 @@ USAGE
|
|
|
539
648
|
rly result <id> result/status now (includes live autosaved draft while open)
|
|
540
649
|
rly list [--json] running boards
|
|
541
650
|
rly open [id] re-open the browser tab of a running board
|
|
542
|
-
rly reopen <id>
|
|
651
|
+
rly reopen <id> [--replies f.json] serve a saved board again, prefilled with saved answers
|
|
652
|
+
(--replies [{annotationId,text}] = agent answers to element comments)
|
|
543
653
|
rly reuse <id> [--dump] re-run a past board as a new board (--dump prints its spec)
|
|
654
|
+
rly update <id> --file spec.json live-mutate a RUNNING board (or --title/--intro/-q); page reloads
|
|
544
655
|
rly stop <id> | --all stop running board(s) (status: cancelled, draft preserved)
|
|
545
656
|
rly history [--limit n] [--json] saved boards
|
|
546
657
|
rly spec <id> print a saved board's spec JSON (edit, then ask --file again)
|
|
@@ -590,6 +701,8 @@ export async function main(argv) {
|
|
|
590
701
|
return await cmdReopen(parseArgs(rest));
|
|
591
702
|
case 'reuse':
|
|
592
703
|
return await cmdReuse(parseArgs(rest));
|
|
704
|
+
case 'update':
|
|
705
|
+
return await cmdUpdate(parseArgs(rest));
|
|
593
706
|
case 'wait':
|
|
594
707
|
return await cmdWait(parseArgs(rest));
|
|
595
708
|
case 'result':
|