agent-anywhere-gateway 0.1.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 +46 -0
- package/bin/agent-anywhere-gateway.js +13 -0
- package/config/cloudflare.gateway.env.example +9 -0
- package/package.json +29 -0
- package/src/adapters/local-agent-adapter.js +131 -0
- package/src/gateway/client.js +422 -0
- package/src/gateway/main.js +224 -0
- package/src/gateway/providers.js +28 -0
- package/src/gateway/runner.js +337 -0
- package/src/gateway.js +7 -0
- package/src/lib/capabilities.js +1 -0
- package/src/lib/local-discovery.js +322 -0
- package/src/lib/path-policy.js +1 -0
- package/src/runtimes/claude-code-headless-runtime.js +547 -0
- package/src/runtimes/claude-code-runtime.js +984 -0
- package/src/runtimes/codex-app-server-client.js +157 -0
- package/src/runtimes/codex-app-server-runtime.js +790 -0
- package/src/runtimes/codex-runtime.js +418 -0
- package/src/runtimes/mock-runtime.js +140 -0
- package/src/shared/capabilities.js +175 -0
- package/src/shared/gateway-protocol.js +26 -0
- package/src/shared/http-utils.js +78 -0
- package/src/shared/image-attachments.js +269 -0
- package/src/shared/path-policy.js +110 -0
- package/src/shared/project-files.js +119 -0
- package/src/shared/providers.js +27 -0
- package/src/shared/runtime-environment.js +32 -0
- package/src/shared/websocket.js +258 -0
|
@@ -0,0 +1,984 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const crypto = require("node:crypto");
|
|
3
|
+
const os = require("node:os");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const { spawnSync } = require("node:child_process");
|
|
6
|
+
const { buildCapabilities } = require("../shared/capabilities");
|
|
7
|
+
const { parseAllowedRoots, resolveProjectPath } = require("../shared/path-policy");
|
|
8
|
+
const {
|
|
9
|
+
normalizeClaudeEffort,
|
|
10
|
+
resolveClaudeExecutable
|
|
11
|
+
} = require("./claude-code-headless-runtime");
|
|
12
|
+
|
|
13
|
+
const DEFAULT_CLAUDE_MODELS = [
|
|
14
|
+
"sonnet",
|
|
15
|
+
"opus",
|
|
16
|
+
"claude-sonnet-4-6",
|
|
17
|
+
"claude-opus-4-5"
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const DEFAULT_READY_TIMEOUT_MS = 45_000;
|
|
21
|
+
const DEFAULT_TURN_TIMEOUT_MS = 5 * 60_000;
|
|
22
|
+
const TOOL_PROGRESS_NAMES = new Set(["Read", "Grep", "Glob", "LS"]);
|
|
23
|
+
const CLAUDE_SESSION_ID_SUPPORT_CACHE = new Map();
|
|
24
|
+
|
|
25
|
+
function remoteControlSandboxFlag(settings = {}) {
|
|
26
|
+
return settings.mode === "full-access" ? "--no-sandbox" : "--sandbox";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function claudePermissionMode(settings = {}) {
|
|
30
|
+
if (settings.mode === "full-access") return "bypassPermissions";
|
|
31
|
+
if (settings.mode === "auto-review") return "auto";
|
|
32
|
+
if (settings.approval_policy === "never") return "dontAsk";
|
|
33
|
+
return "default";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function remoteControlSessionName({ session = {}, project = {}, message = "" } = {}) {
|
|
37
|
+
if (session.title) {
|
|
38
|
+
return String(session.title);
|
|
39
|
+
}
|
|
40
|
+
const projectName = project.path ? project.path.split(/[\\/]/).filter(Boolean).pop() : "";
|
|
41
|
+
if (projectName) {
|
|
42
|
+
return `Agent Anywhere - ${projectName}`;
|
|
43
|
+
}
|
|
44
|
+
const text = String(message || "").trim();
|
|
45
|
+
if (text) {
|
|
46
|
+
return `Agent Anywhere - ${text.slice(0, 48)}`;
|
|
47
|
+
}
|
|
48
|
+
return "Agent Anywhere";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function buildClaudeRemoteControlArgs({
|
|
52
|
+
session,
|
|
53
|
+
project,
|
|
54
|
+
message,
|
|
55
|
+
settings = {},
|
|
56
|
+
sessionId,
|
|
57
|
+
useSessionId = false
|
|
58
|
+
} = {}) {
|
|
59
|
+
const args = [
|
|
60
|
+
"--remote-control",
|
|
61
|
+
remoteControlSessionName({ session, project, message }),
|
|
62
|
+
"--permission-mode",
|
|
63
|
+
claudePermissionMode(settings)
|
|
64
|
+
];
|
|
65
|
+
if (settings.model) {
|
|
66
|
+
args.push("--model", settings.model);
|
|
67
|
+
}
|
|
68
|
+
if (settings.reasoning_effort) {
|
|
69
|
+
args.push("--effort", normalizeClaudeEffort(settings.reasoning_effort));
|
|
70
|
+
}
|
|
71
|
+
if (useSessionId && sessionId) {
|
|
72
|
+
args.push("--session-id", sessionId);
|
|
73
|
+
}
|
|
74
|
+
return args;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function stripAnsi(text) {
|
|
78
|
+
return String(text || "")
|
|
79
|
+
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
|
|
80
|
+
.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "")
|
|
81
|
+
.replace(/\x1b[>=<][ -~]?/g, "");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function extractRemoteControlUrl(text) {
|
|
85
|
+
const match = stripAnsi(text).match(/https:\/\/claude\.ai\/code[^\s)'"]*/);
|
|
86
|
+
return match ? match[0] : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function shouldConfirmRemoteControl(text) {
|
|
90
|
+
return /Enable Remote Control\?\s*\(y\/n\)/i.test(stripAnsi(text));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function hasConcreteClaudeSessionId(session = {}) {
|
|
94
|
+
return Boolean(session.runtime_session_id) &&
|
|
95
|
+
!String(session.runtime_session_id).startsWith("claude-remote-control:");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function remoteControlRuntimeSessionId(session = {}, { generate = false } = {}) {
|
|
99
|
+
if (hasConcreteClaudeSessionId(session)) {
|
|
100
|
+
return session.runtime_session_id;
|
|
101
|
+
}
|
|
102
|
+
if (generate) {
|
|
103
|
+
return crypto.randomUUID();
|
|
104
|
+
}
|
|
105
|
+
return session.runtime_session_id || `claude-remote-control:${session.id || Date.now()}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function supportsRemoteControlSessionId(claudePath, {
|
|
109
|
+
env = process.env,
|
|
110
|
+
spawnSyncImpl = spawnSync
|
|
111
|
+
} = {}) {
|
|
112
|
+
if (env.AGENT_ANYWHERE_CLAUDE_DISABLE_SESSION_ID === "1") {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
if (CLAUDE_SESSION_ID_SUPPORT_CACHE.has(claudePath)) {
|
|
116
|
+
return CLAUDE_SESSION_ID_SUPPORT_CACHE.get(claudePath);
|
|
117
|
+
}
|
|
118
|
+
let supported = false;
|
|
119
|
+
try {
|
|
120
|
+
const result = spawnSyncImpl(claudePath, ["--help"], {
|
|
121
|
+
encoding: "utf8",
|
|
122
|
+
env,
|
|
123
|
+
timeout: 5000
|
|
124
|
+
});
|
|
125
|
+
const output = `${result.stdout || ""}\n${result.stderr || ""}`;
|
|
126
|
+
supported = /(?:^|\s)--session-id(?:\s|,|$)/.test(output) &&
|
|
127
|
+
/(?:^|\s)--remote-control(?:\s|,|$)/.test(output);
|
|
128
|
+
} catch {
|
|
129
|
+
supported = false;
|
|
130
|
+
}
|
|
131
|
+
CLAUDE_SESSION_ID_SUPPORT_CACHE.set(claudePath, supported);
|
|
132
|
+
return supported;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function commandOutput(result = {}) {
|
|
136
|
+
return stripAnsi(`${result.stdout || ""}\n${result.stderr || ""}`)
|
|
137
|
+
.split(/\r?\n/)
|
|
138
|
+
.map((line) => line.trim())
|
|
139
|
+
.filter(Boolean)
|
|
140
|
+
.join("\n");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function assertClaudeRemoteControlAvailable(claudePath, {
|
|
144
|
+
env = process.env,
|
|
145
|
+
spawnSyncImpl = spawnSync
|
|
146
|
+
} = {}) {
|
|
147
|
+
if (env.AGENT_ANYWHERE_CLAUDE_SKIP_REMOTE_CONTROL_CHECK === "1") {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
let result;
|
|
151
|
+
try {
|
|
152
|
+
result = spawnSyncImpl(claudePath, ["--help"], {
|
|
153
|
+
encoding: "utf8",
|
|
154
|
+
env,
|
|
155
|
+
timeout: 5000
|
|
156
|
+
});
|
|
157
|
+
} catch (cause) {
|
|
158
|
+
const error = new Error(`Claude Remote Control 预检失败:${cause?.message || cause}`);
|
|
159
|
+
error.cause = cause;
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
const output = commandOutput(result);
|
|
163
|
+
if (result.error || result.status !== 0 || result.signal) {
|
|
164
|
+
const hint = output || result.error?.message || `exit=${result.status ?? "unknown"} signal=${result.signal || "none"}`;
|
|
165
|
+
const error = new Error(
|
|
166
|
+
`Claude Remote Control 预检失败:${hint}\n请确认服务进程使用的 Claude CLI 可正常运行。`
|
|
167
|
+
);
|
|
168
|
+
error.details = {
|
|
169
|
+
status: result.status,
|
|
170
|
+
signal: result.signal || null,
|
|
171
|
+
output
|
|
172
|
+
};
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
if (!/(?:^|\s)--remote-control(?:\s|,|$)/.test(output)) {
|
|
176
|
+
const error = new Error(
|
|
177
|
+
"Claude Remote Control 不可用:当前 Claude CLI 不支持 --remote-control。\n请确认服务进程使用的是支持 Remote Control 的 Claude Code 版本。"
|
|
178
|
+
);
|
|
179
|
+
error.details = { output };
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function truthyEnv(value) {
|
|
185
|
+
return /^(1|true|yes|on)$/i.test(String(value || "").trim());
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function positiveInteger(value, fallback) {
|
|
189
|
+
const parsed = Number.parseInt(value, 10);
|
|
190
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function claudeConfigPath(env = process.env) {
|
|
194
|
+
return env.CLAUDE_CONFIG_PATH || path.join(os.homedir(), ".claude.json");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function ensureClaudeWorkspaceTrusted(projectPath, {
|
|
198
|
+
configPath = claudeConfigPath(),
|
|
199
|
+
env = process.env,
|
|
200
|
+
fsImpl = fs
|
|
201
|
+
} = {}) {
|
|
202
|
+
if (!truthyEnv(env.AGENT_ANYWHERE_CLAUDE_AUTO_TRUST) || !projectPath) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const allowedRoots = parseAllowedRoots(env.AGENT_ANYWHERE_ALLOWED_ROOTS);
|
|
207
|
+
const resolvedProjectPath = resolveProjectPath(projectPath, allowedRoots);
|
|
208
|
+
let config = {};
|
|
209
|
+
if (fsImpl.existsSync(configPath)) {
|
|
210
|
+
config = JSON.parse(fsImpl.readFileSync(configPath, "utf8"));
|
|
211
|
+
}
|
|
212
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
213
|
+
config = {};
|
|
214
|
+
}
|
|
215
|
+
if (!config.projects || typeof config.projects !== "object" || Array.isArray(config.projects)) {
|
|
216
|
+
config.projects = {};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const existing = config.projects[resolvedProjectPath] &&
|
|
220
|
+
typeof config.projects[resolvedProjectPath] === "object" &&
|
|
221
|
+
!Array.isArray(config.projects[resolvedProjectPath])
|
|
222
|
+
? config.projects[resolvedProjectPath]
|
|
223
|
+
: {};
|
|
224
|
+
if (existing.hasTrustDialogAccepted === true) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
config.projects[resolvedProjectPath] = {
|
|
229
|
+
allowedTools: [],
|
|
230
|
+
mcpContextUris: [],
|
|
231
|
+
mcpServers: {},
|
|
232
|
+
enabledMcpjsonServers: [],
|
|
233
|
+
disabledMcpjsonServers: [],
|
|
234
|
+
hasTrustDialogAccepted: true,
|
|
235
|
+
projectOnboardingSeenCount: 0,
|
|
236
|
+
hasClaudeMdExternalIncludesApproved: false,
|
|
237
|
+
hasClaudeMdExternalIncludesWarningShown: false,
|
|
238
|
+
...existing,
|
|
239
|
+
hasTrustDialogAccepted: true
|
|
240
|
+
};
|
|
241
|
+
fsImpl.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
242
|
+
fsImpl.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function claudeProjectKey(projectPath) {
|
|
247
|
+
return path.resolve(projectPath).split(path.sep).join("-") || "project";
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function claudeProjectDir(projectPath, homeDir = os.homedir()) {
|
|
251
|
+
return path.join(homeDir, ".claude", "projects", claudeProjectKey(projectPath));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function listTranscriptFiles(projectPath, { homeDir = os.homedir(), fsImpl = fs } = {}) {
|
|
255
|
+
const dir = claudeProjectDir(projectPath, homeDir);
|
|
256
|
+
if (!fsImpl.existsSync(dir)) {
|
|
257
|
+
return [];
|
|
258
|
+
}
|
|
259
|
+
return fsImpl.readdirSync(dir)
|
|
260
|
+
.filter((name) => name.endsWith(".jsonl"))
|
|
261
|
+
.map((name) => path.join(dir, name))
|
|
262
|
+
.sort((left, right) => fsImpl.statSync(right).mtimeMs - fsImpl.statSync(left).mtimeMs);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function readTranscript(filePath, { fsImpl = fs } = {}) {
|
|
266
|
+
if (!filePath || !fsImpl.existsSync(filePath)) {
|
|
267
|
+
return [];
|
|
268
|
+
}
|
|
269
|
+
return fsImpl.readFileSync(filePath, "utf8")
|
|
270
|
+
.split(/\r?\n/)
|
|
271
|
+
.filter(Boolean)
|
|
272
|
+
.map((line) => {
|
|
273
|
+
try {
|
|
274
|
+
return JSON.parse(line);
|
|
275
|
+
} catch {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
.filter(Boolean);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function transcriptFileSize(filePath, { fsImpl = fs } = {}) {
|
|
283
|
+
if (!filePath || !fsImpl.existsSync(filePath)) {
|
|
284
|
+
return 0;
|
|
285
|
+
}
|
|
286
|
+
return fsImpl.statSync(filePath).size;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function textFromClaudeContent(content) {
|
|
290
|
+
if (typeof content === "string") {
|
|
291
|
+
return content;
|
|
292
|
+
}
|
|
293
|
+
if (!Array.isArray(content)) {
|
|
294
|
+
return content?.text || "";
|
|
295
|
+
}
|
|
296
|
+
return content
|
|
297
|
+
.map((item) => typeof item === "string" ? item : item?.type === "text" ? item.text : "")
|
|
298
|
+
.filter(Boolean)
|
|
299
|
+
.join("\n");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function userPromptOf(event = {}) {
|
|
303
|
+
return textFromClaudeContent(event.message?.content).trim();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function assistantTextOf(event = {}) {
|
|
307
|
+
if (!event) {
|
|
308
|
+
return "";
|
|
309
|
+
}
|
|
310
|
+
if (event.type !== "assistant" || event.message?.role !== "assistant") {
|
|
311
|
+
return "";
|
|
312
|
+
}
|
|
313
|
+
return textFromClaudeContent(event.message.content).trim();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function isAssistantFinal(event = {}) {
|
|
317
|
+
const stopReason = event.message?.stop_reason;
|
|
318
|
+
return stopReason == null || stopReason === "end_turn" || stopReason === "stop_sequence";
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function eventTimeMs(event = {}) {
|
|
322
|
+
const time = Date.parse(event.timestamp || "");
|
|
323
|
+
return Number.isFinite(time) ? time : 0;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function findTranscriptState(projectPath, { afterMs = 0, prompt, homeDir = os.homedir(), fsImpl = fs } = {}) {
|
|
327
|
+
const promptText = prompt == null ? "" : String(prompt).trim();
|
|
328
|
+
for (const filePath of listTranscriptFiles(projectPath, { homeDir, fsImpl })) {
|
|
329
|
+
const events = readTranscript(filePath, { fsImpl });
|
|
330
|
+
const bridge = [...events].reverse().find((event) => (
|
|
331
|
+
event.type === "system" &&
|
|
332
|
+
event.subtype === "bridge_status" &&
|
|
333
|
+
event.url &&
|
|
334
|
+
eventTimeMs(event) >= afterMs
|
|
335
|
+
));
|
|
336
|
+
let userIndex = -1;
|
|
337
|
+
if (promptText) {
|
|
338
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
339
|
+
const event = events[index];
|
|
340
|
+
if (
|
|
341
|
+
event.type === "user" &&
|
|
342
|
+
eventTimeMs(event) >= afterMs &&
|
|
343
|
+
userPromptOf(event) === promptText
|
|
344
|
+
) {
|
|
345
|
+
userIndex = index;
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
let assistant = null;
|
|
351
|
+
if (userIndex >= 0) {
|
|
352
|
+
assistant = events.slice(userIndex + 1).find((event) => (
|
|
353
|
+
assistantTextOf(event) &&
|
|
354
|
+
isAssistantFinal(event)
|
|
355
|
+
));
|
|
356
|
+
}
|
|
357
|
+
if (!bridge && userIndex < 0 && !assistant) {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const sessionEvent = bridge || events[userIndex] || assistant || events.find((event) => event.sessionId);
|
|
361
|
+
const sessionId = sessionEvent?.sessionId || path.basename(filePath, ".jsonl");
|
|
362
|
+
return {
|
|
363
|
+
filePath,
|
|
364
|
+
sessionId,
|
|
365
|
+
url: bridge?.url || null,
|
|
366
|
+
assistantText: assistantTextOf(assistant)
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
return { filePath: null, sessionId: null, url: null, assistantText: "" };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function findTranscriptBinding(projectPath, {
|
|
373
|
+
afterMs = 0,
|
|
374
|
+
expectedSessionId,
|
|
375
|
+
homeDir = os.homedir(),
|
|
376
|
+
fsImpl = fs
|
|
377
|
+
} = {}) {
|
|
378
|
+
for (const filePath of listTranscriptFiles(projectPath, { homeDir, fsImpl })) {
|
|
379
|
+
const events = readTranscript(filePath, { fsImpl });
|
|
380
|
+
const bridge = [...events].reverse().find((event) => {
|
|
381
|
+
if (
|
|
382
|
+
event.type !== "system" ||
|
|
383
|
+
event.subtype !== "bridge_status" ||
|
|
384
|
+
!event.url ||
|
|
385
|
+
eventTimeMs(event) < afterMs
|
|
386
|
+
) {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
return !expectedSessionId || event.sessionId === expectedSessionId;
|
|
390
|
+
});
|
|
391
|
+
if (!bridge) {
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
filePath,
|
|
396
|
+
sessionId: bridge.sessionId || path.basename(filePath, ".jsonl"),
|
|
397
|
+
url: bridge.url || null,
|
|
398
|
+
offset: transcriptFileSize(filePath, { fsImpl })
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
return { filePath: null, sessionId: null, url: null, offset: 0 };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
class TranscriptTail {
|
|
405
|
+
constructor({ filePath, offset = 0, fsImpl = fs }) {
|
|
406
|
+
this.filePath = filePath;
|
|
407
|
+
this.offset = offset;
|
|
408
|
+
this.fsImpl = fsImpl;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
readNewEvents() {
|
|
412
|
+
if (!this.filePath || !this.fsImpl.existsSync(this.filePath)) {
|
|
413
|
+
return [];
|
|
414
|
+
}
|
|
415
|
+
const size = this.fsImpl.statSync(this.filePath).size;
|
|
416
|
+
if (size <= this.offset) {
|
|
417
|
+
return [];
|
|
418
|
+
}
|
|
419
|
+
const fd = this.fsImpl.openSync(this.filePath, "r");
|
|
420
|
+
try {
|
|
421
|
+
const chunk = Buffer.alloc(size - this.offset);
|
|
422
|
+
const bytesRead = this.fsImpl.readSync(fd, chunk, 0, chunk.length, this.offset);
|
|
423
|
+
const buffer = chunk.subarray(0, bytesRead);
|
|
424
|
+
const lastNewline = buffer.lastIndexOf(0x0a);
|
|
425
|
+
if (lastNewline < 0) {
|
|
426
|
+
return [];
|
|
427
|
+
}
|
|
428
|
+
const consumed = buffer.subarray(0, lastNewline + 1);
|
|
429
|
+
this.offset += consumed.length;
|
|
430
|
+
return consumed.toString("utf8")
|
|
431
|
+
.split(/\r?\n/)
|
|
432
|
+
.filter(Boolean)
|
|
433
|
+
.map((line) => {
|
|
434
|
+
try {
|
|
435
|
+
return JSON.parse(line);
|
|
436
|
+
} catch {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
})
|
|
440
|
+
.filter(Boolean);
|
|
441
|
+
} finally {
|
|
442
|
+
this.fsImpl.closeSync(fd);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function renderClaudeValue(value) {
|
|
448
|
+
if (value === undefined || value === null) {
|
|
449
|
+
return "";
|
|
450
|
+
}
|
|
451
|
+
if (typeof value === "string") {
|
|
452
|
+
return value;
|
|
453
|
+
}
|
|
454
|
+
if (Array.isArray(value)) {
|
|
455
|
+
return value.map((item) => renderClaudeValue(item)).filter(Boolean).join("\n");
|
|
456
|
+
}
|
|
457
|
+
if (typeof value === "object") {
|
|
458
|
+
if (typeof value.text === "string") {
|
|
459
|
+
return value.text;
|
|
460
|
+
}
|
|
461
|
+
if (Array.isArray(value.content)) {
|
|
462
|
+
return renderClaudeValue(value.content);
|
|
463
|
+
}
|
|
464
|
+
try {
|
|
465
|
+
return JSON.stringify(value, null, 2);
|
|
466
|
+
} catch {
|
|
467
|
+
return String(value);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return String(value);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function claudeContentBlocks(content) {
|
|
474
|
+
if (Array.isArray(content)) {
|
|
475
|
+
return content;
|
|
476
|
+
}
|
|
477
|
+
if (content === undefined || content === null) {
|
|
478
|
+
return [];
|
|
479
|
+
}
|
|
480
|
+
return [{ type: "text", text: String(content) }];
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function toolProgressMessage(block = {}) {
|
|
484
|
+
if (!TOOL_PROGRESS_NAMES.has(block.name)) {
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
const input = block.input || {};
|
|
488
|
+
if (block.name === "Read" && input.file_path) {
|
|
489
|
+
return `读取文件:${input.file_path}`;
|
|
490
|
+
}
|
|
491
|
+
if (block.name === "Grep" && input.pattern) {
|
|
492
|
+
return `搜索文本:${input.pattern}`;
|
|
493
|
+
}
|
|
494
|
+
if (block.name === "Glob" && input.pattern) {
|
|
495
|
+
return `匹配文件:${input.pattern}`;
|
|
496
|
+
}
|
|
497
|
+
if (block.name === "LS" && input.path) {
|
|
498
|
+
return `列出目录:${input.path}`;
|
|
499
|
+
}
|
|
500
|
+
return `执行工具:${block.name}`;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function convertTranscriptEvent(event = {}, state = {}) {
|
|
504
|
+
if (state.sessionId && event.sessionId && event.sessionId !== state.sessionId) {
|
|
505
|
+
return [];
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const events = [];
|
|
509
|
+
if (!state.seenPrompt) {
|
|
510
|
+
if (event.type === "user" && userPromptOf(event) === state.prompt) {
|
|
511
|
+
state.seenPrompt = true;
|
|
512
|
+
}
|
|
513
|
+
return events;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (event.type === "assistant" && event.message?.role === "assistant") {
|
|
517
|
+
const final = isAssistantFinal(event);
|
|
518
|
+
for (const block of claudeContentBlocks(event.message.content)) {
|
|
519
|
+
if (block?.type === "text" && block.text) {
|
|
520
|
+
if (final && !state.emittedFinalText) {
|
|
521
|
+
state.emittedFinalText = true;
|
|
522
|
+
events.push({ type: "delta", payload: { text: block.text } });
|
|
523
|
+
} else if (!final) {
|
|
524
|
+
events.push({ type: "activity", payload: { message: block.text, kind: "status" } });
|
|
525
|
+
}
|
|
526
|
+
} else if (block?.type === "tool_use") {
|
|
527
|
+
if (!state.seenToolUseIds) {
|
|
528
|
+
state.seenToolUseIds = new Set();
|
|
529
|
+
}
|
|
530
|
+
if (!block.id || !state.seenToolUseIds.has(block.id)) {
|
|
531
|
+
if (block.id) {
|
|
532
|
+
state.seenToolUseIds.add(block.id);
|
|
533
|
+
}
|
|
534
|
+
events.push({
|
|
535
|
+
type: "tool_use",
|
|
536
|
+
payload: {
|
|
537
|
+
tool_name: block.name || "tool",
|
|
538
|
+
tool_input: block.input || {},
|
|
539
|
+
tool_use_id: block.id || null
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
const message = toolProgressMessage(block);
|
|
543
|
+
if (message) {
|
|
544
|
+
events.push({
|
|
545
|
+
type: "activity",
|
|
546
|
+
payload: {
|
|
547
|
+
message,
|
|
548
|
+
kind: "tool_progress",
|
|
549
|
+
tool_use_id: block.id || null
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (event.message.usage) {
|
|
557
|
+
events.push({ type: "usage", payload: { usage: event.message.usage } });
|
|
558
|
+
}
|
|
559
|
+
if (final) {
|
|
560
|
+
state.completed = true;
|
|
561
|
+
}
|
|
562
|
+
return events;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (event.type === "user") {
|
|
566
|
+
for (const block of claudeContentBlocks(event.message?.content)) {
|
|
567
|
+
if (block?.type === "tool_result") {
|
|
568
|
+
events.push({
|
|
569
|
+
type: "tool_result",
|
|
570
|
+
payload: {
|
|
571
|
+
tool_use_id: block.tool_use_id || null,
|
|
572
|
+
content: renderClaudeValue(block.content),
|
|
573
|
+
is_error: Boolean(block.is_error)
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return events;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (event.type === "result" && (event.usage || event.total_usage)) {
|
|
582
|
+
events.push({
|
|
583
|
+
type: "usage",
|
|
584
|
+
payload: {
|
|
585
|
+
usage: event.usage || event.total_usage,
|
|
586
|
+
cost_usd: event.cost_usd ?? event.total_cost_usd ?? null
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
return events;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function wait(ms) {
|
|
594
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
async function waitForTranscriptState(predicate, {
|
|
598
|
+
timeoutMs,
|
|
599
|
+
intervalMs = 250,
|
|
600
|
+
readState
|
|
601
|
+
}) {
|
|
602
|
+
const started = Date.now();
|
|
603
|
+
let latest = null;
|
|
604
|
+
while (Date.now() - started < timeoutMs) {
|
|
605
|
+
latest = readState();
|
|
606
|
+
if (predicate(latest)) {
|
|
607
|
+
return latest;
|
|
608
|
+
}
|
|
609
|
+
await wait(intervalMs);
|
|
610
|
+
}
|
|
611
|
+
const error = new Error("等待 Claude transcript 超时。");
|
|
612
|
+
error.latest = latest;
|
|
613
|
+
throw error;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function recentTerminalOutput(active = {}) {
|
|
617
|
+
return (active.recentOutput || [])
|
|
618
|
+
.map((line) => String(line || "").trim())
|
|
619
|
+
.filter(Boolean)
|
|
620
|
+
.join("\n");
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function createClaudeStartupError(active = {}, latest = {}) {
|
|
624
|
+
const output = recentTerminalOutput(active);
|
|
625
|
+
const suffix = output ? `:${output}` : ":Claude 进程已退出,但未生成 transcript。";
|
|
626
|
+
const error = new Error(`Claude Remote Control 启动失败${suffix}`);
|
|
627
|
+
error.details = {
|
|
628
|
+
project_path: active.projectPath || "",
|
|
629
|
+
runtime_session_id: active.runtimeSessionId || null,
|
|
630
|
+
transcript_file: latest?.filePath || active.transcriptFile || null,
|
|
631
|
+
exit_code: active.exitCode ?? null,
|
|
632
|
+
exit_signal: active.exitSignal ?? null,
|
|
633
|
+
recent_output: active.recentOutput || []
|
|
634
|
+
};
|
|
635
|
+
return error;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function isRemoteControlTerminalReady(active = {}) {
|
|
639
|
+
return Boolean(active.url) || /(?:Remote Control active|\/remote-control is active)/i.test(recentTerminalOutput(active));
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function ensureNodePtyExecutable() {
|
|
643
|
+
try {
|
|
644
|
+
const helperRoot = path.join(process.cwd(), "node_modules", "node-pty", "prebuilds");
|
|
645
|
+
if (!fs.existsSync(helperRoot)) {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
for (const platformDir of fs.readdirSync(helperRoot)) {
|
|
649
|
+
const helperPath = path.join(helperRoot, platformDir, "spawn-helper");
|
|
650
|
+
if (fs.existsSync(helperPath)) {
|
|
651
|
+
fs.chmodSync(helperPath, 0o755);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
} catch {
|
|
655
|
+
// Best effort. node-pty will surface a concrete error if the helper cannot run.
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function createPtyTerminal(command, args, options = {}) {
|
|
660
|
+
ensureNodePtyExecutable();
|
|
661
|
+
const pty = require("node-pty");
|
|
662
|
+
const terminal = pty.spawn(command, args, {
|
|
663
|
+
cwd: options.cwd,
|
|
664
|
+
env: options.env,
|
|
665
|
+
cols: 120,
|
|
666
|
+
rows: 32,
|
|
667
|
+
name: "xterm-256color"
|
|
668
|
+
});
|
|
669
|
+
return {
|
|
670
|
+
write: (text) => terminal.write(text),
|
|
671
|
+
kill: () => terminal.kill(),
|
|
672
|
+
onData: (handler) => terminal.onData(handler),
|
|
673
|
+
onExit: (handler) => terminal.onExit(({ exitCode, signal }) => handler(exitCode, signal)),
|
|
674
|
+
get killed() {
|
|
675
|
+
return false;
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
class ClaudeCodeRuntime {
|
|
681
|
+
static activeRemoteControls = new Map();
|
|
682
|
+
|
|
683
|
+
constructor({
|
|
684
|
+
claudePathOverride,
|
|
685
|
+
cliPath,
|
|
686
|
+
env = process.env,
|
|
687
|
+
fsImpl = fs,
|
|
688
|
+
homeDir = os.homedir(),
|
|
689
|
+
provider = "claude-code",
|
|
690
|
+
spawnSyncImpl = spawnSync,
|
|
691
|
+
terminalFactory = createPtyTerminal,
|
|
692
|
+
trustConfigPath
|
|
693
|
+
} = {}) {
|
|
694
|
+
this.provider = provider;
|
|
695
|
+
this.env = env;
|
|
696
|
+
this.fsImpl = fsImpl;
|
|
697
|
+
this.homeDir = homeDir;
|
|
698
|
+
this.trustConfigPath = trustConfigPath;
|
|
699
|
+
this.claudePathOverride = claudePathOverride || cliPath || env.CLAUDE_CODE_CLI_PATH || env.CLAUDE_CLI_PATH || undefined;
|
|
700
|
+
this.spawnSyncImpl = spawnSyncImpl;
|
|
701
|
+
this.terminalFactory = terminalFactory;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
startRemoteControl({ session, project, message, settings }) {
|
|
705
|
+
const claudePath = resolveClaudeExecutable(this.claudePathOverride);
|
|
706
|
+
assertClaudeRemoteControlAvailable(claudePath, {
|
|
707
|
+
env: this.env,
|
|
708
|
+
spawnSyncImpl: this.spawnSyncImpl
|
|
709
|
+
});
|
|
710
|
+
const canFixSessionId = supportsRemoteControlSessionId(claudePath, {
|
|
711
|
+
env: this.env,
|
|
712
|
+
spawnSyncImpl: this.spawnSyncImpl
|
|
713
|
+
});
|
|
714
|
+
const requestedSessionId = canFixSessionId
|
|
715
|
+
? remoteControlRuntimeSessionId(session, { generate: true })
|
|
716
|
+
: null;
|
|
717
|
+
const args = buildClaudeRemoteControlArgs({
|
|
718
|
+
session,
|
|
719
|
+
project,
|
|
720
|
+
message,
|
|
721
|
+
settings,
|
|
722
|
+
sessionId: requestedSessionId,
|
|
723
|
+
useSessionId: canFixSessionId
|
|
724
|
+
});
|
|
725
|
+
const title = remoteControlSessionName({ session, project, message });
|
|
726
|
+
const terminal = this.terminalFactory(claudePath, args, {
|
|
727
|
+
cwd: project.path || process.cwd(),
|
|
728
|
+
env: this.env
|
|
729
|
+
});
|
|
730
|
+
const active = {
|
|
731
|
+
cancelled: false,
|
|
732
|
+
exited: false,
|
|
733
|
+
expectedSessionId: requestedSessionId,
|
|
734
|
+
transcriptFile: null,
|
|
735
|
+
transcriptOffset: 0,
|
|
736
|
+
projectPath: project.path || "",
|
|
737
|
+
recentOutput: [],
|
|
738
|
+
runtimeSessionId: requestedSessionId || remoteControlRuntimeSessionId(session),
|
|
739
|
+
terminal,
|
|
740
|
+
title,
|
|
741
|
+
url: null,
|
|
742
|
+
exitCode: null,
|
|
743
|
+
exitSignal: null
|
|
744
|
+
};
|
|
745
|
+
terminal.onData((chunk) => {
|
|
746
|
+
const text = stripAnsi(chunk);
|
|
747
|
+
const line = text.trim();
|
|
748
|
+
if (line) {
|
|
749
|
+
active.recentOutput.push(line.slice(-1000));
|
|
750
|
+
if (active.recentOutput.length > 8) {
|
|
751
|
+
active.recentOutput.shift();
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
if (shouldConfirmRemoteControl(text)) {
|
|
755
|
+
terminal.write("y\r");
|
|
756
|
+
}
|
|
757
|
+
const url = extractRemoteControlUrl(text);
|
|
758
|
+
if (url) {
|
|
759
|
+
active.url = url;
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
terminal.onExit((exitCode, signal) => {
|
|
763
|
+
active.exited = true;
|
|
764
|
+
active.exitCode = exitCode;
|
|
765
|
+
active.exitSignal = signal;
|
|
766
|
+
});
|
|
767
|
+
return active;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
async *run({ session = {}, project = {}, message = "", settings = {} } = {}) {
|
|
771
|
+
const key = session.id || remoteControlRuntimeSessionId(session);
|
|
772
|
+
let active = ClaudeCodeRuntime.activeRemoteControls.get(key);
|
|
773
|
+
const didAutoTrust = ensureClaudeWorkspaceTrusted(project.path, {
|
|
774
|
+
configPath: this.trustConfigPath,
|
|
775
|
+
env: this.env
|
|
776
|
+
});
|
|
777
|
+
const turnStartedAt = Date.now();
|
|
778
|
+
|
|
779
|
+
if (!active || active.exited) {
|
|
780
|
+
active = this.startRemoteControl({ session, project, message, settings });
|
|
781
|
+
ClaudeCodeRuntime.activeRemoteControls.set(key, active);
|
|
782
|
+
yield {
|
|
783
|
+
type: "activity",
|
|
784
|
+
payload: { message: `启动 Claude Remote Control:${project.path || ""}`, kind: "status" }
|
|
785
|
+
};
|
|
786
|
+
if (didAutoTrust) {
|
|
787
|
+
yield {
|
|
788
|
+
type: "activity",
|
|
789
|
+
payload: { message: `已自动信任 Claude 工作区:${project.path}`, kind: "status" }
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
const ready = await waitForTranscriptState((state) => state.filePath || isRemoteControlTerminalReady(active), {
|
|
793
|
+
timeoutMs: positiveInteger(this.env.AGENT_ANYWHERE_CLAUDE_READY_TIMEOUT_MS, DEFAULT_READY_TIMEOUT_MS),
|
|
794
|
+
readState: () => {
|
|
795
|
+
const state = findTranscriptBinding(project.path, {
|
|
796
|
+
afterMs: turnStartedAt - 1000,
|
|
797
|
+
expectedSessionId: active.expectedSessionId,
|
|
798
|
+
homeDir: this.homeDir,
|
|
799
|
+
fsImpl: this.fsImpl
|
|
800
|
+
});
|
|
801
|
+
if (state.url) active.url = state.url;
|
|
802
|
+
if (state.sessionId) active.runtimeSessionId = state.sessionId;
|
|
803
|
+
if (state.filePath) active.transcriptFile = state.filePath;
|
|
804
|
+
if (state.offset) active.transcriptOffset = state.offset;
|
|
805
|
+
if (active.exited && !state.filePath && !isRemoteControlTerminalReady(active)) {
|
|
806
|
+
throw createClaudeStartupError(active, state);
|
|
807
|
+
}
|
|
808
|
+
return state;
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
active.url = active.url || ready.url;
|
|
812
|
+
active.runtimeSessionId = ready.sessionId || active.runtimeSessionId;
|
|
813
|
+
active.transcriptFile = ready.filePath || active.transcriptFile;
|
|
814
|
+
active.transcriptOffset = ready.offset || active.transcriptOffset || transcriptFileSize(active.transcriptFile, {
|
|
815
|
+
fsImpl: this.fsImpl
|
|
816
|
+
});
|
|
817
|
+
yield {
|
|
818
|
+
type: "runtime_session",
|
|
819
|
+
payload: {
|
|
820
|
+
runtime_session_id: active.runtimeSessionId,
|
|
821
|
+
working_directory: project.path || "",
|
|
822
|
+
title: active.title
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
if (active.url) {
|
|
826
|
+
yield {
|
|
827
|
+
type: "activity",
|
|
828
|
+
payload: { message: `Claude Remote Control 已连接:${active.url}`, kind: "status" }
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
} else {
|
|
832
|
+
yield {
|
|
833
|
+
type: "runtime_session",
|
|
834
|
+
payload: {
|
|
835
|
+
runtime_session_id: active.runtimeSessionId,
|
|
836
|
+
working_directory: active.projectPath,
|
|
837
|
+
title: active.title
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const prompt = String(message || "").trim();
|
|
843
|
+
if (!prompt) {
|
|
844
|
+
yield { type: "complete", payload: { message: "Claude Remote Control 已连接。" } };
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const hadTranscriptBeforePrompt = Boolean(active.transcriptFile);
|
|
849
|
+
active.transcriptOffset = hadTranscriptBeforePrompt
|
|
850
|
+
? transcriptFileSize(active.transcriptFile, { fsImpl: this.fsImpl })
|
|
851
|
+
: 0;
|
|
852
|
+
active.terminal.write(`${prompt}\r`);
|
|
853
|
+
|
|
854
|
+
if (!active.transcriptFile) {
|
|
855
|
+
const transcript = await waitForTranscriptState((state) => Boolean(state.filePath), {
|
|
856
|
+
timeoutMs: positiveInteger(this.env.AGENT_ANYWHERE_CLAUDE_READY_TIMEOUT_MS, DEFAULT_READY_TIMEOUT_MS),
|
|
857
|
+
readState: () => {
|
|
858
|
+
const state = findTranscriptState(project.path, {
|
|
859
|
+
afterMs: turnStartedAt - 1000,
|
|
860
|
+
prompt,
|
|
861
|
+
homeDir: this.homeDir,
|
|
862
|
+
fsImpl: this.fsImpl
|
|
863
|
+
});
|
|
864
|
+
if (state.url) active.url = state.url;
|
|
865
|
+
if (state.sessionId) active.runtimeSessionId = state.sessionId;
|
|
866
|
+
if (state.filePath) active.transcriptFile = state.filePath;
|
|
867
|
+
if (active.exited && !state.filePath) {
|
|
868
|
+
throw createClaudeStartupError(active, state);
|
|
869
|
+
}
|
|
870
|
+
return state;
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
active.url = active.url || transcript.url;
|
|
874
|
+
active.runtimeSessionId = transcript.sessionId || active.runtimeSessionId;
|
|
875
|
+
active.transcriptFile = transcript.filePath || active.transcriptFile;
|
|
876
|
+
active.transcriptOffset = 0;
|
|
877
|
+
yield {
|
|
878
|
+
type: "runtime_session",
|
|
879
|
+
payload: {
|
|
880
|
+
runtime_session_id: active.runtimeSessionId,
|
|
881
|
+
working_directory: active.projectPath,
|
|
882
|
+
title: active.title
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const tail = new TranscriptTail({
|
|
888
|
+
filePath: active.transcriptFile,
|
|
889
|
+
offset: active.transcriptOffset,
|
|
890
|
+
fsImpl: this.fsImpl
|
|
891
|
+
});
|
|
892
|
+
const transcriptState = {
|
|
893
|
+
prompt,
|
|
894
|
+
sessionId: active.runtimeSessionId,
|
|
895
|
+
seenPrompt: false,
|
|
896
|
+
emittedFinalText: false,
|
|
897
|
+
completed: false,
|
|
898
|
+
seenToolUseIds: new Set()
|
|
899
|
+
};
|
|
900
|
+
const timeoutMs = positiveInteger(this.env.AGENT_ANYWHERE_CLAUDE_TURN_TIMEOUT_MS, DEFAULT_TURN_TIMEOUT_MS);
|
|
901
|
+
const waitStartedAt = Date.now();
|
|
902
|
+
while (Date.now() - waitStartedAt < timeoutMs) {
|
|
903
|
+
if (active.cancelled) {
|
|
904
|
+
yield { type: "cancelled", payload: { message: "Claude Remote Control 已取消。" } };
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
for (const event of tail.readNewEvents()) {
|
|
908
|
+
if (event.url) active.url = event.url;
|
|
909
|
+
if (event.sessionId) {
|
|
910
|
+
active.runtimeSessionId = event.sessionId;
|
|
911
|
+
transcriptState.sessionId = event.sessionId;
|
|
912
|
+
}
|
|
913
|
+
for (const normalized of convertTranscriptEvent(event, transcriptState)) {
|
|
914
|
+
yield normalized;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
active.transcriptOffset = tail.offset;
|
|
918
|
+
if (transcriptState.completed) {
|
|
919
|
+
yield {
|
|
920
|
+
type: "complete",
|
|
921
|
+
payload: { message: "Claude Remote Control 回合完成。" }
|
|
922
|
+
};
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
if (active.exited) {
|
|
926
|
+
break;
|
|
927
|
+
}
|
|
928
|
+
await wait(250);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const error = new Error("等待 Claude Remote Control 回合完成超时。");
|
|
932
|
+
error.details = {
|
|
933
|
+
transcript_file: active.transcriptFile,
|
|
934
|
+
transcript_offset: active.transcriptOffset,
|
|
935
|
+
runtime_session_id: active.runtimeSessionId,
|
|
936
|
+
saw_prompt: transcriptState.seenPrompt,
|
|
937
|
+
recent_output: active.recentOutput
|
|
938
|
+
};
|
|
939
|
+
throw error;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
async discoverCapabilities() {
|
|
943
|
+
return buildCapabilities(this.provider, {
|
|
944
|
+
models: DEFAULT_CLAUDE_MODELS,
|
|
945
|
+
default_model: "sonnet",
|
|
946
|
+
input_modalities: ["text"],
|
|
947
|
+
reasoning_efforts: ["medium"],
|
|
948
|
+
approval_policies: ["on-request"],
|
|
949
|
+
modes: ["default", "full-access"]
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
async cancelTurn({ session } = {}) {
|
|
954
|
+
const key = session?.id || session?.runtime_session_id;
|
|
955
|
+
const active = key ? ClaudeCodeRuntime.activeRemoteControls.get(key) : null;
|
|
956
|
+
if (!active) {
|
|
957
|
+
const error = new Error("没有可停止的 Claude Remote Control 进程。");
|
|
958
|
+
error.statusCode = 409;
|
|
959
|
+
throw error;
|
|
960
|
+
}
|
|
961
|
+
active.cancelled = true;
|
|
962
|
+
active.terminal.kill();
|
|
963
|
+
ClaudeCodeRuntime.activeRemoteControls.delete(key);
|
|
964
|
+
return {};
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
module.exports = {
|
|
969
|
+
ClaudeCodeRuntime,
|
|
970
|
+
assertClaudeRemoteControlAvailable,
|
|
971
|
+
buildClaudeRemoteControlArgs,
|
|
972
|
+
claudeProjectDir,
|
|
973
|
+
claudeProjectKey,
|
|
974
|
+
convertTranscriptEvent,
|
|
975
|
+
ensureClaudeWorkspaceTrusted,
|
|
976
|
+
extractRemoteControlUrl,
|
|
977
|
+
findTranscriptBinding,
|
|
978
|
+
findTranscriptState,
|
|
979
|
+
remoteControlSandboxFlag,
|
|
980
|
+
remoteControlSessionName,
|
|
981
|
+
shouldConfirmRemoteControl,
|
|
982
|
+
supportsRemoteControlSessionId,
|
|
983
|
+
textFromClaudeContent
|
|
984
|
+
};
|