@velvetmonkey/flywheel-memory 2.0.26 → 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 +1465 -631
  2. package/package.json +3 -3
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,19 +7679,58 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
7174
7679
  };
7175
7680
  }
7176
7681
  );
7177
- }
7178
-
7179
- // src/tools/read/query.ts
7180
- import { z as z4 } from "zod";
7181
- import {
7182
- searchEntities,
7183
- searchEntitiesPrefix
7184
- } from "@velvetmonkey/vault-core";
7185
- function matchesFrontmatter(note, where) {
7186
- for (const [key, value] of Object.entries(where)) {
7187
- const noteValue = note.frontmatter[key];
7188
- if (value === null || value === void 0) {
7189
- if (noteValue !== null && noteValue !== void 0) {
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
+ }
7722
+
7723
+ // src/tools/read/query.ts
7724
+ import { z as z4 } from "zod";
7725
+ import {
7726
+ searchEntities,
7727
+ searchEntitiesPrefix
7728
+ } from "@velvetmonkey/vault-core";
7729
+ function matchesFrontmatter(note, where) {
7730
+ for (const [key, value] of Object.entries(where)) {
7731
+ const noteValue = note.frontmatter[key];
7732
+ if (value === null || value === void 0) {
7733
+ if (noteValue !== null && noteValue !== void 0) {
7190
7734
  return false;
7191
7735
  }
7192
7736
  continue;
@@ -7451,10 +7995,10 @@ 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
- import { scanVaultEntities as scanVaultEntities2 } from "@velvetmonkey/vault-core";
8001
+ import { scanVaultEntities as scanVaultEntities2, getEntityIndexFromDb as getEntityIndexFromDb2 } from "@velvetmonkey/vault-core";
7458
8002
  function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfig, getStateDb) {
7459
8003
  const RefreshIndexOutputSchema = {
7460
8004
  success: z5.boolean().describe("Whether the refresh succeeded"),
@@ -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
  }
@@ -7898,15 +8442,58 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
7898
8442
  };
7899
8443
  }
7900
8444
  );
8445
+ server2.registerTool(
8446
+ "list_entities",
8447
+ {
8448
+ title: "List Entities",
8449
+ description: "Get all entities grouped by category with aliases and hub scores. Returns the full EntityIndex from StateDb.",
8450
+ inputSchema: {
8451
+ category: z5.string().optional().describe('Filter to a specific category (e.g. "people", "technologies")'),
8452
+ limit: z5.coerce.number().default(2e3).describe("Maximum entities per category")
8453
+ }
8454
+ },
8455
+ async ({
8456
+ category,
8457
+ limit: perCategoryLimit
8458
+ }) => {
8459
+ const stateDb2 = getStateDb?.();
8460
+ if (!stateDb2) {
8461
+ return {
8462
+ content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
8463
+ };
8464
+ }
8465
+ const entityIndex2 = getEntityIndexFromDb2(stateDb2);
8466
+ if (category) {
8467
+ const allCategories = Object.keys(entityIndex2).filter((k) => k !== "_metadata");
8468
+ for (const cat of allCategories) {
8469
+ if (cat !== category) {
8470
+ entityIndex2[cat] = [];
8471
+ }
8472
+ }
8473
+ }
8474
+ if (perCategoryLimit) {
8475
+ const allCategories = Object.keys(entityIndex2).filter((k) => k !== "_metadata");
8476
+ for (const cat of allCategories) {
8477
+ const arr = entityIndex2[cat];
8478
+ if (Array.isArray(arr) && arr.length > perCategoryLimit) {
8479
+ entityIndex2[cat] = arr.slice(0, perCategoryLimit);
8480
+ }
8481
+ }
8482
+ }
8483
+ return {
8484
+ content: [{ type: "text", text: JSON.stringify(entityIndex2) }]
8485
+ };
8486
+ }
8487
+ );
7901
8488
  }
7902
8489
 
7903
8490
  // src/tools/read/primitives.ts
7904
8491
  import { z as z6 } from "zod";
7905
8492
 
7906
8493
  // src/tools/read/structure.ts
7907
- import * as fs11 from "fs";
7908
- import * as path11 from "path";
7909
- 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+(.+)$/;
7910
8497
  function extractHeadings(content) {
7911
8498
  const lines = content.split("\n");
7912
8499
  const headings = [];
@@ -7918,7 +8505,7 @@ function extractHeadings(content) {
7918
8505
  continue;
7919
8506
  }
7920
8507
  if (inCodeBlock) continue;
7921
- const match = line.match(HEADING_REGEX);
8508
+ const match = line.match(HEADING_REGEX2);
7922
8509
  if (match) {
7923
8510
  headings.push({
7924
8511
  level: match[1].length,
@@ -7959,10 +8546,10 @@ function buildSections(headings, totalLines) {
7959
8546
  async function getNoteStructure(index, notePath, vaultPath2) {
7960
8547
  const note = index.notes.get(notePath);
7961
8548
  if (!note) return null;
7962
- const absolutePath = path11.join(vaultPath2, notePath);
8549
+ const absolutePath = path13.join(vaultPath2, notePath);
7963
8550
  let content;
7964
8551
  try {
7965
- content = await fs11.promises.readFile(absolutePath, "utf-8");
8552
+ content = await fs12.promises.readFile(absolutePath, "utf-8");
7966
8553
  } catch {
7967
8554
  return null;
7968
8555
  }
@@ -7982,10 +8569,10 @@ async function getNoteStructure(index, notePath, vaultPath2) {
7982
8569
  async function getSectionContent(index, notePath, headingText, vaultPath2, includeSubheadings = true) {
7983
8570
  const note = index.notes.get(notePath);
7984
8571
  if (!note) return null;
7985
- const absolutePath = path11.join(vaultPath2, notePath);
8572
+ const absolutePath = path13.join(vaultPath2, notePath);
7986
8573
  let content;
7987
8574
  try {
7988
- content = await fs11.promises.readFile(absolutePath, "utf-8");
8575
+ content = await fs12.promises.readFile(absolutePath, "utf-8");
7989
8576
  } catch {
7990
8577
  return null;
7991
8578
  }
@@ -8024,10 +8611,10 @@ async function findSections(index, headingPattern, vaultPath2, folder) {
8024
8611
  const results = [];
8025
8612
  for (const note of index.notes.values()) {
8026
8613
  if (folder && !note.path.startsWith(folder)) continue;
8027
- const absolutePath = path11.join(vaultPath2, note.path);
8614
+ const absolutePath = path13.join(vaultPath2, note.path);
8028
8615
  let content;
8029
8616
  try {
8030
- content = await fs11.promises.readFile(absolutePath, "utf-8");
8617
+ content = await fs12.promises.readFile(absolutePath, "utf-8");
8031
8618
  } catch {
8032
8619
  continue;
8033
8620
  }
@@ -8046,127 +8633,6 @@ async function findSections(index, headingPattern, vaultPath2, folder) {
8046
8633
  return results;
8047
8634
  }
8048
8635
 
8049
- // src/tools/read/tasks.ts
8050
- import * as fs12 from "fs";
8051
- import * as path12 from "path";
8052
- var TASK_REGEX = /^(\s*)- \[([ xX\-])\]\s+(.+)$/;
8053
- var TAG_REGEX2 = /#([a-zA-Z][a-zA-Z0-9_/-]*)/g;
8054
- var DATE_REGEX = /\b(\d{4}-\d{2}-\d{2}|\d{1,2}\/\d{1,2}\/\d{2,4})\b/;
8055
- var HEADING_REGEX2 = /^(#{1,6})\s+(.+)$/;
8056
- function parseStatus(char) {
8057
- if (char === " ") return "open";
8058
- if (char === "-") return "cancelled";
8059
- return "completed";
8060
- }
8061
- function extractTags2(text) {
8062
- const tags = [];
8063
- let match;
8064
- TAG_REGEX2.lastIndex = 0;
8065
- while ((match = TAG_REGEX2.exec(text)) !== null) {
8066
- tags.push(match[1]);
8067
- }
8068
- return tags;
8069
- }
8070
- function extractDueDate(text) {
8071
- const match = text.match(DATE_REGEX);
8072
- return match ? match[1] : void 0;
8073
- }
8074
- async function extractTasksFromNote(notePath, absolutePath) {
8075
- let content;
8076
- try {
8077
- content = await fs12.promises.readFile(absolutePath, "utf-8");
8078
- } catch {
8079
- return [];
8080
- }
8081
- const lines = content.split("\n");
8082
- const tasks = [];
8083
- let currentHeading;
8084
- let inCodeBlock = false;
8085
- for (let i = 0; i < lines.length; i++) {
8086
- const line = lines[i];
8087
- if (line.startsWith("```")) {
8088
- inCodeBlock = !inCodeBlock;
8089
- continue;
8090
- }
8091
- if (inCodeBlock) continue;
8092
- const headingMatch = line.match(HEADING_REGEX2);
8093
- if (headingMatch) {
8094
- currentHeading = headingMatch[2].trim();
8095
- continue;
8096
- }
8097
- const taskMatch = line.match(TASK_REGEX);
8098
- if (taskMatch) {
8099
- const statusChar = taskMatch[2];
8100
- const text = taskMatch[3].trim();
8101
- tasks.push({
8102
- path: notePath,
8103
- line: i + 1,
8104
- text,
8105
- status: parseStatus(statusChar),
8106
- raw: line,
8107
- context: currentHeading,
8108
- tags: extractTags2(text),
8109
- due_date: extractDueDate(text)
8110
- });
8111
- }
8112
- }
8113
- return tasks;
8114
- }
8115
- async function getAllTasks(index, vaultPath2, options = {}) {
8116
- const { status = "all", folder, tag, excludeTags = [], limit } = options;
8117
- const allTasks = [];
8118
- for (const note of index.notes.values()) {
8119
- if (folder && !note.path.startsWith(folder)) continue;
8120
- const absolutePath = path12.join(vaultPath2, note.path);
8121
- const tasks = await extractTasksFromNote(note.path, absolutePath);
8122
- allTasks.push(...tasks);
8123
- }
8124
- let filteredTasks = allTasks;
8125
- if (status !== "all") {
8126
- filteredTasks = allTasks.filter((t) => t.status === status);
8127
- }
8128
- if (tag) {
8129
- filteredTasks = filteredTasks.filter((t) => t.tags.includes(tag));
8130
- }
8131
- if (excludeTags.length > 0) {
8132
- filteredTasks = filteredTasks.filter(
8133
- (t) => !excludeTags.some((excludeTag) => t.tags.includes(excludeTag))
8134
- );
8135
- }
8136
- const openCount = allTasks.filter((t) => t.status === "open").length;
8137
- const completedCount = allTasks.filter((t) => t.status === "completed").length;
8138
- const cancelledCount = allTasks.filter((t) => t.status === "cancelled").length;
8139
- const returnTasks = limit ? filteredTasks.slice(0, limit) : filteredTasks;
8140
- return {
8141
- total: allTasks.length,
8142
- open_count: openCount,
8143
- completed_count: completedCount,
8144
- cancelled_count: cancelledCount,
8145
- tasks: returnTasks
8146
- };
8147
- }
8148
- async function getTasksFromNote(index, notePath, vaultPath2, excludeTags = []) {
8149
- const note = index.notes.get(notePath);
8150
- if (!note) return null;
8151
- const absolutePath = path12.join(vaultPath2, notePath);
8152
- let tasks = await extractTasksFromNote(notePath, absolutePath);
8153
- if (excludeTags.length > 0) {
8154
- tasks = tasks.filter(
8155
- (t) => !excludeTags.some((excludeTag) => t.tags.includes(excludeTag))
8156
- );
8157
- }
8158
- return tasks;
8159
- }
8160
- async function getTasksWithDueDates(index, vaultPath2, options = {}) {
8161
- const { status = "open", folder, excludeTags } = options;
8162
- const result = await getAllTasks(index, vaultPath2, { status, folder, excludeTags });
8163
- return result.tasks.filter((t) => t.due_date).sort((a, b) => {
8164
- const dateA = a.due_date || "";
8165
- const dateB = b.due_date || "";
8166
- return dateA.localeCompare(dateB);
8167
- });
8168
- }
8169
-
8170
8636
  // src/tools/read/primitives.ts
8171
8637
  function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = () => ({})) {
8172
8638
  server2.registerTool(
@@ -8179,18 +8645,18 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
8179
8645
  include_content: z6.boolean().default(false).describe("Include the text content under each top-level section")
8180
8646
  }
8181
8647
  },
8182
- async ({ path: path29, include_content }) => {
8648
+ async ({ path: path30, include_content }) => {
8183
8649
  const index = getIndex();
8184
8650
  const vaultPath2 = getVaultPath();
8185
- const result = await getNoteStructure(index, path29, vaultPath2);
8651
+ const result = await getNoteStructure(index, path30, vaultPath2);
8186
8652
  if (!result) {
8187
8653
  return {
8188
- 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) }]
8189
8655
  };
8190
8656
  }
8191
8657
  if (include_content) {
8192
8658
  for (const section of result.sections) {
8193
- const sectionResult = await getSectionContent(index, path29, section.heading.text, vaultPath2, true);
8659
+ const sectionResult = await getSectionContent(index, path30, section.heading.text, vaultPath2, true);
8194
8660
  if (sectionResult) {
8195
8661
  section.content = sectionResult.content;
8196
8662
  }
@@ -8212,15 +8678,15 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
8212
8678
  include_subheadings: z6.boolean().default(true).describe("Include content under subheadings")
8213
8679
  }
8214
8680
  },
8215
- async ({ path: path29, heading, include_subheadings }) => {
8681
+ async ({ path: path30, heading, include_subheadings }) => {
8216
8682
  const index = getIndex();
8217
8683
  const vaultPath2 = getVaultPath();
8218
- const result = await getSectionContent(index, path29, heading, vaultPath2, include_subheadings);
8684
+ const result = await getSectionContent(index, path30, heading, vaultPath2, include_subheadings);
8219
8685
  if (!result) {
8220
8686
  return {
8221
8687
  content: [{ type: "text", text: JSON.stringify({
8222
8688
  error: "Section not found",
8223
- path: path29,
8689
+ path: path30,
8224
8690
  heading
8225
8691
  }, null, 2) }]
8226
8692
  };
@@ -8274,16 +8740,16 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
8274
8740
  offset: z6.coerce.number().default(0).describe("Number of results to skip (for pagination)")
8275
8741
  }
8276
8742
  },
8277
- 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 }) => {
8278
8744
  const limit = Math.min(requestedLimit ?? 25, MAX_LIMIT);
8279
8745
  const index = getIndex();
8280
8746
  const vaultPath2 = getVaultPath();
8281
8747
  const config = getConfig();
8282
- if (path29) {
8283
- 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 || []);
8284
8750
  if (!result2) {
8285
8751
  return {
8286
- 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) }]
8287
8753
  };
8288
8754
  }
8289
8755
  let filtered = result2;
@@ -8293,7 +8759,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
8293
8759
  const paged2 = filtered.slice(offset, offset + limit);
8294
8760
  return {
8295
8761
  content: [{ type: "text", text: JSON.stringify({
8296
- path: path29,
8762
+ path: path30,
8297
8763
  total_count: filtered.length,
8298
8764
  returned_count: paged2.length,
8299
8765
  open: result2.filter((t) => t.status === "open").length,
@@ -8302,6 +8768,42 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
8302
8768
  }, null, 2) }]
8303
8769
  };
8304
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
+ }
8305
8807
  if (has_due_date) {
8306
8808
  const allResults = await getTasksWithDueDates(index, vaultPath2, {
8307
8809
  status,
@@ -8409,7 +8911,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
8409
8911
  // src/tools/read/migrations.ts
8410
8912
  import { z as z7 } from "zod";
8411
8913
  import * as fs13 from "fs/promises";
8412
- import * as path13 from "path";
8914
+ import * as path14 from "path";
8413
8915
  import matter2 from "gray-matter";
8414
8916
  function getNotesInFolder(index, folder) {
8415
8917
  const notes = [];
@@ -8422,7 +8924,7 @@ function getNotesInFolder(index, folder) {
8422
8924
  return notes;
8423
8925
  }
8424
8926
  async function readFileContent(notePath, vaultPath2) {
8425
- const fullPath = path13.join(vaultPath2, notePath);
8927
+ const fullPath = path14.join(vaultPath2, notePath);
8426
8928
  try {
8427
8929
  return await fs13.readFile(fullPath, "utf-8");
8428
8930
  } catch {
@@ -8430,7 +8932,7 @@ async function readFileContent(notePath, vaultPath2) {
8430
8932
  }
8431
8933
  }
8432
8934
  async function writeFileContent(notePath, vaultPath2, content) {
8433
- const fullPath = path13.join(vaultPath2, notePath);
8935
+ const fullPath = path14.join(vaultPath2, notePath);
8434
8936
  try {
8435
8937
  await fs13.writeFile(fullPath, content, "utf-8");
8436
8938
  return true;
@@ -8611,7 +9113,7 @@ function registerMigrationTools(server2, getIndex, getVaultPath) {
8611
9113
 
8612
9114
  // src/tools/read/graphAnalysis.ts
8613
9115
  import fs14 from "node:fs";
8614
- import path14 from "node:path";
9116
+ import path15 from "node:path";
8615
9117
  import { z as z8 } from "zod";
8616
9118
 
8617
9119
  // src/tools/read/schema.ts
@@ -9165,7 +9667,26 @@ function purgeOldSnapshots(stateDb2, retentionDays = 90) {
9165
9667
  }
9166
9668
 
9167
9669
  // src/tools/read/graphAnalysis.ts
9168
- 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) {
9169
9690
  server2.registerTool(
9170
9691
  "graph_analysis",
9171
9692
  {
@@ -9188,7 +9709,7 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb)
9188
9709
  const index = getIndex();
9189
9710
  switch (analysis) {
9190
9711
  case "orphans": {
9191
- const allOrphans = findOrphanNotes(index, folder);
9712
+ const allOrphans = findOrphanNotes(index, folder).filter((o) => !isPeriodicNote(o.path));
9192
9713
  const orphans = allOrphans.slice(offset, offset + limit);
9193
9714
  return {
9194
9715
  content: [{ type: "text", text: JSON.stringify({
@@ -9231,7 +9752,17 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb)
9231
9752
  };
9232
9753
  }
9233
9754
  case "hubs": {
9234
- 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
+ });
9235
9766
  const hubs = allHubs.slice(offset, offset + limit);
9236
9767
  return {
9237
9768
  content: [{ type: "text", text: JSON.stringify({
@@ -9273,14 +9804,14 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb)
9273
9804
  case "immature": {
9274
9805
  const vaultPath2 = getVaultPath();
9275
9806
  const allNotes = Array.from(index.notes.values()).filter(
9276
- (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)
9277
9808
  );
9278
9809
  const conventions = inferFolderConventions(index, folder, 0.5);
9279
9810
  const expectedFields = conventions.inferred_fields.map((f) => f.name);
9280
9811
  const scored = allNotes.map((note) => {
9281
9812
  let wordCount = 0;
9282
9813
  try {
9283
- const content = fs14.readFileSync(path14.join(vaultPath2, note.path), "utf-8");
9814
+ const content = fs14.readFileSync(path15.join(vaultPath2, note.path), "utf-8");
9284
9815
  const body = content.replace(/^---[\s\S]*?---\n?/, "");
9285
9816
  wordCount = body.split(/\s+/).filter((w) => w.length > 0).length;
9286
9817
  } catch {
@@ -9329,8 +9860,8 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb)
9329
9860
  };
9330
9861
  }
9331
9862
  case "evolution": {
9332
- const db3 = getStateDb?.();
9333
- if (!db3) {
9863
+ const db4 = getStateDb?.();
9864
+ if (!db4) {
9334
9865
  return {
9335
9866
  content: [{ type: "text", text: JSON.stringify({
9336
9867
  error: "StateDb not available \u2014 graph evolution requires persistent state"
@@ -9338,7 +9869,7 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb)
9338
9869
  };
9339
9870
  }
9340
9871
  const daysBack = days ?? 30;
9341
- const evolutions = getGraphEvolution(db3, daysBack);
9872
+ const evolutions = getGraphEvolution(db4, daysBack);
9342
9873
  return {
9343
9874
  content: [{ type: "text", text: JSON.stringify({
9344
9875
  analysis: "evolution",
@@ -9348,8 +9879,8 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb)
9348
9879
  };
9349
9880
  }
9350
9881
  case "emerging_hubs": {
9351
- const db3 = getStateDb?.();
9352
- if (!db3) {
9882
+ const db4 = getStateDb?.();
9883
+ if (!db4) {
9353
9884
  return {
9354
9885
  content: [{ type: "text", text: JSON.stringify({
9355
9886
  error: "StateDb not available \u2014 emerging hubs requires persistent state"
@@ -9357,7 +9888,23 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb)
9357
9888
  };
9358
9889
  }
9359
9890
  const daysBack = days ?? 30;
9360
- 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
+ }
9361
9908
  return {
9362
9909
  content: [{ type: "text", text: JSON.stringify({
9363
9910
  analysis: "emerging_hubs",
@@ -9846,13 +10393,12 @@ import { z as z10 } from "zod";
9846
10393
 
9847
10394
  // src/tools/read/bidirectional.ts
9848
10395
  import * as fs15 from "fs/promises";
9849
- import * as path15 from "path";
10396
+ import * as path16 from "path";
9850
10397
  import matter3 from "gray-matter";
9851
10398
  var PROSE_PATTERN_REGEX = /^([A-Za-z][A-Za-z0-9 _-]*):\s*(?:\[\[([^\]]+)\]\]|"([^"]+)"|([^\n]+?))\s*$/gm;
9852
10399
  var CODE_BLOCK_REGEX2 = /```[\s\S]*?```|`[^`\n]+`/g;
9853
- var WIKILINK_REGEX2 = /\[\[([^\]|#]+)(?:#[^\]|]*)?(?:\|[^\]]+)?\]\]/g;
9854
10400
  async function readFileContent2(notePath, vaultPath2) {
9855
- const fullPath = path15.join(vaultPath2, notePath);
10401
+ const fullPath = path16.join(vaultPath2, notePath);
9856
10402
  try {
9857
10403
  return await fs15.readFile(fullPath, "utf-8");
9858
10404
  } catch {
@@ -9875,21 +10421,6 @@ function removeCodeBlocks(content) {
9875
10421
  return "\n".repeat(newlines);
9876
10422
  });
9877
10423
  }
9878
- function extractWikilinksFromValue(value) {
9879
- if (typeof value === "string") {
9880
- const matches = [];
9881
- let match;
9882
- WIKILINK_REGEX2.lastIndex = 0;
9883
- while ((match = WIKILINK_REGEX2.exec(value)) !== null) {
9884
- matches.push(match[1].trim());
9885
- }
9886
- return matches;
9887
- }
9888
- if (Array.isArray(value)) {
9889
- return value.flatMap((v) => extractWikilinksFromValue(v));
9890
- }
9891
- return [];
9892
- }
9893
10424
  function isWikilinkValue(value) {
9894
10425
  return /^\[\[.+\]\]$/.test(value.trim());
9895
10426
  }
@@ -10043,89 +10574,13 @@ async function suggestWikilinksInFrontmatter(index, notePath, vaultPath2) {
10043
10574
  suggestions
10044
10575
  };
10045
10576
  }
10046
- async function validateCrossLayer(index, notePath, vaultPath2) {
10047
- const content = await readFileContent2(notePath, vaultPath2);
10048
- if (content === null) {
10049
- return {
10050
- path: notePath,
10051
- frontmatter_only: [],
10052
- prose_only: [],
10053
- consistent: [],
10054
- error: "File not found"
10055
- };
10056
- }
10057
- let frontmatter = {};
10058
- let body = content;
10059
- try {
10060
- const parsed = matter3(content);
10061
- frontmatter = parsed.data;
10062
- body = parsed.content;
10063
- } catch {
10064
- }
10065
- const frontmatterRefs = /* @__PURE__ */ new Map();
10066
- for (const [field, value] of Object.entries(frontmatter)) {
10067
- const wikilinks = extractWikilinksFromValue(value);
10068
- for (const target of wikilinks) {
10069
- frontmatterRefs.set(normalizeRef(target), { field, target });
10070
- }
10071
- if (typeof value === "string" && !isWikilinkValue(value)) {
10072
- const normalized = normalizeRef(value);
10073
- if (index.entities.has(normalized)) {
10074
- frontmatterRefs.set(normalized, { field, target: value });
10075
- }
10076
- }
10077
- if (Array.isArray(value)) {
10078
- for (const v of value) {
10079
- if (typeof v === "string" && !isWikilinkValue(v)) {
10080
- const normalized = normalizeRef(v);
10081
- if (index.entities.has(normalized)) {
10082
- frontmatterRefs.set(normalized, { field, target: v });
10083
- }
10084
- }
10085
- }
10086
- }
10087
- }
10088
- const proseRefs = /* @__PURE__ */ new Map();
10089
- const cleanBody = removeCodeBlocks(body);
10090
- const lines = cleanBody.split("\n");
10091
- for (let i = 0; i < lines.length; i++) {
10092
- const line = lines[i];
10093
- WIKILINK_REGEX2.lastIndex = 0;
10094
- let match;
10095
- while ((match = WIKILINK_REGEX2.exec(line)) !== null) {
10096
- const target = match[1].trim();
10097
- proseRefs.set(normalizeRef(target), { line: i + 1, target });
10098
- }
10099
- }
10100
- const frontmatter_only = [];
10101
- const prose_only = [];
10102
- const consistent = [];
10103
- for (const [normalized, { field, target }] of frontmatterRefs) {
10104
- if (proseRefs.has(normalized)) {
10105
- consistent.push({ field, target });
10106
- } else {
10107
- frontmatter_only.push({ field, target });
10108
- }
10109
- }
10110
- for (const [normalized, { line, target }] of proseRefs) {
10111
- if (!frontmatterRefs.has(normalized)) {
10112
- prose_only.push({ pattern: `[[${target}]]`, target, line });
10113
- }
10114
- }
10115
- return {
10116
- path: notePath,
10117
- frontmatter_only,
10118
- prose_only,
10119
- consistent
10120
- };
10121
- }
10122
10577
 
10123
10578
  // src/tools/read/computed.ts
10124
10579
  import * as fs16 from "fs/promises";
10125
- import * as path16 from "path";
10580
+ import * as path17 from "path";
10126
10581
  import matter4 from "gray-matter";
10127
10582
  async function readFileContent3(notePath, vaultPath2) {
10128
- const fullPath = path16.join(vaultPath2, notePath);
10583
+ const fullPath = path17.join(vaultPath2, notePath);
10129
10584
  try {
10130
10585
  return await fs16.readFile(fullPath, "utf-8");
10131
10586
  } catch {
@@ -10133,7 +10588,7 @@ async function readFileContent3(notePath, vaultPath2) {
10133
10588
  }
10134
10589
  }
10135
10590
  async function getFileStats(notePath, vaultPath2) {
10136
- const fullPath = path16.join(vaultPath2, notePath);
10591
+ const fullPath = path17.join(vaultPath2, notePath);
10137
10592
  try {
10138
10593
  const stats = await fs16.stat(fullPath);
10139
10594
  return {
@@ -10266,18 +10721,17 @@ async function computeFrontmatter(index, notePath, vaultPath2, fields) {
10266
10721
  // src/tools/read/noteIntelligence.ts
10267
10722
  import fs17 from "node:fs";
10268
10723
  import nodePath from "node:path";
10269
- function registerNoteIntelligenceTools(server2, getIndex, getVaultPath) {
10724
+ function registerNoteIntelligenceTools(server2, getIndex, getVaultPath, getConfig) {
10270
10725
  server2.registerTool(
10271
10726
  "note_intelligence",
10272
10727
  {
10273
10728
  title: "Note Intelligence",
10274
- 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" })',
10275
10730
  inputSchema: {
10276
10731
  analysis: z10.enum([
10277
10732
  "prose_patterns",
10278
10733
  "suggest_frontmatter",
10279
10734
  "suggest_wikilinks",
10280
- "cross_layer",
10281
10735
  "compute",
10282
10736
  "semantic_links",
10283
10737
  "all"
@@ -10309,12 +10763,6 @@ function registerNoteIntelligenceTools(server2, getIndex, getVaultPath) {
10309
10763
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
10310
10764
  };
10311
10765
  }
10312
- case "cross_layer": {
10313
- const result = await validateCrossLayer(index, notePath, vaultPath2);
10314
- return {
10315
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
10316
- };
10317
- }
10318
10766
  case "compute": {
10319
10767
  const result = await computeFrontmatter(index, notePath, vaultPath2, fields);
10320
10768
  return {
@@ -10345,10 +10793,26 @@ function registerNoteIntelligenceTools(server2, getIndex, getVaultPath) {
10345
10793
  while ((wlMatch = wikilinkRegex.exec(noteContent)) !== null) {
10346
10794
  linkedEntities.add(wlMatch[1].toLowerCase());
10347
10795
  }
10796
+ const excludeTags = new Set(
10797
+ (getConfig?.()?.exclude_analysis_tags ?? []).map((t) => t.toLowerCase())
10798
+ );
10348
10799
  try {
10349
10800
  const contentEmbedding = await embedTextCached(noteContent);
10350
10801
  const matches = findSemanticallySimilarEntities(contentEmbedding, 20, linkedEntities);
10351
- 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) => ({
10352
10816
  entity: m.entityName,
10353
10817
  similarity: m.similarity
10354
10818
  }));
@@ -10370,11 +10834,10 @@ function registerNoteIntelligenceTools(server2, getIndex, getVaultPath) {
10370
10834
  }
10371
10835
  }
10372
10836
  case "all": {
10373
- const [prosePatterns, suggestedFrontmatter, suggestedWikilinks, crossLayer, computed] = await Promise.all([
10837
+ const [prosePatterns, suggestedFrontmatter, suggestedWikilinks, computed] = await Promise.all([
10374
10838
  detectProsePatterns(index, notePath, vaultPath2),
10375
10839
  suggestFrontmatterFromProse(index, notePath, vaultPath2),
10376
10840
  suggestWikilinksInFrontmatter(index, notePath, vaultPath2),
10377
- validateCrossLayer(index, notePath, vaultPath2),
10378
10841
  computeFrontmatter(index, notePath, vaultPath2, fields)
10379
10842
  ]);
10380
10843
  return {
@@ -10383,7 +10846,6 @@ function registerNoteIntelligenceTools(server2, getIndex, getVaultPath) {
10383
10846
  prose_patterns: prosePatterns,
10384
10847
  suggested_frontmatter: suggestedFrontmatter,
10385
10848
  suggested_wikilinks: suggestedWikilinks,
10386
- cross_layer: crossLayer,
10387
10849
  computed
10388
10850
  }, null, 2) }]
10389
10851
  };
@@ -10397,7 +10859,7 @@ function registerNoteIntelligenceTools(server2, getIndex, getVaultPath) {
10397
10859
  init_writer();
10398
10860
  import { z as z11 } from "zod";
10399
10861
  import fs20 from "fs/promises";
10400
- import path19 from "path";
10862
+ import path20 from "path";
10401
10863
 
10402
10864
  // src/core/write/validator.ts
10403
10865
  var TIMESTAMP_PATTERN = /^\*\*\d{2}:\d{2}\*\*/;
@@ -10600,7 +11062,7 @@ function runValidationPipeline(content, format, options = {}) {
10600
11062
  // src/core/write/mutation-helpers.ts
10601
11063
  init_writer();
10602
11064
  import fs19 from "fs/promises";
10603
- import path18 from "path";
11065
+ import path19 from "path";
10604
11066
  init_constants();
10605
11067
  init_writer();
10606
11068
  function formatMcpResult(result) {
@@ -10649,7 +11111,7 @@ async function handleGitCommit(vaultPath2, notePath, commit, prefix) {
10649
11111
  return info;
10650
11112
  }
10651
11113
  async function ensureFileExists(vaultPath2, notePath) {
10652
- const fullPath = path18.join(vaultPath2, notePath);
11114
+ const fullPath = path19.join(vaultPath2, notePath);
10653
11115
  try {
10654
11116
  await fs19.access(fullPath);
10655
11117
  return null;
@@ -10754,10 +11216,10 @@ async function withVaultFrontmatter(options, operation) {
10754
11216
 
10755
11217
  // src/tools/write/mutations.ts
10756
11218
  async function createNoteFromTemplate(vaultPath2, notePath, config) {
10757
- const fullPath = path19.join(vaultPath2, notePath);
10758
- await fs20.mkdir(path19.dirname(fullPath), { recursive: true });
11219
+ const fullPath = path20.join(vaultPath2, notePath);
11220
+ await fs20.mkdir(path20.dirname(fullPath), { recursive: true });
10759
11221
  const templates = config.templates || {};
10760
- const filename = path19.basename(notePath, ".md").toLowerCase();
11222
+ const filename = path20.basename(notePath, ".md").toLowerCase();
10761
11223
  let templatePath;
10762
11224
  const dailyPattern = /^\d{4}-\d{2}-\d{2}/;
10763
11225
  const weeklyPattern = /^\d{4}-W\d{2}/;
@@ -10778,10 +11240,10 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
10778
11240
  let templateContent;
10779
11241
  if (templatePath) {
10780
11242
  try {
10781
- const absTemplatePath = path19.join(vaultPath2, templatePath);
11243
+ const absTemplatePath = path20.join(vaultPath2, templatePath);
10782
11244
  templateContent = await fs20.readFile(absTemplatePath, "utf-8");
10783
11245
  } catch {
10784
- const title = path19.basename(notePath, ".md");
11246
+ const title = path20.basename(notePath, ".md");
10785
11247
  templateContent = `---
10786
11248
  ---
10787
11249
 
@@ -10790,7 +11252,7 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
10790
11252
  templatePath = void 0;
10791
11253
  }
10792
11254
  } else {
10793
- const title = path19.basename(notePath, ".md");
11255
+ const title = path20.basename(notePath, ".md");
10794
11256
  templateContent = `---
10795
11257
  ---
10796
11258
 
@@ -10799,7 +11261,13 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
10799
11261
  }
10800
11262
  const now = /* @__PURE__ */ new Date();
10801
11263
  const dateStr = now.toISOString().split("T")[0];
10802
- 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"));
11265
+ const matter9 = (await import("gray-matter")).default;
11266
+ const parsed = matter9(templateContent);
11267
+ if (!parsed.data.date) {
11268
+ parsed.data.date = dateStr;
11269
+ }
11270
+ templateContent = matter9.stringify(parsed.content, parsed.data);
10803
11271
  await fs20.writeFile(fullPath, templateContent, "utf-8");
10804
11272
  return { created: true, templateUsed: templatePath };
10805
11273
  }
@@ -10832,7 +11300,7 @@ Example: vault_add_to_section({ path: "daily/2026-02-15.md", section: "Log", con
10832
11300
  let noteCreated = false;
10833
11301
  let templateUsed;
10834
11302
  if (create_if_missing) {
10835
- const fullPath = path19.join(vaultPath2, notePath);
11303
+ const fullPath = path20.join(vaultPath2, notePath);
10836
11304
  try {
10837
11305
  await fs20.access(fullPath);
10838
11306
  } catch {
@@ -11129,6 +11597,8 @@ function registerTaskTools(server2, vaultPath2) {
11129
11597
  finalFrontmatter = injectMutationMetadata(frontmatter, { agent_id, session_id });
11130
11598
  }
11131
11599
  await writeVaultFile(vaultPath2, notePath, toggleResult.content, finalFrontmatter);
11600
+ await updateTaskCacheForFile(vaultPath2, notePath).catch(() => {
11601
+ });
11132
11602
  const gitInfo = await handleGitCommit(vaultPath2, notePath, commit, "[Flywheel:Task]");
11133
11603
  const newStatus = toggleResult.newState ? "completed" : "incomplete";
11134
11604
  const checkbox = toggleResult.newState ? "[x]" : "[ ]";
@@ -11283,7 +11753,7 @@ Example: vault_update_frontmatter({ path: "projects/alpha.md", frontmatter: { st
11283
11753
  init_writer();
11284
11754
  import { z as z14 } from "zod";
11285
11755
  import fs21 from "fs/promises";
11286
- import path20 from "path";
11756
+ import path21 from "path";
11287
11757
  function registerNoteTools(server2, vaultPath2, getIndex) {
11288
11758
  server2.tool(
11289
11759
  "vault_create_note",
@@ -11306,23 +11776,23 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
11306
11776
  if (!validatePath(vaultPath2, notePath)) {
11307
11777
  return formatMcpResult(errorResult(notePath, "Invalid path: path traversal not allowed"));
11308
11778
  }
11309
- const fullPath = path20.join(vaultPath2, notePath);
11779
+ const fullPath = path21.join(vaultPath2, notePath);
11310
11780
  const existsCheck = await ensureFileExists(vaultPath2, notePath);
11311
11781
  if (existsCheck === null && !overwrite) {
11312
11782
  return formatMcpResult(errorResult(notePath, `File already exists: ${notePath}. Use overwrite=true to replace.`));
11313
11783
  }
11314
- const dir = path20.dirname(fullPath);
11784
+ const dir = path21.dirname(fullPath);
11315
11785
  await fs21.mkdir(dir, { recursive: true });
11316
11786
  let effectiveContent = content;
11317
11787
  let effectiveFrontmatter = frontmatter;
11318
11788
  if (template) {
11319
- const templatePath = path20.join(vaultPath2, template);
11789
+ const templatePath = path21.join(vaultPath2, template);
11320
11790
  try {
11321
11791
  const raw = await fs21.readFile(templatePath, "utf-8");
11322
11792
  const matter9 = (await import("gray-matter")).default;
11323
11793
  const parsed = matter9(raw);
11324
11794
  const dateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
11325
- const title = path20.basename(notePath, ".md");
11795
+ const title = path21.basename(notePath, ".md");
11326
11796
  let templateContent = parsed.content.replace(/\{\{date\}\}/g, dateStr).replace(/\{\{title\}\}/g, title);
11327
11797
  if (content) {
11328
11798
  templateContent = templateContent.trimEnd() + "\n\n" + content;
@@ -11333,8 +11803,11 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
11333
11803
  return formatMcpResult(errorResult(notePath, `Template not found: ${template}`));
11334
11804
  }
11335
11805
  }
11806
+ if (!effectiveFrontmatter.date) {
11807
+ effectiveFrontmatter.date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
11808
+ }
11336
11809
  const warnings = [];
11337
- const noteName = path20.basename(notePath, ".md");
11810
+ const noteName = path21.basename(notePath, ".md");
11338
11811
  const existingAliases = Array.isArray(effectiveFrontmatter?.aliases) ? effectiveFrontmatter.aliases.filter((a) => typeof a === "string") : [];
11339
11812
  const preflight = await checkPreflightSimilarity(noteName);
11340
11813
  if (preflight.existingEntity) {
@@ -11451,7 +11924,7 @@ ${sources}`;
11451
11924
  }
11452
11925
  return formatMcpResult(errorResult(notePath, previewLines.join("\n")));
11453
11926
  }
11454
- const fullPath = path20.join(vaultPath2, notePath);
11927
+ const fullPath = path21.join(vaultPath2, notePath);
11455
11928
  await fs21.unlink(fullPath);
11456
11929
  const gitInfo = await handleGitCommit(vaultPath2, notePath, commit, "[Flywheel:Delete]");
11457
11930
  const message = backlinkWarning ? `Deleted note: ${notePath}
@@ -11471,7 +11944,7 @@ Warning: ${backlinkWarning}` : `Deleted note: ${notePath}`;
11471
11944
  init_writer();
11472
11945
  import { z as z15 } from "zod";
11473
11946
  import fs22 from "fs/promises";
11474
- import path21 from "path";
11947
+ import path22 from "path";
11475
11948
  import matter6 from "gray-matter";
11476
11949
  function escapeRegex(str) {
11477
11950
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -11490,7 +11963,7 @@ function extractWikilinks2(content) {
11490
11963
  return wikilinks;
11491
11964
  }
11492
11965
  function getTitleFromPath(filePath) {
11493
- return path21.basename(filePath, ".md");
11966
+ return path22.basename(filePath, ".md");
11494
11967
  }
11495
11968
  async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
11496
11969
  const results = [];
@@ -11499,7 +11972,7 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
11499
11972
  const files = [];
11500
11973
  const entries = await fs22.readdir(dir, { withFileTypes: true });
11501
11974
  for (const entry of entries) {
11502
- const fullPath = path21.join(dir, entry.name);
11975
+ const fullPath = path22.join(dir, entry.name);
11503
11976
  if (entry.isDirectory() && !entry.name.startsWith(".")) {
11504
11977
  files.push(...await scanDir(fullPath));
11505
11978
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
@@ -11510,7 +11983,7 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
11510
11983
  }
11511
11984
  const allFiles = await scanDir(vaultPath2);
11512
11985
  for (const filePath of allFiles) {
11513
- const relativePath = path21.relative(vaultPath2, filePath);
11986
+ const relativePath = path22.relative(vaultPath2, filePath);
11514
11987
  const content = await fs22.readFile(filePath, "utf-8");
11515
11988
  const wikilinks = extractWikilinks2(content);
11516
11989
  const matchingLinks = [];
@@ -11530,7 +12003,7 @@ async function findBacklinks(vaultPath2, targetTitle, targetAliases) {
11530
12003
  return results;
11531
12004
  }
11532
12005
  async function updateBacklinksInFile(vaultPath2, filePath, oldTitles, newTitle) {
11533
- const fullPath = path21.join(vaultPath2, filePath);
12006
+ const fullPath = path22.join(vaultPath2, filePath);
11534
12007
  const raw = await fs22.readFile(fullPath, "utf-8");
11535
12008
  const parsed = matter6(raw);
11536
12009
  let content = parsed.content;
@@ -11597,8 +12070,8 @@ function registerMoveNoteTools(server2, vaultPath2) {
11597
12070
  };
11598
12071
  return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
11599
12072
  }
11600
- const oldFullPath = path21.join(vaultPath2, oldPath);
11601
- const newFullPath = path21.join(vaultPath2, newPath);
12073
+ const oldFullPath = path22.join(vaultPath2, oldPath);
12074
+ const newFullPath = path22.join(vaultPath2, newPath);
11602
12075
  try {
11603
12076
  await fs22.access(oldFullPath);
11604
12077
  } catch {
@@ -11648,7 +12121,7 @@ function registerMoveNoteTools(server2, vaultPath2) {
11648
12121
  }
11649
12122
  }
11650
12123
  }
11651
- const destDir = path21.dirname(newFullPath);
12124
+ const destDir = path22.dirname(newFullPath);
11652
12125
  await fs22.mkdir(destDir, { recursive: true });
11653
12126
  await fs22.rename(oldFullPath, newFullPath);
11654
12127
  let gitCommit;
@@ -11734,10 +12207,10 @@ function registerMoveNoteTools(server2, vaultPath2) {
11734
12207
  if (sanitizedTitle !== newTitle) {
11735
12208
  console.error(`[Flywheel] Title sanitized: "${newTitle}" \u2192 "${sanitizedTitle}"`);
11736
12209
  }
11737
- const fullPath = path21.join(vaultPath2, notePath);
11738
- const dir = path21.dirname(notePath);
11739
- const newPath = dir === "." ? `${sanitizedTitle}.md` : path21.join(dir, `${sanitizedTitle}.md`);
11740
- 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);
11741
12214
  try {
11742
12215
  await fs22.access(fullPath);
11743
12216
  } catch {
@@ -11845,15 +12318,146 @@ function registerMoveNoteTools(server2, vaultPath2) {
11845
12318
  );
11846
12319
  }
11847
12320
 
11848
- // src/tools/write/system.ts
12321
+ // src/tools/write/merge.ts
12322
+ init_writer();
11849
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";
11850
12454
  function registerSystemTools2(server2, vaultPath2) {
11851
12455
  server2.tool(
11852
12456
  "vault_undo_last_mutation",
11853
12457
  "Undo the last git commit (typically the last Flywheel mutation). Performs a soft reset.",
11854
12458
  {
11855
- confirm: z16.boolean().default(false).describe("Must be true to confirm undo operation"),
11856
- 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.")
11857
12461
  },
11858
12462
  async ({ confirm, hash }) => {
11859
12463
  try {
@@ -11954,7 +12558,7 @@ Message: ${undoResult.undoneCommit.message}` : void 0
11954
12558
  }
11955
12559
 
11956
12560
  // src/tools/write/policy.ts
11957
- import { z as z18 } from "zod";
12561
+ import { z as z19 } from "zod";
11958
12562
 
11959
12563
  // src/core/write/policy/index.ts
11960
12564
  init_template();
@@ -11962,8 +12566,8 @@ init_schema();
11962
12566
 
11963
12567
  // src/core/write/policy/parser.ts
11964
12568
  init_schema();
11965
- import fs23 from "fs/promises";
11966
- import path22 from "path";
12569
+ import fs24 from "fs/promises";
12570
+ import path23 from "path";
11967
12571
  import matter7 from "gray-matter";
11968
12572
  function parseYaml(content) {
11969
12573
  const parsed = matter7(`---
@@ -11988,7 +12592,7 @@ function parsePolicyString(yamlContent) {
11988
12592
  }
11989
12593
  async function loadPolicyFile(filePath) {
11990
12594
  try {
11991
- const content = await fs23.readFile(filePath, "utf-8");
12595
+ const content = await fs24.readFile(filePath, "utf-8");
11992
12596
  return parsePolicyString(content);
11993
12597
  } catch (error) {
11994
12598
  if (error.code === "ENOENT") {
@@ -12012,15 +12616,15 @@ async function loadPolicyFile(filePath) {
12012
12616
  }
12013
12617
  }
12014
12618
  async function loadPolicy(vaultPath2, policyName) {
12015
- const policiesDir = path22.join(vaultPath2, ".claude", "policies");
12016
- const policyPath = path22.join(policiesDir, `${policyName}.yaml`);
12619
+ const policiesDir = path23.join(vaultPath2, ".claude", "policies");
12620
+ const policyPath = path23.join(policiesDir, `${policyName}.yaml`);
12017
12621
  try {
12018
- await fs23.access(policyPath);
12622
+ await fs24.access(policyPath);
12019
12623
  return loadPolicyFile(policyPath);
12020
12624
  } catch {
12021
- const ymlPath = path22.join(policiesDir, `${policyName}.yml`);
12625
+ const ymlPath = path23.join(policiesDir, `${policyName}.yml`);
12022
12626
  try {
12023
- await fs23.access(ymlPath);
12627
+ await fs24.access(ymlPath);
12024
12628
  return loadPolicyFile(ymlPath);
12025
12629
  } catch {
12026
12630
  return {
@@ -12158,8 +12762,8 @@ init_template();
12158
12762
  init_conditions();
12159
12763
  init_schema();
12160
12764
  init_writer();
12161
- import fs25 from "fs/promises";
12162
- import path24 from "path";
12765
+ import fs26 from "fs/promises";
12766
+ import path25 from "path";
12163
12767
  init_constants();
12164
12768
  async function executeStep(step, vaultPath2, context, conditionResults) {
12165
12769
  const { execute, reason } = shouldStepExecute(step.when, conditionResults);
@@ -12228,9 +12832,9 @@ async function executeAddToSection(params, vaultPath2, context) {
12228
12832
  const preserveListNesting = params.preserveListNesting !== false;
12229
12833
  const suggestOutgoingLinks = params.suggestOutgoingLinks !== false;
12230
12834
  const maxSuggestions = Number(params.maxSuggestions) || 3;
12231
- const fullPath = path24.join(vaultPath2, notePath);
12835
+ const fullPath = path25.join(vaultPath2, notePath);
12232
12836
  try {
12233
- await fs25.access(fullPath);
12837
+ await fs26.access(fullPath);
12234
12838
  } catch {
12235
12839
  return { success: false, message: `File not found: ${notePath}`, path: notePath };
12236
12840
  }
@@ -12268,9 +12872,9 @@ async function executeRemoveFromSection(params, vaultPath2) {
12268
12872
  const pattern = String(params.pattern || "");
12269
12873
  const mode = params.mode || "first";
12270
12874
  const useRegex = Boolean(params.useRegex);
12271
- const fullPath = path24.join(vaultPath2, notePath);
12875
+ const fullPath = path25.join(vaultPath2, notePath);
12272
12876
  try {
12273
- await fs25.access(fullPath);
12877
+ await fs26.access(fullPath);
12274
12878
  } catch {
12275
12879
  return { success: false, message: `File not found: ${notePath}`, path: notePath };
12276
12880
  }
@@ -12299,9 +12903,9 @@ async function executeReplaceInSection(params, vaultPath2, context) {
12299
12903
  const mode = params.mode || "first";
12300
12904
  const useRegex = Boolean(params.useRegex);
12301
12905
  const skipWikilinks = Boolean(params.skipWikilinks);
12302
- const fullPath = path24.join(vaultPath2, notePath);
12906
+ const fullPath = path25.join(vaultPath2, notePath);
12303
12907
  try {
12304
- await fs25.access(fullPath);
12908
+ await fs26.access(fullPath);
12305
12909
  } catch {
12306
12910
  return { success: false, message: `File not found: ${notePath}`, path: notePath };
12307
12911
  }
@@ -12342,16 +12946,16 @@ async function executeCreateNote(params, vaultPath2, context) {
12342
12946
  if (!validatePath(vaultPath2, notePath)) {
12343
12947
  return { success: false, message: "Invalid path: path traversal not allowed", path: notePath };
12344
12948
  }
12345
- const fullPath = path24.join(vaultPath2, notePath);
12949
+ const fullPath = path25.join(vaultPath2, notePath);
12346
12950
  try {
12347
- await fs25.access(fullPath);
12951
+ await fs26.access(fullPath);
12348
12952
  if (!overwrite) {
12349
12953
  return { success: false, message: `File already exists: ${notePath}`, path: notePath };
12350
12954
  }
12351
12955
  } catch {
12352
12956
  }
12353
- const dir = path24.dirname(fullPath);
12354
- await fs25.mkdir(dir, { recursive: true });
12957
+ const dir = path25.dirname(fullPath);
12958
+ await fs26.mkdir(dir, { recursive: true });
12355
12959
  const { content: processedContent } = maybeApplyWikilinks(content, skipWikilinks, notePath);
12356
12960
  await writeVaultFile(vaultPath2, notePath, processedContent, frontmatter);
12357
12961
  return {
@@ -12370,13 +12974,13 @@ async function executeDeleteNote(params, vaultPath2) {
12370
12974
  if (!validatePath(vaultPath2, notePath)) {
12371
12975
  return { success: false, message: "Invalid path: path traversal not allowed", path: notePath };
12372
12976
  }
12373
- const fullPath = path24.join(vaultPath2, notePath);
12977
+ const fullPath = path25.join(vaultPath2, notePath);
12374
12978
  try {
12375
- await fs25.access(fullPath);
12979
+ await fs26.access(fullPath);
12376
12980
  } catch {
12377
12981
  return { success: false, message: `File not found: ${notePath}`, path: notePath };
12378
12982
  }
12379
- await fs25.unlink(fullPath);
12983
+ await fs26.unlink(fullPath);
12380
12984
  return {
12381
12985
  success: true,
12382
12986
  message: `Deleted note: ${notePath}`,
@@ -12387,9 +12991,9 @@ async function executeToggleTask(params, vaultPath2) {
12387
12991
  const notePath = String(params.path || "");
12388
12992
  const task = String(params.task || "");
12389
12993
  const section = params.section ? String(params.section) : void 0;
12390
- const fullPath = path24.join(vaultPath2, notePath);
12994
+ const fullPath = path25.join(vaultPath2, notePath);
12391
12995
  try {
12392
- await fs25.access(fullPath);
12996
+ await fs26.access(fullPath);
12393
12997
  } catch {
12394
12998
  return { success: false, message: `File not found: ${notePath}`, path: notePath };
12395
12999
  }
@@ -12430,9 +13034,9 @@ async function executeAddTask(params, vaultPath2, context) {
12430
13034
  const completed = Boolean(params.completed);
12431
13035
  const skipWikilinks = Boolean(params.skipWikilinks);
12432
13036
  const preserveListNesting = params.preserveListNesting !== false;
12433
- const fullPath = path24.join(vaultPath2, notePath);
13037
+ const fullPath = path25.join(vaultPath2, notePath);
12434
13038
  try {
12435
- await fs25.access(fullPath);
13039
+ await fs26.access(fullPath);
12436
13040
  } catch {
12437
13041
  return { success: false, message: `File not found: ${notePath}`, path: notePath };
12438
13042
  }
@@ -12467,9 +13071,9 @@ async function executeAddTask(params, vaultPath2, context) {
12467
13071
  async function executeUpdateFrontmatter(params, vaultPath2) {
12468
13072
  const notePath = String(params.path || "");
12469
13073
  const updates = params.frontmatter || {};
12470
- const fullPath = path24.join(vaultPath2, notePath);
13074
+ const fullPath = path25.join(vaultPath2, notePath);
12471
13075
  try {
12472
- await fs25.access(fullPath);
13076
+ await fs26.access(fullPath);
12473
13077
  } catch {
12474
13078
  return { success: false, message: `File not found: ${notePath}`, path: notePath };
12475
13079
  }
@@ -12489,9 +13093,9 @@ async function executeAddFrontmatterField(params, vaultPath2) {
12489
13093
  const notePath = String(params.path || "");
12490
13094
  const key = String(params.key || "");
12491
13095
  const value = params.value;
12492
- const fullPath = path24.join(vaultPath2, notePath);
13096
+ const fullPath = path25.join(vaultPath2, notePath);
12493
13097
  try {
12494
- await fs25.access(fullPath);
13098
+ await fs26.access(fullPath);
12495
13099
  } catch {
12496
13100
  return { success: false, message: `File not found: ${notePath}`, path: notePath };
12497
13101
  }
@@ -12649,15 +13253,15 @@ async function executePolicy(policy, vaultPath2, variables, commit = false) {
12649
13253
  async function rollbackChanges(vaultPath2, originalContents, filesModified) {
12650
13254
  for (const filePath of filesModified) {
12651
13255
  const original = originalContents.get(filePath);
12652
- const fullPath = path24.join(vaultPath2, filePath);
13256
+ const fullPath = path25.join(vaultPath2, filePath);
12653
13257
  if (original === null) {
12654
13258
  try {
12655
- await fs25.unlink(fullPath);
13259
+ await fs26.unlink(fullPath);
12656
13260
  } catch {
12657
13261
  }
12658
13262
  } else if (original !== void 0) {
12659
13263
  try {
12660
- await fs25.writeFile(fullPath, original);
13264
+ await fs26.writeFile(fullPath, original);
12661
13265
  } catch {
12662
13266
  }
12663
13267
  }
@@ -12703,27 +13307,27 @@ async function previewPolicy(policy, vaultPath2, variables) {
12703
13307
  }
12704
13308
 
12705
13309
  // src/core/write/policy/storage.ts
12706
- import fs26 from "fs/promises";
12707
- import path25 from "path";
13310
+ import fs27 from "fs/promises";
13311
+ import path26 from "path";
12708
13312
  function getPoliciesDir(vaultPath2) {
12709
- return path25.join(vaultPath2, ".claude", "policies");
13313
+ return path26.join(vaultPath2, ".claude", "policies");
12710
13314
  }
12711
13315
  async function ensurePoliciesDir(vaultPath2) {
12712
13316
  const dir = getPoliciesDir(vaultPath2);
12713
- await fs26.mkdir(dir, { recursive: true });
13317
+ await fs27.mkdir(dir, { recursive: true });
12714
13318
  }
12715
13319
  async function listPolicies(vaultPath2) {
12716
13320
  const dir = getPoliciesDir(vaultPath2);
12717
13321
  const policies = [];
12718
13322
  try {
12719
- const files = await fs26.readdir(dir);
13323
+ const files = await fs27.readdir(dir);
12720
13324
  for (const file of files) {
12721
13325
  if (!file.endsWith(".yaml") && !file.endsWith(".yml")) {
12722
13326
  continue;
12723
13327
  }
12724
- const filePath = path25.join(dir, file);
12725
- const stat3 = await fs26.stat(filePath);
12726
- 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");
12727
13331
  const metadata = extractPolicyMetadata(content);
12728
13332
  policies.push({
12729
13333
  name: metadata.name || file.replace(/\.ya?ml$/, ""),
@@ -12746,10 +13350,10 @@ async function writePolicyRaw(vaultPath2, policyName, content, overwrite = false
12746
13350
  const dir = getPoliciesDir(vaultPath2);
12747
13351
  await ensurePoliciesDir(vaultPath2);
12748
13352
  const filename = `${policyName}.yaml`;
12749
- const filePath = path25.join(dir, filename);
13353
+ const filePath = path26.join(dir, filename);
12750
13354
  if (!overwrite) {
12751
13355
  try {
12752
- await fs26.access(filePath);
13356
+ await fs27.access(filePath);
12753
13357
  return {
12754
13358
  success: false,
12755
13359
  path: filename,
@@ -12766,7 +13370,7 @@ async function writePolicyRaw(vaultPath2, policyName, content, overwrite = false
12766
13370
  message: `Invalid policy: ${validation.errors.map((e) => e.message).join("; ")}`
12767
13371
  };
12768
13372
  }
12769
- await fs26.writeFile(filePath, content, "utf-8");
13373
+ await fs27.writeFile(filePath, content, "utf-8");
12770
13374
  return {
12771
13375
  success: true,
12772
13376
  path: filename,
@@ -12781,71 +13385,71 @@ function registerPolicyTools(server2, vaultPath2) {
12781
13385
  "policy",
12782
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).',
12783
13387
  {
12784
- 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"),
12785
13389
  // validate
12786
- yaml: z18.string().optional().describe('Policy YAML content (required for "validate")'),
13390
+ yaml: z19.string().optional().describe('Policy YAML content (required for "validate")'),
12787
13391
  // preview, execute, revise
12788
- 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")'),
12789
13393
  // preview, execute
12790
- 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")'),
12791
13395
  // execute
12792
- 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")'),
12793
13397
  // author
12794
- name: z18.string().optional().describe('Name for the policy (required for "author")'),
12795
- description: z18.string().optional().describe('Description of what the policy should do (required for "author")'),
12796
- steps: z18.array(z18.object({
12797
- tool: z18.string().describe("Tool to call (e.g., vault_add_to_section)"),
12798
- description: z18.string().describe("What this step does"),
12799
- 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")
12800
13404
  })).optional().describe('Steps the policy should perform (required for "author")'),
12801
- authorVariables: z18.array(z18.object({
12802
- name: z18.string().describe("Variable name"),
12803
- type: z18.enum(["string", "number", "boolean", "array", "enum"]).describe("Variable type"),
12804
- required: z18.boolean().default(true).describe("Whether variable is required"),
12805
- default: z18.unknown().optional().describe("Default value"),
12806
- enum: z18.array(z18.string()).optional().describe("Allowed values for enum type"),
12807
- 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")
12808
13412
  })).optional().describe('Variables the policy accepts (for "author")'),
12809
- conditions: z18.array(z18.object({
12810
- id: z18.string().describe("Condition ID"),
12811
- check: z18.string().describe("Condition type (file_exists, section_exists, etc.)"),
12812
- path: z18.string().optional().describe("File path"),
12813
- section: z18.string().optional().describe("Section name"),
12814
- field: z18.string().optional().describe("Frontmatter field"),
12815
- 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")
12816
13420
  })).optional().describe('Conditions for conditional execution (for "author")'),
12817
13421
  // author, revise
12818
- 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")'),
12819
13423
  // revise
12820
- changes: z18.object({
12821
- description: z18.string().optional().describe("New description"),
12822
- addVariables: z18.array(z18.object({
12823
- name: z18.string(),
12824
- type: z18.enum(["string", "number", "boolean", "array", "enum"]),
12825
- required: z18.boolean().default(true),
12826
- default: z18.unknown().optional(),
12827
- enum: z18.array(z18.string()).optional(),
12828
- 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()
12829
13433
  })).optional().describe("Variables to add"),
12830
- removeVariables: z18.array(z18.string()).optional().describe("Variable names to remove"),
12831
- addSteps: z18.array(z18.object({
12832
- id: z18.string(),
12833
- tool: z18.string(),
12834
- params: z18.record(z18.unknown()),
12835
- when: z18.string().optional(),
12836
- description: z18.string().optional(),
12837
- 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")
12838
13442
  })).optional().describe("Steps to add"),
12839
- removeSteps: z18.array(z18.string()).optional().describe("Step IDs to remove"),
12840
- addConditions: z18.array(z18.object({
12841
- id: z18.string(),
12842
- check: z18.string(),
12843
- path: z18.string().optional(),
12844
- section: z18.string().optional(),
12845
- field: z18.string().optional(),
12846
- 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()
12847
13451
  })).optional().describe("Conditions to add"),
12848
- removeConditions: z18.array(z18.string()).optional().describe("Condition IDs to remove")
13452
+ removeConditions: z19.array(z19.string()).optional().describe("Condition IDs to remove")
12849
13453
  }).optional().describe('Changes to make (required for "revise")')
12850
13454
  },
12851
13455
  async (params) => {
@@ -13286,11 +13890,11 @@ function registerPolicyTools(server2, vaultPath2) {
13286
13890
  }
13287
13891
 
13288
13892
  // src/tools/write/tags.ts
13289
- import { z as z19 } from "zod";
13893
+ import { z as z20 } from "zod";
13290
13894
 
13291
13895
  // src/core/write/tagRename.ts
13292
- import * as fs27 from "fs/promises";
13293
- import * as path26 from "path";
13896
+ import * as fs28 from "fs/promises";
13897
+ import * as path27 from "path";
13294
13898
  import matter8 from "gray-matter";
13295
13899
  import { getProtectedZones } from "@velvetmonkey/vault-core";
13296
13900
  function getNotesInFolder3(index, folder) {
@@ -13396,10 +14000,10 @@ async function renameTag(index, vaultPath2, oldTag, newTag, options) {
13396
14000
  const previews = [];
13397
14001
  let totalChanges = 0;
13398
14002
  for (const note of affectedNotes) {
13399
- const fullPath = path26.join(vaultPath2, note.path);
14003
+ const fullPath = path27.join(vaultPath2, note.path);
13400
14004
  let fileContent;
13401
14005
  try {
13402
- fileContent = await fs27.readFile(fullPath, "utf-8");
14006
+ fileContent = await fs28.readFile(fullPath, "utf-8");
13403
14007
  } catch {
13404
14008
  continue;
13405
14009
  }
@@ -13472,7 +14076,7 @@ async function renameTag(index, vaultPath2, oldTag, newTag, options) {
13472
14076
  previews.push(preview);
13473
14077
  if (!dryRun) {
13474
14078
  const newContent = matter8.stringify(updatedContent, fm);
13475
- await fs27.writeFile(fullPath, newContent, "utf-8");
14079
+ await fs28.writeFile(fullPath, newContent, "utf-8");
13476
14080
  }
13477
14081
  }
13478
14082
  }
@@ -13495,12 +14099,12 @@ function registerTagTools(server2, getIndex, getVaultPath) {
13495
14099
  title: "Rename Tag",
13496
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.",
13497
14101
  inputSchema: {
13498
- old_tag: z19.string().describe('Tag to rename (without #, e.g., "project")'),
13499
- new_tag: z19.string().describe('New tag name (without #, e.g., "work")'),
13500
- rename_children: z19.boolean().optional().describe("Also rename child tags (e.g., #project/active \u2192 #work/active). Default: true"),
13501
- folder: z19.string().optional().describe('Limit to notes in this folder (e.g., "projects")'),
13502
- dry_run: z19.boolean().optional().describe("Preview only, no changes (default: true)"),
13503
- 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)")
13504
14108
  }
13505
14109
  },
13506
14110
  async ({ old_tag, new_tag, rename_children, folder, dry_run, commit }) => {
@@ -13525,20 +14129,20 @@ function registerTagTools(server2, getIndex, getVaultPath) {
13525
14129
  }
13526
14130
 
13527
14131
  // src/tools/write/wikilinkFeedback.ts
13528
- import { z as z20 } from "zod";
14132
+ import { z as z21 } from "zod";
13529
14133
  function registerWikilinkFeedbackTools(server2, getStateDb) {
13530
14134
  server2.registerTool(
13531
14135
  "wikilink_feedback",
13532
14136
  {
13533
14137
  title: "Wikilink Feedback",
13534
- 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.',
13535
14139
  inputSchema: {
13536
- mode: z20.enum(["report", "list", "stats"]).describe("Operation mode"),
13537
- entity: z20.string().optional().describe("Entity name (required for report mode, optional filter for list/stats)"),
13538
- note_path: z20.string().optional().describe("Note path where the wikilink appeared (for report mode)"),
13539
- context: z20.string().optional().describe("Surrounding text context (for report mode)"),
13540
- correct: z20.boolean().optional().describe("Whether the wikilink was correct (for report mode)"),
13541
- 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)")
13542
14146
  }
13543
14147
  },
13544
14148
  async ({ mode, entity, note_path, context, correct, limit }) => {
@@ -13588,6 +14192,16 @@ function registerWikilinkFeedbackTools(server2, getStateDb) {
13588
14192
  };
13589
14193
  break;
13590
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
+ }
13591
14205
  }
13592
14206
  return {
13593
14207
  content: [
@@ -13601,8 +14215,57 @@ function registerWikilinkFeedbackTools(server2, getStateDb) {
13601
14215
  );
13602
14216
  }
13603
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
+
13604
14267
  // src/tools/read/metrics.ts
13605
- import { z as z21 } from "zod";
14268
+ import { z as z23 } from "zod";
13606
14269
 
13607
14270
  // src/core/shared/metrics.ts
13608
14271
  var ALL_METRICS = [
@@ -13768,10 +14431,10 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
13768
14431
  title: "Vault Growth",
13769
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.',
13770
14433
  inputSchema: {
13771
- mode: z21.enum(["current", "history", "trends", "index_activity"]).describe("Query mode: current snapshot, historical time series, trend analysis, or index rebuild activity"),
13772
- metric: z21.string().optional().describe('Filter to specific metric (e.g., "note_count"). Omit for all metrics.'),
13773
- days_back: z21.number().optional().describe("Number of days to look back for history/trends (default: 30)"),
13774
- 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)")
13775
14438
  }
13776
14439
  },
13777
14440
  async ({ mode, metric, days_back, limit: eventLimit }) => {
@@ -13844,7 +14507,7 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
13844
14507
  }
13845
14508
 
13846
14509
  // src/tools/read/activity.ts
13847
- import { z as z22 } from "zod";
14510
+ import { z as z24 } from "zod";
13848
14511
 
13849
14512
  // src/core/shared/toolTracking.ts
13850
14513
  function recordToolInvocation(stateDb2, event) {
@@ -13924,8 +14587,8 @@ function getNoteAccessFrequency(stateDb2, daysBack = 30) {
13924
14587
  }
13925
14588
  }
13926
14589
  }
13927
- return Array.from(noteMap.entries()).map(([path29, stats]) => ({
13928
- path: path29,
14590
+ return Array.from(noteMap.entries()).map(([path30, stats]) => ({
14591
+ path: path30,
13929
14592
  access_count: stats.access_count,
13930
14593
  last_accessed: stats.last_accessed,
13931
14594
  tools_used: Array.from(stats.tools)
@@ -14004,10 +14667,10 @@ function registerActivityTools(server2, getStateDb, getSessionId2) {
14004
14667
  title: "Vault Activity",
14005
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)',
14006
14669
  inputSchema: {
14007
- mode: z22.enum(["session", "sessions", "note_access", "tool_usage"]).describe("Activity query mode"),
14008
- session_id: z22.string().optional().describe("Specific session ID (for session mode, defaults to current)"),
14009
- days_back: z22.number().optional().describe("Number of days to look back (default: 30)"),
14010
- 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)")
14011
14674
  }
14012
14675
  },
14013
14676
  async ({ mode, session_id, days_back, limit: resultLimit }) => {
@@ -14074,11 +14737,11 @@ function registerActivityTools(server2, getStateDb, getSessionId2) {
14074
14737
  }
14075
14738
 
14076
14739
  // src/tools/read/similarity.ts
14077
- import { z as z23 } from "zod";
14740
+ import { z as z25 } from "zod";
14078
14741
 
14079
14742
  // src/core/read/similarity.ts
14080
- import * as fs28 from "fs";
14081
- import * as path27 from "path";
14743
+ import * as fs29 from "fs";
14744
+ import * as path28 from "path";
14082
14745
  var STOP_WORDS = /* @__PURE__ */ new Set([
14083
14746
  "the",
14084
14747
  "be",
@@ -14213,12 +14876,12 @@ function extractKeyTerms(content, maxTerms = 15) {
14213
14876
  }
14214
14877
  return Array.from(freq.entries()).sort((a, b) => b[1] - a[1]).slice(0, maxTerms).map(([word]) => word);
14215
14878
  }
14216
- function findSimilarNotes(db3, vaultPath2, index, sourcePath, options = {}) {
14879
+ function findSimilarNotes(db4, vaultPath2, index, sourcePath, options = {}) {
14217
14880
  const limit = options.limit ?? 10;
14218
- const absPath = path27.join(vaultPath2, sourcePath);
14881
+ const absPath = path28.join(vaultPath2, sourcePath);
14219
14882
  let content;
14220
14883
  try {
14221
- content = fs28.readFileSync(absPath, "utf-8");
14884
+ content = fs29.readFileSync(absPath, "utf-8");
14222
14885
  } catch {
14223
14886
  return [];
14224
14887
  }
@@ -14226,7 +14889,7 @@ function findSimilarNotes(db3, vaultPath2, index, sourcePath, options = {}) {
14226
14889
  if (terms.length === 0) return [];
14227
14890
  const query = terms.join(" OR ");
14228
14891
  try {
14229
- const results = db3.prepare(`
14892
+ const results = db4.prepare(`
14230
14893
  SELECT
14231
14894
  path,
14232
14895
  title,
@@ -14294,9 +14957,9 @@ async function findSemanticSimilarNotes(vaultPath2, index, sourcePath, options =
14294
14957
  // Semantic results don't have snippets
14295
14958
  }));
14296
14959
  }
14297
- async function findHybridSimilarNotes(db3, vaultPath2, index, sourcePath, options = {}) {
14960
+ async function findHybridSimilarNotes(db4, vaultPath2, index, sourcePath, options = {}) {
14298
14961
  const limit = options.limit ?? 10;
14299
- const bm25Results = findSimilarNotes(db3, vaultPath2, index, sourcePath, {
14962
+ const bm25Results = findSimilarNotes(db4, vaultPath2, index, sourcePath, {
14300
14963
  limit: limit * 2,
14301
14964
  excludeLinked: options.excludeLinked
14302
14965
  });
@@ -14338,12 +15001,12 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
14338
15001
  title: "Find Similar Notes",
14339
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.",
14340
15003
  inputSchema: {
14341
- path: z23.string().describe('Path to the source note (relative to vault root, e.g. "projects/alpha.md")'),
14342
- limit: z23.number().optional().describe("Maximum number of similar notes to return (default: 10)"),
14343
- 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)")
14344
15007
  }
14345
15008
  },
14346
- async ({ path: path29, limit, exclude_linked }) => {
15009
+ async ({ path: path30, limit, exclude_linked }) => {
14347
15010
  const index = getIndex();
14348
15011
  const vaultPath2 = getVaultPath();
14349
15012
  const stateDb2 = getStateDb();
@@ -14352,10 +15015,10 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
14352
15015
  content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
14353
15016
  };
14354
15017
  }
14355
- if (!index.notes.has(path29)) {
15018
+ if (!index.notes.has(path30)) {
14356
15019
  return {
14357
15020
  content: [{ type: "text", text: JSON.stringify({
14358
- error: `Note not found: ${path29}`,
15021
+ error: `Note not found: ${path30}`,
14359
15022
  hint: "Use the full relative path including .md extension"
14360
15023
  }, null, 2) }]
14361
15024
  };
@@ -14366,12 +15029,12 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
14366
15029
  };
14367
15030
  const useHybrid = hasEmbeddingsIndex();
14368
15031
  const method = useHybrid ? "hybrid" : "bm25";
14369
- 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);
14370
15033
  return {
14371
15034
  content: [{
14372
15035
  type: "text",
14373
15036
  text: JSON.stringify({
14374
- source: path29,
15037
+ source: path30,
14375
15038
  method,
14376
15039
  exclude_linked: exclude_linked ?? true,
14377
15040
  count: results.length,
@@ -14384,7 +15047,7 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
14384
15047
  }
14385
15048
 
14386
15049
  // src/tools/read/semantic.ts
14387
- import { z as z24 } from "zod";
15050
+ import { z as z26 } from "zod";
14388
15051
  import { getAllEntitiesFromDb } from "@velvetmonkey/vault-core";
14389
15052
  function registerSemanticTools(server2, getVaultPath, getStateDb) {
14390
15053
  server2.registerTool(
@@ -14393,7 +15056,7 @@ function registerSemanticTools(server2, getVaultPath, getStateDb) {
14393
15056
  title: "Initialize Semantic Search",
14394
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.",
14395
15058
  inputSchema: {
14396
- force: z24.boolean().optional().describe(
15059
+ force: z26.boolean().optional().describe(
14397
15060
  "Rebuild all embeddings even if they already exist (default: false)"
14398
15061
  )
14399
15062
  }
@@ -14471,6 +15134,142 @@ function registerSemanticTools(server2, getVaultPath, getStateDb) {
14471
15134
  );
14472
15135
  }
14473
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
+
14474
15273
  // src/resources/vault.ts
14475
15274
  function registerVaultResources(server2, getIndex) {
14476
15275
  server2.registerResource(
@@ -14643,11 +15442,11 @@ function parseEnabledCategories() {
14643
15442
  categories.add(c);
14644
15443
  }
14645
15444
  } else {
14646
- console.error(`[Memory] Warning: Unknown tool category "${item}" - ignoring`);
15445
+ serverLog("server", `Unknown tool category "${item}" \u2014 ignoring`, "warn");
14647
15446
  }
14648
15447
  }
14649
15448
  if (categories.size === 0) {
14650
- console.error(`[Memory] No valid categories found, using default (${DEFAULT_PRESET})`);
15449
+ serverLog("server", `No valid categories found, using default (${DEFAULT_PRESET})`, "warn");
14651
15450
  return new Set(PRESETS[DEFAULT_PRESET]);
14652
15451
  }
14653
15452
  return categories;
@@ -14661,6 +15460,7 @@ var TOOL_CATEGORY = {
14661
15460
  refresh_index: "health",
14662
15461
  // absorbed rebuild_search_index
14663
15462
  get_all_entities: "health",
15463
+ list_entities: "hubs",
14664
15464
  get_unlinked_mentions: "health",
14665
15465
  // search (unified: metadata + content + entities)
14666
15466
  search: "search",
@@ -14715,7 +15515,16 @@ var TOOL_CATEGORY = {
14715
15515
  // health (activity tracking)
14716
15516
  vault_activity: "health",
14717
15517
  // schema (content similarity)
14718
- 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"
14719
15528
  };
14720
15529
  var server = new McpServer({
14721
15530
  name: "flywheel-memory",
@@ -14792,7 +15601,7 @@ if (_originalRegisterTool) {
14792
15601
  };
14793
15602
  }
14794
15603
  var categoryList = Array.from(enabledCategories).sort().join(", ");
14795
- console.error(`[Memory] Tool categories: ${categoryList}`);
15604
+ serverLog("server", `Tool categories: ${categoryList}`);
14796
15605
  registerHealthTools(server, () => vaultIndex, () => vaultPath, () => flywheelConfig, () => stateDb);
14797
15606
  registerSystemTools(
14798
15607
  server,
@@ -14810,19 +15619,28 @@ registerGraphTools(server, () => vaultIndex, () => vaultPath);
14810
15619
  registerWikilinkTools(server, () => vaultIndex, () => vaultPath);
14811
15620
  registerQueryTools(server, () => vaultIndex, () => vaultPath, () => stateDb);
14812
15621
  registerPrimitiveTools(server, () => vaultIndex, () => vaultPath, () => flywheelConfig);
14813
- registerGraphAnalysisTools(server, () => vaultIndex, () => vaultPath, () => stateDb);
15622
+ registerGraphAnalysisTools(server, () => vaultIndex, () => vaultPath, () => stateDb, () => flywheelConfig);
14814
15623
  registerVaultSchemaTools(server, () => vaultIndex, () => vaultPath);
14815
- registerNoteIntelligenceTools(server, () => vaultIndex, () => vaultPath);
15624
+ registerNoteIntelligenceTools(server, () => vaultIndex, () => vaultPath, () => flywheelConfig);
14816
15625
  registerMigrationTools(server, () => vaultIndex, () => vaultPath);
14817
15626
  registerMutationTools(server, vaultPath, () => flywheelConfig);
14818
15627
  registerTaskTools(server, vaultPath);
14819
15628
  registerFrontmatterTools(server, vaultPath);
14820
15629
  registerNoteTools(server, vaultPath, () => vaultIndex);
14821
15630
  registerMoveNoteTools(server, vaultPath);
15631
+ registerMergeTools(server, vaultPath);
14822
15632
  registerSystemTools2(server, vaultPath);
14823
15633
  registerPolicyTools(server, vaultPath);
14824
15634
  registerTagTools(server, () => vaultIndex, () => vaultPath);
14825
15635
  registerWikilinkFeedbackTools(server, () => stateDb);
15636
+ registerConfigTools(
15637
+ server,
15638
+ () => flywheelConfig,
15639
+ (newConfig) => {
15640
+ flywheelConfig = newConfig;
15641
+ },
15642
+ () => stateDb
15643
+ );
14826
15644
  registerMetricsTools(server, () => vaultIndex, () => stateDb);
14827
15645
  registerActivityTools(server, () => stateDb, () => {
14828
15646
  try {
@@ -14833,66 +15651,68 @@ registerActivityTools(server, () => stateDb, () => {
14833
15651
  });
14834
15652
  registerSimilarityTools(server, () => vaultIndex, () => vaultPath, () => stateDb);
14835
15653
  registerSemanticTools(server, () => vaultPath, () => stateDb);
15654
+ registerMergeTools2(server, () => stateDb);
14836
15655
  registerVaultResources(server, () => vaultIndex ?? null);
14837
- console.error(`[Memory] Registered ${_registeredCount} tools, skipped ${_skippedCount}`);
15656
+ serverLog("server", `Registered ${_registeredCount} tools, skipped ${_skippedCount}`);
14838
15657
  async function main() {
14839
- console.error(`[Memory] Starting Flywheel Memory server...`);
14840
- console.error(`[Memory] Vault: ${vaultPath}`);
15658
+ serverLog("server", "Starting Flywheel Memory server...");
15659
+ serverLog("server", `Vault: ${vaultPath}`);
14841
15660
  const startTime = Date.now();
14842
15661
  try {
14843
15662
  stateDb = openStateDb(vaultPath);
14844
- console.error("[Memory] StateDb initialized");
15663
+ serverLog("statedb", "StateDb initialized");
14845
15664
  setFTS5Database(stateDb.db);
14846
15665
  setEmbeddingsDatabase(stateDb.db);
15666
+ setTaskCacheDatabase(stateDb.db);
14847
15667
  loadEntityEmbeddingsToMemory();
14848
15668
  setWriteStateDb(stateDb);
14849
15669
  } catch (err) {
14850
15670
  const msg = err instanceof Error ? err.message : String(err);
14851
- console.error(`[Memory] StateDb initialization failed: ${msg}`);
14852
- 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");
14853
15673
  }
14854
15674
  const transport = new StdioServerTransport();
14855
15675
  await server.connect(transport);
14856
- console.error("[Memory] MCP server connected");
15676
+ serverLog("server", "MCP server connected");
14857
15677
  initializeLogger(vaultPath).then(() => {
14858
15678
  const logger3 = getLogger();
14859
15679
  if (logger3?.enabled) {
14860
- console.error(`[Memory] Unified logging enabled`);
15680
+ serverLog("server", "Unified logging enabled");
14861
15681
  }
14862
15682
  }).catch(() => {
14863
15683
  });
14864
15684
  initializeLogger2(vaultPath).catch((err) => {
14865
- console.error(`[Memory] Write logger initialization failed: ${err}`);
15685
+ serverLog("server", `Write logger initialization failed: ${err}`, "error");
14866
15686
  });
14867
15687
  if (process.env.FLYWHEEL_SKIP_FTS5 !== "true") {
14868
15688
  if (isIndexStale(vaultPath)) {
14869
15689
  buildFTS5Index(vaultPath).then(() => {
14870
- console.error("[Memory] FTS5 search index ready");
15690
+ serverLog("fts5", "Search index ready");
14871
15691
  }).catch((err) => {
14872
- console.error("[Memory] FTS5 build failed:", err);
15692
+ serverLog("fts5", `Build failed: ${err instanceof Error ? err.message : err}`, "error");
14873
15693
  });
14874
15694
  } else {
14875
- console.error("[Memory] FTS5 search index already fresh, skipping rebuild");
15695
+ serverLog("fts5", "Search index already fresh, skipping rebuild");
14876
15696
  }
14877
15697
  } else {
14878
- console.error("[Memory] FTS5 indexing skipped (FLYWHEEL_SKIP_FTS5=true)");
15698
+ serverLog("fts5", "Skipping \u2014 FLYWHEEL_SKIP_FTS5");
14879
15699
  }
14880
15700
  let cachedIndex = null;
14881
15701
  if (stateDb) {
14882
15702
  try {
14883
15703
  const files = await scanVault(vaultPath);
14884
15704
  const noteCount = files.length;
14885
- console.error(`[Memory] Found ${noteCount} markdown files`);
15705
+ serverLog("index", `Found ${noteCount} markdown files`);
14886
15706
  cachedIndex = loadVaultIndexFromCache(stateDb, noteCount);
14887
15707
  } catch (err) {
14888
- console.error("[Memory] Cache check failed:", err);
15708
+ serverLog("index", `Cache check failed: ${err instanceof Error ? err.message : err}`, "warn");
14889
15709
  }
14890
15710
  }
14891
15711
  if (cachedIndex) {
14892
15712
  vaultIndex = cachedIndex;
14893
15713
  setIndexState("ready");
14894
15714
  const duration = Date.now() - startTime;
14895
- console.error(`[Memory] Index loaded from cache in ${duration}ms`);
15715
+ serverLog("index", `Loaded from cache in ${duration}ms \u2014 ${cachedIndex.notes.size} notes`);
14896
15716
  if (stateDb) {
14897
15717
  recordIndexEvent(stateDb, {
14898
15718
  trigger: "startup_cache",
@@ -14902,12 +15722,12 @@ async function main() {
14902
15722
  }
14903
15723
  runPostIndexWork(vaultIndex);
14904
15724
  } else {
14905
- console.error("[Memory] Building vault index...");
15725
+ serverLog("index", "Building vault index...");
14906
15726
  try {
14907
15727
  vaultIndex = await buildVaultIndex(vaultPath);
14908
15728
  setIndexState("ready");
14909
15729
  const duration = Date.now() - startTime;
14910
- console.error(`[Memory] Vault index ready in ${duration}ms`);
15730
+ serverLog("index", `Vault index ready in ${duration}ms \u2014 ${vaultIndex.notes.size} notes`);
14911
15731
  if (stateDb) {
14912
15732
  recordIndexEvent(stateDb, {
14913
15733
  trigger: "startup_build",
@@ -14918,9 +15738,9 @@ async function main() {
14918
15738
  if (stateDb) {
14919
15739
  try {
14920
15740
  saveVaultIndexToCache(stateDb, vaultIndex);
14921
- console.error("[Memory] Index cache saved");
15741
+ serverLog("index", "Index cache saved");
14922
15742
  } catch (err) {
14923
- console.error("[Memory] Failed to save index cache:", err);
15743
+ serverLog("index", `Failed to save index cache: ${err instanceof Error ? err.message : err}`, "error");
14924
15744
  }
14925
15745
  }
14926
15746
  await runPostIndexWork(vaultIndex);
@@ -14936,7 +15756,7 @@ async function main() {
14936
15756
  error: err instanceof Error ? err.message : String(err)
14937
15757
  });
14938
15758
  }
14939
- console.error("[Memory] Failed to build vault index:", err);
15759
+ serverLog("index", `Failed to build vault index: ${err instanceof Error ? err.message : err}`, "error");
14940
15760
  }
14941
15761
  }
14942
15762
  }
@@ -14967,9 +15787,9 @@ async function updateEntitiesInStateDb() {
14967
15787
  ]
14968
15788
  });
14969
15789
  stateDb.replaceAllEntities(entityIndex2);
14970
- console.error(`[Memory] Updated ${entityIndex2._metadata.total_entities} entities in StateDb`);
15790
+ serverLog("index", `Updated ${entityIndex2._metadata.total_entities} entities in StateDb`);
14971
15791
  } catch (e) {
14972
- 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");
14973
15793
  }
14974
15794
  }
14975
15795
  async function runPostIndexWork(index) {
@@ -14983,9 +15803,9 @@ async function runPostIndexWork(index) {
14983
15803
  purgeOldMetrics(stateDb, 90);
14984
15804
  purgeOldIndexEvents(stateDb, 90);
14985
15805
  purgeOldInvocations(stateDb, 90);
14986
- console.error("[Memory] Growth metrics recorded");
15806
+ serverLog("server", "Growth metrics recorded");
14987
15807
  } catch (err) {
14988
- console.error("[Memory] Failed to record metrics:", err);
15808
+ serverLog("server", `Failed to record metrics: ${err instanceof Error ? err.message : err}`, "error");
14989
15809
  }
14990
15810
  }
14991
15811
  if (stateDb) {
@@ -14994,14 +15814,14 @@ async function runPostIndexWork(index) {
14994
15814
  recordGraphSnapshot(stateDb, graphMetrics);
14995
15815
  purgeOldSnapshots(stateDb, 90);
14996
15816
  } catch (err) {
14997
- console.error("[Memory] Failed to record graph snapshot:", err);
15817
+ serverLog("server", `Failed to record graph snapshot: ${err instanceof Error ? err.message : err}`, "error");
14998
15818
  }
14999
15819
  }
15000
15820
  if (stateDb) {
15001
15821
  try {
15002
15822
  updateSuppressionList(stateDb);
15003
15823
  } catch (err) {
15004
- console.error("[Memory] Failed to update suppression list:", err);
15824
+ serverLog("server", `Failed to update suppression list: ${err instanceof Error ? err.message : err}`, "error");
15005
15825
  }
15006
15826
  }
15007
15827
  const existing = loadConfig(stateDb);
@@ -15010,21 +15830,25 @@ async function runPostIndexWork(index) {
15010
15830
  saveConfig(stateDb, inferred, existing);
15011
15831
  }
15012
15832
  flywheelConfig = loadConfig(stateDb);
15833
+ if (stateDb) {
15834
+ refreshIfStale(vaultPath, index, flywheelConfig.exclude_task_tags);
15835
+ serverLog("tasks", "Task cache ready");
15836
+ }
15013
15837
  if (flywheelConfig.vault_name) {
15014
- console.error(`[Memory] Vault: ${flywheelConfig.vault_name}`);
15838
+ serverLog("config", `Vault: ${flywheelConfig.vault_name}`);
15015
15839
  }
15016
15840
  if (process.env.FLYWHEEL_SKIP_EMBEDDINGS !== "true") {
15017
15841
  if (hasEmbeddingsIndex()) {
15018
- console.error("[Memory] Embeddings already built, skipping full scan");
15842
+ serverLog("semantic", "Embeddings already built, skipping full scan");
15019
15843
  } else {
15020
15844
  setEmbeddingsBuilding(true);
15021
15845
  buildEmbeddingsIndex(vaultPath, (p) => {
15022
15846
  if (p.current % 100 === 0 || p.current === p.total) {
15023
- console.error(`[Semantic] ${p.current}/${p.total}`);
15847
+ serverLog("semantic", `Embedding ${p.current}/${p.total} notes...`);
15024
15848
  }
15025
15849
  }).then(async () => {
15026
15850
  if (stateDb) {
15027
- const entities = getAllEntitiesFromDb2(stateDb);
15851
+ const entities = getAllEntitiesFromDb3(stateDb);
15028
15852
  if (entities.length > 0) {
15029
15853
  const entityMap = new Map(entities.map((e) => [e.name, {
15030
15854
  name: e.name,
@@ -15036,29 +15860,29 @@ async function runPostIndexWork(index) {
15036
15860
  }
15037
15861
  }
15038
15862
  loadEntityEmbeddingsToMemory();
15039
- console.error("[Memory] Embeddings refreshed");
15863
+ serverLog("semantic", "Embeddings ready");
15040
15864
  }).catch((err) => {
15041
- console.error("[Memory] Embeddings refresh failed:", err);
15865
+ serverLog("semantic", `Embeddings build failed: ${err instanceof Error ? err.message : err}`, "error");
15042
15866
  });
15043
15867
  }
15044
15868
  } else {
15045
- console.error("[Memory] Embeddings skipped (FLYWHEEL_SKIP_EMBEDDINGS=true)");
15869
+ serverLog("semantic", "Skipping \u2014 FLYWHEEL_SKIP_EMBEDDINGS");
15046
15870
  }
15047
15871
  if (process.env.FLYWHEEL_WATCH !== "false") {
15048
15872
  const config = parseWatcherConfig();
15049
- console.error(`[Memory] File watcher enabled (debounce: ${config.debounceMs}ms)`);
15873
+ serverLog("watcher", `File watcher enabled (debounce: ${config.debounceMs}ms)`);
15050
15874
  const watcher = createVaultWatcher({
15051
15875
  vaultPath,
15052
15876
  config,
15053
15877
  onBatch: async (batch) => {
15054
- console.error(`[Memory] Processing ${batch.events.length} file changes`);
15878
+ serverLog("watcher", `Processing ${batch.events.length} file changes`);
15055
15879
  const batchStart = Date.now();
15056
15880
  const changedPaths = batch.events.map((e) => e.path);
15057
15881
  try {
15058
15882
  vaultIndex = await buildVaultIndex(vaultPath);
15059
15883
  setIndexState("ready");
15060
15884
  const duration = Date.now() - batchStart;
15061
- console.error(`[Memory] Index rebuilt in ${duration}ms`);
15885
+ serverLog("watcher", `Index rebuilt in ${duration}ms`);
15062
15886
  if (stateDb) {
15063
15887
  recordIndexEvent(stateDb, {
15064
15888
  trigger: "watcher",
@@ -15076,7 +15900,7 @@ async function runPostIndexWork(index) {
15076
15900
  if (event.type === "delete") {
15077
15901
  removeEmbedding(event.path);
15078
15902
  } else if (event.path.endsWith(".md")) {
15079
- const absPath = path28.join(vaultPath, event.path);
15903
+ const absPath = path29.join(vaultPath, event.path);
15080
15904
  await updateEmbedding(event.path, absPath);
15081
15905
  }
15082
15906
  } catch {
@@ -15085,7 +15909,7 @@ async function runPostIndexWork(index) {
15085
15909
  }
15086
15910
  if (hasEntityEmbeddingsIndex() && stateDb) {
15087
15911
  try {
15088
- const allEntities = getAllEntitiesFromDb2(stateDb);
15912
+ const allEntities = getAllEntitiesFromDb3(stateDb);
15089
15913
  for (const event of batch.events) {
15090
15914
  if (event.type === "delete" || !event.path.endsWith(".md")) continue;
15091
15915
  const matching = allEntities.filter((e) => e.path === event.path);
@@ -15105,7 +15929,17 @@ async function runPostIndexWork(index) {
15105
15929
  try {
15106
15930
  saveVaultIndexToCache(stateDb, vaultIndex);
15107
15931
  } catch (err) {
15108
- 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 {
15109
15943
  }
15110
15944
  }
15111
15945
  } catch (err) {
@@ -15122,16 +15956,16 @@ async function runPostIndexWork(index) {
15122
15956
  error: err instanceof Error ? err.message : String(err)
15123
15957
  });
15124
15958
  }
15125
- console.error("[Memory] Failed to rebuild index:", err);
15959
+ serverLog("watcher", `Failed to rebuild index: ${err instanceof Error ? err.message : err}`, "error");
15126
15960
  }
15127
15961
  },
15128
15962
  onStateChange: (status) => {
15129
15963
  if (status.state === "dirty") {
15130
- console.error("[Memory] Warning: Index may be stale");
15964
+ serverLog("watcher", "Index may be stale", "warn");
15131
15965
  }
15132
15966
  },
15133
15967
  onError: (err) => {
15134
- console.error("[Memory] Watcher error:", err.message);
15968
+ serverLog("watcher", `Watcher error: ${err.message}`, "error");
15135
15969
  }
15136
15970
  });
15137
15971
  watcher.start();
@@ -15142,15 +15976,15 @@ if (process.argv.includes("--init-semantic")) {
15142
15976
  console.error("[Semantic] Pre-warming semantic search...");
15143
15977
  console.error(`[Semantic] Vault: ${vaultPath}`);
15144
15978
  try {
15145
- const db3 = openStateDb(vaultPath);
15146
- setEmbeddingsDatabase(db3.db);
15979
+ const db4 = openStateDb(vaultPath);
15980
+ setEmbeddingsDatabase(db4.db);
15147
15981
  const progress = await buildEmbeddingsIndex(vaultPath, (p) => {
15148
15982
  if (p.current % 50 === 0 || p.current === p.total) {
15149
15983
  console.error(`[Semantic] Embedding ${p.current}/${p.total} notes (${p.skipped} skipped)...`);
15150
15984
  }
15151
15985
  });
15152
15986
  console.error(`[Semantic] Done. Embedded ${progress.total - progress.skipped} notes, skipped ${progress.skipped}.`);
15153
- db3.close();
15987
+ db4.close();
15154
15988
  process.exit(0);
15155
15989
  } catch (err) {
15156
15990
  console.error("[Semantic] Failed:", err instanceof Error ? err.message : err);