@velvetmonkey/flywheel-memory 2.0.150 → 2.0.152

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.
Files changed (4) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +11 -5
  3. package/dist/index.js +619 -246
  4. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -127,6 +127,50 @@ var init_levenshtein = __esm({
127
127
  }
128
128
  });
129
129
 
130
+ // src/core/shared/serverLog.ts
131
+ function serverLog(component, message, level = "info") {
132
+ const entry = {
133
+ ts: Date.now(),
134
+ component,
135
+ message,
136
+ level
137
+ };
138
+ buffer.push(entry);
139
+ if (buffer.length > MAX_ENTRIES) {
140
+ buffer.shift();
141
+ }
142
+ const now = /* @__PURE__ */ new Date();
143
+ const hms = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`;
144
+ const prefix = level === "error" ? "[Memory] ERROR" : level === "warn" ? "[Memory] WARN" : "[Memory]";
145
+ console.error(`${prefix} [${hms}] [${component}] ${message}`);
146
+ }
147
+ function getServerLog(options = {}) {
148
+ const { since, component, limit = 100 } = options;
149
+ let entries = buffer;
150
+ if (since) {
151
+ entries = entries.filter((e) => e.ts > since);
152
+ }
153
+ if (component) {
154
+ entries = entries.filter((e) => e.component === component);
155
+ }
156
+ if (entries.length > limit) {
157
+ entries = entries.slice(-limit);
158
+ }
159
+ return {
160
+ entries,
161
+ server_uptime_ms: Date.now() - serverStartTs
162
+ };
163
+ }
164
+ var MAX_ENTRIES, buffer, serverStartTs;
165
+ var init_serverLog = __esm({
166
+ "src/core/shared/serverLog.ts"() {
167
+ "use strict";
168
+ MAX_ENTRIES = 200;
169
+ buffer = [];
170
+ serverStartTs = Date.now();
171
+ }
172
+ });
173
+
130
174
  // src/vault-scope.ts
131
175
  import { AsyncLocalStorage } from "node:async_hooks";
132
176
  function getActiveScopeOrNull() {
@@ -280,7 +324,27 @@ async function embedTextCached(text) {
280
324
  return embedding;
281
325
  }
282
326
  function contentHash(content) {
283
- return crypto.createHash("sha256").update(content).digest("hex").slice(0, 16);
327
+ return crypto.createHash("sha256").update(content + EMBEDDING_TEXT_VERSION).digest("hex").slice(0, 16);
328
+ }
329
+ function buildNoteEmbeddingText(content, filePath) {
330
+ const title = filePath.replace(/\.md$/, "").split("/").pop() || "";
331
+ const fmMatch = content.match(/^---[\s\S]*?---\n([\s\S]*)$/);
332
+ const body = fmMatch ? fmMatch[1] : content;
333
+ const frontmatter = fmMatch ? content.slice(0, content.indexOf("---", 3) + 3) : "";
334
+ const tags = [];
335
+ const arrayMatch = frontmatter.match(/^tags:\s*\[([^\]]*)\]/m);
336
+ if (arrayMatch) {
337
+ tags.push(...arrayMatch[1].split(",").map((t) => t.trim().replace(/['"]/g, "")).filter(Boolean));
338
+ } else {
339
+ const listMatch = frontmatter.match(/^tags:\s*\n((?:\s+-\s+.+\n?)+)/m);
340
+ if (listMatch) {
341
+ const items = listMatch[1].matchAll(/^\s+-\s+(.+)/gm);
342
+ for (const m of items) tags.push(m[1].trim().replace(/['"]/g, ""));
343
+ }
344
+ }
345
+ const parts = [`Note: ${title}`];
346
+ if (tags.length > 0) parts.push(`Tags: ${tags.slice(0, 5).join(", ")}`);
347
+ return parts.join(". ") + ".\n\n" + body;
284
348
  }
285
349
  function shouldIndexFile(filePath) {
286
350
  const parts = filePath.split("/");
@@ -321,7 +385,7 @@ async function buildEmbeddingsIndex(vaultPath2, onProgress) {
321
385
  if (onProgress) onProgress(progress);
322
386
  continue;
323
387
  }
324
- const embedding = await embedText(content);
388
+ const embedding = await embedText(buildNoteEmbeddingText(content, file.path));
325
389
  const buf = Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
326
390
  upsert.run(file.path, buf, hash, activeModelConfig.id, Date.now());
327
391
  } catch (err) {
@@ -351,7 +415,7 @@ async function updateEmbedding(notePath, absolutePath) {
351
415
  const hash = contentHash(content);
352
416
  const existing = db4.prepare("SELECT content_hash FROM note_embeddings WHERE path = ?").get(notePath);
353
417
  if (existing?.content_hash === hash) return;
354
- const embedding = await embedText(content);
418
+ const embedding = await embedText(buildNoteEmbeddingText(content, notePath));
355
419
  const buf = Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
356
420
  db4.prepare(`
357
421
  INSERT OR REPLACE INTO note_embeddings (path, embedding, content_hash, model, updated_at)
@@ -652,7 +716,7 @@ function getEntityEmbeddingsCount() {
652
716
  return 0;
653
717
  }
654
718
  }
