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.
Files changed (75) hide show
  1. package/CHANGELOG.md +85 -46
  2. package/LICENSE +21 -21
  3. package/README-ko.md +282 -0
  4. package/README.md +282 -337
  5. package/mcp-config.json +10 -10
  6. package/package.json +14 -6
  7. package/src/adapters/index.ts +370 -159
  8. package/src/cli.ts +162 -57
  9. package/src/commands/benchmark.ts +187 -0
  10. package/src/commands/correlate.ts +180 -0
  11. package/src/commands/dashboard.ts +404 -0
  12. package/src/commands/diagnose.ts +56 -14
  13. package/src/commands/doctor.ts +243 -0
  14. package/src/commands/evolution.ts +190 -0
  15. package/src/commands/fix.ts +158 -138
  16. package/src/commands/hooks.ts +136 -0
  17. package/src/commands/lang.ts +41 -41
  18. package/src/commands/mcp.ts +129 -0
  19. package/src/commands/plugin.ts +110 -0
  20. package/src/commands/restart.ts +14 -0
  21. package/src/commands/score.ts +276 -208
  22. package/src/commands/start.ts +155 -96
  23. package/src/commands/stats.ts +103 -0
  24. package/src/commands/status.ts +157 -0
  25. package/src/commands/stop.ts +68 -49
  26. package/src/commands/suggest.ts +211 -0
  27. package/src/commands/sync.ts +567 -21
  28. package/src/commands/vaccine.ts +177 -0
  29. package/src/constants.ts +32 -8
  30. package/src/core/boast.ts +280 -265
  31. package/src/core/config.ts +49 -49
  32. package/src/core/correlation-engine.ts +265 -0
  33. package/src/core/db.ts +145 -46
  34. package/src/core/discovery.ts +65 -65
  35. package/src/core/evolution.ts +215 -0
  36. package/src/core/federation.ts +129 -0
  37. package/src/core/hologram/engine.ts +71 -0
  38. package/src/core/hologram/fallback.ts +11 -0
  39. package/src/core/hologram/go-extractor.ts +203 -0
  40. package/src/core/hologram/incremental.ts +227 -0
  41. package/src/core/hologram/py-extractor.ts +132 -0
  42. package/src/core/hologram/rust-extractor.ts +244 -0
  43. package/src/core/hologram/ts-extractor.ts +406 -0
  44. package/src/core/hologram/types.ts +27 -0
  45. package/src/core/hologram.ts +73 -243
  46. package/src/core/hook-manager.ts +259 -0
  47. package/src/core/i18n/messages.ts +309 -266
  48. package/src/core/immune.ts +8 -123
  49. package/src/core/locale.ts +88 -88
  50. package/src/core/log-rotate.ts +33 -0
  51. package/src/core/log-utils.ts +38 -0
  52. package/src/core/lru-map.ts +61 -0
  53. package/src/core/notify.ts +74 -66
  54. package/src/core/plugin-manager.ts +225 -0
  55. package/src/core/rule-engine.ts +287 -0
  56. package/src/core/rule-suggestion.ts +127 -0
  57. package/src/core/semantic-diff.ts +432 -0
  58. package/src/core/telemetry.ts +94 -0
  59. package/src/core/vaccine-registry.ts +212 -0
  60. package/src/core/validator-generator.ts +224 -0
  61. package/src/core/workspace.ts +28 -0
  62. package/src/core/yaml-minimal.ts +176 -0
  63. package/src/daemon/client.ts +78 -37
  64. package/src/daemon/event-batcher.ts +108 -0
  65. package/src/daemon/guards.ts +13 -0
  66. package/src/daemon/http-routes.ts +376 -0
  67. package/src/daemon/mcp-handler.ts +575 -0
  68. package/src/daemon/mcp-subscriptions.ts +81 -0
  69. package/src/daemon/mesh.ts +51 -0
  70. package/src/daemon/server.ts +655 -504
  71. package/src/daemon/types.ts +121 -0
  72. package/src/daemon/workspace-map.ts +104 -0
  73. package/src/platform.ts +60 -39
  74. package/src/version.ts +15 -0
  75. package/README.ko.md +0 -306
