autonomous-flow-daemon 1.0.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 +142 -125
- package/README.md +119 -134
- package/package.json +11 -5
- package/src/adapters/index.ts +247 -35
- package/src/cli.ts +79 -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/lang.ts +41 -0
- package/src/commands/mcp.ts +129 -0
- package/src/commands/restart.ts +14 -0
- package/src/commands/score.ts +192 -64
- package/src/commands/start.ts +137 -37
- package/src/commands/stats.ts +103 -0
- package/src/commands/status.ts +157 -0
- package/src/commands/stop.ts +42 -9
- package/src/commands/sync.ts +253 -20
- package/src/commands/vaccine.ts +177 -0
- package/src/constants.ts +26 -1
- package/src/core/boast.ts +280 -0
- package/src/core/config.ts +49 -0
- package/src/core/db.ts +74 -3
- package/src/core/discovery.ts +65 -0
- 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 +309 -0
- package/src/core/immune.ts +8 -123
- package/src/core/locale.ts +88 -0
- 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 +53 -14
- 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 +492 -273
- package/src/daemon/types.ts +100 -0
- package/src/daemon/workspace-map.ts +92 -0
- package/src/platform.ts +60 -0
- 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
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
import { daemonRequest } from "../daemon/client";
|
|
2
|
+
import { fmtNum, visualWidth, localizedBoast } from "../core/boast";
|
|
3
|
+
import type { ShiftSummary } from "../core/boast";
|
|
4
|
+
import { getSystemLanguage } from "../core/locale";
|
|
5
|
+
import { getMessages, t } from "../core/i18n/messages";
|
|
2
6
|
|
|
3
|
-
interface
|
|
7
|
+
interface HologramEntry {
|
|
4
8
|
requests: number;
|
|
5
9
|
originalChars: number;
|
|
6
10
|
hologramChars: number;
|
|
7
11
|
savings: number;
|
|
8
12
|
}
|
|
9
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
|
+
|
|
10
27
|
interface AutoHealEntry {
|
|
11
28
|
id: string;
|
|
12
29
|
at: number;
|
|
@@ -28,6 +45,17 @@ interface EcosystemScore {
|
|
|
28
45
|
primary: string;
|
|
29
46
|
}
|
|
30
47
|
|
|
48
|
+
interface SuppressionScore {
|
|
49
|
+
massEventsSkipped: number;
|
|
50
|
+
dormantTransitions: number;
|
|
51
|
+
activeTaps: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface DynamicImmuneScore {
|
|
55
|
+
activeValidators: number;
|
|
56
|
+
validatorNames: string[];
|
|
57
|
+
}
|
|
58
|
+
|
|
31
59
|
interface ScoreData {
|
|
32
60
|
uptime: number;
|
|
33
61
|
filesDetected: number;
|
|
@@ -39,8 +67,27 @@ interface ScoreData {
|
|
|
39
67
|
hologram: HologramScore;
|
|
40
68
|
immune: ImmuneScore;
|
|
41
69
|
ecosystem: EcosystemScore;
|
|
70
|
+
suppression: SuppressionScore;
|
|
71
|
+
dynamicImmune?: DynamicImmuneScore;
|
|
42
72
|
}
|
|
43
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
|
+
|
|
44
91
|
function formatUptime(seconds: number): string {
|
|
45
92
|
if (seconds < 60) return `${seconds}s`;
|
|
46
93
|
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
@@ -49,9 +96,13 @@ function formatUptime(seconds: number): string {
|
|
|
49
96
|
return `${h}h ${m}m`;
|
|
50
97
|
}
|
|
51
98
|
|
|
52
|
-
function
|
|
53
|
-
const
|
|
54
|
-
|
|
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}%`;
|
|
55
106
|
}
|
|
56
107
|
|
|
57
108
|
function formatChars(n: number): string {
|
|
@@ -60,89 +111,166 @@ function formatChars(n: number): string {
|
|
|
60
111
|
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
61
112
|
}
|
|
62
113
|
|
|
63
|
-
const W =
|
|
64
|
-
const line = "
|
|
65
|
-
|
|
114
|
+
const W = 52;
|
|
115
|
+
const line = "─".repeat(W);
|
|
116
|
+
|
|
117
|
+
function row(content: string): string {
|
|
118
|
+
// Strip ANSI for width calculation
|
|
119
|
+
const stripped = content.replace(/\x1b\[[0-9;]*m/g, "");
|
|
120
|
+
const vw = visualWidth(stripped);
|
|
121
|
+
const padSize = Math.max(0, W - vw);
|
|
122
|
+
return `│${content}${" ".repeat(padSize)}│`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function section(title: string): string {
|
|
126
|
+
return row(` ${C.bold}${C.cyan}${title}${C.reset}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function kv(label: string, value: string): string {
|
|
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 };
|
|
142
|
+
}
|
|
66
143
|
|
|
67
144
|
export async function scoreCommand() {
|
|
145
|
+
const lang = getSystemLanguage();
|
|
146
|
+
const i18n = getMessages(lang);
|
|
147
|
+
|
|
68
148
|
try {
|
|
69
149
|
const data = await daemonRequest<ScoreData>("/score");
|
|
70
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}┤`);
|
|
71
160
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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}┤`);
|
|
165
|
+
|
|
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}`));
|
|
76
169
|
if (data.ecosystem.detected.length > 1) {
|
|
77
170
|
const others = data.ecosystem.detected.slice(1).map(e => e.name).join(", ");
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
console.log(`\u251C${line}\u2524`);
|
|
81
|
-
console.log(row(` Uptime : ${formatUptime(data.uptime)}`));
|
|
82
|
-
console.log(row(` Events : ${data.totalEvents}`));
|
|
83
|
-
console.log(row(` Files Found : ${data.watchedFiles.length}`));
|
|
84
|
-
console.log(`\u251C${line}\u2524`);
|
|
85
|
-
console.log(row(` Activity ${heatBar(data.totalEvents, 100)}`));
|
|
86
|
-
|
|
87
|
-
// Context Efficiency section
|
|
88
|
-
console.log(`\u251C${line}\u2524`);
|
|
89
|
-
console.log(row(" Context Efficiency (Hologram)"));
|
|
90
|
-
console.log(row(` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
|
|
91
|
-
if (h.requests > 0) {
|
|
92
|
-
const saved = h.originalChars - h.hologramChars;
|
|
93
|
-
console.log(row(` Requests : ${h.requests}`));
|
|
94
|
-
console.log(row(` Original : ${formatChars(h.originalChars)} chars`));
|
|
95
|
-
console.log(row(` Hologram : ${formatChars(h.hologramChars)} chars`));
|
|
96
|
-
console.log(row(` Saved : ${formatChars(saved)} chars (${h.savings}%)`));
|
|
97
|
-
console.log(row(` Efficiency ${heatBar(h.savings, 100)}`));
|
|
98
|
-
} else {
|
|
99
|
-
console.log(row(" No hologram requests yet."));
|
|
100
|
-
console.log(row(" Use: GET /hologram?file=<path>"));
|
|
171
|
+
out.push(kv(i18n.SCORE_ALSO_FOUND, `${C.dim}${others}${C.reset}`));
|
|
101
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}`));
|
|
102
176
|
|
|
103
|
-
// Immune System
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
console.log(row(` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
|
|
177
|
+
// ── Immune System ──
|
|
178
|
+
out.push(`├${line}┤`);
|
|
179
|
+
out.push(section(lang === "ko" ? "면역 시스템" : "Immune System"));
|
|
107
180
|
const ab = data.immune.antibodies;
|
|
108
181
|
const ah = data.immune.autoHealed;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
console.log(row(` Immunity ${heatBar(ab, 10)}`));
|
|
113
|
-
console.log(row(` Auto-healed : ${ah} background event${ah !== 1 ? "s" : ""}`));
|
|
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}`));
|
|
114
185
|
if (data.immune.lastAutoHeal) {
|
|
115
186
|
const ago = formatUptime(Math.floor((Date.now() - data.immune.lastAutoHeal.at) / 1000));
|
|
116
|
-
|
|
187
|
+
out.push(kv(lang === "ko" ? "마지막 치유" : "Last Heal", `${data.immune.lastAutoHeal.id} ${C.dim}(${ago} ago)${C.reset}`));
|
|
117
188
|
}
|
|
118
189
|
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
+
));
|
|
206
|
+
}
|
|
207
|
+
|
|
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}`));
|
|
128
220
|
}
|
|
129
221
|
} else {
|
|
130
|
-
|
|
222
|
+
out.push(row(` ${C.dim}${i18n.SCORE_HOLOGRAM_EMPTY}${C.reset}`));
|
|
223
|
+
out.push(row(` ${C.dim}${i18n.SCORE_HOLOGRAM_HINT}${C.reset}`));
|
|
131
224
|
}
|
|
132
225
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
226
|
+
// ── Value Delivered (ROI) ──
|
|
227
|
+
try {
|
|
228
|
+
const summary = await daemonRequest<ShiftSummary>("/shift-summary");
|
|
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
|
+
));
|
|
258
|
+
if (summary.suppressionsSkipped > 0) {
|
|
259
|
+
out.push(kv(lang === "ko" ? "억제 횟수" : "Suppressed", `${summary.suppressionsSkipped}`));
|
|
260
|
+
}
|
|
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}┘`);
|
|
141
269
|
|
|
142
|
-
console.log(
|
|
270
|
+
console.log(out.join("\n"));
|
|
143
271
|
} catch (err: unknown) {
|
|
144
272
|
const msg = err instanceof Error ? err.message : String(err);
|
|
145
|
-
console.error(
|
|
273
|
+
console.error(`${C.red}[afd] ${msg}${C.reset}`);
|
|
146
274
|
process.exit(1);
|
|
147
275
|
}
|
|
148
276
|
}
|
package/src/commands/start.ts
CHANGED
|
@@ -1,55 +1,155 @@
|
|
|
1
|
-
import { spawn } from "child_process";
|
|
2
1
|
import { resolve } from "path";
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import { openSync, mkdirSync } from "fs";
|
|
3
4
|
import { getDaemonInfo, isDaemonAlive } from "../daemon/client";
|
|
4
|
-
import {
|
|
5
|
-
import { mkdirSync } from "fs";
|
|
5
|
+
import { WATCH_TARGETS, resolveWorkspacePaths } from "../constants";
|
|
6
6
|
import { detectEcosystem } from "../adapters/index";
|
|
7
|
+
import type { DetectionResult } from "../adapters/index";
|
|
8
|
+
import { detachedSpawnOptions, IS_WINDOWS } from "../platform";
|
|
9
|
+
import { rotateLogIfNeeded } from "../core/log-rotate";
|
|
10
|
+
import { getSystemLanguage } from "../core/locale";
|
|
11
|
+
import { getMessages, t } from "../core/i18n/messages";
|
|
12
|
+
import type { MessageDict } from "../core/i18n/messages";
|
|
13
|
+
import { discoverWatchTargets } from "../core/discovery";
|
|
14
|
+
|
|
15
|
+
const STARTUP_POLL_INTERVAL_MS = 100;
|
|
16
|
+
const STARTUP_POLL_MAX_MS = 3000;
|
|
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"] = [];
|
|
7
41
|
|
|
8
|
-
|
|
9
|
-
|
|
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
|
+
}
|
|
10
78
|
|
|
11
|
-
|
|
79
|
+
return results;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function startCommand(options?: { mcp?: boolean }) {
|
|
83
|
+
// MCP stdio mode: run daemon in foreground with stdio transport
|
|
84
|
+
if (options?.mcp) {
|
|
85
|
+
const { main: runDaemon } = await import("../daemon/server");
|
|
86
|
+
runDaemon({ mcp: true });
|
|
87
|
+
return; // never reaches here — stdio loop blocks
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const lang = getSystemLanguage();
|
|
91
|
+
const msg = getMessages(lang);
|
|
92
|
+
|
|
93
|
+
const paths = resolveWorkspacePaths();
|
|
94
|
+
mkdirSync(paths.afdDir, { recursive: true });
|
|
95
|
+
|
|
96
|
+
// ── Idempotency: check if already running ──
|
|
12
97
|
const existing = getDaemonInfo();
|
|
13
|
-
if (existing && await isDaemonAlive(existing)) {
|
|
14
|
-
console.log(
|
|
98
|
+
if (existing && (await isDaemonAlive(existing))) {
|
|
99
|
+
console.log(msg.DAEMON_ALREADY_RUNNING);
|
|
15
100
|
return;
|
|
16
101
|
}
|
|
17
102
|
|
|
18
|
-
// Spawn detached daemon
|
|
103
|
+
// ── Spawn detached daemon with log redirection ──
|
|
19
104
|
const daemonScript = resolve(import.meta.dirname, "../daemon/server.ts");
|
|
20
|
-
const
|
|
105
|
+
const logPath = paths.logFile;
|
|
106
|
+
rotateLogIfNeeded(logPath);
|
|
107
|
+
const logFd = openSync(logPath, "a"); // append mode
|
|
108
|
+
|
|
109
|
+
// On Windows, wrap in shell for proper detach; quote path for spaces
|
|
110
|
+
const args = IS_WINDOWS
|
|
111
|
+
? ["run", `"${daemonScript}"`]
|
|
112
|
+
: ["run", daemonScript];
|
|
21
113
|
|
|
22
|
-
const child = spawn(
|
|
23
|
-
detached: true,
|
|
24
|
-
stdio: ["ignore", "ignore", "ignore"],
|
|
25
|
-
cwd: process.cwd(),
|
|
26
|
-
env: { ...process.env },
|
|
27
|
-
});
|
|
114
|
+
const child = spawn("bun", args, detachedSpawnOptions(logFd));
|
|
28
115
|
|
|
116
|
+
// Detach: allow parent to exit without killing child
|
|
29
117
|
child.unref();
|
|
30
118
|
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
if (adapter.configureStatusLine) {
|
|
47
|
-
const slResult = adapter.configureStatusLine(process.cwd());
|
|
48
|
-
if (slResult.configured) console.log(`[afd] ${slResult.message}`);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
119
|
+
// ── Poll for daemon readiness instead of fixed sleep ──
|
|
120
|
+
const info = await pollForDaemon(STARTUP_POLL_MAX_MS, STARTUP_POLL_INTERVAL_MS);
|
|
121
|
+
|
|
122
|
+
if (info) {
|
|
123
|
+
console.log(t(msg.DAEMON_STARTED, { pid: info.pid, port: info.port }));
|
|
124
|
+
|
|
125
|
+
// Smart Discovery: show what we're actually watching
|
|
126
|
+
const discovery = discoverWatchTargets(WATCH_TARGETS);
|
|
127
|
+
console.log(t(msg.DAEMON_WATCHING, { count: discovery.targets.length }));
|
|
128
|
+
console.log(`[afd] Targets: ${discovery.targets.join(", ")}`);
|
|
129
|
+
console.log(t(msg.DAEMON_LOGS, { path: logPath }));
|
|
130
|
+
|
|
131
|
+
// One-Command Zero-Touch: auto-provision all ecosystem integrations
|
|
132
|
+
setupEcosystem(process.cwd(), msg);
|
|
51
133
|
} else {
|
|
52
|
-
console.error(
|
|
134
|
+
console.error(t(msg.DAEMON_START_FAILED, { path: logPath }));
|
|
53
135
|
process.exit(1);
|
|
54
136
|
}
|
|
55
137
|
}
|
|
138
|
+
|
|
139
|
+
/** Poll until daemon PID/port files appear and health check passes */
|
|
140
|
+
async function pollForDaemon(
|
|
141
|
+
maxMs: number,
|
|
142
|
+
intervalMs: number,
|
|
143
|
+
): Promise<{ pid: number; port: number } | null> {
|
|
144
|
+
const deadline = Date.now() + maxMs;
|
|
145
|
+
|
|
146
|
+
while (Date.now() < deadline) {
|
|
147
|
+
const info = getDaemonInfo();
|
|
148
|
+
if (info && (await isDaemonAlive(info))) {
|
|
149
|
+
return info;
|
|
150
|
+
}
|
|
151
|
+
await Bun.sleep(intervalMs);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return null;
|
|
155
|
+
}
|