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.
@@ -0,0 +1,35 @@
1
+ import { getDaemonInfo, isDaemonAlive, daemonRequest } from "../daemon/client";
2
+ import { unlinkSync } from "fs";
3
+ import { PID_FILE, PORT_FILE } from "../constants";
4
+
5
+ function cleanupFiles() {
6
+ try { unlinkSync(PID_FILE); } catch {}
7
+ try { unlinkSync(PORT_FILE); } catch {}
8
+ }
9
+
10
+ export async function stopCommand() {
11
+ const info = getDaemonInfo();
12
+ if (!info) {
13
+ console.log("[afd] No daemon running.");
14
+ return;
15
+ }
16
+
17
+ if (await isDaemonAlive(info)) {
18
+ try {
19
+ await daemonRequest("/stop");
20
+ console.log(`[afd] Daemon stopped (pid=${info.pid})`);
21
+ } catch {
22
+ // Force kill if graceful stop fails
23
+ try {
24
+ process.kill(info.pid, "SIGTERM");
25
+ console.log(`[afd] Daemon killed (pid=${info.pid})`);
26
+ } catch {
27
+ console.log("[afd] Daemon process already gone.");
28
+ }
29
+ }
30
+ } else {
31
+ console.log("[afd] Daemon not responding. Cleaning up stale PID files.");
32
+ }
33
+
34
+ cleanupFiles();
35
+ }
@@ -0,0 +1,50 @@
1
+ import { readFileSync } from "fs";
2
+ import { resolve } from "path";
3
+ import { daemonRequest } from "../daemon/client";
4
+ import { AFD_DIR } from "../constants";
5
+
6
+ interface SyncResponse {
7
+ status: string;
8
+ path: string;
9
+ count: number;
10
+ }
11
+
12
+ export async function syncCommand() {
13
+ let result: SyncResponse;
14
+ try {
15
+ result = await daemonRequest<SyncResponse>("/sync");
16
+ } catch (err: unknown) {
17
+ const msg = err instanceof Error ? err.message : String(err);
18
+ console.error(`[afd sync] ${msg}`);
19
+ process.exit(1);
20
+ }
21
+
22
+ if (result.count === 0) {
23
+ console.log("[afd sync] No antibodies to export. Run `afd fix` first to learn patterns.");
24
+ return;
25
+ }
26
+
27
+ // Read the generated payload for display
28
+ const payloadPath = resolve(AFD_DIR, "global-vaccine-payload.json");
29
+ const payload = JSON.parse(readFileSync(payloadPath, "utf-8"));
30
+
31
+ const box = "\u2500".repeat(46);
32
+ console.log(`\u250C${box}\u2510`);
33
+ console.log(`\u2502 afd sync \u2014 Vaccine Network \u2502`);
34
+ console.log(`\u251C${box}\u2524`);
35
+ console.log(`\u2502 Ecosystem : ${payload.ecosystem.padEnd(31)}\u2502`);
36
+ console.log(`\u2502 Antibodies : ${String(payload.antibodyCount).padEnd(31)}\u2502`);
37
+ console.log(`\u2502 Generated : ${payload.generatedAt.substring(0, 19).padEnd(31)}\u2502`);
38
+ console.log(`\u251C${box}\u2524`);
39
+
40
+ for (const ab of payload.antibodies) {
41
+ const patches = ab.patches.map((p: { op: string; path: string }) => `${p.op} ${p.path}`).join(", ");
42
+ console.log(`\u2502 [${ab.id}] ${ab.patternType.padEnd(20)} ${patches.substring(0, 12).padEnd(12)}\u2502`);
43
+ }
44
+
45
+ console.log(`\u251C${box}\u2524`);
46
+ console.log(`\u2502 Payload: .afd/global-vaccine-payload.json \u2502`);
47
+ console.log(`\u2514${box}\u2518`);
48
+ console.log();
49
+ console.log(`[afd sync] Vaccine payload generated. ${result.count} antibody(ies) ready for global federation.`);
50
+ }
@@ -0,0 +1,7 @@
1
+ import { join } from "path";
2
+
3
+ export const AFD_DIR = ".afd";
4
+ export const PID_FILE = join(AFD_DIR, "daemon.pid");
5
+ export const PORT_FILE = join(AFD_DIR, "daemon.port");
6
+ export const DB_FILE = join(AFD_DIR, "antibodies.sqlite");
7
+ export const WATCH_TARGETS = [".claude/", "CLAUDE.md", ".cursorrules"];
package/src/core/db.ts ADDED
@@ -0,0 +1,46 @@
1
+ import { mkdirSync } from "fs";
2
+ import { Database } from "bun:sqlite";
3
+ import { AFD_DIR, DB_FILE } from "../constants";
4
+
5
+ export function initDb(): Database {
6
+ mkdirSync(AFD_DIR, { recursive: true });
7
+ const db = new Database(DB_FILE);
8
+ db.exec("PRAGMA journal_mode = WAL");
9
+
10
+ db.exec(`
11
+ CREATE TABLE IF NOT EXISTS events (
12
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
13
+ type TEXT NOT NULL,
14
+ path TEXT NOT NULL,
15
+ timestamp INTEGER NOT NULL
16
+ )
17
+ `);
18
+
19
+ db.exec(`
20
+ CREATE TABLE IF NOT EXISTS antibodies (
21
+ id TEXT PRIMARY KEY,
22
+ pattern_type TEXT NOT NULL,
23
+ file_target TEXT NOT NULL,
24
+ patch_op TEXT NOT NULL,
25
+ dormant INTEGER NOT NULL DEFAULT 0,
26
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
27
+ )
28
+ `);
29
+
30
+ // Migration: add dormant column if missing (existing DBs)
31
+ try {
32
+ db.exec("ALTER TABLE antibodies ADD COLUMN dormant INTEGER NOT NULL DEFAULT 0");
33
+ } catch {
34
+ // Column already exists — safe to ignore
35
+ }
36
+
37
+ db.exec(`
38
+ CREATE TABLE IF NOT EXISTS unlink_log (
39
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
40
+ file_path TEXT NOT NULL,
41
+ timestamp INTEGER NOT NULL
42
+ )
43
+ `);
44
+
45
+ return db;
46
+ }
@@ -0,0 +1,243 @@
1
+ import ts from "typescript";
2
+
3
+ export interface HologramResult {
4
+ hologram: string;
5
+ originalLength: number;
6
+ hologramLength: number;
7
+ savings: number; // percentage 0-100
8
+ }
9
+
10
+ export function generateHologram(filePath: string, source: string): HologramResult {
11
+ const sf = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true);
12
+ const lines: string[] = [];
13
+
14
+ for (const stmt of sf.statements) {
15
+ const line = extractNode(stmt, source);
16
+ if (line) lines.push(line);
17
+ }
18
+
19
+ const hologram = lines.join("\n");
20
+ const originalLength = source.length;
21
+ const hologramLength = hologram.length;
22
+ const savings = originalLength > 0
23
+ ? Math.round((originalLength - hologramLength) / originalLength * 1000) / 10
24
+ : 0;
25
+
26
+ return { hologram, originalLength, hologramLength, savings };
27
+ }
28
+
29
+ function extractNode(node: ts.Node, source: string): string | null {
30
+ // Import declarations — keep as-is
31
+ if (ts.isImportDeclaration(node)) {
32
+ return node.getText().replace(/\s+/g, " ").trim();
33
+ }
34
+
35
+ // Export declarations (re-exports)
36
+ if (ts.isExportDeclaration(node)) {
37
+ return node.getText().replace(/\s+/g, " ").trim();
38
+ }
39
+
40
+ // Export assignment (export default ...)
41
+ if (ts.isExportAssignment(node)) {
42
+ return `export default ${getTypeName(node.expression)};`;
43
+ }
44
+
45
+ // Type alias
46
+ if (ts.isTypeAliasDeclaration(node)) {
47
+ return collapseWhitespace(node.getText());
48
+ }
49
+
50
+ // Interface
51
+ if (ts.isInterfaceDeclaration(node)) {
52
+ return extractInterface(node);
53
+ }
54
+
55
+ // Enum
56
+ if (ts.isEnumDeclaration(node)) {
57
+ return extractEnum(node);
58
+ }
59
+
60
+ // Class
61
+ if (ts.isClassDeclaration(node)) {
62
+ return extractClass(node);
63
+ }
64
+
65
+ // Function declaration
66
+ if (ts.isFunctionDeclaration(node)) {
67
+ return extractFunction(node);
68
+ }
69
+
70
+ // Variable statement (const/let/var — may contain arrow functions, objects, etc.)
71
+ if (ts.isVariableStatement(node)) {
72
+ return extractVariableStatement(node);
73
+ }
74
+
75
+ // Fallback: skip unknown top-level statements
76
+ return null;
77
+ }
78
+
79
+ function extractInterface(node: ts.InterfaceDeclaration): string {
80
+ const mods = getModifiers(node);
81
+ const name = node.name.text;
82
+ const ext = node.heritageClauses
83
+ ? " " + node.heritageClauses.map(h => h.getText()).join(", ")
84
+ : "";
85
+ const members = node.members.map(m => {
86
+ const text = collapseWhitespace(m.getText()).replace(/;$/, "");
87
+ return " " + text + ";";
88
+ }).join("\n");
89
+ return `${mods}interface ${name}${ext} {\n${members}\n}`;
90
+ }
91
+
92
+ function extractEnum(node: ts.EnumDeclaration): string {
93
+ const mods = getModifiers(node);
94
+ const name = node.name.text;
95
+ const members = node.members.map(m => collapseWhitespace(m.getText())).join(", ");
96
+ return `${mods}enum ${name} { ${members} }`;
97
+ }
98
+
99
+ function extractClass(node: ts.ClassDeclaration): string {
100
+ const mods = getModifiers(node);
101
+ const name = node.name?.text ?? "Anonymous";
102
+ const ext = node.heritageClauses
103
+ ? " " + node.heritageClauses.map(h => h.getText()).join(", ")
104
+ : "";
105
+
106
+ const members: string[] = [];
107
+ for (const member of node.members) {
108
+ if (ts.isPropertyDeclaration(member)) {
109
+ members.push(" " + extractProperty(member) + ";");
110
+ } else if (ts.isMethodDeclaration(member) || ts.isGetAccessor(member) || ts.isSetAccessor(member)) {
111
+ members.push(" " + extractMethodSignature(member) + ";");
112
+ } else if (ts.isConstructorDeclaration(member)) {
113
+ const params = extractParams(member.parameters);
114
+ members.push(` constructor(${params});`);
115
+ }
116
+ }
117
+
118
+ return `${mods}class ${name}${ext} {\n${members.join("\n")}\n}`;
119
+ }
120
+
121
+ function extractFunction(node: ts.FunctionDeclaration): string {
122
+ const mods = getModifiers(node);
123
+ const name = node.name?.text ?? "anonymous";
124
+ const typeParams = node.typeParameters
125
+ ? `<${node.typeParameters.map(t => t.getText()).join(", ")}>`
126
+ : "";
127
+ const params = extractParams(node.parameters);
128
+ const ret = node.type ? ": " + collapseWhitespace(node.type.getText()) : "";
129
+ const async = hasModifier(node, ts.SyntaxKind.AsyncKeyword) ? "async " : "";
130
+ return `${mods}${async}function ${name}${typeParams}(${params})${ret} {…}`;
131
+ }
132
+
133
+ function extractVariableStatement(node: ts.VariableStatement): string {
134
+ const mods = getModifiers(node);
135
+ const keyword = node.declarationList.flags & ts.NodeFlags.Const ? "const"
136
+ : node.declarationList.flags & ts.NodeFlags.Let ? "let" : "var";
137
+
138
+ const decls = node.declarationList.declarations.map(d => {
139
+ const name = d.name.getText();
140
+ const typeAnn = d.type ? ": " + collapseWhitespace(d.type.getText()) : "";
141
+
142
+ if (d.initializer) {
143
+ // Arrow function or function expression
144
+ if (ts.isArrowFunction(d.initializer) || ts.isFunctionExpression(d.initializer)) {
145
+ const fn = d.initializer;
146
+ const async = hasModifier(fn, ts.SyntaxKind.AsyncKeyword) ? "async " : "";
147
+ const typeParams = fn.typeParameters
148
+ ? `<${fn.typeParameters.map(t => t.getText()).join(", ")}>`
149
+ : "";
150
+ const params = extractParams(fn.parameters);
151
+ const ret = fn.type ? ": " + collapseWhitespace(fn.type.getText()) : "";
152
+ return `${name} = ${async}${typeParams}(${params})${ret} => {…}`;
153
+ }
154
+ // Object/array/other — just show type or truncated value
155
+ if (typeAnn) return `${name}${typeAnn}`;
156
+ return `${name} = …`;
157
+ }
158
+
159
+ return `${name}${typeAnn}`;
160
+ });
161
+
162
+ return `${mods}${keyword} ${decls.join(", ")};`;
163
+ }
164
+
165
+ function extractProperty(node: ts.PropertyDeclaration): string {
166
+ const mods = getMemberModifiers(node);
167
+ const name = node.name.getText();
168
+ const type = node.type ? ": " + collapseWhitespace(node.type.getText()) : "";
169
+ const optional = node.questionToken ? "?" : "";
170
+ return `${mods}${name}${optional}${type}`;
171
+ }
172
+
173
+ function extractMethodSignature(node: ts.MethodDeclaration | ts.GetAccessorDeclaration | ts.SetAccessorDeclaration): string {
174
+ const mods = getMemberModifiers(node);
175
+ const name = node.name.getText();
176
+ const async = hasModifier(node, ts.SyntaxKind.AsyncKeyword) ? "async " : "";
177
+
178
+ if (ts.isGetAccessor(node)) {
179
+ const ret = node.type ? ": " + collapseWhitespace(node.type.getText()) : "";
180
+ return `${mods}get ${name}()${ret}`;
181
+ }
182
+ if (ts.isSetAccessor(node)) {
183
+ const params = extractParams(node.parameters);
184
+ return `${mods}set ${name}(${params})`;
185
+ }
186
+
187
+ const md = node as ts.MethodDeclaration;
188
+ const typeParams = md.typeParameters
189
+ ? `<${md.typeParameters.map(t => t.getText()).join(", ")}>`
190
+ : "";
191
+ const params = extractParams(md.parameters);
192
+ const ret = md.type ? ": " + collapseWhitespace(md.type.getText()) : "";
193
+ return `${mods}${async}${name}${typeParams}(${params})${ret}`;
194
+ }
195
+
196
+ function extractParams(params: ts.NodeArray<ts.ParameterDeclaration>): string {
197
+ return params.map(p => {
198
+ const name = p.name.getText();
199
+ const optional = p.questionToken ? "?" : "";
200
+ const type = p.type ? ": " + collapseWhitespace(p.type.getText()) : "";
201
+ const rest = p.dotDotDotToken ? "..." : "";
202
+ return `${rest}${name}${optional}${type}`;
203
+ }).join(", ");
204
+ }
205
+
206
+ function getModifiers(node: ts.Node): string {
207
+ const mods = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
208
+ if (!mods) return "";
209
+ const relevant = mods
210
+ .filter(m => m.kind === ts.SyntaxKind.ExportKeyword || m.kind === ts.SyntaxKind.DefaultKeyword || m.kind === ts.SyntaxKind.DeclareKeyword)
211
+ .map(m => m.getText());
212
+ return relevant.length ? relevant.join(" ") + " " : "";
213
+ }
214
+
215
+ function getMemberModifiers(node: ts.Node): string {
216
+ const mods = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
217
+ if (!mods) return "";
218
+ const relevant = mods
219
+ .filter(m =>
220
+ m.kind === ts.SyntaxKind.PublicKeyword ||
221
+ m.kind === ts.SyntaxKind.PrivateKeyword ||
222
+ m.kind === ts.SyntaxKind.ProtectedKeyword ||
223
+ m.kind === ts.SyntaxKind.StaticKeyword ||
224
+ m.kind === ts.SyntaxKind.ReadonlyKeyword ||
225
+ m.kind === ts.SyntaxKind.AbstractKeyword
226
+ )
227
+ .map(m => m.getText());
228
+ return relevant.length ? relevant.join(" ") + " " : "";
229
+ }
230
+
231
+ function hasModifier(node: ts.Node, kind: ts.SyntaxKind): boolean {
232
+ const mods = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
233
+ return mods?.some(m => m.kind === kind) ?? false;
234
+ }
235
+
236
+ function getTypeName(node: ts.Node): string {
237
+ if (ts.isIdentifier(node)) return node.text;
238
+ return "…";
239
+ }
240
+
241
+ function collapseWhitespace(s: string): string {
242
+ return s.replace(/\s+/g, " ").trim();
243
+ }
@@ -0,0 +1,150 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+
3
+ // RFC 6902 JSON-Patch operation
4
+ export interface PatchOp {
5
+ op: "add" | "remove" | "replace" | "move" | "copy" | "test";
6
+ path: string;
7
+ value?: string;
8
+ }
9
+
10
+ export interface Symptom {
11
+ id: string;
12
+ patternType: string;
13
+ fileTarget: string;
14
+ // Front-stage: human-readable
15
+ title: string;
16
+ description: string;
17
+ severity: "critical" | "warning" | "info";
18
+ // Back-stage: AI-optimized
19
+ patches: PatchOp[];
20
+ }
21
+
22
+ export interface DiagnosisResult {
23
+ symptoms: Symptom[];
24
+ healthy: string[];
25
+ }
26
+
27
+ const CLAUDEIGNORE_DEFAULT = `# Autonomous Flow Daemon defaults
28
+ node_modules/
29
+ dist/
30
+ .afd/
31
+ *.log
32
+ .env
33
+ `;
34
+
35
+ const HOOKS_DEFAULT = `{
36
+ "hooks": []
37
+ }
38
+ `;
39
+
40
+ type Check = () => Symptom | null;
41
+
42
+ const checks: Check[] = [
43
+ // Check 1: Missing .claudeignore
44
+ () => {
45
+ if (existsSync(".claudeignore")) return null;
46
+ return {
47
+ id: "IMM-001",
48
+ patternType: "missing-file",
49
+ fileTarget: ".claudeignore",
50
+ title: "Missing .claudeignore",
51
+ description:
52
+ "No .claudeignore found. Without it, AI agents ingest node_modules, build artifacts, and other noise — wasting tokens and degrading context quality.",
53
+ severity: "critical",
54
+ patches: [
55
+ { op: "add", path: "/.claudeignore", value: CLAUDEIGNORE_DEFAULT },
56
+ ],
57
+ };
58
+ },
59
+
60
+ // Check 2: Missing .claude/hooks.json fallback
61
+ () => {
62
+ const hooksPath = ".claude/hooks.json";
63
+ if (existsSync(hooksPath)) {
64
+ try {
65
+ const content = readFileSync(hooksPath, "utf-8");
66
+ JSON.parse(content);
67
+ return null;
68
+ } catch {
69
+ return {
70
+ id: "IMM-002",
71
+ patternType: "invalid-json",
72
+ fileTarget: hooksPath,
73
+ title: "Invalid hooks.json",
74
+ description:
75
+ "hooks.json exists but contains invalid JSON. Agents may fail silently when hooks cannot be parsed.",
76
+ severity: "critical",
77
+ patches: [
78
+ { op: "replace", path: "/.claude/hooks.json", value: HOOKS_DEFAULT },
79
+ ],
80
+ };
81
+ }
82
+ }
83
+ return {
84
+ id: "IMM-002",
85
+ patternType: "missing-file",
86
+ fileTarget: hooksPath,
87
+ title: "No fallback in hooks.json",
88
+ description:
89
+ "No .claude/hooks.json found. Without a hooks file, pre/post-command automation cannot be configured.",
90
+ severity: "warning",
91
+ patches: [
92
+ { op: "add", path: "/.claude/hooks.json", value: HOOKS_DEFAULT },
93
+ ],
94
+ };
95
+ },
96
+
97
+ // Check 3: CLAUDE.md missing or empty
98
+ () => {
99
+ if (!existsSync("CLAUDE.md")) {
100
+ return {
101
+ id: "IMM-003",
102
+ patternType: "missing-file",
103
+ fileTarget: "CLAUDE.md",
104
+ title: "Missing CLAUDE.md",
105
+ description:
106
+ "No CLAUDE.md found. AI agents have no project constitution to follow.",
107
+ severity: "critical",
108
+ patches: [
109
+ { op: "add", path: "/CLAUDE.md", value: "# Project Constitution\n\n<!-- Add project rules here -->\n" },
110
+ ],
111
+ };
112
+ }
113
+ const content = readFileSync("CLAUDE.md", "utf-8").trim();
114
+ if (content.length < 20) {
115
+ return {
116
+ id: "IMM-003",
117
+ patternType: "insufficient-content",
118
+ fileTarget: "CLAUDE.md",
119
+ title: "CLAUDE.md is nearly empty",
120
+ description:
121
+ `CLAUDE.md has only ${content.length} chars. Agents work better with clear project rules.`,
122
+ severity: "info",
123
+ patches: [],
124
+ };
125
+ }
126
+ return null;
127
+ },
128
+ ];
129
+
130
+ export function diagnose(knownAntibodies: string[], opts?: { raw?: boolean }): DiagnosisResult {
131
+ const symptoms: Symptom[] = [];
132
+ const healthy: string[] = [];
133
+
134
+ for (const check of checks) {
135
+ const symptom = check();
136
+ if (!symptom) {
137
+ healthy.push("OK");
138
+ continue;
139
+ }
140
+ // In raw mode, report all symptoms regardless of antibodies
141
+ // (used by auto-heal to detect regressions)
142
+ if (!opts?.raw && knownAntibodies.includes(symptom.id)) {
143
+ healthy.push(`${symptom.id} (immunized)`);
144
+ continue;
145
+ }
146
+ symptoms.push(symptom);
147
+ }
148
+
149
+ return { symptoms, healthy };
150
+ }
@@ -0,0 +1,35 @@
1
+ import { spawn } from "child_process";
2
+
3
+ /**
4
+ * Fire an OS-native toast notification (Windows 10+).
5
+ * Runs asynchronously, never blocks, silently ignores all errors.
6
+ */
7
+ export function notifyAutoHeal(patternId: string): void {
8
+ const title = "\u{1F6E1}\uFE0F afd Auto-Healed";
9
+ const body = `Silently fixed: ${patternId}`;
10
+
11
+ // PowerShell BalloonTip — fire and forget
12
+ const ps = `
13
+ Add-Type -AssemblyName System.Windows.Forms
14
+ $n = New-Object System.Windows.Forms.NotifyIcon
15
+ $n.Icon = [System.Drawing.SystemIcons]::Shield
16
+ $n.BalloonTipTitle = '${title}'
17
+ $n.BalloonTipText = '${body}'
18
+ $n.BalloonTipIcon = 'Info'
19
+ $n.Visible = $true
20
+ $n.ShowBalloonTip(3000)
21
+ Start-Sleep -Milliseconds 3500
22
+ $n.Dispose()
23
+ `.replace(/\n\s*/g, " ");
24
+
25
+ try {
26
+ const child = spawn("powershell", ["-NoProfile", "-NonInteractive", "-Command", ps], {
27
+ detached: true,
28
+ stdio: "ignore",
29
+ windowsHide: true,
30
+ });
31
+ child.unref();
32
+ } catch {
33
+ // Crash-only: silently ignore notification failures
34
+ }
35
+ }
@@ -0,0 +1,37 @@
1
+ import { readFileSync, existsSync } from "fs";
2
+ import { PID_FILE, PORT_FILE } from "../constants";
3
+
4
+ export interface DaemonInfo {
5
+ pid: number;
6
+ port: number;
7
+ }
8
+
9
+ export function getDaemonInfo(): DaemonInfo | null {
10
+ if (!existsSync(PID_FILE) || !existsSync(PORT_FILE)) return null;
11
+ const pid = parseInt(readFileSync(PID_FILE, "utf-8").trim(), 10);
12
+ const port = parseInt(readFileSync(PORT_FILE, "utf-8").trim(), 10);
13
+ if (isNaN(pid) || isNaN(port)) return null;
14
+ return { pid, port };
15
+ }
16
+
17
+ export async function isDaemonAlive(info: DaemonInfo): Promise<boolean> {
18
+ try {
19
+ const res = await fetch(`http://127.0.0.1:${info.port}/health`, {
20
+ signal: AbortSignal.timeout(2000),
21
+ });
22
+ const data = await res.json() as { status: string; pid: number };
23
+ return data.status === "alive" && data.pid === info.pid;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ export async function daemonRequest<T = unknown>(path: string): Promise<T> {
30
+ const info = getDaemonInfo();
31
+ if (!info) throw new Error("Daemon not running. Run `afd start` first.");
32
+ const res = await fetch(`http://127.0.0.1:${info.port}${path}`, {
33
+ signal: AbortSignal.timeout(5000),
34
+ });
35
+ if (!res.ok) throw new Error(`Daemon returned ${res.status}`);
36
+ return res.json() as T;
37
+ }