@siteboon/claude-code-ui 1.19.1 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ <svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Gemini</title><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" fill="#3186FF"></path><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" fill="url(#lobe-icons-gemini-fill-0)"></path><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" fill="url(#lobe-icons-gemini-fill-1)"></path><path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" fill="url(#lobe-icons-gemini-fill-2)"></path><defs><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-gemini-fill-0" x1="7" x2="11" y1="15.5" y2="12"><stop stop-color="#08B962"></stop><stop offset="1" stop-color="#08B962" stop-opacity="0"></stop></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-gemini-fill-1" x1="8" x2="11.5" y1="5.5" y2="11"><stop stop-color="#F94543"></stop><stop offset="1" stop-color="#F94543" stop-opacity="0"></stop></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-gemini-fill-2" x1="3.5" x2="17.5" y1="13.5" y2="12"><stop stop-color="#FABC12"></stop><stop offset=".46" stop-color="#FABC12" stop-opacity="0"></stop></linearGradient></defs></svg>
package/dist/index.html CHANGED
@@ -25,11 +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-0DqtvI36.js"></script>
28
+ <script type="module" crossorigin src="/assets/index-DN2ZJcRJ.js"></script>
29
29
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-DIN4KjD2.js">
30
- <link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-l-lAmaJ1.js">
31
- <link rel="modulepreload" crossorigin href="/assets/vendor-xterm-DfaPXD3y.js">
32
- <link rel="stylesheet" crossorigin href="/assets/index-BPHfv_yU.css">
30
+ <link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-BMLq5tLB.js">
31
+ <link rel="modulepreload" crossorigin href="/assets/vendor-xterm-CJZjLICi.js">
32
+ <link rel="stylesheet" crossorigin href="/assets/index-Cxnz_sny.css">
33
33
  </head>
34
34
  <body>
35
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.19.1",
3
+ "version": "1.21.0",
4
4
  "description": "A web-based UI for Claude Code CLI",
5
5
  "type": "module",
6
6
  "main": "server/index.js",
@@ -45,7 +45,7 @@
45
45
  "author": "CloudCLI UI Contributors",
46
46
  "license": "GPL-3.0",
