@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.
@@ -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 — ratio of monograph_* vs Grep/Glob, plus $ saved
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 monograph = d.monograph_call || 0;
730
- const search = (d.grep_call || 0) + (d.glob_call || 0)
731
- + (d.bash_grep_call || 0) + (d.bash_find_call || 0);
732
- const total = monograph + search;
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
- monograph,
736
- search,
737
- pct: Math.round((monograph / total) * 100),
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,CA4qCrE;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,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 — ratio of monograph_* vs Grep/Glob/Bash-grep, + $ saved
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 monograph = d.monograph_call || 0;
859
- const search = (d.grep_call || 0) + (d.glob_call || 0)
860
- + (d.bash_grep_call || 0) + (d.bash_find_call || 0);
861
- const total = monograph + search;
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 { monograph: monograph, search: search, pct: Math.round((monograph / total) * 100), dollarsSaved: d.dollars_saved || 0 };
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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"}
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"}
@@ -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') {