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.
- package/CHANGELOG.md +85 -85
- package/LICENSE +21 -21
- package/README-ko.md +282 -0
- package/README.md +282 -266
- package/mcp-config.json +10 -10
- package/package.json +4 -2
- package/src/adapters/index.ts +370 -370
- package/src/cli.ts +162 -127
- package/src/commands/benchmark.ts +187 -187
- package/src/commands/correlate.ts +180 -0
- package/src/commands/dashboard.ts +404 -0
- package/src/commands/evolution.ts +84 -1
- package/src/commands/fix.ts +158 -158
- package/src/commands/lang.ts +41 -41
- package/src/commands/plugin.ts +110 -0
- package/src/commands/restart.ts +14 -14
- package/src/commands/score.ts +276 -276
- package/src/commands/start.ts +155 -155
- package/src/commands/status.ts +157 -157
- package/src/commands/stop.ts +68 -68
- package/src/commands/suggest.ts +211 -0
- package/src/commands/sync.ts +329 -16
- package/src/constants.ts +32 -32
- package/src/core/boast.ts +280 -280
- package/src/core/config.ts +49 -49
- package/src/core/correlation-engine.ts +265 -0
- package/src/core/db.ts +145 -117
- package/src/core/discovery.ts +65 -65
- package/src/core/federation.ts +129 -0
- package/src/core/hologram/engine.ts +71 -71
- package/src/core/hologram/fallback.ts +11 -11
- package/src/core/hologram/go-extractor.ts +203 -0
- package/src/core/hologram/incremental.ts +227 -227
- package/src/core/hologram/py-extractor.ts +132 -132
- package/src/core/hologram/rust-extractor.ts +244 -0
- package/src/core/hologram/ts-extractor.ts +406 -320
- package/src/core/hologram/types.ts +27 -25
- package/src/core/hologram.ts +73 -71
- package/src/core/i18n/messages.ts +309 -309
- package/src/core/locale.ts +88 -88
- package/src/core/log-rotate.ts +33 -33
- package/src/core/log-utils.ts +38 -38
- package/src/core/lru-map.ts +61 -61
- package/src/core/notify.ts +74 -74
- package/src/core/plugin-manager.ts +225 -0
- package/src/core/rule-suggestion.ts +127 -0
- package/src/core/validator-generator.ts +224 -0
- package/src/core/workspace.ts +28 -28
- package/src/daemon/client.ts +78 -65
- package/src/daemon/event-batcher.ts +108 -108
- package/src/daemon/guards.ts +13 -13
- package/src/daemon/http-routes.ts +376 -293
- package/src/daemon/mcp-handler.ts +575 -270
- package/src/daemon/mcp-subscriptions.ts +81 -0
- package/src/daemon/mesh.ts +51 -0
- package/src/daemon/server.ts +655 -590
- package/src/daemon/types.ts +121 -100
- package/src/daemon/workspace-map.ts +104 -92
- package/src/platform.ts +60 -60
- package/src/version.ts +15 -15
- package/README.ko.md +0 -266
package/src/daemon/client.ts
CHANGED
|
@@ -1,65 +1,78 @@
|
|
|
1
|
-
import { readFileSync, existsSync, unlinkSync } from "fs";
|
|
2
|
-
import { resolveWorkspacePaths } from "../constants";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if
|
|
27
|
-
|
|
28
|
-
try { unlinkSync(paths.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/daemon/guards.ts
CHANGED
|
@@ -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
|
+
}
|