@khanglvm/relay 0.7.0 → 0.8.1

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.
Files changed (3) hide show
  1. package/README.md +48 -24
  2. package/package.json +1 -1
  3. package/src/cli.js +204 -2
package/README.md CHANGED
@@ -25,42 +25,42 @@ npm i -g @khanglvm/relay
25
25
  # the agent skill → every detected agent (Claude Code, Codex, Cursor, …)
26
26
  npx skills add khanglvm/relay --skill relay --all
27
27
 
28
- # enforce it (recommended) — a skill alone is an ignorable hint; append relay's
29
- # short usage rules to your agent's global instructions (per-agent commands below)
30
- rly skill rules
28
+ # enforce it (recommended) — a skill alone is an ignorable hint. `rly install`
29
+ # writes relay's short usage rules into the right file for each agent (details below):
30
+ rly install --all # every agent detected on this machine
31
31
  ```
32
32
 
33
33
  That's it. Next time your agent needs a decision or wants to show you a plan,
34
34
  it opens a board like the ones above and waits for your Submit.
35
35
 
36
+ Keep relay current with **`rly upgrade`** — it installs the latest CLI and
37
+ refreshes the skill in one step, leaving any boards you have open untouched.
38
+
36
39
  ### Tell your agent to actually use it
37
40
 
38
- `rly skill rules` prints relay's short usage rules. A skill is just an ignorable
39
- hint — these rules in the file your agent *always* reads are what make it reach for
40
- relay. Append them to your agent's **global** instructions:
41
+ A skill is just an ignorable hint — relay's short usage rules in the file your
42
+ agent *always* reads are what make it reach for relay. **`rly install` writes
43
+ those rules into the right file for each agent**, cross-platform (macOS / Linux /
44
+ Windows):
41
45
 
42
46
  ```sh
