botapp-cli 0.2.4 → 0.2.7

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
@@ -1,6 +1,13 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
1
8
  // src/index.ts
2
- import { Command as Command20 } from "commander";
3
- import pc21 from "picocolors";
9
+ import { Command as Command25 } from "commander";
10
+ import pc26 from "picocolors";
4
11
 
5
12
  // src/commands/server.ts
6
13
  import { Command } from "commander";
@@ -151,11 +158,11 @@ function findServerEntry() {
151
158
  }
152
159
 
153
160
  // src/commands/daemon.ts
154
- import { spawn as spawn2 } from "child_process";
155
- import { randomUUID } from "crypto";
156
- import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync3, statSync } from "fs";
157
- import { homedir as homedir3 } from "os";
158
- import { join as join3, resolve as resolve3 } from "path";
161
+ import { spawn as spawn3 } from "child_process";
162
+ import { randomUUID as randomUUID2 } from "crypto";
163
+ import { existsSync as existsSync5, readdirSync, readFileSync as readFileSync3, statSync as statSync3 } from "fs";
164
+ import { homedir as homedir5 } from "os";
165
+ import { join as join5, resolve as resolve4 } from "path";
159
166
  import { createInterface as createInterface2 } from "readline";
160
167
  import { Command as Command3 } from "commander";
161
168
  import { WebSocket } from "ws";
@@ -187,7 +194,8 @@ function writeYaml(profiles) {
187
194
  server: p.server,
188
195
  daemonId: p.daemonId,
189
196
  daemonName: p.daemonName,
190
- token: p.token
197
+ token: p.token,
198
+ ...p.userEmail ? { userEmail: p.userEmail } : {}
191
199
  };
192
200
  }
193
201
  writeFileSync2(DAEMON_FILE, stringify2({ profiles: map }), "utf-8");
@@ -227,7 +235,8 @@ function loadDaemonProfiles() {
227
235
  server: normalizeServer(value.server),
228
236
  daemonId: value.daemonId,
229
237
  daemonName: value.daemonName ?? value.daemonId,
230
- token: value.token
238
+ token: value.token,
239
+ userEmail: value.userEmail
231
240
  });
232
241
  }
233
242
  }
@@ -266,7 +275,8 @@ function saveDaemonProfile(input2) {
266
275
  server,
267
276
  daemonId: input2.daemonId,
268
277
  daemonName: input2.daemonName ?? input2.daemonId,
269
- token: input2.token
278
+ token: input2.token,
279
+ userEmail: input2.userEmail
270
280
  };
271
281
  writeYaml([...remaining, next]);
272
282
  return next;
@@ -483,7 +493,557 @@ async function daemonSelfRequest(server, token, path, opts) {
483
493
  return data;
484
494
  }
485
495
 
496
+ // src/rpc/registry.ts
497
+ var RpcRegistry = class {
498
+ handlers = /* @__PURE__ */ new Map();
499
+ register(op, handler) {
500
+ if (this.handlers.has(op)) {
501
+ throw new Error(`RPC op already registered: ${op}`);
502
+ }
503
+ this.handlers.set(op, handler);
504
+ }
505
+ get(op) {
506
+ return this.handlers.get(op);
507
+ }
508
+ has(op) {
509
+ return this.handlers.has(op);
510
+ }
511
+ };
512
+ var InternalRpcContext = class {
513
+ constructor(appName, rpcId, ws) {
514
+ this.appName = appName;
515
+ this.rpcId = rpcId;
516
+ this.ws = ws;
517
+ }
518
+ appName;
519
+ rpcId;
520
+ ws;
521
+ cancelled = false;
522
+ inputCallback = null;
523
+ cancelCallback = null;
524
+ pushChunk(payload) {
525
+ sendJson(this.ws, {
526
+ type: "daemon_rpc_stream",
527
+ rpcId: this.rpcId,
528
+ payload
529
+ });
530
+ }
531
+ isCancelled() {
532
+ return this.cancelled;
533
+ }
534
+ onInput(callback) {
535
+ this.inputCallback = callback;
536
+ }
537
+ onCancel(callback) {
538
+ this.cancelCallback = callback;
539
+ }
540
+ cancel() {
541
+ if (this.cancelled) return;
542
+ this.cancelled = true;
543
+ const cb = this.cancelCallback;
544
+ this.cancelCallback = null;
545
+ if (cb) {
546
+ try {
547
+ cb();
548
+ } catch {
549
+ }
550
+ }
551
+ }
552
+ receiveInput(payload) {
553
+ this.inputCallback?.(payload);
554
+ }
555
+ };
556
+ var RpcDispatcher = class {
557
+ constructor(registry, ws) {
558
+ this.registry = registry;
559
+ this.ws = ws;
560
+ }
561
+ registry;
562
+ ws;
563
+ inflight = /* @__PURE__ */ new Map();
564
+ /** Handle a `daemon_rpc_request` frame. */
565
+ async dispatchRequest(frame) {
566
+ const handler = this.registry.get(frame.op);
567
+ if (!handler) {
568
+ this.sendResponse(frame.rpcId, {
569
+ ok: false,
570
+ error: `unknown daemon RPC op: ${frame.op}`
571
+ });
572
+ return;
573
+ }
574
+ const ctx = new InternalRpcContext(
575
+ frame.appName ?? "unknown",
576
+ frame.rpcId,
577
+ this.ws
578
+ );
579
+ this.inflight.set(frame.rpcId, { ctx });
580
+ try {
581
+ const result = await handler(frame.params ?? {}, ctx);
582
+ if (this.inflight.has(frame.rpcId)) {
583
+ this.sendResponse(frame.rpcId, { ok: true, result });
584
+ }
585
+ } catch (e) {
586
+ if (this.inflight.has(frame.rpcId)) {
587
+ this.sendResponse(frame.rpcId, {
588
+ ok: false,
589
+ error: e?.message ?? String(e)
590
+ });
591
+ }
592
+ } finally {
593
+ this.inflight.delete(frame.rpcId);
594
+ }
595
+ }
596
+ /** Handle a `daemon_rpc_input` frame. */
597
+ dispatchInput(frame) {
598
+ const call = this.inflight.get(frame.rpcId);
599
+ if (!call) return;
600
+ call.ctx.receiveInput(frame.payload);
601
+ }
602
+ /** Handle a `daemon_rpc_cancel` frame. */
603
+ dispatchCancel(frame) {
604
+ const call = this.inflight.get(frame.rpcId);
605
+ if (!call) return;
606
+ call.ctx.cancel();
607
+ }
608
+ /** Cancel everything (called on socket close). */
609
+ shutdown() {
610
+ for (const [, call] of this.inflight) {
611
+ call.ctx.cancel();
612
+ }
613
+ this.inflight.clear();
614
+ }
615
+ sendResponse(rpcId, body) {
616
+ sendJson(this.ws, { type: "daemon_rpc_response", rpcId, ...body });
617
+ }
618
+ };
619
+ function sendJson(ws, frame) {
620
+ if (ws.readyState !== ws.OPEN) return;
621
+ ws.send(JSON.stringify(frame));
622
+ }
623
+
624
+ // src/rpc/file-handlers.ts
625
+ import { Buffer as Buffer2 } from "buffer";
626
+ import {
627
+ promises as fsp,
628
+ realpathSync,
629
+ statSync
630
+ } from "fs";
631
+ import { homedir as homedir3 } from "os";
632
+ import { dirname, isAbsolute, join as join3, relative, resolve as resolve3, sep } from "path";
633
+ function registerFileHandlers(registry) {
634
+ registry.register("file.tree", fileTree);
635
+ registry.register("file.read", fileRead);
636
+ registry.register("file.write", fileWrite);
637
+ registry.register("file.stat", fileStat);
638
+ registry.register("file.mkdir", fileMkdir);
639
+ registry.register("file.delete", fileDelete);
640
+ registry.register("file.rename", fileRename);
641
+ registry.register("fs.browse", fsBrowse);
642
+ registry.register("fs.home", fsHome);
643
+ registry.register("fs.mkdir", fsMkdir);
644
+ }
645
+ function expandHome(p) {
646
+ if (p === "~") return homedir3();
647
+ if (p.startsWith("~/")) return join3(homedir3(), p.slice(2));
648
+ return p;
649
+ }
650
+ function resolveRoot(root) {
651
+ if (typeof root !== "string" || !root.trim()) {
652
+ throw new Error("file.*: `root` is required");
653
+ }
654
+ const expanded = expandHome(root);
655
+ if (!isAbsolute(expanded)) {
656
+ throw new Error(`file.*: \`root\` must be absolute (got "${root}")`);
657
+ }
658
+ try {
659
+ return realpathSync(expanded);
660
+ } catch {
661
+ return resolve3(expanded);
662
+ }
663
+ }
664
+ function jailedPath(root, sub) {
665
+ const canonicalRoot = resolveRoot(root);
666
+ const candidate = sub == null || sub === "" ? canonicalRoot : isAbsolute(sub) ? sub : resolve3(canonicalRoot, sub);
667
+ let realBase = candidate;
668
+ let tail = "";
669
+ while (true) {
670
+ try {
671
+ realBase = realpathSync(realBase);
672
+ break;
673
+ } catch {
674
+ const parent = dirname(realBase);
675
+ if (parent === realBase) {
676
+ realBase = candidate;
677
+ tail = "";
678
+ break;
679
+ }
680
+ tail = tail ? join3(realBase.slice(parent.length + 1), tail) : realBase.slice(parent.length + 1);
681
+ realBase = parent;
682
+ }
683
+ }
684
+ const final = tail ? join3(realBase, tail) : realBase;
685
+ const rel = relative(canonicalRoot, final);
686
+ if (rel.startsWith("..") || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
687
+ throw new Error(`file.*: path "${sub ?? ""}" escapes workspace root "${root}"`);
688
+ }
689
+ return final;
690
+ }
691
+ var DEFAULT_IGNORE = /* @__PURE__ */ new Set([
692
+ "node_modules",
693
+ ".git",
694
+ ".next",
695
+ ".turbo",
696
+ "dist",
697
+ "build",
698
+ ".venv",
699
+ "__pycache__",
700
+ ".DS_Store"
701
+ ]);
702
+ async function fileTree(params) {
703
+ const root = resolveRoot(params.root);
704
+ const start = jailedPath(params.root, params.path);
705
+ const depth = Math.max(0, params.depth ?? 1);
706
+ const ignore = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...params.ignore ?? []]);
707
+ const out = [];
708
+ async function walk2(absDir, level) {
709
+ let entries;
710
+ try {
711
+ entries = await fsp.readdir(absDir, {
712
+ withFileTypes: true,
713
+ encoding: "utf8"
714
+ });
715
+ } catch (e) {
716
+ if (e?.code === "ENOENT" || e?.code === "ENOTDIR") return;
717
+ throw e;
718
+ }
719
+ entries.sort((a, b) => {
720
+ if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
721
+ return a.name.localeCompare(b.name);
722
+ });
723
+ for (const entry of entries) {
724
+ if (ignore.has(entry.name)) continue;
725
+ const abs = join3(absDir, entry.name);
726
+ const rel = relative(root, abs);
727
+ let stat = null;
728
+ try {
729
+ stat = await fsp.stat(abs);
730
+ } catch {
731
+ continue;
732
+ }
733
+ const node = {
734
+ name: entry.name,
735
+ path: rel,
736
+ isDir: entry.isDirectory(),
737
+ size: entry.isFile() ? stat.size : void 0,
738
+ mtimeMs: stat.mtimeMs
739
+ };
740
+ out.push(node);
741
+ if (entry.isDirectory() && level < depth) {
742
+ await walk2(abs, level + 1);
743
+ }
744
+ }
745
+ }
746
+ await walk2(start, 1);
747
+ return out;
748
+ }
749
+ async function fileRead(params) {
750
+ const abs = jailedPath(params.root, params.path);
751
+ const maxBytes = params.maxBytes ?? 5 * 1024 * 1024;
752
+ const stat = await fsp.stat(abs);
753
+ if (!stat.isFile()) {
754
+ throw new Error(`file.read: not a file: ${params.path}`);
755
+ }
756
+ if (stat.size > maxBytes) {
757
+ throw new Error(
758
+ `file.read: file too large (${stat.size} bytes > ${maxBytes}). Pass maxBytes to override or chunk the read.`
759
+ );
760
+ }
761
+ const buf = await fsp.readFile(abs);
762
+ const encoding = params.encoding ?? "utf8";
763
+ return {
764
+ path: params.path,
765
+ content: encoding === "base64" ? buf.toString("base64") : buf.toString("utf8"),
766
+ encoding,
767
+ size: stat.size,
768
+ mtimeMs: stat.mtimeMs
769
+ };
770
+ }
771
+ async function fileWrite(params) {
772
+ const abs = jailedPath(params.root, params.path);
773
+ if (params.createDirs !== false) {
774
+ await fsp.mkdir(dirname(abs), { recursive: true });
775
+ }
776
+ const buf = (params.encoding ?? "utf8") === "base64" ? Buffer2.from(params.content, "base64") : Buffer2.from(params.content, "utf8");
777
+ await fsp.writeFile(abs, buf);
778
+ const stat = await fsp.stat(abs);
779
+ return { path: params.path, size: stat.size, mtimeMs: stat.mtimeMs };
780
+ }
781
+ async function fileStat(params) {
782
+ let abs;
783
+ try {
784
+ abs = params.path == null || params.path === "" ? expandHome(params.root) : jailedPath(params.root, params.path);
785
+ } catch (e) {
786
+ if (/escapes workspace root/.test(e?.message ?? "")) throw e;
787
+ return { exists: false, isFile: false, isDir: false, size: 0, mtimeMs: 0 };
788
+ }
789
+ try {
790
+ const stat = statSync(abs);
791
+ return {
792
+ exists: true,
793
+ isFile: stat.isFile(),
794
+ isDir: stat.isDirectory(),
795
+ size: stat.size,
796
+ mtimeMs: stat.mtimeMs
797
+ };
798
+ } catch {
799
+ return { exists: false, isFile: false, isDir: false, size: 0, mtimeMs: 0 };
800
+ }
801
+ }
802
+ async function fileMkdir(params) {
803
+ const abs = jailedPath(params.root, params.path);
804
+ await fsp.mkdir(abs, { recursive: true });
805
+ return { path: params.path };
806
+ }
807
+ async function fileDelete(params) {
808
+ const abs = jailedPath(params.root, params.path);
809
+ await fsp.rm(abs, { recursive: !!params.recursive, force: false });
810
+ return { path: params.path };
811
+ }
812
+ async function fileRename(params) {
813
+ const fromAbs = jailedPath(params.root, params.from);
814
+ const toAbs = jailedPath(params.root, params.to);
815
+ await fsp.rename(fromAbs, toAbs);
816
+ return { from: params.from, to: params.to };
817
+ }
818
+ async function fsBrowse(params) {
819
+ const expanded = expandHome(params.path ?? "~");
820
+ if (!isAbsolute(expanded)) {
821
+ throw new Error(`fs.browse: path must be absolute (got "${params.path}")`);
822
+ }
823
+ let resolved;
824
+ try {
825
+ resolved = realpathSync(expanded);
826
+ } catch {
827
+ resolved = resolve3(expanded);
828
+ }
829
+ let raw;
830
+ try {
831
+ raw = await fsp.readdir(resolved, {
832
+ withFileTypes: true,
833
+ encoding: "utf8"
834
+ });
835
+ } catch (e) {
836
+ throw new Error(`fs.browse: cannot read ${resolved}: ${e?.code ?? e?.message ?? e}`);
837
+ }
838
+ const entries = [];
839
+ for (const entry of raw) {
840
+ if (!params.showHidden && entry.name.startsWith(".")) continue;
841
+ const isDir = entry.isDirectory();
842
+ if (!isDir && !params.includeFiles) continue;
843
+ entries.push({
844
+ name: entry.name,
845
+ path: join3(resolved, entry.name),
846
+ isDir
847
+ });
848
+ }
849
+ entries.sort((a, b) => {
850
+ if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
851
+ return a.name.localeCompare(b.name);
852
+ });
853
+ const parent = dirname(resolved);
854
+ return {
855
+ path: resolved,
856
+ parent: parent === resolved ? null : parent,
857
+ entries
858
+ };
859
+ }
860
+ async function fsHome() {
861
+ return { home: homedir3(), cwd: process.cwd() };
862
+ }
863
+ async function fsMkdir(params) {
864
+ const expanded = expandHome(params.path ?? "");
865
+ if (!expanded || !isAbsolute(expanded)) {
866
+ throw new Error(`fs.mkdir: path must be absolute (got "${params.path}")`);
867
+ }
868
+ await fsp.mkdir(expanded, { recursive: true });
869
+ return { path: expanded };
870
+ }
871
+
872
+ // src/rpc/pty-handlers.ts
873
+ import { spawn as spawn2 } from "child_process";
874
+ import { randomUUID } from "crypto";
875
+ import { existsSync as existsSync4, statSync as statSync2 } from "fs";
876
+ import { homedir as homedir4, platform } from "os";
877
+ import { join as join4 } from "path";
878
+ import { isAbsolute as isAbsolute2 } from "path";
879
+ var handles = /* @__PURE__ */ new Map();
880
+ function registerPtyHandlers(registry) {
881
+ registry.register("pty.spawn", ptySpawn);
882
+ registry.register("pty.write", ptyWrite);
883
+ registry.register("pty.resize", ptyResize);
884
+ registry.register("pty.kill", ptyKill);
885
+ }
886
+ async function ptySpawn(params, ctx) {
887
+ if (!params.cwd || typeof params.cwd !== "string" || !isAbsolute2(params.cwd)) {
888
+ throw new Error("pty.spawn: `cwd` is required and must be absolute");
889
+ }
890
+ let cwdStat;
891
+ try {
892
+ cwdStat = statSync2(params.cwd);
893
+ } catch {
894
+ throw new Error(`pty.spawn: cwd does not exist: ${params.cwd}`);
895
+ }
896
+ if (!cwdStat.isDirectory()) {
897
+ throw new Error(`pty.spawn: cwd is not a directory: ${params.cwd}`);
898
+ }
899
+ const command = params.command || defaultShell();
900
+ if (!existsSync4(command)) {
901
+ throw new Error(
902
+ `pty.spawn: shell binary not found: ${command} (set $SHELL in the daemon's environment, or pass an explicit \`command\`)`
903
+ );
904
+ }
905
+ const args = params.args ?? defaultShellArgs();
906
+ const env = { TERM: "xterm-256color" };
907
+ for (const [k, v] of Object.entries(process.env)) {
908
+ if (typeof v === "string") env[k] = v;
909
+ }
910
+ for (const [k, v] of Object.entries(params.env ?? {})) {
911
+ env[k] = v;
912
+ }
913
+ const cols = params.cols ?? 80;
914
+ const rows = params.rows ?? 24;
915
+ const handle = await openPty({ command, args, cwd: params.cwd, env, cols, rows, ctx });
916
+ handles.set(handle.handle.ptyId, handle.handle);
917
+ ctx.onInput((payload) => {
918
+ if (typeof payload === "string") handle.handle.write(payload);
919
+ else if (payload && typeof payload.data === "string") {
920
+ handle.handle.write(payload.data);
921
+ }
922
+ });
923
+ ctx.onCancel(() => {
924
+ handle.handle.kill("SIGTERM");
925
+ setTimeout(() => {
926
+ try {
927
+ handle.handle.kill("SIGKILL");
928
+ } catch {
929
+ }
930
+ }, 5e3).unref();
931
+ });
932
+ ctx.pushChunk({ kind: "mode", mode: handle.mode, ptyId: handle.handle.ptyId });
933
+ const exitCode = await handle.exited;
934
+ handles.delete(handle.handle.ptyId);
935
+ return { ptyId: handle.handle.ptyId, exitCode, mode: handle.mode };
936
+ }
937
+ async function ptyWrite(params) {
938
+ const handle = handles.get(params.ptyId);
939
+ if (!handle) throw new Error(`pty.write: unknown ptyId ${params.ptyId}`);
940
+ handle.write(params.data);
941
+ return { ok: true };
942
+ }
943
+ async function ptyResize(params) {
944
+ const handle = handles.get(params.ptyId);
945
+ if (!handle) throw new Error(`pty.resize: unknown ptyId ${params.ptyId}`);
946
+ handle.resize(params.cols, params.rows);
947
+ return { ok: true };
948
+ }
949
+ async function ptyKill(params) {
950
+ const handle = handles.get(params.ptyId);
951
+ if (!handle) throw new Error(`pty.kill: unknown ptyId ${params.ptyId}`);
952
+ handle.kill(params.signal ?? "SIGTERM");
953
+ return { ok: true };
954
+ }
955
+ async function openPty(opts) {
956
+ const nodePty = await loadNodePty();
957
+ if (nodePty) {
958
+ const proc2 = nodePty.spawn(opts.command, opts.args, {
959
+ name: "xterm-256color",
960
+ cols: opts.cols,
961
+ rows: opts.rows,
962
+ cwd: opts.cwd,
963
+ env: opts.env
964
+ });
965
+ const ptyId2 = randomUUID();
966
+ proc2.onData((data) => {
967
+ opts.ctx.pushChunk({ kind: "data", data });
968
+ });
969
+ const exited2 = new Promise((resolve11) => {
970
+ proc2.onExit(({ exitCode }) => {
971
+ resolve11(exitCode);
972
+ });
973
+ });
974
+ return {
975
+ mode: "pty",
976
+ exited: exited2,
977
+ handle: {
978
+ ptyId: ptyId2,
979
+ resize: (cols, rows) => proc2.resize(cols, rows),
980
+ write: (data) => proc2.write(data),
981
+ kill: (signal) => proc2.kill(signal)
982
+ }
983
+ };
984
+ }
985
+ const proc = spawn2(
986
+ opts.command,
987
+ opts.args,
988
+ {
989
+ cwd: opts.cwd,
990
+ env: opts.env,
991
+ stdio: ["pipe", "pipe", "pipe"]
992
+ }
993
+ );
994
+ const ptyId = randomUUID();
995
+ proc.stdout.on("data", (chunk) => {
996
+ opts.ctx.pushChunk({ kind: "data", data: chunk.toString("utf8") });
997
+ });
998
+ proc.stderr.on("data", (chunk) => {
999
+ opts.ctx.pushChunk({ kind: "data", data: chunk.toString("utf8") });
1000
+ });
1001
+ const exited = new Promise((resolve11) => {
1002
+ proc.on("close", (code) => resolve11(code));
1003
+ });
1004
+ return {
1005
+ mode: "pipe",
1006
+ exited,
1007
+ handle: {
1008
+ ptyId,
1009
+ resize: () => {
1010
+ },
1011
+ write: (data) => {
1012
+ proc.stdin.write(data);
1013
+ },
1014
+ kill: (signal) => {
1015
+ proc.kill(signal ?? "SIGTERM");
1016
+ }
1017
+ }
1018
+ };
1019
+ }
1020
+ var nodePtyCache = void 0;
1021
+ async function loadNodePty() {
1022
+ if (nodePtyCache !== void 0) return nodePtyCache;
1023
+ try {
1024
+ const moduleName = "node-pty";
1025
+ nodePtyCache = await import(moduleName);
1026
+ return nodePtyCache;
1027
+ } catch {
1028
+ nodePtyCache = null;
1029
+ return null;
1030
+ }
1031
+ }
1032
+ function defaultShell() {
1033
+ if (platform() === "win32") {
1034
+ return process.env.COMSPEC || "cmd.exe";
1035
+ }
1036
+ return process.env.SHELL || "/bin/bash";
1037
+ }
1038
+ function defaultShellArgs() {
1039
+ if (platform() === "win32") return [];
1040
+ return ["-l", "-i"];
1041
+ }
1042
+
486
1043
  // src/commands/daemon.ts
