@siteboon/claude-code-ui 1.8.12 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,85 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Clear Cache - Claude Code UI</title>
5
+ <style>
6
+ body {
7
+ font-family: system-ui, -apple-system, sans-serif;
8
+ max-width: 600px;
9
+ margin: 50px auto;
10
+ padding: 20px;
11
+ line-height: 1.6;
12
+ }
13
+ .success { color: green; }
14
+ .error { color: red; }
15
+ button {
16
+ background: #007bff;
17
+ color: white;
18
+ border: none;
19
+ padding: 10px 20px;
20
+ border-radius: 5px;
21
+ cursor: pointer;
22
+ font-size: 16px;
23
+ margin: 10px 5px;
24
+ }
25
+ button:hover {
26
+ background: #0056b3;
27
+ }
28
+ #status {
29
+ margin-top: 20px;
30
+ padding: 15px;
31
+ border-radius: 5px;
32
+ background: #f0f0f0;
33
+ }
34
+ </style>
35
+ </head>
36
+ <body>
37
+ <h1>Clear Cache & Service Worker</h1>
38
+ <p>If you're seeing a blank page or old content, click the button below to clear all cached data.</p>
39
+
40
+ <button onclick="clearEverything()">Clear Cache & Reload</button>
41
+
42
+ <div id="status"></div>
43
+
44
+ <script>
45
+ async function clearEverything() {
46
+ const status = document.getElementById('status');
47
+ status.innerHTML = '<p>Clearing cache and service workers...</p>';
48
+
49
+ try {
50
+ // Unregister all service workers
51
+ if ('serviceWorker' in navigator) {
52
+ const registrations = await navigator.serviceWorker.getRegistrations();
53
+ for (let registration of registrations) {
54
+ await registration.unregister();
55
+ status.innerHTML += '<p class="success">✓ Unregistered service worker</p>';
56
+ }
57
+ }
58
+
59
+ // Clear all caches
60
+ if ('caches' in window) {
61
+ const cacheNames = await caches.keys();
62
+ for (let cacheName of cacheNames) {
63
+ await caches.delete(cacheName);
64
+ status.innerHTML += `<p class="success">✓ Deleted cache: ${cacheName}</p>`;
65
+ }
66
+ }
67
+
68
+ // Clear localStorage
69
+ localStorage.clear();
70
+ status.innerHTML += '<p class="success">✓ Cleared localStorage</p>';
71
+
72
+ // Clear sessionStorage
73
+ sessionStorage.clear();
74
+ status.innerHTML += '<p class="success">✓ Cleared sessionStorage</p>';
75
+
76
+ status.innerHTML += '<p class="success"><strong>✓ All caches cleared!</strong></p>';
77
+ status.innerHTML += '<p>Cache cleared successfully. You can now close this tab or <a href="/">go to home page</a>.</p>';
78
+
79
+ } catch (error) {
80
+ status.innerHTML += `<p class="error">✗ Error: ${error.message}</p>`;
81
+ }
82
+ }
83
+ </script>
84
+ </body>
85
+ </html>
package/dist/index.html CHANGED
@@ -25,8 +25,11 @@
25
25
 
26
26
  <!-- Prevent zoom on iOS -->
27
27
  <meta name="format-detection" content="telephone=no" />
28
- <script type="module" crossorigin src="/assets/index-Cl5xisCA.js"></script>
29
- <link rel="stylesheet" crossorigin href="/assets/index-Co7ALK3i.css">
28
+ <script type="module" crossorigin src="/assets/index-Be0ToEQx.js"></script>
29
+ <link rel="modulepreload" crossorigin href="/assets/vendor-react-7V_UDHjJ.js">
30
+ <link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-D2k1L1JZ.js">
31
+ <link rel="modulepreload" crossorigin href="/assets/vendor-xterm-jI4BCHEb.js">
32
+ <link rel="stylesheet" crossorigin href="/assets/index-Bmo7Hu70.css">
30
33
  </head>
31
34
  <body>
32
35
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@siteboon/claude-code-ui",
3
- "version": "1.8.12",
3
+ "version": "1.9.1",
4
4
  "description": "A web-based UI for Claude Code CLI",
5
5
  "type": "module",
6
6
  "main": "server/index.js",
@@ -39,6 +39,7 @@
39
39
  "author": "Claude Code UI Contributors",
40
40
  "license": "MIT",
