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/bin/bot.js CHANGED
@@ -1,8 +1,14 @@
1
1
  #!/usr/bin/env node
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
2
8
 
3
9
  // src/index.ts
4
- import { Command as Command20 } from "commander";
5
- import pc21 from "picocolors";
10
+ import { Command as Command25 } from "commander";
11
+ import pc26 from "picocolors";
6
12
 
7
13
  // src/commands/server.ts
8
14
  import { Command } from "commander";
@@ -153,11 +159,11 @@ function findServerEntry() {
153
159
  }
154
160
 
155
161
  // src/commands/daemon.ts
156
- import { spawn as spawn2 } from "child_process";
157
- import { randomUUID } from "crypto";
158
- import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync3, statSync } from "fs";
159
- import { homedir as homedir3 } from "os";
160
- import { join as join3, resolve as resolve3 } from "path";
162
+ import { spawn as spawn3 } from "child_process";
163
+ import { randomUUID as randomUUID2 } from "crypto";
164
+ import { existsSync as existsSync5, readdirSync, readFileSync as readFileSync3, statSync as statSync3 } from "fs";
165
+ import { homedir as homedir5 } from "os";
166
+ import { join as join5, resolve as resolve4 } from "path";
161
167
  import { createInterface as createInterface2 } from "readline";
162
168
  import { Command as Command3 } from "commander";
163
169
  import { WebSocket } from "ws";
@@ -189,7 +195,8 @@ function writeYaml(profiles) {
189
195
  server: p.server,
190
196
  daemonId: p.daemonId,
191
197
  daemonName: p.daemonName,
192
- token: p.token
198
+ token: p.token,
199
+ ...p.userEmail ? { userEmail: p.userEmail } : {}
193
200
  };
194
201
  }
195
202
  writeFileSync2(DAEMON_FILE, stringify2({ profiles: map }), "utf-8");
@@ -229,7 +236,8 @@ function loadDaemonProfiles() {
229
236
  server: normalizeServer(value.server),
230
237
  daemonId: value.daemonId,
231
238
  daemonName: value.daemonName ?? value.daemonId,
232
- token: value.token
239
+ token: value.token,
240
+ userEmail: value.userEmail
233
241
  });
234
242
  }
235
243
  }
@@ -265,11 +273,21 @@ function saveDaemonProfile(input2) {
265
273
  server,
266
274
  daemonId: input2.daemonId,
267
275
  daemonName: input2.daemonName ?? input2.daemonId,
268
- token: input2.token
276
+ token: input2.token,
277
+ userEmail: input2.userEmail
269
278
  };
270
279
  writeYaml([...remaining, next]);
271
280
  return next;
272
281
  }
282
+ function removeDaemonProfile(serverOrAlias) {
283
+ const target = findDaemonProfile(serverOrAlias);
284
+ if (!target) return false;
285
+ const remaining = loadDaemonProfiles().filter(
286
+ (p) => p.alias !== target.alias
287
+ );
288
+ writeYaml(remaining);
289
+ return true;
290
+ }
273
291
 
274
292
  // src/commands/daemon-agent-config.ts
275
293
  import { spawnSync } from "child_process";
@@ -473,7 +491,557 @@ async function daemonSelfRequest(server, token, path, opts) {
473
491
  return data;
474
492
  }
475
493
 
494
+ // src/rpc/registry.ts
495
+ var RpcRegistry = class {
496
+ handlers = /* @__PURE__ */ new Map();
497
+ register(op, handler) {
498
+ if (this.handlers.has(op)) {
499
+ throw new Error(`RPC op already registered: ${op}`);
500
+ }
501
+ this.handlers.set(op, handler);
502
+ }
503
+ get(op) {
504
+ return this.handlers.get(op);
505
+ }
506
+ has(op) {
507
+ return this.handlers.has(op);
508
+ }
509
+ };
510
+ var InternalRpcContext = class {
511
+ constructor(appName, rpcId, ws) {
512
+ this.appName = appName;
513
+ this.rpcId = rpcId;
514
+ this.ws = ws;
515
+ }
516
+ appName;
517
+ rpcId;
518
+ ws;
519
+ cancelled = false;
520
+ inputCallback = null;
521
+ cancelCallback = null;
522
+ pushChunk(payload) {
523
+ sendJson(this.ws, {
524
+ type: "daemon_rpc_stream",
525
+ rpcId: this.rpcId,
526
+ payload
527
+ });
528
+ }
529
+ isCancelled() {
530
+ return this.cancelled;
531
+ }
532
+ onInput(callback) {
533
+ this.inputCallback = callback;
534
+ }
535
+ onCancel(callback) {
536
+ this.cancelCallback = callback;
537
+ }
538
+ cancel() {
539
+ if (this.cancelled) return;
540
+ this.cancelled = true;
541
+ const cb = this.cancelCallback;
542
+ this.cancelCallback = null;
543
+ if (cb) {
544
+ try {
545
+ cb();
546
+ } catch {
547
+ }
548
+ }
549
+ }
550
+ receiveInput(payload) {
551
+ this.inputCallback?.(payload);
552
+ }
553
+ };
554
+ var RpcDispatcher = class {
555
+ constructor(registry, ws) {
556
+ this.registry = registry;
557
+ this.ws = ws;
558
+ }
559
+ registry;
560
+ ws;
561
+ inflight = /* @__PURE__ */ new Map();
562
+ /** Handle a `daemon_rpc_request` frame. */
563
+ async dispatchRequest(frame) {
564
+ const handler = this.registry.get(frame.op);
565
+ if (!handler) {
566
+ this.sendResponse(frame.rpcId, {
567
+ ok: false,
568
+ error: `unknown daemon RPC op: ${frame.op}`
569
+ });
570
+ return;
571
+ }
572
+ const ctx = new InternalRpcContext(
573
+ frame.appName ?? "unknown",
574
+ frame.rpcId,
575
+ this.ws
576
+ );
577
+ this.inflight.set(frame.rpcId, { ctx });
578
+ try {
579
+ const result = await handler(frame.params ?? {}, ctx);
580
+ if (this.inflight.has(frame.rpcId)) {
581
+ this.sendResponse(frame.rpcId, { ok: true, result });
582
+ }
583
+ } catch (e) {
584
+ if (this.inflight.has(frame.rpcId)) {
585
+ this.sendResponse(frame.rpcId, {
586
+ ok: false,
587
+ error: e?.message ?? String(e)
588
+ });
589
+ }
590
+ } finally {
591
+ this.inflight.delete(frame.rpcId);
592
+ }
593
+ }
594
+ /** Handle a `daemon_rpc_input` frame. */
595
+ dispatchInput(frame) {
596
+ const call = this.inflight.get(frame.rpcId);
597
+ if (!call) return;
598
+ call.ctx.receiveInput(frame.payload);
599
+ }
600
+ /** Handle a `daemon_rpc_cancel` frame. */
601
+ dispatchCancel(frame) {
602
+ const call = this.inflight.get(frame.rpcId);
603
+ if (!call) return;
604
+ call.ctx.cancel();
605
+ }
606
+ /** Cancel everything (called on socket close). */
607
+ shutdown() {
608
+ for (const [, call] of this.inflight) {
609
+ call.ctx.cancel();
610
+ }
611
+ this.inflight.clear();
612
+ }
613
+ sendResponse(rpcId, body) {
614
+ sendJson(this.ws, { type: "daemon_rpc_response", rpcId, ...body });
615
+ }
616
+ };
617
+ function sendJson(ws, frame) {
618
+ if (ws.readyState !== ws.OPEN) return;
619
+ ws.send(JSON.stringify(frame));
620
+ }
621
+
622
+ // src/rpc/file-handlers.ts
623
+ import { Buffer as Buffer2 } from "buffer";
624
+ import {
625
+ promises as fsp,
626
+ realpathSync,
627
+ statSync
628
+ } from "fs";
629
+ import { homedir as homedir3 } from "os";
630
+ import { dirname, isAbsolute, join as join3, relative, resolve as resolve3, sep } from "path";
631
+ function registerFileHandlers(registry) {
632
+ registry.register("file.tree", fileTree);
633
+ registry.register("file.read", fileRead);
634
+ registry.register("file.write", fileWrite);
635
+ registry.register("file.stat", fileStat);
636
+ registry.register("file.mkdir", fileMkdir);
637
+ registry.register("file.delete", fileDelete);
638
+ registry.register("file.rename", fileRename);
639
+ registry.register("fs.browse", fsBrowse);
640
+ registry.register("fs.home", fsHome);
641
+ registry.register("fs.mkdir", fsMkdir);
642
+ }
643
+ function expandHome(p) {
644
+ if (p === "~") return homedir3();
645
+ if (p.startsWith("~/")) return join3(homedir3(), p.slice(2));
646
+ return p;
647
+ }
648
+ function resolveRoot(root) {
649
+ if (typeof root !== "string" || !root.trim()) {
650
+ throw new Error("file.*: `root` is required");
651
+ }
652
+ const expanded = expandHome(root);
653
+ if (!isAbsolute(expanded)) {
654
+ throw new Error(`file.*: \`root\` must be absolute (got "${root}")`);
655
+ }
656
+ try {
657
+ return realpathSync(expanded);
658
+ } catch {
659
+ return resolve3(expanded);
660
+ }
661
+ }
662
+ function jailedPath(root, sub) {
663
+ const canonicalRoot = resolveRoot(root);
664
+ const candidate = sub == null || sub === "" ? canonicalRoot : isAbsolute(sub) ? sub : resolve3(canonicalRoot, sub);
665
+ let realBase = candidate;
666
+ let tail = "";
667
+ while (true) {
668
+ try {
669
+ realBase = realpathSync(realBase);
670
+ break;
671
+ } catch {
672
+ const parent = dirname(realBase);
673
+ if (parent === realBase) {
674
+ realBase = candidate;
675
+ tail = "";
676
+ break;
677
+ }
678
+ tail = tail ? join3(realBase.slice(parent.length + 1), tail) : realBase.slice(parent.length + 1);
679
+ realBase = parent;
680
+ }
681
+ }
682
+ const final = tail ? join3(realBase, tail) : realBase;
683
+ const rel = relative(canonicalRoot, final);
684
+ if (rel.startsWith("..") || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
685
+ throw new Error(`file.*: path "${sub ?? ""}" escapes workspace root "${root}"`);
686
+ }
687
+ return final;
688
+ }
689
+ var DEFAULT_IGNORE = /* @__PURE__ */ new Set([
690
+ "node_modules",
691
+ ".git",
692
+ ".next",
693
+ ".turbo",
694
+ "dist",
695
+ "build",
696
+ ".venv",
697
+ "__pycache__",
698
+ ".DS_Store"
699
+ ]);
700
+ async function fileTree(params) {
701
+ const root = resolveRoot(params.root);
702
+ const start = jailedPath(params.root, params.path);
703
+ const depth = Math.max(0, params.depth ?? 1);
704
+ const ignore = /* @__PURE__ */ new Set([...DEFAULT_IGNORE, ...params.ignore ?? []]);
705
+ const out = [];
706
+ async function walk2(absDir, level) {
707
+ let entries;
708
+ try {
709
+ entries = await fsp.readdir(absDir, {
710
+ withFileTypes: true,
711
+ encoding: "utf8"
712
+ });
713
+ } catch (e) {
714
+ if (e?.code === "ENOENT" || e?.code === "ENOTDIR") return;
715
+ throw e;
716
+ }
717
+ entries.sort((a, b) => {
718
+ if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
719
+ return a.name.localeCompare(b.name);
720
+ });
721
+ for (const entry of entries) {
722
+ if (ignore.has(entry.name)) continue;
723
+ const abs = join3(absDir, entry.name);
724
+ const rel = relative(root, abs);
725
+ let stat = null;
726
+ try {
727
+ stat = await fsp.stat(abs);
728
+ } catch {
729
+ continue;
730
+ }
731
+ const node = {
732
+ name: entry.name,
733
+ path: rel,
734
+ isDir: entry.isDirectory(),
735
+ size: entry.isFile() ? stat.size : void 0,
736
+ mtimeMs: stat.mtimeMs
737
+ };
738
+ out.push(node);
739
+ if (entry.isDirectory() && level < depth) {
740
+ await walk2(abs, level + 1);
741
+ }
742
+ }
743
+ }
744
+ await walk2(start, 1);
745
+ return out;
746
+ }
747
+ async function fileRead(params) {
748
+ const abs = jailedPath(params.root, params.path);
749
+ const maxBytes = params.maxBytes ?? 5 * 1024 * 1024;
750
+ const stat = await fsp.stat(abs);
751
+ if (!stat.isFile()) {
752
+ throw new Error(`file.read: not a file: ${params.path}`);
753
+ }
754
+ if (stat.size > maxBytes) {
755
+ throw new Error(
756
+ `file.read: file too large (${stat.size} bytes > ${maxBytes}). Pass maxBytes to override or chunk the read.`
757
+ );
758
+ }
759
+ const buf = await fsp.readFile(abs);
760
+ const encoding = params.encoding ?? "utf8";
761
+ return {
762
+ path: params.path,
763
+ content: encoding === "base64" ? buf.toString("base64") : buf.toString("utf8"),
764
+ encoding,
765
+ size: stat.size,
766
+ mtimeMs: stat.mtimeMs
767
+ };
768
+ }
769
+ async function fileWrite(params) {
770
+ const abs = jailedPath(params.root, params.path);
771
+ if (params.createDirs !== false) {
772
+ await fsp.mkdir(dirname(abs), { recursive: true });
773
+ }
774
+ const buf = (params.encoding ?? "utf8") === "base64" ? Buffer2.from(params.content, "base64") : Buffer2.from(params.content, "utf8");
775
+ await fsp.writeFile(abs, buf);
776
+ const stat = await fsp.stat(abs);
777
+ return { path: params.path, size: stat.size, mtimeMs: stat.mtimeMs };
778
+ }
779
+ async function fileStat(params) {
780
+ let abs;
781
+ try {
782
+ abs = params.path == null || params.path === "" ? expandHome(params.root) : jailedPath(params.root, params.path);
783
+ } catch (e) {
784
+ if (/escapes workspace root/.test(e?.message ?? "")) throw e;
785
+ return { exists: false, isFile: false, isDir: false, size: 0, mtimeMs: 0 };
786
+ }
787
+ try {
788
+ const stat = statSync(abs);
789
+ return {
790
+ exists: true,
791
+ isFile: stat.isFile(),
792
+ isDir: stat.isDirectory(),
793
+ size: stat.size,
794
+ mtimeMs: stat.mtimeMs
795
+ };
796
+ } catch {
797
+ return { exists: false, isFile: false, isDir: false, size: 0, mtimeMs: 0 };
798
+ }
799
+ }
800
+ async function fileMkdir(params) {
801
+ const abs = jailedPath(params.root, params.path);
802
+ await fsp.mkdir(abs, { recursive: true });
803
+ return { path: params.path };
804
+ }
805
+ async function fileDelete(params) {
806
+ const abs = jailedPath(params.root, params.path);
807
+ await fsp.rm(abs, { recursive: !!params.recursive, force: false });
808
+ return { path: params.path };
809
+ }
810
+ async function fileRename(params) {
811
+ const fromAbs = jailedPath(params.root, params.from);
812
+ const toAbs = jailedPath(params.root, params.to);
813
+ await fsp.rename(fromAbs, toAbs);
814
+ return { from: params.from, to: params.to };
815
+ }
816
+ async function fsBrowse(params) {
817
+ const expanded = expandHome(params.path ?? "~");
818
+ if (!isAbsolute(expanded)) {
819
+ throw new Error(`fs.browse: path must be absolute (got "${params.path}")`);
820
+ }
821
+ let resolved;
822
+ try {
823
+ resolved = realpathSync(expanded);
824
+ } catch {
825
+ resolved = resolve3(expanded);
826
+ }
827
+ let raw;
828
+ try {
829
+ raw = await fsp.readdir(resolved, {
830
+ withFileTypes: true,
831
+ encoding: "utf8"
832
+ });
833
+ } catch (e) {
834
+ throw new Error(`fs.browse: cannot read ${resolved}: ${e?.code ?? e?.message ?? e}`);
835
+ }
836
+ const entries = [];
837
+ for (const entry of raw) {
838
+ if (!params.showHidden && entry.name.startsWith(".")) continue;
839
+ const isDir = entry.isDirectory();
840
+ if (!isDir && !params.includeFiles) continue;
841
+ entries.push({
842
+ name: entry.name,
843
+ path: join3(resolved, entry.name),
844
+ isDir
845
+ });
846
+ }
847
+ entries.sort((a, b) => {
848
+ if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
849
+ return a.name.localeCompare(b.name);
850
+ });
851
+ const parent = dirname(resolved);
852
+ return {
853
+ path: resolved,
854
+ parent: parent === resolved ? null : parent,
855
+ entries
856
+ };
857
+ }
858
+ async function fsHome() {
859
+ return { home: homedir3(), cwd: process.cwd() };
860
+ }
861
+ async function fsMkdir(params) {
862
+ const expanded = expandHome(params.path ?? "");
863
+ if (!expanded || !isAbsolute(expanded)) {
864
+ throw new Error(`fs.mkdir: path must be absolute (got "${params.path}")`);
865
+ }
866
+ await fsp.mkdir(expanded, { recursive: true });
867
+ return { path: expanded };
868
+ }
869
+
870
+ // src/rpc/pty-handlers.ts
871
+ import { spawn as spawn2 } from "child_process";
872
+ import { randomUUID } from "crypto";
873
+ import { existsSync as existsSync4, statSync as statSync2 } from "fs";
874
+ import { homedir as homedir4, platform } from "os";
875
+ import { join as join4 } from "path";
876
+ import { isAbsolute as isAbsolute2 } from "path";
877
+ var handles = /* @__PURE__ */ new Map();
878
+ function registerPtyHandlers(registry) {
879
+ registry.register("pty.spawn", ptySpawn);
880
+ registry.register("pty.write", ptyWrite);
881
+ registry.register("pty.resize", ptyResize);
882
+ registry.register("pty.kill", ptyKill);
883
+ }
884
+ async function ptySpawn(params, ctx) {
885
+ if (!params.cwd || typeof params.cwd !== "string" || !isAbsolute2(params.cwd)) {
886
+ throw new Error("pty.spawn: `cwd` is required and must be absolute");
887
+ }
888
+ let cwdStat;
889
+ try {
890
+ cwdStat = statSync2(params.cwd);
891
+ } catch {
892
+ throw new Error(`pty.spawn: cwd does not exist: ${params.cwd}`);
893
+ }
894
+ if (!cwdStat.isDirectory()) {
895
+ throw new Error(`pty.spawn: cwd is not a directory: ${params.cwd}`);
896
+ }
897
+ const command = params.command || defaultShell();
898
+ if (!existsSync4(command)) {
899
+ throw new Error(
900
+ `pty.spawn: shell binary not found: ${command} (set $SHELL in the daemon's environment, or pass an explicit \`command\`)`
901
+ );
902
+ }
903
+ const args = params.args ?? defaultShellArgs();
904
+ const env = { TERM: "xterm-256color" };
905
+ for (const [k, v] of Object.entries(process.env)) {
906
+ if (typeof v === "string") env[k] = v;
907
+ }
908
+ for (const [k, v] of Object.entries(params.env ?? {})) {
909
+ env[k] = v;
910
+ }
911
+ const cols = params.cols ?? 80;
912
+ const rows = params.rows ?? 24;
913
+ const handle = await openPty({ command, args, cwd: params.cwd, env, cols, rows, ctx });
914
+ handles.set(handle.handle.ptyId, handle.handle);
915
+ ctx.onInput((payload) => {
916
+ if (typeof payload === "string") handle.handle.write(payload);
917
+ else if (payload && typeof payload.data === "string") {
918
+ handle.handle.write(payload.data);
919
+ }
920
+ });
921
+ ctx.onCancel(() => {
922
+ handle.handle.kill("SIGTERM");
923
+ setTimeout(() => {
924
+ try {
925
+ handle.handle.kill("SIGKILL");
926
+ } catch {
927
+ }
928
+ }, 5e3).unref();
929
+ });
930
+ ctx.pushChunk({ kind: "mode", mode: handle.mode, ptyId: handle.handle.ptyId });
931
+ const exitCode = await handle.exited;
932
+ handles.delete(handle.handle.ptyId);
933
+ return { ptyId: handle.handle.ptyId, exitCode, mode: handle.mode };
934
+ }
935
+ async function ptyWrite(params) {
936
+ const handle = handles.get(params.ptyId);
937
+ if (!handle) throw new Error(`pty.write: unknown ptyId ${params.ptyId}`);
938
+ handle.write(params.data);
939
+ return { ok: true };
940
+ }
941
+ async function ptyResize(params) {
942
+ const handle = handles.get(params.ptyId);
943
+ if (!handle) throw new Error(`pty.resize: unknown ptyId ${params.ptyId}`);
944
+ handle.resize(params.cols, params.rows);
945
+ return { ok: true };
946
+ }
947
+ async function ptyKill(params) {
948
+ const handle = handles.get(params.ptyId);
949
+ if (!handle) throw new Error(`pty.kill: unknown ptyId ${params.ptyId}`);
950
+ handle.kill(params.signal ?? "SIGTERM");
951
+ return { ok: true };
952
+ }
953
+ async function openPty(opts) {
954
+ const nodePty = await loadNodePty();
955
+ if (nodePty) {
956
+ const proc2 = nodePty.spawn(opts.command, opts.args, {
957
+ name: "xterm-256color",
958
+ cols: opts.cols,
959
+ rows: opts.rows,
960
+ cwd: opts.cwd,
961
+ env: opts.env
962
+ });
963
+ const ptyId2 = randomUUID();
964
+ proc2.onData((data) => {
965
+ opts.ctx.pushChunk({ kind: "data", data });
966
+ });
967
+ const exited2 = new Promise((resolve11) => {
968
+ proc2.onExit(({ exitCode }) => {
969
+ resolve11(exitCode);
970
+ });
971
+ });
972
+ return {
973
+ mode: "pty",
974
+ exited: exited2,
975
+ handle: {
976
+ ptyId: ptyId2,
977
+ resize: (cols, rows) => proc2.resize(cols, rows),
978
+ write: (data) => proc2.write(data),
979
+ kill: (signal) => proc2.kill(signal)
980
+ }
981
+ };
982
+ }
983
+ const proc = spawn2(
984
+ opts.command,
985
+ opts.args,
986
+ {
987
+ cwd: opts.cwd,
988
+ env: opts.env,
989
+ stdio: ["pipe", "pipe", "pipe"]
990
+ }
991
+ );
992
+ const ptyId = randomUUID();
993
+ proc.stdout.on("data", (chunk) => {
994
+ opts.ctx.pushChunk({ kind: "data", data: chunk.toString("utf8") });
995
+ });
996
+ proc.stderr.on("data", (chunk) => {
997
+ opts.ctx.pushChunk({ kind: "data", data: chunk.toString("utf8") });
998
+ });
999
+ const exited = new Promise((resolve11) => {
1000
+ proc.on("close", (code) => resolve11(code));
1001
+ });
1002
+ return {
1003
+ mode: "pipe",
1004
+ exited,
1005
+ handle: {
1006
+ ptyId,
1007
+ resize: () => {
1008
+ },
1009
+ write: (data) => {
1010
+ proc.stdin.write(data);
1011
+ },
1012
+ kill: (signal) => {
1013
+ proc.kill(signal ?? "SIGTERM");
1014
+ }
1015
+ }
1016
+ };
1017
+ }
1018
+ var nodePtyCache = void 0;
1019
+ async function loadNodePty() {
1020
+ if (nodePtyCache !== void 0) return nodePtyCache;
1021
+ try {
1022
+ const moduleName = "node-pty";
1023
+ nodePtyCache = await import(moduleName);
1024
+ return nodePtyCache;
1025
+ } catch {
1026
+ nodePtyCache = null;
1027
+ return null;
1028
+ }
1029
+ }
1030
+ function defaultShell() {
1031
+ if (platform() === "win32") {
1032
+ return process.env.COMSPEC || "cmd.exe";
1033
+ }
1034
+ return process.env.SHELL || "/bin/bash";
1035
+ }
1036
+ function defaultShellArgs() {
1037
+ if (platform() === "win32") return [];
1038
+ return ["-l", "-i"];
1039
+ }
1040
+
476
1041
  // src/commands/daemon.ts
