@velvetmonkey/flywheel-memory 2.0.27 → 2.0.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +1399 -631
  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,7 +475,7 @@ 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);
478
+ const fullPath = path18.join(vaultPath2, notePath);
479
479
  const rawContent = await fs18.readFile(fullPath, "utf-8");
480
480
  const lineEnding = detectLineEnding(rawContent);
481
481
  const normalizedContent = normalizeLineEndings(rawContent);
@@ -524,7 +524,7 @@ async function writeVaultFile(vaultPath2, notePath, content, frontmatter, lineEn
524
524
  if (!validation.valid) {
525
525
  throw new Error(`Invalid path: ${validation.reason}`);
526
526
  }
527
- const fullPath = path17.join(vaultPath2, notePath);
527
+ const fullPath = path18.join(vaultPath2, notePath);
528
528
  let output = matter5.stringify(content, frontmatter);
529
529
  output = normalizeTrailingNewline(output);
530
530
  output = convertLineEndings(output, lineEnding);
@@ -831,8 +831,8 @@ function createContext(variables = {}) {
831
831
  }
832
832
  };
833
833
  }
834
- function resolvePath(obj, path29) {
835
- const parts = path29.split(".");
834
+ function resolvePath(obj, path30) {
835
+ const parts = path30.split(".");
836
836
  let current = obj;
837
837
  for (const part of parts) {
838
838
  if (current === void 0 || current === null) {
@@ -984,7 +984,7 @@ __export(schema_exports, {
984
984
  validatePolicySchema: () => validatePolicySchema,
985
985
  validateVariables: () => validateVariables
986
986
  });
987
- import { z as z17 } from "zod";
987
+ import { z as z18 } from "zod";
988
988
  function validatePolicySchema(policy) {
989
989
  const errors = [];
990
990
  const warnings = [];
@@ -1179,13 +1179,13 @@ var PolicyVariableTypeSchema, PolicyVariableSchema, ConditionCheckTypeSchema, Po
1179
1179
  var init_schema = __esm({
1180
1180
  "src/core/write/policy/schema.ts"() {
1181
1181
  "use strict";
1182
- PolicyVariableTypeSchema = z17.enum(["string", "number", "boolean", "array", "enum"]);
1183
- PolicyVariableSchema = z17.object({
1182
+ PolicyVariableTypeSchema = z18.enum(["string", "number", "boolean", "array", "enum"]);
1183
+ PolicyVariableSchema = z18.object({
1184
1184
  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()
1185
+ required: z18.boolean().optional(),
1186
+ default: z18.union([z18.string(), z18.number(), z18.boolean(), z18.array(z18.string())]).optional(),
1187
+ enum: z18.array(z18.string()).optional(),
1188
+ description: z18.string().optional()
1189
1189
  }).refine(
1190
1190
  (data) => {
1191
1191
  if (data.type === "enum" && (!data.enum || data.enum.length === 0)) {
@@ -1195,7 +1195,7 @@ var init_schema = __esm({
1195
1195
  },
1196
1196
  { message: "Enum type requires a non-empty enum array" }
1197
1197
  );
1198
- ConditionCheckTypeSchema = z17.enum([
1198
+ ConditionCheckTypeSchema = z18.enum([
1199
1199
  "file_exists",
1200
1200
  "file_not_exists",
1201
1201
  "section_exists",
@@ -1204,13 +1204,13 @@ var init_schema = __esm({
1204
1204
  "frontmatter_exists",
1205
1205
  "frontmatter_not_exists"
1206
1206
  ]);
1207
- PolicyConditionSchema = z17.object({
1208
- id: z17.string().min(1, "Condition id is required"),
1207
+ PolicyConditionSchema = z18.object({
1208
+ id: z18.string().min(1, "Condition id is required"),
1209
1209
  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()
1210
+ path: z18.string().optional(),
1211
+ section: z18.string().optional(),
1212
+ field: z18.string().optional(),
1213
+ value: z18.union([z18.string(), z18.number(), z18.boolean()]).optional()
1214
1214
  }).refine(
1215
1215
  (data) => {
1216
1216
  if (["file_exists", "file_not_exists"].includes(data.check) && !data.path) {
@@ -1229,7 +1229,7 @@ var init_schema = __esm({
1229
1229
  },
1230
1230
  { message: "Condition is missing required fields for its check type" }
1231
1231
  );
1232
- PolicyToolNameSchema = z17.enum([
1232
+ PolicyToolNameSchema = z18.enum([
1233
1233
  "vault_add_to_section",
1234
1234
  "vault_remove_from_section",
1235
1235
  "vault_replace_in_section",
@@ -1240,24 +1240,24 @@ var init_schema = __esm({
1240
1240
  "vault_update_frontmatter",
1241
1241
  "vault_add_frontmatter_field"
1242
1242
  ]);
1243
- PolicyStepSchema = z17.object({
1244
- id: z17.string().min(1, "Step id is required"),
1243
+ PolicyStepSchema = z18.object({
1244
+ id: z18.string().min(1, "Step id is required"),
1245
1245
  tool: PolicyToolNameSchema,
1246
- when: z17.string().optional(),
1247
- params: z17.record(z17.unknown()),
1248
- description: z17.string().optional()
1246
+ when: z18.string().optional(),
1247
+ params: z18.record(z18.unknown()),
1248
+ description: z18.string().optional()
1249
1249
  });
1250
- PolicyOutputSchema = z17.object({
1251
- summary: z17.string().optional(),
1252
- files: z17.array(z17.string()).optional()
1250
+ PolicyOutputSchema = z18.object({
1251
+ summary: z18.string().optional(),
1252
+ files: z18.array(z18.string()).optional()
1253
1253
  });
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"),
1254
+ PolicyDefinitionSchema = z18.object({
1255
+ version: z18.literal("1.0"),
1256
+ name: z18.string().min(1, "Policy name is required"),
1257
+ description: z18.string().min(1, "Policy description is required"),
1258
+ variables: z18.record(PolicyVariableSchema).optional(),
1259
+ conditions: z18.array(PolicyConditionSchema).optional(),
1260
+ steps: z18.array(PolicyStepSchema).min(1, "At least one step is required"),
1261
1261
  output: PolicyOutputSchema.optional()
1262
1262
  });
1263
1263
  }
@@ -1270,8 +1270,8 @@ __export(conditions_exports, {
1270
1270
  evaluateCondition: () => evaluateCondition,
1271
1271
  shouldStepExecute: () => shouldStepExecute
1272
1272
  });
1273
- import fs24 from "fs/promises";
1274
- import path23 from "path";
1273
+ import fs25 from "fs/promises";
1274
+ import path24 from "path";
1275
1275
  async function evaluateCondition(condition, vaultPath2, context) {
1276
1276
  const interpolatedPath = condition.path ? interpolate(condition.path, context) : void 0;
1277
1277
  const interpolatedSection = condition.section ? interpolate(condition.section, context) : void 0;
@@ -1324,9 +1324,9 @@ async function evaluateCondition(condition, vaultPath2, context) {
1324
1324
  }
1325
1325
  }
1326
1326
  async function evaluateFileExists(vaultPath2, notePath, expectExists) {
1327
- const fullPath = path23.join(vaultPath2, notePath);
1327
+ const fullPath = path24.join(vaultPath2, notePath);
1328
1328
  try {
1329
- await fs24.access(fullPath);
1329
+ await fs25.access(fullPath);
1330
1330
  return {
1331
1331
  met: expectExists,
1332
1332
  reason: expectExists ? `File exists: ${notePath}` : `File exists (expected not to): ${notePath}`
@@ -1339,9 +1339,9 @@ async function evaluateFileExists(vaultPath2, notePath, expectExists) {
1339
1339
  }
1340
1340
  }
1341
1341
  async function evaluateSectionExists(vaultPath2, notePath, sectionName, expectExists) {
1342
- const fullPath = path23.join(vaultPath2, notePath);
1342
+ const fullPath = path24.join(vaultPath2, notePath);
1343
1343
  try {
1344
- await fs24.access(fullPath);
1344
+ await fs25.access(fullPath);
1345
1345
  } catch {
1346
1346
  return {
1347
1347
  met: !expectExists,
@@ -1370,9 +1370,9 @@ async function evaluateSectionExists(vaultPath2, notePath, sectionName, expectEx
1370
1370
  }
1371
1371
  }
1372
1372
  async function evaluateFrontmatterExists(vaultPath2, notePath, fieldName, expectExists) {
1373
- const fullPath = path23.join(vaultPath2, notePath);
1373
+ const fullPath = path24.join(vaultPath2, notePath);
1374
1374
  try {
1375
- await fs24.access(fullPath);
1375
+ await fs25.access(fullPath);
1376
1376
  } catch {
1377
1377
  return {
1378
1378
  met: !expectExists,
@@ -1401,9 +1401,9 @@ async function evaluateFrontmatterExists(vaultPath2, notePath, fieldName, expect
1401
1401
  }
1402
1402
  }
1403
1403
  async function evaluateFrontmatterEquals(vaultPath2, notePath, fieldName, expectedValue) {
1404
- const fullPath = path23.join(vaultPath2, notePath);
1404
+ const fullPath = path24.join(vaultPath2, notePath);
1405
1405
  try {
1406
- await fs24.access(fullPath);
1406
+ await fs25.access(fullPath);
1407
1407
  } catch {
1408
1408
  return {
1409
1409
  met: false,
@@ -1544,7 +1544,7 @@ var init_taskHelpers = __esm({
1544
1544
  });
1545
1545
 
1546
1546
  // src/index.ts
1547
- import * as path28 from "path";
1547
+ import * as path29 from "path";
1548
1548
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1549
1549
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1550
1550
 
@@ -1633,6 +1633,21 @@ function isBinaryContent(content) {
1633
1633
  const nonPrintable = sample.replace(/[\x20-\x7E\t\n\r]/g, "").length;
1634
1634
  return nonPrintable / sample.length > 0.1;
1635
1635
  }
1636
+ function parseFrontmatterDate(value) {
1637
+ if (value == null) return void 0;
1638
+ let date;
1639
+ if (value instanceof Date) {
1640
+ date = value;
1641
+ } else if (typeof value === "string" || typeof value === "number") {
1642
+ date = new Date(value);
1643
+ } else {
1644
+ return void 0;
1645
+ }
1646
+ if (isNaN(date.getTime())) return void 0;
1647
+ const year = date.getFullYear();
1648
+ if (year < 2e3 || date.getTime() > Date.now() + 864e5) return void 0;
1649
+ return date;
1650
+ }
1636
1651
  var WIKILINK_REGEX = /\[\[([^\]|#]+)(?:#[^\]|]*)?(?:\|([^\]]+))?\]\]/g;
1637
1652
  var TAG_REGEX = /(?:^|\s)#([a-zA-Z][a-zA-Z0-9_/-]*)/g;
1638
1653
  var CODE_BLOCK_REGEX = /```[\s\S]*?```|`[^`\n]+`/g;
@@ -1753,6 +1768,7 @@ async function parseNoteWithWarnings(file) {
1753
1768
  warnings.push(`Malformed frontmatter: ${err instanceof Error ? err.message : String(err)}`);
1754
1769
  }
1755
1770
  const title = file.path.replace(/\.md$/, "").split("/").pop() || file.path;
1771
+ const created = parseFrontmatterDate(frontmatter.created) ?? file.created;
1756
1772
  return {
1757
1773
  note: {
1758
1774
  path: file.path,
@@ -1762,7 +1778,7 @@ async function parseNoteWithWarnings(file) {
1762
1778
  outlinks: extractWikilinks(markdown),
1763
1779
  tags: extractTags(markdown, frontmatter),
1764
1780
  modified: file.modified,
1765
- created: file.created
1781
+ created
1766
1782
  },
1767
1783
  warnings,
1768
1784
  skipped: false
@@ -2189,8 +2205,8 @@ function updateIndexProgress(parsed, total) {
2189
2205
  function normalizeTarget(target) {
2190
2206
  return target.toLowerCase().replace(/\.md$/, "");
2191
2207
  }
2192
- function normalizeNotePath(path29) {
2193
- return path29.toLowerCase().replace(/\.md$/, "");
2208
+ function normalizeNotePath(path30) {
2209
+ return path30.toLowerCase().replace(/\.md$/, "");
2194
2210
  }
2195
2211
  async function buildVaultIndex(vaultPath2, options = {}) {
2196
2212
  const { timeoutMs = DEFAULT_TIMEOUT_MS, onProgress } = options;
@@ -2356,7 +2372,7 @@ function findSimilarEntity(index, target) {
2356
2372
  }
2357
2373
  const maxDist = normalizedLen <= 10 ? 1 : 2;
2358
2374
  let bestMatch;
2359
- for (const [entity, path29] of index.entities) {
2375
+ for (const [entity, path30] of index.entities) {
2360
2376
  const lenDiff = Math.abs(entity.length - normalizedLen);
2361
2377
  if (lenDiff > maxDist) {
2362
2378
  continue;
@@ -2364,7 +2380,7 @@ function findSimilarEntity(index, target) {
2364
2380
  const dist = levenshteinDistance(normalized, entity);
2365
2381
  if (dist > 0 && dist <= maxDist) {
2366
2382
  if (!bestMatch || dist < bestMatch.distance) {
2367
- bestMatch = { path: path29, entity, distance: dist };
2383
+ bestMatch = { path: path30, entity, distance: dist };
2368
2384
  if (dist === 1) {
2369
2385
  return bestMatch;
2370
2386
  }
@@ -2464,7 +2480,8 @@ import {
2464
2480
  saveFlywheelConfigToDb
2465
2481
  } from "@velvetmonkey/vault-core";
2466
2482
  var DEFAULT_CONFIG = {
2467
- exclude_task_tags: []
2483
+ exclude_task_tags: [],
2484
+ exclude_analysis_tags: []
2468
2485
  };
2469
2486
  function loadConfig(stateDb2) {
2470
2487
  if (stateDb2) {
@@ -2528,6 +2545,7 @@ function findMatchingFolder(folders, patterns) {
2528
2545
  function inferConfig(index, vaultPath2) {
2529
2546
  const inferred = {
2530
2547
  exclude_task_tags: [],
2548
+ exclude_analysis_tags: [],
2531
2549
  paths: {}
2532
2550
  };
2533
2551
  if (vaultPath2) {
@@ -2553,6 +2571,7 @@ function inferConfig(index, vaultPath2) {
2553
2571
  const lowerTag = tag.toLowerCase();
2554
2572
  if (RECURRING_TAG_PATTERNS.some((pattern) => lowerTag.includes(pattern))) {
2555
2573
  inferred.exclude_task_tags.push(tag);
2574
+ inferred.exclude_analysis_tags.push(tag);
2556
2575
  }
2557
2576
  }
2558
2577
  return inferred;
@@ -2826,30 +2845,30 @@ var EventQueue = class {
2826
2845
  * Add a new event to the queue
2827
2846
  */
2828
2847
  push(type, rawPath) {
2829
- const path29 = normalizePath(rawPath);
2848
+ const path30 = normalizePath(rawPath);
2830
2849
  const now = Date.now();
2831
2850
  const event = {
2832
2851
  type,
2833
- path: path29,
2852
+ path: path30,
2834
2853
  timestamp: now
2835
2854
  };
2836
- let pending = this.pending.get(path29);
2855
+ let pending = this.pending.get(path30);
2837
2856
  if (!pending) {
2838
2857
  pending = {
2839
2858
  events: [],
2840
2859
  timer: null,
2841
2860
  lastEvent: now
2842
2861
  };
2843
- this.pending.set(path29, pending);
2862
+ this.pending.set(path30, pending);
2844
2863
  }
2845
2864
  pending.events.push(event);
2846
2865
  pending.lastEvent = now;
2847
- console.error(`[flywheel] QUEUE: pushed ${type} for ${path29}, pending=${this.pending.size}`);
2866
+ console.error(`[flywheel] QUEUE: pushed ${type} for ${path30}, pending=${this.pending.size}`);
2848
2867
  if (pending.timer) {
2849
2868
  clearTimeout(pending.timer);
2850
2869
  }
2851
2870
  pending.timer = setTimeout(() => {
2852
- this.flushPath(path29);
2871
+ this.flushPath(path30);
2853
2872
  }, this.config.debounceMs);
2854
2873
  if (this.pending.size >= this.config.batchSize) {
2855
2874
  this.flush();
@@ -2870,10 +2889,10 @@ var EventQueue = class {
2870
2889
  /**
2871
2890
  * Flush a single path's events
2872
2891
  */
2873
- flushPath(path29) {
2874
- const pending = this.pending.get(path29);
2892
+ flushPath(path30) {
2893
+ const pending = this.pending.get(path30);
2875
2894
  if (!pending || pending.events.length === 0) return;
2876
- console.error(`[flywheel] QUEUE: flushing ${path29}, events=${pending.events.length}`);
2895
+ console.error(`[flywheel] QUEUE: flushing ${path30}, events=${pending.events.length}`);
2877
2896
  if (pending.timer) {
2878
2897
  clearTimeout(pending.timer);
2879
2898
  pending.timer = null;
@@ -2882,7 +2901,7 @@ var EventQueue = class {
2882
2901
  if (coalescedType) {
2883
2902
  const coalesced = {
2884
2903
  type: coalescedType,
2885
- path: path29,
2904
+ path: path30,
2886
2905
  originalEvents: [...pending.events]
2887
2906
  };
2888
2907
  this.onBatch({
@@ -2890,7 +2909,7 @@ var EventQueue = class {
2890
2909
  timestamp: Date.now()
2891
2910
  });
2892
2911
  }
2893
- this.pending.delete(path29);
2912
+ this.pending.delete(path30);
2894
2913
  }
2895
2914
  /**
2896
2915
  * Flush all pending events
@@ -2902,7 +2921,7 @@ var EventQueue = class {
2902
2921
  }
2903
2922
  if (this.pending.size === 0) return;
2904
2923
  const events = [];
2905
- for (const [path29, pending] of this.pending) {
2924
+ for (const [path30, pending] of this.pending) {
2906
2925
  if (pending.timer) {
2907
2926
  clearTimeout(pending.timer);
2908
2927
  }
@@ -2910,7 +2929,7 @@ var EventQueue = class {
2910
2929
  if (coalescedType) {
2911
2930
  events.push({
2912
2931
  type: coalescedType,
2913
- path: path29,
2932
+ path: path30,
2914
2933
  originalEvents: [...pending.events]
2915
2934
  });
2916
2935
  }
@@ -3059,31 +3078,31 @@ function createVaultWatcher(options) {
3059
3078
  usePolling: config.usePolling,
3060
3079
  interval: config.usePolling ? config.pollInterval : void 0
3061
3080
  });
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);
3081
+ watcher.on("add", (path30) => {
3082
+ console.error(`[flywheel] RAW EVENT: add ${path30}`);
3083
+ if (shouldWatch(path30, vaultPath2)) {
3084
+ console.error(`[flywheel] ACCEPTED: add ${path30}`);
3085
+ eventQueue.push("add", path30);
3067
3086
  } else {
3068
- console.error(`[flywheel] FILTERED: add ${path29}`);
3087
+ console.error(`[flywheel] FILTERED: add ${path30}`);
3069
3088
  }
3070
3089
  });
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);
3090
+ watcher.on("change", (path30) => {
3091
+ console.error(`[flywheel] RAW EVENT: change ${path30}`);
3092
+ if (shouldWatch(path30, vaultPath2)) {
3093
+ console.error(`[flywheel] ACCEPTED: change ${path30}`);
3094
+ eventQueue.push("change", path30);
3076
3095
  } else {
3077
- console.error(`[flywheel] FILTERED: change ${path29}`);
3096
+ console.error(`[flywheel] FILTERED: change ${path30}`);
3078
3097
  }
3079
3098
  });
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);
3099
+ watcher.on("unlink", (path30) => {
3100
+ console.error(`[flywheel] RAW EVENT: unlink ${path30}`);
3101
+ if (shouldWatch(path30, vaultPath2)) {
3102
+ console.error(`[flywheel] ACCEPTED: unlink ${path30}`);
3103
+ eventQueue.push("unlink", path30);
3085
3104
  } else {
3086
- console.error(`[flywheel] FILTERED: unlink ${path29}`);
3105
+ console.error(`[flywheel] FILTERED: unlink ${path30}`);
3087
3106
  }
3088
3107
  });
3089
3108
  watcher.on("ready", () => {
@@ -3326,6 +3345,11 @@ function getSuppressedCount(stateDb2) {
3326
3345
  ).get();
3327
3346
  return row.count;
3328
3347
  }
3348
+ function getSuppressedEntities(stateDb2) {
3349
+ return stateDb2.db.prepare(
3350
+ "SELECT entity, false_positive_rate FROM wikilink_suppressions ORDER BY false_positive_rate DESC"
3351
+ ).all();
3352
+ }
3329
3353
  function computeBoostFromAccuracy(accuracy, sampleCount) {
3330
3354
  if (sampleCount < FEEDBACK_BOOST_MIN_SAMPLES) return 0;
3331
3355
  for (const tier of FEEDBACK_BOOST_TIERS) {
@@ -3373,10 +3397,10 @@ function getAllFeedbackBoosts(stateDb2, folder) {
3373
3397
  for (const row of globalRows) {
3374
3398
  let accuracy;
3375
3399
  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;
3400
+ const fs30 = folderStats?.get(row.entity);
3401
+ if (fs30 && fs30.count >= FEEDBACK_BOOST_MIN_SAMPLES) {
3402
+ accuracy = fs30.accuracy;
3403
+ sampleCount = fs30.count;
3380
3404
  } else {
3381
3405
  accuracy = row.correct_count / row.total;
3382
3406
  sampleCount = row.total;
@@ -3429,6 +3453,97 @@ function processImplicitFeedback(stateDb2, notePath, currentContent) {
3429
3453
  transaction();
3430
3454
  return removed;
3431
3455
  }
3456
+ var TIER_LABELS = [
3457
+ { label: "Champion (+5)", boost: 5, minAccuracy: 0.95, minSamples: 20 },
3458
+ { label: "Strong (+2)", boost: 2, minAccuracy: 0.8, minSamples: 5 },
3459
+ { label: "Neutral (0)", boost: 0, minAccuracy: 0.6, minSamples: 5 },
3460
+ { label: "Weak (-2)", boost: -2, minAccuracy: 0.4, minSamples: 5 },
3461
+ { label: "Poor (-4)", boost: -4, minAccuracy: 0, minSamples: 5 }
3462
+ ];
3463
+ function getDashboardData(stateDb2) {
3464
+ const entityStats = getEntityStats(stateDb2);
3465
+ const boostTiers = TIER_LABELS.map((t) => ({
3466
+ label: t.label,
3467
+ boost: t.boost,
3468
+ min_accuracy: t.minAccuracy,
3469
+ min_samples: t.minSamples,
3470
+ entities: []
3471
+ }));
3472
+ const learning = [];
3473
+ for (const es of entityStats) {
3474
+ if (es.total < FEEDBACK_BOOST_MIN_SAMPLES) {
3475
+ learning.push({ entity: es.entity, accuracy: es.accuracy, total: es.total });
3476
+ continue;
3477
+ }
3478
+ const boost = computeBoostFromAccuracy(es.accuracy, es.total);
3479
+ const tierIdx = boostTiers.findIndex((t) => t.boost === boost);
3480
+ if (tierIdx >= 0) {
3481
+ boostTiers[tierIdx].entities.push({ entity: es.entity, accuracy: es.accuracy, total: es.total });
3482
+ }
3483
+ }
3484
+ const sourceRows = stateDb2.db.prepare(`
3485
+ SELECT
3486
+ CASE WHEN context LIKE 'implicit:%' THEN 'implicit' ELSE 'explicit' END as source,
3487
+ COUNT(*) as count,
3488
+ SUM(CASE WHEN correct = 1 THEN 1 ELSE 0 END) as correct_count
3489
+ FROM wikilink_feedback
3490
+ GROUP BY source
3491
+ `).all();
3492
+ const feedbackSources = {
3493
+ explicit: { count: 0, correct: 0 },
3494
+ implicit: { count: 0, correct: 0 }
3495
+ };
3496
+ for (const row of sourceRows) {
3497
+ if (row.source === "implicit") {
3498
+ feedbackSources.implicit = { count: row.count, correct: row.correct_count };
3499
+ } else {
3500
+ feedbackSources.explicit = { count: row.count, correct: row.correct_count };
3501
+ }
3502
+ }
3503
+ const appRows = stateDb2.db.prepare(
3504
+ `SELECT status, COUNT(*) as count FROM wikilink_applications GROUP BY status`
3505
+ ).all();
3506
+ const applications = { applied: 0, removed: 0 };
3507
+ for (const row of appRows) {
3508
+ if (row.status === "applied") applications.applied = row.count;
3509
+ else if (row.status === "removed") applications.removed = row.count;
3510
+ }
3511
+ const recent = getFeedback(stateDb2, void 0, 50);
3512
+ const suppressed = getSuppressedEntities(stateDb2);
3513
+ const timeline = stateDb2.db.prepare(`
3514
+ SELECT
3515
+ date(created_at) as day,
3516
+ COUNT(*) as count,
3517
+ SUM(CASE WHEN correct = 1 THEN 1 ELSE 0 END) as correct_count,
3518
+ SUM(CASE WHEN correct = 0 THEN 1 ELSE 0 END) as incorrect_count
3519
+ FROM wikilink_feedback
3520
+ WHERE created_at >= datetime('now', '-30 days')
3521
+ GROUP BY day
3522
+ ORDER BY day
3523
+ `).all();
3524
+ const totalFeedback = feedbackSources.explicit.count + feedbackSources.implicit.count;
3525
+ const totalCorrect = feedbackSources.explicit.correct + feedbackSources.implicit.correct;
3526
+ const totalIncorrect = totalFeedback - totalCorrect;
3527
+ return {
3528
+ total_feedback: totalFeedback,
3529
+ total_correct: totalCorrect,
3530
+ total_incorrect: totalIncorrect,
3531
+ overall_accuracy: totalFeedback > 0 ? Math.round(totalCorrect / totalFeedback * 1e3) / 1e3 : 0,
3532
+ total_suppressed: suppressed.length,
3533
+ feedback_sources: feedbackSources,
3534
+ applications,
3535
+ boost_tiers: boostTiers,
3536
+ learning,
3537
+ suppressed,
3538
+ recent,
3539
+ timeline: timeline.map((t) => ({
3540
+ day: t.day,
3541
+ count: t.count,
3542
+ correct: t.correct_count,
3543
+ incorrect: t.incorrect_count
3544
+ }))
3545
+ };
3546
+ }
3432
3547
 
3433
3548
  // src/core/write/git.ts
3434
3549
  import { simpleGit, CheckRepoActions } from "simple-git";
@@ -5875,12 +5990,402 @@ function getFTS5State() {
5875
5990
  return { ...state };
5876
5991
  }
5877
5992
 
5878
- // src/index.ts
5879
- import { openStateDb, scanVaultEntities as scanVaultEntities3, getSessionId, getAllEntitiesFromDb as getAllEntitiesFromDb2 } from "@velvetmonkey/vault-core";
5993
+ // src/core/read/taskCache.ts
5994
+ import * as path10 from "path";
5880
5995
 
5881
- // src/tools/read/graph.ts
5996
+ // src/tools/read/tasks.ts
5882
5997
  import * as fs8 from "fs";
5883
5998
  import * as path9 from "path";
5999
+ var TASK_REGEX = /^(\s*)- \[([ xX\-])\]\s+(.+)$/;
6000
+ var TAG_REGEX2 = /#([a-zA-Z][a-zA-Z0-9_/-]*)/g;
6001
+ var DATE_REGEX = /\b(\d{4}-\d{2}-\d{2}|\d{1,2}\/\d{1,2}\/\d{2,4})\b/;
6002
+ var HEADING_REGEX = /^(#{1,6})\s+(.+)$/;
6003
+ function parseStatus(char) {
6004
+ if (char === " ") return "open";
6005
+ if (char === "-") return "cancelled";
6006
+ return "completed";
6007
+ }
6008
+ function extractTags2(text) {
6009
+ const tags = [];
6010
+ let match;
6011
+ TAG_REGEX2.lastIndex = 0;
6012
+ while ((match = TAG_REGEX2.exec(text)) !== null) {
6013
+ tags.push(match[1]);
6014
+ }
6015
+ return tags;
6016
+ }
6017
+ function extractDueDate(text) {
6018
+ const match = text.match(DATE_REGEX);
6019
+ return match ? match[1] : void 0;
6020
+ }
6021
+ async function extractTasksFromNote(notePath, absolutePath) {
6022
+ let content;
6023
+ try {
6024
+ content = await fs8.promises.readFile(absolutePath, "utf-8");
6025
+ } catch {
6026
+ return [];
6027
+ }
6028
+ const lines = content.split("\n");
6029
+ const tasks = [];
6030
+ let currentHeading;
6031
+ let inCodeBlock = false;
6032
+ for (let i = 0; i < lines.length; i++) {
6033
+ const line = lines[i];
6034
+ if (line.startsWith("```")) {
6035
+ inCodeBlock = !inCodeBlock;
6036
+ continue;
6037
+ }
6038
+ if (inCodeBlock) continue;
6039
+ const headingMatch = line.match(HEADING_REGEX);
6040
+ if (headingMatch) {
6041
+ currentHeading = headingMatch[2].trim();
6042
+ continue;
6043
+ }
6044
+ const taskMatch = line.match(TASK_REGEX);
6045
+ if (taskMatch) {
6046
+ const statusChar = taskMatch[2];
6047
+ const text = taskMatch[3].trim();
6048
+ tasks.push({
6049
+ path: notePath,
6050
+ line: i + 1,
6051
+ text,
6052
+ status: parseStatus(statusChar),
6053
+ raw: line,
6054
+ context: currentHeading,
6055
+ tags: extractTags2(text),
6056
+ due_date: extractDueDate(text)
6057
+ });
6058
+ }
6059
+ }
6060
+ return tasks;
6061
+ }
6062
+ async function getAllTasks(index, vaultPath2, options = {}) {
6063
+ const { status = "all", folder, tag, excludeTags = [], limit } = options;
6064
+ const allTasks = [];
6065
+ for (const note of index.notes.values()) {
6066
+ if (folder && !note.path.startsWith(folder)) continue;
6067
+ const absolutePath = path9.join(vaultPath2, note.path);
6068
+ const tasks = await extractTasksFromNote(note.path, absolutePath);
6069
+ allTasks.push(...tasks);
6070
+ }
6071
+ let filteredTasks = allTasks;
6072
+ if (status !== "all") {
6073
+ filteredTasks = allTasks.filter((t) => t.status === status);
6074
+ }
6075
+ if (tag) {
6076
+ filteredTasks = filteredTasks.filter((t) => t.tags.includes(tag));
6077
+ }
6078
+ if (excludeTags.length > 0) {
6079
+ filteredTasks = filteredTasks.filter(
6080
+ (t) => !excludeTags.some((excludeTag) => t.tags.includes(excludeTag))
6081
+ );
6082
+ }
6083
+ filteredTasks.sort((a, b) => {
6084
+ if (a.due_date && !b.due_date) return -1;
6085
+ if (!a.due_date && b.due_date) return 1;
6086
+ if (a.due_date && b.due_date) {
6087
+ const cmp = b.due_date.localeCompare(a.due_date);
6088
+ if (cmp !== 0) return cmp;
6089
+ }
6090
+ const noteA = index.notes.get(a.path);
6091
+ const noteB = index.notes.get(b.path);
6092
+ const mtimeA = noteA?.modified?.getTime() ?? 0;
6093
+ const mtimeB = noteB?.modified?.getTime() ?? 0;
6094
+ return mtimeB - mtimeA;
6095
+ });
6096
+ const openCount = allTasks.filter((t) => t.status === "open").length;
6097
+ const completedCount = allTasks.filter((t) => t.status === "completed").length;
6098
+ const cancelledCount = allTasks.filter((t) => t.status === "cancelled").length;
6099
+ const returnTasks = limit ? filteredTasks.slice(0, limit) : filteredTasks;
6100
+ return {
6101
+ total: allTasks.length,
6102
+ open_count: openCount,
6103
+ completed_count: completedCount,
6104
+ cancelled_count: cancelledCount,
6105
+ tasks: returnTasks
6106
+ };
6107
+ }
6108
+ async function getTasksFromNote(index, notePath, vaultPath2, excludeTags = []) {
6109
+ const note = index.notes.get(notePath);
6110
+ if (!note) return null;
6111
+ const absolutePath = path9.join(vaultPath2, notePath);
6112
+ let tasks = await extractTasksFromNote(notePath, absolutePath);
6113
+ if (excludeTags.length > 0) {
6114
+ tasks = tasks.filter(
6115
+ (t) => !excludeTags.some((excludeTag) => t.tags.includes(excludeTag))
6116
+ );
6117
+ }
6118
+ return tasks;
6119
+ }
6120
+ async function getTasksWithDueDates(index, vaultPath2, options = {}) {
6121
+ const { status = "open", folder, excludeTags } = options;
6122
+ const result = await getAllTasks(index, vaultPath2, { status, folder, excludeTags });
6123
+ return result.tasks.filter((t) => t.due_date).sort((a, b) => {
6124
+ const dateA = a.due_date || "";
6125
+ const dateB = b.due_date || "";
6126
+ return dateA.localeCompare(dateB);
6127
+ });
6128
+ }
6129
+
6130
+ // src/core/shared/serverLog.ts
6131
+ var MAX_ENTRIES = 200;
6132
+ var buffer = [];
6133
+ var serverStartTs = Date.now();
6134
+ function serverLog(component, message, level = "info") {
6135
+ const entry = {
6136
+ ts: Date.now(),
6137
+ component,
6138
+ message,
6139
+ level
6140
+ };
6141
+ buffer.push(entry);
6142
+ if (buffer.length > MAX_ENTRIES) {
6143
+ buffer.shift();
6144
+ }
6145
+ const prefix = level === "error" ? "[Memory] ERROR" : level === "warn" ? "[Memory] WARN" : "[Memory]";
6146
+ console.error(`${prefix} [${component}] ${message}`);
6147
+ }
6148
+ function getServerLog(options = {}) {
6149
+ const { since, component, limit = 100 } = options;
6150
+ let entries = buffer;
6151
+ if (since) {
6152
+ entries = entries.filter((e) => e.ts > since);
6153
+ }
6154
+ if (component) {
6155
+ entries = entries.filter((e) => e.component === component);
6156
+ }
6157
+ if (entries.length > limit) {
6158
+ entries = entries.slice(-limit);
6159
+ }
6160
+ return {
6161
+ entries,
6162
+ server_uptime_ms: Date.now() - serverStartTs
6163
+ };
6164
+ }
6165
+
6166
+ // src/core/read/taskCache.ts
6167
+ var db3 = null;
6168
+ var TASK_CACHE_STALE_MS = 30 * 60 * 1e3;
6169
+ var cacheReady = false;
6170
+ var rebuildInProgress = false;
6171
+ function setTaskCacheDatabase(database) {
6172
+ db3 = database;
6173
+ try {
6174
+ const row = db3.prepare(
6175
+ "SELECT value FROM fts_metadata WHERE key = ?"
6176
+ ).get("task_cache_built");
6177
+ if (row) {
6178
+ cacheReady = true;
6179
+ }
6180
+ } catch {
6181
+ }
6182
+ }
6183
+ function isTaskCacheReady() {
6184
+ return cacheReady && db3 !== null;
6185
+ }
6186
+ async function buildTaskCache(vaultPath2, index, excludeTags) {
6187
+ if (!db3) {
6188
+ throw new Error("Task cache database not initialized. Call setTaskCacheDatabase() first.");
6189
+ }
6190
+ if (rebuildInProgress) return;
6191
+ rebuildInProgress = true;
6192
+ const start = Date.now();
6193
+ try {
6194
+ const insertStmt = db3.prepare(`
6195
+ INSERT OR REPLACE INTO tasks (path, line, text, status, raw, context, tags_json, due_date)
6196
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
6197
+ `);
6198
+ const insertAll = db3.transaction(() => {
6199
+ db3.prepare("DELETE FROM tasks").run();
6200
+ let count = 0;
6201
+ const promises7 = [];
6202
+ const notePaths2 = [];
6203
+ for (const note of index.notes.values()) {
6204
+ notePaths2.push(note.path);
6205
+ }
6206
+ return { notePaths: notePaths2, insertStmt };
6207
+ });
6208
+ const { notePaths, insertStmt: stmt } = insertAll();
6209
+ let totalTasks = 0;
6210
+ for (const notePath of notePaths) {
6211
+ const absolutePath = path10.join(vaultPath2, notePath);
6212
+ const tasks = await extractTasksFromNote(notePath, absolutePath);
6213
+ if (tasks.length > 0) {
6214
+ const insertBatch = db3.transaction(() => {
6215
+ for (const task of tasks) {
6216
+ if (excludeTags?.length && excludeTags.some((t) => task.tags.includes(t))) {
6217
+ continue;
6218
+ }
6219
+ stmt.run(
6220
+ task.path,
6221
+ task.line,
6222
+ task.text,
6223
+ task.status,
6224
+ task.raw,
6225
+ task.context ?? null,
6226
+ task.tags.length > 0 ? JSON.stringify(task.tags) : null,
6227
+ task.due_date ?? null
6228
+ );
6229
+ totalTasks++;
6230
+ }
6231
+ });
6232
+ insertBatch();
6233
+ }
6234
+ }
6235
+ db3.prepare(
6236
+ "INSERT OR REPLACE INTO fts_metadata (key, value) VALUES (?, ?)"
6237
+ ).run("task_cache_built", (/* @__PURE__ */ new Date()).toISOString());
6238
+ cacheReady = true;
6239
+ const duration = Date.now() - start;
6240
+ serverLog("tasks", `Task cache built: ${totalTasks} tasks from ${notePaths.length} notes in ${duration}ms`);
6241
+ } finally {
6242
+ rebuildInProgress = false;
6243
+ }
6244
+ }
6245
+ async function updateTaskCacheForFile(vaultPath2, relativePath) {
6246
+ if (!db3) return;
6247
+ db3.prepare("DELETE FROM tasks WHERE path = ?").run(relativePath);
6248
+ const absolutePath = path10.join(vaultPath2, relativePath);
6249
+ const tasks = await extractTasksFromNote(relativePath, absolutePath);
6250
+ if (tasks.length > 0) {
6251
+ const insertStmt = db3.prepare(`
6252
+ INSERT OR REPLACE INTO tasks (path, line, text, status, raw, context, tags_json, due_date)
6253
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
6254
+ `);
6255
+ const insertBatch = db3.transaction(() => {
6256
+ for (const task of tasks) {
6257
+ insertStmt.run(
6258
+ task.path,
6259
+ task.line,
6260
+ task.text,
6261
+ task.status,
6262
+ task.raw,
6263
+ task.context ?? null,
6264
+ task.tags.length > 0 ? JSON.stringify(task.tags) : null,
6265
+ task.due_date ?? null
6266
+ );
6267
+ }
6268
+ });
6269
+ insertBatch();
6270
+ }
6271
+ }
6272
+ function removeTaskCacheForFile(relativePath) {
6273
+ if (!db3) return;
6274
+ db3.prepare("DELETE FROM tasks WHERE path = ?").run(relativePath);
6275
+ }
6276
+ function queryTasksFromCache(options) {
6277
+ if (!db3) {
6278
+ throw new Error("Task cache database not initialized.");
6279
+ }
6280
+ const { status = "all", folder, tag, excludeTags = [], has_due_date, limit, offset = 0 } = options;
6281
+ const conditions = [];
6282
+ const params = [];
6283
+ if (status !== "all") {
6284
+ conditions.push("status = ?");
6285
+ params.push(status);
6286
+ }
6287
+ if (folder) {
6288
+ conditions.push("path LIKE ?");
6289
+ params.push(folder + "%");
6290
+ }
6291
+ if (has_due_date) {
6292
+ conditions.push("due_date IS NOT NULL");
6293
+ }
6294
+ if (tag) {
6295
+ conditions.push("EXISTS (SELECT 1 FROM json_each(tags_json) WHERE value = ?)");
6296
+ params.push(tag);
6297
+ }
6298
+ if (excludeTags.length > 0) {
6299
+ const placeholders = excludeTags.map(() => "?").join(", ");
6300
+ conditions.push(`NOT EXISTS (SELECT 1 FROM json_each(tags_json) WHERE value IN (${placeholders}))`);
6301
+ params.push(...excludeTags);
6302
+ }
6303
+ const whereClause = conditions.length > 0 ? "WHERE " + conditions.join(" AND ") : "";
6304
+ const countConditions = [];
6305
+ const countParams = [];
6306
+ if (folder) {
6307
+ countConditions.push("path LIKE ?");
6308
+ countParams.push(folder + "%");
6309
+ }
6310
+ if (excludeTags.length > 0) {
6311
+ const placeholders = excludeTags.map(() => "?").join(", ");
6312
+ countConditions.push(`NOT EXISTS (SELECT 1 FROM json_each(tags_json) WHERE value IN (${placeholders}))`);
6313
+ countParams.push(...excludeTags);
6314
+ }
6315
+ const countWhere = countConditions.length > 0 ? "WHERE " + countConditions.join(" AND ") : "";
6316
+ const countRows = db3.prepare(
6317
+ `SELECT status, COUNT(*) as cnt FROM tasks ${countWhere} GROUP BY status`
6318
+ ).all(...countParams);
6319
+ let openCount = 0;
6320
+ let completedCount = 0;
6321
+ let cancelledCount = 0;
6322
+ let total = 0;
6323
+ for (const row of countRows) {
6324
+ total += row.cnt;
6325
+ if (row.status === "open") openCount = row.cnt;
6326
+ else if (row.status === "completed") completedCount = row.cnt;
6327
+ else if (row.status === "cancelled") cancelledCount = row.cnt;
6328
+ }
6329
+ let orderBy;
6330
+ if (has_due_date) {
6331
+ orderBy = "ORDER BY due_date ASC, path";
6332
+ } else {
6333
+ orderBy = "ORDER BY CASE WHEN due_date IS NOT NULL THEN 0 ELSE 1 END, due_date DESC, path";
6334
+ }
6335
+ let limitClause = "";
6336
+ const queryParams = [...params];
6337
+ if (limit !== void 0) {
6338
+ limitClause = " LIMIT ? OFFSET ?";
6339
+ queryParams.push(limit, offset);
6340
+ }
6341
+ const rows = db3.prepare(
6342
+ `SELECT path, line, text, status, raw, context, tags_json, due_date FROM tasks ${whereClause} ${orderBy}${limitClause}`
6343
+ ).all(...queryParams);
6344
+ const tasks = rows.map((row) => ({
6345
+ path: row.path,
6346
+ line: row.line,
6347
+ text: row.text,
6348
+ status: row.status,
6349
+ raw: row.raw,
6350
+ context: row.context ?? void 0,
6351
+ tags: row.tags_json ? JSON.parse(row.tags_json) : [],
6352
+ due_date: row.due_date ?? void 0
6353
+ }));
6354
+ return {
6355
+ total,
6356
+ open_count: openCount,
6357
+ completed_count: completedCount,
6358
+ cancelled_count: cancelledCount,
6359
+ tasks
6360
+ };
6361
+ }
6362
+ function isTaskCacheStale() {
6363
+ if (!db3) return true;
6364
+ try {
6365
+ const row = db3.prepare(
6366
+ "SELECT value FROM fts_metadata WHERE key = ?"
6367
+ ).get("task_cache_built");
6368
+ if (!row) return true;
6369
+ const builtAt = new Date(row.value).getTime();
6370
+ const age = Date.now() - builtAt;
6371
+ return age > TASK_CACHE_STALE_MS;
6372
+ } catch {
6373
+ return true;
6374
+ }
6375
+ }
6376
+ function refreshIfStale(vaultPath2, index, excludeTags) {
6377
+ if (!isTaskCacheStale() || rebuildInProgress) return;
6378
+ buildTaskCache(vaultPath2, index, excludeTags).catch((err) => {
6379
+ serverLog("tasks", `Task cache background refresh failed: ${err instanceof Error ? err.message : err}`, "error");
6380
+ });
6381
+ }
6382
+
6383
+ // src/index.ts
6384
+ import { openStateDb, scanVaultEntities as scanVaultEntities3, getSessionId, getAllEntitiesFromDb as getAllEntitiesFromDb3 } from "@velvetmonkey/vault-core";
6385
+
6386
+ // src/tools/read/graph.ts
6387
+ import * as fs9 from "fs";
6388
+ import * as path11 from "path";
5884
6389
  import { z } from "zod";
5885
6390
 
5886
6391
  // src/core/read/constants.ts
@@ -6164,8 +6669,8 @@ function requireIndex() {
6164
6669
  // src/tools/read/graph.ts
6165
6670
  async function getContext(vaultPath2, sourcePath, line, contextLines = 1) {
6166
6671
  try {
6167
- const fullPath = path9.join(vaultPath2, sourcePath);
6168
- const content = await fs8.promises.readFile(fullPath, "utf-8");
6672
+ const fullPath = path11.join(vaultPath2, sourcePath);
6673
+ const content = await fs9.promises.readFile(fullPath, "utf-8");
6169
6674
  const lines = content.split("\n");
6170
6675
  const startLine = Math.max(0, line - 1 - contextLines);
6171
6676
  const endLine = Math.min(lines.length, line + contextLines);
@@ -6468,14 +6973,14 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
6468
6973
  };
6469
6974
  function findSimilarEntity2(target, entities) {
6470
6975
  const targetLower = target.toLowerCase();
6471
- for (const [name, path29] of entities) {
6976
+ for (const [name, path30] of entities) {
6472
6977
  if (name.startsWith(targetLower) || targetLower.startsWith(name)) {
6473
- return path29;
6978
+ return path30;
6474
6979
  }
6475
6980
  }
6476
- for (const [name, path29] of entities) {
6981
+ for (const [name, path30] of entities) {
6477
6982
  if (name.includes(targetLower) || targetLower.includes(name)) {
6478
- return path29;
6983
+ return path30;
6479
6984
  }
6480
6985
  }
6481
6986
  return void 0;
@@ -6557,7 +7062,7 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
6557
7062
  }
6558
7063
 
6559
7064
  // src/tools/read/health.ts
6560
- import * as fs9 from "fs";
7065
+ import * as fs10 from "fs";
6561
7066
  import { z as z3 } from "zod";
6562
7067
 
6563
7068
  // src/tools/read/periodic.ts
@@ -6929,7 +7434,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
6929
7434
  const indexErrorObj = getIndexError();
6930
7435
  let vaultAccessible = false;
6931
7436
  try {
6932
- fs9.accessSync(vaultPath2, fs9.constants.R_OK);
7437
+ fs10.accessSync(vaultPath2, fs10.constants.R_OK);
6933
7438
  vaultAccessible = true;
6934
7439
  } catch {
6935
7440
  vaultAccessible = false;
@@ -7067,8 +7572,8 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
7067
7572
  daily_counts: z3.record(z3.number())
7068
7573
  }).describe("Activity summary for the last 7 days")
7069
7574
  };
7070
- function isPeriodicNote(path29) {
7071
- const filename = path29.split("/").pop() || "";
7575
+ function isPeriodicNote2(path30) {
7576
+ const filename = path30.split("/").pop() || "";
7072
7577
  const nameWithoutExt = filename.replace(/\.md$/, "");
7073
7578
  const patterns = [
7074
7579
  /^\d{4}-\d{2}-\d{2}$/,
@@ -7083,7 +7588,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
7083
7588
  // YYYY (yearly)
7084
7589
  ];
7085
7590
  const periodicFolders = ["daily", "weekly", "monthly", "quarterly", "yearly", "journal", "journals"];
7086
- const folder = path29.split("/")[0]?.toLowerCase() || "";
7591
+ const folder = path30.split("/")[0]?.toLowerCase() || "";
7087
7592
  return patterns.some((p) => p.test(nameWithoutExt)) || periodicFolders.includes(folder);
7088
7593
  }
7089
7594
  server2.registerTool(
@@ -7117,7 +7622,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
7117
7622
  const backlinks = getBacklinksForNote(index, note.path);
7118
7623
  if (backlinks.length === 0) {
7119
7624
  orphanTotal++;
7120
- if (isPeriodicNote(note.path)) {
7625
+ if (isPeriodicNote2(note.path)) {
7121
7626
  orphanPeriodic++;
7122
7627
  } else {
7123
7628
  orphanContent++;
@@ -7174,7 +7679,46 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
7174
7679
  };
7175
7680
  }
7176
7681
  );
7177
- }
7682
+ const LogEntrySchema = z3.object({
7683
+ ts: z3.number().describe("Unix timestamp (ms)"),
7684
+ component: z3.string().describe("Source component"),
7685
+ message: z3.string().describe("Log message"),
7686
+ level: z3.enum(["info", "warn", "error"]).describe("Log level")
7687
+ });
7688
+ const ServerLogOutputSchema = {
7689
+ entries: z3.array(LogEntrySchema).describe("Log entries (oldest first)"),
7690
+ server_uptime_ms: z3.coerce.number().describe("Server uptime in milliseconds")
7691
+ };
7692
+ server2.registerTool(
7693
+ "server_log",
7694
+ {
7695
+ title: "Server Activity Log",
7696
+ 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.",
7697
+ inputSchema: {
7698
+ since: z3.coerce.number().optional().describe("Only return entries after this Unix timestamp (ms)"),
7699
+ component: z3.string().optional().describe("Filter by component (server, index, fts5, semantic, tasks, watcher, statedb, config)"),
7700
+ limit: z3.coerce.number().optional().describe("Max entries to return (default 100)")
7701
+ },
7702
+ outputSchema: ServerLogOutputSchema
7703
+ },
7704
+ async (params) => {
7705
+ const result = getServerLog({
7706
+ since: params.since,
7707
+ component: params.component,
7708
+ limit: params.limit
7709
+ });
7710
+ return {
7711
+ content: [
7712
+ {
7713
+ type: "text",
7714
+ text: JSON.stringify(result, null, 2)
7715
+ }
7716
+ ],
7717
+ structuredContent: result
7718
+ };
7719
+ }
7720
+ );
7721
+ }
7178
7722
 
7179
7723
  // src/tools/read/query.ts
7180
7724
  import { z as z4 } from "zod";
@@ -7451,8 +7995,8 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
7451
7995
  }
7452
7996
 
7453
7997
  // src/tools/read/system.ts
7454
- import * as fs10 from "fs";
7455
- import * as path10 from "path";
7998
+ import * as fs11 from "fs";
7999
+ import * as path12 from "path";
7456
8000
  import { z as z5 } from "zod";
7457
8001
  import { scanVaultEntities as scanVaultEntities2, getEntityIndexFromDb as getEntityIndexFromDb2 } from "@velvetmonkey/vault-core";
7458
8002
  function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfig, getStateDb) {
@@ -7685,8 +8229,8 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
7685
8229
  continue;
7686
8230
  }
7687
8231
  try {
7688
- const fullPath = path10.join(vaultPath2, note.path);
7689
- const content = await fs10.promises.readFile(fullPath, "utf-8");
8232
+ const fullPath = path12.join(vaultPath2, note.path);
8233
+ const content = await fs11.promises.readFile(fullPath, "utf-8");
7690
8234
  const lines = content.split("\n");
7691
8235
  for (let i = 0; i < lines.length; i++) {
7692
8236
  const line = lines[i];
@@ -7801,8 +8345,8 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
7801
8345
  let wordCount;
7802
8346
  if (include_word_count) {
7803
8347
  try {
7804
- const fullPath = path10.join(vaultPath2, resolvedPath);
7805
- const content = await fs10.promises.readFile(fullPath, "utf-8");
8348
+ const fullPath = path12.join(vaultPath2, resolvedPath);
8349
+ const content = await fs11.promises.readFile(fullPath, "utf-8");
7806
8350
  wordCount = content.split(/\s+/).filter((w) => w.length > 0).length;
7807
8351
  } catch {
7808
8352
  }
@@ -7947,9 +8491,9 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
7947
8491
  import { z as z6 } from "zod";
7948
8492
 
7949
8493
  // src/tools/read/structure.ts
7950
- import * as fs11 from "fs";
7951
- import * as path11 from "path";
7952
- var HEADING_REGEX = /^(#{1,6})\s+(.+)$/;
8494
+ import * as fs12 from "fs";
8495
+ import * as path13 from "path";
8496
+ var HEADING_REGEX2 = /^(#{1,6})\s+(.+)$/;
7953
8497
  function extractHeadings(content) {
7954
8498
  const lines = content.split("\n");
7955
8499
  const headings = [];
@@ -7961,7 +8505,7 @@ function extractHeadings(content) {
7961
8505
  continue;
7962
8506
  }
7963
8507
  if (inCodeBlock) continue;
7964
- const match = line.match(HEADING_REGEX);
8508
+ const match = line.match(HEADING_REGEX2);
7965
8509
  if (match) {
7966
8510
  headings.push({
7967
8511
  level: match[1].length,
@@ -8002,10 +8546,10 @@ function buildSections(headings, totalLines) {
8002
8546
  async function getNoteStructure(index, notePath, vaultPath2) {
8003
8547
  const note = index.notes.get(notePath);
8004
8548
  if (!note) return null;
8005
- const absolutePath = path11.join(vaultPath2, notePath);
8549
+ const absolutePath = path13.join(vaultPath2, notePath);
8006
8550
  let content;
8007
8551
  try {
8008
- content = await fs11.promises.readFile(absolutePath, "utf-8");
8552
+ content = await fs12.promises.readFile(absolutePath, "utf-8");
8009
8553
  } catch {
8010
8554
  return null;
8011
8555
  }
@@ -8025,10 +8569,10 @@ async function getNoteStructure(index, notePath, vaultPath2) {
8025
8569
  async function getSectionContent(index, notePath, headingText, vaultPath2, includeSubheadings = true) {
8026
8570
  const note = index.notes.get(notePath);
8027
8571
  if (!note) return null;
8028
- const absolutePath = path11.join(vaultPath2, notePath);
8572
+ const absolutePath = path13.join(vaultPath2, notePath);
8029
8573
  let content;
8030
8574
  try {
8031
- content = await fs11.promises.readFile(absolutePath, "utf-8");
8575
+ content = await fs12.promises.readFile(absolutePath, "utf-8");
8032
8576
  } catch {
8033
8577
  return null;
8034
8578
  }
@@ -8067,10 +8611,10 @@ async function findSections(index, headingPattern, vaultPath2, folder) {
8067
8611
  const results = [];
8068
8612
  for (const note of index.notes.values()) {
8069
8613
  if (folder && !note.path.startsWith(folder)) continue;
8070
- const absolutePath = path11.join(vaultPath2, note.path);
8614
+ const absolutePath = path13.join(vaultPath2, note.path);
8071
8615
  let content;
8072
8616
  try {
8073
- content = await fs11.promises.readFile(absolutePath, "utf-8");
8617
+ content = await fs12.promises.readFile(absolutePath, "utf-8");
8074
8618
  } catch {
8075
8619
  continue;
8076
8620
  }
@@ -8089,140 +8633,6 @@ async function findSections(index, headingPattern, vaultPath2, folder) {
8089
8633
  return results;
8090
8634
  }
8091
8635
 
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
8636
  // src/tools/read/primitives.ts
8227
8637
  function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = () => ({})) {
8228
8638
  server2.registerTool(
@@ -8235,18 +8645,18 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
8235
8645
  include_content: z6.boolean().default(false).describe("Include the text content under each top-level section")
8236
8646
  }
8237
8647
  },
8238
- async ({ path: path29, include_content }) => {
8648
+ async ({ path: path30, include_content }) => {
8239
8649
  const index = getIndex();
8240
8650
  const vaultPath2 = getVaultPath();
8241
- const result = await getNoteStructure(index, path29, vaultPath2);
8651
+ const result = await getNoteStructure(index, path30, vaultPath2);
8242
8652
  if (!result) {
8243
8653
  return {
8244
- content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path29 }, null, 2) }]
8654
+ content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path30 }, null, 2) }]
8245
8655
  };
8246
8656
  }
8247
8657
  if (include_content) {
8248
8658
  for (const section of result.sections) {
8249
- const sectionResult = await getSectionContent(index, path29, section.heading.text, vaultPath2, true);
8659
+ const sectionResult = await getSectionContent(index, path30, section.heading.text, vaultPath2, true);
8250
8660
  if (sectionResult) {
8251
8661
  section.content = sectionResult.content;
8252
8662
  }
@@ -8268,15 +8678,15 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
8268
8678
  include_subheadings: z6.boolean().default(true).describe("Include content under subheadings")
8269
8679
  }
8270
8680
  },
8271
- async ({ path: path29, heading, include_subheadings }) => {
8681
+ async ({ path: path30, heading, include_subheadings }) => {
8272
8682
  const index = getIndex();
8273
8683
  const vaultPath2 = getVaultPath();
8274
- const result = await getSectionContent(index, path29, heading, vaultPath2, include_subheadings);
8684
+ const result = await getSectionContent(index, path30, heading, vaultPath2, include_subheadings);
8275
8685
  if (!result) {
8276
8686
  return {
8277
8687
  content: [{ type: "text", text: JSON.stringify({
8278
8688
  error: "Section not found",
8279
- path: path29,
8689
+ path: path30,
8280
8690
  heading
8281
8691
  }, null, 2) }]
8282
8692
  };
@@ -8330,16 +8740,16 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
8330
8740
  offset: z6.coerce.number().default(0).describe("Number of results to skip (for pagination)")
8331
8741
  }
8332
8742
  },
8333
- async ({ path: path29, status, has_due_date, folder, tag, limit: requestedLimit, offset }) => {
8743
+ async ({ path: path30, status, has_due_date, folder, tag, limit: requestedLimit, offset }) => {
8334
8744
  const limit = Math.min(requestedLimit ?? 25, MAX_LIMIT);
8335
8745
  const index = getIndex();
8336
8746
  const vaultPath2 = getVaultPath();
8337
8747
  const config = getConfig();
8338
- if (path29) {
8339
- const result2 = await getTasksFromNote(index, path29, vaultPath2, config.exclude_task_tags || []);
8748
+ if (path30) {
8749
+ const result2 = await getTasksFromNote(index, path30, vaultPath2, config.exclude_task_tags || []);
8340
8750
  if (!result2) {
8341
8751
  return {
8342
- content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path29 }, null, 2) }]
8752
+ content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path30 }, null, 2) }]
8343
8753
  };
8344
8754
  }
8345
8755
  let filtered = result2;
@@ -8349,7 +8759,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
8349
8759
  const paged2 = filtered.slice(offset, offset + limit);
8350
8760
  return {
8351
8761
  content: [{ type: "text", text: JSON.stringify({
8352
- path: path29,
8762
+ path: path30,
8353
8763
  total_count: filtered.length,
8354
8764
  returned_count: paged2.length,
8355
8765
  open: result2.filter((t) => t.status === "open").length,
@@ -8358,6 +8768,42 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
8358
8768
  }, null, 2) }]
8359
8769
  };
8360
8770
  }
8771
+ if (isTaskCacheReady()) {
8772
+ refreshIfStale(vaultPath2, index, config.exclude_task_tags);
8773
+ if (has_due_date) {
8774
+ const result3 = queryTasksFromCache({
8775
+ status,
8776
+ folder,
8777
+ excludeTags: config.exclude_task_tags,
8778
+ has_due_date: true,
8779
+ limit,
8780
+ offset
8781
+ });
8782
+ return {
8783
+ content: [{ type: "text", text: JSON.stringify({
8784
+ total_count: result3.total,
8785
+ returned_count: result3.tasks.length,
8786
+ tasks: result3.tasks
8787
+ }, null, 2) }]
8788
+ };
8789
+ }
8790
+ const result2 = queryTasksFromCache({
8791
+ status,
8792
+ folder,
8793
+ tag,
8794
+ excludeTags: config.exclude_task_tags,
8795
+ limit,
8796
+ offset
8797
+ });
8798
+ return {
8799
+ content: [{ type: "text", text: JSON.stringify({
8800
+ total_count: result2.total,
8801
+ open_count: result2.open_count,
8802
+ returned_count: result2.tasks.length,
8803
+ tasks: result2.tasks
8804
+ }, null, 2) }]
8805
+ };
8806
+ }
8361
8807
  if (has_due_date) {
8362
8808
  const allResults = await getTasksWithDueDates(index, vaultPath2, {
8363
8809
  status,
@@ -8465,7 +8911,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
8465
8911
  // src/tools/read/migrations.ts
8466
8912
  import { z as z7 } from "zod";
8467
8913
  import * as fs13 from "fs/promises";
8468
- import * as path13 from "path";
8914
+ import * as path14 from "path";
8469
8915
  import matter2 from "gray-matter";
8470
8916
  function getNotesInFolder(index, folder) {
8471
8917
  const notes = [];
@@ -8478,7 +8924,7 @@ function getNotesInFolder(index, folder) {
8478
8924
  return notes;
8479
8925
  }
8480
8926
  async function readFileContent(notePath, vaultPath2) {
8481
- const fullPath = path13.join(vaultPath2, notePath);
8927
+ const fullPath = path14.join(vaultPath2, notePath);
8482
8928
  try {
8483
8929
  return await fs13.readFile(fullPath, "utf-8");
8484
8930
  } catch {
@@ -8486,7 +8932,7 @@ async function readFileContent(notePath, vaultPath2) {
8486
8932
  }
8487
8933
  }
8488
8934
  async function writeFileContent(notePath, vaultPath2, content) {
8489
- const fullPath = path13.join(vaultPath2, notePath);
8935
+ const fullPath = path14.join(vaultPath2, notePath);
8490
8936
  try {
8491
8937
  await fs13.writeFile(fullPath, content, "utf-8");
8492
8938
  return true;
@@ -8667,7 +9113,7 @@ function registerMigrationTools(server2, getIndex, getVaultPath) {
8667
9113
 
8668
9114
  // src/tools/read/graphAnalysis.ts
8669
9115
  import fs14 from "node:fs";
8670
- import path14 from "node:path";
9116
+ import path15 from "node:path";
8671
9117
  import { z as z8 } from "zod";
8672
9118
 
8673
9119
  // src/tools/read/schema.ts
@@ -9221,7 +9667,26 @@ function purgeOldSnapshots(stateDb2, retentionDays = 90) {
9221
9667
  }
9222
9668
 
9223
9669
  // src/tools/read/graphAnalysis.ts
9224
- function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb) {
9670
+ function isPeriodicNote(notePath) {
9671
+ const filename = notePath.split("/").pop() || "";
9672
+ const nameWithoutExt = filename.replace(/\.md$/, "");
9673
+ const patterns = [
9674
+ /^\d{4}-\d{2}-\d{2}$/,
9675
+ // YYYY-MM-DD (daily)
9676
+ /^\d{4}-W\d{2}$/,
9677
+ // YYYY-Wnn (weekly)
9678
+ /^\d{4}-\d{2}$/,
9679
+ // YYYY-MM (monthly)
9680
+ /^\d{4}-Q[1-4]$/,
9681
+ // YYYY-Qn (quarterly)
9682
+ /^\d{4}$/
9683
+ // YYYY (yearly)
9684
+ ];
9685
+ const periodicFolders = ["daily", "weekly", "monthly", "quarterly", "yearly", "journal", "journals"];
9686
+ const folder = notePath.split("/")[0]?.toLowerCase() || "";
9687
+ return patterns.some((p) => p.test(nameWithoutExt)) || periodicFolders.includes(folder);
9688
+ }
9689
+ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb, getConfig) {
9225
9690
  server2.registerTool(
9226
9691
  "graph_analysis",
9227
9692
  {
@@ -9244,7 +9709,7 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb)
9244
9709
  const index = getIndex();
9245
9710
  switch (analysis) {
9246
9711
  case "orphans": {
9247
- const allOrphans = findOrphanNotes(index, folder);
9712
+ const allOrphans = findOrphanNotes(index, folder).filter((o) => !isPeriodicNote(o.path));
9248
9713
  const orphans = allOrphans.slice(offset, offset + limit);
9249
9714
  return {
9250
9715
  content: [{ type: "text", text: JSON.stringify({
@@ -9287,7 +9752,17 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb)
9287
9752
  };
9288
9753
  }
9289
9754
  case "hubs": {
9290
- const allHubs = findHubNotes(index, min_links);
9755
+ const excludeTags = new Set(
9756
+ (getConfig?.()?.exclude_analysis_tags ?? []).map((t) => t.toLowerCase())
9757
+ );
9758
+ const allHubs = findHubNotes(index, min_links).filter((h) => {
9759
+ if (excludeTags.size === 0) return true;
9760
+ const note = index.notes.get(h.path);
9761
+ if (!note) return true;
9762
+ const tags = note.frontmatter?.tags;
9763
+ const tagList = Array.isArray(tags) ? tags : typeof tags === "string" ? [tags] : [];
9764
+ return !tagList.some((t) => excludeTags.has(String(t).toLowerCase()));
9765
+ });
9291
9766
  const hubs = allHubs.slice(offset, offset + limit);
9292
9767
  return {
9293
9768
  content: [{ type: "text", text: JSON.stringify({
@@ -9329,14 +9804,14 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb)
9329
9804
  case "immature": {
9330
9805
  const vaultPath2 = getVaultPath();
9331
9806
  const allNotes = Array.from(index.notes.values()).filter(
9332
- (note) => !folder || note.path.startsWith(folder + "/") || note.path.substring(0, note.path.lastIndexOf("/")) === folder
9807
+ (note) => (!folder || note.path.startsWith(folder + "/") || note.path.substring(0, note.path.lastIndexOf("/")) === folder) && !isPeriodicNote(note.path)
9333
9808
  );
9334
9809
  const conventions = inferFolderConventions(index, folder, 0.5);
9335
9810
  const expectedFields = conventions.inferred_fields.map((f) => f.name);
9336
9811
  const scored = allNotes.map((note) => {
9337
9812
  let wordCount = 0;
9338
9813
  try {
9339
- const content = fs14.readFileSync(path14.join(vaultPath2, note.path), "utf-8");
9814
+ const content = fs14.readFileSync(path15.join(vaultPath2, note.path), "utf-8");
9340
9815
  const body = content.replace(/^---[\s\S]*?---\n?/, "");
9341
9816
  wordCount = body.split(/\s+/).filter((w) => w.length > 0).length;
9342
9817
  } catch {
@@ -9385,8 +9860,8 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb)
9385
9860
  };
9386
9861
  }
9387
9862
  case "evolution": {
9388
- const db3 = getStateDb?.();
9389
- if (!db3) {
9863
+ const db4 = getStateDb?.();
9864
+ if (!db4) {
9390
9865
  return {
9391
9866
  content: [{ type: "text", text: JSON.stringify({
9392
9867
  error: "StateDb not available \u2014 graph evolution requires persistent state"
@@ -9394,7 +9869,7 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb)
9394
9869
  };
9395
9870
  }
9396
9871
  const daysBack = days ?? 30;
9397
- const evolutions = getGraphEvolution(db3, daysBack);
9872
+ const evolutions = getGraphEvolution(db4, daysBack);
9398
9873
  return {
9399
9874
  content: [{ type: "text", text: JSON.stringify({
9400
9875
  analysis: "evolution",
@@ -9404,8 +9879,8 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb)
9404
9879
  };
9405
9880
  }
9406
9881
  case "emerging_hubs": {
9407
- const db3 = getStateDb?.();
9408
- if (!db3) {
9882
+ const db4 = getStateDb?.();
9883
+ if (!db4) {
9409
9884
  return {
9410
9885
  content: [{ type: "text", text: JSON.stringify({
9411
9886
  error: "StateDb not available \u2014 emerging hubs requires persistent state"
@@ -9413,7 +9888,23 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb)
9413
9888
  };
9414
9889
  }
9415
9890
  const daysBack = days ?? 30;
9416
- const hubs = getEmergingHubs(db3, daysBack);
9891
+ let hubs = getEmergingHubs(db4, daysBack);
9892
+ const excludeTags = new Set(
9893
+ (getConfig?.()?.exclude_analysis_tags ?? []).map((t) => t.toLowerCase())
9894
+ );
9895
+ if (excludeTags.size > 0) {
9896
+ const notesByTitle = /* @__PURE__ */ new Map();
9897
+ for (const note of index.notes.values()) {
9898
+ notesByTitle.set(note.title.toLowerCase(), note);
9899
+ }
9900
+ hubs = hubs.filter((hub) => {
9901
+ const note = notesByTitle.get(hub.entity.toLowerCase());
9902
+ if (!note) return true;
9903
+ const tags = note.frontmatter?.tags;
9904
+ const tagList = Array.isArray(tags) ? tags : typeof tags === "string" ? [tags] : [];
9905
+ return !tagList.some((t) => excludeTags.has(String(t).toLowerCase()));
9906
+ });
9907
+ }
9417
9908
  return {
9418
9909
  content: [{ type: "text", text: JSON.stringify({
9419
9910
  analysis: "emerging_hubs",
@@ -9902,13 +10393,12 @@ import { z as z10 } from "zod";
9902
10393
 
9903
10394
  // src/tools/read/bidirectional.ts
9904
10395
  import * as fs15 from "fs/promises";
9905
- import * as path15 from "path";
10396
+ import * as path16 from "path";
9906
10397
  import matter3 from "gray-matter";
9907
10398
  var PROSE_PATTERN_REGEX = /^([A-Za-z][A-Za-z0-9 _-]*):\s*(?:\[\[([^\]]+)\]\]|"([^"]+)"|([^\n]+?))\s*$/gm;
9908
10399
  var CODE_BLOCK_REGEX2 = /```[\s\S]*?```|`[^`\n]+`/g;
9909
- var WIKILINK_REGEX2 = /\[\[([^\]|#]+)(?:#[^\]|]*)?(?:\|[^\]]+)?\]\]/g;
9910
10400
  async function readFileContent2(notePath, vaultPath2) {
9911
- const fullPath = path15.join(vaultPath2, notePath);
10401
+ const fullPath = path16.join(vaultPath2, notePath);
9912
10402
  try {
9913
10403
  return await fs15.readFile(fullPath, "utf-8");
9914
10404
  } catch {
@@ -9931,21 +10421,6 @@ function removeCodeBlocks(content) {
9931
10421
  return "\n".repeat(newlines);
9932
10422
  });
9933
10423
  }
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
10424
  function isWikilinkValue(value) {
9950
10425
  return /^\[\[.+\]\]$/.test(value.trim());
9951
10426
  }
@@ -10099,89 +10574,13 @@ async function suggestWikilinksInFrontmatter(index, notePath, vaultPath2) {
10099
10574
  suggestions
10100
10575
  };
10101
10576
  }
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 });
10154
- }
10155
- }
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 });
10162
- } 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 });
10169
- }
10170
- }
10171
- return {
10172
- path: notePath,
10173
- frontmatter_only,
10174
- prose_only,
10175
- consistent
10176
- };
10177
- }
10178
10577
 
10179
10578
  // src/tools/read/computed.ts
10180
10579
  import * as fs16 from "fs/promises";
10181
- import * as path16 from "path";
10580
+ import * as path17 from "path";
10182
10581
  import matter4 from "gray-matter";
10183
10582
  async function readFileContent3(notePath, vaultPath2) {
10184
- const fullPath = path16.join(vaultPath2, notePath);
10583
+ const fullPath = path17.join(vaultPath2, notePath);
10185
10584
  try {
10186
10585
  return await fs16.readFile(fullPath, "utf-8");
10187
10586
  } catch {
@@ -10189,7 +10588,7 @@ async function readFileContent3(notePath, vaultPath2) {
10189
10588
  }
10190
10589
  }
10191
10590
  async function getFileStats(notePath, vaultPath2) {
10192
- const fullPath = path16.join(vaultPath2, notePath);
10591
+ const fullPath = path17.join(vaultPath2, notePath);
10193
10592
  try {
10194
10593
  const stats = await fs16.stat(fullPath);
10195
10594
  return {
@@ -10322,18 +10721,17 @@ async function computeFrontmatter(index, notePath, vaultPath2, fields) {
10322
10721
  // src/tools/read/noteIntelligence.ts
10323
10722
  import fs17 from "node:fs";
10324
10723
  import nodePath from "node:path";
10325
- function registerNoteIntelligenceTools(server2, getIndex, getVaultPath) {
10724
+ function registerNoteIntelligenceTools(server2, getIndex, getVaultPath, getConfig) {
10326
10725
  server2.registerTool(
10327
10726
  "note_intelligence",
10328
10727
  {
10329
10728
  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" })',
10729
+ 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
10730
  inputSchema: {
10332
10731
  analysis: z10.enum([
10333
10732
  "prose_patterns",
10334
10733
  "suggest_frontmatter",
10335
10734
  "suggest_wikilinks",
10336
- "cross_layer",
10337
10735
  "compute",
10338
10736
  "semantic_links",
10339
10737
  "all"
@@ -10365,12 +10763,6 @@ function registerNoteIntelligenceTools(server2, getIndex, getVaultPath) {
10365
10763
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
10366
10764
  };
10367
10765
  }
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
10766
  case "compute": {
10375
10767
  const result = await computeFrontmatter(index, notePath, vaultPath2, fields);
10376
10768
  return {
@@ -10401,10 +10793,26 @@ function registerNoteIntelligenceTools(server2, getIndex, getVaultPath) {
10401
10793
  while ((wlMatch = wikilinkRegex.exec(noteContent)) !== null) {
10402
10794
  linkedEntities.add(wlMatch[1].toLowerCase());
10403
10795
  }
10796
+ const excludeTags = new Set(
10797
+ (getConfig?.()?.exclude_analysis_tags ?? []).map((t) => t.toLowerCase())
10798
+ );
10404
10799
  try {
10405
10800
  const contentEmbedding = await embedTextCached(noteContent);
10406
10801
  const matches = findSemanticallySimilarEntities(contentEmbedding, 20, linkedEntities);
10407
- const suggestions = matches.filter((m) => m.similarity >= 0.3).map((m) => ({
10802
+ const suggestions = matches.filter((m) => {
10803
+ if (m.similarity < 0.3) return false;
10804
+ if (excludeTags.size > 0) {
10805
+ const entityNote = index.notes.get(m.entityName.toLowerCase() + ".md") ?? [...index.notes.values()].find((n) => n.title.toLowerCase() === m.entityName.toLowerCase());
10806
+ if (entityNote) {
10807
+ const noteTags = Object.keys(entityNote.frontmatter).filter((k) => k === "tags").flatMap((k) => {
10808
+ const v = entityNote.frontmatter[k];
10809
+ return Array.isArray(v) ? v : typeof v === "string" ? [v] : [];
10810
+ }).map((t) => String(t).toLowerCase());
10811
+ if (noteTags.some((t) => excludeTags.has(t))) return false;
10812
+ }
10813
+ }
10814
+ return true;
10815
+ }).map((m) => ({
10408
10816
  entity: m.entityName,
10409
10817
  similarity: m.similarity
10410
10818
  }));
@@ -10426,11 +10834,10 @@ function registerNoteIntelligenceTools(server2, getIndex, getVaultPath) {
10426
10834
  }
10427
10835
  }
10428
10836
  case "all": {
10429
- const [prosePatterns, suggestedFrontmatter, suggestedWikilinks, crossLayer, computed] = await Promise.all([
10837
+ const [prosePatterns, suggestedFrontmatter, suggestedWikilinks, computed] = await Promise.all([
10430
10838
  detectProsePatterns(index, notePath, vaultPath2),
10431
10839
  suggestFrontmatterFromProse(index, notePath, vaultPath2),
10432
10840
  suggestWikilinksInFrontmatter(index, notePath, vaultPath2),
10433
- validateCrossLayer(index, notePath, vaultPath2),
10434
10841
  computeFrontmatter(index, notePath, vaultPath2, fields)
10435
10842
  ]);
10436
10843
  return {
@@ -10439,7 +10846,6 @@ function registerNoteIntelligenceTools(server2, getIndex, getVaultPath) {
10439
10846
  prose_patterns: prosePatterns,
10440
10847
  suggested_frontmatter: suggestedFrontmatter,
10441
10848
  suggested_wikilinks: suggestedWikilinks,
10442
- cross_layer: crossLayer,
10443
10849
  computed
10444
10850
  }, null, 2) }]
10445
10851
  };
@@ -10453,7 +10859,7 @@ function registerNoteIntelligenceTools(server2, getIndex, getVaultPath) {
10453
10859
  init_writer();
10454
10860
  import { z as z11 } from "zod";
10455
10861
  import fs20 from "fs/promises";
10456
- import path19 from "path";
10862
+ import path20 from "path";
10457
10863
 
10458
10864
  // src/core/write/validator.ts
10459
10865
  var TIMESTAMP_PATTERN = /^\*\*\d{2}:\d{2}\*\*/;
@@ -10656,7 +11062,7 @@ function runValidationPipeline(content, format, options = {}) {
10656
11062
  // src/core/write/mutation-helpers.ts
10657
11063
  init_writer();
10658
11064
  import fs19 from "fs/promises";
10659
- import path18 from "path";
11065
+ import path19 from "path";
10660
11066
  init_constants();
10661
11067
  init_writer();
10662
11068
  function formatMcpResult(result) {
@@ -10705,7 +11111,7 @@ async function handleGitCommit(vaultPath2, notePath, commit, prefix) {
10705
11111
  return info;
10706
11112
  }
10707
11113
  async function ensureFileExists(vaultPath2, notePath) {
10708
- const fullPath = path18.join(vaultPath2, notePath);
11114
+ const fullPath = path19.join(vaultPath2, notePath);
10709
11115
  try {
10710
11116
  await fs19.access(fullPath);
10711
11117
  return null;
@@ -10810,10 +11216,10 @@ async function withVaultFrontmatter(options, operation) {
10810
11216
 
10811
11217
  // src/tools/write/mutations.ts
10812
11218
  async function createNoteFromTemplate(vaultPath2, notePath, config) {
10813
- const fullPath = path19.join(vaultPath2, notePath);
10814
- await fs20.mkdir(path19.dirname(fullPath), { recursive: true });
11219
+ const fullPath = path20.join(vaultPath2, notePath);
11220
+ await fs20.mkdir(path20.dirname(fullPath), { recursive: true });
10815
11221
  const templates = config.templates || {};
10816
- const filename = path19.basename(notePath, ".md").toLowerCase();
11222
+ const filename = path20.basename(notePath, ".md").toLowerCase();
10817
11223
  let templatePath;
10818
11224
  const dailyPattern = /^\d{4}-\d{2}-\d{2}/;
10819
11225
  const weeklyPattern = /^\d{4}-W\d{2}/;
@@ -10834,10 +11240,10 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
10834
11240
  let templateContent;
10835
11241
  if (templatePath) {
10836
11242
  try {
10837
- const absTemplatePath = path19.join(vaultPath2, templatePath);
11243
+ const absTemplatePath = path20.join(vaultPath2, templatePath);
10838
11244
  templateContent = await fs20.readFile(absTemplatePath, "utf-8");
10839
11245
  } catch {
10840
- const title = path19.basename(notePath, ".md");
11246
+ const title = path20.basename(notePath, ".md");
10841
11247
  templateContent = `---
10842
11248
  ---
10843
11249
 
@@ -10846,7 +11252,7 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
10846
11252
  templatePath = void 0;
10847
11253
  }
10848
11254
  } else {
10849
- const title = path19.basename(notePath, ".md");
11255
+ const title = path20.basename(notePath, ".md");
10850
11256
  templateContent = `---
10851
11257
  ---
10852
11258
 
@@ -10855,7 +11261,7 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
10855
11261
  }
10856
11262
  const now = /* @__PURE__ */ new Date();
10857
11263
  const dateStr = now.toISOString().split("T")[0];
10858
- templateContent = templateContent.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g, path19.basename(notePath, ".md"));
11264
+ templateContent = templateContent.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g, path20.basename(notePath, ".md"));
10859
11265
  const matter9 = (await import("gray-matter")).default;
10860
11266
  const parsed = matter9(templateContent);
10861
11267
  if (!parsed.data.date) {
@@ -10894,7 +11300,7 @@ Example: vault_add_to_section({ path: "daily/2026-02-15.md", section: "Log", con
10894
11300
  let noteCreated = false;
10895
11301
  let templateUsed;
10896
11302
  if (create_if_missing) {
10897
- const fullPath = path19.join(vaultPath2, notePath);
11303
+ const fullPath = path20.join(vaultPath2, notePath);
10898
11304
  try {
10899
11305
  await fs20.access(fullPath);
10900
11306
  } catch {
@@ -11191,6 +11597,8 @@ function registerTaskTools(server2, vaultPath2) {
11191
11597
  finalFrontmatter = injectMutationMetadata(frontmatter, { agent_id, session_id });
11192
11598
  }
11193
11599
  await writeVaultFile(vaultPath2, notePath, toggleResult.content, finalFrontmatter);
11600
+ await updateTaskCacheForFile(vaultPath2, notePath).catch(() => {
11601
+ });
11194
11602
  const gitInfo = await handleGitCommit(vaultPath2, notePath, commit, "[Flywheel:Task]");
11195
11603
  const newStatus = toggleResult.newState ? "completed" : "incomplete";
11196
11604
  const checkbox = toggleResult.newState ? "[x]" : "[ ]";
@@ -11345,7 +11753,7 @@ Example: vault_update_frontmatter({ path: "projects/alpha.md", frontmatter: { st
11345
11753
  init_writer();
11346
11754
  import { z as z14 } from "zod";
11347
11755
  import fs21 from "fs/promises";
11348
- import path20 from "path";
11756
+ import path21 from "path";
11349
11757
  function registerNoteTools(server2, vaultPath2, getIndex) {
11350
11758
  server2.tool(
11351
11759
  "vault_create_note",
@@ -11368,23 +11776,23 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
11368
11776
  if (!validatePath(vaultPath2, notePath)) {
11369
11777
  return formatMcpResult(errorResult(notePath, "Invalid path: path traversal not allowed"));
11370
11778
  }
11371
- const fullPath = path20.join(vaultPath2, notePath);
11779
+ const fullPath = path21.join(vaultPath2, notePath);
11372
11780
  const existsCheck = await ensureFileExists(vaultPath2, notePath);
11373
11781
  if (existsCheck === null && !overwrite) {
11374
11782
  return formatMcpResult(errorResult(notePath, `File already exists: ${notePath}. Use overwrite=true to replace.`));
11375
11783
  }
11376
- const dir = path20.dirname(fullPath);
11784
+ const dir = path21.dirname(fullPath);
11377
11785
  await fs21.mkdir(dir, { recursive: true });
11378
11786
  let effectiveContent = content;
11379
11787
  let effectiveFrontmatter = frontmatter;
11380
11788
  if (template) {
11381
- const templatePath = path20.join(vaultPath2, template);
11789
+ const templatePath = path21.join(vaultPath2, template);
11382
11790
  try {
11383
11791
  const raw = await fs21.readFile(templatePath, "utf-8");
11384
11792
  const matter9 = (await import("gray-matter")).default;
11385
11793
  const parsed = matter9(raw);
11386
11794
  const dateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
11387
- const title = path20.basename(notePath, ".md");
11795
+ const title = path21.basename(notePath, ".md");
11388
11796
  let templateContent = parsed.content.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g, title);
11389
11797
  if (content) {
11390
11798
  templateContent = templateContent.trimEnd() + "\n\n" + content;
@@ -11399,7 +11807,7 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
11399
11807
  effectiveFrontmatter.date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
11400
11808
  }
11401
11809
  const warnings = [];
11402
- const noteName = path20.basename(notePath, ".md");
11810
+ const noteName = path21.basename(notePath, ".md");
11403
11811
  const existingAliases = Array.isArray(effectiveFrontmatter?.aliases) ? effectiveFrontmatter.aliases.filter((a) => typeof a === "string") : [];
11404
11812
  const preflight = await checkPreflightSimilarity(noteName);
11405
11813
  if (preflight.existingEntity) {
@@ -11516,7 +11924,7 @@ ${sources}`;
11516
11924
  }
11517
11925
  return formatMcpResult(errorResult(notePath, previewLines.join("\n")));
11518
11926
  }
11519
- const fullPath = path20.join(vaultPath2, notePath);
11927
+ const fullPath = path21.join(vaultPath2, notePath);
11520
11928
  await fs21.unlink(fullPath);
11521
11929
  const gitInfo = await handleGitCommit(vaultPath2, notePath, commit, "[Flywheel:Delete]");
11522
11930
  const message = backlinkWarning ? `Deleted note: ${notePath}
@@ -11536,7 +11944,7 @@ Warning: ${backlinkWarning}` : `Deleted note: ${notePath}`;
11536
11944
  init_writer();
11537
11945
  import { z as z15 } from "zod";
11538
11946
  import fs22 from "fs/promises";
11539
- import path21 from "path";
11947
+ import path22 from "path";
11540
11948
  import matter6 from "gray-matter";
11541
11949
  function escapeRegex(str) {
11542
11950
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -11555,7 +11963,7 @@ function extractWikilinks2(content) {
11555
11963
  return wikilinks;
11556
11964
  }
11557
11965
  function getTitleFromPath(filePath) {
11558
- return path21.basename(filePath, ".md");
11966
+ return path22.basename(filePath, ".md");
11559
11967
  }
11560
11968
  async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
11561
11969
  const results = [];
@@ -11564,7 +11972,7 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
11564
11972
  const files = [];
11565
11973
  const entries = await fs22.readdir(dir, { withFileTypes: true });
11566
11974
  for (const entry of entries) {
11567
- const fullPath = path21.join(dir, entry.name);
11975
+ const fullPath = path22.join(dir, entry.name);
11568
11976
  if (entry.isDirectory() && !entry.name.startsWith(".")) {
11569
11977
  files.push(...await scanDir(fullPath));
11570
11978
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
@@ -11575,7 +11983,7 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
11575
11983
  }
11576
11984
  const allFiles = await scanDir(vaultPath2);
11577
11985
  for (const filePath of allFiles) {
11578
- const relativePath = path21.relative(vaultPath2, filePath);
11986
+ const relativePath = path22.relative(vaultPath2, filePath);
11579
11987
  const content = await fs22.readFile(filePath, "utf-8");
11580
11988
  const wikilinks = extractWikilinks2(content);
11581
11989
  const matchingLinks = [];
@@ -11595,7 +12003,7 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
11595
12003
  return results;
11596
12004
  }
11597
12005
  async function updateBacklinksInFile(vaultPath2, filePath, oldTitles, newTitle) {
11598
- const fullPath = path21.join(vaultPath2, filePath);
12006
+ const fullPath = path22.join(vaultPath2, filePath);
11599
12007
  const raw = await fs22.readFile(fullPath, "utf-8");
11600
12008
  const parsed = matter6(raw);
11601
12009
  let content = parsed.content;
@@ -11662,8 +12070,8 @@ function registerMoveNoteTools(server2, vaultPath2) {
11662
12070
  };
11663
12071
  return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
11664
12072
  }
11665
- const oldFullPath = path21.join(vaultPath2, oldPath);
11666
- const newFullPath = path21.join(vaultPath2, newPath);
12073
+ const oldFullPath = path22.join(vaultPath2, oldPath);
12074
+ const newFullPath = path22.join(vaultPath2, newPath);
11667
12075
  try {
11668
12076
  await fs22.access(oldFullPath);
11669
12077
  } catch {
@@ -11713,7 +12121,7 @@ function registerMoveNoteTools(server2, vaultPath2) {
11713
12121
  }
11714
12122
  }
11715
12123
  }
11716
- const destDir = path21.dirname(newFullPath);
12124
+ const destDir = path22.dirname(newFullPath);
11717
12125
  await fs22.mkdir(destDir, { recursive: true });
11718
12126
  await fs22.rename(oldFullPath, newFullPath);
11719
12127
  let gitCommit;
@@ -11799,10 +12207,10 @@ function registerMoveNoteTools(server2, vaultPath2) {
11799
12207
  if (sanitizedTitle !== newTitle) {
11800
12208
  console.error(`[Flywheel] Title sanitized: "${newTitle}" \u2192 "${sanitizedTitle}"`);
11801
12209
  }
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);
12210
+ const fullPath = path22.join(vaultPath2, notePath);
12211
+ const dir = path22.dirname(notePath);
12212
+ const newPath = dir === "." ? `${sanitizedTitle}.md` : path22.join(dir, `${sanitizedTitle}.md`);
12213
+ const newFullPath = path22.join(vaultPath2, newPath);
11806
12214
  try {
11807
12215
  await fs22.access(fullPath);
11808
12216
  } catch {
@@ -11910,15 +12318,146 @@ function registerMoveNoteTools(server2, vaultPath2) {
11910
12318
  );
11911
12319
  }
11912
12320
 
11913
- // src/tools/write/system.ts
12321
+ // src/tools/write/merge.ts
12322
+ init_writer();
11914
12323
  import { z as z16 } from "zod";
12324
+ import fs23 from "fs/promises";
12325
+ function registerMergeTools(server2, vaultPath2) {
12326
+ server2.tool(
12327
+ "merge_entities",
12328
+ "Merge a source entity note into a target entity note: adds alias, appends content, updates wikilinks, deletes source",
12329
+ {
12330
+ source_path: z16.string().describe("Vault-relative path of the note to merge FROM (will be deleted)"),
12331
+ target_path: z16.string().describe("Vault-relative path of the note to merge INTO (receives alias + content)")
12332
+ },
12333
+ async ({ source_path, target_path }) => {
12334
+ try {
12335
+ if (!validatePath(vaultPath2, source_path)) {
12336
+ const result2 = {
12337
+ success: false,
12338
+ message: "Invalid source path: path traversal not allowed",
12339
+ path: source_path
12340
+ };
12341
+ return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
12342
+ }
12343
+ if (!validatePath(vaultPath2, target_path)) {
12344
+ const result2 = {
12345
+ success: false,
12346
+ message: "Invalid target path: path traversal not allowed",
12347
+ path: target_path
12348
+ };
12349
+ return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
12350
+ }
12351
+ let sourceContent;
12352
+ let sourceFrontmatter;
12353
+ try {
12354
+ const source = await readVaultFile(vaultPath2, source_path);
12355
+ sourceContent = source.content;
12356
+ sourceFrontmatter = source.frontmatter;
12357
+ } catch {
12358
+ const result2 = {
12359
+ success: false,
12360
+ message: `Source file not found: ${source_path}`,
12361
+ path: source_path
12362
+ };
12363
+ return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
12364
+ }
12365
+ let targetContent;
12366
+ let targetFrontmatter;
12367
+ try {
12368
+ const target = await readVaultFile(vaultPath2, target_path);
12369
+ targetContent = target.content;
12370
+ targetFrontmatter = target.frontmatter;
12371
+ } catch {
12372
+ const result2 = {
12373
+ success: false,
12374
+ message: `Target file not found: ${target_path}`,
12375
+ path: target_path
12376
+ };
12377
+ return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
12378
+ }
12379
+ const sourceTitle = getTitleFromPath(source_path);
12380
+ const targetTitle = getTitleFromPath(target_path);
12381
+ const existingAliases = extractAliases2(targetFrontmatter);
12382
+ const sourceAliases = extractAliases2(sourceFrontmatter);
12383
+ const allNewAliases = [sourceTitle, ...sourceAliases];
12384
+ const deduped = /* @__PURE__ */ new Set([...existingAliases]);
12385
+ for (const alias of allNewAliases) {
12386
+ if (alias.toLowerCase() !== targetTitle.toLowerCase()) {
12387
+ deduped.add(alias);
12388
+ }
12389
+ }
12390
+ targetFrontmatter.aliases = Array.from(deduped);
12391
+ const trimmedSource = sourceContent.trim();
12392
+ if (trimmedSource.length > 10) {
12393
+ const mergedSection = `
12394
+
12395
+ ## Merged from ${sourceTitle}
12396
+
12397
+ ${trimmedSource}`;
12398
+ targetContent = targetContent.trimEnd() + mergedSection;
12399
+ }
12400
+ const allSourceTitles = [sourceTitle, ...sourceAliases];
12401
+ const backlinks = await findBacklinks(vaultPath2, sourceTitle, sourceAliases);
12402
+ let totalBacklinksUpdated = 0;
12403
+ const modifiedFiles = [];
12404
+ for (const backlink of backlinks) {
12405
+ if (backlink.path === source_path || backlink.path === target_path) continue;
12406
+ const updateResult = await updateBacklinksInFile(
12407
+ vaultPath2,
12408
+ backlink.path,
12409
+ allSourceTitles,
12410
+ targetTitle
12411
+ );
12412
+ if (updateResult.updated) {
12413
+ totalBacklinksUpdated += updateResult.linksUpdated;
12414
+ modifiedFiles.push(backlink.path);
12415
+ }
12416
+ }
12417
+ await writeVaultFile(vaultPath2, target_path, targetContent, targetFrontmatter);
12418
+ const fullSourcePath = `${vaultPath2}/${source_path}`;
12419
+ await fs23.unlink(fullSourcePath);
12420
+ initializeEntityIndex(vaultPath2).catch((err) => {
12421
+ console.error(`[Flywheel] Entity cache rebuild failed: ${err}`);
12422
+ });
12423
+ const previewLines = [
12424
+ `Merged: "${sourceTitle}" \u2192 "${targetTitle}"`,
12425
+ `Aliases added: ${allNewAliases.join(", ")}`,
12426
+ `Source content appended: ${trimmedSource.length > 10 ? "yes" : "no"}`,
12427
+ `Backlinks updated: ${totalBacklinksUpdated}`
12428
+ ];
12429
+ if (modifiedFiles.length > 0) {
12430
+ previewLines.push(`Files modified: ${modifiedFiles.join(", ")}`);
12431
+ }
12432
+ const result = {
12433
+ success: true,
12434
+ message: `Merged "${sourceTitle}" into "${targetTitle}"`,
12435
+ path: target_path,
12436
+ preview: previewLines.join("\n"),
12437
+ backlinks_updated: totalBacklinksUpdated
12438
+ };
12439
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
12440
+ } catch (error) {
12441
+ const result = {
12442
+ success: false,
12443
+ message: `Failed to merge entities: ${error instanceof Error ? error.message : String(error)}`,
12444
+ path: source_path
12445
+ };
12446
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
12447
+ }
12448
+ }
12449
+ );
12450
+ }
12451
+
12452
+ // src/tools/write/system.ts
12453
+ import { z as z17 } from "zod";
11915
12454
  function registerSystemTools2(server2, vaultPath2) {
11916
12455
  server2.tool(
11917
12456
  "vault_undo_last_mutation",
11918
12457
  "Undo the last git commit (typically the last Flywheel mutation). Performs a soft reset.",
11919
12458
  {
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.")
12459
+ confirm: z17.boolean().default(false).describe("Must be true to confirm undo operation"),
12460
+ 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
12461
  },
11923
12462
  async ({ confirm, hash }) => {
11924
12463
  try {
@@ -12019,7 +12558,7 @@ Message: ${undoResult.undoneCommit.message}` : void 0
12019
12558
  }
12020
12559
 
12021
12560
  // src/tools/write/policy.ts
12022
- import { z as z18 } from "zod";
12561
+ import { z as z19 } from "zod";
12023
12562
 
12024
12563
  // src/core/write/policy/index.ts
12025
12564
  init_template();
@@ -12027,8 +12566,8 @@ init_schema();
12027
12566
 
12028
12567
  // src/core/write/policy/parser.ts
12029
12568
  init_schema();
12030
- import fs23 from "fs/promises";
12031
- import path22 from "path";
12569
+ import fs24 from "fs/promises";
12570
+ import path23 from "path";
12032
12571
  import matter7 from "gray-matter";
12033
12572
  function parseYaml(content) {
12034
12573
  const parsed = matter7(`---
@@ -12053,7 +12592,7 @@ function parsePolicyString(yamlContent) {
12053
12592
  }
12054
12593
  async function loadPolicyFile(filePath) {
12055
12594
  try {
12056
- const content = await fs23.readFile(filePath, "utf-8");
12595
+ const content = await fs24.readFile(filePath, "utf-8");
12057
12596
  return parsePolicyString(content);
12058
12597
  } catch (error) {
12059
12598
  if (error.code === "ENOENT") {
@@ -12077,15 +12616,15 @@ async function loadPolicyFile(filePath) {
12077
12616
  }
12078
12617
  }
12079
12618
  async function loadPolicy(vaultPath2, policyName) {
12080
- const policiesDir = path22.join(vaultPath2, ".claude", "policies");
12081
- const policyPath = path22.join(policiesDir, `${policyName}.yaml`);
12619
+ const policiesDir = path23.join(vaultPath2, ".claude", "policies");
12620
+ const policyPath = path23.join(policiesDir, `${policyName}.yaml`);
12082
12621
  try {
12083
- await fs23.access(policyPath);
12622
+ await fs24.access(policyPath);
12084
12623
  return loadPolicyFile(policyPath);
12085
12624
  } catch {
12086
- const ymlPath = path22.join(policiesDir, `${policyName}.yml`);
12625
+ const ymlPath = path23.join(policiesDir, `${policyName}.yml`);
12087
12626
  try {
12088
- await fs23.access(ymlPath);
12627
+ await fs24.access(ymlPath);
12089
12628
  return loadPolicyFile(ymlPath);
12090
12629
  } catch {
12091
12630
  return {
@@ -12223,8 +12762,8 @@ init_template();
12223
12762
  init_conditions();
12224
12763
  init_schema();
12225
12764
  init_writer();
12226
- import fs25 from "fs/promises";
12227
- import path24 from "path";
12765
+ import fs26 from "fs/promises";
12766
+ import path25 from "path";
12228
12767
  init_constants();
12229
12768
  async function executeStep(step, vaultPath2, context, conditionResults) {
12230
12769
  const { execute, reason } = shouldStepExecute(step.when, conditionResults);
@@ -12293,9 +12832,9 @@ async function executeAddToSection(params, vaultPath2, context) {
12293
12832
  const preserveListNesting = params.preserveListNesting !== false;
12294
12833
  const suggestOutgoingLinks = params.suggestOutgoingLinks !== false;
12295
12834
  const maxSuggestions = Number(params.maxSuggestions) || 3;
12296
- const fullPath = path24.join(vaultPath2, notePath);
12835
+ const fullPath = path25.join(vaultPath2, notePath);
12297
12836
  try {
12298
- await fs25.access(fullPath);
12837
+ await fs26.access(fullPath);
12299
12838
  } catch {
12300
12839
  return { success: false, message: `File not found: ${notePath}`, path: notePath };
12301
12840
  }
@@ -12333,9 +12872,9 @@ async function executeRemoveFromSection(params, vaultPath2) {
12333
12872
  const pattern = String(params.pattern || "");
12334
12873
  const mode = params.mode || "first";
12335
12874
  const useRegex = Boolean(params.useRegex);
12336
- const fullPath = path24.join(vaultPath2, notePath);
12875
+ const fullPath = path25.join(vaultPath2, notePath);
12337
12876
  try {
12338
- await fs25.access(fullPath);
12877
+ await fs26.access(fullPath);
12339
12878
  } catch {
12340
12879
  return { success: false, message: `File not found: ${notePath}`, path: notePath };
12341
12880
  }
@@ -12364,9 +12903,9 @@ async function executeReplaceInSection(params, vaultPath2, context) {
12364
12903
  const mode = params.mode || "first";
12365
12904
  const useRegex = Boolean(params.useRegex);
12366
12905
  const skipWikilinks = Boolean(params.skipWikilinks);
12367
- const fullPath = path24.join(vaultPath2, notePath);
12906
+ const fullPath = path25.join(vaultPath2, notePath);
12368
12907
  try {
12369
- await fs25.access(fullPath);
12908
+ await fs26.access(fullPath);
12370
12909
  } catch {
12371
12910
  return { success: false, message: `File not found: ${notePath}`, path: notePath };
12372
12911
  }
@@ -12407,16 +12946,16 @@ async function executeCreateNote(params, vaultPath2, context) {
12407
12946
  if (!validatePath(vaultPath2, notePath)) {
12408
12947
  return { success: false, message: "Invalid path: path traversal not allowed", path: notePath };
12409
12948
  }
12410
- const fullPath = path24.join(vaultPath2, notePath);
12949
+ const fullPath = path25.join(vaultPath2, notePath);
12411
12950
  try {
12412
- await fs25.access(fullPath);
12951
+ await fs26.access(fullPath);
12413
12952
  if (!overwrite) {
12414
12953
  return { success: false, message: `File already exists: ${notePath}`, path: notePath };
12415
12954
  }
12416
12955
  } catch {
12417
12956
  }
12418
- const dir = path24.dirname(fullPath);
12419
- await fs25.mkdir(dir, { recursive: true });
12957
+ const dir = path25.dirname(fullPath);
12958
+ await fs26.mkdir(dir, { recursive: true });
12420
12959
  const { content: processedContent } = maybeApplyWikilinks(content, skipWikilinks, notePath);
12421
12960
  await writeVaultFile(vaultPath2, notePath, processedContent, frontmatter);
12422
12961
  return {
@@ -12435,13 +12974,13 @@ async function executeDeleteNote(params, vaultPath2) {
12435
12974
  if (!validatePath(vaultPath2, notePath)) {
12436
12975
  return { success: false, message: "Invalid path: path traversal not allowed", path: notePath };
12437
12976
  }
12438
- const fullPath = path24.join(vaultPath2, notePath);
12977
+ const fullPath = path25.join(vaultPath2, notePath);
12439
12978
  try {
12440
- await fs25.access(fullPath);
12979
+ await fs26.access(fullPath);
12441
12980
  } catch {
12442
12981
  return { success: false, message: `File not found: ${notePath}`, path: notePath };
12443
12982
  }
12444
- await fs25.unlink(fullPath);
12983
+ await fs26.unlink(fullPath);
12445
12984
  return {
12446
12985
  success: true,
12447
12986
  message: `Deleted note: ${notePath}`,
@@ -12452,9 +12991,9 @@ async function executeToggleTask(params, vaultPath2) {
12452
12991
  const notePath = String(params.path || "");
12453
12992
  const task = String(params.task || "");
12454
12993
  const section = params.section ? String(params.section) : void 0;
12455
- const fullPath = path24.join(vaultPath2, notePath);
12994
+ const fullPath = path25.join(vaultPath2, notePath);
12456
12995
  try {
12457
- await fs25.access(fullPath);
12996
+ await fs26.access(fullPath);
12458
12997
  } catch {
12459
12998
  return { success: false, message: `File not found: ${notePath}`, path: notePath };
12460
12999
  }
@@ -12495,9 +13034,9 @@ async function executeAddTask(params, vaultPath2, context) {
12495
13034
  const completed = Boolean(params.completed);
12496
13035
  const skipWikilinks = Boolean(params.skipWikilinks);
12497
13036
  const preserveListNesting = params.preserveListNesting !== false;
12498
- const fullPath = path24.join(vaultPath2, notePath);
13037
+ const fullPath = path25.join(vaultPath2, notePath);
12499
13038
  try {
12500
- await fs25.access(fullPath);
13039
+ await fs26.access(fullPath);
12501
13040
  } catch {
12502
13041
  return { success: false, message: `File not found: ${notePath}`, path: notePath };
12503
13042
  }
@@ -12532,9 +13071,9 @@ async function executeAddTask(params, vaultPath2, context) {
12532
13071
  async function executeUpdateFrontmatter(params, vaultPath2) {
12533
13072
  const notePath = String(params.path || "");
12534
13073
  const updates = params.frontmatter || {};
12535
- const fullPath = path24.join(vaultPath2, notePath);
13074
+ const fullPath = path25.join(vaultPath2, notePath);
12536
13075
  try {
12537
- await fs25.access(fullPath);
13076
+ await fs26.access(fullPath);
12538
13077
  } catch {
12539
13078
  return { success: false, message: `File not found: ${notePath}`, path: notePath };
12540
13079
  }
@@ -12554,9 +13093,9 @@ async function executeAddFrontmatterField(params, vaultPath2) {
12554
13093
  const notePath = String(params.path || "");
12555
13094
  const key = String(params.key || "");
12556
13095
  const value = params.value;
12557
- const fullPath = path24.join(vaultPath2, notePath);
13096
+ const fullPath = path25.join(vaultPath2, notePath);
12558
13097
  try {
12559
- await fs25.access(fullPath);
13098
+ await fs26.access(fullPath);
12560
13099
  } catch {
12561
13100
  return { success: false, message: `File not found: ${notePath}`, path: notePath };
12562
13101
  }
@@ -12714,15 +13253,15 @@ async function executePolicy(policy, vaultPath2, variables, commit = false) {
12714
13253
  async function rollbackChanges(vaultPath2, originalContents, filesModified) {
12715
13254
  for (const filePath of filesModified) {
12716
13255
  const original = originalContents.get(filePath);
12717
- const fullPath = path24.join(vaultPath2, filePath);
13256
+ const fullPath = path25.join(vaultPath2, filePath);
12718
13257
  if (original === null) {
12719
13258
  try {
12720
- await fs25.unlink(fullPath);
13259
+ await fs26.unlink(fullPath);
12721
13260
  } catch {
12722
13261
  }
12723
13262
  } else if (original !== void 0) {
12724
13263
  try {
12725
- await fs25.writeFile(fullPath, original);
13264
+ await fs26.writeFile(fullPath, original);
12726
13265
  } catch {
12727
13266
  }
12728
13267
  }
@@ -12768,27 +13307,27 @@ async function previewPolicy(policy, vaultPath2, variables) {
12768
13307
  }
12769
13308
 
12770
13309
  // src/core/write/policy/storage.ts
12771
- import fs26 from "fs/promises";
12772
- import path25 from "path";
13310
+ import fs27 from "fs/promises";
13311
+ import path26 from "path";
12773
13312
  function getPoliciesDir(vaultPath2) {
12774
- return path25.join(vaultPath2, ".claude", "policies");
13313
+ return path26.join(vaultPath2, ".claude", "policies");
12775
13314
  }
12776
13315
  async function ensurePoliciesDir(vaultPath2) {
12777
13316
  const dir = getPoliciesDir(vaultPath2);
12778
- await fs26.mkdir(dir, { recursive: true });
13317
+ await fs27.mkdir(dir, { recursive: true });
12779
13318
  }
12780
13319
  async function listPolicies(vaultPath2) {
12781
13320
  const dir = getPoliciesDir(vaultPath2);
12782
13321
  const policies = [];
12783
13322
  try {
12784
- const files = await fs26.readdir(dir);
13323
+ const files = await fs27.readdir(dir);
12785
13324
  for (const file of files) {
12786
13325
  if (!file.endsWith(".yaml") && !file.endsWith(".yml")) {
12787
13326
  continue;
12788
13327
  }
12789
- const filePath = path25.join(dir, file);
12790
- const stat3 = await fs26.stat(filePath);
12791
- const content = await fs26.readFile(filePath, "utf-8");
13328
+ const filePath = path26.join(dir, file);
13329
+ const stat3 = await fs27.stat(filePath);
13330
+ const content = await fs27.readFile(filePath, "utf-8");
12792
13331
  const metadata = extractPolicyMetadata(content);
12793
13332
  policies.push({
12794
13333
  name: metadata.name || file.replace(/\.ya?ml$/, ""),
@@ -12811,10 +13350,10 @@ async function writePolicyRaw(vaultPath2, policyName, content, overwrite = false
12811
13350
  const dir = getPoliciesDir(vaultPath2);
12812
13351
  await ensurePoliciesDir(vaultPath2);
12813
13352
  const filename = `${policyName}.yaml`;
12814
- const filePath = path25.join(dir, filename);
13353
+ const filePath = path26.join(dir, filename);
12815
13354
  if (!overwrite) {
12816
13355
  try {
12817
- await fs26.access(filePath);
13356
+ await fs27.access(filePath);
12818
13357
  return {
12819
13358
  success: false,
12820
13359
  path: filename,
@@ -12831,7 +13370,7 @@ async function writePolicyRaw(vaultPath2, policyName, content, overwrite = false
12831
13370
  message: `Invalid policy: ${validation.errors.map((e) => e.message).join("; ")}`
12832
13371
  };
12833
13372
  }
12834
- await fs26.writeFile(filePath, content, "utf-8");
13373
+ await fs27.writeFile(filePath, content, "utf-8");
12835
13374
  return {
12836
13375
  success: true,
12837
13376
  path: filename,
@@ -12846,71 +13385,71 @@ function registerPolicyTools(server2, vaultPath2) {
12846
13385
  "policy",
12847
13386
  '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
13387
  {
12849
- action: z18.enum(["list", "validate", "preview", "execute", "author", "revise"]).describe("Action to perform"),
13388
+ action: z19.enum(["list", "validate", "preview", "execute", "author", "revise"]).describe("Action to perform"),
12850
13389
  // validate
12851
- yaml: z18.string().optional().describe('Policy YAML content (required for "validate")'),
13390
+ yaml: z19.string().optional().describe('Policy YAML content (required for "validate")'),
12852
13391
  // preview, execute, revise
12853
- policy: z18.string().optional().describe('Policy name or full YAML content (required for "preview", "execute", "revise")'),
13392
+ policy: z19.string().optional().describe('Policy name or full YAML content (required for "preview", "execute", "revise")'),
12854
13393
  // preview, execute
12855
- variables: z18.record(z18.unknown()).optional().describe('Variables to pass to the policy (for "preview", "execute")'),
13394
+ variables: z19.record(z19.unknown()).optional().describe('Variables to pass to the policy (for "preview", "execute")'),
12856
13395
  // execute
12857
- commit: z18.boolean().optional().describe('If true, commit all changes with single atomic commit (for "execute")'),
13396
+ commit: z19.boolean().optional().describe('If true, commit all changes with single atomic commit (for "execute")'),
12858
13397
  // 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")
13398
+ name: z19.string().optional().describe('Name for the policy (required for "author")'),
13399
+ description: z19.string().optional().describe('Description of what the policy should do (required for "author")'),
13400
+ steps: z19.array(z19.object({
13401
+ tool: z19.string().describe("Tool to call (e.g., vault_add_to_section)"),
13402
+ description: z19.string().describe("What this step does"),
13403
+ params: z19.record(z19.unknown()).describe("Parameters for the tool")
12865
13404
  })).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")
13405
+ authorVariables: z19.array(z19.object({
13406
+ name: z19.string().describe("Variable name"),
13407
+ type: z19.enum(["string", "number", "boolean", "array", "enum"]).describe("Variable type"),
13408
+ required: z19.boolean().default(true).describe("Whether variable is required"),
13409
+ default: z19.unknown().optional().describe("Default value"),
13410
+ enum: z19.array(z19.string()).optional().describe("Allowed values for enum type"),
13411
+ description: z19.string().optional().describe("Variable description")
12873
13412
  })).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")
13413
+ conditions: z19.array(z19.object({
13414
+ id: z19.string().describe("Condition ID"),
13415
+ check: z19.string().describe("Condition type (file_exists, section_exists, etc.)"),
13416
+ path: z19.string().optional().describe("File path"),
13417
+ section: z19.string().optional().describe("Section name"),
13418
+ field: z19.string().optional().describe("Frontmatter field"),
13419
+ value: z19.unknown().optional().describe("Expected value")
12881
13420
  })).optional().describe('Conditions for conditional execution (for "author")'),
12882
13421
  // author, revise
12883
- save: z18.boolean().optional().describe('If true, save to .claude/policies/ (for "author", "revise")'),
13422
+ save: z19.boolean().optional().describe('If true, save to .claude/policies/ (for "author", "revise")'),
12884
13423
  // 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()
13424
+ changes: z19.object({
13425
+ description: z19.string().optional().describe("New description"),
13426
+ addVariables: z19.array(z19.object({
13427
+ name: z19.string(),
13428
+ type: z19.enum(["string", "number", "boolean", "array", "enum"]),
13429
+ required: z19.boolean().default(true),
13430
+ default: z19.unknown().optional(),
13431
+ enum: z19.array(z19.string()).optional(),
13432
+ description: z19.string().optional()
12894
13433
  })).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")
13434
+ removeVariables: z19.array(z19.string()).optional().describe("Variable names to remove"),
13435
+ addSteps: z19.array(z19.object({
13436
+ id: z19.string(),
13437
+ tool: z19.string(),
13438
+ params: z19.record(z19.unknown()),
13439
+ when: z19.string().optional(),
13440
+ description: z19.string().optional(),
13441
+ afterStep: z19.string().optional().describe("Insert after this step ID")
12903
13442
  })).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()
13443
+ removeSteps: z19.array(z19.string()).optional().describe("Step IDs to remove"),
13444
+ addConditions: z19.array(z19.object({
13445
+ id: z19.string(),
13446
+ check: z19.string(),
13447
+ path: z19.string().optional(),
13448
+ section: z19.string().optional(),
13449
+ field: z19.string().optional(),
13450
+ value: z19.unknown().optional()
12912
13451
  })).optional().describe("Conditions to add"),
12913
- removeConditions: z18.array(z18.string()).optional().describe("Condition IDs to remove")
13452
+ removeConditions: z19.array(z19.string()).optional().describe("Condition IDs to remove")
12914
13453
  }).optional().describe('Changes to make (required for "revise")')
12915
13454
  },
12916
13455
  async (params) => {
@@ -13351,11 +13890,11 @@ function registerPolicyTools(server2, vaultPath2) {
13351
13890
  }
13352
13891
 
13353
13892
  // src/tools/write/tags.ts
13354
- import { z as z19 } from "zod";
13893
+ import { z as z20 } from "zod";
13355
13894
 
13356
13895
  // src/core/write/tagRename.ts
13357
- import * as fs27 from "fs/promises";
13358
- import * as path26 from "path";
13896
+ import * as fs28 from "fs/promises";
13897
+ import * as path27 from "path";
13359
13898
  import matter8 from "gray-matter";
13360
13899
  import { getProtectedZones } from "@velvetmonkey/vault-core";
13361
13900
  function getNotesInFolder3(index, folder) {
@@ -13461,10 +14000,10 @@ async function renameTag(index, vaultPath2, oldTag, newTag, options) {
13461
14000
  const previews = [];
13462
14001
  let totalChanges = 0;
13463
14002
  for (const note of affectedNotes) {
13464
- const fullPath = path26.join(vaultPath2, note.path);
14003
+ const fullPath = path27.join(vaultPath2, note.path);
13465
14004
  let fileContent;
13466
14005
  try {
13467
- fileContent = await fs27.readFile(fullPath, "utf-8");
14006
+ fileContent = await fs28.readFile(fullPath, "utf-8");
13468
14007
  } catch {
13469
14008
  continue;
13470
14009
  }
@@ -13537,7 +14076,7 @@ async function renameTag(index, vaultPath2, oldTag, newTag, options) {
13537
14076
  previews.push(preview);
13538
14077
  if (!dryRun) {
13539
14078
  const newContent = matter8.stringify(updatedContent, fm);
13540
- await fs27.writeFile(fullPath, newContent, "utf-8");
14079
+ await fs28.writeFile(fullPath, newContent, "utf-8");
13541
14080
  }
13542
14081
  }
13543
14082
  }
@@ -13560,12 +14099,12 @@ function registerTagTools(server2, getIndex, getVaultPath) {
13560
14099
  title: "Rename Tag",
13561
14100
  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
14101
  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)")
14102
+ old_tag: z20.string().describe('Tag to rename (without #, e.g., "project")'),
14103
+ new_tag: z20.string().describe('New tag name (without #, e.g., "work")'),
14104
+ rename_children: z20.boolean().optional().describe("Also rename child tags (e.g., #project/active \u2192 #work/active). Default: true"),
14105
+ folder: z20.string().optional().describe('Limit to notes in this folder (e.g., "projects")'),
14106
+ dry_run: z20.boolean().optional().describe("Preview only, no changes (default: true)"),
14107
+ commit: z20.boolean().optional().describe("Commit changes to git (default: false)")
13569
14108
  }
13570
14109
  },
13571
14110
  async ({ old_tag, new_tag, rename_children, folder, dry_run, commit }) => {
@@ -13590,20 +14129,20 @@ function registerTagTools(server2, getIndex, getVaultPath) {
13590
14129
  }
13591
14130
 
13592
14131
  // src/tools/write/wikilinkFeedback.ts
13593
- import { z as z20 } from "zod";
14132
+ import { z as z21 } from "zod";
13594
14133
  function registerWikilinkFeedbackTools(server2, getStateDb) {
13595
14134
  server2.registerTool(
13596
14135
  "wikilink_feedback",
13597
14136
  {
13598
14137
  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.',
14138
+ 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
14139
  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)")
14140
+ mode: z21.enum(["report", "list", "stats", "dashboard"]).describe("Operation mode"),
14141
+ entity: z21.string().optional().describe("Entity name (required for report mode, optional filter for list/stats)"),
14142
+ note_path: z21.string().optional().describe("Note path where the wikilink appeared (for report mode)"),
14143
+ context: z21.string().optional().describe("Surrounding text context (for report mode)"),
14144
+ correct: z21.boolean().optional().describe("Whether the wikilink was correct (for report mode)"),
14145
+ limit: z21.number().optional().describe("Max entries to return for list mode (default: 20)")
13607
14146
  }
13608
14147
  },
13609
14148
  async ({ mode, entity, note_path, context, correct, limit }) => {
@@ -13653,6 +14192,16 @@ function registerWikilinkFeedbackTools(server2, getStateDb) {
13653
14192
  };
13654
14193
  break;
13655
14194
  }
14195
+ case "dashboard": {
14196
+ const dashboard = getDashboardData(stateDb2);
14197
+ result = {
14198
+ mode: "dashboard",
14199
+ dashboard,
14200
+ total_feedback: dashboard.total_feedback,
14201
+ total_suppressed: dashboard.total_suppressed
14202
+ };
14203
+ break;
14204
+ }
13656
14205
  }
13657
14206
  return {
13658
14207
  content: [
@@ -13666,8 +14215,57 @@ function registerWikilinkFeedbackTools(server2, getStateDb) {
13666
14215
  );
13667
14216
  }
13668
14217
 
14218
+ // src/tools/write/config.ts
14219
+ import { z as z22 } from "zod";
14220
+ import { saveFlywheelConfigToDb as saveFlywheelConfigToDb2 } from "@velvetmonkey/vault-core";
14221
+ function registerConfigTools(server2, getConfig, setConfig, getStateDb) {
14222
+ server2.registerTool(
14223
+ "flywheel_config",
14224
+ {
14225
+ title: "Flywheel Config",
14226
+ 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"] })',
14227
+ inputSchema: {
14228
+ mode: z22.enum(["get", "set"]).describe("Operation mode"),
14229
+ key: z22.string().optional().describe("Config key to update (required for set mode)"),
14230
+ value: z22.unknown().optional().describe("New value for the key (required for set mode)")
14231
+ }
14232
+ },
14233
+ async ({ mode, key, value }) => {
14234
+ switch (mode) {
14235
+ case "get": {
14236
+ const config = getConfig();
14237
+ return {
14238
+ content: [{ type: "text", text: JSON.stringify(config, null, 2) }]
14239
+ };
14240
+ }
14241
+ case "set": {
14242
+ if (!key) {
14243
+ return {
14244
+ content: [{ type: "text", text: JSON.stringify({ error: "key is required for set mode" }) }]
14245
+ };
14246
+ }
14247
+ const stateDb2 = getStateDb();
14248
+ if (!stateDb2) {
14249
+ return {
14250
+ content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
14251
+ };
14252
+ }
14253
+ const current = getConfig();
14254
+ const updated = { ...current, [key]: value };
14255
+ saveFlywheelConfigToDb2(stateDb2, updated);
14256
+ const reloaded = loadConfig(stateDb2);
14257
+ setConfig(reloaded);
14258
+ return {
14259
+ content: [{ type: "text", text: JSON.stringify(reloaded, null, 2) }]
14260
+ };
14261
+ }
14262
+ }
14263
+ }
14264
+ );
14265
+ }
14266
+
13669
14267
  // src/tools/read/metrics.ts
13670
- import { z as z21 } from "zod";
14268
+ import { z as z23 } from "zod";
13671
14269
 
13672
14270
  // src/core/shared/metrics.ts
13673
14271
  var ALL_METRICS = [
@@ -13833,10 +14431,10 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
13833
14431
  title: "Vault Growth",
13834
14432
  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
14433
  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)")
14434
+ mode: z23.enum(["current", "history", "trends", "index_activity"]).describe("Query mode: current snapshot, historical time series, trend analysis, or index rebuild activity"),
14435
+ metric: z23.string().optional().describe('Filter to specific metric (e.g., "note_count"). Omit for all metrics.'),
14436
+ days_back: z23.number().optional().describe("Number of days to look back for history/trends (default: 30)"),
14437
+ limit: z23.number().optional().describe("Number of recent events to return for index_activity mode (default: 20)")
13840
14438
  }
13841
14439
  },
13842
14440
  async ({ mode, metric, days_back, limit: eventLimit }) => {
@@ -13909,7 +14507,7 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
13909
14507
  }
13910
14508
 
13911
14509
  // src/tools/read/activity.ts
13912
- import { z as z22 } from "zod";
14510
+ import { z as z24 } from "zod";
13913
14511
 
13914
14512
  // src/core/shared/toolTracking.ts
13915
14513
  function recordToolInvocation(stateDb2, event) {
@@ -13989,8 +14587,8 @@ function getNoteAccessFrequency(stateDb2, daysBack = 30) {
13989
14587
  }
13990
14588
  }
13991
14589
  }
13992
- return Array.from(noteMap.entries()).map(([path29, stats]) => ({
13993
- path: path29,
14590
+ return Array.from(noteMap.entries()).map(([path30, stats]) => ({
14591
+ path: path30,
13994
14592
  access_count: stats.access_count,
13995
14593
  last_accessed: stats.last_accessed,
13996
14594
  tools_used: Array.from(stats.tools)
@@ -14069,10 +14667,10 @@ function registerActivityTools(server2, getStateDb, getSessionId2) {
14069
14667
  title: "Vault Activity",
14070
14668
  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
14669
  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)")
14670
+ mode: z24.enum(["session", "sessions", "note_access", "tool_usage"]).describe("Activity query mode"),
14671
+ session_id: z24.string().optional().describe("Specific session ID (for session mode, defaults to current)"),
14672
+ days_back: z24.number().optional().describe("Number of days to look back (default: 30)"),
14673
+ limit: z24.number().optional().describe("Maximum results to return (default: 20)")
14076
14674
  }
14077
14675
  },
14078
14676
  async ({ mode, session_id, days_back, limit: resultLimit }) => {
@@ -14139,11 +14737,11 @@ function registerActivityTools(server2, getStateDb, getSessionId2) {
14139
14737
  }
14140
14738
 
14141
14739
  // src/tools/read/similarity.ts
14142
- import { z as z23 } from "zod";
14740
+ import { z as z25 } from "zod";
14143
14741
 
14144
14742
  // src/core/read/similarity.ts
14145
- import * as fs28 from "fs";
14146
- import * as path27 from "path";
14743
+ import * as fs29 from "fs";
14744
+ import * as path28 from "path";
14147
14745
  var STOP_WORDS = /* @__PURE__ */ new Set([
14148
14746
  "the",
14149
14747
  "be",
@@ -14278,12 +14876,12 @@ function extractKeyTerms(content, maxTerms = 15) {
14278
14876
  }
14279
14877
  return Array.from(freq.entries()).sort((a, b) => b[1] - a[1]).slice(0, maxTerms).map(([word]) => word);
14280
14878
  }
14281
- function findSimilarNotes(db3, vaultPath2, index, sourcePath, options = {}) {
14879
+ function findSimilarNotes(db4, vaultPath2, index, sourcePath, options = {}) {
14282
14880
  const limit = options.limit ?? 10;
14283
- const absPath = path27.join(vaultPath2, sourcePath);
14881
+ const absPath = path28.join(vaultPath2, sourcePath);
14284
14882
  let content;
14285
14883
  try {
14286
- content = fs28.readFileSync(absPath, "utf-8");
14884
+ content = fs29.readFileSync(absPath, "utf-8");
14287
14885
  } catch {
14288
14886
  return [];
14289
14887
  }
@@ -14291,7 +14889,7 @@ function findSimilarNotes(db3, vaultPath2, index, sourcePath, options = {}) {
14291
14889
  if (terms.length === 0) return [];
14292
14890
  const query = terms.join(" OR ");
14293
14891
  try {
14294
- const results = db3.prepare(`
14892
+ const results = db4.prepare(`
14295
14893
  SELECT
14296
14894
  path,
14297
14895
  title,
@@ -14359,9 +14957,9 @@ async function findSemanticSimilarNotes(vaultPath2, index, sourcePath, options =
14359
14957
  // Semantic results don't have snippets
14360
14958
  }));
14361
14959
  }
14362
- async function findHybridSimilarNotes(db3, vaultPath2, index, sourcePath, options = {}) {
14960
+ async function findHybridSimilarNotes(db4, vaultPath2, index, sourcePath, options = {}) {
14363
14961
  const limit = options.limit ?? 10;
14364
- const bm25Results = findSimilarNotes(db3, vaultPath2, index, sourcePath, {
14962
+ const bm25Results = findSimilarNotes(db4, vaultPath2, index, sourcePath, {
14365
14963
  limit: limit * 2,
14366
14964
  excludeLinked: options.excludeLinked
14367
14965
  });
@@ -14403,12 +15001,12 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
14403
15001
  title: "Find Similar Notes",
14404
15002
  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
15003
  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)")
15004
+ path: z25.string().describe('Path to the source note (relative to vault root, e.g. "projects/alpha.md")'),
15005
+ limit: z25.number().optional().describe("Maximum number of similar notes to return (default: 10)"),
15006
+ exclude_linked: z25.boolean().optional().describe("Exclude notes already linked to/from the source note (default: true)")
14409
15007
  }
14410
15008
  },
14411
- async ({ path: path29, limit, exclude_linked }) => {
15009
+ async ({ path: path30, limit, exclude_linked }) => {
14412
15010
  const index = getIndex();
14413
15011
  const vaultPath2 = getVaultPath();
14414
15012
  const stateDb2 = getStateDb();
@@ -14417,10 +15015,10 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
14417
15015
  content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
14418
15016
  };
14419
15017
  }
14420
- if (!index.notes.has(path29)) {
15018
+ if (!index.notes.has(path30)) {
14421
15019
  return {
14422
15020
  content: [{ type: "text", text: JSON.stringify({
14423
- error: `Note not found: ${path29}`,
15021
+ error: `Note not found: ${path30}`,
14424
15022
  hint: "Use the full relative path including .md extension"
14425
15023
  }, null, 2) }]
14426
15024
  };
@@ -14431,12 +15029,12 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
14431
15029
  };
14432
15030
  const useHybrid = hasEmbeddingsIndex();
14433
15031
  const method = useHybrid ? "hybrid" : "bm25";
14434
- const results = useHybrid ? await findHybridSimilarNotes(stateDb2.db, vaultPath2, index, path29, opts) : findSimilarNotes(stateDb2.db, vaultPath2, index, path29, opts);
15032
+ const results = useHybrid ? await findHybridSimilarNotes(stateDb2.db, vaultPath2, index, path30, opts) : findSimilarNotes(stateDb2.db, vaultPath2, index, path30, opts);
14435
15033
  return {
14436
15034
  content: [{
14437
15035
  type: "text",
14438
15036
  text: JSON.stringify({
14439
- source: path29,
15037
+ source: path30,
14440
15038
  method,
14441
15039
  exclude_linked: exclude_linked ?? true,
14442
15040
  count: results.length,
@@ -14449,7 +15047,7 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
14449
15047
  }
14450
15048
 
14451
15049
  // src/tools/read/semantic.ts
14452
- import { z as z24 } from "zod";
15050
+ import { z as z26 } from "zod";
14453
15051
  import { getAllEntitiesFromDb } from "@velvetmonkey/vault-core";
14454
15052
  function registerSemanticTools(server2, getVaultPath, getStateDb) {
14455
15053
  server2.registerTool(
@@ -14458,7 +15056,7 @@ function registerSemanticTools(server2, getVaultPath, getStateDb) {
14458
15056
  title: "Initialize Semantic Search",
14459
15057
  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
15058
  inputSchema: {
14461
- force: z24.boolean().optional().describe(
15059
+ force: z26.boolean().optional().describe(
14462
15060
  "Rebuild all embeddings even if they already exist (default: false)"
14463
15061
  )
14464
15062
  }
@@ -14536,6 +15134,142 @@ function registerSemanticTools(server2, getVaultPath, getStateDb) {
14536
15134
  );
14537
15135
  }
14538
15136
 
15137
+ // src/tools/read/merges.ts
15138
+ init_levenshtein();
15139
+ import { z as z27 } from "zod";
15140
+ import { getAllEntitiesFromDb as getAllEntitiesFromDb2, getDismissedMergePairs, recordMergeDismissal } from "@velvetmonkey/vault-core";
15141
+ function normalizeName(name) {
15142
+ return name.toLowerCase().replace(/[.\-_]/g, "").replace(/js$/, "").replace(/ts$/, "");
15143
+ }
15144
+ function registerMergeTools2(server2, getStateDb) {
15145
+ server2.tool(
15146
+ "suggest_entity_merges",
15147
+ "Find potential duplicate entities that could be merged based on name similarity",
15148
+ {
15149
+ limit: z27.number().optional().default(50).describe("Maximum number of suggestions to return")
15150
+ },
15151
+ async ({ limit }) => {
15152
+ const stateDb2 = getStateDb();
15153
+ if (!stateDb2) {
15154
+ return {
15155
+ content: [{ type: "text", text: JSON.stringify({ suggestions: [], error: "StateDb not available" }) }]
15156
+ };
15157
+ }
15158
+ const entities = getAllEntitiesFromDb2(stateDb2);
15159
+ if (entities.length === 0) {
15160
+ return {
15161
+ content: [{ type: "text", text: JSON.stringify({ suggestions: [] }) }]
15162
+ };
15163
+ }
15164
+ const dismissedPairs = getDismissedMergePairs(stateDb2);
15165
+ const suggestions = [];
15166
+ const seen = /* @__PURE__ */ new Set();
15167
+ for (let i = 0; i < entities.length; i++) {
15168
+ for (let j = i + 1; j < entities.length; j++) {
15169
+ const a = entities[i];
15170
+ const b = entities[j];
15171
+ if (a.path === b.path) continue;
15172
+ const pairKey = [a.path, b.path].sort().join("::");
15173
+ if (seen.has(pairKey)) continue;
15174
+ if (dismissedPairs.has(pairKey)) continue;
15175
+ const aLower = a.name.toLowerCase();
15176
+ const bLower = b.name.toLowerCase();
15177
+ const aNorm = normalizeName(a.name);
15178
+ const bNorm = normalizeName(b.name);
15179
+ let reason = "";
15180
+ let confidence = 0;
15181
+ if (aLower === bLower) {
15182
+ reason = "exact name match (case-insensitive)";
15183
+ confidence = 0.95;
15184
+ } else if (aNorm === bNorm && aNorm.length >= 3) {
15185
+ reason = "normalized name match";
15186
+ confidence = 0.85;
15187
+ } else if (aLower.length >= 3 && bLower.length >= 3) {
15188
+ if (aLower.includes(bLower) || bLower.includes(aLower)) {
15189
+ const shorter = aLower.length <= bLower.length ? aLower : bLower;
15190
+ const longer = aLower.length > bLower.length ? aLower : bLower;
15191
+ const ratio = shorter.length / longer.length;
15192
+ if (ratio > 0.5) {
15193
+ reason = "substring match";
15194
+ confidence = 0.6 + ratio * 0.2;
15195
+ }
15196
+ }
15197
+ }
15198
+ if (!reason && aLower.length >= 4 && bLower.length >= 4) {
15199
+ const maxLen = Math.max(aLower.length, bLower.length);
15200
+ const dist = levenshteinDistance(aLower, bLower);
15201
+ const ratio = dist / maxLen;
15202
+ if (ratio < 0.35) {
15203
+ reason = `similar name (edit distance ${dist})`;
15204
+ confidence = 0.5 + (1 - ratio) * 0.4;
15205
+ }
15206
+ }
15207
+ if (!reason) continue;
15208
+ seen.add(pairKey);
15209
+ const aHub = a.hubScore ?? 0;
15210
+ const bHub = b.hubScore ?? 0;
15211
+ let source = a;
15212
+ let target = b;
15213
+ if (aHub > bHub || aHub === bHub && a.name.length > b.name.length) {
15214
+ source = b;
15215
+ target = a;
15216
+ }
15217
+ suggestions.push({
15218
+ source: {
15219
+ name: source.name,
15220
+ path: source.path,
15221
+ category: source.category,
15222
+ hubScore: source.hubScore ?? 0,
15223
+ aliases: source.aliases ?? []
15224
+ },
15225
+ target: {
15226
+ name: target.name,
15227
+ path: target.path,
15228
+ category: target.category,
15229
+ hubScore: target.hubScore ?? 0,
15230
+ aliases: target.aliases ?? []
15231
+ },
15232
+ reason,
15233
+ confidence
15234
+ });
15235
+ }
15236
+ }
15237
+ suggestions.sort((a, b) => b.confidence - a.confidence);
15238
+ const result = {
15239
+ suggestions: suggestions.slice(0, limit),
15240
+ total_candidates: suggestions.length
15241
+ };
15242
+ return {
15243
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
15244
+ };
15245
+ }
15246
+ );
15247
+ server2.tool(
15248
+ "dismiss_merge_suggestion",
15249
+ "Permanently dismiss a merge suggestion so it never reappears",
15250
+ {
15251
+ source_path: z27.string().describe("Path of the source entity"),
15252
+ target_path: z27.string().describe("Path of the target entity"),
15253
+ source_name: z27.string().describe("Name of the source entity"),
15254
+ target_name: z27.string().describe("Name of the target entity"),
15255
+ reason: z27.string().describe("Original suggestion reason")
15256
+ },
15257
+ async ({ source_path, target_path, source_name, target_name, reason }) => {
15258
+ const stateDb2 = getStateDb();
15259
+ if (!stateDb2) {
15260
+ return {
15261
+ content: [{ type: "text", text: JSON.stringify({ dismissed: false, error: "StateDb not available" }) }]
15262
+ };
15263
+ }
15264
+ recordMergeDismissal(stateDb2, source_path, target_path, source_name, target_name, reason);
15265
+ const pairKey = [source_path, target_path].sort().join("::");
15266
+ return {
15267
+ content: [{ type: "text", text: JSON.stringify({ dismissed: true, pair_key: pairKey }) }]
15268
+ };
15269
+ }
15270
+ );
15271
+ }
15272
+
14539
15273
  // src/resources/vault.ts
14540
15274
  function registerVaultResources(server2, getIndex) {
14541
15275
  server2.registerResource(
@@ -14708,11 +15442,11 @@ function parseEnabledCategories() {
14708
15442
  categories.add(c);
14709
15443
  }
14710
15444
  } else {
14711
- console.error(`[Memory] Warning: Unknown tool category "${item}" - ignoring`);
15445
+ serverLog("server", `Unknown tool category "${item}" \u2014 ignoring`, "warn");
14712
15446
  }
14713
15447
  }
14714
15448
  if (categories.size === 0) {
14715
- console.error(`[Memory] No valid categories found, using default (${DEFAULT_PRESET})`);
15449
+ serverLog("server", `No valid categories found, using default (${DEFAULT_PRESET})`, "warn");
14716
15450
  return new Set(PRESETS[DEFAULT_PRESET]);
14717
15451
  }
14718
15452
  return categories;
@@ -14781,7 +15515,16 @@ var TOOL_CATEGORY = {
14781
15515
  // health (activity tracking)
14782
15516
  vault_activity: "health",
14783
15517
  // schema (content similarity)
14784
- find_similar: "schema"
15518
+ find_similar: "schema",
15519
+ // health (config management)
15520
+ flywheel_config: "health",
15521
+ // health (server activity log)
15522
+ server_log: "health",
15523
+ // health (merge suggestions)
15524
+ suggest_entity_merges: "health",
15525
+ dismiss_merge_suggestion: "health",
15526
+ // notes (entity merge)
15527
+ merge_entities: "notes"
14785
15528
  };
14786
15529
  var server = new McpServer({
14787
15530
  name: "flywheel-memory",
@@ -14858,7 +15601,7 @@ if (_originalRegisterTool) {
14858
15601
  };
14859
15602
  }
14860
15603
  var categoryList = Array.from(enabledCategories).sort().join(", ");
14861
- console.error(`[Memory] Tool categories: ${categoryList}`);
15604
+ serverLog("server", `Tool categories: ${categoryList}`);
14862
15605
  registerHealthTools(server, () => vaultIndex, () => vaultPath, () => flywheelConfig, () => stateDb);
14863
15606
  registerSystemTools(
14864
15607
  server,
@@ -14876,19 +15619,28 @@ registerGraphTools(server, () => vaultIndex, () => vaultPath);
14876
15619
  registerWikilinkTools(server, () => vaultIndex, () => vaultPath);
14877
15620
  registerQueryTools(server, () => vaultIndex, () => vaultPath, () => stateDb);
14878
15621
  registerPrimitiveTools(server, () => vaultIndex, () => vaultPath, () => flywheelConfig);
14879
- registerGraphAnalysisTools(server, () => vaultIndex, () => vaultPath, () => stateDb);
15622
+ registerGraphAnalysisTools(server, () => vaultIndex, () => vaultPath, () => stateDb, () => flywheelConfig);
14880
15623
  registerVaultSchemaTools(server, () => vaultIndex, () => vaultPath);
14881
- registerNoteIntelligenceTools(server, () => vaultIndex, () => vaultPath);
15624
+ registerNoteIntelligenceTools(server, () => vaultIndex, () => vaultPath, () => flywheelConfig);
14882
15625
  registerMigrationTools(server, () => vaultIndex, () => vaultPath);
14883
15626
  registerMutationTools(server, vaultPath, () => flywheelConfig);
14884
15627
  registerTaskTools(server, vaultPath);
14885
15628
  registerFrontmatterTools(server, vaultPath);
14886
15629
  registerNoteTools(server, vaultPath, () => vaultIndex);
14887
15630
  registerMoveNoteTools(server, vaultPath);
15631
+ registerMergeTools(server, vaultPath);
14888
15632
  registerSystemTools2(server, vaultPath);
14889
15633
  registerPolicyTools(server, vaultPath);
14890
15634
  registerTagTools(server, () => vaultIndex, () => vaultPath);
14891
15635
  registerWikilinkFeedbackTools(server, () => stateDb);
15636
+ registerConfigTools(
15637
+ server,
15638
+ () => flywheelConfig,
15639
+ (newConfig) => {
15640
+ flywheelConfig = newConfig;
15641
+ },
15642
+ () => stateDb
15643
+ );
14892
15644
  registerMetricsTools(server, () => vaultIndex, () => stateDb);
14893
15645
  registerActivityTools(server, () => stateDb, () => {
14894
15646
  try {
@@ -14899,66 +15651,68 @@ registerActivityTools(server, () => stateDb, () => {
14899
15651
  });
14900
15652
  registerSimilarityTools(server, () => vaultIndex, () => vaultPath, () => stateDb);
14901
15653
  registerSemanticTools(server, () => vaultPath, () => stateDb);
15654
+ registerMergeTools2(server, () => stateDb);
14902
15655
  registerVaultResources(server, () => vaultIndex ?? null);
14903
- console.error(`[Memory] Registered ${_registeredCount} tools, skipped ${_skippedCount}`);
15656
+ serverLog("server", `Registered ${_registeredCount} tools, skipped ${_skippedCount}`);
14904
15657
  async function main() {
14905
- console.error(`[Memory] Starting Flywheel Memory server...`);
14906
- console.error(`[Memory] Vault: ${vaultPath}`);
15658
+ serverLog("server", "Starting Flywheel Memory server...");
15659
+ serverLog("server", `Vault: ${vaultPath}`);
14907
15660
  const startTime = Date.now();
14908
15661
  try {
14909
15662
  stateDb = openStateDb(vaultPath);
14910
- console.error("[Memory] StateDb initialized");
15663
+ serverLog("statedb", "StateDb initialized");
14911
15664
  setFTS5Database(stateDb.db);
14912
15665
  setEmbeddingsDatabase(stateDb.db);
15666
+ setTaskCacheDatabase(stateDb.db);
14913
15667
  loadEntityEmbeddingsToMemory();
14914
15668
  setWriteStateDb(stateDb);
14915
15669
  } catch (err) {
14916
15670
  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");
15671
+ serverLog("statedb", `StateDb initialization failed: ${msg}`, "error");
15672
+ serverLog("server", "Auto-wikilinks will be disabled for this session", "warn");
14919
15673
  }
14920
15674
  const transport = new StdioServerTransport();
14921
15675
  await server.connect(transport);
14922
- console.error("[Memory] MCP server connected");
15676
+ serverLog("server", "MCP server connected");
14923
15677
  initializeLogger(vaultPath).then(() => {
14924
15678
  const logger3 = getLogger();
14925
15679
  if (logger3?.enabled) {
14926
- console.error(`[Memory] Unified logging enabled`);
15680
+ serverLog("server", "Unified logging enabled");
14927
15681
  }
14928
15682
  }).catch(() => {
14929
15683
  });
14930
15684
  initializeLogger2(vaultPath).catch((err) => {
14931
- console.error(`[Memory] Write logger initialization failed: ${err}`);
15685
+ serverLog("server", `Write logger initialization failed: ${err}`, "error");
14932
15686
  });
14933
15687
  if (process.env.FLYWHEEL_SKIP_FTS5 !== "true") {
14934
15688
  if (isIndexStale(vaultPath)) {
14935
15689
  buildFTS5Index(vaultPath).then(() => {
14936
- console.error("[Memory] FTS5 search index ready");
15690
+ serverLog("fts5", "Search index ready");
14937
15691
  }).catch((err) => {
14938
- console.error("[Memory] FTS5 build failed:", err);
15692
+ serverLog("fts5", `Build failed: ${err instanceof Error ? err.message : err}`, "error");
14939
15693
  });
14940
15694
  } else {
14941
- console.error("[Memory] FTS5 search index already fresh, skipping rebuild");
15695
+ serverLog("fts5", "Search index already fresh, skipping rebuild");
14942
15696
  }
14943
15697
  } else {
14944
- console.error("[Memory] FTS5 indexing skipped (FLYWHEEL_SKIP_FTS5=true)");
15698
+ serverLog("fts5", "Skipping \u2014 FLYWHEEL_SKIP_FTS5");
14945
15699
  }
14946
15700
  let cachedIndex = null;
14947
15701
  if (stateDb) {
14948
15702
  try {
14949
15703
  const files = await scanVault(vaultPath);
14950
15704
  const noteCount = files.length;
14951
- console.error(`[Memory] Found ${noteCount} markdown files`);
15705
+ serverLog("index", `Found ${noteCount} markdown files`);
14952
15706
  cachedIndex = loadVaultIndexFromCache(stateDb, noteCount);
14953
15707
  } catch (err) {
14954
- console.error("[Memory] Cache check failed:", err);
15708
+ serverLog("index", `Cache check failed: ${err instanceof Error ? err.message : err}`, "warn");
14955
15709
  }
14956
15710
  }
14957
15711
  if (cachedIndex) {
14958
15712
  vaultIndex = cachedIndex;
14959
15713
  setIndexState("ready");
14960
15714
  const duration = Date.now() - startTime;
14961
- console.error(`[Memory] Index loaded from cache in ${duration}ms`);
15715
+ serverLog("index", `Loaded from cache in ${duration}ms \u2014 ${cachedIndex.notes.size} notes`);
14962
15716
  if (stateDb) {
14963
15717
  recordIndexEvent(stateDb, {
14964
15718
  trigger: "startup_cache",
@@ -14968,12 +15722,12 @@ async function main() {
14968
15722
  }
14969
15723
  runPostIndexWork(vaultIndex);
14970
15724
  } else {
14971
- console.error("[Memory] Building vault index...");
15725
+ serverLog("index", "Building vault index...");
14972
15726
  try {
14973
15727
  vaultIndex = await buildVaultIndex(vaultPath);
14974
15728
  setIndexState("ready");
14975
15729
  const duration = Date.now() - startTime;
14976
- console.error(`[Memory] Vault index ready in ${duration}ms`);
15730
+ serverLog("index", `Vault index ready in ${duration}ms \u2014 ${vaultIndex.notes.size} notes`);
14977
15731
  if (stateDb) {
14978
15732
  recordIndexEvent(stateDb, {
14979
15733
  trigger: "startup_build",
@@ -14984,9 +15738,9 @@ async function main() {
14984
15738
  if (stateDb) {
14985
15739
  try {
14986
15740
  saveVaultIndexToCache(stateDb, vaultIndex);
14987
- console.error("[Memory] Index cache saved");
15741
+ serverLog("index", "Index cache saved");
14988
15742
  } catch (err) {
14989
- console.error("[Memory] Failed to save index cache:", err);
15743
+ serverLog("index", `Failed to save index cache: ${err instanceof Error ? err.message : err}`, "error");
14990
15744
  }
14991
15745
  }
14992
15746
  await runPostIndexWork(vaultIndex);
@@ -15002,7 +15756,7 @@ async function main() {
15002
15756
  error: err instanceof Error ? err.message : String(err)
15003
15757
  });
15004
15758
  }
15005
- console.error("[Memory] Failed to build vault index:", err);
15759
+ serverLog("index", `Failed to build vault index: ${err instanceof Error ? err.message : err}`, "error");
15006
15760
  }
15007
15761
  }
15008
15762
  }
@@ -15033,9 +15787,9 @@ async function updateEntitiesInStateDb() {
15033
15787
  ]
15034
15788
  });
15035
15789
  stateDb.replaceAllEntities(entityIndex2);
15036
- console.error(`[Memory] Updated ${entityIndex2._metadata.total_entities} entities in StateDb`);
15790
+ serverLog("index", `Updated ${entityIndex2._metadata.total_entities} entities in StateDb`);
15037
15791
  } catch (e) {
15038
- console.error("[Memory] Failed to update entities in StateDb:", e);
15792
+ serverLog("index", `Failed to update entities in StateDb: ${e instanceof Error ? e.message : e}`, "error");
15039
15793
  }
15040
15794
  }
15041
15795
  async function runPostIndexWork(index) {
@@ -15049,9 +15803,9 @@ async function runPostIndexWork(index) {
15049
15803
  purgeOldMetrics(stateDb, 90);
15050
15804
  purgeOldIndexEvents(stateDb, 90);
15051
15805
  purgeOldInvocations(stateDb, 90);
15052
- console.error("[Memory] Growth metrics recorded");
15806
+ serverLog("server", "Growth metrics recorded");
15053
15807
  } catch (err) {
15054
- console.error("[Memory] Failed to record metrics:", err);
15808
+ serverLog("server", `Failed to record metrics: ${err instanceof Error ? err.message : err}`, "error");
15055
15809
  }
15056
15810
  }
15057
15811
  if (stateDb) {
@@ -15060,14 +15814,14 @@ async function runPostIndexWork(index) {
15060
15814
  recordGraphSnapshot(stateDb, graphMetrics);
15061
15815
  purgeOldSnapshots(stateDb, 90);
15062
15816
  } catch (err) {
15063
- console.error("[Memory] Failed to record graph snapshot:", err);
15817
+ serverLog("server", `Failed to record graph snapshot: ${err instanceof Error ? err.message : err}`, "error");
15064
15818
  }
15065
15819
  }
15066
15820
  if (stateDb) {
15067
15821
  try {
15068
15822
  updateSuppressionList(stateDb);
15069
15823
  } catch (err) {
15070
- console.error("[Memory] Failed to update suppression list:", err);
15824
+ serverLog("server", `Failed to update suppression list: ${err instanceof Error ? err.message : err}`, "error");
15071
15825
  }
15072
15826
  }
15073
15827
  const existing = loadConfig(stateDb);
@@ -15076,21 +15830,25 @@ async function runPostIndexWork(index) {
15076
15830
  saveConfig(stateDb, inferred, existing);
15077
15831
  }
15078
15832
  flywheelConfig = loadConfig(stateDb);
15833
+ if (stateDb) {
15834
+ refreshIfStale(vaultPath, index, flywheelConfig.exclude_task_tags);
15835
+ serverLog("tasks", "Task cache ready");
15836
+ }
15079
15837
  if (flywheelConfig.vault_name) {
15080
- console.error(`[Memory] Vault: ${flywheelConfig.vault_name}`);
15838
+ serverLog("config", `Vault: ${flywheelConfig.vault_name}`);
15081
15839
  }
15082
15840
  if (process.env.FLYWHEEL_SKIP_EMBEDDINGS !== "true") {
15083
15841
  if (hasEmbeddingsIndex()) {
15084
- console.error("[Memory] Embeddings already built, skipping full scan");
15842
+ serverLog("semantic", "Embeddings already built, skipping full scan");
15085
15843
  } else {
15086
15844
  setEmbeddingsBuilding(true);
15087
15845
  buildEmbeddingsIndex(vaultPath, (p) => {
15088
15846
  if (p.current % 100 === 0 || p.current === p.total) {
15089
- console.error(`[Semantic] ${p.current}/${p.total}`);
15847
+ serverLog("semantic", `Embedding ${p.current}/${p.total} notes...`);
15090
15848
  }
15091
15849
  }).then(async () => {
15092
15850
  if (stateDb) {
15093
- const entities = getAllEntitiesFromDb2(stateDb);
15851
+ const entities = getAllEntitiesFromDb3(stateDb);
15094
15852
  if (entities.length > 0) {
15095
15853
  const entityMap = new Map(entities.map((e) => [e.name, {
15096
15854
  name: e.name,
@@ -15102,29 +15860,29 @@ async function runPostIndexWork(index) {
15102
15860
  }
15103
15861
  }
15104
15862
  loadEntityEmbeddingsToMemory();
15105
- console.error("[Memory] Embeddings refreshed");
15863
+ serverLog("semantic", "Embeddings ready");
15106
15864
  }).catch((err) => {
15107
- console.error("[Memory] Embeddings refresh failed:", err);
15865
+ serverLog("semantic", `Embeddings build failed: ${err instanceof Error ? err.message : err}`, "error");
15108
15866
  });
15109
15867
  }
15110
15868
  } else {
15111
- console.error("[Memory] Embeddings skipped (FLYWHEEL_SKIP_EMBEDDINGS=true)");
15869
+ serverLog("semantic", "Skipping \u2014 FLYWHEEL_SKIP_EMBEDDINGS");
15112
15870
  }
15113
15871
  if (process.env.FLYWHEEL_WATCH !== "false") {
15114
15872
  const config = parseWatcherConfig();
15115
- console.error(`[Memory] File watcher enabled (debounce: ${config.debounceMs}ms)`);
15873
+ serverLog("watcher", `File watcher enabled (debounce: ${config.debounceMs}ms)`);
15116
15874
  const watcher = createVaultWatcher({
15117
15875
  vaultPath,
15118
15876
  config,
15119
15877
  onBatch: async (batch) => {
15120
- console.error(`[Memory] Processing ${batch.events.length} file changes`);
15878
+ serverLog("watcher", `Processing ${batch.events.length} file changes`);
15121
15879
  const batchStart = Date.now();
15122
15880
  const changedPaths = batch.events.map((e) => e.path);
15123
15881
  try {
15124
15882
  vaultIndex = await buildVaultIndex(vaultPath);
15125
15883
  setIndexState("ready");
15126
15884
  const duration = Date.now() - batchStart;
15127
- console.error(`[Memory] Index rebuilt in ${duration}ms`);
15885
+ serverLog("watcher", `Index rebuilt in ${duration}ms`);
15128
15886
  if (stateDb) {
15129
15887
  recordIndexEvent(stateDb, {
15130
15888
  trigger: "watcher",
@@ -15142,7 +15900,7 @@ async function runPostIndexWork(index) {
15142
15900
  if (event.type === "delete") {
15143
15901
  removeEmbedding(event.path);
15144
15902
  } else if (event.path.endsWith(".md")) {
15145
- const absPath = path28.join(vaultPath, event.path);
15903
+ const absPath = path29.join(vaultPath, event.path);
15146
15904
  await updateEmbedding(event.path, absPath);
15147
15905
  }
15148
15906
  } catch {
@@ -15151,7 +15909,7 @@ async function runPostIndexWork(index) {
15151
15909
  }
15152
15910
  if (hasEntityEmbeddingsIndex() && stateDb) {
15153
15911
  try {
15154
- const allEntities = getAllEntitiesFromDb2(stateDb);
15912
+ const allEntities = getAllEntitiesFromDb3(stateDb);
15155
15913
  for (const event of batch.events) {
15156
15914
  if (event.type === "delete" || !event.path.endsWith(".md")) continue;
15157
15915
  const matching = allEntities.filter((e) => e.path === event.path);
@@ -15171,7 +15929,17 @@ async function runPostIndexWork(index) {
15171
15929
  try {
15172
15930
  saveVaultIndexToCache(stateDb, vaultIndex);
15173
15931
  } catch (err) {
15174
- console.error("[Memory] Failed to update index cache:", err);
15932
+ serverLog("index", `Failed to update index cache: ${err instanceof Error ? err.message : err}`, "error");
15933
+ }
15934
+ }
15935
+ for (const event of batch.events) {
15936
+ try {
15937
+ if (event.type === "delete") {
15938
+ removeTaskCacheForFile(event.path);
15939
+ } else if (event.path.endsWith(".md")) {
15940
+ await updateTaskCacheForFile(vaultPath, event.path);
15941
+ }
15942
+ } catch {
15175
15943
  }
15176
15944
  }
15177
15945
  } catch (err) {
@@ -15188,16 +15956,16 @@ async function runPostIndexWork(index) {
15188
15956
  error: err instanceof Error ? err.message : String(err)
15189
15957
  });
15190
15958
  }
15191
- console.error("[Memory] Failed to rebuild index:", err);
15959
+ serverLog("watcher", `Failed to rebuild index: ${err instanceof Error ? err.message : err}`, "error");
15192
15960
  }
15193
15961
  },
15194
15962
  onStateChange: (status) => {
15195
15963
  if (status.state === "dirty") {
15196
- console.error("[Memory] Warning: Index may be stale");
15964
+ serverLog("watcher", "Index may be stale", "warn");
15197
15965
  }
15198
15966
  },
15199
15967
  onError: (err) => {
15200
- console.error("[Memory] Watcher error:", err.message);
15968
+ serverLog("watcher", `Watcher error: ${err.message}`, "error");
15201
15969
  }
15202
15970
  });
15203
15971
  watcher.start();
@@ -15208,15 +15976,15 @@ if (process.argv.includes("--init-semantic")) {
15208
15976
  console.error("[Semantic] Pre-warming semantic search...");
15209
15977
  console.error(`[Semantic] Vault: ${vaultPath}`);
15210
15978
  try {
15211
- const db3 = openStateDb(vaultPath);
15212
- setEmbeddingsDatabase(db3.db);
15979
+ const db4 = openStateDb(vaultPath);
15980
+ setEmbeddingsDatabase(db4.db);
15213
15981
  const progress = await buildEmbeddingsIndex(vaultPath, (p) => {
15214
15982
  if (p.current % 50 === 0 || p.current === p.total) {
15215
15983
  console.error(`[Semantic] Embedding ${p.current}/${p.total} notes (${p.skipped} skipped)...`);
15216
15984
  }
15217
15985
  });
15218
15986
  console.error(`[Semantic] Done. Embedded ${progress.total - progress.skipped} notes, skipped ${progress.skipped}.`);
15219
- db3.close();
15987
+ db4.close();
15220
15988
  process.exit(0);
15221
15989
  } catch (err) {
15222
15990
  console.error("[Semantic] Failed:", err instanceof Error ? err.message : err);