mulch-cli 0.4.3 → 0.6.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 (193) hide show
  1. package/README.md +24 -4
  2. package/package.json +11 -16
  3. package/src/api.ts +310 -0
  4. package/src/cli.ts +54 -0
  5. package/src/commands/add.ts +61 -0
  6. package/src/commands/compact.ts +924 -0
  7. package/src/commands/delete.ts +103 -0
  8. package/src/commands/diff.ts +209 -0
  9. package/src/commands/doctor.ts +586 -0
  10. package/src/commands/edit.ts +253 -0
  11. package/src/commands/init.ts +33 -0
  12. package/src/commands/learn.ts +170 -0
  13. package/src/commands/onboard.ts +362 -0
  14. package/src/commands/prime.ts +327 -0
  15. package/src/commands/prune.ts +128 -0
  16. package/src/commands/query.ts +177 -0
  17. package/src/commands/ready.ts +194 -0
  18. package/src/commands/record.ts +959 -0
  19. package/src/commands/search.ts +234 -0
  20. package/src/commands/setup.ts +823 -0
  21. package/src/commands/status.ts +83 -0
  22. package/src/commands/sync.ts +224 -0
  23. package/src/commands/update.ts +112 -0
  24. package/src/commands/validate.ts +107 -0
  25. package/src/index.ts +50 -0
  26. package/src/schemas/config.ts +31 -0
  27. package/src/schemas/index.ts +18 -0
  28. package/src/schemas/record-schema.ts +177 -0
  29. package/src/schemas/record.ts +83 -0
  30. package/src/utils/bm25.ts +243 -0
  31. package/src/utils/budget.ts +157 -0
  32. package/src/utils/config.ts +117 -0
  33. package/src/utils/expertise.ts +379 -0
  34. package/src/utils/format.ts +767 -0
  35. package/src/utils/git.ts +89 -0
  36. package/src/utils/index.ts +54 -0
  37. package/src/utils/json-output.ts +13 -0
  38. package/src/utils/lock.ts +82 -0
  39. package/src/utils/markers.ts +51 -0
  40. package/src/utils/scoring.ts +101 -0
  41. package/src/utils/version.ts +46 -0
  42. package/dist/cli.d.ts +0 -3
  43. package/dist/cli.d.ts.map +0 -1
  44. package/dist/cli.js +0 -50
  45. package/dist/cli.js.map +0 -1
  46. package/dist/commands/add.d.ts +0 -3
  47. package/dist/commands/add.d.ts.map +0 -1
  48. package/dist/commands/add.js +0 -47
  49. package/dist/commands/add.js.map +0 -1
  50. package/dist/commands/compact.d.ts +0 -5
  51. package/dist/commands/compact.d.ts.map +0 -1
  52. package/dist/commands/compact.js +0 -709
  53. package/dist/commands/compact.js.map +0 -1
  54. package/dist/commands/delete.d.ts +0 -3
  55. package/dist/commands/delete.d.ts.map +0 -1
  56. package/dist/commands/delete.js +0 -82
  57. package/dist/commands/delete.js.map +0 -1
  58. package/dist/commands/diff.d.ts +0 -11
  59. package/dist/commands/diff.d.ts.map +0 -1
  60. package/dist/commands/diff.js +0 -170
  61. package/dist/commands/diff.js.map +0 -1
  62. package/dist/commands/doctor.d.ts +0 -3
  63. package/dist/commands/doctor.d.ts.map +0 -1
  64. package/dist/commands/doctor.js +0 -391
  65. package/dist/commands/doctor.js.map +0 -1
  66. package/dist/commands/edit.d.ts +0 -3
  67. package/dist/commands/edit.d.ts.map +0 -1
  68. package/dist/commands/edit.js +0 -210
  69. package/dist/commands/edit.js.map +0 -1
  70. package/dist/commands/init.d.ts +0 -3
  71. package/dist/commands/init.d.ts.map +0 -1
  72. package/dist/commands/init.js +0 -30
  73. package/dist/commands/init.js.map +0 -1
  74. package/dist/commands/learn.d.ts +0 -12
  75. package/dist/commands/learn.d.ts.map +0 -1
  76. package/dist/commands/learn.js +0 -130
  77. package/dist/commands/learn.js.map +0 -1
  78. package/dist/commands/onboard.d.ts +0 -10
  79. package/dist/commands/onboard.d.ts.map +0 -1
  80. package/dist/commands/onboard.js +0 -286
  81. package/dist/commands/onboard.js.map +0 -1
  82. package/dist/commands/prime.d.ts +0 -3
  83. package/dist/commands/prime.d.ts.map +0 -1
  84. package/dist/commands/prime.js +0 -242
  85. package/dist/commands/prime.js.map +0 -1
  86. package/dist/commands/prune.d.ts +0 -8
  87. package/dist/commands/prune.d.ts.map +0 -1
  88. package/dist/commands/prune.js +0 -90
  89. package/dist/commands/prune.js.map +0 -1
  90. package/dist/commands/query.d.ts +0 -3
  91. package/dist/commands/query.d.ts.map +0 -1
  92. package/dist/commands/query.js +0 -118
  93. package/dist/commands/query.js.map +0 -1
  94. package/dist/commands/ready.d.ts +0 -3
  95. package/dist/commands/ready.d.ts.map +0 -1
  96. package/dist/commands/ready.js +0 -160
  97. package/dist/commands/ready.js.map +0 -1
  98. package/dist/commands/record.d.ts +0 -13
  99. package/dist/commands/record.d.ts.map +0 -1
  100. package/dist/commands/record.js +0 -688
  101. package/dist/commands/record.js.map +0 -1
  102. package/dist/commands/search.d.ts +0 -3
  103. package/dist/commands/search.d.ts.map +0 -1
  104. package/dist/commands/search.js +0 -163
  105. package/dist/commands/search.js.map +0 -1
  106. package/dist/commands/setup.d.ts +0 -29
  107. package/dist/commands/setup.d.ts.map +0 -1
  108. package/dist/commands/setup.js +0 -548
  109. package/dist/commands/setup.js.map +0 -1
  110. package/dist/commands/status.d.ts +0 -3
  111. package/dist/commands/status.d.ts.map +0 -1
  112. package/dist/commands/status.js +0 -61
  113. package/dist/commands/status.js.map +0 -1
  114. package/dist/commands/sync.d.ts +0 -3
  115. package/dist/commands/sync.d.ts.map +0 -1
  116. package/dist/commands/sync.js +0 -176
  117. package/dist/commands/sync.js.map +0 -1
  118. package/dist/commands/update.d.ts +0 -3
  119. package/dist/commands/update.d.ts.map +0 -1
  120. package/dist/commands/update.js +0 -72
  121. package/dist/commands/update.js.map +0 -1
  122. package/dist/commands/validate.d.ts +0 -3
  123. package/dist/commands/validate.d.ts.map +0 -1
  124. package/dist/commands/validate.js +0 -86
  125. package/dist/commands/validate.js.map +0 -1
  126. package/dist/index.d.ts +0 -7
  127. package/dist/index.d.ts.map +0 -1
  128. package/dist/index.js +0 -8
  129. package/dist/index.js.map +0 -1
  130. package/dist/schemas/config.d.ts +0 -17
  131. package/dist/schemas/config.d.ts.map +0 -1
  132. package/dist/schemas/config.js +0 -16
  133. package/dist/schemas/config.js.map +0 -1
  134. package/dist/schemas/index.d.ts +0 -5
  135. package/dist/schemas/index.d.ts.map +0 -1
  136. package/dist/schemas/index.js +0 -3
  137. package/dist/schemas/index.js.map +0 -1
  138. package/dist/schemas/record-schema.d.ts +0 -379
  139. package/dist/schemas/record-schema.d.ts.map +0 -1
  140. package/dist/schemas/record-schema.js +0 -148
  141. package/dist/schemas/record-schema.js.map +0 -1
  142. package/dist/schemas/record.d.ts +0 -60
  143. package/dist/schemas/record.d.ts.map +0 -1
  144. package/dist/schemas/record.js +0 -2
  145. package/dist/schemas/record.js.map +0 -1
  146. package/dist/utils/bm25.d.ts +0 -39
  147. package/dist/utils/bm25.d.ts.map +0 -1
  148. package/dist/utils/bm25.js +0 -171
  149. package/dist/utils/bm25.js.map +0 -1
  150. package/dist/utils/budget.d.ts +0 -35
  151. package/dist/utils/budget.d.ts.map +0 -1
  152. package/dist/utils/budget.js +0 -114
  153. package/dist/utils/budget.js.map +0 -1
  154. package/dist/utils/config.d.ts +0 -12
  155. package/dist/utils/config.d.ts.map +0 -1
  156. package/dist/utils/config.js +0 -89
  157. package/dist/utils/config.js.map +0 -1
  158. package/dist/utils/expertise.d.ts +0 -57
  159. package/dist/utils/expertise.d.ts.map +0 -1
  160. package/dist/utils/expertise.js +0 -264
  161. package/dist/utils/expertise.js.map +0 -1
  162. package/dist/utils/format.d.ts +0 -31
  163. package/dist/utils/format.d.ts.map +0 -1
  164. package/dist/utils/format.js +0 -556
  165. package/dist/utils/format.js.map +0 -1
  166. package/dist/utils/git.d.ts +0 -6
  167. package/dist/utils/git.d.ts.map +0 -1
  168. package/dist/utils/git.js +0 -81
  169. package/dist/utils/git.js.map +0 -1
  170. package/dist/utils/index.d.ts +0 -8
  171. package/dist/utils/index.d.ts.map +0 -1
  172. package/dist/utils/index.js +0 -8
  173. package/dist/utils/index.js.map +0 -1
  174. package/dist/utils/json-output.d.ts +0 -8
  175. package/dist/utils/json-output.d.ts.map +0 -1
  176. package/dist/utils/json-output.js +0 -7
  177. package/dist/utils/json-output.js.map +0 -1
  178. package/dist/utils/lock.d.ts +0 -6
  179. package/dist/utils/lock.d.ts.map +0 -1
  180. package/dist/utils/lock.js +0 -70
  181. package/dist/utils/lock.js.map +0 -1
  182. package/dist/utils/markers.d.ts +0 -22
  183. package/dist/utils/markers.d.ts.map +0 -1
  184. package/dist/utils/markers.js +0 -42
  185. package/dist/utils/markers.js.map +0 -1
  186. package/dist/utils/scoring.d.ts +0 -73
  187. package/dist/utils/scoring.d.ts.map +0 -1
  188. package/dist/utils/scoring.js +0 -80
  189. package/dist/utils/scoring.js.map +0 -1
  190. package/dist/utils/version.d.ts +0 -15
  191. package/dist/utils/version.d.ts.map +0 -1
  192. package/dist/utils/version.js +0 -48
  193. package/dist/utils/version.js.map +0 -1
