monomind 1.12.0 → 1.13.0

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.
@@ -25,6 +25,30 @@ function _requireMonograph() {
25
25
  // Memoized at module scope — opening monograph.db can take 7-10s.
26
26
  // Callers MUST NOT close the returned handle.
27
27
  var _cachedMonographDb = undefined;
28
+
29
+ // LRU cache for getMonographSuggestions: avoids re-querying the DB for
30
+ // the same task text within a single hook execution process lifetime.
31
+ // Max 20 entries; evicts the least-recently-used on overflow.
32
+ var _suggestCache = { _map: Object.create(null), _order: [], _max: 20 };
33
+ function _suggestCacheGet(key) {
34
+ if (key in _suggestCache._map) {
35
+ // Move to end (most recently used)
36
+ var idx = _suggestCache._order.indexOf(key);
37
+ if (idx !== -1) { _suggestCache._order.splice(idx, 1); _suggestCache._order.push(key); }
38
+ return _suggestCache._map[key];
39
+ }
40
+ return undefined;
41
+ }
42
+ function _suggestCacheSet(key, value) {
43
+ if (!(key in _suggestCache._map)) {
44
+ if (_suggestCache._order.length >= _suggestCache._max) {
45
+ var evict = _suggestCache._order.shift();
46
+ delete _suggestCache._map[evict];
47
+ }
48
+ _suggestCache._order.push(key);
49
+ }
50
+ _suggestCache._map[key] = value;
51
+ }
28
52
  function _openMonographDb() {
29
53
  if (_cachedMonographDb !== undefined) return _cachedMonographDb;
30
54
  try {
@@ -39,6 +63,10 @@ function _openMonographDb() {
39
63
 
40
64
  function getMonographSuggestions(taskText, limit) {
41
65
  if (!taskText || typeof taskText !== 'string') return [];
66
+ // Fast path: return cached result for repeated identical queries.
67
+ var cacheKey = taskText.slice(0, 200) + '|' + (limit || 5);
68
+ var cached = _suggestCacheGet(cacheKey);
69
+ if (cached !== undefined) return cached;
42
70
  var db = _openMonographDb();
43
71
  if (!db) return [];
44
72
  try {
@@ -56,7 +84,7 @@ function getMonographSuggestions(taskText, limit) {
56
84
  var rows = [];
57
85
  try {
58
86
  rows = db.prepare(
59
- 'SELECT n.id, n.name, n.label, n.file_path AS file, ' +
87
+ 'SELECT n.id, n.name, n.label, n.file_path AS file, n.start_line AS startLine, ' +
60
88
  'bm25(nodes_fts) AS bm25_score, ' +
61
89
  '(SELECT COUNT(*) FROM edges WHERE source_id=n.id OR target_id=n.id) AS deg, ' +
62
90
  'CASE n.label WHEN \'File\' THEN 3 WHEN \'Function\' THEN 3 WHEN \'Class\' THEN 3 ' +
@@ -72,7 +100,7 @@ function getMonographSuggestions(taskText, limit) {
72
100
  var likeFrag = keys.map(function(){ return 'lower(n.name) LIKE ?'; }).join(' OR ');
73
101
  var likeArgs = keys.map(function(k){ return '%' + k + '%'; });
74
102
  var stmt = db.prepare(
75
- 'SELECT n.id, n.name, n.label, n.file_path AS file, ' +
103
+ 'SELECT n.id, n.name, n.label, n.file_path AS file, n.start_line AS startLine, ' +
76
104
  '(SELECT COUNT(*) FROM edges WHERE source_id=n.id OR target_id=n.id) AS deg ' +
77
105
  'FROM nodes n WHERE (' + likeFrag + ') AND n.file_path IS NOT NULL AND n.file_path != \'\' ' +
78
106
  'AND n.label NOT IN (\'Concept\') ' +
@@ -80,7 +108,9 @@ function getMonographSuggestions(taskText, limit) {
80
108
  );
81
109
  rows = stmt.all.apply(stmt, likeArgs.concat([lim]));
82
110
  }
83
- return rows || [];
111
+ var result = rows || [];
112
+ _suggestCacheSet(cacheKey, result);
113
+ return result;
84
114
  } catch (e) { return []; }
85
115
  finally { /* db is shared/cached; do not close */ }
86
116
  }
@@ -287,8 +317,13 @@ function injectGodNodesContext(CWD) {
287
317
  // Staleness indicator: compare stored commit hash with current HEAD.
288
318
  var staleIndicator = '';
289
319
  try {
320
+ // The orchestrator writes 'last_commit_hash'; fall back to legacy keys.
290
321
  var lastCommitRow = null;
291
- try { lastCommitRow = db.prepare("SELECT value FROM index_meta WHERE key='ua_last_commit'").get(); } catch (_) {}
322
+ try {
323
+ lastCommitRow = db.prepare("SELECT value FROM index_meta WHERE key='last_commit_hash'").get() ||
324
+ db.prepare("SELECT value FROM index_meta WHERE key='lastCommit'").get() ||
325
+ db.prepare("SELECT value FROM index_meta WHERE key='ua_last_commit'").get();
326
+ } catch (_) {}
292
327
  if (lastCommitRow && lastCommitRow.value) {
293
328
  var { execFileSync: execSync } = require('child_process');
294
329
  var currentHead = '';
@@ -10,14 +10,14 @@ const CWD = process.env.CLAUDE_PROJECT_DIR || process.cwd();
10
10
  function _recordRecentEdit(filePath) {
11
11
  if (!filePath) return;
12
12
  try {
13
- var relPath = path.isAbsolute(filePath) ? path.relative(CWD, filePath) : filePath;
13
+ var storedPath = filePath;
14
14
  var f = path.join(CWD, '.monomind', 'metrics', 'recent-edits.json');
15
15
  fs.mkdirSync(path.dirname(f), { recursive: true });
16
16
  var d = { edits: [] };
17
17
  try { d = JSON.parse(fs.readFileSync(f, 'utf-8')); } catch (_) {}
18
18
  if (!Array.isArray(d.edits)) d.edits = [];
19
- d.edits = d.edits.filter(function(e) { return e.file !== relPath; });
20
- d.edits.unshift({ file: relPath, editedAt: Date.now() });
19
+ d.edits = d.edits.filter(function(e) { return e.file !== storedPath; });
20
+ d.edits.unshift({ file: storedPath, editedAt: Date.now() });
21
21
  if (d.edits.length > 10) d.edits = d.edits.slice(0, 10);
22
22
  fs.writeFileSync(f, JSON.stringify(d));
23
23
  } catch (e) { /* non-fatal */ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "monomind",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "description": "Monomind - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -436,6 +436,84 @@ async function checkMonograph() {
436
436
  };
437
437
  }
438
438
  }
439
+ // Check monograph graph freshness (is the graph built? how stale?)
440
+ async function checkMonographFreshness() {
441
+ try {
442
+ const cwd = process.cwd();
443
+ const dbPath = join(cwd, '.monomind', 'monograph.db');
444
+ const lockPath = join(cwd, '.monomind', 'graph', '.rebuild-lock');
445
+ const statsPath = join(cwd, '.monomind', 'graph', 'stats.json');
446
+ // Check if graph exists at all
447
+ const hasDb = existsSync(dbPath);
448
+ const hasLock = existsSync(lockPath);
449
+ const hasStats = existsSync(statsPath);
450
+ if (!hasDb && !hasStats) {
451
+ return {
452
+ name: 'Graph freshness',
453
+ status: 'warn',
454
+ message: 'No monograph graph built yet',
455
+ fix: 'mcp__monomind__monograph_build codeOnly:true — or run npx monomind@latest hooks graph-status',
456
+ };
457
+ }
458
+ // Determine last build time
459
+ let buildMs = 0;
460
+ if (hasDb) {
461
+ try {
462
+ buildMs = Math.max(buildMs, statSync(dbPath).mtimeMs);
463
+ }
464
+ catch { /* ignore */ }
465
+ }
466
+ if (hasLock) {
467
+ try {
468
+ buildMs = Math.max(buildMs, statSync(lockPath).mtimeMs);
469
+ }
470
+ catch { /* ignore */ }
471
+ }
472
+ if (hasStats) {
473
+ try {
474
+ buildMs = Math.max(buildMs, statSync(statsPath).mtimeMs);
475
+ }
476
+ catch { /* ignore */ }
477
+ }
478
+ if (buildMs === 0) {
479
+ return { name: 'Graph freshness', status: 'warn', message: 'Graph exists but build time unknown' };
480
+ }
481
+ // Count commits since last build
482
+ const buildIso = new Date(buildMs).toISOString();
483
+ let commitsBehind = 0;
484
+ try {
485
+ const out = execSync(`git rev-list --count --since='${buildIso}' HEAD 2>/dev/null`, {
486
+ encoding: 'utf8', timeout: 2000, cwd,
487
+ }).trim();
488
+ commitsBehind = parseInt(out, 10) || 0;
489
+ }
490
+ catch { /* git not available or not a git repo */ }
491
+ const ageMinutes = Math.floor((Date.now() - buildMs) / 60000);
492
+ const ageStr = ageMinutes < 60 ? `${ageMinutes}m ago` : `${Math.floor(ageMinutes / 60)}h ago`;
493
+ if (commitsBehind === 0) {
494
+ return { name: 'Graph freshness', status: 'pass', message: `FRESH — built ${ageStr}, 0 commits behind` };
495
+ }
496
+ else if (commitsBehind <= 5) {
497
+ return {
498
+ name: 'Graph freshness',
499
+ status: 'warn',
500
+ message: `${commitsBehind} commit(s) behind — built ${ageStr}`,
501
+ fix: 'mcp__monomind__monograph_build codeOnly:true',
502
+ };
503
+ }
504
+ else {
505
+ return {
506
+ name: 'Graph freshness',
507
+ status: 'fail',
508
+ message: `STALE — ${commitsBehind} commits behind (built ${ageStr})`,
509
+ fix: 'mcp__monomind__monograph_build codeOnly:true',
510
+ };
511
+ }
512
+ }
513
+ catch {
514
+ return { name: 'Graph freshness', status: 'warn', message: 'Could not check graph freshness' };
515
+ }
516
+ }
439
517
  // Check @monoes/memory (optional HNSW vector search package)
440
518
  async function checkMonoesMemory() {
441
519
  try {
@@ -798,7 +876,7 @@ export const doctorCommand = {
798
876
  {
799
877
  name: 'component',
800
878
  short: 'c',
801
- description: 'Check specific component (version, node, npm, config, daemon, memory, api, git, mcp, claude, disk, typescript, monograph, memory-pkg, helpers, agentic-flow, monoes, gates, gitignore)',
879
+ description: 'Check specific component (version, node, npm, config, daemon, memory, api, git, mcp, claude, disk, typescript, monograph, graph-freshness, memory-pkg, helpers, agentic-flow, monoes, gates, gitignore)',
802
880
  type: 'string'
803
881
  },
804
882
  {
@@ -841,6 +919,7 @@ export const doctorCommand = {
841
919
  checkDiskSpace,
842
920
  checkBuildTools,
843
921
  checkMonograph,
922
+ checkMonographFreshness,
844
923
  checkMonoesMemory,
845
924
  checkHelpersFresh,
846
925
  checkAgenticFlow,
@@ -863,6 +942,7 @@ export const doctorCommand = {
863
942
  'disk': checkDiskSpace,
864
943
  'typescript': checkBuildTools,
865
944
  'monograph': checkMonograph,
945
+ 'graph-freshness': checkMonographFreshness,
866
946
  'memory-pkg': checkMonoesMemory,
867
947
  'helpers': checkHelpersFresh,
868
948
  'agentic-flow': checkAgenticFlow,
@@ -889,6 +969,14 @@ export const doctorCommand = {
889
969
  const result = settledResult.value;
890
970
  results.push(result);
891
971
  output.writeln(formatCheck(result));
972
+ if (result.fix && result.status === 'fail') {
973
+ // Always show fix inline for failures — no flag needed
974
+ output.writeln(output.dim(` Fix: ${result.fix}`));
975
+ }
976
+ else if (result.fix && result.status === 'warn') {
977
+ // Show fix inline for warnings too, so users don't need --fix for common issues
978
+ output.writeln(output.dim(` Hint: ${result.fix}`));
979
+ }
892
980
  if (result.fix && (result.status === 'fail' || result.status === 'warn')) {
893
981
  fixes.push(`${result.name}: ${result.fix}`);
894
982
  }
@@ -949,9 +1037,13 @@ export const doctorCommand = {
949
1037
  output.writeln(output.dim(` ${fix}`));
950
1038
  }
951
1039
  }
952
- else if (fixes.length > 0 && !showFix) {
953
- output.writeln();
954
- output.writeln(output.dim(`Run with --fix to see ${fixes.length} suggested fix${fixes.length > 1 ? 'es' : ''}`));
1040
+ else if (!showFix) {
1041
+ // Only nudge about --fix for warnings (failures already showed their fix inline)
1042
+ const warnFixes = results.filter(r => r.status === 'warn' && r.fix).length;
1043
+ if (warnFixes > 0) {
1044
+ output.writeln();
1045
+ output.writeln(output.dim(`Run with --fix to see ${warnFixes} suggested fix${warnFixes > 1 ? 'es' : ''} for warnings`));
1046
+ }
955
1047
  }
956
1048
  // Overall result
957
1049
  if (failed > 0) {