@velvetmonkey/flywheel-memory 2.0.77 → 2.0.79
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 +314 -258
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -121,7 +121,7 @@ function isEmptyPlaceholder(line) {
|
|
|
121
121
|
const trimmed = line.trim();
|
|
122
122
|
return EMPTY_PLACEHOLDER_PATTERNS.some((p) => p.test(trimmed));
|
|
123
123
|
}
|
|
124
|
-
function
|
|
124
|
+
function extractHeadings3(content) {
|
|
125
125
|
const lines = content.split("\n");
|
|
126
126
|
const headings = [];
|
|
127
127
|
let inCodeBlock = false;
|
|
@@ -144,7 +144,7 @@ function extractHeadings2(content) {
|
|
|
144
144
|
return headings;
|
|
145
145
|
}
|
|
146
146
|
function findSection(content, sectionName) {
|
|
147
|
-
const headings =
|
|
147
|
+
const headings = extractHeadings3(content);
|
|
148
148
|
const lines = content.split("\n");
|
|
149
149
|
const normalizedSearch = sectionName.replace(/^#+\s*/, "").trim().toLowerCase();
|
|
150
150
|
const headingIndex = headings.findIndex(
|
|
@@ -1707,6 +1707,14 @@ function extractWikilinks(content) {
|
|
|
1707
1707
|
}
|
|
1708
1708
|
return links;
|
|
1709
1709
|
}
|
|
1710
|
+
function extractHeadings(markdown) {
|
|
1711
|
+
const headings = [];
|
|
1712
|
+
for (const line of markdown.split("\n")) {
|
|
1713
|
+
const match = line.match(/^(#{1,6})\s+(.+)/);
|
|
1714
|
+
if (match) headings.push(match[2].trim());
|
|
1715
|
+
}
|
|
1716
|
+
return headings;
|
|
1717
|
+
}
|
|
1710
1718
|
function extractTags(content, frontmatter) {
|
|
1711
1719
|
const tags = /* @__PURE__ */ new Set();
|
|
1712
1720
|
const fmTags = frontmatter.tags;
|
|
@@ -1808,6 +1816,7 @@ async function parseNoteWithWarnings(file) {
|
|
|
1808
1816
|
frontmatter,
|
|
1809
1817
|
outlinks: extractWikilinks(markdown),
|
|
1810
1818
|
tags: extractTags(markdown, frontmatter),
|
|
1819
|
+
headings: extractHeadings(markdown),
|
|
1811
1820
|
modified: file.modified,
|
|
1812
1821
|
created
|
|
1813
1822
|
},
|
|
@@ -7213,7 +7222,7 @@ function searchFTS5(_vaultPath, query, limit = 10) {
|
|
|
7213
7222
|
SELECT
|
|
7214
7223
|
path,
|
|
7215
7224
|
title,
|
|
7216
|
-
snippet(notes_fts, 3, '<mark>', '</mark>', '...',
|
|
7225
|
+
snippet(notes_fts, 3, '<mark>', '</mark>', '...', 64) as snippet
|
|
7217
7226
|
FROM notes_fts
|
|
7218
7227
|
WHERE notes_fts MATCH ?
|
|
7219
7228
|
ORDER BY bm25(notes_fts, 0.0, 5.0, 10.0, 1.0)
|
|
@@ -7231,6 +7240,19 @@ function searchFTS5(_vaultPath, query, limit = 10) {
|
|
|
7231
7240
|
function getFTS5State() {
|
|
7232
7241
|
return { ...state };
|
|
7233
7242
|
}
|
|
7243
|
+
function getContentPreview(notePath, maxChars = 300) {
|
|
7244
|
+
if (!db2) return null;
|
|
7245
|
+
try {
|
|
7246
|
+
const row = db2.prepare(
|
|
7247
|
+
"SELECT substr(content, 1, ?) as preview FROM notes_fts WHERE path = ?"
|
|
7248
|
+
).get(maxChars + 50, notePath);
|
|
7249
|
+
if (!row?.preview) return null;
|
|
7250
|
+
const truncated = row.preview.length > maxChars ? row.preview.slice(0, maxChars).replace(/\s\S*$/, "") + "..." : row.preview;
|
|
7251
|
+
return truncated;
|
|
7252
|
+
} catch {
|
|
7253
|
+
return null;
|
|
7254
|
+
}
|
|
7255
|
+
}
|
|
7234
7256
|
function countFTS5Mentions(term) {
|
|
7235
7257
|
if (!db2) return 0;
|
|
7236
7258
|
try {
|
|
@@ -9592,7 +9614,8 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
9592
9614
|
import { z as z4 } from "zod";
|
|
9593
9615
|
import {
|
|
9594
9616
|
searchEntities,
|
|
9595
|
-
searchEntitiesPrefix
|
|
9617
|
+
searchEntitiesPrefix,
|
|
9618
|
+
getEntityByName as getEntityByName2
|
|
9596
9619
|
} from "@velvetmonkey/vault-core";
|
|
9597
9620
|
function matchesFrontmatter(note, where) {
|
|
9598
9621
|
for (const [key, value] of Object.entries(where)) {
|
|
@@ -9640,6 +9663,50 @@ function inFolder(note, folder) {
|
|
|
9640
9663
|
const normalizedFolder = folder.endsWith("/") ? folder : folder + "/";
|
|
9641
9664
|
return note.path.startsWith(normalizedFolder) || note.path.split("/")[0] === folder.replace("/", "");
|
|
9642
9665
|
}
|
|
9666
|
+
function enrichResult(result, index, stateDb2) {
|
|
9667
|
+
const note = index.notes.get(result.path);
|
|
9668
|
+
const normalizedPath = result.path.toLowerCase().replace(/\.md$/, "");
|
|
9669
|
+
const backlinks = index.backlinks.get(normalizedPath) || [];
|
|
9670
|
+
const enriched = {
|
|
9671
|
+
path: result.path,
|
|
9672
|
+
title: result.title
|
|
9673
|
+
};
|
|
9674
|
+
if (result.snippet) enriched.snippet = result.snippet;
|
|
9675
|
+
if (note) {
|
|
9676
|
+
enriched.frontmatter = note.frontmatter;
|
|
9677
|
+
enriched.tags = note.tags;
|
|
9678
|
+
enriched.aliases = note.aliases;
|
|
9679
|
+
enriched.backlink_count = backlinks.length;
|
|
9680
|
+
enriched.backlinks = backlinks.map((bl) => ({ source: bl.source, line: bl.line }));
|
|
9681
|
+
enriched.outlink_count = note.outlinks.length;
|
|
9682
|
+
enriched.outlinks = note.outlinks.map((l) => {
|
|
9683
|
+
const targetLower = l.target.toLowerCase();
|
|
9684
|
+
const exists = index.entities.has(targetLower);
|
|
9685
|
+
const out = { target: l.target, line: l.line, exists };
|
|
9686
|
+
if (l.alias) out.alias = l.alias;
|
|
9687
|
+
return out;
|
|
9688
|
+
});
|
|
9689
|
+
enriched.headings = note.headings || [];
|
|
9690
|
+
enriched.modified = note.modified.toISOString();
|
|
9691
|
+
if (note.created) enriched.created = note.created.toISOString();
|
|
9692
|
+
}
|
|
9693
|
+
if (stateDb2) {
|
|
9694
|
+
try {
|
|
9695
|
+
const entity = getEntityByName2(stateDb2, result.title);
|
|
9696
|
+
if (entity) {
|
|
9697
|
+
enriched.category = entity.category;
|
|
9698
|
+
enriched.hub_score = entity.hubScore;
|
|
9699
|
+
if (entity.description) enriched.description = entity.description;
|
|
9700
|
+
}
|
|
9701
|
+
} catch {
|
|
9702
|
+
}
|
|
9703
|
+
}
|
|
9704
|
+
if (!result.snippet) {
|
|
9705
|
+
const preview = getContentPreview(result.path);
|
|
9706
|
+
if (preview) enriched.content_preview = preview;
|
|
9707
|
+
}
|
|
9708
|
+
return enriched;
|
|
9709
|
+
}
|
|
9643
9710
|
function sortNotes(notes, sortBy, order) {
|
|
9644
9711
|
const sorted = [...notes];
|
|
9645
9712
|
sorted.sort((a, b) => {
|
|
@@ -9664,7 +9731,7 @@ function sortNotes(notes, sortBy, order) {
|
|
|
9664
9731
|
function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
9665
9732
|
server2.tool(
|
|
9666
9733
|
"search",
|
|
9667
|
-
'Search the vault across metadata, content, and entities. Scope controls what to search: "metadata" for frontmatter/tags/folders, "content" for full-text search (FTS5), "entities" for people/projects/technologies, "all" (default) tries metadata then falls back to content search. When embeddings have been built (via init_semantic), content and all scopes automatically include embedding-based results via hybrid ranking.\n\nExample: search({ query: "quarterly review", scope: "content", limit: 5 })\nExample: search({ where: { type: "project", status: "active" }, scope: "metadata" })',
|
|
9734
|
+
'Search the vault \u2014 always try this before reading files. Returns frontmatter, backlinks (with lines), outlinks (with lines + exists), headings, content snippet or preview, entity metadata, and timestamps for every hit.\n\nSearch the vault across metadata, content, and entities. Scope controls what to search: "metadata" for frontmatter/tags/folders, "content" for full-text search (FTS5), "entities" for people/projects/technologies, "all" (default) tries metadata then falls back to content search. When embeddings have been built (via init_semantic), content and all scopes automatically include embedding-based results via hybrid ranking.\n\nExample: search({ query: "quarterly review", scope: "content", limit: 5 })\nExample: search({ where: { type: "project", status: "active" }, scope: "metadata" })',
|
|
9668
9735
|
{
|
|
9669
9736
|
query: z4.string().optional().describe('Search query text. Required for scope "content", "entities", "all". For "metadata" scope, use filters instead.'),
|
|
9670
9737
|
scope: z4.enum(["metadata", "content", "entities", "all"]).default("all").describe("What to search: metadata (frontmatter/tags/folders), content (FTS5 full-text), entities (people/projects), all (metadata then content). Semantic results are automatically included when embeddings have been built (via init_semantic)."),
|
|
@@ -9751,14 +9818,10 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
9751
9818
|
matchingNotes = sortNotes(matchingNotes, sort_by ?? "modified", order ?? "desc");
|
|
9752
9819
|
const totalMatches = matchingNotes.length;
|
|
9753
9820
|
const limitedNotes = matchingNotes.slice(0, limit);
|
|
9754
|
-
const
|
|
9755
|
-
|
|
9756
|
-
title: note.title,
|
|
9757
|
-
|
|
9758
|
-
created: note.created?.toISOString(),
|
|
9759
|
-
tags: note.tags,
|
|
9760
|
-
frontmatter: note.frontmatter
|
|
9761
|
-
}));
|
|
9821
|
+
const stateDb2 = getStateDb();
|
|
9822
|
+
const notes = limitedNotes.map(
|
|
9823
|
+
(note) => enrichResult({ path: note.path, title: note.title }, index, stateDb2)
|
|
9824
|
+
);
|
|
9762
9825
|
if (scope === "metadata" || hasMetadataFilters || totalMatches > 0) {
|
|
9763
9826
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
9764
9827
|
scope: "metadata",
|
|
@@ -9847,22 +9910,20 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
9847
9910
|
const fts5Map = new Map(fts5Results.map((r) => [normalizePath2(r.path), r]));
|
|
9848
9911
|
const semanticMap = new Map(semanticResults.map((r) => [normalizePath2(r.path), r]));
|
|
9849
9912
|
const entityMap = new Map(entityResults.map((r) => [normalizePath2(r.path), r]));
|
|
9850
|
-
const merged = Array.from(allPaths).map((p) =>
|
|
9851
|
-
|
|
9852
|
-
|
|
9853
|
-
|
|
9854
|
-
|
|
9855
|
-
|
|
9856
|
-
|
|
9857
|
-
|
|
9858
|
-
}));
|
|
9859
|
-
merged.sort((a, b) => b.rrf_score - a.rrf_score);
|
|
9913
|
+
const merged = Array.from(allPaths).map((p) => {
|
|
9914
|
+
const title = fts5Map.get(p)?.title || semanticMap.get(p)?.title || entityMap.get(p)?.name || p.replace(/\.md$/, "").split("/").pop() || p;
|
|
9915
|
+
const snippet = fts5Map.get(p)?.snippet;
|
|
9916
|
+
const rrfScore = rrfScores.get(p) || 0;
|
|
9917
|
+
return { rrfScore, ...enrichResult({ path: p, title, snippet }, index, getStateDb()) };
|
|
9918
|
+
});
|
|
9919
|
+
merged.sort((a, b) => b.rrfScore - a.rrfScore);
|
|
9920
|
+
const results = merged.slice(0, limit).map(({ rrfScore, ...rest }) => rest);
|
|
9860
9921
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
9861
9922
|
scope,
|
|
9862
9923
|
method: "hybrid",
|
|
9863
9924
|
query,
|
|
9864
9925
|
total_results: Math.min(merged.length, limit),
|
|
9865
|
-
results
|
|
9926
|
+
results
|
|
9866
9927
|
}, null, 2) }] };
|
|
9867
9928
|
} catch (err) {
|
|
9868
9929
|
console.error("[Semantic] Hybrid search failed, falling back to FTS5:", err instanceof Error ? err.message : err);
|
|
@@ -9871,11 +9932,10 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
9871
9932
|
if (entityResults.length > 0) {
|
|
9872
9933
|
const normalizePath2 = (p) => p.replace(/\\/g, "/").replace(/\/+/g, "/");
|
|
9873
9934
|
const fts5Map = new Map(fts5Results.map((r) => [normalizePath2(r.path), r]));
|
|
9874
|
-
const entityNormMap = new Map(entityResults.map((r) => [normalizePath2(r.path), r]));
|
|
9875
9935
|
const entityRanked = entityResults.filter((r) => !fts5Map.has(normalizePath2(r.path)));
|
|
9876
9936
|
const merged = [
|
|
9877
|
-
...fts5Results.map((r) => ({ path: r.path, title: r.title, snippet: r.snippet,
|
|
9878
|
-
...entityRanked.map((r) => ({ path: r.path, title: r.name,
|
|
9937
|
+
...fts5Results.map((r) => enrichResult({ path: r.path, title: r.title, snippet: r.snippet }, index, getStateDb())),
|
|
9938
|
+
...entityRanked.map((r) => enrichResult({ path: r.path, title: r.name }, index, getStateDb()))
|
|
9879
9939
|
];
|
|
9880
9940
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
9881
9941
|
scope: "content",
|
|
@@ -9885,12 +9945,13 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
9885
9945
|
results: merged.slice(0, limit)
|
|
9886
9946
|
}, null, 2) }] };
|
|
9887
9947
|
}
|
|
9948
|
+
const enrichedFts5 = fts5Results.map((r) => enrichResult({ path: r.path, title: r.title, snippet: r.snippet }, index, getStateDb()));
|
|
9888
9949
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
9889
9950
|
scope: "content",
|
|
9890
9951
|
method: "fts5",
|
|
9891
9952
|
query,
|
|
9892
|
-
total_results:
|
|
9893
|
-
results:
|
|
9953
|
+
total_results: enrichedFts5.length,
|
|
9954
|
+
results: enrichedFts5
|
|
9894
9955
|
}, null, 2) }] };
|
|
9895
9956
|
}
|
|
9896
9957
|
return { content: [{ type: "text", text: JSON.stringify({ error: "Invalid scope" }, null, 2) }] };
|
|
@@ -10260,104 +10321,6 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
10260
10321
|
};
|
|
10261
10322
|
}
|
|
10262
10323
|
);
|
|
10263
|
-
const GetNoteMetadataOutputSchema = {
|
|
10264
|
-
path: z5.string().describe("Path to the note"),
|
|
10265
|
-
title: z5.string().describe("Note title"),
|
|
10266
|
-
exists: z5.boolean().describe("Whether the note exists"),
|
|
10267
|
-
frontmatter: z5.record(z5.unknown()).describe("Frontmatter properties"),
|
|
10268
|
-
tags: z5.array(z5.string()).describe("Tags on this note"),
|
|
10269
|
-
aliases: z5.array(z5.string()).describe("Aliases for this note"),
|
|
10270
|
-
outlink_count: z5.number().describe("Number of outgoing links"),
|
|
10271
|
-
backlink_count: z5.number().describe("Number of incoming links"),
|
|
10272
|
-
word_count: z5.number().optional().describe("Approximate word count"),
|
|
10273
|
-
created: z5.string().optional().describe("Created date (ISO format)"),
|
|
10274
|
-
modified: z5.string().describe("Last modified date (ISO format)")
|
|
10275
|
-
};
|
|
10276
|
-
server2.registerTool(
|
|
10277
|
-
"get_note_metadata",
|
|
10278
|
-
{
|
|
10279
|
-
title: "Get Note Metadata",
|
|
10280
|
-
description: "Get metadata about a note (frontmatter, tags, link counts) without reading full content. Useful for quick analysis.",
|
|
10281
|
-
inputSchema: {
|
|
10282
|
-
path: z5.string().describe("Path to the note"),
|
|
10283
|
-
include_word_count: z5.boolean().default(false).describe("Count words (requires reading file)")
|
|
10284
|
-
},
|
|
10285
|
-
outputSchema: GetNoteMetadataOutputSchema
|
|
10286
|
-
},
|
|
10287
|
-
async ({
|
|
10288
|
-
path: notePath,
|
|
10289
|
-
include_word_count
|
|
10290
|
-
}) => {
|
|
10291
|
-
requireIndex();
|
|
10292
|
-
const index = getIndex();
|
|
10293
|
-
const vaultPath2 = getVaultPath();
|
|
10294
|
-
let resolvedPath = notePath;
|
|
10295
|
-
if (!notePath.endsWith(".md")) {
|
|
10296
|
-
const resolved = index.entities.get(notePath.toLowerCase());
|
|
10297
|
-
if (resolved) {
|
|
10298
|
-
resolvedPath = resolved;
|
|
10299
|
-
} else {
|
|
10300
|
-
resolvedPath = notePath + ".md";
|
|
10301
|
-
}
|
|
10302
|
-
}
|
|
10303
|
-
const note = index.notes.get(resolvedPath);
|
|
10304
|
-
if (!note) {
|
|
10305
|
-
const output2 = {
|
|
10306
|
-
path: resolvedPath,
|
|
10307
|
-
title: resolvedPath.replace(/\.md$/, "").split("/").pop() || "",
|
|
10308
|
-
exists: false,
|
|
10309
|
-
frontmatter: {},
|
|
10310
|
-
tags: [],
|
|
10311
|
-
aliases: [],
|
|
10312
|
-
outlink_count: 0,
|
|
10313
|
-
backlink_count: 0,
|
|
10314
|
-
modified: (/* @__PURE__ */ new Date()).toISOString()
|
|
10315
|
-
};
|
|
10316
|
-
return {
|
|
10317
|
-
content: [
|
|
10318
|
-
{
|
|
10319
|
-
type: "text",
|
|
10320
|
-
text: JSON.stringify(output2, null, 2)
|
|
10321
|
-
}
|
|
10322
|
-
],
|
|
10323
|
-
structuredContent: output2
|
|
10324
|
-
};
|
|
10325
|
-
}
|
|
10326
|
-
const normalizedPath = resolvedPath.toLowerCase().replace(/\.md$/, "");
|
|
10327
|
-
const backlinks = index.backlinks.get(normalizedPath) || [];
|
|
10328
|
-
let wordCount;
|
|
10329
|
-
if (include_word_count) {
|
|
10330
|
-
try {
|
|
10331
|
-
const fullPath = path14.join(vaultPath2, resolvedPath);
|
|
10332
|
-
const content = await fs11.promises.readFile(fullPath, "utf-8");
|
|
10333
|
-
wordCount = content.split(/\s+/).filter((w) => w.length > 0).length;
|
|
10334
|
-
} catch {
|
|
10335
|
-
}
|
|
10336
|
-
}
|
|
10337
|
-
const output = {
|
|
10338
|
-
path: note.path,
|
|
10339
|
-
title: note.title,
|
|
10340
|
-
exists: true,
|
|
10341
|
-
frontmatter: note.frontmatter,
|
|
10342
|
-
tags: note.tags,
|
|
10343
|
-
aliases: note.aliases,
|
|
10344
|
-
outlink_count: note.outlinks.length,
|
|
10345
|
-
backlink_count: backlinks.length,
|
|
10346
|
-
word_count: wordCount,
|
|
10347
|
-
created: note.created?.toISOString(),
|
|
10348
|
-
modified: note.modified.toISOString()
|
|
10349
|
-
};
|
|
10350
|
-
return {
|
|
10351
|
-
content: [
|
|
10352
|
-
{
|
|
10353
|
-
type: "text",
|
|
10354
|
-
text: JSON.stringify(output, null, 2)
|
|
10355
|
-
}
|
|
10356
|
-
],
|
|
10357
|
-
structuredContent: output
|
|
10358
|
-
};
|
|
10359
|
-
}
|
|
10360
|
-
);
|
|
10361
10324
|
const GetFolderStructureOutputSchema = {
|
|
10362
10325
|
folder_count: z5.number().describe("Total number of folders"),
|
|
10363
10326
|
folders: z5.array(
|
|
@@ -10561,7 +10524,7 @@ import { z as z6 } from "zod";
|
|
|
10561
10524
|
import * as fs12 from "fs";
|
|
10562
10525
|
import * as path15 from "path";
|
|
10563
10526
|
var HEADING_REGEX2 = /^(#{1,6})\s+(.+)$/;
|
|
10564
|
-
function
|
|
10527
|
+
function extractHeadings2(content) {
|
|
10565
10528
|
const lines = content.split("\n");
|
|
10566
10529
|
const headings = [];
|
|
10567
10530
|
let inCodeBlock = false;
|
|
@@ -10621,7 +10584,7 @@ async function getNoteStructure(index, notePath, vaultPath2) {
|
|
|
10621
10584
|
return null;
|
|
10622
10585
|
}
|
|
10623
10586
|
const lines = content.split("\n");
|
|
10624
|
-
const headings =
|
|
10587
|
+
const headings = extractHeadings2(content);
|
|
10625
10588
|
const sections = buildSections(headings, lines.length);
|
|
10626
10589
|
const contentWithoutCode = content.replace(/```[\s\S]*?```/g, "");
|
|
10627
10590
|
const words = contentWithoutCode.split(/\s+/).filter((w) => w.length > 0);
|
|
@@ -10644,7 +10607,7 @@ async function getSectionContent(index, notePath, headingText, vaultPath2, inclu
|
|
|
10644
10607
|
return null;
|
|
10645
10608
|
}
|
|
10646
10609
|
const lines = content.split("\n");
|
|
10647
|
-
const headings =
|
|
10610
|
+
const headings = extractHeadings2(content);
|
|
10648
10611
|
const targetHeading = headings.find(
|
|
10649
10612
|
(h) => h.text.toLowerCase() === headingText.toLowerCase()
|
|
10650
10613
|
);
|
|
@@ -10685,7 +10648,7 @@ async function findSections(index, headingPattern, vaultPath2, folder) {
|
|
|
10685
10648
|
} catch {
|
|
10686
10649
|
continue;
|
|
10687
10650
|
}
|
|
10688
|
-
const headings =
|
|
10651
|
+
const headings = extractHeadings2(content);
|
|
10689
10652
|
for (const heading of headings) {
|
|
10690
10653
|
if (regex.test(heading.text)) {
|
|
10691
10654
|
results.push({
|
|
@@ -10701,12 +10664,13 @@ async function findSections(index, headingPattern, vaultPath2, folder) {
|
|
|
10701
10664
|
}
|
|
10702
10665
|
|
|
10703
10666
|
// src/tools/read/primitives.ts
|
|
10704
|
-
|
|
10667
|
+
import { getEntityByName as getEntityByName3 } from "@velvetmonkey/vault-core";
|
|
10668
|
+
function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = () => ({}), getStateDb = () => null) {
|
|
10705
10669
|
server2.registerTool(
|
|
10706
10670
|
"get_note_structure",
|
|
10707
10671
|
{
|
|
10708
10672
|
title: "Get Note Structure",
|
|
10709
|
-
description: "
|
|
10673
|
+
description: "Read the structure of a specific note. Use after search identifies a note you need more detail on. Returns headings, frontmatter, tags, word count. Set include_content: true to get the full markdown.",
|
|
10710
10674
|
inputSchema: {
|
|
10711
10675
|
path: z6.string().describe("Path to the note"),
|
|
10712
10676
|
include_content: z6.boolean().default(false).describe("Include the text content under each top-level section")
|
|
@@ -10729,8 +10693,31 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
10729
10693
|
}
|
|
10730
10694
|
}
|
|
10731
10695
|
}
|
|
10696
|
+
const note = index.notes.get(path33);
|
|
10697
|
+
const enriched = { ...result };
|
|
10698
|
+
if (note) {
|
|
10699
|
+
enriched.frontmatter = note.frontmatter;
|
|
10700
|
+
enriched.tags = note.tags;
|
|
10701
|
+
enriched.aliases = note.aliases;
|
|
10702
|
+
const normalizedPath = path33.toLowerCase().replace(/\.md$/, "");
|
|
10703
|
+
const backlinks = index.backlinks.get(normalizedPath) || [];
|
|
10704
|
+
enriched.backlink_count = backlinks.length;
|
|
10705
|
+
enriched.outlink_count = note.outlinks.length;
|
|
10706
|
+
}
|
|
10707
|
+
const stateDb2 = getStateDb();
|
|
10708
|
+
if (stateDb2 && note) {
|
|
10709
|
+
try {
|
|
10710
|
+
const entity = getEntityByName3(stateDb2, note.title);
|
|
10711
|
+
if (entity) {
|
|
10712
|
+
enriched.category = entity.category;
|
|
10713
|
+
enriched.hub_score = entity.hubScore;
|
|
10714
|
+
if (entity.description) enriched.description = entity.description;
|
|
10715
|
+
}
|
|
10716
|
+
} catch {
|
|
10717
|
+
}
|
|
10718
|
+
}
|
|
10732
10719
|
return {
|
|
10733
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
10720
|
+
content: [{ type: "text", text: JSON.stringify(enriched, null, 2) }]
|
|
10734
10721
|
};
|
|
10735
10722
|
}
|
|
10736
10723
|
);
|
|
@@ -13270,7 +13257,7 @@ function ensureSectionExists(content, section, notePath) {
|
|
|
13270
13257
|
if (boundary) {
|
|
13271
13258
|
return { boundary };
|
|
13272
13259
|
}
|
|
13273
|
-
const headings =
|
|
13260
|
+
const headings = extractHeadings3(content);
|
|
13274
13261
|
let message;
|
|
13275
13262
|
if (headings.length === 0) {
|
|
13276
13263
|
message = `Section '${section}' not found. This file has no headings. Add section structure (## Heading) to enable section-scoped mutations.`;
|
|
@@ -19333,59 +19320,58 @@ var watcherStatus = null;
|
|
|
19333
19320
|
function getWatcherStatus() {
|
|
19334
19321
|
return watcherStatus;
|
|
19335
19322
|
}
|
|
19336
|
-
var PRESETS = {
|
|
19337
|
-
// Presets
|
|
19338
|
-
minimal: ["search", "structure", "append", "frontmatter", "notes"],
|
|
19339
|
-
writer: ["search", "structure", "append", "frontmatter", "notes", "tasks"],
|
|
19340
|
-
agent: ["search", "structure", "append", "frontmatter", "notes", "memory"],
|
|
19341
|
-
researcher: ["search", "structure", "backlinks", "hubs", "paths"],
|
|
19342
|
-
full: [
|
|
19343
|
-
"search",
|
|
19344
|
-
"backlinks",
|
|
19345
|
-
"orphans",
|
|
19346
|
-
"hubs",
|
|
19347
|
-
"paths",
|
|
19348
|
-
"schema",
|
|
19349
|
-
"structure",
|
|
19350
|
-
"tasks",
|
|
19351
|
-
"health",
|
|
19352
|
-
"wikilinks",
|
|
19353
|
-
"append",
|
|
19354
|
-
"frontmatter",
|
|
19355
|
-
"notes",
|
|
19356
|
-
"note-ops",
|
|
19357
|
-
"git",
|
|
19358
|
-
"policy",
|
|
19359
|
-
"memory"
|
|
19360
|
-
],
|
|
19361
|
-
// Composable bundles
|
|
19362
|
-
graph: ["backlinks", "orphans", "hubs", "paths"],
|
|
19363
|
-
analysis: ["schema", "wikilinks"],
|
|
19364
|
-
tasks: ["tasks"],
|
|
19365
|
-
health: ["health"],
|
|
19366
|
-
ops: ["git", "policy"],
|
|
19367
|
-
"note-ops": ["note-ops"]
|
|
19368
|
-
};
|
|
19369
19323
|
var ALL_CATEGORIES = [
|
|
19370
|
-
"backlinks",
|
|
19371
|
-
"orphans",
|
|
19372
|
-
"hubs",
|
|
19373
|
-
"paths",
|
|
19374
19324
|
"search",
|
|
19325
|
+
"read",
|
|
19326
|
+
"write",
|
|
19327
|
+
"graph",
|
|
19375
19328
|
"schema",
|
|
19376
|
-
"structure",
|
|
19377
|
-
"tasks",
|
|
19378
|
-
"health",
|
|
19379
19329
|
"wikilinks",
|
|
19380
|
-
"
|
|
19381
|
-
"
|
|
19382
|
-
"
|
|
19330
|
+
"corrections",
|
|
19331
|
+
"tasks",
|
|
19332
|
+
"memory",
|
|
19383
19333
|
"note-ops",
|
|
19384
|
-
"
|
|
19385
|
-
"
|
|
19386
|
-
"memory"
|
|
19334
|
+
"diagnostics",
|
|
19335
|
+
"automation"
|
|
19387
19336
|
];
|
|
19388
|
-
var
|
|
19337
|
+
var PRESETS = {
|
|
19338
|
+
// Presets
|
|
19339
|
+
default: ["search", "read", "write", "tasks"],
|
|
19340
|
+
agent: ["search", "read", "write", "memory"],
|
|
19341
|
+
full: ALL_CATEGORIES,
|
|
19342
|
+
// Composable bundles (one per category)
|
|
19343
|
+
graph: ["graph"],
|
|
19344
|
+
schema: ["schema"],
|
|
19345
|
+
wikilinks: ["wikilinks"],
|
|
19346
|
+
corrections: ["corrections"],
|
|
19347
|
+
tasks: ["tasks"],
|
|
19348
|
+
memory: ["memory"],
|
|
19349
|
+
"note-ops": ["note-ops"],
|
|
19350
|
+
diagnostics: ["diagnostics"],
|
|
19351
|
+
automation: ["automation"]
|
|
19352
|
+
};
|
|
19353
|
+
var DEFAULT_PRESET = "default";
|
|
19354
|
+
var DEPRECATED_ALIASES = {
|
|
19355
|
+
minimal: "default",
|
|
19356
|
+
writer: "default",
|
|
19357
|
+
// writer was default+tasks, now default includes tasks
|
|
19358
|
+
researcher: "default",
|
|
19359
|
+
// use default,graph for graph exploration
|
|
19360
|
+
backlinks: "graph",
|
|
19361
|
+
// get_backlinks moved to graph
|
|
19362
|
+
structure: "read",
|
|
19363
|
+
append: "write",
|
|
19364
|
+
frontmatter: "write",
|
|
19365
|
+
notes: "write",
|
|
19366
|
+
orphans: "graph",
|
|
19367
|
+
hubs: "graph",
|
|
19368
|
+
paths: "graph",
|
|
19369
|
+
health: "diagnostics",
|
|
19370
|
+
analysis: "wikilinks",
|
|
19371
|
+
git: "automation",
|
|
19372
|
+
ops: "automation",
|
|
19373
|
+
policy: "automation"
|
|
19374
|
+
};
|
|
19389
19375
|
function parseEnabledCategories() {
|
|
19390
19376
|
const envValue = (process.env.FLYWHEEL_TOOLS ?? process.env.FLYWHEEL_PRESET)?.trim();
|
|
19391
19377
|
if (!envValue) {
|
|
@@ -19395,13 +19381,25 @@ function parseEnabledCategories() {
|
|
|
19395
19381
|
if (PRESETS[lowerValue]) {
|
|
19396
19382
|
return new Set(PRESETS[lowerValue]);
|
|
19397
19383
|
}
|
|
19384
|
+
if (DEPRECATED_ALIASES[lowerValue]) {
|
|
19385
|
+
const resolved = DEPRECATED_ALIASES[lowerValue];
|
|
19386
|
+
serverLog("server", `Preset "${lowerValue}" is deprecated \u2014 use "${resolved}" instead`, "warn");
|
|
19387
|
+
if (PRESETS[resolved]) {
|
|
19388
|
+
return new Set(PRESETS[resolved]);
|
|
19389
|
+
}
|
|
19390
|
+
return /* @__PURE__ */ new Set([resolved]);
|
|
19391
|
+
}
|
|
19398
19392
|
const categories = /* @__PURE__ */ new Set();
|
|
19399
19393
|
for (const item of envValue.split(",")) {
|
|
19400
|
-
const
|
|
19401
|
-
|
|
19402
|
-
|
|
19403
|
-
|
|
19404
|
-
|
|
19394
|
+
const raw = item.trim().toLowerCase();
|
|
19395
|
+
const resolved = DEPRECATED_ALIASES[raw] ?? raw;
|
|
19396
|
+
if (resolved !== raw) {
|
|
19397
|
+
serverLog("server", `Category "${raw}" is deprecated \u2014 use "${resolved}" instead`, "warn");
|
|
19398
|
+
}
|
|
19399
|
+
if (ALL_CATEGORIES.includes(resolved)) {
|
|
19400
|
+
categories.add(resolved);
|
|
19401
|
+
} else if (PRESETS[resolved]) {
|
|
19402
|
+
for (const c of PRESETS[resolved]) {
|
|
19405
19403
|
categories.add(c);
|
|
19406
19404
|
}
|
|
19407
19405
|
} else {
|
|
@@ -19416,88 +19414,146 @@ function parseEnabledCategories() {
|
|
|
19416
19414
|
}
|
|
19417
19415
|
var enabledCategories = parseEnabledCategories();
|
|
19418
19416
|
var TOOL_CATEGORY = {
|
|
19419
|
-
//
|
|
19420
|
-
health_check: "health",
|
|
19421
|
-
get_vault_stats: "health",
|
|
19422
|
-
get_folder_structure: "health",
|
|
19423
|
-
refresh_index: "health",
|
|
19424
|
-
// absorbed rebuild_search_index
|
|
19425
|
-
get_all_entities: "health",
|
|
19426
|
-
list_entities: "hubs",
|
|
19427
|
-
get_unlinked_mentions: "health",
|
|
19428
|
-
// search (unified: metadata + content + entities)
|
|
19417
|
+
// search (3 tools)
|
|
19429
19418
|
search: "search",
|
|
19430
19419
|
init_semantic: "search",
|
|
19431
|
-
|
|
19432
|
-
|
|
19433
|
-
|
|
19434
|
-
|
|
19435
|
-
|
|
19436
|
-
|
|
19437
|
-
|
|
19438
|
-
|
|
19439
|
-
|
|
19440
|
-
|
|
19420
|
+
find_similar: "search",
|
|
19421
|
+
// read (3 tools) — note reading
|
|
19422
|
+
get_note_structure: "read",
|
|
19423
|
+
get_section_content: "read",
|
|
19424
|
+
find_sections: "read",
|
|
19425
|
+
// write (5 tools) — content mutations + frontmatter + note creation
|
|
19426
|
+
vault_add_to_section: "write",
|
|
19427
|
+
vault_remove_from_section: "write",
|
|
19428
|
+
vault_replace_in_section: "write",
|
|
19429
|
+
vault_update_frontmatter: "write",
|
|
19430
|
+
vault_create_note: "write",
|
|
19431
|
+
// graph (9 tools) — structural analysis + link detail
|
|
19432
|
+
graph_analysis: "graph",
|
|
19433
|
+
get_backlinks: "graph",
|
|
19434
|
+
get_forward_links: "graph",
|
|
19435
|
+
get_connection_strength: "graph",
|
|
19436
|
+
list_entities: "graph",
|
|
19437
|
+
get_link_path: "graph",
|
|
19438
|
+
get_common_neighbors: "graph",
|
|
19439
|
+
get_weighted_links: "graph",
|
|
19440
|
+
get_strong_connections: "graph",
|
|
19441
|
+
// schema (5 tools) — schema intelligence + migrations
|
|
19441
19442
|
vault_schema: "schema",
|
|
19442
19443
|
note_intelligence: "schema",
|
|
19443
|
-
|
|
19444
|
-
|
|
19445
|
-
|
|
19446
|
-
|
|
19447
|
-
|
|
19448
|
-
|
|
19444
|
+
rename_field: "schema",
|
|
19445
|
+
migrate_field_values: "schema",
|
|
19446
|
+
rename_tag: "schema",
|
|
19447
|
+
// wikilinks (7 tools) — suggestions, validation, discovery
|
|
19448
|
+
suggest_wikilinks: "wikilinks",
|
|
19449
|
+
validate_links: "wikilinks",
|
|
19450
|
+
wikilink_feedback: "wikilinks",
|
|
19451
|
+
discover_stub_candidates: "wikilinks",
|
|
19452
|
+
discover_cooccurrence_gaps: "wikilinks",
|
|
19453
|
+
suggest_entity_aliases: "wikilinks",
|
|
19454
|
+
unlinked_mentions_report: "wikilinks",
|
|
19455
|
+
// corrections (4 tools)
|
|
19456
|
+
vault_record_correction: "corrections",
|
|
19457
|
+
vault_list_corrections: "corrections",
|
|
19458
|
+
vault_resolve_correction: "corrections",
|
|
19459
|
+
absorb_as_alias: "corrections",
|
|
19460
|
+
// tasks (3 tools)
|
|
19449
19461
|
tasks: "tasks",
|
|
19450
19462
|
vault_toggle_task: "tasks",
|
|
19451
19463
|
vault_add_task: "tasks",
|
|
19452
|
-
//
|
|
19453
|
-
|
|
19454
|
-
|
|
19455
|
-
|
|
19456
|
-
|
|
19457
|
-
vault_remove_from_section: "append",
|
|
19458
|
-
vault_replace_in_section: "append",
|
|
19459
|
-
// frontmatter (absorbed vault_add_frontmatter_field via only_if_missing)
|
|
19460
|
-
vault_update_frontmatter: "frontmatter",
|
|
19461
|
-
// notes (create only)
|
|
19462
|
-
vault_create_note: "notes",
|
|
19463
|
-
// note-ops (file management)
|
|
19464
|
+
// memory (3 tools) — agent working memory
|
|
19465
|
+
memory: "memory",
|
|
19466
|
+
recall: "memory",
|
|
19467
|
+
brief: "memory",
|
|
19468
|
+
// note-ops (4 tools) — file management
|
|
19464
19469
|
vault_delete_note: "note-ops",
|
|
19465
19470
|
vault_move_note: "note-ops",
|
|
19466
19471
|
vault_rename_note: "note-ops",
|
|
19467
|
-
// git
|
|
19468
|
-
vault_undo_last_mutation: "git",
|
|
19469
|
-
// policy
|
|
19470
|
-
policy: "policy",
|
|
19471
|
-
// schema (migrations + tag rename)
|
|
19472
|
-
rename_field: "schema",
|
|
19473
|
-
migrate_field_values: "schema",
|
|
19474
|
-
rename_tag: "schema",
|
|
19475
|
-
// health (growth metrics)
|
|
19476
|
-
vault_growth: "health",
|
|
19477
|
-
// wikilinks (feedback)
|
|
19478
|
-
wikilink_feedback: "wikilinks",
|
|
19479
|
-
// health (activity tracking)
|
|
19480
|
-
vault_activity: "health",
|
|
19481
|
-
// schema (content similarity)
|
|
19482
|
-
find_similar: "schema",
|
|
19483
|
-
// health (config management)
|
|
19484
|
-
flywheel_config: "health",
|
|
19485
|
-
// health (server activity log)
|
|
19486
|
-
server_log: "health",
|
|
19487
|
-
// health (merge suggestions)
|
|
19488
|
-
suggest_entity_merges: "health",
|
|
19489
|
-
dismiss_merge_suggestion: "health",
|
|
19490
|
-
// note-ops (entity merge)
|
|
19491
19472
|
merge_entities: "note-ops",
|
|
19492
|
-
//
|
|
19493
|
-
|
|
19494
|
-
|
|
19495
|
-
|
|
19473
|
+
// diagnostics (13 tools) — vault health, stats, config, activity
|
|
19474
|
+
health_check: "diagnostics",
|
|
19475
|
+
get_vault_stats: "diagnostics",
|
|
19476
|
+
get_folder_structure: "diagnostics",
|
|
19477
|
+
refresh_index: "diagnostics",
|
|
19478
|
+
get_all_entities: "diagnostics",
|
|
19479
|
+
get_unlinked_mentions: "diagnostics",
|
|
19480
|
+
vault_growth: "diagnostics",
|
|
19481
|
+
vault_activity: "diagnostics",
|
|
19482
|
+
flywheel_config: "diagnostics",
|
|
19483
|
+
server_log: "diagnostics",
|
|
19484
|
+
suggest_entity_merges: "diagnostics",
|
|
19485
|
+
dismiss_merge_suggestion: "diagnostics",
|
|
19486
|
+
vault_init: "diagnostics",
|
|
19487
|
+
// automation (2 tools) — git undo + policy engine
|
|
19488
|
+
vault_undo_last_mutation: "automation",
|
|
19489
|
+
policy: "automation"
|
|
19496
19490
|
};
|
|
19497
|
-
|
|
19498
|
-
|
|
19499
|
-
|
|
19500
|
-
|
|
19491
|
+
function generateInstructions(categories) {
|
|
19492
|
+
const parts = [];
|
|
19493
|
+
parts.push(`Flywheel provides tools to search, read, and write an Obsidian vault's knowledge graph.
|
|
19494
|
+
|
|
19495
|
+
Tool selection:
|
|
19496
|
+
1. "search" is the primary tool. Each result includes: frontmatter, tags, aliases,
|
|
19497
|
+
backlinks (with line numbers), outlinks (with line numbers and existence check),
|
|
19498
|
+
headings, content snippet or preview, entity category, hub score, and timestamps.
|
|
19499
|
+
This is usually enough to answer without reading any files.
|
|
19500
|
+
2. Escalate to "get_note_structure" only when you need the full markdown content
|
|
19501
|
+
or word count. Use "get_section_content" to read one section by heading name.
|
|
19502
|
+
3. Use vault write tools instead of raw file writes \u2014 they auto-link entities
|
|
19503
|
+
and commit changes.`);
|
|
19504
|
+
if (categories.has("read")) {
|
|
19505
|
+
parts.push(`
|
|
19506
|
+
## Read
|
|
19507
|
+
|
|
19508
|
+
Escalation: "search" (enriched metadata + content preview) \u2192 "get_note_structure"
|
|
19509
|
+
(full content + word count) \u2192 "get_section_content" (single section).
|
|
19510
|
+
"find_sections" finds headings across the vault by pattern.`);
|
|
19511
|
+
}
|
|
19512
|
+
if (categories.has("write")) {
|
|
19513
|
+
parts.push(`
|
|
19514
|
+
## Write
|
|
19515
|
+
|
|
19516
|
+
Write to existing notes with "vault_add_to_section". Create new notes with "vault_create_note". Update metadata with "vault_update_frontmatter". All writes auto-link entities \u2014 no manual [[wikilinks]] needed.`);
|
|
19517
|
+
}
|
|
19518
|
+
if (categories.has("memory")) {
|
|
19519
|
+
parts.push(`
|
|
19520
|
+
## Memory
|
|
19521
|
+
|
|
19522
|
+
Session workflow: call "brief" at conversation start for vault context (recent sessions, active entities, stored memories). Use "recall" before answering questions \u2014 it searches entities, notes, and memories with graph-boosted ranking. Use "memory" to store observations that should persist across sessions.`);
|
|
19523
|
+
}
|
|
19524
|
+
if (categories.has("graph")) {
|
|
19525
|
+
parts.push(`
|
|
19526
|
+
## Graph
|
|
19527
|
+
|
|
19528
|
+
Use "get_backlinks" for per-backlink surrounding text (reads source files).
|
|
19529
|
+
Use "get_forward_links" for resolved file paths and alias text.
|
|
19530
|
+
Use "graph_analysis" for structural queries (hubs, orphans, dead ends).
|
|
19531
|
+
Use "get_connection_strength" to measure link strength between notes.
|
|
19532
|
+
Use "get_link_path" to find shortest paths.`);
|
|
19533
|
+
}
|
|
19534
|
+
if (categories.has("tasks")) {
|
|
19535
|
+
parts.push(`
|
|
19536
|
+
## Tasks
|
|
19537
|
+
|
|
19538
|
+
Use "tasks" to query tasks across the vault (filter by status, due date, path). Use "vault_add_task" to create tasks and "vault_toggle_task" to complete them.`);
|
|
19539
|
+
}
|
|
19540
|
+
if (categories.has("schema")) {
|
|
19541
|
+
parts.push(`
|
|
19542
|
+
## Schema
|
|
19543
|
+
|
|
19544
|
+
Use "vault_schema" before bulk operations to understand field conventions, inconsistencies, and note types. Use "note_intelligence" for per-note analysis.`);
|
|
19545
|
+
}
|
|
19546
|
+
return parts.join("\n");
|
|
19547
|
+
}
|
|
19548
|
+
var server = new McpServer(
|
|
19549
|
+
{
|
|
19550
|
+
name: "flywheel-memory",
|
|
19551
|
+
version: pkg.version
|
|
19552
|
+
},
|
|
19553
|
+
{
|
|
19554
|
+
instructions: generateInstructions(enabledCategories)
|
|
19555
|
+
}
|
|
19556
|
+
);
|
|
19501
19557
|
var _registeredCount = 0;
|
|
19502
19558
|
var _skippedCount = 0;
|
|
19503
19559
|
function gateByCategory(name) {
|
|
@@ -19587,7 +19643,7 @@ registerSystemTools(
|
|
|
19587
19643
|
registerGraphTools(server, () => vaultIndex, () => vaultPath, () => stateDb);
|
|
19588
19644
|
registerWikilinkTools(server, () => vaultIndex, () => vaultPath);
|
|
19589
19645
|
registerQueryTools(server, () => vaultIndex, () => vaultPath, () => stateDb);
|
|
19590
|
-
registerPrimitiveTools(server, () => vaultIndex, () => vaultPath, () => flywheelConfig);
|
|
19646
|
+
registerPrimitiveTools(server, () => vaultIndex, () => vaultPath, () => flywheelConfig, () => stateDb);
|
|
19591
19647
|
registerGraphAnalysisTools(server, () => vaultIndex, () => vaultPath, () => stateDb, () => flywheelConfig);
|
|
19592
19648
|
registerVaultSchemaTools(server, () => vaultIndex, () => vaultPath);
|
|
19593
19649
|
registerNoteIntelligenceTools(server, () => vaultIndex, () => vaultPath, () => flywheelConfig);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velvetmonkey/flywheel-memory",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.79",
|
|
4
4
|
"description": "MCP server that gives Claude full read/write access to your Obsidian vault. Select from 51 tools for search, backlinks, graph queries, mutations, agent memory, and hybrid semantic search.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
55
|
-
"@velvetmonkey/vault-core": "^2.0.
|
|
55
|
+
"@velvetmonkey/vault-core": "^2.0.78",
|
|
56
56
|
"better-sqlite3": "^11.0.0",
|
|
57
57
|
"chokidar": "^4.0.0",
|
|
58
58
|
"gray-matter": "^4.0.3",
|