1042
+ var rpcRegistry = new RpcRegistry();
1043
+ registerFileHandlers(rpcRegistry);
1044
+ registerPtyHandlers(rpcRegistry);
477
1045
  var daemonCommand = new Command3("daemon").description("Manage and run the local botapp daemon");
478
1046
  daemonCommand.command("run").description(
479
1047
  "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."
@@ -517,9 +1085,31 @@ function pickProfilesToRun(alias, server) {
517
1085
  }
518
1086
  return loadDaemonProfiles();
519
1087
  }
1088
+ daemonCommand.command("unpair").description("Remove a paired profile from ~/.botapp/daemon.yaml (does not touch the server)").argument("<aliasOrServer>", "Profile alias (e.g. local) or server URL").action((aliasOrServer) => {
1089
+ const removed = removeDaemonProfile(aliasOrServer);
1090
+ if (!removed) {
1091
+ console.error(pc3.red(`No paired profile matching "${aliasOrServer}".`));
1092
+ process.exitCode = 1;
1093
+ return;
1094
+ }
1095
+ console.log(pc3.green(`Removed daemon profile: ${aliasOrServer}`));
1096
+ });
1097
+ daemonCommand.command("list").alias("ls").description("List paired daemon profiles from ~/.botapp/daemon.yaml").action(() => {
1098
+ const profiles = loadDaemonProfiles();
1099
+ if (profiles.length === 0) {
1100
+ console.log(pc3.dim("No paired daemons. Run `bot pair` to add one."));
1101
+ return;
1102
+ }
1103
+ for (const p of profiles) {
1104
+ console.log(pc3.bold(p.alias ?? p.server));
1105
+ console.log(` Server: ${p.server}`);
1106
+ console.log(` Daemon: ${p.daemonName} ${pc3.dim(`(${p.daemonId})`)}`);
1107
+ console.log(` User: ${p.userEmail ?? pc3.dim("unknown \u2014 re-pair to capture")}`);
1108
+ }
1109
+ });
520
1110
  daemonCommand.command("stop").description("Stop the background daemon started by `bot launch`").action(async () => {
521
- const pidFile = join3(homedir3(), ".botapp", "daemon.pid");
522
- if (!existsSync4(pidFile)) {
1111
+ const pidFile = join5(homedir5(), ".botapp", "daemon.pid");
1112
+ if (!existsSync5(pidFile)) {
523
1113
  console.log(pc3.yellow("No background daemon PID file found."));
524
1114
  return;
525
1115
  }
@@ -545,16 +1135,17 @@ daemonCommand.command("agent").description("Manage agents hosted by this daemon"
545
1135
  for (const agent of data.agents ?? []) {
546
1136
  console.log(`${pc3.bold(agent.name)} ${pc3.dim(`(${agent.id})`)}`);
547
1137
  console.log(` Kind: ${agent.kind}`);
1138
+ if (agent.model) console.log(` Model: ${agent.model}`);
548
1139
  console.log(` Command: ${agent.command} ${(agent.args ?? []).join(" ")}`);
549
1140
  if (agent.cwd) console.log(` CWD: ${agent.cwd}`);
550
1141
  }
551
1142
  })
552
1143
  ).addCommand(createDaemonAgentConfigCommand()).addCommand(
553
- new Command3("add").description("Register a local agent command").argument("<name>", "Name, e.g. codex, claude-code, openclaw, hermes, or hermes-agent").requiredOption("--command <command>", "Executable command").option(
1144
+ new Command3("add").description("Register a local agent command").argument("<name>", "Name, e.g. codex, claude-code, kimi, openclaw, hermes, or hermes-agent").requiredOption("--command <command>", "Executable command").option(
554
1145
  "--kind <kind>",
555
- "Agent adapter kind: acp, codex, claude-code, openclaw, hermes, hermes-agent, or shell",
1146
+ "Agent adapter kind: acp, codex, claude-code, kimi, openclaw, hermes, hermes-agent, or shell",
556
1147
  "acp"
557
- ).option("--arg <arg...>", "Argument passed to the executable").option("--cwd <cwd>", "Working directory for the agent process").option("--env <entry...>", "Environment entries in KEY=VALUE form").option("--profile <alias>", "Daemon profile alias to register against").option("--server <url>", "Daemon profile by server URL").action(async (name, opts) => {
1148
+ ).option("--arg <arg...>", "Argument passed to the executable").option("--cwd <cwd>", "Working directory for the agent process").option("--env <entry...>", "Environment entries in KEY=VALUE form").option("--model <model>", "Provider model to pass to the agent runtime").option("--profile <alias>", "Daemon profile alias to register against").option("--server <url>", "Daemon profile by server URL").action(async (name, opts) => {
558
1149
  const profile = requireSelectedProfile(opts);
559
1150
  if (!profile) return;
560
1151
  const env = parseEnv(opts.env ?? []);
@@ -570,12 +1161,14 @@ daemonCommand.command("agent").description("Manage agents hosted by this daemon"
570
1161
  command: opts.command,
571
1162
  args: opts.arg ?? [],
572
1163
  cwd: opts.cwd,
573
- env
1164
+ env,
1165
+ model: opts.model
574
1166
  }
575
1167
  }
576
1168
  );
577
1169
  console.log(pc3.green(`Registered daemon agent: ${pc3.bold(data.agent.name)}`));
578
1170
  console.log(` ID: ${data.agent.id}`);
1171
+ if (data.agent.model) console.log(` Model: ${data.agent.model}`);
579
1172
  console.log(` Command: ${data.agent.command} ${(data.agent.args ?? []).join(" ")}`);
580
1173
  })
581
1174
  ).addCommand(
@@ -647,6 +1240,7 @@ function runDaemonSocket(wsUrl, setActiveWs) {
647
1240
  return new Promise((resolveRun) => {
648
1241
  const ws = new WebSocket(wsUrl);
649
1242
  setActiveWs(ws);
1243
+ const rpcDispatcher = new RpcDispatcher(rpcRegistry, ws);
650
1244
  let opened = false;
651
1245
  let superseded = false;
652
1246
  let settled = false;
@@ -655,6 +1249,7 @@ function runDaemonSocket(wsUrl, setActiveWs) {
655
1249
  if (settled) return;
656
1250
  settled = true;
657
1251
  if (ping) clearInterval(ping);
1252
+ rpcDispatcher.shutdown();
658
1253
  resolveRun({ opened, superseded });
659
1254
  }
660
1255
  ws.on("open", () => {
@@ -666,7 +1261,7 @@ function runDaemonSocket(wsUrl, setActiveWs) {
666
1261
  }, 3e4);
667
1262
  });
668
1263
  ws.on("message", (raw) => {
669
- void handleFrame(ws, raw.toString());
1264
+ void handleFrame(ws, raw.toString(), rpcDispatcher);
670
1265
  });
671
1266
  ws.on("close", (code, reason) => {
672
1267
  if (code === 4e3) superseded = true;
@@ -682,8 +1277,20 @@ function runDaemonSocket(wsUrl, setActiveWs) {
682
1277
  });
683
1278
  });
684
1279
  }
685
- async function handleFrame(ws, raw) {
1280
+ async function handleFrame(ws, raw, rpcDispatcher) {
686
1281
  const frame = JSON.parse(raw);
1282
+ if (frame.type === "daemon_rpc_request") {
1283
+ void rpcDispatcher.dispatchRequest(frame);
1284
+ return;
1285
+ }
1286
+ if (frame.type === "daemon_rpc_input") {
1287
+ rpcDispatcher.dispatchInput(frame);
1288
+ return;
1289
+ }
1290
+ if (frame.type === "daemon_rpc_cancel") {
1291
+ rpcDispatcher.dispatchCancel(frame);
1292
+ return;
1293
+ }
687
1294
  if (frame.type === "daemon_job") {
688
1295
  const job = frame.job;
689
1296
  console.log(pc3.blue(`Running ${job.agent.name} job ${job.id}`));
@@ -727,7 +1334,10 @@ async function runAgentJob(job, update) {
727
1334
  if (job.agent.kind === "claude-code" || job.agent.kind === "claude_code") {
728
1335
  return runClaudeCodeAgent(job, update);
729
1336
  }
730
- if (job.agent.kind === "openclaw" || job.agent.kind === "kimiclaw") {
1337
+ if (job.agent.kind === "kimi") {
1338
+ return runKimiCodeAgent(job, update);
1339
+ }
1340
+ if (job.agent.kind === "openclaw") {
731
1341
  return runOpenClawAgent(job, update);
732
1342
  }
733
1343
  if (job.agent.kind === "hermes" || job.agent.kind === "hermes-agent") {
@@ -739,7 +1349,7 @@ async function runAgentJob(job, update) {
739
1349
  return runAcpAgent(job);
740
1350
  }
741
1351
  async function runShellAgent(job) {
742
- const child = spawn2(job.agent.command, [...job.agent.args, job.query], {
1352
+ const child = spawn3(job.agent.command, [...job.agent.args, job.query], {
743
1353
  cwd: job.agent.cwd ?? process.cwd(),
744
1354
  env: { ...process.env, ...job.agent.env ?? {} }
745
1355
  });
@@ -751,7 +1361,7 @@ async function runShellAgent(job) {
751
1361
  child.stderr.on("data", (chunk) => {
752
1362
  stderr += chunk.toString();
753
1363
  });
754
- const code = await new Promise((resolve7) => child.on("close", resolve7));
1364
+ const code = await new Promise((resolve11) => child.on("close", resolve11));
755
1365
  if (code !== 0) {
756
1366
  throw new Error(stderr.trim() || `Agent exited with code ${code}`);
757
1367
  }
@@ -768,6 +1378,9 @@ async function runCodexAgent(job, update) {
768
1378
  if (!args.includes("--skip-git-repo-check")) {
769
1379
  args.push("--skip-git-repo-check");
770
1380
  }
1381
+ if (job.agent.model && !hasAnyFlag(args, ["--model", "-m"])) {
1382
+ args.push("--model", job.agent.model);
1383
+ }
771
1384
  if (!hasAnyFlag(args, [
772
1385
  "--sandbox",
773
1386
  "-s",
@@ -779,7 +1392,7 @@ async function runCodexAgent(job, update) {
779
1392
  args.push("--dangerously-bypass-approvals-and-sandbox");
780
1393
  }
781
1394
  args.push(job.query);
782
- const child = spawn2(job.agent.command, args, {
1395
+ const child = spawn3(job.agent.command, args, {
783
1396
  cwd: job.agent.cwd ?? process.cwd(),
784
1397
  env: { ...process.env, ...job.agent.env ?? {} },
785
1398
  stdio: ["ignore", "pipe", "pipe"]
@@ -796,12 +1409,19 @@ async function runCodexAgent(job, update) {
796
1409
  rawEvents: []
797
1410
  };
798
1411
  const toolCalls = /* @__PURE__ */ new Map();
1412
+ const errors = [];
799
1413
  function processLine(line) {
800
1414
  if (!line.trim()) return;
801
1415
  try {
802
1416
  const event = JSON.parse(line);
803
1417
  result.rawEvents.push(event);
804
1418
  update({ kind: "codex_event", event });
1419
+ if (event?.type === "error" && typeof event.message === "string") {
1420
+ errors.push(event.message);
1421
+ }
1422
+ if (event?.type === "turn.failed" && typeof event.error?.message === "string") {
1423
+ errors.push(event.error.message);
1424
+ }
805
1425
  if (event?.type === "item.completed" && event.item?.type === "agent_message") {
806
1426
  if (typeof event.item.text === "string") {
807
1427
  result.messages.push(event.item.text);
@@ -829,9 +1449,9 @@ async function runCodexAgent(job, update) {
829
1449
  }
830
1450
  const rl = createInterface2({ input: child.stdout });
831
1451
  rl.on("line", processLine);
832
- const code = await new Promise((resolve7) => child.on("close", resolve7));
1452
+ const code = await new Promise((resolve11) => child.on("close", resolve11));
833
1453
  if (code !== 0) {
834
- throw new Error(stderr.trim() || `Codex exited with code ${code}`);
1454
+ throw new Error(errors.at(-1) ?? (stderr.trim() || `Codex exited with code ${code}`));
835
1455
  }
836
1456
  result.text = result.messages.at(-1)?.trim() || "";
837
1457
  result.toolCalls = [...toolCalls.values()];
@@ -848,6 +1468,9 @@ async function runClaudeCodeAgent(job, update) {
848
1468
  if (!args.includes("--verbose")) {
849
1469
  args.push("--verbose");
850
1470
  }
1471
+ if (job.agent.model && !hasAnyFlag(args, ["--model"])) {
1472
+ args.push("--model", job.agent.model);
1473
+ }
851
1474
  if (!hasAnyFlag(args, [
852
1475
  "--permission-mode",
853
1476
  "--dangerously-skip-permissions",
@@ -859,7 +1482,7 @@ async function runClaudeCodeAgent(job, update) {
859
1482
  if (resume && !args.includes("--resume")) {
860
1483
  args.push("--resume", resume);
861
1484
  }
862
- const child = spawn2(job.agent.command, args, {
1485
+ const child = spawn3(job.agent.command, args, {
863
1486
  cwd: job.agent.cwd ?? process.cwd(),
864
1487
  env: { ...process.env, ...job.agent.env ?? {} },
865
1488
  stdio: ["pipe", "pipe", "pipe"]
@@ -870,6 +1493,9 @@ async function runClaudeCodeAgent(job, update) {
870
1493
  child.stderr.on("data", (chunk) => {
871
1494
  stderr += chunk.toString();
872
1495
  });
1496
+ const noiseLines = [];
1497
+ const errorEvents = [];
1498
+ const NOISE_TAIL = 6;
873
1499
  const result = {
874
1500
  kind: "claude-code",
875
1501
  text: "",
@@ -927,11 +1553,19 @@ async function runClaudeCodeAgent(job, update) {
927
1553
  try {
928
1554
  event = JSON.parse(line);
929
1555
  } catch {
1556
+ noiseLines.push(line);
1557
+ if (noiseLines.length > NOISE_TAIL) noiseLines.shift();
930
1558
  return;
931
1559
  }
932
1560
  result.rawEvents.push(event);
933
1561
  update({ kind: "claude_code_event", event });
934
1562
  captureSessionId(event);
1563
+ if (event && (event.is_error === true || event.subtype === "error" || event.type === "result" && event.is_error)) {
1564
+ try {
1565
+ errorEvents.push(JSON.stringify(event).slice(0, 1e3));
1566
+ } catch {
1567
+ }
1568
+ }
935
1569
  if (event.type === "assistant" && event.message?.content) {
936
1570
  ingestAssistantContent(
937
1571
  Array.isArray(event.message.content) ? event.message.content : []
@@ -950,9 +1584,148 @@ async function runClaudeCodeAgent(job, update) {
950
1584
  }
951
1585
  const rl = createInterface2({ input: child.stdout });
952
1586
  rl.on("line", processLine);
953
- const code = await new Promise((resolve7) => child.on("close", resolve7));
1587
+ const code = await new Promise((resolve11) => child.on("close", resolve11));
1588
+ if (code !== 0) {
1589
+ const parts = [];
1590
+ if (stderr.trim()) parts.push(stderr.trim());
1591
+ if (errorEvents.length > 0) {
1592
+ parts.push(`Error events: ${errorEvents.join(" | ")}`);
1593
+ }
1594
+ if (noiseLines.length > 0 && parts.length === 0) {
1595
+ parts.push(`Last stdout lines: ${noiseLines.join(" | ").slice(-1500)}`);
1596
+ }
1597
+ if (parts.length === 0) {
1598
+ parts.push(`Claude Code exited with code ${code} (no stderr or stdout output)`);
1599
+ }
1600
+ throw new Error(parts.join(" \u2014 "));
1601
+ }
1602
+ if (!result.text) {
1603
+ result.text = result.messages.at(-1)?.trim() ?? "";
1604
+ }
1605
+ result.toolCalls = [...toolCalls.values()];
1606
+ return JSON.stringify(result);
1607
+ }
1608
+ async function runKimiCodeAgent(job, update) {
1609
+ const args = [...job.agent.args];
1610
+ if (!args.includes("--print") && !args.includes("-p")) {
1611
+ args.push("--print");
1612
+ }
1613
+ if (!args.includes("--output-format")) {
1614
+ args.push("--output-format", "stream-json");
1615
+ }
1616
+ if (!hasAnyFlag(args, ["--yolo", "--yes", "-y"])) {
1617
+ args.push("--yolo");
1618
+ }
1619
+ if (job.agent.model && !hasAnyFlag(args, ["--model", "-m"])) {
1620
+ args.push("--model", job.agent.model);
1621
+ }
1622
+ const resume = job.resumeSessionId ?? job.agent.env?.KIMI_SESSION_ID ?? null;
1623
+ if (resume && !hasAnyFlag(args, ["--session", "-S", "-r", "--resume", "--continue", "-C"])) {
1624
+ args.push("-r", resume);
1625
+ }
1626
+ const cwd = job.agent.cwd ?? process.cwd();
1627
+ let prompt = job.query;
1628
+ if (!resume) {
1629
+ const agentsMdPath = join5(cwd, "AGENTS.md");
1630
+ if (existsSync5(agentsMdPath)) {
1631
+ try {
1632
+ const primer = readFileSync3(agentsMdPath, "utf-8").trim();
1633
+ if (primer) {
1634
+ prompt = `# System instructions (read carefully before responding)
1635
+
1636
+ ${primer}
1637
+
1638
+ ---
1639
+
1640
+ # Your task
1641
+
1642
+ ${job.query}`;
1643
+ }
1644
+ } catch {
1645
+ }
1646
+ }
1647
+ }
1648
+ const child = spawn3(job.agent.command, args, {
1649
+ cwd,
1650
+ env: { ...process.env, ...job.agent.env ?? {} },
1651
+ stdio: ["pipe", "pipe", "pipe"]
1652
+ });
1653
+ child.stdin.write(prompt);
1654
+ child.stdin.end();
1655
+ let stderr = "";
1656
+ child.stderr.on("data", (chunk) => {
1657
+ stderr += chunk.toString();
1658
+ });
1659
+ const result = {
1660
+ kind: "kimi",
1661
+ text: "",
1662
+ messages: [],
1663
+ toolCalls: [],
1664
+ sessionId: resume,
1665
+ rawEvents: []
1666
+ };
1667
+ const toolCalls = /* @__PURE__ */ new Map();
1668
+ function ingestAssistantContent(blocks) {
1669
+ for (const block of blocks) {
1670
+ if (!block || typeof block !== "object") continue;
1671
+ if (block.type === "text" && typeof block.text === "string") {
1672
+ result.messages.push(block.text);
1673
+ update({ kind: "message", text: block.text });
1674
+ }
1675
+ }
1676
+ }
1677
+ function ingestToolCalls(calls) {
1678
+ for (const call of calls) {
1679
+ if (!call || typeof call !== "object") continue;
1680
+ const id = typeof call.id === "string" ? call.id : null;
1681
+ if (!id) continue;
1682
+ const name = String(call.function?.name ?? call.name ?? "tool");
1683
+ let input2 = call.function?.arguments ?? call.arguments;
1684
+ if (typeof input2 === "string") {
1685
+ try {
1686
+ input2 = JSON.parse(input2);
1687
+ } catch {
1688
+ }
1689
+ }
1690
+ const next = { ...toolCalls.get(id) ?? {}, id, name, input: input2 };
1691
+ toolCalls.set(id, next);
1692
+ update({ kind: "tool_call", toolCall: next });
1693
+ }
1694
+ }
1695
+ function ingestToolResult(event) {
1696
+ const id = event?.tool_call_id;
1697
+ if (typeof id !== "string") return;
1698
+ const existing = toolCalls.get(id) ?? { id, name: "tool" };
1699
+ const output2 = typeof event.content === "string" ? event.content : JSON.stringify(event.content);
1700
+ const next = { ...existing, output: output2 };
1701
+ toolCalls.set(id, next);
1702
+ update({ kind: "tool_call", toolCall: next });
1703
+ }
1704
+ function processLine(line) {
1705
+ const trimmed = line.trim();
1706
+ if (!trimmed || trimmed[0] !== "{") return;
1707
+ let event;
1708
+ try {
1709
+ event = JSON.parse(trimmed);
1710
+ } catch {
1711
+ return;
1712
+ }
1713
+ result.rawEvents.push(event);
1714
+ update({ kind: "kimi_event", event });
1715
+ if (event.role === "assistant") {
1716
+ if (Array.isArray(event.content)) ingestAssistantContent(event.content);
1717
+ if (Array.isArray(event.tool_calls)) ingestToolCalls(event.tool_calls);
1718
+ } else if (event.role === "tool") {
1719
+ ingestToolResult(event);
1720
+ }
1721
+ }
1722
+ const rl = createInterface2({ input: child.stdout });
1723
+ rl.on("line", processLine);
1724
+ const code = await new Promise((resolve11) => child.on("close", resolve11));
1725
+ const resumeMatch = stderr.match(/To resume this session:\s*kimi\s+-r\s+([A-Za-z0-9_-]+)/);
1726
+ if (resumeMatch) result.sessionId = resumeMatch[1];
954
1727
  if (code !== 0) {
955
- throw new Error(stderr.trim() || `Claude Code exited with code ${code}`);
1728
+ throw new Error(stderr.trim() || `Kimi Code exited with code ${code}`);
956
1729
  }
957
1730
  if (!result.text) {
958
1731
  result.text = result.messages.at(-1)?.trim() ?? "";
@@ -982,7 +1755,7 @@ async function runOpenClawAgent(job, update) {
982
1755
  }
983
1756
  let requestedSessionId = getFlagValue(args, "--session-id");
984
1757
  if (!requestedSessionId) {
985
- requestedSessionId = job.resumeSessionId || `botapp-${randomUUID()}`;
1758
+ requestedSessionId = job.resumeSessionId || `botapp-${randomUUID2()}`;
986
1759
  args.push("--session-id", requestedSessionId);
987
1760
  }
988
1761
  if (!hasFlag(args, "--verbose")) {
@@ -992,7 +1765,7 @@ async function runOpenClawAgent(job, update) {
992
1765
  const state = {
993
1766
  startedAtMs: Date.now(),
994
1767
  sessionDir,
995
- sessionStorePath: join3(sessionDir, "sessions.json"),
1768
+ sessionStorePath: join5(sessionDir, "sessions.json"),
996
1769
  requestedSessionId,
997
1770
  sessionKey: null,
998
1771
  sessionId: null,
@@ -1016,14 +1789,23 @@ async function runOpenClawAgent(job, update) {
1016
1789
  };
1017
1790
  const toolCalls = /* @__PURE__ */ new Map();
1018
1791
  let stderr = "";
1792
+ const stdoutLines = [];
1019
1793
  let stopPolling = false;
1020
- const child = spawn2(job.agent.command, args, {
1794
+ const child = spawn3(job.agent.command, args, {
1021
1795
  cwd: job.agent.cwd ?? process.cwd(),
1022
1796
  env,
1023
1797
  stdio: ["ignore", "pipe", "pipe"]
1024
1798
  });
1025
1799
  const stdout = createInterface2({ input: child.stdout });
1026
- stdout.on("line", () => {
1800
+ stdout.on("line", (line) => {
1801
+ const text = line.trim();
1802
+ if (!text) return;
1803
+ stdoutLines.push(text);
1804
+ const stdoutText = extractOpenClawStdoutText(text);
1805
+ if (!stdoutText) return;
1806
+ result.text = stdoutText;
1807
+ result.messages.push(stdoutText);
1808
+ update({ kind: "message", text: stdoutText });
1027
1809
  });
1028
1810
  const stderrReader = createInterface2({ input: child.stderr });
1029
1811
  stderrReader.on("line", (line) => {
@@ -1055,11 +1837,11 @@ async function runOpenClawAgent(job, update) {
1055
1837
  }
1056
1838
  result.sessionFile = state.selectedFile;
1057
1839
  result.toolCalls = [...toolCalls.values()];
1058
- result.text = result.text || result.messages.at(-1)?.trim() || "";
1840
+ result.text = result.text || result.messages.at(-1)?.trim() || extractOpenClawStdoutText(stdoutLines.at(-1) ?? "") || "";
1059
1841
  if (code !== 0) {
1060
1842
  throw new Error(stderr.trim() || `OpenClaw exited with code ${code}`);
1061
1843
  }
1062
- if (!result.sessionId) {
1844
+ if (!result.sessionId && !result.text && result.toolCalls.length === 0) {
1063
1845
  throw new Error(
1064
1846
  [
1065
1847
  "OpenClaw completed, but the session key was not resolved from sessions.json.",
@@ -1080,11 +1862,22 @@ async function runOpenClawAgent(job, update) {
1080
1862
  }
1081
1863
  return JSON.stringify(result);
1082
1864
  }
1865
+ function extractOpenClawStdoutText(line) {
1866
+ const text = line.trim();
1867
+ if (!text) return "";
1868
+ try {
1869
+ const parsed = JSON.parse(text);
1870
+ if (parsed && typeof parsed === "object") return JSON.stringify(parsed);
1871
+ if (typeof parsed === "string") return parsed;
1872
+ } catch {
1873
+ }
1874
+ return text;
1875
+ }
1083
1876
  function resolveOpenClawSessionDir(args, env) {
1084
1877
  const configured = env.BOTAPP_OPENCLAW_SESSION_DIR ?? env.OPENCLAW_SESSION_DIR;
1085
1878
  if (configured) return expandPath(configured);
1086
1879
  const agentName = resolveOpenClawAgentName(args);
1087
- return join3(homedir3(), ".openclaw", "agents", agentName, "sessions");
1880
+ return join5(homedir5(), ".openclaw", "agents", agentName, "sessions");
1088
1881
  }
1089
1882
  function resolveOpenClawAgentName(args) {
1090
1883
  return getFlagValue(args, "--agent") ?? "main";
@@ -1093,7 +1886,7 @@ function snapshotOpenClawSessionOffsets(sessionDir) {
1093
1886
  const offsets = /* @__PURE__ */ new Map();
1094
1887
  for (const file of listOpenClawSessionFiles(sessionDir)) {
1095
1888
  try {
1096
- offsets.set(file, statSync(file).size);
1889
+ offsets.set(file, statSync3(file).size);
1097
1890
  } catch {
1098
1891
  }
1099
1892
  }
@@ -1169,7 +1962,7 @@ function selectOpenClawSessionFile(state) {
1169
1962
  const files = listOpenClawSessionFiles(state.sessionDir);
1170
1963
  if (files.length === 0) return null;
1171
1964
  if (state.sessionId) {
1172
- const exact = join3(state.sessionDir, `${state.sessionId}.jsonl`);
1965
+ const exact = join5(state.sessionDir, `${state.sessionId}.jsonl`);
1173
1966
  if (files.includes(exact)) return exact;
1174
1967
  const matching = files.filter((file) => file.includes(state.sessionId ?? ""));
1175
1968
  if (matching.length > 0) return newestFile(matching);
@@ -1177,7 +1970,7 @@ function selectOpenClawSessionFile(state) {
1177
1970
  if (files.length === 1) return files[0];
1178
1971
  const recent = files.filter((file) => {
1179
1972
  try {
1180
- return statSync(file).mtimeMs >= state.startedAtMs - 1e3;
1973
+ return statSync3(file).mtimeMs >= state.startedAtMs - 1e3;
1181
1974
  } catch {
1182
1975
  return false;
1183
1976
  }
@@ -1186,10 +1979,10 @@ function selectOpenClawSessionFile(state) {
1186
1979
  }
1187
1980
  function listOpenClawSessionFiles(sessionDir) {
1188
1981
  try {
1189
- if (!existsSync4(sessionDir)) return [];
1190
- return readdirSync(sessionDir).filter((name) => name.endsWith(".jsonl")).map((name) => join3(sessionDir, name)).filter((file) => {
1982
+ if (!existsSync5(sessionDir)) return [];
1983
+ return readdirSync(sessionDir).filter((name) => name.endsWith(".jsonl")).map((name) => join5(sessionDir, name)).filter((file) => {
1191
1984
  try {
1192
- return statSync(file).isFile();
1985
+ return statSync3(file).isFile();
1193
1986
  } catch {
1194
1987
  return false;
1195
1988
  }
@@ -1202,7 +1995,7 @@ function newestFile(files) {
1202
1995
  if (files.length === 0) return null;
1203
1996
  return files.reduce((selected, file) => {
1204
1997
  try {
1205
- return statSync(file).mtimeMs > statSync(selected).mtimeMs ? file : selected;
1998
+ return statSync3(file).mtimeMs > statSync3(selected).mtimeMs ? file : selected;
1206
1999
  } catch {
1207
2000
  return selected;
1208
2001
  }
@@ -1326,7 +2119,7 @@ async function runHermesAgent(job, update) {
1326
2119
  let stderr = "";
1327
2120
  let stdoutText = "";
1328
2121
  let stopPolling = false;
1329
- const child = spawn2(job.agent.command, args, {
2122
+ const child = spawn3(job.agent.command, args, {
1330
2123
  cwd: job.agent.cwd ?? process.cwd(),
1331
2124
  env,
1332
2125
  stdio: ["ignore", "pipe", "pipe"]
@@ -1393,10 +2186,10 @@ async function runHermesAgent(job, update) {
1393
2186
  function resolveHermesSessionDir(env) {
1394
2187
  const configured = env.BOTAPP_HERMES_SESSION_DIR ?? env.HERMES_SESSION_DIR;
1395
2188
  if (configured) return expandPath(configured);
1396
- return join3(homedir3(), ".hermes", "sessions");
2189
+ return join5(homedir5(), ".hermes", "sessions");
1397
2190
  }
1398
2191
  function hermesSessionFile(sessionDir, sessionId) {
1399
- return join3(sessionDir, `session_${sessionId}.json`);
2192
+ return join5(sessionDir, `session_${sessionId}.json`);
1400
2193
  }
1401
2194
  function parseHermesSessionId(text) {
1402
2195
  const match = text.match(/session_id:\s*([A-Za-z0-9_-]+)/);
@@ -1444,13 +2237,13 @@ function readHermesSessionUpdates(state, result, toolCalls, update, final = fals
1444
2237
  function selectHermesSessionFile(state) {
1445
2238
  if (state.resumeSessionId) {
1446
2239
  const exact = hermesSessionFile(state.sessionDir, state.resumeSessionId);
1447
- if (existsSync4(exact)) return exact;
2240
+ if (existsSync5(exact)) return exact;
1448
2241
  }
1449
2242
  const files = listHermesSessionFiles(state.sessionDir);
1450
2243
  if (files.length === 0) return null;
1451
2244
  const recent = files.filter((file) => {
1452
2245
  try {
1453
- return statSync(file).mtimeMs >= state.startedAtMs - 1e3;
2246
+ return statSync3(file).mtimeMs >= state.startedAtMs - 1e3;
1454
2247
  } catch {
1455
2248
  return false;
1456
2249
  }
@@ -1459,10 +2252,10 @@ function selectHermesSessionFile(state) {
1459
2252
  }
1460
2253
  function listHermesSessionFiles(sessionDir) {
1461
2254
  try {
1462
- if (!existsSync4(sessionDir)) return [];
1463
- return readdirSync(sessionDir).filter((name) => /^session_.+\.json$/.test(name)).map((name) => join3(sessionDir, name)).filter((file) => {
2255
+ if (!existsSync5(sessionDir)) return [];
2256
+ return readdirSync(sessionDir).filter((name) => /^session_.+\.json$/.test(name)).map((name) => join5(sessionDir, name)).filter((file) => {
1464
2257
  try {
1465
- return statSync(file).isFile();
2258
+ return statSync3(file).isFile();
1466
2259
  } catch {
1467
2260
  return false;
1468
2261
  }
@@ -1482,7 +2275,7 @@ function ingestHermesMessage(message, result, toolCalls, update) {
1482
2275
  }
1483
2276
  if (Array.isArray(message.tool_calls)) {
1484
2277
  for (const call of message.tool_calls) {
1485
- const id = String(call?.id ?? call?.call_id ?? call?.response_item_id ?? randomUUID());
2278
+ const id = String(call?.id ?? call?.call_id ?? call?.response_item_id ?? randomUUID2());
1486
2279
  const name = String(call?.function?.name ?? call?.name ?? "tool");
1487
2280
  const next = {
1488
2281
  ...toolCalls.get(id),
@@ -1511,7 +2304,7 @@ function ingestHermesMessage(message, result, toolCalls, update) {
1511
2304
  }
1512
2305
  }
1513
2306
  async function runAcpAgent(job) {
1514
- const child = spawn2(job.agent.command, job.agent.args, {
2307
+ const child = spawn3(job.agent.command, job.agent.args, {
1515
2308
  cwd: job.agent.cwd ?? process.cwd(),
1516
2309
  env: { ...process.env, ...job.agent.env ?? {} },
1517
2310
  stdio: ["pipe", "pipe", "pipe"]
@@ -1567,8 +2360,8 @@ Invalid ACP stdout: ${line}`;
1567
2360
  function request2(method, params) {
1568
2361
  const id = nextId++;
1569
2362
  child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
1570
- return new Promise((resolve7, reject) => {
1571
- pending.set(id, { resolve: resolve7, reject });
2363
+ return new Promise((resolve11, reject) => {
2364
+ pending.set(id, { resolve: resolve11, reject });
1572
2365
  });
1573
2366
  }
1574
2367
  child.on("exit", (code) => {
@@ -1592,7 +2385,7 @@ Invalid ACP stdout: ${line}`;
1592
2385
  clientInfo: {
1593
2386
  name: "botapp-daemon",
1594
2387
  title: "botapp daemon",
1595
- version: "0.2.4"
2388
+ version: "0.2.7"
1596
2389
  }
1597
2390
  });
1598
2391
  const session = await request2("session/new", {
@@ -1684,9 +2477,9 @@ function hasAnyFlag(args, flags) {
1684
2477
  return flags.some((flag) => hasFlag(args, flag));
1685
2478
  }
1686
2479
  function expandPath(path) {
1687
- if (path === "~") return homedir3();
1688
- if (path.startsWith("~/")) return join3(homedir3(), path.slice(2));
1689
- return resolve3(path);
2480
+ if (path === "~") return homedir5();
2481
+ if (path.startsWith("~/")) return join5(homedir5(), path.slice(2));
2482
+ return resolve4(path);
1690
2483
  }
1691
2484
  function envNumber(env, name, fallback) {
1692
2485
  const value = env[name];
@@ -1742,10 +2535,10 @@ async function waitForChild(child, timeoutMs, label) {
1742
2535
 
1743
2536
  // src/commands/launch.ts
1744
2537
  import { Command as Command4 } from "commander";
1745
- import { spawn as spawn4 } from "child_process";
1746
- import { resolve as resolve4, join as join5 } from "path";
1747
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, openSync, writeFileSync as writeFileSync3 } from "fs";
1748
- import { homedir as homedir5 } from "os";
2538
+ import { spawn as spawn5 } from "child_process";
2539
+ import { resolve as resolve5, join as join7 } from "path";
2540
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, openSync, writeFileSync as writeFileSync3 } from "fs";
2541
+ import { homedir as homedir7 } from "os";
1749
2542
  import { createInterface as createInterface3 } from "readline";
1750
2543
  import { hostname } from "os";
1751
2544
  import pc5 from "picocolors";
@@ -1753,7 +2546,7 @@ import pc5 from "picocolors";
1753
2546
  // src/auth/browser-auth.ts
1754
2547
  import { createServer } from "http";
1755
2548
  import { randomBytes } from "crypto";
1756
- import { spawn as spawn3 } from "child_process";
2549
+ import { spawn as spawn4 } from "child_process";
1757
2550
  import pc4 from "picocolors";
1758
2551
  var OK_HTML = `<!doctype html>
1759
2552
  <meta charset="utf-8">
@@ -1848,11 +2641,11 @@ async function startLoopback(expectedState) {
1848
2641
  let rejectCb = () => {
1849
2642
  };
1850
2643
  let settled = false;
1851
- const callbackPromise = new Promise((resolve7, reject) => {
2644
+ const callbackPromise = new Promise((resolve11, reject) => {
1852
2645
  resolveCb = (p) => {
1853
2646
  if (settled) return;
1854
2647
  settled = true;
1855
- resolve7(p);
2648
+ resolve11(p);
1856
2649
  };
1857
2650
  rejectCb = (e) => {
1858
2651
  if (settled) return;
@@ -1863,11 +2656,11 @@ async function startLoopback(expectedState) {
1863
2656
  const server = createServer((req, res) => {
1864
2657
  void handleLoopback(req, res, expectedState, resolveCb, rejectCb);
1865
2658
  });
1866
- await new Promise((resolve7, reject) => {
2659
+ await new Promise((resolve11, reject) => {
1867
2660
  server.once("error", reject);
1868
2661
  server.listen(0, "127.0.0.1", () => {
1869
2662
  server.removeListener("error", reject);
1870
- resolve7();
2663
+ resolve11();
1871
2664
  });
1872
2665
  });
1873
2666
  const address = server.address();
@@ -1979,7 +2772,7 @@ function asString(v) {
1979
2772
  return typeof v === "string" ? v : void 0;
1980
2773
  }
1981
2774
  function readJsonBody(req) {
1982
- return new Promise((resolve7, reject) => {
2775
+ return new Promise((resolve11, reject) => {
1983
2776
  const chunks = [];
1984
2777
  let total = 0;
1985
2778
  req.on("data", (c) => {
@@ -1993,9 +2786,9 @@ function readJsonBody(req) {
1993
2786
  });
1994
2787
  req.on("end", () => {
1995
2788
  const raw = Buffer.concat(chunks).toString("utf-8");
1996
- if (!raw) return resolve7(null);
2789
+ if (!raw) return resolve11(null);
1997
2790
  try {
1998
- resolve7(JSON.parse(raw));
2791
+ resolve11(JSON.parse(raw));
1999
2792
  } catch (e) {
2000
2793
  reject(e);
2001
2794
  }
@@ -2007,20 +2800,20 @@ function openUrl(url) {
2007
2800
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
2008
2801
  const args = process.platform === "win32" ? ["/c", "start", '""', url] : [url];
2009
2802
  try {
2010
- spawn3(cmd, args, { stdio: "ignore", detached: true }).unref();
2803
+ spawn4(cmd, args, { stdio: "ignore", detached: true }).unref();
2011
2804
  } catch {
2012
2805
  }
2013
2806
  }
2014
2807
 
2015
2808
  // src/commands/daemon-supervisor.ts
2016
- import { existsSync as existsSync5, readFileSync as readFileSync4, unlinkSync } from "fs";
2017
- import { homedir as homedir4 } from "os";
2018
- import { join as join4 } from "path";
2809
+ import { existsSync as existsSync6, readFileSync as readFileSync4, unlinkSync } from "fs";
2810
+ import { homedir as homedir6 } from "os";
2811
+ import { join as join6 } from "path";
2019
2812
  function daemonPidFile() {
2020
- return join4(homedir4(), ".botapp", "daemon.pid");
2813
+ return join6(homedir6(), ".botapp", "daemon.pid");
2021
2814
  }
2022
2815
  function isDaemonRunningLocally(pidFile = daemonPidFile()) {
2023
- if (!existsSync5(pidFile)) return false;
2816
+ if (!existsSync6(pidFile)) return false;
2024
2817
  try {
2025
2818
  const pid = parseInt(readFileSync4(pidFile, "utf-8").trim(), 10);
2026
2819
  if (!Number.isInteger(pid) || pid <= 0) return false;
@@ -2167,7 +2960,8 @@ Configured for ${serverUrl}`));
2167
2960
  server: result.serverUrl ?? serverUrl,
2168
2961
  daemonId: data.daemon.id,
2169
2962
  daemonName: data.daemon.name,
2170
- token: data.token
2963
+ token: data.token,
2964
+ userEmail: result.userEmail
2171
2965
  });
2172
2966
  console.log(
2173
2967
  ` Daemon: ${pc5.bold(data.daemon.name)} ${pc5.dim(`(${data.daemon.id})`)}` + pc5.dim(` profile=${savedProfile.alias}`)
@@ -2188,20 +2982,20 @@ async function autoStartDaemon(opts, serverUrl, daemonId) {
2188
2982
  Next: run \`bot daemon run\` to bring this machine online.`));
2189
2983
  return;
2190
2984
  }
2191
- const dir = join5(homedir5(), ".botapp");
2192
- const pidFile = join5(dir, "daemon.pid");
2193
- const logFile = join5(dir, "daemon.log");
2985
+ const dir = join7(homedir7(), ".botapp");
2986
+ const pidFile = join7(dir, "daemon.pid");
2987
+ const logFile = join7(dir, "daemon.log");
2194
2988
  mkdirSync3(dir, { recursive: true });
2195
2989
  if (isDaemonRunningLocally(pidFile)) {
2196
2990
  stopExistingDaemon(pidFile);
2197
2991
  }
2198
2992
  const botBin = process.argv[1];
2199
- if (!botBin || !existsSync6(botBin)) {
2993
+ if (!botBin || !existsSync7(botBin)) {
2200
2994
  console.log(pc5.yellow(` Running: cannot resolve \`bot\` binary \u2014 run \`bot daemon run\` manually`));
2201
2995
  return;
2202
2996
  }
2203
2997
  const logFd = openSync(logFile, "a");
2204
- const child = spawn4(process.execPath, [botBin, "daemon", "run"], {
2998
+ const child = spawn5(process.execPath, [botBin, "daemon", "run"], {
2205
2999
  stdio: ["ignore", logFd, logFd],
2206
3000
  detached: true
2207
3001
  });
@@ -2306,7 +3100,7 @@ Or run a local server from source:
2306
3100
  return false;
2307
3101
  }
2308
3102
  console.log(pc5.blue("Starting local server..."));
2309
- const child = spawn4("node", ["--import", "tsx", serverEntry], {
3103
+ const child = spawn5("node", ["--import", "tsx", serverEntry], {
2310
3104
  env: { ...process.env, PORT: opts.port },
2311
3105
  stdio: opts.background ? "ignore" : "inherit",
2312
3106
  detached: opts.background
@@ -2323,12 +3117,12 @@ Or run a local server from source:
2323
3117
  }
2324
3118
  function findServerEntry2() {
2325
3119
  const candidates = [
2326
- resolve4(process.cwd(), "packages/server/src/index.ts"),
2327
- resolve4(process.cwd(), "../server/src/index.ts"),
2328
- resolve4(process.cwd(), "../../packages/server/src/index.ts")
3120
+ resolve5(process.cwd(), "packages/server/src/index.ts"),
3121
+ resolve5(process.cwd(), "../server/src/index.ts"),
3122
+ resolve5(process.cwd(), "../../packages/server/src/index.ts")
2329
3123
  ];
2330
3124
  for (const c of candidates) {
2331
- if (existsSync6(c)) return c;
3125
+ if (existsSync7(c)) return c;
2332
3126
  }
2333
3127
  return null;
2334
3128
  }
@@ -2530,21 +3324,21 @@ var loginCommand = new Command7("login").description("Login to a botapp server")
2530
3324
 
2531
3325
  // src/commands/install.ts
2532
3326
  import { Command as Command8 } from "commander";
2533
- import { resolve as resolve5, basename } from "path";
2534
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, symlinkSync, cpSync, readFileSync as readFileSync5 } from "fs";
2535
- import { homedir as homedir6 } from "os";
2536
- import { join as join6 } from "path";
3327
+ import { resolve as resolve6, basename } from "path";
3328
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, symlinkSync, cpSync, readFileSync as readFileSync5 } from "fs";
3329
+ import { homedir as homedir8 } from "os";
3330
+ import { join as join8 } from "path";
2537
3331
  import pc9 from "picocolors";
2538
- var APPS_DIR = join6(homedir6(), ".botapp", "apps");
3332
+ var APPS_DIR = join8(homedir8(), ".botapp", "apps");
2539
3333
  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) => {
2540
- const absPath = resolve5(appPath);
2541
- if (!existsSync7(absPath)) {
3334
+ const absPath = resolve6(appPath);
3335
+ if (!existsSync8(absPath)) {
2542
3336
  console.error(pc9.red(`Path not found: ${absPath}`));
2543
3337
  process.exitCode = 1;
2544
3338
  return;
2545
3339
  }
2546
- const manifestPath = join6(absPath, "botapp.app.json");
2547
- if (!existsSync7(manifestPath)) {
3340
+ const manifestPath = join8(absPath, "botapp.app.json");
3341
+ if (!existsSync8(manifestPath)) {
2548
3342
  console.error(pc9.red(`No botapp.app.json found in ${absPath}`));
2549
3343
  process.exitCode = 1;
2550
3344
  return;
@@ -2558,9 +3352,9 @@ var installCommand = new Command8("install").description("Install an app from a
2558
3352
  return;
2559
3353
  }
2560
3354
  const appName = manifest.name || basename(absPath);
2561
- const targetDir = join6(APPS_DIR, appName);
3355
+ const targetDir = join8(APPS_DIR, appName);
2562
3356
  mkdirSync4(APPS_DIR, { recursive: true });
2563
- if (existsSync7(targetDir)) {
3357
+ if (existsSync8(targetDir)) {
2564
3358
  console.log(pc9.yellow(`App "${appName}" is already installed. Reinstalling...`));
2565
3359
  const { rmSync: rmSync2 } = await import("fs");
2566
3360
  rmSync2(targetDir, { recursive: true, force: true });
@@ -2579,14 +3373,14 @@ var installCommand = new Command8("install").description("Install an app from a
2579
3373
 
2580
3374
  // src/commands/uninstall.ts
2581
3375
  import { Command as Command9 } from "commander";
2582
- import { existsSync as existsSync8, rmSync } from "fs";
2583
- import { homedir as homedir7 } from "os";
2584
- import { join as join7 } from "path";
3376
+ import { existsSync as existsSync9, rmSync } from "fs";
3377
+ import { homedir as homedir9 } from "os";
3378
+ import { join as join9 } from "path";
2585
3379
  import pc10 from "picocolors";
2586
- var APPS_DIR2 = join7(homedir7(), ".botapp", "apps");
3380
+ var APPS_DIR2 = join9(homedir9(), ".botapp", "apps");
2587
3381
  var uninstallCommand = new Command9("uninstall").description("Uninstall an app").argument("<name>", "App name to uninstall").action(async (name) => {
2588
- const targetDir = join7(APPS_DIR2, name);
2589
- if (!existsSync8(targetDir)) {
3382
+ const targetDir = join9(APPS_DIR2, name);
3383
+ if (!existsSync9(targetDir)) {
2590
3384
  console.error(pc10.red(`App "${name}" is not installed`));
2591
3385
  process.exitCode = 1;
2592
3386
  return;
@@ -2598,21 +3392,21 @@ var uninstallCommand = new Command9("uninstall").description("Uninstall an app")
2598
3392
 
2599
3393
  // src/commands/dev.ts
2600
3394
  import { Command as Command10 } from "commander";
2601
- import { resolve as resolve6, basename as basename2, join as join8 } from "path";
2602
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, symlinkSync as symlinkSync2, readFileSync as readFileSync6, lstatSync } from "fs";
2603
- import { homedir as homedir8 } from "os";
2604
- import { spawn as spawn5 } from "child_process";
3395
+ import { resolve as resolve7, basename as basename2, join as join10 } from "path";
3396
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, symlinkSync as symlinkSync2, readFileSync as readFileSync6, lstatSync } from "fs";
3397
+ import { homedir as homedir10 } from "os";
3398
+ import { spawn as spawn6 } from "child_process";
2605
3399
  import pc11 from "picocolors";
2606
- var APPS_DIR3 = join8(homedir8(), ".botapp", "apps");
3400
+ var APPS_DIR3 = join10(homedir10(), ".botapp", "apps");
2607
3401
  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) => {
2608
- const absPath = resolve6(appPath);
2609
- if (!existsSync9(absPath)) {
3402
+ const absPath = resolve7(appPath);
3403
+ if (!existsSync10(absPath)) {
2610
3404
  console.error(pc11.red(`Path not found: ${absPath}`));
2611
3405
  process.exitCode = 1;
2612
3406
  return;
2613
3407
  }
2614
- const manifestPath = join8(absPath, "botapp.app.json");
2615
- if (!existsSync9(manifestPath)) {
3408
+ const manifestPath = join10(absPath, "botapp.app.json");
3409
+ if (!existsSync10(manifestPath)) {
2616
3410
  console.error(pc11.red(`No botapp.app.json found in ${absPath}`));
2617
3411
  process.exitCode = 1;
2618
3412
  return;
@@ -2626,9 +3420,9 @@ var devCommand = new Command10("dev").description("Start development mode for an
2626
3420
  return;
2627
3421
  }
2628
3422
  const appName = manifest.name || basename2(absPath);
2629
- const targetDir = join8(APPS_DIR3, appName);
3423
+ const targetDir = join10(APPS_DIR3, appName);
2630
3424
  mkdirSync5(APPS_DIR3, { recursive: true });
2631
- if (!existsSync9(targetDir)) {
3425
+ if (!existsSync10(targetDir)) {
2632
3426
  symlinkSync2(absPath, targetDir, "dir");
2633
3427
  console.log(pc11.blue(`Linked ${pc11.bold(appName)} \u2192 ${pc11.dim(absPath)}`));
2634
3428
  } else {
@@ -2661,7 +3455,7 @@ Start the server separately, then restart it to load the app:
2661
3455
  return;
2662
3456
  }
2663
3457
  console.log(pc11.blue("Starting botapp server..."));
2664
- const child = spawn5("node", ["--import", "tsx", serverEntry], {
3458
+ const child = spawn6("node", ["--import", "tsx", serverEntry], {
2665
3459
  env: { ...process.env, PORT: opts.port },
2666
3460
  stdio: "inherit"
2667
3461
  });
@@ -2675,11 +3469,11 @@ Start the server separately, then restart it to load the app:
2675
3469
  });
2676
3470
  function findServerEntry3() {
2677
3471
  const candidates = [
2678
- resolve6(process.cwd(), "packages/server/src/index.ts"),
2679
- resolve6(process.cwd(), "../server/src/index.ts")
3472
+ resolve7(process.cwd(), "packages/server/src/index.ts"),
3473
+ resolve7(process.cwd(), "../server/src/index.ts")
2680
3474
  ];
2681
3475
  for (const c of candidates) {
2682
- if (existsSync9(c)) return c;
3476
+ if (existsSync10(c)) return c;
2683
3477
  }
2684
3478
  return null;
2685
3479
  }
@@ -2913,13 +3707,13 @@ All agents:`);
2913
3707
 
2914
3708
  // src/commands/register.ts
2915
3709
  import { Command as Command14 } from "commander";
2916
- import { readFileSync as readFileSync7, existsSync as existsSync10 } from "fs";
3710
+ import { readFileSync as readFileSync7, existsSync as existsSync11 } from "fs";
2917
3711
  import pc15 from "picocolors";
2918
3712
  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) => {
2919
3713
  const globalOpts = cmd.parent?.opts() ?? {};
2920
3714
  const serverUrl = resolveServerUrl(globalOpts.server);
2921
3715
  const token = resolveToken(globalOpts.token);
2922
- if (!existsSync10(manifestPath)) {
3716
+ if (!existsSync11(manifestPath)) {
2923
3717
  console.error(pc15.red(`Error: Manifest not found: ${manifestPath}`));
2924
3718
  process.exitCode = 1;
2925
3719
  return;
@@ -2956,13 +3750,13 @@ var registerCommand = new Command14("register").description("Register an externa
2956
3750
 
2957
3751
  // src/commands/wrap.ts
2958
3752
  import { Command as Command15 } from "commander";
2959
- import { readFileSync as readFileSync8, existsSync as existsSync11 } from "fs";
3753
+ import { readFileSync as readFileSync8, existsSync as existsSync12 } from "fs";
2960
3754
  import pc16 from "picocolors";
2961
3755
  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) => {
2962
3756
  const globalOpts = cmd.parent?.opts() ?? {};
2963
3757
  const serverUrl = resolveServerUrl(globalOpts.server);
2964
3758
  const token = resolveToken(globalOpts.token);
2965
- if (!existsSync11(manifestPath)) {
3759
+ if (!existsSync12(manifestPath)) {
2966
3760
  console.error(pc16.red(`Error: Manifest not found: ${manifestPath}`));
2967
3761
  process.exitCode = 1;
2968
3762
  return;
@@ -3226,7 +4020,8 @@ var pairingCommand = new Command18("pairing").alias("pair").description("Pair th
3226
4020
  server: serverUrl,
3227
4021
  daemonId: data.daemon.id,
3228
4022
  daemonName: data.daemon.name,
3229
- token: data.token
4023
+ token: data.token,
4024
+ userEmail: grant.userEmail
3230
4025
  });
3231
4026
  console.log(pc19.green(`Paired daemon: ${pc19.bold(data.daemon.name)}`));
3232
4027
  console.log(` ID: ${data.daemon.id}`);
@@ -3262,13 +4057,130 @@ async function obtainPairingToken(opts) {
3262
4057
  return { pairingToken: result.pairingToken, userEmail: result.userEmail };
3263
4058
  }
3264
4059
 
3265
- // src/commands/update.ts
3266
- import { spawn as spawn6 } from "child_process";
3267
- import { realpathSync } from "fs";
4060
+ // src/commands/doctor.ts
3268
4061
  import { Command as Command19 } from "commander";
3269
4062
  import pc20 from "picocolors";
4063
+ var doctorCommand = new Command19("doctor").description("Diagnose paired daemon profiles (server reachability, token validity, account ownership)").action(async () => {
4064
+ const profiles = loadDaemonProfiles();
4065
+ if (profiles.length === 0) {
4066
+ console.log(pc20.dim("No paired daemons in ~/.botapp/daemon.yaml."));
4067
+ console.log(pc20.dim("Run `bot pair` to add one."));
4068
+ return;
4069
+ }
4070
+ let warns = 0;
4071
+ let fails = 0;
4072
+ for (const profile of profiles) {
4073
+ console.log(pc20.bold(`
4074
+ [${profile.alias ?? profile.server}]`));
4075
+ console.log(` Server: ${profile.server}`);
4076
+ console.log(` Daemon: ${profile.daemonName} ${pc20.dim(`(${profile.daemonId})`)}`);
4077
+ const findings = await checkProfile(profile);
4078
+ for (const f of findings) {
4079
+ const tag = f.severity === "ok" ? pc20.green(" \u2713 ") : f.severity === "warn" ? pc20.yellow(" \u26A0 ") : pc20.red(" \u2717 ");
4080
+ console.log(`${tag}${f.message}`);
4081
+ if (f.fix) console.log(pc20.dim(` \u2192 ${f.fix}`));
4082
+ if (f.severity === "warn") warns += 1;
4083
+ if (f.severity === "fail") fails += 1;
4084
+ }
4085
+ }
4086
+ console.log("");
4087
+ if (fails === 0 && warns === 0) {
4088
+ console.log(pc20.green("All checks passed."));
4089
+ } else {
4090
+ const parts = [];
4091
+ if (fails > 0) parts.push(pc20.red(`${fails} failing`));
4092
+ if (warns > 0) parts.push(pc20.yellow(`${warns} warning${warns === 1 ? "" : "s"}`));
4093
+ console.log(parts.join(", ") + ".");
4094
+ if (fails > 0) process.exitCode = 1;
4095
+ }
4096
+ });
4097
+ async function checkProfile(profile) {
4098
+ const findings = [];
4099
+ let serverOk = false;
4100
+ try {
4101
+ const res = await fetch(`${profile.server}/health`, {
4102
+ signal: AbortSignal.timeout(5e3)
4103
+ });
4104
+ if (res.ok) {
4105
+ findings.push({ severity: "ok", message: "Server reachable" });
4106
+ serverOk = true;
4107
+ } else {
4108
+ findings.push({
4109
+ severity: "fail",
4110
+ message: `Server returned ${res.status} ${res.statusText}`,
4111
+ fix: `Verify the server at ${profile.server} is running.`
4112
+ });
4113
+ }
4114
+ } catch (e) {
4115
+ findings.push({
4116
+ severity: "fail",
4117
+ message: `Server unreachable: ${e.message ?? e}`,
4118
+ fix: `Start the server, or remove this profile with \`bot daemon unpair ${profile.alias ?? profile.server}\`.`
4119
+ });
4120
+ }
4121
+ if (!serverOk) return findings;
4122
+ let serverEmail = null;
4123
+ try {
4124
+ const data = await daemonRequest(profile.server, profile.token, "/api/daemon/self");
4125
+ findings.push({ severity: "ok", message: "Token valid" });
4126
+ serverEmail = data.user?.email ?? null;
4127
+ if (serverEmail) {
4128
+ findings.push({
4129
+ severity: "ok",
4130
+ message: `Token paired under ${pc20.bold(serverEmail)}`
4131
+ });
4132
+ } else {
4133
+ findings.push({
4134
+ severity: "warn",
4135
+ message: "Server returned no user record for this daemon",
4136
+ fix: "The owning user may have been deleted. Re-pair with `bot pair`."
4137
+ });
4138
+ }
4139
+ if (data.daemon?.status === "online") {
4140
+ findings.push({ severity: "ok", message: "Daemon currently online" });
4141
+ } else {
4142
+ findings.push({
4143
+ severity: "warn",
4144
+ message: `Daemon status: ${data.daemon?.status ?? "unknown"}`,
4145
+ fix: `Run \`bot daemon run --server ${profile.server}\` to bring it online.`
4146
+ });
4147
+ }
4148
+ } catch (e) {
4149
+ const msg = e?.message ?? String(e);
4150
+ if (/unauthor/i.test(msg) || /token/i.test(msg)) {
4151
+ findings.push({
4152
+ severity: "fail",
4153
+ message: `Token rejected: ${msg}`,
4154
+ fix: "The token is stale (likely the server DB was reset). Re-pair with `bot pair`."
4155
+ });
4156
+ return findings;
4157
+ }
4158
+ findings.push({ severity: "fail", message: `Identity probe failed: ${msg}` });
4159
+ return findings;
4160
+ }
4161
+ if (profile.userEmail && serverEmail && profile.userEmail !== serverEmail) {
4162
+ findings.push({
4163
+ severity: "warn",
4164
+ message: `daemon.yaml says \`userEmail: ${profile.userEmail}\` but server says \`${serverEmail}\``,
4165
+ fix: "Re-pair to refresh the recorded email."
4166
+ });
4167
+ } else if (!profile.userEmail && serverEmail) {
4168
+ findings.push({
4169
+ severity: "warn",
4170
+ message: "daemon.yaml has no userEmail recorded",
4171
+ fix: "Re-pair to capture it (cosmetic \u2014 auth still works)."
4172
+ });
4173
+ }
4174
+ return findings;
4175
+ }
4176
+
4177
+ // src/commands/update.ts
4178
+ import { spawn as spawn7 } from "child_process";
4179
+ import { realpathSync as realpathSync2 } from "fs";
4180
+ import { Command as Command20 } from "commander";
4181
+ import pc21 from "picocolors";
3270
4182
  var PACKAGE_NAME = "botapp-cli";
3271
- var updateCommand = new Command19("update").description("Update the `bot` CLI itself to the latest published version").option(
4183
+ var updateCommand = new Command20("update").description("Update the `bot` CLI itself to the latest published version").option(
3272
4184
  "--manager <pm>",
3273
4185
  "Force a package manager: npm | pnpm | yarn | brew (default: auto-detect)"
3274
4186
  ).option("--dry-run", "Print the command that would run, but do not execute it").action(async (opts) => {
@@ -3276,12 +4188,12 @@ var updateCommand = new Command19("update").description("Update the `bot` CLI it
3276
4188
  const manager = forced ?? detectPackageManager();
3277
4189
  if (manager === "npx") {
3278
4190
  console.log(
3279
- pc20.green(
4191
+ pc21.green(
3280
4192
  "You are running `bot` via `npx -y botapp-cli@latest` \u2014 every invocation already fetches the latest version."
3281
4193
  )
3282
4194
  );
3283
4195
  console.log(
3284
- pc20.dim(
4196
+ pc21.dim(
3285
4197
  "For a faster startup, install it globally instead:\n npm i -g botapp-cli@latest\n pnpm add -g botapp-cli@latest"
3286
4198
  )
3287
4199
  );
@@ -3289,40 +4201,40 @@ var updateCommand = new Command19("update").description("Update the `bot` CLI it
3289
4201
  }
3290
4202
  if (!manager) {
3291
4203
  console.error(
3292
- pc20.red(
4204
+ pc21.red(
3293
4205
  "Couldn't detect how `bot` was installed. Pick one of these manually:"
3294
4206
  )
3295
4207
  );
3296
- console.log(pc20.cyan(" npm i -g botapp-cli@latest"));
3297
- console.log(pc20.cyan(" pnpm add -g botapp-cli@latest"));
3298
- console.log(pc20.cyan(" yarn global add botapp-cli@latest"));
3299
- console.log(pc20.cyan(" brew upgrade botapp-cli"));
4208
+ console.log(pc21.cyan(" npm i -g botapp-cli@latest"));
4209
+ console.log(pc21.cyan(" pnpm add -g botapp-cli@latest"));
4210
+ console.log(pc21.cyan(" yarn global add botapp-cli@latest"));
4211
+ console.log(pc21.cyan(" brew upgrade botapp-cli"));
3300
4212
  process.exitCode = 1;
3301
4213
  return;
3302
4214
  }
3303
4215
  const { command, args } = updateCommandFor(manager);
3304
- console.log(pc20.dim(`$ ${command} ${args.join(" ")}`));
4216
+ console.log(pc21.dim(`$ ${command} ${args.join(" ")}`));
3305
4217
  if (opts.dryRun) return;
3306
- const child = spawn6(command, args, { stdio: "inherit" });
3307
- const code = await new Promise((resolve7) => {
4218
+ const child = spawn7(command, args, { stdio: "inherit" });
4219
+ const code = await new Promise((resolve11) => {
3308
4220
  child.once("error", (err) => {
3309
- console.error(pc20.red(`Failed to spawn ${command}: ${err.message}`));
3310
- resolve7(127);
4221
+ console.error(pc21.red(`Failed to spawn ${command}: ${err.message}`));
4222
+ resolve11(127);
3311
4223
  });
3312
- child.once("close", resolve7);
4224
+ child.once("close", resolve11);
3313
4225
  });
3314
4226
  if (code !== 0) {
3315
- console.error(pc20.red(`Update failed (exit ${code}).`));
4227
+ console.error(pc21.red(`Update failed (exit ${code}).`));
3316
4228
  process.exitCode = code ?? 1;
3317
4229
  return;
3318
4230
  }
3319
- console.log(pc20.green("botapp-cli updated. Run `bot --version` to confirm."));
4231
+ console.log(pc21.green("botapp-cli updated. Run `bot --version` to confirm."));
3320
4232
  });
3321
4233
  function detectPackageManager() {
3322
4234
  const argv = process.argv[1] ?? "";
3323
4235
  let real = "";
3324
4236
  try {
3325
- real = argv ? realpathSync(argv) : "";
4237
+ real = argv ? realpathSync2(argv) : "";
3326
4238
  } catch {
3327
4239
  }
3328
4240
  const candidates = [argv, real].filter(Boolean);
@@ -3350,9 +4262,755 @@ function updateCommandFor(manager) {
3350
4262
  }
3351
4263
  }
3352
4264
 
4265
+ // src/commands/simulate.ts
4266
+ import { Command as Command21 } from "commander";
4267
+ import { resolve as resolve8, join as join11 } from "path";
4268
+ import { existsSync as existsSync13, readFileSync as readFileSync9 } from "fs";
4269
+ import { spawn as spawn8 } from "child_process";
4270
+ import pc22 from "picocolors";
4271
+ var simulateCommand = new Command21("simulate").description("Dev-tunnel a local app to a botapp server (per-user shadow)").argument("[path]", "Path to the app directory", ".").option("-s, --server <url>", "botapp server URL (default: active profile / $BOTAPP_SERVER)").option("-t, --token <token>", "auth token (default: active profile / $BOTAPP_TOKEN)").option("--entry <file>", "override the manifest entry path").option("--lifetime <minutes>", "dev-session token lifetime in minutes", "480").action(async (appPath, opts) => {
4272
+ const absPath = resolve8(appPath);
4273
+ if (!existsSync13(absPath)) {
4274
+ console.error(pc22.red(`Path not found: ${absPath}`));
4275
+ process.exitCode = 1;
4276
+ return;
4277
+ }
4278
+ const manifestPath = join11(absPath, "botapp.app.json");
4279
+ if (!existsSync13(manifestPath)) {
4280
+ console.error(pc22.red(`No botapp.app.json found in ${absPath}`));
4281
+ process.exitCode = 1;
4282
+ return;
4283
+ }
4284
+ const manifest = JSON.parse(readFileSync9(manifestPath, "utf8"));
4285
+ if (!manifest.name) {
4286
+ console.error(pc22.red('manifest missing "name"'));
4287
+ process.exitCode = 1;
4288
+ return;
4289
+ }
4290
+ const httpServer = resolveServerUrl(opts.server);
4291
+ const wsServer = httpServer.replace(/^http:/, "ws:").replace(/^https:/, "wss:") + "/ws/host";
4292
+ const token = resolveToken(opts.token);
4293
+ if (!token) {
4294
+ console.error(pc22.red("not logged in: run `bot login` or pass --token"));
4295
+ process.exitCode = 1;
4296
+ return;
4297
+ }
4298
+ const lifetimeMs = Math.max(6e4, Number(opts.lifetime) * 6e4);
4299
+ console.log(pc22.dim(`requesting dev token for "${manifest.name}" from ${httpServer}...`));
4300
+ const devToken = await issueDevToken({
4301
+ serverUrl: httpServer,
4302
+ token,
4303
+ appName: manifest.name,
4304
+ lifetimeMs
4305
+ });
4306
+ console.log(pc22.green("\u2713"), `dev token issued (lifetime ${opts.lifetime} min)`);
4307
+ const entryPath = resolveEntry(absPath, opts.entry ?? manifest.entry);
4308
+ if (!existsSync13(entryPath)) {
4309
+ console.error(pc22.red(`entry not found: ${entryPath}`));
4310
+ console.error(pc22.dim(" run `pnpm build` (or your build script) first."));
4311
+ process.exitCode = 1;
4312
+ return;
4313
+ }
4314
+ console.log(pc22.dim(`spawning ${entryPath} ...`));
4315
+ console.log(pc22.dim(` BOTAPP_SERVER=${wsServer}`));
4316
+ console.log(pc22.dim(` BOTAPP_APP_NAME=${manifest.name}`));
4317
+ console.log(
4318
+ pc22.cyan(
4319
+ `
4320
+ When ready, your dashboard at ${httpServer} will route "${manifest.name}" to this process for your account only.
4321
+ `
4322
+ )
4323
+ );
4324
+ const child = spawn8("node", [entryPath], {
4325
+ cwd: absPath,
4326
+ env: {
4327
+ ...process.env,
4328
+ BOTAPP_SERVER: wsServer,
4329
+ BOTAPP_APP_TOKEN: devToken,
4330
+ BOTAPP_APP_NAME: manifest.name,
4331
+ BOTAPP_DATA_DIR: join11(absPath, ".botapp-sim")
4332
+ },
4333
+ stdio: "inherit"
4334
+ });
4335
+ const stop = (signal) => {
4336
+ console.log(pc22.dim(`
4337
+ stopping (${signal})...`));
4338
+ if (!child.killed) child.kill(signal);
4339
+ };
4340
+ process.on("SIGINT", () => stop("SIGINT"));
4341
+ process.on("SIGTERM", () => stop("SIGTERM"));
4342
+ child.on("exit", (code, sig) => {
4343
+ const reason = sig ? `signal ${sig}` : `exit ${code}`;
4344
+ console.log(pc22.dim(`child exited (${reason})`));
4345
+ process.exit(typeof code === "number" ? code : 0);
4346
+ });
4347
+ });
4348
+ async function issueDevToken(opts) {
4349
+ const res = await fetch(`${opts.serverUrl}/api/dev/token`, {
4350
+ method: "POST",
4351
+ headers: authHeaders(opts.token),
4352
+ body: JSON.stringify({ appName: opts.appName, lifetimeMs: opts.lifetimeMs })
4353
+ });
4354
+ if (!res.ok) {
4355
+ const text = await res.text().catch(() => "");
4356
+ throw new Error(`dev-token request failed (${res.status}): ${text}`);
4357
+ }
4358
+ const data = await res.json();
4359
+ if (!data.token) {
4360
+ throw new Error(`dev-token response missing token: ${JSON.stringify(data)}`);
4361
+ }
4362
+ return data.token;
4363
+ }
4364
+ function resolveEntry(appDir, entry) {
4365
+ const candidates = [
4366
+ entry,
4367
+ "dist/api.js",
4368
+ "dist/api/index.js",
4369
+ "dist/index.js",
4370
+ "api/index.ts",
4371
+ "api/index.js"
4372
+ ].filter(Boolean);
4373
+ for (const c of candidates) {
4374
+ const full = resolve8(appDir, c);
4375
+ if (existsSync13(full)) return full;
4376
+ }
4377
+ return resolve8(appDir, candidates[0] ?? "dist/api.js");
4378
+ }
4379
+
4380
+ // src/commands/init.ts
4381
+ import { Command as Command22 } from "commander";
4382
+ import { existsSync as existsSync14, mkdirSync as mkdirSync6, writeFileSync as writeFileSync4 } from "fs";
4383
+ import { resolve as resolve9, join as join12 } from "path";
4384
+ import pc23 from "picocolors";
4385
+ var initCommand = new Command22("init").description("Scaffold a new hosted-tier botapp app project").argument("<name>", "App name (kebab-case; used in URL paths and manifest)").option("-d, --dir <path>", "Output directory (default: ./<name>)").option("--headless", "No frontend \u2014 backend-only app (skip src/, tailwind, etc.)").option("--description <text>", "Short description for AI agents to read").option("-f, --force", "Overwrite existing directory contents").action(async (name, opts) => {
4386
+ if (!/^[a-z][a-z0-9-]*$/.test(name)) {
4387
+ console.error(pc23.red("Invalid name. Use lowercase letters, digits, dashes; must start with a letter."));
4388
+ console.error(pc23.dim(" e.g. my-app, todo-tracker, gomoku-2"));
4389
+ process.exitCode = 1;
4390
+ return;
4391
+ }
4392
+ const targetDir = resolve9(opts.dir ?? `./${name}`);
4393
+ if (existsSync14(targetDir) && !opts.force) {
4394
+ const stat = (() => {
4395
+ try {
4396
+ return __require("fs").readdirSync(targetDir);
4397
+ } catch {
4398
+ return [];
4399
+ }
4400
+ })();
4401
+ if (Array.isArray(stat) && stat.length > 0) {
4402
+ console.error(pc23.red(`Target directory exists and is not empty: ${targetDir}`));
4403
+ console.error(pc23.dim(" pass --force to overwrite, or pick a different --dir"));
4404
+ process.exitCode = 1;
4405
+ return;
4406
+ }
4407
+ }
4408
+ const ctx = {
4409
+ name,
4410
+ description: opts.description ?? `${name} \u2014 a botapp app`,
4411
+ headless: !!opts.headless
4412
+ };
4413
+ mkdirSync6(targetDir, { recursive: true });
4414
+ const files = ctx.headless ? headlessFiles(ctx) : fullFiles(ctx);
4415
+ for (const [rel, content] of Object.entries(files)) {
4416
+ const full = join12(targetDir, rel);
4417
+ mkdirSync6(dirname2(full), { recursive: true });
4418
+ writeFileSync4(full, content);
4419
+ }
4420
+ console.log(pc23.green("\u2713"), `Scaffolded ${ctx.headless ? "headless " : ""}app at`, pc23.cyan(targetDir));
4421
+ console.log();
4422
+ console.log("Next steps:");
4423
+ console.log(pc23.dim(` cd ${targetDir.replace(process.cwd() + "/", "")}`));
4424
+ console.log(pc23.dim(" pnpm install # or npm install"));
4425
+ if (!ctx.headless) console.log(pc23.dim(" pnpm build # builds api/ + frontend (dist/)"));
4426
+ else console.log(pc23.dim(" pnpm build # builds api/ to dist/api.js"));
4427
+ console.log(pc23.dim(` bot login --server <your-botapp-server>`));
4428
+ console.log(pc23.dim(` bot simulate # dev-tunnel into the server, hot-reload as you build`));
4429
+ console.log();
4430
+ console.log(
4431
+ pc23.dim("Once it works, `bot publish` ships it to the server (per-user install by default).")
4432
+ );
4433
+ });
4434
+ function fullFiles(ctx) {
4435
+ return {
4436
+ "botapp.app.json": manifestJson(ctx),
4437
+ "package.json": packageJson(
4438
+ ctx,
4439
+ /*headless*/
4440
+ false
4441
+ ),
4442
+ "tsconfig.json": tsconfigJson(false),
4443
+ "tsconfig.api.json": tsconfigApiJson(),
4444
+ "tsconfig.frontend.json": tsconfigFrontendJson(),
4445
+ "tsup.api.config.ts": tsupApiConfig(),
4446
+ "vite.config.ts": viteConfig(ctx),
4447
+ "index.html": indexHtml(ctx),
4448
+ "api/index.ts": apiEntryTs(ctx, false),
4449
+ "src/main.tsx": srcMainTsx(ctx),
4450
+ "src/App.tsx": srcAppTsx(ctx),
4451
+ "src/lib/api.ts": srcApiTs(),
4452
+ "contracts/types.ts": contractsTs(ctx),
4453
+ ".gitignore": gitignore(),
4454
+ "README.md": readme(ctx, false)
4455
+ };
4456
+ }
4457
+ function headlessFiles(ctx) {
4458
+ return {
4459
+ "botapp.app.json": manifestJson(ctx),
4460
+ "package.json": packageJson(
4461
+ ctx,
4462
+ /*headless*/
4463
+ true
4464
+ ),
4465
+ "tsconfig.json": tsconfigJson(true),
4466
+ "tsup.api.config.ts": tsupApiConfig(),
4467
+ "api/index.ts": apiEntryTs(ctx, true),
4468
+ "contracts/types.ts": contractsTs(ctx),
4469
+ ".gitignore": gitignore(),
4470
+ "README.md": readme(ctx, true)
4471
+ };
4472
+ }
4473
+ function manifestJson(ctx) {
4474
+ const m = {
4475
+ name: ctx.name,
4476
+ version: "0.1.0",
4477
+ description: ctx.description,
4478
+ entry: "./dist/api.js",
4479
+ tier: "user",
4480
+ visibility: "private"
4481
+ };
4482
+ if (!ctx.headless) {
4483
+ m.hasFrontend = true;
4484
+ }
4485
+ return JSON.stringify(m, null, 2) + "\n";
4486
+ }
4487
+ function packageJson(ctx, headless) {
4488
+ const scripts = {
4489
+ build: headless ? "tsup --config tsup.api.config.ts" : "tsup --config tsup.api.config.ts && vite build",
4490
+ "build:api": "tsup --config tsup.api.config.ts",
4491
+ dev: headless ? "tsup --config tsup.api.config.ts --watch" : "tsup --config tsup.api.config.ts --watch & vite",
4492
+ typecheck: "tsc --noEmit"
4493
+ };
4494
+ const deps = {
4495
+ "botapp-sdk": "^0.1.0",
4496
+ ws: "^8.18.0"
4497
+ };
4498
+ const devDeps = {
4499
+ tsup: "^8.4.0",
4500
+ typescript: "^5.8.0",
4501
+ "@types/node": "^22.0.0",
4502
+ "@types/ws": "^8.5.0"
4503
+ };
4504
+ if (!headless) {
4505
+ Object.assign(deps, {
4506
+ react: "^19.0.0",
4507
+ "react-dom": "^19.0.0"
4508
+ });
4509
+ Object.assign(devDeps, {
4510
+ vite: "^7.0.0",
4511
+ "@vitejs/plugin-react": "^5.0.0",
4512
+ "@types/react": "^19.0.0",
4513
+ "@types/react-dom": "^19.0.0"
4514
+ });
4515
+ }
4516
+ return JSON.stringify(
4517
+ {
4518
+ name: ctx.name,
4519
+ version: "0.1.0",
4520
+ description: ctx.description,
4521
+ type: "module",
4522
+ private: true,
4523
+ scripts,
4524
+ dependencies: deps,
4525
+ devDependencies: devDeps
4526
+ },
4527
+ null,
4528
+ 2
4529
+ ) + "\n";
4530
+ }
4531
+ function tsconfigJson(headless) {
4532
+ if (headless) {
4533
+ return JSON.stringify(
4534
+ {
4535
+ compilerOptions: {
4536
+ target: "ES2022",
4537
+ module: "ESNext",
4538
+ moduleResolution: "bundler",
4539
+ esModuleInterop: true,
4540
+ strict: true,
4541
+ skipLibCheck: true,
4542
+ noEmit: true
4543
+ },
4544
+ include: ["api/**/*.ts", "contracts/**/*.ts"]
4545
+ },
4546
+ null,
4547
+ 2
4548
+ ) + "\n";
4549
+ }
4550
+ return JSON.stringify(
4551
+ {
4552
+ files: [],
4553
+ references: [
4554
+ { path: "./tsconfig.api.json" },
4555
+ { path: "./tsconfig.frontend.json" }
4556
+ ]
4557
+ },
4558
+ null,
4559
+ 2
4560
+ ) + "\n";
4561
+ }
4562
+ function tsconfigApiJson() {
4563
+ return JSON.stringify(
4564
+ {
4565
+ compilerOptions: {
4566
+ target: "ES2022",
4567
+ module: "ESNext",
4568
+ moduleResolution: "bundler",
4569
+ esModuleInterop: true,
4570
+ strict: true,
4571
+ skipLibCheck: true,
4572
+ noEmit: true
4573
+ },
4574
+ include: ["api/**/*.ts", "contracts/**/*.ts"]
4575
+ },
4576
+ null,
4577
+ 2
4578
+ ) + "\n";
4579
+ }
4580
+ function tsconfigFrontendJson() {
4581
+ return JSON.stringify(
4582
+ {
4583
+ compilerOptions: {
4584
+ target: "ES2022",
4585
+ module: "ESNext",
4586
+ moduleResolution: "bundler",
4587
+ esModuleInterop: true,
4588
+ strict: true,
4589
+ skipLibCheck: true,
4590
+ jsx: "react-jsx",
4591
+ lib: ["ES2022", "DOM", "DOM.Iterable"],
4592
+ noEmit: true
4593
+ },
4594
+ include: ["src/**/*.ts", "src/**/*.tsx", "contracts/**/*.ts"]
4595
+ },
4596
+ null,
4597
+ 2
4598
+ ) + "\n";
4599
+ }
4600
+ function tsupApiConfig() {
4601
+ return `import { defineConfig } from 'tsup'
4602
+
4603
+ export default defineConfig({
4604
+ entry: { api: 'api/index.ts' },
4605
+ format: ['esm'],
4606
+ outDir: 'dist',
4607
+ clean: true,
4608
+ sourcemap: true,
4609
+ noExternal: [],
4610
+ })
4611
+ `;
4612
+ }
4613
+ function viteConfig(ctx) {
4614
+ return `import { defineConfig } from 'vite'
4615
+ import react from '@vitejs/plugin-react'
4616
+
4617
+ export default defineConfig({
4618
+ plugins: [react()],
4619
+ base: '/apps/${ctx.name}/',
4620
+ build: {
4621
+ outDir: 'dist/public',
4622
+ emptyOutDir: true,
4623
+ },
4624
+ })
4625
+ `;
4626
+ }
4627
+ function indexHtml(ctx) {
4628
+ return `<!doctype html>
4629
+ <html lang="en">
4630
+ <head>
4631
+ <meta charset="UTF-8" />
4632
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
4633
+ <title>${escapeHtml(ctx.description)}</title>
4634
+ </head>
4635
+ <body>
4636
+ <div id="root"></div>
4637
+ <script type="module" src="/src/main.tsx"></script>
4638
+ </body>
4639
+ </html>
4640
+ `;
4641
+ }
4642
+ function apiEntryTs(ctx, headless) {
4643
+ const widget = headless ? "" : `
4644
+ ctx.registerWidget({
4645
+ refresh: { intervalMs: 30_000 },
4646
+ render: async ({ state }) => {
4647
+ const count = (await state.get('count')) ?? 0
4648
+ return {
4649
+ html: \`<div class="card"><div class="label">${ctx.name.toUpperCase()}</div><div class="value">\${count}</div></div>\`,
4650
+ css: \`.card { padding: 20px; font-family: sans-serif; } .value { font-size: 32px; font-weight: 700; }\`,
4651
+ }
4652
+ },
4653
+ })
4654
+
4655
+ ctx.serveStatic('./dist/public')
4656
+ `;
4657
+ return `import { BotApp } from 'botapp-sdk'
4658
+
4659
+ const app = new BotApp({
4660
+ name: '${ctx.name}',
4661
+ version: '0.1.0',
4662
+ description: '${ctx.description.replace(/'/g, "\\'")}',
4663
+ async setup(ctx) {
4664
+ ctx.registerCommand('hello', {
4665
+ description: 'Greet the caller',
4666
+ params: {
4667
+ name: { type: 'string', required: false, default: 'world', description: 'Who to greet' },
4668
+ },
4669
+ handler: async ({ name }, cmdCtx) => {
4670
+ const count = ((await cmdCtx.state.get('count')) as number | null) ?? 0
4671
+ await cmdCtx.state.set('count', count + 1)
4672
+ ctx.invalidateWidget?.()
4673
+ return \`Hello, \${name}! (called \${count + 1}x by \${cmdCtx.agent.id})\`
4674
+ },
4675
+ })
4676
+ ${widget} },
4677
+ })
4678
+
4679
+ await app.start()
4680
+ `;
4681
+ }
4682
+ function srcMainTsx(_ctx) {
4683
+ return `import { StrictMode } from 'react'
4684
+ import { createRoot } from 'react-dom/client'
4685
+ import { App } from './App'
4686
+
4687
+ createRoot(document.getElementById('root')!).render(
4688
+ <StrictMode>
4689
+ <App />
4690
+ </StrictMode>,
4691
+ )
4692
+ `;
4693
+ }
4694
+ function srcAppTsx(ctx) {
4695
+ return `import { useEffect, useState } from 'react'
4696
+ import { callCommand } from './lib/api'
4697
+
4698
+ export function App() {
4699
+ const [count, setCount] = useState<number | null>(null)
4700
+ const [error, setError] = useState<string | null>(null)
4701
+ useEffect(() => {
4702
+ callCommand('hello', { name: 'browser' })
4703
+ .then((res) => {
4704
+ const m = /\\(called (\\d+)x/.exec(String(res))
4705
+ if (m) setCount(Number(m[1]))
4706
+ })
4707
+ .catch((e: Error) => setError(e.message))
4708
+ }, [])
4709
+ return (
4710
+ <main style={{ fontFamily: 'sans-serif', padding: 24 }}>
4711
+ <h1>${ctx.name}</h1>
4712
+ {error ? <pre style={{ color: 'crimson' }}>{error}</pre> : <p>Calls so far: {count ?? '...'}</p>}
4713
+ </main>
4714
+ )
4715
+ }
4716
+ `;
4717
+ }
4718
+ function srcApiTs() {
4719
+ return `// Tiny client for calling app routes/commands from the browser.
4720
+ // Routes resolve as /apps/<name>/* on the platform; the platform forwards
4721
+ // each request to your app's WebSocket session.
4722
+
4723
+ const APP_BASE = ((): string => {
4724
+ const m = location.pathname.match(/^\\/apps\\/[^/]+\\//)
4725
+ return m ? m[0] : '/'
4726
+ })()
4727
+
4728
+ export async function callCommand(name: string, params: Record<string, unknown> = {}) {
4729
+ const r = await fetch(\`\${APP_BASE}api/commands/\${encodeURIComponent(name)}\`, {
4730
+ method: 'POST',
4731
+ headers: { 'Content-Type': 'application/json' },
4732
+ body: JSON.stringify(params),
4733
+ })
4734
+ if (!r.ok) throw new Error(await r.text())
4735
+ return r.json()
4736
+ }
4737
+
4738
+ export async function callAction(name: string, params: Record<string, unknown> = {}) {
4739
+ const r = await fetch(\`\${APP_BASE}api/actions/\${encodeURIComponent(name)}\`, {
4740
+ method: 'POST',
4741
+ headers: { 'Content-Type': 'application/json' },
4742
+ body: JSON.stringify(params),
4743
+ })
4744
+ if (!r.ok) throw new Error(await r.text())
4745
+ return r.json()
4746
+ }
4747
+ `;
4748
+ }
4749
+ function contractsTs(ctx) {
4750
+ return `// Types shared between api/ (backend) and src/ (frontend).
4751
+ // Importing from one side picks up changes on the other immediately.
4752
+
4753
+ export interface ${pascal(ctx.name)}State {
4754
+ count: number
4755
+ }
4756
+ `;
4757
+ }
4758
+ function gitignore() {
4759
+ return `node_modules/
4760
+ dist/
4761
+ .botapp-sim/
4762
+ *.log
4763
+ .DS_Store
4764
+ .env*.local
4765
+ `;
4766
+ }
4767
+ function readme(ctx, headless) {
4768
+ const surfaces = headless ? "- Backend only (no frontend)" : "- React + Vite frontend (built to `dist/public/`)\n- Dashboard widget (declared in `api/index.ts`)";
4769
+ return `# ${ctx.name}
4770
+
4771
+ ${ctx.description}
4772
+
4773
+ ## Surfaces
4774
+
4775
+ ${surfaces}
4776
+ - One agent-facing command: \`hello\`
4777
+
4778
+ ## Develop
4779
+
4780
+ \`\`\`bash
4781
+ pnpm install
4782
+ pnpm build # api \u2192 dist/api.js${headless ? "" : ", frontend \u2192 dist/public/"}
4783
+ bot login --server <your-botapp-server>
4784
+ bot simulate # dev-tunnel; hot-reload as you build
4785
+ \`\`\`
4786
+
4787
+ The simulator opens a per-user shadow of this app on the server. Your
4788
+ real dashboard at the server's URL routes the app to *your* local
4789
+ process; nobody else sees it.
4790
+
4791
+ ## Publish
4792
+
4793
+ \`\`\`bash
4794
+ bot publish # private, only you see it
4795
+ bot publish --public # requests admin review for public visibility
4796
+ \`\`\`
4797
+ `;
4798
+ }
4799
+ function escapeHtml(s) {
4800
+ return s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
4801
+ }
4802
+ function pascal(s) {
4803
+ return s.split(/[-_\s]+/).filter(Boolean).map((w) => w[0].toUpperCase() + w.slice(1)).join("");
4804
+ }
4805
+ function dirname2(p) {
4806
+ const i = p.lastIndexOf("/");
4807
+ return i < 0 ? "." : p.slice(0, i);
4808
+ }
4809
+
4810
+ // src/commands/publish.ts
4811
+ import { Command as Command23 } from "commander";
4812
+ import { resolve as resolve10, join as join13, relative as relative2 } from "path";
4813
+ import { existsSync as existsSync15, readFileSync as readFileSync10, statSync as statSync4, readdirSync as readdirSync2 } from "fs";
4814
+ import { createGzip } from "zlib";
4815
+ import { spawn as spawn9 } from "child_process";
4816
+ import pc24 from "picocolors";
4817
+ 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) => {
4818
+ const absPath = resolve10(appPath);
4819
+ const manifestPath = join13(absPath, "botapp.app.json");
4820
+ if (!existsSync15(manifestPath)) {
4821
+ console.error(pc24.red(`No botapp.app.json found in ${absPath}`));
4822
+ process.exitCode = 1;
4823
+ return;
4824
+ }
4825
+ const manifest = JSON.parse(readFileSync10(manifestPath, "utf8"));
4826
+ if (!manifest.name) {
4827
+ console.error(pc24.red('manifest missing "name"'));
4828
+ process.exitCode = 1;
4829
+ return;
4830
+ }
4831
+ const server = resolveServerUrl(opts.server);
4832
+ const token = resolveToken(opts.token);
4833
+ if (!token) {
4834
+ console.error(pc24.red("not logged in: run `bot login` or pass --token"));
4835
+ process.exitCode = 1;
4836
+ return;
4837
+ }
4838
+ if (opts.build !== false) {
4839
+ console.log(pc24.dim("building app..."));
4840
+ const ok = await runBuild(absPath);
4841
+ if (!ok) {
4842
+ console.error(pc24.red("build failed; aborting"));
4843
+ process.exitCode = 1;
4844
+ return;
4845
+ }
4846
+ }
4847
+ const bundleDir = opts.bundleDir ? resolve10(absPath, opts.bundleDir) : pickBundleDir(absPath);
4848
+ let bundleB64;
4849
+ if (bundleDir && existsSync15(bundleDir)) {
4850
+ console.log(pc24.dim(`packing ${relative2(absPath, bundleDir)}/ \u2192 tar.gz...`));
4851
+ const bytes = await packDirToTarGz(bundleDir);
4852
+ bundleB64 = bytes.toString("base64");
4853
+ console.log(pc24.dim(` bundle: ${(bytes.length / 1024).toFixed(1)} KiB`));
4854
+ } else {
4855
+ console.log(pc24.dim("no frontend bundle to upload (headless app or no dist/public)"));
4856
+ }
4857
+ console.log(pc24.dim(`uploading to ${server}/api/apps/upload (${opts.public ? "public" : "private"})...`));
4858
+ const res = await fetch(`${server}/api/apps/upload`, {
4859
+ method: "POST",
4860
+ headers: authHeaders(token),
4861
+ body: JSON.stringify({
4862
+ manifest,
4863
+ bundleB64,
4864
+ visibility: opts.public ? "public" : "private"
4865
+ })
4866
+ });
4867
+ if (!res.ok) {
4868
+ const body = await res.text().catch(() => "");
4869
+ console.error(pc24.red(`upload failed (${res.status}): ${body}`));
4870
+ process.exitCode = 1;
4871
+ return;
4872
+ }
4873
+ const data = await res.json();
4874
+ if (!data.ok) {
4875
+ console.error(pc24.red(`upload rejected: ${data.error ?? "unknown"}`));
4876
+ process.exitCode = 1;
4877
+ return;
4878
+ }
4879
+ console.log(pc24.green("\u2713"), data.message ?? "uploaded");
4880
+ if (data.install) {
4881
+ console.log(pc24.dim(` id: ${data.install.id}`));
4882
+ console.log(pc24.dim(` version: ${data.install.version}`));
4883
+ console.log(pc24.dim(` visibility: ${data.install.visibility}`));
4884
+ if (data.install.reviewStatus !== "none") {
4885
+ console.log(pc24.dim(` review: ${data.install.reviewStatus}`));
4886
+ }
4887
+ }
4888
+ });
4889
+ function runBuild(cwd) {
4890
+ const pkgManager = existsSync15(join13(cwd, "pnpm-lock.yaml")) ? "pnpm" : "npm";
4891
+ const args = pkgManager === "pnpm" ? ["build"] : ["run", "build"];
4892
+ return new Promise((resolveP) => {
4893
+ const child = spawn9(pkgManager, args, { cwd, stdio: "inherit" });
4894
+ child.on("exit", (code) => resolveP(code === 0));
4895
+ child.on("error", () => resolveP(false));
4896
+ });
4897
+ }
4898
+ function pickBundleDir(appDir) {
4899
+ const distPublic = join13(appDir, "dist", "public");
4900
+ if (existsSync15(distPublic)) return distPublic;
4901
+ const dist = join13(appDir, "dist");
4902
+ if (existsSync15(dist)) return dist;
4903
+ return null;
4904
+ }
4905
+ async function packDirToTarGz(rootDir) {
4906
+ const entries = [];
4907
+ walk(rootDir, "", entries);
4908
+ const blocks = [];
4909
+ for (const e of entries) {
4910
+ blocks.push(tarHeader(e.name, e.size));
4911
+ blocks.push(e.data);
4912
+ const pad = (512 - e.size % 512) % 512;
4913
+ if (pad) blocks.push(Buffer.alloc(pad));
4914
+ }
4915
+ blocks.push(Buffer.alloc(1024));
4916
+ const tar = Buffer.concat(blocks);
4917
+ return await gzip(tar);
4918
+ }
4919
+ function walk(root, prefix, out) {
4920
+ for (const entry of readdirSync2(root)) {
4921
+ const full = join13(root, entry);
4922
+ const st = statSync4(full);
4923
+ const rel = (prefix ? `${prefix}/` : "") + entry;
4924
+ if (st.isDirectory()) {
4925
+ walk(full, rel, out);
4926
+ } else if (st.isFile()) {
4927
+ const data = readFileSync10(full);
4928
+ out.push({ name: rel, size: data.length, data });
4929
+ }
4930
+ }
4931
+ }
4932
+ function tarHeader(name, size) {
4933
+ const h = Buffer.alloc(512);
4934
+ h.write(name.length > 100 ? name.slice(name.length - 100) : name, 0, 100);
4935
+ h.write("0000644", 100, 7);
4936
+ h.write("0000000", 108, 7);
4937
+ h.write("0000000", 116, 7);
4938
+ h.write(size.toString(8).padStart(11, "0"), 124, 11);
4939
+ h.write(Math.floor(Date.now() / 1e3).toString(8).padStart(11, "0"), 136, 11);
4940
+ h.write(" ", 148, 8);
4941
+ h.write("0", 156, 1);
4942
+ h.write("ustar ", 257, 8);
4943
+ let cksum = 0;
4944
+ for (const b of h) cksum += b;
4945
+ h.write(cksum.toString(8).padStart(6, "0") + "\0 ", 148, 8);
4946
+ return h;
4947
+ }
4948
+ function gzip(input2) {
4949
+ return new Promise((resolveP, rejectP) => {
4950
+ const gz = createGzip();
4951
+ const chunks = [];
4952
+ gz.on("data", (c) => chunks.push(c));
4953
+ gz.on("end", () => resolveP(Buffer.concat(chunks)));
4954
+ gz.on("error", rejectP);
4955
+ gz.end(input2);
4956
+ });
4957
+ }
4958
+
4959
+ // src/commands/review.ts
4960
+ import { Command as Command24 } from "commander";
4961
+ import pc25 from "picocolors";
4962
+ var reviewCommand = new Command24("review").description("Admin: review queue for public-visibility uploads");
4963
+ reviewCommand.command("list").description("List pending public-visibility uploads").option("-s, --server <url>", "Server URL").option("-t, --token <token>", "Auth token").action(async (opts) => {
4964
+ const server = resolveServerUrl(opts.server);
4965
+ const token = resolveToken(opts.token);
4966
+ if (!token) return die("not logged in: run `bot login` or pass --token");
4967
+ const res = await fetch(`${server}/api/admin/review-queue`, { headers: authHeaders(token) });
4968
+ if (!res.ok) {
4969
+ const body = await res.text().catch(() => "");
4970
+ return die(`request failed (${res.status}): ${body}`);
4971
+ }
4972
+ const data = await res.json();
4973
+ if (!data.pending?.length) {
4974
+ console.log(pc25.dim("queue empty"));
4975
+ return;
4976
+ }
4977
+ for (const i of data.pending) {
4978
+ console.log(pc25.bold(i.id), pc25.cyan(i.appName), pc25.dim(`v${i.version}`));
4979
+ console.log(pc25.dim(` uploader: ${i.ownerUserId ?? "(server-wide)"}`));
4980
+ console.log(pc25.dim(` uploaded: ${i.uploadedAt}`));
4981
+ const desc = i.manifest?.description;
4982
+ if (desc) console.log(pc25.dim(` description: ${desc}`));
4983
+ console.log();
4984
+ }
4985
+ });
4986
+ 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));
4987
+ 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));
4988
+ async function decide(id, decision, opts) {
4989
+ const server = resolveServerUrl(opts.server);
4990
+ const token = resolveToken(opts.token);
4991
+ if (!token) return die("not logged in: run `bot login` or pass --token");
4992
+ const res = await fetch(`${server}/api/admin/review/${encodeURIComponent(id)}`, {
4993
+ method: "POST",
4994
+ headers: authHeaders(token),
4995
+ body: JSON.stringify({ decision, notes: opts.notes })
4996
+ });
4997
+ if (!res.ok) {
4998
+ const body = await res.text().catch(() => "");
4999
+ return die(`request failed (${res.status}): ${body}`);
5000
+ }
5001
+ const data = await res.json();
5002
+ if (!data.ok) return die(`server rejected: ${data.error ?? "unknown"}`);
5003
+ console.log(pc25.green("\u2713"), `${decision}d`, data.install?.appName, pc25.dim(`(${data.install?.id})`));
5004
+ if (data.install?.reviewNotes) console.log(pc25.dim(` notes: ${data.install.reviewNotes}`));
5005
+ }
5006
+ function die(msg) {
5007
+ console.error(pc25.red(msg));
5008
+ process.exitCode = 1;
5009
+ }
5010
+
3353
5011
  // src/index.ts
3354
- var version = "0.2.4";
3355
- 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");
5012
+ var version = "0.2.7";
5013
+ 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");
3356
5014
  program.addCommand(launchCommand);
3357
5015
  program.addCommand(runCommand);
3358
5016
  program.addCommand(appsCommand);
@@ -3363,10 +5021,15 @@ program.addCommand(loginCommand);
3363
5021
  program.addCommand(agentCommand);
3364
5022
  program.addCommand(pairingCommand);
3365
5023
  program.addCommand(daemonCommand);
5024
+ program.addCommand(doctorCommand);
5025
+ program.addCommand(initCommand);
3366
5026
  program.addCommand(installCommand);
3367
5027
  program.addCommand(uninstallCommand);
3368
5028
  program.addCommand(reloadCommand);
3369
5029
  program.addCommand(configCommand);
5030
+ program.addCommand(simulateCommand);
5031
+ program.addCommand(publishCommand);
5032
+ program.addCommand(reviewCommand);
3370
5033
  program.addCommand(serverCommand);
3371
5034
  program.addCommand(updateCommand);
3372
5035
  program.addCommand(devCommand, { hidden: true });
@@ -3410,29 +5073,29 @@ To discover what params a command accepts:
3410
5073
  program.on("command:*", (operands) => {
3411
5074
  const first = operands[0];
3412
5075
  const known = program.commands.filter((c) => !c._hidden).map((c) => c.name());
3413
- const topLevelHint = `Run ${pc21.cyan("bot --help")} for the list of top-level commands.`;
5076
+ const topLevelHint = `Run ${pc26.cyan("bot --help")} for the list of top-level commands.`;
3414
5077
  const argv = process.argv.slice(2);
3415
5078
  const firstIdx = argv.indexOf(first);
3416
5079
  const tail = firstIdx >= 0 ? argv.slice(firstIdx + 1) : [];
3417
5080
  if (tail.length > 0) {
3418
5081
  const suggested = `bot run ${first} ${tail.join(" ")}`;
3419
5082
  console.error(
3420
- pc21.red(`error: unknown command '${first}'`) + `
5083
+ pc26.red(`error: unknown command '${first}'`) + `
3421
5084
 
3422
- App commands go through ${pc21.bold("bot run")}. Did you mean:
5085
+ App commands go through ${pc26.bold("bot run")}. Did you mean:
3423
5086
 
3424
- ${pc21.cyan(suggested)}
5087
+ ${pc26.cyan(suggested)}
3425
5088
 
3426
5089
  ${topLevelHint}`
3427
5090
  );
3428
5091
  process.exit(1);
3429
5092
  }
3430
5093
  console.error(
3431
- pc21.red(`error: unknown command '${first}'`) + `
5094
+ pc26.red(`error: unknown command '${first}'`) + `
3432
5095
 
3433
5096
  If '${first}' is an app name, invoke one of its commands with:
3434
- ${pc21.cyan(`bot run ${first} <command> [--key value ...]`)}
3435
- ${pc21.cyan("bot apps --json")} ${pc21.dim("(to see what commands exist)")}
5097
+ ${pc26.cyan(`bot run ${first} <command> [--key value ...]`)}
5098
+ ${pc26.cyan("bot apps --json")} ${pc26.dim("(to see what commands exist)")}
3436
5099
 
3437
5100
  Top-level commands: ${known.join(", ")}
3438
5101
  ${topLevelHint}`