@velvetmonkey/flywheel-crank 0.1.0 → 0.2.0

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 (3) hide show
  1. package/README.md +57 -16
  2. package/dist/index.js +46 -47
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -14,7 +14,7 @@ While **Flywheel** provides read-only graph intelligence (backlinks, queries, st
14
14
  - Toggle and create tasks
15
15
  - Update frontmatter
16
16
  - Create and delete notes
17
- - Auto-commit with undo support
17
+ - Optional git commits with undo support
18
18
 
19
19
  ## Installation
20
20
 
@@ -27,10 +27,7 @@ Add to your project's `.mcp.json` (in your vault root). **Zero-config** if `.mcp
27
27
  "mcpServers": {
28
28
  "flywheel-crank": {
29
29
  "command": "npx",
30
- "args": ["-y", "@velvetmonkey/flywheel-crank"],
31
- "env": {
32
- "AUTO_COMMIT": "true"
33
- }
30
+ "args": ["-y", "@velvetmonkey/flywheel-crank"]
34
31
  }
35
32
  }
36
33
  }
@@ -52,8 +49,7 @@ If `.mcp.json` is NOT in your vault, set `PROJECT_PATH`:
52
49
  "command": "npx",
53
50
  "args": ["-y", "@velvetmonkey/flywheel-crank"],
54
51
  "env": {
55
- "PROJECT_PATH": "/path/to/your/vault",
56
- "AUTO_COMMIT": "true"
52
+ "PROJECT_PATH": "/path/to/your/vault"
57
53
  }
58
54
  }
59
55
  }
@@ -69,8 +65,7 @@ If `.mcp.json` is NOT in your vault, set `PROJECT_PATH`:
69
65
  "command": "cmd",
70
66
  "args": ["/c", "npx", "-y", "@velvetmonkey/flywheel-crank"],
71
67
  "env": {
72
- "PROJECT_PATH": "C:/Users/yourname/path/to/vault",
73
- "AUTO_COMMIT": "true"
68
+ "PROJECT_PATH": "C:/Users/yourname/path/to/vault"
74
69
  }
75
70
  }
76
71
  }
@@ -83,10 +78,10 @@ If `.mcp.json` is NOT in your vault, set `PROJECT_PATH`:
83
78
 
84
79
  ```bash
85
80
  # Zero-config (run from vault directory)
86
- claude mcp add flywheel-crank -e AUTO_COMMIT=true -- npx -y @velvetmonkey/flywheel-crank
81
+ claude mcp add flywheel-crank -- npx -y @velvetmonkey/flywheel-crank
87
82
 
88
83
  # With explicit vault path
89
- claude mcp add flywheel-crank -e PROJECT_PATH=/path/to/vault -e AUTO_COMMIT=true -- npx -y @velvetmonkey/flywheel-crank
84
+ claude mcp add flywheel-crank -e PROJECT_PATH=/path/to/vault -- npx -y @velvetmonkey/flywheel-crank
90
85
  ```
91
86
 
92
87
  ### Verify
@@ -105,18 +100,65 @@ claude mcp list # Should show: flywheel-crank
105
100
  | Notes | `vault_create_note`, `vault_delete_note` |
106
101
  | System | `vault_list_sections`, `vault_undo_last_mutation` |
107
102
 
