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/README.md +111 -0
- package/assets/CLAUDE.md +48 -0
- package/assets/README.md +72 -0
- package/assets/check-crg.sh +34 -0
- package/assets/crg-cheatsheet.pdf +0 -0
- package/assets/setup-crg.ps1 +182 -0
- package/assets/setup-crg.sh +242 -0
- package/bin/cli.js +250 -0
- package/bin/tutorial.js +198 -0
- package/lib/analytics.js +250 -0
- package/lib/roi.js +250 -0
- package/package.json +38 -0
- package/server.js +502 -0
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
|
+
}
|
package/bin/tutorial.js
ADDED
|
@@ -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);
|
package/lib/analytics.js
ADDED
|
@@ -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
|
+
};
|