claude-code-templates 1.10.1 → 1.12.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,610 @@
1
+ const chalk = require('chalk');
2
+ const { spawn, exec } = require('child_process');
3
+ const WebSocket = require('ws');
4
+ const EventEmitter = require('events');
5
+ const fs = require('fs');
6
+
7
+ /**
8
+ * ConsoleBridge - Bridges Claude Code console interactions with WebSocket
9
+ * Intercepts stdin/stdout to enable web-based interactions
10
+ */
11
+ class ConsoleBridge extends EventEmitter {
12
+ constructor(options = {}) {
13
+ super();
14
+ this.options = {
15
+ port: options.port || 3334,
16
+ debug: options.debug || false,
17
+ ...options
18
+ };
19
+
20
+ this.wss = null;
21
+ this.clients = new Set();
22
+ this.currentInteraction = null;
23
+ this.interactionQueue = [];
24
+ this.isProcessingInteraction = false;
25
+
26
+ // Pattern recognition for Claude Code prompts
27
+ this.promptPatterns = [
28
+ /Do you want to proceed\?/,
29
+ /\s*❯\s*1\.\s*Yes/,
30
+ /\s*2\.\s*Yes,\s*and\s*(add|don't ask)/,
31
+ /\s*3\.\s*No,\s*and\s*tell\s*Claude/,
32
+ /Choose an option \(1-\d+\):/,
33
+ /Enter your choice:/,
34
+ /Please provide input:/
35
+ ];
36
+
37
+ // Track multi-line prompt building
38
+ this.promptBuffer = [];
39
+ this.isCapturingPrompt = false;
40
+ this.promptTimeout = null;
41
+ }
42
+
43
+ /**
44
+ * Initialize the console bridge
45
+ */
46
+ async initialize() {
47
+ console.log(chalk.blue('🌉 Initializing Console Bridge...'));
48
+
49
+ try {
50
+ await this.setupWebSocketServer();
51
+ this.setupProcessMonitoring();
52
+
53
+ console.log(chalk.green('✅ Console Bridge initialized successfully'));
54
+ console.log(chalk.cyan(`🔌 WebSocket server running on port ${this.options.port}`));
55
+
56
+ return true;
57
+ } catch (error) {
58
+ console.error(chalk.red('❌ Failed to initialize Console Bridge:'), error);
59
+ return false;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Setup WebSocket server for web interface communication
65
+ */
66
+ async setupWebSocketServer() {
67
+ return new Promise((resolve, reject) => {
68
+ this.wss = new WebSocket.Server({
69
+ port: this.options.port,
70
+ host: 'localhost'
71
+ });
72
+
73
+ this.wss.on('connection', (ws) => {
74
+ console.log(chalk.blue('🔌 Web interface connected to Console Bridge'));
75
+ this.clients.add(ws);
76
+
77
+ // Send current interaction if any
78
+ if (this.currentInteraction) {
79
+ ws.send(JSON.stringify({
80
+ type: 'console_interaction',
81
+ data: this.currentInteraction
82
+ }));
83
+ }
84
+
85
+ ws.on('message', (message) => {
86
+ try {
87
+ const data = JSON.parse(message);
88
+ this.handleWebMessage(data);
89
+ } catch (error) {
90
+ console.error(chalk.red('❌ Invalid message from web interface:'), error);
91
+ }
92
+ });
93
+
94
+ ws.on('close', () => {
95
+ console.log(chalk.yellow('🔌 Web interface disconnected from Console Bridge'));
96
+ this.clients.delete(ws);
97
+ });
98
+
99
+ ws.on('error', (error) => {
100
+ console.error(chalk.red('❌ WebSocket error:'), error);
101
+ this.clients.delete(ws);
102
+ });
103
+ });
104
+
105
+ this.wss.on('listening', resolve);
106
+ this.wss.on('error', reject);
107
+ });
108
+ }
109
+
110
+ /**
111
+ * Setup monitoring of Claude Code processes
112
+ */
113
+ setupProcessMonitoring() {
114
+ // Monitor for new Claude Code processes
115
+ setInterval(() => {
116
+ this.scanForClaudeProcesses();
117
+ }, 5000);
118
+
119
+ // Initial scan
120
+ this.scanForClaudeProcesses();
121
+ }
122
+
123
+ /**
124
+ * Scan for running Claude Code processes
125
+ */
126
+ async scanForClaudeProcesses() {
127
+ try {
128
+ const { exec } = require('child_process');
129
+
130
+ exec('ps aux | grep -E "claude[^-]|Claude" | grep -v grep', (error, stdout) => {
131
+ if (error) return;
132
+
133
+ const processes = stdout.split('\n')
134
+ .filter(line => line.trim())
135
+ .map(line => {
136
+ const parts = line.trim().split(/\s+/);
137
+ return {
138
+ pid: parts[1],
139
+ command: parts.slice(10).join(' '),
140
+ user: parts[0]
141
+ };
142
+ })
143
+ .filter(proc =>
144
+ proc.command.includes('claude') &&
145
+ !proc.command.includes('claude-code-templates') &&
146
+ !proc.command.includes('grep')
147
+ );
148
+
149
+ if (processes.length > 0) {
150
+ this.debug('Found Claude processes:', processes);
151
+
152
+ // Attempt to attach to the most likely Claude Code process
153
+ const claudeProcess = processes.find(p =>
154
+ p.command.includes('claude') &&
155
+ !p.command.includes('Helper') &&
156
+ !p.command.includes('.app')
157
+ );
158
+
159
+ if (claudeProcess && claudeProcess.pid !== this.attachedPid) {
160
+ this.attemptProcessAttachment(claudeProcess.pid);
161
+ }
162
+ }
163
+ });
164
+ } catch (error) {
165
+ this.debug('Error scanning for Claude processes:', error);
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Attempt to attach to a Claude Code process
171
+ */
172
+ async attemptProcessAttachment(pid) {
173
+ try {
174
+ this.debug(`Attempting to attach to Claude process ${pid}`);
175
+
176
+ // Get terminal device for this process
177
+ const terminalInfo = await this.getProcessTerminal(pid);
178
+ if (!terminalInfo) {
179
+ console.warn(chalk.yellow(`⚠️ Could not determine terminal for process ${pid}`));
180
+ return;
181
+ }
182
+
183
+ this.attachedPid = pid;
184
+ this.terminalDevice = terminalInfo.tty;
185
+
186
+ console.log(chalk.green(`✅ Attached to Claude Code process ${pid} on ${terminalInfo.tty}`));
187
+
188
+ // Start monitoring terminal output
189
+ await this.startTerminalMonitoring(terminalInfo.tty);
190
+
191
+ // For development, also simulate some prompts
192
+ this.simulatePromptDetection();
193
+
194
+ } catch (error) {
195
+ console.error(chalk.red(`❌ Failed to attach to process ${pid}:`), error);
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Get terminal device information for a process
201
+ * @param {string} pid - Process ID
202
+ * @returns {Promise<Object|null>} Terminal information
203
+ */
204
+ async getProcessTerminal(pid) {
205
+ return new Promise((resolve) => {
206
+ exec(`lsof -p ${pid} | grep -E "(tty|pts)" | head -1`, (error, stdout) => {
207
+ if (error || !stdout.trim()) {
208
+ resolve(null);
209
+ return;
210
+ }
211
+
212
+ const parts = stdout.trim().split(/\s+/);
213
+ const ttyPath = parts[parts.length - 1]; // Last part is the device path
214
+
215
+ if (ttyPath.startsWith('/dev/')) {
216
+ resolve({
217
+ tty: ttyPath,
218
+ pid: pid
219
+ });
220
+ } else {
221
+ resolve(null);
222
+ }
223
+ });
224
+ });
225
+ }
226
+
227
+ /**
228
+ * Start monitoring terminal output for prompts
229
+ * @param {string} ttyPath - Path to terminal device
230
+ */
231
+ async startTerminalMonitoring(ttyPath) {
232
+ try {
233
+ console.log(chalk.blue(`📡 Starting terminal monitoring for ${ttyPath}`));
234
+
235
+ // Use script command to capture terminal output
236
+ // This creates a typescript of the terminal session
237
+ const logFile = `/tmp/claude-terminal-${this.attachedPid}.log`;
238
+
239
+ // Monitor using tail -f approach on the terminal device (if readable)
240
+ this.startTerminalPolling(ttyPath, logFile);
241
+
242
+ } catch (error) {
243
+ console.error(chalk.red('❌ Error starting terminal monitoring:'), error);
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Poll terminal for new output
249
+ * @param {string} ttyPath - Terminal device path
250
+ * @param {string} logFile - Log file path
251
+ */
252
+ startTerminalPolling(ttyPath, logFile) {
253
+ // Try to read directly from terminal device (may need permissions)
254
+ if (this.terminalPollingInterval) {
255
+ clearInterval(this.terminalPollingInterval);
256
+ }
257
+
258
+ let lastPosition = 0;
259
+ let outputBuffer = '';
260
+
261
+ this.terminalPollingInterval = setInterval(async () => {
262
+ try {
263
+ // Try to read the screen content using a more accessible approach
264
+ // Use script to capture current terminal state
265
+ exec(`script -q /dev/null tail -n 50 ${ttyPath}`, { timeout: 1000 }, (error, stdout, stderr) => {
266
+ if (!error && stdout) {
267
+ // Look for prompt patterns in the output
268
+ this.analyzeTerminalOutput(stdout);
269
+ }
270
+ });
271
+
272
+ // Alternative: monitor process output through ps
273
+ this.monitorProcessStatus();
274
+
275
+ } catch (error) {
276
+ this.debug('Terminal polling error:', error.message);
277
+ }
278
+ }, 2000); // Check every 2 seconds
279
+ }
280
+
281
+ /**
282
+ * Monitor the Claude Code process status for changes
283
+ */
284
+ monitorProcessStatus() {
285
+ if (!this.attachedPid) return;
286
+
287
+ exec(`ps -p ${this.attachedPid} -o state,time,command`, (error, stdout) => {
288
+ if (error) {
289
+ // Process might have ended
290
+ console.log(chalk.yellow('🔄 Monitored Claude Code process ended'));
291
+ this.attachedPid = null;
292
+ return;
293
+ }
294
+
295
+ const lines = stdout.trim().split('\n');
296
+ if (lines.length > 1) {
297
+ const processLine = lines[1];
298
+ const state = processLine.split(/\s+/)[0];
299
+
300
+ // Check if process is waiting for input (state: T, S+)
301
+ if (state.includes('T') || state.includes('S+')) {
302
+ this.debug('Process appears to be waiting for input');
303
+ // This might indicate a prompt is active
304
+ }
305
+ }
306
+ });
307
+ }
308
+
309
+ /**
310
+ * Analyze terminal output for prompts
311
+ * @param {string} output - Terminal output to analyze
312
+ */
313
+ analyzeTerminalOutput(output) {
314
+ const lines = output.split('\n');
315
+ const recentLines = lines.slice(-10); // Last 10 lines
316
+ const fullText = recentLines.join('\n');
317
+
318
+ // Check for Claude Code prompt patterns
319
+ for (const pattern of this.promptPatterns) {
320
+ if (pattern.test(fullText)) {
321
+ console.log(chalk.yellow('🎯 Potential prompt detected in terminal output!'));
322
+ console.log(chalk.gray('Lines:', recentLines.slice(-5).join(' | ')));
323
+
324
+ // Try to extract and parse the prompt
325
+ this.handleDetectedPrompt(fullText);
326
+ break;
327
+ }
328
+ }
329
+ }
330
+
331
+ /**
332
+ * Simulate prompt detection (placeholder for actual implementation)
333
+ * In reality, this would intercept actual Claude Code output
334
+ */
335
+ simulatePromptDetection() {
336
+ // This is a simulation - in reality we'd parse actual output
337
+ setTimeout(() => {
338
+ if (Math.random() > 0.7) { // Simulate occasional prompts
339
+ this.handleDetectedPrompt(`Read file
340
+
341
+ Search(pattern: "(?:Yes|No|yes|no)(?:,\\s*and\\s*don't\\s*ask\\s*again)?", path:
342
+ "../../../../../../../.claude/projects/-Users-danipower-Proyectos-Github-claude-code-templates", include: "*.jsonl")
343
+
344
+ Do you want to proceed?
345
+ ❯ 1. Yes
346
+ 2. Yes, and add /Users/danipower/.claude/projects/-Users-danipower-Proyectos-Github-claude-code-templates as a working directory for this session
347
+ 3. No, and tell Claude what to do differently (esc)`);
348
+ }
349
+
350
+ // Continue simulation
351
+ setTimeout(() => this.simulatePromptDetection(), 10000 + Math.random() * 20000);
352
+ }, 5000);
353
+ }
354
+
355
+ /**
356
+ * Handle detected prompt from Claude Code
357
+ */
358
+ handleDetectedPrompt(promptText) {
359
+ //console.log(chalk.yellow('🤖 Claude Code prompt detected:'));
360
+ //console.log(chalk.gray(promptText));
361
+
362
+ const interaction = this.parsePrompt(promptText);
363
+
364
+ if (interaction) {
365
+ this.currentInteraction = {
366
+ ...interaction,
367
+ id: 'claude-prompt-' + Date.now(),
368
+ timestamp: new Date().toISOString()
369
+ };
370
+
371
+ // Send to web interface
372
+ this.broadcastToClients({
373
+ type: 'console_interaction',
374
+ data: this.currentInteraction
375
+ });
376
+
377
+ //console.log(chalk.blue('📡 Sent prompt to web interface'));
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Parse Claude Code prompt text into structured interaction
383
+ */
384
+ parsePrompt(promptText) {
385
+ const lines = promptText.split('\n').map(line => line.trim());
386
+
387
+ // Look for the main prompt question
388
+ const promptLine = lines.find(line =>
389
+ line.includes('Do you want to proceed?') ||
390
+ line.includes('Choose an option') ||
391
+ line.includes('Enter your choice') ||
392
+ line.includes('Please provide input')
393
+ );
394
+
395
+ if (!promptLine) return null;
396
+
397
+ // Look for numbered options
398
+ const optionLines = lines.filter(line =>
399
+ /^\s*❯?\s*\d+\.\s*/.test(line) ||
400
+ /^\s*\d+\.\s*/.test(line)
401
+ );
402
+
403
+ if (optionLines.length > 0) {
404
+ // Choice-based prompt
405
+ const options = optionLines.map(line =>
406
+ line.replace(/^\s*❯?\s*\d+\.\s*/, '').trim()
407
+ );
408
+
409
+ // Extract tool description (usually the first few lines)
410
+ const descriptionLines = lines.slice(0, lines.indexOf(lines.find(l => l.includes('Do you want'))) || 3);
411
+ const description = descriptionLines.join('\n').trim();
412
+
413
+ return {
414
+ type: 'choice',
415
+ tool: this.extractToolName(description),
416
+ description,
417
+ prompt: promptLine,
418
+ options
419
+ };
420
+ } else {
421
+ // Text input prompt
422
+ return {
423
+ type: 'text',
424
+ tool: 'Console Input',
425
+ description: lines.slice(0, -1).join('\n').trim(),
426
+ prompt: promptLine
427
+ };
428
+ }
429
+ }
430
+
431
+ /**
432
+ * Extract tool name from description
433
+ */
434
+ extractToolName(description) {
435
+ const toolMatch = description.match(/^([A-Za-z]+)(\(|$)/);
436
+ return toolMatch ? toolMatch[1] : 'Tool';
437
+ }
438
+
439
+ /**
440
+ * Handle message from web interface
441
+ */
442
+ handleWebMessage(data) {
443
+ if (data.type === 'console_response' && this.currentInteraction) {
444
+ console.log(chalk.green('📱 Received response from web interface:'), data.data);
445
+
446
+ // In a real implementation, this would send the response to Claude Code
447
+ this.sendResponseToClaudeCode(data.data);
448
+
449
+ this.currentInteraction = null;
450
+ }
451
+ }
452
+
453
+ /**
454
+ * Send response back to Claude Code process
455
+ * This attempts to write directly to the terminal device
456
+ */
457
+ sendResponseToClaudeCode(response) {
458
+ console.log(chalk.blue('🔄 Sending response to Claude Code...'));
459
+
460
+ if (!this.attachedPid || !this.terminalDevice) {
461
+ console.warn(chalk.yellow('⚠️ No attached process - falling back to simulation'));
462
+ this.simulateResponse(response);
463
+ return;
464
+ }
465
+
466
+ if (response.type === 'choice') {
467
+ // Send the choice number (1-indexed)
468
+ const choiceNumber = response.value + 1;
469
+ console.log(chalk.green(`✅ Choice selected: ${choiceNumber} - ${response.text}`));
470
+
471
+ this.writeToTerminal(choiceNumber.toString() + '\n');
472
+
473
+ } else if (response.type === 'text') {
474
+ console.log(chalk.green(`✅ Text input: "${response.value}"`));
475
+
476
+ this.writeToTerminal(response.value + '\n');
477
+
478
+ } else if (response.type === 'cancel') {
479
+ console.log(chalk.yellow('🚫 User cancelled interaction'));
480
+
481
+ // Send ESC key or Ctrl+C
482
+ this.writeToTerminal('\x1b'); // ESC key
483
+ }
484
+ }
485
+
486
+ /**
487
+ * Write text to the terminal device
488
+ * @param {string} text - Text to write
489
+ */
490
+ writeToTerminal(text) {
491
+ if (!this.terminalDevice) {
492
+ console.warn(chalk.yellow('⚠️ No terminal device available'));
493
+ return;
494
+ }
495
+
496
+ try {
497
+ // Try different approaches to send input to the terminal
498
+
499
+ // Method 1: Use expect script to send input
500
+ const expectScript = `
501
+ spawn -open [open ${this.terminalDevice} w]
502
+ send "${text.replace(/"/g, '\\"')}"
503
+ close
504
+ `;
505
+
506
+ exec(`expect -c '${expectScript}'`, (error, stdout, stderr) => {
507
+ if (error) {
508
+ console.log(chalk.yellow('⚠️ Expect method failed, trying alternative...'));
509
+ this.tryAlternativeInput(text);
510
+ } else {
511
+ console.log(chalk.green('✅ Input sent via expect'));
512
+ }
513
+ });
514
+
515
+ } catch (error) {
516
+ console.error(chalk.red('❌ Error writing to terminal:'), error);
517
+ this.tryAlternativeInput(text);
518
+ }
519
+ }
520
+
521
+ /**
522
+ * Try alternative method to send input
523
+ * @param {string} text - Text to send
524
+ */
525
+ tryAlternativeInput(text) {
526
+ // Method 2: Try using osascript (AppleScript on macOS) to send keystrokes
527
+ if (process.platform === 'darwin') {
528
+ const script = `
529
+ tell application "Terminal"
530
+ do script "${text.replace(/"/g, '\\"').replace(/\n/g, '\\n')}" in front window
531
+ end tell
532
+ `;
533
+
534
+ exec(`osascript -e '${script}'`, (error) => {
535
+ if (error) {
536
+ console.log(chalk.yellow('⚠️ AppleScript method failed, falling back to simulation'));
537
+ this.simulateResponse({ type: 'choice', value: parseInt(text) - 1, text: text.trim() });
538
+ } else {
539
+ console.log(chalk.green('✅ Input sent via AppleScript'));
540
+ }
541
+ });
542
+ } else {
543
+ // On Linux, try using xdotool or similar
544
+ console.log(chalk.yellow('⚠️ Non-macOS platform - input simulation not implemented'));
545
+ this.simulateResponse({ type: 'choice', value: parseInt(text) - 1, text: text.trim() });
546
+ }
547
+ }
548
+
549
+ /**
550
+ * Simulate response (fallback when real interaction fails)
551
+ * @param {Object} response - Response object
552
+ */
553
+ simulateResponse(response) {
554
+ if (response.type === 'choice') {
555
+ const choiceNumber = response.value + 1;
556
+ console.log(chalk.gray(`[Simulated] Sending "${choiceNumber}" to Claude Code stdin`));
557
+ } else if (response.type === 'text') {
558
+ console.log(chalk.gray(`[Simulated] Sending "${response.value}" to Claude Code stdin`));
559
+ } else if (response.type === 'cancel') {
560
+ console.log(chalk.gray('[Simulated] Sending ESC to Claude Code stdin'));
561
+ }
562
+ }
563
+
564
+ /**
565
+ * Broadcast message to all connected web clients
566
+ */
567
+ broadcastToClients(message) {
568
+ const messageStr = JSON.stringify(message);
569
+
570
+ this.clients.forEach(client => {
571
+ if (client.readyState === WebSocket.OPEN) {
572
+ client.send(messageStr);
573
+ }
574
+ });
575
+ }
576
+
577
+ /**
578
+ * Debug logging
579
+ */
580
+ debug(...args) {
581
+ if (this.options.debug) {
582
+ console.log(chalk.gray('[ConsoleBridge Debug]'), ...args);
583
+ }
584
+ }
585
+
586
+ /**
587
+ * Cleanup and shutdown
588
+ */
589
+ async shutdown() {
590
+ console.log(chalk.yellow('🛑 Shutting down Console Bridge...'));
591
+
592
+ // Stop terminal monitoring
593
+ if (this.terminalPollingInterval) {
594
+ clearInterval(this.terminalPollingInterval);
595
+ this.terminalPollingInterval = null;
596
+ }
597
+
598
+ if (this.wss) {
599
+ this.wss.close();
600
+ }
601
+
602
+ this.clients.clear();
603
+ this.attachedPid = null;
604
+ this.terminalDevice = null;
605
+
606
+ console.log(chalk.green('✅ Console Bridge shutdown complete'));
607
+ }
608
+ }
609
+
610
+ module.exports = ConsoleBridge;