autonomous-flow-daemon 1.6.0 → 1.9.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 (61) hide show
  1. package/CHANGELOG.md +85 -85
  2. package/LICENSE +21 -21
  3. package/README-ko.md +282 -0
  4. package/README.md +282 -266
  5. package/mcp-config.json +10 -10
  6. package/package.json +4 -2
  7. package/src/adapters/index.ts +370 -370
  8. package/src/cli.ts +162 -127
  9. package/src/commands/benchmark.ts +187 -187
  10. package/src/commands/correlate.ts +180 -0
  11. package/src/commands/dashboard.ts +404 -0
  12. package/src/commands/evolution.ts +84 -1
  13. package/src/commands/fix.ts +158 -158
  14. package/src/commands/lang.ts +41 -41
  15. package/src/commands/plugin.ts +110 -0
  16. package/src/commands/restart.ts +14 -14
  17. package/src/commands/score.ts +276 -276
  18. package/src/commands/start.ts +155 -155
  19. package/src/commands/status.ts +157 -157
  20. package/src/commands/stop.ts +68 -68
  21. package/src/commands/suggest.ts +211 -0
  22. package/src/commands/sync.ts +329 -16
  23. package/src/constants.ts +32 -32
  24. package/src/core/boast.ts +280 -280
  25. package/src/core/config.ts +49 -49
  26. package/src/core/correlation-engine.ts +265 -0
  27. package/src/core/db.ts +145 -117
  28. package/src/core/discovery.ts +65 -65
  29. package/src/core/federation.ts +129 -0
  30. package/src/core/hologram/engine.ts +71 -71
  31. package/src/core/hologram/fallback.ts +11 -11
  32. package/src/core/hologram/go-extractor.ts +203 -0
  33. package/src/core/hologram/incremental.ts +227 -227
  34. package/src/core/hologram/py-extractor.ts +132 -132
  35. package/src/core/hologram/rust-extractor.ts +244 -0
  36. package/src/core/hologram/ts-extractor.ts +406 -320
  37. package/src/core/hologram/types.ts +27 -25
  38. package/src/core/hologram.ts +73 -71
  39. package/src/core/i18n/messages.ts +309 -309
  40. package/src/core/locale.ts +88 -88
  41. package/src/core/log-rotate.ts +33 -33
  42. package/src/core/log-utils.ts +38 -38
  43. package/src/core/lru-map.ts +61 -61
  44. package/src/core/notify.ts +74 -74
  45. package/src/core/plugin-manager.ts +225 -0
  46. package/src/core/rule-suggestion.ts +127 -0
  47. package/src/core/validator-generator.ts +224 -0
  48. package/src/core/workspace.ts +28 -28
  49. package/src/daemon/client.ts +78 -65
  50. package/src/daemon/event-batcher.ts +108 -108
  51. package/src/daemon/guards.ts +13 -13
  52. package/src/daemon/http-routes.ts +376 -293
  53. package/src/daemon/mcp-handler.ts +575 -270
  54. package/src/daemon/mcp-subscriptions.ts +81 -0
  55. package/src/daemon/mesh.ts +51 -0
  56. package/src/daemon/server.ts +655 -590
  57. package/src/daemon/types.ts +121 -100
  58. package/src/daemon/workspace-map.ts +104 -92
  59. package/src/platform.ts +60 -60
  60. package/src/version.ts +15 -15
  61. package/README.ko.md +0 -266
