monomind 1.10.26 → 1.10.27

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,61 @@ 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
+ var slotsLeft = 8 - anchors.length;
203
+ if (slotsLeft > 0) {
204
+ var gods = db.prepare(
205
+ "SELECT n.name, n.label, n.file_path, " +
206
+ "(SELECT COUNT(*) FROM edges WHERE source_id=n.id OR target_id=n.id) AS deg " +
207
+ "FROM nodes n " +
208
+ "WHERE n.label NOT IN ('Concept') AND n.file_path IS NOT NULL AND n.file_path != '' " +
209
+ "AND n.name NOT LIKE '(%' AND n.name NOT LIKE '%=>%' AND length(n.name) >= 3 " +
210
+ "ORDER BY deg DESC LIMIT 15"
211
+ ).all();
212
+ for (var gi = 0; gi < gods.length && anchors.length < 8; gi++) {
213
+ if (!seenPaths[gods[gi].file_path]) {
214
+ seenPaths[gods[gi].file_path] = 1;
215
+ anchors.push({ name: gods[gi].name, label: gods[gi].label, file_path: gods[gi].file_path, deg: gods[gi].deg, tag: '' });
216
+ }
217
+ }
218
+ }
219
+
220
+ if (anchors.length > 0) {
221
+ console.log('[COMPACT_GRAPH] ' + nodeC + ' nodes. Session context (✎ = recently edited):');
222
+ for (var ci = 0; ci < anchors.length; ci++) {
223
+ var g = anchors[ci];
224
+ console.log(' ' + (g.tag || ' ') + ' ' + g.name + ' [' + g.label + '] — ' + g.file_path + ' (deg ' + g.deg + ')');
190
225
  }
191
226
  console.log(' Use mcp__monomind__monograph_suggest first when navigating.');
192
227
  }
@@ -194,6 +229,37 @@ function _injectCompactGraphMap() {
194
229
  } catch (e) {}
195
230
  }
196
231
 
232
+ // ── Recent edit history ────────────────────────────────────────────────────────
233
+ // Track last N edited file paths so compact injection and pre-resolve can surface
234
+ // the files the LLM was actively working on instead of pure centrality anchors.
235
+ function _recordRecentEdit(filePath) {
236
+ if (!filePath) return;
237
+ try {
238
+ var f = path.join(CWD, '.monomind', 'metrics', 'recent-edits.json');
239
+ fs.mkdirSync(path.dirname(f), { recursive: true });
240
+ var d = { edits: [] };
241
+ try { d = JSON.parse(fs.readFileSync(f, 'utf-8')); } catch (_) {}
242
+ if (!Array.isArray(d.edits)) d.edits = [];
243
+ // Remove stale entry for same file, then prepend fresh one
244
+ d.edits = d.edits.filter(function(e) { return e.file !== filePath; });
245
+ d.edits.unshift({ file: filePath, editedAt: Date.now() });
246
+ if (d.edits.length > 10) d.edits = d.edits.slice(0, 10);
247
+ fs.writeFileSync(f, JSON.stringify(d));
248
+ } catch (e) { /* non-fatal */ }
249
+ }
250
+
251
+ function _getRecentEdits() {
252
+ try {
253
+ var f = path.join(CWD, '.monomind', 'metrics', 'recent-edits.json');
254
+ if (!fs.existsSync(f)) return [];
255
+ var d = JSON.parse(fs.readFileSync(f, 'utf-8'));
256
+ if (!Array.isArray(d.edits)) return [];
257
+ // Only return edits from the last 2 hours (session-scoped)
258
+ var cutoff = Date.now() - 2 * 60 * 60 * 1000;
259
+ return d.edits.filter(function(e) { return e.editedAt > cutoff; });
260
+ } catch (e) { return []; }
261
+ }
262
+
197
263
  // ── Loop drift detection ───────────────────────────────────────────────────────
198
264
  // Record tool call signatures per session, warn when the same call recurs ≥3×.
