@monoes/monomindcli 1.10.38 → 1.10.39

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.
@@ -942,7 +942,7 @@ function chunkTranscript(messages) {
942
942
  if (msg.role === 'user') {
943
943
  const isSynthetic = Array.isArray(msg.content) &&
944
944
  msg.content.every(b => b.type === 'tool_result');
945
- if (isSynthetic && currentChunk) continue;
945
+ if (isSynthetic) continue;
946
946
  if (currentChunk) chunks.push(currentChunk);
947
947
  currentChunk = {
948
948
  userMessage: msg,
@@ -389,6 +389,69 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
389
389
  /* ── forecast row ────────────────────────────────────────── */
390
390
  .m-val.forecast { color:var(--text-lo); font-size:11px; font-weight:500; }
391
391
 
392
+ /* ── session date groups ─────────────────────────────────── */
393
+ .sg-section { margin-bottom:4px; }
394
+ .sg-header { display:flex; align-items:center; gap:6px; padding:5px 2px 3px; cursor:pointer; user-select:none; }
395
+ .sg-title { font-size:10px; letter-spacing:0.07em; text-transform:uppercase; color:var(--text-xs); font-weight:600; }
396
+ .sg-count { font-size:10px; color:var(--text-lo); background:var(--surface-hi); border-radius:8px; padding:1px 6px; }
397
+ .sg-toggle { font-size:10px; color:var(--text-xs); margin-left:auto; }
398
+ .sg-header:hover .sg-title { color:var(--text-lo); }
399
+ .sg-body.collapsed { display:none; }
400
+
401
+ /* ── session heatmap calendar ─────────────────────────────── */
402
+ #sess-heatmap { margin-bottom:14px; }
403
+ .shm-grid { display:grid; grid-template-rows:repeat(7,10px); grid-auto-flow:column; grid-auto-columns:10px; gap:2px; margin-top:6px; }
404
+ .shm-cell { border-radius:2px; background:var(--surface-hi); cursor:pointer; transition:outline 0.1s; }
405
+ .shm-cell:hover { outline:1px solid var(--accent); outline-offset:-1px; }
406
+ .shm-cell.shm-1 { background:oklch(72% 0.18 75 / 0.22); }
407
+ .shm-cell.shm-2 { background:oklch(72% 0.18 75 / 0.42); }
408
+ .shm-cell.shm-3 { background:oklch(72% 0.18 75 / 0.65); }
409
+ .shm-cell.shm-4 { background:oklch(72% 0.18 75 / 0.85); }
410
+ .shm-cell.shm-active { outline:2px solid var(--accent); outline-offset:-1px; }
411
+ .shm-label { font-size:10px; color:var(--text-xs); display:flex; align-items:center; justify-content:space-between; }
412
+ #shm-clear { font-size:10px; color:var(--accent); background:none; border:none; cursor:pointer; padding:0; display:none; }
413
+ #shm-clear.show { display:inline; }
414
+
415
+ /* ── cost period toggle ───────────────────────────────────── */
416
+ .period-toggles { display:flex; gap:4px; margin-bottom:10px; flex-wrap:wrap; }
417
+ .period-btn { font-size:11px; padding:3px 10px; border-radius:8px; border:1px solid var(--border); background:transparent; color:var(--text-lo); cursor:pointer; font-family:var(--sans); transition:color 0.1s, background 0.1s; }
418
+ .period-btn:hover { color:var(--text-hi); }
419
+ .period-btn.active { background:var(--accent-dim); color:var(--accent); border-color:oklch(72% 0.18 75 / 0.3); }
420
+
421
+ /* ── bulk session actions ─────────────────────────────────── */
422
+ .sess-row.bulk-sel { background:oklch(72% 0.18 75 / 0.08); outline:1px solid oklch(72% 0.18 75 / 0.3); outline-offset:-1px; }
423
+ #bulk-toolbar { display:none; position:sticky; top:0; z-index:10; background:oklch(18% 0.009 55); border:1px solid oklch(72% 0.18 75 / 0.3); border-radius:var(--r); padding:8px 14px; margin-bottom:10px; gap:10px; align-items:center; }
424
+ #bulk-toolbar.show { display:flex; }
425
+ .bulk-count { font-size:12px; color:var(--text-mid); flex:1; }
426
+ .bulk-btn { font-size:11px; padding:4px 12px; border-radius:6px; border:1px solid var(--border); background:var(--surface); color:var(--text-mid); cursor:pointer; font-family:var(--sans); transition:color 0.1s; }
427
+ .bulk-btn:hover { color:var(--text-hi); }
428
+ .bulk-btn.danger { color:oklch(65% 0.15 25); border-color:oklch(65% 0.15 25 / 0.3); }
429
+
430
+ /* ── files touched ────────────────────────────────────────── */
431
+ .sr-files { font-size:10px; color:var(--text-xs); margin-top:2px; display:flex; flex-wrap:wrap; gap:3px; }
432
+ .sr-file-chip { font-family:var(--mono); background:var(--surface-hi); border-radius:3px; padding:1px 5px; color:var(--text-lo); }
433
+
434
+ /* ── memory age bars ──────────────────────────────────────── */
435
+ .dr-age-bar { height:2px; border-radius:1px; margin-top:4px; background:oklch(72% 0.18 75 / 0.25); }
436
+ .dr-age-bar-fill { height:100%; border-radius:1px; background:var(--accent); }
437
+
438
+ /* ── loop creation form ───────────────────────────────────── */
439
+ #loop-create-form { background:var(--surface); border:1px solid var(--border); border-radius:var(--r); padding:14px 16px; margin-bottom:16px; }
440
+ .lcf-title { font-size:12px; font-weight:600; color:var(--text-hi); margin-bottom:10px; }
441
+ .lcf-row { display:flex; flex-direction:column; gap:3px; margin-bottom:10px; }
442
+ .lcf-label { font-size:10px; color:var(--text-xs); text-transform:uppercase; letter-spacing:0.06em; }
443
+ .lcf-input, .lcf-textarea { background:var(--bg); border:1px solid var(--border); border-radius:4px; color:var(--text-hi); font-family:var(--sans); font-size:12px; padding:6px 10px; transition:border-color 0.15s; }
444
+ .lcf-textarea { resize:vertical; min-height:56px; }
445
+ .lcf-input:focus, .lcf-textarea:focus { outline:none; border-color:oklch(72% 0.18 75 / 0.5); }
446
+ .lcf-row-inline { display:flex; gap:8px; }
447
+ .lcf-row-inline .lcf-row { flex:1; margin-bottom:0; }
448
+ .lcf-actions { display:flex; justify-content:flex-end; gap:8px; margin-top:4px; }
449
+ .lcf-submit { background:var(--accent); color:oklch(11% 0.009 55); border:none; border-radius:6px; font-size:12px; font-weight:600; padding:5px 16px; cursor:pointer; font-family:var(--sans); }
450
+ .lcf-submit:hover { background:oklch(78% 0.18 75); }
451
+ .lcf-cancel { background:transparent; border:1px solid var(--border); border-radius:6px; font-size:12px; color:var(--text-lo); padding:5px 12px; cursor:pointer; font-family:var(--sans); }
452
+ #btn-new-loop { font-size:11px; color:var(--text-lo); background:transparent; border:1px solid var(--border); border-radius:8px; padding:3px 10px; cursor:pointer; transition:color 0.1s; font-family:var(--sans); margin-bottom:12px; }
453
+ #btn-new-loop:hover { color:var(--accent); border-color:oklch(72% 0.18 75 / 0.4); }
454
+
392
455
  /* ── auto-tags ───────────────────────────────────────────── */
393
456
  .sr-autotag { font-size:10px; padding:1px 6px; border-radius:8px; background:oklch(72% 0.18 75 / 0.1); color:oklch(78% 0.18 75); border:1px solid oklch(72% 0.18 75 / 0.2); }
394
457
  .tag-filter-bar { display:flex; flex-wrap:wrap; gap:5px; margin-bottom:12px; }
@@ -908,6 +971,23 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
908
971
  <div id="patterns-panel" style="display:none;margin-bottom:16px">
909
972
  <div id="patterns-body"></div>
910
973
  </div>
974
+ <div id="sess-heatmap" style="margin-bottom:14px;display:none">
975
+ <div class="shm-label"><span>12-week activity</span><button id="shm-clear" onclick="clearHeatmapFilter()">✕ Clear filter</button></div>
976
+ <div class="shm-grid" id="shm-grid"></div>
977
+ </div>
978
+ <div class="period-toggles" id="period-toggles">
979
+ <span style="font-size:10px;color:var(--text-xs);align-self:center;text-transform:uppercase;letter-spacing:0.06em">Period:</span>
980
+ <button class="period-btn active" data-period="day" onclick="setPeriod('day')">Day</button>
981
+ <button class="period-btn" data-period="week" onclick="setPeriod('week')">Week</button>
982
+ <button class="period-btn" data-period="month" onclick="setPeriod('month')">Month</button>
983
+ <button class="period-btn" data-period="all" onclick="setPeriod('all')">All</button>
984
+ </div>
985
+ <div id="bulk-toolbar">
986
+ <span class="bulk-count" id="bulk-count">0 selected</span>
987
+ <button class="bulk-btn" onclick="bulkExport()">⬇ Export</button>
988
+ <button class="bulk-btn" onclick="bulkBookmark()">☆ Bookmark all</button>
989
+ <button class="bulk-btn danger" onclick="clearBulkSelection()">✕ Clear</button>
990
+ </div>
911
991
  <div id="sess-filter-wrap">
912
992
  <input id="sess-filter-input" type="text" placeholder="Filter sessions by prompt…" oninput="filterSessions(this.value)" autocomplete="off">
913
993
  <span id="sess-filter-count"></span>
@@ -921,6 +1001,32 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
921
1001
  <div class="vscroll">
922
1002
  <div class="pg-title">Loops</div>
923
1003
  <div class="pg-sub">Scheduled automation loops</div>
1004
+ <button id="btn-new-loop" onclick="showLoopForm()">+ New Loop</button>
1005
+ <div id="loop-create-form" style="display:none">
1006
+ <div class="lcf-title">Create Loop</div>
1007
+ <div class="lcf-row">
1008
+ <label class="lcf-label">Prompt</label>
1009
+ <textarea class="lcf-textarea" id="lcf-prompt" placeholder="What should the agent do each iteration?"></textarea>
1010
+ </div>
1011
+ <div class="lcf-row">
1012
+ <label class="lcf-label">Name (optional)</label>
1013
+ <input class="lcf-input" id="lcf-name" type="text" placeholder="My loop">
1014
+ </div>
1015
+ <div class="lcf-row-inline">
1016
+ <div class="lcf-row">
1017
+ <label class="lcf-label">Interval</label>
1018
+ <input class="lcf-input" id="lcf-interval" type="text" placeholder="1h" value="1h">
1019
+ </div>
1020
+ <div class="lcf-row">
1021
+ <label class="lcf-label">Max reps (blank = ∞)</label>
1022
+ <input class="lcf-input" id="lcf-maxreps" type="number" placeholder="∞" min="1">
1023
+ </div>
1024
+ </div>
1025
+ <div class="lcf-actions">
1026
+ <button class="lcf-cancel" onclick="hideLoopForm()">Cancel</button>
1027
+ <button class="lcf-submit" onclick="createLoop()">Create Loop</button>
1028
+ </div>
1029
+ </div>
924
1030
  <div id="loops-content" class="loop-list"><div class="loading-txt">Loading…</div></div>
925
1031
  </div>
926
1032
  </div>
@@ -964,6 +1070,24 @@ textarea.sess-note-input:focus { border-color:var(--accent); }
964
1070
  </div><!-- /app -->
965
1071
  <div id="toast-rack"></div>
966
1072
 
1073
+ <!-- shortcut help modal -->
1074
+ <div id="shortcut-modal" onclick="if(event.target===this)closeShortcutHelp()">
1075
+ <div id="shortcut-box">
1076
+ <div class="sk-title">Keyboard Shortcuts <button class="sk-close" onclick="closeShortcutHelp()">✕</button></div>
1077
+ <div class="sk-section">Feed (Now view)</div>
1078
+ <div class="sk-row"><span class="sk-desc">Navigate entries</span><span class="sk-keys"><kbd>J</kbd><kbd>K</kbd></span></div>
1079
+ <div class="sk-row"><span class="sk-desc">Open detail drawer</span><span class="sk-keys"><kbd>↵</kbd></span></div>
1080
+ <div class="sk-row"><span class="sk-desc">Search in feed</span><span class="sk-keys"><kbd>/</kbd></span></div>
1081
+ <div class="sk-row"><span class="sk-desc">Jump to live session</span><span class="sk-keys"><kbd>G</kbd></span></div>
1082
+ <div class="sk-row"><span class="sk-desc">Refresh current view</span><span class="sk-keys"><kbd>R</kbd></span></div>
1083
+ <div class="sk-row"><span class="sk-desc">Toggle ambient mode</span><span class="sk-keys"><kbd>A</kbd></span></div>
1084
+ <div class="sk-section">Global</div>
1085
+ <div class="sk-row"><span class="sk-desc">Command palette</span><span class="sk-keys"><kbd>⌘</kbd><kbd>K</kbd></span></div>
1086
+ <div class="sk-row"><span class="sk-desc">Close / dismiss</span><span class="sk-keys"><kbd>Esc</kbd></span></div>
1087
+ <div class="sk-row"><span class="sk-desc">This help</span><span class="sk-keys"><kbd>?</kbd></span></div>
1088
+ </div>
1089
+ </div>
1090
+
967
1091
  <!-- budget modal (fixed overlay, outside app) -->
968
1092
  <div id="budget-modal" onclick="if(event.target===this)closeBudgetModal()">
969
1093
  <div id="budget-box">
@@ -1053,9 +1177,19 @@ async function init() {
1053
1177
  document.getElementById('sb-path').textContent = DIR;
1054
1178
  document.getElementById('sb-proj').textContent = DIR.split('/').filter(Boolean).pop() || '—';
1055
1179
  } catch (_) {}
1180
+ // deep-link: ?sess=<id>&proj=<path>
1181
+ const params = new URLSearchParams(window.location.search);
1182
+ const projParam = params.get('proj');
1183
+ const sessParam = params.get('sess');
1184
+ if (projParam) {
1185
+ DIR = projParam;
1186
+ document.getElementById('sb-proj').textContent = DIR.split('/').filter(Boolean).pop() || '—';
1187
+ document.getElementById('sb-path').textContent = DIR;
1188
+ }
1056
1189
  viewRendered['now'] = true; // prevents switchView from re-rendering NOW on jumpToSession
1057
1190
  updateBudgetBtnStyle();
1058
1191
  await refreshNow();
1192
+ if (sessParam) setTimeout(() => jumpToSession(sessParam), 300);
1059
1193
  startPolling();
1060
1194
  }
1061
1195
 
@@ -1275,6 +1409,8 @@ function renderFeedEvents(events, silent) {
1275
1409
  updateBurnRate(filtered);
1276
1410
  // session recap card
1277
1411
  buildRecap(filtered, allSessions[sessionIdx]);
1412
+ // infer current activity from most recent tool
1413
+ updateCurrentActivity(visible);
1278
1414
  }
1279
1415
 
1280
1416
  function renderGroupRow(g) {
@@ -1646,6 +1782,9 @@ async function renderSessions() {
1646
1782
  el.innerHTML = '<div class="empty"><div class="empty-ico">☆</div><div>No bookmarked sessions</div></div>';
1647
1783
  return;
1648
1784
  }
1785
+ // compute median cost for anomaly detection
1786
+ const costsForMedian = sessions.map(s => s.totalCost || 0).filter(c => c > 0).sort((a, b) => a - b);
1787
+ const medianCost = costsForMedian.length ? costsForMedian[Math.floor(costsForMedian.length / 2)] : 0;
1649
1788
  const sessData = JSON.stringify(toShow).replace(/'/g, '&#39;');
1650
1789
  el.innerHTML = toShow.map((s, idx) => {
1651
1790
  const dur = s.totalDurationMs ? fmtDur(s.totalDurationMs) : '';
@@ -1653,6 +1792,14 @@ async function renderSessions() {
1653
1792
  const cost = typeof s.totalCost === 'number' ? '$' + s.totalCost.toFixed(2)
1654
1793
  : typeof s.cost === 'number' ? '$' + s.cost.toFixed(2) : '';
1655
1794
  const meta = [dur, msgs, cost].filter(Boolean).join(' · ') || s.id.slice(0, 16);
1795
+ // anomaly badge
1796
+ const sCost = s.totalCost || 0;
1797
+ let anomBadge = '';
1798
+ if (medianCost > 0.05 && sCost > medianCost * 3 && sCost > 0.5) {
1799
+ anomBadge = `<span class="sess-anomaly anom-cost" title="Cost ${((sCost/medianCost).toFixed(1))}× above median">! costly</span>`;
1800
+ } else if (s.toolCalls > 0 && (s.errorCount || 0) / s.toolCalls > 0.3) {
1801
+ anomBadge = `<span class="sess-anomaly anom-err" title="${s.errorCount} tool errors">! errors</span>`;
1802
+ }
1656
1803
  const satPct = Math.min(100, Math.round((s.totalMessages || 0) / 200 * 100));
1657
1804
  const satColor = satPct > 80 ? 'oklch(65% 0.2 25)' : satPct > 50 ? 'oklch(70% 0.18 80)' : 'var(--accent)';
1658
1805
  const satBar = satPct > 0 ? `<div class="ctx-sat-wrap" title="Context saturation ~${satPct}% (${s.totalMessages||0} turns / 200 est. max)">
@@ -1671,7 +1818,7 @@ async function renderSessions() {
1671
1818
  <button class="sess-star${isStarred ? ' on' : ''}" data-sid="${esc(s.id)}" onclick="toggleBookmark('${esc(s.id)}',event)" title="${isStarred ? 'Remove bookmark' : 'Bookmark session'}">${isStarred ? '★' : '☆'}</button>
1672
1819
  <span class="sr-view">→ view</span>
1673
1820
  </div>
1674
- <div class="sr-meta">${esc(meta)}</div>
1821
+ <div class="sr-meta">${esc(meta)}${anomBadge}</div>
1675
1822
  ${(summaries || autoTags) ? `<div class="sr-tags">${summaries}${autoTags}</div>` : ''}
1676
1823
  ${satBar}
1677
1824
  <div class="sess-notes-wrap" onclick="event.stopPropagation()">
@@ -1716,6 +1863,7 @@ async function renderSessions() {
1716
1863
 
1717
1864
  function jumpToSession(id) {
1718
1865
  switchView('now');
1866
+ history.replaceState(null, '', '?sess=' + encodeURIComponent(id) + '&proj=' + encodeURIComponent(DIR));
1719
1867
  setTimeout(() => {
1720
1868
  const i = allSessions.findIndex(x => x.id === id);
1721
1869
  if (i >= 0) { sessionIdx = i; userScrolled = false; loadFeedForSession(allSessions[i]); }
@@ -2099,8 +2247,21 @@ function buildDigest() {
2099
2247
  ...themes.map(t => `#${t}`),
2100
2248
  ].filter(Boolean);
2101
2249
 
2250
+ // cost forecast: project monthly spend from daily average
2251
+ const today2 = new Date();
2252
+ const daysInMonth = new Date(today2.getFullYear(), today2.getMonth() + 1, 0).getDate();
2253
+ const dayOfMonth = today2.getDate();
2254
+ const monthCostSoFar = allSessions.filter(s => {
2255
+ const t = s.firstTs || s.mtime;
2256
+ if (!t) return false;
2257
+ const d = new Date(typeof t === 'number' ? t : t);
2258
+ return d.getFullYear() === today2.getFullYear() && d.getMonth() === today2.getMonth();
2259
+ }).reduce((a, s) => a + (s.totalCost || 0), 0);
2260
+ const dailyAvg = dayOfMonth > 0 ? monthCostSoFar / dayOfMonth : 0;
2261
+ const projected = dailyAvg * daysInMonth;
2262
+ const forecastHtml = projected > 0.05 ? `<span class="digest-stat" title="Projected monthly spend at current pace" style="color:oklch(72% 0.14 200)">~$${projected.toFixed(2)}/mo</span>` : '';
2102
2263
  document.getElementById('digest-stats').innerHTML =
2103
- stats.map(s => `<span class="digest-stat">${esc(s)}</span>`).join('');
2264
+ stats.map(s => `<span class="digest-stat">${esc(s)}</span>`).join('') + forecastHtml;
2104
2265
  document.getElementById('digest-card').classList.add('show');
2105
2266
  }
2106
2267
 
@@ -2610,6 +2771,7 @@ function buildWeeklyRecap() {
2610
2771
  return new Date(typeof t === 'number' ? t : t).toDateString();
2611
2772
  })).size;
2612
2773
  const longestMs = Math.max(...weekSess.map(s => s.totalDurationMs||0));
2774
+ const streak = calcStreak();
2613
2775
  const stats = [
2614
2776
  `${weekSess.length} session${weekSess.length!==1?'s':''}`,
2615
2777
  `${days} day${days!==1?'s':''}`,
@@ -2617,7 +2779,8 @@ function buildWeeklyRecap() {
2617
2779
  totalTools > 0 ? `${totalTools.toLocaleString()} tool calls` : null,
2618
2780
  longestMs > 0 ? `${fmtDur(longestMs)} longest` : null,
2619
2781
  ].filter(Boolean);
2620
- document.getElementById('weekly-stats').innerHTML = stats.map(s => `<span class="digest-stat">${esc(s)}</span>`).join('');
2782
+ const streakHtml = streak >= 2 ? `<span class="streak-chip" title="${streak} consecutive days with sessions">🔥 ${streak}d streak</span>` : '';
2783
+ document.getElementById('weekly-stats').innerHTML = stats.map(s => `<span class="digest-stat">${esc(s)}</span>`).join('') + streakHtml;
2621
2784
  document.getElementById('weekly-card').classList.add('show');
2622
2785
  }
2623
2786
  function dismissWeekly() {
@@ -3118,7 +3281,8 @@ document.addEventListener('keydown', e => {
3118
3281
  if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
3119
3282
  if (document.getElementById('cmd-palette').classList.contains('open')) return;
3120
3283
 
3121
- if (e.key === 'Escape') { closeDetail(); closeCmdPalette(); if (document.getElementById('app').classList.contains('ambient')) toggleAmbient(); }
3284
+ if (e.key === 'Escape') { closeDetail(); closeCmdPalette(); closeShortcutHelp(); if (document.getElementById('app').classList.contains('ambient')) toggleAmbient(); }
3285
+ if (e.key === '?') { e.preventDefault(); openShortcutHelp(); return; }
3122
3286
  if (e.key === 'a' || e.key === 'A') { if (currentView === 'now') { e.preventDefault(); toggleAmbient(); } }
3123
3287
 
3124
3288
  if (currentView === 'now') {
@@ -3147,6 +3311,109 @@ document.addEventListener('keydown', e => {
3147
3311
  }
3148
3312
  });
3149
3313
 
3314
+ // ── feature 31: inline session filter ─────────────────────
3315
+ function filterSessions(q) {
3316
+ const rows = document.querySelectorAll('#sess-content .sess-row');
3317
+ const lq = q.toLowerCase().trim();
3318
+ let visible = 0;
3319
+ rows.forEach(row => {
3320
+ const prompt = (row.querySelector('.sr-prompt')?.textContent || '').toLowerCase();
3321
+ const meta = (row.querySelector('.sr-meta')?.textContent || '').toLowerCase();
3322
+ const tags = (row.querySelector('.sr-tags')?.textContent || '').toLowerCase();
3323
+ const match = !lq || prompt.includes(lq) || meta.includes(lq) || tags.includes(lq);
3324
+ row.style.display = match ? '' : 'none';
3325
+ if (match) visible++;
3326
+ });
3327
+ const countEl = document.getElementById('sess-filter-count');
3328
+ if (countEl) countEl.textContent = lq && rows.length ? `${visible} / ${rows.length}` : '';
3329
+ }
3330
+
3331
+ // ── feature 32: keyboard shortcut help modal ──────────────
3332
+ function openShortcutHelp() { document.getElementById('shortcut-modal').classList.add('open'); }
3333
+ function closeShortcutHelp() { document.getElementById('shortcut-modal').classList.remove('open'); }
3334
+
3335
+ // ── feature 33: "currently working on" inference ──────────
3336
+ function updateCurrentActivity(events) {
3337
+ const el = document.getElementById('topbar-activity');
3338
+ if (!el) return;
3339
+ if (!events?.length) { el.textContent = ''; el.classList.remove('loaded'); return; }
3340
+ const recent = [...events].reverse().find(ev => ev.kind === 'tool');
3341
+ if (!recent) { el.textContent = ''; el.classList.remove('loaded'); return; }
3342
+ const name = recent.name || '';
3343
+ let activity = '';
3344
+ if (['Write', 'Edit', 'Read', 'MultiEdit'].includes(name)) {
3345
+ const lbl = recent.label || '';
3346
+ const match = lbl.match(/([^/\\]+\.[a-zA-Z0-9]+)/) ;
3347
+ activity = match ? match[1] : (lbl.split('/').pop() || name);
3348
+ } else if (name === 'Bash') {
3349
+ activity = 'bash: ' + (recent.label || '').slice(0, 24);
3350
+ } else if (name === 'WebSearch' || name === 'WebFetch') {
3351
+ activity = 'web: ' + (recent.label || '').slice(0, 20);
3352
+ } else if (name) {
3353
+ activity = name;
3354
+ }
3355
+ if (activity) { el.textContent = '⤷ ' + activity; el.classList.add('loaded'); }
3356
+ else { el.textContent = ''; el.classList.remove('loaded'); }
3357
+ }
3358
+
3359
+ // ── feature 34: prompt pattern analysis ───────────────────
3360
+ const STOP_WORDS = new Set(['the','a','an','and','or','but','in','on','at','to','for','of','with','by','from','is','it','this','that','be','as','i','my','me','we','you','your','can','do','did','have','had','has','was','were','are','will','would','should','could','not','no','if','so','then','what','how','when','where','which','who','all','get','make','add','new','old','use','its','into','out','up','any','just','let','set','also','more','using','used','fix','run','file','please']);
3361
+ let patternsOpen = false;
3362
+ function togglePatterns() {
3363
+ patternsOpen = !patternsOpen;
3364
+ document.getElementById('btn-patterns').classList.toggle('on', patternsOpen);
3365
+ const p = document.getElementById('patterns-panel');
3366
+ p.style.display = patternsOpen ? '' : 'none';
3367
+ if (patternsOpen) buildPatterns();
3368
+ }
3369
+ function buildPatterns() {
3370
+ const el = document.getElementById('patterns-body');
3371
+ if (!allSessions.length) { el.innerHTML = '<div class="loading-txt">No sessions loaded</div>'; return; }
3372
+ const freq = {};
3373
+ for (const s of allSessions) {
3374
+ const words = (s.lastPrompt || '').toLowerCase().match(/\b[a-z]{4,}\b/g) || [];
3375
+ const seen = new Set();
3376
+ for (const w of words) {
3377
+ if (!STOP_WORDS.has(w) && !seen.has(w)) { freq[w] = (freq[w] || 0) + 1; seen.add(w); }
3378
+ }
3379
+ }
3380
+ const sorted = Object.entries(freq).filter(([,c]) => c >= 2).sort((a, b) => b[1] - a[1]).slice(0, 20);
3381
+ if (!sorted.length) { el.innerHTML = '<div class="loading-txt">Not enough prompt data</div>'; return; }
3382
+ const maxCount = sorted[0][1];
3383
+ const rows = sorted.map(([word, count], i) => {
3384
+ const barW = Math.round((count / maxCount) * 100);
3385
+ return `<tr><td class="lb-rank">${i + 1}</td>
3386
+ <td style="font-size:12px;color:var(--text-mid)">${esc(word)}</td>
3387
+ <td style="width:100px;padding:4px 6px"><div style="height:4px;background:var(--surface-hi);border-radius:2px;overflow:hidden">
3388
+ <div style="height:100%;width:${barW}%;background:oklch(70% 0.15 300);border-radius:2px"></div></div></td>
3389
+ <td class="lb-cost" style="color:var(--text-mid)">${count}</td></tr>`;
3390
+ }).join('');
3391
+ el.innerHTML = `<table class="lb-table"><thead><tr>
3392
+ <th class="lb-rank">#</th><th>Term</th><th></th><th class="lb-cost">Sessions</th>
3393
+ </tr></thead><tbody>${rows}</tbody></table>`;
3394
+ }
3395
+
3396
+ // ── feature 35: session streak tracker ────────────────────
3397
+ function calcStreak() {
3398
+ const dates = new Set(allSessions.map(s => {
3399
+ const t = s.firstTs || s.mtime;
3400
+ if (!t) return null;
3401
+ return new Date(typeof t === 'number' ? t : t).toDateString();
3402
+ }).filter(Boolean));
3403
+ let streak = 0;
3404
+ const today = new Date();
3405
+ for (let i = 0; i <= 365; i++) {
3406
+ const d = new Date(today);
3407
+ d.setDate(d.getDate() - i);
3408
+ if (dates.has(d.toDateString())) {
3409
+ streak++;
3410
+ } else if (i > 0) {
3411
+ break;
3412
+ }
3413
+ }
3414
+ return streak;
3415
+ }
3416
+
3150
3417
  // ── feature 25: notification toasts ──────────────────────
3151
3418
  let _toastLastBudgetKey = '';
3152
3419
  function showToast(title, msg, type = 'info', duration = 5000) {
@@ -372,6 +372,7 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
372
372
  const id = f.replace('.jsonl', '');
373
373
  let lastPrompt = '', summaries = [], totalDurationMs = 0, totalMessages = 0, firstTs = null, lastTs = null, totalCost = 0, toolCalls = 0, userMessages = 0, cacheReadTokens = 0, totalInputTokens = 0;
374
374
  const modelBreakdown = {};
375
+ const filesTouchedSet = new Set();
375
376
  try {
376
377
  const lines = fs.readFileSync(fp, 'utf8').split('\n').filter(Boolean);
377
378
  let pendingCompact = false;
@@ -394,7 +395,12 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
394
395
  if (e.type === 'assistant') {
395
396
  const msg = e.message || {};
396
397
  for (const block of (msg.content || [])) {
397
- if (block && block.type === 'tool_use') toolCalls++;
398
+ if (block && block.type === 'tool_use') {
399
+ toolCalls++;
400
+ if (['Write','Edit','Read','MultiEdit'].includes(block.name) && block.input?.file_path) {
401
+ filesTouchedSet.add(path.basename(block.input.file_path));
402
+ }
403
+ }
398
404
  }
399
405
  if (msg.usage && msg.model) {
400
406
  const c = _sjCalcCost(msg.model, msg.usage);
@@ -415,7 +421,8 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
415
421
  }
416
422
  }
417
423
  } catch {}
418
- sessions.push({ id, mtime, firstTs, lastTs, lastPrompt, summaries, totalDurationMs, totalMessages, totalCost, toolCalls, userMessages, cacheReadTokens, totalInputTokens, modelBreakdown, file: fp });
424
+ const filesTouched = [...filesTouchedSet].slice(0, 20);
425
+ sessions.push({ id, mtime, firstTs, lastTs, lastPrompt, summaries, totalDurationMs, totalMessages, totalCost, toolCalls, userMessages, cacheReadTokens, totalInputTokens, modelBreakdown, filesTouched, file: fp });
419
426
  }
420
427
  res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-cache' });
421
428
  res.end(JSON.stringify({ sessions }));
@@ -978,6 +985,51 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
978
985
  return;
979
986
  }
980
987
 
988
+ // ---------------------------------------------------------- POST /api/loops/create
989
+ if (req.method === 'POST' && url === '/api/loops/create') {
990
+ let body = '';
991
+ req.on('data', chunk => { body += chunk; });
992
+ req.on('end', () => {
993
+ try {
994
+ const { name, prompt, interval, maxReps } = JSON.parse(body);
995
+ if (!prompt) { res.writeHead(400); res.end(JSON.stringify({ error: 'prompt required' })); return; }
996
+ const loopsDir = path.join(projectDir || process.cwd(), '.monomind', 'loops');
997
+ fs.mkdirSync(loopsDir, { recursive: true });
998
+ const id = `loop-${Date.now()}-${Math.random().toString(36).slice(2,7)}`;
999
+ const loop = { id, name: name || prompt.slice(0, 40), prompt, interval: interval || '1h', maxReps: maxReps || null, status: 'active', currentRep: 0, startedAt: new Date().toISOString(), lastRunAt: null };
1000
+ fs.writeFileSync(path.join(loopsDir, `${id}.json`), JSON.stringify(loop, null, 2));
1001
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
1002
+ res.end(JSON.stringify({ ok: true, id }));
1003
+ } catch (err) { res.writeHead(500); res.end(JSON.stringify({ error: err.message })); }
1004
+ });
1005
+ return;
1006
+ }
1007
+
1008
+ // ---------------------------------------------------------- GET /api/events-stream (SSE)
1009
+ if (req.method === 'GET' && url.startsWith('/api/events-stream')) {
1010
+ const qs = new URL(req.url, 'http://localhost').searchParams;
1011
+ const d = path.resolve(qs.get('dir') || projectDir || process.cwd());
1012
+ const slug = d.replace(/\//g, '-');
1013
+ const projectClaudeDir = path.join(os.homedir(), '.claude', 'projects', slug);
1014
+ res.writeHead(200, {
1015
+ 'Content-Type': 'text/event-stream',
1016
+ 'Cache-Control': 'no-cache',
1017
+ 'Connection': 'keep-alive',
1018
+ 'Access-Control-Allow-Origin': '*',
1019
+ });
1020
+ const send = (ev, data) => { try { res.write(`event: ${ev}\ndata: ${JSON.stringify(data)}\n\n`); } catch {} };
1021
+ send('connected', { ts: Date.now() });
1022
+ let watcher = null;
1023
+ try {
1024
+ watcher = fs.watch(projectClaudeDir, { persistent: false }, (evtype) => {
1025
+ if (evtype === 'change' || evtype === 'rename') send('update', { ts: Date.now() });
1026
+ });
1027
+ } catch {}
1028
+ const pingInterval = setInterval(() => { try { res.write(': ping\n\n'); } catch {} }, 20000);
1029
+ req.on('close', () => { clearInterval(pingInterval); try { watcher?.close(); } catch {} });
1030
+ return;
1031
+ }
1032
+
981
1033
  // ------------------------------------------------------- DELETE /api/knowledge-chunk
982
1034
  if (req.method === 'DELETE' && url === '/api/knowledge-chunk') {
983
1035
  let body = '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monoes/monomindcli",
3
- "version": "1.10.38",
3
+ "version": "1.10.39",
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",