autonomous-flow-daemon 1.6.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 (61) hide show
  1. package/CHANGELOG.md +85 -85
  2. package/LICENSE +21 -21
  3. package/README-ko.md +282 -0
  4. package/README.md +282 -266
  5. package/mcp-config.json +10 -10
  6. package/package.json +4 -2
  7. package/src/adapters/index.ts +370 -370
  8. package/src/cli.ts +162 -127
  9. package/src/commands/benchmark.ts +187 -187
  10. package/src/commands/correlate.ts +180 -0
  11. package/src/commands/dashboard.ts +404 -0
  12. package/src/commands/evolution.ts +84 -1
  13. package/src/commands/fix.ts +158 -158
  14. package/src/commands/lang.ts +41 -41
  15. package/src/commands/plugin.ts +110 -0
  16. package/src/commands/restart.ts +14 -14
  17. package/src/commands/score.ts +276 -276
  18. package/src/commands/start.ts +155 -155
  19. package/src/commands/status.ts +157 -157
  20. package/src/commands/stop.ts +68 -68
  21. package/src/commands/suggest.ts +211 -0
  22. package/src/commands/sync.ts +329 -16
  23. package/src/constants.ts +32 -32
  24. package/src/core/boast.ts +280 -280
  25. package/src/core/config.ts +49 -49
  26. package/src/core/correlation-engine.ts +265 -0
  27. package/src/core/db.ts +145 -117
  28. package/src/core/discovery.ts +65 -65
  29. package/src/core/federation.ts +129 -0
  30. package/src/core/hologram/engine.ts +71 -71
  31. package/src/core/hologram/fallback.ts +11 -11
  32. package/src/core/hologram/go-extractor.ts +203 -0
  33. package/src/core/hologram/incremental.ts +227 -227
  34. package/src/core/hologram/py-extractor.ts +132 -132
  35. package/src/core/hologram/rust-extractor.ts +244 -0
  36. package/src/core/hologram/ts-extractor.ts +406 -320
  37. package/src/core/hologram/types.ts +27 -25
  38. package/src/core/hologram.ts +73 -71
  39. package/src/core/i18n/messages.ts +309 -309
  40. package/src/core/locale.ts +88 -88
  41. package/src/core/log-rotate.ts +33 -33
  42. package/src/core/log-utils.ts +38 -38
  43. package/src/core/lru-map.ts +61 -61
  44. package/src/core/notify.ts +74 -74
  45. package/src/core/plugin-manager.ts +225 -0
  46. package/src/core/rule-suggestion.ts +127 -0
  47. package/src/core/validator-generator.ts +224 -0
  48. package/src/core/workspace.ts +28 -28
  49. package/src/daemon/client.ts +78 -65
  50. package/src/daemon/event-batcher.ts +108 -108
  51. package/src/daemon/guards.ts +13 -13
  52. package/src/daemon/http-routes.ts +376 -293
  53. package/src/daemon/mcp-handler.ts +575 -270
  54. package/src/daemon/mcp-subscriptions.ts +81 -0
  55. package/src/daemon/mesh.ts +51 -0
  56. package/src/daemon/server.ts +655 -590
  57. package/src/daemon/types.ts +121 -100
  58. package/src/daemon/workspace-map.ts +104 -92
  59. package/src/platform.ts +60 -60
  60. package/src/version.ts +15 -15
  61. package/README.ko.md +0 -266
@@ -1,8 +1,10 @@
1
1
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
2
2
  import { resolve, join } from "path";
3
- import { daemonRequest } from "../daemon/client";
3
+ import { daemonRequest, getMeshPeers } from "../daemon/client";
4
4
  import { AFD_DIR } from "../constants";
5
5
  import { getSystemLanguage } from "../core/locale";
6
+ import { resolveScope } from "../core/federation";
7
+ import type { FederatedAntibody, FederatedPayload } from "../core/federation";
6
8
 
7
9
  interface SyncResponse {
8
10
  status: string;
@@ -10,26 +12,15 @@ interface SyncResponse {
10
12
  count: number;
11
13
  }
12
14
 
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
- }
15
+ // Re-export federated types under legacy names for local push/pull compatibility
16
+ type VaccinePayload = FederatedPayload;
17
+ type VaccineAntibody = FederatedAntibody;
28
18
 
29
19
  interface SyncOptions {
30
20
  push?: boolean;
31
21
  pull?: boolean;
32
22
  remote?: string;
23
+ localMesh?: boolean;
33
24
  }
34
25
 
