autonomous-flow-daemon 1.6.0 → 1.9.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 (61) hide show
  1. package/CHANGELOG.md +85 -85
  2. package/LICENSE +21 -21
  3. package/README-ko.md +282 -0
  4. package/README.md +282 -266
  5. package/mcp-config.json +10 -10
  6. package/package.json +4 -2
  7. package/src/adapters/index.ts +370 -370
  8. package/src/cli.ts +162 -127
  9. package/src/commands/benchmark.ts +187 -187
  10. package/src/commands/correlate.ts +180 -0
  11. package/src/commands/dashboard.ts +404 -0
  12. package/src/commands/evolution.ts +84 -1
  13. package/src/commands/fix.ts +158 -158
  14. package/src/commands/lang.ts +41 -41
  15. package/src/commands/plugin.ts +110 -0
  16. package/src/commands/restart.ts +14 -14
  17. package/src/commands/score.ts +276 -276
  18. package/src/commands/start.ts +155 -155
  19. package/src/commands/status.ts +157 -157
  20. package/src/commands/stop.ts +68 -68
  21. package/src/commands/suggest.ts +211 -0
  22. package/src/commands/sync.ts +329 -16
  23. package/src/constants.ts +32 -32
  24. package/src/core/boast.ts +280 -280
  25. package/src/core/config.ts +49 -49
  26. package/src/core/correlation-engine.ts +265 -0
  27. package/src/core/db.ts +145 -117
  28. package/src/core/discovery.ts +65 -65
  29. package/src/core/federation.ts +129 -0
  30. package/src/core/hologram/engine.ts +71 -71
  31. package/src/core/hologram/fallback.ts +11 -11
  32. package/src/core/hologram/go-extractor.ts +203 -0
  33. package/src/core/hologram/incremental.ts +227 -227
  34. package/src/core/hologram/py-extractor.ts +132 -132
  35. package/src/core/hologram/rust-extractor.ts +244 -0
  36. package/src/core/hologram/ts-extractor.ts +406 -320
  37. package/src/core/hologram/types.ts +27 -25
  38. package/src/core/hologram.ts +73 -71
  39. package/src/core/i18n/messages.ts +309 -309
  40. package/src/core/locale.ts +88 -88
  41. package/src/core/log-rotate.ts +33 -33
  42. package/src/core/log-utils.ts +38 -38
  43. package/src/core/lru-map.ts +61 -61
  44. package/src/core/notify.ts +74 -74
  45. package/src/core/plugin-manager.ts +225 -0
  46. package/src/core/rule-suggestion.ts +127 -0
  47. package/src/core/validator-generator.ts +224 -0
  48. package/src/core/workspace.ts +28 -28
  49. package/src/daemon/client.ts +78 -65
  50. package/src/daemon/event-batcher.ts +108 -108
  51. package/src/daemon/guards.ts +13 -13
  52. package/src/daemon/http-routes.ts +376 -293
  53. package/src/daemon/mcp-handler.ts +575 -270
  54. package/src/daemon/mcp-subscriptions.ts +81 -0
  55. package/src/daemon/mesh.ts +51 -0
  56. package/src/daemon/server.ts +655 -590
  57. package/src/daemon/types.ts +121 -100
  58. package/src/daemon/workspace-map.ts +104 -92
  59. package/src/platform.ts +60 -60
  60. package/src/version.ts +15 -15
  61. package/README.ko.md +0 -266
