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.
@@ -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
+ };