@grainulation/wheat 1.0.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 (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +136 -0
  3. package/bin/wheat.js +193 -0
  4. package/compiler/detect-sprints.js +319 -0
  5. package/compiler/generate-manifest.js +280 -0
  6. package/compiler/wheat-compiler.js +1229 -0
  7. package/lib/compiler.js +35 -0
  8. package/lib/connect.js +418 -0
  9. package/lib/disconnect.js +188 -0
  10. package/lib/guard.js +151 -0
  11. package/lib/index.js +14 -0
  12. package/lib/init.js +457 -0
  13. package/lib/install-prompt.js +186 -0
  14. package/lib/quickstart.js +276 -0
  15. package/lib/serve-mcp.js +509 -0
  16. package/lib/server.js +391 -0
  17. package/lib/stats.js +184 -0
  18. package/lib/status.js +135 -0
  19. package/lib/update.js +71 -0
  20. package/package.json +53 -0
  21. package/public/index.html +1798 -0
  22. package/templates/claude.md +122 -0
  23. package/templates/commands/blind-spot.md +47 -0
  24. package/templates/commands/brief.md +73 -0
  25. package/templates/commands/calibrate.md +39 -0
  26. package/templates/commands/challenge.md +72 -0
  27. package/templates/commands/connect.md +104 -0
  28. package/templates/commands/evaluate.md +80 -0
  29. package/templates/commands/feedback.md +60 -0
  30. package/templates/commands/handoff.md +53 -0
  31. package/templates/commands/init.md +68 -0
  32. package/templates/commands/merge.md +51 -0
  33. package/templates/commands/present.md +52 -0
  34. package/templates/commands/prototype.md +68 -0
  35. package/templates/commands/replay.md +61 -0
  36. package/templates/commands/research.md +73 -0
  37. package/templates/commands/resolve.md +42 -0
  38. package/templates/commands/status.md +56 -0
  39. package/templates/commands/witness.md +79 -0
  40. package/templates/explainer.html +343 -0
@@ -0,0 +1,35 @@
1
+ /**
2
+ * wheat compile — delegates to wheat-compiler.js with --dir
3
+ *
4
+ * Instead of duplicating the compiler, we ship the real one and shell out.
5
+ * This ensures the npm package always produces identical output to the
6
+ * standalone compiler.
7
+ *
8
+ * Zero npm dependencies.
9
+ */
10
+
11
+ import path from 'path';
12
+ import { execFileSync } from 'child_process';
13
+ import { fileURLToPath } from 'url';
14
+
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = path.dirname(__filename);
17
+
18
+ const COMPILER_PATH = path.join(__dirname, '..', 'compiler', 'wheat-compiler.js');
19
+
20
+ export async function run(dir, args) {
21
+ // Build argv for the real compiler: --dir <targetDir> + passthrough flags
22
+ const compilerArgs = [COMPILER_PATH, '--dir', dir, ...args];
23
+
24
+ try {
25
+ const result = execFileSync(process.execPath, compilerArgs, {
26
+ cwd: dir,
27
+ timeout: 30_000,
28
+ stdio: ['inherit', 'inherit', 'inherit'],
29
+ });
30
+ } catch (err) {
31
+ // execFileSync throws on non-zero exit — let it propagate
32
+ if (err.status) process.exit(err.status);
33
+ throw err;
34
+ }
35
+ }
package/lib/connect.js ADDED
@@ -0,0 +1,418 @@
1
+ /**
2
+ * wheat connect farmer — Auto-configure Claude Code hooks for Farmer
3
+ *
4
+ * Detects farmer on localhost, writes hooks to project-level
5
+ * .claude/settings.local.json. Atomic writes with lockfile.
6
+ * Zero npm dependencies.
7
+ */
8
+
9
+ import http from 'node:http';
10
+ import https from 'node:https';
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+
14
+ // ─── Constants ─────────────────────────────────────────────────────────────
15
+
16
+ const DEFAULT_PORTS = [9090, 9091];
17
+ const DETECT_TIMEOUT_MS = 2000;
18
+ const VERIFY_TIMEOUT_MS = 5000;
19
+ const LOCK_RETRY_MS = 200;
20
+ const LOCK_MAX_RETRIES = 10;
21
+ const SETTINGS_FILENAME = '.claude/settings.local.json';
22
+
23
+ const HOOK_ENDPOINTS = {
24
+ permission: '/hooks/permission',
25
+ activity: '/hooks/activity',
26
+ notification: '/hooks/notification',
27
+ };
28
+
29
+ // ─── Argument parsing ──────────────────────────────────────────────────────
30
+
31
+ function parseArgs(args) {
32
+ const flags = {};
33
+ for (let i = 0; i < args.length; i++) {
34
+ if (args[i] === '--url' && args[i + 1]) {
35
+ flags.url = args[i + 1]; i++;
36
+ } else if (args[i] === '--port' && args[i + 1]) {
37
+ flags.port = parseInt(args[i + 1], 10); i++;
38
+ } else if (args[i] === '--dry-run') {
39
+ flags.dryRun = true;
40
+ } else if (args[i] === '--force') {
41
+ flags.force = true;
42
+ } else if (args[i] === '--json') {
43
+ flags.json = true;
44
+ } else if (args[i] === '--help' || args[i] === '-h') {
45
+ flags.help = true;
46
+ }
47
+ }
48
+ return flags;
49
+ }
50
+
51
+ // ─── HTTP helpers (zero-dep) ───────────────────────────────────────────────
52
+
53
+ function httpRequest(url, options = {}) {
54
+ return new Promise(resolve => {
55
+ const parsed = new URL(url);
56
+ const client = parsed.protocol === 'https:' ? https : http;
57
+ const timeout = options.timeout || DETECT_TIMEOUT_MS;
58
+
59
+ const req = client.request(parsed, {
60
+ method: options.method || 'GET',
61
+ headers: options.headers || {},
62
+ timeout,
63
+ }, res => {
64
+ let body = '';
65
+ res.on('data', chunk => { body += chunk; });
66
+ res.on('end', () => {
67
+ resolve({ status: res.statusCode, body, error: null });
68
+ });
69
+ });
70
+
71
+ req.on('error', err => resolve({ status: 0, body: '', error: err.message }));
72
+ req.on('timeout', () => { req.destroy(); resolve({ status: 0, body: '', error: 'timeout' }); });
73
+
74
+ if (options.body) {
75
+ req.write(typeof options.body === 'string' ? options.body : JSON.stringify(options.body));
76
+ }
77
+ req.end();
78
+ });
79
+ }
80
+
81
+ // ─── Farmer detection ──────────────────────────────────────────────────────
82
+
83
+ async function probeFarmer(baseUrl) {
84
+ // Primary detection: hit /api/state which is the canonical farmer endpoint
85
+ const stateResp = await httpRequest(baseUrl + '/api/state', { timeout: DETECT_TIMEOUT_MS });
86
+ if (stateResp.error) return { found: false, error: stateResp.error };
87
+
88
+ if (stateResp.status !== 200) {
89
+ // Fallback: try root to see if it's farmer at all
90
+ const rootResp = await httpRequest(baseUrl + '/', { timeout: DETECT_TIMEOUT_MS });
91
+ if (rootResp.error || rootResp.status !== 200) {
92
+ return { found: false, error: `Port responds (HTTP ${stateResp.status}) but /api/state not available` };
93
+ }
94
+ const looksLikeFarmer = rootResp.body.includes('farmer') || rootResp.body.includes('Farmer');
95
+ if (!looksLikeFarmer) {
96
+ return { found: false, error: `Port responds but does not look like Farmer` };
97
+ }
98
+ }
99
+
100
+ // Hook probe: verify the permission endpoint accepts POSTs
101
+ const probePayload = {
102
+ hook_event_name: 'PreToolUse',
103
+ tool_name: '__wheat_connect_probe__',
104
+ tool_input: '{}',
105
+ session_id: 'wheat-connect-probe',
106
+ };
107
+
108
+ const hookResp = await httpRequest(baseUrl + HOOK_ENDPOINTS.permission, {
109
+ method: 'POST',
110
+ headers: { 'Content-Type': 'application/json' },
111
+ body: probePayload,
112
+ timeout: VERIFY_TIMEOUT_MS,
113
+ });
114
+
115
+ if (hookResp.error) {
116
+ return { found: true, verified: false, error: `Farmer found but hook probe failed: ${hookResp.error}` };
117
+ }
118
+
119
+ let isVerified = false;
120
+ try {
121
+ isVerified = !!JSON.parse(hookResp.body).hookSpecificOutput;
122
+ } catch {}
123
+
124
+ return {
125
+ found: true,
126
+ verified: isVerified,
127
+ status: hookResp.status,
128
+ error: isVerified ? null : `Hook endpoint returned unexpected response (HTTP ${hookResp.status})`,
129
+ };
130
+ }
131
+
132
+ async function verifySse(baseUrl) {
133
+ return new Promise(resolve => {
134
+ const parsed = new URL(baseUrl + '/events');
135
+ const req = http.request(parsed, { method: 'GET', timeout: VERIFY_TIMEOUT_MS }, res => {
136
+ const isSSE = res.headers['content-type']?.includes('text/event-stream');
137
+ res.destroy(); // We only need to confirm it opens
138
+ resolve({ ok: isSSE, status: res.statusCode });
139
+ });
140
+ req.on('error', err => resolve({ ok: false, error: err.message }));
141
+ req.on('timeout', () => { req.destroy(); resolve({ ok: false, error: 'timeout' }); });
142
+ req.end();
143
+ });
144
+ }
145
+
146
+ async function detectFarmer(preferredPort) {
147
+ const ports = preferredPort ? [preferredPort] : DEFAULT_PORTS;
148
+ for (const port of ports) {
149
+ const baseUrl = `http://127.0.0.1:${port}`;
150
+ const result = await probeFarmer(baseUrl);
151
+ if (result.found) return { ...result, url: baseUrl, port };
152
+ }
153
+ return { found: false, url: null, port: null, error: 'No farmer server found on default ports' };
154
+ }
155
+
156
+ // ─── Settings file management ──────────────────────────────────────────────
157
+
158
+ function hookCommand(farmerUrl, endpoint) {
159
+ return `cat | curl -s -X POST ${farmerUrl}${endpoint} -H 'Content-Type: application/json' --data-binary @- 2>/dev/null || true`;
160
+ }
161
+
162
+ function buildHooksConfig(farmerUrl) {
163
+ return {
164
+ PreToolUse: [{
165
+ matcher: '',
166
+ hooks: [{ type: 'command', command: hookCommand(farmerUrl, HOOK_ENDPOINTS.permission) }],
167
+ }],
168
+ PostToolUse: [{
169
+ matcher: '',
170
+ hooks: [{ type: 'command', command: hookCommand(farmerUrl, HOOK_ENDPOINTS.activity) }],
171
+ }],
172
+ Notification: [{
173
+ matcher: '',
174
+ hooks: [{ type: 'command', command: hookCommand(farmerUrl, HOOK_ENDPOINTS.notification) }],
175
+ }],
176
+ };
177
+ }
178
+
179
+ function readSettings(settingsPath) {
180
+ try {
181
+ return JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
182
+ } catch (err) {
183
+ if (err.code === 'ENOENT') return {};
184
+ throw new Error(`Cannot parse ${settingsPath}: ${err.message}`);
185
+ }
186
+ }
187
+
188
+ function isFarmerHookEntry(entry) {
189
+ if (!entry.hooks || !Array.isArray(entry.hooks)) return false;
190
+ return entry.hooks.some(h =>
191
+ (h.type === 'command' && h.command && h.command.includes('/hooks/'))
192
+ || (h.type === 'url' && h.url && h.url.includes('/hooks/'))
193
+ );
194
+ }
195
+
196
+ function mergeHooks(existing, farmerHooks) {
197
+ const merged = JSON.parse(JSON.stringify(existing));
198
+ if (!merged.hooks) merged.hooks = {};
199
+
200
+ for (const hookType of Object.keys(farmerHooks)) {
201
+ const existingHooks = merged.hooks[hookType] || [];
202
+ const nonFarmerHooks = existingHooks.filter(entry => !isFarmerHookEntry(entry));
203
+ merged.hooks[hookType] = [...nonFarmerHooks, ...farmerHooks[hookType]];
204
+ }
205
+ return merged;
206
+ }
207
+
208
+ async function writeSettingsAtomic(settingsPath, settings) {
209
+ const lockPath = settingsPath + '.lock';
210
+ const backupPath = settingsPath + '.backup';
211
+ const tmpPath = settingsPath + '.tmp';
212
+
213
+ let lockAcquired = false;
214
+ for (let i = 0; i < LOCK_MAX_RETRIES; i++) {
215
+ try {
216
+ const fd = fs.openSync(lockPath, 'wx');
217
+ fs.writeSync(fd, String(process.pid));
218
+ fs.closeSync(fd);
219
+ lockAcquired = true;
220
+ break;
221
+ } catch (err) {
222
+ if (err.code === 'EEXIST') {
223
+ try {
224
+ const holderPid = parseInt(fs.readFileSync(lockPath, 'utf8').trim(), 10);
225
+ process.kill(holderPid, 0);
226
+ await new Promise(r => setTimeout(r, LOCK_RETRY_MS));
227
+ } catch {
228
+ try { fs.unlinkSync(lockPath); } catch {}
229
+ }
230
+ } else {
231
+ throw err;
232
+ }
233
+ }
234
+ }
235
+
236
+ if (!lockAcquired) {
237
+ throw new Error('Cannot acquire file lock — another process is writing to settings');
238
+ }
239
+
240
+ try {
241
+ if (fs.existsSync(settingsPath)) {
242
+ fs.copyFileSync(settingsPath, backupPath);
243
+ }
244
+ fs.writeFileSync(tmpPath, JSON.stringify(settings, null, 2) + '\n');
245
+ fs.renameSync(tmpPath, settingsPath);
246
+ } finally {
247
+ try { fs.unlinkSync(lockPath); } catch {}
248
+ }
249
+ }
250
+
251
+ // ─── Output formatting ────────────────────────────────────────────────────
252
+
253
+ function printSuccess(farmerUrl, settingsPath, dryRun) {
254
+ console.log();
255
+ console.log(' \x1b[32m\u2713\x1b[0m \x1b[1mFarmer connected\x1b[0m');
256
+ console.log(' \u2500'.repeat(40));
257
+ console.log(` Server: ${farmerUrl}`);
258
+ console.log(` Settings: ${settingsPath}`);
259
+ console.log();
260
+ console.log(' Hooks configured:');
261
+ console.log(` PreToolUse \u2192 ${farmerUrl}/hooks/permission`);
262
+ console.log(` PostToolUse \u2192 ${farmerUrl}/hooks/activity`);
263
+ console.log(` Notification \u2192 ${farmerUrl}/hooks/notification`);
264
+ console.log();
265
+ if (dryRun) {
266
+ console.log(' \x1b[33m(dry run \u2014 no files were modified)\x1b[0m');
267
+ console.log();
268
+ }
269
+ console.log(' What this means:');
270
+ console.log(' Every Claude Code tool call in this project now routes');
271
+ console.log(' through Farmer. You can approve, deny, or monitor');
272
+ console.log(' from your phone or desktop.');
273
+ console.log();
274
+ console.log(' What was verified:');
275
+ console.log(' - Farmer is running and responding to hook probes');
276
+ console.log(' - SSE event stream is available for live monitoring');
277
+ console.log(' - Hooks were merged without overwriting existing settings');
278
+ console.log();
279
+ console.log(' What to do next:');
280
+ console.log(' Open Claude Code in this directory. If Farmer goes down,');
281
+ console.log(' hooks fail silently (|| true) so your workflow is never blocked.');
282
+ console.log();
283
+ }
284
+
285
+ function printNotFound(triedPorts) {
286
+ console.log();
287
+ console.log(' \x1b[31m\u2717\x1b[0m \x1b[1mFarmer not detected\x1b[0m');
288
+ console.log(' \u2500'.repeat(40));
289
+ console.log(` Tried ports: ${triedPorts.join(', ')}`);
290
+ console.log();
291
+ console.log(' To start Farmer:');
292
+ console.log(' npx @grainulation/farmer start');
293
+ console.log();
294
+ console.log(' Or connect to a remote Farmer:');
295
+ console.log(' wheat connect farmer --url https://your-tunnel.trycloudflare.com');
296
+ console.log();
297
+ }
298
+
299
+ // ─── Main ──────────────────────────────────────────────────────────────────
300
+
301
+ export async function run(dir, args) {
302
+ const flags = parseArgs(args || []);
303
+
304
+ if (flags.help) {
305
+ console.log(`
306
+ wheat connect farmer — Auto-configure Claude Code hooks for Farmer
307
+
308
+ Usage:
309
+ wheat connect farmer [options]
310
+
311
+ Options:
312
+ --url <url> Connect to a specific Farmer URL (remote/tunnel)
313
+ --port <port> Try a specific port instead of defaults (9090, 9091)
314
+ --dry-run Show what would be configured without writing
315
+ --force Overwrite existing farmer hooks
316
+ --json Output result as JSON (for scripting)
317
+ --help Show this help
318
+ `);
319
+ return;
320
+ }
321
+
322
+ const targetDir = dir || process.cwd();
323
+ const settingsPath = path.join(targetDir, SETTINGS_FILENAME);
324
+ const settingsDir = path.dirname(settingsPath);
325
+
326
+ if (!fs.existsSync(settingsDir)) {
327
+ fs.mkdirSync(settingsDir, { recursive: true });
328
+ }
329
+
330
+ // Step 1: Detect or connect to farmer
331
+ let farmerUrl;
332
+ let detection;
333
+
334
+ if (flags.url) {
335
+ farmerUrl = flags.url.replace(/\/+$/, '');
336
+ console.log(`\n Connecting to ${farmerUrl}...`);
337
+ detection = await probeFarmer(farmerUrl);
338
+ if (!detection.found) {
339
+ if (flags.json) {
340
+ console.log(JSON.stringify({ success: false, error: detection.error }));
341
+ } else {
342
+ console.log(`\n \x1b[31m\u2717\x1b[0m Cannot reach Farmer at ${farmerUrl}: ${detection.error}\n`);
343
+ }
344
+ process.exit(1);
345
+ }
346
+ } else {
347
+ const ports = flags.port ? [flags.port] : DEFAULT_PORTS;
348
+ console.log(`\n Detecting Farmer on localhost (ports: ${ports.join(', ')})...`);
349
+ detection = await detectFarmer(flags.port);
350
+ if (!detection.found) {
351
+ if (flags.json) {
352
+ console.log(JSON.stringify({ success: false, error: detection.error }));
353
+ } else {
354
+ printNotFound(ports);
355
+ }
356
+ process.exit(1);
357
+ }
358
+ farmerUrl = detection.url;
359
+ }
360
+
361
+ if (!detection.verified) {
362
+ console.log(` \x1b[33m!\x1b[0m Farmer found but hook verification failed.`);
363
+ console.log(` ${detection.error || 'Unknown verification error'}`);
364
+ console.log(` Proceeding with configuration anyway...`);
365
+ } else {
366
+ console.log(` \x1b[32m\u2713\x1b[0m Farmer detected at ${farmerUrl}`);
367
+ }
368
+
369
+ // Verify SSE endpoint is available
370
+ const sseResult = await verifySse(farmerUrl);
371
+ if (sseResult.ok) {
372
+ console.log(` \x1b[32m\u2713\x1b[0m SSE event stream verified at ${farmerUrl}/events`);
373
+ } else {
374
+ console.log(` \x1b[33m!\x1b[0m SSE endpoint not confirmed (${sseResult.error || 'unexpected response'})`);
375
+ console.log(` Live monitoring may not work until Farmer restarts.`);
376
+ }
377
+
378
+ // Step 2: Read existing settings, merge, write
379
+ const existing = readSettings(settingsPath);
380
+
381
+ const hasExistingFarmerHooks = existing.hooks && Object.values(existing.hooks).some(
382
+ entries => Array.isArray(entries) && entries.some(entry => isFarmerHookEntry(entry))
383
+ );
384
+
385
+ if (hasExistingFarmerHooks && !flags.force) {
386
+ if (flags.json) {
387
+ console.log(JSON.stringify({ success: true, alreadyConfigured: true, url: farmerUrl }));
388
+ } else {
389
+ console.log(` \x1b[33m!\x1b[0m Farmer hooks already configured in ${SETTINGS_FILENAME}`);
390
+ console.log(' Use --force to overwrite.');
391
+ }
392
+ return;
393
+ }
394
+
395
+ const farmerHooks = buildHooksConfig(farmerUrl);
396
+ const merged = mergeHooks(existing, farmerHooks);
397
+
398
+ if (flags.dryRun) {
399
+ if (flags.json) {
400
+ console.log(JSON.stringify({ success: true, dryRun: true, url: farmerUrl, settings: merged }));
401
+ } else {
402
+ console.log('\n Would write to: ' + settingsPath);
403
+ console.log();
404
+ console.log(JSON.stringify(merged, null, 2));
405
+ printSuccess(farmerUrl, settingsPath, true);
406
+ }
407
+ return;
408
+ }
409
+
410
+ // Step 3: Write settings atomically
411
+ await writeSettingsAtomic(settingsPath, merged);
412
+
413
+ if (flags.json) {
414
+ console.log(JSON.stringify({ success: true, url: farmerUrl, settingsPath, verified: detection.verified }));
415
+ } else {
416
+ printSuccess(farmerUrl, settingsPath, false);
417
+ }
418
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * wheat disconnect farmer — Remove Farmer hooks from Claude Code settings
3
+ *
4
+ * Reads .claude/settings.local.json, strips any hook entries whose
5
+ * command or URL contains "/hooks/", writes back atomically (tmp + rename).
6
+ * Zero npm dependencies.
7
+ */
8
+
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+
12
+ // ─── Constants ─────────────────────────────────────────────────────────────
13
+
14
+ const SETTINGS_FILENAME = '.claude/settings.local.json';
15
+
16
+ // ─── Argument parsing ──────────────────────────────────────────────────────
17
+
18
+ function parseArgs(args) {
19
+ const flags = {};
20
+ for (let i = 0; i < args.length; i++) {
21
+ if (args[i] === '--dry-run') {
22
+ flags.dryRun = true;
23
+ } else if (args[i] === '--json') {
24
+ flags.json = true;
25
+ } else if (args[i] === '--help' || args[i] === '-h') {
26
+ flags.help = true;
27
+ }
28
+ }
29
+ return flags;
30
+ }
31
+
32
+ // ─── Hook detection (matches connect.js logic) ────────────────────────────
33
+
34
+ function isFarmerHookEntry(entry) {
35
+ if (!entry.hooks || !Array.isArray(entry.hooks)) return false;
36
+ return entry.hooks.some(h =>
37
+ (h.type === 'command' && h.command && h.command.includes('/hooks/'))
38
+ || (h.type === 'url' && h.url && h.url.includes('/hooks/'))
39
+ );
40
+ }
41
+
42
+ // ─── Settings I/O ─────────────────────────────────────────────────────────
43
+
44
+ function readSettings(settingsPath) {
45
+ try {
46
+ return JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
47
+ } catch (err) {
48
+ if (err.code === 'ENOENT') return null;
49
+ throw new Error(`Cannot parse ${settingsPath}: ${err.message}`);
50
+ }
51
+ }
52
+
53
+ function writeSettingsAtomic(settingsPath, settings) {
54
+ const tmpPath = settingsPath + '.tmp';
55
+ fs.writeFileSync(tmpPath, JSON.stringify(settings, null, 2) + '\n');
56
+ fs.renameSync(tmpPath, settingsPath);
57
+ }
58
+
59
+ // ─── Core logic ───────────────────────────────────────────────────────────
60
+
61
+ function findFarmerHooks(settings) {
62
+ const found = [];
63
+ if (!settings || !settings.hooks) return found;
64
+ for (const [hookType, entries] of Object.entries(settings.hooks)) {
65
+ if (!Array.isArray(entries)) continue;
66
+ for (const entry of entries) {
67
+ if (isFarmerHookEntry(entry)) {
68
+ found.push({ hookType, entry });
69
+ }
70
+ }
71
+ }
72
+ return found;
73
+ }
74
+
75
+ function removeFarmerHooks(settings) {
76
+ const cleaned = JSON.parse(JSON.stringify(settings));
77
+ if (!cleaned.hooks) return cleaned;
78
+ for (const hookType of Object.keys(cleaned.hooks)) {
79
+ if (!Array.isArray(cleaned.hooks[hookType])) continue;
80
+ cleaned.hooks[hookType] = cleaned.hooks[hookType].filter(
81
+ entry => !isFarmerHookEntry(entry)
82
+ );
83
+ // Remove empty arrays to keep the file clean
84
+ if (cleaned.hooks[hookType].length === 0) {
85
+ delete cleaned.hooks[hookType];
86
+ }
87
+ }
88
+ // Remove empty hooks object
89
+ if (Object.keys(cleaned.hooks).length === 0) {
90
+ delete cleaned.hooks;
91
+ }
92
+ return cleaned;
93
+ }
94
+
95
+ // ─── Main ──────────────────────────────────────────────────────────────────
96
+
97
+ export async function run(dir, args) {
98
+ const flags = parseArgs(args || []);
99
+
100
+ if (flags.help) {
101
+ console.log(`
102
+ wheat disconnect farmer -- Remove Farmer hooks from Claude Code settings
103
+
104
+ Usage:
105
+ wheat disconnect farmer [options]
106
+
107
+ Options:
108
+ --dry-run Show what would be removed without writing
109
+ --json Output result as JSON (for scripting)
110
+ --help Show this help
111
+
112
+ This removes all hook entries in .claude/settings.local.json whose
113
+ command or URL contains "/hooks/". Other settings are preserved.
114
+ `);
115
+ return;
116
+ }
117
+
118
+ const targetDir = dir || process.cwd();
119
+ const settingsPath = path.join(targetDir, SETTINGS_FILENAME);
120
+
121
+ const settings = readSettings(settingsPath);
122
+
123
+ if (!settings) {
124
+ if (flags.json) {
125
+ console.log(JSON.stringify({ success: true, removed: 0, message: 'No settings file found' }));
126
+ } else {
127
+ console.log('\n No farmer hooks found (settings file does not exist).\n');
128
+ }
129
+ return;
130
+ }
131
+
132
+ const farmerHooks = findFarmerHooks(settings);
133
+
134
+ if (farmerHooks.length === 0) {
135
+ if (flags.json) {
136
+ console.log(JSON.stringify({ success: true, removed: 0, message: 'No farmer hooks found' }));
137
+ } else {
138
+ console.log('\n No farmer hooks found in ' + SETTINGS_FILENAME + '.\n');
139
+ }
140
+ return;
141
+ }
142
+
143
+ // Show what will be removed
144
+ if (flags.dryRun) {
145
+ if (flags.json) {
146
+ console.log(JSON.stringify({
147
+ success: true,
148
+ dryRun: true,
149
+ removed: farmerHooks.length,
150
+ hookTypes: farmerHooks.map(h => h.hookType),
151
+ }));
152
+ } else {
153
+ console.log('\n Would remove ' + farmerHooks.length + ' farmer hook(s):');
154
+ for (const { hookType } of farmerHooks) {
155
+ console.log(' - ' + hookType);
156
+ }
157
+ console.log('\n (dry run -- no files were modified)\n');
158
+ }
159
+ return;
160
+ }
161
+
162
+ // Remove and write
163
+ const cleaned = removeFarmerHooks(settings);
164
+ writeSettingsAtomic(settingsPath, cleaned);
165
+
166
+ if (flags.json) {
167
+ console.log(JSON.stringify({
168
+ success: true,
169
+ removed: farmerHooks.length,
170
+ hookTypes: farmerHooks.map(h => h.hookType),
171
+ settingsPath,
172
+ }));
173
+ } else {
174
+ console.log();
175
+ console.log(' \x1b[32m\u2713\x1b[0m \x1b[1mFarmer disconnected\x1b[0m');
176
+ console.log(' \u2500'.repeat(40));
177
+ console.log(' Removed ' + farmerHooks.length + ' hook(s):');
178
+ for (const { hookType } of farmerHooks) {
179
+ console.log(' - ' + hookType);
180
+ }
181
+ console.log();
182
+ console.log(' Settings: ' + settingsPath);
183
+ console.log();
184
+ console.log(' To reconnect:');
185
+ console.log(' wheat connect farmer');
186
+ console.log();
187
+ }
188
+ }