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,158 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
2
+ import { join, dirname } from "path";
3
+
4
+ export interface HarnessSchema {
5
+ configFiles: string[];
6
+ ignoreFile: string | null;
7
+ rulesFile: string | null;
8
+ hooksFile: string | null;
9
+ }
10
+
11
+ export interface EcosystemAdapter {
12
+ name: string;
13
+ detect(cwd: string): boolean;
14
+ getHarnessSchema(): HarnessSchema;
15
+ injectHooks?(cwd: string): { injected: boolean; message: string };
16
+ configureStatusLine?(cwd: string): { configured: boolean; message: string };
17
+ }
18
+
19
+ const AFD_HOOK_MARKER = "afd-auto-heal";
20
+
21
+ interface HooksConfig {
22
+ hooks?: Record<string, HookEntry[]>;
23
+ [key: string]: unknown;
24
+ }
25
+
26
+ interface HookEntry {
27
+ matcher?: string;
28
+ command: string;
29
+ id?: string;
30
+ [key: string]: unknown;
31
+ }
32
+
33
+ export const ClaudeCodeAdapter: EcosystemAdapter = {
34
+ name: "Claude Code",
35
+ detect(cwd: string): boolean {
36
+ return (
37
+ existsSync(join(cwd, ".claude")) ||
38
+ existsSync(join(cwd, "CLAUDE.md"))
39
+ );
40
+ },
41
+ getHarnessSchema(): HarnessSchema {
42
+ return {
43
+ configFiles: [".claude/settings.json", ".claude/settings.local.json", "CLAUDE.md"],
44
+ ignoreFile: ".claudeignore",
45
+ rulesFile: "CLAUDE.md",
46
+ hooksFile: ".claude/hooks.json",
47
+ };
48
+ },
49
+ injectHooks(cwd: string): { injected: boolean; message: string } {
50
+ const hooksPath = join(cwd, ".claude", "hooks.json");
51
+ const hookCommand = "bun run " + join(cwd, "src", "cli.ts").replace(/\\/g, "/") + " diagnose --format a2a --auto-heal";
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
+ }
70
+
71
+ if (!config.hooks || Array.isArray(config.hooks) || typeof config.hooks !== "object") {
72
+ config.hooks = {};
73
+ }
74
+ if (!config.hooks.PreToolUse) config.hooks.PreToolUse = [];
75
+
76
+ // Check if already injected
77
+ const existing = config.hooks.PreToolUse.find(
78
+ (h: HookEntry) => h.id === AFD_HOOK_MARKER
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
+ }
86
+
87
+ config.hooks.PreToolUse.push(newHook);
88
+ writeFileSync(hooksPath, JSON.stringify(config, null, 2), "utf-8");
89
+ return { injected: true, message: "Auto-heal hook injected into PreToolUse" };
90
+ },
91
+ configureStatusLine(cwd: string): { configured: boolean; message: string } {
92
+ const settingsPath = join(cwd, ".claude", "settings.local.json");
93
+ const statusScript = join(cwd, ".claude", "statusline-command.js").replace(/\\/g, "/");
94
+
95
+ // Only configure if the statusline-command.js exists
96
+ if (!existsSync(statusScript)) {
97
+ return { configured: false, message: "No statusline-command.js found" };
98
+ }
99
+
100
+ let settings: Record<string, unknown>;
101
+ if (existsSync(settingsPath)) {
102
+ try {
103
+ settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
104
+ } catch {
105
+ settings = {};
106
+ }
107
+ } else {
108
+ mkdirSync(dirname(settingsPath), { recursive: true });
109
+ settings = {};
110
+ }
111
+
112
+ const expectedCommand = `node ${statusScript}`;
113
+ const current = settings.statusLine as { type?: string; command?: string } | undefined;
114
+
115
+ if (current?.type === "command" && current?.command === expectedCommand) {
116
+ return { configured: false, message: "Status line already configured" };
117
+ }
118
+
119
+ settings.statusLine = { type: "command", command: expectedCommand };
120
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
121
+ return { configured: true, message: "Status line configured with afd integration" };
122
+ },
123
+ };
124
+
125
+ export const CursorAdapter: EcosystemAdapter = {
126
+ name: "Cursor",
127
+ detect(cwd: string): boolean {
128
+ return existsSync(join(cwd, ".cursorrules"));
129
+ },
130
+ getHarnessSchema(): HarnessSchema {
131
+ return {
132
+ configFiles: [".cursorrules", ".cursor/settings.json"],
133
+ ignoreFile: ".cursorignore",
134
+ rulesFile: ".cursorrules",
135
+ hooksFile: null,
136
+ };
137
+ },
138
+ };
139
+
140
+ const adapters: EcosystemAdapter[] = [ClaudeCodeAdapter, CursorAdapter];
141
+
142
+ export interface DetectionResult {
143
+ adapter: EcosystemAdapter;
144
+ confidence: "primary" | "secondary";
145
+ }
146
+
147
+ export function detectEcosystem(cwd: string): DetectionResult[] {
148
+ const results: DetectionResult[] = [];
149
+ for (const adapter of adapters) {
150
+ if (adapter.detect(cwd)) {
151
+ results.push({
152
+ adapter,
153
+ confidence: results.length === 0 ? "primary" : "secondary",
154
+ });
155
+ }
156
+ }
157
+ return results;
158
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env bun
2
+ import { Command } from "commander";
3
+ import { startCommand } from "./commands/start";
4
+ import { stopCommand } from "./commands/stop";
5
+ import { scoreCommand } from "./commands/score";
6
+ import { fixCommand } from "./commands/fix";
7
+ import { syncCommand } from "./commands/sync";
8
+ import { diagnoseCommand } from "./commands/diagnose";
9
+
10
+ const program = new Command();
11
+
12
+ program
13
+ .name("afd")
14
+ .description("Autonomous Flow Daemon - The Immune System for AI Workflows")
15
+ .version("1.0.0");
16
+
17
+ program
18
+ .command("start")
19
+ .description("Start the afd daemon (background file watcher)")
20
+ .action(startCommand);
21
+
22
+ program
23
+ .command("stop")
24
+ .description("Stop the afd daemon")
25
+ .action(stopCommand);
26
+
27
+ program
28
+ .command("score")
29
+ .description("Show current diagnostic stats from the daemon")
30
+ .action(scoreCommand);
31
+
32
+ program
33
+ .command("fix")
34
+ .description("Auto-fix detected issues in AI workflow config")
35
+ .action(fixCommand);
36
+
37
+ program
38
+ .command("sync")
39
+ .description("Synchronize AI agent configs across team")
40
+ .action(syncCommand);
41
+
42
+ program
43
+ .command("diagnose")
44
+ .description("Run headless diagnosis (used by auto-heal hooks)")
45
+ .option("--format <type>", "Output format: a2a or human", "human")
46
+ .option("--auto-heal", "Auto-apply patches for known antibodies")
47
+ .action(diagnoseCommand);
48
+
49
+ program.parse();
@@ -0,0 +1,151 @@
1
+ import { writeFileSync, mkdirSync, existsSync } from "fs";
2
+ import { dirname } from "path";
3
+ import { daemonRequest } from "../daemon/client";
4
+ import type { Symptom, PatchOp, DiagnosisResult } from "../core/immune";
5
+ import { notifyAutoHeal } from "../core/notify";
6
+
7
+ interface DiagnoseOptions {
8
+ format?: string;
9
+ autoHeal?: boolean;
10
+ }
11
+
12
+ interface AutoHealResponse {
13
+ status: string;
14
+ healed: string[];
15
+ skipped: string[];
16
+ }
17
+
18
+ function applyPatch(patch: PatchOp): boolean {
19
+ const filePath = patch.path.replace(/^\//, "");
20
+
21
+ if (patch.op === "add") {
22
+ if (existsSync(filePath)) return false;
23
+ const dir = dirname(filePath);
24
+ if (dir !== ".") mkdirSync(dir, { recursive: true });
25
+ writeFileSync(filePath, patch.value ?? "", "utf-8");
26
+ return true;
27
+ }
28
+
29
+ if (patch.op === "replace") {
30
+ const dir = dirname(filePath);
31
+ if (dir !== ".") mkdirSync(dir, { recursive: true });
32
+ writeFileSync(filePath, patch.value ?? "", "utf-8");
33
+ return true;
34
+ }
35
+
36
+ return false;
37
+ }
38
+
39
+ export async function diagnoseCommand(opts: DiagnoseOptions) {
40
+ const isA2A = opts.format === "a2a";
41
+ const autoHeal = opts.autoHeal === true;
42
+
43
+ let diagnosis: DiagnosisResult;
44
+ // In auto-heal mode, use raw diagnosis to detect regressions even for immunized patterns
45
+ const diagnosePath = autoHeal ? "/diagnose?raw=true" : "/diagnose";
46
+ try {
47
+ diagnosis = await daemonRequest<DiagnosisResult>(diagnosePath);
48
+ } catch {
49
+ // Daemon not running — exit silently in auto-heal mode to not block agent
50
+ if (autoHeal) process.exit(0);
51
+ console.error("[afd diagnose] Daemon not running. Run `afd start` first.");
52
+ process.exit(1);
53
+ }
54
+
55
+ // No symptoms — nothing to do
56
+ if (diagnosis.symptoms.length === 0) {
57
+ if (isA2A) {
58
+ console.log(JSON.stringify({ status: "healthy", symptoms: [], healed: [] }));
59
+ } else {
60
+ console.log("[afd diagnose] System healthy.");
61
+ }
62
+ return;
63
+ }
64
+
65
+ if (!autoHeal) {
66
+ // Non-auto mode: just report
67
+ if (isA2A) {
68
+ console.log(JSON.stringify({
69
+ status: "symptomatic",
70
+ symptoms: diagnosis.symptoms.map(s => ({
71
+ id: s.id,
72
+ title: s.title,
73
+ severity: s.severity,
74
+ patches: s.patches,
75
+ })),
76
+ }));
77
+ } else {
78
+ for (const s of diagnosis.symptoms) {
79
+ console.log(`[${s.severity}] ${s.id}: ${s.title}`);
80
+ }
81
+ }
82
+ return;
83
+ }
84
+
85
+ // Auto-heal mode: only apply patches for symptoms that have known antibodies
86
+ // Query antibodies to see which patterns we've learned before
87
+ let knownIds: string[];
88
+ try {
89
+ const abData = await daemonRequest<{ antibodies: { id: string }[] }>("/antibodies");
90
+ knownIds = abData.antibodies.map(a => a.id);
91
+ } catch {
92
+ if (isA2A) console.log(JSON.stringify({ status: "error", healed: [], skipped: [] }));
93
+ process.exit(0);
94
+ }
95
+
96
+ const healed: string[] = [];
97
+ const skipped: string[] = [];
98
+
99
+ for (const symptom of diagnosis.symptoms) {
100
+ // Only auto-heal if we have a known antibody for this pattern
101
+ if (!knownIds.includes(symptom.id)) {
102
+ skipped.push(symptom.id);
103
+ continue;
104
+ }
105
+
106
+ if (symptom.patches.length === 0) {
107
+ skipped.push(symptom.id);
108
+ continue;
109
+ }
110
+
111
+ let applied = false;
112
+ for (const patch of symptom.patches) {
113
+ if (applyPatch(patch)) applied = true;
114
+ }
115
+
116
+ if (applied) {
117
+ // Notify daemon of auto-heal event
118
+ try {
119
+ await fetch(
120
+ `http://127.0.0.1:${getDaemonPort()}/auto-heal/record`,
121
+ {
122
+ method: "POST",
123
+ headers: { "Content-Type": "application/json" },
124
+ body: JSON.stringify({ id: symptom.id }),
125
+ signal: AbortSignal.timeout(1000),
126
+ }
127
+ );
128
+ } catch {
129
+ // Non-critical — don't block
130
+ }
131
+ // Fire OS toast notification (async, non-blocking)
132
+ notifyAutoHeal(symptom.id);
133
+ healed.push(symptom.id);
134
+ } else {
135
+ skipped.push(symptom.id);
136
+ }
137
+ }
138
+
139
+ if (isA2A) {
140
+ console.log(JSON.stringify({ status: healed.length > 0 ? "healed" : "no-action", healed, skipped }));
141
+ } else {
142
+ if (healed.length > 0) console.log(`[afd diagnose] Auto-healed: ${healed.join(", ")}`);
143
+ if (skipped.length > 0) console.log(`[afd diagnose] Skipped (unknown): ${skipped.join(", ")}`);
144
+ }
145
+ }
146
+
147
+ function getDaemonPort(): number {
148
+ const { readFileSync } = require("fs");
149
+ const { PORT_FILE } = require("../constants");
150
+ return parseInt(readFileSync(PORT_FILE, "utf-8").trim(), 10);
151
+ }
@@ -0,0 +1,138 @@
1
+ import { writeFileSync, mkdirSync, existsSync } from "fs";
2
+ import { dirname } from "path";
3
+ import { daemonRequest } from "../daemon/client";
4
+ import type { Symptom, PatchOp, DiagnosisResult } from "../core/immune";
5
+
6
+ const SEVERITY_ICON: Record<string, string> = {
7
+ critical: "[!]",
8
+ warning: "[~]",
9
+ info: "[i]",
10
+ };
11
+
12
+ function applyPatch(patch: PatchOp): boolean {
13
+ // Map JSON-Patch path to filesystem path (strip leading /)
14
+ const filePath = patch.path.replace(/^\//, "");
15
+
16
+ if (patch.op === "add") {
17
+ if (existsSync(filePath)) return false; // don't overwrite
18
+ const dir = dirname(filePath);
19
+ if (dir !== ".") mkdirSync(dir, { recursive: true });
20
+ writeFileSync(filePath, patch.value ?? "", "utf-8");
21
+ return true;
22
+ }
23
+
24
+ if (patch.op === "replace") {
25
+ const dir = dirname(filePath);
26
+ if (dir !== ".") mkdirSync(dir, { recursive: true });
27
+ writeFileSync(filePath, patch.value ?? "", "utf-8");
28
+ return true;
29
+ }
30
+
31
+ // remove, move, copy, test — not needed yet
32
+ return false;
33
+ }
34
+
35
+ async function learnAntibody(symptom: Symptom): Promise<void> {
36
+ await fetch(
37
+ `http://127.0.0.1:${(await getDaemonPort())}/antibodies/learn`,
38
+ {
39
+ method: "POST",
40
+ headers: { "Content-Type": "application/json" },
41
+ body: JSON.stringify({
42
+ id: symptom.id,
43
+ patternType: symptom.patternType,
44
+ fileTarget: symptom.fileTarget,
45
+ patches: symptom.patches,
46
+ }),
47
+ }
48
+ );
49
+ }
50
+
51
+ async function getDaemonPort(): Promise<number> {
52
+ const { readFileSync } = await import("fs");
53
+ const { PORT_FILE } = await import("../constants");
54
+ return parseInt(readFileSync(PORT_FILE, "utf-8").trim(), 10);
55
+ }
56
+
57
+ export async function fixCommand() {
58
+ let diagnosis: DiagnosisResult;
59
+ try {
60
+ diagnosis = await daemonRequest<DiagnosisResult>("/diagnose");
61
+ } catch (err: unknown) {
62
+ const msg = err instanceof Error ? err.message : String(err);
63
+ console.error(`[afd fix] ${msg}`);
64
+ process.exit(1);
65
+ }
66
+
67
+ if (diagnosis.symptoms.length === 0) {
68
+ console.log("[afd fix] No symptoms detected. System is healthy.");
69
+ if (diagnosis.healthy.length > 0) {
70
+ console.log(`[afd fix] Passed checks: ${diagnosis.healthy.join(", ")}`);
71
+ }
72
+ return;
73
+ }
74
+
75
+ // Display symptoms
76
+ console.log(`\n[afd fix] Found ${diagnosis.symptoms.length} symptom(s):\n`);
77
+
78
+ for (const s of diagnosis.symptoms) {
79
+ const icon = SEVERITY_ICON[s.severity] ?? "[?]";
80
+ console.log(` ${icon} ${s.id}: ${s.title} (${s.severity})`);
81
+ console.log(` ${s.description}`);
82
+ if (s.patches.length > 0) {
83
+ console.log(` Patch: ${s.patches.map(p => `${p.op} ${p.path}`).join(", ")}`);
84
+ }
85
+ console.log();
86
+ }
87
+
88
+ // Back-stage: dump full JSON-Patch for AI consumers
89
+ const allPatches = diagnosis.symptoms.flatMap(s =>
90
+ s.patches.map(p => ({ symptomId: s.id, ...p }))
91
+ );
92
+ console.log("[afd fix] JSON-Patch (back-stage):");
93
+ console.log(JSON.stringify(allPatches, null, 2));
94
+ console.log();
95
+
96
+ // Prompt user
97
+ process.stdout.write("Apply these fixes? [Y/n] ");
98
+ const answer = await readLine();
99
+
100
+ if (answer.toLowerCase() === "n") {
101
+ console.log("[afd fix] Aborted.");
102
+ return;
103
+ }
104
+
105
+ // Apply patches and learn antibodies
106
+ let applied = 0;
107
+ for (const symptom of diagnosis.symptoms) {
108
+ if (symptom.patches.length === 0) continue;
109
+ let success = true;
110
+ for (const patch of symptom.patches) {
111
+ if (!applyPatch(patch)) {
112
+ console.log(` [skip] ${patch.op} ${patch.path} (already exists or unsupported)`);
113
+ success = false;
114
+ } else {
115
+ console.log(` [done] ${patch.op} ${patch.path}`);
116
+ applied++;
117
+ }
118
+ }
119
+ if (success) {
120
+ await learnAntibody(symptom);
121
+ console.log(` [immune] Learned antibody: ${symptom.id}`);
122
+ }
123
+ }
124
+
125
+ console.log(`\n[afd fix] Applied ${applied} patch(es). Immune system updated.`);
126
+ }
127
+
128
+ function readLine(): Promise<string> {
129
+ return new Promise((resolve) => {
130
+ const buf: Buffer[] = [];
131
+ process.stdin.setEncoding("utf-8");
132
+ process.stdin.resume();
133
+ process.stdin.once("data", (chunk: string) => {
134
+ process.stdin.pause();
135
+ resolve(chunk.toString().trim());
136
+ });
137
+ });
138
+ }
@@ -0,0 +1,148 @@
1
+ import { daemonRequest } from "../daemon/client";
2
+
3
+ interface HologramScore {
4
+ requests: number;
5
+ originalChars: number;
6
+ hologramChars: number;
7
+ savings: number;
8
+ }
9
+
10
+ interface AutoHealEntry {
11
+ id: string;
12
+ at: number;
13
+ }
14
+
15
+ interface ImmuneScore {
16
+ antibodies: number;
17
+ autoHealed: number;
18
+ lastAutoHeal: AutoHealEntry | null;
19
+ }
20
+
21
+ interface EcosystemEntry {
22
+ name: string;
23
+ confidence: string;
24
+ }
25
+
26
+ interface EcosystemScore {
27
+ detected: EcosystemEntry[];
28
+ primary: string;
29
+ }
30
+
31
+ interface ScoreData {
32
+ uptime: number;
33
+ filesDetected: number;
34
+ totalEvents: number;
35
+ lastEvent: string | null;
36
+ lastEventAt: number | null;
37
+ watchedFiles: string[];
38
+ watchTargets: string[];
39
+ hologram: HologramScore;
40
+ immune: ImmuneScore;
41
+ ecosystem: EcosystemScore;
42
+ }
43
+
44
+ function formatUptime(seconds: number): string {
45
+ if (seconds < 60) return `${seconds}s`;
46
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
47
+ const h = Math.floor(seconds / 3600);
48
+ const m = Math.floor((seconds % 3600) / 60);
49
+ return `${h}h ${m}m`;
50
+ }
51
+
52
+ function heatBar(value: number, max: number, width = 20): string {
53
+ const filled = Math.min(Math.round((value / Math.max(max, 1)) * width), width);
54
+ return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
55
+ }
56
+
57
+ function formatChars(n: number): string {
58
+ if (n < 1000) return `${n}`;
59
+ if (n < 1_000_000) return `${(n / 1000).toFixed(1)}K`;
60
+ return `${(n / 1_000_000).toFixed(1)}M`;
61
+ }
62
+
63
+ const W = 46; // inner box width
64
+ const line = "\u2500".repeat(W);
65
+ const row = (content: string) => `\u2502${content.padEnd(W)}\u2502`;
66
+
67
+ export async function scoreCommand() {
68
+ try {
69
+ const data = await daemonRequest<ScoreData>("/score");
70
+ const h = data.hologram;
71
+
72
+ console.log(`\u250C${line}\u2510`);
73
+ console.log(row(" afd score \u2014 Daemon Diagnostics"));
74
+ console.log(`\u251C${line}\u2524`);
75
+ console.log(row(` Ecosystem : ${data.ecosystem.primary}`));
76
+ if (data.ecosystem.detected.length > 1) {
77
+ const others = data.ecosystem.detected.slice(1).map(e => e.name).join(", ");
78
+ console.log(row(` Also found : ${others}`));
79
+ }
80
+ console.log(`\u251C${line}\u2524`);
81
+ console.log(row(` Uptime : ${formatUptime(data.uptime)}`));
82
+ console.log(row(` Events : ${data.totalEvents}`));
83
+ console.log(row(` Files Found : ${data.watchedFiles.length}`));
84
+ console.log(`\u251C${line}\u2524`);
85
+ console.log(row(` Activity ${heatBar(data.totalEvents, 100)}`));
86
+
87
+ // Context Efficiency section
88
+ console.log(`\u251C${line}\u2524`);
89
+ console.log(row(" Context Efficiency (Hologram)"));
90
+ console.log(row(` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
91
+ if (h.requests > 0) {
92
+ const saved = h.originalChars - h.hologramChars;
93
+ console.log(row(` Requests : ${h.requests}`));
94
+ console.log(row(` Original : ${formatChars(h.originalChars)} chars`));
95
+ console.log(row(` Hologram : ${formatChars(h.hologramChars)} chars`));
96
+ console.log(row(` Saved : ${formatChars(saved)} chars (${h.savings}%)`));
97
+ console.log(row(` Efficiency ${heatBar(h.savings, 100)}`));
98
+ } else {
99
+ console.log(row(" No hologram requests yet."));
100
+ console.log(row(" Use: GET /hologram?file=<path>"));
101
+ }
102
+
103
+ // Immune System section
104
+ console.log(`\u251C${line}\u2524`);
105
+ console.log(row(" Immune System"));
106
+ console.log(row(` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
107
+ const ab = data.immune.antibodies;
108
+ const ah = data.immune.autoHealed;
109
+ const immuneLevel = ab === 0 ? "Vulnerable" : ab < 3 ? "Learning" : ab < 6 ? "Guarded" : "Fortified";
110
+ console.log(row(` Antibodies : ${ab}`));
111
+ console.log(row(` Level : ${immuneLevel}`));
112
+ console.log(row(` Immunity ${heatBar(ab, 10)}`));
113
+ console.log(row(` Auto-healed : ${ah} background event${ah !== 1 ? "s" : ""}`));
114
+ if (data.immune.lastAutoHeal) {
115
+ const ago = formatUptime(Math.floor((Date.now() - data.immune.lastAutoHeal.at) / 1000));
116
+ console.log(row(` Last heal : ${data.immune.lastAutoHeal.id} (${ago} ago)`));
117
+ }
118
+
119
+ // Watched files
120
+ console.log(`\u251C${line}\u2524`);
121
+ if (data.watchedFiles.length > 0) {
122
+ console.log(row(" Watched Files:"));
123
+ for (const f of data.watchedFiles.slice(0, 8)) {
124
+ console.log(row(` ${f.substring(0, W - 6)}`));
125
+ }
126
+ if (data.watchedFiles.length > 8) {
127
+ console.log(row(` ... +${data.watchedFiles.length - 8} more`));
128
+ }
129
+ } else {
130
+ console.log(row(" No files detected yet."));
131
+ }
132
+
133
+ if (data.lastEvent) {
134
+ const ago = data.lastEventAt
135
+ ? formatUptime(Math.floor((Date.now() - data.lastEventAt) / 1000)) + " ago"
136
+ : "unknown";
137
+ console.log(`\u251C${line}\u2524`);
138
+ console.log(row(` Last: ${data.lastEvent.substring(0, 36)}`));
139
+ console.log(row(` ${ago}`));
140
+ }
141
+
142
+ console.log(`\u2514${line}\u2518`);
143
+ } catch (err: unknown) {
144
+ const msg = err instanceof Error ? err.message : String(err);
145
+ console.error(`[afd] ${msg}`);
146
+ process.exit(1);
147
+ }
148
+ }
@@ -0,0 +1,55 @@
1
+ import { spawn } from "child_process";
2
+ import { resolve } from "path";
3
+ import { getDaemonInfo, isDaemonAlive } from "../daemon/client";
4
+ import { AFD_DIR } from "../constants";
5
+ import { mkdirSync } from "fs";
6
+ import { detectEcosystem } from "../adapters/index";
7
+
8
+ export async function startCommand() {
9
+ mkdirSync(AFD_DIR, { recursive: true });
10
+
11
+ // Check if already running
12
+ const existing = getDaemonInfo();
13
+ if (existing && await isDaemonAlive(existing)) {
14
+ console.log(`[afd] Daemon already running (pid=${existing.pid}, port=${existing.port})`);
15
+ return;
16
+ }
17
+
18
+ // Spawn detached daemon
19
+ const daemonScript = resolve(import.meta.dirname, "../daemon/server.ts");
20
+ const bunPath = process.execPath;
21
+
22
+ const child = spawn(bunPath, ["run", daemonScript], {
23
+ detached: true,
24
+ stdio: ["ignore", "ignore", "ignore"],
25
+ cwd: process.cwd(),
26
+ env: { ...process.env },
27
+ });
28
+
29
+ child.unref();
30
+
31
+ // Wait for daemon to write its port file (Windows needs more time)
32
+ await new Promise((r) => setTimeout(r, 1500));
33
+
34
+ const info = getDaemonInfo();
35
+ if (info && await isDaemonAlive(info)) {
36
+ console.log(`[afd] Daemon started (pid=${info.pid}, port=${info.port})`);
37
+ console.log(`[afd] Watching: .claude/, CLAUDE.md, .cursorrules`);
38
+
39
+ // Silently inject auto-heal hook and status line into detected ecosystem
40
+ const ecosystems = detectEcosystem(process.cwd());
41
+ for (const { adapter } of ecosystems) {
42
+ if (adapter.injectHooks) {
43
+ const hookResult = adapter.injectHooks(process.cwd());
44
+ console.log(`[afd] ${hookResult.message}`);
45
+ }
46
+ if (adapter.configureStatusLine) {
47
+ const slResult = adapter.configureStatusLine(process.cwd());
48
+ if (slResult.configured) console.log(`[afd] ${slResult.message}`);
49
+ }
50
+ }
51
+ } else {
52
+ console.error("[afd] Failed to start daemon. Check logs.");
53
+ process.exit(1);
54
+ }
55
+ }