41
41
  "dependencies": {
42
+ "@anthropic-ai/claude-agent-sdk": "^0.1.29",
42
43
  "@codemirror/lang-css": "^6.3.1",
43
44
  "@codemirror/lang-html": "^6.4.9",
44
45
  "@codemirror/lang-javascript": "^6.2.4",
@@ -58,6 +59,8 @@
58
59
  "cors": "^2.8.5",
59
60
  "cross-spawn": "^7.0.3",
60
61
  "express": "^4.18.2",
62
+ "fuse.js": "^7.0.0",
63
+ "gray-matter": "^4.0.3",
61
64
  "jsonwebtoken": "^9.0.2",
62
65
  "lucide-react": "^0.515.0",
63
66
  "mime-types": "^3.0.1",
@@ -73,8 +76,8 @@
73
76
  "sqlite3": "^5.1.7",
74
77
  "tailwind-merge": "^3.3.1",
75
78
  "ws": "^8.14.2",
76
- "xterm": "^5.3.0",
77
- "xterm-addon-fit": "^0.8.0"
79
+ "@xterm/xterm": "^5.5.0",
80
+ "@xterm/addon-fit": "^0.10.0"
78
81
  },
79
82
  "devDependencies": {
80
83
  "@types/react": "^18.2.43",
@@ -0,0 +1,513 @@
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
+
15
+ import { query } from '@anthropic-ai/claude-agent-sdk';
16
+ import { promises as fs } from 'fs';
17
+ import path from 'path';
18
+ import os from 'os';
19
+
20
+ // Session tracking: Map of session IDs to active query instances
21
+ const activeSessions = new Map();
22
+
23
+ /**
24
+ * Maps CLI options to SDK-compatible options format
25
+ * @param {Object} options - CLI options
26
+ * @returns {Object} SDK-compatible options
27
+ */
28
+ function mapCliOptionsToSDK(options = {}) {
29
+ const { sessionId, cwd, toolsSettings, permissionMode, images } = options;
30
+
31
+ const sdkOptions = {};
32
+
33
+ // Map working directory
34
+ if (cwd) {
35
+ sdkOptions.cwd = cwd;
36
+ }
37
+
38
+ // Map permission mode
39
+ if (permissionMode && permissionMode !== 'default') {
40
+ sdkOptions.permissionMode = permissionMode;
41
+ }
42
+
43
+ // Map tool settings
44
+ const settings = toolsSettings || {
45
+ allowedTools: [],
46
+ disallowedTools: [],
47
+ skipPermissions: false
48
+ };
49
+
50
+ // Handle tool permissions
51
+ if (settings.skipPermissions && permissionMode !== 'plan') {
52
+ // When skipping permissions, use bypassPermissions mode
53
+ sdkOptions.permissionMode = 'bypassPermissions';
54
+ } else {
55
+ // Map allowed tools
56
+ let allowedTools = [...(settings.allowedTools || [])];
57
+
58
+ // Add plan mode default tools
59
+ if (permissionMode === 'plan') {
60
+ const planModeTools = ['Read', 'Task', 'exit_plan_mode', 'TodoRead', 'TodoWrite'];
61
+ for (const tool of planModeTools) {
62
+ if (!allowedTools.includes(tool)) {
63
+ allowedTools.push(tool);
64
+ }
65
+ }
66
+ }
67
+
68
+ if (allowedTools.length > 0) {
69
+ sdkOptions.allowedTools = allowedTools;
70
+ }
71
+
72
+ // Map disallowed tools
73
+ if (settings.disallowedTools && settings.disallowedTools.length > 0) {
74
+ sdkOptions.disallowedTools = settings.disallowedTools;
75
+ }
76
+ }
77
+
78
+ // Map model (default to sonnet)
79
+ // Map model (default to sonnet)
80
+ sdkOptions.model = options.model || 'sonnet';
81
+
82
+ // Map resume session
83
+ if (sessionId) {
84
+ sdkOptions.resume = sessionId;
85
+ }
86
+
87
+ return sdkOptions;
88
+ }
89
+
90
+ /**
91
+ * Adds a session to the active sessions map
92
+ * @param {string} sessionId - Session identifier
93
+ * @param {Object} queryInstance - SDK query instance
94
+ * @param {Array<string>} tempImagePaths - Temp image file paths for cleanup
95
+ * @param {string} tempDir - Temp directory for cleanup
96
+ */
97
+ function addSession(sessionId, queryInstance, tempImagePaths = [], tempDir = null) {
98
+ activeSessions.set(sessionId, {
99
+ instance: queryInstance,
100
+ startTime: Date.now(),
101
+ status: 'active',
102
+ tempImagePaths,
103
+ tempDir
104
+ });
105
+ }
106
+
107
+ /**
108
+ * Removes a session from the active sessions map
109
+ * @param {string} sessionId - Session identifier
110
+ */
111
+ function removeSession(sessionId) {
112
+ activeSessions.delete(sessionId);
113
+ }
114
+
115
+ /**
116
+ * Gets a session from the active sessions map
117
+ * @param {string} sessionId - Session identifier
118
+ * @returns {Object|undefined} Session data or undefined
119
+ */
120
+ function getSession(sessionId) {
121
+ return activeSessions.get(sessionId);
122
+ }
123
+
124
+ /**
125
+ * Gets all active session IDs
126
+ * @returns {Array<string>} Array of active session IDs
127
+ */
128
+ function getAllSessions() {
129
+ return Array.from(activeSessions.keys());
130
+ }
131
+
132
+ /**
133
+ * Transforms SDK messages to WebSocket format expected by frontend
134
+ * @param {Object} sdkMessage - SDK message object
135
+ * @returns {Object} Transformed message ready for WebSocket
136
+ */
137
+ function transformMessage(sdkMessage) {
138
+ // SDK messages are already in a format compatible with the frontend
139
+ // The CLI sends them wrapped in {type: 'claude-response', data: message}
140
+ // We'll do the same here to maintain compatibility
141
+ return sdkMessage;
142
+ }
143
+
144
+ /**
145
+ * Extracts token usage from SDK result messages
146
+ * @param {Object} resultMessage - SDK result message
147
+ * @returns {Object|null} Token budget object or null
148
+ */
149
+ function extractTokenBudget(resultMessage) {
150
+ if (resultMessage.type !== 'result' || !resultMessage.modelUsage) {
151
+ return null;
152
+ }
153
+
154
+ // Get the first model's usage data
155
+ const modelKey = Object.keys(resultMessage.modelUsage)[0];
156
+ const modelData = resultMessage.modelUsage[modelKey];
157
+
158
+ if (!modelData) {
159
+ return null;
160
+ }
161
+
162
+ // Use cumulative tokens if available (tracks total for the session)
163
+ // Otherwise fall back to per-request tokens
164
+ const inputTokens = modelData.cumulativeInputTokens || modelData.inputTokens || 0;
165
+ const outputTokens = modelData.cumulativeOutputTokens || modelData.outputTokens || 0;
166
+ const cacheReadTokens = modelData.cumulativeCacheReadInputTokens || modelData.cacheReadInputTokens || 0;
167
+ const cacheCreationTokens = modelData.cumulativeCacheCreationInputTokens || modelData.cacheCreationInputTokens || 0;
168
+
169
+ // Total used = input + output + cache tokens
170
+ const totalUsed = inputTokens + outputTokens + cacheReadTokens + cacheCreationTokens;
171
+
172
+ // Use configured context window budget from environment (default 160000)
173
+ // This is the user's budget limit, not the model's context window
174
+ const contextWindow = parseInt(process.env.CONTEXT_WINDOW) || 160000;
175
+
176
+ console.log(`📊 Token calculation: input=${inputTokens}, output=${outputTokens}, cache=${cacheReadTokens + cacheCreationTokens}, total=${totalUsed}/${contextWindow}`);
177
+
178
+ return {
179
+ used: totalUsed,
180
+ total: contextWindow
181
+ };
182
+ }
183
+
184
+ /**
185
+ * Handles image processing for SDK queries
186
+ * Saves base64 images to temporary files and returns modified prompt with file paths
187
+ * @param {string} command - Original user prompt
188
+ * @param {Array} images - Array of image objects with base64 data
189
+ * @param {string} cwd - Working directory for temp file creation
190
+ * @returns {Promise<Object>} {modifiedCommand, tempImagePaths, tempDir}
191
+ */
192
+ async function handleImages(command, images, cwd) {
193
+ const tempImagePaths = [];
194
+ let tempDir = null;
195
+
196
+ if (!images || images.length === 0) {
197
+ return { modifiedCommand: command, tempImagePaths, tempDir };
198
+ }
199
+
200
+ try {
201
+ // Create temp directory in the project directory
202
+ const workingDir = cwd || process.cwd();
203
+ tempDir = path.join(workingDir, '.tmp', 'images', Date.now().toString());
204
+ await fs.mkdir(tempDir, { recursive: true });
205
+
206
+ // Save each image to a temp file
207
+ for (const [index, image] of images.entries()) {
208
+ // Extract base64 data and mime type
209
+ const matches = image.data.match(/^data:([^;]+);base64,(.+)$/);
210
+ if (!matches) {
211
+ console.error('Invalid image data format');
212
+ continue;
213
+ }
214
+
215
+ const [, mimeType, base64Data] = matches;
216
+ const extension = mimeType.split('/')[1] || 'png';
217
+ const filename = `image_${index}.${extension}`;
218
+ const filepath = path.join(tempDir, filename);
219
+
220
+ // Write base64 data to file
221
+ await fs.writeFile(filepath, Buffer.from(base64Data, 'base64'));
222
+ tempImagePaths.push(filepath);
223
+ }
224
+
225
+ // Include the full image paths in the prompt
226
+ let modifiedCommand = command;
227
+ if (tempImagePaths.length > 0 && command && command.trim()) {
228
+ const imageNote = `\n\n[Images provided at the following paths:]\n${tempImagePaths.map((p, i) => `${i + 1}. ${p}`).join('\n')}`;
229
+ modifiedCommand = command + imageNote;
230
+ }
231
+
232
+ console.log(`📸 Processed ${tempImagePaths.length} images to temp directory: ${tempDir}`);
233
+ return { modifiedCommand, tempImagePaths, tempDir };
234
+ } catch (error) {
235
+ console.error('Error processing images for SDK:', error);
236
+ return { modifiedCommand: command, tempImagePaths, tempDir };
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Cleans up temporary image files
242
+ * @param {Array<string>} tempImagePaths - Array of temp file paths to delete
243
+ * @param {string} tempDir - Temp directory to remove
244
+ */
245
+ async function cleanupTempFiles(tempImagePaths, tempDir) {
246
+ if (!tempImagePaths || tempImagePaths.length === 0) {
247
+ return;
248
+ }
249
+
250
+ try {
251
+ // Delete individual temp files
252
+ for (const imagePath of tempImagePaths) {
253
+ await fs.unlink(imagePath).catch(err =>
254
+ console.error(`Failed to delete temp image ${imagePath}:`, err)
255
+ );
256
+ }
257
+
258
+ // Delete temp directory
259
+ if (tempDir) {
260
+ await fs.rm(tempDir, { recursive: true, force: true }).catch(err =>
261
+ console.error(`Failed to delete temp directory ${tempDir}:`, err)
262
+ );
263
+ }
264
+
265
+ console.log(`🧹 Cleaned up ${tempImagePaths.length} temp image files`);
266
+ } catch (error) {
267
+ console.error('Error during temp file cleanup:', error);
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Loads MCP server configurations from ~/.claude.json
273
+ * @param {string} cwd - Current working directory for project-specific configs
274
+ * @returns {Object|null} MCP servers object or null if none found
275
+ */
276
+ async function loadMcpConfig(cwd) {
277
+ try {
278
+ const claudeConfigPath = path.join(os.homedir(), '.claude.json');
279
+
280
+ // Check if config file exists
281
+ try {
282
+ await fs.access(claudeConfigPath);
283
+ } catch (error) {
284
+ // File doesn't exist, return null
285
+ console.log('📡 No ~/.claude.json found, proceeding without MCP servers');
286
+ return null;
287
+ }
288
+
289
+ // Read and parse config file
290
+ let claudeConfig;
291
+ try {
292
+ const configContent = await fs.readFile(claudeConfigPath, 'utf8');
293
+ claudeConfig = JSON.parse(configContent);
294
+ } catch (error) {
295
+ console.error('❌ Failed to parse ~/.claude.json:', error.message);
296
+ return null;
297
+ }
298
+
299
+ // Extract MCP servers (merge global and project-specific)
300
+ let mcpServers = {};
301
+
302
+ // Add global MCP servers
303
+ if (claudeConfig.mcpServers && typeof claudeConfig.mcpServers === 'object') {
304
+ mcpServers = { ...claudeConfig.mcpServers };
305
+ console.log(`📡 Loaded ${Object.keys(mcpServers).length} global MCP servers`);
306
+ }
307
+
308
+ // Add/override with project-specific MCP servers
309
+ if (claudeConfig.claudeProjects && cwd) {
310
+ const projectConfig = claudeConfig.claudeProjects[cwd];
311
+ if (projectConfig && projectConfig.mcpServers && typeof projectConfig.mcpServers === 'object') {
312
+ mcpServers = { ...mcpServers, ...projectConfig.mcpServers };
313
+ console.log(`📡 Loaded ${Object.keys(projectConfig.mcpServers).length} project-specific MCP servers`);
314
+ }
315
+ }
316
+
317
+ // Return null if no servers found
318
+ if (Object.keys(mcpServers).length === 0) {
319
+ console.log('📡 No MCP servers configured');
320
+ return null;
321
+ }
322
+
323
+ console.log(`✅ Total MCP servers loaded: ${Object.keys(mcpServers).length}`);
324
+ return mcpServers;
325
+ } catch (error) {
326
+ console.error('❌ Error loading MCP config:', error.message);
327
+ return null;
328
+ }
329
+ }
330
+
331
+ /**
332
+ * Executes a Claude query using the SDK
333
+ * @param {string} command - User prompt/command
334
+ * @param {Object} options - Query options
335
+ * @param {Object} ws - WebSocket connection
336
+ * @returns {Promise<void>}
337
+ */
338
+ async function queryClaudeSDK(command, options = {}, ws) {
339
+ const { sessionId } = options;
340
+ let capturedSessionId = sessionId;
341
+ let sessionCreatedSent = false;
342
+ let tempImagePaths = [];
343
+ let tempDir = null;
344
+
345
+ try {
346
+ // Map CLI options to SDK format
347
+ const sdkOptions = mapCliOptionsToSDK(options);
348
+
349
+ // Load MCP configuration
350
+ const mcpServers = await loadMcpConfig(options.cwd);
351
+ if (mcpServers) {
352
+ sdkOptions.mcpServers = mcpServers;
353
+ }
354
+
355
+ // Handle images - save to temp files and modify prompt
356
+ const imageResult = await handleImages(command, options.images, options.cwd);
357
+ const finalCommand = imageResult.modifiedCommand;
358
+ tempImagePaths = imageResult.tempImagePaths;
359
+ tempDir = imageResult.tempDir;
360
+
361
+ // Create SDK query instance
362
+ const queryInstance = query({
363
+ prompt: finalCommand,
364
+ options: sdkOptions
365
+ });
366
+
367
+ // Track the query instance for abort capability
368
+ if (capturedSessionId) {
369
+ addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir);
370
+ }
371
+
372
+ // Process streaming messages
373
+ console.log('🔄 Starting async generator loop for session:', capturedSessionId || 'NEW');
374
+ for await (const message of queryInstance) {
375
+ // Capture session ID from first message
376
+ if (message.session_id && !capturedSessionId) {
377
+ console.log('📝 Captured session ID:', message.session_id);
378
+ capturedSessionId = message.session_id;
379
+ addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir);
380
+
381
+ // Send session-created event only once for new sessions
382
+ if (!sessionId && !sessionCreatedSent) {
383
+ sessionCreatedSent = true;
384
+ ws.send(JSON.stringify({
385
+ type: 'session-created',
386
+ sessionId: capturedSessionId
387
+ }));
388
+ } else {
389
+ console.log('⚠️ Not sending session-created. sessionId:', sessionId, 'sessionCreatedSent:', sessionCreatedSent);
390
+ }
391
+ } else {
392
+ console.log('⚠️ No session_id in message or already captured. message.session_id:', message.session_id, 'capturedSessionId:', capturedSessionId);
393
+ }
394
+
395
+ // Transform and send message to WebSocket
396
+ const transformedMessage = transformMessage(message);
397
+ ws.send(JSON.stringify({
398
+ type: 'claude-response',
399
+ data: transformedMessage
400
+ }));
401
+
402
+ // Extract and send token budget updates from result messages
403
+ if (message.type === 'result') {
404
+ const tokenBudget = extractTokenBudget(message);
405
+ if (tokenBudget) {
406
+ console.log('📊 Token budget from modelUsage:', tokenBudget);
407
+ ws.send(JSON.stringify({
408
+ type: 'token-budget',
409
+ data: tokenBudget
410
+ }));
411
+ }
412
+ }
413
+ }
414
+
415
+ // Clean up session on completion
416
+ if (capturedSessionId) {
417
+ removeSession(capturedSessionId);
418
+ }
419
+
420
+ // Clean up temporary image files
421
+ await cleanupTempFiles(tempImagePaths, tempDir);
422
+
423
+ // Send completion event
424
+ console.log('✅ Streaming complete, sending claude-complete event');
425
+ ws.send(JSON.stringify({
426
+ type: 'claude-complete',
427
+ sessionId: capturedSessionId,
428
+ exitCode: 0,
429
+ isNewSession: !sessionId && !!command
430
+ }));
431
+ console.log('📤 claude-complete event sent');
432
+
433
+ } catch (error) {
434
+ console.error('SDK query error:', error);
435
+
436
+ // Clean up session on error
437
+ if (capturedSessionId) {
438
+ removeSession(capturedSessionId);
439
+ }
440
+
441
+ // Clean up temporary image files on error
442
+ await cleanupTempFiles(tempImagePaths, tempDir);
443
+
444
+ // Send error to WebSocket
445
+ ws.send(JSON.stringify({
446
+ type: 'claude-error',
447
+ error: error.message
448
+ }));
449
+
450
+ throw error;
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Aborts an active SDK session
456
+ * @param {string} sessionId - Session identifier
457
+ * @returns {boolean} True if session was aborted, false if not found
458
+ */
459
+ async function abortClaudeSDKSession(sessionId) {
460
+ const session = getSession(sessionId);
461
+
462
+ if (!session) {
463
+ console.log(`Session ${sessionId} not found`);
464
+ return false;
465
+ }
466
+
467
+ try {
468
+ console.log(`🛑 Aborting SDK session: ${sessionId}`);
469
+
470
+ // Call interrupt() on the query instance
471
+ await session.instance.interrupt();
472
+
473
+ // Update session status
474
+ session.status = 'aborted';
475
+
476
+ // Clean up temporary image files
477
+ await cleanupTempFiles(session.tempImagePaths, session.tempDir);
478
+
479
+ // Clean up session
480
+ removeSession(sessionId);
481
+
482
+ return true;
483
+ } catch (error) {
484
+ console.error(`Error aborting session ${sessionId}:`, error);
485
+ return false;
486
+ }
487
+ }
488
+
489
+ /**
490
+ * Checks if an SDK session is currently active
491
+ * @param {string} sessionId - Session identifier
492
+ * @returns {boolean} True if session is active
493
+ */
494
+ function isClaudeSDKSessionActive(sessionId) {
495
+ const session = getSession(sessionId);
496
+ return session && session.status === 'active';
497
+ }
498
+
499
+ /**
500
+ * Gets all active SDK session IDs
501
+ * @returns {Array<string>} Array of active session IDs
502
+ */
503
+ function getActiveClaudeSDKSessions() {
504
+ return getAllSessions();
505
+ }
506
+
507
+ // Export public API
508
+ export {
509
+ queryClaudeSDK,
510
+ abortClaudeSDKSession,
511
+ isClaudeSDKSessionActive,
512
+ getActiveClaudeSDKSessions
513
+ };
@@ -159,6 +159,7 @@ async function spawnCursor(command, options = {}, ws) {
159
159
  // Send completion event
160
160
  ws.send(JSON.stringify({
161
161
  type: 'cursor-result',
162
+ sessionId: capturedSessionId || sessionId,
162
163
  data: response,
163
164
  success: response.subtype === 'success'
164
165
  }));
@@ -198,9 +199,10 @@ async function spawnCursor(command, options = {}, ws) {
198
199
  // Clean up process reference
199
200
  const finalSessionId = capturedSessionId || sessionId || processKey;
200
201
  activeCursorProcesses.delete(finalSessionId);
201
-
202
+
202
203
  ws.send(JSON.stringify({
203
204
  type: 'claude-complete',
205
+ sessionId: finalSessionId,
204
206
  exitCode: code,
205
207
  isNewSession: !sessionId && !!command // Flag to indicate this was a new session
206
208
  }));
@@ -244,7 +246,17 @@ function abortCursorSession(sessionId) {
244
246
  return false;
245
247
  }
246
248
 
249
+ function isCursorSessionActive(sessionId) {
250
+ return activeCursorProcesses.has(sessionId);
251
+ }
252
+
253
+ function getActiveCursorSessions() {
254
+ return Array.from(activeCursorProcesses.keys());
255
+ }
256
+
247
257
  export {
248
258
  spawnCursor,
249
- abortCursorSession
259
+ abortCursorSession,
260
+ isCursorSessionActive,
261
+ getActiveCursorSessions
250
262
  };
Binary file