47
47
  "dependencies": {
48
- "@anthropic-ai/claude-agent-sdk": "^0.1.71",
48
+ "@anthropic-ai/claude-agent-sdk": "^0.2.59",
49
49
  "@codemirror/lang-css": "^6.3.1",
50
50
  "@codemirror/lang-html": "^6.4.9",
51
51
  "@codemirror/lang-javascript": "^6.2.4",
@@ -66,7 +66,7 @@
66
66
  "@xterm/addon-webgl": "^0.18.0",
67
67
  "@xterm/xterm": "^5.5.0",
68
68
  "bcrypt": "^6.0.0",
69
- "better-sqlite3": "^12.2.0",
69
+ "better-sqlite3": "^12.6.2",
70
70
  "chokidar": "^4.0.3",
71
71
  "class-variance-authority": "^0.7.1",
72
72
  "clsx": "^2.1.1",
@@ -87,6 +87,7 @@
87
87
  "react": "^18.2.0",
88
88
  "react-dom": "^18.2.0",
89
89
  "react-dropzone": "^14.2.3",
90
+ "react-error-boundary": "^4.1.2",
90
91
  "react-i18next": "^16.5.3",
91
92
  "react-markdown": "^10.1.0",
92
93
  "react-router-dom": "^6.8.1",
@@ -593,6 +593,9 @@ async function queryClaudeSDK(command, options = {}, ws) {
593
593
  console.log('No session_id in message or already captured. message.session_id:', message.session_id, 'capturedSessionId:', capturedSessionId);
594
594
  }
595
595
 
596
+ // logs which model was used in the message
597
+ console.log("---> Model was sent using:", Object.keys(message.modelUsage || {}));
598
+
596
599
  // Transform and send message to WebSocket
597
600
  const transformedMessage = transformMessage(message);
598
601
  ws.send({
@@ -40,6 +40,22 @@ if (process.env.DATABASE_PATH) {
40
40
  }
41
41
  }
42
42
 
43
+ // As part of 1.19.2 we are introducing a new location for auth.db. The below handles exisitng moving legacy database from install directory to new location
44
+ const LEGACY_DB_PATH = path.join(__dirname, 'auth.db');
45
+ if (DB_PATH !== LEGACY_DB_PATH && !fs.existsSync(DB_PATH) && fs.existsSync(LEGACY_DB_PATH)) {
46
+ try {
47
+ fs.copyFileSync(LEGACY_DB_PATH, DB_PATH);
48
+ console.log(`[MIGRATION] Copied database from ${LEGACY_DB_PATH} to ${DB_PATH}`);
49
+ for (const suffix of ['-wal', '-shm']) {
50
+ if (fs.existsSync(LEGACY_DB_PATH + suffix)) {
51
+ fs.copyFileSync(LEGACY_DB_PATH + suffix, DB_PATH + suffix);
52
+ }
53
+ }
54
+ } catch (err) {
55
+ console.warn(`[MIGRATION] Could not copy legacy database: ${err.message}`);
56
+ }
57
+ }
58
+
43
59
  // Create database connection
44
60
  const db = new Database(DB_PATH);
45
61
 
@@ -128,12 +144,12 @@ const userDb = {
128
144
  }
129
145
  },
130
146
 
131
- // Update last login time
147
+ // Update last login time (non-fatal — logged but not thrown)
132
148
  updateLastLogin: (userId) => {
133
149
  try {
134
150
  db.prepare('UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?').run(userId);
135
151
  } catch (err) {
136
- throw err;
152
+ console.warn('Failed to update last login:', err.message);
137
153
  }
138
154
  },
139
155
 
@@ -0,0 +1,455 @@
1
+ import { spawn } from 'child_process';
2
+ import crossSpawn from 'cross-spawn';
3
+
4
+ // Use cross-spawn on Windows for correct .cmd resolution (same pattern as cursor-cli.js)
5
+ const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
6
+ import { promises as fs } from 'fs';
7
+ import path from 'path';
8
+ import os from 'os';
9
+ import { getSessions, getSessionMessages } from './projects.js';
10
+ import sessionManager from './sessionManager.js';
11
+ import GeminiResponseHandler from './gemini-response-handler.js';
12
+
13
+ let activeGeminiProcesses = new Map(); // Track active processes by session ID
14
+
15
+ async function spawnGemini(command, options = {}, ws) {
16
+ const { sessionId, projectPath, cwd, resume, toolsSettings, permissionMode, images } = options;
17
+ let capturedSessionId = sessionId; // Track session ID throughout the process
18
+ let sessionCreatedSent = false; // Track if we've already sent session-created event
19
+ let assistantBlocks = []; // Accumulate the full response blocks including tools
20
+
21
+ // Use tools settings passed from frontend, or defaults
22
+ const settings = toolsSettings || {
23
+ allowedTools: [],
24
+ disallowedTools: [],
25
+ skipPermissions: false
26
+ };
27
+
28
+ // Build Gemini CLI command - start with print/resume flags first
29
+ const args = [];
30
+
31
+ // Add prompt flag with command if we have a command
32
+ if (command && command.trim()) {
33
+ args.push('--prompt', command);
34
+ }
35
+
36
+ // If we have a sessionId, we want to resume
37
+ if (sessionId) {
38
+ const session = sessionManager.getSession(sessionId);
39
+ if (session && session.cliSessionId) {
40
+ args.push('--resume', session.cliSessionId);
41
+ }
42
+ }
43
+
44
+ // Use cwd (actual project directory) instead of projectPath (Gemini's metadata directory)
45
+ // Clean the path by removing any non-printable characters
46
+ const cleanPath = (cwd || projectPath || process.cwd()).replace(/[^\x20-\x7E]/g, '').trim();
47
+ const workingDir = cleanPath;
48
+
49
+ // Handle images by saving them to temporary files and passing paths to Gemini
50
+ const tempImagePaths = [];
51
+ let tempDir = null;
52
+ if (images && images.length > 0) {
53
+ try {
54
+ // Create temp directory in the project directory so Gemini can access it
55
+ tempDir = path.join(workingDir, '.tmp', 'images', Date.now().toString());
56
+ await fs.mkdir(tempDir, { recursive: true });
57
+
58
+ // Save each image to a temp file
59
+ for (const [index, image] of images.entries()) {
60
+ // Extract base64 data and mime type
61
+ const matches = image.data.match(/^data:([^;]+);base64,(.+)$/);
62
+ if (!matches) {
63
+ continue;
64
+ }
65
+
66
+ const [, mimeType, base64Data] = matches;
67
+ const extension = mimeType.split('/')[1] || 'png';
68
+ const filename = `image_${index}.${extension}`;
69
+ const filepath = path.join(tempDir, filename);
70
+
71
+ // Write base64 data to file
72
+ await fs.writeFile(filepath, Buffer.from(base64Data, 'base64'));
73
+ tempImagePaths.push(filepath);
74
+ }
75
+
76
+ // Include the full image paths in the prompt for Gemini to reference
77
+ // Gemini CLI can read images from file paths in the prompt
78
+ if (tempImagePaths.length > 0 && command && command.trim()) {
79
+ const imageNote = `\n\n[Images given: ${tempImagePaths.length} images are located at the following paths:]\n${tempImagePaths.map((p, i) => `${i + 1}. ${p}`).join('\n')}`;
80
+ const modifiedCommand = command + imageNote;
81
+
82
+ // Update the command in args
83
+ const promptIndex = args.indexOf('--prompt');
84
+ if (promptIndex !== -1 && args[promptIndex + 1] === command) {
85
+ args[promptIndex + 1] = modifiedCommand;
86
+ } else if (promptIndex !== -1) {
87
+ // If we're using context, update the full prompt
88
+ args[promptIndex + 1] = args[promptIndex + 1] + imageNote;
89
+ }
90
+ }
91
+ } catch (error) {
92
+ console.error('Error processing images for Gemini:', error);
93
+ }
94
+ }
95
+
96
+ // Add basic flags for Gemini
97
+ if (options.debug) {
98
+ args.push('--debug');
99
+ }
100
+
101
+ // Add MCP config flag only if MCP servers are configured
102
+ try {
103
+ const geminiConfigPath = path.join(os.homedir(), '.gemini.json');
104
+ let hasMcpServers = false;
105
+
106
+ try {
107
+ await fs.access(geminiConfigPath);
108
+ const geminiConfigRaw = await fs.readFile(geminiConfigPath, 'utf8');
109
+ const geminiConfig = JSON.parse(geminiConfigRaw);
110
+
111
+ // Check global MCP servers
112
+ if (geminiConfig.mcpServers && Object.keys(geminiConfig.mcpServers).length > 0) {
113
+ hasMcpServers = true;
114
+ }
115
+
116
+ // Check project-specific MCP servers
117
+ if (!hasMcpServers && geminiConfig.geminiProjects) {
118
+ const currentProjectPath = process.cwd();
119
+ const projectConfig = geminiConfig.geminiProjects[currentProjectPath];
120
+ if (projectConfig && projectConfig.mcpServers && Object.keys(projectConfig.mcpServers).length > 0) {
121
+ hasMcpServers = true;
122
+ }
123
+ }
124
+ } catch (e) {
125
+ // Ignore if file doesn't exist or isn't parsable
126
+ }
127
+
128
+ if (hasMcpServers) {
129
+ args.push('--mcp-config', geminiConfigPath);
130
+ }
131
+ } catch (error) {
132
+ // Ignore outer errors
133
+ }
134
+
135
+ // Add model for all sessions (both new and resumed)
136
+ let modelToUse = options.model || 'gemini-2.5-flash';
137
+ args.push('--model', modelToUse);
138
+ args.push('--output-format', 'stream-json');
139
+
140
+ // Handle approval modes and allowed tools
141
+ if (settings.skipPermissions || options.skipPermissions || permissionMode === 'yolo') {
142
+ args.push('--yolo');
143
+ } else if (permissionMode === 'auto_edit') {
144
+ args.push('--approval-mode', 'auto_edit');
145
+ } else if (permissionMode === 'plan') {
146
+ args.push('--approval-mode', 'plan');
147
+ }
148
+
149
+ if (settings.allowedTools && settings.allowedTools.length > 0) {
150
+ args.push('--allowed-tools', settings.allowedTools.join(','));
151
+ }
152
+
153
+ // Try to find gemini in PATH first, then fall back to environment variable
154
+ const geminiPath = process.env.GEMINI_PATH || 'gemini';
155
+ console.log('Spawning Gemini CLI:', geminiPath, args.join(' '));
156
+ console.log('Working directory:', workingDir);
157
+
158
+ let spawnCmd = geminiPath;
159
+ let spawnArgs = args;
160
+
161
+ // On non-Windows platforms, wrap the execution in a shell to avoid ENOEXEC
162
+ // which happens when the target is a script lacking a shebang.
163
+ if (os.platform() !== 'win32') {
164
+ spawnCmd = 'sh';
165
+ // Use exec to replace the shell process, ensuring signals hit gemini directly
166
+ spawnArgs = ['-c', 'exec "$0" "$@"', geminiPath, ...args];
167
+ }
168
+
169
+ return new Promise((resolve, reject) => {
170
+ const geminiProcess = spawnFunction(spawnCmd, spawnArgs, {
171
+ cwd: workingDir,
172
+ stdio: ['pipe', 'pipe', 'pipe'],
173
+ env: { ...process.env } // Inherit all environment variables
174
+ });
175
+
176
+ // Attach temp file info to process for cleanup later
177
+ geminiProcess.tempImagePaths = tempImagePaths;
178
+ geminiProcess.tempDir = tempDir;
179
+
180
+ // Store process reference for potential abort
181
+ const processKey = capturedSessionId || sessionId || Date.now().toString();
182
+ activeGeminiProcesses.set(processKey, geminiProcess);
183
+
184
+ // Store sessionId on the process object for debugging
185
+ geminiProcess.sessionId = processKey;
186
+
187
+ // Close stdin to signal we're done sending input
188
+ geminiProcess.stdin.end();
189
+
190
+ // Add timeout handler
191
+ let hasReceivedOutput = false;
192
+ const timeoutMs = 120000; // 120 seconds for slower models
193
+ let timeout;
194
+
195
+ const startTimeout = () => {
196
+ if (timeout) clearTimeout(timeout);
197
+ timeout = setTimeout(() => {
198
+ const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : (capturedSessionId || sessionId || processKey);
199
+ ws.send({
200
+ type: 'gemini-error',
201
+ sessionId: socketSessionId,
202
+ error: `Gemini CLI timeout - no response received for ${timeoutMs / 1000} seconds`
203
+ });
204
+ try {
205
+ geminiProcess.kill('SIGTERM');
206
+ } catch (e) { }
207
+ }, timeoutMs);
208
+ };
209
+
210
+ startTimeout();
211
+
212
+ // Save user message to session when starting
213
+ if (command && capturedSessionId) {
214
+ sessionManager.addMessage(capturedSessionId, 'user', command);
215
+ }
216
+
217
+ // Create response handler for NDJSON buffering
218
+ let responseHandler;
219
+ if (ws) {
220
+ responseHandler = new GeminiResponseHandler(ws, {
221
+ onContentFragment: (content) => {
222
+ if (assistantBlocks.length > 0 && assistantBlocks[assistantBlocks.length - 1].type === 'text') {
223
+ assistantBlocks[assistantBlocks.length - 1].text += content;
224
+ } else {
225
+ assistantBlocks.push({ type: 'text', text: content });
226
+ }
227
+ },
228
+ onToolUse: (event) => {
229
+ assistantBlocks.push({
230
+ type: 'tool_use',
231
+ id: event.tool_id,
232
+ name: event.tool_name,
233
+ input: event.parameters
234
+ });
235
+ },
236
+ onToolResult: (event) => {
237
+ if (capturedSessionId) {
238
+ if (assistantBlocks.length > 0) {
239
+ sessionManager.addMessage(capturedSessionId, 'assistant', [...assistantBlocks]);
240
+ assistantBlocks = [];
241
+ }
242
+ sessionManager.addMessage(capturedSessionId, 'user', [{
243
+ type: 'tool_result',
244
+ tool_use_id: event.tool_id,
245
+ content: event.output === undefined ? null : event.output,
246
+ is_error: event.status === 'error'
247
+ }]);
248
+ }
249
+ },
250
+ onInit: (event) => {
251
+ if (capturedSessionId) {
252
+ const sess = sessionManager.getSession(capturedSessionId);
253
+ if (sess && !sess.cliSessionId) {
254
+ sess.cliSessionId = event.session_id;
255
+ sessionManager.saveSession(capturedSessionId);
256
+ }
257
+ }
258
+ }
259
+ });
260
+ }
261
+
262
+ // Handle stdout
263
+ geminiProcess.stdout.on('data', (data) => {
264
+ const rawOutput = data.toString();
265
+ hasReceivedOutput = true;
266
+ startTimeout(); // Re-arm the timeout
267
+
268
+ // For new sessions, create a session ID FIRST
269
+ if (!sessionId && !sessionCreatedSent && !capturedSessionId) {
270
+ capturedSessionId = `gemini_${Date.now()}`;
271
+ sessionCreatedSent = true;
272
+
273
+ // Create session in session manager
274
+ sessionManager.createSession(capturedSessionId, cwd || process.cwd());
275
+
276
+ // Save the user message now that we have a session ID
277
+ if (command) {
278
+ sessionManager.addMessage(capturedSessionId, 'user', command);
279
+ }
280
+
281
+ // Update process key with captured session ID
282
+ if (processKey !== capturedSessionId) {
283
+ activeGeminiProcesses.delete(processKey);
284
+ activeGeminiProcesses.set(capturedSessionId, geminiProcess);
285
+ }
286
+
287
+ ws.setSessionId && typeof ws.setSessionId === 'function' && ws.setSessionId(capturedSessionId);
288
+
289
+ ws.send({
290
+ type: 'session-created',
291
+ sessionId: capturedSessionId
292
+ });
293
+
294
+ // Emit fake system init so the frontend immediately navigates and saves the session
295
+ ws.send({
296
+ type: 'claude-response',
297
+ sessionId: capturedSessionId,
298
+ data: {
299
+ type: 'system',
300
+ subtype: 'init',
301
+ session_id: capturedSessionId
302
+ }
303
+ });
304
+ }
305
+
306
+ if (responseHandler) {
307
+ responseHandler.processData(rawOutput);
308
+ } else if (rawOutput) {
309
+ // Fallback to direct sending for raw CLI mode without WS
310
+ if (assistantBlocks.length > 0 && assistantBlocks[assistantBlocks.length - 1].type === 'text') {
311
+ assistantBlocks[assistantBlocks.length - 1].text += rawOutput;
312
+ } else {
313
+ assistantBlocks.push({ type: 'text', text: rawOutput });
314
+ }
315
+ const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : (capturedSessionId || sessionId);
316
+ ws.send({
317
+ type: 'gemini-response',
318
+ sessionId: socketSessionId,
319
+ data: {
320
+ type: 'message',
321
+ content: rawOutput
322
+ }
323
+ });
324
+ }
325
+ });
326
+
327
+ // Handle stderr
328
+ geminiProcess.stderr.on('data', (data) => {
329
+ const errorMsg = data.toString();
330
+
331
+ // Filter out deprecation warnings and "Loaded cached credentials" message
332
+ if (errorMsg.includes('[DEP0040]') ||
333
+ errorMsg.includes('DeprecationWarning') ||
334
+ errorMsg.includes('--trace-deprecation') ||
335
+ errorMsg.includes('Loaded cached credentials')) {
336
+ return;
337
+ }
338
+
339
+ const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : (capturedSessionId || sessionId);
340
+ ws.send({
341
+ type: 'gemini-error',
342
+ sessionId: socketSessionId,
343
+ error: errorMsg
344
+ });
345
+ });
346
+
347
+ // Handle process completion
348
+ geminiProcess.on('close', async (code) => {
349
+ clearTimeout(timeout);
350
+
351
+ // Flush any remaining buffered content
352
+ if (responseHandler) {
353
+ responseHandler.forceFlush();
354
+ responseHandler.destroy();
355
+ }
356
+
357
+ // Clean up process reference
358
+ const finalSessionId = capturedSessionId || sessionId || processKey;
359
+ activeGeminiProcesses.delete(finalSessionId);
360
+
361
+ // Save assistant response to session if we have one
362
+ if (finalSessionId && assistantBlocks.length > 0) {
363
+ sessionManager.addMessage(finalSessionId, 'assistant', assistantBlocks);
364
+ }
365
+
366
+ ws.send({
367
+ type: 'claude-complete', // Use claude-complete for compatibility with UI
368
+ sessionId: finalSessionId,
369
+ exitCode: code,
370
+ isNewSession: !sessionId && !!command // Flag to indicate this was a new session
371
+ });
372
+
373
+ // Clean up temporary image files if any
374
+ if (geminiProcess.tempImagePaths && geminiProcess.tempImagePaths.length > 0) {
375
+ for (const imagePath of geminiProcess.tempImagePaths) {
376
+ await fs.unlink(imagePath).catch(err => { });
377
+ }
378
+ if (geminiProcess.tempDir) {
379
+ await fs.rm(geminiProcess.tempDir, { recursive: true, force: true }).catch(err => { });
380
+ }
381
+ }
382
+
383
+ if (code === 0) {
384
+ resolve();
385
+ } else {
386
+ reject(new Error(code === null ? 'Gemini CLI process was terminated or timed out' : `Gemini CLI exited with code ${code}`));
387
+ }
388
+ });
389
+
390
+ // Handle process errors
391
+ geminiProcess.on('error', (error) => {
392
+ // Clean up process reference on error
393
+ const finalSessionId = capturedSessionId || sessionId || processKey;
394
+ activeGeminiProcesses.delete(finalSessionId);
395
+
396
+ const errorSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : finalSessionId;
397
+ ws.send({
398
+ type: 'gemini-error',
399
+ sessionId: errorSessionId,
400
+ error: error.message
401
+ });
402
+
403
+ reject(error);
404
+ });
405
+
406
+ });
407
+ }
408
+
409
+ function abortGeminiSession(sessionId) {
410
+ let geminiProc = activeGeminiProcesses.get(sessionId);
411
+ let processKey = sessionId;
412
+
413
+ if (!geminiProc) {
414
+ for (const [key, proc] of activeGeminiProcesses.entries()) {
415
+ if (proc.sessionId === sessionId) {
416
+ geminiProc = proc;
417
+ processKey = key;
418
+ break;
419
+ }
420
+ }
421
+ }
422
+
423
+ if (geminiProc) {
424
+ try {
425
+ geminiProc.kill('SIGTERM');
426
+ setTimeout(() => {
427
+ if (activeGeminiProcesses.has(processKey)) {
428
+ try {
429
+ geminiProc.kill('SIGKILL');
430
+ } catch (e) { }
431
+ }
432
+ }, 2000); // Wait 2 seconds before force kill
433
+
434
+ return true;
435
+ } catch (error) {
436
+ return false;
437
+ }
438
+ }
439
+ return false;
440
+ }
441
+
442
+ function isGeminiSessionActive(sessionId) {
443
+ return activeGeminiProcesses.has(sessionId);
444
+ }
445
+
446
+ function getActiveGeminiSessions() {
447
+ return Array.from(activeGeminiProcesses.keys());
448
+ }
449
+
450
+ export {
451
+ spawnGemini,
452
+ abortGeminiSession,
453
+ isGeminiSessionActive,
454
+ getActiveGeminiSessions
455
+ };