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.
Files changed (55) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.ko.md +124 -164
  3. package/README.md +99 -170
  4. package/package.json +11 -5
  5. package/src/adapters/index.ts +246 -35
  6. package/src/cli.ts +71 -1
  7. package/src/commands/benchmark.ts +187 -0
  8. package/src/commands/diagnose.ts +56 -14
  9. package/src/commands/doctor.ts +243 -0
  10. package/src/commands/evolution.ts +107 -0
  11. package/src/commands/fix.ts +22 -2
  12. package/src/commands/hooks.ts +136 -0
  13. package/src/commands/mcp.ts +129 -0
  14. package/src/commands/restart.ts +14 -0
  15. package/src/commands/score.ts +164 -96
  16. package/src/commands/start.ts +74 -15
  17. package/src/commands/stats.ts +103 -0
  18. package/src/commands/status.ts +157 -0
  19. package/src/commands/stop.ts +23 -4
  20. package/src/commands/sync.ts +253 -20
  21. package/src/commands/vaccine.ts +177 -0
  22. package/src/constants.ts +25 -1
  23. package/src/core/boast.ts +27 -12
  24. package/src/core/db.ts +74 -3
  25. package/src/core/evolution.ts +215 -0
  26. package/src/core/hologram/engine.ts +71 -0
  27. package/src/core/hologram/fallback.ts +11 -0
  28. package/src/core/hologram/incremental.ts +227 -0
  29. package/src/core/hologram/py-extractor.ts +132 -0
  30. package/src/core/hologram/ts-extractor.ts +320 -0
  31. package/src/core/hologram/types.ts +25 -0
  32. package/src/core/hologram.ts +64 -236
  33. package/src/core/hook-manager.ts +259 -0
  34. package/src/core/i18n/messages.ts +43 -0
  35. package/src/core/immune.ts +8 -123
  36. package/src/core/log-rotate.ts +33 -0
  37. package/src/core/log-utils.ts +38 -0
  38. package/src/core/lru-map.ts +61 -0
  39. package/src/core/notify.ts +27 -19
  40. package/src/core/rule-engine.ts +287 -0
  41. package/src/core/semantic-diff.ts +432 -0
  42. package/src/core/telemetry.ts +94 -0
  43. package/src/core/vaccine-registry.ts +212 -0
  44. package/src/core/workspace.ts +28 -0
  45. package/src/core/yaml-minimal.ts +176 -0
  46. package/src/daemon/client.ts +34 -6
  47. package/src/daemon/event-batcher.ts +108 -0
  48. package/src/daemon/guards.ts +13 -0
  49. package/src/daemon/http-routes.ts +293 -0
  50. package/src/daemon/mcp-handler.ts +270 -0
  51. package/src/daemon/server.ts +439 -353
  52. package/src/daemon/types.ts +100 -0
  53. package/src/daemon/workspace-map.ts +92 -0
  54. package/src/platform.ts +23 -2
  55. package/src/version.ts +15 -0
@@ -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
+ }
@@ -1,17 +1,19 @@
1
1
  import { getDaemonInfo, isDaemonAlive, daemonRequest } from "../daemon/client";
2
2
  import { unlinkSync } from "fs";
3
- import { PID_FILE, PORT_FILE } from "../constants";
3
+ import { resolveWorkspacePaths } from "../constants";
4
4
  import { formatShiftSummary } from "../core/boast";
5
5
  import type { ShiftSummary } from "../core/boast";
6
6
  import { getSystemLanguage } from "../core/locale";
7
7
  import { getMessages, t } from "../core/i18n/messages";
8
+ import { detectEcosystem } from "../adapters/index";
8
9
 
9
10
  function cleanupFiles() {
10
- try { unlinkSync(PID_FILE); } catch {}
11
- try { unlinkSync(PORT_FILE); } catch {}
11
+ const paths = resolveWorkspacePaths();
12
+ try { unlinkSync(paths.pidFile); } catch {}
13
+ try { unlinkSync(paths.portFile); } catch {}
12
14
  }
13
15
 
14
- export async function stopCommand() {
16
+ export async function stopCommand(options?: { clean?: boolean }) {
15
17
  const lang = getSystemLanguage();
16
18
  const msg = getMessages(lang);
17
19
  const info = getDaemonInfo();
@@ -46,4 +48,21 @@ export async function stopCommand() {
46
48
  }
47
49
 
48
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
+ }
49
68
  }
@@ -1,7 +1,8 @@
1
- import { readFileSync } from "fs";
2
- import { resolve } from "path";
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
2
+ import { resolve, join } from "path";
3
3
  import { daemonRequest } from "../daemon/client";
4
4
  import { AFD_DIR } from "../constants";
5
+ import { getSystemLanguage } from "../core/locale";
5
6
 
