@siteboon/claude-code-ui 1.8.11 → 1.9.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.
@@ -1,391 +0,0 @@
1
- import { spawn } from 'child_process';
2
- import crossSpawn from 'cross-spawn';
3
- import { promises as fs } from 'fs';
4
- import path from 'path';
5
- import os from 'os';
6
-
7
- // Use cross-spawn on Windows for better command execution
8
- const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
9
-
10
- let activeClaudeProcesses = new Map(); // Track active processes by session ID
11
-
12
- async function spawnClaude(command, options = {}, ws) {
13
- return new Promise(async (resolve, reject) => {
14
- const { sessionId, projectPath, cwd, resume, toolsSettings, permissionMode, images } = options;
15
- let capturedSessionId = sessionId; // Track session ID throughout the process
16
- let sessionCreatedSent = false; // Track if we've already sent session-created event
17
-
18
- // Use tools settings passed from frontend, or defaults
19
- const settings = toolsSettings || {
20
- allowedTools: [],
21
- disallowedTools: [],
22
- skipPermissions: false
23
- };
24
-
25
- // Build Claude CLI command - start with print/resume flags first
26
- const args = [];
27
-
28
- // Add print flag with command if we have a command
29
- if (command && command.trim()) {
30
-
31
- // Separate arguments for better cross-platform compatibility
32
- // This prevents issues with spaces and quotes on Windows
33
- args.push('--print');
34
- args.push(command);
35
- }
36
-
37
- // Use cwd (actual project directory) instead of projectPath (Claude's metadata directory)
38
- const workingDir = cwd || process.cwd();
39
-
40
- // Handle images by saving them to temporary files and passing paths to Claude
41
- const tempImagePaths = [];
42
- let tempDir = null;
43
- if (images && images.length > 0) {
44
- try {
45
- // Create temp directory in the project directory so Claude can access it
46
- tempDir = path.join(workingDir, '.tmp', 'images', Date.now().toString());
47
- await fs.mkdir(tempDir, { recursive: true });
48
-
49
- // Save each image to a temp file
50
- for (const [index, image] of images.entries()) {
51
- // Extract base64 data and mime type
52
- const matches = image.data.match(/^data:([^;]+);base64,(.+)$/);
53
- if (!matches) {
54
- console.error('Invalid image data format');
55
- continue;
56
- }
57
-
58
- const [, mimeType, base64Data] = matches;
59
- const extension = mimeType.split('/')[1] || 'png';
60
- const filename = `image_${index}.${extension}`;
61
- const filepath = path.join(tempDir, filename);
62
-
63
- // Write base64 data to file
64
- await fs.writeFile(filepath, Buffer.from(base64Data, 'base64'));
65
- tempImagePaths.push(filepath);
66
- }
67
-
68
- // Include the full image paths in the prompt for Claude to reference
69
- // Only modify the command if we actually have images and a command
70
- if (tempImagePaths.length > 0 && command && command.trim()) {
71
- const imageNote = `\n\n[Images provided at the following paths:]\n${tempImagePaths.map((p, i) => `${i + 1}. ${p}`).join('\n')}`;
72
- const modifiedCommand = command + imageNote;
73
-
74
- // Update the command in args - now that --print and command are separate
75
- const printIndex = args.indexOf('--print');
76
- if (printIndex !== -1 && printIndex + 1 < args.length && args[printIndex + 1] === command) {
77
- args[printIndex + 1] = modifiedCommand;
78
- }
79
- }
80
-
81
-
82
- } catch (error) {
83
- console.error('Error processing images for Claude:', error);
84
- }
85
- }
86
-
87
- // Add resume flag if resuming
88
- if (resume && sessionId) {
89
- args.push('--resume', sessionId);
90
- }
91
-
92
- // Add basic flags
93
- args.push('--output-format', 'stream-json', '--verbose');
94
-
95
- // Add MCP config flag only if MCP servers are configured
96
- try {
97
- console.log('🔍 Starting MCP config check...');
98
- // Use already imported modules (fs.promises is imported as fs, path, os)
99
- const fsSync = await import('fs'); // Import synchronous fs methods
100
- console.log('✅ Successfully imported fs sync methods');
101
-
102
- // Check for MCP config in ~/.claude.json
103
- const claudeConfigPath = path.join(os.homedir(), '.claude.json');
104
-
105
- console.log(`🔍 Checking for MCP configs in: ${claudeConfigPath}`);
106
- console.log(` Claude config exists: ${fsSync.existsSync(claudeConfigPath)}`);
107
-
108
- let hasMcpServers = false;
109
-
110
- // Check Claude config for MCP servers
111
- if (fsSync.existsSync(claudeConfigPath)) {
112
- try {
113
- const claudeConfig = JSON.parse(fsSync.readFileSync(claudeConfigPath, 'utf8'));
114
-
115
- // Check global MCP servers
116
- if (claudeConfig.mcpServers && Object.keys(claudeConfig.mcpServers).length > 0) {
117
- console.log(`✅ Found ${Object.keys(claudeConfig.mcpServers).length} global MCP servers`);
118
- hasMcpServers = true;
119
- }
120
-
121
- // Check project-specific MCP servers
122
- if (!hasMcpServers && claudeConfig.claudeProjects) {
123
- const currentProjectPath = process.cwd();
124
- const projectConfig = claudeConfig.claudeProjects[currentProjectPath];
125
- if (projectConfig && projectConfig.mcpServers && Object.keys(projectConfig.mcpServers).length > 0) {
126
- console.log(`✅ Found ${Object.keys(projectConfig.mcpServers).length} project MCP servers`);
127
- hasMcpServers = true;
128
- }
129
- }
130
- } catch (e) {
131
- console.log(`❌ Failed to parse Claude config:`, e.message);
132
- }
133
- }
134
-
135
- console.log(`🔍 hasMcpServers result: ${hasMcpServers}`);
136
-
137
- if (hasMcpServers) {
138
- // Use Claude config file if it has MCP servers
139
- let configPath = null;
140
-
141
- if (fsSync.existsSync(claudeConfigPath)) {
142
- try {
143
- const claudeConfig = JSON.parse(fsSync.readFileSync(claudeConfigPath, 'utf8'));
144
-
145
- // Check if we have any MCP servers (global or project-specific)
146
- const hasGlobalServers = claudeConfig.mcpServers && Object.keys(claudeConfig.mcpServers).length > 0;
147
- const currentProjectPath = process.cwd();
148
- const projectConfig = claudeConfig.claudeProjects && claudeConfig.claudeProjects[currentProjectPath];
149
- const hasProjectServers = projectConfig && projectConfig.mcpServers && Object.keys(projectConfig.mcpServers).length > 0;
150
-
151
- if (hasGlobalServers || hasProjectServers) {
152
- configPath = claudeConfigPath;
153
- }
154
- } catch (e) {
155
- // No valid config found
156
- }
157
- }
158
-
159
- if (configPath) {
160
- console.log(`📡 Adding MCP config: ${configPath}`);
161
- args.push('--mcp-config', configPath);
162
- } else {
163
- console.log('⚠️ MCP servers detected but no valid config file found');
164
- }
165
- }
166
- } catch (error) {
167
- // If there's any error checking for MCP configs, don't add the flag
168
- console.log('❌ MCP config check failed:', error.message);
169
- console.log('📍 Error stack:', error.stack);
170
- console.log('Note: MCP config check failed, proceeding without MCP support');
171
- }
172
-
173
- // Add model for new sessions
174
- if (!resume) {
175
- args.push('--model', 'sonnet');
176
- }
177
-
178
- // Add permission mode if specified (works for both new and resumed sessions)
179
- if (permissionMode && permissionMode !== 'default') {
180
- args.push('--permission-mode', permissionMode);
181
- console.log('🔒 Using permission mode:', permissionMode);
182
- }
183
-
184
- // Add tools settings flags
185
- // Don't use --dangerously-skip-permissions when in plan mode
186
- if (settings.skipPermissions && permissionMode !== 'plan') {
187
- args.push('--dangerously-skip-permissions');
188
- console.log('⚠️ Using --dangerously-skip-permissions (skipping other tool settings)');
189
- } else {
190
- // Only add allowed/disallowed tools if not skipping permissions
191
-
192
- // Collect all allowed tools, including plan mode defaults
193
- let allowedTools = [...(settings.allowedTools || [])];
194
-
195
- // Add plan mode specific tools
196
- if (permissionMode === 'plan') {
197
- const planModeTools = ['Read', 'Task', 'exit_plan_mode', 'TodoRead', 'TodoWrite'];
198
- // Add plan mode tools that aren't already in the allowed list
199
- for (const tool of planModeTools) {
200
- if (!allowedTools.includes(tool)) {
201
- allowedTools.push(tool);
202
- }
203
- }
204
- console.log('📝 Plan mode: Added default allowed tools:', planModeTools);
205
- }
206
-
207
- // Add allowed tools
208
- if (allowedTools.length > 0) {
209
- for (const tool of allowedTools) {
210
- args.push('--allowedTools', tool);
211
- console.log('✅ Allowing tool:', tool);
212
- }
213
- }
214
-
215
- // Add disallowed tools
216
- if (settings.disallowedTools && settings.disallowedTools.length > 0) {
217
- for (const tool of settings.disallowedTools) {
218
- args.push('--disallowedTools', tool);
219
- console.log('❌ Disallowing tool:', tool);
220
- }
221
- }
222
-
223
- // Log when skip permissions is disabled due to plan mode
224
- if (settings.skipPermissions && permissionMode === 'plan') {
225
- console.log('📝 Skip permissions disabled due to plan mode');
226
- }
227
- }
228
-
229
- console.log('Spawning Claude CLI:', 'claude', args.map(arg => {
230
- const cleanArg = arg.replace(/\n/g, '\\n').replace(/\r/g, '\\r');
231
- return cleanArg.includes(' ') ? `"${cleanArg}"` : cleanArg;
232
- }).join(' '));
233
- console.log('Working directory:', workingDir);
234
- console.log('Session info - Input sessionId:', sessionId, 'Resume:', resume);
235
- console.log('🔍 Full command args:', JSON.stringify(args, null, 2));
236
- console.log('🔍 Final Claude command will be: claude ' + args.join(' '));
237
-
238
- const claudeProcess = spawnFunction('claude', args, {
239
- cwd: workingDir,
240
- stdio: ['pipe', 'pipe', 'pipe'],
241
- env: { ...process.env } // Inherit all environment variables
242
- });
243
-
244
- // Attach temp file info to process for cleanup later
245
- claudeProcess.tempImagePaths = tempImagePaths;
246
- claudeProcess.tempDir = tempDir;
247
-
248
- // Store process reference for potential abort
249
- const processKey = capturedSessionId || sessionId || Date.now().toString();
250
- activeClaudeProcesses.set(processKey, claudeProcess);
251
-
252
- // Handle stdout (streaming JSON responses)
253
- claudeProcess.stdout.on('data', (data) => {
254
- const rawOutput = data.toString();
255
- console.log('📤 Claude CLI stdout:', rawOutput);
256
-
257
- const lines = rawOutput.split('\n').filter(line => line.trim());
258
-
259
- for (const line of lines) {
260
- try {
261
- const response = JSON.parse(line);
262
- console.log('📄 Parsed JSON response:', response);
263
-
264
- // Capture session ID if it's in the response
265
- if (response.session_id && !capturedSessionId) {
266
- capturedSessionId = response.session_id;
267
- console.log('📝 Captured session ID:', capturedSessionId);
268
-
269
- // Update process key with captured session ID
270
- if (processKey !== capturedSessionId) {
271
- activeClaudeProcesses.delete(processKey);
272
- activeClaudeProcesses.set(capturedSessionId, claudeProcess);
273
- }
274
-
275
- // Send session-created event only once for new sessions
276
- if (!sessionId && !sessionCreatedSent) {
277
- sessionCreatedSent = true;
278
- ws.send(JSON.stringify({
279
- type: 'session-created',
280
- sessionId: capturedSessionId
281
- }));
282
- }
283
- }
284
-
285
- // Send parsed response to WebSocket
286
- ws.send(JSON.stringify({
287
- type: 'claude-response',
288
- data: response
289
- }));
290
- } catch (parseError) {
291
- console.log('📄 Non-JSON response:', line);
292
- // If not JSON, send as raw text
293
- ws.send(JSON.stringify({
294
- type: 'claude-output',
295
- data: line
296
- }));
297
- }
298
- }
299
- });
300
-
301
- // Handle stderr
302
- claudeProcess.stderr.on('data', (data) => {
303
- console.error('Claude CLI stderr:', data.toString());
304
- ws.send(JSON.stringify({
305
- type: 'claude-error',
306
- error: data.toString()
307
- }));
308
- });
309
-
310
- // Handle process completion
311
- claudeProcess.on('close', async (code) => {
312
- console.log(`Claude CLI process exited with code ${code}`);
313
-
314
- // Clean up process reference
315
- const finalSessionId = capturedSessionId || sessionId || processKey;
316
- activeClaudeProcesses.delete(finalSessionId);
317
-
318
- ws.send(JSON.stringify({
319
- type: 'claude-complete',
320
- exitCode: code,
321
- isNewSession: !sessionId && !!command // Flag to indicate this was a new session
322
- }));
323
-
324
- // Clean up temporary image files if any
325
- if (claudeProcess.tempImagePaths && claudeProcess.tempImagePaths.length > 0) {
326
- for (const imagePath of claudeProcess.tempImagePaths) {
327
- await fs.unlink(imagePath).catch(err =>
328
- console.error(`Failed to delete temp image ${imagePath}:`, err)
329
- );
330
- }
331
- if (claudeProcess.tempDir) {
332
- await fs.rm(claudeProcess.tempDir, { recursive: true, force: true }).catch(err =>
333
- console.error(`Failed to delete temp directory ${claudeProcess.tempDir}:`, err)
334
- );
335
- }
336
- }
337
-
338
- if (code === 0) {
339
- resolve();
340
- } else {
341
- reject(new Error(`Claude CLI exited with code ${code}`));
342
- }
343
- });
344
-
345
- // Handle process errors
346
- claudeProcess.on('error', (error) => {
347
- console.error('Claude CLI process error:', error);
348
-
349
- // Clean up process reference on error
350
- const finalSessionId = capturedSessionId || sessionId || processKey;
351
- activeClaudeProcesses.delete(finalSessionId);
352
-
353
- ws.send(JSON.stringify({
354
- type: 'claude-error',
355
- error: error.message
356
- }));
357
-
358
- reject(error);
359
- });
360
-
361
- // Handle stdin for interactive mode
362
- if (command) {
363
- // For --print mode with arguments, we don't need to write to stdin
364
- claudeProcess.stdin.end();
365
- } else {
366
- // For interactive mode, we need to write the command to stdin if provided later
367
- // Keep stdin open for interactive session
368
- if (command !== undefined) {
369
- claudeProcess.stdin.write(command + '\n');
370
- claudeProcess.stdin.end();
371
- }
372
- // If no command provided, stdin stays open for interactive use
373
- }
374
- });
375
- }
376
-
377
- function abortClaudeSession(sessionId) {
378
- const process = activeClaudeProcesses.get(sessionId);
379
- if (process) {
380
- console.log(`🛑 Aborting Claude session: ${sessionId}`);
381
- process.kill('SIGTERM');
382
- activeClaudeProcesses.delete(sessionId);
383
- return true;
384
- }
385
- return false;
386
- }
387
-
388
- export {
389
- spawnClaude,
390
- abortClaudeSession
391
- };