103
+ ## The `commit` Parameter (Undo Support)
104
+
105
+ Every mutation tool has an optional `commit` parameter. Here's what it does and why you'd use it:
106
+
107
+ ### What is a commit?
108
+
109
+ If your vault is tracked with **git** (a version control system), setting `commit: true` creates a **save point** after each change. Think of it like a checkpoint in a video game - you can always go back to it.
110
+
111
+ ### Why use it?
112
+
113
+ | `commit: false` (default) | `commit: true` |
114
+ |---------------------------|----------------|
115
+ | Changes saved to file immediately | Changes saved AND recorded in git history |
116
+ | No undo available | Can undo with `vault_undo_last_mutation` |
117
+ | Faster (no git overhead) | Slightly slower |
118
+ | Good for: bulk edits, testing | Good for: important changes you might want to reverse |
119
+
120
+ ### Example
121
+
122
+ ```javascript
123
+ // Without commit - change is made, no undo available
124
+ vault_add_to_section({
125
+ path: "daily/2026-01-28.md",
126
+ section: "Log",
127
+ content: "Meeting with team"
128
+ })
129
+
130
+ // With commit - change is made AND you can undo it later
131
+ vault_add_to_section({
132
+ path: "daily/2026-01-28.md",
133
+ section: "Log",
134
+ content: "Meeting with team",
135
+ commit: true // Creates undo point
136
+ })
137
+ ```
138
+
139
+ ### Undoing a change
140
+
141
+ If you used `commit: true`, you can undo with:
142
+
143
+ ```javascript
144
+ vault_undo_last_mutation({ confirm: true })
145
+ ```
146
+
147
+ This reverts your vault to the state before the last committed change.
148
+
149
+ > **Note**: Undo only works if your vault is a git repository. Most Obsidian users who sync via git already have this set up. If you're unsure, the tool will tell you if undo isn't available.
150
+
108
151
  ## Configuration
109
152
 
110
153
  | Environment Variable | Required | Default | Description |
111
154
  |---------------------|:--------:|---------|-------------|
112
155
  | `PROJECT_PATH` | No | `cwd()` | Path to markdown vault directory |
113
- | `AUTO_COMMIT` | No | `false` | Auto-commit mutations to git |
114
156
 
115
157
  ## Design Principles
116
158
 
117
159
  - **Deterministic**: No AI-driven edits, predictable output
118
160
  - **Atomic**: Each mutation is a single, reversible operation
119
- - **Safe**: Path sandboxing, auto-commit, undo support
161
+ - **Safe**: Path sandboxing, explicit commit opt-in, undo support
120
162
  - **Obsidian-compatible**: Follows Obsidian conventions (task format, wikilinks, etc.)
121
163
 
122
164
  ## Flywheel + Crank
@@ -132,8 +174,7 @@ Use both together for full vault intelligence:
132
174
  },
133
175
  "flywheel-crank": {
134
176
  "command": "npx",
135
- "args": ["-y", "@velvetmonkey/flywheel-crank"],
136
- "env": { "AUTO_COMMIT": "true" }
177
+ "args": ["-y", "@velvetmonkey/flywheel-crank"]
137
178
  }
138
179
  }
139
180
  }
@@ -144,7 +185,7 @@ Use both together for full vault intelligence:
144
185
 
145
186
  ## License
146
187
 
