@velvetmonkey/flywheel-mcp 1.27.18 → 1.27.20

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 +401 -141
  2. package/package.json +3 -1
package/dist/index.js CHANGED
@@ -248,8 +248,8 @@ function getIndexProgress() {
248
248
  function getIndexError() {
249
249
  return indexError;
250
250
  }
251
- function setIndexState(state) {
252
- indexState = state;
251
+ function setIndexState(state2) {
252
+ indexState = state2;
253
253
  }
254
254
  function setIndexError(error) {
255
255
  indexError = error;
@@ -260,8 +260,8 @@ function updateIndexProgress(parsed, total) {
260
260
  function normalizeTarget(target) {
261
261
  return target.toLowerCase().replace(/\.md$/, "");
262
262
  }
263
- function normalizeNotePath(path13) {
264
- return path13.toLowerCase().replace(/\.md$/, "");
263
+ function normalizeNotePath(path14) {
264
+ return path14.toLowerCase().replace(/\.md$/, "");
265
265
  }
266
266
  async function buildVaultIndex(vaultPath2, options = {}) {
267
267
  const { timeoutMs = DEFAULT_TIMEOUT_MS, onProgress } = options;
@@ -455,7 +455,7 @@ function findSimilarEntity(index, target) {
455
455
  }
456
456
  const maxDist = normalizedLen <= 10 ? 1 : 2;
457
457
  let bestMatch;
458
- for (const [entity, path13] of index.entities) {
458
+ for (const [entity, path14] of index.entities) {
459
459
  const lenDiff = Math.abs(entity.length - normalizedLen);
460
460
  if (lenDiff > maxDist) {
461
461
  continue;
@@ -463,7 +463,7 @@ function findSimilarEntity(index, target) {
463
463
  const dist = levenshteinDistance(normalized, entity);
464
464
  if (dist > 0 && dist <= maxDist) {
465
465
  if (!bestMatch || dist < bestMatch.distance) {
466
- bestMatch = { path: path13, entity, distance: dist };
466
+ bestMatch = { path: path14, entity, distance: dist };
467
467
  if (dist === 1) {
468
468
  return bestMatch;
469
469
  }
@@ -483,13 +483,13 @@ var MAX_LIMIT = 200;
483
483
 
484
484
  // src/core/indexGuard.ts
485
485
  function requireIndex() {
486
- const state = getIndexState();
487
- if (state === "building") {
486
+ const state2 = getIndexState();
487
+ if (state2 === "building") {
488
488
  const { parsed, total } = getIndexProgress();
489
489
  const progress = total > 0 ? ` (${parsed}/${total} files)` : "";
490
490
  throw new Error(`Index building${progress}... try again shortly`);
491
491
  }
492
- if (state === "error") {
492
+ if (state2 === "error") {
493
493
  const error = getIndexError();
494
494
  throw new Error(`Index failed to build: ${error?.message || "unknown error"}`);
495
495
  }
@@ -889,14 +889,14 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
889
889
  };
890
890
  function findSimilarEntity2(target, entities) {
891
891
  const targetLower = target.toLowerCase();
892
- for (const [name, path13] of entities) {
892
+ for (const [name, path14] of entities) {
893
893
  if (name.startsWith(targetLower) || targetLower.startsWith(name)) {
894
- return path13;
894
+ return path14;
895
895
  }
896
896
  }
897
- for (const [name, path13] of entities) {
897
+ for (const [name, path14] of entities) {
898
898
  if (name.includes(targetLower) || targetLower.includes(name)) {
899
- return path13;
899
+ return path14;
900
900
  }
901
901
  }
902
902
  return void 0;
@@ -1193,8 +1193,8 @@ function registerHealthTools(server2, getIndex, getVaultPath) {
1193
1193
  top_tags: z3.array(TagStatSchema).describe("Top 20 most used tags"),
1194
1194
  folders: z3.array(FolderStatSchema).describe("Note counts by top-level folder")
1195
1195
  };
1196
- function isPeriodicNote(path13) {
1197
- const filename = path13.split("/").pop() || "";
1196
+ function isPeriodicNote(path14) {
1197
+ const filename = path14.split("/").pop() || "";
1198
1198
  const nameWithoutExt = filename.replace(/\.md$/, "");
1199
1199
  const patterns = [
1200
1200
  /^\d{4}-\d{2}-\d{2}$/,
@@ -1209,7 +1209,7 @@ function registerHealthTools(server2, getIndex, getVaultPath) {
1209
1209
  // YYYY (yearly)
1210
1210
  ];
1211
1211
  const periodicFolders = ["daily", "weekly", "monthly", "quarterly", "yearly", "journal", "journals"];
1212
- const folder = path13.split("/")[0]?.toLowerCase() || "";
1212
+ const folder = path14.split("/")[0]?.toLowerCase() || "";
1213
1213
  return patterns.some((p) => p.test(nameWithoutExt)) || periodicFolders.includes(folder);
1214
1214
  }
1215
1215
  server2.registerTool(
@@ -1302,6 +1302,166 @@ function registerHealthTools(server2, getIndex, getVaultPath) {
1302
1302
 
1303
1303
  // src/tools/query.ts
1304
1304
  import { z as z4 } from "zod";
1305
+
1306
+ // src/core/fts5.ts
1307
+ import Database from "better-sqlite3";
1308
+ import * as fs5 from "fs";
1309
+ import * as path3 from "path";
1310
+ var EXCLUDED_DIRS2 = /* @__PURE__ */ new Set([
1311
+ ".obsidian",
1312
+ ".trash",
1313
+ ".git",
1314
+ "node_modules",
1315
+ "templates",
1316
+ ".claude"
1317
+ ]);
1318
+ var MAX_INDEX_FILE_SIZE = 5 * 1024 * 1024;
1319
+ var STALE_THRESHOLD_MS = 60 * 60 * 1e3;
1320
+ var db = null;
1321
+ var state = {
1322
+ ready: false,
1323
+ lastBuilt: null,
1324
+ noteCount: 0,
1325
+ error: null
1326
+ };
1327
+ function getDbPath(vaultPath2) {
1328
+ const claudeDir = path3.join(vaultPath2, ".claude");
1329
+ if (!fs5.existsSync(claudeDir)) {
1330
+ fs5.mkdirSync(claudeDir, { recursive: true });
1331
+ }
1332
+ return path3.join(claudeDir, "vault-search.db");
1333
+ }
1334
+ function initDatabase(vaultPath2) {
1335
+ const dbPath = getDbPath(vaultPath2);
1336
+ const database = new Database(dbPath);
1337
+ database.exec(`
1338
+ CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(
1339
+ path,
1340
+ title,
1341
+ content,
1342
+ tokenize='porter'
1343
+ );
1344
+
1345
+ CREATE TABLE IF NOT EXISTS fts_metadata (
1346
+ key TEXT PRIMARY KEY,
1347
+ value TEXT
1348
+ );
1349
+ `);
1350
+ return database;
1351
+ }
1352
+ function shouldIndexFile(filePath) {
1353
+ const parts = filePath.split("/");
1354
+ return !parts.some((part) => EXCLUDED_DIRS2.has(part));
1355
+ }
1356
+ async function buildFTS5Index(vaultPath2) {
1357
+ try {
1358
+ state.error = null;
1359
+ db = initDatabase(vaultPath2);
1360
+ db.exec("DELETE FROM notes_fts");
1361
+ const files = await scanVault(vaultPath2);
1362
+ const indexableFiles = files.filter((f) => shouldIndexFile(f.path));
1363
+ const insert = db.prepare(
1364
+ "INSERT INTO notes_fts (path, title, content) VALUES (?, ?, ?)"
1365
+ );
1366
+ const insertMany = db.transaction((filesToIndex) => {
1367
+ let indexed2 = 0;
1368
+ for (const file of filesToIndex) {
1369
+ try {
1370
+ const stats = fs5.statSync(file.absolutePath);
1371
+ if (stats.size > MAX_INDEX_FILE_SIZE) {
1372
+ continue;
1373
+ }
1374
+ const content = fs5.readFileSync(file.absolutePath, "utf-8");
1375
+ const title = file.path.replace(/\.md$/, "").split("/").pop() || file.path;
1376
+ insert.run(file.path, title, content);
1377
+ indexed2++;
1378
+ } catch (err) {
1379
+ console.error(`[FTS5] Skipping ${file.path}:`, err);
1380
+ }
1381
+ }
1382
+ return indexed2;
1383
+ });
1384
+ const indexed = insertMany(indexableFiles);
1385
+ const now = /* @__PURE__ */ new Date();
1386
+ db.prepare(
1387
+ "INSERT OR REPLACE INTO fts_metadata (key, value) VALUES (?, ?)"
1388
+ ).run("last_built", now.toISOString());
1389
+ state = {
1390
+ ready: true,
1391
+ lastBuilt: now,
1392
+ noteCount: indexed,
1393
+ error: null
1394
+ };
1395
+ console.error(`[FTS5] Indexed ${indexed} notes`);
1396
+ return state;
1397
+ } catch (err) {
1398
+ state = {
1399
+ ready: false,
1400
+ lastBuilt: null,
1401
+ noteCount: 0,
1402
+ error: err instanceof Error ? err.message : String(err)
1403
+ };
1404
+ throw err;
1405
+ }
1406
+ }
1407
+ function isIndexStale(vaultPath2) {
1408
+ const dbPath = getDbPath(vaultPath2);
1409
+ if (!fs5.existsSync(dbPath)) {
1410
+ return true;
1411
+ }
1412
+ try {
1413
+ const database = new Database(dbPath, { readonly: true });
1414
+ const row = database.prepare(
1415
+ "SELECT value FROM fts_metadata WHERE key = ?"
1416
+ ).get("last_built");
1417
+ database.close();
1418
+ if (!row) {
1419
+ return true;
1420
+ }
1421
+ const lastBuilt = new Date(row.value);
1422
+ const age = Date.now() - lastBuilt.getTime();
1423
+ return age > STALE_THRESHOLD_MS;
1424
+ } catch {
1425
+ return true;
1426
+ }
1427
+ }
1428
+ function ensureDb(vaultPath2) {
1429
+ if (!db) {
1430
+ const dbPath = getDbPath(vaultPath2);
1431
+ if (!fs5.existsSync(dbPath)) {
1432
+ throw new Error("Search index not built. Call rebuild_search_index first.");
1433
+ }
1434
+ db = new Database(dbPath);
1435
+ }
1436
+ return db;
1437
+ }
1438
+ function searchFTS5(vaultPath2, query, limit = 10) {
1439
+ const database = ensureDb(vaultPath2);
1440
+ try {
1441
+ const stmt = database.prepare(`
1442
+ SELECT
1443
+ path,
1444
+ title,
1445
+ snippet(notes_fts, 2, '[', ']', '...', 20) as snippet
1446
+ FROM notes_fts
1447
+ WHERE notes_fts MATCH ?
1448
+ ORDER BY rank
1449
+ LIMIT ?
1450
+ `);
1451
+ const results = stmt.all(query, limit);
1452
+ return results;
1453
+ } catch (err) {
1454
+ if (err instanceof Error && err.message.includes("fts5: syntax error")) {
1455
+ throw new Error(`Invalid search query: ${query}. Check FTS5 syntax.`);
1456
+ }
1457
+ throw err;
1458
+ }
1459
+ }
1460
+ function getFTS5State() {
1461
+ return { ...state };
1462
+ }
1463
+
1464
+ // src/tools/query.ts
1305
1465
  function matchesFrontmatter(note, where) {
1306
1466
  for (const [key, value] of Object.entries(where)) {
1307
1467
  const noteValue = note.frontmatter[key];
@@ -1480,25 +1640,125 @@ function registerQueryTools(server2, getIndex, getVaultPath) {
1480
1640
  };
1481
1641
  }
1482
1642
  );
1643
+ const FTS5ResultSchema = z4.object({
1644
+ path: z4.string().describe("Path to the note"),
1645
+ title: z4.string().describe("Note title"),
1646
+ snippet: z4.string().describe("Matching snippet with highlighted terms")
1647
+ });
1648
+ const FullTextSearchOutputSchema = {
1649
+ query: z4.string().describe("The search query that was executed"),
1650
+ total_results: z4.number().describe("Number of matching results"),
1651
+ results: z4.array(FTS5ResultSchema).describe("Matching notes with snippets")
1652
+ };
1653
+ server2.registerTool(
1654
+ "full_text_search",
1655
+ {
1656
+ title: "Full-Text Search",
1657
+ description: 'Search note content using SQLite FTS5 full-text search. Supports stemming (running matches run/runs/ran), phrases ("exact phrase"), boolean operators (AND, OR, NOT), and prefix matching (auth*).',
1658
+ inputSchema: {
1659
+ query: z4.string().describe(
1660
+ 'Search query. Examples: "authentication", "exact phrase", "term1 AND term2", "prefix*"'
1661
+ ),
1662
+ limit: z4.number().default(10).describe("Maximum number of results to return")
1663
+ },
1664
+ outputSchema: FullTextSearchOutputSchema
1665
+ },
1666
+ async ({
1667
+ query,
1668
+ limit: requestedLimit = 10
1669
+ }) => {
1670
+ const vaultPath2 = getVaultPath();
1671
+ const limit = Math.min(requestedLimit, MAX_LIMIT);
1672
+ const ftsState = getFTS5State();
1673
+ if (!ftsState.ready || isIndexStale(vaultPath2)) {
1674
+ console.error("[FTS5] Index stale or missing, rebuilding...");
1675
+ await buildFTS5Index(vaultPath2);
1676
+ }
1677
+ const results = searchFTS5(vaultPath2, query, limit);
1678
+ const output = {
1679
+ query,
1680
+ total_results: results.length,
1681
+ results
1682
+ };
1683
+ return {
1684
+ content: [
1685
+ {
1686
+ type: "text",
1687
+ text: JSON.stringify(output, null, 2)
1688
+ }
1689
+ ],
1690
+ structuredContent: output
1691
+ };
1692
+ }
1693
+ );
1694
+ const RebuildIndexOutputSchema = {
1695
+ status: z4.enum(["success", "error"]).describe("Whether the rebuild succeeded"),
1696
+ notes_indexed: z4.number().describe("Number of notes indexed"),
1697
+ message: z4.string().describe("Status message")
1698
+ };
1699
+ server2.registerTool(
1700
+ "rebuild_search_index",
1701
+ {
1702
+ title: "Rebuild Search Index",
1703
+ description: "Manually rebuild the FTS5 full-text search index. Use this after bulk changes to the vault or if search results seem stale.",
1704
+ inputSchema: {},
1705
+ outputSchema: RebuildIndexOutputSchema
1706
+ },
1707
+ async () => {
1708
+ const vaultPath2 = getVaultPath();
1709
+ try {
1710
+ const state2 = await buildFTS5Index(vaultPath2);
1711
+ const output = {
1712
+ status: "success",
1713
+ notes_indexed: state2.noteCount,
1714
+ message: `Successfully indexed ${state2.noteCount} notes`
1715
+ };
1716
+ return {
1717
+ content: [
1718
+ {
1719
+ type: "text",
1720
+ text: JSON.stringify(output, null, 2)
1721
+ }
1722
+ ],
1723
+ structuredContent: output
1724
+ };
1725
+ } catch (err) {
1726
+ const output = {
1727
+ status: "error",
1728
+ notes_indexed: 0,
1729
+ message: err instanceof Error ? err.message : String(err)
1730
+ };
1731
+ return {
1732
+ content: [
1733
+ {
1734
+ type: "text",
1735
+ text: JSON.stringify(output, null, 2)
1736
+ }
1737
+ ],
1738
+ structuredContent: output
1739
+ };
1740
+ }
1741
+ }
1742
+ );
1483
1743
  }
1484
1744
 
1485
1745
  // src/tools/system.ts
1486
- import * as fs6 from "fs";
1487
- import * as path4 from "path";
1746
+ import * as fs7 from "fs";
1747
+ import * as path5 from "path";
1488
1748
  import { z as z5 } from "zod";
1489
1749
 
1490
1750
  // src/core/config.ts
1491
- import * as fs5 from "fs";
1492
- import * as path3 from "path";
1751
+ import * as fs6 from "fs";
1752
+ import * as path4 from "path";
1493
1753
  var DEFAULT_CONFIG = {
1494
1754
  exclude_task_tags: []
1495
1755
  };
1496
1756
  function loadConfig(vaultPath2) {
1497
- const claudeDir = path3.join(vaultPath2, ".claude");
1498
- const configPath = path3.join(claudeDir, ".flywheel.json");
1757
+ const claudeDir = path4.join(vaultPath2, ".claude");
1758
+ const configPath = path4.join(claudeDir, ".flywheel.json");
1499
1759
  try {
1500
- if (fs5.existsSync(configPath)) {
1501
- const content = fs5.readFileSync(configPath, "utf-8");
1760
+ if (fs6.existsSync(configPath)) {
1761
+ const content = fs6.readFileSync(configPath, "utf-8");
1502
1762
  const config = JSON.parse(content);
1503
1763
  return { ...DEFAULT_CONFIG, ...config };
1504
1764
  }
@@ -1528,7 +1788,7 @@ var FOLDER_PATTERNS = {
1528
1788
  function extractFolders(index) {
1529
1789
  const folders = /* @__PURE__ */ new Set();
1530
1790
  for (const notePath of index.notes.keys()) {
1531
- const dir = path3.dirname(notePath);
1791
+ const dir = path4.dirname(notePath);
1532
1792
  if (dir && dir !== ".") {
1533
1793
  const parts = dir.split(/[/\\]/);
1534
1794
  for (let i = 1; i <= parts.length; i++) {
@@ -1545,7 +1805,7 @@ function extractFolders(index) {
1545
1805
  function findMatchingFolder(folders, patterns) {
1546
1806
  const lowerPatterns = patterns.map((p) => p.toLowerCase());
1547
1807
  for (const folder of folders) {
1548
- const folderName = path3.basename(folder).toLowerCase();
1808
+ const folderName = path4.basename(folder).toLowerCase();
1549
1809
  if (lowerPatterns.includes(folderName)) {
1550
1810
  return folder;
1551
1811
  }
@@ -1558,7 +1818,7 @@ function inferConfig(index, vaultPath2) {
1558
1818
  paths: {}
1559
1819
  };
1560
1820
  if (vaultPath2) {
1561
- inferred.vault_name = path3.basename(vaultPath2);
1821
+ inferred.vault_name = path4.basename(vaultPath2);
1562
1822
  }
1563
1823
  const folders = extractFolders(index);
1564
1824
  const detectedPath = findMatchingFolder(folders, FOLDER_PATTERNS.daily_notes);
@@ -1582,11 +1842,11 @@ function inferConfig(index, vaultPath2) {
1582
1842
  return inferred;
1583
1843
  }
1584
1844
  function saveConfig(vaultPath2, inferred, existing) {
1585
- const claudeDir = path3.join(vaultPath2, ".claude");
1586
- const configPath = path3.join(claudeDir, ".flywheel.json");
1845
+ const claudeDir = path4.join(vaultPath2, ".claude");
1846
+ const configPath = path4.join(claudeDir, ".flywheel.json");
1587
1847
  try {
1588
- if (!fs5.existsSync(claudeDir)) {
1589
- fs5.mkdirSync(claudeDir, { recursive: true });
1848
+ if (!fs6.existsSync(claudeDir)) {
1849
+ fs6.mkdirSync(claudeDir, { recursive: true });
1590
1850
  }
1591
1851
  const mergedPaths = {
1592
1852
  ...inferred.paths,
@@ -1600,7 +1860,7 @@ function saveConfig(vaultPath2, inferred, existing) {
1600
1860
  ...Object.keys(mergedPaths).length > 0 ? { paths: mergedPaths } : {}
1601
1861
  };
1602
1862
  const content = JSON.stringify(merged, null, 2);
1603
- fs5.writeFileSync(configPath, content, "utf-8");
1863
+ fs6.writeFileSync(configPath, content, "utf-8");
1604
1864
  console.error(`[Flywheel] Saved .claude/.flywheel.json`);
1605
1865
  } catch (err) {
1606
1866
  console.error("[Flywheel] Failed to save .claude/.flywheel.json:", err);
@@ -1849,8 +2109,8 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
1849
2109
  continue;
1850
2110
  }
1851
2111
  try {
1852
- const fullPath = path4.join(vaultPath2, note.path);
1853
- const content = await fs6.promises.readFile(fullPath, "utf-8");
2112
+ const fullPath = path5.join(vaultPath2, note.path);
2113
+ const content = await fs7.promises.readFile(fullPath, "utf-8");
1854
2114
  const lines = content.split("\n");
1855
2115
  for (let i = 0; i < lines.length; i++) {
1856
2116
  const line = lines[i];
@@ -1965,8 +2225,8 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
1965
2225
  let wordCount;
1966
2226
  if (include_word_count) {
1967
2227
  try {
1968
- const fullPath = path4.join(vaultPath2, resolvedPath);
1969
- const content = await fs6.promises.readFile(fullPath, "utf-8");
2228
+ const fullPath = path5.join(vaultPath2, resolvedPath);
2229
+ const content = await fs7.promises.readFile(fullPath, "utf-8");
1970
2230
  wordCount = content.split(/\s+/).filter((w) => w.length > 0).length;
1971
2231
  } catch {
1972
2232
  }
@@ -2131,8 +2391,8 @@ function getStaleNotes(index, days, minBacklinks = 0) {
2131
2391
  return b.days_since_modified - a.days_since_modified;
2132
2392
  });
2133
2393
  }
2134
- function getContemporaneousNotes(index, path13, hours = 24) {
2135
- const targetNote = index.notes.get(path13);
2394
+ function getContemporaneousNotes(index, path14, hours = 24) {
2395
+ const targetNote = index.notes.get(path14);
2136
2396
  if (!targetNote) {
2137
2397
  return [];
2138
2398
  }
@@ -2140,7 +2400,7 @@ function getContemporaneousNotes(index, path13, hours = 24) {
2140
2400
  const windowMs = hours * 60 * 60 * 1e3;
2141
2401
  const results = [];
2142
2402
  for (const note of index.notes.values()) {
2143
- if (note.path === path13) continue;
2403
+ if (note.path === path14) continue;
2144
2404
  const timeDiff = Math.abs(note.modified.getTime() - targetTime);
2145
2405
  if (timeDiff <= windowMs) {
2146
2406
  results.push({
@@ -2188,8 +2448,8 @@ function getActivitySummary(index, days) {
2188
2448
  }
2189
2449
 
2190
2450
  // src/tools/structure.ts
2191
- import * as fs7 from "fs";
2192
- import * as path5 from "path";
2451
+ import * as fs8 from "fs";
2452
+ import * as path6 from "path";
2193
2453
  var HEADING_REGEX = /^(#{1,6})\s+(.+)$/;
2194
2454
  function extractHeadings(content) {
2195
2455
  const lines = content.split("\n");
@@ -2243,10 +2503,10 @@ function buildSections(headings, totalLines) {
2243
2503
  async function getNoteStructure(index, notePath, vaultPath2) {
2244
2504
  const note = index.notes.get(notePath);
2245
2505
  if (!note) return null;
2246
- const absolutePath = path5.join(vaultPath2, notePath);
2506
+ const absolutePath = path6.join(vaultPath2, notePath);
2247
2507
  let content;
2248
2508
  try {
2249
- content = await fs7.promises.readFile(absolutePath, "utf-8");
2509
+ content = await fs8.promises.readFile(absolutePath, "utf-8");
2250
2510
  } catch {
2251
2511
  return null;
2252
2512
  }
@@ -2266,10 +2526,10 @@ async function getNoteStructure(index, notePath, vaultPath2) {
2266
2526
  async function getHeadings(index, notePath, vaultPath2) {
2267
2527
  const note = index.notes.get(notePath);
2268
2528
  if (!note) return null;
2269
- const absolutePath = path5.join(vaultPath2, notePath);
2529
+ const absolutePath = path6.join(vaultPath2, notePath);
2270
2530
  let content;
2271
2531
  try {
2272
- content = await fs7.promises.readFile(absolutePath, "utf-8");
2532
+ content = await fs8.promises.readFile(absolutePath, "utf-8");
2273
2533
  } catch {
2274
2534
  return null;
2275
2535
  }
@@ -2278,10 +2538,10 @@ async function getHeadings(index, notePath, vaultPath2) {
2278
2538
  async function getSectionContent(index, notePath, headingText, vaultPath2, includeSubheadings = true) {
2279
2539
  const note = index.notes.get(notePath);
2280
2540
  if (!note) return null;
2281
- const absolutePath = path5.join(vaultPath2, notePath);
2541
+ const absolutePath = path6.join(vaultPath2, notePath);
2282
2542
  let content;
2283
2543
  try {
2284
- content = await fs7.promises.readFile(absolutePath, "utf-8");
2544
+ content = await fs8.promises.readFile(absolutePath, "utf-8");
2285
2545
  } catch {
2286
2546
  return null;
2287
2547
  }
@@ -2320,10 +2580,10 @@ async function findSections(index, headingPattern, vaultPath2, folder) {
2320
2580
  const results = [];
2321
2581
  for (const note of index.notes.values()) {
2322
2582
  if (folder && !note.path.startsWith(folder)) continue;
2323
- const absolutePath = path5.join(vaultPath2, note.path);
2583
+ const absolutePath = path6.join(vaultPath2, note.path);
2324
2584
  let content;
2325
2585
  try {
2326
- content = await fs7.promises.readFile(absolutePath, "utf-8");
2586
+ content = await fs8.promises.readFile(absolutePath, "utf-8");
2327
2587
  } catch {
2328
2588
  continue;
2329
2589
  }
@@ -2343,8 +2603,8 @@ async function findSections(index, headingPattern, vaultPath2, folder) {
2343
2603
  }
2344
2604
 
2345
2605
  // src/tools/tasks.ts
2346
- import * as fs8 from "fs";
2347
- import * as path6 from "path";
2606
+ import * as fs9 from "fs";
2607
+ import * as path7 from "path";
2348
2608
  var TASK_REGEX = /^(\s*)- \[([ xX\-])\]\s+(.+)$/;
2349
2609
  var TAG_REGEX2 = /#([a-zA-Z][a-zA-Z0-9_/-]*)/g;
2350
2610
  var DATE_REGEX = /\b(\d{4}-\d{2}-\d{2}|\d{1,2}\/\d{1,2}\/\d{2,4})\b/;
@@ -2370,7 +2630,7 @@ function extractDueDate(text) {
2370
2630
  async function extractTasksFromNote(notePath, absolutePath) {
2371
2631
  let content;
2372
2632
  try {
2373
- content = await fs8.promises.readFile(absolutePath, "utf-8");
2633
+ content = await fs9.promises.readFile(absolutePath, "utf-8");
2374
2634
  } catch {
2375
2635
  return [];
2376
2636
  }
@@ -2413,7 +2673,7 @@ async function getAllTasks(index, vaultPath2, options = {}) {
2413
2673
  const allTasks = [];
2414
2674
  for (const note of index.notes.values()) {
2415
2675
  if (folder && !note.path.startsWith(folder)) continue;
2416
- const absolutePath = path6.join(vaultPath2, note.path);
2676
+ const absolutePath = path7.join(vaultPath2, note.path);
2417
2677
  const tasks = await extractTasksFromNote(note.path, absolutePath);
2418
2678
  allTasks.push(...tasks);
2419
2679
  }
@@ -2444,7 +2704,7 @@ async function getAllTasks(index, vaultPath2, options = {}) {
2444
2704
  async function getTasksFromNote(index, notePath, vaultPath2, excludeTags = []) {
2445
2705
  const note = index.notes.get(notePath);
2446
2706
  if (!note) return null;
2447
- const absolutePath = path6.join(vaultPath2, notePath);
2707
+ const absolutePath = path7.join(vaultPath2, notePath);
2448
2708
  let tasks = await extractTasksFromNote(notePath, absolutePath);
2449
2709
  if (excludeTags.length > 0) {
2450
2710
  tasks = tasks.filter(
@@ -2932,14 +3192,14 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
2932
3192
  offset: z6.number().default(0).describe("Number of results to skip (for pagination)")
2933
3193
  }
2934
3194
  },
2935
- async ({ path: path13, hours, limit: requestedLimit, offset }) => {
3195
+ async ({ path: path14, hours, limit: requestedLimit, offset }) => {
2936
3196
  const limit = Math.min(requestedLimit ?? 50, MAX_LIMIT);
2937
3197
  const index = getIndex();
2938
- const allResults = getContemporaneousNotes(index, path13, hours);
3198
+ const allResults = getContemporaneousNotes(index, path14, hours);
2939
3199
  const result = allResults.slice(offset, offset + limit);
2940
3200
  return {
2941
3201
  content: [{ type: "text", text: JSON.stringify({
2942
- reference_note: path13,
3202
+ reference_note: path14,
2943
3203
  window_hours: hours,
2944
3204
  total_count: allResults.length,
2945
3205
  returned_count: result.length,
@@ -2977,13 +3237,13 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
2977
3237
  path: z6.string().describe("Path to the note")
2978
3238
  }
2979
3239
  },
2980
- async ({ path: path13 }) => {
3240
+ async ({ path: path14 }) => {
2981
3241
  const index = getIndex();
2982
3242
  const vaultPath2 = getVaultPath();
2983
- const result = await getNoteStructure(index, path13, vaultPath2);
3243
+ const result = await getNoteStructure(index, path14, vaultPath2);
2984
3244
  if (!result) {
2985
3245
  return {
2986
- content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path13 }, null, 2) }]
3246
+ content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path14 }, null, 2) }]
2987
3247
  };
2988
3248
  }
2989
3249
  return {
@@ -3000,18 +3260,18 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
3000
3260
  path: z6.string().describe("Path to the note")
3001
3261
  }
3002
3262
  },
3003
- async ({ path: path13 }) => {
3263
+ async ({ path: path14 }) => {
3004
3264
  const index = getIndex();
3005
3265
  const vaultPath2 = getVaultPath();
3006
- const result = await getHeadings(index, path13, vaultPath2);
3266
+ const result = await getHeadings(index, path14, vaultPath2);
3007
3267
  if (!result) {
3008
3268
  return {
3009
- content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path13 }, null, 2) }]
3269
+ content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path14 }, null, 2) }]
3010
3270
  };
3011
3271
  }
3012
3272
  return {
3013
3273
  content: [{ type: "text", text: JSON.stringify({
3014
- path: path13,
3274
+ path: path14,
3015
3275
  heading_count: result.length,
3016
3276
  headings: result
3017
3277
  }, null, 2) }]
@@ -3029,15 +3289,15 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
3029
3289
  include_subheadings: z6.boolean().default(true).describe("Include content under subheadings")
3030
3290
  }
3031
3291
  },
3032
- async ({ path: path13, heading, include_subheadings }) => {
3292
+ async ({ path: path14, heading, include_subheadings }) => {
3033
3293
  const index = getIndex();
3034
3294
  const vaultPath2 = getVaultPath();
3035
- const result = await getSectionContent(index, path13, heading, vaultPath2, include_subheadings);
3295
+ const result = await getSectionContent(index, path14, heading, vaultPath2, include_subheadings);
3036
3296
  if (!result) {
3037
3297
  return {
3038
3298
  content: [{ type: "text", text: JSON.stringify({
3039
3299
  error: "Section not found",
3040
- path: path13,
3300
+ path: path14,
3041
3301
  heading
3042
3302
  }, null, 2) }]
3043
3303
  };
@@ -3114,19 +3374,19 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
3114
3374
  path: z6.string().describe("Path to the note")
3115
3375
  }
3116
3376
  },
3117
- async ({ path: path13 }) => {
3377
+ async ({ path: path14 }) => {
3118
3378
  const index = getIndex();
3119
3379
  const vaultPath2 = getVaultPath();
3120
3380
  const config = getConfig();
3121
- const result = await getTasksFromNote(index, path13, vaultPath2, config.exclude_task_tags || []);
3381
+ const result = await getTasksFromNote(index, path14, vaultPath2, config.exclude_task_tags || []);
3122
3382
  if (!result) {
3123
3383
  return {
3124
- content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path13 }, null, 2) }]
3384
+ content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path14 }, null, 2) }]
3125
3385
  };
3126
3386
  }
3127
3387
  return {
3128
3388
  content: [{ type: "text", text: JSON.stringify({
3129
- path: path13,
3389
+ path: path14,
3130
3390
  task_count: result.length,
3131
3391
  open: result.filter((t) => t.status === "open").length,
3132
3392
  completed: result.filter((t) => t.status === "completed").length,
@@ -3258,14 +3518,14 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
3258
3518
  offset: z6.number().default(0).describe("Number of results to skip (for pagination)")
3259
3519
  }
3260
3520
  },
3261
- async ({ path: path13, limit: requestedLimit, offset }) => {
3521
+ async ({ path: path14, limit: requestedLimit, offset }) => {
3262
3522
  const limit = Math.min(requestedLimit ?? 50, MAX_LIMIT);
3263
3523
  const index = getIndex();
3264
- const allResults = findBidirectionalLinks(index, path13);
3524
+ const allResults = findBidirectionalLinks(index, path14);
3265
3525
  const result = allResults.slice(offset, offset + limit);
3266
3526
  return {
3267
3527
  content: [{ type: "text", text: JSON.stringify({
3268
- scope: path13 || "all",
3528
+ scope: path14 || "all",
3269
3529
  total_count: allResults.length,
3270
3530
  returned_count: result.length,
3271
3531
  pairs: result
@@ -3672,16 +3932,16 @@ function registerPeriodicTools(server2, getIndex) {
3672
3932
 
3673
3933
  // src/tools/bidirectional.ts
3674
3934
  import { z as z8 } from "zod";
3675
- import * as fs9 from "fs/promises";
3676
- import * as path7 from "path";
3935
+ import * as fs10 from "fs/promises";
3936
+ import * as path8 from "path";
3677
3937
  import matter2 from "gray-matter";
3678
3938
  var PROSE_PATTERN_REGEX = /^([A-Za-z][A-Za-z0-9 _-]*):\s*(?:\[\[([^\]]+)\]\]|"([^"]+)"|([^\n]+?))\s*$/gm;
3679
3939
  var CODE_BLOCK_REGEX2 = /```[\s\S]*?```|`[^`\n]+`/g;
3680
3940
  var WIKILINK_REGEX2 = /\[\[([^\]|#]+)(?:#[^\]|]*)?(?:\|[^\]]+)?\]\]/g;
3681
3941
  async function readFileContent(notePath, vaultPath2) {
3682
- const fullPath = path7.join(vaultPath2, notePath);
3942
+ const fullPath = path8.join(vaultPath2, notePath);
3683
3943
  try {
3684
- return await fs9.readFile(fullPath, "utf-8");
3944
+ return await fs10.readFile(fullPath, "utf-8");
3685
3945
  } catch {
3686
3946
  return null;
3687
3947
  }
@@ -4414,21 +4674,21 @@ function registerSchemaTools(server2, getIndex, getVaultPath) {
4414
4674
 
4415
4675
  // src/tools/computed.ts
4416
4676
  import { z as z10 } from "zod";
4417
- import * as fs10 from "fs/promises";
4418
- import * as path8 from "path";
4677
+ import * as fs11 from "fs/promises";
4678
+ import * as path9 from "path";
4419
4679
  import matter3 from "gray-matter";
4420
4680
  async function readFileContent2(notePath, vaultPath2) {
4421
- const fullPath = path8.join(vaultPath2, notePath);
4681
+ const fullPath = path9.join(vaultPath2, notePath);
4422
4682
  try {
4423
- return await fs10.readFile(fullPath, "utf-8");
4683
+ return await fs11.readFile(fullPath, "utf-8");
4424
4684
  } catch {
4425
4685
  return null;
4426
4686
  }
4427
4687
  }
4428
4688
  async function getFileStats(notePath, vaultPath2) {
4429
- const fullPath = path8.join(vaultPath2, notePath);
4689
+ const fullPath = path9.join(vaultPath2, notePath);
4430
4690
  try {
4431
- const stats = await fs10.stat(fullPath);
4691
+ const stats = await fs11.stat(fullPath);
4432
4692
  return {
4433
4693
  modified: stats.mtime,
4434
4694
  created: stats.birthtime
@@ -4584,8 +4844,8 @@ function registerComputedTools(server2, getIndex, getVaultPath) {
4584
4844
 
4585
4845
  // src/tools/migrations.ts
4586
4846
  import { z as z11 } from "zod";
4587
- import * as fs11 from "fs/promises";
4588
- import * as path9 from "path";
4847
+ import * as fs12 from "fs/promises";
4848
+ import * as path10 from "path";
4589
4849
  import matter4 from "gray-matter";
4590
4850
  function getNotesInFolder2(index, folder) {
4591
4851
  const notes = [];
@@ -4598,17 +4858,17 @@ function getNotesInFolder2(index, folder) {
4598
4858
  return notes;
4599
4859
  }
4600
4860
  async function readFileContent3(notePath, vaultPath2) {
4601
- const fullPath = path9.join(vaultPath2, notePath);
4861
+ const fullPath = path10.join(vaultPath2, notePath);
4602
4862
  try {
4603
- return await fs11.readFile(fullPath, "utf-8");
4863
+ return await fs12.readFile(fullPath, "utf-8");
4604
4864
  } catch {
4605
4865
  return null;
4606
4866
  }
4607
4867
  }
4608
4868
  async function writeFileContent(notePath, vaultPath2, content) {
4609
- const fullPath = path9.join(vaultPath2, notePath);
4869
+ const fullPath = path10.join(vaultPath2, notePath);
4610
4870
  try {
4611
- await fs11.writeFile(fullPath, content, "utf-8");
4871
+ await fs12.writeFile(fullPath, content, "utf-8");
4612
4872
  return true;
4613
4873
  } catch {
4614
4874
  return false;
@@ -4786,19 +5046,19 @@ function registerMigrationTools(server2, getIndex, getVaultPath) {
4786
5046
  }
4787
5047
 
4788
5048
  // src/core/vaultRoot.ts
4789
- import * as fs12 from "fs";
4790
- import * as path10 from "path";
5049
+ import * as fs13 from "fs";
5050
+ import * as path11 from "path";
4791
5051
  var VAULT_MARKERS = [".obsidian", ".claude"];
4792
5052
  function findVaultRoot(startPath) {
4793
- let current = path10.resolve(startPath || process.cwd());
5053
+ let current = path11.resolve(startPath || process.cwd());
4794
5054
  while (true) {
4795
5055
  for (const marker of VAULT_MARKERS) {
4796
- const markerPath = path10.join(current, marker);
4797
- if (fs12.existsSync(markerPath) && fs12.statSync(markerPath).isDirectory()) {
5056
+ const markerPath = path11.join(current, marker);
5057
+ if (fs13.existsSync(markerPath) && fs13.statSync(markerPath).isDirectory()) {
4798
5058
  return current;
4799
5059
  }
4800
5060
  }
4801
- const parent = path10.dirname(current);
5061
+ const parent = path11.dirname(current);
4802
5062
  if (parent === current) {
4803
5063
  return startPath || process.cwd();
4804
5064
  }
@@ -4810,7 +5070,7 @@ function findVaultRoot(startPath) {
4810
5070
  import chokidar from "chokidar";
4811
5071
 
4812
5072
  // src/core/watch/pathFilter.ts
4813
- import path11 from "path";
5073
+ import path12 from "path";
4814
5074
  var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
4815
5075
  ".git",
4816
5076
  ".obsidian",
@@ -4908,7 +5168,7 @@ function isIgnoredDirectory(segment) {
4908
5168
  return IGNORED_DIRECTORIES.has(segment);
4909
5169
  }
4910
5170
  function hasIgnoredExtension(filePath) {
4911
- const ext = path11.extname(filePath).toLowerCase();
5171
+ const ext = path12.extname(filePath).toLowerCase();
4912
5172
  return IGNORED_EXTENSIONS.has(ext);
4913
5173
  }
4914
5174
  function matchesIgnoredPattern(filename) {
@@ -4922,7 +5182,7 @@ function normalizePath(filePath) {
4922
5182
  return normalized;
4923
5183
  }
4924
5184
  function getRelativePath(vaultPath2, filePath) {
4925
- const relative = path11.relative(vaultPath2, filePath);
5185
+ const relative = path12.relative(vaultPath2, filePath);
4926
5186
  return normalizePath(relative);
4927
5187
  }
4928
5188
  function shouldWatch(filePath, vaultPath2) {
@@ -5006,30 +5266,30 @@ var EventQueue = class {
5006
5266
  * Add a new event to the queue
5007
5267
  */
5008
5268
  push(type, rawPath) {
5009
- const path13 = normalizePath(rawPath);
5269
+ const path14 = normalizePath(rawPath);
5010
5270
  const now = Date.now();
5011
5271
  const event = {
5012
5272
  type,
5013
- path: path13,
5273
+ path: path14,
5014
5274
  timestamp: now
5015
5275
  };
5016
- let pending = this.pending.get(path13);
5276
+ let pending = this.pending.get(path14);
5017
5277
  if (!pending) {
5018
5278
  pending = {
5019
5279
  events: [],
5020
5280
  timer: null,
5021
5281
  lastEvent: now
5022
5282
  };
5023
- this.pending.set(path13, pending);
5283
+ this.pending.set(path14, pending);
5024
5284
  }
5025
5285
  pending.events.push(event);
5026
5286
  pending.lastEvent = now;
5027
- console.error(`[flywheel] QUEUE: pushed ${type} for ${path13}, pending=${this.pending.size}`);
5287
+ console.error(`[flywheel] QUEUE: pushed ${type} for ${path14}, pending=${this.pending.size}`);
5028
5288
  if (pending.timer) {
5029
5289
  clearTimeout(pending.timer);
5030
5290
  }
5031
5291
  pending.timer = setTimeout(() => {
5032
- this.flushPath(path13);
5292
+ this.flushPath(path14);
5033
5293
  }, this.config.debounceMs);
5034
5294
  if (this.pending.size >= this.config.batchSize) {
5035
5295
  this.flush();
@@ -5050,10 +5310,10 @@ var EventQueue = class {
5050
5310
  /**
5051
5311
  * Flush a single path's events
5052
5312
  */
5053
- flushPath(path13) {
5054
- const pending = this.pending.get(path13);
5313
+ flushPath(path14) {
5314
+ const pending = this.pending.get(path14);
5055
5315
  if (!pending || pending.events.length === 0) return;
5056
- console.error(`[flywheel] QUEUE: flushing ${path13}, events=${pending.events.length}`);
5316
+ console.error(`[flywheel] QUEUE: flushing ${path14}, events=${pending.events.length}`);
5057
5317
  if (pending.timer) {
5058
5318
  clearTimeout(pending.timer);
5059
5319
  pending.timer = null;
@@ -5062,7 +5322,7 @@ var EventQueue = class {
5062
5322
  if (coalescedType) {
5063
5323
  const coalesced = {
5064
5324
  type: coalescedType,
5065
- path: path13,
5325
+ path: path14,
5066
5326
  originalEvents: [...pending.events]
5067
5327
  };
5068
5328
  this.onBatch({
@@ -5070,7 +5330,7 @@ var EventQueue = class {
5070
5330
  timestamp: Date.now()
5071
5331
  });
5072
5332
  }
5073
- this.pending.delete(path13);
5333
+ this.pending.delete(path14);
5074
5334
  }
5075
5335
  /**
5076
5336
  * Flush all pending events
@@ -5082,7 +5342,7 @@ var EventQueue = class {
5082
5342
  }
5083
5343
  if (this.pending.size === 0) return;
5084
5344
  const events = [];
5085
- for (const [path13, pending] of this.pending) {
5345
+ for (const [path14, pending] of this.pending) {
5086
5346
  if (pending.timer) {
5087
5347
  clearTimeout(pending.timer);
5088
5348
  }
@@ -5090,7 +5350,7 @@ var EventQueue = class {
5090
5350
  if (coalescedType) {
5091
5351
  events.push({
5092
5352
  type: coalescedType,
5093
- path: path13,
5353
+ path: path14,
5094
5354
  originalEvents: [...pending.events]
5095
5355
  });
5096
5356
  }
@@ -5173,20 +5433,20 @@ function createVaultWatcher(options) {
5173
5433
  ...parseWatcherConfig(),
5174
5434
  ...options.config
5175
5435
  };
5176
- let state = "starting";
5436
+ let state2 = "starting";
5177
5437
  let lastRebuild = null;
5178
5438
  let error = null;
5179
5439
  let watcher = null;
5180
5440
  let processingBatch = false;
5181
5441
  let pendingBatches = [];
5182
5442
  const getStatus = () => ({
5183
- state,
5443
+ state: state2,
5184
5444
  pendingEvents: eventQueue?.eventCount || 0,
5185
5445
  lastRebuild,
5186
5446
  error
5187
5447
  });
5188
5448
  const setState = (newState, newError = null) => {
5189
- state = newState;
5449
+ state2 = newState;
5190
5450
  error = newError;
5191
5451
  onStateChange?.(getStatus());
5192
5452
  };
@@ -5239,31 +5499,31 @@ function createVaultWatcher(options) {
5239
5499
  usePolling: config.usePolling,
5240
5500
  interval: config.usePolling ? config.pollInterval : void 0
5241
5501
  });
5242
- watcher.on("add", (path13) => {
5243
- console.error(`[flywheel] RAW EVENT: add ${path13}`);
5244
- if (shouldWatch(path13, vaultPath2)) {
5245
- console.error(`[flywheel] ACCEPTED: add ${path13}`);
5246
- eventQueue.push("add", path13);
5502
+ watcher.on("add", (path14) => {
5503
+ console.error(`[flywheel] RAW EVENT: add ${path14}`);
5504
+ if (shouldWatch(path14, vaultPath2)) {
5505
+ console.error(`[flywheel] ACCEPTED: add ${path14}`);
5506
+ eventQueue.push("add", path14);
5247
5507
  } else {
5248
- console.error(`[flywheel] FILTERED: add ${path13}`);
5508
+ console.error(`[flywheel] FILTERED: add ${path14}`);
5249
5509
  }
5250
5510
  });
5251
- watcher.on("change", (path13) => {
5252
- console.error(`[flywheel] RAW EVENT: change ${path13}`);
5253
- if (shouldWatch(path13, vaultPath2)) {
5254
- console.error(`[flywheel] ACCEPTED: change ${path13}`);
5255
- eventQueue.push("change", path13);
5511
+ watcher.on("change", (path14) => {
5512
+ console.error(`[flywheel] RAW EVENT: change ${path14}`);
5513
+ if (shouldWatch(path14, vaultPath2)) {
5514
+ console.error(`[flywheel] ACCEPTED: change ${path14}`);
5515
+ eventQueue.push("change", path14);
5256
5516
  } else {
5257
- console.error(`[flywheel] FILTERED: change ${path13}`);
5517
+ console.error(`[flywheel] FILTERED: change ${path14}`);
5258
5518
  }
5259
5519
  });
5260
- watcher.on("unlink", (path13) => {
5261
- console.error(`[flywheel] RAW EVENT: unlink ${path13}`);
5262
- if (shouldWatch(path13, vaultPath2)) {
5263
- console.error(`[flywheel] ACCEPTED: unlink ${path13}`);
5264
- eventQueue.push("unlink", path13);
5520
+ watcher.on("unlink", (path14) => {
5521
+ console.error(`[flywheel] RAW EVENT: unlink ${path14}`);
5522
+ if (shouldWatch(path14, vaultPath2)) {
5523
+ console.error(`[flywheel] ACCEPTED: unlink ${path14}`);
5524
+ eventQueue.push("unlink", path14);
5265
5525
  } else {
5266
- console.error(`[flywheel] FILTERED: unlink ${path13}`);
5526
+ console.error(`[flywheel] FILTERED: unlink ${path14}`);
5267
5527
  }
5268
5528
  });
5269
5529
  watcher.on("ready", () => {
@@ -5295,8 +5555,8 @@ function createVaultWatcher(options) {
5295
5555
  }
5296
5556
 
5297
5557
  // src/core/hubExport.ts
5298
- import fs13 from "fs/promises";
5299
- import path12 from "path";
5558
+ import fs14 from "fs/promises";
5559
+ import path13 from "path";
5300
5560
  function computeHubScores(index) {
5301
5561
  const hubScores = /* @__PURE__ */ new Map();
5302
5562
  for (const note of index.notes.values()) {
@@ -5351,16 +5611,16 @@ function enrichEntityIndex(index, hubScores) {
5351
5611
  return enriched;
5352
5612
  }
5353
5613
  async function exportHubScores(vaultPath2, vaultIndex2) {
5354
- const cachePath = path12.join(vaultPath2, ".claude", "wikilink-entities.json");
5614
+ const cachePath = path13.join(vaultPath2, ".claude", "wikilink-entities.json");
5355
5615
  try {
5356
- await fs13.access(cachePath);
5616
+ await fs14.access(cachePath);
5357
5617
  } catch {
5358
5618
  console.error("[Flywheel] Entity cache not found, skipping hub score export");
5359
5619
  return -1;
5360
5620
  }
5361
5621
  let entityIndex;
5362
5622
  try {
5363
- const content = await fs13.readFile(cachePath, "utf-8");
5623
+ const content = await fs14.readFile(cachePath, "utf-8");
5364
5624
  entityIndex = JSON.parse(content);
5365
5625
  } catch (e) {
5366
5626
  console.error("[Flywheel] Failed to load entity cache:", e);
@@ -5388,7 +5648,7 @@ async function exportHubScores(vaultPath2, vaultIndex2) {
5388
5648
  }
5389
5649
  }
5390
5650
  try {
5391
- await fs13.writeFile(cachePath, JSON.stringify(enriched, null, 2), "utf-8");
5651
+ await fs14.writeFile(cachePath, JSON.stringify(enriched, null, 2), "utf-8");
5392
5652
  console.error(`[Flywheel] Exported hub scores: ${hubCount} entities with backlinks`);
5393
5653
  return hubCount;
5394
5654
  } catch (e) {
@@ -5567,8 +5827,8 @@ async function main() {
5567
5827
  }
5568
5828
  });
5569
5829
  let rebuildTimer;
5570
- legacyWatcher.on("all", (event, path13) => {
5571
- if (!path13.endsWith(".md")) return;
5830
+ legacyWatcher.on("all", (event, path14) => {
5831
+ if (!path14.endsWith(".md")) return;
5572
5832
  clearTimeout(rebuildTimer);
5573
5833
  rebuildTimer = setTimeout(() => {
5574
5834
  console.error("[flywheel] Rebuilding index (file changed)");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velvetmonkey/flywheel-mcp",
3
- "version": "1.27.18",
3
+ "version": "1.27.20",
4
4
  "description": "Graph intelligence for markdown vaults. MCP server for Obsidian.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -24,11 +24,13 @@
24
24
  ],
25
25
  "dependencies": {
26
26
  "@modelcontextprotocol/sdk": "^1.25.1",
27
+ "better-sqlite3": "^11.0.0",
27
28
  "chokidar": "^4.0.0",
28
29
  "gray-matter": "^4.0.3",
29
30
  "zod": "^3.22.4"
30
31
  },
31
32
  "devDependencies": {
33
+ "@types/better-sqlite3": "^7.6.0",
32
34
  "@types/node": "^20.0.0",
33
35
  "@vitest/coverage-v8": "^2.0.0",
34
36
  "esbuild": "^0.24.0",