autonomous-flow-daemon 1.1.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.ko.md +124 -164
  3. package/README.md +99 -170
  4. package/package.json +11 -5
  5. package/src/adapters/index.ts +246 -35
  6. package/src/cli.ts +71 -1
  7. package/src/commands/benchmark.ts +187 -0
  8. package/src/commands/diagnose.ts +56 -14
  9. package/src/commands/doctor.ts +243 -0
  10. package/src/commands/evolution.ts +107 -0
  11. package/src/commands/fix.ts +22 -2
  12. package/src/commands/hooks.ts +136 -0
  13. package/src/commands/mcp.ts +129 -0
  14. package/src/commands/restart.ts +14 -0
  15. package/src/commands/score.ts +164 -96
  16. package/src/commands/start.ts +74 -15
  17. package/src/commands/stats.ts +103 -0
  18. package/src/commands/status.ts +157 -0
  19. package/src/commands/stop.ts +23 -4
  20. package/src/commands/sync.ts +253 -20
  21. package/src/commands/vaccine.ts +177 -0
  22. package/src/constants.ts +25 -1
  23. package/src/core/boast.ts +27 -12
  24. package/src/core/db.ts +74 -3
  25. package/src/core/evolution.ts +215 -0
  26. package/src/core/hologram/engine.ts +71 -0
  27. package/src/core/hologram/fallback.ts +11 -0
  28. package/src/core/hologram/incremental.ts +227 -0
  29. package/src/core/hologram/py-extractor.ts +132 -0
  30. package/src/core/hologram/ts-extractor.ts +320 -0
  31. package/src/core/hologram/types.ts +25 -0
  32. package/src/core/hologram.ts +64 -236
  33. package/src/core/hook-manager.ts +259 -0
  34. package/src/core/i18n/messages.ts +43 -0
  35. package/src/core/immune.ts +8 -123
  36. package/src/core/log-rotate.ts +33 -0
  37. package/src/core/log-utils.ts +38 -0
  38. package/src/core/lru-map.ts +61 -0
  39. package/src/core/notify.ts +27 -19
  40. package/src/core/rule-engine.ts +287 -0
  41. package/src/core/semantic-diff.ts +432 -0
  42. package/src/core/telemetry.ts +94 -0
  43. package/src/core/vaccine-registry.ts +212 -0
  44. package/src/core/workspace.ts +28 -0
  45. package/src/core/yaml-minimal.ts +176 -0
  46. package/src/daemon/client.ts +34 -6
  47. package/src/daemon/event-batcher.ts +108 -0
  48. package/src/daemon/guards.ts +13 -0
  49. package/src/daemon/http-routes.ts +293 -0
  50. package/src/daemon/mcp-handler.ts +270 -0
  51. package/src/daemon/server.ts +439 -353
  52. package/src/daemon/types.ts +100 -0
  53. package/src/daemon/workspace-map.ts +92 -0
  54. package/src/platform.ts +23 -2
  55. package/src/version.ts +15 -0
