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 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 Command24 } from "commander";
11
- import pc25 from "picocolors";
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 === "openclaw" || job.agent.kind === "kimiclaw") {
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
- throw new Error(stderr.trim() || `Claude Code exited with code ${code}`);
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.5"
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 Command19 } from "commander";
3839
- import pc20 from "picocolors";
4180
+ import { Command as Command20 } from "commander";
4181
+ import pc21 from "picocolors";
3840
4182
  var PACKAGE_NAME = "botapp-cli";
3841
- var updateCommand = new Command19("update").description("Update the `bot` CLI itself to the latest published version").option(
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
- pc20.green(
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
- pc20.dim(
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
- pc20.red(
4204
+ pc21.red(
3863
4205
  "Couldn't detect how `bot` was installed. Pick one of these manually:"
3864
4206
  )
3865
4207
  );
3866
- console.log(pc20.cyan(" npm i -g botapp-cli@latest"));
3867
- console.log(pc20.cyan(" pnpm add -g botapp-cli@latest"));
3868
- console.log(pc20.cyan(" yarn global add botapp-cli@latest"));
3869
- console.log(pc20.cyan(" brew upgrade botapp-cli"));
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(pc20.dim(`$ ${command} ${args.join(" ")}`));
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(pc20.red(`Failed to spawn ${command}: ${err.message}`));
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(pc20.red(`Update failed (exit ${code}).`));
4227
+ console.error(pc21.red(`Update failed (exit ${code}).`));
3886
4228
  process.exitCode = code ?? 1;
3887
4229
  return;
3888
4230
  }
3889
- console.log(pc20.green("botapp-cli updated. Run `bot --version` to confirm."));
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 Command20 } from "commander";
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 pc21 from "picocolors";
3929
- var simulateCommand = new Command20("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) => {
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(pc21.red(`Path not found: ${absPath}`));
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(pc21.red(`No botapp.app.json found in ${absPath}`));
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(pc21.red('manifest missing "name"'));
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(pc21.red("not logged in: run `bot login` or pass --token"));
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(pc21.dim(`requesting dev token for "${manifest.name}" from ${httpServer}...`));
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(pc21.green("\u2713"), `dev token issued (lifetime ${opts.lifetime} min)`);
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(pc21.red(`entry not found: ${entryPath}`));
3968
- console.error(pc21.dim(" run `pnpm build` (or your build script) first."));
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(pc21.dim(`spawning ${entryPath} ...`));
3973
- console.log(pc21.dim(` BOTAPP_SERVER=${wsServer}`));
3974
- console.log(pc21.dim(` BOTAPP_APP_NAME=${manifest.name}`));
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
- pc21.cyan(
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(pc21.dim(`
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(pc21.dim(`child exited (${reason})`));
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 Command21 } from "commander";
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 pc22 from "picocolors";
4043
- var initCommand = new Command21("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) => {
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(pc22.red("Invalid name. Use lowercase letters, digits, dashes; must start with a letter."));
4046
- console.error(pc22.dim(" e.g. my-app, todo-tracker, gomoku-2"));
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(pc22.red(`Target directory exists and is not empty: ${targetDir}`));
4061
- console.error(pc22.dim(" pass --force to overwrite, or pick a different --dir"));
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(pc22.green("\u2713"), `Scaffolded ${ctx.headless ? "headless " : ""}app at`, pc22.cyan(targetDir));
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(pc22.dim(` cd ${targetDir.replace(process.cwd() + "/", "")}`));
4082
- console.log(pc22.dim(" pnpm install # or npm install"));
4083
- if (!ctx.headless) console.log(pc22.dim(" pnpm build # builds api/ + frontend (dist/)"));
4084
- else console.log(pc22.dim(" pnpm build # builds api/ to dist/api.js"));
4085
- console.log(pc22.dim(` bot login --server <your-botapp-server>`));
4086
- console.log(pc22.dim(` bot simulate # dev-tunnel into the server, hot-reload as you build`));
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
- pc22.dim("Once it works, `bot publish` ships it to the server (per-user install by default).")
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.0",
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
- await app.start()
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
- // Routes resolve as /apps/<name>/* on the platform; the platform forwards
4379
- // each request to your app's WebSocket session.
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
- const APP_BASE = ((): string => {
4382
- const m = location.pathname.match(/^\\/apps\\/[^/]+\\//)
4383
- return m ? m[0] : '/'
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
- export async function callCommand(name: string, params: Record<string, unknown> = {}) {
4387
- const r = await fetch(\`\${APP_BASE}api/commands/\${encodeURIComponent(name)}\`, {
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
- return r.json()
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
- const r = await fetch(\`\${APP_BASE}api/actions/\${encodeURIComponent(name)}\`, {
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 Command22 } from "commander";
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 pc23 from "picocolors";
4475
- var publishCommand = new Command22("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) => {
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(pc23.red(`No botapp.app.json found in ${absPath}`));
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(pc23.red('manifest missing "name"'));
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(pc23.red("not logged in: run `bot login` or pass --token"));
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(pc23.dim("building app..."));
4869
+ console.log(pc24.dim("building app..."));
4498
4870
  const ok = await runBuild(absPath);
4499
4871
  if (!ok) {
4500
- console.error(pc23.red("build failed; aborting"));
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(pc23.dim(`packing ${relative2(absPath, bundleDir)}/ \u2192 tar.gz...`));
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(pc23.dim(` bundle: ${(bytes.length / 1024).toFixed(1)} KiB`));
4883
+ console.log(pc24.dim(` bundle: ${(bytes.length / 1024).toFixed(1)} KiB`));
4512
4884
  } else {
4513
- console.log(pc23.dim("no frontend bundle to upload (headless app or no dist/public)"));
4885
+ console.log(pc24.dim("no frontend bundle to upload (headless app or no dist/public)"));
4514
4886
  }
4515
- console.log(pc23.dim(`uploading to ${server}/api/apps/upload (${opts.public ? "public" : "private"})...`));
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(pc23.red(`upload failed (${res.status}): ${body}`));
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(pc23.red(`upload rejected: ${data.error ?? "unknown"}`));
4905
+ console.error(pc24.red(`upload rejected: ${data.error ?? "unknown"}`));
4534
4906
  process.exitCode = 1;
4535
4907
  return;
4536
4908
  }
4537
- console.log(pc23.green("\u2713"), data.message ?? "uploaded");
4909
+ console.log(pc24.green("\u2713"), data.message ?? "uploaded");
4538
4910
  if (data.install) {
4539
- console.log(pc23.dim(` id: ${data.install.id}`));
4540
- console.log(pc23.dim(` version: ${data.install.version}`));
4541
- console.log(pc23.dim(` visibility: ${data.install.visibility}`));
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(pc23.dim(` review: ${data.install.reviewStatus}`));
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 Command23 } from "commander";
4619
- import pc24 from "picocolors";
4620
- var reviewCommand = new Command23("review").description("Admin: review queue for public-visibility uploads");
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(pc24.dim("queue empty"));
5004
+ console.log(pc25.dim("queue empty"));
4633
5005
  return;
4634
5006
  }
4635
5007
  for (const i of data.pending) {
4636
- console.log(pc24.bold(i.id), pc24.cyan(i.appName), pc24.dim(`v${i.version}`));
4637
- console.log(pc24.dim(` uploader: ${i.ownerUserId ?? "(server-wide)"}`));
4638
- console.log(pc24.dim(` uploaded: ${i.uploadedAt}`));
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(pc24.dim(` description: ${desc}`));
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(pc24.green("\u2713"), `${decision}d`, data.install?.appName, pc24.dim(`(${data.install?.id})`));
4662
- if (data.install?.reviewNotes) console.log(pc24.dim(` notes: ${data.install.reviewNotes}`));
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(pc24.red(msg));
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.5";
4671
- var program = new Command24().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");
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 ${pc25.cyan("bot --help")} for the list of top-level commands.`;
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
- pc25.red(`error: unknown command '${first}'`) + `
5113
+ pc26.red(`error: unknown command '${first}'`) + `
4741
5114
 
4742
- App commands go through ${pc25.bold("bot run")}. Did you mean:
5115
+ App commands go through ${pc26.bold("bot run")}. Did you mean:
4743
5116
 
4744
- ${pc25.cyan(suggested)}
5117
+ ${pc26.cyan(suggested)}
4745
5118
 
4746
5119
  ${topLevelHint}`
4747
5120
  );
4748
5121
  process.exit(1);
4749
5122
  }
4750
5123
  console.error(
4751
- pc25.red(`error: unknown command '${first}'`) + `
5124
+ pc26.red(`error: unknown command '${first}'`) + `
4752
5125
 
4753
5126
  If '${first}' is an app name, invoke one of its commands with:
4754
- ${pc25.cyan(`bot run ${first} <command> [--key value ...]`)}
4755
- ${pc25.cyan("bot apps --json")} ${pc25.dim("(to see what commands exist)")}
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}`