147
- AGPL-3.0 — [Ben Cassie](https://github.com/velvetmonkey)
188
+ AGPL-3.0 — [velvetmonkey](https://github.com/velvetmonkey)
148
189
 
149
190
  ## Links
150
191
 
package/dist/index.js CHANGED
@@ -308,7 +308,7 @@ async function undoLastCommit(vaultPath2) {
308
308
  // src/tools/mutations.ts
309
309
  import fs2 from "fs/promises";
310
310
  import path3 from "path";
311
- function registerMutationTools(server2, vaultPath2, autoCommit2) {
311
+ function registerMutationTools(server2, vaultPath2) {
312
312
  server2.tool(
313
313
  "vault_add_to_section",
314
314
  "Add content to a specific section in a markdown note",
@@ -317,9 +317,10 @@ function registerMutationTools(server2, vaultPath2, autoCommit2) {
317
317
  section: z.string().describe('Heading text to add to (e.g., "Log" or "## Log")'),
318
318
  content: z.string().describe("Content to add to the section"),
319
319
  position: z.enum(["append", "prepend"]).default("append").describe("Where to insert content"),
320
- format: z.enum(["plain", "bullet", "task", "numbered", "timestamp-bullet"]).default("plain").describe("How to format the content")
320
+ format: z.enum(["plain", "bullet", "task", "numbered", "timestamp-bullet"]).default("plain").describe("How to format the content"),
321
+ commit: z.boolean().default(false).describe("If true, commit this change to git (creates undo point)")
321
322
  },
322
- async ({ path: notePath, section, content, position, format }) => {
323
+ async ({ path: notePath, section, content, position, format, commit }) => {
323
324
  try {
324
325
  const fullPath = path3.join(vaultPath2, notePath);
325
326
  try {
@@ -352,7 +353,7 @@ function registerMutationTools(server2, vaultPath2, autoCommit2) {
352
353
  await writeVaultFile(vaultPath2, notePath, updatedContent, frontmatter);
353
354
  let gitCommit;
354
355
  let gitError;
355
- if (autoCommit2) {
356
+ if (commit) {
356
357
  const gitResult = await commitChange(vaultPath2, notePath, "[Crank:Add]");
357
358
  if (gitResult.success) {
358
359
  gitCommit = gitResult.hash;
@@ -388,9 +389,10 @@ function registerMutationTools(server2, vaultPath2, autoCommit2) {
388
389
  section: z.string().describe('Heading text to remove from (e.g., "Log" or "## Log")'),
389
390
  pattern: z.string().describe("Text or pattern to match for removal"),
390
391
  mode: z.enum(["first", "last", "all"]).default("first").describe("Which matches to remove"),
391
- useRegex: z.boolean().default(false).describe("Treat pattern as regex")
392
+ useRegex: z.boolean().default(false).describe("Treat pattern as regex"),
393
+ commit: z.boolean().default(false).describe("If true, commit this change to git (creates undo point)")
392
394
  },
393
- async ({ path: notePath, section, pattern, mode, useRegex }) => {
395
+ async ({ path: notePath, section, pattern, mode, useRegex, commit }) => {
394
396
  try {
395
397
  const fullPath = path3.join(vaultPath2, notePath);
396
398
  try {
@@ -431,7 +433,7 @@ function registerMutationTools(server2, vaultPath2, autoCommit2) {
431
433
  await writeVaultFile(vaultPath2, notePath, removeResult.content, frontmatter);
432
434
  let gitCommit;
433
435
  let gitError;
434
- if (autoCommit2) {
436
+ if (commit) {
435
437
  const gitResult = await commitChange(vaultPath2, notePath, "[Crank:Remove]");
436
438
  if (gitResult.success) {
437
439
  gitCommit = gitResult.hash;
@@ -467,9 +469,10 @@ function registerMutationTools(server2, vaultPath2, autoCommit2) {
467
469
  search: z.string().describe("Text or pattern to search for"),
468
470
  replacement: z.string().describe("Text to replace with"),
469
471
  mode: z.enum(["first", "last", "all"]).default("first").describe("Which matches to replace"),
470
- useRegex: z.boolean().default(false).describe("Treat search as regex")
472
+ useRegex: z.boolean().default(false).describe("Treat search as regex"),
473
+ commit: z.boolean().default(false).describe("If true, commit this change to git (creates undo point)")
471
474
  },
472
- async ({ path: notePath, section, search, replacement, mode, useRegex }) => {
475
+ async ({ path: notePath, section, search, replacement, mode, useRegex, commit }) => {
473
476
  try {
474
477
  const fullPath = path3.join(vaultPath2, notePath);
475
478
  try {
@@ -511,7 +514,7 @@ function registerMutationTools(server2, vaultPath2, autoCommit2) {
511
514
  await writeVaultFile(vaultPath2, notePath, replaceResult.content, frontmatter);
512
515
  let gitCommit;
513
516
  let gitError;
514
- if (autoCommit2) {
517
+ if (commit) {
515
518
  const gitResult = await commitChange(vaultPath2, notePath, "[Crank:Replace]");
516
519
  if (gitResult.success) {
517
520
  gitCommit = gitResult.hash;
@@ -592,16 +595,17 @@ function toggleTask(content, lineNumber) {
592
595
  newState
593
596
  };
594
597
  }
595
- function registerTaskTools(server2, vaultPath2, autoCommit2) {
598
+ function registerTaskTools(server2, vaultPath2) {
596
599
  server2.tool(
597
600
  "vault_toggle_task",
598
601
  "Toggle a task checkbox between checked and unchecked",
599
602
  {
600
603
  path: z2.string().describe("Vault-relative path to the note"),
601
604
  task: z2.string().describe("Task text to find (partial match supported)"),
602
- section: z2.string().optional().describe("Optional: limit search to this section")
605
+ section: z2.string().optional().describe("Optional: limit search to this section"),
606
+ commit: z2.boolean().default(false).describe("If true, commit this change to git (creates undo point)")
603
607
  },
604
- async ({ path: notePath, task, section }) => {
608
+ async ({ path: notePath, task, section, commit }) => {
605
609
  try {
606
610
  const fullPath = path4.join(vaultPath2, notePath);
607
611
  try {
@@ -653,7 +657,7 @@ function registerTaskTools(server2, vaultPath2, autoCommit2) {
653
657
  await writeVaultFile(vaultPath2, notePath, toggleResult.content, frontmatter);
654
658
  let gitCommit;
655
659
  let gitError;
656
- if (autoCommit2) {
660
+ if (commit) {
657
661
  const gitResult = await commitChange(vaultPath2, notePath, "[Crank:Task]");
658
662
  if (gitResult.success) {
659
663
  gitCommit = gitResult.hash;
@@ -690,9 +694,10 @@ function registerTaskTools(server2, vaultPath2, autoCommit2) {
690
694
  section: z2.string().describe("Section to add the task to"),
691
695
  task: z2.string().describe("Task text (without checkbox)"),
692
696
  position: z2.enum(["append", "prepend"]).default("append").describe("Where to add the task"),
693
- completed: z2.boolean().default(false).describe("Whether the task should start as completed")
697
+ completed: z2.boolean().default(false).describe("Whether the task should start as completed"),
698
+ commit: z2.boolean().default(false).describe("If true, commit this change to git (creates undo point)")
694
699
  },
695
- async ({ path: notePath, section, task, position, completed }) => {
700
+ async ({ path: notePath, section, task, position, completed, commit }) => {
696
701
  try {
697
702
  const fullPath = path4.join(vaultPath2, notePath);
698
703
  try {
@@ -726,7 +731,7 @@ function registerTaskTools(server2, vaultPath2, autoCommit2) {
726
731
  await writeVaultFile(vaultPath2, notePath, updatedContent, frontmatter);
727
732
  let gitCommit;
728
733
  let gitError;
729
- if (autoCommit2) {
734
+ if (commit) {
730
735
  const gitResult = await commitChange(vaultPath2, notePath, "[Crank:Task]");
731
736
  if (gitResult.success) {
732
737
  gitCommit = gitResult.hash;
@@ -760,15 +765,16 @@ function registerTaskTools(server2, vaultPath2, autoCommit2) {
760
765
  import { z as z3 } from "zod";
761
766
  import fs4 from "fs/promises";
762
767
  import path5 from "path";
763
- function registerFrontmatterTools(server2, vaultPath2, autoCommit2) {
768
+ function registerFrontmatterTools(server2, vaultPath2) {
764
769
  server2.tool(
765
770
  "vault_update_frontmatter",
766
771
  "Update frontmatter fields in a note (merge with existing frontmatter)",
767
772
  {
768
773
  path: z3.string().describe("Vault-relative path to the note"),
769
- frontmatter: z3.record(z3.any()).describe("Frontmatter fields to update (JSON object)")
774
+ frontmatter: z3.record(z3.any()).describe("Frontmatter fields to update (JSON object)"),
775
+ commit: z3.boolean().default(false).describe("If true, commit this change to git (creates undo point)")
770
776
  },
771
- async ({ path: notePath, frontmatter: updates }) => {
777
+ async ({ path: notePath, frontmatter: updates, commit }) => {
772
778
  try {
773
779
  const fullPath = path5.join(vaultPath2, notePath);
774
780
  try {
@@ -786,7 +792,7 @@ function registerFrontmatterTools(server2, vaultPath2, autoCommit2) {
786
792
  await writeVaultFile(vaultPath2, notePath, content, updatedFrontmatter);
787
793
  let gitCommit;
788
794
  let gitError;
789
- if (autoCommit2) {
795
+ if (commit) {
790
796
  const gitResult = await commitChange(vaultPath2, notePath, "[Crank:FM]");
791
797
  if (gitResult.success) {
792
798
  gitCommit = gitResult.hash;
@@ -821,9 +827,10 @@ function registerFrontmatterTools(server2, vaultPath2, autoCommit2) {
821
827
  {
822
828
  path: z3.string().describe("Vault-relative path to the note"),
823
829
  key: z3.string().describe("Field name to add"),
824
- value: z3.any().describe("Field value (string, number, boolean, array, object)")
830
+ value: z3.any().describe("Field value (string, number, boolean, array, object)"),
831
+ commit: z3.boolean().default(false).describe("If true, commit this change to git (creates undo point)")
825
832
  },
826
- async ({ path: notePath, key, value }) => {
833
+ async ({ path: notePath, key, value, commit }) => {
827
834
  try {
828
835
  const fullPath = path5.join(vaultPath2, notePath);
829
836
  try {
@@ -849,7 +856,7 @@ function registerFrontmatterTools(server2, vaultPath2, autoCommit2) {
849
856
  await writeVaultFile(vaultPath2, notePath, content, updatedFrontmatter);
850
857
  let gitCommit;
851
858
  let gitError;
852
- if (autoCommit2) {
859
+ if (commit) {
853
860
  const gitResult = await commitChange(vaultPath2, notePath, "[Crank:FM]");
854
861
  if (gitResult.success) {
855
862
  gitCommit = gitResult.hash;
@@ -883,7 +890,7 @@ function registerFrontmatterTools(server2, vaultPath2, autoCommit2) {
883
890
  import { z as z4 } from "zod";
884
891
  import fs5 from "fs/promises";
885
892
  import path6 from "path";
886
- function registerNoteTools(server2, vaultPath2, autoCommit2) {
893
+ function registerNoteTools(server2, vaultPath2) {
887
894
  server2.tool(
888
895
  "vault_create_note",
889
896
  "Create a new note in the vault with optional frontmatter and content",
@@ -891,9 +898,10 @@ function registerNoteTools(server2, vaultPath2, autoCommit2) {
891
898
  path: z4.string().describe('Vault-relative path for the new note (e.g., "daily-notes/2026-01-28.md")'),
892
899
  content: z4.string().default("").describe("Initial content for the note"),
893
900
  frontmatter: z4.record(z4.any()).default({}).describe("Frontmatter fields (JSON object)"),
894
- overwrite: z4.boolean().default(false).describe("If true, overwrite existing file")
901
+ overwrite: z4.boolean().default(false).describe("If true, overwrite existing file"),
902
+ commit: z4.boolean().default(false).describe("If true, commit this change to git (creates undo point)")
895
903
  },
896
- async ({ path: notePath, content, frontmatter, overwrite }) => {
904
+ async ({ path: notePath, content, frontmatter, overwrite, commit }) => {
897
905
  try {
898
906
  if (!validatePath(vaultPath2, notePath)) {
899
907
  const result2 = {
@@ -921,7 +929,7 @@ function registerNoteTools(server2, vaultPath2, autoCommit2) {
921
929
  await writeVaultFile(vaultPath2, notePath, content, frontmatter);
922
930
  let gitCommit;
923
931
  let gitError;
924
- if (autoCommit2) {
932
+ if (commit) {
925
933
  const gitResult = await commitChange(vaultPath2, notePath, "[Crank:Create]");
926
934
  if (gitResult.success) {
927
935
  gitCommit = gitResult.hash;
@@ -954,9 +962,10 @@ Content length: ${content.length} chars`,
954
962
  "Delete a note from the vault",
955
963
  {
956
964
  path: z4.string().describe("Vault-relative path to the note to delete"),
957
- confirm: z4.boolean().default(false).describe("Must be true to confirm deletion")
965
+ confirm: z4.boolean().default(false).describe("Must be true to confirm deletion"),
966
+ commit: z4.boolean().default(false).describe("If true, commit this change to git (creates undo point)")
958
967
  },
959
- async ({ path: notePath, confirm }) => {
968
+ async ({ path: notePath, confirm, commit }) => {
960
969
  try {
961
970
  if (!confirm) {
962
971
  const result2 = {
@@ -988,7 +997,7 @@ Content length: ${content.length} chars`,
988
997
  await fs5.unlink(fullPath);
989
998
  let gitCommit;
990
999
  let gitError;
991
- if (autoCommit2) {
1000
+ if (commit) {
992
1001
  const gitResult = await commitChange(vaultPath2, notePath, "[Crank:Delete]");
993
1002
  if (gitResult.success) {
994
1003
  gitCommit = gitResult.hash;
@@ -1021,7 +1030,7 @@ Content length: ${content.length} chars`,
1021
1030
  import { z as z5 } from "zod";
1022
1031
  import fs6 from "fs/promises";
1023
1032
  import path7 from "path";
1024
- function registerSystemTools(server2, vaultPath2, autoCommit2) {
1033
+ function registerSystemTools(server2, vaultPath2) {
1025
1034
  server2.tool(
1026
1035
  "vault_list_sections",
1027
1036
  "List all sections (headings) in a markdown note",
@@ -1107,14 +1116,6 @@ Date: ${lastCommit.date}`
1107
1116
  };
1108
1117
  return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
1109
1118
  }
1110
- if (!autoCommit2) {
1111
- const result2 = {
1112
- success: false,
1113
- message: 'Undo is only available when AUTO_COMMIT is enabled. Set AUTO_COMMIT="true" in .mcp.json to enable automatic git commits for each mutation.',
1114
- path: ""
1115
- };
1116
- return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
1117
- }
1118
1119
  const isRepo = await isGitRepo(vaultPath2);
1119
1120
  if (!isRepo) {
1120
1121
  const result2 = {
@@ -1181,15 +1182,13 @@ var server = new McpServer({
1181
1182
  version: "0.1.0"
1182
1183
  });
1183
1184
  var vaultPath = process.env.PROJECT_PATH || findVaultRoot();
1184
- var autoCommit = process.env.AUTO_COMMIT === "true";
1185
1185
  console.error(`[Crank] Starting Flywheel Crank MCP server`);
1186
1186
  console.error(`[Crank] Vault path: ${vaultPath}`);
1187
- console.error(`[Crank] Auto-commit: ${autoCommit}`);
1188
- registerMutationTools(server, vaultPath, autoCommit);
1189
- registerTaskTools(server, vaultPath, autoCommit);
1190
- registerFrontmatterTools(server, vaultPath, autoCommit);
1191
- registerNoteTools(server, vaultPath, autoCommit);
1192
- registerSystemTools(server, vaultPath, autoCommit);
1187
+ registerMutationTools(server, vaultPath);
1188
+ registerTaskTools(server, vaultPath);
1189
+ registerFrontmatterTools(server, vaultPath);
1190
+ registerNoteTools(server, vaultPath);
1191
+ registerSystemTools(server, vaultPath);
1193
1192
  var transport = new StdioServerTransport();
1194
1193
  await server.connect(transport);
1195
1194
  console.error(`[Crank] Flywheel Crank MCP server started successfully`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velvetmonkey/flywheel-crank",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Deterministic vault mutations for Obsidian via MCP",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -15,7 +15,7 @@
15
15
  "url": "https://github.com/velvetmonkey/flywheel-crank/issues"
16
16
  },
17
17
  "homepage": "https://github.com/velvetmonkey/flywheel-crank#readme",
18
- "author": "Ben Cassie",
18
+ "author": "velvetmonkey",
19
19
  "keywords": ["obsidian", "mcp", "model-context-protocol", "claude", "mutations", "vault-writes", "markdown"],
20
20
  "scripts": {
21
21
  "build": "npx esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --packages=external && chmod +x dist/index.js",