@@ -0,0 +1,293 @@
1
+ /**
2
+ * HTTP routes for daemon IPC — extracted from server.ts.
3
+ */
4
+
5
+ import { readFileSync, writeFileSync } from "fs";
6
+ import { resolve } from "path";
7
+ import { generateHologram } from "../core/hologram";
8
+ import { diagnose } from "../core/immune";
9
+ import type { PatchOp } from "../core/immune";
10
+ import { buildShiftSummary } from "../core/boast";
11
+ import { analyzeQuarantine, listQuarantine, evolve } from "../core/evolution";
12
+ import { MAX_SSE_CLIENTS } from "./types";
13
+ import type { DaemonContext } from "./types";
14
+ import { assertInsideWorkspace as _assertWs } from "./guards";
15
+
16
+ /** Create the HTTP fetch handler for Bun.serve */
17
+ export function createHttpHandler(ctx: DaemonContext, cleanup: () => void) {
18
+ return async function fetch(req: Request): Promise<Response> {
19
+ const url = new URL(req.url);
20
+
21
+ if (url.pathname === "/health") {
22
+ return Response.json({ status: "alive", pid: process.pid, workspace: ctx.ws.root, port: ctx.port });
23
+ }
24
+
25
+ if (url.pathname === "/mini-status") {
26
+ const last = ctx.state.autoHealLog.length > 0
27
+ ? ctx.state.autoHealLog[ctx.state.autoHealLog.length - 1].id
28
+ : null;
29
+ // Defense reasons from in-memory mistakeCache (not DB query — stays under 200ms)
30
+ const reasonSet = new Set<string>();
31
+ for (const entries of ctx.state.mistakeCache.values()) {
32
+ for (const e of entries) {
33
+ reasonSet.add(e.mistake_type);
34
+ if (reasonSet.size >= 3) break;
35
+ }
36
+ if (reasonSet.size >= 3) break;
37
+ }
38
+ return Response.json({
39
+ status: "ON",
40
+ healed_count: ctx.state.autoHealCount,
41
+ last_healed: last,
42
+ total_defenses: ctx.state.autoHealCount,
43
+ defense_reasons: [...reasonSet],
44
+ saved_tokens_k: Math.round(Math.max(0, ctx.state.hologramStats.totalOriginalChars - ctx.state.hologramStats.totalHologramChars) / 4 / 100) / 10,
45
+ session_saved_tokens_k: Math.round(Math.max(0, ctx.state.hologramStats.sessionOriginalChars - ctx.state.hologramStats.sessionHologramChars) / 4 / 100) / 10,
46
+ });
47
+ }
48
+
49
+ // Track HTTP API calls as telemetry
50
+ const _apiPath = url.pathname.replace(/^\//, "");
51
+ if (["/hologram", "/read", "/diagnose", "/score", "/evolution", "/sync"].includes(url.pathname)) {
52
+ try { ctx.insertTelemetry.run("mcp", `http_${_apiPath}`, null, null, Date.now()); } catch { /* crash-only */ }
53
+ }
54
+
55
+ if (url.pathname === "/hologram") {
56
+ const file = url.searchParams.get("file");
57
+ if (!file) return Response.json({ error: "?file= required" }, { status: 400 });
58
+ try {
59
+ const absPath = resolve(file);
60
+ _assertWs(absPath, ctx.ws.root);
61
+ const source = readFileSync(absPath, "utf-8");
62
+ const contextFile = url.searchParams.get("contextFile");
63
+ const result = await generateHologram(file, source, contextFile ? { contextFile: resolve(contextFile) } : undefined);
64
+ ctx.persistHologramStats(result.originalLength, result.hologramLength);
65
+ return Response.json(result);
66
+ } catch (err: unknown) {
67
+ return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 404 });
68
+ }
69
+ }
70
+
71
+ if (url.pathname === "/workspace-map") {
72
+ return new Response(ctx.getWorkspaceMap(), { headers: { "Content-Type": "text/plain" } });
73
+ }
74
+
75
+ if (url.pathname === "/read") {
76
+ const file = url.searchParams.get("file");
77
+ if (!file) return Response.json({ error: "?file= required" }, { status: 400 });
78
+ try {
79
+ const AFD_READ_THRESHOLD = 10 * 1024;
80
+ const absPath = resolve(file);
81
+ _assertWs(absPath, ctx.ws.root);
82
+ const source = readFileSync(absPath, "utf-8");
83
+ const rawStart = parseInt(url.searchParams.get("startLine") ?? "", 10);
84
+ const rawEnd = parseInt(url.searchParams.get("endLine") ?? "", 10);
85
+
86
+ if (Number.isFinite(rawStart) && Number.isFinite(rawEnd)) {
87
+ const lines = source.split("\n");
88
+ const s = Math.max(1, rawStart) - 1;
89
+ const e = Math.min(lines.length, rawEnd);
90
+ return Response.json({ file, lines: lines.slice(s, e), range: [s + 1, e], totalLines: lines.length });
91
+ }
92
+ if (source.length < AFD_READ_THRESHOLD) {
93
+ return Response.json({ file, content: source, mode: "full" });
94
+ }
95
+ const result = await generateHologram(file, source);
96
+ ctx.persistHologramStats(result.originalLength, result.hologramLength);
97
+ return Response.json({ file, hologram: result.hologram, mode: "hologram", originalSize: source.length, totalLines: source.split("\n").length });
98
+ } catch (err: unknown) {
99
+ return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 404 });
100
+ }
101
+ }
102
+
103
+ if (url.pathname === "/mistake-history") {
104
+ const file = url.searchParams.get("file");
105
+ if (!file) return Response.json({ error: "?file= required" }, { status: 400 });
106
+ const normalizedFile = file.replace(/\\/g, "/");
107
+ const cached = ctx.state.mistakeCache.get(normalizedFile);
108
+ return Response.json({ mistakes: cached ?? [] });
109
+ }
110
+
111
+ if (url.pathname === "/diagnose") {
112
+ const raw = url.searchParams.get("raw") === "true";
113
+ const known = ctx.antibodyIds.all().map(r => r.id);
114
+ const result = diagnose(known, { raw });
115
+ const PROACTIVE_HOLOGRAM_THRESHOLD = 5 * 1024;
116
+
117
+ const enriched = await Promise.all(result.symptoms.map(async (s: { fileTarget: string; [k: string]: unknown }) => {
118
+ const snapshot = ctx.state.fileSnapshots.get(s.fileTarget)
119
+ ?? ctx.state.fileSnapshots.get(s.fileTarget.replace(/\//g, "\\"));
120
+ if (!snapshot) return s;
121
+ if (snapshot.length > PROACTIVE_HOLOGRAM_THRESHOLD) {
122
+ const hologram = await ctx.safeHologram(s.fileTarget, snapshot);
123
+ return {
124
+ ...s, hologram,
125
+ contextNote: `File is ${(snapshot.length / 1024).toFixed(1)}KB — hologram skeleton provided to save tokens (${Math.round((1 - hologram.length / snapshot.length) * 100)}% reduction).`,
126
+ };
127
+ }
128
+ return { ...s, context: snapshot };
129
+ }));
130
+ return Response.json({ ...result, symptoms: enriched });
131
+ }
132
+
133
+ if (url.pathname === "/antibodies") {
134
+ return Response.json({ antibodies: ctx.listAntibodies.all() });
135
+ }
136
+
137
+ if (url.pathname === "/antibodies/learn" && req.method === "POST") {
138
+ try {
139
+ const body = await req.json() as { id: string; patternType: string; fileTarget: string; patches: PatchOp[] };
140
+ ctx.insertAntibody.run(body.id, body.patternType, body.fileTarget, JSON.stringify(body.patches));
141
+ return Response.json({ status: "learned", id: body.id });
142
+ } catch (err: unknown) {
143
+ return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 400 });
144
+ }
145
+ }
146
+
147
+ if (url.pathname === "/auto-heal/record" && req.method === "POST") {
148
+ try {
149
+ const body = await req.json() as { id: string };
150
+ ctx.state.autoHealCount++;
151
+ ctx.state.autoHealLog.push({ id: body.id, at: Date.now() });
152
+ if (ctx.state.autoHealLog.length > 100) ctx.state.autoHealLog.shift();
153
+ try { ctx.insertTelemetry.run("immune", "heal_hit", JSON.stringify({ antibodyId: body.id }), null, Date.now()); } catch { /* crash-only */ }
154
+ return Response.json({ status: "recorded", total: ctx.state.autoHealCount });
155
+ } catch (err: unknown) {
156
+ return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 400 });
157
+ }
158
+ }
159
+
160
+ if (url.pathname === "/score") {
161
+ const uptime = Math.floor((Date.now() - ctx.state.startedAt) / 1000);
162
+ const eventCount = ctx.db.query("SELECT COUNT(*) as cnt FROM events").get() as { cnt: number };
163
+ const abCount = ctx.countAntibodies.get() as { cnt: number };
164
+ const hs = ctx.state.hologramStats;
165
+ const globalSavings = hs.totalOriginalChars > 0
166
+ ? Math.round((hs.totalOriginalChars - hs.totalHologramChars) / hs.totalOriginalChars * 1000) / 10
167
+ : 0;
168
+ const dailyRows = ctx.getDailyAll.all() as { date: string; requests: number; original_chars: number; hologram_chars: number }[];
169
+ const todayRow = dailyRows.find(r => r.date === ctx.today());
170
+ return Response.json({
171
+ uptime,
172
+ filesDetected: ctx.state.filesDetected,
173
+ totalEvents: eventCount.cnt,
174
+ lastEvent: ctx.state.lastEvent,
175
+ lastEventAt: ctx.state.lastEventAt,
176
+ watchedFiles: [...ctx.state.watchedFiles],
177
+ watchTargets: ctx.discoveryTargets,
178
+ hologram: {
179
+ lifetime: { requests: hs.totalRequests, originalChars: hs.totalOriginalChars, hologramChars: hs.totalHologramChars, savings: globalSavings },
180
+ today: todayRow ? {
181
+ requests: todayRow.requests, originalChars: todayRow.original_chars, hologramChars: todayRow.hologram_chars,
182
+ savings: todayRow.original_chars > 0 ? Math.round((todayRow.original_chars - todayRow.hologram_chars) / todayRow.original_chars * 1000) / 10 : 0,
183
+ } : null,
184
+ daily: dailyRows.map(r => ({ date: r.date, requests: r.requests, originalChars: r.original_chars, hologramChars: r.hologram_chars })),
185
+ },
186
+ immune: {
187
+ antibodies: abCount.cnt,
188
+ autoHealed: ctx.state.autoHealCount,
189
+ lastAutoHeal: ctx.state.autoHealLog.length > 0 ? ctx.state.autoHealLog[ctx.state.autoHealLog.length - 1] : null,
190
+ },
191
+ ecosystem: {
192
+ detected: ctx.state.ecosystems.map(e => ({ name: e.adapter.name, confidence: e.confidence, schema: e.adapter.getHarnessSchema() })),
193
+ primary: ctx.state.ecosystems[0]?.adapter.name ?? "Unknown",
194
+ },
195
+ suppression: {
196
+ massEventsSkipped: ctx.state.suppressionSkippedCount,
197
+ dormantTransitions: ctx.state.dormantTransitions.length,
198
+ activeTaps: ctx.state.firstTapTimestamps.size,
199
+ },
200
+ evolution: (() => {
201
+ const q = listQuarantine();
202
+ return { totalQuarantined: q.length, totalLearned: q.filter(e => e.learned).length, pending: q.filter(e => !e.learned).length };
203
+ })(),
204
+ dynamicImmune: {
205
+ activeValidators: ctx.state.customValidators.size,
206
+ validatorNames: [...ctx.state.customValidators.keys()],
207
+ },
208
+ });
209
+ }
210
+
211
+ if (url.pathname === "/evolution") {
212
+ return Response.json(evolve());
213
+ }
214
+
215
+ if (url.pathname === "/evolution/status") {
216
+ const q = listQuarantine();
217
+ const stats = analyzeQuarantine();
218
+ return Response.json({
219
+ totalQuarantined: q.length, totalLearned: q.filter(e => e.learned).length, pending: stats.pending,
220
+ lessons: stats.lessons.map(l => ({ file: l.entry.originalPath, type: l.failureType, timestamp: l.entry.timestamp, suggestion: l.suggestion })),
221
+ });
222
+ }
223
+
224
+ if (url.pathname === "/sync") {
225
+ const rows = ctx.listAntibodies.all() as { id: string; pattern_type: string; file_target: string; patch_op: string; created_at: string }[];
226
+ const sanitized = rows.flatMap(r => {
227
+ let patches: PatchOp[];
228
+ try { patches = JSON.parse(r.patch_op) as PatchOp[]; } catch { return []; }
229
+ const cleanPatches = patches.map(p => ({
230
+ ...p,
231
+ path: p.path.replace(/^[A-Za-z]:/, "").replace(/\\/g, "/"),
232
+ value: p.value?.replace(/[A-Za-z]:\\[^\s"']*/g, "<redacted>"),
233
+ }));
234
+ return [{ id: r.id, patternType: r.pattern_type, fileTarget: r.file_target.replace(/^[A-Za-z]:/, "").replace(/\\/g, "/"), patches: cleanPatches, learnedAt: r.created_at }];
235
+ });
236
+ const payload = {
237
+ version: "0.1.0", generatedAt: new Date().toISOString(),
238
+ ecosystem: ctx.state.ecosystems[0]?.adapter.name ?? "Unknown",
239
+ antibodyCount: sanitized.length, antibodies: sanitized,
240
+ };
241
+ const payloadPath = resolve(ctx.ws.afdDir, "global-vaccine-payload.json");
242
+ writeFileSync(payloadPath, JSON.stringify(payload, null, 2), "utf-8");
243
+ return Response.json({ status: "exported", path: payloadPath, count: sanitized.length });
244
+ }
245
+
246
+ if (url.pathname === "/shift-summary") {
247
+ const uptime = Math.floor((Date.now() - ctx.state.startedAt) / 1000);
248
+ const eventCount = ctx.db.query("SELECT COUNT(*) as cnt FROM events").get() as { cnt: number };
249
+ const hs = ctx.state.hologramStats;
250
+ const hologramSavedChars = Math.max(0, hs.totalOriginalChars - hs.totalHologramChars);
251
+ return Response.json(buildShiftSummary({
252
+ uptimeSeconds: uptime, totalEvents: eventCount.cnt, healsPerformed: ctx.state.autoHealCount,
253
+ totalFileBytesSaved: ctx.state.totalFileBytesSaved, suppressionsSkipped: ctx.state.suppressionSkippedCount,
254
+ dormantTransitions: ctx.state.dormantTransitions.length, hologramSavedChars,
255
+ }));
256
+ }
257
+
258
+ if (url.pathname === "/events") {
259
+ if (ctx.state.sseClients.size >= MAX_SSE_CLIENTS) {
260
+ return Response.json({ error: "Too many SSE clients" }, { status: 429 });
261
+ }
262
+ let sseController: ReadableStreamDefaultController<Uint8Array> | null = null;
263
+ const stream = new ReadableStream<Uint8Array>({
264
+ start(controller) { sseController = controller; ctx.state.sseClients.add(controller); },
265
+ cancel() { if (sseController) ctx.state.sseClients.delete(sseController); },
266
+ });
267
+ return new Response(stream, {
268
+ headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive" },
269
+ });
270
+ }
271
+
272
+ if (url.pathname === "/telemetry") {
273
+ const days = parseInt(url.searchParams.get("days") ?? "7", 10) || 7;
274
+ const since = Date.now() - days * 86_400_000;
275
+ try {
276
+ const rows = ctx.db.prepare(
277
+ "SELECT category, action, COUNT(*) as cnt, AVG(duration_ms) as avg_ms FROM telemetry WHERE timestamp >= ? GROUP BY category, action ORDER BY cnt DESC"
278
+ ).all(since) as { category: string; action: string; cnt: number; avg_ms: number | null }[];
279
+ return Response.json({ days, rows });
280
+ } catch {
281
+ return Response.json({ days, rows: [] });
282
+ }
283
+ }
284
+
285
+ if (url.pathname === "/stop") {
286
+ cleanup();
287
+ setTimeout(() => process.exit(0), 100);
288
+ return Response.json({ status: "stopping" });
289
+ }
290
+
291
+ return Response.json({ error: "not found" }, { status: 404 });
292
+ };
293
+ }
@@ -0,0 +1,270 @@
1
+ /**
2
+ * MCP stdio handler — JSON-RPC dispatcher for tools and resources.
3
+ * Extracted from server.ts for modularity.
4
+ */
5
+
6
+ import { readFileSync } from "fs";
7
+ import { resolve } from "path";
8
+ import { generateHologram } from "../core/hologram";
9
+ import { diagnose } from "../core/immune";
10
+ import type { DaemonContext } from "./types";
11
+ import { APP_VERSION } from "../version";
12
+ import { assertInsideWorkspace as _assertWs } from "./guards";
13
+
14
+ const MCP_MAX_BUFFER = 1024 * 1024; // 1 MB
15
+
16
+ const mcpToolDefs = [
17
+ {
18
+ name: "afd_diagnose",
19
+ description: "Run health diagnosis on the current project. Returns symptoms and healthy checks.",
20
+ inputSchema: {
21
+ type: "object" as const,
22
+ properties: {
23
+ raw: { type: "boolean" as const, description: "If true, report all symptoms ignoring antibodies" },
24
+ },
25
+ },
26
+ },
27
+ {
28
+ name: "afd_score",
29
+ description: "Get daemon runtime stats: uptime, events, heals, hologram savings, suppression metrics.",
30
+ inputSchema: { type: "object" as const, properties: {} },
31
+ },
32
+ {
33
+ name: "afd_hologram",
34
+ description: "Generate a token-efficient hologram (type skeleton) for a source file (TS, JS, Python). Use contextFile for L1 filtering. Use diffOnly for incremental updates showing only changed declarations.",
35
+ inputSchema: {
36
+ type: "object" as const,
37
+ properties: {
38
+ file: { type: "string" as const, description: "Relative or absolute file path" },
39
+ contextFile: { type: "string" as const, description: "Optional: the file that imports from 'file'. Enables L1 filtering for higher compression." },
40
+ diffOnly: { type: "boolean" as const, description: "Optional: return only changed declarations since last hologram call (unified-diff format). Saves tokens on repeated reads." },
41
+ },
42
+ required: ["file"],
43
+ },
44
+ },
45
+ {
46
+ name: "afd_read",
47
+ description: "Smart file reader that saves tokens. Files <10KB return full content. Files >=10KB return a structural hologram instead. Use 'startLine' and 'endLine' to read specific line ranges of large files at full fidelity.",
48
+ inputSchema: {
49
+ type: "object" as const,
50
+ properties: {
51
+ file: { type: "string" as const, description: "Relative or absolute file path" },
52
+ startLine: { type: "number" as const, description: "Start line number (1-based, inclusive). Use with endLine to read a specific range of large files." },
53
+ endLine: { type: "number" as const, description: "End line number (1-based, inclusive)." },
54
+ },
55
+ required: ["file"],
56
+ },
57
+ },
58
+ ];
59
+
60
+ function mcpResponse(id: unknown, result: unknown) {
61
+ process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, result }) + "\n");
62
+ }
63
+
64
+ function mcpError(id: unknown, code: number, message: string) {
65
+ process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } }) + "\n");
66
+ }
67
+
68
+
69
+ async function handleMcpRequest(ctx: DaemonContext, req: { id?: unknown; method?: string; params?: Record<string, unknown> }) {
70
+ const { id, method, params } = req;
71
+
72
+ if (method === "initialize") {
73
+ mcpResponse(id, {
74
+ protocolVersion: "2024-11-05",
75
+ capabilities: { tools: {}, resources: {} },
76
+ serverInfo: { name: "afd", version: APP_VERSION },
77
+ });
78
+ return;
79
+ }
80
+
81
+ if (method === "notifications/initialized") return;
82
+
83
+ if (method === "tools/list") {
84
+ mcpResponse(id, { tools: mcpToolDefs });
85
+ return;
86
+ }
87
+
88
+ if (method === "resources/list") {
89
+ mcpResponse(id, {
90
+ resources: [{
91
+ uri: "afd://workspace-map",
92
+ name: "Workspace Map",
93
+ description: "Project file tree with export signatures. Read this first to understand the codebase structure before reading individual files.",
94
+ mimeType: "text/plain",
95
+ }],
96
+ });
97
+ return;
98
+ }
99
+
100
+ if (method === "resources/read") {
101
+ const uri = params?.uri as string | undefined;
102
+ if (uri === "afd://workspace-map") {
103
+ mcpResponse(id, {
104
+ contents: [{ uri: "afd://workspace-map", mimeType: "text/plain", text: ctx.getWorkspaceMap() }],
105
+ });
106
+ return;
107
+ }
108
+ mcpError(id, -32602, `Unknown resource: ${uri}`);
109
+ return;
110
+ }
111
+
112
+ if (method === "tools/call") {
113
+ const toolName = params?.name as string | undefined;
114
+ const args = (params?.arguments ?? {}) as Record<string, unknown>;
115
+
116
+ // Track MCP tool call
117
+ if (toolName) {
118
+ try { ctx.insertTelemetry.run("mcp", toolName, null, null, Date.now()); } catch { /* crash-only */ }
119
+ }
120
+
121
+ if (toolName === "afd_diagnose") {
122
+ const raw = args.raw === true;
123
+ const known = ctx.antibodyIds.all().map(r => r.id);
124
+ const result = diagnose(known, { raw });
125
+
126
+ const PROACTIVE_THRESHOLD = 5 * 1024;
127
+ const enriched = await Promise.all(result.symptoms.map(async (s: { fileTarget: string; [k: string]: unknown }) => {
128
+ const snapshot = ctx.state.fileSnapshots.get(s.fileTarget)
129
+ ?? ctx.state.fileSnapshots.get(s.fileTarget.replace(/\//g, "\\"));
130
+ if (!snapshot) return s;
131
+ if (snapshot.length > PROACTIVE_THRESHOLD) {
132
+ const hologram = await ctx.safeHologram(s.fileTarget, snapshot);
133
+ return { ...s, hologram, contextNote: `File is ${(snapshot.length / 1024).toFixed(1)}KB — hologram provided to save tokens.` };
134
+ }
135
+ return { ...s, context: snapshot };
136
+ }));
137
+
138
+ mcpResponse(id, {
139
+ content: [{ type: "text", text: JSON.stringify({ ...result, symptoms: enriched }, null, 2), cache_control: { type: "ephemeral" } }],
140
+ });
141
+ return;
142
+ }
143
+
144
+ if (toolName === "afd_score") {
145
+ const uptime = Math.floor((Date.now() - ctx.state.startedAt) / 1000);
146
+ const eventCount = ctx.db.query("SELECT COUNT(*) as cnt FROM events").get() as { cnt: number };
147
+ const abCount = ctx.countAntibodies.get() as { cnt: number };
148
+ const hs = ctx.state.hologramStats;
149
+ mcpResponse(id, {
150
+ content: [{ type: "text", text: JSON.stringify({
151
+ uptime,
152
+ filesDetected: ctx.state.filesDetected,
153
+ totalEvents: eventCount.cnt,
154
+ antibodies: abCount.cnt,
155
+ autoHealed: ctx.state.autoHealCount,
156
+ hologramRequests: hs.totalRequests,
157
+ hologramSavings: hs.totalOriginalChars > 0
158
+ ? `${Math.round((hs.totalOriginalChars - hs.totalHologramChars) / hs.totalOriginalChars * 100)}%`
159
+ : "0%",
160
+ suppression: {
161
+ massEventsSkipped: ctx.state.suppressionSkippedCount,
162
+ dormantTransitions: ctx.state.dormantTransitions.length,
163
+ },
164
+ }, null, 2), cache_control: { type: "ephemeral" } }],
165
+ });
166
+ return;
167
+ }
168
+
169
+ if (toolName === "afd_hologram") {
170
+ const file = args.file as string | undefined;
171
+ if (!file) { mcpError(id, -32602, "Missing required argument: file"); return; }
172
+ try {
173
+ const absPath = resolve(file);
174
+ _assertWs(absPath, ctx.ws.root);
175
+ const source = readFileSync(absPath, "utf-8");
176
+ const contextFile = args.contextFile as string | undefined;
177
+ const diffOnly = args.diffOnly === true;
178
+ const opts: Record<string, unknown> = {};
179
+ if (contextFile) opts.contextFile = resolve(contextFile);
180
+ if (diffOnly) opts.diffOnly = true;
181
+ const result = await generateHologram(file, source, Object.keys(opts).length > 0 ? opts as { contextFile?: string; diffOnly?: boolean } : undefined);
182
+ ctx.persistHologramStats(result.originalLength, result.hologramLength);
183
+ mcpResponse(id, {
184
+ content: [{ type: "text", text: result.hologram, cache_control: { type: "ephemeral" } }],
185
+ });
186
+ } catch (err: unknown) {
187
+ mcpError(id, -32603, err instanceof Error ? err.message : String(err));
188
+ }
189
+ return;
190
+ }
191
+
192
+ if (toolName === "afd_read") {
193
+ const file = args.file as string | undefined;
194
+ if (!file) { mcpError(id, -32602, "Missing required argument: file"); return; }
195
+ try {
196
+ const AFD_READ_THRESHOLD = 10 * 1024;
197
+ const absPath = resolve(file);
198
+ _assertWs(absPath, ctx.ws.root);
199
+ const source = readFileSync(absPath, "utf-8");
200
+ const sizeKB = (source.length / 1024).toFixed(1);
201
+ const rawStart = args.startLine;
202
+ const rawEnd = args.endLine;
203
+ const startLine = typeof rawStart === "number" && Number.isFinite(rawStart) ? rawStart : undefined;
204
+ const endLine = typeof rawEnd === "number" && Number.isFinite(rawEnd) ? rawEnd : undefined;
205
+
206
+ if (startLine !== undefined && endLine !== undefined) {
207
+ const lines = source.split("\n");
208
+ const start = Math.max(1, Math.floor(startLine)) - 1;
209
+ const end = Math.min(lines.length, Math.floor(endLine));
210
+ const slice = lines.slice(start, end).map((l, i) => `${start + i + 1}\t${l}`).join("\n");
211
+ mcpResponse(id, {
212
+ content: [{ type: "text", text: `// ${file} lines ${start + 1}-${end} (${sizeKB}KB total)\n${slice}`, cache_control: { type: "ephemeral" } }],
213
+ });
214
+ return;
215
+ }
216
+
217
+ if (source.length < AFD_READ_THRESHOLD) {
218
+ mcpResponse(id, {
219
+ content: [{ type: "text", text: source, cache_control: { type: "ephemeral" } }],
220
+ });
221
+ return;
222
+ }
223
+
224
+ const result = await generateHologram(file, source);
225
+ ctx.persistHologramStats(result.originalLength, result.hologramLength);
226
+ const savings = Math.round((1 - result.hologramLength / result.originalLength) * 100);
227
+ const header = `⚠️ [afd-Optimizer]: File is ${sizeKB}KB. To save tokens, only the structural hologram is provided (${savings}% reduction).\nUse afd_read with startLine/endLine to read specific sections at full fidelity.\nTotal lines: ${source.split("\n").length}\n\n`;
228
+ mcpResponse(id, {
229
+ content: [{ type: "text", text: header + result.hologram, cache_control: { type: "ephemeral" } }],
230
+ });
231
+ } catch (err: unknown) {
232
+ mcpError(id, -32603, err instanceof Error ? err.message : String(err));
233
+ }
234
+ return;
235
+ }
236
+
237
+ mcpError(id, -32601, `Unknown tool: ${toolName}`);
238
+ return;
239
+ }
240
+
241
+ mcpError(id, -32601, `Unknown method: ${method}`);
242
+ }
243
+
244
+ /** Start MCP stdio mode — blocks until stdin closes */
245
+ export function startMcpStdio(ctx: DaemonContext) {
246
+ console.error("[afd] MCP stdio mode — awaiting JSON-RPC on stdin");
247
+
248
+ (async () => {
249
+ const decoder = new TextDecoder();
250
+ let buffer = "";
251
+ for await (const chunk of Bun.stdin.stream()) {
252
+ buffer += decoder.decode(chunk);
253
+ if (buffer.length > MCP_MAX_BUFFER) {
254
+ console.error("[afd] MCP buffer overflow — dropping buffer");
255
+ buffer = "";
256
+ continue;
257
+ }
258
+ let newlineIdx: number;
259
+ while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
260
+ const line = buffer.slice(0, newlineIdx).trim();
261
+ buffer = buffer.slice(newlineIdx + 1);
262
+ if (!line) continue;
263
+ try { handleMcpRequest(ctx, JSON.parse(line)); } catch { /* crash-only */ }
264
+ }
265
+ }
266
+ })().catch((err) => {
267
+ console.error("[afd] MCP stdin loop error:", err instanceof Error ? err.message : String(err));
268
+ process.exit(1);
269
+ });
270
+ }