@monoes/monomindcli 1.10.13 → 1.10.15

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ description: Show current budget status — today, month, limits, autotuned flag
3
+ ---
4
+
5
+ ```bash
6
+ node "$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs" budget-status
7
+ ```
@@ -0,0 +1,7 @@
1
+ ---
2
+ description: Single-line graph stats — nodes, edges, freshness, usage %
3
+ ---
4
+
5
+ ```bash
6
+ node "$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs" graph-status
7
+ ```
@@ -0,0 +1,7 @@
1
+ ---
2
+ description: List active loops — command, type, run count, HIL status
3
+ ---
4
+
5
+ ```bash
6
+ node "$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs" loops-status
7
+ ```
@@ -81,11 +81,28 @@ try {
81
81
  // Write lock file; the build process removes it on completion
82
82
  try { fs.writeFileSync(lockPath, String(process.pid)); } catch { /* non-fatal */ }
83
83
 
84
- // Spawn a detached node process to run buildAsync from @monoes/monograph (ESM)
84
+ // Spawn a detached node process to run buildAsync from @monoes/monograph (ESM).
85
+ // After the build, VACUUM the DB if it has >50% bloat (reclaim space from
86
+ // delete/insert churn; opens are ~5x faster on a tight DB).
87
+ const dbPathStr = JSON.stringify(path.join(projectDir, '.monomind', 'monograph.db'));
85
88
  const script = `
86
89
  import { buildAsync } from ${JSON.stringify(pathToFileURL(entryPoint).href)};
87
- import { unlinkSync } from 'fs';
88
- try { await buildAsync(${JSON.stringify(projectDir)}); } finally {
90
+ import { unlinkSync, statSync } from 'fs';
91
+ import { execSync } from 'child_process';
92
+ try {
93
+ await buildAsync(${JSON.stringify(projectDir)});
94
+ // Vacuum if bloat ratio is high — keeps openDb fast over time.
95
+ try {
96
+ const dbPath = ${dbPathStr};
97
+ const fileMB = statSync(dbPath).size / 1024 / 1024;
98
+ const liveMB = parseInt(
99
+ execSync('sqlite3 "' + dbPath + '" "SELECT SUM(pgsize)/1024/1024 FROM dbstat;"',
100
+ { encoding: 'utf-8' }).trim(), 10);
101
+ if (fileMB > 100 && liveMB / fileMB < 0.5) {
102
+ execSync('sqlite3 "' + dbPath + '" "VACUUM;"', { timeout: 120000 });
103
+ }
104
+ } catch (_) {}
105
+ } finally {
89
106
  try { unlinkSync(${JSON.stringify(lockPath)}); } catch {}
90
107
  }`;
91
108
  const child = spawn(process.execPath, ['--input-type=module', '--eval', script], {
@@ -29,14 +29,20 @@ function _requireMonograph() {
29
29
  // Used by route (pre-resolve), pre-search (Grep/Glob redirect), and post-read
30
30
  // (neighbor footer). All calls are best-effort; failures are silent.
31
31
 
32
+ // Memoized at module scope — opening a multi-GB monograph.db can take 7-10s,
33
+ // and we call this 3+ times per route hook. Cache for the lifetime of this
34
+ // hook process. Callers should NOT close the returned handle.
35
+ var _cachedMonographDb = undefined;
32
36
  function _openMonographDb() {
37
+ if (_cachedMonographDb !== undefined) return _cachedMonographDb;
33
38
  try {
34
39
  var dbPath = path.join(CWD, '.monomind', 'monograph.db');
35
- if (!fs.existsSync(dbPath)) return null;
40
+ if (!fs.existsSync(dbPath)) { _cachedMonographDb = null; return null; }
36
41
  var mod = _requireMonograph();
37
- if (!mod || !mod.openDb) return null;
38
- return mod.openDb(dbPath);
39
- } catch (e) { return null; }
42
+ if (!mod || !mod.openDb) { _cachedMonographDb = null; return null; }
43
+ _cachedMonographDb = mod.openDb(dbPath);
44
+ return _cachedMonographDb;
45
+ } catch (e) { _cachedMonographDb = null; return null; }
40
46
  }
41
47
 
42
48
  function getMonographSuggestions(taskText, limit) {
@@ -91,7 +97,7 @@ function getMonographSuggestions(taskText, limit) {
91
97
  }
92
98
  return rows || [];
93
99
  } catch (e) { return []; }
94
- finally { try { db.close(); } catch (_) {} }
100
+ finally { /* db is shared/cached; do not close */ }
95
101
  }
96
102
 
97
103
  function getMonographNeighbors(filePath) {
@@ -119,7 +125,7 @@ function getMonographNeighbors(filePath) {
119
125
 
120
126
  return { imports: imports, importedBy: importedBy };
121
127
  } catch (e) { return null; }
122
- finally { try { db.close(); } catch (_) {} }
128
+ finally { /* db is shared/cached; do not close */ }
123
129
  }
124
130
 
125
131
  // Rough per-event token + USD cost estimates. Tuned to Sonnet input pricing
@@ -184,7 +190,7 @@ function _injectCompactGraphMap() {
184
190
  }
185
191
  console.log(' Use mcp__monomind__monograph_suggest first when navigating.');
186
192
  }
187
- } finally { try { db.close(); } catch (_) {} }
193
+ } finally { /* db is shared/cached; do not close */ }
188
194
  } catch (e) {}
