@monoes/monomindcli 2.4.0 → 2.5.1

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.
@@ -1035,6 +1035,89 @@ export async function startServer({ port = 4242, projectDir, openBrowser = true
1035
1035
  .catch(() => { /* warm-up is best-effort */ });
1036
1036
  }, 3000);
1037
1037
  if (_warmTimer.unref) _warmTimer.unref();
1038
+
1039
+ // ── Second Brain live ingestion ──────────────────────────────────
1040
+ // This server is the one long-lived local process AND holds the warm
1041
+ // embedding model — so it watches for document changes and ingests
1042
+ // in-process within seconds, instead of waiting for the next session
1043
+ // start. Best-effort: recursive fs.watch is unsupported on some
1044
+ // platforms/volumes; the session-start reindex remains the backstop.
1045
+ try {
1046
+ const _sbDocExts = new Set(['.md', '.txt', '.pdf', '.docx']);
1047
+ const _sbSkip = /(^|\/)(node_modules|\.git|dist|\.monomind|\.claude|\.next|__pycache__|\.venv|vendor)(\/|$)/;
1048
+ const _sbPending = new Map(); // file -> debounce timer
1049
+ const _sbRoot = path.resolve(projectDir || process.cwd());
1050
+ const _sbWatcher = fs.watch(_sbRoot, { recursive: true }, (_evt, rel) => {
1051
+ try {
1052
+ if (!rel) return;
1053
+ const relStr = String(rel);
1054
+ if (_sbSkip.test(relStr) || relStr.startsWith('.')) return;
1055
+ if (!_sbDocExts.has(path.extname(relStr).toLowerCase())) return;
1056
+ const full = path.join(_sbRoot, relStr);
1057
+ clearTimeout(_sbPending.get(full));
1058
+ _sbPending.set(full, setTimeout(async () => {
1059
+ _sbPending.delete(full);
1060
+ try {
1061
+ if (!fs.existsSync(full)) return; // deleted — session-start reindex handles removal
1062
+ const pipeline = await import('../knowledge/document-pipeline.js');
1063
+ const r = await pipeline.ingestDocument(full, 'shared', _sbRoot);
1064
+ if (r.chunksIndexed > 0 && !r.skipped) {
1065
+ console.log(`[knowledge] live-ingested ${path.basename(full)} (${r.chunksIndexed} chunks)`);
1066
+ }
1067
+ } catch (_) { /* single-file ingest failure never matters here */ }
1068
+ }, 5000));
1069
+ } catch (_) { /* watcher callback must never throw */ }
1070
+ });
1071
+ activeWatchers.push(_sbWatcher);
1072
+ } catch (_) { /* recursive watch unavailable — the sweep below still covers it */ }
1073
+
1074
+ // Polling sweep backstop: fs.watch/fsevents silently stops delivering on
1075
+ // some volumes (exFAT/SMB — exactly where many projects live). Every 60s,
1076
+ // a bounded mtime walk ingests anything the watcher missed. Stat-only
1077
+ // when nothing changed; skips while a sweep is already running.
1078
+ try {
1079
+ const _sbDocExts2 = new Set(['.md', '.txt', '.pdf', '.docx']);
1080
+ const _sbSkipDirs = new Set(['node_modules', '.git', 'dist', '.monomind', '.claude', '.next', '__pycache__', '.venv', 'vendor']);
1081
+ const _sbSweepRoot = path.resolve(projectDir || process.cwd());
1082
+ let _sbLastSweep = Date.now();
1083
+ let _sbSweeping = false;
1084
+ const _sbSweepTimer = setInterval(async () => {
1085
+ if (_sbSweeping) return;
1086
+ _sbSweeping = true;
1087
+ const since = _sbLastSweep - 5000; // small overlap so boundary writes aren't missed
1088
+ _sbLastSweep = Date.now();
1089
+ try {
1090
+ const changed = [];
1091
+ let scanned = 0;
1092
+ const walk = (dir, depth) => {
1093
+ if (depth > 4 || scanned > 3000 || changed.length >= 20) return;
1094
+ let names;
1095
+ try { names = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
1096
+ for (const ent of names) {
1097
+ if (scanned++ > 3000 || changed.length >= 20) return;
1098
+ if (ent.name.startsWith('.') || _sbSkipDirs.has(ent.name)) continue;
1099
+ const full = path.join(dir, ent.name);
1100
+ if (ent.isDirectory()) { walk(full, depth + 1); continue; }
1101
+ if (!_sbDocExts2.has(path.extname(ent.name).toLowerCase())) continue;
1102
+ try { if (fs.statSync(full).mtimeMs > since) changed.push(full); } catch (_) {}
1103
+ }
1104
+ };
1105
+ walk(_sbSweepRoot, 0);
1106
+ if (changed.length) {
1107
+ const pipeline = await import('../knowledge/document-pipeline.js');
1108
+ for (const f of changed) {
1109
+ try {
1110
+ const r = await pipeline.ingestDocument(f, 'shared', _sbSweepRoot);
1111
+ if (r.chunksIndexed > 0 && !r.skipped) console.log(`[knowledge] sweep-ingested ${path.basename(f)} (${r.chunksIndexed} chunks)`);
1112
+ } catch (_) { /* per-file failures never matter here */ }
1113
+ }
1114
+ }
1115
+ } catch (_) { /* sweep is best-effort */ }
1116
+ finally { _sbSweeping = false; }
1117
+ }, 60_000);
1118
+ if (_sbSweepTimer.unref) _sbSweepTimer.unref();
1119
+ activeWatchers.push({ close: () => clearInterval(_sbSweepTimer) });
1120
+ } catch (_) { /* non-fatal */ }
1038
1121
  }
1039
1122
  } catch (_) { /* non-fatal */ }
1040
1123