@@ -1,7 +1,10 @@
1
- import { readFileSync } from "fs";
2
- import { resolve } from "path";
3
- import { daemonRequest } from "../daemon/client";
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
2
+ import { resolve, join } from "path";
3
+ import { daemonRequest, getMeshPeers } from "../daemon/client";
4
4
  import { AFD_DIR } from "../constants";
5
+ import { getSystemLanguage } from "../core/locale";
6
+ import { resolveScope } from "../core/federation";
7
+ import type { FederatedAntibody, FederatedPayload } from "../core/federation";
5
8
 
6
9
  interface SyncResponse {
7
10
  status: string;
@@ -9,7 +12,249 @@ interface SyncResponse {
9
12
  count: number;
10
13
  }
11
14
 
12
- export async function syncCommand() {
15
+ // Re-export federated types under legacy names for local push/pull compatibility
16
+ type VaccinePayload = FederatedPayload;
17
+ type VaccineAntibody = FederatedAntibody;
18
+
19
+ interface SyncOptions {
20
+ push?: boolean;
21
+ pull?: boolean;
22
+ remote?: string;
23
+ localMesh?: boolean;
24
+ }
25
+
26
+ const msgs = {
27
+ en: {
28
+ title: "afd sync — Vaccine Network",
29
+ ecosystem: "Ecosystem",
30
+ antibodies: "Antibodies",
31
+ generated: "Generated",
32
+ payload: "Payload",
33
+ noAntibodies: "No antibodies to export. Run `afd fix` first.",
34
+ exported: "Vaccine payload generated.",
35
+ pushTitle: "afd sync --push",
36
+ pushSuccess: "Pushed to team vaccine store",
37
+ pushCreated: "Created team vaccine store",
38
+ pullTitle: "afd sync --pull",
39
+ pullSuccess: "Pulled from team vaccine store",
40
+ pullMerged: "merged",
41
+ pullNew: "new",
42
+ pullSkipped: "skipped (already known)",
43
+ pullNoStore: "No team vaccine store found.",
44
+ pullHint: "Run `afd sync --push` first to create the shared store.",
45
+ learnedVia: "Learned via pull",
46
+ ready: "antibody(ies) ready for global federation.",
47
+ // Remote
48
+ remotePushTitle: "afd sync --push --remote",
49
+ remotePushSuccess: "Pushed to remote vaccine store",
50
+ remotePullTitle: "afd sync --pull --remote",
51
+ remotePullSuccess: "Pulled from remote vaccine store",
52
+ remoteSyncTitle: "afd sync --remote (bidirectional)",
53
+ remoteInvalidUrl: "Invalid URL. Must start with http:// or https://",
54
+ remoteTimeout: "Request timed out (10s)",
55
+ remoteNetworkError: "Network error",
56
+ remoteInvalidResponse: "Invalid response payload from remote",
57
+ remoteStatusError: "Remote returned error status",
58
+ },
59
+ ko: {
60
+ title: "afd sync — 백신 네트워크",
61
+ ecosystem: "에코시스템",
62
+ antibodies: "항체",
63
+ generated: "생성일",
64
+ payload: "페이로드",
65
+ noAntibodies: "내보낼 항체가 없습니다. `afd fix`를 먼저 실행하세요.",
66
+ exported: "백신 페이로드 생성 완료.",
67
+ pushTitle: "afd sync --push",
68
+ pushSuccess: "팀 백신 저장소에 푸시 완료",
69
+ pushCreated: "팀 백신 저장소 생성",
70
+ pullTitle: "afd sync --pull",
71
+ pullSuccess: "팀 백신 저장소에서 풀 완료",
72
+ pullMerged: "병합됨",
73
+ pullNew: "신규",
74
+ pullSkipped: "건너뜀 (이미 존재)",
75
+ pullNoStore: "팀 백신 저장소를 찾을 수 없습니다.",
76
+ pullHint: "`afd sync --push`를 먼저 실행하여 공유 저장소를 생성하세요.",
77
+ learnedVia: "풀로 학습됨",
78
+ ready: "개 항체가 글로벌 페더레이션 준비 완료.",
79
+ // 원격
80
+ remotePushTitle: "afd sync --push --remote",
81
+ remotePushSuccess: "원격 백신 저장소에 푸시 완료",
82
+ remotePullTitle: "afd sync --pull --remote",
83
+ remotePullSuccess: "원격 백신 저장소에서 풀 완료",
84
+ remoteSyncTitle: "afd sync --remote (양방향 동기화)",
85
+ remoteInvalidUrl: "올바르지 않은 URL입니다. http:// 또는 https://로 시작해야 합니다.",
86
+ remoteTimeout: "요청 시간 초과 (10초)",
87
+ remoteNetworkError: "네트워크 오류",
88
+ remoteInvalidResponse: "원격 서버의 응답 페이로드가 올바르지 않습니다.",
89
+ remoteStatusError: "원격 서버가 오류 상태를 반환했습니다.",
90
+ },
91
+ };
92
+
93
+ const TEAM_STORE_DIR = join(AFD_DIR, "team-vaccines");
94
+ const TEAM_PAYLOAD_FILE = join(TEAM_STORE_DIR, "shared-vaccine-payload.json");
95
+
96
+ export async function syncCommand(opts: SyncOptions = {}) {
97
+ const lang = getSystemLanguage();
98
+ const m = msgs[lang];
99
+
100
+ if (opts.localMesh) {
101
+ await syncLocalMesh(m);
102
+ return;
103
+ }
104
+
105
+ if (opts.remote) {
106
+ const url = validateRemoteUrl(opts.remote, m);
107
+ if (!url) return;
108
+
109
+ if (opts.push && !opts.pull) {
110
+ await syncRemotePush(url, m);
111
+ } else if (opts.pull && !opts.push) {
112
+ await syncRemotePull(url, m);
113
+ } else {
114
+ // --remote alone (or both flags): bidirectional — pull first, then push
115
+ await syncRemotePull(url, m);
116
+ await syncRemotePush(url, m);
117
+ }
118
+ return;
119
+ }
120
+
121
+ if (opts.push) {
122
+ await syncPush(m);
123
+ return;
124
+ }
125
+
126
+ if (opts.pull) {
127
+ await syncPull(m);
128
+ return;
129
+ }
130
+
131
+ // Default: export local payload (original behavior)
132
+ await syncExport(m);
133
+ }
134
+
135
+ // ─── Local mesh sync ─────────────────────────────────────────────────────────
136
+
137
+ /**
138
+ * Bidirectional antibody sync across all live mesh peers (monorepo daemons).
139
+ * For each peer:
140
+ * 1. Pull their antibodies → POST to our /antibodies/learn
141
+ * 2. Push our antibodies → POST to their /antibodies/learn
142
+ * Conflict arbitration is handled by shouldAcceptRemote() on each daemon's side.
143
+ */
144
+ async function syncLocalMesh(m: typeof msgs.en) {
145
+ const BOX = { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│", ml: "├", mr: "┤" };
146
+ const W = 60;
147
+ const hline = (l: string, r: string) => `${l}${BOX.h.repeat(W)}${r}`;
148
+ const row = (s: string) => `${BOX.v} ${s}${" ".repeat(Math.max(0, W - 2 - s.length))} ${BOX.v}`;
149
+
150
+ let peers: Awaited<ReturnType<typeof getMeshPeers>>;
151
+ try {
152
+ peers = await getMeshPeers();
153
+ } catch {
154
+ console.error("[afd sync] Daemon not running. Start with `afd start`.");
155
+ process.exit(1);
156
+ }
157
+
158
+ console.log(hline(BOX.tl, BOX.tr));
159
+ console.log(row(`🔗 afd sync --local-mesh`));
160
+ console.log(hline(BOX.ml, BOX.mr));
161
+
162
+ if (peers.length === 0) {
163
+ console.log(row("No live mesh peers found. Start daemons in other workspaces."));
164
+ console.log(hline(BOX.bl, BOX.br));
165
+ return;
166
+ }
167
+
168
+ // Fetch our own antibodies once
169
+ const ours = (await daemonRequest<{ antibodies: AntibodyRow[] }>("/antibodies")).antibodies;
170
+
171
+ let totalPulled = 0;
172
+ let totalPushed = 0;
173
+
174
+ for (const peer of peers) {
175
+ const baseUrl = `http://127.0.0.1:${peer.port}`;
176
+ console.log(row(`Peer: ${peer.workspace} (port ${peer.port})`));
177
+
178
+ // 1. Pull from peer
179
+ try {
180
+ const theirData = await fetchJson<{ antibodies: AntibodyRow[] }>(`${baseUrl}/antibodies`);
181
+ for (const ab of theirData.antibodies) {
182
+ await daemonRequest<unknown>("/antibodies/learn", "POST", {
183
+ id: ab.id,
184
+ patternType: ab.pattern_type,
185
+ fileTarget: ab.file_target,
186
+ patches: JSON.parse(ab.patch_op),
187
+ scope: ab.scope ?? "local",
188
+ version: ab.ab_version ?? 1,
189
+ updatedAt: ab.updated_at,
190
+ });
191
+ totalPulled++;
192
+ }
193
+ } catch {
194
+ console.log(row(` ⚠ Pull failed (peer may be busy)`));
195
+ }
196
+
197
+ // 2. Push to peer
198
+ try {
199
+ for (const ab of ours) {
200
+ await fetchJson<unknown>(`${baseUrl}/antibodies/learn`, {
201
+ method: "POST",
202
+ body: JSON.stringify({
203
+ id: ab.id,
204
+ patternType: ab.pattern_type,
205
+ fileTarget: ab.file_target,
206
+ patches: JSON.parse(ab.patch_op),
207
+ scope: ab.scope ?? "local",
208
+ version: ab.ab_version ?? 1,
209
+ updatedAt: ab.updated_at,
210
+ }),
211
+ headers: { "Content-Type": "application/json" },
212
+ });
213
+ totalPushed++;
214
+ }
215
+ } catch {
216
+ console.log(row(` ⚠ Push failed (peer may be busy)`));
217
+ }
218
+ }
219
+
220
+ console.log(hline(BOX.ml, BOX.mr));
221
+ console.log(row(`✅ Synced ${peers.length} peer(s) — pulled ${totalPulled}, pushed ${totalPushed}`));
222
+ console.log(hline(BOX.bl, BOX.br));
223
+ }
224
+
225
+ interface AntibodyRow {
226
+ id: string;
227
+ pattern_type: string;
228
+ file_target: string;
229
+ patch_op: string;
230
+ scope: string;
231
+ ab_version: number;
232
+ updated_at: string;
233
+ }
234
+
235
+ async function fetchJson<T>(url: string, init?: RequestInit): Promise<T> {
236
+ const res = await fetch(url, init);
237
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
238
+ return res.json() as T;
239
+ }
240
+
241
+ // ─── Remote helpers ───────────────────────────────────────────────────────────
242
+
243
+ const REMOTE_TIMEOUT_MS = 10_000;
244
+
245
+ function validateRemoteUrl(raw: string, m: typeof msgs.en): string | null {
246
+ try {
247
+ const u = new URL(raw);
248
+ if (u.protocol !== "http:" && u.protocol !== "https:") throw new Error();
249
+ return u.toString();
250
+ } catch {
251
+ console.error(`[afd sync] ${m.remoteInvalidUrl}: ${raw}`);
252
+ return null;
253
+ }
254
+ }
255
+
256
+ async function syncRemotePush(url: string, m: typeof msgs.en) {
257
+ // 1. Export latest local payload via daemon
13
258
  let result: SyncResponse;
14
259
  try {
15
260
  result = await daemonRequest<SyncResponse>("/sync");
@@ -20,31 +265,332 @@ export async function syncCommand() {
20
265
  }
21
266
 
22
267
  if (result.count === 0) {
23
- console.log("[afd sync] No antibodies to export. Run `afd fix` first to learn patterns.");
268
+ console.log(`[afd sync] ${m.noAntibodies}`);
269
+ return;
270
+ }
271
+
272
+ const localPayloadPath = resolve(AFD_DIR, "global-vaccine-payload.json");
273
+ const rawPayload: VaccinePayload = JSON.parse(readFileSync(localPayloadPath, "utf-8"));
274
+
275
+ // Stamp publisher scope on all antibodies before sending
276
+ const publisherScope = resolveScope();
277
+ const now = new Date().toISOString();
278
+ const payload: VaccinePayload = {
279
+ ...rawPayload,
280
+ version: "1.7",
281
+ scope: publisherScope,
282
+ antibodies: rawPayload.antibodies.map(ab => ({
283
+ ...ab,
284
+ scope: publisherScope,
285
+ fqid: `${publisherScope}/${ab.id}`,
286
+ version: ab.version ?? 1,
287
+ updatedAt: ab.updatedAt ?? ab.learnedAt ?? now,
288
+ })),
289
+ };
290
+
291
+ // 2. POST to remote
292
+ let res: Response;
293
+ try {
294
+ res = await fetch(url, {
295
+ method: "POST",
296
+ headers: { "Content-Type": "application/json", "User-Agent": "afd-sync/1.7" },
297
+ body: JSON.stringify(payload),
298
+ signal: AbortSignal.timeout(REMOTE_TIMEOUT_MS),
299
+ });
300
+ } catch (err: unknown) {
301
+ const isTimeout = err instanceof Error && err.name === "TimeoutError";
302
+ console.error(`[afd sync] ${isTimeout ? m.remoteTimeout : m.remoteNetworkError}: ${url}`);
303
+ process.exit(1);
304
+ }
305
+
306
+ if (!res.ok) {
307
+ console.error(`[afd sync] ${m.remoteStatusError} ${res.status} ${res.statusText}`);
308
+ process.exit(1);
309
+ }
310
+
311
+ const BOX = { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│", ml: "├", mr: "┤" };
312
+ const W = 50;
313
+ const line = (l: string, r: string) => `${l}${BOX.h.repeat(W)}${r}`;
314
+ const row = (s: string) => `${BOX.v} ${s}${" ".repeat(Math.max(0, W - 2 - s.length))}${BOX.v}`;
315
+
316
+ console.log(line(BOX.tl, BOX.tr));
317
+ console.log(row(`📤 ${m.remotePushTitle}`));
318
+ console.log(line(BOX.ml, BOX.mr));
319
+ console.log(row(`${m.antibodies}: ${payload.antibodyCount}`));
320
+ console.log(row(`URL: ${url}`));
321
+ console.log(line(BOX.ml, BOX.mr));
322
+ console.log(row(`✅ ${m.remotePushSuccess}`));
323
+ console.log(line(BOX.bl, BOX.br));
324
+ }
325
+
326
+ async function syncRemotePull(url: string, m: typeof msgs.en) {
327
+ // 1. GET payload from remote
328
+ let res: Response;
329
+ try {
330
+ res = await fetch(url, {
331
+ method: "GET",
332
+ headers: { "Accept": "application/json", "User-Agent": "afd-sync/1.7" },
333
+ signal: AbortSignal.timeout(REMOTE_TIMEOUT_MS),
334
+ });
335
+ } catch (err: unknown) {
336
+ const isTimeout = err instanceof Error && err.name === "TimeoutError";
337
+ console.error(`[afd sync] ${isTimeout ? m.remoteTimeout : m.remoteNetworkError}: ${url}`);
338
+ process.exit(1);
339
+ }
340
+
341
+ if (!res.ok) {
342
+ console.error(`[afd sync] ${m.remoteStatusError} ${res.status} ${res.statusText}`);
343
+ process.exit(1);
344
+ }
345
+
346
+ let remotePayload: VaccinePayload;
347
+ try {
348
+ const json = await res.json();
349
+ if (!json || !Array.isArray(json.antibodies)) throw new Error("missing antibodies array");
350
+ remotePayload = json as VaccinePayload;
351
+ } catch {
352
+ console.error(`[afd sync] ${m.remoteInvalidResponse}`);
353
+ process.exit(1);
354
+ }
355
+
356
+ // 2. Learn each antibody into the running daemon (same as local pull)
357
+ let newCount = 0;
358
+ let skippedCount = 0;
359
+ const results: { id: string; status: string }[] = [];
360
+
361
+ for (const ab of remotePayload.antibodies) {
362
+ try {
363
+ const learnRes = await fetch(
364
+ `http://127.0.0.1:${getDaemonPort()}/antibodies/learn`,
365
+ {
366
+ method: "POST",
367
+ headers: { "Content-Type": "application/json" },
368
+ body: JSON.stringify({
369
+ id: ab.id,
370
+ scope: ab.scope ?? remotePayload.scope ?? "remote",
371
+ version: ab.version ?? 1,
372
+ updatedAt: ab.updatedAt ?? ab.learnedAt,
373
+ patternType: ab.patternType,
374
+ fileTarget: ab.fileTarget,
375
+ patches: ab.patches,
376
+ }),
377
+ signal: AbortSignal.timeout(2000),
378
+ }
379
+ );
380
+ if (learnRes.ok) {
381
+ results.push({ id: ab.id, status: m.pullNew });
382
+ newCount++;
383
+ } else {
384
+ results.push({ id: ab.id, status: m.pullSkipped });
385
+ skippedCount++;
386
+ }
387
+ } catch {
388
+ results.push({ id: ab.id, status: m.pullSkipped });
389
+ skippedCount++;
390
+ }
391
+ }
392
+
393
+ const BOX = { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│", ml: "├", mr: "┤" };
394
+ const W = 50;
395
+ const line = (l: string, r: string) => `${l}${BOX.h.repeat(W)}${r}`;
396
+ const row = (s: string) => `${BOX.v} ${s}${" ".repeat(Math.max(0, W - 2 - s.length))}${BOX.v}`;
397
+
398
+ console.log(line(BOX.tl, BOX.tr));
399
+ console.log(row(`📥 ${m.remotePullTitle}`));
400
+ console.log(line(BOX.ml, BOX.mr));
401
+ console.log(row(`URL: ${url}`));
402
+ console.log(line(BOX.ml, BOX.mr));
403
+
404
+ for (const r of results) {
405
+ const icon = r.status === m.pullNew ? "✅" : "⏭️";
406
+ console.log(row(`${icon} ${r.id}: ${r.status}`));
407
+ }
408
+
409
+ console.log(line(BOX.ml, BOX.mr));
410
+ console.log(row(`✅ ${m.remotePullSuccess}: ${newCount} ${m.pullMerged}, ${skippedCount} ${m.pullSkipped}`));
411
+ console.log(line(BOX.bl, BOX.br));
412
+ }
413
+
414
+ async function syncExport(m: typeof msgs.en) {
415
+ let result: SyncResponse;
416
+ try {
417
+ result = await daemonRequest<SyncResponse>("/sync");
418
+ } catch (err: unknown) {
419
+ const msg = err instanceof Error ? err.message : String(err);
420
+ console.error(`[afd sync] ${msg}`);
421
+ process.exit(1);
422
+ }
423
+
424
+ if (result.count === 0) {
425
+ console.log(`[afd sync] ${m.noAntibodies}`);
24
426
  return;
25
427
  }
26
428
 
27
- // Read the generated payload for display
28
429
  const payloadPath = resolve(AFD_DIR, "global-vaccine-payload.json");
29
430
  const payload = JSON.parse(readFileSync(payloadPath, "utf-8"));
431
+ renderPayloadBox(payload, m);
432
+ console.log(`\n[afd sync] ${m.exported} ${result.count} ${m.ready}`);
433
+ }
434
+
435
+ async function syncPush(m: typeof msgs.en) {
436
+ // First, export latest payload
437
+ let result: SyncResponse;
438
+ try {
439
+ result = await daemonRequest<SyncResponse>("/sync");
440
+ } catch (err: unknown) {
441
+ const msg = err instanceof Error ? err.message : String(err);
442
+ console.error(`[afd sync] ${msg}`);
443
+ process.exit(1);
444
+ }
445
+
446
+ if (result.count === 0) {
447
+ console.log(`[afd sync] ${m.noAntibodies}`);
448
+ return;
449
+ }
450
+
451
+ // Copy to team store
452
+ const localPayloadPath = resolve(AFD_DIR, "global-vaccine-payload.json");
453
+ const localPayload = readFileSync(localPayloadPath, "utf-8");
454
+
455
+ mkdirSync(TEAM_STORE_DIR, { recursive: true });
456
+
457
+ // Merge with existing team payload if present
458
+ let teamPayload: VaccinePayload;
459
+ if (existsSync(TEAM_PAYLOAD_FILE)) {
460
+ try {
461
+ teamPayload = JSON.parse(readFileSync(TEAM_PAYLOAD_FILE, "utf-8"));
462
+ } catch {
463
+ teamPayload = JSON.parse(localPayload);
464
+ }
465
+ // Merge: add new antibodies, update existing ones
466
+ const newPayload = JSON.parse(localPayload) as VaccinePayload;
467
+ const existingIds = new Set(teamPayload.antibodies.map(a => a.id));
468
+ for (const ab of newPayload.antibodies) {
469
+ if (existingIds.has(ab.id)) {
470
+ // Update existing
471
+ const idx = teamPayload.antibodies.findIndex(a => a.id === ab.id);
472
+ if (idx >= 0) teamPayload.antibodies[idx] = ab;
473
+ } else {
474
+ teamPayload.antibodies.push(ab);
475
+ }
476
+ }
477
+ teamPayload.antibodyCount = teamPayload.antibodies.length;
478
+ teamPayload.generatedAt = new Date().toISOString();
479
+ } else {
480
+ teamPayload = JSON.parse(localPayload);
481
+ console.log(`[afd sync] ${m.pushCreated}`);
482
+ }
483
+
484
+ writeFileSync(TEAM_PAYLOAD_FILE, JSON.stringify(teamPayload, null, 2), "utf-8");
485
+
486
+ const BOX = { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│", ml: "├", mr: "┤" };
487
+ const W = 50;
488
+ const line = (l: string, r: string) => `${l}${BOX.h.repeat(W)}${r}`;
489
+ const row = (s: string) => `${BOX.v} ${s}${" ".repeat(Math.max(0, W - 2 - s.length))}${BOX.v}`;
490
+
491
+ console.log(line(BOX.tl, BOX.tr));
492
+ console.log(row(`📤 ${m.pushTitle}`));
493
+ console.log(line(BOX.ml, BOX.mr));
494
+ console.log(row(`${m.antibodies}: ${teamPayload.antibodyCount}`));
495
+ console.log(row(`${m.payload}: ${TEAM_PAYLOAD_FILE}`));
496
+ console.log(line(BOX.ml, BOX.mr));
497
+ console.log(row(`✅ ${m.pushSuccess}`));
498
+ console.log(line(BOX.bl, BOX.br));
499
+ }
500
+
501
+ async function syncPull(m: typeof msgs.en) {
502
+ if (!existsSync(TEAM_PAYLOAD_FILE)) {
503
+ console.log(`[afd sync] ${m.pullNoStore}`);
504
+ console.log(`[afd sync] ${m.pullHint}`);
505
+ return;
506
+ }
507
+
508
+ let teamPayload: VaccinePayload;
509
+ try {
510
+ teamPayload = JSON.parse(readFileSync(TEAM_PAYLOAD_FILE, "utf-8"));
511
+ } catch {
512
+ console.error("[afd sync] Failed to parse team vaccine payload.");
513
+ process.exit(1);
514
+ }
515
+
516
+ // Learn each antibody into the daemon
517
+ let newCount = 0;
518
+ let skippedCount = 0;
519
+ const results: { id: string; status: string }[] = [];
520
+
521
+ for (const ab of teamPayload.antibodies) {
522
+ try {
523
+ // Try to learn via daemon API
524
+ const res = await fetch(`http://127.0.0.1:${getDaemonPort()}/antibodies/learn`, {
525
+ method: "POST",
526
+ headers: { "Content-Type": "application/json" },
527
+ body: JSON.stringify({
528
+ id: ab.id,
529
+ patternType: ab.patternType,
530
+ fileTarget: ab.fileTarget,
531
+ patches: ab.patches,
532
+ }),
533
+ signal: AbortSignal.timeout(2000),
534
+ });
535
+ if (res.ok) {
536
+ results.push({ id: ab.id, status: m.pullNew });
537
+ newCount++;
538
+ } else {
539
+ results.push({ id: ab.id, status: m.pullSkipped });
540
+ skippedCount++;
541
+ }
542
+ } catch {
543
+ results.push({ id: ab.id, status: m.pullSkipped });
544
+ skippedCount++;
545
+ }
546
+ }
547
+
548
+ const BOX = { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│", ml: "├", mr: "┤" };
549
+ const W = 50;
550
+ const line = (l: string, r: string) => `${l}${BOX.h.repeat(W)}${r}`;
551
+ const row = (s: string) => `${BOX.v} ${s}${" ".repeat(Math.max(0, W - 2 - s.length))}${BOX.v}`;
552
+
553
+ console.log(line(BOX.tl, BOX.tr));
554
+ console.log(row(`📥 ${m.pullTitle}`));
555
+ console.log(line(BOX.ml, BOX.mr));
556
+
557
+ for (const r of results) {
558
+ const icon = r.status === m.pullNew ? "✅" : "⏭️";
559
+ console.log(row(`${icon} ${r.id}: ${r.status}`));
560
+ }
561
+
562
+ console.log(line(BOX.ml, BOX.mr));
563
+ console.log(row(`✅ ${m.pullSuccess}: ${newCount} ${m.pullMerged}`));
564
+ console.log(line(BOX.bl, BOX.br));
565
+ }
566
+
567
+ function getDaemonPort(): number {
568
+ const { getDaemonInfo } = require("../daemon/client");
569
+ const info = getDaemonInfo();
570
+ if (!info) throw new Error("Daemon not running");
571
+ return info.port;
572
+ }
573
+
574
+ function renderPayloadBox(payload: VaccinePayload, m: typeof msgs.en) {
575
+ const BOX = { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│", ml: "├", mr: "┤" };
576
+ const W = 48;
577
+ const line = (l: string, r: string) => `${l}${BOX.h.repeat(W)}${r}`;
578
+ const row = (s: string) => `${BOX.v} ${s}${" ".repeat(Math.max(0, W - 2 - s.length))}${BOX.v}`;
30
579
 
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`);
580
+ console.log(line(BOX.tl, BOX.tr));
581
+ console.log(row(`${m.title}`));
582
+ console.log(line(BOX.ml, BOX.mr));
583
+ console.log(row(`${m.ecosystem} : ${payload.ecosystem}`));
584
+ console.log(row(`${m.antibodies} : ${payload.antibodyCount}`));
585
+ console.log(row(`${m.generated} : ${payload.generatedAt.substring(0, 19)}`));
586
+ console.log(line(BOX.ml, BOX.mr));
39
587
 
40
588
  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`);
589
+ const patches = ab.patches.map(p => `${p.op} ${p.path}`).join(", ");
590
+ console.log(row(`[${ab.id}] ${ab.patternType.padEnd(18)} ${patches.substring(0, 14)}`));
43
591
  }
44
592
 
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.`);
593
+ console.log(line(BOX.ml, BOX.mr));
594
+ console.log(row(`${m.payload}: .afd/global-vaccine-payload.json`));
595
+ console.log(line(BOX.bl, BOX.br));
50
596
  }