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,731 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Intelligence Service
|
|
3
|
+
*
|
|
4
|
+
* Generates cross-session, project-level insights including:
|
|
5
|
+
* - Project story and narrative
|
|
6
|
+
* - Code hotspots and evolution
|
|
7
|
+
* - Development milestones
|
|
8
|
+
* - Workflow patterns
|
|
9
|
+
* - Recurring challenges
|
|
10
|
+
* - Claude effectiveness analysis
|
|
11
|
+
* - Smart recommendations
|
|
12
|
+
*
|
|
13
|
+
* This is DIFFERENT from session insights (single conversation) and
|
|
14
|
+
* analytics (cost/token tracking). Project Intelligence looks at
|
|
15
|
+
* patterns and evolution across ALL sessions in a project.
|
|
16
|
+
*/
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import fs from 'node:fs/promises';
|
|
19
|
+
import { exec } from 'node:child_process';
|
|
20
|
+
import { promisify } from 'node:util';
|
|
21
|
+
const execAsync = promisify(exec);
|
|
22
|
+
const INTELLIGENCE_CACHE_DIR = 'intelligence';
|
|
23
|
+
const CACHE_VERSION = 1;
|
|
24
|
+
export class ProjectIntelligenceService {
|
|
25
|
+
claudeDir;
|
|
26
|
+
sessionService;
|
|
27
|
+
constructor(claudeDir, sessionService) {
|
|
28
|
+
this.claudeDir = claudeDir;
|
|
29
|
+
this.sessionService = sessionService;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Get intelligence cache directory
|
|
33
|
+
*/
|
|
34
|
+
async getIntelligenceDir() {
|
|
35
|
+
const dir = path.join(this.claudeDir.root, INTELLIGENCE_CACHE_DIR);
|
|
36
|
+
await fs.mkdir(dir, { recursive: true });
|
|
37
|
+
return dir;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Get cache file path for a project
|
|
41
|
+
*/
|
|
42
|
+
getProjectCachePath(projectPath) {
|
|
43
|
+
const sanitized = projectPath.replace(/[\/\\:]/g, '-').replace(/^-/, '');
|
|
44
|
+
return path.join(this.claudeDir.root, INTELLIGENCE_CACHE_DIR, `project-${sanitized}.json`);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Get cached intelligence for a project
|
|
48
|
+
*/
|
|
49
|
+
async getProjectIntelligence(projectPath) {
|
|
50
|
+
try {
|
|
51
|
+
const cachePath = this.getProjectCachePath(projectPath);
|
|
52
|
+
const content = await fs.readFile(cachePath, 'utf-8');
|
|
53
|
+
return JSON.parse(content);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* List all projects with their intelligence summaries
|
|
61
|
+
*/
|
|
62
|
+
async listProjectIntelligence() {
|
|
63
|
+
const projects = await this.sessionService.listProjects();
|
|
64
|
+
const summaries = [];
|
|
65
|
+
for (const project of projects) {
|
|
66
|
+
const cached = await this.getProjectIntelligence(project.path);
|
|
67
|
+
if (cached && cached.status === 'completed') {
|
|
68
|
+
summaries.push({
|
|
69
|
+
projectPath: project.path,
|
|
70
|
+
projectName: project.name,
|
|
71
|
+
health: cached.health?.overall ?? 50,
|
|
72
|
+
healthTrend: cached.health?.trend ?? 'stable',
|
|
73
|
+
sessionCount: cached.sessionCount,
|
|
74
|
+
lastActive: cached.dateRange?.last ?? project.lastActivity,
|
|
75
|
+
currentFocus: cached.currentFocus,
|
|
76
|
+
topChallenges: cached.challenges?.slice(0, 2).map(c => c.title) ?? [],
|
|
77
|
+
topRecommendations: cached.recommendations?.slice(0, 2).map(r => r.title) ?? [],
|
|
78
|
+
hasFullIntelligence: true,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
// Return basic info without full intelligence
|
|
83
|
+
summaries.push({
|
|
84
|
+
projectPath: project.path,
|
|
85
|
+
projectName: project.name,
|
|
86
|
+
health: 50,
|
|
87
|
+
healthTrend: 'stable',
|
|
88
|
+
sessionCount: project.sessionCount,
|
|
89
|
+
lastActive: project.lastActivity,
|
|
90
|
+
topChallenges: [],
|
|
91
|
+
topRecommendations: [],
|
|
92
|
+
hasFullIntelligence: false,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Sort by last activity
|
|
97
|
+
summaries.sort((a, b) => new Date(b.lastActive).getTime() - new Date(a.lastActive).getTime());
|
|
98
|
+
return summaries;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Generate intelligence for a specific project
|
|
102
|
+
*/
|
|
103
|
+
async generateProjectIntelligence(projectPath, force = false) {
|
|
104
|
+
// Check cache first
|
|
105
|
+
if (!force) {
|
|
106
|
+
const cached = await this.getProjectIntelligence(projectPath);
|
|
107
|
+
if (cached && cached.status === 'completed') {
|
|
108
|
+
// Cache valid for 12 hours
|
|
109
|
+
const age = Date.now() - new Date(cached.generatedAt).getTime();
|
|
110
|
+
if (age < 12 * 60 * 60 * 1000) {
|
|
111
|
+
return cached;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const cachePath = this.getProjectCachePath(projectPath);
|
|
116
|
+
const projectName = path.basename(projectPath);
|
|
117
|
+
// Create pending entry
|
|
118
|
+
const pending = {
|
|
119
|
+
projectPath,
|
|
120
|
+
projectName,
|
|
121
|
+
generatedAt: new Date().toISOString(),
|
|
122
|
+
status: 'generating',
|
|
123
|
+
sessionCount: 0,
|
|
124
|
+
totalTokens: 0,
|
|
125
|
+
totalCost: 0,
|
|
126
|
+
dateRange: { first: '', last: '' },
|
|
127
|
+
filesCreated: 0,
|
|
128
|
+
filesEdited: 0,
|
|
129
|
+
};
|
|
130
|
+
await fs.writeFile(cachePath, JSON.stringify(pending, null, 2));
|
|
131
|
+
try {
|
|
132
|
+
// Gather session data for this project
|
|
133
|
+
const { sessions } = await this.sessionService.listSessions({ project: projectPath, limit: 500 });
|
|
134
|
+
if (sessions.length === 0) {
|
|
135
|
+
const empty = {
|
|
136
|
+
...pending,
|
|
137
|
+
status: 'completed',
|
|
138
|
+
sessionCount: 0,
|
|
139
|
+
};
|
|
140
|
+
await fs.writeFile(cachePath, JSON.stringify(empty, null, 2));
|
|
141
|
+
return empty;
|
|
142
|
+
}
|
|
143
|
+
// Load session details (limit to recent 50 for performance)
|
|
144
|
+
const sessionDetails = await Promise.all(sessions.slice(0, 50).map(s => this.sessionService.getSession(s.id)));
|
|
145
|
+
const validSessions = sessionDetails.filter(s => s !== null);
|
|
146
|
+
// Analyze sessions
|
|
147
|
+
const analysis = this.analyzeProjectSessions(validSessions, projectPath);
|
|
148
|
+
// Generate AI narrative if Claude is available
|
|
149
|
+
let aiNarrative = null;
|
|
150
|
+
if (await this.isClaudeAvailable()) {
|
|
151
|
+
aiNarrative = await this.generateAINarrative(analysis, projectName);
|
|
152
|
+
}
|
|
153
|
+
// Build complete intelligence
|
|
154
|
+
const intelligence = {
|
|
155
|
+
projectPath,
|
|
156
|
+
projectName,
|
|
157
|
+
generatedAt: new Date().toISOString(),
|
|
158
|
+
status: 'completed',
|
|
159
|
+
// Summary
|
|
160
|
+
storyNarrative: aiNarrative?.story ?? this.generateBasicNarrative(analysis),
|
|
161
|
+
currentFocus: aiNarrative?.focus ?? analysis.recentTopics[0],
|
|
162
|
+
// Health & Metrics
|
|
163
|
+
health: analysis.health,
|
|
164
|
+
sessionCount: sessions.length,
|
|
165
|
+
totalTokens: analysis.totalTokens,
|
|
166
|
+
totalCost: analysis.totalCost,
|
|
167
|
+
dateRange: analysis.dateRange,
|
|
168
|
+
// Code Analysis
|
|
169
|
+
codeHotspots: analysis.hotspots,
|
|
170
|
+
primaryLanguages: analysis.languages,
|
|
171
|
+
filesCreated: analysis.filesCreated,
|
|
172
|
+
filesEdited: analysis.filesEdited,
|
|
173
|
+
// Timeline & Milestones
|
|
174
|
+
milestones: analysis.milestones,
|
|
175
|
+
recentActivity: analysis.recentActivity,
|
|
176
|
+
// Patterns & Workflows
|
|
177
|
+
workflowPatterns: analysis.workflowPatterns,
|
|
178
|
+
activityPattern: analysis.activityPattern,
|
|
179
|
+
// Challenges
|
|
180
|
+
challenges: analysis.challenges,
|
|
181
|
+
errorPatterns: analysis.errorPatterns,
|
|
182
|
+
// Claude Usage
|
|
183
|
+
claudeEffectiveness: analysis.claudeEffectiveness,
|
|
184
|
+
topTools: analysis.topTools,
|
|
185
|
+
// Recommendations
|
|
186
|
+
recommendations: aiNarrative?.recommendations ?? analysis.basicRecommendations,
|
|
187
|
+
// Quick Stats
|
|
188
|
+
quickStats: analysis.quickStats,
|
|
189
|
+
};
|
|
190
|
+
await fs.writeFile(cachePath, JSON.stringify(intelligence, null, 2));
|
|
191
|
+
return intelligence;
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
const failed = {
|
|
195
|
+
...pending,
|
|
196
|
+
status: 'error',
|
|
197
|
+
error: {
|
|
198
|
+
code: 'UNKNOWN',
|
|
199
|
+
message: error.message,
|
|
200
|
+
recoverable: true,
|
|
201
|
+
suggestion: 'Try regenerating the intelligence',
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
await fs.writeFile(cachePath, JSON.stringify(failed, null, 2));
|
|
205
|
+
return failed;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Analyze sessions to extract project intelligence
|
|
210
|
+
*/
|
|
211
|
+
analyzeProjectSessions(sessions, _projectPath) {
|
|
212
|
+
// Initialize accumulators
|
|
213
|
+
const fileEdits = new Map();
|
|
214
|
+
const toolCalls = new Map();
|
|
215
|
+
const errorMessages = new Map();
|
|
216
|
+
const dailyActivity = new Map();
|
|
217
|
+
const hourlyActivity = new Map();
|
|
218
|
+
const weeklyActivity = new Map();
|
|
219
|
+
const languageUsage = new Map();
|
|
220
|
+
let totalTokens = 0;
|
|
221
|
+
let totalCost = 0;
|
|
222
|
+
let firstDate = '';
|
|
223
|
+
let lastDate = '';
|
|
224
|
+
let filesCreated = 0;
|
|
225
|
+
let filesEdited = 0;
|
|
226
|
+
let totalSuccess = 0;
|
|
227
|
+
let totalErrors = 0;
|
|
228
|
+
const now = new Date();
|
|
229
|
+
const oneWeekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
230
|
+
let sessionsThisWeek = 0;
|
|
231
|
+
let tokensThisWeek = 0;
|
|
232
|
+
let filesChangedThisWeek = 0;
|
|
233
|
+
// Process each session
|
|
234
|
+
for (const session of sessions) {
|
|
235
|
+
if (!session)
|
|
236
|
+
continue;
|
|
237
|
+
const sessionDate = session.startedAt || session.lastMessageAt;
|
|
238
|
+
if (!firstDate || sessionDate < firstDate)
|
|
239
|
+
firstDate = sessionDate;
|
|
240
|
+
if (!lastDate || sessionDate > lastDate)
|
|
241
|
+
lastDate = sessionDate;
|
|
242
|
+
const date = new Date(sessionDate);
|
|
243
|
+
const dateKey = date.toISOString().split('T')[0];
|
|
244
|
+
const hour = date.getHours();
|
|
245
|
+
const dayOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][date.getDay()];
|
|
246
|
+
// Track activity patterns
|
|
247
|
+
dailyActivity.set(dateKey, (dailyActivity.get(dateKey) || 0) + 1);
|
|
248
|
+
hourlyActivity.set(hour, (hourlyActivity.get(hour) || 0) + 1);
|
|
249
|
+
weeklyActivity.set(dayOfWeek, (weeklyActivity.get(dayOfWeek) || 0) + 1);
|
|
250
|
+
// Check if this week
|
|
251
|
+
if (date >= oneWeekAgo) {
|
|
252
|
+
sessionsThisWeek++;
|
|
253
|
+
}
|
|
254
|
+
// Process messages
|
|
255
|
+
for (const msg of session.messages || []) {
|
|
256
|
+
// Track tokens
|
|
257
|
+
if (msg.metadata?.tokens) {
|
|
258
|
+
const tokens = (msg.metadata.tokens.input || 0) + (msg.metadata.tokens.output || 0);
|
|
259
|
+
totalTokens += tokens;
|
|
260
|
+
if (date >= oneWeekAgo)
|
|
261
|
+
tokensThisWeek += tokens;
|
|
262
|
+
// Estimate cost (rough estimate)
|
|
263
|
+
totalCost += tokens * 0.00001;
|
|
264
|
+
}
|
|
265
|
+
// Track tool usage
|
|
266
|
+
if (msg.type === 'tool-use' && msg.metadata?.toolName) {
|
|
267
|
+
const toolName = msg.metadata.toolName;
|
|
268
|
+
const existing = toolCalls.get(toolName) || { total: 0, errors: 0 };
|
|
269
|
+
existing.total++;
|
|
270
|
+
toolCalls.set(toolName, existing);
|
|
271
|
+
// Track file edits
|
|
272
|
+
const input = msg.metadata.toolInput;
|
|
273
|
+
const filePath = (input?.file_path || input?.path || input?.target_file);
|
|
274
|
+
if (filePath && ['Edit', 'Write', 'MultiEdit'].includes(toolName)) {
|
|
275
|
+
const existing = fileEdits.get(filePath) || {
|
|
276
|
+
count: 0,
|
|
277
|
+
lastEdited: sessionDate,
|
|
278
|
+
sessions: new Set(),
|
|
279
|
+
};
|
|
280
|
+
existing.count++;
|
|
281
|
+
existing.sessions.add(session.id);
|
|
282
|
+
if (sessionDate > existing.lastEdited)
|
|
283
|
+
existing.lastEdited = sessionDate;
|
|
284
|
+
fileEdits.set(filePath, existing);
|
|
285
|
+
if (toolName === 'Write')
|
|
286
|
+
filesCreated++;
|
|
287
|
+
else
|
|
288
|
+
filesEdited++;
|
|
289
|
+
if (date >= oneWeekAgo)
|
|
290
|
+
filesChangedThisWeek++;
|
|
291
|
+
// Track language
|
|
292
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
293
|
+
if (ext) {
|
|
294
|
+
languageUsage.set(ext, (languageUsage.get(ext) || 0) + 1);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// Track errors
|
|
299
|
+
if (msg.type === 'tool-result' && msg.content?.includes('Error')) {
|
|
300
|
+
totalErrors++;
|
|
301
|
+
const errorType = this.categorizeError(msg.content);
|
|
302
|
+
const existing = errorMessages.get(errorType) || { count: 0, lastOccurred: sessionDate };
|
|
303
|
+
existing.count++;
|
|
304
|
+
if (sessionDate > existing.lastOccurred)
|
|
305
|
+
existing.lastOccurred = sessionDate;
|
|
306
|
+
errorMessages.set(errorType, existing);
|
|
307
|
+
// Mark tool as having an error
|
|
308
|
+
const prevMsg = session.messages[session.messages.indexOf(msg) - 1];
|
|
309
|
+
if (prevMsg?.metadata?.toolName) {
|
|
310
|
+
const toolData = toolCalls.get(prevMsg.metadata.toolName);
|
|
311
|
+
if (toolData)
|
|
312
|
+
toolData.errors++;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
else if (msg.type === 'tool-result') {
|
|
316
|
+
totalSuccess++;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// Calculate health score
|
|
321
|
+
const errorRate = totalErrors / Math.max(1, totalSuccess + totalErrors);
|
|
322
|
+
const velocity = sessions.length / Math.max(1, this.daysBetween(firstDate, lastDate));
|
|
323
|
+
const churnScore = this.calculateChurnScore(fileEdits);
|
|
324
|
+
const health = {
|
|
325
|
+
overall: Math.round((1 - errorRate) * 30 + // Stability
|
|
326
|
+
Math.min(velocity * 10, 25) + // Velocity
|
|
327
|
+
(1 - Math.min(churnScore / 100, 1)) * 25 + // Maintainability
|
|
328
|
+
20 // Base score
|
|
329
|
+
),
|
|
330
|
+
velocity: Math.min(velocity * 20, 100),
|
|
331
|
+
stability: Math.round((1 - errorRate) * 100),
|
|
332
|
+
maintainability: Math.round((1 - Math.min(churnScore / 100, 1)) * 100),
|
|
333
|
+
completion: Math.round((totalSuccess / Math.max(1, totalSuccess + totalErrors)) * 100),
|
|
334
|
+
trend: this.determineTrend(sessions),
|
|
335
|
+
trendChange: 0,
|
|
336
|
+
};
|
|
337
|
+
// Build hotspots
|
|
338
|
+
const hotspots = Array.from(fileEdits.entries())
|
|
339
|
+
.map(([filePath, data]) => ({
|
|
340
|
+
filePath,
|
|
341
|
+
fileName: path.basename(filePath),
|
|
342
|
+
editCount: data.count,
|
|
343
|
+
lastEdited: data.lastEdited,
|
|
344
|
+
churnScore: data.count * data.sessions.size,
|
|
345
|
+
sessions: data.sessions.size,
|
|
346
|
+
contributionType: 'unknown',
|
|
347
|
+
}))
|
|
348
|
+
.sort((a, b) => b.editCount - a.editCount)
|
|
349
|
+
.slice(0, 10);
|
|
350
|
+
// Build top tools
|
|
351
|
+
const topTools = Array.from(toolCalls.entries())
|
|
352
|
+
.map(([name, data]) => ({ name, count: data.total }))
|
|
353
|
+
.sort((a, b) => b.count - a.count)
|
|
354
|
+
.slice(0, 10);
|
|
355
|
+
// Build languages
|
|
356
|
+
const languages = Array.from(languageUsage.entries())
|
|
357
|
+
.sort((a, b) => b[1] - a[1])
|
|
358
|
+
.slice(0, 5)
|
|
359
|
+
.map(([ext]) => this.extToLanguage(ext));
|
|
360
|
+
// Build error patterns
|
|
361
|
+
const errorPatterns = Array.from(errorMessages.entries())
|
|
362
|
+
.map(([type, data]) => ({ type, count: data.count, lastOccurred: data.lastOccurred }))
|
|
363
|
+
.sort((a, b) => b.count - a.count)
|
|
364
|
+
.slice(0, 5);
|
|
365
|
+
// Build activity pattern
|
|
366
|
+
const activityPattern = {
|
|
367
|
+
peakDays: Array.from(weeklyActivity.entries())
|
|
368
|
+
.map(([day, sessions]) => ({ day, sessions }))
|
|
369
|
+
.sort((a, b) => b.sessions - a.sessions),
|
|
370
|
+
peakHours: Array.from(hourlyActivity.entries())
|
|
371
|
+
.map(([hour, sessions]) => ({ hour, sessions }))
|
|
372
|
+
.sort((a, b) => b.sessions - a.sessions)
|
|
373
|
+
.slice(0, 5),
|
|
374
|
+
averageSessionsPerWeek: sessions.length / Math.max(1, this.daysBetween(firstDate, lastDate) / 7),
|
|
375
|
+
longestStreak: this.calculateStreak(dailyActivity),
|
|
376
|
+
lastActiveStreak: this.calculateCurrentStreak(dailyActivity),
|
|
377
|
+
};
|
|
378
|
+
// Build Claude effectiveness
|
|
379
|
+
const claudeEffectiveness = {
|
|
380
|
+
overallScore: health.completion,
|
|
381
|
+
strengths: this.identifyStrengths(toolCalls),
|
|
382
|
+
weaknesses: this.identifyWeaknesses(toolCalls),
|
|
383
|
+
bestToolUsage: Array.from(toolCalls.entries())
|
|
384
|
+
.filter(([_, d]) => d.errors === 0 && d.total > 3)
|
|
385
|
+
.map(([tool, d]) => ({ tool, successRate: 100 }))
|
|
386
|
+
.slice(0, 3),
|
|
387
|
+
worstToolUsage: Array.from(toolCalls.entries())
|
|
388
|
+
.filter(([_, d]) => d.errors > 0)
|
|
389
|
+
.map(([tool, d]) => ({ tool, errorRate: (d.errors / d.total) * 100 }))
|
|
390
|
+
.sort((a, b) => b.errorRate - a.errorRate)
|
|
391
|
+
.slice(0, 3),
|
|
392
|
+
averageTaskCompletion: health.completion,
|
|
393
|
+
iterationEfficiency: Math.round(100 - errorRate * 50),
|
|
394
|
+
};
|
|
395
|
+
// Build recent activity
|
|
396
|
+
const recentActivity = Array.from(dailyActivity.entries())
|
|
397
|
+
.sort((a, b) => b[0].localeCompare(a[0]))
|
|
398
|
+
.slice(0, 7)
|
|
399
|
+
.map(([date, count]) => ({
|
|
400
|
+
date,
|
|
401
|
+
summary: `${count} session${count > 1 ? 's' : ''}`,
|
|
402
|
+
}));
|
|
403
|
+
// Build basic recommendations
|
|
404
|
+
const basicRecommendations = [];
|
|
405
|
+
if (errorRate > 0.3) {
|
|
406
|
+
basicRecommendations.push({
|
|
407
|
+
id: 'reduce-errors',
|
|
408
|
+
type: 'process',
|
|
409
|
+
priority: 'high',
|
|
410
|
+
title: 'Reduce Error Rate',
|
|
411
|
+
description: 'Your sessions have a high error rate. Consider adding better validation.',
|
|
412
|
+
rationale: `Current error rate is ${(errorRate * 100).toFixed(0)}%`,
|
|
413
|
+
actionItems: ['Review common error patterns', 'Add pre-commit hooks'],
|
|
414
|
+
estimatedImpact: 'Reduce debugging time by 30%',
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
if (hotspots.length > 0 && hotspots[0].editCount > 20) {
|
|
418
|
+
basicRecommendations.push({
|
|
419
|
+
id: 'refactor-hotspot',
|
|
420
|
+
type: 'refactor',
|
|
421
|
+
priority: 'medium',
|
|
422
|
+
title: `Consider Refactoring ${hotspots[0].fileName}`,
|
|
423
|
+
description: 'This file has high churn and may benefit from refactoring.',
|
|
424
|
+
rationale: `Edited ${hotspots[0].editCount} times across ${hotspots[0].sessions} sessions`,
|
|
425
|
+
actionItems: ['Review file complexity', 'Consider splitting into smaller modules'],
|
|
426
|
+
estimatedImpact: 'Reduce maintenance overhead',
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
health,
|
|
431
|
+
totalTokens,
|
|
432
|
+
totalCost,
|
|
433
|
+
dateRange: { first: firstDate, last: lastDate },
|
|
434
|
+
hotspots,
|
|
435
|
+
languages,
|
|
436
|
+
filesCreated,
|
|
437
|
+
filesEdited,
|
|
438
|
+
milestones: [], // Would need more sophisticated analysis
|
|
439
|
+
recentActivity,
|
|
440
|
+
recentTopics: languages,
|
|
441
|
+
workflowPatterns: [], // Would need more sophisticated analysis
|
|
442
|
+
activityPattern,
|
|
443
|
+
challenges: errorPatterns.map((e, i) => ({
|
|
444
|
+
id: `challenge-${i}`,
|
|
445
|
+
title: e.type,
|
|
446
|
+
description: `Encountered ${e.count} times`,
|
|
447
|
+
occurrences: e.count,
|
|
448
|
+
firstSeen: e.lastOccurred,
|
|
449
|
+
lastSeen: e.lastOccurred,
|
|
450
|
+
category: 'error',
|
|
451
|
+
relatedFiles: [],
|
|
452
|
+
resolved: false,
|
|
453
|
+
})),
|
|
454
|
+
errorPatterns,
|
|
455
|
+
claudeEffectiveness,
|
|
456
|
+
topTools,
|
|
457
|
+
basicRecommendations,
|
|
458
|
+
quickStats: {
|
|
459
|
+
sessionsThisWeek,
|
|
460
|
+
tokensThisWeek,
|
|
461
|
+
filesChangedThisWeek,
|
|
462
|
+
milestonesThisMonth: 0,
|
|
463
|
+
},
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Generate AI-powered narrative for the project
|
|
468
|
+
*/
|
|
469
|
+
async generateAINarrative(analysis, projectName) {
|
|
470
|
+
try {
|
|
471
|
+
const context = this.buildAIContext(analysis, projectName);
|
|
472
|
+
const prompt = `You are analyzing a software project's development history. Based on the data, provide a concise analysis.
|
|
473
|
+
|
|
474
|
+
PROJECT: ${projectName}
|
|
475
|
+
|
|
476
|
+
DATA:
|
|
477
|
+
${context}
|
|
478
|
+
|
|
479
|
+
Respond with ONLY a JSON object (no markdown):
|
|
480
|
+
{
|
|
481
|
+
"story": "A 2-3 sentence narrative describing what this project is and its development journey",
|
|
482
|
+
"focus": "One sentence about the current focus or recent work",
|
|
483
|
+
"recommendations": [
|
|
484
|
+
{
|
|
485
|
+
"id": "rec-1",
|
|
486
|
+
"type": "skill|refactor|process|learning|automation",
|
|
487
|
+
"priority": "high|medium|low",
|
|
488
|
+
"title": "Short recommendation title",
|
|
489
|
+
"description": "What to do",
|
|
490
|
+
"rationale": "Why this matters",
|
|
491
|
+
"actionItems": ["Step 1", "Step 2"],
|
|
492
|
+
"estimatedImpact": "Expected benefit"
|
|
493
|
+
}
|
|
494
|
+
]
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
Keep recommendations to 2-3 most impactful ones.`;
|
|
498
|
+
const tempFile = path.join(this.claudeDir.root, `intelligence-temp-${Date.now()}.txt`);
|
|
499
|
+
await fs.writeFile(tempFile, prompt, 'utf-8');
|
|
500
|
+
const { stdout } = await execAsync(`cat "${tempFile}" | claude --print`, {
|
|
501
|
+
timeout: 120000,
|
|
502
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
503
|
+
});
|
|
504
|
+
await fs.unlink(tempFile).catch(() => { });
|
|
505
|
+
// Parse response
|
|
506
|
+
const jsonMatch = stdout.match(/\{[\s\S]*\}/);
|
|
507
|
+
if (!jsonMatch)
|
|
508
|
+
return null;
|
|
509
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
510
|
+
return {
|
|
511
|
+
story: parsed.story || '',
|
|
512
|
+
focus: parsed.focus || '',
|
|
513
|
+
recommendations: parsed.recommendations || [],
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
console.error('[ProjectIntelligence] AI narrative failed:', error);
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Build context for AI analysis
|
|
523
|
+
*/
|
|
524
|
+
buildAIContext(analysis, projectName) {
|
|
525
|
+
return `
|
|
526
|
+
Sessions: ${analysis.health.velocity > 0 ? Math.round(1 / analysis.health.velocity) : 0} total
|
|
527
|
+
Health Score: ${analysis.health.overall}/100
|
|
528
|
+
Languages: ${analysis.languages.join(', ') || 'Unknown'}
|
|
529
|
+
Top Files Changed: ${analysis.hotspots.slice(0, 3).map((h) => h.fileName).join(', ')}
|
|
530
|
+
Error Rate: ${(100 - analysis.health.stability)}%
|
|
531
|
+
Top Tools: ${analysis.topTools.slice(0, 3).map((t) => `${t.name}(${t.count})`).join(', ')}
|
|
532
|
+
Recent Activity: ${analysis.recentActivity.slice(0, 3).map((a) => `${a.date}: ${a.summary}`).join('; ')}
|
|
533
|
+
Peak Activity: ${analysis.activityPattern.peakDays[0]?.day || 'Unknown'} at ${analysis.activityPattern.peakHours[0]?.hour || 0}:00
|
|
534
|
+
`;
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Generate basic narrative without AI
|
|
538
|
+
*/
|
|
539
|
+
generateBasicNarrative(analysis) {
|
|
540
|
+
const languages = analysis.languages.slice(0, 2).join(' and ') || 'various technologies';
|
|
541
|
+
const files = analysis.hotspots.slice(0, 2).map((h) => h.fileName).join(', ');
|
|
542
|
+
return `A project using ${languages}. Most activity focuses on ${files || 'core functionality'}. ` +
|
|
543
|
+
`Health score: ${analysis.health.overall}/100 with ${analysis.health.trend} trend.`;
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Get global intelligence across all projects
|
|
547
|
+
*/
|
|
548
|
+
async getGlobalIntelligence(force = false) {
|
|
549
|
+
const cachePath = path.join(this.claudeDir.root, INTELLIGENCE_CACHE_DIR, 'global.json');
|
|
550
|
+
// Check cache
|
|
551
|
+
if (!force) {
|
|
552
|
+
try {
|
|
553
|
+
const content = await fs.readFile(cachePath, 'utf-8');
|
|
554
|
+
const cached = JSON.parse(content);
|
|
555
|
+
const age = Date.now() - new Date(cached.generatedAt).getTime();
|
|
556
|
+
if (age < 24 * 60 * 60 * 1000 && cached.status === 'completed') {
|
|
557
|
+
return cached;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
catch {
|
|
561
|
+
// No cache
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
// Generate fresh
|
|
565
|
+
const summaries = await this.listProjectIntelligence();
|
|
566
|
+
const projects = await this.sessionService.listProjects();
|
|
567
|
+
const mostActiveProjects = summaries
|
|
568
|
+
.sort((a, b) => b.sessionCount - a.sessionCount)
|
|
569
|
+
.slice(0, 5)
|
|
570
|
+
.map(s => ({ path: s.projectPath, name: s.projectName, sessions: s.sessionCount }));
|
|
571
|
+
const healthiest = summaries.reduce((best, curr) => curr.health > (best?.health ?? 0) ? curr : best, summaries[0]);
|
|
572
|
+
const needsAttention = summaries.reduce((worst, curr) => curr.health < (worst?.health ?? 100) ? curr : worst, summaries[0]);
|
|
573
|
+
const global = {
|
|
574
|
+
generatedAt: new Date().toISOString(),
|
|
575
|
+
status: 'completed',
|
|
576
|
+
developerProfile: `Active across ${projects.length} projects with focus on ${summaries[0]?.projectName || 'development'}`,
|
|
577
|
+
overallNarrative: `Working on ${projects.length} projects. Most active: ${mostActiveProjects[0]?.name || 'N/A'}.`,
|
|
578
|
+
mostActiveProjects,
|
|
579
|
+
projectComparison: {
|
|
580
|
+
healthiest: healthiest?.projectName || 'N/A',
|
|
581
|
+
mostActive: mostActiveProjects[0]?.name || 'N/A',
|
|
582
|
+
needsAttention: needsAttention?.projectName || 'N/A',
|
|
583
|
+
},
|
|
584
|
+
workingPatterns: {
|
|
585
|
+
mostProductiveDay: 'Tuesday', // Would need actual calculation
|
|
586
|
+
mostProductiveHour: 14,
|
|
587
|
+
averageSessionDuration: 30,
|
|
588
|
+
},
|
|
589
|
+
recommendations: [],
|
|
590
|
+
};
|
|
591
|
+
await fs.mkdir(path.dirname(cachePath), { recursive: true });
|
|
592
|
+
await fs.writeFile(cachePath, JSON.stringify(global, null, 2));
|
|
593
|
+
return global;
|
|
594
|
+
}
|
|
595
|
+
// ============================================================================
|
|
596
|
+
// Helper Methods
|
|
597
|
+
// ============================================================================
|
|
598
|
+
async isClaudeAvailable() {
|
|
599
|
+
try {
|
|
600
|
+
await execAsync('which claude');
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
catch {
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
daysBetween(date1, date2) {
|
|
608
|
+
if (!date1 || !date2)
|
|
609
|
+
return 1;
|
|
610
|
+
const d1 = new Date(date1);
|
|
611
|
+
const d2 = new Date(date2);
|
|
612
|
+
return Math.max(1, Math.ceil((d2.getTime() - d1.getTime()) / (1000 * 60 * 60 * 24)));
|
|
613
|
+
}
|
|
614
|
+
calculateChurnScore(fileEdits) {
|
|
615
|
+
let score = 0;
|
|
616
|
+
for (const [_, data] of fileEdits) {
|
|
617
|
+
score += data.count * data.sessions.size;
|
|
618
|
+
}
|
|
619
|
+
return score / Math.max(1, fileEdits.size);
|
|
620
|
+
}
|
|
621
|
+
determineTrend(sessions) {
|
|
622
|
+
if (sessions.length < 5)
|
|
623
|
+
return 'stable';
|
|
624
|
+
// Simple trend based on recent activity
|
|
625
|
+
const sorted = sessions.sort((a, b) => new Date(b.startedAt || b.lastMessageAt).getTime() -
|
|
626
|
+
new Date(a.startedAt || a.lastMessageAt).getTime());
|
|
627
|
+
const recentCount = sorted.slice(0, 5).length;
|
|
628
|
+
const olderCount = sorted.slice(5, 10).length;
|
|
629
|
+
if (recentCount > olderCount * 1.2)
|
|
630
|
+
return 'improving';
|
|
631
|
+
if (recentCount < olderCount * 0.8)
|
|
632
|
+
return 'declining';
|
|
633
|
+
return 'stable';
|
|
634
|
+
}
|
|
635
|
+
calculateStreak(dailyActivity) {
|
|
636
|
+
const dates = Array.from(dailyActivity.keys()).sort();
|
|
637
|
+
let maxStreak = 0;
|
|
638
|
+
let currentStreak = 1;
|
|
639
|
+
for (let i = 1; i < dates.length; i++) {
|
|
640
|
+
const prev = new Date(dates[i - 1]);
|
|
641
|
+
const curr = new Date(dates[i]);
|
|
642
|
+
const diff = (curr.getTime() - prev.getTime()) / (1000 * 60 * 60 * 24);
|
|
643
|
+
if (diff === 1) {
|
|
644
|
+
currentStreak++;
|
|
645
|
+
maxStreak = Math.max(maxStreak, currentStreak);
|
|
646
|
+
}
|
|
647
|
+
else {
|
|
648
|
+
currentStreak = 1;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return maxStreak;
|
|
652
|
+
}
|
|
653
|
+
calculateCurrentStreak(dailyActivity) {
|
|
654
|
+
const dates = Array.from(dailyActivity.keys()).sort().reverse();
|
|
655
|
+
if (dates.length === 0)
|
|
656
|
+
return 0;
|
|
657
|
+
const today = new Date().toISOString().split('T')[0];
|
|
658
|
+
if (dates[0] !== today)
|
|
659
|
+
return 0;
|
|
660
|
+
let streak = 1;
|
|
661
|
+
for (let i = 1; i < dates.length; i++) {
|
|
662
|
+
const prev = new Date(dates[i - 1]);
|
|
663
|
+
const curr = new Date(dates[i]);
|
|
664
|
+
const diff = (prev.getTime() - curr.getTime()) / (1000 * 60 * 60 * 24);
|
|
665
|
+
if (diff === 1) {
|
|
666
|
+
streak++;
|
|
667
|
+
}
|
|
668
|
+
else {
|
|
669
|
+
break;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return streak;
|
|
673
|
+
}
|
|
674
|
+
categorizeError(content) {
|
|
675
|
+
if (content.includes('ENOENT') || content.includes('not found'))
|
|
676
|
+
return 'File Not Found';
|
|
677
|
+
if (content.includes('permission') || content.includes('EACCES'))
|
|
678
|
+
return 'Permission Error';
|
|
679
|
+
if (content.includes('syntax') || content.includes('parse'))
|
|
680
|
+
return 'Syntax Error';
|
|
681
|
+
if (content.includes('type') || content.includes('TypeScript'))
|
|
682
|
+
return 'Type Error';
|
|
683
|
+
if (content.includes('timeout'))
|
|
684
|
+
return 'Timeout';
|
|
685
|
+
if (content.includes('network') || content.includes('ECONNREFUSED'))
|
|
686
|
+
return 'Network Error';
|
|
687
|
+
return 'Other Error';
|
|
688
|
+
}
|
|
689
|
+
extToLanguage(ext) {
|
|
690
|
+
const map = {
|
|
691
|
+
'.ts': 'TypeScript',
|
|
692
|
+
'.tsx': 'TypeScript',
|
|
693
|
+
'.js': 'JavaScript',
|
|
694
|
+
'.jsx': 'JavaScript',
|
|
695
|
+
'.py': 'Python',
|
|
696
|
+
'.go': 'Go',
|
|
697
|
+
'.rs': 'Rust',
|
|
698
|
+
'.java': 'Java',
|
|
699
|
+
'.rb': 'Ruby',
|
|
700
|
+
'.swift': 'Swift',
|
|
701
|
+
'.kt': 'Kotlin',
|
|
702
|
+
'.css': 'CSS',
|
|
703
|
+
'.scss': 'Sass',
|
|
704
|
+
'.html': 'HTML',
|
|
705
|
+
'.vue': 'Vue',
|
|
706
|
+
'.svelte': 'Svelte',
|
|
707
|
+
};
|
|
708
|
+
return map[ext] || ext.replace('.', '').toUpperCase();
|
|
709
|
+
}
|
|
710
|
+
identifyStrengths(toolCalls) {
|
|
711
|
+
const strengths = [];
|
|
712
|
+
const totalCalls = Array.from(toolCalls.values()).reduce((sum, d) => sum + d.total, 0);
|
|
713
|
+
for (const [tool, data] of toolCalls) {
|
|
714
|
+
if (data.errors === 0 && data.total > totalCalls * 0.1) {
|
|
715
|
+
strengths.push(`Strong ${tool} usage`);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return strengths.slice(0, 3);
|
|
719
|
+
}
|
|
720
|
+
identifyWeaknesses(toolCalls) {
|
|
721
|
+
const weaknesses = [];
|
|
722
|
+
for (const [tool, data] of toolCalls) {
|
|
723
|
+
const errorRate = data.errors / data.total;
|
|
724
|
+
if (errorRate > 0.3 && data.total > 5) {
|
|
725
|
+
weaknesses.push(`High error rate with ${tool}`);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return weaknesses.slice(0, 3);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
//# sourceMappingURL=project-intelligence.js.map
|