@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/server.cjs CHANGED
@@ -34,7 +34,6 @@ __export(server_exports, {
34
34
  startBot: () => startBot
35
35
  });
36
36
  module.exports = __toCommonJS(server_exports);
37
- var import_node_path4 = require("path");
38
37
 
39
38
  // src/config.ts
40
39
  var import_node_fs = require("fs");
@@ -137,11 +136,11 @@ function parseEnv(value) {
137
136
  if (value === void 0 || value === null) return void 0;
138
137
  const record = asRecord(value, "agent.env");
139
138
  const env = {};
140
- for (const [key, entry2] of Object.entries(record)) {
141
- if (typeof entry2 !== "string") {
139
+ for (const [key, entry] of Object.entries(record)) {
140
+ if (typeof entry !== "string") {
142
141
  throw new ConfigError(`Invalid "agent.env.${key}": expected a string`);
143
142
  }
144
- env[key] = entry2;
143
+ env[key] = entry;
145
144
  }
146
145
  return env;
147
146
  }
@@ -185,7 +184,7 @@ function parseAgent(root, configDir) {
185
184
  return {
186
185
  name: agentName,
187
186
  command: optionalString(agentRaw.command, "agent.command"),
188
- args: Array.isArray(argsRaw) ? argsRaw.map((entry2) => String(entry2)) : void 0,
187
+ args: Array.isArray(argsRaw) ? argsRaw.map((entry) => String(entry)) : void 0,
189
188
  cwd,
190
189
  model: optionalString(agentRaw.model, "agent.model") ?? null,
191
190
  env: parseEnv(agentRaw.env)
@@ -250,6 +249,9 @@ var acp = __toESM(require("@agentclientprotocol/sdk"), 1);
250
249
  // src/agents/types.ts
251
250
  var EMPTY_USAGE = { used: 0, size: 0, cost: null };
252
251
 
252
+ // src/version.ts
253
+ var VBOT_VERSION = "0.1.3";
254
+
253
255
  // src/agents/acp.ts
254
256
  var AgentAbortError = class extends Error {
255
257
  constructor() {
@@ -266,6 +268,14 @@ var AgentStartError = class extends Error {
266
268
  stderr;
267
269
  };
268
270
  var STDERR_LIMIT = 4e3;
271
+ function flattenSelectOptions(options) {
272
+ return options.flatMap(
273
+ (entry) => "group" in entry ? entry.options.map((option) => ({ value: option.value, label: option.name })) : [{ value: entry.value, label: entry.name }]
274
+ );
275
+ }
276
+ function isModelOption(option) {
277
+ return option.type === "select" && (option.category === "model" || option.id === "model");
278
+ }
269
279
  var AcpAgent = class {
270
280
  constructor(name, config) {
271
281
  this.name = name;
@@ -295,6 +305,14 @@ var AcpAgent = class {
295
305
  sessionId() {
296
306
  return this.session;
297
307
  }
308
+ currentModel() {
309
+ const option = this.configOptions.find(isModelOption);
310
+ if (!option || option.type !== "select") return null;
311
+ const value = String(option.currentValue);
312
+ const options = flattenSelectOptions(option.options);
313
+ const label = options.find((entry) => entry.value === value)?.label ?? value;
314
+ return { value, label, options };
315
+ }
298
316
  resetSession() {
299
317
  this.session = null;
300
318
  }
@@ -362,7 +380,7 @@ var AcpAgent = class {
362
380
  const init = await this.ctx.request(acp.methods.agent.initialize, {
363
381
  protocolVersion: 1,
364
382
  clientCapabilities: {},
365
- clientInfo: { name: "vbot", version: "0.1.0" }
383
+ clientInfo: { name: "vbot", version: VBOT_VERSION }
366
384
  });
367
385
  this.initCaps = init.agentCapabilities;
368
386
  } catch (error) {
@@ -449,7 +467,7 @@ var AcpAgent = class {
449
467
  );
450
468
  pending.catch(() => void 0);
451
469
  const response = await Promise.race([pending, aborted]);
452
- await new Promise((resolve3) => setImmediate(resolve3));
470
+ await new Promise((resolve2) => setImmediate(resolve2));
453
471
  if (response.stopReason === "cancelled" || request.signal.aborted) {
454
472
  throw new AgentAbortError();
455
473
  }
@@ -481,15 +499,14 @@ var AcpAgent = class {
481
499
  this.pendingModel = model;
482
500
  return true;
483
501
  }
484
- const option = this.configOptions.find(
485
- (entry2) => entry2.type === "select" && (entry2.id === "model" || entry2.category === "model")
486
- );
502
+ const option = this.configOptions.find(isModelOption);
487
503
  if (!option) return false;
488
- await this.ctx.request(acp.methods.agent.session.setConfigOption, {
504
+ const response = await this.ctx.request(acp.methods.agent.session.setConfigOption, {
489
505
  sessionId: this.session,
490
506
  configId: option.id,
491
507
  value: model
492
508
  });
509
+ if (response?.configOptions?.length) this.configOptions = response.configOptions;
493
510
  this.pendingModel = null;
494
511
  return true;
495
512
  }
@@ -508,8 +525,8 @@ var AcpAgent = class {
508
525
  } catch {
509
526
  }
510
527
  if (!child || child.exitCode !== null) return;
511
- const exited = new Promise((resolve3) => {
512
- child.once("exit", () => resolve3());
528
+ const exited = new Promise((resolve2) => {
529
+ child.once("exit", () => resolve2());
513
530
  });
514
531
  child.kill("SIGTERM");
515
532
  const timer = setTimeout(() => child.kill("SIGKILL"), 3e3);
@@ -625,21 +642,29 @@ var BotQueue = class {
625
642
  }
626
643
  };
627
644
 
628
- // src/version.ts
629
- var VBOT_VERSION = "0.1.1";
630
-
631
645
  // src/core/dispatcher.ts
632
- var HELP_TEXT = `Available commands:
633
- /new \u2014 Start a new session
634
- /stop \u2014 Abort the current request
635
- /status \u2014 Show agent, session and queue status
636
- /agent [name] \u2014 Show or switch agent backend
637
- /model [name] \u2014 Show or switch model
638
- /cost \u2014 Show context usage and cost
639
- /version \u2014 Show vbot version
640
- /help \u2014 Show this help
641
-
642
- Other /commands are sent to the agent as prompts.`;
646
+ var import_node_os2 = require("os");
647
+ var HELP_ROWS = [
648
+ ["/new", "Start a new session"],
649
+ ["/stop", "Abort the current request"],
650
+ ["/status", "Show agent, session and queue status"],
651
+ ["/agent [name]", "Show or switch agent backend"],
652
+ ["/model [name]", "Show or switch model"],
653
+ ["/cost", "Show context usage and cost"],
654
+ ["/version", "Show vbot version"],
655
+ ["/help", "Show this help"]
656
+ ];
657
+ var HELP_FOOTER = "Other /commands are sent to the agent as prompts.";
658
+ var NEW_BUSY_TEXT = "Cannot reset the session while a request is running. Use /stop first.";
659
+ var MODEL_BUSY_TEXT = "Cannot switch model while a request is running. Use /stop first.";
660
+ var HELP_TEXT = [
661
+ table(
662
+ ["Command", "Description"],
663
+ HELP_ROWS.map(([command, description]) => [command, description])
664
+ ),
665
+ "",
666
+ HELP_FOOTER
667
+ ].join("\n");
643
668
  function createDispatcher(runtime) {
644
669
  return (text, reply) => {
645
670
  const send = (message) => {
@@ -653,11 +678,57 @@ function createDispatcher(runtime) {
653
678
  });
654
679
  };
655
680
  }
681
+ function escapeCell(value) {
682
+ return value.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
683
+ }
684
+ function table(headers, rows) {
685
+ const head = `| ${headers.map(escapeCell).join(" | ")} |`;
686
+ const separator = `| ${headers.map(() => "---").join(" | ")} |`;
687
+ const body = rows.map((row) => `| ${row.map(escapeCell).join(" | ")} |`);
688
+ return [head, separator, ...body].join("\n");
689
+ }
690
+ function formatNumber(value) {
691
+ if (!Number.isFinite(value)) return String(value);
692
+ const negative = value < 0;
693
+ const digits = Math.abs(Math.trunc(value)).toString();
694
+ let out = "";
695
+ for (let i = 0; i < digits.length; i += 1) {
696
+ if (i > 0 && (digits.length - i) % 3 === 0) out += ",";
697
+ out += digits[i];
698
+ }
699
+ return negative ? "-" + out : out;
700
+ }
701
+ function percentOf(used, size) {
702
+ if (size <= 0) return null;
703
+ return `${(used / size * 100).toFixed(1)}%`;
704
+ }
705
+ function usageText(runtime) {
706
+ const { used, size } = runtime.state().usage;
707
+ if (size <= 0) return "not reported";
708
+ const base = `${formatNumber(used)} / ${formatNumber(size)}`;
709
+ const percent = percentOf(used, size);
710
+ return percent ? `${base} (${percent})` : base;
711
+ }
712
+ function compactPath(path) {
713
+ const home = (0, import_node_os2.homedir)();
714
+ if (!home) return path;
715
+ if (path === home) return "~";
716
+ const prefix = home + "/";
717
+ if (path.startsWith(prefix)) return "~" + path.slice(home.length);
718
+ return path;
719
+ }
720
+ function modelText(runtime) {
721
+ const current = runtime.agent().currentModel();
722
+ if (current) return current.label;
723
+ const configured = runtime.config().agent.model;
724
+ if (configured) return `${configured} (configured, not applied yet)`;
725
+ return "unknown (session not started)";
726
+ }
656
727
  function handleSlashCommand(runtime, text, send) {
657
728
  const [rawCommand, ...args] = text.split(/\s+/);
658
729
  switch (rawCommand.toLowerCase()) {
659
730
  case "/new":
660
- handleNew(runtime, send);
731
+ void handleNew(runtime, send);
661
732
  return null;
662
733
  case "/stop":
663
734
  handleStop(runtime, send);
@@ -669,7 +740,7 @@ function handleSlashCommand(runtime, text, send) {
669
740
  handleAgent(runtime, args, send);
670
741
  return null;
671
742
  case "/model":
672
- handleModel(runtime, args, send);
743
+ void handleModel(runtime, args, send);
673
744
  return null;
674
745
  case "/cost":
675
746
  send(costText(runtime));
@@ -684,12 +755,18 @@ function handleSlashCommand(runtime, text, send) {
684
755
  return text.slice(1);
685
756
  }
686
757
  }
687
- function handleNew(runtime, send) {
688
- runtime.agent().resetSession();
689
- const state = runtime.state();
690
- state.acpSessionId = null;
691
- state.usage = { used: 0, size: 0, cost: null };
692
- runtime.saveState();
758
+ async function handleNew(runtime, send) {
759
+ if (runtime.queue().status().busy) {
760
+ send(NEW_BUSY_TEXT);
761
+ return;
762
+ }
763
+ await runtime.serialize(async () => {
764
+ runtime.agent().resetSession();
765
+ const state = runtime.state();
766
+ state.acpSessionId = null;
767
+ state.usage = { used: 0, size: 0, cost: null };
768
+ runtime.saveState();
769
+ });
693
770
  send("Session reset. Starting a new conversation.");
694
771
  }
695
772
  function handleStop(runtime, send) {
@@ -705,12 +782,18 @@ function handleStop(runtime, send) {
705
782
  }
706
783
  function handleAgent(runtime, args, send) {
707
784
  const name = args.join(" ").trim();
785
+ const current = runtime.config().agent.name;
708
786
  if (!name) {
709
- const current = runtime.config().agent.name;
710
- const list = AGENT_NAMES.map((entry2) => `${entry2 === current ? "*" : " "} ${entry2}`).join("\n");
711
- send(`Current agent: ${current}
712
- Available agents:
713
- ${list}`);
787
+ send(
788
+ table(
789
+ ["Agent", "Backend", ""],
790
+ AGENT_NAMES.map((entry) => [
791
+ entry,
792
+ AGENTS[entry].label,
793
+ entry === current ? "\u2190 current" : ""
794
+ ])
795
+ )
796
+ );
714
797
  return;
715
798
  }
716
799
  if (!isAgentName(name)) {
@@ -726,56 +809,95 @@ ${list}`);
726
809
  (error) => send(`Error: ${error instanceof Error ? error.message : String(error)}`)
727
810
  );
728
811
  }
729
- function handleModel(runtime, args, send) {
812
+ async function handleModel(runtime, args, send) {
730
813
  const model = args.join(" ").trim();
731
814
  if (!model) {
732
- send(`Current model: ${runtime.config().agent.model ?? "default"}`);
815
+ send(modelReport(runtime));
816
+ return;
817
+ }
818
+ if (runtime.queue().status().busy) {
819
+ send(MODEL_BUSY_TEXT);
733
820
  return;
734
821
  }
735
822
  const config = runtime.config();
736
823
  const hadSession = runtime.agent().sessionId() !== null;
737
- config.agent.model = model;
738
- runtime.saveConfig();
739
- runtime.agent().setModel(model).then((supported) => {
740
- if (!supported) {
741
- send(
742
- `Model saved: ${model}. This agent does not expose a runtime model option; it will be used on the next session if supported.`
743
- );
744
- return;
745
- }
824
+ const supported = await runtime.serialize(async () => {
825
+ const accepted = await runtime.agent().setModel(model);
826
+ config.agent.model = model;
827
+ runtime.saveConfig();
828
+ return accepted;
829
+ });
830
+ const applied = runtime.agent().currentModel()?.label ?? model;
831
+ if (!supported) {
746
832
  send(
747
- hadSession ? `Model switched to: ${model}` : `Model set to: ${model}. It will apply when the session starts.`
833
+ `Model saved: ${model}. This agent does not expose a runtime model option; it will be used on the next session if supported.`
748
834
  );
749
- }).catch(
750
- (error) => send(`Error: ${error instanceof Error ? error.message : String(error)}`)
835
+ return;
836
+ }
837
+ send(
838
+ hadSession ? `Model switched to: ${applied}` : `Model set to: ${applied}. It will apply when the session starts.`
751
839
  );
752
840
  }
841
+ function modelReport(runtime) {
842
+ const current = runtime.agent().currentModel();
843
+ if (!current) {
844
+ const configured = runtime.config().agent.model;
845
+ const hint = configured ? `Configured: ${configured}
846
+
847
+ 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.";
848
+ return `Current: unknown
849
+
850
+ ${hint}`;
851
+ }
852
+ const lines = [`Current: ${current.label}`];
853
+ if (current.options.length > 0) {
854
+ lines.push(
855
+ "",
856
+ table(
857
+ ["Model", ""],
858
+ current.options.map((option) => [
859
+ option.label,
860
+ option.value === current.value ? "\u2190 current" : ""
861
+ ])
862
+ )
863
+ );
864
+ }
865
+ lines.push("", "Switch: /model <name>");
866
+ return lines.join("\n");
867
+ }
753
868
  function statusText(runtime) {
754
869
  const config = runtime.config();
755
870
  const agent = runtime.agent();
756
871
  const state = runtime.state();
757
872
  const queue = runtime.queue().status();
758
- const lines = [
759
- `Agent: ${config.agent.name} (${AGENTS[config.agent.name].label})`,
760
- `Session: ${agent.sessionId() ?? "none"}`,
761
- `Work dir: ${config.agent.cwd}`,
762
- `Model: ${config.agent.model ?? "default"}`,
763
- `Status: ${queue.busy ? "busy" : "idle"}`,
764
- `Queue: ${queue.queued}`,
765
- `Usage: ${state.usage.used}/${state.usage.size} tokens`,
766
- `Capabilities: ${capabilitiesText(agent.capabilities())}`
873
+ const { used, size } = state.usage;
874
+ const summary = [
875
+ `**${config.agent.name}**`,
876
+ queue.busy ? "busy" : "idle",
877
+ size > 0 ? `${formatNumber(used)} / ${formatNumber(size)}` : null
878
+ ].filter(Boolean).join(" \xB7 ");
879
+ const rows = [
880
+ ["Agent", `${config.agent.name} (${AGENTS[config.agent.name].label})`],
881
+ ["Session", agent.sessionId() ?? "none"],
882
+ ["Work dir", compactPath(config.agent.cwd)],
883
+ ["Model", modelText(runtime)],
884
+ ["Status", queue.busy ? "busy" : "idle"],
885
+ ["Queue", String(queue.queued)],
886
+ ["Usage", usageText(runtime)],
887
+ ["Capabilities", capabilitiesText(agent.capabilities())]
767
888
  ];
768
- return lines.join("\n");
889
+ return [summary, "", table(["Field", "Value"], rows)].join("\n");
769
890
  }
770
891
  function costText(runtime) {
771
892
  const usage = runtime.state().usage;
772
- const context = `Context: ${usage.used}/${usage.size} tokens`;
773
- if (!usage.cost) {
774
- return `${context}
775
- Cost: this agent does not report cost.`;
776
- }
777
- return `${context}
778
- Cost: ${usage.cost.amount} ${usage.cost.currency}`;
893
+ const cost = usage.cost ? `${usage.cost.amount} ${usage.cost.currency}` : "not reported by this agent";
894
+ return table(
895
+ ["Field", "Value"],
896
+ [
897
+ ["Context", usageText(runtime)],
898
+ ["Cost", cost]
899
+ ]
900
+ );
779
901
  }
780
902
  function capabilitiesText(caps) {
781
903
  const enabled = [
@@ -830,10 +952,10 @@ var import_node_path3 = require("path");
830
952
  // src/core/state.ts
831
953
  var import_node_crypto = require("crypto");
832
954
  var import_node_fs2 = require("fs");
833
- var import_node_os2 = require("os");
955
+ var import_node_os3 = require("os");
834
956
  var import_node_path2 = require("path");
835
957
  function vbotHome() {
836
- return process.env.VBOT_HOME?.trim() || (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".vbot");
958
+ return process.env.VBOT_HOME?.trim() || (0, import_node_path2.join)((0, import_node_os3.homedir)(), ".vbot");
837
959
  }
838
960
  function stateDir() {
839
961
  return (0, import_node_path2.join)(vbotHome(), "state");
@@ -991,10 +1113,10 @@ var VicvicWire = class {
991
1113
  if (!socket || socket.readyState !== import_ws.default.OPEN) {
992
1114
  throw new Error("Bot connection is not open");
993
1115
  }
994
- await new Promise((resolve3, reject) => {
1116
+ await new Promise((resolve2, reject) => {
995
1117
  socket.send(JSON.stringify({ type: "reply", msgUid, text }), (error) => {
996
1118
  if (error) reject(error);
997
- else resolve3();
1119
+ else resolve2();
998
1120
  });
999
1121
  });
1000
1122
  }
@@ -1162,13 +1284,17 @@ async function startBot(configPath) {
1162
1284
  saveConfig: () => saveConfig(config, configPath)
1163
1285
  };
1164
1286
  holder.queue = new BotQueue((item, signal) => gate(() => processPrompt(ctx, item, signal)));
1165
- const runtime = createRuntime(ctx, (name) => {
1166
- if (switching) return Promise.reject(new Error("Agent switch already in progress."));
1167
- switching = true;
1168
- return gate(() => switchAgent(ctx, name)).finally(() => {
1169
- switching = false;
1170
- });
1171
- });
1287
+ const runtime = createRuntime(
1288
+ ctx,
1289
+ (task) => gate(task),
1290
+ (name) => {
1291
+ if (switching) return Promise.reject(new Error("Agent switch already in progress."));
1292
+ switching = true;
1293
+ return gate(() => switchAgent(ctx, name)).finally(() => {
1294
+ switching = false;
1295
+ });
1296
+ }
1297
+ );
1172
1298
  const dispatch = createDispatcher(runtime);
1173
1299
  let notifySignal = () => void 0;
1174
1300
  const wire = new VicvicWire({
@@ -1229,7 +1355,7 @@ async function runBot(configPath) {
1229
1355
  const bot = await startBot(configPath);
1230
1356
  await bot.finished;
1231
1357
  }
1232
- function createRuntime(ctx, switchAgent2) {
1358
+ function createRuntime(ctx, gate, switchAgent2) {
1233
1359
  return {
1234
1360
  config: () => ctx.config,
1235
1361
  agent: () => ctx.getAgent(),
@@ -1237,6 +1363,7 @@ function createRuntime(ctx, switchAgent2) {
1237
1363
  state: () => ctx.state,
1238
1364
  saveState: () => ctx.saveState(),
1239
1365
  saveConfig: () => ctx.saveConfig(),
1366
+ serialize: (task) => gate(task),
1240
1367
  switchAgent: switchAgent2
1241
1368
  };
1242
1369
  }
@@ -1288,19 +1415,6 @@ ${error.stderr}`);
1288
1415
  console.error("[server] agent run failed:", error);
1289
1416
  replySafe(reply, `Error: ${error instanceof Error ? error.message : String(error)}`);
1290
1417
  }
1291
- var entry = process.argv[1] ?? "";
1292
- if (/(^|[\\/])server\.(js|cjs|ts)$/.test(entry)) {
1293
- const configPath = process.argv[2];
1294
- if (!configPath) {
1295
- console.error("Usage: server.js <config-path>");
1296
- process.exit(1);
1297
- }
1298
- runBot((0, import_node_path4.resolve)(configPath)).catch((error) => {
1299
- console.error(error instanceof Error ? error.message : String(error));
1300
- const expected = error instanceof ConfigError || error instanceof LockError;
1301
- process.exit(expected ? 2 : 1);
1302
- });
1303
- }
1304
1418
  // Annotate the CommonJS export names for ESM import in node:
1305
1419
  0 && (module.exports = {
1306
1420
  runBot,