claude-code-templates
Version:
CLI tool to setup Claude Code configurations with framework-specific commands, automation hooks and MCP Servers for your projects
258 lines (227 loc) • 8.93 kB
JavaScript
const { exec } = require('child_process');
const fs = require('fs-extra');
const path = require('path');
/**
* ProcessDetector - Handles Claude CLI process detection and conversation matching
* Extracted from monolithic analytics.js for better maintainability
*/
class ProcessDetector {
constructor() {
// Cache for process detection to avoid repeated shell commands
this.processCache = {
data: null,
timestamp: 0,
ttl: 500 // 500ms cache
};
}
/**
* Detect running Claude CLI processes
* @returns {Promise<Array>} Array of active Claude processes
*/
async detectRunningClaudeProcesses() {
// Check cache first
const now = Date.now();
if (this.processCache.data && (now - this.processCache.timestamp) < this.processCache.ttl) {
return this.processCache.data;
}
return new Promise((resolve) => {
// Search for processes containing 'claude' but exclude our own analytics process and system processes
exec('ps aux | grep -i claude | grep -v grep | grep -v analytics | grep -v "/Applications/Claude.app" | grep -v "npm start" | grep -v chats-mobile', (error, stdout) => {
if (error) {
resolve([]);
return;
}
console.log('🔍 Raw Claude processes output:', stdout); // Debug output
const processes = stdout.split('\n')
.filter(line => line.trim())
.filter(line => {
// More flexible Claude CLI process detection
const fullCommand = line.split(/\s+/).slice(10).join(' ');
const isClaudeProcess = (
fullCommand.includes('claude') &&
!fullCommand.includes('chrome_crashpad_handler') &&
!fullCommand.includes('create-claude-config') &&
!fullCommand.includes('chats-mobile') &&
!fullCommand.includes('analytics') &&
// Allow various Claude CLI invocations
(fullCommand.trim() === 'claude' ||
fullCommand.includes('claude --') ||
fullCommand.includes('claude ') ||
fullCommand.includes('/claude') ||
fullCommand.includes('bin/claude'))
);
if (isClaudeProcess) {
console.log('✅ Found Claude process:', fullCommand);
}
return isClaudeProcess;
})
.map(line => {
const parts = line.split(/\s+/);
const fullCommand = parts.slice(10).join(' ');
// Extract useful information from command
const cwdMatch = fullCommand.match(/--cwd[=\s]+([^\s]+)/);
let workingDir = cwdMatch ? cwdMatch[1] : 'unknown';
// Skip pwdx for now since it doesn't exist on macOS
return {
pid: parts[1],
command: fullCommand,
workingDir: workingDir,
startTime: new Date(), // For now we use current time
status: 'running',
user: parts[0]
};
});
// Cache the result
this.processCache = {
data: processes,
timestamp: now,
ttl: 500
};
resolve(processes);
});
});
}
/**
* Enrich conversation data with running process information
* @param {Array} conversations - Array of conversation objects
* @param {string} claudeDir - Path to Claude directory for file operations
* @param {Object} stateCalculator - StateCalculator instance for state calculations
* @returns {Promise<Object>} Object with enriched conversations and orphan processes
*/
async enrichWithRunningProcesses(conversations, claudeDir, stateCalculator) {
try {
const runningProcesses = await this.detectRunningClaudeProcesses();
// Add active process information to each conversation
for (const conversation of conversations) {
// Look for active process for this project
// If workingDir is unknown, match with the most recently modified conversation
let matchingProcess = runningProcesses.find(process =>
process.workingDir.includes(conversation.project) ||
process.command.includes(conversation.project)
);
// Fallback: if no direct match and workingDir is unknown,
// assume the most recently modified conversation belongs to the active process
if (!matchingProcess && runningProcesses.length > 0 && runningProcesses[0].workingDir === 'unknown') {
// Find the most recently modified conversation
const sortedConversations = [...conversations].sort((a, b) =>
new Date(b.lastModified) - new Date(a.lastModified)
);
if (conversation === sortedConversations[0]) {
matchingProcess = runningProcesses[0];
}
}
if (matchingProcess) {
// ENRICH without changing existing logic
conversation.runningProcess = {
pid: matchingProcess.pid,
startTime: matchingProcess.startTime,
workingDir: matchingProcess.workingDir,
hasActiveCommand: true
};
// Only change status if not already marked as active by existing logic
if (conversation.status !== 'active') {
conversation.status = 'active';
conversation.statusReason = 'running_process';
}
// Recalculate conversation state with process information
const conversationFile = path.join(claudeDir, conversation.fileName);
try {
const content = await fs.readFile(conversationFile, 'utf8');
const parsedMessages = content.split('\n')
.filter(line => line.trim())
.map(line => JSON.parse(line));
const stats = await fs.stat(conversationFile);
conversation.conversationState = stateCalculator.determineConversationState(
parsedMessages,
stats.mtime,
conversation.runningProcess
);
} catch (error) {
// If we can't read the file, keep the existing state
}
} else {
conversation.runningProcess = null;
}
}
// Disable orphan process detection to reduce noise
const orphanProcesses = [];
return {
conversations,
orphanProcesses,
activeProcessCount: runningProcesses.length
};
} catch (error) {
// Silently handle process detection errors
return {
conversations,
orphanProcesses: [],
activeProcessCount: 0
};
}
}
/**
* Get cached processes without triggering detection
* @returns {Array} Cached process data or empty array
*/
getCachedProcesses() {
const now = Date.now();
if (this.processCache.data && (now - this.processCache.timestamp) < this.processCache.ttl) {
return this.processCache.data;
}
return [];
}
/**
* Clear process cache to force fresh detection
*/
clearCache() {
this.processCache = {
data: null,
timestamp: 0,
ttl: 500
};
}
/**
* Check if there are any active Claude processes
* @returns {Promise<boolean>} True if there are active processes
*/
async hasActiveProcesses() {
const processes = await this.detectRunningClaudeProcesses();
return processes.length > 0;
}
/**
* Get process statistics
* @returns {Promise<Object>} Process statistics
*/
async getProcessStats() {
const processes = await this.detectRunningClaudeProcesses();
return {
total: processes.length,
withKnownWorkingDir: processes.filter(p => p.workingDir !== 'unknown').length,
withUnknownWorkingDir: processes.filter(p => p.workingDir === 'unknown').length,
processes: processes
};
}
/**
* Match a specific process to conversations
* @param {Object} process - Process object
* @param {Array} conversations - Array of conversation objects
* @returns {Object|null} Matched conversation or null
*/
matchProcessToConversation(process, conversations) {
// Direct match by working directory or project name
let match = conversations.find(conv =>
process.workingDir.includes(conv.project) ||
process.command.includes(conv.project)
);
// Fallback for unknown working directories
if (!match && process.workingDir === 'unknown' && conversations.length > 0) {
// Match to most recently modified conversation
const sorted = [...conversations].sort((a, b) =>
new Date(b.lastModified) - new Date(a.lastModified)
);
match = sorted[0];
}
return match;
}
}
module.exports = ProcessDetector;