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,65 +1,78 @@
1
- import { readFileSync, existsSync, unlinkSync } from "fs";
2
- import { resolveWorkspacePaths } from "../constants";
3
-
4
- export interface DaemonInfo {
5
- pid: number;
6
- port: number;
7
- workspace: string;
8
- }
9
-
10
- /**
11
- * Read daemon PID/port from the workspace-local `.afd/` directory.
12
- * Walks up from cwd to find the workspace root, so CLI commands
13
- * work correctly even when invoked from subdirectories.
14
- *
15
- * If PID file exists but process is dead, cleans up stale files.
16
- */
17
- export function getDaemonInfo(): DaemonInfo | null {
18
- const paths = resolveWorkspacePaths();
19
- if (!existsSync(paths.pidFile) || !existsSync(paths.portFile)) return null;
20
-
21
- const pid = parseInt(readFileSync(paths.pidFile, "utf-8").trim(), 10);
22
- const port = parseInt(readFileSync(paths.portFile, "utf-8").trim(), 10);
23
- if (isNaN(pid) || isNaN(port)) return null;
24
-
25
- // Stale PID detection: check if process is alive at OS level
26
- if (!isProcessAlive(pid)) {
27
- try { unlinkSync(paths.pidFile); } catch {}
28
- try { unlinkSync(paths.portFile); } catch {}
29
- return null;
30
- }
31
-
32
- return { pid, port, workspace: paths.root };
33
- }
34
-
35
- /** Check if a process exists at OS level (does not verify it's afd) */
36
- function isProcessAlive(pid: number): boolean {
37
- try {
38
- process.kill(pid, 0); // signal 0 = existence check
39
- return true;
40
- } catch {
41
- return false;
42
- }
43
- }
44
-
45
- export async function isDaemonAlive(info: DaemonInfo): Promise<boolean> {
46
- try {
47
- const res = await fetch(`http://127.0.0.1:${info.port}/health`, {
48
- signal: AbortSignal.timeout(2000),
49
- });
50
- const data = await res.json() as { status: string; pid: number };
51
- return data.status === "alive" && data.pid === info.pid;
52
- } catch {
53
- return false;
54
- }
55
- }
56
-
57
- export async function daemonRequest<T = unknown>(path: string): Promise<T> {
58
- const info = getDaemonInfo();
59
- if (!info) throw new Error("Daemon not running. Run `afd start` first.");
60
- const res = await fetch(`http://127.0.0.1:${info.port}${path}`, {
61
- signal: AbortSignal.timeout(5000),
62
- });
63
- if (!res.ok) throw new Error(`Daemon returned ${res.status}`);
64
- return res.json() as T;
65
- }
1
+ import { readFileSync, existsSync, unlinkSync } from "fs";
2
+ import { resolveWorkspacePaths } from "../constants";
3
+ import type { MeshEntry } from "./mesh";
4
+
5
+ export interface DaemonInfo {
6
+ pid: number;
7
+ port: number;
8
+ workspace: string;
9
+ }
10
+
11
+ /**
12
+ * Read daemon PID/port from the workspace-local `.afd/` directory.
13
+ * Walks up from cwd to find the workspace root, so CLI commands
14
+ * work correctly even when invoked from subdirectories.
15
+ *
16
+ * If PID file exists but process is dead, cleans up stale files.
17
+ */
18
+ export function getDaemonInfo(): DaemonInfo | null {
19
+ const paths = resolveWorkspacePaths();
20
+ if (!existsSync(paths.pidFile) || !existsSync(paths.portFile)) return null;
21
+
22
+ const pid = parseInt(readFileSync(paths.pidFile, "utf-8").trim(), 10);
23
+ const port = parseInt(readFileSync(paths.portFile, "utf-8").trim(), 10);
24
+ if (isNaN(pid) || isNaN(port)) return null;
25
+
26
+ // Stale PID detection: check if process is alive at OS level
27
+ if (!isProcessAlive(pid)) {
28
+ try { unlinkSync(paths.pidFile); } catch {}
29
+ try { unlinkSync(paths.portFile); } catch {}
30
+ return null;
31
+ }
32
+
33
+ return { pid, port, workspace: paths.root };
34
+ }
35
+
36
+ /** Check if a process exists at OS level (does not verify it's afd) */
37
+ function isProcessAlive(pid: number): boolean {
38
+ try {
39
+ process.kill(pid, 0); // signal 0 = existence check
40
+ return true;
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+
46
+ export async function isDaemonAlive(info: DaemonInfo): Promise<boolean> {
47
+ try {
48
+ const res = await fetch(`http://127.0.0.1:${info.port}/health`, {
49
+ signal: AbortSignal.timeout(2000),
50
+ });
51
+ const data = await res.json() as { status: string; pid: number };
52
+ return data.status === "alive" && data.pid === info.pid;
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
57
+
58
+ export async function daemonRequest<T = unknown>(path: string, method?: "GET"): Promise<T>;
59
+ export async function daemonRequest<T = unknown>(path: string, method: "POST", body: unknown): Promise<T>;
60
+ export async function daemonRequest<T = unknown>(path: string, method: "GET" | "POST" = "GET", body?: unknown): Promise<T> {
61
+ const info = getDaemonInfo();
62
+ if (!info) throw new Error("Daemon not running. Run `afd start` first.");
63
+ const init: RequestInit = {
64
+ method,
65
+ signal: AbortSignal.timeout(5000),
66
+ };
67
+ if (method === "POST" && body !== undefined) {
68
+ init.body = JSON.stringify(body);
69
+ init.headers = { "Content-Type": "application/json" };
70
+ }
71
+ const res = await fetch(`http://127.0.0.1:${info.port}${path}`, init);
72
+ if (!res.ok) throw new Error(`Daemon returned ${res.status}`);
73
+ return res.json() as T;
74
+ }
75
+
76
+ export async function getMeshPeers(): Promise<MeshEntry[]> {
77
+ return daemonRequest<MeshEntry[]>("/mesh/peers");
78
+ }
@@ -1,108 +1,108 @@
1
- /**
2
- * EventBatcher — Adaptive debounce for file watcher events.
3
- *
4
- * Strategy:
5
- * - Immune file changes → fast-path (immediate, no debounce)
6
- * - All other events → 300ms debounce batch
7
- * - Deduplicates: same file multiple events → last event wins
8
- * - Cancels out: add + unlink on same file → removed from batch
9
- */
10
-
11
- export interface BatchedEvent {
12
- event: string;
13
- path: string;
14
- timestamp: number;
15
- }
16
-
17
- export interface EventBatcherOptions {
18
- /** Debounce window in ms (default: 300) */
19
- debounceMs?: number;
20
- /** Check if a path is an immune-protected file (fast-path) */
21
- isImmunePath?: (path: string) => boolean;
22
- /** Handler for fast-path (immediate) events */
23
- onImmediate: (event: string, path: string) => void;
24
- /** Handler for batched events (fired after debounce window) */
25
- onBatch: (events: BatchedEvent[]) => void;
26
- }
27
-
28
- export class EventBatcher {
29
- private readonly debounceMs: number;
30
- private readonly isImmunePath: (path: string) => boolean;
31
- private readonly onImmediate: (event: string, path: string) => void;
32
- private readonly onBatch: (events: BatchedEvent[]) => void;
33
-
34
- private pendingEvents = new Map<string, BatchedEvent>();
35
- private timer: ReturnType<typeof setTimeout> | null = null;
36
- private batchCount = 0;
37
-
38
- constructor(options: EventBatcherOptions) {
39
- this.debounceMs = options.debounceMs ?? 300;
40
- this.isImmunePath = options.isImmunePath ?? (() => false);
41
- this.onImmediate = options.onImmediate;
42
- this.onBatch = options.onBatch;
43
- }
44
-
45
- /** Push a new file event. Returns true if handled immediately (fast-path). */
46
- push(event: string, path: string): boolean {
47
- // Fast-path: immune file change → immediate processing for auto-heal responsiveness
48
- if (event === "change" && this.isImmunePath(path)) {
49
- this.onImmediate(event, path);
50
- return true;
51
- }
52
-
53
- const now = Date.now();
54
- const existing = this.pendingEvents.get(path);
55
-
56
- // Cancel out: add + unlink on same file
57
- if (existing) {
58
- if ((existing.event === "add" && event === "unlink") ||
59
- (existing.event === "unlink" && event === "add")) {
60
- this.pendingEvents.delete(path);
61
- return false;
62
- }
63
- }
64
-
65
- // Last event wins for same file
66
- this.pendingEvents.set(path, { event, path, timestamp: now });
67
-
68
- // Start/reset debounce timer
69
- if (this.timer) clearTimeout(this.timer);
70
- this.timer = setTimeout(() => this.flush(), this.debounceMs);
71
-
72
- return false;
73
- }
74
-
75
- /** Flush all pending events immediately */
76
- flush(): void {
77
- if (this.timer) {
78
- clearTimeout(this.timer);
79
- this.timer = null;
80
- }
81
-
82
- if (this.pendingEvents.size === 0) return;
83
-
84
- const events = [...this.pendingEvents.values()];
85
- this.pendingEvents.clear();
86
- this.batchCount++;
87
- this.onBatch(events);
88
- }
89
-
90
- /** Get the number of batches processed */
91
- get totalBatches(): number {
92
- return this.batchCount;
93
- }
94
-
95
- /** Get the number of pending events */
96
- get pendingCount(): number {
97
- return this.pendingEvents.size;
98
- }
99
-
100
- /** Destroy the batcher, clearing any pending timers */
101
- destroy(): void {
102
- if (this.timer) {
103
- clearTimeout(this.timer);
104
- this.timer = null;
105
- }
106
- this.pendingEvents.clear();
107
- }
108
- }
1
+ /**
2
+ * EventBatcher — Adaptive debounce for file watcher events.
3
+ *
4
+ * Strategy:
5
+ * - Immune file changes → fast-path (immediate, no debounce)
6
+ * - All other events → 300ms debounce batch
7
+ * - Deduplicates: same file multiple events → last event wins
8
+ * - Cancels out: add + unlink on same file → removed from batch
9
+ */
10
+
11
+ export interface BatchedEvent {
12
+ event: string;
13
+ path: string;
14
+ timestamp: number;
15
+ }
16
+
17
+ export interface EventBatcherOptions {
18
+ /** Debounce window in ms (default: 300) */
19
+ debounceMs?: number;
20
+ /** Check if a path is an immune-protected file (fast-path) */
21
+ isImmunePath?: (path: string) => boolean;
22
+ /** Handler for fast-path (immediate) events */
23
+ onImmediate: (event: string, path: string) => void;
24
+ /** Handler for batched events (fired after debounce window) */
25
+ onBatch: (events: BatchedEvent[]) => void;
26
+ }
27
+
28
+ export class EventBatcher {
29
+ private readonly debounceMs: number;
30
+ private readonly isImmunePath: (path: string) => boolean;
31
+ private readonly onImmediate: (event: string, path: string) => void;
32
+ private readonly onBatch: (events: BatchedEvent[]) => void;
33
+
34
+ private pendingEvents = new Map<string, BatchedEvent>();
35
+ private timer: ReturnType<typeof setTimeout> | null = null;
36
+ private batchCount = 0;
37
+
38
+ constructor(options: EventBatcherOptions) {
39
+ this.debounceMs = options.debounceMs ?? 300;
40
+ this.isImmunePath = options.isImmunePath ?? (() => false);
41
+ this.onImmediate = options.onImmediate;
42
+ this.onBatch = options.onBatch;
43
+ }
44
+
45
+ /** Push a new file event. Returns true if handled immediately (fast-path). */
46
+ push(event: string, path: string): boolean {
47
+ // Fast-path: immune file change → immediate processing for auto-heal responsiveness
48
+ if (event === "change" && this.isImmunePath(path)) {
49
+ this.onImmediate(event, path);
50
+ return true;
51
+ }
52
+
53
+ const now = Date.now();
54
+ const existing = this.pendingEvents.get(path);
55
+
56
+ // Cancel out: add + unlink on same file
57
+ if (existing) {
58
+ if ((existing.event === "add" && event === "unlink") ||
59
+ (existing.event === "unlink" && event === "add")) {
60
+ this.pendingEvents.delete(path);
61
+ return false;
62
+ }
63
+ }
64
+
65
+ // Last event wins for same file
66
+ this.pendingEvents.set(path, { event, path, timestamp: now });
67
+
68
+ // Start/reset debounce timer
69
+ if (this.timer) clearTimeout(this.timer);
70
+ this.timer = setTimeout(() => this.flush(), this.debounceMs);
71
+
72
+ return false;
73
+ }
74
+
75
+ /** Flush all pending events immediately */
76
+ flush(): void {
77
+ if (this.timer) {
78
+ clearTimeout(this.timer);
79
+ this.timer = null;
80
+ }
81
+
82
+ if (this.pendingEvents.size === 0) return;
83
+
84
+ const events = [...this.pendingEvents.values()];
85
+ this.pendingEvents.clear();
86
+ this.batchCount++;
87
+ this.onBatch(events);
88
+ }
89
+
90
+ /** Get the number of batches processed */
91
+ get totalBatches(): number {
92
+ return this.batchCount;
93
+ }
94
+
95
+ /** Get the number of pending events */
96
+ get pendingCount(): number {
97
+ return this.pendingEvents.size;
98
+ }
99
+
100
+ /** Destroy the batcher, clearing any pending timers */
101
+ destroy(): void {
102
+ if (this.timer) {
103
+ clearTimeout(this.timer);
104
+ this.timer = null;
105
+ }
106
+ this.pendingEvents.clear();
107
+ }
108
+ }
@@ -1,13 +1,13 @@
1
- import { resolve } from "path";
2
-
3
- /**
4
- * Guard: reject resolved paths outside the workspace root.
5
- * Throws if absPath is not under wsRoot.
6
- */
7
- export function assertInsideWorkspace(absPath: string, wsRoot: string): void {
8
- const normalizedPath = absPath.replace(/\\/g, "/").toLowerCase();
9
- const normalizedRoot = resolve(wsRoot).replace(/\\/g, "/").toLowerCase();
10
- if (!normalizedPath.startsWith(normalizedRoot + "/") && normalizedPath !== normalizedRoot) {
11
- throw new Error("Access denied: path outside workspace");
12
- }
13
- }
1
+ import { resolve } from "path";
2
+
3
+ /**
4
+ * Guard: reject resolved paths outside the workspace root.
5
+ * Throws if absPath is not under wsRoot.
6
+ */
7
+ export function assertInsideWorkspace(absPath: string, wsRoot: string): void {
8
+ const normalizedPath = absPath.replace(/\\/g, "/").toLowerCase();
9
+ const normalizedRoot = resolve(wsRoot).replace(/\\/g, "/").toLowerCase();
10
+ if (!normalizedPath.startsWith(normalizedRoot + "/") && normalizedPath !== normalizedRoot) {
11
+ throw new Error("Access denied: path outside workspace");
12
+ }
13
+ }