rl-workspaces 0.0.0-stage → 0.6.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/LICENSE +21 -0
- package/README.md +735 -2
- package/bin/rl +54 -0
- package/config.example.zsh +9 -0
- package/dist/access.js +40 -0
- package/dist/activity.js +90 -0
- package/dist/agent-claude.js +16 -0
- package/dist/agent-codex.js +22 -0
- package/dist/agent-launch.js +74 -0
- package/dist/agent-presets.js +70 -0
- package/dist/agent-registry.js +8 -0
- package/dist/agent-resume.js +96 -0
- package/dist/agent-runner.js +386 -0
- package/dist/agent-workflows.js +275 -0
- package/dist/agents.js +73 -0
- package/dist/automation-config.js +381 -0
- package/dist/base.js +66 -0
- package/dist/cleanup-path.js +33 -0
- package/dist/cli.js +57 -0
- package/dist/codex-sessions.js +188 -0
- package/dist/command-context.js +67 -0
- package/dist/common.js +262 -0
- package/dist/delete.js +113 -0
- package/dist/display.js +134 -0
- package/dist/doctor.js +145 -0
- package/dist/git-safety.js +24 -0
- package/dist/git-workflow.js +89 -0
- package/dist/help.js +726 -0
- package/dist/hooks.js +159 -0
- package/dist/json.js +34 -0
- package/dist/mcp.js +336 -0
- package/dist/picker.js +182 -0
- package/dist/pr-discovery.js +223 -0
- package/dist/pr-update.js +419 -0
- package/dist/pr-workflow.js +161 -0
- package/dist/repos.js +537 -0
- package/dist/review-policy.js +150 -0
- package/dist/session-workflows.js +650 -0
- package/dist/session.js +12 -0
- package/dist/state.js +250 -0
- package/dist/storage.js +366 -0
- package/dist/store.js +379 -0
- package/dist/summary.js +164 -0
- package/dist/task-state.js +65 -0
- package/dist/task.js +192 -0
- package/dist/templates.js +4 -0
- package/dist/workflow-collaboration.js +531 -0
- package/dist/workflow-config.js +177 -0
- package/dist/workflow.js +211 -0
- package/lib/rl.zsh +329 -0
- package/node_modules/graceful-fs/LICENSE +15 -0
- package/node_modules/graceful-fs/README.md +143 -0
- package/node_modules/graceful-fs/clone.js +23 -0
- package/node_modules/graceful-fs/graceful-fs.js +448 -0
- package/node_modules/graceful-fs/legacy-streams.js +118 -0
- package/node_modules/graceful-fs/package.json +53 -0
- package/node_modules/graceful-fs/polyfills.js +355 -0
- package/node_modules/proper-lockfile/LICENSE +21 -0
- package/node_modules/proper-lockfile/README.md +183 -0
- package/node_modules/proper-lockfile/index.js +40 -0
- package/node_modules/proper-lockfile/lib/adapter.js +85 -0
- package/node_modules/proper-lockfile/lib/lockfile.js +342 -0
- package/node_modules/proper-lockfile/lib/mtime-precision.js +55 -0
- package/node_modules/proper-lockfile/package.json +71 -0
- package/node_modules/retry/.npmignore +3 -0
- package/node_modules/retry/.travis.yml +15 -0
- package/node_modules/retry/License +21 -0
- package/node_modules/retry/Makefile +18 -0
- package/node_modules/retry/README.md +227 -0
- package/node_modules/retry/equation.gif +0 -0
- package/node_modules/retry/example/dns.js +31 -0
- package/node_modules/retry/example/stop.js +40 -0
- package/node_modules/retry/index.js +1 -0
- package/node_modules/retry/lib/retry.js +100 -0
- package/node_modules/retry/lib/retry_operation.js +158 -0
- package/node_modules/retry/package.json +32 -0
- package/node_modules/retry/test/common.js +10 -0
- package/node_modules/retry/test/integration/test-forever.js +24 -0
- package/node_modules/retry/test/integration/test-retry-operation.js +258 -0
- package/node_modules/retry/test/integration/test-retry-wrap.js +101 -0
- package/node_modules/retry/test/integration/test-timeouts.js +69 -0
- package/node_modules/signal-exit/LICENSE.txt +16 -0
- package/node_modules/signal-exit/README.md +39 -0
- package/node_modules/signal-exit/index.js +202 -0
- package/node_modules/signal-exit/package.json +38 -0
- package/node_modules/signal-exit/signals.js +53 -0
- package/package.json +57 -4
- package/shell/rl.zsh +11 -0
package/dist/hooks.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { observeWorkflowEvent } from "./session-workflows.js";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { agentAdapter } from "./agent-registry.js";
|
|
6
|
+
import { recordActivity } from "./activity.js";
|
|
7
|
+
import { git, json, message, now, object } from "./common.js";
|
|
8
|
+
import { contextText } from "./store.js";
|
|
9
|
+
export async function handleHook(store, key, name) {
|
|
10
|
+
const adapter = agentAdapter(name);
|
|
11
|
+
const payload = JSON.parse(fs.readFileSync(0, "utf8"));
|
|
12
|
+
if (!object(payload) ||
|
|
13
|
+
typeof payload.cwd !== "string" ||
|
|
14
|
+
!path.isAbsolute(payload.cwd))
|
|
15
|
+
throw new Error("Hook requires an absolute cwd");
|
|
16
|
+
// Resolve repository membership before waiting for the state lock. Recheck
|
|
17
|
+
// the branch inside it so a branch switch while waiting cannot bind a session.
|
|
18
|
+
const [root] = store.worktree(payload.cwd);
|
|
19
|
+
return store.locked(() => {
|
|
20
|
+
// Establish ownership before persisting even diagnostics. A copied hook
|
|
21
|
+
// cannot change the original instance or the destination instance.
|
|
22
|
+
const [file, data] = store.instanceForWorktree(root, git(root, "branch", "--show-current"));
|
|
23
|
+
if (data.key !== key)
|
|
24
|
+
throw new Error("Hook belongs to a different RL instance; run 'rl adopt' to repair it");
|
|
25
|
+
try {
|
|
26
|
+
const event = adapter.parse(payload);
|
|
27
|
+
// Workflow workers are ephemeral. Supply task context without turning a
|
|
28
|
+
// review pass into a saved conversation or changing the user's last-used
|
|
29
|
+
// session. Other native/managed hooks remain under the CLI's own policy.
|
|
30
|
+
if (process.env.RL_WORKFLOW_RUN && process.env.RL_WORKFLOW_STEP) {
|
|
31
|
+
const context = [
|
|
32
|
+
"session.started",
|
|
33
|
+
"context.restored",
|
|
34
|
+
"child.started",
|
|
35
|
+
"turn.started",
|
|
36
|
+
].includes(event.type)
|
|
37
|
+
? contextText(file, data)
|
|
38
|
+
: undefined;
|
|
39
|
+
const response = adapter.response(event, context);
|
|
40
|
+
if (response !== undefined)
|
|
41
|
+
json(response);
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
const stamp = now();
|
|
45
|
+
let context, hash;
|
|
46
|
+
if ([
|
|
47
|
+
"session.started",
|
|
48
|
+
"context.restored",
|
|
49
|
+
"child.started",
|
|
50
|
+
"turn.started",
|
|
51
|
+
].includes(event.type)) {
|
|
52
|
+
const text = contextText(file, data);
|
|
53
|
+
const digest = createHash("sha256").update(text);
|
|
54
|
+
// Changes beyond the injected excerpt must also trigger a refresh.
|
|
55
|
+
for (const name of ["context.md", "progress.md"])
|
|
56
|
+
digest.update(fs.readFileSync(path.join(path.dirname(file), name)));
|
|
57
|
+
hash = digest.digest("hex");
|
|
58
|
+
const previous = data.activity?.[adapter.name]?.find((s) => s.sessionId === event.sessionId && s.childId === event.childId);
|
|
59
|
+
if (event.type !== "turn.started" || previous?.contextHash !== hash)
|
|
60
|
+
context = text;
|
|
61
|
+
}
|
|
62
|
+
// Keep last-used selection tied to starts/prompts, not a late shutdown
|
|
63
|
+
// event from an older session or a child's activity.
|
|
64
|
+
const used = event.resumable &&
|
|
65
|
+
["session.started", "context.restored", "turn.started"].includes(event.type);
|
|
66
|
+
if (used)
|
|
67
|
+
store.associateSession(file, data, adapter.name, event.sessionId, stamp);
|
|
68
|
+
else
|
|
69
|
+
store.assertSessionOwner(file, adapter.name, event.sessionId);
|
|
70
|
+
recordActivity(data, adapter.name, event, stamp, hash);
|
|
71
|
+
if (used)
|
|
72
|
+
store.touch(file, data, stamp);
|
|
73
|
+
else
|
|
74
|
+
store.save(file, data);
|
|
75
|
+
// Native workers never recursively schedule their own workflow. Tool hooks
|
|
76
|
+
// describe what happened; the parent remains responsible for delegation.
|
|
77
|
+
let queued = [];
|
|
78
|
+
if (!event.childId && !payload.stop_hook_active) {
|
|
79
|
+
const tool = typeof payload.tool_name === "string" ? payload.tool_name : undefined;
|
|
80
|
+
const input = object(payload.tool_input) ? payload.tool_input : {};
|
|
81
|
+
const coordination = !!tool &&
|
|
82
|
+
(tool.includes("rl_workflow_") ||
|
|
83
|
+
(tool === "Bash" &&
|
|
84
|
+
[input.command, input.cmd].some((value) => typeof value === "string" &&
|
|
85
|
+
/^(?:command\s+)?rl\s+workflow\s+/.test(value.trim()))));
|
|
86
|
+
const paths = [];
|
|
87
|
+
const addPath = (value) => {
|
|
88
|
+
if (typeof value === "string") {
|
|
89
|
+
const relative = path.relative(root, path.resolve(event.cwd, value));
|
|
90
|
+
if (relative && relative !== ".." && !relative.startsWith("../"))
|
|
91
|
+
paths.push(relative);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
for (const value of [input.file_path, input.path])
|
|
95
|
+
addPath(value);
|
|
96
|
+
const patch = typeof input.patch === "string"
|
|
97
|
+
? input.patch
|
|
98
|
+
: typeof input.input === "string"
|
|
99
|
+
? input.input
|
|
100
|
+
: typeof payload.tool_input === "string"
|
|
101
|
+
? payload.tool_input
|
|
102
|
+
: "";
|
|
103
|
+
for (const match of patch.matchAll(/^\*\*\* (?:(?:Add|Update|Delete) File|Move to): (.+)$/gm))
|
|
104
|
+
addPath(match[1]);
|
|
105
|
+
const edit = !!tool &&
|
|
106
|
+
["Edit", "Write", "MultiEdit", "apply_patch"].includes(tool);
|
|
107
|
+
const milestone = event.type === "session.started"
|
|
108
|
+
? "session_start"
|
|
109
|
+
: event.type === "turn.started"
|
|
110
|
+
? "prompt"
|
|
111
|
+
: event.type === "turn.stopped"
|
|
112
|
+
? "before_finish"
|
|
113
|
+
: event.type === "tool.before"
|
|
114
|
+
? edit
|
|
115
|
+
? "before_edit"
|
|
116
|
+
: "before_tool"
|
|
117
|
+
: event.type === "tool.after"
|
|
118
|
+
? edit
|
|
119
|
+
? "after_edit"
|
|
120
|
+
: "after_tool"
|
|
121
|
+
: undefined;
|
|
122
|
+
if (milestone && !coordination)
|
|
123
|
+
queued = observeWorkflowEvent(store, file, data, { agent: adapter.name, sessionId: event.sessionId }, milestone, tool, paths);
|
|
124
|
+
}
|
|
125
|
+
const notification = queued.length
|
|
126
|
+
? `RL workflow requests for this parent session (${adapter.name}, sessionId ${event.sessionId}): ${JSON.stringify(queued)}. Use rl_workflow_start with requestId, then rl_workflow_claim, spawn your own native subagents, and rl_workflow_report their evidence. If native delegation is unavailable, dismiss the request with the reason and tell the user. Follow current instructions and workspace permissions.`
|
|
127
|
+
: undefined;
|
|
128
|
+
if (notification)
|
|
129
|
+
context = [context, notification].filter(Boolean).join("\n\n");
|
|
130
|
+
if (context && !event.childId)
|
|
131
|
+
context += `\nCurrent RL parent conversation: agent=${adapter.name}, sessionId=${event.sessionId}. Use this identity for workflow coordination tools.`;
|
|
132
|
+
if (event.childId && context)
|
|
133
|
+
context +=
|
|
134
|
+
"\nYou are a native child. Follow your assigned role; do not create workflows or spawn further agents.";
|
|
135
|
+
const response = event.type === "tool.before" && notification
|
|
136
|
+
? {
|
|
137
|
+
hookSpecificOutput: {
|
|
138
|
+
hookEventName: event.nativeEvent,
|
|
139
|
+
permissionDecision: "deny",
|
|
140
|
+
permissionDecisionReason: notification,
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
: event.type === "turn.stopped" && notification
|
|
144
|
+
? { decision: "block", reason: notification }
|
|
145
|
+
: adapter.response(event, context);
|
|
146
|
+
if (response !== undefined)
|
|
147
|
+
json(response);
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
((data.hookHealth ??= {})[adapter.name] ??= {}).lastError = {
|
|
152
|
+
message: message(error),
|
|
153
|
+
at: now(),
|
|
154
|
+
};
|
|
155
|
+
store.save(file, data);
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
package/dist/json.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
export function isJsonObject(value) {
|
|
3
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
function isJsonValue(value) {
|
|
6
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
7
|
+
return true;
|
|
8
|
+
if (typeof value === "number")
|
|
9
|
+
return Number.isFinite(value);
|
|
10
|
+
if (Array.isArray(value))
|
|
11
|
+
return value.every(isJsonValue);
|
|
12
|
+
return typeof value === "object" && Object.values(value).every(isJsonValue);
|
|
13
|
+
}
|
|
14
|
+
/** Raw JSON enters the program through this validated boundary. */
|
|
15
|
+
export function parseJson(text) {
|
|
16
|
+
const value = JSON.parse(text);
|
|
17
|
+
if (!isJsonValue(value))
|
|
18
|
+
throw new Error("Invalid JSON value");
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
export function parseJsonObject(text) {
|
|
22
|
+
const value = parseJson(text);
|
|
23
|
+
if (!isJsonObject(value))
|
|
24
|
+
throw new Error("Expected a JSON object");
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
export function readJsonObject(file) {
|
|
28
|
+
try {
|
|
29
|
+
return parseJsonObject(fs.readFileSync(file, "utf8"));
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
throw new Error(`Cannot read ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { args, arity, message, required } from "./common.js";
|
|
3
|
+
import { activityView } from "./activity.js";
|
|
4
|
+
import { contextText, Store } from "./store.js";
|
|
5
|
+
import { agentAdapter } from "./agent-registry.js";
|
|
6
|
+
import { currentSessionId } from "./session.js";
|
|
7
|
+
import { syncCodexSessions } from "./codex-sessions.js";
|
|
8
|
+
import { pauseTask, PAUSE_FIELDS } from "./task.js";
|
|
9
|
+
import { HANDOFF_FIELDS, MAX_HANDOFF_TEXT } from "./task-state.js";
|
|
10
|
+
import { automationContext, callWorkflowTool, WORKFLOW_TOOLS, } from "./session-workflows.js";
|
|
11
|
+
import { isJsonObject, parseJson, readJsonObject, } from "./json.js";
|
|
12
|
+
const PROTOCOL_VERSION = "2025-11-25";
|
|
13
|
+
const RPC_VERSION = "2.0";
|
|
14
|
+
const MAX_MESSAGE_BYTES = 1024 * 1024;
|
|
15
|
+
const MAX_NOTE_LENGTH = 64 * 1024;
|
|
16
|
+
const ERROR = {
|
|
17
|
+
parse: -32700,
|
|
18
|
+
request: -32600,
|
|
19
|
+
method: -32601,
|
|
20
|
+
params: -32602,
|
|
21
|
+
};
|
|
22
|
+
const TOOLS = [
|
|
23
|
+
{
|
|
24
|
+
name: "rl_task_pause",
|
|
25
|
+
description: "When the user asks to pause this task, save its current session and a durable handoff for rl continue. Include concrete changes, checks actually run and their results (or Not run), next steps, and blockers. agent defaults to codex. Supply the current session UUID if this server does not inherit it. Saving a pause does not terminate the agent.",
|
|
26
|
+
append: false,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "rl_session_save",
|
|
30
|
+
description: "Save a session for this workspace so it appears in status and can be resumed. agent defaults to codex. Supply sessionId with the full UUID of the current conversation; if omitted, Codex IDs are read from this server's environment when available. Claude requires sessionId.",
|
|
31
|
+
append: false,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "rl_status",
|
|
35
|
+
description: "Discover and save Codex sessions for this workspace, then read its branch, sessions and observed agent activity.",
|
|
36
|
+
append: false,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "rl_context",
|
|
40
|
+
description: "Read shared task context and progress for agent handoff.",
|
|
41
|
+
append: false,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "rl_progress",
|
|
45
|
+
description: "Read the complete shared progress document.",
|
|
46
|
+
append: false,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "rl_context_append",
|
|
50
|
+
description: "Append durable design or domain notes to shared context.",
|
|
51
|
+
append: true,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: "rl_progress_append",
|
|
55
|
+
description: "Append a milestone or handoff update to shared progress.",
|
|
56
|
+
append: true,
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
async function* readMessages(input) {
|
|
60
|
+
let parts = [];
|
|
61
|
+
let size = 0;
|
|
62
|
+
let oversized = false;
|
|
63
|
+
for await (const chunk of input) {
|
|
64
|
+
if (!Buffer.isBuffer(chunk))
|
|
65
|
+
throw new Error("MCP input must be bytes");
|
|
66
|
+
let start = 0;
|
|
67
|
+
while (start < chunk.length) {
|
|
68
|
+
const newline = chunk.indexOf("\n", start);
|
|
69
|
+
const end = newline < 0 ? chunk.length : newline;
|
|
70
|
+
const length = end - start;
|
|
71
|
+
if (!oversized) {
|
|
72
|
+
if (size + length > MAX_MESSAGE_BYTES) {
|
|
73
|
+
oversized = true;
|
|
74
|
+
parts = [];
|
|
75
|
+
size = 0;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
parts.push(chunk.subarray(start, end));
|
|
79
|
+
size += length;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (newline < 0)
|
|
83
|
+
break;
|
|
84
|
+
yield oversized ? null : Buffer.concat(parts, size).toString("utf8");
|
|
85
|
+
parts = [];
|
|
86
|
+
size = 0;
|
|
87
|
+
oversized = false;
|
|
88
|
+
start = newline + 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (oversized || size)
|
|
92
|
+
yield oversized ? null : Buffer.concat(parts, size).toString("utf8");
|
|
93
|
+
}
|
|
94
|
+
async function callTool(store, tool, input) {
|
|
95
|
+
const name = tool.name;
|
|
96
|
+
const session = name === "rl_session_save";
|
|
97
|
+
const allowed = session
|
|
98
|
+
? ["agent", "sessionId"]
|
|
99
|
+
: name === "rl_task_pause"
|
|
100
|
+
? PAUSE_FIELDS
|
|
101
|
+
: tool.append
|
|
102
|
+
? ["text"]
|
|
103
|
+
: [];
|
|
104
|
+
if (Object.keys(input).some((key) => !allowed.includes(key)))
|
|
105
|
+
throw new Error("Unexpected tool argument");
|
|
106
|
+
if (name === "rl_task_pause")
|
|
107
|
+
return JSON.stringify(await pauseTask(store, input));
|
|
108
|
+
const text = input.text;
|
|
109
|
+
if (tool.append &&
|
|
110
|
+
(typeof text !== "string" || !text.trim() || text.length > MAX_NOTE_LENGTH))
|
|
111
|
+
throw new Error(`text must be non-empty and at most ${MAX_NOTE_LENGTH} characters`);
|
|
112
|
+
if (name === "rl_status")
|
|
113
|
+
await syncCodexSessions(store, process.cwd());
|
|
114
|
+
return store.locked(() => {
|
|
115
|
+
const [file, data] = store.current();
|
|
116
|
+
switch (name) {
|
|
117
|
+
case "rl_session_save": {
|
|
118
|
+
if (input.agent !== undefined && typeof input.agent !== "string")
|
|
119
|
+
throw new Error("agent must be codex or claude");
|
|
120
|
+
const agent = agentAdapter(input.agent ?? "codex").name;
|
|
121
|
+
const sid = currentSessionId(agent, input.sessionId);
|
|
122
|
+
store.recordSession(file, data, agent, sid);
|
|
123
|
+
return JSON.stringify({ agent, sessionId: sid, instance: data.id });
|
|
124
|
+
}
|
|
125
|
+
case "rl_status":
|
|
126
|
+
return JSON.stringify({
|
|
127
|
+
id: data.id,
|
|
128
|
+
branch: data.branch,
|
|
129
|
+
worktree: data.worktree,
|
|
130
|
+
sessions: data.sessions,
|
|
131
|
+
task: data.task ?? null,
|
|
132
|
+
activity: {
|
|
133
|
+
codex: activityView(data.activity?.codex),
|
|
134
|
+
claude: activityView(data.activity?.claude),
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
case "rl_context":
|
|
138
|
+
return contextText(file, data);
|
|
139
|
+
case "rl_progress":
|
|
140
|
+
return store.readDocument(file, "progress");
|
|
141
|
+
default: {
|
|
142
|
+
if (typeof text !== "string")
|
|
143
|
+
throw new Error("text is required");
|
|
144
|
+
store.updateDocument(file, data, name === "rl_context_append" ? "context" : "progress", "append", text);
|
|
145
|
+
return "Update appended.";
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
export async function serveMcp(store) {
|
|
151
|
+
const metadata = readJsonObject(fileURLToPath(new URL("../package.json", import.meta.url)));
|
|
152
|
+
if (typeof metadata.version !== "string")
|
|
153
|
+
throw new Error("Invalid package version");
|
|
154
|
+
const version = metadata.version;
|
|
155
|
+
let initialized = false;
|
|
156
|
+
let ready = false;
|
|
157
|
+
for await (const line of readMessages(process.stdin)) {
|
|
158
|
+
let id = null;
|
|
159
|
+
let code = ERROR.parse;
|
|
160
|
+
try {
|
|
161
|
+
if (line === null)
|
|
162
|
+
throw new Error("Message too large");
|
|
163
|
+
const raw = parseJson(line);
|
|
164
|
+
code = ERROR.request;
|
|
165
|
+
if (!isJsonObject(raw) ||
|
|
166
|
+
raw.jsonrpc !== RPC_VERSION ||
|
|
167
|
+
typeof raw.method !== "string")
|
|
168
|
+
throw new Error("Invalid JSON-RPC request");
|
|
169
|
+
if (raw.id !== undefined &&
|
|
170
|
+
typeof raw.id !== "string" &&
|
|
171
|
+
typeof raw.id !== "number")
|
|
172
|
+
throw new Error("Invalid request id");
|
|
173
|
+
if (raw.id === undefined) {
|
|
174
|
+
if (raw.method === "notifications/initialized" && initialized)
|
|
175
|
+
ready = true;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
id = raw.id;
|
|
179
|
+
const params = raw.params ?? {};
|
|
180
|
+
code = ERROR.params;
|
|
181
|
+
if (!isJsonObject(params))
|
|
182
|
+
throw new Error("params must be an object");
|
|
183
|
+
let result;
|
|
184
|
+
if (raw.method === "initialize") {
|
|
185
|
+
if (initialized)
|
|
186
|
+
throw new Error("Already initialized");
|
|
187
|
+
if (typeof params.protocolVersion !== "string" ||
|
|
188
|
+
!isJsonObject(params.capabilities) ||
|
|
189
|
+
!isJsonObject(params.clientInfo) ||
|
|
190
|
+
typeof params.clientInfo.name !== "string" ||
|
|
191
|
+
typeof params.clientInfo.version !== "string")
|
|
192
|
+
throw new Error("Invalid initialization parameters");
|
|
193
|
+
initialized = true;
|
|
194
|
+
result = {
|
|
195
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
196
|
+
capabilities: { tools: {} },
|
|
197
|
+
serverInfo: { name: "rl", version },
|
|
198
|
+
instructions: automationContext(store.current()[1].worktree) ||
|
|
199
|
+
"Use RL tools for the current workspace's shared context and sessions.",
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
else if (raw.method === "ping")
|
|
203
|
+
result = {};
|
|
204
|
+
else {
|
|
205
|
+
if (!ready)
|
|
206
|
+
throw new Error("Initialize before using tools");
|
|
207
|
+
if (raw.method === "tools/list") {
|
|
208
|
+
result = {
|
|
209
|
+
tools: [
|
|
210
|
+
...TOOLS.map((tool) => ({
|
|
211
|
+
name: tool.name,
|
|
212
|
+
description: tool.description,
|
|
213
|
+
inputSchema: {
|
|
214
|
+
type: "object",
|
|
215
|
+
properties: tool.name === "rl_task_pause"
|
|
216
|
+
? {
|
|
217
|
+
...Object.fromEntries(HANDOFF_FIELDS.map((field) => [
|
|
218
|
+
field,
|
|
219
|
+
{
|
|
220
|
+
type: "string",
|
|
221
|
+
maxLength: MAX_HANDOFF_TEXT,
|
|
222
|
+
...(field === "blockers"
|
|
223
|
+
? {}
|
|
224
|
+
: { minLength: 1 }),
|
|
225
|
+
},
|
|
226
|
+
])),
|
|
227
|
+
agent: { type: "string", enum: ["codex", "claude"] },
|
|
228
|
+
sessionId: {
|
|
229
|
+
type: "string",
|
|
230
|
+
description: "Full UUID of the current conversation.",
|
|
231
|
+
},
|
|
232
|
+
}
|
|
233
|
+
: tool.name === "rl_session_save"
|
|
234
|
+
? {
|
|
235
|
+
agent: {
|
|
236
|
+
type: "string",
|
|
237
|
+
enum: ["codex", "claude"],
|
|
238
|
+
},
|
|
239
|
+
sessionId: {
|
|
240
|
+
type: "string",
|
|
241
|
+
description: "Full session UUID; never a name or prefix.",
|
|
242
|
+
},
|
|
243
|
+
}
|
|
244
|
+
: tool.append
|
|
245
|
+
? {
|
|
246
|
+
text: {
|
|
247
|
+
type: "string",
|
|
248
|
+
minLength: 1,
|
|
249
|
+
maxLength: MAX_NOTE_LENGTH,
|
|
250
|
+
},
|
|
251
|
+
}
|
|
252
|
+
: {},
|
|
253
|
+
required: tool.name === "rl_task_pause"
|
|
254
|
+
? ["summary", "changes", "validation", "next"]
|
|
255
|
+
: tool.append
|
|
256
|
+
? ["text"]
|
|
257
|
+
: [],
|
|
258
|
+
additionalProperties: false,
|
|
259
|
+
},
|
|
260
|
+
annotations: {
|
|
261
|
+
readOnlyHint: !tool.append &&
|
|
262
|
+
tool.name !== "rl_session_save" &&
|
|
263
|
+
tool.name !== "rl_status" &&
|
|
264
|
+
tool.name !== "rl_task_pause",
|
|
265
|
+
destructiveHint: false,
|
|
266
|
+
openWorldHint: false,
|
|
267
|
+
},
|
|
268
|
+
})),
|
|
269
|
+
...WORKFLOW_TOOLS.map((tool) => ({
|
|
270
|
+
name: tool.name,
|
|
271
|
+
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
|
+
},
|
|
285
|
+
annotations: {
|
|
286
|
+
readOnlyHint: tool.readOnly,
|
|
287
|
+
destructiveHint: false,
|
|
288
|
+
openWorldHint: false,
|
|
289
|
+
},
|
|
290
|
+
})),
|
|
291
|
+
],
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
else if (raw.method === "tools/call") {
|
|
295
|
+
if (typeof params.name !== "string" ||
|
|
296
|
+
(params.arguments !== undefined && !isJsonObject(params.arguments)))
|
|
297
|
+
throw new Error("Invalid tool call");
|
|
298
|
+
const tool = TOOLS.find((item) => item.name === params.name);
|
|
299
|
+
const workflowTool = WORKFLOW_TOOLS.find((item) => item.name === params.name);
|
|
300
|
+
if (!tool && !workflowTool)
|
|
301
|
+
throw new Error(`Unknown tool: ${params.name}`);
|
|
302
|
+
try {
|
|
303
|
+
const text = workflowTool
|
|
304
|
+
? JSON.stringify(await callWorkflowTool(store, workflowTool.name, params.arguments ?? {}))
|
|
305
|
+
: await callTool(store, tool, params.arguments ?? {});
|
|
306
|
+
result = { content: [{ type: "text", text }] };
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
result = {
|
|
310
|
+
isError: true,
|
|
311
|
+
content: [{ type: "text", text: message(error) }],
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
code = ERROR.method;
|
|
317
|
+
throw new Error("Method not found");
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
process.stdout.write(JSON.stringify({ jsonrpc: RPC_VERSION, id, result }) + "\n");
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
process.stdout.write(JSON.stringify({
|
|
324
|
+
jsonrpc: RPC_VERSION,
|
|
325
|
+
id,
|
|
326
|
+
error: { code, message: message(error) },
|
|
327
|
+
}) + "\n");
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return 0;
|
|
331
|
+
}
|
|
332
|
+
export async function main(argv) {
|
|
333
|
+
const { values, positionals } = args(argv, ["repo", "state-dir"], []);
|
|
334
|
+
arity(positionals, 0);
|
|
335
|
+
return serveMcp(new Store(required(values, "repo"), required(values, "state-dir")));
|
|
336
|
+
}
|