1044
+ var rpcRegistry = new RpcRegistry();
1045
+ registerFileHandlers(rpcRegistry);
1046
+ registerPtyHandlers(rpcRegistry);
487
1047
  var daemonCommand = new Command3("daemon").description("Manage and run the local botapp daemon");
488
1048
  daemonCommand.command("run").description(
489
1049
  "Run paired daemons and wait for server jobs. With no flags, runs every paired profile concurrently \u2014 so a single `bot daemon run` covers both local and remote pairings."
@@ -527,9 +1087,31 @@ function pickProfilesToRun(alias, server) {
527
1087
  }
528
1088
  return loadDaemonProfiles();
529
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
+ });
530
1112
  daemonCommand.command("stop").description("Stop the background daemon started by `bot launch`").action(async () => {
531
- const pidFile = join3(homedir3(), ".botapp", "daemon.pid");
532
- if (!existsSync4(pidFile)) {
1113
+ const pidFile = join5(homedir5(), ".botapp", "daemon.pid");
1114
+ if (!existsSync5(pidFile)) {
533
1115
  console.log(pc3.yellow("No background daemon PID file found."));
534
1116
  return;
535
1117
  }
@@ -555,16 +1137,17 @@ daemonCommand.command("agent").description("Manage agents hosted by this daemon"
555
1137
  for (const agent of data.agents ?? []) {
556
1138
  console.log(`${pc3.bold(agent.name)} ${pc3.dim(`(${agent.id})`)}`);
557
1139
  console.log(` Kind: ${agent.kind}`);
1140
+ if (agent.model) console.log(` Model: ${agent.model}`);
558
1141
  console.log(` Command: ${agent.command} ${(agent.args ?? []).join(" ")}`);
559
1142
  if (agent.cwd) console.log(` CWD: ${agent.cwd}`);
560
1143
  }
561
1144
  })
562
1145
  ).addCommand(createDaemonAgentConfigCommand()).addCommand(
563
- 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(
564
1147
  "--kind <kind>",
565
- "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",
566
1149
  "acp"
567
- ).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) => {
568
1151
  const profile = requireSelectedProfile(opts);
569
1152
  if (!profile) return;
570
1153
  const env = parseEnv(opts.env ?? []);
@@ -580,12 +1163,14 @@ daemonCommand.command("agent").description("Manage agents hosted by this daemon"
580
1163
  command: opts.command,
581
1164
  args: opts.arg ?? [],
582
1165
  cwd: opts.cwd,
583
- env
1166
+ env,
1167
+ model: opts.model
584
1168
  }
585
1169
  }
586
1170
  );
587
1171
  console.log(pc3.green(`Registered daemon agent: ${pc3.bold(data.agent.name)}`));
588
1172
  console.log(` ID: ${data.agent.id}`);
1173
+ if (data.agent.model) console.log(` Model: ${data.agent.model}`);
589
1174
  console.log(` Command: ${data.agent.command} ${(data.agent.args ?? []).join(" ")}`);
590
1175
  })
