@khanglvm/relay 0.13.8 → 0.14.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 CHANGED
@@ -43,7 +43,7 @@ step, leaving any boards you have open untouched.
43
43
  relay also runs as an **MCP App** ([SEP-1865](https://modelcontextprotocol.io/seps/1865-mcp-apps-interactive-user-interfaces-for-mcp))
44
44
  — so the same board renders **inline, right in the conversation**, on Claude
45
45
  desktop **and mobile** and in Codex, no browser tab. `rly mcp` is a
46
- zero-dependency stdio MCP server; register it once and the agent gets two tools,
46
+ stdio MCP server with no npm runtime dependencies; register it once and the agent gets two tools,
47
47
  `relay_ask` (collect decisions/feedback with real form controls) and
48
48
  `relay_show` (present a plan, diagram, diff, table, or prototype):
49
49
 
@@ -81,16 +81,18 @@ unchanged; pick whichever surface fits.
81
81
  | Feedback = another wall of text | Click any chart point, diagram node, table cell, or sentence to comment — the agent replies and the thread grows on the board |
82
82
 
83
83
  Everything autosaves in real time (drafts survive timeouts), multiple boards
84
- run at once, and the package has **zero runtime dependencies** — plain
85
- Node ≥ 18; Chart.js / Mermaid / Graphviz are vendored and lazy-loaded offline.
84
+ run at once, and the package has **zero npm runtime dependencies** — plain
85
+ Node ≥ 18. Browser-side Chart.js, Mermaid, and Viz.js assets are vendored in
86
+ the package and lazy-loaded offline when a board actually needs them.
86
87
 
87
88
  ## Learn more
88
89
 
89
90
  | | |
90
91
  |---|---|
91
92
  | `rly help` | every command at a glance |
92
- | `rly git pick` / `rly git cherry-pick` | choose commit actions and rank the order directly on a board |
93
+ | `rly git pick` / `rly git cherry-pick` | choose commit actions and rank the order directly on a board; add `--code` to cherry-pick with split code review and per-hunk Apply/Skip/Hold |
93
94
  | `rly git conflict [files…]` | auto-detect unmerged conflict files, or open specific local paths, and return resolved content in `result.blockEdits` |
95
+ | `rly share <board-id>` | activate/list/revoke same-Wi-Fi reviewer or collaborator links for a running browser board |
94
96
  | `rly view <file.md> …` | open a quick read-only board that renders local markdown file(s), data files, or PDFs — library-free; great for plans, READMEs, reports, quotes |
95
97
  | `rly install --target <agent>` | write relay's rules into an agent's instruction file — `claude` `codex` `cursor` `copilot` `kiro` `windsurf` `cline` `gemini` `opencode` `droid` `agents`; `--all`, `--scope`, `--print`, `--list` |
96
98
  | `rly upgrade` | update the CLI **and** refresh the skill in one step (safe around open boards; `--dry-run`, `--cli-only`, `--skill-only`) |
@@ -102,14 +104,14 @@ Node ≥ 18; Chart.js / Mermaid / Graphviz are vendored and lazy-loaded offline.
102
104
  ## Development
103
105
 
104
106
  ```sh
105
- npm test # zero-dep smoke tests (spawns real servers, fake-submits)
107
+ npm test # smoke tests with no external services (spawns real servers, fake-submits)
106
108
  ```
107
109
 
108
110
  ## Changelog
109
111
 
110
112
  ### 0.11.0 — render inline inside the Claude & Codex apps (MCP App)
111
113
  - **`rly mcp` — relay as an MCP App** ([SEP-1865](https://modelcontextprotocol.io/seps/1865-mcp-apps-interactive-user-interfaces-for-mcp),
112
- extension `io.modelcontextprotocol/ui`). A zero-dependency stdio MCP server
114
+ extension `io.modelcontextprotocol/ui`). A stdio MCP server with no npm runtime dependencies
113
115
  that declares a `ui://relay/board` resource (`text/html;profile=mcp-app`) and
114
116
  two tools, **`relay_ask`** and **`relay_show`**, linked to it via
115
117
  `_meta.ui.resourceUri` (plus `openai/outputTemplate` for ChatGPT/Codex). The
@@ -172,7 +174,8 @@ npm test # zero-dep smoke tests (spawns real servers, fake-submits)
172
174
  - **Durable drafts / rescue** — every autosave mirrors to `localStorage`; a
173
175
  board whose connection drops blocks further input instead of losing it, and
174
176
  `rly rescue <id>` re-serves on the same port so an open tab reconnects.
175
- - Still **zero runtime dependencies**, offline, and cross-platform.
177
+ - Still **zero npm runtime dependencies**, offline, and cross-platform. Vendored
178
+ browser libraries are loaded only for boards that need them.
176
179
 
177
180
  ### 0.9.1
178
181
  - The board **intro renders as markdown** (bold/italic/code/links/lists).
package/docs/AGENT.md CHANGED
@@ -71,6 +71,28 @@ Answers **autosave in real time** as the user fills the board — a page reload
71
71
  restores them, and a draft survives timeouts/cancellation (included in those
72
72
  results), so partial input is never lost.
73
73
 
74
+ ## Same-Wi-Fi sharing
75
+
76
+ Browser boards are local-owner only by default. A same-Wi-Fi phone/tablet/other
77
+ laptop gets a locked page until sharing is explicitly activated. The owner can
78
+ click **Share** at the bottom of the board, or you can manage the same links for
79
+ the user:
80
+
81
+ ```sh
82
+ rly share b-xxxxx # list active share roles
83
+ rly share b-xxxxx --role review # reviewer: add comments only
84
+ rly share b-xxxxx --role collab # collaborator: edit/comment/submit
85
+ rly share b-xxxxx --role review --revoke # revoke one role
86
+ rly share b-xxxxx --revoke --all # revoke all active links
87
+ ```
88
+
89
+ Use `review` unless the user explicitly wants the other device to submit as the
90
+ owner. Reviewers cannot change answers, submit, edit comments, or delete
91
+ comments; their autosaves merge new comments into the existing draft.
92
+ Collaborators can edit answers, comment, open allowed local file links, and
93
+ submit. Shared viewers refresh from the live draft when another viewer saves,
94
+ deferred while someone is typing/commenting.
95
+
74
96
  ## Inline mode — relay as an MCP App (Claude & Codex apps)
75
97
 
76
98
  Everything above is the **CLI** surface (you run `rly` in a terminal and read
@@ -166,6 +188,7 @@ Pick/cherry-pick commits or resolve conflicts on a board:
166
188
  ```sh
167
189
  rly git pick --limit 20 # checklist: pick/cherry-pick/drop + rank order
168
190
  rly git cherry-pick main..feature # checklist: cherry-pick/skip/hold + rank order
191
+ rly git cherry-pick --code main..feature # adds split code review with Apply/Skip/Hold per hunk
169
192
  rly git conflict # auto-detect unmerged files with conflict markers
170
193
  rly git conflict src/app.js # resolve a specific conflicted file
171
194
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khanglvm/relay",
3
- "version": "0.13.8",
3
+ "version": "0.14.0",
4
4
  "description": "Question boards with rich blocks (markdown, charts, mermaid, tables, code, diffs, video, sandboxed HTML), clickable local file-links, and element-level annotations for AI coding agents (Claude Code, Codex, …): ask users structured questions, present interactive visuals, collect inline comments, read answers as JSON — in a local browser board OR rendered INLINE inside the Claude & Codex apps as an MCP App (SEP-1865).",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -46,7 +46,7 @@ at — show it in a relay board instead of printing it.**
46
46
  | Gather requirements / plan approval / feedback round | **rly** |
47
47
  | Architecture or flow that benefits from a diagram | **rly** (mermaid block) |
48
48
  | "Show me the diff" / git diff / code changes / before-after | **rly** (`diff` block — run `git diff`, render it; never dump it in the terminal) |
49
- | Pick/cherry-pick commits or resolve conflict files | **rly git** (`rly git pick`, `rly git cherry-pick`, `rly git conflict`) |
49
+ | Pick/cherry-pick commits or resolve conflict files | **rly git** (`rly git pick`, `rly git cherry-pick --code`, `rly git conflict`) |
50
50
  | A demo, screen recording or walkthrough | **rly** (`video` block) |
51
51
  | Point the user at a file to open (log, capture, report) | **rly** (a clickable local file-link in markdown) |
52
52
  | Let the user read a markdown file (README, plan, report) | **rly view file.md** (or a `markdown` block with `mdFile`) — never dump the file into the terminal |
@@ -174,6 +174,28 @@ Exit codes: 0 submitted · 2 timeout · 3 cancelled · 5 not found. On
174
174
  timeout/cancel the result still contains the autosaved `draft` of partial
175
175
  answers and any annotations written so far.
176
176
 
177
+ ## Same-Wi-Fi sharing
178
+
179
+ Browser boards are local-owner only by default. A phone/tablet/other laptop on
180
+ the same Wi-Fi gets a locked page until sharing is explicitly activated. The
181
+ owner can click **Share** at the bottom of the board, or an agent can manage the
182
+ same links with `rly share`:
183
+
184
+ ```sh
185
+ rly share b-xxxxx # list active share roles
186
+ rly share b-xxxxx --role review # activate a reviewer link
187
+ rly share b-xxxxx --role collab # activate a collaborator link
188
+ rly share b-xxxxx --role review --revoke # revoke one role
189
+ rly share b-xxxxx --revoke --all # revoke all active share links
190
+ ```
191
+
192
+ Use `review` when the other device should add comments only. Reviewers cannot
193
+ change answers, submit, edit comments, or delete comments; their autosaves merge
194
+ new comments into the existing draft. Use `collab` only when the user explicitly
195
+ wants that device to act as the owner: collaborators can edit answers, comment,
196
+ open allowed local file links, and submit. Shared viewers refresh from the live
197
+ draft when another viewer saves, deferred while someone is typing/commenting.
198
+
177
199
  ## Minimal spec
178
200
 
179
201
  ```json
@@ -347,8 +369,9 @@ the option (and stays annotatable); the label row selects. Use per-option
347
369
  blocks whenever a question's choices have visual/example context; skip them
348
370
  for plainly textual options.
349
371
 
350
- Chart.js, Mermaid, and Graphviz are **vendored and lazy-loaded** — the base board
351
- stays dependency-free. PlantUML uses the public plantuml.com server by default;
372
+ Chart.js, Mermaid, and Graphviz are **vendored and lazy-loaded** — the package
373
+ has no npm runtime dependencies, but these browser assets are bundled for rich
374
+ blocks. PlantUML uses the public plantuml.com server by default;
352
375
  pass `"server"` for a self-hosted instance. Legacy `"html"` / `"htmlFile"` /
353
376
  `"htmlHeight"` on root or questions are still accepted and normalised automatically.
354
377
 
package/src/cli.js CHANGED
@@ -33,7 +33,7 @@ const VALUED_FLAGS = new Set([
33
33
  'file', 'html', 'html-file', 'title', 'intro', 'timeout', 'port',
34
34
  'submit-label', 'height', 'limit', 'target', 'id', 'replies',
35
35
  'on-result', 'notify-cmd', 'idle-grace', 'scope', 'range',
36
- 'host', 'token', 'allow-origin',
36
+ 'host', 'token', 'allow-origin', 'role',
37
37
  ]);
38
38
 
39
39
  function camel(key) {
@@ -313,6 +313,19 @@ function gitCommitRows(range, limit) {
313
313
  .filter((r) => r.sha && r.short);
314
314
  }
315
315
 
316
+ function gitCommitDiff(commit) {
317
+ return runGit([
318
+ '--no-pager',
319
+ 'show',
320
+ '--format=',
321
+ '--patch',
322
+ '--find-renames',
323
+ '--find-copies',
324
+ '--no-ext-diff',
325
+ commit,
326
+ ], `git show ${commit} failed`).trimEnd();
327
+ }
328
+
316
329
  async function cmdGit(rest) {
317
330
  const args = parseArgs(rest);
318
331
  const modes = new Set(['pick', 'cherry-pick', 'cherrypick', 'conflict', 'conflicts', 'resolve']);
@@ -323,11 +336,13 @@ async function cmdGit(rest) {
323
336
 
324
337
  const blocks = [];
325
338
  const questions = [];
339
+ let reviewDiffCount = 0;
326
340
 
327
341
  if (mode === 'pick' || mode === 'cherry-pick') {
328
342
  const range = args.range || args._[0] || '';
329
343
  const commits = gitCommitRows(range, args.limit);
330
344
  if (!commits.length) throw new CliError(`git log${range ? ` ${range}` : ''} produced no commits to show.`);
345
+ const withCode = args.code === true || args.patch === true || args.diff === true;
331
346
  blocks.push({
332
347
  type: 'table',
333
348
  columns: ['short', 'date', 'author', 'subject'],
@@ -351,13 +366,32 @@ async function cmdGit(rest) {
351
366
  ? [{ value: 'cherry-pick', label: 'Cherry-pick', tone: 'ok' }, { value: 'skip', label: 'Skip', tone: 'muted' }, { value: 'hold', label: 'Hold', tone: 'warn' }]
352
367
  : [{ value: 'pick', label: 'Pick', tone: 'ok' }, { value: 'cherry-pick', label: 'Cherry-pick', tone: 'warn' }, { value: 'drop', label: 'Drop', tone: 'bad' }],
353
368
  });
354
- questions.push({
355
- id: 'commit_order',
356
- type: 'rank',
357
- label: mode === 'cherry-pick' ? 'Cherry-pick order' : 'Preferred commit order',
358
- description: 'Drag or move commits into the order the agent should apply them.',
359
- options,
360
- });
369
+ if (options.length > 1) {
370
+ questions.push({
371
+ id: 'commit_order',
372
+ type: 'rank',
373
+ label: mode === 'cherry-pick' ? 'Cherry-pick order' : 'Preferred commit order',
374
+ description: 'Drag or move commits into the order the agent should apply them.',
375
+ options,
376
+ });
377
+ }
378
+ if (withCode) {
379
+ for (const c of commits) {
380
+ const diff = gitCommitDiff(c.sha);
381
+ if (!diff.trim()) continue;
382
+ blocks.push({
383
+ type: 'diff',
384
+ title: `${c.short} ${c.subject}`,
385
+ filename: `${c.short} ${c.subject}`,
386
+ commit: c.sha,
387
+ reviewKind: mode,
388
+ review: true,
389
+ view: 'split',
390
+ diff,
391
+ });
392
+ reviewDiffCount++;
393
+ }
394
+ }
361
395
  }
362
396
 
363
397
  const paths = mode === 'conflict' ? gitConflictPaths(args._) : gitConflictPaths([]);
@@ -381,7 +415,9 @@ async function cmdGit(rest) {
381
415
  : mode === 'cherry-pick' ? 'Cherry-pick commits' : 'Pick commits');
382
416
  const intro = args.intro || (conflictCount
383
417
  ? 'Resolve each conflict on the board. The submitted result includes result.blockEdits with per-hunk choices and a full resolved file preview for every conflict block.'
384
- : 'Pick commit actions and order on the board. Submit returns the selected actions and rank order as JSON.');
418
+ : reviewDiffCount
419
+ ? 'Review commit code hunks on the board. Apply, skip, or hold each hunk; submit returns commit actions plus diff-review hunk choices as JSON.'
420
+ : 'Pick commit actions and order on the board. Submit returns the selected actions and rank order as JSON.');
385
421
  const spec = normalizeSpec({ title, intro, blocks, questions, submitLabel: 'Submit git choices' });
386
422
  await assertSpecReady(spec);
387
423
  const record = createBoard(spec);
@@ -743,6 +779,45 @@ async function cmdResult(args) {
743
779
  return 5;
744
780
  }
745
781
 
782
+ async function cmdShare(args) {
783
+ const record = mustLoad(args._[0]);
784
+ const running = loadRunning(record.id);
785
+ if (!running || !isAlive(running.pid)) {
786
+ throw new CliError(`board "${record.id}" is not running. Use \`rly reopen ${record.id} --detach\` first.`, 5);
787
+ }
788
+ const role = args.all ? 'all' : args.role;
789
+ if (role !== undefined && !['collab', 'review', 'all'].includes(role)) {
790
+ throw new CliError('--role must be collab or review.', 4);
791
+ }
792
+ const shareUrl = new URL('/api/share', running.url);
793
+ let res;
794
+ try {
795
+ if (args.revoke) {
796
+ res = await fetch(shareUrl, {
797
+ method: 'DELETE',
798
+ headers: { 'content-type': 'application/json' },
799
+ body: JSON.stringify({ role: role || 'all' }),
800
+ });
801
+ } else if (role && role !== 'all') {
802
+ res = await fetch(shareUrl, {
803
+ method: 'POST',
804
+ headers: { 'content-type': 'application/json' },
805
+ body: JSON.stringify({ role }),
806
+ });
807
+ } else {
808
+ res = await fetch(shareUrl, { method: 'GET' });
809
+ }
810
+ } catch (err) {
811
+ throw new CliError(`could not reach board "${record.id}" at ${running.url}: ${String((err && err.message) || err)}`, 5);
812
+ }
813
+ const body = await res.json().catch(() => null);
814
+ if (!res.ok) {
815
+ throw new CliError((body && body.error) || `share request failed with HTTP ${res.status}`, res.status === 400 ? 4 : 5);
816
+ }
817
+ printJson({ status: args.revoke ? 'revoked' : role && role !== 'all' ? 'active' : 'shares', boardId: record.id, ...body });
818
+ return 0;
819
+ }
820
+
746
821
  function cmdList(args) {
747
822
  const running = listRunning();
748
823
  if (args.json) {
@@ -1020,6 +1095,10 @@ the user should view — put it in relay instead of printing it.**
1020
1095
  - Point the user at a file with a clickable local path in a markdown block; embed a
1021
1096
  screen recording with a \`video\` block; when answer choices are visual, give each
1022
1097
  option its own visual (\`options[].blocks\`) so the user picks by looking.
1098
+ - Same-Wi-Fi sharing is locked by default. When the user wants another device to
1099
+ review or co-fill a running board, manage explicit links with \`rly share <id>\`:
1100
+ \`--role review\` creates a comments-only reviewer link, \`--role collab\` creates
1101
+ an edit/comment/submit link, and \`--revoke\` disables active links.
1023
1102
  - **There's a purpose-built component for most content — use the MOST SPECIFIC one,
1024
1103
  never plain prose when a block fits.** Blocks: \`table\` (sortable/filterable/CSV,
1025
1104
  load from .csv/.json), \`chart\`, \`kpi\` (stat cards), \`mermaid\`/\`graphviz\`/\`plantuml\`,
@@ -1515,6 +1594,7 @@ USAGE
1515
1594
  --title; other args pass to git: rly diff --staged | HEAD~1 | -- path)
1516
1595
  rly git pick [range] board for choosing pick/cherry-pick/drop actions plus commit order
1517
1596
  rly git cherry-pick [range] board for choosing commits and cherry-pick order
1597
+ add --code for split code view with per-hunk Apply/Skip/Hold
1518
1598
  rly git conflict [files…] board for resolving conflict-marker files; with no files, auto-detects
1519
1599
  unmerged git files and returns resolved content in result.blockEdits
1520
1600
  rly wait <id> [--timeout 3600] block until board finishes, print result JSON
@@ -1524,6 +1604,11 @@ USAGE
1524
1604
  NB: also writes the FULL result to "resultFile" (first field) —
1525
1605
  read that file if your shell truncates stdout; never pipe to head/tail
1526
1606
  rly result <id> result/status now (includes live autosaved draft + presence while open)
1607
+ rly share <id> list active same-Wi-Fi share links for a running board
1608
+ rly share <id> --role review activate a reviewer link (comments only)
1609
+ rly share <id> --role collab activate a collaborator link (edit/comment/submit)
1610
+ rly share <id> --role review --revoke
1611
+ revoke one share role; use --revoke --all to disable all roles
1527
1612
  rly list [--json] running boards
1528
1613
  rly open [id] re-open the browser tab of a running board
1529
1614
  rly reopen <id> [--replies f.json] serve a saved board again, prefilled with saved answers
@@ -1607,6 +1692,8 @@ export async function main(argv) {
1607
1692
  return await cmdWait(parseArgs(rest));
1608
1693
  case 'result':
1609
1694
  return await cmdResult(parseArgs(rest));
1695
+ case 'share':
1696
+ return await cmdShare(parseArgs(rest));
1610
1697
  case 'list':
1611
1698
  return cmdList(parseArgs(rest));
1612
1699
  case 'open':
package/src/mcp.js CHANGED
@@ -1,8 +1,9 @@
1
1
  // mcp.js — relay as an MCP App (SEP-1865, extension "io.modelcontextprotocol/ui").
2
2
  //
3
- // `rly mcp` starts a zero-dependency MCP server over stdio. Register it with a
4
- // host that supports MCP Apps (Claude desktop/mobile, Codex, …) and relay's
5
- // boards render INLINE in the conversation instead of opening a browser tab:
3
+ // `rly mcp` starts an MCP server over stdio with no npm runtime dependencies.
4
+ // Register it with a host that supports MCP Apps (Claude desktop/mobile, Codex,
5
+ // …) and relay's boards render INLINE in the conversation instead of opening a
6
+ // browser tab:
6
7
  //
7
8
  // • the server declares a UI resource ui://relay/board (text/html;profile=mcp-app)
8
9
  // • the tools `relay_ask` / `relay_show` link to it via _meta.ui.resourceUri