@velvetmonkey/flywheel-mcp 1.27.19 → 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.
- package/dist/index.js +401 -141
- 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(
|
|
252
|
-
indexState =
|
|
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(
|
|
264
|
-
return
|
|
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,
|
|
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:
|
|
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
|
|
487
|
-
if (
|
|
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 (
|
|
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,
|
|
892
|
+
for (const [name, path14] of entities) {
|
|
893
893
|
if (name.startsWith(targetLower) || targetLower.startsWith(name)) {
|
|
894
|
-
return
|
|
894
|
+
return path14;
|
|
895
895
|
}
|
|
896
896
|
}
|
|
897
|
-
for (const [name,
|
|
897
|
+
for (const [name, path14] of entities) {
|
|
898
898
|
if (name.includes(targetLower) || targetLower.includes(name)) {
|
|
899
|
-
return
|
|
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(
|
|
1197
|
-
const filename =
|
|
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 =
|
|
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
|
|
1487
|
-
import * as
|
|
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
|
|
1492
|
-
import * as
|
|
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 =
|
|
1498
|
-
const configPath =
|
|
1757
|
+
const claudeDir = path4.join(vaultPath2, ".claude");
|
|
1758
|
+
const configPath = path4.join(claudeDir, ".flywheel.json");
|
|
1499
1759
|
try {
|
|
1500
|
-
if (
|
|
1501
|
-
const content =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1586
|
-
const configPath =
|
|
1845
|
+
const claudeDir = path4.join(vaultPath2, ".claude");
|
|
1846
|
+
const configPath = path4.join(claudeDir, ".flywheel.json");
|
|
1587
1847
|
try {
|
|
1588
|
-
if (!
|
|
1589
|
-
|
|
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
|
-
|
|
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 =
|
|
1853
|
-
const content = await
|
|
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 =
|
|
1969
|
-
const content = await
|
|
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,
|
|
2135
|
-
const targetNote = index.notes.get(
|
|
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 ===
|
|
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
|
|
2192
|
-
import * as
|
|
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 =
|
|
2506
|
+
const absolutePath = path6.join(vaultPath2, notePath);
|
|
2247
2507
|
let content;
|
|
2248
2508
|
try {
|
|
2249
|
-
content = await
|
|
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 =
|
|
2529
|
+
const absolutePath = path6.join(vaultPath2, notePath);
|
|
2270
2530
|
let content;
|
|
2271
2531
|
try {
|
|
2272
|
-
content = await
|
|
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 =
|
|
2541
|
+
const absolutePath = path6.join(vaultPath2, notePath);
|
|
2282
2542
|
let content;
|
|
2283
2543
|
try {
|
|
2284
|
-
content = await
|
|
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 =
|
|
2583
|
+
const absolutePath = path6.join(vaultPath2, note.path);
|
|
2324
2584
|
let content;
|
|
2325
2585
|
try {
|
|
2326
|
-
content = await
|
|
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
|
|
2347
|
-
import * as
|
|
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
|
|
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 =
|
|
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 =
|
|
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:
|
|
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,
|
|
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:
|
|
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:
|
|
3240
|
+
async ({ path: path14 }) => {
|
|
2981
3241
|
const index = getIndex();
|
|
2982
3242
|
const vaultPath2 = getVaultPath();
|
|
2983
|
-
const result = await getNoteStructure(index,
|
|
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:
|
|
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:
|
|
3263
|
+
async ({ path: path14 }) => {
|
|
3004
3264
|
const index = getIndex();
|
|
3005
3265
|
const vaultPath2 = getVaultPath();
|
|
3006
|
-
const result = await getHeadings(index,
|
|
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:
|
|
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:
|
|
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:
|
|
3292
|
+
async ({ path: path14, heading, include_subheadings }) => {
|
|
3033
3293
|
const index = getIndex();
|
|
3034
3294
|
const vaultPath2 = getVaultPath();
|
|
3035
|
-
const result = await getSectionContent(index,
|
|
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:
|
|
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:
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
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:
|
|
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
|
|
3676
|
-
import * as
|
|
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 =
|
|
3942
|
+
const fullPath = path8.join(vaultPath2, notePath);
|
|
3683
3943
|
try {
|
|
3684
|
-
return await
|
|
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
|
|
4418
|
-
import * as
|
|
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 =
|
|
4681
|
+
const fullPath = path9.join(vaultPath2, notePath);
|
|
4422
4682
|
try {
|
|
4423
|
-
return await
|
|
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 =
|
|
4689
|
+
const fullPath = path9.join(vaultPath2, notePath);
|
|
4430
4690
|
try {
|
|
4431
|
-
const stats = await
|
|
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
|
|
4588
|
-
import * as
|
|
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 =
|
|
4861
|
+
const fullPath = path10.join(vaultPath2, notePath);
|
|
4602
4862
|
try {
|
|
4603
|
-
return await
|
|
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 =
|
|
4869
|
+
const fullPath = path10.join(vaultPath2, notePath);
|
|
4610
4870
|
try {
|
|
4611
|
-
await
|
|
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
|
|
4790
|
-
import * as
|
|
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 =
|
|
5053
|
+
let current = path11.resolve(startPath || process.cwd());
|
|
4794
5054
|
while (true) {
|
|
4795
5055
|
for (const marker of VAULT_MARKERS) {
|
|
4796
|
-
const markerPath =
|
|
4797
|
-
if (
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
5269
|
+
const path14 = normalizePath(rawPath);
|
|
5010
5270
|
const now = Date.now();
|
|
5011
5271
|
const event = {
|
|
5012
5272
|
type,
|
|
5013
|
-
path:
|
|
5273
|
+
path: path14,
|
|
5014
5274
|
timestamp: now
|
|
5015
5275
|
};
|
|
5016
|
-
let pending = this.pending.get(
|
|
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(
|
|
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 ${
|
|
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(
|
|
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(
|
|
5054
|
-
const pending = this.pending.get(
|
|
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 ${
|
|
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:
|
|
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(
|
|
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 [
|
|
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:
|
|
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
|
|
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
|
-
|
|
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", (
|
|
5243
|
-
console.error(`[flywheel] RAW EVENT: add ${
|
|
5244
|
-
if (shouldWatch(
|
|
5245
|
-
console.error(`[flywheel] ACCEPTED: add ${
|
|
5246
|
-
eventQueue.push("add",
|
|
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 ${
|
|
5508
|
+
console.error(`[flywheel] FILTERED: add ${path14}`);
|
|
5249
5509
|
}
|
|
5250
5510
|
});
|
|
5251
|
-
watcher.on("change", (
|
|
5252
|
-
console.error(`[flywheel] RAW EVENT: change ${
|
|
5253
|
-
if (shouldWatch(
|
|
5254
|
-
console.error(`[flywheel] ACCEPTED: change ${
|
|
5255
|
-
eventQueue.push("change",
|
|
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 ${
|
|
5517
|
+
console.error(`[flywheel] FILTERED: change ${path14}`);
|
|
5258
5518
|
}
|
|
5259
5519
|
});
|
|
5260
|
-
watcher.on("unlink", (
|
|
5261
|
-
console.error(`[flywheel] RAW EVENT: unlink ${
|
|
5262
|
-
if (shouldWatch(
|
|
5263
|
-
console.error(`[flywheel] ACCEPTED: unlink ${
|
|
5264
|
-
eventQueue.push("unlink",
|
|
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 ${
|
|
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
|
|
5299
|
-
import
|
|
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 =
|
|
5614
|
+
const cachePath = path13.join(vaultPath2, ".claude", "wikilink-entities.json");
|
|
5355
5615
|
try {
|
|
5356
|
-
await
|
|
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
|
|
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
|
|
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,
|
|
5571
|
-
if (!
|
|
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.
|
|
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",
|