655
- var MODEL_REGISTRY, DEFAULT_MODEL, activeModelConfig, EXCLUDED_DIRS2, MAX_FILE_SIZE2, db, pipeline, initPromise, embeddingsBuilding, embeddingCache, EMBEDDING_CACHE_MAX, entityEmbeddingsMap;
719
+ var MODEL_REGISTRY, DEFAULT_MODEL, activeModelConfig, EXCLUDED_DIRS2, MAX_FILE_SIZE2, db, pipeline, initPromise, embeddingsBuilding, embeddingCache, EMBEDDING_CACHE_MAX, entityEmbeddingsMap, EMBEDDING_TEXT_VERSION;
656
720
  var init_embeddings = __esm({
657
721
  "src/core/read/embeddings.ts"() {
658
722
  "use strict";
@@ -683,50 +747,7 @@ var init_embeddings = __esm({
683
747
  embeddingCache = /* @__PURE__ */ new Map();
684
748
  EMBEDDING_CACHE_MAX = 500;
685
749
  entityEmbeddingsMap = /* @__PURE__ */ new Map();
686
- }
687
- });
688
-
689
- // src/core/shared/serverLog.ts
690
- function serverLog(component, message, level = "info") {
691
- const entry = {
692
- ts: Date.now(),
693
- component,
694
- message,
695
- level
696
- };
697
- buffer.push(entry);
698
- if (buffer.length > MAX_ENTRIES) {
699
- buffer.shift();
700
- }
701
- const now = /* @__PURE__ */ new Date();
702
- const hms = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`;
703
- const prefix = level === "error" ? "[Memory] ERROR" : level === "warn" ? "[Memory] WARN" : "[Memory]";
704
- console.error(`${prefix} [${hms}] [${component}] ${message}`);
705
- }
706
- function getServerLog(options = {}) {
707
- const { since, component, limit = 100 } = options;
708
- let entries = buffer;
709
- if (since) {
710
- entries = entries.filter((e) => e.ts > since);
711
- }
712
- if (component) {
713
- entries = entries.filter((e) => e.component === component);
714
- }
715
- if (entries.length > limit) {
716
- entries = entries.slice(-limit);
717
- }
718
- return {
719
- entries,
720
- server_uptime_ms: Date.now() - serverStartTs
721
- };
722
- }
723
- var MAX_ENTRIES, buffer, serverStartTs;
724
- var init_serverLog = __esm({
725
- "src/core/shared/serverLog.ts"() {
726
- "use strict";
727
- MAX_ENTRIES = 200;
728
- buffer = [];
729
- serverStartTs = Date.now();
750
+ EMBEDDING_TEXT_VERSION = 2;
730
751
  }
731
752
  });
732
753
 
@@ -810,8 +831,8 @@ function getRecencyBoost(entityName, index) {
810
831
  if (ageHours < 168) return 1;
811
832
  return 0;
812
833
  }
813
- function loadRecencyFromStateDb() {
814
- const stateDb2 = getStateDb();
834
+ function loadRecencyFromStateDb(explicitStateDb) {
835
+ const stateDb2 = explicitStateDb ?? getStateDb();
815
836
  if (!stateDb2) return null;
816
837
  try {
817
838
  const rows = getAllRecencyFromDb(stateDb2);
@@ -833,8 +854,8 @@ function loadRecencyFromStateDb() {
833
854
  return null;
834
855
  }
835
856
  }
836
- function saveRecencyToStateDb(index) {
837
- const stateDb2 = getStateDb();
857
+ function saveRecencyToStateDb(index, explicitStateDb) {
858
+ const stateDb2 = explicitStateDb ?? getStateDb();
838
859
  if (!stateDb2) {
839
860
  console.error("[Flywheel] saveRecencyToStateDb: No StateDb available");
840
861
  return;
@@ -3487,6 +3508,7 @@ function isValidWikilinkText(text) {
3487
3508
  if (target !== target.trim()) return false;
3488
3509
  const trimmed = target.trim();
3489
3510
  if (trimmed.length === 0) return false;
3511
+ if (/\n/.test(trimmed)) return false;
3490
3512
  if (/[?!;]/.test(trimmed)) return false;
3491
3513
  if (/[,.]$/.test(trimmed)) return false;
3492
3514
  if (trimmed.includes(">")) return false;
@@ -3501,7 +3523,9 @@ function isValidWikilinkText(text) {
3501
3523
  }
3502
3524
  function sanitizeWikilinks(content) {
3503
3525
  const removed = [];
3504
- const sanitized = content.replace(/\[\[([^\]]+?)\]\]/g, (fullMatch, inner) => {
3526
+ let repaired = content.replace(/\[\s*\n\s*\[/g, "[[");
3527
+ repaired = repaired.replace(/\]\s*\n\s*\]/g, "]]");
3528
+ const sanitized = repaired.replace(/\[\[([^\]]+?)\]\]/g, (fullMatch, inner) => {
3505
3529
  if (isValidWikilinkText(inner)) {
3506
3530
  return fullMatch;
3507
3531
  }
@@ -4416,7 +4440,7 @@ var init_wikilinks = __esm({
4416
4440
  init_vault_scope();
4417
4441
  moduleStateDb4 = null;
4418
4442
  moduleConfig = null;
4419
- ALL_IMPLICIT_PATTERNS = ["proper-nouns", "single-caps", "camel-case", "acronyms", "quoted-terms"];
4443
+ ALL_IMPLICIT_PATTERNS = ["proper-nouns", "single-caps", "camel-case", "acronyms", "quoted-terms", "ticket-refs"];
4420
4444
  entityIndex = null;
4421
4445
  indexReady = false;
4422
4446
  indexError2 = null;
@@ -4891,6 +4915,38 @@ function isPreformattedList(content) {
4891
4915
  /^\d+\.\s/.test(firstLine) || // Numbered
4892
4916
  /^[-*+]\s*\[[ xX]\]/.test(firstLine);
4893
4917
  }
4918
+ function sanitizeForList(content) {
4919
+ const lines = content.split("\n");
4920
+ const result = [];
4921
+ let inCodeBlock = false;
4922
+ for (const line of lines) {
4923
+ if (line.trim().startsWith("```")) {
4924
+ inCodeBlock = !inCodeBlock;
4925
+ result.push(line);
4926
+ continue;
4927
+ }
4928
+ if (inCodeBlock) {
4929
+ result.push(line);
4930
+ continue;
4931
+ }
4932
+ const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
4933
+ if (headingMatch) {
4934
+ result.push(`**${headingMatch[2]}**`);
4935
+ continue;
4936
+ }
4937
+ const starBulletMatch = line.match(/^(\s*)\*\s(.+)$/);
4938
+ if (starBulletMatch) {
4939
+ result.push(`${starBulletMatch[1]}- ${starBulletMatch[2]}`);
4940
+ continue;
4941
+ }
4942
+ if (/^-{3,}$/.test(line.trim()) || /^\*{3,}$/.test(line.trim()) || /^_{3,}$/.test(line.trim())) {
4943
+ result.push("\u2014");
4944
+ continue;
4945
+ }
4946
+ result.push(line);
4947
+ }
4948
+ return result.join("\n");
4949
+ }
4894
4950
  function formatContent(content, format) {
4895
4951
  const trimmed = content.trim();
4896
4952
  if (trimmed === "") {
@@ -4914,9 +4970,10 @@ function formatContent(content, format) {
4914
4970
  return trimmed;
4915
4971
  case "bullet": {
4916
4972
  if (isPreformattedList(trimmed)) {
4917
- return trimmed;
4973
+ return sanitizeForList(trimmed);
4918
4974
  }
4919
- const lines = trimmed.split("\n");
4975
+ const sanitized = sanitizeForList(trimmed);
4976
+ const lines = sanitized.split("\n");
4920
4977
  return lines.map((line, i) => {
4921
4978
  if (i === 0) return `- ${line}`;
4922
4979
  if (line === "") return " ";
@@ -4947,13 +5004,14 @@ function formatContent(content, format) {
4947
5004
  }
4948
5005
  case "timestamp-bullet": {
4949
5006
  if (isPreformattedList(trimmed)) {
4950
- return trimmed;
5007
+ return sanitizeForList(trimmed);
4951
5008
  }
5009
+ const sanitized = sanitizeForList(trimmed);
4952
5010
  const now = /* @__PURE__ */ new Date();
4953
5011
  const hours = String(now.getHours()).padStart(2, "0");
4954
5012
  const minutes = String(now.getMinutes()).padStart(2, "0");
4955
5013
  const prefix = `- **${hours}:${minutes}** `;
4956
- const lines = trimmed.split("\n");
5014
+ const lines = sanitized.split("\n");
4957
5015
  const indent = " ";
4958
5016
  return lines.map((line, i) => {
4959
5017
  if (i === 0) return `${prefix}${line}`;
@@ -6596,6 +6654,7 @@ function createEmptyNote(file) {
6596
6654
 
6597
6655
  // src/core/read/graph.ts
6598
6656
  init_levenshtein();
6657
+ init_serverLog();
6599
6658
  init_embeddings();
6600
6659
  init_vault_scope();
6601
6660
  init_levenshtein();
@@ -6664,14 +6723,14 @@ async function buildVaultIndexInternal(vaultPath2, startTime, onProgress) {
6664
6723
  return { file, note };
6665
6724
  })
6666
6725
  );
6667
- for (const result of results) {
6726
+ for (let j = 0; j < results.length; j++) {
6727
+ const result = results[j];
6668
6728
  if (result.status === "fulfilled") {
6669
6729
  notes.set(result.value.note.path, result.value.note);
6670
6730
  } else {
6671
- const batchIndex = results.indexOf(result);
6672
- if (batchIndex >= 0 && batch[batchIndex]) {
6673
- parseErrors.push(batch[batchIndex].path);
6674
- }
6731
+ const filePath = batch[j]?.path ?? "<unknown>";
6732
+ const reason = result.reason instanceof Error ? result.reason.message : String(result.reason);
6733
+ parseErrors.push({ path: filePath, reason });
6675
6734
  }
6676
6735
  parsedCount++;
6677
6736
  }
@@ -6683,9 +6742,13 @@ async function buildVaultIndexInternal(vaultPath2, startTime, onProgress) {
6683
6742
  }
6684
6743
  }
6685
6744
  if (parseErrors.length > 0) {
6686
- console.error(`Failed to parse ${parseErrors.length} files:`);
6687
- for (const errorPath of parseErrors) {
6688
- console.error(` - ${errorPath}`);
6745
+ const msg = `Failed to parse ${parseErrors.length} file(s):`;
6746
+ console.error(msg);
6747
+ serverLog("index", msg, "error");
6748
+ for (const err of parseErrors) {
6749
+ const detail = ` - ${err.path}: ${err.reason}`;
6750
+ console.error(detail);
6751
+ serverLog("index", detail, "error");
6689
6752
  }
6690
6753
  }
6691
6754
  const entities = /* @__PURE__ */ new Map();
@@ -7877,7 +7940,8 @@ import {
7877
7940
  rangeOverlapsProtectedZone,
7878
7941
  detectImplicitEntities as detectImplicitEntities2,
7879
7942
  checkDbIntegrity,
7880
- safeBackupAsync
7943
+ safeBackupAsync,
7944
+ recordEntityMention as recordEntityMention2
7881
7945
  } from "@velvetmonkey/vault-core";
7882
7946
  init_serverLog();
7883
7947
 
@@ -8816,6 +8880,7 @@ var PipelineRunner = class {
8816
8880
  await runStep("forward_links", tracker, { files: p.events.length }, () => this.forwardLinks());
8817
8881
  await this.wikilinkCheck();
8818
8882
  await this.implicitFeedback();
8883
+ await runStep("incremental_recency", tracker, { files: p.events.length }, () => this.incrementalRecency());
8819
8884
  await runStep("corrections", tracker, {}, () => this.corrections());
8820
8885
  await runStep("prospect_scan", tracker, { files: p.events.length }, () => this.prospectScan());
8821
8886
  await this.suggestionScoring();
@@ -8912,9 +8977,19 @@ var PipelineRunner = class {
8912
8977
  const rows = p.sd.db.prepare("SELECT name, hub_score FROM entities").all();
8913
8978
  for (const r of rows) this.hubBefore.set(r.name, r.hub_score);
8914
8979
  }
8980
+ const entityScanAgeMs = p.ctx.lastEntityScanAt > 0 ? Date.now() - p.ctx.lastEntityScanAt : Infinity;
8981
+ if (entityScanAgeMs < 5 * 60 * 1e3) {
8982
+ tracker.start("entity_scan", {});
8983
+ tracker.skip("entity_scan", `cache valid (${Math.round(entityScanAgeMs / 1e3)}s old)`);
8984
+ this.entitiesBefore = p.sd ? getAllEntitiesFromDb(p.sd) : [];
8985
+ this.entitiesAfter = this.entitiesBefore;
8986
+ serverLog("watcher", `Entity scan: throttled (${Math.round(entityScanAgeMs / 1e3)}s old)`);
8987
+ return;
8988
+ }
8915
8989
  this.entitiesBefore = p.sd ? getAllEntitiesFromDb(p.sd) : [];
8916
8990
  tracker.start("entity_scan", { note_count: vaultIndex2.notes.size });
8917
8991
  await p.updateEntitiesInStateDb(p.vp, p.sd);
8992
+ p.ctx.lastEntityScanAt = Date.now();
8918
8993
  this.entitiesAfter = p.sd ? getAllEntitiesFromDb(p.sd) : [];
8919
8994
  const entityDiff = computeEntityDiff(this.entitiesBefore, this.entitiesAfter);
8920
8995
  const categoryChanges = [];
@@ -8951,6 +9026,11 @@ var PipelineRunner = class {
8951
9026
  // ── Step 3: Hub scores ────────────────────────────────────────────
8952
9027
  async hubScores() {
8953
9028
  const { p } = this;
9029
+ const hubAgeMs = p.ctx.lastHubScoreRebuildAt > 0 ? Date.now() - p.ctx.lastHubScoreRebuildAt : Infinity;
9030
+ if (hubAgeMs < 5 * 60 * 1e3) {
9031
+ serverLog("watcher", `Hub scores: throttled (${Math.round(hubAgeMs / 1e3)}s old)`);
9032
+ return { skipped: true, age_ms: hubAgeMs };
9033
+ }
8954
9034
  const vaultIndex2 = p.getVaultIndex();
8955
9035
  const hubUpdated = await exportHubScores(vaultIndex2, p.sd);
8956
9036
  const hubDiffs = [];
@@ -8961,18 +9041,19 @@ var PipelineRunner = class {
8961
9041
  if (prev !== r.hub_score) hubDiffs.push({ entity: r.name, before: prev, after: r.hub_score });
8962
9042
  }
8963
9043
  }
9044
+ p.ctx.lastHubScoreRebuildAt = Date.now();
8964
9045
  serverLog("watcher", `Hub scores: ${hubUpdated ?? 0} updated`);
8965
9046
  return { updated: hubUpdated ?? 0, diffs: hubDiffs.slice(0, 10) };
8966
9047
  }
8967
9048
  // ── Step 3.5: Recency ─────────────────────────────────────────────
8968
9049
  async recency() {
8969
9050
  const { p } = this;
8970
- const cachedRecency = loadRecencyFromStateDb();
9051
+ const cachedRecency = loadRecencyFromStateDb(p.sd ?? void 0);
8971
9052
  const cacheAgeMs = cachedRecency ? Date.now() - (cachedRecency.lastUpdated ?? 0) : Infinity;
8972
9053
  if (cacheAgeMs >= 60 * 60 * 1e3) {
8973
9054
  const entities = this.entitiesAfter.map((e) => ({ name: e.name, path: e.path, aliases: e.aliases }));
8974
9055
  const recencyIndex2 = await buildRecencyIndex(p.vp, entities);
8975
- saveRecencyToStateDb(recencyIndex2);
9056
+ saveRecencyToStateDb(recencyIndex2, p.sd ?? void 0);
8976
9057
  serverLog("watcher", `Recency: rebuilt ${recencyIndex2.lastMentioned.size} entities`);
8977
9058
  return { rebuilt: true, entities: recencyIndex2.lastMentioned.size };
8978
9059
  }
@@ -9082,9 +9163,16 @@ var PipelineRunner = class {
9082
9163
  const { p, tracker } = this;
9083
9164
  const vaultIndex2 = p.getVaultIndex();
9084
9165
  if (p.sd) {
9166
+ const cacheAgeMs = p.ctx.lastIndexCacheSaveAt > 0 ? Date.now() - p.ctx.lastIndexCacheSaveAt : Infinity;
9167
+ if (cacheAgeMs < 30 * 1e3) {
9168
+ tracker.start("index_cache", {});
9169
+ tracker.skip("index_cache", `saved recently (${Math.round(cacheAgeMs / 1e3)}s ago)`);
9170
+ return;
9171
+ }
9085
9172
  tracker.start("index_cache", { note_count: vaultIndex2.notes.size });
9086
9173
  try {
9087
9174
  saveVaultIndexToCache(p.sd, vaultIndex2);
9175
+ p.ctx.lastIndexCacheSaveAt = Date.now();
9088
9176
  tracker.end({ saved: true });
9089
9177
  serverLog("watcher", "Index cache saved");
9090
9178
  } catch (err) {
@@ -9341,6 +9429,7 @@ var PipelineRunner = class {
9341
9429
  );
9342
9430
  for (const diff of this.linkDiffs) {
9343
9431
  if (deletedFiles.has(diff.file)) continue;
9432
+ const newlyTracked = [];
9344
9433
  for (const target of diff.added) {
9345
9434
  if (checkApplication.get(target, diff.file)) continue;
9346
9435
  const entity = this.entitiesAfter.find(
@@ -9349,8 +9438,15 @@ var PipelineRunner = class {
9349
9438
  if (entity) {
9350
9439
  recordFeedback(p.sd, entity.name, "implicit:manual_added", diff.file, true);
9351
9440
  additionResults.push({ entity: entity.name, file: diff.file });
9441
+ newlyTracked.push({
9442
+ entity: entity.name,
9443
+ matchedTerm: entity.nameLower === target ? void 0 : target
9444
+ });
9352
9445
  }
9353
9446
  }
9447
+ if (newlyTracked.length > 0) {
9448
+ trackWikilinkApplications(p.sd, diff.file, newlyTracked);
9449
+ }
9354
9450
  }
9355
9451
  }
9356
9452
  const newlySuppressed = [];
@@ -9370,6 +9466,20 @@ var PipelineRunner = class {
9370
9466
  serverLog("watcher", `Suppression: ${newlySuppressed.length} entities newly suppressed: ${newlySuppressed.join(", ")}`);
9371
9467
  }
9372
9468
  }
9469
+ // ── Step 10.1: Incremental recency ──────────────────────────────
9470
+ async incrementalRecency() {
9471
+ const { p } = this;
9472
+ if (!p.sd) return { skipped: true };
9473
+ let updated = 0;
9474
+ const now = /* @__PURE__ */ new Date();
9475
+ for (const entry of this.forwardLinkResults) {
9476
+ for (const target of entry.resolved) {
9477
+ recordEntityMention2(p.sd, target, now);
9478
+ updated++;
9479
+ }
9480
+ }
9481
+ return { entities_updated: updated };
9482
+ }
9373
9483
  // ── Step 10.5: Corrections ────────────────────────────────────────
9374
9484
  async corrections() {
9375
9485
  const { p } = this;
@@ -9635,7 +9745,7 @@ import { openStateDb, scanVaultEntities as scanVaultEntities4, getAllEntitiesFro
9635
9745
 
9636
9746
  // src/core/write/memory.ts
9637
9747
  init_wikilinkFeedback();
9638
- import { recordEntityMention as recordEntityMention2 } from "@velvetmonkey/vault-core";
9748
+ import { recordEntityMention as recordEntityMention3 } from "@velvetmonkey/vault-core";
9639
9749
  var entityCacheMap = /* @__PURE__ */ new Map();
9640
9750
  var ENTITY_CACHE_TTL_MS = 6e4;
9641
9751
  function getEntityList(stateDb2) {
@@ -9676,7 +9786,7 @@ function updateGraphSignals(stateDb2, memoryKey, entities) {
9676
9786
  if (entities.length === 0) return;
9677
9787
  const now = /* @__PURE__ */ new Date();
9678
9788
  for (const entity of entities) {
9679
- recordEntityMention2(stateDb2, entity, now);
9789
+ recordEntityMention3(stateDb2, entity, now);
9680
9790
  }
9681
9791
  const sourcePath = `memory:${memoryKey}`;
9682
9792
  const targets = new Set(entities.map((e) => e.toLowerCase()));
@@ -13854,6 +13964,38 @@ function enrichResultLight(result, index, stateDb2) {
13854
13964
  }
13855
13965
  return enriched;
13856
13966
  }
13967
+ function enrichEntityCompact(entityName, stateDb2, index) {
13968
+ const enriched = {};
13969
+ if (stateDb2) {
13970
+ try {
13971
+ const entity = getEntityByName2(stateDb2, entityName);
13972
+ if (entity) {
13973
+ enriched.category = entity.category;
13974
+ enriched.hub_score = entity.hubScore;
13975
+ if (entity.aliases.length > 0) enriched.aliases = entity.aliases;
13976
+ enriched.path = entity.path;
13977
+ }
13978
+ } catch {
13979
+ }
13980
+ }
13981
+ if (index) {
13982
+ const entityPath = enriched.path ?? index.entities.get(entityName.toLowerCase());
13983
+ if (entityPath) {
13984
+ const note = index.notes.get(entityPath);
13985
+ const normalizedPath = entityPath.toLowerCase().replace(/\.md$/, "");
13986
+ const backlinks = index.backlinks.get(normalizedPath) || [];
13987
+ enriched.backlink_count = backlinks.length;
13988
+ if (note) {
13989
+ if (Object.keys(note.frontmatter).length > 0) enriched.frontmatter = note.frontmatter;
13990
+ if (note.tags.length > 0) enriched.tags = note.tags;
13991
+ if (note.outlinks.length > 0) {
13992
+ enriched.outlink_names = getOutlinkNames(note.outlinks, entityPath, index, stateDb2, COMPACT_OUTLINK_NAMES);
13993
+ }
13994
+ }
13995
+ }
13996
+ }
13997
+ return enriched;
13998
+ }
13857
13999
 
13858
14000
  // src/core/read/multihop.ts
13859
14001
  import { getEntityByName as getEntityByName3, searchEntities } from "@velvetmonkey/vault-core";
@@ -14156,6 +14298,151 @@ function extractDates(text) {
14156
14298
 
14157
14299
  // src/tools/read/query.ts
14158
14300
  init_stemmer();
14301
+
14302
+ // src/tools/read/structure.ts
14303
+ import * as fs14 from "fs";
14304
+ import * as path17 from "path";
14305
+ var HEADING_REGEX2 = /^(#{1,6})\s+(.+)$/;
14306
+ function extractHeadings2(content) {
14307
+ const lines = content.split("\n");
14308
+ const headings = [];
14309
+ let inCodeBlock = false;
14310
+ for (let i = 0; i < lines.length; i++) {
14311
+ const line = lines[i];
14312
+ if (line.startsWith("```")) {
14313
+ inCodeBlock = !inCodeBlock;
14314
+ continue;
14315
+ }
14316
+ if (inCodeBlock) continue;
14317
+ const match = line.match(HEADING_REGEX2);
14318
+ if (match) {
14319
+ headings.push({
14320
+ level: match[1].length,
14321
+ text: match[2].trim(),
14322
+ line: i + 1
14323
+ // 1-indexed
14324
+ });
14325
+ }
14326
+ }
14327
+ return headings;
14328
+ }
14329
+ function buildSections(headings, totalLines) {
14330
+ if (headings.length === 0) return [];
14331
+ const sections = [];
14332
+ const stack = [];
14333
+ for (let i = 0; i < headings.length; i++) {
14334
+ const heading = headings[i];
14335
+ const nextHeading = headings[i + 1];
14336
+ const lineEnd = nextHeading ? nextHeading.line - 1 : totalLines;
14337
+ const section = {
14338
+ heading,
14339
+ line_start: heading.line,
14340
+ line_end: lineEnd,
14341
+ subsections: []
14342
+ };
14343
+ while (stack.length > 0 && stack[stack.length - 1].heading.level >= heading.level) {
14344
+ stack.pop();
14345
+ }
14346
+ if (stack.length === 0) {
14347
+ sections.push(section);
14348
+ } else {
14349
+ stack[stack.length - 1].subsections.push(section);
14350
+ }
14351
+ stack.push(section);
14352
+ }
14353
+ return sections;
14354
+ }
14355
+ async function getNoteStructure(index, notePath, vaultPath2) {
14356
+ const note = index.notes.get(notePath);
14357
+ if (!note) return null;
14358
+ const absolutePath = path17.join(vaultPath2, notePath);
14359
+ let content;
14360
+ try {
14361
+ content = await fs14.promises.readFile(absolutePath, "utf-8");
14362
+ } catch {
14363
+ return null;
14364
+ }
14365
+ const lines = content.split("\n");
14366
+ const headings = extractHeadings2(content);
14367
+ const sections = buildSections(headings, lines.length);
14368
+ const contentWithoutCode = content.replace(/```[\s\S]*?```/g, "");
14369
+ const words = contentWithoutCode.split(/\s+/).filter((w) => w.length > 0);
14370
+ return {
14371
+ path: notePath,
14372
+ headings,
14373
+ sections,
14374
+ word_count: words.length,
14375
+ line_count: lines.length
14376
+ };
14377
+ }
14378
+ async function getSectionContent(index, notePath, headingText, vaultPath2, includeSubheadings = true) {
14379
+ const note = index.notes.get(notePath);
14380
+ if (!note) return null;
14381
+ const absolutePath = path17.join(vaultPath2, notePath);
14382
+ let content;
14383
+ try {
14384
+ content = await fs14.promises.readFile(absolutePath, "utf-8");
14385
+ } catch {
14386
+ return null;
14387
+ }
14388
+ const lines = content.split("\n");
14389
+ const headings = extractHeadings2(content);
14390
+ const targetHeading = headings.find(
14391
+ (h) => h.text.toLowerCase() === headingText.toLowerCase()
14392
+ );
14393
+ if (!targetHeading) return null;
14394
+ let lineEnd = lines.length;
14395
+ for (const h of headings) {
14396
+ if (h.line > targetHeading.line) {
14397
+ if (includeSubheadings) {
14398
+ if (h.level <= targetHeading.level) {
14399
+ lineEnd = h.line - 1;
14400
+ break;
14401
+ }
14402
+ } else {
14403
+ lineEnd = h.line - 1;
14404
+ break;
14405
+ }
14406
+ }
14407
+ }
14408
+ const sectionLines = lines.slice(targetHeading.line, lineEnd);
14409
+ const sectionContent = sectionLines.join("\n").trim();
14410
+ return {
14411
+ heading: targetHeading.text,
14412
+ level: targetHeading.level,
14413
+ content: sectionContent,
14414
+ line_start: targetHeading.line,
14415
+ line_end: lineEnd
14416
+ };
14417
+ }
14418
+ async function findSections(index, headingPattern, vaultPath2, folder) {
14419
+ const regex = new RegExp(headingPattern, "i");
14420
+ const results = [];
14421
+ for (const note of index.notes.values()) {
14422
+ if (folder && !note.path.startsWith(folder)) continue;
14423
+ const absolutePath = path17.join(vaultPath2, note.path);
14424
+ let content;
14425
+ try {
14426
+ content = await fs14.promises.readFile(absolutePath, "utf-8");
14427
+ } catch {
14428
+ continue;
14429
+ }
14430
+ const headings = extractHeadings2(content);
14431
+ for (const heading of headings) {
14432
+ if (regex.test(heading.text)) {
14433
+ results.push({
14434
+ path: note.path,
14435
+ heading: heading.text,
14436
+ level: heading.level,
14437
+ line: heading.line
14438
+ });
14439
+ }
14440
+ }
14441
+ }
14442
+ return results;
14443
+ }
14444
+
14445
+ // src/tools/read/query.ts
14159
14446
  function applyGraphReranking(results, stateDb2) {
14160
14447
  if (!stateDb2) return;
14161
14448
  const cooccurrenceIndex2 = getCooccurrenceIndex();
@@ -14188,8 +14475,20 @@ function applyGraphReranking(results, stateDb2) {
14188
14475
  }
14189
14476
  function applySandwichOrdering(results) {
14190
14477
  if (results.length < 3) return;
14191
- const secondBest = results.splice(1, 1)[0];
14192
- results.push(secondBest);
14478
+ const n = results.length;
14479
+ const out = new Array(n);
14480
+ let front = 0;
14481
+ let back = n - 1;
14482
+ for (let i = 0; i < n; i++) {
14483
+ if (i % 2 === 0) {
14484
+ out[front++] = results[i];
14485
+ } else {
14486
+ out[back--] = results[i];
14487
+ }
14488
+ }
14489
+ for (let i = 0; i < n; i++) {
14490
+ results[i] = out[i];
14491
+ }
14193
14492
  }
14194
14493
  function applyEntityBridging(results, stateDb2, maxBridgesPerResult = 3) {
14195
14494
  if (!stateDb2 || results.length < 2) return;
@@ -14233,6 +14532,92 @@ function stripInternalFields(results) {
14233
14532
  for (const key of INTERNAL) delete r[key];
14234
14533
  }
14235
14534
  }
14535
+ function scoreAndRankMemories(memories, limit) {
14536
+ const now = Date.now();
14537
+ const scored = [];
14538
+ for (const m of memories) {
14539
+ const confidenceBoost = m.confidence * 5;
14540
+ let typeBoost = 0;
14541
+ switch (m.memory_type) {
14542
+ case "fact":
14543
+ typeBoost = 3;
14544
+ break;
14545
+ case "preference":
14546
+ typeBoost = 2;
14547
+ break;
14548
+ case "observation": {
14549
+ const ageDays = (now - m.updated_at) / 864e5;
14550
+ const recencyFactor = Math.max(0.2, 1 - ageDays / 7);
14551
+ typeBoost = 1 + 4 * recencyFactor;
14552
+ break;
14553
+ }
14554
+ case "summary":
14555
+ typeBoost = 1;
14556
+ break;
14557
+ }
14558
+ scored.push({ memory: m, score: confidenceBoost + typeBoost });
14559
+ }
14560
+ scored.sort((a, b) => b.score - a.score);
14561
+ return scored.slice(0, limit).map(({ memory: m }) => {
14562
+ const result = {
14563
+ key: m.key,
14564
+ value: m.value,
14565
+ type: m.memory_type
14566
+ };
14567
+ if (m.entity) result.entity = m.entity;
14568
+ if (m.entities_json) {
14569
+ try {
14570
+ const entities = JSON.parse(m.entities_json);
14571
+ if (Array.isArray(entities) && entities.length > 0) result.entities = entities;
14572
+ } catch {
14573
+ }
14574
+ }
14575
+ return result;
14576
+ });
14577
+ }
14578
+ async function buildEntitySection(entityResults, query, stateDb2, index, limit) {
14579
+ if (!stateDb2 || entityResults.length === 0 && query.length < 20) return [];
14580
+ const entityMap = /* @__PURE__ */ new Map();
14581
+ for (const e of entityResults) {
14582
+ entityMap.set(e.name.toLowerCase(), e);
14583
+ }
14584
+ if (query.length >= 20 && hasEntityEmbeddingsIndex()) {
14585
+ try {
14586
+ const embedding = await embedTextCached(query);
14587
+ const semanticMatches = findSemanticallySimilarEntities(embedding, limit);
14588
+ for (const match of semanticMatches) {
14589
+ if (match.similarity < 0.3) continue;
14590
+ const key = match.entityName.toLowerCase();
14591
+ if (!entityMap.has(key)) {
14592
+ entityMap.set(key, {
14593
+ id: 0,
14594
+ name: match.entityName,
14595
+ nameLower: key,
14596
+ path: "",
14597
+ category: "unknown",
14598
+ aliases: [],
14599
+ hubScore: 0,
14600
+ rank: 0
14601
+ });
14602
+ }
14603
+ }
14604
+ } catch {
14605
+ }
14606
+ }
14607
+ if (entityMap.size === 0) return [];
14608
+ const enriched = [];
14609
+ let count = 0;
14610
+ for (const [, entity] of entityMap) {
14611
+ if (count >= limit) break;
14612
+ enriched.push({
14613
+ name: entity.name,
14614
+ ...entity.description ? { description: entity.description } : {},
14615
+ ...enrichEntityCompact(entity.name, stateDb2, index)
14616
+ });
14617
+ count++;
14618
+ }
14619
+ return enriched;
14620
+ }
14236
14621
  async function enhanceSnippets(results, query, vaultPath2) {
14237
14622
  if (!hasEmbeddingsIndex()) return;
14238
14623
  const queryTokens = tokenize(query).map((t) => t.toLowerCase());
@@ -14256,6 +14641,29 @@ async function enhanceSnippets(results, query, vaultPath2) {
14256
14641
  }
14257
14642
  }
14258
14643
  }
14644
+ async function expandToSections(results, index, vaultPath2, maxExpand = 5, maxSectionChars = 2500) {
14645
+ const toExpand = results.slice(0, maxExpand);
14646
+ for (const r of toExpand) {
14647
+ const sectionHeading = r.section;
14648
+ const notePath = r.path;
14649
+ if (!sectionHeading || !notePath) continue;
14650
+ try {
14651
+ const section = await getSectionContent(index, notePath, sectionHeading, vaultPath2, true);
14652
+ if (!section || !section.content) continue;
14653
+ const heading = `## ${section.heading}`;
14654
+ let content = section.content;
14655
+ if (content.length > maxSectionChars) {
14656
+ const truncated = content.slice(0, maxSectionChars);
14657
+ const lastBreak = truncated.lastIndexOf("\n\n");
14658
+ content = (lastBreak > 0 ? truncated.slice(0, lastBreak) : truncated) + "\n\u2026";
14659
+ }
14660
+ r.section_content = `${heading}
14661
+
14662
+ ${content}`;
14663
+ } catch {
14664
+ }
14665
+ }
14666
+ }
14259
14667
  function matchesFrontmatter(note, where) {
14260
14668
  for (const [key, value] of Object.entries(where)) {
14261
14669
  const noteValue = note.frontmatter[key];
@@ -14326,7 +14734,7 @@ function sortNotes(notes, sortBy, order) {
14326
14734
  function registerQueryTools(server2, getIndex, getVaultPath, getStateDb3) {
14327
14735
  server2.tool(
14328
14736
  "search",
14329
- 'Search everything \u2014 notes, entities, and memories \u2014 in one call. Returns a decision surface with three sections: note results (with section provenance, dates, bridges, confidence), matching entity profiles, and relevant memories.\n\nNote results carry full metadata (frontmatter, scored backlinks/outlinks, snippets). Start with just a query, no filters. Narrow with filters only if needed. Use get_note_structure for full content, get_section_content to read one section.\n\nSearches note content (FTS5 + hybrid semantic), entity profiles (people, projects, technologies), and stored memories. Hybrid results included automatically when embeddings are built (via init_semantic).\n\nExample: search({ query: "quarterly review", limit: 5 })\nExample: search({ where: { type: "project", status: "active" } })\n\nMulti-vault: omitting `vault` searches all vaults and merges results. Pass `vault` to search a specific vault.',
14737
+ 'Search everything \u2014 notes, entities, and memories \u2014 in one call. Returns a decision surface with three sections: note results (with section provenance, full section content, dates, bridges, confidence), matching entity profiles, and relevant memories.\n\nTop note results carry full metadata (frontmatter, scored backlinks/outlinks, snippets) plus section_content \u2014 the complete ## section around the match (up to 2,500 chars). Start with just a query, no filters. Narrow with filters only if needed. Between snippet, section_content, and frontmatter, most questions can be answered without follow-up reads.\n\nSearches note content (FTS5 + hybrid semantic with contextual embeddings), entity profiles (people, projects, technologies), and stored memories. Hybrid results included automatically when embeddings are built (via init_semantic).\n\nExample: search({ query: "quarterly review", limit: 5 })\nExample: search({ where: { type: "project", status: "active" } })\n\nMulti-vault: omitting `vault` searches all vaults and merges results. Pass `vault` to search a specific vault.',
14330
14738
  {
14331
14739
  query: z5.string().optional().describe("Search query text. Required unless using metadata filters (where, has_tag, folder, etc.)"),
14332
14740
  // Metadata filters
@@ -14440,6 +14848,15 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb3) {
14440
14848
  } catch {
14441
14849
  }
14442
14850
  }
14851
+ let memoryResults = [];
14852
+ if (stateDbEntity) {
14853
+ try {
14854
+ const rawMemories = searchMemories(stateDbEntity, { query, limit });
14855
+ memoryResults = scoreAndRankMemories(rawMemories, limit);
14856
+ } catch {
14857
+ }
14858
+ }
14859
+ const entitySectionPromise = buildEntitySection(entityResults, query, stateDbEntity, index, limit);
14443
14860
  let edgeRanked = [];
14444
14861
  if (context_note) {
14445
14862
  const ctxStateDb = getStateDb3();
@@ -14532,12 +14949,16 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb3) {
14532
14949
  applyEntityBridging(results2, stateDb2);
14533
14950
  applySandwichOrdering(results2);
14534
14951
  await enhanceSnippets(results2, query, vaultPath2);
14952
+ await expandToSections(results2, index, vaultPath2, detailN);
14535
14953
  stripInternalFields(results2);
14954
+ const entitySection2 = await entitySectionPromise;
14536
14955
  return { content: [{ type: "text", text: JSON.stringify({
14537
14956
  method: "hybrid",
14538
14957
  query,
14539
14958
  total_results: filtered.length,
14540
- results: results2
14959
+ results: results2,
14960
+ ...entitySection2.length > 0 ? { entities: entitySection2 } : {},
14961
+ ...memoryResults.length > 0 ? { memories: memoryResults } : {}
14541
14962
  }, null, 2) }] };
14542
14963
  } catch (err) {
14543
14964
  console.error("[Semantic] Hybrid search failed, falling back to FTS5:", err instanceof Error ? err.message : err);
@@ -14574,12 +14995,16 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb3) {
14574
14995
  applyEntityBridging(results2, stateDb2);
14575
14996
  applySandwichOrdering(results2);
14576
14997
  await enhanceSnippets(results2, query, vaultPath2);
14998
+ await expandToSections(results2, index, vaultPath2, detailN);
14577
14999
  stripInternalFields(results2);
15000
+ const entitySection2 = await entitySectionPromise;
14578
15001
  return { content: [{ type: "text", text: JSON.stringify({
14579
15002
  method: "fts5",
14580
15003
  query,
14581
15004
  total_results: filtered.length,
14582
- results: results2
15005
+ results: results2,
15006
+ ...entitySection2.length > 0 ? { entities: entitySection2 } : {},
15007
+ ...memoryResults.length > 0 ? { memories: memoryResults } : {}
14583
15008
  }, null, 2) }] };
14584
15009
  }
14585
15010
  const stateDbFts = getStateDb3();
@@ -14595,12 +15020,16 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb3) {
14595
15020
  applyEntityBridging(results, stateDbFts);
14596
15021
  applySandwichOrdering(results);
14597
15022
  await enhanceSnippets(results, query, vaultPath2);
15023
+ await expandToSections(results, index, vaultPath2, detailN);
14598
15024
  stripInternalFields(results);
15025
+ const entitySection = await entitySectionPromise;
14599
15026
  return { content: [{ type: "text", text: JSON.stringify({
14600
15027
  method: "fts5",
14601
15028
  query,
14602
15029
  total_results: results.length,
14603
- results
15030
+ results,
15031
+ ...entitySection.length > 0 ? { entities: entitySection } : {},
15032
+ ...memoryResults.length > 0 ? { memories: memoryResults } : {}
14604
15033
  }, null, 2) }] };
14605
15034
  }
14606
15035
  return { content: [{ type: "text", text: JSON.stringify({ error: "Provide a query or metadata filters (where, has_tag, folder, etc.)" }, null, 2) }] };
@@ -14609,8 +15038,8 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb3) {
14609
15038
  }
14610
15039
 
14611
15040
  // src/tools/read/system.ts
14612
- import * as fs14 from "fs";
14613
- import * as path17 from "path";
15041
+ import * as fs15 from "fs";
15042
+ import * as path18 from "path";
14614
15043
  import { z as z6 } from "zod";
14615
15044
  import { scanVaultEntities as scanVaultEntities2, getEntityIndexFromDb as getEntityIndexFromDb2 } from "@velvetmonkey/vault-core";
14616
15045
 
@@ -14925,8 +15354,8 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
14925
15354
  continue;
14926
15355
  }
14927
15356
  try {
14928
- const fullPath = path17.join(vaultPath2, note.path);
14929
- const content = await fs14.promises.readFile(fullPath, "utf-8");
15357
+ const fullPath = path18.join(vaultPath2, note.path);
15358
+ const content = await fs15.promises.readFile(fullPath, "utf-8");
14930
15359
  const lines = content.split("\n");
14931
15360
  for (let i = 0; i < lines.length; i++) {
14932
15361
  const line = lines[i];
@@ -15183,151 +15612,6 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
15183
15612
 
15184
15613
  // src/tools/read/primitives.ts
15185
15614
  import { z as z7 } from "zod";
15186
-
15187
- // src/tools/read/structure.ts
15188
- import * as fs15 from "fs";
15189
- import * as path18 from "path";
15190
- var HEADING_REGEX2 = /^(#{1,6})\s+(.+)$/;
15191
- function extractHeadings2(content) {
15192
- const lines = content.split("\n");
15193
- const headings = [];
15194
- let inCodeBlock = false;
15195
- for (let i = 0; i < lines.length; i++) {
15196
- const line = lines[i];
15197
- if (line.startsWith("```")) {
15198
- inCodeBlock = !inCodeBlock;
15199
- continue;
15200
- }
15201
- if (inCodeBlock) continue;
15202
- const match = line.match(HEADING_REGEX2);
15203
- if (match) {
15204
- headings.push({
15205
- level: match[1].length,
15206
- text: match[2].trim(),
15207
- line: i + 1
15208
- // 1-indexed
15209
- });
15210
- }
15211
- }
15212
- return headings;
15213
- }
15214
- function buildSections(headings, totalLines) {
15215
- if (headings.length === 0) return [];
15216
- const sections = [];
15217
- const stack = [];
15218
- for (let i = 0; i < headings.length; i++) {
15219
- const heading = headings[i];
15220
- const nextHeading = headings[i + 1];
15221
- const lineEnd = nextHeading ? nextHeading.line - 1 : totalLines;
15222
- const section = {
15223
- heading,
15224
- line_start: heading.line,
15225
- line_end: lineEnd,
15226
- subsections: []
15227
- };
15228
- while (stack.length > 0 && stack[stack.length - 1].heading.level >= heading.level) {
15229
- stack.pop();
15230
- }
15231
- if (stack.length === 0) {
15232
- sections.push(section);
15233
- } else {
15234
- stack[stack.length - 1].subsections.push(section);
15235
- }
15236
- stack.push(section);
15237
- }
15238
- return sections;
15239
- }
15240
- async function getNoteStructure(index, notePath, vaultPath2) {
15241
- const note = index.notes.get(notePath);
15242
- if (!note) return null;
15243
- const absolutePath = path18.join(vaultPath2, notePath);
15244
- let content;
15245
- try {
15246
- content = await fs15.promises.readFile(absolutePath, "utf-8");
15247
- } catch {
15248
- return null;
15249
- }
15250
- const lines = content.split("\n");
15251
- const headings = extractHeadings2(content);
15252
- const sections = buildSections(headings, lines.length);
15253
- const contentWithoutCode = content.replace(/```[\s\S]*?```/g, "");
15254
- const words = contentWithoutCode.split(/\s+/).filter((w) => w.length > 0);
15255
- return {
15256
- path: notePath,
15257
- headings,
15258
- sections,
15259
- word_count: words.length,
15260
- line_count: lines.length
15261
- };
15262
- }
15263
- async function getSectionContent(index, notePath, headingText, vaultPath2, includeSubheadings = true) {
15264
- const note = index.notes.get(notePath);
15265
- if (!note) return null;
15266
- const absolutePath = path18.join(vaultPath2, notePath);
15267
- let content;
15268
- try {
15269
- content = await fs15.promises.readFile(absolutePath, "utf-8");
15270
- } catch {
15271
- return null;
15272
- }
15273
- const lines = content.split("\n");
15274
- const headings = extractHeadings2(content);
15275
- const targetHeading = headings.find(
15276
- (h) => h.text.toLowerCase() === headingText.toLowerCase()
15277
- );
15278
- if (!targetHeading) return null;
15279
- let lineEnd = lines.length;
15280
- for (const h of headings) {
15281
- if (h.line > targetHeading.line) {
15282
- if (includeSubheadings) {
15283
- if (h.level <= targetHeading.level) {
15284
- lineEnd = h.line - 1;
15285
- break;
15286
- }
15287
- } else {
15288
- lineEnd = h.line - 1;
15289
- break;
15290
- }
15291
- }
15292
- }
15293
- const sectionLines = lines.slice(targetHeading.line, lineEnd);
15294
- const sectionContent = sectionLines.join("\n").trim();
15295
- return {
15296
- heading: targetHeading.text,
15297
- level: targetHeading.level,
15298
- content: sectionContent,
15299
- line_start: targetHeading.line,
15300
- line_end: lineEnd
15301
- };
15302
- }
15303
- async function findSections(index, headingPattern, vaultPath2, folder) {
15304
- const regex = new RegExp(headingPattern, "i");
15305
- const results = [];
15306
- for (const note of index.notes.values()) {
15307
- if (folder && !note.path.startsWith(folder)) continue;
15308
- const absolutePath = path18.join(vaultPath2, note.path);
15309
- let content;
15310
- try {
15311
- content = await fs15.promises.readFile(absolutePath, "utf-8");
15312
- } catch {
15313
- continue;
15314
- }
15315
- const headings = extractHeadings2(content);
15316
- for (const heading of headings) {
15317
- if (regex.test(heading.text)) {
15318
- results.push({
15319
- path: note.path,
15320
- heading: heading.text,
15321
- level: heading.level,
15322
- line: heading.line
15323
- });
15324
- }
15325
- }
15326
- }
15327
- return results;
15328
- }
15329
-
15330
- // src/tools/read/primitives.ts
15331
15615
  import { getEntityByName as getEntityByName4 } from "@velvetmonkey/vault-core";
15332
15616
  function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = () => ({}), getStateDb3 = () => null) {
15333
15617
  server2.registerTool(
@@ -17771,14 +18055,40 @@ async function executeMutation(options, operation) {
17771
18055
  if (existsError) {
17772
18056
  return { success: false, result: existsError, filesWritten: [] };
17773
18057
  }
17774
- const { content, frontmatter, lineEnding, contentHash: contentHash2 } = await readVaultFile(vaultPath2, notePath);
18058
+ let { content, frontmatter, lineEnding, contentHash: contentHash2 } = await readVaultFile(vaultPath2, notePath);
17775
18059
  let sectionBoundary;
17776
18060
  if (section) {
17777
18061
  const sectionResult = await ensureSectionExists(content, section, notePath, vaultPath2);
17778
18062
  if ("error" in sectionResult) {
17779
- return { success: false, result: sectionResult.error, filesWritten: [] };
18063
+ if (options.autoCreateSection) {
18064
+ const sectionName = section.replace(/^#+\s*/, "").trim();
18065
+ const headings = extractHeadings3(content);
18066
+ let level = 1;
18067
+ if (headings.length > 0) {
18068
+ const topLevel = Math.min(...headings.map((h) => h.level));
18069
+ const topHeadings = headings.filter((h) => h.level === topLevel);
18070
+ level = topHeadings.length === 1 ? topLevel + 1 : topLevel;
18071
+ }
18072
+ const hashes = "#".repeat(level);
18073
+ const newSection = `
18074
+ ${hashes} ${sectionName}
18075
+ -
18076
+ `;
18077
+ content = content.trimEnd() + "\n" + newSection;
18078
+ await writeVaultFile(vaultPath2, notePath, content, frontmatter, lineEnding, contentHash2);
18079
+ ({ content, frontmatter, lineEnding, contentHash: contentHash2 } = await readVaultFile(vaultPath2, notePath));
18080
+ const retryResult = await ensureSectionExists(content, section, notePath, vaultPath2);
18081
+ if ("error" in retryResult) {
18082
+ return { success: false, result: retryResult.error, filesWritten: [] };
18083
+ }
18084
+ sectionBoundary = retryResult.boundary;
18085
+ console.error(`[Flywheel] Auto-created section "${sectionName}" (${hashes}) in ${notePath}`);
18086
+ } else {
18087
+ return { success: false, result: sectionResult.error, filesWritten: [] };
18088
+ }
18089
+ } else {
18090
+ sectionBoundary = sectionResult.boundary;
17780
18091
  }
17781
- sectionBoundary = sectionResult.boundary;
17782
18092
  }
17783
18093
  const ctx = { content, frontmatter, lineEnding, sectionBoundary, vaultPath: vaultPath2, notePath };
17784
18094
  const opResult = await operation(ctx);
@@ -17837,7 +18147,8 @@ async function withVaultFile(options, operation) {
17837
18147
  section: options.section,
17838
18148
  actionDescription: options.actionDescription,
17839
18149
  scoping: options.scoping,
17840
- dryRun: options.dryRun
18150
+ dryRun: options.dryRun,
18151
+ autoCreateSection: options.autoCreateSection
17841
18152
  }, operation);
17842
18153
  if (!outcome.success || options.dryRun) {
17843
18154
  return formatMcpResult(outcome.result);
@@ -17989,21 +18300,44 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
17989
18300
  const templates = config.templates || {};
17990
18301
  const filename = path25.basename(notePath, ".md").toLowerCase();
17991
18302
  let templatePath;
18303
+ let periodicType;
17992
18304
  const dailyPattern = /^\d{4}-\d{2}-\d{2}/;
17993
18305
  const weeklyPattern = /^\d{4}-W\d{2}/;
17994
18306
  const monthlyPattern = /^\d{4}-\d{2}$/;
17995
18307
  const quarterlyPattern = /^\d{4}-Q[1-4]$/;
17996
18308
  const yearlyPattern = /^\d{4}$/;
17997
- if (dailyPattern.test(filename) && templates.daily) {
18309
+ if (dailyPattern.test(filename)) {
17998
18310
  templatePath = templates.daily;
17999
- } else if (weeklyPattern.test(filename) && templates.weekly) {
18311
+ periodicType = "daily";
18312
+ } else if (weeklyPattern.test(filename)) {
18000
18313
  templatePath = templates.weekly;
18001
- } else if (monthlyPattern.test(filename) && templates.monthly) {
18314
+ periodicType = "weekly";
18315
+ } else if (monthlyPattern.test(filename)) {
18002
18316
  templatePath = templates.monthly;
18003
- } else if (quarterlyPattern.test(filename) && templates.quarterly) {
18317
+ periodicType = "monthly";
18318
+ } else if (quarterlyPattern.test(filename)) {
18004
18319
  templatePath = templates.quarterly;
18005
- } else if (yearlyPattern.test(filename) && templates.yearly) {
18320
+ periodicType = "quarterly";
18321
+ } else if (yearlyPattern.test(filename)) {
18006
18322
  templatePath = templates.yearly;
18323
+ periodicType = "yearly";
18324
+ }
18325
+ if (!templatePath && periodicType) {
18326
+ const candidates = [
18327
+ `templates/${periodicType}.md`,
18328
+ `templates/${periodicType[0].toUpperCase() + periodicType.slice(1)}.md`,
18329
+ `templates/${periodicType}-note.md`,
18330
+ `templates/${periodicType[0].toUpperCase() + periodicType.slice(1)} Note.md`
18331
+ ];
18332
+ for (const candidate of candidates) {
18333
+ try {
18334
+ await fs23.access(path25.join(vaultPath2, candidate));
18335
+ templatePath = candidate;
18336
+ console.error(`[Flywheel] Template not in config but found at ${candidate} \u2014 using it`);
18337
+ break;
18338
+ } catch {
18339
+ }
18340
+ }
18007
18341
  }
18008
18342
  let templateContent;
18009
18343
  if (templatePath) {
@@ -18011,6 +18345,7 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
18011
18345
  const absTemplatePath = path25.join(vaultPath2, templatePath);
18012
18346
  templateContent = await fs23.readFile(absTemplatePath, "utf-8");
18013
18347
  } catch {
18348
+ console.error(`[Flywheel] Template at ${templatePath} not readable, using minimal fallback`);
18014
18349
  const title = path25.basename(notePath, ".md");
18015
18350
  templateContent = `---
18016
18351
  ---
@@ -18020,6 +18355,9 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
18020
18355
  templatePath = void 0;
18021
18356
  }
18022
18357
  } else {
18358
+ if (periodicType) {
18359
+ console.error(`[Flywheel] No ${periodicType} template found in config or vault \u2014 using minimal fallback`);
18360
+ }
18023
18361
  const title = path25.basename(notePath, ".md");
18024
18362
  templateContent = `---
18025
18363
  ---
@@ -18104,7 +18442,8 @@ Example: vault_add_to_section({ path: "daily/2026-02-15.md", section: "Log", con
18104
18442
  section,
18105
18443
  actionDescription: "add content",
18106
18444
  scoping: agent_id || session_id ? { agent_id, session_id } : void 0,
18107
- dryRun: dry_run
18445
+ dryRun: dry_run,
18446
+ autoCreateSection: noteCreated
18108
18447
  },
18109
18448
  async (ctx) => {
18110
18449
  const validationResult = runValidationPipeline(content, format, {
@@ -21256,6 +21595,7 @@ function registerWikilinkFeedbackTools(server2, getStateDb3) {
21256
21595
  isError: true
21257
21596
  };
21258
21597
  }
21598
+ console.error(`[Flywheel] wikilink_feedback: stateDb path=${stateDb2.db.name}`);
21259
21599
  try {
21260
21600
  recordFeedback(stateDb2, entity, context || "", note_path || "", correct);
21261
21601
  } catch (e) {
@@ -21266,6 +21606,8 @@ function registerWikilinkFeedbackTools(server2, getStateDb3) {
21266
21606
  isError: true
21267
21607
  };
21268
21608
  }
21609
+ const rowCount = stateDb2.db.prepare("SELECT COUNT(*) as cnt FROM wikilink_feedback").get().cnt;
21610
+ console.error(`[Flywheel] wikilink_feedback: after insert, total rows=${rowCount}, db=${stateDb2.db.name}`);
21269
21611
  if (!correct && note_path && !skip_status_update) {
21270
21612
  stateDb2.db.prepare(
21271
21613
  `UPDATE wikilink_applications SET status = 'removed' WHERE entity = ? AND note_path = ? COLLATE NOCASE`
@@ -21282,7 +21624,8 @@ function registerWikilinkFeedbackTools(server2, getStateDb3) {
21282
21624
  correct,
21283
21625
  suppression_updated: suppressionUpdated
21284
21626
  },
21285
- total_suppressed: getSuppressedCount(stateDb2)
21627
+ total_suppressed: getSuppressedCount(stateDb2),
21628
+ total_feedback_rows: rowCount
21286
21629
  };
21287
21630
  break;
21288
21631
  }
@@ -24692,23 +25035,48 @@ function applyToolGating(targetServer, categories, getDb4, registry, getVaultPat
24692
25035
  } catch {
24693
25036
  }
24694
25037
  }
24695
- const merged = [];
25038
+ const mergedResults = [];
25039
+ const mergedEntities = [];
25040
+ const mergedMemories = [];
24696
25041
  const vaultsSearched = [];
24697
25042
  let query;
24698
25043
  for (const { vault, data } of perVault) {
24699
25044
  vaultsSearched.push(vault);
24700
25045
  if (data.query) query = data.query;
24701
25046
  if (data.error || data.building) continue;
24702
- const items = data.results || data.notes || data.entities || [];
25047
+ const items = data.results || data.notes || [];
24703
25048
  for (const item of items) {
24704
- merged.push({ vault, ...item });
25049
+ mergedResults.push({ vault, ...item });
25050
+ }
25051
+ if (Array.isArray(data.entities)) {
25052
+ for (const item of data.entities) {
25053
+ mergedEntities.push({ vault, ...item });
25054
+ }
25055
+ }
25056
+ if (Array.isArray(data.memories)) {
25057
+ for (const item of data.memories) {
25058
+ mergedMemories.push({ vault, ...item });
25059
+ }
24705
25060
  }
24706
25061
  }
24707
- if (merged.some((r) => r.rrf_score != null)) {
24708
- merged.sort((a, b) => (b.rrf_score ?? 0) - (a.rrf_score ?? 0));
25062
+ if (mergedResults.some((r) => r.rrf_score != null)) {
25063
+ mergedResults.sort((a, b) => (b.rrf_score ?? 0) - (a.rrf_score ?? 0));
24709
25064
  }
25065
+ const seenEntities = /* @__PURE__ */ new Set();
25066
+ const dedupedEntities = mergedEntities.filter((e) => {
25067
+ const key = (e.name || "").toLowerCase();
25068
+ if (seenEntities.has(key)) return false;
25069
+ seenEntities.add(key);
25070
+ return true;
25071
+ });
25072
+ const seenMemories = /* @__PURE__ */ new Set();
25073
+ const dedupedMemories = mergedMemories.filter((m) => {
25074
+ if (seenMemories.has(m.key)) return false;
25075
+ seenMemories.add(m.key);
25076
+ return true;
25077
+ });
24710
25078
  const limit = args[0]?.limit ?? 10;
24711
- const truncated = merged.slice(0, limit);
25079
+ const truncated = mergedResults.slice(0, limit);
24712
25080
  return {
24713
25081
  content: [{
24714
25082
  type: "text",
@@ -24716,9 +25084,11 @@ function applyToolGating(targetServer, categories, getDb4, registry, getVaultPat
24716
25084
  method: "cross_vault",
24717
25085
  query,
24718
25086
  vaults_searched: vaultsSearched,
24719
- total_results: merged.length,
25087
+ total_results: mergedResults.length,
24720
25088
  returned: truncated.length,
24721
- results: truncated
25089
+ results: truncated,
25090
+ ...dedupedEntities.length > 0 ? { entities: dedupedEntities.slice(0, limit) } : {},
25091
+ ...dedupedMemories.length > 0 ? { memories: dedupedMemories.slice(0, limit) } : {}
24722
25092
  }, null, 2)
24723
25093
  }]
24724
25094
  };
@@ -24950,7 +25320,10 @@ async function initializeVault(name, vaultPathArg) {
24950
25320
  indexState: "building",
24951
25321
  indexError: null,
24952
25322
  lastCooccurrenceRebuildAt: 0,
24953
- lastEdgeWeightRebuildAt: 0
25323
+ lastEdgeWeightRebuildAt: 0,
25324
+ lastEntityScanAt: 0,
25325
+ lastHubScoreRebuildAt: 0,
25326
+ lastIndexCacheSaveAt: 0
24954
25327
  };
24955
25328
  try {
24956
25329
  ctx.stateDb = openStateDb(vaultPathArg);