@monoes/cli 1.0.5 → 1.0.7

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.
@@ -23,8 +23,8 @@ export function generateStatuslineScript(options) {
23
23
  const maxAgents = options.runtime.maxAgents;
24
24
  return `#!/usr/bin/env node
25
25
  /**
26
- * Monobrain Statusline Generator (Optimized)
27
- * Displays real-time V1 implementation progress and system status
26
+ * Monobrain V1 Statusline Generator (Optimized)
27
+ * Displays real-time v1 implementation progress and system status
28
28
  *
29
29
  * Usage: node statusline.cjs [--json] [--compact]
30
30
  *
@@ -49,6 +49,31 @@ const CONFIG = {
49
49
 
50
50
  const CWD = process.cwd();
51
51
 
52
+ // Read version from nearest package.json
53
+ function getVersion() {
54
+ const scriptDir = path.dirname(__filename);
55
+ const walkCandidates = [
56
+ path.join(scriptDir, '..', '..', 'package.json'),
57
+ path.join(scriptDir, '..', '..', '..', 'package.json'),
58
+ path.join(scriptDir, '..', '..', '..', '..', 'package.json'),
59
+ ];
60
+ for (const p of walkCandidates) {
61
+ try {
62
+ const pkg = JSON.parse(fs.readFileSync(p, 'utf-8'));
63
+ if (pkg.version && (pkg.name === 'monobrain' || pkg.name === '@monoes/cli' || (pkg.name || '').startsWith('@monobrain'))) {
64
+ return \`v\${pkg.version}\`;
65
+ }
66
+ } catch { /* ignore */ }
67
+ }
68
+ try {
69
+ const prefix = execSync('npm config get prefix', { encoding: 'utf-8', timeout: 2000 }).trim();
70
+ const pkg = JSON.parse(fs.readFileSync(path.join(prefix, 'lib', 'node_modules', 'monobrain', 'package.json'), 'utf-8'));
71
+ if (pkg.version) return \`v\${pkg.version}\`;
72
+ } catch { /* ignore */ }
73
+ return 'v1.0.6';
74
+ }
75
+ const VERSION = getVersion();
76
+
52
77
  // ANSI colors
53
78
  const c = {
54
79
  reset: '\\x1b[0m',
@@ -130,7 +155,7 @@ function getGitInfo() {
130
155
  'git rev-list --left-right --count HEAD...@{upstream} 2>/dev/null || echo "0 0"',
131
156
  ].join('; ');
132
157
 
133
- const raw = safeExec("sh -c '" + script + "'", 3000);
158
+ const raw = safeExec(\`sh -c '\${script}'\`, 3000);
134
159
  if (!raw) return result;
135
160
 
136
161
  const parts = raw.split('---SEP---').map(s => s.trim());
@@ -158,44 +183,101 @@ function getGitInfo() {
158
183
  return result;
159
184
  }
160
185
 
186
+ // Normalise a model ID string to a short display name
187
+ function modelLabel(id) {
188
+ if (id.includes('opus')) return 'Opus 4.6';
189
+ if (id.includes('sonnet')) return 'Sonnet 4.6';
190
+ if (id.includes('haiku')) return 'Haiku 4.5';
191
+ return id.split('-').slice(1, 3).join(' ');
192
+ }
193
+
194
+ // Read the last assistant model from the most recent session JSONL.
195
+ // Claude Code writes each assistant turn to ~/.claude/projects/<escaped-cwd>/<uuid>.jsonl
196
+ // with a "message.model" field — this is the most accurate live source and
197
+ // correctly reflects /model session overrides.
198
+ function getModelFromSessionJSONL() {
199
+ try {
200
+ // Escape CWD the same way Claude Code does: replace '/' with '-'
201
+ const escaped = CWD.replace(/\\//g, '-');
202
+ const projectsDir = path.join(os.homedir(), '.claude', 'projects', escaped);
203
+ if (!fs.existsSync(projectsDir)) return null;
204
+
205
+ // Most recently modified JSONL = current (or latest) session
206
+ const files = fs.readdirSync(projectsDir)
207
+ .filter(f => f.endsWith('.jsonl'))
208
+ .map(f => ({ f, mt: (() => { try { return fs.statSync(path.join(projectsDir, f)).mtimeMs; } catch { return 0; } })() }))
209
+ .sort((a, b) => b.mt - a.mt);
210
+ if (files.length === 0) return null;
211
+
212
+ const sessionFile = path.join(projectsDir, files[0].f);
213
+ const raw = fs.readFileSync(sessionFile, 'utf-8');
214
+ const lines = raw.split('\\n').filter(Boolean);
215
+
216
+ // Scan from the end to find the most recent assistant model
217
+ for (let i = lines.length - 1; i >= 0; i--) {
218
+ try {
219
+ const entry = JSON.parse(lines[i]);
220
+ const model = entry?.message?.model || entry?.model;
221
+ if (model && typeof model === 'string' && model.startsWith('claude')) {
222
+ return model;
223
+ }
224
+ } catch { /* skip malformed line */ }
225
+ }
226
+ } catch { /* ignore */ }
227
+ return null;
228
+ }
229
+
161
230
  // Detect model name from Claude config (pure file reads, no exec)
162
231
  function getModelName() {
232
+ // PRIMARY: scan the live session JSONL — reflects /model overrides in real time
233
+ const sessionModel = getModelFromSessionJSONL();
234
+ if (sessionModel) return modelLabel(sessionModel);
235
+
236
+ // SECONDARY: ~/.claude.json lastModelUsage for this exact project path
237
+ // (longest-prefix match to avoid short paths like /Users matching first)
163
238
  try {
164
239
  const claudeConfig = readJSON(path.join(os.homedir(), '.claude.json'));
165
- if (claudeConfig && claudeConfig.projects) {
240
+ if (claudeConfig?.projects) {
241
+ let bestMatch = null;
242
+ let bestLen = -1;
166
243
  for (const [projectPath, projectConfig] of Object.entries(claudeConfig.projects)) {
167
244
  if (CWD === projectPath || CWD.startsWith(projectPath + '/')) {
168
- const usage = projectConfig.lastModelUsage;
169
- if (usage) {
170
- const ids = Object.keys(usage);
171
- if (ids.length > 0) {
172
- let modelId = ids[ids.length - 1];
173
- let latest = 0;
174
- for (const id of ids) {
175
- const ts = usage[id] && usage[id].lastUsedAt ? new Date(usage[id].lastUsedAt).getTime() : 0;
176
- if (ts > latest) { latest = ts; modelId = id; }
177
- }
178
- if (modelId.includes('opus')) return 'Opus 4.6 (1M context)';
179
- if (modelId.includes('sonnet')) return 'Sonnet 4.6';
180
- if (modelId.includes('haiku')) return 'Haiku 4.5';
181
- return modelId.split('-').slice(1, 3).join(' ');
182
- }
245
+ if (projectPath.length > bestLen) {
246
+ bestLen = projectPath.length;
247
+ bestMatch = projectConfig;
183
248
  }
184
- break;
249
+ }
250
+ }
251
+ if (bestMatch?.lastModelUsage) {
252
+ const usage = bestMatch.lastModelUsage;
253
+ const ids = Object.keys(usage);
254
+ if (ids.length > 0) {
255
+ let bestId = ids[ids.length - 1];
256
+ let bestTokens = -1;
257
+ for (const id of ids) {
258
+ const e = usage[id] || {};
259
+ const tokens = (e.inputTokens || 0) + (e.outputTokens || 0);
260
+ if (tokens > bestTokens) { bestTokens = tokens; bestId = id; }
261
+ }
262
+ return modelLabel(bestId);
185
263
  }
186
264
  }
187
265
  }
188
266
  } catch { /* ignore */ }
189
267
 
190
- // Fallback: settings.json model field
268
+ // TERTIARY: settings.json model field (configured default, not live session).
191
269
  const settings = getSettings();
192
- if (settings && settings.model) {
193
- const m = settings.model;
194
- if (m.includes('opus')) return 'Opus 4.6 (1M context)';
195
- if (m.includes('sonnet')) return 'Sonnet 4.6';
196
- if (m.includes('haiku')) return 'Haiku 4.5';
197
- }
198
- return 'Claude Code';
270
+ if (settings?.model) return modelLabel(settings.model);
271
+
272
+ // QUATERNARY: read ANTHROPIC_MODEL or CLAUDE_MODEL env var (set by the CLI at launch)
273
+ const envModel = process.env.ANTHROPIC_MODEL || process.env.CLAUDE_MODEL || process.env.MODEL;
274
+ if (envModel && envModel.startsWith('claude')) return modelLabel(envModel);
275
+
276
+ // QUINARY: current model from the model ID in the env injected by Claude Code itself
277
+ const claudeModel = process.env.CLAUDE_CODE_MODEL;
278
+ if (claudeModel) return modelLabel(claudeModel);
279
+
280
+ return 'Sonnet 4.6'; // known current default rather than the generic "Claude Code"
199
281
  }
200
282
 
201
283
  // Get learning stats from memory database (pure stat calls)
@@ -233,12 +315,12 @@ function getLearningStats() {
233
315
  }
234
316
 
235
317
  // progress from metrics files (pure file reads)
236
- function getDevProgress() {
318
+ function getv1Progress() {
237
319
  const learning = getLearningStats();
238
320
  const totalDomains = 5;
239
321
 
240
322
  const dddData = readJSON(path.join(CWD, '.monobrain', 'metrics', 'ddd-progress.json'));
241
- let dddProgress = dddData ? (dddData.progress || 0) : 0;
323
+ let dddProgress = dddData?.progress || 0;
242
324
  let domainsCompleted = Math.min(5, Math.floor(dddProgress / 20));
243
325
 
244
326
  if (dddProgress === 0 && learning.patterns > 0) {
@@ -263,6 +345,7 @@ function getSecurityStatus() {
263
345
  if (auditData) {
264
346
  const auditDate = auditData.lastAudit || auditData.lastScan;
265
347
  if (!auditDate) {
348
+ // No audit has ever run — show as pending, not stale
266
349
  return { status: 'PENDING', cvesFixed: 0, totalCves: 0 };
267
350
  }
268
351
  const auditAge = Date.now() - new Date(auditDate).getTime();
@@ -292,8 +375,31 @@ function getSecurityStatus() {
292
375
  // Swarm status (pure file reads, NO ps aux)
293
376
  function getSwarmStatus() {
294
377
  const staleThresholdMs = 5 * 60 * 1000;
378
+ const agentRegTtlMs = 30 * 60 * 1000; // registration files expire after 30 min
295
379
  const now = Date.now();
296
380
 
381
+ // PRIMARY: count live registration files written by SubagentStart hook
382
+ // Each file = one active sub-agent. Stale files (>30 min) are ignored.
383
+ const regDir = path.join(CWD, '.monobrain', 'agents', 'registrations');
384
+ if (fs.existsSync(regDir)) {
385
+ try {
386
+ const files = fs.readdirSync(regDir).filter(f => f.endsWith('.json'));
387
+ const liveCount = files.filter(f => {
388
+ try {
389
+ return (now - fs.statSync(path.join(regDir, f)).mtimeMs) < agentRegTtlMs;
390
+ } catch { return false; }
391
+ }).length;
392
+ if (liveCount > 0) {
393
+ return {
394
+ activeAgents: liveCount,
395
+ maxAgents: CONFIG.maxAgents,
396
+ coordinationActive: true,
397
+ };
398
+ }
399
+ } catch { /* fall through */ }
400
+ }
401
+
402
+ // SECONDARY: swarm-state.json written by MCP swarm_init — trust if fresh
297
403
  const swarmStatePath = path.join(CWD, '.monobrain', 'swarm', 'swarm-state.json');
298
404
  const swarmState = readJSON(swarmStatePath);
299
405
  if (swarmState) {
@@ -301,16 +407,17 @@ function getSwarmStatus() {
301
407
  const age = updatedAt ? now - new Date(updatedAt).getTime() : Infinity;
302
408
  if (age < staleThresholdMs) {
303
409
  return {
304
- activeAgents: (swarmState.agents && swarmState.agents.length) || swarmState.agentCount || 0,
410
+ activeAgents: swarmState.agents?.length || swarmState.agentCount || 0,
305
411
  maxAgents: swarmState.maxAgents || CONFIG.maxAgents,
306
412
  coordinationActive: true,
307
413
  };
308
414
  }
309
415
  }
310
416
 
417
+ // TERTIARY: swarm-activity.json refreshed by post-task hook
311
418
  const activityData = readJSON(path.join(CWD, '.monobrain', 'metrics', 'swarm-activity.json'));
312
- if (activityData && activityData.swarm) {
313
- const updatedAt = activityData.timestamp || (activityData.swarm && activityData.swarm.timestamp);
419
+ if (activityData?.swarm) {
420
+ const updatedAt = activityData.timestamp || activityData.swarm.timestamp;
314
421
  const age = updatedAt ? now - new Date(updatedAt).getTime() : Infinity;
315
422
  if (age < staleThresholdMs) {
316
423
  return {
@@ -335,9 +442,10 @@ function getSystemMetrics() {
335
442
  let intelligencePct = 0;
336
443
  let contextPct = 0;
337
444
 
338
- if (learningData && learningData.intelligence && learningData.intelligence.score !== undefined) {
445
+ if (learningData?.intelligence?.score !== undefined) {
339
446
  intelligencePct = Math.min(100, Math.floor(learningData.intelligence.score));
340
447
  } else {
448
+ // Use actual vector/entry counts — 2000 entries = 100%
341
449
  const fromPatterns = learning.patterns > 0 ? Math.min(100, Math.floor(learning.patterns / 20)) : 0;
342
450
  const fromVectors = agentdb.vectorCount > 0 ? Math.min(100, Math.floor(agentdb.vectorCount / 20)) : 0;
343
451
  intelligencePct = Math.max(fromPatterns, fromVectors);
@@ -356,7 +464,7 @@ function getSystemMetrics() {
356
464
  intelligencePct = Math.min(100, score);
357
465
  }
358
466
 
359
- if (learningData && learningData.sessions && learningData.sessions.total !== undefined) {
467
+ if (learningData?.sessions?.total !== undefined) {
360
468
  contextPct = Math.min(100, learningData.sessions.total * 5);
361
469
  } else {
362
470
  contextPct = Math.min(100, Math.floor(learning.sessions * 5));
@@ -365,7 +473,7 @@ function getSystemMetrics() {
365
473
  // Sub-agents from file metrics (no ps aux)
366
474
  let subAgents = 0;
367
475
  const activityData = readJSON(path.join(CWD, '.monobrain', 'metrics', 'swarm-activity.json'));
368
- if (activityData && activityData.processes && activityData.processes.estimated_agents) {
476
+ if (activityData?.processes?.estimated_agents) {
369
477
  subAgents = activityData.processes.estimated_agents;
370
478
  }
371
479
 
@@ -387,6 +495,7 @@ function getADRStatus() {
387
495
  const files = fs.readdirSync(adrPath).filter(f =>
388
496
  f.endsWith('.md') && (f.startsWith('ADR-') || f.startsWith('adr-') || /^\\d{4}-/.test(f))
389
497
  );
498
+ // Report actual count — don't guess compliance without reading files
390
499
  return { count: files.length, implemented: files.length, compliance: 0 };
391
500
  }
392
501
  } catch { /* ignore */ }
@@ -401,12 +510,12 @@ function getHooksStatus() {
401
510
  let total = 0;
402
511
  const settings = getSettings();
403
512
 
404
- if (settings && settings.hooks) {
513
+ if (settings?.hooks) {
405
514
  for (const category of Object.keys(settings.hooks)) {
406
515
  const matchers = settings.hooks[category];
407
516
  if (!Array.isArray(matchers)) continue;
408
517
  for (const matcher of matchers) {
409
- const hooks = matcher && matcher.hooks;
518
+ const hooks = matcher?.hooks;
410
519
  if (Array.isArray(hooks)) {
411
520
  total += hooks.length;
412
521
  enabled += hooks.length;
@@ -427,6 +536,33 @@ function getHooksStatus() {
427
536
  return { enabled, total };
428
537
  }
429
538
 
539
+ // Active agent — reads last routing result persisted by hook-handler
540
+ function getActiveAgent() {
541
+ const routeFile = path.join(CWD, '.monobrain', 'last-route.json');
542
+ try {
543
+ if (!fs.existsSync(routeFile)) return null;
544
+ const data = JSON.parse(fs.readFileSync(routeFile, 'utf-8'));
545
+ if (!data || !data.agent) return null;
546
+
547
+ // Stale after 30 minutes (session likely changed)
548
+ const age = Date.now() - new Date(data.updatedAt || 0).getTime();
549
+ if (age > 30 * 60 * 1000) return null;
550
+
551
+ // Prefer display name if set (from load-agent), else format the slug
552
+ const displayName = data.name || data.agent
553
+ .replace(/-/g, ' ')
554
+ .replace(/\\b\\w/g, c => c.toUpperCase());
555
+
556
+ return {
557
+ slug: data.agent,
558
+ name: displayName,
559
+ category: data.category || null,
560
+ confidence: data.confidence || 0,
561
+ activated: data.activated || false, // true = manually loaded extras agent
562
+ };
563
+ } catch { return null; }
564
+ }
565
+
430
566
  // AgentDB stats — count real entries, not file-size heuristics
431
567
  function getAgentDBStats() {
432
568
  let vectorCount = 0;
@@ -442,14 +578,15 @@ function getAgentDBStats() {
442
578
  try {
443
579
  const store = JSON.parse(fs.readFileSync(storePath, 'utf-8'));
444
580
  if (Array.isArray(store)) vectorCount += store.length;
445
- else if (store && store.entries) vectorCount += store.entries.length;
446
- } catch { /* fall back */ }
581
+ else if (store?.entries) vectorCount += store.entries.length;
582
+ } catch { /* fall back to size estimate */ }
447
583
  }
448
584
 
449
585
  // 2. Count entries from ranked-context.json
586
+ const rankedPath = path.join(CWD, '.monobrain', 'data', 'ranked-context.json');
450
587
  try {
451
- const ranked = readJSON(path.join(CWD, '.monobrain', 'data', 'ranked-context.json'));
452
- if (ranked && ranked.entries && ranked.entries.length > vectorCount) vectorCount = ranked.entries.length;
588
+ const ranked = readJSON(rankedPath);
589
+ if (ranked?.entries?.length > vectorCount) vectorCount = ranked.entries.length;
453
590
  } catch { /* ignore */ }
454
591
 
455
592
  // 3. Add DB file sizes
@@ -466,18 +603,25 @@ function getAgentDBStats() {
466
603
  }
467
604
  }
468
605
 
469
- // 4. Graph data size
470
- const graphStat = safeStat(path.join(CWD, 'data', 'memory.graph'));
606
+ // 4. Check for graph data
607
+ const graphPath = path.join(CWD, 'data', 'memory.graph');
608
+ const graphStat = safeStat(graphPath);
471
609
  if (graphStat) dbSizeKB += graphStat.size / 1024;
472
610
 
473
- // 5. HNSW index or memory package
611
+ // 5. HNSW index
474
612
  const hnswPaths = [
475
613
  path.join(CWD, '.swarm', 'hnsw.index'),
476
614
  path.join(CWD, '.monobrain', 'hnsw.index'),
477
615
  ];
478
616
  for (const p of hnswPaths) {
479
- if (safeStat(p)) { hasHnsw = true; break; }
617
+ const stat = safeStat(p);
618
+ if (stat) {
619
+ hasHnsw = true;
620
+ break;
621
+ }
480
622
  }
623
+
624
+ // HNSW is available if memory package is present
481
625
  if (!hasHnsw) {
482
626
  const memPkgPaths = [
483
627
  path.join(CWD, 'packages', '@monobrain', 'memory', 'dist'),
@@ -495,8 +639,7 @@ function getAgentDBStats() {
495
639
  function getTestStats() {
496
640
  let testFiles = 0;
497
641
 
498
- function countTestFiles(dir, depth) {
499
- if (depth === undefined) depth = 0;
642
+ function countTestFiles(dir, depth = 0) {
500
643
  if (depth > 6) return;
501
644
  try {
502
645
  if (!fs.existsSync(dir)) return;
@@ -514,11 +657,12 @@ function getTestStats() {
514
657
  } catch { /* ignore */ }
515
658
  }
516
659
 
517
- var testDirNames = ['tests', 'test', '__tests__', 'src', 'v1'];
518
- for (var i = 0; i < testDirNames.length; i++) {
519
- countTestFiles(path.join(CWD, testDirNames[i]));
660
+ // Scan all source directories
661
+ for (const d of ['tests', 'test', '__tests__', 'src', 'v1']) {
662
+ countTestFiles(path.join(CWD, d));
520
663
  }
521
664
 
665
+ // Estimate ~4 test cases per file (avoids reading every file)
522
666
  return { testFiles, testCases: testFiles * 4 };
523
667
  }
524
668
 
@@ -527,7 +671,7 @@ function getIntegrationStatus() {
527
671
  const mcpServers = { total: 0, enabled: 0 };
528
672
  const settings = getSettings();
529
673
 
530
- if (settings && settings.mcpServers && typeof settings.mcpServers === 'object') {
674
+ if (settings?.mcpServers && typeof settings.mcpServers === 'object') {
531
675
  const servers = Object.keys(settings.mcpServers);
532
676
  mcpServers.total = servers.length;
533
677
  mcpServers.enabled = settings.enabledMcpjsonServers
@@ -535,10 +679,11 @@ function getIntegrationStatus() {
535
679
  : servers.length;
536
680
  }
537
681
 
682
+ // Fallback: .mcp.json
538
683
  if (mcpServers.total === 0) {
539
684
  const mcpConfig = readJSON(path.join(CWD, '.mcp.json'))
540
685
  || readJSON(path.join(os.homedir(), '.claude', 'mcp.json'));
541
- if (mcpConfig && mcpConfig.mcpServers) {
686
+ if (mcpConfig?.mcpServers) {
542
687
  const s = Object.keys(mcpConfig.mcpServers);
543
688
  mcpServers.total = s.length;
544
689
  mcpServers.enabled = s.length;
@@ -554,237 +699,393 @@ function getIntegrationStatus() {
554
699
 
555
700
  // Session stats (pure file reads)
556
701
  function getSessionStats() {
557
- var sessionPaths = ['.monobrain/session.json', '.claude/session.json'];
558
- for (var i = 0; i < sessionPaths.length; i++) {
559
- const data = readJSON(path.join(CWD, sessionPaths[i]));
560
- if (data && data.startTime) {
702
+ for (const p of ['.monobrain/session.json', '.claude/session.json']) {
703
+ const data = readJSON(path.join(CWD, p));
704
+ if (data?.startTime) {
561
705
  const diffMs = Date.now() - new Date(data.startTime).getTime();
562
706
  const mins = Math.floor(diffMs / 60000);
563
- const duration = mins < 60 ? mins + 'm' : Math.floor(mins / 60) + 'h' + (mins % 60) + 'm';
564
- return { duration: duration };
707
+ const duration = mins < 60 ? \`\${mins}m\` : \`\${Math.floor(mins / 60)}h\${mins % 60}m\`;
708
+ return { duration };
565
709
  }
566
710
  }
567
711
  return { duration: '' };
568
712
  }
569
713
 
570
- // ─── Rendering ──────────────────────────────────────────────────
714
+ // ─── Extended 256-color palette ─────────────────────────────────
715
+ const x = {
716
+ // Backgrounds (used sparingly for labels)
717
+ bgPurple: '\\x1b[48;5;55m',
718
+ bgTeal: '\\x1b[48;5;23m',
719
+ bgReset: '\\x1b[49m',
720
+ // Foregrounds
721
+ purple: '\\x1b[38;5;141m', // soft lavender-purple (brand)
722
+ violet: '\\x1b[38;5;99m', // deeper purple
723
+ teal: '\\x1b[38;5;51m', // bright teal
724
+ mint: '\\x1b[38;5;120m', // soft green
725
+ gold: '\\x1b[38;5;220m', // warm gold
726
+ orange: '\\x1b[38;5;208m', // alert orange
727
+ coral: '\\x1b[38;5;203m', // error red-pink
728
+ sky: '\\x1b[38;5;117m', // soft blue
729
+ rose: '\\x1b[38;5;218m', // warm pink
730
+ slate: '\\x1b[38;5;245m', // neutral grey
731
+ white: '\\x1b[38;5;255m', // bright white
732
+ green: '\\x1b[38;5;82m', // vivid green
733
+ red: '\\x1b[38;5;196m', // vivid red
734
+ yellow: '\\x1b[38;5;226m', // vivid yellow
735
+ // Shared
736
+ bold: '\\x1b[1m',
737
+ dim: '\\x1b[2m',
738
+ reset: '\\x1b[0m',
739
+ };
740
+
741
+ // ── Helpers ──────────────────────────────────────────────────────
742
+
743
+ // Block progress bar: ▰▰▰▱▱ (5 blocks)
744
+ function blockBar(current, total, width = 5) {
745
+ const filled = Math.min(width, Math.round((current / Math.max(total, 1)) * width));
746
+ return '\\u25B0'.repeat(filled) + \`\${x.slate}\\u25B1\${x.reset}\`.repeat(width - filled);
747
+ }
748
+
749
+ // Health dot: ● colored by status
750
+ function dot(ok) {
751
+ if (ok === 'good') return \`\${x.green}●\${x.reset}\`;
752
+ if (ok === 'warn') return \`\${x.gold}●\${x.reset}\`;
753
+ if (ok === 'error') return \`\${x.coral}●\${x.reset}\`;
754
+ return \`\${x.slate}●\${x.reset}\`; // 'none'
755
+ }
756
+
757
+ // Pill badge: [ LABEL ] with background
758
+ function badge(label, color) {
759
+ return \`\${color}[\${label}]\${x.reset}\`;
760
+ }
761
+
762
+ // Divider character
763
+ const DIV = \`\${x.slate}│\${x.reset}\`;
764
+ const SEP = \`\${x.slate}──────────────────────────────────────────────────────\${x.reset}\`;
765
+
766
+ // Pct → color
767
+ function pctColor(pct) {
768
+ if (pct >= 75) return x.green;
769
+ if (pct >= 40) return x.gold;
770
+ if (pct > 0) return x.orange;
771
+ return x.slate;
772
+ }
773
+
774
+ // Security status → label + color
775
+ function secBadge(status) {
776
+ if (status === 'CLEAN') return { label: '✔ CLEAN', col: x.green };
777
+ if (status === 'STALE') return { label: '⟳ STALE', col: x.gold };
778
+ if (status === 'IN_PROGRESS') return { label: '⟳ RUNNING', col: x.sky };
779
+ if (status === 'SCANNED') return { label: '✔ SCANNED', col: x.mint };
780
+ if (status === 'PENDING') return { label: '⏸ PENDING', col: x.gold };
781
+ return { label: '✖ NONE', col: x.slate };
782
+ }
571
783
 
572
- function progressBar(current, total) {
573
- const width = 5;
574
- const filled = Math.round((current / total) * width);
575
- return '[' + '\\u25CF'.repeat(filled) + '\\u25CB'.repeat(width - filled) + ']';
784
+ // ── Knowledge & trigger stats (Tasks 28 + 32) ────────────────────
785
+ function getKnowledgeStats() {
786
+ const chunksPath = path.join(CWD, '.monobrain', 'knowledge', 'chunks.jsonl');
787
+ const skillsPath = path.join(CWD, '.monobrain', 'skills.jsonl');
788
+ let chunks = 0, skills = 0;
789
+ try {
790
+ if (fs.existsSync(chunksPath)) {
791
+ chunks = fs.readFileSync(chunksPath, 'utf-8').split('\\n').filter(Boolean).length;
792
+ }
793
+ } catch { /* ignore */ }
794
+ try {
795
+ if (fs.existsSync(skillsPath)) {
796
+ skills = fs.readFileSync(skillsPath, 'utf-8').split('\\n').filter(Boolean).length;
797
+ }
798
+ } catch { /* ignore */ }
799
+ return { chunks, skills };
576
800
  }
577
801
 
802
+ function getTriggerStats() {
803
+ const indexPath = path.join(CWD, '.monobrain', 'trigger-index.json');
804
+ try {
805
+ if (!fs.existsSync(indexPath)) return { triggers: 0, agents: 0 };
806
+ const raw = JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
807
+ const idx = raw.index || raw;
808
+ const triggers = Object.keys(idx).length;
809
+ const agents = Object.values(idx).flat().length;
810
+ return { triggers, agents };
811
+ } catch { return { triggers: 0, agents: 0 }; }
812
+ }
813
+
814
+ function getSIBudget() {
815
+ const SI_LIMIT = 1500;
816
+ const siPath = path.join(CWD, '.agents', 'shared_instructions.md');
817
+ try {
818
+ if (!fs.existsSync(siPath)) return null;
819
+ const len = fs.readFileSync(siPath, 'utf-8').length;
820
+ return { len, pct: Math.round((len / SI_LIMIT) * 100), limit: SI_LIMIT };
821
+ } catch { return null; }
822
+ }
823
+
824
+ // ── Single-line statusline (compact) ─────────────────────────────
578
825
  function generateStatusline() {
579
- const git = getGitInfo();
580
- // Prefer model name from Claude Code stdin data, fallback to file-based detection
581
- const modelName = getModelFromStdin() || getModelName();
582
- const ctxInfo = getContextFromStdin();
583
- const costInfo = getCostFromStdin();
584
- const progress = getDevProgress();
585
- const security = getSecurityStatus();
586
- const swarm = getSwarmStatus();
587
- const system = getSystemMetrics();
588
- const adrs = getADRStatus();
589
- const hooks = getHooksStatus();
590
- const agentdb = getAgentDBStats();
591
- const tests = getTestStats();
592
- const session = getSessionStats();
826
+ const git = getGitInfo();
827
+ const swarm = getSwarmStatus();
828
+ const system = getSystemMetrics();
829
+ const hooks = getHooksStatus();
830
+ const knowledge = getKnowledgeStats();
831
+ const triggers = getTriggerStats();
832
+ const parts = [];
833
+
834
+ // Brand + swarm dot
835
+ const swarmDot = swarm.coordinationActive ? \`\${x.green}●\${x.reset}\` : \`\${x.slate}○\${x.reset}\`;
836
+ parts.push(\`\${x.bold}\${x.purple}▊ Monobrain\${x.reset} \${swarmDot}\`);
837
+
838
+ // Git branch + changes (compact)
839
+ if (git.gitBranch) {
840
+ let b = \`\${x.sky}⎇ \${x.bold}\${git.gitBranch}\${x.reset}\`;
841
+ if (git.staged > 0) b += \` \${x.green}+\${git.staged}\${x.reset}\`;
842
+ if (git.modified > 0) b += \` \${x.gold}~\${git.modified}\${x.reset}\`;
843
+ if (git.ahead > 0) b += \` \${x.green}↑\${git.ahead}\${x.reset}\`;
844
+ if (git.behind > 0) b += \` \${x.coral}↓\${git.behind}\${x.reset}\`;
845
+ parts.push(b);
846
+ }
847
+
848
+ // Model
849
+ parts.push(\`\${x.violet}\${getModelName()}\${x.reset}\`);
850
+
851
+ // Active agent
852
+ const activeAgent = getActiveAgent();
853
+ if (activeAgent) {
854
+ const col = activeAgent.activated ? x.green : x.sky;
855
+ const icon = activeAgent.activated ? '●' : '→';
856
+ parts.push(\`\${col}\${icon} \${x.bold}\${activeAgent.name}\${x.reset}\`);
857
+ }
858
+
859
+ // Intelligence
860
+ const ic = pctColor(system.intelligencePct);
861
+ parts.push(\`\${ic}💡 \${system.intelligencePct}%\${x.reset}\`);
862
+
863
+ // Knowledge chunks (Task 28) — show when populated
864
+ if (knowledge.chunks > 0) {
865
+ parts.push(\`\${x.teal}📚 \${knowledge.chunks}k\${x.reset}\`);
866
+ }
867
+
868
+ // Triggers (Task 32) — show when populated
869
+ if (triggers.triggers > 0) {
870
+ parts.push(\`\${x.mint}🎯 \${triggers.triggers}t\${x.reset}\`);
871
+ }
872
+
873
+ // Swarm agents (only when active)
874
+ if (swarm.activeAgents > 0) {
875
+ parts.push(\`\${x.gold}🐝 \${swarm.activeAgents}/\${swarm.maxAgents}\${x.reset}\`);
876
+ }
877
+
878
+ // Hooks
879
+ if (hooks.enabled > 0) {
880
+ parts.push(\`\${x.mint}⚡ \${hooks.enabled}h\${x.reset}\`);
881
+ }
882
+
883
+ return parts.join(\` \${DIV} \`);
884
+ }
885
+
886
+ // ── Multi-line dashboard (full mode) ─────────────────────────────
887
+ function generateDashboard() {
888
+ const git = getGitInfo();
889
+ const modelName = getModelName();
890
+ const progress = getv1Progress();
891
+ const security = getSecurityStatus();
892
+ const swarm = getSwarmStatus();
893
+ const system = getSystemMetrics();
894
+ const adrs = getADRStatus();
895
+ const hooks = getHooksStatus();
896
+ const agentdb = getAgentDBStats();
897
+ const tests = getTestStats();
898
+ const session = getSessionStats();
593
899
  const integration = getIntegrationStatus();
594
- const lines = [];
900
+ const knowledge = getKnowledgeStats();
901
+ const triggers = getTriggerStats();
902
+ const si = getSIBudget();
903
+ const sec = secBadge(security.status);
904
+ const activeAgent = getActiveAgent();
905
+ const lines = [];
906
+
907
+ // ── Header: brand + git + model + session ────────────────────
908
+ const swarmDot = swarm.coordinationActive ? \`\${x.green}● LIVE\${x.reset}\` : \`\${x.slate}○ IDLE\${x.reset}\`;
909
+ let hdr = \`\${x.bold}\${x.purple}▊ Monobrain \${VERSION}\${x.reset} \${swarmDot} \${x.teal}\${x.bold}\${git.name}\${x.reset}\`;
595
910
 
596
- // Header
597
- let header = c.bold + c.brightPurple + '\\u258A Monobrain ' + c.reset;
598
- header += (swarm.coordinationActive ? c.brightCyan : c.dim) + '\\u25CF ' + c.brightCyan + git.name + c.reset;
599
911
  if (git.gitBranch) {
600
- header += ' ' + c.dim + '\\u2502' + c.reset + ' ' + c.brightBlue + '\\u23C7 ' + git.gitBranch + c.reset;
601
- const changes = git.modified + git.staged + git.untracked;
602
- if (changes > 0) {
603
- let ind = '';
604
- if (git.staged > 0) ind += c.brightGreen + '+' + git.staged + c.reset;
605
- if (git.modified > 0) ind += c.brightYellow + '~' + git.modified + c.reset;
606
- if (git.untracked > 0) ind += c.dim + '?' + git.untracked + c.reset;
607
- header += ' ' + ind;
608
- }
609
- if (git.ahead > 0) header += ' ' + c.brightGreen + '\\u2191' + git.ahead + c.reset;
610
- if (git.behind > 0) header += ' ' + c.brightRed + '\\u2193' + git.behind + c.reset;
611
- }
612
- header += ' ' + c.dim + '\\u2502' + c.reset + ' ' + c.purple + modelName + c.reset;
613
- // Show session duration from Claude Code stdin if available, else from local files
614
- const duration = costInfo ? costInfo.duration : session.duration;
615
- if (duration) header += ' ' + c.dim + '\\u2502' + c.reset + ' ' + c.cyan + '\\u23F1 ' + duration + c.reset;
616
- // Show context usage from Claude Code stdin if available
617
- if (ctxInfo && ctxInfo.usedPct > 0) {
618
- const ctxColor = ctxInfo.usedPct >= 90 ? c.brightRed : ctxInfo.usedPct >= 70 ? c.brightYellow : c.brightGreen;
619
- header += ' ' + c.dim + '\\u2502' + c.reset + ' ' + ctxColor + '\\u25CF ' + ctxInfo.usedPct + '% ctx' + c.reset;
620
- }
621
- // Show cost from Claude Code stdin if available
622
- if (costInfo && costInfo.costUsd > 0) {
623
- header += ' ' + c.dim + '\\u2502' + c.reset + ' ' + c.brightYellow + '$' + costInfo.costUsd.toFixed(2) + c.reset;
624
- }
625
- lines.push(header);
626
-
627
- // Separator
628
- lines.push(c.dim + '\\u2500'.repeat(53) + c.reset);
629
-
630
- // Line 1: DDD Domains
631
- const domainsColor = progress.domainsCompleted >= 3 ? c.brightGreen : progress.domainsCompleted > 0 ? c.yellow : c.red;
632
- let perfIndicator;
633
- if (agentdb.hasHnsw && agentdb.vectorCount > 0) {
634
- const speedup = agentdb.vectorCount > 10000 ? '12500x' : agentdb.vectorCount > 1000 ? '150x' : '10x';
635
- perfIndicator = c.brightGreen + '\\u26A1 HNSW ' + speedup + c.reset;
636
- } else if (progress.patternsLearned > 0) {
637
- const pk = progress.patternsLearned >= 1000 ? (progress.patternsLearned / 1000).toFixed(1) + 'k' : String(progress.patternsLearned);
638
- perfIndicator = c.brightYellow + '\\uD83D\\uDCDA ' + pk + ' patterns' + c.reset;
639
- } else {
640
- perfIndicator = c.dim + '\\u26A1 target: 150x-12500x' + c.reset;
912
+ hdr += \` \${DIV} \${x.sky}⎇ \${x.bold}\${git.gitBranch}\${x.reset}\`;
913
+ if (git.staged > 0) hdr += \` \${x.green}+\${git.staged}\${x.reset}\`;
914
+ if (git.modified > 0) hdr += \` \${x.gold}~\${git.modified} mod\${x.reset}\`;
915
+ if (git.untracked > 0) hdr += \` \${x.slate}?\${git.untracked}\${x.reset}\`;
916
+ if (git.ahead > 0) hdr += \` \${x.green}↑\${git.ahead}\${x.reset}\`;
917
+ if (git.behind > 0) hdr += \` \${x.coral}↓\${git.behind}\${x.reset}\`;
641
918
  }
919
+
920
+ hdr += \` \${DIV} 🤖 \${x.violet}\${x.bold}\${modelName}\${x.reset}\`;
921
+ if (session.duration) hdr += \` \${x.dim}⏱ \${session.duration}\${x.reset}\`;
922
+
923
+ lines.push(hdr);
924
+ lines.push(SEP);
925
+
926
+ // ── Row 1: Intelligence & Learning ───────────────────────────
927
+ const intellCol = pctColor(system.intelligencePct);
928
+ const intellBar = blockBar(system.intelligencePct, 100, 6);
929
+
930
+ // Knowledge (Task 28)
931
+ const knowStr = knowledge.chunks > 0
932
+ ? \`\${x.teal}📚 \${x.bold}\${knowledge.chunks}\${x.reset}\${x.slate} chunks\${x.reset}\`
933
+ : \`\${x.slate}📚 no chunks\${x.reset}\`;
934
+
935
+ // Skills (Task 45)
936
+ const skillStr = knowledge.skills > 0
937
+ ? \` \${x.mint}✦ \${knowledge.skills} skills\${x.reset}\`
938
+ : '';
939
+
940
+ // Patterns
941
+ const patStr = progress.patternsLearned > 0
942
+ ? \`\${x.gold}\${progress.patternsLearned >= 1000 ? (progress.patternsLearned / 1000).toFixed(1) + 'k' : progress.patternsLearned} patterns\${x.reset}\`
943
+ : \`\${x.slate}0 patterns\${x.reset}\`;
944
+
642
945
  lines.push(
643
- c.brightCyan + '\\uD83C\\uDFD7\\uFE0F DDD Domains' + c.reset + ' ' + progressBar(progress.domainsCompleted, progress.totalDomains) + ' ' +
644
- domainsColor + progress.domainsCompleted + c.reset + '/' + c.brightWhite + progress.totalDomains + c.reset + ' ' + perfIndicator
946
+ \`\${x.purple}💡 INTEL\${x.reset} \` +
947
+ \`\${intellCol}\${intellBar} \${x.bold}\${system.intelligencePct}%\${x.reset} \${DIV} \` +
948
+ \`\${knowStr}\${skillStr} \${DIV} \` +
949
+ patStr
645
950
  );
646
-
647
- // Line 2: Swarm + Hooks + CVE + Memory + Intelligence
648
- const swarmInd = swarm.coordinationActive ? c.brightGreen + '\\u25C9' + c.reset : c.dim + '\\u25CB' + c.reset;
649
- const agentsColor = swarm.activeAgents > 0 ? c.brightGreen : c.red;
650
- const secIcon = security.status === 'CLEAN' ? '\\uD83D\\uDFE2' : (security.status === 'IN_PROGRESS' || security.status === 'STALE') ? '\\uD83D\\uDFE1' : (security.status === 'NONE' ? '\\u26AA' : '\\uD83D\\uDD34');
651
- const secColor = security.status === 'CLEAN' ? c.brightGreen : (security.status === 'IN_PROGRESS' || security.status === 'STALE') ? c.brightYellow : (security.status === 'NONE' ? c.dim : c.brightRed);
652
- const hooksColor = hooks.enabled > 0 ? c.brightGreen : c.dim;
653
- const intellColor = system.intelligencePct >= 80 ? c.brightGreen : system.intelligencePct >= 40 ? c.brightYellow : c.dim;
951
+ lines.push(SEP);
952
+
953
+ // ── Row 2: Agents & Triggers ──────────────────────────────────
954
+ const agentCol = swarm.activeAgents > 0 ? x.green : x.slate;
955
+ const hookCol = hooks.enabled > 0 ? x.mint : x.slate;
956
+
957
+ // Triggers (Task 32)
958
+ const trigStr = triggers.triggers > 0
959
+ ? \`\${x.mint}🎯 \${x.bold}\${triggers.triggers}\${x.reset}\${x.slate} triggers · \${triggers.agents} agents\${x.reset}\`
960
+ : \`\${x.slate}🎯 no triggers\${x.reset}\`;
961
+
962
+ // Active agent badge
963
+ let agentBadge;
964
+ if (activeAgent) {
965
+ const col = activeAgent.activated ? x.green : x.sky;
966
+ const mark = activeAgent.activated ? '● ACTIVE' : '→ ROUTED';
967
+ const conf = activeAgent.activated ? '' : \` \${x.slate}\${(activeAgent.confidence * 100).toFixed(0)}%\${x.reset}\`;
968
+ const cat = activeAgent.category ? \` \${x.slate}[\${activeAgent.category}]\${x.reset}\` : '';
969
+ agentBadge = \`\${col}\${x.bold}\${mark}\${x.reset} \${col}👤 \${x.bold}\${activeAgent.name}\${x.reset}\${cat}\${conf}\`;
970
+ } else {
971
+ agentBadge = \`\${x.slate}👤 no agent routed\${x.reset}\`;
972
+ }
654
973
 
655
974
  lines.push(
656
- c.brightYellow + '\\uD83E\\uDD16 Swarm' + c.reset + ' ' + swarmInd + ' [' + agentsColor + String(swarm.activeAgents).padStart(2) + c.reset + '/' + c.brightWhite + swarm.maxAgents + c.reset + '] ' +
657
- c.brightPurple + '\\uD83D\\uDC65 ' + system.subAgents + c.reset + ' ' +
658
- c.brightBlue + '\\uD83E\\uDE9D ' + hooksColor + hooks.enabled + c.reset + '/' + c.brightWhite + hooks.total + c.reset + ' ' +
659
- secIcon + ' ' + secColor + 'CVE ' + security.cvesFixed + c.reset + '/' + c.brightWhite + security.totalCves + c.reset + ' ' +
660
- c.brightCyan + '\\uD83D\\uDCBE ' + system.memoryMB + 'MB' + c.reset + ' ' +
661
- intellColor + '\\uD83E\\uDDE0 ' + String(system.intelligencePct).padStart(3) + '%' + c.reset
975
+ \`\${x.gold}🐝 SWARM\${x.reset} \` +
976
+ \`\${agentCol}\${x.bold}\${swarm.activeAgents}\${x.reset}\${x.slate}/\${x.reset}\${x.white}\${swarm.maxAgents}\${x.reset} agents \` +
977
+ \`\${hookCol}⚡ \${hooks.enabled}/\${hooks.total} hooks\${x.reset} \${DIV} \` +
978
+ \`\${trigStr} \${DIV} \` +
979
+ agentBadge
662
980
  );
981
+ lines.push(SEP);
663
982
 
664
- // Line 3: Architecture
665
- const dddColor = progress.dddProgress >= 50 ? c.brightGreen : progress.dddProgress > 0 ? c.yellow : c.red;
666
- const adrColor = adrs.count > 0 ? (adrs.implemented === adrs.count ? c.brightGreen : c.yellow) : c.dim;
667
- const adrDisplay = adrs.compliance > 0 ? adrColor + '\\u25CF' + adrs.compliance + '%' + c.reset : adrColor + '\\u25CF' + adrs.implemented + '/' + adrs.count + c.reset;
983
+ // ── Row 3: Architecture & Security ───────────────────────────
984
+ const adrCol = adrs.count > 0
985
+ ? (adrs.implemented >= adrs.count ? x.green : x.gold)
986
+ : x.slate;
987
+ const adrStr = adrs.count > 0
988
+ ? \`\${adrCol}\${x.bold}\${adrs.implemented}\${x.reset}\${x.slate}/\${x.reset}\${x.white}\${adrs.count}\${x.reset} ADRs\`
989
+ : \`\${x.slate}no ADRs\${x.reset}\`;
990
+
991
+ const dddCol = pctColor(progress.dddProgress);
992
+ const dddBar = blockBar(progress.dddProgress, 100, 5);
993
+
994
+ const cveStatus = security.totalCves === 0
995
+ ? (security.status === 'NONE' ? \`\${x.slate}not scanned\${x.reset}\` : \`\${x.green}✔ clean\${x.reset}\`)
996
+ : \`\${x.coral}\${security.cvesFixed}/\${security.totalCves} fixed\${x.reset}\`;
668
997
 
669
998
  lines.push(
670
- c.brightPurple + '\\uD83D\\uDD27 Architecture' + c.reset + ' ' +
671
- c.cyan + 'ADRs' + c.reset + ' ' + adrDisplay + ' ' + c.dim + '\\u2502' + c.reset + ' ' +
672
- c.cyan + 'DDD' + c.reset + ' ' + dddColor + '\\u25CF' + String(progress.dddProgress).padStart(3) + '%' + c.reset + ' ' + c.dim + '\\u2502' + c.reset + ' ' +
673
- c.cyan + 'Security' + c.reset + ' ' + secColor + '\\u25CF' + security.status + c.reset
999
+ \`\${x.purple}🧩 ARCH\${x.reset} \` +
1000
+ \`\${adrStr} \${DIV} \` +
1001
+ \`DDD \${dddBar} \${dddCol}\${x.bold}\${progress.dddProgress}%\${x.reset} \${DIV} \` +
1002
+ \`🛡️ \${sec.col}\${sec.label}\${x.reset} \${DIV} \` +
1003
+ \`CVE \${cveStatus}\`
674
1004
  );
1005
+ lines.push(SEP);
675
1006
 
676
- // Line 4: AgentDB, Tests, Integration
677
- const hnswInd = agentdb.hasHnsw ? c.brightGreen + '\\u26A1' + c.reset : '';
678
- const sizeDisp = agentdb.dbSizeKB >= 1024 ? (agentdb.dbSizeKB / 1024).toFixed(1) + 'MB' : agentdb.dbSizeKB + 'KB';
679
- const vectorColor = agentdb.vectorCount > 0 ? c.brightGreen : c.dim;
680
- const testColor = tests.testFiles > 0 ? c.brightGreen : c.dim;
1007
+ // ── Row 4: Memory & Tests ─────────────────────────────────────
1008
+ const vecCol = agentdb.vectorCount > 0 ? x.green : x.slate;
1009
+ const hnswTag = agentdb.hasHnsw && agentdb.vectorCount > 0 ? \` \${x.green}⚡ HNSW\${x.reset}\` : '';
1010
+ const sizeDisp = agentdb.dbSizeKB >= 1024
1011
+ ? \`\${(agentdb.dbSizeKB / 1024).toFixed(1)} MB\` : \`\${agentdb.dbSizeKB} KB\`;
1012
+ const testCol = tests.testFiles > 0 ? x.green : x.slate;
1013
+ const memCol = system.memoryMB > 200 ? x.orange : x.sky;
681
1014
 
682
- let integStr = '';
1015
+ const chips = [];
683
1016
  if (integration.mcpServers.total > 0) {
684
- const mcpCol = integration.mcpServers.enabled === integration.mcpServers.total ? c.brightGreen :
685
- integration.mcpServers.enabled > 0 ? c.brightYellow : c.red;
686
- integStr += c.cyan + 'MCP' + c.reset + ' ' + mcpCol + '\\u25CF' + integration.mcpServers.enabled + '/' + integration.mcpServers.total + c.reset;
1017
+ const mc = integration.mcpServers.enabled === integration.mcpServers.total ? x.green
1018
+ : integration.mcpServers.enabled > 0 ? x.gold : x.coral;
1019
+ chips.push(\`\${mc}MCP \${integration.mcpServers.enabled}/\${integration.mcpServers.total}\${x.reset}\`);
687
1020
  }
688
- if (integration.hasDatabase) integStr += (integStr ? ' ' : '') + c.brightGreen + '\\u25C6' + c.reset + 'DB';
689
- if (integration.hasApi) integStr += (integStr ? ' ' : '') + c.brightGreen + '\\u25C6' + c.reset + 'API';
690
- if (!integStr) integStr = c.dim + '\\u25CF none' + c.reset;
1021
+ if (integration.hasDatabase) chips.push(\`\${x.green}DB ✔\${x.reset}\`);
1022
+ if (integration.hasApi) chips.push(\`\${x.green}API ✔\${x.reset}\`);
1023
+ const integStr = chips.length ? chips.join(' ') : \`\${x.slate}none\${x.reset}\`;
691
1024
 
692
1025
  lines.push(
693
- c.brightCyan + '\\uD83D\\uDCCA AgentDB' + c.reset + ' ' +
694
- c.cyan + 'Vectors' + c.reset + ' ' + vectorColor + '\\u25CF' + agentdb.vectorCount + hnswInd + c.reset + ' ' + c.dim + '\\u2502' + c.reset + ' ' +
695
- c.cyan + 'Size' + c.reset + ' ' + c.brightWhite + sizeDisp + c.reset + ' ' + c.dim + '\\u2502' + c.reset + ' ' +
696
- c.cyan + 'Tests' + c.reset + ' ' + testColor + '\\u25CF' + tests.testFiles + c.reset + ' ' + c.dim + '(~' + tests.testCases + ' cases)' + c.reset + ' ' + c.dim + '\\u2502' + c.reset + ' ' +
1026
+ \`\${x.teal}🗄️ MEMORY\${x.reset} \` +
1027
+ \`\${vecCol}\${x.bold}\${agentdb.vectorCount}\${x.reset}\${x.slate} vectors\${x.reset}\${hnswTag} \${DIV} \` +
1028
+ \`\${x.white}\${sizeDisp}\${x.reset} \${DIV} \` +
1029
+ \`\${testCol}🧪 \${tests.testFiles} test files\${x.reset} \${DIV} \` +
697
1030
  integStr
698
1031
  );
1032
+ lines.push(SEP);
1033
+
1034
+ // ── Row 5: Context budget ─────────────────────────────────────
1035
+ // SI budget (Task 23 monitor)
1036
+ let siStr;
1037
+ if (si) {
1038
+ const siCol = si.pct > 100 ? x.coral : si.pct > 80 ? x.gold : x.green;
1039
+ siStr = \`\${siCol}📄 SI \${si.pct}% budget\${x.reset} \${x.dim}(\${si.len}/\${si.limit} chars)\${x.reset}\`;
1040
+ } else {
1041
+ siStr = \`\${x.slate}📄 no shared instructions\${x.reset}\`;
1042
+ }
1043
+
1044
+ // Domains
1045
+ const domCol = progress.domainsCompleted >= 4 ? x.green
1046
+ : progress.domainsCompleted >= 2 ? x.gold
1047
+ : progress.domainsCompleted >= 1 ? x.orange
1048
+ : x.slate;
1049
+ const domBar = blockBar(progress.domainsCompleted, progress.totalDomains);
1050
+
1051
+ lines.push(
1052
+ \`\${x.slate}📋 CONTEXT\${x.reset} \` +
1053
+ \`\${siStr} \${DIV} \` +
1054
+ \`\${x.teal}🏗 \${domBar} \${domCol}\${x.bold}\${progress.domainsCompleted}\${x.reset}\${x.slate}/\${x.reset}\${x.white}\${progress.totalDomains}\${x.reset} domains \${DIV} \` +
1055
+ \`\${x.dim}💾 \${system.memoryMB} MB RAM\${x.reset}\`
1056
+ );
699
1057
 
700
1058
  return lines.join('\\n');
701
1059
  }
702
1060
 
703
- // JSON output
1061
+ // ── JSON output ──────────────────────────────────────────────────
704
1062
  function generateJSON() {
705
1063
  const git = getGitInfo();
706
1064
  return {
707
- user: { name: git.name, gitBranch: git.gitBranch, modelName: getModelName() },
708
- devProgress: getDevProgress(),
709
- security: getSecurityStatus(),
710
- swarm: getSwarmStatus(),
711
- system: getSystemMetrics(),
712
- adrs: getADRStatus(),
713
- hooks: getHooksStatus(),
714
- agentdb: getAgentDBStats(),
715
- tests: getTestStats(),
716
- git: { modified: git.modified, untracked: git.untracked, staged: git.staged, ahead: git.ahead, behind: git.behind },
1065
+ user: { name: git.name, gitBranch: git.gitBranch, modelName: getModelName() },
1066
+ domains: getv1Progress(),
1067
+ security: getSecurityStatus(),
1068
+ swarm: getSwarmStatus(),
1069
+ system: getSystemMetrics(),
1070
+ adrs: getADRStatus(),
1071
+ hooks: getHooksStatus(),
1072
+ agentdb: getAgentDBStats(),
1073
+ tests: getTestStats(),
1074
+ git: { modified: git.modified, untracked: git.untracked, staged: git.staged, ahead: git.ahead, behind: git.behind },
717
1075
  lastUpdated: new Date().toISOString(),
718
1076
  };
719
1077
  }
720
1078
 
721
- // ─── Stdin reader (Claude Code pipes session JSON) ──────────────
1079
+ // ─── Mode state file ─────────────────────────────────────────────
1080
+ const MODE_FILE = path.join(CWD, '.monobrain', 'statusline-mode.txt');
722
1081
 
723
- // Claude Code sends session JSON via stdin (model, context, cost, etc.)
724
- // Read it synchronously so the script works both:
725
- // 1. When invoked by Claude Code (stdin has JSON)
726
- // 2. When invoked manually from terminal (stdin is empty/tty)
727
- let _stdinData = null;
728
- function getStdinData() {
729
- if (_stdinData !== undefined && _stdinData !== null) return _stdinData;
1082
+ function readMode() {
730
1083
  try {
731
- // Check if stdin is a TTY (manual run) — skip reading
732
- if (process.stdin.isTTY) { _stdinData = null; return null; }
733
- // Read stdin synchronously via fd 0
734
- const chunks = [];
735
- const buf = Buffer.alloc(4096);
736
- let bytesRead;
737
- try {
738
- while ((bytesRead = fs.readSync(0, buf, 0, buf.length, null)) > 0) {
739
- chunks.push(buf.slice(0, bytesRead));
740
- }
741
- } catch { /* EOF or read error */ }
742
- const raw = Buffer.concat(chunks).toString('utf-8').trim();
743
- if (raw && raw.startsWith('{')) {
744
- _stdinData = JSON.parse(raw);
745
- } else {
746
- _stdinData = null;
1084
+ if (fs.existsSync(MODE_FILE)) {
1085
+ return fs.readFileSync(MODE_FILE, 'utf-8').trim();
747
1086
  }
748
- } catch {
749
- _stdinData = null;
750
- }
751
- return _stdinData;
752
- }
753
-
754
- // Override model detection to prefer stdin data from Claude Code
755
- function getModelFromStdin() {
756
- const data = getStdinData();
757
- if (data && data.model && data.model.display_name) return data.model.display_name;
758
- return null;
759
- }
760
-
761
- // Get context window info from Claude Code session
762
- function getContextFromStdin() {
763
- const data = getStdinData();
764
- if (data && data.context_window) {
765
- return {
766
- usedPct: Math.floor(data.context_window.used_percentage || 0),
767
- remainingPct: Math.floor(data.context_window.remaining_percentage || 100),
768
- };
769
- }
770
- return null;
771
- }
772
-
773
- // Get cost info from Claude Code session
774
- function getCostFromStdin() {
775
- const data = getStdinData();
776
- if (data && data.cost) {
777
- const durationMs = data.cost.total_duration_ms || 0;
778
- const mins = Math.floor(durationMs / 60000);
779
- const secs = Math.floor((durationMs % 60000) / 1000);
780
- return {
781
- costUsd: data.cost.total_cost_usd || 0,
782
- duration: mins > 0 ? mins + 'm' + secs + 's' : secs + 's',
783
- linesAdded: data.cost.total_lines_added || 0,
784
- linesRemoved: data.cost.total_lines_removed || 0,
785
- };
786
- }
787
- return null;
1087
+ } catch { /* ignore */ }
1088
+ return 'full'; // default
788
1089
  }
789
1090
 
790
1091
  // ─── Main ───────────────────────────────────────────────────────
@@ -792,14 +1093,32 @@ if (process.argv.includes('--json')) {
792
1093
  console.log(JSON.stringify(generateJSON(), null, 2));
793
1094
  } else if (process.argv.includes('--compact')) {
794
1095
  console.log(JSON.stringify(generateJSON()));
795
- } else {
1096
+ } else if (process.argv.includes('--single-line')) {
796
1097
  console.log(generateStatusline());
1098
+ } else if (process.argv.includes('--toggle')) {
1099
+ // Toggle mode and print the new view
1100
+ const current = readMode();
1101
+ const next = current === 'compact' ? 'full' : 'compact';
1102
+ try {
1103
+ fs.mkdirSync(path.dirname(MODE_FILE), { recursive: true });
1104
+ fs.writeFileSync(MODE_FILE, next, 'utf-8');
1105
+ } catch { /* ignore */ }
1106
+ if (next === 'compact') {
1107
+ console.log(generateStatusline());
1108
+ } else {
1109
+ console.log(generateDashboard());
1110
+ }
1111
+ } else {
1112
+ // Default: respect mode state file
1113
+ const mode = readMode();
1114
+ if (mode === 'compact') {
1115
+ console.log(generateStatusline());
1116
+ } else {
1117
+ console.log(generateDashboard());
1118
+ }
797
1119
  }
798
1120
  `;
799
1121
  }
800
- /**
801
- * Generate statusline hook for shell integration
802
- */
803
1122
  export function generateStatuslineHook(options) {
804
1123
  if (!options.statusline.enabled) {
805
1124
  return '#!/bin/bash\n# Statusline disabled\n';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monoes/cli",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "type": "module",
5
5
  "description": "Monobrain CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
6
6
  "main": "dist/src/index.js",