rl-workspaces 0.6.0 → 0.7.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.
Files changed (49) hide show
  1. package/README.md +62 -53
  2. package/bin/rl +2 -54
  3. package/dist/agent-launch.js +1 -1
  4. package/dist/agent-resume.js +2 -2
  5. package/dist/bootstrap.js +74 -0
  6. package/dist/cli.js +2 -0
  7. package/dist/command-context.js +1 -1
  8. package/dist/common.js +3 -2
  9. package/dist/dependencies.js +197 -0
  10. package/dist/doctor.js +1 -1
  11. package/dist/help.js +81 -84
  12. package/dist/hooks.js +1 -1
  13. package/dist/install.js +143 -0
  14. package/dist/mcp-setup.js +171 -0
  15. package/dist/mcp.js +13 -16
  16. package/dist/picker.js +24 -7
  17. package/dist/pr-workflow.js +7 -28
  18. package/dist/repos.js +5 -1
  19. package/dist/session-workflows.js +71 -1
  20. package/dist/session.js +1 -1
  21. package/dist/state.js +4 -14
  22. package/dist/storage.js +2 -2
  23. package/dist/store.js +2 -2
  24. package/dist/summary.js +1 -1
  25. package/dist/workflow.js +24 -38
  26. package/lib/entry.zsh +54 -0
  27. package/lib/rl.zsh +35 -6
  28. package/node_modules/proper-lockfile/CHANGELOG.md +108 -0
  29. package/node_modules/smol-toml/LICENSE +24 -0
  30. package/node_modules/smol-toml/README.md +605 -0
  31. package/node_modules/smol-toml/dist/date.d.ts +41 -0
  32. package/node_modules/smol-toml/dist/date.js +156 -0
  33. package/node_modules/smol-toml/dist/error.d.ts +37 -0
  34. package/node_modules/smol-toml/dist/error.js +67 -0
  35. package/node_modules/smol-toml/dist/extract.js +293 -0
  36. package/node_modules/smol-toml/dist/index.cjs +771 -0
  37. package/node_modules/smol-toml/dist/index.d.ts +44 -0
  38. package/node_modules/smol-toml/dist/index.js +34 -0
  39. package/node_modules/smol-toml/dist/parse.d.ts +73 -0
  40. package/node_modules/smol-toml/dist/parse.js +164 -0
  41. package/node_modules/smol-toml/dist/primitive.js +151 -0
  42. package/node_modules/smol-toml/dist/stringify.d.ts +32 -0
  43. package/node_modules/smol-toml/dist/stringify.js +219 -0
  44. package/node_modules/smol-toml/dist/struct.js +156 -0
  45. package/node_modules/smol-toml/dist/util.d.ts +43 -0
  46. package/node_modules/smol-toml/dist/util.js +57 -0
  47. package/node_modules/smol-toml/package.json +68 -0
  48. package/package.json +6 -3
  49. package/shell/rl.zsh +1 -1
