autonomous-flow-daemon 1.6.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.
- package/CHANGELOG.md +85 -85
- package/LICENSE +21 -21
- package/README-ko.md +282 -0
- package/README.md +282 -266
- package/mcp-config.json +10 -10
- package/package.json +4 -2
- package/src/adapters/index.ts +370 -370
- package/src/cli.ts +162 -127
- package/src/commands/benchmark.ts +187 -187
- package/src/commands/correlate.ts +180 -0
- package/src/commands/dashboard.ts +404 -0
- package/src/commands/evolution.ts +84 -1
- package/src/commands/fix.ts +158 -158
- package/src/commands/lang.ts +41 -41
- package/src/commands/plugin.ts +110 -0
- package/src/commands/restart.ts +14 -14
- package/src/commands/score.ts +276 -276
- package/src/commands/start.ts +155 -155
- package/src/commands/status.ts +157 -157
- package/src/commands/stop.ts +68 -68
- package/src/commands/suggest.ts +211 -0
- package/src/commands/sync.ts +329 -16
- package/src/constants.ts +32 -32
- package/src/core/boast.ts +280 -280
- package/src/core/config.ts +49 -49
- package/src/core/correlation-engine.ts +265 -0
- package/src/core/db.ts +145 -117
- package/src/core/discovery.ts +65 -65
- package/src/core/federation.ts +129 -0
- package/src/core/hologram/engine.ts +71 -71
- package/src/core/hologram/fallback.ts +11 -11
- package/src/core/hologram/go-extractor.ts +203 -0
- package/src/core/hologram/incremental.ts +227 -227
- package/src/core/hologram/py-extractor.ts +132 -132
- package/src/core/hologram/rust-extractor.ts +244 -0
- package/src/core/hologram/ts-extractor.ts +406 -320
- package/src/core/hologram/types.ts +27 -25
- package/src/core/hologram.ts +73 -71
- package/src/core/i18n/messages.ts +309 -309
- package/src/core/locale.ts +88 -88
- package/src/core/log-rotate.ts +33 -33
- package/src/core/log-utils.ts +38 -38
- package/src/core/lru-map.ts +61 -61
- package/src/core/notify.ts +74 -74
- package/src/core/plugin-manager.ts +225 -0
- package/src/core/rule-suggestion.ts +127 -0
- package/src/core/validator-generator.ts +224 -0
- package/src/core/workspace.ts +28 -28
- package/src/daemon/client.ts +78 -65
- package/src/daemon/event-batcher.ts +108 -108
- package/src/daemon/guards.ts +13 -13
- package/src/daemon/http-routes.ts +376 -293
- package/src/daemon/mcp-handler.ts +575 -270
- package/src/daemon/mcp-subscriptions.ts +81 -0
- package/src/daemon/mesh.ts +51 -0
- package/src/daemon/server.ts +655 -590
- package/src/daemon/types.ts +121 -100
- package/src/daemon/workspace-map.ts +104 -92
- package/src/platform.ts +60 -60
- package/src/version.ts +15 -15
- package/README.ko.md +0 -266
package/src/core/boast.ts
CHANGED
|
@@ -1,280 +1,280 @@
|
|
|
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
|
+
/**
|
|
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
|
+
}
|
package/src/core/config.ts
CHANGED
|
@@ -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
|
+
}
|