autonomous-flow-daemon 1.0.0 → 1.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.
- package/CHANGELOG.md +39 -0
- package/README.ko.md +142 -125
- package/README.md +119 -134
- package/package.json +11 -5
- package/src/adapters/index.ts +247 -35
- package/src/cli.ts +79 -1
- package/src/commands/benchmark.ts +187 -0
- package/src/commands/diagnose.ts +56 -14
- package/src/commands/doctor.ts +243 -0
- package/src/commands/evolution.ts +107 -0
- package/src/commands/fix.ts +22 -2
- package/src/commands/hooks.ts +136 -0
- package/src/commands/lang.ts +41 -0
- package/src/commands/mcp.ts +129 -0
- package/src/commands/restart.ts +14 -0
- package/src/commands/score.ts +192 -64
- package/src/commands/start.ts +137 -37
- package/src/commands/stats.ts +103 -0
- package/src/commands/status.ts +157 -0
- package/src/commands/stop.ts +42 -9
- package/src/commands/sync.ts +253 -20
- package/src/commands/vaccine.ts +177 -0
- package/src/constants.ts +26 -1
- package/src/core/boast.ts +280 -0
- package/src/core/config.ts +49 -0
- package/src/core/db.ts +74 -3
- package/src/core/discovery.ts +65 -0
- package/src/core/evolution.ts +215 -0
- package/src/core/hologram/engine.ts +71 -0
- package/src/core/hologram/fallback.ts +11 -0
- package/src/core/hologram/incremental.ts +227 -0
- package/src/core/hologram/py-extractor.ts +132 -0
- package/src/core/hologram/ts-extractor.ts +320 -0
- package/src/core/hologram/types.ts +25 -0
- package/src/core/hologram.ts +64 -236
- package/src/core/hook-manager.ts +259 -0
- package/src/core/i18n/messages.ts +309 -0
- package/src/core/immune.ts +8 -123
- package/src/core/locale.ts +88 -0
- package/src/core/log-rotate.ts +33 -0
- package/src/core/log-utils.ts +38 -0
- package/src/core/lru-map.ts +61 -0
- package/src/core/notify.ts +53 -14
- package/src/core/rule-engine.ts +287 -0
- package/src/core/semantic-diff.ts +432 -0
- package/src/core/telemetry.ts +94 -0
- package/src/core/vaccine-registry.ts +212 -0
- package/src/core/workspace.ts +28 -0
- package/src/core/yaml-minimal.ts +176 -0
- package/src/daemon/client.ts +34 -6
- package/src/daemon/event-batcher.ts +108 -0
- package/src/daemon/guards.ts +13 -0
- package/src/daemon/http-routes.ts +293 -0
- package/src/daemon/mcp-handler.ts +270 -0
- package/src/daemon/server.ts +492 -273
- package/src/daemon/types.ts +100 -0
- package/src/daemon/workspace-map.ts +92 -0
- package/src/platform.ts +60 -0
- package/src/version.ts +15 -0
package/src/adapters/index.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
2
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";
|
|
3
11
|
|
|
4
12
|
export interface HarnessSchema {
|
|
5
13
|
configFiles: string[];
|
|
@@ -14,6 +22,9 @@ export interface EcosystemAdapter {
|
|
|
14
22
|
getHarnessSchema(): HarnessSchema;
|
|
15
23
|
injectHooks?(cwd: string): { injected: boolean; message: string };
|
|
16
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 };
|
|
17
28
|
}
|
|
18
29
|
|
|
19
30
|
const AFD_HOOK_MARKER = "afd-auto-heal";
|
|
@@ -48,45 +59,27 @@ export const ClaudeCodeAdapter: EcosystemAdapter = {
|
|
|
48
59
|
},
|
|
49
60
|
injectHooks(cwd: string): { injected: boolean; message: string } {
|
|
50
61
|
const hooksPath = join(cwd, ".claude", "hooks.json");
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
const newHook: HookEntry = {
|
|
54
|
-
id: AFD_HOOK_MARKER,
|
|
55
|
-
matcher: "",
|
|
56
|
-
command: hookCommand,
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
let config: HooksConfig;
|
|
60
|
-
if (existsSync(hooksPath)) {
|
|
61
|
-
try {
|
|
62
|
-
config = JSON.parse(readFileSync(hooksPath, "utf-8"));
|
|
63
|
-
} catch {
|
|
64
|
-
config = { hooks: {} };
|
|
65
|
-
}
|
|
66
|
-
} else {
|
|
67
|
-
mkdirSync(dirname(hooksPath), { recursive: true });
|
|
68
|
-
config = { hooks: {} };
|
|
69
|
-
}
|
|
62
|
+
const config = readHooksFile(hooksPath);
|
|
70
63
|
|
|
71
64
|
if (!config.hooks || Array.isArray(config.hooks) || typeof config.hooks !== "object") {
|
|
72
65
|
config.hooks = {};
|
|
73
66
|
}
|
|
74
67
|
if (!config.hooks.PreToolUse) config.hooks.PreToolUse = [];
|
|
75
68
|
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
);
|
|
80
|
-
if (existing) {
|
|
81
|
-
// Update command in case path changed
|
|
82
|
-
existing.command = hookCommand;
|
|
83
|
-
writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
|
|
84
|
-
return { injected: false, message: "Auto-heal hook already present (updated)" };
|
|
85
|
-
}
|
|
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);
|
|
86
73
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
+
};
|
|
90
83
|
},
|
|
91
84
|
configureStatusLine(cwd: string): { configured: boolean; message: string } {
|
|
92
85
|
const settingsPath = join(cwd, ".claude", "settings.local.json");
|
|
@@ -120,24 +113,243 @@ export const ClaudeCodeAdapter: EcosystemAdapter = {
|
|
|
120
113
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
121
114
|
return { configured: true, message: "Status line configured with afd integration" };
|
|
122
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
|
+
},
|
|
123
190
|
};
|
|
124
191
|
|
|
125
192
|
export const CursorAdapter: EcosystemAdapter = {
|
|
126
193
|
name: "Cursor",
|
|
127
194
|
detect(cwd: string): boolean {
|
|
128
|
-
return existsSync(join(cwd, ".cursorrules"));
|
|
195
|
+
return existsSync(join(cwd, ".cursorrules")) || existsSync(join(cwd, ".cursor"));
|
|
129
196
|
},
|
|
130
197
|
getHarnessSchema(): HarnessSchema {
|
|
131
198
|
return {
|
|
132
199
|
configFiles: [".cursorrules", ".cursor/settings.json"],
|
|
133
200
|
ignoreFile: ".cursorignore",
|
|
134
201
|
rulesFile: ".cursorrules",
|
|
135
|
-
hooksFile:
|
|
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,
|
|
136
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" };
|
|
137
349
|
},
|
|
138
350
|
};
|
|
139
351
|
|
|
140
|
-
const adapters: EcosystemAdapter[] = [ClaudeCodeAdapter, CursorAdapter];
|
|
352
|
+
const adapters: EcosystemAdapter[] = [ClaudeCodeAdapter, CursorAdapter, WindsurfAdapter, CodexAdapter];
|
|
141
353
|
|
|
142
354
|
export interface DetectionResult {
|
|
143
355
|
adapter: EcosystemAdapter;
|
package/src/cli.ts
CHANGED
|
@@ -2,28 +2,57 @@
|
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { startCommand } from "./commands/start";
|
|
4
4
|
import { stopCommand } from "./commands/stop";
|
|
5
|
+
import { restartCommand } from "./commands/restart";
|
|
6
|
+
import { statusCommand } from "./commands/status";
|
|
5
7
|
import { scoreCommand } from "./commands/score";
|
|
6
8
|
import { fixCommand } from "./commands/fix";
|
|
7
9
|
import { syncCommand } from "./commands/sync";
|
|
8
10
|
import { diagnoseCommand } from "./commands/diagnose";
|
|
11
|
+
import { doctorCommand } from "./commands/doctor";
|
|
12
|
+
|
|
13
|
+
import { vaccineCommand } from "./commands/vaccine";
|
|
14
|
+
import { langCommand } from "./commands/lang";
|
|
15
|
+
import { evolutionCommand } from "./commands/evolution";
|
|
16
|
+
import { mcpCommand } from "./commands/mcp";
|
|
17
|
+
import { statsCommand } from "./commands/stats";
|
|
18
|
+
import { hooksCommand } from "./commands/hooks";
|
|
19
|
+
import { benchmarkCommand } from "./commands/benchmark";
|
|
20
|
+
import { APP_VERSION } from "./version";
|
|
21
|
+
import { trackCliCommand } from "./core/telemetry";
|
|
9
22
|
|
|
10
23
|
const program = new Command();
|
|
11
24
|
|
|
12
25
|
program
|
|
13
26
|
.name("afd")
|
|
14
27
|
.description("Autonomous Flow Daemon - The Immune System for AI Workflows")
|
|
15
|
-
.version(
|
|
28
|
+
.version(APP_VERSION);
|
|
29
|
+
|
|
30
|
+
program.hook("preAction", (thisCommand) => {
|
|
31
|
+
trackCliCommand(thisCommand.name());
|
|
32
|
+
});
|
|
16
33
|
|
|
17
34
|
program
|
|
18
35
|
.command("start")
|
|
19
36
|
.description("Start the afd daemon (background file watcher)")
|
|
37
|
+
.option("--mcp", "Run in MCP stdio mode (for Claude Code tool integration)")
|
|
20
38
|
.action(startCommand);
|
|
21
39
|
|
|
22
40
|
program
|
|
23
41
|
.command("stop")
|
|
24
42
|
.description("Stop the afd daemon")
|
|
43
|
+
.option("--clean", "Remove all injected hooks and MCP registrations")
|
|
25
44
|
.action(stopCommand);
|
|
26
45
|
|
|
46
|
+
program
|
|
47
|
+
.command("restart")
|
|
48
|
+
.description("Restart the afd daemon (stop + start)")
|
|
49
|
+
.action(restartCommand);
|
|
50
|
+
|
|
51
|
+
program
|
|
52
|
+
.command("status")
|
|
53
|
+
.description("Quick health check — daemon, hooks, defenses, quarantine")
|
|
54
|
+
.action(statusCommand);
|
|
55
|
+
|
|
27
56
|
program
|
|
28
57
|
.command("score")
|
|
29
58
|
.description("Show current diagnostic stats from the daemon")
|
|
@@ -37,8 +66,17 @@ program
|
|
|
37
66
|
program
|
|
38
67
|
.command("sync")
|
|
39
68
|
.description("Synchronize AI agent configs across team")
|
|
69
|
+
.option("--push", "Push local antibodies to team vaccine store")
|
|
70
|
+
.option("--pull", "Pull antibodies from team vaccine store")
|
|
71
|
+
.option("--remote <url>", "Remote vaccine store URL (future)")
|
|
40
72
|
.action(syncCommand);
|
|
41
73
|
|
|
74
|
+
program
|
|
75
|
+
.command("doctor")
|
|
76
|
+
.description("Deep health analysis with recommendations and auto-fix")
|
|
77
|
+
.option("--fix", "Auto-fix detected issues")
|
|
78
|
+
.action(doctorCommand);
|
|
79
|
+
|
|
42
80
|
program
|
|
43
81
|
.command("diagnose")
|
|
44
82
|
.description("Run headless diagnosis (used by auto-heal hooks)")
|
|
@@ -46,4 +84,44 @@ program
|
|
|
46
84
|
.option("--auto-heal", "Auto-apply patches for known antibodies")
|
|
47
85
|
.action(diagnoseCommand);
|
|
48
86
|
|
|
87
|
+
program
|
|
88
|
+
.command("vaccine [subcommand] [arg]")
|
|
89
|
+
.description("Vaccine registry: list, search, install, publish")
|
|
90
|
+
.action(vaccineCommand);
|
|
91
|
+
|
|
92
|
+
program
|
|
93
|
+
.command("evolution")
|
|
94
|
+
.description("Self-Evolution: analyze quarantined failures and generate lessons for AI agents")
|
|
95
|
+
.action(evolutionCommand);
|
|
96
|
+
|
|
97
|
+
program
|
|
98
|
+
.command("mcp [subcommand]")
|
|
99
|
+
.description("MCP server management (install)")
|
|
100
|
+
.action(mcpCommand);
|
|
101
|
+
|
|
102
|
+
program
|
|
103
|
+
.command("lang [language]")
|
|
104
|
+
.description("Show or change display language (en, ko)")
|
|
105
|
+
.option("--list", "Show all supported languages")
|
|
106
|
+
.action(langCommand);
|
|
107
|
+
|
|
108
|
+
program
|
|
109
|
+
.command("stats")
|
|
110
|
+
.description("Feature usage telemetry dashboard (developer-only)")
|
|
111
|
+
.option("--days <n>", "Number of days to aggregate", "7")
|
|
112
|
+
.action(statsCommand);
|
|
113
|
+
|
|
114
|
+
program
|
|
115
|
+
.command("hooks [subcommand]")
|
|
116
|
+
.description("Hook Manager: inspect and sync hook ordering (afd → omc → user)")
|
|
117
|
+
.action(hooksCommand);
|
|
118
|
+
|
|
119
|
+
program
|
|
120
|
+
.command("benchmark")
|
|
121
|
+
.description("Hologram AST compression benchmark across all source files")
|
|
122
|
+
.option("--sort <key>", "Sort by: savings (default), size, name")
|
|
123
|
+
.option("--top <n>", "Show only top N files")
|
|
124
|
+
.option("--json", "Output raw JSON for programmatic use")
|
|
125
|
+
.action(benchmarkCommand);
|
|
126
|
+
|
|
49
127
|
program.parse();
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
import { generateHologram } from "../core/hologram";
|
|
4
|
+
import { getSystemLanguage } from "../core/locale";
|
|
5
|
+
|
|
6
|
+
// ── ANSI helpers ──
|
|
7
|
+
const C = {
|
|
8
|
+
reset: "\x1b[0m",
|
|
9
|
+
bold: "\x1b[1m",
|
|
10
|
+
dim: "\x1b[2m",
|
|
11
|
+
red: "\x1b[31m",
|
|
12
|
+
green: "\x1b[32m",
|
|
13
|
+
yellow: "\x1b[33m",
|
|
14
|
+
cyan: "\x1b[36m",
|
|
15
|
+
white: "\x1b[37m",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
interface FileResult {
|
|
19
|
+
path: string;
|
|
20
|
+
originalLines: number;
|
|
21
|
+
hologramLines: number;
|
|
22
|
+
originalChars: number;
|
|
23
|
+
hologramChars: number;
|
|
24
|
+
savings: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function collectTsFiles(dir: string, base: string): string[] {
|
|
28
|
+
const results: string[] = [];
|
|
29
|
+
const skipDirs = new Set(["node_modules", ".git", ".afd", "dist", "coverage", ".omc"]);
|
|
30
|
+
|
|
31
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
32
|
+
if (entry.isDirectory()) {
|
|
33
|
+
if (!skipDirs.has(entry.name)) {
|
|
34
|
+
results.push(...collectTsFiles(join(dir, entry.name), base));
|
|
35
|
+
}
|
|
36
|
+
} else if (/\.(ts|tsx|js|jsx)$/.test(entry.name) && !entry.name.endsWith(".d.ts")) {
|
|
37
|
+
results.push(join(dir, entry.name));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return results;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function formatChars(n: number): string {
|
|
44
|
+
if (n < 1000) return `${n}`;
|
|
45
|
+
if (n < 1_000_000) return `${(n / 1000).toFixed(1)}K`;
|
|
46
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function savingsColor(pct: number): string {
|
|
50
|
+
if (pct >= 80) return C.green;
|
|
51
|
+
if (pct >= 50) return C.yellow;
|
|
52
|
+
return C.red;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function padRight(s: string, w: number): string {
|
|
56
|
+
const stripped = s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
57
|
+
return s + " ".repeat(Math.max(0, w - stripped.length));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function padLeft(s: string, w: number): string {
|
|
61
|
+
const stripped = s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
62
|
+
return " ".repeat(Math.max(0, w - stripped.length)) + s;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function benchmarkCommand(options: { sort?: string; top?: string; json?: boolean }) {
|
|
66
|
+
const lang = getSystemLanguage();
|
|
67
|
+
const ko = lang === "ko";
|
|
68
|
+
const cwd = process.cwd();
|
|
69
|
+
const files = collectTsFiles(join(cwd, "src"), cwd);
|
|
70
|
+
|
|
71
|
+
if (files.length === 0) {
|
|
72
|
+
console.error(`${C.red}[afd] ${ko ? "src/ 디렉토리에 TS/JS 파일이 없습니다." : "No TS/JS files found in src/."}${C.reset}`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const results: FileResult[] = [];
|
|
77
|
+
const startTime = performance.now();
|
|
78
|
+
|
|
79
|
+
for (const filePath of files) {
|
|
80
|
+
try {
|
|
81
|
+
const source = readFileSync(filePath, "utf-8");
|
|
82
|
+
const { hologram, originalLength, hologramLength, savings } = await generateHologram(filePath, source);
|
|
83
|
+
results.push({
|
|
84
|
+
path: relative(cwd, filePath),
|
|
85
|
+
originalLines: source.split("\n").length,
|
|
86
|
+
hologramLines: hologram.split("\n").length,
|
|
87
|
+
originalChars: originalLength,
|
|
88
|
+
hologramChars: hologramLength,
|
|
89
|
+
savings: Math.round(savings * 10) / 10,
|
|
90
|
+
});
|
|
91
|
+
} catch {
|
|
92
|
+
// Skip files that fail to parse
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const elapsed = Math.round(performance.now() - startTime);
|
|
97
|
+
|
|
98
|
+
// Sort
|
|
99
|
+
const sortKey = options.sort ?? "savings";
|
|
100
|
+
results.sort((a, b) => {
|
|
101
|
+
if (sortKey === "size") return b.originalChars - a.originalChars;
|
|
102
|
+
if (sortKey === "name") return a.path.localeCompare(b.path);
|
|
103
|
+
return b.savings - a.savings; // default: savings desc
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const limit = options.top ? parseInt(options.top, 10) : results.length;
|
|
107
|
+
const display = results.slice(0, limit);
|
|
108
|
+
|
|
109
|
+
// JSON output
|
|
110
|
+
if (options.json) {
|
|
111
|
+
const totalOriginal = results.reduce((s, r) => s + r.originalChars, 0);
|
|
112
|
+
const totalHologram = results.reduce((s, r) => s + r.hologramChars, 0);
|
|
113
|
+
console.log(JSON.stringify({
|
|
114
|
+
files: results.length,
|
|
115
|
+
totalOriginalChars: totalOriginal,
|
|
116
|
+
totalHologramChars: totalHologram,
|
|
117
|
+
totalSavedChars: totalOriginal - totalHologram,
|
|
118
|
+
overallCompression: totalOriginal > 0 ? Math.round((1 - totalHologram / totalOriginal) * 1000) / 10 : 0,
|
|
119
|
+
estimatedTokensSaved: Math.round((totalOriginal - totalHologram) / 4),
|
|
120
|
+
elapsedMs: elapsed,
|
|
121
|
+
results: results.map(r => ({
|
|
122
|
+
path: r.path,
|
|
123
|
+
originalLines: r.originalLines,
|
|
124
|
+
hologramLines: r.hologramLines,
|
|
125
|
+
originalChars: r.originalChars,
|
|
126
|
+
hologramChars: r.hologramChars,
|
|
127
|
+
savings: r.savings,
|
|
128
|
+
})),
|
|
129
|
+
}, null, 2));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Header ──
|
|
134
|
+
console.log();
|
|
135
|
+
console.log(`${C.bold}${C.cyan} ╔══════════════════════════════════════════════════════════════╗${C.reset}`);
|
|
136
|
+
console.log(`${C.bold}${C.cyan} ║ ${ko ? "홀로그램 AST 압축 벤치마크" : "Hologram AST Compression Benchmark"} ║${C.reset}`);
|
|
137
|
+
console.log(`${C.bold}${C.cyan} ╚══════════════════════════════════════════════════════════════╝${C.reset}`);
|
|
138
|
+
console.log();
|
|
139
|
+
|
|
140
|
+
// ── Table Header ──
|
|
141
|
+
const colFile = ko ? "파일" : "File";
|
|
142
|
+
const colLines = ko ? "원본줄" : "Lines";
|
|
143
|
+
const colHolo = ko ? "홀로줄" : "Holo";
|
|
144
|
+
const colOrig = ko ? "원본" : "Original";
|
|
145
|
+
const colComp = ko ? "압축" : "Compressed";
|
|
146
|
+
const colSave = ko ? "절감률" : "Savings";
|
|
147
|
+
|
|
148
|
+
console.log(
|
|
149
|
+
` ${C.dim}${padRight(colFile, 40)} ${padLeft(colLines, 6)} ${padLeft(colHolo, 6)} ${padLeft(colOrig, 8)} ${padLeft(colComp, 8)} ${padLeft(colSave, 8)}${C.reset}`
|
|
150
|
+
);
|
|
151
|
+
console.log(` ${C.dim}${"─".repeat(80)}${C.reset}`);
|
|
152
|
+
|
|
153
|
+
// ── Rows ──
|
|
154
|
+
for (const r of display) {
|
|
155
|
+
const sc = savingsColor(r.savings);
|
|
156
|
+
const savingsStr = `${sc}${r.savings.toFixed(1)}%${C.reset}`;
|
|
157
|
+
console.log(
|
|
158
|
+
` ${padRight(r.path, 40)} ${padLeft(String(r.originalLines), 6)} ${padLeft(String(r.hologramLines), 6)} ${padLeft(formatChars(r.originalChars), 8)} ${padLeft(formatChars(r.hologramChars), 8)} ${padLeft(savingsStr, 8)}`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (limit < results.length) {
|
|
163
|
+
console.log(` ${C.dim}... ${ko ? `외 ${results.length - limit}개 파일` : `and ${results.length - limit} more files`}${C.reset}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Summary ──
|
|
167
|
+
const totalOriginal = results.reduce((s, r) => s + r.originalChars, 0);
|
|
168
|
+
const totalHologram = results.reduce((s, r) => s + r.hologramChars, 0);
|
|
169
|
+
const totalSaved = totalOriginal - totalHologram;
|
|
170
|
+
const overallPct = totalOriginal > 0 ? Math.round((1 - totalHologram / totalOriginal) * 1000) / 10 : 0;
|
|
171
|
+
const estimatedTokens = Math.round(totalSaved / 4);
|
|
172
|
+
const high = results.filter(r => r.savings >= 70).length;
|
|
173
|
+
|
|
174
|
+
console.log();
|
|
175
|
+
console.log(` ${C.dim}${"─".repeat(80)}${C.reset}`);
|
|
176
|
+
console.log(` ${C.bold}${ko ? "요약" : "Summary"}${C.reset}`);
|
|
177
|
+
console.log();
|
|
178
|
+
console.log(` ${ko ? "분석 파일" : "Files analyzed"} ${C.bold}${results.length}${C.reset}`);
|
|
179
|
+
console.log(` ${ko ? "전체 압축률" : "Overall compression"} ${C.bold}${savingsColor(overallPct)}${overallPct}%${C.reset}`);
|
|
180
|
+
console.log(` ${ko ? "원본 크기" : "Original size"} ${C.bold}${formatChars(totalOriginal)}${C.reset} ${C.dim}(${(totalOriginal / 1024).toFixed(0)} KB)${C.reset}`);
|
|
181
|
+
console.log(` ${ko ? "압축 크기" : "Compressed size"} ${C.bold}${formatChars(totalHologram)}${C.reset} ${C.dim}(${(totalHologram / 1024).toFixed(0)} KB)${C.reset}`);
|
|
182
|
+
console.log(` ${ko ? "절약 크기" : "Saved"} ${C.bold}${C.green}${formatChars(totalSaved)}${C.reset} ${C.dim}(${(totalSaved / 1024).toFixed(0)} KB)${C.reset}`);
|
|
183
|
+
console.log(` ${ko ? "추정 토큰 절약" : "Est. tokens saved"} ${C.bold}${C.green}~${estimatedTokens.toLocaleString()}${C.reset}`);
|
|
184
|
+
console.log(` ${ko ? "70%+ 압축 파일" : "70%+ compression"} ${C.bold}${high}${C.reset}/${results.length} ${C.dim}(${Math.round(high / results.length * 100)}%)${C.reset}`);
|
|
185
|
+
console.log(` ${ko ? "처리 시간" : "Elapsed"} ${C.dim}${elapsed}ms${C.reset}`);
|
|
186
|
+
console.log();
|
|
187
|
+
}
|