monomind 1.18.11 → 1.18.13
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/helpers/graphify-freshen.cjs +28 -0
- package/.claude/helpers/handlers/route-handler.cjs +1 -1
- package/.claude/helpers/hook-handler.cjs +5 -2
- package/.claude/helpers/utils/monograph.cjs +36 -10
- package/package.json +1 -1
- package/packages/@monomind/cli/.claude/helpers/graphify-freshen.cjs +28 -0
- package/packages/@monomind/cli/.claude/helpers/handlers/route-handler.cjs +1 -1
- package/packages/@monomind/cli/.claude/helpers/hook-handler.cjs +5 -2
- package/packages/@monomind/cli/.claude/helpers/utils/monograph.cjs +36 -10
- package/packages/@monomind/cli/dist/src/mcp-tools/monograph-tools.js +213 -1
- package/packages/@monomind/cli/package.json +1 -1
|
@@ -66,6 +66,34 @@ if (!entryPoint) {
|
|
|
66
66
|
process.exit(0);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
// Skip if index is already fresh — don't waste CPU on every session start
|
|
70
|
+
const dbPath = path.join(projectDir, '.monomind', 'monograph.db');
|
|
71
|
+
if (fs.existsSync(dbPath)) {
|
|
72
|
+
try {
|
|
73
|
+
const mod = (() => {
|
|
74
|
+
try { return require(path.join(projectDir, 'packages', '@monomind', 'monograph', 'dist', 'src', 'index.js')); } catch {}
|
|
75
|
+
try { return require('@monoes/monograph'); } catch {}
|
|
76
|
+
return null;
|
|
77
|
+
})();
|
|
78
|
+
if (mod && mod.openDb) {
|
|
79
|
+
const db = mod.openDb(dbPath);
|
|
80
|
+
try {
|
|
81
|
+
const row = db.prepare("SELECT value FROM index_meta WHERE key='last_commit_hash'").get();
|
|
82
|
+
if (row && row.value) {
|
|
83
|
+
const { execSync } = require('child_process');
|
|
84
|
+
const behind = parseInt(execSync('git rev-list --count ' + row.value + '..HEAD 2>/dev/null', {
|
|
85
|
+
cwd: projectDir, encoding: 'utf-8', timeout: 2000
|
|
86
|
+
}).trim(), 10) || 0;
|
|
87
|
+
if (behind === 0) {
|
|
88
|
+
console.log('[graph] index is fresh — skipping rebuild');
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} finally { mod.closeDb(db); }
|
|
93
|
+
}
|
|
94
|
+
} catch { /* can't check — proceed with build */ }
|
|
95
|
+
}
|
|
96
|
+
|
|
69
97
|
// Skip if another build is already in progress (avoids SQLite BUSY on concurrent init + session-start)
|
|
70
98
|
const lockPath = path.join(graphDir, 'build.lock');
|
|
71
99
|
const now = Date.now();
|
|
@@ -381,7 +381,7 @@ module.exports = {
|
|
|
381
381
|
}
|
|
382
382
|
} catch (e) { /* ignore */ }
|
|
383
383
|
}
|
|
384
|
-
if (nodeCount > 100) {
|
|
384
|
+
if (nodeCount > 100 && hCtx._isGraphFresh()) {
|
|
385
385
|
// Pre-resolve top-3 relevant files for the user's prompt — the LLM
|
|
386
386
|
// sees the answer inline instead of being told to call a tool.
|
|
387
387
|
// 3 is enough signal; more files inflate token cost on every prompt.
|
|
@@ -216,6 +216,7 @@ var hCtx = {
|
|
|
216
216
|
_recordToolCall: _recordToolCall,
|
|
217
217
|
_openMonographDb: _openMonographDb,
|
|
218
218
|
_requireMonograph: _requireMonograph,
|
|
219
|
+
_isGraphFresh: _isGraphFresh,
|
|
219
220
|
_triggerExtractYamlValue: _triggerExtractYamlValue,
|
|
220
221
|
_triggerFinalize: _triggerFinalize,
|
|
221
222
|
_triggerExtractFromFrontmatter: _triggerExtractFromFrontmatter,
|
|
@@ -312,7 +313,8 @@ const handlers = {
|
|
|
312
313
|
if (db) {
|
|
313
314
|
// Stop words: language keywords + generic single-word identifiers
|
|
314
315
|
// that match Variable nodes in markdown/scripts (not real code symbols)
|
|
315
|
-
|
|
316
|
+
// monolean: only JS/TS reserved words — generic identifiers (handler, config, router, etc.) are valid symbol names
|
|
317
|
+
var _grepStop = {import:1,export:1,from:1,require:1,return:1,function:1,const:1,let:1,var:1,class:1,interface:1,type:1,extends:1,implements:1,async:1,await:1,yield:1,throw:1,catch:1,finally:1,typeof:1,instanceof:1,void:1,null:1,undefined:1,true:1,false:1,this:1,super:1,new:1,delete:1,switch:1,case:1,break:1,continue:1,default:1,else:1,while:1,for:1,with:1,static:1,enum:1,string:1,number:1,object:1,boolean:1};
|
|
316
318
|
|
|
317
319
|
// --- Strategy 1: clean identifier → exact name match (case-sensitive) ---
|
|
318
320
|
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(pattern) && pattern.length >= 4
|
|
@@ -479,7 +481,8 @@ const handlers = {
|
|
|
479
481
|
var db = _openMonographDb();
|
|
480
482
|
if (db) {
|
|
481
483
|
try {
|
|
482
|
-
|
|
484
|
+
// monolean: only JS/TS reserved words — generic identifiers are valid symbol names
|
|
485
|
+
var _searchStop = {import:1,export:1,from:1,require:1,return:1,function:1,const:1,let:1,var:1,class:1,interface:1,type:1,extends:1,implements:1,async:1,await:1,yield:1,throw:1,catch:1,finally:1,typeof:1,instanceof:1,void:1,null:1,undefined:1,true:1,false:1,this:1,super:1,new:1,delete:1,switch:1,case:1,break:1,continue:1,default:1,else:1,while:1,for:1,with:1,static:1,enum:1,string:1,number:1,object:1,boolean:1};
|
|
483
486
|
|
|
484
487
|
var isCleanSymbol = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(grepPattern);
|
|
485
488
|
|
|
@@ -183,19 +183,31 @@ function _getDollarRate() {
|
|
|
183
183
|
|
|
184
184
|
// Staleness guard — skip graph DB lookups when the index is too far behind HEAD.
|
|
185
185
|
// Returns true if it's safe to use the graph, false if stale.
|
|
186
|
+
// Uses last_commit_hash from the DB (not file mtime, which drifts from backups/WAL/VACUUM).
|
|
186
187
|
var _graphFreshnessCache = undefined;
|
|
187
188
|
function _isGraphFresh() {
|
|
188
189
|
if (_graphFreshnessCache !== undefined) return _graphFreshnessCache;
|
|
189
190
|
try {
|
|
190
|
-
var
|
|
191
|
-
|
|
191
|
+
var db = _openMonographDb();
|
|
192
|
+
if (!db) { _graphFreshnessCache = true; return true; }
|
|
193
|
+
var row = null;
|
|
194
|
+
try {
|
|
195
|
+
row = db.prepare("SELECT value FROM index_meta WHERE key='last_commit_hash'").get() ||
|
|
196
|
+
db.prepare("SELECT value FROM index_meta WHERE key='lastCommit'").get();
|
|
197
|
+
} catch (_) {}
|
|
198
|
+
if (!row || !row.value) {
|
|
199
|
+
// No commit hash → can't check staleness, but DB may still have useful data
|
|
200
|
+
var nodeCount = 0;
|
|
201
|
+
try { nodeCount = db.prepare('SELECT COUNT(*) AS c FROM nodes').get().c; } catch (_) {}
|
|
202
|
+
_graphFreshnessCache = nodeCount > 0;
|
|
203
|
+
return _graphFreshnessCache;
|
|
204
|
+
}
|
|
192
205
|
var { execSync } = require('child_process');
|
|
193
|
-
var
|
|
194
|
-
var out = execSync("git rev-list --count --since='" + buildIso + "' HEAD 2>/dev/null", {
|
|
206
|
+
var out = execSync('git rev-list --count ' + row.value + '..HEAD 2>/dev/null', {
|
|
195
207
|
encoding: 'utf-8', timeout: 1500, stdio: ['pipe', 'pipe', 'pipe']
|
|
196
208
|
}).trim();
|
|
197
209
|
var behind = parseInt(out, 10) || 0;
|
|
198
|
-
_graphFreshnessCache = behind <= 50;
|
|
210
|
+
_graphFreshnessCache = behind <= 50;
|
|
199
211
|
} catch(e) {
|
|
200
212
|
_graphFreshnessCache = true; // can't check → assume fresh
|
|
201
213
|
}
|
|
@@ -354,14 +366,24 @@ function injectGodNodesContext(CWD) {
|
|
|
354
366
|
try {
|
|
355
367
|
var nodeCount = db.prepare('SELECT COUNT(*) AS c FROM nodes').get().c;
|
|
356
368
|
var edgeCount = db.prepare('SELECT COUNT(*) AS c FROM edges').get().c;
|
|
369
|
+
// Precompute degree via indexed edge lookups to avoid O(N) correlated subquery
|
|
357
370
|
var godNodes = db.prepare(
|
|
358
|
-
"
|
|
359
|
-
"
|
|
360
|
-
"FROM
|
|
371
|
+
"WITH deg AS (" +
|
|
372
|
+
" SELECT id, cnt FROM (" +
|
|
373
|
+
" SELECT source_id AS id, COUNT(*) AS cnt FROM edges GROUP BY source_id " +
|
|
374
|
+
" UNION ALL " +
|
|
375
|
+
" SELECT target_id AS id, COUNT(*) AS cnt FROM edges GROUP BY target_id" +
|
|
376
|
+
" ) GROUP BY id" +
|
|
377
|
+
" HAVING SUM(cnt) > 0" +
|
|
378
|
+
"), ranked AS (" +
|
|
379
|
+
" SELECT d.id, SUM(d.cnt) AS deg FROM deg d GROUP BY d.id ORDER BY deg DESC LIMIT 50" +
|
|
380
|
+
") " +
|
|
381
|
+
"SELECT n.name, n.label, n.file_path, r.deg " +
|
|
382
|
+
"FROM ranked r JOIN nodes n ON n.id = r.id " +
|
|
361
383
|
"WHERE n.label NOT IN ('Concept') " +
|
|
362
384
|
"AND n.file_path IS NOT NULL AND n.file_path != '' " +
|
|
363
385
|
"AND n.file_path NOT LIKE '%/node_modules/%' AND n.file_path NOT LIKE '%node_modules%' " +
|
|
364
|
-
"ORDER BY deg DESC LIMIT 12"
|
|
386
|
+
"ORDER BY r.deg DESC LIMIT 12"
|
|
365
387
|
).all();
|
|
366
388
|
|
|
367
389
|
// Staleness indicator: compare stored commit hash with current HEAD.
|
|
@@ -396,6 +418,7 @@ function injectGodNodesContext(CWD) {
|
|
|
396
418
|
return n.name + ' (' + n.label + ', ' + n.deg + ' links)';
|
|
397
419
|
}).join(', ');
|
|
398
420
|
console.log('[MONOGRAPH_CONTEXT] ' + nodeCount + ' nodes · ' + edgeCount + ' edges. Key nodes: ' + godStr + staleIndicator);
|
|
421
|
+
console.log('[MONOGRAPH_ACTIVE] Indexed knowledge graph available — prefer monograph_query / monograph_suggest over grep/find for symbol and definition lookups.');
|
|
399
422
|
|
|
400
423
|
// Write god nodes into knowledge/chunks.jsonl so semantic search finds them.
|
|
401
424
|
var knowledgeDir = path.join(CWD, '.monomind', 'knowledge');
|
|
@@ -416,7 +439,10 @@ function injectGodNodesContext(CWD) {
|
|
|
416
439
|
try { return JSON.parse(line).id !== 'monograph-god-nodes'; } catch(e) { return true; }
|
|
417
440
|
});
|
|
418
441
|
existing.push(godChunk);
|
|
419
|
-
|
|
442
|
+
// Atomic write: tmp file + rename to avoid corruption on kill/timeout
|
|
443
|
+
var tmpChunks = chunksFile + '.tmp.' + process.pid;
|
|
444
|
+
fs.writeFileSync(tmpChunks, existing.join('\n') + '\n');
|
|
445
|
+
fs.renameSync(tmpChunks, chunksFile);
|
|
420
446
|
} catch(e) {}
|
|
421
447
|
}
|
|
422
448
|
} catch(e) { /* non-fatal */ }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "monomind",
|
|
3
|
-
"version": "1.18.
|
|
3
|
+
"version": "1.18.13",
|
|
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",
|
|
@@ -66,6 +66,34 @@ if (!entryPoint) {
|
|
|
66
66
|
process.exit(0);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
// Skip if index is already fresh — don't waste CPU on every session start
|
|
70
|
+
const dbPath = path.join(projectDir, '.monomind', 'monograph.db');
|
|
71
|
+
if (fs.existsSync(dbPath)) {
|
|
72
|
+
try {
|
|
73
|
+
const mod = (() => {
|
|
74
|
+
try { return require(path.join(projectDir, 'packages', '@monomind', 'monograph', 'dist', 'src', 'index.js')); } catch {}
|
|
75
|
+
try { return require('@monoes/monograph'); } catch {}
|
|
76
|
+
return null;
|
|
77
|
+
})();
|
|
78
|
+
if (mod && mod.openDb) {
|
|
79
|
+
const db = mod.openDb(dbPath);
|
|
80
|
+
try {
|
|
81
|
+
const row = db.prepare("SELECT value FROM index_meta WHERE key='last_commit_hash'").get();
|
|
82
|
+
if (row && row.value) {
|
|
83
|
+
const { execSync } = require('child_process');
|
|
84
|
+
const behind = parseInt(execSync('git rev-list --count ' + row.value + '..HEAD 2>/dev/null', {
|
|
85
|
+
cwd: projectDir, encoding: 'utf-8', timeout: 2000
|
|
86
|
+
}).trim(), 10) || 0;
|
|
87
|
+
if (behind === 0) {
|
|
88
|
+
console.log('[graph] index is fresh — skipping rebuild');
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} finally { mod.closeDb(db); }
|
|
93
|
+
}
|
|
94
|
+
} catch { /* can't check — proceed with build */ }
|
|
95
|
+
}
|
|
96
|
+
|
|
69
97
|
// Skip if another build is already in progress (avoids SQLite BUSY on concurrent init + session-start)
|
|
70
98
|
const lockPath = path.join(graphDir, 'build.lock');
|
|
71
99
|
const now = Date.now();
|
|
@@ -381,7 +381,7 @@ module.exports = {
|
|
|
381
381
|
}
|
|
382
382
|
} catch (e) { /* ignore */ }
|
|
383
383
|
}
|
|
384
|
-
if (nodeCount > 100) {
|
|
384
|
+
if (nodeCount > 100 && hCtx._isGraphFresh()) {
|
|
385
385
|
// Pre-resolve top-3 relevant files for the user's prompt — the LLM
|
|
386
386
|
// sees the answer inline instead of being told to call a tool.
|
|
387
387
|
// 3 is enough signal; more files inflate token cost on every prompt.
|
|
@@ -216,6 +216,7 @@ var hCtx = {
|
|
|
216
216
|
_recordToolCall: _recordToolCall,
|
|
217
217
|
_openMonographDb: _openMonographDb,
|
|
218
218
|
_requireMonograph: _requireMonograph,
|
|
219
|
+
_isGraphFresh: _isGraphFresh,
|
|
219
220
|
_triggerExtractYamlValue: _triggerExtractYamlValue,
|
|
220
221
|
_triggerFinalize: _triggerFinalize,
|
|
221
222
|
_triggerExtractFromFrontmatter: _triggerExtractFromFrontmatter,
|
|
@@ -312,7 +313,8 @@ const handlers = {
|
|
|
312
313
|
if (db) {
|
|
313
314
|
// Stop words: language keywords + generic single-word identifiers
|
|
314
315
|
// that match Variable nodes in markdown/scripts (not real code symbols)
|
|
315
|
-
|
|
316
|
+
// monolean: only JS/TS reserved words — generic identifiers (handler, config, router, etc.) are valid symbol names
|
|
317
|
+
var _grepStop = {import:1,export:1,from:1,require:1,return:1,function:1,const:1,let:1,var:1,class:1,interface:1,type:1,extends:1,implements:1,async:1,await:1,yield:1,throw:1,catch:1,finally:1,typeof:1,instanceof:1,void:1,null:1,undefined:1,true:1,false:1,this:1,super:1,new:1,delete:1,switch:1,case:1,break:1,continue:1,default:1,else:1,while:1,for:1,with:1,static:1,enum:1,string:1,number:1,object:1,boolean:1};
|
|
316
318
|
|
|
317
319
|
// --- Strategy 1: clean identifier → exact name match (case-sensitive) ---
|
|
318
320
|
if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(pattern) && pattern.length >= 4
|
|
@@ -479,7 +481,8 @@ const handlers = {
|
|
|
479
481
|
var db = _openMonographDb();
|
|
480
482
|
if (db) {
|
|
481
483
|
try {
|
|
482
|
-
|
|
484
|
+
// monolean: only JS/TS reserved words — generic identifiers are valid symbol names
|
|
485
|
+
var _searchStop = {import:1,export:1,from:1,require:1,return:1,function:1,const:1,let:1,var:1,class:1,interface:1,type:1,extends:1,implements:1,async:1,await:1,yield:1,throw:1,catch:1,finally:1,typeof:1,instanceof:1,void:1,null:1,undefined:1,true:1,false:1,this:1,super:1,new:1,delete:1,switch:1,case:1,break:1,continue:1,default:1,else:1,while:1,for:1,with:1,static:1,enum:1,string:1,number:1,object:1,boolean:1};
|
|
483
486
|
|
|
484
487
|
var isCleanSymbol = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(grepPattern);
|
|
485
488
|
|
|
@@ -183,19 +183,31 @@ function _getDollarRate() {
|
|
|
183
183
|
|
|
184
184
|
// Staleness guard — skip graph DB lookups when the index is too far behind HEAD.
|
|
185
185
|
// Returns true if it's safe to use the graph, false if stale.
|
|
186
|
+
// Uses last_commit_hash from the DB (not file mtime, which drifts from backups/WAL/VACUUM).
|
|
186
187
|
var _graphFreshnessCache = undefined;
|
|
187
188
|
function _isGraphFresh() {
|
|
188
189
|
if (_graphFreshnessCache !== undefined) return _graphFreshnessCache;
|
|
189
190
|
try {
|
|
190
|
-
var
|
|
191
|
-
|
|
191
|
+
var db = _openMonographDb();
|
|
192
|
+
if (!db) { _graphFreshnessCache = true; return true; }
|
|
193
|
+
var row = null;
|
|
194
|
+
try {
|
|
195
|
+
row = db.prepare("SELECT value FROM index_meta WHERE key='last_commit_hash'").get() ||
|
|
196
|
+
db.prepare("SELECT value FROM index_meta WHERE key='lastCommit'").get();
|
|
197
|
+
} catch (_) {}
|
|
198
|
+
if (!row || !row.value) {
|
|
199
|
+
// No commit hash → can't check staleness, but DB may still have useful data
|
|
200
|
+
var nodeCount = 0;
|
|
201
|
+
try { nodeCount = db.prepare('SELECT COUNT(*) AS c FROM nodes').get().c; } catch (_) {}
|
|
202
|
+
_graphFreshnessCache = nodeCount > 0;
|
|
203
|
+
return _graphFreshnessCache;
|
|
204
|
+
}
|
|
192
205
|
var { execSync } = require('child_process');
|
|
193
|
-
var
|
|
194
|
-
var out = execSync("git rev-list --count --since='" + buildIso + "' HEAD 2>/dev/null", {
|
|
206
|
+
var out = execSync('git rev-list --count ' + row.value + '..HEAD 2>/dev/null', {
|
|
195
207
|
encoding: 'utf-8', timeout: 1500, stdio: ['pipe', 'pipe', 'pipe']
|
|
196
208
|
}).trim();
|
|
197
209
|
var behind = parseInt(out, 10) || 0;
|
|
198
|
-
_graphFreshnessCache = behind <= 50;
|
|
210
|
+
_graphFreshnessCache = behind <= 50;
|
|
199
211
|
} catch(e) {
|
|
200
212
|
_graphFreshnessCache = true; // can't check → assume fresh
|
|
201
213
|
}
|
|
@@ -354,14 +366,24 @@ function injectGodNodesContext(CWD) {
|
|
|
354
366
|
try {
|
|
355
367
|
var nodeCount = db.prepare('SELECT COUNT(*) AS c FROM nodes').get().c;
|
|
356
368
|
var edgeCount = db.prepare('SELECT COUNT(*) AS c FROM edges').get().c;
|
|
369
|
+
// Precompute degree via indexed edge lookups to avoid O(N) correlated subquery
|
|
357
370
|
var godNodes = db.prepare(
|
|
358
|
-
"
|
|
359
|
-
"
|
|
360
|
-
"FROM
|
|
371
|
+
"WITH deg AS (" +
|
|
372
|
+
" SELECT id, cnt FROM (" +
|
|
373
|
+
" SELECT source_id AS id, COUNT(*) AS cnt FROM edges GROUP BY source_id " +
|
|
374
|
+
" UNION ALL " +
|
|
375
|
+
" SELECT target_id AS id, COUNT(*) AS cnt FROM edges GROUP BY target_id" +
|
|
376
|
+
" ) GROUP BY id" +
|
|
377
|
+
" HAVING SUM(cnt) > 0" +
|
|
378
|
+
"), ranked AS (" +
|
|
379
|
+
" SELECT d.id, SUM(d.cnt) AS deg FROM deg d GROUP BY d.id ORDER BY deg DESC LIMIT 50" +
|
|
380
|
+
") " +
|
|
381
|
+
"SELECT n.name, n.label, n.file_path, r.deg " +
|
|
382
|
+
"FROM ranked r JOIN nodes n ON n.id = r.id " +
|
|
361
383
|
"WHERE n.label NOT IN ('Concept') " +
|
|
362
384
|
"AND n.file_path IS NOT NULL AND n.file_path != '' " +
|
|
363
385
|
"AND n.file_path NOT LIKE '%/node_modules/%' AND n.file_path NOT LIKE '%node_modules%' " +
|
|
364
|
-
"ORDER BY deg DESC LIMIT 12"
|
|
386
|
+
"ORDER BY r.deg DESC LIMIT 12"
|
|
365
387
|
).all();
|
|
366
388
|
|
|
367
389
|
// Staleness indicator: compare stored commit hash with current HEAD.
|
|
@@ -396,6 +418,7 @@ function injectGodNodesContext(CWD) {
|
|
|
396
418
|
return n.name + ' (' + n.label + ', ' + n.deg + ' links)';
|
|
397
419
|
}).join(', ');
|
|
398
420
|
console.log('[MONOGRAPH_CONTEXT] ' + nodeCount + ' nodes · ' + edgeCount + ' edges. Key nodes: ' + godStr + staleIndicator);
|
|
421
|
+
console.log('[MONOGRAPH_ACTIVE] Indexed knowledge graph available — prefer monograph_query / monograph_suggest over grep/find for symbol and definition lookups.');
|
|
399
422
|
|
|
400
423
|
// Write god nodes into knowledge/chunks.jsonl so semantic search finds them.
|
|
401
424
|
var knowledgeDir = path.join(CWD, '.monomind', 'knowledge');
|
|
@@ -416,7 +439,10 @@ function injectGodNodesContext(CWD) {
|
|
|
416
439
|
try { return JSON.parse(line).id !== 'monograph-god-nodes'; } catch(e) { return true; }
|
|
417
440
|
});
|
|
418
441
|
existing.push(godChunk);
|
|
419
|
-
|
|
442
|
+
// Atomic write: tmp file + rename to avoid corruption on kill/timeout
|
|
443
|
+
var tmpChunks = chunksFile + '.tmp.' + process.pid;
|
|
444
|
+
fs.writeFileSync(tmpChunks, existing.join('\n') + '\n');
|
|
445
|
+
fs.renameSync(tmpChunks, chunksFile);
|
|
420
446
|
} catch(e) {}
|
|
421
447
|
}
|
|
422
448
|
} catch(e) { /* non-fatal */ }
|
|
@@ -131,8 +131,18 @@ const monographHealthTool = {
|
|
|
131
131
|
// Fall back to legacy 'lastCommit' for indexes built with older versions.
|
|
132
132
|
const meta = db.prepare("SELECT value FROM index_meta WHERE key = 'last_commit_hash'").get() ?? db.prepare("SELECT value FROM index_meta WHERE key = 'lastCommit'").get();
|
|
133
133
|
const lastCommit = meta?.value ?? null;
|
|
134
|
-
if (!lastCommit)
|
|
134
|
+
if (!lastCommit) {
|
|
135
|
+
// last_commit_hash can be missing even when the index is populated
|
|
136
|
+
// (e.g. git rev-parse failed during build). Check actual data before
|
|
137
|
+
// claiming "never built".
|
|
138
|
+
const nodeCount = db.prepare('SELECT COUNT(*) AS c FROM nodes').get().c;
|
|
139
|
+
if (nodeCount > 0) {
|
|
140
|
+
const indexedAt = db.prepare("SELECT value FROM index_meta WHERE key = 'indexed_at'").get()?.value;
|
|
141
|
+
return text(`Index is built (${nodeCount} nodes${indexedAt ? `, indexed at ${indexedAt}` : ''}) but no commit hash was recorded — staleness tracking unavailable.\n` +
|
|
142
|
+
'Run monograph_build to fix commit tracking.');
|
|
143
|
+
}
|
|
135
144
|
return text('Index has never been built. Run monograph_build first.');
|
|
145
|
+
}
|
|
136
146
|
if (!/^[0-9a-f]{7,40}$/i.test(lastCommit)) {
|
|
137
147
|
return text('Index metadata is corrupt: invalid commit SHA. Run monograph_build to re-index.');
|
|
138
148
|
}
|
|
@@ -1809,6 +1819,207 @@ const monographSuggestAutoTool = {
|
|
|
1809
1819
|
},
|
|
1810
1820
|
};
|
|
1811
1821
|
// ── Export all tools ──────────────────────────────────────────────────────────
|
|
1822
|
+
// ── monograph_dead_code ──────────────────────────────────────────────────────
|
|
1823
|
+
const monographDeadCodeTool = {
|
|
1824
|
+
name: 'monograph_dead_code',
|
|
1825
|
+
description: 'Detect dead code: exported functions with zero inbound references, files with no importers, and stale dist build artifacts. Returns structured JSON with candidates grouped by category. Always verify candidates with grep before deleting.',
|
|
1826
|
+
inputSchema: {
|
|
1827
|
+
type: 'object',
|
|
1828
|
+
properties: {
|
|
1829
|
+
path: { type: 'string', description: 'Absolute path to the repo (defaults to project cwd)' },
|
|
1830
|
+
categories: {
|
|
1831
|
+
type: 'array',
|
|
1832
|
+
items: { type: 'string', enum: ['dead-functions', 'orphan-files', 'stale-dist'] },
|
|
1833
|
+
description: 'Which categories to check (default: all three)',
|
|
1834
|
+
},
|
|
1835
|
+
},
|
|
1836
|
+
},
|
|
1837
|
+
handler: async (input) => {
|
|
1838
|
+
const { openDb } = await import('@monoes/monograph');
|
|
1839
|
+
const repoPath = input.path ?? getProjectCwd();
|
|
1840
|
+
const cats = input.categories ?? ['dead-functions', 'orphan-files', 'stale-dist'];
|
|
1841
|
+
const result = {};
|
|
1842
|
+
const dbPath = join(repoPath, '.monomind', 'monograph.db');
|
|
1843
|
+
let db = null;
|
|
1844
|
+
try {
|
|
1845
|
+
db = openDb(dbPath);
|
|
1846
|
+
}
|
|
1847
|
+
catch {
|
|
1848
|
+
return text(JSON.stringify({ error: 'No monograph index found. Run monograph_build first.' }));
|
|
1849
|
+
}
|
|
1850
|
+
try {
|
|
1851
|
+
if (cats.includes('dead-functions')) {
|
|
1852
|
+
const { detectDeadCodeNodes } = await import('@monoes/monograph');
|
|
1853
|
+
const { readFileSync } = await import('fs');
|
|
1854
|
+
const nodes = detectDeadCodeNodes(db);
|
|
1855
|
+
// Filter out stale graph nodes: verify the function name actually appears in the source file
|
|
1856
|
+
const verified = nodes.filter(n => {
|
|
1857
|
+
if (!n.filePath)
|
|
1858
|
+
return false;
|
|
1859
|
+
try {
|
|
1860
|
+
const content = readFileSync(join(repoPath, n.filePath), 'utf-8');
|
|
1861
|
+
return content.includes(n.name);
|
|
1862
|
+
}
|
|
1863
|
+
catch {
|
|
1864
|
+
return false;
|
|
1865
|
+
}
|
|
1866
|
+
});
|
|
1867
|
+
const staleCount = nodes.length - verified.length;
|
|
1868
|
+
result['dead-functions'] = {
|
|
1869
|
+
count: verified.length,
|
|
1870
|
+
candidates: verified.map(n => ({
|
|
1871
|
+
name: n.name,
|
|
1872
|
+
location: n.filePath ? (n.startLine ? `${n.filePath}:${n.startLine}` : n.filePath) : null,
|
|
1873
|
+
})),
|
|
1874
|
+
...(staleCount > 0 ? { staleIndexEntries: staleCount, note: 'Some graph entries reference deleted functions. Rebuild the index with monograph_build to clean up.' } : {}),
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
if (cats.includes('orphan-files')) {
|
|
1878
|
+
const rows = db.prepare(`
|
|
1879
|
+
SELECT n.name, n.file_path,
|
|
1880
|
+
(SELECT COUNT(*) FROM edges e WHERE e.source_id = n.id AND e.relation = 'IMPORTS') as imports_out,
|
|
1881
|
+
(SELECT COUNT(*) FROM edges e WHERE e.target_id = n.id AND e.relation = 'IMPORTS') as imported_by
|
|
1882
|
+
FROM nodes n
|
|
1883
|
+
WHERE n.label = 'File'
|
|
1884
|
+
AND (SELECT COUNT(*) FROM edges e WHERE e.target_id = n.id AND e.relation = 'IMPORTS') = 0
|
|
1885
|
+
AND n.file_path NOT LIKE '%test%'
|
|
1886
|
+
AND n.file_path NOT LIKE '%__tests__%'
|
|
1887
|
+
AND n.file_path NOT LIKE '%spec%'
|
|
1888
|
+
AND n.file_path NOT LIKE '%/index.%'
|
|
1889
|
+
AND n.file_path NOT LIKE 'bin/%'
|
|
1890
|
+
AND n.file_path NOT LIKE 'scripts/%'
|
|
1891
|
+
AND n.file_path NOT LIKE '%/cli.ts'
|
|
1892
|
+
AND n.file_path NOT LIKE '%/cli.js'
|
|
1893
|
+
AND n.file_path NOT LIKE '%/main.ts'
|
|
1894
|
+
AND n.file_path NOT LIKE '%/main.js'
|
|
1895
|
+
AND n.file_path NOT LIKE '%/dist/%'
|
|
1896
|
+
AND n.file_path NOT LIKE '%node_modules%'
|
|
1897
|
+
ORDER BY n.file_path
|
|
1898
|
+
`).all();
|
|
1899
|
+
const withOutbound = rows.filter((r) => r.imports_out > 0);
|
|
1900
|
+
const isolated = rows.filter((r) => r.imports_out === 0);
|
|
1901
|
+
result['orphan-files'] = {
|
|
1902
|
+
count: withOutbound.length,
|
|
1903
|
+
note: 'Files that import other modules but nothing imports them. May be lazy-loaded or dynamically imported — verify with grep.',
|
|
1904
|
+
candidates: withOutbound.map((r) => ({ file: r.file_path, outboundImports: r.imports_out })),
|
|
1905
|
+
...(isolated.length > 0 ? {
|
|
1906
|
+
isolated: {
|
|
1907
|
+
count: isolated.length,
|
|
1908
|
+
note: 'Files with zero edges in either direction — likely standalone scripts or entry points.',
|
|
1909
|
+
files: isolated.map((r) => r.file_path),
|
|
1910
|
+
},
|
|
1911
|
+
} : {}),
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
if (cats.includes('stale-dist')) {
|
|
1915
|
+
result['stale-dist'] = findStaleDist(repoPath);
|
|
1916
|
+
}
|
|
1917
|
+
return text(JSON.stringify(result, null, 2));
|
|
1918
|
+
}
|
|
1919
|
+
finally {
|
|
1920
|
+
db.close();
|
|
1921
|
+
}
|
|
1922
|
+
},
|
|
1923
|
+
};
|
|
1924
|
+
function findStaleDist(repoPath) {
|
|
1925
|
+
const { readdirSync, existsSync } = require('fs');
|
|
1926
|
+
const distSrc = join(repoPath, 'dist', 'src');
|
|
1927
|
+
const src = join(repoPath, 'src');
|
|
1928
|
+
// Scan a single package for stale dist artifacts
|
|
1929
|
+
const scanPkg = (pkgPath, pkgName) => {
|
|
1930
|
+
const pkgDistSrc = join(pkgPath, 'dist', 'src');
|
|
1931
|
+
const pkgSrc = join(pkgPath, 'src');
|
|
1932
|
+
if (!existsSync(pkgDistSrc) || !existsSync(pkgSrc))
|
|
1933
|
+
return null;
|
|
1934
|
+
const staleDirs = [];
|
|
1935
|
+
const staleFiles = [];
|
|
1936
|
+
let resourceForks = 0;
|
|
1937
|
+
try {
|
|
1938
|
+
const distDirs = readdirSync(pkgDistSrc, { withFileTypes: true })
|
|
1939
|
+
.filter(d => d.isDirectory() && !d.name.startsWith('.') && !d.name.startsWith('._'));
|
|
1940
|
+
const srcDirs = new Set(readdirSync(pkgSrc, { withFileTypes: true })
|
|
1941
|
+
.filter(d => d.isDirectory() && !d.name.startsWith('.'))
|
|
1942
|
+
.map(d => d.name));
|
|
1943
|
+
for (const d of distDirs) {
|
|
1944
|
+
if (!srcDirs.has(d.name))
|
|
1945
|
+
staleDirs.push(d.name);
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
catch { /* skip */ }
|
|
1949
|
+
try {
|
|
1950
|
+
const distFiles = readdirSync(pkgDistSrc)
|
|
1951
|
+
.filter(f => f.endsWith('.js') && !f.startsWith('.') && !f.startsWith('._'));
|
|
1952
|
+
for (const f of distFiles) {
|
|
1953
|
+
const tsName = f.replace(/\.js$/, '.ts');
|
|
1954
|
+
if (!existsSync(join(pkgSrc, tsName)))
|
|
1955
|
+
staleFiles.push(f);
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
catch { /* skip */ }
|
|
1959
|
+
// Count macOS resource fork files
|
|
1960
|
+
const countRF = (dir) => {
|
|
1961
|
+
try {
|
|
1962
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1963
|
+
if (entry.name.startsWith('._'))
|
|
1964
|
+
resourceForks++;
|
|
1965
|
+
else if (entry.isDirectory())
|
|
1966
|
+
countRF(join(dir, entry.name));
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
catch { /* skip */ }
|
|
1970
|
+
};
|
|
1971
|
+
countRF(pkgDistSrc);
|
|
1972
|
+
if (staleDirs.length === 0 && staleFiles.length === 0 && resourceForks === 0)
|
|
1973
|
+
return null;
|
|
1974
|
+
return {
|
|
1975
|
+
package: pkgName,
|
|
1976
|
+
staleDirs,
|
|
1977
|
+
staleFiles,
|
|
1978
|
+
...(resourceForks > 0 ? { macosResourceForks: resourceForks } : {}),
|
|
1979
|
+
};
|
|
1980
|
+
};
|
|
1981
|
+
// Single-package repo
|
|
1982
|
+
if (existsSync(distSrc) && existsSync(src)) {
|
|
1983
|
+
const finding = scanPkg(repoPath, '.');
|
|
1984
|
+
return {
|
|
1985
|
+
count: finding ? finding.staleDirs.length + finding.staleFiles.length : 0,
|
|
1986
|
+
note: 'Directories/files in dist/src/ with no corresponding source. Fix: rm -rf dist && npm run build.',
|
|
1987
|
+
...(finding ? { findings: [finding] } : {}),
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
1990
|
+
// Monorepo: scan all packages
|
|
1991
|
+
const packagesDir = join(repoPath, 'packages');
|
|
1992
|
+
if (!existsSync(packagesDir))
|
|
1993
|
+
return { count: 0, note: 'No dist/src or packages/ found' };
|
|
1994
|
+
const findings = [];
|
|
1995
|
+
try {
|
|
1996
|
+
for (const scope of readdirSync(packagesDir, { withFileTypes: true })) {
|
|
1997
|
+
if (!scope.isDirectory())
|
|
1998
|
+
continue;
|
|
1999
|
+
const scopeDir = join(packagesDir, scope.name);
|
|
2000
|
+
if (scope.name.startsWith('@')) {
|
|
2001
|
+
for (const pkg of readdirSync(scopeDir, { withFileTypes: true })) {
|
|
2002
|
+
if (!pkg.isDirectory())
|
|
2003
|
+
continue;
|
|
2004
|
+
const f = scanPkg(join(scopeDir, pkg.name), `${scope.name}/${pkg.name}`);
|
|
2005
|
+
if (f)
|
|
2006
|
+
findings.push(f);
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
else {
|
|
2010
|
+
const f = scanPkg(scopeDir, scope.name);
|
|
2011
|
+
if (f)
|
|
2012
|
+
findings.push(f);
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
catch { /* skip */ }
|
|
2017
|
+
return {
|
|
2018
|
+
count: findings.reduce((s, f) => s + (f.staleDirs?.length ?? 0) + (f.staleFiles?.length ?? 0), 0),
|
|
2019
|
+
note: 'Directories/files in dist/src/ with no corresponding source. Fix: rm -rf dist && npm run build.',
|
|
2020
|
+
findings,
|
|
2021
|
+
};
|
|
2022
|
+
}
|
|
1812
2023
|
export const monographTools = [
|
|
1813
2024
|
monographBuildTool,
|
|
1814
2025
|
monographQueryTool,
|
|
@@ -1854,5 +2065,6 @@ export const monographTools = [
|
|
|
1854
2065
|
monographListReposTool,
|
|
1855
2066
|
monographGroupContractsTool,
|
|
1856
2067
|
monographGroupStatusTool,
|
|
2068
|
+
monographDeadCodeTool,
|
|
1857
2069
|
];
|
|
1858
2070
|
//# sourceMappingURL=monograph-tools.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monoes/monomindcli",
|
|
3
|
-
"version": "1.18.
|
|
3
|
+
"version": "1.18.13",
|
|
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",
|