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