35
26
  const msgs = {
@@ -53,6 +44,17 @@ const msgs = {
53
44
  pullHint: "Run `afd sync --push` first to create the shared store.",
54
45
  learnedVia: "Learned via pull",
55
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",
56
58
  },
57
59
  ko: {
58
60
  title: "afd sync — 백신 네트워크",
@@ -74,6 +76,17 @@ const msgs = {
74
76
  pullHint: "`afd sync --push`를 먼저 실행하여 공유 저장소를 생성하세요.",
75
77
  learnedVia: "풀로 학습됨",
76
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: "원격 서버가 오류 상태를 반환했습니다.",
77
90
  },
78
91
  };
79
92
 
@@ -84,6 +97,27 @@ export async function syncCommand(opts: SyncOptions = {}) {
84
97
  const lang = getSystemLanguage();
85
98
  const m = msgs[lang];
86
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
+
87
121
  if (opts.push) {
88
122
  await syncPush(m);
89
123
  return;
@@ -98,6 +132,285 @@ export async function syncCommand(opts: SyncOptions = {}) {
98
132
  await syncExport(m);
99
133
  }
100
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
258
+ let result: SyncResponse;
259
+ try {
260
+ result = await daemonRequest<SyncResponse>("/sync");
261
+ } catch (err: unknown) {
262
+ const msg = err instanceof Error ? err.message : String(err);
263
+ console.error(`[afd sync] ${msg}`);
264
+ process.exit(1);
265
+ }
266
+
267
+ if (result.count === 0) {
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
+
101
414
  async function syncExport(m: typeof msgs.en) {
102
415
  let result: SyncResponse;
103
416
  try {
package/src/constants.ts CHANGED
@@ -1,32 +1,32 @@
1
- import { join } from "path";
2
- import { findWorkspaceRoot } from "./core/workspace";
3
-
4
- // Relative paths (used when cwd is already the workspace root)
5
- export const AFD_DIR = ".afd";
6
- export const PID_FILE = join(AFD_DIR, "daemon.pid");
7
- export const PORT_FILE = join(AFD_DIR, "daemon.port");
8
- export const DB_FILE = join(AFD_DIR, "antibodies.sqlite");
9
- export const LOG_FILE = join(AFD_DIR, "daemon.log");
10
- export const QUARANTINE_DIR = join(AFD_DIR, "quarantine");
11
- export const WATCH_TARGETS = [
12
- ".claude/", "CLAUDE.md", ".cursorrules", ".claudeignore", ".gitignore",
13
- ".windsurfrules", ".windsurf/", "codex.md", ".codex/",
14
- ".cursorignore", ".windsurfignore", ".codexignore",
15
- ];
16
-
17
- /**
18
- * Resolve all `.afd/` paths against the workspace root.
19
- * Works correctly even when CLI is invoked from a subdirectory.
20
- */
21
- export function resolveWorkspacePaths(from?: string) {
22
- const root = findWorkspaceRoot(from);
23
- return {
24
- root,
25
- afdDir: join(root, AFD_DIR),
26
- pidFile: join(root, PID_FILE),
27
- portFile: join(root, PORT_FILE),
28
- dbFile: join(root, DB_FILE),
29
- logFile: join(root, LOG_FILE),
30
- quarantineDir: join(root, QUARANTINE_DIR),
31
- };
32
- }
1
+ import { join } from "path";
2
+ import { findWorkspaceRoot } from "./core/workspace";
3
+
4
+ // Relative paths (used when cwd is already the workspace root)
5
+ export const AFD_DIR = ".afd";
6
+ export const PID_FILE = join(AFD_DIR, "daemon.pid");
7
+ export const PORT_FILE = join(AFD_DIR, "daemon.port");
8
+ export const DB_FILE = join(AFD_DIR, "antibodies.sqlite");
9
+ export const LOG_FILE = join(AFD_DIR, "daemon.log");
10
+ export const QUARANTINE_DIR = join(AFD_DIR, "quarantine");
11
+ export const WATCH_TARGETS = [
12
+ ".claude/", "CLAUDE.md", ".cursorrules", ".claudeignore", ".gitignore",
13
+ ".windsurfrules", ".windsurf/", "codex.md", ".codex/",
14
+ ".cursorignore", ".windsurfignore", ".codexignore",
15
+ ];
16
+
17
+ /**
18
+ * Resolve all `.afd/` paths against the workspace root.
19
+ * Works correctly even when CLI is invoked from a subdirectory.
20
+ */
21
+ export function resolveWorkspacePaths(from?: string) {
22
+ const root = findWorkspaceRoot(from);
23
+ return {
24
+ root,
25
+ afdDir: join(root, AFD_DIR),
26
+ pidFile: join(root, PID_FILE),
27
+ portFile: join(root, PORT_FILE),
28
+ dbFile: join(root, DB_FILE),
29
+ logFile: join(root, LOG_FILE),
30
+ quarantineDir: join(root, QUARANTINE_DIR),
31
+ };
32
+ }