autonomous-flow-daemon 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Persistent user configuration — ~/.afdrc
3
+ *
4
+ * JSON file with user preferences. Created on first `afd lang` call.
5
+ * Read is sync and cached; write is sync (rare operation).
6
+ */
7
+
8
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
9
+ import { join, dirname } from "path";
10
+ import { homedir } from "os";
11
+
12
+ export interface AfdConfig {
13
+ lang?: string;
14
+ }
15
+
16
+ const RC_PATH = join(homedir(), ".afdrc");
17
+
18
+ let configCache: AfdConfig | null = null;
19
+
20
+ /** Read ~/.afdrc. Returns empty object if missing or invalid. */
21
+ export function readConfig(): AfdConfig {
22
+ if (configCache) return configCache;
23
+ if (!existsSync(RC_PATH)) {
24
+ configCache = {};
25
+ return configCache;
26
+ }
27
+ try {
28
+ configCache = JSON.parse(readFileSync(RC_PATH, "utf-8"));
29
+ return configCache!;
30
+ } catch {
31
+ configCache = {};
32
+ return configCache;
33
+ }
34
+ }
35
+
36
+ /** Write a key to ~/.afdrc. Merges with existing config. */
37
+ export function writeConfig(partial: Partial<AfdConfig>): AfdConfig {
38
+ const current = readConfig();
39
+ const merged = { ...current, ...partial };
40
+ mkdirSync(dirname(RC_PATH), { recursive: true });
41
+ writeFileSync(RC_PATH, JSON.stringify(merged, null, 2) + "\n", "utf-8");
42
+ configCache = merged;
43
+ return merged;
44
+ }
45
+
46
+ /** Get the RC file path (for display). */
47
+ export function getConfigPath(): string {
48
+ return RC_PATH;
49
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Smart Discovery — scans project root for AI-agent config patterns.
3
+ *
4
+ * Runs ONCE at startup. O(n) existsSync calls on a known candidate list.
5
+ * No directory traversal, no glob — just fast stat checks (< 5ms).
6
+ */
7
+
8
+ import { existsSync } from "fs";
9
+
10
+ /** All known AI-agent config patterns to probe. */
11
+ const DISCOVERY_CANDIDATES = [
12
+ // Claude Code ecosystem
13
+ ".claude/",
14
+ "CLAUDE.md",
15
+ ".claudeignore",
16
+ // Cursor ecosystem
17
+ ".cursorrules",
18
+ ".cursorignore",
19
+ // Git essentials
20
+ ".gitignore",
21
+ // MCP configs
22
+ "mcp-config.json",
23
+ ".mcp.json",
24
+ // Generic AI config directories
25
+ ".ai/",
26
+ // Custom rules (various tools)
27
+ ".customrules",
28
+ ".windsurfrules",
29
+ // Copilot
30
+ ".github/copilot-instructions.md",
31
+ ] as const;
32
+
33
+ export interface DiscoveryResult {
34
+ /** All targets that exist on disk (merged with defaults, deduplicated). */
35
+ targets: string[];
36
+ /** How many were found via smart discovery (beyond the hardcoded defaults). */
37
+ discoveredCount: number;
38
+ /** Elapsed time in ms. */
39
+ elapsedMs: number;
40
+ }
41
+
42
+ /**
43
+ * Discover AI-context files in the project root.
44
+ * Merges found targets with the provided defaults, deduplicates, and returns.
45
+ */
46
+ export function discoverWatchTargets(defaults: readonly string[]): DiscoveryResult {
47
+ const t0 = performance.now();
48
+ const seen = new Set<string>(defaults);
49
+ let discoveredCount = 0;
50
+
51
+ for (const candidate of DISCOVERY_CANDIDATES) {
52
+ if (seen.has(candidate)) continue;
53
+ if (existsSync(candidate)) {
54
+ seen.add(candidate);
55
+ discoveredCount++;
56
+ }
57
+ }
58
+
59
+ const elapsedMs = Math.round((performance.now() - t0) * 100) / 100;
60
+ return {
61
+ targets: [...seen],
62
+ discoveredCount,
63
+ elapsedMs,
64
+ };
65
+ }
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Bilingual Dictionary — "Boastful Doctor" persona
3
+ *
4
+ * Keys use template literals with {placeholders}.
5
+ * All arrays are variant pools — a random entry is picked at runtime.
6
+ */
7
+
8
+ import type { SupportedLang } from "../locale";
9
+
10
+ export interface MessageDict {
11
+ // ── Heal event ──
12
+ HEAL_LOG: string; // "{fileName}" "{ms}" "{tokens}" "{mins}"
13
+ BOAST_HEAL: string[]; // witty one-liners after heal (1-in-5 chance)
14
+ BOAST_HEAL_PREFIX: string; // e.g. "[afd] 🗣️"
15
+
16
+ // ── Dormant transition ──
17
+ BOAST_DORMANT: string[];
18
+ DORMANT_LOG: string; // "{id}" "{boast}"
19
+
20
+ // ── Shift summary box ──
21
+ SHIFT_TITLE: string;
22
+ SHIFT_ON_DUTY: string;
23
+ SHIFT_EVENTS: string;
24
+ SHIFT_HEALS: string;
25
+ SHIFT_TOKENS: string;
26
+ SHIFT_TIME: string;
27
+ SHIFT_COST: string;
28
+ SHIFT_SUPPRESSED: string;
29
+ SHIFT_RETIRED: string;
30
+ BOAST_SHIFT_END: string[];
31
+
32
+ // ── Score dashboard ──
33
+ SCORE_TITLE: string;
34
+ SCORE_ECOSYSTEM: string;
35
+ SCORE_ALSO_FOUND: string;
36
+ SCORE_UPTIME: string;
37
+ SCORE_EVENTS: string;
38
+ SCORE_FILES_FOUND: string;
39
+ SCORE_ACTIVITY: string;
40
+ SCORE_HOLOGRAM_TITLE: string;
41
+ SCORE_HOLOGRAM_REQUESTS: string;
42
+ SCORE_HOLOGRAM_ORIGINAL: string;
43
+ SCORE_HOLOGRAM_COMPRESSED: string;
44
+ SCORE_HOLOGRAM_SAVED: string;
45
+ SCORE_HOLOGRAM_EFFICIENCY: string;
46
+ SCORE_HOLOGRAM_EMPTY: string;
47
+ SCORE_HOLOGRAM_HINT: string;
48
+ SCORE_IMMUNE_TITLE: string;
49
+ SCORE_ANTIBODIES: string;
50
+ SCORE_LEVEL: string;
51
+ SCORE_IMMUNITY: string;
52
+ SCORE_AUTO_HEALED_LABEL: string;
53
+ SCORE_AUTO_HEALED: string; // "{count}" "{s}"
54
+ SCORE_LAST_HEAL: string; // "{id}" "{ago}"
55
+ SCORE_WATCHED_FILES: string;
56
+ SCORE_NO_FILES: string;
57
+ SCORE_LAST_EVENT: string;
58
+ SCORE_AGO: string; // "{time}"
59
+ SCORE_VALUE_TITLE: string;
60
+ SCORE_IMMUNE_VULNERABLE: string;
61
+ SCORE_IMMUNE_LEARNING: string;
62
+ SCORE_IMMUNE_GUARDED: string;
63
+ SCORE_IMMUNE_FORTIFIED: string;
64
+
65
+ // ── Lang command ──
66
+ LANG_CURRENT: string; // "{lang}"
67
+ LANG_CHANGED: string; // "{lang}"
68
+ LANG_SAVED: string; // "{path}"
69
+ LANG_LIST_TITLE: string;
70
+ LANG_INVALID: string; // "{lang}" "{supported}"
71
+
72
+ // ── CLI messages ──
73
+ DAEMON_ALREADY_RUNNING: string;
74
+ DAEMON_STARTED: string; // "{pid}" "{port}"
75
+ DAEMON_WATCHING: string; // "{count}" "{targets}"
76
+ DAEMON_LOGS: string; // "{path}"
77
+ DAEMON_STOPPED: string; // "{pid}"
78
+ DAEMON_KILLED: string; // "{pid}"
79
+ DAEMON_NOT_RUNNING: string;
80
+ DAEMON_NOT_RESPONDING: string;
81
+ DAEMON_START_FAILED: string; // "{path}"
82
+ }
83
+
84
+ const en: MessageDict = {
85
+ HEAL_LOG: "[afd] 🩹 Healed {fileName} in {ms}ms | 📉 Saved ~{tokens} tokens & {mins} mins of debugging",
86
+ BOAST_HEAL: [
87
+ "Dodged a bullet there! Restored in {ms}ms. You owe me a coffee ☕",
88
+ "Claude tried to delete a critical config. I said 'Not today.' 🛡️",
89
+ "Another mutation neutralized. The flow remains immortal. 💉",
90
+ "File vanished. I brought it back before you even blinked. 👁️",
91
+ "That deletion looked suspicious. Good thing I was on shift. 🔬",
92
+ "Patched faster than you can say 'git checkout'. No charge. 🩺",
93
+ "A lesser daemon would have let that one slide. Not me. 🦠→🛡️",
94
+ "Config restored. Your AI agent will never know it was gone. 🤫",
95
+ "Intercepted a fatal mutation mid-flight. Routine procedure. ✂️",
96
+ "The immune system holds. Another day, another heal. 💪",
97
+ ],
98
+ BOAST_HEAL_PREFIX: "[afd] 🗣️",
99
+ BOAST_DORMANT: [
100
+ "You deleted that twice? Fine, I'll respect your wishes, doctor. 🫡",
101
+ "Double-tap detected. Standing down — your call, chief. 🤝",
102
+ "I know when I'm not wanted. Antibody retired gracefully. 😌",
103
+ ],
104
+ DORMANT_LOG: "[afd] 🫡 Antibody {id} retired. {boast}",
105
+ SHIFT_TITLE: "🏥 afd Shift Summary",
106
+ SHIFT_ON_DUTY: "On duty",
107
+ SHIFT_EVENTS: "Events",
108
+ SHIFT_HEALS: "Heals",
109
+ SHIFT_TOKENS: "Tokens saved",
110
+ SHIFT_TIME: "Time saved",
111
+ SHIFT_COST: "Cost saved",
112
+ SHIFT_SUPPRESSED: "Suppressed",
113
+ SHIFT_RETIRED: "Retired",
114
+ BOAST_SHIFT_END: [
115
+ "Another shift complete. The flow is stronger than ever. 💎",
116
+ "Signing off. Your configs are safe... for now. 🌙",
117
+ "Shift ended. Not a single mutation got past me. Well, almost. 😏",
118
+ "Clocking out. Remember: I never sleep, I just pause. ⏸️",
119
+ "End of watch. Zero casualties on my side. 🏥",
120
+ ],
121
+ SCORE_TITLE: "afd score — Daemon Diagnostics",
122
+ SCORE_ECOSYSTEM: "Ecosystem",
123
+ SCORE_ALSO_FOUND: "Also found",
124
+ SCORE_UPTIME: "Uptime",
125
+ SCORE_EVENTS: "Events",
126
+ SCORE_FILES_FOUND: "Files Found",
127
+ SCORE_ACTIVITY: "Activity",
128
+ SCORE_HOLOGRAM_TITLE: "Context Efficiency (Hologram)",
129
+ SCORE_HOLOGRAM_REQUESTS: "Requests",
130
+ SCORE_HOLOGRAM_ORIGINAL: "Original",
131
+ SCORE_HOLOGRAM_COMPRESSED: "Hologram",
132
+ SCORE_HOLOGRAM_SAVED: "Saved",
133
+ SCORE_HOLOGRAM_EFFICIENCY: "Efficiency",
134
+ SCORE_HOLOGRAM_EMPTY: "No hologram requests yet.",
135
+ SCORE_HOLOGRAM_HINT: "Use: GET /hologram?file=<path>",
136
+ SCORE_IMMUNE_TITLE: "Immune System",
137
+ SCORE_ANTIBODIES: "Antibodies",
138
+ SCORE_LEVEL: "Level",
139
+ SCORE_IMMUNITY: "Immunity",
140
+ SCORE_AUTO_HEALED_LABEL: "Auto-healed",
141
+ SCORE_AUTO_HEALED: "{count} background event{s}",
142
+ SCORE_LAST_HEAL: "{id} ({ago} ago)",
143
+ SCORE_WATCHED_FILES: "Watched Files:",
144
+ SCORE_NO_FILES: "No files detected yet.",
145
+ SCORE_LAST_EVENT: "Last",
146
+ SCORE_AGO: "{time} ago",
147
+ SCORE_VALUE_TITLE: "\uD83D\uDCC8 Value Delivered",
148
+ SCORE_IMMUNE_VULNERABLE: "Vulnerable",
149
+ SCORE_IMMUNE_LEARNING: "Learning",
150
+ SCORE_IMMUNE_GUARDED: "Guarded",
151
+ SCORE_IMMUNE_FORTIFIED: "Fortified",
152
+ LANG_CURRENT: "[afd] Current language: {lang}",
153
+ LANG_CHANGED: "[afd] Language changed to: {lang}",
154
+ LANG_SAVED: "[afd] Saved to {path}",
155
+ LANG_LIST_TITLE: "[afd] Supported languages:",
156
+ LANG_INVALID: "[afd] Unknown language '{lang}'. Supported: {supported}",
157
+ DAEMON_ALREADY_RUNNING: "\uD83D\uDEE1\uFE0F afd daemon is already running",
158
+ DAEMON_STARTED: "[afd] 🛡️ Daemon started (pid={pid}, port={port})",
159
+ DAEMON_WATCHING: "[afd] 🛡️ Smart Discovery: Watching {count} AI-context targets",
160
+ DAEMON_LOGS: "[afd] Logs: {path}",
161
+ DAEMON_STOPPED: "[afd] Daemon stopped (pid={pid})",
162
+ DAEMON_KILLED: "[afd] Daemon killed (pid={pid})",
163
+ DAEMON_NOT_RUNNING: "[afd] No daemon running.",
164
+ DAEMON_NOT_RESPONDING: "[afd] Daemon not responding. Cleaning up stale PID files.",
165
+ DAEMON_START_FAILED: "[afd] Failed to start daemon. Check logs: {path}",
166
+ };
167
+
168
+ const ko: MessageDict = {
169
+ HEAL_LOG: "[afd] 🩹 {fileName} 살려냈습니다 ({ms}ms) | 📉 토큰 ~{tokens}개 & {mins}분 아꼈네요",
170
+ BOAST_HEAL: [
171
+ "위험할 뻔했네요! {ms}ms 만에 복구 완료. 커피 한 잔 사세요 ☕",
172
+ "클로드가 핵심 설정을 지우려길래 제가 컷했습니다 🛡️",
173
+ "변이 한 놈 더 잡았습니다. 코딩 흐름 끊기지 않게 💉",
174
+ "파일이 삭제될 뻔했지만 눈 깜짝할 새 되돌려놨어요 👁️",
175
+ "수상한 움직임을 감지했습니다. 제가 당직이라 다행인 줄 아세요 🔬",
176
+ "git checkout 치기도 전에 고쳐놨습니다. 이건 서비스예요 🩺",
177
+ "일반 데몬이었으면 멍 때렸겠지만, 전 아니죠 🦠→🛡️",
178
+ "설정 복구 완료. AI 에이전트는 감쪽같이 모를 거예요 🤫",
179
+ "심각한 변이를 비행 중에 요격했습니다. 늘 있는 일이죠 ✂️",
180
+ "면역 체계 이상 무. 오늘도 한 건 해결했습니다 💪",
181
+ ],
182
+ BOAST_HEAL_PREFIX: "[afd] 🗣️",
183
+ BOAST_DORMANT: [
184
+ "두 번이나 지우시겠다고요? 알겠습니다, 뜻대로 하세요 🫡",
185
+ "더블 클릭 감지. 이번엔 원하시는 대로 물러나 드릴게요 🤝",
186
+ "필요 없으시다면야... 항체 우아하게 퇴장합니다 😌",
187
+ ],
188
+ DORMANT_LOG: "[afd] 🫡 항체 {id} 은퇴. {boast}",
189
+ SHIFT_TITLE: "🏥 afd 오늘의 근무 리포트",
190
+ SHIFT_ON_DUTY: "근무 시간",
191
+ SHIFT_EVENTS: "발생 이벤트",
192
+ SHIFT_HEALS: "치료 횟수",
193
+ SHIFT_TOKENS: "절약한 토큰",
194
+ SHIFT_TIME: "아낀 시간",
195
+ SHIFT_COST: "절감 비용",
196
+ SHIFT_SUPPRESSED: "억제됨",
197
+ SHIFT_RETIRED: "은퇴함",
198
+ BOAST_SHIFT_END: [
199
+ "오늘 근무 끝. 덕분에 프로젝트가 더 튼튼해졌네요 💎",
200
+ "퇴근합니다. 설정 파일들은 안전해요... 아직은요 🌙",
201
+ "근무 종료. 단 하나의 변이도 놓치지 않았습니다. (아마도요) 😏",
202
+ "퇴근할게요. 전 자는 게 아니라 잠시 멈추는 겁니다 ⏸️",
203
+ "당직 종료. 제 구역 사상자는 없습니다 🏥",
204
+ ],
205
+ SCORE_TITLE: "afd score — 프로젝트 건강 검진",
206
+ SCORE_ECOSYSTEM: "에코시스템",
207
+ SCORE_ALSO_FOUND: "추가 감지",
208
+ SCORE_UPTIME: "가동 시간",
209
+ SCORE_EVENTS: "이벤트",
210
+ SCORE_FILES_FOUND: "감지된 파일",
211
+ SCORE_ACTIVITY: "활동량",
212
+ SCORE_HOLOGRAM_TITLE: "컨텍스트 효율 (Hologram)",
213
+ SCORE_HOLOGRAM_REQUESTS: "요청 수",
214
+ SCORE_HOLOGRAM_ORIGINAL: "원본 크기",
215
+ SCORE_HOLOGRAM_COMPRESSED: "홀로그램",
216
+ SCORE_HOLOGRAM_SAVED: "절약됨",
217
+ SCORE_HOLOGRAM_EFFICIENCY: "압축 효율",
218
+ SCORE_HOLOGRAM_EMPTY: "아직 홀로그램 요청이 없습니다.",
219
+ SCORE_HOLOGRAM_HINT: "사용법: GET /hologram?file=<경로>",
220
+ SCORE_IMMUNE_TITLE: "면역 시스템",
221
+ SCORE_ANTIBODIES: "항체 수",
222
+ SCORE_LEVEL: "방어 레벨",
223
+ SCORE_IMMUNITY: "면역력",
224
+ SCORE_AUTO_HEALED_LABEL: "자동 치유",
225
+ SCORE_AUTO_HEALED: "{count}건 백그라운드 치유됨",
226
+ SCORE_LAST_HEAL: "{id} ({ago} 전)",
227
+ SCORE_WATCHED_FILES: "감시 중인 파일:",
228
+ SCORE_NO_FILES: "아직 감지된 파일이 없습니다.",
229
+ SCORE_LAST_EVENT: "최근 기록",
230
+ SCORE_AGO: "{time} 전",
231
+ SCORE_VALUE_TITLE: "📈 전달된 가치",
232
+ SCORE_IMMUNE_VULNERABLE: "취약",
233
+ SCORE_IMMUNE_LEARNING: "학습 중",
234
+ SCORE_IMMUNE_GUARDED: "경계 중",
235
+ SCORE_IMMUNE_FORTIFIED: "철통 방어",
236
+ LANG_CURRENT: "[afd] 현재 언어: {lang}",
237
+ LANG_CHANGED: "[afd] 언어가 변경되었습니다: {lang}",
238
+ LANG_SAVED: "[afd] 저장 완료 → {path}",
239
+ LANG_LIST_TITLE: "[afd] 지원하는 언어:",
240
+ LANG_INVALID: "[afd] '{lang}' 은(는) 알 수 없는 언어예요. 지원: {supported}",
241
+ DAEMON_ALREADY_RUNNING: "🛡️ afd 데몬이 이미 열심히 일하고 있습니다",
242
+ DAEMON_STARTED: "[afd] 🛡️ 데몬 시작 (pid={pid}, port={port})",
243
+ DAEMON_WATCHING: "[afd] 🛡️ 스마트 탐색 중: AI 컨텍스트 대상 {count}개 감시 시작",
244
+ DAEMON_LOGS: "[afd] 로그 위치: {path}",
245
+ DAEMON_STOPPED: "[afd] 데몬이 중지되었습니다 (pid={pid})",
246
+ DAEMON_KILLED: "[afd] 데몬 강제 종료 완료 (pid={pid})",
247
+ DAEMON_NOT_RUNNING: "[afd] 실행 중인 데몬을 찾을 수 없습니다.",
248
+ DAEMON_NOT_RESPONDING: "[afd] 데몬이 응답하지 않네요. 남은 PID 파일을 정리합니다.",
249
+ DAEMON_START_FAILED: "[afd] 데몬 시작 실패. 로그를 확인해 보세요: {path}",
250
+ };
251
+
252
+ const dictionaries: Record<SupportedLang, MessageDict> = { en, ko };
253
+
254
+ /** Get the full dictionary for a language. */
255
+ export function getMessages(lang: SupportedLang): MessageDict {
256
+ return dictionaries[lang];
257
+ }
258
+
259
+ /** Template interpolation: replaces {key} with values. */
260
+ export function t(template: string, vars: Record<string, string | number> = {}): string {
261
+ let result = template;
262
+ for (const [key, val] of Object.entries(vars)) {
263
+ result = result.replaceAll(`{${key}}`, String(val));
264
+ }
265
+ return result;
266
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * OS Locale Detection — cached per-process.
3
+ *
4
+ * Priority:
5
+ * 0. ~/.afdrc config file (persistent user preference via `afd lang`)
6
+ * 1. AFD_LANG env (explicit override per-session)
7
+ * 2. LC_ALL / LANG / LANGUAGE env (user shell config, skip "C"/"POSIX")
8
+ * 3. macOS AppleLocale (system preferences — solves LANG=C.UTF-8 on Korean macOS)
9
+ * 4. Intl API (runtime default)
10
+ * 5. Default: "ko"
11
+ */
12
+
13
+ import { execSync } from "child_process";
14
+ import { readConfig } from "./config";
15
+
16
+ export type SupportedLang = "en" | "ko";
17
+
18
+ const SUPPORTED: SupportedLang[] = ["en", "ko"];
19
+
20
+ let cached: SupportedLang | null = null;
21
+
22
+ function isSupported(value: string): value is SupportedLang {
23
+ return SUPPORTED.includes(value as SupportedLang);
24
+ }
25
+
26
+ function matchKo(value: string): boolean {
27
+ return value.startsWith("ko");
28
+ }
29
+
30
+ /** Detect system language. Returns 'ko' or 'en'. */
31
+ export function getSystemLanguage(): SupportedLang {
32
+ if (cached) return cached;
33
+
34
+ // 0. Persistent config (~/.afdrc)
35
+ const rc = readConfig();
36
+ if (rc.lang && isSupported(rc.lang)) {
37
+ cached = rc.lang as SupportedLang;
38
+ return cached;
39
+ }
40
+
41
+ // 1. Explicit env override
42
+ const afdLang = process.env.AFD_LANG ?? "";
43
+ if (isSupported(afdLang)) { cached = afdLang; return cached; }
44
+ if (matchKo(afdLang)) { cached = "ko"; return cached; }
45
+
46
+ // 2. Standard env — skip generic "C" / "POSIX"
47
+ const envLang = process.env.LC_ALL || process.env.LANG || process.env.LANGUAGE || "";
48
+ if (envLang !== "" && !envLang.startsWith("C") && !envLang.startsWith("POSIX")) {
49
+ if (matchKo(envLang)) { cached = "ko"; return cached; }
50
+ cached = "en";
51
+ return cached;
52
+ }
53
+
54
+ // 3. macOS: AppleLocale
55
+ if (process.platform === "darwin") {
56
+ try {
57
+ const appleLocale = execSync("defaults read -g AppleLocale", {
58
+ encoding: "utf-8",
59
+ timeout: 500,
60
+ }).trim();
61
+ if (matchKo(appleLocale)) { cached = "ko"; return cached; }
62
+ } catch {
63
+ // Not macOS or defaults unavailable
64
+ }
65
+ }
66
+
67
+ // 4. Intl API fallback
68
+ try {
69
+ const intlLocale = Intl.DateTimeFormat().resolvedOptions().locale;
70
+ if (matchKo(intlLocale)) { cached = "ko"; return cached; }
71
+ } catch {
72
+ // Fallback to default
73
+ }
74
+
75
+ // 5. Default
76
+ cached = "ko";
77
+ return cached;
78
+ }
79
+
80
+ /** Get list of supported languages. */
81
+ export function getSupportedLanguages(): SupportedLang[] {
82
+ return [...SUPPORTED];
83
+ }
84
+
85
+ /** Override locale (for testing or after `afd lang` write). */
86
+ export function setLanguageOverride(lang: SupportedLang | null): void {
87
+ cached = lang;
88
+ }
@@ -1,14 +1,32 @@
1
1
  import { spawn } from "child_process";
2
+ import { IS_WINDOWS, IS_MACOS } from "../platform";
2
3
 
3
4
  /**
4
- * Fire an OS-native toast notification (Windows 10+).
5
+ * Fire an OS-native toast notification.
5
6
  * Runs asynchronously, never blocks, silently ignores all errors.
7
+ *
8
+ * - Windows 10+: PowerShell BalloonTip
9
+ * - macOS: osascript display notification
10
+ * - Linux: notify-send (libnotify)
6
11
  */
7
12
  export function notifyAutoHeal(patternId: string): void {
8
13
  const title = "\u{1F6E1}\uFE0F afd Auto-Healed";
9
14
  const body = `Silently fixed: ${patternId}`;
10
15
 
11
- // PowerShell BalloonTip — fire and forget
16
+ try {
17
+ if (IS_WINDOWS) {
18
+ notifyWindows(title, body);
19
+ } else if (IS_MACOS) {
20
+ notifyMacOS(title, body);
21
+ } else {
22
+ notifyLinux(title, body);
23
+ }
24
+ } catch {
25
+ // Crash-only: silently ignore notification failures
26
+ }
27
+ }
28
+
29
+ function notifyWindows(title: string, body: string): void {
12
30
  const ps = `
13
31
  Add-Type -AssemblyName System.Windows.Forms
14
32
  $n = New-Object System.Windows.Forms.NotifyIcon
@@ -22,14 +40,27 @@ export function notifyAutoHeal(patternId: string): void {
22
40
  $n.Dispose()
23
41
  `.replace(/\n\s*/g, " ");
24
42
 
25
- try {
26
- const child = spawn("powershell", ["-NoProfile", "-NonInteractive", "-Command", ps], {
27
- detached: true,
28
- stdio: "ignore",
29
- windowsHide: true,
30
- });
31
- child.unref();
32
- } catch {
33
- // Crash-only: silently ignore notification failures
34
- }
43
+ const child = spawn("powershell", ["-NoProfile", "-NonInteractive", "-Command", ps], {
44
+ detached: true,
45
+ stdio: "ignore",
46
+ windowsHide: true,
47
+ });
48
+ child.unref();
49
+ }
50
+
51
+ function notifyMacOS(title: string, body: string): void {
52
+ const script = `display notification "${body}" with title "${title}"`;
53
+ const child = spawn("osascript", ["-e", script], {
54
+ detached: true,
55
+ stdio: "ignore",
56
+ });
57
+ child.unref();
58
+ }
59
+
60
+ function notifyLinux(title: string, body: string): void {
61
+ const child = spawn("notify-send", [title, body, "--icon=dialog-information"], {
62
+ detached: true,
63
+ stdio: "ignore",
64
+ });
65
+ child.unref();
35
66
  }