botapp-cli 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/bot.js +3253 -0
- package/dist/bin/bot.js.map +1 -0
- package/dist/index.js +3251 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3251 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { Command as Command19 } from "commander";
|
|
3
|
+
import pc20 from "picocolors";
|
|
4
|
+
|
|
5
|
+
// src/commands/server.ts
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import { spawn } from "child_process";
|
|
8
|
+
import { resolve } from "path";
|
|
9
|
+
import { existsSync as existsSync2 } from "fs";
|
|
10
|
+
import pc from "picocolors";
|
|
11
|
+
|
|
12
|
+
// src/config/profile.ts
|
|
13
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
import { homedir } from "os";
|
|
16
|
+
import { parse, stringify } from "yaml";
|
|
17
|
+
var CONFIG_DIR = join(homedir(), ".botapp");
|
|
18
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.yaml");
|
|
19
|
+
function defaultConfig() {
|
|
20
|
+
return {
|
|
21
|
+
active_profile: "local",
|
|
22
|
+
profiles: {
|
|
23
|
+
local: {
|
|
24
|
+
server: "http://localhost:7100"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function loadProfile() {
|
|
30
|
+
try {
|
|
31
|
+
if (!existsSync(CONFIG_FILE)) return defaultConfig();
|
|
32
|
+
const raw = readFileSync(CONFIG_FILE, "utf-8");
|
|
33
|
+
const parsed = parse(raw);
|
|
34
|
+
return { ...defaultConfig(), ...parsed };
|
|
35
|
+
} catch {
|
|
36
|
+
return defaultConfig();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function saveProfile(config) {
|
|
40
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
41
|
+
writeFileSync(CONFIG_FILE, stringify(config), "utf-8");
|
|
42
|
+
}
|
|
43
|
+
function getActiveProfile() {
|
|
44
|
+
const config = loadProfile();
|
|
45
|
+
return config.profiles[config.active_profile] ?? config.profiles.local ?? { server: "http://localhost:7100" };
|
|
46
|
+
}
|
|
47
|
+
function updateActiveProfile(updates) {
|
|
48
|
+
const config = loadProfile();
|
|
49
|
+
const name = config.active_profile;
|
|
50
|
+
config.profiles[name] = { ...config.profiles[name], ...updates };
|
|
51
|
+
saveProfile(config);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/config/server.ts
|
|
55
|
+
function resolveServerUrl(flagUrl) {
|
|
56
|
+
if (flagUrl) return flagUrl.replace(/\/$/, "");
|
|
57
|
+
const env = process.env.BOTAPP_SERVER;
|
|
58
|
+
if (env) return env.replace(/\/$/, "");
|
|
59
|
+
const profile = getActiveProfile();
|
|
60
|
+
return profile.server.replace(/\/$/, "");
|
|
61
|
+
}
|
|
62
|
+
function resolveToken(flagToken) {
|
|
63
|
+
if (flagToken) return flagToken;
|
|
64
|
+
const env = process.env.BOTAPP_TOKEN;
|
|
65
|
+
if (env) return env;
|
|
66
|
+
return getActiveProfile().token;
|
|
67
|
+
}
|
|
68
|
+
function authHeaders(token) {
|
|
69
|
+
const headers = {
|
|
70
|
+
"Content-Type": "application/json"
|
|
71
|
+
};
|
|
72
|
+
if (token) {
|
|
73
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
74
|
+
}
|
|
75
|
+
return headers;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/commands/server.ts
|
|
79
|
+
var serverCommand = new Command("server").description("Manage the local botapp server process");
|
|
80
|
+
serverCommand.command("start").description("Start the local server process (no auth setup)").option("-p, --port <port>", "Port for local server", "7100").option("--background", "Run in background", false).action(async (opts) => {
|
|
81
|
+
const serverUrl = `http://localhost:${opts.port}`;
|
|
82
|
+
try {
|
|
83
|
+
const res = await fetch(`${serverUrl}/health`);
|
|
84
|
+
if (res.ok) {
|
|
85
|
+
console.log(pc.yellow("Server is already running at"), pc.cyan(serverUrl));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
90
|
+
const serverEntry = findServerEntry();
|
|
91
|
+
if (!serverEntry) {
|
|
92
|
+
console.error(
|
|
93
|
+
pc.red("Could not find the server package.\n") + `
|
|
94
|
+
This command requires the botapp monorepo source.
|
|
95
|
+
If you installed the CLI from npm, use ${pc.cyan("bot launch")} and
|
|
96
|
+
pick option 2 (cloud) or 3 (custom) instead.`
|
|
97
|
+
);
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
console.log(pc.blue("Starting local server..."));
|
|
102
|
+
const child = spawn("node", ["--import", "tsx", serverEntry], {
|
|
103
|
+
env: { ...process.env, PORT: opts.port },
|
|
104
|
+
stdio: opts.background ? "ignore" : "inherit",
|
|
105
|
+
detached: opts.background
|
|
106
|
+
});
|
|
107
|
+
if (opts.background) {
|
|
108
|
+
child.unref();
|
|
109
|
+
console.log(pc.green("Server started in background"), `(PID: ${child.pid})`);
|
|
110
|
+
}
|
|
111
|
+
for (let i = 0; i < 30; i++) {
|
|
112
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
113
|
+
try {
|
|
114
|
+
const res = await fetch(`${serverUrl}/health`);
|
|
115
|
+
if (res.ok) {
|
|
116
|
+
if (opts.background) console.log(pc.green("Server ready at"), pc.cyan(serverUrl));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (opts.background) console.log(pc.yellow("Server may still be starting..."));
|
|
123
|
+
});
|
|
124
|
+
serverCommand.command("status").description("Check if the server is running").action(async () => {
|
|
125
|
+
const serverUrl = resolveServerUrl();
|
|
126
|
+
try {
|
|
127
|
+
const res = await fetch(`${serverUrl}/health`);
|
|
128
|
+
const data = await res.json();
|
|
129
|
+
console.log(pc.green("Server is running"));
|
|
130
|
+
console.log(` URL: ${pc.cyan(serverUrl)}`);
|
|
131
|
+
console.log(` Mode: ${data.mode}`);
|
|
132
|
+
console.log(` Apps: ${data.apps}`);
|
|
133
|
+
} catch {
|
|
134
|
+
console.log(pc.red("Server is not running"));
|
|
135
|
+
process.exitCode = 1;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
serverCommand.command("stop").description("Stop the botapp server").action(async () => {
|
|
139
|
+
console.log(pc.yellow("Server stop not yet implemented (kill the process manually)"));
|
|
140
|
+
});
|
|
141
|
+
function findServerEntry() {
|
|
142
|
+
const candidates = [
|
|
143
|
+
resolve(process.cwd(), "packages/server/src/index.ts"),
|
|
144
|
+
resolve(process.cwd(), "../server/src/index.ts"),
|
|
145
|
+
resolve(process.cwd(), "../../packages/server/src/index.ts")
|
|
146
|
+
];
|
|
147
|
+
for (const c of candidates) {
|
|
148
|
+
if (existsSync2(c)) return c;
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/commands/launch.ts
|
|
154
|
+
import { Command as Command3 } from "commander";
|
|
155
|
+
import { spawn as spawn3 } from "child_process";
|
|
156
|
+
import { resolve as resolve3, join as join3 } from "path";
|
|
157
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, openSync, writeFileSync as writeFileSync3, readFileSync as readFileSync3 } from "fs";
|
|
158
|
+
import { homedir as homedir3 } from "os";
|
|
159
|
+
import { createInterface as createInterface2 } from "readline";
|
|
160
|
+
import { hostname } from "os";
|
|
161
|
+
import pc4 from "picocolors";
|
|
162
|
+
|
|
163
|
+
// src/config/daemon.ts
|
|
164
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
165
|
+
import { homedir as homedir2 } from "os";
|
|
166
|
+
import { join as join2 } from "path";
|
|
167
|
+
import { parse as parse2, stringify as stringify2 } from "yaml";
|
|
168
|
+
var CONFIG_DIR2 = join2(homedir2(), ".botapp");
|
|
169
|
+
var DAEMON_FILE = join2(CONFIG_DIR2, "daemon.yaml");
|
|
170
|
+
function loadDaemonProfile() {
|
|
171
|
+
try {
|
|
172
|
+
if (!existsSync3(DAEMON_FILE)) return null;
|
|
173
|
+
const parsed = parse2(readFileSync2(DAEMON_FILE, "utf-8"));
|
|
174
|
+
if (!parsed?.server || !parsed.daemonId || !parsed.token) return null;
|
|
175
|
+
return {
|
|
176
|
+
server: parsed.server.replace(/\/$/, ""),
|
|
177
|
+
daemonId: parsed.daemonId,
|
|
178
|
+
daemonName: parsed.daemonName ?? parsed.daemonId,
|
|
179
|
+
token: parsed.token
|
|
180
|
+
};
|
|
181
|
+
} catch {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function saveDaemonProfile(profile) {
|
|
186
|
+
mkdirSync2(CONFIG_DIR2, { recursive: true });
|
|
187
|
+
writeFileSync2(DAEMON_FILE, stringify2(profile), "utf-8");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/auth/browser-auth.ts
|
|
191
|
+
import { createServer } from "http";
|
|
192
|
+
import { randomBytes } from "crypto";
|
|
193
|
+
import { spawn as spawn2 } from "child_process";
|
|
194
|
+
import pc2 from "picocolors";
|
|
195
|
+
var OK_HTML = `<!doctype html>
|
|
196
|
+
<meta charset="utf-8">
|
|
197
|
+
<title>botapp \xB7 Authentication successful</title>
|
|
198
|
+
<style>
|
|
199
|
+
:root { color-scheme: dark; }
|
|
200
|
+
html, body { height: 100%; margin: 0; }
|
|
201
|
+
body {
|
|
202
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', Roboto, sans-serif;
|
|
203
|
+
background: #0b0b0d;
|
|
204
|
+
color: #e9e9ec;
|
|
205
|
+
display: grid;
|
|
206
|
+
place-items: center;
|
|
207
|
+
-webkit-font-smoothing: antialiased;
|
|
208
|
+
}
|
|
209
|
+
.card {
|
|
210
|
+
width: min(92vw, 520px);
|
|
211
|
+
background: #151518;
|
|
212
|
+
border: 1px solid #242428;
|
|
213
|
+
border-radius: 16px;
|
|
214
|
+
padding: 48px 40px 40px;
|
|
215
|
+
text-align: center;
|
|
216
|
+
}
|
|
217
|
+
.check {
|
|
218
|
+
width: 64px; height: 64px; margin: 0 auto 22px;
|
|
219
|
+
border-radius: 50%;
|
|
220
|
+
background: rgba(34, 197, 94, 0.14);
|
|
221
|
+
color: #22c55e;
|
|
222
|
+
display: grid; place-items: center;
|
|
223
|
+
font-size: 30px;
|
|
224
|
+
}
|
|
225
|
+
.glyph { color: #6b6b72; font-size: 20px; margin: 8px 0 12px; }
|
|
226
|
+
h1 { font-size: 28px; font-weight: 700; margin: 0 0 10px; letter-spacing: -0.01em; }
|
|
227
|
+
p { color: #9a9aa1; margin: 0; font-size: 15px; line-height: 1.5; }
|
|
228
|
+
.dim { color: #6b6b72; font-size: 13px; margin-top: 14px; }
|
|
229
|
+
</style>
|
|
230
|
+
<div class="card">
|
|
231
|
+
<div class="check">\u2713</div>
|
|
232
|
+
<div class="glyph">\u2731</div>
|
|
233
|
+
<h1>Authentication successful</h1>
|
|
234
|
+
<p>You can close this tab and return to the terminal.</p>
|
|
235
|
+
<p class="dim">Your CLI session is now authenticated.</p>
|
|
236
|
+
</div>`;
|
|
237
|
+
async function runBrowserAuth(opts) {
|
|
238
|
+
const state = randomBytes(24).toString("base64url");
|
|
239
|
+
const timeoutMs = opts.timeoutMs ?? 10 * 6e4;
|
|
240
|
+
const { port, waitForCallback, shutdown } = await startLoopback(state);
|
|
241
|
+
const cliCallback = `http://127.0.0.1:${port}/callback`;
|
|
242
|
+
const params = new URLSearchParams({
|
|
243
|
+
cli_callback: cliCallback,
|
|
244
|
+
cli_state: state,
|
|
245
|
+
scope: opts.scope
|
|
246
|
+
});
|
|
247
|
+
if (opts.name) params.set("name", opts.name);
|
|
248
|
+
const approvalUrl = `${opts.appUrl.replace(/\/$/, "")}/cli-auth?${params.toString()}`;
|
|
249
|
+
console.log(pc2.dim("Opening browser to authenticate..."));
|
|
250
|
+
console.log(pc2.dim("If the browser didn't open, visit:"));
|
|
251
|
+
console.log(` ${pc2.cyan(approvalUrl)}`);
|
|
252
|
+
console.log();
|
|
253
|
+
console.log(pc2.dim("Waiting for authentication..."));
|
|
254
|
+
openUrl(approvalUrl);
|
|
255
|
+
try {
|
|
256
|
+
const payload = await Promise.race([
|
|
257
|
+
waitForCallback(),
|
|
258
|
+
new Promise((_, reject) => {
|
|
259
|
+
setTimeout(
|
|
260
|
+
() => reject(new Error("timed out waiting for authentication")),
|
|
261
|
+
timeoutMs
|
|
262
|
+
);
|
|
263
|
+
})
|
|
264
|
+
]);
|
|
265
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
266
|
+
return {
|
|
267
|
+
scope: opts.scope,
|
|
268
|
+
serverUrl: payload.serverUrl ?? opts.serverUrl,
|
|
269
|
+
name: payload.name,
|
|
270
|
+
pairingToken: payload.pairingToken,
|
|
271
|
+
userId: payload.userId,
|
|
272
|
+
userEmail: payload.userEmail,
|
|
273
|
+
userToken: payload.userToken,
|
|
274
|
+
agentId: payload.agentId,
|
|
275
|
+
agentName: payload.agentName,
|
|
276
|
+
daemonPairingToken: payload.daemonPairingToken
|
|
277
|
+
};
|
|
278
|
+
} finally {
|
|
279
|
+
shutdown();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
async function startLoopback(expectedState) {
|
|
283
|
+
let resolveCb = () => {
|
|
284
|
+
};
|
|
285
|
+
let rejectCb = () => {
|
|
286
|
+
};
|
|
287
|
+
let settled = false;
|
|
288
|
+
const callbackPromise = new Promise((resolve7, reject) => {
|
|
289
|
+
resolveCb = (p) => {
|
|
290
|
+
if (settled) return;
|
|
291
|
+
settled = true;
|
|
292
|
+
resolve7(p);
|
|
293
|
+
};
|
|
294
|
+
rejectCb = (e) => {
|
|
295
|
+
if (settled) return;
|
|
296
|
+
settled = true;
|
|
297
|
+
reject(e);
|
|
298
|
+
};
|
|
299
|
+
});
|
|
300
|
+
const server = createServer((req, res) => {
|
|
301
|
+
void handleLoopback(req, res, expectedState, resolveCb, rejectCb);
|
|
302
|
+
});
|
|
303
|
+
await new Promise((resolve7, reject) => {
|
|
304
|
+
server.once("error", reject);
|
|
305
|
+
server.listen(0, "127.0.0.1", () => {
|
|
306
|
+
server.removeListener("error", reject);
|
|
307
|
+
resolve7();
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
const address = server.address();
|
|
311
|
+
if (!address || typeof address === "string") {
|
|
312
|
+
server.close();
|
|
313
|
+
throw new Error("could not bind loopback port");
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
port: address.port,
|
|
317
|
+
waitForCallback: () => callbackPromise,
|
|
318
|
+
shutdown: () => {
|
|
319
|
+
rejectCb(new Error("loopback shut down"));
|
|
320
|
+
server.close();
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
async function handleLoopback(req, res, expectedState, onOk, onErr) {
|
|
325
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
326
|
+
if (!url.pathname.startsWith("/callback")) {
|
|
327
|
+
res.statusCode = 404;
|
|
328
|
+
res.end("not found");
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (req.method === "GET") {
|
|
332
|
+
const state = url.searchParams.get("state");
|
|
333
|
+
if (!state || state !== expectedState) {
|
|
334
|
+
res.statusCode = 400;
|
|
335
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
336
|
+
res.end(errorHtml("State mismatch. Re-run the CLI and try again."), () => {
|
|
337
|
+
onErr(new Error("callback state mismatch"));
|
|
338
|
+
});
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
res.statusCode = 200;
|
|
342
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
343
|
+
res.end(OK_HTML, () => {
|
|
344
|
+
onOk({
|
|
345
|
+
serverUrl: url.searchParams.get("serverUrl") ?? void 0,
|
|
346
|
+
name: url.searchParams.get("name") ?? void 0,
|
|
347
|
+
pairingToken: url.searchParams.get("pairingToken") ?? void 0,
|
|
348
|
+
userId: url.searchParams.get("userId") ?? void 0,
|
|
349
|
+
userEmail: url.searchParams.get("userEmail") ?? void 0,
|
|
350
|
+
userToken: url.searchParams.get("userToken") ?? void 0,
|
|
351
|
+
agentId: url.searchParams.get("agentId") ?? void 0,
|
|
352
|
+
agentName: url.searchParams.get("agentName") ?? void 0,
|
|
353
|
+
daemonPairingToken: url.searchParams.get("daemonPairingToken") ?? void 0
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const origin = req.headers.origin ?? "*";
|
|
359
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
360
|
+
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
361
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
362
|
+
res.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
363
|
+
res.setHeader("Vary", "Origin");
|
|
364
|
+
if (req.method === "OPTIONS") {
|
|
365
|
+
res.statusCode = 204;
|
|
366
|
+
res.end();
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (req.method !== "POST") {
|
|
370
|
+
res.statusCode = 405;
|
|
371
|
+
res.end("method not allowed");
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
try {
|
|
375
|
+
const body = await readJsonBody(req);
|
|
376
|
+
if (!body || typeof body !== "object") {
|
|
377
|
+
res.statusCode = 400;
|
|
378
|
+
res.end("invalid body");
|
|
379
|
+
onErr(new Error("invalid callback body"));
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const parsed = body;
|
|
383
|
+
if (typeof parsed.state !== "string" || parsed.state !== expectedState) {
|
|
384
|
+
res.statusCode = 400;
|
|
385
|
+
res.end("state mismatch");
|
|
386
|
+
onErr(new Error("callback state mismatch"));
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
res.statusCode = 200;
|
|
390
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
391
|
+
res.end(OK_HTML);
|
|
392
|
+
onOk({
|
|
393
|
+
serverUrl: asString(parsed.serverUrl),
|
|
394
|
+
name: asString(parsed.name),
|
|
395
|
+
pairingToken: asString(parsed.pairingToken),
|
|
396
|
+
userId: asString(parsed.userId),
|
|
397
|
+
userEmail: asString(parsed.userEmail),
|
|
398
|
+
userToken: asString(parsed.userToken),
|
|
399
|
+
agentId: asString(parsed.agentId),
|
|
400
|
+
agentName: asString(parsed.agentName),
|
|
401
|
+
daemonPairingToken: asString(parsed.daemonPairingToken)
|
|
402
|
+
});
|
|
403
|
+
} catch (e) {
|
|
404
|
+
res.statusCode = 500;
|
|
405
|
+
res.end("error");
|
|
406
|
+
onErr(e);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function errorHtml(msg) {
|
|
410
|
+
const safe = msg.replace(/[<>&]/g, (c) => ({ "<": "<", ">": ">", "&": "&" })[c] ?? c);
|
|
411
|
+
return `<!doctype html><meta charset="utf-8"><title>botapp \xB7 CLI auth failed</title>
|
|
412
|
+
<style>body{font-family:ui-monospace,Menlo,monospace;background:#0b0b0d;color:#eee;display:grid;place-items:center;min-height:100vh;margin:0}.c{border:1px solid #333;padding:2rem 2.5rem;max-width:520px}</style>
|
|
413
|
+
<div class="c"><h1>CLI auth failed</h1><p>${safe}</p></div>`;
|
|
414
|
+
}
|
|
415
|
+
function asString(v) {
|
|
416
|
+
return typeof v === "string" ? v : void 0;
|
|
417
|
+
}
|
|
418
|
+
function readJsonBody(req) {
|
|
419
|
+
return new Promise((resolve7, reject) => {
|
|
420
|
+
const chunks = [];
|
|
421
|
+
let total = 0;
|
|
422
|
+
req.on("data", (c) => {
|
|
423
|
+
total += c.length;
|
|
424
|
+
if (total > 64 * 1024) {
|
|
425
|
+
reject(new Error("payload too large"));
|
|
426
|
+
req.destroy();
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
chunks.push(c);
|
|
430
|
+
});
|
|
431
|
+
req.on("end", () => {
|
|
432
|
+
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
433
|
+
if (!raw) return resolve7(null);
|
|
434
|
+
try {
|
|
435
|
+
resolve7(JSON.parse(raw));
|
|
436
|
+
} catch (e) {
|
|
437
|
+
reject(e);
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
req.on("error", reject);
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
function openUrl(url) {
|
|
444
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
445
|
+
const args = process.platform === "win32" ? ["/c", "start", '""', url] : [url];
|
|
446
|
+
try {
|
|
447
|
+
spawn2(cmd, args, { stdio: "ignore", detached: true }).unref();
|
|
448
|
+
} catch {
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// src/commands/daemon-agent-config.ts
|
|
453
|
+
import { spawnSync } from "child_process";
|
|
454
|
+
import { createInterface } from "readline/promises";
|
|
455
|
+
import { stdin as input, stdout as output } from "process";
|
|
456
|
+
import { resolve as resolve2 } from "path";
|
|
457
|
+
import { Command as Command2 } from "commander";
|
|
458
|
+
import pc3 from "picocolors";
|
|
459
|
+
var LOCAL_AGENT_SPECS = [
|
|
460
|
+
{ name: "codex", kind: "codex", command: "codex" },
|
|
461
|
+
{ name: "claude", kind: "claude-code", command: "claude" },
|
|
462
|
+
{ name: "openclaw", kind: "openclaw", command: "openclaw" },
|
|
463
|
+
{ name: "hermes", kind: "hermes-agent", command: "hermes" }
|
|
464
|
+
];
|
|
465
|
+
async function registerAllDetectedAgents(input2) {
|
|
466
|
+
const detected = scanLocalDaemonAgents();
|
|
467
|
+
if (detected.length === 0) return { created: [], skipped: [], detected: 0 };
|
|
468
|
+
const existing = await daemonSelfRequest(
|
|
469
|
+
input2.server,
|
|
470
|
+
input2.token,
|
|
471
|
+
"/api/daemon/self/agents"
|
|
472
|
+
);
|
|
473
|
+
const registered = existing.agents ?? [];
|
|
474
|
+
const created = [];
|
|
475
|
+
const skipped = [];
|
|
476
|
+
const cwd = resolve2(input2.cwd);
|
|
477
|
+
for (const candidate of detected) {
|
|
478
|
+
if (registered.find((a) => a.name === candidate.name)) {
|
|
479
|
+
skipped.push(candidate.name);
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
try {
|
|
483
|
+
await daemonSelfRequest(
|
|
484
|
+
input2.server,
|
|
485
|
+
input2.token,
|
|
486
|
+
"/api/daemon/self/agents",
|
|
487
|
+
{
|
|
488
|
+
method: "POST",
|
|
489
|
+
body: {
|
|
490
|
+
name: candidate.name,
|
|
491
|
+
kind: candidate.kind,
|
|
492
|
+
command: candidate.command,
|
|
493
|
+
args: [],
|
|
494
|
+
cwd,
|
|
495
|
+
env: null
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
);
|
|
499
|
+
created.push(candidate.name);
|
|
500
|
+
} catch {
|
|
501
|
+
skipped.push(candidate.name);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return { created, skipped, detected: detected.length };
|
|
505
|
+
}
|
|
506
|
+
function createDaemonAgentConfigCommand() {
|
|
507
|
+
return new Command2("config").description("Scan local agent CLIs and register selected daemon agents").option("--all", "Register every detected agent without prompting").option("--agent <name...>", "Register only these detected agent names, e.g. codex claude").option("--cwd <cwd>", "Working directory stored on registered daemon agents", process.cwd()).action(async (opts) => {
|
|
508
|
+
await configureDaemonAgents({
|
|
509
|
+
all: Boolean(opts.all),
|
|
510
|
+
names: Array.isArray(opts.agent) ? opts.agent : [],
|
|
511
|
+
cwd: opts.cwd
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
async function configureDaemonAgents(inputOpts) {
|
|
516
|
+
const profile = loadDaemonProfile();
|
|
517
|
+
if (!profile) {
|
|
518
|
+
console.error(pc3.red("No paired daemon found. Run `bot pair` first."));
|
|
519
|
+
process.exitCode = 1;
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
const detected = scanLocalDaemonAgents();
|
|
523
|
+
if (detected.length === 0) {
|
|
524
|
+
console.log(pc3.yellow("No supported local agent CLIs were found on PATH."));
|
|
525
|
+
console.log(pc3.dim("Scanned: codex, claude, openclaw, hermes"));
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const existing = await daemonSelfRequest(
|
|
529
|
+
profile.server,
|
|
530
|
+
profile.token,
|
|
531
|
+
"/api/daemon/self/agents"
|
|
532
|
+
);
|
|
533
|
+
const registered = existing.agents ?? [];
|
|
534
|
+
const selected = await selectCandidates(detected, inputOpts);
|
|
535
|
+
if (selected.length === 0) {
|
|
536
|
+
console.log(pc3.dim("No agents selected."));
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const cwd = resolve2(inputOpts.cwd);
|
|
540
|
+
let created = 0;
|
|
541
|
+
let skipped = 0;
|
|
542
|
+
for (const candidate of selected) {
|
|
543
|
+
const alreadyRegistered = registered.find((agent) => agent.name === candidate.name);
|
|
544
|
+
if (alreadyRegistered) {
|
|
545
|
+
skipped += 1;
|
|
546
|
+
console.log(
|
|
547
|
+
pc3.dim(
|
|
548
|
+
`Skipping ${candidate.name}: already registered as ${alreadyRegistered.id}`
|
|
549
|
+
)
|
|
550
|
+
);
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
const data = await daemonSelfRequest(
|
|
554
|
+
profile.server,
|
|
555
|
+
profile.token,
|
|
556
|
+
"/api/daemon/self/agents",
|
|
557
|
+
{
|
|
558
|
+
method: "POST",
|
|
559
|
+
body: {
|
|
560
|
+
name: candidate.name,
|
|
561
|
+
kind: candidate.kind,
|
|
562
|
+
command: candidate.command,
|
|
563
|
+
args: [],
|
|
564
|
+
cwd,
|
|
565
|
+
env: null
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
);
|
|
569
|
+
created += 1;
|
|
570
|
+
console.log(
|
|
571
|
+
pc3.green(`Registered ${pc3.bold(data.agent.name)}`) + pc3.dim(` (${data.agent.kind}, ${data.agent.command}, cwd ${cwd})`)
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
console.log(pc3.green(`Done. Registered ${created}, skipped ${skipped}.`));
|
|
575
|
+
}
|
|
576
|
+
function scanLocalDaemonAgents() {
|
|
577
|
+
const candidates = [];
|
|
578
|
+
for (const spec of LOCAL_AGENT_SPECS) {
|
|
579
|
+
const path = findExecutable(spec.command);
|
|
580
|
+
if (!path) continue;
|
|
581
|
+
candidates.push({ ...spec, path });
|
|
582
|
+
}
|
|
583
|
+
return candidates;
|
|
584
|
+
}
|
|
585
|
+
function findExecutable(command) {
|
|
586
|
+
const lookupCommand = process.platform === "win32" ? "where" : "which";
|
|
587
|
+
const result = spawnSync(lookupCommand, [command], {
|
|
588
|
+
encoding: "utf-8",
|
|
589
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
590
|
+
});
|
|
591
|
+
if (result.status !== 0) return null;
|
|
592
|
+
return result.stdout.split(/\r?\n/).find((line) => line.trim())?.trim() ?? null;
|
|
593
|
+
}
|
|
594
|
+
async function selectCandidates(detected, opts) {
|
|
595
|
+
if (opts.all) return detected;
|
|
596
|
+
const requested = new Set(opts.names.map((name) => name.toLowerCase()));
|
|
597
|
+
if (requested.size > 0) {
|
|
598
|
+
const selected = detected.filter((candidate) => requested.has(candidate.name.toLowerCase()));
|
|
599
|
+
const missing = [...requested].filter(
|
|
600
|
+
(name) => !detected.some((candidate) => candidate.name.toLowerCase() === name)
|
|
601
|
+
);
|
|
602
|
+
for (const name of missing) {
|
|
603
|
+
console.log(pc3.yellow(`Not detected on PATH: ${name}`));
|
|
604
|
+
}
|
|
605
|
+
return selected;
|
|
606
|
+
}
|
|
607
|
+
if (!process.stdin.isTTY) {
|
|
608
|
+
console.error(pc3.red("Interactive selection requires a TTY. Use --all or --agent <name...>."));
|
|
609
|
+
process.exitCode = 1;
|
|
610
|
+
return [];
|
|
611
|
+
}
|
|
612
|
+
console.log(pc3.bold("Detected local agents:"));
|
|
613
|
+
detected.forEach((candidate, index) => {
|
|
614
|
+
console.log(
|
|
615
|
+
` ${index + 1}. ${pc3.bold(candidate.name)} ` + pc3.dim(`${candidate.kind} / ${candidate.command} / ${candidate.path}`)
|
|
616
|
+
);
|
|
617
|
+
});
|
|
618
|
+
const rl = createInterface({ input, output });
|
|
619
|
+
try {
|
|
620
|
+
const answer = await rl.question(
|
|
621
|
+
'Select agents to register (comma-separated numbers, "all", or empty for all): '
|
|
622
|
+
);
|
|
623
|
+
const value = answer.trim().toLowerCase();
|
|
624
|
+
if (!value || value === "all" || value === "a") return detected;
|
|
625
|
+
if (value === "none" || value === "n") return [];
|
|
626
|
+
const indexes = new Set(
|
|
627
|
+
value.split(",").map((part) => Number(part.trim())).filter((index) => Number.isInteger(index) && index >= 1 && index <= detected.length)
|
|
628
|
+
);
|
|
629
|
+
return detected.filter((_, index) => indexes.has(index + 1));
|
|
630
|
+
} finally {
|
|
631
|
+
rl.close();
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
async function daemonSelfRequest(server, token, path, opts) {
|
|
635
|
+
const headers = {
|
|
636
|
+
Authorization: `Bearer ${token}`
|
|
637
|
+
};
|
|
638
|
+
if (opts?.body !== void 0) {
|
|
639
|
+
headers["Content-Type"] = "application/json";
|
|
640
|
+
}
|
|
641
|
+
const res = await fetch(`${server}${path}`, {
|
|
642
|
+
method: opts?.method ?? "GET",
|
|
643
|
+
headers,
|
|
644
|
+
body: opts?.body !== void 0 ? JSON.stringify(opts.body) : void 0
|
|
645
|
+
});
|
|
646
|
+
const data = await res.json().catch(() => ({}));
|
|
647
|
+
if (!res.ok) {
|
|
648
|
+
throw new Error(data.error ?? data.message ?? res.statusText);
|
|
649
|
+
}
|
|
650
|
+
return data;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// src/commands/launch.ts
|
|
654
|
+
var DEFAULT_SERVER_URL = "https://api.botapp.ai";
|
|
655
|
+
var DEFAULT_APP_URL = "https://botapp.ai";
|
|
656
|
+
function ask(question) {
|
|
657
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
658
|
+
return new Promise((res) => {
|
|
659
|
+
rl.question(question, (answer) => {
|
|
660
|
+
rl.close();
|
|
661
|
+
res(answer.trim());
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
var launchCommand = new Command3("launch").alias("start").description("Set up this CLI (auth + default agent + daemon pair + agent register)").option("-p, --port <port>", "Port for local server", "7100").option("--background", "Run local server in background", false).option("-s, --server <url>", "Server URL (skip the interactive picker)").option("--app-url <url>", "Dashboard URL (defaults to the server URL)").option("--no-browser", "Error instead of opening a browser").option("--no-agents", "Skip auto-registering local agent CLIs").option("--agent-cwd <cwd>", "Working directory stored on registered daemon agents", process.cwd()).option("--no-start", "Skip starting the daemon process in background").action(async (opts) => {
|
|
666
|
+
if (opts.server) {
|
|
667
|
+
await launchAgainstServer(opts.server, opts.appUrl, opts);
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
console.log(pc4.bold("Where do you want to run botapp?\n"));
|
|
671
|
+
console.log(` ${pc4.cyan("1)")} Local ${pc4.dim("\u2014 start a server on this machine")}`);
|
|
672
|
+
console.log(` ${pc4.cyan("2)")} Cloud ${pc4.dim(`\u2014 connect to ${DEFAULT_APP_URL}`)}`);
|
|
673
|
+
console.log(` ${pc4.cyan("3)")} Custom ${pc4.dim("\u2014 enter your own server URL")}
|
|
674
|
+
`);
|
|
675
|
+
const choice = await ask(`${pc4.bold("Select [1/2/3]:")} `);
|
|
676
|
+
switch (choice) {
|
|
677
|
+
case "1":
|
|
678
|
+
await launchLocal(opts);
|
|
679
|
+
break;
|
|
680
|
+
case "2":
|
|
681
|
+
await launchAgainstServer(DEFAULT_SERVER_URL, DEFAULT_APP_URL, opts);
|
|
682
|
+
break;
|
|
683
|
+
case "3": {
|
|
684
|
+
const url = await ask(`
|
|
685
|
+
${pc4.bold("Server URL:")} `);
|
|
686
|
+
if (!url) {
|
|
687
|
+
console.error(pc4.red("No URL provided"));
|
|
688
|
+
process.exitCode = 1;
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
await launchAgainstServer(url.replace(/\/$/, ""), void 0, opts);
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
default:
|
|
695
|
+
console.error(pc4.red(`Invalid choice: ${choice}`));
|
|
696
|
+
process.exitCode = 1;
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
async function launchLocal(opts) {
|
|
700
|
+
const serverUrl = `http://localhost:${opts.port}`;
|
|
701
|
+
if (!await isServerUp(serverUrl)) {
|
|
702
|
+
const spawned = await startLocalServer(opts);
|
|
703
|
+
if (!spawned) {
|
|
704
|
+
process.exitCode = 1;
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
} else {
|
|
708
|
+
console.log(pc4.yellow("Server already running at"), pc4.cyan(serverUrl));
|
|
709
|
+
}
|
|
710
|
+
await launchAgainstServer(serverUrl, serverUrl, opts);
|
|
711
|
+
}
|
|
712
|
+
async function launchAgainstServer(serverUrl, appUrl, opts) {
|
|
713
|
+
console.log(pc4.dim(`
|
|
714
|
+
Configured for ${serverUrl}`));
|
|
715
|
+
const health = await fetchHealth(serverUrl);
|
|
716
|
+
if (!health) {
|
|
717
|
+
console.error(pc4.red(`Cannot reach ${serverUrl}`));
|
|
718
|
+
process.exitCode = 1;
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
const profileName = pickProfileName(serverUrl);
|
|
722
|
+
if (health.mode === "local" && health.token) {
|
|
723
|
+
const config2 = loadProfile();
|
|
724
|
+
config2.profiles[profileName] = {
|
|
725
|
+
server: serverUrl,
|
|
726
|
+
app_url: appUrl ?? serverUrl,
|
|
727
|
+
token: health.token,
|
|
728
|
+
user_id: "local",
|
|
729
|
+
agent_id: "local-agent"
|
|
730
|
+
};
|
|
731
|
+
config2.active_profile = profileName;
|
|
732
|
+
saveProfile(config2);
|
|
733
|
+
console.log(pc4.green("Logged in (local mode)"));
|
|
734
|
+
console.log(` Profile: ${pc4.bold(profileName)}`);
|
|
735
|
+
console.log(` Server: ${pc4.cyan(serverUrl)}`);
|
|
736
|
+
const paired = await tryLocalDaemonPair(serverUrl, hostname());
|
|
737
|
+
if (paired) {
|
|
738
|
+
console.log(
|
|
739
|
+
` Daemon: ${pc4.bold(paired.name)} ${pc4.dim(`(${paired.id})`)}`
|
|
740
|
+
);
|
|
741
|
+
await autoRegisterAgents(opts);
|
|
742
|
+
await autoStartDaemon(opts, serverUrl, paired.id);
|
|
743
|
+
} else {
|
|
744
|
+
console.log(pc4.yellow(` Daemon: pairing failed \u2014 run \`bot pair\` later`));
|
|
745
|
+
}
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
if (opts.noBrowser) {
|
|
749
|
+
console.error(pc4.red("Hosted server needs browser auth but --no-browser was passed."));
|
|
750
|
+
process.exitCode = 1;
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
const result = await runBrowserAuth({
|
|
754
|
+
serverUrl,
|
|
755
|
+
appUrl: appUrl ?? serverUrl,
|
|
756
|
+
scope: "launch",
|
|
757
|
+
name: hostname()
|
|
758
|
+
});
|
|
759
|
+
if (!result.userToken || !result.userId || !result.agentId) {
|
|
760
|
+
console.error(pc4.red("server did not return a user token / agent"));
|
|
761
|
+
process.exitCode = 1;
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
const config = loadProfile();
|
|
765
|
+
config.profiles[profileName] = {
|
|
766
|
+
server: result.serverUrl ?? serverUrl,
|
|
767
|
+
app_url: appUrl ?? serverUrl,
|
|
768
|
+
token: result.userToken,
|
|
769
|
+
user_id: result.userId,
|
|
770
|
+
agent_id: result.agentId
|
|
771
|
+
};
|
|
772
|
+
config.active_profile = profileName;
|
|
773
|
+
saveProfile(config);
|
|
774
|
+
if (result.userEmail) {
|
|
775
|
+
console.log(pc4.green(`Authenticated as ${pc4.bold(result.userEmail)}`));
|
|
776
|
+
}
|
|
777
|
+
console.log(` Profile: ${pc4.bold(profileName)}`);
|
|
778
|
+
console.log(` Server: ${pc4.cyan(result.serverUrl ?? serverUrl)}`);
|
|
779
|
+
console.log(
|
|
780
|
+
` Agent: ${pc4.bold(result.agentName ?? "default")} ${pc4.dim(`(${result.agentId})`)}`
|
|
781
|
+
);
|
|
782
|
+
if (result.daemonPairingToken) {
|
|
783
|
+
try {
|
|
784
|
+
const res = await fetch(`${result.serverUrl ?? serverUrl}/api/daemon/pair`, {
|
|
785
|
+
method: "POST",
|
|
786
|
+
headers: { "Content-Type": "application/json" },
|
|
787
|
+
body: JSON.stringify({
|
|
788
|
+
token: result.daemonPairingToken,
|
|
789
|
+
name: hostname(),
|
|
790
|
+
machine: hostname()
|
|
791
|
+
})
|
|
792
|
+
});
|
|
793
|
+
const data = await res.json().catch(() => ({}));
|
|
794
|
+
if (res.ok && data?.daemon?.id) {
|
|
795
|
+
saveDaemonProfile({
|
|
796
|
+
server: result.serverUrl ?? serverUrl,
|
|
797
|
+
daemonId: data.daemon.id,
|
|
798
|
+
daemonName: data.daemon.name,
|
|
799
|
+
token: data.token
|
|
800
|
+
});
|
|
801
|
+
console.log(
|
|
802
|
+
` Daemon: ${pc4.bold(data.daemon.name)} ${pc4.dim(`(${data.daemon.id})`)}`
|
|
803
|
+
);
|
|
804
|
+
await autoRegisterAgents(opts);
|
|
805
|
+
await autoStartDaemon(opts, result.serverUrl ?? serverUrl, data.daemon.id);
|
|
806
|
+
} else {
|
|
807
|
+
console.log(pc4.yellow(` Daemon: pairing failed \u2014 run \`bot pair\` later`));
|
|
808
|
+
}
|
|
809
|
+
} catch {
|
|
810
|
+
console.log(pc4.yellow(` Daemon: pairing skipped \u2014 run \`bot pair\` later`));
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
async function autoStartDaemon(opts, serverUrl, daemonId) {
|
|
815
|
+
if (opts.start === false) {
|
|
816
|
+
console.log(pc4.dim(`
|
|
817
|
+
Next: run \`bot daemon run\` to bring this machine online.`));
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
const dir = join3(homedir3(), ".botapp");
|
|
821
|
+
const pidFile = join3(dir, "daemon.pid");
|
|
822
|
+
const logFile = join3(dir, "daemon.log");
|
|
823
|
+
mkdirSync3(dir, { recursive: true });
|
|
824
|
+
if (isDaemonRunningLocally(pidFile)) {
|
|
825
|
+
console.log(pc4.dim(` Running: already (pid $(cat ~/.botapp/daemon.pid))`));
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
const botBin = process.argv[1];
|
|
829
|
+
if (!botBin || !existsSync4(botBin)) {
|
|
830
|
+
console.log(pc4.yellow(` Running: cannot resolve \`bot\` binary \u2014 run \`bot daemon run\` manually`));
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
const logFd = openSync(logFile, "a");
|
|
834
|
+
const child = spawn3(process.execPath, [botBin, "daemon", "run"], {
|
|
835
|
+
stdio: ["ignore", logFd, logFd],
|
|
836
|
+
detached: true
|
|
837
|
+
});
|
|
838
|
+
child.unref();
|
|
839
|
+
if (typeof child.pid === "number") {
|
|
840
|
+
writeFileSync3(pidFile, `${child.pid}
|
|
841
|
+
`, "utf-8");
|
|
842
|
+
}
|
|
843
|
+
let online = false;
|
|
844
|
+
for (let i = 0; i < 20; i++) {
|
|
845
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
846
|
+
if (await isDaemonOnline(serverUrl, daemonId)) {
|
|
847
|
+
online = true;
|
|
848
|
+
break;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
if (online) {
|
|
852
|
+
console.log(` Running: ${pc4.green("online")} ${pc4.dim(`(pid ${child.pid}, logs ~/.botapp/daemon.log)`)}`);
|
|
853
|
+
} else {
|
|
854
|
+
console.log(
|
|
855
|
+
pc4.yellow(
|
|
856
|
+
` Running: started but didn't report online in 10s \u2014 check ~/.botapp/daemon.log`
|
|
857
|
+
)
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
function isDaemonRunningLocally(pidFile) {
|
|
862
|
+
if (!existsSync4(pidFile)) return false;
|
|
863
|
+
try {
|
|
864
|
+
const pid = parseInt(readFileSync3(pidFile, "utf-8").trim(), 10);
|
|
865
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
866
|
+
process.kill(pid, 0);
|
|
867
|
+
return true;
|
|
868
|
+
} catch {
|
|
869
|
+
return false;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
async function isDaemonOnline(serverUrl, daemonId) {
|
|
873
|
+
try {
|
|
874
|
+
const res = await fetch(`${serverUrl}/api/daemons`, {
|
|
875
|
+
signal: AbortSignal.timeout(2e3)
|
|
876
|
+
});
|
|
877
|
+
if (!res.ok) return false;
|
|
878
|
+
const data = await res.json();
|
|
879
|
+
const d = (data.daemons ?? []).find((x) => x.id === daemonId);
|
|
880
|
+
return d?.status === "online" || d?.status === "connected";
|
|
881
|
+
} catch {
|
|
882
|
+
return false;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
async function autoRegisterAgents(opts) {
|
|
886
|
+
if (opts.agents === false) return;
|
|
887
|
+
const daemon = loadDaemonProfile();
|
|
888
|
+
if (!daemon) return;
|
|
889
|
+
try {
|
|
890
|
+
const { created, skipped, detected } = await registerAllDetectedAgents({
|
|
891
|
+
server: daemon.server,
|
|
892
|
+
token: daemon.token,
|
|
893
|
+
cwd: opts.agentCwd
|
|
894
|
+
});
|
|
895
|
+
if (detected === 0) {
|
|
896
|
+
console.log(
|
|
897
|
+
pc4.dim(
|
|
898
|
+
` Agents: none detected on PATH (scanned: codex, claude, openclaw, hermes)`
|
|
899
|
+
)
|
|
900
|
+
);
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
const summary = [];
|
|
904
|
+
if (created.length > 0) summary.push(`${created.length} registered (${created.join(", ")})`);
|
|
905
|
+
if (skipped.length > 0) summary.push(`${skipped.length} skipped`);
|
|
906
|
+
console.log(
|
|
907
|
+
` Agents: ${pc4.bold(summary.join(" \xB7 ") || "up to date")}`
|
|
908
|
+
);
|
|
909
|
+
} catch (e) {
|
|
910
|
+
console.log(pc4.yellow(` Agents: registration failed (${e.message})`));
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
async function tryLocalDaemonPair(serverUrl, name) {
|
|
914
|
+
try {
|
|
915
|
+
const tokRes = await fetch(`${serverUrl}/api/daemon/pairing-tokens`, {
|
|
916
|
+
method: "POST",
|
|
917
|
+
headers: { "Content-Type": "application/json" },
|
|
918
|
+
body: JSON.stringify({ name })
|
|
919
|
+
});
|
|
920
|
+
if (!tokRes.ok) return null;
|
|
921
|
+
const tokData = await tokRes.json();
|
|
922
|
+
if (!tokData.token) return null;
|
|
923
|
+
const pairRes = await fetch(`${serverUrl}/api/daemon/pair`, {
|
|
924
|
+
method: "POST",
|
|
925
|
+
headers: { "Content-Type": "application/json" },
|
|
926
|
+
body: JSON.stringify({ token: tokData.token, name, machine: name })
|
|
927
|
+
});
|
|
928
|
+
if (!pairRes.ok) return null;
|
|
929
|
+
const data = await pairRes.json();
|
|
930
|
+
if (!data.daemon?.id || !data.token) return null;
|
|
931
|
+
saveDaemonProfile({
|
|
932
|
+
server: serverUrl,
|
|
933
|
+
daemonId: data.daemon.id,
|
|
934
|
+
daemonName: data.daemon.name,
|
|
935
|
+
token: data.token
|
|
936
|
+
});
|
|
937
|
+
return { id: data.daemon.id, name: data.daemon.name };
|
|
938
|
+
} catch {
|
|
939
|
+
return null;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
function pickProfileName(serverUrl) {
|
|
943
|
+
try {
|
|
944
|
+
const host = new URL(serverUrl).hostname;
|
|
945
|
+
if (host === "localhost" || host === "127.0.0.1") return "local";
|
|
946
|
+
if (host === new URL(DEFAULT_SERVER_URL).hostname) return "cloud";
|
|
947
|
+
return host.replace(/\./g, "-");
|
|
948
|
+
} catch {
|
|
949
|
+
return "remote";
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
async function fetchHealth(serverUrl) {
|
|
953
|
+
try {
|
|
954
|
+
const res = await fetch(`${serverUrl}/health`, { signal: AbortSignal.timeout(5e3) });
|
|
955
|
+
if (!res.ok) return null;
|
|
956
|
+
return await res.json();
|
|
957
|
+
} catch {
|
|
958
|
+
return null;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
async function isServerUp(serverUrl) {
|
|
962
|
+
return await fetchHealth(serverUrl) !== null;
|
|
963
|
+
}
|
|
964
|
+
async function startLocalServer(opts) {
|
|
965
|
+
const serverUrl = `http://localhost:${opts.port}`;
|
|
966
|
+
const serverEntry = findServerEntry2();
|
|
967
|
+
if (!serverEntry) {
|
|
968
|
+
console.error(
|
|
969
|
+
pc4.red("Could not find the server package.\n") + `
|
|
970
|
+
Local mode requires the botapp monorepo source.
|
|
971
|
+
If you installed the CLI from npm, pick option ${pc4.cyan("2")} (cloud) or ${pc4.cyan("3")} (custom) instead.
|
|
972
|
+
|
|
973
|
+
Or run a local server from source:
|
|
974
|
+
${pc4.cyan("git clone https://github.com/wangdinglu/botapp")}
|
|
975
|
+
${pc4.cyan("cd botapp && pnpm install && pnpm build")}
|
|
976
|
+
${pc4.cyan("pnpm --filter @botapp/server dev")}`
|
|
977
|
+
);
|
|
978
|
+
return false;
|
|
979
|
+
}
|
|
980
|
+
console.log(pc4.blue("Starting local server..."));
|
|
981
|
+
const child = spawn3("node", ["--import", "tsx", serverEntry], {
|
|
982
|
+
env: { ...process.env, PORT: opts.port },
|
|
983
|
+
stdio: opts.background ? "ignore" : "inherit",
|
|
984
|
+
detached: opts.background
|
|
985
|
+
});
|
|
986
|
+
if (opts.background) {
|
|
987
|
+
child.unref();
|
|
988
|
+
}
|
|
989
|
+
for (let i = 0; i < 30; i++) {
|
|
990
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
991
|
+
if (await isServerUp(serverUrl)) return true;
|
|
992
|
+
}
|
|
993
|
+
console.error(pc4.red("Server did not come up in 15s"));
|
|
994
|
+
return false;
|
|
995
|
+
}
|
|
996
|
+
function findServerEntry2() {
|
|
997
|
+
const candidates = [
|
|
998
|
+
resolve3(process.cwd(), "packages/server/src/index.ts"),
|
|
999
|
+
resolve3(process.cwd(), "../server/src/index.ts"),
|
|
1000
|
+
resolve3(process.cwd(), "../../packages/server/src/index.ts")
|
|
1001
|
+
];
|
|
1002
|
+
for (const c of candidates) {
|
|
1003
|
+
if (existsSync4(c)) return c;
|
|
1004
|
+
}
|
|
1005
|
+
return null;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// src/commands/apps.ts
|
|
1009
|
+
import { Command as Command4 } from "commander";
|
|
1010
|
+
import pc5 from "picocolors";
|
|
1011
|
+
|
|
1012
|
+
// src/client/http.ts
|
|
1013
|
+
async function request(url, method, token, body) {
|
|
1014
|
+
let res;
|
|
1015
|
+
try {
|
|
1016
|
+
res = await fetch(url, {
|
|
1017
|
+
method,
|
|
1018
|
+
headers: authHeaders(token),
|
|
1019
|
+
body: body ? JSON.stringify(body) : void 0
|
|
1020
|
+
});
|
|
1021
|
+
} catch (e) {
|
|
1022
|
+
const cause = e?.cause;
|
|
1023
|
+
const code = cause?.code;
|
|
1024
|
+
const parsed = (() => {
|
|
1025
|
+
try {
|
|
1026
|
+
return new URL(url);
|
|
1027
|
+
} catch {
|
|
1028
|
+
return null;
|
|
1029
|
+
}
|
|
1030
|
+
})();
|
|
1031
|
+
const origin = parsed ? `${parsed.protocol}//${parsed.host}` : url;
|
|
1032
|
+
let hint = "";
|
|
1033
|
+
if (code === "ECONNREFUSED") {
|
|
1034
|
+
hint = ` \u2014 nothing is listening on ${origin}. Run \`bot server start\` to pick a server, or point at a different one with \`-s <url>\` or BOTAPP_SERVER.`;
|
|
1035
|
+
} else if (code === "ENOTFOUND") {
|
|
1036
|
+
hint = ` \u2014 cannot resolve host ${parsed?.hostname ?? origin}. Check the URL in ~/.botapp/config.yaml.`;
|
|
1037
|
+
} else if (code === "ETIMEDOUT" || code === "UND_ERR_CONNECT_TIMEOUT") {
|
|
1038
|
+
hint = ` \u2014 connection to ${origin} timed out.`;
|
|
1039
|
+
} else if (code) {
|
|
1040
|
+
hint = ` (${code})`;
|
|
1041
|
+
} else if (cause?.message) {
|
|
1042
|
+
hint = ` \u2014 ${cause.message}`;
|
|
1043
|
+
}
|
|
1044
|
+
throw new Error(`Cannot reach ${origin}${hint}`);
|
|
1045
|
+
}
|
|
1046
|
+
const data = await res.json().catch(() => null);
|
|
1047
|
+
if (!res.ok) {
|
|
1048
|
+
const msg = data?.error ?? data?.message ?? `HTTP ${res.status}`;
|
|
1049
|
+
throw Object.assign(new Error(msg), { status: res.status });
|
|
1050
|
+
}
|
|
1051
|
+
return data;
|
|
1052
|
+
}
|
|
1053
|
+
function createClient(serverUrl, token) {
|
|
1054
|
+
return {
|
|
1055
|
+
async health() {
|
|
1056
|
+
return request(`${serverUrl}/health`, "GET");
|
|
1057
|
+
},
|
|
1058
|
+
async listApps() {
|
|
1059
|
+
return request(`${serverUrl}/api/apps`, "GET", token);
|
|
1060
|
+
},
|
|
1061
|
+
async getApp(name) {
|
|
1062
|
+
return request(`${serverUrl}/api/apps/${name}`, "GET", token);
|
|
1063
|
+
},
|
|
1064
|
+
async runCommand(app, command, params) {
|
|
1065
|
+
return request(
|
|
1066
|
+
`${serverUrl}/api/apps/${app}/commands/${command}`,
|
|
1067
|
+
"POST",
|
|
1068
|
+
token,
|
|
1069
|
+
params
|
|
1070
|
+
);
|
|
1071
|
+
},
|
|
1072
|
+
async runAction(app, action, params) {
|
|
1073
|
+
return request(
|
|
1074
|
+
`${serverUrl}/api/apps/${app}/actions/${action}`,
|
|
1075
|
+
"POST",
|
|
1076
|
+
token,
|
|
1077
|
+
params
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// src/commands/apps.ts
|
|
1084
|
+
var appsCommand = new Command4("apps").description("List installed apps").option("--json", "Output as JSON").action(async (opts, cmd) => {
|
|
1085
|
+
const globalOpts = cmd.parent?.opts() ?? {};
|
|
1086
|
+
const serverUrl = resolveServerUrl(globalOpts.server);
|
|
1087
|
+
const token = resolveToken(globalOpts.token);
|
|
1088
|
+
const client = createClient(serverUrl, token);
|
|
1089
|
+
try {
|
|
1090
|
+
const data = await client.listApps();
|
|
1091
|
+
const apps = data.apps ?? [];
|
|
1092
|
+
if (opts.json || globalOpts.json) {
|
|
1093
|
+
console.log(JSON.stringify(apps, null, 2));
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
if (apps.length === 0) {
|
|
1097
|
+
console.log(pc5.yellow("No apps installed"));
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
for (const app of apps) {
|
|
1101
|
+
const status = app.status === "running" ? pc5.green("\u25CF") : app.status === "errored" ? pc5.red("\u25CF") : pc5.gray("\u25CB");
|
|
1102
|
+
console.log(`${status} ${pc5.bold(app.name)} v${app.version}`);
|
|
1103
|
+
if (app.description) {
|
|
1104
|
+
console.log(` ${pc5.dim(app.description)}`);
|
|
1105
|
+
}
|
|
1106
|
+
if (app.commands?.length > 0) {
|
|
1107
|
+
console.log(
|
|
1108
|
+
` Commands: ${app.commands.map((c) => pc5.cyan(c.name)).join(", ")}`
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
if (app.actions?.length > 0) {
|
|
1112
|
+
console.log(
|
|
1113
|
+
` Actions: ${app.actions.map((a) => pc5.yellow(a.name)).join(", ")}`
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
console.log();
|
|
1117
|
+
}
|
|
1118
|
+
} catch (e) {
|
|
1119
|
+
console.error(pc5.red(`Error: ${e.message}`));
|
|
1120
|
+
process.exitCode = 1;
|
|
1121
|
+
}
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
// src/commands/run.ts
|
|
1125
|
+
import { Command as Command5 } from "commander";
|
|
1126
|
+
import pc6 from "picocolors";
|
|
1127
|
+
var runCommand = new Command5("run").description("Run an app command").argument("<app>", "App name").argument("<command>", "Command name").allowUnknownOption(true).allowExcessArguments(true).action(async (appName, commandName, _opts, cmd) => {
|
|
1128
|
+
const globalOpts = cmd.parent?.opts() ?? {};
|
|
1129
|
+
const serverUrl = resolveServerUrl(globalOpts.server);
|
|
1130
|
+
const token = resolveToken(globalOpts.token);
|
|
1131
|
+
const client = createClient(serverUrl, token);
|
|
1132
|
+
const params = parseKwargs(process.argv.slice(process.argv.indexOf(commandName) + 1));
|
|
1133
|
+
try {
|
|
1134
|
+
const data = await client.runCommand(appName, commandName, params);
|
|
1135
|
+
if (globalOpts.json) {
|
|
1136
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1137
|
+
} else if (data.status === "success") {
|
|
1138
|
+
console.log(data.result);
|
|
1139
|
+
} else {
|
|
1140
|
+
console.error(pc6.red(`Error: ${data.error}`));
|
|
1141
|
+
process.exitCode = 1;
|
|
1142
|
+
}
|
|
1143
|
+
} catch (e) {
|
|
1144
|
+
console.error(pc6.red(`Error: ${e.message}`));
|
|
1145
|
+
process.exitCode = 1;
|
|
1146
|
+
}
|
|
1147
|
+
});
|
|
1148
|
+
function parseKwargs(args) {
|
|
1149
|
+
const params = {};
|
|
1150
|
+
for (let i = 0; i < args.length; i++) {
|
|
1151
|
+
const arg = args[i];
|
|
1152
|
+
if (arg.startsWith("--")) {
|
|
1153
|
+
const key = arg.slice(2);
|
|
1154
|
+
const next = args[i + 1];
|
|
1155
|
+
if (!next || next.startsWith("--")) {
|
|
1156
|
+
params[key] = true;
|
|
1157
|
+
} else {
|
|
1158
|
+
const num = Number(next);
|
|
1159
|
+
params[key] = isNaN(num) ? next : num;
|
|
1160
|
+
i++;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
return params;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
// src/commands/login.ts
|
|
1168
|
+
import { Command as Command6 } from "commander";
|
|
1169
|
+
import pc7 from "picocolors";
|
|
1170
|
+
var loginCommand = new Command6("login").description("Login to a botapp server").option("-s, --server <url>", "Server URL").action(async (opts) => {
|
|
1171
|
+
const serverUrl = resolveServerUrl(opts.server);
|
|
1172
|
+
console.log(`Connecting to ${pc7.cyan(serverUrl)}...`);
|
|
1173
|
+
try {
|
|
1174
|
+
const res = await fetch(`${serverUrl}/health`);
|
|
1175
|
+
if (!res.ok) {
|
|
1176
|
+
console.error(pc7.red(`Server returned ${res.status}`));
|
|
1177
|
+
process.exitCode = 1;
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
const data = await res.json();
|
|
1181
|
+
if (data.token) {
|
|
1182
|
+
updateActiveProfile({
|
|
1183
|
+
server: serverUrl,
|
|
1184
|
+
token: data.token,
|
|
1185
|
+
user_id: "local",
|
|
1186
|
+
agent_id: "local-agent"
|
|
1187
|
+
});
|
|
1188
|
+
console.log(pc7.green("Logged in successfully"));
|
|
1189
|
+
console.log(` Server: ${pc7.cyan(serverUrl)}`);
|
|
1190
|
+
console.log(` Mode: ${data.mode}`);
|
|
1191
|
+
console.log(` Token: ${pc7.dim(data.token.slice(0, 12) + "...")}`);
|
|
1192
|
+
} else {
|
|
1193
|
+
console.log(pc7.yellow("Server did not return a token (hosted mode login not yet implemented)"));
|
|
1194
|
+
}
|
|
1195
|
+
} catch (e) {
|
|
1196
|
+
const code = e?.cause?.code;
|
|
1197
|
+
const extra = code === "ECONNREFUSED" ? ` \u2014 nothing is listening. Run \`bot server start\` to pick a server, or set BOTAPP_SERVER to point at a running instance.` : code === "ENOTFOUND" ? ` \u2014 cannot resolve hostname. Check the URL.` : code ? ` (${code})` : "";
|
|
1198
|
+
console.error(pc7.red(`Cannot reach ${serverUrl}${extra}`));
|
|
1199
|
+
process.exitCode = 1;
|
|
1200
|
+
}
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
// src/commands/install.ts
|
|
1204
|
+
import { Command as Command7 } from "commander";
|
|
1205
|
+
import { resolve as resolve4, basename } from "path";
|
|
1206
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, symlinkSync, cpSync, readFileSync as readFileSync4 } from "fs";
|
|
1207
|
+
import { homedir as homedir4 } from "os";
|
|
1208
|
+
import { join as join4 } from "path";
|
|
1209
|
+
import pc8 from "picocolors";
|
|
1210
|
+
var APPS_DIR = join4(homedir4(), ".botapp", "apps");
|
|
1211
|
+
var installCommand = new Command7("install").description("Install an app from a local path").argument("<path>", "Path to the app directory").option("--dev", "Install in dev mode (symlink)").action(async (appPath, opts) => {
|
|
1212
|
+
const absPath = resolve4(appPath);
|
|
1213
|
+
if (!existsSync5(absPath)) {
|
|
1214
|
+
console.error(pc8.red(`Path not found: ${absPath}`));
|
|
1215
|
+
process.exitCode = 1;
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
const manifestPath = join4(absPath, "botapp.app.json");
|
|
1219
|
+
if (!existsSync5(manifestPath)) {
|
|
1220
|
+
console.error(pc8.red(`No botapp.app.json found in ${absPath}`));
|
|
1221
|
+
process.exitCode = 1;
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
let manifest;
|
|
1225
|
+
try {
|
|
1226
|
+
manifest = JSON.parse(readFileSync4(manifestPath, "utf-8"));
|
|
1227
|
+
} catch {
|
|
1228
|
+
console.error(pc8.red("Failed to parse botapp.app.json"));
|
|
1229
|
+
process.exitCode = 1;
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
const appName = manifest.name || basename(absPath);
|
|
1233
|
+
const targetDir = join4(APPS_DIR, appName);
|
|
1234
|
+
mkdirSync4(APPS_DIR, { recursive: true });
|
|
1235
|
+
if (existsSync5(targetDir)) {
|
|
1236
|
+
console.log(pc8.yellow(`App "${appName}" is already installed. Reinstalling...`));
|
|
1237
|
+
const { rmSync: rmSync2 } = await import("fs");
|
|
1238
|
+
rmSync2(targetDir, { recursive: true, force: true });
|
|
1239
|
+
}
|
|
1240
|
+
if (opts.dev) {
|
|
1241
|
+
symlinkSync(absPath, targetDir, "dir");
|
|
1242
|
+
console.log(pc8.green(`Installed ${pc8.bold(appName)} in dev mode (symlink)`));
|
|
1243
|
+
console.log(` ${pc8.dim(absPath)} \u2192 ${pc8.dim(targetDir)}`);
|
|
1244
|
+
} else {
|
|
1245
|
+
cpSync(absPath, targetDir, { recursive: true });
|
|
1246
|
+
console.log(pc8.green(`Installed ${pc8.bold(appName)}`));
|
|
1247
|
+
console.log(` ${pc8.dim(targetDir)}`);
|
|
1248
|
+
}
|
|
1249
|
+
console.log(pc8.dim("Restart the server to load the new app."));
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
// src/commands/uninstall.ts
|
|
1253
|
+
import { Command as Command8 } from "commander";
|
|
1254
|
+
import { existsSync as existsSync6, rmSync } from "fs";
|
|
1255
|
+
import { homedir as homedir5 } from "os";
|
|
1256
|
+
import { join as join5 } from "path";
|
|
1257
|
+
import pc9 from "picocolors";
|
|
1258
|
+
var APPS_DIR2 = join5(homedir5(), ".botapp", "apps");
|
|
1259
|
+
var uninstallCommand = new Command8("uninstall").description("Uninstall an app").argument("<name>", "App name to uninstall").action(async (name) => {
|
|
1260
|
+
const targetDir = join5(APPS_DIR2, name);
|
|
1261
|
+
if (!existsSync6(targetDir)) {
|
|
1262
|
+
console.error(pc9.red(`App "${name}" is not installed`));
|
|
1263
|
+
process.exitCode = 1;
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
rmSync(targetDir, { recursive: true, force: true });
|
|
1267
|
+
console.log(pc9.green(`Uninstalled ${pc9.bold(name)}`));
|
|
1268
|
+
console.log(pc9.dim("Restart the server to apply changes."));
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
// src/commands/dev.ts
|
|
1272
|
+
import { Command as Command9 } from "commander";
|
|
1273
|
+
import { resolve as resolve5, basename as basename2, join as join6 } from "path";
|
|
1274
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, symlinkSync as symlinkSync2, readFileSync as readFileSync5, lstatSync } from "fs";
|
|
1275
|
+
import { homedir as homedir6 } from "os";
|
|
1276
|
+
import { spawn as spawn4 } from "child_process";
|
|
1277
|
+
import pc10 from "picocolors";
|
|
1278
|
+
var APPS_DIR3 = join6(homedir6(), ".botapp", "apps");
|
|
1279
|
+
var devCommand = new Command9("dev").description("Start development mode for an app").argument("[path]", "Path to the app directory", ".").option("-p, --port <port>", "Server port", "7100").action(async (appPath, opts) => {
|
|
1280
|
+
const absPath = resolve5(appPath);
|
|
1281
|
+
if (!existsSync7(absPath)) {
|
|
1282
|
+
console.error(pc10.red(`Path not found: ${absPath}`));
|
|
1283
|
+
process.exitCode = 1;
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
const manifestPath = join6(absPath, "botapp.app.json");
|
|
1287
|
+
if (!existsSync7(manifestPath)) {
|
|
1288
|
+
console.error(pc10.red(`No botapp.app.json found in ${absPath}`));
|
|
1289
|
+
process.exitCode = 1;
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
let manifest;
|
|
1293
|
+
try {
|
|
1294
|
+
manifest = JSON.parse(readFileSync5(manifestPath, "utf-8"));
|
|
1295
|
+
} catch {
|
|
1296
|
+
console.error(pc10.red("Failed to parse botapp.app.json"));
|
|
1297
|
+
process.exitCode = 1;
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
const appName = manifest.name || basename2(absPath);
|
|
1301
|
+
const targetDir = join6(APPS_DIR3, appName);
|
|
1302
|
+
mkdirSync5(APPS_DIR3, { recursive: true });
|
|
1303
|
+
if (!existsSync7(targetDir)) {
|
|
1304
|
+
symlinkSync2(absPath, targetDir, "dir");
|
|
1305
|
+
console.log(pc10.blue(`Linked ${pc10.bold(appName)} \u2192 ${pc10.dim(absPath)}`));
|
|
1306
|
+
} else {
|
|
1307
|
+
const stat = lstatSync(targetDir);
|
|
1308
|
+
if (stat.isSymbolicLink()) {
|
|
1309
|
+
console.log(pc10.dim(`${appName} already linked`));
|
|
1310
|
+
} else {
|
|
1311
|
+
console.log(pc10.yellow(`${appName} is installed (not symlinked). Using existing installation.`));
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
const serverUrl = resolveServerUrl();
|
|
1315
|
+
let serverRunning = false;
|
|
1316
|
+
try {
|
|
1317
|
+
const res = await fetch(`${serverUrl}/health`);
|
|
1318
|
+
serverRunning = res.ok;
|
|
1319
|
+
} catch {
|
|
1320
|
+
}
|
|
1321
|
+
if (serverRunning) {
|
|
1322
|
+
console.log(pc10.green(`Server already running at ${serverUrl}`));
|
|
1323
|
+
console.log(pc10.yellow("Restart the server to load the new app."));
|
|
1324
|
+
} else {
|
|
1325
|
+
const serverEntry = findServerEntry3();
|
|
1326
|
+
if (!serverEntry) {
|
|
1327
|
+
console.log(
|
|
1328
|
+
pc10.yellow("No local server found.") + ` App ${pc10.bold(appName)} has been linked to ${pc10.dim(targetDir)}
|
|
1329
|
+
|
|
1330
|
+
Start the server separately, then restart it to load the app:
|
|
1331
|
+
${pc10.cyan("bot server start")}`
|
|
1332
|
+
);
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
console.log(pc10.blue("Starting botapp server..."));
|
|
1336
|
+
const child = spawn4("node", ["--import", "tsx", serverEntry], {
|
|
1337
|
+
env: { ...process.env, PORT: opts.port },
|
|
1338
|
+
stdio: "inherit"
|
|
1339
|
+
});
|
|
1340
|
+
child.on("exit", (code) => {
|
|
1341
|
+
process.exitCode = code ?? 0;
|
|
1342
|
+
});
|
|
1343
|
+
process.on("SIGINT", () => {
|
|
1344
|
+
child.kill("SIGINT");
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
});
|
|
1348
|
+
function findServerEntry3() {
|
|
1349
|
+
const candidates = [
|
|
1350
|
+
resolve5(process.cwd(), "packages/server/src/index.ts"),
|
|
1351
|
+
resolve5(process.cwd(), "../server/src/index.ts")
|
|
1352
|
+
];
|
|
1353
|
+
for (const c of candidates) {
|
|
1354
|
+
if (existsSync7(c)) return c;
|
|
1355
|
+
}
|
|
1356
|
+
return null;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// src/commands/connect.ts
|
|
1360
|
+
import { Command as Command10 } from "commander";
|
|
1361
|
+
import { WebSocket } from "ws";
|
|
1362
|
+
import pc11 from "picocolors";
|
|
1363
|
+
var connectCommand = new Command10("connect").description("Open a persistent WebSocket connection and stream events").option("--subscribe <patterns>", "Comma-separated event patterns to subscribe to", "*").action(async (opts, cmd) => {
|
|
1364
|
+
const globalOpts = cmd.parent?.opts() ?? {};
|
|
1365
|
+
const serverUrl = resolveServerUrl(globalOpts.server);
|
|
1366
|
+
const token = resolveToken(globalOpts.token);
|
|
1367
|
+
const wsUrl = serverUrl.replace(/^http/, "ws");
|
|
1368
|
+
const url = token ? `${wsUrl}/ws/agent?token=${token}` : `${wsUrl}/ws/agent`;
|
|
1369
|
+
console.log(pc11.blue(`Connecting to ${serverUrl}...`));
|
|
1370
|
+
const ws = new WebSocket(url);
|
|
1371
|
+
ws.on("open", () => {
|
|
1372
|
+
console.log(pc11.green("Connected"));
|
|
1373
|
+
const patterns = opts.subscribe.split(",").map((p) => p.trim());
|
|
1374
|
+
ws.send(JSON.stringify({
|
|
1375
|
+
type: "subscribe",
|
|
1376
|
+
patterns
|
|
1377
|
+
}));
|
|
1378
|
+
});
|
|
1379
|
+
ws.on("message", (raw) => {
|
|
1380
|
+
try {
|
|
1381
|
+
const frame = JSON.parse(raw.toString());
|
|
1382
|
+
switch (frame.type) {
|
|
1383
|
+
case "subscribed":
|
|
1384
|
+
console.log(pc11.dim(`Subscribed to: ${frame.patterns.join(", ")}`));
|
|
1385
|
+
break;
|
|
1386
|
+
case "event": {
|
|
1387
|
+
const e = frame.event;
|
|
1388
|
+
const ts = new Date(e.timestamp).toLocaleTimeString();
|
|
1389
|
+
const status = e.result?.status === "success" ? pc11.green("\u2713") : pc11.red("\u2717");
|
|
1390
|
+
console.log(
|
|
1391
|
+
`${pc11.dim(ts)} ${status} ${pc11.bold(e.app)}.${e.action} [${e.source}]` + (e.duration_ms != null ? pc11.dim(` ${e.duration_ms}ms`) : "")
|
|
1392
|
+
);
|
|
1393
|
+
if (globalOpts.verbose && e.result) {
|
|
1394
|
+
console.log(pc11.dim(` \u2192 ${JSON.stringify(e.result)}`));
|
|
1395
|
+
}
|
|
1396
|
+
break;
|
|
1397
|
+
}
|
|
1398
|
+
case "result":
|
|
1399
|
+
if (frame.status === "success") {
|
|
1400
|
+
console.log(pc11.green(`Result: ${JSON.stringify(frame.output)}`));
|
|
1401
|
+
} else {
|
|
1402
|
+
console.log(pc11.red(`Error: ${frame.error}`));
|
|
1403
|
+
}
|
|
1404
|
+
break;
|
|
1405
|
+
case "pong":
|
|
1406
|
+
break;
|
|
1407
|
+
case "error":
|
|
1408
|
+
console.log(pc11.red(`Server error: ${frame.message}`));
|
|
1409
|
+
break;
|
|
1410
|
+
default:
|
|
1411
|
+
if (globalOpts.verbose) {
|
|
1412
|
+
console.log(pc11.dim(JSON.stringify(frame)));
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
} catch {
|
|
1416
|
+
console.log(pc11.dim(raw.toString()));
|
|
1417
|
+
}
|
|
1418
|
+
});
|
|
1419
|
+
ws.on("close", (code, reason) => {
|
|
1420
|
+
console.log(pc11.yellow(`Disconnected (${code}: ${reason.toString() || "no reason"})`));
|
|
1421
|
+
process.exit(0);
|
|
1422
|
+
});
|
|
1423
|
+
ws.on("error", (err) => {
|
|
1424
|
+
console.error(pc11.red(`WebSocket error: ${err.message}`));
|
|
1425
|
+
process.exit(1);
|
|
1426
|
+
});
|
|
1427
|
+
const pingInterval = setInterval(() => {
|
|
1428
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
1429
|
+
ws.send(JSON.stringify({ type: "ping" }));
|
|
1430
|
+
}
|
|
1431
|
+
}, 3e4);
|
|
1432
|
+
process.on("SIGINT", () => {
|
|
1433
|
+
clearInterval(pingInterval);
|
|
1434
|
+
ws.close();
|
|
1435
|
+
});
|
|
1436
|
+
});
|
|
1437
|
+
|
|
1438
|
+
// src/commands/events.ts
|
|
1439
|
+
import { Command as Command11 } from "commander";
|
|
1440
|
+
import pc12 from "picocolors";
|
|
1441
|
+
var eventsCommand = new Command11("events").description("Fetch recent events").option("--app <name>", "Filter by app name").option("--since <timestamp>", "Show events since ISO timestamp").option("--limit <n>", "Max events to return", "20").option("--json", "Output as JSON").action(async (opts, cmd) => {
|
|
1442
|
+
const globalOpts = { ...cmd.parent?.opts() ?? {}, ...opts };
|
|
1443
|
+
const serverUrl = resolveServerUrl(globalOpts.server);
|
|
1444
|
+
const token = resolveToken(globalOpts.token);
|
|
1445
|
+
const params = new URLSearchParams();
|
|
1446
|
+
if (opts.app) params.set("app", opts.app);
|
|
1447
|
+
if (opts.since) params.set("since", opts.since);
|
|
1448
|
+
if (opts.limit) params.set("limit", opts.limit);
|
|
1449
|
+
const url = `${serverUrl}/api/events?${params.toString()}`;
|
|
1450
|
+
try {
|
|
1451
|
+
const res = await fetch(url, {
|
|
1452
|
+
headers: {
|
|
1453
|
+
...authHeaders(token)
|
|
1454
|
+
}
|
|
1455
|
+
});
|
|
1456
|
+
if (!res.ok) {
|
|
1457
|
+
const body = await res.json().catch(() => ({}));
|
|
1458
|
+
console.error(pc12.red(`Error: ${body.error ?? res.statusText}`));
|
|
1459
|
+
process.exitCode = 1;
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
const data = await res.json();
|
|
1463
|
+
if (globalOpts.json) {
|
|
1464
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
if (data.events.length === 0) {
|
|
1468
|
+
console.log(pc12.dim("No events found"));
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
for (const e of data.events.reverse()) {
|
|
1472
|
+
const ts = new Date(e.timestamp).toLocaleTimeString();
|
|
1473
|
+
const status = e.result?.status === "success" ? pc12.green("\u2713") : pc12.red("\u2717");
|
|
1474
|
+
console.log(
|
|
1475
|
+
`${pc12.dim(ts)} ${status} ${pc12.bold(e.app)}.${e.action} [${e.source}]` + (e.duration_ms != null ? pc12.dim(` ${e.duration_ms}ms`) : "")
|
|
1476
|
+
);
|
|
1477
|
+
if (globalOpts.verbose && e.result) {
|
|
1478
|
+
console.log(pc12.dim(` \u2192 ${JSON.stringify(e.result)}`));
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
console.log(pc12.dim(`
|
|
1482
|
+
${data.events.length} event(s)`));
|
|
1483
|
+
} catch (e) {
|
|
1484
|
+
console.error(pc12.red(`Error: ${e.message}`));
|
|
1485
|
+
process.exitCode = 1;
|
|
1486
|
+
}
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
// src/commands/agent.ts
|
|
1490
|
+
import { Command as Command12 } from "commander";
|
|
1491
|
+
import pc13 from "picocolors";
|
|
1492
|
+
var agentCommand = new Command12("agent").description("Manage agents");
|
|
1493
|
+
agentCommand.command("list").description("List agents for current user").action(async (_opts, cmd) => {
|
|
1494
|
+
const globalOpts = cmd.parent?.parent?.opts() ?? {};
|
|
1495
|
+
const serverUrl = resolveServerUrl(globalOpts.server);
|
|
1496
|
+
const token = resolveToken(globalOpts.token);
|
|
1497
|
+
try {
|
|
1498
|
+
const res = await fetch(`${serverUrl}/auth/agents`, {
|
|
1499
|
+
headers: authHeaders(token)
|
|
1500
|
+
});
|
|
1501
|
+
if (!res.ok) {
|
|
1502
|
+
const body = await res.json().catch(() => ({}));
|
|
1503
|
+
console.error(pc13.red(`Error: ${body.error ?? res.statusText}`));
|
|
1504
|
+
process.exitCode = 1;
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
const data = await res.json();
|
|
1508
|
+
if (globalOpts.json) {
|
|
1509
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
if (data.agents.length === 0) {
|
|
1513
|
+
console.log(pc13.dim("No agents"));
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
for (const agent of data.agents) {
|
|
1517
|
+
console.log(`${pc13.bold(agent.name)} ${pc13.dim(`(${agent.id})`)}`);
|
|
1518
|
+
console.log(` Token: ${pc13.dim(agent.token)}`);
|
|
1519
|
+
console.log(` Created: ${pc13.dim(agent.createdAt)}`);
|
|
1520
|
+
}
|
|
1521
|
+
} catch (e) {
|
|
1522
|
+
console.error(pc13.red(`Error: ${e.message}`));
|
|
1523
|
+
process.exitCode = 1;
|
|
1524
|
+
}
|
|
1525
|
+
});
|
|
1526
|
+
agentCommand.command("create").description("Create a new agent").argument("<name>", "Agent name").action(async (name, _opts, cmd) => {
|
|
1527
|
+
const globalOpts = cmd.parent?.parent?.opts() ?? {};
|
|
1528
|
+
const serverUrl = resolveServerUrl(globalOpts.server);
|
|
1529
|
+
const token = resolveToken(globalOpts.token);
|
|
1530
|
+
try {
|
|
1531
|
+
const res = await fetch(`${serverUrl}/auth/agents`, {
|
|
1532
|
+
method: "POST",
|
|
1533
|
+
headers: authHeaders(token),
|
|
1534
|
+
body: JSON.stringify({ name })
|
|
1535
|
+
});
|
|
1536
|
+
if (!res.ok) {
|
|
1537
|
+
const body = await res.json().catch(() => ({}));
|
|
1538
|
+
console.error(pc13.red(`Error: ${body.error ?? res.statusText}`));
|
|
1539
|
+
process.exitCode = 1;
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
const data = await res.json();
|
|
1543
|
+
console.log(pc13.green(`Created agent: ${pc13.bold(data.agent.name)}`));
|
|
1544
|
+
console.log(` ID: ${data.agent.id}`);
|
|
1545
|
+
console.log(` Token: ${data.agent.token}`);
|
|
1546
|
+
} catch (e) {
|
|
1547
|
+
console.error(pc13.red(`Error: ${e.message}`));
|
|
1548
|
+
process.exitCode = 1;
|
|
1549
|
+
}
|
|
1550
|
+
});
|
|
1551
|
+
agentCommand.command("info").description("Show current user and agent info").action(async (_opts, cmd) => {
|
|
1552
|
+
const globalOpts = cmd.parent?.parent?.opts() ?? {};
|
|
1553
|
+
const serverUrl = resolveServerUrl(globalOpts.server);
|
|
1554
|
+
const token = resolveToken(globalOpts.token);
|
|
1555
|
+
try {
|
|
1556
|
+
const res = await fetch(`${serverUrl}/auth/me`, {
|
|
1557
|
+
headers: authHeaders(token)
|
|
1558
|
+
});
|
|
1559
|
+
if (!res.ok) {
|
|
1560
|
+
const body = await res.json().catch(() => ({}));
|
|
1561
|
+
console.error(pc13.red(`Error: ${body.error ?? res.statusText}`));
|
|
1562
|
+
process.exitCode = 1;
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
const data = await res.json();
|
|
1566
|
+
if (globalOpts.json) {
|
|
1567
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
console.log(`User: ${pc13.bold(data.user.email)} ${pc13.dim(`(${data.user.id})`)}`);
|
|
1571
|
+
console.log(`Agent: ${pc13.bold(data.currentAgent.name)} ${pc13.dim(`(${data.currentAgent.id})`)}`);
|
|
1572
|
+
if (data.agents.length > 1) {
|
|
1573
|
+
console.log(`
|
|
1574
|
+
All agents:`);
|
|
1575
|
+
for (const a of data.agents) {
|
|
1576
|
+
const current = a.id === data.currentAgent.id ? pc13.green(" (current)") : "";
|
|
1577
|
+
console.log(` ${pc13.bold(a.name)} ${pc13.dim(a.id)}${current}`);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
} catch (e) {
|
|
1581
|
+
console.error(pc13.red(`Error: ${e.message}`));
|
|
1582
|
+
process.exitCode = 1;
|
|
1583
|
+
}
|
|
1584
|
+
});
|
|
1585
|
+
|
|
1586
|
+
// src/commands/register.ts
|
|
1587
|
+
import { Command as Command13 } from "commander";
|
|
1588
|
+
import { readFileSync as readFileSync6, existsSync as existsSync8 } from "fs";
|
|
1589
|
+
import pc14 from "picocolors";
|
|
1590
|
+
var registerCommand = new Command13("register").description("Register an external app via HTTP bridge manifest").argument("<manifest>", "Path to YAML manifest file").option("--adapter <url>", "Override base URL from manifest").action(async (manifestPath, opts, cmd) => {
|
|
1591
|
+
const globalOpts = cmd.parent?.opts() ?? {};
|
|
1592
|
+
const serverUrl = resolveServerUrl(globalOpts.server);
|
|
1593
|
+
const token = resolveToken(globalOpts.token);
|
|
1594
|
+
if (!existsSync8(manifestPath)) {
|
|
1595
|
+
console.error(pc14.red(`Error: Manifest not found: ${manifestPath}`));
|
|
1596
|
+
process.exitCode = 1;
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
try {
|
|
1600
|
+
const raw = readFileSync6(manifestPath, "utf-8");
|
|
1601
|
+
const res = await fetch(`${serverUrl}/api/bridges/http`, {
|
|
1602
|
+
method: "POST",
|
|
1603
|
+
headers: authHeaders(token),
|
|
1604
|
+
body: JSON.stringify({
|
|
1605
|
+
manifest: raw,
|
|
1606
|
+
adapterUrl: opts.adapter
|
|
1607
|
+
})
|
|
1608
|
+
});
|
|
1609
|
+
if (!res.ok) {
|
|
1610
|
+
const body = await res.json().catch(() => ({}));
|
|
1611
|
+
console.error(pc14.red(`Error: ${body.error ?? res.statusText}`));
|
|
1612
|
+
process.exitCode = 1;
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
const data = await res.json();
|
|
1616
|
+
console.log(pc14.green(`Registered bridge: ${pc14.bold(data.name)}`));
|
|
1617
|
+
if (data.commands?.length > 0) {
|
|
1618
|
+
console.log(` Commands: ${data.commands.join(", ")}`);
|
|
1619
|
+
}
|
|
1620
|
+
if (data.actions?.length > 0) {
|
|
1621
|
+
console.log(` Actions: ${data.actions.join(", ")}`);
|
|
1622
|
+
}
|
|
1623
|
+
} catch (e) {
|
|
1624
|
+
console.error(pc14.red(`Error: ${e.message}`));
|
|
1625
|
+
process.exitCode = 1;
|
|
1626
|
+
}
|
|
1627
|
+
});
|
|
1628
|
+
|
|
1629
|
+
// src/commands/wrap.ts
|
|
1630
|
+
import { Command as Command14 } from "commander";
|
|
1631
|
+
import { readFileSync as readFileSync7, existsSync as existsSync9 } from "fs";
|
|
1632
|
+
import pc15 from "picocolors";
|
|
1633
|
+
var wrapCommand = new Command14("wrap").description("Register CLI tool as app commands via YAML manifest").argument("<manifest>", "Path to CLI wrapper YAML manifest").action(async (manifestPath, _opts, cmd) => {
|
|
1634
|
+
const globalOpts = cmd.parent?.opts() ?? {};
|
|
1635
|
+
const serverUrl = resolveServerUrl(globalOpts.server);
|
|
1636
|
+
const token = resolveToken(globalOpts.token);
|
|
1637
|
+
if (!existsSync9(manifestPath)) {
|
|
1638
|
+
console.error(pc15.red(`Error: Manifest not found: ${manifestPath}`));
|
|
1639
|
+
process.exitCode = 1;
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
try {
|
|
1643
|
+
const raw = readFileSync7(manifestPath, "utf-8");
|
|
1644
|
+
const res = await fetch(`${serverUrl}/api/bridges/cli`, {
|
|
1645
|
+
method: "POST",
|
|
1646
|
+
headers: authHeaders(token),
|
|
1647
|
+
body: JSON.stringify({ manifest: raw })
|
|
1648
|
+
});
|
|
1649
|
+
if (!res.ok) {
|
|
1650
|
+
const body = await res.json().catch(() => ({}));
|
|
1651
|
+
console.error(pc15.red(`Error: ${body.error ?? res.statusText}`));
|
|
1652
|
+
process.exitCode = 1;
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
const data = await res.json();
|
|
1656
|
+
console.log(pc15.green(`Wrapped CLI: ${pc15.bold(data.name)}`));
|
|
1657
|
+
if (data.commands?.length > 0) {
|
|
1658
|
+
console.log(` Commands: ${data.commands.join(", ")}`);
|
|
1659
|
+
}
|
|
1660
|
+
} catch (e) {
|
|
1661
|
+
console.error(pc15.red(`Error: ${e.message}`));
|
|
1662
|
+
process.exitCode = 1;
|
|
1663
|
+
}
|
|
1664
|
+
});
|
|
1665
|
+
|
|
1666
|
+
// src/commands/reload.ts
|
|
1667
|
+
import { Command as Command15 } from "commander";
|
|
1668
|
+
import pc16 from "picocolors";
|
|
1669
|
+
var reloadCommand = new Command15("reload").description("Reload an app").argument("<app>", "App name to reload").action(async (appName, _opts, cmd) => {
|
|
1670
|
+
const globalOpts = cmd.parent?.opts() ?? {};
|
|
1671
|
+
const serverUrl = resolveServerUrl(globalOpts.server);
|
|
1672
|
+
const token = resolveToken(globalOpts.token);
|
|
1673
|
+
try {
|
|
1674
|
+
const res = await fetch(`${serverUrl}/api/apps/${appName}/reload`, {
|
|
1675
|
+
method: "POST",
|
|
1676
|
+
headers: authHeaders(token),
|
|
1677
|
+
body: "{}"
|
|
1678
|
+
});
|
|
1679
|
+
if (!res.ok) {
|
|
1680
|
+
const body = await res.json().catch(() => ({}));
|
|
1681
|
+
console.error(pc16.red(`Error: ${body.error ?? res.statusText}`));
|
|
1682
|
+
process.exitCode = 1;
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
const data = await res.json();
|
|
1686
|
+
console.log(pc16.green(`Reloaded: ${pc16.bold(data.name)} (${data.status})`));
|
|
1687
|
+
} catch (e) {
|
|
1688
|
+
console.error(pc16.red(`Error: ${e.message}`));
|
|
1689
|
+
process.exitCode = 1;
|
|
1690
|
+
}
|
|
1691
|
+
});
|
|
1692
|
+
var configCommand = new Command15("config").description("View or set app configuration").argument("[app]", "App name").argument("[key]", "Config key").argument("[value]", "Config value (omit to read)").action(async (appName, key, value, _opts, cmd) => {
|
|
1693
|
+
const globalOpts = cmd.parent?.opts() ?? {};
|
|
1694
|
+
const serverUrl = resolveServerUrl(globalOpts.server);
|
|
1695
|
+
const token = resolveToken(globalOpts.token);
|
|
1696
|
+
if (!appName) {
|
|
1697
|
+
try {
|
|
1698
|
+
const res = await fetch(`${serverUrl}/api/config`, {
|
|
1699
|
+
headers: authHeaders(token)
|
|
1700
|
+
});
|
|
1701
|
+
const data = await res.json();
|
|
1702
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1703
|
+
} catch (e) {
|
|
1704
|
+
console.error(pc16.red(`Error: ${e.message}`));
|
|
1705
|
+
process.exitCode = 1;
|
|
1706
|
+
}
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
if (!key) {
|
|
1710
|
+
try {
|
|
1711
|
+
const res = await fetch(`${serverUrl}/api/apps/${appName}`, {
|
|
1712
|
+
headers: authHeaders(token)
|
|
1713
|
+
});
|
|
1714
|
+
if (!res.ok) {
|
|
1715
|
+
console.error(pc16.red(`App not found: ${appName}`));
|
|
1716
|
+
process.exitCode = 1;
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
const data = await res.json();
|
|
1720
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1721
|
+
} catch (e) {
|
|
1722
|
+
console.error(pc16.red(`Error: ${e.message}`));
|
|
1723
|
+
process.exitCode = 1;
|
|
1724
|
+
}
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
console.log(pc16.dim(`Config management for ${appName}.${key}${value ? ` = ${value}` : ""}`));
|
|
1728
|
+
console.log(pc16.dim("(Config persistence coming in a future update)"));
|
|
1729
|
+
});
|
|
1730
|
+
|
|
1731
|
+
// src/commands/skill.ts
|
|
1732
|
+
import { Command as Command16 } from "commander";
|
|
1733
|
+
import pc17 from "picocolors";
|
|
1734
|
+
async function fetchSkill(serverUrl, token, appName, opts) {
|
|
1735
|
+
const qs = new URLSearchParams();
|
|
1736
|
+
if (opts.json) qs.set("format", "json");
|
|
1737
|
+
if (opts.refresh) qs.set("refresh", "1");
|
|
1738
|
+
const url = `${serverUrl}/api/apps/${appName}/skill${qs.toString() ? `?${qs}` : ""}`;
|
|
1739
|
+
let res;
|
|
1740
|
+
try {
|
|
1741
|
+
res = await fetch(url, { headers: authHeaders(token) });
|
|
1742
|
+
} catch (e) {
|
|
1743
|
+
const code = e?.cause?.code;
|
|
1744
|
+
const hint = code === "ECONNREFUSED" ? ` \u2014 nothing is listening. Run \`bot server start\` to pick a server, or set BOTAPP_SERVER to point at a running instance.` : code ? ` (${code})` : "";
|
|
1745
|
+
throw new Error(`Cannot reach ${serverUrl}${hint}`);
|
|
1746
|
+
}
|
|
1747
|
+
if (!res.ok) {
|
|
1748
|
+
const body = await res.json().catch(() => ({}));
|
|
1749
|
+
throw new Error(body?.error ?? `HTTP ${res.status}`);
|
|
1750
|
+
}
|
|
1751
|
+
if (opts.json) {
|
|
1752
|
+
return await res.json();
|
|
1753
|
+
}
|
|
1754
|
+
return await res.text();
|
|
1755
|
+
}
|
|
1756
|
+
var skillSingle = new Command16("skill").description("Fetch an app's skill doc (agent-facing narrative + reference)").argument("[app]", "App name. Omit to list all apps with their skill source.").option("--json", "Emit structured JSON instead of markdown").option("--refresh", "Bypass the server-side URL cache").option("--bundle", "When no app given, print every app as one big markdown doc").action(async (appName, opts, cmd) => {
|
|
1757
|
+
const globalOpts = cmd.parent?.opts() ?? {};
|
|
1758
|
+
const serverUrl = resolveServerUrl(globalOpts.server);
|
|
1759
|
+
const token = resolveToken(globalOpts.token);
|
|
1760
|
+
try {
|
|
1761
|
+
if (!appName) {
|
|
1762
|
+
const client = createClient(serverUrl, token);
|
|
1763
|
+
const data = await client.listApps();
|
|
1764
|
+
const apps = data.apps ?? [];
|
|
1765
|
+
if (opts.bundle) {
|
|
1766
|
+
const chunks = [
|
|
1767
|
+
"# botapp server \u2014 app skills bundle",
|
|
1768
|
+
"",
|
|
1769
|
+
`_Generated from ${serverUrl}. ${apps.length} app(s)._`,
|
|
1770
|
+
"",
|
|
1771
|
+
"---",
|
|
1772
|
+
""
|
|
1773
|
+
];
|
|
1774
|
+
for (const app of apps) {
|
|
1775
|
+
try {
|
|
1776
|
+
const md = await fetchSkill(serverUrl, token, app.name, {});
|
|
1777
|
+
chunks.push(md, "", "---", "");
|
|
1778
|
+
} catch (e) {
|
|
1779
|
+
chunks.push(
|
|
1780
|
+
`# ${app.name}`,
|
|
1781
|
+
"",
|
|
1782
|
+
`> (failed to load: ${e.message})`,
|
|
1783
|
+
"",
|
|
1784
|
+
"---",
|
|
1785
|
+
""
|
|
1786
|
+
);
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
console.log(chunks.join("\n"));
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
if (globalOpts.json || opts.json) {
|
|
1793
|
+
console.log(JSON.stringify(apps, null, 2));
|
|
1794
|
+
return;
|
|
1795
|
+
}
|
|
1796
|
+
if (apps.length === 0) {
|
|
1797
|
+
console.log(pc17.yellow("No apps installed"));
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
console.log(pc17.bold("App skills:"));
|
|
1801
|
+
console.log();
|
|
1802
|
+
for (const app of apps) {
|
|
1803
|
+
const src = app.skill ? /^https?:\/\//i.test(app.skill) ? pc17.cyan(app.skill) : pc17.green(app.skill) : pc17.dim("(none declared \u2014 fallback to ./SKILL.md or auto-gen)");
|
|
1804
|
+
console.log(
|
|
1805
|
+
` ${pc17.bold(app.name)}${pc17.dim(` \u2014 ${app.description ?? ""}`)}`
|
|
1806
|
+
);
|
|
1807
|
+
console.log(` skill: ${src}`);
|
|
1808
|
+
}
|
|
1809
|
+
console.log();
|
|
1810
|
+
console.log(
|
|
1811
|
+
pc17.dim(
|
|
1812
|
+
`Run \`bot skill <app>\` to fetch one, or \`bot skill --bundle\` for everything.`
|
|
1813
|
+
)
|
|
1814
|
+
);
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
const wantJson = globalOpts.json || opts.json;
|
|
1818
|
+
const result = await fetchSkill(serverUrl, token, appName, {
|
|
1819
|
+
json: wantJson,
|
|
1820
|
+
refresh: opts.refresh
|
|
1821
|
+
});
|
|
1822
|
+
if (wantJson) {
|
|
1823
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1824
|
+
} else {
|
|
1825
|
+
console.log(result);
|
|
1826
|
+
}
|
|
1827
|
+
} catch (e) {
|
|
1828
|
+
console.error(pc17.red(`Error: ${e.message}`));
|
|
1829
|
+
process.exitCode = 1;
|
|
1830
|
+
}
|
|
1831
|
+
}).addHelpText(
|
|
1832
|
+
"after",
|
|
1833
|
+
`
|
|
1834
|
+
Examples:
|
|
1835
|
+
$ bot skill List every app with its skill source
|
|
1836
|
+
$ bot skill trading Print the trading app's skill markdown
|
|
1837
|
+
$ bot skill trading --json Structured response (source, url, content)
|
|
1838
|
+
$ bot skill trading --refresh Bypass the 5-minute URL cache
|
|
1839
|
+
$ bot skill --bundle Print every app's skill in one markdown doc
|
|
1840
|
+
|
|
1841
|
+
Sources, in lookup order:
|
|
1842
|
+
manifest.skill (URL) Fetched, cached 5 min
|
|
1843
|
+
manifest.skill (path) Read from disk relative to the app's manifest
|
|
1844
|
+
./SKILL.md (convention) Read if no manifest field
|
|
1845
|
+
auto Generated from registered commands/actions/widgets
|
|
1846
|
+
`
|
|
1847
|
+
);
|
|
1848
|
+
var skillCommand = skillSingle;
|
|
1849
|
+
|
|
1850
|
+
// src/commands/pairing.ts
|
|
1851
|
+
import { hostname as hostname2 } from "os";
|
|
1852
|
+
import { Command as Command17 } from "commander";
|
|
1853
|
+
import pc18 from "picocolors";
|
|
1854
|
+
var pairingCommand = new Command17("pairing").alias("pair").description("Pair this machine as a botapp daemon").option("--token <token>", "Pairing token (skip browser flow)").option("--name <name>", "Daemon name", hostname2()).option("--app-url <url>", "Dashboard URL (defaults to server URL)").option("--no-browser", "Error instead of opening a browser").action(async (opts, cmd) => {
|
|
1855
|
+
const globalOpts = cmd.parent?.opts() ?? {};
|
|
1856
|
+
const serverUrl = resolveServerUrl(globalOpts.server);
|
|
1857
|
+
const name = opts.name;
|
|
1858
|
+
try {
|
|
1859
|
+
const grant = await obtainPairingToken({
|
|
1860
|
+
serverUrl,
|
|
1861
|
+
appUrl: opts.appUrl,
|
|
1862
|
+
name,
|
|
1863
|
+
explicitToken: opts.token,
|
|
1864
|
+
allowBrowser: opts.browser !== false
|
|
1865
|
+
});
|
|
1866
|
+
if (!grant) {
|
|
1867
|
+
console.error(
|
|
1868
|
+
pc18.red(
|
|
1869
|
+
"No pairing token and browser flow disabled. Pass --token, or drop --no-browser."
|
|
1870
|
+
)
|
|
1871
|
+
);
|
|
1872
|
+
process.exitCode = 1;
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
if (grant.userEmail) {
|
|
1876
|
+
console.log(pc18.green(`Authenticated as ${pc18.bold(grant.userEmail)}`));
|
|
1877
|
+
}
|
|
1878
|
+
const res = await fetch(`${serverUrl}/api/daemon/pair`, {
|
|
1879
|
+
method: "POST",
|
|
1880
|
+
headers: { "Content-Type": "application/json" },
|
|
1881
|
+
body: JSON.stringify({
|
|
1882
|
+
token: grant.pairingToken,
|
|
1883
|
+
name,
|
|
1884
|
+
machine: hostname2()
|
|
1885
|
+
})
|
|
1886
|
+
});
|
|
1887
|
+
const data = await res.json().catch(() => ({}));
|
|
1888
|
+
if (!res.ok) {
|
|
1889
|
+
console.error(pc18.red(`Error: ${data.error ?? res.statusText}`));
|
|
1890
|
+
process.exitCode = 1;
|
|
1891
|
+
return;
|
|
1892
|
+
}
|
|
1893
|
+
saveDaemonProfile({
|
|
1894
|
+
server: serverUrl,
|
|
1895
|
+
daemonId: data.daemon.id,
|
|
1896
|
+
daemonName: data.daemon.name,
|
|
1897
|
+
token: data.token
|
|
1898
|
+
});
|
|
1899
|
+
console.log(pc18.green(`Paired daemon: ${pc18.bold(data.daemon.name)}`));
|
|
1900
|
+
console.log(` ID: ${data.daemon.id}`);
|
|
1901
|
+
console.log(` Server: ${serverUrl}`);
|
|
1902
|
+
console.log(pc18.dim("\nNext: botapp daemon agent add codex --command codex"));
|
|
1903
|
+
console.log(pc18.dim("Then: botapp daemon run"));
|
|
1904
|
+
} catch (e) {
|
|
1905
|
+
console.error(pc18.red(`Error: ${e.message}`));
|
|
1906
|
+
process.exitCode = 1;
|
|
1907
|
+
}
|
|
1908
|
+
});
|
|
1909
|
+
async function obtainPairingToken(opts) {
|
|
1910
|
+
if (opts.explicitToken) return { pairingToken: opts.explicitToken };
|
|
1911
|
+
const localToken = await tryLocalFastPath(opts.serverUrl, opts.name);
|
|
1912
|
+
if (localToken) return { pairingToken: localToken };
|
|
1913
|
+
if (!opts.allowBrowser) return null;
|
|
1914
|
+
const appUrl = opts.appUrl ?? opts.serverUrl;
|
|
1915
|
+
const result = await runBrowserAuth({
|
|
1916
|
+
serverUrl: opts.serverUrl,
|
|
1917
|
+
appUrl,
|
|
1918
|
+
scope: "pair",
|
|
1919
|
+
name: opts.name
|
|
1920
|
+
});
|
|
1921
|
+
if (!result.pairingToken) {
|
|
1922
|
+
throw new Error("server returned no pairing token");
|
|
1923
|
+
}
|
|
1924
|
+
return { pairingToken: result.pairingToken, userEmail: result.userEmail };
|
|
1925
|
+
}
|
|
1926
|
+
async function tryLocalFastPath(serverUrl, name) {
|
|
1927
|
+
let mode = null;
|
|
1928
|
+
try {
|
|
1929
|
+
const res = await fetch(`${serverUrl}/health`, {
|
|
1930
|
+
signal: AbortSignal.timeout(3e3)
|
|
1931
|
+
});
|
|
1932
|
+
if (!res.ok) return null;
|
|
1933
|
+
const data = await res.json().catch(() => ({}));
|
|
1934
|
+
mode = data.mode ?? null;
|
|
1935
|
+
} catch {
|
|
1936
|
+
return null;
|
|
1937
|
+
}
|
|
1938
|
+
if (mode !== "local") return null;
|
|
1939
|
+
try {
|
|
1940
|
+
const res = await fetch(`${serverUrl}/api/daemon/pairing-tokens`, {
|
|
1941
|
+
method: "POST",
|
|
1942
|
+
headers: { "Content-Type": "application/json" },
|
|
1943
|
+
body: JSON.stringify({ name })
|
|
1944
|
+
});
|
|
1945
|
+
if (!res.ok) return null;
|
|
1946
|
+
const data = await res.json().catch(() => ({}));
|
|
1947
|
+
return data.token ?? null;
|
|
1948
|
+
} catch {
|
|
1949
|
+
return null;
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
// src/commands/daemon.ts
|
|
1954
|
+
import { spawn as spawn5 } from "child_process";
|
|
1955
|
+
import { randomUUID } from "crypto";
|
|
1956
|
+
import { existsSync as existsSync10, readdirSync, readFileSync as readFileSync8, statSync } from "fs";
|
|
1957
|
+
import { homedir as homedir7 } from "os";
|
|
1958
|
+
import { join as join7, resolve as resolve6 } from "path";
|
|
1959
|
+
import { createInterface as createInterface3 } from "readline";
|
|
1960
|
+
import { Command as Command18 } from "commander";
|
|
1961
|
+
import { WebSocket as WebSocket2 } from "ws";
|
|
1962
|
+
import pc19 from "picocolors";
|
|
1963
|
+
var daemonCommand = new Command18("daemon").description("Manage and run the local botapp daemon");
|
|
1964
|
+
daemonCommand.command("run").description("Run the local daemon and wait for server jobs").action(async () => {
|
|
1965
|
+
const profile = loadDaemonProfile();
|
|
1966
|
+
if (!profile) {
|
|
1967
|
+
console.error(pc19.red("No paired daemon found. Run `bot pair` first."));
|
|
1968
|
+
process.exitCode = 1;
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
await runDaemonConnectionLoop(profile);
|
|
1972
|
+
});
|
|
1973
|
+
daemonCommand.command("stop").description("Stop the background daemon started by `bot launch`").action(async () => {
|
|
1974
|
+
const pidFile = join7(homedir7(), ".botapp", "daemon.pid");
|
|
1975
|
+
if (!existsSync10(pidFile)) {
|
|
1976
|
+
console.log(pc19.yellow("No background daemon PID file found."));
|
|
1977
|
+
return;
|
|
1978
|
+
}
|
|
1979
|
+
try {
|
|
1980
|
+
const pid = parseInt(readFileSync8(pidFile, "utf-8").trim(), 10);
|
|
1981
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
1982
|
+
console.error(pc19.red(`Invalid PID in ${pidFile}`));
|
|
1983
|
+
process.exitCode = 1;
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
process.kill(pid, "SIGTERM");
|
|
1987
|
+
console.log(pc19.green(`Stopped daemon (pid ${pid})`));
|
|
1988
|
+
} catch (e) {
|
|
1989
|
+
console.error(pc19.red(`Failed to stop daemon: ${e.message}`));
|
|
1990
|
+
process.exitCode = 1;
|
|
1991
|
+
}
|
|
1992
|
+
});
|
|
1993
|
+
daemonCommand.command("agent").description("Manage agents hosted by this daemon").addCommand(
|
|
1994
|
+
new Command18("list").description("List registered daemon agents").action(async () => {
|
|
1995
|
+
const profile = requireProfile();
|
|
1996
|
+
if (!profile) return;
|
|
1997
|
+
const data = await daemonRequest(profile.server, profile.token, "/api/daemon/self/agents");
|
|
1998
|
+
for (const agent of data.agents ?? []) {
|
|
1999
|
+
console.log(`${pc19.bold(agent.name)} ${pc19.dim(`(${agent.id})`)}`);
|
|
2000
|
+
console.log(` Kind: ${agent.kind}`);
|
|
2001
|
+
console.log(` Command: ${agent.command} ${(agent.args ?? []).join(" ")}`);
|
|
2002
|
+
if (agent.cwd) console.log(` CWD: ${agent.cwd}`);
|
|
2003
|
+
}
|
|
2004
|
+
})
|
|
2005
|
+
).addCommand(createDaemonAgentConfigCommand()).addCommand(
|
|
2006
|
+
new Command18("add").description("Register a local agent command").argument("<name>", "Name, e.g. codex, claude-code, openclaw, hermes, or hermes-agent").requiredOption("--command <command>", "Executable command").option(
|
|
2007
|
+
"--kind <kind>",
|
|
2008
|
+
"Agent adapter kind: acp, codex, claude-code, openclaw, hermes, hermes-agent, or shell",
|
|
2009
|
+
"acp"
|
|
2010
|
+
).option("--arg <arg...>", "Argument passed to the executable").option("--cwd <cwd>", "Working directory for the agent process").option("--env <entry...>", "Environment entries in KEY=VALUE form").action(async (name, opts) => {
|
|
2011
|
+
const profile = requireProfile();
|
|
2012
|
+
if (!profile) return;
|
|
2013
|
+
const env = parseEnv(opts.env ?? []);
|
|
2014
|
+
const data = await daemonRequest(
|
|
2015
|
+
profile.server,
|
|
2016
|
+
profile.token,
|
|
2017
|
+
"/api/daemon/self/agents",
|
|
2018
|
+
{
|
|
2019
|
+
method: "POST",
|
|
2020
|
+
body: {
|
|
2021
|
+
name,
|
|
2022
|
+
kind: opts.kind,
|
|
2023
|
+
command: opts.command,
|
|
2024
|
+
args: opts.arg ?? [],
|
|
2025
|
+
cwd: opts.cwd,
|
|
2026
|
+
env
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
);
|
|
2030
|
+
console.log(pc19.green(`Registered daemon agent: ${pc19.bold(data.agent.name)}`));
|
|
2031
|
+
console.log(` ID: ${data.agent.id}`);
|
|
2032
|
+
console.log(` Command: ${data.agent.command} ${(data.agent.args ?? []).join(" ")}`);
|
|
2033
|
+
})
|
|
2034
|
+
).addCommand(
|
|
2035
|
+
new Command18("remove").alias("rm").description("Remove a daemon agent by id or name").argument("<agent>", "Daemon agent id or name").action(async (agent) => {
|
|
2036
|
+
const profile = requireProfile();
|
|
2037
|
+
if (!profile) return;
|
|
2038
|
+
const data = await daemonRequest(
|
|
2039
|
+
profile.server,
|
|
2040
|
+
profile.token,
|
|
2041
|
+
`/api/daemon/self/agents/${encodeURIComponent(agent)}`,
|
|
2042
|
+
{ method: "DELETE" }
|
|
2043
|
+
);
|
|
2044
|
+
console.log(pc19.green(`Removed daemon agent: ${pc19.bold(data.agent.name)}`));
|
|
2045
|
+
console.log(` ID: ${data.agent.id}`);
|
|
2046
|
+
})
|
|
2047
|
+
);
|
|
2048
|
+
async function runDaemonConnectionLoop(profile) {
|
|
2049
|
+
const wsUrl = `${profile.server.replace(/^http/, "ws")}/ws/daemon?token=${profile.token}`;
|
|
2050
|
+
const baseRetryMs = envNumber(process.env, "BOTAPP_DAEMON_RETRY_MS", 1e3);
|
|
2051
|
+
const maxRetryMs = envNumber(process.env, "BOTAPP_DAEMON_MAX_RETRY_MS", 3e4);
|
|
2052
|
+
let stopped = false;
|
|
2053
|
+
let activeWs = null;
|
|
2054
|
+
let retryMs = baseRetryMs;
|
|
2055
|
+
let retryTimer = null;
|
|
2056
|
+
let resolveRetry = null;
|
|
2057
|
+
const stop = () => {
|
|
2058
|
+
stopped = true;
|
|
2059
|
+
if (activeWs && activeWs.readyState !== WebSocket2.CLOSED) {
|
|
2060
|
+
activeWs.close();
|
|
2061
|
+
}
|
|
2062
|
+
if (retryTimer) {
|
|
2063
|
+
clearTimeout(retryTimer);
|
|
2064
|
+
retryTimer = null;
|
|
2065
|
+
}
|
|
2066
|
+
resolveRetry?.();
|
|
2067
|
+
};
|
|
2068
|
+
process.once("SIGINT", stop);
|
|
2069
|
+
process.once("SIGTERM", stop);
|
|
2070
|
+
while (!stopped) {
|
|
2071
|
+
console.log(pc19.blue(`Connecting daemon ${profile.daemonName} to ${profile.server}...`));
|
|
2072
|
+
const outcome = await runDaemonSocket(wsUrl, (ws) => {
|
|
2073
|
+
activeWs = ws;
|
|
2074
|
+
});
|
|
2075
|
+
activeWs = null;
|
|
2076
|
+
if (stopped) break;
|
|
2077
|
+
const waitMs = outcome.opened ? baseRetryMs : retryMs;
|
|
2078
|
+
retryMs = outcome.opened ? baseRetryMs : Math.min(retryMs * 2, maxRetryMs);
|
|
2079
|
+
console.log(pc19.yellow(`Retrying daemon connection in ${Math.round(waitMs / 1e3)}s...`));
|
|
2080
|
+
await new Promise((resolveWait) => {
|
|
2081
|
+
resolveRetry = resolveWait;
|
|
2082
|
+
retryTimer = setTimeout(() => {
|
|
2083
|
+
retryTimer = null;
|
|
2084
|
+
resolveRetry = null;
|
|
2085
|
+
resolveWait();
|
|
2086
|
+
}, waitMs);
|
|
2087
|
+
});
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
function runDaemonSocket(wsUrl, setActiveWs) {
|
|
2091
|
+
return new Promise((resolveRun) => {
|
|
2092
|
+
const ws = new WebSocket2(wsUrl);
|
|
2093
|
+
setActiveWs(ws);
|
|
2094
|
+
let opened = false;
|
|
2095
|
+
let settled = false;
|
|
2096
|
+
let ping = null;
|
|
2097
|
+
function finish() {
|
|
2098
|
+
if (settled) return;
|
|
2099
|
+
settled = true;
|
|
2100
|
+
if (ping) clearInterval(ping);
|
|
2101
|
+
resolveRun({ opened });
|
|
2102
|
+
}
|
|
2103
|
+
ws.on("open", () => {
|
|
2104
|
+
opened = true;
|
|
2105
|
+
console.log(pc19.green("Daemon connected"));
|
|
2106
|
+
ws.send(JSON.stringify({ type: "daemon_ready" }));
|
|
2107
|
+
ping = setInterval(() => {
|
|
2108
|
+
if (ws.readyState === WebSocket2.OPEN) ws.send(JSON.stringify({ type: "ping" }));
|
|
2109
|
+
}, 3e4);
|
|
2110
|
+
});
|
|
2111
|
+
ws.on("message", (raw) => {
|
|
2112
|
+
void handleFrame(ws, raw.toString());
|
|
2113
|
+
});
|
|
2114
|
+
ws.on("close", (code, reason) => {
|
|
2115
|
+
console.log(pc19.yellow(`Daemon disconnected (${code}: ${reason.toString() || "no reason"})`));
|
|
2116
|
+
finish();
|
|
2117
|
+
});
|
|
2118
|
+
ws.on("error", (err) => {
|
|
2119
|
+
console.error(pc19.red(`Daemon WebSocket error: ${err.message}`));
|
|
2120
|
+
if (!opened) {
|
|
2121
|
+
if (ws.readyState !== WebSocket2.CLOSED) ws.terminate();
|
|
2122
|
+
finish();
|
|
2123
|
+
}
|
|
2124
|
+
});
|
|
2125
|
+
});
|
|
2126
|
+
}
|
|
2127
|
+
async function handleFrame(ws, raw) {
|
|
2128
|
+
const frame = JSON.parse(raw);
|
|
2129
|
+
if (frame.type === "daemon_job") {
|
|
2130
|
+
const job = frame.job;
|
|
2131
|
+
console.log(pc19.blue(`Running ${job.agent.name} job ${job.id}`));
|
|
2132
|
+
ws.send(JSON.stringify({ type: "daemon_job_started", jobId: job.id }));
|
|
2133
|
+
try {
|
|
2134
|
+
const result = await runAgentJob(job, (update) => {
|
|
2135
|
+
if (ws.readyState === WebSocket2.OPEN) {
|
|
2136
|
+
ws.send(JSON.stringify({
|
|
2137
|
+
type: "daemon_job_update",
|
|
2138
|
+
jobId: job.id,
|
|
2139
|
+
update
|
|
2140
|
+
}));
|
|
2141
|
+
}
|
|
2142
|
+
});
|
|
2143
|
+
ws.send(JSON.stringify({
|
|
2144
|
+
type: "daemon_job_result",
|
|
2145
|
+
jobId: job.id,
|
|
2146
|
+
status: "succeeded",
|
|
2147
|
+
result
|
|
2148
|
+
}));
|
|
2149
|
+
console.log(pc19.green(`Completed job ${job.id}`));
|
|
2150
|
+
} catch (e) {
|
|
2151
|
+
ws.send(JSON.stringify({
|
|
2152
|
+
type: "daemon_job_result",
|
|
2153
|
+
jobId: job.id,
|
|
2154
|
+
status: "failed",
|
|
2155
|
+
error: e.message
|
|
2156
|
+
}));
|
|
2157
|
+
console.error(pc19.red(`Job ${job.id} failed: ${e.message}`));
|
|
2158
|
+
}
|
|
2159
|
+
return;
|
|
2160
|
+
}
|
|
2161
|
+
if (frame.type === "error") {
|
|
2162
|
+
console.error(pc19.red(`Server error: ${frame.message}`));
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
async function runAgentJob(job, update) {
|
|
2166
|
+
if (job.agent.kind === "codex") {
|
|
2167
|
+
return runCodexAgent(job, update);
|
|
2168
|
+
}
|
|
2169
|
+
if (job.agent.kind === "claude-code" || job.agent.kind === "claude_code") {
|
|
2170
|
+
return runClaudeCodeAgent(job, update);
|
|
2171
|
+
}
|
|
2172
|
+
if (job.agent.kind === "openclaw" || job.agent.kind === "kimiclaw") {
|
|
2173
|
+
return runOpenClawAgent(job, update);
|
|
2174
|
+
}
|
|
2175
|
+
if (job.agent.kind === "hermes" || job.agent.kind === "hermes-agent") {
|
|
2176
|
+
return runHermesAgent(job, update);
|
|
2177
|
+
}
|
|
2178
|
+
if (job.agent.kind === "shell") {
|
|
2179
|
+
return runShellAgent(job);
|
|
2180
|
+
}
|
|
2181
|
+
return runAcpAgent(job);
|
|
2182
|
+
}
|
|
2183
|
+
async function runShellAgent(job) {
|
|
2184
|
+
const child = spawn5(job.agent.command, [...job.agent.args, job.query], {
|
|
2185
|
+
cwd: job.agent.cwd ?? process.cwd(),
|
|
2186
|
+
env: { ...process.env, ...job.agent.env ?? {} }
|
|
2187
|
+
});
|
|
2188
|
+
let stdout = "";
|
|
2189
|
+
let stderr = "";
|
|
2190
|
+
child.stdout.on("data", (chunk) => {
|
|
2191
|
+
stdout += chunk.toString();
|
|
2192
|
+
});
|
|
2193
|
+
child.stderr.on("data", (chunk) => {
|
|
2194
|
+
stderr += chunk.toString();
|
|
2195
|
+
});
|
|
2196
|
+
const code = await new Promise((resolve7) => child.on("close", resolve7));
|
|
2197
|
+
if (code !== 0) {
|
|
2198
|
+
throw new Error(stderr.trim() || `Agent exited with code ${code}`);
|
|
2199
|
+
}
|
|
2200
|
+
return stdout.trim();
|
|
2201
|
+
}
|
|
2202
|
+
async function runCodexAgent(job, update) {
|
|
2203
|
+
const args = [...job.agent.args];
|
|
2204
|
+
if (!args.includes("exec") && !args.includes("e")) {
|
|
2205
|
+
args.unshift("exec");
|
|
2206
|
+
}
|
|
2207
|
+
if (!args.includes("--json")) {
|
|
2208
|
+
args.push("--json");
|
|
2209
|
+
}
|
|
2210
|
+
if (!args.includes("--skip-git-repo-check")) {
|
|
2211
|
+
args.push("--skip-git-repo-check");
|
|
2212
|
+
}
|
|
2213
|
+
if (!hasAnyFlag(args, [
|
|
2214
|
+
"--sandbox",
|
|
2215
|
+
"-s",
|
|
2216
|
+
"--ask-for-approval",
|
|
2217
|
+
"-a",
|
|
2218
|
+
"--full-auto",
|
|
2219
|
+
"--dangerously-bypass-approvals-and-sandbox"
|
|
2220
|
+
])) {
|
|
2221
|
+
args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
2222
|
+
}
|
|
2223
|
+
args.push(job.query);
|
|
2224
|
+
const child = spawn5(job.agent.command, args, {
|
|
2225
|
+
cwd: job.agent.cwd ?? process.cwd(),
|
|
2226
|
+
env: { ...process.env, ...job.agent.env ?? {} },
|
|
2227
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2228
|
+
});
|
|
2229
|
+
let stderr = "";
|
|
2230
|
+
child.stderr.on("data", (chunk) => {
|
|
2231
|
+
stderr += chunk.toString();
|
|
2232
|
+
});
|
|
2233
|
+
const result = {
|
|
2234
|
+
kind: "codex",
|
|
2235
|
+
text: "",
|
|
2236
|
+
messages: [],
|
|
2237
|
+
toolCalls: [],
|
|
2238
|
+
rawEvents: []
|
|
2239
|
+
};
|
|
2240
|
+
const toolCalls = /* @__PURE__ */ new Map();
|
|
2241
|
+
function processLine(line) {
|
|
2242
|
+
if (!line.trim()) return;
|
|
2243
|
+
try {
|
|
2244
|
+
const event = JSON.parse(line);
|
|
2245
|
+
result.rawEvents.push(event);
|
|
2246
|
+
update({ kind: "codex_event", event });
|
|
2247
|
+
if (event?.type === "item.completed" && event.item?.type === "agent_message") {
|
|
2248
|
+
if (typeof event.item.text === "string") {
|
|
2249
|
+
result.messages.push(event.item.text);
|
|
2250
|
+
update({ kind: "message", text: event.item.text });
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
if ((event?.type === "item.started" || event?.type === "item.completed") && typeof event.item?.id === "string" && event.item?.type && event.item.type !== "agent_message") {
|
|
2254
|
+
const existing = toolCalls.get(event.item.id) ?? {
|
|
2255
|
+
id: event.item.id,
|
|
2256
|
+
type: String(event.item.type)
|
|
2257
|
+
};
|
|
2258
|
+
const next = {
|
|
2259
|
+
...existing,
|
|
2260
|
+
type: String(event.item.type),
|
|
2261
|
+
status: event.item.status ?? existing.status,
|
|
2262
|
+
command: event.item.command ?? existing.command,
|
|
2263
|
+
output: event.item.aggregated_output ?? event.item.output ?? existing.output,
|
|
2264
|
+
exitCode: event.item.exit_code ?? existing.exitCode
|
|
2265
|
+
};
|
|
2266
|
+
toolCalls.set(event.item.id, next);
|
|
2267
|
+
update({ kind: "tool_call", toolCall: next });
|
|
2268
|
+
}
|
|
2269
|
+
} catch {
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
const rl = createInterface3({ input: child.stdout });
|
|
2273
|
+
rl.on("line", processLine);
|
|
2274
|
+
const code = await new Promise((resolve7) => child.on("close", resolve7));
|
|
2275
|
+
if (code !== 0) {
|
|
2276
|
+
throw new Error(stderr.trim() || `Codex exited with code ${code}`);
|
|
2277
|
+
}
|
|
2278
|
+
result.text = result.messages.at(-1)?.trim() || "";
|
|
2279
|
+
result.toolCalls = [...toolCalls.values()];
|
|
2280
|
+
return JSON.stringify(result);
|
|
2281
|
+
}
|
|
2282
|
+
async function runClaudeCodeAgent(job, update) {
|
|
2283
|
+
const args = [...job.agent.args];
|
|
2284
|
+
if (!args.includes("-p") && !args.includes("--print")) {
|
|
2285
|
+
args.push("-p");
|
|
2286
|
+
}
|
|
2287
|
+
if (!args.includes("--output-format")) {
|
|
2288
|
+
args.push("--output-format", "stream-json");
|
|
2289
|
+
}
|
|
2290
|
+
if (!args.includes("--verbose")) {
|
|
2291
|
+
args.push("--verbose");
|
|
2292
|
+
}
|
|
2293
|
+
if (!hasAnyFlag(args, [
|
|
2294
|
+
"--permission-mode",
|
|
2295
|
+
"--dangerously-skip-permissions",
|
|
2296
|
+
"--allow-dangerously-skip-permissions"
|
|
2297
|
+
])) {
|
|
2298
|
+
args.push("--permission-mode", "bypassPermissions");
|
|
2299
|
+
}
|
|
2300
|
+
const resume = job.resumeSessionId ?? job.agent.env?.CLAUDE_SESSION_ID ?? null;
|
|
2301
|
+
if (resume && !args.includes("--resume")) {
|
|
2302
|
+
args.push("--resume", resume);
|
|
2303
|
+
}
|
|
2304
|
+
const child = spawn5(job.agent.command, args, {
|
|
2305
|
+
cwd: job.agent.cwd ?? process.cwd(),
|
|
2306
|
+
env: { ...process.env, ...job.agent.env ?? {} },
|
|
2307
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2308
|
+
});
|
|
2309
|
+
child.stdin.write(job.query);
|
|
2310
|
+
child.stdin.end();
|
|
2311
|
+
let stderr = "";
|
|
2312
|
+
child.stderr.on("data", (chunk) => {
|
|
2313
|
+
stderr += chunk.toString();
|
|
2314
|
+
});
|
|
2315
|
+
const result = {
|
|
2316
|
+
kind: "claude-code",
|
|
2317
|
+
text: "",
|
|
2318
|
+
messages: [],
|
|
2319
|
+
toolCalls: [],
|
|
2320
|
+
sessionId: resume,
|
|
2321
|
+
rawEvents: []
|
|
2322
|
+
};
|
|
2323
|
+
const toolCalls = /* @__PURE__ */ new Map();
|
|
2324
|
+
function captureSessionId(event) {
|
|
2325
|
+
const id = event?.session_id ?? event?.sessionId;
|
|
2326
|
+
if (typeof id === "string" && id.length > 0) {
|
|
2327
|
+
result.sessionId = id;
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
function ingestAssistantContent(blocks) {
|
|
2331
|
+
for (const block of blocks) {
|
|
2332
|
+
if (!block || typeof block !== "object") continue;
|
|
2333
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
2334
|
+
result.messages.push(block.text);
|
|
2335
|
+
update({ kind: "message", text: block.text });
|
|
2336
|
+
}
|
|
2337
|
+
if (block.type === "tool_use" && typeof block.id === "string") {
|
|
2338
|
+
const existing = toolCalls.get(block.id) ?? { id: block.id, name: String(block.name ?? "tool") };
|
|
2339
|
+
const next = {
|
|
2340
|
+
...existing,
|
|
2341
|
+
name: String(block.name ?? existing.name ?? "tool"),
|
|
2342
|
+
input: block.input ?? existing.input
|
|
2343
|
+
};
|
|
2344
|
+
toolCalls.set(block.id, next);
|
|
2345
|
+
update({ kind: "tool_call", toolCall: next });
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
function ingestUserContent(blocks) {
|
|
2350
|
+
for (const block of blocks) {
|
|
2351
|
+
if (!block || typeof block !== "object") continue;
|
|
2352
|
+
if (block.type !== "tool_result") continue;
|
|
2353
|
+
const id = block.tool_use_id;
|
|
2354
|
+
if (typeof id !== "string") continue;
|
|
2355
|
+
const existing = toolCalls.get(id) ?? { id, name: "tool" };
|
|
2356
|
+
const output2 = typeof block.content === "string" ? block.content : Array.isArray(block.content) ? block.content.map((item) => typeof item?.text === "string" ? item.text : "").join("") : void 0;
|
|
2357
|
+
const next = {
|
|
2358
|
+
...existing,
|
|
2359
|
+
output: output2 ?? existing.output,
|
|
2360
|
+
isError: Boolean(block.is_error ?? existing.isError)
|
|
2361
|
+
};
|
|
2362
|
+
toolCalls.set(id, next);
|
|
2363
|
+
update({ kind: "tool_call", toolCall: next });
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
function processLine(line) {
|
|
2367
|
+
if (!line.trim()) return;
|
|
2368
|
+
let event;
|
|
2369
|
+
try {
|
|
2370
|
+
event = JSON.parse(line);
|
|
2371
|
+
} catch {
|
|
2372
|
+
return;
|
|
2373
|
+
}
|
|
2374
|
+
result.rawEvents.push(event);
|
|
2375
|
+
update({ kind: "claude_code_event", event });
|
|
2376
|
+
captureSessionId(event);
|
|
2377
|
+
if (event.type === "assistant" && event.message?.content) {
|
|
2378
|
+
ingestAssistantContent(
|
|
2379
|
+
Array.isArray(event.message.content) ? event.message.content : []
|
|
2380
|
+
);
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
if (event.type === "user" && event.message?.content) {
|
|
2384
|
+
ingestUserContent(
|
|
2385
|
+
Array.isArray(event.message.content) ? event.message.content : []
|
|
2386
|
+
);
|
|
2387
|
+
return;
|
|
2388
|
+
}
|
|
2389
|
+
if (event.type === "result" && typeof event.result === "string") {
|
|
2390
|
+
result.text = event.result;
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
const rl = createInterface3({ input: child.stdout });
|
|
2394
|
+
rl.on("line", processLine);
|
|
2395
|
+
const code = await new Promise((resolve7) => child.on("close", resolve7));
|
|
2396
|
+
if (code !== 0) {
|
|
2397
|
+
throw new Error(stderr.trim() || `Claude Code exited with code ${code}`);
|
|
2398
|
+
}
|
|
2399
|
+
if (!result.text) {
|
|
2400
|
+
result.text = result.messages.at(-1)?.trim() ?? "";
|
|
2401
|
+
}
|
|
2402
|
+
result.toolCalls = [...toolCalls.values()];
|
|
2403
|
+
return JSON.stringify(result);
|
|
2404
|
+
}
|
|
2405
|
+
async function runOpenClawAgent(job, update) {
|
|
2406
|
+
const env = { ...process.env, ...job.agent.env ?? {} };
|
|
2407
|
+
const args = [...job.agent.args];
|
|
2408
|
+
if (!args.includes("agent")) {
|
|
2409
|
+
args.unshift("agent");
|
|
2410
|
+
}
|
|
2411
|
+
let usedPromptPlaceholder = false;
|
|
2412
|
+
for (let i = 0; i < args.length; i++) {
|
|
2413
|
+
if (!args[i].includes("{prompt}")) continue;
|
|
2414
|
+
args[i] = args[i].replaceAll("{prompt}", job.query);
|
|
2415
|
+
usedPromptPlaceholder = true;
|
|
2416
|
+
}
|
|
2417
|
+
if (!usedPromptPlaceholder) {
|
|
2418
|
+
const promptArg = env.BOTAPP_OPENCLAW_PROMPT_ARG ?? env.OPENCLAW_PROMPT_ARG ?? "--message";
|
|
2419
|
+
if (promptArg) {
|
|
2420
|
+
args.push(promptArg, job.query);
|
|
2421
|
+
} else {
|
|
2422
|
+
args.push(job.query);
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
let requestedSessionId = getFlagValue(args, "--session-id");
|
|
2426
|
+
if (!requestedSessionId) {
|
|
2427
|
+
requestedSessionId = job.resumeSessionId || `botapp-${randomUUID()}`;
|
|
2428
|
+
args.push("--session-id", requestedSessionId);
|
|
2429
|
+
}
|
|
2430
|
+
if (!hasFlag(args, "--verbose")) {
|
|
2431
|
+
args.push("--verbose", "on");
|
|
2432
|
+
}
|
|
2433
|
+
const sessionDir = resolveOpenClawSessionDir(args, env);
|
|
2434
|
+
const state = {
|
|
2435
|
+
startedAtMs: Date.now(),
|
|
2436
|
+
sessionDir,
|
|
2437
|
+
sessionStorePath: join7(sessionDir, "sessions.json"),
|
|
2438
|
+
requestedSessionId,
|
|
2439
|
+
sessionKey: null,
|
|
2440
|
+
sessionId: null,
|
|
2441
|
+
sessionResolvedBy: null,
|
|
2442
|
+
initialOffsets: snapshotOpenClawSessionOffsets(sessionDir),
|
|
2443
|
+
selectedFile: null,
|
|
2444
|
+
offset: 0,
|
|
2445
|
+
pending: "",
|
|
2446
|
+
seenEventIds: /* @__PURE__ */ new Set()
|
|
2447
|
+
};
|
|
2448
|
+
const result = {
|
|
2449
|
+
kind: "openclaw",
|
|
2450
|
+
text: "",
|
|
2451
|
+
messages: [],
|
|
2452
|
+
toolCalls: [],
|
|
2453
|
+
sessionId: null,
|
|
2454
|
+
requestedSessionId,
|
|
2455
|
+
sessionKey: null,
|
|
2456
|
+
sessionFile: null,
|
|
2457
|
+
rawEvents: []
|
|
2458
|
+
};
|
|
2459
|
+
const toolCalls = /* @__PURE__ */ new Map();
|
|
2460
|
+
let stderr = "";
|
|
2461
|
+
let stopPolling = false;
|
|
2462
|
+
const child = spawn5(job.agent.command, args, {
|
|
2463
|
+
cwd: job.agent.cwd ?? process.cwd(),
|
|
2464
|
+
env,
|
|
2465
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2466
|
+
});
|
|
2467
|
+
const stdout = createInterface3({ input: child.stdout });
|
|
2468
|
+
stdout.on("line", () => {
|
|
2469
|
+
});
|
|
2470
|
+
const stderrReader = createInterface3({ input: child.stderr });
|
|
2471
|
+
stderrReader.on("line", (line) => {
|
|
2472
|
+
if (!line.trim()) return;
|
|
2473
|
+
stderr += `${line}
|
|
2474
|
+
`;
|
|
2475
|
+
});
|
|
2476
|
+
const pollIntervalMs = envNumber(env, "BOTAPP_OPENCLAW_POLL_INTERVAL_MS", 500);
|
|
2477
|
+
const stableAfterExitMs = envNumber(env, "BOTAPP_OPENCLAW_STABLE_AFTER_EXIT_MS", 1e3);
|
|
2478
|
+
const timeoutMs = resolveOpenClawTimeoutMs(env);
|
|
2479
|
+
const tailTask = (async () => {
|
|
2480
|
+
while (!stopPolling) {
|
|
2481
|
+
readOpenClawSessionUpdates(state, result, toolCalls, update);
|
|
2482
|
+
await sleep(pollIntervalMs);
|
|
2483
|
+
}
|
|
2484
|
+
readOpenClawSessionUpdates(state, result, toolCalls, update, true);
|
|
2485
|
+
})();
|
|
2486
|
+
let code;
|
|
2487
|
+
try {
|
|
2488
|
+
code = await waitForChild(child, timeoutMs, "OpenClaw");
|
|
2489
|
+
await sleep(stableAfterExitMs);
|
|
2490
|
+
} finally {
|
|
2491
|
+
stopPolling = true;
|
|
2492
|
+
await tailTask;
|
|
2493
|
+
}
|
|
2494
|
+
if (state.pending.trim()) {
|
|
2495
|
+
handleOpenClawJsonlLine(state.pending.trim(), state, result, toolCalls, update);
|
|
2496
|
+
state.pending = "";
|
|
2497
|
+
}
|
|
2498
|
+
result.sessionFile = state.selectedFile;
|
|
2499
|
+
result.toolCalls = [...toolCalls.values()];
|
|
2500
|
+
result.text = result.text || result.messages.at(-1)?.trim() || "";
|
|
2501
|
+
if (code !== 0) {
|
|
2502
|
+
throw new Error(stderr.trim() || `OpenClaw exited with code ${code}`);
|
|
2503
|
+
}
|
|
2504
|
+
if (!result.sessionId) {
|
|
2505
|
+
throw new Error(
|
|
2506
|
+
[
|
|
2507
|
+
"OpenClaw completed, but the session key was not resolved from sessions.json.",
|
|
2508
|
+
`Session store: ${state.sessionStorePath}`,
|
|
2509
|
+
`Requested session id fragment: ${state.requestedSessionId}`
|
|
2510
|
+
].join("\n")
|
|
2511
|
+
);
|
|
2512
|
+
}
|
|
2513
|
+
if (!result.text && result.toolCalls.length === 0) {
|
|
2514
|
+
throw new Error(
|
|
2515
|
+
[
|
|
2516
|
+
"OpenClaw completed, but no assistant message was found in session JSONL.",
|
|
2517
|
+
`Session directory: ${sessionDir}`,
|
|
2518
|
+
state.selectedFile ? `Selected session file: ${state.selectedFile}` : "Selected session file: none",
|
|
2519
|
+
"Check BOTAPP_OPENCLAW_SESSION_DIR, --agent, or the OpenClaw JSONL message format."
|
|
2520
|
+
].join("\n")
|
|
2521
|
+
);
|
|
2522
|
+
}
|
|
2523
|
+
return JSON.stringify(result);
|
|
2524
|
+
}
|
|
2525
|
+
function resolveOpenClawSessionDir(args, env) {
|
|
2526
|
+
const configured = env.BOTAPP_OPENCLAW_SESSION_DIR ?? env.OPENCLAW_SESSION_DIR;
|
|
2527
|
+
if (configured) return expandPath(configured);
|
|
2528
|
+
const agentName = resolveOpenClawAgentName(args);
|
|
2529
|
+
return join7(homedir7(), ".openclaw", "agents", agentName, "sessions");
|
|
2530
|
+
}
|
|
2531
|
+
function resolveOpenClawAgentName(args) {
|
|
2532
|
+
return getFlagValue(args, "--agent") ?? "main";
|
|
2533
|
+
}
|
|
2534
|
+
function snapshotOpenClawSessionOffsets(sessionDir) {
|
|
2535
|
+
const offsets = /* @__PURE__ */ new Map();
|
|
2536
|
+
for (const file of listOpenClawSessionFiles(sessionDir)) {
|
|
2537
|
+
try {
|
|
2538
|
+
offsets.set(file, statSync(file).size);
|
|
2539
|
+
} catch {
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
return offsets;
|
|
2543
|
+
}
|
|
2544
|
+
function readOpenClawSessionUpdates(state, result, toolCalls, update, final = false) {
|
|
2545
|
+
resolveOpenClawRealSessionId(state, result);
|
|
2546
|
+
if (!state.sessionId) return;
|
|
2547
|
+
const selectedFile = state.selectedFile ?? selectOpenClawSessionFile(state);
|
|
2548
|
+
if (!selectedFile) return;
|
|
2549
|
+
if (!state.selectedFile) {
|
|
2550
|
+
state.selectedFile = selectedFile;
|
|
2551
|
+
state.offset = state.sessionResolvedBy === "key-fragment" ? 0 : state.initialOffsets.get(selectedFile) ?? 0;
|
|
2552
|
+
}
|
|
2553
|
+
let data = "";
|
|
2554
|
+
try {
|
|
2555
|
+
const buffer = readFileSync8(selectedFile);
|
|
2556
|
+
if (state.offset > buffer.length) state.offset = 0;
|
|
2557
|
+
data = buffer.subarray(state.offset).toString("utf-8");
|
|
2558
|
+
state.offset = buffer.length;
|
|
2559
|
+
} catch {
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
if (!data && !final) return;
|
|
2563
|
+
state.pending += data;
|
|
2564
|
+
if (!state.pending) return;
|
|
2565
|
+
const parts = state.pending.split("\n");
|
|
2566
|
+
const completeLines = final || state.pending.endsWith("\n") ? parts : parts.slice(0, -1);
|
|
2567
|
+
state.pending = final || state.pending.endsWith("\n") ? "" : parts.at(-1) ?? "";
|
|
2568
|
+
for (const rawLine of completeLines) {
|
|
2569
|
+
const line = rawLine.trim();
|
|
2570
|
+
if (line) handleOpenClawJsonlLine(line, state, result, toolCalls, update);
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
function resolveOpenClawRealSessionId(state, result) {
|
|
2574
|
+
if (state.sessionId) return;
|
|
2575
|
+
let parsed;
|
|
2576
|
+
try {
|
|
2577
|
+
parsed = JSON.parse(readFileSync8(state.sessionStorePath, "utf-8"));
|
|
2578
|
+
} catch {
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2581
|
+
if (!parsed || typeof parsed !== "object") return;
|
|
2582
|
+
let entry = null;
|
|
2583
|
+
let sessionKey = null;
|
|
2584
|
+
let resolvedBy = null;
|
|
2585
|
+
for (const candidateKey of Object.keys(parsed)) {
|
|
2586
|
+
if (!candidateKey.includes(state.requestedSessionId)) continue;
|
|
2587
|
+
sessionKey = candidateKey;
|
|
2588
|
+
entry = parsed[candidateKey];
|
|
2589
|
+
resolvedBy = "key-fragment";
|
|
2590
|
+
break;
|
|
2591
|
+
}
|
|
2592
|
+
if (!entry) {
|
|
2593
|
+
for (const [candidateKey, candidateEntry] of Object.entries(parsed)) {
|
|
2594
|
+
if (candidateEntry && typeof candidateEntry === "object" && candidateEntry.sessionId === state.requestedSessionId) {
|
|
2595
|
+
sessionKey = candidateKey;
|
|
2596
|
+
entry = candidateEntry;
|
|
2597
|
+
resolvedBy = "entry-session-id";
|
|
2598
|
+
break;
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
const realSessionId = entry?.sessionId;
|
|
2603
|
+
if (typeof realSessionId !== "string" || !realSessionId.trim()) return;
|
|
2604
|
+
state.sessionKey = sessionKey;
|
|
2605
|
+
state.sessionId = realSessionId;
|
|
2606
|
+
state.sessionResolvedBy = resolvedBy;
|
|
2607
|
+
result.sessionKey = sessionKey;
|
|
2608
|
+
result.sessionId = realSessionId;
|
|
2609
|
+
}
|
|
2610
|
+
function selectOpenClawSessionFile(state) {
|
|
2611
|
+
const files = listOpenClawSessionFiles(state.sessionDir);
|
|
2612
|
+
if (files.length === 0) return null;
|
|
2613
|
+
if (state.sessionId) {
|
|
2614
|
+
const exact = join7(state.sessionDir, `${state.sessionId}.jsonl`);
|
|
2615
|
+
if (files.includes(exact)) return exact;
|
|
2616
|
+
const matching = files.filter((file) => file.includes(state.sessionId ?? ""));
|
|
2617
|
+
if (matching.length > 0) return newestFile(matching);
|
|
2618
|
+
}
|
|
2619
|
+
if (files.length === 1) return files[0];
|
|
2620
|
+
const recent = files.filter((file) => {
|
|
2621
|
+
try {
|
|
2622
|
+
return statSync(file).mtimeMs >= state.startedAtMs - 1e3;
|
|
2623
|
+
} catch {
|
|
2624
|
+
return false;
|
|
2625
|
+
}
|
|
2626
|
+
});
|
|
2627
|
+
return newestFile(recent.length > 0 ? recent : files);
|
|
2628
|
+
}
|
|
2629
|
+
function listOpenClawSessionFiles(sessionDir) {
|
|
2630
|
+
try {
|
|
2631
|
+
if (!existsSync10(sessionDir)) return [];
|
|
2632
|
+
return readdirSync(sessionDir).filter((name) => name.endsWith(".jsonl")).map((name) => join7(sessionDir, name)).filter((file) => {
|
|
2633
|
+
try {
|
|
2634
|
+
return statSync(file).isFile();
|
|
2635
|
+
} catch {
|
|
2636
|
+
return false;
|
|
2637
|
+
}
|
|
2638
|
+
});
|
|
2639
|
+
} catch {
|
|
2640
|
+
return [];
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
function newestFile(files) {
|
|
2644
|
+
if (files.length === 0) return null;
|
|
2645
|
+
return files.reduce((selected, file) => {
|
|
2646
|
+
try {
|
|
2647
|
+
return statSync(file).mtimeMs > statSync(selected).mtimeMs ? file : selected;
|
|
2648
|
+
} catch {
|
|
2649
|
+
return selected;
|
|
2650
|
+
}
|
|
2651
|
+
}, files[0]);
|
|
2652
|
+
}
|
|
2653
|
+
function handleOpenClawJsonlLine(line, state, result, toolCalls, update) {
|
|
2654
|
+
let event;
|
|
2655
|
+
try {
|
|
2656
|
+
event = JSON.parse(line);
|
|
2657
|
+
} catch {
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
if (!event || typeof event !== "object") return;
|
|
2661
|
+
const eventId = event.id;
|
|
2662
|
+
if (typeof eventId === "string") {
|
|
2663
|
+
if (state.seenEventIds.has(eventId)) return;
|
|
2664
|
+
state.seenEventIds.add(eventId);
|
|
2665
|
+
}
|
|
2666
|
+
result.rawEvents.push(event);
|
|
2667
|
+
update({ kind: "openclaw_event", event });
|
|
2668
|
+
if (event.type !== "message" || !event.message || typeof event.message !== "object") {
|
|
2669
|
+
return;
|
|
2670
|
+
}
|
|
2671
|
+
const message = event.message;
|
|
2672
|
+
if (message.role === "assistant") {
|
|
2673
|
+
ingestOpenClawAssistantMessage(event, result, toolCalls, update);
|
|
2674
|
+
}
|
|
2675
|
+
if (message.role === "toolResult") {
|
|
2676
|
+
ingestOpenClawToolResult(event, toolCalls, update);
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
function ingestOpenClawAssistantMessage(event, result, toolCalls, update) {
|
|
2680
|
+
const message = event.message;
|
|
2681
|
+
const text = openClawContentText(message.content);
|
|
2682
|
+
if (text) {
|
|
2683
|
+
result.messages.push(text);
|
|
2684
|
+
result.text = text;
|
|
2685
|
+
update({ kind: "message", text });
|
|
2686
|
+
}
|
|
2687
|
+
for (const part of openClawContentParts(message.content)) {
|
|
2688
|
+
if (part.type !== "toolCall" || typeof part.id !== "string") continue;
|
|
2689
|
+
const toolCall = {
|
|
2690
|
+
id: part.id,
|
|
2691
|
+
name: String(part.name ?? "tool"),
|
|
2692
|
+
arguments: part.arguments,
|
|
2693
|
+
messageId: typeof event.id === "string" ? event.id : void 0
|
|
2694
|
+
};
|
|
2695
|
+
toolCalls.set(toolCall.id, { ...toolCalls.get(toolCall.id), ...toolCall });
|
|
2696
|
+
update({ kind: "tool_call", toolCall: toolCalls.get(toolCall.id) });
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
function ingestOpenClawToolResult(event, toolCalls, update) {
|
|
2700
|
+
const message = event.message;
|
|
2701
|
+
if (typeof message.toolCallId !== "string") return;
|
|
2702
|
+
const existing = toolCalls.get(message.toolCallId) ?? {
|
|
2703
|
+
id: message.toolCallId,
|
|
2704
|
+
name: String(message.toolName ?? "tool")
|
|
2705
|
+
};
|
|
2706
|
+
const next = {
|
|
2707
|
+
...existing,
|
|
2708
|
+
name: String(message.toolName ?? existing.name),
|
|
2709
|
+
output: openClawContentText(message.content),
|
|
2710
|
+
isError: Boolean(message.isError ?? existing.isError)
|
|
2711
|
+
};
|
|
2712
|
+
toolCalls.set(message.toolCallId, next);
|
|
2713
|
+
update({ kind: "tool_call", toolCall: next });
|
|
2714
|
+
}
|
|
2715
|
+
function openClawContentParts(content) {
|
|
2716
|
+
if (Array.isArray(content)) return content.filter((part) => part && typeof part === "object");
|
|
2717
|
+
if (content && typeof content === "object") return [content];
|
|
2718
|
+
return [];
|
|
2719
|
+
}
|
|
2720
|
+
function openClawContentText(content) {
|
|
2721
|
+
if (typeof content === "string") return content;
|
|
2722
|
+
return openClawContentParts(content).map((part) => {
|
|
2723
|
+
if (part.type === "text" && typeof part.text === "string") return part.text;
|
|
2724
|
+
return "";
|
|
2725
|
+
}).join("");
|
|
2726
|
+
}
|
|
2727
|
+
async function runHermesAgent(job, update) {
|
|
2728
|
+
const env = { ...process.env, ...job.agent.env ?? {} };
|
|
2729
|
+
const args = [...job.agent.args];
|
|
2730
|
+
if (!args.includes("chat")) {
|
|
2731
|
+
args.unshift("chat");
|
|
2732
|
+
}
|
|
2733
|
+
let usedPromptPlaceholder = false;
|
|
2734
|
+
for (let i = 0; i < args.length; i++) {
|
|
2735
|
+
if (!args[i].includes("{prompt}")) continue;
|
|
2736
|
+
args[i] = args[i].replaceAll("{prompt}", job.query);
|
|
2737
|
+
usedPromptPlaceholder = true;
|
|
2738
|
+
}
|
|
2739
|
+
if (!usedPromptPlaceholder && !hasAnyFlag(args, ["-q", "--query"])) {
|
|
2740
|
+
args.push("-q", job.query);
|
|
2741
|
+
}
|
|
2742
|
+
if (!hasAnyFlag(args, ["-Q", "--quiet"])) {
|
|
2743
|
+
args.push("-Q");
|
|
2744
|
+
}
|
|
2745
|
+
const resume = job.resumeSessionId ?? job.agent.env?.HERMES_SESSION_ID ?? null;
|
|
2746
|
+
if (resume && !hasAnyFlag(args, ["-r", "--resume"])) {
|
|
2747
|
+
args.push("--resume", resume);
|
|
2748
|
+
}
|
|
2749
|
+
const sessionDir = resolveHermesSessionDir(env);
|
|
2750
|
+
const state = {
|
|
2751
|
+
startedAtMs: Date.now(),
|
|
2752
|
+
sessionDir,
|
|
2753
|
+
resumeSessionId: resume,
|
|
2754
|
+
initialMessageCounts: snapshotHermesMessageCounts(sessionDir),
|
|
2755
|
+
selectedFile: null,
|
|
2756
|
+
processedMessages: 0
|
|
2757
|
+
};
|
|
2758
|
+
const result = {
|
|
2759
|
+
kind: "hermes",
|
|
2760
|
+
text: "",
|
|
2761
|
+
messages: [],
|
|
2762
|
+
toolCalls: [],
|
|
2763
|
+
sessionId: resume,
|
|
2764
|
+
sessionFile: null,
|
|
2765
|
+
rawMessages: []
|
|
2766
|
+
};
|
|
2767
|
+
const toolCalls = /* @__PURE__ */ new Map();
|
|
2768
|
+
let stderr = "";
|
|
2769
|
+
let stdoutText = "";
|
|
2770
|
+
let stopPolling = false;
|
|
2771
|
+
const child = spawn5(job.agent.command, args, {
|
|
2772
|
+
cwd: job.agent.cwd ?? process.cwd(),
|
|
2773
|
+
env,
|
|
2774
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2775
|
+
});
|
|
2776
|
+
const stdout = createInterface3({ input: child.stdout });
|
|
2777
|
+
stdout.on("line", (line) => {
|
|
2778
|
+
stdoutText += `${line}
|
|
2779
|
+
`;
|
|
2780
|
+
const sessionId = parseHermesSessionId(line);
|
|
2781
|
+
if (sessionId) result.sessionId = sessionId;
|
|
2782
|
+
});
|
|
2783
|
+
const stderrReader = createInterface3({ input: child.stderr });
|
|
2784
|
+
stderrReader.on("line", (line) => {
|
|
2785
|
+
if (!line.trim()) return;
|
|
2786
|
+
stderr += `${line}
|
|
2787
|
+
`;
|
|
2788
|
+
});
|
|
2789
|
+
const pollIntervalMs = envNumber(env, "BOTAPP_HERMES_POLL_INTERVAL_MS", 500);
|
|
2790
|
+
const stableAfterExitMs = envNumber(env, "BOTAPP_HERMES_STABLE_AFTER_EXIT_MS", 1e3);
|
|
2791
|
+
const timeoutMs = resolveHermesTimeoutMs(env);
|
|
2792
|
+
const tailTask = (async () => {
|
|
2793
|
+
while (!stopPolling) {
|
|
2794
|
+
readHermesSessionUpdates(state, result, toolCalls, update);
|
|
2795
|
+
await sleep(pollIntervalMs);
|
|
2796
|
+
}
|
|
2797
|
+
readHermesSessionUpdates(state, result, toolCalls, update, true);
|
|
2798
|
+
})();
|
|
2799
|
+
let code;
|
|
2800
|
+
try {
|
|
2801
|
+
code = await waitForChild(child, timeoutMs, "Hermes");
|
|
2802
|
+
await sleep(stableAfterExitMs);
|
|
2803
|
+
} finally {
|
|
2804
|
+
stopPolling = true;
|
|
2805
|
+
await tailTask;
|
|
2806
|
+
}
|
|
2807
|
+
const sessionIdFromStdout = parseHermesSessionId(stdoutText);
|
|
2808
|
+
if (sessionIdFromStdout) {
|
|
2809
|
+
result.sessionId = sessionIdFromStdout;
|
|
2810
|
+
}
|
|
2811
|
+
if (!state.selectedFile && result.sessionId) {
|
|
2812
|
+
state.selectedFile = hermesSessionFile(sessionDir, result.sessionId);
|
|
2813
|
+
readHermesSessionUpdates(state, result, toolCalls, update, true);
|
|
2814
|
+
}
|
|
2815
|
+
result.sessionFile = state.selectedFile;
|
|
2816
|
+
result.toolCalls = [...toolCalls.values()];
|
|
2817
|
+
result.text = result.text || result.messages.at(-1)?.trim() || "";
|
|
2818
|
+
if (code !== 0) {
|
|
2819
|
+
throw new Error(stderr.trim() || `Hermes exited with code ${code}`);
|
|
2820
|
+
}
|
|
2821
|
+
if (!result.sessionId) {
|
|
2822
|
+
throw new Error("Hermes completed, but no session_id line was found on stdout.");
|
|
2823
|
+
}
|
|
2824
|
+
if (!result.text && result.toolCalls.length === 0) {
|
|
2825
|
+
throw new Error(
|
|
2826
|
+
[
|
|
2827
|
+
"Hermes completed, but no assistant message was found in the session file.",
|
|
2828
|
+
`Session directory: ${sessionDir}`,
|
|
2829
|
+
result.sessionFile ? `Selected session file: ${result.sessionFile}` : "Selected session file: none"
|
|
2830
|
+
].join("\n")
|
|
2831
|
+
);
|
|
2832
|
+
}
|
|
2833
|
+
return JSON.stringify(result);
|
|
2834
|
+
}
|
|
2835
|
+
function resolveHermesSessionDir(env) {
|
|
2836
|
+
const configured = env.BOTAPP_HERMES_SESSION_DIR ?? env.HERMES_SESSION_DIR;
|
|
2837
|
+
if (configured) return expandPath(configured);
|
|
2838
|
+
return join7(homedir7(), ".hermes", "sessions");
|
|
2839
|
+
}
|
|
2840
|
+
function hermesSessionFile(sessionDir, sessionId) {
|
|
2841
|
+
return join7(sessionDir, `session_${sessionId}.json`);
|
|
2842
|
+
}
|
|
2843
|
+
function parseHermesSessionId(text) {
|
|
2844
|
+
const match = text.match(/session_id:\s*([A-Za-z0-9_-]+)/);
|
|
2845
|
+
return match?.[1] ?? null;
|
|
2846
|
+
}
|
|
2847
|
+
function snapshotHermesMessageCounts(sessionDir) {
|
|
2848
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2849
|
+
for (const file of listHermesSessionFiles(sessionDir)) {
|
|
2850
|
+
counts.set(file, readHermesMessageCount(file));
|
|
2851
|
+
}
|
|
2852
|
+
return counts;
|
|
2853
|
+
}
|
|
2854
|
+
function readHermesMessageCount(file) {
|
|
2855
|
+
try {
|
|
2856
|
+
const parsed = JSON.parse(readFileSync8(file, "utf-8"));
|
|
2857
|
+
return Array.isArray(parsed?.messages) ? parsed.messages.length : 0;
|
|
2858
|
+
} catch {
|
|
2859
|
+
return 0;
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
function readHermesSessionUpdates(state, result, toolCalls, update, final = false) {
|
|
2863
|
+
const selectedFile = state.selectedFile ?? selectHermesSessionFile(state);
|
|
2864
|
+
if (!selectedFile) return;
|
|
2865
|
+
if (!state.selectedFile) {
|
|
2866
|
+
state.selectedFile = selectedFile;
|
|
2867
|
+
state.processedMessages = state.resumeSessionId ? state.initialMessageCounts.get(selectedFile) ?? 0 : 0;
|
|
2868
|
+
}
|
|
2869
|
+
let parsed;
|
|
2870
|
+
try {
|
|
2871
|
+
parsed = JSON.parse(readFileSync8(selectedFile, "utf-8"));
|
|
2872
|
+
} catch {
|
|
2873
|
+
return;
|
|
2874
|
+
}
|
|
2875
|
+
if (typeof parsed?.session_id === "string") {
|
|
2876
|
+
result.sessionId = parsed.session_id;
|
|
2877
|
+
}
|
|
2878
|
+
if (!Array.isArray(parsed?.messages)) return;
|
|
2879
|
+
if (parsed.messages.length <= state.processedMessages && !final) return;
|
|
2880
|
+
const nextMessages = parsed.messages.slice(state.processedMessages);
|
|
2881
|
+
state.processedMessages = parsed.messages.length;
|
|
2882
|
+
for (const message of nextMessages) {
|
|
2883
|
+
ingestHermesMessage(message, result, toolCalls, update);
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
function selectHermesSessionFile(state) {
|
|
2887
|
+
if (state.resumeSessionId) {
|
|
2888
|
+
const exact = hermesSessionFile(state.sessionDir, state.resumeSessionId);
|
|
2889
|
+
if (existsSync10(exact)) return exact;
|
|
2890
|
+
}
|
|
2891
|
+
const files = listHermesSessionFiles(state.sessionDir);
|
|
2892
|
+
if (files.length === 0) return null;
|
|
2893
|
+
const recent = files.filter((file) => {
|
|
2894
|
+
try {
|
|
2895
|
+
return statSync(file).mtimeMs >= state.startedAtMs - 1e3;
|
|
2896
|
+
} catch {
|
|
2897
|
+
return false;
|
|
2898
|
+
}
|
|
2899
|
+
});
|
|
2900
|
+
return newestFile(recent);
|
|
2901
|
+
}
|
|
2902
|
+
function listHermesSessionFiles(sessionDir) {
|
|
2903
|
+
try {
|
|
2904
|
+
if (!existsSync10(sessionDir)) return [];
|
|
2905
|
+
return readdirSync(sessionDir).filter((name) => /^session_.+\.json$/.test(name)).map((name) => join7(sessionDir, name)).filter((file) => {
|
|
2906
|
+
try {
|
|
2907
|
+
return statSync(file).isFile();
|
|
2908
|
+
} catch {
|
|
2909
|
+
return false;
|
|
2910
|
+
}
|
|
2911
|
+
});
|
|
2912
|
+
} catch {
|
|
2913
|
+
return [];
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
function ingestHermesMessage(message, result, toolCalls, update) {
|
|
2917
|
+
if (!message || typeof message !== "object") return;
|
|
2918
|
+
result.rawMessages.push(message);
|
|
2919
|
+
if (message.role === "assistant") {
|
|
2920
|
+
if (typeof message.content === "string" && message.content.trim()) {
|
|
2921
|
+
result.messages.push(message.content);
|
|
2922
|
+
result.text = message.content;
|
|
2923
|
+
update({ kind: "message", text: message.content });
|
|
2924
|
+
}
|
|
2925
|
+
if (Array.isArray(message.tool_calls)) {
|
|
2926
|
+
for (const call of message.tool_calls) {
|
|
2927
|
+
const id = String(call?.id ?? call?.call_id ?? call?.response_item_id ?? randomUUID());
|
|
2928
|
+
const name = String(call?.function?.name ?? call?.name ?? "tool");
|
|
2929
|
+
const next = {
|
|
2930
|
+
...toolCalls.get(id),
|
|
2931
|
+
id,
|
|
2932
|
+
name,
|
|
2933
|
+
arguments: parseMaybeJson(call?.function?.arguments ?? call?.arguments)
|
|
2934
|
+
};
|
|
2935
|
+
toolCalls.set(id, next);
|
|
2936
|
+
update({ kind: "tool_call", toolCall: next });
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
if (message.role === "tool" && typeof message.tool_call_id === "string") {
|
|
2941
|
+
const existing = toolCalls.get(message.tool_call_id) ?? {
|
|
2942
|
+
id: message.tool_call_id,
|
|
2943
|
+
name: "tool"
|
|
2944
|
+
};
|
|
2945
|
+
const parsedOutput = parseMaybeJson(message.content);
|
|
2946
|
+
const next = {
|
|
2947
|
+
...existing,
|
|
2948
|
+
output: typeof message.content === "string" ? message.content : JSON.stringify(message.content),
|
|
2949
|
+
isError: Boolean(parsedOutput?.status === "error" || parsedOutput?.is_error)
|
|
2950
|
+
};
|
|
2951
|
+
toolCalls.set(message.tool_call_id, next);
|
|
2952
|
+
update({ kind: "tool_call", toolCall: next });
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
async function runAcpAgent(job) {
|
|
2956
|
+
const child = spawn5(job.agent.command, job.agent.args, {
|
|
2957
|
+
cwd: job.agent.cwd ?? process.cwd(),
|
|
2958
|
+
env: { ...process.env, ...job.agent.env ?? {} },
|
|
2959
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2960
|
+
});
|
|
2961
|
+
let nextId = 1;
|
|
2962
|
+
const pending = /* @__PURE__ */ new Map();
|
|
2963
|
+
const chunks = [];
|
|
2964
|
+
let stderr = "";
|
|
2965
|
+
child.stderr.on("data", (chunk) => {
|
|
2966
|
+
stderr += chunk.toString();
|
|
2967
|
+
});
|
|
2968
|
+
const rl = createInterface3({ input: child.stdout });
|
|
2969
|
+
rl.on("line", (line) => {
|
|
2970
|
+
if (!line.trim()) return;
|
|
2971
|
+
let msg;
|
|
2972
|
+
try {
|
|
2973
|
+
msg = JSON.parse(line);
|
|
2974
|
+
} catch {
|
|
2975
|
+
stderr += `
|
|
2976
|
+
Invalid ACP stdout: ${line}`;
|
|
2977
|
+
return;
|
|
2978
|
+
}
|
|
2979
|
+
if (msg.id != null && (msg.result !== void 0 || msg.error)) {
|
|
2980
|
+
const waiting = pending.get(Number(msg.id));
|
|
2981
|
+
if (!waiting) return;
|
|
2982
|
+
pending.delete(Number(msg.id));
|
|
2983
|
+
if (msg.error) waiting.reject(new Error(msg.error.message));
|
|
2984
|
+
else waiting.resolve(msg.result);
|
|
2985
|
+
return;
|
|
2986
|
+
}
|
|
2987
|
+
if (msg.id != null && msg.method) {
|
|
2988
|
+
child.stdin.write(JSON.stringify({
|
|
2989
|
+
jsonrpc: "2.0",
|
|
2990
|
+
id: msg.id,
|
|
2991
|
+
error: { code: -32601, message: `Unsupported ACP client method: ${msg.method}` }
|
|
2992
|
+
}) + "\n");
|
|
2993
|
+
return;
|
|
2994
|
+
}
|
|
2995
|
+
if (msg.method === "session/update") {
|
|
2996
|
+
const update = msg.params?.update;
|
|
2997
|
+
if (update?.sessionUpdate === "agent_message_chunk") {
|
|
2998
|
+
const text = update.content?.text;
|
|
2999
|
+
if (text) chunks.push(String(text));
|
|
3000
|
+
}
|
|
3001
|
+
if (update?.sessionUpdate === "tool_call_update" && Array.isArray(update.content)) {
|
|
3002
|
+
for (const item of update.content) {
|
|
3003
|
+
const text = item?.content?.text;
|
|
3004
|
+
if (text) chunks.push(String(text));
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
});
|
|
3009
|
+
function request2(method, params) {
|
|
3010
|
+
const id = nextId++;
|
|
3011
|
+
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
|
|
3012
|
+
return new Promise((resolve7, reject) => {
|
|
3013
|
+
pending.set(id, { resolve: resolve7, reject });
|
|
3014
|
+
});
|
|
3015
|
+
}
|
|
3016
|
+
child.on("exit", (code) => {
|
|
3017
|
+
if (code === 0) return;
|
|
3018
|
+
for (const [, waiting] of pending) {
|
|
3019
|
+
waiting.reject(new Error(stderr.trim() || `ACP agent exited with code ${code}`));
|
|
3020
|
+
}
|
|
3021
|
+
pending.clear();
|
|
3022
|
+
});
|
|
3023
|
+
const timeout = setTimeout(() => {
|
|
3024
|
+
child.kill("SIGTERM");
|
|
3025
|
+
for (const [, waiting] of pending) {
|
|
3026
|
+
waiting.reject(new Error("ACP agent timed out"));
|
|
3027
|
+
}
|
|
3028
|
+
pending.clear();
|
|
3029
|
+
}, 10 * 60 * 1e3);
|
|
3030
|
+
try {
|
|
3031
|
+
await request2("initialize", {
|
|
3032
|
+
protocolVersion: 1,
|
|
3033
|
+
clientCapabilities: {},
|
|
3034
|
+
clientInfo: {
|
|
3035
|
+
name: "botapp-daemon",
|
|
3036
|
+
title: "botapp daemon",
|
|
3037
|
+
version: "0.1.0"
|
|
3038
|
+
}
|
|
3039
|
+
});
|
|
3040
|
+
const session = await request2("session/new", {
|
|
3041
|
+
cwd: job.agent.cwd ?? process.cwd()
|
|
3042
|
+
});
|
|
3043
|
+
const sessionId = session?.sessionId ?? session?.id;
|
|
3044
|
+
if (!sessionId) throw new Error("ACP agent did not return a sessionId");
|
|
3045
|
+
await request2("session/prompt", {
|
|
3046
|
+
sessionId,
|
|
3047
|
+
prompt: [{ type: "text", text: job.query }]
|
|
3048
|
+
});
|
|
3049
|
+
return chunks.join("").trim();
|
|
3050
|
+
} finally {
|
|
3051
|
+
clearTimeout(timeout);
|
|
3052
|
+
child.kill("SIGTERM");
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
function requireProfile() {
|
|
3056
|
+
const profile = loadDaemonProfile();
|
|
3057
|
+
if (!profile) {
|
|
3058
|
+
console.error(pc19.red("No paired daemon found. Run `bot pair` first."));
|
|
3059
|
+
process.exitCode = 1;
|
|
3060
|
+
return null;
|
|
3061
|
+
}
|
|
3062
|
+
return profile;
|
|
3063
|
+
}
|
|
3064
|
+
async function daemonRequest(server, token, path, opts) {
|
|
3065
|
+
const headers = {
|
|
3066
|
+
Authorization: `Bearer ${token}`
|
|
3067
|
+
};
|
|
3068
|
+
if (opts?.body !== void 0) {
|
|
3069
|
+
headers["Content-Type"] = "application/json";
|
|
3070
|
+
}
|
|
3071
|
+
const res = await fetch(`${server}${path}`, {
|
|
3072
|
+
method: opts?.method ?? "GET",
|
|
3073
|
+
headers,
|
|
3074
|
+
body: opts?.body !== void 0 ? JSON.stringify(opts.body) : void 0
|
|
3075
|
+
});
|
|
3076
|
+
const data = await res.json().catch(() => ({}));
|
|
3077
|
+
if (!res.ok) {
|
|
3078
|
+
throw new Error(data.error ?? data.message ?? res.statusText);
|
|
3079
|
+
}
|
|
3080
|
+
return data;
|
|
3081
|
+
}
|
|
3082
|
+
function parseEnv(entries) {
|
|
3083
|
+
if (entries.length === 0) return null;
|
|
3084
|
+
return Object.fromEntries(entries.map((entry) => {
|
|
3085
|
+
const idx = entry.indexOf("=");
|
|
3086
|
+
if (idx < 0) return [entry, ""];
|
|
3087
|
+
return [entry.slice(0, idx), entry.slice(idx + 1)];
|
|
3088
|
+
}));
|
|
3089
|
+
}
|
|
3090
|
+
function getFlagValue(args, flag) {
|
|
3091
|
+
for (let i = 0; i < args.length; i++) {
|
|
3092
|
+
const arg = args[i];
|
|
3093
|
+
if (arg === flag) return args[i + 1] ?? null;
|
|
3094
|
+
if (arg.startsWith(`${flag}=`)) return arg.slice(flag.length + 1) || null;
|
|
3095
|
+
}
|
|
3096
|
+
return null;
|
|
3097
|
+
}
|
|
3098
|
+
function hasFlag(args, flag) {
|
|
3099
|
+
return args.some((arg) => arg === flag || arg.startsWith(`${flag}=`));
|
|
3100
|
+
}
|
|
3101
|
+
function hasAnyFlag(args, flags) {
|
|
3102
|
+
return flags.some((flag) => hasFlag(args, flag));
|
|
3103
|
+
}
|
|
3104
|
+
function expandPath(path) {
|
|
3105
|
+
if (path === "~") return homedir7();
|
|
3106
|
+
if (path.startsWith("~/")) return join7(homedir7(), path.slice(2));
|
|
3107
|
+
return resolve6(path);
|
|
3108
|
+
}
|
|
3109
|
+
function envNumber(env, name, fallback) {
|
|
3110
|
+
const value = env[name];
|
|
3111
|
+
if (!value) return fallback;
|
|
3112
|
+
const parsed = Number(value);
|
|
3113
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
3114
|
+
}
|
|
3115
|
+
function resolveOpenClawTimeoutMs(env) {
|
|
3116
|
+
const explicitMs = envNumber(env, "BOTAPP_OPENCLAW_TIMEOUT_MS", 0);
|
|
3117
|
+
if (explicitMs > 0) return explicitMs;
|
|
3118
|
+
const explicitSeconds = envNumber(env, "BOTAPP_OPENCLAW_TIMEOUT_SECONDS", 0);
|
|
3119
|
+
if (explicitSeconds > 0) return explicitSeconds * 1e3;
|
|
3120
|
+
return 15 * 60 * 1e3;
|
|
3121
|
+
}
|
|
3122
|
+
function resolveHermesTimeoutMs(env) {
|
|
3123
|
+
const explicitMs = envNumber(env, "BOTAPP_HERMES_TIMEOUT_MS", 0);
|
|
3124
|
+
if (explicitMs > 0) return explicitMs;
|
|
3125
|
+
const explicitSeconds = envNumber(env, "BOTAPP_HERMES_TIMEOUT_SECONDS", 0);
|
|
3126
|
+
if (explicitSeconds > 0) return explicitSeconds * 1e3;
|
|
3127
|
+
return 15 * 60 * 1e3;
|
|
3128
|
+
}
|
|
3129
|
+
function parseMaybeJson(value) {
|
|
3130
|
+
if (typeof value !== "string") return value;
|
|
3131
|
+
try {
|
|
3132
|
+
return JSON.parse(value);
|
|
3133
|
+
} catch {
|
|
3134
|
+
return value;
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
function sleep(ms) {
|
|
3138
|
+
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
3139
|
+
}
|
|
3140
|
+
async function waitForChild(child, timeoutMs, label) {
|
|
3141
|
+
return new Promise((resolveWait, reject) => {
|
|
3142
|
+
const timeout = setTimeout(() => {
|
|
3143
|
+
child.kill("SIGTERM");
|
|
3144
|
+
setTimeout(() => {
|
|
3145
|
+
if (child.exitCode === null) child.kill("SIGKILL");
|
|
3146
|
+
}, 5e3).unref();
|
|
3147
|
+
reject(new Error(`${label} timed out after ${Math.round(timeoutMs / 1e3)}s`));
|
|
3148
|
+
}, timeoutMs);
|
|
3149
|
+
timeout.unref();
|
|
3150
|
+
child.once("error", (error) => {
|
|
3151
|
+
clearTimeout(timeout);
|
|
3152
|
+
reject(error);
|
|
3153
|
+
});
|
|
3154
|
+
child.once("close", (code) => {
|
|
3155
|
+
clearTimeout(timeout);
|
|
3156
|
+
resolveWait(code);
|
|
3157
|
+
});
|
|
3158
|
+
});
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
// src/index.ts
|
|
3162
|
+
var version = "0.1.0";
|
|
3163
|
+
var program = new Command19().name("bot").description("botapp CLI \u2014 operate apps from the command line").version(version).enablePositionalOptions(true).option("--json", "Output as JSON").option("-s, --server <url>", "Server URL override").option("-t, --token <token>", "Auth token override").option("-v, --verbose", "Verbose output");
|
|
3164
|
+
program.addCommand(launchCommand);
|
|
3165
|
+
program.addCommand(runCommand);
|
|
3166
|
+
program.addCommand(appsCommand);
|
|
3167
|
+
program.addCommand(skillCommand);
|
|
3168
|
+
program.addCommand(connectCommand);
|
|
3169
|
+
program.addCommand(eventsCommand);
|
|
3170
|
+
program.addCommand(loginCommand);
|
|
3171
|
+
program.addCommand(agentCommand);
|
|
3172
|
+
program.addCommand(pairingCommand);
|
|
3173
|
+
program.addCommand(daemonCommand);
|
|
3174
|
+
program.addCommand(installCommand);
|
|
3175
|
+
program.addCommand(uninstallCommand);
|
|
3176
|
+
program.addCommand(reloadCommand);
|
|
3177
|
+
program.addCommand(configCommand);
|
|
3178
|
+
program.addCommand(serverCommand);
|
|
3179
|
+
program.addCommand(devCommand, { hidden: true });
|
|
3180
|
+
program.addCommand(registerCommand, { hidden: true });
|
|
3181
|
+
program.addCommand(wrapCommand, { hidden: true });
|
|
3182
|
+
program.addHelpText(
|
|
3183
|
+
"after",
|
|
3184
|
+
`
|
|
3185
|
+
Examples:
|
|
3186
|
+
$ bot start Connect to a server and log in
|
|
3187
|
+
$ bot run trading quote --symbol NVDA
|
|
3188
|
+
Run an app command
|
|
3189
|
+
$ bot apps List installed apps
|
|
3190
|
+
$ bot skill trading Print the app's agent-facing skill doc
|
|
3191
|
+
$ bot connect --subscribe 'trading.*'
|
|
3192
|
+
Live event stream
|
|
3193
|
+
$ bot pair Pair this machine (browser flow / local fast-path)
|
|
3194
|
+
$ bot pair --token <jwt> Pair with a token pasted from dashboard Settings
|
|
3195
|
+
$ bot daemon run Run the paired local daemon
|
|
3196
|
+
`
|
|
3197
|
+
);
|
|
3198
|
+
runCommand.addHelpText(
|
|
3199
|
+
"after",
|
|
3200
|
+
`
|
|
3201
|
+
Examples:
|
|
3202
|
+
$ bot run hello greet --name alice
|
|
3203
|
+
$ bot run trading quote --symbol NVDA
|
|
3204
|
+
$ bot run trading buy --symbol AAPL --qty 10
|
|
3205
|
+
$ bot --json run trading list-positions
|
|
3206
|
+
|
|
3207
|
+
Arguments after <command> become the command's params object:
|
|
3208
|
+
--key value \u2192 params.key = value
|
|
3209
|
+
--key \u2192 params.key = true (flag-only)
|
|
3210
|
+
Numeric strings are auto-coerced: "--qty 10" \u2192 10
|
|
3211
|
+
|
|
3212
|
+
To discover what params a command accepts:
|
|
3213
|
+
$ bot apps --json | jq '.[] | select(.name == "trading") | .commands'
|
|
3214
|
+
`
|
|
3215
|
+
);
|
|
3216
|
+
program.on("command:*", (operands) => {
|
|
3217
|
+
const first = operands[0];
|
|
3218
|
+
const known = program.commands.filter((c) => !c._hidden).map((c) => c.name());
|
|
3219
|
+
const topLevelHint = `Run ${pc20.cyan("bot --help")} for the list of top-level commands.`;
|
|
3220
|
+
const argv = process.argv.slice(2);
|
|
3221
|
+
const firstIdx = argv.indexOf(first);
|
|
3222
|
+
const tail = firstIdx >= 0 ? argv.slice(firstIdx + 1) : [];
|
|
3223
|
+
if (tail.length > 0) {
|
|
3224
|
+
const suggested = `bot run ${first} ${tail.join(" ")}`;
|
|
3225
|
+
console.error(
|
|
3226
|
+
pc20.red(`error: unknown command '${first}'`) + `
|
|
3227
|
+
|
|
3228
|
+
App commands go through ${pc20.bold("bot run")}. Did you mean:
|
|
3229
|
+
|
|
3230
|
+
${pc20.cyan(suggested)}
|
|
3231
|
+
|
|
3232
|
+
${topLevelHint}`
|
|
3233
|
+
);
|
|
3234
|
+
process.exit(1);
|
|
3235
|
+
}
|
|
3236
|
+
console.error(
|
|
3237
|
+
pc20.red(`error: unknown command '${first}'`) + `
|
|
3238
|
+
|
|
3239
|
+
If '${first}' is an app name, invoke one of its commands with:
|
|
3240
|
+
${pc20.cyan(`bot run ${first} <command> [--key value ...]`)}
|
|
3241
|
+
${pc20.cyan("bot apps --json")} ${pc20.dim("(to see what commands exist)")}
|
|
3242
|
+
|
|
3243
|
+
Top-level commands: ${known.join(", ")}
|
|
3244
|
+
${topLevelHint}`
|
|
3245
|
+
);
|
|
3246
|
+
process.exit(1);
|
|
3247
|
+
});
|
|
3248
|
+
export {
|
|
3249
|
+
program
|
|
3250
|
+
};
|
|
3251
|
+
//# sourceMappingURL=index.js.map
|