botapp-cli 0.2.5 → 0.2.8
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 +495 -122
- package/dist/bin/bot.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +486 -122
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/bin/bot.js
CHANGED
|
@@ -7,8 +7,8 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
// src/index.ts
|
|
10
|
-
import { Command as
|
|
11
|
-
import
|
|
10
|
+
import { Command as Command25 } from "commander";
|
|
11
|
+
import pc26 from "picocolors";
|
|
12
12
|
|
|
13
13
|
// src/commands/server.ts
|
|
14
14
|
import { Command } from "commander";
|
|
@@ -195,7 +195,8 @@ function writeYaml(profiles) {
|
|
|
195
195
|
server: p.server,
|
|
196
196
|
daemonId: p.daemonId,
|
|
197
197
|
daemonName: p.daemonName,
|
|
198
|
-
token: p.token
|
|
198
|
+
token: p.token,
|
|
199
|
+
...p.userEmail ? { userEmail: p.userEmail } : {}
|
|
199
200
|
};
|
|
200
201
|
}
|
|
201
202
|
writeFileSync2(DAEMON_FILE, stringify2({ profiles: map }), "utf-8");
|
|
@@ -235,7 +236,8 @@ function loadDaemonProfiles() {
|
|
|
235
236
|
server: normalizeServer(value.server),
|
|
236
237
|
daemonId: value.daemonId,
|
|
237
238
|
daemonName: value.daemonName ?? value.daemonId,
|
|
238
|
-
token: value.token
|
|
239
|
+
token: value.token,
|
|
240
|
+
userEmail: value.userEmail
|
|
239
241
|
});
|
|
240
242
|
}
|
|
241
243
|
}
|
|
@@ -271,11 +273,21 @@ function saveDaemonProfile(input2) {
|
|
|
271
273
|
server,
|
|
272
274
|
daemonId: input2.daemonId,
|
|
273
275
|
daemonName: input2.daemonName ?? input2.daemonId,
|
|
274
|
-
token: input2.token
|
|
276
|
+
token: input2.token,
|
|
277
|
+
userEmail: input2.userEmail
|
|
275
278
|
};
|
|
276
279
|
writeYaml([...remaining, next]);
|
|
277
280
|
return next;
|
|
278
281
|
}
|
|
282
|
+
function removeDaemonProfile(serverOrAlias) {
|
|
283
|
+
const target = findDaemonProfile(serverOrAlias);
|
|
284
|
+
if (!target) return false;
|
|
285
|
+
const remaining = loadDaemonProfiles().filter(
|
|
286
|
+
(p) => p.alias !== target.alias
|
|
287
|
+
);
|
|
288
|
+
writeYaml(remaining);
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
279
291
|
|
|
280
292
|
// src/commands/daemon-agent-config.ts
|
|
281
293
|
import { spawnSync } from "child_process";
|
|
@@ -1073,6 +1085,28 @@ function pickProfilesToRun(alias, server) {
|
|
|
1073
1085
|
}
|
|
1074
1086
|
return loadDaemonProfiles();
|
|
1075
1087
|
}
|
|
1088
|
+
daemonCommand.command("unpair").description("Remove a paired profile from ~/.botapp/daemon.yaml (does not touch the server)").argument("<aliasOrServer>", "Profile alias (e.g. local) or server URL").action((aliasOrServer) => {
|
|
1089
|
+
const removed = removeDaemonProfile(aliasOrServer);
|
|
1090
|
+
if (!removed) {
|
|
1091
|
+
console.error(pc3.red(`No paired profile matching "${aliasOrServer}".`));
|
|
1092
|
+
process.exitCode = 1;
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
console.log(pc3.green(`Removed daemon profile: ${aliasOrServer}`));
|
|
1096
|
+
});
|
|
1097
|
+
daemonCommand.command("list").alias("ls").description("List paired daemon profiles from ~/.botapp/daemon.yaml").action(() => {
|
|
1098
|
+
const profiles = loadDaemonProfiles();
|
|
1099
|
+
if (profiles.length === 0) {
|
|
1100
|
+
console.log(pc3.dim("No paired daemons. Run `bot pair` to add one."));
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
for (const p of profiles) {
|
|
1104
|
+
console.log(pc3.bold(p.alias ?? p.server));
|
|
1105
|
+
console.log(` Server: ${p.server}`);
|
|
1106
|
+
console.log(` Daemon: ${p.daemonName} ${pc3.dim(`(${p.daemonId})`)}`);
|
|
1107
|
+
console.log(` User: ${p.userEmail ?? pc3.dim("unknown \u2014 re-pair to capture")}`);
|
|
1108
|
+
}
|
|
1109
|
+
});
|
|
1076
1110
|
daemonCommand.command("stop").description("Stop the background daemon started by `bot launch`").action(async () => {
|
|
1077
1111
|
const pidFile = join5(homedir5(), ".botapp", "daemon.pid");
|
|
1078
1112
|
if (!existsSync5(pidFile)) {
|
|
@@ -1101,16 +1135,17 @@ daemonCommand.command("agent").description("Manage agents hosted by this daemon"
|
|
|
1101
1135
|
for (const agent of data.agents ?? []) {
|
|
1102
1136
|
console.log(`${pc3.bold(agent.name)} ${pc3.dim(`(${agent.id})`)}`);
|
|
1103
1137
|
console.log(` Kind: ${agent.kind}`);
|
|
1138
|
+
if (agent.model) console.log(` Model: ${agent.model}`);
|
|
1104
1139
|
console.log(` Command: ${agent.command} ${(agent.args ?? []).join(" ")}`);
|
|
1105
1140
|
if (agent.cwd) console.log(` CWD: ${agent.cwd}`);
|
|
1106
1141
|
}
|
|
1107
1142
|
})
|
|
1108
1143
|
).addCommand(createDaemonAgentConfigCommand()).addCommand(
|
|
1109
|
-
new Command3("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(
|
|
1144
|
+
new Command3("add").description("Register a local agent command").argument("<name>", "Name, e.g. codex, claude-code, kimi, openclaw, hermes, or hermes-agent").requiredOption("--command <command>", "Executable command").option(
|
|
1110
1145
|
"--kind <kind>",
|
|
1111
|
-
"Agent adapter kind: acp, codex, claude-code, openclaw, hermes, hermes-agent, or shell",
|
|
1146
|
+
"Agent adapter kind: acp, codex, claude-code, kimi, openclaw, hermes, hermes-agent, or shell",
|
|
1112
1147
|
"acp"
|
|
1113
|
-
).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").option("--profile <alias>", "Daemon profile alias to register against").option("--server <url>", "Daemon profile by server URL").action(async (name, opts) => {
|
|
1148
|
+
).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").option("--model <model>", "Provider model to pass to the agent runtime").option("--profile <alias>", "Daemon profile alias to register against").option("--server <url>", "Daemon profile by server URL").action(async (name, opts) => {
|
|
1114
1149
|
const profile = requireSelectedProfile(opts);
|
|
1115
1150
|
if (!profile) return;
|
|
1116
1151
|
const env = parseEnv(opts.env ?? []);
|
|
@@ -1126,12 +1161,14 @@ daemonCommand.command("agent").description("Manage agents hosted by this daemon"
|
|
|
1126
1161
|
command: opts.command,
|
|
1127
1162
|
args: opts.arg ?? [],
|
|
1128
1163
|
cwd: opts.cwd,
|
|
1129
|
-
env
|
|
1164
|
+
env,
|
|
1165
|
+
model: opts.model
|
|
1130
1166
|
}
|
|
1131
1167
|
}
|
|
1132
1168
|
);
|
|
1133
1169
|
console.log(pc3.green(`Registered daemon agent: ${pc3.bold(data.agent.name)}`));
|
|
1134
1170
|
console.log(` ID: ${data.agent.id}`);
|
|
1171
|
+
if (data.agent.model) console.log(` Model: ${data.agent.model}`);
|
|
1135
1172
|
console.log(` Command: ${data.agent.command} ${(data.agent.args ?? []).join(" ")}`);
|
|
1136
1173
|
})
|
|
1137
1174
|
).addCommand(
|
|
@@ -1297,7 +1334,10 @@ async function runAgentJob(job, update) {
|
|
|
1297
1334
|
if (job.agent.kind === "claude-code" || job.agent.kind === "claude_code") {
|
|
1298
1335
|
return runClaudeCodeAgent(job, update);
|
|
1299
1336
|
}
|
|
1300
|
-
if (job.agent.kind === "
|
|
1337
|
+
if (job.agent.kind === "kimi") {
|
|
1338
|
+
return runKimiCodeAgent(job, update);
|
|
1339
|
+
}
|
|
1340
|
+
if (job.agent.kind === "openclaw") {
|
|
1301
1341
|
return runOpenClawAgent(job, update);
|
|
1302
1342
|
}
|
|
1303
1343
|
if (job.agent.kind === "hermes" || job.agent.kind === "hermes-agent") {
|
|
@@ -1338,6 +1378,9 @@ async function runCodexAgent(job, update) {
|
|
|
1338
1378
|
if (!args.includes("--skip-git-repo-check")) {
|
|
1339
1379
|
args.push("--skip-git-repo-check");
|
|
1340
1380
|
}
|
|
1381
|
+
if (job.agent.model && !hasAnyFlag(args, ["--model", "-m"])) {
|
|
1382
|
+
args.push("--model", job.agent.model);
|
|
1383
|
+
}
|
|
1341
1384
|
if (!hasAnyFlag(args, [
|
|
1342
1385
|
"--sandbox",
|
|
1343
1386
|
"-s",
|
|
@@ -1366,12 +1409,19 @@ async function runCodexAgent(job, update) {
|
|
|
1366
1409
|
rawEvents: []
|
|
1367
1410
|
};
|
|
1368
1411
|
const toolCalls = /* @__PURE__ */ new Map();
|
|
1412
|
+
const errors = [];
|
|
1369
1413
|
function processLine(line) {
|
|
1370
1414
|
if (!line.trim()) return;
|
|
1371
1415
|
try {
|
|
1372
1416
|
const event = JSON.parse(line);
|
|
1373
1417
|
result.rawEvents.push(event);
|
|
1374
1418
|
update({ kind: "codex_event", event });
|
|
1419
|
+
if (event?.type === "error" && typeof event.message === "string") {
|
|
1420
|
+
errors.push(event.message);
|
|
1421
|
+
}
|
|
1422
|
+
if (event?.type === "turn.failed" && typeof event.error?.message === "string") {
|
|
1423
|
+
errors.push(event.error.message);
|
|
1424
|
+
}
|
|
1375
1425
|
if (event?.type === "item.completed" && event.item?.type === "agent_message") {
|
|
1376
1426
|
if (typeof event.item.text === "string") {
|
|
1377
1427
|
result.messages.push(event.item.text);
|
|
@@ -1401,7 +1451,7 @@ async function runCodexAgent(job, update) {
|
|
|
1401
1451
|
rl.on("line", processLine);
|
|
1402
1452
|
const code = await new Promise((resolve11) => child.on("close", resolve11));
|
|
1403
1453
|
if (code !== 0) {
|
|
1404
|
-
throw new Error(stderr.trim() || `Codex exited with code ${code}`);
|
|
1454
|
+
throw new Error(errors.at(-1) ?? (stderr.trim() || `Codex exited with code ${code}`));
|
|
1405
1455
|
}
|
|
1406
1456
|
result.text = result.messages.at(-1)?.trim() || "";
|
|
1407
1457
|
result.toolCalls = [...toolCalls.values()];
|
|
@@ -1418,6 +1468,9 @@ async function runClaudeCodeAgent(job, update) {
|
|
|
1418
1468
|
if (!args.includes("--verbose")) {
|
|
1419
1469
|
args.push("--verbose");
|
|
1420
1470
|
}
|
|
1471
|
+
if (job.agent.model && !hasAnyFlag(args, ["--model"])) {
|
|
1472
|
+
args.push("--model", job.agent.model);
|
|
1473
|
+
}
|
|
1421
1474
|
if (!hasAnyFlag(args, [
|
|
1422
1475
|
"--permission-mode",
|
|
1423
1476
|
"--dangerously-skip-permissions",
|
|
@@ -1440,6 +1493,9 @@ async function runClaudeCodeAgent(job, update) {
|
|
|
1440
1493
|
child.stderr.on("data", (chunk) => {
|
|
1441
1494
|
stderr += chunk.toString();
|
|
1442
1495
|
});
|
|
1496
|
+
const noiseLines = [];
|
|
1497
|
+
const errorEvents = [];
|
|
1498
|
+
const NOISE_TAIL = 6;
|
|
1443
1499
|
const result = {
|
|
1444
1500
|
kind: "claude-code",
|
|
1445
1501
|
text: "",
|
|
@@ -1497,11 +1553,19 @@ async function runClaudeCodeAgent(job, update) {
|
|
|
1497
1553
|
try {
|
|
1498
1554
|
event = JSON.parse(line);
|
|
1499
1555
|
} catch {
|
|
1556
|
+
noiseLines.push(line);
|
|
1557
|
+
if (noiseLines.length > NOISE_TAIL) noiseLines.shift();
|
|
1500
1558
|
return;
|
|
1501
1559
|
}
|
|
1502
1560
|
result.rawEvents.push(event);
|
|
1503
1561
|
update({ kind: "claude_code_event", event });
|
|
1504
1562
|
captureSessionId(event);
|
|
1563
|
+
if (event && (event.is_error === true || event.subtype === "error" || event.type === "result" && event.is_error)) {
|
|
1564
|
+
try {
|
|
1565
|
+
errorEvents.push(JSON.stringify(event).slice(0, 1e3));
|
|
1566
|
+
} catch {
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1505
1569
|
if (event.type === "assistant" && event.message?.content) {
|
|
1506
1570
|
ingestAssistantContent(
|
|
1507
1571
|
Array.isArray(event.message.content) ? event.message.content : []
|
|
@@ -1522,7 +1586,146 @@ async function runClaudeCodeAgent(job, update) {
|
|
|
1522
1586
|
rl.on("line", processLine);
|
|
1523
1587
|
const code = await new Promise((resolve11) => child.on("close", resolve11));
|
|
1524
1588
|
if (code !== 0) {
|
|
1525
|
-
|
|
1589
|
+
const parts = [];
|
|
1590
|
+
if (stderr.trim()) parts.push(stderr.trim());
|
|
1591
|
+
if (errorEvents.length > 0) {
|
|
1592
|
+
parts.push(`Error events: ${errorEvents.join(" | ")}`);
|
|
1593
|
+
}
|
|
1594
|
+
if (noiseLines.length > 0 && parts.length === 0) {
|
|
1595
|
+
parts.push(`Last stdout lines: ${noiseLines.join(" | ").slice(-1500)}`);
|
|
1596
|
+
}
|
|
1597
|
+
if (parts.length === 0) {
|
|
1598
|
+
parts.push(`Claude Code exited with code ${code} (no stderr or stdout output)`);
|
|
1599
|
+
}
|
|
1600
|
+
throw new Error(parts.join(" \u2014 "));
|
|
1601
|
+
}
|
|
1602
|
+
if (!result.text) {
|
|
1603
|
+
result.text = result.messages.at(-1)?.trim() ?? "";
|
|
1604
|
+
}
|
|
1605
|
+
result.toolCalls = [...toolCalls.values()];
|
|
1606
|
+
return JSON.stringify(result);
|
|
1607
|
+
}
|
|
1608
|
+
async function runKimiCodeAgent(job, update) {
|
|
1609
|
+
const args = [...job.agent.args];
|
|
1610
|
+
if (!args.includes("--print") && !args.includes("-p")) {
|
|
1611
|
+
args.push("--print");
|
|
1612
|
+
}
|
|
1613
|
+
if (!args.includes("--output-format")) {
|
|
1614
|
+
args.push("--output-format", "stream-json");
|
|
1615
|
+
}
|
|
1616
|
+
if (!hasAnyFlag(args, ["--yolo", "--yes", "-y"])) {
|
|
1617
|
+
args.push("--yolo");
|
|
1618
|
+
}
|
|
1619
|
+
if (job.agent.model && !hasAnyFlag(args, ["--model", "-m"])) {
|
|
1620
|
+
args.push("--model", job.agent.model);
|
|
1621
|
+
}
|
|
1622
|
+
const resume = job.resumeSessionId ?? job.agent.env?.KIMI_SESSION_ID ?? null;
|
|
1623
|
+
if (resume && !hasAnyFlag(args, ["--session", "-S", "-r", "--resume", "--continue", "-C"])) {
|
|
1624
|
+
args.push("-r", resume);
|
|
1625
|
+
}
|
|
1626
|
+
const cwd = job.agent.cwd ?? process.cwd();
|
|
1627
|
+
let prompt = job.query;
|
|
1628
|
+
if (!resume) {
|
|
1629
|
+
const agentsMdPath = join5(cwd, "AGENTS.md");
|
|
1630
|
+
if (existsSync5(agentsMdPath)) {
|
|
1631
|
+
try {
|
|
1632
|
+
const primer = readFileSync3(agentsMdPath, "utf-8").trim();
|
|
1633
|
+
if (primer) {
|
|
1634
|
+
prompt = `# System instructions (read carefully before responding)
|
|
1635
|
+
|
|
1636
|
+
${primer}
|
|
1637
|
+
|
|
1638
|
+
---
|
|
1639
|
+
|
|
1640
|
+
# Your task
|
|
1641
|
+
|
|
1642
|
+
${job.query}`;
|
|
1643
|
+
}
|
|
1644
|
+
} catch {
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
const child = spawn3(job.agent.command, args, {
|
|
1649
|
+
cwd,
|
|
1650
|
+
env: { ...process.env, ...job.agent.env ?? {} },
|
|
1651
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1652
|
+
});
|
|
1653
|
+
child.stdin.write(prompt);
|
|
1654
|
+
child.stdin.end();
|
|
1655
|
+
let stderr = "";
|
|
1656
|
+
child.stderr.on("data", (chunk) => {
|
|
1657
|
+
stderr += chunk.toString();
|
|
1658
|
+
});
|
|
1659
|
+
const result = {
|
|
1660
|
+
kind: "kimi",
|
|
1661
|
+
text: "",
|
|
1662
|
+
messages: [],
|
|
1663
|
+
toolCalls: [],
|
|
1664
|
+
sessionId: resume,
|
|
1665
|
+
rawEvents: []
|
|
1666
|
+
};
|
|
1667
|
+
const toolCalls = /* @__PURE__ */ new Map();
|
|
1668
|
+
function ingestAssistantContent(blocks) {
|
|
1669
|
+
for (const block of blocks) {
|
|
1670
|
+
if (!block || typeof block !== "object") continue;
|
|
1671
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
1672
|
+
result.messages.push(block.text);
|
|
1673
|
+
update({ kind: "message", text: block.text });
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
function ingestToolCalls(calls) {
|
|
1678
|
+
for (const call of calls) {
|
|
1679
|
+
if (!call || typeof call !== "object") continue;
|
|
1680
|
+
const id = typeof call.id === "string" ? call.id : null;
|
|
1681
|
+
if (!id) continue;
|
|
1682
|
+
const name = String(call.function?.name ?? call.name ?? "tool");
|
|
1683
|
+
let input2 = call.function?.arguments ?? call.arguments;
|
|
1684
|
+
if (typeof input2 === "string") {
|
|
1685
|
+
try {
|
|
1686
|
+
input2 = JSON.parse(input2);
|
|
1687
|
+
} catch {
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
const next = { ...toolCalls.get(id) ?? {}, id, name, input: input2 };
|
|
1691
|
+
toolCalls.set(id, next);
|
|
1692
|
+
update({ kind: "tool_call", toolCall: next });
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
function ingestToolResult(event) {
|
|
1696
|
+
const id = event?.tool_call_id;
|
|
1697
|
+
if (typeof id !== "string") return;
|
|
1698
|
+
const existing = toolCalls.get(id) ?? { id, name: "tool" };
|
|
1699
|
+
const output2 = typeof event.content === "string" ? event.content : JSON.stringify(event.content);
|
|
1700
|
+
const next = { ...existing, output: output2 };
|
|
1701
|
+
toolCalls.set(id, next);
|
|
1702
|
+
update({ kind: "tool_call", toolCall: next });
|
|
1703
|
+
}
|
|
1704
|
+
function processLine(line) {
|
|
1705
|
+
const trimmed = line.trim();
|
|
1706
|
+
if (!trimmed || trimmed[0] !== "{") return;
|
|
1707
|
+
let event;
|
|
1708
|
+
try {
|
|
1709
|
+
event = JSON.parse(trimmed);
|
|
1710
|
+
} catch {
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1713
|
+
result.rawEvents.push(event);
|
|
1714
|
+
update({ kind: "kimi_event", event });
|
|
1715
|
+
if (event.role === "assistant") {
|
|
1716
|
+
if (Array.isArray(event.content)) ingestAssistantContent(event.content);
|
|
1717
|
+
if (Array.isArray(event.tool_calls)) ingestToolCalls(event.tool_calls);
|
|
1718
|
+
} else if (event.role === "tool") {
|
|
1719
|
+
ingestToolResult(event);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
const rl = createInterface2({ input: child.stdout });
|
|
1723
|
+
rl.on("line", processLine);
|
|
1724
|
+
const code = await new Promise((resolve11) => child.on("close", resolve11));
|
|
1725
|
+
const resumeMatch = stderr.match(/To resume this session:\s*kimi\s+-r\s+([A-Za-z0-9_-]+)/);
|
|
1726
|
+
if (resumeMatch) result.sessionId = resumeMatch[1];
|
|
1727
|
+
if (code !== 0) {
|
|
1728
|
+
throw new Error(stderr.trim() || `Kimi Code exited with code ${code}`);
|
|
1526
1729
|
}
|
|
1527
1730
|
if (!result.text) {
|
|
1528
1731
|
result.text = result.messages.at(-1)?.trim() ?? "";
|
|
@@ -1586,6 +1789,7 @@ async function runOpenClawAgent(job, update) {
|
|
|
1586
1789
|
};
|
|
1587
1790
|
const toolCalls = /* @__PURE__ */ new Map();
|
|
1588
1791
|
let stderr = "";
|
|
1792
|
+
const stdoutLines = [];
|
|
1589
1793
|
let stopPolling = false;
|
|
1590
1794
|
const child = spawn3(job.agent.command, args, {
|
|
1591
1795
|
cwd: job.agent.cwd ?? process.cwd(),
|
|
@@ -1593,7 +1797,15 @@ async function runOpenClawAgent(job, update) {
|
|
|
1593
1797
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1594
1798
|
});
|
|
1595
1799
|
const stdout = createInterface2({ input: child.stdout });
|
|
1596
|
-
stdout.on("line", () => {
|
|
1800
|
+
stdout.on("line", (line) => {
|
|
1801
|
+
const text = line.trim();
|
|
1802
|
+
if (!text) return;
|
|
1803
|
+
stdoutLines.push(text);
|
|
1804
|
+
const stdoutText = extractOpenClawStdoutText(text);
|
|
1805
|
+
if (!stdoutText) return;
|
|
1806
|
+
result.text = stdoutText;
|
|
1807
|
+
result.messages.push(stdoutText);
|
|
1808
|
+
update({ kind: "message", text: stdoutText });
|
|
1597
1809
|
});
|
|
1598
1810
|
const stderrReader = createInterface2({ input: child.stderr });
|
|
1599
1811
|
stderrReader.on("line", (line) => {
|
|
@@ -1625,11 +1837,11 @@ async function runOpenClawAgent(job, update) {
|
|
|
1625
1837
|
}
|
|
1626
1838
|
result.sessionFile = state.selectedFile;
|
|
1627
1839
|
result.toolCalls = [...toolCalls.values()];
|
|
1628
|
-
result.text = result.text || result.messages.at(-1)?.trim() || "";
|
|
1840
|
+
result.text = result.text || result.messages.at(-1)?.trim() || extractOpenClawStdoutText(stdoutLines.at(-1) ?? "") || "";
|
|
1629
1841
|
if (code !== 0) {
|
|
1630
1842
|
throw new Error(stderr.trim() || `OpenClaw exited with code ${code}`);
|
|
1631
1843
|
}
|
|
1632
|
-
if (!result.sessionId) {
|
|
1844
|
+
if (!result.sessionId && !result.text && result.toolCalls.length === 0) {
|
|
1633
1845
|
throw new Error(
|
|
1634
1846
|
[
|
|
1635
1847
|
"OpenClaw completed, but the session key was not resolved from sessions.json.",
|
|
@@ -1650,6 +1862,17 @@ async function runOpenClawAgent(job, update) {
|
|
|
1650
1862
|
}
|
|
1651
1863
|
return JSON.stringify(result);
|
|
1652
1864
|
}
|
|
1865
|
+
function extractOpenClawStdoutText(line) {
|
|
1866
|
+
const text = line.trim();
|
|
1867
|
+
if (!text) return "";
|
|
1868
|
+
try {
|
|
1869
|
+
const parsed = JSON.parse(text);
|
|
1870
|
+
if (parsed && typeof parsed === "object") return JSON.stringify(parsed);
|
|
1871
|
+
if (typeof parsed === "string") return parsed;
|
|
1872
|
+
} catch {
|
|
1873
|
+
}
|
|
1874
|
+
return text;
|
|
1875
|
+
}
|
|
1653
1876
|
function resolveOpenClawSessionDir(args, env) {
|
|
1654
1877
|
const configured = env.BOTAPP_OPENCLAW_SESSION_DIR ?? env.OPENCLAW_SESSION_DIR;
|
|
1655
1878
|
if (configured) return expandPath(configured);
|
|
@@ -2162,7 +2385,7 @@ Invalid ACP stdout: ${line}`;
|
|
|
2162
2385
|
clientInfo: {
|
|
2163
2386
|
name: "botapp-daemon",
|
|
2164
2387
|
title: "botapp daemon",
|
|
2165
|
-
version: "0.2.
|
|
2388
|
+
version: "0.2.8"
|
|
2166
2389
|
}
|
|
2167
2390
|
});
|
|
2168
2391
|
const session = await request2("session/new", {
|
|
@@ -2737,7 +2960,8 @@ Configured for ${serverUrl}`));
|
|
|
2737
2960
|
server: result.serverUrl ?? serverUrl,
|
|
2738
2961
|
daemonId: data.daemon.id,
|
|
2739
2962
|
daemonName: data.daemon.name,
|
|
2740
|
-
token: data.token
|
|
2963
|
+
token: data.token,
|
|
2964
|
+
userEmail: result.userEmail
|
|
2741
2965
|
});
|
|
2742
2966
|
console.log(
|
|
2743
2967
|
` Daemon: ${pc5.bold(data.daemon.name)} ${pc5.dim(`(${data.daemon.id})`)}` + pc5.dim(` profile=${savedProfile.alias}`)
|
|
@@ -3796,7 +4020,8 @@ var pairingCommand = new Command18("pairing").alias("pair").description("Pair th
|
|
|
3796
4020
|
server: serverUrl,
|
|
3797
4021
|
daemonId: data.daemon.id,
|
|
3798
4022
|
daemonName: data.daemon.name,
|
|
3799
|
-
token: data.token
|
|
4023
|
+
token: data.token,
|
|
4024
|
+
userEmail: grant.userEmail
|
|
3800
4025
|
});
|
|
3801
4026
|
console.log(pc19.green(`Paired daemon: ${pc19.bold(data.daemon.name)}`));
|
|
3802
4027
|
console.log(` ID: ${data.daemon.id}`);
|
|
@@ -3832,13 +4057,130 @@ async function obtainPairingToken(opts) {
|
|
|
3832
4057
|
return { pairingToken: result.pairingToken, userEmail: result.userEmail };
|
|
3833
4058
|
}
|
|
3834
4059
|
|
|
4060
|
+
// src/commands/doctor.ts
|
|
4061
|
+
import { Command as Command19 } from "commander";
|
|
4062
|
+
import pc20 from "picocolors";
|
|
4063
|
+
var doctorCommand = new Command19("doctor").description("Diagnose paired daemon profiles (server reachability, token validity, account ownership)").action(async () => {
|
|
4064
|
+
const profiles = loadDaemonProfiles();
|
|
4065
|
+
if (profiles.length === 0) {
|
|
4066
|
+
console.log(pc20.dim("No paired daemons in ~/.botapp/daemon.yaml."));
|
|
4067
|
+
console.log(pc20.dim("Run `bot pair` to add one."));
|
|
4068
|
+
return;
|
|
4069
|
+
}
|
|
4070
|
+
let warns = 0;
|
|
4071
|
+
let fails = 0;
|
|
4072
|
+
for (const profile of profiles) {
|
|
4073
|
+
console.log(pc20.bold(`
|
|
4074
|
+
[${profile.alias ?? profile.server}]`));
|
|
4075
|
+
console.log(` Server: ${profile.server}`);
|
|
4076
|
+
console.log(` Daemon: ${profile.daemonName} ${pc20.dim(`(${profile.daemonId})`)}`);
|
|
4077
|
+
const findings = await checkProfile(profile);
|
|
4078
|
+
for (const f of findings) {
|
|
4079
|
+
const tag = f.severity === "ok" ? pc20.green(" \u2713 ") : f.severity === "warn" ? pc20.yellow(" \u26A0 ") : pc20.red(" \u2717 ");
|
|
4080
|
+
console.log(`${tag}${f.message}`);
|
|
4081
|
+
if (f.fix) console.log(pc20.dim(` \u2192 ${f.fix}`));
|
|
4082
|
+
if (f.severity === "warn") warns += 1;
|
|
4083
|
+
if (f.severity === "fail") fails += 1;
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4086
|
+
console.log("");
|
|
4087
|
+
if (fails === 0 && warns === 0) {
|
|
4088
|
+
console.log(pc20.green("All checks passed."));
|
|
4089
|
+
} else {
|
|
4090
|
+
const parts = [];
|
|
4091
|
+
if (fails > 0) parts.push(pc20.red(`${fails} failing`));
|
|
4092
|
+
if (warns > 0) parts.push(pc20.yellow(`${warns} warning${warns === 1 ? "" : "s"}`));
|
|
4093
|
+
console.log(parts.join(", ") + ".");
|
|
4094
|
+
if (fails > 0) process.exitCode = 1;
|
|
4095
|
+
}
|
|
4096
|
+
});
|
|
4097
|
+
async function checkProfile(profile) {
|
|
4098
|
+
const findings = [];
|
|
4099
|
+
let serverOk = false;
|
|
4100
|
+
try {
|
|
4101
|
+
const res = await fetch(`${profile.server}/health`, {
|
|
4102
|
+
signal: AbortSignal.timeout(5e3)
|
|
4103
|
+
});
|
|
4104
|
+
if (res.ok) {
|
|
4105
|
+
findings.push({ severity: "ok", message: "Server reachable" });
|
|
4106
|
+
serverOk = true;
|
|
4107
|
+
} else {
|
|
4108
|
+
findings.push({
|
|
4109
|
+
severity: "fail",
|
|
4110
|
+
message: `Server returned ${res.status} ${res.statusText}`,
|
|
4111
|
+
fix: `Verify the server at ${profile.server} is running.`
|
|
4112
|
+
});
|
|
4113
|
+
}
|
|
4114
|
+
} catch (e) {
|
|
4115
|
+
findings.push({
|
|
4116
|
+
severity: "fail",
|
|
4117
|
+
message: `Server unreachable: ${e.message ?? e}`,
|
|
4118
|
+
fix: `Start the server, or remove this profile with \`bot daemon unpair ${profile.alias ?? profile.server}\`.`
|
|
4119
|
+
});
|
|
4120
|
+
}
|
|
4121
|
+
if (!serverOk) return findings;
|
|
4122
|
+
let serverEmail = null;
|
|
4123
|
+
try {
|
|
4124
|
+
const data = await daemonRequest(profile.server, profile.token, "/api/daemon/self");
|
|
4125
|
+
findings.push({ severity: "ok", message: "Token valid" });
|
|
4126
|
+
serverEmail = data.user?.email ?? null;
|
|
4127
|
+
if (serverEmail) {
|
|
4128
|
+
findings.push({
|
|
4129
|
+
severity: "ok",
|
|
4130
|
+
message: `Token paired under ${pc20.bold(serverEmail)}`
|
|
4131
|
+
});
|
|
4132
|
+
} else {
|
|
4133
|
+
findings.push({
|
|
4134
|
+
severity: "warn",
|
|
4135
|
+
message: "Server returned no user record for this daemon",
|
|
4136
|
+
fix: "The owning user may have been deleted. Re-pair with `bot pair`."
|
|
4137
|
+
});
|
|
4138
|
+
}
|
|
4139
|
+
if (data.daemon?.status === "online") {
|
|
4140
|
+
findings.push({ severity: "ok", message: "Daemon currently online" });
|
|
4141
|
+
} else {
|
|
4142
|
+
findings.push({
|
|
4143
|
+
severity: "warn",
|
|
4144
|
+
message: `Daemon status: ${data.daemon?.status ?? "unknown"}`,
|
|
4145
|
+
fix: `Run \`bot daemon run --server ${profile.server}\` to bring it online.`
|
|
4146
|
+
});
|
|
4147
|
+
}
|
|
4148
|
+
} catch (e) {
|
|
4149
|
+
const msg = e?.message ?? String(e);
|
|
4150
|
+
if (/unauthor/i.test(msg) || /token/i.test(msg)) {
|
|
4151
|
+
findings.push({
|
|
4152
|
+
severity: "fail",
|
|
4153
|
+
message: `Token rejected: ${msg}`,
|
|
4154
|
+
fix: "The token is stale (likely the server DB was reset). Re-pair with `bot pair`."
|
|
4155
|
+
});
|
|
4156
|
+
return findings;
|
|
4157
|
+
}
|
|
4158
|
+
findings.push({ severity: "fail", message: `Identity probe failed: ${msg}` });
|
|
4159
|
+
return findings;
|
|
4160
|
+
}
|
|
4161
|
+
if (profile.userEmail && serverEmail && profile.userEmail !== serverEmail) {
|
|
4162
|
+
findings.push({
|
|
4163
|
+
severity: "warn",
|
|
4164
|
+
message: `daemon.yaml says \`userEmail: ${profile.userEmail}\` but server says \`${serverEmail}\``,
|
|
4165
|
+
fix: "Re-pair to refresh the recorded email."
|
|
4166
|
+
});
|
|
4167
|
+
} else if (!profile.userEmail && serverEmail) {
|
|
4168
|
+
findings.push({
|
|
4169
|
+
severity: "warn",
|
|
4170
|
+
message: "daemon.yaml has no userEmail recorded",
|
|
4171
|
+
fix: "Re-pair to capture it (cosmetic \u2014 auth still works)."
|
|
4172
|
+
});
|
|
4173
|
+
}
|
|
4174
|
+
return findings;
|
|
4175
|
+
}
|
|
4176
|
+
|
|
3835
4177
|
// src/commands/update.ts
|
|
3836
4178
|
import { spawn as spawn7 } from "child_process";
|
|
3837
4179
|
import { realpathSync as realpathSync2 } from "fs";
|
|
3838
|
-
import { Command as
|
|
3839
|
-
import
|
|
4180
|
+
import { Command as Command20 } from "commander";
|
|
4181
|
+
import pc21 from "picocolors";
|
|
3840
4182
|
var PACKAGE_NAME = "botapp-cli";
|
|
3841
|
-
var updateCommand = new
|
|
4183
|
+
var updateCommand = new Command20("update").description("Update the `bot` CLI itself to the latest published version").option(
|
|
3842
4184
|
"--manager <pm>",
|
|
3843
4185
|
"Force a package manager: npm | pnpm | yarn | brew (default: auto-detect)"
|
|
3844
4186
|
).option("--dry-run", "Print the command that would run, but do not execute it").action(async (opts) => {
|
|
@@ -3846,12 +4188,12 @@ var updateCommand = new Command19("update").description("Update the `bot` CLI it
|
|
|
3846
4188
|
const manager = forced ?? detectPackageManager();
|
|
3847
4189
|
if (manager === "npx") {
|
|
3848
4190
|
console.log(
|
|
3849
|
-
|
|
4191
|
+
pc21.green(
|
|
3850
4192
|
"You are running `bot` via `npx -y botapp-cli@latest` \u2014 every invocation already fetches the latest version."
|
|
3851
4193
|
)
|
|
3852
4194
|
);
|
|
3853
4195
|
console.log(
|
|
3854
|
-
|
|
4196
|
+
pc21.dim(
|
|
3855
4197
|
"For a faster startup, install it globally instead:\n npm i -g botapp-cli@latest\n pnpm add -g botapp-cli@latest"
|
|
3856
4198
|
)
|
|
3857
4199
|
);
|
|
@@ -3859,34 +4201,34 @@ var updateCommand = new Command19("update").description("Update the `bot` CLI it
|
|
|
3859
4201
|
}
|
|
3860
4202
|
if (!manager) {
|
|
3861
4203
|
console.error(
|
|
3862
|
-
|
|
4204
|
+
pc21.red(
|
|
3863
4205
|
"Couldn't detect how `bot` was installed. Pick one of these manually:"
|
|
3864
4206
|
)
|
|
3865
4207
|
);
|
|
3866
|
-
console.log(
|
|
3867
|
-
console.log(
|
|
3868
|
-
console.log(
|
|
3869
|
-
console.log(
|
|
4208
|
+
console.log(pc21.cyan(" npm i -g botapp-cli@latest"));
|
|
4209
|
+
console.log(pc21.cyan(" pnpm add -g botapp-cli@latest"));
|
|
4210
|
+
console.log(pc21.cyan(" yarn global add botapp-cli@latest"));
|
|
4211
|
+
console.log(pc21.cyan(" brew upgrade botapp-cli"));
|
|
3870
4212
|
process.exitCode = 1;
|
|
3871
4213
|
return;
|
|
3872
4214
|
}
|
|
3873
4215
|
const { command, args } = updateCommandFor(manager);
|
|
3874
|
-
console.log(
|
|
4216
|
+
console.log(pc21.dim(`$ ${command} ${args.join(" ")}`));
|
|
3875
4217
|
if (opts.dryRun) return;
|
|
3876
4218
|
const child = spawn7(command, args, { stdio: "inherit" });
|
|
3877
4219
|
const code = await new Promise((resolve11) => {
|
|
3878
4220
|
child.once("error", (err) => {
|
|
3879
|
-
console.error(
|
|
4221
|
+
console.error(pc21.red(`Failed to spawn ${command}: ${err.message}`));
|
|
3880
4222
|
resolve11(127);
|
|
3881
4223
|
});
|
|
3882
4224
|
child.once("close", resolve11);
|
|
3883
4225
|
});
|
|
3884
4226
|
if (code !== 0) {
|
|
3885
|
-
console.error(
|
|
4227
|
+
console.error(pc21.red(`Update failed (exit ${code}).`));
|
|
3886
4228
|
process.exitCode = code ?? 1;
|
|
3887
4229
|
return;
|
|
3888
4230
|
}
|
|
3889
|
-
console.log(
|
|
4231
|
+
console.log(pc21.green("botapp-cli updated. Run `bot --version` to confirm."));
|
|
3890
4232
|
});
|
|
3891
4233
|
function detectPackageManager() {
|
|
3892
4234
|
const argv = process.argv[1] ?? "";
|
|
@@ -3921,27 +4263,27 @@ function updateCommandFor(manager) {
|
|
|
3921
4263
|
}
|
|
3922
4264
|
|
|
3923
4265
|
// src/commands/simulate.ts
|
|
3924
|
-
import { Command as
|
|
4266
|
+
import { Command as Command21 } from "commander";
|
|
3925
4267
|
import { resolve as resolve8, join as join11 } from "path";
|
|
3926
4268
|
import { existsSync as existsSync13, readFileSync as readFileSync9 } from "fs";
|
|
3927
4269
|
import { spawn as spawn8 } from "child_process";
|
|
3928
|
-
import
|
|
3929
|
-
var simulateCommand = new
|
|
4270
|
+
import pc22 from "picocolors";
|
|
4271
|
+
var simulateCommand = new Command21("simulate").description("Dev-tunnel a local app to a botapp server (per-user shadow)").argument("[path]", "Path to the app directory", ".").option("-s, --server <url>", "botapp server URL (default: active profile / $BOTAPP_SERVER)").option("-t, --token <token>", "auth token (default: active profile / $BOTAPP_TOKEN)").option("--entry <file>", "override the manifest entry path").option("--lifetime <minutes>", "dev-session token lifetime in minutes", "480").action(async (appPath, opts) => {
|
|
3930
4272
|
const absPath = resolve8(appPath);
|
|
3931
4273
|
if (!existsSync13(absPath)) {
|
|
3932
|
-
console.error(
|
|
4274
|
+
console.error(pc22.red(`Path not found: ${absPath}`));
|
|
3933
4275
|
process.exitCode = 1;
|
|
3934
4276
|
return;
|
|
3935
4277
|
}
|
|
3936
4278
|
const manifestPath = join11(absPath, "botapp.app.json");
|
|
3937
4279
|
if (!existsSync13(manifestPath)) {
|
|
3938
|
-
console.error(
|
|
4280
|
+
console.error(pc22.red(`No botapp.app.json found in ${absPath}`));
|
|
3939
4281
|
process.exitCode = 1;
|
|
3940
4282
|
return;
|
|
3941
4283
|
}
|
|
3942
4284
|
const manifest = JSON.parse(readFileSync9(manifestPath, "utf8"));
|
|
3943
4285
|
if (!manifest.name) {
|
|
3944
|
-
console.error(
|
|
4286
|
+
console.error(pc22.red('manifest missing "name"'));
|
|
3945
4287
|
process.exitCode = 1;
|
|
3946
4288
|
return;
|
|
3947
4289
|
}
|
|
@@ -3949,31 +4291,31 @@ var simulateCommand = new Command20("simulate").description("Dev-tunnel a local
|
|
|
3949
4291
|
const wsServer = httpServer.replace(/^http:/, "ws:").replace(/^https:/, "wss:") + "/ws/host";
|
|
3950
4292
|
const token = resolveToken(opts.token);
|
|
3951
4293
|
if (!token) {
|
|
3952
|
-
console.error(
|
|
4294
|
+
console.error(pc22.red("not logged in: run `bot login` or pass --token"));
|
|
3953
4295
|
process.exitCode = 1;
|
|
3954
4296
|
return;
|
|
3955
4297
|
}
|
|
3956
4298
|
const lifetimeMs = Math.max(6e4, Number(opts.lifetime) * 6e4);
|
|
3957
|
-
console.log(
|
|
4299
|
+
console.log(pc22.dim(`requesting dev token for "${manifest.name}" from ${httpServer}...`));
|
|
3958
4300
|
const devToken = await issueDevToken({
|
|
3959
4301
|
serverUrl: httpServer,
|
|
3960
4302
|
token,
|
|
3961
4303
|
appName: manifest.name,
|
|
3962
4304
|
lifetimeMs
|
|
3963
4305
|
});
|
|
3964
|
-
console.log(
|
|
4306
|
+
console.log(pc22.green("\u2713"), `dev token issued (lifetime ${opts.lifetime} min)`);
|
|
3965
4307
|
const entryPath = resolveEntry(absPath, opts.entry ?? manifest.entry);
|
|
3966
4308
|
if (!existsSync13(entryPath)) {
|
|
3967
|
-
console.error(
|
|
3968
|
-
console.error(
|
|
4309
|
+
console.error(pc22.red(`entry not found: ${entryPath}`));
|
|
4310
|
+
console.error(pc22.dim(" run `pnpm build` (or your build script) first."));
|
|
3969
4311
|
process.exitCode = 1;
|
|
3970
4312
|
return;
|
|
3971
4313
|
}
|
|
3972
|
-
console.log(
|
|
3973
|
-
console.log(
|
|
3974
|
-
console.log(
|
|
4314
|
+
console.log(pc22.dim(`spawning ${entryPath} ...`));
|
|
4315
|
+
console.log(pc22.dim(` BOTAPP_SERVER=${wsServer}`));
|
|
4316
|
+
console.log(pc22.dim(` BOTAPP_APP_NAME=${manifest.name}`));
|
|
3975
4317
|
console.log(
|
|
3976
|
-
|
|
4318
|
+
pc22.cyan(
|
|
3977
4319
|
`
|
|
3978
4320
|
When ready, your dashboard at ${httpServer} will route "${manifest.name}" to this process for your account only.
|
|
3979
4321
|
`
|
|
@@ -3991,7 +4333,7 @@ When ready, your dashboard at ${httpServer} will route "${manifest.name}" to thi
|
|
|
3991
4333
|
stdio: "inherit"
|
|
3992
4334
|
});
|
|
3993
4335
|
const stop = (signal) => {
|
|
3994
|
-
console.log(
|
|
4336
|
+
console.log(pc22.dim(`
|
|
3995
4337
|
stopping (${signal})...`));
|
|
3996
4338
|
if (!child.killed) child.kill(signal);
|
|
3997
4339
|
};
|
|
@@ -3999,7 +4341,7 @@ stopping (${signal})...`));
|
|
|
3999
4341
|
process.on("SIGTERM", () => stop("SIGTERM"));
|
|
4000
4342
|
child.on("exit", (code, sig) => {
|
|
4001
4343
|
const reason = sig ? `signal ${sig}` : `exit ${code}`;
|
|
4002
|
-
console.log(
|
|
4344
|
+
console.log(pc22.dim(`child exited (${reason})`));
|
|
4003
4345
|
process.exit(typeof code === "number" ? code : 0);
|
|
4004
4346
|
});
|
|
4005
4347
|
});
|
|
@@ -4036,14 +4378,14 @@ function resolveEntry(appDir, entry) {
|
|
|
4036
4378
|
}
|
|
4037
4379
|
|
|
4038
4380
|
// src/commands/init.ts
|
|
4039
|
-
import { Command as
|
|
4381
|
+
import { Command as Command22 } from "commander";
|
|
4040
4382
|
import { existsSync as existsSync14, mkdirSync as mkdirSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
4041
4383
|
import { resolve as resolve9, join as join12 } from "path";
|
|
4042
|
-
import
|
|
4043
|
-
var initCommand = new
|
|
4384
|
+
import pc23 from "picocolors";
|
|
4385
|
+
var initCommand = new Command22("init").description("Scaffold a new hosted-tier botapp app project").argument("<name>", "App name (kebab-case; used in URL paths and manifest)").option("-d, --dir <path>", "Output directory (default: ./<name>)").option("--headless", "No frontend \u2014 backend-only app (skip src/, tailwind, etc.)").option("--description <text>", "Short description for AI agents to read").option("-f, --force", "Overwrite existing directory contents").action(async (name, opts) => {
|
|
4044
4386
|
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
4045
|
-
console.error(
|
|
4046
|
-
console.error(
|
|
4387
|
+
console.error(pc23.red("Invalid name. Use lowercase letters, digits, dashes; must start with a letter."));
|
|
4388
|
+
console.error(pc23.dim(" e.g. my-app, todo-tracker, gomoku-2"));
|
|
4047
4389
|
process.exitCode = 1;
|
|
4048
4390
|
return;
|
|
4049
4391
|
}
|
|
@@ -4057,8 +4399,8 @@ var initCommand = new Command21("init").description("Scaffold a new hosted-tier
|
|
|
4057
4399
|
}
|
|
4058
4400
|
})();
|
|
4059
4401
|
if (Array.isArray(stat) && stat.length > 0) {
|
|
4060
|
-
console.error(
|
|
4061
|
-
console.error(
|
|
4402
|
+
console.error(pc23.red(`Target directory exists and is not empty: ${targetDir}`));
|
|
4403
|
+
console.error(pc23.dim(" pass --force to overwrite, or pick a different --dir"));
|
|
4062
4404
|
process.exitCode = 1;
|
|
4063
4405
|
return;
|
|
4064
4406
|
}
|
|
@@ -4075,18 +4417,18 @@ var initCommand = new Command21("init").description("Scaffold a new hosted-tier
|
|
|
4075
4417
|
mkdirSync6(dirname2(full), { recursive: true });
|
|
4076
4418
|
writeFileSync4(full, content);
|
|
4077
4419
|
}
|
|
4078
|
-
console.log(
|
|
4420
|
+
console.log(pc23.green("\u2713"), `Scaffolded ${ctx.headless ? "headless " : ""}app at`, pc23.cyan(targetDir));
|
|
4079
4421
|
console.log();
|
|
4080
4422
|
console.log("Next steps:");
|
|
4081
|
-
console.log(
|
|
4082
|
-
console.log(
|
|
4083
|
-
if (!ctx.headless) console.log(
|
|
4084
|
-
else console.log(
|
|
4085
|
-
console.log(
|
|
4086
|
-
console.log(
|
|
4423
|
+
console.log(pc23.dim(` cd ${targetDir.replace(process.cwd() + "/", "")}`));
|
|
4424
|
+
console.log(pc23.dim(" pnpm install # or npm install"));
|
|
4425
|
+
if (!ctx.headless) console.log(pc23.dim(" pnpm build # builds api/ + frontend (dist/)"));
|
|
4426
|
+
else console.log(pc23.dim(" pnpm build # builds api/ to dist/api.js"));
|
|
4427
|
+
console.log(pc23.dim(` bot login --server <your-botapp-server>`));
|
|
4428
|
+
console.log(pc23.dim(` bot simulate # dev-tunnel into the server, hot-reload as you build`));
|
|
4087
4429
|
console.log();
|
|
4088
4430
|
console.log(
|
|
4089
|
-
|
|
4431
|
+
pc23.dim("Once it works, `bot publish` ships it to the server (per-user install by default).")
|
|
4090
4432
|
);
|
|
4091
4433
|
});
|
|
4092
4434
|
function fullFiles(ctx) {
|
|
@@ -4150,7 +4492,7 @@ function packageJson(ctx, headless) {
|
|
|
4150
4492
|
typecheck: "tsc --noEmit"
|
|
4151
4493
|
};
|
|
4152
4494
|
const deps = {
|
|
4153
|
-
"botapp-sdk": "^0.1.
|
|
4495
|
+
"botapp-sdk": "^0.1.1",
|
|
4154
4496
|
ws: "^8.18.0"
|
|
4155
4497
|
};
|
|
4156
4498
|
const devDeps = {
|
|
@@ -4312,7 +4654,7 @@ function apiEntryTs(ctx, headless) {
|
|
|
4312
4654
|
|
|
4313
4655
|
ctx.serveStatic('./dist/public')
|
|
4314
4656
|
`;
|
|
4315
|
-
return `import { BotApp } from 'botapp-sdk'
|
|
4657
|
+
return `import { BotApp, runHosted } from 'botapp-sdk'
|
|
4316
4658
|
|
|
4317
4659
|
const app = new BotApp({
|
|
4318
4660
|
name: '${ctx.name}',
|
|
@@ -4334,7 +4676,16 @@ const app = new BotApp({
|
|
|
4334
4676
|
${widget} },
|
|
4335
4677
|
})
|
|
4336
4678
|
|
|
4337
|
-
|
|
4679
|
+
export default app
|
|
4680
|
+
|
|
4681
|
+
// Hosted-tier bridge: only connects when launched with BOTAPP_SERVER +
|
|
4682
|
+
// BOTAPP_APP_TOKEN (set by \`bot simulate\` and the platform-spawned runner).
|
|
4683
|
+
// In every other context (tests, in-process discovery) the bridge stays
|
|
4684
|
+
// dormant. Use globalThis.process so this typechecks without @types/node.
|
|
4685
|
+
const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env ?? {}
|
|
4686
|
+
if (env.BOTAPP_SERVER && env.BOTAPP_APP_TOKEN) {
|
|
4687
|
+
await runHosted(app)
|
|
4688
|
+
}
|
|
4338
4689
|
`;
|
|
4339
4690
|
}
|
|
4340
4691
|
function srcMainTsx(_ctx) {
|
|
@@ -4375,32 +4726,53 @@ export function App() {
|
|
|
4375
4726
|
}
|
|
4376
4727
|
function srcApiTs() {
|
|
4377
4728
|
return `// Tiny client for calling app routes/commands from the browser.
|
|
4378
|
-
//
|
|
4379
|
-
//
|
|
4729
|
+
// Two ways an app frontend reaches its backend on a botapp server:
|
|
4730
|
+
//
|
|
4731
|
+
// 1. Path-prefixed: /apps/<name>/api/commands/<cmd> (on bare apex)
|
|
4732
|
+
// 2. Subdomain: /api/apps/<name>/commands/<cmd> (on app subdomain)
|
|
4733
|
+
//
|
|
4734
|
+
// The subdomain dispatcher in the server leaves /api/* unrewritten on
|
|
4735
|
+
// subdomain hosts, so the frontend has to construct the absolute
|
|
4736
|
+
// /api/apps/<name>/... URL itself. We resolve <name> three ways and
|
|
4737
|
+
// take the first that works:
|
|
4738
|
+
// \u2022 from a /apps/<name>/ path prefix (host is the apex)
|
|
4739
|
+
// \u2022 from the first DNS label (host is <name>.<domain>)
|
|
4740
|
+
// \u2022 fallback: skip the /api/apps/<name> prefix (single-app local dev)
|
|
4380
4741
|
|
|
4381
|
-
|
|
4382
|
-
const m = location.pathname.match(/^\\/apps\\/[^/]
|
|
4383
|
-
|
|
4384
|
-
|
|
4742
|
+
function resolveAppName(): string | null {
|
|
4743
|
+
const m = location.pathname.match(/^\\/apps\\/([^/]+)\\//)
|
|
4744
|
+
if (m) return m[1]
|
|
4745
|
+
const host = location.hostname
|
|
4746
|
+
const first = host.split('.')[0]
|
|
4747
|
+
if (first && first !== 'www' && host.split('.').length >= 2) return first
|
|
4748
|
+
return null
|
|
4749
|
+
}
|
|
4385
4750
|
|
|
4386
|
-
|
|
4387
|
-
|
|
4751
|
+
const APP_NAME = resolveAppName()
|
|
4752
|
+
const API_BASE = APP_NAME ? \`/api/apps/\${APP_NAME}\` : '/api'
|
|
4753
|
+
|
|
4754
|
+
async function call(kind: 'commands' | 'actions', name: string, params: Record<string, unknown>): Promise<unknown> {
|
|
4755
|
+
const r = await fetch(\`\${API_BASE}/\${kind}/\${encodeURIComponent(name)}\`, {
|
|
4388
4756
|
method: 'POST',
|
|
4389
4757
|
headers: { 'Content-Type': 'application/json' },
|
|
4390
4758
|
body: JSON.stringify(params),
|
|
4391
4759
|
})
|
|
4392
4760
|
if (!r.ok) throw new Error(await r.text())
|
|
4393
|
-
|
|
4761
|
+
// The /api/apps/<name>/... handler wraps responses as
|
|
4762
|
+
// \`{ status: 'success', result }\` or \`{ status: 'error', error }\`.
|
|
4763
|
+
// Unwrap so callers see the bare result; throw on error.
|
|
4764
|
+
const json = (await r.json()) as { status?: string; result?: unknown; error?: string }
|
|
4765
|
+
if (json.status === 'error') throw new Error(json.error ?? 'unknown error')
|
|
4766
|
+
if (json.status === 'success') return json.result
|
|
4767
|
+
return json
|
|
4768
|
+
}
|
|
4769
|
+
|
|
4770
|
+
export async function callCommand(name: string, params: Record<string, unknown> = {}) {
|
|
4771
|
+
return call('commands', name, params)
|
|
4394
4772
|
}
|
|
4395
4773
|
|
|
4396
4774
|
export async function callAction(name: string, params: Record<string, unknown> = {}) {
|
|
4397
|
-
|
|
4398
|
-
method: 'POST',
|
|
4399
|
-
headers: { 'Content-Type': 'application/json' },
|
|
4400
|
-
body: JSON.stringify(params),
|
|
4401
|
-
})
|
|
4402
|
-
if (!r.ok) throw new Error(await r.text())
|
|
4403
|
-
return r.json()
|
|
4775
|
+
return call('actions', name, params)
|
|
4404
4776
|
}
|
|
4405
4777
|
`;
|
|
4406
4778
|
}
|
|
@@ -4466,38 +4838,38 @@ function dirname2(p) {
|
|
|
4466
4838
|
}
|
|
4467
4839
|
|
|
4468
4840
|
// src/commands/publish.ts
|
|
4469
|
-
import { Command as
|
|
4841
|
+
import { Command as Command23 } from "commander";
|
|
4470
4842
|
import { resolve as resolve10, join as join13, relative as relative2 } from "path";
|
|
4471
4843
|
import { existsSync as existsSync15, readFileSync as readFileSync10, statSync as statSync4, readdirSync as readdirSync2 } from "fs";
|
|
4472
4844
|
import { createGzip } from "zlib";
|
|
4473
4845
|
import { spawn as spawn9 } from "child_process";
|
|
4474
|
-
import
|
|
4475
|
-
var publishCommand = new
|
|
4846
|
+
import pc24 from "picocolors";
|
|
4847
|
+
var publishCommand = new Command23("publish").description("Build, pack, and upload an app to a botapp server").argument("[path]", "App directory", ".").option("-s, --server <url>", "Server URL (default: active profile / $BOTAPP_SERVER)").option("-t, --token <token>", "Auth token (default: active profile / $BOTAPP_TOKEN)").option("--public", "Request public visibility (queues admin review). Default: private.").option("--no-build", "Skip running `pnpm build` / `npm run build`").option("--bundle-dir <dir>", "Directory to bundle into tar.gz (default: dist/public if exists, else dist)").action(async (appPath, opts) => {
|
|
4476
4848
|
const absPath = resolve10(appPath);
|
|
4477
4849
|
const manifestPath = join13(absPath, "botapp.app.json");
|
|
4478
4850
|
if (!existsSync15(manifestPath)) {
|
|
4479
|
-
console.error(
|
|
4851
|
+
console.error(pc24.red(`No botapp.app.json found in ${absPath}`));
|
|
4480
4852
|
process.exitCode = 1;
|
|
4481
4853
|
return;
|
|
4482
4854
|
}
|
|
4483
4855
|
const manifest = JSON.parse(readFileSync10(manifestPath, "utf8"));
|
|
4484
4856
|
if (!manifest.name) {
|
|
4485
|
-
console.error(
|
|
4857
|
+
console.error(pc24.red('manifest missing "name"'));
|
|
4486
4858
|
process.exitCode = 1;
|
|
4487
4859
|
return;
|
|
4488
4860
|
}
|
|
4489
4861
|
const server = resolveServerUrl(opts.server);
|
|
4490
4862
|
const token = resolveToken(opts.token);
|
|
4491
4863
|
if (!token) {
|
|
4492
|
-
console.error(
|
|
4864
|
+
console.error(pc24.red("not logged in: run `bot login` or pass --token"));
|
|
4493
4865
|
process.exitCode = 1;
|
|
4494
4866
|
return;
|
|
4495
4867
|
}
|
|
4496
4868
|
if (opts.build !== false) {
|
|
4497
|
-
console.log(
|
|
4869
|
+
console.log(pc24.dim("building app..."));
|
|
4498
4870
|
const ok = await runBuild(absPath);
|
|
4499
4871
|
if (!ok) {
|
|
4500
|
-
console.error(
|
|
4872
|
+
console.error(pc24.red("build failed; aborting"));
|
|
4501
4873
|
process.exitCode = 1;
|
|
4502
4874
|
return;
|
|
4503
4875
|
}
|
|
@@ -4505,14 +4877,14 @@ var publishCommand = new Command22("publish").description("Build, pack, and uplo
|
|
|
4505
4877
|
const bundleDir = opts.bundleDir ? resolve10(absPath, opts.bundleDir) : pickBundleDir(absPath);
|
|
4506
4878
|
let bundleB64;
|
|
4507
4879
|
if (bundleDir && existsSync15(bundleDir)) {
|
|
4508
|
-
console.log(
|
|
4880
|
+
console.log(pc24.dim(`packing ${relative2(absPath, bundleDir)}/ \u2192 tar.gz...`));
|
|
4509
4881
|
const bytes = await packDirToTarGz(bundleDir);
|
|
4510
4882
|
bundleB64 = bytes.toString("base64");
|
|
4511
|
-
console.log(
|
|
4883
|
+
console.log(pc24.dim(` bundle: ${(bytes.length / 1024).toFixed(1)} KiB`));
|
|
4512
4884
|
} else {
|
|
4513
|
-
console.log(
|
|
4885
|
+
console.log(pc24.dim("no frontend bundle to upload (headless app or no dist/public)"));
|
|
4514
4886
|
}
|
|
4515
|
-
console.log(
|
|
4887
|
+
console.log(pc24.dim(`uploading to ${server}/api/apps/upload (${opts.public ? "public" : "private"})...`));
|
|
4516
4888
|
const res = await fetch(`${server}/api/apps/upload`, {
|
|
4517
4889
|
method: "POST",
|
|
4518
4890
|
headers: authHeaders(token),
|
|
@@ -4524,23 +4896,23 @@ var publishCommand = new Command22("publish").description("Build, pack, and uplo
|
|
|
4524
4896
|
});
|
|
4525
4897
|
if (!res.ok) {
|
|
4526
4898
|
const body = await res.text().catch(() => "");
|
|
4527
|
-
console.error(
|
|
4899
|
+
console.error(pc24.red(`upload failed (${res.status}): ${body}`));
|
|
4528
4900
|
process.exitCode = 1;
|
|
4529
4901
|
return;
|
|
4530
4902
|
}
|
|
4531
4903
|
const data = await res.json();
|
|
4532
4904
|
if (!data.ok) {
|
|
4533
|
-
console.error(
|
|
4905
|
+
console.error(pc24.red(`upload rejected: ${data.error ?? "unknown"}`));
|
|
4534
4906
|
process.exitCode = 1;
|
|
4535
4907
|
return;
|
|
4536
4908
|
}
|
|
4537
|
-
console.log(
|
|
4909
|
+
console.log(pc24.green("\u2713"), data.message ?? "uploaded");
|
|
4538
4910
|
if (data.install) {
|
|
4539
|
-
console.log(
|
|
4540
|
-
console.log(
|
|
4541
|
-
console.log(
|
|
4911
|
+
console.log(pc24.dim(` id: ${data.install.id}`));
|
|
4912
|
+
console.log(pc24.dim(` version: ${data.install.version}`));
|
|
4913
|
+
console.log(pc24.dim(` visibility: ${data.install.visibility}`));
|
|
4542
4914
|
if (data.install.reviewStatus !== "none") {
|
|
4543
|
-
console.log(
|
|
4915
|
+
console.log(pc24.dim(` review: ${data.install.reviewStatus}`));
|
|
4544
4916
|
}
|
|
4545
4917
|
}
|
|
4546
4918
|
});
|
|
@@ -4615,9 +4987,9 @@ function gzip(input2) {
|
|
|
4615
4987
|
}
|
|
4616
4988
|
|
|
4617
4989
|
// src/commands/review.ts
|
|
4618
|
-
import { Command as
|
|
4619
|
-
import
|
|
4620
|
-
var reviewCommand = new
|
|
4990
|
+
import { Command as Command24 } from "commander";
|
|
4991
|
+
import pc25 from "picocolors";
|
|
4992
|
+
var reviewCommand = new Command24("review").description("Admin: review queue for public-visibility uploads");
|
|
4621
4993
|
reviewCommand.command("list").description("List pending public-visibility uploads").option("-s, --server <url>", "Server URL").option("-t, --token <token>", "Auth token").action(async (opts) => {
|
|
4622
4994
|
const server = resolveServerUrl(opts.server);
|
|
4623
4995
|
const token = resolveToken(opts.token);
|
|
@@ -4629,15 +5001,15 @@ reviewCommand.command("list").description("List pending public-visibility upload
|
|
|
4629
5001
|
}
|
|
4630
5002
|
const data = await res.json();
|
|
4631
5003
|
if (!data.pending?.length) {
|
|
4632
|
-
console.log(
|
|
5004
|
+
console.log(pc25.dim("queue empty"));
|
|
4633
5005
|
return;
|
|
4634
5006
|
}
|
|
4635
5007
|
for (const i of data.pending) {
|
|
4636
|
-
console.log(
|
|
4637
|
-
console.log(
|
|
4638
|
-
console.log(
|
|
5008
|
+
console.log(pc25.bold(i.id), pc25.cyan(i.appName), pc25.dim(`v${i.version}`));
|
|
5009
|
+
console.log(pc25.dim(` uploader: ${i.ownerUserId ?? "(server-wide)"}`));
|
|
5010
|
+
console.log(pc25.dim(` uploaded: ${i.uploadedAt}`));
|
|
4639
5011
|
const desc = i.manifest?.description;
|
|
4640
|
-
if (desc) console.log(
|
|
5012
|
+
if (desc) console.log(pc25.dim(` description: ${desc}`));
|
|
4641
5013
|
console.log();
|
|
4642
5014
|
}
|
|
4643
5015
|
});
|
|
@@ -4658,17 +5030,17 @@ async function decide(id, decision, opts) {
|
|
|
4658
5030
|
}
|
|
4659
5031
|
const data = await res.json();
|
|
4660
5032
|
if (!data.ok) return die(`server rejected: ${data.error ?? "unknown"}`);
|
|
4661
|
-
console.log(
|
|
4662
|
-
if (data.install?.reviewNotes) console.log(
|
|
5033
|
+
console.log(pc25.green("\u2713"), `${decision}d`, data.install?.appName, pc25.dim(`(${data.install?.id})`));
|
|
5034
|
+
if (data.install?.reviewNotes) console.log(pc25.dim(` notes: ${data.install.reviewNotes}`));
|
|
4663
5035
|
}
|
|
4664
5036
|
function die(msg) {
|
|
4665
|
-
console.error(
|
|
5037
|
+
console.error(pc25.red(msg));
|
|
4666
5038
|
process.exitCode = 1;
|
|
4667
5039
|
}
|
|
4668
5040
|
|
|
4669
5041
|
// src/index.ts
|
|
4670
|
-
var version = "0.2.
|
|
4671
|
-
var program = new
|
|
5042
|
+
var version = "0.2.8";
|
|
5043
|
+
var program = new Command25().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");
|
|
4672
5044
|
program.addCommand(launchCommand);
|
|
4673
5045
|
program.addCommand(runCommand);
|
|
4674
5046
|
program.addCommand(appsCommand);
|
|
@@ -4679,6 +5051,7 @@ program.addCommand(loginCommand);
|
|
|
4679
5051
|
program.addCommand(agentCommand);
|
|
4680
5052
|
program.addCommand(pairingCommand);
|
|
4681
5053
|
program.addCommand(daemonCommand);
|
|
5054
|
+
program.addCommand(doctorCommand);
|
|
4682
5055
|
program.addCommand(initCommand);
|
|
4683
5056
|
program.addCommand(installCommand);
|
|
4684
5057
|
program.addCommand(uninstallCommand);
|
|
@@ -4730,29 +5103,29 @@ To discover what params a command accepts:
|
|
|
4730
5103
|
program.on("command:*", (operands) => {
|
|
4731
5104
|
const first = operands[0];
|
|
4732
5105
|
const known = program.commands.filter((c) => !c._hidden).map((c) => c.name());
|
|
4733
|
-
const topLevelHint = `Run ${
|
|
5106
|
+
const topLevelHint = `Run ${pc26.cyan("bot --help")} for the list of top-level commands.`;
|
|
4734
5107
|
const argv = process.argv.slice(2);
|
|
4735
5108
|
const firstIdx = argv.indexOf(first);
|
|
4736
5109
|
const tail = firstIdx >= 0 ? argv.slice(firstIdx + 1) : [];
|
|
4737
5110
|
if (tail.length > 0) {
|
|
4738
5111
|
const suggested = `bot run ${first} ${tail.join(" ")}`;
|
|
4739
5112
|
console.error(
|
|
4740
|
-
|
|
5113
|
+
pc26.red(`error: unknown command '${first}'`) + `
|
|
4741
5114
|
|
|
4742
|
-
App commands go through ${
|
|
5115
|
+
App commands go through ${pc26.bold("bot run")}. Did you mean:
|
|
4743
5116
|
|
|
4744
|
-
${
|
|
5117
|
+
${pc26.cyan(suggested)}
|
|
4745
5118
|
|
|
4746
5119
|
${topLevelHint}`
|
|
4747
5120
|
);
|
|
4748
5121
|
process.exit(1);
|
|
4749
5122
|
}
|
|
4750
5123
|
console.error(
|
|
4751
|
-
|
|
5124
|
+
pc26.red(`error: unknown command '${first}'`) + `
|
|
4752
5125
|
|
|
4753
5126
|
If '${first}' is an app name, invoke one of its commands with:
|
|
4754
|
-
${
|
|
4755
|
-
${
|
|
5127
|
+
${pc26.cyan(`bot run ${first} <command> [--key value ...]`)}
|
|
5128
|
+
${pc26.cyan("bot apps --json")} ${pc26.dim("(to see what commands exist)")}
|
|
4756
5129
|
|
|
4757
5130
|
Top-level commands: ${known.join(", ")}
|
|
4758
5131
|
${topLevelHint}`
|