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.
- package/CHANGELOG.md +85 -46
- package/LICENSE +21 -21
- package/README-ko.md +282 -0
- package/README.md +282 -337
- package/mcp-config.json +10 -10
- package/package.json +14 -6
- package/src/adapters/index.ts +370 -159
- package/src/cli.ts +162 -57
- package/src/commands/benchmark.ts +187 -0
- package/src/commands/correlate.ts +180 -0
- package/src/commands/dashboard.ts +404 -0
- package/src/commands/diagnose.ts +56 -14
- package/src/commands/doctor.ts +243 -0
- package/src/commands/evolution.ts +190 -0
- package/src/commands/fix.ts +158 -138
- package/src/commands/hooks.ts +136 -0
- package/src/commands/lang.ts +41 -41
- package/src/commands/mcp.ts +129 -0
- package/src/commands/plugin.ts +110 -0
- package/src/commands/restart.ts +14 -0
- package/src/commands/score.ts +276 -208
- package/src/commands/start.ts +155 -96
- package/src/commands/stats.ts +103 -0
- package/src/commands/status.ts +157 -0
- package/src/commands/stop.ts +68 -49
- package/src/commands/suggest.ts +211 -0
- package/src/commands/sync.ts +567 -21
- package/src/commands/vaccine.ts +177 -0
- package/src/constants.ts +32 -8
- package/src/core/boast.ts +280 -265
- package/src/core/config.ts +49 -49
- package/src/core/correlation-engine.ts +265 -0
- package/src/core/db.ts +145 -46
- package/src/core/discovery.ts +65 -65
- package/src/core/evolution.ts +215 -0
- package/src/core/federation.ts +129 -0
- package/src/core/hologram/engine.ts +71 -0
- package/src/core/hologram/fallback.ts +11 -0
- package/src/core/hologram/go-extractor.ts +203 -0
- package/src/core/hologram/incremental.ts +227 -0
- package/src/core/hologram/py-extractor.ts +132 -0
- package/src/core/hologram/rust-extractor.ts +244 -0
- package/src/core/hologram/ts-extractor.ts +406 -0
- package/src/core/hologram/types.ts +27 -0
- package/src/core/hologram.ts +73 -243
- package/src/core/hook-manager.ts +259 -0
- package/src/core/i18n/messages.ts +309 -266
- package/src/core/immune.ts +8 -123
- package/src/core/locale.ts +88 -88
- package/src/core/log-rotate.ts +33 -0
- package/src/core/log-utils.ts +38 -0
- package/src/core/lru-map.ts +61 -0
- package/src/core/notify.ts +74 -66
- package/src/core/plugin-manager.ts +225 -0
- package/src/core/rule-engine.ts +287 -0
- package/src/core/rule-suggestion.ts +127 -0
- package/src/core/semantic-diff.ts +432 -0
- package/src/core/telemetry.ts +94 -0
- package/src/core/vaccine-registry.ts +212 -0
- package/src/core/validator-generator.ts +224 -0
- package/src/core/workspace.ts +28 -0
- package/src/core/yaml-minimal.ts +176 -0
- package/src/daemon/client.ts +78 -37
- package/src/daemon/event-batcher.ts +108 -0
- package/src/daemon/guards.ts +13 -0
- package/src/daemon/http-routes.ts +376 -0
- package/src/daemon/mcp-handler.ts +575 -0
- package/src/daemon/mcp-subscriptions.ts +81 -0
- package/src/daemon/mesh.ts +51 -0
- package/src/daemon/server.ts +655 -504
- package/src/daemon/types.ts +121 -0
- package/src/daemon/workspace-map.ts +104 -0
- package/src/platform.ts +60 -39
- package/src/version.ts +15 -0
- package/README.ko.md +0 -306
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { getDaemonInfo, daemonRequest } from "../daemon/client";
|
|
2
|
+
import { fmtNum, visualWidth } from "../core/boast";
|
|
3
|
+
|
|
4
|
+
// ── Types ────────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
interface HologramEntry {
|
|
7
|
+
requests: number;
|
|
8
|
+
originalChars: number;
|
|
9
|
+
hologramChars: number;
|
|
10
|
+
savings: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface HologramDailyRow {
|
|
14
|
+
date: string;
|
|
15
|
+
requests: number;
|
|
16
|
+
originalChars: number;
|
|
17
|
+
hologramChars: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface HologramScore {
|
|
21
|
+
lifetime: HologramEntry;
|
|
22
|
+
today: HologramEntry | null;
|
|
23
|
+
daily: HologramDailyRow[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface CtxSavingsRow {
|
|
27
|
+
date: string;
|
|
28
|
+
type: string;
|
|
29
|
+
requests: number;
|
|
30
|
+
original_chars: number;
|
|
31
|
+
saved_chars: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface CtxSavingsLifetimeRow {
|
|
35
|
+
type: string;
|
|
36
|
+
total_requests: number;
|
|
37
|
+
total_original_chars: number;
|
|
38
|
+
total_saved_chars: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface ScoreData {
|
|
42
|
+
uptime: number;
|
|
43
|
+
totalEvents: number;
|
|
44
|
+
hologram: HologramScore;
|
|
45
|
+
ctxSavings: {
|
|
46
|
+
daily: CtxSavingsRow[];
|
|
47
|
+
lifetime: CtxSavingsLifetimeRow[];
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Locale ───────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
function detectKorean(): boolean {
|
|
54
|
+
const lang = process.env.LANG ?? process.env.LC_ALL ?? process.env.LC_MESSAGES ?? "";
|
|
55
|
+
if (/ko[_\-]/i.test(lang)) return true;
|
|
56
|
+
try {
|
|
57
|
+
return Intl.DateTimeFormat().resolvedOptions().locale.startsWith("ko");
|
|
58
|
+
} catch { return false; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const isKo = detectKorean();
|
|
62
|
+
|
|
63
|
+
const T = isKo ? {
|
|
64
|
+
title: "afd 토큰 대시보드",
|
|
65
|
+
todaySavings: "오늘의 절약",
|
|
66
|
+
lifetimeRoi: "누적 ROI & 분류",
|
|
67
|
+
weekHistory: "최근 7일 내역",
|
|
68
|
+
systemStatus: "시스템 상태",
|
|
69
|
+
startTracking:"afd_read 또는 afd_hologram 사용 시 추적 시작",
|
|
70
|
+
noHistory: "아직 일별 기록이 없습니다",
|
|
71
|
+
totalSaved: "총 절약량",
|
|
72
|
+
estValue: "추정 가치",
|
|
73
|
+
hologram: "홀로그램",
|
|
74
|
+
wsmap: "워크스페이스 맵",
|
|
75
|
+
pinpoint: "핀포인트",
|
|
76
|
+
requests: "요청",
|
|
77
|
+
uptime: "가동시간",
|
|
78
|
+
events: "이벤트",
|
|
79
|
+
updated: "갱신",
|
|
80
|
+
exitHint: "Ctrl+C로 종료",
|
|
81
|
+
labelOrig: "원본 ",
|
|
82
|
+
labelAct: "실제 ",
|
|
83
|
+
labelSaved: "절약 ",
|
|
84
|
+
savedSuffix: "절약됨",
|
|
85
|
+
todayLabel: "오늘",
|
|
86
|
+
} : {
|
|
87
|
+
title: "afd token dashboard",
|
|
88
|
+
todaySavings: "TODAY'S SAVINGS",
|
|
89
|
+
lifetimeRoi: "LIFETIME ROI & BREAKDOWN",
|
|
90
|
+
weekHistory: "7-DAY HISTORY",
|
|
91
|
+
systemStatus: "SYSTEM STATUS",
|
|
92
|
+
startTracking:"Use afd_read or afd_hologram to start tracking",
|
|
93
|
+
noHistory: "No daily history yet",
|
|
94
|
+
totalSaved: "Total Saved",
|
|
95
|
+
estValue: "Est. Value",
|
|
96
|
+
hologram: "Hologram",
|
|
97
|
+
wsmap: "W/S Map",
|
|
98
|
+
pinpoint: "Pinpoint",
|
|
99
|
+
requests: "Requests",
|
|
100
|
+
uptime: "Uptime",
|
|
101
|
+
events: "Events",
|
|
102
|
+
updated: "Updated",
|
|
103
|
+
exitHint: "Press Ctrl+C to exit",
|
|
104
|
+
labelOrig: "Original ",
|
|
105
|
+
labelAct: "Actual ",
|
|
106
|
+
labelSaved: "Saved ",
|
|
107
|
+
savedSuffix: "saved",
|
|
108
|
+
todayLabel: "Today",
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// ── ANSI ──────────────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
const C = {
|
|
114
|
+
reset: "\x1b[0m",
|
|
115
|
+
bold: "\x1b[1m",
|
|
116
|
+
dim: "\x1b[2m",
|
|
117
|
+
red: "\x1b[31m",
|
|
118
|
+
green: "\x1b[32m",
|
|
119
|
+
yellow: "\x1b[33m",
|
|
120
|
+
cyan: "\x1b[36m",
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const CHARS_PER_TOKEN = 3.5;
|
|
124
|
+
const W = 58;
|
|
125
|
+
const INNER = W - 2;
|
|
126
|
+
const HBAR = "─".repeat(W);
|
|
127
|
+
|
|
128
|
+
function vw(s: string): number {
|
|
129
|
+
return visualWidth(s.replace(/\x1b\[[0-9;]*m/g, ""));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function row(content: string): string {
|
|
133
|
+
const pad = Math.max(0, INNER - vw(content));
|
|
134
|
+
return `│${content}${" ".repeat(pad)}│`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function divider(): string {
|
|
138
|
+
return `├${HBAR}┤`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function formatK(chars: number): string {
|
|
142
|
+
const tok = chars / CHARS_PER_TOKEN;
|
|
143
|
+
if (tok >= 1_000_000) return `${(tok / 1_000_000).toFixed(1)}M tok`;
|
|
144
|
+
if (tok >= 1_000) return `${(tok / 1_000).toFixed(1)}K tok`;
|
|
145
|
+
return `${Math.round(tok)} tok`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function formatUptime(s: number): string {
|
|
149
|
+
if (s < 60) return `${s}s`;
|
|
150
|
+
if (s < 3600) return `${Math.floor(s / 60)}m ${s % 60}s`;
|
|
151
|
+
return `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function weekday(dateStr: string, todayStr: string): string {
|
|
155
|
+
if (dateStr === todayStr) return T.todayLabel;
|
|
156
|
+
try {
|
|
157
|
+
const locale = isKo ? "ko" : "en";
|
|
158
|
+
return new Date(dateStr + "T12:00:00").toLocaleDateString(locale, { weekday: "short" });
|
|
159
|
+
} catch { return ""; }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── Bar helpers ───────────────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
function barSaved(pct: number, width: number): string {
|
|
165
|
+
const filled = Math.min(width, Math.round((pct / 100) * width));
|
|
166
|
+
const empty = width - filled;
|
|
167
|
+
return `${C.green}${"▓".repeat(filled)}${C.dim}${"░".repeat(empty)}${C.reset}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function barActual(pct: number, width: number): string {
|
|
171
|
+
const filled = Math.min(width, Math.round((pct / 100) * width));
|
|
172
|
+
const empty = width - filled;
|
|
173
|
+
return `${C.yellow}${"█".repeat(filled)}${C.dim}${"░".repeat(empty)}${C.reset}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function barOriginal(width: number): string {
|
|
177
|
+
return `${C.dim}${"█".repeat(width)}${C.reset}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── Render ────────────────────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
function render(score: ScoreData, lastUpdated: string): void {
|
|
183
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
184
|
+
|
|
185
|
+
const out: string[] = [];
|
|
186
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
187
|
+
const h = score.hologram;
|
|
188
|
+
const ctx = score.ctxSavings ?? { daily: [], lifetime: [] };
|
|
189
|
+
|
|
190
|
+
// ── Header ──
|
|
191
|
+
out.push(`┌${HBAR}┐`);
|
|
192
|
+
{
|
|
193
|
+
const liveBadge = `${C.red}[● live]${C.reset}`;
|
|
194
|
+
const dateStr = `${C.dim}${today}${C.reset}`;
|
|
195
|
+
const title = ` ${C.cyan}${C.bold}${T.title}${C.reset} ${liveBadge} ${dateStr}`;
|
|
196
|
+
out.push(row(title));
|
|
197
|
+
}
|
|
198
|
+
out.push(divider());
|
|
199
|
+
|
|
200
|
+
// ── TODAY'S SAVINGS ──
|
|
201
|
+
// Denominator = ALL bytes processed by afd tools (hologram_daily includes small files
|
|
202
|
+
// since afd_read now records even full-content reads).
|
|
203
|
+
{
|
|
204
|
+
out.push(row(` ${C.cyan}${C.bold}${T.todaySavings}${C.reset}`));
|
|
205
|
+
|
|
206
|
+
const hToday = h.today ?? (h.daily[0]?.date === today ? h.daily[0] : null);
|
|
207
|
+
// holoOriginal = total bytes that went through afd_read (small files + large files)
|
|
208
|
+
// holoActual = bytes actually sent (small files at full size + large files at hologram size)
|
|
209
|
+
const holoOriginal = hToday?.originalChars ?? 0;
|
|
210
|
+
const holoActual = hToday?.hologramChars ?? 0;
|
|
211
|
+
const holoRequests = hToday?.requests ?? 0;
|
|
212
|
+
|
|
213
|
+
const wsmapToday = ctx.daily.find(r => r.date === today && r.type === "wsmap");
|
|
214
|
+
const pinpointToday = ctx.daily.find(r => r.date === today && r.type === "pinpoint");
|
|
215
|
+
const wsmapOriginal = wsmapToday?.original_chars ?? 0;
|
|
216
|
+
const wsmapSavedChars = wsmapToday?.saved_chars ?? 0;
|
|
217
|
+
const pinpointOriginal = pinpointToday?.original_chars ?? 0;
|
|
218
|
+
const pinpointSavedChars = pinpointToday?.saved_chars ?? 0;
|
|
219
|
+
|
|
220
|
+
// totalOriginal = what WOULD have been consumed without afd
|
|
221
|
+
// totalActual = what WAS actually consumed through afd
|
|
222
|
+
const totalOriginal = holoOriginal + wsmapOriginal + pinpointOriginal;
|
|
223
|
+
const totalActual = holoActual + (wsmapOriginal - wsmapSavedChars) + (pinpointOriginal - pinpointSavedChars);
|
|
224
|
+
const hasData = holoRequests > 0 || (wsmapToday?.requests ?? 0) > 0 || (pinpointToday?.requests ?? 0) > 0;
|
|
225
|
+
|
|
226
|
+
if (hasData && totalOriginal > 0) {
|
|
227
|
+
const barW = 20;
|
|
228
|
+
const savedTok = Math.max(0, totalOriginal - totalActual);
|
|
229
|
+
const savedPct = Math.round((savedTok / totalOriginal) * 100);
|
|
230
|
+
const actPct = (totalActual / totalOriginal) * 100;
|
|
231
|
+
|
|
232
|
+
out.push(row(` ${T.labelOrig}${barOriginal(barW)} ${C.dim}${formatK(totalOriginal)}${C.reset}`));
|
|
233
|
+
out.push(row(` ${T.labelAct}${barActual(actPct, barW)} ${C.yellow}${formatK(totalActual)}${C.reset}`));
|
|
234
|
+
out.push(row(` ${T.labelSaved}${barSaved(savedPct, barW)} ${C.green}${savedPct}%${C.reset} ${C.dim}(${formatK(savedTok)} ${T.savedSuffix})${C.reset}`));
|
|
235
|
+
} else {
|
|
236
|
+
out.push(row(` ${C.dim}${T.startTracking}${C.reset}`));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
out.push(divider());
|
|
241
|
+
|
|
242
|
+
// ── LIFETIME ROI & BREAKDOWN ──
|
|
243
|
+
{
|
|
244
|
+
out.push(row(` ${C.cyan}${C.bold}${T.lifetimeRoi}${C.reset}`));
|
|
245
|
+
|
|
246
|
+
const lt = h.lifetime;
|
|
247
|
+
// hologramSavedChars = compression savings only (small files cancel out: orig == actual)
|
|
248
|
+
const hologramSavedChars = Math.max(0, lt.originalChars - lt.hologramChars);
|
|
249
|
+
|
|
250
|
+
const wsmapRow = ctx.lifetime.find(r => r.type === "wsmap");
|
|
251
|
+
const pinpointRow = ctx.lifetime.find(r => r.type === "pinpoint");
|
|
252
|
+
const wsmapSaved = wsmapRow?.total_saved_chars ?? 0;
|
|
253
|
+
const pinpointSaved = pinpointRow?.total_saved_chars ?? 0;
|
|
254
|
+
const totalSavedChars = hologramSavedChars + wsmapSaved + pinpointSaved;
|
|
255
|
+
const totalSavedTok = totalSavedChars / CHARS_PER_TOKEN;
|
|
256
|
+
const estValue = Math.round(totalSavedTok / 1000 * 0.003 * 100) / 100;
|
|
257
|
+
|
|
258
|
+
const totalLine = ` ${C.bold}${C.green}${T.totalSaved} ~${fmtNum(Math.round(totalSavedTok))} tok${C.reset} ${C.dim}│ ${T.estValue} $${estValue.toFixed(2)}${C.reset}`;
|
|
259
|
+
out.push(row(totalLine));
|
|
260
|
+
out.push(row(` ${C.dim}${"─".repeat(INNER - 4)}${C.reset}`));
|
|
261
|
+
|
|
262
|
+
const holoTok = Math.round(hologramSavedChars / CHARS_PER_TOKEN);
|
|
263
|
+
const holoColor = holoTok > 0 ? C.green : C.dim;
|
|
264
|
+
const holoPct = totalSavedChars > 0 ? Math.round((hologramSavedChars / totalSavedChars) * 100) : 0;
|
|
265
|
+
out.push(row(` ${holoColor}[${holoTok > 0 ? "✓" : "·"}] ${T.hologram.padEnd(12)} ~${formatK(hologramSavedChars).padEnd(9)} (${holoPct}%)${C.reset}`));
|
|
266
|
+
|
|
267
|
+
const wsmapTok = Math.round(wsmapSaved / CHARS_PER_TOKEN);
|
|
268
|
+
const wsmapColor = wsmapTok > 0 ? C.green : C.dim;
|
|
269
|
+
const wsmapPct = totalSavedChars > 0 ? Math.round((wsmapSaved / totalSavedChars) * 100) : 0;
|
|
270
|
+
out.push(row(` ${wsmapColor}[${wsmapTok > 0 ? "✓" : "·"}] ${T.wsmap.padEnd(12)} ~${formatK(wsmapSaved).padEnd(9)} (${wsmapPct}%)${C.reset}`));
|
|
271
|
+
|
|
272
|
+
const pinTok = Math.round(pinpointSaved / CHARS_PER_TOKEN);
|
|
273
|
+
const pinColor = pinTok > 0 ? C.green : C.dim;
|
|
274
|
+
const pinPct = totalSavedChars > 0 ? Math.round((pinpointSaved / totalSavedChars) * 100) : 0;
|
|
275
|
+
out.push(row(` ${pinColor}[${pinTok > 0 ? "✓" : "·"}] ${T.pinpoint.padEnd(12)} ~${formatK(pinpointSaved).padEnd(9)} (${pinPct}%)${C.reset}`));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
out.push(divider());
|
|
279
|
+
|
|
280
|
+
// ── 7-DAY HISTORY ──
|
|
281
|
+
{
|
|
282
|
+
out.push(row(` ${C.cyan}${C.bold}${T.weekHistory}${C.reset}`));
|
|
283
|
+
|
|
284
|
+
// Merge hologram daily (which now includes small file reads) + wsmap/pinpoint daily
|
|
285
|
+
const dailyMap = new Map<string, { original: number; saved: number }>();
|
|
286
|
+
for (const d of h.daily) {
|
|
287
|
+
dailyMap.set(d.date, { original: d.originalChars, saved: d.originalChars - d.hologramChars });
|
|
288
|
+
}
|
|
289
|
+
for (const r of ctx.daily) {
|
|
290
|
+
const entry = dailyMap.get(r.date) ?? { original: 0, saved: 0 };
|
|
291
|
+
dailyMap.set(r.date, { original: entry.original + r.original_chars, saved: entry.saved + r.saved_chars });
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const sortedDates = [...dailyMap.keys()].sort().reverse().slice(0, 7);
|
|
295
|
+
|
|
296
|
+
if (sortedDates.length > 0) {
|
|
297
|
+
for (const date of sortedDates) {
|
|
298
|
+
const { original, saved } = dailyMap.get(date)!;
|
|
299
|
+
const pct = original > 0 ? Math.round((saved / original) * 100) : 0;
|
|
300
|
+
const barW = 14;
|
|
301
|
+
const filled = Math.min(barW, Math.round((pct / 100) * barW));
|
|
302
|
+
const empty = barW - filled;
|
|
303
|
+
const barColor = pct >= 70 ? C.green : pct >= 40 ? C.yellow : C.dim;
|
|
304
|
+
const bar = `${barColor}${"█".repeat(filled)}${C.dim}${"░".repeat(empty)}${C.reset}`;
|
|
305
|
+
const wd = weekday(date, today);
|
|
306
|
+
const dateLabel = `${C.dim}${date.slice(5)}${C.reset} ${C.dim}(${wd})${C.reset}`;
|
|
307
|
+
const pctColor = pct >= 70 ? C.green : pct >= 40 ? C.yellow : C.dim;
|
|
308
|
+
const tokRange = `${C.dim}${formatK(original)} → ${formatK(original - saved)}${C.reset}`;
|
|
309
|
+
out.push(row(` ${dateLabel} ${bar} ${pctColor}${pct}%${C.reset} │ ${tokRange}`));
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
out.push(row(` ${C.dim}${T.noHistory}${C.reset}`));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
out.push(divider());
|
|
317
|
+
|
|
318
|
+
// ── SYSTEM STATUS ──
|
|
319
|
+
{
|
|
320
|
+
out.push(row(` ${C.cyan}${C.bold}${T.systemStatus}${C.reset}`));
|
|
321
|
+
const lt = h.lifetime;
|
|
322
|
+
const reqStr = `${T.requests}: ${lt.requests}`;
|
|
323
|
+
const uptStr = `${T.uptime}: ${formatUptime(score.uptime)}`;
|
|
324
|
+
const evtStr = `${T.events}: ${score.totalEvents}`;
|
|
325
|
+
out.push(row(` ${C.dim}${reqStr} │ ${uptStr} │ ${evtStr}${C.reset}`));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
out.push(`└${HBAR}┘`);
|
|
329
|
+
out.push(` ${C.dim}${T.updated}: ${lastUpdated} | ${T.exitHint}${C.reset}`);
|
|
330
|
+
|
|
331
|
+
process.stdout.write(out.join("\n") + "\n");
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ── Live loop ─────────────────────────────────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
export async function dashboardCommand(): Promise<void> {
|
|
337
|
+
const info = getDaemonInfo();
|
|
338
|
+
if (!info) {
|
|
339
|
+
const msg = isKo
|
|
340
|
+
? `[afd] 데몬이 실행 중이 아닙니다. \`afd start\`를 먼저 실행하세요.`
|
|
341
|
+
: `[afd] Daemon not running. Run \`afd start\` first.`;
|
|
342
|
+
console.error(`${C.red}${msg}${C.reset}`);
|
|
343
|
+
process.exit(1);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const ac = new AbortController();
|
|
347
|
+
|
|
348
|
+
process.on("exit", () => process.stdout.write("\x1b[?25h"));
|
|
349
|
+
const cleanup = () => { ac.abort(); process.exit(0); };
|
|
350
|
+
process.on("SIGINT", cleanup);
|
|
351
|
+
process.on("SIGTERM", cleanup);
|
|
352
|
+
process.stdout.write("\x1b[?25l");
|
|
353
|
+
|
|
354
|
+
// Initial render
|
|
355
|
+
try {
|
|
356
|
+
const score = await daemonRequest<ScoreData>("/score");
|
|
357
|
+
render(score, new Date().toLocaleTimeString());
|
|
358
|
+
} catch (err) {
|
|
359
|
+
process.stdout.write("\x1b[?25h");
|
|
360
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
361
|
+
console.error(`${C.red}[afd] ${msg}${C.reset}`);
|
|
362
|
+
process.exit(1);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function doRefresh() {
|
|
366
|
+
try {
|
|
367
|
+
const score = await daemonRequest<ScoreData>("/score");
|
|
368
|
+
render(score, new Date().toLocaleTimeString());
|
|
369
|
+
} catch { /* non-fatal */ }
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const pollTimer = setInterval(doRefresh, 3000);
|
|
373
|
+
ac.signal.addEventListener("abort", () => clearInterval(pollTimer));
|
|
374
|
+
|
|
375
|
+
// SSE for instant updates
|
|
376
|
+
(async () => {
|
|
377
|
+
while (!ac.signal.aborted) {
|
|
378
|
+
try {
|
|
379
|
+
const res = await fetch(`http://127.0.0.1:${info.port}/events`, { signal: ac.signal });
|
|
380
|
+
if (!res.body) { await new Promise(r => setTimeout(r, 3000)); continue; }
|
|
381
|
+
|
|
382
|
+
const reader = res.body.getReader();
|
|
383
|
+
const dec = new TextDecoder();
|
|
384
|
+
let buf = "";
|
|
385
|
+
|
|
386
|
+
while (!ac.signal.aborted) {
|
|
387
|
+
const { value, done } = await reader.read();
|
|
388
|
+
if (done) break;
|
|
389
|
+
buf += dec.decode(value, { stream: true });
|
|
390
|
+
const frames = buf.split("\n\n");
|
|
391
|
+
buf = frames.pop() ?? "";
|
|
392
|
+
for (const frame of frames) {
|
|
393
|
+
if (frame.trim() && frame.includes("data:")) await doRefresh();
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
} catch {
|
|
397
|
+
if (ac.signal.aborted) break;
|
|
398
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
})();
|
|
402
|
+
|
|
403
|
+
await new Promise<void>(resolve => ac.signal.addEventListener("abort", resolve));
|
|
404
|
+
}
|
package/src/commands/diagnose.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { writeFileSync, mkdirSync, existsSync } from "fs";
|
|
2
2
|
import { dirname } from "path";
|
|
3
|
-
import { daemonRequest } from "../daemon/client";
|
|
3
|
+
import { daemonRequest, getDaemonInfo } from "../daemon/client";
|
|
4
4
|
import type { Symptom, PatchOp, DiagnosisResult } from "../core/immune";
|
|
5
5
|
import { notifyAutoHeal } from "../core/notify";
|
|
6
6
|
|
|
@@ -18,6 +18,9 @@ interface AutoHealResponse {
|
|
|
18
18
|
function applyPatch(patch: PatchOp): boolean {
|
|
19
19
|
const filePath = patch.path.replace(/^\//, "");
|
|
20
20
|
|
|
21
|
+
// Guard: reject path traversal attempts
|
|
22
|
+
if (filePath.includes("..") || filePath.startsWith("/") || /^[A-Za-z]:/.test(filePath)) return false;
|
|
23
|
+
|
|
21
24
|
if (patch.op === "add") {
|
|
22
25
|
if (existsSync(filePath)) return false;
|
|
23
26
|
const dir = dirname(filePath);
|
|
@@ -52,10 +55,50 @@ export async function diagnoseCommand(opts: DiagnoseOptions) {
|
|
|
52
55
|
process.exit(1);
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
// Helper: fetch past mistakes for passive defense injection
|
|
59
|
+
async function fetchPastMistakes(): Promise<string[]> {
|
|
60
|
+
if (!isA2A) return [];
|
|
61
|
+
try {
|
|
62
|
+
const info = getDaemonInfo();
|
|
63
|
+
if (!info) return [];
|
|
64
|
+
// Query all recent mistakes (not file-specific in healthy path)
|
|
65
|
+
const resp = await fetch(`http://127.0.0.1:${info.port}/mistake-history?file=*`, {
|
|
66
|
+
signal: AbortSignal.timeout(500),
|
|
67
|
+
});
|
|
68
|
+
// Fall back to empty if the wildcard isn't supported
|
|
69
|
+
return [];
|
|
70
|
+
} catch { return []; }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function fetchMistakesForFiles(files: string[]): Promise<string[]> {
|
|
74
|
+
if (!isA2A) return [];
|
|
75
|
+
const warnings: string[] = [];
|
|
76
|
+
try {
|
|
77
|
+
const info = getDaemonInfo();
|
|
78
|
+
if (!info) return [];
|
|
79
|
+
for (const file of files.slice(0, 3)) {
|
|
80
|
+
try {
|
|
81
|
+
const resp = await fetch(`http://127.0.0.1:${info.port}/mistake-history?file=${encodeURIComponent(file)}`, {
|
|
82
|
+
signal: AbortSignal.timeout(500),
|
|
83
|
+
});
|
|
84
|
+
const data = await resp.json() as { mistakes: { mistake_type: string; description: string }[] };
|
|
85
|
+
for (const m of data.mistakes.slice(0, 3)) {
|
|
86
|
+
warnings.push(`Previous mistake on ${file}: '${m.description}'. Be careful.`.slice(0, 200));
|
|
87
|
+
}
|
|
88
|
+
} catch { /* skip this file */ }
|
|
89
|
+
}
|
|
90
|
+
} catch { /* crash-only */ }
|
|
91
|
+
return warnings;
|
|
92
|
+
}
|
|
93
|
+
|
|
55
94
|
// No symptoms — nothing to do
|
|
56
95
|
if (diagnosis.symptoms.length === 0) {
|
|
57
96
|
if (isA2A) {
|
|
58
|
-
|
|
97
|
+
const output: Record<string, unknown> = { status: "healthy", symptoms: [], healed: [] };
|
|
98
|
+
// Inject past mistakes even when healthy (proactive warning)
|
|
99
|
+
const pastMistakes = await fetchPastMistakes();
|
|
100
|
+
if (pastMistakes.length > 0) output.pastMistakes = pastMistakes;
|
|
101
|
+
console.log(JSON.stringify(output));
|
|
59
102
|
} else {
|
|
60
103
|
console.log("[afd diagnose] System healthy.");
|
|
61
104
|
}
|
|
@@ -116,15 +159,15 @@ export async function diagnoseCommand(opts: DiagnoseOptions) {
|
|
|
116
159
|
if (applied) {
|
|
117
160
|
// Notify daemon of auto-heal event
|
|
118
161
|
try {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
{
|
|
162
|
+
const info = getDaemonInfo();
|
|
163
|
+
if (info) {
|
|
164
|
+
await fetch(`http://127.0.0.1:${info.port}/auto-heal/record`, {
|
|
122
165
|
method: "POST",
|
|
123
166
|
headers: { "Content-Type": "application/json" },
|
|
124
167
|
body: JSON.stringify({ id: symptom.id }),
|
|
125
168
|
signal: AbortSignal.timeout(1000),
|
|
126
|
-
}
|
|
127
|
-
|
|
169
|
+
});
|
|
170
|
+
}
|
|
128
171
|
} catch {
|
|
129
172
|
// Non-critical — don't block
|
|
130
173
|
}
|
|
@@ -137,15 +180,14 @@ export async function diagnoseCommand(opts: DiagnoseOptions) {
|
|
|
137
180
|
}
|
|
138
181
|
|
|
139
182
|
if (isA2A) {
|
|
140
|
-
|
|
183
|
+
const output: Record<string, unknown> = { status: healed.length > 0 ? "healed" : "no-action", healed, skipped };
|
|
184
|
+
// Inject past mistakes for healed files (passive defense)
|
|
185
|
+
const affectedFiles = diagnosis.symptoms.map(s => s.fileTarget ?? s.id).filter(Boolean);
|
|
186
|
+
const pastMistakes = await fetchMistakesForFiles(affectedFiles);
|
|
187
|
+
if (pastMistakes.length > 0) output.pastMistakes = pastMistakes;
|
|
188
|
+
console.log(JSON.stringify(output));
|
|
141
189
|
} else {
|
|
142
190
|
if (healed.length > 0) console.log(`[afd diagnose] Auto-healed: ${healed.join(", ")}`);
|
|
143
191
|
if (skipped.length > 0) console.log(`[afd diagnose] Skipped (unknown): ${skipped.join(", ")}`);
|
|
144
192
|
}
|
|
145
193
|
}
|
|
146
|
-
|
|
147
|
-
function getDaemonPort(): number {
|
|
148
|
-
const { readFileSync } = require("fs");
|
|
149
|
-
const { PORT_FILE } = require("../constants");
|
|
150
|
-
return parseInt(readFileSync(PORT_FILE, "utf-8").trim(), 10);
|
|
151
|
-
}
|