@monoes/monomindcli 1.10.11 → 1.10.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/commands/monomind/adr.md +11 -0
- package/.claude/helpers/hook-handler.cjs +94 -4
- package/.claude/helpers/statusline.cjs +14 -8
- package/dist/src/init/statusline-generator.d.ts.map +1 -1
- package/dist/src/init/statusline-generator.js +8 -6
- package/dist/src/init/statusline-generator.js.map +1 -1
- package/dist/src/ui/server.mjs +64 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Draft an Architecture Decision Record from accumulated decision markers in this session's prompts
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Run the hook handler's adr-draft action to scan `.monomind/decisions.jsonl` for the last 7 days of decision markers (e.g. "let's go with X", "we chose Y", "decision: Z") and produce an ADR template in `docs/adrs/`.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
node "$CLAUDE_PROJECT_DIR/.claude/helpers/hook-handler.cjs" adr-draft
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
After running, open the generated `docs/adrs/ADR-NNNN-YYYY-MM-DD-session-decisions.md`, fill in the Context and Consequences sections, and change Status to Accepted/Rejected.
|
|
@@ -62,6 +62,8 @@ function getMonographSuggestions(taskText, limit) {
|
|
|
62
62
|
try {
|
|
63
63
|
// BM25 ranks better than degree for keyword relevance; tie-break by deg.
|
|
64
64
|
// File/Function/Class outrank Section so navigable nodes win.
|
|
65
|
+
// Filter out anonymous lambdas, arrow expressions, and other unnamed
|
|
66
|
+
// garbage that the AST extraction picks up but isn't navigable.
|
|
65
67
|
rows = db.prepare(
|
|
66
68
|
"SELECT n.id, n.name, n.label, n.file_path AS file, " +
|
|
67
69
|
"bm25(nodes_fts) AS bm25_score, " +
|
|
@@ -71,6 +73,8 @@ function getMonographSuggestions(taskText, limit) {
|
|
|
71
73
|
"FROM nodes_fts f JOIN nodes n ON f.rowid = n.rowid " +
|
|
72
74
|
"WHERE nodes_fts MATCH ? AND n.file_path IS NOT NULL AND n.file_path != '' " +
|
|
73
75
|
"AND n.label NOT IN ('Concept') " +
|
|
76
|
+
"AND n.name NOT LIKE '(%' AND n.name NOT LIKE '%=>%' AND n.name != 'function' " +
|
|
77
|
+
"AND length(n.name) >= 3 " +
|
|
74
78
|
"ORDER BY label_rank DESC, bm25_score ASC, deg DESC LIMIT ?"
|
|
75
79
|
).all(ftsQuery, lim);
|
|
76
80
|
} catch (e) {
|
|
@@ -157,6 +161,33 @@ function _recordGraphTelemetry(event) {
|
|
|
157
161
|
} catch (e) { /* non-fatal */ }
|
|
158
162
|
}
|
|
159
163
|
|
|
164
|
+
// Re-inject graph god nodes after compaction so the LLM doesn't lose its spatial map.
|
|
165
|
+
function _injectCompactGraphMap() {
|
|
166
|
+
try {
|
|
167
|
+
var db = _openMonographDb();
|
|
168
|
+
if (!db) return;
|
|
169
|
+
try {
|
|
170
|
+
var nodeC = db.prepare("SELECT COUNT(*) AS c FROM nodes").get().c;
|
|
171
|
+
var gods = db.prepare(
|
|
172
|
+
"SELECT n.name, n.label, n.file_path, " +
|
|
173
|
+
"(SELECT COUNT(*) FROM edges WHERE source_id=n.id OR target_id=n.id) AS deg " +
|
|
174
|
+
"FROM nodes n " +
|
|
175
|
+
"WHERE n.label NOT IN ('Concept') AND n.file_path IS NOT NULL AND n.file_path != '' " +
|
|
176
|
+
"AND n.name NOT LIKE '(%' AND n.name NOT LIKE '%=>%' AND length(n.name) >= 3 " +
|
|
177
|
+
"ORDER BY deg DESC LIMIT 8"
|
|
178
|
+
).all();
|
|
179
|
+
if (gods.length > 0) {
|
|
180
|
+
console.log('[COMPACT_GRAPH] ' + nodeC + ' nodes available. Top spatial anchors:');
|
|
181
|
+
for (var ci = 0; ci < gods.length; ci++) {
|
|
182
|
+
var g = gods[ci];
|
|
183
|
+
console.log(' · ' + g.name + ' [' + g.label + '] — ' + g.file_path + ' (deg ' + g.deg + ')');
|
|
184
|
+
}
|
|
185
|
+
console.log(' Use mcp__monomind__monograph_suggest first when navigating.');
|
|
186
|
+
}
|
|
187
|
+
} finally { try { db.close(); } catch (_) {} }
|
|
188
|
+
} catch (e) {}
|
|
189
|
+
}
|
|
190
|
+
|
|
160
191
|
// ── Loop drift detection ───────────────────────────────────────────────────────
|
|
161
192
|
// Record tool call signatures per session, warn when the same call recurs ≥3×.
|
|
162
193
|
function _recordToolCall(signature) {
|
|
@@ -1130,6 +1161,7 @@ const handlers = {
|
|
|
1130
1161
|
if (clean.length >= 3) {
|
|
1131
1162
|
var hits = getMonographSuggestions(clean, 5);
|
|
1132
1163
|
if (hits.length > 0) {
|
|
1164
|
+
_recordGraphTelemetry('graph_assist_search');
|
|
1133
1165
|
console.log('[MONOGRAPH_HIT] Graph has ' + hits.length + ' file(s) for "' + clean.slice(0, 40) + '" — consider monograph_query instead of shell grep:');
|
|
1134
1166
|
for (var j = 0; j < hits.length; j++) {
|
|
1135
1167
|
var h = hits[j];
|
|
@@ -1154,7 +1186,6 @@ const handlers = {
|
|
|
1154
1186
|
var clean = pattern.replace(/[\\^$.*+?()\[\]{}|]/g, ' ').trim();
|
|
1155
1187
|
if (clean.length < 3) return;
|
|
1156
1188
|
|
|
1157
|
-
// Loop drift detection
|
|
1158
1189
|
var sig = (toolName || 'Search') + ':' + clean.slice(0, 60);
|
|
1159
1190
|
var count = _recordToolCall(sig);
|
|
1160
1191
|
if (count >= 3) {
|
|
@@ -1162,6 +1193,9 @@ const handlers = {
|
|
|
1162
1193
|
}
|
|
1163
1194
|
var suggestions = getMonographSuggestions(clean, 5);
|
|
1164
1195
|
if (suggestions.length === 0) return;
|
|
1196
|
+
// Successful intercept — count as a "graph assist" so the ratio reflects
|
|
1197
|
+
// server-side wins, not just LLM-initiated MCP calls.
|
|
1198
|
+
_recordGraphTelemetry('graph_assist_search');
|
|
1165
1199
|
console.log('[MONOGRAPH_HIT] Graph already knows ' + suggestions.length + ' file(s) matching "' + clean.slice(0, 40) + '":');
|
|
1166
1200
|
for (var i = 0; i < suggestions.length; i++) {
|
|
1167
1201
|
var s = suggestions[i];
|
|
@@ -1183,6 +1217,7 @@ const handlers = {
|
|
|
1183
1217
|
if (n.importedBy.length > 0) parts.push('imported-by: ' + n.importedBy.slice(0, 4).join(', '));
|
|
1184
1218
|
if (n.imports.length > 0) parts.push('imports: ' + n.imports.slice(0, 4).join(', '));
|
|
1185
1219
|
if (parts.length === 0) return;
|
|
1220
|
+
_recordGraphTelemetry('graph_assist_neighbors');
|
|
1186
1221
|
console.log('[MONOGRAPH_NEIGHBORS] ' + parts.join(' · '));
|
|
1187
1222
|
} catch (e) { /* non-fatal */ }
|
|
1188
1223
|
},
|
|
@@ -2080,11 +2115,9 @@ const handlers = {
|
|
|
2080
2115
|
},
|
|
2081
2116
|
|
|
2082
2117
|
'compact-manual': async () => {
|
|
2083
|
-
// Consolidate intelligence before compaction so patterns survive
|
|
2084
2118
|
if (intelligence && intelligence.consolidate) {
|
|
2085
2119
|
try { await runWithTimeout(function() { return intelligence.consolidate(); }, 'intelligence.consolidate()'); } catch (e) { /* non-fatal */ }
|
|
2086
2120
|
}
|
|
2087
|
-
// Save current routing context for post-compact restore
|
|
2088
2121
|
try {
|
|
2089
2122
|
var lastRoute = path.join(CWD, '.monomind', 'last-route.json');
|
|
2090
2123
|
if (fs.existsSync(lastRoute)) {
|
|
@@ -2092,11 +2125,11 @@ const handlers = {
|
|
|
2092
2125
|
console.log('[COMPACT_CONTEXT] Last route: ' + route.agent + ' (' + (route.confidence != null ? (route.confidence * 100).toFixed(0) : '?') + '%)');
|
|
2093
2126
|
}
|
|
2094
2127
|
} catch (e) { /* non-fatal */ }
|
|
2128
|
+
_injectCompactGraphMap();
|
|
2095
2129
|
console.log('[COMPACT] Manual compaction — intelligence consolidated, context preserved');
|
|
2096
2130
|
},
|
|
2097
2131
|
|
|
2098
2132
|
'compact-auto': async () => {
|
|
2099
|
-
// Same consolidation for auto-compact
|
|
2100
2133
|
if (intelligence && intelligence.consolidate) {
|
|
2101
2134
|
try { await runWithTimeout(function() { return intelligence.consolidate(); }, 'intelligence.consolidate()'); } catch (e) { /* non-fatal */ }
|
|
2102
2135
|
}
|
|
@@ -2107,6 +2140,7 @@ const handlers = {
|
|
|
2107
2140
|
console.log('[COMPACT_CONTEXT] Last route: ' + route.agent + ' (' + (route.confidence != null ? (route.confidence * 100).toFixed(0) : '?') + '%)');
|
|
2108
2141
|
}
|
|
2109
2142
|
} catch (e) { /* non-fatal */ }
|
|
2143
|
+
_injectCompactGraphMap();
|
|
2110
2144
|
console.log('[COMPACT] Auto compaction — intelligence consolidated, context preserved');
|
|
2111
2145
|
console.log('GOLDEN RULE: 1 message = all parallel operations');
|
|
2112
2146
|
},
|
|
@@ -2199,6 +2233,62 @@ const handlers = {
|
|
|
2199
2233
|
console.log('[OK] Agent registered');
|
|
2200
2234
|
},
|
|
2201
2235
|
|
|
2236
|
+
// Draft an ADR from accumulated decision markers in .monomind/decisions.jsonl.
|
|
2237
|
+
// Usage: node hook-handler.cjs adr-draft (or via /adr slash command)
|
|
2238
|
+
'adr-draft': () => {
|
|
2239
|
+
var jsonl = path.join(CWD, '.monomind', 'decisions.jsonl');
|
|
2240
|
+
if (!fs.existsSync(jsonl)) {
|
|
2241
|
+
console.log('[ADR] No decisions recorded yet. Type prompts containing markers like "let\'s go with X", "we chose Y", "decision: Z" to populate the log.');
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
var lines = fs.readFileSync(jsonl, 'utf-8').trim().split('\n').filter(Boolean);
|
|
2245
|
+
if (lines.length === 0) {
|
|
2246
|
+
console.log('[ADR] decisions.jsonl is empty.');
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
// Group decisions captured in the last 7 days
|
|
2250
|
+
var cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
|
2251
|
+
var recent = lines.map(function(l) { try { return JSON.parse(l); } catch (_) { return null; } })
|
|
2252
|
+
.filter(function(d) { return d && d.ts >= cutoff; });
|
|
2253
|
+
if (recent.length === 0) {
|
|
2254
|
+
console.log('[ADR] No decisions in the last 7 days. Older entries: ' + lines.length + '.');
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
var adrsDir = path.join(CWD, 'docs', 'adrs');
|
|
2259
|
+
try { fs.mkdirSync(adrsDir, { recursive: true }); } catch (_) {}
|
|
2260
|
+
// Pick next ADR number
|
|
2261
|
+
var existing = [];
|
|
2262
|
+
try { existing = fs.readdirSync(adrsDir).filter(function(f) { return /^ADR-\d{4}/.test(f); }); } catch (_) {}
|
|
2263
|
+
var nextNum = existing.length + 1;
|
|
2264
|
+
var num = String(nextNum).padStart(4, '0');
|
|
2265
|
+
var stamp = new Date().toISOString().slice(0,10);
|
|
2266
|
+
var slug = 'session-decisions';
|
|
2267
|
+
var fname = 'ADR-' + num + '-' + stamp + '-' + slug + '.md';
|
|
2268
|
+
var outPath = path.join(adrsDir, fname);
|
|
2269
|
+
|
|
2270
|
+
var body = '# ADR-' + num + ': Session decisions (' + stamp + ')\n\n' +
|
|
2271
|
+
'**Status:** Proposed\n**Date:** ' + stamp + '\n\n' +
|
|
2272
|
+
'## Context\n\n' +
|
|
2273
|
+
'During recent sessions, the following decision markers were captured ' +
|
|
2274
|
+
'from user prompts. Each excerpt is the surrounding sentence at the time.\n\n' +
|
|
2275
|
+
'## Decisions\n\n';
|
|
2276
|
+
for (var i = 0; i < recent.length; i++) {
|
|
2277
|
+
var d = recent[i];
|
|
2278
|
+
var date = new Date(d.ts).toISOString().slice(0,16).replace('T',' ');
|
|
2279
|
+
body += '### ' + (i + 1) + '. ' + date + '\n\n';
|
|
2280
|
+
for (var j = 0; j < d.excerpts.length; j++) {
|
|
2281
|
+
body += '> ' + d.excerpts[j].trim() + '\n\n';
|
|
2282
|
+
}
|
|
2283
|
+
if (d.prompt) body += '_Prompt:_ ' + d.prompt.slice(0, 200) + (d.prompt.length > 200 ? '…' : '') + '\n\n';
|
|
2284
|
+
}
|
|
2285
|
+
body += '## Consequences\n\n_(fill in after review)_\n\n' +
|
|
2286
|
+
'## Status\n\nProposed — awaiting human review and refinement.\n';
|
|
2287
|
+
fs.writeFileSync(outPath, body);
|
|
2288
|
+
console.log('[ADR_DRAFT] Wrote ' + recent.length + ' decision(s) to ' + outPath);
|
|
2289
|
+
console.log(' Edit the file to fill in Context and Consequences, then change Status to Accepted/Rejected.');
|
|
2290
|
+
},
|
|
2291
|
+
|
|
2202
2292
|
'status': () => {
|
|
2203
2293
|
console.log('[OK] Status check');
|
|
2204
2294
|
},
|
|
@@ -720,21 +720,27 @@ function getHookLatency() {
|
|
|
720
720
|
} catch { return null; }
|
|
721
721
|
}
|
|
722
722
|
|
|
723
|
-
// Graph usage telemetry —
|
|
723
|
+
// Graph usage telemetry — counts ALL graph wins (MCP calls + silent assists)
|
|
724
|
+
// vs greps that got no graph help. "graph %" reflects how often the graph
|
|
725
|
+
// actually touched the LLM's flow, not just how often the LLM invoked MCP.
|
|
724
726
|
function getGraphUsage() {
|
|
725
727
|
const usagePath = path.join(CWD, '.monomind', 'metrics', 'graph-usage.json');
|
|
726
728
|
try {
|
|
727
729
|
if (!fs.existsSync(usagePath)) return null;
|
|
728
730
|
const d = JSON.parse(fs.readFileSync(usagePath, 'utf-8'));
|
|
729
|
-
const
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
731
|
+
const graphWins =
|
|
732
|
+
(d.monograph_call || 0) // LLM-initiated MCP monograph tool call
|
|
733
|
+
+ (d.preresolve_hit || 0) // route hook injected pre-resolved files
|
|
734
|
+
+ (d.graph_assist_search || 0) // pre-search / pre-bash injected hits
|
|
735
|
+
+ (d.graph_assist_neighbors || 0); // post-read neighbor footer
|
|
736
|
+
const searches = (d.grep_call || 0) + (d.glob_call || 0)
|
|
737
|
+
+ (d.bash_grep_call || 0) + (d.bash_find_call || 0);
|
|
738
|
+
const total = graphWins + searches;
|
|
733
739
|
if (total === 0) return null;
|
|
734
740
|
return {
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
pct: Math.round((
|
|
741
|
+
graphWins,
|
|
742
|
+
searches,
|
|
743
|
+
pct: Math.round((graphWins / total) * 100),
|
|
738
744
|
dollarsSaved: d.dollars_saved || 0,
|
|
739
745
|
};
|
|
740
746
|
} catch { return null; }
|
|
@@ -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,
|
|
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,CA8qCrE;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CA8BnE"}
|
|
@@ -849,18 +849,20 @@ function getHookLatency() {
|
|
|
849
849
|
} catch { return null; }
|
|
850
850
|
}
|
|
851
851
|
|
|
852
|
-
// Graph usage telemetry —
|
|
852
|
+
// Graph usage telemetry — counts ALL graph wins (MCP calls + silent assists)
|
|
853
|
+
// vs greps that got no graph help.
|
|
853
854
|
function getGraphUsage() {
|
|
854
855
|
const usagePath = path.join(CWD, '.monomind', 'metrics', 'graph-usage.json');
|
|
855
856
|
try {
|
|
856
857
|
if (!fs.existsSync(usagePath)) return null;
|
|
857
858
|
const d = JSON.parse(fs.readFileSync(usagePath, 'utf-8'));
|
|
858
|
-
const
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
859
|
+
const graphWins = (d.monograph_call || 0) + (d.preresolve_hit || 0)
|
|
860
|
+
+ (d.graph_assist_search || 0) + (d.graph_assist_neighbors || 0);
|
|
861
|
+
const searches = (d.grep_call || 0) + (d.glob_call || 0)
|
|
862
|
+
+ (d.bash_grep_call || 0) + (d.bash_find_call || 0);
|
|
863
|
+
const total = graphWins + searches;
|
|
862
864
|
if (total === 0) return null;
|
|
863
|
-
return {
|
|
865
|
+
return { graphWins: graphWins, searches: searches, pct: Math.round((graphWins / total) * 100), dollarsSaved: d.dollars_saved || 0 };
|
|
864
866
|
} catch { return null; }
|
|
865
867
|
}
|
|
866
868
|
|
|
@@ -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
|
|
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAopCvB,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"}
|
package/dist/src/ui/server.mjs
CHANGED
|
@@ -2128,6 +2128,70 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
|
|
|
2128
2128
|
return;
|
|
2129
2129
|
}
|
|
2130
2130
|
|
|
2131
|
+
// ------------------------------------------------- Monograph
|
|
2132
|
+
// GET /api/monograph — node/edge counts, top god nodes, type distribution.
|
|
2133
|
+
// (Distinct from /api/graph which serves session/journal graph data.)
|
|
2134
|
+
// Reads .monomind/monograph.db via sqlite3 CLI to avoid bundling better-sqlite3.
|
|
2135
|
+
if (req.method === 'GET' && url === '/api/monograph') {
|
|
2136
|
+
try {
|
|
2137
|
+
const dbPath = path.join(projectDir || process.cwd(), '.monomind', 'monograph.db');
|
|
2138
|
+
if (!fs.existsSync(dbPath)) {
|
|
2139
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2140
|
+
res.end(JSON.stringify({ exists: false }));
|
|
2141
|
+
return;
|
|
2142
|
+
}
|
|
2143
|
+
const { execSync } = await import('child_process');
|
|
2144
|
+
// Pipe SQL via stdin to avoid shell quoting issues with single-quoted SQL strings.
|
|
2145
|
+
const runSql = (sql, timeout = 5000) => {
|
|
2146
|
+
try {
|
|
2147
|
+
return execSync(`sqlite3 -json "${dbPath}"`,
|
|
2148
|
+
{ encoding: 'utf-8', timeout: timeout, input: sql + ';' });
|
|
2149
|
+
} catch (e) { return '[]'; }
|
|
2150
|
+
};
|
|
2151
|
+
const counts = JSON.parse(runSql(
|
|
2152
|
+
"SELECT (SELECT COUNT(*) FROM nodes) AS nodes, (SELECT COUNT(*) FROM edges) AS edges;"
|
|
2153
|
+
) || '[{}]')[0] || { nodes: 0, edges: 0 };
|
|
2154
|
+
// Compute degree in one pass via GROUP BY (much faster than per-row subquery).
|
|
2155
|
+
const gods = JSON.parse(runSql(
|
|
2156
|
+
"WITH deg(node_id, d) AS (" +
|
|
2157
|
+
" SELECT source_id, COUNT(*) FROM edges GROUP BY source_id " +
|
|
2158
|
+
" UNION ALL " +
|
|
2159
|
+
" SELECT target_id, COUNT(*) FROM edges GROUP BY target_id" +
|
|
2160
|
+
"), totals AS (" +
|
|
2161
|
+
" SELECT node_id, SUM(d) AS deg FROM deg GROUP BY node_id" +
|
|
2162
|
+
") " +
|
|
2163
|
+
"SELECT n.name, n.label, n.file_path, t.deg " +
|
|
2164
|
+
"FROM nodes n JOIN totals t ON t.node_id = n.id " +
|
|
2165
|
+
"WHERE n.label NOT IN ('Concept') " +
|
|
2166
|
+
"AND n.file_path IS NOT NULL AND n.file_path != '' " +
|
|
2167
|
+
"AND n.name NOT LIKE '(%' AND length(n.name) >= 3 " +
|
|
2168
|
+
"ORDER BY t.deg DESC LIMIT 20",
|
|
2169
|
+
10000
|
|
2170
|
+
) || '[]');
|
|
2171
|
+
const types = JSON.parse(runSql(
|
|
2172
|
+
"SELECT label, COUNT(*) AS count FROM nodes GROUP BY label ORDER BY count DESC LIMIT 12"
|
|
2173
|
+
) || '[]');
|
|
2174
|
+
const relations = JSON.parse(runSql(
|
|
2175
|
+
"SELECT relation, COUNT(*) AS count FROM edges GROUP BY relation ORDER BY count DESC"
|
|
2176
|
+
) || '[]');
|
|
2177
|
+
|
|
2178
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2179
|
+
res.end(JSON.stringify({
|
|
2180
|
+
exists: true,
|
|
2181
|
+
nodes: counts.nodes,
|
|
2182
|
+
edges: counts.edges,
|
|
2183
|
+
godNodes: gods,
|
|
2184
|
+
typeDistribution: types,
|
|
2185
|
+
relationDistribution: relations,
|
|
2186
|
+
updatedAt: fs.statSync(dbPath).mtime,
|
|
2187
|
+
}));
|
|
2188
|
+
} catch (err) {
|
|
2189
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
2190
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
2191
|
+
}
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2131
2195
|
// ------------------------------------------------- Org management
|
|
2132
2196
|
// GET /api/orgs — list all saved org configs
|
|
2133
2197
|
if (req.method === 'GET' && url === '/api/orgs') {
|