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.
- package/.agents/skills/adapters/SKILL.md +51 -0
- package/.agents/skills/analyzer/SKILL.md +77 -0
- package/.agents/skills/apps/SKILL.md +66 -0
- package/.agents/skills/docs/SKILL.md +73 -0
- package/.agents/skills/milens/SKILL.md +198 -0
- package/.agents/skills/milens-architect/SKILL.md +128 -0
- package/.agents/skills/milens-code-review/SKILL.md +186 -0
- package/.agents/skills/milens-debugger/SKILL.md +141 -0
- package/.agents/skills/milens-eval/SKILL.md +221 -0
- package/.agents/skills/milens-plan/SKILL.md +227 -0
- package/.agents/skills/milens-refactor-clean/SKILL.md +209 -0
- package/.agents/skills/milens-security-review/SKILL.md +224 -0
- package/.agents/skills/milens-tdd/SKILL.md +156 -0
- package/.agents/skills/orchestrator/SKILL.md +59 -0
- package/.agents/skills/parser/SKILL.md +81 -0
- package/.agents/skills/root/SKILL.md +86 -0
- package/.agents/skills/scripts/SKILL.md +45 -0
- package/.agents/skills/security/SKILL.md +65 -0
- package/.agents/skills/server/SKILL.md +72 -0
- package/.agents/skills/store/SKILL.md +75 -0
- package/.agents/skills/test/SKILL.md +121 -0
- package/LICENSE +21 -75
- package/README.md +356 -453
- package/adapters/README.md +107 -0
- package/adapters/claude-code/.claude/mcp.json +9 -0
- package/adapters/claude-code/CLAUDE.md +58 -0
- package/adapters/codex/.codex/codex.md +52 -0
- package/adapters/copilot/.github/copilot-instructions.md +62 -0
- package/adapters/cursor/.cursorrules +9 -0
- package/adapters/gemini/.gemini/context.md +58 -0
- package/adapters/opencode/.opencode/config.json +9 -0
- package/adapters/opencode/AGENTS.md +58 -0
- package/adapters/zed/.zed/settings.json +8 -0
- package/dist/agents-md.d.ts +3 -0
- package/dist/agents-md.d.ts.map +1 -0
- package/dist/agents-md.js +114 -0
- package/dist/agents-md.js.map +1 -0
- package/dist/analyzer/engine.d.ts +1 -0
- package/dist/analyzer/engine.d.ts.map +1 -1
- package/dist/analyzer/engine.js +37 -7
- package/dist/analyzer/engine.js.map +1 -1
- package/dist/cli.js +1472 -406
- package/dist/cli.js.map +1 -1
- package/dist/metrics.d.ts +51 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +64 -0
- package/dist/metrics.js.map +1 -0
- package/dist/orchestrator/orchestrator.d.ts +65 -0
- package/dist/orchestrator/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator/orchestrator.js +178 -0
- package/dist/orchestrator/orchestrator.js.map +1 -0
- package/dist/orchestrator/reporter.d.ts +15 -0
- package/dist/orchestrator/reporter.d.ts.map +1 -0
- package/dist/orchestrator/reporter.js +38 -0
- package/dist/orchestrator/reporter.js.map +1 -0
- package/dist/parser/lang-go.js +47 -47
- package/dist/parser/lang-java.js +29 -29
- package/dist/parser/lang-js.js +105 -105
- package/dist/parser/lang-php.js +38 -38
- package/dist/parser/lang-py.js +34 -34
- package/dist/parser/lang-ruby.js +14 -14
- package/dist/parser/lang-rust.js +30 -30
- package/dist/parser/lang-ts.js +191 -191
- package/dist/security/deps.d.ts +38 -0
- package/dist/security/deps.d.ts.map +1 -0
- package/dist/security/deps.js +685 -0
- package/dist/security/deps.js.map +1 -0
- package/dist/security/rules.d.ts +42 -0
- package/dist/security/rules.d.ts.map +1 -0
- package/dist/security/rules.js +943 -0
- package/dist/security/rules.js.map +1 -0
- package/dist/server/hooks.d.ts +29 -0
- package/dist/server/hooks.d.ts.map +1 -0
- package/dist/server/hooks.js +332 -0
- package/dist/server/hooks.js.map +1 -0
- package/dist/server/mcp-prompts.d.ts +277 -0
- package/dist/server/mcp-prompts.d.ts.map +1 -0
- package/dist/server/mcp-prompts.js +627 -0
- package/dist/server/mcp-prompts.js.map +1 -0
- package/dist/server/mcp.d.ts.map +1 -1
- package/dist/server/mcp.js +1030 -652
- package/dist/server/mcp.js.map +1 -1
- package/dist/server/test-plan.d.ts +20 -0
- package/dist/server/test-plan.d.ts.map +1 -0
- package/dist/server/test-plan.js +100 -0
- package/dist/server/test-plan.js.map +1 -0
- package/dist/server/watcher.d.ts +39 -0
- package/dist/server/watcher.d.ts.map +1 -0
- package/dist/server/watcher.js +134 -0
- package/dist/server/watcher.js.map +1 -0
- package/dist/skills.js +197 -153
- package/dist/skills.js.map +1 -1
- package/dist/store/annotations.d.ts +41 -0
- package/dist/store/annotations.d.ts.map +1 -0
- package/dist/store/annotations.js +195 -0
- package/dist/store/annotations.js.map +1 -0
- package/dist/store/confidence.d.ts +28 -0
- package/dist/store/confidence.d.ts.map +1 -0
- package/dist/store/confidence.js +109 -0
- package/dist/store/confidence.js.map +1 -0
- package/dist/store/db.d.ts +53 -14
- package/dist/store/db.d.ts.map +1 -1
- package/dist/store/db.js +447 -240
- package/dist/store/db.js.map +1 -1
- package/dist/store/schema.sql +143 -116
- package/dist/store/vectors.js +2 -2
- package/dist/types.d.ts +101 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +22 -0
- package/package.json +81 -66
- package/dist/gateway/analyzer.d.ts +0 -6
- package/dist/gateway/analyzer.d.ts.map +0 -1
- package/dist/gateway/analyzer.js +0 -218
- package/dist/gateway/analyzer.js.map +0 -1
- package/dist/gateway/cache.d.ts +0 -35
- package/dist/gateway/cache.d.ts.map +0 -1
- package/dist/gateway/cache.js +0 -175
- package/dist/gateway/cache.js.map +0 -1
- package/dist/gateway/config.d.ts +0 -10
- package/dist/gateway/config.d.ts.map +0 -1
- package/dist/gateway/config.js +0 -167
- package/dist/gateway/config.js.map +0 -1
- package/dist/gateway/context-memory.d.ts +0 -68
- package/dist/gateway/context-memory.d.ts.map +0 -1
- package/dist/gateway/context-memory.js +0 -157
- package/dist/gateway/context-memory.js.map +0 -1
- package/dist/gateway/observability.d.ts +0 -83
- package/dist/gateway/observability.d.ts.map +0 -1
- package/dist/gateway/observability.js +0 -152
- package/dist/gateway/observability.js.map +0 -1
- package/dist/gateway/privacy.d.ts +0 -27
- package/dist/gateway/privacy.d.ts.map +0 -1
- package/dist/gateway/privacy.js +0 -139
- package/dist/gateway/privacy.js.map +0 -1
- package/dist/gateway/providers.d.ts +0 -66
- package/dist/gateway/providers.d.ts.map +0 -1
- package/dist/gateway/providers.js +0 -377
- package/dist/gateway/providers.js.map +0 -1
- package/dist/gateway/router.d.ts +0 -18
- package/dist/gateway/router.d.ts.map +0 -1
- package/dist/gateway/router.js +0 -102
- package/dist/gateway/router.js.map +0 -1
- package/dist/gateway/server.d.ts +0 -20
- package/dist/gateway/server.d.ts.map +0 -1
- package/dist/gateway/server.js +0 -387
- package/dist/gateway/server.js.map +0 -1
- package/dist/gateway/translator.d.ts +0 -19
- package/dist/gateway/translator.d.ts.map +0 -1
- package/dist/gateway/translator.js +0 -340
- package/dist/gateway/translator.js.map +0 -1
- package/dist/gateway/types.d.ts +0 -215
- package/dist/gateway/types.d.ts.map +0 -1
- package/dist/gateway/types.js +0 -3
- package/dist/gateway/types.js.map +0 -1
- 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
|
-
.
|
|
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
|
-
|
|
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(`${
|
|
206
|
-
for (const entry of
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
<
|
|
337
|
-
<
|
|
338
|
-
<
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
<
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
font-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
/* ──
|
|
381
|
-
.
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
386
|
-
.
|
|
387
|
-
.
|
|
388
|
-
.
|
|
389
|
-
.
|
|
390
|
-
.
|
|
391
|
-
|
|
392
|
-
.
|
|
393
|
-
.
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
.
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
.kpi
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
.
|
|
410
|
-
.
|
|
411
|
-
.
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
415
|
-
.
|
|
416
|
-
.
|
|
417
|
-
.
|
|
418
|
-
.
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
.
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
.
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
.
|
|
430
|
-
.
|
|
431
|
-
.
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
.
|
|
436
|
-
.
|
|
437
|
-
.
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
.tool-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
.
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
.
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
<div class="
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
</div>
|
|
543
|
-
<div class="
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
<div class="
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
</div>
|
|
557
|
-
<div class="
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
<
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
<
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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()">↻ 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">⚙</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">⚡</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">★</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">⏱</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 & 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 · 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 & 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 = '↻ 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
|