@velvetmonkey/flywheel-memory 2.0.78 → 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 +240 -238
- package/package.json +1 -1
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 {
|
|
@@ -9655,7 +9677,16 @@ function enrichResult(result, index, stateDb2) {
|
|
|
9655
9677
|
enriched.tags = note.tags;
|
|
9656
9678
|
enriched.aliases = note.aliases;
|
|
9657
9679
|
enriched.backlink_count = backlinks.length;
|
|
9680
|
+
enriched.backlinks = backlinks.map((bl) => ({ source: bl.source, line: bl.line }));
|
|
9658
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 || [];
|
|
9659
9690
|
enriched.modified = note.modified.toISOString();
|
|
9660
9691
|
if (note.created) enriched.created = note.created.toISOString();
|
|
9661
9692
|
}
|
|
@@ -9670,6 +9701,10 @@ function enrichResult(result, index, stateDb2) {
|
|
|
9670
9701
|
} catch {
|
|
9671
9702
|
}
|
|
9672
9703
|
}
|
|
9704
|
+
if (!result.snippet) {
|
|
9705
|
+
const preview = getContentPreview(result.path);
|
|
9706
|
+
if (preview) enriched.content_preview = preview;
|
|
9707
|
+
}
|
|
9673
9708
|
return enriched;
|
|
9674
9709
|
}
|
|
9675
9710
|
function sortNotes(notes, sortBy, order) {
|
|
@@ -9696,7 +9731,7 @@ function sortNotes(notes, sortBy, order) {
|
|
|
9696
9731
|
function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
9697
9732
|
server2.tool(
|
|
9698
9733
|
"search",
|
|
9699
|
-
'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" })',
|
|
9700
9735
|
{
|
|
9701
9736
|
query: z4.string().optional().describe('Search query text. Required for scope "content", "entities", "all". For "metadata" scope, use filters instead.'),
|
|
9702
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)."),
|
|
@@ -9783,14 +9818,10 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
9783
9818
|
matchingNotes = sortNotes(matchingNotes, sort_by ?? "modified", order ?? "desc");
|
|
9784
9819
|
const totalMatches = matchingNotes.length;
|
|
9785
9820
|
const limitedNotes = matchingNotes.slice(0, limit);
|
|
9786
|
-
const
|
|
9787
|
-
|
|
9788
|
-
title: note.title,
|
|
9789
|
-
|
|
9790
|
-
created: note.created?.toISOString(),
|
|
9791
|
-
tags: note.tags,
|
|
9792
|
-
frontmatter: note.frontmatter
|
|
9793
|
-
}));
|
|
9821
|
+
const stateDb2 = getStateDb();
|
|
9822
|
+
const notes = limitedNotes.map(
|
|
9823
|
+
(note) => enrichResult({ path: note.path, title: note.title }, index, stateDb2)
|
|
9824
|
+
);
|
|
9794
9825
|
if (scope === "metadata" || hasMetadataFilters || totalMatches > 0) {
|
|
9795
9826
|
return { content: [{ type: "text", text: JSON.stringify({
|
|
9796
9827
|
scope: "metadata",
|
|
@@ -10290,104 +10321,6 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
10290
10321
|
};
|
|
10291
10322
|
}
|
|
10292
10323
|
);
|
|
10293
|
-
const GetNoteMetadataOutputSchema = {
|
|
10294
|
-
path: z5.string().describe("Path to the note"),
|
|
10295
|
-
title: z5.string().describe("Note title"),
|
|
10296
|
-
exists: z5.boolean().describe("Whether the note exists"),
|
|
10297
|
-
frontmatter: z5.record(z5.unknown()).describe("Frontmatter properties"),
|
|
10298
|
-
tags: z5.array(z5.string()).describe("Tags on this note"),
|
|
10299
|
-
aliases: z5.array(z5.string()).describe("Aliases for this note"),
|
|
10300
|
-
outlink_count: z5.number().describe("Number of outgoing links"),
|
|
10301
|
-
backlink_count: z5.number().describe("Number of incoming links"),
|
|
10302
|
-
word_count: z5.number().optional().describe("Approximate word count"),
|
|
10303
|
-
created: z5.string().optional().describe("Created date (ISO format)"),
|
|
10304
|
-
modified: z5.string().describe("Last modified date (ISO format)")
|
|
10305
|
-
};
|
|
10306
|
-
server2.registerTool(
|
|
10307
|
-
"get_note_metadata",
|
|
10308
|
-
{
|
|
10309
|
-
title: "Get Note Metadata",
|
|
10310
|
-
description: "Get metadata about a note (frontmatter, tags, link counts) without reading full content. Useful for quick analysis.",
|
|
10311
|
-
inputSchema: {
|
|
10312
|
-
path: z5.string().describe("Path to the note"),
|
|
10313
|
-
include_word_count: z5.boolean().default(false).describe("Count words (requires reading file)")
|
|
10314
|
-
},
|
|
10315
|
-
outputSchema: GetNoteMetadataOutputSchema
|
|
10316
|
-
},
|
|
10317
|
-
async ({
|
|
10318
|
-
path: notePath,
|
|
10319
|
-
include_word_count
|
|
10320
|
-
}) => {
|
|
10321
|
-
requireIndex();
|
|
10322
|
-
const index = getIndex();
|
|
10323
|
-
const vaultPath2 = getVaultPath();
|
|
10324
|
-
let resolvedPath = notePath;
|
|
10325
|
-
if (!notePath.endsWith(".md")) {
|
|
10326
|
-
const resolved = index.entities.get(notePath.toLowerCase());
|
|
10327
|
-
if (resolved) {
|
|
10328
|
-
resolvedPath = resolved;
|
|
10329
|
-
} else {
|
|
10330
|
-
resolvedPath = notePath + ".md";
|
|
10331
|
-
}
|
|
10332
|
-
}
|
|
10333
|
-
const note = index.notes.get(resolvedPath);
|
|
10334
|
-
if (!note) {
|
|
10335
|
-
const output2 = {
|
|
10336
|
-
path: resolvedPath,
|
|
10337
|
-
title: resolvedPath.replace(/\.md$/, "").split("/").pop() || "",
|
|
10338
|
-
exists: false,
|
|
10339
|
-
frontmatter: {},
|
|
10340
|
-
tags: [],
|
|
10341
|
-
aliases: [],
|
|
10342
|
-
outlink_count: 0,
|
|
10343
|
-
backlink_count: 0,
|
|
10344
|
-
modified: (/* @__PURE__ */ new Date()).toISOString()
|
|
10345
|
-
};
|
|
10346
|
-
return {
|
|
10347
|
-
content: [
|
|
10348
|
-
{
|
|
10349
|
-
type: "text",
|
|
10350
|
-
text: JSON.stringify(output2, null, 2)
|
|
10351
|
-
}
|
|
10352
|
-
],
|
|
10353
|
-
structuredContent: output2
|
|
10354
|
-
};
|
|
10355
|
-
}
|
|
10356
|
-
const normalizedPath = resolvedPath.toLowerCase().replace(/\.md$/, "");
|
|
10357
|
-
const backlinks = index.backlinks.get(normalizedPath) || [];
|
|
10358
|
-
let wordCount;
|
|
10359
|
-
if (include_word_count) {
|
|
10360
|
-
try {
|
|
10361
|
-
const fullPath = path14.join(vaultPath2, resolvedPath);
|
|
10362
|
-
const content = await fs11.promises.readFile(fullPath, "utf-8");
|
|
10363
|
-
wordCount = content.split(/\s+/).filter((w) => w.length > 0).length;
|
|
10364
|
-
} catch {
|
|
10365
|
-
}
|
|
10366
|
-
}
|
|
10367
|
-
const output = {
|
|
10368
|
-
path: note.path,
|
|
10369
|
-
title: note.title,
|
|
10370
|
-
exists: true,
|
|
10371
|
-
frontmatter: note.frontmatter,
|
|
10372
|
-
tags: note.tags,
|
|
10373
|
-
aliases: note.aliases,
|
|
10374
|
-
outlink_count: note.outlinks.length,
|
|
10375
|
-
backlink_count: backlinks.length,
|
|
10376
|
-
word_count: wordCount,
|
|
10377
|
-
created: note.created?.toISOString(),
|
|
10378
|
-
modified: note.modified.toISOString()
|
|
10379
|
-
};
|
|
10380
|
-
return {
|
|
10381
|
-
content: [
|
|
10382
|
-
{
|
|
10383
|
-
type: "text",
|
|
10384
|
-
text: JSON.stringify(output, null, 2)
|
|
10385
|
-
}
|
|
10386
|
-
],
|
|
10387
|
-
structuredContent: output
|
|
10388
|
-
};
|
|
10389
|
-
}
|
|
10390
|
-
);
|
|
10391
10324
|
const GetFolderStructureOutputSchema = {
|
|
10392
10325
|
folder_count: z5.number().describe("Total number of folders"),
|
|
10393
10326
|
folders: z5.array(
|
|
@@ -10591,7 +10524,7 @@ import { z as z6 } from "zod";
|
|
|
10591
10524
|
import * as fs12 from "fs";
|
|
10592
10525
|
import * as path15 from "path";
|
|
10593
10526
|
var HEADING_REGEX2 = /^(#{1,6})\s+(.+)$/;
|
|
10594
|
-
function
|
|
10527
|
+
function extractHeadings2(content) {
|
|
10595
10528
|
const lines = content.split("\n");
|
|
10596
10529
|
const headings = [];
|
|
10597
10530
|
let inCodeBlock = false;
|
|
@@ -10651,7 +10584,7 @@ async function getNoteStructure(index, notePath, vaultPath2) {
|
|
|
10651
10584
|
return null;
|
|
10652
10585
|
}
|
|
10653
10586
|
const lines = content.split("\n");
|
|
10654
|
-
const headings =
|
|
10587
|
+
const headings = extractHeadings2(content);
|
|
10655
10588
|
const sections = buildSections(headings, lines.length);
|
|
10656
10589
|
const contentWithoutCode = content.replace(/```[\s\S]*?```/g, "");
|
|
10657
10590
|
const words = contentWithoutCode.split(/\s+/).filter((w) => w.length > 0);
|
|
@@ -10674,7 +10607,7 @@ async function getSectionContent(index, notePath, headingText, vaultPath2, inclu
|
|
|
10674
10607
|
return null;
|
|
10675
10608
|
}
|
|
10676
10609
|
const lines = content.split("\n");
|
|
10677
|
-
const headings =
|
|
10610
|
+
const headings = extractHeadings2(content);
|
|
10678
10611
|
const targetHeading = headings.find(
|
|
10679
10612
|
(h) => h.text.toLowerCase() === headingText.toLowerCase()
|
|
10680
10613
|
);
|
|
@@ -10715,7 +10648,7 @@ async function findSections(index, headingPattern, vaultPath2, folder) {
|
|
|
10715
10648
|
} catch {
|
|
10716
10649
|
continue;
|
|
10717
10650
|
}
|
|
10718
|
-
const headings =
|
|
10651
|
+
const headings = extractHeadings2(content);
|
|
10719
10652
|
for (const heading of headings) {
|
|
10720
10653
|
if (regex.test(heading.text)) {
|
|
10721
10654
|
results.push({
|
|
@@ -10737,7 +10670,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
10737
10670
|
"get_note_structure",
|
|
10738
10671
|
{
|
|
10739
10672
|
title: "Get Note Structure",
|
|
10740
|
-
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.",
|
|
10741
10674
|
inputSchema: {
|
|
10742
10675
|
path: z6.string().describe("Path to the note"),
|
|
10743
10676
|
include_content: z6.boolean().default(false).describe("Include the text content under each top-level section")
|
|
@@ -13324,7 +13257,7 @@ function ensureSectionExists(content, section, notePath) {
|
|
|
13324
13257
|
if (boundary) {
|
|
13325
13258
|
return { boundary };
|
|
13326
13259
|
}
|
|
13327
|
-
const headings =
|
|
13260
|
+
const headings = extractHeadings3(content);
|
|
13328
13261
|
let message;
|
|
13329
13262
|
if (headings.length === 0) {
|
|
13330
13263
|
message = `Section '${section}' not found. This file has no headings. Add section structure (## Heading) to enable section-scoped mutations.`;
|
|
@@ -19387,59 +19320,58 @@ var watcherStatus = null;
|
|
|
19387
19320
|
function getWatcherStatus() {
|
|
19388
19321
|
return watcherStatus;
|
|
19389
19322
|
}
|
|
19390
|
-
var PRESETS = {
|
|
19391
|
-
// Presets
|
|
19392
|
-
minimal: ["search", "structure", "append", "frontmatter", "notes"],
|
|
19393
|
-
writer: ["search", "structure", "append", "frontmatter", "notes", "tasks"],
|
|
19394
|
-
agent: ["search", "structure", "append", "frontmatter", "notes", "memory"],
|
|
19395
|
-
researcher: ["search", "structure", "backlinks", "hubs", "paths"],
|
|
19396
|
-
full: [
|
|
19397
|
-
"search",
|
|
19398
|
-
"backlinks",
|
|
19399
|
-
"orphans",
|
|
19400
|
-
"hubs",
|
|
19401
|
-
"paths",
|
|
19402
|
-
"schema",
|
|
19403
|
-
"structure",
|
|
19404
|
-
"tasks",
|
|
19405
|
-
"health",
|
|
19406
|
-
"wikilinks",
|
|
19407
|
-
"append",
|
|
19408
|
-
"frontmatter",
|
|
19409
|
-
"notes",
|
|
19410
|
-
"note-ops",
|
|
19411
|
-
"git",
|
|
19412
|
-
"policy",
|
|
19413
|
-
"memory"
|
|
19414
|
-
],
|
|
19415
|
-
// Composable bundles
|
|
19416
|
-
graph: ["backlinks", "orphans", "hubs", "paths"],
|
|
19417
|
-
analysis: ["schema", "wikilinks"],
|
|
19418
|
-
tasks: ["tasks"],
|
|
19419
|
-
health: ["health"],
|
|
19420
|
-
ops: ["git", "policy"],
|
|
19421
|
-
"note-ops": ["note-ops"]
|
|
19422
|
-
};
|
|
19423
19323
|
var ALL_CATEGORIES = [
|
|
19424
|
-
"backlinks",
|
|
19425
|
-
"orphans",
|
|
19426
|
-
"hubs",
|
|
19427
|
-
"paths",
|
|
19428
19324
|
"search",
|
|
19325
|
+
"read",
|
|
19326
|
+
"write",
|
|
19327
|
+
"graph",
|
|
19429
19328
|
"schema",
|
|
19430
|
-
"structure",
|
|
19431
|
-
"tasks",
|
|
19432
|
-
"health",
|
|
19433
19329
|
"wikilinks",
|
|
19434
|
-
"
|
|
19435
|
-
"
|
|
19436
|
-
"
|
|
19330
|
+
"corrections",
|
|
19331
|
+
"tasks",
|
|
19332
|
+
"memory",
|
|
19437
19333
|
"note-ops",
|
|
19438
|
-
"
|
|
19439
|
-
"
|
|
19440
|
-
"memory"
|
|
19334
|
+
"diagnostics",
|
|
19335
|
+
"automation"
|
|
19441
19336
|
];
|
|
19442
|
-
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
|
+
};
|
|
19443
19375
|
function parseEnabledCategories() {
|
|
19444
19376
|
const envValue = (process.env.FLYWHEEL_TOOLS ?? process.env.FLYWHEEL_PRESET)?.trim();
|
|
19445
19377
|
if (!envValue) {
|
|
@@ -19449,13 +19381,25 @@ function parseEnabledCategories() {
|
|
|
19449
19381
|
if (PRESETS[lowerValue]) {
|
|
19450
19382
|
return new Set(PRESETS[lowerValue]);
|
|
19451
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
|
+
}
|
|
19452
19392
|
const categories = /* @__PURE__ */ new Set();
|
|
19453
19393
|
for (const item of envValue.split(",")) {
|
|
19454
|
-
const
|
|
19455
|
-
|
|
19456
|
-
|
|
19457
|
-
|
|
19458
|
-
|
|
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]) {
|
|
19459
19403
|
categories.add(c);
|
|
19460
19404
|
}
|
|
19461
19405
|
} else {
|
|
@@ -19470,88 +19414,146 @@ function parseEnabledCategories() {
|
|
|
19470
19414
|
}
|
|
19471
19415
|
var enabledCategories = parseEnabledCategories();
|
|
19472
19416
|
var TOOL_CATEGORY = {
|
|
19473
|
-
//
|
|
19474
|
-
health_check: "health",
|
|
19475
|
-
get_vault_stats: "health",
|
|
19476
|
-
get_folder_structure: "health",
|
|
19477
|
-
refresh_index: "health",
|
|
19478
|
-
// absorbed rebuild_search_index
|
|
19479
|
-
get_all_entities: "health",
|
|
19480
|
-
list_entities: "hubs",
|
|
19481
|
-
get_unlinked_mentions: "health",
|
|
19482
|
-
// search (unified: metadata + content + entities)
|
|
19417
|
+
// search (3 tools)
|
|
19483
19418
|
search: "search",
|
|
19484
19419
|
init_semantic: "search",
|
|
19485
|
-
|
|
19486
|
-
|
|
19487
|
-
|
|
19488
|
-
|
|
19489
|
-
|
|
19490
|
-
|
|
19491
|
-
|
|
19492
|
-
|
|
19493
|
-
|
|
19494
|
-
|
|
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
|
|
19495
19442
|
vault_schema: "schema",
|
|
19496
19443
|
note_intelligence: "schema",
|
|
19497
|
-
|
|
19498
|
-
|
|
19499
|
-
|
|
19500
|
-
|
|
19501
|
-
|
|
19502
|
-
|
|
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)
|
|
19503
19461
|
tasks: "tasks",
|
|
19504
19462
|
vault_toggle_task: "tasks",
|
|
19505
19463
|
vault_add_task: "tasks",
|
|
19506
|
-
//
|
|
19507
|
-
|
|
19508
|
-
|
|
19509
|
-
|
|
19510
|
-
|
|
19511
|
-
vault_remove_from_section: "append",
|
|
19512
|
-
vault_replace_in_section: "append",
|
|
19513
|
-
// frontmatter (absorbed vault_add_frontmatter_field via only_if_missing)
|
|
19514
|
-
vault_update_frontmatter: "frontmatter",
|
|
19515
|
-
// notes (create only)
|
|
19516
|
-
vault_create_note: "notes",
|
|
19517
|
-
// 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
|
|
19518
19469
|
vault_delete_note: "note-ops",
|
|
19519
19470
|
vault_move_note: "note-ops",
|
|
19520
19471
|
vault_rename_note: "note-ops",
|
|
19521
|
-
// git
|
|
19522
|
-
vault_undo_last_mutation: "git",
|
|
19523
|
-
// policy
|
|
19524
|
-
policy: "policy",
|
|
19525
|
-
// schema (migrations + tag rename)
|
|
19526
|
-
rename_field: "schema",
|
|
19527
|
-
migrate_field_values: "schema",
|
|
19528
|
-
rename_tag: "schema",
|
|
19529
|
-
// health (growth metrics)
|
|
19530
|
-
vault_growth: "health",
|
|
19531
|
-
// wikilinks (feedback)
|
|
19532
|
-
wikilink_feedback: "wikilinks",
|
|
19533
|
-
// health (activity tracking)
|
|
19534
|
-
vault_activity: "health",
|
|
19535
|
-
// schema (content similarity)
|
|
19536
|
-
find_similar: "schema",
|
|
19537
|
-
// health (config management)
|
|
19538
|
-
flywheel_config: "health",
|
|
19539
|
-
// health (server activity log)
|
|
19540
|
-
server_log: "health",
|
|
19541
|
-
// health (merge suggestions)
|
|
19542
|
-
suggest_entity_merges: "health",
|
|
19543
|
-
dismiss_merge_suggestion: "health",
|
|
19544
|
-
// note-ops (entity merge)
|
|
19545
19472
|
merge_entities: "note-ops",
|
|
19546
|
-
//
|
|
19547
|
-
|
|
19548
|
-
|
|
19549
|
-
|
|
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"
|
|
19550
19490
|
};
|
|
19551
|
-
|
|
19552
|
-
|
|
19553
|
-
|
|
19554
|
-
|
|
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
|
+
);
|
|
19555
19557
|
var _registeredCount = 0;
|
|
19556
19558
|
var _skippedCount = 0;
|
|
19557
19559
|
function gateByCategory(name) {
|
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",
|