@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.
Files changed (129) hide show
  1. package/README.md +183 -0
  2. package/dist/README.md +25 -0
  3. package/dist/agent/attachment-service.d.ts +83 -0
  4. package/dist/agent/attachment-service.js +370 -0
  5. package/dist/agent/attachment-service.js.map +1 -0
  6. package/dist/agent/ccr-detector.d.ts +59 -0
  7. package/dist/agent/ccr-detector.js +311 -0
  8. package/dist/agent/ccr-detector.js.map +1 -0
  9. package/dist/agent/chat-service.d.ts +50 -0
  10. package/dist/agent/chat-service.js +439 -0
  11. package/dist/agent/chat-service.js.map +1 -0
  12. package/dist/agent/db/client.d.ts +26 -0
  13. package/dist/agent/db/client.js +244 -0
  14. package/dist/agent/db/client.js.map +1 -0
  15. package/dist/agent/db/index.d.ts +5 -0
  16. package/dist/agent/db/index.js +22 -0
  17. package/dist/agent/db/index.js.map +1 -0
  18. package/dist/agent/db/schema.d.ts +711 -0
  19. package/dist/agent/db/schema.js +121 -0
  20. package/dist/agent/db/schema.js.map +1 -0
  21. package/dist/agent/directory-picker.d.ts +11 -0
  22. package/dist/agent/directory-picker.js +149 -0
  23. package/dist/agent/directory-picker.js.map +1 -0
  24. package/dist/agent/engines/claude.d.ts +79 -0
  25. package/dist/agent/engines/claude.js +1338 -0
  26. package/dist/agent/engines/claude.js.map +1 -0
  27. package/dist/agent/engines/codex.d.ts +48 -0
  28. package/dist/agent/engines/codex.js +822 -0
  29. package/dist/agent/engines/codex.js.map +1 -0
  30. package/dist/agent/engines/types.d.ts +133 -0
  31. package/dist/agent/engines/types.js +3 -0
  32. package/dist/agent/engines/types.js.map +1 -0
  33. package/dist/agent/message-service.d.ts +56 -0
  34. package/dist/agent/message-service.js +198 -0
  35. package/dist/agent/message-service.js.map +1 -0
  36. package/dist/agent/open-project.d.ts +25 -0
  37. package/dist/agent/open-project.js +469 -0
  38. package/dist/agent/open-project.js.map +1 -0
  39. package/dist/agent/project-service.d.ts +49 -0
  40. package/dist/agent/project-service.js +254 -0
  41. package/dist/agent/project-service.js.map +1 -0
  42. package/dist/agent/project-types.d.ts +27 -0
  43. package/dist/agent/project-types.js +3 -0
  44. package/dist/agent/project-types.js.map +1 -0
  45. package/dist/agent/session-service.d.ts +198 -0
  46. package/dist/agent/session-service.js +292 -0
  47. package/dist/agent/session-service.js.map +1 -0
  48. package/dist/agent/storage.d.ts +27 -0
  49. package/dist/agent/storage.js +73 -0
  50. package/dist/agent/storage.js.map +1 -0
  51. package/dist/agent/stream-manager.d.ts +42 -0
  52. package/dist/agent/stream-manager.js +243 -0
  53. package/dist/agent/stream-manager.js.map +1 -0
  54. package/dist/agent/tool-bridge.d.ts +44 -0
  55. package/dist/agent/tool-bridge.js +50 -0
  56. package/dist/agent/tool-bridge.js.map +1 -0
  57. package/dist/agent/types.d.ts +6 -0
  58. package/dist/agent/types.js +3 -0
  59. package/dist/agent/types.js.map +1 -0
  60. package/dist/cli.d.ts +2 -0
  61. package/dist/cli.js +224 -0
  62. package/dist/cli.js.map +1 -0
  63. package/dist/constant/index.d.ts +60 -0
  64. package/dist/constant/index.js +80 -0
  65. package/dist/constant/index.js.map +1 -0
  66. package/dist/file-handler.d.ts +41 -0
  67. package/dist/file-handler.js +295 -0
  68. package/dist/file-handler.js.map +1 -0
  69. package/dist/index.d.ts +2 -0
  70. package/dist/index.js +35 -0
  71. package/dist/index.js.map +1 -0
  72. package/dist/mcp/mcp-server-stdio.d.ts +72 -0
  73. package/dist/mcp/mcp-server-stdio.js +143 -0
  74. package/dist/mcp/mcp-server-stdio.js.map +1 -0
  75. package/dist/mcp/mcp-server.d.ts +36 -0
  76. package/dist/mcp/mcp-server.js +26 -0
  77. package/dist/mcp/mcp-server.js.map +1 -0
  78. package/dist/mcp/register-tools.d.ts +2 -0
  79. package/dist/mcp/register-tools.js +148 -0
  80. package/dist/mcp/register-tools.js.map +1 -0
  81. package/dist/mcp/stdio-config.json +3 -0
  82. package/dist/native-messaging-host.d.ts +42 -0
  83. package/dist/native-messaging-host.js +312 -0
  84. package/dist/native-messaging-host.js.map +1 -0
  85. package/dist/run_host.bat +194 -0
  86. package/dist/run_host.sh +264 -0
  87. package/dist/scripts/browser-config.d.ts +28 -0
  88. package/dist/scripts/browser-config.js +229 -0
  89. package/dist/scripts/browser-config.js.map +1 -0
  90. package/dist/scripts/build.d.ts +1 -0
  91. package/dist/scripts/build.js +126 -0
  92. package/dist/scripts/build.js.map +1 -0
  93. package/dist/scripts/constant.d.ts +4 -0
  94. package/dist/scripts/constant.js +8 -0
  95. package/dist/scripts/constant.js.map +1 -0
  96. package/dist/scripts/doctor.d.ts +70 -0
  97. package/dist/scripts/doctor.js +930 -0
  98. package/dist/scripts/doctor.js.map +1 -0
  99. package/dist/scripts/postinstall.d.ts +2 -0
  100. package/dist/scripts/postinstall.js +246 -0
  101. package/dist/scripts/postinstall.js.map +1 -0
  102. package/dist/scripts/register-dev.d.ts +1 -0
  103. package/dist/scripts/register-dev.js +5 -0
  104. package/dist/scripts/register-dev.js.map +1 -0
  105. package/dist/scripts/register.d.ts +2 -0
  106. package/dist/scripts/register.js +28 -0
  107. package/dist/scripts/register.js.map +1 -0
  108. package/dist/scripts/report.d.ts +96 -0
  109. package/dist/scripts/report.js +686 -0
  110. package/dist/scripts/report.js.map +1 -0
  111. package/dist/scripts/utils.d.ts +64 -0
  112. package/dist/scripts/utils.js +443 -0
  113. package/dist/scripts/utils.js.map +1 -0
  114. package/dist/server/index.d.ts +35 -0
  115. package/dist/server/index.js +312 -0
  116. package/dist/server/index.js.map +1 -0
  117. package/dist/server/routes/agent.d.ts +21 -0
  118. package/dist/server/routes/agent.js +971 -0
  119. package/dist/server/routes/agent.js.map +1 -0
  120. package/dist/server/routes/index.d.ts +4 -0
  121. package/dist/server/routes/index.js +9 -0
  122. package/dist/server/routes/index.js.map +1 -0
  123. package/dist/trace-analyzer.d.ts +14 -0
  124. package/dist/trace-analyzer.js +113 -0
  125. package/dist/trace-analyzer.js.map +1 -0
  126. package/dist/util/logger.d.ts +1 -0
  127. package/dist/util/logger.js +43 -0
  128. package/dist/util/logger.js.map +1 -0
  129. 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