@cloudcli-ai/cloudcli 1.29.1 → 1.29.3

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 (113) hide show
  1. package/dist/assets/{index-ByHZ8-LL.css → index-D-9h5kNf.css} +1 -1
  2. package/dist/assets/{index-BOWaxSnV.js → index-y8yqvoLv.js} +229 -229
  3. package/dist/index.html +2 -2
  4. package/dist-server/server/claude-sdk.js +694 -0
  5. package/dist-server/server/claude-sdk.js.map +1 -0
  6. package/dist-server/server/cli.js +642 -0
  7. package/dist-server/server/cli.js.map +1 -0
  8. package/dist-server/server/constants/config.js +6 -0
  9. package/dist-server/server/constants/config.js.map +1 -0
  10. package/dist-server/server/cursor-cli.js +271 -0
  11. package/dist-server/server/cursor-cli.js.map +1 -0
  12. package/dist-server/server/database/db.js +535 -0
  13. package/dist-server/server/database/db.js.map +1 -0
  14. package/dist-server/server/database/schema.js +97 -0
  15. package/dist-server/server/database/schema.js.map +1 -0
  16. package/dist-server/server/gemini-cli.js +394 -0
  17. package/dist-server/server/gemini-cli.js.map +1 -0
  18. package/dist-server/server/gemini-response-handler.js +72 -0
  19. package/dist-server/server/gemini-response-handler.js.map +1 -0
  20. package/dist-server/server/index.js +2244 -0
  21. package/dist-server/server/index.js.map +1 -0
  22. package/dist-server/server/load-env.js +32 -0
  23. package/dist-server/server/load-env.js.map +1 -0
  24. package/dist-server/server/middleware/auth.js +111 -0
  25. package/dist-server/server/middleware/auth.js.map +1 -0
  26. package/dist-server/server/openai-codex.js +367 -0
  27. package/dist-server/server/openai-codex.js.map +1 -0
  28. package/dist-server/server/projects.js +2331 -0
  29. package/dist-server/server/projects.js.map +1 -0
  30. package/dist-server/server/providers/claude/adapter.js +266 -0
  31. package/dist-server/server/providers/claude/adapter.js.map +1 -0
  32. package/dist-server/server/providers/codex/adapter.js +232 -0
  33. package/dist-server/server/providers/codex/adapter.js.map +1 -0
  34. package/dist-server/server/providers/cursor/adapter.js +347 -0
  35. package/dist-server/server/providers/cursor/adapter.js.map +1 -0
  36. package/dist-server/server/providers/gemini/adapter.js +174 -0
  37. package/dist-server/server/providers/gemini/adapter.js.map +1 -0
  38. package/dist-server/server/providers/registry.js +39 -0
  39. package/dist-server/server/providers/registry.js.map +1 -0
  40. package/dist-server/server/providers/types.js +106 -0
  41. package/dist-server/server/providers/types.js.map +1 -0
  42. package/dist-server/server/providers/utils.js +28 -0
  43. package/dist-server/server/providers/utils.js.map +1 -0
  44. package/dist-server/server/routes/agent.js +1147 -0
  45. package/dist-server/server/routes/agent.js.map +1 -0
  46. package/dist-server/server/routes/auth.js +117 -0
  47. package/dist-server/server/routes/auth.js.map +1 -0
  48. package/dist-server/server/routes/cli-auth.js +406 -0
  49. package/dist-server/server/routes/cli-auth.js.map +1 -0
  50. package/dist-server/server/routes/codex.js +294 -0
  51. package/dist-server/server/routes/codex.js.map +1 -0
  52. package/dist-server/server/routes/commands.js +531 -0
  53. package/dist-server/server/routes/commands.js.map +1 -0
  54. package/dist-server/server/routes/cursor.js +545 -0
  55. package/dist-server/server/routes/cursor.js.map +1 -0
  56. package/dist-server/server/routes/gemini.js +21 -0
  57. package/dist-server/server/routes/gemini.js.map +1 -0
  58. package/dist-server/server/routes/git.js +1259 -0
  59. package/dist-server/server/routes/git.js.map +1 -0
  60. package/dist-server/server/routes/mcp-utils.js +46 -0
  61. package/dist-server/server/routes/mcp-utils.js.map +1 -0
  62. package/dist-server/server/routes/mcp.js +480 -0
  63. package/dist-server/server/routes/mcp.js.map +1 -0
  64. package/dist-server/server/routes/messages.js +56 -0
  65. package/dist-server/server/routes/messages.js.map +1 -0
  66. package/dist-server/server/routes/plugins.js +266 -0
  67. package/dist-server/server/routes/plugins.js.map +1 -0
  68. package/dist-server/server/routes/projects.js +505 -0
  69. package/dist-server/server/routes/projects.js.map +1 -0
  70. package/dist-server/server/routes/settings.js +249 -0
  71. package/dist-server/server/routes/settings.js.map +1 -0
  72. package/dist-server/server/routes/taskmaster.js +1832 -0
  73. package/dist-server/server/routes/taskmaster.js.map +1 -0
  74. package/dist-server/server/routes/user.js +115 -0
  75. package/dist-server/server/routes/user.js.map +1 -0
  76. package/dist-server/server/services/notification-orchestrator.js +177 -0
  77. package/dist-server/server/services/notification-orchestrator.js.map +1 -0
  78. package/dist-server/server/services/vapid-keys.js +26 -0
  79. package/dist-server/server/services/vapid-keys.js.map +1 -0
  80. package/dist-server/server/sessionManager.js +194 -0
  81. package/dist-server/server/sessionManager.js.map +1 -0
  82. package/dist-server/server/utils/commandParser.js +255 -0
  83. package/dist-server/server/utils/commandParser.js.map +1 -0
  84. package/dist-server/server/utils/frontmatter.js +16 -0
  85. package/dist-server/server/utils/frontmatter.js.map +1 -0
  86. package/dist-server/server/utils/gitConfig.js +36 -0
  87. package/dist-server/server/utils/gitConfig.js.map +1 -0
  88. package/dist-server/server/utils/mcp-detector.js +183 -0
  89. package/dist-server/server/utils/mcp-detector.js.map +1 -0
  90. package/dist-server/server/utils/plugin-loader.js +413 -0
  91. package/dist-server/server/utils/plugin-loader.js.map +1 -0
  92. package/dist-server/server/utils/plugin-process-manager.js +163 -0
  93. package/dist-server/server/utils/plugin-process-manager.js.map +1 -0
  94. package/dist-server/server/utils/runtime-paths.js +30 -0
  95. package/dist-server/server/utils/runtime-paths.js.map +1 -0
  96. package/dist-server/server/utils/taskmaster-websocket.js +118 -0
  97. package/dist-server/server/utils/taskmaster-websocket.js.map +1 -0
  98. package/dist-server/shared/modelConstants.js +87 -0
  99. package/dist-server/shared/modelConstants.js.map +1 -0
  100. package/dist-server/shared/networkHosts.js +20 -0
  101. package/dist-server/shared/networkHosts.js.map +1 -0
  102. package/package.json +21 -10
  103. package/server/cli.js +35 -33
  104. package/server/database/db.js +24 -61
  105. package/server/database/schema.js +102 -0
  106. package/server/index.js +12 -12
  107. package/server/load-env.js +11 -6
  108. package/server/routes/commands.js +6 -4
  109. package/server/routes/cursor.js +0 -218
  110. package/server/tsconfig.json +33 -0
  111. package/server/utils/runtime-paths.js +37 -0
  112. package/shared/modelConstants.js +4 -2
  113. package/server/database/init.sql +0 -99
