crg-dev-kit 1.0.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/bin/cli.js ADDED
@@ -0,0 +1,250 @@
1
+ #!/usr/bin/env node
2
+ const { spawn, execFile } = require('child_process');
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+
7
+ const pkg = require('../package.json');
8
+ const ASSETS = path.join(__dirname, '..', 'assets');
9
+ const args = process.argv.slice(2);
10
+ const cmd = args[0];
11
+ const roi = require('../lib/roi');
12
+
13
+ const NO_ROI = args.includes('--no-roi');
14
+
15
+ function printHelp() {
16
+ console.log(`
17
+ \x1b[36mCRG Dev Kit\x1b[0m — One-click code-review-graph setup
18
+
19
+ \x1b[1mUsage:\x1b[0m
20
+ npx crg-dev-kit [command] [options]
21
+
22
+ \x1b[1mCommands:\x1b[0m
23
+ install, setup Copy setup scripts to current project
24
+ start, serve Launch the dashboard (default)
25
+ uninstall Remove CRG files from current project
26
+ status Check if CRG is installed in current project
27
+ roi Show ROI report (token savings)
28
+ tutorial Launch interactive tutorial
29
+ help Show this help message
30
+ version Show version
31
+
32
+ \x1b[1mOptions:\x1b[0m
33
+ --port <number> Dashboard port (default: 8742)
34
+ --no-open Don't auto-open browser
35
+ --communities Include community detection flag
36
+ --no-roi Skip ROI tracking (installs without analytics)
37
+
38
+ \x1b[1mExamples:\x1b[0m
39
+ npx crg-dev-kit # Open dashboard
40
+ npx crg-dev-kit install # Copy scripts to project
41
+ npx crg-dev-kit start --port 9000 # Custom port
42
+ npx crg-dev-kit status # Check installation
43
+ `);
44
+ }
45
+
46
+ function printVersion() {
47
+ console.log(`crg-dev-kit v${pkg.version}`);
48
+ }
49
+
50
+ function copyFiles() {
51
+ const cwd = process.cwd();
52
+ const files = [
53
+ { name: 'setup-crg.sh', executable: true },
54
+ { name: 'setup-crg.ps1', executable: false },
55
+ { name: 'check-crg.sh', executable: true },
56
+ { name: 'CLAUDE.md', executable: false },
57
+ { name: 'crg-cheatsheet.pdf', executable: false },
58
+ { name: 'README.md', executable: false },
59
+ ];
60
+
61
+ let copied = 0;
62
+ files.forEach(f => {
63
+ const src = path.join(ASSETS, f.name);
64
+ const dst = path.join(cwd, f.name);
65
+ if (fs.existsSync(src)) {
66
+ if (fs.existsSync(dst)) {
67
+ console.log(` \x1b[33m~\x1b[0m ${f.name} \x1b[2m(already exists, skipping)\x1b[0m`);
68
+ return;
69
+ }
70
+ fs.copyFileSync(src, dst);
71
+ if (f.executable) {
72
+ try { fs.chmodSync(dst, '755'); } catch {}
73
+ }
74
+ console.log(` \x1b[32m✓\x1b[0m ${f.name}`);
75
+ copied++;
76
+ }
77
+ });
78
+
79
+ // Track install date (only if --no-roi not set)
80
+ if (!NO_ROI) {
81
+ roi.setInstallDate(cwd);
82
+ console.log('\n \x1b[36m✓ Install date recorded for ROI tracking\x1b[0m');
83
+ }
84
+
85
+ if (copied === 0) {
86
+ console.log('\n \x1b[33mFiles already exist, install date updated.\x1b[0m');
87
+ return;
88
+ }
89
+
90
+ const isWin = process.platform === 'win32';
91
+ console.log(`\n \x1b[32m${copied} file(s) copied to ${cwd}\x1b[0m\n`);
92
+ console.log(' \x1b[1mNext steps:\x1b[0m');
93
+ if (isWin) {
94
+ console.log(' .\\setup-crg.ps1 -WithCommunities');
95
+ console.log(' code-review-graph status');
96
+ } else {
97
+ console.log(' bash setup-crg.sh --with-communities');
98
+ console.log(' bash check-crg.sh');
99
+ }
100
+ console.log('');
101
+ }
102
+
103
+ function checkStatus() {
104
+ const cwd = process.cwd();
105
+ const checks = [
106
+ { file: 'setup-crg.sh', label: 'Linux/macOS setup script' },
107
+ { file: 'setup-crg.ps1', label: 'Windows setup script' },
108
+ { file: 'check-crg.sh', label: 'Health check script' },
109
+ { file: 'CLAUDE.md', label: 'AI configuration' },
110
+ ];
111
+
112
+ console.log('\n \x1b[36mCRG Installation Status\x1b[0m\n');
113
+ let found = 0;
114
+ checks.forEach(c => {
115
+ const exists = fs.existsSync(path.join(cwd, c.file));
116
+ if (exists) found++;
117
+ console.log(` ${exists ? '\x1b[32m✓\x1b[0m' : '\x1b[31m✗\x1b[0m'} ${c.label}`);
118
+ });
119
+
120
+ // Check if code-review-graph is installed
121
+ const checkCRG = () => {
122
+ return new Promise(resolve => {
123
+ execFile('code-review-graph', ['--version'], (err) => {
124
+ resolve(!err);
125
+ });
126
+ });
127
+ };
128
+
129
+ checkCRG().then(installed => {
130
+ console.log(` ${installed ? '\x1b[32m✓\x1b[0m' : '\x1b[31m✗\x1b[0m'} code-review-graph CLI`);
131
+ const total = checks.length + 1;
132
+ const score = found + (installed ? 1 : 0);
133
+ console.log(`\n ${score}/${total} components present`);
134
+ if (score === total) {
135
+ console.log(' \x1b[32m✓ Fully set up!\x1b[0m\n');
136
+ } else if (score >= 3) {
137
+ console.log(' \x1b[33m~ Partially set up. Run install command.\x1b[0m\n');
138
+ } else {
139
+ console.log(' \x1b[31m✗ Not set up. Run: npx crg-dev-kit install\x1b[0m\n');
140
+ }
141
+ });
142
+ }
143
+
144
+ function uninstallFiles() {
145
+ const cwd = process.cwd();
146
+ const files = ['setup-crg.sh', 'setup-crg.ps1', 'check-crg.sh', 'CLAUDE.md', 'crg-cheatsheet.pdf', 'README.md'];
147
+ let removed = 0;
148
+
149
+ files.forEach(f => {
150
+ const dst = path.join(cwd, f);
151
+ if (fs.existsSync(dst)) {
152
+ fs.unlinkSync(dst);
153
+ console.log(` \x1b[31m✗\x1b[0m ${f}`);
154
+ removed++;
155
+ }
156
+ });
157
+
158
+ if (removed === 0) {
159
+ console.log('\n \x1b[33mNo CRG files found to remove.\x1b[0m\n');
160
+ } else {
161
+ console.log(`\n \x1b[31m${removed} file(s) removed from ${cwd}\x1b[0m\n`);
162
+ }
163
+ }
164
+
165
+ function startServer() {
166
+ let port = 8742;
167
+ let noOpen = false;
168
+
169
+ for (let i = 0; i < args.length; i++) {
170
+ if (args[i] === '--port' && args[i + 1]) {
171
+ port = parseInt(args[i + 1], 10);
172
+ if (isNaN(port) || port < 1024 || port > 65535) {
173
+ console.error('\x1b[31mError: Port must be between 1024 and 65535\x1b[0m');
174
+ process.exit(1);
175
+ }
176
+ i++;
177
+ }
178
+ if (args[i] === '--no-open') noOpen = true;
179
+ }
180
+
181
+ const serverPath = path.join(__dirname, '..', 'server.js');
182
+ const server = require(serverPath);
183
+ server.start(port, noOpen);
184
+ }
185
+
186
+ function showROI() {
187
+ const cwd = process.cwd();
188
+ const report = roi.generateROIReport(cwd);
189
+ console.log('\n' + report);
190
+ }
191
+
192
+ function runTutorial() {
193
+ const tutorialPath = path.join(__dirname, 'tutorial.js');
194
+ const tutorial = require(tutorialPath);
195
+ }
196
+
197
+ // Command routing
198
+ switch (cmd) {
199
+ case 'install':
200
+ case 'setup':
201
+ copyFiles();
202
+ break;
203
+
204
+ case 'uninstall':
205
+ case 'remove':
206
+ uninstallFiles();
207
+ break;
208
+
209
+ case 'status':
210
+ case 'check':
211
+ checkStatus();
212
+ break;
213
+
214
+ case 'start':
215
+ case 'serve':
216
+ case 'dashboard':
217
+ startServer();
218
+ break;
219
+
220
+ case 'help':
221
+ case '--help':
222
+ case '-h':
223
+ printHelp();
224
+ break;
225
+
226
+ case 'version':
227
+ case '--version':
228
+ case '-v':
229
+ printVersion();
230
+ break;
231
+
232
+ case 'roi':
233
+ showROI();
234
+ break;
235
+
236
+ case 'tutorial':
237
+ runTutorial();
238
+ break;
239
+
240
+ default:
241
+ // No command or unknown command = start server
242
+ if (!cmd || cmd.startsWith('-')) {
243
+ startServer();
244
+ } else {
245
+ console.log(`\x1b[31mUnknown command: ${cmd}\x1b[0m`);
246
+ console.log('Run \x1b[1mnpx crg-dev-kit help\x1b[0m for usage.\n');
247
+ process.exit(1);
248
+ }
249
+ break;
250
+ }
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env node
2
+ const readline = require('readline');
3
+
4
+ const colors = {
5
+ reset: '\x1b[0m',
6
+ bright: '\x1b[1m',
7
+ dim: '\x1b[2m',
8
+ cyan: '\x1b[36m',
9
+ yellow: '\x1b[33m',
10
+ green: '\x1b[32m',
11
+ red: '\x1b[31m',
12
+ gray: '\x1b[90m'
13
+ };
14
+
15
+ function c(color, text) {
16
+ return colors[color] + text + colors.reset;
17
+ }
18
+
19
+ const rl = readline.createInterface({
20
+ input: process.stdin,
21
+ output: process.stdout
22
+ });
23
+
24
+ const menu = `
25
+ ${c('cyan','╔════════════════════════════════════════════════════════════╗')}
26
+ ${c('cyan','║')} ${c('bright','🎯 CODE SCOPE INTERACTIVE TUTORIAL')} ${c('cyan','║')}
27
+ ${c('cyan','╚════════════════════════════════════════════════════════════╝')}
28
+
29
+ ${c('cyan','1.')} What is Code Scope?
30
+ ${c('cyan','2.')} See how it works (before/after)
31
+ ${c('cyan','3.')} Try a demo query
32
+ ${c('cyan','4.')} Your project stats
33
+ ${c('cyan','5.')} FAQ
34
+ ${c('cyan','6.')} Exit
35
+
36
+ `;
37
+
38
+ const whatIsCodeScope = `
39
+ ${c('yellow','🔍 WHAT IS CODE SCOPE?')}
40
+
41
+ Code Scope gives your AI coding assistant (Claude Code, Cursor, etc.)
42
+ a ${c('bright','knowledge graph')} of your codebase.
43
+
44
+ Instead of reading every file, Claude can query:
45
+ • Who calls this function?
46
+ • What tests cover this file?
47
+ • What's affected by this change?
48
+
49
+ Think of it like a ${c('bright','GPS for your code')} — instead of
50
+ reading every road sign, you ask "how do I get there?"
51
+
52
+ ${c('gray','─'.repeat(60))}
53
+ ${c('gray','Press Enter to go back...')}
54
+ `;
55
+
56
+ const beforeAfter = `
57
+ ${c('yellow','⚡ BEFORE VS AFTER')}
58
+
59
+ ${c('red','BEFORE:')}
60
+ You ask: "Who calls auth_user()?"
61
+ Claude: Reads 542 JavaScript files...
62
+ Tokens: ~50,000
63
+ Time: 30 seconds
64
+
65
+ ${c('green','AFTER:')}
66
+ You ask: "Who calls auth_user()?"
67
+ Claude: Queries knowledge graph
68
+ Tokens: ~400 (structured JSON)
69
+ Time: 1 second
70
+
71
+ ${c('bright','Result: ~100x fewer tokens, 30x faster')}
72
+
73
+ ${c('gray','─'.repeat(60))}
74
+ ${c('gray','Press Enter to go back...')}
75
+ `;
76
+
77
+ const demoQuery = `
78
+ ${c('yellow','🎮 DEMO: TRY A QUERY')}
79
+
80
+ In your project, Claude can answer questions like:
81
+
82
+ ${c('cyan','▸ "Find callers of login()"')}
83
+ → Shows all functions that call login()
84
+
85
+ ${c('cyan','▸ "Find tests for auth.ts"')}
86
+ → Shows test files for auth module
87
+
88
+ ${c('cyan','▸ "Use detect_changes"')}
89
+ → Risk-scoped code review
90
+
91
+ ${c('cyan','▸ "Show me architecture"')}
92
+ → Get community structure
93
+
94
+ ${c('gray','After restarting Claude Code, try one of these!')}
95
+ ${c('gray','─'.repeat(60))}
96
+ ${c('gray','Press Enter to go back...')}
97
+ `;
98
+
99
+ const projectStats = () => {
100
+ const { execSync } = require('child_process');
101
+ try {
102
+ const stats = execSync('code-review-graph status 2>/dev/null', { encoding: 'utf8' });
103
+ return `${c('yellow','📊 YOUR PROJECT STATS')}
104
+
105
+ ${stats}
106
+ ${c('gray','─'.repeat(60))}
107
+ ${c('gray','Press Enter to go back...')}`;
108
+ } catch {
109
+ return `${c('yellow','📊 YOUR PROJECT STATS')}
110
+
111
+ Run ${c('cyan','code-review-graph status')} in your project first!
112
+
113
+ ${c('gray','─'.repeat(60))}
114
+ ${c('gray','Press Enter to go back...')}`;
115
+ }
116
+ };
117
+
118
+ const faq = `
119
+ ${c('yellow','❓ FAQ')}
120
+
121
+ ${c('cyan','Q: Does this slow down my workflow?')}
122
+ A: No. Graph builds once, updates incrementally.
123
+
124
+ ${c('cyan','Q: Does it read my code?')}
125
+ A: Only once to build. After that, queries the graph.
126
+
127
+ ${c('cyan','Q: Is my data safe?')}
128
+ A: 100% local. No cloud. No telemetry.
129
+
130
+ ${c('cyan','Q: What if I don\'t want ROI tracking?')}
131
+ A: Use ${c('cyan','--no-roi')} flag:
132
+ npx crg-dev-kit install --no-roi
133
+
134
+ ${c('gray','─'.repeat(60))}
135
+ ${c('gray','Press Enter to go back...')}
136
+ `;
137
+
138
+ function showScreen(screenName) {
139
+ console.clear();
140
+
141
+ if (screenName === 'main') {
142
+ console.log(menu);
143
+ rl.question(c('gray','Choose: '), (answer) => {
144
+ handleChoice(answer.trim());
145
+ });
146
+ } else if (screenName === 'stats') {
147
+ console.log(projectStats());
148
+ rl.question('', () => {
149
+ showScreen('main');
150
+ });
151
+ } else {
152
+ const screens = {
153
+ '1': whatIsCodeScope,
154
+ '2': beforeAfter,
155
+ '3': demoQuery,
156
+ '4': 'stats',
157
+ '5': faq
158
+ };
159
+ const content = screens[screenName];
160
+ if (content === 'stats') {
161
+ showScreen('stats');
162
+ } else {
163
+ console.log(content);
164
+ rl.question('', () => {
165
+ showScreen('main');
166
+ });
167
+ }
168
+ }
169
+ }
170
+
171
+ function handleChoice(choice) {
172
+ if (choice === '6' || choice === 'exit' || choice === 'q') {
173
+ console.clear();
174
+ console.log(c('green','\n🎉 Thanks for trying Code Scope!'));
175
+ console.log(c('gray',' Run ') + c('cyan','npx crg-dev-kit') + c('gray',' anytime for dashboard'));
176
+ console.log(c('gray',' Run ') + c('cyan','npx crg-dev-kit roi') + c('gray',' for ROI report\n'));
177
+ rl.close();
178
+ process.exit(0);
179
+ }
180
+
181
+ const validChoices = ['1', '2', '3', '4', '5'];
182
+ if (validChoices.includes(choice)) {
183
+ showScreen(choice);
184
+ } else {
185
+ console.log(c('red','Invalid choice. Try 1-6.'));
186
+ showScreen('main');
187
+ }
188
+ }
189
+
190
+ console.clear();
191
+ console.log(`
192
+ ${c('cyan','╔════════════════════════════════════════════════════════════╗')}
193
+ ${c('cyan','║')} ${c('bright','🎯 Welcome to Code Scope Interactive Tutorial!')} ${c('cyan','║')}
194
+ ${c('cyan','║')} ${c('cyan','║')}
195
+ ${c('cyan','║')} Learn how to use the knowledge graph in 2 minutes ${c('cyan','║')}
196
+ ${c('cyan','╚════════════════════════════════════════════════════════════╝')}
197
+ `);
198
+ setTimeout(() => showScreen('main'), 500);
@@ -0,0 +1,250 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const crypto = require('crypto');
4
+
5
+ const ANALYTICS_DIR = path.join(process.env.HOME || process.env.USERPROFILE, '.crg-analytics');
6
+ const SESSIONS_FILE = path.join(ANALYTICS_DIR, 'sessions.json');
7
+ const TOKEN_BASELINES = path.join(ANALYTICS_DIR, 'baselines.json');
8
+
9
+ const TOKEN_ESTIMATES = {
10
+ read_file_per_100_lines: 75,
11
+ get_review_context: 400,
12
+ detect_changes: 300,
13
+ query_graph: 200,
14
+ semantic_search: 250,
15
+ get_impact_radius: 350,
16
+ get_architecture_overview: 600,
17
+ list_graph_stats: 100,
18
+ get_flow: 450,
19
+ list_flows: 300,
20
+ get_community: 350,
21
+ list_communities: 200,
22
+ refactor_tool: 500,
23
+ get_review_context_tool: 400,
24
+ };
25
+
26
+ function ensureDir() {
27
+ if (!fs.existsSync(ANALYTICS_DIR)) {
28
+ fs.mkdirSync(ANALYTICS_DIR, { recursive: true });
29
+ }
30
+ }
31
+
32
+ function readJSON(file, fallback) {
33
+ try {
34
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
35
+ } catch {
36
+ return fallback;
37
+ }
38
+ }
39
+
40
+ function writeJSON(file, data) {
41
+ ensureDir();
42
+ fs.writeFileSync(file, JSON.stringify(data, null, 2));
43
+ }
44
+
45
+ function estimateTokensWithoutCRG(fileCount, avgLinesPerFile, operation = 'review') {
46
+ const totalLines = fileCount * avgLinesPerFile;
47
+ const naiveTokens = Math.ceil(totalLines / 100) * TOKEN_ESTIMATES.read_file_per_100_lines;
48
+ const contextTokens = naiveTokens * 1.2;
49
+ return Math.ceil(contextTokens);
50
+ }
51
+
52
+ function estimateTokensWithCRG(toolsUsed) {
53
+ let total = 0;
54
+ toolsUsed.forEach(tool => {
55
+ const toolName = tool.name || tool;
56
+ const count = tool.count || 1;
57
+ total += (TOKEN_ESTIMATES[toolName] || 300) * count;
58
+ });
59
+ return total;
60
+ }
61
+
62
+ function createSession(projectPath, operation) {
63
+ ensureDir();
64
+ const sessions = readJSON(SESSIONS_FILE, []);
65
+ const session = {
66
+ id: crypto.randomUUID(),
67
+ project: projectPath,
68
+ operation: operation || 'code_review',
69
+ startTime: new Date().toISOString(),
70
+ toolsUsed: [],
71
+ filesReviewed: 0,
72
+ avgLinesPerFile: 0,
73
+ tokensWithoutCRG: 0,
74
+ tokensWithCRG: 0,
75
+ savings: 0,
76
+ status: 'active'
77
+ };
78
+ sessions.push(session);
79
+ writeJSON(SESSIONS_FILE, sessions);
80
+ return session;
81
+ }
82
+
83
+ function logToolUsage(sessionId, toolName, count = 1) {
84
+ const sessions = readJSON(SESSIONS_FILE, []);
85
+ const session = sessions.find(s => s.id === sessionId);
86
+ if (!session) return null;
87
+
88
+ const existing = session.toolsUsed.find(t => t.name === toolName);
89
+ if (existing) {
90
+ existing.count += count;
91
+ } else {
92
+ session.toolsUsed.push({ name: toolName, count });
93
+ }
94
+
95
+ session.tokensWithCRG = estimateTokensWithCRG(session.toolsUsed);
96
+ session.tokensWithoutCRG = estimateTokensWithoutCRG(
97
+ session.filesReviewed || 5,
98
+ session.avgLinesPerFile || 100,
99
+ session.operation
100
+ );
101
+ session.savings = session.tokensWithoutCRG > 0
102
+ ? Math.round((1 - session.tokensWithCRG / session.tokensWithoutCRG) * 100)
103
+ : 0;
104
+
105
+ writeJSON(SESSIONS_FILE, sessions);
106
+ return session;
107
+ }
108
+
109
+ function endSession(sessionId, filesReviewed, avgLinesPerFile) {
110
+ const sessions = readJSON(SESSIONS_FILE, []);
111
+ const session = sessions.find(s => s.id === sessionId);
112
+ if (!session) return null;
113
+
114
+ session.filesReviewed = filesReviewed;
115
+ session.avgLinesPerFile = avgLinesPerFile;
116
+ session.tokensWithoutCRG = estimateTokensWithoutCRG(filesReviewed, avgLinesPerFile, session.operation);
117
+ session.tokensWithCRG = estimateTokensWithCRG(session.toolsUsed);
118
+ session.savings = session.tokensWithoutCRG > 0
119
+ ? Math.round((1 - session.tokensWithCRG / session.tokensWithoutCRG) * 100)
120
+ : 0;
121
+ session.endTime = new Date().toISOString();
122
+ session.status = 'completed';
123
+
124
+ writeJSON(SESSIONS_FILE, sessions);
125
+ return session;
126
+ }
127
+
128
+ function getProjectStats(projectPath) {
129
+ const sessions = readJSON(SESSIONS_FILE, []);
130
+ const projectSessions = sessions.filter(s =>
131
+ s.project === projectPath && s.status === 'completed'
132
+ );
133
+
134
+ if (projectSessions.length === 0) return null;
135
+
136
+ const totalTokensWithoutCRG = projectSessions.reduce((sum, s) => sum + (s.tokensWithoutCRG || 0), 0);
137
+ const totalTokensWithCRG = projectSessions.reduce((sum, s) => sum + (s.tokensWithCRG || 0), 0);
138
+ const avgSavings = projectSessions.reduce((sum, s) => sum + (s.savings || 0), 0) / projectSessions.length;
139
+ const totalSessions = projectSessions.length;
140
+ const totalFilesReviewed = projectSessions.reduce((sum, s) => sum + (s.filesReviewed || 0), 0);
141
+
142
+ const toolUsage = {};
143
+ projectSessions.forEach(s => {
144
+ s.toolsUsed.forEach(t => {
145
+ toolUsage[t.name] = (toolUsage[t.name] || 0) + t.count;
146
+ });
147
+ });
148
+
149
+ return {
150
+ project: projectPath,
151
+ totalSessions,
152
+ totalFilesReviewed,
153
+ totalTokensWithoutCRG,
154
+ totalTokensWithCRG,
155
+ totalTokensSaved: totalTokensWithoutCRG - totalTokensWithCRG,
156
+ avgSavingsPercent: Math.round(avgSavings),
157
+ estimatedCostSavings: Math.round((totalTokensWithoutCRG - totalTokensWithCRG) / 1000 * 0.01 * 100) / 100,
158
+ toolUsage,
159
+ lastSession: projectSessions[projectSessions.length - 1].endTime,
160
+ sessions: projectSessions.slice(-10)
161
+ };
162
+ }
163
+
164
+ function getAllStats() {
165
+ const sessions = readJSON(SESSIONS_FILE, []);
166
+ const completedSessions = sessions.filter(s => s.status === 'completed');
167
+
168
+ if (completedSessions.length === 0) return null;
169
+
170
+ const projectPaths = [...new Set(completedSessions.map(s => s.project))];
171
+ const projectStats = projectPaths.map(p => getProjectStats(p));
172
+
173
+ const totalTokensWithoutCRG = projectStats.reduce((sum, p) => sum + p.totalTokensWithoutCRG, 0);
174
+ const totalTokensWithCRG = projectStats.reduce((sum, p) => sum + p.totalTokensWithCRG, 0);
175
+ const avgSavings = projectStats.reduce((sum, p) => sum + p.avgSavingsPercent, 0) / projectStats.length;
176
+
177
+ return {
178
+ totalProjects: projectPaths.length,
179
+ totalSessions: completedSessions.length,
180
+ totalTokensWithoutCRG,
181
+ totalTokensWithCRG,
182
+ totalTokensSaved: totalTokensWithoutCRG - totalTokensWithCRG,
183
+ avgSavingsPercent: Math.round(avgSavings),
184
+ estimatedCostSavings: Math.round((totalTokensWithoutCRG - totalTokensWithCRG) / 1000 * 0.01 * 100) / 100,
185
+ projects: projectStats
186
+ };
187
+ }
188
+
189
+ function generateReport(projectPath) {
190
+ const stats = projectPath ? getProjectStats(projectPath) : getAllStats();
191
+ if (!stats) return 'No analytics data found. Run code reviews with CRG to start tracking.';
192
+
193
+ let report = `# CRG Token Analytics Report\n\n`;
194
+ report += `Generated: ${new Date().toISOString()}\n\n`;
195
+
196
+ if (stats.totalProjects) {
197
+ report += `## Overview\n\n`;
198
+ report += `| Metric | Value |\n|--------|-------|\n`;
199
+ report += `| Projects Tracked | ${stats.totalProjects} |\n`;
200
+ report += `| Total Sessions | ${stats.totalSessions} |\n`;
201
+ report += `| Avg Token Savings | ${stats.avgSavingsPercent}% |\n`;
202
+ report += `| Total Tokens Saved | ${stats.totalTokensSaved.toLocaleString()} |\n`;
203
+ report += `| Est. Cost Savings | $${stats.estimatedCostSavings} |\n\n`;
204
+
205
+ report += `## Projects\n\n`;
206
+ stats.projects.forEach(p => {
207
+ report += `### ${p.project}\n\n`;
208
+ report += `| Metric | Value |\n|--------|-------|\n`;
209
+ report += `| Sessions | ${p.totalSessions} |\n`;
210
+ report += `| Files Reviewed | ${p.totalFilesReviewed} |\n`;
211
+ report += `| Token Savings | ${p.avgSavingsPercent}% |\n`;
212
+ report += `| Tokens Saved | ${p.totalTokensSaved.toLocaleString()} |\n\n`;
213
+ });
214
+ } else {
215
+ report += `## Session Summary\n\n`;
216
+ report += `| Metric | Value |\n|--------|-------|\n`;
217
+ report += `| Sessions | ${stats.totalSessions} |\n`;
218
+ report += `| Files Reviewed | ${stats.totalFilesReviewed} |\n`;
219
+ report += `| Token Savings | ${stats.avgSavingsPercent}% |\n`;
220
+ report += `| Tokens Without CRG | ${stats.totalTokensWithoutCRG.toLocaleString()} |\n`;
221
+ report += `| Tokens With CRG | ${stats.totalTokensWithCRG.toLocaleString()} |\n`;
222
+ report += `| Tokens Saved | ${stats.totalTokensSaved.toLocaleString()} |\n`;
223
+ report += `| Est. Cost Savings | $${stats.estimatedCostSavings} |\n\n`;
224
+
225
+ if (Object.keys(stats.toolUsage).length > 0) {
226
+ report += `## Tool Usage\n\n`;
227
+ report += `| Tool | Times Used |\n|------|------------|\n`;
228
+ Object.entries(stats.toolUsage)
229
+ .sort((a, b) => b[1] - a[1])
230
+ .forEach(([tool, count]) => {
231
+ report += `| ${tool} | ${count} |\n`;
232
+ });
233
+ report += '\n';
234
+ }
235
+ }
236
+
237
+ return report;
238
+ }
239
+
240
+ module.exports = {
241
+ createSession,
242
+ logToolUsage,
243
+ endSession,
244
+ getProjectStats,
245
+ getAllStats,
246
+ generateReport,
247
+ estimateTokensWithoutCRG,
248
+ estimateTokensWithCRG,
249
+ TOKEN_ESTIMATES
250
+ };