@velvetmonkey/flywheel-memory 2.0.27 → 2.0.29

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 (2) hide show
  1. package/dist/index.js +1557 -671
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -59,7 +59,7 @@ var init_constants = __esm({
59
59
 
60
60
  // src/core/write/writer.ts
61
61
  import fs18 from "fs/promises";
62
- import path17 from "path";
62
+ import path18 from "path";
63
63
  import matter5 from "gray-matter";
64
64
  function isSensitivePath(filePath) {
65
65
  const normalizedPath = filePath.replace(/\\/g, "/");
@@ -386,8 +386,8 @@ function validatePath(vaultPath2, notePath) {
386
386
  if (notePath.startsWith("\\")) {
387
387
  return false;
388
388
  }
389
- const resolvedVault = path17.resolve(vaultPath2);
390
- const resolvedNote = path17.resolve(vaultPath2, notePath);
389
+ const resolvedVault = path18.resolve(vaultPath2);
390
+ const resolvedNote = path18.resolve(vaultPath2, notePath);
391
391
  return resolvedNote.startsWith(resolvedVault);
392
392
  }
393
393
  async function validatePathSecure(vaultPath2, notePath) {
@@ -415,8 +415,8 @@ async function validatePathSecure(vaultPath2, notePath) {
415
415
  reason: "Path traversal not allowed"
416
416
  };
417
417
  }
418
- const resolvedVault = path17.resolve(vaultPath2);
419
- const resolvedNote = path17.resolve(vaultPath2, notePath);
418
+ const resolvedVault = path18.resolve(vaultPath2);
419
+ const resolvedNote = path18.resolve(vaultPath2, notePath);
420
420
  if (!resolvedNote.startsWith(resolvedVault)) {
421
421
  return {
422
422
  valid: false,
@@ -430,7 +430,7 @@ async function validatePathSecure(vaultPath2, notePath) {
430
430
  };
431
431
  }
432
432
  try {
433
- const fullPath = path17.join(vaultPath2, notePath);
433
+ const fullPath = path18.join(vaultPath2, notePath);
434
434
  try {
435
435
  await fs18.access(fullPath);
436
436
  const realPath = await fs18.realpath(fullPath);
@@ -441,7 +441,7 @@ async function validatePathSecure(vaultPath2, notePath) {
441
441
  reason: "Symlink target is outside vault"
442
442
  };
443
443
  }
444
- const relativePath = path17.relative(realVaultPath, realPath);
444
+ const relativePath = path18.relative(realVaultPath, realPath);
445
445
  if (isSensitivePath(relativePath)) {
446
446
  return {
447
447
  valid: false,
@@ -449,7 +449,7 @@ async function validatePathSecure(vaultPath2, notePath) {
449
449
  };
450
450
  }
451
451
  } catch {
452
- const parentDir = path17.dirname(fullPath);
452
+ const parentDir = path18.dirname(fullPath);
453
453
  try {
454
454
  await fs18.access(parentDir);
455
455
  const realParentPath = await fs18.realpath(parentDir);
@@ -475,8 +475,11 @@ async function readVaultFile(vaultPath2, notePath) {
475
475
  if (!validatePath(vaultPath2, notePath)) {
476
476
  throw new Error("Invalid path: path traversal not allowed");
477
477
  }
478
- const fullPath = path17.join(vaultPath2, notePath);
479
- const rawContent = await fs18.readFile(fullPath, "utf-8");
478
+ const fullPath = path18.join(vaultPath2, notePath);
479
+ const [rawContent, stat3] = await Promise.all([
480
+ fs18.readFile(fullPath, "utf-8"),
481
+ fs18.stat(fullPath)
482
+ ]);
480
483
  const lineEnding = detectLineEnding(rawContent);
481
484
  const normalizedContent = normalizeLineEndings(rawContent);
482
485
  const parsed = matter5(normalizedContent);
@@ -485,7 +488,8 @@ async function readVaultFile(vaultPath2, notePath) {
485
488
  content: parsed.content,
486
489
  frontmatter,
487
490
  rawContent,
488
- lineEnding
491
+ lineEnding,
492
+ mtimeMs: stat3.mtimeMs
489
493
  };
490
494
  }
491
495
  function deepCloneFrontmatter(obj) {
@@ -524,7 +528,7 @@ async function writeVaultFile(vaultPath2, notePath, content, frontmatter, lineEn
524
528
  if (!validation.valid) {
525
529
  throw new Error(`Invalid path: ${validation.reason}`);
526
530
  }
527
- const fullPath = path17.join(vaultPath2, notePath);
531
+ const fullPath = path18.join(vaultPath2, notePath);
528
532
  let output = matter5.stringify(content, frontmatter);
529
533
  output = normalizeTrailingNewline(output);
530
534
  output = convertLineEndings(output, lineEnding);
@@ -831,8 +835,8 @@ function createContext(variables = {}) {
831
835
  }
832
836
  };
833
837
  }
834
- function resolvePath(obj, path29) {
835
- const parts = path29.split(".");
838
+ function resolvePath(obj, path30) {
839
+ const parts = path30.split(".");
836
840
  let current = obj;
837
841
  for (const part of parts) {
838
842
  if (current === void 0 || current === null) {
@@ -984,7 +988,7 @@ __export(schema_exports, {
984
988
  validatePolicySchema: () => validatePolicySchema,
985
989
  validateVariables: () => validateVariables
986
990
  });
987
- import { z as z17 } from "zod";
991
+ import { z as z18 } from "zod";
988
992
  function validatePolicySchema(policy) {
989
993
  const errors = [];
990
994
  const warnings = [];
@@ -1179,13 +1183,13 @@ var PolicyVariableTypeSchema, PolicyVariableSchema, ConditionCheckTypeSchema, Po
1179
1183
  var init_schema = __esm({
1180
1184
  "src/core/write/policy/schema.ts"() {
1181
1185
  "use strict";
1182
- PolicyVariableTypeSchema = z17.enum(["string", "number", "boolean", "array", "enum"]);
1183
- PolicyVariableSchema = z17.object({
1186
+ PolicyVariableTypeSchema = z18.enum(["string", "number", "boolean", "array", "enum"]);
1187
+ PolicyVariableSchema = z18.object({
1184
1188
  type: PolicyVariableTypeSchema,
1185
- required: z17.boolean().optional(),
1186
- default: z17.union([z17.string(), z17.number(), z17.boolean(), z17.array(z17.string())]).optional(),
1187
- enum: z17.array(z17.string()).optional(),
1188
- description: z17.string().optional()
1189
+ required: z18.boolean().optional(),
1190
+ default: z18.union([z18.string(), z18.number(), z18.boolean(), z18.array(z18.string())]).optional(),
1191
+ enum: z18.array(z18.string()).optional(),
1192
+ description: z18.string().optional()
1189
1193
  }).refine(
1190
1194
  (data) => {
1191
1195
  if (data.type === "enum" && (!data.enum || data.enum.length === 0)) {
@@ -1195,7 +1199,7 @@ var init_schema = __esm({
1195
1199
  },
1196
1200
  { message: "Enum type requires a non-empty enum array" }
1197
1201
  );
1198
- ConditionCheckTypeSchema = z17.enum([
1202
+ ConditionCheckTypeSchema = z18.enum([
1199
1203
  "file_exists",
1200
1204
  "file_not_exists",
1201
1205
  "section_exists",
@@ -1204,13 +1208,13 @@ var init_schema = __esm({
1204
1208
  "frontmatter_exists",
1205
1209
  "frontmatter_not_exists"
1206
1210
  ]);
1207
- PolicyConditionSchema = z17.object({
1208
- id: z17.string().min(1, "Condition id is required"),
1211
+ PolicyConditionSchema = z18.object({
1212
+ id: z18.string().min(1, "Condition id is required"),
1209
1213
  check: ConditionCheckTypeSchema,
1210
- path: z17.string().optional(),
1211
- section: z17.string().optional(),
1212
- field: z17.string().optional(),
1213
- value: z17.union([z17.string(), z17.number(), z17.boolean()]).optional()
1214
+ path: z18.string().optional(),
1215
+ section: z18.string().optional(),
1216
+ field: z18.string().optional(),
1217
+ value: z18.union([z18.string(), z18.number(), z18.boolean()]).optional()
1214
1218
  }).refine(
1215
1219
  (data) => {
1216
1220
  if (["file_exists", "file_not_exists"].includes(data.check) && !data.path) {
@@ -1229,7 +1233,7 @@ var init_schema = __esm({
1229
1233
  },
1230
1234
  { message: "Condition is missing required fields for its check type" }
1231
1235
  );
1232
- PolicyToolNameSchema = z17.enum([
1236
+ PolicyToolNameSchema = z18.enum([
1233
1237
  "vault_add_to_section",
1234
1238
  "vault_remove_from_section",
1235
1239
  "vault_replace_in_section",
@@ -1240,24 +1244,24 @@ var init_schema = __esm({
1240
1244
  "vault_update_frontmatter",
1241
1245
  "vault_add_frontmatter_field"
1242
1246
  ]);
1243
- PolicyStepSchema = z17.object({
1244
- id: z17.string().min(1, "Step id is required"),
1247
+ PolicyStepSchema = z18.object({
1248
+ id: z18.string().min(1, "Step id is required"),
1245
1249
  tool: PolicyToolNameSchema,
1246
- when: z17.string().optional(),
1247
- params: z17.record(z17.unknown()),
1248
- description: z17.string().optional()
1250
+ when: z18.string().optional(),
1251
+ params: z18.record(z18.unknown()),
1252
+ description: z18.string().optional()
1249
1253
  });
1250
- PolicyOutputSchema = z17.object({
1251
- summary: z17.string().optional(),
1252
- files: z17.array(z17.string()).optional()
1254
+ PolicyOutputSchema = z18.object({
1255
+ summary: z18.string().optional(),
1256
+ files: z18.array(z18.string()).optional()
1253
1257
  });
1254
- PolicyDefinitionSchema = z17.object({
1255
- version: z17.literal("1.0"),
1256
- name: z17.string().min(1, "Policy name is required"),
1257
- description: z17.string().min(1, "Policy description is required"),
1258
- variables: z17.record(PolicyVariableSchema).optional(),
1259
- conditions: z17.array(PolicyConditionSchema).optional(),
1260
- steps: z17.array(PolicyStepSchema).min(1, "At least one step is required"),
1258
+ PolicyDefinitionSchema = z18.object({
1259
+ version: z18.literal("1.0"),
1260
+ name: z18.string().min(1, "Policy name is required"),
1261
+ description: z18.string().min(1, "Policy description is required"),
1262
+ variables: z18.record(PolicyVariableSchema).optional(),
1263
+ conditions: z18.array(PolicyConditionSchema).optional(),
1264
+ steps: z18.array(PolicyStepSchema).min(1, "At least one step is required"),
1261
1265
  output: PolicyOutputSchema.optional()
1262
1266
  });
1263
1267
  }
@@ -1270,8 +1274,8 @@ __export(conditions_exports, {
1270
1274
  evaluateCondition: () => evaluateCondition,
1271
1275
  shouldStepExecute: () => shouldStepExecute
1272
1276
  });
1273
- import fs24 from "fs/promises";
1274
- import path23 from "path";
1277
+ import fs25 from "fs/promises";
1278
+ import path24 from "path";
1275
1279
  async function evaluateCondition(condition, vaultPath2, context) {
1276
1280
  const interpolatedPath = condition.path ? interpolate(condition.path, context) : void 0;
1277
1281
  const interpolatedSection = condition.section ? interpolate(condition.section, context) : void 0;
@@ -1324,9 +1328,9 @@ async function evaluateCondition(condition, vaultPath2, context) {
1324
1328
  }
1325
1329
  }
1326
1330
  async function evaluateFileExists(vaultPath2, notePath, expectExists) {
1327
- const fullPath = path23.join(vaultPath2, notePath);
1331
+ const fullPath = path24.join(vaultPath2, notePath);
1328
1332
  try {
1329
- await fs24.access(fullPath);
1333
+ await fs25.access(fullPath);
1330
1334
  return {
1331
1335
  met: expectExists,
1332
1336
  reason: expectExists ? `File exists: ${notePath}` : `File exists (expected not to): ${notePath}`
@@ -1339,9 +1343,9 @@ async function evaluateFileExists(vaultPath2, notePath, expectExists) {
1339
1343
  }
1340
1344
  }
1341
1345
  async function evaluateSectionExists(vaultPath2, notePath, sectionName, expectExists) {
1342
- const fullPath = path23.join(vaultPath2, notePath);
1346
+ const fullPath = path24.join(vaultPath2, notePath);
1343
1347
  try {
1344
- await fs24.access(fullPath);
1348
+ await fs25.access(fullPath);
1345
1349
  } catch {
1346
1350
  return {
1347
1351
  met: !expectExists,
@@ -1370,9 +1374,9 @@ async function evaluateSectionExists(vaultPath2, notePath, sectionName, expectEx
1370
1374
  }
1371
1375
  }
1372
1376
  async function evaluateFrontmatterExists(vaultPath2, notePath, fieldName, expectExists) {
1373
- const fullPath = path23.join(vaultPath2, notePath);
1377
+ const fullPath = path24.join(vaultPath2, notePath);
1374
1378
  try {
1375
- await fs24.access(fullPath);
1379
+ await fs25.access(fullPath);
1376
1380
  } catch {
1377
1381
  return {
1378
1382
  met: !expectExists,
@@ -1401,9 +1405,9 @@ async function evaluateFrontmatterExists(vaultPath2, notePath, fieldName, expect
1401
1405
  }
1402
1406
  }
1403
1407
  async function evaluateFrontmatterEquals(vaultPath2, notePath, fieldName, expectedValue) {
1404
- const fullPath = path23.join(vaultPath2, notePath);
1408
+ const fullPath = path24.join(vaultPath2, notePath);
1405
1409
  try {
1406
- await fs24.access(fullPath);
1410
+ await fs25.access(fullPath);
1407
1411
  } catch {
1408
1412
  return {
1409
1413
  met: false,
@@ -1544,7 +1548,7 @@ var init_taskHelpers = __esm({
1544
1548
  });
1545
1549
 
1546
1550
  // src/index.ts
1547
- import * as path28 from "path";
1551
+ import * as path29 from "path";
1548
1552
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1549
1553
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1550
1554
 
@@ -1633,6 +1637,21 @@ function isBinaryContent(content) {
1633
1637
  const nonPrintable = sample.replace(/[\x20-\x7E\t\n\r]/g, "").length;
1634
1638
  return nonPrintable / sample.length > 0.1;
1635
1639
  }
1640
+ function parseFrontmatterDate(value) {
1641
+ if (value == null) return void 0;
1642
+ let date;
1643
+ if (value instanceof Date) {
1644
+ date = value;
1645
+ } else if (typeof value === "string" || typeof value === "number") {
1646
+ date = new Date(value);
1647
+ } else {
1648
+ return void 0;
1649
+ }
1650
+ if (isNaN(date.getTime())) return void 0;
1651
+ const year = date.getFullYear();
1652
+ if (year < 2e3 || date.getTime() > Date.now() + 864e5) return void 0;
1653
+ return date;
1654
+ }
1636
1655
  var WIKILINK_REGEX = /\[\[([^\]|#]+)(?:#[^\]|]*)?(?:\|([^\]]+))?\]\]/g;
1637
1656
  var TAG_REGEX = /(?:^|\s)#([a-zA-Z][a-zA-Z0-9_/-]*)/g;
1638
1657
  var CODE_BLOCK_REGEX = /```[\s\S]*?```|`[^`\n]+`/g;
@@ -1753,6 +1772,7 @@ async function parseNoteWithWarnings(file) {
1753
1772
  warnings.push(`Malformed frontmatter: ${err instanceof Error ? err.message : String(err)}`);
1754
1773
  }
1755
1774
  const title = file.path.replace(/\.md$/, "").split("/").pop() || file.path;
1775
+ const created = parseFrontmatterDate(frontmatter.created) ?? file.created;
1756
1776
  return {
1757
1777
  note: {
1758
1778
  path: file.path,
@@ -1762,7 +1782,7 @@ async function parseNoteWithWarnings(file) {
1762
1782
  outlinks: extractWikilinks(markdown),
1763
1783
  tags: extractTags(markdown, frontmatter),
1764
1784
  modified: file.modified,
1765
- created: file.created
1785
+ created
1766
1786
  },
1767
1787
  warnings,
1768
1788
  skipped: false
@@ -2189,8 +2209,8 @@ function updateIndexProgress(parsed, total) {
2189
2209
  function normalizeTarget(target) {
2190
2210
  return target.toLowerCase().replace(/\.md$/, "");
2191
2211
  }
2192
- function normalizeNotePath(path29) {
2193
- return path29.toLowerCase().replace(/\.md$/, "");
2212
+ function normalizeNotePath(path30) {
2213
+ return path30.toLowerCase().replace(/\.md$/, "");
2194
2214
  }
2195
2215
  async function buildVaultIndex(vaultPath2, options = {}) {
2196
2216
  const { timeoutMs = DEFAULT_TIMEOUT_MS, onProgress } = options;
@@ -2356,7 +2376,7 @@ function findSimilarEntity(index, target) {
2356
2376
  }
2357
2377
  const maxDist = normalizedLen <= 10 ? 1 : 2;
2358
2378
  let bestMatch;
2359
- for (const [entity, path29] of index.entities) {
2379
+ for (const [entity, path30] of index.entities) {
2360
2380
  const lenDiff = Math.abs(entity.length - normalizedLen);
2361
2381
  if (lenDiff > maxDist) {
2362
2382
  continue;
@@ -2364,7 +2384,7 @@ function findSimilarEntity(index, target) {
2364
2384
  const dist = levenshteinDistance(normalized, entity);
2365
2385
  if (dist > 0 && dist <= maxDist) {
2366
2386
  if (!bestMatch || dist < bestMatch.distance) {
2367
- bestMatch = { path: path29, entity, distance: dist };
2387
+ bestMatch = { path: path30, entity, distance: dist };
2368
2388
  if (dist === 1) {
2369
2389
  return bestMatch;
2370
2390
  }
@@ -2464,7 +2484,8 @@ import {
2464
2484
  saveFlywheelConfigToDb
2465
2485
  } from "@velvetmonkey/vault-core";
2466
2486
  var DEFAULT_CONFIG = {
2467
- exclude_task_tags: []
2487
+ exclude_task_tags: [],
2488
+ exclude_analysis_tags: []
2468
2489
  };
2469
2490
  function loadConfig(stateDb2) {
2470
2491
  if (stateDb2) {
@@ -2528,6 +2549,7 @@ function findMatchingFolder(folders, patterns) {
2528
2549
  function inferConfig(index, vaultPath2) {
2529
2550
  const inferred = {
2530
2551
  exclude_task_tags: [],
2552
+ exclude_analysis_tags: [],
2531
2553
  paths: {}
2532
2554
  };
2533
2555
  if (vaultPath2) {
@@ -2553,6 +2575,7 @@ function inferConfig(index, vaultPath2) {
2553
2575
  const lowerTag = tag.toLowerCase();
2554
2576
  if (RECURRING_TAG_PATTERNS.some((pattern) => lowerTag.includes(pattern))) {
2555
2577
  inferred.exclude_task_tags.push(tag);
2578
+ inferred.exclude_analysis_tags.push(tag);
2556
2579
  }
2557
2580
  }
2558
2581
  return inferred;
@@ -2826,30 +2849,30 @@ var EventQueue = class {
2826
2849
  * Add a new event to the queue
2827
2850
  */
2828
2851
  push(type, rawPath) {
2829
- const path29 = normalizePath(rawPath);
2852
+ const path30 = normalizePath(rawPath);
2830
2853
  const now = Date.now();
2831
2854
  const event = {
2832
2855
  type,
2833
- path: path29,
2856
+ path: path30,
2834
2857
  timestamp: now
2835
2858
  };
2836
- let pending = this.pending.get(path29);
2859
+ let pending = this.pending.get(path30);
2837
2860
  if (!pending) {
2838
2861
  pending = {
2839
2862
  events: [],
2840
2863
  timer: null,
2841
2864
  lastEvent: now
2842
2865
  };
2843
- this.pending.set(path29, pending);
2866
+ this.pending.set(path30, pending);
2844
2867
  }
2845
2868
  pending.events.push(event);
2846
2869
  pending.lastEvent = now;
2847
- console.error(`[flywheel] QUEUE: pushed ${type} for ${path29}, pending=${this.pending.size}`);
2870
+ console.error(`[flywheel] QUEUE: pushed ${type} for ${path30}, pending=${this.pending.size}`);
2848
2871
  if (pending.timer) {
2849
2872
  clearTimeout(pending.timer);
2850
2873
  }
2851
2874
  pending.timer = setTimeout(() => {
2852
- this.flushPath(path29);
2875
+ this.flushPath(path30);
2853
2876
  }, this.config.debounceMs);
2854
2877
  if (this.pending.size >= this.config.batchSize) {
2855
2878
  this.flush();
@@ -2870,10 +2893,10 @@ var EventQueue = class {
2870
2893
  /**
2871
2894
  * Flush a single path's events
2872
2895
  */
2873
- flushPath(path29) {
2874
- const pending = this.pending.get(path29);
2896
+ flushPath(path30) {
2897
+ const pending = this.pending.get(path30);
2875
2898
  if (!pending || pending.events.length === 0) return;
2876
- console.error(`[flywheel] QUEUE: flushing ${path29}, events=${pending.events.length}`);
2899
+ console.error(`[flywheel] QUEUE: flushing ${path30}, events=${pending.events.length}`);
2877
2900
  if (pending.timer) {
2878
2901
  clearTimeout(pending.timer);
2879
2902
  pending.timer = null;
@@ -2882,7 +2905,7 @@ var EventQueue = class {
2882
2905
  if (coalescedType) {
2883
2906
  const coalesced = {
2884
2907
  type: coalescedType,
2885
- path: path29,
2908
+ path: path30,
2886
2909
  originalEvents: [...pending.events]
2887
2910
  };
2888
2911
  this.onBatch({
@@ -2890,7 +2913,7 @@ var EventQueue = class {
2890
2913
  timestamp: Date.now()
2891
2914
  });
2892
2915
  }
2893
- this.pending.delete(path29);
2916
+ this.pending.delete(path30);
2894
2917
  }
2895
2918
  /**
2896
2919
  * Flush all pending events
@@ -2902,7 +2925,7 @@ var EventQueue = class {
2902
2925
  }
2903
2926
  if (this.pending.size === 0) return;
2904
2927
  const events = [];
2905
- for (const [path29, pending] of this.pending) {
2928
+ for (const [path30, pending] of this.pending) {
2906
2929
  if (pending.timer) {
2907
2930
  clearTimeout(pending.timer);
2908
2931
  }
@@ -2910,7 +2933,7 @@ var EventQueue = class {
2910
2933
  if (coalescedType) {
2911
2934
  events.push({
2912
2935
  type: coalescedType,
2913
- path: path29,
2936
+ path: path30,
2914
2937
  originalEvents: [...pending.events]
2915
2938
  });
2916
2939
  }
@@ -3059,31 +3082,31 @@ function createVaultWatcher(options) {
3059
3082
  usePolling: config.usePolling,
3060
3083
  interval: config.usePolling ? config.pollInterval : void 0
3061
3084
  });
3062
- watcher.on("add", (path29) => {
3063
- console.error(`[flywheel] RAW EVENT: add ${path29}`);
3064
- if (shouldWatch(path29, vaultPath2)) {
3065
- console.error(`[flywheel] ACCEPTED: add ${path29}`);
3066
- eventQueue.push("add", path29);
3085
+ watcher.on("add", (path30) => {
3086
+ console.error(`[flywheel] RAW EVENT: add ${path30}`);
3087
+ if (shouldWatch(path30, vaultPath2)) {
3088
+ console.error(`[flywheel] ACCEPTED: add ${path30}`);
3089
+ eventQueue.push("add", path30);
3067
3090
  } else {
3068
- console.error(`[flywheel] FILTERED: add ${path29}`);
3091
+ console.error(`[flywheel] FILTERED: add ${path30}`);
3069
3092
  }
3070
3093
  });
3071
- watcher.on("change", (path29) => {
3072
- console.error(`[flywheel] RAW EVENT: change ${path29}`);
3073
- if (shouldWatch(path29, vaultPath2)) {
3074
- console.error(`[flywheel] ACCEPTED: change ${path29}`);
3075
- eventQueue.push("change", path29);
3094
+ watcher.on("change", (path30) => {
3095
+ console.error(`[flywheel] RAW EVENT: change ${path30}`);
3096
+ if (shouldWatch(path30, vaultPath2)) {
3097
+ console.error(`[flywheel] ACCEPTED: change ${path30}`);
3098
+ eventQueue.push("change", path30);
3076
3099
  } else {
3077
- console.error(`[flywheel] FILTERED: change ${path29}`);
3100
+ console.error(`[flywheel] FILTERED: change ${path30}`);
3078
3101
  }
3079
3102
  });
3080
- watcher.on("unlink", (path29) => {
3081
- console.error(`[flywheel] RAW EVENT: unlink ${path29}`);
3082
- if (shouldWatch(path29, vaultPath2)) {
3083
- console.error(`[flywheel] ACCEPTED: unlink ${path29}`);
3084
- eventQueue.push("unlink", path29);
3103
+ watcher.on("unlink", (path30) => {
3104
+ console.error(`[flywheel] RAW EVENT: unlink ${path30}`);
3105
+ if (shouldWatch(path30, vaultPath2)) {
3106
+ console.error(`[flywheel] ACCEPTED: unlink ${path30}`);
3107
+ eventQueue.push("unlink", path30);
3085
3108
  } else {
3086
- console.error(`[flywheel] FILTERED: unlink ${path29}`);
3109
+ console.error(`[flywheel] FILTERED: unlink ${path30}`);
3087
3110
  }
3088
3111
  });
3089
3112
  watcher.on("ready", () => {
@@ -3326,6 +3349,11 @@ function getSuppressedCount(stateDb2) {
3326
3349
  ).get();
3327
3350
  return row.count;
3328
3351
  }
3352
+ function getSuppressedEntities(stateDb2) {
3353
+ return stateDb2.db.prepare(
3354
+ "SELECT entity, false_positive_rate FROM wikilink_suppressions ORDER BY false_positive_rate DESC"
3355
+ ).all();
3356
+ }
3329
3357
  function computeBoostFromAccuracy(accuracy, sampleCount) {
3330
3358
  if (sampleCount < FEEDBACK_BOOST_MIN_SAMPLES) return 0;
3331
3359
  for (const tier of FEEDBACK_BOOST_TIERS) {
@@ -3373,10 +3401,10 @@ function getAllFeedbackBoosts(stateDb2, folder) {
3373
3401
  for (const row of globalRows) {
3374
3402
  let accuracy;
3375
3403
  let sampleCount;
3376
- const fs29 = folderStats?.get(row.entity);
3377
- if (fs29 && fs29.count >= FEEDBACK_BOOST_MIN_SAMPLES) {
3378
- accuracy = fs29.accuracy;
3379
- sampleCount = fs29.count;
3404
+ const fs30 = folderStats?.get(row.entity);
3405
+ if (fs30 && fs30.count >= FEEDBACK_BOOST_MIN_SAMPLES) {
3406
+ accuracy = fs30.accuracy;
3407
+ sampleCount = fs30.count;
3380
3408
  } else {
3381
3409
  accuracy = row.correct_count / row.total;
3382
3410
  sampleCount = row.total;
@@ -3429,6 +3457,97 @@ function processImplicitFeedback(stateDb2, notePath, currentContent) {
3429
3457
  transaction();
3430
3458
  return removed;
3431
3459
  }
3460
+ var TIER_LABELS = [
3461
+ { label: "Champion (+5)", boost: 5, minAccuracy: 0.95, minSamples: 20 },
3462
+ { label: "Strong (+2)", boost: 2, minAccuracy: 0.8, minSamples: 5 },
3463
+ { label: "Neutral (0)", boost: 0, minAccuracy: 0.6, minSamples: 5 },
3464
+ { label: "Weak (-2)", boost: -2, minAccuracy: 0.4, minSamples: 5 },
3465
+ { label: "Poor (-4)", boost: -4, minAccuracy: 0, minSamples: 5 }
3466
+ ];
3467
+ function getDashboardData(stateDb2) {
3468
+ const entityStats = getEntityStats(stateDb2);
3469
+ const boostTiers = TIER_LABELS.map((t) => ({
3470
+ label: t.label,
3471
+ boost: t.boost,
3472
+ min_accuracy: t.minAccuracy,
3473
+ min_samples: t.minSamples,
3474
+ entities: []
3475
+ }));
3476
+ const learning = [];
3477
+ for (const es of entityStats) {
3478
+ if (es.total < FEEDBACK_BOOST_MIN_SAMPLES) {
3479
+ learning.push({ entity: es.entity, accuracy: es.accuracy, total: es.total });
3480
+ continue;
3481
+ }
3482
+ const boost = computeBoostFromAccuracy(es.accuracy, es.total);
3483
+ const tierIdx = boostTiers.findIndex((t) => t.boost === boost);
3484
+ if (tierIdx >= 0) {
3485
+ boostTiers[tierIdx].entities.push({ entity: es.entity, accuracy: es.accuracy, total: es.total });
3486
+ }
3487
+ }
3488
+ const sourceRows = stateDb2.db.prepare(`
3489
+ SELECT
3490
+ CASE WHEN context LIKE 'implicit:%' THEN 'implicit' ELSE 'explicit' END as source,
3491
+ COUNT(*) as count,
3492
+ SUM(CASE WHEN correct = 1 THEN 1 ELSE 0 END) as correct_count
3493
+ FROM wikilink_feedback
3494
+ GROUP BY source
3495
+ `).all();
3496
+ const feedbackSources = {
3497
+ explicit: { count: 0, correct: 0 },
3498
+ implicit: { count: 0, correct: 0 }
3499
+ };
3500
+ for (const row of sourceRows) {
3501
+ if (row.source === "implicit") {
3502
+ feedbackSources.implicit = { count: row.count, correct: row.correct_count };
3503
+ } else {
3504
+ feedbackSources.explicit = { count: row.count, correct: row.correct_count };
3505
+ }
3506
+ }
3507
+ const appRows = stateDb2.db.prepare(
3508
+ `SELECT status, COUNT(*) as count FROM wikilink_applications GROUP BY status`
3509
+ ).all();
3510
+ const applications = { applied: 0, removed: 0 };
3511
+ for (const row of appRows) {
3512
+ if (row.status === "applied") applications.applied = row.count;
3513
+ else if (row.status === "removed") applications.removed = row.count;
3514
+ }
3515
+ const recent = getFeedback(stateDb2, void 0, 50);
3516
+ const suppressed = getSuppressedEntities(stateDb2);
3517
+ const timeline = stateDb2.db.prepare(`
3518
+ SELECT
3519
+ date(created_at) as day,
3520
+ COUNT(*) as count,
3521
+ SUM(CASE WHEN correct = 1 THEN 1 ELSE 0 END) as correct_count,
3522
+ SUM(CASE WHEN correct = 0 THEN 1 ELSE 0 END) as incorrect_count
3523
+ FROM wikilink_feedback
3524
+ WHERE created_at >= datetime('now', '-30 days')
3525
+ GROUP BY day
3526
+ ORDER BY day
3527
+ `).all();
3528
+ const totalFeedback = feedbackSources.explicit.count + feedbackSources.implicit.count;
3529
+ const totalCorrect = feedbackSources.explicit.correct + feedbackSources.implicit.correct;
3530
+ const totalIncorrect = totalFeedback - totalCorrect;
3531
+ return {
3532
+ total_feedback: totalFeedback,
3533
+ total_correct: totalCorrect,
3534
+ total_incorrect: totalIncorrect,
3535
+ overall_accuracy: totalFeedback > 0 ? Math.round(totalCorrect / totalFeedback * 1e3) / 1e3 : 0,
3536
+ total_suppressed: suppressed.length,
3537
+ feedback_sources: feedbackSources,
3538
+ applications,
3539
+ boost_tiers: boostTiers,
3540
+ learning,
3541
+ suppressed,
3542
+ recent,
3543
+ timeline: timeline.map((t) => ({
3544
+ day: t.day,
3545
+ count: t.count,
3546
+ correct: t.correct_count,
3547
+ incorrect: t.incorrect_count
3548
+ }))
3549
+ };
3550
+ }
3432
3551
 
3433
3552
  // src/core/write/git.ts
3434
3553
  import { simpleGit, CheckRepoActions } from "simple-git";
@@ -5875,12 +5994,402 @@ function getFTS5State() {
5875
5994
  return { ...state };
5876
5995
  }
5877
5996
 
5878
- // src/index.ts
5879
- import { openStateDb, scanVaultEntities as scanVaultEntities3, getSessionId, getAllEntitiesFromDb as getAllEntitiesFromDb2 } from "@velvetmonkey/vault-core";
5997
+ // src/core/read/taskCache.ts
5998
+ import * as path10 from "path";
5880
5999
 
5881
- // src/tools/read/graph.ts
6000
+ // src/tools/read/tasks.ts
5882
6001
  import * as fs8 from "fs";
5883
6002
  import * as path9 from "path";
6003
+ var TASK_REGEX = /^(\s*)- \[([ xX\-])\]\s+(.+)$/;
6004
+ var TAG_REGEX2 = /#([a-zA-Z][a-zA-Z0-9_/-]*)/g;
6005
+ var DATE_REGEX = /\b(\d{4}-\d{2}-\d{2}|\d{1,2}\/\d{1,2}\/\d{2,4})\b/;
6006
+ var HEADING_REGEX = /^(#{1,6})\s+(.+)$/;
6007
+ function parseStatus(char) {
6008
+ if (char === " ") return "open";
6009
+ if (char === "-") return "cancelled";
6010
+ return "completed";
6011
+ }
6012
+ function extractTags2(text) {
6013
+ const tags = [];
6014
+ let match;
6015
+ TAG_REGEX2.lastIndex = 0;
6016
+ while ((match = TAG_REGEX2.exec(text)) !== null) {
6017
+ tags.push(match[1]);
6018
+ }
6019
+ return tags;
6020
+ }
6021
+ function extractDueDate(text) {
6022
+ const match = text.match(DATE_REGEX);
6023
+ return match ? match[1] : void 0;
6024
+ }
6025
+ async function extractTasksFromNote(notePath, absolutePath) {
6026
+ let content;
6027
+ try {
6028
+ content = await fs8.promises.readFile(absolutePath, "utf-8");
6029
+ } catch {
6030
+ return [];
6031
+ }
6032
+ const lines = content.split("\n");
6033
+ const tasks = [];
6034
+ let currentHeading;
6035
+ let inCodeBlock = false;
6036
+ for (let i = 0; i < lines.length; i++) {
6037
+ const line = lines[i];
6038
+ if (line.startsWith("```")) {
6039
+ inCodeBlock = !inCodeBlock;
6040
+ continue;
6041
+ }
6042
+ if (inCodeBlock) continue;
6043
+ const headingMatch = line.match(HEADING_REGEX);
6044
+ if (headingMatch) {
6045
+ currentHeading = headingMatch[2].trim();
6046
+ continue;
6047
+ }
6048
+ const taskMatch = line.match(TASK_REGEX);
6049
+ if (taskMatch) {
6050
+ const statusChar = taskMatch[2];
6051
+ const text = taskMatch[3].trim();
6052
+ tasks.push({
6053
+ path: notePath,
6054
+ line: i + 1,
6055
+ text,
6056
+ status: parseStatus(statusChar),
6057
+ raw: line,
6058
+ context: currentHeading,
6059
+ tags: extractTags2(text),
6060
+ due_date: extractDueDate(text)
6061
+ });
6062
+ }
6063
+ }
6064
+ return tasks;
6065
+ }
6066
+ async function getAllTasks(index, vaultPath2, options = {}) {
6067
+ const { status = "all", folder, tag, excludeTags = [], limit } = options;
6068
+ const allTasks = [];
6069
+ for (const note of index.notes.values()) {
6070
+ if (folder && !note.path.startsWith(folder)) continue;
6071
+ const absolutePath = path9.join(vaultPath2, note.path);
6072
+ const tasks = await extractTasksFromNote(note.path, absolutePath);
6073
+ allTasks.push(...tasks);
6074
+ }
6075
+ let filteredTasks = allTasks;
6076
+ if (status !== "all") {
6077
+ filteredTasks = allTasks.filter((t) => t.status === status);
6078
+ }
6079
+ if (tag) {
6080
+ filteredTasks = filteredTasks.filter((t) => t.tags.includes(tag));
6081
+ }
6082
+ if (excludeTags.length > 0) {
6083
+ filteredTasks = filteredTasks.filter(
6084
+ (t) => !excludeTags.some((excludeTag) => t.tags.includes(excludeTag))
6085
+ );
6086
+ }
6087
+ filteredTasks.sort((a, b) => {
6088
+ if (a.due_date && !b.due_date) return -1;
6089
+ if (!a.due_date && b.due_date) return 1;
6090
+ if (a.due_date && b.due_date) {
6091
+ const cmp = b.due_date.localeCompare(a.due_date);
6092
+ if (cmp !== 0) return cmp;
6093
+ }
6094
+ const noteA = index.notes.get(a.path);
6095
+ const noteB = index.notes.get(b.path);
6096
+ const mtimeA = noteA?.modified?.getTime() ?? 0;
6097
+ const mtimeB = noteB?.modified?.getTime() ?? 0;
6098
+ return mtimeB - mtimeA;
6099
+ });
6100
+ const openCount = allTasks.filter((t) => t.status === "open").length;
6101
+ const completedCount = allTasks.filter((t) => t.status === "completed").length;
6102
+ const cancelledCount = allTasks.filter((t) => t.status === "cancelled").length;
6103
+ const returnTasks = limit ? filteredTasks.slice(0, limit) : filteredTasks;
6104
+ return {
6105
+ total: allTasks.length,
6106
+ open_count: openCount,
6107
+ completed_count: completedCount,
6108
+ cancelled_count: cancelledCount,
6109
+ tasks: returnTasks
6110
+ };
6111
+ }
6112
+ async function getTasksFromNote(index, notePath, vaultPath2, excludeTags = []) {
6113
+ const note = index.notes.get(notePath);
6114
+ if (!note) return null;
6115
+ const absolutePath = path9.join(vaultPath2, notePath);
6116
+ let tasks = await extractTasksFromNote(notePath, absolutePath);
6117
+ if (excludeTags.length > 0) {
6118
+ tasks = tasks.filter(
6119
+ (t) => !excludeTags.some((excludeTag) => t.tags.includes(excludeTag))
6120
+ );
6121
+ }
6122
+ return tasks;
6123
+ }
6124
+ async function getTasksWithDueDates(index, vaultPath2, options = {}) {
6125
+ const { status = "open", folder, excludeTags } = options;
6126
+ const result = await getAllTasks(index, vaultPath2, { status, folder, excludeTags });
6127
+ return result.tasks.filter((t) => t.due_date).sort((a, b) => {
6128
+ const dateA = a.due_date || "";
6129
+ const dateB = b.due_date || "";
6130
+ return dateA.localeCompare(dateB);
6131
+ });
6132
+ }
6133
+
6134
+ // src/core/shared/serverLog.ts
6135
+ var MAX_ENTRIES = 200;
6136
+ var buffer = [];
6137
+ var serverStartTs = Date.now();
6138
+ function serverLog(component, message, level = "info") {
6139
+ const entry = {
6140
+ ts: Date.now(),
6141
+ component,
6142
+ message,
6143
+ level
6144
+ };
6145
+ buffer.push(entry);
6146
+ if (buffer.length > MAX_ENTRIES) {
6147
+ buffer.shift();
6148
+ }
6149
+ const prefix = level === "error" ? "[Memory] ERROR" : level === "warn" ? "[Memory] WARN" : "[Memory]";
6150
+ console.error(`${prefix} [${component}] ${message}`);
6151
+ }
6152
+ function getServerLog(options = {}) {
6153
+ const { since, component, limit = 100 } = options;
6154
+ let entries = buffer;
6155
+ if (since) {
6156
+ entries = entries.filter((e) => e.ts > since);
6157
+ }
6158
+ if (component) {
6159
+ entries = entries.filter((e) => e.component === component);
6160
+ }
6161
+ if (entries.length > limit) {
6162
+ entries = entries.slice(-limit);
6163
+ }
6164
+ return {
6165
+ entries,
6166
+ server_uptime_ms: Date.now() - serverStartTs
6167
+ };
6168
+ }
6169
+
6170
+ // src/core/read/taskCache.ts
6171
+ var db3 = null;
6172
+ var TASK_CACHE_STALE_MS = 30 * 60 * 1e3;
6173
+ var cacheReady = false;
6174
+ var rebuildInProgress = false;
6175
+ function setTaskCacheDatabase(database) {
6176
+ db3 = database;
6177
+ try {
6178
+ const row = db3.prepare(
6179
+ "SELECT value FROM fts_metadata WHERE key = ?"
6180
+ ).get("task_cache_built");
6181
+ if (row) {
6182
+ cacheReady = true;
6183
+ }
6184
+ } catch {
6185
+ }
6186
+ }
6187
+ function isTaskCacheReady() {
6188
+ return cacheReady && db3 !== null;
6189
+ }
6190
+ async function buildTaskCache(vaultPath2, index, excludeTags) {
6191
+ if (!db3) {
6192
+ throw new Error("Task cache database not initialized. Call setTaskCacheDatabase() first.");
6193
+ }
6194
+ if (rebuildInProgress) return;
6195
+ rebuildInProgress = true;
6196
+ const start = Date.now();
6197
+ try {
6198
+ const insertStmt = db3.prepare(`
6199
+ INSERT OR REPLACE INTO tasks (path, line, text, status, raw, context, tags_json, due_date)
6200
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
6201
+ `);
6202
+ const insertAll = db3.transaction(() => {
6203
+ db3.prepare("DELETE FROM tasks").run();
6204
+ let count = 0;
6205
+ const promises7 = [];
6206
+ const notePaths2 = [];
6207
+ for (const note of index.notes.values()) {
6208
+ notePaths2.push(note.path);
6209
+ }
6210
+ return { notePaths: notePaths2, insertStmt };
6211
+ });
6212
+ const { notePaths, insertStmt: stmt } = insertAll();
6213
+ let totalTasks = 0;
6214
+ for (const notePath of notePaths) {
6215
+ const absolutePath = path10.join(vaultPath2, notePath);
6216
+ const tasks = await extractTasksFromNote(notePath, absolutePath);
6217
+ if (tasks.length > 0) {
6218
+ const insertBatch = db3.transaction(() => {
6219
+ for (const task of tasks) {
6220
+ if (excludeTags?.length && excludeTags.some((t) => task.tags.includes(t))) {
6221
+ continue;
6222
+ }
6223
+ stmt.run(
6224
+ task.path,
6225
+ task.line,
6226
+ task.text,
6227
+ task.status,
6228
+ task.raw,
6229
+ task.context ?? null,
6230
+ task.tags.length > 0 ? JSON.stringify(task.tags) : null,
6231
+ task.due_date ?? null
6232
+ );
6233
+ totalTasks++;
6234
+ }
6235
+ });
6236
+ insertBatch();
6237
+ }
6238
+ }
6239
+ db3.prepare(
6240
+ "INSERT OR REPLACE INTO fts_metadata (key, value) VALUES (?, ?)"
6241
+ ).run("task_cache_built", (/* @__PURE__ */ new Date()).toISOString());
6242
+ cacheReady = true;
6243
+ const duration = Date.now() - start;
6244
+ serverLog("tasks", `Task cache built: ${totalTasks} tasks from ${notePaths.length} notes in ${duration}ms`);
6245
+ } finally {
6246
+ rebuildInProgress = false;
6247
+ }
6248
+ }
6249
+ async function updateTaskCacheForFile(vaultPath2, relativePath) {
6250
+ if (!db3) return;
6251
+ db3.prepare("DELETE FROM tasks WHERE path = ?").run(relativePath);
6252
+ const absolutePath = path10.join(vaultPath2, relativePath);
6253
+ const tasks = await extractTasksFromNote(relativePath, absolutePath);
6254
+ if (tasks.length > 0) {
6255
+ const insertStmt = db3.prepare(`
6256
+ INSERT OR REPLACE INTO tasks (path, line, text, status, raw, context, tags_json, due_date)
6257
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
6258
+ `);
6259
+ const insertBatch = db3.transaction(() => {
6260
+ for (const task of tasks) {
6261
+ insertStmt.run(
6262
+ task.path,
6263
+ task.line,
6264
+ task.text,
6265
+ task.status,
6266
+ task.raw,
6267
+ task.context ?? null,
6268
+ task.tags.length > 0 ? JSON.stringify(task.tags) : null,
6269
+ task.due_date ?? null
6270
+ );
6271
+ }
6272
+ });
6273
+ insertBatch();
6274
+ }
6275
+ }
6276
+ function removeTaskCacheForFile(relativePath) {
6277
+ if (!db3) return;
6278
+ db3.prepare("DELETE FROM tasks WHERE path = ?").run(relativePath);
6279
+ }
6280
+ function queryTasksFromCache(options) {
6281
+ if (!db3) {
6282
+ throw new Error("Task cache database not initialized.");
6283
+ }
6284
+ const { status = "all", folder, tag, excludeTags = [], has_due_date, limit, offset = 0 } = options;
6285
+ const conditions = [];
6286
+ const params = [];
6287
+ if (status !== "all") {
6288
+ conditions.push("status = ?");
6289
+ params.push(status);
6290
+ }
6291
+ if (folder) {
6292
+ conditions.push("path LIKE ?");
6293
+ params.push(folder + "%");
6294
+ }
6295
+ if (has_due_date) {
6296
+ conditions.push("due_date IS NOT NULL");
6297
+ }
6298
+ if (tag) {
6299
+ conditions.push("EXISTS (SELECT 1 FROM json_each(tags_json) WHERE value = ?)");
6300
+ params.push(tag);
6301
+ }
6302
+ if (excludeTags.length > 0) {
6303
+ const placeholders = excludeTags.map(() => "?").join(", ");
6304
+ conditions.push(`NOT EXISTS (SELECT 1 FROM json_each(tags_json) WHERE value IN (${placeholders}))`);
6305
+ params.push(...excludeTags);
6306
+ }
6307
+ const whereClause = conditions.length > 0 ? "WHERE " + conditions.join(" AND ") : "";
6308
+ const countConditions = [];
6309
+ const countParams = [];
6310
+ if (folder) {
6311
+ countConditions.push("path LIKE ?");
6312
+ countParams.push(folder + "%");
6313
+ }
6314
+ if (excludeTags.length > 0) {
6315
+ const placeholders = excludeTags.map(() => "?").join(", ");
6316
+ countConditions.push(`NOT EXISTS (SELECT 1 FROM json_each(tags_json) WHERE value IN (${placeholders}))`);
6317
+ countParams.push(...excludeTags);
6318
+ }
6319
+ const countWhere = countConditions.length > 0 ? "WHERE " + countConditions.join(" AND ") : "";
6320
+ const countRows = db3.prepare(
6321
+ `SELECT status, COUNT(*) as cnt FROM tasks ${countWhere} GROUP BY status`
6322
+ ).all(...countParams);
6323
+ let openCount = 0;
6324
+ let completedCount = 0;
6325
+ let cancelledCount = 0;
6326
+ let total = 0;
6327
+ for (const row of countRows) {
6328
+ total += row.cnt;
6329
+ if (row.status === "open") openCount = row.cnt;
6330
+ else if (row.status === "completed") completedCount = row.cnt;
6331
+ else if (row.status === "cancelled") cancelledCount = row.cnt;
6332
+ }
6333
+ let orderBy;
6334
+ if (has_due_date) {
6335
+ orderBy = "ORDER BY due_date ASC, path";
6336
+ } else {
6337
+ orderBy = "ORDER BY CASE WHEN due_date IS NOT NULL THEN 0 ELSE 1 END, due_date DESC, path";
6338
+ }
6339
+ let limitClause = "";
6340
+ const queryParams = [...params];
6341
+ if (limit !== void 0) {
6342
+ limitClause = " LIMIT ? OFFSET ?";
6343
+ queryParams.push(limit, offset);
6344
+ }
6345
+ const rows = db3.prepare(
6346
+ `SELECT path, line, text, status, raw, context, tags_json, due_date FROM tasks ${whereClause} ${orderBy}${limitClause}`
6347
+ ).all(...queryParams);
6348
+ const tasks = rows.map((row) => ({
6349
+ path: row.path,
6350
+ line: row.line,
6351
+ text: row.text,
6352
+ status: row.status,
6353
+ raw: row.raw,
6354
+ context: row.context ?? void 0,
6355
+ tags: row.tags_json ? JSON.parse(row.tags_json) : [],
6356
+ due_date: row.due_date ?? void 0
6357
+ }));
6358
+ return {
6359
+ total,
6360
+ open_count: openCount,
6361
+ completed_count: completedCount,
6362
+ cancelled_count: cancelledCount,
6363
+ tasks
6364
+ };
6365
+ }
6366
+ function isTaskCacheStale() {
6367
+ if (!db3) return true;
6368
+ try {
6369
+ const row = db3.prepare(
6370
+ "SELECT value FROM fts_metadata WHERE key = ?"
6371
+ ).get("task_cache_built");
6372
+ if (!row) return true;
6373
+ const builtAt = new Date(row.value).getTime();
6374
+ const age = Date.now() - builtAt;
6375
+ return age > TASK_CACHE_STALE_MS;
6376
+ } catch {
6377
+ return true;
6378
+ }
6379
+ }
6380
+ function refreshIfStale(vaultPath2, index, excludeTags) {
6381
+ if (!isTaskCacheStale() || rebuildInProgress) return;
6382
+ buildTaskCache(vaultPath2, index, excludeTags).catch((err) => {
6383
+ serverLog("tasks", `Task cache background refresh failed: ${err instanceof Error ? err.message : err}`, "error");
6384
+ });
6385
+ }
6386
+
6387
+ // src/index.ts
6388
+ import { openStateDb, scanVaultEntities as scanVaultEntities3, getSessionId, getAllEntitiesFromDb as getAllEntitiesFromDb3 } from "@velvetmonkey/vault-core";
6389
+
6390
+ // src/tools/read/graph.ts
6391
+ import * as fs9 from "fs";
6392
+ import * as path11 from "path";
5884
6393
  import { z } from "zod";
5885
6394
 
5886
6395
  // src/core/read/constants.ts
@@ -6164,8 +6673,8 @@ function requireIndex() {
6164
6673
  // src/tools/read/graph.ts
6165
6674
  async function getContext(vaultPath2, sourcePath, line, contextLines = 1) {
6166
6675
  try {
6167
- const fullPath = path9.join(vaultPath2, sourcePath);
6168
- const content = await fs8.promises.readFile(fullPath, "utf-8");
6676
+ const fullPath = path11.join(vaultPath2, sourcePath);
6677
+ const content = await fs9.promises.readFile(fullPath, "utf-8");
6169
6678
  const lines = content.split("\n");
6170
6679
  const startLine = Math.max(0, line - 1 - contextLines);
6171
6680
  const endLine = Math.min(lines.length, line + contextLines);
@@ -6468,14 +6977,14 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
6468
6977
  };
6469
6978
  function findSimilarEntity2(target, entities) {
6470
6979
  const targetLower = target.toLowerCase();
6471
- for (const [name, path29] of entities) {
6980
+ for (const [name, path30] of entities) {
6472
6981
  if (name.startsWith(targetLower) || targetLower.startsWith(name)) {
6473
- return path29;
6982
+ return path30;
6474
6983
  }
6475
6984
  }
6476
- for (const [name, path29] of entities) {
6985
+ for (const [name, path30] of entities) {
6477
6986
  if (name.includes(targetLower) || targetLower.includes(name)) {
6478
- return path29;
6987
+ return path30;
6479
6988
  }
6480
6989
  }
6481
6990
  return void 0;
@@ -6557,7 +7066,7 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
6557
7066
  }
6558
7067
 
6559
7068
  // src/tools/read/health.ts
6560
- import * as fs9 from "fs";
7069
+ import * as fs10 from "fs";
6561
7070
  import { z as z3 } from "zod";
6562
7071
 
6563
7072
  // src/tools/read/periodic.ts
@@ -6897,6 +7406,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
6897
7406
  note_count: z3.coerce.number().describe("Number of notes in the index"),
6898
7407
  entity_count: z3.coerce.number().describe("Number of linkable entities (titles + aliases)"),
6899
7408
  tag_count: z3.coerce.number().describe("Number of unique tags"),
7409
+ link_count: z3.coerce.number().describe("Total number of outgoing wikilinks"),
6900
7410
  periodic_notes: z3.array(PeriodicNoteInfoSchema).optional().describe("Detected periodic note conventions"),
6901
7411
  config: z3.record(z3.unknown()).optional().describe("Current flywheel config (paths, templates, etc.)"),
6902
7412
  last_rebuild: z3.object({
@@ -6929,7 +7439,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
6929
7439
  const indexErrorObj = getIndexError();
6930
7440
  let vaultAccessible = false;
6931
7441
  try {
6932
- fs9.accessSync(vaultPath2, fs9.constants.R_OK);
7442
+ fs10.accessSync(vaultPath2, fs10.constants.R_OK);
6933
7443
  vaultAccessible = true;
6934
7444
  } catch {
6935
7445
  vaultAccessible = false;
@@ -6950,6 +7460,10 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
6950
7460
  const noteCount = indexBuilt ? index.notes.size : 0;
6951
7461
  const entityCount = indexBuilt ? index.entities.size : 0;
6952
7462
  const tagCount = indexBuilt ? index.tags.size : 0;
7463
+ let linkCount = 0;
7464
+ if (indexBuilt) {
7465
+ for (const note of index.notes.values()) linkCount += note.outlinks.length;
7466
+ }
6953
7467
  if (indexBuilt && noteCount === 0 && vaultAccessible) {
6954
7468
  recommendations.push("No notes found in vault. Is PROJECT_PATH pointing to a markdown vault?");
6955
7469
  }
@@ -7010,6 +7524,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
7010
7524
  note_count: noteCount,
7011
7525
  entity_count: entityCount,
7012
7526
  tag_count: tagCount,
7527
+ link_count: linkCount,
7013
7528
  periodic_notes: periodicNotes && periodicNotes.length > 0 ? periodicNotes : void 0,
7014
7529
  config: configInfo,
7015
7530
  last_rebuild: lastRebuild,
@@ -7067,8 +7582,8 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
7067
7582
  daily_counts: z3.record(z3.number())
7068
7583
  }).describe("Activity summary for the last 7 days")
7069
7584
  };
7070
- function isPeriodicNote(path29) {
7071
- const filename = path29.split("/").pop() || "";
7585
+ function isPeriodicNote2(path30) {
7586
+ const filename = path30.split("/").pop() || "";
7072
7587
  const nameWithoutExt = filename.replace(/\.md$/, "");
7073
7588
  const patterns = [
7074
7589
  /^\d{4}-\d{2}-\d{2}$/,
@@ -7083,7 +7598,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
7083
7598
  // YYYY (yearly)
7084
7599
  ];
7085
7600
  const periodicFolders = ["daily", "weekly", "monthly", "quarterly", "yearly", "journal", "journals"];
7086
- const folder = path29.split("/")[0]?.toLowerCase() || "";
7601
+ const folder = path30.split("/")[0]?.toLowerCase() || "";
7087
7602
  return patterns.some((p) => p.test(nameWithoutExt)) || periodicFolders.includes(folder);
7088
7603
  }
7089
7604
  server2.registerTool(
@@ -7117,7 +7632,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
7117
7632
  const backlinks = getBacklinksForNote(index, note.path);
7118
7633
  if (backlinks.length === 0) {
7119
7634
  orphanTotal++;
7120
- if (isPeriodicNote(note.path)) {
7635
+ if (isPeriodicNote2(note.path)) {
7121
7636
  orphanPeriodic++;
7122
7637
  } else {
7123
7638
  orphanContent++;
@@ -7174,6 +7689,45 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
7174
7689
  };
7175
7690
  }
7176
7691
  );
7692
+ const LogEntrySchema = z3.object({
7693
+ ts: z3.number().describe("Unix timestamp (ms)"),
7694
+ component: z3.string().describe("Source component"),
7695
+ message: z3.string().describe("Log message"),
7696
+ level: z3.enum(["info", "warn", "error"]).describe("Log level")
7697
+ });
7698
+ const ServerLogOutputSchema = {
7699
+ entries: z3.array(LogEntrySchema).describe("Log entries (oldest first)"),
7700
+ server_uptime_ms: z3.coerce.number().describe("Server uptime in milliseconds")
7701
+ };
7702
+ server2.registerTool(
7703
+ "server_log",
7704
+ {
7705
+ title: "Server Activity Log",
7706
+ description: "Query the server activity log. Returns timestamped entries for startup stages, indexing progress, errors, and runtime events. Useful for diagnosing startup issues or checking what the server has been doing.",
7707
+ inputSchema: {
7708
+ since: z3.coerce.number().optional().describe("Only return entries after this Unix timestamp (ms)"),
7709
+ component: z3.string().optional().describe("Filter by component (server, index, fts5, semantic, tasks, watcher, statedb, config)"),
7710
+ limit: z3.coerce.number().optional().describe("Max entries to return (default 100)")
7711
+ },
7712
+ outputSchema: ServerLogOutputSchema
7713
+ },
7714
+ async (params) => {
7715
+ const result = getServerLog({
7716
+ since: params.since,
7717
+ component: params.component,
7718
+ limit: params.limit
7719
+ });
7720
+ return {
7721
+ content: [
7722
+ {
7723
+ type: "text",
7724
+ text: JSON.stringify(result, null, 2)
7725
+ }
7726
+ ],
7727
+ structuredContent: result
7728
+ };
7729
+ }
7730
+ );
7177
7731
  }
7178
7732
 
7179
7733
  // src/tools/read/query.ts
@@ -7451,10 +8005,68 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
7451
8005
  }
7452
8006
 
7453
8007
  // src/tools/read/system.ts
7454
- import * as fs10 from "fs";
7455
- import * as path10 from "path";
8008
+ import * as fs11 from "fs";
8009
+ import * as path12 from "path";
7456
8010
  import { z as z5 } from "zod";
7457
8011
  import { scanVaultEntities as scanVaultEntities2, getEntityIndexFromDb as getEntityIndexFromDb2 } from "@velvetmonkey/vault-core";
8012
+
8013
+ // src/core/read/aliasSuggestions.ts
8014
+ function generateAliasCandidates(entityName, existingAliases) {
8015
+ const existing = new Set(existingAliases.map((a) => a.toLowerCase()));
8016
+ const candidates = [];
8017
+ const words = entityName.split(/[\s-]+/).filter((w) => w.length > 0);
8018
+ if (words.length >= 2) {
8019
+ const acronym = words.map((w) => w[0]).join("").toUpperCase();
8020
+ if (acronym.length >= 2 && acronym.length <= 6 && !existing.has(acronym.toLowerCase())) {
8021
+ candidates.push({ candidate: acronym, type: "acronym" });
8022
+ }
8023
+ if (words.length >= 3) {
8024
+ const short = words[0];
8025
+ if (short.length >= 3 && !existing.has(short.toLowerCase())) {
8026
+ candidates.push({ candidate: short, type: "short_form" });
8027
+ }
8028
+ }
8029
+ }
8030
+ return candidates;
8031
+ }
8032
+ function suggestEntityAliases(stateDb2, folder) {
8033
+ const db4 = stateDb2.db;
8034
+ const entities = folder ? db4.prepare(
8035
+ "SELECT name, path, aliases_json FROM entities WHERE path LIKE ? || '/%'"
8036
+ ).all(folder) : db4.prepare("SELECT name, path, aliases_json FROM entities").all();
8037
+ const allEntityNames = new Set(
8038
+ db4.prepare("SELECT name_lower FROM entities").all().map((r) => r.name_lower)
8039
+ );
8040
+ const suggestions = [];
8041
+ const countStmt = db4.prepare(
8042
+ "SELECT COUNT(*) as cnt FROM notes_fts WHERE content MATCH ?"
8043
+ );
8044
+ for (const row of entities) {
8045
+ const aliases = row.aliases_json ? JSON.parse(row.aliases_json) : [];
8046
+ const candidates = generateAliasCandidates(row.name, aliases);
8047
+ for (const { candidate, type } of candidates) {
8048
+ if (allEntityNames.has(candidate.toLowerCase())) continue;
8049
+ let mentions = 0;
8050
+ try {
8051
+ const result = countStmt.get(`"${candidate}"`);
8052
+ mentions = result?.cnt ?? 0;
8053
+ } catch {
8054
+ }
8055
+ suggestions.push({
8056
+ entity: row.name,
8057
+ entity_path: row.path,
8058
+ current_aliases: aliases,
8059
+ candidate,
8060
+ type,
8061
+ mentions
8062
+ });
8063
+ }
8064
+ }
8065
+ suggestions.sort((a, b) => b.mentions - a.mentions || a.entity.localeCompare(b.entity));
8066
+ return suggestions;
8067
+ }
8068
+
8069
+ // src/tools/read/system.ts
7458
8070
  function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfig, getStateDb) {
7459
8071
  const RefreshIndexOutputSchema = {
7460
8072
  success: z5.boolean().describe("Whether the refresh succeeded"),
@@ -7685,8 +8297,8 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
7685
8297
  continue;
7686
8298
  }
7687
8299
  try {
7688
- const fullPath = path10.join(vaultPath2, note.path);
7689
- const content = await fs10.promises.readFile(fullPath, "utf-8");
8300
+ const fullPath = path12.join(vaultPath2, note.path);
8301
+ const content = await fs11.promises.readFile(fullPath, "utf-8");
7690
8302
  const lines = content.split("\n");
7691
8303
  for (let i = 0; i < lines.length; i++) {
7692
8304
  const line = lines[i];
@@ -7801,8 +8413,8 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
7801
8413
  let wordCount;
7802
8414
  if (include_word_count) {
7803
8415
  try {
7804
- const fullPath = path10.join(vaultPath2, resolvedPath);
7805
- const content = await fs10.promises.readFile(fullPath, "utf-8");
8416
+ const fullPath = path12.join(vaultPath2, resolvedPath);
8417
+ const content = await fs11.promises.readFile(fullPath, "utf-8");
7806
8418
  wordCount = content.split(/\s+/).filter((w) => w.length > 0).length;
7807
8419
  } catch {
7808
8420
  }
@@ -7941,15 +8553,44 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
7941
8553
  };
7942
8554
  }
7943
8555
  );
8556
+ server2.registerTool(
8557
+ "suggest_entity_aliases",
8558
+ {
8559
+ title: "Suggest Entity Aliases",
8560
+ description: "Generate alias suggestions for entities in a folder based on acronyms and short forms, validated against vault content.",
8561
+ inputSchema: {
8562
+ folder: z5.string().optional().describe("Folder path to scope suggestions to"),
8563
+ limit: z5.number().default(20).describe("Max suggestions to return")
8564
+ }
8565
+ },
8566
+ async ({
8567
+ folder,
8568
+ limit: requestedLimit
8569
+ }) => {
8570
+ const stateDb2 = getStateDb?.();
8571
+ if (!stateDb2) {
8572
+ return {
8573
+ content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
8574
+ };
8575
+ }
8576
+ const suggestions = suggestEntityAliases(stateDb2, folder || void 0);
8577
+ const limit = Math.min(requestedLimit ?? 20, 50);
8578
+ const limited = suggestions.slice(0, limit);
8579
+ const output = { suggestion_count: limited.length, suggestions: limited };
8580
+ return {
8581
+ content: [{ type: "text", text: JSON.stringify(output, null, 2) }]
8582
+ };
8583
+ }
8584
+ );
7944
8585
  }
7945
8586
 
7946
8587
  // src/tools/read/primitives.ts
7947
8588
  import { z as z6 } from "zod";
7948
8589
 
7949
8590
  // src/tools/read/structure.ts
7950
- import * as fs11 from "fs";
7951
- import * as path11 from "path";
7952
- var HEADING_REGEX = /^(#{1,6})\s+(.+)$/;
8591
+ import * as fs12 from "fs";
8592
+ import * as path13 from "path";
8593
+ var HEADING_REGEX2 = /^(#{1,6})\s+(.+)$/;
7953
8594
  function extractHeadings(content) {
7954
8595
  const lines = content.split("\n");
7955
8596
  const headings = [];
@@ -7961,7 +8602,7 @@ function extractHeadings(content) {
7961
8602
  continue;
7962
8603
  }
7963
8604
  if (inCodeBlock) continue;
7964
- const match = line.match(HEADING_REGEX);
8605
+ const match = line.match(HEADING_REGEX2);
7965
8606
  if (match) {
7966
8607
  headings.push({
7967
8608
  level: match[1].length,
@@ -8002,10 +8643,10 @@ function buildSections(headings, totalLines) {
8002
8643
  async function getNoteStructure(index, notePath, vaultPath2) {
8003
8644
  const note = index.notes.get(notePath);
8004
8645
  if (!note) return null;
8005
- const absolutePath = path11.join(vaultPath2, notePath);
8646
+ const absolutePath = path13.join(vaultPath2, notePath);
8006
8647
  let content;
8007
8648
  try {
8008
- content = await fs11.promises.readFile(absolutePath, "utf-8");
8649
+ content = await fs12.promises.readFile(absolutePath, "utf-8");
8009
8650
  } catch {
8010
8651
  return null;
8011
8652
  }
@@ -8025,10 +8666,10 @@ async function getNoteStructure(index, notePath, vaultPath2) {
8025
8666
  async function getSectionContent(index, notePath, headingText, vaultPath2, includeSubheadings = true) {
8026
8667
  const note = index.notes.get(notePath);
8027
8668
  if (!note) return null;
8028
- const absolutePath = path11.join(vaultPath2, notePath);
8669
+ const absolutePath = path13.join(vaultPath2, notePath);
8029
8670
  let content;
8030
8671
  try {
8031
- content = await fs11.promises.readFile(absolutePath, "utf-8");
8672
+ content = await fs12.promises.readFile(absolutePath, "utf-8");
8032
8673
  } catch {
8033
8674
  return null;
8034
8675
  }
@@ -8067,10 +8708,10 @@ async function findSections(index, headingPattern, vaultPath2, folder) {
8067
8708
  const results = [];
8068
8709
  for (const note of index.notes.values()) {
8069
8710
  if (folder && !note.path.startsWith(folder)) continue;
8070
- const absolutePath = path11.join(vaultPath2, note.path);
8711
+ const absolutePath = path13.join(vaultPath2, note.path);
8071
8712
  let content;
8072
8713
  try {
8073
- content = await fs11.promises.readFile(absolutePath, "utf-8");
8714
+ content = await fs12.promises.readFile(absolutePath, "utf-8");
8074
8715
  } catch {
8075
8716
  continue;
8076
8717
  }
@@ -8089,140 +8730,6 @@ async function findSections(index, headingPattern, vaultPath2, folder) {
8089
8730
  return results;
8090
8731
  }
8091
8732
 
8092
- // src/tools/read/tasks.ts
8093
- import * as fs12 from "fs";
8094
- import * as path12 from "path";
8095
- var TASK_REGEX = /^(\s*)- \[([ xX\-])\]\s+(.+)$/;
8096
- var TAG_REGEX2 = /#([a-zA-Z][a-zA-Z0-9_/-]*)/g;
8097
- var DATE_REGEX = /\b(\d{4}-\d{2}-\d{2}|\d{1,2}\/\d{1,2}\/\d{2,4})\b/;
8098
- var HEADING_REGEX2 = /^(#{1,6})\s+(.+)$/;
8099
- function parseStatus(char) {
8100
- if (char === " ") return "open";
8101
- if (char === "-") return "cancelled";
8102
- return "completed";
8103
- }
8104
- function extractTags2(text) {
8105
- const tags = [];
8106
- let match;
8107
- TAG_REGEX2.lastIndex = 0;
8108
- while ((match = TAG_REGEX2.exec(text)) !== null) {
8109
- tags.push(match[1]);
8110
- }
8111
- return tags;
8112
- }
8113
- function extractDueDate(text) {
8114
- const match = text.match(DATE_REGEX);
8115
- return match ? match[1] : void 0;
8116
- }
8117
- async function extractTasksFromNote(notePath, absolutePath) {
8118
- let content;
8119
- try {
8120
- content = await fs12.promises.readFile(absolutePath, "utf-8");
8121
- } catch {
8122
- return [];
8123
- }
8124
- const lines = content.split("\n");
8125
- const tasks = [];
8126
- let currentHeading;
8127
- let inCodeBlock = false;
8128
- for (let i = 0; i < lines.length; i++) {
8129
- const line = lines[i];
8130
- if (line.startsWith("```")) {
8131
- inCodeBlock = !inCodeBlock;
8132
- continue;
8133
- }
8134
- if (inCodeBlock) continue;
8135
- const headingMatch = line.match(HEADING_REGEX2);
8136
- if (headingMatch) {
8137
- currentHeading = headingMatch[2].trim();
8138
- continue;
8139
- }
8140
- const taskMatch = line.match(TASK_REGEX);
8141
- if (taskMatch) {
8142
- const statusChar = taskMatch[2];
8143
- const text = taskMatch[3].trim();
8144
- tasks.push({
8145
- path: notePath,
8146
- line: i + 1,
8147
- text,
8148
- status: parseStatus(statusChar),
8149
- raw: line,
8150
- context: currentHeading,
8151
- tags: extractTags2(text),
8152
- due_date: extractDueDate(text)
8153
- });
8154
- }
8155
- }
8156
- return tasks;
8157
- }
8158
- async function getAllTasks(index, vaultPath2, options = {}) {
8159
- const { status = "all", folder, tag, excludeTags = [], limit } = options;
8160
- const allTasks = [];
8161
- for (const note of index.notes.values()) {
8162
- if (folder && !note.path.startsWith(folder)) continue;
8163
- const absolutePath = path12.join(vaultPath2, note.path);
8164
- const tasks = await extractTasksFromNote(note.path, absolutePath);
8165
- allTasks.push(...tasks);
8166
- }
8167
- let filteredTasks = allTasks;
8168
- if (status !== "all") {
8169
- filteredTasks = allTasks.filter((t) => t.status === status);
8170
- }
8171
- if (tag) {
8172
- filteredTasks = filteredTasks.filter((t) => t.tags.includes(tag));
8173
- }
8174
- if (excludeTags.length > 0) {
8175
- filteredTasks = filteredTasks.filter(
8176
- (t) => !excludeTags.some((excludeTag) => t.tags.includes(excludeTag))
8177
- );
8178
- }
8179
- filteredTasks.sort((a, b) => {
8180
- if (a.due_date && !b.due_date) return -1;
8181
- if (!a.due_date && b.due_date) return 1;
8182
- if (a.due_date && b.due_date) {
8183
- const cmp = b.due_date.localeCompare(a.due_date);
8184
- if (cmp !== 0) return cmp;
8185
- }
8186
- const noteA = index.notes.get(a.path);
8187
- const noteB = index.notes.get(b.path);
8188
- const mtimeA = noteA?.modified?.getTime() ?? 0;
8189
- const mtimeB = noteB?.modified?.getTime() ?? 0;
8190
- return mtimeB - mtimeA;
8191
- });
8192
- const openCount = allTasks.filter((t) => t.status === "open").length;
8193
- const completedCount = allTasks.filter((t) => t.status === "completed").length;
8194
- const cancelledCount = allTasks.filter((t) => t.status === "cancelled").length;
8195
- const returnTasks = limit ? filteredTasks.slice(0, limit) : filteredTasks;
8196
- return {
8197
- total: allTasks.length,
8198
- open_count: openCount,
8199
- completed_count: completedCount,
8200
- cancelled_count: cancelledCount,
8201
- tasks: returnTasks
8202
- };
8203
- }
8204
- async function getTasksFromNote(index, notePath, vaultPath2, excludeTags = []) {
8205
- const note = index.notes.get(notePath);
8206
- if (!note) return null;
8207
- const absolutePath = path12.join(vaultPath2, notePath);
8208
- let tasks = await extractTasksFromNote(notePath, absolutePath);
8209
- if (excludeTags.length > 0) {
8210
- tasks = tasks.filter(
8211
- (t) => !excludeTags.some((excludeTag) => t.tags.includes(excludeTag))
8212
- );
8213
- }
8214
- return tasks;
8215
- }
8216
- async function getTasksWithDueDates(index, vaultPath2, options = {}) {
8217
- const { status = "open", folder, excludeTags } = options;
8218
- const result = await getAllTasks(index, vaultPath2, { status, folder, excludeTags });
8219
- return result.tasks.filter((t) => t.due_date).sort((a, b) => {
8220
- const dateA = a.due_date || "";
8221
- const dateB = b.due_date || "";
8222
- return dateA.localeCompare(dateB);
8223
- });
8224
- }
8225
-
8226
8733
  // src/tools/read/primitives.ts
8227
8734
  function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = () => ({})) {
8228
8735
  server2.registerTool(
@@ -8235,18 +8742,18 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
8235
8742
  include_content: z6.boolean().default(false).describe("Include the text content under each top-level section")
8236
8743
  }
8237
8744
  },
8238
- async ({ path: path29, include_content }) => {
8745
+ async ({ path: path30, include_content }) => {
8239
8746
  const index = getIndex();
8240
8747
  const vaultPath2 = getVaultPath();
8241
- const result = await getNoteStructure(index, path29, vaultPath2);
8748
+ const result = await getNoteStructure(index, path30, vaultPath2);
8242
8749
  if (!result) {
8243
8750
  return {
8244
- content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path29 }, null, 2) }]
8751
+ content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path30 }, null, 2) }]
8245
8752
  };
8246
8753
  }
8247
8754
  if (include_content) {
8248
8755
  for (const section of result.sections) {
8249
- const sectionResult = await getSectionContent(index, path29, section.heading.text, vaultPath2, true);
8756
+ const sectionResult = await getSectionContent(index, path30, section.heading.text, vaultPath2, true);
8250
8757
  if (sectionResult) {
8251
8758
  section.content = sectionResult.content;
8252
8759
  }
@@ -8268,15 +8775,15 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
8268
8775
  include_subheadings: z6.boolean().default(true).describe("Include content under subheadings")
8269
8776
  }
8270
8777
  },
8271
- async ({ path: path29, heading, include_subheadings }) => {
8778
+ async ({ path: path30, heading, include_subheadings }) => {
8272
8779
  const index = getIndex();
8273
8780
  const vaultPath2 = getVaultPath();
8274
- const result = await getSectionContent(index, path29, heading, vaultPath2, include_subheadings);
8781
+ const result = await getSectionContent(index, path30, heading, vaultPath2, include_subheadings);
8275
8782
  if (!result) {
8276
8783
  return {
8277
8784
  content: [{ type: "text", text: JSON.stringify({
8278
8785
  error: "Section not found",
8279
- path: path29,
8786
+ path: path30,
8280
8787
  heading
8281
8788
  }, null, 2) }]
8282
8789
  };
@@ -8330,16 +8837,16 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
8330
8837
  offset: z6.coerce.number().default(0).describe("Number of results to skip (for pagination)")
8331
8838
  }
8332
8839
  },
8333
- async ({ path: path29, status, has_due_date, folder, tag, limit: requestedLimit, offset }) => {
8840
+ async ({ path: path30, status, has_due_date, folder, tag, limit: requestedLimit, offset }) => {
8334
8841
  const limit = Math.min(requestedLimit ?? 25, MAX_LIMIT);
8335
8842
  const index = getIndex();
8336
8843
  const vaultPath2 = getVaultPath();
8337
8844
  const config = getConfig();
8338
- if (path29) {
8339
- const result2 = await getTasksFromNote(index, path29, vaultPath2, config.exclude_task_tags || []);
8845
+ if (path30) {
8846
+ const result2 = await getTasksFromNote(index, path30, vaultPath2, config.exclude_task_tags || []);
8340
8847
  if (!result2) {
8341
8848
  return {
8342
- content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path29 }, null, 2) }]
8849
+ content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path30 }, null, 2) }]
8343
8850
  };
8344
8851
  }
8345
8852
  let filtered = result2;
@@ -8349,7 +8856,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
8349
8856
  const paged2 = filtered.slice(offset, offset + limit);
8350
8857
  return {
8351
8858
  content: [{ type: "text", text: JSON.stringify({
8352
- path: path29,
8859
+ path: path30,
8353
8860
  total_count: filtered.length,
8354
8861
  returned_count: paged2.length,
8355
8862
  open: result2.filter((t) => t.status === "open").length,
@@ -8358,6 +8865,44 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
8358
8865
  }, null, 2) }]
8359
8866
  };
8360
8867
  }
8868
+ if (isTaskCacheReady()) {
8869
+ refreshIfStale(vaultPath2, index, config.exclude_task_tags);
8870
+ if (has_due_date) {
8871
+ const result3 = queryTasksFromCache({
8872
+ status,
8873
+ folder,
8874
+ excludeTags: config.exclude_task_tags,
8875
+ has_due_date: true,
8876
+ limit,
8877
+ offset
8878
+ });
8879
+ return {
8880
+ content: [{ type: "text", text: JSON.stringify({
8881
+ total_count: result3.total,
8882
+ returned_count: result3.tasks.length,
8883
+ tasks: result3.tasks
8884
+ }, null, 2) }]
8885
+ };
8886
+ }
8887
+ const result2 = queryTasksFromCache({
8888
+ status,
8889
+ folder,
8890
+ tag,
8891
+ excludeTags: config.exclude_task_tags,
8892
+ limit,
8893
+ offset
8894
+ });
8895
+ return {
8896
+ content: [{ type: "text", text: JSON.stringify({
8897
+ total_count: result2.total,
8898
+ open_count: result2.open_count,
8899
+ completed_count: result2.completed_count,
8900
+ cancelled_count: result2.cancelled_count,
8901
+ returned_count: result2.tasks.length,
8902
+ tasks: result2.tasks
8903
+ }, null, 2) }]
8904
+ };
8905
+ }
8361
8906
  if (has_due_date) {
8362
8907
  const allResults = await getTasksWithDueDates(index, vaultPath2, {
8363
8908
  status,
@@ -8385,6 +8930,8 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
8385
8930
  content: [{ type: "text", text: JSON.stringify({
8386
8931
  total_count: result.total,
8387
8932
  open_count: result.open_count,
8933
+ completed_count: result.completed_count,
8934
+ cancelled_count: result.cancelled_count,
8388
8935
  returned_count: paged.length,
8389
8936
  tasks: paged
8390
8937
  }, null, 2) }]
@@ -8465,7 +9012,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
8465
9012
  // src/tools/read/migrations.ts
8466
9013
  import { z as z7 } from "zod";
8467
9014
  import * as fs13 from "fs/promises";
8468
- import * as path13 from "path";
9015
+ import * as path14 from "path";
8469
9016
  import matter2 from "gray-matter";
8470
9017
  function getNotesInFolder(index, folder) {
8471
9018
  const notes = [];
@@ -8478,7 +9025,7 @@ function getNotesInFolder(index, folder) {
8478
9025
  return notes;
8479
9026
  }
8480
9027
  async function readFileContent(notePath, vaultPath2) {
8481
- const fullPath = path13.join(vaultPath2, notePath);
9028
+ const fullPath = path14.join(vaultPath2, notePath);
8482
9029
  try {
8483
9030
  return await fs13.readFile(fullPath, "utf-8");
8484
9031
  } catch {
@@ -8486,7 +9033,7 @@ async function readFileContent(notePath, vaultPath2) {
8486
9033
  }
8487
9034
  }
8488
9035
  async function writeFileContent(notePath, vaultPath2, content) {
8489
- const fullPath = path13.join(vaultPath2, notePath);
9036
+ const fullPath = path14.join(vaultPath2, notePath);
8490
9037
  try {
8491
9038
  await fs13.writeFile(fullPath, content, "utf-8");
8492
9039
  return true;
@@ -8667,7 +9214,7 @@ function registerMigrationTools(server2, getIndex, getVaultPath) {
8667
9214
 
8668
9215
  // src/tools/read/graphAnalysis.ts
8669
9216
  import fs14 from "node:fs";
8670
- import path14 from "node:path";
9217
+ import path15 from "node:path";
8671
9218
  import { z as z8 } from "zod";
8672
9219
 
8673
9220
  // src/tools/read/schema.ts
@@ -9221,7 +9768,26 @@ function purgeOldSnapshots(stateDb2, retentionDays = 90) {
9221
9768
  }
9222
9769
 
9223
9770
  // src/tools/read/graphAnalysis.ts
9224
- function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb) {
9771
+ function isPeriodicNote(notePath) {
9772
+ const filename = notePath.split("/").pop() || "";
9773
+ const nameWithoutExt = filename.replace(/\.md$/, "");
9774
+ const patterns = [
9775
+ /^\d{4}-\d{2}-\d{2}$/,
9776
+ // YYYY-MM-DD (daily)
9777
+ /^\d{4}-W\d{2}$/,
9778
+ // YYYY-Wnn (weekly)
9779
+ /^\d{4}-\d{2}$/,
9780
+ // YYYY-MM (monthly)
9781
+ /^\d{4}-Q[1-4]$/,
9782
+ // YYYY-Qn (quarterly)
9783
+ /^\d{4}$/
9784
+ // YYYY (yearly)
9785
+ ];
9786
+ const periodicFolders = ["daily", "weekly", "monthly", "quarterly", "yearly", "journal", "journals"];
9787
+ const folder = notePath.split("/")[0]?.toLowerCase() || "";
9788
+ return patterns.some((p) => p.test(nameWithoutExt)) || periodicFolders.includes(folder);
9789
+ }
9790
+ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb, getConfig) {
9225
9791
  server2.registerTool(
9226
9792
  "graph_analysis",
9227
9793
  {
@@ -9244,7 +9810,7 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb)
9244
9810
  const index = getIndex();
9245
9811
  switch (analysis) {
9246
9812
  case "orphans": {
9247
- const allOrphans = findOrphanNotes(index, folder);
9813
+ const allOrphans = findOrphanNotes(index, folder).filter((o) => !isPeriodicNote(o.path));
9248
9814
  const orphans = allOrphans.slice(offset, offset + limit);
9249
9815
  return {
9250
9816
  content: [{ type: "text", text: JSON.stringify({
@@ -9287,7 +9853,17 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb)
9287
9853
  };
9288
9854
  }
9289
9855
  case "hubs": {
9290
- const allHubs = findHubNotes(index, min_links);
9856
+ const excludeTags = new Set(
9857
+ (getConfig?.()?.exclude_analysis_tags ?? []).map((t) => t.toLowerCase())
9858
+ );
9859
+ const allHubs = findHubNotes(index, min_links).filter((h) => {
9860
+ if (excludeTags.size === 0) return true;
9861
+ const note = index.notes.get(h.path);
9862
+ if (!note) return true;
9863
+ const tags = note.frontmatter?.tags;
9864
+ const tagList = Array.isArray(tags) ? tags : typeof tags === "string" ? [tags] : [];
9865
+ return !tagList.some((t) => excludeTags.has(String(t).toLowerCase()));
9866
+ });
9291
9867
  const hubs = allHubs.slice(offset, offset + limit);
9292
9868
  return {
9293
9869
  content: [{ type: "text", text: JSON.stringify({
@@ -9329,14 +9905,14 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb)
9329
9905
  case "immature": {
9330
9906
  const vaultPath2 = getVaultPath();
9331
9907
  const allNotes = Array.from(index.notes.values()).filter(
9332
- (note) => !folder || note.path.startsWith(folder + "/") || note.path.substring(0, note.path.lastIndexOf("/")) === folder
9908
+ (note) => (!folder || note.path.startsWith(folder + "/") || note.path.substring(0, note.path.lastIndexOf("/")) === folder) && !isPeriodicNote(note.path)
9333
9909
  );
9334
9910
  const conventions = inferFolderConventions(index, folder, 0.5);
9335
9911
  const expectedFields = conventions.inferred_fields.map((f) => f.name);
9336
9912
  const scored = allNotes.map((note) => {
9337
9913
  let wordCount = 0;
9338
9914
  try {
9339
- const content = fs14.readFileSync(path14.join(vaultPath2, note.path), "utf-8");
9915
+ const content = fs14.readFileSync(path15.join(vaultPath2, note.path), "utf-8");
9340
9916
  const body = content.replace(/^---[\s\S]*?---\n?/, "");
9341
9917
  wordCount = body.split(/\s+/).filter((w) => w.length > 0).length;
9342
9918
  } catch {
@@ -9385,8 +9961,8 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb)
9385
9961
  };
9386
9962
  }
9387
9963
  case "evolution": {
9388
- const db3 = getStateDb?.();
9389
- if (!db3) {
9964
+ const db4 = getStateDb?.();
9965
+ if (!db4) {
9390
9966
  return {
9391
9967
  content: [{ type: "text", text: JSON.stringify({
9392
9968
  error: "StateDb not available \u2014 graph evolution requires persistent state"
@@ -9394,7 +9970,7 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb)
9394
9970
  };
9395
9971
  }
9396
9972
  const daysBack = days ?? 30;
9397
- const evolutions = getGraphEvolution(db3, daysBack);
9973
+ const evolutions = getGraphEvolution(db4, daysBack);
9398
9974
  return {
9399
9975
  content: [{ type: "text", text: JSON.stringify({
9400
9976
  analysis: "evolution",
@@ -9404,8 +9980,8 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb)
9404
9980
  };
9405
9981
  }
9406
9982
  case "emerging_hubs": {
9407
- const db3 = getStateDb?.();
9408
- if (!db3) {
9983
+ const db4 = getStateDb?.();
9984
+ if (!db4) {
9409
9985
  return {
9410
9986
  content: [{ type: "text", text: JSON.stringify({
9411
9987
  error: "StateDb not available \u2014 emerging hubs requires persistent state"
@@ -9413,7 +9989,23 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb)
9413
9989
  };
9414
9990
  }
9415
9991
  const daysBack = days ?? 30;
9416
- const hubs = getEmergingHubs(db3, daysBack);
9992
+ let hubs = getEmergingHubs(db4, daysBack);
9993
+ const excludeTags = new Set(
9994
+ (getConfig?.()?.exclude_analysis_tags ?? []).map((t) => t.toLowerCase())
9995
+ );
9996
+ if (excludeTags.size > 0) {
9997
+ const notesByTitle = /* @__PURE__ */ new Map();
9998
+ for (const note of index.notes.values()) {
9999
+ notesByTitle.set(note.title.toLowerCase(), note);
10000
+ }
10001
+ hubs = hubs.filter((hub) => {
10002
+ const note = notesByTitle.get(hub.entity.toLowerCase());
10003
+ if (!note) return true;
10004
+ const tags = note.frontmatter?.tags;
10005
+ const tagList = Array.isArray(tags) ? tags : typeof tags === "string" ? [tags] : [];
10006
+ return !tagList.some((t) => excludeTags.has(String(t).toLowerCase()));
10007
+ });
10008
+ }
9417
10009
  return {
9418
10010
  content: [{ type: "text", text: JSON.stringify({
9419
10011
  analysis: "emerging_hubs",
@@ -9902,13 +10494,12 @@ import { z as z10 } from "zod";
9902
10494
 
9903
10495
  // src/tools/read/bidirectional.ts
9904
10496
  import * as fs15 from "fs/promises";
9905
- import * as path15 from "path";
10497
+ import * as path16 from "path";
9906
10498
  import matter3 from "gray-matter";
9907
10499
  var PROSE_PATTERN_REGEX = /^([A-Za-z][A-Za-z0-9 _-]*):\s*(?:\[\[([^\]]+)\]\]|"([^"]+)"|([^\n]+?))\s*$/gm;
9908
10500
  var CODE_BLOCK_REGEX2 = /```[\s\S]*?```|`[^`\n]+`/g;
9909
- var WIKILINK_REGEX2 = /\[\[([^\]|#]+)(?:#[^\]|]*)?(?:\|[^\]]+)?\]\]/g;
9910
10501
  async function readFileContent2(notePath, vaultPath2) {
9911
- const fullPath = path15.join(vaultPath2, notePath);
10502
+ const fullPath = path16.join(vaultPath2, notePath);
9912
10503
  try {
9913
10504
  return await fs15.readFile(fullPath, "utf-8");
9914
10505
  } catch {
@@ -9931,21 +10522,6 @@ function removeCodeBlocks(content) {
9931
10522
  return "\n".repeat(newlines);
9932
10523
  });
9933
10524
  }
9934
- function extractWikilinksFromValue(value) {
9935
- if (typeof value === "string") {
9936
- const matches = [];
9937
- let match;
9938
- WIKILINK_REGEX2.lastIndex = 0;
9939
- while ((match = WIKILINK_REGEX2.exec(value)) !== null) {
9940
- matches.push(match[1].trim());
9941
- }
9942
- return matches;
9943
- }
9944
- if (Array.isArray(value)) {
9945
- return value.flatMap((v) => extractWikilinksFromValue(v));
9946
- }
9947
- return [];
9948
- }
9949
10525
  function isWikilinkValue(value) {
9950
10526
  return /^\[\[.+\]\]$/.test(value.trim());
9951
10527
  }
@@ -10072,116 +10648,40 @@ async function suggestWikilinksInFrontmatter(index, notePath, vaultPath2) {
10072
10648
  const suggestions = [];
10073
10649
  function checkValue(field, value, arrayIndex) {
10074
10650
  if (typeof value !== "string") return;
10075
- if (isWikilinkValue(value)) return;
10076
- if (!value.trim()) return;
10077
- const normalizedValue = normalizeRef(value);
10078
- const matchedPath = index.entities.get(normalizedValue);
10079
- if (matchedPath) {
10080
- const targetNote = index.notes.get(matchedPath);
10081
- suggestions.push({
10082
- field,
10083
- current_value: value,
10084
- suggested_link: `[[${targetNote?.title || value}]]`,
10085
- target_note: matchedPath,
10086
- array_index: arrayIndex
10087
- });
10088
- }
10089
- }
10090
- for (const [field, value] of Object.entries(frontmatter)) {
10091
- if (Array.isArray(value)) {
10092
- value.forEach((v, i) => checkValue(field, v, i));
10093
- } else {
10094
- checkValue(field, value);
10095
- }
10096
- }
10097
- return {
10098
- path: notePath,
10099
- suggestions
10100
- };
10101
- }
10102
- async function validateCrossLayer(index, notePath, vaultPath2) {
10103
- const content = await readFileContent2(notePath, vaultPath2);
10104
- if (content === null) {
10105
- return {
10106
- path: notePath,
10107
- frontmatter_only: [],
10108
- prose_only: [],
10109
- consistent: [],
10110
- error: "File not found"
10111
- };
10112
- }
10113
- let frontmatter = {};
10114
- let body = content;
10115
- try {
10116
- const parsed = matter3(content);
10117
- frontmatter = parsed.data;
10118
- body = parsed.content;
10119
- } catch {
10120
- }
10121
- const frontmatterRefs = /* @__PURE__ */ new Map();
10122
- for (const [field, value] of Object.entries(frontmatter)) {
10123
- const wikilinks = extractWikilinksFromValue(value);
10124
- for (const target of wikilinks) {
10125
- frontmatterRefs.set(normalizeRef(target), { field, target });
10126
- }
10127
- if (typeof value === "string" && !isWikilinkValue(value)) {
10128
- const normalized = normalizeRef(value);
10129
- if (index.entities.has(normalized)) {
10130
- frontmatterRefs.set(normalized, { field, target: value });
10131
- }
10132
- }
10133
- if (Array.isArray(value)) {
10134
- for (const v of value) {
10135
- if (typeof v === "string" && !isWikilinkValue(v)) {
10136
- const normalized = normalizeRef(v);
10137
- if (index.entities.has(normalized)) {
10138
- frontmatterRefs.set(normalized, { field, target: v });
10139
- }
10140
- }
10141
- }
10142
- }
10143
- }
10144
- const proseRefs = /* @__PURE__ */ new Map();
10145
- const cleanBody = removeCodeBlocks(body);
10146
- const lines = cleanBody.split("\n");
10147
- for (let i = 0; i < lines.length; i++) {
10148
- const line = lines[i];
10149
- WIKILINK_REGEX2.lastIndex = 0;
10150
- let match;
10151
- while ((match = WIKILINK_REGEX2.exec(line)) !== null) {
10152
- const target = match[1].trim();
10153
- proseRefs.set(normalizeRef(target), { line: i + 1, target });
10651
+ if (isWikilinkValue(value)) return;
10652
+ if (!value.trim()) return;
10653
+ const normalizedValue = normalizeRef(value);
10654
+ const matchedPath = index.entities.get(normalizedValue);
10655
+ if (matchedPath) {
10656
+ const targetNote = index.notes.get(matchedPath);
10657
+ suggestions.push({
10658
+ field,
10659
+ current_value: value,
10660
+ suggested_link: `[[${targetNote?.title || value}]]`,
10661
+ target_note: matchedPath,
10662
+ array_index: arrayIndex
10663
+ });
10154
10664
  }
10155
10665
  }
10156
- const frontmatter_only = [];
10157
- const prose_only = [];
10158
- const consistent = [];
10159
- for (const [normalized, { field, target }] of frontmatterRefs) {
10160
- if (proseRefs.has(normalized)) {
10161
- consistent.push({ field, target });
10666
+ for (const [field, value] of Object.entries(frontmatter)) {
10667
+ if (Array.isArray(value)) {
10668
+ value.forEach((v, i) => checkValue(field, v, i));
10162
10669
  } else {
10163
- frontmatter_only.push({ field, target });
10164
- }
10165
- }
10166
- for (const [normalized, { line, target }] of proseRefs) {
10167
- if (!frontmatterRefs.has(normalized)) {
10168
- prose_only.push({ pattern: `[[${target}]]`, target, line });
10670
+ checkValue(field, value);
10169
10671
  }
10170
10672
  }
10171
10673
  return {
10172
10674
  path: notePath,
10173
- frontmatter_only,
10174
- prose_only,
10175
- consistent
10675
+ suggestions
10176
10676
  };
10177
10677
  }
10178
10678
 
10179
10679
  // src/tools/read/computed.ts
10180
10680
  import * as fs16 from "fs/promises";
10181
- import * as path16 from "path";
10681
+ import * as path17 from "path";
10182
10682
  import matter4 from "gray-matter";
10183
10683
  async function readFileContent3(notePath, vaultPath2) {
10184
- const fullPath = path16.join(vaultPath2, notePath);
10684
+ const fullPath = path17.join(vaultPath2, notePath);
10185
10685
  try {
10186
10686
  return await fs16.readFile(fullPath, "utf-8");
10187
10687
  } catch {
@@ -10189,7 +10689,7 @@ async function readFileContent3(notePath, vaultPath2) {
10189
10689
  }
10190
10690
  }
10191
10691
  async function getFileStats(notePath, vaultPath2) {
10192
- const fullPath = path16.join(vaultPath2, notePath);
10692
+ const fullPath = path17.join(vaultPath2, notePath);
10193
10693
  try {
10194
10694
  const stats = await fs16.stat(fullPath);
10195
10695
  return {
@@ -10322,18 +10822,17 @@ async function computeFrontmatter(index, notePath, vaultPath2, fields) {
10322
10822
  // src/tools/read/noteIntelligence.ts
10323
10823
  import fs17 from "node:fs";
10324
10824
  import nodePath from "node:path";
10325
- function registerNoteIntelligenceTools(server2, getIndex, getVaultPath) {
10825
+ function registerNoteIntelligenceTools(server2, getIndex, getVaultPath, getConfig) {
10326
10826
  server2.registerTool(
10327
10827
  "note_intelligence",
10328
10828
  {
10329
10829
  title: "Note Intelligence",
10330
- description: 'Analyze a note for patterns, suggestions, and consistency. Use analysis to pick the mode:\n- "prose_patterns": Find "Key: Value" or "Key: [[wikilink]]" patterns in prose\n- "suggest_frontmatter": Suggest YAML frontmatter from detected prose patterns\n- "suggest_wikilinks": Find frontmatter values that could be wikilinks\n- "cross_layer": Check consistency between frontmatter and prose references\n- "compute": Auto-compute derived fields (word_count, link_count, etc.)\n- "semantic_links": Find semantically related entities not currently linked in the note (requires init_semantic)\n- "all": Run all analyses and return combined result\n\nExample: note_intelligence({ path: "projects/alpha.md", analysis: "wikilinks" })\nExample: note_intelligence({ path: "projects/alpha.md", analysis: "compute", fields: ["word_count", "link_count"] })\nExample: note_intelligence({ path: "projects/alpha.md", analysis: "semantic_links" })',
10830
+ description: 'Analyze a note for patterns, suggestions, and consistency. Use analysis to pick the mode:\n- "prose_patterns": Find "Key: Value" or "Key: [[wikilink]]" patterns in prose\n- "suggest_frontmatter": Suggest YAML frontmatter from detected prose patterns\n- "suggest_wikilinks": Find frontmatter values that could be wikilinks\n- "compute": Auto-compute derived fields (word_count, link_count, etc.)\n- "semantic_links": Find semantically related entities not currently linked in the note (requires init_semantic)\n- "all": Run all analyses and return combined result\n\nExample: note_intelligence({ path: "projects/alpha.md", analysis: "wikilinks" })\nExample: note_intelligence({ path: "projects/alpha.md", analysis: "compute", fields: ["word_count", "link_count"] })\nExample: note_intelligence({ path: "projects/alpha.md", analysis: "semantic_links" })',
10331
10831
  inputSchema: {
10332
10832
  analysis: z10.enum([
10333
10833
  "prose_patterns",
10334
10834
  "suggest_frontmatter",
10335
10835
  "suggest_wikilinks",
10336
- "cross_layer",
10337
10836
  "compute",
10338
10837
  "semantic_links",
10339
10838
  "all"
@@ -10365,12 +10864,6 @@ function registerNoteIntelligenceTools(server2, getIndex, getVaultPath) {
10365
10864
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
10366
10865
  };
10367
10866
  }
10368
- case "cross_layer": {
10369
- const result = await validateCrossLayer(index, notePath, vaultPath2);
10370
- return {
10371
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
10372
- };
10373
- }
10374
10867
  case "compute": {
10375
10868
  const result = await computeFrontmatter(index, notePath, vaultPath2, fields);
10376
10869
  return {
@@ -10401,10 +10894,26 @@ function registerNoteIntelligenceTools(server2, getIndex, getVaultPath) {
10401
10894
  while ((wlMatch = wikilinkRegex.exec(noteContent)) !== null) {
10402
10895
  linkedEntities.add(wlMatch[1].toLowerCase());
10403
10896
  }
10897
+ const excludeTags = new Set(
10898
+ (getConfig?.()?.exclude_analysis_tags ?? []).map((t) => t.toLowerCase())
10899
+ );
10404
10900
  try {
10405
10901
  const contentEmbedding = await embedTextCached(noteContent);
10406
10902
  const matches = findSemanticallySimilarEntities(contentEmbedding, 20, linkedEntities);
10407
- const suggestions = matches.filter((m) => m.similarity >= 0.3).map((m) => ({
10903
+ const suggestions = matches.filter((m) => {
10904
+ if (m.similarity < 0.3) return false;
10905
+ if (excludeTags.size > 0) {
10906
+ const entityNote = index.notes.get(m.entityName.toLowerCase() + ".md") ?? [...index.notes.values()].find((n) => n.title.toLowerCase() === m.entityName.toLowerCase());
10907
+ if (entityNote) {
10908
+ const noteTags = Object.keys(entityNote.frontmatter).filter((k) => k === "tags").flatMap((k) => {
10909
+ const v = entityNote.frontmatter[k];
10910
+ return Array.isArray(v) ? v : typeof v === "string" ? [v] : [];
10911
+ }).map((t) => String(t).toLowerCase());
10912
+ if (noteTags.some((t) => excludeTags.has(t))) return false;
10913
+ }
10914
+ }
10915
+ return true;
10916
+ }).map((m) => ({
10408
10917
  entity: m.entityName,
10409
10918
  similarity: m.similarity
10410
10919
  }));
@@ -10426,11 +10935,10 @@ function registerNoteIntelligenceTools(server2, getIndex, getVaultPath) {
10426
10935
  }
10427
10936
  }
10428
10937
  case "all": {
10429
- const [prosePatterns, suggestedFrontmatter, suggestedWikilinks, crossLayer, computed] = await Promise.all([
10938
+ const [prosePatterns, suggestedFrontmatter, suggestedWikilinks, computed] = await Promise.all([
10430
10939
  detectProsePatterns(index, notePath, vaultPath2),
10431
10940
  suggestFrontmatterFromProse(index, notePath, vaultPath2),
10432
10941
  suggestWikilinksInFrontmatter(index, notePath, vaultPath2),
10433
- validateCrossLayer(index, notePath, vaultPath2),
10434
10942
  computeFrontmatter(index, notePath, vaultPath2, fields)
10435
10943
  ]);
10436
10944
  return {
@@ -10439,7 +10947,6 @@ function registerNoteIntelligenceTools(server2, getIndex, getVaultPath) {
10439
10947
  prose_patterns: prosePatterns,
10440
10948
  suggested_frontmatter: suggestedFrontmatter,
10441
10949
  suggested_wikilinks: suggestedWikilinks,
10442
- cross_layer: crossLayer,
10443
10950
  computed
10444
10951
  }, null, 2) }]
10445
10952
  };
@@ -10453,7 +10960,7 @@ function registerNoteIntelligenceTools(server2, getIndex, getVaultPath) {
10453
10960
  init_writer();
10454
10961
  import { z as z11 } from "zod";
10455
10962
  import fs20 from "fs/promises";
10456
- import path19 from "path";
10963
+ import path20 from "path";
10457
10964
 
10458
10965
  // src/core/write/validator.ts
10459
10966
  var TIMESTAMP_PATTERN = /^\*\*\d{2}:\d{2}\*\*/;
@@ -10656,7 +11163,7 @@ function runValidationPipeline(content, format, options = {}) {
10656
11163
  // src/core/write/mutation-helpers.ts
10657
11164
  init_writer();
10658
11165
  import fs19 from "fs/promises";
10659
- import path18 from "path";
11166
+ import path19 from "path";
10660
11167
  init_constants();
10661
11168
  init_writer();
10662
11169
  function formatMcpResult(result) {
@@ -10705,7 +11212,7 @@ async function handleGitCommit(vaultPath2, notePath, commit, prefix) {
10705
11212
  return info;
10706
11213
  }
10707
11214
  async function ensureFileExists(vaultPath2, notePath) {
10708
- const fullPath = path18.join(vaultPath2, notePath);
11215
+ const fullPath = path19.join(vaultPath2, notePath);
10709
11216
  try {
10710
11217
  await fs19.access(fullPath);
10711
11218
  return null;
@@ -10735,41 +11242,58 @@ async function withVaultFile(options, operation) {
10735
11242
  if (existsError) {
10736
11243
  return formatMcpResult(existsError);
10737
11244
  }
10738
- const { content, frontmatter, lineEnding } = await readVaultFile(vaultPath2, notePath);
10739
- const writeStateDb = getWriteStateDb();
10740
- if (writeStateDb) {
10741
- processImplicitFeedback(writeStateDb, notePath, content);
11245
+ const runMutation = async () => {
11246
+ const { content, frontmatter: frontmatter2, lineEnding: lineEnding2, mtimeMs } = await readVaultFile(vaultPath2, notePath);
11247
+ const writeStateDb = getWriteStateDb();
11248
+ if (writeStateDb) {
11249
+ processImplicitFeedback(writeStateDb, notePath, content);
11250
+ }
11251
+ let sectionBoundary;
11252
+ if (section) {
11253
+ const sectionResult = ensureSectionExists(content, section, notePath);
11254
+ if ("error" in sectionResult) {
11255
+ return { error: sectionResult.error };
11256
+ }
11257
+ sectionBoundary = sectionResult.boundary;
11258
+ }
11259
+ const ctx = {
11260
+ content,
11261
+ frontmatter: frontmatter2,
11262
+ lineEnding: lineEnding2,
11263
+ sectionBoundary,
11264
+ vaultPath: vaultPath2,
11265
+ notePath
11266
+ };
11267
+ const opResult2 = await operation(ctx);
11268
+ return { opResult: opResult2, frontmatter: frontmatter2, lineEnding: lineEnding2, mtimeMs };
11269
+ };
11270
+ let result = await runMutation();
11271
+ if ("error" in result) {
11272
+ return formatMcpResult(result.error);
10742
11273
  }
10743
- let sectionBoundary;
10744
- if (section) {
10745
- const sectionResult = ensureSectionExists(content, section, notePath);
10746
- if ("error" in sectionResult) {
10747
- return formatMcpResult(sectionResult.error);
11274
+ const fullPath = path19.join(vaultPath2, notePath);
11275
+ const statBefore = await fs19.stat(fullPath);
11276
+ if (statBefore.mtimeMs !== result.mtimeMs) {
11277
+ console.warn(`[withVaultFile] External modification detected on ${notePath}, re-reading and retrying`);
11278
+ result = await runMutation();
11279
+ if ("error" in result) {
11280
+ return formatMcpResult(result.error);
10748
11281
  }
10749
- sectionBoundary = sectionResult.boundary;
10750
11282
  }
10751
- const ctx = {
10752
- content,
10753
- frontmatter,
10754
- lineEnding,
10755
- sectionBoundary,
10756
- vaultPath: vaultPath2,
10757
- notePath
10758
- };
10759
- const opResult = await operation(ctx);
11283
+ const { opResult, frontmatter, lineEnding } = result;
10760
11284
  let finalFrontmatter = opResult.updatedFrontmatter ?? frontmatter;
10761
11285
  if (scoping && (scoping.agent_id || scoping.session_id)) {
10762
11286
  finalFrontmatter = injectMutationMetadata(finalFrontmatter, scoping);
10763
11287
  }
10764
11288
  await writeVaultFile(vaultPath2, notePath, opResult.updatedContent, finalFrontmatter, lineEnding);
10765
11289
  const gitInfo = await handleGitCommit(vaultPath2, notePath, commit, commitPrefix);
10766
- const result = successResult(notePath, opResult.message, gitInfo, {
11290
+ const successRes = successResult(notePath, opResult.message, gitInfo, {
10767
11291
  preview: opResult.preview,
10768
11292
  warnings: opResult.warnings,
10769
11293
  outputIssues: opResult.outputIssues,
10770
11294
  normalizationChanges: opResult.normalizationChanges
10771
11295
  });
10772
- return formatMcpResult(result);
11296
+ return formatMcpResult(successRes);
10773
11297
  } catch (error) {
10774
11298
  const extras = {};
10775
11299
  if (error instanceof DiagnosticError) {
@@ -10810,10 +11334,10 @@ async function withVaultFrontmatter(options, operation) {
10810
11334
 
10811
11335
  // src/tools/write/mutations.ts
10812
11336
  async function createNoteFromTemplate(vaultPath2, notePath, config) {
10813
- const fullPath = path19.join(vaultPath2, notePath);
10814
- await fs20.mkdir(path19.dirname(fullPath), { recursive: true });
11337
+ const fullPath = path20.join(vaultPath2, notePath);
11338
+ await fs20.mkdir(path20.dirname(fullPath), { recursive: true });
10815
11339
  const templates = config.templates || {};
10816
- const filename = path19.basename(notePath, ".md").toLowerCase();
11340
+ const filename = path20.basename(notePath, ".md").toLowerCase();
10817
11341
  let templatePath;
10818
11342
  const dailyPattern = /^\d{4}-\d{2}-\d{2}/;
10819
11343
  const weeklyPattern = /^\d{4}-W\d{2}/;
@@ -10834,10 +11358,10 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
10834
11358
  let templateContent;
10835
11359
  if (templatePath) {
10836
11360
  try {
10837
- const absTemplatePath = path19.join(vaultPath2, templatePath);
11361
+ const absTemplatePath = path20.join(vaultPath2, templatePath);
10838
11362
  templateContent = await fs20.readFile(absTemplatePath, "utf-8");
10839
11363
  } catch {
10840
- const title = path19.basename(notePath, ".md");
11364
+ const title = path20.basename(notePath, ".md");
10841
11365
  templateContent = `---
10842
11366
  ---
10843
11367
 
@@ -10846,7 +11370,7 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
10846
11370
  templatePath = void 0;
10847
11371
  }
10848
11372
  } else {
10849
- const title = path19.basename(notePath, ".md");
11373
+ const title = path20.basename(notePath, ".md");
10850
11374
  templateContent = `---
10851
11375
  ---
10852
11376
 
@@ -10855,7 +11379,7 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
10855
11379
  }
10856
11380
  const now = /* @__PURE__ */ new Date();
10857
11381
  const dateStr = now.toISOString().split("T")[0];
10858
- templateContent = templateContent.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g, path19.basename(notePath, ".md"));
11382
+ templateContent = templateContent.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g, path20.basename(notePath, ".md"));
10859
11383
  const matter9 = (await import("gray-matter")).default;
10860
11384
  const parsed = matter9(templateContent);
10861
11385
  if (!parsed.data.date) {
@@ -10894,7 +11418,7 @@ Example: vault_add_to_section({ path: "daily/2026-02-15.md", section: "Log", con
10894
11418
  let noteCreated = false;
10895
11419
  let templateUsed;
10896
11420
  if (create_if_missing) {
10897
- const fullPath = path19.join(vaultPath2, notePath);
11421
+ const fullPath = path20.join(vaultPath2, notePath);
10898
11422
  try {
10899
11423
  await fs20.access(fullPath);
10900
11424
  } catch {
@@ -11191,6 +11715,8 @@ function registerTaskTools(server2, vaultPath2) {
11191
11715
  finalFrontmatter = injectMutationMetadata(frontmatter, { agent_id, session_id });
11192
11716
  }
11193
11717
  await writeVaultFile(vaultPath2, notePath, toggleResult.content, finalFrontmatter);
11718
+ await updateTaskCacheForFile(vaultPath2, notePath).catch(() => {
11719
+ });
11194
11720
  const gitInfo = await handleGitCommit(vaultPath2, notePath, commit, "[Flywheel:Task]");
11195
11721
  const newStatus = toggleResult.newState ? "completed" : "incomplete";
11196
11722
  const checkbox = toggleResult.newState ? "[x]" : "[ ]";
@@ -11345,7 +11871,7 @@ Example: vault_update_frontmatter({ path: "projects/alpha.md", frontmatter: { st
11345
11871
  init_writer();
11346
11872
  import { z as z14 } from "zod";
11347
11873
  import fs21 from "fs/promises";
11348
- import path20 from "path";
11874
+ import path21 from "path";
11349
11875
  function registerNoteTools(server2, vaultPath2, getIndex) {
11350
11876
  server2.tool(
11351
11877
  "vault_create_note",
@@ -11368,23 +11894,23 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
11368
11894
  if (!validatePath(vaultPath2, notePath)) {
11369
11895
  return formatMcpResult(errorResult(notePath, "Invalid path: path traversal not allowed"));
11370
11896
  }
11371
- const fullPath = path20.join(vaultPath2, notePath);
11897
+ const fullPath = path21.join(vaultPath2, notePath);
11372
11898
  const existsCheck = await ensureFileExists(vaultPath2, notePath);
11373
11899
  if (existsCheck === null && !overwrite) {
11374
11900
  return formatMcpResult(errorResult(notePath, `File already exists: ${notePath}. Use overwrite=true to replace.`));
11375
11901
  }
11376
- const dir = path20.dirname(fullPath);
11902
+ const dir = path21.dirname(fullPath);
11377
11903
  await fs21.mkdir(dir, { recursive: true });
11378
11904
  let effectiveContent = content;
11379
11905
  let effectiveFrontmatter = frontmatter;
11380
11906
  if (template) {
11381
- const templatePath = path20.join(vaultPath2, template);
11907
+ const templatePath = path21.join(vaultPath2, template);
11382
11908
  try {
11383
11909
  const raw = await fs21.readFile(templatePath, "utf-8");
11384
11910
  const matter9 = (await import("gray-matter")).default;
11385
11911
  const parsed = matter9(raw);
11386
11912
  const dateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
11387
- const title = path20.basename(notePath, ".md");
11913
+ const title = path21.basename(notePath, ".md");
11388
11914
  let templateContent = parsed.content.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g, title);
11389
11915
  if (content) {
11390
11916
  templateContent = templateContent.trimEnd() + "\n\n" + content;
@@ -11399,7 +11925,7 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
11399
11925
  effectiveFrontmatter.date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
11400
11926
  }
11401
11927
  const warnings = [];
11402
- const noteName = path20.basename(notePath, ".md");
11928
+ const noteName = path21.basename(notePath, ".md");
11403
11929
  const existingAliases = Array.isArray(effectiveFrontmatter?.aliases) ? effectiveFrontmatter.aliases.filter((a) => typeof a === "string") : [];
11404
11930
  const preflight = await checkPreflightSimilarity(noteName);
11405
11931
  if (preflight.existingEntity) {
@@ -11516,7 +12042,7 @@ ${sources}`;
11516
12042
  }
11517
12043
  return formatMcpResult(errorResult(notePath, previewLines.join("\n")));
11518
12044
  }
11519
- const fullPath = path20.join(vaultPath2, notePath);
12045
+ const fullPath = path21.join(vaultPath2, notePath);
11520
12046
  await fs21.unlink(fullPath);
11521
12047
  const gitInfo = await handleGitCommit(vaultPath2, notePath, commit, "[Flywheel:Delete]");
11522
12048
  const message = backlinkWarning ? `Deleted note: ${notePath}
@@ -11536,7 +12062,7 @@ Warning: ${backlinkWarning}` : `Deleted note: ${notePath}`;
11536
12062
  init_writer();
11537
12063
  import { z as z15 } from "zod";
11538
12064
  import fs22 from "fs/promises";
11539
- import path21 from "path";
12065
+ import path22 from "path";
11540
12066
  import matter6 from "gray-matter";
11541
12067
  function escapeRegex(str) {
11542
12068
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -11555,7 +12081,7 @@ function extractWikilinks2(content) {
11555
12081
  return wikilinks;
11556
12082
  }
11557
12083
  function getTitleFromPath(filePath) {
11558
- return path21.basename(filePath, ".md");
12084
+ return path22.basename(filePath, ".md");
11559
12085
  }
11560
12086
  async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
11561
12087
  const results = [];
@@ -11564,7 +12090,7 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
11564
12090
  const files = [];
11565
12091
  const entries = await fs22.readdir(dir, { withFileTypes: true });
11566
12092
  for (const entry of entries) {
11567
- const fullPath = path21.join(dir, entry.name);
12093
+ const fullPath = path22.join(dir, entry.name);
11568
12094
  if (entry.isDirectory() && !entry.name.startsWith(".")) {
11569
12095
  files.push(...await scanDir(fullPath));
11570
12096
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
@@ -11575,7 +12101,7 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
11575
12101
  }
11576
12102
  const allFiles = await scanDir(vaultPath2);
11577
12103
  for (const filePath of allFiles) {
11578
- const relativePath = path21.relative(vaultPath2, filePath);
12104
+ const relativePath = path22.relative(vaultPath2, filePath);
11579
12105
  const content = await fs22.readFile(filePath, "utf-8");
11580
12106
  const wikilinks = extractWikilinks2(content);
11581
12107
  const matchingLinks = [];
@@ -11595,7 +12121,7 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
11595
12121
  return results;
11596
12122
  }
11597
12123
  async function updateBacklinksInFile(vaultPath2, filePath, oldTitles, newTitle) {
11598
- const fullPath = path21.join(vaultPath2, filePath);
12124
+ const fullPath = path22.join(vaultPath2, filePath);
11599
12125
  const raw = await fs22.readFile(fullPath, "utf-8");
11600
12126
  const parsed = matter6(raw);
11601
12127
  let content = parsed.content;
@@ -11662,8 +12188,8 @@ function registerMoveNoteTools(server2, vaultPath2) {
11662
12188
  };
11663
12189
  return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
11664
12190
  }
11665
- const oldFullPath = path21.join(vaultPath2, oldPath);
11666
- const newFullPath = path21.join(vaultPath2, newPath);
12191
+ const oldFullPath = path22.join(vaultPath2, oldPath);
12192
+ const newFullPath = path22.join(vaultPath2, newPath);
11667
12193
  try {
11668
12194
  await fs22.access(oldFullPath);
11669
12195
  } catch {
@@ -11713,7 +12239,7 @@ function registerMoveNoteTools(server2, vaultPath2) {
11713
12239
  }
11714
12240
  }
11715
12241
  }
11716
- const destDir = path21.dirname(newFullPath);
12242
+ const destDir = path22.dirname(newFullPath);
11717
12243
  await fs22.mkdir(destDir, { recursive: true });
11718
12244
  await fs22.rename(oldFullPath, newFullPath);
11719
12245
  let gitCommit;
@@ -11799,10 +12325,10 @@ function registerMoveNoteTools(server2, vaultPath2) {
11799
12325
  if (sanitizedTitle !== newTitle) {
11800
12326
  console.error(`[Flywheel] Title sanitized: "${newTitle}" \u2192 "${sanitizedTitle}"`);
11801
12327
  }
11802
- const fullPath = path21.join(vaultPath2, notePath);
11803
- const dir = path21.dirname(notePath);
11804
- const newPath = dir === "." ? `${sanitizedTitle}.md` : path21.join(dir, `${sanitizedTitle}.md`);
11805
- const newFullPath = path21.join(vaultPath2, newPath);
12328
+ const fullPath = path22.join(vaultPath2, notePath);
12329
+ const dir = path22.dirname(notePath);
12330
+ const newPath = dir === "." ? `${sanitizedTitle}.md` : path22.join(dir, `${sanitizedTitle}.md`);
12331
+ const newFullPath = path22.join(vaultPath2, newPath);
11806
12332
  try {
11807
12333
  await fs22.access(fullPath);
11808
12334
  } catch {
@@ -11910,15 +12436,146 @@ function registerMoveNoteTools(server2, vaultPath2) {
11910
12436
  );
11911
12437
  }
11912
12438
 
11913
- // src/tools/write/system.ts
12439
+ // src/tools/write/merge.ts
12440
+ init_writer();
11914
12441
  import { z as z16 } from "zod";
12442
+ import fs23 from "fs/promises";
12443
+ function registerMergeTools(server2, vaultPath2) {
12444
+ server2.tool(
12445
+ "merge_entities",
12446
+ "Merge a source entity note into a target entity note: adds alias, appends content, updates wikilinks, deletes source",
12447
+ {
12448
+ source_path: z16.string().describe("Vault-relative path of the note to merge FROM (will be deleted)"),
12449
+ target_path: z16.string().describe("Vault-relative path of the note to merge INTO (receives alias + content)")
12450
+ },
12451
+ async ({ source_path, target_path }) => {
12452
+ try {
12453
+ if (!validatePath(vaultPath2, source_path)) {
12454
+ const result2 = {
12455
+ success: false,
12456
+ message: "Invalid source path: path traversal not allowed",
12457
+ path: source_path
12458
+ };
12459
+ return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
12460
+ }
12461
+ if (!validatePath(vaultPath2, target_path)) {
12462
+ const result2 = {
12463
+ success: false,
12464
+ message: "Invalid target path: path traversal not allowed",
12465
+ path: target_path
12466
+ };
12467
+ return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
12468
+ }
12469
+ let sourceContent;
12470
+ let sourceFrontmatter;
12471
+ try {
12472
+ const source = await readVaultFile(vaultPath2, source_path);
12473
+ sourceContent = source.content;
12474
+ sourceFrontmatter = source.frontmatter;
12475
+ } catch {
12476
+ const result2 = {
12477
+ success: false,
12478
+ message: `Source file not found: ${source_path}`,
12479
+ path: source_path
12480
+ };
12481
+ return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
12482
+ }
12483
+ let targetContent;
12484
+ let targetFrontmatter;
12485
+ try {
12486
+ const target = await readVaultFile(vaultPath2, target_path);
12487
+ targetContent = target.content;
12488
+ targetFrontmatter = target.frontmatter;
12489
+ } catch {
12490
+ const result2 = {
12491
+ success: false,
12492
+ message: `Target file not found: ${target_path}`,
12493
+ path: target_path
12494
+ };
12495
+ return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
12496
+ }
12497
+ const sourceTitle = getTitleFromPath(source_path);
12498
+ const targetTitle = getTitleFromPath(target_path);
12499
+ const existingAliases = extractAliases2(targetFrontmatter);
12500
+ const sourceAliases = extractAliases2(sourceFrontmatter);
12501
+ const allNewAliases = [sourceTitle, ...sourceAliases];
12502
+ const deduped = /* @__PURE__ */ new Set([...existingAliases]);
12503
+ for (const alias of allNewAliases) {
12504
+ if (alias.toLowerCase() !== targetTitle.toLowerCase()) {
12505
+ deduped.add(alias);
12506
+ }
12507
+ }
12508
+ targetFrontmatter.aliases = Array.from(deduped);
12509
+ const trimmedSource = sourceContent.trim();
12510
+ if (trimmedSource.length > 10) {
12511
+ const mergedSection = `
12512
+
12513
+ ## Merged from ${sourceTitle}
12514
+
12515
+ ${trimmedSource}`;
12516
+ targetContent = targetContent.trimEnd() + mergedSection;
12517
+ }
12518
+ const allSourceTitles = [sourceTitle, ...sourceAliases];
12519
+ const backlinks = await findBacklinks(vaultPath2, sourceTitle, sourceAliases);
12520
+ let totalBacklinksUpdated = 0;
12521
+ const modifiedFiles = [];
12522
+ for (const backlink of backlinks) {
12523
+ if (backlink.path === source_path || backlink.path === target_path) continue;
12524
+ const updateResult = await updateBacklinksInFile(
12525
+ vaultPath2,
12526
+ backlink.path,
12527
+ allSourceTitles,
12528
+ targetTitle
12529
+ );
12530
+ if (updateResult.updated) {
12531
+ totalBacklinksUpdated += updateResult.linksUpdated;
12532
+ modifiedFiles.push(backlink.path);
12533
+ }
12534
+ }
12535
+ await writeVaultFile(vaultPath2, target_path, targetContent, targetFrontmatter);
12536
+ const fullSourcePath = `${vaultPath2}/${source_path}`;
12537
+ await fs23.unlink(fullSourcePath);
12538
+ initializeEntityIndex(vaultPath2).catch((err) => {
12539
+ console.error(`[Flywheel] Entity cache rebuild failed: ${err}`);
12540
+ });
12541
+ const previewLines = [
12542
+ `Merged: "${sourceTitle}" \u2192 "${targetTitle}"`,
12543
+ `Aliases added: ${allNewAliases.join(", ")}`,
12544
+ `Source content appended: ${trimmedSource.length > 10 ? "yes" : "no"}`,
12545
+ `Backlinks updated: ${totalBacklinksUpdated}`
12546
+ ];
12547
+ if (modifiedFiles.length > 0) {
12548
+ previewLines.push(`Files modified: ${modifiedFiles.join(", ")}`);
12549
+ }
12550
+ const result = {
12551
+ success: true,
12552
+ message: `Merged "${sourceTitle}" into "${targetTitle}"`,
12553
+ path: target_path,
12554
+ preview: previewLines.join("\n"),
12555
+ backlinks_updated: totalBacklinksUpdated
12556
+ };
12557
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
12558
+ } catch (error) {
12559
+ const result = {
12560
+ success: false,
12561
+ message: `Failed to merge entities: ${error instanceof Error ? error.message : String(error)}`,
12562
+ path: source_path
12563
+ };
12564
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
12565
+ }
12566
+ }
12567
+ );
12568
+ }
12569
+
12570
+ // src/tools/write/system.ts
12571
+ import { z as z17 } from "zod";
11915
12572
  function registerSystemTools2(server2, vaultPath2) {
11916
12573
  server2.tool(
11917
12574
  "vault_undo_last_mutation",
11918
12575
  "Undo the last git commit (typically the last Flywheel mutation). Performs a soft reset.",
11919
12576
  {
11920
- confirm: z16.boolean().default(false).describe("Must be true to confirm undo operation"),
11921
- hash: z16.string().optional().describe("Expected commit hash. If provided, undo only proceeds if HEAD matches this hash. Prevents accidentally undoing the wrong commit.")
12577
+ confirm: z17.boolean().default(false).describe("Must be true to confirm undo operation"),
12578
+ hash: z17.string().optional().describe("Expected commit hash. If provided, undo only proceeds if HEAD matches this hash. Prevents accidentally undoing the wrong commit.")
11922
12579
  },
11923
12580
  async ({ confirm, hash }) => {
11924
12581
  try {
@@ -12019,7 +12676,7 @@ Message: ${undoResult.undoneCommit.message}` : void 0
12019
12676
  }
12020
12677
 
12021
12678
  // src/tools/write/policy.ts
12022
- import { z as z18 } from "zod";
12679
+ import { z as z19 } from "zod";
12023
12680
 
12024
12681
  // src/core/write/policy/index.ts
12025
12682
  init_template();
@@ -12027,8 +12684,8 @@ init_schema();
12027
12684
 
12028
12685
  // src/core/write/policy/parser.ts
12029
12686
  init_schema();
12030
- import fs23 from "fs/promises";
12031
- import path22 from "path";
12687
+ import fs24 from "fs/promises";
12688
+ import path23 from "path";
12032
12689
  import matter7 from "gray-matter";
12033
12690
  function parseYaml(content) {
12034
12691
  const parsed = matter7(`---
@@ -12053,7 +12710,7 @@ function parsePolicyString(yamlContent) {
12053
12710
  }
12054
12711
  async function loadPolicyFile(filePath) {
12055
12712
  try {
12056
- const content = await fs23.readFile(filePath, "utf-8");
12713
+ const content = await fs24.readFile(filePath, "utf-8");
12057
12714
  return parsePolicyString(content);
12058
12715
  } catch (error) {
12059
12716
  if (error.code === "ENOENT") {
@@ -12077,15 +12734,15 @@ async function loadPolicyFile(filePath) {
12077
12734
  }
12078
12735
  }
12079
12736
  async function loadPolicy(vaultPath2, policyName) {
12080
- const policiesDir = path22.join(vaultPath2, ".claude", "policies");
12081
- const policyPath = path22.join(policiesDir, `${policyName}.yaml`);
12737
+ const policiesDir = path23.join(vaultPath2, ".claude", "policies");
12738
+ const policyPath = path23.join(policiesDir, `${policyName}.yaml`);
12082
12739
  try {
12083
- await fs23.access(policyPath);
12740
+ await fs24.access(policyPath);
12084
12741
  return loadPolicyFile(policyPath);
12085
12742
  } catch {
12086
- const ymlPath = path22.join(policiesDir, `${policyName}.yml`);
12743
+ const ymlPath = path23.join(policiesDir, `${policyName}.yml`);
12087
12744
  try {
12088
- await fs23.access(ymlPath);
12745
+ await fs24.access(ymlPath);
12089
12746
  return loadPolicyFile(ymlPath);
12090
12747
  } catch {
12091
12748
  return {
@@ -12223,8 +12880,8 @@ init_template();
12223
12880
  init_conditions();
12224
12881
  init_schema();
12225
12882
  init_writer();
12226
- import fs25 from "fs/promises";
12227
- import path24 from "path";
12883
+ import fs26 from "fs/promises";
12884
+ import path25 from "path";
12228
12885
  init_constants();
12229
12886
  async function executeStep(step, vaultPath2, context, conditionResults) {
12230
12887
  const { execute, reason } = shouldStepExecute(step.when, conditionResults);
@@ -12293,9 +12950,9 @@ async function executeAddToSection(params, vaultPath2, context) {
12293
12950
  const preserveListNesting = params.preserveListNesting !== false;
12294
12951
  const suggestOutgoingLinks = params.suggestOutgoingLinks !== false;
12295
12952
  const maxSuggestions = Number(params.maxSuggestions) || 3;
12296
- const fullPath = path24.join(vaultPath2, notePath);
12953
+ const fullPath = path25.join(vaultPath2, notePath);
12297
12954
  try {
12298
- await fs25.access(fullPath);
12955
+ await fs26.access(fullPath);
12299
12956
  } catch {
12300
12957
  return { success: false, message: `File not found: ${notePath}`, path: notePath };
12301
12958
  }
@@ -12333,9 +12990,9 @@ async function executeRemoveFromSection(params, vaultPath2) {
12333
12990
  const pattern = String(params.pattern || "");
12334
12991
  const mode = params.mode || "first";
12335
12992
  const useRegex = Boolean(params.useRegex);
12336
- const fullPath = path24.join(vaultPath2, notePath);
12993
+ const fullPath = path25.join(vaultPath2, notePath);
12337
12994
  try {
12338
- await fs25.access(fullPath);
12995
+ await fs26.access(fullPath);
12339
12996
  } catch {
12340
12997
  return { success: false, message: `File not found: ${notePath}`, path: notePath };
12341
12998
  }
@@ -12364,9 +13021,9 @@ async function executeReplaceInSection(params, vaultPath2, context) {
12364
13021
  const mode = params.mode || "first";
12365
13022
  const useRegex = Boolean(params.useRegex);
12366
13023
  const skipWikilinks = Boolean(params.skipWikilinks);
12367
- const fullPath = path24.join(vaultPath2, notePath);
13024
+ const fullPath = path25.join(vaultPath2, notePath);
12368
13025
  try {
12369
- await fs25.access(fullPath);
13026
+ await fs26.access(fullPath);
12370
13027
  } catch {
12371
13028
  return { success: false, message: `File not found: ${notePath}`, path: notePath };
12372
13029
  }
@@ -12407,16 +13064,16 @@ async function executeCreateNote(params, vaultPath2, context) {
12407
13064
  if (!validatePath(vaultPath2, notePath)) {
12408
13065
  return { success: false, message: "Invalid path: path traversal not allowed", path: notePath };
12409
13066
  }
12410
- const fullPath = path24.join(vaultPath2, notePath);
13067
+ const fullPath = path25.join(vaultPath2, notePath);
12411
13068
  try {
12412
- await fs25.access(fullPath);
13069
+ await fs26.access(fullPath);
12413
13070
  if (!overwrite) {
12414
13071
  return { success: false, message: `File already exists: ${notePath}`, path: notePath };
12415
13072
  }
12416
13073
  } catch {
12417
13074
  }
12418
- const dir = path24.dirname(fullPath);
12419
- await fs25.mkdir(dir, { recursive: true });
13075
+ const dir = path25.dirname(fullPath);
13076
+ await fs26.mkdir(dir, { recursive: true });
12420
13077
  const { content: processedContent } = maybeApplyWikilinks(content, skipWikilinks, notePath);
12421
13078
  await writeVaultFile(vaultPath2, notePath, processedContent, frontmatter);
12422
13079
  return {
@@ -12435,13 +13092,13 @@ async function executeDeleteNote(params, vaultPath2) {
12435
13092
  if (!validatePath(vaultPath2, notePath)) {
12436
13093
  return { success: false, message: "Invalid path: path traversal not allowed", path: notePath };
12437
13094
  }
12438
- const fullPath = path24.join(vaultPath2, notePath);
13095
+ const fullPath = path25.join(vaultPath2, notePath);
12439
13096
  try {
12440
- await fs25.access(fullPath);
13097
+ await fs26.access(fullPath);
12441
13098
  } catch {
12442
13099
  return { success: false, message: `File not found: ${notePath}`, path: notePath };
12443
13100
  }
12444
- await fs25.unlink(fullPath);
13101
+ await fs26.unlink(fullPath);
12445
13102
  return {
12446
13103
  success: true,
12447
13104
  message: `Deleted note: ${notePath}`,
@@ -12452,9 +13109,9 @@ async function executeToggleTask(params, vaultPath2) {
12452
13109
  const notePath = String(params.path || "");
12453
13110
  const task = String(params.task || "");
12454
13111
  const section = params.section ? String(params.section) : void 0;
12455
- const fullPath = path24.join(vaultPath2, notePath);
13112
+ const fullPath = path25.join(vaultPath2, notePath);
12456
13113
  try {
12457
- await fs25.access(fullPath);
13114
+ await fs26.access(fullPath);
12458
13115
  } catch {
12459
13116
  return { success: false, message: `File not found: ${notePath}`, path: notePath };
12460
13117
  }
@@ -12495,9 +13152,9 @@ async function executeAddTask(params, vaultPath2, context) {
12495
13152
  const completed = Boolean(params.completed);
12496
13153
  const skipWikilinks = Boolean(params.skipWikilinks);
12497
13154
  const preserveListNesting = params.preserveListNesting !== false;
12498
- const fullPath = path24.join(vaultPath2, notePath);
13155
+ const fullPath = path25.join(vaultPath2, notePath);
12499
13156
  try {
12500
- await fs25.access(fullPath);
13157
+ await fs26.access(fullPath);
12501
13158
  } catch {
12502
13159
  return { success: false, message: `File not found: ${notePath}`, path: notePath };
12503
13160
  }
@@ -12532,9 +13189,9 @@ async function executeAddTask(params, vaultPath2, context) {
12532
13189
  async function executeUpdateFrontmatter(params, vaultPath2) {
12533
13190
  const notePath = String(params.path || "");
12534
13191
  const updates = params.frontmatter || {};
12535
- const fullPath = path24.join(vaultPath2, notePath);
13192
+ const fullPath = path25.join(vaultPath2, notePath);
12536
13193
  try {
12537
- await fs25.access(fullPath);
13194
+ await fs26.access(fullPath);
12538
13195
  } catch {
12539
13196
  return { success: false, message: `File not found: ${notePath}`, path: notePath };
12540
13197
  }
@@ -12554,9 +13211,9 @@ async function executeAddFrontmatterField(params, vaultPath2) {
12554
13211
  const notePath = String(params.path || "");
12555
13212
  const key = String(params.key || "");
12556
13213
  const value = params.value;
12557
- const fullPath = path24.join(vaultPath2, notePath);
13214
+ const fullPath = path25.join(vaultPath2, notePath);
12558
13215
  try {
12559
- await fs25.access(fullPath);
13216
+ await fs26.access(fullPath);
12560
13217
  } catch {
12561
13218
  return { success: false, message: `File not found: ${notePath}`, path: notePath };
12562
13219
  }
@@ -12714,15 +13371,15 @@ async function executePolicy(policy, vaultPath2, variables, commit = false) {
12714
13371
  async function rollbackChanges(vaultPath2, originalContents, filesModified) {
12715
13372
  for (const filePath of filesModified) {
12716
13373
  const original = originalContents.get(filePath);
12717
- const fullPath = path24.join(vaultPath2, filePath);
13374
+ const fullPath = path25.join(vaultPath2, filePath);
12718
13375
  if (original === null) {
12719
13376
  try {
12720
- await fs25.unlink(fullPath);
13377
+ await fs26.unlink(fullPath);
12721
13378
  } catch {
12722
13379
  }
12723
13380
  } else if (original !== void 0) {
12724
13381
  try {
12725
- await fs25.writeFile(fullPath, original);
13382
+ await fs26.writeFile(fullPath, original);
12726
13383
  } catch {
12727
13384
  }
12728
13385
  }
@@ -12768,27 +13425,27 @@ async function previewPolicy(policy, vaultPath2, variables) {
12768
13425
  }
12769
13426
 
12770
13427
  // src/core/write/policy/storage.ts
12771
- import fs26 from "fs/promises";
12772
- import path25 from "path";
13428
+ import fs27 from "fs/promises";
13429
+ import path26 from "path";
12773
13430
  function getPoliciesDir(vaultPath2) {
12774
- return path25.join(vaultPath2, ".claude", "policies");
13431
+ return path26.join(vaultPath2, ".claude", "policies");
12775
13432
  }
12776
13433
  async function ensurePoliciesDir(vaultPath2) {
12777
13434
  const dir = getPoliciesDir(vaultPath2);
12778
- await fs26.mkdir(dir, { recursive: true });
13435
+ await fs27.mkdir(dir, { recursive: true });
12779
13436
  }
12780
13437
  async function listPolicies(vaultPath2) {
12781
13438
  const dir = getPoliciesDir(vaultPath2);
12782
13439
  const policies = [];
12783
13440
  try {
12784
- const files = await fs26.readdir(dir);
13441
+ const files = await fs27.readdir(dir);
12785
13442
  for (const file of files) {
12786
13443
  if (!file.endsWith(".yaml") && !file.endsWith(".yml")) {
12787
13444
  continue;
12788
13445
  }
12789
- const filePath = path25.join(dir, file);
12790
- const stat3 = await fs26.stat(filePath);
12791
- const content = await fs26.readFile(filePath, "utf-8");
13446
+ const filePath = path26.join(dir, file);
13447
+ const stat3 = await fs27.stat(filePath);
13448
+ const content = await fs27.readFile(filePath, "utf-8");
12792
13449
  const metadata = extractPolicyMetadata(content);
12793
13450
  policies.push({
12794
13451
  name: metadata.name || file.replace(/\.ya?ml$/, ""),
@@ -12811,10 +13468,10 @@ async function writePolicyRaw(vaultPath2, policyName, content, overwrite = false
12811
13468
  const dir = getPoliciesDir(vaultPath2);
12812
13469
  await ensurePoliciesDir(vaultPath2);
12813
13470
  const filename = `${policyName}.yaml`;
12814
- const filePath = path25.join(dir, filename);
13471
+ const filePath = path26.join(dir, filename);
12815
13472
  if (!overwrite) {
12816
13473
  try {
12817
- await fs26.access(filePath);
13474
+ await fs27.access(filePath);
12818
13475
  return {
12819
13476
  success: false,
12820
13477
  path: filename,
@@ -12831,7 +13488,7 @@ async function writePolicyRaw(vaultPath2, policyName, content, overwrite = false
12831
13488
  message: `Invalid policy: ${validation.errors.map((e) => e.message).join("; ")}`
12832
13489
  };
12833
13490
  }
12834
- await fs26.writeFile(filePath, content, "utf-8");
13491
+ await fs27.writeFile(filePath, content, "utf-8");
12835
13492
  return {
12836
13493
  success: true,
12837
13494
  path: filename,
@@ -12846,71 +13503,71 @@ function registerPolicyTools(server2, vaultPath2) {
12846
13503
  "policy",
12847
13504
  'Manage vault policies. Actions: "list" (list all policies), "validate" (validate YAML), "preview" (dry-run), "execute" (run policy), "author" (generate policy YAML), "revise" (modify existing policy).',
12848
13505
  {
12849
- action: z18.enum(["list", "validate", "preview", "execute", "author", "revise"]).describe("Action to perform"),
13506
+ action: z19.enum(["list", "validate", "preview", "execute", "author", "revise"]).describe("Action to perform"),
12850
13507
  // validate
12851
- yaml: z18.string().optional().describe('Policy YAML content (required for "validate")'),
13508
+ yaml: z19.string().optional().describe('Policy YAML content (required for "validate")'),
12852
13509
  // preview, execute, revise
12853
- policy: z18.string().optional().describe('Policy name or full YAML content (required for "preview", "execute", "revise")'),
13510
+ policy: z19.string().optional().describe('Policy name or full YAML content (required for "preview", "execute", "revise")'),
12854
13511
  // preview, execute
12855
- variables: z18.record(z18.unknown()).optional().describe('Variables to pass to the policy (for "preview", "execute")'),
13512
+ variables: z19.record(z19.unknown()).optional().describe('Variables to pass to the policy (for "preview", "execute")'),
12856
13513
  // execute
12857
- commit: z18.boolean().optional().describe('If true, commit all changes with single atomic commit (for "execute")'),
13514
+ commit: z19.boolean().optional().describe('If true, commit all changes with single atomic commit (for "execute")'),
12858
13515
  // author
12859
- name: z18.string().optional().describe('Name for the policy (required for "author")'),
12860
- description: z18.string().optional().describe('Description of what the policy should do (required for "author")'),
12861
- steps: z18.array(z18.object({
12862
- tool: z18.string().describe("Tool to call (e.g., vault_add_to_section)"),
12863
- description: z18.string().describe("What this step does"),
12864
- params: z18.record(z18.unknown()).describe("Parameters for the tool")
13516
+ name: z19.string().optional().describe('Name for the policy (required for "author")'),
13517
+ description: z19.string().optional().describe('Description of what the policy should do (required for "author")'),
13518
+ steps: z19.array(z19.object({
13519
+ tool: z19.string().describe("Tool to call (e.g., vault_add_to_section)"),
13520
+ description: z19.string().describe("What this step does"),
13521
+ params: z19.record(z19.unknown()).describe("Parameters for the tool")
12865
13522
  })).optional().describe('Steps the policy should perform (required for "author")'),
12866
- authorVariables: z18.array(z18.object({
12867
- name: z18.string().describe("Variable name"),
12868
- type: z18.enum(["string", "number", "boolean", "array", "enum"]).describe("Variable type"),
12869
- required: z18.boolean().default(true).describe("Whether variable is required"),
12870
- default: z18.unknown().optional().describe("Default value"),
12871
- enum: z18.array(z18.string()).optional().describe("Allowed values for enum type"),
12872
- description: z18.string().optional().describe("Variable description")
13523
+ authorVariables: z19.array(z19.object({
13524
+ name: z19.string().describe("Variable name"),
13525
+ type: z19.enum(["string", "number", "boolean", "array", "enum"]).describe("Variable type"),
13526
+ required: z19.boolean().default(true).describe("Whether variable is required"),
13527
+ default: z19.unknown().optional().describe("Default value"),
13528
+ enum: z19.array(z19.string()).optional().describe("Allowed values for enum type"),
13529
+ description: z19.string().optional().describe("Variable description")
12873
13530
  })).optional().describe('Variables the policy accepts (for "author")'),
12874
- conditions: z18.array(z18.object({
12875
- id: z18.string().describe("Condition ID"),
12876
- check: z18.string().describe("Condition type (file_exists, section_exists, etc.)"),
12877
- path: z18.string().optional().describe("File path"),
12878
- section: z18.string().optional().describe("Section name"),
12879
- field: z18.string().optional().describe("Frontmatter field"),
12880
- value: z18.unknown().optional().describe("Expected value")
13531
+ conditions: z19.array(z19.object({
13532
+ id: z19.string().describe("Condition ID"),
13533
+ check: z19.string().describe("Condition type (file_exists, section_exists, etc.)"),
13534
+ path: z19.string().optional().describe("File path"),
13535
+ section: z19.string().optional().describe("Section name"),
13536
+ field: z19.string().optional().describe("Frontmatter field"),
13537
+ value: z19.unknown().optional().describe("Expected value")
12881
13538
  })).optional().describe('Conditions for conditional execution (for "author")'),
12882
13539
  // author, revise
12883
- save: z18.boolean().optional().describe('If true, save to .claude/policies/ (for "author", "revise")'),
13540
+ save: z19.boolean().optional().describe('If true, save to .claude/policies/ (for "author", "revise")'),
12884
13541
  // revise
12885
- changes: z18.object({
12886
- description: z18.string().optional().describe("New description"),
12887
- addVariables: z18.array(z18.object({
12888
- name: z18.string(),
12889
- type: z18.enum(["string", "number", "boolean", "array", "enum"]),
12890
- required: z18.boolean().default(true),
12891
- default: z18.unknown().optional(),
12892
- enum: z18.array(z18.string()).optional(),
12893
- description: z18.string().optional()
13542
+ changes: z19.object({
13543
+ description: z19.string().optional().describe("New description"),
13544
+ addVariables: z19.array(z19.object({
13545
+ name: z19.string(),
13546
+ type: z19.enum(["string", "number", "boolean", "array", "enum"]),
13547
+ required: z19.boolean().default(true),
13548
+ default: z19.unknown().optional(),
13549
+ enum: z19.array(z19.string()).optional(),
13550
+ description: z19.string().optional()
12894
13551
  })).optional().describe("Variables to add"),
12895
- removeVariables: z18.array(z18.string()).optional().describe("Variable names to remove"),
12896
- addSteps: z18.array(z18.object({
12897
- id: z18.string(),
12898
- tool: z18.string(),
12899
- params: z18.record(z18.unknown()),
12900
- when: z18.string().optional(),
12901
- description: z18.string().optional(),
12902
- afterStep: z18.string().optional().describe("Insert after this step ID")
13552
+ removeVariables: z19.array(z19.string()).optional().describe("Variable names to remove"),
13553
+ addSteps: z19.array(z19.object({
13554
+ id: z19.string(),
13555
+ tool: z19.string(),
13556
+ params: z19.record(z19.unknown()),
13557
+ when: z19.string().optional(),
13558
+ description: z19.string().optional(),
13559
+ afterStep: z19.string().optional().describe("Insert after this step ID")
12903
13560
  })).optional().describe("Steps to add"),
12904
- removeSteps: z18.array(z18.string()).optional().describe("Step IDs to remove"),
12905
- addConditions: z18.array(z18.object({
12906
- id: z18.string(),
12907
- check: z18.string(),
12908
- path: z18.string().optional(),
12909
- section: z18.string().optional(),
12910
- field: z18.string().optional(),
12911
- value: z18.unknown().optional()
13561
+ removeSteps: z19.array(z19.string()).optional().describe("Step IDs to remove"),
13562
+ addConditions: z19.array(z19.object({
13563
+ id: z19.string(),
13564
+ check: z19.string(),
13565
+ path: z19.string().optional(),
13566
+ section: z19.string().optional(),
13567
+ field: z19.string().optional(),
13568
+ value: z19.unknown().optional()
12912
13569
  })).optional().describe("Conditions to add"),
12913
- removeConditions: z18.array(z18.string()).optional().describe("Condition IDs to remove")
13570
+ removeConditions: z19.array(z19.string()).optional().describe("Condition IDs to remove")
12914
13571
  }).optional().describe('Changes to make (required for "revise")')
12915
13572
  },
12916
13573
  async (params) => {
@@ -13351,11 +14008,11 @@ function registerPolicyTools(server2, vaultPath2) {
13351
14008
  }
13352
14009
 
13353
14010
  // src/tools/write/tags.ts
13354
- import { z as z19 } from "zod";
14011
+ import { z as z20 } from "zod";
13355
14012
 
13356
14013
  // src/core/write/tagRename.ts
13357
- import * as fs27 from "fs/promises";
13358
- import * as path26 from "path";
14014
+ import * as fs28 from "fs/promises";
14015
+ import * as path27 from "path";
13359
14016
  import matter8 from "gray-matter";
13360
14017
  import { getProtectedZones } from "@velvetmonkey/vault-core";
13361
14018
  function getNotesInFolder3(index, folder) {
@@ -13461,10 +14118,10 @@ async function renameTag(index, vaultPath2, oldTag, newTag, options) {
13461
14118
  const previews = [];
13462
14119
  let totalChanges = 0;
13463
14120
  for (const note of affectedNotes) {
13464
- const fullPath = path26.join(vaultPath2, note.path);
14121
+ const fullPath = path27.join(vaultPath2, note.path);
13465
14122
  let fileContent;
13466
14123
  try {
13467
- fileContent = await fs27.readFile(fullPath, "utf-8");
14124
+ fileContent = await fs28.readFile(fullPath, "utf-8");
13468
14125
  } catch {
13469
14126
  continue;
13470
14127
  }
@@ -13537,7 +14194,7 @@ async function renameTag(index, vaultPath2, oldTag, newTag, options) {
13537
14194
  previews.push(preview);
13538
14195
  if (!dryRun) {
13539
14196
  const newContent = matter8.stringify(updatedContent, fm);
13540
- await fs27.writeFile(fullPath, newContent, "utf-8");
14197
+ await fs28.writeFile(fullPath, newContent, "utf-8");
13541
14198
  }
13542
14199
  }
13543
14200
  }
@@ -13560,12 +14217,12 @@ function registerTagTools(server2, getIndex, getVaultPath) {
13560
14217
  title: "Rename Tag",
13561
14218
  description: "Bulk rename a tag across all notes (frontmatter and inline). Supports hierarchical rename (#project \u2192 #work also transforms #project/active \u2192 #work/active). Dry-run by default (preview only). Handles deduplication when new tag already exists.",
13562
14219
  inputSchema: {
13563
- old_tag: z19.string().describe('Tag to rename (without #, e.g., "project")'),
13564
- new_tag: z19.string().describe('New tag name (without #, e.g., "work")'),
13565
- rename_children: z19.boolean().optional().describe("Also rename child tags (e.g., #project/active \u2192 #work/active). Default: true"),
13566
- folder: z19.string().optional().describe('Limit to notes in this folder (e.g., "projects")'),
13567
- dry_run: z19.boolean().optional().describe("Preview only, no changes (default: true)"),
13568
- commit: z19.boolean().optional().describe("Commit changes to git (default: false)")
14220
+ old_tag: z20.string().describe('Tag to rename (without #, e.g., "project")'),
14221
+ new_tag: z20.string().describe('New tag name (without #, e.g., "work")'),
14222
+ rename_children: z20.boolean().optional().describe("Also rename child tags (e.g., #project/active \u2192 #work/active). Default: true"),
14223
+ folder: z20.string().optional().describe('Limit to notes in this folder (e.g., "projects")'),
14224
+ dry_run: z20.boolean().optional().describe("Preview only, no changes (default: true)"),
14225
+ commit: z20.boolean().optional().describe("Commit changes to git (default: false)")
13569
14226
  }
13570
14227
  },
13571
14228
  async ({ old_tag, new_tag, rename_children, folder, dry_run, commit }) => {
@@ -13590,20 +14247,20 @@ function registerTagTools(server2, getIndex, getVaultPath) {
13590
14247
  }
13591
14248
 
13592
14249
  // src/tools/write/wikilinkFeedback.ts
13593
- import { z as z20 } from "zod";
14250
+ import { z as z21 } from "zod";
13594
14251
  function registerWikilinkFeedbackTools(server2, getStateDb) {
13595
14252
  server2.registerTool(
13596
14253
  "wikilink_feedback",
13597
14254
  {
13598
14255
  title: "Wikilink Feedback",
13599
- description: 'Report and query wikilink accuracy feedback. Modes: "report" (record feedback), "list" (view recent feedback), "stats" (entity accuracy statistics). Entities with >=30% false positive rate (and >=10 samples) are auto-suppressed from future wikilink application.',
14256
+ description: 'Report and query wikilink accuracy feedback. Modes: "report" (record feedback), "list" (view recent feedback), "stats" (entity accuracy statistics), "dashboard" (full feedback loop data for visualization). Entities with >=30% false positive rate (and >=10 samples) are auto-suppressed from future wikilink application.',
13600
14257
  inputSchema: {
13601
- mode: z20.enum(["report", "list", "stats"]).describe("Operation mode"),
13602
- entity: z20.string().optional().describe("Entity name (required for report mode, optional filter for list/stats)"),
13603
- note_path: z20.string().optional().describe("Note path where the wikilink appeared (for report mode)"),
13604
- context: z20.string().optional().describe("Surrounding text context (for report mode)"),
13605
- correct: z20.boolean().optional().describe("Whether the wikilink was correct (for report mode)"),
13606
- limit: z20.number().optional().describe("Max entries to return for list mode (default: 20)")
14258
+ mode: z21.enum(["report", "list", "stats", "dashboard"]).describe("Operation mode"),
14259
+ entity: z21.string().optional().describe("Entity name (required for report mode, optional filter for list/stats)"),
14260
+ note_path: z21.string().optional().describe("Note path where the wikilink appeared (for report mode)"),
14261
+ context: z21.string().optional().describe("Surrounding text context (for report mode)"),
14262
+ correct: z21.boolean().optional().describe("Whether the wikilink was correct (for report mode)"),
14263
+ limit: z21.number().optional().describe("Max entries to return for list mode (default: 20)")
13607
14264
  }
13608
14265
  },
13609
14266
  async ({ mode, entity, note_path, context, correct, limit }) => {
@@ -13653,6 +14310,16 @@ function registerWikilinkFeedbackTools(server2, getStateDb) {
13653
14310
  };
13654
14311
  break;
13655
14312
  }
14313
+ case "dashboard": {
14314
+ const dashboard = getDashboardData(stateDb2);
14315
+ result = {
14316
+ mode: "dashboard",
14317
+ dashboard,
14318
+ total_feedback: dashboard.total_feedback,
14319
+ total_suppressed: dashboard.total_suppressed
14320
+ };
14321
+ break;
14322
+ }
13656
14323
  }
13657
14324
  return {
13658
14325
  content: [
@@ -13666,8 +14333,57 @@ function registerWikilinkFeedbackTools(server2, getStateDb) {
13666
14333
  );
13667
14334
  }
13668
14335
 
14336
+ // src/tools/write/config.ts
14337
+ import { z as z22 } from "zod";
14338
+ import { saveFlywheelConfigToDb as saveFlywheelConfigToDb2 } from "@velvetmonkey/vault-core";
14339
+ function registerConfigTools(server2, getConfig, setConfig, getStateDb) {
14340
+ server2.registerTool(
14341
+ "flywheel_config",
14342
+ {
14343
+ title: "Flywheel Config",
14344
+ description: 'Read or update Flywheel configuration.\n- "get": Returns the current FlywheelConfig\n- "set": Updates a single config key and returns the updated config\n\nExample: flywheel_config({ mode: "get" })\nExample: flywheel_config({ mode: "set", key: "exclude_analysis_tags", value: ["habit", "daily"] })',
14345
+ inputSchema: {
14346
+ mode: z22.enum(["get", "set"]).describe("Operation mode"),
14347
+ key: z22.string().optional().describe("Config key to update (required for set mode)"),
14348
+ value: z22.unknown().optional().describe("New value for the key (required for set mode)")
14349
+ }
14350
+ },
14351
+ async ({ mode, key, value }) => {
14352
+ switch (mode) {
14353
+ case "get": {
14354
+ const config = getConfig();
14355
+ return {
14356
+ content: [{ type: "text", text: JSON.stringify(config, null, 2) }]
14357
+ };
14358
+ }
14359
+ case "set": {
14360
+ if (!key) {
14361
+ return {
14362
+ content: [{ type: "text", text: JSON.stringify({ error: "key is required for set mode" }) }]
14363
+ };
14364
+ }
14365
+ const stateDb2 = getStateDb();
14366
+ if (!stateDb2) {
14367
+ return {
14368
+ content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
14369
+ };
14370
+ }
14371
+ const current = getConfig();
14372
+ const updated = { ...current, [key]: value };
14373
+ saveFlywheelConfigToDb2(stateDb2, updated);
14374
+ const reloaded = loadConfig(stateDb2);
14375
+ setConfig(reloaded);
14376
+ return {
14377
+ content: [{ type: "text", text: JSON.stringify(reloaded, null, 2) }]
14378
+ };
14379
+ }
14380
+ }
14381
+ }
14382
+ );
14383
+ }
14384
+
13669
14385
  // src/tools/read/metrics.ts
13670
- import { z as z21 } from "zod";
14386
+ import { z as z23 } from "zod";
13671
14387
 
13672
14388
  // src/core/shared/metrics.ts
13673
14389
  var ALL_METRICS = [
@@ -13833,10 +14549,10 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
13833
14549
  title: "Vault Growth",
13834
14550
  description: 'Track vault growth over time. Modes: "current" (live snapshot), "history" (time series), "trends" (deltas vs N days ago), "index_activity" (rebuild history). Tracks 11 metrics: note_count, link_count, orphan_count, tag_count, entity_count, avg_links_per_note, link_density, connected_ratio, wikilink_accuracy, wikilink_feedback_volume, wikilink_suppressed_count.',
13835
14551
  inputSchema: {
13836
- mode: z21.enum(["current", "history", "trends", "index_activity"]).describe("Query mode: current snapshot, historical time series, trend analysis, or index rebuild activity"),
13837
- metric: z21.string().optional().describe('Filter to specific metric (e.g., "note_count"). Omit for all metrics.'),
13838
- days_back: z21.number().optional().describe("Number of days to look back for history/trends (default: 30)"),
13839
- limit: z21.number().optional().describe("Number of recent events to return for index_activity mode (default: 20)")
14552
+ mode: z23.enum(["current", "history", "trends", "index_activity"]).describe("Query mode: current snapshot, historical time series, trend analysis, or index rebuild activity"),
14553
+ metric: z23.string().optional().describe('Filter to specific metric (e.g., "note_count"). Omit for all metrics.'),
14554
+ days_back: z23.number().optional().describe("Number of days to look back for history/trends (default: 30)"),
14555
+ limit: z23.number().optional().describe("Number of recent events to return for index_activity mode (default: 20)")
13840
14556
  }
13841
14557
  },
13842
14558
  async ({ mode, metric, days_back, limit: eventLimit }) => {
@@ -13909,7 +14625,7 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
13909
14625
  }
13910
14626
 
13911
14627
  // src/tools/read/activity.ts
13912
- import { z as z22 } from "zod";
14628
+ import { z as z24 } from "zod";
13913
14629
 
13914
14630
  // src/core/shared/toolTracking.ts
13915
14631
  function recordToolInvocation(stateDb2, event) {
@@ -13989,8 +14705,8 @@ function getNoteAccessFrequency(stateDb2, daysBack = 30) {
13989
14705
  }
13990
14706
  }
13991
14707
  }
13992
- return Array.from(noteMap.entries()).map(([path29, stats]) => ({
13993
- path: path29,
14708
+ return Array.from(noteMap.entries()).map(([path30, stats]) => ({
14709
+ path: path30,
13994
14710
  access_count: stats.access_count,
13995
14711
  last_accessed: stats.last_accessed,
13996
14712
  tools_used: Array.from(stats.tools)
@@ -14069,10 +14785,10 @@ function registerActivityTools(server2, getStateDb, getSessionId2) {
14069
14785
  title: "Vault Activity",
14070
14786
  description: 'Track tool usage patterns and session activity. Modes:\n- "session": Current session summary (tools called, notes accessed)\n- "sessions": List of recent sessions\n- "note_access": Notes ranked by query frequency\n- "tool_usage": Tool usage patterns (most-used tools, avg duration)',
14071
14787
  inputSchema: {
14072
- mode: z22.enum(["session", "sessions", "note_access", "tool_usage"]).describe("Activity query mode"),
14073
- session_id: z22.string().optional().describe("Specific session ID (for session mode, defaults to current)"),
14074
- days_back: z22.number().optional().describe("Number of days to look back (default: 30)"),
14075
- limit: z22.number().optional().describe("Maximum results to return (default: 20)")
14788
+ mode: z24.enum(["session", "sessions", "note_access", "tool_usage"]).describe("Activity query mode"),
14789
+ session_id: z24.string().optional().describe("Specific session ID (for session mode, defaults to current)"),
14790
+ days_back: z24.number().optional().describe("Number of days to look back (default: 30)"),
14791
+ limit: z24.number().optional().describe("Maximum results to return (default: 20)")
14076
14792
  }
14077
14793
  },
14078
14794
  async ({ mode, session_id, days_back, limit: resultLimit }) => {
@@ -14139,11 +14855,11 @@ function registerActivityTools(server2, getStateDb, getSessionId2) {
14139
14855
  }
14140
14856
 
14141
14857
  // src/tools/read/similarity.ts
14142
- import { z as z23 } from "zod";
14858
+ import { z as z25 } from "zod";
14143
14859
 
14144
14860
  // src/core/read/similarity.ts
14145
- import * as fs28 from "fs";
14146
- import * as path27 from "path";
14861
+ import * as fs29 from "fs";
14862
+ import * as path28 from "path";
14147
14863
  var STOP_WORDS = /* @__PURE__ */ new Set([
14148
14864
  "the",
14149
14865
  "be",
@@ -14278,12 +14994,12 @@ function extractKeyTerms(content, maxTerms = 15) {
14278
14994
  }
14279
14995
  return Array.from(freq.entries()).sort((a, b) => b[1] - a[1]).slice(0, maxTerms).map(([word]) => word);
14280
14996
  }
14281
- function findSimilarNotes(db3, vaultPath2, index, sourcePath, options = {}) {
14997
+ function findSimilarNotes(db4, vaultPath2, index, sourcePath, options = {}) {
14282
14998
  const limit = options.limit ?? 10;
14283
- const absPath = path27.join(vaultPath2, sourcePath);
14999
+ const absPath = path28.join(vaultPath2, sourcePath);
14284
15000
  let content;
14285
15001
  try {
14286
- content = fs28.readFileSync(absPath, "utf-8");
15002
+ content = fs29.readFileSync(absPath, "utf-8");
14287
15003
  } catch {
14288
15004
  return [];
14289
15005
  }
@@ -14291,7 +15007,7 @@ function findSimilarNotes(db3, vaultPath2, index, sourcePath, options = {}) {
14291
15007
  if (terms.length === 0) return [];
14292
15008
  const query = terms.join(" OR ");
14293
15009
  try {
14294
- const results = db3.prepare(`
15010
+ const results = db4.prepare(`
14295
15011
  SELECT
14296
15012
  path,
14297
15013
  title,
@@ -14359,9 +15075,9 @@ async function findSemanticSimilarNotes(vaultPath2, index, sourcePath, options =
14359
15075
  // Semantic results don't have snippets
14360
15076
  }));
14361
15077
  }
14362
- async function findHybridSimilarNotes(db3, vaultPath2, index, sourcePath, options = {}) {
15078
+ async function findHybridSimilarNotes(db4, vaultPath2, index, sourcePath, options = {}) {
14363
15079
  const limit = options.limit ?? 10;
14364
- const bm25Results = findSimilarNotes(db3, vaultPath2, index, sourcePath, {
15080
+ const bm25Results = findSimilarNotes(db4, vaultPath2, index, sourcePath, {
14365
15081
  limit: limit * 2,
14366
15082
  excludeLinked: options.excludeLinked
14367
15083
  });
@@ -14403,12 +15119,12 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
14403
15119
  title: "Find Similar Notes",
14404
15120
  description: "Find notes similar to a given note using FTS5 keyword matching. When embeddings have been built (via init_semantic), automatically uses hybrid ranking (BM25 + embedding similarity via Reciprocal Rank Fusion). Use exclude_linked to filter out notes already connected via wikilinks.",
14405
15121
  inputSchema: {
14406
- path: z23.string().describe('Path to the source note (relative to vault root, e.g. "projects/alpha.md")'),
14407
- limit: z23.number().optional().describe("Maximum number of similar notes to return (default: 10)"),
14408
- exclude_linked: z23.boolean().optional().describe("Exclude notes already linked to/from the source note (default: true)")
15122
+ path: z25.string().describe('Path to the source note (relative to vault root, e.g. "projects/alpha.md")'),
15123
+ limit: z25.number().optional().describe("Maximum number of similar notes to return (default: 10)"),
15124
+ exclude_linked: z25.boolean().optional().describe("Exclude notes already linked to/from the source note (default: true)")
14409
15125
  }
14410
15126
  },
14411
- async ({ path: path29, limit, exclude_linked }) => {
15127
+ async ({ path: path30, limit, exclude_linked }) => {
14412
15128
  const index = getIndex();
14413
15129
  const vaultPath2 = getVaultPath();
14414
15130
  const stateDb2 = getStateDb();
@@ -14417,10 +15133,10 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
14417
15133
  content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
14418
15134
  };
14419
15135
  }
14420
- if (!index.notes.has(path29)) {
15136
+ if (!index.notes.has(path30)) {
14421
15137
  return {
14422
15138
  content: [{ type: "text", text: JSON.stringify({
14423
- error: `Note not found: ${path29}`,
15139
+ error: `Note not found: ${path30}`,
14424
15140
  hint: "Use the full relative path including .md extension"
14425
15141
  }, null, 2) }]
14426
15142
  };
@@ -14431,12 +15147,12 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
14431
15147
  };
14432
15148
  const useHybrid = hasEmbeddingsIndex();
14433
15149
  const method = useHybrid ? "hybrid" : "bm25";
14434
- const results = useHybrid ? await findHybridSimilarNotes(stateDb2.db, vaultPath2, index, path29, opts) : findSimilarNotes(stateDb2.db, vaultPath2, index, path29, opts);
15150
+ const results = useHybrid ? await findHybridSimilarNotes(stateDb2.db, vaultPath2, index, path30, opts) : findSimilarNotes(stateDb2.db, vaultPath2, index, path30, opts);
14435
15151
  return {
14436
15152
  content: [{
14437
15153
  type: "text",
14438
15154
  text: JSON.stringify({
14439
- source: path29,
15155
+ source: path30,
14440
15156
  method,
14441
15157
  exclude_linked: exclude_linked ?? true,
14442
15158
  count: results.length,
@@ -14449,7 +15165,7 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
14449
15165
  }
14450
15166
 
14451
15167
  // src/tools/read/semantic.ts
14452
- import { z as z24 } from "zod";
15168
+ import { z as z26 } from "zod";
14453
15169
  import { getAllEntitiesFromDb } from "@velvetmonkey/vault-core";
14454
15170
  function registerSemanticTools(server2, getVaultPath, getStateDb) {
14455
15171
  server2.registerTool(
@@ -14458,7 +15174,7 @@ function registerSemanticTools(server2, getVaultPath, getStateDb) {
14458
15174
  title: "Initialize Semantic Search",
14459
15175
  description: "Download the embedding model and build semantic search index for this vault. After running, search and find_similar automatically use hybrid ranking (BM25 + semantic). Run once per vault \u2014 subsequent calls skip already-embedded notes unless force=true.",
14460
15176
  inputSchema: {
14461
- force: z24.boolean().optional().describe(
15177
+ force: z26.boolean().optional().describe(
14462
15178
  "Rebuild all embeddings even if they already exist (default: false)"
14463
15179
  )
14464
15180
  }
@@ -14536,6 +15252,142 @@ function registerSemanticTools(server2, getVaultPath, getStateDb) {
14536
15252
  );
14537
15253
  }
14538
15254
 
15255
+ // src/tools/read/merges.ts
15256
+ init_levenshtein();
15257
+ import { z as z27 } from "zod";
15258
+ import { getAllEntitiesFromDb as getAllEntitiesFromDb2, getDismissedMergePairs, recordMergeDismissal } from "@velvetmonkey/vault-core";
15259
+ function normalizeName(name) {
15260
+ return name.toLowerCase().replace(/[.\-_]/g, "").replace(/js$/, "").replace(/ts$/, "");
15261
+ }
15262
+ function registerMergeTools2(server2, getStateDb) {
15263
+ server2.tool(
15264
+ "suggest_entity_merges",
15265
+ "Find potential duplicate entities that could be merged based on name similarity",
15266
+ {
15267
+ limit: z27.number().optional().default(50).describe("Maximum number of suggestions to return")
15268
+ },
15269
+ async ({ limit }) => {
15270
+ const stateDb2 = getStateDb();
15271
+ if (!stateDb2) {
15272
+ return {
15273
+ content: [{ type: "text", text: JSON.stringify({ suggestions: [], error: "StateDb not available" }) }]
15274
+ };
15275
+ }
15276
+ const entities = getAllEntitiesFromDb2(stateDb2);
15277
+ if (entities.length === 0) {
15278
+ return {
15279
+ content: [{ type: "text", text: JSON.stringify({ suggestions: [] }) }]
15280
+ };
15281
+ }
15282
+ const dismissedPairs = getDismissedMergePairs(stateDb2);
15283
+ const suggestions = [];
15284
+ const seen = /* @__PURE__ */ new Set();
15285
+ for (let i = 0; i < entities.length; i++) {
15286
+ for (let j = i + 1; j < entities.length; j++) {
15287
+ const a = entities[i];
15288
+ const b = entities[j];
15289
+ if (a.path === b.path) continue;
15290
+ const pairKey = [a.path, b.path].sort().join("::");
15291
+ if (seen.has(pairKey)) continue;
15292
+ if (dismissedPairs.has(pairKey)) continue;
15293
+ const aLower = a.name.toLowerCase();
15294
+ const bLower = b.name.toLowerCase();
15295
+ const aNorm = normalizeName(a.name);
15296
+ const bNorm = normalizeName(b.name);
15297
+ let reason = "";
15298
+ let confidence = 0;
15299
+ if (aLower === bLower) {
15300
+ reason = "exact name match (case-insensitive)";
15301
+ confidence = 0.95;
15302
+ } else if (aNorm === bNorm && aNorm.length >= 3) {
15303
+ reason = "normalized name match";
15304
+ confidence = 0.85;
15305
+ } else if (aLower.length >= 3 && bLower.length >= 3) {
15306
+ if (aLower.includes(bLower) || bLower.includes(aLower)) {
15307
+ const shorter = aLower.length <= bLower.length ? aLower : bLower;
15308
+ const longer = aLower.length > bLower.length ? aLower : bLower;
15309
+ const ratio = shorter.length / longer.length;
15310
+ if (ratio > 0.5) {
15311
+ reason = "substring match";
15312
+ confidence = 0.6 + ratio * 0.2;
15313
+ }
15314
+ }
15315
+ }
15316
+ if (!reason && aLower.length >= 4 && bLower.length >= 4) {
15317
+ const maxLen = Math.max(aLower.length, bLower.length);
15318
+ const dist = levenshteinDistance(aLower, bLower);
15319
+ const ratio = dist / maxLen;
15320
+ if (ratio < 0.35) {
15321
+ reason = `similar name (edit distance ${dist})`;
15322
+ confidence = 0.5 + (1 - ratio) * 0.4;
15323
+ }
15324
+ }
15325
+ if (!reason) continue;
15326
+ seen.add(pairKey);
15327
+ const aHub = a.hubScore ?? 0;
15328
+ const bHub = b.hubScore ?? 0;
15329
+ let source = a;
15330
+ let target = b;
15331
+ if (aHub > bHub || aHub === bHub && a.name.length > b.name.length) {
15332
+ source = b;
15333
+ target = a;
15334
+ }
15335
+ suggestions.push({
15336
+ source: {
15337
+ name: source.name,
15338
+ path: source.path,
15339
+ category: source.category,
15340
+ hubScore: source.hubScore ?? 0,
15341
+ aliases: source.aliases ?? []
15342
+ },
15343
+ target: {
15344
+ name: target.name,
15345
+ path: target.path,
15346
+ category: target.category,
15347
+ hubScore: target.hubScore ?? 0,
15348
+ aliases: target.aliases ?? []
15349
+ },
15350
+ reason,
15351
+ confidence
15352
+ });
15353
+ }
15354
+ }
15355
+ suggestions.sort((a, b) => b.confidence - a.confidence);
15356
+ const result = {
15357
+ suggestions: suggestions.slice(0, limit),
15358
+ total_candidates: suggestions.length
15359
+ };
15360
+ return {
15361
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
15362
+ };
15363
+ }
15364
+ );
15365
+ server2.tool(
15366
+ "dismiss_merge_suggestion",
15367
+ "Permanently dismiss a merge suggestion so it never reappears",
15368
+ {
15369
+ source_path: z27.string().describe("Path of the source entity"),
15370
+ target_path: z27.string().describe("Path of the target entity"),
15371
+ source_name: z27.string().describe("Name of the source entity"),
15372
+ target_name: z27.string().describe("Name of the target entity"),
15373
+ reason: z27.string().describe("Original suggestion reason")
15374
+ },
15375
+ async ({ source_path, target_path, source_name, target_name, reason }) => {
15376
+ const stateDb2 = getStateDb();
15377
+ if (!stateDb2) {
15378
+ return {
15379
+ content: [{ type: "text", text: JSON.stringify({ dismissed: false, error: "StateDb not available" }) }]
15380
+ };
15381
+ }
15382
+ recordMergeDismissal(stateDb2, source_path, target_path, source_name, target_name, reason);
15383
+ const pairKey = [source_path, target_path].sort().join("::");
15384
+ return {
15385
+ content: [{ type: "text", text: JSON.stringify({ dismissed: true, pair_key: pairKey }) }]
15386
+ };
15387
+ }
15388
+ );
15389
+ }
15390
+
14539
15391
  // src/resources/vault.ts
14540
15392
  function registerVaultResources(server2, getIndex) {
14541
15393
  server2.registerResource(
@@ -14708,11 +15560,11 @@ function parseEnabledCategories() {
14708
15560
  categories.add(c);
14709
15561
  }
14710
15562
  } else {
14711
- console.error(`[Memory] Warning: Unknown tool category "${item}" - ignoring`);
15563
+ serverLog("server", `Unknown tool category "${item}" \u2014 ignoring`, "warn");
14712
15564
  }
14713
15565
  }
14714
15566
  if (categories.size === 0) {
14715
- console.error(`[Memory] No valid categories found, using default (${DEFAULT_PRESET})`);
15567
+ serverLog("server", `No valid categories found, using default (${DEFAULT_PRESET})`, "warn");
14716
15568
  return new Set(PRESETS[DEFAULT_PRESET]);
14717
15569
  }
14718
15570
  return categories;
@@ -14781,7 +15633,16 @@ var TOOL_CATEGORY = {
14781
15633
  // health (activity tracking)
14782
15634
  vault_activity: "health",
14783
15635
  // schema (content similarity)
14784
- find_similar: "schema"
15636
+ find_similar: "schema",
15637
+ // health (config management)
15638
+ flywheel_config: "health",
15639
+ // health (server activity log)
15640
+ server_log: "health",
15641
+ // health (merge suggestions)
15642
+ suggest_entity_merges: "health",
15643
+ dismiss_merge_suggestion: "health",
15644
+ // notes (entity merge)
15645
+ merge_entities: "notes"
14785
15646
  };
14786
15647
  var server = new McpServer({
14787
15648
  name: "flywheel-memory",
@@ -14858,7 +15719,7 @@ if (_originalRegisterTool) {
14858
15719
  };
14859
15720
  }
14860
15721
  var categoryList = Array.from(enabledCategories).sort().join(", ");
14861
- console.error(`[Memory] Tool categories: ${categoryList}`);
15722
+ serverLog("server", `Tool categories: ${categoryList}`);
14862
15723
  registerHealthTools(server, () => vaultIndex, () => vaultPath, () => flywheelConfig, () => stateDb);
14863
15724
  registerSystemTools(
14864
15725
  server,
@@ -14876,19 +15737,28 @@ registerGraphTools(server, () => vaultIndex, () => vaultPath);
14876
15737
  registerWikilinkTools(server, () => vaultIndex, () => vaultPath);
14877
15738
  registerQueryTools(server, () => vaultIndex, () => vaultPath, () => stateDb);
14878
15739
  registerPrimitiveTools(server, () => vaultIndex, () => vaultPath, () => flywheelConfig);
14879
- registerGraphAnalysisTools(server, () => vaultIndex, () => vaultPath, () => stateDb);
15740
+ registerGraphAnalysisTools(server, () => vaultIndex, () => vaultPath, () => stateDb, () => flywheelConfig);
14880
15741
  registerVaultSchemaTools(server, () => vaultIndex, () => vaultPath);
14881
- registerNoteIntelligenceTools(server, () => vaultIndex, () => vaultPath);
15742
+ registerNoteIntelligenceTools(server, () => vaultIndex, () => vaultPath, () => flywheelConfig);
14882
15743
  registerMigrationTools(server, () => vaultIndex, () => vaultPath);
14883
15744
  registerMutationTools(server, vaultPath, () => flywheelConfig);
14884
15745
  registerTaskTools(server, vaultPath);
14885
15746
  registerFrontmatterTools(server, vaultPath);
14886
15747
  registerNoteTools(server, vaultPath, () => vaultIndex);
14887
15748
  registerMoveNoteTools(server, vaultPath);
15749
+ registerMergeTools(server, vaultPath);
14888
15750
  registerSystemTools2(server, vaultPath);
14889
15751
  registerPolicyTools(server, vaultPath);
14890
15752
  registerTagTools(server, () => vaultIndex, () => vaultPath);
14891
15753
  registerWikilinkFeedbackTools(server, () => stateDb);
15754
+ registerConfigTools(
15755
+ server,
15756
+ () => flywheelConfig,
15757
+ (newConfig) => {
15758
+ flywheelConfig = newConfig;
15759
+ },
15760
+ () => stateDb
15761
+ );
14892
15762
  registerMetricsTools(server, () => vaultIndex, () => stateDb);
14893
15763
  registerActivityTools(server, () => stateDb, () => {
14894
15764
  try {
@@ -14899,66 +15769,68 @@ registerActivityTools(server, () => stateDb, () => {
14899
15769
  });
14900
15770
  registerSimilarityTools(server, () => vaultIndex, () => vaultPath, () => stateDb);
14901
15771
  registerSemanticTools(server, () => vaultPath, () => stateDb);
15772
+ registerMergeTools2(server, () => stateDb);
14902
15773
  registerVaultResources(server, () => vaultIndex ?? null);
14903
- console.error(`[Memory] Registered ${_registeredCount} tools, skipped ${_skippedCount}`);
15774
+ serverLog("server", `Registered ${_registeredCount} tools, skipped ${_skippedCount}`);
14904
15775
  async function main() {
14905
- console.error(`[Memory] Starting Flywheel Memory server...`);
14906
- console.error(`[Memory] Vault: ${vaultPath}`);
15776
+ serverLog("server", "Starting Flywheel Memory server...");
15777
+ serverLog("server", `Vault: ${vaultPath}`);
14907
15778
  const startTime = Date.now();
14908
15779
  try {
14909
15780
  stateDb = openStateDb(vaultPath);
14910
- console.error("[Memory] StateDb initialized");
15781
+ serverLog("statedb", "StateDb initialized");
14911
15782
  setFTS5Database(stateDb.db);
14912
15783
  setEmbeddingsDatabase(stateDb.db);
15784
+ setTaskCacheDatabase(stateDb.db);
14913
15785
  loadEntityEmbeddingsToMemory();
14914
15786
  setWriteStateDb(stateDb);
14915
15787
  } catch (err) {
14916
15788
  const msg = err instanceof Error ? err.message : String(err);
14917
- console.error(`[Memory] StateDb initialization failed: ${msg}`);
14918
- console.error("[Memory] Auto-wikilinks will be disabled for this session");
15789
+ serverLog("statedb", `StateDb initialization failed: ${msg}`, "error");
15790
+ serverLog("server", "Auto-wikilinks will be disabled for this session", "warn");
14919
15791
  }
14920
15792
  const transport = new StdioServerTransport();
14921
15793
  await server.connect(transport);
14922
- console.error("[Memory] MCP server connected");
15794
+ serverLog("server", "MCP server connected");
14923
15795
  initializeLogger(vaultPath).then(() => {
14924
15796
  const logger3 = getLogger();
14925
15797
  if (logger3?.enabled) {
14926
- console.error(`[Memory] Unified logging enabled`);
15798
+ serverLog("server", "Unified logging enabled");
14927
15799
  }
14928
15800
  }).catch(() => {
14929
15801
  });
14930
15802
  initializeLogger2(vaultPath).catch((err) => {
14931
- console.error(`[Memory] Write logger initialization failed: ${err}`);
15803
+ serverLog("server", `Write logger initialization failed: ${err}`, "error");
14932
15804
  });
14933
15805
  if (process.env.FLYWHEEL_SKIP_FTS5 !== "true") {
14934
15806
  if (isIndexStale(vaultPath)) {
14935
15807
  buildFTS5Index(vaultPath).then(() => {
14936
- console.error("[Memory] FTS5 search index ready");
15808
+ serverLog("fts5", "Search index ready");
14937
15809
  }).catch((err) => {
14938
- console.error("[Memory] FTS5 build failed:", err);
15810
+ serverLog("fts5", `Build failed: ${err instanceof Error ? err.message : err}`, "error");
14939
15811
  });
14940
15812
  } else {
14941
- console.error("[Memory] FTS5 search index already fresh, skipping rebuild");
15813
+ serverLog("fts5", "Search index already fresh, skipping rebuild");
14942
15814
  }
14943
15815
  } else {
14944
- console.error("[Memory] FTS5 indexing skipped (FLYWHEEL_SKIP_FTS5=true)");
15816
+ serverLog("fts5", "Skipping \u2014 FLYWHEEL_SKIP_FTS5");
14945
15817
  }
14946
15818
  let cachedIndex = null;
14947
15819
  if (stateDb) {
14948
15820
  try {
14949
15821
  const files = await scanVault(vaultPath);
14950
15822
  const noteCount = files.length;
14951
- console.error(`[Memory] Found ${noteCount} markdown files`);
15823
+ serverLog("index", `Found ${noteCount} markdown files`);
14952
15824
  cachedIndex = loadVaultIndexFromCache(stateDb, noteCount);
14953
15825
  } catch (err) {
14954
- console.error("[Memory] Cache check failed:", err);
15826
+ serverLog("index", `Cache check failed: ${err instanceof Error ? err.message : err}`, "warn");
14955
15827
  }
14956
15828
  }
14957
15829
  if (cachedIndex) {
14958
15830
  vaultIndex = cachedIndex;
14959
15831
  setIndexState("ready");
14960
15832
  const duration = Date.now() - startTime;
14961
- console.error(`[Memory] Index loaded from cache in ${duration}ms`);
15833
+ serverLog("index", `Loaded from cache in ${duration}ms \u2014 ${cachedIndex.notes.size} notes`);
14962
15834
  if (stateDb) {
14963
15835
  recordIndexEvent(stateDb, {
14964
15836
  trigger: "startup_cache",
@@ -14968,12 +15840,12 @@ async function main() {
14968
15840
  }
14969
15841
  runPostIndexWork(vaultIndex);
14970
15842
  } else {
14971
- console.error("[Memory] Building vault index...");
15843
+ serverLog("index", "Building vault index...");
14972
15844
  try {
14973
15845
  vaultIndex = await buildVaultIndex(vaultPath);
14974
15846
  setIndexState("ready");
14975
15847
  const duration = Date.now() - startTime;
14976
- console.error(`[Memory] Vault index ready in ${duration}ms`);
15848
+ serverLog("index", `Vault index ready in ${duration}ms \u2014 ${vaultIndex.notes.size} notes`);
14977
15849
  if (stateDb) {
14978
15850
  recordIndexEvent(stateDb, {
14979
15851
  trigger: "startup_build",
@@ -14984,9 +15856,9 @@ async function main() {
14984
15856
  if (stateDb) {
14985
15857
  try {
14986
15858
  saveVaultIndexToCache(stateDb, vaultIndex);
14987
- console.error("[Memory] Index cache saved");
15859
+ serverLog("index", "Index cache saved");
14988
15860
  } catch (err) {
14989
- console.error("[Memory] Failed to save index cache:", err);
15861
+ serverLog("index", `Failed to save index cache: ${err instanceof Error ? err.message : err}`, "error");
14990
15862
  }
14991
15863
  }
14992
15864
  await runPostIndexWork(vaultIndex);
@@ -15002,7 +15874,7 @@ async function main() {
15002
15874
  error: err instanceof Error ? err.message : String(err)
15003
15875
  });
15004
15876
  }
15005
- console.error("[Memory] Failed to build vault index:", err);
15877
+ serverLog("index", `Failed to build vault index: ${err instanceof Error ? err.message : err}`, "error");
15006
15878
  }
15007
15879
  }
15008
15880
  }
@@ -15033,9 +15905,9 @@ async function updateEntitiesInStateDb() {
15033
15905
  ]
15034
15906
  });
15035
15907
  stateDb.replaceAllEntities(entityIndex2);
15036
- console.error(`[Memory] Updated ${entityIndex2._metadata.total_entities} entities in StateDb`);
15908
+ serverLog("index", `Updated ${entityIndex2._metadata.total_entities} entities in StateDb`);
15037
15909
  } catch (e) {
15038
- console.error("[Memory] Failed to update entities in StateDb:", e);
15910
+ serverLog("index", `Failed to update entities in StateDb: ${e instanceof Error ? e.message : e}`, "error");
15039
15911
  }
15040
15912
  }
15041
15913
  async function runPostIndexWork(index) {
@@ -15049,9 +15921,9 @@ async function runPostIndexWork(index) {
15049
15921
  purgeOldMetrics(stateDb, 90);
15050
15922
  purgeOldIndexEvents(stateDb, 90);
15051
15923
  purgeOldInvocations(stateDb, 90);
15052
- console.error("[Memory] Growth metrics recorded");
15924
+ serverLog("server", "Growth metrics recorded");
15053
15925
  } catch (err) {
15054
- console.error("[Memory] Failed to record metrics:", err);
15926
+ serverLog("server", `Failed to record metrics: ${err instanceof Error ? err.message : err}`, "error");
15055
15927
  }
15056
15928
  }
15057
15929
  if (stateDb) {
@@ -15060,14 +15932,14 @@ async function runPostIndexWork(index) {
15060
15932
  recordGraphSnapshot(stateDb, graphMetrics);
15061
15933
  purgeOldSnapshots(stateDb, 90);
15062
15934
  } catch (err) {
15063
- console.error("[Memory] Failed to record graph snapshot:", err);
15935
+ serverLog("server", `Failed to record graph snapshot: ${err instanceof Error ? err.message : err}`, "error");
15064
15936
  }
15065
15937
  }
15066
15938
  if (stateDb) {
15067
15939
  try {
15068
15940
  updateSuppressionList(stateDb);
15069
15941
  } catch (err) {
15070
- console.error("[Memory] Failed to update suppression list:", err);
15942
+ serverLog("server", `Failed to update suppression list: ${err instanceof Error ? err.message : err}`, "error");
15071
15943
  }
15072
15944
  }
15073
15945
  const existing = loadConfig(stateDb);
@@ -15076,21 +15948,25 @@ async function runPostIndexWork(index) {
15076
15948
  saveConfig(stateDb, inferred, existing);
15077
15949
  }
15078
15950
  flywheelConfig = loadConfig(stateDb);
15951
+ if (stateDb) {
15952
+ refreshIfStale(vaultPath, index, flywheelConfig.exclude_task_tags);
15953
+ serverLog("tasks", "Task cache ready");
15954
+ }
15079
15955
  if (flywheelConfig.vault_name) {
15080
- console.error(`[Memory] Vault: ${flywheelConfig.vault_name}`);
15956
+ serverLog("config", `Vault: ${flywheelConfig.vault_name}`);
15081
15957
  }
15082
15958
  if (process.env.FLYWHEEL_SKIP_EMBEDDINGS !== "true") {
15083
15959
  if (hasEmbeddingsIndex()) {
15084
- console.error("[Memory] Embeddings already built, skipping full scan");
15960
+ serverLog("semantic", "Embeddings already built, skipping full scan");
15085
15961
  } else {
15086
15962
  setEmbeddingsBuilding(true);
15087
15963
  buildEmbeddingsIndex(vaultPath, (p) => {
15088
15964
  if (p.current % 100 === 0 || p.current === p.total) {
15089
- console.error(`[Semantic] ${p.current}/${p.total}`);
15965
+ serverLog("semantic", `Embedding ${p.current}/${p.total} notes...`);
15090
15966
  }
15091
15967
  }).then(async () => {
15092
15968
  if (stateDb) {
15093
- const entities = getAllEntitiesFromDb2(stateDb);
15969
+ const entities = getAllEntitiesFromDb3(stateDb);
15094
15970
  if (entities.length > 0) {
15095
15971
  const entityMap = new Map(entities.map((e) => [e.name, {
15096
15972
  name: e.name,
@@ -15102,29 +15978,29 @@ async function runPostIndexWork(index) {
15102
15978
  }
15103
15979
  }
15104
15980
  loadEntityEmbeddingsToMemory();
15105
- console.error("[Memory] Embeddings refreshed");
15981
+ serverLog("semantic", "Embeddings ready");
15106
15982
  }).catch((err) => {
15107
- console.error("[Memory] Embeddings refresh failed:", err);
15983
+ serverLog("semantic", `Embeddings build failed: ${err instanceof Error ? err.message : err}`, "error");
15108
15984
  });
15109
15985
  }
15110
15986
  } else {
15111
- console.error("[Memory] Embeddings skipped (FLYWHEEL_SKIP_EMBEDDINGS=true)");
15987
+ serverLog("semantic", "Skipping \u2014 FLYWHEEL_SKIP_EMBEDDINGS");
15112
15988
  }
15113
15989
  if (process.env.FLYWHEEL_WATCH !== "false") {
15114
15990
  const config = parseWatcherConfig();
15115
- console.error(`[Memory] File watcher enabled (debounce: ${config.debounceMs}ms)`);
15991
+ serverLog("watcher", `File watcher enabled (debounce: ${config.debounceMs}ms)`);
15116
15992
  const watcher = createVaultWatcher({
15117
15993
  vaultPath,
15118
15994
  config,
15119
15995
  onBatch: async (batch) => {
15120
- console.error(`[Memory] Processing ${batch.events.length} file changes`);
15996
+ serverLog("watcher", `Processing ${batch.events.length} file changes`);
15121
15997
  const batchStart = Date.now();
15122
15998
  const changedPaths = batch.events.map((e) => e.path);
15123
15999
  try {
15124
16000
  vaultIndex = await buildVaultIndex(vaultPath);
15125
16001
  setIndexState("ready");
15126
16002
  const duration = Date.now() - batchStart;
15127
- console.error(`[Memory] Index rebuilt in ${duration}ms`);
16003
+ serverLog("watcher", `Index rebuilt in ${duration}ms`);
15128
16004
  if (stateDb) {
15129
16005
  recordIndexEvent(stateDb, {
15130
16006
  trigger: "watcher",
@@ -15142,7 +16018,7 @@ async function runPostIndexWork(index) {
15142
16018
  if (event.type === "delete") {
15143
16019
  removeEmbedding(event.path);
15144
16020
  } else if (event.path.endsWith(".md")) {
15145
- const absPath = path28.join(vaultPath, event.path);
16021
+ const absPath = path29.join(vaultPath, event.path);
15146
16022
  await updateEmbedding(event.path, absPath);
15147
16023
  }
15148
16024
  } catch {
@@ -15151,7 +16027,7 @@ async function runPostIndexWork(index) {
15151
16027
  }
15152
16028
  if (hasEntityEmbeddingsIndex() && stateDb) {
15153
16029
  try {
15154
- const allEntities = getAllEntitiesFromDb2(stateDb);
16030
+ const allEntities = getAllEntitiesFromDb3(stateDb);
15155
16031
  for (const event of batch.events) {
15156
16032
  if (event.type === "delete" || !event.path.endsWith(".md")) continue;
15157
16033
  const matching = allEntities.filter((e) => e.path === event.path);
@@ -15171,7 +16047,17 @@ async function runPostIndexWork(index) {
15171
16047
  try {
15172
16048
  saveVaultIndexToCache(stateDb, vaultIndex);
15173
16049
  } catch (err) {
15174
- console.error("[Memory] Failed to update index cache:", err);
16050
+ serverLog("index", `Failed to update index cache: ${err instanceof Error ? err.message : err}`, "error");
16051
+ }
16052
+ }
16053
+ for (const event of batch.events) {
16054
+ try {
16055
+ if (event.type === "delete") {
16056
+ removeTaskCacheForFile(event.path);
16057
+ } else if (event.path.endsWith(".md")) {
16058
+ await updateTaskCacheForFile(vaultPath, event.path);
16059
+ }
16060
+ } catch {
15175
16061
  }
15176
16062
  }
15177
16063
  } catch (err) {
@@ -15188,16 +16074,16 @@ async function runPostIndexWork(index) {
15188
16074
  error: err instanceof Error ? err.message : String(err)
15189
16075
  });
15190
16076
  }
15191
- console.error("[Memory] Failed to rebuild index:", err);
16077
+ serverLog("watcher", `Failed to rebuild index: ${err instanceof Error ? err.message : err}`, "error");
15192
16078
  }
15193
16079
  },
15194
16080
  onStateChange: (status) => {
15195
16081
  if (status.state === "dirty") {
15196
- console.error("[Memory] Warning: Index may be stale");
16082
+ serverLog("watcher", "Index may be stale", "warn");
15197
16083
  }
15198
16084
  },
15199
16085
  onError: (err) => {
15200
- console.error("[Memory] Watcher error:", err.message);
16086
+ serverLog("watcher", `Watcher error: ${err.message}`, "error");
15201
16087
  }
15202
16088
  });
15203
16089
  watcher.start();
@@ -15208,15 +16094,15 @@ if (process.argv.includes("--init-semantic")) {
15208
16094
  console.error("[Semantic] Pre-warming semantic search...");
15209
16095
  console.error(`[Semantic] Vault: ${vaultPath}`);
15210
16096
  try {
15211
- const db3 = openStateDb(vaultPath);
15212
- setEmbeddingsDatabase(db3.db);
16097
+ const db4 = openStateDb(vaultPath);
16098
+ setEmbeddingsDatabase(db4.db);
15213
16099
  const progress = await buildEmbeddingsIndex(vaultPath, (p) => {
15214
16100
  if (p.current % 50 === 0 || p.current === p.total) {
15215
16101
  console.error(`[Semantic] Embedding ${p.current}/${p.total} notes (${p.skipped} skipped)...`);
15216
16102
  }
15217
16103
  });
15218
16104
  console.error(`[Semantic] Done. Embedded ${progress.total - progress.skipped} notes, skipped ${progress.skipped}.`);
15219
- db3.close();
16105
+ db4.close();
15220
16106
  process.exit(0);
15221
16107
  } catch (err) {
15222
16108
  console.error("[Semantic] Failed:", err instanceof Error ? err.message : err);