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.
- package/README.md +62 -53
- package/bin/rl +2 -54
- package/dist/agent-launch.js +1 -1
- package/dist/agent-resume.js +2 -2
- package/dist/bootstrap.js +74 -0
- package/dist/cli.js +2 -0
- package/dist/command-context.js +1 -1
- package/dist/common.js +3 -2
- package/dist/dependencies.js +197 -0
- package/dist/doctor.js +1 -1
- package/dist/help.js +81 -84
- package/dist/hooks.js +1 -1
- package/dist/install.js +143 -0
- package/dist/mcp-setup.js +171 -0
- package/dist/mcp.js +13 -16
- package/dist/picker.js +24 -7
- package/dist/pr-workflow.js +7 -28
- package/dist/repos.js +5 -1
- package/dist/session-workflows.js +71 -1
- package/dist/session.js +1 -1
- package/dist/state.js +4 -14
- package/dist/storage.js +2 -2
- package/dist/store.js +2 -2
- package/dist/summary.js +1 -1
- package/dist/workflow.js +24 -38
- package/lib/entry.zsh +54 -0
- package/lib/rl.zsh +35 -6
- package/node_modules/proper-lockfile/CHANGELOG.md +108 -0
- package/node_modules/smol-toml/LICENSE +24 -0
- package/node_modules/smol-toml/README.md +605 -0
- package/node_modules/smol-toml/dist/date.d.ts +41 -0
- package/node_modules/smol-toml/dist/date.js +156 -0
- package/node_modules/smol-toml/dist/error.d.ts +37 -0
- package/node_modules/smol-toml/dist/error.js +67 -0
- package/node_modules/smol-toml/dist/extract.js +293 -0
- package/node_modules/smol-toml/dist/index.cjs +771 -0
- package/node_modules/smol-toml/dist/index.d.ts +44 -0
- package/node_modules/smol-toml/dist/index.js +34 -0
- package/node_modules/smol-toml/dist/parse.d.ts +73 -0
- package/node_modules/smol-toml/dist/parse.js +164 -0
- package/node_modules/smol-toml/dist/primitive.js +151 -0
- package/node_modules/smol-toml/dist/stringify.d.ts +32 -0
- package/node_modules/smol-toml/dist/stringify.js +219 -0
- package/node_modules/smol-toml/dist/struct.js +156 -0
- package/node_modules/smol-toml/dist/util.d.ts +43 -0
- package/node_modules/smol-toml/dist/util.js +57 -0
- package/node_modules/smol-toml/package.json +68 -0
- package/package.json +6 -3
- 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
|
-
"
|
|
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: ["
|
|
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
|
|
67
|
+
"Use rl list for workspace overviews and --include-deleted for archived records.",
|
|
68
68
|
],
|
|
69
|
-
related: ["list"
|
|
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: [
|
|
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
|
|
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
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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
|
|
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
|
package/dist/install.js
ADDED
|
@@ -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
|
-
|
|
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
|
}
|