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.
- package/LICENSE +21 -0
- package/README.md +275 -0
- package/dist/__tests__/package.test.d.ts +2 -0
- package/dist/__tests__/package.test.d.ts.map +1 -0
- package/dist/__tests__/package.test.js +53 -0
- package/dist/__tests__/package.test.js.map +1 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +200 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/server/index.d.ts +11 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +1207 -0
- package/dist/server/index.js.map +1 -0
- package/dist/services/advisor.d.ts +117 -0
- package/dist/services/advisor.d.ts.map +1 -0
- package/dist/services/advisor.js +2636 -0
- package/dist/services/advisor.js.map +1 -0
- package/dist/services/agent-generator.d.ts +71 -0
- package/dist/services/agent-generator.d.ts.map +1 -0
- package/dist/services/agent-generator.js +295 -0
- package/dist/services/agent-generator.js.map +1 -0
- package/dist/services/agents.d.ts +67 -0
- package/dist/services/agents.d.ts.map +1 -0
- package/dist/services/agents.js +405 -0
- package/dist/services/agents.js.map +1 -0
- package/dist/services/analytics.d.ts +145 -0
- package/dist/services/analytics.d.ts.map +1 -0
- package/dist/services/analytics.js +609 -0
- package/dist/services/analytics.js.map +1 -0
- package/dist/services/blueprints.d.ts +31 -0
- package/dist/services/blueprints.d.ts.map +1 -0
- package/dist/services/blueprints.js +317 -0
- package/dist/services/blueprints.js.map +1 -0
- package/dist/services/claude-dir.d.ts +50 -0
- package/dist/services/claude-dir.d.ts.map +1 -0
- package/dist/services/claude-dir.js +193 -0
- package/dist/services/claude-dir.js.map +1 -0
- package/dist/services/hooks.d.ts +38 -0
- package/dist/services/hooks.d.ts.map +1 -0
- package/dist/services/hooks.js +165 -0
- package/dist/services/hooks.js.map +1 -0
- package/dist/services/insights.d.ts +52 -0
- package/dist/services/insights.d.ts.map +1 -0
- package/dist/services/insights.js +1035 -0
- package/dist/services/insights.js.map +1 -0
- package/dist/services/memory.d.ts +14 -0
- package/dist/services/memory.d.ts.map +1 -0
- package/dist/services/memory.js +25 -0
- package/dist/services/memory.js.map +1 -0
- package/dist/services/plans.d.ts +20 -0
- package/dist/services/plans.d.ts.map +1 -0
- package/dist/services/plans.js +149 -0
- package/dist/services/plans.js.map +1 -0
- package/dist/services/project-intelligence.d.ts +75 -0
- package/dist/services/project-intelligence.d.ts.map +1 -0
- package/dist/services/project-intelligence.js +731 -0
- package/dist/services/project-intelligence.js.map +1 -0
- package/dist/services/search.d.ts +32 -0
- package/dist/services/search.d.ts.map +1 -0
- package/dist/services/search.js +203 -0
- package/dist/services/search.js.map +1 -0
- package/dist/services/sessions.d.ts +25 -0
- package/dist/services/sessions.d.ts.map +1 -0
- package/dist/services/sessions.js +248 -0
- package/dist/services/sessions.js.map +1 -0
- package/dist/services/skills.d.ts +30 -0
- package/dist/services/skills.d.ts.map +1 -0
- package/dist/services/skills.js +197 -0
- package/dist/services/skills.js.map +1 -0
- package/dist/services/stats.d.ts +23 -0
- package/dist/services/stats.d.ts.map +1 -0
- package/dist/services/stats.js +88 -0
- package/dist/services/stats.js.map +1 -0
- package/dist/services/teams.d.ts +115 -0
- package/dist/services/teams.d.ts.map +1 -0
- package/dist/services/teams.js +421 -0
- package/dist/services/teams.js.map +1 -0
- package/dist/services/tech-stack.d.ts +98 -0
- package/dist/services/tech-stack.d.ts.map +1 -0
- package/dist/services/tech-stack.js +1088 -0
- package/dist/services/tech-stack.js.map +1 -0
- package/dist/services/terminal.d.ts +75 -0
- package/dist/services/terminal.d.ts.map +1 -0
- package/dist/services/terminal.js +224 -0
- package/dist/services/terminal.js.map +1 -0
- package/dist/types.d.ts +1095 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +18 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/assets/index-BiH4Nhdk.css +1 -0
- package/dist/ui/assets/index-Brv-K8bd.css +1 -0
- package/dist/ui/assets/index-BwMBEdQz.js +3108 -0
- package/dist/ui/assets/index-BwMBEdQz.js.map +1 -0
- package/dist/ui/assets/index-CEWz7ABD.js +3108 -0
- package/dist/ui/assets/index-CEWz7ABD.js.map +1 -0
- package/dist/ui/assets/index-CIZ3vvc-.css +1 -0
- package/dist/ui/assets/index-CsU3cI0n.js +3108 -0
- package/dist/ui/assets/index-CsU3cI0n.js.map +1 -0
- package/dist/ui/assets/index-D3AY6iCS.js +3133 -0
- package/dist/ui/assets/index-D3AY6iCS.js.map +1 -0
- package/dist/ui/assets/index-D8lNZ0Ye.css +1 -0
- package/dist/ui/assets/index-DmgeppSA.js +3108 -0
- package/dist/ui/assets/index-DmgeppSA.js.map +1 -0
- package/dist/ui/favicon.svg +43 -0
- package/dist/ui/index.html +23 -0
- package/dist/utils/jsonl.d.ts +16 -0
- package/dist/utils/jsonl.d.ts.map +1 -0
- package/dist/utils/jsonl.js +51 -0
- package/dist/utils/jsonl.js.map +1 -0
- 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
|