@velvetmonkey/flywheel-memory 2.0.7 → 2.0.9
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 +181 -69
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -567,6 +567,7 @@ function injectMutationMetadata(frontmatter, scoping) {
|
|
|
567
567
|
if (scoping.session_id) {
|
|
568
568
|
frontmatter._session_id = scoping.session_id;
|
|
569
569
|
}
|
|
570
|
+
frontmatter._source = "ai";
|
|
570
571
|
if (scoping.agent_id && scoping.session_id) {
|
|
571
572
|
frontmatter._last_modified_by = `${scoping.agent_id}:${scoping.session_id}`;
|
|
572
573
|
} else if (scoping.agent_id) {
|
|
@@ -2732,9 +2733,9 @@ import { simpleGit, CheckRepoActions } from "simple-git";
|
|
|
2732
2733
|
import path5 from "path";
|
|
2733
2734
|
import fs5 from "fs/promises";
|
|
2734
2735
|
import {
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2736
|
+
setWriteState,
|
|
2737
|
+
getWriteState,
|
|
2738
|
+
deleteWriteState
|
|
2738
2739
|
} from "@velvetmonkey/vault-core";
|
|
2739
2740
|
var moduleStateDb = null;
|
|
2740
2741
|
function setGitStateDb(stateDb2) {
|
|
@@ -2747,9 +2748,9 @@ var DEFAULT_RETRY = {
|
|
|
2747
2748
|
jitter: true
|
|
2748
2749
|
};
|
|
2749
2750
|
var STALE_LOCK_THRESHOLD_MS = 3e4;
|
|
2750
|
-
function
|
|
2751
|
+
function saveLastMutationCommit(hash, message) {
|
|
2751
2752
|
if (!moduleStateDb) {
|
|
2752
|
-
console.error("[
|
|
2753
|
+
console.error("[Flywheel] No StateDb available for saving last commit");
|
|
2753
2754
|
return;
|
|
2754
2755
|
}
|
|
2755
2756
|
const data = {
|
|
@@ -2758,27 +2759,27 @@ function saveLastCrankCommit(hash, message) {
|
|
|
2758
2759
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2759
2760
|
};
|
|
2760
2761
|
try {
|
|
2761
|
-
|
|
2762
|
+
setWriteState(moduleStateDb, "last_commit", data);
|
|
2762
2763
|
} catch (e) {
|
|
2763
|
-
console.error("[
|
|
2764
|
+
console.error("[Flywheel] Failed to save last commit to StateDb:", e);
|
|
2764
2765
|
}
|
|
2765
2766
|
}
|
|
2766
|
-
function
|
|
2767
|
+
function getLastMutationCommit() {
|
|
2767
2768
|
if (!moduleStateDb) {
|
|
2768
2769
|
return null;
|
|
2769
2770
|
}
|
|
2770
2771
|
try {
|
|
2771
|
-
return
|
|
2772
|
+
return getWriteState(moduleStateDb, "last_commit");
|
|
2772
2773
|
} catch {
|
|
2773
2774
|
return null;
|
|
2774
2775
|
}
|
|
2775
2776
|
}
|
|
2776
|
-
function
|
|
2777
|
+
function clearLastMutationCommit() {
|
|
2777
2778
|
if (!moduleStateDb) {
|
|
2778
2779
|
return;
|
|
2779
2780
|
}
|
|
2780
2781
|
try {
|
|
2781
|
-
|
|
2782
|
+
deleteWriteState(moduleStateDb, "last_commit");
|
|
2782
2783
|
} catch {
|
|
2783
2784
|
}
|
|
2784
2785
|
}
|
|
@@ -2860,7 +2861,7 @@ async function commitChange(vaultPath2, filePath, messagePrefix, retryConfig = D
|
|
|
2860
2861
|
const commitMessage = `${messagePrefix} Update ${fileName}`;
|
|
2861
2862
|
const result = await git.commit(commitMessage);
|
|
2862
2863
|
if (result.commit) {
|
|
2863
|
-
|
|
2864
|
+
saveLastMutationCommit(result.commit, commitMessage);
|
|
2864
2865
|
}
|
|
2865
2866
|
return {
|
|
2866
2867
|
success: true,
|
|
@@ -2995,7 +2996,7 @@ Files:
|
|
|
2995
2996
|
${files.map((f) => `- ${f}`).join("\n")}`;
|
|
2996
2997
|
const result = await git.commit(commitMessage);
|
|
2997
2998
|
if (result.commit) {
|
|
2998
|
-
|
|
2999
|
+
saveLastMutationCommit(result.commit, commitMessage);
|
|
2999
3000
|
}
|
|
3000
3001
|
return {
|
|
3001
3002
|
success: true,
|
|
@@ -3040,8 +3041,8 @@ ${files.map((f) => `- ${f}`).join("\n")}`;
|
|
|
3040
3041
|
|
|
3041
3042
|
// src/core/write/hints.ts
|
|
3042
3043
|
import {
|
|
3043
|
-
|
|
3044
|
-
|
|
3044
|
+
setWriteState as setWriteState2,
|
|
3045
|
+
getWriteState as getWriteState2
|
|
3045
3046
|
} from "@velvetmonkey/vault-core";
|
|
3046
3047
|
var moduleStateDb2 = null;
|
|
3047
3048
|
function setHintsStateDb(stateDb2) {
|
|
@@ -3115,7 +3116,7 @@ async function buildRecencyIndex(vaultPath2, entities) {
|
|
|
3115
3116
|
}
|
|
3116
3117
|
}
|
|
3117
3118
|
} catch (error) {
|
|
3118
|
-
console.error(`[
|
|
3119
|
+
console.error(`[Flywheel] Error building recency index: ${error}`);
|
|
3119
3120
|
}
|
|
3120
3121
|
return {
|
|
3121
3122
|
lastMentioned,
|
|
@@ -3158,16 +3159,16 @@ function loadRecencyFromStateDb() {
|
|
|
3158
3159
|
}
|
|
3159
3160
|
function saveRecencyToStateDb(index) {
|
|
3160
3161
|
if (!moduleStateDb3) {
|
|
3161
|
-
console.error("[
|
|
3162
|
+
console.error("[Flywheel] No StateDb available for saving recency");
|
|
3162
3163
|
return;
|
|
3163
3164
|
}
|
|
3164
3165
|
try {
|
|
3165
3166
|
for (const [entityNameLower, timestamp] of index.lastMentioned) {
|
|
3166
3167
|
recordEntityMention(moduleStateDb3, entityNameLower, new Date(timestamp));
|
|
3167
3168
|
}
|
|
3168
|
-
console.error(`[
|
|
3169
|
+
console.error(`[Flywheel] Saved ${index.lastMentioned.size} recency entries to StateDb`);
|
|
3169
3170
|
} catch (e) {
|
|
3170
|
-
console.error("[
|
|
3171
|
+
console.error("[Flywheel] Failed to save recency to StateDb:", e);
|
|
3171
3172
|
}
|
|
3172
3173
|
}
|
|
3173
3174
|
|
|
@@ -4068,7 +4069,7 @@ function getCooccurrenceBoost(entityName, matchedEntities, cooccurrenceIndex2, r
|
|
|
4068
4069
|
|
|
4069
4070
|
// src/core/write/wikilinks.ts
|
|
4070
4071
|
var moduleStateDb4 = null;
|
|
4071
|
-
function
|
|
4072
|
+
function setWriteStateDb(stateDb2) {
|
|
4072
4073
|
moduleStateDb4 = stateDb2;
|
|
4073
4074
|
setGitStateDb(stateDb2);
|
|
4074
4075
|
setHintsStateDb(stateDb2);
|
|
@@ -4113,21 +4114,21 @@ async function initializeEntityIndex(vaultPath2) {
|
|
|
4113
4114
|
entityIndex = dbIndex;
|
|
4114
4115
|
indexReady = true;
|
|
4115
4116
|
lastLoadedAt = Date.now();
|
|
4116
|
-
console.error(`[
|
|
4117
|
+
console.error(`[Flywheel] Loaded ${dbIndex._metadata.total_entities} entities from StateDb`);
|
|
4117
4118
|
return;
|
|
4118
4119
|
}
|
|
4119
4120
|
} catch (e) {
|
|
4120
|
-
console.error("[
|
|
4121
|
+
console.error("[Flywheel] Failed to load from StateDb:", e);
|
|
4121
4122
|
}
|
|
4122
4123
|
}
|
|
4123
4124
|
await rebuildIndex(vaultPath2);
|
|
4124
4125
|
} catch (error) {
|
|
4125
4126
|
indexError2 = error instanceof Error ? error : new Error(String(error));
|
|
4126
|
-
console.error(`[
|
|
4127
|
+
console.error(`[Flywheel] Failed to initialize entity index: ${indexError2.message}`);
|
|
4127
4128
|
}
|
|
4128
4129
|
}
|
|
4129
4130
|
async function rebuildIndex(vaultPath2) {
|
|
4130
|
-
console.error(`[
|
|
4131
|
+
console.error(`[Flywheel] Scanning vault for entities...`);
|
|
4131
4132
|
const startTime = Date.now();
|
|
4132
4133
|
entityIndex = await scanVaultEntities(vaultPath2, {
|
|
4133
4134
|
excludeFolders: DEFAULT_EXCLUDE_FOLDERS
|
|
@@ -4135,13 +4136,13 @@ async function rebuildIndex(vaultPath2) {
|
|
|
4135
4136
|
indexReady = true;
|
|
4136
4137
|
lastLoadedAt = Date.now();
|
|
4137
4138
|
const entityDuration = Date.now() - startTime;
|
|
4138
|
-
console.error(`[
|
|
4139
|
+
console.error(`[Flywheel] Entity index built: ${entityIndex._metadata.total_entities} entities in ${entityDuration}ms`);
|
|
4139
4140
|
if (moduleStateDb4) {
|
|
4140
4141
|
try {
|
|
4141
4142
|
moduleStateDb4.replaceAllEntities(entityIndex);
|
|
4142
|
-
console.error(`[
|
|
4143
|
+
console.error(`[Flywheel] Saved entities to StateDb`);
|
|
4143
4144
|
} catch (e) {
|
|
4144
|
-
console.error(`[
|
|
4145
|
+
console.error(`[Flywheel] Failed to save entities to StateDb: ${e}`);
|
|
4145
4146
|
}
|
|
4146
4147
|
}
|
|
4147
4148
|
const entities = getAllEntities(entityIndex);
|
|
@@ -4150,25 +4151,25 @@ async function rebuildIndex(vaultPath2) {
|
|
|
4150
4151
|
const cooccurrenceStart = Date.now();
|
|
4151
4152
|
cooccurrenceIndex = await mineCooccurrences(vaultPath2, entityNames);
|
|
4152
4153
|
const cooccurrenceDuration = Date.now() - cooccurrenceStart;
|
|
4153
|
-
console.error(`[
|
|
4154
|
+
console.error(`[Flywheel] Co-occurrence index built: ${cooccurrenceIndex._metadata.total_associations} associations in ${cooccurrenceDuration}ms`);
|
|
4154
4155
|
} catch (e) {
|
|
4155
|
-
console.error(`[
|
|
4156
|
+
console.error(`[Flywheel] Failed to build co-occurrence index: ${e}`);
|
|
4156
4157
|
}
|
|
4157
4158
|
try {
|
|
4158
4159
|
const cachedRecency = loadRecencyFromStateDb();
|
|
4159
4160
|
const cacheAgeMs = cachedRecency ? Date.now() - cachedRecency.lastUpdated : Infinity;
|
|
4160
4161
|
if (cachedRecency && cacheAgeMs < 60 * 60 * 1e3) {
|
|
4161
4162
|
recencyIndex = cachedRecency;
|
|
4162
|
-
console.error(`[
|
|
4163
|
+
console.error(`[Flywheel] Recency index loaded from StateDb (${recencyIndex.lastMentioned.size} entities)`);
|
|
4163
4164
|
} else {
|
|
4164
4165
|
const recencyStart = Date.now();
|
|
4165
4166
|
recencyIndex = await buildRecencyIndex(vaultPath2, entities);
|
|
4166
4167
|
const recencyDuration = Date.now() - recencyStart;
|
|
4167
|
-
console.error(`[
|
|
4168
|
+
console.error(`[Flywheel] Recency index built: ${recencyIndex.lastMentioned.size} entities in ${recencyDuration}ms`);
|
|
4168
4169
|
saveRecencyToStateDb(recencyIndex);
|
|
4169
4170
|
}
|
|
4170
4171
|
} catch (e) {
|
|
4171
|
-
console.error(`[
|
|
4172
|
+
console.error(`[Flywheel] Failed to build recency index: ${e}`);
|
|
4172
4173
|
}
|
|
4173
4174
|
}
|
|
4174
4175
|
function isEntityIndexReady() {
|
|
@@ -4181,16 +4182,16 @@ function checkAndRefreshIfStale() {
|
|
|
4181
4182
|
if (!metadata.entitiesBuiltAt) return;
|
|
4182
4183
|
const dbBuiltAt = new Date(metadata.entitiesBuiltAt).getTime();
|
|
4183
4184
|
if (dbBuiltAt > lastLoadedAt) {
|
|
4184
|
-
console.error("[
|
|
4185
|
+
console.error("[Flywheel] Entity index stale, reloading from StateDb...");
|
|
4185
4186
|
const dbIndex = getEntityIndexFromDb(moduleStateDb4);
|
|
4186
4187
|
if (dbIndex._metadata.total_entities > 0) {
|
|
4187
4188
|
entityIndex = dbIndex;
|
|
4188
4189
|
lastLoadedAt = Date.now();
|
|
4189
|
-
console.error(`[
|
|
4190
|
+
console.error(`[Flywheel] Reloaded ${dbIndex._metadata.total_entities} entities`);
|
|
4190
4191
|
}
|
|
4191
4192
|
}
|
|
4192
4193
|
} catch (e) {
|
|
4193
|
-
console.error("[
|
|
4194
|
+
console.error("[Flywheel] Failed to check for stale entities:", e);
|
|
4194
4195
|
}
|
|
4195
4196
|
}
|
|
4196
4197
|
function sortEntitiesByPriority(entities, notePath) {
|
|
@@ -4210,7 +4211,7 @@ function sortEntitiesByPriority(entities, notePath) {
|
|
|
4210
4211
|
}
|
|
4211
4212
|
function processWikilinks(content, notePath) {
|
|
4212
4213
|
if (!isEntityIndexReady() || !entityIndex) {
|
|
4213
|
-
console.error("[
|
|
4214
|
+
console.error("[Flywheel:DEBUG] Entity index not ready, entities:", entityIndex?._metadata?.total_entities ?? 0);
|
|
4214
4215
|
return {
|
|
4215
4216
|
content,
|
|
4216
4217
|
linksAdded: 0,
|
|
@@ -4218,7 +4219,7 @@ function processWikilinks(content, notePath) {
|
|
|
4218
4219
|
};
|
|
4219
4220
|
}
|
|
4220
4221
|
const entities = getAllEntities(entityIndex);
|
|
4221
|
-
console.error(`[
|
|
4222
|
+
console.error(`[Flywheel:DEBUG] Processing wikilinks with ${entities.length} entities`);
|
|
4222
4223
|
const sortedEntities = sortEntitiesByPriority(entities, notePath);
|
|
4223
4224
|
const resolved = resolveAliasWikilinks(content, sortedEntities, {
|
|
4224
4225
|
caseInsensitive: true
|
|
@@ -4818,9 +4819,9 @@ async function initializeLogger2(vaultPath2) {
|
|
|
4818
4819
|
try {
|
|
4819
4820
|
const sessionId = generateSessionId2();
|
|
4820
4821
|
setSessionId2(sessionId);
|
|
4821
|
-
logger2 = await createLoggerFromConfig2(vaultPath2, "
|
|
4822
|
+
logger2 = await createLoggerFromConfig2(vaultPath2, "write");
|
|
4822
4823
|
} catch (error) {
|
|
4823
|
-
console.error(`[
|
|
4824
|
+
console.error(`[Flywheel] Failed to initialize logger: ${error}`);
|
|
4824
4825
|
logger2 = null;
|
|
4825
4826
|
}
|
|
4826
4827
|
}
|
|
@@ -6140,7 +6141,7 @@ function sortNotes(notes, sortBy, order) {
|
|
|
6140
6141
|
function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
6141
6142
|
server2.tool(
|
|
6142
6143
|
"search",
|
|
6143
|
-
'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
|
|
6144
|
+
'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.\n\nExample: search({ query: "quarterly review", scope: "content", limit: 5 })\nExample: search({ where: { type: "project", status: "active" }, scope: "metadata" })',
|
|
6144
6145
|
{
|
|
6145
6146
|
query: z4.string().optional().describe('Search query text. Required for scope "content", "entities", "all". For "metadata" scope, use filters instead.'),
|
|
6146
6147
|
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)"),
|
|
@@ -7411,7 +7412,7 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath) {
|
|
|
7411
7412
|
"graph_analysis",
|
|
7412
7413
|
{
|
|
7413
7414
|
title: "Graph Analysis",
|
|
7414
|
-
description: 'Analyze vault link graph structure. Use analysis to pick the mode:\n- "orphans": Notes with no backlinks (disconnected content)\n- "dead_ends": Notes with backlinks but no outgoing links\n- "sources": Notes with outgoing links but no backlinks\n- "hubs": Highly connected notes (many links to/from)\n- "stale": Important notes (by backlink count) not recently modified',
|
|
7415
|
+
description: 'Analyze vault link graph structure. Use analysis to pick the mode:\n- "orphans": Notes with no backlinks (disconnected content)\n- "dead_ends": Notes with backlinks but no outgoing links\n- "sources": Notes with outgoing links but no backlinks\n- "hubs": Highly connected notes (many links to/from)\n- "stale": Important notes (by backlink count) not recently modified\n\nExample: graph_analysis({ analysis: "hubs", limit: 10 })\nExample: graph_analysis({ analysis: "stale", days: 30, min_backlinks: 3 })',
|
|
7415
7416
|
inputSchema: {
|
|
7416
7417
|
analysis: z8.enum(["orphans", "dead_ends", "sources", "hubs", "stale"]).describe("Type of graph analysis to perform"),
|
|
7417
7418
|
folder: z8.string().optional().describe("Limit to notes in this folder (orphans, dead_ends, sources)"),
|
|
@@ -7984,7 +7985,7 @@ function registerVaultSchemaTools(server2, getIndex, getVaultPath) {
|
|
|
7984
7985
|
"vault_schema",
|
|
7985
7986
|
{
|
|
7986
7987
|
title: "Vault Schema",
|
|
7987
|
-
description: 'Analyze and validate vault frontmatter schema. Use analysis to pick the mode:\n- "overview": Schema of all frontmatter fields across the vault\n- "field_values": All unique values for a specific field\n- "inconsistencies": Fields with multiple types across notes\n- "validate": Validate notes against a provided schema\n- "missing": Find notes missing expected fields by folder\n- "conventions": Auto-detect metadata conventions for a folder\n- "incomplete": Find notes missing expected fields (inferred)\n- "suggest_values": Suggest values for a field based on usage',
|
|
7988
|
+
description: 'Analyze and validate vault frontmatter schema. Use analysis to pick the mode:\n- "overview": Schema of all frontmatter fields across the vault\n- "field_values": All unique values for a specific field\n- "inconsistencies": Fields with multiple types across notes\n- "validate": Validate notes against a provided schema\n- "missing": Find notes missing expected fields by folder\n- "conventions": Auto-detect metadata conventions for a folder\n- "incomplete": Find notes missing expected fields (inferred)\n- "suggest_values": Suggest values for a field based on usage\n\nExample: vault_schema({ analysis: "field_values", field: "status" })\nExample: vault_schema({ analysis: "conventions", folder: "projects" })',
|
|
7988
7989
|
inputSchema: {
|
|
7989
7990
|
analysis: z9.enum([
|
|
7990
7991
|
"overview",
|
|
@@ -8554,7 +8555,7 @@ function registerNoteIntelligenceTools(server2, getIndex, getVaultPath) {
|
|
|
8554
8555
|
"note_intelligence",
|
|
8555
8556
|
{
|
|
8556
8557
|
title: "Note Intelligence",
|
|
8557
|
-
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- "cross_layer": Check consistency between frontmatter and prose references\n- "compute": Auto-compute derived fields (word_count, link_count, etc.)\n- "all": Run all analyses and return combined result',
|
|
8558
|
+
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- "cross_layer": Check consistency between frontmatter and prose references\n- "compute": Auto-compute derived fields (word_count, link_count, etc.)\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"] })',
|
|
8558
8559
|
inputSchema: {
|
|
8559
8560
|
analysis: z10.enum([
|
|
8560
8561
|
"prose_patterns",
|
|
@@ -9031,7 +9032,9 @@ async function createNoteFromTemplate(vaultPath2, notePath, config) {
|
|
|
9031
9032
|
function registerMutationTools(server2, vaultPath2, getConfig = () => ({})) {
|
|
9032
9033
|
server2.tool(
|
|
9033
9034
|
"vault_add_to_section",
|
|
9034
|
-
|
|
9035
|
+
`Add content to a specific section in a markdown note. Set create_if_missing=true to auto-create the note from template if it doesn't exist (enables 1-call daily capture).
|
|
9036
|
+
|
|
9037
|
+
Example: vault_add_to_section({ path: "daily/2026-02-15.md", section: "Log", content: "Met with team about Q1", format: "timestamp-bullet", create_if_missing: true })`,
|
|
9035
9038
|
{
|
|
9036
9039
|
path: z11.string().describe('Vault-relative path to the note (e.g., "daily-notes/2026-01-28.md")'),
|
|
9037
9040
|
section: z11.string().describe('Heading text to add to (e.g., "Log" or "## Log")'),
|
|
@@ -9069,7 +9072,7 @@ function registerMutationTools(server2, vaultPath2, getConfig = () => ({})) {
|
|
|
9069
9072
|
vaultPath: vaultPath2,
|
|
9070
9073
|
notePath,
|
|
9071
9074
|
commit,
|
|
9072
|
-
commitPrefix: "[
|
|
9075
|
+
commitPrefix: "[Flywheel:Add]",
|
|
9073
9076
|
section,
|
|
9074
9077
|
actionDescription: "add content",
|
|
9075
9078
|
scoping: agent_id || session_id ? { agent_id, session_id } : void 0
|
|
@@ -9144,7 +9147,7 @@ function registerMutationTools(server2, vaultPath2, getConfig = () => ({})) {
|
|
|
9144
9147
|
vaultPath: vaultPath2,
|
|
9145
9148
|
notePath,
|
|
9146
9149
|
commit,
|
|
9147
|
-
commitPrefix: "[
|
|
9150
|
+
commitPrefix: "[Flywheel:Remove]",
|
|
9148
9151
|
section,
|
|
9149
9152
|
actionDescription: "remove content",
|
|
9150
9153
|
scoping: agent_id || session_id ? { agent_id, session_id } : void 0
|
|
@@ -9195,7 +9198,7 @@ function registerMutationTools(server2, vaultPath2, getConfig = () => ({})) {
|
|
|
9195
9198
|
vaultPath: vaultPath2,
|
|
9196
9199
|
notePath,
|
|
9197
9200
|
commit,
|
|
9198
|
-
commitPrefix: "[
|
|
9201
|
+
commitPrefix: "[Flywheel:Replace]",
|
|
9199
9202
|
section,
|
|
9200
9203
|
actionDescription: "replace content",
|
|
9201
9204
|
scoping: agent_id || session_id ? { agent_id, session_id } : void 0
|
|
@@ -9295,7 +9298,7 @@ function toggleTask(content, lineNumber) {
|
|
|
9295
9298
|
function registerTaskTools(server2, vaultPath2) {
|
|
9296
9299
|
server2.tool(
|
|
9297
9300
|
"vault_toggle_task",
|
|
9298
|
-
|
|
9301
|
+
'Toggle a task checkbox between checked and unchecked.\n\nExample: vault_toggle_task({ path: "daily/2026-02-15.md", task: "review PR", section: "Tasks" })',
|
|
9299
9302
|
{
|
|
9300
9303
|
path: z12.string().describe("Vault-relative path to the note"),
|
|
9301
9304
|
task: z12.string().describe("Task text to find (partial match supported)"),
|
|
@@ -9338,7 +9341,7 @@ function registerTaskTools(server2, vaultPath2) {
|
|
|
9338
9341
|
finalFrontmatter = injectMutationMetadata(frontmatter, { agent_id, session_id });
|
|
9339
9342
|
}
|
|
9340
9343
|
await writeVaultFile(vaultPath2, notePath, toggleResult.content, finalFrontmatter);
|
|
9341
|
-
const gitInfo = await handleGitCommit(vaultPath2, notePath, commit, "[
|
|
9344
|
+
const gitInfo = await handleGitCommit(vaultPath2, notePath, commit, "[Flywheel:Task]");
|
|
9342
9345
|
const newStatus = toggleResult.newState ? "completed" : "incomplete";
|
|
9343
9346
|
const checkbox = toggleResult.newState ? "[x]" : "[ ]";
|
|
9344
9347
|
return formatMcpResult(
|
|
@@ -9355,7 +9358,7 @@ function registerTaskTools(server2, vaultPath2) {
|
|
|
9355
9358
|
);
|
|
9356
9359
|
server2.tool(
|
|
9357
9360
|
"vault_add_task",
|
|
9358
|
-
|
|
9361
|
+
'Add a new task to a section in a markdown note.\n\nExample: vault_add_task({ path: "daily/2026-02-15.md", section: "Tasks", task: "Write unit tests for auth module" })',
|
|
9359
9362
|
{
|
|
9360
9363
|
path: z12.string().describe("Vault-relative path to the note"),
|
|
9361
9364
|
section: z12.string().describe("Section to add the task to"),
|
|
@@ -9379,7 +9382,7 @@ function registerTaskTools(server2, vaultPath2) {
|
|
|
9379
9382
|
vaultPath: vaultPath2,
|
|
9380
9383
|
notePath,
|
|
9381
9384
|
commit,
|
|
9382
|
-
commitPrefix: "[
|
|
9385
|
+
commitPrefix: "[Flywheel:Task]",
|
|
9383
9386
|
section,
|
|
9384
9387
|
actionDescription: "add task",
|
|
9385
9388
|
scoping: agent_id || session_id ? { agent_id, session_id } : void 0
|
|
@@ -9433,7 +9436,9 @@ import { z as z13 } from "zod";
|
|
|
9433
9436
|
function registerFrontmatterTools(server2, vaultPath2) {
|
|
9434
9437
|
server2.tool(
|
|
9435
9438
|
"vault_update_frontmatter",
|
|
9436
|
-
|
|
9439
|
+
`Update frontmatter fields in a note (merge with existing). Set only_if_missing=true to only add fields that don't already exist (absorbed vault_add_frontmatter_field).
|
|
9440
|
+
|
|
9441
|
+
Example: vault_update_frontmatter({ path: "projects/alpha.md", frontmatter: { status: "active", priority: 1 }, only_if_missing: true })`,
|
|
9437
9442
|
{
|
|
9438
9443
|
path: z13.string().describe("Vault-relative path to the note"),
|
|
9439
9444
|
frontmatter: z13.record(z13.any()).describe("Frontmatter fields to update (JSON object)"),
|
|
@@ -9446,7 +9451,7 @@ function registerFrontmatterTools(server2, vaultPath2) {
|
|
|
9446
9451
|
vaultPath: vaultPath2,
|
|
9447
9452
|
notePath,
|
|
9448
9453
|
commit,
|
|
9449
|
-
commitPrefix: "[
|
|
9454
|
+
commitPrefix: "[Flywheel:FM]",
|
|
9450
9455
|
actionDescription: "update frontmatter"
|
|
9451
9456
|
},
|
|
9452
9457
|
async (ctx) => {
|
|
@@ -9494,7 +9499,7 @@ import path18 from "path";
|
|
|
9494
9499
|
function registerNoteTools(server2, vaultPath2, getIndex) {
|
|
9495
9500
|
server2.tool(
|
|
9496
9501
|
"vault_create_note",
|
|
9497
|
-
|
|
9502
|
+
'Create a new note in the vault with optional frontmatter and content.\n\nExample: vault_create_note({ path: "people/Jane Smith.md", content: "# Jane Smith\\n\\nProduct manager at Acme.", frontmatter: { type: "person", company: "Acme" } })',
|
|
9498
9503
|
{
|
|
9499
9504
|
path: z14.string().describe('Vault-relative path for the new note (e.g., "daily-notes/2026-01-28.md")'),
|
|
9500
9505
|
content: z14.string().default("").describe("Initial content for the note"),
|
|
@@ -9559,7 +9564,7 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
|
|
|
9559
9564
|
finalFrontmatter = injectMutationMetadata(frontmatter, { agent_id, session_id });
|
|
9560
9565
|
}
|
|
9561
9566
|
await writeVaultFile(vaultPath2, notePath, processedContent, finalFrontmatter);
|
|
9562
|
-
const gitInfo = await handleGitCommit(vaultPath2, notePath, commit, "[
|
|
9567
|
+
const gitInfo = await handleGitCommit(vaultPath2, notePath, commit, "[Flywheel:Create]");
|
|
9563
9568
|
const infoLines = [wikilinkInfo, suggestInfo].filter(Boolean);
|
|
9564
9569
|
const previewLines = [
|
|
9565
9570
|
`Frontmatter fields: ${Object.keys(frontmatter).join(", ") || "none"}`,
|
|
@@ -9639,7 +9644,7 @@ ${sources}`;
|
|
|
9639
9644
|
}
|
|
9640
9645
|
const fullPath = path18.join(vaultPath2, notePath);
|
|
9641
9646
|
await fs18.unlink(fullPath);
|
|
9642
|
-
const gitInfo = await handleGitCommit(vaultPath2, notePath, commit, "[
|
|
9647
|
+
const gitInfo = await handleGitCommit(vaultPath2, notePath, commit, "[Flywheel:Delete]");
|
|
9643
9648
|
const message = backlinkWarning ? `Deleted note: ${notePath}
|
|
9644
9649
|
|
|
9645
9650
|
Warning: ${backlinkWarning}` : `Deleted note: ${notePath}`;
|
|
@@ -9843,7 +9848,7 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
9843
9848
|
let lockAgeMs;
|
|
9844
9849
|
if (commit) {
|
|
9845
9850
|
const filesToCommit = [newPath, ...backlinkUpdates.map((b) => b.path)];
|
|
9846
|
-
const gitResult = await commitChange(vaultPath2, filesToCommit.join(", "), `[
|
|
9851
|
+
const gitResult = await commitChange(vaultPath2, filesToCommit.join(", "), `[Flywheel:Move] ${oldPath} \u2192 ${newPath}`);
|
|
9847
9852
|
if (gitResult.success && gitResult.hash) {
|
|
9848
9853
|
gitCommit = gitResult.hash;
|
|
9849
9854
|
undoAvailable = gitResult.undoAvailable;
|
|
@@ -9854,7 +9859,7 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
9854
9859
|
}
|
|
9855
9860
|
}
|
|
9856
9861
|
initializeEntityIndex(vaultPath2).catch((err) => {
|
|
9857
|
-
console.error(`[
|
|
9862
|
+
console.error(`[Flywheel] Entity cache rebuild failed: ${err}`);
|
|
9858
9863
|
});
|
|
9859
9864
|
const previewLines = [
|
|
9860
9865
|
`Moved: ${oldPath} \u2192 ${newPath}`
|
|
@@ -9918,7 +9923,7 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
9918
9923
|
}
|
|
9919
9924
|
const sanitizedTitle = newTitle.replace(/[<>:"/\\|?*]/g, "");
|
|
9920
9925
|
if (sanitizedTitle !== newTitle) {
|
|
9921
|
-
console.error(`[
|
|
9926
|
+
console.error(`[Flywheel] Title sanitized: "${newTitle}" \u2192 "${sanitizedTitle}"`);
|
|
9922
9927
|
}
|
|
9923
9928
|
const fullPath = path19.join(vaultPath2, notePath);
|
|
9924
9929
|
const dir = path19.dirname(notePath);
|
|
@@ -9983,7 +9988,7 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
9983
9988
|
let lockAgeMs;
|
|
9984
9989
|
if (commit) {
|
|
9985
9990
|
const filesToCommit = [newPath, ...backlinkUpdates.map((b) => b.path)];
|
|
9986
|
-
const gitResult = await commitChange(vaultPath2, filesToCommit.join(", "), `[
|
|
9991
|
+
const gitResult = await commitChange(vaultPath2, filesToCommit.join(", "), `[Flywheel:Rename] ${oldTitle} \u2192 ${sanitizedTitle}`);
|
|
9987
9992
|
if (gitResult.success && gitResult.hash) {
|
|
9988
9993
|
gitCommit = gitResult.hash;
|
|
9989
9994
|
undoAvailable = gitResult.undoAvailable;
|
|
@@ -9994,7 +9999,7 @@ function registerMoveNoteTools(server2, vaultPath2) {
|
|
|
9994
9999
|
}
|
|
9995
10000
|
}
|
|
9996
10001
|
initializeEntityIndex(vaultPath2).catch((err) => {
|
|
9997
|
-
console.error(`[
|
|
10002
|
+
console.error(`[Flywheel] Entity cache rebuild failed: ${err}`);
|
|
9998
10003
|
});
|
|
9999
10004
|
const previewLines = [
|
|
10000
10005
|
`Renamed: "${oldTitle}" \u2192 "${sanitizedTitle}"`
|
|
@@ -10036,7 +10041,7 @@ import { z as z16 } from "zod";
|
|
|
10036
10041
|
function registerSystemTools2(server2, vaultPath2) {
|
|
10037
10042
|
server2.tool(
|
|
10038
10043
|
"vault_undo_last_mutation",
|
|
10039
|
-
"Undo the last git commit (typically the last
|
|
10044
|
+
"Undo the last git commit (typically the last Flywheel mutation). Performs a soft reset.",
|
|
10040
10045
|
{
|
|
10041
10046
|
confirm: z16.boolean().default(false).describe("Must be true to confirm undo operation"),
|
|
10042
10047
|
hash: z16.string().optional().describe("Expected commit hash. If provided, undo only proceeds if HEAD matches this hash. Prevents accidentally undoing the wrong commit.")
|
|
@@ -10095,15 +10100,15 @@ Actual HEAD: ${currentHead.hash} "${currentHead.message}"`
|
|
|
10095
10100
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
10096
10101
|
}
|
|
10097
10102
|
}
|
|
10098
|
-
const
|
|
10103
|
+
const lastMutationCommit = getLastMutationCommit();
|
|
10099
10104
|
const lastCommit = await getLastCommit(vaultPath2);
|
|
10100
|
-
if (
|
|
10101
|
-
if (lastCommit.hash !==
|
|
10105
|
+
if (lastMutationCommit && lastCommit) {
|
|
10106
|
+
if (lastCommit.hash !== lastMutationCommit.hash) {
|
|
10102
10107
|
const result2 = {
|
|
10103
10108
|
success: false,
|
|
10104
|
-
message: `Cannot undo: HEAD (${lastCommit.hash.substring(0, 7)}) doesn't match last
|
|
10109
|
+
message: `Cannot undo: HEAD (${lastCommit.hash.substring(0, 7)}) doesn't match last Flywheel commit (${lastMutationCommit.hash.substring(0, 7)}). Another process may have committed since your mutation.`,
|
|
10105
10110
|
path: "",
|
|
10106
|
-
preview: `Expected: ${
|
|
10111
|
+
preview: `Expected: ${lastMutationCommit.hash.substring(0, 7)} "${lastMutationCommit.message}"
|
|
10107
10112
|
Actual HEAD: ${lastCommit.hash.substring(0, 7)} "${lastCommit.message}"`
|
|
10108
10113
|
};
|
|
10109
10114
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
@@ -10118,7 +10123,7 @@ Actual HEAD: ${lastCommit.hash.substring(0, 7)} "${lastCommit.message}"`
|
|
|
10118
10123
|
};
|
|
10119
10124
|
return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
|
|
10120
10125
|
}
|
|
10121
|
-
|
|
10126
|
+
clearLastMutationCommit();
|
|
10122
10127
|
const result = {
|
|
10123
10128
|
success: true,
|
|
10124
10129
|
message: undoResult.message,
|
|
@@ -11471,6 +11476,109 @@ function registerPolicyTools(server2, vaultPath2) {
|
|
|
11471
11476
|
);
|
|
11472
11477
|
}
|
|
11473
11478
|
|
|
11479
|
+
// src/resources/vault.ts
|
|
11480
|
+
function registerVaultResources(server2, getIndex) {
|
|
11481
|
+
server2.registerResource(
|
|
11482
|
+
"vault-stats",
|
|
11483
|
+
"vault://stats",
|
|
11484
|
+
{
|
|
11485
|
+
title: "Vault Statistics",
|
|
11486
|
+
description: "Overview of vault size, tags, links, and orphan count",
|
|
11487
|
+
mimeType: "application/json"
|
|
11488
|
+
},
|
|
11489
|
+
async (uri) => {
|
|
11490
|
+
const index = getIndex();
|
|
11491
|
+
if (!index) {
|
|
11492
|
+
return {
|
|
11493
|
+
contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify({ error: "Index not ready" }) }]
|
|
11494
|
+
};
|
|
11495
|
+
}
|
|
11496
|
+
const noteCount = index.notes.size;
|
|
11497
|
+
const tagCount = index.tags.size;
|
|
11498
|
+
let totalLinks = 0;
|
|
11499
|
+
for (const note of index.notes.values()) {
|
|
11500
|
+
totalLinks += note.outlinks.length;
|
|
11501
|
+
}
|
|
11502
|
+
let orphanCount = 0;
|
|
11503
|
+
for (const note of index.notes.values()) {
|
|
11504
|
+
const normalizedTitle = note.title.toLowerCase();
|
|
11505
|
+
const backlinks = index.backlinks.get(normalizedTitle);
|
|
11506
|
+
if (!backlinks || backlinks.length === 0) {
|
|
11507
|
+
orphanCount++;
|
|
11508
|
+
}
|
|
11509
|
+
}
|
|
11510
|
+
const stats = {
|
|
11511
|
+
note_count: noteCount,
|
|
11512
|
+
tag_count: tagCount,
|
|
11513
|
+
total_links: totalLinks,
|
|
11514
|
+
orphan_count: orphanCount,
|
|
11515
|
+
index_built_at: index.builtAt.toISOString()
|
|
11516
|
+
};
|
|
11517
|
+
return {
|
|
11518
|
+
contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(stats, null, 2) }]
|
|
11519
|
+
};
|
|
11520
|
+
}
|
|
11521
|
+
);
|
|
11522
|
+
server2.registerResource(
|
|
11523
|
+
"vault-schema",
|
|
11524
|
+
"vault://schema",
|
|
11525
|
+
{
|
|
11526
|
+
title: "Vault Schema",
|
|
11527
|
+
description: "Frontmatter field summary: field names, types, frequency",
|
|
11528
|
+
mimeType: "application/json"
|
|
11529
|
+
},
|
|
11530
|
+
async (uri) => {
|
|
11531
|
+
const index = getIndex();
|
|
11532
|
+
if (!index) {
|
|
11533
|
+
return {
|
|
11534
|
+
contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify({ error: "Index not ready" }) }]
|
|
11535
|
+
};
|
|
11536
|
+
}
|
|
11537
|
+
const schema = getFrontmatterSchema(index);
|
|
11538
|
+
const compact = {
|
|
11539
|
+
total_notes: schema.total_notes,
|
|
11540
|
+
notes_with_frontmatter: schema.notes_with_frontmatter,
|
|
11541
|
+
field_count: schema.field_count,
|
|
11542
|
+
fields: schema.fields.map((f) => ({
|
|
11543
|
+
name: f.name,
|
|
11544
|
+
types: f.types,
|
|
11545
|
+
count: f.count,
|
|
11546
|
+
examples: f.examples.slice(0, 3)
|
|
11547
|
+
}))
|
|
11548
|
+
};
|
|
11549
|
+
return {
|
|
11550
|
+
contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(compact, null, 2) }]
|
|
11551
|
+
};
|
|
11552
|
+
}
|
|
11553
|
+
);
|
|
11554
|
+
server2.registerResource(
|
|
11555
|
+
"vault-recent",
|
|
11556
|
+
"vault://recent",
|
|
11557
|
+
{
|
|
11558
|
+
title: "Recently Modified Notes",
|
|
11559
|
+
description: "Last 10 modified notes in the vault",
|
|
11560
|
+
mimeType: "application/json"
|
|
11561
|
+
},
|
|
11562
|
+
async (uri) => {
|
|
11563
|
+
const index = getIndex();
|
|
11564
|
+
if (!index) {
|
|
11565
|
+
return {
|
|
11566
|
+
contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify({ error: "Index not ready" }) }]
|
|
11567
|
+
};
|
|
11568
|
+
}
|
|
11569
|
+
const notes = Array.from(index.notes.values()).sort((a, b) => b.modified.getTime() - a.modified.getTime()).slice(0, 10).map((n) => ({
|
|
11570
|
+
path: n.path,
|
|
11571
|
+
title: n.title,
|
|
11572
|
+
modified: n.modified.toISOString(),
|
|
11573
|
+
tags: n.tags
|
|
11574
|
+
}));
|
|
11575
|
+
return {
|
|
11576
|
+
contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify({ recent_notes: notes }, null, 2) }]
|
|
11577
|
+
};
|
|
11578
|
+
}
|
|
11579
|
+
);
|
|
11580
|
+
}
|
|
11581
|
+
|
|
11474
11582
|
// src/index.ts
|
|
11475
11583
|
var vaultPath = process.env.PROJECT_PATH || process.env.VAULT_PATH || findVaultRoot();
|
|
11476
11584
|
var vaultIndex;
|
|
@@ -11661,6 +11769,7 @@ registerNoteTools(server, vaultPath, () => vaultIndex);
|
|
|
11661
11769
|
registerMoveNoteTools(server, vaultPath);
|
|
11662
11770
|
registerSystemTools2(server, vaultPath);
|
|
11663
11771
|
registerPolicyTools(server, vaultPath);
|
|
11772
|
+
registerVaultResources(server, () => vaultIndex ?? null);
|
|
11664
11773
|
console.error(`[Memory] Registered ${_registeredCount} tools, skipped ${_skippedCount}`);
|
|
11665
11774
|
async function main() {
|
|
11666
11775
|
console.error(`[Memory] Starting Flywheel Memory server...`);
|
|
@@ -11670,7 +11779,7 @@ async function main() {
|
|
|
11670
11779
|
stateDb = openStateDb(vaultPath);
|
|
11671
11780
|
console.error("[Memory] StateDb initialized");
|
|
11672
11781
|
setFTS5Database(stateDb.db);
|
|
11673
|
-
|
|
11782
|
+
setWriteStateDb(stateDb);
|
|
11674
11783
|
await initializeEntityIndex(vaultPath);
|
|
11675
11784
|
} catch (err) {
|
|
11676
11785
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -11817,6 +11926,9 @@ async function runPostIndexWork(index) {
|
|
|
11817
11926
|
} else {
|
|
11818
11927
|
const debounceMs = parseInt(process.env.FLYWHEEL_DEBOUNCE_MS || "60000");
|
|
11819
11928
|
console.error(`[Memory] File watcher v1 enabled (debounce: ${debounceMs}ms)`);
|
|
11929
|
+
if (debounceMs >= 6e4) {
|
|
11930
|
+
console.error("[Memory] Warning: Legacy watcher using high debounce (60s). Set FLYWHEEL_WATCH_V2=true for 200ms responsiveness.");
|
|
11931
|
+
}
|
|
11820
11932
|
const legacyWatcher = chokidar2.watch(vaultPath, {
|
|
11821
11933
|
ignored: /(^|[\/\\])\../,
|
|
11822
11934
|
persistent: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velvetmonkey/flywheel-memory",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.9",
|
|
4
4
|
"description": "MCP server that gives Claude full read/write access to your Obsidian vault. 36 tools for search, backlinks, graph queries, and mutations.",
|
|
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.9",
|
|
54
54
|
"better-sqlite3": "^11.0.0",
|
|
55
55
|
"chokidar": "^4.0.0",
|
|
56
56
|
"gray-matter": "^4.0.3",
|