@helmisatria/mcp-chrome-bridge 1.0.30
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 +183 -0
- package/dist/README.md +25 -0
- package/dist/agent/attachment-service.d.ts +83 -0
- package/dist/agent/attachment-service.js +370 -0
- package/dist/agent/attachment-service.js.map +1 -0
- package/dist/agent/ccr-detector.d.ts +59 -0
- package/dist/agent/ccr-detector.js +311 -0
- package/dist/agent/ccr-detector.js.map +1 -0
- package/dist/agent/chat-service.d.ts +50 -0
- package/dist/agent/chat-service.js +439 -0
- package/dist/agent/chat-service.js.map +1 -0
- package/dist/agent/db/client.d.ts +26 -0
- package/dist/agent/db/client.js +244 -0
- package/dist/agent/db/client.js.map +1 -0
- package/dist/agent/db/index.d.ts +5 -0
- package/dist/agent/db/index.js +22 -0
- package/dist/agent/db/index.js.map +1 -0
- package/dist/agent/db/schema.d.ts +711 -0
- package/dist/agent/db/schema.js +121 -0
- package/dist/agent/db/schema.js.map +1 -0
- package/dist/agent/directory-picker.d.ts +11 -0
- package/dist/agent/directory-picker.js +149 -0
- package/dist/agent/directory-picker.js.map +1 -0
- package/dist/agent/engines/claude.d.ts +79 -0
- package/dist/agent/engines/claude.js +1338 -0
- package/dist/agent/engines/claude.js.map +1 -0
- package/dist/agent/engines/codex.d.ts +48 -0
- package/dist/agent/engines/codex.js +822 -0
- package/dist/agent/engines/codex.js.map +1 -0
- package/dist/agent/engines/types.d.ts +133 -0
- package/dist/agent/engines/types.js +3 -0
- package/dist/agent/engines/types.js.map +1 -0
- package/dist/agent/message-service.d.ts +56 -0
- package/dist/agent/message-service.js +198 -0
- package/dist/agent/message-service.js.map +1 -0
- package/dist/agent/open-project.d.ts +25 -0
- package/dist/agent/open-project.js +469 -0
- package/dist/agent/open-project.js.map +1 -0
- package/dist/agent/project-service.d.ts +49 -0
- package/dist/agent/project-service.js +254 -0
- package/dist/agent/project-service.js.map +1 -0
- package/dist/agent/project-types.d.ts +27 -0
- package/dist/agent/project-types.js +3 -0
- package/dist/agent/project-types.js.map +1 -0
- package/dist/agent/session-service.d.ts +198 -0
- package/dist/agent/session-service.js +292 -0
- package/dist/agent/session-service.js.map +1 -0
- package/dist/agent/storage.d.ts +27 -0
- package/dist/agent/storage.js +73 -0
- package/dist/agent/storage.js.map +1 -0
- package/dist/agent/stream-manager.d.ts +42 -0
- package/dist/agent/stream-manager.js +243 -0
- package/dist/agent/stream-manager.js.map +1 -0
- package/dist/agent/tool-bridge.d.ts +44 -0
- package/dist/agent/tool-bridge.js +50 -0
- package/dist/agent/tool-bridge.js.map +1 -0
- package/dist/agent/types.d.ts +6 -0
- package/dist/agent/types.js +3 -0
- package/dist/agent/types.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +224 -0
- package/dist/cli.js.map +1 -0
- package/dist/constant/index.d.ts +60 -0
- package/dist/constant/index.js +80 -0
- package/dist/constant/index.js.map +1 -0
- package/dist/file-handler.d.ts +41 -0
- package/dist/file-handler.js +295 -0
- package/dist/file-handler.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/mcp-server-stdio.d.ts +72 -0
- package/dist/mcp/mcp-server-stdio.js +143 -0
- package/dist/mcp/mcp-server-stdio.js.map +1 -0
- package/dist/mcp/mcp-server.d.ts +36 -0
- package/dist/mcp/mcp-server.js +26 -0
- package/dist/mcp/mcp-server.js.map +1 -0
- package/dist/mcp/register-tools.d.ts +2 -0
- package/dist/mcp/register-tools.js +148 -0
- package/dist/mcp/register-tools.js.map +1 -0
- package/dist/mcp/stdio-config.json +3 -0
- package/dist/native-messaging-host.d.ts +42 -0
- package/dist/native-messaging-host.js +312 -0
- package/dist/native-messaging-host.js.map +1 -0
- package/dist/run_host.bat +194 -0
- package/dist/run_host.sh +264 -0
- package/dist/scripts/browser-config.d.ts +28 -0
- package/dist/scripts/browser-config.js +229 -0
- package/dist/scripts/browser-config.js.map +1 -0
- package/dist/scripts/build.d.ts +1 -0
- package/dist/scripts/build.js +126 -0
- package/dist/scripts/build.js.map +1 -0
- package/dist/scripts/constant.d.ts +4 -0
- package/dist/scripts/constant.js +8 -0
- package/dist/scripts/constant.js.map +1 -0
- package/dist/scripts/doctor.d.ts +70 -0
- package/dist/scripts/doctor.js +930 -0
- package/dist/scripts/doctor.js.map +1 -0
- package/dist/scripts/postinstall.d.ts +2 -0
- package/dist/scripts/postinstall.js +246 -0
- package/dist/scripts/postinstall.js.map +1 -0
- package/dist/scripts/register-dev.d.ts +1 -0
- package/dist/scripts/register-dev.js +5 -0
- package/dist/scripts/register-dev.js.map +1 -0
- package/dist/scripts/register.d.ts +2 -0
- package/dist/scripts/register.js +28 -0
- package/dist/scripts/register.js.map +1 -0
- package/dist/scripts/report.d.ts +96 -0
- package/dist/scripts/report.js +686 -0
- package/dist/scripts/report.js.map +1 -0
- package/dist/scripts/utils.d.ts +64 -0
- package/dist/scripts/utils.js +443 -0
- package/dist/scripts/utils.js.map +1 -0
- package/dist/server/index.d.ts +35 -0
- package/dist/server/index.js +312 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/routes/agent.d.ts +21 -0
- package/dist/server/routes/agent.js +971 -0
- package/dist/server/routes/agent.js.map +1 -0
- package/dist/server/routes/index.d.ts +4 -0
- package/dist/server/routes/index.js +9 -0
- package/dist/server/routes/index.js.map +1 -0
- package/dist/trace-analyzer.d.ts +14 -0
- package/dist/trace-analyzer.js +113 -0
- package/dist/trace-analyzer.js.map +1 -0
- package/dist/util/logger.d.ts +1 -0
- package/dist/util/logger.js +43 -0
- package/dist/util/logger.js.map +1 -0
- package/package.json +91 -0
|
@@ -0,0 +1,1338 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ClaudeEngine = void 0;
|
|
7
|
+
const node_crypto_1 = require("node:crypto");
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const ccr_detector_1 = require("../ccr-detector");
|
|
10
|
+
const project_service_1 = require("../project-service");
|
|
11
|
+
const constant_1 = require("../../constant");
|
|
12
|
+
/**
|
|
13
|
+
* Map of tool names to their corresponding actions.
|
|
14
|
+
*/
|
|
15
|
+
const TOOL_NAME_ACTION_MAP = {
|
|
16
|
+
read: 'Read',
|
|
17
|
+
read_file: 'Read',
|
|
18
|
+
write: 'Created',
|
|
19
|
+
write_file: 'Created',
|
|
20
|
+
create_file: 'Created',
|
|
21
|
+
edit: 'Edited',
|
|
22
|
+
edit_file: 'Edited',
|
|
23
|
+
apply_patch: 'Edited',
|
|
24
|
+
patch_file: 'Edited',
|
|
25
|
+
remove_file: 'Deleted',
|
|
26
|
+
delete_file: 'Deleted',
|
|
27
|
+
list_files: 'Searched',
|
|
28
|
+
glob: 'Searched',
|
|
29
|
+
glob_files: 'Searched',
|
|
30
|
+
search_files: 'Searched',
|
|
31
|
+
grep: 'Searched',
|
|
32
|
+
bash: 'Executed',
|
|
33
|
+
run: 'Executed',
|
|
34
|
+
shell: 'Executed',
|
|
35
|
+
todo_write: 'Generated',
|
|
36
|
+
plan_write: 'Generated',
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* ClaudeEngine integrates the Claude Agent SDK as an AgentEngine implementation.
|
|
40
|
+
*
|
|
41
|
+
* This engine uses the @anthropic-ai/claude-agent-sdk to interact with Claude,
|
|
42
|
+
* streaming events back to the sidepanel UI via RealtimeEvent envelopes.
|
|
43
|
+
*/
|
|
44
|
+
class ClaudeEngine {
|
|
45
|
+
constructor() {
|
|
46
|
+
this.name = 'claude';
|
|
47
|
+
this.supportsMcp = true;
|
|
48
|
+
}
|
|
49
|
+
async initializeAndRun(options, ctx) {
|
|
50
|
+
var _a;
|
|
51
|
+
const { sessionId, instruction, model, projectRoot, requestId, signal, attachments, resolvedImagePaths, projectId, permissionMode, allowDangerouslySkipPermissions, systemPromptConfig, optionsConfig, resumeClaudeSessionId, useCcr, } = options;
|
|
52
|
+
const repoPath = this.resolveRepoPath(projectRoot);
|
|
53
|
+
// Check if already aborted
|
|
54
|
+
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
|
|
55
|
+
throw new Error('ClaudeEngine: execution was cancelled');
|
|
56
|
+
}
|
|
57
|
+
const normalizedInstruction = instruction.trim();
|
|
58
|
+
if (!normalizedInstruction) {
|
|
59
|
+
throw new Error('ClaudeEngine: instruction must not be empty');
|
|
60
|
+
}
|
|
61
|
+
// Dynamically import the Claude Agent SDK
|
|
62
|
+
// Images are passed via temp file paths appended to the prompt string
|
|
63
|
+
let query;
|
|
64
|
+
try {
|
|
65
|
+
// Dynamic import to avoid hard dependency - install @anthropic-ai/claude-agent-sdk to use this engine
|
|
66
|
+
// Use string variable to bypass TypeScript module resolution
|
|
67
|
+
const sdkModuleName = '@anthropic-ai/claude-agent-sdk';
|
|
68
|
+
const sdk = await Function('moduleName', 'return import(moduleName)')(sdkModuleName);
|
|
69
|
+
query = sdk.query;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
73
|
+
throw new Error(`ClaudeEngine: Failed to load Claude Agent SDK. Please install @anthropic-ai/claude-agent-sdk. Error: ${message}`);
|
|
74
|
+
}
|
|
75
|
+
// Resolve model
|
|
76
|
+
const resolvedModel = (model === null || model === void 0 ? void 0 : model.trim()) || process.env.CLAUDE_DEFAULT_MODEL || 'claude-sonnet-4-20250514';
|
|
77
|
+
// State management
|
|
78
|
+
const stderrBuffer = [];
|
|
79
|
+
let assistantBuffer = '';
|
|
80
|
+
let assistantMessageId = null;
|
|
81
|
+
let assistantCreatedAt = null;
|
|
82
|
+
let lastAssistantEmitted = null;
|
|
83
|
+
const streamedToolHashes = new Set();
|
|
84
|
+
// Tool input accumulation for streaming tool_use blocks
|
|
85
|
+
// Key: content block index, Value: { toolName, toolId, inputJson }
|
|
86
|
+
const pendingToolInputs = new Map();
|
|
87
|
+
let currentContentBlockIndex = -1;
|
|
88
|
+
/**
|
|
89
|
+
* Emit assistant message to the stream.
|
|
90
|
+
* Includes deduplication to prevent multiple identical final emissions.
|
|
91
|
+
*/
|
|
92
|
+
const emitAssistant = (isFinal) => {
|
|
93
|
+
const content = assistantBuffer.trim();
|
|
94
|
+
if (!content)
|
|
95
|
+
return;
|
|
96
|
+
// Deduplicate: skip if same content and isFinal state was already emitted
|
|
97
|
+
if (lastAssistantEmitted &&
|
|
98
|
+
lastAssistantEmitted.content === content &&
|
|
99
|
+
lastAssistantEmitted.isFinal === isFinal) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
lastAssistantEmitted = { content, isFinal };
|
|
103
|
+
if (!assistantMessageId) {
|
|
104
|
+
assistantMessageId = (0, node_crypto_1.randomUUID)();
|
|
105
|
+
}
|
|
106
|
+
if (!assistantCreatedAt) {
|
|
107
|
+
assistantCreatedAt = new Date().toISOString();
|
|
108
|
+
}
|
|
109
|
+
const message = {
|
|
110
|
+
id: assistantMessageId,
|
|
111
|
+
sessionId,
|
|
112
|
+
role: 'assistant',
|
|
113
|
+
content,
|
|
114
|
+
messageType: 'chat',
|
|
115
|
+
cliSource: this.name,
|
|
116
|
+
requestId,
|
|
117
|
+
isStreaming: !isFinal,
|
|
118
|
+
isFinal,
|
|
119
|
+
createdAt: assistantCreatedAt,
|
|
120
|
+
};
|
|
121
|
+
ctx.emit({ type: 'message', data: message });
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Emit tool message with deduplication.
|
|
125
|
+
*/
|
|
126
|
+
const dispatchToolMessage = (content, metadata, messageType, isStreaming) => {
|
|
127
|
+
const trimmed = content.trim();
|
|
128
|
+
if (!trimmed)
|
|
129
|
+
return;
|
|
130
|
+
const hash = this.encodeHash(`${messageType}:${trimmed}:${JSON.stringify(metadata)}:${sessionId}:${requestId || ''}`).slice(0, 16);
|
|
131
|
+
if (streamedToolHashes.has(hash))
|
|
132
|
+
return;
|
|
133
|
+
streamedToolHashes.add(hash);
|
|
134
|
+
const message = {
|
|
135
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
136
|
+
sessionId,
|
|
137
|
+
role: 'tool',
|
|
138
|
+
content: trimmed,
|
|
139
|
+
messageType,
|
|
140
|
+
cliSource: this.name,
|
|
141
|
+
requestId,
|
|
142
|
+
isStreaming,
|
|
143
|
+
isFinal: !isStreaming,
|
|
144
|
+
createdAt: new Date().toISOString(),
|
|
145
|
+
metadata: { cli_type: 'claude', ...metadata },
|
|
146
|
+
};
|
|
147
|
+
ctx.emit({ type: 'message', data: message });
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Infer tool action from tool name.
|
|
151
|
+
*/
|
|
152
|
+
const inferActionFromToolName = (toolName) => {
|
|
153
|
+
var _a;
|
|
154
|
+
if (typeof toolName !== 'string')
|
|
155
|
+
return undefined;
|
|
156
|
+
const normalized = toolName.trim().toLowerCase();
|
|
157
|
+
if (!normalized)
|
|
158
|
+
return undefined;
|
|
159
|
+
if (TOOL_NAME_ACTION_MAP[normalized]) {
|
|
160
|
+
return TOOL_NAME_ACTION_MAP[normalized];
|
|
161
|
+
}
|
|
162
|
+
// Try suffix after colon (e.g., "mcp__server__tool" -> "tool")
|
|
163
|
+
const suffix = (_a = normalized.split(':').pop()) !== null && _a !== void 0 ? _a : normalized;
|
|
164
|
+
if (suffix && TOOL_NAME_ACTION_MAP[suffix]) {
|
|
165
|
+
return TOOL_NAME_ACTION_MAP[suffix];
|
|
166
|
+
}
|
|
167
|
+
// Infer from name patterns
|
|
168
|
+
if (normalized.includes('edit') ||
|
|
169
|
+
normalized.includes('modify') ||
|
|
170
|
+
normalized.includes('patch')) {
|
|
171
|
+
return 'Edited';
|
|
172
|
+
}
|
|
173
|
+
if (normalized.includes('write') || normalized.includes('create')) {
|
|
174
|
+
return 'Created';
|
|
175
|
+
}
|
|
176
|
+
if (normalized.includes('read') || normalized.includes('view')) {
|
|
177
|
+
return 'Read';
|
|
178
|
+
}
|
|
179
|
+
if (normalized.includes('delete') || normalized.includes('remove')) {
|
|
180
|
+
return 'Deleted';
|
|
181
|
+
}
|
|
182
|
+
if (normalized.includes('search') ||
|
|
183
|
+
normalized.includes('find') ||
|
|
184
|
+
normalized.includes('glob') ||
|
|
185
|
+
normalized.includes('grep')) {
|
|
186
|
+
return 'Searched';
|
|
187
|
+
}
|
|
188
|
+
if (normalized.includes('bash') ||
|
|
189
|
+
normalized.includes('shell') ||
|
|
190
|
+
normalized.includes('exec')) {
|
|
191
|
+
return 'Executed';
|
|
192
|
+
}
|
|
193
|
+
if (normalized.includes('todo') || normalized.includes('plan')) {
|
|
194
|
+
return 'Generated';
|
|
195
|
+
}
|
|
196
|
+
return undefined;
|
|
197
|
+
};
|
|
198
|
+
/**
|
|
199
|
+
* Build tool metadata from content block with detailed tool-specific information.
|
|
200
|
+
*/
|
|
201
|
+
const buildToolMetadata = (contentBlock) => {
|
|
202
|
+
const toolName = this.pickFirstString(contentBlock.name) || 'unknown';
|
|
203
|
+
const toolId = this.pickFirstString(contentBlock.id);
|
|
204
|
+
const input = contentBlock.input;
|
|
205
|
+
const action = inferActionFromToolName(toolName);
|
|
206
|
+
const metadata = {
|
|
207
|
+
toolName,
|
|
208
|
+
tool_name: toolName,
|
|
209
|
+
toolId,
|
|
210
|
+
action,
|
|
211
|
+
};
|
|
212
|
+
if (!input) {
|
|
213
|
+
return metadata;
|
|
214
|
+
}
|
|
215
|
+
// Extract tool-specific details
|
|
216
|
+
const normalizedName = toolName.toLowerCase();
|
|
217
|
+
// File operations (read, write, edit)
|
|
218
|
+
if (typeof input.file_path === 'string') {
|
|
219
|
+
metadata.filePath = input.file_path;
|
|
220
|
+
}
|
|
221
|
+
// Edit tool - extract diff information
|
|
222
|
+
if (normalizedName.includes('edit') ||
|
|
223
|
+
normalizedName === 'apply_patch' ||
|
|
224
|
+
normalizedName === 'patch_file') {
|
|
225
|
+
if (typeof input.old_string === 'string') {
|
|
226
|
+
metadata.oldString = input.old_string;
|
|
227
|
+
metadata.deletedLines = input.old_string.split('\n').length;
|
|
228
|
+
}
|
|
229
|
+
if (typeof input.new_string === 'string') {
|
|
230
|
+
metadata.newString = input.new_string;
|
|
231
|
+
metadata.addedLines = input.new_string.split('\n').length;
|
|
232
|
+
}
|
|
233
|
+
if (typeof input.replace_all === 'boolean') {
|
|
234
|
+
metadata.replaceAll = input.replace_all;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// Write tool - content preview
|
|
238
|
+
if (normalizedName.includes('write') || normalizedName === 'create_file') {
|
|
239
|
+
if (typeof input.content === 'string') {
|
|
240
|
+
metadata.contentPreview = input.content.slice(0, 200);
|
|
241
|
+
metadata.totalLines = input.content.split('\n').length;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// Read tool - offset/limit
|
|
245
|
+
if (normalizedName.includes('read')) {
|
|
246
|
+
if (typeof input.offset === 'number')
|
|
247
|
+
metadata.offset = input.offset;
|
|
248
|
+
if (typeof input.limit === 'number')
|
|
249
|
+
metadata.limit = input.limit;
|
|
250
|
+
}
|
|
251
|
+
// Bash/shell - command
|
|
252
|
+
if (normalizedName === 'bash' ||
|
|
253
|
+
normalizedName.includes('shell') ||
|
|
254
|
+
normalizedName === 'run') {
|
|
255
|
+
if (typeof input.command === 'string') {
|
|
256
|
+
metadata.command = input.command;
|
|
257
|
+
}
|
|
258
|
+
if (typeof input.description === 'string') {
|
|
259
|
+
metadata.commandDescription = input.description;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Search tools (grep, glob)
|
|
263
|
+
if (normalizedName === 'grep' || normalizedName.includes('search')) {
|
|
264
|
+
if (typeof input.pattern === 'string')
|
|
265
|
+
metadata.pattern = input.pattern;
|
|
266
|
+
if (typeof input.path === 'string')
|
|
267
|
+
metadata.searchPath = input.path;
|
|
268
|
+
if (typeof input.glob === 'string')
|
|
269
|
+
metadata.glob = input.glob;
|
|
270
|
+
if (typeof input.output_mode === 'string')
|
|
271
|
+
metadata.outputMode = input.output_mode;
|
|
272
|
+
}
|
|
273
|
+
if (normalizedName === 'glob' || normalizedName === 'glob_files') {
|
|
274
|
+
if (typeof input.pattern === 'string')
|
|
275
|
+
metadata.pattern = input.pattern;
|
|
276
|
+
if (typeof input.path === 'string')
|
|
277
|
+
metadata.searchPath = input.path;
|
|
278
|
+
}
|
|
279
|
+
// TodoWrite
|
|
280
|
+
if (normalizedName === 'todo_write' || normalizedName === 'todowrite') {
|
|
281
|
+
if (Array.isArray(input.todos)) {
|
|
282
|
+
metadata.todoCount = input.todos.length;
|
|
283
|
+
metadata.todos = input.todos;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// Store raw input for debugging (truncated)
|
|
287
|
+
metadata.rawInput = JSON.stringify(input).slice(0, 1000);
|
|
288
|
+
return metadata;
|
|
289
|
+
};
|
|
290
|
+
// State for temp file cleanup
|
|
291
|
+
const tempFiles = [];
|
|
292
|
+
const cleanupTempFiles = async () => {
|
|
293
|
+
if (tempFiles.length === 0)
|
|
294
|
+
return;
|
|
295
|
+
try {
|
|
296
|
+
const fs = await import('node:fs/promises');
|
|
297
|
+
for (const filePath of tempFiles) {
|
|
298
|
+
try {
|
|
299
|
+
await fs.unlink(filePath);
|
|
300
|
+
console.error(`[ClaudeEngine] Cleaned up temp file: ${filePath}`);
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
// Best-effort cleanup; ignore failures (file may already be deleted)
|
|
304
|
+
console.error(`[ClaudeEngine] Failed to cleanup temp file ${filePath}:`, err);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
catch (err) {
|
|
309
|
+
console.error('[ClaudeEngine] Failed to cleanup temp files:', err);
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
// Build prompt instruction (may be modified if images are attached)
|
|
313
|
+
let promptInstruction = normalizedInstruction;
|
|
314
|
+
try {
|
|
315
|
+
// Use console.error for logging to avoid polluting stdout (Native Messaging protocol)
|
|
316
|
+
console.error(`[ClaudeEngine] Starting query with model: ${resolvedModel}`);
|
|
317
|
+
console.error(`[ClaudeEngine] Working directory: ${repoPath}`);
|
|
318
|
+
// Check for image attachments - prefer resolvedImagePaths (persisted), fallback to temp files
|
|
319
|
+
const hasResolvedPaths = resolvedImagePaths && resolvedImagePaths.length > 0;
|
|
320
|
+
const imageAttachments = (attachments !== null && attachments !== void 0 ? attachments : []).filter((a) => a.type === 'image');
|
|
321
|
+
const hasImages = hasResolvedPaths || imageAttachments.length > 0;
|
|
322
|
+
if (hasImages) {
|
|
323
|
+
// Strip any legacy "Image #N path:" lines to avoid duplicating references
|
|
324
|
+
const instructionWithoutLegacyPaths = normalizedInstruction
|
|
325
|
+
.replace(/\n*Image #\d+ path: [^\n]+/g, '')
|
|
326
|
+
.trim();
|
|
327
|
+
const imageLines = [];
|
|
328
|
+
if (hasResolvedPaths) {
|
|
329
|
+
// Use pre-resolved persistent paths (preferred - no temp files needed)
|
|
330
|
+
console.error(`[ClaudeEngine] Using ${resolvedImagePaths.length} pre-resolved image path(s)`);
|
|
331
|
+
for (let index = 0; index < resolvedImagePaths.length; index++) {
|
|
332
|
+
imageLines.push(`Image #${index + 1} path: ${resolvedImagePaths[index]}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
// Fallback: write base64 to temp files (legacy behavior)
|
|
337
|
+
console.error(`[ClaudeEngine] Writing ${imageAttachments.length} image attachment(s) to temp files (fallback)`);
|
|
338
|
+
for (let index = 0; index < imageAttachments.length; index++) {
|
|
339
|
+
const attachment = imageAttachments[index];
|
|
340
|
+
const tempFilePath = await this.writeAttachmentToTemp(attachment);
|
|
341
|
+
tempFiles.push(tempFilePath);
|
|
342
|
+
imageLines.push(`Image #${index + 1} path: ${tempFilePath}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// Build final instruction with image paths appended
|
|
346
|
+
promptInstruction = [instructionWithoutLegacyPaths, imageLines.join('\n')]
|
|
347
|
+
.filter((segment) => segment && segment.trim().length > 0)
|
|
348
|
+
.join('\n\n')
|
|
349
|
+
.trim();
|
|
350
|
+
console.error(`[ClaudeEngine] Prompt with image paths: ${promptInstruction.slice(0, 200)}...`);
|
|
351
|
+
}
|
|
352
|
+
// Start Claude Agent SDK query
|
|
353
|
+
// Session resumption: if resumeClaudeSessionId is provided (from sessions.engineSessionId or legacy project),
|
|
354
|
+
// pass it as 'resume' to continue a previous Claude conversation.
|
|
355
|
+
// If not provided, SDK will create a new session.
|
|
356
|
+
// Build environment for Claude Code Router support
|
|
357
|
+
// SDK treats options.env as a complete replacement, so we must merge with process.env
|
|
358
|
+
// Reference: https://github.com/musistudio/claude-code-router/issues/855
|
|
359
|
+
const claudeEnv = await this.buildClaudeEnv(useCcr);
|
|
360
|
+
// Validate CCR configuration and emit friendly warning before calling into CCR
|
|
361
|
+
// This prevents users from seeing cryptic "includes of undefined" errors
|
|
362
|
+
if (useCcr) {
|
|
363
|
+
await this.validateAndWarnCcrConfig(sessionId, requestId, ctx);
|
|
364
|
+
}
|
|
365
|
+
// Resolve permission mode from session config or use default
|
|
366
|
+
// SDK default is 'default', but AgentChat defaults to 'bypassPermissions' for headless operation
|
|
367
|
+
const allowedPermissionModes = new Set([
|
|
368
|
+
'default',
|
|
369
|
+
'acceptEdits',
|
|
370
|
+
'bypassPermissions',
|
|
371
|
+
'plan',
|
|
372
|
+
'dontAsk',
|
|
373
|
+
]);
|
|
374
|
+
const normalizedPermissionMode = typeof permissionMode === 'string' ? permissionMode.trim() : '';
|
|
375
|
+
let resolvedPermissionMode;
|
|
376
|
+
if (normalizedPermissionMode === '') {
|
|
377
|
+
// No permission mode specified - use AgentChat default for headless operation
|
|
378
|
+
resolvedPermissionMode = 'bypassPermissions';
|
|
379
|
+
}
|
|
380
|
+
else if (allowedPermissionModes.has(normalizedPermissionMode)) {
|
|
381
|
+
// Valid permission mode - use as specified
|
|
382
|
+
resolvedPermissionMode = normalizedPermissionMode;
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
// Invalid permission mode - fall back to SDK default and warn
|
|
386
|
+
console.error(`[ClaudeEngine] Invalid permissionMode "${normalizedPermissionMode}", falling back to SDK default "default"`);
|
|
387
|
+
resolvedPermissionMode = 'default';
|
|
388
|
+
}
|
|
389
|
+
// allowDangerouslySkipPermissions must be true when using bypassPermissions mode
|
|
390
|
+
// SDK requirement: bypass mode requires explicit acknowledgment via allowDangerouslySkipPermissions=true
|
|
391
|
+
const resolvedAllowDangerouslySkipPermissions = (() => {
|
|
392
|
+
const explicitValue = typeof allowDangerouslySkipPermissions === 'boolean'
|
|
393
|
+
? allowDangerouslySkipPermissions
|
|
394
|
+
: undefined;
|
|
395
|
+
if (resolvedPermissionMode === 'bypassPermissions') {
|
|
396
|
+
// Force true for bypassPermissions mode - SDK requirement
|
|
397
|
+
if (explicitValue === false) {
|
|
398
|
+
console.error('[ClaudeEngine] Warning: allowDangerouslySkipPermissions=false is incompatible with bypassPermissions mode, forcing to true');
|
|
399
|
+
}
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
// For non-bypass modes, use explicit value or default to false
|
|
403
|
+
return explicitValue !== null && explicitValue !== void 0 ? explicitValue : false;
|
|
404
|
+
})();
|
|
405
|
+
// Parse optionsConfig for additional SDK options
|
|
406
|
+
const optionsRecord = optionsConfig && typeof optionsConfig === 'object' && !Array.isArray(optionsConfig)
|
|
407
|
+
? optionsConfig
|
|
408
|
+
: undefined;
|
|
409
|
+
// Resolve project-scoped Chrome MCP toggle (default: enabled)
|
|
410
|
+
const enableChromeMcp = await (async () => {
|
|
411
|
+
if (!projectId)
|
|
412
|
+
return true;
|
|
413
|
+
try {
|
|
414
|
+
const project = await (0, project_service_1.getProject)(projectId);
|
|
415
|
+
return (project === null || project === void 0 ? void 0 : project.enableChromeMcp) !== false;
|
|
416
|
+
}
|
|
417
|
+
catch (err) {
|
|
418
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
419
|
+
console.error(`[ClaudeEngine] Failed to load project enableChromeMcp, defaulting to enabled: ${message}`);
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
})();
|
|
423
|
+
// Resolve setting sources
|
|
424
|
+
// SDK isolation mode: settingSources=[] prevents loading any filesystem settings
|
|
425
|
+
// Default behavior: include 'project' to load CLAUDE.md
|
|
426
|
+
const resolvedSettingSources = (() => {
|
|
427
|
+
const allowedSettingSources = new Set(['user', 'project', 'local']);
|
|
428
|
+
const raw = optionsRecord === null || optionsRecord === void 0 ? void 0 : optionsRecord.settingSources;
|
|
429
|
+
// Check for explicit isolation mode (empty array)
|
|
430
|
+
if (Array.isArray(raw) && raw.length === 0) {
|
|
431
|
+
console.error('[ClaudeEngine] Isolation mode enabled: settingSources=[]');
|
|
432
|
+
return [];
|
|
433
|
+
}
|
|
434
|
+
// Parse provided sources
|
|
435
|
+
if (Array.isArray(raw)) {
|
|
436
|
+
const sources = [];
|
|
437
|
+
for (const entry of raw) {
|
|
438
|
+
if (typeof entry === 'string' && allowedSettingSources.has(entry)) {
|
|
439
|
+
sources.push(entry);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// If valid sources were provided, use them as-is (trust user config)
|
|
443
|
+
if (sources.length > 0) {
|
|
444
|
+
return sources;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
// Default: include 'project' to load CLAUDE.md
|
|
448
|
+
return ['project'];
|
|
449
|
+
})();
|
|
450
|
+
// Resolve system prompt from session config
|
|
451
|
+
const resolvedSystemPrompt = (() => {
|
|
452
|
+
if (typeof systemPromptConfig === 'string') {
|
|
453
|
+
const trimmed = systemPromptConfig.trim();
|
|
454
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
455
|
+
}
|
|
456
|
+
if (!systemPromptConfig ||
|
|
457
|
+
typeof systemPromptConfig !== 'object' ||
|
|
458
|
+
Array.isArray(systemPromptConfig)) {
|
|
459
|
+
return undefined;
|
|
460
|
+
}
|
|
461
|
+
const record = systemPromptConfig;
|
|
462
|
+
const type = record.type;
|
|
463
|
+
if (type === 'custom' && typeof record.text === 'string') {
|
|
464
|
+
const trimmed = record.text.trim();
|
|
465
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
466
|
+
}
|
|
467
|
+
if (type === 'preset' && record.preset === 'claude_code') {
|
|
468
|
+
// Trim append and ignore empty strings to avoid "append is empty but object is passed" edge case
|
|
469
|
+
const rawAppend = typeof record.append === 'string' ? record.append.trim() : '';
|
|
470
|
+
const append = rawAppend.length > 0 ? rawAppend : undefined;
|
|
471
|
+
return append
|
|
472
|
+
? { type: 'preset', preset: 'claude_code', append }
|
|
473
|
+
: { type: 'preset', preset: 'claude_code' };
|
|
474
|
+
}
|
|
475
|
+
return undefined;
|
|
476
|
+
})();
|
|
477
|
+
// Create internal AbortController that mirrors the external signal
|
|
478
|
+
// SDK expects abortController option, not raw AbortSignal
|
|
479
|
+
const internalAbortController = new AbortController();
|
|
480
|
+
if (signal) {
|
|
481
|
+
// Propagate external abort to internal controller
|
|
482
|
+
if (signal.aborted) {
|
|
483
|
+
internalAbortController.abort();
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
signal.addEventListener('abort', () => {
|
|
487
|
+
internalAbortController.abort();
|
|
488
|
+
}, { once: true });
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
const queryOptions = {
|
|
492
|
+
cwd: repoPath,
|
|
493
|
+
additionalDirectories: [repoPath],
|
|
494
|
+
model: resolvedModel,
|
|
495
|
+
// Permission settings are session-configurable (defaults preserve previous behavior)
|
|
496
|
+
permissionMode: resolvedPermissionMode,
|
|
497
|
+
allowDangerouslySkipPermissions: resolvedAllowDangerouslySkipPermissions,
|
|
498
|
+
// Enable streaming: emit stream_event with content_block_delta for real-time UI updates
|
|
499
|
+
// Without this, SDK only outputs aggregated assistant/result messages
|
|
500
|
+
includePartialMessages: true,
|
|
501
|
+
// Load CLAUDE.md / .claude/settings.json from the project root
|
|
502
|
+
settingSources: resolvedSettingSources,
|
|
503
|
+
// Custom system prompt if provided
|
|
504
|
+
systemPrompt: resolvedSystemPrompt,
|
|
505
|
+
// AbortController for cancellation support - SDK uses this to terminate underlying processes
|
|
506
|
+
abortController: internalAbortController,
|
|
507
|
+
// Pass merged env to support Claude Code Router (CCR)
|
|
508
|
+
// This allows users to set ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN via:
|
|
509
|
+
// 1. eval "$(ccr activate)" before launching Chrome
|
|
510
|
+
// 2. Or setting env vars in shell profile
|
|
511
|
+
env: claudeEnv,
|
|
512
|
+
stderr: (data) => {
|
|
513
|
+
const line = String(data).trimEnd();
|
|
514
|
+
if (!line)
|
|
515
|
+
return;
|
|
516
|
+
if (stderrBuffer.length > ClaudeEngine.MAX_STDERR_LINES) {
|
|
517
|
+
stderrBuffer.shift();
|
|
518
|
+
}
|
|
519
|
+
stderrBuffer.push(line);
|
|
520
|
+
console.error(`[ClaudeEngine][stderr] ${line}`);
|
|
521
|
+
},
|
|
522
|
+
};
|
|
523
|
+
// Apply additional SDK options from optionsConfig
|
|
524
|
+
if (optionsRecord) {
|
|
525
|
+
const isStringArray = (value) => Array.isArray(value) && value.every((v) => typeof v === 'string');
|
|
526
|
+
if (isStringArray(optionsRecord.allowedTools)) {
|
|
527
|
+
queryOptions.allowedTools = optionsRecord.allowedTools;
|
|
528
|
+
}
|
|
529
|
+
if (isStringArray(optionsRecord.disallowedTools)) {
|
|
530
|
+
queryOptions.disallowedTools = optionsRecord.disallowedTools;
|
|
531
|
+
}
|
|
532
|
+
const tools = optionsRecord.tools;
|
|
533
|
+
if (isStringArray(tools)) {
|
|
534
|
+
queryOptions.tools = tools;
|
|
535
|
+
}
|
|
536
|
+
else if (tools && typeof tools === 'object' && !Array.isArray(tools)) {
|
|
537
|
+
const toolsRecord = tools;
|
|
538
|
+
if (toolsRecord.type === 'preset' && toolsRecord.preset === 'claude_code') {
|
|
539
|
+
queryOptions.tools = { type: 'preset', preset: 'claude_code' };
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (isStringArray(optionsRecord.betas)) {
|
|
543
|
+
queryOptions.betas = optionsRecord.betas;
|
|
544
|
+
}
|
|
545
|
+
if (typeof optionsRecord.maxThinkingTokens === 'number' &&
|
|
546
|
+
Number.isFinite(optionsRecord.maxThinkingTokens)) {
|
|
547
|
+
queryOptions.maxThinkingTokens = optionsRecord.maxThinkingTokens;
|
|
548
|
+
}
|
|
549
|
+
if (typeof optionsRecord.maxTurns === 'number' && Number.isFinite(optionsRecord.maxTurns)) {
|
|
550
|
+
queryOptions.maxTurns = optionsRecord.maxTurns;
|
|
551
|
+
}
|
|
552
|
+
if (typeof optionsRecord.maxBudgetUsd === 'number' &&
|
|
553
|
+
Number.isFinite(optionsRecord.maxBudgetUsd)) {
|
|
554
|
+
queryOptions.maxBudgetUsd = optionsRecord.maxBudgetUsd;
|
|
555
|
+
}
|
|
556
|
+
if (optionsRecord.mcpServers &&
|
|
557
|
+
typeof optionsRecord.mcpServers === 'object' &&
|
|
558
|
+
!Array.isArray(optionsRecord.mcpServers)) {
|
|
559
|
+
queryOptions.mcpServers = optionsRecord.mcpServers;
|
|
560
|
+
}
|
|
561
|
+
if (optionsRecord.outputFormat &&
|
|
562
|
+
typeof optionsRecord.outputFormat === 'object' &&
|
|
563
|
+
!Array.isArray(optionsRecord.outputFormat)) {
|
|
564
|
+
queryOptions.outputFormat = optionsRecord.outputFormat;
|
|
565
|
+
}
|
|
566
|
+
if (typeof optionsRecord.enableFileCheckpointing === 'boolean') {
|
|
567
|
+
queryOptions.enableFileCheckpointing = optionsRecord.enableFileCheckpointing;
|
|
568
|
+
}
|
|
569
|
+
if (optionsRecord.sandbox &&
|
|
570
|
+
typeof optionsRecord.sandbox === 'object' &&
|
|
571
|
+
!Array.isArray(optionsRecord.sandbox)) {
|
|
572
|
+
queryOptions.sandbox = optionsRecord.sandbox;
|
|
573
|
+
}
|
|
574
|
+
// Merge session-level env overrides with base claudeEnv
|
|
575
|
+
// Session env takes precedence over process env (useful for per-session API keys, etc.)
|
|
576
|
+
if (optionsRecord.env &&
|
|
577
|
+
typeof optionsRecord.env === 'object' &&
|
|
578
|
+
!Array.isArray(optionsRecord.env)) {
|
|
579
|
+
const sessionEnv = optionsRecord.env;
|
|
580
|
+
const mergedEnv = { ...claudeEnv };
|
|
581
|
+
for (const [key, value] of Object.entries(sessionEnv)) {
|
|
582
|
+
if (typeof value === 'string') {
|
|
583
|
+
mergedEnv[key] = value;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
// Ensure Node.js bin directory is still in PATH after merge
|
|
587
|
+
// Session may have overwritten PATH, which would break child processes
|
|
588
|
+
const nodeBinDir = node_path_1.default.dirname(process.execPath);
|
|
589
|
+
const mergedPath = mergedEnv.PATH || mergedEnv.Path || '';
|
|
590
|
+
if (!mergedPath.includes(nodeBinDir)) {
|
|
591
|
+
mergedEnv.PATH = [nodeBinDir, mergedPath].filter(Boolean).join(node_path_1.default.delimiter);
|
|
592
|
+
}
|
|
593
|
+
queryOptions.env = mergedEnv;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
// Inject the local Chrome MCP server based on project preference.
|
|
597
|
+
// This only controls the built-in "chrome-mcp" entry; user-configured MCP servers remain untouched.
|
|
598
|
+
const CHROME_MCP_SERVER_NAME = 'chrome-mcp';
|
|
599
|
+
if (enableChromeMcp) {
|
|
600
|
+
const existingMcpServers = queryOptions.mcpServers &&
|
|
601
|
+
typeof queryOptions.mcpServers === 'object' &&
|
|
602
|
+
!Array.isArray(queryOptions.mcpServers)
|
|
603
|
+
? queryOptions.mcpServers
|
|
604
|
+
: {};
|
|
605
|
+
queryOptions.mcpServers = {
|
|
606
|
+
...existingMcpServers,
|
|
607
|
+
[CHROME_MCP_SERVER_NAME]: {
|
|
608
|
+
type: 'http',
|
|
609
|
+
url: (0, constant_1.getChromeMcpUrl)(),
|
|
610
|
+
},
|
|
611
|
+
};
|
|
612
|
+
console.error(`[ClaudeEngine] Chrome MCP server enabled: ${(0, constant_1.getChromeMcpUrl)()}`);
|
|
613
|
+
}
|
|
614
|
+
else if (queryOptions.mcpServers &&
|
|
615
|
+
typeof queryOptions.mcpServers === 'object' &&
|
|
616
|
+
!Array.isArray(queryOptions.mcpServers)) {
|
|
617
|
+
// If Chrome MCP is disabled, remove it from existing mcpServers if present
|
|
618
|
+
const existing = queryOptions.mcpServers;
|
|
619
|
+
if (CHROME_MCP_SERVER_NAME in existing) {
|
|
620
|
+
const { [CHROME_MCP_SERVER_NAME]: _removed, ...rest } = existing;
|
|
621
|
+
if (Object.keys(rest).length > 0) {
|
|
622
|
+
queryOptions.mcpServers = rest;
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
delete queryOptions.mcpServers;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
console.error('[ClaudeEngine] Chrome MCP server disabled');
|
|
629
|
+
}
|
|
630
|
+
// Add resume option if we have a valid Claude session ID
|
|
631
|
+
if (resumeClaudeSessionId) {
|
|
632
|
+
queryOptions.resume = resumeClaudeSessionId;
|
|
633
|
+
console.error(`[ClaudeEngine] Resuming Claude session: ${resumeClaudeSessionId}`);
|
|
634
|
+
}
|
|
635
|
+
const response = query({
|
|
636
|
+
prompt: promptInstruction,
|
|
637
|
+
options: queryOptions,
|
|
638
|
+
});
|
|
639
|
+
// Process streaming response
|
|
640
|
+
for await (const message of response) {
|
|
641
|
+
// Check for cancellation before processing each message
|
|
642
|
+
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
|
|
643
|
+
console.error('[ClaudeEngine] Execution cancelled via abort signal');
|
|
644
|
+
throw new Error('ClaudeEngine: execution was cancelled');
|
|
645
|
+
}
|
|
646
|
+
console.error('[ClaudeEngine] Message type:', message.type);
|
|
647
|
+
if (message.type === 'stream_event') {
|
|
648
|
+
const event = (_a = message.event) !== null && _a !== void 0 ? _a : {};
|
|
649
|
+
const eventType = this.pickFirstString(event.type);
|
|
650
|
+
switch (eventType) {
|
|
651
|
+
case 'message_start': {
|
|
652
|
+
// Reset assistant state for new message
|
|
653
|
+
assistantBuffer = '';
|
|
654
|
+
assistantMessageId = (0, node_crypto_1.randomUUID)();
|
|
655
|
+
assistantCreatedAt = new Date().toISOString();
|
|
656
|
+
lastAssistantEmitted = null;
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
case 'content_block_start': {
|
|
660
|
+
const contentBlock = event.content_block;
|
|
661
|
+
const blockIndex = typeof event.index === 'number' ? event.index : ++currentContentBlockIndex;
|
|
662
|
+
currentContentBlockIndex = blockIndex;
|
|
663
|
+
if (contentBlock && contentBlock.type === 'tool_use') {
|
|
664
|
+
const toolName = this.pickFirstString(contentBlock.name) || 'tool';
|
|
665
|
+
const toolId = this.pickFirstString(contentBlock.id) || '';
|
|
666
|
+
// Store pending tool input for accumulation
|
|
667
|
+
// Don't emit message here - wait for content_block_stop with complete input
|
|
668
|
+
pendingToolInputs.set(blockIndex, {
|
|
669
|
+
toolName,
|
|
670
|
+
toolId,
|
|
671
|
+
inputJsonParts: [],
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
else if (contentBlock && contentBlock.type === 'tool_result') {
|
|
675
|
+
// Handle tool_result in content_block_start
|
|
676
|
+
const metadata = this.buildToolResultMetadata(contentBlock);
|
|
677
|
+
const content = this.extractToolResultContent(contentBlock);
|
|
678
|
+
const isError = contentBlock.is_error === true;
|
|
679
|
+
dispatchToolMessage(isError
|
|
680
|
+
? `Error: ${content || 'Tool execution failed'}`
|
|
681
|
+
: content || 'Tool completed', metadata, 'tool_result', false);
|
|
682
|
+
}
|
|
683
|
+
break;
|
|
684
|
+
}
|
|
685
|
+
case 'content_block_stop': {
|
|
686
|
+
const blockIndex = typeof event.index === 'number' ? event.index : currentContentBlockIndex;
|
|
687
|
+
// Check if we have accumulated tool input for this block
|
|
688
|
+
if (pendingToolInputs.has(blockIndex)) {
|
|
689
|
+
const pending = pendingToolInputs.get(blockIndex);
|
|
690
|
+
pendingToolInputs.delete(blockIndex);
|
|
691
|
+
// Parse the accumulated JSON
|
|
692
|
+
const fullJsonStr = pending.inputJsonParts.join('');
|
|
693
|
+
let input = {};
|
|
694
|
+
try {
|
|
695
|
+
if (fullJsonStr) {
|
|
696
|
+
input = JSON.parse(fullJsonStr);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
catch (e) {
|
|
700
|
+
console.error(`[ClaudeEngine] Failed to parse tool input JSON: ${e}`);
|
|
701
|
+
}
|
|
702
|
+
console.error(`[ClaudeEngine] content_block_stop - toolName: ${pending.toolName}, input: ${JSON.stringify(input).slice(0, 500)}`);
|
|
703
|
+
// Build metadata with full input
|
|
704
|
+
const metadata = buildToolMetadata({
|
|
705
|
+
name: pending.toolName,
|
|
706
|
+
id: pending.toolId,
|
|
707
|
+
input,
|
|
708
|
+
});
|
|
709
|
+
// Build informative content
|
|
710
|
+
let content = `Using tool: ${pending.toolName}`;
|
|
711
|
+
if (input.command)
|
|
712
|
+
content = `Running: ${input.command}`;
|
|
713
|
+
else if (input.file_path)
|
|
714
|
+
content = `Operating on: ${input.file_path}`;
|
|
715
|
+
else if (input.pattern)
|
|
716
|
+
content = `Searching: ${input.pattern}`;
|
|
717
|
+
else if (input.query)
|
|
718
|
+
content = `Searching: ${input.query}`;
|
|
719
|
+
// Emit final tool_use message with complete metadata
|
|
720
|
+
dispatchToolMessage(content, metadata, 'tool_use', false);
|
|
721
|
+
}
|
|
722
|
+
// Check if this block was a tool_result
|
|
723
|
+
const contentBlock = event.content_block;
|
|
724
|
+
if (contentBlock && contentBlock.type === 'tool_result') {
|
|
725
|
+
const metadata = this.buildToolResultMetadata(contentBlock);
|
|
726
|
+
const content = this.extractToolResultContent(contentBlock);
|
|
727
|
+
const isError = contentBlock.is_error === true;
|
|
728
|
+
dispatchToolMessage(isError
|
|
729
|
+
? `Error: ${content || 'Tool execution failed'}`
|
|
730
|
+
: content || 'Tool completed', metadata, 'tool_result', false);
|
|
731
|
+
}
|
|
732
|
+
break;
|
|
733
|
+
}
|
|
734
|
+
case 'content_block_delta': {
|
|
735
|
+
const delta = event.delta;
|
|
736
|
+
const blockIndex = typeof event.index === 'number' ? event.index : currentContentBlockIndex;
|
|
737
|
+
// Check if this is a tool_use input_json_delta
|
|
738
|
+
if (delta && typeof delta === 'object' && delta.type === 'input_json_delta') {
|
|
739
|
+
const partialJson = delta.partial_json;
|
|
740
|
+
if (partialJson && pendingToolInputs.has(blockIndex)) {
|
|
741
|
+
pendingToolInputs.get(blockIndex).inputJsonParts.push(partialJson);
|
|
742
|
+
}
|
|
743
|
+
break;
|
|
744
|
+
}
|
|
745
|
+
// Handle text delta for assistant messages
|
|
746
|
+
let textChunk = '';
|
|
747
|
+
if (typeof delta === 'string') {
|
|
748
|
+
textChunk = delta;
|
|
749
|
+
}
|
|
750
|
+
else if (delta && typeof delta === 'object') {
|
|
751
|
+
if (typeof delta.text === 'string') {
|
|
752
|
+
textChunk = delta.text;
|
|
753
|
+
}
|
|
754
|
+
else if (typeof delta.delta === 'string') {
|
|
755
|
+
textChunk = delta.delta;
|
|
756
|
+
}
|
|
757
|
+
else if (typeof delta.partial === 'string') {
|
|
758
|
+
textChunk = delta.partial;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
if (textChunk) {
|
|
762
|
+
assistantBuffer += textChunk;
|
|
763
|
+
emitAssistant(false);
|
|
764
|
+
}
|
|
765
|
+
break;
|
|
766
|
+
}
|
|
767
|
+
case 'message_delta': {
|
|
768
|
+
// message_delta usually contains metadata only (stop_reason, usage)
|
|
769
|
+
// Don't emit final here to avoid duplicate finals
|
|
770
|
+
break;
|
|
771
|
+
}
|
|
772
|
+
case 'message_stop': {
|
|
773
|
+
// Emit final assistant message only on message_stop
|
|
774
|
+
emitAssistant(true);
|
|
775
|
+
break;
|
|
776
|
+
}
|
|
777
|
+
default:
|
|
778
|
+
// Other stream events are ignored
|
|
779
|
+
break;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
else if (message.type === 'assistant') {
|
|
783
|
+
// Fallback for non-streaming assistant messages
|
|
784
|
+
const content = this.extractMessageContent(message);
|
|
785
|
+
if (content) {
|
|
786
|
+
assistantBuffer = content;
|
|
787
|
+
emitAssistant(true);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
else if (message.type === 'result') {
|
|
791
|
+
// Final result - check for errors first
|
|
792
|
+
const resultRecord = message;
|
|
793
|
+
// Log full result for debugging
|
|
794
|
+
console.error(`[ClaudeEngine] Result message: ${JSON.stringify(resultRecord, null, 2)}`);
|
|
795
|
+
// Extract and emit usage statistics
|
|
796
|
+
const usage = resultRecord.usage;
|
|
797
|
+
const totalCostUsd = typeof resultRecord.total_cost_usd === 'number' ? resultRecord.total_cost_usd : 0;
|
|
798
|
+
const durationMs = typeof resultRecord.duration_ms === 'number' ? resultRecord.duration_ms : 0;
|
|
799
|
+
const numTurns = typeof resultRecord.num_turns === 'number' ? resultRecord.num_turns : 0;
|
|
800
|
+
if (usage || totalCostUsd > 0) {
|
|
801
|
+
ctx.emit({
|
|
802
|
+
type: 'usage',
|
|
803
|
+
data: {
|
|
804
|
+
sessionId,
|
|
805
|
+
requestId,
|
|
806
|
+
inputTokens: typeof (usage === null || usage === void 0 ? void 0 : usage.input_tokens) === 'number' ? usage.input_tokens : 0,
|
|
807
|
+
outputTokens: typeof (usage === null || usage === void 0 ? void 0 : usage.output_tokens) === 'number' ? usage.output_tokens : 0,
|
|
808
|
+
cacheReadInputTokens: typeof (usage === null || usage === void 0 ? void 0 : usage.cache_read_input_tokens) === 'number'
|
|
809
|
+
? usage.cache_read_input_tokens
|
|
810
|
+
: undefined,
|
|
811
|
+
cacheCreationInputTokens: typeof (usage === null || usage === void 0 ? void 0 : usage.cache_creation_input_tokens) === 'number'
|
|
812
|
+
? usage.cache_creation_input_tokens
|
|
813
|
+
: undefined,
|
|
814
|
+
totalCostUsd,
|
|
815
|
+
durationMs,
|
|
816
|
+
numTurns,
|
|
817
|
+
},
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
// Check if result contains errors (SDK puts error details here)
|
|
821
|
+
// Note: is_error can be true even with empty errors array
|
|
822
|
+
if (resultRecord.is_error) {
|
|
823
|
+
const errors = resultRecord.errors;
|
|
824
|
+
const resultText = resultRecord.result;
|
|
825
|
+
const errorMsg = (errors === null || errors === void 0 ? void 0 : errors.length)
|
|
826
|
+
? errors.join('; ')
|
|
827
|
+
: resultText || 'Unknown error from Claude Code';
|
|
828
|
+
console.error(`[ClaudeEngine] Result error: ${errorMsg}`);
|
|
829
|
+
// Check if this is a resume failure
|
|
830
|
+
const isResumeFailure = errorMsg.includes('No conversation found') ||
|
|
831
|
+
errorMsg.includes('Failed to resume session') ||
|
|
832
|
+
errorMsg.includes('session ID');
|
|
833
|
+
if (isResumeFailure && resumeClaudeSessionId) {
|
|
834
|
+
// Clear the stored session ID so next request starts fresh
|
|
835
|
+
if (ctx.persistClaudeSessionId && projectId) {
|
|
836
|
+
try {
|
|
837
|
+
// Pass empty string to clear the session
|
|
838
|
+
await ctx.persistClaudeSessionId('');
|
|
839
|
+
console.error('[ClaudeEngine] Cleared invalid session ID');
|
|
840
|
+
}
|
|
841
|
+
catch (_b) {
|
|
842
|
+
// Ignore clear errors
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
throw new Error(`Resume failed: ${errorMsg}. Session has been cleared - please retry.`);
|
|
846
|
+
}
|
|
847
|
+
throw new Error(errorMsg);
|
|
848
|
+
}
|
|
849
|
+
// Extract content from successful result
|
|
850
|
+
const resultContent = this.extractMessageContent(message);
|
|
851
|
+
if (resultContent && resultContent !== assistantBuffer.trim()) {
|
|
852
|
+
assistantBuffer = resultContent;
|
|
853
|
+
emitAssistant(true);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
else if (message.type === 'system') {
|
|
857
|
+
// Handle system messages
|
|
858
|
+
const record = message;
|
|
859
|
+
const subtype = this.pickFirstString(record.subtype);
|
|
860
|
+
if (subtype === 'init') {
|
|
861
|
+
// system:init - contains session_id and management information
|
|
862
|
+
const claudeSessionId = record.session_id ? String(record.session_id) : undefined;
|
|
863
|
+
if (claudeSessionId) {
|
|
864
|
+
console.error(`[ClaudeEngine] Session initialized: ${claudeSessionId}`);
|
|
865
|
+
// Persist the session ID if callback is provided and projectId exists
|
|
866
|
+
if (ctx.persistClaudeSessionId && projectId) {
|
|
867
|
+
try {
|
|
868
|
+
await ctx.persistClaudeSessionId(claudeSessionId);
|
|
869
|
+
console.error(`[ClaudeEngine] Session ID persisted for project: ${projectId}`);
|
|
870
|
+
}
|
|
871
|
+
catch (persistError) {
|
|
872
|
+
console.error('[ClaudeEngine] Failed to persist session ID:', persistError);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
// Extract and persist management information
|
|
877
|
+
if (ctx.persistManagementInfo) {
|
|
878
|
+
try {
|
|
879
|
+
const managementInfo = {
|
|
880
|
+
tools: Array.isArray(record.tools)
|
|
881
|
+
? record.tools.filter((t) => typeof t === 'string')
|
|
882
|
+
: undefined,
|
|
883
|
+
agents: Array.isArray(record.agents)
|
|
884
|
+
? record.agents.filter((a) => typeof a === 'string')
|
|
885
|
+
: undefined,
|
|
886
|
+
// SDK returns plugins as { name, path }[] objects
|
|
887
|
+
plugins: Array.isArray(record.plugins)
|
|
888
|
+
? record.plugins
|
|
889
|
+
.filter((p) => p && typeof p.name === 'string')
|
|
890
|
+
.map((p) => ({
|
|
891
|
+
name: String(p.name),
|
|
892
|
+
path: p.path ? String(p.path) : undefined,
|
|
893
|
+
}))
|
|
894
|
+
: undefined,
|
|
895
|
+
skills: Array.isArray(record.skills)
|
|
896
|
+
? record.skills.filter((s) => typeof s === 'string')
|
|
897
|
+
: undefined,
|
|
898
|
+
mcpServers: Array.isArray(record.mcp_servers)
|
|
899
|
+
? record.mcp_servers
|
|
900
|
+
.filter((s) => s && typeof s.name === 'string')
|
|
901
|
+
.map((s) => ({
|
|
902
|
+
name: String(s.name),
|
|
903
|
+
status: String(s.status || 'unknown'),
|
|
904
|
+
}))
|
|
905
|
+
: undefined,
|
|
906
|
+
slashCommands: Array.isArray(record.slash_commands)
|
|
907
|
+
? record.slash_commands.filter((c) => typeof c === 'string')
|
|
908
|
+
: undefined,
|
|
909
|
+
model: this.pickFirstString(record.model),
|
|
910
|
+
permissionMode: this.pickFirstString(record.permissionMode),
|
|
911
|
+
cwd: this.pickFirstString(record.cwd),
|
|
912
|
+
outputStyle: this.pickFirstString(record.output_style),
|
|
913
|
+
betas: Array.isArray(record.betas)
|
|
914
|
+
? record.betas.filter((b) => typeof b === 'string')
|
|
915
|
+
: undefined,
|
|
916
|
+
claudeCodeVersion: this.pickFirstString(record.claude_code_version),
|
|
917
|
+
apiKeySource: this.pickFirstString(record.apiKeySource),
|
|
918
|
+
};
|
|
919
|
+
await ctx.persistManagementInfo(managementInfo);
|
|
920
|
+
console.error('[ClaudeEngine] Management info persisted');
|
|
921
|
+
}
|
|
922
|
+
catch (persistError) {
|
|
923
|
+
console.error('[ClaudeEngine] Failed to persist management info:', persistError);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
else if (subtype === 'status') {
|
|
928
|
+
// system:status - log for debugging (e.g., compacting)
|
|
929
|
+
const statusText = this.pickFirstString(record.status);
|
|
930
|
+
console.error(`[ClaudeEngine] System status: ${statusText || 'unknown'}`);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
else if (message.type === 'auth_status') {
|
|
934
|
+
// Handle authentication status - SDK fields: isAuthenticating, output, error
|
|
935
|
+
const record = message;
|
|
936
|
+
const isAuthenticating = record.isAuthenticating === true;
|
|
937
|
+
const output = Array.isArray(record.output)
|
|
938
|
+
? record.output.filter((o) => typeof o === 'string')
|
|
939
|
+
: [];
|
|
940
|
+
const authError = this.pickFirstString(record.error);
|
|
941
|
+
console.error(`[ClaudeEngine] Auth status: isAuthenticating=${isAuthenticating}, hasError=${!!authError}`);
|
|
942
|
+
// Build content from output or error
|
|
943
|
+
const content = authError || output.join('\n') || 'Authentication in progress...';
|
|
944
|
+
// Determine if login is required:
|
|
945
|
+
// - Not currently authenticating AND (has error OR output contains login keywords)
|
|
946
|
+
const outputText = output.join(' ').toLowerCase();
|
|
947
|
+
const requiresLogin = !isAuthenticating &&
|
|
948
|
+
(!!authError ||
|
|
949
|
+
outputText.includes('login') ||
|
|
950
|
+
outputText.includes('authenticate') ||
|
|
951
|
+
outputText.includes('sign in'));
|
|
952
|
+
// Emit auth status as a system message so UI can display login prompts
|
|
953
|
+
const authSystemMessage = {
|
|
954
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
955
|
+
sessionId,
|
|
956
|
+
role: 'system',
|
|
957
|
+
content,
|
|
958
|
+
messageType: 'status',
|
|
959
|
+
cliSource: this.name,
|
|
960
|
+
requestId,
|
|
961
|
+
isStreaming: false,
|
|
962
|
+
isFinal: !isAuthenticating,
|
|
963
|
+
createdAt: new Date().toISOString(),
|
|
964
|
+
metadata: {
|
|
965
|
+
cli_type: 'claude',
|
|
966
|
+
event_type: 'auth_status',
|
|
967
|
+
isAuthenticating,
|
|
968
|
+
output,
|
|
969
|
+
error: authError,
|
|
970
|
+
requires_login: requiresLogin,
|
|
971
|
+
},
|
|
972
|
+
};
|
|
973
|
+
ctx.emit({ type: 'message', data: authSystemMessage });
|
|
974
|
+
}
|
|
975
|
+
else if (message.type === 'tool_progress') {
|
|
976
|
+
// Handle tool progress - SDK fields: tool_use_id, tool_name, parent_tool_use_id, elapsed_time_seconds
|
|
977
|
+
const record = message;
|
|
978
|
+
const toolUseId = this.pickFirstString(record.tool_use_id);
|
|
979
|
+
const toolName = this.pickFirstString(record.tool_name);
|
|
980
|
+
const parentToolUseId = this.pickFirstString(record.parent_tool_use_id);
|
|
981
|
+
const elapsedTimeSeconds = typeof record.elapsed_time_seconds === 'number'
|
|
982
|
+
? record.elapsed_time_seconds
|
|
983
|
+
: undefined;
|
|
984
|
+
if (toolName || toolUseId) {
|
|
985
|
+
const displayName = toolName || toolUseId || 'tool';
|
|
986
|
+
const elapsedStr = elapsedTimeSeconds !== undefined ? ` (${elapsedTimeSeconds.toFixed(1)}s)` : '';
|
|
987
|
+
console.error(`[ClaudeEngine] Tool progress: ${displayName}${elapsedStr}`);
|
|
988
|
+
// Use tool_use_id as message id if available, so UI can update the same progress entry
|
|
989
|
+
const messageId = toolUseId ? `progress-${toolUseId}` : (0, node_crypto_1.randomUUID)();
|
|
990
|
+
// Emit tool progress as a tool message
|
|
991
|
+
const progressMessage = {
|
|
992
|
+
id: messageId,
|
|
993
|
+
sessionId,
|
|
994
|
+
role: 'tool',
|
|
995
|
+
content: `${displayName} in progress${elapsedStr}`,
|
|
996
|
+
messageType: 'tool_use',
|
|
997
|
+
cliSource: this.name,
|
|
998
|
+
requestId,
|
|
999
|
+
isStreaming: true,
|
|
1000
|
+
isFinal: false,
|
|
1001
|
+
createdAt: new Date().toISOString(),
|
|
1002
|
+
metadata: {
|
|
1003
|
+
cli_type: 'claude',
|
|
1004
|
+
event_type: 'tool_progress',
|
|
1005
|
+
toolUseId,
|
|
1006
|
+
toolName,
|
|
1007
|
+
parentToolUseId,
|
|
1008
|
+
elapsedTimeSeconds,
|
|
1009
|
+
},
|
|
1010
|
+
};
|
|
1011
|
+
ctx.emit({ type: 'message', data: progressMessage });
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
// Ensure final message is emitted
|
|
1016
|
+
if (assistantBuffer.trim()) {
|
|
1017
|
+
emitAssistant(true);
|
|
1018
|
+
}
|
|
1019
|
+
console.error('[ClaudeEngine] Query completed successfully');
|
|
1020
|
+
}
|
|
1021
|
+
catch (error) {
|
|
1022
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1023
|
+
// Log full stderr for debugging
|
|
1024
|
+
console.error(`[ClaudeEngine] Error: ${message}`);
|
|
1025
|
+
if (stderrBuffer.length > 0) {
|
|
1026
|
+
console.error(`[ClaudeEngine] Stderr (${stderrBuffer.length} lines):`);
|
|
1027
|
+
stderrBuffer.slice(-10).forEach((line) => console.error(` ${line}`));
|
|
1028
|
+
}
|
|
1029
|
+
// Check if this is a resume failure from stderr
|
|
1030
|
+
const stderrText = stderrBuffer.join('\n');
|
|
1031
|
+
const isResumeFailure = stderrText.includes('No conversation found') ||
|
|
1032
|
+
stderrText.includes('Failed to resume session') ||
|
|
1033
|
+
stderrText.includes('session ID') ||
|
|
1034
|
+
message.includes('Resume failed');
|
|
1035
|
+
if (isResumeFailure && resumeClaudeSessionId && ctx.persistClaudeSessionId && projectId) {
|
|
1036
|
+
// Clear the stored session ID so next request starts fresh
|
|
1037
|
+
try {
|
|
1038
|
+
await ctx.persistClaudeSessionId('');
|
|
1039
|
+
console.error('[ClaudeEngine] Cleared invalid session ID due to resume failure');
|
|
1040
|
+
}
|
|
1041
|
+
catch (_c) {
|
|
1042
|
+
// Ignore clear errors
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
// Enhance error message for CCR-related errors
|
|
1046
|
+
const enhancedMessage = await this.enhanceCcrErrorMessage(message, stderrText);
|
|
1047
|
+
// Classify errors for better UX
|
|
1048
|
+
const errorMessage = this.classifyError(enhancedMessage, stderrBuffer);
|
|
1049
|
+
throw new Error(`ClaudeEngine: ${errorMessage}`);
|
|
1050
|
+
}
|
|
1051
|
+
finally {
|
|
1052
|
+
// Always cleanup temp files, even on error
|
|
1053
|
+
await cleanupTempFiles();
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Build environment variables for Claude Code.
|
|
1058
|
+
* Supports Claude Code Router (CCR) when useCcr is true:
|
|
1059
|
+
* 1. Auto-detecting CCR from config file (~/.claude-code-router/config.json)
|
|
1060
|
+
* 2. Passing through env vars if already set (via `eval "$(ccr activate)"`)
|
|
1061
|
+
*
|
|
1062
|
+
* SDK treats options.env as a complete replacement (not merged with process.env),
|
|
1063
|
+
* so we must explicitly include all necessary variables.
|
|
1064
|
+
*
|
|
1065
|
+
* @param useCcr - Whether CCR is enabled for this project. When false/undefined, CCR detection is skipped.
|
|
1066
|
+
*/
|
|
1067
|
+
async buildClaudeEnv(useCcr) {
|
|
1068
|
+
const env = { ...process.env };
|
|
1069
|
+
// Ensure Node.js bin directory is in PATH (for child processes)
|
|
1070
|
+
const nodeBinDir = node_path_1.default.dirname(process.execPath);
|
|
1071
|
+
const currentPath = env.PATH || env.Path || '';
|
|
1072
|
+
if (!currentPath.includes(nodeBinDir)) {
|
|
1073
|
+
env.PATH = [nodeBinDir, currentPath].filter(Boolean).join(node_path_1.default.delimiter);
|
|
1074
|
+
}
|
|
1075
|
+
// Only detect CCR if explicitly enabled for this project
|
|
1076
|
+
if (useCcr && !env.ANTHROPIC_BASE_URL) {
|
|
1077
|
+
try {
|
|
1078
|
+
const ccrResult = await (0, ccr_detector_1.detectCcr)();
|
|
1079
|
+
if (ccrResult.detected && ccrResult.baseUrl && ccrResult.authToken) {
|
|
1080
|
+
env.ANTHROPIC_BASE_URL = ccrResult.baseUrl;
|
|
1081
|
+
env.ANTHROPIC_AUTH_TOKEN = ccrResult.authToken;
|
|
1082
|
+
console.error(`[ClaudeEngine] CCR auto-detected (source: ${ccrResult.source})`);
|
|
1083
|
+
}
|
|
1084
|
+
else if (ccrResult.error) {
|
|
1085
|
+
console.error(`[ClaudeEngine] CCR detection failed: ${ccrResult.error}`);
|
|
1086
|
+
}
|
|
1087
|
+
else {
|
|
1088
|
+
console.error('[ClaudeEngine] CCR enabled but not detected (config not found or service not running)');
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
catch (err) {
|
|
1092
|
+
// CCR detection is best-effort, don't fail the request
|
|
1093
|
+
console.error(`[ClaudeEngine] CCR detection error: ${err}`);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
// Log CCR-related env vars for debugging (without exposing full token)
|
|
1097
|
+
const baseUrl = env.ANTHROPIC_BASE_URL;
|
|
1098
|
+
const authToken = env.ANTHROPIC_AUTH_TOKEN;
|
|
1099
|
+
if (baseUrl) {
|
|
1100
|
+
console.error(`[ClaudeEngine] Using ANTHROPIC_BASE_URL: ${baseUrl}`);
|
|
1101
|
+
}
|
|
1102
|
+
if (authToken) {
|
|
1103
|
+
const preview = authToken.length > 8 ? `${authToken.slice(0, 4)}...${authToken.slice(-4)}` : '****';
|
|
1104
|
+
console.error(`[ClaudeEngine] Using ANTHROPIC_AUTH_TOKEN: ${preview}`);
|
|
1105
|
+
}
|
|
1106
|
+
return env;
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Resolve project root path.
|
|
1110
|
+
*/
|
|
1111
|
+
resolveRepoPath(projectRoot) {
|
|
1112
|
+
const base = (projectRoot && projectRoot.trim()) || process.env.MCP_AGENT_PROJECT_ROOT || process.cwd();
|
|
1113
|
+
return node_path_1.default.resolve(base);
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Pick first string value from unknown input.
|
|
1117
|
+
*/
|
|
1118
|
+
pickFirstString(value) {
|
|
1119
|
+
if (typeof value === 'string') {
|
|
1120
|
+
const trimmed = value.trim();
|
|
1121
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
1122
|
+
}
|
|
1123
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
1124
|
+
return String(value);
|
|
1125
|
+
}
|
|
1126
|
+
if (Array.isArray(value)) {
|
|
1127
|
+
for (const entry of value) {
|
|
1128
|
+
const candidate = this.pickFirstString(entry);
|
|
1129
|
+
if (candidate)
|
|
1130
|
+
return candidate;
|
|
1131
|
+
}
|
|
1132
|
+
return undefined;
|
|
1133
|
+
}
|
|
1134
|
+
return undefined;
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Extract content from SDK message.
|
|
1138
|
+
* Handles various message structures from Claude Agent SDK:
|
|
1139
|
+
* - result.result (final result text)
|
|
1140
|
+
* - assistant.message (nested message content)
|
|
1141
|
+
* - content/text (direct content fields)
|
|
1142
|
+
* - content[] (array of content blocks)
|
|
1143
|
+
*
|
|
1144
|
+
* @param message - The message object to extract content from
|
|
1145
|
+
* @param depth - Current recursion depth (max 3 to prevent infinite loops)
|
|
1146
|
+
*/
|
|
1147
|
+
extractMessageContent(message, depth = 0) {
|
|
1148
|
+
// Prevent infinite recursion
|
|
1149
|
+
if (depth > 3 || !message || typeof message !== 'object')
|
|
1150
|
+
return undefined;
|
|
1151
|
+
const record = message;
|
|
1152
|
+
// Handle result message: result field contains final text
|
|
1153
|
+
if (typeof record.result === 'string') {
|
|
1154
|
+
return record.result.trim();
|
|
1155
|
+
}
|
|
1156
|
+
// Handle assistant message: message field may contain nested content
|
|
1157
|
+
if (record.message && typeof record.message === 'object') {
|
|
1158
|
+
const nested = this.extractMessageContent(record.message, depth + 1);
|
|
1159
|
+
if (nested)
|
|
1160
|
+
return nested;
|
|
1161
|
+
}
|
|
1162
|
+
// Try common content fields
|
|
1163
|
+
if (typeof record.content === 'string') {
|
|
1164
|
+
return record.content.trim();
|
|
1165
|
+
}
|
|
1166
|
+
if (typeof record.text === 'string') {
|
|
1167
|
+
return record.text.trim();
|
|
1168
|
+
}
|
|
1169
|
+
if (Array.isArray(record.content)) {
|
|
1170
|
+
const textParts = [];
|
|
1171
|
+
for (const part of record.content) {
|
|
1172
|
+
if (part && typeof part === 'object' && part.type === 'text') {
|
|
1173
|
+
const text = part.text;
|
|
1174
|
+
if (typeof text === 'string') {
|
|
1175
|
+
textParts.push(text);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
if (textParts.length > 0) {
|
|
1180
|
+
return textParts.join('').trim();
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
return undefined;
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Format error message for user display.
|
|
1187
|
+
* Preserves the original error message and only appends stderr context if useful.
|
|
1188
|
+
*/
|
|
1189
|
+
classifyError(message, stderrBuffer) {
|
|
1190
|
+
// Always preserve the original error message
|
|
1191
|
+
// Only append stderr context if it contains useful information beyond the spawn line
|
|
1192
|
+
const usefulStderr = stderrBuffer.filter((line) => !line.includes('Spawning Claude Code:') && line.trim().length > 0);
|
|
1193
|
+
if (usefulStderr.length > 0) {
|
|
1194
|
+
const lastLines = usefulStderr.slice(-3).join(' | ');
|
|
1195
|
+
return `${message} (stderr: ${lastLines})`;
|
|
1196
|
+
}
|
|
1197
|
+
return message;
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Validate CCR configuration and emit a warning message if issues are found.
|
|
1201
|
+
* This is a best-effort check to provide actionable guidance before CCR crashes.
|
|
1202
|
+
*/
|
|
1203
|
+
async validateAndWarnCcrConfig(sessionId, requestId, ctx) {
|
|
1204
|
+
var _a, _b;
|
|
1205
|
+
try {
|
|
1206
|
+
const validation = await (0, ccr_detector_1.validateCcrConfig)();
|
|
1207
|
+
if (!validation.checked || validation.valid) {
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
// Build user-friendly warning message
|
|
1211
|
+
const lines = [
|
|
1212
|
+
'⚠️ Claude Code Router (CCR) configuration issue detected:',
|
|
1213
|
+
(_a = validation.issue) !== null && _a !== void 0 ? _a : 'CCR configuration appears invalid.',
|
|
1214
|
+
'',
|
|
1215
|
+
(_b = validation.suggestion) !== null && _b !== void 0 ? _b : 'Please check your CCR configuration.',
|
|
1216
|
+
];
|
|
1217
|
+
if (validation.suggestedFix) {
|
|
1218
|
+
lines.push('', `Suggested fix: Router.default = "${validation.suggestedFix}"`);
|
|
1219
|
+
}
|
|
1220
|
+
const content = lines.join('\n');
|
|
1221
|
+
console.error(`[ClaudeEngine] CCR config warning: ${validation.issue}`);
|
|
1222
|
+
const warningMessage = {
|
|
1223
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
1224
|
+
sessionId,
|
|
1225
|
+
role: 'system',
|
|
1226
|
+
content,
|
|
1227
|
+
messageType: 'status',
|
|
1228
|
+
cliSource: this.name,
|
|
1229
|
+
requestId,
|
|
1230
|
+
isStreaming: false,
|
|
1231
|
+
isFinal: true,
|
|
1232
|
+
createdAt: new Date().toISOString(),
|
|
1233
|
+
metadata: {
|
|
1234
|
+
cli_type: 'claude',
|
|
1235
|
+
warning_type: 'ccr_config',
|
|
1236
|
+
ccr_issue: validation.issue,
|
|
1237
|
+
ccr_suggested_fix: validation.suggestedFix,
|
|
1238
|
+
},
|
|
1239
|
+
};
|
|
1240
|
+
ctx.emit({ type: 'message', data: warningMessage });
|
|
1241
|
+
}
|
|
1242
|
+
catch (err) {
|
|
1243
|
+
// CCR config validation is best-effort, don't fail the request
|
|
1244
|
+
console.error('[ClaudeEngine] CCR config validation error:', err);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* Enhance error messages for CCR-related errors.
|
|
1249
|
+
* Detects the common "includes of undefined" crash and provides actionable guidance.
|
|
1250
|
+
*/
|
|
1251
|
+
async enhanceCcrErrorMessage(message, stderrText) {
|
|
1252
|
+
const combinedText = `${message}\n${stderrText}`;
|
|
1253
|
+
// Detect CCR's "includes of undefined" error pattern
|
|
1254
|
+
const isCcrIncludesError = combinedText.includes('claude-code-router') &&
|
|
1255
|
+
(combinedText.includes("reading 'includes'") || combinedText.includes('transformRequestIn'));
|
|
1256
|
+
if (!isCcrIncludesError) {
|
|
1257
|
+
return message;
|
|
1258
|
+
}
|
|
1259
|
+
// Try to get specific fix suggestion from CCR config
|
|
1260
|
+
let suggestion = 'Edit ~/.claude-code-router/config.json and set Router.default to "provider,model" format (e.g., "venus,claude-4-5-sonnet-20250929"), then restart CCR.';
|
|
1261
|
+
try {
|
|
1262
|
+
const validation = await (0, ccr_detector_1.validateCcrConfig)();
|
|
1263
|
+
if (validation.checked && !validation.valid && validation.suggestion) {
|
|
1264
|
+
suggestion = validation.suggestion;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
catch (_a) {
|
|
1268
|
+
// Use default suggestion if validation fails
|
|
1269
|
+
}
|
|
1270
|
+
return [
|
|
1271
|
+
message,
|
|
1272
|
+
'',
|
|
1273
|
+
'💡 CCR Configuration Issue Detected:',
|
|
1274
|
+
'This error is commonly caused by Router.default being set to only a provider name',
|
|
1275
|
+
'(e.g., "venus") instead of the required "provider,model" format.',
|
|
1276
|
+
'',
|
|
1277
|
+
`Fix: ${suggestion}`,
|
|
1278
|
+
].join('\n');
|
|
1279
|
+
}
|
|
1280
|
+
/**
|
|
1281
|
+
* Build metadata for tool result events.
|
|
1282
|
+
*/
|
|
1283
|
+
buildToolResultMetadata(block) {
|
|
1284
|
+
const toolUseId = this.pickFirstString(block.tool_use_id);
|
|
1285
|
+
const isError = block.is_error === true;
|
|
1286
|
+
return {
|
|
1287
|
+
toolUseId,
|
|
1288
|
+
is_error: isError,
|
|
1289
|
+
status: isError ? 'failed' : 'completed',
|
|
1290
|
+
cli_type: 'claude',
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* Extract content from a tool_result block.
|
|
1295
|
+
*/
|
|
1296
|
+
extractToolResultContent(block) {
|
|
1297
|
+
const content = block.content;
|
|
1298
|
+
if (typeof content === 'string')
|
|
1299
|
+
return content;
|
|
1300
|
+
if (Array.isArray(content)) {
|
|
1301
|
+
const textParts = content
|
|
1302
|
+
.filter((c) => c && typeof c === 'object' && c.type === 'text')
|
|
1303
|
+
.map((c) => c.text)
|
|
1304
|
+
.filter(Boolean);
|
|
1305
|
+
if (textParts.length > 0) {
|
|
1306
|
+
return textParts.join('\n');
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
return undefined;
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Encode string to base64 for hashing.
|
|
1313
|
+
*/
|
|
1314
|
+
encodeHash(value) {
|
|
1315
|
+
return Buffer.from(value, 'utf-8').toString('base64');
|
|
1316
|
+
}
|
|
1317
|
+
/**
|
|
1318
|
+
* Write an attachment to a temporary file and return its path.
|
|
1319
|
+
*/
|
|
1320
|
+
async writeAttachmentToTemp(attachment) {
|
|
1321
|
+
const os = await import('node:os');
|
|
1322
|
+
const fs = await import('node:fs/promises');
|
|
1323
|
+
const tempDir = os.tmpdir();
|
|
1324
|
+
const ext = attachment.mimeType.split('/')[1] || 'bin';
|
|
1325
|
+
const sanitizedName = attachment.name.replace(/[^a-zA-Z0-9.-]/g, '_');
|
|
1326
|
+
const fileName = `mcp-agent-${Date.now()}-${sanitizedName}.${ext}`;
|
|
1327
|
+
const filePath = node_path_1.default.join(tempDir, fileName);
|
|
1328
|
+
const buffer = Buffer.from(attachment.dataBase64, 'base64');
|
|
1329
|
+
await fs.writeFile(filePath, buffer);
|
|
1330
|
+
return filePath;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
exports.ClaudeEngine = ClaudeEngine;
|
|
1334
|
+
/**
|
|
1335
|
+
* Maximum number of stderr lines to keep in memory.
|
|
1336
|
+
*/
|
|
1337
|
+
ClaudeEngine.MAX_STDERR_LINES = 200;
|
|
1338
|
+
//# sourceMappingURL=claude.js.map
|