@annals/agent-bridge 0.1.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/dist/index.d.ts +2 -0
- package/dist/index.js +1347 -0
- package/package.json +26 -0
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,1347 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { program } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/utils/config.ts
|
|
7
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { homedir } from "os";
|
|
10
|
+
var CONFIG_DIR = join(homedir(), ".agent-bridge");
|
|
11
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
12
|
+
function ensureDir() {
|
|
13
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
14
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function loadConfig() {
|
|
18
|
+
try {
|
|
19
|
+
const raw = readFileSync(CONFIG_FILE, "utf-8");
|
|
20
|
+
return JSON.parse(raw);
|
|
21
|
+
} catch {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function saveConfig(config) {
|
|
26
|
+
ensureDir();
|
|
27
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", { encoding: "utf-8", mode: 384 });
|
|
28
|
+
}
|
|
29
|
+
function updateConfig(partial) {
|
|
30
|
+
const existing = loadConfig();
|
|
31
|
+
saveConfig({ ...existing, ...partial });
|
|
32
|
+
}
|
|
33
|
+
function getConfigPath() {
|
|
34
|
+
return CONFIG_FILE;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/platform/auth.ts
|
|
38
|
+
function loadToken() {
|
|
39
|
+
return loadConfig().token;
|
|
40
|
+
}
|
|
41
|
+
function saveToken(token) {
|
|
42
|
+
updateConfig({ token });
|
|
43
|
+
}
|
|
44
|
+
function hasToken() {
|
|
45
|
+
return !!loadToken();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/platform/ws-client.ts
|
|
49
|
+
import { EventEmitter } from "events";
|
|
50
|
+
import WebSocket from "ws";
|
|
51
|
+
import { BRIDGE_PROTOCOL_VERSION } from "@annals/bridge-protocol";
|
|
52
|
+
|
|
53
|
+
// src/utils/logger.ts
|
|
54
|
+
var RESET = "\x1B[0m";
|
|
55
|
+
var RED = "\x1B[31m";
|
|
56
|
+
var GREEN = "\x1B[32m";
|
|
57
|
+
var YELLOW = "\x1B[33m";
|
|
58
|
+
var BLUE = "\x1B[34m";
|
|
59
|
+
var GRAY = "\x1B[90m";
|
|
60
|
+
var BOLD = "\x1B[1m";
|
|
61
|
+
function timestamp() {
|
|
62
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
|
|
63
|
+
}
|
|
64
|
+
var log = {
|
|
65
|
+
info(msg, ...args) {
|
|
66
|
+
console.log(`${GRAY}${timestamp()}${RESET} ${BLUE}INFO${RESET} ${msg}`, ...args);
|
|
67
|
+
},
|
|
68
|
+
success(msg, ...args) {
|
|
69
|
+
console.log(`${GRAY}${timestamp()}${RESET} ${GREEN}OK${RESET} ${msg}`, ...args);
|
|
70
|
+
},
|
|
71
|
+
warn(msg, ...args) {
|
|
72
|
+
console.warn(`${GRAY}${timestamp()}${RESET} ${YELLOW}WARN${RESET} ${msg}`, ...args);
|
|
73
|
+
},
|
|
74
|
+
error(msg, ...args) {
|
|
75
|
+
console.error(`${GRAY}${timestamp()}${RESET} ${RED}ERROR${RESET} ${msg}`, ...args);
|
|
76
|
+
},
|
|
77
|
+
debug(msg, ...args) {
|
|
78
|
+
if (process.env.DEBUG) {
|
|
79
|
+
console.log(`${GRAY}${timestamp()} DEBUG ${msg}${RESET}`, ...args);
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
banner(text) {
|
|
83
|
+
console.log(`
|
|
84
|
+
${BOLD}${text}${RESET}
|
|
85
|
+
`);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// src/platform/ws-client.ts
|
|
90
|
+
var HEARTBEAT_INTERVAL = 3e4;
|
|
91
|
+
var INITIAL_RECONNECT_DELAY = 1e3;
|
|
92
|
+
var MAX_RECONNECT_DELAY = 3e4;
|
|
93
|
+
var BridgeWSClient = class extends EventEmitter {
|
|
94
|
+
ws = null;
|
|
95
|
+
heartbeatTimer = null;
|
|
96
|
+
reconnectTimer = null;
|
|
97
|
+
reconnectDelay = INITIAL_RECONNECT_DELAY;
|
|
98
|
+
startTime = Date.now();
|
|
99
|
+
activeSessions = 0;
|
|
100
|
+
intentionalClose = false;
|
|
101
|
+
registered = false;
|
|
102
|
+
opts;
|
|
103
|
+
constructor(opts) {
|
|
104
|
+
super();
|
|
105
|
+
this.opts = opts;
|
|
106
|
+
}
|
|
107
|
+
async connect() {
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
this.intentionalClose = false;
|
|
110
|
+
this.registered = false;
|
|
111
|
+
try {
|
|
112
|
+
const wsUrl = new URL(this.opts.url);
|
|
113
|
+
wsUrl.searchParams.set("agent_id", this.opts.agentId);
|
|
114
|
+
this.ws = new WebSocket(wsUrl.toString());
|
|
115
|
+
} catch (err) {
|
|
116
|
+
reject(new Error(`Failed to create WebSocket: ${err}`));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const onFirstRegistered = (msg) => {
|
|
120
|
+
if (msg.type === "registered") {
|
|
121
|
+
this.off("_raw", onFirstRegistered);
|
|
122
|
+
if (msg.status === "ok") {
|
|
123
|
+
this.registered = true;
|
|
124
|
+
this.reconnectDelay = INITIAL_RECONNECT_DELAY;
|
|
125
|
+
this.startHeartbeat();
|
|
126
|
+
resolve();
|
|
127
|
+
} else {
|
|
128
|
+
reject(new Error(`Registration failed: ${msg.error || "unknown"}`));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
this.on("_raw", onFirstRegistered);
|
|
133
|
+
this.ws.on("open", () => {
|
|
134
|
+
log.debug("WebSocket connected, sending register...");
|
|
135
|
+
const reg = {
|
|
136
|
+
type: "register",
|
|
137
|
+
agent_id: this.opts.agentId,
|
|
138
|
+
token: this.opts.token,
|
|
139
|
+
bridge_version: String(BRIDGE_PROTOCOL_VERSION),
|
|
140
|
+
agent_type: this.opts.agentType,
|
|
141
|
+
capabilities: this.opts.capabilities || []
|
|
142
|
+
};
|
|
143
|
+
this.ws.send(JSON.stringify(reg));
|
|
144
|
+
});
|
|
145
|
+
this.ws.on("message", (data) => {
|
|
146
|
+
try {
|
|
147
|
+
const msg = JSON.parse(data.toString());
|
|
148
|
+
this.emit("_raw", msg);
|
|
149
|
+
if (this.registered) {
|
|
150
|
+
this.emit("message", msg);
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
log.debug("Failed to parse message from worker");
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
this.ws.on("close", (code, reason) => {
|
|
157
|
+
this.stopHeartbeat();
|
|
158
|
+
this.registered = false;
|
|
159
|
+
if (this.intentionalClose) {
|
|
160
|
+
log.info("Connection closed");
|
|
161
|
+
this.emit("close");
|
|
162
|
+
} else {
|
|
163
|
+
log.warn(`Connection lost (${code}: ${reason}), reconnecting in ${this.reconnectDelay}ms...`);
|
|
164
|
+
this.emit("disconnect");
|
|
165
|
+
this.scheduleReconnect();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
this.ws.on("error", (err) => {
|
|
169
|
+
log.error(`WebSocket error: ${err.message}`);
|
|
170
|
+
this.emit("error", err);
|
|
171
|
+
});
|
|
172
|
+
setTimeout(() => {
|
|
173
|
+
if (!this.registered) {
|
|
174
|
+
this.off("_raw", onFirstRegistered);
|
|
175
|
+
reject(new Error("Registration timed out"));
|
|
176
|
+
this.ws?.close();
|
|
177
|
+
}
|
|
178
|
+
}, 15e3);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
send(msg) {
|
|
182
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
183
|
+
this.ws.send(JSON.stringify(msg));
|
|
184
|
+
} else {
|
|
185
|
+
log.warn("Cannot send: WebSocket not connected");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
onMessage(cb) {
|
|
189
|
+
this.on("message", cb);
|
|
190
|
+
}
|
|
191
|
+
setActiveSessions(count) {
|
|
192
|
+
this.activeSessions = count;
|
|
193
|
+
}
|
|
194
|
+
close() {
|
|
195
|
+
this.intentionalClose = true;
|
|
196
|
+
this.stopHeartbeat();
|
|
197
|
+
if (this.reconnectTimer) {
|
|
198
|
+
clearTimeout(this.reconnectTimer);
|
|
199
|
+
this.reconnectTimer = null;
|
|
200
|
+
}
|
|
201
|
+
if (this.ws) {
|
|
202
|
+
this.ws.close();
|
|
203
|
+
this.ws = null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
get isConnected() {
|
|
207
|
+
return this.registered && this.ws?.readyState === WebSocket.OPEN;
|
|
208
|
+
}
|
|
209
|
+
startHeartbeat() {
|
|
210
|
+
this.stopHeartbeat();
|
|
211
|
+
this.heartbeatTimer = setInterval(() => {
|
|
212
|
+
this.send({
|
|
213
|
+
type: "heartbeat",
|
|
214
|
+
active_sessions: this.activeSessions,
|
|
215
|
+
uptime_ms: Date.now() - this.startTime
|
|
216
|
+
});
|
|
217
|
+
}, HEARTBEAT_INTERVAL);
|
|
218
|
+
}
|
|
219
|
+
stopHeartbeat() {
|
|
220
|
+
if (this.heartbeatTimer) {
|
|
221
|
+
clearInterval(this.heartbeatTimer);
|
|
222
|
+
this.heartbeatTimer = null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
scheduleReconnect() {
|
|
226
|
+
if (this.reconnectTimer) return;
|
|
227
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
228
|
+
this.reconnectTimer = null;
|
|
229
|
+
if (this.ws) {
|
|
230
|
+
try {
|
|
231
|
+
this.ws.close();
|
|
232
|
+
} catch {
|
|
233
|
+
}
|
|
234
|
+
this.ws = null;
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
log.info("Attempting reconnect...");
|
|
238
|
+
await this.connect();
|
|
239
|
+
log.success("Reconnected to bridge worker");
|
|
240
|
+
this.emit("reconnect");
|
|
241
|
+
} catch (err) {
|
|
242
|
+
log.error(`Reconnect failed: ${err}`);
|
|
243
|
+
this.reconnectDelay = Math.min(this.reconnectDelay * 2, MAX_RECONNECT_DELAY);
|
|
244
|
+
this.scheduleReconnect();
|
|
245
|
+
}
|
|
246
|
+
}, this.reconnectDelay);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// src/bridge/manager.ts
|
|
251
|
+
import { BridgeErrorCode } from "@annals/bridge-protocol";
|
|
252
|
+
|
|
253
|
+
// src/bridge/session-pool.ts
|
|
254
|
+
var SessionPool = class {
|
|
255
|
+
sessions = /* @__PURE__ */ new Map();
|
|
256
|
+
get(sessionId) {
|
|
257
|
+
const entry = this.sessions.get(sessionId);
|
|
258
|
+
if (entry) {
|
|
259
|
+
entry.lastActiveAt = Date.now();
|
|
260
|
+
}
|
|
261
|
+
return entry?.handle;
|
|
262
|
+
}
|
|
263
|
+
set(sessionId, handle) {
|
|
264
|
+
const now = Date.now();
|
|
265
|
+
this.sessions.set(sessionId, {
|
|
266
|
+
sessionId,
|
|
267
|
+
handle,
|
|
268
|
+
createdAt: now,
|
|
269
|
+
lastActiveAt: now
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
delete(sessionId) {
|
|
273
|
+
this.sessions.delete(sessionId);
|
|
274
|
+
}
|
|
275
|
+
has(sessionId) {
|
|
276
|
+
return this.sessions.has(sessionId);
|
|
277
|
+
}
|
|
278
|
+
get size() {
|
|
279
|
+
return this.sessions.size;
|
|
280
|
+
}
|
|
281
|
+
clear() {
|
|
282
|
+
for (const [id, entry] of this.sessions) {
|
|
283
|
+
log.debug(`Cleaning up session ${id}`);
|
|
284
|
+
entry.handle.kill();
|
|
285
|
+
}
|
|
286
|
+
this.sessions.clear();
|
|
287
|
+
}
|
|
288
|
+
keys() {
|
|
289
|
+
return this.sessions.keys();
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// src/bridge/manager.ts
|
|
294
|
+
var BridgeManager = class {
|
|
295
|
+
wsClient;
|
|
296
|
+
adapter;
|
|
297
|
+
adapterConfig;
|
|
298
|
+
pool = new SessionPool();
|
|
299
|
+
constructor(opts) {
|
|
300
|
+
this.wsClient = opts.wsClient;
|
|
301
|
+
this.adapter = opts.adapter;
|
|
302
|
+
this.adapterConfig = opts.adapterConfig;
|
|
303
|
+
}
|
|
304
|
+
start() {
|
|
305
|
+
this.wsClient.onMessage((msg) => this.handleWorkerMessage(msg));
|
|
306
|
+
log.info(`Bridge manager started with ${this.adapter.displayName} adapter`);
|
|
307
|
+
}
|
|
308
|
+
stop() {
|
|
309
|
+
this.pool.clear();
|
|
310
|
+
log.info("Bridge manager stopped");
|
|
311
|
+
}
|
|
312
|
+
get sessionCount() {
|
|
313
|
+
return this.pool.size;
|
|
314
|
+
}
|
|
315
|
+
handleWorkerMessage(msg) {
|
|
316
|
+
switch (msg.type) {
|
|
317
|
+
case "message":
|
|
318
|
+
this.handleMessage(msg);
|
|
319
|
+
break;
|
|
320
|
+
case "cancel":
|
|
321
|
+
this.handleCancel(msg);
|
|
322
|
+
break;
|
|
323
|
+
case "registered":
|
|
324
|
+
break;
|
|
325
|
+
default:
|
|
326
|
+
log.warn(`Unknown message type from worker: ${msg.type}`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
handleMessage(msg) {
|
|
330
|
+
const { session_id, request_id, content, attachments } = msg;
|
|
331
|
+
log.info(`Message received: session=${session_id.slice(0, 8)}... request=${request_id.slice(0, 8)}...`);
|
|
332
|
+
let handle = this.pool.get(session_id);
|
|
333
|
+
if (!handle) {
|
|
334
|
+
try {
|
|
335
|
+
handle = this.adapter.createSession(session_id, this.adapterConfig);
|
|
336
|
+
this.pool.set(session_id, handle);
|
|
337
|
+
this.updateSessionCount();
|
|
338
|
+
} catch (err) {
|
|
339
|
+
log.error(`Failed to create session: ${err}`);
|
|
340
|
+
this.sendError(session_id, request_id, BridgeErrorCode.ADAPTER_CRASH, `Failed to create session: ${err}`);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
this.wireSession(handle, session_id, request_id);
|
|
345
|
+
try {
|
|
346
|
+
handle.send(content, attachments);
|
|
347
|
+
} catch (err) {
|
|
348
|
+
log.error(`Failed to send to adapter: ${err}`);
|
|
349
|
+
this.sendError(session_id, request_id, BridgeErrorCode.ADAPTER_CRASH, `Adapter send failed: ${err}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
wireSession(handle, sessionId, requestId) {
|
|
353
|
+
handle.onChunk((delta) => {
|
|
354
|
+
const chunk = {
|
|
355
|
+
type: "chunk",
|
|
356
|
+
session_id: sessionId,
|
|
357
|
+
request_id: requestId,
|
|
358
|
+
delta
|
|
359
|
+
};
|
|
360
|
+
this.wsClient.send(chunk);
|
|
361
|
+
});
|
|
362
|
+
handle.onDone(() => {
|
|
363
|
+
const done = {
|
|
364
|
+
type: "done",
|
|
365
|
+
session_id: sessionId,
|
|
366
|
+
request_id: requestId
|
|
367
|
+
};
|
|
368
|
+
this.wsClient.send(done);
|
|
369
|
+
log.info(`Request done: session=${sessionId.slice(0, 8)}... request=${requestId.slice(0, 8)}...`);
|
|
370
|
+
});
|
|
371
|
+
handle.onError((err) => {
|
|
372
|
+
log.error(`Adapter error (session=${sessionId.slice(0, 8)}...): ${err.message}`);
|
|
373
|
+
this.sendError(sessionId, requestId, BridgeErrorCode.ADAPTER_CRASH, err.message);
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
handleCancel(msg) {
|
|
377
|
+
const { session_id } = msg;
|
|
378
|
+
log.info(`Cancel received: session=${session_id.slice(0, 8)}...`);
|
|
379
|
+
const handle = this.pool.get(session_id);
|
|
380
|
+
if (handle) {
|
|
381
|
+
handle.kill();
|
|
382
|
+
this.adapter.destroySession(session_id);
|
|
383
|
+
this.pool.delete(session_id);
|
|
384
|
+
this.updateSessionCount();
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
sendError(sessionId, requestId, code, message) {
|
|
388
|
+
const err = {
|
|
389
|
+
type: "error",
|
|
390
|
+
session_id: sessionId,
|
|
391
|
+
request_id: requestId,
|
|
392
|
+
code,
|
|
393
|
+
message
|
|
394
|
+
};
|
|
395
|
+
this.wsClient.send(err);
|
|
396
|
+
}
|
|
397
|
+
updateSessionCount() {
|
|
398
|
+
this.wsClient.setActiveSessions(this.pool.size);
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
// src/adapters/openclaw.ts
|
|
403
|
+
import WebSocket2 from "ws";
|
|
404
|
+
import { randomUUID } from "crypto";
|
|
405
|
+
|
|
406
|
+
// src/adapters/base.ts
|
|
407
|
+
var AgentAdapter = class {
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
// src/adapters/openclaw.ts
|
|
411
|
+
var DEFAULT_GATEWAY_URL = "ws://127.0.0.1:18789";
|
|
412
|
+
var OpenClawSession = class {
|
|
413
|
+
constructor(sessionId, config) {
|
|
414
|
+
this.config = config;
|
|
415
|
+
this.gatewayUrl = config.gatewayUrl || DEFAULT_GATEWAY_URL;
|
|
416
|
+
this.gatewayToken = config.gatewayToken || "";
|
|
417
|
+
this.sessionKey = `bridge:${sessionId}`;
|
|
418
|
+
}
|
|
419
|
+
chunkCallbacks = [];
|
|
420
|
+
doneCallbacks = [];
|
|
421
|
+
errorCallbacks = [];
|
|
422
|
+
fullText = "";
|
|
423
|
+
ws = null;
|
|
424
|
+
isConnected = false;
|
|
425
|
+
gatewayUrl;
|
|
426
|
+
gatewayToken;
|
|
427
|
+
sessionKey;
|
|
428
|
+
send(message, _attachments) {
|
|
429
|
+
if (this.ws && this.ws.readyState === WebSocket2.OPEN && this.isConnected) {
|
|
430
|
+
this.sendAgentRequest(message);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
this.fullText = "";
|
|
434
|
+
this.connectAndSend(message);
|
|
435
|
+
}
|
|
436
|
+
onChunk(cb) {
|
|
437
|
+
this.chunkCallbacks.push(cb);
|
|
438
|
+
}
|
|
439
|
+
onDone(cb) {
|
|
440
|
+
this.doneCallbacks.push(cb);
|
|
441
|
+
}
|
|
442
|
+
onError(cb) {
|
|
443
|
+
this.errorCallbacks.push(cb);
|
|
444
|
+
}
|
|
445
|
+
kill() {
|
|
446
|
+
if (this.ws) {
|
|
447
|
+
this.ws.close();
|
|
448
|
+
this.ws = null;
|
|
449
|
+
}
|
|
450
|
+
this.isConnected = false;
|
|
451
|
+
}
|
|
452
|
+
connectAndSend(message) {
|
|
453
|
+
try {
|
|
454
|
+
this.ws = new WebSocket2(this.gatewayUrl);
|
|
455
|
+
} catch (err) {
|
|
456
|
+
this.emitError(new Error(`Failed to connect to OpenClaw: ${err}`));
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
this.ws.on("open", () => {
|
|
460
|
+
log.debug(`OpenClaw WS connected to ${this.gatewayUrl}`);
|
|
461
|
+
const connectMsg = {
|
|
462
|
+
type: "req",
|
|
463
|
+
id: randomUUID(),
|
|
464
|
+
method: "connect",
|
|
465
|
+
params: {
|
|
466
|
+
minProtocol: 3,
|
|
467
|
+
maxProtocol: 3,
|
|
468
|
+
client: {
|
|
469
|
+
id: "gateway-client",
|
|
470
|
+
displayName: "Agent Bridge CLI",
|
|
471
|
+
version: "0.1.0",
|
|
472
|
+
platform: "node",
|
|
473
|
+
mode: "backend"
|
|
474
|
+
},
|
|
475
|
+
role: "operator",
|
|
476
|
+
scopes: ["operator.read", "operator.write"],
|
|
477
|
+
caps: [],
|
|
478
|
+
commands: [],
|
|
479
|
+
permissions: {},
|
|
480
|
+
auth: { token: this.gatewayToken }
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
this.ws.send(JSON.stringify(connectMsg));
|
|
484
|
+
});
|
|
485
|
+
this.ws.on("message", (data) => {
|
|
486
|
+
this.handleMessage(data.toString(), message);
|
|
487
|
+
});
|
|
488
|
+
this.ws.on("error", (err) => {
|
|
489
|
+
this.emitError(new Error(`OpenClaw WebSocket error: ${err.message}`));
|
|
490
|
+
});
|
|
491
|
+
this.ws.on("close", () => {
|
|
492
|
+
this.isConnected = false;
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
handleMessage(raw, pendingMessage) {
|
|
496
|
+
let msg;
|
|
497
|
+
try {
|
|
498
|
+
msg = JSON.parse(raw);
|
|
499
|
+
} catch {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
if (msg.type === "event" && msg.event !== "agent") {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (msg.type === "res" && !this.isConnected) {
|
|
506
|
+
if (msg.ok && msg.payload?.type === "hello-ok") {
|
|
507
|
+
this.isConnected = true;
|
|
508
|
+
log.debug("OpenClaw handshake complete");
|
|
509
|
+
if (pendingMessage) {
|
|
510
|
+
this.sendAgentRequest(pendingMessage);
|
|
511
|
+
}
|
|
512
|
+
} else {
|
|
513
|
+
this.emitError(
|
|
514
|
+
new Error(`OpenClaw auth failed: ${msg.error?.message || "unknown"}`)
|
|
515
|
+
);
|
|
516
|
+
this.ws?.close();
|
|
517
|
+
}
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (msg.type === "event" && msg.event === "agent" && msg.payload) {
|
|
521
|
+
const { stream, data } = msg.payload;
|
|
522
|
+
if (stream === "assistant" && data?.text) {
|
|
523
|
+
const prevLen = this.fullText.length;
|
|
524
|
+
this.fullText = data.text;
|
|
525
|
+
if (this.fullText.length > prevLen) {
|
|
526
|
+
const delta = this.fullText.slice(prevLen);
|
|
527
|
+
for (const cb of this.chunkCallbacks) cb(delta);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (stream === "lifecycle" && data?.phase === "end") {
|
|
531
|
+
for (const cb of this.doneCallbacks) cb();
|
|
532
|
+
}
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
if (msg.type === "res" && this.isConnected) {
|
|
536
|
+
if (msg.ok && msg.payload) {
|
|
537
|
+
if (msg.payload.status === "accepted") {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (msg.payload.response) {
|
|
541
|
+
for (const cb of this.chunkCallbacks) cb(msg.payload.response);
|
|
542
|
+
}
|
|
543
|
+
for (const cb of this.doneCallbacks) cb();
|
|
544
|
+
} else {
|
|
545
|
+
this.emitError(
|
|
546
|
+
new Error(`OpenClaw error: ${msg.error?.message || "unknown"}`)
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
if (msg.type === "error") {
|
|
552
|
+
this.emitError(new Error(`OpenClaw error: ${msg.message || "unknown"}`));
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
sendAgentRequest(message) {
|
|
556
|
+
const req = {
|
|
557
|
+
type: "req",
|
|
558
|
+
id: randomUUID(),
|
|
559
|
+
method: "agent",
|
|
560
|
+
params: {
|
|
561
|
+
message,
|
|
562
|
+
sessionKey: this.sessionKey,
|
|
563
|
+
idempotencyKey: `idem-${Date.now()}-${randomUUID().slice(0, 8)}`
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
try {
|
|
567
|
+
this.ws.send(JSON.stringify(req));
|
|
568
|
+
} catch (err) {
|
|
569
|
+
this.emitError(new Error(`Failed to send to OpenClaw: ${err}`));
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
emitError(err) {
|
|
573
|
+
if (this.errorCallbacks.length > 0) {
|
|
574
|
+
for (const cb of this.errorCallbacks) cb(err);
|
|
575
|
+
} else {
|
|
576
|
+
log.error(err.message);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
var OpenClawAdapter = class extends AgentAdapter {
|
|
581
|
+
type = "openclaw";
|
|
582
|
+
displayName = "OpenClaw Gateway";
|
|
583
|
+
sessions = /* @__PURE__ */ new Map();
|
|
584
|
+
config;
|
|
585
|
+
constructor(config = {}) {
|
|
586
|
+
super();
|
|
587
|
+
this.config = config;
|
|
588
|
+
}
|
|
589
|
+
async isAvailable() {
|
|
590
|
+
const url = this.config.gatewayUrl || DEFAULT_GATEWAY_URL;
|
|
591
|
+
return new Promise((resolve) => {
|
|
592
|
+
let ws;
|
|
593
|
+
const timer = setTimeout(() => {
|
|
594
|
+
ws?.close();
|
|
595
|
+
resolve(false);
|
|
596
|
+
}, 5e3);
|
|
597
|
+
try {
|
|
598
|
+
ws = new WebSocket2(url);
|
|
599
|
+
} catch {
|
|
600
|
+
clearTimeout(timer);
|
|
601
|
+
resolve(false);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
ws.on("open", () => {
|
|
605
|
+
clearTimeout(timer);
|
|
606
|
+
ws.close();
|
|
607
|
+
resolve(true);
|
|
608
|
+
});
|
|
609
|
+
ws.on("error", () => {
|
|
610
|
+
clearTimeout(timer);
|
|
611
|
+
resolve(false);
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
createSession(id, config) {
|
|
616
|
+
const merged = { ...this.config, ...config };
|
|
617
|
+
const session = new OpenClawSession(id, merged);
|
|
618
|
+
this.sessions.set(id, session);
|
|
619
|
+
return session;
|
|
620
|
+
}
|
|
621
|
+
destroySession(id) {
|
|
622
|
+
const session = this.sessions.get(id);
|
|
623
|
+
if (session) {
|
|
624
|
+
session.kill();
|
|
625
|
+
this.sessions.delete(id);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
// src/utils/process.ts
|
|
631
|
+
import { spawn } from "child_process";
|
|
632
|
+
|
|
633
|
+
// src/utils/sandbox.ts
|
|
634
|
+
import { execSync } from "child_process";
|
|
635
|
+
import { join as join2 } from "path";
|
|
636
|
+
var SRT_PACKAGE = "@anthropic-ai/sandbox-runtime";
|
|
637
|
+
var SENSITIVE_PATHS = [
|
|
638
|
+
// SSH & crypto keys
|
|
639
|
+
"~/.ssh",
|
|
640
|
+
"~/.gnupg",
|
|
641
|
+
// Cloud provider credentials
|
|
642
|
+
"~/.aws",
|
|
643
|
+
"~/.config/gcloud",
|
|
644
|
+
"~/.azure",
|
|
645
|
+
"~/.kube",
|
|
646
|
+
// Claude Code — fine-grained: block credentials & privacy, allow skills/agents
|
|
647
|
+
"~/.claude.json",
|
|
648
|
+
// API key
|
|
649
|
+
"~/.claude/projects",
|
|
650
|
+
// per-project memory (may contain secrets from other projects)
|
|
651
|
+
"~/.claude/history.jsonl",
|
|
652
|
+
// conversation history (privacy)
|
|
653
|
+
"~/.claude/settings.json",
|
|
654
|
+
// may contain sensitive config
|
|
655
|
+
"~/.claude/sessions",
|
|
656
|
+
// session data
|
|
657
|
+
"~/.claude/ide",
|
|
658
|
+
// IDE integration data
|
|
659
|
+
// NOT blocked (needed for functionality):
|
|
660
|
+
// ~/.claude/skills/ — skill code & prompts
|
|
661
|
+
// ~/.claude/agents/ — custom agent definitions
|
|
662
|
+
// ~/.claude/commands/ — custom commands
|
|
663
|
+
// ~/.claude/hooks/ — event hooks
|
|
664
|
+
// Other AI agent configs (contain API keys / tokens)
|
|
665
|
+
"~/.openclaw",
|
|
666
|
+
"~/.agent-bridge",
|
|
667
|
+
"~/.codex",
|
|
668
|
+
// Package manager tokens
|
|
669
|
+
"~/.npmrc",
|
|
670
|
+
"~/.yarnrc",
|
|
671
|
+
"~/.config/pip",
|
|
672
|
+
// Git credentials & config
|
|
673
|
+
"~/.gitconfig",
|
|
674
|
+
"~/.netrc",
|
|
675
|
+
"~/.git-credentials",
|
|
676
|
+
// Docker
|
|
677
|
+
"~/.docker",
|
|
678
|
+
// macOS Keychain databases
|
|
679
|
+
"~/Library/Keychains"
|
|
680
|
+
];
|
|
681
|
+
var SANDBOX_PRESETS = {
|
|
682
|
+
claude: {
|
|
683
|
+
denyRead: [...SENSITIVE_PATHS],
|
|
684
|
+
allowWrite: [".", "/tmp"],
|
|
685
|
+
denyWrite: [".env", ".env.*"]
|
|
686
|
+
},
|
|
687
|
+
codex: {
|
|
688
|
+
denyRead: [...SENSITIVE_PATHS],
|
|
689
|
+
allowWrite: [".", "/tmp"],
|
|
690
|
+
denyWrite: [".env", ".env.*"]
|
|
691
|
+
},
|
|
692
|
+
gemini: {
|
|
693
|
+
denyRead: [...SENSITIVE_PATHS],
|
|
694
|
+
allowWrite: [".", "/tmp"],
|
|
695
|
+
denyWrite: [".env", ".env.*"]
|
|
696
|
+
},
|
|
697
|
+
openclaw: {
|
|
698
|
+
denyRead: [...SENSITIVE_PATHS],
|
|
699
|
+
allowWrite: ["/tmp"],
|
|
700
|
+
denyWrite: [".env", ".env.*"]
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
var sandboxManager = null;
|
|
704
|
+
var sandboxInitialized = false;
|
|
705
|
+
async function importSandboxManager() {
|
|
706
|
+
try {
|
|
707
|
+
const globalRoot = execSync("npm root -g", { encoding: "utf-8" }).trim();
|
|
708
|
+
const srtPath = join2(globalRoot, "@anthropic-ai/sandbox-runtime/dist/index.js");
|
|
709
|
+
const mod = await import(srtPath);
|
|
710
|
+
return mod.SandboxManager;
|
|
711
|
+
} catch {
|
|
712
|
+
log.debug("Failed to import SandboxManager from global npm");
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
var _importOverride = null;
|
|
717
|
+
async function resolveManager() {
|
|
718
|
+
if (_importOverride) return _importOverride();
|
|
719
|
+
return importSandboxManager();
|
|
720
|
+
}
|
|
721
|
+
function getSandboxPreset(agentType) {
|
|
722
|
+
return SANDBOX_PRESETS[agentType] ?? SANDBOX_PRESETS.claude;
|
|
723
|
+
}
|
|
724
|
+
async function initSandbox(agentType) {
|
|
725
|
+
let mgr = await resolveManager();
|
|
726
|
+
if (!mgr) {
|
|
727
|
+
log.info("Sandbox runtime (srt) not found, installing...");
|
|
728
|
+
const installed = installSandboxRuntime();
|
|
729
|
+
if (!installed) return false;
|
|
730
|
+
mgr = await resolveManager();
|
|
731
|
+
if (!mgr) {
|
|
732
|
+
log.error("srt installed but SandboxManager not found. Try restarting your terminal.");
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
if (!mgr.isSupportedPlatform()) {
|
|
737
|
+
log.warn("Sandbox is not supported on this platform (requires macOS)");
|
|
738
|
+
return false;
|
|
739
|
+
}
|
|
740
|
+
const filesystem = getSandboxPreset(agentType);
|
|
741
|
+
try {
|
|
742
|
+
await mgr.initialize({
|
|
743
|
+
network: { allowedDomains: ["placeholder.example.com"], deniedDomains: [] },
|
|
744
|
+
filesystem
|
|
745
|
+
});
|
|
746
|
+
mgr.updateConfig({
|
|
747
|
+
network: { deniedDomains: [] },
|
|
748
|
+
filesystem
|
|
749
|
+
});
|
|
750
|
+
sandboxManager = mgr;
|
|
751
|
+
sandboxInitialized = true;
|
|
752
|
+
log.success("Sandbox enabled (srt programmatic API)");
|
|
753
|
+
return true;
|
|
754
|
+
} catch (err) {
|
|
755
|
+
log.error(`Failed to initialize sandbox: ${err}`);
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
async function wrapWithSandbox(command, filesystemOverride) {
|
|
760
|
+
if (!sandboxInitialized || !sandboxManager) return null;
|
|
761
|
+
if (filesystemOverride) {
|
|
762
|
+
sandboxManager.updateConfig({
|
|
763
|
+
filesystem: filesystemOverride
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
try {
|
|
767
|
+
return await sandboxManager.wrapWithSandbox(command);
|
|
768
|
+
} catch (err) {
|
|
769
|
+
log.error(`wrapWithSandbox failed: ${err}`);
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
async function resetSandbox() {
|
|
774
|
+
if (sandboxManager) {
|
|
775
|
+
try {
|
|
776
|
+
await sandboxManager.reset();
|
|
777
|
+
} catch {
|
|
778
|
+
}
|
|
779
|
+
sandboxManager = null;
|
|
780
|
+
sandboxInitialized = false;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
function shellQuote(s) {
|
|
784
|
+
if (/^[a-zA-Z0-9_\-./=:@]+$/.test(s)) return s;
|
|
785
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
786
|
+
}
|
|
787
|
+
function buildCommandString(command, args) {
|
|
788
|
+
return [command, ...args.map(shellQuote)].join(" ");
|
|
789
|
+
}
|
|
790
|
+
function installSandboxRuntime() {
|
|
791
|
+
log.info(`Installing ${SRT_PACKAGE}...`);
|
|
792
|
+
try {
|
|
793
|
+
execSync(`npm install -g ${SRT_PACKAGE}`, { stdio: "inherit" });
|
|
794
|
+
log.success(`${SRT_PACKAGE} installed successfully`);
|
|
795
|
+
return true;
|
|
796
|
+
} catch {
|
|
797
|
+
log.error(`Failed to install ${SRT_PACKAGE}. You can install it manually:`);
|
|
798
|
+
log.error(` npm install -g ${SRT_PACKAGE}`);
|
|
799
|
+
return false;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// src/utils/process.ts
|
|
804
|
+
async function spawnAgent(command, args, options) {
|
|
805
|
+
const { sandboxEnabled, sandboxFilesystem, ...spawnOptions } = options ?? {};
|
|
806
|
+
let finalCommand;
|
|
807
|
+
let finalArgs;
|
|
808
|
+
if (sandboxEnabled) {
|
|
809
|
+
const cmdString = buildCommandString(command, args);
|
|
810
|
+
const wrapped = await wrapWithSandbox(cmdString, sandboxFilesystem);
|
|
811
|
+
if (wrapped) {
|
|
812
|
+
finalCommand = "bash";
|
|
813
|
+
finalArgs = ["-c", wrapped];
|
|
814
|
+
} else {
|
|
815
|
+
finalCommand = command;
|
|
816
|
+
finalArgs = args;
|
|
817
|
+
}
|
|
818
|
+
} else {
|
|
819
|
+
finalCommand = command;
|
|
820
|
+
finalArgs = args;
|
|
821
|
+
}
|
|
822
|
+
const child = spawn(finalCommand, finalArgs, {
|
|
823
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
824
|
+
...spawnOptions
|
|
825
|
+
});
|
|
826
|
+
return {
|
|
827
|
+
child,
|
|
828
|
+
stdout: child.stdout,
|
|
829
|
+
stderr: child.stderr,
|
|
830
|
+
stdin: child.stdin,
|
|
831
|
+
kill() {
|
|
832
|
+
if (!child.killed) {
|
|
833
|
+
child.kill("SIGTERM");
|
|
834
|
+
setTimeout(() => {
|
|
835
|
+
if (!child.killed) {
|
|
836
|
+
child.kill("SIGKILL");
|
|
837
|
+
}
|
|
838
|
+
}, 5e3);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// src/adapters/claude.ts
|
|
845
|
+
import { createInterface } from "readline";
|
|
846
|
+
|
|
847
|
+
// src/utils/which.ts
|
|
848
|
+
import { execFile } from "child_process";
|
|
849
|
+
var ALLOWED_COMMANDS = /^[a-zA-Z0-9._-]+$/;
|
|
850
|
+
function which(command) {
|
|
851
|
+
if (!ALLOWED_COMMANDS.test(command)) {
|
|
852
|
+
return Promise.resolve(null);
|
|
853
|
+
}
|
|
854
|
+
return new Promise((resolve) => {
|
|
855
|
+
execFile("which", [command], (err, stdout) => {
|
|
856
|
+
if (err || !stdout.trim()) {
|
|
857
|
+
resolve(null);
|
|
858
|
+
} else {
|
|
859
|
+
resolve(stdout.trim());
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// src/utils/session-workspace.ts
|
|
866
|
+
import { mkdirSync as mkdirSync2, rmSync, existsSync as existsSync2 } from "fs";
|
|
867
|
+
import { execSync as execSync2 } from "child_process";
|
|
868
|
+
import { join as join3 } from "path";
|
|
869
|
+
import { tmpdir } from "os";
|
|
870
|
+
var SESSIONS_BASE = join3(tmpdir(), "agent-bridge-sessions");
|
|
871
|
+
function isGitRepo(dir) {
|
|
872
|
+
try {
|
|
873
|
+
execSync2("git rev-parse --git-dir", { cwd: dir, stdio: "ignore" });
|
|
874
|
+
return true;
|
|
875
|
+
} catch {
|
|
876
|
+
return false;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
function createSessionWorkspace(sessionId, projectPath) {
|
|
880
|
+
const sessionDir = join3(SESSIONS_BASE, sessionId);
|
|
881
|
+
if (projectPath && isGitRepo(projectPath)) {
|
|
882
|
+
try {
|
|
883
|
+
execSync2(
|
|
884
|
+
`git worktree add --detach ${JSON.stringify(sessionDir)}`,
|
|
885
|
+
{ cwd: projectPath, stdio: "ignore" }
|
|
886
|
+
);
|
|
887
|
+
log.debug(`Created git worktree for session ${sessionId.slice(0, 8)}...`);
|
|
888
|
+
return { path: sessionDir, isWorktree: true };
|
|
889
|
+
} catch (err) {
|
|
890
|
+
log.warn(`Failed to create git worktree: ${err}. Falling back to temp directory.`);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
mkdirSync2(sessionDir, { recursive: true });
|
|
894
|
+
log.debug(`Created temp workspace for session ${sessionId.slice(0, 8)}...`);
|
|
895
|
+
return { path: sessionDir, isWorktree: false };
|
|
896
|
+
}
|
|
897
|
+
function destroySessionWorkspace(sessionId, projectPath) {
|
|
898
|
+
const sessionDir = join3(SESSIONS_BASE, sessionId);
|
|
899
|
+
if (!existsSync2(sessionDir)) return;
|
|
900
|
+
if (projectPath) {
|
|
901
|
+
try {
|
|
902
|
+
execSync2(
|
|
903
|
+
`git worktree remove --force ${JSON.stringify(sessionDir)}`,
|
|
904
|
+
{ cwd: projectPath, stdio: "ignore" }
|
|
905
|
+
);
|
|
906
|
+
log.debug(`Removed git worktree for session ${sessionId.slice(0, 8)}...`);
|
|
907
|
+
return;
|
|
908
|
+
} catch {
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
rmSync(sessionDir, { recursive: true, force: true });
|
|
912
|
+
log.debug(`Removed temp workspace for session ${sessionId.slice(0, 8)}...`);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// src/adapters/claude.ts
|
|
916
|
+
var IDLE_TIMEOUT = 5 * 60 * 1e3;
|
|
917
|
+
var ClaudeSession = class {
|
|
918
|
+
constructor(sessionId, config, sandboxFilesystem) {
|
|
919
|
+
this.sessionId = sessionId;
|
|
920
|
+
this.config = config;
|
|
921
|
+
this.sandboxFilesystem = sandboxFilesystem;
|
|
922
|
+
}
|
|
923
|
+
chunkCallbacks = [];
|
|
924
|
+
doneCallbacks = [];
|
|
925
|
+
errorCallbacks = [];
|
|
926
|
+
process = null;
|
|
927
|
+
idleTimer = null;
|
|
928
|
+
config;
|
|
929
|
+
sandboxFilesystem;
|
|
930
|
+
send(message, _attachments) {
|
|
931
|
+
this.resetIdleTimer();
|
|
932
|
+
const args = ["-p", message, "--output-format", "stream-json", "--verbose", "--max-turns", "1"];
|
|
933
|
+
if (this.config.project) {
|
|
934
|
+
args.push("--project", this.config.project);
|
|
935
|
+
}
|
|
936
|
+
this.launchProcess(args);
|
|
937
|
+
}
|
|
938
|
+
async launchProcess(args) {
|
|
939
|
+
try {
|
|
940
|
+
this.process = await spawnAgent("claude", args, {
|
|
941
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
942
|
+
sandboxEnabled: this.config.sandboxEnabled,
|
|
943
|
+
sandboxFilesystem: this.sandboxFilesystem
|
|
944
|
+
});
|
|
945
|
+
} catch (err) {
|
|
946
|
+
this.emitError(new Error(`Failed to spawn claude: ${err}`));
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
const rl = createInterface({ input: this.process.stdout });
|
|
950
|
+
rl.on("line", (line) => {
|
|
951
|
+
this.resetIdleTimer();
|
|
952
|
+
if (!line.trim()) return;
|
|
953
|
+
try {
|
|
954
|
+
const event = JSON.parse(line);
|
|
955
|
+
this.handleEvent(event);
|
|
956
|
+
} catch {
|
|
957
|
+
log.debug(`Claude non-JSON line: ${line}`);
|
|
958
|
+
}
|
|
959
|
+
});
|
|
960
|
+
this.process.stderr.on("data", (data) => {
|
|
961
|
+
const text = data.toString().trim();
|
|
962
|
+
if (text) log.debug(`Claude stderr: ${text}`);
|
|
963
|
+
});
|
|
964
|
+
this.process.child.on("exit", (code) => {
|
|
965
|
+
if (code !== 0 && code !== null) {
|
|
966
|
+
this.emitError(new Error(`Claude process exited with code ${code}`));
|
|
967
|
+
}
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
onChunk(cb) {
|
|
971
|
+
this.chunkCallbacks.push(cb);
|
|
972
|
+
}
|
|
973
|
+
onDone(cb) {
|
|
974
|
+
this.doneCallbacks.push(cb);
|
|
975
|
+
}
|
|
976
|
+
onError(cb) {
|
|
977
|
+
this.errorCallbacks.push(cb);
|
|
978
|
+
}
|
|
979
|
+
kill() {
|
|
980
|
+
this.clearIdleTimer();
|
|
981
|
+
if (this.process) {
|
|
982
|
+
this.process.kill();
|
|
983
|
+
this.process = null;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
handleEvent(event) {
|
|
987
|
+
if (event.type === "assistant" && event.subtype === "text_delta" && event.delta?.text) {
|
|
988
|
+
for (const cb of this.chunkCallbacks) cb(event.delta.text);
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
if (event.type === "content_block_delta" && event.delta?.type === "text_delta" && event.delta?.text) {
|
|
992
|
+
for (const cb of this.chunkCallbacks) cb(event.delta.text);
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
if (event.type === "assistant" && event.message?.content) {
|
|
996
|
+
for (const block of event.message.content) {
|
|
997
|
+
if (block.type === "text" && block.text) {
|
|
998
|
+
for (const cb of this.chunkCallbacks) cb(block.text);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
if (event.type === "result") {
|
|
1004
|
+
if (event.result?.type === "string" || typeof event.result === "string") {
|
|
1005
|
+
const text = event.result;
|
|
1006
|
+
if (text) {
|
|
1007
|
+
for (const cb of this.chunkCallbacks) cb(text);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
for (const cb of this.doneCallbacks) cb();
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
if (event.type === "assistant" && event.subtype === "end") {
|
|
1014
|
+
for (const cb of this.doneCallbacks) cb();
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
resetIdleTimer() {
|
|
1019
|
+
this.clearIdleTimer();
|
|
1020
|
+
this.idleTimer = setTimeout(() => {
|
|
1021
|
+
log.warn(`Claude session ${this.sessionId} idle timeout, killing process`);
|
|
1022
|
+
this.kill();
|
|
1023
|
+
}, IDLE_TIMEOUT);
|
|
1024
|
+
}
|
|
1025
|
+
clearIdleTimer() {
|
|
1026
|
+
if (this.idleTimer) {
|
|
1027
|
+
clearTimeout(this.idleTimer);
|
|
1028
|
+
this.idleTimer = null;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
emitError(err) {
|
|
1032
|
+
if (this.errorCallbacks.length > 0) {
|
|
1033
|
+
for (const cb of this.errorCallbacks) cb(err);
|
|
1034
|
+
} else {
|
|
1035
|
+
log.error(err.message);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
};
|
|
1039
|
+
var ClaudeAdapter = class extends AgentAdapter {
|
|
1040
|
+
type = "claude";
|
|
1041
|
+
displayName = "Claude Code";
|
|
1042
|
+
sessions = /* @__PURE__ */ new Map();
|
|
1043
|
+
config;
|
|
1044
|
+
constructor(config = {}) {
|
|
1045
|
+
super();
|
|
1046
|
+
this.config = config;
|
|
1047
|
+
}
|
|
1048
|
+
async isAvailable() {
|
|
1049
|
+
return !!await which("claude");
|
|
1050
|
+
}
|
|
1051
|
+
createSession(id, config) {
|
|
1052
|
+
const merged = { ...this.config, ...config };
|
|
1053
|
+
let workspace = null;
|
|
1054
|
+
let sessionConfig = merged;
|
|
1055
|
+
let sandboxFilesystem;
|
|
1056
|
+
if (merged.sandboxEnabled && merged.project) {
|
|
1057
|
+
workspace = createSessionWorkspace(id, merged.project);
|
|
1058
|
+
const projectPath = workspace.isWorktree ? workspace.path : merged.project;
|
|
1059
|
+
const preset = getSandboxPreset("claude");
|
|
1060
|
+
sandboxFilesystem = {
|
|
1061
|
+
denyRead: preset.denyRead,
|
|
1062
|
+
allowWrite: [workspace.path, "/tmp"],
|
|
1063
|
+
denyWrite: preset.denyWrite
|
|
1064
|
+
};
|
|
1065
|
+
sessionConfig = {
|
|
1066
|
+
...merged,
|
|
1067
|
+
project: projectPath
|
|
1068
|
+
};
|
|
1069
|
+
log.info(`Session ${id.slice(0, 8)}... workspace: ${workspace.path} (${workspace.isWorktree ? "git worktree" : "temp dir"})`);
|
|
1070
|
+
}
|
|
1071
|
+
const session = new ClaudeSession(id, sessionConfig, sandboxFilesystem);
|
|
1072
|
+
this.sessions.set(id, { session, workspace });
|
|
1073
|
+
return session;
|
|
1074
|
+
}
|
|
1075
|
+
destroySession(id) {
|
|
1076
|
+
const entry = this.sessions.get(id);
|
|
1077
|
+
if (entry) {
|
|
1078
|
+
entry.session.kill();
|
|
1079
|
+
if (entry.workspace) {
|
|
1080
|
+
destroySessionWorkspace(id, this.config.project);
|
|
1081
|
+
}
|
|
1082
|
+
this.sessions.delete(id);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1086
|
+
|
|
1087
|
+
// src/adapters/codex.ts
|
|
1088
|
+
var CodexAdapter = class extends AgentAdapter {
|
|
1089
|
+
type = "codex";
|
|
1090
|
+
displayName = "Codex CLI";
|
|
1091
|
+
async isAvailable() {
|
|
1092
|
+
return false;
|
|
1093
|
+
}
|
|
1094
|
+
createSession(_id, _config) {
|
|
1095
|
+
throw new Error("Codex adapter not yet implemented");
|
|
1096
|
+
}
|
|
1097
|
+
destroySession(_id) {
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
|
|
1101
|
+
// src/adapters/gemini.ts
|
|
1102
|
+
var GeminiAdapter = class extends AgentAdapter {
|
|
1103
|
+
type = "gemini";
|
|
1104
|
+
displayName = "Gemini CLI";
|
|
1105
|
+
async isAvailable() {
|
|
1106
|
+
return false;
|
|
1107
|
+
}
|
|
1108
|
+
createSession(_id, _config) {
|
|
1109
|
+
throw new Error("Gemini adapter not yet implemented");
|
|
1110
|
+
}
|
|
1111
|
+
destroySession(_id) {
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
|
|
1115
|
+
// src/utils/openclaw-config.ts
|
|
1116
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1117
|
+
import { join as join4 } from "path";
|
|
1118
|
+
import { homedir as homedir2 } from "os";
|
|
1119
|
+
var OPENCLAW_CONFIG_PATH = join4(homedir2(), ".openclaw", "openclaw.json");
|
|
1120
|
+
function readOpenClawToken(configPath) {
|
|
1121
|
+
const path = configPath || OPENCLAW_CONFIG_PATH;
|
|
1122
|
+
try {
|
|
1123
|
+
const raw = readFileSync2(path, "utf-8");
|
|
1124
|
+
const config = JSON.parse(raw);
|
|
1125
|
+
const token = config?.gateway?.auth?.token;
|
|
1126
|
+
if (typeof token === "string" && token.length > 0) {
|
|
1127
|
+
return token;
|
|
1128
|
+
}
|
|
1129
|
+
log.warn("OpenClaw config found but gateway.auth.token is missing or empty");
|
|
1130
|
+
return null;
|
|
1131
|
+
} catch {
|
|
1132
|
+
return null;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// src/commands/connect.ts
|
|
1137
|
+
var DEFAULT_BRIDGE_URL = "wss://bridge.agents.hot/ws";
|
|
1138
|
+
function createAdapter(type, config) {
|
|
1139
|
+
switch (type) {
|
|
1140
|
+
case "openclaw":
|
|
1141
|
+
return new OpenClawAdapter(config);
|
|
1142
|
+
case "claude":
|
|
1143
|
+
return new ClaudeAdapter(config);
|
|
1144
|
+
case "codex":
|
|
1145
|
+
return new CodexAdapter(config);
|
|
1146
|
+
case "gemini":
|
|
1147
|
+
return new GeminiAdapter(config);
|
|
1148
|
+
default:
|
|
1149
|
+
throw new Error(`Unknown agent type: ${type}. Supported: openclaw, claude, codex, gemini`);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
function registerConnectCommand(program2) {
|
|
1153
|
+
program2.command("connect [type]").description("Connect a local agent to the Agents.Hot platform").option("--setup <url>", "One-click setup from agents.hot connect ticket URL").option("--agent-id <id>", "Agent ID registered on Agents.Hot").option("--project <path>", "Project path (for claude adapter)").option("--gateway-url <url>", "OpenClaw gateway URL (for openclaw adapter)").option("--gateway-token <token>", "OpenClaw gateway token").option("--bridge-url <url>", "Bridge Worker WebSocket URL").option("--sandbox", "Run agent inside a sandbox (requires srt)").option("--no-sandbox", "Disable sandbox even if enabled in config").action(async (type, opts) => {
|
|
1154
|
+
const config = loadConfig();
|
|
1155
|
+
if (opts.setup) {
|
|
1156
|
+
log.info("Fetching configuration from connect ticket...");
|
|
1157
|
+
try {
|
|
1158
|
+
const response = await fetch(opts.setup);
|
|
1159
|
+
if (!response.ok) {
|
|
1160
|
+
const body = await response.json().catch(() => ({ message: response.statusText }));
|
|
1161
|
+
log.error(`Ticket redemption failed: ${body.message || response.statusText}`);
|
|
1162
|
+
if (response.status === 404) {
|
|
1163
|
+
log.error("The ticket may have expired or already been used.");
|
|
1164
|
+
}
|
|
1165
|
+
process.exit(1);
|
|
1166
|
+
}
|
|
1167
|
+
const ticketData = await response.json();
|
|
1168
|
+
let gatewayToken = opts.gatewayToken;
|
|
1169
|
+
if (ticketData.agent_type === "openclaw" && !gatewayToken) {
|
|
1170
|
+
const localToken = readOpenClawToken();
|
|
1171
|
+
if (localToken) {
|
|
1172
|
+
gatewayToken = localToken;
|
|
1173
|
+
log.success("Auto-detected OpenClaw gateway token from ~/.openclaw/openclaw.json");
|
|
1174
|
+
} else {
|
|
1175
|
+
log.warn("Could not auto-detect OpenClaw token. Use --gateway-token to provide it manually.");
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
updateConfig({
|
|
1179
|
+
agentId: ticketData.agent_id,
|
|
1180
|
+
token: ticketData.bridge_token,
|
|
1181
|
+
defaultAgentType: ticketData.agent_type,
|
|
1182
|
+
bridgeUrl: ticketData.bridge_url,
|
|
1183
|
+
gatewayToken
|
|
1184
|
+
});
|
|
1185
|
+
log.success("Configuration saved to ~/.agent-bridge/config.json");
|
|
1186
|
+
opts.agentId = ticketData.agent_id;
|
|
1187
|
+
opts.bridgeUrl = ticketData.bridge_url;
|
|
1188
|
+
opts.gatewayToken = gatewayToken;
|
|
1189
|
+
type = ticketData.agent_type;
|
|
1190
|
+
} catch (err) {
|
|
1191
|
+
if (err instanceof Error && err.message.includes("fetch")) {
|
|
1192
|
+
log.error(`Failed to fetch ticket: ${err.message}`);
|
|
1193
|
+
} else {
|
|
1194
|
+
throw err;
|
|
1195
|
+
}
|
|
1196
|
+
process.exit(1);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
const agentType = type || config.defaultAgentType;
|
|
1200
|
+
if (!agentType) {
|
|
1201
|
+
log.error("Agent type is required. Use: agent-bridge connect <type> or agent-bridge connect --setup <url>");
|
|
1202
|
+
process.exit(1);
|
|
1203
|
+
}
|
|
1204
|
+
const agentId = opts.agentId || config.agentId;
|
|
1205
|
+
if (!agentId) {
|
|
1206
|
+
log.error("--agent-id is required. Use --setup for automatic configuration.");
|
|
1207
|
+
process.exit(1);
|
|
1208
|
+
}
|
|
1209
|
+
const token = opts.setup ? loadConfig().token : loadToken() || config.token;
|
|
1210
|
+
if (!token) {
|
|
1211
|
+
log.error("Not authenticated. Run `agent-bridge login` or use `agent-bridge connect --setup <url>`.");
|
|
1212
|
+
process.exit(1);
|
|
1213
|
+
}
|
|
1214
|
+
const bridgeUrl = opts.bridgeUrl || config.bridgeUrl || DEFAULT_BRIDGE_URL;
|
|
1215
|
+
const sandboxEnabled = opts.sandbox ?? config.sandbox ?? false;
|
|
1216
|
+
if (sandboxEnabled) {
|
|
1217
|
+
const ok = await initSandbox(agentType);
|
|
1218
|
+
if (!ok) {
|
|
1219
|
+
log.warn("Sandbox not available on this platform, continuing without sandbox");
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
const adapterConfig = {
|
|
1223
|
+
project: opts.project,
|
|
1224
|
+
gatewayUrl: opts.gatewayUrl || config.gatewayUrl,
|
|
1225
|
+
gatewayToken: opts.gatewayToken || config.gatewayToken,
|
|
1226
|
+
sandboxEnabled
|
|
1227
|
+
};
|
|
1228
|
+
const adapter = createAdapter(agentType, adapterConfig);
|
|
1229
|
+
log.info(`Checking ${adapter.displayName} availability...`);
|
|
1230
|
+
const available = await adapter.isAvailable();
|
|
1231
|
+
if (!available) {
|
|
1232
|
+
if (agentType === "codex" || agentType === "gemini") {
|
|
1233
|
+
log.error(`${adapter.displayName} adapter is not yet implemented. Supported adapters: openclaw, claude`);
|
|
1234
|
+
} else {
|
|
1235
|
+
log.error(`${adapter.displayName} is not available. Make sure it is installed and running.`);
|
|
1236
|
+
}
|
|
1237
|
+
process.exit(1);
|
|
1238
|
+
}
|
|
1239
|
+
log.success(`${adapter.displayName} is available`);
|
|
1240
|
+
log.info(`Connecting to bridge worker at ${bridgeUrl}...`);
|
|
1241
|
+
const wsClient = new BridgeWSClient({
|
|
1242
|
+
url: bridgeUrl,
|
|
1243
|
+
token,
|
|
1244
|
+
agentId,
|
|
1245
|
+
agentType
|
|
1246
|
+
});
|
|
1247
|
+
try {
|
|
1248
|
+
await wsClient.connect();
|
|
1249
|
+
} catch (err) {
|
|
1250
|
+
log.error(`Failed to connect to bridge worker: ${err}`);
|
|
1251
|
+
process.exit(1);
|
|
1252
|
+
}
|
|
1253
|
+
log.success(`Registered as agent "${agentId}" (${agentType})`);
|
|
1254
|
+
const manager = new BridgeManager({
|
|
1255
|
+
wsClient,
|
|
1256
|
+
adapter,
|
|
1257
|
+
adapterConfig
|
|
1258
|
+
});
|
|
1259
|
+
manager.start();
|
|
1260
|
+
log.banner(`Agent bridge is running. Press Ctrl+C to stop.`);
|
|
1261
|
+
const shutdown = () => {
|
|
1262
|
+
log.info("Shutting down...");
|
|
1263
|
+
manager.stop();
|
|
1264
|
+
wsClient.close();
|
|
1265
|
+
resetSandbox();
|
|
1266
|
+
process.exit(0);
|
|
1267
|
+
};
|
|
1268
|
+
process.on("SIGINT", shutdown);
|
|
1269
|
+
process.on("SIGTERM", shutdown);
|
|
1270
|
+
wsClient.on("reconnect", () => {
|
|
1271
|
+
manager.stop();
|
|
1272
|
+
manager.start();
|
|
1273
|
+
});
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// src/commands/login.ts
|
|
1278
|
+
import { createInterface as createInterface2 } from "readline";
|
|
1279
|
+
function readLine(prompt) {
|
|
1280
|
+
const rl = createInterface2({
|
|
1281
|
+
input: process.stdin,
|
|
1282
|
+
output: process.stderr
|
|
1283
|
+
});
|
|
1284
|
+
return new Promise((resolve) => {
|
|
1285
|
+
rl.question(prompt, (answer) => {
|
|
1286
|
+
rl.close();
|
|
1287
|
+
resolve(answer.trim());
|
|
1288
|
+
});
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
function registerLoginCommand(program2) {
|
|
1292
|
+
program2.command("login").description("Authenticate with the Agents.Hot platform").option("--token <token>", "Provide token directly (skip interactive prompt)").action(async (opts) => {
|
|
1293
|
+
if (hasToken()) {
|
|
1294
|
+
log.info("You are already logged in. Use --token to update your token.");
|
|
1295
|
+
}
|
|
1296
|
+
let token = opts.token;
|
|
1297
|
+
if (!token) {
|
|
1298
|
+
log.banner("Agent Bridge Login");
|
|
1299
|
+
console.log("1. Visit https://agents.hot/dashboard/settings to get your CLI token");
|
|
1300
|
+
console.log("2. Copy the token and paste it below\n");
|
|
1301
|
+
token = await readLine("Token: ");
|
|
1302
|
+
}
|
|
1303
|
+
if (!token) {
|
|
1304
|
+
log.error("No token provided");
|
|
1305
|
+
process.exit(1);
|
|
1306
|
+
}
|
|
1307
|
+
saveToken(token);
|
|
1308
|
+
log.success(`Token saved to ${getConfigPath()}`);
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// src/commands/status.ts
|
|
1313
|
+
function registerStatusCommand(program2) {
|
|
1314
|
+
program2.command("status").description("Check authentication and connection status").action(async () => {
|
|
1315
|
+
log.banner("Agent Bridge Status");
|
|
1316
|
+
const config = loadConfig();
|
|
1317
|
+
const configPath = getConfigPath();
|
|
1318
|
+
console.log(`Config: ${configPath}`);
|
|
1319
|
+
if (!hasToken()) {
|
|
1320
|
+
console.log("Auth: Not logged in");
|
|
1321
|
+
console.log("\nRun `agent-bridge login` to authenticate.");
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
const token = loadToken();
|
|
1325
|
+
const maskedToken = token.slice(0, 8) + "..." + token.slice(-4);
|
|
1326
|
+
console.log(`Auth: Logged in (token: ${maskedToken})`);
|
|
1327
|
+
if (config.defaultAgentType) {
|
|
1328
|
+
console.log(`Agent: ${config.defaultAgentType}`);
|
|
1329
|
+
}
|
|
1330
|
+
if (config.bridgeUrl) {
|
|
1331
|
+
console.log(`Bridge: ${config.bridgeUrl}`);
|
|
1332
|
+
}
|
|
1333
|
+
if (config.gatewayUrl) {
|
|
1334
|
+
console.log(`Gateway: ${config.gatewayUrl}`);
|
|
1335
|
+
}
|
|
1336
|
+
console.log("\nTo connect an agent, run:");
|
|
1337
|
+
console.log(" agent-bridge connect <type> --agent-id <id>");
|
|
1338
|
+
console.log("\nSupported types: openclaw, claude, codex, gemini");
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// src/index.ts
|
|
1343
|
+
program.name("agent-bridge").description("Connect local AI agents to the Skills.Hot platform").version("0.1.0");
|
|
1344
|
+
registerConnectCommand(program);
|
|
1345
|
+
registerLoginCommand(program);
|
|
1346
|
+
registerStatusCommand(program);
|
|
1347
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@annals/agent-bridge",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI bridge connecting local AI agents to the Agents.Hot platform",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"agent-bridge": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsup",
|
|
12
|
+
"dev": "tsup --watch"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@annals/bridge-protocol": "^0.1.0",
|
|
16
|
+
"commander": "^13.0.0",
|
|
17
|
+
"ws": "^8.18.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/ws": "^8.5.0",
|
|
21
|
+
"tsup": "^8.3.0",
|
|
22
|
+
"typescript": "^5.7.0"
|
|
23
|
+
},
|
|
24
|
+
"files": ["dist"],
|
|
25
|
+
"license": "MIT"
|
|
26
|
+
}
|