autonomous-flow-daemon 1.1.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 (75) hide show
  1. package/CHANGELOG.md +85 -46
  2. package/LICENSE +21 -21
  3. package/README-ko.md +282 -0
  4. package/README.md +282 -337
  5. package/mcp-config.json +10 -10
  6. package/package.json +14 -6
  7. package/src/adapters/index.ts +370 -159
  8. package/src/cli.ts +162 -57
  9. package/src/commands/benchmark.ts +187 -0
  10. package/src/commands/correlate.ts +180 -0
  11. package/src/commands/dashboard.ts +404 -0
  12. package/src/commands/diagnose.ts +56 -14
  13. package/src/commands/doctor.ts +243 -0
  14. package/src/commands/evolution.ts +190 -0
  15. package/src/commands/fix.ts +158 -138
  16. package/src/commands/hooks.ts +136 -0
  17. package/src/commands/lang.ts +41 -41
  18. package/src/commands/mcp.ts +129 -0
  19. package/src/commands/plugin.ts +110 -0
  20. package/src/commands/restart.ts +14 -0
  21. package/src/commands/score.ts +276 -208
  22. package/src/commands/start.ts +155 -96
  23. package/src/commands/stats.ts +103 -0
  24. package/src/commands/status.ts +157 -0
  25. package/src/commands/stop.ts +68 -49
  26. package/src/commands/suggest.ts +211 -0
  27. package/src/commands/sync.ts +567 -21
  28. package/src/commands/vaccine.ts +177 -0
  29. package/src/constants.ts +32 -8
  30. package/src/core/boast.ts +280 -265
  31. package/src/core/config.ts +49 -49
  32. package/src/core/correlation-engine.ts +265 -0
  33. package/src/core/db.ts +145 -46
  34. package/src/core/discovery.ts +65 -65
  35. package/src/core/evolution.ts +215 -0
  36. package/src/core/federation.ts +129 -0
  37. package/src/core/hologram/engine.ts +71 -0
  38. package/src/core/hologram/fallback.ts +11 -0
  39. package/src/core/hologram/go-extractor.ts +203 -0
  40. package/src/core/hologram/incremental.ts +227 -0
  41. package/src/core/hologram/py-extractor.ts +132 -0
  42. package/src/core/hologram/rust-extractor.ts +244 -0
  43. package/src/core/hologram/ts-extractor.ts +406 -0
  44. package/src/core/hologram/types.ts +27 -0
  45. package/src/core/hologram.ts +73 -243
  46. package/src/core/hook-manager.ts +259 -0
  47. package/src/core/i18n/messages.ts +309 -266
  48. package/src/core/immune.ts +8 -123
  49. package/src/core/locale.ts +88 -88
  50. package/src/core/log-rotate.ts +33 -0
  51. package/src/core/log-utils.ts +38 -0
  52. package/src/core/lru-map.ts +61 -0
  53. package/src/core/notify.ts +74 -66
  54. package/src/core/plugin-manager.ts +225 -0
  55. package/src/core/rule-engine.ts +287 -0
  56. package/src/core/rule-suggestion.ts +127 -0
  57. package/src/core/semantic-diff.ts +432 -0
  58. package/src/core/telemetry.ts +94 -0
  59. package/src/core/vaccine-registry.ts +212 -0
  60. package/src/core/validator-generator.ts +224 -0
  61. package/src/core/workspace.ts +28 -0
  62. package/src/core/yaml-minimal.ts +176 -0
  63. package/src/daemon/client.ts +78 -37
  64. package/src/daemon/event-batcher.ts +108 -0
  65. package/src/daemon/guards.ts +13 -0
  66. package/src/daemon/http-routes.ts +376 -0
  67. package/src/daemon/mcp-handler.ts +575 -0
  68. package/src/daemon/mcp-subscriptions.ts +81 -0
  69. package/src/daemon/mesh.ts +51 -0
  70. package/src/daemon/server.ts +655 -504
  71. package/src/daemon/types.ts +121 -0
  72. package/src/daemon/workspace-map.ts +104 -0
  73. package/src/platform.ts +60 -39
  74. package/src/version.ts +15 -0
  75. package/README.ko.md +0 -306
