autonomous-flow-daemon 1.0.0 → 1.6.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 (59) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.ko.md +142 -125
  3. package/README.md +119 -134
  4. package/package.json +11 -5
  5. package/src/adapters/index.ts +247 -35
  6. package/src/cli.ts +79 -1
  7. package/src/commands/benchmark.ts +187 -0
  8. package/src/commands/diagnose.ts +56 -14
  9. package/src/commands/doctor.ts +243 -0
  10. package/src/commands/evolution.ts +107 -0
  11. package/src/commands/fix.ts +22 -2
  12. package/src/commands/hooks.ts +136 -0
  13. package/src/commands/lang.ts +41 -0
  14. package/src/commands/mcp.ts +129 -0
  15. package/src/commands/restart.ts +14 -0
  16. package/src/commands/score.ts +192 -64
  17. package/src/commands/start.ts +137 -37
  18. package/src/commands/stats.ts +103 -0
  19. package/src/commands/status.ts +157 -0
  20. package/src/commands/stop.ts +42 -9
  21. package/src/commands/sync.ts +253 -20
  22. package/src/commands/vaccine.ts +177 -0
  23. package/src/constants.ts +26 -1
  24. package/src/core/boast.ts +280 -0
  25. package/src/core/config.ts +49 -0
  26. package/src/core/db.ts +74 -3
  27. package/src/core/discovery.ts +65 -0
  28. package/src/core/evolution.ts +215 -0
  29. package/src/core/hologram/engine.ts +71 -0
  30. package/src/core/hologram/fallback.ts +11 -0
  31. package/src/core/hologram/incremental.ts +227 -0
  32. package/src/core/hologram/py-extractor.ts +132 -0
  33. package/src/core/hologram/ts-extractor.ts +320 -0
  34. package/src/core/hologram/types.ts +25 -0
  35. package/src/core/hologram.ts +64 -236
  36. package/src/core/hook-manager.ts +259 -0
  37. package/src/core/i18n/messages.ts +309 -0
  38. package/src/core/immune.ts +8 -123
  39. package/src/core/locale.ts +88 -0
  40. package/src/core/log-rotate.ts +33 -0
  41. package/src/core/log-utils.ts +38 -0
  42. package/src/core/lru-map.ts +61 -0
  43. package/src/core/notify.ts +53 -14
  44. package/src/core/rule-engine.ts +287 -0
  45. package/src/core/semantic-diff.ts +432 -0
  46. package/src/core/telemetry.ts +94 -0
  47. package/src/core/vaccine-registry.ts +212 -0
  48. package/src/core/workspace.ts +28 -0
  49. package/src/core/yaml-minimal.ts +176 -0
  50. package/src/daemon/client.ts +34 -6
  51. package/src/daemon/event-batcher.ts +108 -0
  52. package/src/daemon/guards.ts +13 -0
  53. package/src/daemon/http-routes.ts +293 -0
  54. package/src/daemon/mcp-handler.ts +270 -0
  55. package/src/daemon/server.ts +492 -273
  56. package/src/daemon/types.ts +100 -0
  57. package/src/daemon/workspace-map.ts +92 -0
  58. package/src/platform.ts +60 -0
  59. package/src/version.ts +15 -0