@@ -1,370 +1,370 @@
1
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
2
- import { join, dirname } from "path";
3
- import { resolveHookCommand } from "../platform";
4
- import {
5
- readHooksFile,
6
- writeHooksFile,
7
- mergeHooks,
8
- getAfdDesiredHooks,
9
- KNOWN_AFD_HOOKS,
10
- } from "../core/hook-manager";
11
-
12
- export interface HarnessSchema {
13
- configFiles: string[];
14
- ignoreFile: string | null;
15
- rulesFile: string | null;
16
- hooksFile: string | null;
17
- }
18
-
19
- export interface EcosystemAdapter {
20
- name: string;
21
- detect(cwd: string): boolean;
22
- getHarnessSchema(): HarnessSchema;
23
- injectHooks?(cwd: string): { injected: boolean; message: string };
24
- configureStatusLine?(cwd: string): { configured: boolean; message: string };
25
- registerMcp?(cwd: string): { registered: boolean; message: string };
26
- removeHooks?(cwd: string): { removed: boolean; message: string };
27
- unregisterMcp?(cwd: string): { removed: boolean; message: string };
28
- }
29
-
30
- const AFD_HOOK_MARKER = "afd-auto-heal";
31
-
32
- interface HooksConfig {
33
- hooks?: Record<string, HookEntry[]>;
34
- [key: string]: unknown;
35
- }
36
-
37
- interface HookEntry {
38
- matcher?: string;
39
- command: string;
40
- id?: string;
41
- [key: string]: unknown;
42
- }
43
-
44
- export const ClaudeCodeAdapter: EcosystemAdapter = {
45
- name: "Claude Code",
46
- detect(cwd: string): boolean {
47
- return (
48
- existsSync(join(cwd, ".claude")) ||
49
- existsSync(join(cwd, "CLAUDE.md"))
50
- );
51
- },
52
- getHarnessSchema(): HarnessSchema {
53
- return {
54
- configFiles: [".claude/settings.json", ".claude/settings.local.json", "CLAUDE.md"],
55
- ignoreFile: ".claudeignore",
56
- rulesFile: "CLAUDE.md",
57
- hooksFile: ".claude/hooks.json",
58
- };
59
- },
60
- injectHooks(cwd: string): { injected: boolean; message: string } {
61
- const hooksPath = join(cwd, ".claude", "hooks.json");
62
- const config = readHooksFile(hooksPath);
63
-
64
- if (!config.hooks || Array.isArray(config.hooks) || typeof config.hooks !== "object") {
65
- config.hooks = {};
66
- }
67
- if (!config.hooks.PreToolUse) config.hooks.PreToolUse = [];
68
-
69
- const before = config.hooks.PreToolUse.length;
70
- const result = mergeHooks(config.hooks.PreToolUse, getAfdDesiredHooks());
71
- config.hooks.PreToolUse = result.merged;
72
- writeHooksFile(hooksPath, config);
73
-
74
- const added = result.changes.added.length > 0;
75
- const after = config.hooks.PreToolUse.length;
76
- if (added) {
77
- return { injected: true, message: "Auto-heal hook injected into PreToolUse" };
78
- }
79
- return {
80
- injected: false,
81
- message: `Auto-heal hook already present (${after} hook${after !== 1 ? "s" : ""} total, ordering: afd → omc → user)`,
82
- };
83
- },
84
- configureStatusLine(cwd: string): { configured: boolean; message: string } {
85
- const settingsPath = join(cwd, ".claude", "settings.local.json");
86
- const statusScript = join(cwd, ".claude", "statusline-command.js").replace(/\\/g, "/");
87
-
88
- // Only configure if the statusline-command.js exists
89
- if (!existsSync(statusScript)) {
90
- return { configured: false, message: "No statusline-command.js found" };
91
- }
92
-
93
- let settings: Record<string, unknown>;
94
- if (existsSync(settingsPath)) {
95
- try {
96
- settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
97
- } catch {
98
- settings = {};
99
- }
100
- } else {
101
- mkdirSync(dirname(settingsPath), { recursive: true });
102
- settings = {};
103
- }
104
-
105
- const expectedCommand = `node ${statusScript}`;
106
- const current = settings.statusLine as { type?: string; command?: string } | undefined;
107
-
108
- if (current?.type === "command" && current?.command === expectedCommand) {
109
- return { configured: false, message: "Status line already configured" };
110
- }
111
-
112
- settings.statusLine = { type: "command", command: expectedCommand };
113
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
114
- return { configured: true, message: "Status line configured with afd integration" };
115
- },
116
- registerMcp(cwd: string): { registered: boolean; message: string } {
117
- const mcpPath = join(cwd, ".mcp.json");
118
- const serverScript = "src/daemon/server.ts";
119
- const expectedArgs = ["run", serverScript, "--mcp"];
120
-
121
- let config: Record<string, unknown>;
122
- if (existsSync(mcpPath)) {
123
- try {
124
- config = JSON.parse(readFileSync(mcpPath, "utf-8"));
125
- } catch {
126
- config = {};
127
- }
128
- } else {
129
- config = {};
130
- }
131
-
132
- const mcpServers = (config.mcpServers ?? {}) as Record<string, unknown>;
133
- const existing = mcpServers.afd as { command?: string; args?: string[] } | undefined;
134
-
135
- if (existing?.command === "bun" &&
136
- JSON.stringify(existing.args) === JSON.stringify(expectedArgs)) {
137
- return { registered: false, message: "MCP server already registered in .mcp.json" };
138
- }
139
-
140
- mcpServers.afd = {
141
- command: "bun",
142
- args: expectedArgs,
143
- };
144
- config.mcpServers = mcpServers;
145
- writeFileSync(mcpPath, JSON.stringify(config, null, 2), "utf-8");
146
- return { registered: true, message: "MCP server 'afd' registered in .mcp.json" };
147
- },
148
- removeHooks(cwd: string): { removed: boolean; message: string } {
149
- const hooksPath = join(cwd, ".claude", "hooks.json");
150
- if (!existsSync(hooksPath)) {
151
- return { removed: false, message: "No hooks file found" };
152
- }
153
- try {
154
- const config: HooksConfig = JSON.parse(readFileSync(hooksPath, "utf-8"));
155
- const arr = config.hooks?.PreToolUse;
156
- if (!arr) return { removed: false, message: "No PreToolUse hooks" };
157
-
158
- // Only remove hooks that are in the canonical KNOWN_AFD_HOOKS set.
159
- // User hooks with an `afd-` prefix (e.g., afd-read-gate) are preserved.
160
- const before = arr.length;
161
- config.hooks!.PreToolUse = arr.filter(
162
- (h: HookEntry) => !KNOWN_AFD_HOOKS.has(h.id ?? "")
163
- );
164
- const removed = before - config.hooks!.PreToolUse.length;
165
- if (removed === 0) return { removed: false, message: "No afd-managed hooks found" };
166
-
167
- writeHooksFile(hooksPath, config);
168
- return { removed: true, message: `Removed ${removed} afd-managed hook${removed !== 1 ? "s" : ""} from PreToolUse` };
169
- } catch {
170
- return { removed: false, message: "Failed to parse hooks file" };
171
- }
172
- },
173
- unregisterMcp(cwd: string): { removed: boolean; message: string } {
174
- const mcpPath = join(cwd, ".mcp.json");
175
- if (!existsSync(mcpPath)) {
176
- return { removed: false, message: "No .mcp.json found" };
177
- }
178
- try {
179
- const config = JSON.parse(readFileSync(mcpPath, "utf-8"));
180
- const servers = config.mcpServers as Record<string, unknown> | undefined;
181
- if (!servers?.afd) return { removed: false, message: "afd not in .mcp.json" };
182
- delete servers.afd;
183
- if (Object.keys(servers).length === 0) delete config.mcpServers;
184
- writeFileSync(mcpPath, JSON.stringify(config, null, 2), "utf-8");
185
- return { removed: true, message: "MCP server 'afd' removed from .mcp.json" };
186
- } catch {
187
- return { removed: false, message: "Failed to parse .mcp.json" };
188
- }
189
- },
190
- };
191
-
192
- export const CursorAdapter: EcosystemAdapter = {
193
- name: "Cursor",
194
- detect(cwd: string): boolean {
195
- return existsSync(join(cwd, ".cursorrules")) || existsSync(join(cwd, ".cursor"));
196
- },
197
- getHarnessSchema(): HarnessSchema {
198
- return {
199
- configFiles: [".cursorrules", ".cursor/settings.json"],
200
- ignoreFile: ".cursorignore",
201
- rulesFile: ".cursorrules",
202
- hooksFile: ".cursor/hooks.json",
203
- };
204
- },
205
- injectHooks(cwd: string): { injected: boolean; message: string } {
206
- // Cursor supports hooks via .cursor/hooks.json (same format as Claude Code)
207
- const hooksPath = join(cwd, ".cursor", "hooks.json");
208
- const hookCommand = resolveHookCommand();
209
-
210
- const newHook: HookEntry = {
211
- id: AFD_HOOK_MARKER,
212
- matcher: "",
213
- command: hookCommand,
214
- };
215
-
216
- let config: HooksConfig;
217
- if (existsSync(hooksPath)) {
218
- try {
219
- config = JSON.parse(readFileSync(hooksPath, "utf-8"));
220
- } catch {
221
- config = { hooks: {} };
222
- }
223
- } else {
224
- mkdirSync(join(cwd, ".cursor"), { recursive: true });
225
- config = { hooks: {} };
226
- }
227
-
228
- if (!config.hooks || Array.isArray(config.hooks) || typeof config.hooks !== "object") {
229
- config.hooks = {};
230
- }
231
- if (!config.hooks.PreToolUse) config.hooks.PreToolUse = [];
232
-
233
- const existing = config.hooks.PreToolUse.find((h: HookEntry) => h.id === AFD_HOOK_MARKER);
234
- if (existing) {
235
- existing.command = hookCommand;
236
- writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
237
- return { injected: false, message: "Cursor: auto-heal hook already present (updated)" };
238
- }
239
-
240
- config.hooks.PreToolUse.push(newHook);
241
- writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
242
- return { injected: true, message: "Cursor: auto-heal hook injected" };
243
- },
244
- };
245
-
246
- export const WindsurfAdapter: EcosystemAdapter = {
247
- name: "Windsurf",
248
- detect(cwd: string): boolean {
249
- return existsSync(join(cwd, ".windsurfrules")) || existsSync(join(cwd, ".windsurf"));
250
- },
251
- getHarnessSchema(): HarnessSchema {
252
- return {
253
- configFiles: [".windsurfrules", ".windsurf/settings.json"],
254
- ignoreFile: ".windsurfignore",
255
- rulesFile: ".windsurfrules",
256
- hooksFile: ".windsurf/hooks.json",
257
- };
258
- },
259
- injectHooks(cwd: string): { injected: boolean; message: string } {
260
- const hooksPath = join(cwd, ".windsurf", "hooks.json");
261
- const hookCommand = resolveHookCommand();
262
-
263
- const newHook: HookEntry = {
264
- id: AFD_HOOK_MARKER,
265
- matcher: "",
266
- command: hookCommand,
267
- };
268
-
269
- let config: HooksConfig;
270
- if (existsSync(hooksPath)) {
271
- try {
272
- config = JSON.parse(readFileSync(hooksPath, "utf-8"));
273
- } catch {
274
- config = { hooks: {} };
275
- }
276
- } else {
277
- mkdirSync(join(cwd, ".windsurf"), { recursive: true });
278
- config = { hooks: {} };
279
- }
280
-
281
- if (!config.hooks || Array.isArray(config.hooks) || typeof config.hooks !== "object") {
282
- config.hooks = {};
283
- }
284
- if (!config.hooks.PreToolUse) config.hooks.PreToolUse = [];
285
-
286
- const existing = config.hooks.PreToolUse.find((h: HookEntry) => h.id === AFD_HOOK_MARKER);
287
- if (existing) {
288
- existing.command = hookCommand;
289
- writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
290
- return { injected: false, message: "Windsurf: auto-heal hook already present (updated)" };
291
- }
292
-
293
- config.hooks.PreToolUse.push(newHook);
294
- writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
295
- return { injected: true, message: "Windsurf: auto-heal hook injected" };
296
- },
297
- };
298
-
299
- export const CodexAdapter: EcosystemAdapter = {
300
- name: "Codex",
301
- detect(cwd: string): boolean {
302
- return existsSync(join(cwd, "codex.md")) || existsSync(join(cwd, ".codex"));
303
- },
304
- getHarnessSchema(): HarnessSchema {
305
- return {
306
- configFiles: ["codex.md", ".codex/settings.json"],
307
- ignoreFile: ".codexignore",
308
- rulesFile: "codex.md",
309
- hooksFile: ".codex/hooks.json",
310
- };
311
- },
312
- injectHooks(cwd: string): { injected: boolean; message: string } {
313
- const hooksPath = join(cwd, ".codex", "hooks.json");
314
- const hookCommand = resolveHookCommand();
315
-
316
- const newHook: HookEntry = {
317
- id: AFD_HOOK_MARKER,
318
- matcher: "",
319
- command: hookCommand,
320
- };
321
-
322
- let config: HooksConfig;
323
- if (existsSync(hooksPath)) {
324
- try {
325
- config = JSON.parse(readFileSync(hooksPath, "utf-8"));
326
- } catch {
327
- config = { hooks: {} };
328
- }
329
- } else {
330
- mkdirSync(join(cwd, ".codex"), { recursive: true });
331
- config = { hooks: {} };
332
- }
333
-
334
- if (!config.hooks || Array.isArray(config.hooks) || typeof config.hooks !== "object") {
335
- config.hooks = {};
336
- }
337
- if (!config.hooks.PreToolUse) config.hooks.PreToolUse = [];
338
-
339
- const existing = config.hooks.PreToolUse.find((h: HookEntry) => h.id === AFD_HOOK_MARKER);
340
- if (existing) {
341
- existing.command = hookCommand;
342
- writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
343
- return { injected: false, message: "Codex: auto-heal hook already present (updated)" };
344
- }
345
-
346
- config.hooks.PreToolUse.push(newHook);
347
- writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
348
- return { injected: true, message: "Codex: auto-heal hook injected" };
349
- },
350
- };
351
-
352
- const adapters: EcosystemAdapter[] = [ClaudeCodeAdapter, CursorAdapter, WindsurfAdapter, CodexAdapter];
353
-
354
- export interface DetectionResult {
355
- adapter: EcosystemAdapter;
356
- confidence: "primary" | "secondary";
357
- }
358
-
359
- export function detectEcosystem(cwd: string): DetectionResult[] {
360
- const results: DetectionResult[] = [];
361
- for (const adapter of adapters) {
362
- if (adapter.detect(cwd)) {
363
- results.push({
364
- adapter,
365
- confidence: results.length === 0 ? "primary" : "secondary",
366
- });
367
- }
368
- }
369
- return results;
370
- }
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
2
+ import { join, dirname } from "path";
3
+ import { resolveHookCommand } from "../platform";
4
+ import {
5
+ readHooksFile,
6
+ writeHooksFile,
7
+ mergeHooks,
8
+ getAfdDesiredHooks,
9
+ KNOWN_AFD_HOOKS,
10
+ } from "../core/hook-manager";
11
+
12
+ export interface HarnessSchema {
13
+ configFiles: string[];
14
+ ignoreFile: string | null;
15
+ rulesFile: string | null;
16
+ hooksFile: string | null;
17
+ }
18
+
19
+ export interface EcosystemAdapter {
20
+ name: string;
21
+ detect(cwd: string): boolean;
22
+ getHarnessSchema(): HarnessSchema;
23
+ injectHooks?(cwd: string): { injected: boolean; message: string };
24
+ configureStatusLine?(cwd: string): { configured: boolean; message: string };
25
+ registerMcp?(cwd: string): { registered: boolean; message: string };
26
+ removeHooks?(cwd: string): { removed: boolean; message: string };
27
+ unregisterMcp?(cwd: string): { removed: boolean; message: string };
28
+ }
29
+
30
+ const AFD_HOOK_MARKER = "afd-auto-heal";
31
+
32
+ interface HooksConfig {
33
+ hooks?: Record<string, HookEntry[]>;
34
+ [key: string]: unknown;
35
+ }
36
+
37
+ interface HookEntry {
38
+ matcher?: string;
39
+ command: string;
40
+ id?: string;
41
+ [key: string]: unknown;
42
+ }
43
+
44
+ export const ClaudeCodeAdapter: EcosystemAdapter = {
45
+ name: "Claude Code",
46
+ detect(cwd: string): boolean {
47
+ return (
48
+ existsSync(join(cwd, ".claude")) ||
49
+ existsSync(join(cwd, "CLAUDE.md"))
50
+ );
51
+ },
52
+ getHarnessSchema(): HarnessSchema {
53
+ return {
54
+ configFiles: [".claude/settings.json", ".claude/settings.local.json", "CLAUDE.md"],
55
+ ignoreFile: ".claudeignore",
56
+ rulesFile: "CLAUDE.md",
57
+ hooksFile: ".claude/hooks.json",
58
+ };
59
+ },
60
+ injectHooks(cwd: string): { injected: boolean; message: string } {
61
+ const hooksPath = join(cwd, ".claude", "hooks.json");
62
+ const config = readHooksFile(hooksPath);
63
+
64
+ if (!config.hooks || Array.isArray(config.hooks) || typeof config.hooks !== "object") {
65
+ config.hooks = {};
66
+ }
67
+ if (!config.hooks.PreToolUse) config.hooks.PreToolUse = [];
68
+
69
+ const before = config.hooks.PreToolUse.length;
70
+ const result = mergeHooks(config.hooks.PreToolUse, getAfdDesiredHooks());
71
+ config.hooks.PreToolUse = result.merged;
72
+ writeHooksFile(hooksPath, config);
73
+
74
+ const added = result.changes.added.length > 0;
75
+ const after = config.hooks.PreToolUse.length;
76
+ if (added) {
77
+ return { injected: true, message: "Auto-heal hook injected into PreToolUse" };
78
+ }
79
+ return {
80
+ injected: false,
81
+ message: `Auto-heal hook already present (${after} hook${after !== 1 ? "s" : ""} total, ordering: afd → omc → user)`,
82
+ };
83
+ },
84
+ configureStatusLine(cwd: string): { configured: boolean; message: string } {
85
+ const settingsPath = join(cwd, ".claude", "settings.local.json");
86
+ const statusScript = join(cwd, ".claude", "statusline-command.js").replace(/\\/g, "/");
87
+
88
+ // Only configure if the statusline-command.js exists
89
+ if (!existsSync(statusScript)) {
90
+ return { configured: false, message: "No statusline-command.js found" };
91
+ }
92
+
93
+ let settings: Record<string, unknown>;
94
+ if (existsSync(settingsPath)) {
95
+ try {
96
+ settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
97
+ } catch {
98
+ settings = {};
99
+ }
100
+ } else {
101
+ mkdirSync(dirname(settingsPath), { recursive: true });
102
+ settings = {};
103
+ }
104
+
105
+ const expectedCommand = `node ${statusScript}`;
106
+ const current = settings.statusLine as { type?: string; command?: string } | undefined;
107
+
108
+ if (current?.type === "command" && current?.command === expectedCommand) {
109
+ return { configured: false, message: "Status line already configured" };
110
+ }
111
+
112
+ settings.statusLine = { type: "command", command: expectedCommand };
113
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
114
+ return { configured: true, message: "Status line configured with afd integration" };
115
+ },
116
+ registerMcp(cwd: string): { registered: boolean; message: string } {
117
+ const mcpPath = join(cwd, ".mcp.json");
118
+ const serverScript = "src/daemon/server.ts";
119
+ const expectedArgs = ["run", serverScript, "--mcp"];
120
+
121
+ let config: Record<string, unknown>;
122
+ if (existsSync(mcpPath)) {
123
+ try {
124
+ config = JSON.parse(readFileSync(mcpPath, "utf-8"));
125
+ } catch {
126
+ config = {};
127
+ }
128
+ } else {
129
+ config = {};
130
+ }
131
+
132
+ const mcpServers = (config.mcpServers ?? {}) as Record<string, unknown>;
133
+ const existing = mcpServers.afd as { command?: string; args?: string[] } | undefined;
134
+
135
+ if (existing?.command === "bun" &&
136
+ JSON.stringify(existing.args) === JSON.stringify(expectedArgs)) {
137
+ return { registered: false, message: "MCP server already registered in .mcp.json" };
138
+ }
139
+
140
+ mcpServers.afd = {
141
+ command: "bun",
142
+ args: expectedArgs,
143
+ };
144
+ config.mcpServers = mcpServers;
145
+ writeFileSync(mcpPath, JSON.stringify(config, null, 2), "utf-8");
146
+ return { registered: true, message: "MCP server 'afd' registered in .mcp.json" };
147
+ },
148
+ removeHooks(cwd: string): { removed: boolean; message: string } {
149
+ const hooksPath = join(cwd, ".claude", "hooks.json");
150
+ if (!existsSync(hooksPath)) {
151
+ return { removed: false, message: "No hooks file found" };
152
+ }
153
+ try {
154
+ const config: HooksConfig = JSON.parse(readFileSync(hooksPath, "utf-8"));
155
+ const arr = config.hooks?.PreToolUse;
156
+ if (!arr) return { removed: false, message: "No PreToolUse hooks" };
157
+
158
+ // Only remove hooks that are in the canonical KNOWN_AFD_HOOKS set.
159
+ // User hooks with an `afd-` prefix (e.g., afd-read-gate) are preserved.
160
+ const before = arr.length;
161
+ config.hooks!.PreToolUse = arr.filter(
162
+ (h: HookEntry) => !KNOWN_AFD_HOOKS.has(h.id ?? "")
163
+ );
164
+ const removed = before - config.hooks!.PreToolUse.length;
165
+ if (removed === 0) return { removed: false, message: "No afd-managed hooks found" };
166
+
167
+ writeHooksFile(hooksPath, config);
168
+ return { removed: true, message: `Removed ${removed} afd-managed hook${removed !== 1 ? "s" : ""} from PreToolUse` };
169
+ } catch {
170
+ return { removed: false, message: "Failed to parse hooks file" };
171
+ }
172
+ },
173
+ unregisterMcp(cwd: string): { removed: boolean; message: string } {
174
+ const mcpPath = join(cwd, ".mcp.json");
175
+ if (!existsSync(mcpPath)) {
176
+ return { removed: false, message: "No .mcp.json found" };
177
+ }
178
+ try {
179
+ const config = JSON.parse(readFileSync(mcpPath, "utf-8"));
180
+ const servers = config.mcpServers as Record<string, unknown> | undefined;
181
+ if (!servers?.afd) return { removed: false, message: "afd not in .mcp.json" };
182
+ delete servers.afd;
183
+ if (Object.keys(servers).length === 0) delete config.mcpServers;
184
+ writeFileSync(mcpPath, JSON.stringify(config, null, 2), "utf-8");
185
+ return { removed: true, message: "MCP server 'afd' removed from .mcp.json" };
186
+ } catch {
187
+ return { removed: false, message: "Failed to parse .mcp.json" };
188
+ }
189
+ },
190
+ };
191
+
192
+ export const CursorAdapter: EcosystemAdapter = {
193
+ name: "Cursor",
194
+ detect(cwd: string): boolean {
195
+ return existsSync(join(cwd, ".cursorrules")) || existsSync(join(cwd, ".cursor"));
196
+ },
197
+ getHarnessSchema(): HarnessSchema {
198
+ return {
199
+ configFiles: [".cursorrules", ".cursor/settings.json"],
200
+ ignoreFile: ".cursorignore",
201
+ rulesFile: ".cursorrules",
202
+ hooksFile: ".cursor/hooks.json",
203
+ };
204
+ },
205
+ injectHooks(cwd: string): { injected: boolean; message: string } {
206
+ // Cursor supports hooks via .cursor/hooks.json (same format as Claude Code)
207
+ const hooksPath = join(cwd, ".cursor", "hooks.json");
208
+ const hookCommand = resolveHookCommand();
209
+
210
+ const newHook: HookEntry = {
211
+ id: AFD_HOOK_MARKER,
212
+ matcher: "",
213
+ command: hookCommand,
214
+ };
215
+
216
+ let config: HooksConfig;
217
+ if (existsSync(hooksPath)) {
218
+ try {
219
+ config = JSON.parse(readFileSync(hooksPath, "utf-8"));
220
+ } catch {
221
+ config = { hooks: {} };
222
+ }
223
+ } else {
224
+ mkdirSync(join(cwd, ".cursor"), { recursive: true });
225
+ config = { hooks: {} };
226
+ }
227
+
228
+ if (!config.hooks || Array.isArray(config.hooks) || typeof config.hooks !== "object") {
229
+ config.hooks = {};
230
+ }
231
+ if (!config.hooks.PreToolUse) config.hooks.PreToolUse = [];
232
+
233
+ const existing = config.hooks.PreToolUse.find((h: HookEntry) => h.id === AFD_HOOK_MARKER);
234
+ if (existing) {
235
+ existing.command = hookCommand;
236
+ writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
237
+ return { injected: false, message: "Cursor: auto-heal hook already present (updated)" };
238
+ }
239
+
240
+ config.hooks.PreToolUse.push(newHook);
241
+ writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
242
+ return { injected: true, message: "Cursor: auto-heal hook injected" };
243
+ },
244
+ };
245
+
246
+ export const WindsurfAdapter: EcosystemAdapter = {
247
+ name: "Windsurf",
248
+ detect(cwd: string): boolean {
249
+ return existsSync(join(cwd, ".windsurfrules")) || existsSync(join(cwd, ".windsurf"));
250
+ },
251
+ getHarnessSchema(): HarnessSchema {
252
+ return {
253
+ configFiles: [".windsurfrules", ".windsurf/settings.json"],
254
+ ignoreFile: ".windsurfignore",
255
+ rulesFile: ".windsurfrules",
256
+ hooksFile: ".windsurf/hooks.json",
257
+ };
258
+ },
259
+ injectHooks(cwd: string): { injected: boolean; message: string } {
260
+ const hooksPath = join(cwd, ".windsurf", "hooks.json");
261
+ const hookCommand = resolveHookCommand();
262
+
263
+ const newHook: HookEntry = {
264
+ id: AFD_HOOK_MARKER,
265
+ matcher: "",
266
+ command: hookCommand,
267
+ };
268
+
269
+ let config: HooksConfig;
270
+ if (existsSync(hooksPath)) {
271
+ try {
272
+ config = JSON.parse(readFileSync(hooksPath, "utf-8"));
273
+ } catch {
274
+ config = { hooks: {} };
275
+ }
276
+ } else {
277
+ mkdirSync(join(cwd, ".windsurf"), { recursive: true });
278
+ config = { hooks: {} };
279
+ }
280
+
281
+ if (!config.hooks || Array.isArray(config.hooks) || typeof config.hooks !== "object") {
282
+ config.hooks = {};
283
+ }
284
+ if (!config.hooks.PreToolUse) config.hooks.PreToolUse = [];
285
+
286
+ const existing = config.hooks.PreToolUse.find((h: HookEntry) => h.id === AFD_HOOK_MARKER);
287
+ if (existing) {
288
+ existing.command = hookCommand;
289
+ writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
290
+ return { injected: false, message: "Windsurf: auto-heal hook already present (updated)" };
291
+ }
292
+
293
+ config.hooks.PreToolUse.push(newHook);
294
+ writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
295
+ return { injected: true, message: "Windsurf: auto-heal hook injected" };
296
+ },
297
+ };
298
+
299
+ export const CodexAdapter: EcosystemAdapter = {
300
+ name: "Codex",
301
+ detect(cwd: string): boolean {
302
+ return existsSync(join(cwd, "codex.md")) || existsSync(join(cwd, ".codex"));
303
+ },
304
+ getHarnessSchema(): HarnessSchema {
305
+ return {
306
+ configFiles: ["codex.md", ".codex/settings.json"],
307
+ ignoreFile: ".codexignore",
308
+ rulesFile: "codex.md",
309
+ hooksFile: ".codex/hooks.json",
310
+ };
311
+ },
312
+ injectHooks(cwd: string): { injected: boolean; message: string } {
313
+ const hooksPath = join(cwd, ".codex", "hooks.json");
314
+ const hookCommand = resolveHookCommand();
315
+
316
+ const newHook: HookEntry = {
317
+ id: AFD_HOOK_MARKER,
318
+ matcher: "",
319
+ command: hookCommand,
320
+ };
321
+
322
+ let config: HooksConfig;
323
+ if (existsSync(hooksPath)) {
324
+ try {
325
+ config = JSON.parse(readFileSync(hooksPath, "utf-8"));
326
+ } catch {
327
+ config = { hooks: {} };
328
+ }
329
+ } else {
330
+ mkdirSync(join(cwd, ".codex"), { recursive: true });
331
+ config = { hooks: {} };
332
+ }
333
+
334
+ if (!config.hooks || Array.isArray(config.hooks) || typeof config.hooks !== "object") {
335
+ config.hooks = {};
336
+ }
337
+ if (!config.hooks.PreToolUse) config.hooks.PreToolUse = [];
338
+
339
+ const existing = config.hooks.PreToolUse.find((h: HookEntry) => h.id === AFD_HOOK_MARKER);
340
+ if (existing) {
341
+ existing.command = hookCommand;
342
+ writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
343
+ return { injected: false, message: "Codex: auto-heal hook already present (updated)" };
344
+ }
345
+
346
+ config.hooks.PreToolUse.push(newHook);
347
+ writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
348
+ return { injected: true, message: "Codex: auto-heal hook injected" };
349
+ },
350
+ };
351
+
352
+ const adapters: EcosystemAdapter[] = [ClaudeCodeAdapter, CursorAdapter, WindsurfAdapter, CodexAdapter];
353
+
354
+ export interface DetectionResult {
355
+ adapter: EcosystemAdapter;
356
+ confidence: "primary" | "secondary";
357
+ }
358
+
359
+ export function detectEcosystem(cwd: string): DetectionResult[] {
360
+ const results: DetectionResult[] = [];
361
+ for (const adapter of adapters) {
362
+ if (adapter.detect(cwd)) {
363
+ results.push({
364
+ adapter,
365
+ confidence: results.length === 0 ? "primary" : "secondary",
366
+ });
367
+ }
368
+ }
369
+ return results;
370
+ }