monomind 1.10.26 → 1.10.28

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.
@@ -167,26 +167,60 @@ function _recordGraphTelemetry(event) {
167
167
  } catch (e) { /* non-fatal */ }
168
168
  }
169
169
 
170
- // Re-inject graph god nodes after compaction so the LLM doesn't lose its spatial map.
170
+ // Re-inject graph context after compaction so the LLM doesn't lose its spatial map.
171
+ // Prefers recently-edited files (session context) over pure degree centrality so
172
+ // the injected anchors match what the LLM was actually working on.
171
173
  function _injectCompactGraphMap() {
172
174
  try {
173
175
  var db = _openMonographDb();
174
176
  if (!db) return;
175
177
  try {
176
178
  var nodeC = db.prepare("SELECT COUNT(*) AS c FROM nodes").get().c;
177
- var gods = db.prepare(
178
- "SELECT n.name, n.label, n.file_path, " +
179
- "(SELECT COUNT(*) FROM edges WHERE source_id=n.id OR target_id=n.id) AS deg " +
180
- "FROM nodes n " +
181
- "WHERE n.label NOT IN ('Concept') AND n.file_path IS NOT NULL AND n.file_path != '' " +
182
- "AND n.name NOT LIKE '(%' AND n.name NOT LIKE '%=>%' AND length(n.name) >= 3 " +
183
- "ORDER BY deg DESC LIMIT 8"
184
- ).all();
185
- if (gods.length > 0) {
186
- console.log('[COMPACT_GRAPH] ' + nodeC + ' nodes available. Top spatial anchors:');
187
- for (var ci = 0; ci < gods.length; ci++) {
188
- var g = gods[ci];
189
- console.log(' · ' + g.name + ' [' + g.label + '] — ' + g.file_path + ' (deg ' + g.deg + ')');
179
+ var anchors = [];
180
+ var seenPaths = {};
181
+
182
+ // 1. Prefer recently-edited files (up to 5) — these are what matters NOW.
183
+ var recentEdits = _getRecentEdits();
184
+ for (var ri = 0; ri < Math.min(recentEdits.length, 5); ri++) {
185
+ var rfile = recentEdits[ri].file;
186
+ // Normalise to relative path for DB lookup
187
+ var rrel = (rfile.indexOf(CWD) === 0) ? rfile.slice(CWD.length + 1) : rfile;
188
+ try {
189
+ var rnode = db.prepare(
190
+ "SELECT n.name, n.label, n.file_path, " +
191
+ "(SELECT COUNT(*) FROM edges WHERE source_id=n.id OR target_id=n.id) AS deg " +
192
+ "FROM nodes n WHERE n.label='File' AND (n.file_path=? OR n.file_path=?) LIMIT 1"
193
+ ).get(rfile, rrel);
194
+ if (rnode && !seenPaths[rnode.file_path]) {
195
+ seenPaths[rnode.file_path] = 1;
196
+ anchors.push({ name: rnode.name, label: rnode.label, file_path: rnode.file_path, deg: rnode.deg, tag: '✎' });
197
+ }
198
+ } catch (e) { /* ignore — file may not be in graph yet */ }
199
+ }
200
+
201
+ // 2. Fill remaining slots (up to 8 total) with god nodes (high centrality).
202
+ if (anchors.length < 8) {
203
+ var gods = db.prepare(
204
+ "SELECT n.name, n.label, n.file_path, " +
205
+ "(SELECT COUNT(*) FROM edges WHERE source_id=n.id OR target_id=n.id) AS deg " +
206
+ "FROM nodes n " +
207
+ "WHERE n.label NOT IN ('Concept') AND n.file_path IS NOT NULL AND n.file_path != '' " +
208
+ "AND n.name NOT LIKE '(%' AND n.name NOT LIKE '%=>%' AND length(n.name) >= 3 " +
209
+ "ORDER BY deg DESC LIMIT 15"
210
+ ).all();
211
+ for (var gi = 0; gi < gods.length && anchors.length < 8; gi++) {
212
+ if (!seenPaths[gods[gi].file_path]) {
213
+ seenPaths[gods[gi].file_path] = 1;
214
+ anchors.push({ name: gods[gi].name, label: gods[gi].label, file_path: gods[gi].file_path, deg: gods[gi].deg, tag: '' });
215
+ }
216
+ }
217
+ }
218
+
219
+ if (anchors.length > 0) {
220
+ console.log('[COMPACT_GRAPH] ' + nodeC + ' nodes. Session context (✎ = recently edited):');
221
+ for (var ci = 0; ci < anchors.length; ci++) {
222
+ var g = anchors[ci];
223
+ console.log(' ' + (g.tag || ' ') + ' ' + g.name + ' [' + g.label + '] — ' + g.file_path + ' (deg ' + g.deg + ')');
190
224
  }
191
225
  console.log(' Use mcp__monomind__monograph_suggest first when navigating.');
192
226
  }
@@ -194,6 +228,37 @@ function _injectCompactGraphMap() {
194
228
  } catch (e) {}
195
229
  }
196
230
 
231
+ // ── Recent edit history ────────────────────────────────────────────────────────
232
+ // Track last N edited file paths so compact injection and pre-resolve can surface
233
+ // the files the LLM was actively working on instead of pure centrality anchors.
234
+ function _recordRecentEdit(filePath) {
235
+ if (!filePath) return;
236
+ try {
237
+ var f = path.join(CWD, '.monomind', 'metrics', 'recent-edits.json');
238
+ fs.mkdirSync(path.dirname(f), { recursive: true });
239
+ var d = { edits: [] };
240
+ try { d = JSON.parse(fs.readFileSync(f, 'utf-8')); } catch (_) {}
241
+ if (!Array.isArray(d.edits)) d.edits = [];
242
+ // Remove stale entry for same file, then prepend fresh one
243
+ d.edits = d.edits.filter(function(e) { return e.file !== filePath; });
244
+ d.edits.unshift({ file: filePath, editedAt: Date.now() });
245
+ if (d.edits.length > 10) d.edits = d.edits.slice(0, 10);
246
+ fs.writeFileSync(f, JSON.stringify(d));
247
+ } catch (e) { /* non-fatal */ }
248
+ }
249
+
250
+ function _getRecentEdits() {
251
+ try {
252
+ var f = path.join(CWD, '.monomind', 'metrics', 'recent-edits.json');
253
+ if (!fs.existsSync(f)) return [];
254
+ var d = JSON.parse(fs.readFileSync(f, 'utf-8'));
255
+ if (!Array.isArray(d.edits)) return [];
256
+ // Only return edits from the last 2 hours (session-scoped)
257
+ var cutoff = Date.now() - 2 * 60 * 60 * 1000;
258
+ return d.edits.filter(function(e) { return e.editedAt > cutoff; });
259
+ } catch (e) { return []; }
260
+ }
261
+
197
262
  // ── Loop drift detection ───────────────────────────────────────────────────────
198
263
  // Record tool call signatures per session, warn when the same call recurs ≥3×.
199
264
  function _recordToolCall(signature) {
@@ -910,11 +975,14 @@ const handlers = {
910
975
  else if (topGraph.label === 'Class' || topGraph.label === 'Interface') agent = 'system-architect';
911
976
  // Functions, files, methods → coder
912
977
  else agent = 'coder';
978
+ // Scale confidence by graph degree: well-connected nodes are stronger anchors.
979
+ var topDeg = topGraph.deg || 0;
980
+ var graphConf = topDeg > 30 ? 0.80 : (topDeg > 10 ? 0.75 : 0.70);
913
981
  result = Object.assign({}, result, {
914
982
  agent: agent,
915
983
  agentSlug: agent,
916
- confidence: 0.70,
917
- reason: 'Graph fallback: top file ' + (topGraph.name || '').substring(0, 30) + ' [' + topGraph.label + ']',
984
+ confidence: graphConf,
985
+ reason: 'Graph fallback: top file ' + (topGraph.name || '').substring(0, 30) + ' [' + topGraph.label + '] deg=' + topDeg,
918
986
  specificAgents: [],
919
987
  extrasMatches: [],
920
988
  });
@@ -1133,11 +1201,46 @@ const handlers = {
1133
1201
  // Pre-resolve top-5 relevant files for the user's prompt — the LLM
1134
1202
  // sees the answer inline instead of being told to call a tool.
1135
1203
  var suggestions = getMonographSuggestions(prompt, 5);
1204
+
1205
+ // Boost recently-edited files to the top of pre-resolve suggestions.
1206
+ // Even when the FTS index hasn't caught up to the latest edits, the
1207
+ // LLM should see the files it just modified as the primary context.
1208
+ try {
1209
+ var recentEditsForRoute = _getRecentEdits();
1210
+ if (recentEditsForRoute.length > 0) {
1211
+ // Extract prompt keywords for relevance gating
1212
+ var promptWords = (prompt || '').toLowerCase().match(/[a-z][a-z0-9_-]{2,}/g) || [];
1213
+ var promptWordSet = {};
1214
+ for (var pw = 0; pw < promptWords.length; pw++) promptWordSet[promptWords[pw]] = 1;
1215
+
1216
+ var existingFiles = {};
1217
+ for (var se = 0; se < suggestions.length; se++) existingFiles[suggestions[se].file || ''] = 1;
1218
+
1219
+ var editBoosts = [];
1220
+ for (var re = 0; re < recentEditsForRoute.length && editBoosts.length < 2; re++) {
1221
+ var reFile = recentEditsForRoute[re].file;
1222
+ // Skip if already in suggestions
1223
+ if (existingFiles[reFile]) continue;
1224
+ var reName = path.basename(reFile, path.extname(reFile)).toLowerCase();
1225
+ // Only boost if filename shares a keyword with the prompt OR the edit is very recent (<3 min)
1226
+ var veryRecent = (Date.now() - recentEditsForRoute[re].editedAt) < 3 * 60 * 1000;
1227
+ var matches = promptWordSet[reName] || veryRecent;
1228
+ if (matches) {
1229
+ editBoosts.push({ name: path.basename(reFile), label: 'File', file: reFile, deg: 0, _editBoost: true });
1230
+ }
1231
+ }
1232
+ if (editBoosts.length > 0) {
1233
+ suggestions = editBoosts.concat(suggestions).slice(0, 5);
1234
+ }
1235
+ }
1236
+ } catch (e) { /* non-fatal */ }
1237
+
1136
1238
  if (suggestions.length > 0) {
1137
1239
  console.log('\n[MONOGRAPH] ' + nodeCount + ' nodes indexed. Top files for this task (pre-resolved from graph):');
1138
1240
  for (var si = 0; si < suggestions.length; si++) {
1139
1241
  var s = suggestions[si];
1140
- console.log(' · ' + s.name + ' [' + s.label + '] — ' + (s.file || '') + ' (deg ' + s.deg + ')');
1242
+ var editTag = s._editBoost ? ' ' : '';
1243
+ console.log(' · ' + s.name + ' [' + s.label + '] — ' + (s.file || '') + (s.deg ? ' (deg ' + s.deg + ')' : '') + editTag);
1141
1244
  }
1142
1245
  console.log(' Use mcp__monomind__monograph_query / monograph_impact for deeper drill-down.');
1143
1246
  _recordGraphTelemetry('preresolve_hit');
@@ -1347,6 +1450,12 @@ const handlers = {
1347
1450
  intelligence.recordEdit(file);
1348
1451
  } catch (e) { /* non-fatal */ }
1349
1452
  }
1453
+ // Track recently-edited files for compact injection and pre-resolve boosting.
1454
+ try {
1455
+ var editedForRecent = hookInput.file_path || toolInput.file_path
1456
+ || process.env.TOOL_INPUT_file_path || args[0] || '';
1457
+ if (editedForRecent) _recordRecentEdit(editedForRecent);
1458
+ } catch (e) { /* non-fatal */ }
1350
1459
  // Increment write counter and rebuild monograph when threshold hit.
1351
1460
  _maybeRebuildMonograph();
1352
1461
 
@@ -47,5 +47,6 @@
47
47
  "Bash(mv monomind-memory.md monomind-memory.md)",
48
48
  "Bash(mv monomind-swarm.md monomind-swarm.md)"
49
49
  ]
50
- }
50
+ },
51
+ "model": "opusplan"
51
52
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "monomind",
3
- "version": "1.10.26",
3
+ "version": "1.10.28",
4
4
  "description": "Monomind - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -127,4 +127,4 @@
127
127
  "access": "public",
128
128
  "tag": "latest"
129
129
  }
130
- }
130
+ }
@@ -6,7 +6,7 @@ import { output } from '../output.js';
6
6
  import { confirm, select, multiSelect, input } from '../prompt.js';
7
7
  import * as fs from 'fs';
8
8
  import * as path from 'path';
9
- import { executeInit, executeUpgrade, executeUpgradeWithMissing, DEFAULT_INIT_OPTIONS, MINIMAL_INIT_OPTIONS, FULL_INIT_OPTIONS, } from '../init/index.js';
9
+ import { executeInit, executeUpgrade, executeUpgradeWithMissing, findMonomindProjects, DEFAULT_INIT_OPTIONS, MINIMAL_INIT_OPTIONS, FULL_INIT_OPTIONS, } from '../init/index.js';
10
10
  // Check if project is already initialized
11
11
  function isInitialized(cwd) {
12
12
  const claudePath = path.join(cwd, '.claude', 'settings.json');
@@ -663,10 +663,57 @@ const upgradeCommand = {
663
663
  type: 'boolean',
664
664
  default: false,
665
665
  },
666
+ {
667
+ name: 'all',
668
+ description: 'Upgrade all known monomind projects on this machine (scans ~/Desktop, ~/projects, etc.)',
669
+ type: 'boolean',
670
+ default: false,
671
+ },
666
672
  ],
667
673
  action: async (ctx) => {
668
674
  const addMissing = (ctx.flags['add-missing'] || ctx.flags.addMissing);
669
675
  const upgradeSettings = (ctx.flags.settings);
676
+ const upgradeAll = (ctx.flags.all);
677
+ // ── --all: scan for every monomind project and upgrade each ──────────────
678
+ if (upgradeAll) {
679
+ output.writeln();
680
+ output.writeln(output.bold('Upgrading all monomind projects'));
681
+ output.writeln(output.dim('Scanning ~/Desktop, ~/projects, ~/code… (this may take a moment)'));
682
+ output.writeln();
683
+ const projects = findMonomindProjects();
684
+ if (projects.length === 0) {
685
+ output.printInfo('No monomind projects found. Install monomind in a project first: npx monomind init');
686
+ return { success: true, exitCode: 0 };
687
+ }
688
+ output.printInfo(`Found ${projects.length} project(s). Upgrading…`);
689
+ output.writeln();
690
+ let succeeded = 0;
691
+ let failed = 0;
692
+ for (const projDir of projects) {
693
+ const spinner = output.createSpinner({ text: projDir });
694
+ spinner.start();
695
+ try {
696
+ const res = addMissing
697
+ ? await executeUpgradeWithMissing(projDir, upgradeSettings)
698
+ : await executeUpgrade(projDir, upgradeSettings);
699
+ if (res.success) {
700
+ spinner.succeed(projDir + ' (' + res.updated.length + ' updated)');
701
+ succeeded++;
702
+ }
703
+ else {
704
+ spinner.fail(projDir + ' — ' + (res.errors[0] || 'unknown error'));
705
+ failed++;
706
+ }
707
+ }
708
+ catch (e) {
709
+ spinner.fail(projDir + ' — ' + (e instanceof Error ? e.message : String(e)));
710
+ failed++;
711
+ }
712
+ }
713
+ output.writeln();
714
+ output.printInfo(`Done: ${succeeded} upgraded, ${failed} failed out of ${projects.length} projects.`);
715
+ return { success: failed === 0, exitCode: failed > 0 ? 1 : 0 };
716
+ }
670
717
  output.writeln();
671
718
  output.writeln(output.bold('Upgrading MonoMind'));
672
719
  if (addMissing && upgradeSettings) {
@@ -34,5 +34,11 @@ export declare function executeUpgrade(targetDir: string, upgradeSettings?: bool
34
34
  * @param upgradeSettings - If true, merge new settings into existing settings.json
35
35
  */
36
36
  export declare function executeUpgradeWithMissing(targetDir: string, upgradeSettings?: boolean): Promise<UpgradeResult>;
37
+ /**
38
+ * Scan common locations for directories that have monomind installed
39
+ * (presence of .claude/helpers/hook-handler.cjs is the definitive signal).
40
+ * Searches up to maxDepth directory levels below each search root.
41
+ */
42
+ export declare function findMonomindProjects(maxDepth?: number): string[];
37
43
  export default executeInit;
38
44
  //# sourceMappingURL=executor.d.ts.map
@@ -2168,5 +2168,66 @@ function countEnabledHooks(options) {
2168
2168
  count++;
2169
2169
  return count;
2170
2170
  }
2171
+ /**
2172
+ * Scan common locations for directories that have monomind installed
2173
+ * (presence of .claude/helpers/hook-handler.cjs is the definitive signal).
2174
+ * Searches up to maxDepth directory levels below each search root.
2175
+ */
2176
+ export function findMonomindProjects(maxDepth = 3) {
2177
+ const esmReq = createRequire(import.meta.url);
2178
+ const os = esmReq('os');
2179
+ const home = os.homedir();
2180
+ const searchRoots = [
2181
+ path.join(home, 'Desktop'),
2182
+ path.join(home, 'projects'),
2183
+ path.join(home, 'code'),
2184
+ path.join(home, 'work'),
2185
+ path.join(home, 'dev'),
2186
+ path.join(home, 'repos'),
2187
+ path.join(home, 'src'),
2188
+ ].filter(r => fs.existsSync(r));
2189
+ // Also check known-projects registry if it exists
2190
+ const registryPath = path.join(home, '.monomind-projects.json');
2191
+ if (fs.existsSync(registryPath)) {
2192
+ try {
2193
+ const reg = JSON.parse(fs.readFileSync(registryPath, 'utf-8'));
2194
+ if (Array.isArray(reg.projects)) {
2195
+ for (const p of reg.projects) {
2196
+ if (!searchRoots.includes(p) && fs.existsSync(p))
2197
+ searchRoots.push(p);
2198
+ }
2199
+ }
2200
+ }
2201
+ catch { }
2202
+ }
2203
+ const found = new Set();
2204
+ function walk(dir, depth) {
2205
+ if (depth > maxDepth)
2206
+ return;
2207
+ const marker = path.join(dir, '.claude', 'helpers', 'hook-handler.cjs');
2208
+ if (fs.existsSync(marker)) {
2209
+ found.add(dir);
2210
+ return;
2211
+ } // don't recurse into a monomind project
2212
+ let entries;
2213
+ try {
2214
+ entries = fs.readdirSync(dir, { withFileTypes: true });
2215
+ }
2216
+ catch {
2217
+ return;
2218
+ }
2219
+ for (const e of entries) {
2220
+ if (!e.isDirectory())
2221
+ continue;
2222
+ if (e.name.startsWith('.') || e.name === 'node_modules')
2223
+ continue;
2224
+ walk(path.join(dir, e.name), depth + 1);
2225
+ }
2226
+ }
2227
+ for (const root of searchRoots) {
2228
+ walk(root, 0);
2229
+ }
2230
+ return [...found];
2231
+ }
2171
2232
  export default executeInit;
2172
2233
  //# sourceMappingURL=executor.js.map
@@ -8,6 +8,6 @@ export { generateMCPConfig, generateMCPJson, } from './mcp-generator.js';
8
8
  export { generateStatuslineScript, generateStatuslineHook, } from './statusline-generator.js';
9
9
  export { generatePreCommitHook, generatePostCommitHook, generateSessionManager, generateAgentRouter, generateMemoryHelper, generateHookHandler, generateIntelligenceStub, generateAutoMemoryHook, } from './helpers-generator.js';
10
10
  export { generateClaudeMd, generateMinimalClaudeMd, CLAUDE_MD_TEMPLATES, } from './claudemd-generator.js';
11
- export { executeInit, executeUpgrade, executeUpgradeWithMissing, default } from './executor.js';
11
+ export { executeInit, executeUpgrade, executeUpgradeWithMissing, findMonomindProjects, default } from './executor.js';
12
12
  export type { UpgradeResult } from './executor.js';
13
13
  //# sourceMappingURL=index.d.ts.map
@@ -11,5 +11,5 @@ export { generateStatuslineScript, generateStatuslineHook, } from './statusline-
11
11
  export { generatePreCommitHook, generatePostCommitHook, generateSessionManager, generateAgentRouter, generateMemoryHelper, generateHookHandler, generateIntelligenceStub, generateAutoMemoryHook, } from './helpers-generator.js';
12
12
  export { generateClaudeMd, generateMinimalClaudeMd, CLAUDE_MD_TEMPLATES, } from './claudemd-generator.js';
13
13
  // Main executor
14
- export { executeInit, executeUpgrade, executeUpgradeWithMissing, default } from './executor.js';
14
+ export { executeInit, executeUpgrade, executeUpgradeWithMissing, findMonomindProjects, default } from './executor.js';
15
15
  //# sourceMappingURL=index.js.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monoes/monomindcli",
3
- "version": "1.10.26",
3
+ "version": "1.10.28",
4
4
  "type": "module",
5
5
  "description": "Monomind CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
6
6
  "main": "dist/src/index.js",
@@ -112,4 +112,4 @@
112
112
  "access": "public",
113
113
  "tag": "latest"
114
114
  }
115
- }
115
+ }
@@ -48,11 +48,12 @@ const dbPathArg = argVal('db') ? resolve(argVal('db')) : join(projectDir,
48
48
  const outputArg = argVal('output')? resolve(argVal('output')) : join(projectDir, '.understand', 'knowledge-graph.json');
49
49
  const batchSize = parseInt(argVal('batch-size') || '5', 10);
50
50
  const maxFiles = parseInt(argVal('max-files') || '0', 10);
51
- const dryRun = hasFlag('dry-run');
52
- const noLlm = hasFlag('no-llm');
53
- const layersOnly = hasFlag('layers-only');
54
- const incremental = hasFlag('incremental');
55
- const onboard = hasFlag('onboard');
51
+ const dryRun = hasFlag('dry-run');
52
+ const noLlm = hasFlag('no-llm');
53
+ const layersOnly = hasFlag('layers-only');
54
+ const incremental = hasFlag('incremental');
55
+ const importAnalysesStdin = hasFlag('import-analyses-stdin');
56
+ const onboard = hasFlag('onboard');
56
57
  const onboardOut = argVal('onboard-out') ? resolve(argVal('onboard-out')) : join(projectDir, 'ONBOARDING.md');
57
58
 
58
59
  // ── Resolve @monoes/monograph for DB access ──────────────────────────────────
@@ -629,6 +630,79 @@ async function main() {
629
630
  try { db.prepare(`ALTER TABLE nodes ADD COLUMN properties TEXT`).run(); } catch {}
630
631
  try { db.prepare(`CREATE TABLE IF NOT EXISTS communities (id INTEGER PRIMARY KEY, label TEXT, size INTEGER NOT NULL DEFAULT 0, cohesion_score REAL NOT NULL DEFAULT 0.0)`).run(); } catch {}
631
632
 
633
+ // ── Import analyses from stdin (slash-command write-back path) ───────────
634
+ // When Claude Code's /monomind:understand has collected per-file summaries
635
+ // through the active session, it pipes them back via:
636
+ // echo "$ANALYSES_JSON" | node understand-analyze.mjs --import-analyses-stdin
637
+ // The JSON shape: { "analyses": [{ "id": <nodeId>, "fileSummary": "...", "tags": [...], ... }] }
638
+ if (importAnalysesStdin) {
639
+ const stdinText = await new Promise((resolve) => {
640
+ let buf = '';
641
+ process.stdin.setEncoding('utf-8');
642
+ process.stdin.on('data', c => { buf += c; });
643
+ process.stdin.on('end', () => resolve(buf));
644
+ process.stdin.on('error', () => resolve(buf));
645
+ // Bail out after 30 s so the slash command never hangs
646
+ setTimeout(() => resolve(buf), 30000);
647
+ process.stdin.resume();
648
+ });
649
+ let parsed = null;
650
+ try { parsed = JSON.parse(stdinText); } catch {}
651
+ const analyses = (parsed && Array.isArray(parsed.analyses)) ? parsed.analyses : [];
652
+ if (analyses.length === 0) {
653
+ console.error('[understand] --import-analyses-stdin: no analyses found in stdin JSON');
654
+ mg.closeDb(db);
655
+ process.exit(1);
656
+ }
657
+ // Resolve each analysis item to a DB integer node id.
658
+ // graph.json nodes have id='file:<path>' (synthetic); the real PK is an integer.
659
+ // Accept: analysis.dbId (integer), analysis.filePath, or derive path from analysis.id.
660
+ const lookupByPath = db.prepare(`SELECT id, properties FROM nodes WHERE file_path=? OR file_path=? LIMIT 1`);
661
+ const lookupById = db.prepare(`SELECT id, properties FROM nodes WHERE id=? LIMIT 1`);
662
+ const updateNode = db.prepare(`UPDATE nodes SET properties = ? WHERE id = ?`);
663
+ let written = 0;
664
+ const tx = db.transaction(() => {
665
+ for (const analysis of analyses) {
666
+ if (!analysis) continue;
667
+ let nodeRow = null;
668
+ // 1. Prefer explicit integer dbId (added by newer graph.json emitter)
669
+ if (analysis.dbId != null) {
670
+ nodeRow = lookupById.get(analysis.dbId);
671
+ }
672
+ // 2. Fall back to filePath from graph.json node
673
+ if (!nodeRow && analysis.filePath) {
674
+ const rel = analysis.filePath.startsWith('/') ? relative(projectDir, analysis.filePath) : analysis.filePath;
675
+ nodeRow = lookupByPath.get(analysis.filePath, rel);
676
+ }
677
+ // 3. Derive path from synthetic id ('file:<path>')
678
+ if (!nodeRow && analysis.id && String(analysis.id).startsWith('file:')) {
679
+ const derivedPath = String(analysis.id).slice(5);
680
+ const rel = derivedPath.startsWith('/') ? relative(projectDir, derivedPath) : derivedPath;
681
+ nodeRow = lookupByPath.get(derivedPath, rel);
682
+ }
683
+ if (!nodeRow) continue;
684
+ const existing = (() => { try { return nodeRow.properties ? JSON.parse(nodeRow.properties) : {}; } catch { return {}; } })();
685
+ const merged = {
686
+ ...existing,
687
+ ...(analysis.fileSummary ? { summary: analysis.fileSummary } : {}),
688
+ ...(analysis.tags ? { tags: analysis.tags } : {}),
689
+ ...(analysis.complexity ? { complexity: analysis.complexity } : {}),
690
+ ...(analysis.functionSummaries ? { functionSummaries: analysis.functionSummaries } : {}),
691
+ ...(analysis.classSummaries ? { classSummaries: analysis.classSummaries } : {}),
692
+ ua_analyzed_at: new Date().toISOString(),
693
+ };
694
+ updateNode.run(JSON.stringify(merged), nodeRow.id);
695
+ written++;
696
+ }
697
+ });
698
+ tx();
699
+ // Rebuild FTS so summaries are immediately searchable
700
+ try { db.prepare(`INSERT INTO nodes_fts(nodes_fts) VALUES('rebuild')`).run(); } catch {}
701
+ mg.closeDb(db);
702
+ console.log(`[understand] Imported ${written} analyses from stdin. FTS rebuilt.`);
703
+ return;
704
+ }
705
+
632
706
  // ── Load all file nodes ──────────────────────────────────────────────────
633
707
  let fileNodes = db.prepare(`SELECT id, name, file_path, properties FROM nodes WHERE label = 'File' AND file_path IS NOT NULL`).all();
634
708
  console.log(`[understand] Found ${fileNodes.length} file nodes in DB`);
@@ -947,6 +1021,7 @@ function buildGraphJson(dir, fileNodes, analysisMap, layers) {
947
1021
  const a = analysisMap[n.file_path] || {};
948
1022
  return {
949
1023
  id: 'file:' + (n.file_path || n.name),
1024
+ dbId: n.id, // integer PK — use this in --import-analyses-stdin payloads
950
1025
  type: 'file',
951
1026
  name: n.name,
952
1027
  filePath: n.file_path,