@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.
Files changed (2) hide show
  1. package/dist/index.js +181 -69
  2. 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
- setCrankState,
2736
- getCrankState,
2737
- deleteCrankState
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 saveLastCrankCommit(hash, message) {
2751
+ function saveLastMutationCommit(hash, message) {
2751
2752
  if (!moduleStateDb) {
2752
- console.error("[Crank] No StateDb available for saving last commit");
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
- setCrankState(moduleStateDb, "last_commit", data);
2762
+ setWriteState(moduleStateDb, "last_commit", data);
2762
2763
  } catch (e) {
2763
- console.error("[Crank] Failed to save last commit to StateDb:", e);
2764
+ console.error("[Flywheel] Failed to save last commit to StateDb:", e);
2764
2765
  }
2765
2766
  }
2766
- function getLastCrankCommit() {
2767
+ function getLastMutationCommit() {
2767
2768
  if (!moduleStateDb) {
2768
2769
  return null;
2769
2770
  }
2770
2771
  try {
2771
- return getCrankState(moduleStateDb, "last_commit");
2772
+ return getWriteState(moduleStateDb, "last_commit");
2772
2773
  } catch {
2773
2774
  return null;
2774
2775
  }
2775
2776
  }
2776
- function clearLastCrankCommit() {
2777
+ function clearLastMutationCommit() {
2777
2778
  if (!moduleStateDb) {
2778
2779
  return;
2779
2780
  }
2780
2781
  try {
2781
- deleteCrankState(moduleStateDb, "last_commit");
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
- saveLastCrankCommit(result.commit, commitMessage);
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
- saveLastCrankCommit(result.commit, commitMessage);
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
- setCrankState as setCrankState2,
3044
- getCrankState as getCrankState2
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(`[Crank] Error building recency index: ${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("[Crank] No StateDb available for saving recency");
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(`[Crank] Saved ${index.lastMentioned.size} recency entries to StateDb`);
3169
+ console.error(`[Flywheel] Saved ${index.lastMentioned.size} recency entries to StateDb`);
3169
3170
  } catch (e) {
3170
- console.error("[Crank] Failed to save recency to StateDb:", e);
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 setCrankStateDb(stateDb2) {
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(`[Crank] Loaded ${dbIndex._metadata.total_entities} entities from StateDb`);
4117
+ console.error(`[Flywheel] Loaded ${dbIndex._metadata.total_entities} entities from StateDb`);
4117
4118
  return;
4118
4119
  }
4119
4120
  } catch (e) {
4120
- console.error("[Crank] Failed to load from StateDb:", e);
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(`[Crank] Failed to initialize entity index: ${indexError2.message}`);
4127
+ console.error(`[Flywheel] Failed to initialize entity index: ${indexError2.message}`);
4127
4128
  }
4128
4129
  }
4129
4130
  async function rebuildIndex(vaultPath2) {
4130
- console.error(`[Crank] Scanning vault for entities...`);
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(`[Crank] Entity index built: ${entityIndex._metadata.total_entities} entities in ${entityDuration}ms`);
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(`[Crank] Saved entities to StateDb`);
4143
+ console.error(`[Flywheel] Saved entities to StateDb`);
4143
4144
  } catch (e) {
4144
- console.error(`[Crank] Failed to save entities to StateDb: ${e}`);
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(`[Crank] Co-occurrence index built: ${cooccurrenceIndex._metadata.total_associations} associations in ${cooccurrenceDuration}ms`);
4154
+ console.error(`[Flywheel] Co-occurrence index built: ${cooccurrenceIndex._metadata.total_associations} associations in ${cooccurrenceDuration}ms`);
4154
4155
  } catch (e) {
4155
- console.error(`[Crank] Failed to build co-occurrence index: ${e}`);
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(`[Crank] Recency index loaded from StateDb (${recencyIndex.lastMentioned.size} entities)`);
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(`[Crank] Recency index built: ${recencyIndex.lastMentioned.size} entities in ${recencyDuration}ms`);
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(`[Crank] Failed to build recency index: ${e}`);
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("[Crank] Entity index stale, reloading from StateDb...");
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(`[Crank] Reloaded ${dbIndex._metadata.total_entities} entities`);
4190
+ console.error(`[Flywheel] Reloaded ${dbIndex._metadata.total_entities} entities`);
4190
4191
  }
4191
4192
  }
4192
4193
  } catch (e) {
4193
- console.error("[Crank] Failed to check for stale entities:", e);
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("[Crank:DEBUG] Entity index not ready, entities:", entityIndex?._metadata?.total_entities ?? 0);
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(`[Crank:DEBUG] Processing wikilinks with ${entities.length} entities`);
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, "crank");
4822
+ logger2 = await createLoggerFromConfig2(vaultPath2, "write");
4822
4823
  } catch (error) {
4823
- console.error(`[Crank] Failed to initialize logger: ${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
- "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).",
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: "[Crank:Add]",
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: "[Crank:Remove]",
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: "[Crank:Replace]",
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
- "Toggle a task checkbox between checked and unchecked",
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, "[Crank:Task]");
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
- "Add a new task to a section in a markdown note",
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: "[Crank:Task]",
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
- "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).",
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: "[Crank:FM]",
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
- "Create a new note in the vault with optional frontmatter and content",
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, "[Crank:Create]");
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, "[Crank:Delete]");
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(", "), `[Crank:Move] ${oldPath} \u2192 ${newPath}`);
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(`[Crank] Entity cache rebuild failed: ${err}`);
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(`[Crank] Title sanitized: "${newTitle}" \u2192 "${sanitizedTitle}"`);
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(", "), `[Crank:Rename] ${oldTitle} \u2192 ${sanitizedTitle}`);
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(`[Crank] Entity cache rebuild failed: ${err}`);
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 Crank mutation). Performs a soft reset.",
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 lastCrankCommit = getLastCrankCommit();
10103
+ const lastMutationCommit = getLastMutationCommit();
10099
10104
  const lastCommit = await getLastCommit(vaultPath2);
10100
- if (lastCrankCommit && lastCommit) {
10101
- if (lastCommit.hash !== lastCrankCommit.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 Crank commit (${lastCrankCommit.hash.substring(0, 7)}). Another process may have committed since your mutation.`,
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: ${lastCrankCommit.hash.substring(0, 7)} "${lastCrankCommit.message}"
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
- clearLastCrankCommit();
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
- setCrankStateDb(stateDb);
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.7",
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.7",
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",