@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.
@@ -1,6 +1,3 @@
1
- // src/server.ts
2
- import { resolve as resolve2 } from "path";
3
-
4
1
  // src/config.ts
5
2
  import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
6
3
  import { homedir } from "os";
@@ -102,11 +99,11 @@ function parseEnv(value) {
102
99
  if (value === void 0 || value === null) return void 0;
103
100
  const record = asRecord(value, "agent.env");
104
101
  const env = {};
105
- for (const [key, entry2] of Object.entries(record)) {
106
- if (typeof entry2 !== "string") {
102
+ for (const [key, entry] of Object.entries(record)) {
103
+ if (typeof entry !== "string") {
107
104
  throw new ConfigError(`Invalid "agent.env.${key}": expected a string`);
108
105
  }
109
- env[key] = entry2;
106
+ env[key] = entry;
110
107
  }
111
108
  return env;
112
109
  }
@@ -150,7 +147,7 @@ function parseAgent(root, configDir) {
150
147
  return {
151
148
  name: agentName,
152
149
  command: optionalString(agentRaw.command, "agent.command"),
153
- args: Array.isArray(argsRaw) ? argsRaw.map((entry2) => String(entry2)) : void 0,
150
+ args: Array.isArray(argsRaw) ? argsRaw.map((entry) => String(entry)) : void 0,
154
151
  cwd,
155
152
  model: optionalString(agentRaw.model, "agent.model") ?? null,
156
153
  env: parseEnv(agentRaw.env)
@@ -215,6 +212,9 @@ import * as acp from "@agentclientprotocol/sdk";
215
212
  // src/agents/types.ts
216
213
  var EMPTY_USAGE = { used: 0, size: 0, cost: null };
217
214
 
215
+ // src/version.ts
216
+ var VBOT_VERSION = "0.1.3";
217
+
218
218
  // src/agents/acp.ts
219
219
  var AgentAbortError = class extends Error {
220
220
  constructor() {
@@ -231,6 +231,14 @@ var AgentStartError = class extends Error {
231
231
  stderr;
232
232
  };
233
233
  var STDERR_LIMIT = 4e3;
234
+ function flattenSelectOptions(options) {
235
+ return options.flatMap(
236
+ (entry) => "group" in entry ? entry.options.map((option) => ({ value: option.value, label: option.name })) : [{ value: entry.value, label: entry.name }]
237
+ );
238
+ }
239
+ function isModelOption(option) {
240
+ return option.type === "select" && (option.category === "model" || option.id === "model");
241
+ }
234
242
  var AcpAgent = class {
235
243
  constructor(name, config) {
236
244
  this.name = name;
@@ -260,6 +268,14 @@ var AcpAgent = class {
260
268
  sessionId() {
261
269
  return this.session;
262
270
  }
271
+ currentModel() {
272
+ const option = this.configOptions.find(isModelOption);
273
+ if (!option || option.type !== "select") return null;
274
+ const value = String(option.currentValue);
275
+ const options = flattenSelectOptions(option.options);
276
+ const label = options.find((entry) => entry.value === value)?.label ?? value;
277
+ return { value, label, options };
278
+ }
263
279
  resetSession() {
264
280
  this.session = null;
265
281
  }
@@ -327,7 +343,7 @@ var AcpAgent = class {
327
343
  const init = await this.ctx.request(acp.methods.agent.initialize, {
328
344
  protocolVersion: 1,
329
345
  clientCapabilities: {},
330
- clientInfo: { name: "vbot", version: "0.1.0" }
346
+ clientInfo: { name: "vbot", version: VBOT_VERSION }
331
347
  });
332
348
  this.initCaps = init.agentCapabilities;
333
349
  } catch (error) {
@@ -414,7 +430,7 @@ var AcpAgent = class {
414
430
  );
415
431
  pending.catch(() => void 0);
416
432
  const response = await Promise.race([pending, aborted]);
417
- await new Promise((resolve3) => setImmediate(resolve3));
433
+ await new Promise((resolve2) => setImmediate(resolve2));
418
434
  if (response.stopReason === "cancelled" || request.signal.aborted) {
419
435
  throw new AgentAbortError();
420
436
  }
@@ -446,15 +462,14 @@ var AcpAgent = class {
446
462
  this.pendingModel = model;
447
463
  return true;
448
464
  }
449
- const option = this.configOptions.find(
450
- (entry2) => entry2.type === "select" && (entry2.id === "model" || entry2.category === "model")
451
- );
465
+ const option = this.configOptions.find(isModelOption);
452
466
  if (!option) return false;
453
- await this.ctx.request(acp.methods.agent.session.setConfigOption, {
467
+ const response = await this.ctx.request(acp.methods.agent.session.setConfigOption, {
454
468
  sessionId: this.session,
455
469
  configId: option.id,
456
470
  value: model
457
471
  });
472
+ if (response?.configOptions?.length) this.configOptions = response.configOptions;
458
473
  this.pendingModel = null;
459
474
  return true;
460
475
  }
@@ -473,8 +488,8 @@ var AcpAgent = class {
473
488
  } catch {
474
489
  }
475
490
  if (!child || child.exitCode !== null) return;
476
- const exited = new Promise((resolve3) => {
477
- child.once("exit", () => resolve3());
491
+ const exited = new Promise((resolve2) => {
492
+ child.once("exit", () => resolve2());
478
493
  });
479
494
  child.kill("SIGTERM");
480
495
  const timer = setTimeout(() => child.kill("SIGKILL"), 3e3);
@@ -590,21 +605,29 @@ var BotQueue = class {
590
605
  }
591
606
  };
592
607
 
593
- // src/version.ts
594
- var VBOT_VERSION = "0.1.1";
595
-
596
608
  // src/core/dispatcher.ts
597
- var HELP_TEXT = `Available commands:
598
- /new \u2014 Start a new session
599
- /stop \u2014 Abort the current request
600
- /status \u2014 Show agent, session and queue status
601
- /agent [name] \u2014 Show or switch agent backend
602
- /model [name] \u2014 Show or switch model
603
- /cost \u2014 Show context usage and cost
604
- /version \u2014 Show vbot version
605
- /help \u2014 Show this help
606
-
607
- Other /commands are sent to the agent as prompts.`;
609
+ import { homedir as homedir2 } from "os";
610
+ var HELP_ROWS = [
611
+ ["/new", "Start a new session"],
612
+ ["/stop", "Abort the current request"],
613
+ ["/status", "Show agent, session and queue status"],
614
+ ["/agent [name]", "Show or switch agent backend"],
615
+ ["/model [name]", "Show or switch model"],
616
+ ["/cost", "Show context usage and cost"],
617
+ ["/version", "Show vbot version"],
618
+ ["/help", "Show this help"]
619
+ ];
620
+ var HELP_FOOTER = "Other /commands are sent to the agent as prompts.";
621
+ var NEW_BUSY_TEXT = "Cannot reset the session while a request is running. Use /stop first.";
622
+ var MODEL_BUSY_TEXT = "Cannot switch model while a request is running. Use /stop first.";
623
+ var HELP_TEXT = [
624
+ table(
625
+ ["Command", "Description"],
626
+ HELP_ROWS.map(([command, description]) => [command, description])
627
+ ),
628
+ "",
629
+ HELP_FOOTER
630
+ ].join("\n");
608
631
  function createDispatcher(runtime) {
609
632
  return (text, reply) => {
610
633
  const send = (message) => {
@@ -618,11 +641,57 @@ function createDispatcher(runtime) {
618
641
  });
619
642
  };
620
643
  }
644
+ function escapeCell(value) {
645
+ return value.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
646
+ }
647
+ function table(headers, rows) {
648
+ const head = `| ${headers.map(escapeCell).join(" | ")} |`;
649
+ const separator = `| ${headers.map(() => "---").join(" | ")} |`;
650
+ const body = rows.map((row) => `| ${row.map(escapeCell).join(" | ")} |`);
651
+ return [head, separator, ...body].join("\n");
652
+ }
653
+ function formatNumber(value) {
654
+ if (!Number.isFinite(value)) return String(value);
655
+ const negative = value < 0;
656
+ const digits = Math.abs(Math.trunc(value)).toString();
657
+ let out = "";
658
+ for (let i = 0; i < digits.length; i += 1) {
659
+ if (i > 0 && (digits.length - i) % 3 === 0) out += ",";
660
+ out += digits[i];
661
+ }
662
+ return negative ? "-" + out : out;
663
+ }
664
+ function percentOf(used, size) {
665
+ if (size <= 0) return null;
666
+ return `${(used / size * 100).toFixed(1)}%`;
667
+ }
668
+ function usageText(runtime) {
669
+ const { used, size } = runtime.state().usage;
670
+ if (size <= 0) return "not reported";
671
+ const base = `${formatNumber(used)} / ${formatNumber(size)}`;
672
+ const percent = percentOf(used, size);
673
+ return percent ? `${base} (${percent})` : base;
674
+ }
675
+ function compactPath(path) {
676
+ const home = homedir2();
677
+ if (!home) return path;
678
+ if (path === home) return "~";
679
+ const prefix = home + "/";
680
+ if (path.startsWith(prefix)) return "~" + path.slice(home.length);
681
+ return path;
682
+ }
683
+ function modelText(runtime) {
684
+ const current = runtime.agent().currentModel();
685
+ if (current) return current.label;
686
+ const configured = runtime.config().agent.model;
687
+ if (configured) return `${configured} (configured, not applied yet)`;
688
+ return "unknown (session not started)";
689
+ }
621
690
  function handleSlashCommand(runtime, text, send) {
622
691
  const [rawCommand, ...args] = text.split(/\s+/);
623
692
  switch (rawCommand.toLowerCase()) {
624
693
  case "/new":
625
- handleNew(runtime, send);
694
+ void handleNew(runtime, send);
626
695
  return null;
627
696
  case "/stop":
628
697
  handleStop(runtime, send);
@@ -634,7 +703,7 @@ function handleSlashCommand(runtime, text, send) {
634
703
  handleAgent(runtime, args, send);
635
704
  return null;
636
705
  case "/model":
637
- handleModel(runtime, args, send);
706
+ void handleModel(runtime, args, send);
638
707
  return null;
639
708
  case "/cost":
640
709
  send(costText(runtime));
@@ -649,12 +718,18 @@ function handleSlashCommand(runtime, text, send) {
649
718
  return text.slice(1);
650
719
  }
651
720
  }
652
- function handleNew(runtime, send) {
653
- runtime.agent().resetSession();
654
- const state = runtime.state();
655
- state.acpSessionId = null;
656
- state.usage = { used: 0, size: 0, cost: null };
657
- runtime.saveState();
721
+ async function handleNew(runtime, send) {
722
+ if (runtime.queue().status().busy) {
723
+ send(NEW_BUSY_TEXT);
724
+ return;
725
+ }
726
+ await runtime.serialize(async () => {
727
+ runtime.agent().resetSession();
728
+ const state = runtime.state();
729
+ state.acpSessionId = null;
730
+ state.usage = { used: 0, size: 0, cost: null };
731
+ runtime.saveState();
732
+ });
658
733
  send("Session reset. Starting a new conversation.");
659
734
  }
660
735
  function handleStop(runtime, send) {
@@ -670,12 +745,18 @@ function handleStop(runtime, send) {
670
745
  }
671
746
  function handleAgent(runtime, args, send) {
672
747
  const name = args.join(" ").trim();
748
+ const current = runtime.config().agent.name;
673
749
  if (!name) {
674
- const current = runtime.config().agent.name;
675
- const list = AGENT_NAMES.map((entry2) => `${entry2 === current ? "*" : " "} ${entry2}`).join("\n");
676
- send(`Current agent: ${current}
677
- Available agents:
678
- ${list}`);
750
+ send(
751
+ table(
752
+ ["Agent", "Backend", ""],
753
+ AGENT_NAMES.map((entry) => [
754
+ entry,
755
+ AGENTS[entry].label,
756
+ entry === current ? "\u2190 current" : ""
757
+ ])
758
+ )
759
+ );
679
760
  return;
680
761
  }
681
762
  if (!isAgentName(name)) {
@@ -691,56 +772,95 @@ ${list}`);
691
772
  (error) => send(`Error: ${error instanceof Error ? error.message : String(error)}`)
692
773
  );
693
774
  }
694
- function handleModel(runtime, args, send) {
775
+ async function handleModel(runtime, args, send) {
695
776
  const model = args.join(" ").trim();
696
777
  if (!model) {
697
- send(`Current model: ${runtime.config().agent.model ?? "default"}`);
778
+ send(modelReport(runtime));
779
+ return;
780
+ }
781
+ if (runtime.queue().status().busy) {
782
+ send(MODEL_BUSY_TEXT);
698
783
  return;
699
784
  }
700
785
  const config = runtime.config();
701
786
  const hadSession = runtime.agent().sessionId() !== null;
702
- config.agent.model = model;
703
- runtime.saveConfig();
704
- runtime.agent().setModel(model).then((supported) => {
705
- if (!supported) {
706
- send(
707
- `Model saved: ${model}. This agent does not expose a runtime model option; it will be used on the next session if supported.`
708
- );
709
- return;
710
- }
787
+ const supported = await runtime.serialize(async () => {
788
+ const accepted = await runtime.agent().setModel(model);
789
+ config.agent.model = model;
790
+ runtime.saveConfig();
791
+ return accepted;
792
+ });
793
+ const applied = runtime.agent().currentModel()?.label ?? model;
794
+ if (!supported) {
711
795
  send(
712
- hadSession ? `Model switched to: ${model}` : `Model set to: ${model}. It will apply when the session starts.`
796
+ `Model saved: ${model}. This agent does not expose a runtime model option; it will be used on the next session if supported.`
713
797
  );
714
- }).catch(
715
- (error) => send(`Error: ${error instanceof Error ? error.message : String(error)}`)
798
+ return;
799
+ }
800
+ send(
801
+ hadSession ? `Model switched to: ${applied}` : `Model set to: ${applied}. It will apply when the session starts.`
716
802
  );
717
803
  }
804
+ function modelReport(runtime) {
805
+ const current = runtime.agent().currentModel();
806
+ if (!current) {
807
+ const configured = runtime.config().agent.model;
808
+ const hint = configured ? `Configured: ${configured}
809
+
810
+ 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.";
811
+ return `Current: unknown
812
+
813
+ ${hint}`;
814
+ }
815
+ const lines = [`Current: ${current.label}`];
816
+ if (current.options.length > 0) {
817
+ lines.push(
818
+ "",
819
+ table(
820
+ ["Model", ""],
821
+ current.options.map((option) => [
822
+ option.label,
823
+ option.value === current.value ? "\u2190 current" : ""
824
+ ])
825
+ )
826
+ );
827
+ }
828
+ lines.push("", "Switch: /model <name>");
829
+ return lines.join("\n");
830
+ }
718
831
  function statusText(runtime) {
719
832
  const config = runtime.config();
720
833
  const agent = runtime.agent();
721
834
  const state = runtime.state();
722
835
  const queue = runtime.queue().status();
723
- const lines = [
724
- `Agent: ${config.agent.name} (${AGENTS[config.agent.name].label})`,
725
- `Session: ${agent.sessionId() ?? "none"}`,
726
- `Work dir: ${config.agent.cwd}`,
727
- `Model: ${config.agent.model ?? "default"}`,
728
- `Status: ${queue.busy ? "busy" : "idle"}`,
729
- `Queue: ${queue.queued}`,
730
- `Usage: ${state.usage.used}/${state.usage.size} tokens`,
731
- `Capabilities: ${capabilitiesText(agent.capabilities())}`
836
+ const { used, size } = state.usage;
837
+ const summary = [
838
+ `**${config.agent.name}**`,
839
+ queue.busy ? "busy" : "idle",
840
+ size > 0 ? `${formatNumber(used)} / ${formatNumber(size)}` : null
841
+ ].filter(Boolean).join(" \xB7 ");
842
+ const rows = [
843
+ ["Agent", `${config.agent.name} (${AGENTS[config.agent.name].label})`],
844
+ ["Session", agent.sessionId() ?? "none"],
845
+ ["Work dir", compactPath(config.agent.cwd)],
846
+ ["Model", modelText(runtime)],
847
+ ["Status", queue.busy ? "busy" : "idle"],
848
+ ["Queue", String(queue.queued)],
849
+ ["Usage", usageText(runtime)],
850
+ ["Capabilities", capabilitiesText(agent.capabilities())]
732
851
  ];
733
- return lines.join("\n");
852
+ return [summary, "", table(["Field", "Value"], rows)].join("\n");
734
853
  }
735
854
  function costText(runtime) {
736
855
  const usage = runtime.state().usage;
737
- const context = `Context: ${usage.used}/${usage.size} tokens`;
738
- if (!usage.cost) {
739
- return `${context}
740
- Cost: this agent does not report cost.`;
741
- }
742
- return `${context}
743
- Cost: ${usage.cost.amount} ${usage.cost.currency}`;
856
+ const cost = usage.cost ? `${usage.cost.amount} ${usage.cost.currency}` : "not reported by this agent";
857
+ return table(
858
+ ["Field", "Value"],
859
+ [
860
+ ["Context", usageText(runtime)],
861
+ ["Cost", cost]
862
+ ]
863
+ );
744
864
  }
745
865
  function capabilitiesText(caps) {
746
866
  const enabled = [
@@ -810,10 +930,10 @@ import {
810
930
  unlinkSync,
811
931
  writeFileSync as writeFileSync2
812
932
  } from "fs";
813
- import { homedir as homedir2 } from "os";
933
+ import { homedir as homedir3 } from "os";
814
934
  import { join as join2 } from "path";
815
935
  function vbotHome() {
816
- return process.env.VBOT_HOME?.trim() || join2(homedir2(), ".vbot");
936
+ return process.env.VBOT_HOME?.trim() || join2(homedir3(), ".vbot");
817
937
  }
818
938
  function stateDir() {
819
939
  return join2(vbotHome(), "state");
@@ -980,10 +1100,10 @@ var VicvicWire = class {
980
1100
  if (!socket || socket.readyState !== WebSocket.OPEN) {
981
1101
  throw new Error("Bot connection is not open");
982
1102
  }
983
- await new Promise((resolve3, reject) => {
1103
+ await new Promise((resolve2, reject) => {
984
1104
  socket.send(JSON.stringify({ type: "reply", msgUid, text }), (error) => {
985
1105
  if (error) reject(error);
986
- else resolve3();
1106
+ else resolve2();
987
1107
  });
988
1108
  });
989
1109
  }
@@ -1151,13 +1271,17 @@ async function startBot(configPath) {
1151
1271
  saveConfig: () => saveConfig(config, configPath)
1152
1272
  };
1153
1273
  holder.queue = new BotQueue((item, signal) => gate(() => processPrompt(ctx, item, signal)));
1154
- const runtime = createRuntime(ctx, (name) => {
1155
- if (switching) return Promise.reject(new Error("Agent switch already in progress."));
1156
- switching = true;
1157
- return gate(() => switchAgent(ctx, name)).finally(() => {
1158
- switching = false;
1159
- });
1160
- });
1274
+ const runtime = createRuntime(
1275
+ ctx,
1276
+ (task) => gate(task),
1277
+ (name) => {
1278
+ if (switching) return Promise.reject(new Error("Agent switch already in progress."));
1279
+ switching = true;
1280
+ return gate(() => switchAgent(ctx, name)).finally(() => {
1281
+ switching = false;
1282
+ });
1283
+ }
1284
+ );
1161
1285
  const dispatch = createDispatcher(runtime);
1162
1286
  let notifySignal = () => void 0;
1163
1287
  const wire = new VicvicWire({
@@ -1218,7 +1342,7 @@ async function runBot(configPath) {
1218
1342
  const bot = await startBot(configPath);
1219
1343
  await bot.finished;
1220
1344
  }
1221
- function createRuntime(ctx, switchAgent2) {
1345
+ function createRuntime(ctx, gate, switchAgent2) {
1222
1346
  return {
1223
1347
  config: () => ctx.config,
1224
1348
  agent: () => ctx.getAgent(),
@@ -1226,6 +1350,7 @@ function createRuntime(ctx, switchAgent2) {
1226
1350
  state: () => ctx.state,
1227
1351
  saveState: () => ctx.saveState(),
1228
1352
  saveConfig: () => ctx.saveConfig(),
1353
+ serialize: (task) => gate(task),
1229
1354
  switchAgent: switchAgent2
1230
1355
  };
1231
1356
  }
@@ -1277,19 +1402,6 @@ ${error.stderr}`);
1277
1402
  console.error("[server] agent run failed:", error);
1278
1403
  replySafe(reply, `Error: ${error instanceof Error ? error.message : String(error)}`);
1279
1404
  }
1280
- var entry = process.argv[1] ?? "";
1281
- if (/(^|[\\/])server\.(js|cjs|ts)$/.test(entry)) {
1282
- const configPath = process.argv[2];
1283
- if (!configPath) {
1284
- console.error("Usage: server.js <config-path>");
1285
- process.exit(1);
1286
- }
1287
- runBot(resolve2(configPath)).catch((error) => {
1288
- console.error(error instanceof Error ? error.message : String(error));
1289
- const expected = error instanceof ConfigError || error instanceof LockError;
1290
- process.exit(expected ? 2 : 1);
1291
- });
1292
- }
1293
1405
 
1294
1406
  export {
1295
1407
  AGENTS,
@@ -1306,12 +1418,6 @@ export {
1306
1418
  loadConfig,
1307
1419
  saveConfig,
1308
1420
  EMPTY_USAGE,
1309
- AgentAbortError,
1310
- AgentStartError,
1311
- AcpAgent,
1312
- BotQueue,
1313
- HELP_TEXT,
1314
- createDispatcher,
1315
1421
  tokenHash,
1316
1422
  statePath,
1317
1423
  emptyState,
@@ -1321,6 +1427,12 @@ export {
1321
1427
  LockError,
1322
1428
  acquireLock,
1323
1429
  releaseLock,
1430
+ AgentAbortError,
1431
+ AgentStartError,
1432
+ AcpAgent,
1433
+ BotQueue,
1434
+ HELP_TEXT,
1435
+ createDispatcher,
1324
1436
  botUrl,
1325
1437
  VicvicWire,
1326
1438
  MAX_MESSAGE_CHARS,
@@ -1328,4 +1440,4 @@ export {
1328
1440
  startBot,
1329
1441
  runBot
1330
1442
  };
1331
- //# sourceMappingURL=chunk-SCVC76UL.js.map
1443
+ //# sourceMappingURL=chunk-ELLWQDOY.js.map