@velvetmonkey/flywheel-memory 2.0.28 → 2.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +141 -23
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -476,7 +476,10 @@ async function readVaultFile(vaultPath2, notePath) {
|
|
|
476
476
|
throw new Error("Invalid path: path traversal not allowed");
|
|
477
477
|
}
|
|
478
478
|
const fullPath = path18.join(vaultPath2, notePath);
|
|
479
|
-
const rawContent = await
|
|
479
|
+
const [rawContent, stat3] = await Promise.all([
|
|
480
|
+
fs18.readFile(fullPath, "utf-8"),
|
|
481
|
+
fs18.stat(fullPath)
|
|
482
|
+
]);
|
|
480
483
|
const lineEnding = detectLineEnding(rawContent);
|
|
481
484
|
const normalizedContent = normalizeLineEndings(rawContent);
|
|
482
485
|
const parsed = matter5(normalizedContent);
|
|
@@ -485,7 +488,8 @@ async function readVaultFile(vaultPath2, notePath) {
|
|
|
485
488
|
content: parsed.content,
|
|
486
489
|
frontmatter,
|
|
487
490
|
rawContent,
|
|
488
|
-
lineEnding
|
|
491
|
+
lineEnding,
|
|
492
|
+
mtimeMs: stat3.mtimeMs
|
|
489
493
|
};
|
|
490
494
|
}
|
|
491
495
|
function deepCloneFrontmatter(obj) {
|
|
@@ -7402,6 +7406,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
7402
7406
|
note_count: z3.coerce.number().describe("Number of notes in the index"),
|
|
7403
7407
|
entity_count: z3.coerce.number().describe("Number of linkable entities (titles + aliases)"),
|
|
7404
7408
|
tag_count: z3.coerce.number().describe("Number of unique tags"),
|
|
7409
|
+
link_count: z3.coerce.number().describe("Total number of outgoing wikilinks"),
|
|
7405
7410
|
periodic_notes: z3.array(PeriodicNoteInfoSchema).optional().describe("Detected periodic note conventions"),
|
|
7406
7411
|
config: z3.record(z3.unknown()).optional().describe("Current flywheel config (paths, templates, etc.)"),
|
|
7407
7412
|
last_rebuild: z3.object({
|
|
@@ -7455,6 +7460,10 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
7455
7460
|
const noteCount = indexBuilt ? index.notes.size : 0;
|
|
7456
7461
|
const entityCount = indexBuilt ? index.entities.size : 0;
|
|
7457
7462
|
const tagCount = indexBuilt ? index.tags.size : 0;
|
|
7463
|
+
let linkCount = 0;
|
|
7464
|
+
if (indexBuilt) {
|
|
7465
|
+
for (const note of index.notes.values()) linkCount += note.outlinks.length;
|
|
7466
|
+
}
|
|
7458
7467
|
if (indexBuilt && noteCount === 0 && vaultAccessible) {
|
|
7459
7468
|
recommendations.push("No notes found in vault. Is PROJECT_PATH pointing to a markdown vault?");
|
|
7460
7469
|
}
|
|
@@ -7515,6 +7524,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig = () =>
|
|
|
7515
7524
|
note_count: noteCount,
|
|
7516
7525
|
entity_count: entityCount,
|
|
7517
7526
|
tag_count: tagCount,
|
|
7527
|
+
link_count: linkCount,
|
|
7518
7528
|
periodic_notes: periodicNotes && periodicNotes.length > 0 ? periodicNotes : void 0,
|
|
7519
7529
|
config: configInfo,
|
|
7520
7530
|
last_rebuild: lastRebuild,
|
|
@@ -7999,6 +8009,64 @@ import * as fs11 from "fs";
|
|
|
7999
8009
|
import * as path12 from "path";
|
|
8000
8010
|
import { z as z5 } from "zod";
|
|
8001
8011
|
import { scanVaultEntities as scanVaultEntities2, getEntityIndexFromDb as getEntityIndexFromDb2 } from "@velvetmonkey/vault-core";
|
|
8012
|
+
|
|
8013
|
+
// src/core/read/aliasSuggestions.ts
|
|
8014
|
+
function generateAliasCandidates(entityName, existingAliases) {
|
|
8015
|
+
const existing = new Set(existingAliases.map((a) => a.toLowerCase()));
|
|
8016
|
+
const candidates = [];
|
|
8017
|
+
const words = entityName.split(/[\s-]+/).filter((w) => w.length > 0);
|
|
8018
|
+
if (words.length >= 2) {
|
|
8019
|
+
const acronym = words.map((w) => w[0]).join("").toUpperCase();
|
|
8020
|
+
if (acronym.length >= 2 && acronym.length <= 6 && !existing.has(acronym.toLowerCase())) {
|
|
8021
|
+
candidates.push({ candidate: acronym, type: "acronym" });
|
|
8022
|
+
}
|
|
8023
|
+
if (words.length >= 3) {
|
|
8024
|
+
const short = words[0];
|
|
8025
|
+
if (short.length >= 3 && !existing.has(short.toLowerCase())) {
|
|
8026
|
+
candidates.push({ candidate: short, type: "short_form" });
|
|
8027
|
+
}
|
|
8028
|
+
}
|
|
8029
|
+
}
|
|
8030
|
+
return candidates;
|
|
8031
|
+
}
|
|
8032
|
+
function suggestEntityAliases(stateDb2, folder) {
|
|
8033
|
+
const db4 = stateDb2.db;
|
|
8034
|
+
const entities = folder ? db4.prepare(
|
|
8035
|
+
"SELECT name, path, aliases_json FROM entities WHERE path LIKE ? || '/%'"
|
|
8036
|
+
).all(folder) : db4.prepare("SELECT name, path, aliases_json FROM entities").all();
|
|
8037
|
+
const allEntityNames = new Set(
|
|
8038
|
+
db4.prepare("SELECT name_lower FROM entities").all().map((r) => r.name_lower)
|
|
8039
|
+
);
|
|
8040
|
+
const suggestions = [];
|
|
8041
|
+
const countStmt = db4.prepare(
|
|
8042
|
+
"SELECT COUNT(*) as cnt FROM notes_fts WHERE content MATCH ?"
|
|
8043
|
+
);
|
|
8044
|
+
for (const row of entities) {
|
|
8045
|
+
const aliases = row.aliases_json ? JSON.parse(row.aliases_json) : [];
|
|
8046
|
+
const candidates = generateAliasCandidates(row.name, aliases);
|
|
8047
|
+
for (const { candidate, type } of candidates) {
|
|
8048
|
+
if (allEntityNames.has(candidate.toLowerCase())) continue;
|
|
8049
|
+
let mentions = 0;
|
|
8050
|
+
try {
|
|
8051
|
+
const result = countStmt.get(`"${candidate}"`);
|
|
8052
|
+
mentions = result?.cnt ?? 0;
|
|
8053
|
+
} catch {
|
|
8054
|
+
}
|
|
8055
|
+
suggestions.push({
|
|
8056
|
+
entity: row.name,
|
|
8057
|
+
entity_path: row.path,
|
|
8058
|
+
current_aliases: aliases,
|
|
8059
|
+
candidate,
|
|
8060
|
+
type,
|
|
8061
|
+
mentions
|
|
8062
|
+
});
|
|
8063
|
+
}
|
|
8064
|
+
}
|
|
8065
|
+
suggestions.sort((a, b) => b.mentions - a.mentions || a.entity.localeCompare(b.entity));
|
|
8066
|
+
return suggestions;
|
|
8067
|
+
}
|
|
8068
|
+
|
|
8069
|
+
// src/tools/read/system.ts
|
|
8002
8070
|
function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfig, getStateDb) {
|
|
8003
8071
|
const RefreshIndexOutputSchema = {
|
|
8004
8072
|
success: z5.boolean().describe("Whether the refresh succeeded"),
|
|
@@ -8485,6 +8553,35 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
8485
8553
|
};
|
|
8486
8554
|
}
|
|
8487
8555
|
);
|
|
8556
|
+
server2.registerTool(
|
|
8557
|
+
"suggest_entity_aliases",
|
|
8558
|
+
{
|
|
8559
|
+
title: "Suggest Entity Aliases",
|
|
8560
|
+
description: "Generate alias suggestions for entities in a folder based on acronyms and short forms, validated against vault content.",
|
|
8561
|
+
inputSchema: {
|
|
8562
|
+
folder: z5.string().optional().describe("Folder path to scope suggestions to"),
|
|
8563
|
+
limit: z5.number().default(20).describe("Max suggestions to return")
|
|
8564
|
+
}
|
|
8565
|
+
},
|
|
8566
|
+
async ({
|
|
8567
|
+
folder,
|
|
8568
|
+
limit: requestedLimit
|
|
8569
|
+
}) => {
|
|
8570
|
+
const stateDb2 = getStateDb?.();
|
|
8571
|
+
if (!stateDb2) {
|
|
8572
|
+
return {
|
|
8573
|
+
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
|
|
8574
|
+
};
|
|
8575
|
+
}
|
|
8576
|
+
const suggestions = suggestEntityAliases(stateDb2, folder || void 0);
|
|
8577
|
+
const limit = Math.min(requestedLimit ?? 20, 50);
|
|
8578
|
+
const limited = suggestions.slice(0, limit);
|
|
8579
|
+
const output = { suggestion_count: limited.length, suggestions: limited };
|
|
8580
|
+
return {
|
|
8581
|
+
content: [{ type: "text", text: JSON.stringify(output, null, 2) }]
|
|
8582
|
+
};
|
|
8583
|
+
}
|
|
8584
|
+
);
|
|
8488
8585
|
}
|
|
8489
8586
|
|
|
8490
8587
|
// src/tools/read/primitives.ts
|
|
@@ -8799,6 +8896,8 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
8799
8896
|
content: [{ type: "text", text: JSON.stringify({
|
|
8800
8897
|
total_count: result2.total,
|
|
8801
8898
|
open_count: result2.open_count,
|
|
8899
|
+
completed_count: result2.completed_count,
|
|
8900
|
+
cancelled_count: result2.cancelled_count,
|
|
8802
8901
|
returned_count: result2.tasks.length,
|
|
8803
8902
|
tasks: result2.tasks
|
|
8804
8903
|
}, null, 2) }]
|
|
@@ -8831,6 +8930,8 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig = ()
|
|
|
8831
8930
|
content: [{ type: "text", text: JSON.stringify({
|
|
8832
8931
|
total_count: result.total,
|
|
8833
8932
|
open_count: result.open_count,
|
|
8933
|
+
completed_count: result.completed_count,
|
|
8934
|
+
cancelled_count: result.cancelled_count,
|
|
8834
8935
|
returned_count: paged.length,
|
|
8835
8936
|
tasks: paged
|
|
8836
8937
|
}, null, 2) }]
|
|
@@ -11141,41 +11242,58 @@ async function withVaultFile(options, operation) {
|
|
|
11141
11242
|
if (existsError) {
|
|
11142
11243
|
return formatMcpResult(existsError);
|
|
11143
11244
|
}
|
|
11144
|
-
const
|
|
11145
|
-
|
|
11146
|
-
|
|
11147
|
-
|
|
11245
|
+
const runMutation = async () => {
|
|
11246
|
+
const { content, frontmatter: frontmatter2, lineEnding: lineEnding2, mtimeMs } = await readVaultFile(vaultPath2, notePath);
|
|
11247
|
+
const writeStateDb = getWriteStateDb();
|
|
11248
|
+
if (writeStateDb) {
|
|
11249
|
+
processImplicitFeedback(writeStateDb, notePath, content);
|
|
11250
|
+
}
|
|
11251
|
+
let sectionBoundary;
|
|
11252
|
+
if (section) {
|
|
11253
|
+
const sectionResult = ensureSectionExists(content, section, notePath);
|
|
11254
|
+
if ("error" in sectionResult) {
|
|
11255
|
+
return { error: sectionResult.error };
|
|
11256
|
+
}
|
|
11257
|
+
sectionBoundary = sectionResult.boundary;
|
|
11258
|
+
}
|
|
11259
|
+
const ctx = {
|
|
11260
|
+
content,
|
|
11261
|
+
frontmatter: frontmatter2,
|
|
11262
|
+
lineEnding: lineEnding2,
|
|
11263
|
+
sectionBoundary,
|
|
11264
|
+
vaultPath: vaultPath2,
|
|
11265
|
+
notePath
|
|
11266
|
+
};
|
|
11267
|
+
const opResult2 = await operation(ctx);
|
|
11268
|
+
return { opResult: opResult2, frontmatter: frontmatter2, lineEnding: lineEnding2, mtimeMs };
|
|
11269
|
+
};
|
|
11270
|
+
let result = await runMutation();
|
|
11271
|
+
if ("error" in result) {
|
|
11272
|
+
return formatMcpResult(result.error);
|
|
11148
11273
|
}
|
|
11149
|
-
|
|
11150
|
-
|
|
11151
|
-
|
|
11152
|
-
|
|
11153
|
-
|
|
11274
|
+
const fullPath = path19.join(vaultPath2, notePath);
|
|
11275
|
+
const statBefore = await fs19.stat(fullPath);
|
|
11276
|
+
if (statBefore.mtimeMs !== result.mtimeMs) {
|
|
11277
|
+
console.warn(`[withVaultFile] External modification detected on ${notePath}, re-reading and retrying`);
|
|
11278
|
+
result = await runMutation();
|
|
11279
|
+
if ("error" in result) {
|
|
11280
|
+
return formatMcpResult(result.error);
|
|
11154
11281
|
}
|
|
11155
|
-
sectionBoundary = sectionResult.boundary;
|
|
11156
11282
|
}
|
|
11157
|
-
const
|
|
11158
|
-
content,
|
|
11159
|
-
frontmatter,
|
|
11160
|
-
lineEnding,
|
|
11161
|
-
sectionBoundary,
|
|
11162
|
-
vaultPath: vaultPath2,
|
|
11163
|
-
notePath
|
|
11164
|
-
};
|
|
11165
|
-
const opResult = await operation(ctx);
|
|
11283
|
+
const { opResult, frontmatter, lineEnding } = result;
|
|
11166
11284
|
let finalFrontmatter = opResult.updatedFrontmatter ?? frontmatter;
|
|
11167
11285
|
if (scoping && (scoping.agent_id || scoping.session_id)) {
|
|
11168
11286
|
finalFrontmatter = injectMutationMetadata(finalFrontmatter, scoping);
|
|
11169
11287
|
}
|
|
11170
11288
|
await writeVaultFile(vaultPath2, notePath, opResult.updatedContent, finalFrontmatter, lineEnding);
|
|
11171
11289
|
const gitInfo = await handleGitCommit(vaultPath2, notePath, commit, commitPrefix);
|
|
11172
|
-
const
|
|
11290
|
+
const successRes = successResult(notePath, opResult.message, gitInfo, {
|
|
11173
11291
|
preview: opResult.preview,
|
|
11174
11292
|
warnings: opResult.warnings,
|
|
11175
11293
|
outputIssues: opResult.outputIssues,
|
|
11176
11294
|
normalizationChanges: opResult.normalizationChanges
|
|
11177
11295
|
});
|
|
11178
|
-
return formatMcpResult(
|
|
11296
|
+
return formatMcpResult(successRes);
|
|
11179
11297
|
} catch (error) {
|
|
11180
11298
|
const extras = {};
|
|
11181
11299
|
if (error instanceof DiagnosticError) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velvetmonkey/flywheel-memory",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.29",
|
|
4
4
|
"description": "MCP server that gives Claude full read/write access to your Obsidian vault. 42 tools for search, backlinks, graph queries, mutations, and hybrid semantic search.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
53
|
-
"@velvetmonkey/vault-core": "^2.0.
|
|
53
|
+
"@velvetmonkey/vault-core": "^2.0.29",
|
|
54
54
|
"better-sqlite3": "^11.0.0",
|
|
55
55
|
"chokidar": "^4.0.0",
|
|
56
56
|
"gray-matter": "^4.0.3",
|