@khanglvm/relay 0.7.0 → 0.8.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/package.json +1 -1
- package/src/cli.js +196 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khanglvm/relay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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/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,188 @@ 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 { 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
|
+
];
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Render the relay rules in the style the target file expects.
|
|
884
|
+
function renderInstruction(style) {
|
|
885
|
+
if (style === 'mdc') {
|
|
886
|
+
return `---\ndescription: relay — collect decisions & show rich visuals in the browser, not the terminal\nalwaysApply: true\n---\n\n${SKILL_RULES}\n`;
|
|
887
|
+
}
|
|
888
|
+
return `${SKILL_RULES}\n`; // dedicated file — relay owns it
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// Upsert relay's marked block into a (possibly shared / pre-existing) file,
|
|
892
|
+
// leaving everything outside the markers untouched. Returns 'added'|'updated'.
|
|
893
|
+
function upsertBlock(file, body) {
|
|
894
|
+
let existing = '';
|
|
895
|
+
try { existing = fs.readFileSync(file, 'utf8'); } catch { /* new file */ }
|
|
896
|
+
const wrapped = `${RELAY_BEGIN}\n${body}\n${RELAY_END}`;
|
|
897
|
+
const re = new RegExp(escapeRegExp(RELAY_BEGIN) + '[\\s\\S]*?' + escapeRegExp(RELAY_END));
|
|
898
|
+
const had = re.test(existing);
|
|
899
|
+
const next = had
|
|
900
|
+
? existing.replace(re, wrapped)
|
|
901
|
+
: (existing.trim() ? existing.replace(/\s*$/, '') + '\n\n' + wrapped + '\n' : wrapped + '\n');
|
|
902
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
903
|
+
fs.writeFileSync(file, next);
|
|
904
|
+
return had ? 'updated' : 'added';
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function writeInstruction(target) {
|
|
908
|
+
if (target.style === 'shared') return upsertBlock(target.file, SKILL_RULES);
|
|
909
|
+
const existed = fs.existsSync(target.file);
|
|
910
|
+
fs.mkdirSync(path.dirname(target.file), { recursive: true });
|
|
911
|
+
fs.writeFileSync(target.file, renderInstruction(target.style));
|
|
912
|
+
return existed ? 'updated' : 'added';
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function cmdInstall(args) {
|
|
916
|
+
const reg = agentRegistry();
|
|
917
|
+
const byId = Object.fromEntries(reg.map((a) => [a.id, a]));
|
|
918
|
+
const scope = args.scope === 'project' ? 'project' : args.scope === 'global' ? 'global' : null;
|
|
919
|
+
|
|
920
|
+
// Pick the target for an agent: explicit --scope wins; else prefer global
|
|
921
|
+
// (machine-wide), falling back to project when the agent has no global file.
|
|
922
|
+
const pick = (a) => {
|
|
923
|
+
if (scope === 'project') return a.project ? { scope: 'project', t: a.project } : null;
|
|
924
|
+
if (scope === 'global') return a.global ? { scope: 'global', t: a.global } : null;
|
|
925
|
+
if (a.global) return { scope: 'global', t: a.global };
|
|
926
|
+
if (a.project) return { scope: 'project', t: a.project };
|
|
927
|
+
return null;
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
const wantAll = args.all === true || String(args.target || '').toLowerCase() === 'all';
|
|
931
|
+
|
|
932
|
+
// No target → show the matrix for THIS platform.
|
|
933
|
+
if (!wantAll && (args.list === true || !args.target)) {
|
|
934
|
+
printJson({
|
|
935
|
+
platform: process.platform,
|
|
936
|
+
agents: reg.map((a) => ({
|
|
937
|
+
agent: a.id, label: a.label,
|
|
938
|
+
global: a.global ? a.global.file : null,
|
|
939
|
+
project: a.project ? a.project.file : null,
|
|
940
|
+
note: a.note,
|
|
941
|
+
})),
|
|
942
|
+
usage: 'rly install --target <agent>[,<agent>] [--scope global|project] [--print] | rly install --all',
|
|
943
|
+
});
|
|
944
|
+
return 0;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// Resolve the set of {agent, scope, target} to act on.
|
|
948
|
+
let chosen;
|
|
949
|
+
if (wantAll) {
|
|
950
|
+
chosen = reg
|
|
951
|
+
.filter((a) => { try { return fs.existsSync(a.detect); } catch { return false; } })
|
|
952
|
+
.map((a) => ({ a, ...(pick(a) || {}) }))
|
|
953
|
+
.filter((x) => x.t);
|
|
954
|
+
if (!chosen.length) {
|
|
955
|
+
throw new CliError('no known agents detected on this machine (no ~/.claude, ~/.codex, ~/.cursor, ~/.kiro, …). Use --target <agent>.', 4);
|
|
956
|
+
}
|
|
957
|
+
} else {
|
|
958
|
+
const ids = String(args.target).split(',').map((s) => s.trim()).filter(Boolean);
|
|
959
|
+
chosen = [];
|
|
960
|
+
for (const id of ids) {
|
|
961
|
+
const a = byId[id];
|
|
962
|
+
if (!a) throw new CliError(`unknown agent "${id}". Run \`rly install --list\` to see supported agents.`, 4);
|
|
963
|
+
const p = pick(a);
|
|
964
|
+
if (!p) throw new CliError(`${a.label} has no ${scope || 'installable'} instruction file${a.note ? ` — ${a.note}` : ''}. Try \`--scope project\`.`, 4);
|
|
965
|
+
chosen.push({ a, ...p });
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// --print: emit content + resolved path for manual copy/paste; no writes.
|
|
970
|
+
if (args.print === true) {
|
|
971
|
+
for (const { a, t } of chosen) {
|
|
972
|
+
const content = t.style === 'shared' ? `${RELAY_BEGIN}\n${SKILL_RULES}\n${RELAY_END}` : renderInstruction(t.style);
|
|
973
|
+
process.stdout.write(`# ${a.label}\n# → ${t.file}\n\n${content}\n\n`);
|
|
974
|
+
}
|
|
975
|
+
return 0;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const installed = chosen.map(({ a, scope: sc, t }) => ({
|
|
979
|
+
agent: a.id, scope: sc, file: t.file, action: writeInstruction(t),
|
|
980
|
+
}));
|
|
981
|
+
printJson({ installed, note: 'reload/re-open your agent (or re-list its rules) if it does not pick this up immediately' });
|
|
982
|
+
return 0;
|
|
983
|
+
}
|
|
984
|
+
|
|
798
985
|
function cmdAgent() {
|
|
799
986
|
console.log(fs.readFileSync(path.join(PKG_ROOT, 'docs', 'AGENT.md'), 'utf8'));
|
|
800
987
|
return 0;
|
|
@@ -863,7 +1050,9 @@ async function cmdUpgrade(args) {
|
|
|
863
1050
|
|
|
864
1051
|
if (wantCli) {
|
|
865
1052
|
process.stderr.write(`\nUpgrading ${PKG_NAME} → latest (npm install -g ${PKG_NAME}@latest)\n`);
|
|
866
|
-
|
|
1053
|
+
// shell:true so Windows resolves `npm` → `npm.cmd` (same reason the skill /
|
|
1054
|
+
// version spawns below use it); the package name has no shell metacharacters.
|
|
1055
|
+
const r = spawnSync('npm', ['install', '-g', `${PKG_NAME}@latest`], { stdio: 'inherit', shell: true });
|
|
867
1056
|
if (r.error || r.status !== 0) {
|
|
868
1057
|
throw new CliError(
|
|
869
1058
|
`npm install failed${r.error ? ` (${r.error.message})` : ` (exit ${r.status})`}. ` +
|
|
@@ -954,6 +1143,9 @@ USAGE
|
|
|
954
1143
|
rly agent FULL GUIDE for AI agents (spec format, blocks, sizing, patterns)
|
|
955
1144
|
rly skill [install|rules|path] bundled universal agent skill (Claude Code, Codex, …)
|
|
956
1145
|
\`rly skill rules >> CLAUDE.md\` adds always-read usage rules
|
|
1146
|
+
rly install --target <agent> inject relay's rules into an agent's instruction file
|
|
1147
|
+
agents: claude codex cursor copilot kiro windsurf cline gemini agents
|
|
1148
|
+
--scope global|project · --print (copy/paste) · --all · --list (no flags)
|
|
957
1149
|
rly upgrade install the latest CLI globally + refresh the skill in one step
|
|
958
1150
|
--stop/--force handle running boards · --dry-run · --cli-only/--skill-only
|
|
959
1151
|
|
|
@@ -1018,6 +1210,8 @@ export async function main(argv) {
|
|
|
1018
1210
|
return cmdRm(parseArgs(rest));
|
|
1019
1211
|
case 'skill':
|
|
1020
1212
|
return cmdSkill(rest);
|
|
1213
|
+
case 'install':
|
|
1214
|
+
return cmdInstall(parseArgs(rest));
|
|
1021
1215
|
case 'upgrade':
|
|
1022
1216
|
case 'self-update':
|
|
1023
1217
|
return await cmdUpgrade(parseArgs(rest));
|