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