@vicvic-im/agent-bot 0.1.1 → 0.1.3

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/cli.cjs CHANGED
@@ -24,8 +24,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/cli.ts
27
- var import_node_child_process2 = require("child_process");
28
- var import_node_path7 = require("path");
27
+ var import_node_child_process3 = require("child_process");
28
+ var import_node_path6 = require("path");
29
29
  var import_commander = require("commander");
30
30
  var import_picocolors2 = __toESM(require("picocolors"), 1);
31
31
 
@@ -136,11 +136,11 @@ function parseEnv(value) {
136
136
  if (value === void 0 || value === null) return void 0;
137
137
  const record = asRecord(value, "agent.env");
138
138
  const env = {};
139
- for (const [key, entry2] of Object.entries(record)) {
140
- if (typeof entry2 !== "string") {
139
+ for (const [key, entry] of Object.entries(record)) {
140
+ if (typeof entry !== "string") {
141
141
  throw new ConfigError(`Invalid "agent.env.${key}": expected a string`);
142
142
  }
143
- env[key] = entry2;
143
+ env[key] = entry;
144
144
  }
145
145
  return env;
146
146
  }
@@ -184,7 +184,7 @@ function parseAgent(root, configDir) {
184
184
  return {
185
185
  name: agentName,
186
186
  command: optionalString(agentRaw.command, "agent.command"),
187
- args: Array.isArray(argsRaw) ? argsRaw.map((entry2) => String(entry2)) : void 0,
187
+ args: Array.isArray(argsRaw) ? argsRaw.map((entry) => String(entry)) : void 0,
188
188
  cwd,
189
189
  model: optionalString(agentRaw.model, "agent.model") ?? null,
190
190
  env: parseEnv(agentRaw.env)
@@ -318,7 +318,7 @@ var import_node_fs3 = require("fs");
318
318
  var import_node_path3 = require("path");
319
319
  function resolveServerEntry(cliPath2) {
320
320
  const real = (0, import_node_fs3.realpathSync)(cliPath2);
321
- const candidate = (0, import_node_path3.join)((0, import_node_path3.dirname)(real), "server.js");
321
+ const candidate = (0, import_node_path3.join)((0, import_node_path3.dirname)(real), "bin-server.js");
322
322
  if (!(0, import_node_fs3.existsSync)(candidate)) {
323
323
  throw new Error(`Server bundle not found at ${candidate}. Reinstall @vicvic-im/agent-bot.`);
324
324
  }
@@ -333,17 +333,38 @@ function readPackageVersion(cliPath2) {
333
333
  }
334
334
  }
335
335
 
336
- // src/server.ts
337
- var import_node_path6 = require("path");
336
+ // src/core/pm2.ts
337
+ var import_node_child_process = require("child_process");
338
+ var ENV_BIN = "/usr/bin/env";
339
+ function hasPm2(exec = import_node_child_process.execFileSync) {
340
+ try {
341
+ exec(ENV_BIN, ["pm2", "--version"], { stdio: "ignore" });
342
+ return true;
343
+ } catch {
344
+ return false;
345
+ }
346
+ }
347
+ function ensurePm2(exec = import_node_child_process.execFileSync) {
348
+ if (hasPm2(exec)) return true;
349
+ try {
350
+ exec(ENV_BIN, ["npm", "i", "-g", "pm2"], { stdio: "inherit" });
351
+ return hasPm2(exec);
352
+ } catch {
353
+ return false;
354
+ }
355
+ }
338
356
 
339
357
  // src/agents/acp.ts
340
- var import_node_child_process = require("child_process");
358
+ var import_node_child_process2 = require("child_process");
341
359
  var import_node_stream = require("stream");
342
360
  var acp = __toESM(require("@agentclientprotocol/sdk"), 1);
343
361
 
344
362
  // src/agents/types.ts
345
363
  var EMPTY_USAGE = { used: 0, size: 0, cost: null };
346
364
 
365
+ // src/version.ts
366
+ var VBOT_VERSION = "0.1.3";
367
+
347
368
  // src/agents/acp.ts
348
369
  var AgentAbortError = class extends Error {
349
370
  constructor() {
@@ -360,6 +381,14 @@ var AgentStartError = class extends Error {
360
381
  stderr;
361
382
  };
362
383
  var STDERR_LIMIT = 4e3;
384
+ function flattenSelectOptions(options) {
385
+ return options.flatMap(
386
+ (entry) => "group" in entry ? entry.options.map((option) => ({ value: option.value, label: option.name })) : [{ value: entry.value, label: entry.name }]
387
+ );
388
+ }
389
+ function isModelOption(option) {
390
+ return option.type === "select" && (option.category === "model" || option.id === "model");
391
+ }
363
392
  var AcpAgent = class {
364
393
  constructor(name, config) {
365
394
  this.name = name;
@@ -389,6 +418,14 @@ var AcpAgent = class {
389
418
  sessionId() {
390
419
  return this.session;
391
420
  }
421
+ currentModel() {
422
+ const option = this.configOptions.find(isModelOption);
423
+ if (!option || option.type !== "select") return null;
424
+ const value = String(option.currentValue);
425
+ const options = flattenSelectOptions(option.options);
426
+ const label = options.find((entry) => entry.value === value)?.label ?? value;
427
+ return { value, label, options };
428
+ }
392
429
  resetSession() {
393
430
  this.session = null;
394
431
  }
@@ -404,7 +441,7 @@ var AcpAgent = class {
404
441
  const definition = definitionOf(this.name);
405
442
  const command = this.config.command ?? definition.command.command;
406
443
  const args = this.config.args ?? definition.command.args;
407
- const child = (0, import_node_child_process.spawn)(command, args, {
444
+ const child = (0, import_node_child_process2.spawn)(command, args, {
408
445
  cwd: this.config.cwd,
409
446
  env: { ...process.env, ...this.config.env },
410
447
  stdio: ["pipe", "pipe", "pipe"]
@@ -456,7 +493,7 @@ var AcpAgent = class {
456
493
  const init = await this.ctx.request(acp.methods.agent.initialize, {
457
494
  protocolVersion: 1,
458
495
  clientCapabilities: {},
459
- clientInfo: { name: "vbot", version: "0.1.0" }
496
+ clientInfo: { name: "vbot", version: VBOT_VERSION }
460
497
  });
461
498
  this.initCaps = init.agentCapabilities;
462
499
  } catch (error) {
@@ -543,7 +580,7 @@ var AcpAgent = class {
543
580
  );
544
581
  pending.catch(() => void 0);
545
582
  const response = await Promise.race([pending, aborted]);
546
- await new Promise((resolve5) => setImmediate(resolve5));
583
+ await new Promise((resolve4) => setImmediate(resolve4));
547
584
  if (response.stopReason === "cancelled" || request.signal.aborted) {
548
585
  throw new AgentAbortError();
549
586
  }
@@ -575,15 +612,14 @@ var AcpAgent = class {
575
612
  this.pendingModel = model;
576
613
  return true;
577
614
  }
578
- const option = this.configOptions.find(
579
- (entry2) => entry2.type === "select" && (entry2.id === "model" || entry2.category === "model")
580
- );
615
+ const option = this.configOptions.find(isModelOption);
581
616
  if (!option) return false;
582
- await this.ctx.request(acp.methods.agent.session.setConfigOption, {
617
+ const response = await this.ctx.request(acp.methods.agent.session.setConfigOption, {
583
618
  sessionId: this.session,
584
619
  configId: option.id,
585
620
  value: model
586
621
  });
622
+ if (response?.configOptions?.length) this.configOptions = response.configOptions;
587
623
  this.pendingModel = null;
588
624
  return true;
589
625
  }
@@ -602,8 +638,8 @@ var AcpAgent = class {
602
638
  } catch {
603
639
  }
604
640
  if (!child || child.exitCode !== null) return;
605
- const exited = new Promise((resolve5) => {
606
- child.once("exit", () => resolve5());
641
+ const exited = new Promise((resolve4) => {
642
+ child.once("exit", () => resolve4());
607
643
  });
608
644
  child.kill("SIGTERM");
609
645
  const timer = setTimeout(() => child.kill("SIGKILL"), 3e3);
@@ -719,21 +755,29 @@ var BotQueue = class {
719
755
  }
720
756
  };
721
757
 
722
- // src/version.ts
723
- var VBOT_VERSION = "0.1.1";
724
-
725
758
  // src/core/dispatcher.ts
726
- var HELP_TEXT = `Available commands:
727
- /new \u2014 Start a new session
728
- /stop \u2014 Abort the current request
729
- /status \u2014 Show agent, session and queue status
730
- /agent [name] \u2014 Show or switch agent backend
731
- /model [name] \u2014 Show or switch model
732
- /cost \u2014 Show context usage and cost
733
- /version \u2014 Show vbot version
734
- /help \u2014 Show this help
735
-
736
- Other /commands are sent to the agent as prompts.`;
759
+ var import_node_os2 = require("os");
760
+ var HELP_ROWS = [
761
+ ["/new", "Start a new session"],
762
+ ["/stop", "Abort the current request"],
763
+ ["/status", "Show agent, session and queue status"],
764
+ ["/agent [name]", "Show or switch agent backend"],
765
+ ["/model [name]", "Show or switch model"],
766
+ ["/cost", "Show context usage and cost"],
767
+ ["/version", "Show vbot version"],
768
+ ["/help", "Show this help"]
769
+ ];
770
+ var HELP_FOOTER = "Other /commands are sent to the agent as prompts.";
771
+ var NEW_BUSY_TEXT = "Cannot reset the session while a request is running. Use /stop first.";
772
+ var MODEL_BUSY_TEXT = "Cannot switch model while a request is running. Use /stop first.";
773
+ var HELP_TEXT = [
774
+ table(
775
+ ["Command", "Description"],
776
+ HELP_ROWS.map(([command, description]) => [command, description])
777
+ ),
778
+ "",
779
+ HELP_FOOTER
780
+ ].join("\n");
737
781
  function createDispatcher(runtime) {
738
782
  return (text, reply) => {
739
783
  const send = (message) => {
@@ -747,11 +791,57 @@ function createDispatcher(runtime) {
747
791
  });
748
792
  };
749
793
  }
794
+ function escapeCell(value) {
795
+ return value.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
796
+ }
797
+ function table(headers, rows) {
798
+ const head = `| ${headers.map(escapeCell).join(" | ")} |`;
799
+ const separator = `| ${headers.map(() => "---").join(" | ")} |`;
800
+ const body = rows.map((row) => `| ${row.map(escapeCell).join(" | ")} |`);
801
+ return [head, separator, ...body].join("\n");
802
+ }
803
+ function formatNumber(value) {
804
+ if (!Number.isFinite(value)) return String(value);
805
+ const negative = value < 0;
806
+ const digits = Math.abs(Math.trunc(value)).toString();
807
+ let out = "";
808
+ for (let i = 0; i < digits.length; i += 1) {
809
+ if (i > 0 && (digits.length - i) % 3 === 0) out += ",";
810
+ out += digits[i];
811
+ }
812
+ return negative ? "-" + out : out;
813
+ }
814
+ function percentOf(used, size) {
815
+ if (size <= 0) return null;
816
+ return `${(used / size * 100).toFixed(1)}%`;
817
+ }
818
+ function usageText(runtime) {
819
+ const { used, size } = runtime.state().usage;
820
+ if (size <= 0) return "not reported";
821
+ const base = `${formatNumber(used)} / ${formatNumber(size)}`;
822
+ const percent = percentOf(used, size);
823
+ return percent ? `${base} (${percent})` : base;
824
+ }
825
+ function compactPath(path) {
826
+ const home = (0, import_node_os2.homedir)();
827
+ if (!home) return path;
828
+ if (path === home) return "~";
829
+ const prefix = home + "/";
830
+ if (path.startsWith(prefix)) return "~" + path.slice(home.length);
831
+ return path;
832
+ }
833
+ function modelText(runtime) {
834
+ const current = runtime.agent().currentModel();
835
+ if (current) return current.label;
836
+ const configured = runtime.config().agent.model;
837
+ if (configured) return `${configured} (configured, not applied yet)`;
838
+ return "unknown (session not started)";
839
+ }
750
840
  function handleSlashCommand(runtime, text, send) {
751
841
  const [rawCommand, ...args] = text.split(/\s+/);
752
842
  switch (rawCommand.toLowerCase()) {
753
843
  case "/new":
754
- handleNew(runtime, send);
844
+ void handleNew(runtime, send);
755
845
  return null;
756
846
  case "/stop":
757
847
  handleStop(runtime, send);
@@ -763,7 +853,7 @@ function handleSlashCommand(runtime, text, send) {
763
853
  handleAgent(runtime, args, send);
764
854
  return null;
765
855
  case "/model":
766
- handleModel(runtime, args, send);
856
+ void handleModel(runtime, args, send);
767
857
  return null;
768
858
  case "/cost":
769
859
  send(costText(runtime));
@@ -778,12 +868,18 @@ function handleSlashCommand(runtime, text, send) {
778
868
  return text.slice(1);
779
869
  }
780
870
  }
781
- function handleNew(runtime, send) {
782
- runtime.agent().resetSession();
783
- const state = runtime.state();
784
- state.acpSessionId = null;
785
- state.usage = { used: 0, size: 0, cost: null };
786
- runtime.saveState();
871
+ async function handleNew(runtime, send) {
872
+ if (runtime.queue().status().busy) {
873
+ send(NEW_BUSY_TEXT);
874
+ return;
875
+ }
876
+ await runtime.serialize(async () => {
877
+ runtime.agent().resetSession();
878
+ const state = runtime.state();
879
+ state.acpSessionId = null;
880
+ state.usage = { used: 0, size: 0, cost: null };
881
+ runtime.saveState();
882
+ });
787
883
  send("Session reset. Starting a new conversation.");
788
884
  }
789
885
  function handleStop(runtime, send) {
@@ -799,12 +895,18 @@ function handleStop(runtime, send) {
799
895
  }
800
896
  function handleAgent(runtime, args, send) {
801
897
  const name = args.join(" ").trim();
898
+ const current = runtime.config().agent.name;
802
899
  if (!name) {
803
- const current = runtime.config().agent.name;
804
- const list = AGENT_NAMES.map((entry2) => `${entry2 === current ? "*" : " "} ${entry2}`).join("\n");
805
- send(`Current agent: ${current}
806
- Available agents:
807
- ${list}`);
900
+ send(
901
+ table(
902
+ ["Agent", "Backend", ""],
903
+ AGENT_NAMES.map((entry) => [
904
+ entry,
905
+ AGENTS[entry].label,
906
+ entry === current ? "\u2190 current" : ""
907
+ ])
908
+ )
909
+ );
808
910
  return;
809
911
  }
810
912
  if (!isAgentName(name)) {
@@ -820,56 +922,95 @@ ${list}`);
820
922
  (error) => send(`Error: ${error instanceof Error ? error.message : String(error)}`)
821
923
  );
822
924
  }
823
- function handleModel(runtime, args, send) {
925
+ async function handleModel(runtime, args, send) {
824
926
  const model = args.join(" ").trim();
825
927
  if (!model) {
826
- send(`Current model: ${runtime.config().agent.model ?? "default"}`);
928
+ send(modelReport(runtime));
929
+ return;
930
+ }
931
+ if (runtime.queue().status().busy) {
932
+ send(MODEL_BUSY_TEXT);
827
933
  return;
828
934
  }
829
935
  const config = runtime.config();
830
936
  const hadSession = runtime.agent().sessionId() !== null;
831
- config.agent.model = model;
832
- runtime.saveConfig();
833
- runtime.agent().setModel(model).then((supported) => {
834
- if (!supported) {
835
- send(
836
- `Model saved: ${model}. This agent does not expose a runtime model option; it will be used on the next session if supported.`
837
- );
838
- return;
839
- }
937
+ const supported = await runtime.serialize(async () => {
938
+ const accepted = await runtime.agent().setModel(model);
939
+ config.agent.model = model;
940
+ runtime.saveConfig();
941
+ return accepted;
942
+ });
943
+ const applied = runtime.agent().currentModel()?.label ?? model;
944
+ if (!supported) {
840
945
  send(
841
- hadSession ? `Model switched to: ${model}` : `Model set to: ${model}. It will apply when the session starts.`
946
+ `Model saved: ${model}. This agent does not expose a runtime model option; it will be used on the next session if supported.`
842
947
  );
843
- }).catch(
844
- (error) => send(`Error: ${error instanceof Error ? error.message : String(error)}`)
948
+ return;
949
+ }
950
+ send(
951
+ hadSession ? `Model switched to: ${applied}` : `Model set to: ${applied}. It will apply when the session starts.`
845
952
  );
846
953
  }
954
+ function modelReport(runtime) {
955
+ const current = runtime.agent().currentModel();
956
+ if (!current) {
957
+ const configured = runtime.config().agent.model;
958
+ const hint = configured ? `Configured: ${configured}
959
+
960
+ The agent has not reported a model yet; it becomes visible after the first message.` : "This agent does not expose a runtime model option, or the session has not started.";
961
+ return `Current: unknown
962
+
963
+ ${hint}`;
964
+ }
965
+ const lines = [`Current: ${current.label}`];
966
+ if (current.options.length > 0) {
967
+ lines.push(
968
+ "",
969
+ table(
970
+ ["Model", ""],
971
+ current.options.map((option) => [
972
+ option.label,
973
+ option.value === current.value ? "\u2190 current" : ""
974
+ ])
975
+ )
976
+ );
977
+ }
978
+ lines.push("", "Switch: /model <name>");
979
+ return lines.join("\n");
980
+ }
847
981
  function statusText(runtime) {
848
982
  const config = runtime.config();
849
983
  const agent = runtime.agent();
850
984
  const state = runtime.state();
851
985
  const queue = runtime.queue().status();
852
- const lines = [
853
- `Agent: ${config.agent.name} (${AGENTS[config.agent.name].label})`,
854
- `Session: ${agent.sessionId() ?? "none"}`,
855
- `Work dir: ${config.agent.cwd}`,
856
- `Model: ${config.agent.model ?? "default"}`,
857
- `Status: ${queue.busy ? "busy" : "idle"}`,
858
- `Queue: ${queue.queued}`,
859
- `Usage: ${state.usage.used}/${state.usage.size} tokens`,
860
- `Capabilities: ${capabilitiesText(agent.capabilities())}`
986
+ const { used, size } = state.usage;
987
+ const summary = [
988
+ `**${config.agent.name}**`,
989
+ queue.busy ? "busy" : "idle",
990
+ size > 0 ? `${formatNumber(used)} / ${formatNumber(size)}` : null
991
+ ].filter(Boolean).join(" \xB7 ");
992
+ const rows = [
993
+ ["Agent", `${config.agent.name} (${AGENTS[config.agent.name].label})`],
994
+ ["Session", agent.sessionId() ?? "none"],
995
+ ["Work dir", compactPath(config.agent.cwd)],
996
+ ["Model", modelText(runtime)],
997
+ ["Status", queue.busy ? "busy" : "idle"],
998
+ ["Queue", String(queue.queued)],
999
+ ["Usage", usageText(runtime)],
1000
+ ["Capabilities", capabilitiesText(agent.capabilities())]
861
1001
  ];
862
- return lines.join("\n");
1002
+ return [summary, "", table(["Field", "Value"], rows)].join("\n");
863
1003
  }
864
1004
  function costText(runtime) {
865
1005
  const usage = runtime.state().usage;
866
- const context = `Context: ${usage.used}/${usage.size} tokens`;
867
- if (!usage.cost) {
868
- return `${context}
869
- Cost: this agent does not report cost.`;
870
- }
871
- return `${context}
872
- Cost: ${usage.cost.amount} ${usage.cost.currency}`;
1006
+ const cost = usage.cost ? `${usage.cost.amount} ${usage.cost.currency}` : "not reported by this agent";
1007
+ return table(
1008
+ ["Field", "Value"],
1009
+ [
1010
+ ["Context", usageText(runtime)],
1011
+ ["Cost", cost]
1012
+ ]
1013
+ );
873
1014
  }
874
1015
  function capabilitiesText(caps) {
875
1016
  const enabled = [
@@ -924,10 +1065,10 @@ var import_node_path5 = require("path");
924
1065
  // src/core/state.ts
925
1066
  var import_node_crypto = require("crypto");
926
1067
  var import_node_fs4 = require("fs");
927
- var import_node_os2 = require("os");
1068
+ var import_node_os3 = require("os");
928
1069
  var import_node_path4 = require("path");
929
1070
  function vbotHome() {
930
- return process.env.VBOT_HOME?.trim() || (0, import_node_path4.join)((0, import_node_os2.homedir)(), ".vbot");
1071
+ return process.env.VBOT_HOME?.trim() || (0, import_node_path4.join)((0, import_node_os3.homedir)(), ".vbot");
931
1072
  }
932
1073
  function stateDir() {
933
1074
  return (0, import_node_path4.join)(vbotHome(), "state");
@@ -1085,10 +1226,10 @@ var VicvicWire = class {
1085
1226
  if (!socket || socket.readyState !== import_ws.default.OPEN) {
1086
1227
  throw new Error("Bot connection is not open");
1087
1228
  }
1088
- await new Promise((resolve5, reject) => {
1229
+ await new Promise((resolve4, reject) => {
1089
1230
  socket.send(JSON.stringify({ type: "reply", msgUid, text }), (error) => {
1090
1231
  if (error) reject(error);
1091
- else resolve5();
1232
+ else resolve4();
1092
1233
  });
1093
1234
  });
1094
1235
  }
@@ -1256,13 +1397,17 @@ async function startBot(configPath) {
1256
1397
  saveConfig: () => saveConfig(config, configPath)
1257
1398
  };
1258
1399
  holder.queue = new BotQueue((item, signal) => gate(() => processPrompt(ctx, item, signal)));
1259
- const runtime = createRuntime(ctx, (name) => {
1260
- if (switching) return Promise.reject(new Error("Agent switch already in progress."));
1261
- switching = true;
1262
- return gate(() => switchAgent(ctx, name)).finally(() => {
1263
- switching = false;
1264
- });
1265
- });
1400
+ const runtime = createRuntime(
1401
+ ctx,
1402
+ (task) => gate(task),
1403
+ (name) => {
1404
+ if (switching) return Promise.reject(new Error("Agent switch already in progress."));
1405
+ switching = true;
1406
+ return gate(() => switchAgent(ctx, name)).finally(() => {
1407
+ switching = false;
1408
+ });
1409
+ }
1410
+ );
1266
1411
  const dispatch = createDispatcher(runtime);
1267
1412
  let notifySignal = () => void 0;
1268
1413
  const wire = new VicvicWire({
@@ -1323,7 +1468,7 @@ async function runBot(configPath) {
1323
1468
  const bot = await startBot(configPath);
1324
1469
  await bot.finished;
1325
1470
  }
1326
- function createRuntime(ctx, switchAgent2) {
1471
+ function createRuntime(ctx, gate, switchAgent2) {
1327
1472
  return {
1328
1473
  config: () => ctx.config,
1329
1474
  agent: () => ctx.getAgent(),
@@ -1331,6 +1476,7 @@ function createRuntime(ctx, switchAgent2) {
1331
1476
  state: () => ctx.state,
1332
1477
  saveState: () => ctx.saveState(),
1333
1478
  saveConfig: () => ctx.saveConfig(),
1479
+ serialize: (task) => gate(task),
1334
1480
  switchAgent: switchAgent2
1335
1481
  };
1336
1482
  }
@@ -1382,35 +1528,14 @@ ${error.stderr}`);
1382
1528
  console.error("[server] agent run failed:", error);
1383
1529
  replySafe(reply, `Error: ${error instanceof Error ? error.message : String(error)}`);
1384
1530
  }
1385
- var entry = process.argv[1] ?? "";
1386
- if (/(^|[\\/])server\.(js|cjs|ts)$/.test(entry)) {
1387
- const configPath = process.argv[2];
1388
- if (!configPath) {
1389
- console.error("Usage: server.js <config-path>");
1390
- process.exit(1);
1391
- }
1392
- runBot((0, import_node_path6.resolve)(configPath)).catch((error) => {
1393
- console.error(error instanceof Error ? error.message : String(error));
1394
- const expected = error instanceof ConfigError || error instanceof LockError;
1395
- process.exit(expected ? 2 : 1);
1396
- });
1397
- }
1398
1531
 
1399
1532
  // src/cli.ts
1400
1533
  function processName(cwd) {
1401
- return `vbot-${(0, import_node_path7.basename)((0, import_node_path7.resolve)(cwd)).replace(/[^a-zA-Z0-9._-]+/g, "-")}`;
1402
- }
1403
- var ENV_BIN = "/usr/bin/env";
1404
- function hasPm2() {
1405
- try {
1406
- (0, import_node_child_process2.execFileSync)(ENV_BIN, ["pm2", "--version"], { stdio: "ignore" });
1407
- return true;
1408
- } catch {
1409
- return false;
1410
- }
1534
+ return `vbot-${(0, import_node_path6.basename)((0, import_node_path6.resolve)(cwd)).replace(/[^a-zA-Z0-9._-]+/g, "-")}`;
1411
1535
  }
1536
+ var ENV_BIN2 = "/usr/bin/env";
1412
1537
  function pm2(args, capture = false) {
1413
- return (0, import_node_child_process2.execFileSync)(ENV_BIN, ["pm2", ...args], {
1538
+ return (0, import_node_child_process3.execFileSync)(ENV_BIN2, ["pm2", ...args], {
1414
1539
  stdio: capture ? "pipe" : "inherit",
1415
1540
  encoding: "utf8"
1416
1541
  });
@@ -1434,12 +1559,14 @@ var program = new import_commander.Command();
1434
1559
  program.name("vbot").description("Connect local coding agents to vicvic.im").version(readPackageVersion(cliPath()));
1435
1560
  program.command("start").description("Start vbot (prompts for config on first run)").option("--foreground", "run in the foreground instead of pm2").option("-c, --config <path>", "config file path").action(async (options) => {
1436
1561
  const { path } = await ensureConfig(options.config);
1437
- if (options.foreground || !hasPm2()) {
1438
- if (!options.foreground) {
1439
- console.log(
1440
- import_picocolors2.default.yellow("pm2 not found; running in the foreground. Install pm2 for background mode.")
1441
- );
1442
- }
1562
+ if (options.foreground) {
1563
+ await runBot(path);
1564
+ return;
1565
+ }
1566
+ if (!hasPm2()) console.log(import_picocolors2.default.yellow("pm2 not found, installing globally..."));
1567
+ if (!ensurePm2()) {
1568
+ console.error(import_picocolors2.default.red("Failed to install pm2. Please install manually: npm i -g pm2"));
1569
+ console.log(import_picocolors2.default.yellow("Running in the foreground instead."));
1443
1570
  await runBot(path);
1444
1571
  return;
1445
1572
  }