autonomous-flow-daemon 1.1.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 (55) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.ko.md +124 -164
  3. package/README.md +99 -170
  4. package/package.json +11 -5
  5. package/src/adapters/index.ts +246 -35
  6. package/src/cli.ts +71 -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/mcp.ts +129 -0
  14. package/src/commands/restart.ts +14 -0
  15. package/src/commands/score.ts +164 -96
  16. package/src/commands/start.ts +74 -15
  17. package/src/commands/stats.ts +103 -0
  18. package/src/commands/status.ts +157 -0
  19. package/src/commands/stop.ts +23 -4
  20. package/src/commands/sync.ts +253 -20
  21. package/src/commands/vaccine.ts +177 -0
  22. package/src/constants.ts +25 -1
  23. package/src/core/boast.ts +27 -12
  24. package/src/core/db.ts +74 -3
  25. package/src/core/evolution.ts +215 -0
  26. package/src/core/hologram/engine.ts +71 -0
  27. package/src/core/hologram/fallback.ts +11 -0
  28. package/src/core/hologram/incremental.ts +227 -0
  29. package/src/core/hologram/py-extractor.ts +132 -0
  30. package/src/core/hologram/ts-extractor.ts +320 -0
  31. package/src/core/hologram/types.ts +25 -0
  32. package/src/core/hologram.ts +64 -236
  33. package/src/core/hook-manager.ts +259 -0
  34. package/src/core/i18n/messages.ts +43 -0
  35. package/src/core/immune.ts +8 -123
  36. package/src/core/log-rotate.ts +33 -0
  37. package/src/core/log-utils.ts +38 -0
  38. package/src/core/lru-map.ts +61 -0
  39. package/src/core/notify.ts +27 -19
  40. package/src/core/rule-engine.ts +287 -0
  41. package/src/core/semantic-diff.ts +432 -0
  42. package/src/core/telemetry.ts +94 -0
  43. package/src/core/vaccine-registry.ts +212 -0
  44. package/src/core/workspace.ts +28 -0
  45. package/src/core/yaml-minimal.ts +176 -0
  46. package/src/daemon/client.ts +34 -6
  47. package/src/daemon/event-batcher.ts +108 -0
  48. package/src/daemon/guards.ts +13 -0
  49. package/src/daemon/http-routes.ts +293 -0
  50. package/src/daemon/mcp-handler.ts +270 -0
  51. package/src/daemon/server.ts +439 -353
  52. package/src/daemon/types.ts +100 -0
  53. package/src/daemon/workspace-map.ts +92 -0
  54. package/src/platform.ts +23 -2
  55. 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
+ }
@@ -4,13 +4,26 @@ import type { ShiftSummary } from "../core/boast";
4
4
  import { getSystemLanguage } from "../core/locale";
5
5
  import { getMessages, t } from "../core/i18n/messages";
6
6
 
