milens 0.6.3 → 0.6.5

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.
Files changed (155) hide show
  1. package/.agents/skills/adapters/SKILL.md +51 -0
  2. package/.agents/skills/analyzer/SKILL.md +77 -0
  3. package/.agents/skills/apps/SKILL.md +66 -0
  4. package/.agents/skills/docs/SKILL.md +73 -0
  5. package/.agents/skills/milens/SKILL.md +198 -0
  6. package/.agents/skills/milens-architect/SKILL.md +128 -0
  7. package/.agents/skills/milens-code-review/SKILL.md +186 -0
  8. package/.agents/skills/milens-debugger/SKILL.md +141 -0
  9. package/.agents/skills/milens-eval/SKILL.md +221 -0
  10. package/.agents/skills/milens-plan/SKILL.md +227 -0
  11. package/.agents/skills/milens-refactor-clean/SKILL.md +209 -0
  12. package/.agents/skills/milens-security-review/SKILL.md +224 -0
  13. package/.agents/skills/milens-tdd/SKILL.md +156 -0
  14. package/.agents/skills/orchestrator/SKILL.md +59 -0
  15. package/.agents/skills/parser/SKILL.md +81 -0
  16. package/.agents/skills/root/SKILL.md +86 -0
  17. package/.agents/skills/scripts/SKILL.md +45 -0
  18. package/.agents/skills/security/SKILL.md +65 -0
  19. package/.agents/skills/server/SKILL.md +72 -0
  20. package/.agents/skills/store/SKILL.md +75 -0
  21. package/.agents/skills/test/SKILL.md +121 -0
  22. package/LICENSE +21 -75
  23. package/README.md +356 -453
  24. package/adapters/README.md +107 -0
  25. package/adapters/claude-code/.claude/mcp.json +9 -0
  26. package/adapters/claude-code/CLAUDE.md +58 -0
  27. package/adapters/codex/.codex/codex.md +52 -0
  28. package/adapters/copilot/.github/copilot-instructions.md +62 -0
  29. package/adapters/cursor/.cursorrules +9 -0
  30. package/adapters/gemini/.gemini/context.md +58 -0
  31. package/adapters/opencode/.opencode/config.json +9 -0
  32. package/adapters/opencode/AGENTS.md +58 -0
  33. package/adapters/zed/.zed/settings.json +8 -0
  34. package/dist/agents-md.d.ts +3 -0
  35. package/dist/agents-md.d.ts.map +1 -0
  36. package/dist/agents-md.js +114 -0
  37. package/dist/agents-md.js.map +1 -0
  38. package/dist/analyzer/engine.d.ts +1 -0
  39. package/dist/analyzer/engine.d.ts.map +1 -1
  40. package/dist/analyzer/engine.js +37 -7
  41. package/dist/analyzer/engine.js.map +1 -1
  42. package/dist/cli.js +1472 -406
  43. package/dist/cli.js.map +1 -1
  44. package/dist/metrics.d.ts +51 -0
  45. package/dist/metrics.d.ts.map +1 -0
  46. package/dist/metrics.js +64 -0
  47. package/dist/metrics.js.map +1 -0
  48. package/dist/orchestrator/orchestrator.d.ts +65 -0
  49. package/dist/orchestrator/orchestrator.d.ts.map +1 -0
  50. package/dist/orchestrator/orchestrator.js +178 -0
  51. package/dist/orchestrator/orchestrator.js.map +1 -0
  52. package/dist/orchestrator/reporter.d.ts +15 -0
  53. package/dist/orchestrator/reporter.d.ts.map +1 -0
  54. package/dist/orchestrator/reporter.js +38 -0
  55. package/dist/orchestrator/reporter.js.map +1 -0
  56. package/dist/parser/lang-go.js +47 -47
  57. package/dist/parser/lang-java.js +29 -29
  58. package/dist/parser/lang-js.js +105 -105
  59. package/dist/parser/lang-php.js +38 -38
  60. package/dist/parser/lang-py.js +34 -34
  61. package/dist/parser/lang-ruby.js +14 -14
  62. package/dist/parser/lang-rust.js +30 -30
  63. package/dist/parser/lang-ts.js +191 -191
  64. package/dist/security/deps.d.ts +38 -0
  65. package/dist/security/deps.d.ts.map +1 -0
  66. package/dist/security/deps.js +685 -0
  67. package/dist/security/deps.js.map +1 -0
  68. package/dist/security/rules.d.ts +42 -0
  69. package/dist/security/rules.d.ts.map +1 -0
  70. package/dist/security/rules.js +943 -0
  71. package/dist/security/rules.js.map +1 -0
  72. package/dist/server/hooks.d.ts +29 -0
  73. package/dist/server/hooks.d.ts.map +1 -0
  74. package/dist/server/hooks.js +332 -0
  75. package/dist/server/hooks.js.map +1 -0
  76. package/dist/server/mcp-prompts.d.ts +277 -0
  77. package/dist/server/mcp-prompts.d.ts.map +1 -0
  78. package/dist/server/mcp-prompts.js +627 -0
  79. package/dist/server/mcp-prompts.js.map +1 -0
  80. package/dist/server/mcp.d.ts.map +1 -1
  81. package/dist/server/mcp.js +1030 -652
  82. package/dist/server/mcp.js.map +1 -1
  83. package/dist/server/test-plan.d.ts +20 -0
  84. package/dist/server/test-plan.d.ts.map +1 -0
  85. package/dist/server/test-plan.js +100 -0
  86. package/dist/server/test-plan.js.map +1 -0
  87. package/dist/server/watcher.d.ts +39 -0
  88. package/dist/server/watcher.d.ts.map +1 -0
  89. package/dist/server/watcher.js +134 -0
  90. package/dist/server/watcher.js.map +1 -0
  91. package/dist/skills.js +197 -153
  92. package/dist/skills.js.map +1 -1
  93. package/dist/store/annotations.d.ts +41 -0
  94. package/dist/store/annotations.d.ts.map +1 -0
  95. package/dist/store/annotations.js +195 -0
  96. package/dist/store/annotations.js.map +1 -0
  97. package/dist/store/confidence.d.ts +28 -0
  98. package/dist/store/confidence.d.ts.map +1 -0
  99. package/dist/store/confidence.js +109 -0
  100. package/dist/store/confidence.js.map +1 -0
  101. package/dist/store/db.d.ts +53 -14
  102. package/dist/store/db.d.ts.map +1 -1
  103. package/dist/store/db.js +447 -240
  104. package/dist/store/db.js.map +1 -1
  105. package/dist/store/schema.sql +143 -116
  106. package/dist/store/vectors.js +2 -2
  107. package/dist/types.d.ts +101 -0
  108. package/dist/types.d.ts.map +1 -1
  109. package/docs/README.md +22 -0
  110. package/package.json +81 -66
  111. package/dist/gateway/analyzer.d.ts +0 -6
  112. package/dist/gateway/analyzer.d.ts.map +0 -1
  113. package/dist/gateway/analyzer.js +0 -218
  114. package/dist/gateway/analyzer.js.map +0 -1
  115. package/dist/gateway/cache.d.ts +0 -35
  116. package/dist/gateway/cache.d.ts.map +0 -1
  117. package/dist/gateway/cache.js +0 -175
  118. package/dist/gateway/cache.js.map +0 -1
  119. package/dist/gateway/config.d.ts +0 -10
  120. package/dist/gateway/config.d.ts.map +0 -1
  121. package/dist/gateway/config.js +0 -167
  122. package/dist/gateway/config.js.map +0 -1
  123. package/dist/gateway/context-memory.d.ts +0 -68
  124. package/dist/gateway/context-memory.d.ts.map +0 -1
  125. package/dist/gateway/context-memory.js +0 -157
  126. package/dist/gateway/context-memory.js.map +0 -1
  127. package/dist/gateway/observability.d.ts +0 -83
  128. package/dist/gateway/observability.d.ts.map +0 -1
  129. package/dist/gateway/observability.js +0 -152
  130. package/dist/gateway/observability.js.map +0 -1
  131. package/dist/gateway/privacy.d.ts +0 -27
  132. package/dist/gateway/privacy.d.ts.map +0 -1
  133. package/dist/gateway/privacy.js +0 -139
  134. package/dist/gateway/privacy.js.map +0 -1
  135. package/dist/gateway/providers.d.ts +0 -66
  136. package/dist/gateway/providers.d.ts.map +0 -1
  137. package/dist/gateway/providers.js +0 -377
  138. package/dist/gateway/providers.js.map +0 -1
  139. package/dist/gateway/router.d.ts +0 -18
  140. package/dist/gateway/router.d.ts.map +0 -1
  141. package/dist/gateway/router.js +0 -102
  142. package/dist/gateway/router.js.map +0 -1
  143. package/dist/gateway/server.d.ts +0 -20
  144. package/dist/gateway/server.d.ts.map +0 -1
  145. package/dist/gateway/server.js +0 -387
  146. package/dist/gateway/server.js.map +0 -1
  147. package/dist/gateway/translator.d.ts +0 -19
  148. package/dist/gateway/translator.d.ts.map +0 -1
  149. package/dist/gateway/translator.js +0 -340
  150. package/dist/gateway/translator.js.map +0 -1
  151. package/dist/gateway/types.d.ts +0 -215
  152. package/dist/gateway/types.d.ts.map +0 -1
  153. package/dist/gateway/types.js +0 -3
  154. package/dist/gateway/types.js.map +0 -1
  155. package/dist/store/gateway-schema.sql +0 -53
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
3
  import { resolve, join, dirname, basename } from 'node:path';
4
- import { mkdirSync, readFileSync, rmSync } from 'node:fs';
4
+ import { mkdirSync, readFileSync, rmSync, existsSync } from 'node:fs';
5
5
  import { createHash } from 'node:crypto';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { loadAliases } from './analyzer/config.js';
@@ -19,6 +19,7 @@ program
19
19
  .option('-o, --output <dir>', 'Output directory for database')
20
20
  .option('-v, --verbose', 'Show detailed progress')
21
21
  .option('-f, --force', 'Force full re-index')
22
+ .option('--files <paths...>', 'Only re-index specific files (relative to root)')
22
23
  .option('-s, --skills', 'Generate skill files for all supported editors')
23
24
  .option('--skills-copilot', 'Generate skill files for GitHub Copilot only')
24
25
  .option('--skills-cursor', 'Generate skill files for Cursor only')
@@ -41,6 +42,7 @@ program
41
42
  force: opts.force,
42
43
  aliases,
43
44
  embeddings: opts.embeddings,
45
+ files: opts.files,
44
46
  });
45
47
  // Register in global registry
46
48
  const contentHash = createHash('sha256').update(JSON.stringify(stats)).digest('hex').slice(0, 12);
@@ -195,15 +197,19 @@ program
195
197
  program
196
198
  .command('list')
197
199
  .description('List all indexed repositories')
