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,547 @@
1
+ const { spawn } = require("node:child_process");
2
+ const fs = require("node:fs");
3
+ const os = require("node:os");
4
+ const path = require("node:path");
5
+ const { buildCapabilities } = require("../shared/capabilities");
6
+
7
+ const DEFAULT_CLAUDE_MODELS = [
8
+ "sonnet",
9
+ "opus",
10
+ "claude-sonnet-4-6",
11
+ "claude-opus-4-5"
12
+ ];
13
+ const DEFAULT_CLAUDE_EFFORTS = ["low", "medium", "high", "xhigh"];
14
+
15
+ function claudeExecutableCandidates() {
16
+ const binaryName = process.platform === "win32" ? "claude.cmd" : "claude";
17
+ return [
18
+ path.join(__dirname, "..", "..", "node_modules", ".bin", binaryName),
19
+ path.join(os.homedir(), ".claude", "local", binaryName)
20
+ ];
21
+ }
22
+
23
+ function resolveClaudeExecutable(claudePathOverride = process.env.CLAUDE_CODE_CLI_PATH || process.env.CLAUDE_CLI_PATH) {
24
+ if (claudePathOverride) {
25
+ const resolved = path.resolve(claudePathOverride);
26
+ if (fs.existsSync(resolved)) {
27
+ return resolved;
28
+ }
29
+ const error = new Error(`CLAUDE_CODE_CLI_PATH 不可用:${claudePathOverride}`);
30
+ error.statusCode = 500;
31
+ throw error;
32
+ }
33
+
34
+ for (const candidate of claudeExecutableCandidates()) {
35
+ if (fs.existsSync(candidate)) {
36
+ return candidate;
37
+ }
38
+ }
39
+ return "claude";
40
+ }
41
+
42
+ function claudePermissionMode(settings = {}) {
43
+ if (settings.mode === "full-access") {
44
+ return "bypassPermissions";
45
+ }
46
+ if (settings.mode === "auto-review") {
47
+ return "auto";
48
+ }
49
+ if (settings.approval_policy === "never") {
50
+ return "dontAsk";
51
+ }
52
+ return "default";
53
+ }
54
+
55
+ function normalizeClaudeEffort(effort) {
56
+ if (effort === "xhigh") {
57
+ return "max";
58
+ }
59
+ return effort || "medium";
60
+ }
61
+
62
+ function buildClaudePrompt(message, attachments = []) {
63
+ const imagePaths = (attachments || [])
64
+ .filter((attachment) => attachment?.path)
65
+ .map((attachment) => String(attachment.path));
66
+ if (!imagePaths.length) {
67
+ return String(message || "");
68
+ }
69
+ return `${String(message || "")}\n\n附件图片路径:\n${imagePaths.map((imagePath) => `- ${imagePath}`).join("\n")}`;
70
+ }
71
+
72
+ function buildClaudeArgs({ runtimeSessionId, message, attachments = [], settings = {} }) {
73
+ const args = [
74
+ "-p",
75
+ buildClaudePrompt(message, attachments),
76
+ "--output-format",
77
+ "stream-json",
78
+ "--verbose",
79
+ "--include-partial-messages",
80
+ "--permission-mode",
81
+ claudePermissionMode(settings)
82
+ ];
83
+
84
+ if (runtimeSessionId) {
85
+ args.push("--resume", runtimeSessionId);
86
+ }
87
+ if (settings.model) {
88
+ args.push("--model", settings.model);
89
+ }
90
+ if (settings.reasoning_effort) {
91
+ args.push("--effort", normalizeClaudeEffort(settings.reasoning_effort));
92
+ }
93
+
94
+ const imagePaths = (attachments || [])
95
+ .filter((attachment) => attachment?.path)
96
+ .map((attachment) => String(attachment.path));
97
+ if (imagePaths.length) {
98
+ args.push("--add-dir", ...uniqueDirectories(imagePaths));
99
+ }
100
+
101
+ return args;
102
+ }
103
+
104
+ function uniqueDirectories(files) {
105
+ const dirs = [];
106
+ for (const file of files) {
107
+ const dir = path.dirname(file);
108
+ if (!dirs.includes(dir)) {
109
+ dirs.push(dir);
110
+ }
111
+ }
112
+ return dirs;
113
+ }
114
+
115
+ function parseClaudeJsonLines(buffer, onObject) {
116
+ const lines = String(buffer || "").split(/\r?\n/);
117
+ const rest = lines.pop() || "";
118
+ for (const line of lines) {
119
+ const text = line.trim();
120
+ if (!text) {
121
+ continue;
122
+ }
123
+ onObject(JSON.parse(text));
124
+ }
125
+ return rest;
126
+ }
127
+
128
+ function renderValue(value) {
129
+ if (value === undefined || value === null) {
130
+ return "";
131
+ }
132
+ if (typeof value === "string") {
133
+ return value;
134
+ }
135
+ if (Array.isArray(value)) {
136
+ return value.map((item) => renderValue(item)).filter(Boolean).join("\n");
137
+ }
138
+ if (typeof value === "object") {
139
+ if (typeof value.text === "string") {
140
+ return value.text;
141
+ }
142
+ if (Array.isArray(value.content)) {
143
+ return renderValue(value.content);
144
+ }
145
+ try {
146
+ return JSON.stringify(value, null, 2);
147
+ } catch {
148
+ return String(value);
149
+ }
150
+ }
151
+ return String(value);
152
+ }
153
+
154
+ function usageFromClaudeResult(message = {}) {
155
+ return message.usage || message.total_usage || message.cost_usd !== undefined || message.total_cost_usd !== undefined
156
+ ? {
157
+ usage: message.usage || message.total_usage || null,
158
+ cost_usd: message.cost_usd ?? message.total_cost_usd ?? null
159
+ }
160
+ : null;
161
+ }
162
+
163
+ function extractSessionId(message = {}) {
164
+ return message.session_id || message.sessionId || message.session?.id || null;
165
+ }
166
+
167
+ function toolInputFromBlock(block = {}) {
168
+ return block.input || block.parameters || {};
169
+ }
170
+
171
+ function convertClaudeStreamEvent(message, state = {}) {
172
+ const event = message.event || {};
173
+ const eventType = event.type;
174
+
175
+ if (eventType === "content_block_start") {
176
+ const block = event.content_block || {};
177
+ if (block.type === "tool_use") {
178
+ state.currentToolUseId = block.id || null;
179
+ state.currentToolInput = "";
180
+ if (!state.seenToolUseIds) {
181
+ state.seenToolUseIds = new Set();
182
+ }
183
+ if (block.id) {
184
+ state.seenToolUseIds.add(block.id);
185
+ }
186
+ return [{
187
+ type: "tool_use",
188
+ payload: {
189
+ tool_name: block.name || "tool",
190
+ tool_input: toolInputFromBlock(block),
191
+ tool_use_id: block.id || null
192
+ }
193
+ }];
194
+ }
195
+ }
196
+
197
+ if (eventType === "content_block_delta") {
198
+ const delta = event.delta || {};
199
+ if (delta.type === "text_delta" && delta.text) {
200
+ state.emittedText = true;
201
+ return [{ type: "delta", payload: { text: delta.text } }];
202
+ }
203
+ if (delta.type === "input_json_delta" && state.currentToolUseId) {
204
+ state.currentToolInput = `${state.currentToolInput || ""}${delta.partial_json || ""}`;
205
+ return [];
206
+ }
207
+ }
208
+
209
+ if (eventType === "content_block_stop" && state.currentToolUseId) {
210
+ const content = state.currentToolInput || "";
211
+ const toolUseId = state.currentToolUseId;
212
+ state.currentToolUseId = null;
213
+ state.currentToolInput = "";
214
+ if (content) {
215
+ return [{
216
+ type: "activity",
217
+ payload: {
218
+ message: `工具参数:${content}`,
219
+ kind: "tool_progress",
220
+ tool_use_id: toolUseId
221
+ }
222
+ }];
223
+ }
224
+ }
225
+
226
+ if (eventType === "message_delta" && event.usage) {
227
+ return [{ type: "usage", payload: { usage: event.usage } }];
228
+ }
229
+
230
+ return [];
231
+ }
232
+
233
+ function convertClaudeAssistantMessage(message = {}, state = {}) {
234
+ const events = [];
235
+ for (const block of message.message?.content || message.content || []) {
236
+ if (block?.type === "text" && block.text && !state.emittedText) {
237
+ state.emittedText = true;
238
+ events.push({ type: "delta", payload: { text: block.text } });
239
+ } else if (block?.type === "tool_use" && !state.seenToolUseIds?.has(block.id)) {
240
+ if (!state.seenToolUseIds) {
241
+ state.seenToolUseIds = new Set();
242
+ }
243
+ if (block.id) {
244
+ state.seenToolUseIds.add(block.id);
245
+ }
246
+ events.push({
247
+ type: "tool_use",
248
+ payload: {
249
+ tool_name: block.name || "tool",
250
+ tool_input: toolInputFromBlock(block),
251
+ tool_use_id: block.id || null
252
+ }
253
+ });
254
+ } else if (block?.type === "tool_result") {
255
+ events.push({
256
+ type: "tool_result",
257
+ payload: {
258
+ tool_use_id: block.tool_use_id || null,
259
+ content: renderValue(block.content),
260
+ is_error: Boolean(block.is_error)
261
+ }
262
+ });
263
+ }
264
+ }
265
+ return events;
266
+ }
267
+
268
+ function convertClaudeEvent(message, state = {}) {
269
+ if (!message || typeof message !== "object") {
270
+ return [];
271
+ }
272
+
273
+ const events = [];
274
+ const sessionId = extractSessionId(message);
275
+ if (sessionId && !state.seenSessionIds?.has(sessionId)) {
276
+ if (!state.seenSessionIds) {
277
+ state.seenSessionIds = new Set();
278
+ }
279
+ state.seenSessionIds.add(sessionId);
280
+ events.push({
281
+ type: "runtime_session",
282
+ payload: {
283
+ runtime_session_id: sessionId,
284
+ working_directory: state.workingDirectory || ""
285
+ }
286
+ });
287
+ }
288
+
289
+ if (message.type === "system") {
290
+ if (message.subtype === "init") {
291
+ return events.length ? events : [{ type: "activity", payload: { message: "Claude Code 初始化完成", kind: "status" } }];
292
+ }
293
+ if (message.subtype === "api_retry") {
294
+ events.push({
295
+ type: "activity",
296
+ payload: {
297
+ message: `Claude Code API 重试 ${message.attempt || "?"}/${message.max_retries || "?"}`,
298
+ kind: "status"
299
+ }
300
+ });
301
+ return events;
302
+ }
303
+ return events;
304
+ }
305
+
306
+ if (message.type === "stream_event") {
307
+ events.push(...convertClaudeStreamEvent(message, state));
308
+ return events;
309
+ }
310
+
311
+ if (message.type === "assistant") {
312
+ events.push(...convertClaudeAssistantMessage(message, state));
313
+ return events;
314
+ }
315
+
316
+ if (message.type === "result") {
317
+ const usage = usageFromClaudeResult(message);
318
+ if (usage) {
319
+ events.push({ type: "usage", payload: usage });
320
+ }
321
+ if (message.subtype && message.subtype !== "success") {
322
+ events.push({ type: "error", payload: { message: message.error || message.subtype } });
323
+ } else if (message.result && !state.emittedText) {
324
+ state.emittedText = true;
325
+ events.push({ type: "delta", payload: { text: String(message.result) } });
326
+ }
327
+ return events;
328
+ }
329
+
330
+ if (message.type === "error") {
331
+ events.push({ type: "error", payload: { message: message.message || "Claude Code 执行失败" } });
332
+ return events;
333
+ }
334
+
335
+ return events;
336
+ }
337
+
338
+ class AsyncEventQueue {
339
+ constructor() {
340
+ this.items = [];
341
+ this.waiters = [];
342
+ this.closed = false;
343
+ this.error = null;
344
+ }
345
+
346
+ push(item) {
347
+ if (this.closed) {
348
+ return;
349
+ }
350
+ const waiter = this.waiters.shift();
351
+ if (waiter) {
352
+ waiter.resolve({ value: item, done: false });
353
+ return;
354
+ }
355
+ this.items.push(item);
356
+ }
357
+
358
+ fail(error) {
359
+ if (this.closed) {
360
+ return;
361
+ }
362
+ this.closed = true;
363
+ this.error = error;
364
+ for (const waiter of this.waiters.splice(0)) {
365
+ waiter.reject(error);
366
+ }
367
+ }
368
+
369
+ complete() {
370
+ if (this.closed) {
371
+ return;
372
+ }
373
+ this.closed = true;
374
+ for (const waiter of this.waiters.splice(0)) {
375
+ waiter.resolve({ done: true });
376
+ }
377
+ }
378
+
379
+ next() {
380
+ if (this.items.length) {
381
+ return Promise.resolve({ value: this.items.shift(), done: false });
382
+ }
383
+ if (this.error) {
384
+ return Promise.reject(this.error);
385
+ }
386
+ if (this.closed) {
387
+ return Promise.resolve({ done: true });
388
+ }
389
+ return new Promise((resolve, reject) => {
390
+ this.waiters.push({ resolve, reject });
391
+ });
392
+ }
393
+
394
+ [Symbol.asyncIterator]() {
395
+ return this;
396
+ }
397
+ }
398
+
399
+ class ClaudeCodeHeadlessRuntime {
400
+ static activeProcesses = new Map();
401
+
402
+ constructor({ claudePathOverride, cliPath, spawnImpl = spawn, provider = "claude-code-headless" } = {}) {
403
+ this.provider = provider;
404
+ this.claudePathOverride = claudePathOverride || cliPath || process.env.CLAUDE_CODE_CLI_PATH || process.env.CLAUDE_CLI_PATH || undefined;
405
+ this.spawnImpl = spawnImpl;
406
+ }
407
+
408
+ async *run({ session, project, message, attachments = [], settings = {} } = {}) {
409
+ const claudePath = resolveClaudeExecutable(this.claudePathOverride);
410
+ const args = buildClaudeArgs({
411
+ runtimeSessionId: session?.runtime_session_id,
412
+ message,
413
+ attachments,
414
+ settings
415
+ });
416
+ const queue = new AsyncEventQueue();
417
+ const state = {
418
+ workingDirectory: project?.path || "",
419
+ seenSessionIds: new Set()
420
+ };
421
+ const child = this.spawnImpl(claudePath, args, {
422
+ cwd: project?.path || process.cwd(),
423
+ env: process.env,
424
+ stdio: ["ignore", "pipe", "pipe"]
425
+ });
426
+ let stdoutRest = "";
427
+ let stderrText = "";
428
+
429
+ const activeEntry = { child, cancelled: false };
430
+ if (session?.id) {
431
+ ClaudeCodeHeadlessRuntime.activeProcesses.set(session.id, activeEntry);
432
+ }
433
+
434
+ child.stdout.on("data", (chunk) => {
435
+ try {
436
+ stdoutRest = parseClaudeJsonLines(`${stdoutRest}${chunk}`, (event) => {
437
+ for (const normalized of convertClaudeEvent(event, state)) {
438
+ queue.push(normalized);
439
+ }
440
+ });
441
+ } catch (error) {
442
+ queue.fail(new Error(`Claude Code 输出不是合法 JSON:${error.message}`));
443
+ }
444
+ });
445
+
446
+ child.stderr.on("data", (chunk) => {
447
+ stderrText += String(chunk);
448
+ if (stderrText.length > 8000) {
449
+ stderrText = stderrText.slice(-8000);
450
+ }
451
+ });
452
+
453
+ child.on("error", (error) => queue.fail(error));
454
+ child.on("close", (code, signal) => {
455
+ try {
456
+ const rest = stdoutRest.trim();
457
+ if (rest) {
458
+ const event = JSON.parse(rest);
459
+ for (const normalized of convertClaudeEvent(event, state)) {
460
+ queue.push(normalized);
461
+ }
462
+ }
463
+ } catch (error) {
464
+ queue.fail(new Error(`Claude Code 输出不是合法 JSON:${error.message}`));
465
+ return;
466
+ }
467
+ if (activeEntry.cancelled) {
468
+ queue.push({
469
+ type: "cancelled",
470
+ payload: { message: "Claude Code 已取消。" }
471
+ });
472
+ queue.complete();
473
+ return;
474
+ }
475
+ if (signal) {
476
+ queue.fail(new Error(`Claude Code 执行被信号中断:${signal}`));
477
+ return;
478
+ }
479
+ if (code && code !== 0) {
480
+ const suffix = stderrText.trim() || `退出码 ${code}${signal ? `,信号 ${signal}` : ""}`;
481
+ queue.fail(new Error(`Claude Code 执行失败:${suffix}`));
482
+ return;
483
+ }
484
+ queue.complete();
485
+ });
486
+
487
+ yield {
488
+ type: "activity",
489
+ payload: {
490
+ message: `启动 Claude Code:${project?.path || ""}`,
491
+ kind: "status"
492
+ }
493
+ };
494
+
495
+ try {
496
+ for await (const event of queue) {
497
+ yield event;
498
+ }
499
+ yield {
500
+ type: "complete",
501
+ payload: {
502
+ message: "Claude Code 执行完成。"
503
+ }
504
+ };
505
+ } finally {
506
+ if (session?.id) {
507
+ ClaudeCodeHeadlessRuntime.activeProcesses.delete(session.id);
508
+ }
509
+ if (!child.killed && child.exitCode === null) {
510
+ child.kill("SIGTERM");
511
+ }
512
+ }
513
+ }
514
+
515
+ async discoverCapabilities() {
516
+ return buildCapabilities(this.provider, {
517
+ models: DEFAULT_CLAUDE_MODELS,
518
+ default_model: "sonnet",
519
+ input_modalities: ["text", "local_image"],
520
+ reasoning_efforts: DEFAULT_CLAUDE_EFFORTS
521
+ });
522
+ }
523
+
524
+ async cancelTurn({ session } = {}) {
525
+ const active = session?.id ? ClaudeCodeHeadlessRuntime.activeProcesses.get(session.id) : null;
526
+ if (!active) {
527
+ const error = new Error("没有可取消的 Claude Code turn。");
528
+ error.statusCode = 409;
529
+ throw error;
530
+ }
531
+ active.cancelled = true;
532
+ active.child.kill("SIGTERM");
533
+ return {};
534
+ }
535
+ }
536
+
537
+ module.exports = {
538
+ ClaudeCodeHeadlessRuntime,
539
+ buildClaudeArgs,
540
+ buildClaudePrompt,
541
+ claudeExecutableCandidates,
542
+ claudePermissionMode,
543
+ convertClaudeEvent,
544
+ normalizeClaudeEffort,
545
+ parseClaudeJsonLines,
546
+ resolveClaudeExecutable
547
+ };