7
- interface HologramScore {
7
+ interface HologramEntry {
8
8
  requests: number;
9
9
  originalChars: number;
10
10
  hologramChars: number;
11
11
  savings: number;
12
12
  }
13
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
+
14
27
  interface AutoHealEntry {
15
28
  id: string;
16
29
  at: number;
@@ -38,6 +51,11 @@ interface SuppressionScore {
38
51
  activeTaps: number;
39
52
  }
40
53
 
54
+ interface DynamicImmuneScore {
55
+ activeValidators: number;
56
+ validatorNames: string[];
57
+ }
58
+
41
59
  interface ScoreData {
42
60
  uptime: number;
43
61
  filesDetected: number;
@@ -50,8 +68,26 @@ interface ScoreData {
50
68
  immune: ImmuneScore;
51
69
  ecosystem: EcosystemScore;
52
70
  suppression: SuppressionScore;
71
+ dynamicImmune?: DynamicImmuneScore;
53
72
  }
54
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
+
55
91
  function formatUptime(seconds: number): string {
56
92
  if (seconds < 60) return `${seconds}s`;
57
93
  if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
@@ -60,9 +96,13 @@ function formatUptime(seconds: number): string {
60
96
  return `${h}h ${m}m`;
61
97
  }
62
98
 
63
- function heatBar(value: number, max: number, width = 20): string {
64
- const filled = Math.min(Math.round((value / Math.max(max, 1)) * width), width);
65
- 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}%`;
66
106
  }
67
107
 
68
108
  function formatChars(n: number): string {
@@ -71,24 +111,34 @@ function formatChars(n: number): string {
71
111
  return `${(n / 1_000_000).toFixed(1)}M`;
72
112
  }
73
113
 
74
- const W = 46;
75
- const line = "\u2500".repeat(W);
76
- const sep = "\u2500".repeat(30);
114
+ const W = 52;
115
+ const line = "".repeat(W);
77
116
 
78
117
  function row(content: string): string {
79
- const vw = visualWidth(content);
118
+ // Strip ANSI for width calculation
119
+ const stripped = content.replace(/\x1b\[[0-9;]*m/g, "");
120
+ const vw = visualWidth(stripped);
80
121
  const padSize = Math.max(0, W - vw);
81
- return `\u2502${content}${" ".repeat(padSize)}\u2502`;
122
+ return `│${content}${" ".repeat(padSize)}│`;
82
123
  }
83
124
 
84
- function vwPad(s: string, target: number): string {
85
- const vw = visualWidth(s);
86
- return s + " ".repeat(Math.max(0, target - vw));
125
+ function section(title: string): string {
126
+ return row(` ${C.bold}${C.cyan}${title}${C.reset}`);
87
127
  }
88
128
 
89
- /** Render a labeled key-value row with visual-width-aware padding. */
90
129
  function kv(label: string, value: string): string {
91
- return row(` ${vwPad(label, 13)}: ${value}`);
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 };
92
142
  }
93
143
 
94
144
  export async function scoreCommand() {
@@ -98,111 +148,129 @@ export async function scoreCommand() {
98
148
  try {
99
149
  const data = await daemonRequest<ScoreData>("/score");
100
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}┤`);
101
160
 
102
- // Title
103
- console.log(`\u250C${line}\u2510`);
104
- console.log(row(` ${i18n.SCORE_TITLE}`));
105
- console.log(`\u251C${line}\u2524`);
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}┤`);
106
165
 
107
- // Ecosystem
108
- console.log(kv(i18n.SCORE_ECOSYSTEM, data.ecosystem.primary));
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}`));
109
169
  if (data.ecosystem.detected.length > 1) {
110
170
  const others = data.ecosystem.detected.slice(1).map(e => e.name).join(", ");
111
- console.log(kv(i18n.SCORE_ALSO_FOUND, others));
171
+ out.push(kv(i18n.SCORE_ALSO_FOUND, `${C.dim}${others}${C.reset}`));
112
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}`));
113
176
 
114
- // Uptime / Events / Files
115
- console.log(`\u251C${line}\u2524`);
116
- console.log(kv(i18n.SCORE_UPTIME, formatUptime(data.uptime)));
117
- console.log(kv(i18n.SCORE_EVENTS, String(data.totalEvents)));
118
- console.log(kv(i18n.SCORE_FILES_FOUND, String(data.watchedFiles.length)));
119
- console.log(`\u251C${line}\u2524`);
120
- console.log(row(` ${vwPad(i18n.SCORE_ACTIVITY, 10)}${heatBar(data.totalEvents, 100)}`));
121
-
122
- // Hologram section
123
- console.log(`\u251C${line}\u2524`);
124
- console.log(row(` ${i18n.SCORE_HOLOGRAM_TITLE}`));
125
- console.log(row(` ${sep}`));
126
- if (h.requests > 0) {
127
- const saved = h.originalChars - h.hologramChars;
128
- console.log(kv(i18n.SCORE_HOLOGRAM_REQUESTS, String(h.requests)));
129
- console.log(kv(i18n.SCORE_HOLOGRAM_ORIGINAL, `${formatChars(h.originalChars)} chars`));
130
- console.log(kv(i18n.SCORE_HOLOGRAM_COMPRESSED, `${formatChars(h.hologramChars)} chars`));
131
- console.log(kv(i18n.SCORE_HOLOGRAM_SAVED, `${formatChars(saved)} chars (${h.savings}%)`));
132
- console.log(row(` ${vwPad(i18n.SCORE_HOLOGRAM_EFFICIENCY, 10)}${heatBar(h.savings, 100)}`));
133
- } else {
134
- console.log(row(` ${i18n.SCORE_HOLOGRAM_EMPTY}`));
135
- console.log(row(` ${i18n.SCORE_HOLOGRAM_HINT}`));
136
- }
137
-
138
- // Immune System section
139
- console.log(`\u251C${line}\u2524`);
140
- console.log(row(` ${i18n.SCORE_IMMUNE_TITLE}`));
141
- console.log(row(` ${sep}`));
177
+ // ── Immune System ──
178
+ out.push(`├${line}┤`);
179
+ out.push(section(lang === "ko" ? "면역 시스템" : "Immune System"));
142
180
  const ab = data.immune.antibodies;
143
181
  const ah = data.immune.autoHealed;
144
- const immuneLevel = ab === 0 ? i18n.SCORE_IMMUNE_VULNERABLE
145
- : ab < 3 ? i18n.SCORE_IMMUNE_LEARNING
146
- : ab < 6 ? i18n.SCORE_IMMUNE_GUARDED
147
- : i18n.SCORE_IMMUNE_FORTIFIED;
148
- console.log(kv(i18n.SCORE_ANTIBODIES, String(ab)));
149
- console.log(kv(i18n.SCORE_LEVEL, immuneLevel));
150
- console.log(row(` ${vwPad(i18n.SCORE_IMMUNITY, 10)}${heatBar(ab, 10)}`));
151
- const healedStr = t(i18n.SCORE_AUTO_HEALED, { count: ah, s: ah !== 1 ? "s" : "" });
152
- console.log(kv(i18n.SCORE_AUTO_HEALED_LABEL, healedStr));
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}`));
153
185
  if (data.immune.lastAutoHeal) {
154
186
  const ago = formatUptime(Math.floor((Date.now() - data.immune.lastAutoHeal.at) / 1000));
155
- const healStr = t(i18n.SCORE_LAST_HEAL, { id: data.immune.lastAutoHeal.id, ago });
156
- console.log(kv(i18n.SCORE_LAST_EVENT, healStr));
187
+ out.push(kv(lang === "ko" ? "마지막 치유" : "Last Heal", `${data.immune.lastAutoHeal.id} ${C.dim}(${ago} ago)${C.reset}`));
157
188
  }
