agent-relay-runner 0.10.19 → 0.10.21
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 +2 -2
- package/plugins/claude/.claude-plugin/plugin.json +4 -1
- package/plugins/claude/hooks/hooks.json +114 -0
- package/plugins/claude/hooks/permission-request.sh +20 -0
- package/plugins/claude/hooks/post-compact.sh +5 -0
- package/plugins/claude/hooks/pre-compact.sh +5 -0
- package/plugins/claude/hooks/relay-status.sh +66 -0
- package/plugins/claude/hooks/session-end.sh +16 -3
- package/plugins/claude/hooks/session-start.sh +14 -0
- package/plugins/claude/hooks/stop-failure.sh +15 -0
- package/plugins/claude/hooks/stop.sh +13 -3
- package/plugins/claude/hooks/subagent-start.sh +12 -0
- package/plugins/claude/hooks/subagent-stop.sh +12 -0
- package/plugins/claude/hooks/user-prompt-submit.sh +2 -3
- package/plugins/claude/monitors/relay-monitor.ts +16 -4
- package/plugins/claude/skills/react/SKILL.md +18 -0
- package/plugins/claude/skills/read-message/SKILL.md +24 -0
- package/plugins/claude/skills/reply/SKILL.md +7 -3
- package/plugins/codex/skills/guide/SKILL.md +15 -0
- package/plugins/codex/skills/react/SKILL.md +17 -0
- package/plugins/codex/skills/read-message/SKILL.md +23 -0
- package/plugins/codex/skills/reply/SKILL.md +6 -2
- package/src/adapter.ts +207 -6
- package/src/adapters/claude-delivery.ts +108 -0
- package/src/adapters/claude.ts +232 -31
- package/src/adapters/codex-client.ts +27 -1
- package/src/adapters/codex.ts +635 -26
- package/src/attachment-cache.ts +190 -0
- package/src/claim-tracker.ts +48 -5
- package/src/control-server.ts +193 -6
- package/src/index.ts +203 -6
- package/src/profile-home.ts +85 -0
- package/src/profile-projection.ts +146 -0
- package/src/relay-instructions.ts +25 -0
- package/src/runner.ts +811 -40
- package/src/version.ts +39 -0
package/src/adapters/claude.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
2
4
|
import type { Message } from "agent-relay-sdk";
|
|
3
|
-
import type
|
|
5
|
+
import { profileAllowsRelayFeature, type ManagedProcess, type ProviderAdapter, type ProviderConfig, type ProviderStatusUpdate, type RunnerSpawnConfig, type SemanticStatus, type SpawnArgs } from "../adapter";
|
|
6
|
+
import { prepareClaudeProfileHome, profileUsesHostProviderGlobals } from "../profile-home";
|
|
4
7
|
|
|
5
8
|
export class ClaudeAdapter implements ProviderAdapter {
|
|
6
9
|
readonly provider = "claude";
|
|
7
|
-
private statusCb: (status:
|
|
10
|
+
private statusCb: (status: ProviderStatusUpdate) => void = () => {};
|
|
8
11
|
private tmuxWatcher?: Timer;
|
|
9
12
|
|
|
10
|
-
onStatusChange(cb: (status:
|
|
13
|
+
onStatusChange(cb: (status: ProviderStatusUpdate) => void): void {
|
|
11
14
|
this.statusCb = cb;
|
|
12
15
|
}
|
|
13
16
|
|
|
@@ -31,44 +34,83 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
31
34
|
|
|
32
35
|
async shutdown(process: ManagedProcess, opts: { graceful: boolean; timeoutMs: number }): Promise<void> {
|
|
33
36
|
const tmuxSession = process.meta?.tmuxSession as string | undefined;
|
|
37
|
+
const tmuxSocket = process.meta?.tmuxSocket as string | undefined;
|
|
34
38
|
if (tmuxSession) {
|
|
35
|
-
await this.shutdownTmux(tmuxSession, opts);
|
|
39
|
+
await this.shutdownTmux(tmuxSession, opts, tmuxSocket);
|
|
36
40
|
return;
|
|
37
41
|
}
|
|
38
42
|
await terminateProcess(process, opts);
|
|
39
43
|
}
|
|
40
44
|
|
|
45
|
+
async compact(process: ManagedProcess): Promise<Record<string, unknown>> {
|
|
46
|
+
const session = process.meta?.tmuxSession as string | undefined;
|
|
47
|
+
const socket = process.meta?.tmuxSocket as string | undefined;
|
|
48
|
+
if (!session || !tmuxHasSession(session, socket)) throw new Error("no active tmux session for compact");
|
|
49
|
+
await submitTextToTmux(session, "/compact", socket);
|
|
50
|
+
return { method: "tmux-inject", command: "/compact" };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async clearContext(process: ManagedProcess): Promise<Record<string, unknown>> {
|
|
54
|
+
const session = process.meta?.tmuxSession as string | undefined;
|
|
55
|
+
const socket = process.meta?.tmuxSocket as string | undefined;
|
|
56
|
+
if (!session || !tmuxHasSession(session, socket)) throw new Error("no active tmux session for clear");
|
|
57
|
+
await submitTextToTmux(session, "/clear", socket);
|
|
58
|
+
return { method: "tmux-inject", command: "/clear" };
|
|
59
|
+
}
|
|
60
|
+
|
|
41
61
|
async deliver(_process: ManagedProcess, messages: Message[]): Promise<void> {
|
|
42
62
|
const monitor = _process.meta?.monitor as { deliver?(messages: Message[]): Promise<number[]> } | undefined;
|
|
43
63
|
if (!monitor?.deliver) throw new Error("Claude monitor delivery is unavailable");
|
|
44
64
|
await monitor.deliver(messages);
|
|
45
65
|
}
|
|
46
66
|
|
|
67
|
+
async deliverInitialPrompt(process: ManagedProcess, prompt: string): Promise<void> {
|
|
68
|
+
const session = process.meta?.tmuxSession as string | undefined;
|
|
69
|
+
const socket = process.meta?.tmuxSocket as string | undefined;
|
|
70
|
+
if (!session || !tmuxHasSession(session, socket)) throw new Error("no active tmux session for initial prompt");
|
|
71
|
+
await waitForClaudeInputReady(session, CLAUDE_TMUX_READY_TIMEOUT_MS, socket);
|
|
72
|
+
await submitTextToTmux(session, prompt, socket);
|
|
73
|
+
}
|
|
74
|
+
|
|
47
75
|
buildSpawnArgs(config: RunnerSpawnConfig, providerConfig: ProviderConfig): SpawnArgs {
|
|
76
|
+
const profileHome = prepareClaudeProfileHome(config);
|
|
48
77
|
const pluginRoot = resolve(import.meta.dir, "../../plugins/claude");
|
|
49
|
-
const
|
|
78
|
+
const defaultArgs = profileUsesHostProviderGlobals(config) ? providerConfig.defaultArgs : [];
|
|
79
|
+
const pluginDirs = profileAllowsRelayFeature(config, "plugins") ? [...new Set([pluginRoot, ...providerConfig.pluginDirs])] : [];
|
|
50
80
|
const isClaudeRig = /claude-rig/.test(providerConfig.command);
|
|
51
|
-
const
|
|
81
|
+
const bypassRigDefaults = config.headless && isClaudeRig && config.approvalMode !== "open";
|
|
82
|
+
const rigPrefix = isClaudeRig && !bypassRigDefaults && config.rig ? ["launch", config.rig] : [];
|
|
83
|
+
const command = bypassRigDefaults || (isClaudeRig && !config.rig && !findClaudeRigRC(config.cwd))
|
|
84
|
+
? "claude"
|
|
85
|
+
: providerConfig.command;
|
|
86
|
+
const providerArgs = config.headless
|
|
87
|
+
? claudeLaunchArgs(defaultArgs, config.providerArgs, config.approvalMode)
|
|
88
|
+
: [...defaultArgs, ...config.providerArgs];
|
|
52
89
|
const args = [
|
|
53
90
|
...rigPrefix,
|
|
54
91
|
...pluginDirs.flatMap((dir) => ["--plugin-dir", dir]),
|
|
55
|
-
...
|
|
56
|
-
...config.
|
|
92
|
+
...(profileAllowsRelayFeature(config, "statusLine") ? sessionStatusLineSettingsArgs(defaultArgs, config.providerArgs) : []),
|
|
93
|
+
...(config.systemPromptAppend ? ["--append-system-prompt", config.systemPromptAppend] : []),
|
|
94
|
+
...providerArgs,
|
|
57
95
|
];
|
|
58
|
-
if (config.
|
|
96
|
+
if (config.model) args.push("--model", config.model);
|
|
97
|
+
if (config.prompt && !config.headless) args.push(String(config.prompt));
|
|
59
98
|
return {
|
|
60
|
-
command
|
|
99
|
+
command,
|
|
61
100
|
args,
|
|
62
101
|
cwd: config.cwd,
|
|
63
102
|
env: {
|
|
64
103
|
...config.env,
|
|
104
|
+
...(profileHome ? { CLAUDE_CONFIG_DIR: profileHome.path } : {}),
|
|
65
105
|
AGENT_RELAY_PROVIDER: "claude",
|
|
106
|
+
...(config.effort ? { CLAUDE_CODE_EFFORT_LEVEL: config.effort } : {}),
|
|
66
107
|
},
|
|
67
108
|
};
|
|
68
109
|
}
|
|
69
110
|
|
|
70
|
-
buildTmuxArgs(config: RunnerSpawnConfig, spawnArgs: SpawnArgs): { sessionName: string; args: string[] } {
|
|
71
|
-
const sessionName = tmuxSessionName(config.providerConfig.headless.tmuxPrefix, config.instanceId, config.label);
|
|
111
|
+
buildTmuxArgs(config: RunnerSpawnConfig, spawnArgs: SpawnArgs): { sessionName: string; socketName: string; args: string[] } {
|
|
112
|
+
const sessionName = config.tmuxSession || tmuxSessionName(config.providerConfig.headless.tmuxPrefix, config.instanceId, config.label);
|
|
113
|
+
const socketName = tmuxSocketName(sessionName);
|
|
72
114
|
const shellCmd = [spawnArgs.command, ...spawnArgs.args].map(shellQuote).join(" ");
|
|
73
115
|
const tmuxArgs = ["new-session", "-d", "-s", sessionName, "-x", "200", "-y", "50"];
|
|
74
116
|
|
|
@@ -80,17 +122,17 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
80
122
|
}
|
|
81
123
|
|
|
82
124
|
tmuxArgs.push("-c", spawnArgs.cwd, shellCmd);
|
|
83
|
-
return { sessionName, args: tmuxArgs };
|
|
125
|
+
return { sessionName, socketName, args: tmuxArgs };
|
|
84
126
|
}
|
|
85
127
|
|
|
86
128
|
private async spawnHeadless(config: RunnerSpawnConfig, spawnArgs: SpawnArgs): Promise<ManagedProcess> {
|
|
87
|
-
const { sessionName, args: tmuxArgs } = this.buildTmuxArgs(config, spawnArgs);
|
|
129
|
+
const { sessionName, socketName, args: tmuxArgs } = this.buildTmuxArgs(config, spawnArgs);
|
|
88
130
|
|
|
89
|
-
Bun.spawnSync(
|
|
131
|
+
Bun.spawnSync(tmuxCommand(socketName, "kill-session", "-t", sessionName), {
|
|
90
132
|
stdin: "ignore", stdout: "ignore", stderr: "ignore",
|
|
91
133
|
});
|
|
92
134
|
|
|
93
|
-
const result = Bun.spawnSync(
|
|
135
|
+
const result = Bun.spawnSync(tmuxCommand(socketName, ...tmuxArgs), {
|
|
94
136
|
stdin: "ignore",
|
|
95
137
|
stdout: "pipe",
|
|
96
138
|
stderr: "pipe",
|
|
@@ -101,7 +143,15 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
101
143
|
throw new Error(`tmux session creation failed: ${stderr || `exit code ${result.exitCode}`}`);
|
|
102
144
|
}
|
|
103
145
|
|
|
104
|
-
this.watchTmuxSession(sessionName);
|
|
146
|
+
this.watchTmuxSession(sessionName, socketName);
|
|
147
|
+
const logFile = spawnArgs.env.AGENT_RELAY_LOG_FILE;
|
|
148
|
+
if (logFile) {
|
|
149
|
+
Bun.spawnSync(tmuxCommand(socketName, "pipe-pane", "-o", "-t", sessionName, `cat >> ${shellQuote(logFile)}`), {
|
|
150
|
+
stdin: "ignore",
|
|
151
|
+
stdout: "ignore",
|
|
152
|
+
stderr: "ignore",
|
|
153
|
+
});
|
|
154
|
+
}
|
|
105
155
|
|
|
106
156
|
return {
|
|
107
157
|
pid: undefined,
|
|
@@ -109,14 +159,15 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
109
159
|
meta: {
|
|
110
160
|
monitor: config.monitor,
|
|
111
161
|
tmuxSession: sessionName,
|
|
162
|
+
tmuxSocket: socketName,
|
|
112
163
|
},
|
|
113
164
|
};
|
|
114
165
|
}
|
|
115
166
|
|
|
116
|
-
private watchTmuxSession(sessionName: string): void {
|
|
167
|
+
private watchTmuxSession(sessionName: string, socketName?: string): void {
|
|
117
168
|
if (this.tmuxWatcher) clearInterval(this.tmuxWatcher);
|
|
118
169
|
this.tmuxWatcher = setInterval(() => {
|
|
119
|
-
if (!tmuxHasSession(sessionName)) {
|
|
170
|
+
if (!tmuxHasSession(sessionName, socketName)) {
|
|
120
171
|
clearInterval(this.tmuxWatcher!);
|
|
121
172
|
this.tmuxWatcher = undefined;
|
|
122
173
|
this.statusCb("offline");
|
|
@@ -124,42 +175,180 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
124
175
|
}, 2000);
|
|
125
176
|
}
|
|
126
177
|
|
|
127
|
-
private async shutdownTmux(sessionName: string, opts: { graceful: boolean; timeoutMs: number }): Promise<void> {
|
|
178
|
+
private async shutdownTmux(sessionName: string, opts: { graceful: boolean; timeoutMs: number }, socketName?: string): Promise<void> {
|
|
128
179
|
if (this.tmuxWatcher) {
|
|
129
180
|
clearInterval(this.tmuxWatcher);
|
|
130
181
|
this.tmuxWatcher = undefined;
|
|
131
182
|
}
|
|
132
183
|
|
|
133
|
-
if (opts.graceful && tmuxHasSession(sessionName)) {
|
|
134
|
-
|
|
135
|
-
stdin: "ignore", stdout: "ignore", stderr: "ignore",
|
|
136
|
-
});
|
|
184
|
+
if (opts.graceful && tmuxHasSession(sessionName, socketName)) {
|
|
185
|
+
await submitTextToTmux(sessionName, CLAUDE_EXIT_COMMAND, socketName);
|
|
137
186
|
|
|
138
|
-
const deadline = Date.now() + opts.timeoutMs;
|
|
187
|
+
const deadline = Date.now() + claudeShutdownGraceMs(opts.timeoutMs);
|
|
139
188
|
while (Date.now() < deadline) {
|
|
140
|
-
if (!tmuxHasSession(sessionName)) return;
|
|
141
|
-
await Bun.sleep(
|
|
189
|
+
if (!tmuxHasSession(sessionName, socketName)) return;
|
|
190
|
+
await Bun.sleep(200);
|
|
142
191
|
}
|
|
143
192
|
}
|
|
144
193
|
|
|
145
|
-
Bun.spawnSync(
|
|
194
|
+
Bun.spawnSync(tmuxCommand(socketName, "kill-session", "-t", sessionName), {
|
|
146
195
|
stdin: "ignore", stdout: "ignore", stderr: "ignore",
|
|
147
196
|
});
|
|
148
197
|
}
|
|
149
198
|
}
|
|
150
199
|
|
|
200
|
+
export const CLAUDE_EXIT_COMMAND = "/exit";
|
|
201
|
+
export const CLAUDE_TMUX_SUBMIT_DELAY_MS = 250;
|
|
202
|
+
export const CLAUDE_TMUX_SUBMIT_KEY = "C-m";
|
|
203
|
+
export const CLAUDE_TMUX_READY_TIMEOUT_MS = 10_000;
|
|
204
|
+
|
|
205
|
+
export function claudeShutdownGraceMs(timeoutMs: number): number {
|
|
206
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return 10_000;
|
|
207
|
+
return Math.min(timeoutMs, 10_000);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function sessionStatusLineSettingsArgs(...argLists: string[][]): string[] {
|
|
211
|
+
if (argLists.some(hasSettingsArg)) return [];
|
|
212
|
+
return ["--settings", JSON.stringify({
|
|
213
|
+
statusLine: {
|
|
214
|
+
type: "command",
|
|
215
|
+
command: "agent-relay context-probe --wrap",
|
|
216
|
+
refreshInterval: 30,
|
|
217
|
+
},
|
|
218
|
+
})];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function claudeLaunchArgs(defaultArgs: string[], providerArgs: string[], approvalMode?: string): string[] {
|
|
222
|
+
return [
|
|
223
|
+
...stripClaudePermissionArgs(defaultArgs),
|
|
224
|
+
...stripClaudePermissionArgs(providerArgs),
|
|
225
|
+
...claudePermissionModeArgs(approvalMode),
|
|
226
|
+
];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function claudePermissionModeArgs(approvalMode?: string): string[] {
|
|
230
|
+
if (approvalMode === "open") return ["--dangerously-skip-permissions"];
|
|
231
|
+
if (approvalMode === "read-only") return [
|
|
232
|
+
"--permission-mode",
|
|
233
|
+
"dontAsk",
|
|
234
|
+
"--allowedTools",
|
|
235
|
+
"Read,Grep,Glob,LS,Bash(agent-relay *)",
|
|
236
|
+
];
|
|
237
|
+
return ["--permission-mode", "default"];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function stripClaudePermissionArgs(args: string[]): string[] {
|
|
241
|
+
const result: string[] = [];
|
|
242
|
+
for (let i = 0; i < args.length; i++) {
|
|
243
|
+
const arg = args[i];
|
|
244
|
+
if (!arg) continue;
|
|
245
|
+
if (
|
|
246
|
+
arg === "--dangerously-skip-permissions" ||
|
|
247
|
+
arg === "--allow-dangerously-skip-permissions" ||
|
|
248
|
+
arg?.startsWith("--permission-mode=")
|
|
249
|
+
) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (arg === "--permission-mode") {
|
|
253
|
+
i++;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
result.push(arg);
|
|
257
|
+
}
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function hasSettingsArg(args: string[]): boolean {
|
|
262
|
+
return args.some((arg) => arg === "--settings" || arg.startsWith("--settings="));
|
|
263
|
+
}
|
|
264
|
+
|
|
151
265
|
export function tmuxSessionName(prefix: string, instanceId: string, label?: string): string {
|
|
152
266
|
if (label) return `${prefix}-${label.replace(/[^a-zA-Z0-9._-]/g, "-").toLowerCase()}`;
|
|
153
267
|
return `${prefix}-${instanceId.slice(0, 8)}`;
|
|
154
268
|
}
|
|
155
269
|
|
|
156
|
-
export function
|
|
157
|
-
|
|
270
|
+
export function tmuxSocketName(sessionName: string): string {
|
|
271
|
+
return `agent-relay-${sessionName}`.replace(/[^a-zA-Z0-9._-]/g, "-").toLowerCase();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function tmuxCommand(socketName: string | undefined, ...args: string[]): string[] {
|
|
275
|
+
return socketName ? ["tmux", "-L", socketName, ...args] : ["tmux", ...args];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function tmuxHasSession(sessionName: string, socketName?: string): boolean {
|
|
279
|
+
const result = Bun.spawnSync(tmuxCommand(socketName, "has-session", "-t", sessionName), {
|
|
158
280
|
stdin: "ignore", stdout: "ignore", stderr: "ignore",
|
|
159
281
|
});
|
|
160
282
|
return result.exitCode === 0;
|
|
161
283
|
}
|
|
162
284
|
|
|
285
|
+
function captureTmuxPane(sessionName: string, socketName?: string): string {
|
|
286
|
+
const result = Bun.spawnSync(tmuxCommand(socketName, "capture-pane", "-p", "-t", sessionName, "-S", "-80"), {
|
|
287
|
+
stdin: "ignore", stdout: "pipe", stderr: "pipe",
|
|
288
|
+
});
|
|
289
|
+
if (result.exitCode !== 0) {
|
|
290
|
+
const stderr = result.stderr.toString().trim();
|
|
291
|
+
throw new Error(`tmux capture-pane failed: ${stderr || `exit code ${result.exitCode}`}`);
|
|
292
|
+
}
|
|
293
|
+
return result.stdout.toString();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function claudePaneLooksReady(text: string): boolean {
|
|
297
|
+
return text.includes("Claude Code")
|
|
298
|
+
&& (
|
|
299
|
+
text.includes("bypass permissions")
|
|
300
|
+
|| text.includes("/effort")
|
|
301
|
+
|| text.includes("What's new")
|
|
302
|
+
|| text.includes("Welcome back")
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function waitForClaudeInputReady(sessionName: string, timeoutMs = CLAUDE_TMUX_READY_TIMEOUT_MS, socketName?: string): Promise<void> {
|
|
307
|
+
const deadline = Date.now() + timeoutMs;
|
|
308
|
+
while (Date.now() < deadline) {
|
|
309
|
+
if (!tmuxHasSession(sessionName, socketName)) throw new Error("tmux session exited before Claude became ready");
|
|
310
|
+
if (claudePaneLooksReady(captureTmuxPane(sessionName, socketName))) return;
|
|
311
|
+
await Bun.sleep(200);
|
|
312
|
+
}
|
|
313
|
+
throw new Error(`Claude input was not ready within ${timeoutMs}ms`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function pasteTextIntoTmux(sessionName: string, text: string, socketName?: string): void {
|
|
317
|
+
const bufferName = `agent-relay-${crypto.randomUUID()}`;
|
|
318
|
+
const setResult = Bun.spawnSync(tmuxCommand(socketName, "set-buffer", "-b", bufferName, text), {
|
|
319
|
+
stdin: "ignore", stdout: "ignore", stderr: "pipe",
|
|
320
|
+
});
|
|
321
|
+
if (setResult.exitCode !== 0) {
|
|
322
|
+
const stderr = setResult.stderr.toString().trim();
|
|
323
|
+
throw new Error(`tmux set-buffer failed: ${stderr || `exit code ${setResult.exitCode}`}`);
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
const pasteResult = Bun.spawnSync(tmuxCommand(socketName, "paste-buffer", "-b", bufferName, "-t", sessionName), {
|
|
327
|
+
stdin: "ignore", stdout: "ignore", stderr: "pipe",
|
|
328
|
+
});
|
|
329
|
+
if (pasteResult.exitCode !== 0) {
|
|
330
|
+
const stderr = pasteResult.stderr.toString().trim();
|
|
331
|
+
throw new Error(`tmux paste-buffer failed: ${stderr || `exit code ${pasteResult.exitCode}`}`);
|
|
332
|
+
}
|
|
333
|
+
} finally {
|
|
334
|
+
Bun.spawnSync(tmuxCommand(socketName, "delete-buffer", "-b", bufferName), {
|
|
335
|
+
stdin: "ignore", stdout: "ignore", stderr: "ignore",
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function submitTextToTmux(sessionName: string, text: string, socketName?: string): Promise<void> {
|
|
341
|
+
pasteTextIntoTmux(sessionName, text, socketName);
|
|
342
|
+
await Bun.sleep(CLAUDE_TMUX_SUBMIT_DELAY_MS);
|
|
343
|
+
const result = Bun.spawnSync(tmuxCommand(socketName, "send-keys", "-t", sessionName, CLAUDE_TMUX_SUBMIT_KEY), {
|
|
344
|
+
stdin: "ignore", stdout: "ignore", stderr: "pipe",
|
|
345
|
+
});
|
|
346
|
+
if (result.exitCode !== 0) {
|
|
347
|
+
const stderr = result.stderr.toString().trim();
|
|
348
|
+
throw new Error(`tmux submit failed: ${stderr || `exit code ${result.exitCode}`}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
163
352
|
export function tmuxEnvKeys(env: Record<string, string>, providerEnv: Record<string, string>): string[] {
|
|
164
353
|
const keys = new Set<string>();
|
|
165
354
|
for (const key of Object.keys(env)) {
|
|
@@ -179,6 +368,18 @@ export function shellQuote(arg: string): string {
|
|
|
179
368
|
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
180
369
|
}
|
|
181
370
|
|
|
371
|
+
export function findClaudeRigRC(cwd: string): string | null {
|
|
372
|
+
const home = homedir();
|
|
373
|
+
let dir = resolve(cwd);
|
|
374
|
+
while (true) {
|
|
375
|
+
const rc = join(dir, ".claude-rig");
|
|
376
|
+
if (existsSync(rc)) return rc;
|
|
377
|
+
const parent = resolve(dir, "..");
|
|
378
|
+
if (parent === dir || dir === home) return null;
|
|
379
|
+
dir = parent;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
182
383
|
async function terminateProcess(process: ManagedProcess, opts: { graceful: boolean; timeoutMs: number }): Promise<void> {
|
|
183
384
|
const proc = process.process;
|
|
184
385
|
if (!proc) return;
|
|
@@ -168,6 +168,10 @@ export class CodexAppClient {
|
|
|
168
168
|
return this.request<ThreadLoadedListResponse>("thread/loaded/list", { limit });
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
async threadCompactStart(threadId: string): Promise<Record<string, never>> {
|
|
172
|
+
return this.request<Record<string, never>>("thread/compact/start", { threadId });
|
|
173
|
+
}
|
|
174
|
+
|
|
171
175
|
async turnStart(threadId: string, text: string): Promise<TurnStartResponse> {
|
|
172
176
|
return this.request<TurnStartResponse>("turn/start", {
|
|
173
177
|
threadId,
|
|
@@ -187,6 +191,20 @@ export class CodexAppClient {
|
|
|
187
191
|
return this.request<Record<string, never>>("turn/interrupt", { threadId, turnId });
|
|
188
192
|
}
|
|
189
193
|
|
|
194
|
+
respondToServerRequest(id: JsonRpcId, result: unknown): void {
|
|
195
|
+
if (!this.connected) {
|
|
196
|
+
throw new Error("websocket not connected");
|
|
197
|
+
}
|
|
198
|
+
this.ws.send(JSON.stringify({ id, result }));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
rejectServerRequest(id: JsonRpcId, code: number, message: string, data?: unknown): void {
|
|
202
|
+
if (!this.connected) {
|
|
203
|
+
throw new Error("websocket not connected");
|
|
204
|
+
}
|
|
205
|
+
this.ws.send(JSON.stringify({ id, error: { code, message, ...(data !== undefined ? { data } : {}) } }));
|
|
206
|
+
}
|
|
207
|
+
|
|
190
208
|
private async request<T = unknown>(method: string, params?: unknown): Promise<T> {
|
|
191
209
|
if (!this.connected) {
|
|
192
210
|
throw new Error("websocket not connected");
|
|
@@ -201,7 +219,15 @@ export class CodexAppClient {
|
|
|
201
219
|
}
|
|
202
220
|
|
|
203
221
|
private handleMessage(raw: string): void {
|
|
204
|
-
|
|
222
|
+
// The frame comes from an external process over a WebSocket; a partial or
|
|
223
|
+
// malformed frame must not throw out of onmessage and drop the connection.
|
|
224
|
+
let parsed: JsonRpcRequest | JsonRpcResponse | JsonRpcNotification;
|
|
225
|
+
try {
|
|
226
|
+
parsed = JSON.parse(raw) as JsonRpcRequest | JsonRpcResponse | JsonRpcNotification;
|
|
227
|
+
} catch {
|
|
228
|
+
this.log(`dropped malformed frame (${raw.length} bytes)`);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
205
231
|
|
|
206
232
|
if ("id" in parsed && ("result" in parsed || "error" in parsed)) {
|
|
207
233
|
const pending = this.pending.get(parsed.id);
|