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
package/src/commands/start.ts
CHANGED
|
@@ -1,96 +1,155 @@
|
|
|
1
|
-
import { resolve } from "path";
|
|
2
|
-
import { spawn } from "child_process";
|
|
3
|
-
import { openSync, mkdirSync } from "fs";
|
|
4
|
-
import { getDaemonInfo, isDaemonAlive } from "../daemon/client";
|
|
5
|
-
import {
|
|
6
|
-
import { detectEcosystem } from "../adapters/index";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
1
|
+
import { resolve } from "path";
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import { openSync, mkdirSync } from "fs";
|
|
4
|
+
import { getDaemonInfo, isDaemonAlive } from "../daemon/client";
|
|
5
|
+
import { WATCH_TARGETS, resolveWorkspacePaths } from "../constants";
|
|
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"] = [];
|
|
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
|
+
|
|
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 ──
|
|
97
|
+
const existing = getDaemonInfo();
|
|
98
|
+
if (existing && (await isDaemonAlive(existing))) {
|
|
99
|
+
console.log(msg.DAEMON_ALREADY_RUNNING);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Spawn detached daemon with log redirection ──
|
|
104
|
+
const daemonScript = resolve(import.meta.dirname, "../daemon/server.ts");
|
|
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];
|
|
113
|
+
|
|
114
|
+
const child = spawn("bun", args, detachedSpawnOptions(logFd));
|
|
115
|
+
|
|
116
|
+
// Detach: allow parent to exit without killing child
|
|
117
|
+
child.unref();
|
|
118
|
+
|
|
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);
|
|
133
|
+
} else {
|
|
134
|
+
console.error(t(msg.DAEMON_START_FAILED, { path: logPath }));
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `afd stats` — Feature usage telemetry dashboard (developer-only).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { queryTelemetry, type TelemetrySummary } from "../core/telemetry";
|
|
6
|
+
|
|
7
|
+
const C = {
|
|
8
|
+
reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
|
|
9
|
+
red: "\x1b[31m", green: "\x1b[32m", yellow: "\x1b[33m",
|
|
10
|
+
blue: "\x1b[34m", magenta: "\x1b[35m", cyan: "\x1b[36m", white: "\x1b[37m",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function bar(count: number, max: number, width = 20): string {
|
|
14
|
+
const ratio = max > 0 ? Math.min(count / max, 1) : 0;
|
|
15
|
+
const filled = Math.round(ratio * width);
|
|
16
|
+
return `${C.green}${"█".repeat(filled)}${C.dim}${"░".repeat(width - filled)}${C.reset}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function sortedEntries(obj: Record<string, number>): [string, number][] {
|
|
20
|
+
return Object.entries(obj).sort((a, b) => b[1] - a[1]);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function renderRankedList(data: Record<string, number>, barWidth = 16): string[] {
|
|
24
|
+
const entries = sortedEntries(data);
|
|
25
|
+
if (entries.length === 0) return [` ${C.dim}(no data)${C.reset}`];
|
|
26
|
+
const maxVal = entries[0][1];
|
|
27
|
+
const maxNameLen = Math.max(...entries.map(([n]) => n.length), 8);
|
|
28
|
+
return entries.map(([name, count]) => {
|
|
29
|
+
const pad = " ".repeat(Math.max(1, maxNameLen - name.length + 2));
|
|
30
|
+
return ` ${C.white}${name}${C.reset}${pad}${bar(count, maxVal, barWidth)} ${count}`;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function renderSection(title: string): string {
|
|
35
|
+
return `\n${C.bold}${C.cyan}${title}${C.reset}\n${"─".repeat(50)}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function statsCommand(opts: { days?: string }) {
|
|
39
|
+
const days = parseInt(opts.days ?? "7", 10) || 7;
|
|
40
|
+
const data: TelemetrySummary = queryTelemetry(days);
|
|
41
|
+
|
|
42
|
+
const out: string[] = [];
|
|
43
|
+
|
|
44
|
+
out.push(`${C.bold}📊 Feature Usage Telemetry${C.reset} ${C.dim}(last ${days} days, ${data.totalEvents} events total)${C.reset}`);
|
|
45
|
+
|
|
46
|
+
// CLI Commands
|
|
47
|
+
out.push(renderSection("CLI Commands"));
|
|
48
|
+
out.push(...renderRankedList(data.cli));
|
|
49
|
+
|
|
50
|
+
// MCP Tools
|
|
51
|
+
out.push(renderSection("MCP Tools"));
|
|
52
|
+
out.push(...renderRankedList(data.mcp));
|
|
53
|
+
|
|
54
|
+
// S.E.A.M Cycle
|
|
55
|
+
out.push(renderSection("S.E.A.M Cycle"));
|
|
56
|
+
const seamEntries = sortedEntries(data.seam.counts);
|
|
57
|
+
if (seamEntries.length === 0) {
|
|
58
|
+
out.push(` ${C.dim}(no data)${C.reset}`);
|
|
59
|
+
} else {
|
|
60
|
+
const maxSeam = seamEntries[0][1];
|
|
61
|
+
const maxNameLen = Math.max(...seamEntries.map(([n]) => n.length), 8);
|
|
62
|
+
for (const [action, count] of seamEntries) {
|
|
63
|
+
const pad = " ".repeat(Math.max(1, maxNameLen - action.length + 2));
|
|
64
|
+
const avg = data.seam.avgDurationMs[action];
|
|
65
|
+
const avgStr = avg != null ? `${C.dim}avg ${avg}ms${C.reset}` : "";
|
|
66
|
+
out.push(` ${C.white}${action}${C.reset}${pad}${bar(count, maxSeam, 12)} ${count} ${avgStr}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Immune Activity + Accuracy
|
|
71
|
+
out.push(renderSection("Immune Activity"));
|
|
72
|
+
const hits = data.immune["heal_hit"] ?? 0;
|
|
73
|
+
const falsePos = data.immune["heal_false_positive"] ?? 0;
|
|
74
|
+
const passes = data.immune["heal_pass"] ?? 0;
|
|
75
|
+
const suppressions = data.immune["suppression"] ?? 0;
|
|
76
|
+
const totalJudgments = hits + falsePos + passes;
|
|
77
|
+
const accuracy = totalJudgments > 0 ? Math.round((hits + passes) / totalJudgments * 100) : null;
|
|
78
|
+
const precisionLabel = (hits + falsePos) > 0 ? `${Math.round(hits / (hits + falsePos) * 100)}%` : "—";
|
|
79
|
+
|
|
80
|
+
out.push(` ${C.white}Hits${C.reset} ${hits} ${C.dim}(corruption detected & restored)${C.reset}`);
|
|
81
|
+
out.push(` ${C.white}Passes${C.reset} ${passes} ${C.dim}(immune file changed, valid)${C.reset}`);
|
|
82
|
+
out.push(` ${C.white}False +${C.reset} ${falsePos} ${C.dim}(restored but user overrode)${C.reset}`);
|
|
83
|
+
out.push(` ${C.white}Suppress${C.reset} ${suppressions} ${C.dim}(mass event skip)${C.reset}`);
|
|
84
|
+
out.push("");
|
|
85
|
+
out.push(` ${C.bold}Accuracy${C.reset} ${accuracy != null ? `${C.green}${accuracy}%${C.reset}` : `${C.dim}—${C.reset}`} ${C.dim}(correct judgments / total)${C.reset}`);
|
|
86
|
+
out.push(` ${C.bold}Precision${C.reset} ${(hits + falsePos) > 0 ? `${C.green}${precisionLabel}${C.reset}` : `${C.dim}—${C.reset}`} ${C.dim}(true hits / all blocks)${C.reset}`);
|
|
87
|
+
|
|
88
|
+
// Validators
|
|
89
|
+
if (Object.keys(data.validator).length > 0) {
|
|
90
|
+
out.push(renderSection("Validator Triggers"));
|
|
91
|
+
out.push(...renderRankedList(data.validator));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Dead features warning
|
|
95
|
+
const allCli = ["start", "stop", "restart", "status", "score", "fix", "sync", "doctor", "diagnose", "vaccine", "evolution", "mcp", "lang", "stats"];
|
|
96
|
+
const unusedCli = allCli.filter(cmd => !(cmd in data.cli));
|
|
97
|
+
if (unusedCli.length > 0 && Object.keys(data.cli).length > 0) {
|
|
98
|
+
out.push(renderSection("Unused CLI Commands"));
|
|
99
|
+
out.push(` ${C.yellow}${unusedCli.join(", ")}${C.reset}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log(out.join("\n"));
|
|
103
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { getDaemonInfo, isDaemonAlive, daemonRequest } from "../daemon/client";
|
|
4
|
+
import { resolveWorkspacePaths } from "../constants";
|
|
5
|
+
import { getSystemLanguage } from "../core/locale";
|
|
6
|
+
|
|
7
|
+
const C = {
|
|
8
|
+
reset: "\x1b[0m",
|
|
9
|
+
bold: "\x1b[1m",
|
|
10
|
+
dim: "\x1b[2m",
|
|
11
|
+
red: "\x1b[31m",
|
|
12
|
+
green: "\x1b[32m",
|
|
13
|
+
yellow: "\x1b[33m",
|
|
14
|
+
cyan: "\x1b[36m",
|
|
15
|
+
white: "\x1b[37m",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const ko = getSystemLanguage() === "ko";
|
|
19
|
+
|
|
20
|
+
interface HealthData {
|
|
21
|
+
status: string;
|
|
22
|
+
pid: number;
|
|
23
|
+
workspace: string;
|
|
24
|
+
port: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ScoreData {
|
|
28
|
+
uptime: number;
|
|
29
|
+
immune: { antibodies: number; autoHealed: number };
|
|
30
|
+
ecosystem: { primary: string };
|
|
31
|
+
dynamicImmune?: { activeValidators: number };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function checkHooksInjected(): boolean {
|
|
35
|
+
const hooksPath = join(resolveWorkspacePaths().root, ".claude/hooks.json");
|
|
36
|
+
if (!existsSync(hooksPath)) return false;
|
|
37
|
+
try {
|
|
38
|
+
const content = readFileSync(hooksPath, "utf-8");
|
|
39
|
+
return content.includes("afd-auto-heal");
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function checkMcpRegistered(): boolean {
|
|
46
|
+
const mcpPath = join(resolveWorkspacePaths().root, ".mcp.json");
|
|
47
|
+
if (!existsSync(mcpPath)) return false;
|
|
48
|
+
try {
|
|
49
|
+
const content = readFileSync(mcpPath, "utf-8");
|
|
50
|
+
return content.includes('"afd"');
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getQuarantinedFiles(): string[] {
|
|
57
|
+
const paths = resolveWorkspacePaths();
|
|
58
|
+
if (!existsSync(paths.quarantineDir)) return [];
|
|
59
|
+
try {
|
|
60
|
+
return readdirSync(paths.quarantineDir).sort().reverse();
|
|
61
|
+
} catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function formatUptime(seconds: number): string {
|
|
67
|
+
if (seconds < 60) return `${seconds}s`;
|
|
68
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
69
|
+
const h = Math.floor(seconds / 3600);
|
|
70
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
71
|
+
return `${h}h ${m}m`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function indicator(ok: boolean): string {
|
|
75
|
+
return ok ? `${C.green}●${C.reset}` : `${C.red}●${C.reset}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function statusCommand() {
|
|
79
|
+
const out: string[] = [];
|
|
80
|
+
|
|
81
|
+
out.push("");
|
|
82
|
+
out.push(`${C.bold}afd status${C.reset}`);
|
|
83
|
+
out.push("");
|
|
84
|
+
|
|
85
|
+
// ── 1. Daemon ──
|
|
86
|
+
const info = getDaemonInfo();
|
|
87
|
+
if (!info || !(await isDaemonAlive(info))) {
|
|
88
|
+
out.push(` ${C.red}●${C.reset} ${C.bold}Daemon${C.reset} ${C.red}STOPPED${C.reset}`);
|
|
89
|
+
out.push("");
|
|
90
|
+
out.push(` ${C.dim}${ko ? "→ afd start 를 실행하세요" : "→ Run afd start to activate"}${C.reset}`);
|
|
91
|
+
out.push("");
|
|
92
|
+
console.log(out.join("\n"));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Fetch live data
|
|
97
|
+
let score: ScoreData | null = null;
|
|
98
|
+
try {
|
|
99
|
+
score = await daemonRequest<ScoreData>("/score");
|
|
100
|
+
} catch { /* use fallback */ }
|
|
101
|
+
|
|
102
|
+
const uptime = score ? formatUptime(score.uptime) : "?";
|
|
103
|
+
const ecosystem = score?.ecosystem.primary ?? "Unknown";
|
|
104
|
+
|
|
105
|
+
out.push(` ${C.green}●${C.reset} ${C.bold}Daemon${C.reset} ${C.green}ACTIVE${C.reset} ${C.dim}(pid=${info.pid} port=${info.port})${C.reset}`);
|
|
106
|
+
out.push(` ${C.dim} Uptime${C.reset} ${uptime} ${C.dim}|${C.reset} ${ecosystem}`);
|
|
107
|
+
out.push("");
|
|
108
|
+
|
|
109
|
+
// ── 2. Connections ──
|
|
110
|
+
out.push(` ${C.bold}${ko ? "연결 상태" : "Connections"}${C.reset}`);
|
|
111
|
+
|
|
112
|
+
const hooksOk = checkHooksInjected();
|
|
113
|
+
out.push(` ${indicator(hooksOk)} Hook ${hooksOk ? `${C.green}INJECTED${C.reset}` : `${C.red}MISSING${C.reset} ${C.dim}(afd start로 주입)${C.reset}`}`);
|
|
114
|
+
|
|
115
|
+
const mcpOk = checkMcpRegistered();
|
|
116
|
+
out.push(` ${indicator(mcpOk)} MCP ${mcpOk ? `${C.green}REGISTERED${C.reset}` : `${C.yellow}NOT SET${C.reset}`}`);
|
|
117
|
+
|
|
118
|
+
out.push("");
|
|
119
|
+
|
|
120
|
+
// ── 3. Defenses ──
|
|
121
|
+
out.push(` ${C.bold}${ko ? "방어막" : "Defenses"}${C.reset}`);
|
|
122
|
+
|
|
123
|
+
const antibodies = score?.immune.antibodies ?? 0;
|
|
124
|
+
const healed = score?.immune.autoHealed ?? 0;
|
|
125
|
+
const validators = score?.dynamicImmune?.activeValidators ?? 0;
|
|
126
|
+
|
|
127
|
+
out.push(` ${indicator(antibodies > 0)} ${ko ? "항체" : "Antibodies"} ${C.bold}${antibodies}${C.reset} ${ko ? "활성" : "active"}${healed > 0 ? ` ${C.dim}(${healed}${ko ? "회 치유" : " healed"})${C.reset}` : ""}`);
|
|
128
|
+
out.push(` ${indicator(validators > 0)} ${ko ? "검증기" : "Validators"} ${validators > 0 ? `${C.bold}${validators}${C.reset} ${ko ? "로드됨" : "loaded"}` : `${C.dim}${ko ? "없음" : "none"}${C.reset} ${C.dim}(.afd/validators/)${C.reset}`}`);
|
|
129
|
+
|
|
130
|
+
out.push("");
|
|
131
|
+
|
|
132
|
+
// ── 4. Quarantine ──
|
|
133
|
+
const quarantined = getQuarantinedFiles();
|
|
134
|
+
|
|
135
|
+
if (quarantined.length > 0) {
|
|
136
|
+
out.push(` ${C.yellow}⚠${C.reset} ${C.bold}${C.yellow}${ko ? "격리 구역" : "Quarantine"}${C.reset} ${C.dim}(${quarantined.length} ${ko ? "파일" : "file"}${quarantined.length !== 1 ? "s" : ""})${C.reset}`);
|
|
137
|
+
|
|
138
|
+
const show = quarantined.slice(0, 5);
|
|
139
|
+
for (const file of show) {
|
|
140
|
+
out.push(` ${C.dim}${file}${C.reset}`);
|
|
141
|
+
}
|
|
142
|
+
if (quarantined.length > 5) {
|
|
143
|
+
out.push(` ${C.dim}... +${quarantined.length - 5} more${C.reset}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
out.push("");
|
|
147
|
+
out.push(` ${C.dim}💡 ${ko
|
|
148
|
+
? "격리된 파일에서 코드를 구출하거나 불필요하면 삭제하세요."
|
|
149
|
+
: "Rescue code from quarantined files or delete if unneeded."}${C.reset}`);
|
|
150
|
+
out.push(` ${C.dim} ${ko ? "경로" : "Path"}: .afd/quarantine/${C.reset}`);
|
|
151
|
+
} else {
|
|
152
|
+
out.push(` ${C.green}●${C.reset} ${ko ? "격리 구역" : "Quarantine"} ${C.dim}${ko ? "비어있음 — 이상 없음" : "empty — all clear"}${C.reset}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
out.push("");
|
|
156
|
+
console.log(out.join("\n"));
|
|
157
|
+
}
|
package/src/commands/stop.ts
CHANGED
|
@@ -1,49 +1,68 @@
|
|
|
1
|
-
import { getDaemonInfo, isDaemonAlive, daemonRequest } from "../daemon/client";
|
|
2
|
-
import { unlinkSync } from "fs";
|
|
3
|
-
import {
|
|
4
|
-
import { formatShiftSummary } from "../core/boast";
|
|
5
|
-
import type { ShiftSummary } from "../core/boast";
|
|
6
|
-
import { getSystemLanguage } from "../core/locale";
|
|
7
|
-
import { getMessages, t } from "../core/i18n/messages";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
console.log(
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
1
|
+
import { getDaemonInfo, isDaemonAlive, daemonRequest } from "../daemon/client";
|
|
2
|
+
import { unlinkSync } from "fs";
|
|
3
|
+
import { resolveWorkspacePaths } from "../constants";
|
|
4
|
+
import { formatShiftSummary } from "../core/boast";
|
|
5
|
+
import type { ShiftSummary } from "../core/boast";
|
|
6
|
+
import { getSystemLanguage } from "../core/locale";
|
|
7
|
+
import { getMessages, t } from "../core/i18n/messages";
|
|
8
|
+
import { detectEcosystem } from "../adapters/index";
|
|
9
|
+
|
|
10
|
+
function cleanupFiles() {
|
|
11
|
+
const paths = resolveWorkspacePaths();
|
|
12
|
+
try { unlinkSync(paths.pidFile); } catch {}
|
|
13
|
+
try { unlinkSync(paths.portFile); } catch {}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function stopCommand(options?: { clean?: boolean }) {
|
|
17
|
+
const lang = getSystemLanguage();
|
|
18
|
+
const msg = getMessages(lang);
|
|
19
|
+
const info = getDaemonInfo();
|
|
20
|
+
|
|
21
|
+
if (!info) {
|
|
22
|
+
console.log(msg.DAEMON_NOT_RUNNING);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (await isDaemonAlive(info)) {
|
|
27
|
+
// Fetch shift summary before stopping
|
|
28
|
+
try {
|
|
29
|
+
const summary = await daemonRequest<ShiftSummary>("/shift-summary");
|
|
30
|
+
console.log(formatShiftSummary(summary, lang));
|
|
31
|
+
} catch {
|
|
32
|
+
// Non-fatal: summary is a nicety, not a requirement
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
await daemonRequest("/stop");
|
|
37
|
+
console.log(t(msg.DAEMON_STOPPED, { pid: info.pid }));
|
|
38
|
+
} catch {
|
|
39
|
+
try {
|
|
40
|
+
process.kill(info.pid, "SIGTERM");
|
|
41
|
+
console.log(t(msg.DAEMON_KILLED, { pid: info.pid }));
|
|
42
|
+
} catch {
|
|
43
|
+
console.log("[afd] Daemon process already gone.");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
console.log(msg.DAEMON_NOT_RESPONDING);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
cleanupFiles();
|
|
51
|
+
|
|
52
|
+
// --clean: remove injected hooks and MCP registration
|
|
53
|
+
if (options?.clean) {
|
|
54
|
+
const cwd = process.cwd();
|
|
55
|
+
const ecosystems = detectEcosystem(cwd);
|
|
56
|
+
for (const { adapter } of ecosystems) {
|
|
57
|
+
if (adapter.removeHooks) {
|
|
58
|
+
const r = adapter.removeHooks(cwd);
|
|
59
|
+
if (r.removed) console.log(`[afd] ${r.message}`);
|
|
60
|
+
}
|
|
61
|
+
if (adapter.unregisterMcp) {
|
|
62
|
+
const r = adapter.unregisterMcp(cwd);
|
|
63
|
+
if (r.removed) console.log(`[afd] ${r.message}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
console.log("[afd] Clean stop complete. All afd integrations removed.");
|
|
67
|
+
}
|
|
68
|
+
}
|