autonomous-flow-daemon 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.
- package/CHANGELOG.md +46 -0
- package/LICENSE +21 -0
- package/README.ko.md +249 -0
- package/README.md +281 -0
- package/mcp-config.json +10 -0
- package/package.json +39 -0
- package/src/adapters/index.ts +158 -0
- package/src/cli.ts +49 -0
- package/src/commands/diagnose.ts +151 -0
- package/src/commands/fix.ts +138 -0
- package/src/commands/score.ts +148 -0
- package/src/commands/start.ts +55 -0
- package/src/commands/stop.ts +35 -0
- package/src/commands/sync.ts +50 -0
- package/src/constants.ts +7 -0
- package/src/core/db.ts +46 -0
- package/src/core/hologram.ts +243 -0
- package/src/core/immune.ts +150 -0
- package/src/core/notify.ts +35 -0
- package/src/daemon/client.ts +37 -0
- package/src/daemon/server.ts +371 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { watch } from "chokidar";
|
|
2
|
+
import { mkdirSync, writeFileSync, unlinkSync, readFileSync, existsSync } from "fs";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
import { AFD_DIR, PID_FILE, PORT_FILE, WATCH_TARGETS } from "../constants";
|
|
5
|
+
import { initDb } from "../core/db";
|
|
6
|
+
import { generateHologram } from "../core/hologram";
|
|
7
|
+
import { diagnose } from "../core/immune";
|
|
8
|
+
import type { PatchOp } from "../core/immune";
|
|
9
|
+
import { detectEcosystem } from "../adapters/index";
|
|
10
|
+
import type { DetectionResult } from "../adapters/index";
|
|
11
|
+
|
|
12
|
+
// ── Suppression Safety Constants ──
|
|
13
|
+
const DOUBLE_TAP_WINDOW_MS = 60_000; // 60 seconds
|
|
14
|
+
const MASS_EVENT_THRESHOLD = 3; // >3 unlinks in 1 second
|
|
15
|
+
const MASS_EVENT_WINDOW_MS = 1_000; // 1 second window
|
|
16
|
+
|
|
17
|
+
interface HologramStats {
|
|
18
|
+
totalRequests: number;
|
|
19
|
+
totalOriginalChars: number;
|
|
20
|
+
totalHologramChars: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface DaemonState {
|
|
24
|
+
startedAt: number;
|
|
25
|
+
filesDetected: number;
|
|
26
|
+
lastEvent: string | null;
|
|
27
|
+
lastEventAt: number | null;
|
|
28
|
+
watchedFiles: string[];
|
|
29
|
+
hologramStats: HologramStats;
|
|
30
|
+
ecosystems: DetectionResult[];
|
|
31
|
+
autoHealCount: number;
|
|
32
|
+
autoHealLog: { id: string; at: number }[];
|
|
33
|
+
// Suppression safety: recent unlink timestamps for mass-event detection
|
|
34
|
+
recentUnlinks: number[];
|
|
35
|
+
// Suppression safety: per-file first-tap timestamps for double-tap detection
|
|
36
|
+
firstTapTimestamps: Map<string, number>;
|
|
37
|
+
suppressionSkippedCount: number;
|
|
38
|
+
dormantTransitions: { antibodyId: string; at: number }[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const state: DaemonState = {
|
|
42
|
+
startedAt: Date.now(),
|
|
43
|
+
filesDetected: 0,
|
|
44
|
+
lastEvent: null,
|
|
45
|
+
lastEventAt: null,
|
|
46
|
+
watchedFiles: [],
|
|
47
|
+
hologramStats: { totalRequests: 0, totalOriginalChars: 0, totalHologramChars: 0 },
|
|
48
|
+
ecosystems: [],
|
|
49
|
+
autoHealCount: 0,
|
|
50
|
+
autoHealLog: [],
|
|
51
|
+
recentUnlinks: [],
|
|
52
|
+
firstTapTimestamps: new Map(),
|
|
53
|
+
suppressionSkippedCount: 0,
|
|
54
|
+
dormantTransitions: [],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function cleanup() {
|
|
58
|
+
try { unlinkSync(PID_FILE); } catch {}
|
|
59
|
+
try { unlinkSync(PORT_FILE); } catch {}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function main() {
|
|
63
|
+
// Detect ecosystem at startup
|
|
64
|
+
state.ecosystems = detectEcosystem(process.cwd());
|
|
65
|
+
|
|
66
|
+
const db = initDb();
|
|
67
|
+
const insertEvent = db.prepare("INSERT INTO events (type, path, timestamp) VALUES (?, ?, ?)");
|
|
68
|
+
const insertAntibody = db.prepare(
|
|
69
|
+
"INSERT OR REPLACE INTO antibodies (id, pattern_type, file_target, patch_op, created_at) VALUES (?, ?, ?, ?, datetime('now'))"
|
|
70
|
+
);
|
|
71
|
+
const listAntibodies = db.prepare("SELECT * FROM antibodies ORDER BY created_at DESC");
|
|
72
|
+
const antibodyIds = db.prepare("SELECT id FROM antibodies WHERE dormant = 0");
|
|
73
|
+
const countAntibodies = db.prepare("SELECT COUNT(*) as cnt FROM antibodies");
|
|
74
|
+
const insertUnlinkLog = db.prepare("INSERT INTO unlink_log (file_path, timestamp) VALUES (?, ?)");
|
|
75
|
+
const findAntibodyByFile = db.prepare("SELECT id, dormant FROM antibodies WHERE file_target = ? AND dormant = 0");
|
|
76
|
+
const setAntibodyDormant = db.prepare("UPDATE antibodies SET dormant = 1 WHERE id = ?");
|
|
77
|
+
|
|
78
|
+
// ── Suppression Safety: Helper Functions ──
|
|
79
|
+
|
|
80
|
+
/** Check if we're in a mass-event burst (>3 unlinks within 1 second) */
|
|
81
|
+
function isMassEvent(now: number): boolean {
|
|
82
|
+
// Prune old entries beyond the window
|
|
83
|
+
state.recentUnlinks = state.recentUnlinks.filter(t => now - t < MASS_EVENT_WINDOW_MS);
|
|
84
|
+
return state.recentUnlinks.length > MASS_EVENT_THRESHOLD;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Clear first-tap state when mass event is detected (bulk ops are not intentional user deletes) */
|
|
88
|
+
function clearTapsOnMassEvent() {
|
|
89
|
+
state.firstTapTimestamps.clear();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Auto-heal: re-apply patches for a given antibody */
|
|
93
|
+
function autoHealFile(antibodyId: string, fileTarget: string, patchOp: string) {
|
|
94
|
+
try {
|
|
95
|
+
const patches = JSON.parse(patchOp) as PatchOp[];
|
|
96
|
+
for (const patch of patches) {
|
|
97
|
+
if (patch.op === "add" && patch.value) {
|
|
98
|
+
const targetPath = resolve(patch.path.replace(/^\//, ""));
|
|
99
|
+
writeFileSync(targetPath, patch.value, "utf-8");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
state.autoHealCount++;
|
|
103
|
+
state.autoHealLog.push({ id: antibodyId, at: Date.now() });
|
|
104
|
+
if (state.autoHealLog.length > 100) state.autoHealLog.shift();
|
|
105
|
+
} catch {
|
|
106
|
+
// Crash-only: if healing fails, let it go
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Handle unlink event with Double-Tap and Mass-Event heuristics.
|
|
112
|
+
* Returns true if the event was handled (healed or made dormant).
|
|
113
|
+
*/
|
|
114
|
+
function handleUnlink(filePath: string, now: number): boolean {
|
|
115
|
+
// Record for mass-event detection
|
|
116
|
+
state.recentUnlinks.push(now);
|
|
117
|
+
insertUnlinkLog.run(filePath, now);
|
|
118
|
+
|
|
119
|
+
// Mass-event check: if >3 unlinks in 1s, skip ALL suppression logic
|
|
120
|
+
if (isMassEvent(now)) {
|
|
121
|
+
state.suppressionSkippedCount++;
|
|
122
|
+
clearTapsOnMassEvent();
|
|
123
|
+
return false; // Do nothing — likely git checkout or bulk operation
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Find active (non-dormant) antibody protecting this file
|
|
127
|
+
const antibody = findAntibodyByFile.get(filePath) as { id: string; dormant: number } | null;
|
|
128
|
+
if (!antibody) return false; // No antibody for this file
|
|
129
|
+
|
|
130
|
+
// Fetch full antibody data for healing
|
|
131
|
+
const fullAntibody = db.prepare("SELECT * FROM antibodies WHERE id = ?").get(antibody.id) as {
|
|
132
|
+
id: string; patch_op: string; file_target: string;
|
|
133
|
+
} | null;
|
|
134
|
+
if (!fullAntibody) return false;
|
|
135
|
+
|
|
136
|
+
// Double-Tap detection
|
|
137
|
+
const previousTap = state.firstTapTimestamps.get(filePath);
|
|
138
|
+
|
|
139
|
+
if (previousTap && (now - previousTap) < DOUBLE_TAP_WINDOW_MS) {
|
|
140
|
+
// SECOND TAP within window → user is intentional → make dormant
|
|
141
|
+
setAntibodyDormant.run(antibody.id);
|
|
142
|
+
state.firstTapTimestamps.delete(filePath);
|
|
143
|
+
state.dormantTransitions.push({ antibodyId: antibody.id, at: now });
|
|
144
|
+
return true; // Dormant — do NOT heal
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// FIRST TAP: record timestamp and auto-heal
|
|
148
|
+
state.firstTapTimestamps.set(filePath, now);
|
|
149
|
+
autoHealFile(fullAntibody.id, fullAntibody.file_target, fullAntibody.patch_op);
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// File watcher
|
|
154
|
+
const watcher = watch(WATCH_TARGETS, {
|
|
155
|
+
ignoreInitial: false,
|
|
156
|
+
persistent: true,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
watcher.on("all", (event, path) => {
|
|
160
|
+
state.filesDetected++;
|
|
161
|
+
state.lastEvent = `${event}:${path}`;
|
|
162
|
+
state.lastEventAt = Date.now();
|
|
163
|
+
if (!state.watchedFiles.includes(path)) {
|
|
164
|
+
state.watchedFiles.push(path);
|
|
165
|
+
}
|
|
166
|
+
insertEvent.run(event, path, Date.now());
|
|
167
|
+
|
|
168
|
+
// Suppression safety: handle unlink events
|
|
169
|
+
if (event === "unlink") {
|
|
170
|
+
handleUnlink(path, Date.now());
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// HTTP server for IPC
|
|
175
|
+
const server = Bun.serve({
|
|
176
|
+
port: 0,
|
|
177
|
+
async fetch(req) {
|
|
178
|
+
const url = new URL(req.url);
|
|
179
|
+
|
|
180
|
+
if (url.pathname === "/health") {
|
|
181
|
+
return Response.json({ status: "alive", pid: process.pid });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (url.pathname === "/mini-status") {
|
|
185
|
+
const last = state.autoHealLog.length > 0
|
|
186
|
+
? state.autoHealLog[state.autoHealLog.length - 1].id
|
|
187
|
+
: null;
|
|
188
|
+
return Response.json({
|
|
189
|
+
status: "ON",
|
|
190
|
+
healed_count: state.autoHealCount,
|
|
191
|
+
last_healed: last,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (url.pathname === "/hologram") {
|
|
196
|
+
const file = url.searchParams.get("file");
|
|
197
|
+
if (!file) return Response.json({ error: "?file= required" }, { status: 400 });
|
|
198
|
+
try {
|
|
199
|
+
const absPath = resolve(file);
|
|
200
|
+
const source = readFileSync(absPath, "utf-8");
|
|
201
|
+
const result = generateHologram(file, source);
|
|
202
|
+
state.hologramStats.totalRequests++;
|
|
203
|
+
state.hologramStats.totalOriginalChars += result.originalLength;
|
|
204
|
+
state.hologramStats.totalHologramChars += result.hologramLength;
|
|
205
|
+
return Response.json(result);
|
|
206
|
+
} catch (err: unknown) {
|
|
207
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
208
|
+
return Response.json({ error: msg }, { status: 404 });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (url.pathname === "/diagnose") {
|
|
213
|
+
const raw = url.searchParams.get("raw") === "true";
|
|
214
|
+
const known = (antibodyIds.all() as { id: string }[]).map(r => r.id);
|
|
215
|
+
const result = diagnose(known, { raw });
|
|
216
|
+
return Response.json(result);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (url.pathname === "/antibodies") {
|
|
220
|
+
const rows = listAntibodies.all();
|
|
221
|
+
return Response.json({ antibodies: rows });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (url.pathname === "/antibodies/learn" && req.method === "POST") {
|
|
225
|
+
try {
|
|
226
|
+
const body = await req.json() as {
|
|
227
|
+
id: string;
|
|
228
|
+
patternType: string;
|
|
229
|
+
fileTarget: string;
|
|
230
|
+
patches: PatchOp[];
|
|
231
|
+
};
|
|
232
|
+
insertAntibody.run(
|
|
233
|
+
body.id,
|
|
234
|
+
body.patternType,
|
|
235
|
+
body.fileTarget,
|
|
236
|
+
JSON.stringify(body.patches)
|
|
237
|
+
);
|
|
238
|
+
return Response.json({ status: "learned", id: body.id });
|
|
239
|
+
} catch (err: unknown) {
|
|
240
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
241
|
+
return Response.json({ error: msg }, { status: 400 });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (url.pathname === "/auto-heal/record" && req.method === "POST") {
|
|
246
|
+
try {
|
|
247
|
+
const body = await req.json() as { id: string };
|
|
248
|
+
state.autoHealCount++;
|
|
249
|
+
state.autoHealLog.push({ id: body.id, at: Date.now() });
|
|
250
|
+
// Keep log bounded
|
|
251
|
+
if (state.autoHealLog.length > 100) state.autoHealLog.shift();
|
|
252
|
+
return Response.json({ status: "recorded", total: state.autoHealCount });
|
|
253
|
+
} catch (err: unknown) {
|
|
254
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
255
|
+
return Response.json({ error: msg }, { status: 400 });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (url.pathname === "/score") {
|
|
260
|
+
const uptime = Math.floor((Date.now() - state.startedAt) / 1000);
|
|
261
|
+
const eventCount = db.query("SELECT COUNT(*) as cnt FROM events").get() as { cnt: number };
|
|
262
|
+
const abCount = countAntibodies.get() as { cnt: number };
|
|
263
|
+
const hs = state.hologramStats;
|
|
264
|
+
const globalSavings = hs.totalOriginalChars > 0
|
|
265
|
+
? Math.round((hs.totalOriginalChars - hs.totalHologramChars) / hs.totalOriginalChars * 1000) / 10
|
|
266
|
+
: 0;
|
|
267
|
+
return Response.json({
|
|
268
|
+
uptime,
|
|
269
|
+
filesDetected: state.filesDetected,
|
|
270
|
+
totalEvents: eventCount.cnt,
|
|
271
|
+
lastEvent: state.lastEvent,
|
|
272
|
+
lastEventAt: state.lastEventAt,
|
|
273
|
+
watchedFiles: state.watchedFiles,
|
|
274
|
+
watchTargets: WATCH_TARGETS,
|
|
275
|
+
hologram: {
|
|
276
|
+
requests: hs.totalRequests,
|
|
277
|
+
originalChars: hs.totalOriginalChars,
|
|
278
|
+
hologramChars: hs.totalHologramChars,
|
|
279
|
+
savings: globalSavings,
|
|
280
|
+
},
|
|
281
|
+
immune: {
|
|
282
|
+
antibodies: abCount.cnt,
|
|
283
|
+
autoHealed: state.autoHealCount,
|
|
284
|
+
lastAutoHeal: state.autoHealLog.length > 0
|
|
285
|
+
? state.autoHealLog[state.autoHealLog.length - 1]
|
|
286
|
+
: null,
|
|
287
|
+
},
|
|
288
|
+
ecosystem: {
|
|
289
|
+
detected: state.ecosystems.map(e => ({
|
|
290
|
+
name: e.adapter.name,
|
|
291
|
+
confidence: e.confidence,
|
|
292
|
+
schema: e.adapter.getHarnessSchema(),
|
|
293
|
+
})),
|
|
294
|
+
primary: state.ecosystems[0]?.adapter.name ?? "Unknown",
|
|
295
|
+
},
|
|
296
|
+
suppression: {
|
|
297
|
+
massEventsSkipped: state.suppressionSkippedCount,
|
|
298
|
+
dormantTransitions: state.dormantTransitions.length,
|
|
299
|
+
activeTaps: state.firstTapTimestamps.size,
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (url.pathname === "/sync") {
|
|
305
|
+
const rows = listAntibodies.all() as {
|
|
306
|
+
id: string;
|
|
307
|
+
pattern_type: string;
|
|
308
|
+
file_target: string;
|
|
309
|
+
patch_op: string;
|
|
310
|
+
created_at: string;
|
|
311
|
+
}[];
|
|
312
|
+
// Sanitize: strip absolute paths, keep only relative patterns
|
|
313
|
+
const sanitized = rows.map(r => {
|
|
314
|
+
const patches = JSON.parse(r.patch_op) as PatchOp[];
|
|
315
|
+
const cleanPatches = patches.map(p => ({
|
|
316
|
+
...p,
|
|
317
|
+
// Ensure paths are relative (strip any leading drive/abs prefix)
|
|
318
|
+
path: p.path.replace(/^[A-Za-z]:/, "").replace(/\\/g, "/"),
|
|
319
|
+
// Strip absolute paths from values
|
|
320
|
+
value: p.value?.replace(/[A-Za-z]:\\[^\s"']*/g, "<redacted>"),
|
|
321
|
+
}));
|
|
322
|
+
return {
|
|
323
|
+
id: r.id,
|
|
324
|
+
patternType: r.pattern_type,
|
|
325
|
+
fileTarget: r.file_target.replace(/^[A-Za-z]:/, "").replace(/\\/g, "/"),
|
|
326
|
+
patches: cleanPatches,
|
|
327
|
+
learnedAt: r.created_at,
|
|
328
|
+
};
|
|
329
|
+
});
|
|
330
|
+
const payload = {
|
|
331
|
+
version: "0.1.0",
|
|
332
|
+
generatedAt: new Date().toISOString(),
|
|
333
|
+
ecosystem: state.ecosystems[0]?.adapter.name ?? "Unknown",
|
|
334
|
+
antibodyCount: sanitized.length,
|
|
335
|
+
antibodies: sanitized,
|
|
336
|
+
};
|
|
337
|
+
// Write payload to disk
|
|
338
|
+
const payloadPath = resolve(AFD_DIR, "global-vaccine-payload.json");
|
|
339
|
+
writeFileSync(payloadPath, JSON.stringify(payload, null, 2), "utf-8");
|
|
340
|
+
return Response.json({ status: "exported", path: payloadPath, count: sanitized.length });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (url.pathname === "/stop") {
|
|
344
|
+
cleanup();
|
|
345
|
+
setTimeout(() => process.exit(0), 100);
|
|
346
|
+
return Response.json({ status: "stopping" });
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return Response.json({ error: "not found" }, { status: 404 });
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
const port = server.port;
|
|
354
|
+
|
|
355
|
+
mkdirSync(AFD_DIR, { recursive: true });
|
|
356
|
+
writeFileSync(PID_FILE, String(process.pid));
|
|
357
|
+
writeFileSync(PORT_FILE, String(port));
|
|
358
|
+
|
|
359
|
+
console.log(`[afd daemon] pid=${process.pid} port=${port}`);
|
|
360
|
+
|
|
361
|
+
process.on("uncaughtException", (err) => {
|
|
362
|
+
console.error("[afd daemon] FATAL:", err.message);
|
|
363
|
+
cleanup();
|
|
364
|
+
process.exit(1);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
process.on("SIGTERM", () => { cleanup(); process.exit(0); });
|
|
368
|
+
process.on("SIGINT", () => { cleanup(); process.exit(0); });
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
main();
|