6
7
  interface SyncResponse {
7
8
  status: string;
@@ -9,7 +10,95 @@ interface SyncResponse {
9
10
  count: number;
10
11
  }
11
12
 
12
- export async function syncCommand() {
13
+ interface VaccinePayload {
14
+ version: string;
15
+ generatedAt: string;
16
+ ecosystem: string;
17
+ antibodyCount: number;
18
+ antibodies: VaccineAntibody[];
19
+ }
20
+
21
+ interface VaccineAntibody {
22
+ id: string;
23
+ patternType: string;
24
+ fileTarget: string;
25
+ patches: { op: string; path: string; value?: string }[];
26
+ learnedAt: string;
27
+ }
28
+
29
+ interface SyncOptions {
30
+ push?: boolean;
31
+ pull?: boolean;
32
+ remote?: string;
33
+ }
34
+
35
+ const msgs = {
36
+ en: {
37
+ title: "afd sync — Vaccine Network",
38
+ ecosystem: "Ecosystem",
39
+ antibodies: "Antibodies",
40
+ generated: "Generated",
41
+ payload: "Payload",
42
+ noAntibodies: "No antibodies to export. Run `afd fix` first.",
43
+ exported: "Vaccine payload generated.",
44
+ pushTitle: "afd sync --push",
45
+ pushSuccess: "Pushed to team vaccine store",
46
+ pushCreated: "Created team vaccine store",
47
+ pullTitle: "afd sync --pull",
48
+ pullSuccess: "Pulled from team vaccine store",
49
+ pullMerged: "merged",
50
+ pullNew: "new",
51
+ pullSkipped: "skipped (already known)",
52
+ pullNoStore: "No team vaccine store found.",
53
+ pullHint: "Run `afd sync --push` first to create the shared store.",
54
+ learnedVia: "Learned via pull",
55
+ ready: "antibody(ies) ready for global federation.",
56
+ },
57
+ ko: {
58
+ title: "afd sync — 백신 네트워크",
59
+ ecosystem: "에코시스템",
60
+ antibodies: "항체",
61
+ generated: "생성일",
62
+ payload: "페이로드",
63
+ noAntibodies: "내보낼 항체가 없습니다. `afd fix`를 먼저 실행하세요.",
64
+ exported: "백신 페이로드 생성 완료.",
65
+ pushTitle: "afd sync --push",
66
+ pushSuccess: "팀 백신 저장소에 푸시 완료",
67
+ pushCreated: "팀 백신 저장소 생성",
68
+ pullTitle: "afd sync --pull",
69
+ pullSuccess: "팀 백신 저장소에서 풀 완료",
70
+ pullMerged: "병합됨",
71
+ pullNew: "신규",
72
+ pullSkipped: "건너뜀 (이미 존재)",
73
+ pullNoStore: "팀 백신 저장소를 찾을 수 없습니다.",
74
+ pullHint: "`afd sync --push`를 먼저 실행하여 공유 저장소를 생성하세요.",
75
+ learnedVia: "풀로 학습됨",
76
+ ready: "개 항체가 글로벌 페더레이션 준비 완료.",
77
+ },
78
+ };
79
+
80
+ const TEAM_STORE_DIR = join(AFD_DIR, "team-vaccines");
81
+ const TEAM_PAYLOAD_FILE = join(TEAM_STORE_DIR, "shared-vaccine-payload.json");
82
+
83
+ export async function syncCommand(opts: SyncOptions = {}) {
84
+ const lang = getSystemLanguage();
85
+ const m = msgs[lang];
86
+
87
+ if (opts.push) {
88
+ await syncPush(m);
89
+ return;
90
+ }
91
+
92
+ if (opts.pull) {
93
+ await syncPull(m);
94
+ return;
95
+ }
96
+
97
+ // Default: export local payload (original behavior)
98
+ await syncExport(m);
99
+ }
100
+
101
+ async function syncExport(m: typeof msgs.en) {
13
102
  let result: SyncResponse;
14
103
  try {
15
104
  result = await daemonRequest<SyncResponse>("/sync");
@@ -20,31 +109,175 @@ export async function syncCommand() {
20
109
  }
21
110
 
22
111
  if (result.count === 0) {
23
- console.log("[afd sync] No antibodies to export. Run `afd fix` first to learn patterns.");
112
+ console.log(`[afd sync] ${m.noAntibodies}`);
24
113
  return;
25
114
  }
26
115
 
27
- // Read the generated payload for display
28
116
  const payloadPath = resolve(AFD_DIR, "global-vaccine-payload.json");
29
117
  const payload = JSON.parse(readFileSync(payloadPath, "utf-8"));
118
+ renderPayloadBox(payload, m);
119
+ console.log(`\n[afd sync] ${m.exported} ${result.count} ${m.ready}`);
120
+ }
121
+
122
+ async function syncPush(m: typeof msgs.en) {
123
+ // First, export latest payload
124
+ let result: SyncResponse;
125
+ try {
126
+ result = await daemonRequest<SyncResponse>("/sync");
127
+ } catch (err: unknown) {
128
+ const msg = err instanceof Error ? err.message : String(err);
129
+ console.error(`[afd sync] ${msg}`);
130
+ process.exit(1);
131
+ }
132
+
133
+ if (result.count === 0) {
134
+ console.log(`[afd sync] ${m.noAntibodies}`);
135
+ return;
136
+ }
137
+
138
+ // Copy to team store
139
+ const localPayloadPath = resolve(AFD_DIR, "global-vaccine-payload.json");
140
+ const localPayload = readFileSync(localPayloadPath, "utf-8");
141
+
142
+ mkdirSync(TEAM_STORE_DIR, { recursive: true });
143
+
144
+ // Merge with existing team payload if present
145
+ let teamPayload: VaccinePayload;
146
+ if (existsSync(TEAM_PAYLOAD_FILE)) {
147
+ try {
148
+ teamPayload = JSON.parse(readFileSync(TEAM_PAYLOAD_FILE, "utf-8"));
149
+ } catch {
150
+ teamPayload = JSON.parse(localPayload);
151
+ }
152
+ // Merge: add new antibodies, update existing ones
153
+ const newPayload = JSON.parse(localPayload) as VaccinePayload;
154
+ const existingIds = new Set(teamPayload.antibodies.map(a => a.id));
155
+ for (const ab of newPayload.antibodies) {
156
+ if (existingIds.has(ab.id)) {
157
+ // Update existing
158
+ const idx = teamPayload.antibodies.findIndex(a => a.id === ab.id);
159
+ if (idx >= 0) teamPayload.antibodies[idx] = ab;
160
+ } else {
161
+ teamPayload.antibodies.push(ab);
162
+ }
163
+ }
164
+ teamPayload.antibodyCount = teamPayload.antibodies.length;
165
+ teamPayload.generatedAt = new Date().toISOString();
166
+ } else {
167
+ teamPayload = JSON.parse(localPayload);
168
+ console.log(`[afd sync] ${m.pushCreated}`);
169
+ }
170
+
171
+ writeFileSync(TEAM_PAYLOAD_FILE, JSON.stringify(teamPayload, null, 2), "utf-8");
172
+
173
+ const BOX = { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│", ml: "├", mr: "┤" };
174
+ const W = 50;
175
+ const line = (l: string, r: string) => `${l}${BOX.h.repeat(W)}${r}`;
176
+ const row = (s: string) => `${BOX.v} ${s}${" ".repeat(Math.max(0, W - 2 - s.length))}${BOX.v}`;
177
+
178
+ console.log(line(BOX.tl, BOX.tr));
179
+ console.log(row(`📤 ${m.pushTitle}`));
180
+ console.log(line(BOX.ml, BOX.mr));
181
+ console.log(row(`${m.antibodies}: ${teamPayload.antibodyCount}`));
182
+ console.log(row(`${m.payload}: ${TEAM_PAYLOAD_FILE}`));
183
+ console.log(line(BOX.ml, BOX.mr));
184
+ console.log(row(`✅ ${m.pushSuccess}`));
185
+ console.log(line(BOX.bl, BOX.br));
186
+ }
187
+
188
+ async function syncPull(m: typeof msgs.en) {
189
+ if (!existsSync(TEAM_PAYLOAD_FILE)) {
190
+ console.log(`[afd sync] ${m.pullNoStore}`);
191
+ console.log(`[afd sync] ${m.pullHint}`);
192
+ return;
193
+ }
194
+
195
+ let teamPayload: VaccinePayload;
196
+ try {
197
+ teamPayload = JSON.parse(readFileSync(TEAM_PAYLOAD_FILE, "utf-8"));
198
+ } catch {
199
+ console.error("[afd sync] Failed to parse team vaccine payload.");
200
+ process.exit(1);
201
+ }
202
+
203
+ // Learn each antibody into the daemon
204
+ let newCount = 0;
205
+ let skippedCount = 0;
206
+ const results: { id: string; status: string }[] = [];
207
+
208
+ for (const ab of teamPayload.antibodies) {
209
+ try {
210
+ // Try to learn via daemon API
211
+ const res = await fetch(`http://127.0.0.1:${getDaemonPort()}/antibodies/learn`, {
212
+ method: "POST",
213
+ headers: { "Content-Type": "application/json" },
214
+ body: JSON.stringify({
215
+ id: ab.id,
216
+ patternType: ab.patternType,
217
+ fileTarget: ab.fileTarget,
218
+ patches: ab.patches,
219
+ }),
220
+ signal: AbortSignal.timeout(2000),
221
+ });
222
+ if (res.ok) {
223
+ results.push({ id: ab.id, status: m.pullNew });
224
+ newCount++;
225
+ } else {
226
+ results.push({ id: ab.id, status: m.pullSkipped });
227
+ skippedCount++;
228
+ }
229
+ } catch {
230
+ results.push({ id: ab.id, status: m.pullSkipped });
231
+ skippedCount++;
232
+ }
233
+ }
234
+
235
+ const BOX = { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│", ml: "├", mr: "┤" };
236
+ const W = 50;
237
+ const line = (l: string, r: string) => `${l}${BOX.h.repeat(W)}${r}`;
238
+ const row = (s: string) => `${BOX.v} ${s}${" ".repeat(Math.max(0, W - 2 - s.length))}${BOX.v}`;
239
+
240
+ console.log(line(BOX.tl, BOX.tr));
241
+ console.log(row(`📥 ${m.pullTitle}`));
242
+ console.log(line(BOX.ml, BOX.mr));
243
+
244
+ for (const r of results) {
245
+ const icon = r.status === m.pullNew ? "✅" : "⏭️";
246
+ console.log(row(`${icon} ${r.id}: ${r.status}`));
247
+ }
248
+
249
+ console.log(line(BOX.ml, BOX.mr));
250
+ console.log(row(`✅ ${m.pullSuccess}: ${newCount} ${m.pullMerged}`));
251
+ console.log(line(BOX.bl, BOX.br));
252
+ }
253
+
254
+ function getDaemonPort(): number {
255
+ const { getDaemonInfo } = require("../daemon/client");
256
+ const info = getDaemonInfo();
257
+ if (!info) throw new Error("Daemon not running");
258
+ return info.port;
259
+ }
260
+
261
+ function renderPayloadBox(payload: VaccinePayload, m: typeof msgs.en) {
262
+ const BOX = { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│", ml: "├", mr: "┤" };
263
+ const W = 48;
264
+ const line = (l: string, r: string) => `${l}${BOX.h.repeat(W)}${r}`;
265
+ const row = (s: string) => `${BOX.v} ${s}${" ".repeat(Math.max(0, W - 2 - s.length))}${BOX.v}`;
30
266
 
31
- const box = "\u2500".repeat(46);
32
- console.log(`\u250C${box}\u2510`);
33
- console.log(`\u2502 afd sync \u2014 Vaccine Network \u2502`);
34
- console.log(`\u251C${box}\u2524`);
35
- console.log(`\u2502 Ecosystem : ${payload.ecosystem.padEnd(31)}\u2502`);
36
- console.log(`\u2502 Antibodies : ${String(payload.antibodyCount).padEnd(31)}\u2502`);
37
- console.log(`\u2502 Generated : ${payload.generatedAt.substring(0, 19).padEnd(31)}\u2502`);
38
- console.log(`\u251C${box}\u2524`);
267
+ console.log(line(BOX.tl, BOX.tr));
268
+ console.log(row(`${m.title}`));
269
+ console.log(line(BOX.ml, BOX.mr));
270
+ console.log(row(`${m.ecosystem} : ${payload.ecosystem}`));
271
+ console.log(row(`${m.antibodies} : ${payload.antibodyCount}`));
272
+ console.log(row(`${m.generated} : ${payload.generatedAt.substring(0, 19)}`));
273
+ console.log(line(BOX.ml, BOX.mr));
39
274
 
40
275
  for (const ab of payload.antibodies) {
41
- const patches = ab.patches.map((p: { op: string; path: string }) => `${p.op} ${p.path}`).join(", ");
42
- console.log(`\u2502 [${ab.id}] ${ab.patternType.padEnd(20)} ${patches.substring(0, 12).padEnd(12)}\u2502`);
276
+ const patches = ab.patches.map(p => `${p.op} ${p.path}`).join(", ");
277
+ console.log(row(`[${ab.id}] ${ab.patternType.padEnd(18)} ${patches.substring(0, 14)}`));
43
278
  }
44
279
 
45
- console.log(`\u251C${box}\u2524`);
46
- console.log(`\u2502 Payload: .afd/global-vaccine-payload.json \u2502`);
47
- console.log(`\u2514${box}\u2518`);
48
- console.log();
49
- console.log(`[afd sync] Vaccine payload generated. ${result.count} antibody(ies) ready for global federation.`);
280
+ console.log(line(BOX.ml, BOX.mr));
281
+ console.log(row(`${m.payload}: .afd/global-vaccine-payload.json`));
282
+ console.log(line(BOX.bl, BOX.br));
50
283
  }