@monoes/monomindcli 1.10.31 → 1.10.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/browse.md +6 -17
- package/.claude/helpers/handlers/budget-status-handler.cjs +14 -0
- package/.claude/helpers/handlers/compact-handler.cjs +33 -0
- package/.claude/helpers/handlers/loops-status-handler.cjs +45 -0
- package/.claude/helpers/handlers/session-restore-handler.cjs +21 -15
- package/.claude/helpers/handlers/stats-handler.cjs +14 -0
- package/.claude/helpers/hook-handler.cjs +10 -70
- package/.claude/skills/agent-browser-testing/SKILL.md +149 -151
- package/.claude/skills/monomind/browse-agentcore.md +20 -21
- package/.claude/skills/monomind/browse-electron.md +45 -46
- package/.claude/skills/monomind/browse-qa.md +29 -30
- package/.claude/skills/monomind/browse-references/authentication.md +39 -40
- package/.claude/skills/monomind/browse-references/trust-boundaries.md +1 -2
- package/.claude/skills/monomind/browse-references/video-recording.md +23 -24
- package/.claude/skills/monomind/browse-slack.md +52 -53
- package/.claude/skills/monomind/browse-vercel.md +26 -27
- package/.claude/skills/monomind/browse.md +273 -273
- package/dist/src/ui/dashboard-v2.html +401 -4
- package/package.json +1 -1
|
@@ -388,6 +388,57 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
388
388
|
|
|
389
389
|
/* ── forecast row ────────────────────────────────────────── */
|
|
390
390
|
.m-val.forecast { color:var(--text-lo); font-size:11px; font-weight:500; }
|
|
391
|
+
|
|
392
|
+
/* ── auto-tags ───────────────────────────────────────────── */
|
|
393
|
+
.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
|
+
.tag-filter-bar { display:flex; flex-wrap:wrap; gap:5px; margin-bottom:12px; }
|
|
395
|
+
.tag-chip { 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; }
|
|
396
|
+
.tag-chip:hover { color:var(--text-hi); }
|
|
397
|
+
.tag-chip.active { background:var(--accent-dim); color:var(--accent); border-color:oklch(72% 0.18 75 / 0.3); }
|
|
398
|
+
|
|
399
|
+
/* ── session recap ───────────────────────────────────────── */
|
|
400
|
+
#feed-recap { display:none; flex-shrink:0; padding:8px 18px; border-bottom:1px solid var(--border); background:oklch(14% 0.009 55); }
|
|
401
|
+
#feed-recap.show { display:block; }
|
|
402
|
+
.recap-text { font-size:12px; color:var(--text-mid); line-height:1.6; }
|
|
403
|
+
.recap-stat { display:inline-flex; align-items:center; gap:4px; font-size:11px; padding:1px 7px; border-radius:8px; margin-right:5px; }
|
|
404
|
+
.recap-stat.rs-tool { background:oklch(65% 0.15 150 / 0.1); color:oklch(65% 0.15 150); }
|
|
405
|
+
.recap-stat.rs-cost { background:oklch(72% 0.18 75 / 0.1); color:oklch(78% 0.18 75); }
|
|
406
|
+
.recap-stat.rs-err { background:oklch(60% 0.18 25 / 0.1); color:oklch(70% 0.18 25); }
|
|
407
|
+
.recap-stat.rs-user { background:var(--surface-hi); color:var(--text-lo); }
|
|
408
|
+
|
|
409
|
+
/* ── replay mode ─────────────────────────────────────────── */
|
|
410
|
+
#replay-bar { display:none; flex-shrink:0; align-items:center; gap:8px; padding:5px 18px; border-bottom:1px solid var(--border); background:oklch(13% 0.009 55); }
|
|
411
|
+
#replay-bar.show { display:flex; }
|
|
412
|
+
.rp-btn { font-size:11px; background:var(--surface); border:1px solid var(--border); border-radius:4px; padding:2px 8px; cursor:pointer; color:var(--text-lo); transition:color 0.1s; }
|
|
413
|
+
.rp-btn:hover { color:var(--text-hi); }
|
|
414
|
+
.rp-btn.active { color:var(--accent); border-color:oklch(72% 0.18 75 / 0.5); }
|
|
415
|
+
#rp-progress { flex:1; height:3px; background:var(--surface-hi); border-radius:2px; overflow:hidden; }
|
|
416
|
+
#rp-fill { height:100%; background:var(--accent); border-radius:2px; transition:width 0.15s; }
|
|
417
|
+
#rp-counter { font-size:11px; color:var(--text-lo); font-family:var(--mono); white-space:nowrap; }
|
|
418
|
+
|
|
419
|
+
/* ── global feed (multi-project) ─────────────────────────── */
|
|
420
|
+
.gf-proj-tag { font-size:10px; padding:1px 6px; border-radius:6px; background:var(--surface-hi); color:var(--text-lo); white-space:nowrap; flex-shrink:0; margin-top:3px; }
|
|
421
|
+
|
|
422
|
+
/* ── project health score ────────────────────────────────── */
|
|
423
|
+
.proj-health { position:absolute; bottom:12px; right:12px; font-size:11px; font-weight:700; width:28px; height:28px; border-radius:50%; display:flex; align-items:center; justify-content:center; }
|
|
424
|
+
.ph-hi { background:oklch(65% 0.15 150 / 0.15); color:oklch(65% 0.15 150); }
|
|
425
|
+
.ph-mid { background:oklch(72% 0.18 75 / 0.15); color:oklch(78% 0.18 75); }
|
|
426
|
+
.ph-lo { background:oklch(60% 0.18 25 / 0.12); color:oklch(70% 0.18 25); }
|
|
427
|
+
|
|
428
|
+
/* ── budget cap ──────────────────────────────────────────── */
|
|
429
|
+
#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
|
+
#budget-modal.open { display:flex; }
|
|
431
|
+
#budget-box { background:oklch(16% 0.009 55); border:1px solid oklch(32% 0.008 55); border-radius:10px; padding:22px 24px; width:320px; box-shadow:0 24px 60px oklch(5% 0 0 / 0.7); }
|
|
432
|
+
.bm-title { font-size:14px; font-weight:600; color:var(--text-hi); margin-bottom:14px; }
|
|
433
|
+
.bm-row { display:flex; flex-direction:column; gap:4px; margin-bottom:12px; }
|
|
434
|
+
.bm-lbl { font-size:11px; color:var(--text-lo); }
|
|
435
|
+
.bm-input { background:var(--surface); border:1px solid var(--border); border-radius:var(--r); padding:6px 10px; font-size:13px; color:var(--text-hi); font-family:var(--mono); outline:none; transition:border-color 0.1s; }
|
|
436
|
+
.bm-input:focus { border-color:var(--accent); }
|
|
437
|
+
.bm-btns { display:flex; gap:8px; margin-top:16px; }
|
|
438
|
+
.bm-save { flex:1; background:var(--accent); border:none; border-radius:var(--r); padding:7px 0; font-size:13px; color:oklch(11% 0.009 55); font-weight:600; cursor:pointer; }
|
|
439
|
+
.bm-save:hover { background:oklch(68% 0.18 75); }
|
|
440
|
+
.bm-cancel { background:transparent; border:1px solid var(--border); border-radius:var(--r); padding:7px 12px; font-size:13px; color:var(--text-lo); cursor:pointer; }
|
|
441
|
+
.bm-cancel:hover { color:var(--text-hi); }
|
|
391
442
|
</style>
|
|
392
443
|
</head>
|
|
393
444
|
<body>
|
|
@@ -428,6 +479,9 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
428
479
|
<div class="nav-item" data-view="orgs">
|
|
429
480
|
<span class="ico">⬡</span><span class="lbl">Orgs</span>
|
|
430
481
|
</div>
|
|
482
|
+
<div class="nav-item" data-view="global">
|
|
483
|
+
<span class="ico">⊕</span><span class="lbl">Global Feed</span>
|
|
484
|
+
</div>
|
|
431
485
|
</div>
|
|
432
486
|
</div>
|
|
433
487
|
<div id="sb-footer">
|
|
@@ -443,6 +497,7 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
443
497
|
<span class="pill"><span class="live-dot"></span> live</span>
|
|
444
498
|
<span id="topbar-cost"></span>
|
|
445
499
|
<div id="tb-right">
|
|
500
|
+
<button class="btn" id="btn-budget" onclick="openBudgetModal()" title="Set daily/monthly cost budget">⚑ Budget</button>
|
|
446
501
|
<button class="btn" onclick="openCmdPalette()">⌕ Search <kbd style="font-size:10px;opacity:0.6;margin-left:3px">⌘K</kbd></button>
|
|
447
502
|
<button class="btn" onclick="refreshCurrent()">↺ Refresh</button>
|
|
448
503
|
</div>
|
|
@@ -476,6 +531,7 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
476
531
|
<span id="feed-sess">—</span>
|
|
477
532
|
<div id="feed-sess-nav">
|
|
478
533
|
<button class="live-tail-btn" id="btn-live-tail" onclick="toggleLiveTail()" title="Toggle live tail (auto-scroll + 5s refresh)">⬤ Tail</button>
|
|
534
|
+
<button class="sess-copy-btn" id="btn-replay" onclick="replayToggle()" title="Replay session event-by-event">⏵ Replay</button>
|
|
479
535
|
<button class="sess-copy-btn" id="btn-copy-sess" onclick="copySession()" title="Copy session as markdown">⎘ Copy</button>
|
|
480
536
|
<button class="density-btn" id="btn-density" onclick="toggleDensity()" title="Toggle compact view">⊟</button>
|
|
481
537
|
<button class="sess-btn" onclick="toggleFeedSearch()" title="Search in feed (/)">⌕</button>
|
|
@@ -494,6 +550,15 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
494
550
|
<span class="sctx-label" id="sctx-label"></span>
|
|
495
551
|
<button class="sctx-live" onclick="goLive()">⬤ Go live</button>
|
|
496
552
|
</div>
|
|
553
|
+
<div id="feed-recap"></div>
|
|
554
|
+
<div id="replay-bar">
|
|
555
|
+
<button class="rp-btn" id="rp-play" onclick="replayToggle()" title="Play/pause replay">▶</button>
|
|
556
|
+
<button class="rp-btn" onclick="replayStep(-1)" title="Step back">‹</button>
|
|
557
|
+
<button class="rp-btn" onclick="replayStep(1)" title="Step forward">›</button>
|
|
558
|
+
<div id="rp-progress"><div id="rp-fill" style="width:0%"></div></div>
|
|
559
|
+
<span id="rp-counter">0 / 0</span>
|
|
560
|
+
<button class="rp-btn" onclick="stopReplay()" title="Exit replay">✕</button>
|
|
561
|
+
</div>
|
|
497
562
|
<div id="feed-timeline" title="Session tool activity timeline"></div>
|
|
498
563
|
<div id="feed-time-filter">
|
|
499
564
|
<span class="tf-lbl">Range</span>
|
|
@@ -592,10 +657,40 @@ html, body { height: 100%; background: var(--bg); color: var(--text-hi); font-fa
|
|
|
592
657
|
</div>
|
|
593
658
|
</div>
|
|
594
659
|
|
|
660
|
+
<!-- GLOBAL FEED -->
|
|
661
|
+
<div class="view" id="view-global">
|
|
662
|
+
<div class="vscroll">
|
|
663
|
+
<div style="display:flex;align-items:baseline;gap:10px;margin-bottom:4px">
|
|
664
|
+
<div class="pg-title" style="margin-bottom:0">Global Feed</div>
|
|
665
|
+
<span class="pg-sub" id="gf-sub" style="margin-bottom:0">Activity across all projects</span>
|
|
666
|
+
</div>
|
|
667
|
+
<div id="gf-content" style="margin-top:16px"><div class="loading-txt">Loading…</div></div>
|
|
668
|
+
</div>
|
|
669
|
+
</div>
|
|
670
|
+
|
|
595
671
|
</div><!-- /view-wrap -->
|
|
596
672
|
</div><!-- /main -->
|
|
597
673
|
</div><!-- /app -->
|
|
598
674
|
|
|
675
|
+
<!-- budget modal (fixed overlay, outside app) -->
|
|
676
|
+
<div id="budget-modal" onclick="if(event.target===this)closeBudgetModal()">
|
|
677
|
+
<div id="budget-box">
|
|
678
|
+
<div class="bm-title">Set Cost Budget</div>
|
|
679
|
+
<div class="bm-row">
|
|
680
|
+
<div class="bm-lbl">Daily limit ($)</div>
|
|
681
|
+
<input class="bm-input" id="bm-daily" type="number" min="0" step="1" placeholder="e.g. 20">
|
|
682
|
+
</div>
|
|
683
|
+
<div class="bm-row">
|
|
684
|
+
<div class="bm-lbl">Monthly limit ($)</div>
|
|
685
|
+
<input class="bm-input" id="bm-monthly" type="number" min="0" step="10" placeholder="e.g. 200">
|
|
686
|
+
</div>
|
|
687
|
+
<div class="bm-btns">
|
|
688
|
+
<button class="bm-cancel" onclick="closeBudgetModal()">Cancel</button>
|
|
689
|
+
<button class="bm-save" onclick="saveBudget()">Save</button>
|
|
690
|
+
</div>
|
|
691
|
+
</div>
|
|
692
|
+
</div>
|
|
693
|
+
|
|
599
694
|
<script>
|
|
600
695
|
// ── state ──────────────────────────────────────────────────
|
|
601
696
|
let DIR = '';
|
|
@@ -611,7 +706,7 @@ let userScrolled = false;
|
|
|
611
706
|
let selectedEntryId = null;
|
|
612
707
|
let allDrawers = [];
|
|
613
708
|
let dismissedAlerts = new Set();
|
|
614
|
-
let alertState = { todayCost: 0, errorCount: 0, longLoops: [], anomaly: null };
|
|
709
|
+
let alertState = { todayCost: 0, errorCount: 0, longLoops: [], anomaly: null, budgetAlert: null, budgetCls: 'alert-warn' };
|
|
615
710
|
let feedTimeFilter = 'all';
|
|
616
711
|
let cmdFocusIdx = 0;
|
|
617
712
|
let cmdItems = [];
|
|
@@ -635,7 +730,7 @@ function switchView(v) {
|
|
|
635
730
|
el.classList.toggle('active', el.dataset.view === v));
|
|
636
731
|
document.querySelectorAll('.view').forEach(el =>
|
|
637
732
|
el.classList.toggle('active', el.id === 'view-' + v));
|
|
638
|
-
const titles = { now:'Now', projects:'Projects', sessions:'Sessions', loops:'Loops', memory:'Memory', orgs:'Orgs' };
|
|
733
|
+
const titles = { now:'Now', projects:'Projects', sessions:'Sessions', loops:'Loops', memory:'Memory', orgs:'Orgs', global:'Global Feed' };
|
|
639
734
|
document.getElementById('view-title').textContent = titles[v] || v;
|
|
640
735
|
if (!viewRendered[v]) { renderView(v); viewRendered[v] = true; }
|
|
641
736
|
}
|
|
@@ -647,6 +742,7 @@ function renderView(v) {
|
|
|
647
742
|
if (v === 'loops') renderLoops();
|
|
648
743
|
if (v === 'memory') renderMemory();
|
|
649
744
|
if (v === 'orgs') renderOrgs();
|
|
745
|
+
if (v === 'global') renderGlobalFeed();
|
|
650
746
|
}
|
|
651
747
|
|
|
652
748
|
function refreshCurrent() {
|
|
@@ -666,6 +762,7 @@ async function init() {
|
|
|
666
762
|
document.getElementById('sb-proj').textContent = DIR.split('/').filter(Boolean).pop() || '—';
|
|
667
763
|
} catch (_) {}
|
|
668
764
|
viewRendered['now'] = true; // prevents switchView from re-rendering NOW on jumpToSession
|
|
765
|
+
updateBudgetBtnStyle();
|
|
669
766
|
await refreshNow();
|
|
670
767
|
startPolling();
|
|
671
768
|
}
|
|
@@ -882,6 +979,8 @@ function renderFeedEvents(events, silent) {
|
|
|
882
979
|
// update timeline + breakdown with all original events (before time-filter)
|
|
883
980
|
buildTimeline(filtered);
|
|
884
981
|
buildBreakdown(filtered);
|
|
982
|
+
// session recap card
|
|
983
|
+
buildRecap(filtered, allSessions[sessionIdx]);
|
|
885
984
|
}
|
|
886
985
|
|
|
887
986
|
function renderGroupRow(g) {
|
|
@@ -1039,6 +1138,10 @@ function updateAlerts() {
|
|
|
1039
1138
|
all.push({ id: 'anomaly-sess', cls: 'alert-warn', ico: '◎', msg: alertState.anomaly });
|
|
1040
1139
|
}
|
|
1041
1140
|
|
|
1141
|
+
if (alertState.budgetAlert) {
|
|
1142
|
+
all.push({ id: 'budget-alert', cls: alertState.budgetCls || 'alert-warn', ico: '⚑', msg: alertState.budgetAlert });
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1042
1145
|
for (const l of alertState.longLoops) {
|
|
1043
1146
|
all.push({ id: 'loop-' + l, cls: 'alert-warn', ico: '↺', msg: `Long-running loop: ${l}` });
|
|
1044
1147
|
}
|
|
@@ -1107,6 +1210,7 @@ async function loadTodayMetrics() {
|
|
|
1107
1210
|
const s = data?.tokens?.summary || {};
|
|
1108
1211
|
alertState.todayCost = typeof s.todayCost === 'number' ? s.todayCost : 0;
|
|
1109
1212
|
updateAlerts();
|
|
1213
|
+
checkBudget();
|
|
1110
1214
|
// topbar cost badge
|
|
1111
1215
|
const badge = document.getElementById('topbar-cost');
|
|
1112
1216
|
if (badge && typeof s.todayCost === 'number') {
|
|
@@ -1206,8 +1310,11 @@ function renderProjectGrid(projects, query) {
|
|
|
1206
1310
|
el.className = 'proj-grid';
|
|
1207
1311
|
el.innerHTML = filtered.map(p => {
|
|
1208
1312
|
const isCurrent = p.path === DIR;
|
|
1313
|
+
const score = computeHealthScore(p);
|
|
1314
|
+
const hCls = healthClass(score);
|
|
1209
1315
|
return `<div class="proj-card${isCurrent ? ' current' : ''}" onclick="switchProject('${esc(p.path || '')}')">
|
|
1210
1316
|
${isCurrent ? '<div class="proj-card-badge">active</div>' : ''}
|
|
1317
|
+
<div class="proj-health ${hCls}" title="Health score: ${score}">${score}</div>
|
|
1211
1318
|
<div class="proj-card-name">${esc(p.name || p.slug)}</div>
|
|
1212
1319
|
<div class="proj-card-path">${esc(p.path || '')}</div>
|
|
1213
1320
|
<div class="proj-card-stats">
|
|
@@ -1230,6 +1337,7 @@ async function renderSessions() {
|
|
|
1230
1337
|
try {
|
|
1231
1338
|
const { sessions = [] } = await apiFetch('/api/session-journal?dir=' + enc(DIR));
|
|
1232
1339
|
allSessions = sessions; // always sync — stale ordering breaks jumpToSession
|
|
1340
|
+
initTags();
|
|
1233
1341
|
document.getElementById('bdg-sessions').textContent = sessions.length || '—';
|
|
1234
1342
|
document.getElementById('sess-pg-sub').textContent =
|
|
1235
1343
|
sessions.length + ' session' + (sessions.length !== 1 ? 's' : '') + ' · ' + (DIR.split('/').pop() || DIR);
|
|
@@ -1237,7 +1345,8 @@ async function renderSessions() {
|
|
|
1237
1345
|
el.innerHTML = '<div class="empty"><div class="empty-ico">◫</div><div>No sessions yet</div></div>';
|
|
1238
1346
|
return;
|
|
1239
1347
|
}
|
|
1240
|
-
|
|
1348
|
+
let toShow = showStarredOnly ? sessions.filter(s => bookmarks.has(s.id)) : sessions;
|
|
1349
|
+
if (activeTagFilter) toShow = toShow.filter(s => (allTags.sessionTags.get(s.id) || []).includes(activeTagFilter));
|
|
1241
1350
|
if (!toShow.length) {
|
|
1242
1351
|
el.innerHTML = '<div class="empty"><div class="empty-ico">☆</div><div>No bookmarked sessions</div></div>';
|
|
1243
1352
|
return;
|
|
@@ -1249,6 +1358,7 @@ async function renderSessions() {
|
|
|
1249
1358
|
: typeof s.cost === 'number' ? '$' + s.cost.toFixed(2) : '';
|
|
1250
1359
|
const meta = [dur, msgs, cost].filter(Boolean).join(' · ') || s.id.slice(0, 16);
|
|
1251
1360
|
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
|
+
const autoTags = (allTags.sessionTags.get(s.id) || []).map(t => `<span class="sr-autotag">${esc(t)}</span>`).join('');
|
|
1252
1362
|
const isStarred = bookmarks.has(s.id);
|
|
1253
1363
|
return `<div class="sess-row" onclick="jumpToSession('${esc(s.id)}')">
|
|
1254
1364
|
<div class="sr-top">
|
|
@@ -1258,9 +1368,13 @@ async function renderSessions() {
|
|
|
1258
1368
|
<span class="sr-view">→ view</span>
|
|
1259
1369
|
</div>
|
|
1260
1370
|
<div class="sr-meta">${esc(meta)}</div>
|
|
1261
|
-
${summaries ? `<div class="sr-tags">${summaries}</div>` : ''}
|
|
1371
|
+
${(summaries || autoTags) ? `<div class="sr-tags">${summaries}${autoTags}</div>` : ''}
|
|
1262
1372
|
</div>`;
|
|
1263
1373
|
}).join('');
|
|
1374
|
+
// prepend tag filter bar if there are common tags
|
|
1375
|
+
if (allTags.common.size > 1) {
|
|
1376
|
+
el.innerHTML = buildTagFilterBar(toShow) + el.innerHTML;
|
|
1377
|
+
}
|
|
1264
1378
|
} catch (err) {
|
|
1265
1379
|
el.innerHTML = '<div class="empty">Could not load sessions: ' + esc(err.message) + '</div>';
|
|
1266
1380
|
}
|
|
@@ -1298,6 +1412,289 @@ function toggleSessStarFilter() {
|
|
|
1298
1412
|
viewRendered['sessions'] = true;
|
|
1299
1413
|
}
|
|
1300
1414
|
|
|
1415
|
+
// ── feature 1: auto-tags ───────────────────────────────────
|
|
1416
|
+
const STOP_WORDS = new Set('the a an and or but in on at to for of is are was were be been being have has had do does did will would could should may might shall can i you he she it we they this that these those with from by about as into through during before after above below up down out off over under again further then once here there when where why how all any both each few more most other some such no nor not only own same so than too very just because if although when while'.split(' '));
|
|
1417
|
+
|
|
1418
|
+
function extractTags(sessions) {
|
|
1419
|
+
// compute per-session tags from lastPrompt text
|
|
1420
|
+
const sessionTags = new Map();
|
|
1421
|
+
const globalFreq = {};
|
|
1422
|
+
for (const s of sessions) {
|
|
1423
|
+
const text = (s.lastPrompt || '').toLowerCase();
|
|
1424
|
+
const words = text.match(/\b[a-z][a-z0-9_-]{2,}\b/g) || [];
|
|
1425
|
+
const freq = {};
|
|
1426
|
+
for (const w of words) {
|
|
1427
|
+
if (!STOP_WORDS.has(w)) freq[w] = (freq[w] || 0) + 1;
|
|
1428
|
+
}
|
|
1429
|
+
// top 3 words for this session
|
|
1430
|
+
const top = Object.entries(freq).sort((a, b) => b[1] - a[1]).slice(0, 3).map(e => e[0]);
|
|
1431
|
+
sessionTags.set(s.id, top);
|
|
1432
|
+
for (const t of top) globalFreq[t] = (globalFreq[t] || 0) + 1;
|
|
1433
|
+
}
|
|
1434
|
+
// only keep tags that appear in 2+ sessions OR are in the current session
|
|
1435
|
+
const common = new Set(Object.entries(globalFreq).filter(([, v]) => v >= 2).map(([k]) => k));
|
|
1436
|
+
return { sessionTags, common };
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
let allTags = { sessionTags: new Map(), common: new Set() };
|
|
1440
|
+
let activeTagFilter = null;
|
|
1441
|
+
|
|
1442
|
+
function initTags() {
|
|
1443
|
+
allTags = extractTags(allSessions);
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
function buildTagFilterBar(sessions) {
|
|
1447
|
+
if (!allTags.common.size) return '';
|
|
1448
|
+
const sorted = [...allTags.common].sort();
|
|
1449
|
+
const chips = sorted.map(t =>
|
|
1450
|
+
`<button class="tag-chip${activeTagFilter === t ? ' active' : ''}" onclick="setTagFilter('${esc(t)}')">${esc(t)}</button>`
|
|
1451
|
+
).join('');
|
|
1452
|
+
return `<div class="tag-filter-bar">${chips}</div>`;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
function setTagFilter(tag) {
|
|
1456
|
+
activeTagFilter = activeTagFilter === tag ? null : tag;
|
|
1457
|
+
viewRendered['sessions'] = false;
|
|
1458
|
+
renderSessions();
|
|
1459
|
+
viewRendered['sessions'] = true;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// ── feature 2: session recap ───────────────────────────────
|
|
1463
|
+
function buildRecap(events, sess) {
|
|
1464
|
+
const recap = document.getElementById('feed-recap');
|
|
1465
|
+
if (!recap) return;
|
|
1466
|
+
const tools = events.filter(e => e.kind === 'tool');
|
|
1467
|
+
const users = events.filter(e => e.kind === 'user');
|
|
1468
|
+
const errors = events.filter(e => e._errored);
|
|
1469
|
+
if (!tools.length && !users.length) { recap.className = ''; return; }
|
|
1470
|
+
|
|
1471
|
+
// dominant tool category
|
|
1472
|
+
const cats = {};
|
|
1473
|
+
for (const e of tools) cats[e.cat || 'other'] = (cats[e.cat || 'other'] || 0) + 1;
|
|
1474
|
+
const topCat = Object.entries(cats).sort((a, b) => b[1] - a[1])[0];
|
|
1475
|
+
const topPct = topCat ? Math.round(topCat[1] / tools.length * 100) : 0;
|
|
1476
|
+
|
|
1477
|
+
const costStr = sess?.totalCost != null ? '$' + sess.totalCost.toFixed(2) : (sess?.cost != null ? '$' + sess.cost.toFixed(2) : null);
|
|
1478
|
+
|
|
1479
|
+
const stats = [
|
|
1480
|
+
tools.length ? `<span class="recap-stat rs-tool">${tools.length} tool calls${topCat ? ' · ' + topPct + '% ' + topCat[0] : ''}</span>` : '',
|
|
1481
|
+
users.length ? `<span class="recap-stat rs-user">${users.length} message${users.length !== 1 ? 's' : ''}</span>` : '',
|
|
1482
|
+
costStr ? `<span class="recap-stat rs-cost">${costStr}</span>` : '',
|
|
1483
|
+
errors.length ? `<span class="recap-stat rs-err">${errors.length} error${errors.length !== 1 ? 's' : ''}</span>` : '',
|
|
1484
|
+
].filter(Boolean).join('');
|
|
1485
|
+
|
|
1486
|
+
recap.innerHTML = `<div class="recap-text">${stats}</div>`;
|
|
1487
|
+
recap.className = 'show';
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// ── feature 3: global feed ─────────────────────────────────
|
|
1491
|
+
async function renderGlobalFeed() {
|
|
1492
|
+
const el = document.getElementById('gf-content');
|
|
1493
|
+
el.innerHTML = '<div class="loading-txt">Loading all projects…</div>';
|
|
1494
|
+
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);
|
|
1498
|
+
if (!projects.length) {
|
|
1499
|
+
el.innerHTML = '<div class="empty"><div class="empty-ico">⊕</div><div>No projects found</div></div>';
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
document.getElementById('gf-sub').textContent = `Last activity across ${projects.length} projects`;
|
|
1503
|
+
|
|
1504
|
+
// fetch sessions for each project in parallel
|
|
1505
|
+
const results = await Promise.allSettled(
|
|
1506
|
+
projects.map(p => apiFetch('/api/session-journal?dir=' + enc(p.path)).then(d => ({ project: p, sessions: d.sessions || [] })))
|
|
1507
|
+
);
|
|
1508
|
+
|
|
1509
|
+
// flatten + sort by recency
|
|
1510
|
+
const entries = [];
|
|
1511
|
+
for (const r of results) {
|
|
1512
|
+
if (r.status !== 'fulfilled') continue;
|
|
1513
|
+
const { project, sessions } = r.value;
|
|
1514
|
+
for (const s of sessions.slice(0, 3)) {
|
|
1515
|
+
entries.push({ project, session: s });
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
entries.sort((a, b) => {
|
|
1519
|
+
const ta = a.session.lastTs || a.session.mtime || 0;
|
|
1520
|
+
const tb = b.session.lastTs || b.session.mtime || 0;
|
|
1521
|
+
return (typeof tb === 'number' ? tb : new Date(tb).getTime()) - (typeof ta === 'number' ? ta : new Date(ta).getTime());
|
|
1522
|
+
});
|
|
1523
|
+
|
|
1524
|
+
if (!entries.length) {
|
|
1525
|
+
el.innerHTML = '<div class="empty"><div class="empty-ico">⊕</div><div>No sessions found</div></div>';
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
el.innerHTML = '<div class="sess-list">' + entries.map(({ project, session: s }) => {
|
|
1530
|
+
const projName = project.name || project.slug || project.path?.split('/').pop() || '?';
|
|
1531
|
+
const dur = s.totalDurationMs ? fmtDur(s.totalDurationMs) : '';
|
|
1532
|
+
const cost = typeof s.totalCost === 'number' ? '$' + s.totalCost.toFixed(2) : '';
|
|
1533
|
+
const meta = [dur, cost].filter(Boolean).join(' · ') || s.id.slice(0, 12);
|
|
1534
|
+
return `<div class="sess-row" onclick="switchProject('${esc(project.path)}');setTimeout(()=>jumpToSession('${esc(s.id)}'),150)">
|
|
1535
|
+
<div class="sr-top">
|
|
1536
|
+
<div class="sr-prompt">${esc(s.lastPrompt || s.id)}</div>
|
|
1537
|
+
<div class="sr-time">${relTime(s.lastTs || s.mtime)}</div>
|
|
1538
|
+
<span class="gf-proj-tag">${esc(projName)}</span>
|
|
1539
|
+
</div>
|
|
1540
|
+
<div class="sr-meta">${esc(meta)}</div>
|
|
1541
|
+
</div>`;
|
|
1542
|
+
}).join('') + '</div>';
|
|
1543
|
+
} catch (err) {
|
|
1544
|
+
el.innerHTML = '<div class="empty">Could not load: ' + esc(err.message) + '</div>';
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
// ── feature 4: budget cap + desktop notification ───────────
|
|
1549
|
+
let budget = JSON.parse(localStorage.getItem('mm-budget') || '{}');
|
|
1550
|
+
|
|
1551
|
+
function openBudgetModal() {
|
|
1552
|
+
const b = budget;
|
|
1553
|
+
document.getElementById('bm-daily').value = b.daily || '';
|
|
1554
|
+
document.getElementById('bm-monthly').value = b.monthly || '';
|
|
1555
|
+
document.getElementById('budget-modal').classList.add('open');
|
|
1556
|
+
document.getElementById('bm-daily').focus();
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
function closeBudgetModal() {
|
|
1560
|
+
document.getElementById('budget-modal').classList.remove('open');
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
function saveBudget() {
|
|
1564
|
+
budget = {
|
|
1565
|
+
daily: parseFloat(document.getElementById('bm-daily').value) || null,
|
|
1566
|
+
monthly: parseFloat(document.getElementById('bm-monthly').value) || null,
|
|
1567
|
+
};
|
|
1568
|
+
localStorage.setItem('mm-budget', JSON.stringify(budget));
|
|
1569
|
+
closeBudgetModal();
|
|
1570
|
+
checkBudget(); // check immediately
|
|
1571
|
+
updateBudgetBtnStyle();
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
function updateBudgetBtnStyle() {
|
|
1575
|
+
const btn = document.getElementById('btn-budget');
|
|
1576
|
+
if (!btn) return;
|
|
1577
|
+
const hasBudget = budget.daily || budget.monthly;
|
|
1578
|
+
btn.style.color = hasBudget ? 'var(--accent)' : '';
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
function checkBudget() {
|
|
1582
|
+
const cost = alertState.todayCost;
|
|
1583
|
+
if (!cost) return;
|
|
1584
|
+
if (budget.daily) {
|
|
1585
|
+
const pct = cost / budget.daily;
|
|
1586
|
+
if (pct >= 1 && !dismissedAlerts.has('budget-daily-over')) {
|
|
1587
|
+
alertState.budgetAlert = `Daily budget exceeded: $${cost.toFixed(2)} / $${budget.daily}`;
|
|
1588
|
+
alertState.budgetCls = 'alert-crit';
|
|
1589
|
+
} else if (pct >= 0.8 && !dismissedAlerts.has('budget-daily-warn')) {
|
|
1590
|
+
alertState.budgetAlert = `Approaching daily budget: $${cost.toFixed(2)} / $${budget.daily}`;
|
|
1591
|
+
alertState.budgetCls = 'alert-warn';
|
|
1592
|
+
maybeNotify('monomind budget', `$${cost.toFixed(2)} of $${budget.daily} daily budget used`);
|
|
1593
|
+
} else {
|
|
1594
|
+
alertState.budgetAlert = null;
|
|
1595
|
+
}
|
|
1596
|
+
updateAlerts();
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
function maybeNotify(title, body) {
|
|
1601
|
+
if (!('Notification' in window)) return;
|
|
1602
|
+
if (Notification.permission === 'granted') {
|
|
1603
|
+
new Notification(title, { body, icon: '' });
|
|
1604
|
+
} else if (Notification.permission !== 'denied') {
|
|
1605
|
+
Notification.requestPermission().then(p => { if (p === 'granted') new Notification(title, { body }); });
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// ── feature 5: session replay ──────────────────────────────
|
|
1610
|
+
let replayEvents = [];
|
|
1611
|
+
let replayIdx = 0;
|
|
1612
|
+
let replayActive = false;
|
|
1613
|
+
let replayTimer = null;
|
|
1614
|
+
|
|
1615
|
+
function startReplay() {
|
|
1616
|
+
// collect visible feed entries as ordered list
|
|
1617
|
+
const entries = [...document.querySelectorAll('#feed-content .feed-entry')];
|
|
1618
|
+
if (!entries.length) return;
|
|
1619
|
+
replayEvents = entries;
|
|
1620
|
+
replayIdx = 0;
|
|
1621
|
+
replayActive = false;
|
|
1622
|
+
document.getElementById('replay-bar').classList.add('show');
|
|
1623
|
+
// dim all entries
|
|
1624
|
+
entries.forEach(el => { el.style.opacity = '0.2'; el.style.transition = 'opacity 0.15s'; });
|
|
1625
|
+
highlightReplayEntry();
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
function stopReplay() {
|
|
1629
|
+
clearInterval(replayTimer);
|
|
1630
|
+
replayActive = false;
|
|
1631
|
+
replayEvents.forEach(el => { el.style.opacity = ''; el.style.transition = ''; });
|
|
1632
|
+
replayEvents = [];
|
|
1633
|
+
document.getElementById('replay-bar').classList.remove('show');
|
|
1634
|
+
document.getElementById('rp-play').textContent = '▶';
|
|
1635
|
+
document.getElementById('rp-play').classList.remove('active');
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
function highlightReplayEntry() {
|
|
1639
|
+
replayEvents.forEach((el, i) => {
|
|
1640
|
+
el.style.opacity = i === replayIdx ? '1' : (i < replayIdx ? '0.5' : '0.2');
|
|
1641
|
+
});
|
|
1642
|
+
const total = replayEvents.length;
|
|
1643
|
+
const pct = total > 1 ? Math.round(replayIdx / (total - 1) * 100) : 100;
|
|
1644
|
+
document.getElementById('rp-fill').style.width = pct + '%';
|
|
1645
|
+
document.getElementById('rp-counter').textContent = `${replayIdx + 1} / ${total}`;
|
|
1646
|
+
// scroll into view
|
|
1647
|
+
replayEvents[replayIdx]?.scrollIntoView({ block: 'nearest' });
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
function replayStep(dir) {
|
|
1651
|
+
replayIdx = Math.max(0, Math.min(replayEvents.length - 1, replayIdx + dir));
|
|
1652
|
+
highlightReplayEntry();
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
function replayToggle() {
|
|
1656
|
+
if (!replayEvents.length) { startReplay(); return; }
|
|
1657
|
+
replayActive = !replayActive;
|
|
1658
|
+
const btn = document.getElementById('rp-play');
|
|
1659
|
+
btn.textContent = replayActive ? '⏸' : '▶';
|
|
1660
|
+
btn.classList.toggle('active', replayActive);
|
|
1661
|
+
if (replayActive) {
|
|
1662
|
+
replayTimer = setInterval(() => {
|
|
1663
|
+
if (replayIdx >= replayEvents.length - 1) { replayToggle(); return; }
|
|
1664
|
+
replayStep(1);
|
|
1665
|
+
}, 600);
|
|
1666
|
+
} else {
|
|
1667
|
+
clearInterval(replayTimer);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// ── feature 6: project health score ───────────────────────
|
|
1672
|
+
function computeHealthScore(p) {
|
|
1673
|
+
let score = 50; // base
|
|
1674
|
+
const now = Date.now();
|
|
1675
|
+
const DAY = 86400000;
|
|
1676
|
+
// recency: up to +30 points for activity in last 7 days
|
|
1677
|
+
if (p.lastActivity) {
|
|
1678
|
+
const age = now - (typeof p.lastActivity === 'number' ? p.lastActivity : new Date(p.lastActivity).getTime());
|
|
1679
|
+
if (age < DAY) score += 30;
|
|
1680
|
+
else if (age < 3*DAY) score += 20;
|
|
1681
|
+
else if (age < 7*DAY) score += 10;
|
|
1682
|
+
else if (age > 30*DAY) score -= 15;
|
|
1683
|
+
}
|
|
1684
|
+
// session count: up to +15
|
|
1685
|
+
const sc = p.sessionCount || 0;
|
|
1686
|
+
score += Math.min(15, sc * 2);
|
|
1687
|
+
// memory: up to +5
|
|
1688
|
+
score += Math.min(5, (p.memoryCount || 0));
|
|
1689
|
+
return Math.max(0, Math.min(99, Math.round(score)));
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
function healthClass(score) {
|
|
1693
|
+
if (score >= 70) return 'ph-hi';
|
|
1694
|
+
if (score >= 40) return 'ph-mid';
|
|
1695
|
+
return 'ph-lo';
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1301
1698
|
// ── loops ──────────────────────────────────────────────────
|
|
1302
1699
|
async function renderLoops() {
|
|
1303
1700
|
const el = document.getElementById('loops-content');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monoes/monomindcli",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.32",
|
|
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",
|