158
189
 
159
- // Watched files
160
- console.log(`\u251C${line}\u2524`);
161
- if (data.watchedFiles.length > 0) {
162
- console.log(row(` ${i18n.SCORE_WATCHED_FILES}`));
163
- for (const f of data.watchedFiles.slice(0, 8)) {
164
- console.log(row(` ${f.substring(0, W - 6)}`));
165
- }
166
- if (data.watchedFiles.length > 8) {
167
- console.log(row(` ... +${data.watchedFiles.length - 8} more`));
168
- }
169
- } else {
170
- console.log(row(` ${i18n.SCORE_NO_FILES}`));
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
+ ));
171
206
  }
172
207
 
173
- // Last event
174
- if (data.lastEvent) {
175
- const ago = data.lastEventAt
176
- ? t(i18n.SCORE_AGO, { time: formatUptime(Math.floor((Date.now() - data.lastEventAt) / 1000)) })
177
- : "unknown";
178
- console.log(`\u251C${line}\u2524`);
179
- console.log(row(` ${vwPad(i18n.SCORE_LAST_EVENT, 6)}: ${data.lastEvent.substring(0, 34)}`));
180
- console.log(row(` ${ago}`));
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}`));
220
+ }
221
+ } else {
222
+ out.push(row(` ${C.dim}${i18n.SCORE_HOLOGRAM_EMPTY}${C.reset}`));
223
+ out.push(row(` ${C.dim}${i18n.SCORE_HOLOGRAM_HINT}${C.reset}`));
181
224
  }
182
225
 
183
- // Value Metrics section
226
+ // ── Value Delivered (ROI) ──
184
227
  try {
185
228
  const summary = await daemonRequest<ShiftSummary>("/shift-summary");
186
- console.log(`\u251C${line}\u2524`);
187
- console.log(row(` ${i18n.SCORE_VALUE_TITLE}`));
188
- console.log(row(` ${sep}`));
189
- console.log(kv(i18n.SHIFT_TOKENS, `~${fmtNum(summary.totalTokensSaved)}`));
190
- console.log(kv(i18n.SHIFT_TIME, `~${summary.totalMinutesSaved} min`));
191
- console.log(kv(i18n.SHIFT_COST, `~$${summary.totalCostSaved.toFixed(2)}`));
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
+ ));
192
258
  if (summary.suppressionsSkipped > 0) {
193
- console.log(kv(i18n.SHIFT_SUPPRESSED, `${summary.suppressionsSkipped}`));
259
+ out.push(kv(lang === "ko" ? "억제 횟수" : "Suppressed", `${summary.suppressionsSkipped}`));
194
260
  }
195
- console.log(`\u251C${line}\u2524`);
196
- const boast = localizedBoast(lang);
197
- console.log(row(` \uD83D\uDDE3\uFE0F ${boast.substring(0, W - 6)}`));
198
- } catch {
199
- // Non-fatal
200
- }
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}┘`);
201
269
 