@@ -0,0 +1,129 @@
1
+ /**
2
+ * afd mcp — MCP server management
3
+ *
4
+ * Subcommands:
5
+ * install — Register afd as an MCP server in project and global Claude config
6
+ */
7
+
8
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
9
+ import { join, resolve, dirname } from "path";
10
+ import { homedir } from "os";
11
+ import { getSystemLanguage } from "../core/locale";
12
+
13
+ const msgs = {
14
+ en: {
15
+ usage: "Usage: afd mcp install",
16
+ unknownSub: (s: string) => `Unknown subcommand: ${s}. Use: afd mcp install`,
17
+ installing: "Registering afd MCP server...",
18
+ projectDone: (p: string) => ` [project] Registered in ${p}`,
19
+ projectSkip: (p: string) => ` [project] Already registered in ${p}`,
20
+ globalDone: (p: string) => ` [global] Registered in ${p}`,
21
+ globalSkip: (p: string) => ` [global] Already registered in ${p}`,
22
+ success: "afd MCP server registration complete. Restart Claude Code to activate.",
23
+ hintRestart: " Hint: Run 'claude --reload-plugins' or restart the IDE to pick up the new MCP server.",
24
+ },
25
+ ko: {
26
+ usage: "사용법: afd mcp install",
27
+ unknownSub: (s: string) => `알 수 없는 하위 명령: ${s}. 사용: afd mcp install`,
28
+ installing: "afd MCP 서버 등록 중...",
29
+ projectDone: (p: string) => ` [project] ${p}에 등록 완료`,
30
+ projectSkip: (p: string) => ` [project] ${p}에 이미 등록됨`,
31
+ globalDone: (p: string) => ` [global] ${p}에 등록 완료`,
32
+ globalSkip: (p: string) => ` [global] ${p}에 이미 등록됨`,
33
+ success: "afd MCP 서버 등록 완료. Claude Code를 재시작하면 활성화됩니다.",
34
+ hintRestart: " 힌트: 'claude --reload-plugins' 또는 IDE를 재시작하여 MCP 서버를 인식시키세요.",
35
+ },
36
+ };
37
+
38
+ interface McpServerEntry {
39
+ command: string;
40
+ args: string[];
41
+ [key: string]: unknown;
42
+ }
43
+
44
+ interface McpConfig {
45
+ mcpServers?: Record<string, McpServerEntry>;
46
+ [key: string]: unknown;
47
+ }
48
+
49
+ /** The canonical MCP server definition for afd */
50
+ function afdMcpEntry(): McpServerEntry {
51
+ return {
52
+ command: "bun",
53
+ args: ["run", "src/daemon/server.ts", "--mcp"],
54
+ };
55
+ }
56
+
57
+ /** Register afd in a JSON config file's mcpServers section */
58
+ function registerInFile(filePath: string, entry: McpServerEntry): "done" | "skip" | "error" {
59
+ let config: McpConfig = {};
60
+ if (existsSync(filePath)) {
61
+ try {
62
+ config = JSON.parse(readFileSync(filePath, "utf-8"));
63
+ } catch {
64
+ // Preserve existing file by reading raw content for backup
65
+ config = {};
66
+ }
67
+ } else {
68
+ try {
69
+ mkdirSync(dirname(filePath), { recursive: true });
70
+ } catch { return "error"; }
71
+ }
72
+
73
+ const servers = (config.mcpServers ?? {}) as Record<string, McpServerEntry>;
74
+ const existing = servers.afd;
75
+
76
+ if (existing?.command === entry.command &&
77
+ JSON.stringify(existing.args) === JSON.stringify(entry.args)) {
78
+ return "skip";
79
+ }
80
+
81
+ servers.afd = entry;
82
+ config.mcpServers = servers;
83
+ try {
84
+ writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
85
+ } catch {
86
+ return "error";
87
+ }
88
+ return "done";
89
+ }
90
+
91
+ /** Resolve the global Claude Code settings path (cross-platform) */
92
+ function globalClaudeConfigPath(): string {
93
+ return join(homedir(), ".claude.json");
94
+ }
95
+
96
+ export async function mcpCommand(subcommand?: string) {
97
+ const lang = getSystemLanguage();
98
+ const m = msgs[lang];
99
+
100
+ if (!subcommand) {
101
+ console.log(m.usage);
102
+ return;
103
+ }
104
+
105
+ if (subcommand !== "install") {
106
+ console.error(m.unknownSub(subcommand));
107
+ process.exit(1);
108
+ }
109
+
110
+ console.log(m.installing);
111
+
112
+ const entry = afdMcpEntry();
113
+
114
+ // 1. Project-level: .mcp.json
115
+ const projectPath = resolve(".mcp.json");
116
+ const projectResult = registerInFile(projectPath, entry);
117
+ if (projectResult === "error") console.error(` [project] Failed to write ${projectPath}`);
118
+ else console.log(projectResult === "done" ? m.projectDone(projectPath) : m.projectSkip(projectPath));
119
+
120
+ // 2. Global-level: ~/.claude.json — mcpServers section
121
+ const globalPath = globalClaudeConfigPath();
122
+ const globalResult = registerInFile(globalPath, entry);
123
+ if (globalResult === "error") console.error(` [global] Failed to write ${globalPath}`);
124
+ else console.log(globalResult === "done" ? m.globalDone(globalPath) : m.globalSkip(globalPath));
125
+
126
+ console.log("");
127
+ console.log(m.success);
128
+ console.log(m.hintRestart);
129
+ }
@@ -0,0 +1,14 @@
1
+ import { stopCommand } from "./stop";
2
+ import { startCommand } from "./start";
3
+ import { getSystemLanguage } from "../core/locale";
4
+ import { getMessages } from "../core/i18n/messages";
5
+
6
+ export async function restartCommand() {
7
+ const lang = getSystemLanguage();
8
+ const msg = getMessages(lang);
9
+
10
+ console.log(msg.DAEMON_RESTARTING);
11
+
12
+ await stopCommand();
13
+ await startCommand();
14
+ }
@@ -1,12 +1,29 @@
1
1
  import { daemonRequest } from "../daemon/client";
