@agnishc/edb-bridge 0.12.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 +17 -0
- package/README.md +39 -0
- package/package.json +42 -0
- package/src/broker.ts +180 -0
- package/src/client.ts +368 -0
- package/src/framing.ts +49 -0
- package/src/index.ts +645 -0
- package/src/paths.ts +25 -0
- package/src/spawn.ts +169 -0
- package/src/types.ts +41 -0
package/src/spawn.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import net from "node:net";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { BRIDGE_DIR, getBrokerPidPath, getBrokerSocketPath, getSpawnLockPath } from "./paths.js";
|
|
7
|
+
|
|
8
|
+
const SOCKET_PATH = getBrokerSocketPath();
|
|
9
|
+
const PID_PATH = getBrokerPidPath();
|
|
10
|
+
const LOCK_PATH = getSpawnLockPath();
|
|
11
|
+
|
|
12
|
+
function sleep(ms: number): Promise<void> {
|
|
13
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function checkConnectable(): Promise<boolean> {
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
const socket = net.connect(SOCKET_PATH);
|
|
19
|
+
const finish = (ok: boolean) => {
|
|
20
|
+
clearTimeout(t);
|
|
21
|
+
socket.off("connect", onConnect);
|
|
22
|
+
socket.off("error", onError);
|
|
23
|
+
resolve(ok);
|
|
24
|
+
};
|
|
25
|
+
const onConnect = () => {
|
|
26
|
+
socket.end();
|
|
27
|
+
finish(true);
|
|
28
|
+
};
|
|
29
|
+
const onError = () => {
|
|
30
|
+
socket.destroy();
|
|
31
|
+
finish(false);
|
|
32
|
+
};
|
|
33
|
+
const t = setTimeout(() => {
|
|
34
|
+
socket.destroy();
|
|
35
|
+
finish(false);
|
|
36
|
+
}, 1000);
|
|
37
|
+
socket.on("connect", onConnect);
|
|
38
|
+
socket.on("error", onError);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function isBrokerRunning(): Promise<boolean> {
|
|
43
|
+
if (await checkConnectable()) return true;
|
|
44
|
+
if (!existsSync(PID_PATH)) return false;
|
|
45
|
+
try {
|
|
46
|
+
const pid = parseInt(readFileSync(PID_PATH, "utf-8").trim(), 10);
|
|
47
|
+
if (!Number.isFinite(pid)) return false;
|
|
48
|
+
process.kill(pid, 0);
|
|
49
|
+
return checkConnectable();
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function acquireLock(): boolean {
|
|
56
|
+
for (let i = 0; i < 5; i++) {
|
|
57
|
+
try {
|
|
58
|
+
writeFileSync(LOCK_PATH, `${process.pid}\n${Date.now()}\n`, { flag: "wx" });
|
|
59
|
+
return true;
|
|
60
|
+
} catch (err: any) {
|
|
61
|
+
if (err.code !== "EEXIST") throw err;
|
|
62
|
+
// Check if stale
|
|
63
|
+
try {
|
|
64
|
+
const [pidLine = "", tsLine = "0"] = readFileSync(LOCK_PATH, "utf-8").trim().split("\n");
|
|
65
|
+
const pid = Number.parseInt(pidLine, 10);
|
|
66
|
+
const age = Date.now() - Number.parseInt(tsLine, 10);
|
|
67
|
+
let stale = age > 10_000;
|
|
68
|
+
if (!stale && Number.isFinite(pid)) {
|
|
69
|
+
try {
|
|
70
|
+
process.kill(pid, 0);
|
|
71
|
+
} catch {
|
|
72
|
+
stale = true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (stale) {
|
|
76
|
+
try {
|
|
77
|
+
unlinkSync(LOCK_PATH);
|
|
78
|
+
} catch {
|
|
79
|
+
/* ignore */
|
|
80
|
+
}
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
/* unreadable = stale */
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function releaseLock(): void {
|
|
93
|
+
try {
|
|
94
|
+
unlinkSync(LOCK_PATH);
|
|
95
|
+
} catch {
|
|
96
|
+
/* ignore */
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function waitForBroker(timeoutMs = 5000): Promise<void> {
|
|
101
|
+
const end = Date.now() + timeoutMs;
|
|
102
|
+
while (Date.now() < end) {
|
|
103
|
+
if (await checkConnectable()) return;
|
|
104
|
+
await sleep(100);
|
|
105
|
+
}
|
|
106
|
+
throw new Error("edb-bridge: broker failed to start within timeout");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function spawnBrokerIfNeeded(): Promise<void> {
|
|
110
|
+
mkdirSync(BRIDGE_DIR, { recursive: true });
|
|
111
|
+
if (await isBrokerRunning()) return;
|
|
112
|
+
|
|
113
|
+
const hasLock = acquireLock();
|
|
114
|
+
if (!hasLock) {
|
|
115
|
+
await waitForBroker();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
if (await isBrokerRunning()) return;
|
|
121
|
+
|
|
122
|
+
const brokerPath = join(dirname(fileURLToPath(import.meta.url)), "broker.ts");
|
|
123
|
+
const nodePath = process.execPath;
|
|
124
|
+
|
|
125
|
+
// Locate tsx cli next to this package
|
|
126
|
+
const tsxCli = join(dirname(fileURLToPath(import.meta.url)), "..", "node_modules", "tsx", "dist", "cli.mjs");
|
|
127
|
+
const [cmd, args] = existsSync(tsxCli)
|
|
128
|
+
? [nodePath, [tsxCli, brokerPath]]
|
|
129
|
+
: ["npx", ["--no-install", "tsx", brokerPath]];
|
|
130
|
+
|
|
131
|
+
const child = spawn(cmd, args, {
|
|
132
|
+
detached: true,
|
|
133
|
+
stdio: "ignore",
|
|
134
|
+
cwd: dirname(brokerPath),
|
|
135
|
+
env: { ...process.env, NODE_NO_WARNINGS: "1" },
|
|
136
|
+
windowsHide: true,
|
|
137
|
+
});
|
|
138
|
+
child.unref();
|
|
139
|
+
|
|
140
|
+
await new Promise<void>((resolve, reject) => {
|
|
141
|
+
const cleanup = () => {
|
|
142
|
+
child.off("error", onError);
|
|
143
|
+
child.off("exit", onExit);
|
|
144
|
+
};
|
|
145
|
+
const onError = (err: Error) => {
|
|
146
|
+
cleanup();
|
|
147
|
+
reject(err);
|
|
148
|
+
};
|
|
149
|
+
const onExit = (code: number | null) => {
|
|
150
|
+
cleanup();
|
|
151
|
+
reject(new Error(`edb-bridge broker exited early with code ${code}`));
|
|
152
|
+
};
|
|
153
|
+
child.once("error", onError);
|
|
154
|
+
child.once("exit", onExit);
|
|
155
|
+
waitForBroker().then(
|
|
156
|
+
() => {
|
|
157
|
+
cleanup();
|
|
158
|
+
resolve();
|
|
159
|
+
},
|
|
160
|
+
(e) => {
|
|
161
|
+
cleanup();
|
|
162
|
+
reject(e);
|
|
163
|
+
},
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
} finally {
|
|
167
|
+
releaseLock();
|
|
168
|
+
}
|
|
169
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// ── Shared types for edb-bridge ───────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
export interface SessionInfo {
|
|
4
|
+
id: string; // broker-assigned session ID
|
|
5
|
+
cwd: string;
|
|
6
|
+
pid: number;
|
|
7
|
+
parentId?: string; // parent broker session ID (set by sub-agents)
|
|
8
|
+
agentId?: string; // edb-subagents agent ID (set by sub-agents)
|
|
9
|
+
startedAt: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface BridgeMessage {
|
|
13
|
+
id: string;
|
|
14
|
+
timestamp: number;
|
|
15
|
+
replyTo?: string; // message ID this is a reply to
|
|
16
|
+
expectsReply?: boolean;
|
|
17
|
+
type?: string; // optional message type tag (e.g. "task_updated", "ask_supervisor")
|
|
18
|
+
content: {
|
|
19
|
+
text: string;
|
|
20
|
+
data?: Record<string, unknown>; // structured payload
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ── Client → Broker ───────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
export type ClientMessage =
|
|
27
|
+
| { type: "register"; session: Omit<SessionInfo, "id"> }
|
|
28
|
+
| { type: "unregister" }
|
|
29
|
+
| { type: "send"; to: string; message: BridgeMessage }
|
|
30
|
+
| { type: "list"; requestId: string };
|
|
31
|
+
|
|
32
|
+
// ── Broker → Client ───────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
export type BrokerMessage =
|
|
35
|
+
| { type: "registered"; sessionId: string }
|
|
36
|
+
| { type: "sessions"; requestId: string; sessions: SessionInfo[] }
|
|
37
|
+
| { type: "message"; from: SessionInfo; message: BridgeMessage }
|
|
38
|
+
| { type: "delivered"; messageId: string }
|
|
39
|
+
| { type: "delivery_failed"; messageId: string; reason: string }
|
|
40
|
+
| { type: "session_joined"; session: SessionInfo }
|
|
41
|
+
| { type: "session_left"; sessionId: string };
|