crg-dev-kit 1.0.0 → 2.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/server.js CHANGED
@@ -4,9 +4,11 @@ const path = require('path');
4
4
  const { execFile } = require('child_process');
5
5
  const analytics = require('./lib/analytics');
6
6
  const roi = require('./lib/roi');
7
+ const actions = require('./lib/actions');
7
8
 
8
9
  const DEFAULT_PORT = 8742;
9
10
  const ASSETS = path.join(__dirname, 'assets');
11
+ const MAX_BODY = 64 * 1024;
10
12
 
11
13
  const DOWNLOADS = [
12
14
  { file: 'setup-crg.sh', label: 'Linux / macOS / WSL Setup', icon: '🐧', desc: 'Bash — auto-detects OS, installs CRG, builds graph, health check' },
@@ -16,14 +18,6 @@ const DOWNLOADS = [
16
18
  { file: 'crg-cheatsheet.pdf', label: 'Cheatsheet PDF', icon: '📄', desc: 'One-page visual reference — tools, workflows, known issues' },
17
19
  ];
18
20
 
19
- const STEPS = [
20
- { n: '1', t: 'Download the setup script', d: 'Pick your OS from the Downloads section below.' },
21
- { n: '2', t: 'Run it', linux: 'bash setup-crg.sh --with-communities', win: '.\\setup-crg.ps1 -WithCommunities' },
22
- { n: '3', t: 'Drop CLAUDE.md in your project root', d: 'Your AI tool reads this file automatically.' },
23
- { n: '4', t: 'Restart your AI tool', d: 'Claude Code / Cursor / Windsurf — restart to load MCP config.' },
24
- { n: '5', t: 'Verify', linux: 'bash check-crg.sh', win: 'code-review-graph status' },
25
- ];
26
-
27
21
  const CRG_TOOLS = [
28
22
  { name: 'detect_changes', label: 'Change Detection', desc: 'Risk-scored impact analysis' },
29
23
  { name: 'get_review_context', label: 'Review Context', desc: 'Token-optimized source snippets' },
@@ -35,7 +29,7 @@ const CRG_TOOLS = [
35
29
  { name: 'list_communities', label: 'Code Communities', desc: 'Clustered code modules' },
36
30
  ];
37
31
 
38
- function esc(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
32
+ function esc(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;'); }
39
33
 
40
34
  function mdToHtml(md) {
41
35
  let lines = md.split('\n'), out = [], inCode = false;
@@ -72,170 +66,62 @@ function formatTokens(n) {
72
66
  return `${n}`;
73
67
  }
74
68
 
75
- function buildPage(port, analyticsData) {
76
- const readmePath = path.join(ASSETS, 'README.md');
77
- const readmeHtml = fs.existsSync(readmePath) ? mdToHtml(fs.readFileSync(readmePath, 'utf8')) : '<p>No README found.</p>';
69
+ function openBrowser(url) {
70
+ const plat = process.platform;
71
+ if (plat === 'win32') execFile('cmd', ['/c', 'start', url]);
72
+ else if (plat === 'darwin') execFile('open', [url]);
73
+ else execFile('xdg-open', [url]);
74
+ }
78
75
 
79
- const dlCards = DOWNLOADS.map(d => `
80
- <a href="/download/${d.file}" class="dl-card" download>
81
- <span class="dl-icon">${d.icon}</span>
82
- <div class="dl-info"><span class="dl-name">${d.label}</span><span class="dl-desc">${d.desc}</span></div>
83
- <span class="dl-size">${fileSize(d.file)}</span>
84
- <span class="dl-btn">Download</span>
85
- </a>`).join('');
76
+ function findAvailablePort(startPort, maxAttempts = 10) {
77
+ return new Promise((resolve, reject) => {
78
+ const tryPort = (port, attempt) => {
79
+ if (attempt > maxAttempts) {
80
+ reject(new Error(`No available port found after ${maxAttempts} attempts`));
81
+ return;
82
+ }
83
+ const server = http.createServer();
84
+ server.on('error', (err) => {
85
+ if (err.code === 'EADDRINUSE') {
86
+ server.close();
87
+ tryPort(port + 1, attempt + 1);
88
+ } else {
89
+ reject(err);
90
+ }
91
+ });
92
+ server.on('listening', () => {
93
+ server.close();
94
+ resolve(port);
95
+ });
96
+ server.listen(port);
97
+ };
98
+ tryPort(startPort, 1);
99
+ });
100
+ }
86
101
 
87
- const stepCards = STEPS.map(s => {
88
- let body = s.d ? `<p class="step-desc">${s.d}</p>` : '';
89
- if (s.linux) body += `
90
- <div class="step-cmds">
91
- <div class="cmd-block"><span class="cmd-label">Linux / macOS</span><code>${s.linux}</code></div>
92
- <div class="cmd-block"><span class="cmd-label">Windows</span><code>${s.win}</code></div>
93
- </div>`;
94
- return `<div class="step"><span class="step-num">${s.n}</span><div class="step-body"><h3>${s.t}</h3>${body}</div></div>`;
95
- }).join('');
102
+ /* ── Shell page (nav + empty #content + footer + CSS + htmx) ─────────── */
96
103
 
97
- const toolCards = CRG_TOOLS.map(t => `
98
- <div class="tool-card">
99
- <span class="tool-name">${t.name}</span>
100
- <span class="tool-label">${t.label}</span>
101
- <span class="tool-desc">${t.desc}</span>
102
- </div>`).join('');
104
+ function buildPage() {
105
+ const tabs = [
106
+ { id: 'dashboard', label: 'Dashboard' },
107
+ { id: 'install', label: 'Install' },
108
+ { id: 'status', label: 'Status' },
109
+ { id: 'analytics', label: 'Analytics' },
110
+ { id: 'tools', label: 'Tools' },
111
+ { id: 'downloads', label: 'Downloads' },
112
+ { id: 'docs', label: 'Docs' },
113
+ ];
103
114
 
104
- let analyticsSection = '';
105
- let roiSection = '';
106
-
107
- // ROI Section
108
- try {
109
- const roiData = roi.getAllProjectsROI();
110
- if (roiData.installed) {
111
- const verdictColor = roiData.roi.verdict === 'positive' ? '#16a34a' : roiData.roi.verdict === 'neutral' ? '#ea580c' : '#dc2626';
112
- const verdictIcon = roiData.roi.verdict === 'positive' ? '✅' : roiData.roi.verdict === 'neutral' ? '➖' : '⚠️';
113
- roiSection = `
114
- <section id="roi">
115
- <div class="st">ROI Calculator</div>
116
- <div class="roi-banner">
117
- <div class="roi-verdict" style="background:${verdictColor}20;border-color:${verdictColor}">
118
- <span class="roi-verdict-icon">${verdictIcon}</span>
119
- <span class="roi-verdict-text">${roiData.roi.verdict === 'positive' ? 'CRG is helping' : roiData.roi.verdict === 'neutral' ? 'No significant change' : 'CRG may not be helping'}</span>
120
- </div>
121
- <div class="roi-installed">Installed: ${new Date(roiData.installDate).toLocaleDateString()}</div>
122
- </div>
123
- <div class="roi-grid">
124
- <div class="roi-card">
125
- <div class="roi-value" style="color:${verdictColor}">${roiData.roi.tokenSavingsPercent}%</div>
126
- <div class="roi-label">Token Savings</div>
127
- </div>
128
- <div class="roi-card">
129
- <div class="roi-value">${roiData.roi.tokensSaved.toLocaleString()}</div>
130
- <div class="roi-label">Tokens Saved</div>
131
- </div>
132
- <div class="roi-card">
133
- <div class="roi-value">${roiData.roi.promptReduction}</div>
134
- <div class="roi-label">Prompts Reduced</div>
135
- </div>
136
- <div class="roi-card">
137
- <div class="roi-value">${roiData.roi.timeSavedHours}h</div>
138
- <div class="roi-label">Time Saved</div>
139
- </div>
140
- </div>
141
- <div class="roi-compare">
142
- <div class="roi-compare-col">
143
- <h4>Before CRG</h4>
144
- <div class="roi-compare-stat">
145
- <span class="roi-compare-label">Avg Tokens/Session</span>
146
- <span class="roi-compare-value">${roiData.baseline.avgTokensPerSession.toLocaleString()}</span>
147
- </div>
148
- <div class="roi-compare-stat">
149
- <span class="roi-compare-label">Total Prompts</span>
150
- <span class="roi-compare-value">${roiData.baseline.totalPrompts}</span>
151
- </div>
152
- </div>
153
- <div class="roi-arrow">→</div>
154
- <div class="roi-compare-col">
155
- <h4>After CRG</h4>
156
- <div class="roi-compare-stat">
157
- <span class="roi-compare-label">Avg Tokens/Session</span>
158
- <span class="roi-compare-value" style="color:${verdictColor}">${roiData.post.avgTokensPerSession.toLocaleString()}</span>
159
- </div>
160
- <div class="roi-compare-stat">
161
- <span class="roi-compare-label">Total Prompts</span>
162
- <span class="roi-compare-value" style="color:${verdictColor}">${roiData.post.totalPrompts}</span>
163
- </div>
164
- </div>
165
- </div>
166
- <div class="roi-cta">
167
- <code>npx crg-dev-kit roi</code> for detailed report
168
- </div>
169
- </section>`;
170
- }
171
- } catch (e) {
172
- // No ROI data yet
173
- }
174
-
175
- if (analyticsData) {
176
- const savingsColor = analyticsData.avgSavingsPercent >= 70 ? '#16a34a' : analyticsData.avgSavingsPercent >= 50 ? '#ea580c' : '#dc2626';
177
- analyticsSection = `
178
- <section id="analytics">
179
- <div class="st">Token Analytics</div>
180
- <div class="analytics-grid">
181
- <div class="stat-card primary">
182
- <div class="stat-value" style="color:${savingsColor}">${analyticsData.avgSavingsPercent}%</div>
183
- <div class="stat-label">Avg Token Savings</div>
184
- <div class="stat-sub">vs reading full source files</div>
185
- </div>
186
- <div class="stat-card">
187
- <div class="stat-value">${formatTokens(analyticsData.totalTokensSaved)}</div>
188
- <div class="stat-label">Total Tokens Saved</div>
189
- <div class="stat-sub">${analyticsData.totalSessions} sessions across ${analyticsData.totalProjects} projects</div>
190
- </div>
191
- <div class="stat-card">
192
- <div class="stat-value">$${analyticsData.estimatedCostSavings}</div>
193
- <div class="stat-label">Estimated Cost Savings</div>
194
- <div class="stat-sub">at $10/M input tokens</div>
195
- </div>
196
- </div>
197
- ${analyticsData.projects && analyticsData.projects.length > 0 ? `
198
- <div class="project-table">
199
- <table>
200
- <thead><tr><th>Project</th><th>Sessions</th><th>Files</th><th>Savings</th><th>Tokens Saved</th></tr></thead>
201
- <tbody>
202
- ${analyticsData.projects.map(p => `
203
- <tr>
204
- <td><code>${esc(p.project.split('/').pop())}</code></td>
205
- <td>${p.totalSessions}</td>
206
- <td>${p.totalFilesReviewed}</td>
207
- <td><span class="savings-badge" style="color:${p.avgSavingsPercent >= 70 ? '#16a34a' : '#ea580c'}">${p.avgSavingsPercent}%</span></td>
208
- <td>${formatTokens(p.totalTokensSaved)}</td>
209
- </tr>`).join('')}
210
- </tbody>
211
- </table>
212
- </div>` : ''}
213
- ${analyticsData.projects && analyticsData.projects.length > 0 && analyticsData.toolUsage ? `
214
- <div class="tool-usage-section">
215
- <h3>Most Used Tools</h3>
216
- <div class="tool-bars">
217
- ${Object.entries(analyticsData.toolUsage)
218
- .sort((a, b) => b[1] - a[1])
219
- .slice(0, 6)
220
- .map(([tool, count]) => {
221
- const maxCount = Math.max(...Object.values(analyticsData.toolUsage));
222
- const pct = (count / maxCount * 100).toFixed(0);
223
- return `<div class="tool-bar">
224
- <span class="tool-bar-name">${tool}</span>
225
- <div class="tool-bar-track"><div class="tool-bar-fill" style="width:${pct}%"></div></div>
226
- <span class="tool-bar-count">${count}</span>
227
- </div>`;
228
- }).join('')}
229
- </div>
230
- </div>` : ''}
231
- </section>`;
232
- }
115
+ const navLinks = tabs.map(t =>
116
+ `<a href="#" hx-get="/tab/${t.id}" hx-target="#content" hx-swap="innerHTML" class="${t.id === 'dashboard' ? 'active' : ''}" onclick="document.querySelectorAll('.nk a').forEach(a=>a.classList.remove('active'));this.classList.add('active')">${t.label}</a>`
117
+ ).join('');
233
118
 
234
119
  return `<!DOCTYPE html>
235
120
  <html lang="en">
236
121
  <head>
237
122
  <meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
238
123
  <title>code-review-graph Dev Kit</title>
124
+ <script src="https://unpkg.com/htmx.org@2.0.4"></script>
239
125
  <style>
240
126
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
241
127
  *{margin:0;padding:0;box-sizing:border-box}
@@ -245,8 +131,9 @@ nav{background:var(--s);border-bottom:1px solid var(--b);padding:0 40px;display:
245
131
  .nb{display:flex;align-items:center;gap:10px;font-weight:800;font-size:15px;letter-spacing:-0.02em}
246
132
  .nl{width:28px;height:28px;background:var(--a);border-radius:6px;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:800;font-size:14px}
247
133
  .nk{margin-left:auto;display:flex;gap:24px}
248
- .nk a{color:var(--t3);text-decoration:none;font-size:13px;font-weight:500;transition:color .15s}
134
+ .nk a{color:var(--t3);text-decoration:none;font-size:13px;font-weight:500;transition:color .15s;padding-bottom:4px}
249
135
  .nk a:hover{color:var(--a)}
136
+ .nk a.active{color:var(--a);border-bottom:2px solid var(--a)}
250
137
  .badge{background:var(--al);color:var(--a);padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700;border:1px solid var(--ab)}
251
138
  .hero{text-align:center;padding:64px 40px 48px;max-width:680px;margin:0 auto}
252
139
  .hero h1{font-size:36px;font-weight:800;letter-spacing:-0.03em;line-height:1.15;margin-bottom:12px}
@@ -338,9 +225,35 @@ footer a{color:var(--a);text-decoration:none;font-weight:600}
338
225
  .tool-name{font-family:var(--m);font-size:11px;color:var(--a);font-weight:600}
339
226
  .tool-label{font-size:12px;font-weight:700;color:var(--t)}
340
227
  .tool-desc{font-size:11px;color:var(--t3)}
228
+ /* Tab navigation */
229
+ .nk a.active{color:var(--a);border-bottom:2px solid var(--a)}
230
+ /* Action buttons */
231
+ .btn{background:var(--a);color:#fff;padding:10px 20px;border:none;border-radius:6px;font-weight:700;font-size:13px;cursor:pointer;font-family:inherit}
232
+ .btn:hover{background:#c2410c}
233
+ .btn-danger{background:#dc2626}
234
+ .btn-danger:hover{background:#b91c1c}
235
+ .btn-secondary{background:var(--bg);color:var(--t2);border:1px solid var(--b)}
236
+ /* Status checks */
237
+ .check-pass{color:#16a34a}
238
+ .check-fail{color:#dc2626}
239
+ .check-item{display:flex;align-items:center;gap:8px;padding:8px 0;border-bottom:1px solid var(--b)}
240
+ .check-item:last-child{border-bottom:none}
241
+ /* Result messages */
242
+ .result-box{background:var(--s);border:1px solid var(--b);border-radius:8px;padding:20px;margin-top:16px}
243
+ .result-success{border-color:#16a34a;background:#f0fdf4}
244
+ .result-error{border-color:#dc2626;background:#fef2f2}
245
+ /* Input fields */
246
+ .input{font-family:var(--m);font-size:13px;padding:8px 12px;border:1px solid var(--b);border-radius:6px;width:100%;background:var(--s)}
247
+ .input:focus{outline:none;border-color:var(--a)}
248
+ /* htmx loading indicator */
249
+ .htmx-indicator{display:none}
250
+ .htmx-request .htmx-indicator{display:inline-block}
251
+ .spinner{width:16px;height:16px;border:2px solid var(--b);border-top-color:var(--a);border-radius:50%;animation:spin .6s linear infinite;display:inline-block}
252
+ @keyframes spin{to{transform:rotate(360deg)}}
341
253
  @media(max-width:768px){
342
254
  .analytics-grid{grid-template-columns:1fr}
343
255
  .tools-grid{grid-template-columns:repeat(2,1fr)}
256
+ .roi-grid{grid-template-columns:repeat(2,1fr)}
344
257
  nav{padding:0 20px}
345
258
  .hero{padding:48px 20px 32px}
346
259
  section{padding:0 20px 32px}
@@ -349,48 +262,312 @@ footer a{color:var(--a);text-decoration:none;font-weight:600}
349
262
  </style>
350
263
  </head>
351
264
  <body>
352
- <nav><div class="nb"><div class="nl">G</div>code-review-graph</div><div class="nk"><a href="#steps">Setup</a><a href="#roi">ROI</a><a href="#analytics">Analytics</a><a href="#tools">Tools</a><a href="#downloads">Downloads</a><a href="#docs">Docs</a><span class="badge">v2.1.0</span></div></nav>
353
- <div class="hero"><h1>Ship code with a <span>knowledge graph</span></h1><p>One command to set up AI-powered code review, impact analysis, and codebase navigation. Track token savings across your team.</p><div class="hs"><div><div class="sv">22</div><div class="sl">MCP Tools</div></div><div><div class="sv">19</div><div class="sl">Languages</div></div><div><div class="sv">8.2x</div><div class="sl">Token Reduction</div></div><div><div class="sv">0</div><div class="sl">Telemetry</div></div></div></div>
354
- <section id="steps"><div class="st">Integration Steps</div><div class="npx-banner"><code>npx crg-dev-kit</code><span class="dim">opens this page</span><span style="color:#a8a29e">|</span><span class="alt">npx crg-dev-kit install</span><span class="dim">copies scripts to your project</span></div><div class="steps">${stepCards}</div></section>
355
- ${roiSection}
356
- ${analyticsSection}
357
- <section id="tools"><div class="st">Available Tools</div><div class="tools-grid">${toolCards}</div></section>
358
- <section id="downloads"><div class="st">Downloads</div><div class="dl-grid">${dlCards}</div></section>
359
- <section id="docs"><div class="st">Documentation</div><div class="rc">${readmeHtml}</div></section>
265
+ <nav>
266
+ <div class="nb"><div class="nl">G</div>code-review-graph</div>
267
+ <div class="nk">${navLinks}<span class="badge">v2.1.0</span></div>
268
+ </nav>
269
+ <main id="content" hx-get="/tab/dashboard" hx-trigger="load" hx-swap="innerHTML"></main>
360
270
  <footer><a href="https://github.com/tirth8205/code-review-graph">github.com/tirth8205/code-review-graph</a> &middot; MIT License &middot; All data stays local</footer>
361
271
  </body></html>`;
362
272
  }
363
273
 
364
- function openBrowser(url) {
365
- const plat = process.platform;
366
- if (plat === 'win32') execFile('cmd', ['/c', 'start', url]);
367
- else if (plat === 'darwin') execFile('open', [url]);
368
- else execFile('xdg-open', [url]);
274
+ /* ── Tab fragments ────────────────────────────────────────────────────── */
275
+
276
+ function tabDashboard() {
277
+ return `
278
+ <div class="hero">
279
+ <h1>Ship code with a <span>knowledge graph</span></h1>
280
+ <p>One command to set up AI-powered code review, impact analysis, and codebase navigation. Track token savings across your team.</p>
281
+ <div class="hs">
282
+ <div><div class="sv">22</div><div class="sl">MCP Tools</div></div>
283
+ <div><div class="sv">19</div><div class="sl">Languages</div></div>
284
+ <div><div class="sv">8.2x</div><div class="sl">Token Reduction</div></div>
285
+ <div><div class="sv">0</div><div class="sl">Telemetry</div></div>
286
+ </div>
287
+ </div>
288
+ <section>
289
+ <div class="st">Quick Status</div>
290
+ <div id="dashboard-status" hx-get="/api/health?fragment=1" hx-trigger="load" hx-swap="innerHTML">
291
+ <span class="spinner"></span> Loading status...
292
+ </div>
293
+ </section>`;
369
294
  }
370
295
 
371
- function findAvailablePort(startPort, maxAttempts = 10) {
372
- return new Promise((resolve, reject) => {
373
- const tryPort = (port, attempt) => {
374
- if (attempt > maxAttempts) {
375
- reject(new Error(`No available port found after ${maxAttempts} attempts`));
376
- return;
377
- }
378
- const server = http.createServer();
379
- server.on('error', (err) => {
380
- if (err.code === 'EADDRINUSE') {
381
- server.close();
382
- tryPort(port + 1, attempt + 1);
383
- } else {
384
- reject(err);
385
- }
386
- });
387
- server.on('listening', () => {
388
- server.close();
389
- resolve(port);
390
- });
391
- server.listen(port);
392
- };
393
- tryPort(startPort, 1);
296
+ function tabInstall(cwd) {
297
+ return `
298
+ <section>
299
+ <div class="st">Install CRG Dev Kit</div>
300
+ <p style="color:var(--t2);font-size:14px;margin-bottom:16px">Copy setup scripts, CLAUDE.md, and health check to your project directory.</p>
301
+ <div style="margin-bottom:16px">
302
+ <label style="font-size:12px;font-weight:600;color:var(--t3);display:block;margin-bottom:6px">Target Directory</label>
303
+ <input class="input" id="install-dir" name="targetDir" value="${esc(cwd)}" />
304
+ </div>
305
+ <div style="display:flex;gap:12px">
306
+ <button class="btn"
307
+ hx-post="/api/install"
308
+ hx-target="#install-result"
309
+ hx-swap="innerHTML"
310
+ hx-include="#install-dir"
311
+ hx-indicator="#install-spinner">
312
+ Install Files <span id="install-spinner" class="htmx-indicator"><span class="spinner"></span></span>
313
+ </button>
314
+ <button class="btn btn-danger"
315
+ hx-post="/api/uninstall"
316
+ hx-target="#install-result"
317
+ hx-swap="innerHTML"
318
+ hx-include="#install-dir"
319
+ hx-indicator="#uninstall-spinner">
320
+ Uninstall <span id="uninstall-spinner" class="htmx-indicator"><span class="spinner"></span></span>
321
+ </button>
322
+ </div>
323
+ <div id="install-result"></div>
324
+ </section>`;
325
+ }
326
+
327
+ function tabStatusFragment(statusData) {
328
+ const items = statusData.checks.map(c => `
329
+ <div class="check-item">
330
+ <span class="${c.exists ? 'check-pass' : 'check-fail'}">${c.exists ? '&#10003;' : '&#10007;'}</span>
331
+ <span style="font-size:13px">${esc(c.label)}</span>
332
+ <span style="font-family:var(--m);font-size:11px;color:var(--t4);margin-left:auto">${esc(c.file)}</span>
333
+ </div>`).join('');
334
+
335
+ const crgItem = `
336
+ <div class="check-item">
337
+ <span class="${statusData.crgInstalled ? 'check-pass' : 'check-fail'}">${statusData.crgInstalled ? '&#10003;' : '&#10007;'}</span>
338
+ <span style="font-size:13px">code-review-graph CLI</span>
339
+ <span style="font-family:var(--m);font-size:11px;color:var(--t4);margin-left:auto">${statusData.crgVersion ? esc(statusData.crgVersion) : 'not found'}</span>
340
+ </div>`;
341
+
342
+ const verdictColors = { ready: '#16a34a', partial: '#ea580c', not_setup: '#dc2626' };
343
+ const verdictLabels = { ready: 'Ready', partial: 'Partial Setup', not_setup: 'Not Set Up' };
344
+ const color = verdictColors[statusData.verdict] || '#dc2626';
345
+ const label = verdictLabels[statusData.verdict] || statusData.verdict;
346
+
347
+ return `
348
+ <section>
349
+ <div class="st">Status</div>
350
+ <div class="result-box" style="border-color:${color}">
351
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:16px">
352
+ <span style="font-size:18px;font-weight:800;color:${color}">${statusData.found}/${statusData.total}</span>
353
+ <span style="font-size:14px;font-weight:600;color:${color}">${label}</span>
354
+ </div>
355
+ <div style="font-family:var(--m);font-size:11px;color:var(--t4);margin-bottom:12px">${esc(statusData.targetDir)}</div>
356
+ ${items}
357
+ ${crgItem}
358
+ </div>
359
+ </section>`;
360
+ }
361
+
362
+ function tabAnalytics() {
363
+ let roiHtml = '';
364
+ try {
365
+ const roiData = roi.getAllProjectsROI();
366
+ if (roiData.installed) {
367
+ const verdictColor = roiData.roi.verdict === 'positive' ? '#16a34a' : roiData.roi.verdict === 'neutral' ? '#ea580c' : '#dc2626';
368
+ const verdictIcon = roiData.roi.verdict === 'positive' ? '&#10003;' : roiData.roi.verdict === 'neutral' ? '&#8722;' : '&#9888;';
369
+ roiHtml = `
370
+ <div class="st">ROI Calculator</div>
371
+ <div class="roi-banner">
372
+ <div class="roi-verdict" style="background:${verdictColor}20;border-color:${verdictColor}">
373
+ <span class="roi-verdict-icon">${verdictIcon}</span>
374
+ <span class="roi-verdict-text">${roiData.roi.verdict === 'positive' ? 'CRG is helping' : roiData.roi.verdict === 'neutral' ? 'No significant change' : 'CRG may not be helping'}</span>
375
+ </div>
376
+ <div class="roi-installed">Installed: ${new Date(roiData.installDate).toLocaleDateString()}</div>
377
+ </div>
378
+ <div class="roi-grid">
379
+ <div class="roi-card">
380
+ <div class="roi-value" style="color:${verdictColor}">${roiData.roi.tokenSavingsPercent}%</div>
381
+ <div class="roi-label">Token Savings</div>
382
+ </div>
383
+ <div class="roi-card">
384
+ <div class="roi-value">${roiData.roi.tokensSaved.toLocaleString()}</div>
385
+ <div class="roi-label">Tokens Saved</div>
386
+ </div>
387
+ <div class="roi-card">
388
+ <div class="roi-value">${roiData.roi.promptReduction}</div>
389
+ <div class="roi-label">Prompts Reduced</div>
390
+ </div>
391
+ <div class="roi-card">
392
+ <div class="roi-value">${roiData.roi.timeSavedHours}h</div>
393
+ <div class="roi-label">Time Saved</div>
394
+ </div>
395
+ </div>
396
+ <div class="roi-compare">
397
+ <div class="roi-compare-col">
398
+ <h4>Before CRG</h4>
399
+ <div class="roi-compare-stat">
400
+ <span class="roi-compare-label">Avg Tokens/Session</span>
401
+ <span class="roi-compare-value">${roiData.baseline.avgTokensPerSession.toLocaleString()}</span>
402
+ </div>
403
+ <div class="roi-compare-stat">
404
+ <span class="roi-compare-label">Total Prompts</span>
405
+ <span class="roi-compare-value">${roiData.baseline.totalPrompts}</span>
406
+ </div>
407
+ </div>
408
+ <div class="roi-arrow">&rarr;</div>
409
+ <div class="roi-compare-col">
410
+ <h4>After CRG</h4>
411
+ <div class="roi-compare-stat">
412
+ <span class="roi-compare-label">Avg Tokens/Session</span>
413
+ <span class="roi-compare-value" style="color:${verdictColor}">${roiData.post.avgTokensPerSession.toLocaleString()}</span>
414
+ </div>
415
+ <div class="roi-compare-stat">
416
+ <span class="roi-compare-label">Total Prompts</span>
417
+ <span class="roi-compare-value" style="color:${verdictColor}">${roiData.post.totalPrompts}</span>
418
+ </div>
419
+ </div>
420
+ </div>
421
+ <div class="roi-cta">
422
+ <code>npx crg-dev-kit roi</code> for detailed report
423
+ </div>
424
+ <div style="margin-bottom:32px"></div>`;
425
+ }
426
+ } catch (e) {
427
+ // No ROI data yet
428
+ }
429
+
430
+ let tokenHtml = '';
431
+ const analyticsData = analytics.getAllStats();
432
+ if (analyticsData) {
433
+ const savingsColor = analyticsData.avgSavingsPercent >= 70 ? '#16a34a' : analyticsData.avgSavingsPercent >= 50 ? '#ea580c' : '#dc2626';
434
+ tokenHtml = `
435
+ <div class="st">Token Analytics</div>
436
+ <div class="analytics-grid">
437
+ <div class="stat-card primary">
438
+ <div class="stat-value" style="color:${savingsColor}">${analyticsData.avgSavingsPercent}%</div>
439
+ <div class="stat-label">Avg Token Savings</div>
440
+ <div class="stat-sub">vs reading full source files</div>
441
+ </div>
442
+ <div class="stat-card">
443
+ <div class="stat-value">${formatTokens(analyticsData.totalTokensSaved)}</div>
444
+ <div class="stat-label">Total Tokens Saved</div>
445
+ <div class="stat-sub">${analyticsData.totalSessions} sessions across ${analyticsData.totalProjects} projects</div>
446
+ </div>
447
+ <div class="stat-card">
448
+ <div class="stat-value">$${analyticsData.estimatedCostSavings}</div>
449
+ <div class="stat-label">Estimated Cost Savings</div>
450
+ <div class="stat-sub">at $10/M input tokens</div>
451
+ </div>
452
+ </div>
453
+ ${analyticsData.projects && analyticsData.projects.length > 0 ? `
454
+ <div class="project-table">
455
+ <table>
456
+ <thead><tr><th>Project</th><th>Sessions</th><th>Files</th><th>Savings</th><th>Tokens Saved</th></tr></thead>
457
+ <tbody>
458
+ ${analyticsData.projects.map(p => `
459
+ <tr>
460
+ <td><code>${esc(p.project.split('/').pop())}</code></td>
461
+ <td>${p.totalSessions}</td>
462
+ <td>${p.totalFilesReviewed}</td>
463
+ <td><span class="savings-badge" style="color:${p.avgSavingsPercent >= 70 ? '#16a34a' : '#ea580c'}">${p.avgSavingsPercent}%</span></td>
464
+ <td>${formatTokens(p.totalTokensSaved)}</td>
465
+ </tr>`).join('')}
466
+ </tbody>
467
+ </table>
468
+ </div>` : ''}
469
+ ${analyticsData.projects && analyticsData.projects.length > 0 && analyticsData.toolUsage ? `
470
+ <div class="tool-usage-section">
471
+ <h3>Most Used Tools</h3>
472
+ <div class="tool-bars">
473
+ ${Object.entries(analyticsData.toolUsage)
474
+ .sort((a, b) => b[1] - a[1])
475
+ .slice(0, 6)
476
+ .map(([tool, count]) => {
477
+ const maxCount = Math.max(...Object.values(analyticsData.toolUsage));
478
+ const pct = (count / maxCount * 100).toFixed(0);
479
+ return `<div class="tool-bar">
480
+ <span class="tool-bar-name">${tool}</span>
481
+ <div class="tool-bar-track"><div class="tool-bar-fill" style="width:${pct}%"></div></div>
482
+ <span class="tool-bar-count">${count}</span>
483
+ </div>`;
484
+ }).join('')}
485
+ </div>
486
+ </div>` : ''}`;
487
+ } else {
488
+ tokenHtml = `<div class="st">Token Analytics</div><p style="color:var(--t3);font-size:14px">No analytics data yet. Use CRG tools in a project to start tracking.</p>`;
489
+ }
490
+
491
+ return `<section>${roiHtml}${tokenHtml}</section>`;
492
+ }
493
+
494
+ function tabTools() {
495
+ const toolCards = CRG_TOOLS.map(t => `
496
+ <div class="tool-card">
497
+ <span class="tool-name">${t.name}</span>
498
+ <span class="tool-label">${t.label}</span>
499
+ <span class="tool-desc">${t.desc}</span>
500
+ </div>`).join('');
501
+ return `<section><div class="st">Available Tools</div><div class="tools-grid">${toolCards}</div></section>`;
502
+ }
503
+
504
+ function tabDownloads() {
505
+ const dlCards = DOWNLOADS.map(d => `
506
+ <a href="/download/${d.file}" class="dl-card" download>
507
+ <span class="dl-icon">${d.icon}</span>
508
+ <div class="dl-info"><span class="dl-name">${d.label}</span><span class="dl-desc">${d.desc}</span></div>
509
+ <span class="dl-size">${fileSize(d.file)}</span>
510
+ <span class="dl-btn">Download</span>
511
+ </a>`).join('');
512
+ return `<section><div class="st">Downloads</div><div class="dl-grid">${dlCards}</div></section>`;
513
+ }
514
+
515
+ function tabDocs() {
516
+ const readmePath = path.join(ASSETS, 'README.md');
517
+ const readmeHtml = fs.existsSync(readmePath) ? mdToHtml(fs.readFileSync(readmePath, 'utf8')) : '<p>No README found.</p>';
518
+ return `<section><div class="st">Documentation</div><div class="rc">${readmeHtml}</div></section>`;
519
+ }
520
+
521
+ /* ── API action fragments ─────────────────────────────────────────────── */
522
+
523
+ function renderInstallResult(result) {
524
+ const isSuccess = result.copied > 0;
525
+ const items = result.results.map(r => {
526
+ if (r.status === 'copied') return `<div class="check-item"><span class="check-pass">&#10003;</span> ${esc(r.file)} copied</div>`;
527
+ if (r.status === 'skipped') return `<div class="check-item"><span style="color:var(--t4)">&#8722;</span> ${esc(r.file)} skipped (${esc(r.reason)})</div>`;
528
+ return `<div class="check-item"><span class="check-fail">&#10007;</span> ${esc(r.file)} — source missing</div>`;
529
+ }).join('');
530
+ return `<div class="result-box ${isSuccess ? 'result-success' : ''}">${items}<p style="margin-top:12px;font-size:13px;color:var(--t3)">${result.copied} of ${result.total} files installed to <code>${esc(result.targetDir)}</code></p></div>`;
531
+ }
532
+
533
+ function renderUninstallResult(result) {
534
+ if (result.removed.length === 0) {
535
+ return `<div class="result-box"><p style="font-size:13px;color:var(--t3)">No CRG files found in <code>${esc(result.targetDir)}</code></p></div>`;
536
+ }
537
+ const items = result.removed.map(f => `<div class="check-item"><span class="check-fail">&#10007;</span> ${esc(f)} removed</div>`).join('');
538
+ return `<div class="result-box result-error">${items}<p style="margin-top:12px;font-size:13px;color:var(--t3)">${result.removed.length} files removed from <code>${esc(result.targetDir)}</code></p></div>`;
539
+ }
540
+
541
+ function renderHealthResult(result) {
542
+ const items = result.results.map(r => `
543
+ <div class="check-item">
544
+ <span class="${r.pass ? 'check-pass' : 'check-fail'}">${r.pass ? '&#10003;' : '&#10007;'}</span>
545
+ <span style="font-size:13px">${esc(r.check)}</span>
546
+ ${r.version ? `<span style="font-family:var(--m);font-size:11px;color:var(--t4);margin-left:auto">${esc(r.version)}</span>` : ''}
547
+ </div>`).join('');
548
+ return `<div class="result-box" style="border-color:${result.pass === result.total ? '#16a34a' : '#ea580c'}"><div style="font-size:14px;font-weight:600;margin-bottom:12px">${result.pass}/${result.total} checks passing</div>${items}</div>`;
549
+ }
550
+
551
+ /* ── Server ───────────────────────────────────────────────────────────── */
552
+
553
+ function readBody(req, res, cb) {
554
+ let body = '';
555
+ req.on('data', chunk => {
556
+ body += chunk;
557
+ if (body.length > MAX_BODY) {
558
+ res.writeHead(413, { 'Content-Type': 'application/json' });
559
+ res.end(JSON.stringify({ error: 'Payload too large' }));
560
+ req.destroy();
561
+ }
562
+ });
563
+ req.on('end', () => {
564
+ if (res.writableEnded) return;
565
+ try {
566
+ cb(JSON.parse(body));
567
+ } catch (e) {
568
+ res.writeHead(400, { 'Content-Type': 'application/json' });
569
+ res.end(JSON.stringify({ error: e.message }));
570
+ }
394
571
  });
395
572
  }
396
573
 
@@ -399,68 +576,170 @@ function start(port, noOpen) {
399
576
  noOpen = noOpen || false;
400
577
 
401
578
  findAvailablePort(port).then(availablePort => {
402
- const analyticsData = analytics.getAllStats();
579
+ const cwd = process.cwd();
403
580
 
404
581
  const server = http.createServer((req, res) => {
405
582
  const url = new URL(req.url, `http://localhost:${availablePort}`);
406
583
 
584
+ /* ── Shell page ─────────────────────────────────────────────── */
407
585
  if (url.pathname === '/' || url.pathname === '') {
408
- const html = buildPage(availablePort, analyticsData);
586
+ const html = buildPage();
409
587
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
410
588
  res.end(html);
589
+
590
+ /* ── Tab fragments (htmx) ───────────────────────────────────── */
591
+ } else if (url.pathname === '/tab/dashboard') {
592
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
593
+ res.end(tabDashboard());
594
+
595
+ } else if (url.pathname === '/tab/install') {
596
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
597
+ res.end(tabInstall(cwd));
598
+
599
+ } else if (url.pathname === '/tab/status') {
600
+ actions.status(cwd).then(statusData => {
601
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
602
+ res.end(tabStatusFragment(statusData));
603
+ }).catch(err => {
604
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
605
+ res.end(`<section><div class="result-box result-error"><p>Error: ${esc(err.message)}</p></div></section>`);
606
+ });
607
+
608
+ } else if (url.pathname === '/tab/analytics') {
609
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
610
+ res.end(tabAnalytics());
611
+
612
+ } else if (url.pathname === '/tab/tools') {
613
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
614
+ res.end(tabTools());
615
+
616
+ } else if (url.pathname === '/tab/downloads') {
617
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
618
+ res.end(tabDownloads());
619
+
620
+ } else if (url.pathname === '/tab/docs') {
621
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
622
+ res.end(tabDocs());
623
+
624
+ /* ── Action API endpoints (return HTML fragments) ───────────── */
625
+ } else if (url.pathname === '/api/install' && req.method === 'POST') {
626
+ readBody(req, res, (data) => {
627
+ const targetDir = data.targetDir || cwd;
628
+ try {
629
+ const result = actions.install(targetDir);
630
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
631
+ res.end(renderInstallResult(result));
632
+ } catch (err) {
633
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
634
+ res.end(`<div class="result-box result-error"><p>Error: ${esc(err.message)}</p></div>`);
635
+ }
636
+ });
637
+
638
+ } else if (url.pathname === '/api/uninstall' && req.method === 'POST') {
639
+ readBody(req, res, (data) => {
640
+ const targetDir = data.targetDir || cwd;
641
+ try {
642
+ const result = actions.uninstall(targetDir);
643
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
644
+ res.end(renderUninstallResult(result));
645
+ } catch (err) {
646
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
647
+ res.end(`<div class="result-box result-error"><p>Error: ${esc(err.message)}</p></div>`);
648
+ }
649
+ });
650
+
651
+ } else if (url.pathname === '/api/health') {
652
+ actions.healthCheck(url.searchParams.get('targetDir') || cwd).then(result => {
653
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
654
+ res.end(renderHealthResult(result));
655
+ }).catch(err => {
656
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
657
+ res.end(`<div class="result-box result-error"><p>Error: ${esc(err.message)}</p></div>`);
658
+ });
659
+
660
+ /* ── Existing JSON API endpoints ────────────────────────────── */
411
661
  } else if (url.pathname === '/api/analytics') {
412
662
  const data = analytics.getAllStats();
413
663
  res.writeHead(200, { 'Content-Type': 'application/json' });
414
664
  res.end(JSON.stringify(data || { message: 'No analytics data yet' }));
665
+
415
666
  } else if (url.pathname === '/api/roi') {
416
667
  const data = roi.getAllProjectsROI();
417
668
  res.writeHead(200, { 'Content-Type': 'application/json' });
418
669
  res.end(JSON.stringify(data));
670
+
419
671
  } else if (url.pathname === '/api/report') {
420
672
  const report = analytics.generateReport();
421
673
  res.writeHead(200, { 'Content-Type': 'text/markdown' });
422
674
  res.end(report);
423
- } else if (url.pathname === '/api/session') {
424
- if (req.method === 'POST') {
425
- let body = '';
426
- req.on('data', chunk => { body += chunk; });
427
- req.on('end', () => {
428
- try {
429
- const data = JSON.parse(body);
430
- const session = analytics.createSession(data.project, data.operation);
431
- res.writeHead(201, { 'Content-Type': 'application/json' });
432
- res.end(JSON.stringify(session));
433
- } catch (e) {
434
- res.writeHead(400, { 'Content-Type': 'application/json' });
435
- res.end(JSON.stringify({ error: e.message }));
436
- }
437
- });
438
- } else {
439
- res.writeHead(405); res.end('Method not allowed');
440
- }
675
+
676
+ } else if (url.pathname === '/api/session' && req.method === 'POST') {
677
+ readBody(req, res, (data) => {
678
+ const session = analytics.createSession(data.project, data.operation);
679
+ res.writeHead(201, { 'Content-Type': 'application/json' });
680
+ res.end(JSON.stringify(session));
681
+ });
682
+
683
+ } else if (url.pathname === '/api/session' && req.method !== 'POST') {
684
+ res.writeHead(405); res.end('Method not allowed');
685
+
686
+ } else if (url.pathname.match(/^\/api\/session\/[^/]+\/tool$/) && req.method === 'POST') {
687
+ const sessionId = url.pathname.split('/')[3];
688
+ readBody(req, res, (data) => {
689
+ if (!data.tool) {
690
+ res.writeHead(400, { 'Content-Type': 'application/json' });
691
+ res.end(JSON.stringify({ error: 'Missing tool field' }));
692
+ return;
693
+ }
694
+ const session = analytics.logToolUsage(sessionId, data.tool, data.count);
695
+ if (!session) {
696
+ res.writeHead(404, { 'Content-Type': 'application/json' });
697
+ res.end(JSON.stringify({ error: 'Session not found' }));
698
+ return;
699
+ }
700
+ res.writeHead(200, { 'Content-Type': 'application/json' });
701
+ res.end(JSON.stringify(session));
702
+ });
703
+
704
+ } else if (url.pathname.match(/^\/api\/session\/[^/]+\/end$/) && req.method === 'POST') {
705
+ const sessionId = url.pathname.split('/')[3];
706
+ readBody(req, res, (data) => {
707
+ if (data.filesReviewed == null) {
708
+ res.writeHead(400, { 'Content-Type': 'application/json' });
709
+ res.end(JSON.stringify({ error: 'Missing filesReviewed field' }));
710
+ return;
711
+ }
712
+ const session = analytics.endSession(sessionId, data.filesReviewed, data.avgLinesPerFile);
713
+ if (!session) {
714
+ res.writeHead(404, { 'Content-Type': 'application/json' });
715
+ res.end(JSON.stringify({ error: 'Session not found' }));
716
+ return;
717
+ }
718
+ res.writeHead(200, { 'Content-Type': 'application/json' });
719
+ res.end(JSON.stringify(session));
720
+ });
721
+
441
722
  } else if (url.pathname.startsWith('/api/session/') && req.method === 'POST') {
442
- const sessionId = url.pathname.split('/').pop();
443
- let body = '';
444
- req.on('data', chunk => { body += chunk; });
445
- req.on('end', () => {
446
- try {
447
- const data = JSON.parse(body);
448
- if (data.tool) {
449
- const session = analytics.logToolUsage(sessionId, data.tool, data.count);
450
- res.writeHead(200, { 'Content-Type': 'application/json' });
451
- res.end(JSON.stringify(session));
452
- } else if (data.filesReviewed) {
453
- const session = analytics.endSession(sessionId, data.filesReviewed, data.avgLinesPerFile);
454
- res.writeHead(200, { 'Content-Type': 'application/json' });
455
- res.end(JSON.stringify(session));
456
- } else {
457
- res.writeHead(400); res.end(JSON.stringify({ error: 'Missing tool or filesReviewed' }));
458
- }
459
- } catch (e) {
723
+ // Backwards compat: old clients POST to /api/session/{id} directly
724
+ const sessionId = url.pathname.split('/')[3];
725
+ readBody(req, res, (data) => {
726
+ if (data.tool) {
727
+ const session = analytics.logToolUsage(sessionId, data.tool, data.count);
728
+ if (!session) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Session not found' })); return; }
729
+ res.writeHead(200, { 'Content-Type': 'application/json' });
730
+ res.end(JSON.stringify(session));
731
+ } else if (data.filesReviewed != null) {
732
+ const session = analytics.endSession(sessionId, data.filesReviewed, data.avgLinesPerFile);
733
+ if (!session) { res.writeHead(404, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Session not found' })); return; }
734
+ res.writeHead(200, { 'Content-Type': 'application/json' });
735
+ res.end(JSON.stringify(session));
736
+ } else {
460
737
  res.writeHead(400, { 'Content-Type': 'application/json' });
461
- res.end(JSON.stringify({ error: e.message }));
738
+ res.end(JSON.stringify({ error: 'Missing tool or filesReviewed' }));
462
739
  }
463
740
  });
741
+
742
+ /* ── File downloads ─────────────────────────────────────────── */
464
743
  } else if (url.pathname.startsWith('/download/')) {
465
744
  const fname = path.basename(url.pathname);
466
745
  const fpath = path.join(ASSETS, fname);
@@ -479,15 +758,17 @@ function start(port, noOpen) {
479
758
  } else {
480
759
  res.writeHead(404); res.end('Not found');
481
760
  }
761
+
482
762
  } else {
483
763
  res.writeHead(404); res.end('Not found');
484
764
  }
485
765
  });
486
766
 
487
- server.listen(availablePort, () => {
767
+ server.listen(availablePort, '127.0.0.1', () => {
488
768
  const url = `http://localhost:${availablePort}`;
489
769
  const portMsg = availablePort !== port ? ` (port ${port} was busy, using ${availablePort})` : '';
490
770
  console.log(`\n \x1b[36mCRG Dev Kit\x1b[0m running at \x1b[1m${url}\x1b[0m${portMsg}\n`);
771
+ const analyticsData = analytics.getAllStats();
491
772
  if (analyticsData) {
492
773
  console.log(` \x1b[32m${analyticsData.avgSavingsPercent}% avg token savings\x1b[0m across \x1b[1m${analyticsData.totalSessions}\x1b[0m sessions\n`);
493
774
  }