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