189
195
  }
190
196
 
@@ -209,30 +215,70 @@ function _recordToolCall(signature) {
209
215
 
210
216
  // ── Cost budget ────────────────────────────────────────────────────────────────
211
217
  // Read today's cost from token-summary and compare against budget ceiling.
218
+ // If no budget.json exists, auto-tune from 30-day rolling mean (1.5x) so we
219
+ // don't shout BUDGET_BREACHED at users whose normal spend is above the default.
212
220
  function _getBudgetStatus() {
213
221
  try {
214
222
  var budgetFile = path.join(CWD, '.monomind', 'budget.json');
215
223
  var summaryFile = path.join(CWD, '.monomind', 'metrics', 'token-summary.json');
216
224
  if (!fs.existsSync(summaryFile)) return null;
217
225
  var summary = JSON.parse(fs.readFileSync(summaryFile, 'utf-8'));
218
- // Support both shapes: { todayCost, monthCost } and { today: { cost }, month: { cost } }
219
226
  var todayCost = summary.todayCost || (summary.today && summary.today.cost) || 0;
220
227
  var monthCost = summary.monthCost || (summary.month && summary.month.cost) || 0;
221
- var dailyLimit = 50, monthlyLimit = 1500; // defaults
222
- try {
223
- if (fs.existsSync(budgetFile)) {
228
+
229
+ var dailyLimit, monthlyLimit, autoTuned = false;
230
+ if (fs.existsSync(budgetFile)) {
231
+ try {
224
232
  var b = JSON.parse(fs.readFileSync(budgetFile, 'utf-8'));
225
- dailyLimit = b.dailyLimit || dailyLimit;
226
- monthlyLimit = b.monthlyLimit || monthlyLimit;
233
+ dailyLimit = b.dailyLimit;
234
+ monthlyLimit = b.monthlyLimit;
235
+ } catch (_) {}
236
+ }
237
+
238
+ // Auto-tune: monthCost / daysSoFar = avg daily; 1.5x that = limit.
239
+ // Only auto-tune when we actually have 7+ days of data and no manual budget.
240
+ if (!dailyLimit || !monthlyLimit) {
241
+ var now = new Date();
242
+ var daysIntoMonth = now.getUTCDate();
243
+ var dailyAvg = daysIntoMonth >= 1 ? monthCost / daysIntoMonth : 0;
244
+ if (dailyAvg > 5 && daysIntoMonth >= 7) {
245
+ dailyLimit = Math.max(dailyLimit || 0, Math.ceil(dailyAvg * 1.5));
246
+ monthlyLimit = Math.max(monthlyLimit || 0, Math.ceil(dailyAvg * 1.5 * 30));
247
+ autoTuned = true;
248
+ // Persist so future runs are stable and the user can edit.
249
+ try {
250
+ fs.mkdirSync(path.dirname(budgetFile), { recursive: true });
251
+ fs.writeFileSync(budgetFile, JSON.stringify({
252
+ dailyLimit: dailyLimit,
253
+ monthlyLimit: monthlyLimit,
254
+ autoTuned: true,
255
+ tunedAt: now.toISOString(),
256
+ basis: 'rolling avg $' + dailyAvg.toFixed(2) + '/day × 1.5',
257
+ note: 'Edit these values to set a hard ceiling. Delete the file to re-tune.',
258
+ }, null, 2));
259
+ } catch (_) {}
260
+ } else {
261
+ // Fall back to sensible defaults when there's not enough history.
262
+ dailyLimit = dailyLimit || 50;
263
+ monthlyLimit = monthlyLimit || 1500;
227
264
  }
228
- } catch (_) {}
265
+ }
266
+
229
267
  var dailyPct = Math.round((todayCost / dailyLimit) * 100);
230
268
  var monthlyPct = Math.round((monthCost / monthlyLimit) * 100);
269
+
270
+ // Spike detection: today is >2x the rolling daily avg (suspicious activity)
271
+ var rollingDaily = (new Date()).getUTCDate() >= 1 ? monthCost / (new Date()).getUTCDate() : 0;
272
+ var spike = rollingDaily > 0 && todayCost > rollingDaily * 2.0 && todayCost > 5;
273
+
231
274
  return {
232
275
  todayCost: todayCost, monthCost: monthCost,
233
276
  dailyLimit: dailyLimit, monthlyLimit: monthlyLimit,
234
277
  dailyPct: dailyPct, monthlyPct: monthlyPct,
235
- alert: dailyPct >= 80 || monthlyPct >= 80,
278
+ autoTuned: autoTuned,
279
+ spike: spike,
280
+ // Alert only when either the limit is breached OR there's a real spike
281
+ alert: dailyPct >= 80 || monthlyPct >= 80 || spike,
236
282
  breached: dailyPct >= 100 || monthlyPct >= 100,
237
283
  };
238
284
  } catch (e) { return null; }
@@ -264,7 +310,7 @@ function _findAffectedTests(filePath) {
264
310
  ).all(filePath, rel);
265
311
  return rows.map(function(r) { return r.file_path; });
266
312
  } catch (e) { return []; }
267
- finally { try { db.close(); } catch (_) {} }
313
+ finally { /* db is shared/cached; do not close */ }
268
314
  }
269
315
 
270
316
  // ── Hook latency tracking ─────────────────────────────────────────────────────
@@ -629,9 +675,8 @@ function _autoIndexKnowledge(knowledgeDir) {
629
675
 
630
676
  if (fs.existsSync(mgDbPath2)) {
631
677
  try {
632
- var mgMod2 = _requireMonograph();
633
- if (mgMod2 && mgMod2.openDb) {
634
- var sumDb = mgMod2.openDb(mgDbPath2);
678
+ var sumDb = _openMonographDb();
679
+ if (sumDb) {
635
680
  try {
636
681
  var nodeC = sumDb.prepare('SELECT COUNT(*) AS c FROM nodes').get().c;
637
682
  var edgeC = sumDb.prepare('SELECT COUNT(*) AS c FROM edges').get().c;
@@ -661,9 +706,7 @@ function _autoIndexKnowledge(knowledgeDir) {
661
706
  ' mcp__monomind__monograph_impact({ name: "<file>" }) — upstream + downstream blast radius',
662
707
  ].join('\n');
663
708
  summaryMeta = { label: 'monograph-graph-summary', source: 'monograph.db', nodes: nodeC, edges: edgeC };
664
- } finally {
665
- try { sumDb.close(); } catch (_) {}
666
- }
709
+ } catch (e) { /* keep summaryText if partial */ }
667
710
  }
668
711
  } catch (e) { /* fall through to legacy */ }
669
712
  }
@@ -826,15 +869,78 @@ const handlers = {
826
869
  }
827
870
  if (router && (router.routeTaskSemantic || router.routeTask)) {
828
871
  const routeFn = router.routeTaskSemantic || router.routeTask;
829
- const result = await Promise.resolve(routeFn(prompt));
872
+ var result = await Promise.resolve(routeFn(prompt));
873
+
874
+ // Graph-fallback override: when the router picked a low-confidence
875
+ // non-dev specialist (marketing slugs etc) but monograph has a strong
876
+ // graph match for the prompt, derive the agent from the top file's
877
+ // label instead. Stops "improve the system" → China E-Commerce.
878
+ try {
879
+ // Don't override when the prompt has obvious non-dev keywords —
880
+ // marketing/sales/finance asks SHOULD route to those specialists.
881
+ var nonDevPrompt = /\b(marketing|advertis|seo|tiktok|instagram|linkedin|sales|customer|brand|blog post|content strategy|copy(?:writ|writing)|pitch|investor|hr|recruit|legal|compliance|tax|invoice|accounting|onboarding|design syst|figma|user research|persona)\b/i.test(prompt);
882
+
883
+ var devAgents = /^(coder|tester|reviewer|planner|researcher|system-architect|backend-dev|backend-architect|mobile-dev|ml-developer|cicd-engineer|api-docs|code-analyzer|production-validator|Technical Writer)$/i;
884
+ var pickedDev = devAgents.test(String(result.agent || '').trim()) ||
885
+ devAgents.test(String(result.agentSlug || '').trim());
886
+
887
+ var resConf = (result.confidence != null ? result.confidence : 0);
888
+ var resReason = String(result.reason || '');
889
+ var fromKeywordStage = resReason.indexOf('Keyword 2-stage') !== -1;
890
+ var promptIsDevish = /\b(improve|refactor|fix|bug|optimi[sz]e|implement|build|debug|deploy|test|feature|system|performance|architecture|memory|hook|graph|statusline|monograph|api|cli|skill|hooks|agent|workflow|init|module|package|registry|server|client|route|handler)\b/i.test(prompt);
891
+
892
+ var shouldOverride = !nonDevPrompt && (
893
+ (!pickedDev && resConf < 0.85) ||
894
+ (fromKeywordStage && promptIsDevish)
895
+ );
896
+ if (shouldOverride) {
897
+ var topGraph = getMonographSuggestions(prompt, 1)[0];
898
+ if (topGraph) {
899
+ var agent = 'coder';
900
+ var file = (topGraph.file || '').toLowerCase();
901
+ // Test files
902
+ if (/\.(test|spec)\./.test(file) || file.includes('__tests__')) agent = 'tester';
903
+ // Architecture/system docs → architect
904
+ else if (/(architect|adr-|design-doc|rfc-)/.test(file)) agent = 'system-architect';
905
+ // Pure docs → tech writer
906
+ else if (file.endsWith('readme.md') || file.startsWith('docs/') || /\/docs\//.test(file)) agent = 'Technical Writer';
907
+ // Other .md (skills, agents, configs) → coder (they're code-adjacent)
908
+ else if (file.endsWith('.md')) agent = 'coder';
909
+ // Class/Interface → architect
910
+ else if (topGraph.label === 'Class' || topGraph.label === 'Interface') agent = 'system-architect';
911
+ // Functions, files, methods → coder
912
+ else agent = 'coder';
913
+ result = Object.assign({}, result, {
914
+ agent: agent,
915
+ agentSlug: agent,
916
+ confidence: 0.70,
917
+ reason: 'Graph fallback: top file ' + (topGraph.name || '').substring(0, 30) + ' [' + topGraph.label + ']',
918
+ specificAgents: [],
919
+ extrasMatches: [],
920
+ });
921
+ }
922
+ }
923
+ } catch (e) {}
924
+
830
925
  var output = [];
831
926
  output.push('[INFO] Routing task: ' + (prompt.substring(0, 80) || '(no prompt)'));
832
927
  output.push('');
833
- output.push('+------------- monomind | Primary Recommendation --------------+');
834
- output.push('| Agent: ' + (result.agent || 'unknown').substring(0, 54).padEnd(54) + '|');
835
- output.push('| Confidence: ' + ((result.confidence != null ? (result.confidence * 100).toFixed(1) : '?') + '%').padEnd(49) + '|');
836
- output.push('| Reason: ' + (result.reason || '').substring(0, 53).padEnd(53) + '|');
837
- output.push('+--------------------------------------------------------------+');
928
+ // Suppress the agent recommendation panel for low-confidence routes on
929
+ // short prompts the recommendation is almost always wrong (e.g.
930
+ // "what else can we do" marketing China E-Commerce). Saves ~150
931
+ // tokens per prompt. Skill matches and specific agents still render
932
+ // below when confidence is decent.
933
+ var conf = result.confidence != null ? result.confidence : 0;
934
+ var promptShort = (prompt || '').trim().length < 60;
935
+ var lowConf = conf < 0.70;
936
+ var suppressPanel = lowConf && promptShort;
937
+ if (!suppressPanel) {
938
+ output.push('+------------- monomind | Primary Recommendation --------------+');
939
+ output.push('| Agent: ' + (result.agent || 'unknown').substring(0, 54).padEnd(54) + '|');
940
+ output.push('| Confidence: ' + ((result.confidence != null ? (result.confidence * 100).toFixed(1) : '?') + '%').padEnd(49) + '|');
941
+ output.push('| Reason: ' + (result.reason || '').substring(0, 53).padEnd(53) + '|');
942
+ output.push('+--------------------------------------------------------------+');
943
+ }
838
944
 
839
945
  // ── Persist routing result for statusline display ─────────────
840
946
  try {
@@ -913,8 +1019,9 @@ const handlers = {
913
1019
  }
914
1020
 
915
1021
  // ── Specific agent panel ──────────────────────────────────────────────────
1022
+ // Skip entirely on suppressed (low-confidence + short) prompts.
916
1023
  var specificAgents = result.specificAgents || [];
917
- if (specificAgents.length > 0) {
1024
+ if (specificAgents.length > 0 && !suppressPanel) {
918
1025
  output.push('');
919
1026
  var saHdr = '------- Specific Agents (' + specificAgents.length + ' available) ';
920
1027
  output.push('+' + saHdr + '-'.repeat(Math.max(1, 62 - saHdr.length)) + '+');
@@ -934,7 +1041,7 @@ const handlers = {
934
1041
  // ── Specialist agents (non-dev domain) — only shown when specificAgents panel wasn't shown ──
935
1042
  var extras = result.extrasMatches || [];
936
1043
  var specificAgentsShown = (result.specificAgents || []).length > 0;
937
- if (extras.length > 0 && !specificAgentsShown) {
1044
+ if (extras.length > 0 && !specificAgentsShown && !suppressPanel) {
938
1045
  output.push('');
939
1046
  var spHdr = '------- Specialist Agents (' + extras.length + ' matched) ';
940
1047
  output.push('+' + spHdr + '-'.repeat(Math.max(1, 62 - spHdr.length)) + '+');
@@ -990,10 +1097,13 @@ const handlers = {
990
1097
  try {
991
1098
  var budget = _getBudgetStatus();
992
1099
  if (budget && budget.alert) {
993
- if (budget.breached) {
994
- console.log('[BUDGET_BREACHED] Daily $' + budget.todayCost.toFixed(2) + '/$' + budget.dailyLimit + ' (' + budget.dailyPct + '%) · Monthly $' + budget.monthCost.toFixed(2) + '/$' + budget.monthlyLimit + ' (' + budget.monthlyPct + '%). Switch to Haiku with /model haiku or raise limits in .monomind/budget.json.');
1100
+ var tunedNote = budget.autoTuned ? ' (auto-tuned)' : '';
1101
+ if (budget.spike && !budget.breached) {
1102
+ console.log('[BUDGET_SPIKE] Today $' + budget.todayCost.toFixed(2) + ' is >2x your rolling daily avg. Unusual spend — review .monomind/metrics/token-summary.json.');
1103
+ } else if (budget.breached) {
1104
+ console.log('[BUDGET_BREACHED] Daily $' + budget.todayCost.toFixed(2) + '/$' + budget.dailyLimit + ' (' + budget.dailyPct + '%) · Monthly $' + budget.monthCost.toFixed(2) + '/$' + budget.monthlyLimit + ' (' + budget.monthlyPct + '%)' + tunedNote + '. Switch to Haiku with /model haiku or edit .monomind/budget.json.');
995
1105
  } else {
996
- console.log('[BUDGET_ALERT] Daily ' + budget.dailyPct + '% of $' + budget.dailyLimit + ' · Monthly ' + budget.monthlyPct + '% of $' + budget.monthlyLimit + '. Adjust .monomind/budget.json if needed.');
1106
+ console.log('[BUDGET_ALERT] Daily ' + budget.dailyPct + '% of $' + budget.dailyLimit + ' · Monthly ' + budget.monthlyPct + '% of $' + budget.monthlyLimit + tunedNote + '.');
997
1107
  }
998
1108
  }
999
1109
  } catch (e) {}
@@ -1007,14 +1117,9 @@ const handlers = {
1007
1117
  var nodeCount = 0;
1008
1118
  if (fs.existsSync(monographDb)) {
1009
1119
  try {
1010
- var mgMod = _requireMonograph();
1011
- if (mgMod && mgMod.openDb) {
1012
- var hintDb = mgMod.openDb(monographDb);
1013
- try {
1014
- nodeCount = hintDb.prepare('SELECT COUNT(*) AS c FROM nodes').get().c;
1015
- } finally {
1016
- try { hintDb.close(); } catch (_) {}
1017
- }
1120
+ var hintDb = _openMonographDb();
1121
+ if (hintDb) {
1122
+ nodeCount = hintDb.prepare('SELECT COUNT(*) AS c FROM nodes').get().c;
1018
1123
  }
1019
1124
  } catch (e) { /* ignore — fall back to legacy */ }
1020
1125
  }
@@ -1451,10 +1556,8 @@ const handlers = {
1451
1556
  try {
1452
1557
  var mgDbPath = path.join(CWD, '.monomind', 'monograph.db');
1453
1558
  if (fs.existsSync(mgDbPath)) {
1454
- var mgMod = null;
1455
- mgMod = _requireMonograph();
1456
- if (mgMod && mgMod.openDb) {
1457
- var mgDb = mgMod.openDb(mgDbPath);
1559
+ var mgDb = _openMonographDb();
1560
+ if (mgDb) {
1458
1561
  try {
1459
1562
  var mgNodeCount = mgDb.prepare('SELECT COUNT(*) AS c FROM nodes').get().c;
1460
1563
  var mgEdgeCount = mgDb.prepare('SELECT COUNT(*) AS c FROM edges').get().c;
@@ -1493,7 +1596,7 @@ const handlers = {
1493
1596
  fs.writeFileSync(mgChunksFile, mgExisting.join('\n') + '\n');
1494
1597
  } catch(e) {}
1495
1598
  }
1496
- } finally { if (mgMod.closeDb) mgMod.closeDb(mgDb); }
1599
+ } catch(e) { /* non-fatal */ }
1497
1600
  }
1498
1601
  }
1499
1602
  } catch(e) { /* non-fatal */ }
@@ -2226,7 +2329,7 @@ const handlers = {
2226
2329
  } catch (_) {}
2227
2330
  console.log(' Use mcp__monomind__monograph_suggest / monograph_query in this subagent before grepping.');
2228
2331
  }
2229
- } finally { try { subDb.close(); } catch (_) {} }
2332
+ } catch (e) { /* non-fatal */ }
2230
2333
  }
2231
2334
  } catch (e) { /* non-fatal */ }
2232
2335
 
@@ -2289,6 +2392,75 @@ const handlers = {
2289
2392
  console.log(' Edit the file to fill in Context and Consequences, then change Status to Accepted/Rejected.');
2290
2393
  },
2291
2394
 
2395
+ 'graph-status': () => {
2396
+ var db = _openMonographDb();
2397
+ if (!db) { console.log('No monograph.db found. Run /monomind:understand to build.'); return; }
2398
+ try {
2399
+ var n = db.prepare("SELECT COUNT(*) AS c FROM nodes").get().c;
2400
+ var e = db.prepare("SELECT COUNT(*) AS c FROM edges").get().c;
2401
+ var usage = (function() {
2402
+ try { return JSON.parse(fs.readFileSync(path.join(CWD, '.monomind', 'metrics', 'graph-usage.json'), 'utf-8')); }
2403
+ catch (_) { return {}; }
2404
+ })();
2405
+ var wins = (usage.monograph_call || 0) + (usage.preresolve_hit || 0)
2406
+ + (usage.graph_assist_search || 0) + (usage.graph_assist_neighbors || 0);
2407
+ var search = (usage.grep_call || 0) + (usage.glob_call || 0)
2408
+ + (usage.bash_grep_call || 0) + (usage.bash_find_call || 0);
2409
+ var pct = (wins + search) > 0 ? Math.round((wins / (wins + search)) * 100) : 0;
2410
+ var saved = usage.dollars_saved || 0;
2411
+ console.log('Monograph: ' + n.toLocaleString() + ' nodes · ' + e.toLocaleString() + ' edges');
2412
+ console.log('Usage: ' + pct + '% graph · ' + (100 - pct) + '% grep · ' +
2413
+ 'wins=' + wins + ' search=' + search +
2414
+ (saved > 0 ? ' · saved $' + saved.toFixed(2) : ''));
2415
+ } catch (err) { console.log('Error: ' + err.message); }
2416
+ },
2417
+
2418
+ 'budget-status': () => {
2419
+ var b = _getBudgetStatus();
2420
+ if (!b) { console.log('No budget data yet — token tracking not initialized.'); return; }
2421
+ console.log('Today: $' + b.todayCost.toFixed(2) + ' / $' + b.dailyLimit + ' (' + b.dailyPct + '%)' + (b.autoTuned ? ' [auto-tuned]' : ''));
2422
+ console.log('Month: $' + b.monthCost.toFixed(2) + ' / $' + b.monthlyLimit + ' (' + b.monthlyPct + '%)');
2423
+ console.log('Status: ' + (b.breached ? 'BREACHED' : b.spike ? 'SPIKE' : b.alert ? 'ALERT' : 'OK'));
2424
+ console.log('Edit .monomind/budget.json to adjust. Delete to re-tune.');
2425
+ },
2426
+
2427
+ 'loops-status': () => {
2428
+ var loopsDir = path.join(CWD, '.monomind', 'loops');
2429
+ if (!fs.existsSync(loopsDir)) { console.log('No loops directory.'); return; }
2430
+ var files = fs.readdirSync(loopsDir).filter(function(f) {
2431
+ return f.endsWith('.json') && !f.includes('-hil') && !f.endsWith('.stop');
2432
+ });
2433
+ var STALE_MS = 6 * 60 * 60 * 1000;
2434
+ var now = Date.now();
2435
+ var active = [], stale = [];
2436
+ files.forEach(function(f) {
2437
+ try {
2438
+ var d = JSON.parse(fs.readFileSync(path.join(loopsDir, f), 'utf-8'));
2439
+ var last = d.lastRunAt || d.startedAt || 0;
2440
+ var ageMs = last ? (now - last) : Infinity;
2441
+ if (ageMs > STALE_MS) stale.push({ d: d, ageH: Math.round(ageMs / 3600000) });
2442
+ else active.push(d);
2443
+ } catch (_) {}
2444
+ });
2445
+ if (active.length === 0 && stale.length === 0) {
2446
+ console.log('No loops.'); return;
2447
+ }
2448
+ if (active.length > 0) {
2449
+ console.log('Active (' + active.length + '):');
2450
+ active.forEach(function(d) {
2451
+ console.log(' · ' + (d.command || '?') + ' [' + (d.type || '?') + '] run ' + (d.currentRep || 0) +
2452
+ (d.maxReps ? '/' + d.maxReps : '') + ' · ' + (d.status || '?'));
2453
+ });
2454
+ }
2455
+ if (stale.length > 0) {
2456
+ console.log('Stale (' + stale.length + ' >6h):');
2457
+ stale.forEach(function(s) {
2458
+ console.log(' · ' + (s.d.command || '?') + ' run ' + (s.d.currentRep || 0) +
2459
+ ' · ' + s.ageH + 'h ago · ' + (s.d.status || '?'));
2460
+ });
2461
+ }
2462
+ },
2463
+
2292
2464
  'status': () => {
2293
2465
  console.log('[OK] Status check');
2294
2466
  },
@@ -2584,6 +2584,7 @@
2584
2584
  <button class="mm-tab" onclick="switchMmTab('loops')">LOOPS</button>
2585
2585
  <button class="mm-tab" onclick="switchMmTab('create')">CREATE ORG</button>
2586
2586
  <button class="mm-tab" onclick="switchMmTab('metrics')">METRICS</button>
2587
+ <button class="mm-tab" onclick="switchMmTab('graph')">GRAPH</button>
2587
2588
  </div>
2588
2589
  <div id="mm-body">
2589
2590
  <div class="mm-pane active" id="mm-pane-orgs">
@@ -2633,6 +2634,18 @@
2633
2634
  <div class="mm-section-title" style="margin-top:16px">Recent Events</div>
2634
2635
  <div id="mm-metrics-events" style="font-size:9px;color:rgba(150,100,200,0.6);font-family:'Azeret Mono',monospace"></div>
2635
2636
  </div>
2637
+ <div class="mm-pane" id="mm-pane-graph">
2638
+ <div class="mm-section-title">Monograph Knowledge Graph</div>
2639
+ <div id="mm-graph-summary" style="display:flex;gap:24px;margin-bottom:16px;font-size:11px">
2640
+ <span style="color:rgba(150,100,200,0.4)">Loading graph stats…</span>
2641
+ </div>
2642
+ <div class="mm-section-title" style="margin-top:8px">Top God Nodes (highest centrality)</div>
2643
+ <div id="mm-graph-gods" style="font-size:9px;color:rgba(180,140,220,0.85);font-family:'Azeret Mono',monospace;line-height:1.6"></div>
2644
+ <div class="mm-section-title" style="margin-top:16px">Node Types</div>
2645
+ <div id="mm-graph-types" style="font-size:9px;color:rgba(180,140,220,0.85);font-family:'Azeret Mono',monospace"></div>
2646
+ <div class="mm-section-title" style="margin-top:16px">Edge Relations</div>
2647
+ <div id="mm-graph-relations" style="font-size:9px;color:rgba(180,140,220,0.85);font-family:'Azeret Mono',monospace"></div>
2648
+ </div>
2636
2649
  </div>
2637
2650
  </div>
2638
2651
  </div>
@@ -10264,6 +10277,50 @@ window.switchMmTab = function(tab) {
10264
10277
  if (tab === 'loops') loadMmLoops();
10265
10278
  if (tab === 'metrics') loadMmMetrics();
10266
10279
  if (tab === 'skills') renderMmSkills('');
10280
+ if (tab === 'graph') loadMmGraph();
10281
+ };
10282
+
10283
+ window.loadMmGraph = async function() {
10284
+ const summary = document.getElementById('mm-graph-summary');
10285
+ const gods = document.getElementById('mm-graph-gods');
10286
+ const types = document.getElementById('mm-graph-types');
10287
+ const relations = document.getElementById('mm-graph-relations');
10288
+ if (!summary) return;
10289
+ try {
10290
+ const r = await fetch('/api/monograph');
10291
+ const d = await r.json();
10292
+ if (!d.exists) {
10293
+ summary.innerHTML = '<span style="color:rgba(220,120,120,0.7)">No monograph.db found. Run /monomind:understand or wait for SessionStart to build the graph.</span>';
10294
+ gods.textContent = ''; types.textContent = ''; relations.textContent = '';
10295
+ return;
10296
+ }
10297
+ summary.innerHTML = `
10298
+ <div><div style="color:rgba(150,100,200,0.5);font-size:8px;letter-spacing:0.1em">NODES</div>
10299
+ <div style="font-size:18px;color:rgba(120,200,160,1);font-weight:bold">${d.nodes.toLocaleString()}</div></div>
10300
+ <div><div style="color:rgba(150,100,200,0.5);font-size:8px;letter-spacing:0.1em">EDGES</div>
10301
+ <div style="font-size:18px;color:rgba(120,200,160,1);font-weight:bold">${d.edges.toLocaleString()}</div></div>
10302
+ <div><div style="color:rgba(150,100,200,0.5);font-size:8px;letter-spacing:0.1em">UPDATED</div>
10303
+ <div style="font-size:11px;color:rgba(210,140,255,0.8)">${new Date(d.updatedAt).toLocaleString()}</div></div>
10304
+ `;
10305
+ gods.innerHTML = (d.godNodes || []).map(n =>
10306
+ `<div>· <span style="color:rgba(255,200,100,0.9)">${(n.deg||0).toString().padStart(4,' ')}</span> &nbsp;
10307
+ <span style="color:rgba(210,140,255,1)">${n.name||''}</span>
10308
+ <span style="color:rgba(150,100,200,0.5)">[${n.label||''}]</span>
10309
+ &nbsp; <span style="color:rgba(120,200,160,0.7)">${n.file_path||''}</span></div>`
10310
+ ).join('') || '<span style="color:rgba(150,100,200,0.4)">none</span>';
10311
+ types.innerHTML = (d.typeDistribution || []).map(t =>
10312
+ `<span style="display:inline-block;padding:2px 8px;margin:2px;background:rgba(200,120,255,0.08);border:1px solid rgba(200,120,255,0.2);border-radius:3px">
10313
+ ${t.label}: <strong style="color:rgba(120,200,160,1)">${t.count.toLocaleString()}</strong>
10314
+ </span>`
10315
+ ).join('');
10316
+ relations.innerHTML = (d.relationDistribution || []).map(r =>
10317
+ `<span style="display:inline-block;padding:2px 8px;margin:2px;background:rgba(200,120,255,0.08);border:1px solid rgba(200,120,255,0.2);border-radius:3px">
10318
+ ${r.relation}: <strong style="color:rgba(255,200,100,1)">${r.count.toLocaleString()}</strong>
10319
+ </span>`
10320
+ ).join('');
10321
+ } catch (err) {
10322
+ summary.innerHTML = `<span style="color:rgba(220,120,120,0.7)">Failed to load graph: ${err.message}</span>`;
10323
+ }
10267
10324
  };
10268
10325
 
10269
10326
  window.filterMmSkills = function(q) { renderMmSkills(q); };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monoes/monomindcli",
3
- "version": "1.10.13",
3
+ "version": "1.10.15",
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",