autonomous-flow-daemon 1.1.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/README.ko.md +124 -164
- package/README.md +99 -170
- package/package.json +11 -5
- package/src/adapters/index.ts +246 -35
- package/src/cli.ts +71 -1
- package/src/commands/benchmark.ts +187 -0
- package/src/commands/diagnose.ts +56 -14
- package/src/commands/doctor.ts +243 -0
- package/src/commands/evolution.ts +107 -0
- package/src/commands/fix.ts +22 -2
- package/src/commands/hooks.ts +136 -0
- package/src/commands/mcp.ts +129 -0
- package/src/commands/restart.ts +14 -0
- package/src/commands/score.ts +164 -96
- package/src/commands/start.ts +74 -15
- package/src/commands/stats.ts +103 -0
- package/src/commands/status.ts +157 -0
- package/src/commands/stop.ts +23 -4
- package/src/commands/sync.ts +253 -20
- package/src/commands/vaccine.ts +177 -0
- package/src/constants.ts +25 -1
- package/src/core/boast.ts +27 -12
- package/src/core/db.ts +74 -3
- package/src/core/evolution.ts +215 -0
- package/src/core/hologram/engine.ts +71 -0
- package/src/core/hologram/fallback.ts +11 -0
- package/src/core/hologram/incremental.ts +227 -0
- package/src/core/hologram/py-extractor.ts +132 -0
- package/src/core/hologram/ts-extractor.ts +320 -0
- package/src/core/hologram/types.ts +25 -0
- package/src/core/hologram.ts +64 -236
- package/src/core/hook-manager.ts +259 -0
- package/src/core/i18n/messages.ts +43 -0
- package/src/core/immune.ts +8 -123
- 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 +27 -19
- package/src/core/rule-engine.ts +287 -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/workspace.ts +28 -0
- package/src/core/yaml-minimal.ts +176 -0
- package/src/daemon/client.ts +34 -6
- package/src/daemon/event-batcher.ts +108 -0
- package/src/daemon/guards.ts +13 -0
- package/src/daemon/http-routes.ts +293 -0
- package/src/daemon/mcp-handler.ts +270 -0
- package/src/daemon/server.ts +439 -353
- package/src/daemon/types.ts +100 -0
- package/src/daemon/workspace-map.ts +92 -0
- package/src/platform.ts +23 -2
- package/src/version.ts +15 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* afd mcp — MCP server management
|
|
3
|
+
*
|
|
4
|
+
* Subcommands:
|
|
5
|
+
* install — Register afd as an MCP server in project and global Claude config
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
9
|
+
import { join, resolve, dirname } from "path";
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
import { getSystemLanguage } from "../core/locale";
|
|
12
|
+
|
|
13
|
+
const msgs = {
|
|
14
|
+
en: {
|
|
15
|
+
usage: "Usage: afd mcp install",
|
|
16
|
+
unknownSub: (s: string) => `Unknown subcommand: ${s}. Use: afd mcp install`,
|
|
17
|
+
installing: "Registering afd MCP server...",
|
|
18
|
+
projectDone: (p: string) => ` [project] Registered in ${p}`,
|
|
19
|
+
projectSkip: (p: string) => ` [project] Already registered in ${p}`,
|
|
20
|
+
globalDone: (p: string) => ` [global] Registered in ${p}`,
|
|
21
|
+
globalSkip: (p: string) => ` [global] Already registered in ${p}`,
|
|
22
|
+
success: "afd MCP server registration complete. Restart Claude Code to activate.",
|
|
23
|
+
hintRestart: " Hint: Run 'claude --reload-plugins' or restart the IDE to pick up the new MCP server.",
|
|
24
|
+
},
|
|
25
|
+
ko: {
|
|
26
|
+
usage: "사용법: afd mcp install",
|
|
27
|
+
unknownSub: (s: string) => `알 수 없는 하위 명령: ${s}. 사용: afd mcp install`,
|
|
28
|
+
installing: "afd MCP 서버 등록 중...",
|
|
29
|
+
projectDone: (p: string) => ` [project] ${p}에 등록 완료`,
|
|
30
|
+
projectSkip: (p: string) => ` [project] ${p}에 이미 등록됨`,
|
|
31
|
+
globalDone: (p: string) => ` [global] ${p}에 등록 완료`,
|
|
32
|
+
globalSkip: (p: string) => ` [global] ${p}에 이미 등록됨`,
|
|
33
|
+
success: "afd MCP 서버 등록 완료. Claude Code를 재시작하면 활성화됩니다.",
|
|
34
|
+
hintRestart: " 힌트: 'claude --reload-plugins' 또는 IDE를 재시작하여 MCP 서버를 인식시키세요.",
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
interface McpServerEntry {
|
|
39
|
+
command: string;
|
|
40
|
+
args: string[];
|
|
41
|
+
[key: string]: unknown;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface McpConfig {
|
|
45
|
+
mcpServers?: Record<string, McpServerEntry>;
|
|
46
|
+
[key: string]: unknown;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The canonical MCP server definition for afd */
|
|
50
|
+
function afdMcpEntry(): McpServerEntry {
|
|
51
|
+
return {
|
|
52
|
+
command: "bun",
|
|
53
|
+
args: ["run", "src/daemon/server.ts", "--mcp"],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Register afd in a JSON config file's mcpServers section */
|
|
58
|
+
function registerInFile(filePath: string, entry: McpServerEntry): "done" | "skip" | "error" {
|
|
59
|
+
let config: McpConfig = {};
|
|
60
|
+
if (existsSync(filePath)) {
|
|
61
|
+
try {
|
|
62
|
+
config = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
63
|
+
} catch {
|
|
64
|
+
// Preserve existing file by reading raw content for backup
|
|
65
|
+
config = {};
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
try {
|
|
69
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
70
|
+
} catch { return "error"; }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const servers = (config.mcpServers ?? {}) as Record<string, McpServerEntry>;
|
|
74
|
+
const existing = servers.afd;
|
|
75
|
+
|
|
76
|
+
if (existing?.command === entry.command &&
|
|
77
|
+
JSON.stringify(existing.args) === JSON.stringify(entry.args)) {
|
|
78
|
+
return "skip";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
servers.afd = entry;
|
|
82
|
+
config.mcpServers = servers;
|
|
83
|
+
try {
|
|
84
|
+
writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
85
|
+
} catch {
|
|
86
|
+
return "error";
|
|
87
|
+
}
|
|
88
|
+
return "done";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Resolve the global Claude Code settings path (cross-platform) */
|
|
92
|
+
function globalClaudeConfigPath(): string {
|
|
93
|
+
return join(homedir(), ".claude.json");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function mcpCommand(subcommand?: string) {
|
|
97
|
+
const lang = getSystemLanguage();
|
|
98
|
+
const m = msgs[lang];
|
|
99
|
+
|
|
100
|
+
if (!subcommand) {
|
|
101
|
+
console.log(m.usage);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (subcommand !== "install") {
|
|
106
|
+
console.error(m.unknownSub(subcommand));
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.log(m.installing);
|
|
111
|
+
|
|
112
|
+
const entry = afdMcpEntry();
|
|
113
|
+
|
|
114
|
+
// 1. Project-level: .mcp.json
|
|
115
|
+
const projectPath = resolve(".mcp.json");
|
|
116
|
+
const projectResult = registerInFile(projectPath, entry);
|
|
117
|
+
if (projectResult === "error") console.error(` [project] Failed to write ${projectPath}`);
|
|
118
|
+
else console.log(projectResult === "done" ? m.projectDone(projectPath) : m.projectSkip(projectPath));
|
|
119
|
+
|
|
120
|
+
// 2. Global-level: ~/.claude.json — mcpServers section
|
|
121
|
+
const globalPath = globalClaudeConfigPath();
|
|
122
|
+
const globalResult = registerInFile(globalPath, entry);
|
|
123
|
+
if (globalResult === "error") console.error(` [global] Failed to write ${globalPath}`);
|
|
124
|
+
else console.log(globalResult === "done" ? m.globalDone(globalPath) : m.globalSkip(globalPath));
|
|
125
|
+
|
|
126
|
+
console.log("");
|
|
127
|
+
console.log(m.success);
|
|
128
|
+
console.log(m.hintRestart);
|
|
129
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { stopCommand } from "./stop";
|
|
2
|
+
import { startCommand } from "./start";
|
|
3
|
+
import { getSystemLanguage } from "../core/locale";
|
|
4
|
+
import { getMessages } from "../core/i18n/messages";
|
|
5
|
+
|
|
6
|
+
export async function restartCommand() {
|
|
7
|
+
const lang = getSystemLanguage();
|
|
8
|
+
const msg = getMessages(lang);
|
|
9
|
+
|
|
10
|
+
console.log(msg.DAEMON_RESTARTING);
|
|
11
|
+
|
|
12
|
+
await stopCommand();
|
|
13
|
+
await startCommand();
|
|
14
|
+
}
|
package/src/commands/score.ts
CHANGED
|
@@ -4,13 +4,26 @@ import type { ShiftSummary } from "../core/boast";
|
|
|
4
4
|
import { getSystemLanguage } from "../core/locale";
|
|
5
5
|
import { getMessages, t } from "../core/i18n/messages";
|
|
6
6
|
|
|
7
|
-
interface
|
|
7
|
+
interface HologramEntry {
|
|
8
8
|
requests: number;
|
|
9
9
|
originalChars: number;
|
|
10
10
|
hologramChars: number;
|
|
11
11
|
savings: number;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
interface HologramDailyRow {
|
|
15
|
+
date: string;
|
|
16
|
+
requests: number;
|
|
17
|
+
originalChars: number;
|
|
18
|
+
hologramChars: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface HologramScore {
|
|
22
|
+
lifetime: HologramEntry;
|
|
23
|
+
today: HologramEntry | null;
|
|
24
|
+
daily: HologramDailyRow[];
|
|
25
|
+
}
|
|
26
|
+
|
|
14
27
|
interface AutoHealEntry {
|
|
15
28
|
id: string;
|
|
16
29
|
at: number;
|
|
@@ -38,6 +51,11 @@ interface SuppressionScore {
|
|
|
38
51
|
activeTaps: number;
|
|
39
52
|
}
|
|
40
53
|
|
|
54
|
+
interface DynamicImmuneScore {
|
|
55
|
+
activeValidators: number;
|
|
56
|
+
validatorNames: string[];
|
|
57
|
+
}
|
|
58
|
+
|
|
41
59
|
interface ScoreData {
|
|
42
60
|
uptime: number;
|
|
43
61
|
filesDetected: number;
|
|
@@ -50,8 +68,26 @@ interface ScoreData {
|
|
|
50
68
|
immune: ImmuneScore;
|
|
51
69
|
ecosystem: EcosystemScore;
|
|
52
70
|
suppression: SuppressionScore;
|
|
71
|
+
dynamicImmune?: DynamicImmuneScore;
|
|
53
72
|
}
|
|
54
73
|
|
|
74
|
+
// ── ANSI helpers ──
|
|
75
|
+
const C = {
|
|
76
|
+
reset: "\x1b[0m",
|
|
77
|
+
bold: "\x1b[1m",
|
|
78
|
+
dim: "\x1b[2m",
|
|
79
|
+
red: "\x1b[31m",
|
|
80
|
+
green: "\x1b[32m",
|
|
81
|
+
yellow: "\x1b[33m",
|
|
82
|
+
blue: "\x1b[34m",
|
|
83
|
+
magenta: "\x1b[35m",
|
|
84
|
+
cyan: "\x1b[36m",
|
|
85
|
+
white: "\x1b[37m",
|
|
86
|
+
bgRed: "\x1b[41m",
|
|
87
|
+
bgGreen: "\x1b[42m",
|
|
88
|
+
bgYellow: "\x1b[43m",
|
|
89
|
+
};
|
|
90
|
+
|
|
55
91
|
function formatUptime(seconds: number): string {
|
|
56
92
|
if (seconds < 60) return `${seconds}s`;
|
|
57
93
|
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
@@ -60,9 +96,13 @@ function formatUptime(seconds: number): string {
|
|
|
60
96
|
return `${h}h ${m}m`;
|
|
61
97
|
}
|
|
62
98
|
|
|
63
|
-
function
|
|
64
|
-
const
|
|
65
|
-
|
|
99
|
+
function gaugeBar(value: number, max: number, width = 20): string {
|
|
100
|
+
const ratio = Math.min(value / Math.max(max, 1), 1);
|
|
101
|
+
const filled = Math.round(ratio * width);
|
|
102
|
+
const empty = width - filled;
|
|
103
|
+
const color = ratio >= 0.7 ? C.green : ratio >= 0.4 ? C.yellow : C.red;
|
|
104
|
+
const pct = Math.round(ratio * 100);
|
|
105
|
+
return `${color}${"█".repeat(filled)}${C.dim}${"░".repeat(empty)}${C.reset} ${pct}%`;
|
|
66
106
|
}
|
|
67
107
|
|
|
68
108
|
function formatChars(n: number): string {
|
|
@@ -71,24 +111,34 @@ function formatChars(n: number): string {
|
|
|
71
111
|
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
72
112
|
}
|
|
73
113
|
|
|
74
|
-
const W =
|
|
75
|
-
const line = "
|
|
76
|
-
const sep = "\u2500".repeat(30);
|
|
114
|
+
const W = 52;
|
|
115
|
+
const line = "─".repeat(W);
|
|
77
116
|
|
|
78
117
|
function row(content: string): string {
|
|
79
|
-
|
|
118
|
+
// Strip ANSI for width calculation
|
|
119
|
+
const stripped = content.replace(/\x1b\[[0-9;]*m/g, "");
|
|
120
|
+
const vw = visualWidth(stripped);
|
|
80
121
|
const padSize = Math.max(0, W - vw);
|
|
81
|
-
return
|
|
122
|
+
return `│${content}${" ".repeat(padSize)}│`;
|
|
82
123
|
}
|
|
83
124
|
|
|
84
|
-
function
|
|
85
|
-
|
|
86
|
-
return s + " ".repeat(Math.max(0, target - vw));
|
|
125
|
+
function section(title: string): string {
|
|
126
|
+
return row(` ${C.bold}${C.cyan}${title}${C.reset}`);
|
|
87
127
|
}
|
|
88
128
|
|
|
89
|
-
/** Render a labeled key-value row with visual-width-aware padding. */
|
|
90
129
|
function kv(label: string, value: string): string {
|
|
91
|
-
|
|
130
|
+
const stripped = label.replace(/\x1b\[[0-9;]*m/g, "");
|
|
131
|
+
const gap = 18 - visualWidth(stripped);
|
|
132
|
+
return row(` ${C.dim}${label}${C.reset}${" ".repeat(Math.max(1, gap))}${value}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function guardianGrade(antibodies: number, heals: number, holoSavings: number): { grade: string; label: string; color: string } {
|
|
136
|
+
const score = Math.min(antibodies * 15, 40) + Math.min(heals * 10, 30) + Math.min(holoSavings * 0.3, 30);
|
|
137
|
+
if (score >= 80) return { grade: "A+", label: "FORTIFIED", color: C.green };
|
|
138
|
+
if (score >= 60) return { grade: "A", label: "GUARDED", color: C.green };
|
|
139
|
+
if (score >= 40) return { grade: "B", label: "LEARNING", color: C.yellow };
|
|
140
|
+
if (score >= 20) return { grade: "C", label: "EXPOSED", color: C.yellow };
|
|
141
|
+
return { grade: "D", label: "VULNERABLE", color: C.red };
|
|
92
142
|
}
|
|
93
143
|
|
|
94
144
|
export async function scoreCommand() {
|
|
@@ -98,111 +148,129 @@ export async function scoreCommand() {
|
|
|
98
148
|
try {
|
|
99
149
|
const data = await daemonRequest<ScoreData>("/score");
|
|
100
150
|
const h = data.hologram;
|
|
151
|
+
const holoSav = h.lifetime.savings;
|
|
152
|
+
const grade = guardianGrade(data.immune.antibodies, data.immune.autoHealed, holoSav);
|
|
153
|
+
|
|
154
|
+
const out: string[] = [];
|
|
155
|
+
|
|
156
|
+
// ── Header ──
|
|
157
|
+
out.push(`┌${line}┐`);
|
|
158
|
+
out.push(row(` ${C.bold}🛡️ Guardian Status${C.reset} ${grade.color}${C.bold}[ ${grade.grade} ]${C.reset} ${grade.color}${grade.label}${C.reset}`));
|
|
159
|
+
out.push(`├${line}┤`);
|
|
101
160
|
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
161
|
+
// ── Health Gauge ──
|
|
162
|
+
const healthPct = Math.min(data.immune.antibodies * 15 + data.immune.autoHealed * 10 + holoSav * 0.3, 100);
|
|
163
|
+
out.push(row(` ${gaugeBar(healthPct, 100, 30)}`));
|
|
164
|
+
out.push(`├${line}┤`);
|
|
106
165
|
|
|
107
|
-
//
|
|
108
|
-
|
|
166
|
+
// ── System Info ──
|
|
167
|
+
out.push(section(lang === "ko" ? "시스템 정보" : "System Info"));
|
|
168
|
+
out.push(kv(i18n.SCORE_ECOSYSTEM, `${C.white}${C.bold}${data.ecosystem.primary}${C.reset}`));
|
|
109
169
|
if (data.ecosystem.detected.length > 1) {
|
|
110
170
|
const others = data.ecosystem.detected.slice(1).map(e => e.name).join(", ");
|
|
111
|
-
|
|
171
|
+
out.push(kv(i18n.SCORE_ALSO_FOUND, `${C.dim}${others}${C.reset}`));
|
|
112
172
|
}
|
|
173
|
+
out.push(kv(i18n.SCORE_UPTIME, `${C.green}${formatUptime(data.uptime)}${C.reset}`));
|
|
174
|
+
out.push(kv(i18n.SCORE_EVENTS, `${data.totalEvents}`));
|
|
175
|
+
out.push(kv(i18n.SCORE_FILES_FOUND, `${data.watchedFiles.length}`));
|
|
113
176
|
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
console.log(kv(i18n.SCORE_EVENTS, String(data.totalEvents)));
|
|
118
|
-
console.log(kv(i18n.SCORE_FILES_FOUND, String(data.watchedFiles.length)));
|
|
119
|
-
console.log(`\u251C${line}\u2524`);
|
|
120
|
-
console.log(row(` ${vwPad(i18n.SCORE_ACTIVITY, 10)}${heatBar(data.totalEvents, 100)}`));
|
|
121
|
-
|
|
122
|
-
// Hologram section
|
|
123
|
-
console.log(`\u251C${line}\u2524`);
|
|
124
|
-
console.log(row(` ${i18n.SCORE_HOLOGRAM_TITLE}`));
|
|
125
|
-
console.log(row(` ${sep}`));
|
|
126
|
-
if (h.requests > 0) {
|
|
127
|
-
const saved = h.originalChars - h.hologramChars;
|
|
128
|
-
console.log(kv(i18n.SCORE_HOLOGRAM_REQUESTS, String(h.requests)));
|
|
129
|
-
console.log(kv(i18n.SCORE_HOLOGRAM_ORIGINAL, `${formatChars(h.originalChars)} chars`));
|
|
130
|
-
console.log(kv(i18n.SCORE_HOLOGRAM_COMPRESSED, `${formatChars(h.hologramChars)} chars`));
|
|
131
|
-
console.log(kv(i18n.SCORE_HOLOGRAM_SAVED, `${formatChars(saved)} chars (${h.savings}%)`));
|
|
132
|
-
console.log(row(` ${vwPad(i18n.SCORE_HOLOGRAM_EFFICIENCY, 10)}${heatBar(h.savings, 100)}`));
|
|
133
|
-
} else {
|
|
134
|
-
console.log(row(` ${i18n.SCORE_HOLOGRAM_EMPTY}`));
|
|
135
|
-
console.log(row(` ${i18n.SCORE_HOLOGRAM_HINT}`));
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Immune System section
|
|
139
|
-
console.log(`\u251C${line}\u2524`);
|
|
140
|
-
console.log(row(` ${i18n.SCORE_IMMUNE_TITLE}`));
|
|
141
|
-
console.log(row(` ${sep}`));
|
|
177
|
+
// ── Immune System ──
|
|
178
|
+
out.push(`├${line}┤`);
|
|
179
|
+
out.push(section(lang === "ko" ? "면역 시스템" : "Immune System"));
|
|
142
180
|
const ab = data.immune.antibodies;
|
|
143
181
|
const ah = data.immune.autoHealed;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
: i18n.SCORE_IMMUNE_FORTIFIED;
|
|
148
|
-
console.log(kv(i18n.SCORE_ANTIBODIES, String(ab)));
|
|
149
|
-
console.log(kv(i18n.SCORE_LEVEL, immuneLevel));
|
|
150
|
-
console.log(row(` ${vwPad(i18n.SCORE_IMMUNITY, 10)}${heatBar(ab, 10)}`));
|
|
151
|
-
const healedStr = t(i18n.SCORE_AUTO_HEALED, { count: ah, s: ah !== 1 ? "s" : "" });
|
|
152
|
-
console.log(kv(i18n.SCORE_AUTO_HEALED_LABEL, healedStr));
|
|
182
|
+
out.push(kv(i18n.SCORE_ANTIBODIES, `${C.bold}${ab}${C.reset}`));
|
|
183
|
+
out.push(row(` ${lang === "ko" ? "면역력" : "Immunity"} ${gaugeBar(ab, 10, 20)}`));
|
|
184
|
+
out.push(kv(lang === "ko" ? "차단 횟수" : "Prevented", `${C.bold}${C.green}${ah}${C.reset}${C.dim} disaster${ah !== 1 ? "s" : ""}${C.reset}`));
|
|
153
185
|
if (data.immune.lastAutoHeal) {
|
|
154
186
|
const ago = formatUptime(Math.floor((Date.now() - data.immune.lastAutoHeal.at) / 1000));
|
|
155
|
-
|
|
156
|
-
console.log(kv(i18n.SCORE_LAST_EVENT, healStr));
|
|
187
|
+
out.push(kv(lang === "ko" ? "마지막 치유" : "Last Heal", `${data.immune.lastAutoHeal.id} ${C.dim}(${ago} ago)${C.reset}`));
|
|
157
188
|
}
|
|
158
189
|
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
190
|
+
// ── Dynamic Immune Synthesis ──
|
|
191
|
+
if (data.dynamicImmune && data.dynamicImmune.activeValidators > 0) {
|
|
192
|
+
out.push(`├${line}┤`);
|
|
193
|
+
out.push(section(lang === "ko" ? "진화 상태 (동적 면역)" : "Evolution (Dynamic Immune)"));
|
|
194
|
+
out.push(kv(
|
|
195
|
+
lang === "ko" ? "활성 검증기" : "Validators",
|
|
196
|
+
`${C.bold}${C.green}${data.dynamicImmune.activeValidators} Active${C.reset}`
|
|
197
|
+
));
|
|
198
|
+
const names = data.dynamicImmune.validatorNames.slice(0, 3).join(", ");
|
|
199
|
+
const extra = data.dynamicImmune.validatorNames.length > 3
|
|
200
|
+
? ` ${C.dim}+${data.dynamicImmune.validatorNames.length - 3} more${C.reset}`
|
|
201
|
+
: "";
|
|
202
|
+
out.push(kv(
|
|
203
|
+
lang === "ko" ? "스크립트" : "Scripts",
|
|
204
|
+
`${C.dim}${names}${C.reset}${extra}`
|
|
205
|
+
));
|
|
171
206
|
}
|
|
172
207
|
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
208
|
+
// ── Hologram Efficiency ──
|
|
209
|
+
out.push(`├${line}┤`);
|
|
210
|
+
out.push(section(lang === "ko" ? "토큰 효율 (홀로그램)" : "Token Efficiency (Hologram)"));
|
|
211
|
+
const lt = h.lifetime;
|
|
212
|
+
if (lt.requests > 0) {
|
|
213
|
+
const ltSaved = lt.originalChars - lt.hologramChars;
|
|
214
|
+
out.push(kv(lang === "ko" ? "총 요청" : "Requests", `${lt.requests}`));
|
|
215
|
+
out.push(kv(lang === "ko" ? "절약된 컨텍스트" : "Saved Context", `${C.green}${formatChars(ltSaved)} chars${C.reset}`));
|
|
216
|
+
out.push(row(` ${lang === "ko" ? "효율" : "Efficiency"} ${gaugeBar(lt.savings, 100, 20)}`));
|
|
217
|
+
if (h.today && h.today.requests > 0) {
|
|
218
|
+
const todaySaved = h.today.originalChars - h.today.hologramChars;
|
|
219
|
+
out.push(kv(lang === "ko" ? "오늘" : "Today", `${h.today.requests} req / ${C.green}${formatChars(todaySaved)} saved${C.reset}`));
|
|
220
|
+
}
|
|
221
|
+
} else {
|
|
222
|
+
out.push(row(` ${C.dim}${i18n.SCORE_HOLOGRAM_EMPTY}${C.reset}`));
|
|
223
|
+
out.push(row(` ${C.dim}${i18n.SCORE_HOLOGRAM_HINT}${C.reset}`));
|
|
181
224
|
}
|
|
182
225
|
|
|
183
|
-
// Value
|
|
226
|
+
// ── Value Delivered (ROI) ──
|
|
184
227
|
try {
|
|
185
228
|
const summary = await daemonRequest<ShiftSummary>("/shift-summary");
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
229
|
+
out.push(`├${line}┤`);
|
|
230
|
+
out.push(section(lang === "ko" ? "전달된 가치 (ROI)" : "Value Delivered (ROI)"));
|
|
231
|
+
|
|
232
|
+
// Breakdown: Auto-Heal
|
|
233
|
+
if (summary.healTokensSaved > 0) {
|
|
234
|
+
out.push(kv(
|
|
235
|
+
lang === "ko" ? "🩹 치유 절약" : "🩹 Heal Saved",
|
|
236
|
+
`${C.dim}~${fmtNum(summary.healTokensSaved)} tok / $${summary.healCostSaved.toFixed(2)}${C.reset}`
|
|
237
|
+
));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Breakdown: Hologram
|
|
241
|
+
if (summary.hologramTokensSaved > 0) {
|
|
242
|
+
out.push(kv(
|
|
243
|
+
lang === "ko" ? "💎 홀로그램" : "💎 Hologram",
|
|
244
|
+
`${C.dim}~${fmtNum(summary.hologramTokensSaved)} tok / $${summary.hologramCostSaved.toFixed(2)}${C.reset}`
|
|
245
|
+
));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Total
|
|
249
|
+
out.push(kv(
|
|
250
|
+
lang === "ko" ? "총 절약 토큰" : "Total Tokens",
|
|
251
|
+
`${C.bold}${C.green}~${fmtNum(summary.totalTokensSaved)}${C.reset}`
|
|
252
|
+
));
|
|
253
|
+
out.push(kv(lang === "ko" ? "절약 시간" : "Time Saved", `${C.green}~${summary.totalMinutesSaved} min${C.reset}`));
|
|
254
|
+
out.push(kv(
|
|
255
|
+
lang === "ko" ? "총 절약 비용" : "Total Cost",
|
|
256
|
+
`${C.bold}${C.green}~$${summary.totalCostSaved.toFixed(2)}${C.reset}`
|
|
257
|
+
));
|
|
192
258
|
if (summary.suppressionsSkipped > 0) {
|
|
193
|
-
|
|
259
|
+
out.push(kv(lang === "ko" ? "억제 횟수" : "Suppressed", `${summary.suppressionsSkipped}`));
|
|
194
260
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
|
|
261
|
+
} catch { /* non-fatal */ }
|
|
262
|
+
|
|
263
|
+
// ── Boast ──
|
|
264
|
+
out.push(`├${line}┤`);
|
|
265
|
+
const boast = localizedBoast(lang);
|
|
266
|
+
const truncBoast = boast.length > W - 6 ? boast.slice(0, W - 9) + "..." : boast;
|
|
267
|
+
out.push(row(` ${C.magenta}🗣️ ${truncBoast}${C.reset}`));
|
|
268
|
+
out.push(`└${line}┘`);
|
|
201
269
|
|
|
202
|
-
console.log(
|
|
270
|
+
console.log(out.join("\n"));
|
|
203
271
|
} catch (err: unknown) {
|
|
204
272
|
const msg = err instanceof Error ? err.message : String(err);
|
|
205
|
-
console.error(
|
|
273
|
+
console.error(`${C.red}[afd] ${msg}${C.reset}`);
|
|
206
274
|
process.exit(1);
|
|
207
275
|
}
|
|
208
276
|
}
|
package/src/commands/start.ts
CHANGED
|
@@ -2,16 +2,83 @@ import { resolve } from "path";
|
|
|
2
2
|
import { spawn } from "child_process";
|
|
3
3
|
import { openSync, mkdirSync } from "fs";
|
|
4
4
|
import { getDaemonInfo, isDaemonAlive } from "../daemon/client";
|
|
5
|
-
import {
|
|
5
|
+
import { WATCH_TARGETS, resolveWorkspacePaths } from "../constants";
|
|
6
6
|
import { detectEcosystem } from "../adapters/index";
|
|
7
|
+
import type { DetectionResult } from "../adapters/index";
|
|
7
8
|
import { detachedSpawnOptions, IS_WINDOWS } from "../platform";
|
|
9
|
+
import { rotateLogIfNeeded } from "../core/log-rotate";
|
|
8
10
|
import { getSystemLanguage } from "../core/locale";
|
|
9
11
|
import { getMessages, t } from "../core/i18n/messages";
|
|
12
|
+
import type { MessageDict } from "../core/i18n/messages";
|
|
10
13
|
import { discoverWatchTargets } from "../core/discovery";
|
|
11
14
|
|
|
12
15
|
const STARTUP_POLL_INTERVAL_MS = 100;
|
|
13
16
|
const STARTUP_POLL_MAX_MS = 3000;
|
|
14
17
|
|
|
18
|
+
interface SetupStep {
|
|
19
|
+
label: string;
|
|
20
|
+
newMsg: string;
|
|
21
|
+
okMsg: string;
|
|
22
|
+
skipMsg: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface SetupResult {
|
|
26
|
+
ecosystem: string;
|
|
27
|
+
steps: { label: string; status: "new" | "ok" | "skip" }[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* One-Command Zero-Touch: detect ecosystem and provision all integration
|
|
32
|
+
* channels (hooks, MCP, statusLine) with idempotency.
|
|
33
|
+
*/
|
|
34
|
+
function setupEcosystem(cwd: string, msg: MessageDict): SetupResult[] {
|
|
35
|
+
const ecosystems = detectEcosystem(cwd);
|
|
36
|
+
const results: SetupResult[] = [];
|
|
37
|
+
|
|
38
|
+
for (const { adapter } of ecosystems) {
|
|
39
|
+
console.log(t(msg.SETUP_HEADER, { ecosystem: adapter.name }));
|
|
40
|
+
const steps: SetupResult["steps"] = [];
|
|
41
|
+
|
|
42
|
+
// 1. Hook injection
|
|
43
|
+
if (adapter.injectHooks) {
|
|
44
|
+
const r = adapter.injectHooks(cwd);
|
|
45
|
+
const status = r.injected ? "new" : "ok";
|
|
46
|
+
console.log(status === "new" ? msg.SETUP_HOOKS_NEW : msg.SETUP_HOOKS_OK);
|
|
47
|
+
steps.push({ label: "hooks", status });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 2. MCP registration
|
|
51
|
+
if (adapter.registerMcp) {
|
|
52
|
+
const r = adapter.registerMcp(cwd);
|
|
53
|
+
const status = r.registered ? "new" : "ok";
|
|
54
|
+
console.log(status === "new" ? msg.SETUP_MCP_NEW : msg.SETUP_MCP_OK);
|
|
55
|
+
steps.push({ label: "mcp", status });
|
|
56
|
+
} else {
|
|
57
|
+
console.log(msg.SETUP_MCP_SKIP);
|
|
58
|
+
steps.push({ label: "mcp", status: "skip" });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 3. StatusLine configuration
|
|
62
|
+
if (adapter.configureStatusLine) {
|
|
63
|
+
const r = adapter.configureStatusLine(cwd);
|
|
64
|
+
const status = r.configured ? "new" : "ok";
|
|
65
|
+
console.log(status === "new" ? msg.SETUP_STATUS_NEW : msg.SETUP_STATUS_OK);
|
|
66
|
+
steps.push({ label: "statusLine", status });
|
|
67
|
+
} else {
|
|
68
|
+
console.log(msg.SETUP_STATUS_SKIP);
|
|
69
|
+
steps.push({ label: "statusLine", status: "skip" });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
results.push({ ecosystem: adapter.name, steps });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (ecosystems.length > 0) {
|
|
76
|
+
console.log(msg.SETUP_DONE);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return results;
|
|
80
|
+
}
|
|
81
|
+
|
|
15
82
|
export async function startCommand(options?: { mcp?: boolean }) {
|
|
16
83
|
// MCP stdio mode: run daemon in foreground with stdio transport
|
|
17
84
|
if (options?.mcp) {
|
|
@@ -23,7 +90,8 @@ export async function startCommand(options?: { mcp?: boolean }) {
|
|
|
23
90
|
const lang = getSystemLanguage();
|
|
24
91
|
const msg = getMessages(lang);
|
|
25
92
|
|
|
26
|
-
|
|
93
|
+
const paths = resolveWorkspacePaths();
|
|
94
|
+
mkdirSync(paths.afdDir, { recursive: true });
|
|
27
95
|
|
|
28
96
|
// ── Idempotency: check if already running ──
|
|
29
97
|
const existing = getDaemonInfo();
|
|
@@ -34,7 +102,8 @@ export async function startCommand(options?: { mcp?: boolean }) {
|
|
|
34
102
|
|
|
35
103
|
// ── Spawn detached daemon with log redirection ──
|
|
36
104
|
const daemonScript = resolve(import.meta.dirname, "../daemon/server.ts");
|
|
37
|
-
const logPath =
|
|
105
|
+
const logPath = paths.logFile;
|
|
106
|
+
rotateLogIfNeeded(logPath);
|
|
38
107
|
const logFd = openSync(logPath, "a"); // append mode
|
|
39
108
|
|
|
40
109
|
// On Windows, wrap in shell for proper detach; quote path for spaces
|
|
@@ -59,18 +128,8 @@ export async function startCommand(options?: { mcp?: boolean }) {
|
|
|
59
128
|
console.log(`[afd] Targets: ${discovery.targets.join(", ")}`);
|
|
60
129
|
console.log(t(msg.DAEMON_LOGS, { path: logPath }));
|
|
61
130
|
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
for (const { adapter } of ecosystems) {
|
|
65
|
-
if (adapter.injectHooks) {
|
|
66
|
-
const hookResult = adapter.injectHooks(process.cwd());
|
|
67
|
-
console.log(`[afd] ${hookResult.message}`);
|
|
68
|
-
}
|
|
69
|
-
if (adapter.configureStatusLine) {
|
|
70
|
-
const slResult = adapter.configureStatusLine(process.cwd());
|
|
71
|
-
if (slResult.configured) console.log(`[afd] ${slResult.message}`);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
131
|
+
// One-Command Zero-Touch: auto-provision all ecosystem integrations
|
|
132
|
+
setupEcosystem(process.cwd(), msg);
|
|
74
133
|
} else {
|
|
75
134
|
console.error(t(msg.DAEMON_START_FAILED, { path: logPath }));
|
|
76
135
|
process.exit(1);
|