milens 0.6.2 → 0.6.4

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