43
- rly skill rules >> ~/.claude/CLAUDE.md # Claude Code
44
- rly skill rules >> ~/.codex/AGENTS.md # Codex
45
- rly skill rules >> ~/.gemini/GEMINI.md # Gemini CLI
46
- rly skill rules >> ~/.config/opencode/AGENTS.md # OpenCode
47
+ rly install --all # every agent detected on this machine
48
+ rly install --target claude # ~/.claude/CLAUDE.md
49
+ rly install --target cursor # → .cursor/rules/relay.mdc (with frontmatter)
50
+ rly install --target copilot # → .github/copilot-instructions.md (+ JetBrains global)
51
+ rly install # no target: print the agent → file map for your OS
47
52
  ```
48
53
 
49
- Cursor and GitHub Copilot keep global rules in a settings panel, not a file — run
50
- `rly skill rules` and paste the output into Cursor's *Settings → Rules → User Rules*
51
- or Copilot's custom-instructions.
52
-
53
- <details>
54
- <summary>More agents</summary>
55
-
56
- ```sh
57
- rly skill rules >> ~/.codeium/windsurf/memories/global_rules.md # Windsurf
58
- rly skill rules >> ~/.factory/AGENTS.md # Droid (Factory)
59
- ```
54
+ Supported targets: `claude`, `codex`, `cursor`, `copilot` (VS Code / Visual Studio /
55
+ JetBrains), `kiro`, `windsurf`, `cline`, `gemini`, `opencode`, `droid` (Factory), and
56
+ the generic `agents` (`AGENTS.md`). Flags: `--scope global|project` (where the rules
57
+ land), `--print` (emit the text + resolved path for manual copy/paste), `--list`.
58
+ relay's block is marker-delimited and idempotent — re-running updates only that block
59
+ and leaves the rest of your file alone.
60
60
 
61
- Any other agent: run `rly skill rules` and paste the block into whatever file or
62
- settings panel it reads as global instructions.
63
- </details>
61
+ Using an agent that isn't listed? `rly skill rules` prints the block to paste into
62
+ whatever file or settings panel it reads e.g. Cursor's *Settings → Rules → User
63
+ Rules* or Copilot's custom instructions (their *user-level* rules aren't file-based).
64
64
 
65
65
  ## What it improves
66
66
 
@@ -82,6 +82,8 @@ Node ≥ 18; Chart.js / Mermaid / Graphviz are vendored and lazy-loaded offline.
82
82
  | | |
83
83
  |---|---|
84
84
  | `rly help` | every command at a glance |
85
+ | `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` |
86
+ | `rly upgrade` | update the CLI **and** refresh the skill in one step (safe around open boards; `--dry-run`, `--cli-only`, `--skill-only`) |
85
87
  | `rly agent` | the full agent guide — spec format, all block types, annotations, patterns ([docs/AGENT.md](docs/AGENT.md)) |
86
88
  | `rly schema` | board spec JSON Schema |
87
89
  | [skills/relay/SKILL.md](skills/relay/SKILL.md) | the bundled skill |
@@ -94,6 +96,28 @@ npm test # zero-dep smoke tests (spawns real servers, fake-submits)
94
96
 
95
97
  ## Changelog
96
98
 
99
+ ### 0.8.1
100
+ - `rly install` adds **OpenCode** (`~/.config/opencode/AGENTS.md`) and **Droid /
101
+ Factory** (`~/.factory/AGENTS.md`) targets.
102
+ - README documents `rly install` and `rly upgrade`.
103
+
104
+ ### 0.8.0 — install into any agent
105
+ - **`rly install --target <agent>`** writes relay's usage rules into the right
106
+ file for Claude Code, Codex, Cursor, GitHub Copilot (VS Code / Visual Studio /
107
+ JetBrains), Kiro, Windsurf, Cline, Gemini, or the generic `AGENTS.md` —
108
+ cross-platform (macOS / Linux / Windows), idempotent, with `--all`, `--scope`,
109
+ and `--print`.
110
+ - Fixed `rly skill install` crashing when the target skill dir was a symlink.
111
+
112
+ ### 0.7.0 — sturdier boards, self-update
113
+ - **Markdown blocks render GFM tables**; element comments moved to an
114
+ Outline-style right sidebar with inline highlights on commented text.
115
+ - **Seamless timeouts** — a detached board that runs past its deadline keeps
116
+ serving so you can still submit (it lands as `submitted`); the page shows a
117
+ calm note instead of disconnecting.
118
+ - **`rly upgrade`** — install the latest CLI and refresh the skill in one step.
119
+ - Per-question notes are multi-line textboxes.
120
+
97
121
  ### 0.6.0 — comment on anything
98
122
  - **Comment on any part of a custom-HTML mockup.** Hover any element — a heading,
99
123
  a button, a card, the price — and a pin appears to leave an inline note. No
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khanglvm/relay",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
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/src/cli.js CHANGED
@@ -30,7 +30,7 @@ const PKG_NAME = PKG_JSON.name; // e.g. "@khanglvm/relay" — the global package
30
30
  const VALUED_FLAGS = new Set([
31
31
  'file', 'html', 'html-file', 'title', 'intro', 'timeout', 'port',
32
32
  'submit-label', 'height', 'limit', 'target', 'id', 'replies',
33
- 'on-result', 'notify-cmd', 'idle-grace',
33
+ 'on-result', 'notify-cmd', 'idle-grace', 'scope',
34
34
  ]);
35
35
 
36
36
  function camel(key) {
@@ -771,6 +771,11 @@ function cmdSkill(rest) {
771
771
  const installed = [];
772
772
  for (const t of targets) {
773
773
  fs.mkdirSync(path.dirname(t), { recursive: true });
774
+ // Clear whatever is already there first. cpSync refuses to overwrite a
775
+ // non-directory (a symlink or file at the target — e.g. a skill dir the
776
+ // user symlinked elsewhere) with a directory, so a plain re-install would
777
+ // crash. rmSync on a symlink removes the link itself, not its target.
778
+ fs.rmSync(t, { recursive: true, force: true });
774
779
  fs.cpSync(SKILL_SRC, t, { recursive: true });
775
780
  fs.writeFileSync(path.join(t, '.rly-version'), VERSION);
776
781
  installed.push(t);
@@ -795,6 +800,196 @@ Full guide: \`rly agent\`.`);
795
800
  return 0;
796
801
  }
797
802
 
