@monoes/monomindcli 2.7.13 → 2.7.14
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/mastermind/createorg.md +2 -0
- package/.claude/commands/mastermind/okf-import.md +6 -1
- package/.claude/helpers/handlers/route-handler.cjs +40 -7
- package/.claude/helpers/handlers/session-restore-handler.cjs +24 -6
- package/.claude/skills/mastermind-createorg/SKILL.md +11 -4
- package/README.md +12 -6
- package/dist/src/capabilities/index.d.ts.map +1 -1
- package/dist/src/capabilities/index.js +17 -0
- package/dist/src/capabilities/index.js.map +1 -1
- package/dist/src/capabilities/types.d.ts +0 -11
- package/dist/src/capabilities/types.d.ts.map +1 -1
- package/dist/src/commands/doc.d.ts.map +1 -1
- package/dist/src/commands/doc.js +251 -8
- package/dist/src/commands/doc.js.map +1 -1
- package/dist/src/commands/doctor-project-checks.d.ts +25 -3
- package/dist/src/commands/doctor-project-checks.d.ts.map +1 -1
- package/dist/src/commands/doctor-project-checks.js +111 -8
- package/dist/src/commands/doctor-project-checks.js.map +1 -1
- package/dist/src/commands/doctor.d.ts.map +1 -1
- package/dist/src/commands/doctor.js +18 -2
- package/dist/src/commands/doctor.js.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +32 -3
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/monograph.d.ts.map +1 -1
- package/dist/src/commands/monograph.js +26 -3
- package/dist/src/commands/monograph.js.map +1 -1
- package/dist/src/commands/org.d.ts +15 -0
- package/dist/src/commands/org.d.ts.map +1 -1
- package/dist/src/commands/org.js +251 -6
- package/dist/src/commands/org.js.map +1 -1
- package/dist/src/init/claudemd-generator.d.ts.map +1 -1
- package/dist/src/init/claudemd-generator.js +4 -1
- package/dist/src/init/claudemd-generator.js.map +1 -1
- package/dist/src/knowledge/document-pipeline.d.ts +77 -4
- package/dist/src/knowledge/document-pipeline.d.ts.map +1 -1
- package/dist/src/knowledge/document-pipeline.js +261 -15
- package/dist/src/knowledge/document-pipeline.js.map +1 -1
- package/dist/src/knowledge/eval/corpus.d.ts +56 -0
- package/dist/src/knowledge/eval/corpus.d.ts.map +1 -0
- package/dist/src/knowledge/eval/corpus.js +126 -0
- package/dist/src/knowledge/eval/corpus.js.map +1 -0
- package/dist/src/knowledge/eval/golden-set.d.ts +62 -0
- package/dist/src/knowledge/eval/golden-set.d.ts.map +1 -0
- package/dist/src/knowledge/eval/golden-set.js +331 -0
- package/dist/src/knowledge/eval/golden-set.js.map +1 -0
- package/dist/src/knowledge/eval/harness.d.ts +221 -0
- package/dist/src/knowledge/eval/harness.d.ts.map +1 -0
- package/dist/src/knowledge/eval/harness.js +610 -0
- package/dist/src/knowledge/eval/harness.js.map +1 -0
- package/dist/src/knowledge/eval/metrics.d.ts +120 -0
- package/dist/src/knowledge/eval/metrics.d.ts.map +1 -0
- package/dist/src/knowledge/eval/metrics.js +243 -0
- package/dist/src/knowledge/eval/metrics.js.map +1 -0
- package/dist/src/knowledge/eval/model-presence.d.ts +48 -0
- package/dist/src/knowledge/eval/model-presence.d.ts.map +1 -0
- package/dist/src/knowledge/eval/model-presence.js +128 -0
- package/dist/src/knowledge/eval/model-presence.js.map +1 -0
- package/dist/src/knowledge/eval/network-guard.d.ts +46 -0
- package/dist/src/knowledge/eval/network-guard.d.ts.map +1 -0
- package/dist/src/knowledge/eval/network-guard.js +112 -0
- package/dist/src/knowledge/eval/network-guard.js.map +1 -0
- package/dist/src/knowledge/eval/retrievers.d.ts +65 -0
- package/dist/src/knowledge/eval/retrievers.d.ts.map +1 -0
- package/dist/src/knowledge/eval/retrievers.js +180 -0
- package/dist/src/knowledge/eval/retrievers.js.map +1 -0
- package/dist/src/knowledge/eval/signals.d.ts +111 -0
- package/dist/src/knowledge/eval/signals.d.ts.map +1 -0
- package/dist/src/knowledge/eval/signals.js +232 -0
- package/dist/src/knowledge/eval/signals.js.map +1 -0
- package/dist/src/mcp-tools/agent-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/agent-tools.js +7 -34
- package/dist/src/mcp-tools/agent-tools.js.map +1 -1
- package/dist/src/mcp-tools/knowledge-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/knowledge-tools.js +88 -6
- package/dist/src/mcp-tools/knowledge-tools.js.map +1 -1
- package/dist/src/mcp-tools/task-tools.d.ts +1 -0
- package/dist/src/mcp-tools/task-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/task-tools.js +76 -111
- package/dist/src/mcp-tools/task-tools.js.map +1 -1
- package/dist/src/mcp-tools/terminal-tools.d.ts.map +1 -1
- package/dist/src/mcp-tools/terminal-tools.js +3 -28
- package/dist/src/mcp-tools/terminal-tools.js.map +1 -1
- package/dist/src/memory/bm25-index.d.ts +137 -0
- package/dist/src/memory/bm25-index.d.ts.map +1 -0
- package/dist/src/memory/bm25-index.js +219 -0
- package/dist/src/memory/bm25-index.js.map +1 -0
- package/dist/src/memory/embedding-operations.d.ts.map +1 -1
- package/dist/src/memory/embedding-operations.js +8 -7
- package/dist/src/memory/embedding-operations.js.map +1 -1
- package/dist/src/memory/ewc-consolidation.d.ts.map +1 -1
- package/dist/src/memory/ewc-consolidation.js +2 -1
- package/dist/src/memory/ewc-consolidation.js.map +1 -1
- package/dist/src/memory/hnsw-operations.d.ts.map +1 -1
- package/dist/src/memory/hnsw-operations.js +4 -3
- package/dist/src/memory/hnsw-operations.js.map +1 -1
- package/dist/src/memory/intelligence.d.ts.map +1 -1
- package/dist/src/memory/intelligence.js +2 -1
- package/dist/src/memory/intelligence.js.map +1 -1
- package/dist/src/memory/memory-bridge.d.ts +34 -0
- package/dist/src/memory/memory-bridge.d.ts.map +1 -1
- package/dist/src/memory/memory-bridge.js +184 -26
- package/dist/src/memory/memory-bridge.js.map +1 -1
- package/dist/src/memory/memory-initializer.d.ts.map +1 -1
- package/dist/src/memory/memory-initializer.js +4 -3
- package/dist/src/memory/memory-initializer.js.map +1 -1
- package/dist/src/memory/memory-schema.d.ts +1 -1
- package/dist/src/memory/memory-schema.js +1 -1
- package/dist/src/memory/text-tokens.d.ts +10 -0
- package/dist/src/memory/text-tokens.d.ts.map +1 -0
- package/dist/src/memory/text-tokens.js +37 -0
- package/dist/src/memory/text-tokens.js.map +1 -0
- package/dist/src/orgrt/daemon.d.ts +16 -1
- package/dist/src/orgrt/daemon.d.ts.map +1 -1
- package/dist/src/orgrt/daemon.js +65 -9
- package/dist/src/orgrt/daemon.js.map +1 -1
- package/dist/src/orgrt/scheduler.d.ts +14 -1
- package/dist/src/orgrt/scheduler.d.ts.map +1 -1
- package/dist/src/orgrt/scheduler.js +51 -5
- package/dist/src/orgrt/scheduler.js.map +1 -1
- package/dist/src/orgrt/session.js +12 -0
- package/dist/src/orgrt/session.js.map +1 -1
- package/dist/src/orgrt/types.d.ts +3 -0
- package/dist/src/orgrt/types.d.ts.map +1 -1
- package/dist/src/orgrt/types.js +7 -1
- package/dist/src/orgrt/types.js.map +1 -1
- package/dist/src/services/crash-reporter.d.ts.map +1 -1
- package/dist/src/services/crash-reporter.js +7 -0
- package/dist/src/services/crash-reporter.js.map +1 -1
- package/dist/src/ui/dashboard.html +13 -10
- package/dist/src/ui/orgs.html +30 -13
- package/dist/src/ui/routes-monograph.mjs +929 -0
- package/dist/src/ui/routes-org.mjs +2513 -0
- package/dist/src/ui/server.mjs +726 -4117
- package/dist/src/utils/json-file.d.ts +11 -0
- package/dist/src/utils/json-file.d.ts.map +1 -1
- package/dist/src/utils/json-file.js +27 -0
- package/dist/src/utils/json-file.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,929 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export async function handleMonographRoutes(req, res, url, corsOrigin, ctx) {
|
|
5
|
+
// ------------------------------------------------------- GET /api/monograph-html
|
|
6
|
+
if (req.method === 'GET' && url === '/api/monograph-html') {
|
|
7
|
+
try {
|
|
8
|
+
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
9
|
+
const dir = qs.get('dir') || ctx.projectDir || process.cwd();
|
|
10
|
+
const d = path.resolve(dir || process.cwd());
|
|
11
|
+
const dbPath = path.join(d, '.monomind', 'monograph.db');
|
|
12
|
+
|
|
13
|
+
// Generate HTML on-the-fly from SQLite DB
|
|
14
|
+
if (fs.existsSync(dbPath)) {
|
|
15
|
+
let html;
|
|
16
|
+
try {
|
|
17
|
+
// Try better-sqlite3 first (fast, in-process)
|
|
18
|
+
const { openDb, closeDb } = await import(new URL('../../../../monograph/dist/src/storage/db.js', import.meta.url).href);
|
|
19
|
+
const { toHtml } = await import(new URL('../../../../monograph/dist/src/export/html.js', import.meta.url).href);
|
|
20
|
+
const db = openDb(dbPath);
|
|
21
|
+
try {
|
|
22
|
+
const rawNodes = db.prepare('SELECT * FROM nodes LIMIT 5000').all();
|
|
23
|
+
const rawEdges = db.prepare('SELECT * FROM edges').all();
|
|
24
|
+
const parsedNodes = rawNodes.map(n => ({
|
|
25
|
+
id: n.id, label: n.label, name: n.name, normLabel: n.norm_label,
|
|
26
|
+
filePath: n.file_path, startLine: n.start_line, endLine: n.end_line,
|
|
27
|
+
communityId: n.community_id, isExported: !!n.is_exported,
|
|
28
|
+
language: n.language, properties: n.properties ? JSON.parse(n.properties) : {},
|
|
29
|
+
}));
|
|
30
|
+
const parsedEdges = rawEdges.map(e => ({
|
|
31
|
+
id: e.id, sourceId: e.source_id, targetId: e.target_id,
|
|
32
|
+
relation: e.relation, confidence: e.confidence,
|
|
33
|
+
confidenceScore: e.confidence_score, weight: e.weight,
|
|
34
|
+
}));
|
|
35
|
+
html = toHtml(parsedNodes, parsedEdges);
|
|
36
|
+
} finally { closeDb(db); }
|
|
37
|
+
} catch {
|
|
38
|
+
// Fallback: sqlite3 CLI + inline Sigma.js graph
|
|
39
|
+
const { execFileSync } = await import('child_process');
|
|
40
|
+
const runSql = (sql) => {
|
|
41
|
+
try { return JSON.parse(execFileSync('sqlite3', ['-json', dbPath], { encoding: 'utf-8', timeout: 15000, maxBuffer: 50*1024*1024, input: sql + ';' }) || '[]'); } catch { return []; }
|
|
42
|
+
};
|
|
43
|
+
const rawNodes = runSql('SELECT id, name, label, file_path, community_id FROM nodes LIMIT 2000');
|
|
44
|
+
const rawEdges = runSql('SELECT source_id, target_id, relation FROM edges');
|
|
45
|
+
const degree = new Map();
|
|
46
|
+
for (const n of rawNodes) degree.set(n.id, 0);
|
|
47
|
+
for (const e of rawEdges) {
|
|
48
|
+
if (degree.has(e.source_id)) degree.set(e.source_id, (degree.get(e.source_id)||0)+1);
|
|
49
|
+
if (degree.has(e.target_id)) degree.set(e.target_id, (degree.get(e.target_id)||0)+1);
|
|
50
|
+
}
|
|
51
|
+
const topNodes = [...rawNodes].sort((a,b) => (degree.get(b.id)||0)-(degree.get(a.id)||0)).slice(0,500);
|
|
52
|
+
const topIds = new Set(topNodes.map(n => n.id));
|
|
53
|
+
const filteredEdges = rawEdges.filter(e => topIds.has(e.source_id) && topIds.has(e.target_id)).slice(0,2000);
|
|
54
|
+
const colors = ['#4E79A7','#F28E2B','#E15759','#76B7B2','#59A14F','#EDC948','#B07AA1','#FF9DA7','#9C755F','#BAB0AC'];
|
|
55
|
+
const nodesJson = JSON.stringify(topNodes.map((n,i) => {
|
|
56
|
+
const d = degree.get(n.id)||1;
|
|
57
|
+
const c = n.community_id != null ? colors[n.community_id % colors.length] : colors[i % colors.length];
|
|
58
|
+
return { id: n.id, label: n.name||n.id, x: Math.cos(i*0.618*Math.PI*2)*300+Math.random()*50, y: Math.sin(i*0.618*Math.PI*2)*300+Math.random()*50, size: Math.min(3+Math.sqrt(d)*2,20), color: c };
|
|
59
|
+
}));
|
|
60
|
+
const edgesJson = JSON.stringify(filteredEdges.map((e,i) => ({ id:'e'+i, source:e.source_id, target:e.target_id })));
|
|
61
|
+
html = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Monograph</title>
|
|
62
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/sigma.js/2.4.0/sigma.min.js"><\/script>
|
|
63
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/graphology/0.25.4/graphology.umd.min.js"><\/script>
|
|
64
|
+
<style>*{margin:0;padding:0}body{background:#0f0f1a;overflow:hidden}#g{width:100vw;height:100vh}</style></head>
|
|
65
|
+
<body><div id="g"></div><script>
|
|
66
|
+
const g=new graphology.Graph();
|
|
67
|
+
${nodesJson}.forEach(n=>g.addNode(n.id,{label:n.label,x:n.x,y:n.y,size:n.size,color:n.color}));
|
|
68
|
+
${edgesJson}.forEach(e=>{try{g.addEdge(e.source,e.target,{size:0.5,color:'#333'})}catch{}});
|
|
69
|
+
new Sigma(g,document.getElementById('g'),{renderEdgeLabels:false,labelColor:{color:'#ccc'},labelSize:10});
|
|
70
|
+
<\/script></body></html>`;
|
|
71
|
+
}
|
|
72
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}), 'Cache-Control': 'no-cache' });
|
|
73
|
+
res.end(html);
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Fallback: try legacy graph.html on disk
|
|
78
|
+
const htmlPath = path.join(d, '.monomind', 'graph', 'graph.html');
|
|
79
|
+
const html = fs.readFileSync(htmlPath, 'utf-8');
|
|
80
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}), 'Cache-Control': 'no-cache' });
|
|
81
|
+
res.end(html);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
res.writeHead(404, { 'Content-Type': 'text/html' });
|
|
84
|
+
res.end('<html><body style="background:#0f0f1a;color:#888;font-family:monospace;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;"><div style="text-align:center;"><h3 style="color:#4E79A7;">No Graph Built Yet</h3><p>Run <code style="color:#00E5C8;">mcp__monomind__monograph_build</code> or click BUILD in the sidebar.</p></div></body></html>');
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ------------------------------------------------------- GET /api/monograph-report
|
|
90
|
+
if (req.method === 'GET' && url === '/api/monograph-report') {
|
|
91
|
+
try {
|
|
92
|
+
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
93
|
+
const dir = qs.get('dir') || ctx.projectDir || process.cwd();
|
|
94
|
+
const d = path.resolve(dir || process.cwd());
|
|
95
|
+
const dbPath = path.join(d, '.monomind', 'monograph.db');
|
|
96
|
+
let report = null, exists = false, stats = null;
|
|
97
|
+
if (fs.existsSync(dbPath)) {
|
|
98
|
+
exists = true;
|
|
99
|
+
const { openDb, closeDb } = await import(new URL('../../../../monograph/dist/src/storage/db.js', import.meta.url).href);
|
|
100
|
+
const db = openDb(dbPath);
|
|
101
|
+
try {
|
|
102
|
+
const nodeCount = db.prepare('SELECT COUNT(*) AS c FROM nodes').get().c;
|
|
103
|
+
const edgeCount = db.prepare('SELECT COUNT(*) AS c FROM edges').get().c;
|
|
104
|
+
const topNodes = db.prepare(`SELECT n.id, n.name, n.label, (SELECT COUNT(*) FROM edges e WHERE e.source_id=n.id OR e.target_id=n.id) AS deg FROM nodes n ORDER BY deg DESC LIMIT 20`).all();
|
|
105
|
+
const labelDist = db.prepare('SELECT label, COUNT(*) AS cnt FROM nodes GROUP BY label ORDER BY cnt DESC LIMIT 10').all();
|
|
106
|
+
const dbStat = fs.statSync(dbPath);
|
|
107
|
+
stats = { nodes: nodeCount, edges: edgeCount, size: dbStat.size, mtime: dbStat.mtimeMs };
|
|
108
|
+
report = [
|
|
109
|
+
'# Monograph Knowledge Graph',
|
|
110
|
+
'',
|
|
111
|
+
`## Overview`,
|
|
112
|
+
`- **Nodes**: ${nodeCount.toLocaleString()}`,
|
|
113
|
+
`- **Edges**: ${edgeCount.toLocaleString()}`,
|
|
114
|
+
`- **Last built**: ${new Date(dbStat.mtimeMs).toLocaleString()}`,
|
|
115
|
+
'',
|
|
116
|
+
'## Top 20 Nodes by Degree',
|
|
117
|
+
...topNodes.map((n, i) => `${String(i+1).padStart(3,' ')}. **${n.name || n.id}** \`${n.label}\` — ${n.deg} connections`),
|
|
118
|
+
'',
|
|
119
|
+
'## Node Type Distribution',
|
|
120
|
+
...labelDist.map(r => `- **${r.label}**: ${r.cnt}`),
|
|
121
|
+
].join('\n');
|
|
122
|
+
} finally { closeDb(db); }
|
|
123
|
+
}
|
|
124
|
+
res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}), 'Cache-Control': 'no-cache' });
|
|
125
|
+
res.end(JSON.stringify({ exists, report, stats }));
|
|
126
|
+
} catch (err) {
|
|
127
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
128
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
129
|
+
}
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// -------------------------------------------------- GET /api/monograph-graph
|
|
134
|
+
if (req.method === 'GET' && url === '/api/monograph-graph') {
|
|
135
|
+
try {
|
|
136
|
+
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
137
|
+
const dir = qs.get('dir') || ctx.projectDir || process.cwd();
|
|
138
|
+
const d = path.resolve(dir || process.cwd());
|
|
139
|
+
const dbPath = path.join(d, '.monomind', 'monograph.db');
|
|
140
|
+
let nodes = [], edges = [];
|
|
141
|
+
if (fs.existsSync(dbPath)) {
|
|
142
|
+
const { execFileSync } = await import('child_process');
|
|
143
|
+
const runSql = (sql, timeout = 10000) => {
|
|
144
|
+
try {
|
|
145
|
+
return JSON.parse(execFileSync('sqlite3', ['-json', dbPath],
|
|
146
|
+
{ encoding: 'utf-8', timeout, maxBuffer: 50 * 1024 * 1024, input: sql + ';' }) || '[]');
|
|
147
|
+
} catch { return []; }
|
|
148
|
+
};
|
|
149
|
+
const nodeLimit = Math.min(parseInt(qs.get('limit') || '500', 10), 5000);
|
|
150
|
+
const labelFilter = qs.get('labels') ? qs.get('labels').split(',').map(s => s.trim()) : null;
|
|
151
|
+
const rawNodes = labelFilter
|
|
152
|
+
? runSql(`SELECT id, name, label, file_path, community_id FROM nodes WHERE label IN (${labelFilter.map(l => "'" + l.replace(/'/g, "''") + "'").join(',')}) LIMIT 5000`)
|
|
153
|
+
: runSql(`SELECT id, name, label, file_path, community_id FROM nodes LIMIT 5000`);
|
|
154
|
+
const rawEdges = runSql('SELECT source_id, target_id, relation FROM edges');
|
|
155
|
+
const degree = new Map();
|
|
156
|
+
for (const n of rawNodes) degree.set(n.id, 0);
|
|
157
|
+
for (const e of rawEdges) {
|
|
158
|
+
if (degree.has(e.source_id)) degree.set(e.source_id, (degree.get(e.source_id) || 0) + 1);
|
|
159
|
+
if (degree.has(e.target_id)) degree.set(e.target_id, (degree.get(e.target_id) || 0) + 1);
|
|
160
|
+
}
|
|
161
|
+
const topNodes = labelFilter
|
|
162
|
+
? rawNodes
|
|
163
|
+
: [...rawNodes].sort((a, b) => (degree.get(b.id) || 0) - (degree.get(a.id) || 0)).slice(0, nodeLimit);
|
|
164
|
+
const topIds = new Set(topNodes.map(n => n.id));
|
|
165
|
+
nodes = topNodes.map(n => ({ id: n.id, label: n.name || n.id, type: n.label || 'unknown', degree: degree.get(n.id) || 0 }));
|
|
166
|
+
edges = rawEdges.filter(e => topIds.has(e.source_id) && topIds.has(e.target_id)).slice(0, 2000).map(e => ({ source: e.source_id, target: e.target_id, relation: e.relation || 'REF' }));
|
|
167
|
+
}
|
|
168
|
+
res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}), 'Cache-Control': 'no-cache' });
|
|
169
|
+
res.end(JSON.stringify({ nodes, edges }));
|
|
170
|
+
} catch (err) {
|
|
171
|
+
res.writeHead(500); res.end(JSON.stringify({ error: err.message }));
|
|
172
|
+
}
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// -------------------------------------------------- POST /api/ua-enrich
|
|
177
|
+
// Trigger semantic enrichment on an existing monograph DB.
|
|
178
|
+
// Imports understand graph.json if present; falls back to structural-only pass.
|
|
179
|
+
if (req.method === 'POST' && url === '/api/ua-enrich') {
|
|
180
|
+
try {
|
|
181
|
+
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
182
|
+
const dir = qs.get('dir') || ctx.projectDir || process.cwd();
|
|
183
|
+
const d = path.resolve(dir || process.cwd());
|
|
184
|
+
const dbFilePath = path.join(d, '.monomind', 'monograph.db');
|
|
185
|
+
|
|
186
|
+
// Check for UA graph.json first
|
|
187
|
+
const uaGraphCandidates = [
|
|
188
|
+
path.join(d, '.understand-anything', 'knowledge-graph.json'),
|
|
189
|
+
path.join(d, '.understand-anything', 'graph.json'),
|
|
190
|
+
path.join(d, '.ua', 'knowledge-graph.json'),
|
|
191
|
+
path.join(d, '.ua', 'graph.json'),
|
|
192
|
+
];
|
|
193
|
+
const uaGraph = uaGraphCandidates.find(p => fs.existsSync(p));
|
|
194
|
+
const importScript = path.join(process.cwd(), 'scripts', 'ua-import.mjs');
|
|
195
|
+
const enrichScript = path.join(process.cwd(), 'scripts', 'ua-enrich.mjs');
|
|
196
|
+
|
|
197
|
+
res.writeHead(202, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
|
|
198
|
+
|
|
199
|
+
if (uaGraph && fs.existsSync(importScript)) {
|
|
200
|
+
res.end(JSON.stringify({ status: 'importing', source: uaGraph }));
|
|
201
|
+
const { spawn: sp } = await import('child_process');
|
|
202
|
+
const child = sp(process.execPath, [importScript, uaGraph, dbFilePath], { stdio: 'ignore', detached: true, cwd: d });
|
|
203
|
+
child.unref();
|
|
204
|
+
} else if (fs.existsSync(enrichScript)) {
|
|
205
|
+
res.end(JSON.stringify({ status: 'enriching', mode: 'structural-only' }));
|
|
206
|
+
const { spawn: sp } = await import('child_process');
|
|
207
|
+
const child = sp(process.execPath, [enrichScript, '--dir', d, '--db', dbFilePath, '--full'], { stdio: 'ignore', detached: true, cwd: d });
|
|
208
|
+
child.unref();
|
|
209
|
+
} else {
|
|
210
|
+
res.end(JSON.stringify({ status: 'skipped', reason: 'No understand graph.json found. Run /monomind:understand in Claude Code first.' }));
|
|
211
|
+
}
|
|
212
|
+
} catch (err) {
|
|
213
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
214
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
215
|
+
}
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// -------------------------------------------------- POST /api/monograph-build
|
|
220
|
+
if (req.method === 'POST' && url === '/api/monograph-build') {
|
|
221
|
+
try {
|
|
222
|
+
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
223
|
+
const dir = qs.get('dir') || ctx.projectDir || process.cwd();
|
|
224
|
+
const d = path.resolve(dir || process.cwd());
|
|
225
|
+
|
|
226
|
+
// Security: this route spawns `node --eval` with `cwd: d`, which lets
|
|
227
|
+
// Node resolve '@monoes/monograph' against d's node_modules. Only ever
|
|
228
|
+
// allow this for the server's own project root — never an
|
|
229
|
+
// attacker-controlled `?dir=` — since a planted node_modules there
|
|
230
|
+
// would achieve RCE. (See P0-6.)
|
|
231
|
+
const _serverRoot = path.resolve(ctx.projectDir || process.cwd());
|
|
232
|
+
if (d !== _serverRoot) {
|
|
233
|
+
res.writeHead(400, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
|
|
234
|
+
res.end(JSON.stringify({ error: 'monograph-build only supports the server project root' }));
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
res.writeHead(202, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
|
|
239
|
+
res.end(JSON.stringify({ status: 'building', dir: d }));
|
|
240
|
+
|
|
241
|
+
// Build via monograph in background
|
|
242
|
+
const { spawn: sp } = await import('child_process');
|
|
243
|
+
const script = `import { buildAsync } from '@monoes/monograph'; await buildAsync(${JSON.stringify(d)});`;
|
|
244
|
+
const child = sp(process.execPath, ['--input-type=module', '--eval', script], { stdio: 'ignore', detached: true, cwd: d });
|
|
245
|
+
child.unref();
|
|
246
|
+
console.log(`[graph] build started for ${d} via monograph`);
|
|
247
|
+
} catch (err) {
|
|
248
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
249
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
250
|
+
}
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// -------------------------------------------------- GET /api/monograph-build-docs-status
|
|
255
|
+
if (req.method === 'GET' && url === '/api/monograph-build-docs-status') {
|
|
256
|
+
const qs2 = new URL(req.url, 'http://localhost').searchParams;
|
|
257
|
+
const d2 = path.resolve(qs2.get('dir') || ctx.projectDir || process.cwd());
|
|
258
|
+
const state = ctx.buildDocsState.get(d2) || { status: 'idle' };
|
|
259
|
+
res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
|
|
260
|
+
res.end(JSON.stringify(state));
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// -------------------------------------------------- POST /api/monograph-build-docs
|
|
265
|
+
if (req.method === 'POST' && url === '/api/monograph-build-docs') {
|
|
266
|
+
try {
|
|
267
|
+
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
268
|
+
const dir = qs.get('dir') || ctx.projectDir || process.cwd();
|
|
269
|
+
const d = path.resolve(dir || process.cwd());
|
|
270
|
+
const dbPath = path.join(d, '.monomind', 'monograph.db');
|
|
271
|
+
if (!fs.existsSync(dbPath)) {
|
|
272
|
+
res.writeHead(400, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
|
|
273
|
+
res.end(JSON.stringify({ error: 'monograph.db not found — run BUILD GRAPH first' }));
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Reject if already running
|
|
278
|
+
const existing = ctx.buildDocsState.get(d);
|
|
279
|
+
if (existing && existing.status === 'pending') {
|
|
280
|
+
res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
|
|
281
|
+
res.end(JSON.stringify({ status: 'pending', message: 'Build already in progress' }));
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const startedAt = Date.now();
|
|
286
|
+
ctx.buildDocsState.set(d, { status: 'pending', sections: 0, files: 0, error: null, startedAt });
|
|
287
|
+
res.writeHead(202, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
|
|
288
|
+
res.end(JSON.stringify({ status: 'pending', dir: d }));
|
|
289
|
+
|
|
290
|
+
// Run doc parsing in background
|
|
291
|
+
(async () => {
|
|
292
|
+
const { openDb, closeDb } = await import(new URL('../../../../monograph/dist/src/storage/db.js', import.meta.url).href);
|
|
293
|
+
const { isFileCached, updateFileCache, hashFileContent } = await import(new URL('../../../../monograph/dist/src/storage/file-cache.js', import.meta.url).href);
|
|
294
|
+
const { readFileSync, readdirSync, statSync } = fs;
|
|
295
|
+
|
|
296
|
+
const docExts = new Set(['.md', '.mdx', '.txt', '.rst']);
|
|
297
|
+
const ignoreDirs = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.nuxt', 'coverage', '.monomind', '__pycache__', 'vendor']);
|
|
298
|
+
const docFiles = [];
|
|
299
|
+
function walk(dir2, depth = 0) {
|
|
300
|
+
if (depth > 12) return;
|
|
301
|
+
let entries;
|
|
302
|
+
try { entries = readdirSync(dir2); } catch { return; }
|
|
303
|
+
for (const e of entries) {
|
|
304
|
+
if (ignoreDirs.has(e) || e.startsWith('.')) continue;
|
|
305
|
+
const full = path.join(dir2, e);
|
|
306
|
+
let st;
|
|
307
|
+
try { st = statSync(full); } catch { continue; }
|
|
308
|
+
if (st.isDirectory()) { walk(full, depth + 1); }
|
|
309
|
+
else if (docExts.has(path.extname(e).toLowerCase()) && st.size < 600000) docFiles.push(full);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
walk(d);
|
|
313
|
+
|
|
314
|
+
const db = openDb(dbPath);
|
|
315
|
+
try {
|
|
316
|
+
const insertNode = db.prepare(`INSERT OR REPLACE INTO nodes (id, label, name, norm_label, file_path, start_line, end_line, language, is_exported) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)`);
|
|
317
|
+
const insertEdge = db.prepare(`INSERT OR IGNORE INTO edges (id, source_id, target_id, relation, confidence, confidence_score, weight) VALUES (?, ?, ?, ?, 'EXTRACTED', 1.0, 1.0)`);
|
|
318
|
+
|
|
319
|
+
const insertAll = db.transaction((nodes, edges) => {
|
|
320
|
+
for (const n of nodes) {
|
|
321
|
+
try { insertNode.run(n.id, n.label, n.name, n.norm_label, n.file_path, n.start_line, n.end_line, n.language); } catch {}
|
|
322
|
+
}
|
|
323
|
+
for (const e of edges) { try { insertEdge.run(e.id, e.src, e.dst, e.rel); } catch {} }
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const normTitle = t => t.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '');
|
|
327
|
+
|
|
328
|
+
let totalSections = 0;
|
|
329
|
+
let skipped = 0;
|
|
330
|
+
for (const filePath of docFiles) {
|
|
331
|
+
let content;
|
|
332
|
+
try { content = readFileSync(filePath, 'utf-8'); } catch { continue; }
|
|
333
|
+
|
|
334
|
+
// Skip unchanged files using file cache
|
|
335
|
+
let isCached = false;
|
|
336
|
+
let contentHash = '';
|
|
337
|
+
try {
|
|
338
|
+
contentHash = hashFileContent(content);
|
|
339
|
+
isCached = isFileCached(db, filePath, contentHash);
|
|
340
|
+
} catch {}
|
|
341
|
+
if (isCached) { skipped++; continue; }
|
|
342
|
+
const relPath = path.relative(d, filePath);
|
|
343
|
+
const ext = path.extname(filePath).slice(1).toLowerCase();
|
|
344
|
+
const fileId = 'doc:' + relPath;
|
|
345
|
+
const lineCount = content.split('\n').length;
|
|
346
|
+
|
|
347
|
+
const nodes = [{ id: fileId, label: 'File', name: relPath, norm_label: normTitle(relPath), file_path: relPath, start_line: 1, end_line: lineCount, language: ext }];
|
|
348
|
+
const edges = [];
|
|
349
|
+
const lines = content.split('\n');
|
|
350
|
+
const sectionStack = [{ id: fileId, depth: 0 }];
|
|
351
|
+
let inCodeBlock = false;
|
|
352
|
+
let codeBlockLang = null;
|
|
353
|
+
|
|
354
|
+
for (let i = 0; i < lines.length; i++) {
|
|
355
|
+
const line = lines[i];
|
|
356
|
+
|
|
357
|
+
// Track fenced code blocks — don't parse headings inside them
|
|
358
|
+
const fenceMatch = line.match(/^```([a-zA-Z0-9_+-]*)$/);
|
|
359
|
+
if (fenceMatch) {
|
|
360
|
+
if (!inCodeBlock) {
|
|
361
|
+
inCodeBlock = true;
|
|
362
|
+
codeBlockLang = fenceMatch[1].trim() || null;
|
|
363
|
+
if (codeBlockLang) {
|
|
364
|
+
const cId = 'concept:lang:' + codeBlockLang.toLowerCase();
|
|
365
|
+
if (!nodes.find(n => n.id === cId)) {
|
|
366
|
+
nodes.push({ id: cId, label: 'Concept', name: codeBlockLang, norm_label: normTitle(codeBlockLang), file_path: null, start_line: 0, end_line: 0, language: null });
|
|
367
|
+
}
|
|
368
|
+
const curSec = sectionStack[sectionStack.length - 1].id;
|
|
369
|
+
edges.push({ id: 'e:' + curSec + ':' + cId + ':code', src: curSec, dst: cId, rel: 'TAGGED_AS' });
|
|
370
|
+
}
|
|
371
|
+
} else { inCodeBlock = false; codeBlockLang = null; }
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
if (inCodeBlock) continue;
|
|
375
|
+
|
|
376
|
+
// ATX headings: # Title
|
|
377
|
+
const hMatch = line.match(/^(#{1,6})\s+(.+)/);
|
|
378
|
+
if (hMatch) {
|
|
379
|
+
const depth = hMatch[1].length;
|
|
380
|
+
const title = hMatch[2].trim().replace(/\s+#+\s*$/, '').trim();
|
|
381
|
+
const secId = 'sec:' + relPath + ':' + (i + 1);
|
|
382
|
+
nodes.push({ id: secId, label: 'Section', name: title, norm_label: normTitle(title), file_path: relPath, start_line: i + 1, end_line: i + 1, language: ext });
|
|
383
|
+
totalSections++;
|
|
384
|
+
while (sectionStack.length > 1 && sectionStack[sectionStack.length - 1].depth >= depth) sectionStack.pop();
|
|
385
|
+
const parentId = sectionStack[sectionStack.length - 1].id;
|
|
386
|
+
edges.push({ id: 'e:' + secId + ':' + parentId + ':parent', src: parentId, dst: secId, rel: 'DEFINES' });
|
|
387
|
+
sectionStack.push({ id: secId, depth });
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// RST-style headings: line followed by ===, ---, ~~~, ^^^, etc.
|
|
392
|
+
if (i + 1 < lines.length && lines[i + 1].match(/^[=\-~^"'`#*+!]{3,}\s*$/) && line.trim().length > 0 && line.trim().length <= lines[i + 1].trim().length + 2) {
|
|
393
|
+
const underlineChar = lines[i + 1].trim()[0];
|
|
394
|
+
const rstDepth = '=-~^"\'`#*+!'.indexOf(underlineChar) + 1 || 3;
|
|
395
|
+
const title = line.trim();
|
|
396
|
+
const secId = 'sec:' + relPath + ':' + (i + 1);
|
|
397
|
+
nodes.push({ id: secId, label: 'Section', name: title, norm_label: normTitle(title), file_path: relPath, start_line: i + 1, end_line: i + 1, language: ext });
|
|
398
|
+
totalSections++;
|
|
399
|
+
const depth = Math.min(6, Math.ceil(rstDepth / 2));
|
|
400
|
+
while (sectionStack.length > 1 && sectionStack[sectionStack.length - 1].depth >= depth) sectionStack.pop();
|
|
401
|
+
const parentId = sectionStack[sectionStack.length - 1].id;
|
|
402
|
+
edges.push({ id: 'e:' + secId + ':' + parentId + ':parent', src: parentId, dst: secId, rel: 'DEFINES' });
|
|
403
|
+
sectionStack.push({ id: secId, depth });
|
|
404
|
+
i++; // skip underline line
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// #hashtag concepts (skip markdown headings already matched)
|
|
409
|
+
const tags = line.match(/#([a-zA-Z][a-zA-Z0-9_-]{2,})/g);
|
|
410
|
+
if (tags) {
|
|
411
|
+
for (const tag of tags) {
|
|
412
|
+
const concept = tag.slice(1);
|
|
413
|
+
const cId = 'concept:tag:' + concept.toLowerCase();
|
|
414
|
+
if (!nodes.find(n => n.id === cId)) {
|
|
415
|
+
nodes.push({ id: cId, label: 'Concept', name: concept, norm_label: normTitle(concept), file_path: null, start_line: 0, end_line: 0, language: null });
|
|
416
|
+
}
|
|
417
|
+
const curSec = sectionStack[sectionStack.length - 1].id;
|
|
418
|
+
edges.push({ id: 'e:' + curSec + ':' + cId + ':tag', src: curSec, dst: cId, rel: 'TAGGED_AS' });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
try {
|
|
424
|
+
insertAll(nodes, edges);
|
|
425
|
+
// Update file cache so we skip unchanged files next run
|
|
426
|
+
try {
|
|
427
|
+
updateFileCache(db, { filePath, contentHash, lastParsed: Date.now(), nodeCount: nodes.length, edgeCount: edges.length });
|
|
428
|
+
} catch {}
|
|
429
|
+
} catch (e) { console.error('[docs-build] error inserting', relPath, e.message); }
|
|
430
|
+
}
|
|
431
|
+
console.log(`[docs-build] indexed ${docFiles.length - skipped} docs (${skipped} cached), ${totalSections} sections → ${dbPath}`);
|
|
432
|
+
ctx.buildDocsState.set(d, { status: 'done', sections: totalSections, files: docFiles.length - skipped, cached: skipped, error: null, startedAt, completedAt: Date.now() });
|
|
433
|
+
} finally { closeDb(db); }
|
|
434
|
+
})().catch(e => {
|
|
435
|
+
console.error('[docs-build] fatal:', e.message);
|
|
436
|
+
ctx.buildDocsState.set(d, { status: 'error', sections: 0, files: 0, error: e.message, startedAt, completedAt: Date.now() });
|
|
437
|
+
});
|
|
438
|
+
} catch (err) {
|
|
439
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
440
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
441
|
+
}
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// -------------------------------------------------- GET /api/monograph-content
|
|
446
|
+
// Returns actual file content for a node (properties.content or file slice)
|
|
447
|
+
if (req.method === 'GET' && url === '/api/monograph-content') {
|
|
448
|
+
try {
|
|
449
|
+
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
450
|
+
const dir = qs.get('dir') || ctx.projectDir || process.cwd();
|
|
451
|
+
const id = qs.get('id') || '';
|
|
452
|
+
const d = path.resolve(dir || process.cwd());
|
|
453
|
+
const dbPath = path.join(d, '.monomind', 'monograph.db');
|
|
454
|
+
if (!id) { res.writeHead(400); res.end(JSON.stringify({ error: 'Missing ?id=' })); return true; }
|
|
455
|
+
if (!fs.existsSync(dbPath)) { res.writeHead(404); res.end(JSON.stringify({ error: 'Graph not built' })); return true; }
|
|
456
|
+
const { openDb, closeDb } = await import(new URL('../../../../monograph/dist/src/storage/db.js', import.meta.url).href);
|
|
457
|
+
const db = openDb(dbPath);
|
|
458
|
+
let content = '', filePath = '', startLine = 0, endLine = 0, language = '', name = '', type = '';
|
|
459
|
+
try {
|
|
460
|
+
const node = db.prepare('SELECT * FROM nodes WHERE id=?').get(id);
|
|
461
|
+
if (!node) { res.writeHead(404); res.end(JSON.stringify({ error: 'Node not found' })); return true; }
|
|
462
|
+
name = node.name || id;
|
|
463
|
+
type = node.label || 'Unknown';
|
|
464
|
+
filePath = node.file_path || '';
|
|
465
|
+
startLine = node.start_line || 0;
|
|
466
|
+
endLine = node.end_line || 0;
|
|
467
|
+
language = node.language || '';
|
|
468
|
+
// Try properties.content first (from official monograph pipeline)
|
|
469
|
+
if (node.properties) {
|
|
470
|
+
try {
|
|
471
|
+
const props = JSON.parse(node.properties);
|
|
472
|
+
if (props.content && props.content.trim()) { content = props.content; }
|
|
473
|
+
} catch {}
|
|
474
|
+
}
|
|
475
|
+
// Fallback: read from actual file
|
|
476
|
+
if (!content && filePath) {
|
|
477
|
+
const absPath = path.isAbsolute(filePath) ? filePath : path.join(d, filePath);
|
|
478
|
+
try {
|
|
479
|
+
const lines = fs.readFileSync(absPath, 'utf-8').split('\n');
|
|
480
|
+
const sl = Math.max(0, (startLine || 1) - 1);
|
|
481
|
+
const el = Math.min(lines.length, (endLine || startLine || lines.length) + 5);
|
|
482
|
+
content = lines.slice(sl, Math.min(el, sl + 120)).join('\n');
|
|
483
|
+
} catch {}
|
|
484
|
+
}
|
|
485
|
+
} finally { closeDb(db); }
|
|
486
|
+
res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
|
|
487
|
+
res.end(JSON.stringify({ content, filePath, startLine, endLine, language, name, type }));
|
|
488
|
+
} catch (err) {
|
|
489
|
+
res.writeHead(500); res.end(JSON.stringify({ error: err.message }));
|
|
490
|
+
}
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// -------------------------------------------------- GET /api/monograph-fts
|
|
495
|
+
// Full-text search with content snippets — powers the wiki search box
|
|
496
|
+
if (req.method === 'GET' && url === '/api/monograph-fts') {
|
|
497
|
+
try {
|
|
498
|
+
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
499
|
+
const dir = qs.get('dir') || ctx.projectDir || process.cwd();
|
|
500
|
+
// Cap ?q= to prevent DoS via megabyte FTS query strings.
|
|
501
|
+
const q = (qs.get('q') || '').trim().slice(0, 4096);
|
|
502
|
+
const limit = Math.min(100, parseInt(qs.get('limit') || '50', 10));
|
|
503
|
+
const d = path.resolve(dir || process.cwd());
|
|
504
|
+
const dbPath = path.join(d, '.monomind', 'monograph.db');
|
|
505
|
+
if (!q) { res.writeHead(400); res.end(JSON.stringify({ error: 'Missing ?q=' })); return true; }
|
|
506
|
+
if (!fs.existsSync(dbPath)) { res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) }); res.end(JSON.stringify({ nodes: [] })); return true; }
|
|
507
|
+
const { openDb, closeDb } = await import(new URL('../../../../monograph/dist/src/storage/db.js', import.meta.url).href);
|
|
508
|
+
const { ftsSearch } = await import(new URL('../../../../monograph/dist/src/storage/fts-store.js', import.meta.url).href);
|
|
509
|
+
const db = openDb(dbPath);
|
|
510
|
+
let nodes = [];
|
|
511
|
+
try {
|
|
512
|
+
const hits = ftsSearch(db, q, limit);
|
|
513
|
+
nodes = hits.map(h => {
|
|
514
|
+
let snippet = '';
|
|
515
|
+
if (h.properties) { try { const p = JSON.parse(h.properties); snippet = (p.content || '').slice(0, 200); } catch {} }
|
|
516
|
+
return { id: h.id, label: h.name, type: h.label, degree: 0, filePath: h.filePath || h.file_path, startLine: h.startLine || h.start_line, snippet };
|
|
517
|
+
});
|
|
518
|
+
} finally { closeDb(db); }
|
|
519
|
+
res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
|
|
520
|
+
res.end(JSON.stringify({ nodes }));
|
|
521
|
+
} catch (err) {
|
|
522
|
+
res.writeHead(500); res.end(JSON.stringify({ error: err.message }));
|
|
523
|
+
}
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// -------------------------------------------------- GET /api/monograph-related
|
|
528
|
+
// BFS from a node — returns node IDs sorted by graph distance (for re-ranking)
|
|
529
|
+
if (req.method === 'GET' && url === '/api/monograph-related') {
|
|
530
|
+
try {
|
|
531
|
+
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
532
|
+
const dir = qs.get('dir') || ctx.projectDir || process.cwd();
|
|
533
|
+
const id = qs.get('id') || '';
|
|
534
|
+
const limit = Math.min(200, parseInt(qs.get('limit') || '60', 10));
|
|
535
|
+
const maxDepth = Math.min(4, parseInt(qs.get('depth') || '3', 10));
|
|
536
|
+
const d = path.resolve(dir || process.cwd());
|
|
537
|
+
const dbPath = path.join(d, '.monomind', 'monograph.db');
|
|
538
|
+
if (!id || !fs.existsSync(dbPath)) { res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) }); res.end(JSON.stringify({ related: [] })); return true; }
|
|
539
|
+
const { openDb, closeDb } = await import(new URL('../../../../monograph/dist/src/storage/db.js', import.meta.url).href);
|
|
540
|
+
const db = openDb(dbPath);
|
|
541
|
+
const related = [];
|
|
542
|
+
try {
|
|
543
|
+
const visited = new Set([id]);
|
|
544
|
+
let frontier = [id];
|
|
545
|
+
for (let depth = 1; depth <= maxDepth && frontier.length > 0 && related.length < limit; depth++) {
|
|
546
|
+
const next = [];
|
|
547
|
+
for (const nodeId of frontier) {
|
|
548
|
+
const rows = db.prepare(`SELECT DISTINCT target_id as nid FROM edges WHERE source_id=? UNION SELECT DISTINCT source_id as nid FROM edges WHERE target_id=? LIMIT 30`).all(nodeId, nodeId);
|
|
549
|
+
for (const r of rows) {
|
|
550
|
+
if (!visited.has(r.nid)) {
|
|
551
|
+
visited.add(r.nid);
|
|
552
|
+
next.push(r.nid);
|
|
553
|
+
related.push({ id: r.nid, distance: depth });
|
|
554
|
+
if (related.length >= limit) break;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (related.length >= limit) break;
|
|
558
|
+
}
|
|
559
|
+
frontier = next;
|
|
560
|
+
}
|
|
561
|
+
} finally { closeDb(db); }
|
|
562
|
+
res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
|
|
563
|
+
res.end(JSON.stringify({ related }));
|
|
564
|
+
} catch (err) {
|
|
565
|
+
res.writeHead(500); res.end(JSON.stringify({ error: err.message }));
|
|
566
|
+
}
|
|
567
|
+
return true;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// -------------------------------------------------- GET /api/monograph-ai-context
|
|
571
|
+
// Builds a rich AI context bundle for a node: content + 1-hop neighbors
|
|
572
|
+
if (req.method === 'GET' && url === '/api/monograph-ai-context') {
|
|
573
|
+
try {
|
|
574
|
+
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
575
|
+
const dir = qs.get('dir') || ctx.projectDir || process.cwd();
|
|
576
|
+
const id = qs.get('id') || '';
|
|
577
|
+
const d = path.resolve(dir || process.cwd());
|
|
578
|
+
const dbPath = path.join(d, '.monomind', 'monograph.db');
|
|
579
|
+
if (!id || !fs.existsSync(dbPath)) { res.writeHead(404); res.end(JSON.stringify({ error: 'Not found' })); return true; }
|
|
580
|
+
const { openDb, closeDb } = await import(new URL('../../../../monograph/dist/src/storage/db.js', import.meta.url).href);
|
|
581
|
+
const db = openDb(dbPath);
|
|
582
|
+
let result = { node: null, content: '', neighbors: [], markdown: '' };
|
|
583
|
+
try {
|
|
584
|
+
const node = db.prepare('SELECT * FROM nodes WHERE id=?').get(id);
|
|
585
|
+
if (!node) { res.writeHead(404); res.end(JSON.stringify({ error: 'Node not found' })); return true; }
|
|
586
|
+
result.node = { id: node.id, name: node.name, type: node.label, filePath: node.file_path, startLine: node.start_line, endLine: node.end_line };
|
|
587
|
+
// Get content
|
|
588
|
+
let content = '';
|
|
589
|
+
if (node.properties) { try { const p = JSON.parse(node.properties); content = p.content || ''; } catch {} }
|
|
590
|
+
if (!content && node.file_path) {
|
|
591
|
+
const absPath = path.isAbsolute(node.file_path) ? node.file_path : path.join(d, node.file_path);
|
|
592
|
+
try {
|
|
593
|
+
const lines = fs.readFileSync(absPath, 'utf-8').split('\n');
|
|
594
|
+
const sl = Math.max(0, (node.start_line || 1) - 1);
|
|
595
|
+
const el = Math.min(lines.length, (node.end_line || node.start_line || lines.length) + 5);
|
|
596
|
+
content = lines.slice(sl, Math.min(el, sl + 80)).join('\n');
|
|
597
|
+
} catch {}
|
|
598
|
+
}
|
|
599
|
+
result.content = content;
|
|
600
|
+
// Get 1-hop neighbors
|
|
601
|
+
const outEdges = db.prepare('SELECT e.relation, n.id, n.name, n.label, n.file_path FROM edges e JOIN nodes n ON n.id=e.target_id WHERE e.source_id=? LIMIT 20').all(id);
|
|
602
|
+
const inEdges = db.prepare('SELECT e.relation, n.id, n.name, n.label, n.file_path FROM edges e JOIN nodes n ON n.id=e.source_id WHERE e.target_id=? LIMIT 20').all(id);
|
|
603
|
+
result.neighbors = [
|
|
604
|
+
...outEdges.map(e => ({ direction: 'out', relation: e.relation, id: e.id, name: e.name, type: e.label, filePath: e.file_path })),
|
|
605
|
+
...inEdges.map(e => ({ direction: 'in', relation: e.relation, id: e.id, name: e.name, type: e.label, filePath: e.file_path })),
|
|
606
|
+
];
|
|
607
|
+
// Build markdown for clipboard/AI
|
|
608
|
+
const lines2 = [];
|
|
609
|
+
lines2.push(`# ${node.name} [${node.label}]`);
|
|
610
|
+
if (node.file_path) lines2.push(`**File:** \`${node.file_path}\`${node.start_line ? ` (line ${node.start_line})` : ''}`);
|
|
611
|
+
if (content) lines2.push(`\n\`\`\`${node.language || ''}\n${content.slice(0, 3000)}\n\`\`\``);
|
|
612
|
+
if (outEdges.length) {
|
|
613
|
+
lines2.push(`\n**Depends on (${outEdges.length}):**`);
|
|
614
|
+
outEdges.forEach(e => lines2.push(`- ${e.relation} → ${e.name} [${e.label}]${e.file_path ? ' `' + e.file_path + '`' : ''}`));
|
|
615
|
+
}
|
|
616
|
+
if (inEdges.length) {
|
|
617
|
+
lines2.push(`\n**Used by (${inEdges.length}):**`);
|
|
618
|
+
inEdges.forEach(e => lines2.push(`- ${e.relation} ← ${e.name} [${e.label}]${e.file_path ? ' `' + e.file_path + '`' : ''}`));
|
|
619
|
+
}
|
|
620
|
+
result.markdown = lines2.join('\n');
|
|
621
|
+
} finally { closeDb(db); }
|
|
622
|
+
res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
|
|
623
|
+
res.end(JSON.stringify(result));
|
|
624
|
+
} catch (err) {
|
|
625
|
+
res.writeHead(500); res.end(JSON.stringify({ error: err.message }));
|
|
626
|
+
}
|
|
627
|
+
return true;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// -------------------------------------------------- GET /api/monograph-query
|
|
631
|
+
if (req.method === 'GET' && url === '/api/monograph-query') {
|
|
632
|
+
try {
|
|
633
|
+
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
634
|
+
const dir = qs.get('dir') || ctx.projectDir || process.cwd();
|
|
635
|
+
const q = (qs.get('q') || '').trim().slice(0, 4096);
|
|
636
|
+
const d = path.resolve(dir || process.cwd());
|
|
637
|
+
const dbPath = path.join(d, '.monomind', 'monograph.db');
|
|
638
|
+
if (!q) { res.writeHead(400); res.end(JSON.stringify({ error: 'Missing ?q= parameter' })); return true; }
|
|
639
|
+
if (!fs.existsSync(dbPath)) { res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) }); res.end(JSON.stringify({ success: false, result: 'Graph not built yet. Run: monomind monograph build' })); return true; }
|
|
640
|
+
const { openDb, closeDb } = await import(new URL('../../../../monograph/dist/src/storage/db.js', import.meta.url).href);
|
|
641
|
+
const { ftsSearch } = await import(new URL('../../../../monograph/dist/src/storage/fts-store.js', import.meta.url).href);
|
|
642
|
+
const db = openDb(dbPath);
|
|
643
|
+
let result = '';
|
|
644
|
+
try {
|
|
645
|
+
const hits = ftsSearch(db, q, 20);
|
|
646
|
+
if (!hits.length) {
|
|
647
|
+
result = `No matches found for: "${q}"`;
|
|
648
|
+
} else {
|
|
649
|
+
result = hits.map((h, i) => `${String(i+1).padStart(3,' ')}. ${h.name} [${h.normLabel}]${h.filePath ? '\n ' + h.filePath : ''}`).join('\n');
|
|
650
|
+
// Show outgoing edges for top hit
|
|
651
|
+
const topHit = hits[0];
|
|
652
|
+
const neighbors = db.prepare('SELECT target_id, relation FROM edges WHERE source_id=? LIMIT 10').all(topHit.id);
|
|
653
|
+
if (neighbors.length) {
|
|
654
|
+
result += `\n\n── ${topHit.name} references:\n` + neighbors.map(n => ` ${n.relation} → ${n.target_id.split('/').pop() || n.target_id}`).join('\n');
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
} finally { closeDb(db); }
|
|
658
|
+
res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
|
|
659
|
+
res.end(JSON.stringify({ success: true, query: q, result }));
|
|
660
|
+
} catch (err) {
|
|
661
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
662
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
663
|
+
}
|
|
664
|
+
return true;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// -------------------------------------------------- GET /api/monograph-explain
|
|
668
|
+
if (req.method === 'GET' && url === '/api/monograph-explain') {
|
|
669
|
+
try {
|
|
670
|
+
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
671
|
+
const dir = qs.get('dir') || ctx.projectDir || process.cwd();
|
|
672
|
+
const nodeQ = (qs.get('node') || '').trim().slice(0, 4096);
|
|
673
|
+
const d = path.resolve(dir || process.cwd());
|
|
674
|
+
const dbPath = path.join(d, '.monomind', 'monograph.db');
|
|
675
|
+
if (!nodeQ) { res.writeHead(400); res.end(JSON.stringify({ error: 'Missing ?node= parameter' })); return true; }
|
|
676
|
+
if (!fs.existsSync(dbPath)) { res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) }); res.end(JSON.stringify({ success: false, explanation: 'Graph not built yet. Run: monomind monograph build' })); return true; }
|
|
677
|
+
const { openDb, closeDb } = await import(new URL('../../../../monograph/dist/src/storage/db.js', import.meta.url).href);
|
|
678
|
+
const { ftsSearch } = await import(new URL('../../../../monograph/dist/src/storage/fts-store.js', import.meta.url).href);
|
|
679
|
+
const db = openDb(dbPath);
|
|
680
|
+
let explanation = '';
|
|
681
|
+
try {
|
|
682
|
+
let nd = db.prepare('SELECT * FROM nodes WHERE id=?').get(nodeQ) || db.prepare('SELECT * FROM nodes WHERE name=?').get(nodeQ);
|
|
683
|
+
if (!nd) { const hits = ftsSearch(db, nodeQ, 1); if (hits[0]) nd = db.prepare('SELECT * FROM nodes WHERE id=?').get(hits[0].id); }
|
|
684
|
+
if (!nd) {
|
|
685
|
+
explanation = `No node found matching: "${nodeQ}"`;
|
|
686
|
+
} else {
|
|
687
|
+
const outEdges = db.prepare('SELECT target_id, relation FROM edges WHERE source_id=? LIMIT 20').all(nd.id);
|
|
688
|
+
const inEdges = db.prepare('SELECT source_id, relation FROM edges WHERE target_id=? LIMIT 20').all(nd.id);
|
|
689
|
+
explanation = [
|
|
690
|
+
`## ${nd.name} [${nd.label}]`,
|
|
691
|
+
nd.file_path ? `File: ${nd.file_path}${nd.start_line ? ':' + nd.start_line : ''}` : '',
|
|
692
|
+
nd.language ? `Language: ${nd.language}` : '',
|
|
693
|
+
nd.is_exported ? 'Exported: yes' : 'Exported: no',
|
|
694
|
+
'',
|
|
695
|
+
outEdges.length ? `References (${outEdges.length}):\n` + outEdges.map(e => ` ${e.relation} → ${e.target_id.split('/').pop() || e.target_id}`).join('\n') : 'No outgoing references.',
|
|
696
|
+
inEdges.length ? `\nReferenced by (${inEdges.length}):\n` + inEdges.map(e => ` ${e.source_id.split('/').pop() || e.source_id} [${e.relation}]`).join('\n') : '',
|
|
697
|
+
].filter(Boolean).join('\n');
|
|
698
|
+
}
|
|
699
|
+
} finally { closeDb(db); }
|
|
700
|
+
res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
|
|
701
|
+
res.end(JSON.stringify({ success: true, node: nodeQ, explanation }));
|
|
702
|
+
} catch (err) {
|
|
703
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
704
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
705
|
+
}
|
|
706
|
+
return true;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// -------------------------------------------------- GET /api/monograph-path
|
|
710
|
+
if (req.method === 'GET' && url === '/api/monograph-path') {
|
|
711
|
+
try {
|
|
712
|
+
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
713
|
+
const dir = qs.get('dir') || ctx.projectDir || process.cwd();
|
|
714
|
+
const from = (qs.get('from') || '').trim().slice(0, 4096);
|
|
715
|
+
const to = (qs.get('to') || '').trim().slice(0, 4096);
|
|
716
|
+
const d = path.resolve(dir || process.cwd());
|
|
717
|
+
const dbPath = path.join(d, '.monomind', 'monograph.db');
|
|
718
|
+
if (!from || !to) { res.writeHead(400); res.end(JSON.stringify({ error: 'Missing ?from= and ?to= parameters' })); return true; }
|
|
719
|
+
if (!fs.existsSync(dbPath)) { res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) }); res.end(JSON.stringify({ success: false, path: 'Graph not built yet.' })); return true; }
|
|
720
|
+
// Import only graphology-free storage modules to avoid broken graphology dep
|
|
721
|
+
const { openDb, closeDb } = await import(new URL('../../../../monograph/dist/src/storage/db.js', import.meta.url).href);
|
|
722
|
+
const { ftsSearch } = await import(new URL('../../../../monograph/dist/src/storage/fts-store.js', import.meta.url).href);
|
|
723
|
+
// SQL-based BFS for shortest path (avoids graphology)
|
|
724
|
+
const getShortestPath = (db, fromId, toId, maxDepth = 6) => {
|
|
725
|
+
if (fromId === toId) return [fromId];
|
|
726
|
+
const visited = new Set([fromId]);
|
|
727
|
+
let frontier = [[fromId]];
|
|
728
|
+
for (let depth = 0; depth < maxDepth; depth++) {
|
|
729
|
+
const next = [];
|
|
730
|
+
for (const chain of frontier) {
|
|
731
|
+
const cur = chain[chain.length - 1];
|
|
732
|
+
const neighbors = db.prepare('SELECT target_id AS id FROM edges WHERE source_id=? UNION SELECT source_id AS id FROM edges WHERE target_id=?').all(cur, cur);
|
|
733
|
+
for (const { id } of neighbors) {
|
|
734
|
+
if (!visited.has(id)) {
|
|
735
|
+
const newChain = [...chain, id];
|
|
736
|
+
if (id === toId) return newChain;
|
|
737
|
+
visited.add(id);
|
|
738
|
+
next.push(newChain);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
if (!next.length) break;
|
|
743
|
+
frontier = next;
|
|
744
|
+
}
|
|
745
|
+
return null;
|
|
746
|
+
};
|
|
747
|
+
const db = openDb(dbPath);
|
|
748
|
+
let pathResult = '';
|
|
749
|
+
try {
|
|
750
|
+
const resolveId = (q) => {
|
|
751
|
+
const direct = db.prepare('SELECT id FROM nodes WHERE id=? OR name=?').get(q, q);
|
|
752
|
+
if (direct) return direct.id;
|
|
753
|
+
const hits = ftsSearch(db, q, 1);
|
|
754
|
+
return hits[0]?.id || q;
|
|
755
|
+
};
|
|
756
|
+
const fromId = resolveId(from);
|
|
757
|
+
const toId = resolveId(to);
|
|
758
|
+
const p = getShortestPath(db, fromId, toId);
|
|
759
|
+
if (!p || !p.length) {
|
|
760
|
+
pathResult = `No path found between "${from}" and "${to}"`;
|
|
761
|
+
} else {
|
|
762
|
+
const names = p.map(id => { const n = db.prepare('SELECT name FROM nodes WHERE id=?').get(id); return n ? n.name : id.split('/').pop() || id; });
|
|
763
|
+
pathResult = names.join(' → ') + ` (${p.length - 1} hop${p.length !== 2 ? 's' : ''})`;
|
|
764
|
+
}
|
|
765
|
+
} finally { closeDb(db); }
|
|
766
|
+
res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
|
|
767
|
+
res.end(JSON.stringify({ success: true, from, to, path: pathResult }));
|
|
768
|
+
} catch (err) {
|
|
769
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
770
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
771
|
+
}
|
|
772
|
+
return true;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// -------------------------------------------------- GET /api/monograph-watch-status
|
|
776
|
+
if (req.method === 'GET' && url === '/api/monograph-watch-status') {
|
|
777
|
+
try {
|
|
778
|
+
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
779
|
+
const dir = qs.get('dir') || ctx.projectDir || process.cwd();
|
|
780
|
+
const d = path.resolve(dir || process.cwd());
|
|
781
|
+
let running = false, pid = null;
|
|
782
|
+
for (const pidName of ['monograph.watch.pid', 'monograph-watch.pid']) {
|
|
783
|
+
try {
|
|
784
|
+
const pp = path.join(d, '.monomind', pidName);
|
|
785
|
+
pid = parseInt(fs.readFileSync(pp, 'utf-8').trim(), 10);
|
|
786
|
+
process.kill(pid, 0);
|
|
787
|
+
if (!ctx.looksLikeOurProcess(pid, d)) continue;
|
|
788
|
+
running = true;
|
|
789
|
+
break;
|
|
790
|
+
} catch {}
|
|
791
|
+
}
|
|
792
|
+
res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
|
|
793
|
+
res.end(JSON.stringify({ running, pid }));
|
|
794
|
+
} catch (err) {
|
|
795
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
796
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
797
|
+
}
|
|
798
|
+
return true;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// -------------------------------------------------- POST /api/monograph-watch-toggle
|
|
802
|
+
if (req.method === 'POST' && url === '/api/monograph-watch-toggle') {
|
|
803
|
+
try {
|
|
804
|
+
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
805
|
+
const dir = qs.get('dir') || ctx.projectDir || process.cwd();
|
|
806
|
+
const d = path.resolve(dir || process.cwd());
|
|
807
|
+
const pidPath = path.join(d, '.monomind', 'monograph.watch.pid');
|
|
808
|
+
let wasRunning = false;
|
|
809
|
+
for (const pidName of ['monograph.watch.pid', 'monograph-watch.pid']) {
|
|
810
|
+
try {
|
|
811
|
+
const pp = path.join(d, '.monomind', pidName);
|
|
812
|
+
const pid = parseInt(fs.readFileSync(pp, 'utf-8').trim(), 10);
|
|
813
|
+
process.kill(pid, 0);
|
|
814
|
+
if (!ctx.looksLikeOurProcess(pid, d)) continue;
|
|
815
|
+
wasRunning = true;
|
|
816
|
+
process.kill(pid, 'SIGTERM');
|
|
817
|
+
try { fs.unlinkSync(pp); } catch {}
|
|
818
|
+
} catch {}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (wasRunning) {
|
|
822
|
+
res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
|
|
823
|
+
res.end(JSON.stringify({ running: false, action: 'stopped' }));
|
|
824
|
+
} else {
|
|
825
|
+
const { spawn: sp } = await import('child_process');
|
|
826
|
+
const child = sp(process.execPath, [process.argv[1], 'monograph', 'watch'], { stdio: 'ignore', detached: true, cwd: d, env: process.env });
|
|
827
|
+
child.unref();
|
|
828
|
+
try { fs.mkdirSync(path.join(d, '.monomind'), { recursive: true }); } catch {}
|
|
829
|
+
try { fs.writeFileSync(pidPath, String(child.pid)); } catch {}
|
|
830
|
+
res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
|
|
831
|
+
res.end(JSON.stringify({ running: true, pid: child.pid, action: 'started' }));
|
|
832
|
+
}
|
|
833
|
+
} catch (err) {
|
|
834
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
835
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
836
|
+
}
|
|
837
|
+
return true;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// -------------------------------------------------- GET /api/monograph-benchmark
|
|
841
|
+
if (req.method === 'GET' && url === '/api/monograph-benchmark') {
|
|
842
|
+
try {
|
|
843
|
+
const qs = new URL(req.url, 'http://localhost').searchParams;
|
|
844
|
+
const dir = qs.get('dir') || ctx.projectDir || process.cwd();
|
|
845
|
+
const d = path.resolve(dir || process.cwd());
|
|
846
|
+
const graphPath = path.join(d, '.monomind', 'graph', 'graph.json');
|
|
847
|
+
const legacyPath = path.join(d, 'graphify-out', 'graph.json');
|
|
848
|
+
const gp = fs.existsSync(graphPath) ? graphPath : (fs.existsSync(legacyPath) ? legacyPath : null);
|
|
849
|
+
|
|
850
|
+
if (!gp) {
|
|
851
|
+
res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
|
|
852
|
+
res.end(JSON.stringify({ available: false }));
|
|
853
|
+
return true;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const { execSync: ex } = await import('child_process');
|
|
857
|
+
const out = ex(`graphify benchmark ${gp}`, { encoding: 'utf8', cwd: d, timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
858
|
+
res.writeHead(200, { 'Content-Type': 'application/json', ...(corsOrigin ? { 'Access-Control-Allow-Origin': corsOrigin } : {}) });
|
|
859
|
+
res.end(JSON.stringify({ available: true, result: out }));
|
|
860
|
+
} catch (err) {
|
|
861
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
862
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
863
|
+
}
|
|
864
|
+
return true;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// Reads .monomind/monograph.db via sqlite3 CLI to avoid bundling better-sqlite3.
|
|
868
|
+
if (req.method === 'GET' && url === '/api/monograph') {
|
|
869
|
+
try {
|
|
870
|
+
const dbPath = path.join(ctx.projectDir || process.cwd(), '.monomind', 'monograph.db');
|
|
871
|
+
if (!fs.existsSync(dbPath)) {
|
|
872
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
873
|
+
res.end(JSON.stringify({ exists: false }));
|
|
874
|
+
return true;
|
|
875
|
+
}
|
|
876
|
+
const { execFileSync } = await import('child_process');
|
|
877
|
+
// Pipe SQL via stdin to avoid shell quoting issues with single-quoted SQL strings.
|
|
878
|
+
const runSql = (sql, timeout = 5000) => {
|
|
879
|
+
try {
|
|
880
|
+
return execFileSync('sqlite3', ['-json', dbPath],
|
|
881
|
+
{ encoding: 'utf-8', timeout: timeout, input: sql + ';' });
|
|
882
|
+
} catch (e) { return '[]'; }
|
|
883
|
+
};
|
|
884
|
+
const counts = JSON.parse(runSql(
|
|
885
|
+
"SELECT (SELECT COUNT(*) FROM nodes) AS nodes, (SELECT COUNT(*) FROM edges) AS edges;"
|
|
886
|
+
) || '[{}]')[0] || { nodes: 0, edges: 0 };
|
|
887
|
+
// Compute degree in one pass via GROUP BY (much faster than per-row subquery).
|
|
888
|
+
const gods = JSON.parse(runSql(
|
|
889
|
+
"WITH deg(node_id, d) AS (" +
|
|
890
|
+
" SELECT source_id, COUNT(*) FROM edges GROUP BY source_id " +
|
|
891
|
+
" UNION ALL " +
|
|
892
|
+
" SELECT target_id, COUNT(*) FROM edges GROUP BY target_id" +
|
|
893
|
+
"), totals AS (" +
|
|
894
|
+
" SELECT node_id, SUM(d) AS deg FROM deg GROUP BY node_id" +
|
|
895
|
+
") " +
|
|
896
|
+
"SELECT n.name, n.label, n.file_path, t.deg " +
|
|
897
|
+
"FROM nodes n JOIN totals t ON t.node_id = n.id " +
|
|
898
|
+
"WHERE n.label NOT IN ('Concept') " +
|
|
899
|
+
"AND n.file_path IS NOT NULL AND n.file_path != '' " +
|
|
900
|
+
"AND n.name NOT LIKE '(%' AND length(n.name) >= 3 " +
|
|
901
|
+
"ORDER BY t.deg DESC LIMIT 20",
|
|
902
|
+
10000
|
|
903
|
+
) || '[]');
|
|
904
|
+
const types = JSON.parse(runSql(
|
|
905
|
+
"SELECT label, COUNT(*) AS count FROM nodes GROUP BY label ORDER BY count DESC LIMIT 12"
|
|
906
|
+
) || '[]');
|
|
907
|
+
const relations = JSON.parse(runSql(
|
|
908
|
+
"SELECT relation, COUNT(*) AS count FROM edges GROUP BY relation ORDER BY count DESC"
|
|
909
|
+
) || '[]');
|
|
910
|
+
|
|
911
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
912
|
+
res.end(JSON.stringify({
|
|
913
|
+
exists: true,
|
|
914
|
+
nodes: counts.nodes,
|
|
915
|
+
edges: counts.edges,
|
|
916
|
+
godNodes: gods,
|
|
917
|
+
typeDistribution: types,
|
|
918
|
+
relationDistribution: relations,
|
|
919
|
+
updatedAt: (() => { try { return fs.statSync(dbPath).mtime; } catch { return null; } })(),
|
|
920
|
+
}));
|
|
921
|
+
} catch (err) {
|
|
922
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
923
|
+
res.end(JSON.stringify({ exists: true, nodes: 0, edges: 0, godNodes: [], typeDistribution: [], relationDistribution: [], error: String(err) }));
|
|
924
|
+
}
|
|
925
|
+
return true;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
return false;
|
|
929
|
+
}
|