package/dist/help.js CHANGED
@@ -21,7 +21,7 @@ const topics = {
21
21
  options: [
22
22
  "-c, --create Create a new branch workspace",
23
23
  "-b, --base <ref> Creation base (default: origin/test unless configured)",
24
- "--no-fetch Use cached refs when creating",
24
+ "--no-fetch Use cached refs when opening or creating",
25
25
  "--setup Run configured project setup",
26
26
  "--agent codex|claude Start an agent after switching",
27
27
  "-- <agent args> Forward options to the selected agent",
@@ -30,13 +30,13 @@ const topics = {
30
30
  "Use rl checkout [repository] to switch repositories in your shell.",
31
31
  "@ opens the main checkout; - returns to the previous workspace.",
32
32
  "Shell integration changes your terminal's directory.",
33
- "Related creation helpers: new reopens existing workspaces; open requires an existing branch.",
33
+ "An existing branch without a checkout gets a managed workspace; origin branches are tracked.",
34
34
  ],
35
35
  examples: [
36
36
  "rl switch feature/login",
37
37
  "rl switch feature/login -c --base origin/test",
38
38
  ],
39
- related: ["new", "open", "path"],
39
+ related: ["path"],
40
40
  },
41
41
  list: {
42
42
  group: "Workspaces",
@@ -64,9 +64,9 @@ const topics = {
64
64
  notes: [
65
65
  "Shows repository selection, checkout path, base, workspaces and saved session counts.",
66
66
  "--workspace includes Git changes, PR, sessions, task handoff and latest workflow.",
67
- "Use rl list for workspace overviews; status --list and --all remain supported.",
67
+ "Use rl list for workspace overviews and --include-deleted for archived records.",
68
68
  ],
69
- related: ["list", "instances"],
69
+ related: ["list"],
70
70
  },
71
71
  delete: {
72
72
  group: "Workspaces",
@@ -122,6 +122,9 @@ const topics = {
122
122
  description: "Configure native agent roles and saved sessions",
123
123
  usage: "rl agent <command>",
124
124
  related: ["resume", "workflow", "policy"],
125
+ notes: [
126
+ "Existing agent sessions discover their workflow with rl workflow context --json. A special launcher or MCP connection is not required.",
127
+ ],
125
128
  },
126
129
  "agent start": {
127
130
  description: "Start an interactive agent",
@@ -180,8 +183,11 @@ const topics = {
180
183
  },
181
184
  "agent mcp": {
182
185
  description: "Serve workspace context and notes through MCP",
183
- usage: "rl agent mcp",
184
- notes: ["Uses stdio. Start the server in the adopted workspace."],
186
+ usage: "rl agent mcp [--workspace <absolute-path>]",
187
+ notes: [
188
+ "Uses stdio. Start the server in the adopted workspace, or bind one explicitly with --workspace.",
189
+ ],
190
+ related: ["setup mcp"],
185
191
  },
186
192
  workflow: {
187
193
  group: "Agents and tasks",
@@ -239,6 +245,9 @@ const topics = {
239
245
  "workflow context": {
240
246
  description: "Discover profiles, policies, workflows and trigger rules",
241
247
  usage: "rl workflow context [--json]",
248
+ notes: [
249
+ "For an existing agent session: --json includes profiles, policies, workflows, and protocol.tools with every CLI command and input schema. Use your normal shell tool; MCP and rl agent start are optional.",
250
+ ],
242
251
  },
243
252
  "workflow pending": {
244
253
  description: "Read queued workflows for the parent session",
@@ -397,10 +406,6 @@ const topics = {
397
406
  description: "Refresh the saved PR snapshot",
398
407
  usage: "rl pr sync [number|URL] [--json]",
399
408
  },
400
- "pr checks": {
401
- description: "Show GitHub check results",
402
- usage: "rl pr checks [--json]",
403
- },
404
409
  "pr checkout": {
405
410
  description: "Create a workspace for a PR",
406
411
  usage: "rl pr checkout <number> [--name <branch>]",
@@ -513,6 +518,34 @@ const topics = {
513
518
  usage: "rl setup adopt [--extended-hooks]",
514
519
  notes: ["Preserves existing hooks. Review Codex hook trust with /hooks."],
515
520
  },
521
+ "setup mcp": {
522
+ description: "Connect this workspace's RL MCP server to your agent",
523
+ usage: "rl setup mcp [codex|claude] [--dry-run] [--json]",
524
+ notes: [
525
+ "Configures both clients by default: .codex/config.toml and .mcp.json in this adopted workspace.",
526
+ "Preserves other settings and servers. Existing unmanaged entries named rl require manual resolution.",
527
+ "Launches the installed Node/RL directly, with this workspace and its RL state bound explicitly. Rerun after moving the installation.",
528
+ "Reload your client and review its normal project trust/MCP approval. Your existing session creates native subagents.",
529
+ ],
530
+ },
531
+ "setup dependencies": {
532
+ description: "Install or check Git, zsh, fzf and GitHub CLI",
533
+ usage: "rl setup dependencies [--check | --dry-run] [--json]",
534
+ notes: [
535
+ "npm installation installs missing tools through an existing package manager. This command repairs skipped or failed dependency setup.",
536
+ "Uses Homebrew on macOS, and Homebrew, APT, DNF, Yum, Pacman or APK on Linux. Administrator access may be required on Linux.",
537
+ "Runs directly with Node, so it also works before zsh or a repository is available.",
538
+ ],
539
+ },
540
+ "setup install": {
541
+ description: "Complete or repair RL installation and shell integration",
542
+ usage: "rl setup install [--shell zsh|bash]",
543
+ notes: [
544
+ "Global npm installation runs this automatically: missing Git, zsh, fzf and gh are installed, then your shell startup file is updated.",
545
+ "Preserves your settings and updates one RL-owned block. New terminals load RL automatically; reload the installing terminal once.",
546
+ "Use this command after an install with lifecycle scripts disabled, or to repair setup.",
547
+ ],
548
+ },
516
549
  "setup shell": {
517
550
  description: "Print shell integration",
518
551
  usage: "rl setup shell <zsh|bash>",
@@ -527,16 +560,6 @@ const topics = {
527
560
  "--repair reinstalls this workspace's RL hooks.",
528
561
  ],
529
562
  },
530
- new: {
531
- description: "Create or reopen a branch workspace",
532
- usage: "rl new <branch> [--base <ref>] [--no-fetch] [--setup]",
533
- related: ["switch"],
534
- },
535
- open: {
536
- description: "Open an existing local or origin branch",
537
- usage: "rl open --branch <branch> [--setup] [--no-fetch]",
538
- related: ["switch"],
539
- },
540
563
  path: {
541
564
  description: "Print a workspace path",
542
565
  usage: "rl path [name|branch|@]",
@@ -547,52 +570,7 @@ const topics = {
547
570
  usage: "rl exec [--workspace <name>] -- <command> [args...]",
548
571
  related: ["setup run"],
549
572
  },
550
- instances: {
551
- description: "Inspect active and archived instance records",
552
- usage: "rl instances",
553
- related: ["status", "list"],
554
- },
555
573
  };
556
- const aliases = [
557
- [["merge"], ["git", "merge"]],
558
- [["clean"], ["storage", "clean"]],
559
- [["summary"], ["notes", "summary"]],
560
- [["handoff"], ["agent", "handoff"]],
561
- [["mcp"], ["agent", "mcp"]],
562
- [
563
- ["session", "list"],
564
- ["agent", "sessions"],
565
- ],
566
- [
567
- ["session", "save"],
568
- ["agent", "save"],
569
- ],
570
- [
571
- ["session", "add"],
572
- ["agent", "save"],
573
- ],
574
- [["session"], ["agent"]],
575
- [
576
- ["config", "init"],
577
- ["setup", "init"],
578
- ],
579
- [
580
- ["config", "show"],
581
- ["setup", "show"],
582
- ],
583
- [["config"], ["setup"]],
584
- [["adopt"], ["setup", "adopt"]],
585
- [["init"], ["setup", "shell"]],
586
- ];
587
- function canonical(words) {
588
- if (words[0] === "status" &&
589
- words.some((word) => word === "--all" || word === "--list"))
590
- return ["list", ...words.slice(1)];
591
- if (["context", "progress"].includes(words[0]))
592
- return ["notes", words[1] ?? "show", words[0], ...words.slice(2)];
593
- const alias = aliases.find(([from]) => from.every((word, i) => words[i] === word));
594
- return alias ? [...alias[1], ...words.slice(alias[0].length)] : words;
595
- }
596
574
  const children = (name) => Object.entries(topics).filter(([key]) => key.startsWith(name + " ") &&
597
575
  key.split(" ").length === name.split(" ").length + 1);
598
576
  const safe = (value) => stripVTControlCharacters(value).replace(/[\x00-\x1f\x7f-\x9f]/g, " ");
@@ -625,9 +603,8 @@ function match(words) {
625
603
  export function topicHelp(words) {
626
604
  if (!words.length)
627
605
  return mainHelp();
628
- const normalized = canonical(words), name = match(normalized);
629
- if (!name ||
630
- (children(name).length && normalized.length > name.split(" ").length))
606
+ const name = match(words);
607
+ if (!name || (children(name).length && words.length > name.split(" ").length))
631
608
  throw new Error(`Unknown help topic: ${safe(words.join(" "))}`);
632
609
  const topic = topics[name];
633
610
  const lines = [topic.description, "", `Usage: ${topic.usage}`];
@@ -666,28 +643,48 @@ export function invocation(module, argv) {
666
643
  while (argv[index]?.startsWith("--") && CONTEXT.has(argv[index].slice(2)))
667
644
  index += 2;
668
645
  const rest = argv.slice(index);
669
- const prefix = {
670
- delete: ["delete"],
671
- summary: ["summary"],
672
- mcp: ["mcp"],
673
- picker: [],
674
- };
675
- const words = Object.hasOwn(prefix, module)
676
- ? [...prefix[module], ...rest]
677
- : module === "agent" && rest[0] !== "handoff"
678
- ? ["agent", ...rest]
679
- : rest;
646
+ if (module === "summary")
647
+ return ["notes", "summary", ...rest];
648
+ if (module === "mcp")
649
+ return ["agent", "mcp", ...rest];
650
+ if (module === "mcp-setup")
651
+ return ["setup", "mcp", ...rest];
652
+ if (module === "delete")
653
+ return ["delete", ...rest];
654
+ if (module === "picker")
655
+ return [];
656
+ if (module === "agent")
657
+ return ["agent", ...rest];
658
+ if (module === "git")
659
+ return ["git", ...rest];
660
+ if (module === "storage")
661
+ return rest[0] === "clean" ? ["storage", ...rest] : rest;
662
+ if (module === "workflow" && rest[0] === "config")
663
+ return ["setup", ...rest.slice(1)];
664
+ if (module === "state") {
665
+ if (rest[0] === "session")
666
+ return [
667
+ "agent",
668
+ rest[1] === "list" ? "sessions" : "save",
669
+ ...rest.slice(2),
670
+ ];
671
+ if (rest[0] === "adopt")
672
+ return ["setup", ...rest];
673
+ if (["context", "progress"].includes(rest[0]))
674
+ return ["notes", rest[1] ?? "show", rest[0], ...rest.slice(2)];
675
+ }
676
+ if (module === "repos" &&
677
+ rest[0] === "status" &&
678
+ rest.some((word) => word === "--all" || word === "--list"))
679
+ return ["list", ...rest.slice(1)];
680
680
  const defaults = {
681
681
  state: "status",
682
682
  repos: "repo",
683
683
  workflow: "switch",
684
- git: "git",
685
- agent: "agent",
686
684
  automation: "workflow",
687
- storage: "storage",
688
685
  "pr-update": "pr",
689
686
  };
690
- return canonical(words.length ? words : defaults[module] ? [defaults[module]] : []);
687
+ return rest.length ? rest : defaults[module] ? [defaults[module]] : [];
691
688
  }
692
689
  export function usageHint(module, argv, details = false) {
693
690
  const words = invocation(module, argv), name = match(words);
package/dist/hooks.js CHANGED
@@ -21,7 +21,7 @@ export async function handleHook(store, key, name) {
21
21
  // cannot change the original instance or the destination instance.
22
22
  const [file, data] = store.instanceForWorktree(root, git(root, "branch", "--show-current"));
23
23
  if (data.key !== key)
24
- throw new Error("Hook belongs to a different RL instance; run 'rl adopt' to repair it");
24
+ throw new Error("Hook belongs to a different RL instance; run 'rl setup adopt' to repair it");
25
25
  try {
26
26
  const event = adapter.parse(payload);
27
27
  // Workflow workers are ephemeral. Supply task context without turning a
@@ -0,0 +1,143 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { parseArgs } from "node:util";
6
+ import { atomicWrite, canonical, inside, shellQuote } from "./common.js";
7
+ import { inspectDependencies, setupDependencies } from "./dependencies.js";
8
+ const BEGIN = "# >>> rl shell integration >>>";
9
+ const END = "# <<< rl shell integration <<<";
10
+ const SHIM = "#!/bin/sh\n# Managed by RL installation\n";
11
+ function profileChange(home, requested, after) {
12
+ const file = canonical(requested);
13
+ if (!inside(file, home))
14
+ throw new Error(`Shell setup path must stay in your home directory: ${requested}`);
15
+ const stat = fs.statSync(file, { throwIfNoEntry: false });
16
+ if (stat && (!stat.isFile() || stat.size > 1024 * 1024))
17
+ throw new Error(`Expected a shell setup file up to 1 MiB: ${requested}`);
18
+ const before = stat ? fs.readFileSync(file, "utf8") : undefined;
19
+ return {
20
+ file,
21
+ before,
22
+ after: after(before ?? ""),
23
+ ...(stat ? { mode: stat.mode & 0o777 } : {}),
24
+ };
25
+ }
26
+ function integration(text, block, shell) {
27
+ const lines = text.split(/\r?\n/);
28
+ const starts = lines.flatMap((line, index) => line === BEGIN ? [index] : []);
29
+ const ends = lines.flatMap((line, index) => (line === END ? [index] : []));
30
+ if (starts.length || ends.length) {
31
+ if (starts.length !== 1 || ends.length !== 1 || starts[0] >= ends[0])
32
+ throw new Error("Malformed RL shell integration block; preserve your settings and repair its markers before reinstalling");
33
+ const prefix = lines.slice(0, starts[0]).join("\n");
34
+ const suffix = lines.slice(ends[0] + 1).join("\n");
35
+ return (prefix ? prefix + "\n" : "") + block + "\n" + suffix;
36
+ }
37
+ const existing = lines.findIndex((line) => [
38
+ `eval "$(rl setup shell ${shell})"`,
39
+ `eval "$(command rl setup shell ${shell})"`,
40
+ `eval "$(rl init ${shell})"`,
41
+ `eval "$(command rl init ${shell})"`,
42
+ ].includes(line.trim()));
43
+ if (existing >= 0) {
44
+ lines[existing] = block;
45
+ return lines.join("\n");
46
+ }
47
+ return (text + (text ? (text.endsWith("\n") ? "\n" : "\n\n") : "") + block + "\n");
48
+ }
49
+ export function configureShell(host, requestedShell) {
50
+ const env = host.env ?? process.env;
51
+ const home = canonical(host.home ?? os.homedir());
52
+ const shell = requestedShell ??
53
+ path.basename(env.SHELL ??
54
+ ((host.platform ?? process.platform) === "darwin" ? "zsh" : "bash"));
55
+ if (shell !== "zsh" && shell !== "bash")
56
+ throw new Error(`Automatic RL shell setup supports zsh and bash. Run 'rl setup install --shell zsh' or '--shell bash' for the shell you use.`);
57
+ const root = host.packageRoot ?? fileURLToPath(new URL("..", import.meta.url));
58
+ const node = host.node ?? process.execPath;
59
+ const executable = path.join(root, "bin/rl");
60
+ const shimDirectory = path.join(home, ".local/share/rl/bin");
61
+ const shim = profileChange(home, path.join(shimDirectory, "rl"), (before) => {
62
+ if (before && !before.startsWith(SHIM))
63
+ throw new Error("The RL installation shim already contains an unmanaged command; it was not replaced");
64
+ return `${SHIM}exec ${shellQuote(node)} ${shellQuote(executable)} "$@"\n`;
65
+ });
66
+ shim.mode = 0o755;
67
+ const toolDirectories = [
68
+ ...new Set([
69
+ path.dirname(node),
70
+ ...inspectDependencies(env).flatMap((tool) => tool.path ? [path.dirname(tool.path)] : []),
71
+ ]),
72
+ ];
73
+ const block = [
74
+ BEGIN,
75
+ `export PATH=${shellQuote(shimDirectory)}:"$PATH":${toolDirectories.map(shellQuote).join(":")}`,
76
+ `eval "$(${shellQuote(node)} ${shellQuote(executable)} setup shell ${shell})"`,
77
+ END,
78
+ ].join("\n");
79
+ const targets = [
80
+ path.join(shell === "zsh" && env.ZDOTDIR ? path.resolve(home, env.ZDOTDIR) : home, shell === "zsh" ? ".zshrc" : ".bashrc"),
81
+ ];
82
+ if (shell === "bash") {
83
+ const login = [".bash_profile", ".bash_login", ".profile"]
84
+ .map((name) => path.join(home, name))
85
+ .find((file) => fs.existsSync(file));
86
+ targets.push(login ?? path.join(home, ".bash_profile"));
87
+ }
88
+ const profiles = targets.map((file) => profileChange(home, file, (text) => integration(text, block, shell)));
89
+ const changes = [
90
+ ...new Map([shim, ...profiles].map((change) => [change.file, change])).values(),
91
+ ];
92
+ for (const change of changes) {
93
+ const current = fs.existsSync(change.file)
94
+ ? fs.readFileSync(change.file, "utf8")
95
+ : undefined;
96
+ if (current !== change.before)
97
+ throw new Error("Shell configuration changed during installation; retry");
98
+ }
99
+ for (const change of changes) {
100
+ if (change.before !== change.after)
101
+ atomicWrite(change.file, change.after);
102
+ if (change.mode !== undefined)
103
+ fs.chmodSync(change.file, change.mode);
104
+ }
105
+ return {
106
+ shell,
107
+ profiles: profiles.map((change) => change.file),
108
+ shim: shim.file,
109
+ };
110
+ }
111
+ export async function installRl(argv, host = {}) {
112
+ const { values, positionals } = parseArgs({
113
+ args: argv,
114
+ options: { shell: { type: "string" } },
115
+ allowPositionals: true,
116
+ });
117
+ if (positionals.length)
118
+ throw new Error("Usage: rl setup install [--shell zsh|bash]");
119
+ const env = host.env ?? process.env;
120
+ if (env.npm_lifecycle_event === "postinstall" &&
121
+ env.npm_config_global !== "true") {
122
+ console.log("RL checkout installed. Run 'rl setup install' to install system tools and register shell integration.");
123
+ return 0;
124
+ }
125
+ const code = await setupDependencies([], host);
126
+ if (code)
127
+ return code;
128
+ const configured = configureShell(host, values.shell);
129
+ console.log(`RL shell integration installed in ${configured.profiles.join(", ")}.`);
130
+ console.log("New terminals can use rl directly. To activate directory switching in this terminal now, run:");
131
+ console.log(` eval "$(${shellQuote(configured.shim)} setup shell ${configured.shell})"`);
132
+ return 0;
133
+ }
134
+ if (process.argv[1] &&
135
+ path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
136
+ try {
137
+ process.exitCode = await installRl(process.argv.slice(2));
138
+ }
139
+ catch (error) {
140
+ console.error(`RL installation: ${error instanceof Error ? error.message : String(error)}`);
141
+ process.exitCode = 1;
142
+ }
143
+ }
@@ -0,0 +1,171 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { isDeepStrictEqual } from "node:util";
5
+ import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
6
+ import { args, arity, atomicWrite, canonical, inside, json, locked, object, } from "./common.js";
7
+ import { commandContext, contextOptions } from "./command-context.js";
8
+ import { parseJsonObject } from "./json.js";
9
+ const BEGIN = "# BEGIN RL MCP - managed by rl setup mcp";
10
+ const END = "# END RL MCP";
11
+ const MARKER = "RL_MCP_MANAGED";
12
+ function readConfig(workspace, relative) {
13
+ const file = path.join(workspace, relative);
14
+ if (fs
15
+ .lstatSync(path.dirname(file), { throwIfNoEntry: false })
16
+ ?.isSymbolicLink() ||
17
+ !inside(canonical(file), workspace))
18
+ throw new Error(`MCP configuration must stay in this workspace: ${relative}`);
19
+ const stat = fs.lstatSync(file, { throwIfNoEntry: false });
20
+ if (stat &&
21
+ (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1024 * 1024))
22
+ throw new Error(`Expected a regular MCP configuration file up to 1 MiB: ${relative}`);
23
+ return { file, ...(stat ? { text: fs.readFileSync(file, "utf8") } : {}) };
24
+ }
25
+ function serverEntry(previous, command, argv, file) {
26
+ if (previous !== undefined &&
27
+ (!object(previous) || !object(previous.env) || previous.env[MARKER] !== "1"))
28
+ throw new Error(`An existing server named rl in ${file} is not managed by RL. Keep it, or remove only that entry before running rl setup mcp.`);
29
+ if (object(previous) &&
30
+ (previous.url !== undefined ||
31
+ (previous.type !== undefined && previous.type !== "stdio")))
32
+ throw new Error(`The existing rl server in ${file} is not a local stdio server`);
33
+ const entry = object(previous) ? previous : {};
34
+ return {
35
+ ...entry,
36
+ command,
37
+ args: argv,
38
+ env: { ...(object(entry.env) ? entry.env : {}), [MARKER]: "1" },
39
+ };
40
+ }
41
+ function claudeConfig(before, command, argv, file) {
42
+ const config = before === undefined ? {} : parseJsonObject(before);
43
+ if (config.mcpServers !== undefined && !object(config.mcpServers))
44
+ throw new Error(`Expected an mcpServers object in ${file}`);
45
+ const servers = object(config.mcpServers) ? config.mcpServers : {};
46
+ const entry = {
47
+ ...serverEntry(servers.rl, command, argv, file),
48
+ type: "stdio",
49
+ };
50
+ return (JSON.stringify({ ...config, mcpServers: { ...servers, rl: entry } }, null, 2) + "\n");
51
+ }
52
+ function codexConfig(before, command, argv, workspace, file) {
53
+ const text = before ?? "";
54
+ const config = parseToml(text, { integersAsBigInt: "asNeeded" });
55
+ if (config.mcp_servers !== undefined && !object(config.mcp_servers))
56
+ throw new Error(`Expected an mcp_servers table in ${file}`);
57
+ const servers = object(config.mcp_servers) ? config.mcp_servers : {};
58
+ const entry = {
59
+ ...serverEntry(servers.rl, command, argv, file),
60
+ cwd: workspace,
61
+ };
62
+ const starts = [
63
+ ...text.matchAll(/^# BEGIN RL MCP - managed by rl setup mcp\r?$/gm),
64
+ ];
65
+ const ends = [...text.matchAll(/^# END RL MCP\r?$/gm)];
66
+ const marked = starts.length || ends.length;
67
+ if (marked &&
68
+ (starts.length !== 1 ||
69
+ ends.length !== 1 ||
70
+ starts[0].index >= ends[0].index ||
71
+ servers.rl === undefined))
72
+ throw new Error(`Invalid managed RL MCP block in ${file}`);
73
+ if (servers.rl !== undefined && !marked)
74
+ throw new Error(`The rl server in ${file} has no managed block; preserve or remove that entry before setup`);
75
+ const block = `${BEGIN}\n${stringifyToml({ mcp_servers: { rl: entry } })}${END}`;
76
+ const updated = marked
77
+ ? text.slice(0, starts[0].index) +
78
+ block +
79
+ text.slice(ends[0].index + ends[0][0].length)
80
+ : text + (text ? (text.endsWith("\n") ? "\n" : "\n\n") : "") + block + "\n";
81
+ // Validate the merged document as well: inline tables and dotted keys may
82
+ // prevent appending a new child table even when the existing TOML is valid.
83
+ const merged = parseToml(updated, { integersAsBigInt: "asNeeded" });
84
+ const [originalSettings, mergedSettings] = [config, merged].map((value) => {
85
+ const copy = { ...value };
86
+ if (object(copy.mcp_servers)) {
87
+ const servers = { ...copy.mcp_servers };
88
+ delete servers.rl;
89
+ if (Object.keys(servers).length)
90
+ copy.mcp_servers = servers;
91
+ else
92
+ delete copy.mcp_servers;
93
+ }
94
+ return copy;
95
+ });
96
+ if (!isDeepStrictEqual(originalSettings, mergedSettings))
97
+ throw new Error(`The managed RL MCP block in ${file} contains unrelated settings. Move them outside the block before setup.`);
98
+ return updated;
99
+ }
100
+ export async function main(argv) {
101
+ const { values, positionals } = args(argv, contextOptions, [
102
+ "dry-run",
103
+ "json",
104
+ ]);
105
+ arity(positionals, 0, 1);
106
+ const selected = positionals[0];
107
+ if (selected !== undefined && selected !== "codex" && selected !== "claude")
108
+ throw new Error("Expected codex or claude, or omit the client to configure both");
109
+ const context = commandContext(values);
110
+ const [, active] = context.store.current();
111
+ const updates = await locked(active.worktree, ".rl-mcp-setup.lock", () => {
112
+ const [, instance] = context.store.current();
113
+ if (instance.key !== active.key)
114
+ throw new Error("RL workspace changed during MCP setup; retry");
115
+ const workspace = instance.worktree;
116
+ const command = process.execPath;
117
+ const serverArgs = [
118
+ fileURLToPath(new URL("./cli.js", import.meta.url)),
119
+ "mcp",
120
+ "--repo",
121
+ context.repo,
122
+ "--state-dir",
123
+ context.stateDir,
124
+ "--workspace",
125
+ workspace,
126
+ ];
127
+ const clients = selected ? [selected] : ["codex", "claude"];
128
+ const plans = clients.map((client) => {
129
+ const { file, text } = readConfig(workspace, client === "codex" ? ".codex/config.toml" : ".mcp.json");
130
+ const after = client === "codex"
131
+ ? codexConfig(text, command, serverArgs, workspace, file)
132
+ : claudeConfig(text, command, serverArgs, file);
133
+ return {
134
+ client,
135
+ file,
136
+ before: text,
137
+ after,
138
+ action: text === after
139
+ ? "unchanged"
140
+ : text === undefined
141
+ ? "created"
142
+ : "updated",
143
+ };
144
+ });
145
+ if (!values["dry-run"]) {
146
+ // Validate every selected file before writing any, and refuse concurrent
147
+ // external edits rather than silently replacing another process's work.
148
+ for (const plan of plans) {
149
+ const current = readConfig(workspace, path.relative(workspace, plan.file));
150
+ if (current.text !== plan.before)
151
+ throw new Error("MCP configuration changed during setup; retry");
152
+ }
153
+ for (const plan of plans)
154
+ if (plan.action !== "unchanged")
155
+ atomicWrite(plan.file, plan.after);
156
+ }
157
+ return plans.map(({ client, file, action }) => ({ client, file, action }));
158
+ });
159
+ if (values.json)
160
+ json({
161
+ dryRun: !!values["dry-run"],
162
+ files: updates,
163
+ next: "Reload your client and review its normal project trust/MCP approval. Native subagents are created by the current session.",
164
+ });
165
+ else {
166
+ for (const update of updates)
167
+ console.log(`${values["dry-run"] ? "Would configure" : update.action === "unchanged" ? "Already configured" : "Configured"} ${update.client}: ${update.file}`);
168
+ console.log("Reload your client, then review its normal project trust/MCP approval. RL tools will be available to your existing session.");
169
+ }
170
+ return 0;
171
+ }
package/dist/mcp.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { fileURLToPath } from "node:url";
2
+ import path from "node:path";
2
3
  import { args, arity, message, required } from "./common.js";
3
4
  import { activityView } from "./activity.js";
4
5
  import { contextText, Store } from "./store.js";
@@ -7,7 +8,7 @@ import { currentSessionId } from "./session.js";
7
8
  import { syncCodexSessions } from "./codex-sessions.js";
8
9
  import { pauseTask, PAUSE_FIELDS } from "./task.js";
9
10
  import { HANDOFF_FIELDS, MAX_HANDOFF_TEXT } from "./task-state.js";
10
- import { automationContext, callWorkflowTool, WORKFLOW_TOOLS, } from "./session-workflows.js";
11
+ import { automationContext, callWorkflowTool, WORKFLOW_TOOLS, workflowToolSchema, } from "./session-workflows.js";
11
12
  import { isJsonObject, parseJson, readJsonObject, } from "./json.js";
12
13
  const PROTOCOL_VERSION = "2025-11-25";
13
14
  const RPC_VERSION = "2.0";
@@ -269,19 +270,7 @@ export async function serveMcp(store) {
269
270
  ...WORKFLOW_TOOLS.map((tool) => ({
270
271
  name: tool.name,
271
272
  description: tool.description,
272
- inputSchema: {
273
- type: "object",
274
- properties: {
275
- agent: { type: "string", enum: ["codex", "claude"] },
276
- sessionId: {
277
- type: "string",
278
- description: "Current parent conversation ID. Claude requires a UUID; Codex also accepts observed thr_ IDs and may inherit it.",
279
- },
280
- ...tool.properties,
281
- },
282
- required: tool.required,
283
- additionalProperties: false,
284
- },
273
+ inputSchema: workflowToolSchema(tool),
285
274
  annotations: {
286
275
  readOnlyHint: tool.readOnly,
287
276
  destructiveHint: false,
@@ -330,7 +319,15 @@ export async function serveMcp(store) {
330
319
  return 0;
331
320
  }
332
321
  export async function main(argv) {
333
- const { values, positionals } = args(argv, ["repo", "state-dir"], []);
322
+ const { values, positionals } = args(argv, ["repo", "state-dir", "workspace"], []);
334
323
  arity(positionals, 0);
335
- return serveMcp(new Store(required(values, "repo"), required(values, "state-dir")));
324
+ const store = new Store(required(values, "repo"), required(values, "state-dir"));
325
+ if (values.workspace !== undefined) {
326
+ const workspace = required(values, "workspace");
327
+ if (!path.isAbsolute(workspace))
328
+ throw new Error("Expected an absolute MCP workspace path");
329
+ const [, instance] = store.current(workspace);
330
+ process.chdir(instance.worktree);
331
+ }
332
+ return serveMcp(store);
336
333
  }