UNPKG

ccoptimizer

Version:

🚀 Optimize your Claude Code experience by analyzing your conversation patterns

175 lines (172 loc) • 6.5 kB
import { readdirSync, statSync } from 'fs'; import { join } from 'path'; import { createReadStream } from 'fs'; import { createInterface } from 'readline'; import { spawn } from 'child_process'; export async function analyzeConversations(projectsPath, spinner) { const conversations = await extractAllConversations(projectsPath, spinner); // Count unique projects const uniqueProjects = new Set(conversations.map(c => c.project)).size; spinner.text = `Found ${conversations.length} chats from ${uniqueProjects} projects`; // Just collect all insights as strings const allInsights = []; // Process first 20 conversations (or all if less) const toAnalyze = conversations.slice(0, Math.min(20, conversations.length)); for (const [index, conversation] of toAnalyze.entries()) { const projectName = conversation.project.split('-').pop(); spinner.text = `Analyzing conversation ${index + 1}/${toAnalyze.length} from ${projectName}...`; const insight = await analyzeWithClaude(conversation); if (insight && insight.length > 0) { allInsights.push(insight); // Show a quick preview of what we found const firstRule = insight.split('\n')[0]; if (firstRule) { spinner.text = `Found: ${firstRule.substring(0, 50)}...`; } } } // Join all insights into one big string const combinedInsights = allInsights.join('\n\n'); return { projectCount: conversations.length, conversationCount: toAnalyze.length, insights: combinedInsights }; } async function extractAllConversations(projectsPath, spinner) { const conversations = []; const projects = readdirSync(projectsPath).filter(dir => statSync(join(projectsPath, dir)).isDirectory()); for (const project of projects) { const projectPath = join(projectsPath, project); const jsonlFiles = readdirSync(projectPath).filter(file => file.endsWith('.jsonl')); for (const file of jsonlFiles) { spinner.text = `Reading ${project}/${file}...`; const conversation = await extractConversation(join(projectPath, file)); if (conversation && conversation.messages.length > 0) { conversations.push({ project, file, messages: conversation.messages, metadata: conversation.metadata }); } } } return conversations; } async function extractConversation(filePath) { const messages = []; const metadata = {}; const fileStream = createReadStream(filePath); const rl = createInterface({ input: fileStream, crlfDelay: Infinity }); for await (const line of rl) { try { const entry = JSON.parse(line); if (entry.type === 'user' && entry.message?.content) { messages.push({ role: 'user', content: entry.message.content, timestamp: entry.timestamp }); } if (entry.type === 'assistant' && entry.message?.content) { let assistantMessage = ''; for (const item of entry.message.content) { if (item.type === 'text') { assistantMessage += item.text; } else if (item.type === 'tool_use') { assistantMessage += `[Used tool: ${item.name}]`; } } if (assistantMessage) { messages.push({ role: 'assistant', content: assistantMessage, timestamp: entry.timestamp }); } } if (entry.cwd && !metadata.cwd) { metadata.cwd = entry.cwd; } } catch (e) { // Skip malformed lines } } return { messages, metadata }; } async function analyzeWithClaude(conversation) { if (conversation.messages.length < 2) { return ""; } // Extract only USER messages to reduce tokens const userMessages = conversation.messages .filter(m => m.role === 'user') .map(m => { if (typeof m.content === 'string') { return m.content.length > 500 ? m.content.substring(0, 500) + '...' : m.content; } return ''; }) .filter(m => m.length > 0) .join('\n'); if (!userMessages) { return ""; } const prompt = ` <task> Analyze these user messages to catch the essence of how the user communicates and what they want. The goal is to distill their preferences into clear guidelines for interaction. </task> <user_messages> ${userMessages} </user_messages> <instructions> Focus on their communication style, technical preferences, and what frustrates them. </instructions> <examples> - Always ultrathink to maximize conciseness - Never add code comments about changes you made - Strive to solve the problem without writing more lines - When user swears, we need to take a step back and reassess the situation </examples>`; if (process.argv.includes('--dry-run')) { return "- Prefers brief responses\n- No code comments\n- Values simplicity"; } try { // Use spawn with stdin pipe - super simple return await new Promise((resolve) => { const claude = spawn('claude', ['-p']); let output = ''; claude.stdout.on('data', (data) => { output += data.toString(); }); // Write prompt to stdin and close it claude.stdin.write(prompt); claude.stdin.end(); // Set timeout const timeout = setTimeout(() => { claude.kill(); resolve(""); // Just return empty string on timeout }, 15000); claude.on('close', () => { clearTimeout(timeout); // Just return whatever we got as a string resolve(output || ""); }); claude.on('error', () => { clearTimeout(timeout); resolve(""); }); }); } catch (error) { return ""; // Return empty string on any error } } //# sourceMappingURL=analyzer.js.map