803
+ // ===========================================================================
804
+ // `rly install` — inject relay's always-read rules into ANY agent's
805
+ // instruction file, cross-platform. `rly skill install` (above) handles the
806
+ // full SKILL.md for skill-aware agents; this covers the long tail (Cursor,
807
+ // Copilot, Kiro, Windsurf, Cline, Gemini, generic AGENTS.md, …) that read a
808
+ // rules/instructions/steering/context markdown file instead.
809
+ // ===========================================================================
810
+
811
+ const RELAY_BEGIN = '<!-- relay:begin (managed by `rly install` — your edits outside these markers are kept) -->';
812
+ const RELAY_END = '<!-- relay:end -->';
813
+ const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
814
+
815
+ // Resolve OS-specific base dirs from an injectable env/home/platform so the
816
+ // path logic is unit-testable for win32 / linux / darwin without running there.
817
+ function platformDirs({ platform, home, env }) {
818
+ const xdg = env.XDG_CONFIG_HOME || path.join(home, '.config');
819
+ const localApp = env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
820
+ // Copilot-in-JetBrains global instructions dir.
821
+ const jetbrainsCopilot = platform === 'win32'
822
+ ? path.join(localApp, 'github-copilot', 'intellij')
823
+ : path.join(xdg, 'github-copilot', 'intellij');
824
+ return { xdg, jetbrainsCopilot, documents: path.join(home, 'Documents') };
825
+ }
826
+
827
+ // The relay instruction registry. Each agent declares how to install relay's
828
+ // rules at `global` (user, machine-wide) and/or `project` (cwd) scope, in what
829
+ // `style`, and a `detect` dir whose existence means "you use this agent"
830
+ // (drives `--all`). Styles:
831
+ // 'shared' — upsert a marked block into a possibly-shared file (CLAUDE.md,
832
+ // AGENTS.md, copilot-instructions.md, …); your other content is
833
+ // preserved, only relay's block is rewritten.
834
+ // 'dedicated' — relay owns the whole file (.kiro/steering, .windsurf/rules,
835
+ // .clinerules, …); safe to overwrite.
836
+ // 'mdc' — dedicated, with Cursor `.mdc` YAML frontmatter.
837
+ export function agentRegistry({ platform = process.platform, home = os.homedir(), cwd = process.cwd(), env = process.env } = {}) {
838
+ const d = platformDirs({ platform, home, env });
839
+ const j = path.join;
840
+ return [
841
+ { id: 'claude', label: 'Claude Code',
842
+ detect: j(home, '.claude'),
843
+ global: { file: j(home, '.claude', 'CLAUDE.md'), style: 'shared' },
844
+ project: { file: j(cwd, 'CLAUDE.md'), style: 'shared' },
845
+ note: 'full skill: `rly skill install`' },
846
+ { id: 'codex', label: 'OpenAI Codex',
847
+ detect: j(home, '.codex'),
848
+ global: { file: j(home, '.codex', 'AGENTS.md'), style: 'shared' },
849
+ project: { file: j(cwd, 'AGENTS.md'), style: 'shared' } },
850
+ { id: 'agents', label: 'AGENTS.md standard (Amp, Jules, Cline, …)',
851
+ detect: j(home, '.agents'),
852
+ global: { file: j(home, '.agents', 'AGENTS.md'), style: 'shared' },
853
+ project: { file: j(cwd, 'AGENTS.md'), style: 'shared' } },
854
+ { id: 'cursor', label: 'Cursor',
855
+ detect: j(home, '.cursor'),
856
+ global: null, // user rules are set in Cursor Settings UI (not file-based)
857
+ project: { file: j(cwd, '.cursor', 'rules', 'relay.mdc'), style: 'mdc' },
858
+ note: 'global "User Rules" are set in Settings UI, not a file' },
859
+ { id: 'copilot', label: 'GitHub Copilot (VS Code / Visual Studio / JetBrains)',
860
+ detect: path.dirname(d.jetbrainsCopilot), // …/github-copilot
861
+ global: { file: j(d.jetbrainsCopilot, 'global-copilot-instructions.md'), style: 'shared' },
862
+ project: { file: j(cwd, '.github', 'copilot-instructions.md'), style: 'shared' },
863
+ note: 'global file applies in JetBrains IDEs; project file applies everywhere' },
864
+ { id: 'kiro', label: 'Kiro',
865
+ detect: j(home, '.kiro'),
866
+ global: { file: j(home, '.kiro', 'steering', 'relay.md'), style: 'dedicated' },
867
+ project: { file: j(cwd, '.kiro', 'steering', 'relay.md'), style: 'dedicated' } },
868
+ { id: 'windsurf', label: 'Windsurf',
869
+ detect: j(home, '.codeium'),
870
+ global: { file: j(home, '.codeium', 'windsurf', 'memories', 'global_rules.md'), style: 'shared' },
871
+ project: { file: j(cwd, '.windsurf', 'rules', 'relay.md'), style: 'dedicated' } },
872
+ { id: 'cline', label: 'Cline',
873
+ detect: j(d.documents, 'Cline'),
874
+ global: { file: j(d.documents, 'Cline', 'Rules', 'relay.md'), style: 'dedicated' },
875
+ project: { file: j(cwd, '.clinerules', 'relay.md'), style: 'dedicated' } },
876
+ { id: 'gemini', label: 'Gemini CLI',
877
+ detect: j(home, '.gemini'),
878
+ global: { file: j(home, '.gemini', 'GEMINI.md'), style: 'shared' },
879
+ project: { file: j(cwd, 'GEMINI.md'), style: 'shared' } },
880
+ { id: 'opencode', label: 'OpenCode',
881
+ detect: j(d.xdg, 'opencode'),
882
+ global: { file: j(d.xdg, 'opencode', 'AGENTS.md'), style: 'shared' },
883
+ project: { file: j(cwd, 'AGENTS.md'), style: 'shared' } },
884
+ { id: 'droid', label: 'Droid (Factory)',
885
+ detect: j(home, '.factory'),
886
+ global: { file: j(home, '.factory', 'AGENTS.md'), style: 'shared' },
887
+ project: { file: j(cwd, 'AGENTS.md'), style: 'shared' } },
888
+ ];
889
+ }
890
+
891
+ // Render the relay rules in the style the target file expects.
892
+ function renderInstruction(style) {
893
+ if (style === 'mdc') {
894
+ return `---\ndescription: relay — collect decisions & show rich visuals in the browser, not the terminal\nalwaysApply: true\n---\n\n${SKILL_RULES}\n`;
895
+ }
896
+ return `${SKILL_RULES}\n`; // dedicated file — relay owns it
897
+ }
898
+
899
+ // Upsert relay's marked block into a (possibly shared / pre-existing) file,
900
+ // leaving everything outside the markers untouched. Returns 'added'|'updated'.
901
+ function upsertBlock(file, body) {
902
+ let existing = '';
903
+ try { existing = fs.readFileSync(file, 'utf8'); } catch { /* new file */ }
904
+ const wrapped = `${RELAY_BEGIN}\n${body}\n${RELAY_END}`;
905
+ const re = new RegExp(escapeRegExp(RELAY_BEGIN) + '[\\s\\S]*?' + escapeRegExp(RELAY_END));
906
+ const had = re.test(existing);
907
+ const next = had
908
+ ? existing.replace(re, wrapped)
909
+ : (existing.trim() ? existing.replace(/\s*$/, '') + '\n\n' + wrapped + '\n' : wrapped + '\n');
910
+ fs.mkdirSync(path.dirname(file), { recursive: true });
911
+ fs.writeFileSync(file, next);
912
+ return had ? 'updated' : 'added';
913
+ }
914
+
915
+ function writeInstruction(target) {
916
+ if (target.style === 'shared') return upsertBlock(target.file, SKILL_RULES);
917
+ const existed = fs.existsSync(target.file);
918
+ fs.mkdirSync(path.dirname(target.file), { recursive: true });
919
+ fs.writeFileSync(target.file, renderInstruction(target.style));
920
+ return existed ? 'updated' : 'added';
921
+ }
922
+
923
+ function cmdInstall(args) {
924
+ const reg = agentRegistry();
925
+ const byId = Object.fromEntries(reg.map((a) => [a.id, a]));
926
+ const scope = args.scope === 'project' ? 'project' : args.scope === 'global' ? 'global' : null;
927
+
928
+ // Pick the target for an agent: explicit --scope wins; else prefer global
929
+ // (machine-wide), falling back to project when the agent has no global file.
930
+ const pick = (a) => {
931
+ if (scope === 'project') return a.project ? { scope: 'project', t: a.project } : null;
932
+ if (scope === 'global') return a.global ? { scope: 'global', t: a.global } : null;
933
+ if (a.global) return { scope: 'global', t: a.global };
934
+ if (a.project) return { scope: 'project', t: a.project };
935
+ return null;
936
+ };
937
+
938
+ const wantAll = args.all === true || String(args.target || '').toLowerCase() === 'all';
939
+
940
+ // No target → show the matrix for THIS platform.
941
+ if (!wantAll && (args.list === true || !args.target)) {
942
+ printJson({
943
+ platform: process.platform,
944
+ agents: reg.map((a) => ({
945
+ agent: a.id, label: a.label,
946
+ global: a.global ? a.global.file : null,
947
+ project: a.project ? a.project.file : null,
948
+ note: a.note,
949
+ })),
950
+ usage: 'rly install --target <agent>[,<agent>] [--scope global|project] [--print] | rly install --all',
951
+ });
952
+ return 0;
953
+ }
954
+
955
+ // Resolve the set of {agent, scope, target} to act on.
956
+ let chosen;
957
+ if (wantAll) {
958
+ chosen = reg
959
+ .filter((a) => { try { return fs.existsSync(a.detect); } catch { return false; } })
960
+ .map((a) => ({ a, ...(pick(a) || {}) }))
961
+ .filter((x) => x.t);
962
+ if (!chosen.length) {
963
+ throw new CliError('no known agents detected on this machine (no ~/.claude, ~/.codex, ~/.cursor, ~/.kiro, …). Use --target <agent>.', 4);
964
+ }
965
+ } else {
966
+ const ids = String(args.target).split(',').map((s) => s.trim()).filter(Boolean);
967
+ chosen = [];
968
+ for (const id of ids) {
969
+ const a = byId[id];
970
+ if (!a) throw new CliError(`unknown agent "${id}". Run \`rly install --list\` to see supported agents.`, 4);
971
+ const p = pick(a);
972
+ if (!p) throw new CliError(`${a.label} has no ${scope || 'installable'} instruction file${a.note ? ` — ${a.note}` : ''}. Try \`--scope project\`.`, 4);
973
+ chosen.push({ a, ...p });
974
+ }
975
+ }
976
+
977
+ // --print: emit content + resolved path for manual copy/paste; no writes.
978
+ if (args.print === true) {
979
+ for (const { a, t } of chosen) {
980
+ const content = t.style === 'shared' ? `${RELAY_BEGIN}\n${SKILL_RULES}\n${RELAY_END}` : renderInstruction(t.style);
981
+ process.stdout.write(`# ${a.label}\n# → ${t.file}\n\n${content}\n\n`);
982
+ }
983
+ return 0;
984
+ }
985
+
986
+ const installed = chosen.map(({ a, scope: sc, t }) => ({
987
+ agent: a.id, scope: sc, file: t.file, action: writeInstruction(t),
988
+ }));
989
+ printJson({ installed, note: 'reload/re-open your agent (or re-list its rules) if it does not pick this up immediately' });
990
+ return 0;
991
+ }
992
+
798
993
  function cmdAgent() {
799
994
  console.log(fs.readFileSync(path.join(PKG_ROOT, 'docs', 'AGENT.md'), 'utf8'));
800
995
  return 0;
@@ -863,7 +1058,9 @@ async function cmdUpgrade(args) {
863
1058
 
864
1059
  if (wantCli) {
865
1060
  process.stderr.write(`\nUpgrading ${PKG_NAME} → latest (npm install -g ${PKG_NAME}@latest)\n`);
866
- const r = spawnSync('npm', ['install', '-g', `${PKG_NAME}@latest`], { stdio: 'inherit' });
1061
+ // shell:true so Windows resolves `npm` `npm.cmd` (same reason the skill /
1062
+ // version spawns below use it); the package name has no shell metacharacters.
1063
+ const r = spawnSync('npm', ['install', '-g', `${PKG_NAME}@latest`], { stdio: 'inherit', shell: true });
867
1064
  if (r.error || r.status !== 0) {
868
1065
  throw new CliError(
869
1066
  `npm install failed${r.error ? ` (${r.error.message})` : ` (exit ${r.status})`}. ` +
@@ -954,6 +1151,9 @@ USAGE
954
1151
  rly agent FULL GUIDE for AI agents (spec format, blocks, sizing, patterns)
955
1152
  rly skill [install|rules|path] bundled universal agent skill (Claude Code, Codex, …)
956
1153
  \`rly skill rules >> CLAUDE.md\` adds always-read usage rules
1154
+ rly install --target <agent> inject relay's rules into an agent's instruction file
1155
+ claude codex cursor copilot kiro windsurf cline gemini opencode droid agents
1156
+ --scope global|project · --print (copy/paste) · --all · --list (no flags)
957
1157
  rly upgrade install the latest CLI globally + refresh the skill in one step
958
1158
  --stop/--force handle running boards · --dry-run · --cli-only/--skill-only
959
1159
 
@@ -1018,6 +1218,8 @@ export async function main(argv) {
1018
1218
  return cmdRm(parseArgs(rest));
1019
1219
  case 'skill':
1020
1220
  return cmdSkill(rest);
1221
+ case 'install':
1222
+ return cmdInstall(parseArgs(rest));
1021
1223
  case 'upgrade':
1022
1224
  case 'self-update':
1023
1225
  return await cmdUpgrade(parseArgs(rest));