591
1176
  ).addCommand(
@@ -657,6 +1242,7 @@ function runDaemonSocket(wsUrl, setActiveWs) {
657
1242
  return new Promise((resolveRun) => {
658
1243
  const ws = new WebSocket(wsUrl);
659
1244
  setActiveWs(ws);
1245
+ const rpcDispatcher = new RpcDispatcher(rpcRegistry, ws);
660
1246
  let opened = false;
661
1247
  let superseded = false;
662
1248
  let settled = false;
@@ -665,6 +1251,7 @@ function runDaemonSocket(wsUrl, setActiveWs) {
665
1251
  if (settled) return;
666
1252
  settled = true;
667
1253
  if (ping) clearInterval(ping);
1254
+ rpcDispatcher.shutdown();
668
1255
  resolveRun({ opened, superseded });
669
1256
  }
670
1257
  ws.on("open", () => {
@@ -676,7 +1263,7 @@ function runDaemonSocket(wsUrl, setActiveWs) {
676
1263
  }, 3e4);
677
1264
  });
678
1265
  ws.on("message", (raw) => {
679
- void handleFrame(ws, raw.toString());
1266
+ void handleFrame(ws, raw.toString(), rpcDispatcher);
680
1267
  });
681
1268
  ws.on("close", (code, reason) => {
682
1269
  if (code === 4e3) superseded = true;
@@ -692,8 +1279,20 @@ function runDaemonSocket(wsUrl, setActiveWs) {
692
1279
  });
693
1280
  });
694
1281
  }
695
- async function handleFrame(ws, raw) {
1282
+ async function handleFrame(ws, raw, rpcDispatcher) {
696
1283
  const frame = JSON.parse(raw);
1284
+ if (frame.type === "daemon_rpc_request") {
1285
+ void rpcDispatcher.dispatchRequest(frame);
1286
+ return;
1287
+ }
1288
+ if (frame.type === "daemon_rpc_input") {
1289
+ rpcDispatcher.dispatchInput(frame);
1290
+ return;
1291
+ }
1292
+ if (frame.type === "daemon_rpc_cancel") {
1293
+ rpcDispatcher.dispatchCancel(frame);
1294
+ return;
1295
+ }
697
1296
  if (frame.type === "daemon_job") {
698
1297
  const job = frame.job;
699
1298
  console.log(pc3.blue(`Running ${job.agent.name} job ${job.id}`));
@@ -737,7 +1336,10 @@ async function runAgentJob(job, update) {
737
1336
  if (job.agent.kind === "claude-code" || job.agent.kind === "claude_code") {
738
1337
  return runClaudeCodeAgent(job, update);
739
1338
  }
740
- 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") {
741
1343
  return runOpenClawAgent(job, update);
742
1344
  }
743
1345
  if (job.agent.kind === "hermes" || job.agent.kind === "hermes-agent") {
@@ -749,7 +1351,7 @@ async function runAgentJob(job, update) {
749
1351
  return runAcpAgent(job);
750
1352
  }
751
1353
  async function runShellAgent(job) {
752
- const child = spawn2(job.agent.command, [...job.agent.args, job.query], {
1354
+ const child = spawn3(job.agent.command, [...job.agent.args, job.query], {
753
1355
  cwd: job.agent.cwd ?? process.cwd(),
754
1356
  env: { ...process.env, ...job.agent.env ?? {} }
755
1357
  });
@@ -761,7 +1363,7 @@ async function runShellAgent(job) {
761
1363
  child.stderr.on("data", (chunk) => {
762
1364
  stderr += chunk.toString();
763
1365
  });
764
- const code = await new Promise((resolve7) => child.on("close", resolve7));
1366
+ const code = await new Promise((resolve11) => child.on("close", resolve11));
765
1367
  if (code !== 0) {
766
1368
  throw new Error(stderr.trim() || `Agent exited with code ${code}`);
767
1369
  }
@@ -778,6 +1380,9 @@ async function runCodexAgent(job, update) {
778
1380
  if (!args.includes("--skip-git-repo-check")) {
779
1381
  args.push("--skip-git-repo-check");
780
1382
  }
1383
+ if (job.agent.model && !hasAnyFlag(args, ["--model", "-m"])) {
1384
+ args.push("--model", job.agent.model);
1385
+ }
781
1386
  if (!hasAnyFlag(args, [
782
1387
  "--sandbox",
783
1388
  "-s",
@@ -789,7 +1394,7 @@ async function runCodexAgent(job, update) {
789
1394
  args.push("--dangerously-bypass-approvals-and-sandbox");
790
1395
  }
791
1396
  args.push(job.query);
792
- const child = spawn2(job.agent.command, args, {
1397
+ const child = spawn3(job.agent.command, args, {
793
1398
  cwd: job.agent.cwd ?? process.cwd(),
794
1399
  env: { ...process.env, ...job.agent.env ?? {} },
795
1400
  stdio: ["ignore", "pipe", "pipe"]
@@ -806,12 +1411,19 @@ async function runCodexAgent(job, update) {
806
1411
  rawEvents: []
807
1412
  };
808
1413
  const toolCalls = /* @__PURE__ */ new Map();
1414
+ const errors = [];
809
1415
  function processLine(line) {
810
1416
  if (!line.trim()) return;
811
1417
  try {
812
1418
  const event = JSON.parse(line);
813
1419
  result.rawEvents.push(event);
814
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
+ }
815
1427
  if (event?.type === "item.completed" && event.item?.type === "agent_message") {
816
1428
  if (typeof event.item.text === "string") {
817
1429
  result.messages.push(event.item.text);
@@ -839,9 +1451,9 @@ async function runCodexAgent(job, update) {
839
1451
  }
840
1452
  const rl = createInterface2({ input: child.stdout });
841
1453
  rl.on("line", processLine);
842
- const code = await new Promise((resolve7) => child.on("close", resolve7));
1454
+ const code = await new Promise((resolve11) => child.on("close", resolve11));
843
1455
  if (code !== 0) {
844
- throw new Error(stderr.trim() || `Codex exited with code ${code}`);
1456
+ throw new Error(errors.at(-1) ?? (stderr.trim() || `Codex exited with code ${code}`));
845
1457
  }
846
1458
  result.text = result.messages.at(-1)?.trim() || "";
847
1459
  result.toolCalls = [...toolCalls.values()];
@@ -858,6 +1470,9 @@ async function runClaudeCodeAgent(job, update) {
858
1470
  if (!args.includes("--verbose")) {
859
1471
  args.push("--verbose");
860
1472
  }
1473
+ if (job.agent.model && !hasAnyFlag(args, ["--model"])) {
1474
+ args.push("--model", job.agent.model);
1475
+ }
861
1476
  if (!hasAnyFlag(args, [
862
1477
  "--permission-mode",
863
1478
  "--dangerously-skip-permissions",
@@ -869,7 +1484,7 @@ async function runClaudeCodeAgent(job, update) {
869
1484
  if (resume && !args.includes("--resume")) {
870
1485
  args.push("--resume", resume);
871
1486
  }
872
- const child = spawn2(job.agent.command, args, {
1487
+ const child = spawn3(job.agent.command, args, {
873
1488
  cwd: job.agent.cwd ?? process.cwd(),
874
1489
  env: { ...process.env, ...job.agent.env ?? {} },
875
1490
  stdio: ["pipe", "pipe", "pipe"]
@@ -880,6 +1495,9 @@ async function runClaudeCodeAgent(job, update) {
880
1495
  child.stderr.on("data", (chunk) => {
881
1496
  stderr += chunk.toString();
882
1497
  });
1498
+ const noiseLines = [];
1499
+ const errorEvents = [];
1500
+ const NOISE_TAIL = 6;
883
1501
  const result = {
884
1502
  kind: "claude-code",
885
1503
  text: "",
@@ -937,11 +1555,19 @@ async function runClaudeCodeAgent(job, update) {
937
1555
  try {
938
1556
  event = JSON.parse(line);
939
1557
  } catch {
1558
+ noiseLines.push(line);
1559
+ if (noiseLines.length > NOISE_TAIL) noiseLines.shift();
940
1560
  return;
941
1561
  }
942
1562
  result.rawEvents.push(event);
943
1563
  update({ kind: "claude_code_event", event });
944
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
+ }
945
1571
  if (event.type === "assistant" && event.message?.content) {
946
1572
  ingestAssistantContent(
947
1573
  Array.isArray(event.message.content) ? event.message.content : []
@@ -960,9 +1586,148 @@ async function runClaudeCodeAgent(job, update) {
960
1586
  }
961
1587
  const rl = createInterface2({ input: child.stdout });
962
1588
  rl.on("line", processLine);
963
- const code = await new Promise((resolve7) => child.on("close", resolve7));
1589
+ const code = await new Promise((resolve11) => child.on("close", resolve11));
964
1590
  if (code !== 0) {
965
- 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}`);
966
1731
  }
967
1732
  if (!result.text) {
968
1733
  result.text = result.messages.at(-1)?.trim() ?? "";
@@ -992,7 +1757,7 @@ async function runOpenClawAgent(job, update) {
992
1757
  }
993
1758
  let requestedSessionId = getFlagValue(args, "--session-id");
994
1759
  if (!requestedSessionId) {
995
- requestedSessionId = job.resumeSessionId || `botapp-${randomUUID()}`;
1760
+ requestedSessionId = job.resumeSessionId || `botapp-${randomUUID2()}`;
996
1761
  args.push("--session-id", requestedSessionId);
997
1762
  }
998
1763
  if (!hasFlag(args, "--verbose")) {
@@ -1002,7 +1767,7 @@ async function runOpenClawAgent(job, update) {
1002
1767
  const state = {
1003
1768
  startedAtMs: Date.now(),
1004
1769
  sessionDir,
1005
- sessionStorePath: join3(sessionDir, "sessions.json"),
1770
+ sessionStorePath: join5(sessionDir, "sessions.json"),
1006
1771
  requestedSessionId,
1007
1772
  sessionKey: null,
1008
1773
  sessionId: null,
@@ -1026,14 +1791,23 @@ async function runOpenClawAgent(job, update) {
1026
1791
  };
1027
1792
  const toolCalls = /* @__PURE__ */ new Map();
1028
1793
  let stderr = "";
1794
+ const stdoutLines = [];
1029
1795
  let stopPolling = false;
1030
- const child = spawn2(job.agent.command, args, {
1796
+ const child = spawn3(job.agent.command, args, {
1031
1797
  cwd: job.agent.cwd ?? process.cwd(),
1032
1798
  env,
1033
1799
  stdio: ["ignore", "pipe", "pipe"]
1034
1800
  });
1035
1801
  const stdout = createInterface2({ input: child.stdout });
1036
- 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 });
1037
1811
  });
1038
1812
  const stderrReader = createInterface2({ input: child.stderr });
1039
1813
  stderrReader.on("line", (line) => {
@@ -1065,11 +1839,11 @@ async function runOpenClawAgent(job, update) {
1065
1839
  }
1066
1840
  result.sessionFile = state.selectedFile;
1067
1841
  result.toolCalls = [...toolCalls.values()];
1068
- result.text = result.text || result.messages.at(-1)?.trim() || "";
1842
+ result.text = result.text || result.messages.at(-1)?.trim() || extractOpenClawStdoutText(stdoutLines.at(-1) ?? "") || "";
1069
1843
  if (code !== 0) {
1070
1844
  throw new Error(stderr.trim() || `OpenClaw exited with code ${code}`);
1071
1845
  }
1072
- if (!result.sessionId) {
1846
+ if (!result.sessionId && !result.text && result.toolCalls.length === 0) {
1073
1847
  throw new Error(
1074
1848
  [
1075
1849
  "OpenClaw completed, but the session key was not resolved from sessions.json.",
@@ -1090,11 +1864,22 @@ async function runOpenClawAgent(job, update) {
1090
1864
  }
1091
1865
  return JSON.stringify(result);
1092
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
+ }
1093
1878
  function resolveOpenClawSessionDir(args, env) {
1094
1879
  const configured = env.BOTAPP_OPENCLAW_SESSION_DIR ?? env.OPENCLAW_SESSION_DIR;
1095
1880
  if (configured) return expandPath(configured);
1096
1881
  const agentName = resolveOpenClawAgentName(args);
1097
- return join3(homedir3(), ".openclaw", "agents", agentName, "sessions");
1882
+ return join5(homedir5(), ".openclaw", "agents", agentName, "sessions");
1098
1883
  }
1099
1884
  function resolveOpenClawAgentName(args) {
1100
1885
  return getFlagValue(args, "--agent") ?? "main";
@@ -1103,7 +1888,7 @@ function snapshotOpenClawSessionOffsets(sessionDir) {
1103
1888
  const offsets = /* @__PURE__ */ new Map();
1104
1889
  for (const file of listOpenClawSessionFiles(sessionDir)) {
1105
1890
  try {
1106
- offsets.set(file, statSync(file).size);
1891
+ offsets.set(file, statSync3(file).size);
1107
1892
  } catch {
1108
1893
  }
1109
1894
  }
@@ -1179,7 +1964,7 @@ function selectOpenClawSessionFile(state) {
1179
1964
  const files = listOpenClawSessionFiles(state.sessionDir);
1180
1965
  if (files.length === 0) return null;
1181
1966
  if (state.sessionId) {
1182
- const exact = join3(state.sessionDir, `${state.sessionId}.jsonl`);
1967
+ const exact = join5(state.sessionDir, `${state.sessionId}.jsonl`);
1183
1968
  if (files.includes(exact)) return exact;
1184
1969
  const matching = files.filter((file) => file.includes(state.sessionId ?? ""));
1185
1970
  if (matching.length > 0) return newestFile(matching);
@@ -1187,7 +1972,7 @@ function selectOpenClawSessionFile(state) {
1187
1972
  if (files.length === 1) return files[0];
1188
1973
  const recent = files.filter((file) => {
1189
1974
  try {
1190
- return statSync(file).mtimeMs >= state.startedAtMs - 1e3;
1975
+ return statSync3(file).mtimeMs >= state.startedAtMs - 1e3;
1191
1976
  } catch {
1192
1977
  return false;
1193
1978
  }
@@ -1196,10 +1981,10 @@ function selectOpenClawSessionFile(state) {
1196
1981
  }
1197
1982
  function listOpenClawSessionFiles(sessionDir) {
1198
1983
  try {
1199
- if (!existsSync4(sessionDir)) return [];
1200
- return readdirSync(sessionDir).filter((name) => name.endsWith(".jsonl")).map((name) => join3(sessionDir, name)).filter((file) => {
1984
+ if (!existsSync5(sessionDir)) return [];
1985
+ return readdirSync(sessionDir).filter((name) => name.endsWith(".jsonl")).map((name) => join5(sessionDir, name)).filter((file) => {
1201
1986
  try {
1202
- return statSync(file).isFile();
1987
+ return statSync3(file).isFile();
1203
1988
  } catch {
1204
1989
  return false;
1205
1990
  }
@@ -1212,7 +1997,7 @@ function newestFile(files) {
1212
1997
  if (files.length === 0) return null;
1213
1998
  return files.reduce((selected, file) => {
1214
1999
  try {
1215
- return statSync(file).mtimeMs > statSync(selected).mtimeMs ? file : selected;
2000
+ return statSync3(file).mtimeMs > statSync3(selected).mtimeMs ? file : selected;
1216
2001
  } catch {
1217
2002
  return selected;
1218
2003
  }
@@ -1336,7 +2121,7 @@ async function runHermesAgent(job, update) {
1336
2121
  let stderr = "";
1337
2122
  let stdoutText = "";
1338
2123
  let stopPolling = false;
1339
- const child = spawn2(job.agent.command, args, {
2124
+ const child = spawn3(job.agent.command, args, {
1340
2125
  cwd: job.agent.cwd ?? process.cwd(),
1341
2126
  env,
1342
2127
  stdio: ["ignore", "pipe", "pipe"]
@@ -1403,10 +2188,10 @@ async function runHermesAgent(job, update) {
1403
2188
  function resolveHermesSessionDir(env) {
1404
2189
  const configured = env.BOTAPP_HERMES_SESSION_DIR ?? env.HERMES_SESSION_DIR;
1405
2190
  if (configured) return expandPath(configured);
1406
- return join3(homedir3(), ".hermes", "sessions");
2191
+ return join5(homedir5(), ".hermes", "sessions");
1407
2192
  }
1408
2193
  function hermesSessionFile(sessionDir, sessionId) {
1409
- return join3(sessionDir, `session_${sessionId}.json`);
2194
+ return join5(sessionDir, `session_${sessionId}.json`);
1410
2195
  }
1411
2196
  function parseHermesSessionId(text) {
1412
2197
  const match = text.match(/session_id:\s*([A-Za-z0-9_-]+)/);
@@ -1454,13 +2239,13 @@ function readHermesSessionUpdates(state, result, toolCalls, update, final = fals
1454
2239
  function selectHermesSessionFile(state) {
1455
2240
  if (state.resumeSessionId) {
1456
2241
  const exact = hermesSessionFile(state.sessionDir, state.resumeSessionId);
1457
- if (existsSync4(exact)) return exact;
2242
+ if (existsSync5(exact)) return exact;
1458
2243
  }
1459
2244
  const files = listHermesSessionFiles(state.sessionDir);
1460
2245
  if (files.length === 0) return null;
1461
2246
  const recent = files.filter((file) => {
1462
2247
  try {
1463
- return statSync(file).mtimeMs >= state.startedAtMs - 1e3;
2248
+ return statSync3(file).mtimeMs >= state.startedAtMs - 1e3;
1464
2249
  } catch {
1465
2250
  return false;
1466
2251
  }
@@ -1469,10 +2254,10 @@ function selectHermesSessionFile(state) {
1469
2254
  }
1470
2255
  function listHermesSessionFiles(sessionDir) {
1471
2256
  try {
1472
- if (!existsSync4(sessionDir)) return [];
1473
- return readdirSync(sessionDir).filter((name) => /^session_.+\.json$/.test(name)).map((name) => join3(sessionDir, name)).filter((file) => {
2257
+ if (!existsSync5(sessionDir)) return [];
2258
+ return readdirSync(sessionDir).filter((name) => /^session_.+\.json$/.test(name)).map((name) => join5(sessionDir, name)).filter((file) => {
1474
2259
  try {
1475
- return statSync(file).isFile();
2260
+ return statSync3(file).isFile();
1476
2261
  } catch {
1477
2262
  return false;
1478
2263
  }
@@ -1492,7 +2277,7 @@ function ingestHermesMessage(message, result, toolCalls, update) {
1492
2277
  }
1493
2278
  if (Array.isArray(message.tool_calls)) {
1494
2279
  for (const call of message.tool_calls) {
1495
- const id = String(call?.id ?? call?.call_id ?? call?.response_item_id ?? randomUUID());
2280
+ const id = String(call?.id ?? call?.call_id ?? call?.response_item_id ?? randomUUID2());
1496
2281
  const name = String(call?.function?.name ?? call?.name ?? "tool");
1497
2282
  const next = {
1498
2283
  ...toolCalls.get(id),
@@ -1521,7 +2306,7 @@ function ingestHermesMessage(message, result, toolCalls, update) {
1521
2306
  }
1522
2307
  }
1523
2308
  async function runAcpAgent(job) {
1524
- const child = spawn2(job.agent.command, job.agent.args, {
2309
+ const child = spawn3(job.agent.command, job.agent.args, {
1525
2310
  cwd: job.agent.cwd ?? process.cwd(),
1526
2311
  env: { ...process.env, ...job.agent.env ?? {} },
1527
2312
  stdio: ["pipe", "pipe", "pipe"]
@@ -1577,8 +2362,8 @@ Invalid ACP stdout: ${line}`;
1577
2362
  function request2(method, params) {
1578
2363
  const id = nextId++;
1579
2364
  child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
1580
- return new Promise((resolve7, reject) => {
1581
- pending.set(id, { resolve: resolve7, reject });
2365
+ return new Promise((resolve11, reject) => {
2366
+ pending.set(id, { resolve: resolve11, reject });
1582
2367
  });
1583
2368
  }
1584
2369
  child.on("exit", (code) => {
@@ -1602,7 +2387,7 @@ Invalid ACP stdout: ${line}`;
1602
2387
  clientInfo: {
1603
2388
  name: "botapp-daemon",
1604
2389
  title: "botapp daemon",
1605
- version: "0.2.4"
2390
+ version: "0.2.7"
1606
2391
  }
1607
2392
  });
1608
2393
  const session = await request2("session/new", {
@@ -1694,9 +2479,9 @@ function hasAnyFlag(args, flags) {
1694
2479
  return flags.some((flag) => hasFlag(args, flag));
1695
2480
  }
1696
2481
  function expandPath(path) {
1697
- if (path === "~") return homedir3();
1698
- if (path.startsWith("~/")) return join3(homedir3(), path.slice(2));
1699
- return resolve3(path);
2482
+ if (path === "~") return homedir5();
2483
+ if (path.startsWith("~/")) return join5(homedir5(), path.slice(2));
2484
+ return resolve4(path);
1700
2485
  }
1701
2486
  function envNumber(env, name, fallback) {
1702
2487
  const value = env[name];
@@ -1752,10 +2537,10 @@ async function waitForChild(child, timeoutMs, label) {
1752
2537
 
1753
2538
  // src/commands/launch.ts
1754
2539
  import { Command as Command4 } from "commander";
1755
- import { spawn as spawn4 } from "child_process";
1756
- import { resolve as resolve4, join as join5 } from "path";
1757
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, openSync, writeFileSync as writeFileSync3 } from "fs";
1758
- import { homedir as homedir5 } from "os";
2540
+ import { spawn as spawn5 } from "child_process";
2541
+ import { resolve as resolve5, join as join7 } from "path";
2542
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, openSync, writeFileSync as writeFileSync3 } from "fs";
2543
+ import { homedir as homedir7 } from "os";
1759
2544
  import { createInterface as createInterface3 } from "readline";
1760
2545
  import { hostname } from "os";
1761
2546
  import pc5 from "picocolors";
@@ -1763,7 +2548,7 @@ import pc5 from "picocolors";
1763
2548
  // src/auth/browser-auth.ts
1764
2549
  import { createServer } from "http";
1765
2550
  import { randomBytes } from "crypto";
1766
- import { spawn as spawn3 } from "child_process";
2551
+ import { spawn as spawn4 } from "child_process";
1767
2552
  import pc4 from "picocolors";
1768
2553
  var OK_HTML = `<!doctype html>
1769
2554
  <meta charset="utf-8">
@@ -1858,11 +2643,11 @@ async function startLoopback(expectedState) {
1858
2643
  let rejectCb = () => {
1859
2644
  };
1860
2645
  let settled = false;
1861
- const callbackPromise = new Promise((resolve7, reject) => {
2646
+ const callbackPromise = new Promise((resolve11, reject) => {
1862
2647
  resolveCb = (p) => {
1863
2648
  if (settled) return;
1864
2649
  settled = true;
1865
- resolve7(p);
2650
+ resolve11(p);
1866
2651
  };
1867
2652
  rejectCb = (e) => {
1868
2653
  if (settled) return;
@@ -1873,11 +2658,11 @@ async function startLoopback(expectedState) {
1873
2658
  const server = createServer((req, res) => {
1874
2659
  void handleLoopback(req, res, expectedState, resolveCb, rejectCb);
1875
2660
  });
1876
- await new Promise((resolve7, reject) => {
2661
+ await new Promise((resolve11, reject) => {
1877
2662
  server.once("error", reject);
1878
2663
  server.listen(0, "127.0.0.1", () => {
1879
2664
  server.removeListener("error", reject);
1880
- resolve7();
2665
+ resolve11();
1881
2666
  });
1882
2667
  });
1883
2668
  const address = server.address();
@@ -1989,7 +2774,7 @@ function asString(v) {
1989
2774
  return typeof v === "string" ? v : void 0;
1990
2775
  }
1991
2776
  function readJsonBody(req) {
1992
- return new Promise((resolve7, reject) => {
2777
+ return new Promise((resolve11, reject) => {
1993
2778
  const chunks = [];
1994
2779
  let total = 0;
1995
2780
  req.on("data", (c) => {
@@ -2003,9 +2788,9 @@ function readJsonBody(req) {
2003
2788
  });
2004
2789
  req.on("end", () => {
2005
2790
  const raw = Buffer.concat(chunks).toString("utf-8");
2006
- if (!raw) return resolve7(null);
2791
+ if (!raw) return resolve11(null);
2007
2792
  try {
2008
- resolve7(JSON.parse(raw));
2793
+ resolve11(JSON.parse(raw));
2009
2794
  } catch (e) {
2010
2795
  reject(e);
2011
2796
  }
@@ -2017,20 +2802,20 @@ function openUrl(url) {
2017
2802
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
2018
2803
  const args = process.platform === "win32" ? ["/c", "start", '""', url] : [url];
2019
2804
  try {
2020
- spawn3(cmd, args, { stdio: "ignore", detached: true }).unref();
2805
+ spawn4(cmd, args, { stdio: "ignore", detached: true }).unref();
2021
2806
  } catch {
2022
2807
  }
2023
2808
  }
2024
2809
 
2025
2810
  // src/commands/daemon-supervisor.ts
2026
- import { existsSync as existsSync5, readFileSync as readFileSync4, unlinkSync } from "fs";
2027
- import { homedir as homedir4 } from "os";
2028
- import { join as join4 } from "path";
2811
+ import { existsSync as existsSync6, readFileSync as readFileSync4, unlinkSync } from "fs";
2812
+ import { homedir as homedir6 } from "os";
2813
+ import { join as join6 } from "path";
2029
2814
  function daemonPidFile() {
2030
- return join4(homedir4(), ".botapp", "daemon.pid");
2815
+ return join6(homedir6(), ".botapp", "daemon.pid");
2031
2816
  }
2032
2817
  function isDaemonRunningLocally(pidFile = daemonPidFile()) {
2033
- if (!existsSync5(pidFile)) return false;
2818
+ if (!existsSync6(pidFile)) return false;
2034
2819
  try {
2035
2820
  const pid = parseInt(readFileSync4(pidFile, "utf-8").trim(), 10);
2036
2821
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -2177,7 +2962,8 @@ Configured for ${serverUrl}`));
2177
2962
  server: result.serverUrl ?? serverUrl,
2178
2963
  daemonId: data.daemon.id,
2179
2964
  daemonName: data.daemon.name,
2180
- token: data.token
2965
+ token: data.token,
2966
+ userEmail: result.userEmail
2181
2967
  });
2182
2968
  console.log(
2183
2969
  ` Daemon: ${pc5.bold(data.daemon.name)} ${pc5.dim(`(${data.daemon.id})`)}` + pc5.dim(` profile=${savedProfile.alias}`)
@@ -2198,20 +2984,20 @@ async function autoStartDaemon(opts, serverUrl, daemonId) {
2198
2984
  Next: run \`bot daemon run\` to bring this machine online.`));
2199
2985
  return;
2200
2986
  }
2201
- const dir = join5(homedir5(), ".botapp");
2202
- const pidFile = join5(dir, "daemon.pid");
2203
- const logFile = join5(dir, "daemon.log");
2987
+ const dir = join7(homedir7(), ".botapp");
2988
+ const pidFile = join7(dir, "daemon.pid");
2989
+ const logFile = join7(dir, "daemon.log");
2204
2990
  mkdirSync3(dir, { recursive: true });
2205
2991
  if (isDaemonRunningLocally(pidFile)) {
2206
2992
  stopExistingDaemon(pidFile);
2207
2993
  }
2208
2994
  const botBin = process.argv[1];
2209
- if (!botBin || !existsSync6(botBin)) {
2995
+ if (!botBin || !existsSync7(botBin)) {
2210
2996
  console.log(pc5.yellow(` Running: cannot resolve \`bot\` binary \u2014 run \`bot daemon run\` manually`));
2211
2997
  return;
2212
2998
  }
2213
2999
  const logFd = openSync(logFile, "a");
2214
- const child = spawn4(process.execPath, [botBin, "daemon", "run"], {
3000
+ const child = spawn5(process.execPath, [botBin, "daemon", "run"], {
2215
3001
  stdio: ["ignore", logFd, logFd],
2216
3002
  detached: true
2217
3003
  });
@@ -2316,7 +3102,7 @@ Or run a local server from source:
2316
3102
  return false;
2317
3103
  }
2318
3104
  console.log(pc5.blue("Starting local server..."));
2319
- const child = spawn4("node", ["--import", "tsx", serverEntry], {
3105
+ const child = spawn5("node", ["--import", "tsx", serverEntry], {
2320
3106
  env: { ...process.env, PORT: opts.port },
2321
3107
  stdio: opts.background ? "ignore" : "inherit",
2322
3108
  detached: opts.background
@@ -2333,12 +3119,12 @@ Or run a local server from source:
2333
3119
  }
2334
3120
  function findServerEntry2() {
2335
3121
  const candidates = [
2336
- resolve4(process.cwd(), "packages/server/src/index.ts"),
2337
- resolve4(process.cwd(), "../server/src/index.ts"),
2338
- resolve4(process.cwd(), "../../packages/server/src/index.ts")
3122
+ resolve5(process.cwd(), "packages/server/src/index.ts"),
3123
+ resolve5(process.cwd(), "../server/src/index.ts"),
3124
+ resolve5(process.cwd(), "../../packages/server/src/index.ts")
2339
3125
  ];
2340
3126
  for (const c of candidates) {
2341
- if (existsSync6(c)) return c;
3127
+ if (existsSync7(c)) return c;
2342
3128
  }
2343
3129
  return null;
2344
3130
  }
@@ -2540,21 +3326,21 @@ var loginCommand = new Command7("login").description("Login to a botapp server")
2540
3326
 
2541
3327
  // src/commands/install.ts
2542
3328
  import { Command as Command8 } from "commander";
2543
- import { resolve as resolve5, basename } from "path";
2544
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, symlinkSync, cpSync, readFileSync as readFileSync5 } from "fs";
2545
- import { homedir as homedir6 } from "os";
2546
- import { join as join6 } from "path";
3329
+ import { resolve as resolve6, basename } from "path";
3330
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, symlinkSync, cpSync, readFileSync as readFileSync5 } from "fs";
3331
+ import { homedir as homedir8 } from "os";
3332
+ import { join as join8 } from "path";
2547
3333
  import pc9 from "picocolors";
2548
- var APPS_DIR = join6(homedir6(), ".botapp", "apps");
3334
+ var APPS_DIR = join8(homedir8(), ".botapp", "apps");
2549
3335
  var installCommand = new Command8("install").description("Install an app from a local path").argument("<path>", "Path to the app directory").option("--dev", "Install in dev mode (symlink)").action(async (appPath, opts) => {
2550
- const absPath = resolve5(appPath);
2551
- if (!existsSync7(absPath)) {
3336
+ const absPath = resolve6(appPath);
3337
+ if (!existsSync8(absPath)) {
2552
3338
  console.error(pc9.red(`Path not found: ${absPath}`));
2553
3339
  process.exitCode = 1;
2554
3340
  return;
2555
3341
  }
2556
- const manifestPath = join6(absPath, "botapp.app.json");
2557
- if (!existsSync7(manifestPath)) {
3342
+ const manifestPath = join8(absPath, "botapp.app.json");
3343
+ if (!existsSync8(manifestPath)) {
2558
3344
  console.error(pc9.red(`No botapp.app.json found in ${absPath}`));
2559
3345
  process.exitCode = 1;
2560
3346
  return;
@@ -2568,9 +3354,9 @@ var installCommand = new Command8("install").description("Install an app from a
2568
3354
  return;
2569
3355
  }
2570
3356
  const appName = manifest.name || basename(absPath);
2571
- const targetDir = join6(APPS_DIR, appName);
3357
+ const targetDir = join8(APPS_DIR, appName);
2572
3358
  mkdirSync4(APPS_DIR, { recursive: true });
2573
- if (existsSync7(targetDir)) {
3359
+ if (existsSync8(targetDir)) {
2574
3360
  console.log(pc9.yellow(`App "${appName}" is already installed. Reinstalling...`));
2575
3361
  const { rmSync: rmSync2 } = await import("fs");
2576
3362
  rmSync2(targetDir, { recursive: true, force: true });
@@ -2589,14 +3375,14 @@ var installCommand = new Command8("install").description("Install an app from a
2589
3375
 
2590
3376
  // src/commands/uninstall.ts
2591
3377
  import { Command as Command9 } from "commander";
2592
- import { existsSync as existsSync8, rmSync } from "fs";
2593
- import { homedir as homedir7 } from "os";
2594
- import { join as join7 } from "path";
3378
+ import { existsSync as existsSync9, rmSync } from "fs";
3379
+ import { homedir as homedir9 } from "os";
3380
+ import { join as join9 } from "path";
2595
3381
  import pc10 from "picocolors";
2596
- var APPS_DIR2 = join7(homedir7(), ".botapp", "apps");
3382
+ var APPS_DIR2 = join9(homedir9(), ".botapp", "apps");
2597
3383
  var uninstallCommand = new Command9("uninstall").description("Uninstall an app").argument("<name>", "App name to uninstall").action(async (name) => {
2598
- const targetDir = join7(APPS_DIR2, name);
2599
- if (!existsSync8(targetDir)) {
3384
+ const targetDir = join9(APPS_DIR2, name);
3385
+ if (!existsSync9(targetDir)) {
2600
3386
  console.error(pc10.red(`App "${name}" is not installed`));
2601
3387
  process.exitCode = 1;
2602
3388
  return;
@@ -2608,21 +3394,21 @@ var uninstallCommand = new Command9("uninstall").description("Uninstall an app")
2608
3394
 
2609
3395
  // src/commands/dev.ts
2610
3396
  import { Command as Command10 } from "commander";
2611
- import { resolve as resolve6, basename as basename2, join as join8 } from "path";
2612
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, symlinkSync as symlinkSync2, readFileSync as readFileSync6, lstatSync } from "fs";
2613
- import { homedir as homedir8 } from "os";
2614
- import { spawn as spawn5 } from "child_process";
3397
+ import { resolve as resolve7, basename as basename2, join as join10 } from "path";
3398
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, symlinkSync as symlinkSync2, readFileSync as readFileSync6, lstatSync } from "fs";
3399
+ import { homedir as homedir10 } from "os";
3400
+ import { spawn as spawn6 } from "child_process";
2615
3401
  import pc11 from "picocolors";
2616
- var APPS_DIR3 = join8(homedir8(), ".botapp", "apps");
3402
+ var APPS_DIR3 = join10(homedir10(), ".botapp", "apps");
2617
3403
  var devCommand = new Command10("dev").description("Start development mode for an app").argument("[path]", "Path to the app directory", ".").option("-p, --port <port>", "Server port", "7100").action(async (appPath, opts) => {
2618
- const absPath = resolve6(appPath);
2619
- if (!existsSync9(absPath)) {
3404
+ const absPath = resolve7(appPath);
3405
+ if (!existsSync10(absPath)) {
2620
3406
  console.error(pc11.red(`Path not found: ${absPath}`));
2621
3407
  process.exitCode = 1;
2622
3408
  return;
2623
3409
  }
2624
- const manifestPath = join8(absPath, "botapp.app.json");
2625
- if (!existsSync9(manifestPath)) {
3410
+ const manifestPath = join10(absPath, "botapp.app.json");
3411
+ if (!existsSync10(manifestPath)) {
2626
3412
  console.error(pc11.red(`No botapp.app.json found in ${absPath}`));
2627
3413
  process.exitCode = 1;
2628
3414
  return;
@@ -2636,9 +3422,9 @@ var devCommand = new Command10("dev").description("Start development mode for an
2636
3422
  return;
2637
3423
  }
2638
3424
  const appName = manifest.name || basename2(absPath);
2639
- const targetDir = join8(APPS_DIR3, appName);
3425
+ const targetDir = join10(APPS_DIR3, appName);
2640
3426
  mkdirSync5(APPS_DIR3, { recursive: true });
2641
- if (!existsSync9(targetDir)) {
3427
+ if (!existsSync10(targetDir)) {
2642
3428
  symlinkSync2(absPath, targetDir, "dir");
2643
3429
  console.log(pc11.blue(`Linked ${pc11.bold(appName)} \u2192 ${pc11.dim(absPath)}`));
2644
3430
  } else {
@@ -2671,7 +3457,7 @@ Start the server separately, then restart it to load the app:
2671
3457
  return;
2672
3458
  }
2673
3459
  console.log(pc11.blue("Starting botapp server..."));
2674
- const child = spawn5("node", ["--import", "tsx", serverEntry], {
3460
+ const child = spawn6("node", ["--import", "tsx", serverEntry], {
2675
3461
  env: { ...process.env, PORT: opts.port },
2676
3462
  stdio: "inherit"
2677
3463
  });
@@ -2685,11 +3471,11 @@ Start the server separately, then restart it to load the app:
2685
3471
  });
2686
3472
  function findServerEntry3() {
2687
3473
  const candidates = [
2688
- resolve6(process.cwd(), "packages/server/src/index.ts"),
2689
- resolve6(process.cwd(), "../server/src/index.ts")
3474
+ resolve7(process.cwd(), "packages/server/src/index.ts"),
3475
+ resolve7(process.cwd(), "../server/src/index.ts")
2690
3476
  ];
2691
3477
  for (const c of candidates) {
2692
- if (existsSync9(c)) return c;
3478
+ if (existsSync10(c)) return c;
2693
3479
  }
2694
3480
  return null;
2695
3481
  }
@@ -2923,13 +3709,13 @@ All agents:`);
2923
3709
 
2924
3710
  // src/commands/register.ts
2925
3711
  import { Command as Command14 } from "commander";
2926
- import { readFileSync as readFileSync7, existsSync as existsSync10 } from "fs";
3712
+ import { readFileSync as readFileSync7, existsSync as existsSync11 } from "fs";
2927
3713
  import pc15 from "picocolors";
2928
3714
  var registerCommand = new Command14("register").description("Register an external app via HTTP bridge manifest").argument("<manifest>", "Path to YAML manifest file").option("--adapter <url>", "Override base URL from manifest").action(async (manifestPath, opts, cmd) => {
2929
3715
  const globalOpts = cmd.parent?.opts() ?? {};
2930
3716
  const serverUrl = resolveServerUrl(globalOpts.server);
2931
3717
  const token = resolveToken(globalOpts.token);
2932
- if (!existsSync10(manifestPath)) {
3718
+ if (!existsSync11(manifestPath)) {
2933
3719
  console.error(pc15.red(`Error: Manifest not found: ${manifestPath}`));
2934
3720
  process.exitCode = 1;
2935
3721
  return;
@@ -2966,13 +3752,13 @@ var registerCommand = new Command14("register").description("Register an externa
2966
3752
 
2967
3753
  // src/commands/wrap.ts
2968
3754
  import { Command as Command15 } from "commander";
2969
- import { readFileSync as readFileSync8, existsSync as existsSync11 } from "fs";
3755
+ import { readFileSync as readFileSync8, existsSync as existsSync12 } from "fs";
2970
3756
  import pc16 from "picocolors";
2971
3757
  var wrapCommand = new Command15("wrap").description("Register CLI tool as app commands via YAML manifest").argument("<manifest>", "Path to CLI wrapper YAML manifest").action(async (manifestPath, _opts, cmd) => {
2972
3758
  const globalOpts = cmd.parent?.opts() ?? {};
2973
3759
  const serverUrl = resolveServerUrl(globalOpts.server);
2974
3760
  const token = resolveToken(globalOpts.token);
2975
- if (!existsSync11(manifestPath)) {
3761
+ if (!existsSync12(manifestPath)) {
2976
3762
  console.error(pc16.red(`Error: Manifest not found: ${manifestPath}`));
2977
3763
  process.exitCode = 1;
2978
3764
  return;
@@ -3236,7 +4022,8 @@ var pairingCommand = new Command18("pairing").alias("pair").description("Pair th
3236
4022
  server: serverUrl,
3237
4023
  daemonId: data.daemon.id,
3238
4024
  daemonName: data.daemon.name,
3239
- token: data.token
4025
+ token: data.token,
4026
+ userEmail: grant.userEmail
3240
4027
  });
3241
4028
  console.log(pc19.green(`Paired daemon: ${pc19.bold(data.daemon.name)}`));
3242
4029
  console.log(` ID: ${data.daemon.id}`);
@@ -3272,13 +4059,130 @@ async function obtainPairingToken(opts) {
3272
4059
  return { pairingToken: result.pairingToken, userEmail: result.userEmail };
3273
4060
  }
3274
4061
 
3275
- // src/commands/update.ts
3276
- import { spawn as spawn6 } from "child_process";
3277
- import { realpathSync } from "fs";
4062
+ // src/commands/doctor.ts
3278
4063
  import { Command as Command19 } from "commander";
3279
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
+
4179
+ // src/commands/update.ts
4180
+ import { spawn as spawn7 } from "child_process";
4181
+ import { realpathSync as realpathSync2 } from "fs";
4182
+ import { Command as Command20 } from "commander";
4183
+ import pc21 from "picocolors";
3280
4184
  var PACKAGE_NAME = "botapp-cli";
3281
- 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(
3282
4186
  "--manager <pm>",
3283
4187
  "Force a package manager: npm | pnpm | yarn | brew (default: auto-detect)"
3284
4188
  ).option("--dry-run", "Print the command that would run, but do not execute it").action(async (opts) => {
@@ -3286,12 +4190,12 @@ var updateCommand = new Command19("update").description("Update the `bot` CLI it
3286
4190
  const manager = forced ?? detectPackageManager();
3287
4191
  if (manager === "npx") {
3288
4192
  console.log(
3289
- pc20.green(
4193
+ pc21.green(
3290
4194
  "You are running `bot` via `npx -y botapp-cli@latest` \u2014 every invocation already fetches the latest version."
3291
4195
  )
3292
4196
  );
3293
4197
  console.log(
3294
- pc20.dim(
4198
+ pc21.dim(
3295
4199
  "For a faster startup, install it globally instead:\n npm i -g botapp-cli@latest\n pnpm add -g botapp-cli@latest"
3296
4200
  )
3297
4201
  );
@@ -3299,40 +4203,40 @@ var updateCommand = new Command19("update").description("Update the `bot` CLI it
3299
4203
  }
3300
4204
  if (!manager) {
3301
4205
  console.error(
3302
- pc20.red(
4206
+ pc21.red(
3303
4207
  "Couldn't detect how `bot` was installed. Pick one of these manually:"
3304
4208
  )
3305
4209
  );
3306
- console.log(pc20.cyan(" npm i -g botapp-cli@latest"));
3307
- console.log(pc20.cyan(" pnpm add -g botapp-cli@latest"));
3308
- console.log(pc20.cyan(" yarn global add botapp-cli@latest"));
3309
- 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"));
3310
4214
  process.exitCode = 1;
3311
4215
  return;
3312
4216
  }
3313
4217
  const { command, args } = updateCommandFor(manager);
3314
- console.log(pc20.dim(`$ ${command} ${args.join(" ")}`));
4218
+ console.log(pc21.dim(`$ ${command} ${args.join(" ")}`));
3315
4219
  if (opts.dryRun) return;
3316
- const child = spawn6(command, args, { stdio: "inherit" });
3317
- const code = await new Promise((resolve7) => {
4220
+ const child = spawn7(command, args, { stdio: "inherit" });
4221
+ const code = await new Promise((resolve11) => {
3318
4222
  child.once("error", (err) => {
3319
- console.error(pc20.red(`Failed to spawn ${command}: ${err.message}`));
3320
- resolve7(127);
4223
+ console.error(pc21.red(`Failed to spawn ${command}: ${err.message}`));
4224
+ resolve11(127);
3321
4225
  });
3322
- child.once("close", resolve7);
4226
+ child.once("close", resolve11);
3323
4227
  });
3324
4228
  if (code !== 0) {
3325
- console.error(pc20.red(`Update failed (exit ${code}).`));
4229
+ console.error(pc21.red(`Update failed (exit ${code}).`));
3326
4230
  process.exitCode = code ?? 1;
3327
4231
  return;
3328
4232
  }
3329
- 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."));
3330
4234
  });
3331
4235
  function detectPackageManager() {
3332
4236
  const argv = process.argv[1] ?? "";
3333
4237
  let real = "";
3334
4238
  try {
3335
- real = argv ? realpathSync(argv) : "";
4239
+ real = argv ? realpathSync2(argv) : "";
3336
4240
  } catch {
3337
4241
  }
3338
4242
  const candidates = [argv, real].filter(Boolean);
@@ -3360,9 +4264,755 @@ function updateCommandFor(manager) {
3360
4264
  }
3361
4265
  }
3362
4266
 
4267
+ // src/commands/simulate.ts
4268
+ import { Command as Command21 } from "commander";
4269
+ import { resolve as resolve8, join as join11 } from "path";
4270
+ import { existsSync as existsSync13, readFileSync as readFileSync9 } from "fs";
4271
+ import { spawn as spawn8 } from "child_process";
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) => {
4274
+ const absPath = resolve8(appPath);
4275
+ if (!existsSync13(absPath)) {
4276
+ console.error(pc22.red(`Path not found: ${absPath}`));
4277
+ process.exitCode = 1;
4278
+ return;
4279
+ }
4280
+ const manifestPath = join11(absPath, "botapp.app.json");
4281
+ if (!existsSync13(manifestPath)) {
4282
+ console.error(pc22.red(`No botapp.app.json found in ${absPath}`));
4283
+ process.exitCode = 1;
4284
+ return;
4285
+ }
4286
+ const manifest = JSON.parse(readFileSync9(manifestPath, "utf8"));
4287
+ if (!manifest.name) {
4288
+ console.error(pc22.red('manifest missing "name"'));
4289
+ process.exitCode = 1;
4290
+ return;
4291
+ }
4292
+ const httpServer = resolveServerUrl(opts.server);
4293
+ const wsServer = httpServer.replace(/^http:/, "ws:").replace(/^https:/, "wss:") + "/ws/host";
4294
+ const token = resolveToken(opts.token);
4295
+ if (!token) {
4296
+ console.error(pc22.red("not logged in: run `bot login` or pass --token"));
4297
+ process.exitCode = 1;
4298
+ return;
4299
+ }
4300
+ const lifetimeMs = Math.max(6e4, Number(opts.lifetime) * 6e4);
4301
+ console.log(pc22.dim(`requesting dev token for "${manifest.name}" from ${httpServer}...`));
4302
+ const devToken = await issueDevToken({
4303
+ serverUrl: httpServer,
4304
+ token,
4305
+ appName: manifest.name,
4306
+ lifetimeMs
4307
+ });
4308
+ console.log(pc22.green("\u2713"), `dev token issued (lifetime ${opts.lifetime} min)`);
4309
+ const entryPath = resolveEntry(absPath, opts.entry ?? manifest.entry);
4310
+ if (!existsSync13(entryPath)) {
4311
+ console.error(pc22.red(`entry not found: ${entryPath}`));
4312
+ console.error(pc22.dim(" run `pnpm build` (or your build script) first."));
4313
+ process.exitCode = 1;
4314
+ return;
4315
+ }
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}`));
4319
+ console.log(
4320
+ pc22.cyan(
4321
+ `
4322
+ When ready, your dashboard at ${httpServer} will route "${manifest.name}" to this process for your account only.
4323
+ `
4324
+ )
4325
+ );
4326
+ const child = spawn8("node", [entryPath], {
4327
+ cwd: absPath,
4328
+ env: {
4329
+ ...process.env,
4330
+ BOTAPP_SERVER: wsServer,
4331
+ BOTAPP_APP_TOKEN: devToken,
4332
+ BOTAPP_APP_NAME: manifest.name,
4333
+ BOTAPP_DATA_DIR: join11(absPath, ".botapp-sim")
4334
+ },
4335
+ stdio: "inherit"
4336
+ });
4337
+ const stop = (signal) => {
4338
+ console.log(pc22.dim(`
4339
+ stopping (${signal})...`));
4340
+ if (!child.killed) child.kill(signal);
4341
+ };
4342
+ process.on("SIGINT", () => stop("SIGINT"));
4343
+ process.on("SIGTERM", () => stop("SIGTERM"));
4344
+ child.on("exit", (code, sig) => {
4345
+ const reason = sig ? `signal ${sig}` : `exit ${code}`;
4346
+ console.log(pc22.dim(`child exited (${reason})`));
4347
+ process.exit(typeof code === "number" ? code : 0);
4348
+ });
4349
+ });
4350
+ async function issueDevToken(opts) {
4351
+ const res = await fetch(`${opts.serverUrl}/api/dev/token`, {
4352
+ method: "POST",
4353
+ headers: authHeaders(opts.token),
4354
+ body: JSON.stringify({ appName: opts.appName, lifetimeMs: opts.lifetimeMs })
4355
+ });
4356
+ if (!res.ok) {
4357
+ const text = await res.text().catch(() => "");
4358
+ throw new Error(`dev-token request failed (${res.status}): ${text}`);
4359
+ }
4360
+ const data = await res.json();
4361
+ if (!data.token) {
4362
+ throw new Error(`dev-token response missing token: ${JSON.stringify(data)}`);
4363
+ }
4364
+ return data.token;
4365
+ }
4366
+ function resolveEntry(appDir, entry) {
4367
+ const candidates = [
4368
+ entry,
4369
+ "dist/api.js",
4370
+ "dist/api/index.js",
4371
+ "dist/index.js",
4372
+ "api/index.ts",
4373
+ "api/index.js"
4374
+ ].filter(Boolean);
4375
+ for (const c of candidates) {
4376
+ const full = resolve8(appDir, c);
4377
+ if (existsSync13(full)) return full;
4378
+ }
4379
+ return resolve8(appDir, candidates[0] ?? "dist/api.js");
4380
+ }
4381
+
4382
+ // src/commands/init.ts
4383
+ import { Command as Command22 } from "commander";
4384
+ import { existsSync as existsSync14, mkdirSync as mkdirSync6, writeFileSync as writeFileSync4 } from "fs";
4385
+ import { resolve as resolve9, join as join12 } from "path";
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) => {
4388
+ if (!/^[a-z][a-z0-9-]*$/.test(name)) {
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"));
4391
+ process.exitCode = 1;
4392
+ return;
4393
+ }
4394
+ const targetDir = resolve9(opts.dir ?? `./${name}`);
4395
+ if (existsSync14(targetDir) && !opts.force) {
4396
+ const stat = (() => {
4397
+ try {
4398
+ return __require("fs").readdirSync(targetDir);
4399
+ } catch {
4400
+ return [];
4401
+ }
4402
+ })();
4403
+ if (Array.isArray(stat) && stat.length > 0) {
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"));
4406
+ process.exitCode = 1;
4407
+ return;
4408
+ }
4409
+ }
4410
+ const ctx = {
4411
+ name,
4412
+ description: opts.description ?? `${name} \u2014 a botapp app`,
4413
+ headless: !!opts.headless
4414
+ };
4415
+ mkdirSync6(targetDir, { recursive: true });
4416
+ const files = ctx.headless ? headlessFiles(ctx) : fullFiles(ctx);
4417
+ for (const [rel, content] of Object.entries(files)) {
4418
+ const full = join12(targetDir, rel);
4419
+ mkdirSync6(dirname2(full), { recursive: true });
4420
+ writeFileSync4(full, content);
4421
+ }
4422
+ console.log(pc23.green("\u2713"), `Scaffolded ${ctx.headless ? "headless " : ""}app at`, pc23.cyan(targetDir));
4423
+ console.log();
4424
+ console.log("Next steps:");
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`));
4431
+ console.log();
4432
+ console.log(
4433
+ pc23.dim("Once it works, `bot publish` ships it to the server (per-user install by default).")
4434
+ );
4435
+ });
4436
+ function fullFiles(ctx) {
4437
+ return {
4438
+ "botapp.app.json": manifestJson(ctx),
4439
+ "package.json": packageJson(
4440
+ ctx,
4441
+ /*headless*/
4442
+ false
4443
+ ),
4444
+ "tsconfig.json": tsconfigJson(false),
4445
+ "tsconfig.api.json": tsconfigApiJson(),
4446
+ "tsconfig.frontend.json": tsconfigFrontendJson(),
4447
+ "tsup.api.config.ts": tsupApiConfig(),
4448
+ "vite.config.ts": viteConfig(ctx),
4449
+ "index.html": indexHtml(ctx),
4450
+ "api/index.ts": apiEntryTs(ctx, false),
4451
+ "src/main.tsx": srcMainTsx(ctx),
4452
+ "src/App.tsx": srcAppTsx(ctx),
4453
+ "src/lib/api.ts": srcApiTs(),
4454
+ "contracts/types.ts": contractsTs(ctx),
4455
+ ".gitignore": gitignore(),
4456
+ "README.md": readme(ctx, false)
4457
+ };
4458
+ }
4459
+ function headlessFiles(ctx) {
4460
+ return {
4461
+ "botapp.app.json": manifestJson(ctx),
4462
+ "package.json": packageJson(
4463
+ ctx,
4464
+ /*headless*/
4465
+ true
4466
+ ),
4467
+ "tsconfig.json": tsconfigJson(true),
4468
+ "tsup.api.config.ts": tsupApiConfig(),
4469
+ "api/index.ts": apiEntryTs(ctx, true),
4470
+ "contracts/types.ts": contractsTs(ctx),
4471
+ ".gitignore": gitignore(),
4472
+ "README.md": readme(ctx, true)
4473
+ };
4474
+ }
4475
+ function manifestJson(ctx) {
4476
+ const m = {
4477
+ name: ctx.name,
4478
+ version: "0.1.0",
4479
+ description: ctx.description,
4480
+ entry: "./dist/api.js",
4481
+ tier: "user",
4482
+ visibility: "private"
4483
+ };
4484
+ if (!ctx.headless) {
4485
+ m.hasFrontend = true;
4486
+ }
4487
+ return JSON.stringify(m, null, 2) + "\n";
4488
+ }
4489
+ function packageJson(ctx, headless) {
4490
+ const scripts = {
4491
+ build: headless ? "tsup --config tsup.api.config.ts" : "tsup --config tsup.api.config.ts && vite build",
4492
+ "build:api": "tsup --config tsup.api.config.ts",
4493
+ dev: headless ? "tsup --config tsup.api.config.ts --watch" : "tsup --config tsup.api.config.ts --watch & vite",
4494
+ typecheck: "tsc --noEmit"
4495
+ };
4496
+ const deps = {
4497
+ "botapp-sdk": "^0.1.0",
4498
+ ws: "^8.18.0"
4499
+ };
4500
+ const devDeps = {
4501
+ tsup: "^8.4.0",
4502
+ typescript: "^5.8.0",
4503
+ "@types/node": "^22.0.0",
4504
+ "@types/ws": "^8.5.0"
4505
+ };
4506
+ if (!headless) {
4507
+ Object.assign(deps, {
4508
+ react: "^19.0.0",
4509
+ "react-dom": "^19.0.0"
4510
+ });
4511
+ Object.assign(devDeps, {
4512
+ vite: "^7.0.0",
4513
+ "@vitejs/plugin-react": "^5.0.0",
4514
+ "@types/react": "^19.0.0",
4515
+ "@types/react-dom": "^19.0.0"
4516
+ });
4517
+ }
4518
+ return JSON.stringify(
4519
+ {
4520
+ name: ctx.name,
4521
+ version: "0.1.0",
4522
+ description: ctx.description,
4523
+ type: "module",
4524
+ private: true,
4525
+ scripts,
4526
+ dependencies: deps,
4527
+ devDependencies: devDeps
4528
+ },
4529
+ null,
4530
+ 2
4531
+ ) + "\n";
4532
+ }
4533
+ function tsconfigJson(headless) {
4534
+ if (headless) {
4535
+ return JSON.stringify(
4536
+ {
4537
+ compilerOptions: {
4538
+ target: "ES2022",
4539
+ module: "ESNext",
4540
+ moduleResolution: "bundler",
4541
+ esModuleInterop: true,
4542
+ strict: true,
4543
+ skipLibCheck: true,
4544
+ noEmit: true
4545
+ },
4546
+ include: ["api/**/*.ts", "contracts/**/*.ts"]
4547
+ },
4548
+ null,
4549
+ 2
4550
+ ) + "\n";
4551
+ }
4552
+ return JSON.stringify(
4553
+ {
4554
+ files: [],
4555
+ references: [
4556
+ { path: "./tsconfig.api.json" },
4557
+ { path: "./tsconfig.frontend.json" }
4558
+ ]
4559
+ },
4560
+ null,
4561
+ 2
4562
+ ) + "\n";
4563
+ }
4564
+ function tsconfigApiJson() {
4565
+ return JSON.stringify(
4566
+ {
4567
+ compilerOptions: {
4568
+ target: "ES2022",
4569
+ module: "ESNext",
4570
+ moduleResolution: "bundler",
4571
+ esModuleInterop: true,
4572
+ strict: true,
4573
+ skipLibCheck: true,
4574
+ noEmit: true
4575
+ },
4576
+ include: ["api/**/*.ts", "contracts/**/*.ts"]
4577
+ },
4578
+ null,
4579
+ 2
4580
+ ) + "\n";
4581
+ }
4582
+ function tsconfigFrontendJson() {
4583
+ return JSON.stringify(
4584
+ {
4585
+ compilerOptions: {
4586
+ target: "ES2022",
4587
+ module: "ESNext",
4588
+ moduleResolution: "bundler",
4589
+ esModuleInterop: true,
4590
+ strict: true,
4591
+ skipLibCheck: true,
4592
+ jsx: "react-jsx",
4593
+ lib: ["ES2022", "DOM", "DOM.Iterable"],
4594
+ noEmit: true
4595
+ },
4596
+ include: ["src/**/*.ts", "src/**/*.tsx", "contracts/**/*.ts"]
4597
+ },
4598
+ null,
4599
+ 2
4600
+ ) + "\n";
4601
+ }
4602
+ function tsupApiConfig() {
4603
+ return `import { defineConfig } from 'tsup'
4604
+
4605
+ export default defineConfig({
4606
+ entry: { api: 'api/index.ts' },
4607
+ format: ['esm'],
4608
+ outDir: 'dist',
4609
+ clean: true,
4610
+ sourcemap: true,
4611
+ noExternal: [],
4612
+ })
4613
+ `;
4614
+ }
4615
+ function viteConfig(ctx) {
4616
+ return `import { defineConfig } from 'vite'
4617
+ import react from '@vitejs/plugin-react'
4618
+
4619
+ export default defineConfig({
4620
+ plugins: [react()],
4621
+ base: '/apps/${ctx.name}/',
4622
+ build: {
4623
+ outDir: 'dist/public',
4624
+ emptyOutDir: true,
4625
+ },
4626
+ })
4627
+ `;
4628
+ }
4629
+ function indexHtml(ctx) {
4630
+ return `<!doctype html>
4631
+ <html lang="en">
4632
+ <head>
4633
+ <meta charset="UTF-8" />
4634
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
4635
+ <title>${escapeHtml(ctx.description)}</title>
4636
+ </head>
4637
+ <body>
4638
+ <div id="root"></div>
4639
+ <script type="module" src="/src/main.tsx"></script>
4640
+ </body>
4641
+ </html>
4642
+ `;
4643
+ }
4644
+ function apiEntryTs(ctx, headless) {
4645
+ const widget = headless ? "" : `
4646
+ ctx.registerWidget({
4647
+ refresh: { intervalMs: 30_000 },
4648
+ render: async ({ state }) => {
4649
+ const count = (await state.get('count')) ?? 0
4650
+ return {
4651
+ html: \`<div class="card"><div class="label">${ctx.name.toUpperCase()}</div><div class="value">\${count}</div></div>\`,
4652
+ css: \`.card { padding: 20px; font-family: sans-serif; } .value { font-size: 32px; font-weight: 700; }\`,
4653
+ }
4654
+ },
4655
+ })
4656
+
4657
+ ctx.serveStatic('./dist/public')
4658
+ `;
4659
+ return `import { BotApp } from 'botapp-sdk'
4660
+
4661
+ const app = new BotApp({
4662
+ name: '${ctx.name}',
4663
+ version: '0.1.0',
4664
+ description: '${ctx.description.replace(/'/g, "\\'")}',
4665
+ async setup(ctx) {
4666
+ ctx.registerCommand('hello', {
4667
+ description: 'Greet the caller',
4668
+ params: {
4669
+ name: { type: 'string', required: false, default: 'world', description: 'Who to greet' },
4670
+ },
4671
+ handler: async ({ name }, cmdCtx) => {
4672
+ const count = ((await cmdCtx.state.get('count')) as number | null) ?? 0
4673
+ await cmdCtx.state.set('count', count + 1)
4674
+ ctx.invalidateWidget?.()
4675
+ return \`Hello, \${name}! (called \${count + 1}x by \${cmdCtx.agent.id})\`
4676
+ },
4677
+ })
4678
+ ${widget} },
4679
+ })
4680
+
4681
+ await app.start()
4682
+ `;
4683
+ }
4684
+ function srcMainTsx(_ctx) {
4685
+ return `import { StrictMode } from 'react'
4686
+ import { createRoot } from 'react-dom/client'
4687
+ import { App } from './App'
4688
+
4689
+ createRoot(document.getElementById('root')!).render(
4690
+ <StrictMode>
4691
+ <App />
4692
+ </StrictMode>,
4693
+ )
4694
+ `;
4695
+ }
4696
+ function srcAppTsx(ctx) {
4697
+ return `import { useEffect, useState } from 'react'
4698
+ import { callCommand } from './lib/api'
4699
+
4700
+ export function App() {
4701
+ const [count, setCount] = useState<number | null>(null)
4702
+ const [error, setError] = useState<string | null>(null)
4703
+ useEffect(() => {
4704
+ callCommand('hello', { name: 'browser' })
4705
+ .then((res) => {
4706
+ const m = /\\(called (\\d+)x/.exec(String(res))
4707
+ if (m) setCount(Number(m[1]))
4708
+ })
4709
+ .catch((e: Error) => setError(e.message))
4710
+ }, [])
4711
+ return (
4712
+ <main style={{ fontFamily: 'sans-serif', padding: 24 }}>
4713
+ <h1>${ctx.name}</h1>
4714
+ {error ? <pre style={{ color: 'crimson' }}>{error}</pre> : <p>Calls so far: {count ?? '...'}</p>}
4715
+ </main>
4716
+ )
4717
+ }
4718
+ `;
4719
+ }
4720
+ function srcApiTs() {
4721
+ return `// Tiny client for calling app routes/commands from the browser.
4722
+ // Routes resolve as /apps/<name>/* on the platform; the platform forwards
4723
+ // each request to your app's WebSocket session.
4724
+
4725
+ const APP_BASE = ((): string => {
4726
+ const m = location.pathname.match(/^\\/apps\\/[^/]+\\//)
4727
+ return m ? m[0] : '/'
4728
+ })()
4729
+
4730
+ export async function callCommand(name: string, params: Record<string, unknown> = {}) {
4731
+ const r = await fetch(\`\${APP_BASE}api/commands/\${encodeURIComponent(name)}\`, {
4732
+ method: 'POST',
4733
+ headers: { 'Content-Type': 'application/json' },
4734
+ body: JSON.stringify(params),
4735
+ })
4736
+ if (!r.ok) throw new Error(await r.text())
4737
+ return r.json()
4738
+ }
4739
+
4740
+ export async function callAction(name: string, params: Record<string, unknown> = {}) {
4741
+ const r = await fetch(\`\${APP_BASE}api/actions/\${encodeURIComponent(name)}\`, {
4742
+ method: 'POST',
4743
+ headers: { 'Content-Type': 'application/json' },
4744
+ body: JSON.stringify(params),
4745
+ })
4746
+ if (!r.ok) throw new Error(await r.text())
4747
+ return r.json()
4748
+ }
4749
+ `;
4750
+ }
4751
+ function contractsTs(ctx) {
4752
+ return `// Types shared between api/ (backend) and src/ (frontend).
4753
+ // Importing from one side picks up changes on the other immediately.
4754
+
4755
+ export interface ${pascal(ctx.name)}State {
4756
+ count: number
4757
+ }
4758
+ `;
4759
+ }
4760
+ function gitignore() {
4761
+ return `node_modules/
4762
+ dist/
4763
+ .botapp-sim/
4764
+ *.log
4765
+ .DS_Store
4766
+ .env*.local
4767
+ `;
4768
+ }
4769
+ function readme(ctx, headless) {
4770
+ const surfaces = headless ? "- Backend only (no frontend)" : "- React + Vite frontend (built to `dist/public/`)\n- Dashboard widget (declared in `api/index.ts`)";
4771
+ return `# ${ctx.name}
4772
+
4773
+ ${ctx.description}
4774
+
4775
+ ## Surfaces
4776
+
4777
+ ${surfaces}
4778
+ - One agent-facing command: \`hello\`
4779
+
4780
+ ## Develop
4781
+
4782
+ \`\`\`bash
4783
+ pnpm install
4784
+ pnpm build # api \u2192 dist/api.js${headless ? "" : ", frontend \u2192 dist/public/"}
4785
+ bot login --server <your-botapp-server>
4786
+ bot simulate # dev-tunnel; hot-reload as you build
4787
+ \`\`\`
4788
+
4789
+ The simulator opens a per-user shadow of this app on the server. Your
4790
+ real dashboard at the server's URL routes the app to *your* local
4791
+ process; nobody else sees it.
4792
+
4793
+ ## Publish
4794
+
4795
+ \`\`\`bash
4796
+ bot publish # private, only you see it
4797
+ bot publish --public # requests admin review for public visibility
4798
+ \`\`\`
4799
+ `;
4800
+ }
4801
+ function escapeHtml(s) {
4802
+ return s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
4803
+ }
4804
+ function pascal(s) {
4805
+ return s.split(/[-_\s]+/).filter(Boolean).map((w) => w[0].toUpperCase() + w.slice(1)).join("");
4806
+ }
4807
+ function dirname2(p) {
4808
+ const i = p.lastIndexOf("/");
4809
+ return i < 0 ? "." : p.slice(0, i);
4810
+ }
4811
+
4812
+ // src/commands/publish.ts
4813
+ import { Command as Command23 } from "commander";
4814
+ import { resolve as resolve10, join as join13, relative as relative2 } from "path";
4815
+ import { existsSync as existsSync15, readFileSync as readFileSync10, statSync as statSync4, readdirSync as readdirSync2 } from "fs";
4816
+ import { createGzip } from "zlib";
4817
+ import { spawn as spawn9 } from "child_process";
4818
+ import pc24 from "picocolors";
4819
+ 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) => {
4820
+ const absPath = resolve10(appPath);
4821
+ const manifestPath = join13(absPath, "botapp.app.json");
4822
+ if (!existsSync15(manifestPath)) {
4823
+ console.error(pc24.red(`No botapp.app.json found in ${absPath}`));
4824
+ process.exitCode = 1;
4825
+ return;
4826
+ }
4827
+ const manifest = JSON.parse(readFileSync10(manifestPath, "utf8"));
4828
+ if (!manifest.name) {
4829
+ console.error(pc24.red('manifest missing "name"'));
4830
+ process.exitCode = 1;
4831
+ return;
4832
+ }
4833
+ const server = resolveServerUrl(opts.server);
4834
+ const token = resolveToken(opts.token);
4835
+ if (!token) {
4836
+ console.error(pc24.red("not logged in: run `bot login` or pass --token"));
4837
+ process.exitCode = 1;
4838
+ return;
4839
+ }
4840
+ if (opts.build !== false) {
4841
+ console.log(pc24.dim("building app..."));
4842
+ const ok = await runBuild(absPath);
4843
+ if (!ok) {
4844
+ console.error(pc24.red("build failed; aborting"));
4845
+ process.exitCode = 1;
4846
+ return;
4847
+ }
4848
+ }
4849
+ const bundleDir = opts.bundleDir ? resolve10(absPath, opts.bundleDir) : pickBundleDir(absPath);
4850
+ let bundleB64;
4851
+ if (bundleDir && existsSync15(bundleDir)) {
4852
+ console.log(pc24.dim(`packing ${relative2(absPath, bundleDir)}/ \u2192 tar.gz...`));
4853
+ const bytes = await packDirToTarGz(bundleDir);
4854
+ bundleB64 = bytes.toString("base64");
4855
+ console.log(pc24.dim(` bundle: ${(bytes.length / 1024).toFixed(1)} KiB`));
4856
+ } else {
4857
+ console.log(pc24.dim("no frontend bundle to upload (headless app or no dist/public)"));
4858
+ }
4859
+ console.log(pc24.dim(`uploading to ${server}/api/apps/upload (${opts.public ? "public" : "private"})...`));
4860
+ const res = await fetch(`${server}/api/apps/upload`, {
4861
+ method: "POST",
4862
+ headers: authHeaders(token),
4863
+ body: JSON.stringify({
4864
+ manifest,
4865
+ bundleB64,
4866
+ visibility: opts.public ? "public" : "private"
4867
+ })
4868
+ });
4869
+ if (!res.ok) {
4870
+ const body = await res.text().catch(() => "");
4871
+ console.error(pc24.red(`upload failed (${res.status}): ${body}`));
4872
+ process.exitCode = 1;
4873
+ return;
4874
+ }
4875
+ const data = await res.json();
4876
+ if (!data.ok) {
4877
+ console.error(pc24.red(`upload rejected: ${data.error ?? "unknown"}`));
4878
+ process.exitCode = 1;
4879
+ return;
4880
+ }
4881
+ console.log(pc24.green("\u2713"), data.message ?? "uploaded");
4882
+ if (data.install) {
4883
+ console.log(pc24.dim(` id: ${data.install.id}`));
4884
+ console.log(pc24.dim(` version: ${data.install.version}`));
4885
+ console.log(pc24.dim(` visibility: ${data.install.visibility}`));
4886
+ if (data.install.reviewStatus !== "none") {
4887
+ console.log(pc24.dim(` review: ${data.install.reviewStatus}`));
4888
+ }
4889
+ }
4890
+ });
4891
+ function runBuild(cwd) {
4892
+ const pkgManager = existsSync15(join13(cwd, "pnpm-lock.yaml")) ? "pnpm" : "npm";
4893
+ const args = pkgManager === "pnpm" ? ["build"] : ["run", "build"];
4894
+ return new Promise((resolveP) => {
4895
+ const child = spawn9(pkgManager, args, { cwd, stdio: "inherit" });
4896
+ child.on("exit", (code) => resolveP(code === 0));
4897
+ child.on("error", () => resolveP(false));
4898
+ });
4899
+ }
4900
+ function pickBundleDir(appDir) {
4901
+ const distPublic = join13(appDir, "dist", "public");
4902
+ if (existsSync15(distPublic)) return distPublic;
4903
+ const dist = join13(appDir, "dist");
4904
+ if (existsSync15(dist)) return dist;
4905
+ return null;
4906
+ }
4907
+ async function packDirToTarGz(rootDir) {
4908
+ const entries = [];
4909
+ walk(rootDir, "", entries);
4910
+ const blocks = [];
4911
+ for (const e of entries) {
4912
+ blocks.push(tarHeader(e.name, e.size));
4913
+ blocks.push(e.data);
4914
+ const pad = (512 - e.size % 512) % 512;
4915
+ if (pad) blocks.push(Buffer.alloc(pad));
4916
+ }
4917
+ blocks.push(Buffer.alloc(1024));
4918
+ const tar = Buffer.concat(blocks);
4919
+ return await gzip(tar);
4920
+ }
4921
+ function walk(root, prefix, out) {
4922
+ for (const entry of readdirSync2(root)) {
4923
+ const full = join13(root, entry);
4924
+ const st = statSync4(full);
4925
+ const rel = (prefix ? `${prefix}/` : "") + entry;
4926
+ if (st.isDirectory()) {
4927
+ walk(full, rel, out);
4928
+ } else if (st.isFile()) {
4929
+ const data = readFileSync10(full);
4930
+ out.push({ name: rel, size: data.length, data });
4931
+ }
4932
+ }
4933
+ }
4934
+ function tarHeader(name, size) {
4935
+ const h = Buffer.alloc(512);
4936
+ h.write(name.length > 100 ? name.slice(name.length - 100) : name, 0, 100);
4937
+ h.write("0000644", 100, 7);
4938
+ h.write("0000000", 108, 7);
4939
+ h.write("0000000", 116, 7);
4940
+ h.write(size.toString(8).padStart(11, "0"), 124, 11);
4941
+ h.write(Math.floor(Date.now() / 1e3).toString(8).padStart(11, "0"), 136, 11);
4942
+ h.write(" ", 148, 8);
4943
+ h.write("0", 156, 1);
4944
+ h.write("ustar ", 257, 8);
4945
+ let cksum = 0;
4946
+ for (const b of h) cksum += b;
4947
+ h.write(cksum.toString(8).padStart(6, "0") + "\0 ", 148, 8);
4948
+ return h;
4949
+ }
4950
+ function gzip(input2) {
4951
+ return new Promise((resolveP, rejectP) => {
4952
+ const gz = createGzip();
4953
+ const chunks = [];
4954
+ gz.on("data", (c) => chunks.push(c));
4955
+ gz.on("end", () => resolveP(Buffer.concat(chunks)));
4956
+ gz.on("error", rejectP);
4957
+ gz.end(input2);
4958
+ });
4959
+ }
4960
+
4961
+ // src/commands/review.ts
4962
+ import { Command as Command24 } from "commander";
4963
+ import pc25 from "picocolors";
4964
+ var reviewCommand = new Command24("review").description("Admin: review queue for public-visibility uploads");
4965
+ reviewCommand.command("list").description("List pending public-visibility uploads").option("-s, --server <url>", "Server URL").option("-t, --token <token>", "Auth token").action(async (opts) => {
4966
+ const server = resolveServerUrl(opts.server);
4967
+ const token = resolveToken(opts.token);
4968
+ if (!token) return die("not logged in: run `bot login` or pass --token");
4969
+ const res = await fetch(`${server}/api/admin/review-queue`, { headers: authHeaders(token) });
4970
+ if (!res.ok) {
4971
+ const body = await res.text().catch(() => "");
4972
+ return die(`request failed (${res.status}): ${body}`);
4973
+ }
4974
+ const data = await res.json();
4975
+ if (!data.pending?.length) {
4976
+ console.log(pc25.dim("queue empty"));
4977
+ return;
4978
+ }
4979
+ for (const i of data.pending) {
4980
+ console.log(pc25.bold(i.id), pc25.cyan(i.appName), pc25.dim(`v${i.version}`));
4981
+ console.log(pc25.dim(` uploader: ${i.ownerUserId ?? "(server-wide)"}`));
4982
+ console.log(pc25.dim(` uploaded: ${i.uploadedAt}`));
4983
+ const desc = i.manifest?.description;
4984
+ if (desc) console.log(pc25.dim(` description: ${desc}`));
4985
+ console.log();
4986
+ }
4987
+ });
4988
+ reviewCommand.command("approve <id>").description("Approve a pending public install").option("--notes <text>", "Optional reviewer notes").option("-s, --server <url>", "Server URL").option("-t, --token <token>", "Auth token").action(async (id, opts) => decide(id, "approve", opts));
4989
+ reviewCommand.command("reject <id>").description("Reject a pending public install").requiredOption("--notes <text>", "Reason for rejection (required)").option("-s, --server <url>", "Server URL").option("-t, --token <token>", "Auth token").action(async (id, opts) => decide(id, "reject", opts));
4990
+ async function decide(id, decision, opts) {
4991
+ const server = resolveServerUrl(opts.server);
4992
+ const token = resolveToken(opts.token);
4993
+ if (!token) return die("not logged in: run `bot login` or pass --token");
4994
+ const res = await fetch(`${server}/api/admin/review/${encodeURIComponent(id)}`, {
4995
+ method: "POST",
4996
+ headers: authHeaders(token),
4997
+ body: JSON.stringify({ decision, notes: opts.notes })
4998
+ });
4999
+ if (!res.ok) {
5000
+ const body = await res.text().catch(() => "");
5001
+ return die(`request failed (${res.status}): ${body}`);
5002
+ }
5003
+ const data = await res.json();
5004
+ if (!data.ok) return die(`server rejected: ${data.error ?? "unknown"}`);
5005
+ console.log(pc25.green("\u2713"), `${decision}d`, data.install?.appName, pc25.dim(`(${data.install?.id})`));
5006
+ if (data.install?.reviewNotes) console.log(pc25.dim(` notes: ${data.install.reviewNotes}`));
5007
+ }
5008
+ function die(msg) {
5009
+ console.error(pc25.red(msg));
5010
+ process.exitCode = 1;
5011
+ }
5012
+
3363
5013
  // src/index.ts
3364
- var version = "0.2.4";
3365
- var program = new Command20().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");
5014
+ var version = "0.2.7";
5015
+ 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");
3366
5016
  program.addCommand(launchCommand);
3367
5017
  program.addCommand(runCommand);
3368
5018
  program.addCommand(appsCommand);
@@ -3373,10 +5023,15 @@ program.addCommand(loginCommand);
3373
5023
  program.addCommand(agentCommand);
3374
5024
  program.addCommand(pairingCommand);
3375
5025
  program.addCommand(daemonCommand);
5026
+ program.addCommand(doctorCommand);
5027
+ program.addCommand(initCommand);
3376
5028
  program.addCommand(installCommand);
3377
5029
  program.addCommand(uninstallCommand);
3378
5030
  program.addCommand(reloadCommand);
3379
5031
  program.addCommand(configCommand);
5032
+ program.addCommand(simulateCommand);
5033
+ program.addCommand(publishCommand);
5034
+ program.addCommand(reviewCommand);
3380
5035
  program.addCommand(serverCommand);
3381
5036
  program.addCommand(updateCommand);
3382
5037
  program.addCommand(devCommand, { hidden: true });
@@ -3420,29 +5075,29 @@ To discover what params a command accepts:
3420
5075
  program.on("command:*", (operands) => {
3421
5076
  const first = operands[0];
3422
5077
  const known = program.commands.filter((c) => !c._hidden).map((c) => c.name());
3423
- const topLevelHint = `Run ${pc21.cyan("bot --help")} for the list of top-level commands.`;
5078
+ const topLevelHint = `Run ${pc26.cyan("bot --help")} for the list of top-level commands.`;
3424
5079
  const argv = process.argv.slice(2);
3425
5080
  const firstIdx = argv.indexOf(first);
3426
5081
  const tail = firstIdx >= 0 ? argv.slice(firstIdx + 1) : [];
3427
5082
  if (tail.length > 0) {
3428
5083
  const suggested = `bot run ${first} ${tail.join(" ")}`;
3429
5084
  console.error(
3430
- pc21.red(`error: unknown command '${first}'`) + `
5085
+ pc26.red(`error: unknown command '${first}'`) + `
3431
5086
 
3432
- App commands go through ${pc21.bold("bot run")}. Did you mean:
5087
+ App commands go through ${pc26.bold("bot run")}. Did you mean:
3433
5088
 
3434
- ${pc21.cyan(suggested)}
5089
+ ${pc26.cyan(suggested)}
3435
5090
 
3436
5091
  ${topLevelHint}`
3437
5092
  );
3438
5093
  process.exit(1);
3439
5094
  }
3440
5095
  console.error(
3441
- pc21.red(`error: unknown command '${first}'`) + `
5096
+ pc26.red(`error: unknown command '${first}'`) + `
3442
5097
 
3443
5098
  If '${first}' is an app name, invoke one of its commands with:
3444
- ${pc21.cyan(`bot run ${first} <command> [--key value ...]`)}
3445
- ${pc21.cyan("bot apps --json")} ${pc21.dim("(to see what commands exist)")}
5099
+ ${pc26.cyan(`bot run ${first} <command> [--key value ...]`)}
5100
+ ${pc26.cyan("bot apps --json")} ${pc26.dim("(to see what commands exist)")}
3446
5101
 
3447
5102
  Top-level commands: ${known.join(", ")}
3448
5103
  ${topLevelHint}`