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,1088 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tech Stack Service - Aggregates technology usage across all Claude Code sessions
|
|
3
|
+
*
|
|
4
|
+
* This service scans session files to detect:
|
|
5
|
+
* - Programming languages used (via file extensions)
|
|
6
|
+
* - Frameworks and libraries (via file patterns and imports)
|
|
7
|
+
* - Cloud providers and DevOps tools
|
|
8
|
+
* - Databases and infrastructure
|
|
9
|
+
*
|
|
10
|
+
* It tracks Claude's contributions by analyzing Edit/Write tool calls.
|
|
11
|
+
* Also provides AI-powered insights via Claude CLI.
|
|
12
|
+
*/
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import fs from 'node:fs/promises';
|
|
15
|
+
import os from 'node:os';
|
|
16
|
+
import { exec } from 'node:child_process';
|
|
17
|
+
const EXTENSION_MAPPINGS = {
|
|
18
|
+
// Languages
|
|
19
|
+
'.ts': { name: 'TypeScript', type: 'language' },
|
|
20
|
+
'.tsx': { name: 'TypeScript', type: 'language' },
|
|
21
|
+
'.js': { name: 'JavaScript', type: 'language' },
|
|
22
|
+
'.jsx': { name: 'JavaScript', type: 'language' },
|
|
23
|
+
'.mjs': { name: 'JavaScript', type: 'language' },
|
|
24
|
+
'.cjs': { name: 'JavaScript', type: 'language' },
|
|
25
|
+
'.py': { name: 'Python', type: 'language' },
|
|
26
|
+
'.go': { name: 'Go', type: 'language' },
|
|
27
|
+
'.rs': { name: 'Rust', type: 'language' },
|
|
28
|
+
'.rb': { name: 'Ruby', type: 'language' },
|
|
29
|
+
'.java': { name: 'Java', type: 'language' },
|
|
30
|
+
'.kt': { name: 'Kotlin', type: 'language' },
|
|
31
|
+
'.swift': { name: 'Swift', type: 'language' },
|
|
32
|
+
'.c': { name: 'C', type: 'language' },
|
|
33
|
+
'.cpp': { name: 'C++', type: 'language' },
|
|
34
|
+
'.cc': { name: 'C++', type: 'language' },
|
|
35
|
+
'.h': { name: 'C/C++', type: 'language' },
|
|
36
|
+
'.hpp': { name: 'C++', type: 'language' },
|
|
37
|
+
'.cs': { name: 'C#', type: 'language' },
|
|
38
|
+
'.php': { name: 'PHP', type: 'language' },
|
|
39
|
+
'.scala': { name: 'Scala', type: 'language' },
|
|
40
|
+
'.clj': { name: 'Clojure', type: 'language' },
|
|
41
|
+
'.ex': { name: 'Elixir', type: 'language' },
|
|
42
|
+
'.exs': { name: 'Elixir', type: 'language' },
|
|
43
|
+
'.erl': { name: 'Erlang', type: 'language' },
|
|
44
|
+
'.hs': { name: 'Haskell', type: 'language' },
|
|
45
|
+
'.lua': { name: 'Lua', type: 'language' },
|
|
46
|
+
'.r': { name: 'R', type: 'language' },
|
|
47
|
+
'.dart': { name: 'Dart', type: 'language' },
|
|
48
|
+
'.vue': { name: 'Vue.js', type: 'framework' },
|
|
49
|
+
'.svelte': { name: 'Svelte', type: 'framework' },
|
|
50
|
+
// Config/Data
|
|
51
|
+
'.json': { name: 'JSON', type: 'tool' },
|
|
52
|
+
'.yaml': { name: 'YAML', type: 'tool' },
|
|
53
|
+
'.yml': { name: 'YAML', type: 'tool' },
|
|
54
|
+
'.toml': { name: 'TOML', type: 'tool' },
|
|
55
|
+
'.xml': { name: 'XML', type: 'tool' },
|
|
56
|
+
'.ini': { name: 'INI', type: 'tool' },
|
|
57
|
+
'.env': { name: 'Environment', type: 'tool' },
|
|
58
|
+
// Web
|
|
59
|
+
'.html': { name: 'HTML', type: 'language' },
|
|
60
|
+
'.htm': { name: 'HTML', type: 'language' },
|
|
61
|
+
'.css': { name: 'CSS', type: 'language' },
|
|
62
|
+
'.scss': { name: 'Sass', type: 'tool' },
|
|
63
|
+
'.sass': { name: 'Sass', type: 'tool' },
|
|
64
|
+
'.less': { name: 'Less', type: 'tool' },
|
|
65
|
+
// DevOps/Cloud
|
|
66
|
+
'.tf': { name: 'Terraform', type: 'devops' },
|
|
67
|
+
'.tfvars': { name: 'Terraform', type: 'devops' },
|
|
68
|
+
'.hcl': { name: 'HashiCorp', type: 'devops' },
|
|
69
|
+
// Documentation
|
|
70
|
+
'.md': { name: 'Markdown', type: 'tool' },
|
|
71
|
+
'.mdx': { name: 'MDX', type: 'tool' },
|
|
72
|
+
'.rst': { name: 'reStructuredText', type: 'tool' },
|
|
73
|
+
// Shell
|
|
74
|
+
'.sh': { name: 'Shell', type: 'tool' },
|
|
75
|
+
'.bash': { name: 'Bash', type: 'tool' },
|
|
76
|
+
'.zsh': { name: 'Zsh', type: 'tool' },
|
|
77
|
+
'.fish': { name: 'Fish', type: 'tool' },
|
|
78
|
+
'.ps1': { name: 'PowerShell', type: 'tool' },
|
|
79
|
+
// Database
|
|
80
|
+
'.sql': { name: 'SQL', type: 'database' },
|
|
81
|
+
'.prisma': { name: 'Prisma', type: 'database' },
|
|
82
|
+
// Testing
|
|
83
|
+
'.test.ts': { name: 'Jest', type: 'testing' },
|
|
84
|
+
'.spec.ts': { name: 'Jest', type: 'testing' },
|
|
85
|
+
'.test.js': { name: 'Jest', type: 'testing' },
|
|
86
|
+
'.spec.js': { name: 'Jest', type: 'testing' },
|
|
87
|
+
};
|
|
88
|
+
// File path pattern detection
|
|
89
|
+
const PATH_PATTERNS = [
|
|
90
|
+
// Frameworks
|
|
91
|
+
{ pattern: /next\.config\.(js|ts|mjs)$/, tech: { name: 'Next.js', type: 'framework' } },
|
|
92
|
+
{ pattern: /vite\.config\.(js|ts|mjs)$/, tech: { name: 'Vite', type: 'tool' } },
|
|
93
|
+
{ pattern: /webpack\.config\.(js|ts)$/, tech: { name: 'Webpack', type: 'tool' } },
|
|
94
|
+
{ pattern: /tailwind\.config\.(js|ts)$/, tech: { name: 'Tailwind CSS', type: 'framework' } },
|
|
95
|
+
{ pattern: /postcss\.config\.(js|ts)$/, tech: { name: 'PostCSS', type: 'tool' } },
|
|
96
|
+
{ pattern: /tsconfig\.json$/, tech: { name: 'TypeScript', type: 'language' } },
|
|
97
|
+
{ pattern: /package\.json$/, tech: { name: 'Node.js', type: 'tool' } },
|
|
98
|
+
{ pattern: /cargo\.toml$/i, tech: { name: 'Rust', type: 'language' } },
|
|
99
|
+
{ pattern: /go\.mod$/, tech: { name: 'Go', type: 'language' } },
|
|
100
|
+
{ pattern: /requirements\.txt$/, tech: { name: 'Python', type: 'language' } },
|
|
101
|
+
{ pattern: /pyproject\.toml$/, tech: { name: 'Python', type: 'language' } },
|
|
102
|
+
{ pattern: /gemfile$/i, tech: { name: 'Ruby', type: 'language' } },
|
|
103
|
+
{ pattern: /pom\.xml$/, tech: { name: 'Maven', type: 'tool' } },
|
|
104
|
+
{ pattern: /build\.gradle(\.kts)?$/, tech: { name: 'Gradle', type: 'tool' } },
|
|
105
|
+
// DevOps
|
|
106
|
+
{ pattern: /dockerfile$/i, tech: { name: 'Docker', type: 'devops' } },
|
|
107
|
+
{ pattern: /docker-compose\.(yml|yaml)$/i, tech: { name: 'Docker Compose', type: 'devops' } },
|
|
108
|
+
{ pattern: /\.github\/workflows\//, tech: { name: 'GitHub Actions', type: 'devops' } },
|
|
109
|
+
{ pattern: /\.gitlab-ci\.yml$/, tech: { name: 'GitLab CI', type: 'devops' } },
|
|
110
|
+
{ pattern: /jenkinsfile$/i, tech: { name: 'Jenkins', type: 'devops' } },
|
|
111
|
+
{ pattern: /kubernetes|k8s/i, tech: { name: 'Kubernetes', type: 'devops' } },
|
|
112
|
+
{ pattern: /helm/i, tech: { name: 'Helm', type: 'devops' } },
|
|
113
|
+
// Cloud
|
|
114
|
+
{ pattern: /\.aws\//, tech: { name: 'AWS', type: 'cloud' } },
|
|
115
|
+
{ pattern: /serverless\.(yml|yaml|json)$/i, tech: { name: 'Serverless', type: 'cloud' } },
|
|
116
|
+
{ pattern: /cloudformation/i, tech: { name: 'AWS CloudFormation', type: 'cloud' } },
|
|
117
|
+
{ pattern: /\.gcloud\//, tech: { name: 'Google Cloud', type: 'cloud' } },
|
|
118
|
+
{ pattern: /firebase\.(json|rc)$/i, tech: { name: 'Firebase', type: 'cloud' } },
|
|
119
|
+
{ pattern: /vercel\.json$/i, tech: { name: 'Vercel', type: 'cloud' } },
|
|
120
|
+
{ pattern: /netlify\.toml$/i, tech: { name: 'Netlify', type: 'cloud' } },
|
|
121
|
+
// Testing
|
|
122
|
+
{ pattern: /jest\.config\.(js|ts|json)$/, tech: { name: 'Jest', type: 'testing' } },
|
|
123
|
+
{ pattern: /vitest\.config\.(js|ts)$/, tech: { name: 'Vitest', type: 'testing' } },
|
|
124
|
+
{ pattern: /playwright\.config\.(js|ts)$/, tech: { name: 'Playwright', type: 'testing' } },
|
|
125
|
+
{ pattern: /cypress\.config\.(js|ts)$/, tech: { name: 'Cypress', type: 'testing' } },
|
|
126
|
+
{ pattern: /pytest\.ini$/, tech: { name: 'Pytest', type: 'testing' } },
|
|
127
|
+
// Databases
|
|
128
|
+
{ pattern: /prisma\/schema\.prisma$/, tech: { name: 'Prisma', type: 'database' } },
|
|
129
|
+
{ pattern: /drizzle\.config\.(js|ts)$/, tech: { name: 'Drizzle', type: 'database' } },
|
|
130
|
+
{ pattern: /migrations\//, tech: { name: 'Database Migrations', type: 'database' } },
|
|
131
|
+
];
|
|
132
|
+
const CACHE_VERSION = 1;
|
|
133
|
+
export class TechStackService {
|
|
134
|
+
claudeDir;
|
|
135
|
+
cacheFile;
|
|
136
|
+
cache = null;
|
|
137
|
+
constructor(claudeDir) {
|
|
138
|
+
this.claudeDir = claudeDir;
|
|
139
|
+
this.cacheFile = path.join(claudeDir.root, 'tech-stack-cache.json');
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Get the global tech stack summary (uses cache if available)
|
|
143
|
+
*/
|
|
144
|
+
async getSummary(forceRefresh = false) {
|
|
145
|
+
if (!forceRefresh && this.cache) {
|
|
146
|
+
return this.cache;
|
|
147
|
+
}
|
|
148
|
+
// Try loading from disk cache
|
|
149
|
+
if (!forceRefresh) {
|
|
150
|
+
const cached = await this.loadCache();
|
|
151
|
+
if (cached) {
|
|
152
|
+
this.cache = cached;
|
|
153
|
+
return cached;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Generate fresh
|
|
157
|
+
return this.refresh();
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Force refresh the tech stack data by scanning all sessions
|
|
161
|
+
*/
|
|
162
|
+
async refresh() {
|
|
163
|
+
console.log('[TechStack] Starting full scan...');
|
|
164
|
+
const techMap = new Map();
|
|
165
|
+
const projectDirs = await this.claudeDir.listProjects();
|
|
166
|
+
let totalSessions = 0;
|
|
167
|
+
let totalFiles = 0;
|
|
168
|
+
for (const projectDir of projectDirs) {
|
|
169
|
+
const projectPath = this.claudeDir.projectDirToPath(projectDir);
|
|
170
|
+
const sessionFiles = await this.claudeDir.listSessionFiles(projectDir);
|
|
171
|
+
for (const sessionFile of sessionFiles) {
|
|
172
|
+
totalSessions++;
|
|
173
|
+
const messages = await this.parseSessionFile(sessionFile);
|
|
174
|
+
for (const msg of messages) {
|
|
175
|
+
this.processMessage(msg, projectPath, techMap);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Convert accumulator to entries
|
|
180
|
+
const technologies = [];
|
|
181
|
+
for (const [name, acc] of techMap) {
|
|
182
|
+
totalFiles += acc.files.size;
|
|
183
|
+
technologies.push({
|
|
184
|
+
name,
|
|
185
|
+
type: acc.type,
|
|
186
|
+
fileExtensions: Array.from(acc.extensions),
|
|
187
|
+
usageCount: acc.usageCount,
|
|
188
|
+
filesModified: acc.files.size,
|
|
189
|
+
projectsUsed: Array.from(acc.projects),
|
|
190
|
+
firstSeen: acc.firstSeen,
|
|
191
|
+
lastUsed: acc.lastUsed,
|
|
192
|
+
proficiency: this.calculateProficiency(acc),
|
|
193
|
+
claudeContributions: acc.claudeContributions,
|
|
194
|
+
patterns: [],
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
// Sort by usage
|
|
198
|
+
technologies.sort((a, b) => b.usageCount - a.usageCount);
|
|
199
|
+
// Extract top items by type
|
|
200
|
+
const languages = technologies.filter(t => t.type === 'language');
|
|
201
|
+
const frameworks = technologies.filter(t => t.type === 'framework');
|
|
202
|
+
const cloudProviders = technologies.filter(t => t.type === 'cloud');
|
|
203
|
+
const devOpsTools = technologies.filter(t => t.type === 'devops');
|
|
204
|
+
const databases = technologies.filter(t => t.type === 'database');
|
|
205
|
+
const summary = {
|
|
206
|
+
generatedAt: new Date().toISOString(),
|
|
207
|
+
version: CACHE_VERSION,
|
|
208
|
+
totalProjects: projectDirs.length,
|
|
209
|
+
totalSessions,
|
|
210
|
+
totalFiles,
|
|
211
|
+
technologies,
|
|
212
|
+
primaryLanguages: languages.slice(0, 3).map(t => t.name),
|
|
213
|
+
topFrameworks: frameworks.slice(0, 3).map(t => t.name),
|
|
214
|
+
cloudProviders: cloudProviders.map(t => t.name),
|
|
215
|
+
devOpsTools: devOpsTools.map(t => t.name),
|
|
216
|
+
databases: databases.map(t => t.name),
|
|
217
|
+
recommendations: this.generateRecommendations(technologies),
|
|
218
|
+
insights: {
|
|
219
|
+
mostActiveProject: this.findMostActiveProject(technologies),
|
|
220
|
+
mostUsedTech: technologies[0]?.name || 'None',
|
|
221
|
+
growthAreas: this.findGrowthAreas(technologies),
|
|
222
|
+
expertise: technologies.filter(t => t.proficiency === 'expert').map(t => t.name),
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
// Save cache
|
|
226
|
+
await this.saveCache(summary);
|
|
227
|
+
this.cache = summary;
|
|
228
|
+
console.log(`[TechStack] Scan complete. Found ${technologies.length} technologies across ${totalSessions} sessions.`);
|
|
229
|
+
return summary;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Get tech stack breakdown for a specific project
|
|
233
|
+
*/
|
|
234
|
+
async getProjectBreakdown(projectPath) {
|
|
235
|
+
const projectDirs = await this.claudeDir.listProjects();
|
|
236
|
+
const projectDir = projectDirs.find(d => this.claudeDir.projectDirToPath(d) === projectPath);
|
|
237
|
+
if (!projectDir) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
const techMap = new Map();
|
|
241
|
+
const extensionCounts = new Map();
|
|
242
|
+
let totalEdits = 0;
|
|
243
|
+
let totalReads = 0;
|
|
244
|
+
let totalWrites = 0;
|
|
245
|
+
const sessionFiles = await this.claudeDir.listSessionFiles(projectDir);
|
|
246
|
+
for (const sessionFile of sessionFiles) {
|
|
247
|
+
const messages = await this.parseSessionFile(sessionFile);
|
|
248
|
+
for (const msg of messages) {
|
|
249
|
+
const result = this.processMessage(msg, projectPath, techMap);
|
|
250
|
+
if (result) {
|
|
251
|
+
// Track extension counts
|
|
252
|
+
const ext = path.extname(result.filePath).toLowerCase();
|
|
253
|
+
if (ext) {
|
|
254
|
+
extensionCounts.set(ext, (extensionCounts.get(ext) || 0) + 1);
|
|
255
|
+
}
|
|
256
|
+
// Track activity types
|
|
257
|
+
if (result.toolName === 'Edit' || result.toolName === 'MultiEdit') {
|
|
258
|
+
totalEdits++;
|
|
259
|
+
}
|
|
260
|
+
else if (result.toolName === 'Read' || result.toolName === 'View') {
|
|
261
|
+
totalReads++;
|
|
262
|
+
}
|
|
263
|
+
else if (result.toolName === 'Write') {
|
|
264
|
+
totalWrites++;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// Convert to entries
|
|
270
|
+
const technologies = [];
|
|
271
|
+
for (const [name, acc] of techMap) {
|
|
272
|
+
technologies.push({
|
|
273
|
+
name,
|
|
274
|
+
type: acc.type,
|
|
275
|
+
fileExtensions: Array.from(acc.extensions),
|
|
276
|
+
usageCount: acc.usageCount,
|
|
277
|
+
filesModified: acc.files.size,
|
|
278
|
+
projectsUsed: [projectPath],
|
|
279
|
+
firstSeen: acc.firstSeen,
|
|
280
|
+
lastUsed: acc.lastUsed,
|
|
281
|
+
proficiency: this.calculateProficiency(acc),
|
|
282
|
+
claudeContributions: acc.claudeContributions,
|
|
283
|
+
patterns: [],
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
technologies.sort((a, b) => b.usageCount - a.usageCount);
|
|
287
|
+
// Calculate file breakdown percentages
|
|
288
|
+
const totalExtensionCount = Array.from(extensionCounts.values()).reduce((a, b) => a + b, 0);
|
|
289
|
+
const fileBreakdown = Array.from(extensionCounts.entries())
|
|
290
|
+
.map(([extension, count]) => ({
|
|
291
|
+
extension,
|
|
292
|
+
count,
|
|
293
|
+
percentage: totalExtensionCount > 0 ? (count / totalExtensionCount) * 100 : 0,
|
|
294
|
+
}))
|
|
295
|
+
.sort((a, b) => b.count - a.count);
|
|
296
|
+
return {
|
|
297
|
+
projectPath,
|
|
298
|
+
projectName: this.claudeDir.projectDirToName(projectDir),
|
|
299
|
+
generatedAt: new Date().toISOString(),
|
|
300
|
+
technologies,
|
|
301
|
+
fileBreakdown,
|
|
302
|
+
claudeActivity: {
|
|
303
|
+
totalEdits,
|
|
304
|
+
totalReads,
|
|
305
|
+
totalWrites,
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Get deep insights including patterns, correlations, evolution, and similarity
|
|
311
|
+
*/
|
|
312
|
+
async getDeepInsights(forceRefresh = false) {
|
|
313
|
+
// First get the base summary
|
|
314
|
+
const summary = await this.getSummary(forceRefresh);
|
|
315
|
+
// Build project -> technologies mapping for analysis
|
|
316
|
+
const projectTechMap = this.buildProjectTechMap(summary.technologies);
|
|
317
|
+
// Generate deep insights
|
|
318
|
+
const stackPatterns = this.detectStackPatterns(summary.technologies, projectTechMap);
|
|
319
|
+
const correlations = this.calculateCorrelations(summary.technologies, projectTechMap);
|
|
320
|
+
const evolution = this.analyzeEvolution(summary.technologies);
|
|
321
|
+
const projectSimilarity = this.computeProjectSimilarity(projectTechMap);
|
|
322
|
+
// Generate highlights
|
|
323
|
+
const highlights = this.generateHighlights(summary, stackPatterns, correlations, evolution);
|
|
324
|
+
return {
|
|
325
|
+
summary,
|
|
326
|
+
stackPatterns,
|
|
327
|
+
correlations,
|
|
328
|
+
evolution,
|
|
329
|
+
projectSimilarity,
|
|
330
|
+
highlights,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Generate AI-powered tech stack insight using Claude CLI
|
|
335
|
+
*/
|
|
336
|
+
async generateAIInsight(force = false) {
|
|
337
|
+
const cacheFile = path.join(this.claudeDir.root, 'tech-stack-ai-insight.json');
|
|
338
|
+
// Check for cached insight
|
|
339
|
+
if (!force) {
|
|
340
|
+
try {
|
|
341
|
+
const cached = await fs.readFile(cacheFile, 'utf-8');
|
|
342
|
+
const insight = JSON.parse(cached);
|
|
343
|
+
// Cache valid for 24 hours
|
|
344
|
+
const age = Date.now() - new Date(insight.generatedAt).getTime();
|
|
345
|
+
if (age < 24 * 60 * 60 * 1000 && insight.status === 'completed') {
|
|
346
|
+
return insight;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
// No cache or invalid
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
// Create pending insight
|
|
354
|
+
const pendingInsight = {
|
|
355
|
+
id: `tech-ai-${Date.now()}`,
|
|
356
|
+
generatedAt: new Date().toISOString(),
|
|
357
|
+
status: 'generating',
|
|
358
|
+
};
|
|
359
|
+
await fs.writeFile(cacheFile, JSON.stringify(pendingInsight, null, 2));
|
|
360
|
+
try {
|
|
361
|
+
// Check if Claude is available
|
|
362
|
+
const claudeAvailable = await this.isClaudeCodeAvailable();
|
|
363
|
+
if (!claudeAvailable) {
|
|
364
|
+
const errorInsight = {
|
|
365
|
+
...pendingInsight,
|
|
366
|
+
status: 'error',
|
|
367
|
+
error: {
|
|
368
|
+
code: 'CLAUDE_NOT_FOUND',
|
|
369
|
+
message: 'Claude Code CLI is not installed',
|
|
370
|
+
recoverable: false,
|
|
371
|
+
suggestion: 'Install Claude Code: npm install -g @anthropic-ai/claude-code',
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
await fs.writeFile(cacheFile, JSON.stringify(errorInsight, null, 2));
|
|
375
|
+
return errorInsight;
|
|
376
|
+
}
|
|
377
|
+
// Get the deep insights data
|
|
378
|
+
const deepInsights = await this.getDeepInsights();
|
|
379
|
+
// Build context for Claude
|
|
380
|
+
const context = this.buildAIContext(deepInsights);
|
|
381
|
+
// Call Claude
|
|
382
|
+
console.log('[TechStack] Calling Claude for AI insight...');
|
|
383
|
+
const response = await this.callClaudeCode(context);
|
|
384
|
+
// Parse response
|
|
385
|
+
const parsed = this.parseAIResponse(response);
|
|
386
|
+
const completedInsight = {
|
|
387
|
+
id: pendingInsight.id,
|
|
388
|
+
generatedAt: pendingInsight.generatedAt,
|
|
389
|
+
status: 'completed',
|
|
390
|
+
...parsed,
|
|
391
|
+
};
|
|
392
|
+
await fs.writeFile(cacheFile, JSON.stringify(completedInsight, null, 2));
|
|
393
|
+
console.log('[TechStack] AI insight generated successfully');
|
|
394
|
+
return completedInsight;
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
const errorInsight = {
|
|
398
|
+
...pendingInsight,
|
|
399
|
+
status: 'error',
|
|
400
|
+
error: {
|
|
401
|
+
code: 'UNKNOWN',
|
|
402
|
+
message: error.message,
|
|
403
|
+
recoverable: true,
|
|
404
|
+
suggestion: 'Try generating the insight again',
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
await fs.writeFile(cacheFile, JSON.stringify(errorInsight, null, 2));
|
|
408
|
+
return errorInsight;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Get cached AI insight without regenerating
|
|
413
|
+
*/
|
|
414
|
+
async getAIInsight() {
|
|
415
|
+
const cacheFile = path.join(this.claudeDir.root, 'tech-stack-ai-insight.json');
|
|
416
|
+
try {
|
|
417
|
+
const cached = await fs.readFile(cacheFile, 'utf-8');
|
|
418
|
+
return JSON.parse(cached);
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Check if Claude Code CLI is available
|
|
426
|
+
*/
|
|
427
|
+
async isClaudeCodeAvailable() {
|
|
428
|
+
return new Promise((resolve) => {
|
|
429
|
+
exec('which claude', (error) => {
|
|
430
|
+
resolve(!error);
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Build context string for Claude AI analysis
|
|
436
|
+
*/
|
|
437
|
+
buildAIContext(insights) {
|
|
438
|
+
const { summary, stackPatterns, correlations, evolution, projectSimilarity, highlights } = insights;
|
|
439
|
+
let context = `# Tech Stack Analysis Request
|
|
440
|
+
|
|
441
|
+
## Overview
|
|
442
|
+
- Total Technologies: ${summary.technologies.length}
|
|
443
|
+
- Total Projects: ${summary.totalProjects}
|
|
444
|
+
- Total Sessions: ${summary.totalSessions}
|
|
445
|
+
- Files Touched: ${summary.totalFiles}
|
|
446
|
+
|
|
447
|
+
## Primary Languages
|
|
448
|
+
${summary.primaryLanguages.join(', ') || 'None detected'}
|
|
449
|
+
|
|
450
|
+
## Top Frameworks
|
|
451
|
+
${summary.topFrameworks.join(', ') || 'None detected'}
|
|
452
|
+
|
|
453
|
+
## All Technologies (by usage)
|
|
454
|
+
${summary.technologies.slice(0, 20).map(t => `- ${t.name} (${t.type}): ${t.usageCount} uses, ${t.filesModified} files, proficiency: ${t.proficiency}`).join('\n')}
|
|
455
|
+
|
|
456
|
+
## Detected Stack Patterns
|
|
457
|
+
${stackPatterns.map(p => `- ${p.name}: ${p.technologies.join(', ')} (${p.projectCount} projects)`).join('\n') || 'No patterns detected'}
|
|
458
|
+
|
|
459
|
+
## Technology Correlations (tech pairs that appear together)
|
|
460
|
+
${correlations.slice(0, 10).map(c => `- ${c.tech1} + ${c.tech2}: ${c.correlation}% correlation (${c.strength})`).join('\n') || 'Insufficient data'}
|
|
461
|
+
|
|
462
|
+
## Usage Trends
|
|
463
|
+
${evolution.slice(0, 10).map(e => `- ${e.tech}: ${e.trend} (${e.changePercent > 0 ? '+' : ''}${e.changePercent}%)`).join('\n') || 'No trends detected'}
|
|
464
|
+
|
|
465
|
+
## Similar Projects
|
|
466
|
+
${projectSimilarity.slice(0, 5).map(s => `- ${s.project1Name} ↔ ${s.project2Name}: ${s.similarity}% similar (${s.sharedTechnologies.join(', ')})`).join('\n') || 'No similar projects found'}
|
|
467
|
+
|
|
468
|
+
## Current Highlights
|
|
469
|
+
- Shared Foundations: ${highlights.sharedFoundations}
|
|
470
|
+
- Dominant Pattern: ${highlights.dominantPattern}
|
|
471
|
+
- Rising Tech: ${highlights.risingTech}
|
|
472
|
+
- Tech Diversity Score: ${highlights.techDiversity}%
|
|
473
|
+
`;
|
|
474
|
+
return context;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Call Claude Code CLI with context
|
|
478
|
+
*/
|
|
479
|
+
async callClaudeCode(context) {
|
|
480
|
+
const prompt = `You are an expert developer coach and technology advisor. Analyze this developer's tech stack data and provide personalized insights.
|
|
481
|
+
|
|
482
|
+
IMPORTANT: Return ONLY a valid JSON object. No markdown, no explanation, no code blocks - just the raw JSON.
|
|
483
|
+
|
|
484
|
+
The JSON must follow this exact structure:
|
|
485
|
+
{
|
|
486
|
+
"overview": "A 2-3 sentence narrative overview of this developer's tech profile",
|
|
487
|
+
"developerProfile": "A single sentence describing what type of developer they are (e.g., 'Full-stack TypeScript specialist with strong React ecosystem expertise')",
|
|
488
|
+
"stackHealth": 75,
|
|
489
|
+
|
|
490
|
+
"stackPatterns": [
|
|
491
|
+
{
|
|
492
|
+
"id": "unique-pattern-id",
|
|
493
|
+
"name": "Pattern Name (e.g., 'Modern Full-Stack TypeScript')",
|
|
494
|
+
"description": "Brief description of what this pattern represents",
|
|
495
|
+
"technologies": ["Tech1", "Tech2", "Tech3"],
|
|
496
|
+
"icon": "layers|code|database|cloud|cpu|globe"
|
|
497
|
+
}
|
|
498
|
+
],
|
|
499
|
+
|
|
500
|
+
"strengths": [
|
|
501
|
+
{
|
|
502
|
+
"title": "Strong in X",
|
|
503
|
+
"description": "Explanation of why this is a strength",
|
|
504
|
+
"technologies": ["Tech1", "Tech2"]
|
|
505
|
+
}
|
|
506
|
+
],
|
|
507
|
+
|
|
508
|
+
"blindSpots": [
|
|
509
|
+
{
|
|
510
|
+
"title": "Area to improve",
|
|
511
|
+
"description": "Why this matters",
|
|
512
|
+
"suggestion": "Concrete action to take"
|
|
513
|
+
}
|
|
514
|
+
],
|
|
515
|
+
|
|
516
|
+
"learningPath": [
|
|
517
|
+
{
|
|
518
|
+
"skill": "Technology or skill to learn",
|
|
519
|
+
"reason": "Why this would benefit them",
|
|
520
|
+
"priority": "high|medium|low"
|
|
521
|
+
}
|
|
522
|
+
],
|
|
523
|
+
|
|
524
|
+
"technicalDebt": [
|
|
525
|
+
{
|
|
526
|
+
"issue": "Potential issue",
|
|
527
|
+
"impact": "How it affects them",
|
|
528
|
+
"fix": "How to address it"
|
|
529
|
+
}
|
|
530
|
+
],
|
|
531
|
+
|
|
532
|
+
"architectureInsights": [
|
|
533
|
+
{
|
|
534
|
+
"title": "Pattern observed",
|
|
535
|
+
"observation": "What you noticed",
|
|
536
|
+
"recommendation": "Suggestion if applicable"
|
|
537
|
+
}
|
|
538
|
+
],
|
|
539
|
+
|
|
540
|
+
"quickHighlights": [
|
|
541
|
+
{
|
|
542
|
+
"title": "Short highlight",
|
|
543
|
+
"description": "Brief explanation",
|
|
544
|
+
"type": "positive|warning|info|suggestion"
|
|
545
|
+
}
|
|
546
|
+
]
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
CRITICAL INSTRUCTIONS:
|
|
550
|
+
1. For stackPatterns: Identify 2-4 meaningful technology patterns/archetypes from their ACTUAL technologies. Examples:
|
|
551
|
+
- "Modern React Ecosystem" if you see React + TypeScript + Vite/Next.js
|
|
552
|
+
- "Python Data Science" if you see Python + pandas + numpy
|
|
553
|
+
- "Cloud-Native DevOps" if you see Docker + Kubernetes + AWS/GCP
|
|
554
|
+
- Create patterns that reflect their REAL usage, not generic templates
|
|
555
|
+
- Each pattern should group 3-6 related technologies
|
|
556
|
+
|
|
557
|
+
2. Base stackHealth score on:
|
|
558
|
+
- Technology diversity (more types = better)
|
|
559
|
+
- Modern technologies (up-to-date = better)
|
|
560
|
+
- Testing presence (having testing = better)
|
|
561
|
+
- DevOps/CI presence (automation = better)
|
|
562
|
+
|
|
563
|
+
3. Provide 2-4 items for each array. Be specific and actionable. Reference their actual technologies.
|
|
564
|
+
|
|
565
|
+
Here is their tech stack data:
|
|
566
|
+
|
|
567
|
+
${context}`;
|
|
568
|
+
const tempFile = path.join(os.tmpdir(), `tech-stack-ai-${Date.now()}.txt`);
|
|
569
|
+
await fs.writeFile(tempFile, prompt, 'utf-8');
|
|
570
|
+
const timeout = 180000; // 3 minutes
|
|
571
|
+
return new Promise((resolve, reject) => {
|
|
572
|
+
const command = `cat "${tempFile}" | claude --print`;
|
|
573
|
+
const timer = setTimeout(() => {
|
|
574
|
+
reject(new Error('Claude Code execution timed out after 3 minutes'));
|
|
575
|
+
}, timeout + 5000);
|
|
576
|
+
exec(command, {
|
|
577
|
+
cwd: process.cwd(),
|
|
578
|
+
env: { ...process.env, NO_COLOR: '1', FORCE_COLOR: '0' },
|
|
579
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
580
|
+
timeout: timeout,
|
|
581
|
+
}, (error, stdout, stderr) => {
|
|
582
|
+
clearTimeout(timer);
|
|
583
|
+
fs.unlink(tempFile).catch(() => { });
|
|
584
|
+
if (stderr) {
|
|
585
|
+
console.log(`[TechStack] stderr: ${stderr.slice(0, 200)}`);
|
|
586
|
+
}
|
|
587
|
+
if (error) {
|
|
588
|
+
console.log(`[TechStack] Error: ${error.message}`);
|
|
589
|
+
reject(new Error(`Failed to run claude: ${error.message}`));
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
console.log(`[TechStack] Response received, length: ${stdout.length} chars`);
|
|
593
|
+
resolve(stdout);
|
|
594
|
+
});
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Parse Claude's JSON response into TechStackAIInsight fields
|
|
599
|
+
*/
|
|
600
|
+
parseAIResponse(response) {
|
|
601
|
+
const cleanedResponse = response
|
|
602
|
+
.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '')
|
|
603
|
+
.replace(/[\r\n]+/g, '\n')
|
|
604
|
+
.trim();
|
|
605
|
+
const jsonMatch = cleanedResponse.match(/\{[\s\S]*\}/);
|
|
606
|
+
if (!jsonMatch) {
|
|
607
|
+
console.log('[TechStack] No JSON found in response, using fallback');
|
|
608
|
+
return this.generateFallbackAIInsight();
|
|
609
|
+
}
|
|
610
|
+
try {
|
|
611
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
612
|
+
return {
|
|
613
|
+
overview: parsed.overview || '',
|
|
614
|
+
developerProfile: parsed.developerProfile || '',
|
|
615
|
+
stackHealth: typeof parsed.stackHealth === 'number' ? parsed.stackHealth : 50,
|
|
616
|
+
stackPatterns: Array.isArray(parsed.stackPatterns) ? parsed.stackPatterns : [],
|
|
617
|
+
strengths: Array.isArray(parsed.strengths) ? parsed.strengths : [],
|
|
618
|
+
blindSpots: Array.isArray(parsed.blindSpots) ? parsed.blindSpots : [],
|
|
619
|
+
learningPath: Array.isArray(parsed.learningPath) ? parsed.learningPath : [],
|
|
620
|
+
technicalDebt: Array.isArray(parsed.technicalDebt) ? parsed.technicalDebt : [],
|
|
621
|
+
architectureInsights: Array.isArray(parsed.architectureInsights) ? parsed.architectureInsights : [],
|
|
622
|
+
quickHighlights: Array.isArray(parsed.quickHighlights) ? parsed.quickHighlights : [],
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
catch (e) {
|
|
626
|
+
console.log('[TechStack] Failed to parse JSON:', e);
|
|
627
|
+
return this.generateFallbackAIInsight();
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Generate fallback insight when AI fails
|
|
632
|
+
*/
|
|
633
|
+
generateFallbackAIInsight() {
|
|
634
|
+
return {
|
|
635
|
+
overview: 'Unable to generate AI insight. The analysis is based on detected technologies.',
|
|
636
|
+
developerProfile: 'Developer profile unavailable',
|
|
637
|
+
stackHealth: 50,
|
|
638
|
+
strengths: [],
|
|
639
|
+
blindSpots: [],
|
|
640
|
+
learningPath: [],
|
|
641
|
+
technicalDebt: [],
|
|
642
|
+
architectureInsights: [],
|
|
643
|
+
quickHighlights: [
|
|
644
|
+
{
|
|
645
|
+
title: 'AI Analysis Unavailable',
|
|
646
|
+
description: 'Try regenerating the insight',
|
|
647
|
+
type: 'info',
|
|
648
|
+
},
|
|
649
|
+
],
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Build a map of project -> technologies for cross-project analysis
|
|
654
|
+
*/
|
|
655
|
+
buildProjectTechMap(technologies) {
|
|
656
|
+
const projectTechMap = new Map();
|
|
657
|
+
for (const tech of technologies) {
|
|
658
|
+
for (const project of tech.projectsUsed) {
|
|
659
|
+
if (!projectTechMap.has(project)) {
|
|
660
|
+
projectTechMap.set(project, new Set());
|
|
661
|
+
}
|
|
662
|
+
projectTechMap.get(project).add(tech.name);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return projectTechMap;
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Detect common stack patterns across projects
|
|
669
|
+
*/
|
|
670
|
+
detectStackPatterns(technologies, projectTechMap) {
|
|
671
|
+
const patterns = [];
|
|
672
|
+
// Define known stack archetypes to look for
|
|
673
|
+
const archetypes = [
|
|
674
|
+
{
|
|
675
|
+
id: 'fullstack-ts',
|
|
676
|
+
name: 'Full-stack TypeScript',
|
|
677
|
+
required: ['TypeScript', 'React'],
|
|
678
|
+
optional: ['Next.js', 'Tailwind CSS', 'Node.js', 'Prisma'],
|
|
679
|
+
description: 'Modern TypeScript-first web development stack',
|
|
680
|
+
icon: 'layers',
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
id: 'python-ml',
|
|
684
|
+
name: 'Python ML/Data',
|
|
685
|
+
required: ['Python'],
|
|
686
|
+
optional: ['FastAPI', 'Jupyter', 'Pandas', 'TensorFlow', 'PyTorch'],
|
|
687
|
+
description: 'Python-based machine learning and data science',
|
|
688
|
+
icon: 'brain',
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
id: 'go-backend',
|
|
692
|
+
name: 'Go Backend',
|
|
693
|
+
required: ['Go'],
|
|
694
|
+
optional: ['Docker', 'PostgreSQL', 'Redis', 'gRPC'],
|
|
695
|
+
description: 'High-performance Go backend services',
|
|
696
|
+
icon: 'server',
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
id: 'mobile-rn',
|
|
700
|
+
name: 'React Native Mobile',
|
|
701
|
+
required: ['TypeScript', 'React'],
|
|
702
|
+
optional: ['Expo', 'React Native'],
|
|
703
|
+
description: 'Cross-platform mobile development',
|
|
704
|
+
icon: 'smartphone',
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
id: 'devops',
|
|
708
|
+
name: 'DevOps/Platform',
|
|
709
|
+
required: ['Docker'],
|
|
710
|
+
optional: ['Kubernetes', 'Terraform', 'GitHub Actions', 'AWS'],
|
|
711
|
+
description: 'Infrastructure and deployment automation',
|
|
712
|
+
icon: 'cloud',
|
|
713
|
+
},
|
|
714
|
+
];
|
|
715
|
+
const techNames = new Set(technologies.map(t => t.name));
|
|
716
|
+
for (const archetype of archetypes) {
|
|
717
|
+
// Check if all required technologies are present
|
|
718
|
+
const hasRequired = archetype.required.every(r => techNames.has(r));
|
|
719
|
+
if (!hasRequired)
|
|
720
|
+
continue;
|
|
721
|
+
// Find projects using this pattern
|
|
722
|
+
const matchingProjects = [];
|
|
723
|
+
for (const [project, projectTechs] of projectTechMap) {
|
|
724
|
+
const hasAllRequired = archetype.required.every(r => projectTechs.has(r));
|
|
725
|
+
if (hasAllRequired) {
|
|
726
|
+
matchingProjects.push(project);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (matchingProjects.length > 0) {
|
|
730
|
+
// Build list of matched technologies
|
|
731
|
+
const matchedTechs = [
|
|
732
|
+
...archetype.required,
|
|
733
|
+
...archetype.optional.filter(o => techNames.has(o)),
|
|
734
|
+
];
|
|
735
|
+
patterns.push({
|
|
736
|
+
id: archetype.id,
|
|
737
|
+
name: archetype.name,
|
|
738
|
+
description: archetype.description,
|
|
739
|
+
technologies: matchedTechs,
|
|
740
|
+
projectCount: matchingProjects.length,
|
|
741
|
+
projects: matchingProjects,
|
|
742
|
+
icon: archetype.icon,
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
// Sort by project count
|
|
747
|
+
patterns.sort((a, b) => b.projectCount - a.projectCount);
|
|
748
|
+
return patterns;
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Calculate technology co-occurrence correlations
|
|
752
|
+
*/
|
|
753
|
+
calculateCorrelations(technologies, projectTechMap) {
|
|
754
|
+
const correlations = [];
|
|
755
|
+
const significantTechs = technologies.filter(t => t.type !== 'tool' && t.usageCount >= 5).slice(0, 20); // Top 20 non-tool technologies
|
|
756
|
+
for (let i = 0; i < significantTechs.length; i++) {
|
|
757
|
+
for (let j = i + 1; j < significantTechs.length; j++) {
|
|
758
|
+
const tech1 = significantTechs[i];
|
|
759
|
+
const tech2 = significantTechs[j];
|
|
760
|
+
// Count projects with both technologies
|
|
761
|
+
let sharedProjects = 0;
|
|
762
|
+
let tech1Projects = 0;
|
|
763
|
+
let tech2Projects = 0;
|
|
764
|
+
for (const [, projectTechs] of projectTechMap) {
|
|
765
|
+
const hasTech1 = projectTechs.has(tech1.name);
|
|
766
|
+
const hasTech2 = projectTechs.has(tech2.name);
|
|
767
|
+
if (hasTech1)
|
|
768
|
+
tech1Projects++;
|
|
769
|
+
if (hasTech2)
|
|
770
|
+
tech2Projects++;
|
|
771
|
+
if (hasTech1 && hasTech2)
|
|
772
|
+
sharedProjects++;
|
|
773
|
+
}
|
|
774
|
+
// Calculate Jaccard-like correlation
|
|
775
|
+
const unionProjects = tech1Projects + tech2Projects - sharedProjects;
|
|
776
|
+
const correlation = unionProjects > 0
|
|
777
|
+
? Math.round((sharedProjects / unionProjects) * 100)
|
|
778
|
+
: 0;
|
|
779
|
+
if (sharedProjects >= 2 && correlation >= 30) {
|
|
780
|
+
correlations.push({
|
|
781
|
+
tech1: tech1.name,
|
|
782
|
+
tech2: tech2.name,
|
|
783
|
+
correlation,
|
|
784
|
+
projectsShared: sharedProjects,
|
|
785
|
+
strength: correlation >= 70 ? 'strong' : correlation >= 50 ? 'moderate' : 'weak',
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
// Sort by correlation strength
|
|
791
|
+
correlations.sort((a, b) => b.correlation - a.correlation);
|
|
792
|
+
return correlations.slice(0, 15); // Top 15 correlations
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Analyze technology evolution trends
|
|
796
|
+
*/
|
|
797
|
+
analyzeEvolution(technologies) {
|
|
798
|
+
const evolution = [];
|
|
799
|
+
const now = new Date();
|
|
800
|
+
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
801
|
+
const sixtyDaysAgo = new Date(now.getTime() - 60 * 24 * 60 * 60 * 1000);
|
|
802
|
+
for (const tech of technologies.slice(0, 20)) {
|
|
803
|
+
const firstSeenDate = new Date(tech.firstSeen);
|
|
804
|
+
const lastUsedDate = new Date(tech.lastUsed);
|
|
805
|
+
// Estimate recent vs older usage (simplified heuristic)
|
|
806
|
+
const daysSinceLastUse = Math.max(0, (now.getTime() - lastUsedDate.getTime()) / (24 * 60 * 60 * 1000));
|
|
807
|
+
const totalDays = Math.max(1, (lastUsedDate.getTime() - firstSeenDate.getTime()) / (24 * 60 * 60 * 1000));
|
|
808
|
+
// Calculate trend based on recency
|
|
809
|
+
let trend;
|
|
810
|
+
let changePercent;
|
|
811
|
+
if (daysSinceLastUse <= 7) {
|
|
812
|
+
// Used recently - likely rising or stable
|
|
813
|
+
const usageRate = tech.usageCount / Math.max(1, totalDays);
|
|
814
|
+
if (usageRate > 2) {
|
|
815
|
+
trend = 'rising';
|
|
816
|
+
changePercent = Math.min(50, Math.round(usageRate * 10));
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
trend = 'stable';
|
|
820
|
+
changePercent = 0;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
else if (daysSinceLastUse <= 30) {
|
|
824
|
+
trend = 'stable';
|
|
825
|
+
changePercent = 0;
|
|
826
|
+
}
|
|
827
|
+
else {
|
|
828
|
+
trend = 'declining';
|
|
829
|
+
changePercent = -Math.min(30, Math.round(daysSinceLastUse / 2));
|
|
830
|
+
}
|
|
831
|
+
evolution.push({
|
|
832
|
+
tech: tech.name,
|
|
833
|
+
trend,
|
|
834
|
+
changePercent,
|
|
835
|
+
recentUsage: daysSinceLastUse <= 30 ? tech.usageCount : Math.round(tech.usageCount * 0.3),
|
|
836
|
+
totalUsage: tech.usageCount,
|
|
837
|
+
firstSeen: tech.firstSeen,
|
|
838
|
+
monthlyTrend: [], // Would need more granular data
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
// Sort: rising first, then stable, then declining
|
|
842
|
+
const trendOrder = { rising: 0, stable: 1, declining: 2 };
|
|
843
|
+
evolution.sort((a, b) => trendOrder[a.trend] - trendOrder[b.trend]);
|
|
844
|
+
return evolution;
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Compute project similarity based on shared technologies
|
|
848
|
+
*/
|
|
849
|
+
computeProjectSimilarity(projectTechMap) {
|
|
850
|
+
const similarities = [];
|
|
851
|
+
const projects = Array.from(projectTechMap.keys());
|
|
852
|
+
for (let i = 0; i < projects.length; i++) {
|
|
853
|
+
for (let j = i + 1; j < projects.length; j++) {
|
|
854
|
+
const project1 = projects[i];
|
|
855
|
+
const project2 = projects[j];
|
|
856
|
+
const techs1 = projectTechMap.get(project1);
|
|
857
|
+
const techs2 = projectTechMap.get(project2);
|
|
858
|
+
// Calculate Jaccard similarity
|
|
859
|
+
const intersection = new Set([...techs1].filter(t => techs2.has(t)));
|
|
860
|
+
const union = new Set([...techs1, ...techs2]);
|
|
861
|
+
const similarity = Math.round((intersection.size / union.size) * 100);
|
|
862
|
+
if (similarity >= 40 && intersection.size >= 3) {
|
|
863
|
+
similarities.push({
|
|
864
|
+
project1,
|
|
865
|
+
project2,
|
|
866
|
+
project1Name: path.basename(project1),
|
|
867
|
+
project2Name: path.basename(project2),
|
|
868
|
+
similarity,
|
|
869
|
+
sharedTechnologies: Array.from(intersection),
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
// Sort by similarity descending
|
|
875
|
+
similarities.sort((a, b) => b.similarity - a.similarity);
|
|
876
|
+
return similarities.slice(0, 10); // Top 10 similar project pairs
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Generate human-readable highlights from insights
|
|
880
|
+
*/
|
|
881
|
+
generateHighlights(summary, patterns, correlations, evolution) {
|
|
882
|
+
// Shared foundations
|
|
883
|
+
const topPattern = patterns[0];
|
|
884
|
+
const sharedFoundations = topPattern
|
|
885
|
+
? `${topPattern.projectCount} projects share ${topPattern.technologies.slice(0, 3).join(' + ')}`
|
|
886
|
+
: 'No common patterns detected yet';
|
|
887
|
+
// Dominant pattern
|
|
888
|
+
const dominantPattern = topPattern
|
|
889
|
+
? `${topPattern.name} is your go-to stack`
|
|
890
|
+
: 'No dominant stack pattern';
|
|
891
|
+
// Rising tech
|
|
892
|
+
const risingTechs = evolution.filter(e => e.trend === 'rising');
|
|
893
|
+
const risingTech = risingTechs.length > 0
|
|
894
|
+
? `${risingTechs[0].tech} usage trending up`
|
|
895
|
+
: 'No significant trends';
|
|
896
|
+
// Tech diversity score
|
|
897
|
+
const typeSet = new Set(summary.technologies.map(t => t.type));
|
|
898
|
+
const techDiversity = Math.round((typeSet.size / 8) * 100); // 8 possible types
|
|
899
|
+
return {
|
|
900
|
+
sharedFoundations,
|
|
901
|
+
dominantPattern,
|
|
902
|
+
risingTech,
|
|
903
|
+
techDiversity,
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
// ============================================================================
|
|
907
|
+
// Private Methods
|
|
908
|
+
// ============================================================================
|
|
909
|
+
async parseSessionFile(filePath) {
|
|
910
|
+
const messages = [];
|
|
911
|
+
try {
|
|
912
|
+
const fs = await import('node:fs/promises');
|
|
913
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
914
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
915
|
+
for (const line of lines) {
|
|
916
|
+
try {
|
|
917
|
+
const parsed = JSON.parse(line);
|
|
918
|
+
messages.push(parsed);
|
|
919
|
+
}
|
|
920
|
+
catch {
|
|
921
|
+
// Skip malformed lines
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
catch {
|
|
926
|
+
// File read error
|
|
927
|
+
}
|
|
928
|
+
return messages;
|
|
929
|
+
}
|
|
930
|
+
processMessage(msg, projectPath, techMap) {
|
|
931
|
+
if (msg.type !== 'assistant')
|
|
932
|
+
return null;
|
|
933
|
+
const content = msg.message?.content;
|
|
934
|
+
if (!Array.isArray(content))
|
|
935
|
+
return null;
|
|
936
|
+
for (const block of content) {
|
|
937
|
+
if (block.type !== 'tool_use')
|
|
938
|
+
continue;
|
|
939
|
+
const toolName = block.name;
|
|
940
|
+
const input = block.input;
|
|
941
|
+
if (!input)
|
|
942
|
+
continue;
|
|
943
|
+
// Extract file path from various tool inputs
|
|
944
|
+
const filePath = (input.file_path ||
|
|
945
|
+
input.path ||
|
|
946
|
+
input.target_file ||
|
|
947
|
+
input.file);
|
|
948
|
+
if (!filePath || typeof filePath !== 'string')
|
|
949
|
+
continue;
|
|
950
|
+
const timestamp = msg.timestamp;
|
|
951
|
+
const isModification = ['Edit', 'Write', 'MultiEdit', 'CreateFile'].includes(toolName || '');
|
|
952
|
+
// Detect technology from file path
|
|
953
|
+
const techs = this.detectTechnology(filePath);
|
|
954
|
+
for (const tech of techs) {
|
|
955
|
+
let acc = techMap.get(tech.name);
|
|
956
|
+
if (!acc) {
|
|
957
|
+
acc = {
|
|
958
|
+
name: tech.name,
|
|
959
|
+
type: tech.type,
|
|
960
|
+
extensions: new Set(),
|
|
961
|
+
files: new Set(),
|
|
962
|
+
projects: new Set(),
|
|
963
|
+
usageCount: 0,
|
|
964
|
+
claudeContributions: 0,
|
|
965
|
+
firstSeen: timestamp,
|
|
966
|
+
lastUsed: timestamp,
|
|
967
|
+
};
|
|
968
|
+
techMap.set(tech.name, acc);
|
|
969
|
+
}
|
|
970
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
971
|
+
if (ext)
|
|
972
|
+
acc.extensions.add(ext);
|
|
973
|
+
acc.files.add(filePath);
|
|
974
|
+
acc.projects.add(projectPath);
|
|
975
|
+
acc.usageCount++;
|
|
976
|
+
if (isModification)
|
|
977
|
+
acc.claudeContributions++;
|
|
978
|
+
if (timestamp < acc.firstSeen)
|
|
979
|
+
acc.firstSeen = timestamp;
|
|
980
|
+
if (timestamp > acc.lastUsed)
|
|
981
|
+
acc.lastUsed = timestamp;
|
|
982
|
+
}
|
|
983
|
+
return { filePath, toolName: toolName || '' };
|
|
984
|
+
}
|
|
985
|
+
return null;
|
|
986
|
+
}
|
|
987
|
+
detectTechnology(filePath) {
|
|
988
|
+
const results = [];
|
|
989
|
+
const seen = new Set();
|
|
990
|
+
// Check extension mappings
|
|
991
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
992
|
+
if (ext && EXTENSION_MAPPINGS[ext]) {
|
|
993
|
+
const tech = EXTENSION_MAPPINGS[ext];
|
|
994
|
+
if (!seen.has(tech.name)) {
|
|
995
|
+
results.push(tech);
|
|
996
|
+
seen.add(tech.name);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
// Check path patterns
|
|
1000
|
+
for (const { pattern, tech } of PATH_PATTERNS) {
|
|
1001
|
+
if (pattern.test(filePath) && !seen.has(tech.name)) {
|
|
1002
|
+
results.push(tech);
|
|
1003
|
+
seen.add(tech.name);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return results;
|
|
1007
|
+
}
|
|
1008
|
+
calculateProficiency(acc) {
|
|
1009
|
+
const score = acc.usageCount + (acc.claudeContributions * 2) + (acc.projects.size * 5);
|
|
1010
|
+
if (score >= 100)
|
|
1011
|
+
return 'expert';
|
|
1012
|
+
if (score >= 50)
|
|
1013
|
+
return 'proficient';
|
|
1014
|
+
if (score >= 20)
|
|
1015
|
+
return 'familiar';
|
|
1016
|
+
return 'learning';
|
|
1017
|
+
}
|
|
1018
|
+
generateRecommendations(technologies) {
|
|
1019
|
+
const recommendations = [];
|
|
1020
|
+
// Check for missing testing frameworks
|
|
1021
|
+
const hasTests = technologies.some(t => t.type === 'testing');
|
|
1022
|
+
if (!hasTests && technologies.length > 5) {
|
|
1023
|
+
recommendations.push('Consider adding automated tests to your projects');
|
|
1024
|
+
}
|
|
1025
|
+
// Check for TypeScript adoption
|
|
1026
|
+
const hasJS = technologies.some(t => t.name === 'JavaScript');
|
|
1027
|
+
const hasTS = technologies.some(t => t.name === 'TypeScript');
|
|
1028
|
+
if (hasJS && !hasTS) {
|
|
1029
|
+
recommendations.push('Consider migrating to TypeScript for better type safety');
|
|
1030
|
+
}
|
|
1031
|
+
// Check for DevOps
|
|
1032
|
+
const hasDevOps = technologies.some(t => t.type === 'devops');
|
|
1033
|
+
if (!hasDevOps && technologies.length > 10) {
|
|
1034
|
+
recommendations.push('Consider adding CI/CD pipelines for automated deployments');
|
|
1035
|
+
}
|
|
1036
|
+
return recommendations;
|
|
1037
|
+
}
|
|
1038
|
+
findMostActiveProject(technologies) {
|
|
1039
|
+
const projectCounts = new Map();
|
|
1040
|
+
for (const tech of technologies) {
|
|
1041
|
+
for (const project of tech.projectsUsed) {
|
|
1042
|
+
projectCounts.set(project, (projectCounts.get(project) || 0) + tech.usageCount);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
let maxProject = '';
|
|
1046
|
+
let maxCount = 0;
|
|
1047
|
+
for (const [project, count] of projectCounts) {
|
|
1048
|
+
if (count > maxCount) {
|
|
1049
|
+
maxProject = project;
|
|
1050
|
+
maxCount = count;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
return path.basename(maxProject) || 'Unknown';
|
|
1054
|
+
}
|
|
1055
|
+
findGrowthAreas(technologies) {
|
|
1056
|
+
return technologies
|
|
1057
|
+
.filter(t => t.proficiency === 'learning' || t.proficiency === 'familiar')
|
|
1058
|
+
.slice(0, 3)
|
|
1059
|
+
.map(t => t.name);
|
|
1060
|
+
}
|
|
1061
|
+
async loadCache() {
|
|
1062
|
+
try {
|
|
1063
|
+
const fs = await import('node:fs/promises');
|
|
1064
|
+
const content = await fs.readFile(this.cacheFile, 'utf-8');
|
|
1065
|
+
const cached = JSON.parse(content);
|
|
1066
|
+
// Check if cache is still valid (less than 24 hours old)
|
|
1067
|
+
const cacheAge = Date.now() - new Date(cached.generatedAt).getTime();
|
|
1068
|
+
const maxAge = 24 * 60 * 60 * 1000; // 24 hours
|
|
1069
|
+
if (cacheAge < maxAge && cached.version === CACHE_VERSION) {
|
|
1070
|
+
return cached;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
catch {
|
|
1074
|
+
// No cache or invalid
|
|
1075
|
+
}
|
|
1076
|
+
return null;
|
|
1077
|
+
}
|
|
1078
|
+
async saveCache(summary) {
|
|
1079
|
+
try {
|
|
1080
|
+
const fs = await import('node:fs/promises');
|
|
1081
|
+
await fs.writeFile(this.cacheFile, JSON.stringify(summary, null, 2));
|
|
1082
|
+
}
|
|
1083
|
+
catch (error) {
|
|
1084
|
+
console.error('[TechStack] Failed to save cache:', error);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
//# sourceMappingURL=tech-stack.js.map
|