claudecto 0.1.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.
Files changed (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +275 -0
  3. package/dist/__tests__/package.test.d.ts +2 -0
  4. package/dist/__tests__/package.test.d.ts.map +1 -0
  5. package/dist/__tests__/package.test.js +53 -0
  6. package/dist/__tests__/package.test.js.map +1 -0
  7. package/dist/cli.d.ts +6 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +200 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/index.d.ts +12 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +12 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/server/index.d.ts +11 -0
  16. package/dist/server/index.d.ts.map +1 -0
  17. package/dist/server/index.js +1207 -0
  18. package/dist/server/index.js.map +1 -0
  19. package/dist/services/advisor.d.ts +117 -0
  20. package/dist/services/advisor.d.ts.map +1 -0
  21. package/dist/services/advisor.js +2636 -0
  22. package/dist/services/advisor.js.map +1 -0
  23. package/dist/services/agent-generator.d.ts +71 -0
  24. package/dist/services/agent-generator.d.ts.map +1 -0
  25. package/dist/services/agent-generator.js +295 -0
  26. package/dist/services/agent-generator.js.map +1 -0
  27. package/dist/services/agents.d.ts +67 -0
  28. package/dist/services/agents.d.ts.map +1 -0
  29. package/dist/services/agents.js +405 -0
  30. package/dist/services/agents.js.map +1 -0
  31. package/dist/services/analytics.d.ts +145 -0
  32. package/dist/services/analytics.d.ts.map +1 -0
  33. package/dist/services/analytics.js +609 -0
  34. package/dist/services/analytics.js.map +1 -0
  35. package/dist/services/blueprints.d.ts +31 -0
  36. package/dist/services/blueprints.d.ts.map +1 -0
  37. package/dist/services/blueprints.js +317 -0
  38. package/dist/services/blueprints.js.map +1 -0
  39. package/dist/services/claude-dir.d.ts +50 -0
  40. package/dist/services/claude-dir.d.ts.map +1 -0
  41. package/dist/services/claude-dir.js +193 -0
  42. package/dist/services/claude-dir.js.map +1 -0
  43. package/dist/services/hooks.d.ts +38 -0
  44. package/dist/services/hooks.d.ts.map +1 -0
  45. package/dist/services/hooks.js +165 -0
  46. package/dist/services/hooks.js.map +1 -0
  47. package/dist/services/insights.d.ts +52 -0
  48. package/dist/services/insights.d.ts.map +1 -0
  49. package/dist/services/insights.js +1035 -0
  50. package/dist/services/insights.js.map +1 -0
  51. package/dist/services/memory.d.ts +14 -0
  52. package/dist/services/memory.d.ts.map +1 -0
  53. package/dist/services/memory.js +25 -0
  54. package/dist/services/memory.js.map +1 -0
  55. package/dist/services/plans.d.ts +20 -0
  56. package/dist/services/plans.d.ts.map +1 -0
  57. package/dist/services/plans.js +149 -0
  58. package/dist/services/plans.js.map +1 -0
  59. package/dist/services/project-intelligence.d.ts +75 -0
  60. package/dist/services/project-intelligence.d.ts.map +1 -0
  61. package/dist/services/project-intelligence.js +731 -0
  62. package/dist/services/project-intelligence.js.map +1 -0
  63. package/dist/services/search.d.ts +32 -0
  64. package/dist/services/search.d.ts.map +1 -0
  65. package/dist/services/search.js +203 -0
  66. package/dist/services/search.js.map +1 -0
  67. package/dist/services/sessions.d.ts +25 -0
  68. package/dist/services/sessions.d.ts.map +1 -0
  69. package/dist/services/sessions.js +248 -0
  70. package/dist/services/sessions.js.map +1 -0
  71. package/dist/services/skills.d.ts +30 -0
  72. package/dist/services/skills.d.ts.map +1 -0
  73. package/dist/services/skills.js +197 -0
  74. package/dist/services/skills.js.map +1 -0
  75. package/dist/services/stats.d.ts +23 -0
  76. package/dist/services/stats.d.ts.map +1 -0
  77. package/dist/services/stats.js +88 -0
  78. package/dist/services/stats.js.map +1 -0
  79. package/dist/services/teams.d.ts +115 -0
  80. package/dist/services/teams.d.ts.map +1 -0
  81. package/dist/services/teams.js +421 -0
  82. package/dist/services/teams.js.map +1 -0
  83. package/dist/services/tech-stack.d.ts +98 -0
  84. package/dist/services/tech-stack.d.ts.map +1 -0
  85. package/dist/services/tech-stack.js +1088 -0
  86. package/dist/services/tech-stack.js.map +1 -0
  87. package/dist/services/terminal.d.ts +75 -0
  88. package/dist/services/terminal.d.ts.map +1 -0
  89. package/dist/services/terminal.js +224 -0
  90. package/dist/services/terminal.js.map +1 -0
  91. package/dist/types.d.ts +1095 -0
  92. package/dist/types.d.ts.map +1 -0
  93. package/dist/types.js +18 -0
  94. package/dist/types.js.map +1 -0
  95. package/dist/ui/assets/index-BiH4Nhdk.css +1 -0
  96. package/dist/ui/assets/index-Brv-K8bd.css +1 -0
  97. package/dist/ui/assets/index-BwMBEdQz.js +3108 -0
  98. package/dist/ui/assets/index-BwMBEdQz.js.map +1 -0
  99. package/dist/ui/assets/index-CEWz7ABD.js +3108 -0
  100. package/dist/ui/assets/index-CEWz7ABD.js.map +1 -0
  101. package/dist/ui/assets/index-CIZ3vvc-.css +1 -0
  102. package/dist/ui/assets/index-CsU3cI0n.js +3108 -0
  103. package/dist/ui/assets/index-CsU3cI0n.js.map +1 -0
  104. package/dist/ui/assets/index-D3AY6iCS.js +3133 -0
  105. package/dist/ui/assets/index-D3AY6iCS.js.map +1 -0
  106. package/dist/ui/assets/index-D8lNZ0Ye.css +1 -0
  107. package/dist/ui/assets/index-DmgeppSA.js +3108 -0
  108. package/dist/ui/assets/index-DmgeppSA.js.map +1 -0
  109. package/dist/ui/favicon.svg +43 -0
  110. package/dist/ui/index.html +23 -0
  111. package/dist/utils/jsonl.d.ts +16 -0
  112. package/dist/utils/jsonl.d.ts.map +1 -0
  113. package/dist/utils/jsonl.js +51 -0
  114. package/dist/utils/jsonl.js.map +1 -0
  115. package/package.json +106 -0
@@ -0,0 +1,1035 @@
1
+ /**
2
+ * Insights service - AI-powered session analysis using Claude Code
3
+ * Provides rich interactive insights including sentiment analysis,
4
+ * topic segmentation, quality scoring, and comprehensive metadata.
5
+ */
6
+ import path from 'node:path';
7
+ import fs from 'node:fs/promises';
8
+ import os from 'node:os';
9
+ import { exec } from 'node:child_process';
10
+ import { EventEmitter } from 'node:events';
11
+ const INSIGHT_VERSION = 2;
12
+ export class InsightsService extends EventEmitter {
13
+ claudeDir;
14
+ sessionService;
15
+ analyticsService;
16
+ generatingInsights = new Map();
17
+ progressMap = new Map();
18
+ constructor(claudeDir, sessionService, analyticsService) {
19
+ super();
20
+ this.claudeDir = claudeDir;
21
+ this.sessionService = sessionService;
22
+ this.analyticsService = analyticsService;
23
+ }
24
+ // --------------------------------------------------------------------------
25
+ // Progress & Error Helpers
26
+ // --------------------------------------------------------------------------
27
+ emitProgress(sessionId, stage, progress, message, error) {
28
+ const progressData = {
29
+ sessionId,
30
+ stage,
31
+ progress,
32
+ message,
33
+ startedAt: this.progressMap.get(sessionId)?.startedAt || new Date().toISOString(),
34
+ error,
35
+ };
36
+ this.progressMap.set(sessionId, progressData);
37
+ this.emit('progress', progressData);
38
+ }
39
+ getProgress(sessionId) {
40
+ return this.progressMap.get(sessionId) || null;
41
+ }
42
+ createAIError(code, message, recoverable, details, suggestion) {
43
+ return {
44
+ code,
45
+ message,
46
+ details,
47
+ recoverable,
48
+ suggestion,
49
+ };
50
+ }
51
+ mapErrorToAIError(error) {
52
+ const msg = error.message.toLowerCase();
53
+ if (msg.includes('timeout')) {
54
+ return this.createAIError('EXECUTION_TIMEOUT', 'Insight generation timed out', true, error.message, 'Try again with a shorter session or increase timeout');
55
+ }
56
+ if (msg.includes('not found') && msg.includes('claude')) {
57
+ return this.createAIError('CLAUDE_NOT_FOUND', 'Claude Code CLI not installed', false, error.message, 'Install Claude Code CLI: npm install -g @anthropic-ai/claude-code');
58
+ }
59
+ if (msg.includes('session not found')) {
60
+ return this.createAIError('SESSION_NOT_FOUND', 'Session not found', false, error.message);
61
+ }
62
+ if (msg.includes('parse') || msg.includes('json')) {
63
+ return this.createAIError('PARSE_ERROR', 'Failed to parse AI response', true, error.message, 'Try regenerating the insight');
64
+ }
65
+ if (msg.includes('rate') || msg.includes('limit')) {
66
+ return this.createAIError('RATE_LIMITED', 'Rate limited by Claude API', true, error.message, 'Wait a moment and try again');
67
+ }
68
+ return this.createAIError('UNKNOWN', 'An unexpected error occurred', true, error.message, 'Try again or check the logs for details');
69
+ }
70
+ // --------------------------------------------------------------------------
71
+ // File Paths
72
+ // --------------------------------------------------------------------------
73
+ getInsightPath(sessionId) {
74
+ return path.join(this.claudeDir.insightsDir, `${sessionId}.json`);
75
+ }
76
+ // --------------------------------------------------------------------------
77
+ // Read/Write Operations
78
+ // --------------------------------------------------------------------------
79
+ async getInsight(sessionId) {
80
+ const insightPath = this.getInsightPath(sessionId);
81
+ return this.claudeDir.readJSON(insightPath);
82
+ }
83
+ async saveInsight(insight) {
84
+ await this.claudeDir.ensureInsightsDir();
85
+ const insightPath = this.getInsightPath(insight.sessionId);
86
+ await this.claudeDir.writeJSON(insightPath, insight);
87
+ }
88
+ async listInsights(projectFilter) {
89
+ const insights = [];
90
+ try {
91
+ const insightsDir = this.claudeDir.insightsDir;
92
+ const fs = await import('node:fs/promises');
93
+ const entries = await fs.readdir(insightsDir, { withFileTypes: true });
94
+ for (const entry of entries) {
95
+ if (entry.isFile() && entry.name.endsWith('.json')) {
96
+ const insightPath = path.join(insightsDir, entry.name);
97
+ const insight = await this.claudeDir.readJSON(insightPath);
98
+ if (insight) {
99
+ if (projectFilter) {
100
+ const session = await this.sessionService.getSession(insight.sessionId);
101
+ if (session && session.projectPath !== projectFilter) {
102
+ continue;
103
+ }
104
+ }
105
+ insights.push(insight);
106
+ }
107
+ }
108
+ }
109
+ }
110
+ catch {
111
+ // Insights directory doesn't exist yet
112
+ }
113
+ return insights.sort((a, b) => new Date(b.generatedAt).getTime() - new Date(a.generatedAt).getTime());
114
+ }
115
+ // --------------------------------------------------------------------------
116
+ // Insight Generation
117
+ // --------------------------------------------------------------------------
118
+ async generateInsight(sessionId, force = false) {
119
+ const existing = this.generatingInsights.get(sessionId);
120
+ if (existing) {
121
+ return existing;
122
+ }
123
+ if (!force) {
124
+ const cached = await this.getInsight(sessionId);
125
+ if (cached && cached.status === 'completed') {
126
+ return cached;
127
+ }
128
+ }
129
+ const pendingInsight = {
130
+ sessionId,
131
+ version: INSIGHT_VERSION,
132
+ generatedAt: new Date().toISOString(),
133
+ status: 'generating',
134
+ };
135
+ await this.saveInsight(pendingInsight);
136
+ const generationPromise = this.doGenerateInsight(sessionId);
137
+ this.generatingInsights.set(sessionId, generationPromise);
138
+ try {
139
+ const result = await generationPromise;
140
+ return result;
141
+ }
142
+ finally {
143
+ this.generatingInsights.delete(sessionId);
144
+ }
145
+ }
146
+ async doGenerateInsight(sessionId) {
147
+ try {
148
+ // Stage 1: Starting
149
+ this.emitProgress(sessionId, 'starting', 0, 'Initializing insight generation...');
150
+ console.log(`[Insights] Starting insight generation for session: ${sessionId}`);
151
+ // Check if Claude is available first
152
+ const claudeAvailable = await this.isClaudeCodeAvailable();
153
+ if (!claudeAvailable) {
154
+ const error = this.createAIError('CLAUDE_NOT_FOUND', 'Claude Code CLI is not installed', false, undefined, 'Install Claude Code: npm install -g @anthropic-ai/claude-code');
155
+ this.emitProgress(sessionId, 'error', 0, 'Claude Code not found', error);
156
+ throw new Error('Claude Code CLI not found. Please install it first.');
157
+ }
158
+ // Stage 2: Loading session
159
+ this.emitProgress(sessionId, 'loading_session', 10, 'Loading session data...');
160
+ const session = await this.sessionService.getSession(sessionId);
161
+ if (!session) {
162
+ const error = this.createAIError('SESSION_NOT_FOUND', `Session ${sessionId} not found`, false);
163
+ this.emitProgress(sessionId, 'error', 10, 'Session not found', error);
164
+ throw new Error('Session not found');
165
+ }
166
+ console.log(`[Insights] Session loaded with ${session.messages?.length ?? 0} messages`);
167
+ // Stage 3: Loading analytics
168
+ this.emitProgress(sessionId, 'loading_analytics', 20, 'Loading session analytics...');
169
+ const projectDirs = await this.claudeDir.listProjects();
170
+ let analytics = null;
171
+ for (const projectDir of projectDirs) {
172
+ const sessionFiles = await this.claudeDir.listSessionFiles(projectDir);
173
+ for (const filePath of sessionFiles) {
174
+ const fileName = path.basename(filePath, '.jsonl');
175
+ if (fileName === sessionId || fileName.startsWith(sessionId)) {
176
+ analytics = await this.analyticsService.getSessionAnalytics(filePath, projectDir);
177
+ break;
178
+ }
179
+ }
180
+ if (analytics)
181
+ break;
182
+ }
183
+ console.log(`[Insights] Analytics loaded: ${analytics ? 'yes' : 'no'}`);
184
+ // Stage 4: Building context
185
+ this.emitProgress(sessionId, 'building_context', 30, 'Building comprehensive context...');
186
+ const context = this.buildComprehensiveContext(session, analytics);
187
+ console.log(`[Insights] Context built, length: ${context.length} chars`);
188
+ // Stage 5: Calling Claude
189
+ this.emitProgress(sessionId, 'calling_claude', 40, 'Analyzing with Claude AI (this may take a few minutes)...');
190
+ console.log(`[Insights] Calling Claude Code for analysis...`);
191
+ const aiResponse = await this.callClaudeCode(context);
192
+ console.log(`[Insights] Claude response received, length: ${aiResponse.length} chars`);
193
+ // Check for empty response
194
+ if (!aiResponse || aiResponse.trim().length === 0) {
195
+ const error = this.createAIError('EMPTY_RESPONSE', 'Claude returned an empty response', true, undefined, 'Try regenerating the insight');
196
+ this.emitProgress(sessionId, 'error', 50, 'Empty response from Claude', error);
197
+ throw new Error('Empty response from Claude Code');
198
+ }
199
+ // Stage 6: Parsing response
200
+ this.emitProgress(sessionId, 'parsing_response', 70, 'Parsing AI response...');
201
+ const parsed = this.parseRichResponse(aiResponse, session, analytics);
202
+ // Build final insight
203
+ const insight = {
204
+ sessionId,
205
+ version: INSIGHT_VERSION,
206
+ generatedAt: new Date().toISOString(),
207
+ status: 'completed',
208
+ ...parsed,
209
+ };
210
+ // Merge analytics data into metadata
211
+ if (insight.metadata && analytics) {
212
+ insight.metadata.tokens.total = analytics.tokens?.totalTokens ?? 0;
213
+ insight.metadata.tokens.input = analytics.tokens?.inputTokens ?? 0;
214
+ insight.metadata.tokens.output = analytics.tokens?.outputTokens ?? 0;
215
+ insight.metadata.tokens.cached = analytics.tokens?.cacheReadTokens ?? 0;
216
+ insight.metadata.duration.total = analytics.durationMinutes ?? 0;
217
+ }
218
+ if (insight.costOptimization && analytics?.cost) {
219
+ insight.costOptimization.totalCost = analytics.cost.totalCost;
220
+ }
221
+ // Stage 7: Saving
222
+ this.emitProgress(sessionId, 'saving', 90, 'Saving insight...');
223
+ await this.saveInsight(insight);
224
+ // Stage 8: Complete
225
+ this.emitProgress(sessionId, 'completed', 100, 'Insight generation complete!');
226
+ this.progressMap.delete(sessionId); // Clean up
227
+ return insight;
228
+ }
229
+ catch (error) {
230
+ const aiError = this.mapErrorToAIError(error);
231
+ this.emitProgress(sessionId, 'error', 0, aiError.message, aiError);
232
+ const errorInsight = {
233
+ sessionId,
234
+ version: INSIGHT_VERSION,
235
+ generatedAt: new Date().toISOString(),
236
+ status: 'error',
237
+ error: aiError.message,
238
+ // Include structured error info in the insight for the UI
239
+ errorDetails: {
240
+ code: aiError.code,
241
+ message: aiError.message,
242
+ recoverable: aiError.recoverable,
243
+ suggestion: aiError.suggestion,
244
+ },
245
+ };
246
+ await this.saveInsight(errorInsight);
247
+ return errorInsight;
248
+ }
249
+ }
250
+ buildComprehensiveContext(session, analytics) {
251
+ const messages = session.messages;
252
+ // Build detailed message list with indices
253
+ const messageDetails = messages.map((m, i) => {
254
+ let detail = `[${i}] ${m.type.toUpperCase()} (${m.timestamp})`;
255
+ if (m.type === 'tool-use' && m.metadata?.toolName) {
256
+ detail += ` - Tool: ${m.metadata.toolName}`;
257
+ }
258
+ if (m.metadata?.tokens) {
259
+ detail += ` - Tokens: ${m.metadata.tokens.input + m.metadata.tokens.output}`;
260
+ }
261
+ detail += `\n${m.content.substring(0, 500)}${m.content.length > 500 ? '...' : ''}`;
262
+ return detail;
263
+ }).join('\n\n');
264
+ // Tool usage summary
265
+ const toolCalls = messages.filter(m => m.type === 'tool-use');
266
+ const toolSummary = toolCalls.reduce((acc, m) => {
267
+ const name = m.metadata?.toolName || 'unknown';
268
+ acc[name] = (acc[name] || 0) + 1;
269
+ return acc;
270
+ }, {});
271
+ // Thinking messages for duration analysis
272
+ const thinkingMessages = messages.filter(m => m.type === 'thinking').length;
273
+ // Code-related tools
274
+ const codeTools = ['Edit', 'Write', 'Read', 'Bash'];
275
+ const codeChanges = toolCalls.filter(m => codeTools.includes(m.metadata?.toolName || ''));
276
+ let context = `# Comprehensive Session Analysis Request
277
+
278
+ ## Session Statistics
279
+ - Total messages: ${messages.length}
280
+ - User messages: ${messages.filter(m => m.type === 'user').length}
281
+ - Assistant messages: ${messages.filter(m => m.type === 'assistant').length}
282
+ - Tool calls: ${toolCalls.length}
283
+ - Thinking blocks: ${thinkingMessages}
284
+ - Code-related operations: ${codeChanges.length}
285
+ `;
286
+ if (analytics) {
287
+ context += `
288
+ ## Analytics Data
289
+ - Total cost: $${analytics.cost?.totalCost?.toFixed(4) ?? 'N/A'}
290
+ - Total tokens: ${analytics.tokens?.totalTokens ?? 'N/A'}
291
+ - Input tokens: ${analytics.tokens?.inputTokens ?? 'N/A'}
292
+ - Output tokens: ${analytics.tokens?.outputTokens ?? 'N/A'}
293
+ - Cached tokens: ${analytics.tokens?.cacheReadTokens ?? 'N/A'}
294
+ - Duration: ${analytics.durationMinutes ?? 'N/A'} minutes
295
+ `;
296
+ }
297
+ context += `
298
+ ## Tool Usage Summary
299
+ ${Object.entries(toolSummary).map(([name, count]) => `- ${name}: ${count} calls`).join('\n') || 'No tools used'}
300
+
301
+ ## Full Message Transcript (with indices)
302
+ ${messageDetails}
303
+ `;
304
+ return context;
305
+ }
306
+ async callClaudeCode(context) {
307
+ const prompt = `You are an expert AI session analyst. Analyze this Claude Code session transcript and provide comprehensive insights.
308
+
309
+ IMPORTANT: Return ONLY a valid JSON object. No markdown, no explanation, no code blocks - just the raw JSON.
310
+
311
+ The JSON must follow this exact structure:
312
+ {
313
+ "title": "Brief title describing the session (5-10 words)",
314
+ "summary": "2-3 sentence summary of what was accomplished",
315
+ "keyAccomplishments": ["accomplishment 1", "accomplishment 2", "accomplishment 3"],
316
+
317
+ "sentiment": {
318
+ "overall": "positive|neutral|negative|mixed",
319
+ "averageScore": 0.5,
320
+ "timeline": [
321
+ {"messageIndex": 0, "timestamp": "ISO timestamp", "sentiment": "positive|neutral|negative|frustrated", "score": 0.7, "reason": "brief reason"}
322
+ ],
323
+ "frustrationSpikes": [
324
+ {"messageIndex": 5, "reason": "why user seemed frustrated"}
325
+ ],
326
+ "moodShifts": [
327
+ {"fromIndex": 2, "toIndex": 5, "from": "positive", "to": "frustrated", "trigger": "what caused the shift"}
328
+ ]
329
+ },
330
+
331
+ "topics": [
332
+ {
333
+ "id": "topic-1",
334
+ "name": "Topic Name",
335
+ "description": "What this segment covers",
336
+ "startIndex": 0,
337
+ "endIndex": 10,
338
+ "messageCount": 11,
339
+ "category": "debugging|feature|explanation|refactoring|testing|documentation|setup|other",
340
+ "keywords": ["keyword1", "keyword2"]
341
+ }
342
+ ],
343
+
344
+ "quality": {
345
+ "averageScore": 85,
346
+ "distribution": {"excellent": 5, "good": 10, "fair": 3, "poor": 1},
347
+ "responses": [
348
+ {"messageIndex": 1, "score": 90, "metrics": {"relevance": 95, "completeness": 85, "accuracy": 90, "helpfulness": 90}, "issues": []}
349
+ ],
350
+ "bestResponses": [{"index": 5, "reason": "why this response was excellent"}],
351
+ "improvementAreas": ["area that could be improved"]
352
+ },
353
+
354
+ "metadata": {
355
+ "duration": {"total": 30, "thinking": 5, "toolExecution": 10, "userWaiting": 15},
356
+ "tokens": {"total": 0, "input": 0, "output": 0, "cached": 0, "bySection": [{"topic": "Topic Name", "tokens": 1000}]},
357
+ "tools": {
358
+ "total": 15,
359
+ "byName": [{"name": "Edit", "count": 5, "successRate": 100}],
360
+ "mostUsed": ["Edit", "Read", "Bash"]
361
+ },
362
+ "codeChanges": {
363
+ "filesEdited": 3,
364
+ "filesCreated": 1,
365
+ "linesAdded": 150,
366
+ "linesRemoved": 50,
367
+ "files": [{"path": "src/file.ts", "action": "edit", "changes": 50}]
368
+ },
369
+ "errors": {
370
+ "count": 2,
371
+ "types": [{"type": "TypeScript", "count": 2}],
372
+ "details": [{"message": "error message", "resolved": true, "messageIndex": 10}]
373
+ }
374
+ },
375
+
376
+ "actionableSummary": {
377
+ "mainTasks": [{"task": "task description", "status": "completed|partial|failed"}],
378
+ "errorsEncountered": [{"error": "error description", "resolution": "how it was resolved"}],
379
+ "codeChanges": [{"file": "filename", "description": "what changed"}],
380
+ "keyDecisions": [{"decision": "what was decided", "rationale": "why"}],
381
+ "learnings": ["key insight or learning from the session"]
382
+ },
383
+
384
+ "costOptimization": {
385
+ "suggestions": [
386
+ {"type": "model|caching|context|tool-usage", "title": "suggestion title", "description": "detailed explanation", "impact": "low|medium|high"}
387
+ ],
388
+ "potentialSavings": 0.05
389
+ },
390
+
391
+ "followUps": [
392
+ {"type": "incomplete|test|documentation|refactor", "title": "follow-up title", "description": "what to do next", "priority": "low|medium|high"}
393
+ ]
394
+ }
395
+
396
+ Analyze the following session thoroughly. Pay special attention to:
397
+ 1. Sentiment changes throughout the conversation
398
+ 2. Natural topic boundaries and transitions
399
+ 3. Quality of assistant responses
400
+ 4. Code changes and their impact
401
+ 5. Errors and how they were handled
402
+ 6. Opportunities for cost optimization
403
+
404
+ Session Data:
405
+ ${context}`;
406
+ // Write prompt to temp file to avoid shell argument limits
407
+ const tempFile = path.join(os.tmpdir(), `claude-insight-${Date.now()}.txt`);
408
+ await fs.writeFile(tempFile, prompt, 'utf-8');
409
+ console.log(`[Insights] Prompt written to temp file: ${tempFile}`);
410
+ return new Promise((resolve, reject) => {
411
+ const timeout = 300000; // 5 minutes
412
+ const timer = setTimeout(() => {
413
+ console.log(`[Insights] TIMEOUT after 5 minutes`);
414
+ fs.unlink(tempFile).catch(() => { });
415
+ reject(new Error('Insight generation timed out after 5 minutes'));
416
+ }, timeout);
417
+ // Use shell with cat to pipe content to claude
418
+ const command = `cat "${tempFile}" | claude -p --output-format text`;
419
+ console.log(`[Insights] Executing command...`);
420
+ exec(command, {
421
+ cwd: process.cwd(),
422
+ env: { ...process.env, NO_COLOR: '1', FORCE_COLOR: '0' },
423
+ maxBuffer: 10 * 1024 * 1024, // 10MB buffer
424
+ timeout: timeout,
425
+ }, (error, stdout, stderr) => {
426
+ clearTimeout(timer);
427
+ fs.unlink(tempFile).catch(() => { }); // Clean up temp file
428
+ if (stderr) {
429
+ console.log(`[Insights] stderr: ${stderr.slice(0, 200)}`);
430
+ }
431
+ if (error) {
432
+ console.log(`[Insights] Error: ${error.message}`);
433
+ reject(new Error(`Failed to run claude: ${error.message}`));
434
+ return;
435
+ }
436
+ console.log(`[Insights] Response received, length: ${stdout.length} chars`);
437
+ resolve(stdout);
438
+ });
439
+ });
440
+ }
441
+ parseRichResponse(response, session, analytics) {
442
+ const cleanedResponse = response
443
+ .replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '')
444
+ .replace(/[\r\n]+/g, '\n')
445
+ .trim();
446
+ const jsonMatch = cleanedResponse.match(/\{[\s\S]*\}/);
447
+ if (!jsonMatch) {
448
+ return this.generateFallbackInsight(session, analytics);
449
+ }
450
+ try {
451
+ const parsed = JSON.parse(jsonMatch[0]);
452
+ return {
453
+ title: parsed.title || 'Session Analysis',
454
+ summary: parsed.summary || '',
455
+ keyAccomplishments: this.ensureArray(parsed.keyAccomplishments),
456
+ sentiment: this.parseSentiment(parsed.sentiment),
457
+ topics: this.parseTopics(parsed.topics),
458
+ quality: this.parseQuality(parsed.quality),
459
+ metadata: this.parseMetadata(parsed.metadata, session, analytics),
460
+ actionableSummary: this.parseActionableSummary(parsed.actionableSummary),
461
+ costOptimization: parsed.costOptimization ? {
462
+ totalCost: analytics?.cost?.totalCost ?? 0,
463
+ suggestions: this.ensureArray(parsed.costOptimization.suggestions).map((s) => ({
464
+ type: s.type || 'context',
465
+ title: s.title || '',
466
+ description: s.description || '',
467
+ impact: s.impact || 'low',
468
+ })),
469
+ potentialSavings: parsed.costOptimization.potentialSavings,
470
+ } : undefined,
471
+ followUps: this.ensureArray(parsed.followUps).map((f) => ({
472
+ type: f.type || 'incomplete',
473
+ title: f.title || '',
474
+ description: f.description || '',
475
+ priority: f.priority || 'low',
476
+ })),
477
+ };
478
+ }
479
+ catch {
480
+ return this.generateFallbackInsight(session, analytics);
481
+ }
482
+ }
483
+ ensureArray(val) {
484
+ return Array.isArray(val) ? val : [];
485
+ }
486
+ parseSentiment(data) {
487
+ if (!data || typeof data !== 'object')
488
+ return undefined;
489
+ const d = data;
490
+ return {
491
+ overall: d.overall || 'neutral',
492
+ averageScore: typeof d.averageScore === 'number' ? d.averageScore : 0,
493
+ timeline: this.ensureArray(d.timeline),
494
+ frustrationSpikes: this.ensureArray(d.frustrationSpikes),
495
+ moodShifts: this.ensureArray(d.moodShifts),
496
+ };
497
+ }
498
+ parseTopics(data) {
499
+ if (!Array.isArray(data))
500
+ return undefined;
501
+ return data.map((t, i) => ({
502
+ id: t.id || `topic-${i}`,
503
+ name: t.name || `Topic ${i + 1}`,
504
+ description: t.description || '',
505
+ startIndex: t.startIndex || 0,
506
+ endIndex: t.endIndex || 0,
507
+ messageCount: t.messageCount || 0,
508
+ category: t.category || 'other',
509
+ keywords: this.ensureArray(t.keywords),
510
+ }));
511
+ }
512
+ parseQuality(data) {
513
+ if (!data || typeof data !== 'object')
514
+ return undefined;
515
+ const d = data;
516
+ return {
517
+ averageScore: typeof d.averageScore === 'number' ? d.averageScore : 0,
518
+ distribution: d.distribution || { excellent: 0, good: 0, fair: 0, poor: 0 },
519
+ responses: this.ensureArray(d.responses),
520
+ bestResponses: this.ensureArray(d.bestResponses),
521
+ improvementAreas: this.ensureArray(d.improvementAreas),
522
+ };
523
+ }
524
+ parseMetadata(data, session, analytics) {
525
+ const d = (data && typeof data === 'object') ? data : {};
526
+ const duration = (d.duration && typeof d.duration === 'object') ? d.duration : {};
527
+ const tokens = (d.tokens && typeof d.tokens === 'object') ? d.tokens : {};
528
+ const tools = (d.tools && typeof d.tools === 'object') ? d.tools : {};
529
+ const codeChanges = (d.codeChanges && typeof d.codeChanges === 'object') ? d.codeChanges : {};
530
+ const errors = (d.errors && typeof d.errors === 'object') ? d.errors : {};
531
+ // Calculate from session data if not provided
532
+ const toolCalls = session.messages.filter(m => m.type === 'tool-use');
533
+ const thinkingMsgs = session.messages.filter(m => m.type === 'thinking');
534
+ return {
535
+ duration: {
536
+ total: duration.total || analytics?.durationMinutes || 0,
537
+ thinking: duration.thinking || thinkingMsgs.length * 0.5, // estimate
538
+ toolExecution: duration.toolExecution || toolCalls.length * 0.3,
539
+ userWaiting: duration.userWaiting || 0,
540
+ },
541
+ tokens: {
542
+ total: analytics?.tokens?.totalTokens || 0,
543
+ input: analytics?.tokens?.inputTokens || 0,
544
+ output: analytics?.tokens?.outputTokens || 0,
545
+ cached: analytics?.tokens?.cacheReadTokens || 0,
546
+ bySection: this.ensureArray(tokens.bySection),
547
+ },
548
+ tools: {
549
+ total: typeof tools.total === 'number' ? tools.total : toolCalls.length,
550
+ byName: this.ensureArray(tools.byName).length > 0
551
+ ? this.ensureArray(tools.byName)
552
+ : (analytics?.toolUsage || []).map(t => ({
553
+ name: t.toolName,
554
+ count: t.count,
555
+ successRate: t.successCount !== undefined && t.count > 0
556
+ ? (t.successCount / t.count) * 100
557
+ : 100,
558
+ })),
559
+ mostUsed: this.ensureArray(tools.mostUsed),
560
+ },
561
+ codeChanges: {
562
+ filesEdited: typeof codeChanges.filesEdited === 'number' ? codeChanges.filesEdited : 0,
563
+ filesCreated: typeof codeChanges.filesCreated === 'number' ? codeChanges.filesCreated : 0,
564
+ linesAdded: typeof codeChanges.linesAdded === 'number' ? codeChanges.linesAdded : 0,
565
+ linesRemoved: typeof codeChanges.linesRemoved === 'number' ? codeChanges.linesRemoved : 0,
566
+ files: this.ensureArray(codeChanges.files),
567
+ },
568
+ errors: {
569
+ count: typeof errors.count === 'number' ? errors.count : 0,
570
+ types: this.ensureArray(errors.types),
571
+ details: this.ensureArray(errors.details),
572
+ },
573
+ };
574
+ }
575
+ parseActionableSummary(data) {
576
+ if (!data || typeof data !== 'object')
577
+ return undefined;
578
+ const d = data;
579
+ return {
580
+ mainTasks: this.ensureArray(d.mainTasks),
581
+ errorsEncountered: this.ensureArray(d.errorsEncountered),
582
+ codeChanges: this.ensureArray(d.codeChanges),
583
+ keyDecisions: this.ensureArray(d.keyDecisions),
584
+ learnings: this.ensureArray(d.learnings),
585
+ };
586
+ }
587
+ generateFallbackInsight(session, analytics) {
588
+ const messages = session.messages;
589
+ const toolCalls = messages.filter(m => m.type === 'tool-use');
590
+ return {
591
+ title: 'Session Analysis',
592
+ summary: 'Automated analysis of the session. AI-generated insights could not be parsed.',
593
+ keyAccomplishments: [],
594
+ metadata: this.parseMetadata({}, session, analytics),
595
+ sentiment: {
596
+ overall: 'neutral',
597
+ averageScore: 0,
598
+ timeline: [],
599
+ frustrationSpikes: [],
600
+ moodShifts: [],
601
+ },
602
+ topics: [{
603
+ id: 'topic-1',
604
+ name: 'Full Session',
605
+ description: 'Complete session transcript',
606
+ startIndex: 0,
607
+ endIndex: messages.length - 1,
608
+ messageCount: messages.length,
609
+ category: 'other',
610
+ keywords: [],
611
+ }],
612
+ quality: {
613
+ averageScore: 0,
614
+ distribution: { excellent: 0, good: 0, fair: 0, poor: 0 },
615
+ responses: [],
616
+ bestResponses: [],
617
+ improvementAreas: [],
618
+ },
619
+ actionableSummary: {
620
+ mainTasks: [],
621
+ errorsEncountered: [],
622
+ codeChanges: [],
623
+ keyDecisions: [],
624
+ learnings: [],
625
+ },
626
+ };
627
+ }
628
+ // --------------------------------------------------------------------------
629
+ // Project-Level AI Insights
630
+ // --------------------------------------------------------------------------
631
+ getProjectInsightPath(projectPath) {
632
+ const sanitized = projectPath.replace(/\//g, '-').replace(/^-/, '');
633
+ return path.join(this.claudeDir.insightsDir, `project-${sanitized}.json`);
634
+ }
635
+ getGlobalInsightPath() {
636
+ return path.join(this.claudeDir.insightsDir, 'global-insight.json');
637
+ }
638
+ async getProjectAIInsight(projectPath) {
639
+ const insightPath = this.getProjectInsightPath(projectPath);
640
+ return this.claudeDir.readJSON(insightPath);
641
+ }
642
+ async getGlobalAIInsight() {
643
+ const insightPath = this.getGlobalInsightPath();
644
+ return this.claudeDir.readJSON(insightPath);
645
+ }
646
+ async generateProjectAIInsight(projectPath, force = false) {
647
+ if (!force) {
648
+ const cached = await this.getProjectAIInsight(projectPath);
649
+ if (cached && cached.status === 'completed') {
650
+ return cached;
651
+ }
652
+ }
653
+ const projectName = path.basename(projectPath);
654
+ const pendingInsight = {
655
+ projectPath,
656
+ projectName,
657
+ version: INSIGHT_VERSION,
658
+ generatedAt: new Date().toISOString(),
659
+ status: 'generating',
660
+ };
661
+ await this.claudeDir.ensureInsightsDir();
662
+ const insightPath = this.getProjectInsightPath(projectPath);
663
+ await this.claudeDir.writeJSON(insightPath, pendingInsight);
664
+ try {
665
+ const insight = await this.doGenerateProjectInsight(projectPath);
666
+ return insight;
667
+ }
668
+ catch (error) {
669
+ const errorInsight = {
670
+ ...pendingInsight,
671
+ status: 'error',
672
+ error: error.message,
673
+ };
674
+ await this.claudeDir.writeJSON(insightPath, errorInsight);
675
+ return errorInsight;
676
+ }
677
+ }
678
+ async generateGlobalAIInsight(force = false) {
679
+ if (!force) {
680
+ const cached = await this.getGlobalAIInsight();
681
+ if (cached && cached.status === 'completed') {
682
+ return cached;
683
+ }
684
+ }
685
+ const pendingInsight = {
686
+ projectPath: 'global',
687
+ projectName: 'All Projects',
688
+ version: INSIGHT_VERSION,
689
+ generatedAt: new Date().toISOString(),
690
+ status: 'generating',
691
+ };
692
+ await this.claudeDir.ensureInsightsDir();
693
+ const insightPath = this.getGlobalInsightPath();
694
+ await this.claudeDir.writeJSON(insightPath, pendingInsight);
695
+ try {
696
+ const insight = await this.doGenerateGlobalInsight();
697
+ return insight;
698
+ }
699
+ catch (error) {
700
+ const errorInsight = {
701
+ ...pendingInsight,
702
+ status: 'error',
703
+ error: error.message,
704
+ };
705
+ await this.claudeDir.writeJSON(insightPath, errorInsight);
706
+ return errorInsight;
707
+ }
708
+ }
709
+ async doGenerateProjectInsight(projectPath) {
710
+ console.log(`[ProjectInsight] Starting for project: ${projectPath}`);
711
+ const projectName = path.basename(projectPath);
712
+ const projectDirs = await this.claudeDir.listProjects();
713
+ const projectDir = projectDirs.find(d => this.claudeDir.projectDirToPath(d) === projectPath);
714
+ if (!projectDir) {
715
+ throw new Error('Project not found');
716
+ }
717
+ // Gather all sessions and analytics for this project
718
+ const sessionFiles = await this.claudeDir.listSessionFiles(projectDir);
719
+ console.log(`[ProjectInsight] Found ${sessionFiles.length} session files`);
720
+ const sessionsData = [];
721
+ for (const filePath of sessionFiles.slice(0, 20)) { // Limit to 20 most recent
722
+ const sessionId = path.basename(filePath, '.jsonl');
723
+ const session = await this.sessionService.getSession(sessionId);
724
+ if (session) {
725
+ const analytics = await this.analyticsService.getSessionAnalytics(filePath, projectDir);
726
+ sessionsData.push({
727
+ id: sessionId,
728
+ messages: session.messages,
729
+ analytics,
730
+ });
731
+ }
732
+ }
733
+ console.log(`[ProjectInsight] Loaded ${sessionsData.length} sessions`);
734
+ const context = this.buildProjectContext(projectName, sessionsData);
735
+ console.log(`[ProjectInsight] Context built, length: ${context.length} chars`);
736
+ console.log(`[ProjectInsight] Calling Claude Code...`);
737
+ const aiResponse = await this.callClaudeForProjectInsight(context);
738
+ console.log(`[ProjectInsight] Response received, length: ${aiResponse.length} chars`);
739
+ const parsed = this.parseProjectInsightResponse(aiResponse);
740
+ const insight = {
741
+ projectPath,
742
+ projectName,
743
+ version: INSIGHT_VERSION,
744
+ generatedAt: new Date().toISOString(),
745
+ status: 'completed',
746
+ ...parsed,
747
+ };
748
+ const insightPath = this.getProjectInsightPath(projectPath);
749
+ await this.claudeDir.writeJSON(insightPath, insight);
750
+ return insight;
751
+ }
752
+ async doGenerateGlobalInsight() {
753
+ const projectDirs = await this.claudeDir.listProjects();
754
+ const allProjectsData = [];
755
+ for (const projectDir of projectDirs.slice(0, 10)) {
756
+ const projectPath = this.claudeDir.projectDirToPath(projectDir);
757
+ const projectName = this.claudeDir.projectDirToName(projectDir);
758
+ const sessionFiles = await this.claudeDir.listSessionFiles(projectDir);
759
+ let totalCost = 0;
760
+ let totalTokens = 0;
761
+ const toolUsage = {};
762
+ const filePatterns = new Set();
763
+ for (const filePath of sessionFiles.slice(0, 10)) {
764
+ const analytics = await this.analyticsService.getSessionAnalytics(filePath, projectDir);
765
+ if (analytics) {
766
+ totalCost += analytics.cost?.totalCost || 0;
767
+ totalTokens += analytics.tokens?.totalTokens || 0;
768
+ for (const tool of analytics.toolUsage || []) {
769
+ toolUsage[tool.toolName] = (toolUsage[tool.toolName] || 0) + tool.count;
770
+ }
771
+ }
772
+ const sessionId = path.basename(filePath, '.jsonl');
773
+ const session = await this.sessionService.getSession(sessionId);
774
+ if (session) {
775
+ for (const msg of session.messages) {
776
+ if (msg.type === 'tool-use' && msg.metadata?.toolInput) {
777
+ const input = msg.metadata.toolInput;
778
+ if (input.file_path && typeof input.file_path === 'string') {
779
+ const ext = path.extname(input.file_path);
780
+ if (ext)
781
+ filePatterns.add(ext);
782
+ }
783
+ }
784
+ }
785
+ }
786
+ }
787
+ allProjectsData.push({
788
+ projectName,
789
+ projectPath,
790
+ sessionCount: sessionFiles.length,
791
+ totalCost,
792
+ totalTokens,
793
+ toolUsage,
794
+ filePatterns: Array.from(filePatterns),
795
+ });
796
+ }
797
+ const context = this.buildGlobalContext(allProjectsData);
798
+ const aiResponse = await this.callClaudeForProjectInsight(context);
799
+ const parsed = this.parseProjectInsightResponse(aiResponse);
800
+ const insight = {
801
+ projectPath: 'global',
802
+ projectName: 'All Projects',
803
+ version: INSIGHT_VERSION,
804
+ generatedAt: new Date().toISOString(),
805
+ status: 'completed',
806
+ ...parsed,
807
+ };
808
+ const insightPath = this.getGlobalInsightPath();
809
+ await this.claudeDir.writeJSON(insightPath, insight);
810
+ return insight;
811
+ }
812
+ buildProjectContext(projectName, sessions) {
813
+ let totalMessages = 0;
814
+ let totalCost = 0;
815
+ let totalTokens = 0;
816
+ const allTools = {};
817
+ const allFiles = new Set();
818
+ const allTopics = [];
819
+ const errorPatterns = [];
820
+ for (const session of sessions) {
821
+ totalMessages += session.messages.length;
822
+ totalCost += session.analytics?.cost?.totalCost || 0;
823
+ totalTokens += session.analytics?.tokens?.totalTokens || 0;
824
+ for (const msg of session.messages) {
825
+ if (msg.type === 'tool-use' && msg.metadata?.toolName) {
826
+ allTools[msg.metadata.toolName] = (allTools[msg.metadata.toolName] || 0) + 1;
827
+ const input = msg.metadata.toolInput;
828
+ if (input?.file_path && typeof input.file_path === 'string') {
829
+ allFiles.add(input.file_path);
830
+ }
831
+ }
832
+ // Collect error patterns
833
+ if (msg.content.toLowerCase().includes('error') ||
834
+ msg.content.toLowerCase().includes('failed') ||
835
+ msg.content.toLowerCase().includes('exception')) {
836
+ const snippet = msg.content.substring(0, 100);
837
+ if (!errorPatterns.includes(snippet)) {
838
+ errorPatterns.push(snippet);
839
+ }
840
+ }
841
+ // Collect topics from user messages
842
+ if (msg.type === 'user' && msg.content.length > 20) {
843
+ allTopics.push(msg.content.substring(0, 200));
844
+ }
845
+ }
846
+ }
847
+ return `# Project-Level AI Analysis Request
848
+
849
+ ## Project: ${projectName}
850
+
851
+ ## Aggregate Statistics
852
+ - Total sessions analyzed: ${sessions.length}
853
+ - Total messages: ${totalMessages}
854
+ - Total cost: $${totalCost.toFixed(4)}
855
+ - Total tokens: ${totalTokens.toLocaleString()}
856
+
857
+ ## Tool Usage Summary
858
+ ${Object.entries(allTools)
859
+ .sort((a, b) => b[1] - a[1])
860
+ .slice(0, 15)
861
+ .map(([tool, count]) => `- ${tool}: ${count} calls`)
862
+ .join('\n')}
863
+
864
+ ## Files Touched (sample)
865
+ ${Array.from(allFiles).slice(0, 30).map(f => `- ${f}`).join('\n')}
866
+
867
+ ## File Extensions Used
868
+ ${Array.from(new Set(Array.from(allFiles).map(f => path.extname(f)).filter(Boolean))).join(', ')}
869
+
870
+ ## Sample User Requests (to understand project focus)
871
+ ${allTopics.slice(0, 15).map((t, i) => `${i + 1}. ${t}`).join('\n')}
872
+
873
+ ## Error Patterns Observed
874
+ ${errorPatterns.slice(0, 10).map((e, i) => `${i + 1}. ${e}`).join('\n')}
875
+ `;
876
+ }
877
+ buildGlobalContext(projects) {
878
+ const totalCost = projects.reduce((sum, p) => sum + p.totalCost, 0);
879
+ const totalTokens = projects.reduce((sum, p) => sum + p.totalTokens, 0);
880
+ const totalSessions = projects.reduce((sum, p) => sum + p.sessionCount, 0);
881
+ const globalToolUsage = {};
882
+ const allFilePatterns = new Set();
883
+ for (const project of projects) {
884
+ for (const [tool, count] of Object.entries(project.toolUsage)) {
885
+ globalToolUsage[tool] = (globalToolUsage[tool] || 0) + count;
886
+ }
887
+ for (const pattern of project.filePatterns) {
888
+ allFilePatterns.add(pattern);
889
+ }
890
+ }
891
+ return `# Global AI Analysis Request (All Projects)
892
+
893
+ ## Overview
894
+ - Total projects: ${projects.length}
895
+ - Total sessions: ${totalSessions}
896
+ - Total cost: $${totalCost.toFixed(4)}
897
+ - Total tokens: ${totalTokens.toLocaleString()}
898
+
899
+ ## Projects Summary
900
+ ${projects.map(p => `- ${p.projectName}: ${p.sessionCount} sessions, $${p.totalCost.toFixed(4)}`).join('\n')}
901
+
902
+ ## Global Tool Usage
903
+ ${Object.entries(globalToolUsage)
904
+ .sort((a, b) => b[1] - a[1])
905
+ .slice(0, 15)
906
+ .map(([tool, count]) => `- ${tool}: ${count} calls`)
907
+ .join('\n')}
908
+
909
+ ## Technology Patterns (file extensions)
910
+ ${Array.from(allFilePatterns).join(', ')}
911
+
912
+ ## Cost Distribution by Project
913
+ ${projects
914
+ .sort((a, b) => b.totalCost - a.totalCost)
915
+ .map(p => `- ${p.projectName}: $${p.totalCost.toFixed(4)} (${((p.totalCost / totalCost) * 100).toFixed(1)}%)`)
916
+ .join('\n')}
917
+ `;
918
+ }
919
+ async callClaudeForProjectInsight(context) {
920
+ const prompt = `Analyze this Claude Code usage data and return ONLY valid JSON (no markdown, no explanation).
921
+
922
+ Required JSON structure with these exact keys:
923
+ - summary: string (2-3 paragraph executive summary)
924
+ - highlights: string[] (3-5 positive findings)
925
+ - concerns: string[] (2-3 areas of concern)
926
+ - healthScore: {overall: number 0-100, categories: {codeQuality, testCoverage, documentation, errorRate, velocity: numbers}, trends: [{category, direction: "improving"|"declining"|"stable", change: number}]}
927
+ - techStack: [{name, type: "language"|"framework"|"library"|"tool", usage: number, proficiency: "beginner"|"intermediate"|"advanced"|"expert", relatedFiles: string[]}]
928
+ - techStackRecommendations: string[]
929
+ - codingPatterns: [{name, description, frequency: number, category: "architecture"|"design"|"testing"|"other", examples: string[], recommendation}]
930
+ - antiPatterns: same structure as codingPatterns
931
+ - developmentPatterns: [{pattern, description, frequency: number, impact: "positive"|"neutral"|"negative", suggestion}]
932
+ - workflowInsights: [{name, description, efficiency: number 0-100, bottlenecks: string[], suggestions: string[]}]
933
+ - productivityMetrics: [{name, value: number, unit, trend: "up"|"down"|"stable", percentChange?: number}]
934
+ - learningInsights: [{topic, proficiencyGain: number, timeInvested: number, resourcesUsed: string[], nextSteps: string[]}]
935
+ - skillGrowth: [{skill, level: number 0-100, growth: number}]
936
+ - costAnalysis: {totalCost, costPerSession, projectedMonthlyCost: numbers, optimizationOpportunities: [{type, title, description, impact: "low"|"medium"|"high"}]}
937
+ - recommendations: [{category: "performance"|"quality"|"cost"|"learning"|"workflow", priority: "low"|"medium"|"high", title, description, expectedImpact}]
938
+
939
+ Data to analyze:
940
+ ${context}`;
941
+ // Write prompt to temp file to avoid shell argument limits
942
+ const tempFile = path.join(os.tmpdir(), `claude-prompt-${Date.now()}.txt`);
943
+ await fs.writeFile(tempFile, prompt, 'utf-8');
944
+ console.log(`[ProjectInsight] Prompt written to temp file: ${tempFile}`);
945
+ return new Promise((resolve, reject) => {
946
+ const timeout = 300000; // 5 minutes
947
+ const timer = setTimeout(() => {
948
+ console.log(`[ProjectInsight] TIMEOUT after 5 minutes`);
949
+ fs.unlink(tempFile).catch(() => { });
950
+ reject(new Error('Project insight generation timed out after 5 minutes'));
951
+ }, timeout);
952
+ // Use shell with cat to pipe content to claude
953
+ const command = `cat "${tempFile}" | claude -p --output-format text`;
954
+ console.log(`[ProjectInsight] Executing: ${command.slice(0, 100)}...`);
955
+ exec(command, {
956
+ cwd: process.cwd(),
957
+ env: { ...process.env, NO_COLOR: '1', FORCE_COLOR: '0' },
958
+ maxBuffer: 10 * 1024 * 1024, // 10MB buffer
959
+ timeout: timeout,
960
+ }, (error, stdout, stderr) => {
961
+ clearTimeout(timer);
962
+ fs.unlink(tempFile).catch(() => { }); // Clean up temp file
963
+ if (stderr) {
964
+ console.log(`[ProjectInsight] stderr: ${stderr.slice(0, 200)}`);
965
+ }
966
+ if (error) {
967
+ console.log(`[ProjectInsight] Error: ${error.message}`);
968
+ reject(error);
969
+ return;
970
+ }
971
+ console.log(`[ProjectInsight] Response received, length: ${stdout.length} chars`);
972
+ resolve(stdout);
973
+ });
974
+ });
975
+ }
976
+ parseProjectInsightResponse(response) {
977
+ const cleanedResponse = response
978
+ .replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '')
979
+ .replace(/[\r\n]+/g, '\n')
980
+ .trim();
981
+ const jsonMatch = cleanedResponse.match(/\{[\s\S]*\}/);
982
+ if (!jsonMatch) {
983
+ return {
984
+ summary: 'Unable to parse AI response.',
985
+ highlights: [],
986
+ concerns: [],
987
+ recommendations: [],
988
+ };
989
+ }
990
+ try {
991
+ const parsed = JSON.parse(jsonMatch[0]);
992
+ return {
993
+ summary: parsed.summary || '',
994
+ highlights: this.ensureArray(parsed.highlights),
995
+ concerns: this.ensureArray(parsed.concerns),
996
+ healthScore: parsed.healthScore,
997
+ techStack: this.ensureArray(parsed.techStack),
998
+ techStackRecommendations: this.ensureArray(parsed.techStackRecommendations),
999
+ codingPatterns: this.ensureArray(parsed.codingPatterns),
1000
+ antiPatterns: this.ensureArray(parsed.antiPatterns),
1001
+ developmentPatterns: this.ensureArray(parsed.developmentPatterns),
1002
+ workflowInsights: this.ensureArray(parsed.workflowInsights),
1003
+ productivityMetrics: this.ensureArray(parsed.productivityMetrics),
1004
+ velocityTrend: this.ensureArray(parsed.velocityTrend),
1005
+ learningInsights: this.ensureArray(parsed.learningInsights),
1006
+ skillGrowth: this.ensureArray(parsed.skillGrowth),
1007
+ costAnalysis: parsed.costAnalysis,
1008
+ collaborationPatterns: parsed.collaborationPatterns,
1009
+ recommendations: this.ensureArray(parsed.recommendations),
1010
+ };
1011
+ }
1012
+ catch {
1013
+ return {
1014
+ summary: 'Unable to parse AI response as JSON.',
1015
+ highlights: [],
1016
+ concerns: [],
1017
+ recommendations: [],
1018
+ };
1019
+ }
1020
+ }
1021
+ // --------------------------------------------------------------------------
1022
+ // Status Check
1023
+ // --------------------------------------------------------------------------
1024
+ async isClaudeCodeAvailable() {
1025
+ try {
1026
+ const { execSync } = await import('node:child_process');
1027
+ execSync('which claude', { stdio: 'ignore' });
1028
+ return true;
1029
+ }
1030
+ catch {
1031
+ return false;
1032
+ }
1033
+ }
1034
+ }
1035
+ //# sourceMappingURL=insights.js.map