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/index.js
CHANGED
|
@@ -6,8 +6,8 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
6
6
|
});
|
|
7
7
|
|
|
8
8
|
// src/index.ts
|
|
9
|
-
import { Command as
|
|
10
|
-
import
|
|
9
|
+
import { Command as Command25 } from "commander";
|
|
10
|
+
import pc26 from "picocolors";
|
|
11
11
|
|
|
12
12
|
// src/commands/server.ts
|
|
13
13
|
import { Command } from "commander";
|
|
@@ -194,7 +194,8 @@ function writeYaml(profiles) {
|
|
|
194
194
|
server: p.server,
|
|
195
195
|
daemonId: p.daemonId,
|
|
196
196
|
daemonName: p.daemonName,
|
|
197
|
-
token: p.token
|
|
197
|
+
token: p.token,
|
|
198
|
+
...p.userEmail ? { userEmail: p.userEmail } : {}
|
|
198
199
|
};
|
|
199
200
|
}
|
|
200
201
|
writeFileSync2(DAEMON_FILE, stringify2({ profiles: map }), "utf-8");
|
|
@@ -234,7 +235,8 @@ function loadDaemonProfiles() {
|
|
|
234
235
|
server: normalizeServer(value.server),
|
|
235
236
|
daemonId: value.daemonId,
|
|
236
237
|
daemonName: value.daemonName ?? value.daemonId,
|
|
237
|
-
token: value.token
|
|
238
|
+
token: value.token,
|
|
239
|
+
userEmail: value.userEmail
|
|
238
240
|
});
|
|
239
241
|
}
|
|
240
242
|
}
|
|
@@ -273,7 +275,8 @@ function saveDaemonProfile(input2) {
|
|
|
273
275
|
server,
|
|
274
276
|
daemonId: input2.daemonId,
|
|
275
277
|
daemonName: input2.daemonName ?? input2.daemonId,
|
|
276
|
-
token: input2.token
|
|
278
|
+
token: input2.token,
|
|
279
|
+
userEmail: input2.userEmail
|
|
277
280
|
};
|
|
278
281
|
writeYaml([...remaining, next]);
|
|
279
282
|
return next;
|
|
@@ -1084,6 +1087,28 @@ function pickProfilesToRun(alias, server) {
|
|
|
1084
1087
|
}
|
|
1085
1088
|
return loadDaemonProfiles();
|
|
1086
1089
|
}
|
|
1090
|
+
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) => {
|
|
1091
|
+
const removed = removeDaemonProfile(aliasOrServer);
|
|
1092
|
+
if (!removed) {
|
|
1093
|
+
console.error(pc3.red(`No paired profile matching "${aliasOrServer}".`));
|
|
1094
|
+
process.exitCode = 1;
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
console.log(pc3.green(`Removed daemon profile: ${aliasOrServer}`));
|
|
1098
|
+
});
|
|
1099
|
+
daemonCommand.command("list").alias("ls").description("List paired daemon profiles from ~/.botapp/daemon.yaml").action(() => {
|
|
1100
|
+
const profiles = loadDaemonProfiles();
|
|
1101
|
+
if (profiles.length === 0) {
|
|
1102
|
+
console.log(pc3.dim("No paired daemons. Run `bot pair` to add one."));
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
for (const p of profiles) {
|
|
1106
|
+
console.log(pc3.bold(p.alias ?? p.server));
|
|
1107
|
+
console.log(` Server: ${p.server}`);
|
|
1108
|
+
console.log(` Daemon: ${p.daemonName} ${pc3.dim(`(${p.daemonId})`)}`);
|
|
1109
|
+
console.log(` User: ${p.userEmail ?? pc3.dim("unknown \u2014 re-pair to capture")}`);
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1087
1112
|
daemonCommand.command("stop").description("Stop the background daemon started by `bot launch`").action(async () => {
|
|
1088
1113
|
const pidFile = join5(homedir5(), ".botapp", "daemon.pid");
|
|
1089
1114
|
if (!existsSync5(pidFile)) {
|
|
@@ -1112,16 +1137,17 @@ daemonCommand.command("agent").description("Manage agents hosted by this daemon"
|
|
|
1112
1137
|
for (const agent of data.agents ?? []) {
|
|
1113
1138
|
console.log(`${pc3.bold(agent.name)} ${pc3.dim(`(${agent.id})`)}`);
|
|
1114
1139
|
console.log(` Kind: ${agent.kind}`);
|
|
1140
|
+
if (agent.model) console.log(` Model: ${agent.model}`);
|
|
1115
1141
|
console.log(` Command: ${agent.command} ${(agent.args ?? []).join(" ")}`);
|
|
1116
1142
|
if (agent.cwd) console.log(` CWD: ${agent.cwd}`);
|
|
1117
1143
|
}
|
|
1118
1144
|
})
|
|
1119
1145
|
).addCommand(createDaemonAgentConfigCommand()).addCommand(
|
|
1120
|
-
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(
|
|
1146
|
+
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(
|
|
1121
1147
|
"--kind <kind>",
|
|
1122
|
-
"Agent adapter kind: acp, codex, claude-code, openclaw, hermes, hermes-agent, or shell",
|
|
1148
|
+
"Agent adapter kind: acp, codex, claude-code, kimi, openclaw, hermes, hermes-agent, or shell",
|
|
1123
1149
|
"acp"
|
|
1124
|
-
).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) => {
|
|
1150
|
+
).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) => {
|
|
1125
1151
|
const profile = requireSelectedProfile(opts);
|
|
1126
1152
|
if (!profile) return;
|
|
1127
1153
|
const env = parseEnv(opts.env ?? []);
|
|
@@ -1137,12 +1163,14 @@ daemonCommand.command("agent").description("Manage agents hosted by this daemon"
|
|
|
1137
1163
|
command: opts.command,
|
|
1138
1164
|
args: opts.arg ?? [],
|
|
1139
1165
|
cwd: opts.cwd,
|
|
1140
|
-
env
|
|
1166
|
+
env,
|
|
1167
|
+
model: opts.model
|
|
1141
1168
|
}
|
|
1142
1169
|
}
|
|
1143
1170
|
);
|
|
1144
1171
|
console.log(pc3.green(`Registered daemon agent: ${pc3.bold(data.agent.name)}`));
|
|
1145
1172
|
console.log(` ID: ${data.agent.id}`);
|
|
1173
|
+
if (data.agent.model) console.log(` Model: ${data.agent.model}`);
|
|
1146
1174
|
console.log(` Command: ${data.agent.command} ${(data.agent.args ?? []).join(" ")}`);
|
|
1147
1175
|
})
|
|
1148
1176
|
).addCommand(
|
|
@@ -1308,7 +1336,10 @@ async function runAgentJob(job, update) {
|
|
|
1308
1336
|
if (job.agent.kind === "claude-code" || job.agent.kind === "claude_code") {
|
|
1309
1337
|
return runClaudeCodeAgent(job, update);
|
|
1310
1338
|
}
|
|
1311
|
-
if (job.agent.kind === "
|
|
1339
|
+
if (job.agent.kind === "kimi") {
|
|
1340
|
+
return runKimiCodeAgent(job, update);
|
|
1341
|
+
}
|
|
1342
|
+
if (job.agent.kind === "openclaw") {
|
|
1312
1343
|
return runOpenClawAgent(job, update);
|
|
1313
1344
|
}
|
|
1314
1345
|
if (job.agent.kind === "hermes" || job.agent.kind === "hermes-agent") {
|
|
@@ -1349,6 +1380,9 @@ async function runCodexAgent(job, update) {
|
|
|
1349
1380
|
if (!args.includes("--skip-git-repo-check")) {
|
|
1350
1381
|
args.push("--skip-git-repo-check");
|
|
1351
1382
|
}
|
|
1383
|
+
if (job.agent.model && !hasAnyFlag(args, ["--model", "-m"])) {
|
|
1384
|
+
args.push("--model", job.agent.model);
|
|
1385
|
+
}
|
|
1352
1386
|
if (!hasAnyFlag(args, [
|
|
1353
1387
|
"--sandbox",
|
|
1354
1388
|
"-s",
|
|
@@ -1377,12 +1411,19 @@ async function runCodexAgent(job, update) {
|
|
|
1377
1411
|
rawEvents: []
|
|
1378
1412
|
};
|
|
1379
1413
|
const toolCalls = /* @__PURE__ */ new Map();
|
|
1414
|
+
const errors = [];
|
|
1380
1415
|
function processLine(line) {
|
|
1381
1416
|
if (!line.trim()) return;
|
|
1382
1417
|
try {
|
|
1383
1418
|
const event = JSON.parse(line);
|
|
1384
1419
|
result.rawEvents.push(event);
|
|
1385
1420
|
update({ kind: "codex_event", event });
|
|
1421
|
+
if (event?.type === "error" && typeof event.message === "string") {
|
|
1422
|
+
errors.push(event.message);
|
|
1423
|
+
}
|
|
1424
|
+
if (event?.type === "turn.failed" && typeof event.error?.message === "string") {
|
|
1425
|
+
errors.push(event.error.message);
|
|
1426
|
+
}
|
|
1386
1427
|
if (event?.type === "item.completed" && event.item?.type === "agent_message") {
|
|
1387
1428
|
if (typeof event.item.text === "string") {
|
|
1388
1429
|
result.messages.push(event.item.text);
|
|
@@ -1412,7 +1453,7 @@ async function runCodexAgent(job, update) {
|
|
|
1412
1453
|
rl.on("line", processLine);
|
|
1413
1454
|
const code = await new Promise((resolve11) => child.on("close", resolve11));
|
|
1414
1455
|
if (code !== 0) {
|
|
1415
|
-
throw new Error(stderr.trim() || `Codex exited with code ${code}`);
|
|
1456
|
+
throw new Error(errors.at(-1) ?? (stderr.trim() || `Codex exited with code ${code}`));
|
|
1416
1457
|
}
|
|
1417
1458
|
result.text = result.messages.at(-1)?.trim() || "";
|
|
1418
1459
|
result.toolCalls = [...toolCalls.values()];
|
|
@@ -1429,6 +1470,9 @@ async function runClaudeCodeAgent(job, update) {
|
|
|
1429
1470
|
if (!args.includes("--verbose")) {
|
|
1430
1471
|
args.push("--verbose");
|
|
1431
1472
|
}
|
|
1473
|
+
if (job.agent.model && !hasAnyFlag(args, ["--model"])) {
|
|
1474
|
+
args.push("--model", job.agent.model);
|
|
1475
|
+
}
|
|
1432
1476
|
if (!hasAnyFlag(args, [
|
|
1433
1477
|
"--permission-mode",
|
|
1434
1478
|
"--dangerously-skip-permissions",
|
|
@@ -1451,6 +1495,9 @@ async function runClaudeCodeAgent(job, update) {
|
|
|
1451
1495
|
child.stderr.on("data", (chunk) => {
|
|
1452
1496
|
stderr += chunk.toString();
|
|
1453
1497
|
});
|
|
1498
|
+
const noiseLines = [];
|
|
1499
|
+
const errorEvents = [];
|
|
1500
|
+
const NOISE_TAIL = 6;
|
|
1454
1501
|
const result = {
|
|
1455
1502
|
kind: "claude-code",
|
|
1456
1503
|
text: "",
|
|
@@ -1508,11 +1555,19 @@ async function runClaudeCodeAgent(job, update) {
|
|
|
1508
1555
|
try {
|
|
1509
1556
|
event = JSON.parse(line);
|
|
1510
1557
|
} catch {
|
|
1558
|
+
noiseLines.push(line);
|
|
1559
|
+
if (noiseLines.length > NOISE_TAIL) noiseLines.shift();
|
|
1511
1560
|
return;
|
|
1512
1561
|
}
|
|
1513
1562
|
result.rawEvents.push(event);
|
|
1514
1563
|
update({ kind: "claude_code_event", event });
|
|
1515
1564
|
captureSessionId(event);
|
|
1565
|
+
if (event && (event.is_error === true || event.subtype === "error" || event.type === "result" && event.is_error)) {
|
|
1566
|
+
try {
|
|
1567
|
+
errorEvents.push(JSON.stringify(event).slice(0, 1e3));
|
|
1568
|
+
} catch {
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1516
1571
|
if (event.type === "assistant" && event.message?.content) {
|
|
1517
1572
|
ingestAssistantContent(
|
|
1518
1573
|
Array.isArray(event.message.content) ? event.message.content : []
|
|
@@ -1533,7 +1588,146 @@ async function runClaudeCodeAgent(job, update) {
|
|
|
1533
1588
|
rl.on("line", processLine);
|
|
1534
1589
|
const code = await new Promise((resolve11) => child.on("close", resolve11));
|
|
1535
1590
|
if (code !== 0) {
|
|
1536
|
-
|
|
1591
|
+
const parts = [];
|
|
1592
|
+
if (stderr.trim()) parts.push(stderr.trim());
|
|
1593
|
+
if (errorEvents.length > 0) {
|
|
1594
|
+
parts.push(`Error events: ${errorEvents.join(" | ")}`);
|
|
1595
|
+
}
|
|
1596
|
+
if (noiseLines.length > 0 && parts.length === 0) {
|
|
1597
|
+
parts.push(`Last stdout lines: ${noiseLines.join(" | ").slice(-1500)}`);
|
|
1598
|
+
}
|
|
1599
|
+
if (parts.length === 0) {
|
|
1600
|
+
parts.push(`Claude Code exited with code ${code} (no stderr or stdout output)`);
|
|
1601
|
+
}
|
|
1602
|
+
throw new Error(parts.join(" \u2014 "));
|
|
1603
|
+
}
|
|
1604
|
+
if (!result.text) {
|
|
1605
|
+
result.text = result.messages.at(-1)?.trim() ?? "";
|
|
1606
|
+
}
|
|
1607
|
+
result.toolCalls = [...toolCalls.values()];
|
|
1608
|
+
return JSON.stringify(result);
|
|
1609
|
+
}
|
|
1610
|
+
async function runKimiCodeAgent(job, update) {
|
|
1611
|
+
const args = [...job.agent.args];
|
|
1612
|
+
if (!args.includes("--print") && !args.includes("-p")) {
|
|
1613
|
+
args.push("--print");
|
|
1614
|
+
}
|
|
1615
|
+
if (!args.includes("--output-format")) {
|
|
1616
|
+
args.push("--output-format", "stream-json");
|
|
1617
|
+
}
|
|
1618
|
+
if (!hasAnyFlag(args, ["--yolo", "--yes", "-y"])) {
|
|
1619
|
+
args.push("--yolo");
|
|
1620
|
+
}
|
|
1621
|
+
if (job.agent.model && !hasAnyFlag(args, ["--model", "-m"])) {
|
|
1622
|
+
args.push("--model", job.agent.model);
|
|
1623
|
+
}
|
|
1624
|
+
const resume = job.resumeSessionId ?? job.agent.env?.KIMI_SESSION_ID ?? null;
|
|
1625
|
+
if (resume && !hasAnyFlag(args, ["--session", "-S", "-r", "--resume", "--continue", "-C"])) {
|
|
1626
|
+
args.push("-r", resume);
|
|
1627
|
+
}
|
|
1628
|
+
const cwd = job.agent.cwd ?? process.cwd();
|
|
1629
|
+
let prompt = job.query;
|
|
1630
|
+
if (!resume) {
|
|
1631
|
+
const agentsMdPath = join5(cwd, "AGENTS.md");
|
|
1632
|
+
if (existsSync5(agentsMdPath)) {
|
|
1633
|
+
try {
|
|
1634
|
+
const primer = readFileSync3(agentsMdPath, "utf-8").trim();
|
|
1635
|
+
if (primer) {
|
|
1636
|
+
prompt = `# System instructions (read carefully before responding)
|
|
1637
|
+
|
|
1638
|
+
${primer}
|
|
1639
|
+
|
|
1640
|
+
---
|
|
1641
|
+
|
|
1642
|
+
# Your task
|
|
1643
|
+
|
|
1644
|
+
${job.query}`;
|
|
1645
|
+
}
|
|
1646
|
+
} catch {
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
const child = spawn3(job.agent.command, args, {
|
|
1651
|
+
cwd,
|
|
1652
|
+
env: { ...process.env, ...job.agent.env ?? {} },
|
|
1653
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1654
|
+
});
|
|
1655
|
+
child.stdin.write(prompt);
|
|
1656
|
+
child.stdin.end();
|
|
1657
|
+
let stderr = "";
|
|
1658
|
+
child.stderr.on("data", (chunk) => {
|
|
1659
|
+
stderr += chunk.toString();
|
|
1660
|
+
});
|
|
1661
|
+
const result = {
|
|
1662
|
+
kind: "kimi",
|
|
1663
|
+
text: "",
|
|
1664
|
+
messages: [],
|
|
1665
|
+
toolCalls: [],
|
|
1666
|
+
sessionId: resume,
|
|
1667
|
+
rawEvents: []
|
|
1668
|
+
};
|
|
1669
|
+
const toolCalls = /* @__PURE__ */ new Map();
|
|
1670
|
+
function ingestAssistantContent(blocks) {
|
|
1671
|
+
for (const block of blocks) {
|
|
1672
|
+
if (!block || typeof block !== "object") continue;
|
|
1673
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
1674
|
+
result.messages.push(block.text);
|
|
1675
|
+
update({ kind: "message", text: block.text });
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
function ingestToolCalls(calls) {
|
|
1680
|
+
for (const call of calls) {
|
|
1681
|
+
if (!call || typeof call !== "object") continue;
|
|
1682
|
+
const id = typeof call.id === "string" ? call.id : null;
|
|
1683
|
+
if (!id) continue;
|
|
1684
|
+
const name = String(call.function?.name ?? call.name ?? "tool");
|
|
1685
|
+
let input2 = call.function?.arguments ?? call.arguments;
|
|
1686
|
+
if (typeof input2 === "string") {
|
|
1687
|
+
try {
|
|
1688
|
+
input2 = JSON.parse(input2);
|
|
1689
|
+
} catch {
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
const next = { ...toolCalls.get(id) ?? {}, id, name, input: input2 };
|
|
1693
|
+
toolCalls.set(id, next);
|
|
1694
|
+
update({ kind: "tool_call", toolCall: next });
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
function ingestToolResult(event) {
|
|
1698
|
+
const id = event?.tool_call_id;
|
|
1699
|
+
if (typeof id !== "string") return;
|
|
1700
|
+
const existing = toolCalls.get(id) ?? { id, name: "tool" };
|
|
1701
|
+
const output2 = typeof event.content === "string" ? event.content : JSON.stringify(event.content);
|
|
1702
|
+
const next = { ...existing, output: output2 };
|
|
1703
|
+
toolCalls.set(id, next);
|
|
1704
|
+
update({ kind: "tool_call", toolCall: next });
|
|
1705
|
+
}
|
|
1706
|
+
function processLine(line) {
|
|
1707
|
+
const trimmed = line.trim();
|
|
1708
|
+
if (!trimmed || trimmed[0] !== "{") return;
|
|
1709
|
+
let event;
|
|
1710
|
+
try {
|
|
1711
|
+
event = JSON.parse(trimmed);
|
|
1712
|
+
} catch {
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
result.rawEvents.push(event);
|
|
1716
|
+
update({ kind: "kimi_event", event });
|
|
1717
|
+
if (event.role === "assistant") {
|
|
1718
|
+
if (Array.isArray(event.content)) ingestAssistantContent(event.content);
|
|
1719
|
+
if (Array.isArray(event.tool_calls)) ingestToolCalls(event.tool_calls);
|
|
1720
|
+
} else if (event.role === "tool") {
|
|
1721
|
+
ingestToolResult(event);
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
const rl = createInterface2({ input: child.stdout });
|
|
1725
|
+
rl.on("line", processLine);
|
|
1726
|
+
const code = await new Promise((resolve11) => child.on("close", resolve11));
|
|
1727
|
+
const resumeMatch = stderr.match(/To resume this session:\s*kimi\s+-r\s+([A-Za-z0-9_-]+)/);
|
|
1728
|
+
if (resumeMatch) result.sessionId = resumeMatch[1];
|
|
1729
|
+
if (code !== 0) {
|
|
1730
|
+
throw new Error(stderr.trim() || `Kimi Code exited with code ${code}`);
|
|
1537
1731
|
}
|
|
1538
1732
|
if (!result.text) {
|
|
1539
1733
|
result.text = result.messages.at(-1)?.trim() ?? "";
|
|
@@ -1597,6 +1791,7 @@ async function runOpenClawAgent(job, update) {
|
|
|
1597
1791
|
};
|
|
1598
1792
|
const toolCalls = /* @__PURE__ */ new Map();
|
|
1599
1793
|
let stderr = "";
|
|
1794
|
+
const stdoutLines = [];
|
|
1600
1795
|
let stopPolling = false;
|
|
1601
1796
|
const child = spawn3(job.agent.command, args, {
|
|
1602
1797
|
cwd: job.agent.cwd ?? process.cwd(),
|
|
@@ -1604,7 +1799,15 @@ async function runOpenClawAgent(job, update) {
|
|
|
1604
1799
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1605
1800
|
});
|
|
1606
1801
|
const stdout = createInterface2({ input: child.stdout });
|
|
1607
|
-
stdout.on("line", () => {
|
|
1802
|
+
stdout.on("line", (line) => {
|
|
1803
|
+
const text = line.trim();
|
|
1804
|
+
if (!text) return;
|
|
1805
|
+
stdoutLines.push(text);
|
|
1806
|
+
const stdoutText = extractOpenClawStdoutText(text);
|
|
1807
|
+
if (!stdoutText) return;
|
|
1808
|
+
result.text = stdoutText;
|
|
1809
|
+
result.messages.push(stdoutText);
|
|
1810
|
+
update({ kind: "message", text: stdoutText });
|
|
1608
1811
|
});
|
|
1609
1812
|
const stderrReader = createInterface2({ input: child.stderr });
|
|
1610
1813
|
stderrReader.on("line", (line) => {
|
|
@@ -1636,11 +1839,11 @@ async function runOpenClawAgent(job, update) {
|
|
|
1636
1839
|
}
|
|
1637
1840
|
result.sessionFile = state.selectedFile;
|
|
1638
1841
|
result.toolCalls = [...toolCalls.values()];
|
|
1639
|
-
result.text = result.text || result.messages.at(-1)?.trim() || "";
|
|
1842
|
+
result.text = result.text || result.messages.at(-1)?.trim() || extractOpenClawStdoutText(stdoutLines.at(-1) ?? "") || "";
|
|
1640
1843
|
if (code !== 0) {
|
|
1641
1844
|
throw new Error(stderr.trim() || `OpenClaw exited with code ${code}`);
|
|
1642
1845
|
}
|
|
1643
|
-
if (!result.sessionId) {
|
|
1846
|
+
if (!result.sessionId && !result.text && result.toolCalls.length === 0) {
|
|
1644
1847
|
throw new Error(
|
|
1645
1848
|
[
|
|
1646
1849
|
"OpenClaw completed, but the session key was not resolved from sessions.json.",
|
|
@@ -1661,6 +1864,17 @@ async function runOpenClawAgent(job, update) {
|
|
|
1661
1864
|
}
|
|
1662
1865
|
return JSON.stringify(result);
|
|
1663
1866
|
}
|
|
1867
|
+
function extractOpenClawStdoutText(line) {
|
|
1868
|
+
const text = line.trim();
|
|
1869
|
+
if (!text) return "";
|
|
1870
|
+
try {
|
|
1871
|
+
const parsed = JSON.parse(text);
|
|
1872
|
+
if (parsed && typeof parsed === "object") return JSON.stringify(parsed);
|
|
1873
|
+
if (typeof parsed === "string") return parsed;
|
|
1874
|
+
} catch {
|
|
1875
|
+
}
|
|
1876
|
+
return text;
|
|
1877
|
+
}
|
|
1664
1878
|
function resolveOpenClawSessionDir(args, env) {
|
|
1665
1879
|
const configured = env.BOTAPP_OPENCLAW_SESSION_DIR ?? env.OPENCLAW_SESSION_DIR;
|
|
1666
1880
|
if (configured) return expandPath(configured);
|
|
@@ -2173,7 +2387,7 @@ Invalid ACP stdout: ${line}`;
|
|
|
2173
2387
|
clientInfo: {
|
|
2174
2388
|
name: "botapp-daemon",
|
|
2175
2389
|
title: "botapp daemon",
|
|
2176
|
-
version: "0.2.
|
|
2390
|
+
version: "0.2.8"
|
|
2177
2391
|
}
|
|
2178
2392
|
});
|
|
2179
2393
|
const session = await request2("session/new", {
|
|
@@ -2748,7 +2962,8 @@ Configured for ${serverUrl}`));
|
|
|
2748
2962
|
server: result.serverUrl ?? serverUrl,
|
|
2749
2963
|
daemonId: data.daemon.id,
|
|
2750
2964
|
daemonName: data.daemon.name,
|
|
2751
|
-
token: data.token
|
|
2965
|
+
token: data.token,
|
|
2966
|
+
userEmail: result.userEmail
|
|
2752
2967
|
});
|
|
2753
2968
|
console.log(
|
|
2754
2969
|
` Daemon: ${pc5.bold(data.daemon.name)} ${pc5.dim(`(${data.daemon.id})`)}` + pc5.dim(` profile=${savedProfile.alias}`)
|
|
@@ -3807,7 +4022,8 @@ var pairingCommand = new Command18("pairing").alias("pair").description("Pair th
|
|
|
3807
4022
|
server: serverUrl,
|
|
3808
4023
|
daemonId: data.daemon.id,
|
|
3809
4024
|
daemonName: data.daemon.name,
|
|
3810
|
-
token: data.token
|
|
4025
|
+
token: data.token,
|
|
4026
|
+
userEmail: grant.userEmail
|
|
3811
4027
|
});
|
|
3812
4028
|
console.log(pc19.green(`Paired daemon: ${pc19.bold(data.daemon.name)}`));
|
|
3813
4029
|
console.log(` ID: ${data.daemon.id}`);
|
|
@@ -3843,13 +4059,130 @@ async function obtainPairingToken(opts) {
|
|
|
3843
4059
|
return { pairingToken: result.pairingToken, userEmail: result.userEmail };
|
|
3844
4060
|
}
|
|
3845
4061
|
|
|
4062
|
+
// src/commands/doctor.ts
|
|
4063
|
+
import { Command as Command19 } from "commander";
|
|
4064
|
+
import pc20 from "picocolors";
|
|
4065
|
+
var doctorCommand = new Command19("doctor").description("Diagnose paired daemon profiles (server reachability, token validity, account ownership)").action(async () => {
|
|
4066
|
+
const profiles = loadDaemonProfiles();
|
|
4067
|
+
if (profiles.length === 0) {
|
|
4068
|
+
console.log(pc20.dim("No paired daemons in ~/.botapp/daemon.yaml."));
|
|
4069
|
+
console.log(pc20.dim("Run `bot pair` to add one."));
|
|
4070
|
+
return;
|
|
4071
|
+
}
|
|
4072
|
+
let warns = 0;
|
|
4073
|
+
let fails = 0;
|
|
4074
|
+
for (const profile of profiles) {
|
|
4075
|
+
console.log(pc20.bold(`
|
|
4076
|
+
[${profile.alias ?? profile.server}]`));
|
|
4077
|
+
console.log(` Server: ${profile.server}`);
|
|
4078
|
+
console.log(` Daemon: ${profile.daemonName} ${pc20.dim(`(${profile.daemonId})`)}`);
|
|
4079
|
+
const findings = await checkProfile(profile);
|
|
4080
|
+
for (const f of findings) {
|
|
4081
|
+
const tag = f.severity === "ok" ? pc20.green(" \u2713 ") : f.severity === "warn" ? pc20.yellow(" \u26A0 ") : pc20.red(" \u2717 ");
|
|
4082
|
+
console.log(`${tag}${f.message}`);
|
|
4083
|
+
if (f.fix) console.log(pc20.dim(` \u2192 ${f.fix}`));
|
|
4084
|
+
if (f.severity === "warn") warns += 1;
|
|
4085
|
+
if (f.severity === "fail") fails += 1;
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
console.log("");
|
|
4089
|
+
if (fails === 0 && warns === 0) {
|
|
4090
|
+
console.log(pc20.green("All checks passed."));
|
|
4091
|
+
} else {
|
|
4092
|
+
const parts = [];
|
|
4093
|
+
if (fails > 0) parts.push(pc20.red(`${fails} failing`));
|
|
4094
|
+
if (warns > 0) parts.push(pc20.yellow(`${warns} warning${warns === 1 ? "" : "s"}`));
|
|
4095
|
+
console.log(parts.join(", ") + ".");
|
|
4096
|
+
if (fails > 0) process.exitCode = 1;
|
|
4097
|
+
}
|
|
4098
|
+
});
|
|
4099
|
+
async function checkProfile(profile) {
|
|
4100
|
+
const findings = [];
|
|
4101
|
+
let serverOk = false;
|
|
4102
|
+
try {
|
|
4103
|
+
const res = await fetch(`${profile.server}/health`, {
|
|
4104
|
+
signal: AbortSignal.timeout(5e3)
|
|
4105
|
+
});
|
|
4106
|
+
if (res.ok) {
|
|
4107
|
+
findings.push({ severity: "ok", message: "Server reachable" });
|
|
4108
|
+
serverOk = true;
|
|
4109
|
+
} else {
|
|
4110
|
+
findings.push({
|
|
4111
|
+
severity: "fail",
|
|
4112
|
+
message: `Server returned ${res.status} ${res.statusText}`,
|
|
4113
|
+
fix: `Verify the server at ${profile.server} is running.`
|
|
4114
|
+
});
|
|
4115
|
+
}
|
|
4116
|
+
} catch (e) {
|
|
4117
|
+
findings.push({
|
|
4118
|
+
severity: "fail",
|
|
4119
|
+
message: `Server unreachable: ${e.message ?? e}`,
|
|
4120
|
+
fix: `Start the server, or remove this profile with \`bot daemon unpair ${profile.alias ?? profile.server}\`.`
|
|
4121
|
+
});
|
|
4122
|
+
}
|
|
4123
|
+
if (!serverOk) return findings;
|
|
4124
|
+
let serverEmail = null;
|
|
4125
|
+
try {
|
|
4126
|
+
const data = await daemonRequest(profile.server, profile.token, "/api/daemon/self");
|
|
4127
|
+
findings.push({ severity: "ok", message: "Token valid" });
|
|
4128
|
+
serverEmail = data.user?.email ?? null;
|
|
4129
|
+
if (serverEmail) {
|
|
4130
|
+
findings.push({
|
|
4131
|
+
severity: "ok",
|
|
4132
|
+
message: `Token paired under ${pc20.bold(serverEmail)}`
|
|
4133
|
+
});
|
|
4134
|
+
} else {
|
|
4135
|
+
findings.push({
|
|
4136
|
+
severity: "warn",
|
|
4137
|
+
message: "Server returned no user record for this daemon",
|
|
4138
|
+
fix: "The owning user may have been deleted. Re-pair with `bot pair`."
|
|
4139
|
+
});
|
|
4140
|
+
}
|
|
4141
|
+
if (data.daemon?.status === "online") {
|
|
4142
|
+
findings.push({ severity: "ok", message: "Daemon currently online" });
|
|
4143
|
+
} else {
|
|
4144
|
+
findings.push({
|
|
4145
|
+
severity: "warn",
|
|
4146
|
+
message: `Daemon status: ${data.daemon?.status ?? "unknown"}`,
|
|
4147
|
+
fix: `Run \`bot daemon run --server ${profile.server}\` to bring it online.`
|
|
4148
|
+
});
|
|
4149
|
+
}
|
|
4150
|
+
} catch (e) {
|
|
4151
|
+
const msg = e?.message ?? String(e);
|
|
4152
|
+
if (/unauthor/i.test(msg) || /token/i.test(msg)) {
|
|
4153
|
+
findings.push({
|
|
4154
|
+
severity: "fail",
|
|
4155
|
+
message: `Token rejected: ${msg}`,
|
|
4156
|
+
fix: "The token is stale (likely the server DB was reset). Re-pair with `bot pair`."
|
|
4157
|
+
});
|
|
4158
|
+
return findings;
|
|
4159
|
+
}
|
|
4160
|
+
findings.push({ severity: "fail", message: `Identity probe failed: ${msg}` });
|
|
4161
|
+
return findings;
|
|
4162
|
+
}
|
|
4163
|
+
if (profile.userEmail && serverEmail && profile.userEmail !== serverEmail) {
|
|
4164
|
+
findings.push({
|
|
4165
|
+
severity: "warn",
|
|
4166
|
+
message: `daemon.yaml says \`userEmail: ${profile.userEmail}\` but server says \`${serverEmail}\``,
|
|
4167
|
+
fix: "Re-pair to refresh the recorded email."
|
|
4168
|
+
});
|
|
4169
|
+
} else if (!profile.userEmail && serverEmail) {
|
|
4170
|
+
findings.push({
|
|
4171
|
+
severity: "warn",
|
|
4172
|
+
message: "daemon.yaml has no userEmail recorded",
|
|
4173
|
+
fix: "Re-pair to capture it (cosmetic \u2014 auth still works)."
|
|
4174
|
+
});
|
|
4175
|
+
}
|
|
4176
|
+
return findings;
|
|
4177
|
+
}
|
|
4178
|
+
|
|
3846
4179
|
// src/commands/update.ts
|
|
3847
4180
|
import { spawn as spawn7 } from "child_process";
|
|
3848
4181
|
import { realpathSync as realpathSync2 } from "fs";
|
|
3849
|
-
import { Command as
|
|
3850
|
-
import
|
|
4182
|
+
import { Command as Command20 } from "commander";
|
|
4183
|
+
import pc21 from "picocolors";
|
|
3851
4184
|
var PACKAGE_NAME = "botapp-cli";
|
|
3852
|
-
var updateCommand = new
|
|
4185
|
+
var updateCommand = new Command20("update").description("Update the `bot` CLI itself to the latest published version").option(
|
|
3853
4186
|
"--manager <pm>",
|
|
3854
4187
|
"Force a package manager: npm | pnpm | yarn | brew (default: auto-detect)"
|
|
3855
4188
|
).option("--dry-run", "Print the command that would run, but do not execute it").action(async (opts) => {
|
|
@@ -3857,12 +4190,12 @@ var updateCommand = new Command19("update").description("Update the `bot` CLI it
|
|
|
3857
4190
|
const manager = forced ?? detectPackageManager();
|
|
3858
4191
|
if (manager === "npx") {
|
|
3859
4192
|
console.log(
|
|
3860
|
-
|
|
4193
|
+
pc21.green(
|
|
3861
4194
|
"You are running `bot` via `npx -y botapp-cli@latest` \u2014 every invocation already fetches the latest version."
|
|
3862
4195
|
)
|
|
3863
4196
|
);
|
|
3864
4197
|
console.log(
|
|
3865
|
-
|
|
4198
|
+
pc21.dim(
|
|
3866
4199
|
"For a faster startup, install it globally instead:\n npm i -g botapp-cli@latest\n pnpm add -g botapp-cli@latest"
|
|
3867
4200
|
)
|
|
3868
4201
|
);
|
|
@@ -3870,34 +4203,34 @@ var updateCommand = new Command19("update").description("Update the `bot` CLI it
|
|
|
3870
4203
|
}
|
|
3871
4204
|
if (!manager) {
|
|
3872
4205
|
console.error(
|
|
3873
|
-
|
|
4206
|
+
pc21.red(
|
|
3874
4207
|
"Couldn't detect how `bot` was installed. Pick one of these manually:"
|
|
3875
4208
|
)
|
|
3876
4209
|
);
|
|
3877
|
-
console.log(
|
|
3878
|
-
console.log(
|
|
3879
|
-
console.log(
|
|
3880
|
-
console.log(
|
|
4210
|
+
console.log(pc21.cyan(" npm i -g botapp-cli@latest"));
|
|
4211
|
+
console.log(pc21.cyan(" pnpm add -g botapp-cli@latest"));
|
|
4212
|
+
console.log(pc21.cyan(" yarn global add botapp-cli@latest"));
|
|
4213
|
+
console.log(pc21.cyan(" brew upgrade botapp-cli"));
|
|
3881
4214
|
process.exitCode = 1;
|
|
3882
4215
|
return;
|
|
3883
4216
|
}
|
|
3884
4217
|
const { command, args } = updateCommandFor(manager);
|
|
3885
|
-
console.log(
|
|
4218
|
+
console.log(pc21.dim(`$ ${command} ${args.join(" ")}`));
|
|
3886
4219
|
if (opts.dryRun) return;
|
|
3887
4220
|
const child = spawn7(command, args, { stdio: "inherit" });
|
|
3888
4221
|
const code = await new Promise((resolve11) => {
|
|
3889
4222
|
child.once("error", (err) => {
|
|
3890
|
-
console.error(
|
|
4223
|
+
console.error(pc21.red(`Failed to spawn ${command}: ${err.message}`));
|
|
3891
4224
|
resolve11(127);
|
|
3892
4225
|
});
|
|
3893
4226
|
child.once("close", resolve11);
|
|
3894
4227
|
});
|
|
3895
4228
|
if (code !== 0) {
|
|
3896
|
-
console.error(
|
|
4229
|
+
console.error(pc21.red(`Update failed (exit ${code}).`));
|
|
3897
4230
|
process.exitCode = code ?? 1;
|
|
3898
4231
|
return;
|
|
3899
4232
|
}
|
|
3900
|
-
console.log(
|
|
4233
|
+
console.log(pc21.green("botapp-cli updated. Run `bot --version` to confirm."));
|
|
3901
4234
|
});
|
|
3902
4235
|
function detectPackageManager() {
|
|
3903
4236
|
const argv = process.argv[1] ?? "";
|
|
@@ -3932,27 +4265,27 @@ function updateCommandFor(manager) {
|
|
|
3932
4265
|
}
|
|
3933
4266
|
|
|
3934
4267
|
// src/commands/simulate.ts
|
|
3935
|
-
import { Command as
|
|
4268
|
+
import { Command as Command21 } from "commander";
|
|
3936
4269
|
import { resolve as resolve8, join as join11 } from "path";
|
|
3937
4270
|
import { existsSync as existsSync13, readFileSync as readFileSync9 } from "fs";
|
|
3938
4271
|
import { spawn as spawn8 } from "child_process";
|
|
3939
|
-
import
|
|
3940
|
-
var simulateCommand = new
|
|
4272
|
+
import pc22 from "picocolors";
|
|
4273
|
+
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) => {
|
|
3941
4274
|
const absPath = resolve8(appPath);
|
|
3942
4275
|
if (!existsSync13(absPath)) {
|
|
3943
|
-
console.error(
|
|
4276
|
+
console.error(pc22.red(`Path not found: ${absPath}`));
|
|
3944
4277
|
process.exitCode = 1;
|
|
3945
4278
|
return;
|
|
3946
4279
|
}
|
|
3947
4280
|
const manifestPath = join11(absPath, "botapp.app.json");
|
|
3948
4281
|
if (!existsSync13(manifestPath)) {
|
|
3949
|
-
console.error(
|
|
4282
|
+
console.error(pc22.red(`No botapp.app.json found in ${absPath}`));
|
|
3950
4283
|
process.exitCode = 1;
|
|
3951
4284
|
return;
|
|
3952
4285
|
}
|
|
3953
4286
|
const manifest = JSON.parse(readFileSync9(manifestPath, "utf8"));
|
|
3954
4287
|
if (!manifest.name) {
|
|
3955
|
-
console.error(
|
|
4288
|
+
console.error(pc22.red('manifest missing "name"'));
|
|
3956
4289
|
process.exitCode = 1;
|
|
3957
4290
|
return;
|
|
3958
4291
|
}
|
|
@@ -3960,31 +4293,31 @@ var simulateCommand = new Command20("simulate").description("Dev-tunnel a local
|
|
|
3960
4293
|
const wsServer = httpServer.replace(/^http:/, "ws:").replace(/^https:/, "wss:") + "/ws/host";
|
|
3961
4294
|
const token = resolveToken(opts.token);
|
|
3962
4295
|
if (!token) {
|
|
3963
|
-
console.error(
|
|
4296
|
+
console.error(pc22.red("not logged in: run `bot login` or pass --token"));
|
|
3964
4297
|
process.exitCode = 1;
|
|
3965
4298
|
return;
|
|
3966
4299
|
}
|
|
3967
4300
|
const lifetimeMs = Math.max(6e4, Number(opts.lifetime) * 6e4);
|
|
3968
|
-
console.log(
|
|
4301
|
+
console.log(pc22.dim(`requesting dev token for "${manifest.name}" from ${httpServer}...`));
|
|
3969
4302
|
const devToken = await issueDevToken({
|
|
3970
4303
|
serverUrl: httpServer,
|
|
3971
4304
|
token,
|
|
3972
4305
|
appName: manifest.name,
|
|
3973
4306
|
lifetimeMs
|
|
3974
4307
|
});
|
|
3975
|
-
console.log(
|
|
4308
|
+
console.log(pc22.green("\u2713"), `dev token issued (lifetime ${opts.lifetime} min)`);
|
|
3976
4309
|
const entryPath = resolveEntry(absPath, opts.entry ?? manifest.entry);
|
|
3977
4310
|
if (!existsSync13(entryPath)) {
|
|
3978
|
-
console.error(
|
|
3979
|
-
console.error(
|
|
4311
|
+
console.error(pc22.red(`entry not found: ${entryPath}`));
|
|
4312
|
+
console.error(pc22.dim(" run `pnpm build` (or your build script) first."));
|
|
3980
4313
|
process.exitCode = 1;
|
|
3981
4314
|
return;
|
|
3982
4315
|
}
|
|
3983
|
-
console.log(
|
|
3984
|
-
console.log(
|
|
3985
|
-
console.log(
|
|
4316
|
+
console.log(pc22.dim(`spawning ${entryPath} ...`));
|
|
4317
|
+
console.log(pc22.dim(` BOTAPP_SERVER=${wsServer}`));
|
|
4318
|
+
console.log(pc22.dim(` BOTAPP_APP_NAME=${manifest.name}`));
|
|
3986
4319
|
console.log(
|
|
3987
|
-
|
|
4320
|
+
pc22.cyan(
|
|
3988
4321
|
`
|
|
3989
4322
|
When ready, your dashboard at ${httpServer} will route "${manifest.name}" to this process for your account only.
|
|
3990
4323
|
`
|
|
@@ -4002,7 +4335,7 @@ When ready, your dashboard at ${httpServer} will route "${manifest.name}" to thi
|
|
|
4002
4335
|
stdio: "inherit"
|
|
4003
4336
|
});
|
|
4004
4337
|
const stop = (signal) => {
|
|
4005
|
-
console.log(
|
|
4338
|
+
console.log(pc22.dim(`
|
|
4006
4339
|
stopping (${signal})...`));
|
|
4007
4340
|
if (!child.killed) child.kill(signal);
|
|
4008
4341
|
};
|
|
@@ -4010,7 +4343,7 @@ stopping (${signal})...`));
|
|
|
4010
4343
|
process.on("SIGTERM", () => stop("SIGTERM"));
|
|
4011
4344
|
child.on("exit", (code, sig) => {
|
|
4012
4345
|
const reason = sig ? `signal ${sig}` : `exit ${code}`;
|
|
4013
|
-
console.log(
|
|
4346
|
+
console.log(pc22.dim(`child exited (${reason})`));
|
|
4014
4347
|
process.exit(typeof code === "number" ? code : 0);
|
|
4015
4348
|
});
|
|
4016
4349
|
});
|
|
@@ -4047,14 +4380,14 @@ function resolveEntry(appDir, entry) {
|
|
|
4047
4380
|
}
|
|
4048
4381
|
|
|
4049
4382
|
// src/commands/init.ts
|
|
4050
|
-
import { Command as
|
|
4383
|
+
import { Command as Command22 } from "commander";
|
|
4051
4384
|
import { existsSync as existsSync14, mkdirSync as mkdirSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
4052
4385
|
import { resolve as resolve9, join as join12 } from "path";
|
|
4053
|
-
import
|
|
4054
|
-
var initCommand = new
|
|
4386
|
+
import pc23 from "picocolors";
|
|
4387
|
+
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) => {
|
|
4055
4388
|
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
4056
|
-
console.error(
|
|
4057
|
-
console.error(
|
|
4389
|
+
console.error(pc23.red("Invalid name. Use lowercase letters, digits, dashes; must start with a letter."));
|
|
4390
|
+
console.error(pc23.dim(" e.g. my-app, todo-tracker, gomoku-2"));
|
|
4058
4391
|
process.exitCode = 1;
|
|
4059
4392
|
return;
|
|
4060
4393
|
}
|
|
@@ -4068,8 +4401,8 @@ var initCommand = new Command21("init").description("Scaffold a new hosted-tier
|
|
|
4068
4401
|
}
|
|
4069
4402
|
})();
|
|
4070
4403
|
if (Array.isArray(stat) && stat.length > 0) {
|
|
4071
|
-
console.error(
|
|
4072
|
-
console.error(
|
|
4404
|
+
console.error(pc23.red(`Target directory exists and is not empty: ${targetDir}`));
|
|
4405
|
+
console.error(pc23.dim(" pass --force to overwrite, or pick a different --dir"));
|
|
4073
4406
|
process.exitCode = 1;
|
|
4074
4407
|
return;
|
|
4075
4408
|
}
|
|
@@ -4086,18 +4419,18 @@ var initCommand = new Command21("init").description("Scaffold a new hosted-tier
|
|
|
4086
4419
|
mkdirSync6(dirname2(full), { recursive: true });
|
|
4087
4420
|
writeFileSync4(full, content);
|
|
4088
4421
|
}
|
|
4089
|
-
console.log(
|
|
4422
|
+
console.log(pc23.green("\u2713"), `Scaffolded ${ctx.headless ? "headless " : ""}app at`, pc23.cyan(targetDir));
|
|
4090
4423
|
console.log();
|
|
4091
4424
|
console.log("Next steps:");
|
|
4092
|
-
console.log(
|
|
4093
|
-
console.log(
|
|
4094
|
-
if (!ctx.headless) console.log(
|
|
4095
|
-
else console.log(
|
|
4096
|
-
console.log(
|
|
4097
|
-
console.log(
|
|
4425
|
+
console.log(pc23.dim(` cd ${targetDir.replace(process.cwd() + "/", "")}`));
|
|
4426
|
+
console.log(pc23.dim(" pnpm install # or npm install"));
|
|
4427
|
+
if (!ctx.headless) console.log(pc23.dim(" pnpm build # builds api/ + frontend (dist/)"));
|
|
4428
|
+
else console.log(pc23.dim(" pnpm build # builds api/ to dist/api.js"));
|
|
4429
|
+
console.log(pc23.dim(` bot login --server <your-botapp-server>`));
|
|
4430
|
+
console.log(pc23.dim(` bot simulate # dev-tunnel into the server, hot-reload as you build`));
|
|
4098
4431
|
console.log();
|
|
4099
4432
|
console.log(
|
|
4100
|
-
|
|
4433
|
+
pc23.dim("Once it works, `bot publish` ships it to the server (per-user install by default).")
|
|
4101
4434
|
);
|
|
4102
4435
|
});
|
|
4103
4436
|
function fullFiles(ctx) {
|
|
@@ -4161,7 +4494,7 @@ function packageJson(ctx, headless) {
|
|
|
4161
4494
|
typecheck: "tsc --noEmit"
|
|
4162
4495
|
};
|
|
4163
4496
|
const deps = {
|
|
4164
|
-
"botapp-sdk": "^0.1.
|
|
4497
|
+
"botapp-sdk": "^0.1.1",
|
|
4165
4498
|
ws: "^8.18.0"
|
|
4166
4499
|
};
|
|
4167
4500
|
const devDeps = {
|
|
@@ -4323,7 +4656,7 @@ function apiEntryTs(ctx, headless) {
|
|
|
4323
4656
|
|
|
4324
4657
|
ctx.serveStatic('./dist/public')
|
|
4325
4658
|
`;
|
|
4326
|
-
return `import { BotApp } from 'botapp-sdk'
|
|
4659
|
+
return `import { BotApp, runHosted } from 'botapp-sdk'
|
|
4327
4660
|
|
|
4328
4661
|
const app = new BotApp({
|
|
4329
4662
|
name: '${ctx.name}',
|
|
@@ -4345,7 +4678,16 @@ const app = new BotApp({
|
|
|
4345
4678
|
${widget} },
|
|
4346
4679
|
})
|
|
4347
4680
|
|
|
4348
|
-
|
|
4681
|
+
export default app
|
|
4682
|
+
|
|
4683
|
+
// Hosted-tier bridge: only connects when launched with BOTAPP_SERVER +
|
|
4684
|
+
// BOTAPP_APP_TOKEN (set by \`bot simulate\` and the platform-spawned runner).
|
|
4685
|
+
// In every other context (tests, in-process discovery) the bridge stays
|
|
4686
|
+
// dormant. Use globalThis.process so this typechecks without @types/node.
|
|
4687
|
+
const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env ?? {}
|
|
4688
|
+
if (env.BOTAPP_SERVER && env.BOTAPP_APP_TOKEN) {
|
|
4689
|
+
await runHosted(app)
|
|
4690
|
+
}
|
|
4349
4691
|
`;
|
|
4350
4692
|
}
|
|
4351
4693
|
function srcMainTsx(_ctx) {
|
|
@@ -4386,32 +4728,53 @@ export function App() {
|
|
|
4386
4728
|
}
|
|
4387
4729
|
function srcApiTs() {
|
|
4388
4730
|
return `// Tiny client for calling app routes/commands from the browser.
|
|
4389
|
-
//
|
|
4390
|
-
//
|
|
4731
|
+
// Two ways an app frontend reaches its backend on a botapp server:
|
|
4732
|
+
//
|
|
4733
|
+
// 1. Path-prefixed: /apps/<name>/api/commands/<cmd> (on bare apex)
|
|
4734
|
+
// 2. Subdomain: /api/apps/<name>/commands/<cmd> (on app subdomain)
|
|
4735
|
+
//
|
|
4736
|
+
// The subdomain dispatcher in the server leaves /api/* unrewritten on
|
|
4737
|
+
// subdomain hosts, so the frontend has to construct the absolute
|
|
4738
|
+
// /api/apps/<name>/... URL itself. We resolve <name> three ways and
|
|
4739
|
+
// take the first that works:
|
|
4740
|
+
// \u2022 from a /apps/<name>/ path prefix (host is the apex)
|
|
4741
|
+
// \u2022 from the first DNS label (host is <name>.<domain>)
|
|
4742
|
+
// \u2022 fallback: skip the /api/apps/<name> prefix (single-app local dev)
|
|
4743
|
+
|
|
4744
|
+
function resolveAppName(): string | null {
|
|
4745
|
+
const m = location.pathname.match(/^\\/apps\\/([^/]+)\\//)
|
|
4746
|
+
if (m) return m[1]
|
|
4747
|
+
const host = location.hostname
|
|
4748
|
+
const first = host.split('.')[0]
|
|
4749
|
+
if (first && first !== 'www' && host.split('.').length >= 2) return first
|
|
4750
|
+
return null
|
|
4751
|
+
}
|
|
4391
4752
|
|
|
4392
|
-
const
|
|
4393
|
-
|
|
4394
|
-
return m ? m[0] : '/'
|
|
4395
|
-
})()
|
|
4753
|
+
const APP_NAME = resolveAppName()
|
|
4754
|
+
const API_BASE = APP_NAME ? \`/api/apps/\${APP_NAME}\` : '/api'
|
|
4396
4755
|
|
|
4397
|
-
|
|
4398
|
-
const r = await fetch(\`\${
|
|
4756
|
+
async function call(kind: 'commands' | 'actions', name: string, params: Record<string, unknown>): Promise<unknown> {
|
|
4757
|
+
const r = await fetch(\`\${API_BASE}/\${kind}/\${encodeURIComponent(name)}\`, {
|
|
4399
4758
|
method: 'POST',
|
|
4400
4759
|
headers: { 'Content-Type': 'application/json' },
|
|
4401
4760
|
body: JSON.stringify(params),
|
|
4402
4761
|
})
|
|
4403
4762
|
if (!r.ok) throw new Error(await r.text())
|
|
4404
|
-
|
|
4763
|
+
// The /api/apps/<name>/... handler wraps responses as
|
|
4764
|
+
// \`{ status: 'success', result }\` or \`{ status: 'error', error }\`.
|
|
4765
|
+
// Unwrap so callers see the bare result; throw on error.
|
|
4766
|
+
const json = (await r.json()) as { status?: string; result?: unknown; error?: string }
|
|
4767
|
+
if (json.status === 'error') throw new Error(json.error ?? 'unknown error')
|
|
4768
|
+
if (json.status === 'success') return json.result
|
|
4769
|
+
return json
|
|
4770
|
+
}
|
|
4771
|
+
|
|
4772
|
+
export async function callCommand(name: string, params: Record<string, unknown> = {}) {
|
|
4773
|
+
return call('commands', name, params)
|
|
4405
4774
|
}
|
|
4406
4775
|
|
|
4407
4776
|
export async function callAction(name: string, params: Record<string, unknown> = {}) {
|
|
4408
|
-
|
|
4409
|
-
method: 'POST',
|
|
4410
|
-
headers: { 'Content-Type': 'application/json' },
|
|
4411
|
-
body: JSON.stringify(params),
|
|
4412
|
-
})
|
|
4413
|
-
if (!r.ok) throw new Error(await r.text())
|
|
4414
|
-
return r.json()
|
|
4777
|
+
return call('actions', name, params)
|
|
4415
4778
|
}
|
|
4416
4779
|
`;
|
|
4417
4780
|
}
|
|
@@ -4477,38 +4840,38 @@ function dirname2(p) {
|
|
|
4477
4840
|
}
|
|
4478
4841
|
|
|
4479
4842
|
// src/commands/publish.ts
|
|
4480
|
-
import { Command as
|
|
4843
|
+
import { Command as Command23 } from "commander";
|
|
4481
4844
|
import { resolve as resolve10, join as join13, relative as relative2 } from "path";
|
|
4482
4845
|
import { existsSync as existsSync15, readFileSync as readFileSync10, statSync as statSync4, readdirSync as readdirSync2 } from "fs";
|
|
4483
4846
|
import { createGzip } from "zlib";
|
|
4484
4847
|
import { spawn as spawn9 } from "child_process";
|
|
4485
|
-
import
|
|
4486
|
-
var publishCommand = new
|
|
4848
|
+
import pc24 from "picocolors";
|
|
4849
|
+
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) => {
|
|
4487
4850
|
const absPath = resolve10(appPath);
|
|
4488
4851
|
const manifestPath = join13(absPath, "botapp.app.json");
|
|
4489
4852
|
if (!existsSync15(manifestPath)) {
|
|
4490
|
-
console.error(
|
|
4853
|
+
console.error(pc24.red(`No botapp.app.json found in ${absPath}`));
|
|
4491
4854
|
process.exitCode = 1;
|
|
4492
4855
|
return;
|
|
4493
4856
|
}
|
|
4494
4857
|
const manifest = JSON.parse(readFileSync10(manifestPath, "utf8"));
|
|
4495
4858
|
if (!manifest.name) {
|
|
4496
|
-
console.error(
|
|
4859
|
+
console.error(pc24.red('manifest missing "name"'));
|
|
4497
4860
|
process.exitCode = 1;
|
|
4498
4861
|
return;
|
|
4499
4862
|
}
|
|
4500
4863
|
const server = resolveServerUrl(opts.server);
|
|
4501
4864
|
const token = resolveToken(opts.token);
|
|
4502
4865
|
if (!token) {
|
|
4503
|
-
console.error(
|
|
4866
|
+
console.error(pc24.red("not logged in: run `bot login` or pass --token"));
|
|
4504
4867
|
process.exitCode = 1;
|
|
4505
4868
|
return;
|
|
4506
4869
|
}
|
|
4507
4870
|
if (opts.build !== false) {
|
|
4508
|
-
console.log(
|
|
4871
|
+
console.log(pc24.dim("building app..."));
|
|
4509
4872
|
const ok = await runBuild(absPath);
|
|
4510
4873
|
if (!ok) {
|
|
4511
|
-
console.error(
|
|
4874
|
+
console.error(pc24.red("build failed; aborting"));
|
|
4512
4875
|
process.exitCode = 1;
|
|
4513
4876
|
return;
|
|
4514
4877
|
}
|
|
@@ -4516,14 +4879,14 @@ var publishCommand = new Command22("publish").description("Build, pack, and uplo
|
|
|
4516
4879
|
const bundleDir = opts.bundleDir ? resolve10(absPath, opts.bundleDir) : pickBundleDir(absPath);
|
|
4517
4880
|
let bundleB64;
|
|
4518
4881
|
if (bundleDir && existsSync15(bundleDir)) {
|
|
4519
|
-
console.log(
|
|
4882
|
+
console.log(pc24.dim(`packing ${relative2(absPath, bundleDir)}/ \u2192 tar.gz...`));
|
|
4520
4883
|
const bytes = await packDirToTarGz(bundleDir);
|
|
4521
4884
|
bundleB64 = bytes.toString("base64");
|
|
4522
|
-
console.log(
|
|
4885
|
+
console.log(pc24.dim(` bundle: ${(bytes.length / 1024).toFixed(1)} KiB`));
|
|
4523
4886
|
} else {
|
|
4524
|
-
console.log(
|
|
4887
|
+
console.log(pc24.dim("no frontend bundle to upload (headless app or no dist/public)"));
|
|
4525
4888
|
}
|
|
4526
|
-
console.log(
|
|
4889
|
+
console.log(pc24.dim(`uploading to ${server}/api/apps/upload (${opts.public ? "public" : "private"})...`));
|
|
4527
4890
|
const res = await fetch(`${server}/api/apps/upload`, {
|
|
4528
4891
|
method: "POST",
|
|
4529
4892
|
headers: authHeaders(token),
|
|
@@ -4535,23 +4898,23 @@ var publishCommand = new Command22("publish").description("Build, pack, and uplo
|
|
|
4535
4898
|
});
|
|
4536
4899
|
if (!res.ok) {
|
|
4537
4900
|
const body = await res.text().catch(() => "");
|
|
4538
|
-
console.error(
|
|
4901
|
+
console.error(pc24.red(`upload failed (${res.status}): ${body}`));
|
|
4539
4902
|
process.exitCode = 1;
|
|
4540
4903
|
return;
|
|
4541
4904
|
}
|
|
4542
4905
|
const data = await res.json();
|
|
4543
4906
|
if (!data.ok) {
|
|
4544
|
-
console.error(
|
|
4907
|
+
console.error(pc24.red(`upload rejected: ${data.error ?? "unknown"}`));
|
|
4545
4908
|
process.exitCode = 1;
|
|
4546
4909
|
return;
|
|
4547
4910
|
}
|
|
4548
|
-
console.log(
|
|
4911
|
+
console.log(pc24.green("\u2713"), data.message ?? "uploaded");
|
|
4549
4912
|
if (data.install) {
|
|
4550
|
-
console.log(
|
|
4551
|
-
console.log(
|
|
4552
|
-
console.log(
|
|
4913
|
+
console.log(pc24.dim(` id: ${data.install.id}`));
|
|
4914
|
+
console.log(pc24.dim(` version: ${data.install.version}`));
|
|
4915
|
+
console.log(pc24.dim(` visibility: ${data.install.visibility}`));
|
|
4553
4916
|
if (data.install.reviewStatus !== "none") {
|
|
4554
|
-
console.log(
|
|
4917
|
+
console.log(pc24.dim(` review: ${data.install.reviewStatus}`));
|
|
4555
4918
|
}
|
|
4556
4919
|
}
|
|
4557
4920
|
});
|
|
@@ -4626,9 +4989,9 @@ function gzip(input2) {
|
|
|
4626
4989
|
}
|
|
4627
4990
|
|
|
4628
4991
|
// src/commands/review.ts
|
|
4629
|
-
import { Command as
|
|
4630
|
-
import
|
|
4631
|
-
var reviewCommand = new
|
|
4992
|
+
import { Command as Command24 } from "commander";
|
|
4993
|
+
import pc25 from "picocolors";
|
|
4994
|
+
var reviewCommand = new Command24("review").description("Admin: review queue for public-visibility uploads");
|
|
4632
4995
|
reviewCommand.command("list").description("List pending public-visibility uploads").option("-s, --server <url>", "Server URL").option("-t, --token <token>", "Auth token").action(async (opts) => {
|
|
4633
4996
|
const server = resolveServerUrl(opts.server);
|
|
4634
4997
|
const token = resolveToken(opts.token);
|
|
@@ -4640,15 +5003,15 @@ reviewCommand.command("list").description("List pending public-visibility upload
|
|
|
4640
5003
|
}
|
|
4641
5004
|
const data = await res.json();
|
|
4642
5005
|
if (!data.pending?.length) {
|
|
4643
|
-
console.log(
|
|
5006
|
+
console.log(pc25.dim("queue empty"));
|
|
4644
5007
|
return;
|
|
4645
5008
|
}
|
|
4646
5009
|
for (const i of data.pending) {
|
|
4647
|
-
console.log(
|
|
4648
|
-
console.log(
|
|
4649
|
-
console.log(
|
|
5010
|
+
console.log(pc25.bold(i.id), pc25.cyan(i.appName), pc25.dim(`v${i.version}`));
|
|
5011
|
+
console.log(pc25.dim(` uploader: ${i.ownerUserId ?? "(server-wide)"}`));
|
|
5012
|
+
console.log(pc25.dim(` uploaded: ${i.uploadedAt}`));
|
|
4650
5013
|
const desc = i.manifest?.description;
|
|
4651
|
-
if (desc) console.log(
|
|
5014
|
+
if (desc) console.log(pc25.dim(` description: ${desc}`));
|
|
4652
5015
|
console.log();
|
|
4653
5016
|
}
|
|
4654
5017
|
});
|
|
@@ -4669,17 +5032,17 @@ async function decide(id, decision, opts) {
|
|
|
4669
5032
|
}
|
|
4670
5033
|
const data = await res.json();
|
|
4671
5034
|
if (!data.ok) return die(`server rejected: ${data.error ?? "unknown"}`);
|
|
4672
|
-
console.log(
|
|
4673
|
-
if (data.install?.reviewNotes) console.log(
|
|
5035
|
+
console.log(pc25.green("\u2713"), `${decision}d`, data.install?.appName, pc25.dim(`(${data.install?.id})`));
|
|
5036
|
+
if (data.install?.reviewNotes) console.log(pc25.dim(` notes: ${data.install.reviewNotes}`));
|
|
4674
5037
|
}
|
|
4675
5038
|
function die(msg) {
|
|
4676
|
-
console.error(
|
|
5039
|
+
console.error(pc25.red(msg));
|
|
4677
5040
|
process.exitCode = 1;
|
|
4678
5041
|
}
|
|
4679
5042
|
|
|
4680
5043
|
// src/index.ts
|
|
4681
|
-
var version = "0.2.
|
|
4682
|
-
var program = new
|
|
5044
|
+
var version = "0.2.8";
|
|
5045
|
+
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");
|
|
4683
5046
|
program.addCommand(launchCommand);
|
|
4684
5047
|
program.addCommand(runCommand);
|
|
4685
5048
|
program.addCommand(appsCommand);
|
|
@@ -4690,6 +5053,7 @@ program.addCommand(loginCommand);
|
|
|
4690
5053
|
program.addCommand(agentCommand);
|
|
4691
5054
|
program.addCommand(pairingCommand);
|
|
4692
5055
|
program.addCommand(daemonCommand);
|
|
5056
|
+
program.addCommand(doctorCommand);
|
|
4693
5057
|
program.addCommand(initCommand);
|
|
4694
5058
|
program.addCommand(installCommand);
|
|
4695
5059
|
program.addCommand(uninstallCommand);
|
|
@@ -4741,29 +5105,29 @@ To discover what params a command accepts:
|
|
|
4741
5105
|
program.on("command:*", (operands) => {
|
|
4742
5106
|
const first = operands[0];
|
|
4743
5107
|
const known = program.commands.filter((c) => !c._hidden).map((c) => c.name());
|
|
4744
|
-
const topLevelHint = `Run ${
|
|
5108
|
+
const topLevelHint = `Run ${pc26.cyan("bot --help")} for the list of top-level commands.`;
|
|
4745
5109
|
const argv = process.argv.slice(2);
|
|
4746
5110
|
const firstIdx = argv.indexOf(first);
|
|
4747
5111
|
const tail = firstIdx >= 0 ? argv.slice(firstIdx + 1) : [];
|
|
4748
5112
|
if (tail.length > 0) {
|
|
4749
5113
|
const suggested = `bot run ${first} ${tail.join(" ")}`;
|
|
4750
5114
|
console.error(
|
|
4751
|
-
|
|
5115
|
+
pc26.red(`error: unknown command '${first}'`) + `
|
|
4752
5116
|
|
|
4753
|
-
App commands go through ${
|
|
5117
|
+
App commands go through ${pc26.bold("bot run")}. Did you mean:
|
|
4754
5118
|
|
|
4755
|
-
${
|
|
5119
|
+
${pc26.cyan(suggested)}
|
|
4756
5120
|
|
|
4757
5121
|
${topLevelHint}`
|
|
4758
5122
|
);
|
|
4759
5123
|
process.exit(1);
|
|
4760
5124
|
}
|
|
4761
5125
|
console.error(
|
|
4762
|
-
|
|
5126
|
+
pc26.red(`error: unknown command '${first}'`) + `
|
|
4763
5127
|
|
|
4764
5128
|
If '${first}' is an app name, invoke one of its commands with:
|
|
4765
|
-
${
|
|
4766
|
-
${
|
|
5129
|
+
${pc26.cyan(`bot run ${first} <command> [--key value ...]`)}
|
|
5130
|
+
${pc26.cyan("bot apps --json")} ${pc26.dim("(to see what commands exist)")}
|
|
4767
5131
|
|
|
4768
5132
|
Top-level commands: ${known.join(", ")}
|
|
4769
5133
|
${topLevelHint}`
|