2
+ import { fmtNum, visualWidth, localizedBoast } from "../core/boast";
3
+ import type { ShiftSummary } from "../core/boast";
4
+ import { getSystemLanguage } from "../core/locale";
5
+ import { getMessages, t } from "../core/i18n/messages";
2
6
 
3
- interface HologramScore {
7
+ interface HologramEntry {
4
8
  requests: number;
5
9
  originalChars: number;
6
10
  hologramChars: number;
7
11
  savings: number;
8
12
  }
9
13
 
14
+ interface HologramDailyRow {
15
+ date: string;
16
+ requests: number;
17
+ originalChars: number;
18
+ hologramChars: number;
19
+ }
20
+
21
+ interface HologramScore {
22
+ lifetime: HologramEntry;
23
+ today: HologramEntry | null;
24
+ daily: HologramDailyRow[];
25
+ }
26
+
10
27
  interface AutoHealEntry {
11
28
  id: string;
12
29
  at: number;
@@ -28,6 +45,17 @@ interface EcosystemScore {
28
45
  primary: string;
29
46
  }
30
47
 
48
+ interface SuppressionScore {
49
+ massEventsSkipped: number;
50
+ dormantTransitions: number;
51
+ activeTaps: number;
52
+ }
53
+
54
+ interface DynamicImmuneScore {
55
+ activeValidators: number;
56
+ validatorNames: string[];
57
+ }
58
+
31
59
  interface ScoreData {
32
60
  uptime: number;
33
61
  filesDetected: number;
@@ -39,8 +67,27 @@ interface ScoreData {
39
67
  hologram: HologramScore;
40
68
  immune: ImmuneScore;
41
69
  ecosystem: EcosystemScore;
70
+ suppression: SuppressionScore;
71
+ dynamicImmune?: DynamicImmuneScore;
42
72
  }
43
73
 
74
+ // ── ANSI helpers ──
75
+ const C = {
76
+ reset: "\x1b[0m",
77
+ bold: "\x1b[1m",
78
+ dim: "\x1b[2m",
79
+ red: "\x1b[31m",
80
+ green: "\x1b[32m",
81
+ yellow: "\x1b[33m",
82
+ blue: "\x1b[34m",
83
+ magenta: "\x1b[35m",
84
+ cyan: "\x1b[36m",
85
+ white: "\x1b[37m",
86
+ bgRed: "\x1b[41m",
87
+ bgGreen: "\x1b[42m",
88
+ bgYellow: "\x1b[43m",
89
+ };
90
+
44
91
  function formatUptime(seconds: number): string {
45
92
  if (seconds < 60) return `${seconds}s`;
46
93
  if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
@@ -49,9 +96,13 @@ function formatUptime(seconds: number): string {
49
96
  return `${h}h ${m}m`;
50
97
  }
51
98
 
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);
99
+ function gaugeBar(value: number, max: number, width = 20): string {
100
+ const ratio = Math.min(value / Math.max(max, 1), 1);
101
+ const filled = Math.round(ratio * width);
102
+ const empty = width - filled;
103
+ const color = ratio >= 0.7 ? C.green : ratio >= 0.4 ? C.yellow : C.red;
104
+ const pct = Math.round(ratio * 100);
105
+ return `${color}${"█".repeat(filled)}${C.dim}${"░".repeat(empty)}${C.reset} ${pct}%`;
55
106
  }
56
107
 
57
108
  function formatChars(n: number): string {
@@ -60,89 +111,166 @@ function formatChars(n: number): string {
60
111
  return `${(n / 1_000_000).toFixed(1)}M`;
61
112
  }
62
113
 
63
- const W = 46; // inner box width
64
- const line = "\u2500".repeat(W);
65
- const row = (content: string) => `\u2502${content.padEnd(W)}\u2502`;
114
+ const W = 52;
115
+ const line = "".repeat(W);
116
+
117
+ function row(content: string): string {
118
+ // Strip ANSI for width calculation
119
+ const stripped = content.replace(/\x1b\[[0-9;]*m/g, "");
120
+ const vw = visualWidth(stripped);
121
+ const padSize = Math.max(0, W - vw);
122
+ return `│${content}${" ".repeat(padSize)}│`;
123
+ }
124
+
125
+ function section(title: string): string {
126
+ return row(` ${C.bold}${C.cyan}${title}${C.reset}`);
127
+ }
128
+
129
+ function kv(label: string, value: string): string {
130
+ const stripped = label.replace(/\x1b\[[0-9;]*m/g, "");
131
+ const gap = 18 - visualWidth(stripped);
132
+ return row(` ${C.dim}${label}${C.reset}${" ".repeat(Math.max(1, gap))}${value}`);
133
+ }
134
+
135
+ function guardianGrade(antibodies: number, heals: number, holoSavings: number): { grade: string; label: string; color: string } {
136
+ const score = Math.min(antibodies * 15, 40) + Math.min(heals * 10, 30) + Math.min(holoSavings * 0.3, 30);
137
+ if (score >= 80) return { grade: "A+", label: "FORTIFIED", color: C.green };
138
+ if (score >= 60) return { grade: "A", label: "GUARDED", color: C.green };
139
+ if (score >= 40) return { grade: "B", label: "LEARNING", color: C.yellow };
140
+ if (score >= 20) return { grade: "C", label: "EXPOSED", color: C.yellow };
141
+ return { grade: "D", label: "VULNERABLE", color: C.red };
142
+ }
66
143
 
67
144
  export async function scoreCommand() {
145
+ const lang = getSystemLanguage();
146
+ const i18n = getMessages(lang);
147
+
68
148
  try {
69
149
  const data = await daemonRequest<ScoreData>("/score");
70
150
  const h = data.hologram;
151
+ const holoSav = h.lifetime.savings;
152
+ const grade = guardianGrade(data.immune.antibodies, data.immune.autoHealed, holoSav);
153
+
154
+ const out: string[] = [];
155
+
156
+ // ── Header ──
157
+ out.push(`┌${line}┐`);
158
+ out.push(row(` ${C.bold}🛡️ Guardian Status${C.reset} ${grade.color}${C.bold}[ ${grade.grade} ]${C.reset} ${grade.color}${grade.label}${C.reset}`));
159
+ out.push(`├${line}┤`);
71
160
 
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}`));
161
+ // ── Health Gauge ──
162
+ const healthPct = Math.min(data.immune.antibodies * 15 + data.immune.autoHealed * 10 + holoSav * 0.3, 100);
163
+ out.push(row(` ${gaugeBar(healthPct, 100, 30)}`));
164
+ out.push(`├${line}┤`);
165
+
166
+ // ── System Info ──
167
+ out.push(section(lang === "ko" ? "시스템 정보" : "System Info"));
168
+ out.push(kv(i18n.SCORE_ECOSYSTEM, `${C.white}${C.bold}${data.ecosystem.primary}${C.reset}`));
76
169
  if (data.ecosystem.detected.length > 1) {
77
170
  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>"));
171
+ out.push(kv(i18n.SCORE_ALSO_FOUND, `${C.dim}${others}${C.reset}`));
101
172
  }
173
+ out.push(kv(i18n.SCORE_UPTIME, `${C.green}${formatUptime(data.uptime)}${C.reset}`));
174
+ out.push(kv(i18n.SCORE_EVENTS, `${data.totalEvents}`));
175
+ out.push(kv(i18n.SCORE_FILES_FOUND, `${data.watchedFiles.length}`));
102
176
 
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`));
177
+ // ── Immune System ──
178
+ out.push(`├${line}┤`);
179
+ out.push(section(lang === "ko" ? "면역 시스템" : "Immune System"));
107
180
  const ab = data.immune.antibodies;
108
181
  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" : ""}`));
182
+ out.push(kv(i18n.SCORE_ANTIBODIES, `${C.bold}${ab}${C.reset}`));
183
+ out.push(row(` ${lang === "ko" ? "면역력" : "Immunity"} ${gaugeBar(ab, 10, 20)}`));
184
+ out.push(kv(lang === "ko" ? "차단 횟수" : "Prevented", `${C.bold}${C.green}${ah}${C.reset}${C.dim} disaster${ah !== 1 ? "s" : ""}${C.reset}`));
114
185
  if (data.immune.lastAutoHeal) {
115
186
  const ago = formatUptime(Math.floor((Date.now() - data.immune.lastAutoHeal.at) / 1000));
116
- console.log(row(` Last heal : ${data.immune.lastAutoHeal.id} (${ago} ago)`));
187
+ out.push(kv(lang === "ko" ? "마지막 치유" : "Last Heal", `${data.immune.lastAutoHeal.id} ${C.dim}(${ago} ago)${C.reset}`));
117
188
  }
118
189
 
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`));
190
+ // ── Dynamic Immune Synthesis ──
191
+ if (data.dynamicImmune && data.dynamicImmune.activeValidators > 0) {
192
+ out.push(`├${line}┤`);
193
+ out.push(section(lang === "ko" ? "진화 상태 (동적 면역)" : "Evolution (Dynamic Immune)"));
194
+ out.push(kv(
195
+ lang === "ko" ? "활성 검증기" : "Validators",
196
+ `${C.bold}${C.green}${data.dynamicImmune.activeValidators} Active${C.reset}`
197
+ ));
198
+ const names = data.dynamicImmune.validatorNames.slice(0, 3).join(", ");
199
+ const extra = data.dynamicImmune.validatorNames.length > 3
200
+ ? ` ${C.dim}+${data.dynamicImmune.validatorNames.length - 3} more${C.reset}`
201
+ : "";
202
+ out.push(kv(
203
+ lang === "ko" ? "스크립트" : "Scripts",
204
+ `${C.dim}${names}${C.reset}${extra}`
205
+ ));
206
+ }
207
+
208
+ // ── Hologram Efficiency ──
209
+ out.push(`├${line}┤`);
210
+ out.push(section(lang === "ko" ? "토큰 효율 (홀로그램)" : "Token Efficiency (Hologram)"));
211
+ const lt = h.lifetime;
212
+ if (lt.requests > 0) {
213
+ const ltSaved = lt.originalChars - lt.hologramChars;
214
+ out.push(kv(lang === "ko" ? "총 요청" : "Requests", `${lt.requests}`));
215
+ out.push(kv(lang === "ko" ? "절약된 컨텍스트" : "Saved Context", `${C.green}${formatChars(ltSaved)} chars${C.reset}`));
216
+ out.push(row(` ${lang === "ko" ? "효율" : "Efficiency"} ${gaugeBar(lt.savings, 100, 20)}`));
217
+ if (h.today && h.today.requests > 0) {
218
+ const todaySaved = h.today.originalChars - h.today.hologramChars;
219
+ out.push(kv(lang === "ko" ? "오늘" : "Today", `${h.today.requests} req / ${C.green}${formatChars(todaySaved)} saved${C.reset}`));
128
220
  }
129
221
  } else {
130
- console.log(row(" No files detected yet."));
222
+ out.push(row(` ${C.dim}${i18n.SCORE_HOLOGRAM_EMPTY}${C.reset}`));
223
+ out.push(row(` ${C.dim}${i18n.SCORE_HOLOGRAM_HINT}${C.reset}`));
131
224
  }
132
225
 
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
- }
226
+ // ── Value Delivered (ROI) ──
227
+ try {
228
+ const summary = await daemonRequest<ShiftSummary>("/shift-summary");
229
+ out.push(`├${line}┤`);
230
+ out.push(section(lang === "ko" ? "전달된 가치 (ROI)" : "Value Delivered (ROI)"));
231
+
232
+ // Breakdown: Auto-Heal
233
+ if (summary.healTokensSaved > 0) {
234
+ out.push(kv(
235
+ lang === "ko" ? "🩹 치유 절약" : "🩹 Heal Saved",
236
+ `${C.dim}~${fmtNum(summary.healTokensSaved)} tok / $${summary.healCostSaved.toFixed(2)}${C.reset}`
237
+ ));
238
+ }
239
+
240
+ // Breakdown: Hologram
241
+ if (summary.hologramTokensSaved > 0) {
242
+ out.push(kv(
243
+ lang === "ko" ? "💎 홀로그램" : "💎 Hologram",
244
+ `${C.dim}~${fmtNum(summary.hologramTokensSaved)} tok / $${summary.hologramCostSaved.toFixed(2)}${C.reset}`
245
+ ));
246
+ }
247
+
248
+ // Total
249
+ out.push(kv(
250
+ lang === "ko" ? "총 절약 토큰" : "Total Tokens",
251
+ `${C.bold}${C.green}~${fmtNum(summary.totalTokensSaved)}${C.reset}`
252
+ ));
253
+ out.push(kv(lang === "ko" ? "절약 시간" : "Time Saved", `${C.green}~${summary.totalMinutesSaved} min${C.reset}`));
254
+ out.push(kv(
255
+ lang === "ko" ? "총 절약 비용" : "Total Cost",
256
+ `${C.bold}${C.green}~$${summary.totalCostSaved.toFixed(2)}${C.reset}`
257
+ ));
258
+ if (summary.suppressionsSkipped > 0) {
259
+ out.push(kv(lang === "ko" ? "억제 횟수" : "Suppressed", `${summary.suppressionsSkipped}`));
260
+ }
261
+ } catch { /* non-fatal */ }
262
+
263
+ // ── Boast ──
264
+ out.push(`├${line}┤`);
265
+ const boast = localizedBoast(lang);
266
+ const truncBoast = boast.length > W - 6 ? boast.slice(0, W - 9) + "..." : boast;
267
+ out.push(row(` ${C.magenta}🗣️ ${truncBoast}${C.reset}`));
268
+ out.push(`└${line}┘`);
141
269
 
142
- console.log(`\u2514${line}\u2518`);
270
+ console.log(out.join("\n"));
143
271
  } catch (err: unknown) {
144
272
  const msg = err instanceof Error ? err.message : String(err);
145
- console.error(`[afd] ${msg}`);
273
+ console.error(`${C.red}[afd] ${msg}${C.reset}`);
146
274
  process.exit(1);
147
275
  }
148
276
  }
@@ -1,55 +1,155 @@
1
- import { spawn } from "child_process";
2
1
  import { resolve } from "path";
2
+ import { spawn } from "child_process";
3
+ import { openSync, mkdirSync } from "fs";
3
4
  import { getDaemonInfo, isDaemonAlive } from "../daemon/client";
4
- import { AFD_DIR } from "../constants";
5
- import { mkdirSync } from "fs";
5
+ import { WATCH_TARGETS, resolveWorkspacePaths } from "../constants";
6
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"] = [];
7
41
 
8
- export async function startCommand() {
9
- mkdirSync(AFD_DIR, { recursive: true });
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
+ }
10
78
 
11
- // Check if already running
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 ──
12
97
  const existing = getDaemonInfo();
13
- if (existing && await isDaemonAlive(existing)) {
14
- console.log(`[afd] Daemon already running (pid=${existing.pid}, port=${existing.port})`);
98
+ if (existing && (await isDaemonAlive(existing))) {
99
+ console.log(msg.DAEMON_ALREADY_RUNNING);
15
100
  return;
16
101
  }
17
102
 
18
- // Spawn detached daemon
103
+ // ── Spawn detached daemon with log redirection ──
19
104
  const daemonScript = resolve(import.meta.dirname, "../daemon/server.ts");
20
- const bunPath = process.execPath;
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];
21
113
 
22
- const child = spawn(bunPath, ["run", daemonScript], {
23
- detached: true,
24
- stdio: ["ignore", "ignore", "ignore"],
25
- cwd: process.cwd(),
26
- env: { ...process.env },
27
- });
114
+ const child = spawn("bun", args, detachedSpawnOptions(logFd));
28
115
 
116
+ // Detach: allow parent to exit without killing child
29
117
  child.unref();
30
118
 
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
- }
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);
51
133
  } else {
52
- console.error("[afd] Failed to start daemon. Check logs.");
134
+ console.error(t(msg.DAEMON_START_FAILED, { path: logPath }));
53
135
  process.exit(1);
54
136
  }
55
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
+ }