@monoes/monomindcli 1.10.9 → 1.10.11

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.
@@ -45,23 +45,33 @@ function getMonographSuggestions(taskText, limit) {
45
45
  if (!db) return [];
46
46
  try {
47
47
  var words = String(taskText).toLowerCase().match(/[a-z][a-z0-9_-]{3,}/g) || [];
48
- var stop = { "this":1,"that":1,"with":1,"from":1,"have":1,"into":1,"their":1,"what":1,"when":1,"where":1,"which":1,"should":1,"would":1,"could":1,"make":1,"just":1,"also":1,"them":1,"they":1,"will":1,"been":1,"were":1,"because":1,"about":1,"does":1,"work":1 };
48
+ var stop = { "this":1,"that":1,"with":1,"from":1,"have":1,"into":1,"their":1,"what":1,"when":1,"where":1,"which":1,"should":1,"would":1,"could":1,"make":1,"just":1,"also":1,"them":1,"they":1,"will":1,"been":1,"were":1,"because":1,"about":1,"does":1,"work":1,"else":1,"more":1,"some":1,"like":1,"need":1,"want":1,"used":1,"using":1,"please":1,"thanks":1,"good":1,"great":1,"nice":1,"thing":1,"things":1,"better":1,"again":1,"first":1,"then":1,"only":1,"even":1 };
49
49
  var uniq = {};
50
50
  for (var i = 0; i < words.length; i++) if (!stop[words[i]]) uniq[words[i]] = 1;
51
51
  var keys = Object.keys(uniq).slice(0, 8);
52
+ // Smart filter: free-form prompts need ≥2 content words to avoid noise.
53
+ // Single-word inputs are allowed only when they look like a symbol/search
54
+ // (entire string is ≤30 chars and contains letters+separators only).
55
+ var isSymbolLookup = taskText.length <= 30 && /^[a-zA-Z0-9_\-./:]+$/.test(taskText.trim());
52
56
  if (keys.length === 0) return [];
57
+ if (keys.length < 2 && !isSymbolLookup) return [];
53
58
 
54
59
  var ftsQuery = keys.map(function(k){ return '"' + k.replace(/"/g, "") + '"'; }).join(" OR ");
55
60
  var lim = Math.max(1, limit || 5);
56
61
  var rows = [];
57
62
  try {
63
+ // BM25 ranks better than degree for keyword relevance; tie-break by deg.
64
+ // File/Function/Class outrank Section so navigable nodes win.
58
65
  rows = db.prepare(
59
66
  "SELECT n.id, n.name, n.label, n.file_path AS file, " +
60
- "(SELECT COUNT(*) FROM edges WHERE source_id=n.id OR target_id=n.id) AS deg " +
67
+ "bm25(nodes_fts) AS bm25_score, " +
68
+ "(SELECT COUNT(*) FROM edges WHERE source_id=n.id OR target_id=n.id) AS deg, " +
69
+ "CASE n.label WHEN 'File' THEN 3 WHEN 'Function' THEN 3 WHEN 'Class' THEN 3 " +
70
+ " WHEN 'Method' THEN 2 WHEN 'Interface' THEN 2 ELSE 1 END AS label_rank " +
61
71
  "FROM nodes_fts f JOIN nodes n ON f.rowid = n.rowid " +
62
72
  "WHERE nodes_fts MATCH ? AND n.file_path IS NOT NULL AND n.file_path != '' " +
63
73
  "AND n.label NOT IN ('Concept') " +
64
- "ORDER BY deg DESC LIMIT ?"
74
+ "ORDER BY label_rank DESC, bm25_score ASC, deg DESC LIMIT ?"
65
75
  ).all(ftsQuery, lim);
66
76
  } catch (e) {
67
77
  var likeFrag = keys.map(function(){ return "lower(n.name) LIKE ?"; }).join(" OR ");
@@ -108,6 +118,17 @@ function getMonographNeighbors(filePath) {
108
118
  finally { try { db.close(); } catch (_) {} }
109
119
  }
110
120
 
121
+ // Rough per-event token + USD cost estimates. Tuned to Sonnet input pricing
122
+ // ($3/M tokens) — adjust if needed. Used by the statusline to surface savings.
123
+ var _TOKEN_PER_EVENT = {
124
+ monograph_call: 300, // typical monograph_query result size
125
+ grep_call: 2000, // typical Grep tool output across many files
126
+ glob_call: 800,
127
+ bash_grep_call: 2000,
128
+ bash_find_call: 800,
129
+ };
130
+ var _DOLLAR_PER_1M_TOKENS = 3.0;
131
+
111
132
  function _recordGraphTelemetry(event) {
112
133
  try {
113
134
  var metricsDir = path.join(CWD, ".monomind", "metrics");
@@ -117,11 +138,181 @@ function _recordGraphTelemetry(event) {
117
138
  try { d = JSON.parse(fs.readFileSync(f, "utf-8")); } catch (e) {}
118
139
  if (typeof d !== "object" || d === null) d = {};
119
140
  d[event] = (d[event] || 0) + 1;
141
+
142
+ // Token-saved estimator: each monograph_call avoids a grep equivalent
143
+ // (~2000 tokens) at a cost of ~300 tokens — net ~1700 saved.
144
+ if (event === 'monograph_call') {
145
+ var saved = (_TOKEN_PER_EVENT.grep_call - _TOKEN_PER_EVENT.monograph_call);
146
+ d.tokens_saved = (d.tokens_saved || 0) + saved;
147
+ d.dollars_saved = ((d.tokens_saved / 1000000) * _DOLLAR_PER_1M_TOKENS);
148
+ }
149
+ // Each unprompted grep/bash_grep "wastes" the same amount vs the graph alternative.
150
+ if (event === 'grep_call' || event === 'bash_grep_call') {
151
+ var wasted = (_TOKEN_PER_EVENT.grep_call - _TOKEN_PER_EVENT.monograph_call);
152
+ d.tokens_wasted = (d.tokens_wasted || 0) + wasted;
153
+ }
154
+
120
155
  d.lastUpdated = Date.now();
121
156
  fs.writeFileSync(f, JSON.stringify(d));
122
157
  } catch (e) { /* non-fatal */ }
123
158
  }
124
159
 
160
+ // ── Loop drift detection ───────────────────────────────────────────────────────
161
+ // Record tool call signatures per session, warn when the same call recurs ≥3×.
162
+ function _recordToolCall(signature) {
163
+ try {
164
+ var f = path.join(CWD, '.monomind', 'metrics', 'tool-calls.json');
165
+ fs.mkdirSync(path.dirname(f), { recursive: true });
166
+ var d = {};
167
+ try { d = JSON.parse(fs.readFileSync(f, 'utf-8')); } catch (_) {}
168
+ if (typeof d !== 'object' || d === null) d = {};
169
+ // Roll over every 4 hours (new session)
170
+ if (!d.startedAt || (Date.now() - d.startedAt) > 4 * 60 * 60 * 1000) {
171
+ d = { startedAt: Date.now(), calls: {} };
172
+ }
173
+ d.calls[signature] = (d.calls[signature] || 0) + 1;
174
+ fs.writeFileSync(f, JSON.stringify(d));
175
+ return d.calls[signature];
176
+ } catch (e) { return 0; }
177
+ }
178
+
179
+ // ── Cost budget ────────────────────────────────────────────────────────────────
180
+ // Read today's cost from token-summary and compare against budget ceiling.
181
+ function _getBudgetStatus() {
182
+ try {
183
+ var budgetFile = path.join(CWD, '.monomind', 'budget.json');
184
+ var summaryFile = path.join(CWD, '.monomind', 'metrics', 'token-summary.json');
185
+ if (!fs.existsSync(summaryFile)) return null;
186
+ var summary = JSON.parse(fs.readFileSync(summaryFile, 'utf-8'));
187
+ // Support both shapes: { todayCost, monthCost } and { today: { cost }, month: { cost } }
188
+ var todayCost = summary.todayCost || (summary.today && summary.today.cost) || 0;
189
+ var monthCost = summary.monthCost || (summary.month && summary.month.cost) || 0;
190
+ var dailyLimit = 50, monthlyLimit = 1500; // defaults
191
+ try {
192
+ if (fs.existsSync(budgetFile)) {
193
+ var b = JSON.parse(fs.readFileSync(budgetFile, 'utf-8'));
194
+ dailyLimit = b.dailyLimit || dailyLimit;
195
+ monthlyLimit = b.monthlyLimit || monthlyLimit;
196
+ }
197
+ } catch (_) {}
198
+ var dailyPct = Math.round((todayCost / dailyLimit) * 100);
199
+ var monthlyPct = Math.round((monthCost / monthlyLimit) * 100);
200
+ return {
201
+ todayCost: todayCost, monthCost: monthCost,
202
+ dailyLimit: dailyLimit, monthlyLimit: monthlyLimit,
203
+ dailyPct: dailyPct, monthlyPct: monthlyPct,
204
+ alert: dailyPct >= 80 || monthlyPct >= 80,
205
+ breached: dailyPct >= 100 || monthlyPct >= 100,
206
+ };
207
+ } catch (e) { return null; }
208
+ }
209
+
210
+ // ── Test feedback (detection only — do not auto-run) ──────────────────────────
211
+ // When LLM edits a source file, find tests that import it via monograph and
212
+ // surface the list so the LLM (or user) knows what to verify.
213
+ function _findAffectedTests(filePath) {
214
+ if (!filePath) return [];
215
+ var db = _openMonographDb();
216
+ if (!db) return [];
217
+ try {
218
+ var rel = filePath;
219
+ if (filePath.indexOf(CWD) === 0) rel = filePath.slice(CWD.length + 1);
220
+ // Find tests that IMPORTS any symbol whose target file_path matches our file.
221
+ // The graph stores IMPORTS edges to symbol nodes, not file nodes — so we
222
+ // match on the target node's file_path field instead of target_id directly.
223
+ var rows = db.prepare(
224
+ "SELECT DISTINCT src.file_path FROM edges e " +
225
+ "JOIN nodes src ON e.source_id = src.id " +
226
+ "JOIN nodes tgt ON e.target_id = tgt.id " +
227
+ "WHERE e.relation IN ('IMPORTS','CALLS','DEPENDS_ON') " +
228
+ "AND (tgt.file_path = ? OR tgt.file_path = ?) " +
229
+ "AND src.file_path IS NOT NULL AND src.file_path != '' " +
230
+ "AND (src.file_path LIKE '%test%' OR src.file_path LIKE '%.spec.%' OR src.file_path LIKE '%__tests__%') " +
231
+ "AND src.file_path NOT LIKE '%.worktrees%' " +
232
+ "LIMIT 5"
233
+ ).all(filePath, rel);
234
+ return rows.map(function(r) { return r.file_path; });
235
+ } catch (e) { return []; }
236
+ finally { try { db.close(); } catch (_) {} }
237
+ }
238
+
239
+ // ── Hook latency tracking ─────────────────────────────────────────────────────
240
+ function _recordHookLatency(handlerName, durationMs) {
241
+ try {
242
+ var f = path.join(CWD, '.monomind', 'metrics', 'hook-latency.json');
243
+ fs.mkdirSync(path.dirname(f), { recursive: true });
244
+ var d = {};
245
+ try { d = JSON.parse(fs.readFileSync(f, 'utf-8')); } catch (_) {}
246
+ if (typeof d !== 'object' || d === null) d = {};
247
+ var entry = d[handlerName] || { count: 0, total: 0, max: 0 };
248
+ entry.count++;
249
+ entry.total += durationMs;
250
+ entry.max = Math.max(entry.max, durationMs);
251
+ entry.mean = Math.round(entry.total / entry.count);
252
+ d[handlerName] = entry;
253
+ d.lastUpdated = Date.now();
254
+ fs.writeFileSync(f, JSON.stringify(d));
255
+ } catch (e) {}
256
+ }
257
+
258
+ // ── Auto-ADR decision detection ───────────────────────────────────────────────
259
+ // Record sentence-level decision markers from user prompts to .monomind/decisions.jsonl
260
+ function _recordDecisionMarkers(promptText) {
261
+ if (!promptText || typeof promptText !== 'string') return;
262
+ var markers = /\b(let's go with|we (?:chose|decided|picked|will go with)|decision[:\s]|choosing|going with|prefer to|let's use)\b[^\.\n]{0,200}/gi;
263
+ var matches = promptText.match(markers);
264
+ if (!matches || matches.length === 0) return;
265
+ try {
266
+ var f = path.join(CWD, '.monomind', 'decisions.jsonl');
267
+ var entry = JSON.stringify({
268
+ ts: Date.now(),
269
+ excerpts: matches.slice(0, 3),
270
+ prompt: promptText.slice(0, 400),
271
+ });
272
+ fs.appendFileSync(f, entry + '\n');
273
+ } catch (e) {}
274
+ }
275
+
276
+ // Auto-rebuild the monograph after N writes — graph staleness during heavy
277
+ // editing is the main reason suggestions go cold. Triggered by post-edit.
278
+ function _maybeRebuildMonograph() {
279
+ try {
280
+ var metricsDir = path.join(CWD, ".monomind", "metrics");
281
+ fs.mkdirSync(metricsDir, { recursive: true });
282
+ var f = path.join(metricsDir, "graph-rebuild.json");
283
+ var d = {};
284
+ try { d = JSON.parse(fs.readFileSync(f, "utf-8")); } catch (_) {}
285
+ if (typeof d !== "object" || d === null) d = {};
286
+ d.writesSinceRebuild = (d.writesSinceRebuild || 0) + 1;
287
+ d.lastWriteAt = Date.now();
288
+ var THRESHOLD = 20;
289
+ var MIN_INTERVAL_MS = 5 * 60 * 1000; // never more often than every 5 min
290
+ var dueByCount = d.writesSinceRebuild >= THRESHOLD;
291
+ var dueByTime = !d.lastRebuildAt || (Date.now() - d.lastRebuildAt) > MIN_INTERVAL_MS;
292
+ if (dueByCount && dueByTime) {
293
+ // Reset counter immediately so concurrent post-edits don't all fire.
294
+ d.writesSinceRebuild = 0;
295
+ d.lastRebuildAt = Date.now();
296
+ fs.writeFileSync(f, JSON.stringify(d));
297
+ // Fire-and-forget background freshen script (same one used by SessionStart).
298
+ try {
299
+ var freshenScript = path.join(CWD, '.claude', 'helpers', 'graphify-freshen.cjs');
300
+ if (fs.existsSync(freshenScript)) {
301
+ var spawn = require('child_process').spawn;
302
+ var child = spawn(process.execPath, [freshenScript], {
303
+ detached: true,
304
+ stdio: 'ignore',
305
+ cwd: CWD,
306
+ });
307
+ child.unref();
308
+ }
309
+ } catch (_) {}
310
+ } else {
311
+ fs.writeFileSync(f, JSON.stringify(d));
312
+ }
313
+ } catch (e) { /* non-fatal */ }
314
+ }
315
+
125
316
  function safeRequire(modulePath) {
126
317
  try {
127
318
  if (fs.existsSync(modulePath)) {
@@ -761,6 +952,21 @@ const handlers = {
761
952
 
762
953
  console.log(output.join('\n'));
763
954
 
955
+ // Record any decision markers in this prompt (auto-ADR pipeline).
956
+ try { _recordDecisionMarkers(prompt); } catch (e) {}
957
+
958
+ // Cost budget — emit amber/red banner when approaching limit.
959
+ try {
960
+ var budget = _getBudgetStatus();
961
+ if (budget && budget.alert) {
962
+ if (budget.breached) {
963
+ 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.');
964
+ } else {
965
+ console.log('[BUDGET_ALERT] Daily ' + budget.dailyPct + '% of $' + budget.dailyLimit + ' · Monthly ' + budget.monthlyPct + '% of $' + budget.monthlyLimit + '. Adjust .monomind/budget.json if needed.');
966
+ }
967
+ }
968
+ } catch (e) {}
969
+
764
970
  // Inject monograph hint for complex tasks.
765
971
  // Source of truth is .monomind/monograph.db (SQLite). Legacy stats.json
766
972
  // is no longer written by the build, so it is checked only as a fallback.
@@ -888,7 +1094,8 @@ const handlers = {
888
1094
 
889
1095
  'pre-bash': () => {
890
1096
  var _rawCmd = hookInput.command || prompt;
891
- var cmd = (typeof _rawCmd === 'string' ? _rawCmd : String(_rawCmd || '')).toLowerCase();
1097
+ var rawCmdStr = (typeof _rawCmd === 'string' ? _rawCmd : String(_rawCmd || ''));
1098
+ var cmd = rawCmdStr.toLowerCase();
892
1099
  var dangerous = ['rm -rf /', 'rm -rf/', 'format c:', 'del /s /q c:\\', ':(){:|:&};:'];
893
1100
  for (var i = 0; i < dangerous.length; i++) {
894
1101
  if (cmd.includes(dangerous[i])) {
@@ -896,6 +1103,43 @@ const handlers = {
896
1103
  process.exit(1);
897
1104
  }
898
1105
  }
1106
+
1107
+ // Intercept Bash-side grep/rg/find/ag — close the loophole where LLM
1108
+ // bypasses the Grep tool by shelling out. Same monograph hint shape as pre-search.
1109
+ try {
1110
+ // Match: grep <flags> <pattern>, rg <flags> <pattern>, ag <pattern>, find . -name <pattern>
1111
+ var grepMatch = rawCmdStr.match(/\b(?:grep|rg|ag)\b(?:\s+-[a-zA-Z]+)*\s+(?:"([^"]+)"|'([^']+)'|([^\s|;&<>]+))/);
1112
+ var findMatch = rawCmdStr.match(/\bfind\b.*?-name\s+(?:"([^"]+)"|'([^']+)'|([^\s|;&<>]+))/);
1113
+ var pattern = '';
1114
+ if (grepMatch) {
1115
+ pattern = grepMatch[1] || grepMatch[2] || grepMatch[3] || '';
1116
+ _recordGraphTelemetry('bash_grep_call');
1117
+ } else if (findMatch) {
1118
+ pattern = findMatch[1] || findMatch[2] || findMatch[3] || '';
1119
+ _recordGraphTelemetry('bash_find_call');
1120
+ }
1121
+ if (pattern && pattern.length >= 3) {
1122
+ var sigB = 'Bash-grep:' + pattern.slice(0, 60);
1123
+ var countB = _recordToolCall(sigB);
1124
+ if (countB >= 3) {
1125
+ console.log('[LOOP_DRIFT] You\'ve grepped "' + pattern.slice(0, 40) + '" ' + countB + 'x — switch to monograph_query.');
1126
+ }
1127
+ }
1128
+ if (pattern && pattern.length >= 3) {
1129
+ var clean = pattern.replace(/[\\^$.*+?()\[\]{}|]/g, ' ').trim();
1130
+ if (clean.length >= 3) {
1131
+ var hits = getMonographSuggestions(clean, 5);
1132
+ if (hits.length > 0) {
1133
+ console.log('[MONOGRAPH_HIT] Graph has ' + hits.length + ' file(s) for "' + clean.slice(0, 40) + '" — consider monograph_query instead of shell grep:');
1134
+ for (var j = 0; j < hits.length; j++) {
1135
+ var h = hits[j];
1136
+ console.log(' · ' + h.name + ' [' + h.label + '] — ' + (h.file || ''));
1137
+ }
1138
+ }
1139
+ }
1140
+ }
1141
+ } catch (e) { /* non-fatal */ }
1142
+
899
1143
  console.log('[OK] Command validated');
900
1144
  },
901
1145
 
@@ -909,6 +1153,13 @@ const handlers = {
909
1153
  if (typeof pattern !== 'string' || pattern.length < 3) return;
910
1154
  var clean = pattern.replace(/[\\^$.*+?()\[\]{}|]/g, ' ').trim();
911
1155
  if (clean.length < 3) return;
1156
+
1157
+ // Loop drift detection
1158
+ var sig = (toolName || 'Search') + ':' + clean.slice(0, 60);
1159
+ var count = _recordToolCall(sig);
1160
+ if (count >= 3) {
1161
+ console.log('[LOOP_DRIFT] You\'ve searched "' + clean.slice(0, 40) + '" ' + count + 'x this session — try monograph_query or monograph_impact for a structural answer.');
1162
+ }
912
1163
  var suggestions = getMonographSuggestions(clean, 5);
913
1164
  if (suggestions.length === 0) return;
914
1165
  console.log('[MONOGRAPH_HIT] Graph already knows ' + suggestions.length + ' file(s) matching "' + clean.slice(0, 40) + '":');
@@ -956,6 +1207,24 @@ const handlers = {
956
1207
  intelligence.recordEdit(file);
957
1208
  } catch (e) { /* non-fatal */ }
958
1209
  }
1210
+ // Increment write counter and rebuild monograph when threshold hit.
1211
+ _maybeRebuildMonograph();
1212
+
1213
+ // Test feedback (detection-only): when editing a source file, list tests
1214
+ // that import it so the LLM/user knows what to verify next.
1215
+ try {
1216
+ var editedFile = hookInput.file_path || toolInput.file_path
1217
+ || process.env.TOOL_INPUT_file_path || args[0] || '';
1218
+ if (editedFile && !editedFile.match(/\.(test|spec)\./) && !editedFile.includes('__tests__')) {
1219
+ var affectedTests = _findAffectedTests(editedFile);
1220
+ if (affectedTests.length > 0) {
1221
+ console.log('[AFFECTED_TESTS] ' + affectedTests.length + ' test(s) cover this file:');
1222
+ for (var ti = 0; ti < Math.min(5, affectedTests.length); ti++) {
1223
+ console.log(' · ' + affectedTests[ti]);
1224
+ }
1225
+ }
1226
+ }
1227
+ } catch (e) {}
959
1228
  // ── Security-Sensitive File Auto-Alert ────────────────────────────────
960
1229
  // When editing auth, security, crypto, or env-related files, flag it
961
1230
  try {
@@ -1886,6 +2155,47 @@ const handlers = {
1886
2155
  'utf-8'
1887
2156
  );
1888
2157
  } catch (e) { /* non-fatal — never block a subagent from starting */ }
2158
+
2159
+ // Subagent context inheritance — inject graph god nodes + parent's last
2160
+ // pre-resolved suggestions so the spawned agent inherits spatial map
2161
+ // instead of starting blind.
2162
+ try {
2163
+ var subDb = _openMonographDb();
2164
+ if (subDb) {
2165
+ try {
2166
+ var godRows = subDb.prepare(
2167
+ "SELECT n.name, n.label, n.file_path AS file, " +
2168
+ "(SELECT COUNT(*) FROM edges WHERE source_id=n.id OR target_id=n.id) AS deg " +
2169
+ "FROM nodes n " +
2170
+ "WHERE n.label NOT IN ('Concept') AND n.file_path IS NOT NULL AND n.file_path != '' " +
2171
+ "ORDER BY deg DESC LIMIT 5"
2172
+ ).all();
2173
+ if (godRows.length > 0) {
2174
+ console.log('[MONOGRAPH_SUBAGENT_CTX] Graph map inherited from parent:');
2175
+ for (var gi = 0; gi < godRows.length; gi++) {
2176
+ var gr = godRows[gi];
2177
+ console.log(' · ' + gr.name + ' [' + gr.label + '] — ' + (gr.file || '') + ' (deg ' + gr.deg + ')');
2178
+ }
2179
+ // Also forward parent's last routing suggestion text if any
2180
+ try {
2181
+ var subAgentDesc = hookInput.description || hookInput.prompt_description || '';
2182
+ if (subAgentDesc && subAgentDesc.length > 8) {
2183
+ var subHints = getMonographSuggestions(subAgentDesc, 3);
2184
+ if (subHints.length > 0) {
2185
+ console.log(' Top files for this subagent task:');
2186
+ for (var si2 = 0; si2 < subHints.length; si2++) {
2187
+ var sh = subHints[si2];
2188
+ console.log(' · ' + sh.name + ' [' + sh.label + '] — ' + (sh.file || ''));
2189
+ }
2190
+ }
2191
+ }
2192
+ } catch (_) {}
2193
+ console.log(' Use mcp__monomind__monograph_suggest / monograph_query in this subagent before grepping.');
2194
+ }
2195
+ } finally { try { subDb.close(); } catch (_) {} }
2196
+ }
2197
+ } catch (e) { /* non-fatal */ }
2198
+
1889
2199
  console.log('[OK] Agent registered');
1890
2200
  },
1891
2201
 
@@ -1903,15 +2213,18 @@ const handlers = {
1903
2213
  };
1904
2214
 
1905
2215
  if (command && handlers[command]) {
2216
+ var _hookStart = Date.now();
1906
2217
  try {
1907
2218
  await Promise.resolve(handlers[command]());
1908
2219
  } catch (e) {
1909
2220
  console.log('[WARN] Hook ' + command + ' encountered an error: ' + e.message);
2221
+ } finally {
2222
+ try { _recordHookLatency(command, Date.now() - _hookStart); } catch (_) {}
1910
2223
  }
1911
2224
  } else if (command) {
1912
2225
  console.log('[OK] Hook: ' + command);
1913
2226
  } else {
1914
- console.log('Usage: hook-handler.cjs <route|pre-bash|post-edit|session-restore|session-end|pre-task|post-task|compact-manual|compact-auto|status|stats>');
2227
+ console.log('Usage: hook-handler.cjs <route|pre-bash|pre-search|post-edit|post-read|post-graph-tool|session-restore|session-end|pre-task|post-task|compact-manual|compact-auto|status|stats>');
1915
2228
  }
1916
2229
  }
1917
2230
 
@@ -696,20 +696,46 @@ function getGraphifyStats() {
696
696
  return { nodes: 0, edges: 0, exists: false };
697
697
  }
698
698
 
699
- // Graph usage telemetry ratio of monograph_* tool calls vs Grep/Glob
699
+ // Hook latencysum of mean times for hooks that fire on every prompt.
700
+ function getHookLatency() {
701
+ const p = path.join(CWD, '.monomind', 'metrics', 'hook-latency.json');
702
+ try {
703
+ if (!fs.existsSync(p)) return null;
704
+ const d = JSON.parse(fs.readFileSync(p, 'utf-8'));
705
+ // Hooks that run per-prompt: route, pre-search, post-read, post-edit
706
+ const perPrompt = ['route'];
707
+ let totalMs = 0;
708
+ let count = 0;
709
+ for (const h of perPrompt) {
710
+ if (d[h] && d[h].mean) { totalMs += d[h].mean; count++; }
711
+ }
712
+ if (count === 0) return null;
713
+ // Find slowest hook
714
+ let slowest = null;
715
+ for (const k of Object.keys(d)) {
716
+ if (k === 'lastUpdated' || !d[k] || typeof d[k] !== 'object') continue;
717
+ if (!slowest || d[k].mean > slowest.mean) slowest = { name: k, mean: d[k].mean };
718
+ }
719
+ return { perPromptMs: totalMs, slowest };
720
+ } catch { return null; }
721
+ }
722
+
723
+ // Graph usage telemetry — ratio of monograph_* vs Grep/Glob, plus $ saved
700
724
  function getGraphUsage() {
701
725
  const usagePath = path.join(CWD, '.monomind', 'metrics', 'graph-usage.json');
702
726
  try {
703
727
  if (!fs.existsSync(usagePath)) return null;
704
728
  const d = JSON.parse(fs.readFileSync(usagePath, 'utf-8'));
705
729
  const monograph = d.monograph_call || 0;
706
- const search = (d.grep_call || 0) + (d.glob_call || 0);
730
+ const search = (d.grep_call || 0) + (d.glob_call || 0)
731
+ + (d.bash_grep_call || 0) + (d.bash_find_call || 0);
707
732
  const total = monograph + search;
708
733
  if (total === 0) return null;
709
734
  return {
710
735
  monograph,
711
736
  search,
712
737
  pct: Math.round((monograph / total) * 100),
738
+ dollarsSaved: d.dollars_saved || 0,
713
739
  };
714
740
  } catch { return null; }
715
741
  }
@@ -1257,15 +1283,27 @@ function generateDashboard() {
1257
1283
  loopStr = `${x.slate}🔄 no active loops${x.reset}`;
1258
1284
  }
1259
1285
 
1260
- // Graph usage ratio — show only when there's data
1286
+ // Graph usage ratio + $ saved — show only when there's data
1261
1287
  const usage = getGraphUsage();
1262
1288
  let usageStr = '';
1263
1289
  if (usage) {
1264
1290
  const col = usage.pct >= 40 ? x.green : usage.pct >= 15 ? x.gold : x.coral;
1265
- usageStr = ` ${DIV} ${col}📊 graph ${usage.pct}%${x.reset}${x.slate} · grep ${100 - usage.pct}%${x.reset}`;
1291
+ const saved = usage.dollarsSaved > 0
1292
+ ? ` ${x.green}💰 +$${usage.dollarsSaved.toFixed(2)}${x.reset}`
1293
+ : '';
1294
+ usageStr = ` ${DIV} ${col}📊 graph ${usage.pct}%${x.reset}${x.slate} · grep ${100 - usage.pct}%${x.reset}${saved}`;
1295
+ }
1296
+
1297
+ // Hook latency — surface when slow (>500ms per prompt)
1298
+ const lat = getHookLatency();
1299
+ let latStr = '';
1300
+ if (lat && lat.perPromptMs > 500) {
1301
+ latStr = ` ${DIV} ${x.coral}⚡ hooks ${lat.perPromptMs}ms${x.reset}`;
1302
+ } else if (lat && lat.perPromptMs > 0) {
1303
+ latStr = ` ${DIV} ${x.dim}⚡ ${lat.perPromptMs}ms${x.reset}`;
1266
1304
  }
1267
1305
 
1268
- lines.push(`${x.purple}🤖 AGENT${x.reset} ${agentStr} ${DIV} ${loopStr}${usageStr}`);
1306
+ lines.push(`${x.purple}🤖 AGENT${x.reset} ${agentStr} ${DIV} ${loopStr}${usageStr}${latStr}`);
1269
1307
  lines.push(SEP);
1270
1308
 
1271
1309
  // ── Row 2: Graph freshness + Pending HIL ─────────────────────
@@ -1 +1 @@
1
- {"version":3,"file":"statusline-generator.d.ts","sourceRoot":"","sources":["../../../src/init/statusline-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CA0oCrE;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CA8BnE"}
1
+ {"version":3,"file":"statusline-generator.d.ts","sourceRoot":"","sources":["../../../src/init/statusline-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CA4qCrE;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CA8BnE"}
@@ -828,17 +828,39 @@ function getTriggerStats() {
828
828
  } catch { return { triggers: 0, agents: 0 }; }
829
829
  }
830
830
 
831
- // Graph usage telemetry ratio of monograph_* tool calls vs Grep/Glob
831
+ // Hook latencysurface slow per-prompt hooks in the statusline.
832
+ function getHookLatency() {
833
+ const p = path.join(CWD, '.monomind', 'metrics', 'hook-latency.json');
834
+ try {
835
+ if (!fs.existsSync(p)) return null;
836
+ const d = JSON.parse(fs.readFileSync(p, 'utf-8'));
837
+ const perPrompt = ['route'];
838
+ let totalMs = 0; let count = 0;
839
+ for (const h of perPrompt) {
840
+ if (d[h] && d[h].mean) { totalMs += d[h].mean; count++; }
841
+ }
842
+ if (count === 0) return null;
843
+ let slowest = null;
844
+ for (const k of Object.keys(d)) {
845
+ if (k === 'lastUpdated' || !d[k] || typeof d[k] !== 'object') continue;
846
+ if (!slowest || d[k].mean > slowest.mean) slowest = { name: k, mean: d[k].mean };
847
+ }
848
+ return { perPromptMs: totalMs, slowest: slowest };
849
+ } catch { return null; }
850
+ }
851
+
852
+ // Graph usage telemetry — ratio of monograph_* vs Grep/Glob/Bash-grep, + $ saved
832
853
  function getGraphUsage() {
833
854
  const usagePath = path.join(CWD, '.monomind', 'metrics', 'graph-usage.json');
834
855
  try {
835
856
  if (!fs.existsSync(usagePath)) return null;
836
857
  const d = JSON.parse(fs.readFileSync(usagePath, 'utf-8'));
837
858
  const monograph = d.monograph_call || 0;
838
- const search = (d.grep_call || 0) + (d.glob_call || 0);
859
+ const search = (d.grep_call || 0) + (d.glob_call || 0)
860
+ + (d.bash_grep_call || 0) + (d.bash_find_call || 0);
839
861
  const total = monograph + search;
840
862
  if (total === 0) return null;
841
- return { monograph: monograph, search: search, pct: Math.round((monograph / total) * 100) };
863
+ return { monograph: monograph, search: search, pct: Math.round((monograph / total) * 100), dollarsSaved: d.dollars_saved || 0 };
842
864
  } catch { return null; }
843
865
  }
844
866
 
@@ -1084,15 +1106,27 @@ function generateDashboard() {
1084
1106
  loopStr = \`\${x.slate}🔄 no active loops\${x.reset}\`;
1085
1107
  }
1086
1108
 
1087
- // Graph usage ratio — show only when there's data
1109
+ // Graph usage ratio + $ saved — show only when there's data
1088
1110
  const usage = getGraphUsage();
1089
1111
  let usageStr = '';
1090
1112
  if (usage) {
1091
1113
  const col = usage.pct >= 40 ? x.green : usage.pct >= 15 ? x.gold : x.coral;
1092
- usageStr = \` \${DIV} \${col}📊 graph \${usage.pct}%\${x.reset}\${x.slate} · grep \${100 - usage.pct}%\${x.reset}\`;
1114
+ const saved = usage.dollarsSaved > 0
1115
+ ? \` \${x.green}💰 +$\${usage.dollarsSaved.toFixed(2)}\${x.reset}\`
1116
+ : '';
1117
+ usageStr = \` \${DIV} \${col}📊 graph \${usage.pct}%\${x.reset}\${x.slate} · grep \${100 - usage.pct}%\${x.reset}\${saved}\`;
1118
+ }
1119
+
1120
+ // Hook latency — surface when slow (>500ms per prompt)
1121
+ const lat = getHookLatency();
1122
+ let latStr = '';
1123
+ if (lat && lat.perPromptMs > 500) {
1124
+ latStr = \` \${DIV} \${x.coral}⚡ hooks \${lat.perPromptMs}ms\${x.reset}\`;
1125
+ } else if (lat && lat.perPromptMs > 0) {
1126
+ latStr = \` \${DIV} \${x.dim}⚡ \${lat.perPromptMs}ms\${x.reset}\`;
1093
1127
  }
1094
1128
 
1095
- lines.push(\`\${x.purple}🤖 AGENT\${x.reset} \${agentStr} \${DIV} \${loopStr}\${usageStr}\`);
1129
+ lines.push(\`\${x.purple}🤖 AGENT\${x.reset} \${agentStr} \${DIV} \${loopStr}\${usageStr}\${latStr}\`);
1096
1130
  lines.push(SEP);
1097
1131
 
1098
1132
  // ── Row 2: Graph freshness + Pending HIL ─────────────────────
@@ -1 +1 @@
1
- {"version":3,"file":"statusline-generator.js","sourceRoot":"","sources":["../../../src/init/statusline-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH;;;;;;;;;GASG;AACH,MAAM,UAAU,wBAAwB,CAAC,OAAoB;IAC3D,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC;IAC5C,OAAO;;;;;;;;;;;;;;;;;;;;;;;eAuBM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgnCvB,CAAC;AACF,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,OAAoB;IACzD,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC;QAChC,OAAO,sCAAsC,CAAC;IAChD,CAAC;IAED,OAAO;;;;;;;;;;;;;;;;;;;;;;;;CAwBR,CAAC;AACF,CAAC"}
1
+ {"version":3,"file":"statusline-generator.js","sourceRoot":"","sources":["../../../src/init/statusline-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH;;;;;;;;;GASG;AACH,MAAM,UAAU,wBAAwB,CAAC,OAAoB;IAC3D,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC;IAC5C,OAAO;;;;;;;;;;;;;;;;;;;;;;;eAuBM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkpCvB,CAAC;AACF,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,OAAoB;IACzD,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC;QAChC,OAAO,sCAAsC,CAAC;IAChD,CAAC;IAED,OAAO;;;;;;;;;;;;;;;;;;;;;;;;CAwBR,CAAC;AACF,CAAC"}