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
package/src/core/boast.ts CHANGED
@@ -1,265 +1,280 @@
1
- /**
2
- * Boastful Doctor — Gamification & Delightful Logging
3
- *
4
- * Lightweight value calculations that stay well under the 270ms budget.
5
- * All math is O(1) — no I/O, no async.
6
- * All strings are localized via the i18n dictionary.
7
- */
8
-
9
- import { getSystemLanguage } from "./locale";
10
- import type { SupportedLang } from "./locale";
11
- import { getMessages, t } from "./i18n/messages";
12
- import type { MessageDict } from "./i18n/messages";
13
-
14
- // ── Token & Cost Estimation ──
15
-
16
- /** Rough chars-per-token ratio for code (conservative estimate) */
17
- const CHARS_PER_TOKEN = 3.5;
18
-
19
- /** Average cost per 1K input tokens (Claude Sonnet ballpark) */
20
- const COST_PER_1K_TOKENS = 0.003;
21
-
22
- /** Estimated minutes a developer spends debugging a missing config */
23
- const DEBUG_MINUTES_BASE = 8;
24
- const DEBUG_MINUTES_PER_KB = 2;
25
-
26
- export interface HealMetrics {
27
- fileSize: number;
28
- healTimeMs: number;
29
- tokensSaved: number;
30
- minutesSaved: number;
31
- costSaved: number;
32
- }
33
-
34
- /** Calculate mock "value saved" from a heal event. O(1), no I/O. */
35
- export function calcHealMetrics(fileSize: number, healTimeMs: number): HealMetrics {
36
- const tokensSaved = Math.round(fileSize / CHARS_PER_TOKEN);
37
- const fileSizeKB = fileSize / 1024;
38
- const minutesSaved = Math.round(DEBUG_MINUTES_BASE + fileSizeKB * DEBUG_MINUTES_PER_KB);
39
- const costSaved = Math.round(tokensSaved / 1000 * COST_PER_1K_TOKENS * 100) / 100;
40
- return { fileSize, healTimeMs, tokensSaved, minutesSaved, costSaved };
41
- }
42
-
43
- export interface ShiftSummary {
44
- uptimeFormatted: string;
45
- totalEvents: number;
46
- healsPerformed: number;
47
- totalTokensSaved: number;
48
- totalMinutesSaved: number;
49
- totalCostSaved: number;
50
- suppressionsSkipped: number;
51
- dormantTransitions: number;
52
- boast: string;
53
- }
54
-
55
- /** Build a shift summary from aggregated daemon stats. */
56
- export function buildShiftSummary(stats: {
57
- uptimeSeconds: number;
58
- totalEvents: number;
59
- healsPerformed: number;
60
- totalFileBytesSaved: number;
61
- suppressionsSkipped: number;
62
- dormantTransitions: number;
63
- }, lang?: SupportedLang): ShiftSummary {
64
- const l = lang ?? getSystemLanguage();
65
- const msg = getMessages(l);
66
- const totalTokensSaved = Math.round(stats.totalFileBytesSaved / CHARS_PER_TOKEN);
67
- const totalMinutesSaved = stats.healsPerformed * DEBUG_MINUTES_BASE;
68
- const totalCostSaved = Math.round(totalTokensSaved / 1000 * COST_PER_1K_TOKENS * 100) / 100;
69
-
70
- return {
71
- uptimeFormatted: formatUptime(stats.uptimeSeconds),
72
- totalEvents: stats.totalEvents,
73
- healsPerformed: stats.healsPerformed,
74
- totalTokensSaved,
75
- totalMinutesSaved,
76
- totalCostSaved,
77
- suppressionsSkipped: stats.suppressionsSkipped,
78
- dormantTransitions: stats.dormantTransitions,
79
- boast: pick(msg.BOAST_SHIFT_END),
80
- };
81
- }
82
-
83
- // ── Boast Selection ──
84
-
85
- function msg(lang?: SupportedLang): MessageDict {
86
- return getMessages(lang ?? getSystemLanguage());
87
- }
88
-
89
- /** Pick a random heal boast. 1-in-N chance (anti-annoyance). */
90
- export function maybeHealBoast(triggerChance = 5, lang?: SupportedLang): string | null {
91
- if (Math.floor(Math.random() * triggerChance) !== 0) return null;
92
- const m = msg(lang);
93
- return pick(m.BOAST_HEAL);
94
- }
95
-
96
- /** Always returns a dormant boast (rare event, always worth noting). */
97
- export function dormantBoast(lang?: SupportedLang): string {
98
- return pick(msg(lang).BOAST_DORMANT);
99
- }
100
-
101
- /** Pick a random shift-end boast in the given locale. */
102
- export function localizedBoast(lang?: SupportedLang): string {
103
- return pick(msg(lang).BOAST_SHIFT_END);
104
- }
105
-
106
- /** Format a single heal log line with metrics. */
107
- export function formatHealLog(
108
- fileName: string,
109
- metrics: HealMetrics,
110
- boast: string | null,
111
- lang?: SupportedLang,
112
- ): string {
113
- const m = msg(lang);
114
- const vars = {
115
- fileName,
116
- ms: metrics.healTimeMs,
117
- tokens: metrics.tokensSaved,
118
- mins: metrics.minutesSaved,
119
- };
120
- const base = t(m.HEAL_LOG, vars);
121
- if (!boast) return base;
122
- const boastLine = t(boast, vars);
123
- return `${base}\n${m.BOAST_HEAL_PREFIX} ${boastLine}`;
124
- }
125
-
126
- /** Format dormant log line. */
127
- export function formatDormantLog(
128
- antibodyId: string,
129
- lang?: SupportedLang,
130
- ): string {
131
- const m = msg(lang);
132
- const boast = pick(m.BOAST_DORMANT);
133
- return t(m.DORMANT_LOG, { id: antibodyId, boast });
134
- }
135
-
136
- /** Format the full shift summary for terminal output. */
137
- export function formatShiftSummary(s: ShiftSummary, lang?: SupportedLang): string {
138
- const m = msg(lang);
139
- const lines = [
140
- "",
141
- "┌──────────────────────────────────────────────┐",
142
- pad(` ${m.SHIFT_TITLE}`),
143
- "├──────────────────────────────────────────────┤",
144
- padKV(m.SHIFT_ON_DUTY, s.uptimeFormatted),
145
- padKV(m.SHIFT_EVENTS, String(s.totalEvents)),
146
- padKV(m.SHIFT_HEALS, String(s.healsPerformed)),
147
- padKV(m.SHIFT_TOKENS, `~${fmtNum(s.totalTokensSaved)}`),
148
- padKV(m.SHIFT_TIME, `~${s.totalMinutesSaved} min`),
149
- padKV(m.SHIFT_COST, `~$${s.totalCostSaved.toFixed(2)}`),
150
- ];
151
-
152
- if (s.suppressionsSkipped > 0) {
153
- padKVPush(lines, m.SHIFT_SUPPRESSED, `${s.suppressionsSkipped} mass events`);
154
- }
155
- if (s.dormantTransitions > 0) {
156
- padKVPush(lines, m.SHIFT_RETIRED, `${s.dormantTransitions} antibodies`);
157
- }
158
-
159
- lines.push("├──────────────────────────────────────────────┤");
160
- // Override server-side boast with locale-appropriate one
161
- const localBoast = pick(m.BOAST_SHIFT_END);
162
- lines.push(pad(` ${localBoast}`));
163
- lines.push("└──────────────────────────────────────────────┘");
164
- lines.push("");
165
-
166
- return lines.join("\n");
167
- }
168
-
169
- /** Format value section for score command. */
170
- export function formatValueSection(s: ShiftSummary, lang?: SupportedLang): string[] {
171
- const m = msg(lang);
172
- const lines: string[] = [];
173
- lines.push(m.SCORE_VALUE_TITLE);
174
- lines.push(`${m.SHIFT_TOKENS}: ~${fmtNum(s.totalTokensSaved)}`);
175
- lines.push(`${m.SHIFT_TIME}: ~${s.totalMinutesSaved} min`);
176
- lines.push(`${m.SHIFT_COST}: ~$${s.totalCostSaved.toFixed(2)}`);
177
- return lines;
178
- }
179
-
180
- // ── Helpers ──
181
-
182
- function pick<T>(arr: T[]): T {
183
- return arr[Math.floor(Math.random() * arr.length)];
184
- }
185
-
186
- function formatUptime(seconds: number): string {
187
- if (seconds < 60) return `${seconds}s`;
188
- if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
189
- const h = Math.floor(seconds / 3600);
190
- const m = Math.floor((seconds % 3600) / 60);
191
- return `${h}h ${m}m`;
192
- }
193
-
194
- const W = 46;
195
-
196
- function pad(content: string): string {
197
- const visual = visualWidth(content);
198
- if (visual > W) {
199
- let len = 0;
200
- let cut = 0;
201
- for (const ch of content) {
202
- const cw = isWideChar(ch) ? 2 : 1;
203
- if (len + cw > W - 1) break;
204
- len += cw;
205
- cut += ch.length;
206
- }
207
- const trimmed = content.slice(0, cut) + "…";
208
- const trimVw = visualWidth(trimmed);
209
- return `│${trimmed}${" ".repeat(Math.max(0, W - trimVw))}│`;
210
- }
211
- return `│${content}${" ".repeat(Math.max(0, W - visual))}│`;
212
- }
213
-
214
- /** Pad a key-value row with aligned colon, visual-width-aware. */
215
- function padKV(key: string, value: string): string {
216
- const keyVw = visualWidth(key);
217
- const padSize = Math.max(0, 13 - keyVw);
218
- return pad(` ${key}${" ".repeat(padSize)}: ${value}`);
219
- }
220
-
221
- function padKVPush(lines: string[], key: string, value: string): void {
222
- lines.push(padKV(key, value));
223
- }
224
-
225
- export function visualWidth(s: string): number {
226
- let w = 0;
227
- for (const ch of s) {
228
- w += isWideChar(ch) ? 2 : 1;
229
- }
230
- return w;
231
- }
232
-
233
- function isWideChar(ch: string): boolean {
234
- const code = ch.codePointAt(0) ?? 0;
235
- return (
236
- // CJK Unified Ideographs
237
- (code >= 0x4E00 && code <= 0x9FFF) ||
238
- // CJK Extension A
239
- (code >= 0x3400 && code <= 0x4DBF) ||
240
- // Hangul Syllables
241
- (code >= 0xAC00 && code <= 0xD7AF) ||
242
- // Hangul Jamo
243
- (code >= 0x1100 && code <= 0x11FF) ||
244
- // Hangul Compatibility Jamo
245
- (code >= 0x3130 && code <= 0x318F) ||
246
- // CJK Compatibility
247
- (code >= 0x3300 && code <= 0x33FF) ||
248
- // Fullwidth Forms
249
- (code >= 0xFF01 && code <= 0xFF60) ||
250
- // Common emoji ranges
251
- (code >= 0x1F300 && code <= 0x1FBFF) ||
252
- (code >= 0x2600 && code <= 0x27BF) ||
253
- (code >= 0xFE00 && code <= 0xFE0F) ||
254
- (code >= 0x200D && code <= 0x200D) ||
255
- (code >= 0x231A && code <= 0x23FA) ||
256
- code === 0x2764 ||
257
- code === 0x2139
258
- );
259
- }
260
-
261
- export function fmtNum(n: number): string {
262
- if (n < 1000) return `${n}`;
263
- if (n < 1_000_000) return `${(n / 1000).toFixed(1)}K`;
264
- return `${(n / 1_000_000).toFixed(1)}M`;
265
- }
1
+ /**
2
+ * Boastful Doctor — Gamification & Delightful Logging
3
+ *
4
+ * O(1) math only no I/O, no async. All strings localized via i18n.
5
+ */
6
+
7
+ import { getSystemLanguage } from "./locale";
8
+ import type { SupportedLang } from "./locale";
9
+ import { getMessages, t } from "./i18n/messages";
10
+ import type { MessageDict } from "./i18n/messages";
11
+
12
+ // ── Token & Cost Estimation ──
13
+
14
+ const CHARS_PER_TOKEN = 3.5;
15
+ const COST_PER_1K_TOKENS = 0.003;
16
+ const DEBUG_MINUTES_BASE = 8;
17
+ const DEBUG_MINUTES_PER_KB = 2;
18
+
19
+ export interface HealMetrics {
20
+ fileSize: number;
21
+ healTimeMs: number;
22
+ tokensSaved: number;
23
+ minutesSaved: number;
24
+ costSaved: number;
25
+ }
26
+
27
+ /** Calculate mock "value saved" from a heal event. O(1), no I/O. */
28
+ export function calcHealMetrics(fileSize: number, healTimeMs: number): HealMetrics {
29
+ const tokensSaved = Math.round(fileSize / CHARS_PER_TOKEN);
30
+ const fileSizeKB = fileSize / 1024;
31
+ const minutesSaved = Math.round(DEBUG_MINUTES_BASE + fileSizeKB * DEBUG_MINUTES_PER_KB);
32
+ const costSaved = Math.round(tokensSaved / 1000 * COST_PER_1K_TOKENS * 100) / 100;
33
+ return { fileSize, healTimeMs, tokensSaved, minutesSaved, costSaved };
34
+ }
35
+
36
+ export interface ShiftSummary {
37
+ uptimeFormatted: string;
38
+ totalEvents: number;
39
+ healsPerformed: number;
40
+ totalTokensSaved: number;
41
+ totalMinutesSaved: number;
42
+ totalCostSaved: number;
43
+ suppressionsSkipped: number;
44
+ dormantTransitions: number;
45
+ boast: string;
46
+ // Unified ROI breakdown
47
+ healTokensSaved: number;
48
+ healCostSaved: number;
49
+ hologramTokensSaved: number;
50
+ hologramCostSaved: number;
51
+ }
52
+
53
+ /** Build a shift summary from aggregated daemon stats. */
54
+ export function buildShiftSummary(stats: {
55
+ uptimeSeconds: number;
56
+ totalEvents: number;
57
+ healsPerformed: number;
58
+ totalFileBytesSaved: number;
59
+ suppressionsSkipped: number;
60
+ dormantTransitions: number;
61
+ hologramSavedChars?: number;
62
+ }, lang?: SupportedLang): ShiftSummary {
63
+ const l = lang ?? getSystemLanguage();
64
+ const m = getMessages(l);
65
+
66
+ // Auto-Heal ROI
67
+ const healTokensSaved = Math.round(stats.totalFileBytesSaved / CHARS_PER_TOKEN);
68
+ const healCostSaved = Math.round(healTokensSaved / 1000 * COST_PER_1K_TOKENS * 100) / 100;
69
+
70
+ // Hologram ROI
71
+ const holoSavedChars = stats.hologramSavedChars ?? 0;
72
+ const hologramTokensSaved = Math.round(holoSavedChars / CHARS_PER_TOKEN);
73
+ const hologramCostSaved = Math.round(hologramTokensSaved / 1000 * COST_PER_1K_TOKENS * 100) / 100;
74
+
75
+ // Unified totals
76
+ const totalTokensSaved = healTokensSaved + hologramTokensSaved;
77
+ const totalMinutesSaved = stats.healsPerformed * DEBUG_MINUTES_BASE;
78
+ const totalCostSaved = Math.round((healCostSaved + hologramCostSaved) * 100) / 100;
79
+
80
+ return {
81
+ uptimeFormatted: formatUptime(stats.uptimeSeconds),
82
+ totalEvents: stats.totalEvents,
83
+ healsPerformed: stats.healsPerformed,
84
+ totalTokensSaved,
85
+ totalMinutesSaved,
86
+ totalCostSaved,
87
+ suppressionsSkipped: stats.suppressionsSkipped,
88
+ dormantTransitions: stats.dormantTransitions,
89
+ boast: pick(m.BOAST_SHIFT_END),
90
+ healTokensSaved,
91
+ healCostSaved,
92
+ hologramTokensSaved,
93
+ hologramCostSaved,
94
+ };
95
+ }
96
+
97
+ // ── Boast Selection ──
98
+
99
+ function msg(lang?: SupportedLang): MessageDict {
100
+ return getMessages(lang ?? getSystemLanguage());
101
+ }
102
+
103
+ /** Pick a random heal boast. 1-in-N chance (anti-annoyance). */
104
+ export function maybeHealBoast(triggerChance = 5, lang?: SupportedLang): string | null {
105
+ if (Math.floor(Math.random() * triggerChance) !== 0) return null;
106
+ const m = msg(lang);
107
+ return pick(m.BOAST_HEAL);
108
+ }
109
+
110
+ /** Always returns a dormant boast (rare event, always worth noting). */
111
+ export function dormantBoast(lang?: SupportedLang): string {
112
+ return pick(msg(lang).BOAST_DORMANT);
113
+ }
114
+
115
+ /** Pick a random shift-end boast in the given locale. */
116
+ export function localizedBoast(lang?: SupportedLang): string {
117
+ return pick(msg(lang).BOAST_SHIFT_END);
118
+ }
119
+
120
+ /** Format a single heal log line with metrics. */
121
+ export function formatHealLog(
122
+ fileName: string,
123
+ metrics: HealMetrics,
124
+ boast: string | null,
125
+ lang?: SupportedLang,
126
+ ): string {
127
+ const m = msg(lang);
128
+ const vars = {
129
+ fileName,
130
+ ms: metrics.healTimeMs,
131
+ tokens: metrics.tokensSaved,
132
+ mins: metrics.minutesSaved,
133
+ };
134
+ const base = t(m.HEAL_LOG, vars);
135
+ if (!boast) return base;
136
+ const boastLine = t(boast, vars);
137
+ return `${base}\n${m.BOAST_HEAL_PREFIX} ${boastLine}`;
138
+ }
139
+
140
+ /** Format dormant log line. */
141
+ export function formatDormantLog(
142
+ antibodyId: string,
143
+ lang?: SupportedLang,
144
+ ): string {
145
+ const m = msg(lang);
146
+ const boast = pick(m.BOAST_DORMANT);
147
+ return t(m.DORMANT_LOG, { id: antibodyId, boast });
148
+ }
149
+
150
+ /** Format the full shift summary for terminal output. */
151
+ export function formatShiftSummary(s: ShiftSummary, lang?: SupportedLang): string {
152
+ const m = msg(lang);
153
+ const lines = [
154
+ "",
155
+ "┌──────────────────────────────────────────────┐",
156
+ pad(` ${m.SHIFT_TITLE}`),
157
+ "├──────────────────────────────────────────────┤",
158
+ padKV(m.SHIFT_ON_DUTY, s.uptimeFormatted),
159
+ padKV(m.SHIFT_EVENTS, String(s.totalEvents)),
160
+ padKV(m.SHIFT_HEALS, String(s.healsPerformed)),
161
+ padKV(m.SHIFT_TOKENS, `~${fmtNum(s.totalTokensSaved)}`),
162
+ padKV(m.SHIFT_TIME, `~${s.totalMinutesSaved} min`),
163
+ padKV(m.SHIFT_COST, `~$${s.totalCostSaved.toFixed(2)}`),
164
+ ];
165
+
166
+ if (s.suppressionsSkipped > 0) {
167
+ padKVPush(lines, m.SHIFT_SUPPRESSED, `${s.suppressionsSkipped} mass events`);
168
+ }
169
+ if (s.dormantTransitions > 0) {
170
+ padKVPush(lines, m.SHIFT_RETIRED, `${s.dormantTransitions} antibodies`);
171
+ }
172
+
173
+ lines.push("├──────────────────────────────────────────────┤");
174
+ // Override server-side boast with locale-appropriate one
175
+ const localBoast = pick(m.BOAST_SHIFT_END);
176
+ lines.push(pad(` ${localBoast}`));
177
+ lines.push("└──────────────────────────────────────────────┘");
178
+ lines.push("");
179
+
180
+ return lines.join("\n");
181
+ }
182
+
183
+ /** Format value section for score command. */
184
+ export function formatValueSection(s: ShiftSummary, lang?: SupportedLang): string[] {
185
+ const m = msg(lang);
186
+ const lines: string[] = [];
187
+ lines.push(m.SCORE_VALUE_TITLE);
188
+ lines.push(`${m.SHIFT_TOKENS}: ~${fmtNum(s.totalTokensSaved)}`);
189
+ lines.push(`${m.SHIFT_TIME}: ~${s.totalMinutesSaved} min`);
190
+ lines.push(`${m.SHIFT_COST}: ~$${s.totalCostSaved.toFixed(2)}`);
191
+ return lines;
192
+ }
193
+
194
+ // ── Helpers ──
195
+
196
+ function pick<T>(arr: T[]): T {
197
+ if (arr.length === 0) return "" as unknown as T;
198
+ return arr[Math.floor(Math.random() * arr.length)];
199
+ }
200
+
201
+ function formatUptime(seconds: number): string {
202
+ if (seconds < 60) return `${seconds}s`;
203
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
204
+ const h = Math.floor(seconds / 3600);
205
+ const m = Math.floor((seconds % 3600) / 60);
206
+ return `${h}h ${m}m`;
207
+ }
208
+
209
+ const W = 46;
210
+
211
+ function pad(content: string): string {
212
+ const visual = visualWidth(content);
213
+ if (visual > W) {
214
+ let len = 0;
215
+ let cut = 0;
216
+ for (const ch of content) {
217
+ const cw = isWideChar(ch) ? 2 : 1;
218
+ if (len + cw > W - 1) break;
219
+ len += cw;
220
+ cut += ch.length;
221
+ }
222
+ const trimmed = content.slice(0, cut) + "…";
223
+ const trimVw = visualWidth(trimmed);
224
+ return `│${trimmed}${" ".repeat(Math.max(0, W - trimVw))}│`;
225
+ }
226
+ return `│${content}${" ".repeat(Math.max(0, W - visual))}│`;
227
+ }
228
+
229
+ /** Pad a key-value row with aligned colon, visual-width-aware. */
230
+ function padKV(key: string, value: string): string {
231
+ const keyVw = visualWidth(key);
232
+ const padSize = Math.max(0, 13 - keyVw);
233
+ return pad(` ${key}${" ".repeat(padSize)}: ${value}`);
234
+ }
235
+
236
+ function padKVPush(lines: string[], key: string, value: string): void {
237
+ lines.push(padKV(key, value));
238
+ }
239
+
240
+ export function visualWidth(s: string): number {
241
+ let w = 0;
242
+ for (const ch of s) {
243
+ w += isWideChar(ch) ? 2 : 1;
244
+ }
245
+ return w;
246
+ }
247
+
248
+ function isWideChar(ch: string): boolean {
249
+ const code = ch.codePointAt(0) ?? 0;
250
+ return (
251
+ // CJK Unified Ideographs
252
+ (code >= 0x4E00 && code <= 0x9FFF) ||
253
+ // CJK Extension A
254
+ (code >= 0x3400 && code <= 0x4DBF) ||
255
+ // Hangul Syllables
256
+ (code >= 0xAC00 && code <= 0xD7AF) ||
257
+ // Hangul Jamo
258
+ (code >= 0x1100 && code <= 0x11FF) ||
259
+ // Hangul Compatibility Jamo
260
+ (code >= 0x3130 && code <= 0x318F) ||
261
+ // CJK Compatibility
262
+ (code >= 0x3300 && code <= 0x33FF) ||
263
+ // Fullwidth Forms
264
+ (code >= 0xFF01 && code <= 0xFF60) ||
265
+ // Common emoji ranges
266
+ (code >= 0x1F300 && code <= 0x1FBFF) ||
267
+ (code >= 0x2600 && code <= 0x27BF) ||
268
+ (code >= 0xFE00 && code <= 0xFE0F) ||
269
+ (code >= 0x200D && code <= 0x200D) ||
270
+ (code >= 0x231A && code <= 0x23FA) ||
271
+ code === 0x2764 ||
272
+ code === 0x2139
273
+ );
274
+ }
275
+
276
+ export function fmtNum(n: number): string {
277
+ if (n < 1000) return `${n}`;
278
+ if (n < 1_000_000) return `${(n / 1000).toFixed(1)}K`;
279
+ return `${(n / 1_000_000).toFixed(1)}M`;
280
+ }
@@ -1,49 +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
- }
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
+ }