@bridge4dev/runner 0.11.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/LICENSE +21 -0
- package/README.md +86 -0
- package/dist/adapters/claude.d.ts +19 -0
- package/dist/adapters/claude.js +631 -0
- package/dist/adapters/codex-home.d.ts +61 -0
- package/dist/adapters/codex-home.js +234 -0
- package/dist/adapters/codex-protocol.d.ts +59 -0
- package/dist/adapters/codex-protocol.js +204 -0
- package/dist/adapters/codex.d.ts +61 -0
- package/dist/adapters/codex.js +1406 -0
- package/dist/adapters/types.d.ts +183 -0
- package/dist/adapters/types.js +5 -0
- package/dist/async-queue.d.ts +11 -0
- package/dist/async-queue.js +50 -0
- package/dist/attachments.d.ts +72 -0
- package/dist/attachments.js +149 -0
- package/dist/auth-relay.d.ts +57 -0
- package/dist/auth-relay.js +289 -0
- package/dist/config.d.ts +96 -0
- package/dist/config.js +73 -0
- package/dist/fsview.d.ts +20 -0
- package/dist/fsview.js +122 -0
- package/dist/git.d.ts +54 -0
- package/dist/git.js +168 -0
- package/dist/gitops.d.ts +136 -0
- package/dist/gitops.js +596 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +352 -0
- package/dist/journal.d.ts +118 -0
- package/dist/journal.js +300 -0
- package/dist/log.d.ts +7 -0
- package/dist/log.js +19 -0
- package/dist/paths.d.ts +7 -0
- package/dist/paths.js +33 -0
- package/dist/policy.d.ts +17 -0
- package/dist/policy.js +272 -0
- package/dist/protocol.d.ts +754 -0
- package/dist/protocol.js +154 -0
- package/dist/self-update.d.ts +75 -0
- package/dist/self-update.js +221 -0
- package/dist/status-file.d.ts +14 -0
- package/dist/status-file.js +29 -0
- package/dist/supervisor.d.ts +216 -0
- package/dist/supervisor.js +1648 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- package/dist/ws-client.d.ts +30 -0
- package/dist/ws-client.js +171 -0
- package/package.json +52 -0
package/dist/version.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type GatewayFrame, type RunnerFrame } from './protocol.js';
|
|
2
|
+
export interface WsClientEvents {
|
|
3
|
+
frame: (frame: GatewayFrame) => void;
|
|
4
|
+
open: () => void;
|
|
5
|
+
close: () => void;
|
|
6
|
+
revoked: (reason: string) => void;
|
|
7
|
+
}
|
|
8
|
+
export declare class RunnerWsClient {
|
|
9
|
+
private readonly wsUrl;
|
|
10
|
+
private readonly token;
|
|
11
|
+
private readonly capabilities;
|
|
12
|
+
private readonly emitter;
|
|
13
|
+
private socket;
|
|
14
|
+
private attempts;
|
|
15
|
+
private authFailures;
|
|
16
|
+
private stopped;
|
|
17
|
+
private reconnectTimer;
|
|
18
|
+
private livenessTimer;
|
|
19
|
+
private lastInboundAt;
|
|
20
|
+
constructor(wsUrl: string, token: string, capabilities?: Record<string, unknown>);
|
|
21
|
+
on<E extends keyof WsClientEvents>(event: E, listener: WsClientEvents[E]): this;
|
|
22
|
+
private emit;
|
|
23
|
+
start(): void;
|
|
24
|
+
stop(): void;
|
|
25
|
+
private ensureLivenessWatchdog;
|
|
26
|
+
get connected(): boolean;
|
|
27
|
+
send(frame: RunnerFrame): boolean;
|
|
28
|
+
private connect;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=ws-client.d.ts.map
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import WebSocket from 'ws';
|
|
4
|
+
import { log } from './log.js';
|
|
5
|
+
import { GatewayFrameSchema } from './protocol.js';
|
|
6
|
+
import { RUNNER_VERSION } from './version.js';
|
|
7
|
+
// Outbound WSS to the DevBridge API — the only channel between this server
|
|
8
|
+
// and DevBridge (plan §3). Reconnects with capped exponential backoff.
|
|
9
|
+
const BACKOFF_BASE_MS = 1_000;
|
|
10
|
+
const BACKOFF_MAX_MS = 60_000;
|
|
11
|
+
const REPLACED_COOLDOWN_MS = 60_000;
|
|
12
|
+
// The gateway pings every 30s; no inbound traffic for 90s means the path is
|
|
13
|
+
// half-dead even though the socket looks OPEN (QA-96 F16).
|
|
14
|
+
const LIVENESS_TIMEOUT_MS = 90_000;
|
|
15
|
+
const LIVENESS_CHECK_MS = 30_000;
|
|
16
|
+
// Repeated HTTP 401/403 on the upgrade = bad token; slow way down.
|
|
17
|
+
const AUTH_FAILURE_THRESHOLD = 5;
|
|
18
|
+
const AUTH_FAILURE_COOLDOWN_MS = 10 * 60_000;
|
|
19
|
+
export class RunnerWsClient {
|
|
20
|
+
wsUrl;
|
|
21
|
+
token;
|
|
22
|
+
capabilities;
|
|
23
|
+
emitter = new EventEmitter();
|
|
24
|
+
socket = null;
|
|
25
|
+
attempts = 0;
|
|
26
|
+
authFailures = 0;
|
|
27
|
+
stopped = false;
|
|
28
|
+
reconnectTimer = null;
|
|
29
|
+
livenessTimer = null;
|
|
30
|
+
lastInboundAt = 0;
|
|
31
|
+
constructor(wsUrl, token, capabilities = { agents: ['claude'], git: true }) {
|
|
32
|
+
this.wsUrl = wsUrl;
|
|
33
|
+
this.token = token;
|
|
34
|
+
this.capabilities = capabilities;
|
|
35
|
+
}
|
|
36
|
+
on(event, listener) {
|
|
37
|
+
this.emitter.on(event, listener);
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
emit(event, ...args) {
|
|
41
|
+
this.emitter.emit(event, ...args);
|
|
42
|
+
}
|
|
43
|
+
start() {
|
|
44
|
+
this.stopped = false;
|
|
45
|
+
this.connect();
|
|
46
|
+
}
|
|
47
|
+
stop() {
|
|
48
|
+
this.stopped = true;
|
|
49
|
+
if (this.reconnectTimer)
|
|
50
|
+
clearTimeout(this.reconnectTimer);
|
|
51
|
+
if (this.livenessTimer)
|
|
52
|
+
clearInterval(this.livenessTimer);
|
|
53
|
+
this.livenessTimer = null;
|
|
54
|
+
this.socket?.close(1000, 'runner shutting down');
|
|
55
|
+
this.socket = null;
|
|
56
|
+
}
|
|
57
|
+
ensureLivenessWatchdog() {
|
|
58
|
+
if (this.livenessTimer)
|
|
59
|
+
return;
|
|
60
|
+
this.livenessTimer = setInterval(() => {
|
|
61
|
+
if (!this.socket || this.socket.readyState !== WebSocket.OPEN)
|
|
62
|
+
return;
|
|
63
|
+
if (Date.now() - this.lastInboundAt > LIVENESS_TIMEOUT_MS) {
|
|
64
|
+
log.warn('ws: no inbound traffic, terminating half-dead connection');
|
|
65
|
+
this.socket.terminate(); // close handler schedules the reconnect
|
|
66
|
+
}
|
|
67
|
+
}, LIVENESS_CHECK_MS);
|
|
68
|
+
this.livenessTimer.unref();
|
|
69
|
+
}
|
|
70
|
+
get connected() {
|
|
71
|
+
return this.socket?.readyState === WebSocket.OPEN;
|
|
72
|
+
}
|
|
73
|
+
send(frame) {
|
|
74
|
+
if (!this.connected || !this.socket)
|
|
75
|
+
return false;
|
|
76
|
+
try {
|
|
77
|
+
this.socket.send(JSON.stringify(frame));
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
log.warn('ws: send failed', { error: String(error) });
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
connect() {
|
|
86
|
+
if (this.stopped)
|
|
87
|
+
return;
|
|
88
|
+
log.info('ws: connecting', { url: this.wsUrl, attempt: this.attempts + 1 });
|
|
89
|
+
const socket = new WebSocket(this.wsUrl, {
|
|
90
|
+
headers: { Authorization: `Bearer ${this.token}` },
|
|
91
|
+
handshakeTimeout: 15_000,
|
|
92
|
+
});
|
|
93
|
+
this.socket = socket;
|
|
94
|
+
socket.on('open', () => {
|
|
95
|
+
this.attempts = 0;
|
|
96
|
+
this.authFailures = 0;
|
|
97
|
+
this.lastInboundAt = Date.now();
|
|
98
|
+
this.ensureLivenessWatchdog();
|
|
99
|
+
this.send({
|
|
100
|
+
type: 'hello',
|
|
101
|
+
runnerVersion: RUNNER_VERSION,
|
|
102
|
+
osInfo: `${os.type()} ${os.release()} ${os.arch()}`.slice(0, 200),
|
|
103
|
+
capabilities: this.capabilities,
|
|
104
|
+
});
|
|
105
|
+
this.emit('open');
|
|
106
|
+
});
|
|
107
|
+
socket.on('ping', () => {
|
|
108
|
+
this.lastInboundAt = Date.now();
|
|
109
|
+
});
|
|
110
|
+
socket.on('message', (data) => {
|
|
111
|
+
this.lastInboundAt = Date.now();
|
|
112
|
+
let parsed;
|
|
113
|
+
try {
|
|
114
|
+
parsed = JSON.parse(String(data));
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
log.warn('ws: non-JSON frame from server');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const frame = GatewayFrameSchema.safeParse(parsed);
|
|
121
|
+
if (!frame.success) {
|
|
122
|
+
log.warn('ws: unrecognized frame', { issue: frame.error.issues[0]?.message });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (frame.data.type === 'revoked') {
|
|
126
|
+
log.error(`ws: server access revoked — ${frame.data.reason}`);
|
|
127
|
+
this.stopped = true;
|
|
128
|
+
this.emit('revoked', frame.data.reason);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
this.emit('frame', frame.data);
|
|
132
|
+
});
|
|
133
|
+
socket.on('close', (code, reason) => {
|
|
134
|
+
if (this.socket === socket)
|
|
135
|
+
this.socket = null;
|
|
136
|
+
this.emit('close');
|
|
137
|
+
if (this.stopped)
|
|
138
|
+
return;
|
|
139
|
+
if (code === 4003) {
|
|
140
|
+
log.error('ws: connection refused as revoked — stopping reconnects');
|
|
141
|
+
this.stopped = true;
|
|
142
|
+
this.emit('revoked', reason.toString() || 'revoked');
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// 4001 = another connection replaced us (a second runner instance?) —
|
|
146
|
+
// back off hard instead of fighting for the slot. Repeated auth
|
|
147
|
+
// rejections during the upgrade get a long cooldown (QA-96 F16).
|
|
148
|
+
const delay = code === 4001
|
|
149
|
+
? REPLACED_COOLDOWN_MS
|
|
150
|
+
: this.authFailures >= AUTH_FAILURE_THRESHOLD
|
|
151
|
+
? AUTH_FAILURE_COOLDOWN_MS
|
|
152
|
+
: Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * 2 ** this.attempts) +
|
|
153
|
+
Math.floor(Math.random() * 1_000);
|
|
154
|
+
this.attempts += 1;
|
|
155
|
+
log.info('ws: disconnected, will reconnect', { code, delayMs: delay });
|
|
156
|
+
this.reconnectTimer = setTimeout(() => this.connect(), delay);
|
|
157
|
+
});
|
|
158
|
+
socket.on('unexpected-response', (_req, res) => {
|
|
159
|
+
log.error(`ws: handshake rejected with HTTP ${res.statusCode ?? '?'}`);
|
|
160
|
+
if (res.statusCode === 401 || res.statusCode === 403) {
|
|
161
|
+
this.authFailures += 1;
|
|
162
|
+
log.error('ws: runner token rejected — re-pair or set a rotated token');
|
|
163
|
+
}
|
|
164
|
+
socket.terminate();
|
|
165
|
+
});
|
|
166
|
+
socket.on('error', (error) => {
|
|
167
|
+
log.warn('ws: socket error', { error: String(error) });
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=ws-client.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bridge4dev/runner",
|
|
3
|
+
"version": "0.11.0",
|
|
4
|
+
"description": "DevBridge dev runner — connects a dev server to DevBridge and runs agent sessions (Claude Code / Codex)",
|
|
5
|
+
"homepage": "https://bridge4.dev",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"bin": {
|
|
10
|
+
"devbridge-runner": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"devbridge",
|
|
14
|
+
"agent",
|
|
15
|
+
"claude-code",
|
|
16
|
+
"codex",
|
|
17
|
+
"cli"
|
|
18
|
+
],
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=20.0.0"
|
|
21
|
+
},
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"!dist/**/*.test.js",
|
|
28
|
+
"!dist/**/*.test.d.ts",
|
|
29
|
+
"!dist/**/*.map"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc",
|
|
33
|
+
"dev": "tsx watch src/index.ts daemon",
|
|
34
|
+
"typecheck": "tsc --noEmit",
|
|
35
|
+
"lint": "eslint src/",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"prepublishOnly": "tsc"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.218",
|
|
41
|
+
"smol-toml": "^1.7.0",
|
|
42
|
+
"ws": "^8.21.1",
|
|
43
|
+
"zod": "^3.24.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^22.10.2",
|
|
47
|
+
"@types/ws": "^8.18.1",
|
|
48
|
+
"tsx": "^4.19.2",
|
|
49
|
+
"typescript": "~5.7.2",
|
|
50
|
+
"vitest": "^2.1.9"
|
|
51
|
+
}
|
|
52
|
+
}
|