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