202
- console.log(`\u2514${line}\u2518`);
270
+ console.log(out.join("\n"));
203
271
  } catch (err: unknown) {
204
272
  const msg = err instanceof Error ? err.message : String(err);
205
- console.error(`[afd] ${msg}`);
273
+ console.error(`${C.red}[afd] ${msg}${C.reset}`);
206
274
  process.exit(1);
207
275
  }
208
276
  }
@@ -2,16 +2,83 @@ import { resolve } from "path";
2
2
  import { spawn } from "child_process";
3
3
  import { openSync, mkdirSync } from "fs";
4
4
  import { getDaemonInfo, isDaemonAlive } from "../daemon/client";
5
- import { AFD_DIR, LOG_FILE, WATCH_TARGETS } from "../constants";
5
+ import { WATCH_TARGETS, resolveWorkspacePaths } from "../constants";
6
6
  import { detectEcosystem } from "../adapters/index";
7
+ import type { DetectionResult } from "../adapters/index";
7
8
  import { detachedSpawnOptions, IS_WINDOWS } from "../platform";
9
+ import { rotateLogIfNeeded } from "../core/log-rotate";
8
10
  import { getSystemLanguage } from "../core/locale";
9
11
  import { getMessages, t } from "../core/i18n/messages";
12
+ import type { MessageDict } from "../core/i18n/messages";
10
13
  import { discoverWatchTargets } from "../core/discovery";
11
14
 
12
15
  const STARTUP_POLL_INTERVAL_MS = 100;
13
16
  const STARTUP_POLL_MAX_MS = 3000;
14
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
+
15
82
  export async function startCommand(options?: { mcp?: boolean }) {
16
83
  // MCP stdio mode: run daemon in foreground with stdio transport
17
84
  if (options?.mcp) {
@@ -23,7 +90,8 @@ export async function startCommand(options?: { mcp?: boolean }) {
23
90
  const lang = getSystemLanguage();
24
91
  const msg = getMessages(lang);
25
92
 
26
- mkdirSync(AFD_DIR, { recursive: true });
93
+ const paths = resolveWorkspacePaths();
94
+ mkdirSync(paths.afdDir, { recursive: true });
27
95
 
28
96
  // ── Idempotency: check if already running ──
29
97
  const existing = getDaemonInfo();
@@ -34,7 +102,8 @@ export async function startCommand(options?: { mcp?: boolean }) {
34
102
 
35
103
  // ── Spawn detached daemon with log redirection ──
36
104
  const daemonScript = resolve(import.meta.dirname, "../daemon/server.ts");
37
- const logPath = resolve(LOG_FILE);
105
+ const logPath = paths.logFile;
106
+ rotateLogIfNeeded(logPath);
38
107
  const logFd = openSync(logPath, "a"); // append mode
39
108
 
40
109
  // On Windows, wrap in shell for proper detach; quote path for spaces
@@ -59,18 +128,8 @@ export async function startCommand(options?: { mcp?: boolean }) {
59
128
  console.log(`[afd] Targets: ${discovery.targets.join(", ")}`);
60
129
  console.log(t(msg.DAEMON_LOGS, { path: logPath }));
61
130
 
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
- }
131
+ // One-Command Zero-Touch: auto-provision all ecosystem integrations
132
+ setupEcosystem(process.cwd(), msg);
74
133
  } else {
75
134
  console.error(t(msg.DAEMON_START_FAILED, { path: logPath }));
76
135
  process.exit(1);