@@ -0,0 +1,823 @@
1
+ import { existsSync } from "node:fs";
2
+ import {
3
+ chmod,
4
+ mkdir,
5
+ readFile,
6
+ stat,
7
+ unlink,
8
+ writeFile,
9
+ } from "node:fs/promises";
10
+ import { dirname, join } from "node:path";
11
+ import chalk from "chalk";
12
+ import type { Command } from "commander";
13
+ import { getMulchDir } from "../utils/config.ts";
14
+ import { outputJson, outputJsonError } from "../utils/json-output.ts";
15
+ import {
16
+ MARKER_END,
17
+ MARKER_START,
18
+ hasMarkerSection,
19
+ removeMarkerSection,
20
+ } from "../utils/markers.ts";
21
+
22
+ /** Supported provider names. */
23
+ const SUPPORTED_PROVIDERS = [
24
+ "claude",
25
+ "cursor",
26
+ "codex",
27
+ "gemini",
28
+ "windsurf",
29
+ "aider",
30
+ ] as const;
31
+
32
+ type Provider = (typeof SUPPORTED_PROVIDERS)[number];
33
+
34
+ function isProvider(value: string): value is Provider {
35
+ return (SUPPORTED_PROVIDERS as readonly string[]).includes(value);
36
+ }
37
+
38
+ /** Result of a provider recipe operation. */
39
+ interface RecipeResult {
40
+ success: boolean;
41
+ message: string;
42
+ }
43
+
44
+ // ────────────────────────────────────────────────────────────
45
+ // Git hook helpers
46
+ // ────────────────────────────────────────────────────────────
47
+
48
+ const HOOK_MARKER_START = "# mulch:start";
49
+ const HOOK_MARKER_END = "# mulch:end";
50
+
51
+ const MULCH_HOOK_SECTION = `${HOOK_MARKER_START}
52
+ # Run mulch validate before committing
53
+ if command -v mulch >/dev/null 2>&1; then
54
+ mulch validate
55
+ if [ $? -ne 0 ]; then
56
+ echo "mulch validate failed. Commit aborted."
57
+ exit 1
58
+ fi
59
+ fi
60
+ ${HOOK_MARKER_END}`;
61
+
62
+ async function installGitHook(cwd: string): Promise<RecipeResult> {
63
+ const gitDir = join(cwd, ".git");
64
+ if (!existsSync(gitDir)) {
65
+ return {
66
+ success: false,
67
+ message: "Not a git repository — .git directory not found.",
68
+ };
69
+ }
70
+
71
+ const hooksDir = join(gitDir, "hooks");
72
+ await mkdir(hooksDir, { recursive: true });
73
+
74
+ const hookPath = join(hooksDir, "pre-commit");
75
+ let content = "";
76
+
77
+ if (existsSync(hookPath)) {
78
+ content = await readFile(hookPath, "utf-8");
79
+ if (content.includes(HOOK_MARKER_START)) {
80
+ return {
81
+ success: true,
82
+ message: "Git pre-commit hook already installed.",
83
+ };
84
+ }
85
+ }
86
+
87
+ if (content) {
88
+ content = `${content.trimEnd()}\n\n${MULCH_HOOK_SECTION}\n`;
89
+ } else {
90
+ content = `#!/bin/sh\n\n${MULCH_HOOK_SECTION}\n`;
91
+ }
92
+
93
+ await writeFile(hookPath, content, "utf-8");
94
+ await chmod(hookPath, 0o755);
95
+
96
+ return { success: true, message: "Installed mulch pre-commit git hook." };
97
+ }
98
+
99
+ async function checkGitHook(cwd: string): Promise<RecipeResult> {
100
+ const hookPath = join(cwd, ".git", "hooks", "pre-commit");
101
+ if (!existsSync(hookPath)) {
102
+ return { success: false, message: "Git pre-commit hook not found." };
103
+ }
104
+
105
+ const content = await readFile(hookPath, "utf-8");
106
+ if (!content.includes(HOOK_MARKER_START)) {
107
+ return {
108
+ success: false,
109
+ message: "Git pre-commit hook exists but has no mulch section.",
110
+ };
111
+ }
112
+
113
+ return { success: true, message: "Git pre-commit hook is installed." };
114
+ }
115
+
116
+ async function removeGitHook(cwd: string): Promise<RecipeResult> {
117
+ const hookPath = join(cwd, ".git", "hooks", "pre-commit");
118
+ if (!existsSync(hookPath)) {
119
+ return {
120
+ success: true,
121
+ message: "Git pre-commit hook not found; nothing to remove.",
122
+ };
123
+ }
124
+
125
+ const content = await readFile(hookPath, "utf-8");
126
+ if (!content.includes(HOOK_MARKER_START)) {
127
+ return {
128
+ success: true,
129
+ message: "No mulch section in pre-commit hook; nothing to remove.",
130
+ };
131
+ }
132
+
133
+ const startIdx = content.indexOf(HOOK_MARKER_START);
134
+ const endIdx = content.indexOf(HOOK_MARKER_END);
135
+ const before = content.substring(0, startIdx);
136
+ const after = content.substring(endIdx + HOOK_MARKER_END.length);
137
+ const cleaned = (before + after).replace(/\n{3,}/g, "\n\n").trim();
138
+
139
+ // If only the shebang (or nothing) remains, delete the file
140
+ if (!cleaned || cleaned === "#!/bin/sh") {
141
+ await unlink(hookPath);
142
+ return {
143
+ success: true,
144
+ message: "Removed mulch pre-commit hook (file deleted).",
145
+ };
146
+ }
147
+
148
+ await writeFile(hookPath, `${cleaned}\n`, "utf-8");
149
+ return {
150
+ success: true,
151
+ message: "Removed mulch section from pre-commit hook.",
152
+ };
153
+ }
154
+
155
+ // ────────────────────────────────────────────────────────────
156
+ // Provider recipes
157
+ // ────────────────────────────────────────────────────────────
158
+
159
+ interface ProviderRecipe {
160
+ /** Install the integration (idempotent). */
161
+ install(cwd: string): Promise<RecipeResult>;
162
+ /** Check whether the integration is installed. */
163
+ check(cwd: string): Promise<RecipeResult>;
164
+ /** Remove the integration. */
165
+ remove(cwd: string): Promise<RecipeResult>;
166
+ }
167
+
168
+ // ── Claude ──────────────────────────────────────────────────
169
+
170
+ interface ClaudeHookEntry {
171
+ type: string;
172
+ command: string;
173
+ }
174
+
175
+ interface ClaudeHookGroup {
176
+ matcher: string;
177
+ hooks: ClaudeHookEntry[];
178
+ }
179
+
180
+ interface ClaudeSettings {
181
+ hooks?: {
182
+ [event: string]: ClaudeHookGroup[];
183
+ };
184
+ [key: string]: unknown;
185
+ }
186
+
187
+ const CLAUDE_HOOK_COMMAND = "mulch prime";
188
+
189
+ function claudeSettingsPath(cwd: string): string {
190
+ return join(cwd, ".claude", "settings.json");
191
+ }
192
+
193
+ function hasMulchHook(groups: ClaudeHookGroup[]): boolean {
194
+ return groups.some((g) =>
195
+ g.hooks.some((h) => h.command === CLAUDE_HOOK_COMMAND),
196
+ );
197
+ }
198
+
199
+ function removeMulchHookGroups(groups: ClaudeHookGroup[]): ClaudeHookGroup[] {
200
+ return groups.filter(
201
+ (g) => !g.hooks.some((h) => h.command === CLAUDE_HOOK_COMMAND),
202
+ );
203
+ }
204
+
205
+ function createMulchHookGroup(): ClaudeHookGroup {
206
+ return {
207
+ matcher: "",
208
+ hooks: [{ type: "command", command: CLAUDE_HOOK_COMMAND }],
209
+ };
210
+ }
211
+
212
+ const claudeRecipe: ProviderRecipe = {
213
+ async install(cwd) {
214
+ const settingsPath = claudeSettingsPath(cwd);
215
+ let settings: ClaudeSettings = {};
216
+
217
+ if (existsSync(settingsPath)) {
218
+ const raw = await readFile(settingsPath, "utf-8");
219
+ settings = JSON.parse(raw) as ClaudeSettings;
220
+ }
221
+
222
+ if (!settings.hooks) {
223
+ settings.hooks = {};
224
+ }
225
+
226
+ const events = ["SessionStart", "PreCompact"];
227
+ let alreadyInstalled = true;
228
+
229
+ for (const event of events) {
230
+ if (!settings.hooks[event]) {
231
+ settings.hooks[event] = [];
232
+ }
233
+ if (!hasMulchHook(settings.hooks[event])) {
234
+ settings.hooks[event].push(createMulchHookGroup());
235
+ alreadyInstalled = false;
236
+ }
237
+ }
238
+
239
+ if (alreadyInstalled) {
240
+ return { success: true, message: "Claude hooks already installed." };
241
+ }
242
+
243
+ await mkdir(dirname(settingsPath), { recursive: true });
244
+ await writeFile(
245
+ settingsPath,
246
+ `${JSON.stringify(settings, null, 2)}\n`,
247
+ "utf-8",
248
+ );
249
+
250
+ return {
251
+ success: true,
252
+ message: "Installed Claude hooks for SessionStart and PreCompact.",
253
+ };
254
+ },
255
+
256
+ async check(cwd) {
257
+ const settingsPath = claudeSettingsPath(cwd);
258
+ if (!existsSync(settingsPath)) {
259
+ return { success: false, message: "Claude settings.json not found." };
260
+ }
261
+
262
+ const raw = await readFile(settingsPath, "utf-8");
263
+ const settings = JSON.parse(raw) as ClaudeSettings;
264
+
265
+ if (!settings.hooks) {
266
+ return {
267
+ success: false,
268
+ message: "No hooks configured in Claude settings.",
269
+ };
270
+ }
271
+
272
+ const events = ["SessionStart", "PreCompact"];
273
+ const missing: string[] = [];
274
+ for (const event of events) {
275
+ if (!settings.hooks[event] || !hasMulchHook(settings.hooks[event])) {
276
+ missing.push(event);
277
+ }
278
+ }
279
+
280
+ if (missing.length > 0) {
281
+ return {
282
+ success: false,
283
+ message: `Missing hooks for: ${missing.join(", ")}.`,
284
+ };
285
+ }
286
+ return {
287
+ success: true,
288
+ message: "Claude hooks are installed and correct.",
289
+ };
290
+ },
291
+
292
+ async remove(cwd) {
293
+ const settingsPath = claudeSettingsPath(cwd);
294
+ if (!existsSync(settingsPath)) {
295
+ return {
296
+ success: true,
297
+ message: "Claude settings.json not found; nothing to remove.",
298
+ };
299
+ }
300
+
301
+ const raw = await readFile(settingsPath, "utf-8");
302
+ const settings = JSON.parse(raw) as ClaudeSettings;
303
+
304
+ if (!settings.hooks) {
305
+ return {
306
+ success: true,
307
+ message: "No hooks in Claude settings; nothing to remove.",
308
+ };
309
+ }
310
+
311
+ let removed = false;
312
+ for (const event of Object.keys(settings.hooks)) {
313
+ const before = settings.hooks[event].length;
314
+ settings.hooks[event] = removeMulchHookGroups(settings.hooks[event]);
315
+ if (settings.hooks[event].length < before) {
316
+ removed = true;
317
+ }
318
+ if (settings.hooks[event].length === 0) {
319
+ delete settings.hooks[event];
320
+ }
321
+ }
322
+
323
+ if (Object.keys(settings.hooks).length === 0) {
324
+ settings.hooks = undefined;
325
+ }
326
+
327
+ await writeFile(
328
+ settingsPath,
329
+ `${JSON.stringify(settings, null, 2)}\n`,
330
+ "utf-8",
331
+ );
332
+
333
+ return {
334
+ success: true,
335
+ message: removed
336
+ ? "Removed mulch hooks from Claude settings."
337
+ : "No mulch hooks found in Claude settings.",
338
+ };
339
+ },
340
+ };
341
+
342
+ // ── Cursor ──────────────────────────────────────────────────
343
+
344
+ function cursorRulePath(cwd: string): string {
345
+ return join(cwd, ".cursor", "rules", "mulch.mdc");
346
+ }
347
+
348
+ const CURSOR_RULE_CONTENT = `---
349
+ description: Mulch expertise integration
350
+ globs: *
351
+ alwaysApply: true
352
+ ---
353
+
354
+ # Mulch Expertise
355
+
356
+ At the start of every session, run the following command to load project expertise:
357
+
358
+ \`\`\`
359
+ mulch prime
360
+ \`\`\`
361
+
362
+ This injects project-specific conventions, patterns, decisions, and other learnings into your context.
363
+ Use \`mulch prime --files src/foo.ts\` to load only records relevant to specific files.
364
+
365
+ **Before completing your task**, review your work for insights worth preserving — conventions discovered,
366
+ patterns applied, failures encountered, or decisions made — and record them:
367
+
368
+ \`\`\`
369
+ mulch record <domain> --type <convention|pattern|failure|decision|reference|guide> [options]
370
+ \`\`\`
371
+
372
+ Link evidence: \`--evidence-commit <sha>\`, \`--evidence-bead <id>\`
373
+
374
+ **Before you finish**, run:
375
+
376
+ \`\`\`
377
+ mulch learn # see what files changed — decide what to record
378
+ mulch record ... # record learnings
379
+ mulch sync # validate, stage, and commit .mulch/ changes
380
+ \`\`\`
381
+ `;
382
+
383
+ const cursorRecipe: ProviderRecipe = {
384
+ async install(cwd) {
385
+ const rulePath = cursorRulePath(cwd);
386
+
387
+ if (existsSync(rulePath)) {
388
+ const existing = await readFile(rulePath, "utf-8");
389
+ if (existing === CURSOR_RULE_CONTENT) {
390
+ return { success: true, message: "Cursor rule already installed." };
391
+ }
392
+ }
393
+
394
+ await mkdir(dirname(rulePath), { recursive: true });
395
+ await writeFile(rulePath, CURSOR_RULE_CONTENT, "utf-8");
396
+
397
+ return {
398
+ success: true,
399
+ message: "Installed Cursor rule at .cursor/rules/mulch.mdc.",
400
+ };
401
+ },
402
+
403
+ async check(cwd) {
404
+ const rulePath = cursorRulePath(cwd);
405
+ if (!existsSync(rulePath)) {
406
+ return { success: false, message: "Cursor rule file not found." };
407
+ }
408
+ const content = await readFile(rulePath, "utf-8");
409
+ if (content !== CURSOR_RULE_CONTENT) {
410
+ return {
411
+ success: false,
412
+ message: "Cursor rule file exists but has been modified.",
413
+ };
414
+ }
415
+ return { success: true, message: "Cursor rule is installed and correct." };
416
+ },
417
+
418
+ async remove(cwd) {
419
+ const rulePath = cursorRulePath(cwd);
420
+ if (!existsSync(rulePath)) {
421
+ return {
422
+ success: true,
423
+ message: "Cursor rule not found; nothing to remove.",
424
+ };
425
+ }
426
+ await unlink(rulePath);
427
+ return { success: true, message: "Removed Cursor rule file." };
428
+ },
429
+ };
430
+
431
+ // ── Codex ───────────────────────────────────────────────────
432
+
433
+ function codexAgentsPath(cwd: string): string {
434
+ return join(cwd, "AGENTS.md");
435
+ }
436
+
437
+ const CODEX_SECTION = `${MARKER_START}
438
+ ## Mulch Expertise
439
+
440
+ At the start of every session, run \`mulch prime\` to load project expertise.
441
+
442
+ This injects project-specific conventions, patterns, decisions, and other learnings into your context.
443
+ Use \`mulch prime --files src/foo.ts\` to load only records relevant to specific files.
444
+
445
+ **Before completing your task**, review your work for insights worth preserving — conventions discovered,
446
+ patterns applied, failures encountered, or decisions made — and record them:
447
+
448
+ \`\`\`
449
+ mulch record <domain> --type <convention|pattern|failure|decision|reference|guide> [options]
450
+ \`\`\`
451
+
452
+ Link evidence: \`--evidence-commit <sha>\`, \`--evidence-bead <id>\`
453
+
454
+ **Before you finish**, run:
455
+
456
+ \`\`\`
457
+ mulch learn # see what files changed — decide what to record
458
+ mulch record ... # record learnings
459
+ mulch sync # validate, stage, and commit .mulch/ changes
460
+ \`\`\`
461
+ ${MARKER_END}`;
462
+
463
+ const codexRecipe: ProviderRecipe = {
464
+ async install(cwd) {
465
+ const agentsPath = codexAgentsPath(cwd);
466
+ let content = "";
467
+
468
+ if (existsSync(agentsPath)) {
469
+ content = await readFile(agentsPath, "utf-8");
470
+ if (hasMarkerSection(content)) {
471
+ return {
472
+ success: true,
473
+ message: "AGENTS.md already contains mulch section.",
474
+ };
475
+ }
476
+ }
477
+
478
+ const newContent = content
479
+ ? `${content.trimEnd()}\n\n${CODEX_SECTION}\n`
480
+ : `${CODEX_SECTION}\n`;
481
+
482
+ await writeFile(agentsPath, newContent, "utf-8");
483
+
484
+ return { success: true, message: "Added mulch section to AGENTS.md." };
485
+ },
486
+
487
+ async check(cwd) {
488
+ const agentsPath = codexAgentsPath(cwd);
489
+ if (!existsSync(agentsPath)) {
490
+ return { success: false, message: "AGENTS.md not found." };
491
+ }
492
+ const content = await readFile(agentsPath, "utf-8");
493
+ if (!hasMarkerSection(content)) {
494
+ return {
495
+ success: false,
496
+ message: "AGENTS.md exists but has no mulch section.",
497
+ };
498
+ }
499
+ return { success: true, message: "AGENTS.md contains mulch section." };
500
+ },
501
+
502
+ async remove(cwd) {
503
+ const agentsPath = codexAgentsPath(cwd);
504
+ if (!existsSync(agentsPath)) {
505
+ return {
506
+ success: true,
507
+ message: "AGENTS.md not found; nothing to remove.",
508
+ };
509
+ }
510
+ const content = await readFile(agentsPath, "utf-8");
511
+ if (!hasMarkerSection(content)) {
512
+ return {
513
+ success: true,
514
+ message: "No mulch section in AGENTS.md; nothing to remove.",
515
+ };
516
+ }
517
+ const cleaned = removeMarkerSection(content);
518
+ await writeFile(agentsPath, cleaned, "utf-8");
519
+ return { success: true, message: "Removed mulch section from AGENTS.md." };
520
+ },
521
+ };
522
+
523
+ // ── Generic markdown-file recipe (gemini, windsurf, aider) ─
524
+
525
+ interface MarkdownRecipeConfig {
526
+ filePath: (cwd: string) => string;
527
+ fileName: string;
528
+ }
529
+
530
+ function createMarkdownRecipe(config: MarkdownRecipeConfig): ProviderRecipe {
531
+ const section = `${MARKER_START}
532
+ ## Mulch Expertise
533
+
534
+ At the start of every session, run \`mulch prime\` to load project expertise.
535
+
536
+ This injects project-specific conventions, patterns, decisions, and other learnings into your context.
537
+ Use \`mulch prime --files src/foo.ts\` to load only records relevant to specific files.
538
+
539
+ **Before completing your task**, review your work for insights worth preserving — conventions discovered,
540
+ patterns applied, failures encountered, or decisions made — and record them:
541
+
542
+ \`\`\`
543
+ mulch record <domain> --type <convention|pattern|failure|decision|reference|guide> [options]
544
+ \`\`\`
545
+
546
+ Link evidence: \`--evidence-commit <sha>\`, \`--evidence-bead <id>\`
547
+
548
+ **Before you finish**, run:
549
+
550
+ \`\`\`
551
+ mulch learn # see what files changed — decide what to record
552
+ mulch record ... # record learnings
553
+ mulch sync # validate, stage, and commit .mulch/ changes
554
+ \`\`\`
555
+ ${MARKER_END}`;
556
+
557
+ return {
558
+ async install(cwd) {
559
+ const filePath = config.filePath(cwd);
560
+ let content = "";
561
+
562
+ if (existsSync(filePath)) {
563
+ content = await readFile(filePath, "utf-8");
564
+ if (hasMarkerSection(content)) {
565
+ return {
566
+ success: true,
567
+ message: `${config.fileName} already contains mulch section.`,
568
+ };
569
+ }
570
+ }
571
+
572
+ const newContent = content
573
+ ? `${content.trimEnd()}\n\n${section}\n`
574
+ : `${section}\n`;
575
+
576
+ await mkdir(dirname(filePath), { recursive: true });
577
+ await writeFile(filePath, newContent, "utf-8");
578
+
579
+ return {
580
+ success: true,
581
+ message: `Added mulch section to ${config.fileName}.`,
582
+ };
583
+ },
584
+
585
+ async check(cwd) {
586
+ const filePath = config.filePath(cwd);
587
+ if (!existsSync(filePath)) {
588
+ return { success: false, message: `${config.fileName} not found.` };
589
+ }
590
+ const content = await readFile(filePath, "utf-8");
591
+ if (!hasMarkerSection(content)) {
592
+ return {
593
+ success: false,
594
+ message: `${config.fileName} exists but has no mulch section.`,
595
+ };
596
+ }
597
+ return {
598
+ success: true,
599
+ message: `${config.fileName} contains mulch section.`,
600
+ };
601
+ },
602
+
603
+ async remove(cwd) {
604
+ const filePath = config.filePath(cwd);
605
+ if (!existsSync(filePath)) {
606
+ return {
607
+ success: true,
608
+ message: `${config.fileName} not found; nothing to remove.`,
609
+ };
610
+ }
611
+ const content = await readFile(filePath, "utf-8");
612
+ if (!hasMarkerSection(content)) {
613
+ return {
614
+ success: true,
615
+ message: `No mulch section in ${config.fileName}; nothing to remove.`,
616
+ };
617
+ }
618
+
619
+ const cleaned = removeMarkerSection(content);
620
+ await writeFile(filePath, cleaned, "utf-8");
621
+ return {
622
+ success: true,
623
+ message: `Removed mulch section from ${config.fileName}.`,
624
+ };
625
+ },
626
+ };
627
+ }
628
+
629
+ const geminiRecipe = createMarkdownRecipe({
630
+ filePath: (cwd) => join(cwd, ".gemini", "settings.md"),
631
+ fileName: ".gemini/settings.md",
632
+ });
633
+
634
+ const windsurfRecipe = createMarkdownRecipe({
635
+ filePath: (cwd) => join(cwd, ".windsurf", "rules.md"),
636
+ fileName: ".windsurf/rules.md",
637
+ });
638
+
639
+ const aiderRecipe = createMarkdownRecipe({
640
+ filePath: (cwd) => join(cwd, ".aider.conf.md"),
641
+ fileName: ".aider.conf.md",
642
+ });
643
+
644
+ // ── Recipe registry ─────────────────────────────────────────
645
+
646
+ const recipes: Record<Provider, ProviderRecipe> = {
647
+ claude: claudeRecipe,
648
+ cursor: cursorRecipe,
649
+ codex: codexRecipe,
650
+ gemini: geminiRecipe,
651
+ windsurf: windsurfRecipe,
652
+ aider: aiderRecipe,
653
+ };
654
+
655
+ // ── Exported helpers for testing ────────────────────────────
656
+
657
+ export {
658
+ recipes,
659
+ SUPPORTED_PROVIDERS,
660
+ CURSOR_RULE_CONTENT,
661
+ CODEX_SECTION,
662
+ CLAUDE_HOOK_COMMAND,
663
+ MULCH_HOOK_SECTION,
664
+ installGitHook,
665
+ checkGitHook,
666
+ removeGitHook,
667
+ };
668
+
669
+ export type { Provider, ProviderRecipe };
670
+
671
+ // ── Command registration ────────────────────────────────────
672
+
673
+ export function registerSetupCommand(program: Command): void {
674
+ program
675
+ .command("setup")
676
+ .argument(
677
+ "[provider]",
678
+ `agent provider (${SUPPORTED_PROVIDERS.join(", ")})`,
679
+ )
680
+ .description("Set up mulch integration for a specific agent provider")
681
+ .option("--check", "verify provider integration is installed")
682
+ .option("--remove", "remove provider integration")
683
+ .option("--hooks", "install a pre-commit git hook running mulch validate")
684
+ .action(
685
+ async (
686
+ provider: string | undefined,
687
+ options: { check?: boolean; remove?: boolean; hooks?: boolean },
688
+ ) => {
689
+ const jsonMode = program.opts().json === true;
690
+
691
+ // Verify .mulch/ exists
692
+ const mulchDir = getMulchDir();
693
+ if (!existsSync(mulchDir)) {
694
+ if (jsonMode) {
695
+ outputJsonError(
696
+ "setup",
697
+ "No .mulch/ directory found. Run `mulch init` first.",
698
+ );
699
+ } else {
700
+ console.error(
701
+ chalk.red(
702
+ "Error: No .mulch/ directory found. Run `mulch init` first.",
703
+ ),
704
+ );
705
+ }
706
+ process.exitCode = 1;
707
+ return;
708
+ }
709
+
710
+ if (!provider && !options.hooks) {
711
+ if (jsonMode) {
712
+ outputJsonError("setup", "Specify a provider or use --hooks.");
713
+ } else {
714
+ console.error(
715
+ chalk.red("Error: specify a provider or use --hooks."),
716
+ );
717
+ }
718
+ process.exitCode = 1;
719
+ return;
720
+ }
721
+
722
+ // Handle --hooks
723
+ if (options.hooks) {
724
+ const cwd = process.cwd();
725
+ let hookResult: RecipeResult;
726
+ const action = options.check
727
+ ? "check"
728
+ : options.remove
729
+ ? "remove"
730
+ : "install";
731
+ if (options.check) {
732
+ hookResult = await checkGitHook(cwd);
733
+ } else if (options.remove) {
734
+ hookResult = await removeGitHook(cwd);
735
+ } else {
736
+ hookResult = await installGitHook(cwd);
737
+ }
738
+
739
+ if (jsonMode) {
740
+ outputJson({
741
+ success: hookResult.success,
742
+ command: "setup",
743
+ target: "hooks",
744
+ action,
745
+ message: hookResult.message,
746
+ });
747
+ } else if (hookResult.success) {
748
+ console.log(chalk.green(`\u2714 ${hookResult.message}`));
749
+ } else {
750
+ console.error(chalk.red(`\u2716 ${hookResult.message}`));
751
+ }
752
+
753
+ if (!hookResult.success) {
754
+ process.exitCode = 1;
755
+ }
756
+
757
+ // If no provider, stop here
758
+ if (!provider) return;
759
+ }
760
+
761
+ // Handle provider
762
+ if (!provider) return;
763
+
764
+ if (!isProvider(provider)) {
765
+ if (jsonMode) {
766
+ outputJsonError(
767
+ "setup",
768
+ `Unknown provider "${provider}". Supported providers: ${SUPPORTED_PROVIDERS.join(", ")}`,
769
+ );
770
+ } else {
771
+ console.error(chalk.red(`Error: unknown provider "${provider}".`));
772
+ console.error(
773
+ chalk.red(
774
+ `Supported providers: ${SUPPORTED_PROVIDERS.join(", ")}`,
775
+ ),
776
+ );
777
+ }
778
+ process.exitCode = 1;
779
+ return;
780
+ }
781
+
782
+ {
783
+ const recipe = recipes[provider];
784
+ const action = options.check
785
+ ? "check"
786
+ : options.remove
787
+ ? "remove"
788
+ : "install";
789
+ let result: RecipeResult;
790
+
791
+ if (options.check) {
792
+ result = await recipe.check(process.cwd());
793
+ } else if (options.remove) {
794
+ result = await recipe.remove(process.cwd());
795
+ } else {
796
+ result = await recipe.install(process.cwd());
797
+ }
798
+
799
+ if (jsonMode) {
800
+ outputJson({
801
+ success: result.success,
802
+ command: "setup",
803
+ provider,
804
+ action,
805
+ message: result.message,
806
+ });
807
+ } else if (result.success) {
808
+ console.log(chalk.green(`\u2714 ${result.message}`));
809
+ } else {
810
+ if (options.check) {
811
+ console.log(chalk.yellow(`\u2716 ${result.message}`));
812
+ } else {
813
+ console.error(chalk.red(`Error: ${result.message}`));
814
+ }
815
+ }
816
+
817
+ if (!result.success) {
818
+ process.exitCode = 1;
819
+ }
820
+ }
821
+ },
822
+ );
823
+ }