@@ -0,0 +1,694 @@
1
+ /**
2
+ * Claude SDK Integration
3
+ *
4
+ * This module provides SDK-based integration with Claude using the @anthropic-ai/claude-agent-sdk.
5
+ * It mirrors the interface of claude-cli.js but uses the SDK internally for better performance
6
+ * and maintainability.
7
+ *
8
+ * Key features:
9
+ * - Direct SDK integration without child processes
10
+ * - Session management with abort capability
11
+ * - Options mapping between CLI and SDK formats
12
+ * - WebSocket message streaming
13
+ */
14
+ import { query } from '@anthropic-ai/claude-agent-sdk';
15
+ import crypto from 'crypto';
16
+ import { promises as fs } from 'fs';
17
+ import path from 'path';
18
+ import os from 'os';
19
+ import { CLAUDE_MODELS } from '../shared/modelConstants.js';
20
+ import { createNotificationEvent, notifyRunFailed, notifyRunStopped, notifyUserIfEnabled } from './services/notification-orchestrator.js';
21
+ import { claudeAdapter } from './providers/claude/adapter.js';
22
+ import { createNormalizedMessage } from './providers/types.js';
23
+ const activeSessions = new Map();
24
+ const pendingToolApprovals = new Map();
25
+ const TOOL_APPROVAL_TIMEOUT_MS = parseInt(process.env.CLAUDE_TOOL_APPROVAL_TIMEOUT_MS, 10) || 55000;
26
+ const TOOLS_REQUIRING_INTERACTION = new Set(['AskUserQuestion']);
27
+ function createRequestId() {
28
+ if (typeof crypto.randomUUID === 'function') {
29
+ return crypto.randomUUID();
30
+ }
31
+ return crypto.randomBytes(16).toString('hex');
32
+ }
33
+ function waitForToolApproval(requestId, options = {}) {
34
+ const { timeoutMs = TOOL_APPROVAL_TIMEOUT_MS, signal, onCancel, metadata } = options;
35
+ return new Promise(resolve => {
36
+ let settled = false;
37
+ const finalize = (decision) => {
38
+ if (settled)
39
+ return;
40
+ settled = true;
41
+ cleanup();
42
+ resolve(decision);
43
+ };
44
+ let timeout;
45
+ const cleanup = () => {
46
+ pendingToolApprovals.delete(requestId);
47
+ if (timeout)
48
+ clearTimeout(timeout);
49
+ if (signal && abortHandler) {
50
+ signal.removeEventListener('abort', abortHandler);
51
+ }
52
+ };
53
+ // timeoutMs 0 = wait indefinitely (interactive tools)
54
+ if (timeoutMs > 0) {
55
+ timeout = setTimeout(() => {
56
+ onCancel?.('timeout');
57
+ finalize(null);
58
+ }, timeoutMs);
59
+ }
60
+ const abortHandler = () => {
61
+ onCancel?.('cancelled');
62
+ finalize({ cancelled: true });
63
+ };
64
+ if (signal) {
65
+ if (signal.aborted) {
66
+ onCancel?.('cancelled');
67
+ finalize({ cancelled: true });
68
+ return;
69
+ }
70
+ signal.addEventListener('abort', abortHandler, { once: true });
71
+ }
72
+ const resolver = (decision) => {
73
+ finalize(decision);
74
+ };
75
+ // Attach metadata for getPendingApprovalsForSession lookup
76
+ if (metadata) {
77
+ Object.assign(resolver, metadata);
78
+ }
79
+ pendingToolApprovals.set(requestId, resolver);
80
+ });
81
+ }
82
+ function resolveToolApproval(requestId, decision) {
83
+ const resolver = pendingToolApprovals.get(requestId);
84
+ if (resolver) {
85
+ resolver(decision);
86
+ }
87
+ }
88
+ // Match stored permission entries against a tool + input combo.
89
+ // This only supports exact tool names and the Bash(command:*) shorthand
90
+ // used by the UI; it intentionally does not implement full glob semantics,
91
+ // introduced to stay consistent with the UI's "Allow rule" format.
92
+ function matchesToolPermission(entry, toolName, input) {
93
+ if (!entry || !toolName) {
94
+ return false;
95
+ }
96
+ if (entry === toolName) {
97
+ return true;
98
+ }
99
+ const bashMatch = entry.match(/^Bash\((.+):\*\)$/);
100
+ if (toolName === 'Bash' && bashMatch) {
101
+ const allowedPrefix = bashMatch[1];
102
+ let command = '';
103
+ if (typeof input === 'string') {
104
+ command = input.trim();
105
+ }
106
+ else if (input && typeof input === 'object' && typeof input.command === 'string') {
107
+ command = input.command.trim();
108
+ }
109
+ if (!command) {
110
+ return false;
111
+ }
112
+ return command.startsWith(allowedPrefix);
113
+ }
114
+ return false;
115
+ }
116
+ /**
117
+ * Maps CLI options to SDK-compatible options format
118
+ * @param {Object} options - CLI options
119
+ * @returns {Object} SDK-compatible options
120
+ */
121
+ function mapCliOptionsToSDK(options = {}) {
122
+ const { sessionId, cwd, toolsSettings, permissionMode } = options;
123
+ const sdkOptions = {};
124
+ // Map working directory
125
+ if (cwd) {
126
+ sdkOptions.cwd = cwd;
127
+ }
128
+ // Map permission mode
129
+ if (permissionMode && permissionMode !== 'default') {
130
+ sdkOptions.permissionMode = permissionMode;
131
+ }
132
+ // Map tool settings
133
+ const settings = toolsSettings || {
134
+ allowedTools: [],
135
+ disallowedTools: [],
136
+ skipPermissions: false
137
+ };
138
+ // Handle tool permissions
139
+ if (settings.skipPermissions && permissionMode !== 'plan') {
140
+ // When skipping permissions, use bypassPermissions mode
141
+ sdkOptions.permissionMode = 'bypassPermissions';
142
+ }
143
+ let allowedTools = [...(settings.allowedTools || [])];
144
+ // Add plan mode default tools
145
+ if (permissionMode === 'plan') {
146
+ const planModeTools = ['Read', 'Task', 'exit_plan_mode', 'TodoRead', 'TodoWrite', 'WebFetch', 'WebSearch'];
147
+ for (const tool of planModeTools) {
148
+ if (!allowedTools.includes(tool)) {
149
+ allowedTools.push(tool);
150
+ }
151
+ }
152
+ }
153
+ sdkOptions.allowedTools = allowedTools;
154
+ // Use the tools preset to make all default built-in tools available (including AskUserQuestion).
155
+ // This was introduced in SDK 0.1.57. Omitting this preserves existing behavior (all tools available),
156
+ // but being explicit ensures forward compatibility and clarity.
157
+ sdkOptions.tools = { type: 'preset', preset: 'claude_code' };
158
+ sdkOptions.disallowedTools = settings.disallowedTools || [];
159
+ // Map model (default to sonnet)
160
+ // Valid models: sonnet, opus, haiku, opusplan, sonnet[1m]
161
+ sdkOptions.model = options.model || CLAUDE_MODELS.DEFAULT;
162
+ // Model logged at query start below
163
+ // Map system prompt configuration
164
+ sdkOptions.systemPrompt = {
165
+ type: 'preset',
166
+ preset: 'claude_code' // Required to use CLAUDE.md
167
+ };
168
+ // Map setting sources for CLAUDE.md loading
169
+ // This loads CLAUDE.md from project, user (~/.config/claude/CLAUDE.md), and local directories
170
+ sdkOptions.settingSources = ['project', 'user', 'local'];
171
+ // Map resume session
172
+ if (sessionId) {
173
+ sdkOptions.resume = sessionId;
174
+ }
175
+ return sdkOptions;
176
+ }
177
+ /**
178
+ * Adds a session to the active sessions map
179
+ * @param {string} sessionId - Session identifier
180
+ * @param {Object} queryInstance - SDK query instance
181
+ * @param {Array<string>} tempImagePaths - Temp image file paths for cleanup
182
+ * @param {string} tempDir - Temp directory for cleanup
183
+ */
184
+ function addSession(sessionId, queryInstance, tempImagePaths = [], tempDir = null, writer = null) {
185
+ activeSessions.set(sessionId, {
186
+ instance: queryInstance,
187
+ startTime: Date.now(),
188
+ status: 'active',
189
+ tempImagePaths,
190
+ tempDir,
191
+ writer
192
+ });
193
+ }
194
+ /**
195
+ * Removes a session from the active sessions map
196
+ * @param {string} sessionId - Session identifier
197
+ */
198
+ function removeSession(sessionId) {
199
+ activeSessions.delete(sessionId);
200
+ }
201
+ /**
202
+ * Gets a session from the active sessions map
203
+ * @param {string} sessionId - Session identifier
204
+ * @returns {Object|undefined} Session data or undefined
205
+ */
206
+ function getSession(sessionId) {
207
+ return activeSessions.get(sessionId);
208
+ }
209
+ /**
210
+ * Gets all active session IDs
211
+ * @returns {Array<string>} Array of active session IDs
212
+ */
213
+ function getAllSessions() {
214
+ return Array.from(activeSessions.keys());
215
+ }
216
+ /**
217
+ * Transforms SDK messages to WebSocket format expected by frontend
218
+ * @param {Object} sdkMessage - SDK message object
219
+ * @returns {Object} Transformed message ready for WebSocket
220
+ */
221
+ function transformMessage(sdkMessage) {
222
+ // Extract parent_tool_use_id for subagent tool grouping
223
+ if (sdkMessage.parent_tool_use_id) {
224
+ return {
225
+ ...sdkMessage,
226
+ parentToolUseId: sdkMessage.parent_tool_use_id
227
+ };
228
+ }
229
+ return sdkMessage;
230
+ }
231
+ /**
232
+ * Extracts token usage from SDK result messages
233
+ * @param {Object} resultMessage - SDK result message
234
+ * @returns {Object|null} Token budget object or null
235
+ */
236
+ function extractTokenBudget(resultMessage) {
237
+ if (resultMessage.type !== 'result' || !resultMessage.modelUsage) {
238
+ return null;
239
+ }
240
+ // Get the first model's usage data
241
+ const modelKey = Object.keys(resultMessage.modelUsage)[0];
242
+ const modelData = resultMessage.modelUsage[modelKey];
243
+ if (!modelData) {
244
+ return null;
245
+ }
246
+ // Use cumulative tokens if available (tracks total for the session)
247
+ // Otherwise fall back to per-request tokens
248
+ const inputTokens = modelData.cumulativeInputTokens || modelData.inputTokens || 0;
249
+ const outputTokens = modelData.cumulativeOutputTokens || modelData.outputTokens || 0;
250
+ const cacheReadTokens = modelData.cumulativeCacheReadInputTokens || modelData.cacheReadInputTokens || 0;
251
+ const cacheCreationTokens = modelData.cumulativeCacheCreationInputTokens || modelData.cacheCreationInputTokens || 0;
252
+ // Total used = input + output + cache tokens
253
+ const totalUsed = inputTokens + outputTokens + cacheReadTokens + cacheCreationTokens;
254
+ // Use configured context window budget from environment (default 160000)
255
+ // This is the user's budget limit, not the model's context window
256
+ const contextWindow = parseInt(process.env.CONTEXT_WINDOW) || 160000;
257
+ // Token calc logged via token-budget WS event
258
+ return {
259
+ used: totalUsed,
260
+ total: contextWindow
261
+ };
262
+ }
263
+ /**
264
+ * Handles image processing for SDK queries
265
+ * Saves base64 images to temporary files and returns modified prompt with file paths
266
+ * @param {string} command - Original user prompt
267
+ * @param {Array} images - Array of image objects with base64 data
268
+ * @param {string} cwd - Working directory for temp file creation
269
+ * @returns {Promise<Object>} {modifiedCommand, tempImagePaths, tempDir}
270
+ */
271
+ async function handleImages(command, images, cwd) {
272
+ const tempImagePaths = [];
273
+ let tempDir = null;
274
+ if (!images || images.length === 0) {
275
+ return { modifiedCommand: command, tempImagePaths, tempDir };
276
+ }
277
+ try {
278
+ // Create temp directory in the project directory
279
+ const workingDir = cwd || process.cwd();
280
+ tempDir = path.join(workingDir, '.tmp', 'images', Date.now().toString());
281
+ await fs.mkdir(tempDir, { recursive: true });
282
+ // Save each image to a temp file
283
+ for (const [index, image] of images.entries()) {
284
+ // Extract base64 data and mime type
285
+ const matches = image.data.match(/^data:([^;]+);base64,(.+)$/);
286
+ if (!matches) {
287
+ console.error('Invalid image data format');
288
+ continue;
289
+ }
290
+ const [, mimeType, base64Data] = matches;
291
+ const extension = mimeType.split('/')[1] || 'png';
292
+ const filename = `image_${index}.${extension}`;
293
+ const filepath = path.join(tempDir, filename);
294
+ // Write base64 data to file
295
+ await fs.writeFile(filepath, Buffer.from(base64Data, 'base64'));
296
+ tempImagePaths.push(filepath);
297
+ }
298
+ // Include the full image paths in the prompt
299
+ let modifiedCommand = command;
300
+ if (tempImagePaths.length > 0 && command && command.trim()) {
301
+ const imageNote = `\n\n[Images provided at the following paths:]\n${tempImagePaths.map((p, i) => `${i + 1}. ${p}`).join('\n')}`;
302
+ modifiedCommand = command + imageNote;
303
+ }
304
+ // Images processed
305
+ return { modifiedCommand, tempImagePaths, tempDir };
306
+ }
307
+ catch (error) {
308
+ console.error('Error processing images for SDK:', error);
309
+ return { modifiedCommand: command, tempImagePaths, tempDir };
310
+ }
311
+ }
312
+ /**
313
+ * Cleans up temporary image files
314
+ * @param {Array<string>} tempImagePaths - Array of temp file paths to delete
315
+ * @param {string} tempDir - Temp directory to remove
316
+ */
317
+ async function cleanupTempFiles(tempImagePaths, tempDir) {
318
+ if (!tempImagePaths || tempImagePaths.length === 0) {
319
+ return;
320
+ }
321
+ try {
322
+ // Delete individual temp files
323
+ for (const imagePath of tempImagePaths) {
324
+ await fs.unlink(imagePath).catch(err => console.error(`Failed to delete temp image ${imagePath}:`, err));
325
+ }
326
+ // Delete temp directory
327
+ if (tempDir) {
328
+ await fs.rm(tempDir, { recursive: true, force: true }).catch(err => console.error(`Failed to delete temp directory ${tempDir}:`, err));
329
+ }
330
+ // Temp files cleaned
331
+ }
332
+ catch (error) {
333
+ console.error('Error during temp file cleanup:', error);
334
+ }
335
+ }
336
+ /**
337
+ * Loads MCP server configurations from ~/.claude.json
338
+ * @param {string} cwd - Current working directory for project-specific configs
339
+ * @returns {Object|null} MCP servers object or null if none found
340
+ */
341
+ async function loadMcpConfig(cwd) {
342
+ try {
343
+ const claudeConfigPath = path.join(os.homedir(), '.claude.json');
344
+ // Check if config file exists
345
+ try {
346
+ await fs.access(claudeConfigPath);
347
+ }
348
+ catch (error) {
349
+ // File doesn't exist, return null
350
+ // No config file
351
+ return null;
352
+ }
353
+ // Read and parse config file
354
+ let claudeConfig;
355
+ try {
356
+ const configContent = await fs.readFile(claudeConfigPath, 'utf8');
357
+ claudeConfig = JSON.parse(configContent);
358
+ }
359
+ catch (error) {
360
+ console.error('Failed to parse ~/.claude.json:', error.message);
361
+ return null;
362
+ }
363
+ // Extract MCP servers (merge global and project-specific)
364
+ let mcpServers = {};
365
+ // Add global MCP servers
366
+ if (claudeConfig.mcpServers && typeof claudeConfig.mcpServers === 'object') {
367
+ mcpServers = { ...claudeConfig.mcpServers };
368
+ // Global MCP servers loaded
369
+ }
370
+ // Add/override with project-specific MCP servers
371
+ if (claudeConfig.claudeProjects && cwd) {
372
+ const projectConfig = claudeConfig.claudeProjects[cwd];
373
+ if (projectConfig && projectConfig.mcpServers && typeof projectConfig.mcpServers === 'object') {
374
+ mcpServers = { ...mcpServers, ...projectConfig.mcpServers };
375
+ // Project MCP servers merged
376
+ }
377
+ }
378
+ // Return null if no servers found
379
+ if (Object.keys(mcpServers).length === 0) {
380
+ return null;
381
+ }
382
+ return mcpServers;
383
+ }
384
+ catch (error) {
385
+ console.error('Error loading MCP config:', error.message);
386
+ return null;
387
+ }
388
+ }
389
+ /**
390
+ * Executes a Claude query using the SDK
391
+ * @param {string} command - User prompt/command
392
+ * @param {Object} options - Query options
393
+ * @param {Object} ws - WebSocket connection
394
+ * @returns {Promise<void>}
395
+ */
396
+ async function queryClaudeSDK(command, options = {}, ws) {
397
+ const { sessionId, sessionSummary } = options;
398
+ let capturedSessionId = sessionId;
399
+ let sessionCreatedSent = false;
400
+ let tempImagePaths = [];
401
+ let tempDir = null;
402
+ const emitNotification = (event) => {
403
+ notifyUserIfEnabled({
404
+ userId: ws?.userId || null,
405
+ writer: ws,
406
+ event
407
+ });
408
+ };
409
+ try {
410
+ // Map CLI options to SDK format
411
+ const sdkOptions = mapCliOptionsToSDK(options);
412
+ // Load MCP configuration
413
+ const mcpServers = await loadMcpConfig(options.cwd);
414
+ if (mcpServers) {
415
+ sdkOptions.mcpServers = mcpServers;
416
+ }
417
+ // Handle images - save to temp files and modify prompt
418
+ const imageResult = await handleImages(command, options.images, options.cwd);
419
+ const finalCommand = imageResult.modifiedCommand;
420
+ tempImagePaths = imageResult.tempImagePaths;
421
+ tempDir = imageResult.tempDir;
422
+ sdkOptions.hooks = {
423
+ Notification: [{
424
+ matcher: '',
425
+ hooks: [async (input) => {
426
+ const message = typeof input?.message === 'string' ? input.message : 'Claude requires your attention.';
427
+ emitNotification(createNotificationEvent({
428
+ provider: 'claude',
429
+ sessionId: capturedSessionId || sessionId || null,
430
+ kind: 'action_required',
431
+ code: 'agent.notification',
432
+ meta: { message, sessionName: sessionSummary },
433
+ severity: 'warning',
434
+ requiresUserAction: true,
435
+ dedupeKey: `claude:hook:notification:${capturedSessionId || sessionId || 'none'}:${message}`
436
+ }));
437
+ return {};
438
+ }]
439
+ }]
440
+ };
441
+ sdkOptions.canUseTool = async (toolName, input, context) => {
442
+ const requiresInteraction = TOOLS_REQUIRING_INTERACTION.has(toolName);
443
+ if (!requiresInteraction) {
444
+ if (sdkOptions.permissionMode === 'bypassPermissions') {
445
+ return { behavior: 'allow', updatedInput: input };
446
+ }
447
+ const isDisallowed = (sdkOptions.disallowedTools || []).some(entry => matchesToolPermission(entry, toolName, input));
448
+ if (isDisallowed) {
449
+ return { behavior: 'deny', message: 'Tool disallowed by settings' };
450
+ }
451
+ const isAllowed = (sdkOptions.allowedTools || []).some(entry => matchesToolPermission(entry, toolName, input));
452
+ if (isAllowed) {
453
+ return { behavior: 'allow', updatedInput: input };
454
+ }
455
+ }
456
+ const requestId = createRequestId();
457
+ ws.send(createNormalizedMessage({ kind: 'permission_request', requestId, toolName, input, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
458
+ emitNotification(createNotificationEvent({
459
+ provider: 'claude',
460
+ sessionId: capturedSessionId || sessionId || null,
461
+ kind: 'action_required',
462
+ code: 'permission.required',
463
+ meta: { toolName, sessionName: sessionSummary },
464
+ severity: 'warning',
465
+ requiresUserAction: true,
466
+ dedupeKey: `claude:permission:${capturedSessionId || sessionId || 'none'}:${requestId}`
467
+ }));
468
+ const decision = await waitForToolApproval(requestId, {
469
+ timeoutMs: requiresInteraction ? 0 : undefined,
470
+ signal: context?.signal,
471
+ metadata: {
472
+ _sessionId: capturedSessionId || sessionId || null,
473
+ _toolName: toolName,
474
+ _input: input,
475
+ _receivedAt: new Date(),
476
+ },
477
+ onCancel: (reason) => {
478
+ ws.send(createNormalizedMessage({ kind: 'permission_cancelled', requestId, reason, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
479
+ }
480
+ });
481
+ if (!decision) {
482
+ return { behavior: 'deny', message: 'Permission request timed out' };
483
+ }
484
+ if (decision.cancelled) {
485
+ return { behavior: 'deny', message: 'Permission request cancelled' };
486
+ }
487
+ if (decision.allow) {
488
+ if (decision.rememberEntry && typeof decision.rememberEntry === 'string') {
489
+ if (!sdkOptions.allowedTools.includes(decision.rememberEntry)) {
490
+ sdkOptions.allowedTools.push(decision.rememberEntry);
491
+ }
492
+ if (Array.isArray(sdkOptions.disallowedTools)) {
493
+ sdkOptions.disallowedTools = sdkOptions.disallowedTools.filter(entry => entry !== decision.rememberEntry);
494
+ }
495
+ }
496
+ return { behavior: 'allow', updatedInput: decision.updatedInput ?? input };
497
+ }
498
+ return { behavior: 'deny', message: decision.message ?? 'User denied tool use' };
499
+ };
500
+ // Set stream-close timeout for interactive tools (Query constructor reads it synchronously). Claude Agent SDK has a default of 5s and this overrides it
501
+ const prevStreamTimeout = process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT;
502
+ process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT = '300000';
503
+ let queryInstance;
504
+ try {
505
+ queryInstance = query({
506
+ prompt: finalCommand,
507
+ options: sdkOptions
508
+ });
509
+ }
510
+ catch (hookError) {
511
+ // Older/newer SDK versions may not accept hook shapes yet.
512
+ // Keep notification behavior operational via runtime events even if hook registration fails.
513
+ console.warn('Failed to initialize Claude query with hooks, retrying without hooks:', hookError?.message || hookError);
514
+ delete sdkOptions.hooks;
515
+ queryInstance = query({
516
+ prompt: finalCommand,
517
+ options: sdkOptions
518
+ });
519
+ }
520
+ // Restore immediately — Query constructor already captured the value
521
+ if (prevStreamTimeout !== undefined) {
522
+ process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT = prevStreamTimeout;
523
+ }
524
+ else {
525
+ delete process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT;
526
+ }
527
+ // Track the query instance for abort capability
528
+ if (capturedSessionId) {
529
+ addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir, ws);
530
+ }
531
+ // Process streaming messages
532
+ console.log('Starting async generator loop for session:', capturedSessionId || 'NEW');
533
+ for await (const message of queryInstance) {
534
+ // Capture session ID from first message
535
+ if (message.session_id && !capturedSessionId) {
536
+ capturedSessionId = message.session_id;
537
+ addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir, ws);
538
+ // Set session ID on writer
539
+ if (ws.setSessionId && typeof ws.setSessionId === 'function') {
540
+ ws.setSessionId(capturedSessionId);
541
+ }
542
+ // Send session-created event only once for new sessions
543
+ if (!sessionId && !sessionCreatedSent) {
544
+ sessionCreatedSent = true;
545
+ ws.send(createNormalizedMessage({ kind: 'session_created', newSessionId: capturedSessionId, sessionId: capturedSessionId, provider: 'claude' }));
546
+ }
547
+ }
548
+ else {
549
+ // session_id already captured
550
+ }
551
+ // Transform and normalize message via adapter
552
+ const transformedMessage = transformMessage(message);
553
+ const sid = capturedSessionId || sessionId || null;
554
+ // Use adapter to normalize SDK events into NormalizedMessage[]
555
+ const normalized = claudeAdapter.normalizeMessage(transformedMessage, sid);
556
+ for (const msg of normalized) {
557
+ // Preserve parentToolUseId from SDK wrapper for subagent tool grouping
558
+ if (transformedMessage.parentToolUseId && !msg.parentToolUseId) {
559
+ msg.parentToolUseId = transformedMessage.parentToolUseId;
560
+ }
561
+ ws.send(msg);
562
+ }
563
+ // Extract and send token budget updates from result messages
564
+ if (message.type === 'result') {
565
+ const models = Object.keys(message.modelUsage || {});
566
+ if (models.length > 0) {
567
+ // Model info available in result message
568
+ }
569
+ const tokenBudgetData = extractTokenBudget(message);
570
+ if (tokenBudgetData) {
571
+ ws.send(createNormalizedMessage({ kind: 'status', text: 'token_budget', tokenBudget: tokenBudgetData, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
572
+ }
573
+ }
574
+ }
575
+ // Clean up session on completion
576
+ if (capturedSessionId) {
577
+ removeSession(capturedSessionId);
578
+ }
579
+ // Clean up temporary image files
580
+ await cleanupTempFiles(tempImagePaths, tempDir);
581
+ // Send completion event
582
+ ws.send(createNormalizedMessage({ kind: 'complete', exitCode: 0, isNewSession: !sessionId && !!command, sessionId: capturedSessionId, provider: 'claude' }));
583
+ notifyRunStopped({
584
+ userId: ws?.userId || null,
585
+ provider: 'claude',
586
+ sessionId: capturedSessionId || sessionId || null,
587
+ sessionName: sessionSummary,
588
+ stopReason: 'completed'
589
+ });
590
+ // Complete
591
+ }
592
+ catch (error) {
593
+ console.error('SDK query error:', error);
594
+ // Clean up session on error
595
+ if (capturedSessionId) {
596
+ removeSession(capturedSessionId);
597
+ }
598
+ // Clean up temporary image files on error
599
+ await cleanupTempFiles(tempImagePaths, tempDir);
600
+ // Send error to WebSocket
601
+ ws.send(createNormalizedMessage({ kind: 'error', content: error.message, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
602
+ notifyRunFailed({
603
+ userId: ws?.userId || null,
604
+ provider: 'claude',
605
+ sessionId: capturedSessionId || sessionId || null,
606
+ sessionName: sessionSummary,
607
+ error
608
+ });
609
+ throw error;
610
+ }
611
+ }
612
+ /**
613
+ * Aborts an active SDK session
614
+ * @param {string} sessionId - Session identifier
615
+ * @returns {boolean} True if session was aborted, false if not found
616
+ */
617
+ async function abortClaudeSDKSession(sessionId) {
618
+ const session = getSession(sessionId);
619
+ if (!session) {
620
+ console.log(`Session ${sessionId} not found`);
621
+ return false;
622
+ }
623
+ try {
624
+ console.log(`Aborting SDK session: ${sessionId}`);
625
+ // Call interrupt() on the query instance
626
+ await session.instance.interrupt();
627
+ // Update session status
628
+ session.status = 'aborted';
629
+ // Clean up temporary image files
630
+ await cleanupTempFiles(session.tempImagePaths, session.tempDir);
631
+ // Clean up session
632
+ removeSession(sessionId);
633
+ return true;
634
+ }
635
+ catch (error) {
636
+ console.error(`Error aborting session ${sessionId}:`, error);
637
+ return false;
638
+ }
639
+ }
640
+ /**
641
+ * Checks if an SDK session is currently active
642
+ * @param {string} sessionId - Session identifier
643
+ * @returns {boolean} True if session is active
644
+ */
645
+ function isClaudeSDKSessionActive(sessionId) {
646
+ const session = getSession(sessionId);
647
+ return session && session.status === 'active';
648
+ }
649
+ /**
650
+ * Gets all active SDK session IDs
651
+ * @returns {Array<string>} Array of active session IDs
652
+ */
653
+ function getActiveClaudeSDKSessions() {
654
+ return getAllSessions();
655
+ }
656
+ /**
657
+ * Get pending tool approvals for a specific session.
658
+ * @param {string} sessionId - The session ID
659
+ * @returns {Array} Array of pending permission request objects
660
+ */
661
+ function getPendingApprovalsForSession(sessionId) {
662
+ const pending = [];
663
+ for (const [requestId, resolver] of pendingToolApprovals.entries()) {
664
+ if (resolver._sessionId === sessionId) {
665
+ pending.push({
666
+ requestId,
667
+ toolName: resolver._toolName || 'UnknownTool',
668
+ input: resolver._input,
669
+ context: resolver._context,
670
+ sessionId,
671
+ receivedAt: resolver._receivedAt || new Date(),
672
+ });
673
+ }
674
+ }
675
+ return pending;
676
+ }
677
+ /**
678
+ * Reconnect a session's WebSocketWriter to a new raw WebSocket.
679
+ * Called when client reconnects (e.g. page refresh) while SDK is still running.
680
+ * @param {string} sessionId - The session ID
681
+ * @param {Object} newRawWs - The new raw WebSocket connection
682
+ * @returns {boolean} True if writer was successfully reconnected
683
+ */
684
+ function reconnectSessionWriter(sessionId, newRawWs) {
685
+ const session = getSession(sessionId);
686
+ if (!session?.writer?.updateWebSocket)
687
+ return false;
688
+ session.writer.updateWebSocket(newRawWs);
689
+ console.log(`[RECONNECT] Writer swapped for session ${sessionId}`);
690
+ return true;
691
+ }
692
+ // Export public API
693
+ export { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions, resolveToolApproval, getPendingApprovalsForSession, reconnectSessionWriter };
694
+ //# sourceMappingURL=claude-sdk.js.map