@monoes/monomindcli 1.10.32 → 1.10.33

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.
@@ -6,7 +6,7 @@ module.exports = {
6
6
  var intelligence = hCtx.intelligence;
7
7
  var args = hCtx.args;
8
8
  if (intelligence && intelligence.stats) {
9
- await Promise.resolve(intelligence.stats(args.includes('--json')));
9
+ try { await Promise.resolve(intelligence.stats(args.includes('--json'))); } catch (e) { /* non-fatal */ }
10
10
  } else {
11
11
  console.log('[WARN] Intelligence module not available. Run session-restore first.');
12
12
  }
@@ -1164,14 +1164,25 @@ function readMode() {
1164
1164
  return 'full'; // default
1165
1165
  }
1166
1166
 
1167
+ // ─── Testability export (when required as a module, not run as CLI) ──────────
1168
+ if (require.main !== module) {
1169
+ module.exports = {
1170
+ readJSON, safeStat, modelLabel,
1171
+ getSecurityStatus, getSwarmStatus, getADRStatus,
1172
+ getHooksStatus, getActiveAgent, getAgentDBStats,
1173
+ getLearningStats, getTestStats, getIntegrationStatus,
1174
+ generateJSON,
1175
+ };
1176
+ }
1177
+
1167
1178
  // ─── Main ───────────────────────────────────────────────────────
1168
- if (process.argv.includes('--json')) {
1179
+ if (require.main === module && process.argv.includes('--json')) {
1169
1180
  console.log(JSON.stringify(generateJSON(), null, 2));
1170
- } else if (process.argv.includes('--compact')) {
1181
+ } else if (require.main === module && process.argv.includes('--compact')) {
1171
1182
  console.log(JSON.stringify(generateJSON()));
1172
- } else if (process.argv.includes('--single-line')) {
1183
+ } else if (require.main === module && process.argv.includes('--single-line')) {
1173
1184
  console.log(generateStatusline());
1174
- } else if (process.argv.includes('--toggle')) {
1185
+ } else if (require.main === module && process.argv.includes('--toggle')) {
1175
1186
  // Toggle mode and print the new view
1176
1187
  const current = readMode();
1177
1188
  const next = current === 'compact' ? 'full' : 'compact';
@@ -1184,7 +1195,7 @@ if (process.argv.includes('--json')) {
1184
1195
  } else {
1185
1196
  console.log(generateDashboard());
1186
1197
  }
1187
- } else {
1198
+ } else if (require.main === module) {
1188
1199
  // Default: respect mode state file
1189
1200
  const mode = readMode();
1190
1201
  if (mode === 'compact') {
@@ -5,7 +5,7 @@
5
5
  "cwd": "/Users/morteza/Desktop/tools/monomind/packages/@monomind/cli/dist/src/ui",
6
6
  "context": {},
7
7
  "metrics": {
8
- "edits": 0,
8
+ "edits": 10,
9
9
  "commands": 0,
10
10
  "tasks": 0,
11
11
  "errors": 0
@@ -74,7 +74,7 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
74
74
  #feed-sess-nav { margin-left: auto; display: flex; gap: 6px; align-items: center; }
75
75
  .sess-btn { font-size: 11px; color: var(--text-lo); background: var(--surface); border: 1px solid var(--border); border-radius: 4px; padding: 2px 8px; cursor: pointer; transition: color 0.1s; line-height: 1.4; }
76
76
  .sess-btn:hover { color: var(--text-hi); }
77
- #feed-scroll { flex: 1; overflow-y: auto; }
77
+ #feed-scroll { flex: 1; overflow-y: auto; min-width: 0; }
78
78
  #feed-scroll::-webkit-scrollbar { width: 3px; }
79
79
  #feed-scroll::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
80
80
 
@@ -425,6 +425,85 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
425
425
  .ph-mid { background:oklch(72% 0.18 75 / 0.15); color:oklch(78% 0.18 75); }
426
426
  .ph-lo { background:oklch(60% 0.18 25 / 0.12); color:oklch(70% 0.18 25); }
427
427
 
428
+ /* ── ambient mode ────────────────────────────────────────── */
429
+ #app.ambient #sidebar,
430
+ #app.ambient #topbar,
431
+ #app.ambient #alerts-rail,
432
+ #app.ambient #feed-head,
433
+ #app.ambient #feed-time-filter,
434
+ #app.ambient #metrics-pane,
435
+ #app.ambient #replay-bar,
436
+ #app.ambient #feed-recap,
437
+ #app.ambient #feed-timeline { display:none !important; }
438
+ #app.ambient #main { background:var(--bg); }
439
+ #app.ambient #view-now { height:100vh; }
440
+ #app.ambient #feed-pane { border:none; }
441
+ #app.ambient .feed-entry { padding: 6px 22px; }
442
+ #app.ambient .feed-lbl { font-size:14px; }
443
+ #app-ambient-hint { display:none; position:fixed; bottom:16px; right:16px; font-size:11px; color:var(--text-xs); z-index:300; pointer-events:none; }
444
+ #app.ambient #app-ambient-hint { display:block; }
445
+
446
+ /* ── minimap scrubber ─────────────────────────────────────── */
447
+ #feed-minimap { position:absolute; top:0; right:0; width:8px; height:100%; z-index:10; cursor:pointer; }
448
+ #feed-minimap-track { position:absolute; inset:0; }
449
+ .mm-pip { position:absolute; right:1px; width:6px; border-radius:3px; min-height:3px; opacity:0.55; transition:opacity 0.1s; }
450
+ .mm-pip:hover { opacity:1; }
451
+ .mm-pip.mp-file { background:oklch(60% 0.12 220); }
452
+ .mm-pip.mp-bash { background:oklch(72% 0.18 75); }
453
+ .mm-pip.mp-agent { background:oklch(70% 0.15 300); }
454
+ .mm-pip.mp-mcp { background:oklch(65% 0.15 200); }
455
+ .mm-pip.mp-err { background:var(--red); opacity:0.8; }
456
+ .mm-pip.mp-user { background:var(--text-xs); }
457
+ .mm-pip.mp-other { background:var(--surface-hi); }
458
+ #mm-thumb { position:absolute; right:0; width:8px; background:oklch(100% 0 0 / 0.08); border-radius:4px; pointer-events:none; transition:top 0.05s; }
459
+ #feed-scroll-wrap { position:relative; flex:1; overflow:hidden; display:flex; }
460
+ #feed-scroll { flex:1; }
461
+
462
+ /* ── daily digest ─────────────────────────────────────────── */
463
+ #digest-card { display:none; flex-shrink:0; border-bottom:1px solid var(--border); background:oklch(13.5% 0.009 55); padding:10px 18px; }
464
+ #digest-card.show { display:block; }
465
+ .digest-row { display:flex; align-items:center; gap:6px; flex-wrap:wrap; }
466
+ .digest-title { font-size:11px; font-weight:600; letter-spacing:0.07em; text-transform:uppercase; color:var(--text-lo); margin-bottom:6px; display:flex; align-items:center; gap:8px; }
467
+ .digest-close { margin-left:auto; background:none; border:none; color:var(--text-xs); cursor:pointer; font-size:13px; line-height:1; padding:0; }
468
+ .digest-close:hover { color:var(--text-lo); }
469
+ .digest-stat { display:inline-flex; align-items:center; gap:4px; font-size:11px; padding:2px 8px; border-radius:8px; background:var(--surface-hi); color:var(--text-mid); white-space:nowrap; }
470
+
471
+ /* ── cost leaderboard ────────────────────────────────────── */
472
+ .lb-toggle { font-size:11px; color:var(--text-lo); background:transparent; border:1px solid transparent; border-radius:8px; padding:2px 9px; cursor:pointer; transition:color 0.1s, background 0.1s; font-family:var(--sans); }
473
+ .lb-toggle:hover { color:var(--text-hi); }
474
+ .lb-toggle.on { background:oklch(72% 0.18 75 / 0.1); color:var(--accent); border-color:oklch(72% 0.18 75 / 0.3); }
475
+ .lb-table { width:100%; border-collapse:collapse; margin-top:4px; }
476
+ .lb-table th { font-size:10px; letter-spacing:0.07em; text-transform:uppercase; color:var(--text-xs); padding:4px 6px; text-align:left; border-bottom:1px solid var(--border); }
477
+ .lb-table td { font-size:12px; padding:7px 6px; border-bottom:1px solid oklch(25% 0.008 55 / 0.5); color:var(--text-mid); vertical-align:top; cursor:pointer; }
478
+ .lb-table tr:hover td { background:var(--surface-hi); color:var(--text-hi); }
479
+ .lb-rank { font-family:var(--mono); color:var(--text-xs); width:22px; }
480
+ .lb-cost { font-family:var(--mono); color:oklch(78% 0.18 75); white-space:nowrap; }
481
+ .lb-dur { font-family:var(--mono); color:var(--text-lo); white-space:nowrap; }
482
+ .lb-prompt { max-width:260px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
483
+
484
+ /* ── session diff ─────────────────────────────────────────── */
485
+ .diff-toggle { font-size:11px; color:var(--text-lo); background:transparent; border:1px solid transparent; border-radius:8px; padding:2px 9px; cursor:pointer; transition:color 0.1s, background 0.1s; font-family:var(--sans); }
486
+ .diff-toggle:hover { color:var(--text-hi); }
487
+ .diff-toggle.on { background:oklch(60% 0.12 220 / 0.1); color:oklch(65% 0.12 220); border-color:oklch(60% 0.12 220 / 0.3); }
488
+ #diff-panel { display:none; border:1px solid var(--border); border-radius:8px; margin-bottom:16px; overflow:hidden; }
489
+ #diff-panel.show { display:block; }
490
+ .diff-header { display:flex; align-items:center; gap:10px; padding:8px 12px; border-bottom:1px solid var(--border); background:oklch(14% 0.009 55); }
491
+ .diff-title { font-size:11px; font-weight:600; letter-spacing:0.07em; text-transform:uppercase; color:var(--text-lo); flex:1; }
492
+ .diff-clear { background:none; border:none; color:var(--text-xs); cursor:pointer; font-size:12px; }
493
+ .diff-cols { display:grid; grid-template-columns:1fr 1fr; }
494
+ .diff-col { padding:10px 14px; }
495
+ .diff-col + .diff-col { border-left:1px solid var(--border); }
496
+ .diff-col-title { font-size:11px; font-weight:600; color:var(--text-mid); margin-bottom:8px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
497
+ .diff-row { display:flex; justify-content:space-between; gap:8px; padding:3px 0; border-bottom:1px solid oklch(25% 0.008 55 / 0.4); }
498
+ .diff-row:last-child { border-bottom:none; }
499
+ .diff-k { font-size:11px; color:var(--text-xs); }
500
+ .diff-v { font-size:11px; font-family:var(--mono); color:var(--text-mid); }
501
+ .diff-v.diff-hi { color:oklch(78% 0.18 75); }
502
+ .diff-v.diff-lo { color:var(--red); }
503
+ .diff-hint { font-size:11px; color:var(--text-xs); padding:8px 12px; text-align:center; }
504
+ .sess-row.diff-sel-a { background:oklch(60% 0.12 220 / 0.08); outline:1px solid oklch(60% 0.12 220 / 0.3); }
505
+ .sess-row.diff-sel-b { background:oklch(72% 0.18 75 / 0.06); outline:1px solid oklch(72% 0.18 75 / 0.3); }
506
+
428
507
  /* ── budget cap ──────────────────────────────────────────── */
429
508
  #budget-modal { display:none; position:fixed; inset:0; z-index:200; background:oklch(5% 0 0 / 0.6); align-items:center; justify-content:center; }
430
509
  #budget-modal.open { display:flex; }
@@ -566,10 +645,20 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
566
645
  <button class="tf-btn" data-tf="1h" onclick="setFeedTimeFilter('1h')">1h</button>
567
646
  <button class="tf-btn" data-tf="6h" onclick="setFeedTimeFilter('6h')">6h</button>
568
647
  <button class="tf-btn" data-tf="24h" onclick="setFeedTimeFilter('24h')">24h</button>
569
- <span class="kb-hint"><kbd>J</kbd><kbd>K</kbd> navigate &nbsp;<kbd>↵</kbd> detail &nbsp;<kbd>/</kbd> find &nbsp;<kbd>G</kbd> live &nbsp;<kbd>⌘K</kbd> search</span>
648
+ <span class="kb-hint"><kbd>J</kbd><kbd>K</kbd> navigate &nbsp;<kbd>↵</kbd> detail &nbsp;<kbd>/</kbd> find &nbsp;<kbd>G</kbd> live &nbsp;<kbd>A</kbd> ambient &nbsp;<kbd>⌘K</kbd> search</span>
649
+ </div>
650
+ <div id="digest-card">
651
+ <div class="digest-title">Today's Digest <button class="digest-close" onclick="dismissDigest()" title="Dismiss">✕</button></div>
652
+ <div class="digest-row" id="digest-stats"></div>
570
653
  </div>
571
- <div id="feed-scroll">
572
- <div id="feed-content"><div class="loading-txt">Loading activity…</div></div>
654
+ <div id="feed-scroll-wrap">
655
+ <div id="feed-scroll">
656
+ <div id="feed-content"><div class="loading-txt">Loading activity…</div></div>
657
+ </div>
658
+ <div id="feed-minimap" title="Click to jump · events map">
659
+ <div id="feed-minimap-track"></div>
660
+ <div id="mm-thumb"></div>
661
+ </div>
573
662
  </div>
574
663
  </div>
575
664
 
@@ -617,11 +706,23 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
617
706
  <!-- SESSIONS -->
618
707
  <div class="view" id="view-sessions">
619
708
  <div class="vscroll">
620
- <div style="display:flex;align-items:baseline;gap:10px;margin-bottom:4px">
709
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:4px;flex-wrap:wrap">
621
710
  <div class="pg-title" style="margin-bottom:0">Sessions</div>
622
711
  <button id="sess-star-filter" onclick="toggleSessStarFilter()" title="Show only bookmarked sessions">☆ Starred</button>
712
+ <button class="lb-toggle" id="btn-leaderboard" onclick="toggleLeaderboard()" title="Cost leaderboard">⬆ Leaderboard</button>
713
+ <button class="diff-toggle" id="btn-diff" onclick="toggleDiffMode()" title="Compare two sessions">⇄ Compare</button>
623
714
  </div>
624
715
  <div class="pg-sub" id="sess-pg-sub">Recent Claude Code sessions for this project</div>
716
+ <div id="diff-panel">
717
+ <div class="diff-header"><span class="diff-title">Session Comparison</span><button class="diff-clear" onclick="clearDiff()" title="Clear">✕</button></div>
718
+ <div class="diff-hint" id="diff-hint">Click two sessions below to compare them</div>
719
+ <div class="diff-cols" id="diff-cols" style="display:none"></div>
720
+ </div>
721
+ <div id="lb-panel" style="display:none;margin-bottom:16px">
722
+ <table class="lb-table"><thead><tr>
723
+ <th class="lb-rank">#</th><th>Session</th><th class="lb-cost">Cost</th><th class="lb-dur">Duration</th>
724
+ </tr></thead><tbody id="lb-body"></tbody></table>
725
+ </div>
625
726
  <div id="sess-content" class="sess-list"><div class="loading-txt">Loading…</div></div>
626
727
  </div>
627
728
  </div>
@@ -670,6 +771,7 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
670
771
 
671
772
  </div><!-- /view-wrap -->
672
773
  </div><!-- /main -->
774
+ <div id="app-ambient-hint">Press A to exit ambient mode</div>
673
775
  </div><!-- /app -->
674
776
 
675
777
  <!-- budget modal (fixed overlay, outside app) -->
@@ -978,7 +1080,8 @@ function renderFeedEvents(events, silent) {
978
1080
 
979
1081
  // update timeline + breakdown with all original events (before time-filter)
980
1082
  buildTimeline(filtered);
981
- buildBreakdown(filtered);
1083
+ buildBreakdownByName(filtered);
1084
+ buildMinimap(filtered);
982
1085
  // session recap card
983
1086
  buildRecap(filtered, allSessions[sessionIdx]);
984
1087
  }
@@ -1287,8 +1390,8 @@ async function renderProjects() {
1287
1390
  const el = document.getElementById('proj-content');
1288
1391
  el.innerHTML = '<div class="loading-txt">Loading…</div>';
1289
1392
  try {
1290
- const data = await apiFetch('/api/data?dir=' + enc(ORIGINAL_DIR));
1291
- allProjects = data?.allProjects || [];
1393
+ const data = await apiFetch('/api/projects');
1394
+ allProjects = data?.projects || [];
1292
1395
  document.getElementById('bdg-projects').textContent = allProjects.length || '—';
1293
1396
  document.getElementById('proj-pg-sub').textContent =
1294
1397
  allProjects.length + ' project' + (allProjects.length !== 1 ? 's' : '') + ' found';
@@ -1351,7 +1454,8 @@ async function renderSessions() {
1351
1454
  el.innerHTML = '<div class="empty"><div class="empty-ico">☆</div><div>No bookmarked sessions</div></div>';
1352
1455
  return;
1353
1456
  }
1354
- el.innerHTML = toShow.map(s => {
1457
+ const sessData = JSON.stringify(toShow).replace(/'/g, '&#39;');
1458
+ el.innerHTML = toShow.map((s, idx) => {
1355
1459
  const dur = s.totalDurationMs ? fmtDur(s.totalDurationMs) : '';
1356
1460
  const msgs = s.totalMessages ? s.totalMessages + ' msg' : '';
1357
1461
  const cost = typeof s.totalCost === 'number' ? '$' + s.totalCost.toFixed(2)
@@ -1360,7 +1464,8 @@ async function renderSessions() {
1360
1464
  const summaries = (s.summaries || []).slice(0, 2).map(sm => { const t = typeof sm === 'string' ? sm : (sm.summary || sm.text || String(sm)); return `<span class="sr-tag">${esc(t.slice(0, 40))}</span>`; }).join('');
1361
1465
  const autoTags = (allTags.sessionTags.get(s.id) || []).map(t => `<span class="sr-autotag">${esc(t)}</span>`).join('');
1362
1466
  const isStarred = bookmarks.has(s.id);
1363
- return `<div class="sess-row" onclick="jumpToSession('${esc(s.id)}')">
1467
+ const sData = JSON.stringify(s).replace(/'/g, '&#39;');
1468
+ return `<div class="sess-row" data-sess-idx="${idx}" onclick="diffMode ? diffSelectSession(JSON.parse(this.dataset.sessData || '{}'), this) : jumpToSession('${esc(s.id)}')" data-sess-data='${sData}'>
1364
1469
  <div class="sr-top">
1365
1470
  <div class="sr-prompt">${esc(s.lastPrompt || s.id)}</div>
1366
1471
  <div class="sr-time">${relTime(s.lastTs || s.mtime)}</div>
@@ -1375,6 +1480,8 @@ async function renderSessions() {
1375
1480
  if (allTags.common.size > 1) {
1376
1481
  el.innerHTML = buildTagFilterBar(toShow) + el.innerHTML;
1377
1482
  }
1483
+ buildDigest();
1484
+ if (leaderboardOpen) renderLeaderboard();
1378
1485
  } catch (err) {
1379
1486
  el.innerHTML = '<div class="empty">Could not load sessions: ' + esc(err.message) + '</div>';
1380
1487
  }
@@ -1492,9 +1599,9 @@ async function renderGlobalFeed() {
1492
1599
  const el = document.getElementById('gf-content');
1493
1600
  el.innerHTML = '<div class="loading-txt">Loading all projects…</div>';
1494
1601
  try {
1495
- // fetch project list using ORIGINAL_DIR
1496
- const data = await apiFetch('/api/data?dir=' + enc(ORIGINAL_DIR));
1497
- const projects = (data?.allProjects || []).slice(0, 8);
1602
+ // fetch project list
1603
+ const data = await apiFetch('/api/projects');
1604
+ const projects = (data?.projects || []).slice(0, 8);
1498
1605
  if (!projects.length) {
1499
1606
  el.innerHTML = '<div class="empty"><div class="empty-ico">⊕</div><div>No projects found</div></div>';
1500
1607
  return;
@@ -1695,6 +1802,256 @@ function healthClass(score) {
1695
1802
  return 'ph-lo';
1696
1803
  }
1697
1804
 
1805
+ // ── feature 7: tool call frequency chart (by name) ─────────
1806
+ function buildBreakdownByName(events) {
1807
+ const counts = {};
1808
+ for (const ev of events) {
1809
+ if (ev.kind !== 'tool') continue;
1810
+ const name = (ev.name || ev.cat || 'other').replace(/^mcp__.*$/, 'MCP').replace(/^m__.*$/, 'MCP');
1811
+ counts[name] = (counts[name] || 0) + 1;
1812
+ }
1813
+ const total = Object.values(counts).reduce((a, b) => a + b, 0);
1814
+ if (!total) {
1815
+ document.getElementById('m-breakdown').innerHTML =
1816
+ '<div class="m-group-title">Tool Usage</div><div class="loading-txt" style="padding:6px 0">—</div>';
1817
+ return;
1818
+ }
1819
+ const CAT_COLOR = { Bash:'oklch(72% 0.18 75)', Read:'oklch(60% 0.12 220)', Edit:'oklch(65% 0.15 160)', Write:'oklch(65% 0.15 160)', Agent:'oklch(70% 0.15 300)', Task:'oklch(65% 0.15 280)', MCP:'oklch(65% 0.15 200)', WebFetch:'oklch(60% 0.12 195)', WebSearch:'oklch(60% 0.12 195)' };
1820
+ const getColor = n => CAT_COLOR[n] || 'var(--text-xs)';
1821
+ const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]).slice(0, 8);
1822
+ const rows = sorted.map(([name, cnt]) => {
1823
+ const pct = Math.round(cnt / total * 100);
1824
+ return `<div class="tb-row">
1825
+ <div class="tb-lbl" style="width:54px" title="${esc(name)}">${esc(name.length > 8 ? name.slice(0,7)+'…' : name)}</div>
1826
+ <div class="tb-bar-wrap"><div class="tb-bar" style="width:${pct}%;background:${getColor(name)}"></div></div>
1827
+ <div class="tb-count">${cnt}</div>
1828
+ </div>`;
1829
+ }).join('');
1830
+ document.getElementById('m-breakdown').innerHTML =
1831
+ `<div class="m-group-title">Tool Usage <span style="font-size:10px;color:var(--text-xs);font-weight:400">${total} calls</span></div><div class="m-breakdown">${rows}</div>`;
1832
+ }
1833
+
1834
+ // ── feature 8: ambient mode ────────────────────────────────
1835
+ function toggleAmbient() {
1836
+ document.getElementById('app').classList.toggle('ambient');
1837
+ }
1838
+
1839
+ // ── feature 9: daily digest ────────────────────────────────
1840
+ const DIGEST_DISMISSED_KEY = 'mm-digest-dismissed';
1841
+
1842
+ function buildDigest() {
1843
+ const todayKey = new Date().toISOString().slice(0, 10);
1844
+ if (localStorage.getItem(DIGEST_DISMISSED_KEY) === todayKey) return;
1845
+ if (!allSessions.length) return;
1846
+
1847
+ const todayStart = new Date(); todayStart.setHours(0, 0, 0, 0);
1848
+ const todaySessions = allSessions.filter(s => {
1849
+ const t = s.lastTs || s.mtime;
1850
+ return t && new Date(typeof t === 'number' ? t : t).getTime() >= todayStart.getTime();
1851
+ });
1852
+ if (!todaySessions.length) return;
1853
+
1854
+ const totalCost = todaySessions.reduce((a, s) => a + (s.totalCost || 0), 0);
1855
+ const totalTools = todaySessions.reduce((a, s) => a + (s.toolCalls || 0), 0);
1856
+ const totalMsgs = todaySessions.reduce((a, s) => a + (s.userMessages || 0), 0);
1857
+ const longestMs = Math.max(...todaySessions.map(s => s.totalDurationMs || 0));
1858
+
1859
+ // gather top tools from today's sessions' tag keywords as themes
1860
+ const themes = [...new Set(todaySessions.flatMap(s => s._tags || []))].slice(0, 3);
1861
+
1862
+ const stats = [
1863
+ `${todaySessions.length} session${todaySessions.length > 1 ? 's' : ''}`,
1864
+ totalCost > 0 ? `$${totalCost.toFixed(2)} spent` : null,
1865
+ totalTools > 0 ? `${totalTools} tool calls` : null,
1866
+ totalMsgs > 0 ? `${totalMsgs} messages` : null,
1867
+ longestMs > 0 ? `${fmtDur(longestMs)} longest` : null,
1868
+ ...themes.map(t => `#${t}`),
1869
+ ].filter(Boolean);
1870
+
1871
+ document.getElementById('digest-stats').innerHTML =
1872
+ stats.map(s => `<span class="digest-stat">${esc(s)}</span>`).join('');
1873
+ document.getElementById('digest-card').classList.add('show');
1874
+ }
1875
+
1876
+ function dismissDigest() {
1877
+ const todayKey = new Date().toISOString().slice(0, 10);
1878
+ localStorage.setItem(DIGEST_DISMISSED_KEY, todayKey);
1879
+ document.getElementById('digest-card').classList.remove('show');
1880
+ }
1881
+
1882
+ // ── feature 10: minimap scrubber ──────────────────────────
1883
+ function buildMinimap(events) {
1884
+ const track = document.getElementById('feed-minimap-track');
1885
+ const thumb = document.getElementById('mm-thumb');
1886
+ const scroll = document.getElementById('feed-scroll');
1887
+ if (!track || !thumb || !scroll) return;
1888
+
1889
+ const tools = events.filter(ev => ev.kind === 'tool' || ev.kind === 'user');
1890
+ if (!tools.length) { track.innerHTML = ''; return; }
1891
+
1892
+ const CAT_CLS = { file:'mp-file', bash:'mp-bash', agent:'mp-agent', mcp:'mp-mcp', user:'mp-user' };
1893
+ const H = scroll.clientHeight || 400;
1894
+ const N = tools.length;
1895
+ track.innerHTML = tools.map((ev, i) => {
1896
+ const top = Math.round((i / N) * H);
1897
+ const h = Math.max(3, Math.round((1 / N) * H * 0.7));
1898
+ const cls = ev._errored ? 'mp-err' : (ev.kind === 'user' ? 'mp-user' : (CAT_CLS[ev.cat] || 'mp-other'));
1899
+ return `<div class="mm-pip ${cls}" style="top:${top}px;height:${h}px" onclick="minimapJump(${i},${N})"></div>`;
1900
+ }).join('');
1901
+
1902
+ // sync thumb
1903
+ const updateThumb = () => {
1904
+ const ratio = scroll.scrollHeight > H ? scroll.scrollTop / (scroll.scrollHeight - H) : 0;
1905
+ const thH = Math.max(20, Math.round(H * (H / Math.max(scroll.scrollHeight, H + 1))));
1906
+ thumb.style.height = thH + 'px';
1907
+ thumb.style.top = Math.round(ratio * (H - thH)) + 'px';
1908
+ };
1909
+ scroll.removeEventListener('scroll', scroll._mmListener || (() => {}));
1910
+ scroll._mmListener = updateThumb;
1911
+ scroll.addEventListener('scroll', scroll._mmListener);
1912
+ updateThumb();
1913
+ }
1914
+
1915
+ function minimapJump(idx, total) {
1916
+ const scroll = document.getElementById('feed-scroll');
1917
+ if (!scroll) return;
1918
+ scroll.scrollTop = Math.round((idx / total) * scroll.scrollHeight);
1919
+ }
1920
+
1921
+ // ── feature 11: cost leaderboard ──────────────────────────
1922
+ let leaderboardOpen = false;
1923
+
1924
+ function toggleLeaderboard() {
1925
+ leaderboardOpen = !leaderboardOpen;
1926
+ document.getElementById('btn-leaderboard').classList.toggle('on', leaderboardOpen);
1927
+ const panel = document.getElementById('lb-panel');
1928
+ panel.style.display = leaderboardOpen ? 'block' : 'none';
1929
+ if (leaderboardOpen) renderLeaderboard();
1930
+ }
1931
+
1932
+ function renderLeaderboard() {
1933
+ const sorted = [...allSessions]
1934
+ .filter(s => typeof s.totalCost === 'number' && s.totalCost > 0)
1935
+ .sort((a, b) => b.totalCost - a.totalCost)
1936
+ .slice(0, 15);
1937
+ const body = document.getElementById('lb-body');
1938
+ if (!sorted.length) { body.innerHTML = '<tr><td colspan="4" style="text-align:center;color:var(--text-xs);padding:12px">No cost data yet</td></tr>'; return; }
1939
+ body.innerHTML = sorted.map((s, i) => {
1940
+ const cost = '$' + s.totalCost.toFixed(2);
1941
+ const dur = s.totalDurationMs ? fmtDur(s.totalDurationMs) : '—';
1942
+ const prompt = s.lastPrompt || s.id;
1943
+ return `<tr onclick="jumpToSession('${esc(s.id)}')" title="${esc(prompt)}">
1944
+ <td class="lb-rank">${i + 1}</td>
1945
+ <td class="lb-prompt">${esc(prompt.slice(0, 60))}</td>
1946
+ <td class="lb-cost">${cost}</td>
1947
+ <td class="lb-dur">${dur}</td>
1948
+ </tr>`;
1949
+ }).join('');
1950
+ }
1951
+
1952
+ // ── feature 12: session diff ──────────────────────────────
1953
+ let diffMode = false;
1954
+ let diffSelA = null; let diffSelB = null;
1955
+
1956
+ function toggleDiffMode() {
1957
+ diffMode = !diffMode;
1958
+ document.getElementById('btn-diff').classList.toggle('on', diffMode);
1959
+ const panel = document.getElementById('diff-panel');
1960
+ panel.classList.toggle('show', diffMode);
1961
+ if (!diffMode) clearDiff();
1962
+ }
1963
+
1964
+ function clearDiff() {
1965
+ diffSelA = null; diffSelB = null;
1966
+ document.getElementById('diff-hint').style.display = '';
1967
+ document.getElementById('diff-cols').style.display = 'none';
1968
+ document.getElementById('diff-cols').innerHTML = '';
1969
+ document.querySelectorAll('.sess-row.diff-sel-a, .sess-row.diff-sel-b').forEach(el => {
1970
+ el.classList.remove('diff-sel-a', 'diff-sel-b');
1971
+ });
1972
+ }
1973
+
1974
+ function diffSelectSession(sess, el) {
1975
+ if (!diffMode) return;
1976
+ if (diffSelA && diffSelA.id === sess.id) {
1977
+ diffSelA = null;
1978
+ el.classList.remove('diff-sel-a');
1979
+ document.getElementById('diff-hint').style.display = '';
1980
+ document.getElementById('diff-cols').style.display = 'none';
1981
+ return;
1982
+ }
1983
+ if (diffSelB && diffSelB.id === sess.id) {
1984
+ diffSelB = null;
1985
+ el.classList.remove('diff-sel-b');
1986
+ renderDiff();
1987
+ return;
1988
+ }
1989
+ if (!diffSelA) {
1990
+ diffSelA = sess;
1991
+ el.classList.add('diff-sel-a');
1992
+ } else if (!diffSelB) {
1993
+ diffSelB = sess;
1994
+ el.classList.add('diff-sel-b');
1995
+ } else {
1996
+ // replace B with new selection
1997
+ document.querySelectorAll('.sess-row.diff-sel-b').forEach(e => e.classList.remove('diff-sel-b'));
1998
+ diffSelB = sess;
1999
+ el.classList.add('diff-sel-b');
2000
+ }
2001
+ if (diffSelA && diffSelB) {
2002
+ renderDiff();
2003
+ } else {
2004
+ document.getElementById('diff-hint').textContent = diffSelA ? 'Now click a second session to compare' : 'Click two sessions below to compare them';
2005
+ document.getElementById('diff-hint').style.display = '';
2006
+ document.getElementById('diff-cols').style.display = 'none';
2007
+ }
2008
+ }
2009
+
2010
+ function renderDiff() {
2011
+ if (!diffSelA || !diffSelB) return;
2012
+ document.getElementById('diff-hint').style.display = 'none';
2013
+ const cols = document.getElementById('diff-cols');
2014
+ cols.style.display = '';
2015
+ const fmt = (a, b, key, prefix = '') => {
2016
+ const va = a[key], vb = b[key];
2017
+ if (va == null && vb == null) return '';
2018
+ const fa = prefix + (va != null ? va : '—');
2019
+ const fb = prefix + (vb != null ? vb : '—');
2020
+ const hiA = va != null && vb != null && va > vb ? ' diff-hi' : (va != null && vb != null && va < vb ? ' diff-lo' : '');
2021
+ const hiB = va != null && vb != null && vb > va ? ' diff-hi' : (va != null && vb != null && vb < va ? ' diff-lo' : '');
2022
+ return [fa, hiA, fb, hiB];
2023
+ };
2024
+
2025
+ const diffCol = (s) => {
2026
+ const cost = typeof s.totalCost === 'number' ? '$' + s.totalCost.toFixed(2) : '—';
2027
+ const dur = s.totalDurationMs ? fmtDur(s.totalDurationMs) : '—';
2028
+ const tools = s.toolCalls != null ? s.toolCalls : '—';
2029
+ const msgs = s.userMessages != null ? s.userMessages : '—';
2030
+ const errs = s.errors != null ? s.errors : '—';
2031
+ const prompt = (s.lastPrompt || s.id || '').slice(0, 50);
2032
+ return { cost, dur, tools, msgs, errs, prompt, s };
2033
+ };
2034
+
2035
+ const a = diffCol(diffSelA), b = diffCol(diffSelB);
2036
+ const rows = [
2037
+ ['Cost', a.cost, b.cost, typeof diffSelA.totalCost === 'number' && typeof diffSelB.totalCost === 'number' ? (diffSelA.totalCost > diffSelB.totalCost ? ['diff-hi','diff-lo'] : diffSelA.totalCost < diffSelB.totalCost ? ['diff-lo','diff-hi'] : ['','']) : ['','']],
2038
+ ['Duration', a.dur, b.dur, ['','']],
2039
+ ['Tool calls', a.tools, b.tools, typeof a.tools === 'number' && typeof b.tools === 'number' ? (a.tools > b.tools ? ['diff-hi','diff-lo'] : a.tools < b.tools ? ['diff-lo','diff-hi'] : ['','']) : ['','']],
2040
+ ['Messages', a.msgs, b.msgs, ['','']],
2041
+ ['Errors', a.errs, b.errs, ['','']],
2042
+ ];
2043
+
2044
+ const colHtml = (idx) => `<div class="diff-col">
2045
+ <div class="diff-col-title">${esc(idx === 0 ? a.prompt : b.prompt)}</div>
2046
+ ${rows.map(([k, va, vb, cls]) => `<div class="diff-row">
2047
+ <span class="diff-k">${k}</span>
2048
+ <span class="diff-v ${cls[idx]}">${esc(idx === 0 ? va : vb)}</span>
2049
+ </div>`).join('')}
2050
+ </div>`;
2051
+
2052
+ cols.innerHTML = colHtml(0) + colHtml(1);
2053
+ }
2054
+
1698
2055
  // ── loops ──────────────────────────────────────────────────
1699
2056
  async function renderLoops() {
1700
2057
  const el = document.getElementById('loops-content');
@@ -2193,7 +2550,8 @@ document.addEventListener('keydown', e => {
2193
2550
  if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
2194
2551
  if (document.getElementById('cmd-palette').classList.contains('open')) return;
2195
2552
 
2196
- if (e.key === 'Escape') { closeDetail(); closeCmdPalette(); }
2553
+ if (e.key === 'Escape') { closeDetail(); closeCmdPalette(); if (document.getElementById('app').classList.contains('ambient')) toggleAmbient(); }
2554
+ if (e.key === 'a' || e.key === 'A') { if (currentView === 'now') { e.preventDefault(); toggleAmbient(); } }
2197
2555
 
2198
2556
  if (currentView === 'now') {
2199
2557
  if (e.key === '/') { e.preventDefault(); toggleFeedSearch(); }
@@ -429,6 +429,40 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
429
429
  return;
430
430
  }
431
431
 
432
+ // ------------------------------------------------------- GET /api/projects
433
+ if (req.method === 'GET' && url === '/api/projects') {
434
+ try {
435
+ const projectsBase = path.join(os.homedir(), '.claude', 'projects');
436
+ let slugDirs = [];
437
+ try { slugDirs = fs.readdirSync(projectsBase, { withFileTypes: true }).filter(e => e.isDirectory()).map(e => e.name); } catch {}
438
+ const projects = slugDirs.map(slug => {
439
+ const projDir = path.join(projectsBase, slug);
440
+ // convert slug back to path: replace leading - and then each - that was /
441
+ const projPath = slug.replace(/-/g, '/');
442
+ const name = slug.split('-').filter(Boolean).pop() || slug;
443
+ let sessionCount = 0; let lastActivity = 0; let memoryCount = 0;
444
+ try {
445
+ const files = fs.readdirSync(projDir).filter(f => f.endsWith('.jsonl'));
446
+ sessionCount = files.length;
447
+ for (const f of files) {
448
+ try { const st = fs.statSync(path.join(projDir, f)); if (st.mtimeMs > lastActivity) lastActivity = st.mtimeMs; } catch {}
449
+ }
450
+ } catch {}
451
+ try {
452
+ const memDir = path.join(projDir, 'memory');
453
+ memoryCount = fs.readdirSync(memDir).filter(f => f.endsWith('.md') && f !== 'MEMORY.md').length;
454
+ } catch {}
455
+ return { slug, path: projPath, name, sessionCount, memoryCount, lastActivity: lastActivity || null };
456
+ }).filter(p => p.sessionCount > 0).sort((a, b) => (b.lastActivity || 0) - (a.lastActivity || 0));
457
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
458
+ res.end(JSON.stringify({ projects }));
459
+ } catch (err) {
460
+ res.writeHead(500, { 'Content-Type': 'application/json' });
461
+ res.end(JSON.stringify({ error: err.message }));
462
+ }
463
+ return;
464
+ }
465
+
432
466
  // ------------------------------------------------------- GET /api/palace
433
467
  if (req.method === 'GET' && url === '/api/palace') {
434
468
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monoes/monomindcli",
3
- "version": "1.10.32",
3
+ "version": "1.10.33",
4
4
  "type": "module",
5
5
  "description": "Monomind 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",