@@ -1,155 +1,155 @@
1
- import { resolve } from "path";
2
- import { spawn } from "child_process";
3
- import { openSync, mkdirSync } from "fs";
4
- import { getDaemonInfo, isDaemonAlive } from "../daemon/client";
5
- import { WATCH_TARGETS, resolveWorkspacePaths } from "../constants";
6
- import { detectEcosystem } from "../adapters/index";
7
- import type { DetectionResult } from "../adapters/index";
8
- import { detachedSpawnOptions, IS_WINDOWS } from "../platform";
9
- import { rotateLogIfNeeded } from "../core/log-rotate";
10
- import { getSystemLanguage } from "../core/locale";
11
- import { getMessages, t } from "../core/i18n/messages";
12
- import type { MessageDict } from "../core/i18n/messages";
13
- import { discoverWatchTargets } from "../core/discovery";
14
-
15
- const STARTUP_POLL_INTERVAL_MS = 100;
16
- const STARTUP_POLL_MAX_MS = 3000;
17
-
18
- interface SetupStep {
19
- label: string;
20
- newMsg: string;
21
- okMsg: string;
22
- skipMsg: string;
23
- }
24
-
25
- interface SetupResult {
26
- ecosystem: string;
27
- steps: { label: string; status: "new" | "ok" | "skip" }[];
28
- }
29
-
30
- /**
31
- * One-Command Zero-Touch: detect ecosystem and provision all integration
32
- * channels (hooks, MCP, statusLine) with idempotency.
33
- */
34
- function setupEcosystem(cwd: string, msg: MessageDict): SetupResult[] {
35
- const ecosystems = detectEcosystem(cwd);
36
- const results: SetupResult[] = [];
37
-
38
- for (const { adapter } of ecosystems) {
39
- console.log(t(msg.SETUP_HEADER, { ecosystem: adapter.name }));
40
- const steps: SetupResult["steps"] = [];
41
-
42
- // 1. Hook injection
43
- if (adapter.injectHooks) {
44
- const r = adapter.injectHooks(cwd);
45
- const status = r.injected ? "new" : "ok";
46
- console.log(status === "new" ? msg.SETUP_HOOKS_NEW : msg.SETUP_HOOKS_OK);
47
- steps.push({ label: "hooks", status });
48
- }
49
-
50
- // 2. MCP registration
51
- if (adapter.registerMcp) {
52
- const r = adapter.registerMcp(cwd);
53
- const status = r.registered ? "new" : "ok";
54
- console.log(status === "new" ? msg.SETUP_MCP_NEW : msg.SETUP_MCP_OK);
55
- steps.push({ label: "mcp", status });
56
- } else {
57
- console.log(msg.SETUP_MCP_SKIP);
58
- steps.push({ label: "mcp", status: "skip" });
59
- }
60
-
61
- // 3. StatusLine configuration
62
- if (adapter.configureStatusLine) {
63
- const r = adapter.configureStatusLine(cwd);
64
- const status = r.configured ? "new" : "ok";
65
- console.log(status === "new" ? msg.SETUP_STATUS_NEW : msg.SETUP_STATUS_OK);
66
- steps.push({ label: "statusLine", status });
67
- } else {
68
- console.log(msg.SETUP_STATUS_SKIP);
69
- steps.push({ label: "statusLine", status: "skip" });
70
- }
71
-
72
- results.push({ ecosystem: adapter.name, steps });
73
- }
74
-
75
- if (ecosystems.length > 0) {
76
- console.log(msg.SETUP_DONE);
77
- }
78
-
79
- return results;
80
- }
81
-
82
- export async function startCommand(options?: { mcp?: boolean }) {
83
- // MCP stdio mode: run daemon in foreground with stdio transport
84
- if (options?.mcp) {
85
- const { main: runDaemon } = await import("../daemon/server");
86
- runDaemon({ mcp: true });
87
- return; // never reaches here — stdio loop blocks
88
- }
89
-
90
- const lang = getSystemLanguage();
91
- const msg = getMessages(lang);
92
-
93
- const paths = resolveWorkspacePaths();
94
- mkdirSync(paths.afdDir, { recursive: true });
95
-
96
- // ── Idempotency: check if already running ──
97
- const existing = getDaemonInfo();
98
- if (existing && (await isDaemonAlive(existing))) {
99
- console.log(msg.DAEMON_ALREADY_RUNNING);
100
- return;
101
- }
102
-
103
- // ── Spawn detached daemon with log redirection ──
104
- const daemonScript = resolve(import.meta.dirname, "../daemon/server.ts");
105
- const logPath = paths.logFile;
106
- rotateLogIfNeeded(logPath);
107
- const logFd = openSync(logPath, "a"); // append mode
108
-
109
- // On Windows, wrap in shell for proper detach; quote path for spaces
110
- const args = IS_WINDOWS
111
- ? ["run", `"${daemonScript}"`]
112
- : ["run", daemonScript];
113
-
114
- const child = spawn("bun", args, detachedSpawnOptions(logFd));
115
-
116
- // Detach: allow parent to exit without killing child
117
- child.unref();
118
-
119
- // ── Poll for daemon readiness instead of fixed sleep ──
120
- const info = await pollForDaemon(STARTUP_POLL_MAX_MS, STARTUP_POLL_INTERVAL_MS);
121
-
122
- if (info) {
123
- console.log(t(msg.DAEMON_STARTED, { pid: info.pid, port: info.port }));
124
-
125
- // Smart Discovery: show what we're actually watching
126
- const discovery = discoverWatchTargets(WATCH_TARGETS);
127
- console.log(t(msg.DAEMON_WATCHING, { count: discovery.targets.length }));
128
- console.log(`[afd] Targets: ${discovery.targets.join(", ")}`);
129
- console.log(t(msg.DAEMON_LOGS, { path: logPath }));
130
-
131
- // One-Command Zero-Touch: auto-provision all ecosystem integrations
132
- setupEcosystem(process.cwd(), msg);
133
- } else {
134
- console.error(t(msg.DAEMON_START_FAILED, { path: logPath }));
135
- process.exit(1);
136
- }
137
- }
138
-
139
- /** Poll until daemon PID/port files appear and health check passes */
140
- async function pollForDaemon(
141
- maxMs: number,
142
- intervalMs: number,
143
- ): Promise<{ pid: number; port: number } | null> {
144
- const deadline = Date.now() + maxMs;
145
-
146
- while (Date.now() < deadline) {
147
- const info = getDaemonInfo();
148
- if (info && (await isDaemonAlive(info))) {
149
- return info;
150
- }
151
- await Bun.sleep(intervalMs);
152
- }
153
-
154
- return null;
155
- }
1
+ import { resolve } from "path";
2
+ import { spawn } from "child_process";
3
+ import { openSync, mkdirSync } from "fs";
4
+ import { getDaemonInfo, isDaemonAlive } from "../daemon/client";
5
+ import { WATCH_TARGETS, resolveWorkspacePaths } from "../constants";
6
+ import { detectEcosystem } from "../adapters/index";
7
+ import type { DetectionResult } from "../adapters/index";
8
+ import { detachedSpawnOptions, IS_WINDOWS } from "../platform";
9
+ import { rotateLogIfNeeded } from "../core/log-rotate";
10
+ import { getSystemLanguage } from "../core/locale";
11
+ import { getMessages, t } from "../core/i18n/messages";
12
+ import type { MessageDict } from "../core/i18n/messages";
13
+ import { discoverWatchTargets } from "../core/discovery";
14
+
15
+ const STARTUP_POLL_INTERVAL_MS = 100;
16
+ const STARTUP_POLL_MAX_MS = 3000;
17
+
18
+ interface SetupStep {
19
+ label: string;
20
+ newMsg: string;
21
+ okMsg: string;
22
+ skipMsg: string;
23
+ }
24
+
25
+ interface SetupResult {
26
+ ecosystem: string;
27
+ steps: { label: string; status: "new" | "ok" | "skip" }[];
28
+ }
29
+
30
+ /**
31
+ * One-Command Zero-Touch: detect ecosystem and provision all integration
32
+ * channels (hooks, MCP, statusLine) with idempotency.
33
+ */
34
+ function setupEcosystem(cwd: string, msg: MessageDict): SetupResult[] {
35
+ const ecosystems = detectEcosystem(cwd);
36
+ const results: SetupResult[] = [];
37
+
38
+ for (const { adapter } of ecosystems) {
39
+ console.log(t(msg.SETUP_HEADER, { ecosystem: adapter.name }));
40
+ const steps: SetupResult["steps"] = [];
41
+
42
+ // 1. Hook injection
43
+ if (adapter.injectHooks) {
44
+ const r = adapter.injectHooks(cwd);
45
+ const status = r.injected ? "new" : "ok";
46
+ console.log(status === "new" ? msg.SETUP_HOOKS_NEW : msg.SETUP_HOOKS_OK);
47
+ steps.push({ label: "hooks", status });
48
+ }
49
+
50
+ // 2. MCP registration
51
+ if (adapter.registerMcp) {
52
+ const r = adapter.registerMcp(cwd);
53
+ const status = r.registered ? "new" : "ok";
54
+ console.log(status === "new" ? msg.SETUP_MCP_NEW : msg.SETUP_MCP_OK);
55
+ steps.push({ label: "mcp", status });
56
+ } else {
57
+ console.log(msg.SETUP_MCP_SKIP);
58
+ steps.push({ label: "mcp", status: "skip" });
59
+ }
60
+
61
+ // 3. StatusLine configuration
62
+ if (adapter.configureStatusLine) {
63
+ const r = adapter.configureStatusLine(cwd);
64
+ const status = r.configured ? "new" : "ok";
65
+ console.log(status === "new" ? msg.SETUP_STATUS_NEW : msg.SETUP_STATUS_OK);
66
+ steps.push({ label: "statusLine", status });
67
+ } else {
68
+ console.log(msg.SETUP_STATUS_SKIP);
69
+ steps.push({ label: "statusLine", status: "skip" });
70
+ }
71
+
72
+ results.push({ ecosystem: adapter.name, steps });
73
+ }
74
+
75
+ if (ecosystems.length > 0) {
76
+ console.log(msg.SETUP_DONE);
77
+ }
78
+
79
+ return results;
80
+ }
81
+
82
+ export async function startCommand(options?: { mcp?: boolean }) {
83
+ // MCP stdio mode: run daemon in foreground with stdio transport
84
+ if (options?.mcp) {
85
+ const { main: runDaemon } = await import("../daemon/server");
86
+ runDaemon({ mcp: true });
87
+ return; // never reaches here — stdio loop blocks
88
+ }
89
+
90
+ const lang = getSystemLanguage();
91
+ const msg = getMessages(lang);
92
+
93
+ const paths = resolveWorkspacePaths();
94
+ mkdirSync(paths.afdDir, { recursive: true });
95
+
96
+ // ── Idempotency: check if already running ──
97
+ const existing = getDaemonInfo();
98
+ if (existing && (await isDaemonAlive(existing))) {
99
+ console.log(msg.DAEMON_ALREADY_RUNNING);
100
+ return;
101
+ }
102
+
103
+ // ── Spawn detached daemon with log redirection ──
104
+ const daemonScript = resolve(import.meta.dirname, "../daemon/server.ts");
105
+ const logPath = paths.logFile;
106
+ rotateLogIfNeeded(logPath);
107
+ const logFd = openSync(logPath, "a"); // append mode
108
+
109
+ // On Windows, wrap in shell for proper detach; quote path for spaces
110
+ const args = IS_WINDOWS
111
+ ? ["run", `"${daemonScript}"`]
112
+ : ["run", daemonScript];
113
+
114
+ const child = spawn("bun", args, detachedSpawnOptions(logFd));
115
+
116
+ // Detach: allow parent to exit without killing child
117
+ child.unref();
118
+
119
+ // ── Poll for daemon readiness instead of fixed sleep ──
120
+ const info = await pollForDaemon(STARTUP_POLL_MAX_MS, STARTUP_POLL_INTERVAL_MS);
121
+
122
+ if (info) {
123
+ console.log(t(msg.DAEMON_STARTED, { pid: info.pid, port: info.port }));
124
+
125
+ // Smart Discovery: show what we're actually watching
126
+ const discovery = discoverWatchTargets(WATCH_TARGETS);
127
+ console.log(t(msg.DAEMON_WATCHING, { count: discovery.targets.length }));
128
+ console.log(`[afd] Targets: ${discovery.targets.join(", ")}`);
129
+ console.log(t(msg.DAEMON_LOGS, { path: logPath }));
130
+
131
+ // One-Command Zero-Touch: auto-provision all ecosystem integrations
132
+ setupEcosystem(process.cwd(), msg);
133
+ } else {
134
+ console.error(t(msg.DAEMON_START_FAILED, { path: logPath }));
135
+ process.exit(1);
136
+ }
137
+ }
138
+
139
+ /** Poll until daemon PID/port files appear and health check passes */
140
+ async function pollForDaemon(
141
+ maxMs: number,
142
+ intervalMs: number,
143
+ ): Promise<{ pid: number; port: number } | null> {
144
+ const deadline = Date.now() + maxMs;
145
+
146
+ while (Date.now() < deadline) {
147
+ const info = getDaemonInfo();
148
+ if (info && (await isDaemonAlive(info))) {
149
+ return info;
150
+ }
151
+ await Bun.sleep(intervalMs);
152
+ }
153
+
154
+ return null;
155
+ }
@@ -1,157 +1,157 @@
1
- import { existsSync, readdirSync, readFileSync } from "fs";
2
- import { join } from "path";
3
- import { getDaemonInfo, isDaemonAlive, daemonRequest } from "../daemon/client";
4
- import { resolveWorkspacePaths } from "../constants";
5
- import { getSystemLanguage } from "../core/locale";
6
-
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
- const ko = getSystemLanguage() === "ko";
19
-
20
- interface HealthData {
21
- status: string;
22
- pid: number;
23
- workspace: string;
24
- port: number;
25
- }
26
-
27
- interface ScoreData {
28
- uptime: number;
29
- immune: { antibodies: number; autoHealed: number };
30
- ecosystem: { primary: string };
31
- dynamicImmune?: { activeValidators: number };
32
- }
33
-
34
- function checkHooksInjected(): boolean {
35
- const hooksPath = join(resolveWorkspacePaths().root, ".claude/hooks.json");
36
- if (!existsSync(hooksPath)) return false;
37
- try {
38
- const content = readFileSync(hooksPath, "utf-8");
39
- return content.includes("afd-auto-heal");
40
- } catch {
41
- return false;
42
- }
43
- }
44
-
45
- function checkMcpRegistered(): boolean {
46
- const mcpPath = join(resolveWorkspacePaths().root, ".mcp.json");
47
- if (!existsSync(mcpPath)) return false;
48
- try {
49
- const content = readFileSync(mcpPath, "utf-8");
50
- return content.includes('"afd"');
51
- } catch {
52
- return false;
53
- }
54
- }
55
-
56
- function getQuarantinedFiles(): string[] {
57
- const paths = resolveWorkspacePaths();
58
- if (!existsSync(paths.quarantineDir)) return [];
59
- try {
60
- return readdirSync(paths.quarantineDir).sort().reverse();
61
- } catch {
62
- return [];
63
- }
64
- }
65
-
66
- function formatUptime(seconds: number): string {
67
- if (seconds < 60) return `${seconds}s`;
68
- if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
69
- const h = Math.floor(seconds / 3600);
70
- const m = Math.floor((seconds % 3600) / 60);
71
- return `${h}h ${m}m`;
72
- }
73
-
74
- function indicator(ok: boolean): string {
75
- return ok ? `${C.green}●${C.reset}` : `${C.red}●${C.reset}`;
76
- }
77
-
78
- export async function statusCommand() {
79
- const out: string[] = [];
80
-
81
- out.push("");
82
- out.push(`${C.bold}afd status${C.reset}`);
83
- out.push("");
84
-
85
- // ── 1. Daemon ──
86
- const info = getDaemonInfo();
87
- if (!info || !(await isDaemonAlive(info))) {
88
- out.push(` ${C.red}●${C.reset} ${C.bold}Daemon${C.reset} ${C.red}STOPPED${C.reset}`);
89
- out.push("");
90
- out.push(` ${C.dim}${ko ? "→ afd start 를 실행하세요" : "→ Run afd start to activate"}${C.reset}`);
91
- out.push("");
92
- console.log(out.join("\n"));
93
- return;
94
- }
95
-
96
- // Fetch live data
97
- let score: ScoreData | null = null;
98
- try {
99
- score = await daemonRequest<ScoreData>("/score");
100
- } catch { /* use fallback */ }
101
-
102
- const uptime = score ? formatUptime(score.uptime) : "?";
103
- const ecosystem = score?.ecosystem.primary ?? "Unknown";
104
-
105
- out.push(` ${C.green}●${C.reset} ${C.bold}Daemon${C.reset} ${C.green}ACTIVE${C.reset} ${C.dim}(pid=${info.pid} port=${info.port})${C.reset}`);
106
- out.push(` ${C.dim} Uptime${C.reset} ${uptime} ${C.dim}|${C.reset} ${ecosystem}`);
107
- out.push("");
108
-
109
- // ── 2. Connections ──
110
- out.push(` ${C.bold}${ko ? "연결 상태" : "Connections"}${C.reset}`);
111
-
112
- const hooksOk = checkHooksInjected();
113
- out.push(` ${indicator(hooksOk)} Hook ${hooksOk ? `${C.green}INJECTED${C.reset}` : `${C.red}MISSING${C.reset} ${C.dim}(afd start로 주입)${C.reset}`}`);
114
-
115
- const mcpOk = checkMcpRegistered();
116
- out.push(` ${indicator(mcpOk)} MCP ${mcpOk ? `${C.green}REGISTERED${C.reset}` : `${C.yellow}NOT SET${C.reset}`}`);
117
-
118
- out.push("");
119
-
120
- // ── 3. Defenses ──
121
- out.push(` ${C.bold}${ko ? "방어막" : "Defenses"}${C.reset}`);
122
-
123
- const antibodies = score?.immune.antibodies ?? 0;
124
- const healed = score?.immune.autoHealed ?? 0;
125
- const validators = score?.dynamicImmune?.activeValidators ?? 0;
126
-
127
- out.push(` ${indicator(antibodies > 0)} ${ko ? "항체" : "Antibodies"} ${C.bold}${antibodies}${C.reset} ${ko ? "활성" : "active"}${healed > 0 ? ` ${C.dim}(${healed}${ko ? "회 치유" : " healed"})${C.reset}` : ""}`);
128
- out.push(` ${indicator(validators > 0)} ${ko ? "검증기" : "Validators"} ${validators > 0 ? `${C.bold}${validators}${C.reset} ${ko ? "로드됨" : "loaded"}` : `${C.dim}${ko ? "없음" : "none"}${C.reset} ${C.dim}(.afd/validators/)${C.reset}`}`);
129
-
130
- out.push("");
131
-
132
- // ── 4. Quarantine ──
133
- const quarantined = getQuarantinedFiles();
134
-
135
- if (quarantined.length > 0) {
136
- out.push(` ${C.yellow}⚠${C.reset} ${C.bold}${C.yellow}${ko ? "격리 구역" : "Quarantine"}${C.reset} ${C.dim}(${quarantined.length} ${ko ? "파일" : "file"}${quarantined.length !== 1 ? "s" : ""})${C.reset}`);
137
-
138
- const show = quarantined.slice(0, 5);
139
- for (const file of show) {
140
- out.push(` ${C.dim}${file}${C.reset}`);
141
- }
142
- if (quarantined.length > 5) {
143
- out.push(` ${C.dim}... +${quarantined.length - 5} more${C.reset}`);
144
- }
145
-
146
- out.push("");
147
- out.push(` ${C.dim}💡 ${ko
148
- ? "격리된 파일에서 코드를 구출하거나 불필요하면 삭제하세요."
149
- : "Rescue code from quarantined files or delete if unneeded."}${C.reset}`);
150
- out.push(` ${C.dim} ${ko ? "경로" : "Path"}: .afd/quarantine/${C.reset}`);
151
- } else {
152
- out.push(` ${C.green}●${C.reset} ${ko ? "격리 구역" : "Quarantine"} ${C.dim}${ko ? "비어있음 — 이상 없음" : "empty — all clear"}${C.reset}`);
153
- }
154
-
155
- out.push("");
156
- console.log(out.join("\n"));
157
- }
1
+ import { existsSync, readdirSync, readFileSync } from "fs";
2
+ import { join } from "path";
3
+ import { getDaemonInfo, isDaemonAlive, daemonRequest } from "../daemon/client";
4
+ import { resolveWorkspacePaths } from "../constants";
5
+ import { getSystemLanguage } from "../core/locale";
6
+
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
+ const ko = getSystemLanguage() === "ko";
19
+
20
+ interface HealthData {
21
+ status: string;
22
+ pid: number;
23
+ workspace: string;
24
+ port: number;
25
+ }
26
+
27
+ interface ScoreData {
28
+ uptime: number;
29
+ immune: { antibodies: number; autoHealed: number };
30
+ ecosystem: { primary: string };
31
+ dynamicImmune?: { activeValidators: number };
32
+ }
33
+
34
+ function checkHooksInjected(): boolean {
35
+ const hooksPath = join(resolveWorkspacePaths().root, ".claude/hooks.json");
36
+ if (!existsSync(hooksPath)) return false;
37
+ try {
38
+ const content = readFileSync(hooksPath, "utf-8");
39
+ return content.includes("afd-auto-heal");
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+
45
+ function checkMcpRegistered(): boolean {
46
+ const mcpPath = join(resolveWorkspacePaths().root, ".mcp.json");
47
+ if (!existsSync(mcpPath)) return false;
48
+ try {
49
+ const content = readFileSync(mcpPath, "utf-8");
50
+ return content.includes('"afd"');
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
55
+
56
+ function getQuarantinedFiles(): string[] {
57
+ const paths = resolveWorkspacePaths();
58
+ if (!existsSync(paths.quarantineDir)) return [];
59
+ try {
60
+ return readdirSync(paths.quarantineDir).sort().reverse();
61
+ } catch {
62
+ return [];
63
+ }
64
+ }
65
+
66
+ function formatUptime(seconds: number): string {
67
+ if (seconds < 60) return `${seconds}s`;
68
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
69
+ const h = Math.floor(seconds / 3600);
70
+ const m = Math.floor((seconds % 3600) / 60);
71
+ return `${h}h ${m}m`;
72
+ }
73
+
74
+ function indicator(ok: boolean): string {
75
+ return ok ? `${C.green}●${C.reset}` : `${C.red}●${C.reset}`;
76
+ }
77
+
78
+ export async function statusCommand() {
79
+ const out: string[] = [];
80
+
81
+ out.push("");
82
+ out.push(`${C.bold}afd status${C.reset}`);
83
+ out.push("");
84
+
85
+ // ── 1. Daemon ──
86
+ const info = getDaemonInfo();
87
+ if (!info || !(await isDaemonAlive(info))) {
88
+ out.push(` ${C.red}●${C.reset} ${C.bold}Daemon${C.reset} ${C.red}STOPPED${C.reset}`);
89
+ out.push("");
90
+ out.push(` ${C.dim}${ko ? "→ afd start 를 실행하세요" : "→ Run afd start to activate"}${C.reset}`);
91
+ out.push("");
92
+ console.log(out.join("\n"));
93
+ return;
94
+ }
95
+
96
+ // Fetch live data
97
+ let score: ScoreData | null = null;
98
+ try {
99
+ score = await daemonRequest<ScoreData>("/score");
100
+ } catch { /* use fallback */ }
101
+
102
+ const uptime = score ? formatUptime(score.uptime) : "?";
103
+ const ecosystem = score?.ecosystem.primary ?? "Unknown";
104
+
105
+ out.push(` ${C.green}●${C.reset} ${C.bold}Daemon${C.reset} ${C.green}ACTIVE${C.reset} ${C.dim}(pid=${info.pid} port=${info.port})${C.reset}`);
106
+ out.push(` ${C.dim} Uptime${C.reset} ${uptime} ${C.dim}|${C.reset} ${ecosystem}`);
107
+ out.push("");
108
+
109
+ // ── 2. Connections ──
110
+ out.push(` ${C.bold}${ko ? "연결 상태" : "Connections"}${C.reset}`);
111
+
112
+ const hooksOk = checkHooksInjected();
113
+ out.push(` ${indicator(hooksOk)} Hook ${hooksOk ? `${C.green}INJECTED${C.reset}` : `${C.red}MISSING${C.reset} ${C.dim}(afd start로 주입)${C.reset}`}`);
114
+
115
+ const mcpOk = checkMcpRegistered();
116
+ out.push(` ${indicator(mcpOk)} MCP ${mcpOk ? `${C.green}REGISTERED${C.reset}` : `${C.yellow}NOT SET${C.reset}`}`);
117
+
118
+ out.push("");
119
+
120
+ // ── 3. Defenses ──
121
+ out.push(` ${C.bold}${ko ? "방어막" : "Defenses"}${C.reset}`);
122
+
123
+ const antibodies = score?.immune.antibodies ?? 0;
124
+ const healed = score?.immune.autoHealed ?? 0;
125
+ const validators = score?.dynamicImmune?.activeValidators ?? 0;
126
+
127
+ out.push(` ${indicator(antibodies > 0)} ${ko ? "항체" : "Antibodies"} ${C.bold}${antibodies}${C.reset} ${ko ? "활성" : "active"}${healed > 0 ? ` ${C.dim}(${healed}${ko ? "회 치유" : " healed"})${C.reset}` : ""}`);
128
+ out.push(` ${indicator(validators > 0)} ${ko ? "검증기" : "Validators"} ${validators > 0 ? `${C.bold}${validators}${C.reset} ${ko ? "로드됨" : "loaded"}` : `${C.dim}${ko ? "없음" : "none"}${C.reset} ${C.dim}(.afd/validators/)${C.reset}`}`);
129
+
130
+ out.push("");
131
+
132
+ // ── 4. Quarantine ──
133
+ const quarantined = getQuarantinedFiles();
134
+
135
+ if (quarantined.length > 0) {
136
+ out.push(` ${C.yellow}⚠${C.reset} ${C.bold}${C.yellow}${ko ? "격리 구역" : "Quarantine"}${C.reset} ${C.dim}(${quarantined.length} ${ko ? "파일" : "file"}${quarantined.length !== 1 ? "s" : ""})${C.reset}`);
137
+
138
+ const show = quarantined.slice(0, 5);
139
+ for (const file of show) {
140
+ out.push(` ${C.dim}${file}${C.reset}`);
141
+ }
142
+ if (quarantined.length > 5) {
143
+ out.push(` ${C.dim}... +${quarantined.length - 5} more${C.reset}`);
144
+ }
145
+
146
+ out.push("");
147
+ out.push(` ${C.dim}💡 ${ko
148
+ ? "격리된 파일에서 코드를 구출하거나 불필요하면 삭제하세요."
149
+ : "Rescue code from quarantined files or delete if unneeded."}${C.reset}`);
150
+ out.push(` ${C.dim} ${ko ? "경로" : "Path"}: .afd/quarantine/${C.reset}`);
151
+ } else {
152
+ out.push(` ${C.green}●${C.reset} ${ko ? "격리 구역" : "Quarantine"} ${C.dim}${ko ? "비어있음 — 이상 없음" : "empty — all clear"}${C.reset}`);
153
+ }
154
+
155
+ out.push("");
156
+ console.log(out.join("\n"));
157
+ }