198
- .action(async () => {
200
+ .option('-p, --path <path>', 'Repository root path (filter by path prefix)')
201
+ .action(async (opts) => {
199
202
  const { RepoRegistry } = await import('./store/registry.js');
200
203
  const entries = new RepoRegistry().listAll();
201
- if (entries.length === 0) {
204
+ const filtered = opts.path
205
+ ? entries.filter((e) => e.rootPath.startsWith(resolve(opts.path)))
206
+ : entries;
207
+ if (filtered.length === 0) {
202
208
  console.log('No indexed repositories.');
203
209
  return;
204
210
  }
205
- console.log(`${entries.length} indexed repositories:\n`);
206
- for (const entry of entries) {
211
+ console.log(`${filtered.length} indexed repositories:\n`);
212
+ for (const entry of filtered) {
207
213
  console.log(` ${entry.rootPath}`);
208
214
  console.log(` DB: ${entry.dbPath}`);
209
215
  console.log(` Indexed: ${entry.analyzedAt}`);
@@ -242,6 +248,7 @@ program
242
248
  .command('dashboard')
243
249
  .description('Open usage analytics dashboard in your browser')
244
250
  .option('--port <port>', 'Port for the dashboard server', '3200')
251
+ .option('-p, --path <path>', 'Repository root path for annotation data')
245
252
  .action(async (opts) => {
246
253
  const { join: joinPath } = await import('node:path');
247
254
  const { homedir: getHomedir } = await import('node:os');
@@ -254,13 +261,55 @@ program
254
261
  }
255
262
  const { Database } = await import('./store/db.js');
256
263
  const db = new Database(trackDbPath);
257
- const stats = db.getToolUsageStats();
264
+ // Determine repo filter from --path
265
+ let repoFilter;
266
+ if (opts.path) {
267
+ repoFilter = resolve(opts.path);
268
+ }
269
+ const stats = db.getToolUsageStats(repoFilter);
258
270
  db.close();
259
271
  if (stats.totalCalls === 0) {
260
- console.log('No usage data yet. Use milens MCP tools first, then check back.');
272
+ if (repoFilter) {
273
+ console.log(`No usage data for ${repoFilter}. Use milens MCP tools with this repo first.`);
274
+ console.log(`Tip: run 'milens dashboard' without --path to see all repos.`);
275
+ }
276
+ else {
277
+ console.log('No usage data yet. Use milens MCP tools first, then check back.');
278
+ }
261
279
  return;
262
280
  }
263
- const html = generateDashboardHtml(stats);
281
+ let annotationStats;
282
+ if (opts.path) {
283
+ try {
284
+ const { RepoRegistry } = await import('./store/registry.js');
285
+ const projDbPath = new RepoRegistry().findDbPath(resolve(opts.path));
286
+ if (projDbPath) {
287
+ const projDb = new Database(projDbPath);
288
+ const { AnnotationStore } = await import('./store/annotations.js');
289
+ const aStore = new AnnotationStore(projDb.connection);
290
+ const allAnn = aStore.recall({ limit: 10000 });
291
+ const bands = [0, 0, 0, 0];
292
+ for (const a of allAnn) {
293
+ if (a.confidence < 0.4)
294
+ bands[0]++;
295
+ else if (a.confidence < 0.7)
296
+ bands[1]++;
297
+ else if (a.confidence < 0.9)
298
+ bands[2]++;
299
+ else
300
+ bands[3]++;
301
+ }
302
+ annotationStats = {
303
+ total: allAnn.length,
304
+ confidenceBands: bands,
305
+ recent: allAnn.slice(-10).reverse().map(a => ({ symbol: a.symbol, key: a.key, confidence: a.confidence, createdAt: a.createdAt })),
306
+ };
307
+ projDb.close();
308
+ }
309
+ }
310
+ catch { /* annotation data is optional */ }
311
+ }
312
+ const html = generateDashboardHtml(stats, annotationStats, repoFilter);
264
313
  const port = parseInt(opts.port);
265
314
  const server = createHttpServer((req, res) => {
266
315
  if (req.url === '/' || req.url === '/dashboard') {
@@ -268,10 +317,10 @@ program
268
317
  res.end(html);
269
318
  }
270
319
  else if (req.url === '/api/stats') {
271
- // Live refresh endpoint — re-read DB
320
+ // Live refresh endpoint — re-read DB with repo filter
272
321
  try {
273
322
  const liveDb = new Database(trackDbPath);
274
- const liveStats = liveDb.getToolUsageStats();
323
+ const liveStats = liveDb.getToolUsageStats(repoFilter);
275
324
  liveDb.close();
276
325
  res.writeHead(200, { 'Content-Type': 'application/json' });
277
326
  res.end(JSON.stringify(liveStats));
@@ -307,6 +356,948 @@ program
307
356
  catch { /* browser open is best-effort */ }
308
357
  });
309
358
  });
359
+ program
360
+ .command('evolve')
361
+ .description('Promote high-confidence annotations to rules/skills, flag stale patterns')
362
+ .option('-p, --path <path>', 'Repository root path', '.')
363
+ .option('-s, --schedule <action>', 'daily|weekly|install|uninstall|status')
364
+ .action(async (opts) => {
365
+ const { Database } = await import('./store/db.js');
366
+ const { RepoRegistry } = await import('./store/registry.js');
367
+ const { AnnotationStore } = await import('./store/annotations.js');
368
+ const { runDecayPass } = await import('./store/confidence.js');
369
+ const { join: pathJoin } = await import('node:path');
370
+ const { existsSync, mkdirSync, writeFileSync } = await import('node:fs');
371
+ const dbPath = new RepoRegistry().findDbPath(resolve(opts.path));
372
+ if (!dbPath) {
373
+ console.error('Not indexed. Run `milens analyze` first.');
374
+ process.exit(1);
375
+ }
376
+ const db = new Database(dbPath);
377
+ const store = new AnnotationStore(db.connection);
378
+ // Handle scheduled evolve
379
+ if (opts.schedule) {
380
+ const { execSync } = await import('node:child_process');
381
+ const { homedir } = await import('node:os');
382
+ const { join, dirname } = await import('node:path');
383
+ const milensBin = process.argv[1] || 'milens';
384
+ switch (opts.schedule) {
385
+ case 'install': {
386
+ const scheduleType = opts.scheduleType || 'weekly';
387
+ const cmd = `node ${process.argv[1]} evolve -p "${resolve(opts.path)}"`;
388
+ if (process.platform === 'win32') {
389
+ // Windows Scheduled Task
390
+ const taskName = 'MilensEvolve';
391
+ const scriptPath = join(homedir(), '.milens', 'evolve.bat');
392
+ try {
393
+ mkdirSync(dirname(scriptPath), { recursive: true });
394
+ writeFileSync(scriptPath, `@echo off\ncd /d "${resolve(opts.path)}"\n${cmd}\n`);
395
+ const interval = scheduleType === 'daily' ? 'DAILY' : 'WEEKLY';
396
+ execSync(`schtasks /Create /SC ${interval} /TN "${taskName}" /TR "${scriptPath}" /F`, { stdio: 'pipe' });
397
+ console.log(`✓ Scheduled task "${taskName}" created (${scheduleType})`);
398
+ }
399
+ catch (e) {
400
+ console.error(`✗ Failed to create scheduled task: ${e.message}`);
401
+ console.log(' You can manually run: milens evolve');
402
+ }
403
+ }
404
+ else {
405
+ // Linux/macOS cron
406
+ const cronSchedule = scheduleType === 'daily' ? '0 6 * * *' : '0 6 * * 1';
407
+ const cronEntry = `${cronSchedule} cd "${resolve(opts.path)}" && ${cmd} >> ~/.milens/evolve.log 2>&1`;
408
+ try {
409
+ const current = execSync('crontab -l 2>/dev/null || echo ""', { encoding: 'utf-8' }).trim();
410
+ const newCron = current ? `${current}\n${cronEntry}` : cronEntry;
411
+ const tmpFile = join(homedir(), '.milens', 'crontab.tmp');
412
+ mkdirSync(dirname(tmpFile), { recursive: true });
413
+ writeFileSync(tmpFile, newCron + '\n');
414
+ execSync(`crontab "${tmpFile}"`, { stdio: 'pipe' });
415
+ console.log(`✓ Cron job installed (${scheduleType})`);
416
+ }
417
+ catch (e) {
418
+ console.error(`✗ Failed to install cron job: ${e.message}`);
419
+ console.log(' Add this to your crontab:');
420
+ console.log(` ${cronEntry}`);
421
+ }
422
+ }
423
+ break;
424
+ }
425
+ case 'uninstall': {
426
+ if (process.platform === 'win32') {
427
+ try {
428
+ execSync('schtasks /Delete /TN "MilensEvolve" /F', { stdio: 'pipe' });
429
+ console.log('✓ Scheduled task "MilensEvolve" removed');
430
+ }
431
+ catch {
432
+ console.log('No scheduled task found.');
433
+ }
434
+ }
435
+ else {
436
+ try {
437
+ const current = execSync('crontab -l 2>/dev/null || echo ""', { encoding: 'utf-8' });
438
+ const filtered = current.split('\n').filter(line => !line.includes('milens evolve')).join('\n');
439
+ const tmpFile = join(homedir(), '.milens', 'crontab.tmp');
440
+ writeFileSync(tmpFile, filtered);
441
+ execSync(`crontab "${tmpFile}"`, { stdio: 'pipe' });
442
+ console.log('✓ Milens cron job removed');
443
+ }
444
+ catch {
445
+ console.log('No cron job found.');
446
+ }
447
+ }
448
+ break;
449
+ }
450
+ case 'status': {
451
+ if (process.platform === 'win32') {
452
+ try {
453
+ const result = execSync('schtasks /Query /TN "MilensEvolve"', { encoding: 'utf-8' });
454
+ console.log('Scheduled task: ACTIVE');
455
+ console.log(result.split('\n').slice(2).join('\n'));
456
+ }
457
+ catch {
458
+ console.log('No scheduled task configured.');
459
+ console.log('Run: milens evolve --schedule install');
460
+ }
461
+ }
462
+ else {
463
+ try {
464
+ const cron = execSync('crontab -l 2>/dev/null || echo ""', { encoding: 'utf-8' });
465
+ const milensLines = cron.split('\n').filter(l => l.includes('milens evolve'));
466
+ if (milensLines.length > 0) {
467
+ console.log('Scheduled cron jobs:');
468
+ milensLines.forEach(l => console.log(` ${l}`));
469
+ }
470
+ else {
471
+ console.log('No cron job configured.');
472
+ console.log('Run: milens evolve --schedule install');
473
+ }
474
+ }
475
+ catch {
476
+ console.log('No cron job configured.');
477
+ }
478
+ }
479
+ break;
480
+ }
481
+ default:
482
+ console.log(`Unknown schedule action: ${opts.schedule}`);
483
+ console.log('Use: install, uninstall, status');
484
+ }
485
+ // Don't run evolve after schedule management
486
+ db.close();
487
+ return;
488
+ }
489
+ // 1. Run decay pass
490
+ const { decayed, archived } = runDecayPass(store);
491
+ if (decayed > 0 || archived > 0) {
492
+ console.log(`Decayed: ${decayed} | Archived: ${archived}`);
493
+ }
494
+ // 2. Find promotable annotations (confidence >= 0.8)
495
+ const promotable = store.getPromotableAnnotations();
496
+ if (promotable.length === 0) {
497
+ console.log('No annotations ready for promotion.');
498
+ db.close();
499
+ return;
500
+ }
501
+ // 3. Group by key
502
+ const groups = new Map();
503
+ for (const ann of promotable) {
504
+ const arr = groups.get(ann.key) ?? [];
505
+ arr.push(ann);
506
+ groups.set(ann.key, arr);
507
+ }
508
+ // 4. Generate rule/skill files
509
+ let promoted = 0;
510
+ for (const [key, anns] of groups) {
511
+ const lines = [
512
+ `# Milens Evolved ${key.toUpperCase()} Rules`,
513
+ `# Auto-generated from ${anns.length} high-confidence annotations`,
514
+ '',
515
+ '## Discovered Knowledge',
516
+ '',
517
+ ];
518
+ for (const a of anns) {
519
+ lines.push(`- **\`${a.symbol}\`**: ${a.value}`);
520
+ }
521
+ lines.push('');
522
+ lines.push('## Using This Knowledge');
523
+ lines.push('Before editing any symbol mentioned above:');
524
+ lines.push('1. `mcp_milens_impact({target: "<symbol>", repo: "<workspaceRoot>"})` — check blast radius');
525
+ lines.push('2. `mcp_milens_context({name: "<symbol>", repo: "<workspaceRoot>"})` — see callers/callees');
526
+ lines.push('3. If depth-1 dependents > 5 → **STOP and warn** before proceeding');
527
+ lines.push('');
528
+ lines.push('> See `milens/SKILL.md` for full mandatory workflows and tool reference.');
529
+ // Write to .agents/skills/milens-evolved-{key}.md (avoid collision with domain skills)
530
+ const skillDir = pathJoin(resolve(opts.path), '.agents', 'skills', `milens-evolved-${key}`);
531
+ if (!existsSync(skillDir))
532
+ mkdirSync(skillDir, { recursive: true });
533
+ writeFileSync(pathJoin(skillDir, 'SKILL.md'), lines.join('\n'));
534
+ // Log promotion
535
+ for (const a of anns) {
536
+ store.logEvolutionEvent(a.id, 'promoted', '', skillDir);
537
+ }
538
+ promoted += anns.length;
539
+ }
540
+ // 5. Find stale annotations (old + low confidence)
541
+ const stale = store.getStaleAnnotations(30, 0.5);
542
+ if (stale.length > 0) {
543
+ console.log(`Flagged stale: ${stale.length} annotations (30+ days, low confidence)`);
544
+ }
545
+ console.log(`\nMilens Evolution Report:`);
546
+ console.log(` Promoted to rules: ${promoted} patterns (from ${groups.size} categories)`);
547
+ console.log(` Flagged stale: ${stale.length} annotations`);
548
+ console.log(` Archived (decayed): ${archived} annotations`);
549
+ db.close();
550
+ });
551
+ program
552
+ .command('metrics')
553
+ .description('Compute code quality and efficiency metrics')
554
+ .option('-p, --path <path>', 'Repository root path', '.')
555
+ .action(async (opts) => {
556
+ const { Database } = await import('./store/db.js');
557
+ const { RepoRegistry } = await import('./store/registry.js');
558
+ const { computeMetrics, formatMetricsReport } = await import('./metrics.js');
559
+ const dbPath = new RepoRegistry().findDbPath(resolve(opts.path));
560
+ if (!dbPath) {
561
+ console.error('Not indexed. Run `milens analyze` first.');
562
+ process.exit(1);
563
+ }
564
+ const db = new Database(dbPath);
565
+ const metrics = computeMetrics(db);
566
+ console.log(formatMetricsReport(metrics));
567
+ db.close();
568
+ });
569
+ program
570
+ .command('workflow <name>')
571
+ .description('Run a predefined milens workflow')
572
+ .option('-p, --path <path>', 'Repository root path', '.')
573
+ .option('--format <format>', 'Output format: table|json|markdown', 'table')
574
+ .action(async (name, opts) => {
575
+ const { Database } = await import('./store/db.js');
576
+ const { RepoRegistry } = await import('./store/registry.js');
577
+ const { AnnotationStore } = await import('./store/annotations.js');
578
+ const dbPath = new RepoRegistry().findDbPath(resolve(opts.path));
579
+ if (!dbPath) {
580
+ console.error('Not indexed. Run `milens analyze` first.');
581
+ process.exit(1);
582
+ }
583
+ const db = new Database(dbPath);
584
+ const root = resolve(opts.path);
585
+ switch (name) {
586
+ case 'tdd': {
587
+ const coverage = db.getTestCoverage();
588
+ const coveragePct = Math.round(coverage.testedSymbols / Math.max(1, coverage.exportedProductionSymbols) * 100);
589
+ console.log('╔══════════════════════════════════╗');
590
+ console.log('║ TDD Workflow — Coverage ║');
591
+ console.log('╠══════════════════════════════════╣');
592
+ console.log(`║ Exported: ${String(coverage.exportedProductionSymbols).padEnd(5)} | Tested: ${String(coverage.testedSymbols).padEnd(5)} ║`);
593
+ console.log(`║ Coverage: ${String(coveragePct).padStart(3)}%${''.padEnd(20)}║`);
594
+ console.log('╚══════════════════════════════════╝');
595
+ const gaps = db.getTestCoverageGaps(15);
596
+ if (gaps.length > 0) {
597
+ console.log(`\n🧪 Top ${gaps.length} Untested Symbols (by risk):`);
598
+ for (let i = 0; i < gaps.length; i++) {
599
+ const s = gaps[i];
600
+ const incoming = db.getIncomingLinks(s.id).filter((l) => l.type !== 'contains');
601
+ const risk = s.heat && s.heat > 80 ? '🔴 CRITICAL' : s.heat && s.heat > 50 ? '🟠 HIGH' : s.heat && s.heat > 30 ? '🟡 MEDIUM' : '🟢 LOW';
602
+ console.log(` ${i + 1}. ${s.name} [${s.kind}] ${s.filePath}:${s.startLine} — heat:${s.heat ?? 0}, deps:${incoming.length} → ${risk}`);
603
+ }
604
+ }
605
+ else {
606
+ console.log(`\n✅ All exported symbols have test coverage.`);
607
+ }
608
+ const testFiles = coverage.testFiles > 0
609
+ ? ` Test files found: ${coverage.testFiles}`
610
+ : ` ⚠ No test files detected in index.`;
611
+ console.log(`\n${testFiles}`);
612
+ console.log(`\n📋 Next steps:`);
613
+ console.log(` 1. Run \`milens workflow tdd\` after each change to track gaps`);
614
+ console.log(` 2. Use \`milens serve\` + MCP \`test_plan({name: "symbol"})\` for per-symbol test strategy`);
615
+ console.log(` 3. Use MCP \`test_generate({symbol: "symbol"})\` to auto-generate test file`);
616
+ break;
617
+ }
618
+ case 'review': {
619
+ console.log('PR Review Report:');
620
+ try {
621
+ const { execSync } = await import('node:child_process');
622
+ const diff = execSync('git diff --name-only HEAD', { cwd: root, encoding: 'utf-8' }).trim();
623
+ const changedFiles = diff ? diff.split('\n').filter(Boolean) : [];
624
+ if (changedFiles.length === 0) {
625
+ console.log(' No changed files detected.');
626
+ }
627
+ else {
628
+ console.log(` ${changedFiles.length} changed files`);
629
+ for (const file of changedFiles.slice(0, 15)) {
630
+ const syms = db.getSymbolsByFile(file);
631
+ if (syms.length > 0) {
632
+ for (const sym of syms.slice(0, 5)) {
633
+ const incoming = db.getIncomingLinks(sym.id).filter((l) => l.type !== 'contains');
634
+ const depsCount = incoming.length;
635
+ const heat = sym.heat ?? 0;
636
+ const hasTest = db.getSymbolTestCoverage(sym.id);
637
+ const score = Math.round((heat / 100) * 40 + Math.min(depsCount / 10, 1) * 35 + (hasTest ? 0 : 25));
638
+ let level = 'LOW';
639
+ if (score > 75)
640
+ level = 'CRITICAL';
641
+ else if (score > 50)
642
+ level = 'HIGH';
643
+ else if (score > 25)
644
+ level = 'MEDIUM';
645
+ console.log(` ${sym.name} [${sym.kind}] ${file} — heat:${heat} deps:${depsCount} → ${level}(${score})`);
646
+ }
647
+ }
648
+ }
649
+ }
650
+ }
651
+ catch {
652
+ console.log(' review_pr requires MCP server. Use milens serve and call via MCP.');
653
+ }
654
+ break;
655
+ }
656
+ case 'plan': {
657
+ const summary = db.getCodebaseSummary();
658
+ console.log(`Codebase Summary: ${summary.symbols} symbols, ${summary.links} links, ${summary.files} files`);
659
+ if (summary.domains.length > 0) {
660
+ console.log(`Domains: ${summary.domains.map((d) => `${d.domain}(${d.symbols}s)`).join(', ')}`);
661
+ }
662
+ if (summary.topHubs.length > 0) {
663
+ console.log(`Top hubs: ${summary.topHubs.map((h) => `${h.name}(${h.kind},heat:${h.heat})`).join(', ')}`);
664
+ }
665
+ break;
666
+ }
667
+ case 'onboard': {
668
+ const summary = db.getCodebaseSummary();
669
+ const coverage = db.getTestCoverage();
670
+ console.log('╔══════════════════════════════════╗');
671
+ console.log('║ Milens Onboarding Report ║');
672
+ console.log('╚══════════════════════════════════╝\n');
673
+ console.log(`📊 Codebase: ${summary.symbols} symbols, ${summary.links} links, ${summary.files} files`);
674
+ console.log(`🧪 Test coverage: ${summary.coveragePct}% (${summary.testedSymbols}/${summary.exportedSymbols} exported)\n`);
675
+ if (summary.domains.length > 0) {
676
+ console.log('🗂️ Domain clusters:');
677
+ for (const d of summary.domains.slice(0, 8)) {
678
+ const pct = summary.symbols > 0 ? Math.round(d.symbols / summary.symbols * 100) : 0;
679
+ console.log(` ${d.domain}: ${d.files}f, ${d.symbols}s (${pct}%)`);
680
+ }
681
+ console.log();
682
+ }
683
+ if (summary.topHubs.length > 0) {
684
+ console.log('⭐ Key entry points:');
685
+ for (const h of summary.topHubs.slice(0, 8)) {
686
+ const incoming = db.getIncomingLinks(h.id).filter((l) => l.type !== 'contains');
687
+ console.log(` ${h.name} [${h.kind}] ${h.filePath}:${h.startLine} — heat:${h.heat ?? 0}, deps:${incoming.length}`);
688
+ }
689
+ console.log();
690
+ }
691
+ const deadCode = db.findDeadCode(undefined, 5);
692
+ if (deadCode.length > 0) {
693
+ console.log(`⚠ Dead code candidates: ${deadCode.length}`);
694
+ for (const s of deadCode.slice(0, 3)) {
695
+ console.log(` ${s.name} [${s.kind}] ${s.filePath}:${s.startLine}`);
696
+ }
697
+ console.log();
698
+ }
699
+ console.log('📋 Getting started:');
700
+ console.log(' 1. Run `milens workflow tdd` to see coverage gaps');
701
+ console.log(' 2. Run `milens workflow review` before committing changes');
702
+ console.log(' 3. Run `milens serve` for MCP integration with your AI agent');
703
+ break;
704
+ }
705
+ case 'security-scan': {
706
+ console.log('╔══════════════════════════════════╗');
707
+ console.log('║ Security Scan Workflow ║');
708
+ console.log('╚══════════════════════════════════╝\n');
709
+ try {
710
+ const { loadRules } = await import('./security/rules.js');
711
+ const { readFileSync, readdirSync, statSync } = await import('node:fs');
712
+ const { join: pathJoin, relative: pathRel } = await import('node:path');
713
+ const rules = loadRules().filter(r => r.enabled);
714
+ console.log(`Loaded ${rules.length} active security rules\n`);
715
+ const findings = [];
716
+ const MAX_FILE_SIZE = 200 * 1024;
717
+ function scanDir(dir) {
718
+ try {
719
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
720
+ const fullPath = pathJoin(dir, entry.name);
721
+ if (entry.isDirectory()) {
722
+ if (['node_modules', '.git', 'dist', 'build', '.next', '__pycache__', '.venv', 'vendor', 'coverage'].includes(entry.name))
723
+ continue;
724
+ scanDir(fullPath);
725
+ }
726
+ else if (entry.isFile()) {
727
+ const ext = entry.name.split('.').pop()?.toLowerCase() || '';
728
+ if (!['ts', 'js', 'tsx', 'jsx', 'py', 'go', 'rs', 'java', 'rb', 'php', 'sql', 'sh', 'yaml', 'yml', 'json', 'html', 'css', 'vue'].includes(ext))
729
+ continue;
730
+ try {
731
+ const stat = statSync(fullPath);
732
+ if (stat.size > MAX_FILE_SIZE)
733
+ return;
734
+ const content = readFileSync(fullPath, 'utf-8');
735
+ for (const rule of rules) {
736
+ for (const pattern of rule.patterns) {
737
+ pattern.lastIndex = 0;
738
+ let match;
739
+ while ((match = pattern.exec(content)) !== null) {
740
+ const lineNum = content.substring(0, match.index).split('\n').length;
741
+ findings.push({
742
+ severity: rule.severity,
743
+ rule: rule.id,
744
+ file: pathRel(root, fullPath).replace(/\\/g, '/'),
745
+ line: lineNum,
746
+ match: match[0].length > 80 ? match[0].slice(0, 77) + '...' : match[0],
747
+ category: rule.category,
748
+ fix: rule.fix,
749
+ });
750
+ }
751
+ }
752
+ }
753
+ }
754
+ catch { /* skip unreadable */ }
755
+ }
756
+ }
757
+ }
758
+ catch { /* skip unreadable dir */ }
759
+ }
760
+ scanDir(root);
761
+ const bySeverity = {};
762
+ const byCategory = {};
763
+ for (const f of findings) {
764
+ bySeverity[f.severity] = (bySeverity[f.severity] || 0) + 1;
765
+ byCategory[f.category] = (byCategory[f.category] || 0) + 1;
766
+ }
767
+ console.log(`📊 Summary: ${findings.length} findings`);
768
+ if (findings.length > 0) {
769
+ for (const s of ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']) {
770
+ if (bySeverity[s])
771
+ console.log(` ${s}: ${bySeverity[s]}`);
772
+ }
773
+ console.log();
774
+ // Show top findings by severity
775
+ const sorted = [...findings].sort((a, b) => {
776
+ const order = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
777
+ return (order[a.severity] ?? 4) - (order[b.severity] ?? 4);
778
+ });
779
+ for (const f of sorted.slice(0, 20)) {
780
+ const sevIcon = f.severity === 'CRITICAL' ? '🔴' : f.severity === 'HIGH' ? '🟠' : f.severity === 'MEDIUM' ? '🟡' : '🟢';
781
+ console.log(`${sevIcon} [${f.severity}] ${f.rule} — ${f.file}:${f.line}`);
782
+ console.log(` ${f.match.slice(0, 100)}`);
783
+ if (f.fix)
784
+ console.log(` 💡 Fix: ${f.fix}`);
785
+ }
786
+ if (findings.length > 20) {
787
+ console.log(`\n... and ${findings.length - 20} more (limit to 20). Run \`milens security scan\` for full output.`);
788
+ }
789
+ }
790
+ else {
791
+ console.log('✅ No security issues found.');
792
+ }
793
+ }
794
+ catch (e) {
795
+ console.error(`Security scan failed: ${e.message || e}`);
796
+ }
797
+ break;
798
+ }
799
+ case 'refactor': {
800
+ const deadCode = db.findDeadCode(undefined, 20);
801
+ console.log('Refactor Workflow — Dead Code Candidates:');
802
+ if (deadCode.length === 0) {
803
+ console.log(' No dead code found.');
804
+ }
805
+ else {
806
+ for (const s of deadCode) {
807
+ console.log(` ${s.name} [${s.kind}] ${s.filePath}:${s.startLine}`);
808
+ }
809
+ }
810
+ console.log(`\nRun 'milens analyze --force' to refresh the index before refactoring.`);
811
+ break;
812
+ }
813
+ case 'handoff': {
814
+ const store = new AnnotationStore(db.connection);
815
+ console.log('╔══════════════════════════════════╗');
816
+ console.log('║ Handoff Workflow ║');
817
+ console.log('╚══════════════════════════════════╝\n');
818
+ // Show recent sessions
819
+ const recentAnns = store.recall({ limit: 30 });
820
+ const sessions = new Set();
821
+ for (const a of recentAnns) {
822
+ if (a.sessionId)
823
+ sessions.add(a.sessionId);
824
+ }
825
+ console.log(`📋 Indexed annotations: ${store.getAnnotationCount()}`);
826
+ console.log(`📂 Recent sessions: ${sessions.size}`);
827
+ try {
828
+ const allAnnotations = store.recall({ limit: 100 });
829
+ // Group by key for summary
830
+ const byKey = new Map();
831
+ for (const a of allAnnotations) {
832
+ const group = byKey.get(a.key) || { count: 0, symbols: [], topConfidence: 0 };
833
+ group.count++;
834
+ if (group.symbols.length < 5)
835
+ group.symbols.push(a.symbol);
836
+ if (a.confidence > group.topConfidence)
837
+ group.topConfidence = a.confidence;
838
+ byKey.set(a.key, group);
839
+ }
840
+ if (byKey.size > 0) {
841
+ console.log(`\n🗂️ Knowledge by category:`);
842
+ for (const [key, group] of byKey) {
843
+ const pct = Math.round(group.topConfidence * 100);
844
+ const bar = '█'.repeat(Math.round(group.topConfidence * 10));
845
+ console.log(` [${key}] ${group.count} annotations, top confidence: ${bar} ${pct}%`);
846
+ if (group.symbols.length > 0) {
847
+ console.log(` Symbols: ${group.symbols.join(', ')}`);
848
+ }
849
+ }
850
+ }
851
+ // Promotable annotations (high confidence)
852
+ const promotable = allAnnotations.filter(a => a.confidence >= 0.8);
853
+ if (promotable.length > 0) {
854
+ console.log(`\n⭐ ${promotable.length} high-confidence annotations ready for promotion:`);
855
+ for (const a of promotable.slice(0, 5)) {
856
+ console.log(` [${a.key}] ${a.symbol}: ${a.value.slice(0, 80)}`);
857
+ }
858
+ console.log(`\n Run \`milens evolve -p "${root}"\` to promote these to rules/skills.`);
859
+ }
860
+ }
861
+ catch { /* annotations may not be available */ }
862
+ console.log(`\n📋 CLI handoff commands:`);
863
+ console.log(` 1. milens workflow onboard --path <path> → fresh agent onboarding`);
864
+ console.log(` 2. milens workflow review --path <path> → pre-handoff review`);
865
+ console.log(` 3. milens evolve -p <path> → promote knowledge to rules`);
866
+ console.log(`\n💡 Tip: Use \`milens serve\` + MCP for full session_start() → annotate() → handoff() flow.`);
867
+ break;
868
+ }
869
+ default:
870
+ console.log(`Unknown workflow: ${name}`);
871
+ console.log(`Available: tdd, review, plan, onboard, security-scan, refactor, handoff`);
872
+ }
873
+ db.close();
874
+ });
875
+ program
876
+ .command('init')
877
+ .description('Bootstrap milens for a project: index + AGENTS.md + skills + hooks')
878
+ .option('-p, --path <path>', 'Repository root path', '.')
879
+ .option('--profile <profile>', 'minimal|standard|full', 'standard')
880
+ .option('--with <modules>', 'Comma-separated extra modules (security,ci,hooks)')
881
+ .option('--interactive', 'Interactive install mode')
882
+ .action(async (opts) => {
883
+ const root = resolve(opts.path);
884
+ // Interactive install mode
885
+ if (opts.interactive) {
886
+ const { createInterface } = await import('node:readline');
887
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
888
+ const ask = (q) => new Promise(resolve => rl.question(q, resolve));
889
+ console.log('\n╔══════════════════════════════════════╗');
890
+ console.log('║ Milens Interactive Installer ║');
891
+ console.log('╚══════════════════════════════════════╝\n');
892
+ // Profile selection
893
+ console.log('Choose a profile:');
894
+ console.log(' 1. minimal — Core tools only (10 tools, ~500 token overhead)');
895
+ console.log(' 2. standard — Full vibe coding toolkit (25 tools) [Recommended]');
896
+ console.log(' 3. full — Everything including experimental features (33 tools)');
897
+ const profileChoice = await ask('\nProfile [2]: ');
898
+ const profileMap = { '1': 'minimal', '2': 'standard', '3': 'full', '': 'standard' };
899
+ opts.profile = profileMap[profileChoice.trim()] || 'standard';
900
+ // Security rules
901
+ const securityChoice = await ask('Include security scanning rules? [Y/n]: ');
902
+ opts.with = opts.with || '';
903
+ if (securityChoice.trim().toLowerCase() !== 'n') {
904
+ opts.with += (opts.with ? ',' : '') + 'security';
905
+ }
906
+ // CI/CD templates
907
+ const ciChoice = await ask('Include CI/CD templates (GitHub Actions)? [Y/n]: ');
908
+ if (ciChoice.trim().toLowerCase() !== 'n') {
909
+ opts.with += (opts.with ? ',' : '') + 'ci';
910
+ }
911
+ // Git hooks
912
+ const hooksChoice = await ask('Install pre-commit hooks? [Y/n]: ');
913
+ if (hooksChoice.trim().toLowerCase() !== 'n') {
914
+ opts.with += (opts.with ? ',' : '') + 'hooks';
915
+ }
916
+ // Harness adapters
917
+ console.log('\nTarget harnesses (comma-separated):');
918
+ console.log(' claude-code, opencode, codex, cursor, copilot, gemini, zed, all');
919
+ const harnessChoice = await ask('Harnesses [all]: ');
920
+ const harnesses = harnessChoice.trim() || 'all';
921
+ // Generate command
922
+ const withFlags = opts.with ? `--with ${opts.with}` : '';
923
+ const harnessFlag = harnesses === 'all' ? '' : `--target ${harnesses}`;
924
+ console.log(`\nGenerated install command:`);
925
+ console.log(` npx milens init --profile ${opts.profile} ${withFlags} ${harnessFlag}`.trim());
926
+ const confirm = await ask('\nProceed with install? [Y/n]: ');
927
+ if (confirm.trim().toLowerCase() === 'n') {
928
+ console.log('Install cancelled. Run the command above when ready.');
929
+ rl.close();
930
+ process.exit(0);
931
+ }
932
+ rl.close();
933
+ console.log();
934
+ // Continue with normal init flow...
935
+ }
936
+ const { Database } = await import('./store/db.js');
937
+ const { RepoRegistry } = await import('./store/registry.js');
938
+ console.log(`Milens init — bootstrapping ${opts.profile} profile for ${root}`);
939
+ const { execSync } = await import('node:child_process');
940
+ console.log('Step 1/4: Analyzing codebase...');
941
+ const milensBin = process.argv[1] || 'npx milens';
942
+ try {
943
+ const cmd = process.argv[0].includes('node')
944
+ ? `node ${process.argv[1]} analyze -p "${root}" --force`
945
+ : `npx milens analyze -p "${root}" --force`;
946
+ execSync(cmd, { stdio: 'pipe', cwd: root });
947
+ }
948
+ catch (e) {
949
+ console.log(' (run `milens analyze -p . --force` manually if this fails)');
950
+ }
951
+ console.log('Step 2/4: Generating AGENTS.md...');
952
+ try {
953
+ const dbPath = new RepoRegistry().findDbPath(root);
954
+ if (dbPath) {
955
+ const db = new Database(dbPath);
956
+ const { generateAgentsMd } = await import('./agents-md.js');
957
+ const agentsMd = generateAgentsMd(db, root);
958
+ const { writeFileSync, mkdirSync, existsSync } = await import('node:fs');
959
+ if (!existsSync(root))
960
+ mkdirSync(root, { recursive: true });
961
+ writeFileSync(resolve(root, 'AGENTS.md'), agentsMd);
962
+ console.log(' ✓ AGENTS.md created');
963
+ db.close();
964
+ }
965
+ }
966
+ catch (e) {
967
+ console.log(` ⚠ AGENTS.md generation skipped: ${e.message}`);
968
+ }
969
+ if (opts.profile !== 'minimal') {
970
+ console.log('Step 3/4: Installing skill files...');
971
+ try {
972
+ const { cpSync, existsSync, mkdirSync } = await import('node:fs');
973
+ const { join: pathJoin } = await import('node:path');
974
+ const { fileURLToPath } = await import('node:url');
975
+ const skillSrc = pathJoin(resolve(fileURLToPath(import.meta.url), '..', '..'), '.agents', 'skills');
976
+ const skillDst = pathJoin(root, '.agents', 'skills');
977
+ if (existsSync(skillSrc)) {
978
+ if (!existsSync(skillDst))
979
+ mkdirSync(skillDst, { recursive: true });
980
+ cpSync(skillSrc, skillDst, { recursive: true });
981
+ console.log(' ✓ Skill files installed');
982
+ }
983
+ else {
984
+ console.log(' ⚠ Skill template not found (run from milens repo)');
985
+ }
986
+ }
987
+ catch (e) {
988
+ console.log(` ⚠ Skill install skipped: ${e.message}`);
989
+ }
990
+ }
991
+ if (opts.profile === 'full' || (opts.with && opts.with.includes('hooks'))) {
992
+ console.log('Step 4/4: Installing git hooks...');
993
+ try {
994
+ const { writeFileSync, existsSync, mkdirSync, chmodSync } = await import('node:fs');
995
+ const hooksDir = resolve(root, '.git', 'hooks');
996
+ if (!existsSync(hooksDir)) {
997
+ mkdirSync(hooksDir, { recursive: true });
998
+ }
999
+ const preCommitContent = `#!/bin/bash
1000
+ # Auto-installed by milens init
1001
+ echo "Milens: Pre-commit check..."
1002
+ npx milens workflow review --path . 2>&1 || true
1003
+ echo "Milens: Done."
1004
+ `;
1005
+ writeFileSync(resolve(hooksDir, 'pre-commit'), preCommitContent);
1006
+ try {
1007
+ chmodSync(resolve(hooksDir, 'pre-commit'), 0o755);
1008
+ }
1009
+ catch { }
1010
+ console.log(' ✓ Pre-commit hook installed');
1011
+ }
1012
+ catch (e) {
1013
+ console.log(` ⚠ Hook install skipped: ${e.message}`);
1014
+ }
1015
+ }
1016
+ console.log(`\n✓ Milens ${opts.profile} profile bootstrapped for ${root}`);
1017
+ console.log('Next steps:');
1018
+ console.log(' 1. Open project in your AI coding agent (Claude Code, OpenCode, etc.)');
1019
+ console.log(' 2. AGENTS.md auto-loads with codebase context');
1020
+ console.log(' 3. Start a session: session_start({agent: "your-agent-name"})');
1021
+ });
1022
+ program
1023
+ .command('hooks <action>')
1024
+ .description('Manage milens hook system')
1025
+ .option('-p, --path <path>', 'Repository root path', '.')
1026
+ .option('--hook <hook>', 'Hook name (sessionStart, sessionEnd, preCommit, fileChange, preCompact, postCompact)')
1027
+ .action(async (action, opts) => {
1028
+ const { HookManager } = await import('./server/hooks.js');
1029
+ const manager = new HookManager();
1030
+ const projectPath = resolve(opts.path);
1031
+ switch (action) {
1032
+ case 'enable': {
1033
+ if (opts.hook) {
1034
+ manager.enableHook(opts.hook, projectPath);
1035
+ console.log(`Hook "${opts.hook}" enabled for ${projectPath}`);
1036
+ }
1037
+ else {
1038
+ const cfg = manager.loadConfig(projectPath);
1039
+ cfg.enabled = true;
1040
+ manager.saveConfig(cfg, projectPath);
1041
+ console.log(`All hooks enabled for ${projectPath}`);
1042
+ }
1043
+ break;
1044
+ }
1045
+ case 'disable': {
1046
+ if (opts.hook) {
1047
+ manager.disableHook(opts.hook, projectPath);
1048
+ console.log(`Hook "${opts.hook}" disabled for ${projectPath}`);
1049
+ }
1050
+ else {
1051
+ const cfg = manager.loadConfig(projectPath);
1052
+ cfg.enabled = false;
1053
+ manager.saveConfig(cfg, projectPath);
1054
+ console.log(`All hooks disabled for ${projectPath}`);
1055
+ }
1056
+ break;
1057
+ }
1058
+ case 'list': {
1059
+ const cfg = manager.loadConfig(projectPath);
1060
+ console.log(`Hook configuration for ${projectPath}:`);
1061
+ console.log(` enabled: ${cfg.enabled}`);
1062
+ console.log(` onSessionStart: ${cfg.onSessionStart}`);
1063
+ console.log(` onSessionEnd: ${cfg.onSessionEnd}`);
1064
+ console.log(` onFileChange: ${cfg.onFileChange}`);
1065
+ console.log(` onPreCommit: ${cfg.onPreCommit}`);
1066
+ console.log(` onPreCompact: ${cfg.onPreCompact}`);
1067
+ console.log(` onPostCompact: ${cfg.onPostCompact}`);
1068
+ break;
1069
+ }
1070
+ case 'profile': {
1071
+ const profileName = opts.hook || 'standard';
1072
+ const cfg = manager.loadConfig(projectPath);
1073
+ cfg.enabled = true;
1074
+ if (profileName === 'minimal') {
1075
+ cfg.onSessionStart = false;
1076
+ cfg.onSessionEnd = false;
1077
+ cfg.onFileChange = false;
1078
+ cfg.onPreCommit = true;
1079
+ cfg.onPreCompact = false;
1080
+ cfg.onPostCompact = false;
1081
+ }
1082
+ else if (profileName === 'strict') {
1083
+ cfg.onSessionStart = true;
1084
+ cfg.onSessionEnd = true;
1085
+ cfg.onFileChange = true;
1086
+ cfg.onPreCommit = true;
1087
+ cfg.onPreCompact = true;
1088
+ cfg.onPostCompact = true;
1089
+ }
1090
+ else {
1091
+ cfg.onSessionStart = true;
1092
+ cfg.onSessionEnd = true;
1093
+ cfg.onFileChange = false;
1094
+ cfg.onPreCommit = true;
1095
+ cfg.onPreCompact = false;
1096
+ cfg.onPostCompact = false;
1097
+ }
1098
+ manager.saveConfig(cfg, projectPath);
1099
+ console.log(`Hook profile set to "${profileName}" for ${projectPath}`);
1100
+ break;
1101
+ }
1102
+ default:
1103
+ console.log(`Unknown action: ${action}. Use: enable, disable, list, profile`);
1104
+ }
1105
+ });
1106
+ const securityCmd = program
1107
+ .command('security')
1108
+ .description('Security scanning and dependency audit');
1109
+ securityCmd
1110
+ .command('scan')
1111
+ .description('Scan project for security vulnerabilities')
1112
+ .option('-p, --path <path>', 'Repository root path', '.')
1113
+ .option('--scope <scope>', 'all|secrets|injection|unicode|dangerous|config|data-leak|crypto|auth|file-access', 'all')
1114
+ .option('--severity <severity>', 'CRITICAL|HIGH|MEDIUM|LOW')
1115
+ .option('--limit <limit>', 'Max findings to report', '30')
1116
+ .option('--format <format>', 'table|json|markdown', 'table')
1117
+ .action(async (opts) => {
1118
+ const root = resolve(opts.path);
1119
+ const maxFindings = parseInt(opts.limit) || 30;
1120
+ const MAX_FILE_SIZE = 200 * 1024; // skip files >200KB to avoid regex timeout
1121
+ const { loadRules } = await import('./security/rules.js');
1122
+ const { readFileSync, existsSync, readdirSync, statSync } = await import('node:fs');
1123
+ const { join: pathJoin, relative: pathRelative } = await import('node:path');
1124
+ const rules = loadRules();
1125
+ const filtered = rules.filter(r => {
1126
+ if (opts.scope !== 'all' && r.category !== opts.scope)
1127
+ return false;
1128
+ if (opts.severity) {
1129
+ const sevOrder = { CRITICAL: 4, HIGH: 3, MEDIUM: 2, LOW: 1 };
1130
+ if ((sevOrder[r.severity] || 0) < (sevOrder[opts.severity] || 0))
1131
+ return false;
1132
+ }
1133
+ return r.enabled;
1134
+ });
1135
+ console.log(`Security Scan — ${filtered.length} active rules, scope: ${opts.scope}\n`);
1136
+ const findings = [];
1137
+ const walkDir = (dir) => {
1138
+ try {
1139
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
1140
+ const fullPath = pathJoin(dir, entry.name);
1141
+ if (entry.isDirectory()) {
1142
+ if (['node_modules', '.git', 'dist', 'build', '.next'].includes(entry.name))
1143
+ continue;
1144
+ walkDir(fullPath);
1145
+ }
1146
+ else if (entry.isFile()) {
1147
+ const ext = entry.name.split('.').pop() || '';
1148
+ if (!['ts', 'js', 'tsx', 'jsx', 'py', 'go', 'rs', 'java', 'rb', 'php', 'sql', 'sh', 'yaml', 'yml', 'json', 'html', 'css'].includes(ext))
1149
+ continue;
1150
+ try {
1151
+ const stat = statSync(fullPath);
1152
+ if (stat.size > MAX_FILE_SIZE) {
1153
+ if (opts.format === 'table')
1154
+ console.log(` Skipping large file: ${pathRelative(root, fullPath)} (${(stat.size / 1024).toFixed(0)}KB)`);
1155
+ return;
1156
+ }
1157
+ const content = readFileSync(fullPath, 'utf-8');
1158
+ const lines = content.split('\n');
1159
+ for (const rule of filtered) {
1160
+ for (const pattern of rule.patterns) {
1161
+ pattern.lastIndex = 0;
1162
+ let match;
1163
+ while ((match = pattern.exec(content)) !== null) {
1164
+ const lineNum = content.substring(0, match.index).split('\n').length;
1165
+ findings.push({
1166
+ rule: rule.id,
1167
+ severity: rule.severity,
1168
+ category: rule.category,
1169
+ owasp: rule.owasp,
1170
+ file: pathRelative(root, fullPath),
1171
+ line: lineNum,
1172
+ match: match[0].length > 80 ? match[0].slice(0, 77) + '...' : match[0],
1173
+ fix: rule.fix,
1174
+ });
1175
+ }
1176
+ }
1177
+ }
1178
+ }
1179
+ catch { }
1180
+ }
1181
+ }
1182
+ }
1183
+ catch { }
1184
+ };
1185
+ walkDir(root);
1186
+ const bySev = {};
1187
+ for (const f of findings) {
1188
+ bySev[f.severity] = (bySev[f.severity] || 0) + 1;
1189
+ }
1190
+ console.log(`Total findings: ${findings.length}`);
1191
+ for (const [s, c] of Object.entries(bySev)) {
1192
+ console.log(` ${s}: ${c}`);
1193
+ }
1194
+ console.log();
1195
+ for (const f of findings.slice(0, maxFindings)) {
1196
+ console.log(`[${f.severity}] ${f.rule} ${f.file}:${f.line} — ${f.match}`);
1197
+ }
1198
+ });
1199
+ securityCmd
1200
+ .command('deps')
1201
+ .description('Audit dependencies for known vulnerabilities')
1202
+ .option('-p, --path <path>', 'Repository root path', '.')
1203
+ .action(async (opts) => {
1204
+ const root = resolve(opts.path);
1205
+ const { auditDependencies } = await import('./security/deps.js');
1206
+ const result = auditDependencies(root);
1207
+ console.log(`Dependency Audit — ${result.ecosystem}`);
1208
+ console.log(` Total: ${result.totalDependencies} | Vulnerable: ${result.vulnerableDependencies}\n`);
1209
+ for (const v of result.findings.slice(0, 20)) {
1210
+ console.log(`[${v.severity}] ${v.package} — ${v.id}${v.cve ? ` (${v.cve})` : ''}`);
1211
+ console.log(` Affected: ${v.affectedVersions} | Fixed: ${v.fixedVersion || 'N/A'}`);
1212
+ console.log(` ${v.description}`);
1213
+ console.log();
1214
+ }
1215
+ });
1216
+ program
1217
+ .command('watch')
1218
+ .description('Watch files for changes and auto re-index')
1219
+ .option('-p, --path <path>', 'Repository root path', '.')
1220
+ .option('--debounce <ms>', 'Debounce time in ms', '1000')
1221
+ .option('--ignore <glob>', 'Files to ignore (comma-separated)')
1222
+ .action(async (opts) => {
1223
+ const root = resolve(opts.path);
1224
+ const { watch, existsSync } = await import('node:fs');
1225
+ const { join: pathJoin } = await import('node:path');
1226
+ console.log(`Watching ${root} for changes... (Ctrl+C to stop)`);
1227
+ let timer = null;
1228
+ const changedFiles = new Set();
1229
+ const debounceMs = parseInt(opts.debounce) || 1000;
1230
+ const ignoreList = opts.ignore ? opts.ignore.split(',') : ['node_modules', '.git', 'dist'];
1231
+ const triggerRebuild = async () => {
1232
+ if (changedFiles.size === 0)
1233
+ return;
1234
+ const files = [...changedFiles];
1235
+ changedFiles.clear();
1236
+ console.log(`\nRe-indexing ${files.length} changed file(s)...`);
1237
+ const { execSync } = await import('node:child_process');
1238
+ try {
1239
+ const fileArgs = files.map(f => `"${f}"`).join(' ');
1240
+ const cmd = process.argv[1]
1241
+ ? `node ${process.argv[1]} analyze -p "${root}" --force --files ${fileArgs}`
1242
+ : `npx milens analyze -p "${root}" --force --files ${fileArgs}`;
1243
+ execSync(cmd, { stdio: 'pipe', cwd: root });
1244
+ console.log(`✓ Index updated`);
1245
+ }
1246
+ catch {
1247
+ console.log(`⚠ Re-index failed — run manually: milens analyze -p . --force`);
1248
+ }
1249
+ };
1250
+ try {
1251
+ const watcher = watch(root, { recursive: true }, (eventType, filename) => {
1252
+ if (!filename)
1253
+ return;
1254
+ if (ignoreList.some((i) => filename.includes(i)))
1255
+ return;
1256
+ changedFiles.add(filename);
1257
+ if (timer)
1258
+ clearTimeout(timer);
1259
+ timer = setTimeout(triggerRebuild, debounceMs);
1260
+ });
1261
+ process.on('SIGINT', () => {
1262
+ console.log('\nWatch stopped.');
1263
+ watcher.close();
1264
+ process.exit(0);
1265
+ });
1266
+ }
1267
+ catch {
1268
+ console.error('File watching failed. Use `milens analyze -p . --force` to manually re-index.');
1269
+ }
1270
+ });
1271
+ program
1272
+ .command('orchestrate')
1273
+ .description('Run full review cycle: detect changes → risk → coverage gaps → dead code')
1274
+ .option('-p, --path <path>', 'Repository root path', '.')
1275
+ .option('--emoji', 'Use emoji in output')
1276
+ .action(async (opts) => {
1277
+ const root = resolve(opts.path);
1278
+ const dbPath = join(root, '.milens', 'milens.db');
1279
+ if (!existsSync(dbPath)) {
1280
+ console.error(`No milens database found. Run \`milens analyze\` first.`);
1281
+ process.exit(1);
1282
+ }
1283
+ const { Orchestrator } = await import('./orchestrator/orchestrator.js');
1284
+ const orchestrator = new Orchestrator({ rootPath: root, dbPath, useEmoji: opts.emoji });
1285
+ // Mark changed files from git diff
1286
+ try {
1287
+ const { execFileSync } = await import('node:child_process');
1288
+ const diffOut = execFileSync('git', ['diff', '--name-only', 'HEAD'], { cwd: root, encoding: 'utf-8' });
1289
+ const staged = execFileSync('git', ['diff', '--cached', '--name-only'], { cwd: root, encoding: 'utf-8' });
1290
+ const changed = [...new Set([...diffOut.trim().split('\n'), ...staged.trim().split('\n')])].filter(Boolean);
1291
+ for (const f of changed)
1292
+ orchestrator.subscribe(f);
1293
+ }
1294
+ catch {
1295
+ console.error('Not a git repository or git not available.');
1296
+ process.exit(1);
1297
+ }
1298
+ const report = await orchestrator.runAndFormat();
1299
+ console.log(report);
1300
+ });
310
1301
  program.parse();
311
1302
  // ── Helpers ──
312
1303
  function deleteIndex(dbPath) {
@@ -323,7 +1314,7 @@ function deleteIndex(dbPath) {
323
1314
  }
324
1315
  }
325
1316
  }
326
- function generateDashboardHtml(stats) {
1317
+ function generateDashboardHtml(stats, annotationStats, repoFilter) {
327
1318
  const byToolJson = JSON.stringify(stats.byTool);
328
1319
  const byDayJson = JSON.stringify(stats.byDay);
329
1320
  const recentJson = JSON.stringify(stats.recentCalls);
@@ -331,401 +1322,476 @@ function generateDashboardHtml(stats) {
331
1322
  ? Math.round((stats.totalTokensSaved / (stats.totalTokensOut + stats.totalTokensSaved)) * 100)
332
1323
  : 0;
333
1324
  const fmtNum = (n) => n >= 1_000_000 ? (n / 1_000_000).toFixed(1) + 'M' : n >= 1_000 ? (n / 1_000).toFixed(1) + 'K' : String(n);
334
- return `<!DOCTYPE html>
335
- <html lang="en">
336
- <head>
337
- <meta charset="UTF-8">
338
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
339
- <title>milens Dashboard</title>
340
- <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
341
- <style>
342
- :root {
343
- --bg: #0a0e14; --surface: #12171e; --card: #161d27; --card-hover: #1a2332;
344
- --border: #1e2a3a; --border-light: #2a3a4e;
345
- --text: #e2e8f0; --text-secondary: #94a3b8; --text-muted: #64748b;
346
- --accent: #60a5fa; --accent-dim: #60a5fa22;
347
- --green: #34d399; --green-dim: #34d39915;
348
- --orange: #fbbf24; --orange-dim: #fbbf2415;
349
- --purple: #a78bfa; --purple-dim: #a78bfa15;
350
- --red: #f87171;
351
- --radius: 16px; --radius-sm: 10px;
352
- }
353
- * { margin: 0; padding: 0; box-sizing: border-box; }
354
- body {
355
- background: var(--bg); color: var(--text);
356
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
357
- line-height: 1.5; min-height: 100vh;
358
- }
359
-
360
- /* ── Layout ── */
361
- .wrapper { max-width: 1400px; margin: 0 auto; padding: 32px 24px 80px; }
362
-
363
- /* ── Header ── */
364
- .header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 32px; }
365
- .header-left { display: flex; align-items: center; gap: 14px; }
366
- .logo { width: 40px; height: 40px; border-radius: 12px; background: linear-gradient(135deg, var(--accent), var(--purple)); display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 18px; color: #fff; }
367
- .header h1 { font-size: 22px; font-weight: 700; letter-spacing: -0.3px; }
368
- .header h1 span { color: var(--text-muted); font-weight: 400; font-size: 14px; margin-left: 8px; }
369
- .header-right { display: flex; align-items: center; gap: 12px; }
370
- .status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); animation: pulse 2s infinite; }
371
- @keyframes pulse { 0%,80%,100% { opacity: 1; } 40% { opacity: 0.4; } }
372
- .status-text { font-size: 12px; color: var(--text-muted); }
373
- .refresh-btn {
374
- background: var(--accent-dim); color: var(--accent); border: 1px solid var(--border);
375
- border-radius: var(--radius-sm); padding: 8px 16px; cursor: pointer;
376
- font-weight: 500; font-size: 13px; transition: all 0.2s;
377
- }
378
- .refresh-btn:hover { background: var(--accent); color: #0a0e14; }
379
-
380
- /* ── KPI Cards ── */
381
- .kpi-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 28px; }
382
- .kpi {
383
- background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
384
- padding: 24px; position: relative; overflow: hidden; transition: border-color 0.2s;
385
- }
386
- .kpi:hover { border-color: var(--border-light); }
387
- .kpi-icon { width: 40px; height: 40px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 20px; margin-bottom: 16px; }
388
- .kpi-icon.blue { background: var(--accent-dim); }
389
- .kpi-icon.green { background: var(--green-dim); }
390
- .kpi-icon.purple { background: var(--purple-dim); }
391
- .kpi-icon.orange { background: var(--orange-dim); }
392
- .kpi .value { font-size: 32px; font-weight: 800; letter-spacing: -1px; line-height: 1; }
393
- .kpi .value.blue { color: var(--accent); }
394
- .kpi .value.green { color: var(--green); }
395
- .kpi .value.purple { color: var(--purple); }
396
- .kpi .value.orange { color: var(--orange); }
397
- .kpi .label { color: var(--text-muted); font-size: 13px; margin-top: 6px; font-weight: 500; }
398
- .kpi .sub { color: var(--text-secondary); font-size: 12px; margin-top: 4px; }
399
- .kpi-glow {
400
- position: absolute; top: -40px; right: -40px; width: 120px; height: 120px;
401
- border-radius: 50%; opacity: 0.06; pointer-events: none;
402
- }
403
- .kpi-glow.blue { background: var(--accent); }
404
- .kpi-glow.green { background: var(--green); }
405
- .kpi-glow.purple { background: var(--purple); }
406
- .kpi-glow.orange { background: var(--orange); }
407
-
408
- /* ── Charts ── */
409
- .grid-2 { display: grid; grid-template-columns: 5fr 7fr; gap: 16px; margin-bottom: 16px; }
410
- .grid-full { margin-bottom: 16px; }
411
- .card {
412
- background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
413
- padding: 24px; transition: border-color 0.2s;
414
- }
415
- .card:hover { border-color: var(--border-light); }
416
- .card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
417
- .card-title { font-size: 14px; font-weight: 600; color: var(--text); }
418
- .card-subtitle { font-size: 12px; color: var(--text-muted); }
419
-
420
- /* ── Chart containers ── */
421
- .chart-container { position: relative; width: 100%; }
422
- .chart-container.h-280 { height: 280px; }
423
- .chart-container.h-300 { height: 300px; }
424
-
425
- /* ── Top Tools Bar ── */
426
- .tool-bars { display: flex; flex-direction: column; gap: 10px; }
427
- .tool-bar-row { display: flex; align-items: center; gap: 12px; }
428
- .tool-bar-name { width: 140px; font-size: 12px; font-family: 'SF Mono', 'Fira Code', monospace; color: var(--text-secondary); text-align: right; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
429
- .tool-bar-track { flex: 1; height: 28px; background: var(--surface); border-radius: 6px; overflow: hidden; position: relative; }
430
- .tool-bar-fill { height: 100%; border-radius: 6px; display: flex; align-items: center; padding: 0 10px; font-size: 11px; font-weight: 600; color: #fff; min-width: fit-content; transition: width 0.6s ease; }
431
- .tool-bar-count { font-size: 12px; color: var(--text-muted); min-width: 36px; text-align: right; }
432
-
433
- /* ── Recent Table ── */
434
- .table-wrapper { max-height: 400px; overflow-y: auto; border-radius: var(--radius-sm); }
435
- .table-wrapper::-webkit-scrollbar { width: 6px; }
436
- .table-wrapper::-webkit-scrollbar-track { background: transparent; }
437
- .table-wrapper::-webkit-scrollbar-thumb { background: var(--border-light); border-radius: 3px; }
438
- table { width: 100%; border-collapse: collapse; font-size: 13px; }
439
- thead { position: sticky; top: 0; z-index: 1; }
440
- th {
441
- text-align: left; color: var(--text-muted); font-weight: 500; padding: 10px 16px;
442
- background: var(--card); border-bottom: 1px solid var(--border); font-size: 11px;
443
- text-transform: uppercase; letter-spacing: 0.5px;
444
- }
445
- td { padding: 10px 16px; border-bottom: 1px solid var(--border); }
446
- tr:hover td { background: var(--surface); }
447
- .tool-badge {
448
- display: inline-flex; align-items: center; gap: 4px;
449
- background: var(--accent-dim); color: var(--accent); padding: 3px 10px;
450
- border-radius: 6px; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 11px; font-weight: 500;
451
- }
452
- .when-text { color: var(--text-muted); }
453
- .duration-text { color: var(--text-secondary); font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; }
454
- .saved-text { color: var(--green); font-weight: 600; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; }
455
-
456
- /* ── Footer ── */
457
- .footer { text-align: center; padding: 24px 0 0; color: var(--text-muted); font-size: 12px; }
458
-
459
- /* ── Responsive ── */
460
- @media (max-width: 1024px) { .kpi-grid { grid-template-columns: repeat(2, 1fr); } .grid-2 { grid-template-columns: 1fr; } }
461
- @media (max-width: 640px) { .kpi-grid { grid-template-columns: 1fr; } .wrapper { padding: 16px 12px 80px; } }
462
- </style>
463
- </head>
464
- <body>
465
- <div class="wrapper">
466
-
467
- <!-- Header -->
468
- <div class="header">
469
- <div class="header-left">
470
- <div class="logo">m</div>
471
- <h1>milens <span>dashboard</span></h1>
472
- </div>
473
- <div class="header-right">
474
- <div class="status-dot"></div>
475
- <span class="status-text">Live</span>
476
- <button class="refresh-btn" onclick="refreshData()">&#8635; Refresh</button>
477
- </div>
478
- </div>
479
-
480
- <!-- KPIs -->
481
- <div class="kpi-grid">
482
- <div class="kpi">
483
- <div class="kpi-icon blue">&#9881;</div>
484
- <div class="value blue" id="kpi-calls">${fmtNum(stats.totalCalls)}</div>
485
- <div class="label">Tool Calls</div>
486
- <div class="sub" id="kpi-calls-sub">${stats.totalCalls.toLocaleString()} total invocations</div>
487
- <div class="kpi-glow blue"></div>
488
- </div>
489
- <div class="kpi">
490
- <div class="kpi-icon green">&#9889;</div>
491
- <div class="value green" id="kpi-saved">${fmtNum(stats.totalTokensSaved)}</div>
492
- <div class="label">Tokens Saved</div>
493
- <div class="sub" id="kpi-saved-sub">${stats.totalTokensSaved.toLocaleString()} tokens not wasted</div>
494
- <div class="kpi-glow green"></div>
495
- </div>
496
- <div class="kpi">
497
- <div class="kpi-icon purple">&#9733;</div>
498
- <div class="value purple" id="kpi-pct">${savingsPercent}%</div>
499
- <div class="label">Token Efficiency</div>
500
- <div class="sub">${fmtNum(stats.totalTokensOut)} returned vs ${fmtNum(stats.totalTokensSaved)} saved</div>
501
- <div class="kpi-glow purple"></div>
502
- </div>
503
- <div class="kpi">
504
- <div class="kpi-icon orange">&#9201;</div>
505
- <div class="value orange" id="kpi-avg">${stats.totalCalls > 0 ? Math.round(stats.totalDurationMs / stats.totalCalls) : 0}ms</div>
506
- <div class="label">Avg Response Time</div>
507
- <div class="sub">${(stats.totalDurationMs / 1000).toFixed(1)}s total processing</div>
508
- <div class="kpi-glow orange"></div>
509
- </div>
510
- </div>
511
-
512
- <!-- Charts Row -->
513
- <div class="grid-2">
514
- <div class="card">
515
- <div class="card-header">
516
- <div>
517
- <div class="card-title">Top Tools</div>
518
- <div class="card-subtitle">By number of calls</div>
519
- </div>
520
- </div>
521
- <div id="toolBars" class="tool-bars"></div>
522
- </div>
523
- <div class="card">
524
- <div class="card-header">
525
- <div>
526
- <div class="card-title">Daily Activity</div>
527
- <div class="card-subtitle">Calls &amp; tokens saved over the last 30 days</div>
528
- </div>
529
- </div>
530
- <div class="chart-container h-280"><canvas id="dayChart"></canvas></div>
531
- </div>
532
- </div>
533
-
534
- <!-- Savings Distribution -->
535
- <div class="grid-2" style="grid-template-columns: 7fr 5fr;">
536
- <div class="card">
537
- <div class="card-header">
538
- <div>
539
- <div class="card-title">Recent Tool Calls</div>
540
- <div class="card-subtitle">Last 50 invocations</div>
541
- </div>
542
- </div>
543
- <div class="table-wrapper">
544
- <table>
545
- <thead><tr><th>Tool</th><th>When</th><th>Duration</th><th style="text-align:right">Tokens Saved</th></tr></thead>
546
- <tbody id="recentBody"></tbody>
547
- </table>
548
- </div>
549
- </div>
550
- <div class="card">
551
- <div class="card-header">
552
- <div>
553
- <div class="card-title">Savings by Tool</div>
554
- <div class="card-subtitle">Token savings distribution</div>
555
- </div>
556
- </div>
557
- <div class="chart-container h-300"><canvas id="savingsChart"></canvas></div>
558
- </div>
559
- </div>
560
-
561
- <div class="footer">milens &middot; auto-refreshes every 30s</div>
562
- </div>
563
-
564
- <script>
565
- const COLORS = ['#60a5fa','#34d399','#fbbf24','#a78bfa','#f87171','#2dd4bf','#818cf8','#fb923c','#e879f9','#38bdf8','#4ade80','#facc15','#f472b6','#22d3ee','#a3e635','#c084fc'];
566
- const BAR_COLORS = ['#60a5fa','#34d399','#fbbf24','#a78bfa','#f87171','#2dd4bf','#818cf8','#fb923c','#e879f9','#38bdf8'];
567
- let byTool = ${byToolJson};
568
- let byDay = ${byDayJson};
569
-
570
- function fmtK(n) { return n >= 1e6 ? (n/1e6).toFixed(1)+'M' : n >= 1e3 ? (n/1e3).toFixed(1)+'K' : n; }
571
-
572
- /* ── Top Tools Horizontal Bars ── */
573
- function renderToolBars() {
574
- const el = document.getElementById('toolBars');
575
- const sorted = [...byTool].sort((a,b) => b.calls - a.calls).slice(0, 10);
576
- const maxCalls = sorted[0]?.calls || 1;
577
- el.innerHTML = sorted.map((t, i) => {
578
- const pct = Math.max(8, (t.calls / maxCalls) * 100);
579
- const col = BAR_COLORS[i % BAR_COLORS.length];
580
- return \`<div class="tool-bar-row">
581
- <span class="tool-bar-name">\${t.tool}</span>
582
- <div class="tool-bar-track">
583
- <div class="tool-bar-fill" style="width:\${pct}%;background:linear-gradient(90deg,\${col}dd,\${col}88)">\${t.calls}</div>
584
- </div>
585
- <span class="tool-bar-count">\${fmtK(t.tokensSaved)}</span>
586
- </div>\`;
587
- }).join('');
588
- }
589
-
590
- /* ── Daily Activity Chart ── */
591
- let dayChartInstance = null;
592
- function renderDayChart() {
593
- if (dayChartInstance) dayChartInstance.destroy();
594
- const ctx = document.getElementById('dayChart');
595
- const labels = byDay.map(d => {
596
- const parts = d.date.split('-');
597
- return parts[1] + '/' + parts[2];
598
- });
599
- dayChartInstance = new Chart(ctx, {
600
- type: 'bar',
601
- data: {
602
- labels,
603
- datasets: [
604
- {
605
- label: 'Calls', data: byDay.map(d => d.calls),
606
- backgroundColor: '#60a5fa44', hoverBackgroundColor: '#60a5fa88',
607
- borderRadius: 4, borderSkipped: false, yAxisID: 'y', barPercentage: 0.7,
608
- },
609
- {
610
- label: 'Tokens Saved', data: byDay.map(d => d.tokensSaved),
611
- type: 'line', borderColor: '#34d399', pointBackgroundColor: '#34d399',
612
- pointRadius: 2, pointHoverRadius: 5, borderWidth: 2.5,
613
- yAxisID: 'y1', tension: 0.4, fill: { target: 'origin', above: '#34d39910' },
614
- },
615
- ],
616
- },
617
- options: {
618
- responsive: true, maintainAspectRatio: false,
619
- interaction: { mode: 'index', intersect: false },
620
- scales: {
621
- x: {
622
- ticks: { color: '#64748b', font: { size: 11 }, maxRotation: 0, autoSkip: true, maxTicksLimit: 15 },
623
- grid: { display: false },
624
- },
625
- y: {
626
- position: 'left', ticks: { color: '#60a5fa', font: { size: 11 } },
627
- grid: { color: '#1e2a3a' }, title: { display: true, text: 'Calls', color: '#60a5fa', font: { size: 11 } },
628
- },
629
- y1: {
630
- position: 'right', ticks: { color: '#34d399', font: { size: 11 }, callback: v => fmtK(v) },
631
- grid: { drawOnChartArea: false }, title: { display: true, text: 'Tokens Saved', color: '#34d399', font: { size: 11 } },
632
- },
633
- },
634
- plugins: {
635
- legend: { labels: { color: '#94a3b8', boxWidth: 12, usePointStyle: true, padding: 16 } },
636
- tooltip: {
637
- backgroundColor: '#1e293b', borderColor: '#334155', borderWidth: 1, titleColor: '#e2e8f0',
638
- bodyColor: '#94a3b8', cornerRadius: 8, padding: 12,
639
- callbacks: { label: ctx => ctx.dataset.label + ': ' + (ctx.datasetIndex === 1 ? fmtK(ctx.raw) : ctx.raw) },
640
- },
641
- },
642
- },
643
- });
644
- }
645
-
646
- /* ── Savings Doughnut ── */
647
- function renderSavingsChart() {
648
- const ctx = document.getElementById('savingsChart');
649
- const sorted = [...byTool].sort((a,b) => b.tokensSaved - a.tokensSaved);
650
- const top = sorted.slice(0, 8);
651
- const rest = sorted.slice(8);
652
- if (rest.length) top.push({ tool: 'others', tokensSaved: rest.reduce((s,t) => s + t.tokensSaved, 0), calls: 0 });
653
- new Chart(ctx, {
654
- type: 'doughnut',
655
- data: {
656
- labels: top.map(t => t.tool),
657
- datasets: [{
658
- data: top.map(t => t.tokensSaved),
659
- backgroundColor: top.map((_, i) => COLORS[i]),
660
- borderWidth: 0, hoverOffset: 6,
661
- }],
662
- },
663
- options: {
664
- responsive: true, cutout: '68%',
665
- plugins: {
666
- legend: { position: 'right', labels: { color: '#94a3b8', font: { size: 12 }, padding: 8, usePointStyle: true, pointStyleWidth: 10 } },
667
- tooltip: {
668
- backgroundColor: '#1e293b', borderColor: '#334155', borderWidth: 1, titleColor: '#e2e8f0',
669
- bodyColor: '#94a3b8', cornerRadius: 8, padding: 12,
670
- callbacks: { label: ctx => ' ' + ctx.label + ': ' + fmtK(ctx.raw) + ' tokens' },
671
- },
672
- },
673
- },
674
- });
675
- }
676
-
677
- /* ── Recent Calls Table ── */
678
- function renderRecent(data) {
679
- const tbody = document.getElementById('recentBody');
680
- tbody.innerHTML = data.map(r => {
681
- const ago = timeAgo(r.calledAt);
682
- return \`<tr>
683
- <td><span class="tool-badge">\${r.tool}</span></td>
684
- <td class="when-text">\${ago}</td>
685
- <td class="duration-text">\${r.durationMs}ms</td>
686
- <td class="saved-text" style="text-align:right">+\${r.tokensSaved.toLocaleString()}</td>
687
- </tr>\`;
688
- }).join('');
689
- }
690
-
691
- function timeAgo(iso) {
692
- const d = new Date(iso.includes('T') ? iso : iso + 'Z');
693
- const s = Math.floor((Date.now() - d.getTime()) / 1000);
694
- if (s < 0) return 'just now';
695
- if (s < 60) return s + 's ago';
696
- if (s < 3600) return Math.floor(s/60) + 'm ago';
697
- if (s < 86400) return Math.floor(s/3600) + 'h ago';
698
- return Math.floor(s/86400) + 'd ago';
699
- }
700
-
701
- /* ── Refresh ── */
702
- async function refreshData() {
703
- const btn = document.querySelector('.refresh-btn');
704
- btn.textContent = '⟳ Loading...';
705
- try {
706
- const res = await fetch('/api/stats');
707
- const data = await res.json();
708
- byTool = data.byTool; byDay = data.byDay;
709
- document.getElementById('kpi-calls').textContent = fmtK(data.totalCalls);
710
- document.getElementById('kpi-calls-sub').textContent = data.totalCalls.toLocaleString() + ' total invocations';
711
- document.getElementById('kpi-saved').textContent = fmtK(data.totalTokensSaved);
712
- document.getElementById('kpi-saved-sub').textContent = data.totalTokensSaved.toLocaleString() + ' tokens not wasted';
713
- const pct = data.totalTokensOut > 0 ? Math.round((data.totalTokensSaved / (data.totalTokensOut + data.totalTokensSaved)) * 100) : 0;
714
- document.getElementById('kpi-pct').textContent = pct + '%';
715
- document.getElementById('kpi-avg').textContent = (data.totalCalls > 0 ? Math.round(data.totalDurationMs / data.totalCalls) : 0) + 'ms';
716
- renderToolBars(); renderDayChart(); renderRecent(data.recentCalls);
717
- } catch(e) { console.error('Refresh failed', e); }
718
- btn.innerHTML = '&#8635; Refresh';
719
- }
720
-
721
- /* ── Init ── */
722
- renderToolBars();
723
- renderDayChart();
724
- renderSavingsChart();
725
- renderRecent(${recentJson});
726
- setInterval(refreshData, 30000);
727
- </script>
728
- </body>
1325
+ const confBars = annotationStats ? renderConfBars(annotationStats.confidenceBands, annotationStats.total) : '';
1326
+ const recentAnnots = annotationStats ? annotationStats.recent.map(a => `
1327
+ <li>
1328
+ <div><span class="annot-sym">${a.symbol}</span><span class="annot-key">${a.key}</span></div>
1329
+ <span class="annot-conf ${a.confidence >= 0.7 ? 'hi' : a.confidence >= 0.4 ? 'md' : 'lo'}">${(a.confidence * 100).toFixed(0)}%</span>
1330
+ </li>`).join('') : '';
1331
+ const learningStats = annotationStats ? `
1332
+ <div class="card learning-section">
1333
+ <div class="card-header">
1334
+ <div><div class="card-title">Confidence Distribution</div><div class="card-subtitle">${annotationStats.total} total annotations</div></div>
1335
+ </div>
1336
+ <div class="tool-bars">${confBars}</div>
1337
+ </div>
1338
+ <div class="card learning-section">
1339
+ <div class="card-header">
1340
+ <div><div class="card-title">Recent Annotations</div><div class="card-subtitle">Last ${annotationStats.recent.length}</div></div>
1341
+ </div>
1342
+ <ul class="annot-list">${recentAnnots}</ul>
1343
+ </div>
1344
+ ` : '';
1345
+ return `<!DOCTYPE html>
1346
+ <html lang="en">
1347
+ <head>
1348
+ <meta charset="UTF-8">
1349
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1350
+ <title>milens Dashboard</title>
1351
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
1352
+ <style>
1353
+ :root {
1354
+ --bg: #0a0e14; --surface: #12171e; --card: #161d27; --card-hover: #1a2332;
1355
+ --border: #1e2a3a; --border-light: #2a3a4e;
1356
+ --text: #e2e8f0; --text-secondary: #94a3b8; --text-muted: #64748b;
1357
+ --accent: #60a5fa; --accent-dim: #60a5fa22;
1358
+ --green: #34d399; --green-dim: #34d39915;
1359
+ --orange: #fbbf24; --orange-dim: #fbbf2415;
1360
+ --purple: #a78bfa; --purple-dim: #a78bfa15;
1361
+ --red: #f87171;
1362
+ --radius: 16px; --radius-sm: 10px;
1363
+ }
1364
+ * { margin: 0; padding: 0; box-sizing: border-box; }
1365
+ body {
1366
+ background: var(--bg); color: var(--text);
1367
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
1368
+ line-height: 1.5; min-height: 100vh;
1369
+ }
1370
+
1371
+ /* ── Layout ── */
1372
+ .wrapper { max-width: 1400px; margin: 0 auto; padding: 32px 24px 80px; }
1373
+
1374
+ /* ── Header ── */
1375
+ .header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 32px; }
1376
+ .header-left { display: flex; align-items: center; gap: 14px; }
1377
+ .logo { width: 40px; height: 40px; border-radius: 12px; background: linear-gradient(135deg, var(--accent), var(--purple)); display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 18px; color: #fff; }
1378
+ .header h1 { font-size: 22px; font-weight: 700; letter-spacing: -0.3px; }
1379
+ .header h1 span { color: var(--text-muted); font-weight: 400; font-size: 14px; margin-left: 8px; }
1380
+ .header-right { display: flex; align-items: center; gap: 12px; }
1381
+ .status-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); animation: pulse 2s infinite; }
1382
+ @keyframes pulse { 0%,80%,100% { opacity: 1; } 40% { opacity: 0.4; } }
1383
+ .status-text { font-size: 12px; color: var(--text-muted); }
1384
+ .refresh-btn {
1385
+ background: var(--accent-dim); color: var(--accent); border: 1px solid var(--border);
1386
+ border-radius: var(--radius-sm); padding: 8px 16px; cursor: pointer;
1387
+ font-weight: 500; font-size: 13px; transition: all 0.2s;
1388
+ }
1389
+ .refresh-btn:hover { background: var(--accent); color: #0a0e14; }
1390
+
1391
+ /* ── KPI Cards ── */
1392
+ .kpi-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 28px; }
1393
+ .kpi {
1394
+ background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
1395
+ padding: 24px; position: relative; overflow: hidden; transition: border-color 0.2s;
1396
+ }
1397
+ .kpi:hover { border-color: var(--border-light); }
1398
+ .kpi-icon { width: 40px; height: 40px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 20px; margin-bottom: 16px; }
1399
+ .kpi-icon.blue { background: var(--accent-dim); }
1400
+ .kpi-icon.green { background: var(--green-dim); }
1401
+ .kpi-icon.purple { background: var(--purple-dim); }
1402
+ .kpi-icon.orange { background: var(--orange-dim); }
1403
+ .kpi .value { font-size: 32px; font-weight: 800; letter-spacing: -1px; line-height: 1; }
1404
+ .kpi .value.blue { color: var(--accent); }
1405
+ .kpi .value.green { color: var(--green); }
1406
+ .kpi .value.purple { color: var(--purple); }
1407
+ .kpi .value.orange { color: var(--orange); }
1408
+ .kpi .label { color: var(--text-muted); font-size: 13px; margin-top: 6px; font-weight: 500; }
1409
+ .kpi .sub { color: var(--text-secondary); font-size: 12px; margin-top: 4px; }
1410
+ .kpi-glow {
1411
+ position: absolute; top: -40px; right: -40px; width: 120px; height: 120px;
1412
+ border-radius: 50%; opacity: 0.06; pointer-events: none;
1413
+ }
1414
+ .kpi-glow.blue { background: var(--accent); }
1415
+ .kpi-glow.green { background: var(--green); }
1416
+ .kpi-glow.purple { background: var(--purple); }
1417
+ .kpi-glow.orange { background: var(--orange); }
1418
+
1419
+ /* ── Charts ── */
1420
+ .grid-2 { display: grid; grid-template-columns: 5fr 7fr; gap: 16px; margin-bottom: 16px; }
1421
+ .grid-full { margin-bottom: 16px; }
1422
+ .card {
1423
+ background: var(--card); border: 1px solid var(--border); border-radius: var(--radius);
1424
+ padding: 24px; transition: border-color 0.2s;
1425
+ }
1426
+ .card:hover { border-color: var(--border-light); }
1427
+ .card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
1428
+ .card-title { font-size: 14px; font-weight: 600; color: var(--text); }
1429
+ .card-subtitle { font-size: 12px; color: var(--text-muted); }
1430
+
1431
+ /* ── Chart containers ── */
1432
+ .chart-container { position: relative; width: 100%; }
1433
+ .chart-container.h-280 { height: 280px; }
1434
+ .chart-container.h-300 { height: 300px; }
1435
+
1436
+ /* ── Top Tools Bar ── */
1437
+ .tool-bars { display: flex; flex-direction: column; gap: 10px; }
1438
+ .tool-bar-row { display: flex; align-items: center; gap: 12px; }
1439
+ .tool-bar-name { width: 140px; font-size: 12px; font-family: 'SF Mono', 'Fira Code', monospace; color: var(--text-secondary); text-align: right; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1440
+ .tool-bar-track { flex: 1; height: 28px; background: var(--surface); border-radius: 6px; overflow: hidden; position: relative; }
1441
+ .tool-bar-fill { height: 100%; border-radius: 6px; display: flex; align-items: center; padding: 0 10px; font-size: 11px; font-weight: 600; color: #fff; min-width: fit-content; transition: width 0.6s ease; }
1442
+ .tool-bar-count { font-size: 12px; color: var(--text-muted); min-width: 36px; text-align: right; }
1443
+
1444
+ /* ── Recent Table ── */
1445
+ .table-wrapper { max-height: 400px; overflow-y: auto; border-radius: var(--radius-sm); }
1446
+ .table-wrapper::-webkit-scrollbar { width: 6px; }
1447
+ .table-wrapper::-webkit-scrollbar-track { background: transparent; }
1448
+ .table-wrapper::-webkit-scrollbar-thumb { background: var(--border-light); border-radius: 3px; }
1449
+ table { width: 100%; border-collapse: collapse; font-size: 13px; }
1450
+ thead { position: sticky; top: 0; z-index: 1; }
1451
+ th {
1452
+ text-align: left; color: var(--text-muted); font-weight: 500; padding: 10px 16px;
1453
+ background: var(--card); border-bottom: 1px solid var(--border); font-size: 11px;
1454
+ text-transform: uppercase; letter-spacing: 0.5px;
1455
+ }
1456
+ td { padding: 10px 16px; border-bottom: 1px solid var(--border); }
1457
+ tr:hover td { background: var(--surface); }
1458
+ .tool-badge {
1459
+ display: inline-flex; align-items: center; gap: 4px;
1460
+ background: var(--accent-dim); color: var(--accent); padding: 3px 10px;
1461
+ border-radius: 6px; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 11px; font-weight: 500;
1462
+ }
1463
+ .when-text { color: var(--text-muted); }
1464
+ .duration-text { color: var(--text-secondary); font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; }
1465
+ .saved-text { color: var(--green); font-weight: 600; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; }
1466
+
1467
+ /* ── Footer ── */
1468
+ .footer { text-align: center; padding: 24px 0 0; color: var(--text-muted); font-size: 12px; }
1469
+
1470
+ /* ── Tabs ── */
1471
+ .tab-nav { display: flex; gap: 4px; margin-bottom: 28px; border-bottom: 2px solid var(--border); padding-bottom: 0; }
1472
+ .tab { padding: 10px 24px; cursor: pointer; font-size: 14px; font-weight: 500; color: var(--text-muted); border-bottom: 2px solid transparent; margin-bottom: -2px; transition: all 0.2s; background: none; border-top: none; border-left: none; border-right: none; outline: none; }
1473
+ .tab:hover { color: var(--text-secondary); }
1474
+ .tab.active { color: var(--accent); border-bottom-color: var(--accent); }
1475
+ .tab-content { display: none; }
1476
+ .tab-content.active { display: block; }
1477
+
1478
+ /* ── Learning ── */
1479
+ .suggestion-box { background: var(--accent-dim); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 20px; text-align: center; margin-bottom: 20px; }
1480
+ .suggestion-box code { background: var(--surface); padding: 2px 8px; border-radius: 4px; font-family: 'SF Mono', 'Fira Code', monospace; font-size: 13px; color: var(--accent); }
1481
+ .learning-section { margin-bottom: 16px; }
1482
+ .annot-list { list-style: none; padding: 0; }
1483
+ .annot-list li { padding: 10px 0; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; }
1484
+ .annot-list li:last-child { border-bottom: none; }
1485
+ .annot-sym { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 13px; color: var(--accent); }
1486
+ .annot-key { font-size: 11px; color: var(--text-muted); background: var(--surface); padding: 2px 8px; border-radius: 4px; margin-left: 8px; }
1487
+ .annot-conf { font-size: 12px; font-weight: 600; }
1488
+ .annot-conf.hi { color: var(--green); }
1489
+ .annot-conf.md { color: var(--orange); }
1490
+ .annot-conf.lo { color: var(--red); }
1491
+
1492
+ /* ── Responsive ── */
1493
+ @media (max-width: 1024px) { .kpi-grid { grid-template-columns: repeat(2, 1fr); } .grid-2 { grid-template-columns: 1fr; } }
1494
+ @media (max-width: 640px) { .kpi-grid { grid-template-columns: 1fr; } .wrapper { padding: 16px 12px 80px; } }
1495
+ </style>
1496
+ </head>
1497
+ <body>
1498
+ <div class="wrapper">
1499
+
1500
+ <!-- Header -->
1501
+ <div class="header">
1502
+ <div class="header-left">
1503
+ <div class="logo">m</div>
1504
+ <h1>milens <span>dashboard</span></h1>
1505
+ </div>
1506
+ <div class="header-right">
1507
+ <span class="repo-badge" style="background:rgba(88,166,255,0.12);color:#58a6ff;border:1px solid rgba(88,166,255,0.25);border-radius:20px;padding:4px 12px;font-size:0.78em;font-weight:600;margin-right:12px;">${repoFilter ? repoFilter.replace(/\\\\/g, '/').split('/').pop() || repoFilter : 'All Repos'}</span>
1508
+ <div class="status-dot"></div>
1509
+ <span class="status-text">Live</span>
1510
+ <button class="refresh-btn" onclick="refreshData()">&#8635; Refresh</button>
1511
+ </div>
1512
+ </div>
1513
+
1514
+ <!-- Tab Navigation -->
1515
+ <div class="tab-nav">
1516
+ <button class="tab active" data-tab="usage" onclick="switchTab('usage')">Usage Analytics</button>
1517
+ <button class="tab" data-tab="learning" onclick="switchTab('learning')">Learning</button>
1518
+ </div>
1519
+
1520
+ <div id="tab-usage" class="tab-content active">
1521
+
1522
+ <!-- KPIs -->
1523
+ <div class="kpi-grid">
1524
+ <div class="kpi">
1525
+ <div class="kpi-icon blue">&#9881;</div>
1526
+ <div class="value blue" id="kpi-calls">${fmtNum(stats.totalCalls)}</div>
1527
+ <div class="label">Tool Calls</div>
1528
+ <div class="sub" id="kpi-calls-sub">${stats.totalCalls.toLocaleString()} total invocations</div>
1529
+ <div class="kpi-glow blue"></div>
1530
+ </div>
1531
+ <div class="kpi">
1532
+ <div class="kpi-icon green">&#9889;</div>
1533
+ <div class="value green" id="kpi-saved">${fmtNum(stats.totalTokensSaved)}</div>
1534
+ <div class="label">Tokens Saved</div>
1535
+ <div class="sub" id="kpi-saved-sub">${stats.totalTokensSaved.toLocaleString()} tokens not wasted</div>
1536
+ <div class="kpi-glow green"></div>
1537
+ </div>
1538
+ <div class="kpi">
1539
+ <div class="kpi-icon purple">&#9733;</div>
1540
+ <div class="value purple" id="kpi-pct">${savingsPercent}%</div>
1541
+ <div class="label">Token Efficiency</div>
1542
+ <div class="sub">${fmtNum(stats.totalTokensOut)} returned vs ${fmtNum(stats.totalTokensSaved)} saved</div>
1543
+ <div class="kpi-glow purple"></div>
1544
+ </div>
1545
+ <div class="kpi">
1546
+ <div class="kpi-icon orange">&#9201;</div>
1547
+ <div class="value orange" id="kpi-avg">${stats.totalCalls > 0 ? Math.round(stats.totalDurationMs / stats.totalCalls) : 0}ms</div>
1548
+ <div class="label">Avg Response Time</div>
1549
+ <div class="sub">${(stats.totalDurationMs / 1000).toFixed(1)}s total processing</div>
1550
+ <div class="kpi-glow orange"></div>
1551
+ </div>
1552
+ </div>
1553
+
1554
+ <!-- Charts Row -->
1555
+ <div class="grid-2">
1556
+ <div class="card">
1557
+ <div class="card-header">
1558
+ <div>
1559
+ <div class="card-title">Top Tools</div>
1560
+ <div class="card-subtitle">By number of calls</div>
1561
+ </div>
1562
+ </div>
1563
+ <div id="toolBars" class="tool-bars"></div>
1564
+ </div>
1565
+ <div class="card">
1566
+ <div class="card-header">
1567
+ <div>
1568
+ <div class="card-title">Daily Activity</div>
1569
+ <div class="card-subtitle">Calls &amp; tokens saved over the last 30 days</div>
1570
+ </div>
1571
+ </div>
1572
+ <div class="chart-container h-280"><canvas id="dayChart"></canvas></div>
1573
+ </div>
1574
+ </div>
1575
+
1576
+ <!-- Savings Distribution -->
1577
+ <div class="grid-2" style="grid-template-columns: 7fr 5fr;">
1578
+ <div class="card">
1579
+ <div class="card-header">
1580
+ <div>
1581
+ <div class="card-title">Recent Tool Calls</div>
1582
+ <div class="card-subtitle">Last 50 invocations</div>
1583
+ </div>
1584
+ </div>
1585
+ <div class="table-wrapper">
1586
+ <table>
1587
+ <thead><tr><th>Tool</th><th>When</th><th>Duration</th><th style="text-align:right">Tokens Saved</th></tr></thead>
1588
+ <tbody id="recentBody"></tbody>
1589
+ </table>
1590
+ </div>
1591
+ </div>
1592
+ <div class="card">
1593
+ <div class="card-header">
1594
+ <div>
1595
+ <div class="card-title">Savings by Tool</div>
1596
+ <div class="card-subtitle">Token savings distribution</div>
1597
+ </div>
1598
+ </div>
1599
+ <div class="chart-container h-300"><canvas id="savingsChart"></canvas></div>
1600
+ </div>
1601
+ </div>
1602
+
1603
+ <div class="footer">milens &middot; auto-refreshes every 30s</div>
1604
+ </div><!-- /tab-usage -->
1605
+
1606
+ <div id="tab-learning" class="tab-content">
1607
+ <div class="suggestion-box">
1608
+ <h3 style="margin-bottom:8px;font-weight:600;">Metrics &amp; Insights</h3>
1609
+ <p style="color:var(--text-secondary);font-size:13px;">Run <code>milens metrics</code> for a full code-quality metrics report.</p>
1610
+ </div>
1611
+ ${learningStats}
1612
+ </div>
1613
+
1614
+ </div><!-- /wrapper -->
1615
+
1616
+ <script>
1617
+ const COLORS = ['#60a5fa','#34d399','#fbbf24','#a78bfa','#f87171','#2dd4bf','#818cf8','#fb923c','#e879f9','#38bdf8','#4ade80','#facc15','#f472b6','#22d3ee','#a3e635','#c084fc'];
1618
+ const BAR_COLORS = ['#60a5fa','#34d399','#fbbf24','#a78bfa','#f87171','#2dd4bf','#818cf8','#fb923c','#e879f9','#38bdf8'];
1619
+ let byTool = ${byToolJson};
1620
+ let byDay = ${byDayJson};
1621
+
1622
+ function fmtK(n) { return n >= 1e6 ? (n/1e6).toFixed(1)+'M' : n >= 1e3 ? (n/1e3).toFixed(1)+'K' : n; }
1623
+
1624
+ /* ── Top Tools Horizontal Bars ── */
1625
+ function renderToolBars() {
1626
+ const el = document.getElementById('toolBars');
1627
+ const sorted = [...byTool].sort((a,b) => b.calls - a.calls).slice(0, 10);
1628
+ const maxCalls = sorted[0]?.calls || 1;
1629
+ el.innerHTML = sorted.map((t, i) => {
1630
+ const pct = Math.max(8, (t.calls / maxCalls) * 100);
1631
+ const col = BAR_COLORS[i % BAR_COLORS.length];
1632
+ return \`<div class="tool-bar-row">
1633
+ <span class="tool-bar-name">\${t.tool}</span>
1634
+ <div class="tool-bar-track">
1635
+ <div class="tool-bar-fill" style="width:\${pct}%;background:linear-gradient(90deg,\${col}dd,\${col}88)">\${t.calls}</div>
1636
+ </div>
1637
+ <span class="tool-bar-count">\${fmtK(t.tokensSaved)}</span>
1638
+ </div>\`;
1639
+ }).join('');
1640
+ }
1641
+
1642
+ /* ── Daily Activity Chart ── */
1643
+ let dayChartInstance = null;
1644
+ function renderDayChart() {
1645
+ if (dayChartInstance) dayChartInstance.destroy();
1646
+ const ctx = document.getElementById('dayChart');
1647
+ const labels = byDay.map(d => {
1648
+ const parts = d.date.split('-');
1649
+ return parts[1] + '/' + parts[2];
1650
+ });
1651
+ dayChartInstance = new Chart(ctx, {
1652
+ type: 'bar',
1653
+ data: {
1654
+ labels,
1655
+ datasets: [
1656
+ {
1657
+ label: 'Calls', data: byDay.map(d => d.calls),
1658
+ backgroundColor: '#60a5fa44', hoverBackgroundColor: '#60a5fa88',
1659
+ borderRadius: 4, borderSkipped: false, yAxisID: 'y', barPercentage: 0.7,
1660
+ },
1661
+ {
1662
+ label: 'Tokens Saved', data: byDay.map(d => d.tokensSaved),
1663
+ type: 'line', borderColor: '#34d399', pointBackgroundColor: '#34d399',
1664
+ pointRadius: 2, pointHoverRadius: 5, borderWidth: 2.5,
1665
+ yAxisID: 'y1', tension: 0.4, fill: { target: 'origin', above: '#34d39910' },
1666
+ },
1667
+ ],
1668
+ },
1669
+ options: {
1670
+ responsive: true, maintainAspectRatio: false,
1671
+ interaction: { mode: 'index', intersect: false },
1672
+ scales: {
1673
+ x: {
1674
+ ticks: { color: '#64748b', font: { size: 11 }, maxRotation: 0, autoSkip: true, maxTicksLimit: 15 },
1675
+ grid: { display: false },
1676
+ },
1677
+ y: {
1678
+ position: 'left', ticks: { color: '#60a5fa', font: { size: 11 } },
1679
+ grid: { color: '#1e2a3a' }, title: { display: true, text: 'Calls', color: '#60a5fa', font: { size: 11 } },
1680
+ },
1681
+ y1: {
1682
+ position: 'right', ticks: { color: '#34d399', font: { size: 11 }, callback: v => fmtK(v) },
1683
+ grid: { drawOnChartArea: false }, title: { display: true, text: 'Tokens Saved', color: '#34d399', font: { size: 11 } },
1684
+ },
1685
+ },
1686
+ plugins: {
1687
+ legend: { labels: { color: '#94a3b8', boxWidth: 12, usePointStyle: true, padding: 16 } },
1688
+ tooltip: {
1689
+ backgroundColor: '#1e293b', borderColor: '#334155', borderWidth: 1, titleColor: '#e2e8f0',
1690
+ bodyColor: '#94a3b8', cornerRadius: 8, padding: 12,
1691
+ callbacks: { label: ctx => ctx.dataset.label + ': ' + (ctx.datasetIndex === 1 ? fmtK(ctx.raw) : ctx.raw) },
1692
+ },
1693
+ },
1694
+ },
1695
+ });
1696
+ }
1697
+
1698
+ /* ── Savings Doughnut ── */
1699
+ function renderSavingsChart() {
1700
+ const ctx = document.getElementById('savingsChart');
1701
+ const sorted = [...byTool].sort((a,b) => b.tokensSaved - a.tokensSaved);
1702
+ const top = sorted.slice(0, 8);
1703
+ const rest = sorted.slice(8);
1704
+ if (rest.length) top.push({ tool: 'others', tokensSaved: rest.reduce((s,t) => s + t.tokensSaved, 0), calls: 0 });
1705
+ new Chart(ctx, {
1706
+ type: 'doughnut',
1707
+ data: {
1708
+ labels: top.map(t => t.tool),
1709
+ datasets: [{
1710
+ data: top.map(t => t.tokensSaved),
1711
+ backgroundColor: top.map((_, i) => COLORS[i]),
1712
+ borderWidth: 0, hoverOffset: 6,
1713
+ }],
1714
+ },
1715
+ options: {
1716
+ responsive: true, cutout: '68%',
1717
+ plugins: {
1718
+ legend: { position: 'right', labels: { color: '#94a3b8', font: { size: 12 }, padding: 8, usePointStyle: true, pointStyleWidth: 10 } },
1719
+ tooltip: {
1720
+ backgroundColor: '#1e293b', borderColor: '#334155', borderWidth: 1, titleColor: '#e2e8f0',
1721
+ bodyColor: '#94a3b8', cornerRadius: 8, padding: 12,
1722
+ callbacks: { label: ctx => ' ' + ctx.label + ': ' + fmtK(ctx.raw) + ' tokens' },
1723
+ },
1724
+ },
1725
+ },
1726
+ });
1727
+ }
1728
+
1729
+ /* ── Recent Calls Table ── */
1730
+ function renderRecent(data) {
1731
+ const tbody = document.getElementById('recentBody');
1732
+ tbody.innerHTML = data.map(r => {
1733
+ const ago = timeAgo(r.calledAt);
1734
+ return \`<tr>
1735
+ <td><span class="tool-badge">\${r.tool}</span></td>
1736
+ <td class="when-text">\${ago}</td>
1737
+ <td class="duration-text">\${r.durationMs}ms</td>
1738
+ <td class="saved-text" style="text-align:right">+\${r.tokensSaved.toLocaleString()}</td>
1739
+ </tr>\`;
1740
+ }).join('');
1741
+ }
1742
+
1743
+ function timeAgo(iso) {
1744
+ const d = new Date(iso.includes('T') ? iso : iso + 'Z');
1745
+ const s = Math.floor((Date.now() - d.getTime()) / 1000);
1746
+ if (s < 0) return 'just now';
1747
+ if (s < 60) return s + 's ago';
1748
+ if (s < 3600) return Math.floor(s/60) + 'm ago';
1749
+ if (s < 86400) return Math.floor(s/3600) + 'h ago';
1750
+ return Math.floor(s/86400) + 'd ago';
1751
+ }
1752
+
1753
+ /* ── Refresh ── */
1754
+ async function refreshData() {
1755
+ const btn = document.querySelector('.refresh-btn');
1756
+ btn.textContent = '⟳ Loading...';
1757
+ try {
1758
+ const res = await fetch('/api/stats');
1759
+ const data = await res.json();
1760
+ byTool = data.byTool; byDay = data.byDay;
1761
+ document.getElementById('kpi-calls').textContent = fmtK(data.totalCalls);
1762
+ document.getElementById('kpi-calls-sub').textContent = data.totalCalls.toLocaleString() + ' total invocations';
1763
+ document.getElementById('kpi-saved').textContent = fmtK(data.totalTokensSaved);
1764
+ document.getElementById('kpi-saved-sub').textContent = data.totalTokensSaved.toLocaleString() + ' tokens not wasted';
1765
+ const pct = data.totalTokensOut > 0 ? Math.round((data.totalTokensSaved / (data.totalTokensOut + data.totalTokensSaved)) * 100) : 0;
1766
+ document.getElementById('kpi-pct').textContent = pct + '%';
1767
+ document.getElementById('kpi-avg').textContent = (data.totalCalls > 0 ? Math.round(data.totalDurationMs / data.totalCalls) : 0) + 'ms';
1768
+ renderToolBars(); renderDayChart(); renderRecent(data.recentCalls);
1769
+ } catch(e) { console.error('Refresh failed', e); }
1770
+ btn.innerHTML = '&#8635; Refresh';
1771
+ }
1772
+
1773
+ /* ── Tab Switching ── */
1774
+ function switchTab(tab) {
1775
+ document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tab));
1776
+ document.querySelectorAll('.tab-content').forEach(c => c.classList.toggle('active', c.id === 'tab-' + tab));
1777
+ }
1778
+
1779
+ /* ── Init ── */
1780
+ renderToolBars();
1781
+ renderDayChart();
1782
+ renderSavingsChart();
1783
+ renderRecent(${recentJson});
1784
+ setInterval(refreshData, 30000);
1785
+ </script>
1786
+ </body>
729
1787
  </html>`;
730
1788
  }
1789
+ function renderConfBars(bands, total) {
1790
+ const labels = ['0.0–0.4', '0.4–0.7', '0.7–0.9', '0.9–1.0'];
1791
+ const colors = ['#f87171', '#fbbf24', '#60a5fa', '#34d399'];
1792
+ return labels.map((label, i) => {
1793
+ const pct = total > 0 ? (bands[i] / total) * 100 : 0;
1794
+ return '<div class="tool-bar-row"><span class="tool-bar-name">' + label + '</span><div class="tool-bar-track"><div class="tool-bar-fill" style="width:' + Math.max(4, pct) + '%;background:' + colors[i] + 'dd">' + bands[i] + '</div></div></div>';
1795
+ }).join('');
1796
+ }
731
1797
  //# sourceMappingURL=cli.js.map