199
265
  function _recordToolCall(signature) {
@@ -910,11 +976,14 @@ const handlers = {
910
976
  else if (topGraph.label === 'Class' || topGraph.label === 'Interface') agent = 'system-architect';
911
977
  // Functions, files, methods → coder
912
978
  else agent = 'coder';
979
+ // Scale confidence by graph degree: well-connected nodes are stronger anchors.
980
+ var topDeg = topGraph.deg || 0;
981
+ var graphConf = topDeg > 30 ? 0.80 : (topDeg > 10 ? 0.75 : 0.70);
913
982
  result = Object.assign({}, result, {
914
983
  agent: agent,
915
984
  agentSlug: agent,
916
- confidence: 0.70,
917
- reason: 'Graph fallback: top file ' + (topGraph.name || '').substring(0, 30) + ' [' + topGraph.label + ']',
985
+ confidence: graphConf,
986
+ reason: 'Graph fallback: top file ' + (topGraph.name || '').substring(0, 30) + ' [' + topGraph.label + '] deg=' + topDeg,
918
987
  specificAgents: [],
919
988
  extrasMatches: [],
920
989
  });
@@ -1133,11 +1202,46 @@ const handlers = {
1133
1202
  // Pre-resolve top-5 relevant files for the user's prompt — the LLM
1134
1203
  // sees the answer inline instead of being told to call a tool.
1135
1204
  var suggestions = getMonographSuggestions(prompt, 5);
1205
+
1206
+ // Boost recently-edited files to the top of pre-resolve suggestions.
1207
+ // Even when the FTS index hasn't caught up to the latest edits, the
1208
+ // LLM should see the files it just modified as the primary context.
1209
+ try {
1210
+ var recentEditsForRoute = _getRecentEdits();
1211
+ if (recentEditsForRoute.length > 0) {
1212
+ // Extract prompt keywords for relevance gating
1213
+ var promptWords = (prompt || '').toLowerCase().match(/[a-z][a-z0-9_-]{2,}/g) || [];
1214
+ var promptWordSet = {};
1215
+ for (var pw = 0; pw < promptWords.length; pw++) promptWordSet[promptWords[pw]] = 1;
1216
+
1217
+ var existingFiles = {};
1218
+ for (var se = 0; se < suggestions.length; se++) existingFiles[suggestions[se].file || ''] = 1;
1219
+
1220
+ var editBoosts = [];
1221
+ for (var re = 0; re < recentEditsForRoute.length && editBoosts.length < 2; re++) {
1222
+ var reFile = recentEditsForRoute[re].file;
1223
+ // Skip if already in suggestions
1224
+ if (existingFiles[reFile]) continue;
1225
+ var reName = path.basename(reFile, path.extname(reFile)).toLowerCase();
1226
+ // Only boost if filename shares a keyword with the prompt OR the edit is very recent (<3 min)
1227
+ var veryRecent = (Date.now() - recentEditsForRoute[re].editedAt) < 3 * 60 * 1000;
1228
+ var matches = promptWordSet[reName] || veryRecent;
1229
+ if (matches) {
1230
+ editBoosts.push({ name: path.basename(reFile), label: 'File', file: reFile, deg: 0, _editBoost: true });
1231
+ }
1232
+ }
1233
+ if (editBoosts.length > 0) {
1234
+ suggestions = editBoosts.concat(suggestions).slice(0, 5);
1235
+ }
1236
+ }
1237
+ } catch (e) { /* non-fatal */ }
1238
+
1136
1239
  if (suggestions.length > 0) {
1137
1240
  console.log('\n[MONOGRAPH] ' + nodeCount + ' nodes indexed. Top files for this task (pre-resolved from graph):');
1138
1241
  for (var si = 0; si < suggestions.length; si++) {
1139
1242
  var s = suggestions[si];
1140
- console.log(' · ' + s.name + ' [' + s.label + '] — ' + (s.file || '') + ' (deg ' + s.deg + ')');
1243
+ var editTag = s._editBoost ? ' ' : '';
1244
+ console.log(' · ' + s.name + ' [' + s.label + '] — ' + (s.file || '') + (s.deg ? ' (deg ' + s.deg + ')' : '') + editTag);
1141
1245
  }
1142
1246
  console.log(' Use mcp__monomind__monograph_query / monograph_impact for deeper drill-down.');
1143
1247
  _recordGraphTelemetry('preresolve_hit');
@@ -1347,6 +1451,12 @@ const handlers = {
1347
1451
  intelligence.recordEdit(file);
1348
1452
  } catch (e) { /* non-fatal */ }
1349
1453
  }
1454
+ // Track recently-edited files for compact injection and pre-resolve boosting.
1455
+ try {
1456
+ var editedForRecent = hookInput.file_path || toolInput.file_path
1457
+ || process.env.TOOL_INPUT_file_path || args[0] || '';
1458
+ if (editedForRecent) _recordRecentEdit(editedForRecent);
1459
+ } catch (e) { /* non-fatal */ }
1350
1460
  // Increment write counter and rebuild monograph when threshold hit.
1351
1461
  _maybeRebuildMonograph();
1352
1462
 
@@ -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.27",
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,65 @@ 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 os = require('os');
2178
+ const home = os.homedir();
2179
+ const searchRoots = [
2180
+ path.join(home, 'Desktop'),
2181
+ path.join(home, 'projects'),
2182
+ path.join(home, 'code'),
2183
+ path.join(home, 'work'),
2184
+ path.join(home, 'dev'),
2185
+ path.join(home, 'repos'),
2186
+ path.join(home, 'src'),
2187
+ ].filter(r => fs.existsSync(r));
2188
+ // Also check known-projects registry if it exists
2189
+ const registryPath = path.join(home, '.monomind-projects.json');
2190
+ if (fs.existsSync(registryPath)) {
2191
+ try {
2192
+ const reg = JSON.parse(fs.readFileSync(registryPath, 'utf-8'));
2193
+ if (Array.isArray(reg.projects)) {
2194
+ for (const p of reg.projects) {
2195
+ if (!searchRoots.includes(p) && fs.existsSync(p))
2196
+ searchRoots.push(p);
2197
+ }
2198
+ }
2199
+ }
2200
+ catch { }
2201
+ }
2202
+ const found = new Set();
2203
+ function walk(dir, depth) {
2204
+ if (depth > maxDepth)
2205
+ return;
2206
+ const marker = path.join(dir, '.claude', 'helpers', 'hook-handler.cjs');
2207
+ if (fs.existsSync(marker)) {
2208
+ found.add(dir);
2209
+ return;
2210
+ } // don't recurse into a monomind project
2211
+ let entries;
2212
+ try {
2213
+ entries = fs.readdirSync(dir, { withFileTypes: true });
2214
+ }
2215
+ catch {
2216
+ return;
2217
+ }
2218
+ for (const e of entries) {
2219
+ if (!e.isDirectory())
2220
+ continue;
2221
+ if (e.name.startsWith('.') || e.name === 'node_modules')
2222
+ continue;
2223
+ walk(path.join(dir, e.name), depth + 1);
2224
+ }
2225
+ }
2226
+ for (const root of searchRoots) {
2227
+ walk(root, 0);
2228
+ }
2229
+ return [...found];
2230
+ }
2171
2231
  export default executeInit;
2172
2232
  //# 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.27",
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,59 @@ 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
+ const updateNode = db.prepare(`UPDATE nodes SET properties = ? WHERE id = ?`);
658
+ let written = 0;
659
+ const tx = db.transaction(() => {
660
+ for (const analysis of analyses) {
661
+ if (!analysis || !analysis.id) continue;
662
+ const existing = (() => {
663
+ try { const row = db.prepare('SELECT properties FROM nodes WHERE id=?').get(analysis.id); return row?.properties ? JSON.parse(row.properties) : {}; } catch { return {}; }
664
+ })();
665
+ const merged = {
666
+ ...existing,
667
+ ...(analysis.fileSummary ? { summary: analysis.fileSummary } : {}),
668
+ ...(analysis.tags ? { tags: analysis.tags } : {}),
669
+ ...(analysis.complexity ? { complexity: analysis.complexity } : {}),
670
+ ...(analysis.functionSummaries ? { functionSummaries: analysis.functionSummaries } : {}),
671
+ ...(analysis.classSummaries ? { classSummaries: analysis.classSummaries } : {}),
672
+ ua_analyzed_at: new Date().toISOString(),
673
+ };
674
+ updateNode.run(JSON.stringify(merged), analysis.id);
675
+ written++;
676
+ }
677
+ });
678
+ tx();
679
+ // Rebuild FTS so summaries are immediately searchable
680
+ try { db.prepare(`INSERT INTO nodes_fts(nodes_fts) VALUES('rebuild')`).run(); } catch {}
681
+ mg.closeDb(db);
682
+ console.log(`[understand] Imported ${written} analyses from stdin. FTS rebuilt.`);
683
+ return;
684
+ }
685
+
632
686
  // ── Load all file nodes ──────────────────────────────────────────────────
633
687
  let fileNodes = db.prepare(`SELECT id, name, file_path, properties FROM nodes WHERE label = 'File' AND file_path IS NOT NULL`).all();
634
688
  console.log(`[understand] Found ${fileNodes.length} file nodes in DB`);