@@ -1,96 +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 { AFD_DIR, LOG_FILE, WATCH_TARGETS } from "../constants";
6
- import { detectEcosystem } from "../adapters/index";
7
- import { detachedSpawnOptions, IS_WINDOWS } from "../platform";
8
- import { getSystemLanguage } from "../core/locale";
9
- import { getMessages, t } from "../core/i18n/messages";
10
- import { discoverWatchTargets } from "../core/discovery";
11
-
12
- const STARTUP_POLL_INTERVAL_MS = 100;
13
- const STARTUP_POLL_MAX_MS = 3000;
14
-
15
- export async function startCommand(options?: { mcp?: boolean }) {
16
- // MCP stdio mode: run daemon in foreground with stdio transport
17
- if (options?.mcp) {
18
- const { main: runDaemon } = await import("../daemon/server");
19
- runDaemon({ mcp: true });
20
- return; // never reaches here — stdio loop blocks
21
- }
22
-
23
- const lang = getSystemLanguage();
24
- const msg = getMessages(lang);
25
-
26
- mkdirSync(AFD_DIR, { recursive: true });
27
-
28
- // ── Idempotency: check if already running ──
29
- const existing = getDaemonInfo();
30
- if (existing && (await isDaemonAlive(existing))) {
31
- console.log(msg.DAEMON_ALREADY_RUNNING);
32
- return;
33
- }
34
-
35
- // ── Spawn detached daemon with log redirection ──
36
- const daemonScript = resolve(import.meta.dirname, "../daemon/server.ts");
37
- const logPath = resolve(LOG_FILE);
38
- const logFd = openSync(logPath, "a"); // append mode
39
-
40
- // On Windows, wrap in shell for proper detach; quote path for spaces
41
- const args = IS_WINDOWS
42
- ? ["run", `"${daemonScript}"`]
43
- : ["run", daemonScript];
44
-
45
- const child = spawn("bun", args, detachedSpawnOptions(logFd));
46
-
47
- // Detach: allow parent to exit without killing child
48
- child.unref();
49
-
50
- // ── Poll for daemon readiness instead of fixed sleep ──
51
- const info = await pollForDaemon(STARTUP_POLL_MAX_MS, STARTUP_POLL_INTERVAL_MS);
52
-
53
- if (info) {
54
- console.log(t(msg.DAEMON_STARTED, { pid: info.pid, port: info.port }));
55
-
56
- // Smart Discovery: show what we're actually watching
57
- const discovery = discoverWatchTargets(WATCH_TARGETS);
58
- console.log(t(msg.DAEMON_WATCHING, { count: discovery.targets.length }));
59
- console.log(`[afd] Targets: ${discovery.targets.join(", ")}`);
60
- console.log(t(msg.DAEMON_LOGS, { path: logPath }));
61
-
62
- // Inject hooks into detected ecosystems
63
- const ecosystems = detectEcosystem(process.cwd());
64
- for (const { adapter } of ecosystems) {
65
- if (adapter.injectHooks) {
66
- const hookResult = adapter.injectHooks(process.cwd());
67
- console.log(`[afd] ${hookResult.message}`);
68
- }
69
- if (adapter.configureStatusLine) {
70
- const slResult = adapter.configureStatusLine(process.cwd());
71
- if (slResult.configured) console.log(`[afd] ${slResult.message}`);
72
- }
73
- }
74
- } else {
75
- console.error(t(msg.DAEMON_START_FAILED, { path: logPath }));
76
- process.exit(1);
77
- }
78
- }
79
-
80
- /** Poll until daemon PID/port files appear and health check passes */
81
- async function pollForDaemon(
82
- maxMs: number,
83
- intervalMs: number,
84
- ): Promise<{ pid: number; port: number } | null> {
85
- const deadline = Date.now() + maxMs;
86
-
87
- while (Date.now() < deadline) {
88
- const info = getDaemonInfo();
89
- if (info && (await isDaemonAlive(info))) {
90
- return info;
91
- }
92
- await Bun.sleep(intervalMs);
93
- }
94
-
95
- return null;
96
- }
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
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * `afd stats` — Feature usage telemetry dashboard (developer-only).
3
+ */
4
+
5
+ import { queryTelemetry, type TelemetrySummary } from "../core/telemetry";
6
+
7
+ const C = {
8
+ reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
9
+ red: "\x1b[31m", green: "\x1b[32m", yellow: "\x1b[33m",
10
+ blue: "\x1b[34m", magenta: "\x1b[35m", cyan: "\x1b[36m", white: "\x1b[37m",
11
+ };
12
+
13
+ function bar(count: number, max: number, width = 20): string {
14
+ const ratio = max > 0 ? Math.min(count / max, 1) : 0;
15
+ const filled = Math.round(ratio * width);
16
+ return `${C.green}${"█".repeat(filled)}${C.dim}${"░".repeat(width - filled)}${C.reset}`;
17
+ }
18
+
19
+ function sortedEntries(obj: Record<string, number>): [string, number][] {
20
+ return Object.entries(obj).sort((a, b) => b[1] - a[1]);
21
+ }
22
+
23
+ function renderRankedList(data: Record<string, number>, barWidth = 16): string[] {
24
+ const entries = sortedEntries(data);
25
+ if (entries.length === 0) return [` ${C.dim}(no data)${C.reset}`];
26
+ const maxVal = entries[0][1];
27
+ const maxNameLen = Math.max(...entries.map(([n]) => n.length), 8);
28
+ return entries.map(([name, count]) => {
29
+ const pad = " ".repeat(Math.max(1, maxNameLen - name.length + 2));
30
+ return ` ${C.white}${name}${C.reset}${pad}${bar(count, maxVal, barWidth)} ${count}`;
31
+ });
32
+ }
33
+
34
+ function renderSection(title: string): string {
35
+ return `\n${C.bold}${C.cyan}${title}${C.reset}\n${"─".repeat(50)}`;
36
+ }
37
+
38
+ export async function statsCommand(opts: { days?: string }) {
39
+ const days = parseInt(opts.days ?? "7", 10) || 7;
40
+ const data: TelemetrySummary = queryTelemetry(days);
41
+
42
+ const out: string[] = [];
43
+
44
+ out.push(`${C.bold}📊 Feature Usage Telemetry${C.reset} ${C.dim}(last ${days} days, ${data.totalEvents} events total)${C.reset}`);
45
+
46
+ // CLI Commands
47
+ out.push(renderSection("CLI Commands"));
48
+ out.push(...renderRankedList(data.cli));
49
+
50
+ // MCP Tools
51
+ out.push(renderSection("MCP Tools"));
52
+ out.push(...renderRankedList(data.mcp));
53
+
54
+ // S.E.A.M Cycle
55
+ out.push(renderSection("S.E.A.M Cycle"));
56
+ const seamEntries = sortedEntries(data.seam.counts);
57
+ if (seamEntries.length === 0) {
58
+ out.push(` ${C.dim}(no data)${C.reset}`);
59
+ } else {
60
+ const maxSeam = seamEntries[0][1];
61
+ const maxNameLen = Math.max(...seamEntries.map(([n]) => n.length), 8);
62
+ for (const [action, count] of seamEntries) {
63
+ const pad = " ".repeat(Math.max(1, maxNameLen - action.length + 2));
64
+ const avg = data.seam.avgDurationMs[action];
65
+ const avgStr = avg != null ? `${C.dim}avg ${avg}ms${C.reset}` : "";
66
+ out.push(` ${C.white}${action}${C.reset}${pad}${bar(count, maxSeam, 12)} ${count} ${avgStr}`);
67
+ }
68
+ }
69
+
70
+ // Immune Activity + Accuracy
71
+ out.push(renderSection("Immune Activity"));
72
+ const hits = data.immune["heal_hit"] ?? 0;
73
+ const falsePos = data.immune["heal_false_positive"] ?? 0;
74
+ const passes = data.immune["heal_pass"] ?? 0;
75
+ const suppressions = data.immune["suppression"] ?? 0;
76
+ const totalJudgments = hits + falsePos + passes;
77
+ const accuracy = totalJudgments > 0 ? Math.round((hits + passes) / totalJudgments * 100) : null;
78
+ const precisionLabel = (hits + falsePos) > 0 ? `${Math.round(hits / (hits + falsePos) * 100)}%` : "—";
79
+
80
+ out.push(` ${C.white}Hits${C.reset} ${hits} ${C.dim}(corruption detected & restored)${C.reset}`);
81
+ out.push(` ${C.white}Passes${C.reset} ${passes} ${C.dim}(immune file changed, valid)${C.reset}`);
82
+ out.push(` ${C.white}False +${C.reset} ${falsePos} ${C.dim}(restored but user overrode)${C.reset}`);
83
+ out.push(` ${C.white}Suppress${C.reset} ${suppressions} ${C.dim}(mass event skip)${C.reset}`);
84
+ out.push("");
85
+ out.push(` ${C.bold}Accuracy${C.reset} ${accuracy != null ? `${C.green}${accuracy}%${C.reset}` : `${C.dim}—${C.reset}`} ${C.dim}(correct judgments / total)${C.reset}`);
86
+ out.push(` ${C.bold}Precision${C.reset} ${(hits + falsePos) > 0 ? `${C.green}${precisionLabel}${C.reset}` : `${C.dim}—${C.reset}`} ${C.dim}(true hits / all blocks)${C.reset}`);
87
+
88
+ // Validators
89
+ if (Object.keys(data.validator).length > 0) {
90
+ out.push(renderSection("Validator Triggers"));
91
+ out.push(...renderRankedList(data.validator));
92
+ }
93
+
94
+ // Dead features warning
95
+ const allCli = ["start", "stop", "restart", "status", "score", "fix", "sync", "doctor", "diagnose", "vaccine", "evolution", "mcp", "lang", "stats"];
96
+ const unusedCli = allCli.filter(cmd => !(cmd in data.cli));
97
+ if (unusedCli.length > 0 && Object.keys(data.cli).length > 0) {
98
+ out.push(renderSection("Unused CLI Commands"));
99
+ out.push(` ${C.yellow}${unusedCli.join(", ")}${C.reset}`);
100
+ }
101
+
102
+ console.log(out.join("\n"));
103
+ }
@@ -0,0 +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,49 +1,68 @@
1
- import { getDaemonInfo, isDaemonAlive, daemonRequest } from "../daemon/client";
2
- import { unlinkSync } from "fs";
3
- import { PID_FILE, PORT_FILE } from "../constants";
4
- import { formatShiftSummary } from "../core/boast";
5
- import type { ShiftSummary } from "../core/boast";
6
- import { getSystemLanguage } from "../core/locale";
7
- import { getMessages, t } from "../core/i18n/messages";
8
-
9
- function cleanupFiles() {
10
- try { unlinkSync(PID_FILE); } catch {}
11
- try { unlinkSync(PORT_FILE); } catch {}
12
- }
13
-
14
- export async function stopCommand() {
15
- const lang = getSystemLanguage();
16
- const msg = getMessages(lang);
17
- const info = getDaemonInfo();
18
-
19
- if (!info) {
20
- console.log(msg.DAEMON_NOT_RUNNING);
21
- return;
22
- }
23
-
24
- if (await isDaemonAlive(info)) {
25
- // Fetch shift summary before stopping
26
- try {
27
- const summary = await daemonRequest<ShiftSummary>("/shift-summary");
28
- console.log(formatShiftSummary(summary, lang));
29
- } catch {
30
- // Non-fatal: summary is a nicety, not a requirement
31
- }
32
-
33
- try {
34
- await daemonRequest("/stop");
35
- console.log(t(msg.DAEMON_STOPPED, { pid: info.pid }));
36
- } catch {
37
- try {
38
- process.kill(info.pid, "SIGTERM");
39
- console.log(t(msg.DAEMON_KILLED, { pid: info.pid }));
40
- } catch {
41
- console.log("[afd] Daemon process already gone.");
42
- }
43
- }
44
- } else {
45
- console.log(msg.DAEMON_NOT_RESPONDING);
46
- }
47
-
48
- cleanupFiles();
49
- }
1
+ import { getDaemonInfo, isDaemonAlive, daemonRequest } from "../daemon/client";
2
+ import { unlinkSync } from "fs";
3
+ import { resolveWorkspacePaths } from "../constants";
4
+ import { formatShiftSummary } from "../core/boast";
5
+ import type { ShiftSummary } from "../core/boast";
6
+ import { getSystemLanguage } from "../core/locale";
7
+ import { getMessages, t } from "../core/i18n/messages";
8
+ import { detectEcosystem } from "../adapters/index";
9
+
10
+ function cleanupFiles() {
11
+ const paths = resolveWorkspacePaths();
12
+ try { unlinkSync(paths.pidFile); } catch {}
13
+ try { unlinkSync(paths.portFile); } catch {}
14
+ }
15
+
16
+ export async function stopCommand(options?: { clean?: boolean }) {
17
+ const lang = getSystemLanguage();
18
+ const msg = getMessages(lang);
19
+ const info = getDaemonInfo();
20
+
21
+ if (!info) {
22
+ console.log(msg.DAEMON_NOT_RUNNING);
23
+ return;
24
+ }
25
+
26
+ if (await isDaemonAlive(info)) {
27
+ // Fetch shift summary before stopping
28
+ try {
29
+ const summary = await daemonRequest<ShiftSummary>("/shift-summary");
30
+ console.log(formatShiftSummary(summary, lang));
31
+ } catch {
32
+ // Non-fatal: summary is a nicety, not a requirement
33
+ }
34
+
35
+ try {
36
+ await daemonRequest("/stop");
37
+ console.log(t(msg.DAEMON_STOPPED, { pid: info.pid }));
38
+ } catch {
39
+ try {
40
+ process.kill(info.pid, "SIGTERM");
41
+ console.log(t(msg.DAEMON_KILLED, { pid: info.pid }));
42
+ } catch {
43
+ console.log("[afd] Daemon process already gone.");
44
+ }
45
+ }
46
+ } else {
47
+ console.log(msg.DAEMON_NOT_RESPONDING);
48
+ }
49
+
50
+ cleanupFiles();
51
+
52
+ // --clean: remove injected hooks and MCP registration
53
+ if (options?.clean) {
54
+ const cwd = process.cwd();
55
+ const ecosystems = detectEcosystem(cwd);
56
+ for (const { adapter } of ecosystems) {
57
+ if (adapter.removeHooks) {
58
+ const r = adapter.removeHooks(cwd);
59
+ if (r.removed) console.log(`[afd] ${r.message}`);
60
+ }
61
+ if (adapter.unregisterMcp) {
62
+ const r = adapter.unregisterMcp(cwd);
63
+ if (r.removed) console.log(`[afd] ${r.message}`);
64
+ }
65
+ }
66
+ console.log("[afd] Clean stop complete. All afd integrations removed.");
67
+ }
68
+ }