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/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 Command24 } from "commander";
10
- import pc25 from "picocolors";
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 === "openclaw" || job.agent.kind === "kimiclaw") {
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
- throw new Error(stderr.trim() || `Claude Code exited with code ${code}`);
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.5"
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 Command19 } from "commander";
3850
- import pc20 from "picocolors";
4182
+ import { Command as Command20 } from "commander";
4183
+ import pc21 from "picocolors";
3851
4184
  var PACKAGE_NAME = "botapp-cli";
3852
- var updateCommand = new Command19("update").description("Update the `bot` CLI itself to the latest published version").option(
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
- pc20.green(
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
- pc20.dim(
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
- pc20.red(
4206
+ pc21.red(
3874
4207
  "Couldn't detect how `bot` was installed. Pick one of these manually:"
3875
4208
  )
3876
4209
  );
3877
- console.log(pc20.cyan(" npm i -g botapp-cli@latest"));
3878
- console.log(pc20.cyan(" pnpm add -g botapp-cli@latest"));
3879
- console.log(pc20.cyan(" yarn global add botapp-cli@latest"));
3880
- console.log(pc20.cyan(" brew upgrade botapp-cli"));
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(pc20.dim(`$ ${command} ${args.join(" ")}`));
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(pc20.red(`Failed to spawn ${command}: ${err.message}`));
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(pc20.red(`Update failed (exit ${code}).`));
4229
+ console.error(pc21.red(`Update failed (exit ${code}).`));
3897
4230
  process.exitCode = code ?? 1;
3898
4231
  return;
3899
4232
  }
3900
- console.log(pc20.green("botapp-cli updated. Run `bot --version` to confirm."));
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 Command20 } from "commander";
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 pc21 from "picocolors";
3940
- 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) => {
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(pc21.red(`Path not found: ${absPath}`));
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(pc21.red(`No botapp.app.json found in ${absPath}`));
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(pc21.red('manifest missing "name"'));
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(pc21.red("not logged in: run `bot login` or pass --token"));
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(pc21.dim(`requesting dev token for "${manifest.name}" from ${httpServer}...`));
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(pc21.green("\u2713"), `dev token issued (lifetime ${opts.lifetime} min)`);
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(pc21.red(`entry not found: ${entryPath}`));
3979
- console.error(pc21.dim(" run `pnpm build` (or your build script) first."));
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(pc21.dim(`spawning ${entryPath} ...`));
3984
- console.log(pc21.dim(` BOTAPP_SERVER=${wsServer}`));
3985
- console.log(pc21.dim(` BOTAPP_APP_NAME=${manifest.name}`));
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
- pc21.cyan(
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(pc21.dim(`
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(pc21.dim(`child exited (${reason})`));
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 Command21 } from "commander";
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 pc22 from "picocolors";
4054
- 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) => {
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(pc22.red("Invalid name. Use lowercase letters, digits, dashes; must start with a letter."));
4057
- console.error(pc22.dim(" e.g. my-app, todo-tracker, gomoku-2"));
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(pc22.red(`Target directory exists and is not empty: ${targetDir}`));
4072
- console.error(pc22.dim(" pass --force to overwrite, or pick a different --dir"));
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(pc22.green("\u2713"), `Scaffolded ${ctx.headless ? "headless " : ""}app at`, pc22.cyan(targetDir));
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(pc22.dim(` cd ${targetDir.replace(process.cwd() + "/", "")}`));
4093
- console.log(pc22.dim(" pnpm install # or npm install"));
4094
- if (!ctx.headless) console.log(pc22.dim(" pnpm build # builds api/ + frontend (dist/)"));
4095
- else console.log(pc22.dim(" pnpm build # builds api/ to dist/api.js"));
4096
- console.log(pc22.dim(` bot login --server <your-botapp-server>`));
4097
- console.log(pc22.dim(` bot simulate # dev-tunnel into the server, hot-reload as you build`));
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
- pc22.dim("Once it works, `bot publish` ships it to the server (per-user install by default).")
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.0",
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
- await app.start()
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
- // Routes resolve as /apps/<name>/* on the platform; the platform forwards
4390
- // each request to your app's WebSocket session.
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 APP_BASE = ((): string => {
4393
- const m = location.pathname.match(/^\\/apps\\/[^/]+\\//)
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
- export async function callCommand(name: string, params: Record<string, unknown> = {}) {
4398
- const r = await fetch(\`\${APP_BASE}api/commands/\${encodeURIComponent(name)}\`, {
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
- return r.json()
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
- const r = await fetch(\`\${APP_BASE}api/actions/\${encodeURIComponent(name)}\`, {
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 Command22 } from "commander";
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 pc23 from "picocolors";
4486
- 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) => {
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(pc23.red(`No botapp.app.json found in ${absPath}`));
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(pc23.red('manifest missing "name"'));
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(pc23.red("not logged in: run `bot login` or pass --token"));
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(pc23.dim("building app..."));
4871
+ console.log(pc24.dim("building app..."));
4509
4872
  const ok = await runBuild(absPath);
4510
4873
  if (!ok) {
4511
- console.error(pc23.red("build failed; aborting"));
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(pc23.dim(`packing ${relative2(absPath, bundleDir)}/ \u2192 tar.gz...`));
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(pc23.dim(` bundle: ${(bytes.length / 1024).toFixed(1)} KiB`));
4885
+ console.log(pc24.dim(` bundle: ${(bytes.length / 1024).toFixed(1)} KiB`));
4523
4886
  } else {
4524
- console.log(pc23.dim("no frontend bundle to upload (headless app or no dist/public)"));
4887
+ console.log(pc24.dim("no frontend bundle to upload (headless app or no dist/public)"));
4525
4888
  }
4526
- console.log(pc23.dim(`uploading to ${server}/api/apps/upload (${opts.public ? "public" : "private"})...`));
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(pc23.red(`upload failed (${res.status}): ${body}`));
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(pc23.red(`upload rejected: ${data.error ?? "unknown"}`));
4907
+ console.error(pc24.red(`upload rejected: ${data.error ?? "unknown"}`));
4545
4908
  process.exitCode = 1;
4546
4909
  return;
4547
4910
  }
4548
- console.log(pc23.green("\u2713"), data.message ?? "uploaded");
4911
+ console.log(pc24.green("\u2713"), data.message ?? "uploaded");
4549
4912
  if (data.install) {
4550
- console.log(pc23.dim(` id: ${data.install.id}`));
4551
- console.log(pc23.dim(` version: ${data.install.version}`));
4552
- console.log(pc23.dim(` visibility: ${data.install.visibility}`));
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(pc23.dim(` review: ${data.install.reviewStatus}`));
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 Command23 } from "commander";
4630
- import pc24 from "picocolors";
4631
- var reviewCommand = new Command23("review").description("Admin: review queue for public-visibility uploads");
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(pc24.dim("queue empty"));
5006
+ console.log(pc25.dim("queue empty"));
4644
5007
  return;
4645
5008
  }
4646
5009
  for (const i of data.pending) {
4647
- console.log(pc24.bold(i.id), pc24.cyan(i.appName), pc24.dim(`v${i.version}`));
4648
- console.log(pc24.dim(` uploader: ${i.ownerUserId ?? "(server-wide)"}`));
4649
- console.log(pc24.dim(` uploaded: ${i.uploadedAt}`));
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(pc24.dim(` description: ${desc}`));
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(pc24.green("\u2713"), `${decision}d`, data.install?.appName, pc24.dim(`(${data.install?.id})`));
4673
- if (data.install?.reviewNotes) console.log(pc24.dim(` notes: ${data.install.reviewNotes}`));
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(pc24.red(msg));
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.5";
4682
- 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");
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 ${pc25.cyan("bot --help")} for the list of top-level commands.`;
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
- pc25.red(`error: unknown command '${first}'`) + `
5115
+ pc26.red(`error: unknown command '${first}'`) + `
4752
5116
 
4753
- App commands go through ${pc25.bold("bot run")}. Did you mean:
5117
+ App commands go through ${pc26.bold("bot run")}. Did you mean:
4754
5118
 
4755
- ${pc25.cyan(suggested)}
5119
+ ${pc26.cyan(suggested)}
4756
5120
 
4757
5121
  ${topLevelHint}`
4758
5122
  );
4759
5123
  process.exit(1);
4760
5124
  }
4761
5125
  console.error(
4762
- pc25.red(`error: unknown command '${first}'`) + `
5126
+ pc26.red(`error: unknown command '${first}'`) + `
4763
5127
 
4764
5128
  If '${first}' is an app name, invoke one of its commands with:
4765
- ${pc25.cyan(`bot run ${first} <command> [--key value ...]`)}
4766
- ${pc25.cyan("bot apps --json")} ${pc25.dim("(to see what commands exist)")}
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}`