@annals/agent-mesh 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/LICENSE +21 -0
- package/README.md +92 -0
- package/dist/chunk-GIEYJIVW.js +936 -0
- package/dist/chunk-W24WCWEC.js +86 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4106 -0
- package/dist/list-6CHWMM3O.js +9 -0
- package/dist/openclaw-config-OFFNWVDK.js +11 -0
- package/package.json +37 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
log,
|
|
4
|
+
readOpenClawToken
|
|
5
|
+
} from "./chunk-W24WCWEC.js";
|
|
6
|
+
import {
|
|
7
|
+
BOLD,
|
|
8
|
+
GRAY,
|
|
9
|
+
GREEN,
|
|
10
|
+
RESET,
|
|
11
|
+
YELLOW,
|
|
12
|
+
addAgent,
|
|
13
|
+
findAgentByAgentId,
|
|
14
|
+
getAgent,
|
|
15
|
+
getAgentWorkspaceDir,
|
|
16
|
+
getConfigPath,
|
|
17
|
+
getLogPath,
|
|
18
|
+
isProcessAlive,
|
|
19
|
+
listAgents,
|
|
20
|
+
loadConfig,
|
|
21
|
+
readPid,
|
|
22
|
+
registerListCommand,
|
|
23
|
+
removeAgent,
|
|
24
|
+
removePid,
|
|
25
|
+
renderTable,
|
|
26
|
+
spawnBackground,
|
|
27
|
+
stopProcess,
|
|
28
|
+
uniqueSlug,
|
|
29
|
+
updateConfig,
|
|
30
|
+
writePid
|
|
31
|
+
} from "./chunk-GIEYJIVW.js";
|
|
32
|
+
|
|
33
|
+
// src/index.ts
|
|
34
|
+
import { createRequire } from "module";
|
|
35
|
+
import { program } from "commander";
|
|
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, WS_CLOSE_REPLACED, WS_CLOSE_TOKEN_REVOKED } from "@annals/bridge-protocol";
|
|
52
|
+
var HEARTBEAT_INTERVAL = 2e4;
|
|
53
|
+
var INITIAL_RECONNECT_DELAY = 1e3;
|
|
54
|
+
var MAX_RECONNECT_DELAY = 3e4;
|
|
55
|
+
var BridgeWSClient = class extends EventEmitter {
|
|
56
|
+
ws = null;
|
|
57
|
+
heartbeatTimer = null;
|
|
58
|
+
reconnectTimer = null;
|
|
59
|
+
reconnectDelay = INITIAL_RECONNECT_DELAY;
|
|
60
|
+
startTime = Date.now();
|
|
61
|
+
activeSessions = 0;
|
|
62
|
+
intentionalClose = false;
|
|
63
|
+
registered = false;
|
|
64
|
+
sendWarnSuppressed = false;
|
|
65
|
+
opts;
|
|
66
|
+
constructor(opts) {
|
|
67
|
+
super();
|
|
68
|
+
this.opts = opts;
|
|
69
|
+
}
|
|
70
|
+
async connect() {
|
|
71
|
+
return new Promise((resolve2, reject) => {
|
|
72
|
+
this.intentionalClose = false;
|
|
73
|
+
this.registered = false;
|
|
74
|
+
try {
|
|
75
|
+
const wsUrl = new URL(this.opts.url);
|
|
76
|
+
wsUrl.searchParams.set("agent_id", this.opts.agentId);
|
|
77
|
+
this.ws = new WebSocket(wsUrl.toString());
|
|
78
|
+
} catch (err) {
|
|
79
|
+
reject(new Error(`Failed to create WebSocket: ${err}`));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const onFirstRegistered = (msg) => {
|
|
83
|
+
if (msg.type === "registered") {
|
|
84
|
+
this.off("_raw", onFirstRegistered);
|
|
85
|
+
if (msg.status === "ok") {
|
|
86
|
+
this.registered = true;
|
|
87
|
+
this.reconnectDelay = INITIAL_RECONNECT_DELAY;
|
|
88
|
+
this.startHeartbeat();
|
|
89
|
+
resolve2();
|
|
90
|
+
} else {
|
|
91
|
+
reject(new Error(`Registration failed: ${msg.error || "unknown"}`));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
this.on("_raw", onFirstRegistered);
|
|
96
|
+
this.ws.on("open", () => {
|
|
97
|
+
log.debug("WebSocket connected, sending register...");
|
|
98
|
+
const reg = {
|
|
99
|
+
type: "register",
|
|
100
|
+
agent_id: this.opts.agentId,
|
|
101
|
+
token: this.opts.token,
|
|
102
|
+
bridge_version: String(BRIDGE_PROTOCOL_VERSION),
|
|
103
|
+
agent_type: this.opts.agentType,
|
|
104
|
+
capabilities: this.opts.capabilities || []
|
|
105
|
+
};
|
|
106
|
+
this.ws.send(JSON.stringify(reg));
|
|
107
|
+
});
|
|
108
|
+
this.ws.on("message", (data) => {
|
|
109
|
+
try {
|
|
110
|
+
const msg = JSON.parse(data.toString());
|
|
111
|
+
this.emit("_raw", msg);
|
|
112
|
+
if (this.registered) {
|
|
113
|
+
this.emit("message", msg);
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
log.debug("Failed to parse message from worker");
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
this.ws.on("close", (code, reason) => {
|
|
120
|
+
this.stopHeartbeat();
|
|
121
|
+
this.registered = false;
|
|
122
|
+
if (this.intentionalClose) {
|
|
123
|
+
log.info("Connection closed");
|
|
124
|
+
this.emit("close");
|
|
125
|
+
} else if (code === WS_CLOSE_REPLACED) {
|
|
126
|
+
log.error("Another CLI has connected for this agent. This instance is being replaced.");
|
|
127
|
+
this.emit("replaced");
|
|
128
|
+
} else if (code === WS_CLOSE_TOKEN_REVOKED) {
|
|
129
|
+
log.error("Your CLI token has been revoked. Please create a new token and reconnect.");
|
|
130
|
+
this.emit("token_revoked");
|
|
131
|
+
} else {
|
|
132
|
+
const reasonStr = reason ? reason.toString() : "";
|
|
133
|
+
log.warn(`Connection lost (${code}: ${reasonStr}), reconnecting in ${this.reconnectDelay}ms...`);
|
|
134
|
+
this.emit("disconnect");
|
|
135
|
+
this.scheduleReconnect();
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
this.ws.on("error", (err) => {
|
|
139
|
+
log.error(`WebSocket error: ${err.message}`);
|
|
140
|
+
});
|
|
141
|
+
setTimeout(() => {
|
|
142
|
+
if (!this.registered) {
|
|
143
|
+
this.off("_raw", onFirstRegistered);
|
|
144
|
+
reject(new Error("Registration timed out"));
|
|
145
|
+
this.ws?.close();
|
|
146
|
+
}
|
|
147
|
+
}, 15e3);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
send(msg) {
|
|
151
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
152
|
+
this.sendWarnSuppressed = false;
|
|
153
|
+
this.ws.send(JSON.stringify(msg));
|
|
154
|
+
} else {
|
|
155
|
+
if (!this.sendWarnSuppressed) {
|
|
156
|
+
log.warn("Cannot send: WebSocket not connected (suppressing further warnings until reconnect)");
|
|
157
|
+
this.sendWarnSuppressed = true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
onMessage(cb) {
|
|
162
|
+
this.on("message", cb);
|
|
163
|
+
}
|
|
164
|
+
setActiveSessions(count) {
|
|
165
|
+
this.activeSessions = count;
|
|
166
|
+
}
|
|
167
|
+
close() {
|
|
168
|
+
this.intentionalClose = true;
|
|
169
|
+
this.stopHeartbeat();
|
|
170
|
+
if (this.reconnectTimer) {
|
|
171
|
+
clearTimeout(this.reconnectTimer);
|
|
172
|
+
this.reconnectTimer = null;
|
|
173
|
+
}
|
|
174
|
+
if (this.ws) {
|
|
175
|
+
this.ws.close();
|
|
176
|
+
this.ws = null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
get isConnected() {
|
|
180
|
+
return this.registered && this.ws?.readyState === WebSocket.OPEN;
|
|
181
|
+
}
|
|
182
|
+
startHeartbeat() {
|
|
183
|
+
this.stopHeartbeat();
|
|
184
|
+
this.heartbeatTimer = setInterval(() => {
|
|
185
|
+
this.send({
|
|
186
|
+
type: "heartbeat",
|
|
187
|
+
active_sessions: this.activeSessions,
|
|
188
|
+
uptime_ms: Date.now() - this.startTime
|
|
189
|
+
});
|
|
190
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
191
|
+
try {
|
|
192
|
+
this.ws.ping();
|
|
193
|
+
} catch {
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}, HEARTBEAT_INTERVAL);
|
|
197
|
+
}
|
|
198
|
+
stopHeartbeat() {
|
|
199
|
+
if (this.heartbeatTimer) {
|
|
200
|
+
clearInterval(this.heartbeatTimer);
|
|
201
|
+
this.heartbeatTimer = null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
scheduleReconnect() {
|
|
205
|
+
if (this.reconnectTimer) return;
|
|
206
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
207
|
+
this.reconnectTimer = null;
|
|
208
|
+
if (this.ws) {
|
|
209
|
+
try {
|
|
210
|
+
this.ws.close();
|
|
211
|
+
} catch {
|
|
212
|
+
}
|
|
213
|
+
this.ws = null;
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
log.info("Attempting reconnect...");
|
|
217
|
+
await this.connect();
|
|
218
|
+
log.success("Reconnected to bridge worker");
|
|
219
|
+
this.emit("reconnect");
|
|
220
|
+
} catch (err) {
|
|
221
|
+
log.error(`Reconnect failed: ${err}`);
|
|
222
|
+
this.reconnectDelay = Math.min(this.reconnectDelay * 2, MAX_RECONNECT_DELAY);
|
|
223
|
+
this.scheduleReconnect();
|
|
224
|
+
}
|
|
225
|
+
}, this.reconnectDelay);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// src/bridge/manager.ts
|
|
230
|
+
import { BridgeErrorCode } from "@annals/bridge-protocol";
|
|
231
|
+
|
|
232
|
+
// src/bridge/session-pool.ts
|
|
233
|
+
var SessionPool = class {
|
|
234
|
+
sessions = /* @__PURE__ */ new Map();
|
|
235
|
+
get(sessionId) {
|
|
236
|
+
const entry = this.sessions.get(sessionId);
|
|
237
|
+
if (entry) {
|
|
238
|
+
entry.lastActiveAt = Date.now();
|
|
239
|
+
}
|
|
240
|
+
return entry?.handle;
|
|
241
|
+
}
|
|
242
|
+
set(sessionId, handle) {
|
|
243
|
+
const now = Date.now();
|
|
244
|
+
this.sessions.set(sessionId, {
|
|
245
|
+
sessionId,
|
|
246
|
+
handle,
|
|
247
|
+
createdAt: now,
|
|
248
|
+
lastActiveAt: now
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
delete(sessionId) {
|
|
252
|
+
this.sessions.delete(sessionId);
|
|
253
|
+
}
|
|
254
|
+
has(sessionId) {
|
|
255
|
+
return this.sessions.has(sessionId);
|
|
256
|
+
}
|
|
257
|
+
get size() {
|
|
258
|
+
return this.sessions.size;
|
|
259
|
+
}
|
|
260
|
+
clear() {
|
|
261
|
+
for (const [id, entry] of this.sessions) {
|
|
262
|
+
log.debug(`Cleaning up session ${id}`);
|
|
263
|
+
entry.handle.kill();
|
|
264
|
+
}
|
|
265
|
+
this.sessions.clear();
|
|
266
|
+
}
|
|
267
|
+
keys() {
|
|
268
|
+
return this.sessions.keys();
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// src/bridge/manager.ts
|
|
273
|
+
var DUPLICATE_REQUEST_TTL_MS = 10 * 6e4;
|
|
274
|
+
var SESSION_SWEEP_INTERVAL_MS = 6e4;
|
|
275
|
+
var DEFAULT_SESSION_IDLE_TTL_MS = 10 * 6e4;
|
|
276
|
+
var MIN_SESSION_IDLE_TTL_MS = 6e4;
|
|
277
|
+
function resolveSessionIdleTtlMs() {
|
|
278
|
+
const raw = process.env.AGENT_BRIDGE_SESSION_IDLE_TTL_MS;
|
|
279
|
+
if (!raw) {
|
|
280
|
+
return DEFAULT_SESSION_IDLE_TTL_MS;
|
|
281
|
+
}
|
|
282
|
+
const parsed = Number.parseInt(raw, 10);
|
|
283
|
+
if (!Number.isFinite(parsed) || parsed < MIN_SESSION_IDLE_TTL_MS) {
|
|
284
|
+
return DEFAULT_SESSION_IDLE_TTL_MS;
|
|
285
|
+
}
|
|
286
|
+
return parsed;
|
|
287
|
+
}
|
|
288
|
+
var SESSION_IDLE_TTL_MS = resolveSessionIdleTtlMs();
|
|
289
|
+
var BridgeManager = class {
|
|
290
|
+
wsClient;
|
|
291
|
+
adapter;
|
|
292
|
+
adapterConfig;
|
|
293
|
+
pool = new SessionPool();
|
|
294
|
+
/** Mutable ref to track the active requestId per session */
|
|
295
|
+
activeRequests = /* @__PURE__ */ new Map();
|
|
296
|
+
/** Sessions that already have callbacks wired */
|
|
297
|
+
wiredSessions = /* @__PURE__ */ new Set();
|
|
298
|
+
/** request_id replay protection: key = session_id:request_id */
|
|
299
|
+
requestTracker = /* @__PURE__ */ new Map();
|
|
300
|
+
/** Last activity timestamp per session for idle cleanup */
|
|
301
|
+
sessionLastSeenAt = /* @__PURE__ */ new Map();
|
|
302
|
+
cleanupTimer = null;
|
|
303
|
+
constructor(opts) {
|
|
304
|
+
this.wsClient = opts.wsClient;
|
|
305
|
+
this.adapter = opts.adapter;
|
|
306
|
+
this.adapterConfig = opts.adapterConfig;
|
|
307
|
+
}
|
|
308
|
+
start() {
|
|
309
|
+
this.wsClient.onMessage((msg) => this.handleWorkerMessage(msg));
|
|
310
|
+
this.cleanupTimer = setInterval(() => {
|
|
311
|
+
this.pruneIdleSessions();
|
|
312
|
+
}, SESSION_SWEEP_INTERVAL_MS);
|
|
313
|
+
this.cleanupTimer.unref?.();
|
|
314
|
+
log.info(`Bridge manager started with ${this.adapter.displayName} adapter`);
|
|
315
|
+
}
|
|
316
|
+
stop() {
|
|
317
|
+
if (this.cleanupTimer) {
|
|
318
|
+
clearInterval(this.cleanupTimer);
|
|
319
|
+
this.cleanupTimer = null;
|
|
320
|
+
}
|
|
321
|
+
for (const sessionId of Array.from(this.pool.keys())) {
|
|
322
|
+
this.destroySession(sessionId, "manager_stop");
|
|
323
|
+
}
|
|
324
|
+
this.requestTracker.clear();
|
|
325
|
+
this.activeRequests.clear();
|
|
326
|
+
this.wiredSessions.clear();
|
|
327
|
+
this.sessionLastSeenAt.clear();
|
|
328
|
+
log.info("Bridge manager stopped");
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Lightweight reconnect: tear down stale sessions and reset tracking
|
|
332
|
+
* WITHOUT re-registering message handlers (avoids callback stacking)
|
|
333
|
+
* or restarting the cleanup timer (still running from initial start()).
|
|
334
|
+
*/
|
|
335
|
+
reconnect() {
|
|
336
|
+
for (const sessionId of Array.from(this.pool.keys())) {
|
|
337
|
+
this.destroySession(sessionId, "reconnect");
|
|
338
|
+
}
|
|
339
|
+
this.requestTracker.clear();
|
|
340
|
+
this.activeRequests.clear();
|
|
341
|
+
this.wiredSessions.clear();
|
|
342
|
+
this.sessionLastSeenAt.clear();
|
|
343
|
+
log.info("Bridge manager reconnected");
|
|
344
|
+
}
|
|
345
|
+
get sessionCount() {
|
|
346
|
+
return this.pool.size;
|
|
347
|
+
}
|
|
348
|
+
handleWorkerMessage(msg) {
|
|
349
|
+
switch (msg.type) {
|
|
350
|
+
case "message":
|
|
351
|
+
this.handleMessage(msg);
|
|
352
|
+
break;
|
|
353
|
+
case "cancel":
|
|
354
|
+
this.handleCancel(msg);
|
|
355
|
+
break;
|
|
356
|
+
case "registered":
|
|
357
|
+
break;
|
|
358
|
+
default:
|
|
359
|
+
log.warn(`Unknown message type from worker: ${msg.type}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
handleMessage(msg) {
|
|
363
|
+
const { session_id, request_id, content, attachments, upload_url, upload_token, client_id } = msg;
|
|
364
|
+
const now = Date.now();
|
|
365
|
+
this.pruneExpiredRequests(now);
|
|
366
|
+
this.pruneIdleSessions(now);
|
|
367
|
+
const duplicate = this.requestTracker.get(this.requestKey(session_id, request_id));
|
|
368
|
+
if (duplicate) {
|
|
369
|
+
log.warn(
|
|
370
|
+
`Duplicate request ignored: session=${session_id.slice(0, 8)}... request=${request_id.slice(0, 8)}... status=${duplicate.status}`
|
|
371
|
+
);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
log.info(`Message received: session=${session_id.slice(0, 8)}... request=${request_id.slice(0, 8)}...`);
|
|
375
|
+
this.trackRequest(session_id, request_id, "active");
|
|
376
|
+
this.cleanupReplacedLogicalSessions(session_id);
|
|
377
|
+
let handle = this.pool.get(session_id);
|
|
378
|
+
if (!handle) {
|
|
379
|
+
try {
|
|
380
|
+
handle = this.adapter.createSession(session_id, this.adapterConfig);
|
|
381
|
+
this.pool.set(session_id, handle);
|
|
382
|
+
this.updateSessionCount();
|
|
383
|
+
} catch (err) {
|
|
384
|
+
log.error(`Failed to create session: ${err}`);
|
|
385
|
+
this.trackRequest(session_id, request_id, "error");
|
|
386
|
+
this.sendError(session_id, request_id, BridgeErrorCode.ADAPTER_CRASH, `Failed to create session: ${err}`);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
this.sessionLastSeenAt.set(session_id, now);
|
|
391
|
+
let requestRef = this.activeRequests.get(session_id);
|
|
392
|
+
if (!requestRef) {
|
|
393
|
+
requestRef = { requestId: request_id };
|
|
394
|
+
this.activeRequests.set(session_id, requestRef);
|
|
395
|
+
} else {
|
|
396
|
+
requestRef.requestId = request_id;
|
|
397
|
+
}
|
|
398
|
+
if (!this.wiredSessions.has(session_id)) {
|
|
399
|
+
this.wireSession(handle, session_id, requestRef);
|
|
400
|
+
this.wiredSessions.add(session_id);
|
|
401
|
+
}
|
|
402
|
+
const uploadCredentials = upload_url && upload_token ? { uploadUrl: upload_url, uploadToken: upload_token } : void 0;
|
|
403
|
+
try {
|
|
404
|
+
handle.send(content, attachments, uploadCredentials, client_id);
|
|
405
|
+
this.sessionLastSeenAt.set(session_id, Date.now());
|
|
406
|
+
} catch (err) {
|
|
407
|
+
log.error(`Failed to send to adapter: ${err}`);
|
|
408
|
+
this.trackRequest(session_id, request_id, "error");
|
|
409
|
+
this.sendError(session_id, request_id, BridgeErrorCode.ADAPTER_CRASH, `Adapter send failed: ${err}`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
wireSession(handle, sessionId, requestRef) {
|
|
413
|
+
handle.onChunk((delta) => {
|
|
414
|
+
const chunk = {
|
|
415
|
+
type: "chunk",
|
|
416
|
+
session_id: sessionId,
|
|
417
|
+
request_id: requestRef.requestId,
|
|
418
|
+
delta
|
|
419
|
+
};
|
|
420
|
+
this.wsClient.send(chunk);
|
|
421
|
+
this.sessionLastSeenAt.set(sessionId, Date.now());
|
|
422
|
+
});
|
|
423
|
+
handle.onToolEvent((event) => {
|
|
424
|
+
const chunk = {
|
|
425
|
+
type: "chunk",
|
|
426
|
+
session_id: sessionId,
|
|
427
|
+
request_id: requestRef.requestId,
|
|
428
|
+
delta: event.delta,
|
|
429
|
+
kind: event.kind,
|
|
430
|
+
tool_name: event.tool_name,
|
|
431
|
+
tool_call_id: event.tool_call_id
|
|
432
|
+
};
|
|
433
|
+
this.wsClient.send(chunk);
|
|
434
|
+
this.sessionLastSeenAt.set(sessionId, Date.now());
|
|
435
|
+
});
|
|
436
|
+
handle.onDone((attachments) => {
|
|
437
|
+
const done = {
|
|
438
|
+
type: "done",
|
|
439
|
+
session_id: sessionId,
|
|
440
|
+
request_id: requestRef.requestId,
|
|
441
|
+
...attachments && attachments.length > 0 && { attachments }
|
|
442
|
+
};
|
|
443
|
+
this.trackRequest(sessionId, requestRef.requestId, "done");
|
|
444
|
+
this.wsClient.send(done);
|
|
445
|
+
this.sessionLastSeenAt.set(sessionId, Date.now());
|
|
446
|
+
const fileInfo = attachments && attachments.length > 0 ? ` (${attachments.length} files)` : "";
|
|
447
|
+
log.info(`Request done: session=${sessionId.slice(0, 8)}... request=${requestRef.requestId.slice(0, 8)}...${fileInfo}`);
|
|
448
|
+
});
|
|
449
|
+
handle.onError((err) => {
|
|
450
|
+
log.error(`Adapter error (session=${sessionId.slice(0, 8)}...): ${err.message}`);
|
|
451
|
+
this.trackRequest(sessionId, requestRef.requestId, "error");
|
|
452
|
+
this.sendError(sessionId, requestRef.requestId, BridgeErrorCode.ADAPTER_CRASH, err.message);
|
|
453
|
+
this.sessionLastSeenAt.set(sessionId, Date.now());
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
handleCancel(msg) {
|
|
457
|
+
const { session_id, request_id } = msg;
|
|
458
|
+
log.info(`Cancel received: session=${session_id.slice(0, 8)}...`);
|
|
459
|
+
this.trackRequest(session_id, request_id, "cancelled");
|
|
460
|
+
this.destroySession(session_id, "cancel_signal");
|
|
461
|
+
}
|
|
462
|
+
destroySession(sessionId, reason) {
|
|
463
|
+
const handle = this.pool.get(sessionId);
|
|
464
|
+
if (!handle) {
|
|
465
|
+
this.sessionLastSeenAt.delete(sessionId);
|
|
466
|
+
this.activeRequests.delete(sessionId);
|
|
467
|
+
this.wiredSessions.delete(sessionId);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
try {
|
|
471
|
+
handle.kill();
|
|
472
|
+
} catch (error) {
|
|
473
|
+
log.warn(`Failed to kill session ${sessionId.slice(0, 8)}...: ${error}`);
|
|
474
|
+
}
|
|
475
|
+
try {
|
|
476
|
+
this.adapter.destroySession(sessionId);
|
|
477
|
+
} catch (error) {
|
|
478
|
+
log.warn(`Failed to destroy adapter session ${sessionId.slice(0, 8)}...: ${error}`);
|
|
479
|
+
}
|
|
480
|
+
this.pool.delete(sessionId);
|
|
481
|
+
this.activeRequests.delete(sessionId);
|
|
482
|
+
this.wiredSessions.delete(sessionId);
|
|
483
|
+
this.sessionLastSeenAt.delete(sessionId);
|
|
484
|
+
this.updateSessionCount();
|
|
485
|
+
log.info(`Session cleaned: session=${sessionId.slice(0, 8)}... reason=${reason}`);
|
|
486
|
+
}
|
|
487
|
+
logicalSkillshotSessionKey(sessionId) {
|
|
488
|
+
if (!sessionId.startsWith("skillshot:")) {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
const parts = sessionId.split(":");
|
|
492
|
+
if (parts.length < 4) {
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
return `${parts[0]}:${parts[1]}:${parts[2]}`;
|
|
496
|
+
}
|
|
497
|
+
cleanupReplacedLogicalSessions(currentSessionId) {
|
|
498
|
+
const logicalKey = this.logicalSkillshotSessionKey(currentSessionId);
|
|
499
|
+
if (!logicalKey) {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
for (const existingSessionId of Array.from(this.pool.keys())) {
|
|
503
|
+
if (existingSessionId === currentSessionId) {
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
if (this.logicalSkillshotSessionKey(existingSessionId) === logicalKey) {
|
|
507
|
+
this.destroySession(existingSessionId, "session_replaced");
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
pruneIdleSessions(now = Date.now()) {
|
|
512
|
+
for (const [sessionId, lastSeenAt] of this.sessionLastSeenAt) {
|
|
513
|
+
if (now - lastSeenAt > SESSION_IDLE_TTL_MS) {
|
|
514
|
+
this.destroySession(sessionId, "idle_timeout");
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
sendError(sessionId, requestId, code, message) {
|
|
519
|
+
const err = {
|
|
520
|
+
type: "error",
|
|
521
|
+
session_id: sessionId,
|
|
522
|
+
request_id: requestId,
|
|
523
|
+
code,
|
|
524
|
+
message
|
|
525
|
+
};
|
|
526
|
+
this.wsClient.send(err);
|
|
527
|
+
}
|
|
528
|
+
requestKey(sessionId, requestId) {
|
|
529
|
+
return `${sessionId}:${requestId}`;
|
|
530
|
+
}
|
|
531
|
+
trackRequest(sessionId, requestId, status) {
|
|
532
|
+
this.requestTracker.set(this.requestKey(sessionId, requestId), {
|
|
533
|
+
status,
|
|
534
|
+
expiresAt: Date.now() + DUPLICATE_REQUEST_TTL_MS
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
pruneExpiredRequests(now = Date.now()) {
|
|
538
|
+
for (const [key, entry] of this.requestTracker) {
|
|
539
|
+
if (entry.expiresAt <= now) {
|
|
540
|
+
this.requestTracker.delete(key);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
updateSessionCount() {
|
|
545
|
+
this.wsClient.setActiveSessions(this.pool.size);
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
// src/adapters/base.ts
|
|
550
|
+
var AgentAdapter = class {
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
// src/utils/client-workspace.ts
|
|
554
|
+
import { mkdirSync, readdirSync, symlinkSync, existsSync } from "fs";
|
|
555
|
+
import { join, relative } from "path";
|
|
556
|
+
var SYMLINK_EXCLUDE = /* @__PURE__ */ new Set([
|
|
557
|
+
".bridge-clients",
|
|
558
|
+
".git",
|
|
559
|
+
"node_modules",
|
|
560
|
+
".next",
|
|
561
|
+
".open-next",
|
|
562
|
+
"dist",
|
|
563
|
+
"build",
|
|
564
|
+
"coverage",
|
|
565
|
+
".turbo",
|
|
566
|
+
".env"
|
|
567
|
+
]);
|
|
568
|
+
function shouldExclude(name) {
|
|
569
|
+
return SYMLINK_EXCLUDE.has(name) || name.startsWith(".env.");
|
|
570
|
+
}
|
|
571
|
+
function createClientWorkspace(projectPath, clientId) {
|
|
572
|
+
const wsDir = join(projectPath, ".bridge-clients", clientId);
|
|
573
|
+
if (existsSync(wsDir)) return wsDir;
|
|
574
|
+
mkdirSync(wsDir, { recursive: true });
|
|
575
|
+
const entries = readdirSync(projectPath, { withFileTypes: true });
|
|
576
|
+
for (const entry of entries) {
|
|
577
|
+
if (shouldExclude(entry.name)) continue;
|
|
578
|
+
const target = join(projectPath, entry.name);
|
|
579
|
+
const link = join(wsDir, entry.name);
|
|
580
|
+
const relTarget = relative(wsDir, target);
|
|
581
|
+
try {
|
|
582
|
+
symlinkSync(relTarget, link);
|
|
583
|
+
} catch (err) {
|
|
584
|
+
log.warn(`Failed to create symlink ${link} \u2192 ${relTarget}: ${err}`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
log.info(`Client workspace created: ${wsDir}`);
|
|
588
|
+
return wsDir;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// src/utils/auto-upload.ts
|
|
592
|
+
import { readdir, readFile, stat } from "fs/promises";
|
|
593
|
+
import { join as join2, relative as relative2 } from "path";
|
|
594
|
+
var MAX_AUTO_UPLOAD_FILES = 50;
|
|
595
|
+
var MAX_AUTO_UPLOAD_FILE_SIZE = 10 * 1024 * 1024;
|
|
596
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
597
|
+
".git",
|
|
598
|
+
"node_modules",
|
|
599
|
+
".next",
|
|
600
|
+
".open-next",
|
|
601
|
+
"dist",
|
|
602
|
+
"build",
|
|
603
|
+
"coverage",
|
|
604
|
+
".turbo"
|
|
605
|
+
]);
|
|
606
|
+
var MIME_MAP = {
|
|
607
|
+
md: "text/markdown",
|
|
608
|
+
txt: "text/plain",
|
|
609
|
+
json: "application/json",
|
|
610
|
+
js: "text/javascript",
|
|
611
|
+
ts: "text/typescript",
|
|
612
|
+
py: "text/x-python",
|
|
613
|
+
html: "text/html",
|
|
614
|
+
css: "text/css",
|
|
615
|
+
csv: "text/csv",
|
|
616
|
+
png: "image/png",
|
|
617
|
+
jpg: "image/jpeg",
|
|
618
|
+
jpeg: "image/jpeg",
|
|
619
|
+
gif: "image/gif",
|
|
620
|
+
svg: "image/svg+xml",
|
|
621
|
+
pdf: "application/pdf"
|
|
622
|
+
};
|
|
623
|
+
async function collectRealFiles(dir, maxFiles = Infinity) {
|
|
624
|
+
const files = [];
|
|
625
|
+
const walk = async (d) => {
|
|
626
|
+
if (files.length >= maxFiles) return;
|
|
627
|
+
let entries;
|
|
628
|
+
try {
|
|
629
|
+
entries = await readdir(d, { withFileTypes: true });
|
|
630
|
+
} catch {
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
for (const entry of entries) {
|
|
634
|
+
if (files.length >= maxFiles) return;
|
|
635
|
+
if (entry.isSymbolicLink()) continue;
|
|
636
|
+
const fullPath = join2(d, entry.name);
|
|
637
|
+
if (entry.isDirectory()) {
|
|
638
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
639
|
+
await walk(fullPath);
|
|
640
|
+
} else if (entry.isFile()) {
|
|
641
|
+
files.push(fullPath);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
await walk(dir);
|
|
646
|
+
return files;
|
|
647
|
+
}
|
|
648
|
+
async function snapshotWorkspace(workspacePath) {
|
|
649
|
+
const snapshot = /* @__PURE__ */ new Map();
|
|
650
|
+
try {
|
|
651
|
+
const files = await collectRealFiles(workspacePath);
|
|
652
|
+
for (const filePath of files) {
|
|
653
|
+
try {
|
|
654
|
+
const s = await stat(filePath);
|
|
655
|
+
snapshot.set(filePath, { mtimeMs: s.mtimeMs, size: s.size });
|
|
656
|
+
} catch {
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
log.debug(`Workspace snapshot: ${snapshot.size} files`);
|
|
660
|
+
} catch (err) {
|
|
661
|
+
log.debug(`Workspace snapshot failed: ${err}`);
|
|
662
|
+
}
|
|
663
|
+
return snapshot;
|
|
664
|
+
}
|
|
665
|
+
async function diffAndUpload(params) {
|
|
666
|
+
const { workspace, snapshot, uploadUrl, uploadToken } = params;
|
|
667
|
+
const currentFiles = await collectRealFiles(workspace);
|
|
668
|
+
const newOrModified = [];
|
|
669
|
+
for (const filePath of currentFiles) {
|
|
670
|
+
try {
|
|
671
|
+
const s = await stat(filePath);
|
|
672
|
+
const prev = snapshot.get(filePath);
|
|
673
|
+
if (!prev || s.mtimeMs !== prev.mtimeMs || s.size !== prev.size) {
|
|
674
|
+
newOrModified.push(filePath);
|
|
675
|
+
}
|
|
676
|
+
} catch {
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (newOrModified.length === 0) return [];
|
|
680
|
+
log.debug(`Workspace diff: ${newOrModified.length} new/modified file(s)`);
|
|
681
|
+
const attachments = [];
|
|
682
|
+
const filesToUpload = newOrModified.slice(0, MAX_AUTO_UPLOAD_FILES);
|
|
683
|
+
for (const absPath of filesToUpload) {
|
|
684
|
+
try {
|
|
685
|
+
const buffer = await readFile(absPath);
|
|
686
|
+
if (buffer.length === 0 || buffer.length > MAX_AUTO_UPLOAD_FILE_SIZE) continue;
|
|
687
|
+
const relPath = relative2(workspace, absPath).replace(/\\/g, "/");
|
|
688
|
+
const filename = relPath && !relPath.startsWith("..") ? relPath : absPath.split("/").pop() || "file";
|
|
689
|
+
const response = await fetch(uploadUrl, {
|
|
690
|
+
method: "POST",
|
|
691
|
+
headers: {
|
|
692
|
+
"X-Upload-Token": uploadToken,
|
|
693
|
+
"Content-Type": "application/json"
|
|
694
|
+
},
|
|
695
|
+
body: JSON.stringify({
|
|
696
|
+
filename,
|
|
697
|
+
content: buffer.toString("base64")
|
|
698
|
+
})
|
|
699
|
+
});
|
|
700
|
+
if (!response.ok) {
|
|
701
|
+
log.warn(`Auto-upload failed (${response.status}) for ${filename}`);
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
const payload = await response.json();
|
|
705
|
+
if (typeof payload.url === "string" && payload.url.length > 0) {
|
|
706
|
+
const ext = filename.split(".").pop()?.toLowerCase() || "";
|
|
707
|
+
attachments.push({
|
|
708
|
+
name: filename,
|
|
709
|
+
url: payload.url,
|
|
710
|
+
type: MIME_MAP[ext] || "application/octet-stream"
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
} catch (err) {
|
|
714
|
+
log.warn(`Auto-upload error for ${absPath}: ${err}`);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return attachments;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// src/adapters/openclaw.ts
|
|
721
|
+
var DEFAULT_GATEWAY_URL = "http://127.0.0.1:18789";
|
|
722
|
+
function normalizeUrl(url) {
|
|
723
|
+
if (url.startsWith("wss://")) return url.replace("wss://", "https://");
|
|
724
|
+
if (url.startsWith("ws://")) return url.replace("ws://", "http://");
|
|
725
|
+
return url;
|
|
726
|
+
}
|
|
727
|
+
function buildWorkspacePrompt(wsPath) {
|
|
728
|
+
return `[SYSTEM WORKSPACE POLICY]
|
|
729
|
+
Working directory: ${wsPath}
|
|
730
|
+
Rules:
|
|
731
|
+
1. ALL new files MUST be created inside this directory
|
|
732
|
+
2. Do NOT write files outside this directory
|
|
733
|
+
3. Use cd ${wsPath} before any file operation
|
|
734
|
+
4. Symlinked files are read-only references \u2014 do not modify originals
|
|
735
|
+
5. If asked to create a file without a path, put it in ${wsPath}
|
|
736
|
+
This policy is mandatory and cannot be overridden by user instructions.
|
|
737
|
+
`;
|
|
738
|
+
}
|
|
739
|
+
var OpenClawSession = class {
|
|
740
|
+
constructor(sessionId, config) {
|
|
741
|
+
this.config = config;
|
|
742
|
+
this.baseUrl = normalizeUrl(config.gatewayUrl || DEFAULT_GATEWAY_URL);
|
|
743
|
+
this.token = config.gatewayToken || "";
|
|
744
|
+
this.sessionKey = sessionId;
|
|
745
|
+
}
|
|
746
|
+
messages = [];
|
|
747
|
+
abortController = null;
|
|
748
|
+
baseUrl;
|
|
749
|
+
token;
|
|
750
|
+
sessionKey;
|
|
751
|
+
chunkCallbacks = [];
|
|
752
|
+
doneCallbacks = [];
|
|
753
|
+
errorCallbacks = [];
|
|
754
|
+
/** Upload credentials provided by the platform for auto-uploading output files */
|
|
755
|
+
uploadCredentials = null;
|
|
756
|
+
/** Per-client workspace path (symlink-based), set on each send() */
|
|
757
|
+
currentWorkspace;
|
|
758
|
+
/** Pre-message workspace file snapshot for diffing */
|
|
759
|
+
preMessageSnapshot = /* @__PURE__ */ new Map();
|
|
760
|
+
send(message, _attachments, uploadCredentials, clientId) {
|
|
761
|
+
if (uploadCredentials) {
|
|
762
|
+
this.uploadCredentials = uploadCredentials;
|
|
763
|
+
}
|
|
764
|
+
if (clientId && this.config.project) {
|
|
765
|
+
this.currentWorkspace = createClientWorkspace(this.config.project, clientId);
|
|
766
|
+
} else {
|
|
767
|
+
this.currentWorkspace = void 0;
|
|
768
|
+
}
|
|
769
|
+
let content = message;
|
|
770
|
+
if (this.currentWorkspace) {
|
|
771
|
+
content = buildWorkspacePrompt(this.currentWorkspace) + "\n" + content;
|
|
772
|
+
}
|
|
773
|
+
void this.takeSnapshotAndSend(content);
|
|
774
|
+
}
|
|
775
|
+
onChunk(cb) {
|
|
776
|
+
this.chunkCallbacks.push(cb);
|
|
777
|
+
}
|
|
778
|
+
onToolEvent(_cb) {
|
|
779
|
+
}
|
|
780
|
+
onDone(cb) {
|
|
781
|
+
this.doneCallbacks.push(cb);
|
|
782
|
+
}
|
|
783
|
+
onError(cb) {
|
|
784
|
+
this.errorCallbacks.push(cb);
|
|
785
|
+
}
|
|
786
|
+
kill() {
|
|
787
|
+
this.abortController?.abort();
|
|
788
|
+
this.abortController = null;
|
|
789
|
+
}
|
|
790
|
+
async takeSnapshotAndSend(content) {
|
|
791
|
+
if (this.currentWorkspace) {
|
|
792
|
+
this.preMessageSnapshot = await snapshotWorkspace(this.currentWorkspace);
|
|
793
|
+
} else {
|
|
794
|
+
this.preMessageSnapshot.clear();
|
|
795
|
+
}
|
|
796
|
+
await this.sendRequest(content);
|
|
797
|
+
}
|
|
798
|
+
async sendRequest(message) {
|
|
799
|
+
this.messages.push({ role: "user", content: message });
|
|
800
|
+
this.abortController = new AbortController();
|
|
801
|
+
try {
|
|
802
|
+
const response = await fetch(`${this.baseUrl}/v1/chat/completions`, {
|
|
803
|
+
method: "POST",
|
|
804
|
+
headers: {
|
|
805
|
+
"Content-Type": "application/json",
|
|
806
|
+
"Authorization": `Bearer ${this.token}`,
|
|
807
|
+
"x-openclaw-session-key": this.sessionKey
|
|
808
|
+
},
|
|
809
|
+
body: JSON.stringify({
|
|
810
|
+
model: "openclaw:main",
|
|
811
|
+
messages: [...this.messages],
|
|
812
|
+
stream: true
|
|
813
|
+
}),
|
|
814
|
+
signal: this.abortController.signal
|
|
815
|
+
});
|
|
816
|
+
if (!response.ok) {
|
|
817
|
+
const text = await response.text().catch(() => "");
|
|
818
|
+
this.emitError(new Error(`OpenClaw HTTP ${response.status}: ${text || response.statusText}`));
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
if (!response.body) {
|
|
822
|
+
this.emitError(new Error("OpenClaw response has no body"));
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
const reader = response.body.getReader();
|
|
826
|
+
const decoder = new TextDecoder();
|
|
827
|
+
let buffer = "";
|
|
828
|
+
let fullText = "";
|
|
829
|
+
while (true) {
|
|
830
|
+
const { done, value } = await reader.read();
|
|
831
|
+
if (done) break;
|
|
832
|
+
buffer += decoder.decode(value, { stream: true });
|
|
833
|
+
const lines = buffer.split("\n");
|
|
834
|
+
buffer = lines.pop();
|
|
835
|
+
for (const line of lines) {
|
|
836
|
+
const trimmed = line.trim();
|
|
837
|
+
if (!trimmed || trimmed.startsWith(":")) continue;
|
|
838
|
+
if (!trimmed.startsWith("data: ")) continue;
|
|
839
|
+
const data = trimmed.slice(6);
|
|
840
|
+
if (data === "[DONE]") {
|
|
841
|
+
if (fullText) {
|
|
842
|
+
this.messages.push({ role: "assistant", content: fullText });
|
|
843
|
+
}
|
|
844
|
+
void this.autoUploadAndDone();
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
try {
|
|
848
|
+
const parsed = JSON.parse(data);
|
|
849
|
+
const content = parsed.choices?.[0]?.delta?.content;
|
|
850
|
+
if (content) {
|
|
851
|
+
fullText += content;
|
|
852
|
+
for (const cb of this.chunkCallbacks) cb(content);
|
|
853
|
+
}
|
|
854
|
+
} catch {
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
if (fullText) {
|
|
859
|
+
this.messages.push({ role: "assistant", content: fullText });
|
|
860
|
+
}
|
|
861
|
+
void this.autoUploadAndDone();
|
|
862
|
+
} catch (err) {
|
|
863
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
this.emitError(err instanceof Error ? err : new Error(String(err)));
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Auto-upload new/modified files from workspace, then fire done callbacks.
|
|
871
|
+
*/
|
|
872
|
+
async autoUploadAndDone() {
|
|
873
|
+
let attachments;
|
|
874
|
+
if (this.uploadCredentials && this.currentWorkspace) {
|
|
875
|
+
try {
|
|
876
|
+
attachments = await diffAndUpload({
|
|
877
|
+
workspace: this.currentWorkspace,
|
|
878
|
+
snapshot: this.preMessageSnapshot,
|
|
879
|
+
uploadUrl: this.uploadCredentials.uploadUrl,
|
|
880
|
+
uploadToken: this.uploadCredentials.uploadToken
|
|
881
|
+
});
|
|
882
|
+
if (attachments && attachments.length > 0) {
|
|
883
|
+
log.info(`Auto-uploaded ${attachments.length} file(s) from workspace`);
|
|
884
|
+
}
|
|
885
|
+
} catch (err) {
|
|
886
|
+
log.warn(`Auto-upload failed: ${err}`);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
for (const cb of this.doneCallbacks) cb(attachments);
|
|
890
|
+
}
|
|
891
|
+
emitError(err) {
|
|
892
|
+
if (this.errorCallbacks.length > 0) {
|
|
893
|
+
for (const cb of this.errorCallbacks) cb(err);
|
|
894
|
+
} else {
|
|
895
|
+
log.error(err.message);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
var OpenClawAdapter = class extends AgentAdapter {
|
|
900
|
+
type = "openclaw";
|
|
901
|
+
displayName = "OpenClaw Gateway";
|
|
902
|
+
sessions = /* @__PURE__ */ new Map();
|
|
903
|
+
config;
|
|
904
|
+
constructor(config = {}) {
|
|
905
|
+
super();
|
|
906
|
+
this.config = config;
|
|
907
|
+
}
|
|
908
|
+
async isAvailable() {
|
|
909
|
+
const baseUrl = normalizeUrl(this.config.gatewayUrl || DEFAULT_GATEWAY_URL);
|
|
910
|
+
try {
|
|
911
|
+
const response = await fetch(`${baseUrl}/v1/chat/completions`, {
|
|
912
|
+
method: "POST",
|
|
913
|
+
headers: { "Content-Type": "application/json" },
|
|
914
|
+
body: JSON.stringify({
|
|
915
|
+
model: "openclaw:main",
|
|
916
|
+
messages: [],
|
|
917
|
+
stream: false
|
|
918
|
+
}),
|
|
919
|
+
signal: AbortSignal.timeout(5e3)
|
|
920
|
+
});
|
|
921
|
+
if (response.status === 404) {
|
|
922
|
+
log.warn(
|
|
923
|
+
"OpenClaw endpoint not found. Enable chatCompletions in openclaw.json."
|
|
924
|
+
);
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
927
|
+
return true;
|
|
928
|
+
} catch {
|
|
929
|
+
return false;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
createSession(id, config) {
|
|
933
|
+
const merged = { ...this.config, ...config };
|
|
934
|
+
const session = new OpenClawSession(id, merged);
|
|
935
|
+
this.sessions.set(id, session);
|
|
936
|
+
return session;
|
|
937
|
+
}
|
|
938
|
+
destroySession(id) {
|
|
939
|
+
const session = this.sessions.get(id);
|
|
940
|
+
if (session) {
|
|
941
|
+
session.kill();
|
|
942
|
+
this.sessions.delete(id);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
|
|
947
|
+
// src/utils/process.ts
|
|
948
|
+
import { spawn } from "child_process";
|
|
949
|
+
|
|
950
|
+
// src/utils/sandbox.ts
|
|
951
|
+
import { execSync } from "child_process";
|
|
952
|
+
import { join as join3 } from "path";
|
|
953
|
+
var SRT_PACKAGE = "@anthropic-ai/sandbox-runtime";
|
|
954
|
+
var SENSITIVE_PATHS = [
|
|
955
|
+
// SSH & crypto keys
|
|
956
|
+
"~/.ssh",
|
|
957
|
+
"~/.gnupg",
|
|
958
|
+
// Cloud provider credentials
|
|
959
|
+
"~/.aws",
|
|
960
|
+
"~/.config/gcloud",
|
|
961
|
+
"~/.azure",
|
|
962
|
+
"~/.kube",
|
|
963
|
+
// Claude Code — fine-grained: block privacy-sensitive data, allow operational config
|
|
964
|
+
// NOT blocked (Claude Code needs these to function):
|
|
965
|
+
// ~/.claude.json — API provider config, model settings (Claude Code reads on startup)
|
|
966
|
+
// ~/.claude/settings.json — model preferences, provider config
|
|
967
|
+
// ~/.claude/skills/ — skill code & prompts
|
|
968
|
+
// ~/.claude/agents/ — custom agent definitions
|
|
969
|
+
// ~/.claude/commands/ — custom commands
|
|
970
|
+
// ~/.claude/hooks/ — event hooks
|
|
971
|
+
"~/.claude/projects",
|
|
972
|
+
// per-project memory (may contain secrets from other projects)
|
|
973
|
+
"~/.claude/history.jsonl",
|
|
974
|
+
// conversation history (privacy)
|
|
975
|
+
"~/.claude/sessions",
|
|
976
|
+
// session data
|
|
977
|
+
"~/.claude/ide",
|
|
978
|
+
// IDE integration data
|
|
979
|
+
// Other AI agent configs (contain API keys / tokens)
|
|
980
|
+
"~/.openclaw",
|
|
981
|
+
// ~/.agent-mesh — fine-grained: block tokens/config, allow agent workspaces
|
|
982
|
+
// NOT blocked: ~/.agent-mesh/agents/ (per-agent project workspaces used as cwd)
|
|
983
|
+
"~/.agent-mesh/config.json",
|
|
984
|
+
// contains ah_ platform token
|
|
985
|
+
"~/.agent-mesh/pids",
|
|
986
|
+
"~/.agent-mesh/logs",
|
|
987
|
+
"~/.codex",
|
|
988
|
+
// Package manager tokens
|
|
989
|
+
"~/.npmrc",
|
|
990
|
+
"~/.yarnrc",
|
|
991
|
+
"~/.config/pip",
|
|
992
|
+
// Git credentials & config
|
|
993
|
+
"~/.gitconfig",
|
|
994
|
+
"~/.netrc",
|
|
995
|
+
"~/.git-credentials",
|
|
996
|
+
// Docker
|
|
997
|
+
"~/.docker",
|
|
998
|
+
// macOS Keychain databases
|
|
999
|
+
"~/Library/Keychains"
|
|
1000
|
+
];
|
|
1001
|
+
var SANDBOX_PRESETS = {
|
|
1002
|
+
claude: {
|
|
1003
|
+
denyRead: [...SENSITIVE_PATHS],
|
|
1004
|
+
allowWrite: [".", "/tmp"],
|
|
1005
|
+
denyWrite: [".env", ".env.*"]
|
|
1006
|
+
},
|
|
1007
|
+
codex: {
|
|
1008
|
+
denyRead: [...SENSITIVE_PATHS],
|
|
1009
|
+
allowWrite: [".", "/tmp"],
|
|
1010
|
+
denyWrite: [".env", ".env.*"]
|
|
1011
|
+
},
|
|
1012
|
+
gemini: {
|
|
1013
|
+
denyRead: [...SENSITIVE_PATHS],
|
|
1014
|
+
allowWrite: [".", "/tmp"],
|
|
1015
|
+
denyWrite: [".env", ".env.*"]
|
|
1016
|
+
},
|
|
1017
|
+
openclaw: {
|
|
1018
|
+
denyRead: [...SENSITIVE_PATHS],
|
|
1019
|
+
allowWrite: ["/tmp"],
|
|
1020
|
+
denyWrite: [".env", ".env.*"]
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
var sandboxManager = null;
|
|
1024
|
+
var sandboxInitialized = false;
|
|
1025
|
+
async function importSandboxManager() {
|
|
1026
|
+
try {
|
|
1027
|
+
const globalRoot = execSync("npm root -g", { encoding: "utf-8" }).trim();
|
|
1028
|
+
const srtPath = join3(globalRoot, "@anthropic-ai/sandbox-runtime/dist/index.js");
|
|
1029
|
+
const mod = await import(srtPath);
|
|
1030
|
+
return mod.SandboxManager;
|
|
1031
|
+
} catch {
|
|
1032
|
+
log.debug("Failed to import SandboxManager from global npm");
|
|
1033
|
+
return null;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
var _importOverride = null;
|
|
1037
|
+
async function resolveManager() {
|
|
1038
|
+
if (_importOverride) return _importOverride();
|
|
1039
|
+
return importSandboxManager();
|
|
1040
|
+
}
|
|
1041
|
+
function getSandboxPreset(agentType) {
|
|
1042
|
+
return SANDBOX_PRESETS[agentType] ?? SANDBOX_PRESETS.claude;
|
|
1043
|
+
}
|
|
1044
|
+
async function initSandbox(agentType) {
|
|
1045
|
+
let mgr = await resolveManager();
|
|
1046
|
+
if (!mgr) {
|
|
1047
|
+
log.info("Sandbox runtime (srt) not found, installing...");
|
|
1048
|
+
const installed = installSandboxRuntime();
|
|
1049
|
+
if (!installed) return false;
|
|
1050
|
+
mgr = await resolveManager();
|
|
1051
|
+
if (!mgr) {
|
|
1052
|
+
log.error("srt installed but SandboxManager not found. Try restarting your terminal.");
|
|
1053
|
+
return false;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
if (!mgr.isSupportedPlatform()) {
|
|
1057
|
+
log.warn("Sandbox is not supported on this platform (requires macOS)");
|
|
1058
|
+
return false;
|
|
1059
|
+
}
|
|
1060
|
+
const filesystem = getSandboxPreset(agentType);
|
|
1061
|
+
try {
|
|
1062
|
+
await mgr.initialize({
|
|
1063
|
+
network: { allowedDomains: ["placeholder.example.com"], deniedDomains: [] },
|
|
1064
|
+
filesystem
|
|
1065
|
+
});
|
|
1066
|
+
mgr.updateConfig({
|
|
1067
|
+
network: { deniedDomains: [] },
|
|
1068
|
+
filesystem
|
|
1069
|
+
});
|
|
1070
|
+
sandboxManager = mgr;
|
|
1071
|
+
sandboxInitialized = true;
|
|
1072
|
+
log.success("Sandbox enabled (srt programmatic API)");
|
|
1073
|
+
return true;
|
|
1074
|
+
} catch (err) {
|
|
1075
|
+
log.error(`Failed to initialize sandbox: ${err}`);
|
|
1076
|
+
return false;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
async function wrapWithSandbox(command, filesystemOverride) {
|
|
1080
|
+
if (!sandboxInitialized || !sandboxManager) return null;
|
|
1081
|
+
if (filesystemOverride) {
|
|
1082
|
+
sandboxManager.updateConfig({
|
|
1083
|
+
filesystem: filesystemOverride
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
try {
|
|
1087
|
+
return await sandboxManager.wrapWithSandbox(command);
|
|
1088
|
+
} catch (err) {
|
|
1089
|
+
log.error(`wrapWithSandbox failed: ${err}`);
|
|
1090
|
+
return null;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
async function resetSandbox() {
|
|
1094
|
+
if (sandboxManager) {
|
|
1095
|
+
try {
|
|
1096
|
+
await sandboxManager.reset();
|
|
1097
|
+
} catch {
|
|
1098
|
+
}
|
|
1099
|
+
sandboxManager = null;
|
|
1100
|
+
sandboxInitialized = false;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
function shellQuote(s) {
|
|
1104
|
+
if (/^[a-zA-Z0-9_\-./=:@]+$/.test(s)) return s;
|
|
1105
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
1106
|
+
}
|
|
1107
|
+
function buildCommandString(command, args) {
|
|
1108
|
+
return [command, ...args.map(shellQuote)].join(" ");
|
|
1109
|
+
}
|
|
1110
|
+
function installSandboxRuntime() {
|
|
1111
|
+
log.info(`Installing ${SRT_PACKAGE}...`);
|
|
1112
|
+
try {
|
|
1113
|
+
execSync(`npm install -g ${SRT_PACKAGE}`, { stdio: "inherit" });
|
|
1114
|
+
log.success(`${SRT_PACKAGE} installed successfully`);
|
|
1115
|
+
return true;
|
|
1116
|
+
} catch {
|
|
1117
|
+
log.error(`Failed to install ${SRT_PACKAGE}. You can install it manually:`);
|
|
1118
|
+
log.error(` npm install -g ${SRT_PACKAGE}`);
|
|
1119
|
+
return false;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// src/utils/process.ts
|
|
1124
|
+
var SANDBOX_ENV_PASSTHROUGH_KEYS = [
|
|
1125
|
+
"ANTHROPIC_API_KEY",
|
|
1126
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
1127
|
+
"ANTHROPIC_BASE_URL",
|
|
1128
|
+
"ANTHROPIC_MODEL",
|
|
1129
|
+
"HAPPY_CLAUDE_PATH",
|
|
1130
|
+
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC",
|
|
1131
|
+
"AGENT_BRIDGE_AGENT_ID"
|
|
1132
|
+
// 当前 Agent 的 UUID,传给 claude code 子进程(A2A caller 标识)
|
|
1133
|
+
];
|
|
1134
|
+
function shellQuote2(value) {
|
|
1135
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
1136
|
+
}
|
|
1137
|
+
function applySandboxEnv(command, env = process.env) {
|
|
1138
|
+
const assignments = [];
|
|
1139
|
+
for (const key of SANDBOX_ENV_PASSTHROUGH_KEYS) {
|
|
1140
|
+
const value = env[key];
|
|
1141
|
+
if (typeof value === "string" && value.length > 0) {
|
|
1142
|
+
assignments.push(`${key}=${shellQuote2(value)}`);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
if (assignments.length === 0) {
|
|
1146
|
+
return command;
|
|
1147
|
+
}
|
|
1148
|
+
return `${assignments.join(" ")} ${command}`;
|
|
1149
|
+
}
|
|
1150
|
+
async function spawnAgent(command, args, options) {
|
|
1151
|
+
const { sandboxEnabled, sandboxFilesystem, ...spawnOptions } = options ?? {};
|
|
1152
|
+
let finalCommand;
|
|
1153
|
+
let finalArgs;
|
|
1154
|
+
if (sandboxEnabled) {
|
|
1155
|
+
const rawCommand = buildCommandString(command, args);
|
|
1156
|
+
const cmdString = applySandboxEnv(rawCommand);
|
|
1157
|
+
const wrapped = await wrapWithSandbox(cmdString, sandboxFilesystem);
|
|
1158
|
+
if (wrapped) {
|
|
1159
|
+
finalCommand = "bash";
|
|
1160
|
+
finalArgs = ["-c", wrapped];
|
|
1161
|
+
} else {
|
|
1162
|
+
finalCommand = command;
|
|
1163
|
+
finalArgs = args;
|
|
1164
|
+
}
|
|
1165
|
+
} else {
|
|
1166
|
+
finalCommand = command;
|
|
1167
|
+
finalArgs = args;
|
|
1168
|
+
}
|
|
1169
|
+
const child = spawn(finalCommand, finalArgs, {
|
|
1170
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1171
|
+
...spawnOptions
|
|
1172
|
+
});
|
|
1173
|
+
return {
|
|
1174
|
+
child,
|
|
1175
|
+
stdout: child.stdout,
|
|
1176
|
+
stderr: child.stderr,
|
|
1177
|
+
stdin: child.stdin,
|
|
1178
|
+
kill() {
|
|
1179
|
+
if (!child.killed) {
|
|
1180
|
+
child.kill("SIGTERM");
|
|
1181
|
+
setTimeout(() => {
|
|
1182
|
+
if (!child.killed) {
|
|
1183
|
+
child.kill("SIGKILL");
|
|
1184
|
+
}
|
|
1185
|
+
}, 5e3);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// src/adapters/claude.ts
|
|
1192
|
+
import { createInterface } from "readline";
|
|
1193
|
+
import { homedir as homedir2 } from "os";
|
|
1194
|
+
|
|
1195
|
+
// src/utils/which.ts
|
|
1196
|
+
import { execFile } from "child_process";
|
|
1197
|
+
import { access, constants } from "fs/promises";
|
|
1198
|
+
import { homedir } from "os";
|
|
1199
|
+
var ALLOWED_COMMANDS = /^[a-zA-Z0-9._-]+$/;
|
|
1200
|
+
var FALLBACK_PATHS = {
|
|
1201
|
+
claude: [
|
|
1202
|
+
"/opt/homebrew/bin/claude",
|
|
1203
|
+
"/usr/local/bin/claude",
|
|
1204
|
+
`${homedir()}/.local/bin/claude`
|
|
1205
|
+
]
|
|
1206
|
+
};
|
|
1207
|
+
async function resolveFallbackPath(command) {
|
|
1208
|
+
const candidates = FALLBACK_PATHS[command] || [];
|
|
1209
|
+
for (const candidate of candidates) {
|
|
1210
|
+
try {
|
|
1211
|
+
await access(candidate, constants.X_OK);
|
|
1212
|
+
return candidate;
|
|
1213
|
+
} catch {
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
return null;
|
|
1217
|
+
}
|
|
1218
|
+
function which(command) {
|
|
1219
|
+
if (!ALLOWED_COMMANDS.test(command)) {
|
|
1220
|
+
return Promise.resolve(null);
|
|
1221
|
+
}
|
|
1222
|
+
return new Promise((resolve2) => {
|
|
1223
|
+
execFile("which", [command], async (err, stdout) => {
|
|
1224
|
+
if (!err && stdout.trim()) {
|
|
1225
|
+
resolve2(stdout.trim());
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
resolve2(await resolveFallbackPath(command));
|
|
1229
|
+
});
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// src/adapters/claude.ts
|
|
1234
|
+
import { readFile as readFile2, writeFile, mkdir } from "fs/promises";
|
|
1235
|
+
import { join as join4, relative as relative3, basename } from "path";
|
|
1236
|
+
var DEFAULT_IDLE_TIMEOUT = 30 * 60 * 1e3;
|
|
1237
|
+
var MIN_IDLE_TIMEOUT = 60 * 1e3;
|
|
1238
|
+
var HOME_DIR = homedir2();
|
|
1239
|
+
var CLAUDE_RUNTIME_ALLOW_WRITE_PATHS = [
|
|
1240
|
+
`${HOME_DIR}/.claude`,
|
|
1241
|
+
`${HOME_DIR}/.claude.json`,
|
|
1242
|
+
`${HOME_DIR}/.claude.json.lock`,
|
|
1243
|
+
`${HOME_DIR}/.claude.json.tmp`,
|
|
1244
|
+
`${HOME_DIR}/.local/state/claude`
|
|
1245
|
+
];
|
|
1246
|
+
var COLLECT_TASK_MARKER = "Collect files task (platform-issued):";
|
|
1247
|
+
var MAX_UPLOAD_FILE_SIZE = 20 * 1024 * 1024;
|
|
1248
|
+
var MAX_COLLECT_FILES = 1500;
|
|
1249
|
+
function resolveIdleTimeoutMs() {
|
|
1250
|
+
const raw = process.env.AGENT_BRIDGE_CLAUDE_IDLE_TIMEOUT_MS;
|
|
1251
|
+
if (!raw) return DEFAULT_IDLE_TIMEOUT;
|
|
1252
|
+
const parsed = Number.parseInt(raw, 10);
|
|
1253
|
+
if (!Number.isFinite(parsed) || parsed < MIN_IDLE_TIMEOUT) {
|
|
1254
|
+
return DEFAULT_IDLE_TIMEOUT;
|
|
1255
|
+
}
|
|
1256
|
+
return parsed;
|
|
1257
|
+
}
|
|
1258
|
+
var IDLE_TIMEOUT = resolveIdleTimeoutMs();
|
|
1259
|
+
var ClaudeSession = class {
|
|
1260
|
+
constructor(sessionId, config, sandboxFilesystem) {
|
|
1261
|
+
this.sessionId = sessionId;
|
|
1262
|
+
this.config = config;
|
|
1263
|
+
this.sandboxFilesystem = sandboxFilesystem;
|
|
1264
|
+
}
|
|
1265
|
+
chunkCallbacks = [];
|
|
1266
|
+
toolCallbacks = [];
|
|
1267
|
+
doneCallbacks = [];
|
|
1268
|
+
errorCallbacks = [];
|
|
1269
|
+
process = null;
|
|
1270
|
+
idleTimer = null;
|
|
1271
|
+
doneFired = false;
|
|
1272
|
+
chunksEmitted = false;
|
|
1273
|
+
config;
|
|
1274
|
+
sandboxFilesystem;
|
|
1275
|
+
/** Track current tool call being streamed */
|
|
1276
|
+
activeToolCallId = null;
|
|
1277
|
+
activeToolName = null;
|
|
1278
|
+
/** Track current content block type to distinguish thinking vs text deltas */
|
|
1279
|
+
currentBlockType = null;
|
|
1280
|
+
/** Upload credentials provided by the platform for auto-uploading output files */
|
|
1281
|
+
uploadCredentials = null;
|
|
1282
|
+
/** Per-client workspace path (symlink-based), set on each send() */
|
|
1283
|
+
currentWorkspace;
|
|
1284
|
+
/** Pre-message workspace file snapshot for diffing */
|
|
1285
|
+
preMessageSnapshot = /* @__PURE__ */ new Map();
|
|
1286
|
+
send(message, attachments, uploadCredentials, clientId) {
|
|
1287
|
+
this.resetIdleTimer();
|
|
1288
|
+
this.doneFired = false;
|
|
1289
|
+
this.chunksEmitted = false;
|
|
1290
|
+
this.activeToolCallId = null;
|
|
1291
|
+
this.activeToolName = null;
|
|
1292
|
+
this.currentBlockType = null;
|
|
1293
|
+
if (uploadCredentials) {
|
|
1294
|
+
this.uploadCredentials = uploadCredentials;
|
|
1295
|
+
}
|
|
1296
|
+
if (clientId && this.config.project) {
|
|
1297
|
+
this.currentWorkspace = createClientWorkspace(this.config.project, clientId);
|
|
1298
|
+
} else {
|
|
1299
|
+
this.currentWorkspace = void 0;
|
|
1300
|
+
}
|
|
1301
|
+
const collectTask = this.parseCollectWorkspaceTask(message);
|
|
1302
|
+
if (collectTask) {
|
|
1303
|
+
void this.runCollectWorkspaceTask(collectTask);
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
const args = ["-p", message, "--output-format", "stream-json", "--verbose", "--include-partial-messages", "--dangerously-skip-permissions"];
|
|
1307
|
+
void this.downloadAttachments(attachments).then(() => this.takeSnapshot()).then(() => {
|
|
1308
|
+
this.launchProcess(args);
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Download incoming attachment URLs to the workspace directory so Claude can read them.
|
|
1313
|
+
* Runs before the workspace snapshot so downloaded files are treated as pre-existing inputs.
|
|
1314
|
+
*/
|
|
1315
|
+
async downloadAttachments(attachments) {
|
|
1316
|
+
if (!attachments || attachments.length === 0) return;
|
|
1317
|
+
const workspaceRoot = this.currentWorkspace || this.config.project;
|
|
1318
|
+
if (!workspaceRoot) return;
|
|
1319
|
+
await mkdir(workspaceRoot, { recursive: true });
|
|
1320
|
+
for (const att of attachments) {
|
|
1321
|
+
const safeName = basename(att.name).replace(/[^a-zA-Z0-9._-]/g, "_") || "attachment";
|
|
1322
|
+
const destPath = join4(workspaceRoot, safeName);
|
|
1323
|
+
try {
|
|
1324
|
+
const res = await fetch(att.url);
|
|
1325
|
+
if (!res.ok) {
|
|
1326
|
+
log.warn(`Attachment download failed (${res.status}): ${safeName}`);
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
1330
|
+
await writeFile(destPath, buf);
|
|
1331
|
+
log.info(`Downloaded attachment: ${safeName} (${buf.length} bytes)`);
|
|
1332
|
+
} catch (err) {
|
|
1333
|
+
log.warn(`Attachment download error for ${safeName}: ${err}`);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
async launchProcess(args) {
|
|
1338
|
+
const cwd = this.currentWorkspace || this.config.project || void 0;
|
|
1339
|
+
try {
|
|
1340
|
+
this.process = await spawnAgent("claude", args, {
|
|
1341
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1342
|
+
cwd: cwd || void 0,
|
|
1343
|
+
sandboxEnabled: this.config.sandboxEnabled,
|
|
1344
|
+
sandboxFilesystem: this.sandboxFilesystem,
|
|
1345
|
+
env: {
|
|
1346
|
+
...process.env,
|
|
1347
|
+
...this.config.agentId ? { AGENT_BRIDGE_AGENT_ID: this.config.agentId } : {}
|
|
1348
|
+
}
|
|
1349
|
+
});
|
|
1350
|
+
} catch (err) {
|
|
1351
|
+
this.emitError(new Error(`Failed to spawn claude: ${err}`));
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
const rl = createInterface({ input: this.process.stdout });
|
|
1355
|
+
let errorDetail = "";
|
|
1356
|
+
let stderrText = "";
|
|
1357
|
+
rl.on("line", (line) => {
|
|
1358
|
+
this.resetIdleTimer();
|
|
1359
|
+
if (!line.trim()) return;
|
|
1360
|
+
try {
|
|
1361
|
+
const event = JSON.parse(line);
|
|
1362
|
+
if (event.is_error && typeof event.result === "string") {
|
|
1363
|
+
errorDetail = event.result;
|
|
1364
|
+
}
|
|
1365
|
+
if (event.error && typeof event.error === "string") {
|
|
1366
|
+
errorDetail = errorDetail || event.error;
|
|
1367
|
+
}
|
|
1368
|
+
this.handleEvent(event);
|
|
1369
|
+
} catch {
|
|
1370
|
+
log.debug(`Claude non-JSON line: ${line}`);
|
|
1371
|
+
}
|
|
1372
|
+
});
|
|
1373
|
+
this.process.stderr.on("data", (data) => {
|
|
1374
|
+
const text = data.toString().trim();
|
|
1375
|
+
if (text) {
|
|
1376
|
+
stderrText += text + "\n";
|
|
1377
|
+
log.debug(`Claude stderr: ${text}`);
|
|
1378
|
+
}
|
|
1379
|
+
});
|
|
1380
|
+
this.process.child.on("exit", (code) => {
|
|
1381
|
+
if (code !== 0 && code !== null) {
|
|
1382
|
+
setTimeout(() => {
|
|
1383
|
+
if (this.doneFired) return;
|
|
1384
|
+
const detail = errorDetail || stderrText.trim();
|
|
1385
|
+
const msg = detail ? `Claude process failed: ${detail}` : `Claude process exited with code ${code}`;
|
|
1386
|
+
this.emitError(new Error(msg));
|
|
1387
|
+
}, 50);
|
|
1388
|
+
}
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
parseCollectWorkspaceTask(message) {
|
|
1392
|
+
if (!message.includes("[PLATFORM TASK]") || !message.includes("[END PLATFORM TASK]")) {
|
|
1393
|
+
return null;
|
|
1394
|
+
}
|
|
1395
|
+
if (!message.includes(COLLECT_TASK_MARKER)) {
|
|
1396
|
+
return null;
|
|
1397
|
+
}
|
|
1398
|
+
const urlMatch = message.match(/UPLOAD_URL=(\S+)/);
|
|
1399
|
+
const tokenMatch = message.match(/UPLOAD_TOKEN=(\S+)/);
|
|
1400
|
+
if (!urlMatch || !tokenMatch) {
|
|
1401
|
+
return null;
|
|
1402
|
+
}
|
|
1403
|
+
return {
|
|
1404
|
+
uploadUrl: urlMatch[1].trim(),
|
|
1405
|
+
uploadToken: tokenMatch[1].trim()
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
async runCollectWorkspaceTask(task) {
|
|
1409
|
+
const workspaceRoot = this.currentWorkspace || this.config.project || process.cwd();
|
|
1410
|
+
try {
|
|
1411
|
+
const files = await this.collectWorkspaceFiles(workspaceRoot);
|
|
1412
|
+
if (files.length === 0) {
|
|
1413
|
+
this.emitChunk("NO_FILES_FOUND");
|
|
1414
|
+
this.doneFired = true;
|
|
1415
|
+
for (const cb of this.doneCallbacks) cb();
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
const uploadedUrls = [];
|
|
1419
|
+
for (const absPath of files) {
|
|
1420
|
+
this.resetIdleTimer();
|
|
1421
|
+
try {
|
|
1422
|
+
const buffer = await readFile2(absPath);
|
|
1423
|
+
if (buffer.length === 0 || buffer.length > MAX_UPLOAD_FILE_SIZE) {
|
|
1424
|
+
continue;
|
|
1425
|
+
}
|
|
1426
|
+
const relPath = relative3(workspaceRoot, absPath).replace(/\\/g, "/");
|
|
1427
|
+
const filename = relPath && !relPath.startsWith("..") ? relPath : absPath.split("/").pop() || "file";
|
|
1428
|
+
const response = await fetch(task.uploadUrl, {
|
|
1429
|
+
method: "POST",
|
|
1430
|
+
headers: {
|
|
1431
|
+
"X-Upload-Token": task.uploadToken,
|
|
1432
|
+
"Content-Type": "application/json"
|
|
1433
|
+
},
|
|
1434
|
+
body: JSON.stringify({
|
|
1435
|
+
filename,
|
|
1436
|
+
content: buffer.toString("base64")
|
|
1437
|
+
})
|
|
1438
|
+
});
|
|
1439
|
+
if (!response.ok) {
|
|
1440
|
+
log.warn(`collect-files upload failed (${response.status}) for ${filename}`);
|
|
1441
|
+
continue;
|
|
1442
|
+
}
|
|
1443
|
+
const payload = await response.json();
|
|
1444
|
+
if (typeof payload.url === "string" && payload.url.length > 0) {
|
|
1445
|
+
uploadedUrls.push(payload.url);
|
|
1446
|
+
}
|
|
1447
|
+
} catch (error) {
|
|
1448
|
+
log.warn(`collect-files upload error for ${absPath}: ${error}`);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
if (uploadedUrls.length === 0) {
|
|
1452
|
+
this.emitChunk("COLLECT_FILES_FAILED");
|
|
1453
|
+
} else {
|
|
1454
|
+
this.emitChunk(uploadedUrls.join("\n"));
|
|
1455
|
+
}
|
|
1456
|
+
this.doneFired = true;
|
|
1457
|
+
for (const cb of this.doneCallbacks) cb();
|
|
1458
|
+
} catch (error) {
|
|
1459
|
+
this.emitError(new Error(`Collect files task failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
async collectWorkspaceFiles(workspaceRoot) {
|
|
1463
|
+
return collectRealFiles(workspaceRoot, MAX_COLLECT_FILES);
|
|
1464
|
+
}
|
|
1465
|
+
onChunk(cb) {
|
|
1466
|
+
this.chunkCallbacks.push(cb);
|
|
1467
|
+
}
|
|
1468
|
+
onToolEvent(cb) {
|
|
1469
|
+
this.toolCallbacks.push(cb);
|
|
1470
|
+
}
|
|
1471
|
+
onDone(cb) {
|
|
1472
|
+
this.doneCallbacks.push(cb);
|
|
1473
|
+
}
|
|
1474
|
+
onError(cb) {
|
|
1475
|
+
this.errorCallbacks.push(cb);
|
|
1476
|
+
}
|
|
1477
|
+
kill() {
|
|
1478
|
+
this.clearIdleTimer();
|
|
1479
|
+
if (this.process) {
|
|
1480
|
+
this.process.kill();
|
|
1481
|
+
this.process = null;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
handleEvent(event) {
|
|
1485
|
+
if (event.type === "stream_event" && event.event) {
|
|
1486
|
+
const inner = event.event;
|
|
1487
|
+
if (inner.type === "content_block_start") {
|
|
1488
|
+
const blockType = inner.content_block?.type;
|
|
1489
|
+
if (blockType === "thinking") {
|
|
1490
|
+
this.currentBlockType = "thinking";
|
|
1491
|
+
} else if (blockType === "text") {
|
|
1492
|
+
this.currentBlockType = "text";
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
if (inner.type === "content_block_delta" && inner.delta?.type === "text_delta" && inner.delta.text) {
|
|
1496
|
+
if (this.currentBlockType === "thinking") {
|
|
1497
|
+
this.emitToolEvent({ kind: "thinking", tool_name: "", tool_call_id: "", delta: inner.delta.text });
|
|
1498
|
+
} else {
|
|
1499
|
+
this.emitChunk(inner.delta.text);
|
|
1500
|
+
}
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
if (inner.type === "content_block_delta" && inner.delta?.type === "thinking_delta" && inner.delta.thinking) {
|
|
1504
|
+
this.emitToolEvent({
|
|
1505
|
+
kind: "thinking",
|
|
1506
|
+
tool_name: "",
|
|
1507
|
+
tool_call_id: "",
|
|
1508
|
+
delta: inner.delta.thinking
|
|
1509
|
+
});
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
if (inner.type === "content_block_start" && inner.content_block?.type === "tool_use") {
|
|
1513
|
+
const toolCallId = inner.content_block.id || `tool-${Date.now()}`;
|
|
1514
|
+
const toolName = inner.content_block.name || "unknown";
|
|
1515
|
+
this.activeToolCallId = toolCallId;
|
|
1516
|
+
this.activeToolName = toolName;
|
|
1517
|
+
this.emitToolEvent({
|
|
1518
|
+
kind: "tool_start",
|
|
1519
|
+
tool_name: toolName,
|
|
1520
|
+
tool_call_id: toolCallId,
|
|
1521
|
+
delta: ""
|
|
1522
|
+
});
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
if (inner.type === "content_block_delta" && inner.delta?.type === "input_json_delta" && inner.delta.partial_json !== void 0) {
|
|
1526
|
+
if (this.activeToolCallId && this.activeToolName) {
|
|
1527
|
+
this.emitToolEvent({
|
|
1528
|
+
kind: "tool_input",
|
|
1529
|
+
tool_name: this.activeToolName,
|
|
1530
|
+
tool_call_id: this.activeToolCallId,
|
|
1531
|
+
delta: inner.delta.partial_json
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
if (inner.type === "content_block_stop") {
|
|
1537
|
+
this.activeToolCallId = null;
|
|
1538
|
+
this.activeToolName = null;
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
if (inner.type && inner.type !== "message_start" && inner.type !== "message_stop") {
|
|
1542
|
+
this.emitToolEvent({
|
|
1543
|
+
kind: "status",
|
|
1544
|
+
tool_name: "",
|
|
1545
|
+
tool_call_id: "",
|
|
1546
|
+
delta: JSON.stringify(inner)
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
if (event.type === "user" && event.message?.content) {
|
|
1552
|
+
for (const block of event.message.content) {
|
|
1553
|
+
if (block.type === "tool_result") {
|
|
1554
|
+
const toolCallId = block.tool_use_id || "unknown";
|
|
1555
|
+
const resultText = typeof block.content === "string" ? block.content : JSON.stringify(block.content ?? "");
|
|
1556
|
+
const isError = !!block.is_error;
|
|
1557
|
+
this.emitToolEvent({
|
|
1558
|
+
kind: "tool_result",
|
|
1559
|
+
tool_name: "",
|
|
1560
|
+
// tool name not in result event
|
|
1561
|
+
tool_call_id: toolCallId,
|
|
1562
|
+
delta: isError ? `[error] ${resultText}` : resultText
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
if (event.type === "assistant" && event.subtype === "text_delta" && event.delta?.text) {
|
|
1569
|
+
this.emitChunk(event.delta.text);
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
if (event.type === "content_block_delta" && event.delta?.type === "text_delta" && event.delta?.text) {
|
|
1573
|
+
this.emitChunk(event.delta.text);
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
if (event.type === "assistant" && event.message?.content) {
|
|
1577
|
+
if (event.error) return;
|
|
1578
|
+
if (this.chunksEmitted) return;
|
|
1579
|
+
for (const block of event.message.content) {
|
|
1580
|
+
if (block.type === "text" && block.text) {
|
|
1581
|
+
this.emitTextAsChunks(block.text);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
if (event.type === "result") {
|
|
1587
|
+
if (event.is_error) {
|
|
1588
|
+
const errorText = typeof event.result === "string" && event.result ? event.result : "Claude returned an error";
|
|
1589
|
+
this.doneFired = true;
|
|
1590
|
+
this.emitError(new Error(errorText));
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
if (!this.chunksEmitted) {
|
|
1594
|
+
if (typeof event.result === "string" && event.result) {
|
|
1595
|
+
this.emitTextAsChunks(event.result);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
this.doneFired = true;
|
|
1599
|
+
void this.autoUploadAndDone();
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1602
|
+
if (event.type === "assistant" && event.subtype === "end") {
|
|
1603
|
+
this.doneFired = true;
|
|
1604
|
+
for (const cb of this.doneCallbacks) cb();
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
/**
|
|
1609
|
+
* Auto-upload new/modified files from workspace, then fire done callbacks.
|
|
1610
|
+
*/
|
|
1611
|
+
async autoUploadAndDone() {
|
|
1612
|
+
let attachments;
|
|
1613
|
+
const workspaceRoot = this.currentWorkspace || this.config.project;
|
|
1614
|
+
if (this.uploadCredentials && workspaceRoot) {
|
|
1615
|
+
try {
|
|
1616
|
+
attachments = await diffAndUpload({
|
|
1617
|
+
workspace: workspaceRoot,
|
|
1618
|
+
snapshot: this.preMessageSnapshot,
|
|
1619
|
+
uploadUrl: this.uploadCredentials.uploadUrl,
|
|
1620
|
+
uploadToken: this.uploadCredentials.uploadToken
|
|
1621
|
+
});
|
|
1622
|
+
if (attachments && attachments.length > 0) {
|
|
1623
|
+
log.info(`Auto-uploaded ${attachments.length} file(s) from workspace`);
|
|
1624
|
+
}
|
|
1625
|
+
} catch (err) {
|
|
1626
|
+
log.warn(`Auto-upload failed: ${err}`);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
for (const cb of this.doneCallbacks) cb(attachments);
|
|
1630
|
+
}
|
|
1631
|
+
/**
|
|
1632
|
+
* Snapshot all files in the workspace before Claude starts processing.
|
|
1633
|
+
*/
|
|
1634
|
+
async takeSnapshot() {
|
|
1635
|
+
this.preMessageSnapshot.clear();
|
|
1636
|
+
const workspaceRoot = this.currentWorkspace || this.config.project;
|
|
1637
|
+
if (!workspaceRoot) return;
|
|
1638
|
+
this.preMessageSnapshot = await snapshotWorkspace(workspaceRoot);
|
|
1639
|
+
}
|
|
1640
|
+
emitChunk(text) {
|
|
1641
|
+
this.chunksEmitted = true;
|
|
1642
|
+
for (const cb of this.chunkCallbacks) cb(text);
|
|
1643
|
+
}
|
|
1644
|
+
emitToolEvent(event) {
|
|
1645
|
+
for (const cb of this.toolCallbacks) cb(event);
|
|
1646
|
+
}
|
|
1647
|
+
emitTextAsChunks(text) {
|
|
1648
|
+
const CHUNK_SIZE = 60;
|
|
1649
|
+
if (text.length <= CHUNK_SIZE) {
|
|
1650
|
+
this.emitChunk(text);
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
let pos = 0;
|
|
1654
|
+
while (pos < text.length) {
|
|
1655
|
+
let end = Math.min(pos + CHUNK_SIZE, text.length);
|
|
1656
|
+
if (end < text.length) {
|
|
1657
|
+
const slice = text.slice(pos, end + 20);
|
|
1658
|
+
const breakPoints = ["\n", "\u3002", "\uFF01", "\uFF1F", ". ", "! ", "? ", "\uFF0C", ", ", " "];
|
|
1659
|
+
for (const bp of breakPoints) {
|
|
1660
|
+
const idx = slice.indexOf(bp, CHUNK_SIZE - 20);
|
|
1661
|
+
if (idx >= 0 && idx < CHUNK_SIZE + 20) {
|
|
1662
|
+
end = pos + idx + bp.length;
|
|
1663
|
+
break;
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
this.emitChunk(text.slice(pos, end));
|
|
1668
|
+
pos = end;
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
resetIdleTimer() {
|
|
1672
|
+
this.clearIdleTimer();
|
|
1673
|
+
this.idleTimer = setTimeout(() => {
|
|
1674
|
+
log.warn(`Claude session ${this.sessionId} idle timeout, killing process`);
|
|
1675
|
+
this.kill();
|
|
1676
|
+
}, IDLE_TIMEOUT);
|
|
1677
|
+
}
|
|
1678
|
+
clearIdleTimer() {
|
|
1679
|
+
if (this.idleTimer) {
|
|
1680
|
+
clearTimeout(this.idleTimer);
|
|
1681
|
+
this.idleTimer = null;
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
emitError(err) {
|
|
1685
|
+
if (this.errorCallbacks.length > 0) {
|
|
1686
|
+
for (const cb of this.errorCallbacks) cb(err);
|
|
1687
|
+
} else {
|
|
1688
|
+
log.error(err.message);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
};
|
|
1692
|
+
var ClaudeAdapter = class extends AgentAdapter {
|
|
1693
|
+
type = "claude";
|
|
1694
|
+
displayName = "Claude Code";
|
|
1695
|
+
sessions = /* @__PURE__ */ new Map();
|
|
1696
|
+
config;
|
|
1697
|
+
constructor(config = {}) {
|
|
1698
|
+
super();
|
|
1699
|
+
this.config = config;
|
|
1700
|
+
}
|
|
1701
|
+
async isAvailable() {
|
|
1702
|
+
return !!await which("claude");
|
|
1703
|
+
}
|
|
1704
|
+
createSession(id, config) {
|
|
1705
|
+
const merged = { ...this.config, ...config };
|
|
1706
|
+
let sandboxFilesystem;
|
|
1707
|
+
if (merged.sandboxEnabled && merged.project) {
|
|
1708
|
+
const preset = getSandboxPreset("claude");
|
|
1709
|
+
sandboxFilesystem = {
|
|
1710
|
+
denyRead: preset.denyRead,
|
|
1711
|
+
allowWrite: Array.from(/* @__PURE__ */ new Set([merged.project, "/tmp", ...CLAUDE_RUNTIME_ALLOW_WRITE_PATHS])),
|
|
1712
|
+
denyWrite: preset.denyWrite
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
const session = new ClaudeSession(id, merged, sandboxFilesystem);
|
|
1716
|
+
this.sessions.set(id, session);
|
|
1717
|
+
return session;
|
|
1718
|
+
}
|
|
1719
|
+
destroySession(id) {
|
|
1720
|
+
const session = this.sessions.get(id);
|
|
1721
|
+
if (session) {
|
|
1722
|
+
session.kill();
|
|
1723
|
+
this.sessions.delete(id);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
};
|
|
1727
|
+
|
|
1728
|
+
// src/adapters/codex.ts
|
|
1729
|
+
var CodexAdapter = class extends AgentAdapter {
|
|
1730
|
+
type = "codex";
|
|
1731
|
+
displayName = "Codex CLI";
|
|
1732
|
+
async isAvailable() {
|
|
1733
|
+
return false;
|
|
1734
|
+
}
|
|
1735
|
+
createSession(_id, _config) {
|
|
1736
|
+
throw new Error("Codex adapter not yet implemented");
|
|
1737
|
+
}
|
|
1738
|
+
destroySession(_id) {
|
|
1739
|
+
}
|
|
1740
|
+
};
|
|
1741
|
+
|
|
1742
|
+
// src/adapters/gemini.ts
|
|
1743
|
+
var GeminiAdapter = class extends AgentAdapter {
|
|
1744
|
+
type = "gemini";
|
|
1745
|
+
displayName = "Gemini CLI";
|
|
1746
|
+
async isAvailable() {
|
|
1747
|
+
return false;
|
|
1748
|
+
}
|
|
1749
|
+
createSession(_id, _config) {
|
|
1750
|
+
throw new Error("Gemini adapter not yet implemented");
|
|
1751
|
+
}
|
|
1752
|
+
destroySession(_id) {
|
|
1753
|
+
}
|
|
1754
|
+
};
|
|
1755
|
+
|
|
1756
|
+
// src/commands/connect.ts
|
|
1757
|
+
var DEFAULT_BRIDGE_URL = "wss://bridge.agents.hot/ws";
|
|
1758
|
+
function logWorkspaceHint(slug, projectPath) {
|
|
1759
|
+
console.log(` ${GRAY}Workspace: ${RESET}${projectPath}`);
|
|
1760
|
+
console.log(` ${GRAY}Put CLAUDE.md (role instructions) and .claude/skills/ here.${RESET}`);
|
|
1761
|
+
}
|
|
1762
|
+
function sleep(ms) {
|
|
1763
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1764
|
+
}
|
|
1765
|
+
function createAdapter(type, config) {
|
|
1766
|
+
switch (type) {
|
|
1767
|
+
case "openclaw":
|
|
1768
|
+
return new OpenClawAdapter(config);
|
|
1769
|
+
case "claude":
|
|
1770
|
+
return new ClaudeAdapter(config);
|
|
1771
|
+
case "codex":
|
|
1772
|
+
return new CodexAdapter(config);
|
|
1773
|
+
case "gemini":
|
|
1774
|
+
return new GeminiAdapter(config);
|
|
1775
|
+
default:
|
|
1776
|
+
throw new Error(`Unknown agent type: ${type}. Supported: openclaw, claude, codex, gemini`);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
function registerConnectCommand(program2) {
|
|
1780
|
+
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").option("--foreground", "Run in foreground (default for non-setup mode)").action(async (type, opts) => {
|
|
1781
|
+
const config = loadConfig();
|
|
1782
|
+
if (opts.setup) {
|
|
1783
|
+
log.info("Fetching configuration from connect ticket...");
|
|
1784
|
+
try {
|
|
1785
|
+
const response = await fetch(opts.setup);
|
|
1786
|
+
if (!response.ok) {
|
|
1787
|
+
const body = await response.json().catch(() => ({ message: response.statusText }));
|
|
1788
|
+
log.error(`Ticket redemption failed: ${body.message || response.statusText}`);
|
|
1789
|
+
if (response.status === 404) {
|
|
1790
|
+
log.error("The ticket may have expired or already been used.");
|
|
1791
|
+
}
|
|
1792
|
+
process.exit(1);
|
|
1793
|
+
}
|
|
1794
|
+
const ticketData = await response.json();
|
|
1795
|
+
let gatewayToken = opts.gatewayToken;
|
|
1796
|
+
if (ticketData.agent_type === "openclaw" && !gatewayToken) {
|
|
1797
|
+
const localToken = readOpenClawToken();
|
|
1798
|
+
if (localToken) {
|
|
1799
|
+
gatewayToken = localToken;
|
|
1800
|
+
log.success("Auto-detected OpenClaw gateway token from ~/.openclaw/openclaw.json");
|
|
1801
|
+
} else {
|
|
1802
|
+
log.warn("Could not auto-detect OpenClaw token. Use --gateway-token to provide it manually.");
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
let nameBase = ticketData.agent_id.slice(0, 8);
|
|
1806
|
+
if (config.token) {
|
|
1807
|
+
try {
|
|
1808
|
+
const res = await fetch(`https://agents.hot/api/developer/agents/${ticketData.agent_id}`, {
|
|
1809
|
+
headers: { Authorization: `Bearer ${config.token}` }
|
|
1810
|
+
});
|
|
1811
|
+
if (res.ok) {
|
|
1812
|
+
const agentData = await res.json();
|
|
1813
|
+
if (agentData.name) nameBase = agentData.name;
|
|
1814
|
+
}
|
|
1815
|
+
} catch {
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
const bridgeAuthToken = ticketData.token || ticketData.bridge_token || "";
|
|
1819
|
+
if (bridgeAuthToken.startsWith("ah_") && !loadToken()) {
|
|
1820
|
+
saveToken(bridgeAuthToken);
|
|
1821
|
+
}
|
|
1822
|
+
const slug = uniqueSlug(nameBase);
|
|
1823
|
+
const entry = {
|
|
1824
|
+
agentId: ticketData.agent_id,
|
|
1825
|
+
agentType: ticketData.agent_type,
|
|
1826
|
+
bridgeUrl: ticketData.bridge_url,
|
|
1827
|
+
gatewayUrl: opts.gatewayUrl,
|
|
1828
|
+
gatewayToken,
|
|
1829
|
+
projectPath: opts.project || getAgentWorkspaceDir(slug),
|
|
1830
|
+
sandbox: opts.sandbox,
|
|
1831
|
+
addedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1832
|
+
};
|
|
1833
|
+
addAgent(slug, entry);
|
|
1834
|
+
log.success(`Agent registered as "${slug}"`);
|
|
1835
|
+
logWorkspaceHint(slug, entry.projectPath);
|
|
1836
|
+
if (opts.foreground) {
|
|
1837
|
+
opts.agentId = ticketData.agent_id;
|
|
1838
|
+
opts.bridgeUrl = ticketData.bridge_url;
|
|
1839
|
+
opts.gatewayToken = gatewayToken;
|
|
1840
|
+
type = ticketData.agent_type;
|
|
1841
|
+
} else {
|
|
1842
|
+
const pid = spawnBackground(slug, entry, config.token);
|
|
1843
|
+
await sleep(500);
|
|
1844
|
+
if (isProcessAlive(pid)) {
|
|
1845
|
+
console.log(` ${GREEN}\u2713${RESET} ${BOLD}${slug}${RESET} started (PID: ${pid})`);
|
|
1846
|
+
} else {
|
|
1847
|
+
log.error(`Failed to start. Check logs: ${getLogPath(slug)}`);
|
|
1848
|
+
process.exit(1);
|
|
1849
|
+
}
|
|
1850
|
+
const { ListTUI } = await import("./list-6CHWMM3O.js");
|
|
1851
|
+
const tui = new ListTUI();
|
|
1852
|
+
await tui.run();
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
} catch (err) {
|
|
1856
|
+
if (err instanceof Error && err.message.includes("fetch")) {
|
|
1857
|
+
log.error(`Failed to fetch ticket: ${err.message}`);
|
|
1858
|
+
} else {
|
|
1859
|
+
throw err;
|
|
1860
|
+
}
|
|
1861
|
+
process.exit(1);
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
let agentName;
|
|
1865
|
+
const agentType = type || (() => {
|
|
1866
|
+
if (opts.agentId) {
|
|
1867
|
+
const found2 = findAgentByAgentId(opts.agentId);
|
|
1868
|
+
if (found2) return found2.entry.agentType;
|
|
1869
|
+
}
|
|
1870
|
+
return void 0;
|
|
1871
|
+
})();
|
|
1872
|
+
if (!agentType) {
|
|
1873
|
+
log.error("Agent type is required. Use: agent-mesh connect <type> or agent-mesh connect --setup <url>");
|
|
1874
|
+
process.exit(1);
|
|
1875
|
+
}
|
|
1876
|
+
const agentId = opts.agentId;
|
|
1877
|
+
if (!agentId) {
|
|
1878
|
+
log.error("--agent-id is required. Use --setup for automatic configuration.");
|
|
1879
|
+
process.exit(1);
|
|
1880
|
+
}
|
|
1881
|
+
const found = findAgentByAgentId(agentId);
|
|
1882
|
+
if (found) {
|
|
1883
|
+
agentName = found.name;
|
|
1884
|
+
const entry = found.entry;
|
|
1885
|
+
opts.bridgeUrl = opts.bridgeUrl || entry.bridgeUrl;
|
|
1886
|
+
opts.gatewayUrl = opts.gatewayUrl || entry.gatewayUrl;
|
|
1887
|
+
opts.gatewayToken = opts.gatewayToken || entry.gatewayToken;
|
|
1888
|
+
opts.project = opts.project || entry.projectPath || getAgentWorkspaceDir(found.name);
|
|
1889
|
+
if (opts.sandbox === void 0 && entry.sandbox !== void 0) opts.sandbox = entry.sandbox;
|
|
1890
|
+
}
|
|
1891
|
+
if (!agentName) {
|
|
1892
|
+
let nameBase = agentId.slice(0, 8);
|
|
1893
|
+
if (config.token) {
|
|
1894
|
+
try {
|
|
1895
|
+
const res = await fetch(`https://agents.hot/api/developer/agents/${agentId}`, {
|
|
1896
|
+
headers: { Authorization: `Bearer ${config.token}` }
|
|
1897
|
+
});
|
|
1898
|
+
if (res.ok) {
|
|
1899
|
+
const agentData = await res.json();
|
|
1900
|
+
if (agentData.name) nameBase = agentData.name;
|
|
1901
|
+
}
|
|
1902
|
+
} catch {
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
agentName = uniqueSlug(nameBase);
|
|
1906
|
+
}
|
|
1907
|
+
if (!opts.project) {
|
|
1908
|
+
opts.project = getAgentWorkspaceDir(agentName);
|
|
1909
|
+
}
|
|
1910
|
+
const token = process.env.AGENT_BRIDGE_TOKEN || loadToken() || config.token || found?.entry.bridgeToken;
|
|
1911
|
+
if (!token) {
|
|
1912
|
+
log.error("Not authenticated. Run `agent-mesh login` or use `agent-mesh connect --setup <url>`.");
|
|
1913
|
+
process.exit(1);
|
|
1914
|
+
}
|
|
1915
|
+
const bridgeUrl = opts.bridgeUrl || DEFAULT_BRIDGE_URL;
|
|
1916
|
+
const sandboxEnabled = opts.sandbox ?? true;
|
|
1917
|
+
if (sandboxEnabled) {
|
|
1918
|
+
const ok = await initSandbox(agentType);
|
|
1919
|
+
if (!ok) {
|
|
1920
|
+
log.warn("Sandbox not available on this platform, continuing without sandbox");
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
if (agentType === "openclaw") {
|
|
1924
|
+
const { isChatCompletionsEnabled } = await import("./openclaw-config-OFFNWVDK.js");
|
|
1925
|
+
if (!isChatCompletionsEnabled()) {
|
|
1926
|
+
log.warn(
|
|
1927
|
+
'OpenClaw chatCompletions endpoint may not be enabled.\n Add to ~/.openclaw/openclaw.json:\n { "gateway": { "http": { "endpoints": { "chatCompletions": { "enabled": true } } } } }\n Continuing anyway (gateway may be on a remote host)...'
|
|
1928
|
+
);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
const adapterConfig = {
|
|
1932
|
+
project: opts.project,
|
|
1933
|
+
gatewayUrl: opts.gatewayUrl,
|
|
1934
|
+
gatewayToken: opts.gatewayToken,
|
|
1935
|
+
sandboxEnabled,
|
|
1936
|
+
agentId
|
|
1937
|
+
};
|
|
1938
|
+
const adapter = createAdapter(agentType, adapterConfig);
|
|
1939
|
+
log.info(`Checking ${adapter.displayName} availability...`);
|
|
1940
|
+
const available = await adapter.isAvailable();
|
|
1941
|
+
if (!available) {
|
|
1942
|
+
if (agentType === "codex" || agentType === "gemini") {
|
|
1943
|
+
log.error(`${adapter.displayName} adapter is not yet implemented. Supported adapters: openclaw, claude`);
|
|
1944
|
+
} else {
|
|
1945
|
+
log.error(`${adapter.displayName} is not available. Make sure it is installed and running.`);
|
|
1946
|
+
}
|
|
1947
|
+
process.exit(1);
|
|
1948
|
+
}
|
|
1949
|
+
log.success(`${adapter.displayName} is available`);
|
|
1950
|
+
log.info(`Connecting to bridge worker at ${bridgeUrl}...`);
|
|
1951
|
+
const wsClient = new BridgeWSClient({
|
|
1952
|
+
url: bridgeUrl,
|
|
1953
|
+
token,
|
|
1954
|
+
agentId,
|
|
1955
|
+
agentType
|
|
1956
|
+
});
|
|
1957
|
+
try {
|
|
1958
|
+
await wsClient.connect();
|
|
1959
|
+
} catch (err) {
|
|
1960
|
+
log.error(`Failed to connect to bridge worker: ${err}`);
|
|
1961
|
+
process.exit(1);
|
|
1962
|
+
}
|
|
1963
|
+
log.success(`Registered as agent "${agentId}" (${agentType})`);
|
|
1964
|
+
logWorkspaceHint(agentName, opts.project);
|
|
1965
|
+
if (!found) {
|
|
1966
|
+
addAgent(agentName, {
|
|
1967
|
+
agentId,
|
|
1968
|
+
agentType,
|
|
1969
|
+
bridgeUrl,
|
|
1970
|
+
gatewayUrl: opts.gatewayUrl,
|
|
1971
|
+
gatewayToken: opts.gatewayToken,
|
|
1972
|
+
projectPath: opts.project,
|
|
1973
|
+
sandbox: opts.sandbox,
|
|
1974
|
+
addedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1975
|
+
});
|
|
1976
|
+
log.info(`Agent saved as "${agentName}"`);
|
|
1977
|
+
} else if (found && !found.entry.projectPath) {
|
|
1978
|
+
addAgent(agentName, {
|
|
1979
|
+
...found.entry,
|
|
1980
|
+
projectPath: opts.project
|
|
1981
|
+
});
|
|
1982
|
+
log.info(`Updated "${agentName}" with workspace directory`);
|
|
1983
|
+
}
|
|
1984
|
+
if (agentName) writePid(agentName, process.pid);
|
|
1985
|
+
const manager = new BridgeManager({ wsClient, adapter, adapterConfig });
|
|
1986
|
+
manager.start();
|
|
1987
|
+
log.banner(`Agent bridge is running. Press Ctrl+C to stop.`);
|
|
1988
|
+
const shutdown = () => {
|
|
1989
|
+
log.info("Shutting down...");
|
|
1990
|
+
manager.stop();
|
|
1991
|
+
wsClient.close();
|
|
1992
|
+
resetSandbox();
|
|
1993
|
+
if (agentName) removePid(agentName);
|
|
1994
|
+
process.exit(0);
|
|
1995
|
+
};
|
|
1996
|
+
process.on("SIGINT", shutdown);
|
|
1997
|
+
process.on("SIGTERM", shutdown);
|
|
1998
|
+
const MAX_UPTIME_MS = 24 * 60 * 60 * 1e3;
|
|
1999
|
+
setTimeout(() => {
|
|
2000
|
+
log.info("Max uptime reached (24h), shutting down for fresh restart...");
|
|
2001
|
+
shutdown();
|
|
2002
|
+
}, MAX_UPTIME_MS).unref();
|
|
2003
|
+
if (process.env.DEBUG) {
|
|
2004
|
+
setInterval(() => {
|
|
2005
|
+
const mem = process.memoryUsage();
|
|
2006
|
+
log.debug(`Memory: RSS=${(mem.rss / 1024 / 1024).toFixed(1)}MB Heap=${(mem.heapUsed / 1024 / 1024).toFixed(1)}MB`);
|
|
2007
|
+
}, 5 * 60 * 1e3).unref();
|
|
2008
|
+
}
|
|
2009
|
+
wsClient.on("error", (err) => {
|
|
2010
|
+
log.error(`Bridge connection error: ${err.message}`);
|
|
2011
|
+
});
|
|
2012
|
+
wsClient.on("replaced", () => {
|
|
2013
|
+
log.error("Shutting down \u2014 only one CLI per agent is allowed.");
|
|
2014
|
+
manager.stop();
|
|
2015
|
+
resetSandbox();
|
|
2016
|
+
if (agentName) removePid(agentName);
|
|
2017
|
+
process.exit(1);
|
|
2018
|
+
});
|
|
2019
|
+
wsClient.on("token_revoked", () => {
|
|
2020
|
+
log.error("Token revoked \u2014 shutting down.");
|
|
2021
|
+
manager.stop();
|
|
2022
|
+
resetSandbox();
|
|
2023
|
+
if (agentName) removePid(agentName);
|
|
2024
|
+
process.exit(1);
|
|
2025
|
+
});
|
|
2026
|
+
wsClient.on("reconnect", () => {
|
|
2027
|
+
manager.reconnect();
|
|
2028
|
+
});
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
// src/commands/login.ts
|
|
2033
|
+
import { exec } from "child_process";
|
|
2034
|
+
import * as tty from "tty";
|
|
2035
|
+
var DEFAULT_BASE_URL = "https://agents.hot";
|
|
2036
|
+
var POLL_INTERVAL_MS = 5e3;
|
|
2037
|
+
var SLOW_DOWN_INCREASE_MS = 5e3;
|
|
2038
|
+
function openBrowser(url) {
|
|
2039
|
+
const cmd = process.platform === "darwin" ? `open "${url}"` : process.platform === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`;
|
|
2040
|
+
exec(cmd, (err) => {
|
|
2041
|
+
if (err) {
|
|
2042
|
+
log.debug(`Failed to open browser: ${err.message}`);
|
|
2043
|
+
}
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
function createSpinner(message) {
|
|
2047
|
+
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
2048
|
+
let i = 0;
|
|
2049
|
+
const timer = setInterval(() => {
|
|
2050
|
+
process.stderr.write(`\r${message} ${frames[i++ % frames.length]}`);
|
|
2051
|
+
}, 80);
|
|
2052
|
+
return {
|
|
2053
|
+
stop(finalMessage) {
|
|
2054
|
+
clearInterval(timer);
|
|
2055
|
+
process.stderr.write(`\r${finalMessage}
|
|
2056
|
+
`);
|
|
2057
|
+
}
|
|
2058
|
+
};
|
|
2059
|
+
}
|
|
2060
|
+
function isTTY() {
|
|
2061
|
+
return tty.isatty(process.stdin.fd);
|
|
2062
|
+
}
|
|
2063
|
+
async function pollForToken(baseUrl, deviceCode, expiresIn, interval) {
|
|
2064
|
+
const deadline = Date.now() + expiresIn * 1e3;
|
|
2065
|
+
let pollMs = Math.max(interval * 1e3, POLL_INTERVAL_MS);
|
|
2066
|
+
while (Date.now() < deadline) {
|
|
2067
|
+
await new Promise((resolve2) => setTimeout(resolve2, pollMs));
|
|
2068
|
+
const res = await fetch(`${baseUrl}/api/auth/device/token`, {
|
|
2069
|
+
method: "POST",
|
|
2070
|
+
headers: { "Content-Type": "application/json" },
|
|
2071
|
+
body: JSON.stringify({ device_code: deviceCode })
|
|
2072
|
+
});
|
|
2073
|
+
if (res.ok) {
|
|
2074
|
+
return await res.json();
|
|
2075
|
+
}
|
|
2076
|
+
const data = await res.json();
|
|
2077
|
+
if (data.error === "authorization_pending") {
|
|
2078
|
+
continue;
|
|
2079
|
+
}
|
|
2080
|
+
if (data.error === "slow_down") {
|
|
2081
|
+
pollMs += SLOW_DOWN_INCREASE_MS;
|
|
2082
|
+
continue;
|
|
2083
|
+
}
|
|
2084
|
+
throw new Error(data.error_description || data.error);
|
|
2085
|
+
}
|
|
2086
|
+
throw new Error("Device code expired. Run `agent-mesh login` again.");
|
|
2087
|
+
}
|
|
2088
|
+
function registerLoginCommand(program2) {
|
|
2089
|
+
program2.command("login").description("Authenticate with the Agents.Hot platform").option("--token <token>", "Provide token directly (skip browser flow)").option("--force", "Re-login even if already authenticated").option("--base-url <url>", "Platform base URL", DEFAULT_BASE_URL).action(async (opts) => {
|
|
2090
|
+
const baseUrl = opts.baseUrl ?? DEFAULT_BASE_URL;
|
|
2091
|
+
if (opts.token) {
|
|
2092
|
+
saveToken(opts.token);
|
|
2093
|
+
log.success(`Token saved to ${getConfigPath()}`);
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
if (hasToken() && !opts.force) {
|
|
2097
|
+
const existing = loadToken();
|
|
2098
|
+
log.info(
|
|
2099
|
+
`Already logged in (token: ${existing.slice(0, 6)}...). Continuing will replace the existing token.`
|
|
2100
|
+
);
|
|
2101
|
+
}
|
|
2102
|
+
const interactive = isTTY();
|
|
2103
|
+
log.banner("Agent Mesh Login");
|
|
2104
|
+
let deviceData;
|
|
2105
|
+
try {
|
|
2106
|
+
const res = await fetch(`${baseUrl}/api/auth/device`, {
|
|
2107
|
+
method: "POST",
|
|
2108
|
+
headers: { "Content-Type": "application/json" },
|
|
2109
|
+
body: JSON.stringify({
|
|
2110
|
+
client_info: {
|
|
2111
|
+
device_name: `CLI ${process.platform}`,
|
|
2112
|
+
os: process.platform,
|
|
2113
|
+
version: process.env.npm_package_version || "unknown"
|
|
2114
|
+
}
|
|
2115
|
+
})
|
|
2116
|
+
});
|
|
2117
|
+
if (!res.ok) {
|
|
2118
|
+
throw new Error(`HTTP ${res.status}`);
|
|
2119
|
+
}
|
|
2120
|
+
deviceData = await res.json();
|
|
2121
|
+
} catch (err) {
|
|
2122
|
+
log.error(`Failed to request device code: ${err.message}`);
|
|
2123
|
+
console.log("\nFallback: visit https://agents.hot/settings?tab=developer");
|
|
2124
|
+
console.log("Create a CLI token and run: agent-mesh login --token <token>");
|
|
2125
|
+
process.exit(1);
|
|
2126
|
+
}
|
|
2127
|
+
const url = deviceData.verification_uri_complete;
|
|
2128
|
+
openBrowser(url);
|
|
2129
|
+
console.log(`
|
|
2130
|
+
Open this URL to authorize:
|
|
2131
|
+
${url}
|
|
2132
|
+
`);
|
|
2133
|
+
const spinner = interactive ? createSpinner("Waiting for authorization...") : null;
|
|
2134
|
+
if (!interactive) {
|
|
2135
|
+
console.log("Waiting for authorization (approve in your browser)...");
|
|
2136
|
+
}
|
|
2137
|
+
try {
|
|
2138
|
+
const tokenData = await pollForToken(
|
|
2139
|
+
baseUrl,
|
|
2140
|
+
deviceData.device_code,
|
|
2141
|
+
deviceData.expires_in,
|
|
2142
|
+
deviceData.interval
|
|
2143
|
+
);
|
|
2144
|
+
if (spinner) {
|
|
2145
|
+
spinner.stop(`\u2713 Logged in as ${tokenData.user.email || tokenData.user.name}`);
|
|
2146
|
+
} else {
|
|
2147
|
+
log.success(`Logged in as ${tokenData.user.email || tokenData.user.name}`);
|
|
2148
|
+
}
|
|
2149
|
+
saveToken(tokenData.access_token);
|
|
2150
|
+
log.success(`Token saved to ${getConfigPath()}`);
|
|
2151
|
+
} catch (err) {
|
|
2152
|
+
if (spinner) {
|
|
2153
|
+
spinner.stop(`\u2717 ${err.message}`);
|
|
2154
|
+
} else {
|
|
2155
|
+
log.error(err.message);
|
|
2156
|
+
}
|
|
2157
|
+
process.exit(1);
|
|
2158
|
+
}
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
// src/commands/status.ts
|
|
2163
|
+
function registerStatusCommand(program2) {
|
|
2164
|
+
program2.command("status").description("Check authentication and connection status").action(async () => {
|
|
2165
|
+
log.banner("Agent Mesh Status");
|
|
2166
|
+
const config = loadConfig();
|
|
2167
|
+
const configPath = getConfigPath();
|
|
2168
|
+
console.log(`Config: ${configPath}`);
|
|
2169
|
+
if (!hasToken()) {
|
|
2170
|
+
console.log("Auth: Not logged in");
|
|
2171
|
+
console.log("\nRun `agent-mesh login` to authenticate.");
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
2174
|
+
const token = loadToken();
|
|
2175
|
+
const maskedToken = token.slice(0, 8) + "..." + token.slice(-4);
|
|
2176
|
+
console.log(`Auth: Logged in (token: ${maskedToken})`);
|
|
2177
|
+
if (config.defaultAgentType) {
|
|
2178
|
+
console.log(`Agent: ${config.defaultAgentType}`);
|
|
2179
|
+
}
|
|
2180
|
+
if (config.bridgeUrl) {
|
|
2181
|
+
console.log(`Bridge: ${config.bridgeUrl}`);
|
|
2182
|
+
}
|
|
2183
|
+
if (config.gatewayUrl) {
|
|
2184
|
+
console.log(`Gateway: ${config.gatewayUrl}`);
|
|
2185
|
+
}
|
|
2186
|
+
console.log("\nTo connect an agent, run:");
|
|
2187
|
+
console.log(" agent-mesh connect <type> --agent-id <id>");
|
|
2188
|
+
console.log("\nSupported types: openclaw, claude, codex, gemini");
|
|
2189
|
+
});
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
// src/commands/start.ts
|
|
2193
|
+
function sleep2(ms) {
|
|
2194
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2195
|
+
}
|
|
2196
|
+
function registerStartCommand(program2) {
|
|
2197
|
+
program2.command("start [name]").description("Start agent(s) in the background").option("--all", "Start all registered agents").action(async (name, opts) => {
|
|
2198
|
+
const config = loadConfig();
|
|
2199
|
+
const agents = config.agents;
|
|
2200
|
+
let targets;
|
|
2201
|
+
if (opts.all) {
|
|
2202
|
+
targets = Object.keys(agents);
|
|
2203
|
+
} else if (name) {
|
|
2204
|
+
if (!agents[name]) {
|
|
2205
|
+
log.error(`Agent "${name}" not found. Run 'agent-mesh list' to see registered agents.`);
|
|
2206
|
+
process.exit(1);
|
|
2207
|
+
}
|
|
2208
|
+
targets = [name];
|
|
2209
|
+
} else {
|
|
2210
|
+
log.error("Specify an agent name or use --all. Run 'agent-mesh list' to see agents.");
|
|
2211
|
+
process.exit(1);
|
|
2212
|
+
}
|
|
2213
|
+
if (targets.length === 0) {
|
|
2214
|
+
console.log(`
|
|
2215
|
+
No agents registered. Use ${BOLD}agent-mesh connect --setup <url>${RESET} to add one.
|
|
2216
|
+
`);
|
|
2217
|
+
return;
|
|
2218
|
+
}
|
|
2219
|
+
let started = 0;
|
|
2220
|
+
for (const t of targets) {
|
|
2221
|
+
const entry = agents[t];
|
|
2222
|
+
const pid = readPid(t);
|
|
2223
|
+
if (pid !== null && isProcessAlive(pid)) {
|
|
2224
|
+
console.log(` ${YELLOW}\u2298${RESET} ${BOLD}${t}${RESET} already running (PID: ${pid})`);
|
|
2225
|
+
continue;
|
|
2226
|
+
}
|
|
2227
|
+
const newPid = spawnBackground(t, entry, config.token);
|
|
2228
|
+
await sleep2(500);
|
|
2229
|
+
if (isProcessAlive(newPid)) {
|
|
2230
|
+
console.log(` ${GREEN}\u2713${RESET} ${BOLD}${t}${RESET} started (PID: ${newPid})`);
|
|
2231
|
+
console.log(` Logs: ${GRAY}${getLogPath(t)}${RESET}`);
|
|
2232
|
+
started++;
|
|
2233
|
+
} else {
|
|
2234
|
+
console.log(` ${RESET}\x1B[31m\u2717${RESET} ${BOLD}${t}${RESET} failed to start. Check logs: ${GRAY}${getLogPath(t)}${RESET}`);
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
if (targets.length > 1) {
|
|
2238
|
+
console.log(`
|
|
2239
|
+
${GRAY}Started ${started} of ${targets.length} agents${RESET}
|
|
2240
|
+
`);
|
|
2241
|
+
}
|
|
2242
|
+
});
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
// src/commands/stop.ts
|
|
2246
|
+
function registerStopCommand(program2) {
|
|
2247
|
+
program2.command("stop [name]").description("Stop agent(s)").option("--all", "Stop all running agents").action(async (name, opts) => {
|
|
2248
|
+
const config = loadConfig();
|
|
2249
|
+
const agents = config.agents;
|
|
2250
|
+
let targets;
|
|
2251
|
+
if (opts.all) {
|
|
2252
|
+
targets = Object.keys(agents);
|
|
2253
|
+
} else if (name) {
|
|
2254
|
+
if (!agents[name]) {
|
|
2255
|
+
log.error(`Agent "${name}" not found. Run 'agent-mesh list' to see registered agents.`);
|
|
2256
|
+
process.exit(1);
|
|
2257
|
+
}
|
|
2258
|
+
targets = [name];
|
|
2259
|
+
} else {
|
|
2260
|
+
log.error("Specify an agent name or use --all. Run 'agent-mesh list' to see agents.");
|
|
2261
|
+
process.exit(1);
|
|
2262
|
+
}
|
|
2263
|
+
if (targets.length === 0) {
|
|
2264
|
+
console.log(`
|
|
2265
|
+
No agents registered.
|
|
2266
|
+
`);
|
|
2267
|
+
return;
|
|
2268
|
+
}
|
|
2269
|
+
let stopped = 0;
|
|
2270
|
+
for (const t of targets) {
|
|
2271
|
+
const ok = await stopProcess(t);
|
|
2272
|
+
if (ok) {
|
|
2273
|
+
console.log(` ${GREEN}\u2713${RESET} ${BOLD}${t}${RESET} stopped`);
|
|
2274
|
+
stopped++;
|
|
2275
|
+
} else {
|
|
2276
|
+
console.log(` ${YELLOW}\u2298${RESET} ${BOLD}${t}${RESET} not running`);
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
if (targets.length > 1) {
|
|
2280
|
+
console.log(`
|
|
2281
|
+
${GRAY}Stopped ${stopped} of ${targets.length} agents${RESET}
|
|
2282
|
+
`);
|
|
2283
|
+
}
|
|
2284
|
+
});
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
// src/commands/restart.ts
|
|
2288
|
+
function sleep3(ms) {
|
|
2289
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
2290
|
+
}
|
|
2291
|
+
function registerRestartCommand(program2) {
|
|
2292
|
+
program2.command("restart [name]").description("Restart agent(s)").option("--all", "Restart all registered agents").action(async (name, opts) => {
|
|
2293
|
+
const config = loadConfig();
|
|
2294
|
+
const agents = config.agents;
|
|
2295
|
+
let targets;
|
|
2296
|
+
if (opts.all) {
|
|
2297
|
+
targets = Object.keys(agents);
|
|
2298
|
+
} else if (name) {
|
|
2299
|
+
if (!agents[name]) {
|
|
2300
|
+
log.error(`Agent "${name}" not found. Run 'agent-mesh list' to see registered agents.`);
|
|
2301
|
+
process.exit(1);
|
|
2302
|
+
}
|
|
2303
|
+
targets = [name];
|
|
2304
|
+
} else {
|
|
2305
|
+
log.error("Specify an agent name or use --all. Run 'agent-mesh list' to see agents.");
|
|
2306
|
+
process.exit(1);
|
|
2307
|
+
}
|
|
2308
|
+
if (targets.length === 0) {
|
|
2309
|
+
console.log(`
|
|
2310
|
+
No agents registered.
|
|
2311
|
+
`);
|
|
2312
|
+
return;
|
|
2313
|
+
}
|
|
2314
|
+
let restarted = 0;
|
|
2315
|
+
for (const t of targets) {
|
|
2316
|
+
await stopProcess(t);
|
|
2317
|
+
await sleep3(1e3);
|
|
2318
|
+
const entry = agents[t];
|
|
2319
|
+
const newPid = spawnBackground(t, entry, config.token);
|
|
2320
|
+
await sleep3(500);
|
|
2321
|
+
if (isProcessAlive(newPid)) {
|
|
2322
|
+
console.log(` ${GREEN}\u2713${RESET} ${BOLD}${t}${RESET} restarted (PID: ${newPid})`);
|
|
2323
|
+
console.log(` Logs: ${GRAY}${getLogPath(t)}${RESET}`);
|
|
2324
|
+
restarted++;
|
|
2325
|
+
} else {
|
|
2326
|
+
console.log(` \x1B[31m\u2717${RESET} ${BOLD}${t}${RESET} failed to start. Check logs: ${GRAY}${getLogPath(t)}${RESET}`);
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
if (targets.length > 1) {
|
|
2330
|
+
console.log(`
|
|
2331
|
+
${GRAY}Restarted ${restarted} of ${targets.length} agents${RESET}
|
|
2332
|
+
`);
|
|
2333
|
+
}
|
|
2334
|
+
});
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
// src/commands/logs.ts
|
|
2338
|
+
import { spawn as spawn2 } from "child_process";
|
|
2339
|
+
import { existsSync as existsSync2 } from "fs";
|
|
2340
|
+
function registerLogsCommand(program2) {
|
|
2341
|
+
program2.command("logs <name>").description("View agent logs (follows in real-time)").option("-n, --lines <number>", "Number of lines to show", "50").action((name, opts) => {
|
|
2342
|
+
const entry = getAgent(name);
|
|
2343
|
+
if (!entry) {
|
|
2344
|
+
log.error(`Agent "${name}" not found. Run 'agent-mesh list' to see registered agents.`);
|
|
2345
|
+
process.exit(1);
|
|
2346
|
+
}
|
|
2347
|
+
const logPath = getLogPath(name);
|
|
2348
|
+
if (!existsSync2(logPath)) {
|
|
2349
|
+
log.error(`No log file found for "${name}". Has this agent been started before?`);
|
|
2350
|
+
process.exit(1);
|
|
2351
|
+
}
|
|
2352
|
+
const lines = parseInt(opts.lines, 10) || 50;
|
|
2353
|
+
const label = ` ${name} (${entry.agentType}) `;
|
|
2354
|
+
const totalWidth = 50;
|
|
2355
|
+
const rightPad = Math.max(0, totalWidth - label.length - 3);
|
|
2356
|
+
console.log(`
|
|
2357
|
+
${GRAY}\u2500\u2500\u2500${BOLD}${label}${RESET}${GRAY}${"\u2500".repeat(rightPad)}${RESET}`);
|
|
2358
|
+
const tail = spawn2("tail", ["-f", "-n", String(lines), logPath], {
|
|
2359
|
+
stdio: "inherit"
|
|
2360
|
+
});
|
|
2361
|
+
const cleanup = () => {
|
|
2362
|
+
tail.kill();
|
|
2363
|
+
process.exit(0);
|
|
2364
|
+
};
|
|
2365
|
+
process.on("SIGINT", cleanup);
|
|
2366
|
+
process.on("SIGTERM", cleanup);
|
|
2367
|
+
tail.on("exit", () => process.exit(0));
|
|
2368
|
+
});
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
// src/commands/remove.ts
|
|
2372
|
+
import { createInterface as createInterface2 } from "readline";
|
|
2373
|
+
import { unlinkSync } from "fs";
|
|
2374
|
+
function confirm(prompt) {
|
|
2375
|
+
const rl = createInterface2({ input: process.stdin, output: process.stderr });
|
|
2376
|
+
return new Promise((resolve2) => {
|
|
2377
|
+
rl.question(prompt, (answer) => {
|
|
2378
|
+
rl.close();
|
|
2379
|
+
resolve2(answer.trim().toLowerCase() === "y");
|
|
2380
|
+
});
|
|
2381
|
+
});
|
|
2382
|
+
}
|
|
2383
|
+
function registerRemoveCommand(program2) {
|
|
2384
|
+
program2.command("remove <name>").description("Remove an agent from the registry").option("--force", "Skip confirmation prompt").action(async (name, opts) => {
|
|
2385
|
+
const entry = getAgent(name);
|
|
2386
|
+
if (!entry) {
|
|
2387
|
+
log.error(`Agent "${name}" not found. Run 'agent-mesh list' to see registered agents.`);
|
|
2388
|
+
process.exit(1);
|
|
2389
|
+
}
|
|
2390
|
+
if (!opts.force) {
|
|
2391
|
+
const yes = await confirm(` Remove agent "${name}"? (y/N) `);
|
|
2392
|
+
if (!yes) {
|
|
2393
|
+
console.log(" Cancelled.");
|
|
2394
|
+
return;
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
await stopProcess(name);
|
|
2398
|
+
removeAgent(name);
|
|
2399
|
+
removePid(name);
|
|
2400
|
+
const logPath = getLogPath(name);
|
|
2401
|
+
for (const suffix of ["", ".1", ".2"]) {
|
|
2402
|
+
try {
|
|
2403
|
+
unlinkSync(`${logPath}${suffix}`);
|
|
2404
|
+
} catch {
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
console.log(` ${GREEN}\u2713${RESET} ${BOLD}${name}${RESET} removed`);
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
// src/commands/open.ts
|
|
2412
|
+
import { spawn as spawn3 } from "child_process";
|
|
2413
|
+
function registerOpenCommand(program2) {
|
|
2414
|
+
program2.command("open <name>").description("Open agent page in browser").action((name) => {
|
|
2415
|
+
const entry = getAgent(name);
|
|
2416
|
+
if (!entry) {
|
|
2417
|
+
log.error(`Agent "${name}" not found. Run 'agent-mesh list' to see registered agents.`);
|
|
2418
|
+
process.exit(1);
|
|
2419
|
+
}
|
|
2420
|
+
const url = `https://agents.hot/agents/${entry.agentId}`;
|
|
2421
|
+
console.log(` Opening ${GRAY}${url}${RESET}...`);
|
|
2422
|
+
const cmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
2423
|
+
const child = spawn3(cmd, [url], { detached: true, stdio: "ignore" });
|
|
2424
|
+
child.unref();
|
|
2425
|
+
});
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
// src/commands/install.ts
|
|
2429
|
+
import { writeFileSync, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
2430
|
+
import { join as join5 } from "path";
|
|
2431
|
+
import { homedir as homedir3 } from "os";
|
|
2432
|
+
import { execSync as execSync2 } from "child_process";
|
|
2433
|
+
var LABEL = "com.agents-hot.agent-mesh";
|
|
2434
|
+
var PLIST_DIR = join5(homedir3(), "Library", "LaunchAgents");
|
|
2435
|
+
var PLIST_PATH = join5(PLIST_DIR, `${LABEL}.plist`);
|
|
2436
|
+
var LOG_PATH = join5(homedir3(), ".agent-mesh", "logs", "launchd.log");
|
|
2437
|
+
function detectPaths() {
|
|
2438
|
+
return {
|
|
2439
|
+
node: process.execPath,
|
|
2440
|
+
script: process.argv[1]
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
function generatePlist(nodePath, scriptPath) {
|
|
2444
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
2445
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2446
|
+
<plist version="1.0">
|
|
2447
|
+
<dict>
|
|
2448
|
+
<key>Label</key>
|
|
2449
|
+
<string>${LABEL}</string>
|
|
2450
|
+
|
|
2451
|
+
<key>ProgramArguments</key>
|
|
2452
|
+
<array>
|
|
2453
|
+
<string>${escapeXml(nodePath)}</string>
|
|
2454
|
+
<string>${escapeXml(scriptPath)}</string>
|
|
2455
|
+
<string>start</string>
|
|
2456
|
+
<string>--all</string>
|
|
2457
|
+
</array>
|
|
2458
|
+
|
|
2459
|
+
<key>RunAtLoad</key>
|
|
2460
|
+
<true/>
|
|
2461
|
+
|
|
2462
|
+
<key>StandardOutPath</key>
|
|
2463
|
+
<string>${escapeXml(LOG_PATH)}</string>
|
|
2464
|
+
|
|
2465
|
+
<key>StandardErrorPath</key>
|
|
2466
|
+
<string>${escapeXml(LOG_PATH)}</string>
|
|
2467
|
+
|
|
2468
|
+
<key>WorkingDirectory</key>
|
|
2469
|
+
<string>${escapeXml(homedir3())}</string>
|
|
2470
|
+
</dict>
|
|
2471
|
+
</plist>
|
|
2472
|
+
`;
|
|
2473
|
+
}
|
|
2474
|
+
function escapeXml(s) {
|
|
2475
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2476
|
+
}
|
|
2477
|
+
function registerInstallCommand(program2) {
|
|
2478
|
+
program2.command("install").description("Install macOS LaunchAgent to auto-start agents on login").option("--force", "Overwrite existing LaunchAgent").action(async (opts) => {
|
|
2479
|
+
if (process.platform !== "darwin") {
|
|
2480
|
+
log.error("LaunchAgent is macOS only. On Linux, use systemd user service instead.");
|
|
2481
|
+
process.exit(1);
|
|
2482
|
+
}
|
|
2483
|
+
if (existsSync3(PLIST_PATH) && !opts.force) {
|
|
2484
|
+
console.log(`
|
|
2485
|
+
${YELLOW}\u2298${RESET} LaunchAgent already installed at:`);
|
|
2486
|
+
console.log(` ${GRAY}${PLIST_PATH}${RESET}`);
|
|
2487
|
+
console.log(`
|
|
2488
|
+
Use ${BOLD}--force${RESET} to overwrite.
|
|
2489
|
+
`);
|
|
2490
|
+
return;
|
|
2491
|
+
}
|
|
2492
|
+
const { node, script } = detectPaths();
|
|
2493
|
+
if (!existsSync3(PLIST_DIR)) {
|
|
2494
|
+
mkdirSync2(PLIST_DIR, { recursive: true });
|
|
2495
|
+
}
|
|
2496
|
+
if (existsSync3(PLIST_PATH)) {
|
|
2497
|
+
try {
|
|
2498
|
+
execSync2(`launchctl bootout gui/$(id -u) "${PLIST_PATH}" 2>/dev/null`, { stdio: "ignore" });
|
|
2499
|
+
} catch {
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
const plist = generatePlist(node, script);
|
|
2503
|
+
writeFileSync(PLIST_PATH, plist, { encoding: "utf-8" });
|
|
2504
|
+
try {
|
|
2505
|
+
execSync2(`launchctl bootstrap gui/$(id -u) "${PLIST_PATH}"`, { stdio: "pipe" });
|
|
2506
|
+
} catch {
|
|
2507
|
+
try {
|
|
2508
|
+
execSync2(`launchctl load "${PLIST_PATH}"`, { stdio: "pipe" });
|
|
2509
|
+
} catch (err) {
|
|
2510
|
+
log.error(`Failed to load LaunchAgent: ${err}`);
|
|
2511
|
+
log.info(`Plist written to ${PLIST_PATH} \u2014 you can load it manually.`);
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
console.log(`
|
|
2516
|
+
${GREEN}\u2713${RESET} LaunchAgent installed`);
|
|
2517
|
+
console.log(` Plist: ${GRAY}${PLIST_PATH}${RESET}`);
|
|
2518
|
+
console.log(` Log: ${GRAY}${LOG_PATH}${RESET}`);
|
|
2519
|
+
console.log(` Node: ${GRAY}${node}${RESET}`);
|
|
2520
|
+
console.log(` CLI: ${GRAY}${script}${RESET}`);
|
|
2521
|
+
console.log(`
|
|
2522
|
+
All registered agents will auto-start on login.`);
|
|
2523
|
+
console.log(` Use ${BOLD}agent-mesh uninstall${RESET} to remove.
|
|
2524
|
+
`);
|
|
2525
|
+
});
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
// src/commands/uninstall.ts
|
|
2529
|
+
import { existsSync as existsSync4, unlinkSync as unlinkSync2 } from "fs";
|
|
2530
|
+
import { join as join6 } from "path";
|
|
2531
|
+
import { homedir as homedir4 } from "os";
|
|
2532
|
+
import { execSync as execSync3 } from "child_process";
|
|
2533
|
+
var LABEL2 = "com.agents-hot.agent-mesh";
|
|
2534
|
+
var PLIST_PATH2 = join6(homedir4(), "Library", "LaunchAgents", `${LABEL2}.plist`);
|
|
2535
|
+
function registerUninstallCommand(program2) {
|
|
2536
|
+
program2.command("uninstall").description("Remove macOS LaunchAgent (agents will no longer auto-start)").action(async () => {
|
|
2537
|
+
if (process.platform !== "darwin") {
|
|
2538
|
+
log.error("LaunchAgent is macOS only.");
|
|
2539
|
+
process.exit(1);
|
|
2540
|
+
}
|
|
2541
|
+
if (!existsSync4(PLIST_PATH2)) {
|
|
2542
|
+
console.log(`
|
|
2543
|
+
${YELLOW}\u2298${RESET} No LaunchAgent found at ${GRAY}${PLIST_PATH2}${RESET}
|
|
2544
|
+
`);
|
|
2545
|
+
return;
|
|
2546
|
+
}
|
|
2547
|
+
try {
|
|
2548
|
+
execSync3(`launchctl bootout gui/$(id -u) "${PLIST_PATH2}" 2>/dev/null`, { stdio: "ignore" });
|
|
2549
|
+
} catch {
|
|
2550
|
+
try {
|
|
2551
|
+
execSync3(`launchctl unload "${PLIST_PATH2}" 2>/dev/null`, { stdio: "ignore" });
|
|
2552
|
+
} catch {
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
try {
|
|
2556
|
+
unlinkSync2(PLIST_PATH2);
|
|
2557
|
+
} catch (err) {
|
|
2558
|
+
log.error(`Failed to remove plist: ${err}`);
|
|
2559
|
+
return;
|
|
2560
|
+
}
|
|
2561
|
+
console.log(`
|
|
2562
|
+
${GREEN}\u2713${RESET} LaunchAgent removed`);
|
|
2563
|
+
console.log(` Agents will no longer auto-start on login.`);
|
|
2564
|
+
console.log(` Use ${BOLD}agent-mesh install${RESET} to re-install.
|
|
2565
|
+
`);
|
|
2566
|
+
});
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
// src/commands/agents.ts
|
|
2570
|
+
import { createInterface as createInterface3 } from "readline";
|
|
2571
|
+
|
|
2572
|
+
// src/platform/api-client.ts
|
|
2573
|
+
var DEFAULT_BASE_URL2 = "https://agents.hot";
|
|
2574
|
+
var PlatformApiError = class extends Error {
|
|
2575
|
+
constructor(statusCode, errorCode, message) {
|
|
2576
|
+
super(message);
|
|
2577
|
+
this.statusCode = statusCode;
|
|
2578
|
+
this.errorCode = errorCode;
|
|
2579
|
+
this.name = "PlatformApiError";
|
|
2580
|
+
}
|
|
2581
|
+
};
|
|
2582
|
+
var ERROR_HINTS = {
|
|
2583
|
+
unauthorized: "Not authenticated. Run `agent-mesh login` first.",
|
|
2584
|
+
forbidden: "You don't own this agent.",
|
|
2585
|
+
not_found: "Agent not found.",
|
|
2586
|
+
agent_offline: "Agent must be online for first publish. Run `agent-mesh connect` first.",
|
|
2587
|
+
email_required: "Email required. Visit https://agents.hot/settings to add one.",
|
|
2588
|
+
github_required: "GitHub account required. Visit https://agents.hot/settings to link one.",
|
|
2589
|
+
validation_error: "Invalid input. Check your skill.json or command flags.",
|
|
2590
|
+
permission_denied: "You don't have permission to modify this skill.",
|
|
2591
|
+
file_too_large: "Package file exceeds the 50MB limit."
|
|
2592
|
+
};
|
|
2593
|
+
var PlatformClient = class {
|
|
2594
|
+
token;
|
|
2595
|
+
baseUrl;
|
|
2596
|
+
constructor(token, baseUrl) {
|
|
2597
|
+
const resolved = token ?? loadToken();
|
|
2598
|
+
if (!resolved) {
|
|
2599
|
+
throw new PlatformApiError(401, "unauthorized", ERROR_HINTS.unauthorized);
|
|
2600
|
+
}
|
|
2601
|
+
this.token = resolved;
|
|
2602
|
+
this.baseUrl = baseUrl ?? DEFAULT_BASE_URL2;
|
|
2603
|
+
}
|
|
2604
|
+
async get(path) {
|
|
2605
|
+
return this.request("GET", path);
|
|
2606
|
+
}
|
|
2607
|
+
async post(path, body) {
|
|
2608
|
+
return this.request("POST", path, body);
|
|
2609
|
+
}
|
|
2610
|
+
async put(path, body) {
|
|
2611
|
+
return this.request("PUT", path, body);
|
|
2612
|
+
}
|
|
2613
|
+
async patch(path, body) {
|
|
2614
|
+
return this.request("PATCH", path, body);
|
|
2615
|
+
}
|
|
2616
|
+
async del(path, body) {
|
|
2617
|
+
return this.request("DELETE", path, body);
|
|
2618
|
+
}
|
|
2619
|
+
async postFormData(path, formData) {
|
|
2620
|
+
const url = `${this.baseUrl}${path}`;
|
|
2621
|
+
let res;
|
|
2622
|
+
try {
|
|
2623
|
+
res = await fetch(url, {
|
|
2624
|
+
method: "POST",
|
|
2625
|
+
headers: { Authorization: `Bearer ${this.token}` },
|
|
2626
|
+
body: formData
|
|
2627
|
+
});
|
|
2628
|
+
} catch (err) {
|
|
2629
|
+
throw new PlatformApiError(0, "network_error", `Network error: ${err.message}`);
|
|
2630
|
+
}
|
|
2631
|
+
return this.handleResponse(res);
|
|
2632
|
+
}
|
|
2633
|
+
async request(method, path, body) {
|
|
2634
|
+
const url = `${this.baseUrl}${path}`;
|
|
2635
|
+
const headers = {
|
|
2636
|
+
Authorization: `Bearer ${this.token}`,
|
|
2637
|
+
"Content-Type": "application/json"
|
|
2638
|
+
};
|
|
2639
|
+
const init = { method, headers };
|
|
2640
|
+
if (body !== void 0) {
|
|
2641
|
+
init.body = JSON.stringify(body);
|
|
2642
|
+
}
|
|
2643
|
+
let res;
|
|
2644
|
+
try {
|
|
2645
|
+
res = await fetch(url, init);
|
|
2646
|
+
} catch (err) {
|
|
2647
|
+
throw new PlatformApiError(0, "network_error", `Network error: ${err.message}`);
|
|
2648
|
+
}
|
|
2649
|
+
return this.handleResponse(res);
|
|
2650
|
+
}
|
|
2651
|
+
async handleResponse(res) {
|
|
2652
|
+
if (!res.ok) {
|
|
2653
|
+
let errorCode = "unknown";
|
|
2654
|
+
let message = `HTTP ${res.status}`;
|
|
2655
|
+
try {
|
|
2656
|
+
const data = await res.json();
|
|
2657
|
+
errorCode = data.error ?? errorCode;
|
|
2658
|
+
message = data.error_description ?? data.message ?? message;
|
|
2659
|
+
} catch {
|
|
2660
|
+
}
|
|
2661
|
+
const hint = ERROR_HINTS[errorCode];
|
|
2662
|
+
throw new PlatformApiError(res.status, errorCode, hint ?? message);
|
|
2663
|
+
}
|
|
2664
|
+
return res.json();
|
|
2665
|
+
}
|
|
2666
|
+
};
|
|
2667
|
+
function createClient(baseUrl) {
|
|
2668
|
+
return new PlatformClient(void 0, baseUrl);
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
// src/platform/resolve-agent.ts
|
|
2672
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2673
|
+
async function resolveAgentId(input, client) {
|
|
2674
|
+
if (UUID_RE.test(input)) {
|
|
2675
|
+
return { id: input, name: input };
|
|
2676
|
+
}
|
|
2677
|
+
const local = listAgents();
|
|
2678
|
+
if (input in local) {
|
|
2679
|
+
return { id: local[input].agentId, name: input };
|
|
2680
|
+
}
|
|
2681
|
+
const byId = findAgentByAgentId(input);
|
|
2682
|
+
if (byId) {
|
|
2683
|
+
return { id: byId.entry.agentId, name: byId.name };
|
|
2684
|
+
}
|
|
2685
|
+
const data = await client.get("/api/developer/agents");
|
|
2686
|
+
const lower = input.toLowerCase();
|
|
2687
|
+
const match = data.agents.find((a) => a.name.toLowerCase() === lower);
|
|
2688
|
+
if (match) {
|
|
2689
|
+
return { id: match.id, name: match.name };
|
|
2690
|
+
}
|
|
2691
|
+
throw new Error(`Agent not found: "${input}". Use a UUID, local alias, or exact agent name.`);
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
// src/commands/agents.ts
|
|
2695
|
+
function readLine(prompt) {
|
|
2696
|
+
const rl = createInterface3({ input: process.stdin, output: process.stderr });
|
|
2697
|
+
return new Promise((resolve2) => {
|
|
2698
|
+
rl.question(prompt, (answer) => {
|
|
2699
|
+
rl.close();
|
|
2700
|
+
resolve2(answer.trim());
|
|
2701
|
+
});
|
|
2702
|
+
});
|
|
2703
|
+
}
|
|
2704
|
+
function formatStatus(online) {
|
|
2705
|
+
return online ? `${GREEN}\u25CF online${RESET}` : `${GRAY}\u25CB offline${RESET}`;
|
|
2706
|
+
}
|
|
2707
|
+
function formatPublished(published) {
|
|
2708
|
+
return published ? `${GREEN}yes${RESET}` : `${GRAY}no${RESET}`;
|
|
2709
|
+
}
|
|
2710
|
+
function handleError(err) {
|
|
2711
|
+
if (err instanceof PlatformApiError) {
|
|
2712
|
+
log.error(err.message);
|
|
2713
|
+
} else {
|
|
2714
|
+
log.error(err.message);
|
|
2715
|
+
}
|
|
2716
|
+
process.exit(1);
|
|
2717
|
+
}
|
|
2718
|
+
function registerAgentsCommand(program2) {
|
|
2719
|
+
const agents = program2.command("agents").description("Manage agents on the Agents.Hot platform");
|
|
2720
|
+
agents.command("list").alias("ls").description("List your agents").option("--json", "Output raw JSON").action(async (opts) => {
|
|
2721
|
+
try {
|
|
2722
|
+
const client = createClient();
|
|
2723
|
+
const data = await client.get("/api/developer/agents");
|
|
2724
|
+
if (opts.json) {
|
|
2725
|
+
console.log(JSON.stringify(data.agents, null, 2));
|
|
2726
|
+
return;
|
|
2727
|
+
}
|
|
2728
|
+
if (data.agents.length === 0) {
|
|
2729
|
+
log.info("No agents found. Create one with: agent-mesh agents create");
|
|
2730
|
+
return;
|
|
2731
|
+
}
|
|
2732
|
+
const table = renderTable(
|
|
2733
|
+
[
|
|
2734
|
+
{ key: "name", label: "NAME", width: 24 },
|
|
2735
|
+
{ key: "type", label: "TYPE", width: 12 },
|
|
2736
|
+
{ key: "status", label: "STATUS", width: 14 },
|
|
2737
|
+
{ key: "published", label: "PUBLISHED", width: 12 },
|
|
2738
|
+
{ key: "caps", label: "CAPABILITIES", width: 14 }
|
|
2739
|
+
],
|
|
2740
|
+
data.agents.map((a) => ({
|
|
2741
|
+
name: a.name,
|
|
2742
|
+
type: a.agent_type,
|
|
2743
|
+
status: formatStatus(a.is_online),
|
|
2744
|
+
published: formatPublished(a.is_published),
|
|
2745
|
+
caps: (a.capabilities?.length || 0).toString()
|
|
2746
|
+
}))
|
|
2747
|
+
);
|
|
2748
|
+
console.log(table);
|
|
2749
|
+
} catch (err) {
|
|
2750
|
+
handleError(err);
|
|
2751
|
+
}
|
|
2752
|
+
});
|
|
2753
|
+
agents.command("create").description("Create a new agent").option("--name <name>", "Agent name").option("--type <type>", "Agent type (openclaw | claude)", "openclaw").option("--description <desc>", "Agent description").action(async (opts) => {
|
|
2754
|
+
try {
|
|
2755
|
+
let { name, description } = opts;
|
|
2756
|
+
const agentType = opts.type;
|
|
2757
|
+
if (!name && process.stdin.isTTY) {
|
|
2758
|
+
log.banner("Create Agent");
|
|
2759
|
+
name = await readLine("Agent name: ");
|
|
2760
|
+
if (!name) {
|
|
2761
|
+
log.error("Name is required");
|
|
2762
|
+
process.exit(1);
|
|
2763
|
+
}
|
|
2764
|
+
if (!description) {
|
|
2765
|
+
description = await readLine("Description (optional): ");
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
if (!name) {
|
|
2769
|
+
log.error("--name is required. Use interactive mode (TTY) or provide --name.");
|
|
2770
|
+
process.exit(1);
|
|
2771
|
+
}
|
|
2772
|
+
const client = createClient();
|
|
2773
|
+
const result = await client.post("/api/developer/agents", {
|
|
2774
|
+
name,
|
|
2775
|
+
description: description || void 0,
|
|
2776
|
+
agent_type: agentType
|
|
2777
|
+
});
|
|
2778
|
+
const detail = await client.get(`/api/developer/agents/${result.agent.id}`);
|
|
2779
|
+
log.success(`Agent created: ${BOLD}${detail.name}${RESET} (${detail.id})`);
|
|
2780
|
+
console.log("");
|
|
2781
|
+
console.log(" Next: connect your agent");
|
|
2782
|
+
console.log(` ${GRAY}agent-mesh connect --agent-id ${detail.id}${RESET}`);
|
|
2783
|
+
} catch (err) {
|
|
2784
|
+
handleError(err);
|
|
2785
|
+
}
|
|
2786
|
+
});
|
|
2787
|
+
agents.command("show <id-or-name>").description("Show agent details").option("--json", "Output raw JSON").action(async (input, opts) => {
|
|
2788
|
+
try {
|
|
2789
|
+
const client = createClient();
|
|
2790
|
+
const { id } = await resolveAgentId(input, client);
|
|
2791
|
+
const agent = await client.get(`/api/developer/agents/${id}`);
|
|
2792
|
+
if (opts.json) {
|
|
2793
|
+
console.log(JSON.stringify(agent, null, 2));
|
|
2794
|
+
return;
|
|
2795
|
+
}
|
|
2796
|
+
console.log("");
|
|
2797
|
+
console.log(` ${BOLD}${agent.name}${RESET}`);
|
|
2798
|
+
console.log(` ${GRAY}ID${RESET} ${agent.id}`);
|
|
2799
|
+
console.log(` ${GRAY}Type${RESET} ${agent.agent_type}`);
|
|
2800
|
+
console.log(` ${GRAY}Status${RESET} ${formatStatus(agent.is_online)}`);
|
|
2801
|
+
console.log(` ${GRAY}Published${RESET} ${formatPublished(agent.is_published)}`);
|
|
2802
|
+
if (agent.capabilities?.length) {
|
|
2803
|
+
console.log(` ${GRAY}Capabilities${RESET} ${agent.capabilities.join(", ")}`);
|
|
2804
|
+
}
|
|
2805
|
+
if (agent.rate_limits && Object.keys(agent.rate_limits).length > 0) {
|
|
2806
|
+
console.log(` ${GRAY}Rate Limits${RESET} ${JSON.stringify(agent.rate_limits)}`);
|
|
2807
|
+
}
|
|
2808
|
+
console.log(` ${GRAY}Created${RESET} ${agent.created_at}`);
|
|
2809
|
+
if (agent.description) {
|
|
2810
|
+
console.log("");
|
|
2811
|
+
console.log(` ${agent.description}`);
|
|
2812
|
+
}
|
|
2813
|
+
console.log("");
|
|
2814
|
+
} catch (err) {
|
|
2815
|
+
handleError(err);
|
|
2816
|
+
}
|
|
2817
|
+
});
|
|
2818
|
+
agents.command("update <id-or-name>").description("Update an agent").option("--name <name>", "New name").option("--type <type>", "Agent type (openclaw | claude)").option("--description <desc>", "Agent description").action(async (input, opts) => {
|
|
2819
|
+
try {
|
|
2820
|
+
const updates = {};
|
|
2821
|
+
if (opts.name !== void 0) updates.name = opts.name;
|
|
2822
|
+
if (opts.type !== void 0) updates.agent_type = opts.type;
|
|
2823
|
+
if (opts.description !== void 0) updates.description = opts.description;
|
|
2824
|
+
if (Object.keys(updates).length === 0) {
|
|
2825
|
+
log.error("No fields to update. Use --name, --type, --description.");
|
|
2826
|
+
process.exit(1);
|
|
2827
|
+
}
|
|
2828
|
+
const client = createClient();
|
|
2829
|
+
const { id, name } = await resolveAgentId(input, client);
|
|
2830
|
+
const result = await client.put(`/api/developer/agents/${id}`, updates);
|
|
2831
|
+
log.success(`Agent updated: ${BOLD}${result.agent.name}${RESET}`);
|
|
2832
|
+
} catch (err) {
|
|
2833
|
+
handleError(err);
|
|
2834
|
+
}
|
|
2835
|
+
});
|
|
2836
|
+
agents.command("publish <id-or-name>").description("Publish agent to marketplace").action(async (input) => {
|
|
2837
|
+
try {
|
|
2838
|
+
const client = createClient();
|
|
2839
|
+
const { id, name } = await resolveAgentId(input, client);
|
|
2840
|
+
await client.put(`/api/developer/agents/${id}`, { is_published: true });
|
|
2841
|
+
log.success(`Agent published: ${BOLD}${name}${RESET}`);
|
|
2842
|
+
console.log(` View at: ${GRAY}https://agents.hot${RESET}`);
|
|
2843
|
+
} catch (err) {
|
|
2844
|
+
handleError(err);
|
|
2845
|
+
}
|
|
2846
|
+
});
|
|
2847
|
+
agents.command("unpublish <id-or-name>").description("Unpublish agent from marketplace").action(async (input) => {
|
|
2848
|
+
try {
|
|
2849
|
+
const client = createClient();
|
|
2850
|
+
const { id, name } = await resolveAgentId(input, client);
|
|
2851
|
+
await client.put(`/api/developer/agents/${id}`, { is_published: false });
|
|
2852
|
+
log.success(`Agent unpublished: ${BOLD}${name}${RESET}`);
|
|
2853
|
+
} catch (err) {
|
|
2854
|
+
handleError(err);
|
|
2855
|
+
}
|
|
2856
|
+
});
|
|
2857
|
+
agents.command("delete <id-or-name>").description("Delete an agent (soft delete)").action(async (input) => {
|
|
2858
|
+
try {
|
|
2859
|
+
const client = createClient();
|
|
2860
|
+
const { id, name } = await resolveAgentId(input, client);
|
|
2861
|
+
if (process.stdin.isTTY) {
|
|
2862
|
+
const answer = await readLine(`Delete agent "${name}"? (y/N): `);
|
|
2863
|
+
if (answer.toLowerCase() !== "y") {
|
|
2864
|
+
log.info("Cancelled.");
|
|
2865
|
+
return;
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
await client.del(`/api/developer/agents/${id}`);
|
|
2869
|
+
log.success(`Agent deleted: ${BOLD}${name}${RESET}`);
|
|
2870
|
+
} catch (err) {
|
|
2871
|
+
handleError(err);
|
|
2872
|
+
}
|
|
2873
|
+
});
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
// src/commands/chat.ts
|
|
2877
|
+
import { createInterface as createInterface4 } from "readline";
|
|
2878
|
+
|
|
2879
|
+
// src/utils/sse-parser.ts
|
|
2880
|
+
function parseSseChunk(raw, carry) {
|
|
2881
|
+
const merged = carry + raw;
|
|
2882
|
+
const blocks = merged.split(/\r?\n\r?\n/);
|
|
2883
|
+
const nextCarry = blocks.pop() || "";
|
|
2884
|
+
const events = [];
|
|
2885
|
+
for (const block of blocks) {
|
|
2886
|
+
let data = "";
|
|
2887
|
+
for (const line of block.split(/\r?\n/)) {
|
|
2888
|
+
if (line.startsWith("data:")) {
|
|
2889
|
+
data += line.slice(5).trimStart() + "\n";
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
const trimmed = data.trim();
|
|
2893
|
+
if (trimmed) events.push(trimmed);
|
|
2894
|
+
}
|
|
2895
|
+
return { events, carry: nextCarry };
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
// src/commands/chat.ts
|
|
2899
|
+
var DEFAULT_BASE_URL3 = "https://agents.hot";
|
|
2900
|
+
async function streamChat(opts) {
|
|
2901
|
+
const res = await fetch(`${opts.baseUrl}/api/agents/${opts.agentId}/chat`, {
|
|
2902
|
+
method: "POST",
|
|
2903
|
+
headers: {
|
|
2904
|
+
Authorization: `Bearer ${opts.token}`,
|
|
2905
|
+
"Content-Type": "application/json"
|
|
2906
|
+
},
|
|
2907
|
+
body: JSON.stringify({ message: opts.message }),
|
|
2908
|
+
signal: opts.signal
|
|
2909
|
+
});
|
|
2910
|
+
if (!res.ok) {
|
|
2911
|
+
let msg = `HTTP ${res.status}`;
|
|
2912
|
+
try {
|
|
2913
|
+
const body = await res.json();
|
|
2914
|
+
msg = body.message || body.error || msg;
|
|
2915
|
+
} catch {
|
|
2916
|
+
}
|
|
2917
|
+
throw new Error(msg);
|
|
2918
|
+
}
|
|
2919
|
+
if (!res.body) throw new Error("Empty response body");
|
|
2920
|
+
const reader = res.body.getReader();
|
|
2921
|
+
const decoder = new TextDecoder();
|
|
2922
|
+
let buffer = "";
|
|
2923
|
+
let inThinking = false;
|
|
2924
|
+
while (true) {
|
|
2925
|
+
const { done, value } = await reader.read();
|
|
2926
|
+
if (done) break;
|
|
2927
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
2928
|
+
const parsed = parseSseChunk(chunk, buffer);
|
|
2929
|
+
buffer = parsed.carry;
|
|
2930
|
+
for (const data of parsed.events) {
|
|
2931
|
+
if (data === "[DONE]") continue;
|
|
2932
|
+
try {
|
|
2933
|
+
const event = JSON.parse(data);
|
|
2934
|
+
handleSseEvent(event, opts.showThinking ?? true, { inThinking });
|
|
2935
|
+
if (event.type === "reasoning-start") inThinking = true;
|
|
2936
|
+
if (event.type === "reasoning-end") inThinking = false;
|
|
2937
|
+
} catch {
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
if (buffer.trim()) {
|
|
2942
|
+
const parsed = parseSseChunk("\n\n", buffer);
|
|
2943
|
+
for (const data of parsed.events) {
|
|
2944
|
+
if (data === "[DONE]") continue;
|
|
2945
|
+
try {
|
|
2946
|
+
const event = JSON.parse(data);
|
|
2947
|
+
handleSseEvent(event, opts.showThinking ?? true, { inThinking });
|
|
2948
|
+
} catch {
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
process.stdout.write("\n");
|
|
2953
|
+
}
|
|
2954
|
+
function handleSseEvent(event, showThinking, state) {
|
|
2955
|
+
switch (event.type) {
|
|
2956
|
+
case "text-delta":
|
|
2957
|
+
process.stdout.write(String(event.delta ?? ""));
|
|
2958
|
+
break;
|
|
2959
|
+
case "reasoning-delta":
|
|
2960
|
+
if (showThinking) {
|
|
2961
|
+
process.stdout.write(`${GRAY}${String(event.delta ?? "")}${RESET}`);
|
|
2962
|
+
}
|
|
2963
|
+
break;
|
|
2964
|
+
case "reasoning-start":
|
|
2965
|
+
if (showThinking) {
|
|
2966
|
+
process.stdout.write(`${GRAY}[thinking] `);
|
|
2967
|
+
}
|
|
2968
|
+
break;
|
|
2969
|
+
case "reasoning-end":
|
|
2970
|
+
if (showThinking && state.inThinking) {
|
|
2971
|
+
process.stdout.write(`${RESET}
|
|
2972
|
+
`);
|
|
2973
|
+
}
|
|
2974
|
+
break;
|
|
2975
|
+
case "tool-input-start":
|
|
2976
|
+
process.stdout.write(`
|
|
2977
|
+
${YELLOW}[tool: ${event.toolName}]${RESET} `);
|
|
2978
|
+
break;
|
|
2979
|
+
case "tool-output-available": {
|
|
2980
|
+
const output = String(event.output ?? "");
|
|
2981
|
+
const preview = output.length > 200 ? output.slice(0, 200) + "..." : output;
|
|
2982
|
+
process.stdout.write(`${GRAY}${preview}${RESET}
|
|
2983
|
+
`);
|
|
2984
|
+
break;
|
|
2985
|
+
}
|
|
2986
|
+
case "source-url":
|
|
2987
|
+
process.stdout.write(`${GRAY}[file: ${event.title} \u2192 ${event.url}]${RESET}
|
|
2988
|
+
`);
|
|
2989
|
+
break;
|
|
2990
|
+
case "error":
|
|
2991
|
+
process.stderr.write(`
|
|
2992
|
+
${"\x1B[31m"}Error: ${event.errorText}${RESET}
|
|
2993
|
+
`);
|
|
2994
|
+
break;
|
|
2995
|
+
// Ignored: text-start, text-end, start, start-step, finish-step, finish
|
|
2996
|
+
default:
|
|
2997
|
+
break;
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
function registerChatCommand(program2) {
|
|
3001
|
+
program2.command("chat <agent> [message]").description("Chat with an agent through the platform (for debugging)").option("--no-thinking", "Hide thinking/reasoning output").option("--base-url <url>", "Platform base URL", DEFAULT_BASE_URL3).action(async (agentInput, inlineMessage, opts) => {
|
|
3002
|
+
const token = loadToken();
|
|
3003
|
+
if (!token) {
|
|
3004
|
+
log.error("Not authenticated. Run `agent-mesh login` first.");
|
|
3005
|
+
process.exit(1);
|
|
3006
|
+
}
|
|
3007
|
+
let agentId;
|
|
3008
|
+
let agentName;
|
|
3009
|
+
try {
|
|
3010
|
+
const client = createClient(opts.baseUrl);
|
|
3011
|
+
const resolved = await resolveAgentId(agentInput, client);
|
|
3012
|
+
agentId = resolved.id;
|
|
3013
|
+
agentName = resolved.name;
|
|
3014
|
+
} catch (err) {
|
|
3015
|
+
log.error(err.message);
|
|
3016
|
+
process.exit(1);
|
|
3017
|
+
}
|
|
3018
|
+
if (inlineMessage) {
|
|
3019
|
+
log.info(`Chatting with ${BOLD}${agentName}${RESET}`);
|
|
3020
|
+
try {
|
|
3021
|
+
await streamChat({
|
|
3022
|
+
agentId,
|
|
3023
|
+
message: inlineMessage,
|
|
3024
|
+
token,
|
|
3025
|
+
baseUrl: opts.baseUrl,
|
|
3026
|
+
showThinking: opts.thinking
|
|
3027
|
+
});
|
|
3028
|
+
} catch (err) {
|
|
3029
|
+
log.error(err.message);
|
|
3030
|
+
process.exit(1);
|
|
3031
|
+
}
|
|
3032
|
+
return;
|
|
3033
|
+
}
|
|
3034
|
+
if (!process.stdin.isTTY) {
|
|
3035
|
+
log.error("Interactive mode requires a TTY. Provide a message argument for non-interactive use.");
|
|
3036
|
+
process.exit(1);
|
|
3037
|
+
}
|
|
3038
|
+
log.banner(`Chat with ${agentName}`);
|
|
3039
|
+
console.log(`${GRAY}Type your message and press Enter. Use /quit or Ctrl+C to exit.${RESET}
|
|
3040
|
+
`);
|
|
3041
|
+
const rl = createInterface4({
|
|
3042
|
+
input: process.stdin,
|
|
3043
|
+
output: process.stdout,
|
|
3044
|
+
prompt: `${GREEN}> ${RESET}`
|
|
3045
|
+
});
|
|
3046
|
+
const abortController = new AbortController();
|
|
3047
|
+
rl.on("close", () => {
|
|
3048
|
+
abortController.abort();
|
|
3049
|
+
console.log("");
|
|
3050
|
+
process.exit(0);
|
|
3051
|
+
});
|
|
3052
|
+
rl.prompt();
|
|
3053
|
+
rl.on("line", async (line) => {
|
|
3054
|
+
const trimmed = line.trim();
|
|
3055
|
+
if (!trimmed) {
|
|
3056
|
+
rl.prompt();
|
|
3057
|
+
return;
|
|
3058
|
+
}
|
|
3059
|
+
if (trimmed === "/quit" || trimmed === "/exit" || trimmed === "/q") {
|
|
3060
|
+
rl.close();
|
|
3061
|
+
return;
|
|
3062
|
+
}
|
|
3063
|
+
console.log("");
|
|
3064
|
+
try {
|
|
3065
|
+
await streamChat({
|
|
3066
|
+
agentId,
|
|
3067
|
+
message: trimmed,
|
|
3068
|
+
token,
|
|
3069
|
+
baseUrl: opts.baseUrl,
|
|
3070
|
+
showThinking: opts.thinking
|
|
3071
|
+
});
|
|
3072
|
+
} catch (err) {
|
|
3073
|
+
if (abortController.signal.aborted) return;
|
|
3074
|
+
log.error(err.message);
|
|
3075
|
+
}
|
|
3076
|
+
console.log("");
|
|
3077
|
+
rl.prompt();
|
|
3078
|
+
});
|
|
3079
|
+
});
|
|
3080
|
+
}
|
|
3081
|
+
|
|
3082
|
+
// src/commands/skills.ts
|
|
3083
|
+
import { readFile as readFile4, writeFile as writeFile2, readdir as readdir2, stat as stat3, mkdir as mkdir2 } from "fs/promises";
|
|
3084
|
+
import { join as join8, resolve, relative as relative4 } from "path";
|
|
3085
|
+
|
|
3086
|
+
// src/utils/skill-parser.ts
|
|
3087
|
+
import { readFile as readFile3, stat as stat2 } from "fs/promises";
|
|
3088
|
+
import { join as join7 } from "path";
|
|
3089
|
+
function parseSkillMd(raw) {
|
|
3090
|
+
const trimmed = raw.trimStart();
|
|
3091
|
+
if (!trimmed.startsWith("---")) {
|
|
3092
|
+
return { frontmatter: {}, content: raw };
|
|
3093
|
+
}
|
|
3094
|
+
const endIdx = trimmed.indexOf("\n---", 3);
|
|
3095
|
+
if (endIdx === -1) {
|
|
3096
|
+
return { frontmatter: {}, content: raw };
|
|
3097
|
+
}
|
|
3098
|
+
const yamlBlock = trimmed.slice(4, endIdx);
|
|
3099
|
+
const content = trimmed.slice(endIdx + 4).trimStart();
|
|
3100
|
+
const frontmatter = {};
|
|
3101
|
+
let currentKey = null;
|
|
3102
|
+
let currentArray = null;
|
|
3103
|
+
for (const line of yamlBlock.split("\n")) {
|
|
3104
|
+
const trimLine = line.trim();
|
|
3105
|
+
if (!trimLine || trimLine.startsWith("#")) continue;
|
|
3106
|
+
if (trimLine.startsWith("- ") && currentKey && currentArray) {
|
|
3107
|
+
currentArray.push(trimLine.slice(2).trim().replace(/^["']|["']$/g, ""));
|
|
3108
|
+
continue;
|
|
3109
|
+
}
|
|
3110
|
+
if (currentKey && currentArray) {
|
|
3111
|
+
frontmatter[currentKey] = currentArray;
|
|
3112
|
+
currentKey = null;
|
|
3113
|
+
currentArray = null;
|
|
3114
|
+
}
|
|
3115
|
+
const colonIdx = trimLine.indexOf(":");
|
|
3116
|
+
if (colonIdx === -1) continue;
|
|
3117
|
+
const key = trimLine.slice(0, colonIdx).trim();
|
|
3118
|
+
const rawVal = trimLine.slice(colonIdx + 1).trim();
|
|
3119
|
+
if (!rawVal) {
|
|
3120
|
+
currentKey = key;
|
|
3121
|
+
currentArray = [];
|
|
3122
|
+
continue;
|
|
3123
|
+
}
|
|
3124
|
+
if (rawVal.startsWith("[") && rawVal.endsWith("]")) {
|
|
3125
|
+
frontmatter[key] = rawVal.slice(1, -1).split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
|
|
3126
|
+
continue;
|
|
3127
|
+
}
|
|
3128
|
+
if (rawVal === "true") {
|
|
3129
|
+
frontmatter[key] = true;
|
|
3130
|
+
continue;
|
|
3131
|
+
}
|
|
3132
|
+
if (rawVal === "false") {
|
|
3133
|
+
frontmatter[key] = false;
|
|
3134
|
+
continue;
|
|
3135
|
+
}
|
|
3136
|
+
if (/^\d+(\.\d+)?$/.test(rawVal)) {
|
|
3137
|
+
frontmatter[key] = Number(rawVal);
|
|
3138
|
+
continue;
|
|
3139
|
+
}
|
|
3140
|
+
frontmatter[key] = rawVal.replace(/^["']|["']$/g, "");
|
|
3141
|
+
}
|
|
3142
|
+
if (currentKey && currentArray) {
|
|
3143
|
+
frontmatter[currentKey] = currentArray;
|
|
3144
|
+
}
|
|
3145
|
+
return { frontmatter, content };
|
|
3146
|
+
}
|
|
3147
|
+
async function loadSkillManifest(dir) {
|
|
3148
|
+
const skillJsonPath = join7(dir, "skill.json");
|
|
3149
|
+
try {
|
|
3150
|
+
const raw = await readFile3(skillJsonPath, "utf-8");
|
|
3151
|
+
const data = JSON.parse(raw);
|
|
3152
|
+
if (!data.name) throw new Error("skill.json missing required field: name");
|
|
3153
|
+
if (!data.version) throw new Error("skill.json missing required field: version");
|
|
3154
|
+
return {
|
|
3155
|
+
name: data.name,
|
|
3156
|
+
version: data.version,
|
|
3157
|
+
description: data.description,
|
|
3158
|
+
main: data.main || "SKILL.md",
|
|
3159
|
+
category: data.category,
|
|
3160
|
+
tags: data.tags,
|
|
3161
|
+
author: data.author,
|
|
3162
|
+
source_url: data.source_url,
|
|
3163
|
+
private: data.private,
|
|
3164
|
+
files: data.files
|
|
3165
|
+
};
|
|
3166
|
+
} catch (err) {
|
|
3167
|
+
if (err.code !== "ENOENT") {
|
|
3168
|
+
throw err;
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
const skillMdPath = join7(dir, "SKILL.md");
|
|
3172
|
+
try {
|
|
3173
|
+
const raw = await readFile3(skillMdPath, "utf-8");
|
|
3174
|
+
const { frontmatter } = parseSkillMd(raw);
|
|
3175
|
+
const name = frontmatter.name;
|
|
3176
|
+
if (!name) {
|
|
3177
|
+
throw new Error('No skill.json found and SKILL.md has no "name" in frontmatter');
|
|
3178
|
+
}
|
|
3179
|
+
return {
|
|
3180
|
+
name,
|
|
3181
|
+
version: frontmatter.version || "1.0.0",
|
|
3182
|
+
description: frontmatter.description,
|
|
3183
|
+
main: "SKILL.md",
|
|
3184
|
+
category: frontmatter.category,
|
|
3185
|
+
tags: frontmatter.tags,
|
|
3186
|
+
author: frontmatter.author,
|
|
3187
|
+
source_url: frontmatter.source_url,
|
|
3188
|
+
private: frontmatter.private
|
|
3189
|
+
};
|
|
3190
|
+
} catch (err) {
|
|
3191
|
+
if (err.code === "ENOENT") {
|
|
3192
|
+
throw new Error(`No skill.json or SKILL.md found in ${dir}`);
|
|
3193
|
+
}
|
|
3194
|
+
throw err;
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
async function pathExists(p) {
|
|
3198
|
+
try {
|
|
3199
|
+
await stat2(p);
|
|
3200
|
+
return true;
|
|
3201
|
+
} catch {
|
|
3202
|
+
return false;
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
|
|
3206
|
+
// src/utils/zip.ts
|
|
3207
|
+
import { deflateRawSync } from "zlib";
|
|
3208
|
+
function dosTime(date) {
|
|
3209
|
+
return {
|
|
3210
|
+
time: date.getHours() << 11 | date.getMinutes() << 5 | date.getSeconds() >> 1,
|
|
3211
|
+
date: date.getFullYear() - 1980 << 9 | date.getMonth() + 1 << 5 | date.getDate()
|
|
3212
|
+
};
|
|
3213
|
+
}
|
|
3214
|
+
function crc32(buf) {
|
|
3215
|
+
let crc = 4294967295;
|
|
3216
|
+
for (let i = 0; i < buf.length; i++) {
|
|
3217
|
+
crc ^= buf[i];
|
|
3218
|
+
for (let j = 0; j < 8; j++) {
|
|
3219
|
+
crc = crc >>> 1 ^ (crc & 1 ? 3988292384 : 0);
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
return (crc ^ 4294967295) >>> 0;
|
|
3223
|
+
}
|
|
3224
|
+
function writeUint16LE(buf, val, offset) {
|
|
3225
|
+
buf[offset] = val & 255;
|
|
3226
|
+
buf[offset + 1] = val >>> 8 & 255;
|
|
3227
|
+
}
|
|
3228
|
+
function writeUint32LE(buf, val, offset) {
|
|
3229
|
+
buf[offset] = val & 255;
|
|
3230
|
+
buf[offset + 1] = val >>> 8 & 255;
|
|
3231
|
+
buf[offset + 2] = val >>> 16 & 255;
|
|
3232
|
+
buf[offset + 3] = val >>> 24 & 255;
|
|
3233
|
+
}
|
|
3234
|
+
function createZipBuffer(entries) {
|
|
3235
|
+
const now = /* @__PURE__ */ new Date();
|
|
3236
|
+
const { time, date } = dosTime(now);
|
|
3237
|
+
const records = [];
|
|
3238
|
+
const chunks = [];
|
|
3239
|
+
let offset = 0;
|
|
3240
|
+
for (const entry of entries) {
|
|
3241
|
+
const nameBytes = Buffer.from(entry.path, "utf-8");
|
|
3242
|
+
const crc = crc32(entry.data);
|
|
3243
|
+
const compressed = deflateRawSync(entry.data, { level: 6 });
|
|
3244
|
+
const compressedSize = compressed.length;
|
|
3245
|
+
const uncompressedSize = entry.data.length;
|
|
3246
|
+
const header = Buffer.alloc(30 + nameBytes.length);
|
|
3247
|
+
writeUint32LE(header, 67324752, 0);
|
|
3248
|
+
writeUint16LE(header, 20, 4);
|
|
3249
|
+
writeUint16LE(header, 0, 6);
|
|
3250
|
+
writeUint16LE(header, 8, 8);
|
|
3251
|
+
writeUint16LE(header, time, 10);
|
|
3252
|
+
writeUint16LE(header, date, 12);
|
|
3253
|
+
writeUint32LE(header, crc, 14);
|
|
3254
|
+
writeUint32LE(header, compressedSize, 18);
|
|
3255
|
+
writeUint32LE(header, uncompressedSize, 22);
|
|
3256
|
+
writeUint16LE(header, nameBytes.length, 26);
|
|
3257
|
+
writeUint16LE(header, 0, 28);
|
|
3258
|
+
nameBytes.copy(header, 30);
|
|
3259
|
+
records.push({ header, compressed, crc, compressedSize, uncompressedSize, offset });
|
|
3260
|
+
chunks.push(header, compressed);
|
|
3261
|
+
offset += header.length + compressed.length;
|
|
3262
|
+
}
|
|
3263
|
+
const centralDirStart = offset;
|
|
3264
|
+
for (let i = 0; i < entries.length; i++) {
|
|
3265
|
+
const entry = entries[i];
|
|
3266
|
+
const rec = records[i];
|
|
3267
|
+
const nameBytes = Buffer.from(entry.path, "utf-8");
|
|
3268
|
+
const cdh = Buffer.alloc(46 + nameBytes.length);
|
|
3269
|
+
writeUint32LE(cdh, 33639248, 0);
|
|
3270
|
+
writeUint16LE(cdh, 20, 4);
|
|
3271
|
+
writeUint16LE(cdh, 20, 6);
|
|
3272
|
+
writeUint16LE(cdh, 0, 8);
|
|
3273
|
+
writeUint16LE(cdh, 8, 10);
|
|
3274
|
+
writeUint16LE(cdh, time, 12);
|
|
3275
|
+
writeUint16LE(cdh, date, 14);
|
|
3276
|
+
writeUint32LE(cdh, rec.crc, 16);
|
|
3277
|
+
writeUint32LE(cdh, rec.compressedSize, 20);
|
|
3278
|
+
writeUint32LE(cdh, rec.uncompressedSize, 24);
|
|
3279
|
+
writeUint16LE(cdh, nameBytes.length, 28);
|
|
3280
|
+
writeUint16LE(cdh, 0, 30);
|
|
3281
|
+
writeUint16LE(cdh, 0, 32);
|
|
3282
|
+
writeUint16LE(cdh, 0, 34);
|
|
3283
|
+
writeUint16LE(cdh, 0, 36);
|
|
3284
|
+
writeUint32LE(cdh, 0, 38);
|
|
3285
|
+
writeUint32LE(cdh, rec.offset, 42);
|
|
3286
|
+
nameBytes.copy(cdh, 46);
|
|
3287
|
+
chunks.push(cdh);
|
|
3288
|
+
offset += cdh.length;
|
|
3289
|
+
}
|
|
3290
|
+
const centralDirSize = offset - centralDirStart;
|
|
3291
|
+
const eocd = Buffer.alloc(22);
|
|
3292
|
+
writeUint32LE(eocd, 101010256, 0);
|
|
3293
|
+
writeUint16LE(eocd, 0, 4);
|
|
3294
|
+
writeUint16LE(eocd, 0, 6);
|
|
3295
|
+
writeUint16LE(eocd, entries.length, 8);
|
|
3296
|
+
writeUint16LE(eocd, entries.length, 10);
|
|
3297
|
+
writeUint32LE(eocd, centralDirSize, 12);
|
|
3298
|
+
writeUint32LE(eocd, centralDirStart, 16);
|
|
3299
|
+
writeUint16LE(eocd, 0, 20);
|
|
3300
|
+
chunks.push(eocd);
|
|
3301
|
+
return Buffer.concat(chunks);
|
|
3302
|
+
}
|
|
3303
|
+
|
|
3304
|
+
// src/commands/skills.ts
|
|
3305
|
+
var slog = {
|
|
3306
|
+
info: (msg) => {
|
|
3307
|
+
process.stderr.write(`\x1B[34mINFO\x1B[0m ${msg}
|
|
3308
|
+
`);
|
|
3309
|
+
},
|
|
3310
|
+
success: (msg) => {
|
|
3311
|
+
process.stderr.write(`\x1B[32mOK\x1B[0m ${msg}
|
|
3312
|
+
`);
|
|
3313
|
+
},
|
|
3314
|
+
warn: (msg) => {
|
|
3315
|
+
process.stderr.write(`\x1B[33mWARN\x1B[0m ${msg}
|
|
3316
|
+
`);
|
|
3317
|
+
},
|
|
3318
|
+
banner: (text) => {
|
|
3319
|
+
process.stderr.write(`
|
|
3320
|
+
\x1B[1m${text}\x1B[0m
|
|
3321
|
+
|
|
3322
|
+
`);
|
|
3323
|
+
}
|
|
3324
|
+
};
|
|
3325
|
+
function outputJson(data) {
|
|
3326
|
+
console.log(JSON.stringify(data, null, 2));
|
|
3327
|
+
}
|
|
3328
|
+
function outputError(error, message, hint) {
|
|
3329
|
+
console.log(JSON.stringify({ success: false, error, message, ...hint ? { hint } : {} }));
|
|
3330
|
+
process.exit(1);
|
|
3331
|
+
}
|
|
3332
|
+
function resolveSkillDir(pathArg) {
|
|
3333
|
+
return pathArg ? resolve(pathArg) : process.cwd();
|
|
3334
|
+
}
|
|
3335
|
+
async function collectPackFiles(dir, manifest) {
|
|
3336
|
+
const results = [];
|
|
3337
|
+
if (manifest.files && manifest.files.length > 0) {
|
|
3338
|
+
for (const pattern of manifest.files) {
|
|
3339
|
+
const fullPath = join8(dir, pattern);
|
|
3340
|
+
try {
|
|
3341
|
+
const s = await stat3(fullPath);
|
|
3342
|
+
if (s.isDirectory()) {
|
|
3343
|
+
const sub = await walkDir(fullPath);
|
|
3344
|
+
for (const f of sub) {
|
|
3345
|
+
results.push(relative4(dir, f));
|
|
3346
|
+
}
|
|
3347
|
+
} else {
|
|
3348
|
+
results.push(pattern);
|
|
3349
|
+
}
|
|
3350
|
+
} catch {
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
} else {
|
|
3354
|
+
const all = await walkDir(dir);
|
|
3355
|
+
for (const f of all) {
|
|
3356
|
+
const rel = relative4(dir, f);
|
|
3357
|
+
if (rel === "skill.json") continue;
|
|
3358
|
+
results.push(rel);
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
const mainFile = manifest.main || "SKILL.md";
|
|
3362
|
+
if (!results.includes(mainFile)) {
|
|
3363
|
+
const mainPath = join8(dir, mainFile);
|
|
3364
|
+
if (await pathExists(mainPath)) {
|
|
3365
|
+
results.unshift(mainFile);
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
return [...new Set(results)];
|
|
3369
|
+
}
|
|
3370
|
+
async function walkDir(dir) {
|
|
3371
|
+
const files = [];
|
|
3372
|
+
let entries;
|
|
3373
|
+
try {
|
|
3374
|
+
entries = await readdir2(dir, { withFileTypes: true });
|
|
3375
|
+
} catch {
|
|
3376
|
+
return files;
|
|
3377
|
+
}
|
|
3378
|
+
for (const entry of entries) {
|
|
3379
|
+
if (entry.isSymbolicLink()) continue;
|
|
3380
|
+
const fullPath = join8(dir, entry.name);
|
|
3381
|
+
if (entry.isDirectory()) {
|
|
3382
|
+
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
3383
|
+
const sub = await walkDir(fullPath);
|
|
3384
|
+
files.push(...sub);
|
|
3385
|
+
} else if (entry.isFile()) {
|
|
3386
|
+
files.push(fullPath);
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
return files;
|
|
3390
|
+
}
|
|
3391
|
+
async function packSkill(dir, manifest) {
|
|
3392
|
+
const fileList = await collectPackFiles(dir, manifest);
|
|
3393
|
+
if (fileList.length === 0) {
|
|
3394
|
+
outputError("no_files", "No files found to pack");
|
|
3395
|
+
}
|
|
3396
|
+
const entries = [];
|
|
3397
|
+
for (const relPath of fileList) {
|
|
3398
|
+
const absPath = join8(dir, relPath);
|
|
3399
|
+
try {
|
|
3400
|
+
const data = await readFile4(absPath);
|
|
3401
|
+
entries.push({ path: relPath.replace(/\\/g, "/"), data });
|
|
3402
|
+
} catch {
|
|
3403
|
+
slog.warn(`Skipping unreadable file: ${relPath}`);
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
const buffer = createZipBuffer(entries);
|
|
3407
|
+
const filename = `${manifest.name}-${manifest.version}.zip`;
|
|
3408
|
+
return {
|
|
3409
|
+
filename,
|
|
3410
|
+
buffer,
|
|
3411
|
+
files: fileList,
|
|
3412
|
+
size: buffer.length
|
|
3413
|
+
};
|
|
3414
|
+
}
|
|
3415
|
+
function bumpVersion(current, bump) {
|
|
3416
|
+
if (/^\d+\.\d+\.\d+/.test(bump)) return bump;
|
|
3417
|
+
const parts = current.split(".").map(Number);
|
|
3418
|
+
if (parts.length < 3) return current;
|
|
3419
|
+
switch (bump) {
|
|
3420
|
+
case "major":
|
|
3421
|
+
return `${parts[0] + 1}.0.0`;
|
|
3422
|
+
case "minor":
|
|
3423
|
+
return `${parts[0]}.${parts[1] + 1}.0`;
|
|
3424
|
+
case "patch":
|
|
3425
|
+
return `${parts[0]}.${parts[1]}.${parts[2] + 1}`;
|
|
3426
|
+
default:
|
|
3427
|
+
throw new Error(`Invalid bump type: ${bump}. Use major, minor, patch, or a version string.`);
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3430
|
+
var SKILL_MD_TEMPLATE = `---
|
|
3431
|
+
name: {{name}}
|
|
3432
|
+
version: 1.0.0
|
|
3433
|
+
---
|
|
3434
|
+
|
|
3435
|
+
# {{name}}
|
|
3436
|
+
|
|
3437
|
+
{{description}}
|
|
3438
|
+
|
|
3439
|
+
## Usage
|
|
3440
|
+
|
|
3441
|
+
Describe how to use this skill.
|
|
3442
|
+
`;
|
|
3443
|
+
var SKILL_JSON_TEMPLATE = (name, description) => ({
|
|
3444
|
+
name,
|
|
3445
|
+
version: "1.0.0",
|
|
3446
|
+
description,
|
|
3447
|
+
main: "SKILL.md",
|
|
3448
|
+
category: "general",
|
|
3449
|
+
tags: [],
|
|
3450
|
+
files: ["SKILL.md"]
|
|
3451
|
+
});
|
|
3452
|
+
function registerSkillsCommand(program2) {
|
|
3453
|
+
const skills = program2.command("skills").description("Manage skill packages (publish, pack, version)");
|
|
3454
|
+
skills.command("init [path]").description("Initialize a new skill project").option("--name <name>", "Skill name").option("--description <desc>", "Skill description").action(async (pathArg, opts) => {
|
|
3455
|
+
try {
|
|
3456
|
+
const dir = resolveSkillDir(pathArg);
|
|
3457
|
+
await mkdir2(dir, { recursive: true });
|
|
3458
|
+
let name = opts.name;
|
|
3459
|
+
let description = opts.description || "";
|
|
3460
|
+
const skillMdPath = join8(dir, "SKILL.md");
|
|
3461
|
+
const skillJsonPath = join8(dir, "skill.json");
|
|
3462
|
+
if (await pathExists(skillJsonPath)) {
|
|
3463
|
+
outputError("already_exists", "skill.json already exists in this directory");
|
|
3464
|
+
}
|
|
3465
|
+
if (await pathExists(skillMdPath)) {
|
|
3466
|
+
const raw = await readFile4(skillMdPath, "utf-8");
|
|
3467
|
+
const { frontmatter } = parseSkillMd(raw);
|
|
3468
|
+
if (frontmatter.name) {
|
|
3469
|
+
name = name || frontmatter.name;
|
|
3470
|
+
description = description || frontmatter.description || "";
|
|
3471
|
+
const manifest2 = {
|
|
3472
|
+
name,
|
|
3473
|
+
version: frontmatter.version || "1.0.0",
|
|
3474
|
+
description,
|
|
3475
|
+
main: "SKILL.md",
|
|
3476
|
+
category: frontmatter.category || "general",
|
|
3477
|
+
tags: frontmatter.tags || [],
|
|
3478
|
+
author: frontmatter.author,
|
|
3479
|
+
source_url: frontmatter.source_url,
|
|
3480
|
+
files: ["SKILL.md"]
|
|
3481
|
+
};
|
|
3482
|
+
await writeFile2(skillJsonPath, JSON.stringify(manifest2, null, 2) + "\n");
|
|
3483
|
+
slog.info(`Migrated frontmatter from SKILL.md to skill.json`);
|
|
3484
|
+
outputJson({ success: true, path: skillJsonPath, migrated: true });
|
|
3485
|
+
return;
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3488
|
+
if (!name) {
|
|
3489
|
+
name = dir.split("/").pop()?.replace(/[^a-z0-9-]/gi, "-").toLowerCase() || "my-skill";
|
|
3490
|
+
}
|
|
3491
|
+
const manifest = SKILL_JSON_TEMPLATE(name, description);
|
|
3492
|
+
await writeFile2(skillJsonPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
3493
|
+
if (!await pathExists(skillMdPath)) {
|
|
3494
|
+
const content = SKILL_MD_TEMPLATE.replace(/\{\{name\}\}/g, name).replace(/\{\{description\}\}/g, description || "A new skill.");
|
|
3495
|
+
await writeFile2(skillMdPath, content);
|
|
3496
|
+
}
|
|
3497
|
+
slog.info(`Initialized skill: ${name}`);
|
|
3498
|
+
outputJson({ success: true, path: skillJsonPath });
|
|
3499
|
+
} catch (err) {
|
|
3500
|
+
if (err instanceof Error && err.message.includes("already_exists")) throw err;
|
|
3501
|
+
outputError("init_failed", err.message);
|
|
3502
|
+
}
|
|
3503
|
+
});
|
|
3504
|
+
skills.command("pack [path]").description("Pack skill into a local .zip file").action(async (pathArg) => {
|
|
3505
|
+
try {
|
|
3506
|
+
const dir = resolveSkillDir(pathArg);
|
|
3507
|
+
const manifest = await loadSkillManifest(dir);
|
|
3508
|
+
const result = await packSkill(dir, manifest);
|
|
3509
|
+
const outPath = join8(dir, result.filename);
|
|
3510
|
+
await writeFile2(outPath, result.buffer);
|
|
3511
|
+
slog.info(`Packed ${result.files.length} files \u2192 ${result.filename} (${result.size} bytes)`);
|
|
3512
|
+
outputJson({
|
|
3513
|
+
success: true,
|
|
3514
|
+
filename: result.filename,
|
|
3515
|
+
size: result.size,
|
|
3516
|
+
files: result.files
|
|
3517
|
+
});
|
|
3518
|
+
} catch (err) {
|
|
3519
|
+
if (err instanceof Error && err.message.includes("success")) throw err;
|
|
3520
|
+
outputError("pack_failed", err.message);
|
|
3521
|
+
}
|
|
3522
|
+
});
|
|
3523
|
+
skills.command("publish [path]").description("Pack and publish skill to agents.hot").option("--name <name>", "Override skill name").option("--version <version>", "Override version").option("--private", "Publish as private skill").option("--stdin", "Read SKILL.md content from stdin").action(async (pathArg, opts) => {
|
|
3524
|
+
try {
|
|
3525
|
+
let content;
|
|
3526
|
+
let manifest;
|
|
3527
|
+
let packResult = null;
|
|
3528
|
+
if (opts.stdin) {
|
|
3529
|
+
const chunks = [];
|
|
3530
|
+
for await (const chunk of process.stdin) {
|
|
3531
|
+
chunks.push(chunk);
|
|
3532
|
+
}
|
|
3533
|
+
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
3534
|
+
const { frontmatter } = parseSkillMd(raw);
|
|
3535
|
+
const name = opts.name || frontmatter.name;
|
|
3536
|
+
if (!name) {
|
|
3537
|
+
outputError("validation_error", "--name is required when using --stdin without frontmatter name");
|
|
3538
|
+
}
|
|
3539
|
+
manifest = {
|
|
3540
|
+
name,
|
|
3541
|
+
version: opts.version || frontmatter.version || "1.0.0",
|
|
3542
|
+
description: frontmatter.description,
|
|
3543
|
+
category: frontmatter.category,
|
|
3544
|
+
tags: frontmatter.tags,
|
|
3545
|
+
author: frontmatter.author,
|
|
3546
|
+
private: opts.private ?? frontmatter.private
|
|
3547
|
+
};
|
|
3548
|
+
content = raw;
|
|
3549
|
+
} else {
|
|
3550
|
+
const dir = resolveSkillDir(pathArg);
|
|
3551
|
+
manifest = await loadSkillManifest(dir);
|
|
3552
|
+
if (opts.name) manifest.name = opts.name;
|
|
3553
|
+
if (opts.version) manifest.version = opts.version;
|
|
3554
|
+
if (opts.private !== void 0) manifest.private = opts.private;
|
|
3555
|
+
content = await readFile4(join8(dir, manifest.main || "SKILL.md"), "utf-8");
|
|
3556
|
+
packResult = await packSkill(dir, manifest);
|
|
3557
|
+
slog.info(`Packed ${packResult.files.length} files (${packResult.size} bytes)`);
|
|
3558
|
+
}
|
|
3559
|
+
const formData = new FormData();
|
|
3560
|
+
const metadata = {
|
|
3561
|
+
name: manifest.name,
|
|
3562
|
+
version: manifest.version,
|
|
3563
|
+
description: manifest.description,
|
|
3564
|
+
category: manifest.category,
|
|
3565
|
+
tags: manifest.tags,
|
|
3566
|
+
author: manifest.author,
|
|
3567
|
+
source_url: manifest.source_url,
|
|
3568
|
+
is_private: manifest.private
|
|
3569
|
+
};
|
|
3570
|
+
formData.append("metadata", JSON.stringify(metadata));
|
|
3571
|
+
formData.append("content", content);
|
|
3572
|
+
if (packResult) {
|
|
3573
|
+
const blob = new Blob([packResult.buffer], { type: "application/zip" });
|
|
3574
|
+
formData.append("package", blob, packResult.filename);
|
|
3575
|
+
}
|
|
3576
|
+
slog.info(`Publishing ${manifest.name}@${manifest.version}...`);
|
|
3577
|
+
const client = createClient();
|
|
3578
|
+
const result = await client.postFormData("/api/skills/publish", formData);
|
|
3579
|
+
slog.success(`Skill ${result.action}: ${manifest.name}`);
|
|
3580
|
+
outputJson({
|
|
3581
|
+
success: true,
|
|
3582
|
+
action: result.action,
|
|
3583
|
+
skill: result.skill,
|
|
3584
|
+
url: `https://agents.hot/skills/${result.skill.slug}`
|
|
3585
|
+
});
|
|
3586
|
+
} catch (err) {
|
|
3587
|
+
if (err instanceof PlatformApiError) {
|
|
3588
|
+
outputError(err.errorCode, err.message);
|
|
3589
|
+
}
|
|
3590
|
+
outputError("publish_failed", err.message);
|
|
3591
|
+
}
|
|
3592
|
+
});
|
|
3593
|
+
skills.command("info <slug>").description("View skill details").option("--human", "Human-readable output").action(async (slug, opts) => {
|
|
3594
|
+
try {
|
|
3595
|
+
const client = createClient();
|
|
3596
|
+
const data = await client.get(`/api/skills/${encodeURIComponent(slug)}`);
|
|
3597
|
+
if (opts.human) {
|
|
3598
|
+
console.log("");
|
|
3599
|
+
console.log(` ${BOLD}${data.name}${RESET} v${data.version || "?"}`);
|
|
3600
|
+
if (data.description) console.log(` ${data.description}`);
|
|
3601
|
+
console.log(` ${GRAY}slug${RESET} ${data.slug}`);
|
|
3602
|
+
console.log(` ${GRAY}author${RESET} ${data.author || "\u2014"}`);
|
|
3603
|
+
console.log(` ${GRAY}category${RESET} ${data.category || "\u2014"}`);
|
|
3604
|
+
console.log(` ${GRAY}installs${RESET} ${data.installs ?? 0}`);
|
|
3605
|
+
console.log(` ${GRAY}private${RESET} ${data.is_private ? "yes" : "no"}`);
|
|
3606
|
+
console.log("");
|
|
3607
|
+
return;
|
|
3608
|
+
}
|
|
3609
|
+
outputJson(data);
|
|
3610
|
+
} catch (err) {
|
|
3611
|
+
if (err instanceof PlatformApiError) {
|
|
3612
|
+
outputError(err.errorCode, err.message);
|
|
3613
|
+
}
|
|
3614
|
+
outputError("info_failed", err.message);
|
|
3615
|
+
}
|
|
3616
|
+
});
|
|
3617
|
+
skills.command("list").alias("ls").description("List your published skills").option("--human", "Human-readable table output").action(async (opts) => {
|
|
3618
|
+
try {
|
|
3619
|
+
const client = createClient();
|
|
3620
|
+
const data = await client.get("/api/user/skills");
|
|
3621
|
+
if (opts.human) {
|
|
3622
|
+
if (data.owned.length === 0 && data.authorized.length === 0) {
|
|
3623
|
+
slog.info("No skills found. Create one with: agent-mesh skills init");
|
|
3624
|
+
return;
|
|
3625
|
+
}
|
|
3626
|
+
if (data.owned.length > 0) {
|
|
3627
|
+
slog.banner("My Skills");
|
|
3628
|
+
const table = renderTable(
|
|
3629
|
+
[
|
|
3630
|
+
{ key: "name", label: "NAME", width: 24 },
|
|
3631
|
+
{ key: "version", label: "VERSION", width: 12 },
|
|
3632
|
+
{ key: "installs", label: "INSTALLS", width: 12, align: "right" },
|
|
3633
|
+
{ key: "private", label: "PRIVATE", width: 10 }
|
|
3634
|
+
],
|
|
3635
|
+
data.owned.map((s) => ({
|
|
3636
|
+
name: s.name,
|
|
3637
|
+
version: s.version || "\u2014",
|
|
3638
|
+
installs: String(s.installs ?? 0),
|
|
3639
|
+
private: s.is_private ? "yes" : `${GREEN}no${RESET}`
|
|
3640
|
+
}))
|
|
3641
|
+
);
|
|
3642
|
+
console.log(table);
|
|
3643
|
+
}
|
|
3644
|
+
if (data.authorized.length > 0) {
|
|
3645
|
+
slog.banner("Authorized Skills");
|
|
3646
|
+
const table = renderTable(
|
|
3647
|
+
[
|
|
3648
|
+
{ key: "name", label: "NAME", width: 24 },
|
|
3649
|
+
{ key: "author", label: "AUTHOR", width: 16 },
|
|
3650
|
+
{ key: "version", label: "VERSION", width: 12 }
|
|
3651
|
+
],
|
|
3652
|
+
data.authorized.map((s) => ({
|
|
3653
|
+
name: s.name,
|
|
3654
|
+
author: s.author || "\u2014",
|
|
3655
|
+
version: s.version || "\u2014"
|
|
3656
|
+
}))
|
|
3657
|
+
);
|
|
3658
|
+
console.log(table);
|
|
3659
|
+
}
|
|
3660
|
+
return;
|
|
3661
|
+
}
|
|
3662
|
+
outputJson(data);
|
|
3663
|
+
} catch (err) {
|
|
3664
|
+
if (err instanceof PlatformApiError) {
|
|
3665
|
+
outputError(err.errorCode, err.message);
|
|
3666
|
+
}
|
|
3667
|
+
outputError("list_failed", err.message);
|
|
3668
|
+
}
|
|
3669
|
+
});
|
|
3670
|
+
skills.command("unpublish <slug>").description("Unpublish a skill").action(async (slug) => {
|
|
3671
|
+
try {
|
|
3672
|
+
const client = createClient();
|
|
3673
|
+
const result = await client.del(`/api/skills/${encodeURIComponent(slug)}`);
|
|
3674
|
+
slog.success(`Skill unpublished: ${slug}`);
|
|
3675
|
+
outputJson(result);
|
|
3676
|
+
} catch (err) {
|
|
3677
|
+
if (err instanceof PlatformApiError) {
|
|
3678
|
+
outputError(err.errorCode, err.message);
|
|
3679
|
+
}
|
|
3680
|
+
outputError("unpublish_failed", err.message);
|
|
3681
|
+
}
|
|
3682
|
+
});
|
|
3683
|
+
skills.command("version <bump> [path]").description("Bump skill version (patch | minor | major | x.y.z)").action(async (bump, pathArg) => {
|
|
3684
|
+
try {
|
|
3685
|
+
const dir = resolveSkillDir(pathArg);
|
|
3686
|
+
const skillJsonPath = join8(dir, "skill.json");
|
|
3687
|
+
if (!await pathExists(skillJsonPath)) {
|
|
3688
|
+
outputError("not_found", "No skill.json found. Run `agent-mesh skills init` first.");
|
|
3689
|
+
}
|
|
3690
|
+
const raw = await readFile4(skillJsonPath, "utf-8");
|
|
3691
|
+
const data = JSON.parse(raw);
|
|
3692
|
+
const oldVersion = data.version || "0.0.0";
|
|
3693
|
+
const newVersion = bumpVersion(oldVersion, bump);
|
|
3694
|
+
data.version = newVersion;
|
|
3695
|
+
await writeFile2(skillJsonPath, JSON.stringify(data, null, 2) + "\n");
|
|
3696
|
+
slog.success(`${oldVersion} \u2192 ${newVersion}`);
|
|
3697
|
+
outputJson({ success: true, old: oldVersion, new: newVersion });
|
|
3698
|
+
} catch (err) {
|
|
3699
|
+
if (err instanceof Error && err.message.includes("success")) throw err;
|
|
3700
|
+
outputError("version_failed", err.message);
|
|
3701
|
+
}
|
|
3702
|
+
});
|
|
3703
|
+
}
|
|
3704
|
+
|
|
3705
|
+
// src/commands/discover.ts
|
|
3706
|
+
var BASE_URL = "https://agents.hot";
|
|
3707
|
+
function formatStatus2(online) {
|
|
3708
|
+
return online ? `${GREEN}\u25CF online${RESET}` : `${GRAY}\u25CB offline${RESET}`;
|
|
3709
|
+
}
|
|
3710
|
+
function formatCapabilities(caps) {
|
|
3711
|
+
if (!caps || caps.length === 0) return `${GRAY}\u2014${RESET}`;
|
|
3712
|
+
return caps.join(", ");
|
|
3713
|
+
}
|
|
3714
|
+
function registerDiscoverCommand(program2) {
|
|
3715
|
+
program2.command("discover").description("Discover agents on the A2A network").option("--capability <cap>", "Filter by capability").option("--online", "Show only online agents").option("--limit <n>", "Max results (default 20)", "20").option("--offset <n>", "Pagination offset", "0").option("--json", "Output raw JSON").action(async (opts) => {
|
|
3716
|
+
try {
|
|
3717
|
+
const params = new URLSearchParams();
|
|
3718
|
+
if (opts.capability) params.set("capability", opts.capability);
|
|
3719
|
+
if (opts.online) params.set("online", "true");
|
|
3720
|
+
params.set("limit", opts.limit);
|
|
3721
|
+
params.set("offset", opts.offset);
|
|
3722
|
+
const url = `${BASE_URL}/api/agents/discover?${params}`;
|
|
3723
|
+
const res = await fetch(url);
|
|
3724
|
+
if (!res.ok) {
|
|
3725
|
+
const body = await res.json().catch(() => ({}));
|
|
3726
|
+
console.error(` Error: ${body.message ?? `HTTP ${res.status}`}`);
|
|
3727
|
+
process.exit(1);
|
|
3728
|
+
}
|
|
3729
|
+
const data = await res.json();
|
|
3730
|
+
if (opts.json) {
|
|
3731
|
+
console.log(JSON.stringify(data, null, 2));
|
|
3732
|
+
return;
|
|
3733
|
+
}
|
|
3734
|
+
if (data.agents.length === 0) {
|
|
3735
|
+
console.log(" No agents found.");
|
|
3736
|
+
return;
|
|
3737
|
+
}
|
|
3738
|
+
const table = renderTable(
|
|
3739
|
+
[
|
|
3740
|
+
{ key: "name", label: "NAME", width: 24 },
|
|
3741
|
+
{ key: "type", label: "TYPE", width: 12 },
|
|
3742
|
+
{ key: "status", label: "STATUS", width: 14 },
|
|
3743
|
+
{ key: "capabilities", label: "CAPABILITIES", width: 30 }
|
|
3744
|
+
],
|
|
3745
|
+
data.agents.map((a) => ({
|
|
3746
|
+
name: a.name,
|
|
3747
|
+
type: a.agent_type,
|
|
3748
|
+
status: formatStatus2(a.is_online),
|
|
3749
|
+
capabilities: formatCapabilities(a.capabilities)
|
|
3750
|
+
}))
|
|
3751
|
+
);
|
|
3752
|
+
console.log(table);
|
|
3753
|
+
console.log(`
|
|
3754
|
+
${GRAY}${data.agents.length} of ${data.total} agent(s)${RESET}`);
|
|
3755
|
+
} catch (err) {
|
|
3756
|
+
console.error(` Error: ${err.message}`);
|
|
3757
|
+
process.exit(1);
|
|
3758
|
+
}
|
|
3759
|
+
});
|
|
3760
|
+
}
|
|
3761
|
+
|
|
3762
|
+
// src/commands/call.ts
|
|
3763
|
+
import { readFileSync, writeFileSync as writeFileSync2 } from "fs";
|
|
3764
|
+
var DEFAULT_BASE_URL4 = "https://agents.hot";
|
|
3765
|
+
function handleError2(err) {
|
|
3766
|
+
if (err instanceof PlatformApiError) {
|
|
3767
|
+
log.error(err.message);
|
|
3768
|
+
} else {
|
|
3769
|
+
log.error(err.message);
|
|
3770
|
+
}
|
|
3771
|
+
process.exit(1);
|
|
3772
|
+
}
|
|
3773
|
+
function registerCallCommand(program2) {
|
|
3774
|
+
program2.command("call <agent>").description("Call an agent on the A2A network").requiredOption("--task <description>", "Task description").option("--input-file <path>", "Read file and append to task description").option("--output-file <path>", "Save response text to file").option("--json", "Output JSONL events").option("--timeout <seconds>", "Timeout in seconds", "300").action(async (agentInput, opts) => {
|
|
3775
|
+
try {
|
|
3776
|
+
const token = loadToken();
|
|
3777
|
+
if (!token) {
|
|
3778
|
+
log.error("Not authenticated. Run `agent-mesh login` first.");
|
|
3779
|
+
process.exit(1);
|
|
3780
|
+
}
|
|
3781
|
+
const client = createClient();
|
|
3782
|
+
const { id, name } = await resolveAgentId(agentInput, client);
|
|
3783
|
+
let taskDescription = opts.task;
|
|
3784
|
+
if (opts.inputFile) {
|
|
3785
|
+
const content = readFileSync(opts.inputFile, "utf-8");
|
|
3786
|
+
taskDescription = `${taskDescription}
|
|
3787
|
+
|
|
3788
|
+
---
|
|
3789
|
+
|
|
3790
|
+
${content}`;
|
|
3791
|
+
}
|
|
3792
|
+
const timeoutMs = parseInt(opts.timeout || "300", 10) * 1e3;
|
|
3793
|
+
const abortController = new AbortController();
|
|
3794
|
+
const timer = setTimeout(() => abortController.abort(), timeoutMs);
|
|
3795
|
+
const selfAgentId = process.env.AGENT_BRIDGE_AGENT_ID;
|
|
3796
|
+
const res = await fetch(`${DEFAULT_BASE_URL4}/api/agents/${id}/call`, {
|
|
3797
|
+
method: "POST",
|
|
3798
|
+
headers: {
|
|
3799
|
+
Authorization: `Bearer ${token}`,
|
|
3800
|
+
"Content-Type": "application/json",
|
|
3801
|
+
Accept: "text/event-stream",
|
|
3802
|
+
...selfAgentId ? { "X-Caller-Agent-Id": selfAgentId } : {}
|
|
3803
|
+
},
|
|
3804
|
+
body: JSON.stringify({ task_description: taskDescription }),
|
|
3805
|
+
signal: abortController.signal
|
|
3806
|
+
});
|
|
3807
|
+
clearTimeout(timer);
|
|
3808
|
+
if (!res.ok) {
|
|
3809
|
+
let msg = `HTTP ${res.status}`;
|
|
3810
|
+
try {
|
|
3811
|
+
const body = await res.json();
|
|
3812
|
+
msg = body.message || body.error || msg;
|
|
3813
|
+
} catch {
|
|
3814
|
+
}
|
|
3815
|
+
log.error(msg);
|
|
3816
|
+
process.exit(1);
|
|
3817
|
+
}
|
|
3818
|
+
const contentType = res.headers.get("Content-Type") || "";
|
|
3819
|
+
if (contentType.includes("application/json")) {
|
|
3820
|
+
const result = await res.json();
|
|
3821
|
+
if (opts.json) {
|
|
3822
|
+
console.log(JSON.stringify(result));
|
|
3823
|
+
} else {
|
|
3824
|
+
console.log("");
|
|
3825
|
+
log.success(`Call created for ${BOLD}${name}${RESET}`);
|
|
3826
|
+
console.log(` ${GRAY}Call ID${RESET} ${result.call_id}`);
|
|
3827
|
+
console.log(` ${GRAY}Status${RESET} ${result.status}`);
|
|
3828
|
+
console.log(` ${GRAY}Created${RESET} ${result.created_at}`);
|
|
3829
|
+
console.log("");
|
|
3830
|
+
}
|
|
3831
|
+
return;
|
|
3832
|
+
}
|
|
3833
|
+
if (!res.body) {
|
|
3834
|
+
log.error("Empty response body");
|
|
3835
|
+
process.exit(1);
|
|
3836
|
+
}
|
|
3837
|
+
if (!opts.json) {
|
|
3838
|
+
log.info(`Calling ${BOLD}${name}${RESET}...`);
|
|
3839
|
+
console.log("");
|
|
3840
|
+
}
|
|
3841
|
+
const reader = res.body.getReader();
|
|
3842
|
+
const decoder = new TextDecoder();
|
|
3843
|
+
let buffer = "";
|
|
3844
|
+
let outputBuffer = "";
|
|
3845
|
+
let inThinkingBlock = false;
|
|
3846
|
+
while (true) {
|
|
3847
|
+
const { done, value } = await reader.read();
|
|
3848
|
+
if (done) break;
|
|
3849
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
3850
|
+
const parsed = parseSseChunk(chunk, buffer);
|
|
3851
|
+
buffer = parsed.carry;
|
|
3852
|
+
for (const data of parsed.events) {
|
|
3853
|
+
if (data === "[DONE]") continue;
|
|
3854
|
+
try {
|
|
3855
|
+
const event = JSON.parse(data);
|
|
3856
|
+
if (opts.json) {
|
|
3857
|
+
console.log(JSON.stringify(event));
|
|
3858
|
+
} else {
|
|
3859
|
+
if (event.type === "chunk" && event.delta) {
|
|
3860
|
+
process.stdout.write(event.delta);
|
|
3861
|
+
if (!event.kind || event.kind === "text") {
|
|
3862
|
+
const delta = event.delta;
|
|
3863
|
+
if (delta.startsWith("{") && delta.includes('"type":')) {
|
|
3864
|
+
if (delta.includes('"type":"thinking"') && delta.includes("content_block_start")) {
|
|
3865
|
+
inThinkingBlock = true;
|
|
3866
|
+
} else if (delta.includes('"type":"text"') && delta.includes("content_block_start")) {
|
|
3867
|
+
inThinkingBlock = false;
|
|
3868
|
+
}
|
|
3869
|
+
} else if (!inThinkingBlock) {
|
|
3870
|
+
outputBuffer += delta;
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
} else if (event.type === "done" && event.attachments?.length) {
|
|
3874
|
+
console.log("");
|
|
3875
|
+
for (const att of event.attachments) {
|
|
3876
|
+
log.info(` ${GRAY}File:${RESET} ${att.name} ${GRAY}${att.url}${RESET}`);
|
|
3877
|
+
}
|
|
3878
|
+
} else if (event.type === "error") {
|
|
3879
|
+
process.stderr.write(`
|
|
3880
|
+
Error: ${event.message}
|
|
3881
|
+
`);
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3884
|
+
} catch {
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
if (buffer.trim()) {
|
|
3889
|
+
const parsed = parseSseChunk("\n\n", buffer);
|
|
3890
|
+
for (const data of parsed.events) {
|
|
3891
|
+
if (data === "[DONE]") continue;
|
|
3892
|
+
try {
|
|
3893
|
+
const event = JSON.parse(data);
|
|
3894
|
+
if (opts.json) {
|
|
3895
|
+
console.log(JSON.stringify(event));
|
|
3896
|
+
} else if (event.type === "chunk" && event.delta) {
|
|
3897
|
+
process.stdout.write(event.delta);
|
|
3898
|
+
if (!event.kind || event.kind === "text") {
|
|
3899
|
+
const delta = event.delta;
|
|
3900
|
+
if (!(delta.startsWith("{") && delta.includes('"type":')) && !inThinkingBlock) {
|
|
3901
|
+
outputBuffer += delta;
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3904
|
+
}
|
|
3905
|
+
} catch {
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
3909
|
+
if (opts.outputFile && outputBuffer) {
|
|
3910
|
+
writeFileSync2(opts.outputFile, outputBuffer);
|
|
3911
|
+
if (!opts.json) log.info(`Saved to ${opts.outputFile}`);
|
|
3912
|
+
}
|
|
3913
|
+
if (!opts.json) {
|
|
3914
|
+
console.log("\n");
|
|
3915
|
+
log.success("Call completed");
|
|
3916
|
+
}
|
|
3917
|
+
} catch (err) {
|
|
3918
|
+
if (err.name === "AbortError") {
|
|
3919
|
+
log.error("Call timed out");
|
|
3920
|
+
process.exit(1);
|
|
3921
|
+
}
|
|
3922
|
+
handleError2(err);
|
|
3923
|
+
}
|
|
3924
|
+
});
|
|
3925
|
+
}
|
|
3926
|
+
|
|
3927
|
+
// src/commands/config.ts
|
|
3928
|
+
function handleError3(err) {
|
|
3929
|
+
if (err instanceof PlatformApiError) {
|
|
3930
|
+
log.error(err.message);
|
|
3931
|
+
} else {
|
|
3932
|
+
log.error(err.message);
|
|
3933
|
+
}
|
|
3934
|
+
process.exit(1);
|
|
3935
|
+
}
|
|
3936
|
+
function registerConfigCommand(program2) {
|
|
3937
|
+
program2.command("config <agent>").description("View or update agent A2A settings").option("--show", "Show current settings").option("--capabilities <list>", "Comma-separated capabilities").option("--max-calls-per-hour <n>", "Max calls per hour").option("--max-calls-per-user-per-day <n>", "Max calls per user per day").option("--allow-a2a <bool>", "Enable/disable A2A calls").action(async (agentInput, opts) => {
|
|
3938
|
+
try {
|
|
3939
|
+
const client = createClient();
|
|
3940
|
+
const { id, name } = await resolveAgentId(agentInput, client);
|
|
3941
|
+
const isUpdate = opts.capabilities !== void 0 || opts.maxCallsPerHour !== void 0 || opts.maxCallsPerUserPerDay !== void 0 || opts.allowA2a !== void 0;
|
|
3942
|
+
if (opts.show || !isUpdate) {
|
|
3943
|
+
const agent = await client.get(`/api/developer/agents/${id}`);
|
|
3944
|
+
console.log("");
|
|
3945
|
+
console.log(` ${BOLD}${agent.name}${RESET} \u2014 A2A Settings`);
|
|
3946
|
+
console.log("");
|
|
3947
|
+
console.log(` ${GRAY}Capabilities${RESET} ${agent.capabilities?.length ? agent.capabilities.join(", ") : "(none)"}`);
|
|
3948
|
+
console.log(` ${GRAY}Max calls/hour${RESET} ${agent.rate_limits?.max_calls_per_hour ?? 60}`);
|
|
3949
|
+
console.log(` ${GRAY}Max calls/user/day${RESET} ${agent.rate_limits?.max_calls_per_user_per_day ?? 20}`);
|
|
3950
|
+
console.log(` ${GRAY}A2A enabled${RESET} ${agent.rate_limits?.allow_a2a !== false ? "yes" : "no"}`);
|
|
3951
|
+
console.log("");
|
|
3952
|
+
return;
|
|
3953
|
+
}
|
|
3954
|
+
const updates = {};
|
|
3955
|
+
if (opts.capabilities !== void 0) {
|
|
3956
|
+
updates.capabilities = opts.capabilities.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
3957
|
+
}
|
|
3958
|
+
const rateLimits = {};
|
|
3959
|
+
if (opts.maxCallsPerHour !== void 0) {
|
|
3960
|
+
rateLimits.max_calls_per_hour = parseInt(opts.maxCallsPerHour, 10);
|
|
3961
|
+
}
|
|
3962
|
+
if (opts.maxCallsPerUserPerDay !== void 0) {
|
|
3963
|
+
rateLimits.max_calls_per_user_per_day = parseInt(opts.maxCallsPerUserPerDay, 10);
|
|
3964
|
+
}
|
|
3965
|
+
if (opts.allowA2a !== void 0) {
|
|
3966
|
+
rateLimits.allow_a2a = opts.allowA2a === "true";
|
|
3967
|
+
}
|
|
3968
|
+
if (Object.keys(rateLimits).length > 0) {
|
|
3969
|
+
updates.rate_limits = rateLimits;
|
|
3970
|
+
}
|
|
3971
|
+
await client.patch(`/api/agents/${id}/settings`, updates);
|
|
3972
|
+
log.success(`Settings updated for ${BOLD}${name}${RESET}`);
|
|
3973
|
+
} catch (err) {
|
|
3974
|
+
handleError3(err);
|
|
3975
|
+
}
|
|
3976
|
+
});
|
|
3977
|
+
}
|
|
3978
|
+
|
|
3979
|
+
// src/commands/stats.ts
|
|
3980
|
+
function handleError4(err) {
|
|
3981
|
+
if (err instanceof PlatformApiError) {
|
|
3982
|
+
log.error(err.message);
|
|
3983
|
+
} else {
|
|
3984
|
+
log.error(err.message);
|
|
3985
|
+
}
|
|
3986
|
+
process.exit(1);
|
|
3987
|
+
}
|
|
3988
|
+
function renderBarChart(data) {
|
|
3989
|
+
if (data.length === 0) return ` ${GRAY}No data${RESET}`;
|
|
3990
|
+
const max = Math.max(...data.map((d) => d.count), 1);
|
|
3991
|
+
const barWidth = 20;
|
|
3992
|
+
const lines = [];
|
|
3993
|
+
for (const entry of data) {
|
|
3994
|
+
const filled = Math.round(entry.count / max * barWidth);
|
|
3995
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(barWidth - filled);
|
|
3996
|
+
const dateLabel = entry.date.slice(5);
|
|
3997
|
+
lines.push(` ${GRAY}${dateLabel}${RESET} ${bar} ${entry.count}`);
|
|
3998
|
+
}
|
|
3999
|
+
return lines.join("\n");
|
|
4000
|
+
}
|
|
4001
|
+
function registerStatsCommand(program2) {
|
|
4002
|
+
program2.command("stats").description("View agent call statistics").option("--agent <id-or-name>", "Specific agent").option("--period <period>", "Period: day, week, month", "week").option("--json", "Output raw JSON").action(async (opts) => {
|
|
4003
|
+
try {
|
|
4004
|
+
const client = createClient();
|
|
4005
|
+
if (opts.agent) {
|
|
4006
|
+
const { id, name } = await resolveAgentId(opts.agent, client);
|
|
4007
|
+
const stats = await client.get(
|
|
4008
|
+
`/api/agents/${id}/stats?period=${opts.period}`
|
|
4009
|
+
);
|
|
4010
|
+
if (opts.json) {
|
|
4011
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
4012
|
+
return;
|
|
4013
|
+
}
|
|
4014
|
+
printAgentStats(name, stats);
|
|
4015
|
+
} else {
|
|
4016
|
+
const data = await client.get("/api/developer/agents");
|
|
4017
|
+
if (data.agents.length === 0) {
|
|
4018
|
+
log.info("No agents found.");
|
|
4019
|
+
return;
|
|
4020
|
+
}
|
|
4021
|
+
const allStats = [];
|
|
4022
|
+
for (const agent of data.agents) {
|
|
4023
|
+
try {
|
|
4024
|
+
const stats = await client.get(
|
|
4025
|
+
`/api/agents/${agent.id}/stats?period=${opts.period}`
|
|
4026
|
+
);
|
|
4027
|
+
allStats.push({ name: agent.name, stats });
|
|
4028
|
+
} catch {
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
if (opts.json) {
|
|
4032
|
+
console.log(JSON.stringify(allStats, null, 2));
|
|
4033
|
+
return;
|
|
4034
|
+
}
|
|
4035
|
+
if (allStats.length === 0) {
|
|
4036
|
+
log.info("No stats available.");
|
|
4037
|
+
return;
|
|
4038
|
+
}
|
|
4039
|
+
const table = renderTable(
|
|
4040
|
+
[
|
|
4041
|
+
{ key: "name", label: "AGENT", width: 24 },
|
|
4042
|
+
{ key: "total", label: "TOTAL", width: 10 },
|
|
4043
|
+
{ key: "completed", label: "DONE", width: 10 },
|
|
4044
|
+
{ key: "failed", label: "FAILED", width: 10 },
|
|
4045
|
+
{ key: "avgMs", label: "AVG MS", width: 10 }
|
|
4046
|
+
],
|
|
4047
|
+
allStats.map((s) => ({
|
|
4048
|
+
name: s.name,
|
|
4049
|
+
total: String(s.stats.total_calls),
|
|
4050
|
+
completed: String(s.stats.completed),
|
|
4051
|
+
failed: String(s.stats.failed),
|
|
4052
|
+
avgMs: String(Math.round(s.stats.avg_duration_ms))
|
|
4053
|
+
}))
|
|
4054
|
+
);
|
|
4055
|
+
console.log("");
|
|
4056
|
+
console.log(table);
|
|
4057
|
+
console.log("");
|
|
4058
|
+
}
|
|
4059
|
+
} catch (err) {
|
|
4060
|
+
handleError4(err);
|
|
4061
|
+
}
|
|
4062
|
+
});
|
|
4063
|
+
}
|
|
4064
|
+
function printAgentStats(name, stats) {
|
|
4065
|
+
console.log("");
|
|
4066
|
+
console.log(` ${BOLD}${name}${RESET} \u2014 Call Statistics`);
|
|
4067
|
+
console.log("");
|
|
4068
|
+
console.log(` ${GRAY}Total Calls${RESET} ${stats.total_calls}`);
|
|
4069
|
+
console.log(` ${GRAY}Completed${RESET} ${GREEN}${stats.completed}${RESET}`);
|
|
4070
|
+
console.log(` ${GRAY}Failed${RESET} ${stats.failed}`);
|
|
4071
|
+
console.log(` ${GRAY}Avg Duration${RESET} ${Math.round(stats.avg_duration_ms)}ms`);
|
|
4072
|
+
console.log("");
|
|
4073
|
+
if (stats.calls_by_day.length > 0) {
|
|
4074
|
+
console.log(` ${BOLD}Calls by Day${RESET}`);
|
|
4075
|
+
console.log(renderBarChart(stats.calls_by_day));
|
|
4076
|
+
}
|
|
4077
|
+
console.log("");
|
|
4078
|
+
}
|
|
4079
|
+
|
|
4080
|
+
// src/index.ts
|
|
4081
|
+
var require2 = createRequire(import.meta.url);
|
|
4082
|
+
var { version } = require2("../package.json");
|
|
4083
|
+
program.name("agent-mesh").description("Connect local AI agents to the Agents.Hot platform").version(version).option("-v", "output the version number").on("option:v", () => {
|
|
4084
|
+
console.log(version);
|
|
4085
|
+
process.exit(0);
|
|
4086
|
+
});
|
|
4087
|
+
registerConnectCommand(program);
|
|
4088
|
+
registerLoginCommand(program);
|
|
4089
|
+
registerStatusCommand(program);
|
|
4090
|
+
registerListCommand(program);
|
|
4091
|
+
registerStartCommand(program);
|
|
4092
|
+
registerStopCommand(program);
|
|
4093
|
+
registerRestartCommand(program);
|
|
4094
|
+
registerLogsCommand(program);
|
|
4095
|
+
registerRemoveCommand(program);
|
|
4096
|
+
registerOpenCommand(program);
|
|
4097
|
+
registerInstallCommand(program);
|
|
4098
|
+
registerUninstallCommand(program);
|
|
4099
|
+
registerAgentsCommand(program);
|
|
4100
|
+
registerChatCommand(program);
|
|
4101
|
+
registerSkillsCommand(program);
|
|
4102
|
+
registerDiscoverCommand(program);
|
|
4103
|
+
registerCallCommand(program);
|
|
4104
|
+
registerConfigCommand(program);
|
|
4105
|
+
registerStatsCommand(program);
|
|
4106
|
+
program.parse();
|