@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.
@@ -0,0 +1,1422 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
24
+
25
+ // src/bin-server.ts
26
+ var import_node_path4 = require("path");
27
+
28
+ // src/config.ts
29
+ var import_node_fs = require("fs");
30
+ var import_node_os = require("os");
31
+ var import_node_path = require("path");
32
+
33
+ // src/agents/registry.ts
34
+ var AGENTS = {
35
+ claude: {
36
+ name: "claude",
37
+ label: "Claude Code",
38
+ command: {
39
+ command: "npx",
40
+ args: ["-y", "@agentclientprotocol/claude-agent-acp@0.100.0"]
41
+ },
42
+ authHint: "Run `claude login` (or configure ANTHROPIC_AUTH_TOKEN) before using vbot."
43
+ },
44
+ codex: {
45
+ name: "codex",
46
+ label: "Codex CLI",
47
+ command: {
48
+ command: "npx",
49
+ args: ["-y", "@agentclientprotocol/codex-acp@2.5.0"]
50
+ },
51
+ authHint: "Run `codex login` before using vbot."
52
+ },
53
+ opencode: {
54
+ name: "opencode",
55
+ label: "OpenCode",
56
+ command: {
57
+ command: "opencode",
58
+ args: ["acp"]
59
+ },
60
+ authHint: "OpenCode >= 1.19 is required; configure a provider/model in OpenCode first."
61
+ },
62
+ pi: {
63
+ name: "pi",
64
+ label: "Pi",
65
+ command: {
66
+ command: "npx",
67
+ args: ["-y", "--package=pi-acp@0.0.34", "--", "pi-acp"]
68
+ },
69
+ authHint: "Install and authenticate Pi (`pi` CLI) before using vbot."
70
+ }
71
+ };
72
+ var AGENT_NAMES = Object.keys(AGENTS);
73
+ function isAgentName(value) {
74
+ return AGENT_NAMES.includes(value);
75
+ }
76
+ function definitionOf(name) {
77
+ return AGENTS[name];
78
+ }
79
+
80
+ // src/config.ts
81
+ var CONFIG_FILENAME = "vbot.json";
82
+ var GLOBAL_CONFIG_DIR = (0, import_node_path.join)((0, import_node_os.homedir)(), ".vbot");
83
+ var GLOBAL_CONFIG_PATH = (0, import_node_path.join)(GLOBAL_CONFIG_DIR, "config.json");
84
+ var DEFAULT_BASE_URL = "https://api.vicvic.im";
85
+ var DEFAULT_TIMEOUT_MS = 36e5;
86
+ var ConfigError = class extends Error {
87
+ constructor(message) {
88
+ super(message);
89
+ this.name = "ConfigError";
90
+ }
91
+ };
92
+ function readJson(path) {
93
+ try {
94
+ return JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
95
+ } catch (error) {
96
+ throw new ConfigError(
97
+ `Failed to read config ${path}: ${error instanceof Error ? error.message : String(error)}`
98
+ );
99
+ }
100
+ }
101
+ function asRecord(value, field) {
102
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
103
+ throw new ConfigError(`Invalid "${field}" in vbot.json: expected an object`);
104
+ }
105
+ return value;
106
+ }
107
+ function asString(value, field) {
108
+ if (typeof value !== "string" || value.trim().length === 0) {
109
+ throw new ConfigError(`Invalid "${field}" in vbot.json: expected a non-empty string`);
110
+ }
111
+ return value.trim();
112
+ }
113
+ function optionalString(value, field) {
114
+ if (value === void 0 || value === null) return void 0;
115
+ if (typeof value !== "string") {
116
+ throw new ConfigError(`Invalid "${field}" in vbot.json: expected a string`);
117
+ }
118
+ return value.trim() || void 0;
119
+ }
120
+ function trimTrailingSlash(value) {
121
+ let end = value.length;
122
+ while (end > 0 && value[end - 1] === "/") end -= 1;
123
+ return value.slice(0, end);
124
+ }
125
+ function parseEnv(value) {
126
+ if (value === void 0 || value === null) return void 0;
127
+ const record = asRecord(value, "agent.env");
128
+ const env = {};
129
+ for (const [key, entry] of Object.entries(record)) {
130
+ if (typeof entry !== "string") {
131
+ throw new ConfigError(`Invalid "agent.env.${key}": expected a string`);
132
+ }
133
+ env[key] = entry;
134
+ }
135
+ return env;
136
+ }
137
+ function parseProvider(root) {
138
+ const provider = optionalString(root.provider, "provider") ?? "vicvic";
139
+ if (provider !== "vicvic") {
140
+ throw new ConfigError(`Unsupported provider "${provider}"; only "vicvic" is supported`);
141
+ }
142
+ return "vicvic";
143
+ }
144
+ function parseVicvic(root, env) {
145
+ const vicvic = asRecord(root.vicvic, "vicvic");
146
+ const baseUrl = trimTrailingSlash(
147
+ optionalString(vicvic.baseUrl, "vicvic.baseUrl") ?? DEFAULT_BASE_URL
148
+ );
149
+ const token = env.VBOT_TOKEN?.trim() || optionalString(vicvic.token, "vicvic.token");
150
+ if (!token) {
151
+ throw new ConfigError(
152
+ 'Missing "vicvic.token"; set it in vbot.json or the VBOT_TOKEN environment variable'
153
+ );
154
+ }
155
+ return { baseUrl, token };
156
+ }
157
+ function parseAgent(root, configDir) {
158
+ const agentRaw = asRecord(root.agent, "agent");
159
+ const agentName = asString(agentRaw.name, "agent.name");
160
+ if (!isAgentName(agentName)) {
161
+ throw new ConfigError(
162
+ `Invalid "agent.name" "${agentName}"; expected one of claude, codex, opencode, pi`
163
+ );
164
+ }
165
+ const cwdValue = asString(agentRaw.cwd, "agent.cwd");
166
+ const cwd = (0, import_node_path.isAbsolute)(cwdValue) ? cwdValue : (0, import_node_path.resolve)(configDir, cwdValue);
167
+ if (!(0, import_node_fs.existsSync)(cwd)) {
168
+ throw new ConfigError(`"agent.cwd" does not exist: ${cwd}`);
169
+ }
170
+ const argsRaw = agentRaw.args;
171
+ if (argsRaw !== void 0 && !Array.isArray(argsRaw)) {
172
+ throw new ConfigError('Invalid "agent.args": expected an array of strings');
173
+ }
174
+ return {
175
+ name: agentName,
176
+ command: optionalString(agentRaw.command, "agent.command"),
177
+ args: Array.isArray(argsRaw) ? argsRaw.map((entry) => String(entry)) : void 0,
178
+ cwd,
179
+ model: optionalString(agentRaw.model, "agent.model") ?? null,
180
+ env: parseEnv(agentRaw.env)
181
+ };
182
+ }
183
+ function parseTimeout(root) {
184
+ const value = typeof root.timeoutMs === "number" ? root.timeoutMs : DEFAULT_TIMEOUT_MS;
185
+ if (!Number.isFinite(value) || value < 1e3 || value > 864e5) {
186
+ throw new ConfigError('Invalid "timeoutMs": expected a number between 1000 and 86400000');
187
+ }
188
+ return value;
189
+ }
190
+ function normalizeConfig(raw, configDir, env = process.env) {
191
+ const root = asRecord(raw, "root");
192
+ return {
193
+ provider: parseProvider(root),
194
+ vicvic: parseVicvic(root, env),
195
+ agent: parseAgent(root, configDir),
196
+ timeoutMs: parseTimeout(root),
197
+ logPrompt: root.logPrompt === true
198
+ };
199
+ }
200
+ function findConfigPath(explicit) {
201
+ if (explicit) {
202
+ const path = (0, import_node_path.resolve)(explicit);
203
+ if (!(0, import_node_fs.existsSync)(path)) throw new ConfigError(`Config file not found: ${path}`);
204
+ return path;
205
+ }
206
+ const local = (0, import_node_path.resolve)(process.cwd(), CONFIG_FILENAME);
207
+ if ((0, import_node_fs.existsSync)(local)) return local;
208
+ if ((0, import_node_fs.existsSync)(GLOBAL_CONFIG_PATH)) return GLOBAL_CONFIG_PATH;
209
+ return void 0;
210
+ }
211
+ function loadConfig(explicit) {
212
+ const path = findConfigPath(explicit);
213
+ if (!path) {
214
+ throw new ConfigError(
215
+ `No vbot.json found in ${process.cwd()} or ${GLOBAL_CONFIG_PATH}. Run "vbot start" to create one.`
216
+ );
217
+ }
218
+ const config = normalizeConfig(readJson(path), (0, import_node_path.dirname)(path));
219
+ return { config, path };
220
+ }
221
+ function saveConfig(config, path) {
222
+ const dir = (0, import_node_path.dirname)(path);
223
+ (0, import_node_fs.mkdirSync)(dir, { recursive: true });
224
+ const tmp = `${path}.${process.pid}.tmp`;
225
+ (0, import_node_fs.writeFileSync)(tmp, `${JSON.stringify(config, null, 2)}
226
+ `, { mode: 384 });
227
+ (0, import_node_fs.renameSync)(tmp, path);
228
+ try {
229
+ (0, import_node_fs.chmodSync)(path, 384);
230
+ } catch {
231
+ }
232
+ }
233
+
234
+ // src/core/lock.ts
235
+ var import_node_fs3 = require("fs");
236
+ var import_node_path3 = require("path");
237
+
238
+ // src/core/state.ts
239
+ var import_node_crypto = require("crypto");
240
+ var import_node_fs2 = require("fs");
241
+ var import_node_os2 = require("os");
242
+ var import_node_path2 = require("path");
243
+
244
+ // src/agents/types.ts
245
+ var EMPTY_USAGE = { used: 0, size: 0, cost: null };
246
+
247
+ // src/core/state.ts
248
+ function vbotHome() {
249
+ return process.env.VBOT_HOME?.trim() || (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".vbot");
250
+ }
251
+ function stateDir() {
252
+ return (0, import_node_path2.join)(vbotHome(), "state");
253
+ }
254
+ function tokenHash(token) {
255
+ return (0, import_node_crypto.createHash)("sha256").update(token).digest("hex").slice(0, 16);
256
+ }
257
+ function statePath(token) {
258
+ return (0, import_node_path2.join)(stateDir(), `${tokenHash(token)}.json`);
259
+ }
260
+ function emptyState(token, agent, cwd) {
261
+ const now = (/* @__PURE__ */ new Date()).toISOString();
262
+ return {
263
+ tokenHash: tokenHash(token),
264
+ agent,
265
+ acpSessionId: null,
266
+ cwd,
267
+ createdAt: now,
268
+ lastUsedAt: now,
269
+ usage: { ...EMPTY_USAGE }
270
+ };
271
+ }
272
+ function loadState(token) {
273
+ const path = statePath(token);
274
+ if (!(0, import_node_fs2.existsSync)(path)) return null;
275
+ try {
276
+ const parsed = JSON.parse((0, import_node_fs2.readFileSync)(path, "utf8"));
277
+ if (typeof parsed.acpSessionId !== "string" && parsed.acpSessionId !== null) return null;
278
+ if (typeof parsed.agent !== "string") return null;
279
+ return {
280
+ tokenHash: tokenHash(token),
281
+ agent: parsed.agent,
282
+ acpSessionId: parsed.acpSessionId ?? null,
283
+ cwd: typeof parsed.cwd === "string" ? parsed.cwd : process.cwd(),
284
+ createdAt: typeof parsed.createdAt === "string" ? parsed.createdAt : (/* @__PURE__ */ new Date()).toISOString(),
285
+ lastUsedAt: typeof parsed.lastUsedAt === "string" ? parsed.lastUsedAt : (/* @__PURE__ */ new Date()).toISOString(),
286
+ usage: parsed.usage && typeof parsed.usage === "object" ? { ...EMPTY_USAGE, ...parsed.usage } : { ...EMPTY_USAGE }
287
+ };
288
+ } catch {
289
+ return null;
290
+ }
291
+ }
292
+ function saveState(state) {
293
+ const dir = stateDir();
294
+ (0, import_node_fs2.mkdirSync)(dir, { recursive: true });
295
+ const path = (0, import_node_path2.join)(dir, `${state.tokenHash}.json`);
296
+ const tmp = `${path}.${process.pid}.tmp`;
297
+ (0, import_node_fs2.writeFileSync)(tmp, `${JSON.stringify(state, null, 2)}
298
+ `, { mode: 384 });
299
+ (0, import_node_fs2.renameSync)(tmp, path);
300
+ }
301
+
302
+ // src/core/lock.ts
303
+ var LockError = class extends Error {
304
+ constructor(message) {
305
+ super(message);
306
+ this.name = "LockError";
307
+ }
308
+ };
309
+ var lockPath = (token) => (0, import_node_path3.join)(stateDir(), `${tokenHash(token)}.lock`);
310
+ function readPid(path) {
311
+ try {
312
+ const pid = Number.parseInt((0, import_node_fs3.readFileSync)(path, "utf8").trim(), 10);
313
+ return Number.isFinite(pid) && pid > 0 ? pid : null;
314
+ } catch {
315
+ return null;
316
+ }
317
+ }
318
+ function isAlive(pid) {
319
+ try {
320
+ process.kill(pid, 0);
321
+ return true;
322
+ } catch {
323
+ return false;
324
+ }
325
+ }
326
+ function acquireLock(token) {
327
+ (0, import_node_fs3.mkdirSync)(stateDir(), { recursive: true });
328
+ const path = lockPath(token);
329
+ for (let attempt = 0; attempt < 2; attempt += 1) {
330
+ try {
331
+ const fd = (0, import_node_fs3.openSync)(path, "wx");
332
+ (0, import_node_fs3.writeSync)(fd, String(process.pid));
333
+ (0, import_node_fs3.closeSync)(fd);
334
+ return () => releaseLock(token);
335
+ } catch (error) {
336
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "EEXIST") {
337
+ throw error;
338
+ }
339
+ const pid = readPid(path);
340
+ if (pid && isAlive(pid)) {
341
+ throw new LockError(`This bot is already running in another vbot process (pid ${pid}).`);
342
+ }
343
+ try {
344
+ (0, import_node_fs3.unlinkSync)(path);
345
+ } catch {
346
+ }
347
+ }
348
+ }
349
+ throw new LockError("Failed to acquire vbot lock; please retry.");
350
+ }
351
+ function releaseLock(token) {
352
+ const path = lockPath(token);
353
+ if (!(0, import_node_fs3.existsSync)(path)) return;
354
+ const pid = readPid(path);
355
+ if (pid !== process.pid) return;
356
+ try {
357
+ (0, import_node_fs3.unlinkSync)(path);
358
+ } catch {
359
+ }
360
+ }
361
+
362
+ // src/agents/acp.ts
363
+ var import_node_child_process = require("child_process");
364
+ var import_node_stream = require("stream");
365
+ var acp = __toESM(require("@agentclientprotocol/sdk"), 1);
366
+
367
+ // src/version.ts
368
+ var VBOT_VERSION = "0.1.3";
369
+
370
+ // src/agents/acp.ts
371
+ var AgentAbortError = class extends Error {
372
+ constructor() {
373
+ super("Aborted");
374
+ this.name = "AbortError";
375
+ }
376
+ };
377
+ var AgentStartError = class extends Error {
378
+ constructor(message, stderr) {
379
+ super(message);
380
+ this.stderr = stderr;
381
+ this.name = "AgentStartError";
382
+ }
383
+ stderr;
384
+ };
385
+ var STDERR_LIMIT = 4e3;
386
+ function flattenSelectOptions(options) {
387
+ return options.flatMap(
388
+ (entry) => "group" in entry ? entry.options.map((option) => ({ value: option.value, label: option.name })) : [{ value: entry.value, label: entry.name }]
389
+ );
390
+ }
391
+ function isModelOption(option) {
392
+ return option.type === "select" && (option.category === "model" || option.id === "model");
393
+ }
394
+ var AcpAgent = class {
395
+ constructor(name, config) {
396
+ this.name = name;
397
+ this.config = config;
398
+ }
399
+ name;
400
+ config;
401
+ child = null;
402
+ connection = null;
403
+ ctx = null;
404
+ startPromise = null;
405
+ session = null;
406
+ configOptions = [];
407
+ initCaps;
408
+ turn = null;
409
+ stderr = "";
410
+ sawUsageCost = false;
411
+ pendingModel = null;
412
+ capabilities() {
413
+ return {
414
+ resume: Boolean(this.initCaps?.sessionCapabilities?.resume),
415
+ load: Boolean(this.initCaps?.loadSession),
416
+ setConfigOption: this.configOptions.length > 0,
417
+ usageCost: this.sawUsageCost
418
+ };
419
+ }
420
+ sessionId() {
421
+ return this.session;
422
+ }
423
+ currentModel() {
424
+ const option = this.configOptions.find(isModelOption);
425
+ if (!option || option.type !== "select") return null;
426
+ const value = String(option.currentValue);
427
+ const options = flattenSelectOptions(option.options);
428
+ const label = options.find((entry) => entry.value === value)?.label ?? value;
429
+ return { value, label, options };
430
+ }
431
+ resetSession() {
432
+ this.session = null;
433
+ }
434
+ async start() {
435
+ if (this.connection) return;
436
+ if (this.startPromise) return this.startPromise;
437
+ this.startPromise = this.doStart().finally(() => {
438
+ this.startPromise = null;
439
+ });
440
+ return this.startPromise;
441
+ }
442
+ async doStart() {
443
+ const definition = definitionOf(this.name);
444
+ const command = this.config.command ?? definition.command.command;
445
+ const args = this.config.args ?? definition.command.args;
446
+ const child = (0, import_node_child_process.spawn)(command, args, {
447
+ cwd: this.config.cwd,
448
+ env: { ...process.env, ...this.config.env },
449
+ stdio: ["pipe", "pipe", "pipe"]
450
+ });
451
+ this.child = child;
452
+ this.stderr = "";
453
+ child.stderr?.on("data", (chunk) => {
454
+ this.stderr = `${this.stderr}${chunk.toString()}`.slice(-STDERR_LIMIT);
455
+ if (this.stderr.length > 0) {
456
+ console.error(`[${this.name}] ${chunk.toString().trimEnd()}`);
457
+ }
458
+ });
459
+ child.on("exit", (code, signal) => {
460
+ if (this.child === child) {
461
+ this.connection = null;
462
+ this.ctx = null;
463
+ this.child = null;
464
+ }
465
+ if (code !== 0 && signal !== "SIGTERM") {
466
+ console.error(`[${this.name}] exited with code ${code ?? "?"} signal ${signal ?? "-"}`);
467
+ }
468
+ });
469
+ if (!child.stdout || !child.stdin) {
470
+ child.kill();
471
+ throw new AgentStartError(
472
+ `Failed to start ${this.name}: stdio pipes are unavailable`,
473
+ this.stderr
474
+ );
475
+ }
476
+ const input = import_node_stream.Writable.toWeb(child.stdin);
477
+ const output = import_node_stream.Readable.toWeb(child.stdout);
478
+ const stream = acp.ndJsonStream(input, output);
479
+ const app = acp.client({ name: "vbot" });
480
+ app.onRequest(acp.methods.client.session.requestPermission, ({ params }) => {
481
+ const options = params.options;
482
+ const chosen = options.find((option) => option.kind === "allow_always") ?? options.find((option) => option.kind === "allow_once");
483
+ if (!chosen) {
484
+ return { outcome: { outcome: "cancelled" } };
485
+ }
486
+ return { outcome: { outcome: "selected", optionId: chosen.optionId } };
487
+ });
488
+ app.onNotification(acp.methods.client.session.update, ({ params }) => {
489
+ this.handleUpdate(params);
490
+ });
491
+ const connection = app.connect(stream);
492
+ this.connection = connection;
493
+ this.ctx = connection.agent;
494
+ try {
495
+ const init = await this.ctx.request(acp.methods.agent.initialize, {
496
+ protocolVersion: 1,
497
+ clientCapabilities: {},
498
+ clientInfo: { name: "vbot", version: VBOT_VERSION }
499
+ });
500
+ this.initCaps = init.agentCapabilities;
501
+ } catch (error) {
502
+ await this.close();
503
+ throw new AgentStartError(
504
+ `Failed to initialize ${this.name}: ${error instanceof Error ? error.message : String(error)}`,
505
+ this.stderr
506
+ );
507
+ }
508
+ }
509
+ async ensureSession(savedSessionId) {
510
+ if (!this.ctx) throw new Error(`${this.name} is not started`);
511
+ if (this.session) return { sessionId: this.session, isNew: false };
512
+ if (savedSessionId) {
513
+ const caps = this.initCaps;
514
+ try {
515
+ if (caps?.sessionCapabilities?.resume) {
516
+ const resumed = await this.ctx.request(acp.methods.agent.session.resume, {
517
+ sessionId: savedSessionId,
518
+ cwd: this.config.cwd,
519
+ mcpServers: []
520
+ });
521
+ this.configOptions = resumed.configOptions ?? [];
522
+ this.session = savedSessionId;
523
+ return { sessionId: savedSessionId, isNew: false };
524
+ }
525
+ if (caps?.loadSession) {
526
+ const loaded = await this.ctx.request(acp.methods.agent.session.load, {
527
+ sessionId: savedSessionId,
528
+ cwd: this.config.cwd,
529
+ mcpServers: []
530
+ });
531
+ this.configOptions = loaded.configOptions ?? [];
532
+ this.session = savedSessionId;
533
+ return { sessionId: savedSessionId, isNew: false };
534
+ }
535
+ } catch (error) {
536
+ console.warn(
537
+ `[${this.name}] failed to resume session ${savedSessionId}, starting a new one: ${error instanceof Error ? error.message : String(error)}`
538
+ );
539
+ }
540
+ }
541
+ const created = await this.ctx.request(acp.methods.agent.session.new, {
542
+ cwd: this.config.cwd,
543
+ mcpServers: []
544
+ });
545
+ this.session = created.sessionId;
546
+ this.configOptions = created.configOptions ?? [];
547
+ return { sessionId: created.sessionId, isNew: true };
548
+ }
549
+ async run(request) {
550
+ await this.start();
551
+ const opened = await this.ensureSession(request.sessionId);
552
+ const model = request.model ?? this.pendingModel;
553
+ if (model) {
554
+ await this.setModel(model).catch(() => false);
555
+ }
556
+ const turn = {
557
+ text: "",
558
+ usage: { ...EMPTY_USAGE },
559
+ sawCost: false,
560
+ onEvent: request.onEvent
561
+ };
562
+ this.turn = turn;
563
+ const aborted = new Promise((_, reject) => {
564
+ if (request.signal.aborted) {
565
+ reject(new AgentAbortError());
566
+ return;
567
+ }
568
+ request.signal.addEventListener("abort", () => reject(new AgentAbortError()), { once: true });
569
+ });
570
+ const onAbort = () => this.cancel().catch(() => void 0);
571
+ if (request.signal.aborted) onAbort();
572
+ else request.signal.addEventListener("abort", onAbort, { once: true });
573
+ try {
574
+ if (!this.ctx) throw new Error(`${this.name} is not started`);
575
+ const pending = this.ctx.request(
576
+ acp.methods.agent.session.prompt,
577
+ {
578
+ sessionId: opened.sessionId,
579
+ prompt: [{ type: "text", text: request.prompt }]
580
+ },
581
+ { cancellationSignal: request.signal, stopWaitingOnCancel: true }
582
+ );
583
+ pending.catch(() => void 0);
584
+ const response = await Promise.race([pending, aborted]);
585
+ await new Promise((resolve3) => setImmediate(resolve3));
586
+ if (response.stopReason === "cancelled" || request.signal.aborted) {
587
+ throw new AgentAbortError();
588
+ }
589
+ return {
590
+ text: turn.text,
591
+ sessionId: opened.sessionId,
592
+ usage: turn.usage,
593
+ newSession: opened.isNew
594
+ };
595
+ } catch (error) {
596
+ if (request.signal.aborted || error instanceof Error && error.name === "AbortError") {
597
+ throw new AgentAbortError();
598
+ }
599
+ throw error;
600
+ } finally {
601
+ request.signal.removeEventListener("abort", onAbort);
602
+ this.turn = null;
603
+ }
604
+ }
605
+ async cancel() {
606
+ if (!this.ctx || !this.session) return;
607
+ try {
608
+ await this.ctx.notify(acp.methods.agent.session.cancel, { sessionId: this.session });
609
+ } catch {
610
+ }
611
+ }
612
+ async setModel(model) {
613
+ if (!this.ctx || !this.session) {
614
+ this.pendingModel = model;
615
+ return true;
616
+ }
617
+ const option = this.configOptions.find(isModelOption);
618
+ if (!option) return false;
619
+ const response = await this.ctx.request(acp.methods.agent.session.setConfigOption, {
620
+ sessionId: this.session,
621
+ configId: option.id,
622
+ value: model
623
+ });
624
+ if (response?.configOptions?.length) this.configOptions = response.configOptions;
625
+ this.pendingModel = null;
626
+ return true;
627
+ }
628
+ async close() {
629
+ const child = this.child;
630
+ const connection = this.connection;
631
+ this.child = null;
632
+ this.connection = null;
633
+ this.ctx = null;
634
+ this.session = null;
635
+ this.configOptions = [];
636
+ this.pendingModel = null;
637
+ this.turn = null;
638
+ try {
639
+ connection?.close();
640
+ } catch {
641
+ }
642
+ if (!child || child.exitCode !== null) return;
643
+ const exited = new Promise((resolve3) => {
644
+ child.once("exit", () => resolve3());
645
+ });
646
+ child.kill("SIGTERM");
647
+ const timer = setTimeout(() => child.kill("SIGKILL"), 3e3);
648
+ await exited;
649
+ clearTimeout(timer);
650
+ }
651
+ handleUpdate(notification) {
652
+ if (notification.sessionId !== this.session) return;
653
+ const turn = this.turn;
654
+ if (!turn) return;
655
+ this.dispatchUpdate(notification.update, turn);
656
+ }
657
+ dispatchUpdate(update, turn) {
658
+ switch (update.sessionUpdate) {
659
+ case "agent_message_chunk":
660
+ this.applyContentChunk(update, turn, "text");
661
+ break;
662
+ case "agent_thought_chunk":
663
+ this.applyContentChunk(update, turn, "thought");
664
+ break;
665
+ case "tool_call":
666
+ turn.onEvent?.({ type: "tool", name: update.title, status: update.status ?? "pending" });
667
+ break;
668
+ case "tool_call_update":
669
+ this.applyToolUpdate(update, turn);
670
+ break;
671
+ case "usage_update":
672
+ this.applyUsage(update, turn);
673
+ break;
674
+ case "config_option_update":
675
+ this.configOptions = update.configOptions;
676
+ break;
677
+ default:
678
+ break;
679
+ }
680
+ }
681
+ applyContentChunk(chunk, turn, kind) {
682
+ if (chunk.content.type !== "text") return;
683
+ if (kind === "text") turn.text += chunk.content.text;
684
+ turn.onEvent?.({ type: kind, text: chunk.content.text });
685
+ }
686
+ applyToolUpdate(tool, turn) {
687
+ turn.onEvent?.({
688
+ type: "tool",
689
+ name: tool.title ?? tool.toolCallId,
690
+ status: tool.status ?? "pending"
691
+ });
692
+ }
693
+ applyUsage(update, turn) {
694
+ turn.usage = { used: update.used, size: update.size, cost: update.cost ?? null };
695
+ if (update.cost) {
696
+ turn.sawCost = true;
697
+ this.sawUsageCost = true;
698
+ }
699
+ turn.onEvent?.({ type: "usage", usage: turn.usage });
700
+ }
701
+ };
702
+
703
+ // src/core/queue.ts
704
+ var BotQueue = class {
705
+ constructor(processor) {
706
+ this.processor = processor;
707
+ }
708
+ processor;
709
+ items = [];
710
+ running = false;
711
+ controller = null;
712
+ currentText = null;
713
+ async enqueue(item) {
714
+ if (this.running) {
715
+ const ahead = this.items.length + 1;
716
+ this.items.push(item);
717
+ await item.reply(`Queued \u2014 ${ahead} message(s) ahead of you`);
718
+ return;
719
+ }
720
+ this.running = true;
721
+ this.items.push(item);
722
+ try {
723
+ await this.drain();
724
+ } finally {
725
+ this.running = false;
726
+ this.controller = null;
727
+ this.currentText = null;
728
+ }
729
+ }
730
+ /** 中止当前任务并清空队列;返回是否有任务被中止。 */
731
+ stop() {
732
+ const wasBusy = this.running;
733
+ this.controller?.abort();
734
+ this.controller = null;
735
+ this.items.length = 0;
736
+ return wasBusy;
737
+ }
738
+ status() {
739
+ return { busy: this.running, queued: this.items.length, current: this.currentText };
740
+ }
741
+ async drain() {
742
+ while (this.items.length > 0) {
743
+ const item = this.items.shift();
744
+ if (!item) break;
745
+ this.controller = new AbortController();
746
+ this.currentText = item.text;
747
+ try {
748
+ await this.processor(item, this.controller.signal);
749
+ } catch (error) {
750
+ console.error("[queue] processor error:", error);
751
+ await item.reply(`Error: ${error instanceof Error ? error.message : String(error)}`).catch(() => void 0);
752
+ } finally {
753
+ this.controller = null;
754
+ this.currentText = null;
755
+ }
756
+ }
757
+ }
758
+ };
759
+
760
+ // src/core/dispatcher.ts
761
+ var import_node_os3 = require("os");
762
+ var HELP_ROWS = [
763
+ ["/new", "Start a new session"],
764
+ ["/stop", "Abort the current request"],
765
+ ["/status", "Show agent, session and queue status"],
766
+ ["/agent [name]", "Show or switch agent backend"],
767
+ ["/model [name]", "Show or switch model"],
768
+ ["/cost", "Show context usage and cost"],
769
+ ["/version", "Show vbot version"],
770
+ ["/help", "Show this help"]
771
+ ];
772
+ var HELP_FOOTER = "Other /commands are sent to the agent as prompts.";
773
+ var NEW_BUSY_TEXT = "Cannot reset the session while a request is running. Use /stop first.";
774
+ var MODEL_BUSY_TEXT = "Cannot switch model while a request is running. Use /stop first.";
775
+ var HELP_TEXT = [
776
+ table(
777
+ ["Command", "Description"],
778
+ HELP_ROWS.map(([command, description]) => [command, description])
779
+ ),
780
+ "",
781
+ HELP_FOOTER
782
+ ].join("\n");
783
+ function createDispatcher(runtime) {
784
+ return (text, reply) => {
785
+ const send = (message) => {
786
+ reply(message).catch((error) => console.error("[dispatch] reply failed:", error));
787
+ };
788
+ const prompt = text.startsWith("/") ? handleSlashCommand(runtime, text, send) : text;
789
+ if (prompt === null) return;
790
+ runtime.queue().enqueue({ text: prompt, reply }).catch((error) => {
791
+ console.error("[dispatch] enqueue failed:", error);
792
+ send("System error, please try again later.");
793
+ });
794
+ };
795
+ }
796
+ function escapeCell(value) {
797
+ return value.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
798
+ }
799
+ function table(headers, rows) {
800
+ const head = `| ${headers.map(escapeCell).join(" | ")} |`;
801
+ const separator = `| ${headers.map(() => "---").join(" | ")} |`;
802
+ const body = rows.map((row) => `| ${row.map(escapeCell).join(" | ")} |`);
803
+ return [head, separator, ...body].join("\n");
804
+ }
805
+ function formatNumber(value) {
806
+ if (!Number.isFinite(value)) return String(value);
807
+ const negative = value < 0;
808
+ const digits = Math.abs(Math.trunc(value)).toString();
809
+ let out = "";
810
+ for (let i = 0; i < digits.length; i += 1) {
811
+ if (i > 0 && (digits.length - i) % 3 === 0) out += ",";
812
+ out += digits[i];
813
+ }
814
+ return negative ? "-" + out : out;
815
+ }
816
+ function percentOf(used, size) {
817
+ if (size <= 0) return null;
818
+ return `${(used / size * 100).toFixed(1)}%`;
819
+ }
820
+ function usageText(runtime) {
821
+ const { used, size } = runtime.state().usage;
822
+ if (size <= 0) return "not reported";
823
+ const base = `${formatNumber(used)} / ${formatNumber(size)}`;
824
+ const percent = percentOf(used, size);
825
+ return percent ? `${base} (${percent})` : base;
826
+ }
827
+ function compactPath(path) {
828
+ const home = (0, import_node_os3.homedir)();
829
+ if (!home) return path;
830
+ if (path === home) return "~";
831
+ const prefix = home + "/";
832
+ if (path.startsWith(prefix)) return "~" + path.slice(home.length);
833
+ return path;
834
+ }
835
+ function modelText(runtime) {
836
+ const current = runtime.agent().currentModel();
837
+ if (current) return current.label;
838
+ const configured = runtime.config().agent.model;
839
+ if (configured) return `${configured} (configured, not applied yet)`;
840
+ return "unknown (session not started)";
841
+ }
842
+ function handleSlashCommand(runtime, text, send) {
843
+ const [rawCommand, ...args] = text.split(/\s+/);
844
+ switch (rawCommand.toLowerCase()) {
845
+ case "/new":
846
+ void handleNew(runtime, send);
847
+ return null;
848
+ case "/stop":
849
+ handleStop(runtime, send);
850
+ return null;
851
+ case "/status":
852
+ send(statusText(runtime));
853
+ return null;
854
+ case "/agent":
855
+ handleAgent(runtime, args, send);
856
+ return null;
857
+ case "/model":
858
+ void handleModel(runtime, args, send);
859
+ return null;
860
+ case "/cost":
861
+ send(costText(runtime));
862
+ return null;
863
+ case "/help":
864
+ send(HELP_TEXT);
865
+ return null;
866
+ case "/version":
867
+ send(`vbot v${VBOT_VERSION}`);
868
+ return null;
869
+ default:
870
+ return text.slice(1);
871
+ }
872
+ }
873
+ async function handleNew(runtime, send) {
874
+ if (runtime.queue().status().busy) {
875
+ send(NEW_BUSY_TEXT);
876
+ return;
877
+ }
878
+ await runtime.serialize(async () => {
879
+ runtime.agent().resetSession();
880
+ const state = runtime.state();
881
+ state.acpSessionId = null;
882
+ state.usage = { used: 0, size: 0, cost: null };
883
+ runtime.saveState();
884
+ });
885
+ send("Session reset. Starting a new conversation.");
886
+ }
887
+ function handleStop(runtime, send) {
888
+ const queue = runtime.queue();
889
+ const queued = queue.status().queued;
890
+ if (!queue.stop()) {
891
+ send("No running request to stop.");
892
+ return;
893
+ }
894
+ send(
895
+ queued > 0 ? `Current request aborted. ${queued} queued message(s) discarded.` : "Current request aborted."
896
+ );
897
+ }
898
+ function handleAgent(runtime, args, send) {
899
+ const name = args.join(" ").trim();
900
+ const current = runtime.config().agent.name;
901
+ if (!name) {
902
+ send(
903
+ table(
904
+ ["Agent", "Backend", ""],
905
+ AGENT_NAMES.map((entry) => [
906
+ entry,
907
+ AGENTS[entry].label,
908
+ entry === current ? "\u2190 current" : ""
909
+ ])
910
+ )
911
+ );
912
+ return;
913
+ }
914
+ if (!isAgentName(name)) {
915
+ send(`Unknown agent "${name}". Available: ${AGENT_NAMES.join(", ")}`);
916
+ return;
917
+ }
918
+ if (runtime.queue().status().busy) {
919
+ send("Cannot switch agent while a request is running. Use /stop first.");
920
+ return;
921
+ }
922
+ send(`Switching to ${name}...`);
923
+ runtime.switchAgent(name).then((message) => send(message)).catch(
924
+ (error) => send(`Error: ${error instanceof Error ? error.message : String(error)}`)
925
+ );
926
+ }
927
+ async function handleModel(runtime, args, send) {
928
+ const model = args.join(" ").trim();
929
+ if (!model) {
930
+ send(modelReport(runtime));
931
+ return;
932
+ }
933
+ if (runtime.queue().status().busy) {
934
+ send(MODEL_BUSY_TEXT);
935
+ return;
936
+ }
937
+ const config = runtime.config();
938
+ const hadSession = runtime.agent().sessionId() !== null;
939
+ const supported = await runtime.serialize(async () => {
940
+ const accepted = await runtime.agent().setModel(model);
941
+ config.agent.model = model;
942
+ runtime.saveConfig();
943
+ return accepted;
944
+ });
945
+ const applied = runtime.agent().currentModel()?.label ?? model;
946
+ if (!supported) {
947
+ send(
948
+ `Model saved: ${model}. This agent does not expose a runtime model option; it will be used on the next session if supported.`
949
+ );
950
+ return;
951
+ }
952
+ send(
953
+ hadSession ? `Model switched to: ${applied}` : `Model set to: ${applied}. It will apply when the session starts.`
954
+ );
955
+ }
956
+ function modelReport(runtime) {
957
+ const current = runtime.agent().currentModel();
958
+ if (!current) {
959
+ const configured = runtime.config().agent.model;
960
+ const hint = configured ? `Configured: ${configured}
961
+
962
+ 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.";
963
+ return `Current: unknown
964
+
965
+ ${hint}`;
966
+ }
967
+ const lines = [`Current: ${current.label}`];
968
+ if (current.options.length > 0) {
969
+ lines.push(
970
+ "",
971
+ table(
972
+ ["Model", ""],
973
+ current.options.map((option) => [
974
+ option.label,
975
+ option.value === current.value ? "\u2190 current" : ""
976
+ ])
977
+ )
978
+ );
979
+ }
980
+ lines.push("", "Switch: /model <name>");
981
+ return lines.join("\n");
982
+ }
983
+ function statusText(runtime) {
984
+ const config = runtime.config();
985
+ const agent = runtime.agent();
986
+ const state = runtime.state();
987
+ const queue = runtime.queue().status();
988
+ const { used, size } = state.usage;
989
+ const summary = [
990
+ `**${config.agent.name}**`,
991
+ queue.busy ? "busy" : "idle",
992
+ size > 0 ? `${formatNumber(used)} / ${formatNumber(size)}` : null
993
+ ].filter(Boolean).join(" \xB7 ");
994
+ const rows = [
995
+ ["Agent", `${config.agent.name} (${AGENTS[config.agent.name].label})`],
996
+ ["Session", agent.sessionId() ?? "none"],
997
+ ["Work dir", compactPath(config.agent.cwd)],
998
+ ["Model", modelText(runtime)],
999
+ ["Status", queue.busy ? "busy" : "idle"],
1000
+ ["Queue", String(queue.queued)],
1001
+ ["Usage", usageText(runtime)],
1002
+ ["Capabilities", capabilitiesText(agent.capabilities())]
1003
+ ];
1004
+ return [summary, "", table(["Field", "Value"], rows)].join("\n");
1005
+ }
1006
+ function costText(runtime) {
1007
+ const usage = runtime.state().usage;
1008
+ const cost = usage.cost ? `${usage.cost.amount} ${usage.cost.currency}` : "not reported by this agent";
1009
+ return table(
1010
+ ["Field", "Value"],
1011
+ [
1012
+ ["Context", usageText(runtime)],
1013
+ ["Cost", cost]
1014
+ ]
1015
+ );
1016
+ }
1017
+ function capabilitiesText(caps) {
1018
+ const enabled = [
1019
+ caps.resume ? "resume" : null,
1020
+ caps.load ? "load" : null,
1021
+ caps.setConfigOption ? "model" : null,
1022
+ caps.usageCost ? "cost" : null
1023
+ ].filter(Boolean);
1024
+ return enabled.length > 0 ? enabled.join(", ") : "basic";
1025
+ }
1026
+
1027
+ // src/core/gate.ts
1028
+ function createGate() {
1029
+ let tail = Promise.resolve();
1030
+ return function run(task) {
1031
+ const result = tail.then(task, task);
1032
+ tail = result.then(
1033
+ () => void 0,
1034
+ () => void 0
1035
+ );
1036
+ return result;
1037
+ };
1038
+ }
1039
+
1040
+ // src/core/switch.ts
1041
+ async function switchAgent(ctx, name) {
1042
+ const previous = ctx.getAgent();
1043
+ const nextConfig = {
1044
+ ...ctx.config.agent,
1045
+ name,
1046
+ command: void 0,
1047
+ args: void 0,
1048
+ model: null
1049
+ };
1050
+ const next = ctx.createAgent ? ctx.createAgent(name, nextConfig) : new AcpAgent(name, nextConfig);
1051
+ await next.start();
1052
+ ctx.setAgent(next);
1053
+ ctx.config.agent = nextConfig;
1054
+ ctx.state.agent = name;
1055
+ ctx.state.acpSessionId = null;
1056
+ ctx.state.usage = { ...EMPTY_USAGE };
1057
+ ctx.saveConfig();
1058
+ ctx.saveState();
1059
+ await previous.close().catch(() => void 0);
1060
+ return `Switched to ${name} (${AGENTS[name].label}). A new session will start.`;
1061
+ }
1062
+
1063
+ // src/core/wire.ts
1064
+ var import_ws = __toESM(require("ws"), 1);
1065
+ var RECONNECT_BASE_MS = 1e3;
1066
+ var RECONNECT_MAX_MS = 3e4;
1067
+ var HEARTBEAT_MS = 3e4;
1068
+ var INVALID_TOKEN_CODE = 4001;
1069
+ function trimTrailingSlash2(value) {
1070
+ let end = value.length;
1071
+ while (end > 0 && value[end - 1] === "/") end -= 1;
1072
+ return value.slice(0, end);
1073
+ }
1074
+ function botUrl(baseUrl, token) {
1075
+ const wsBase = trimTrailingSlash2(baseUrl.replace(/^http/, "ws"));
1076
+ return `${wsBase}/bot/connect?token=${encodeURIComponent(token)}`;
1077
+ }
1078
+ var VicvicWire = class {
1079
+ constructor(options) {
1080
+ this.options = options;
1081
+ }
1082
+ options;
1083
+ socket = null;
1084
+ stopped = false;
1085
+ attempt = 0;
1086
+ reconnectTimer = null;
1087
+ heartbeatTimer = null;
1088
+ start() {
1089
+ this.stopped = false;
1090
+ this.connect();
1091
+ }
1092
+ stop() {
1093
+ this.stopped = true;
1094
+ if (this.reconnectTimer) {
1095
+ clearTimeout(this.reconnectTimer);
1096
+ this.reconnectTimer = null;
1097
+ }
1098
+ this.clearHeartbeat();
1099
+ this.socket?.close();
1100
+ this.socket = null;
1101
+ }
1102
+ /** 发送回复;socket 不可用时 reject,由调用方决定是否记录日志。 */
1103
+ async reply(msgUid, text) {
1104
+ const socket = this.socket;
1105
+ if (!socket || socket.readyState !== import_ws.default.OPEN) {
1106
+ throw new Error("Bot connection is not open");
1107
+ }
1108
+ await new Promise((resolve3, reject) => {
1109
+ socket.send(JSON.stringify({ type: "reply", msgUid, text }), (error) => {
1110
+ if (error) reject(error);
1111
+ else resolve3();
1112
+ });
1113
+ });
1114
+ }
1115
+ connect() {
1116
+ if (this.stopped) return;
1117
+ const url = botUrl(this.options.baseUrl, this.options.token);
1118
+ const socket = new import_ws.default(url);
1119
+ this.socket = socket;
1120
+ socket.on("open", () => {
1121
+ this.attempt = 0;
1122
+ this.startHeartbeat();
1123
+ console.log("[vicvic] connected");
1124
+ });
1125
+ socket.on("message", (raw) => {
1126
+ let frame;
1127
+ try {
1128
+ frame = JSON.parse(String(raw));
1129
+ } catch {
1130
+ return;
1131
+ }
1132
+ if (frame.type === "ready") {
1133
+ console.log("[vicvic] ready");
1134
+ this.options.handlers.onReady?.();
1135
+ return;
1136
+ }
1137
+ if (frame.type === "prompt" && typeof frame.text === "string" && frame.text.length > 0) {
1138
+ this.options.handlers.onPrompt(
1139
+ frame.text,
1140
+ frame.msgUid,
1141
+ (text) => this.reply(frame.msgUid, text)
1142
+ );
1143
+ }
1144
+ });
1145
+ socket.on("close", (code, reason) => {
1146
+ this.clearHeartbeat();
1147
+ if (this.socket === socket) this.socket = null;
1148
+ if (code === INVALID_TOKEN_CODE) {
1149
+ this.stopped = true;
1150
+ this.options.handlers.onFatal?.(
1151
+ new Error(`Invalid bot token (${reason.toString() || "4001"}).`)
1152
+ );
1153
+ return;
1154
+ }
1155
+ console.log("[vicvic] disconnected");
1156
+ this.options.handlers.onDisconnected?.();
1157
+ this.scheduleReconnect();
1158
+ });
1159
+ socket.on("error", (error) => {
1160
+ console.error("[vicvic] ws error:", error.message);
1161
+ });
1162
+ }
1163
+ scheduleReconnect() {
1164
+ if (this.stopped || this.reconnectTimer) return;
1165
+ const delay = Math.min(RECONNECT_BASE_MS * 2 ** this.attempt, RECONNECT_MAX_MS);
1166
+ this.attempt += 1;
1167
+ console.log(`[vicvic] reconnecting in ${Math.round(delay / 1e3)}s`);
1168
+ this.reconnectTimer = setTimeout(() => {
1169
+ this.reconnectTimer = null;
1170
+ this.connect();
1171
+ }, delay);
1172
+ }
1173
+ startHeartbeat() {
1174
+ this.clearHeartbeat();
1175
+ this.heartbeatTimer = setInterval(() => {
1176
+ try {
1177
+ this.socket?.ping();
1178
+ } catch {
1179
+ }
1180
+ }, HEARTBEAT_MS);
1181
+ }
1182
+ clearHeartbeat() {
1183
+ if (this.heartbeatTimer) {
1184
+ clearInterval(this.heartbeatTimer);
1185
+ this.heartbeatTimer = null;
1186
+ }
1187
+ }
1188
+ };
1189
+
1190
+ // src/core/chunk.ts
1191
+ var MAX_MESSAGE_CHARS = 4e3;
1192
+ function isLowSurrogate(code) {
1193
+ return code >= 56320 && code <= 57343;
1194
+ }
1195
+ function fenceCount(text) {
1196
+ return (text.match(/```/g) ?? []).length;
1197
+ }
1198
+ function avoidSurrogateSplit(text, cut, start) {
1199
+ return cut > start && isLowSurrogate(text.charCodeAt(cut)) ? cut - 1 : cut;
1200
+ }
1201
+ function preferNewline(text, start, cut, max) {
1202
+ const newline = text.lastIndexOf("\n", cut);
1203
+ return newline >= start + Math.floor(max / 2) ? newline + 1 : cut;
1204
+ }
1205
+ function avoidFenceSplit(text, start, cut, max) {
1206
+ if (fenceCount(text.slice(start, cut)) % 2 === 0) return cut;
1207
+ const fenceStart = text.lastIndexOf("```", cut);
1208
+ return fenceStart >= start + Math.floor(max / 4) ? fenceStart : cut;
1209
+ }
1210
+ function findCut(text, start, max) {
1211
+ const hardCut = Math.min(start + max, text.length);
1212
+ let cut = avoidSurrogateSplit(text, hardCut, start);
1213
+ cut = preferNewline(text, start, cut, max);
1214
+ cut = avoidFenceSplit(text, start, cut, max);
1215
+ return cut > start ? cut : hardCut;
1216
+ }
1217
+ function splitMessage(text, max = MAX_MESSAGE_CHARS) {
1218
+ if (!text) return [];
1219
+ if (max < 2) throw new Error("max must be >= 2");
1220
+ const chunks = [];
1221
+ let start = 0;
1222
+ while (start < text.length) {
1223
+ if (text.length - start <= max) {
1224
+ chunks.push(text.slice(start));
1225
+ break;
1226
+ }
1227
+ const cut = findCut(text, start, max);
1228
+ const chunk = text.slice(start, cut);
1229
+ if (chunk.trim().length > 0) chunks.push(chunk);
1230
+ start = cut;
1231
+ }
1232
+ return chunks;
1233
+ }
1234
+
1235
+ // src/server.ts
1236
+ function replySafe(reply, text) {
1237
+ reply(text).catch((error) => console.error("[server] reply failed:", error));
1238
+ }
1239
+ function logAgentEvent(event) {
1240
+ if (event.type === "tool") {
1241
+ console.log(`[agent] tool ${event.name} (${event.status})`);
1242
+ } else if (event.type === "usage") {
1243
+ const cost = event.usage.cost ? ` ${event.usage.cost.amount} ${event.usage.cost.currency}` : "";
1244
+ console.log(`[agent] usage ${event.usage.used}/${event.usage.size}${cost}`);
1245
+ } else if (event.type === "thought") {
1246
+ console.log(`[agent] thought: ${event.text.slice(0, 80)}`);
1247
+ }
1248
+ }
1249
+ async function startBot(configPath2) {
1250
+ const { config } = loadConfig(configPath2);
1251
+ const token = config.vicvic.token;
1252
+ const release = acquireLock(token);
1253
+ const state = loadState(token) ?? emptyState(token, config.agent.name, config.agent.cwd);
1254
+ state.agent = config.agent.name;
1255
+ state.cwd = config.agent.cwd;
1256
+ let agent = new AcpAgent(config.agent.name, config.agent);
1257
+ const gate = createGate();
1258
+ let switching = false;
1259
+ const holder = { queue: null };
1260
+ const ctx = {
1261
+ config,
1262
+ configPath: configPath2,
1263
+ state,
1264
+ getAgent: () => agent,
1265
+ setAgent: (next) => {
1266
+ agent = next;
1267
+ },
1268
+ getQueue: () => {
1269
+ if (!holder.queue) throw new Error("queue is not initialized");
1270
+ return holder.queue;
1271
+ },
1272
+ saveState: () => {
1273
+ state.lastUsedAt = (/* @__PURE__ */ new Date()).toISOString();
1274
+ saveState(state);
1275
+ },
1276
+ saveConfig: () => saveConfig(config, configPath2)
1277
+ };
1278
+ holder.queue = new BotQueue((item, signal) => gate(() => processPrompt(ctx, item, signal)));
1279
+ const runtime = createRuntime(
1280
+ ctx,
1281
+ (task) => gate(task),
1282
+ (name) => {
1283
+ if (switching) return Promise.reject(new Error("Agent switch already in progress."));
1284
+ switching = true;
1285
+ return gate(() => switchAgent(ctx, name)).finally(() => {
1286
+ switching = false;
1287
+ });
1288
+ }
1289
+ );
1290
+ const dispatch = createDispatcher(runtime);
1291
+ let notifySignal = () => void 0;
1292
+ const wire = new VicvicWire({
1293
+ baseUrl: config.vicvic.baseUrl,
1294
+ token,
1295
+ handlers: {
1296
+ onPrompt: (text, _msgUid, reply) => {
1297
+ if (config.logPrompt) console.log(`[vicvic] prompt: ${text.slice(0, 100)}...`);
1298
+ else console.log("[vicvic] prompt");
1299
+ dispatch(text, reply);
1300
+ },
1301
+ onFatal: (error) => {
1302
+ console.error(`[vicvic] fatal: ${error.message}`);
1303
+ process.exitCode = 2;
1304
+ notifySignal();
1305
+ }
1306
+ }
1307
+ });
1308
+ const lifecycle = setupLifecycle(wire, () => agent, release);
1309
+ notifySignal = lifecycle.onSignal;
1310
+ announce(config);
1311
+ wire.start();
1312
+ return { finished: lifecycle.finished, stop: lifecycle.stop };
1313
+ }
1314
+ function setupLifecycle(getWire, getAgent, release) {
1315
+ let finish = () => void 0;
1316
+ const finished = new Promise((resolveFinished) => {
1317
+ finish = resolveFinished;
1318
+ });
1319
+ let stopped = false;
1320
+ const onSignal = () => {
1321
+ console.log("[vbot] shutting down");
1322
+ stop().catch((error) => console.error("[server] shutdown failed:", error));
1323
+ };
1324
+ const stop = async () => {
1325
+ if (stopped) return;
1326
+ stopped = true;
1327
+ getWire.stop();
1328
+ process.off("SIGINT", onSignal);
1329
+ process.off("SIGTERM", onSignal);
1330
+ await getAgent().close().catch(() => void 0);
1331
+ release();
1332
+ finish();
1333
+ };
1334
+ process.once("SIGINT", onSignal);
1335
+ process.once("SIGTERM", onSignal);
1336
+ return { finished, onSignal, stop };
1337
+ }
1338
+ function announce(config) {
1339
+ console.warn(
1340
+ `vbot runs ${config.agent.name} with your local user permissions and auto-approves permission requests.
1341
+ Only use it in a directory you trust. Authentication must be completed in the agent CLI beforehand.`
1342
+ );
1343
+ console.log(AGENTS[config.agent.name].authHint);
1344
+ console.log(`vbot starting. agent=${config.agent.name} cwd=${config.agent.cwd}`);
1345
+ }
1346
+ async function runBot(configPath2) {
1347
+ const bot = await startBot(configPath2);
1348
+ await bot.finished;
1349
+ }
1350
+ function createRuntime(ctx, gate, switchAgent2) {
1351
+ return {
1352
+ config: () => ctx.config,
1353
+ agent: () => ctx.getAgent(),
1354
+ queue: () => ctx.getQueue(),
1355
+ state: () => ctx.state,
1356
+ saveState: () => ctx.saveState(),
1357
+ saveConfig: () => ctx.saveConfig(),
1358
+ serialize: (task) => gate(task),
1359
+ switchAgent: switchAgent2
1360
+ };
1361
+ }
1362
+ async function processPrompt(ctx, item, signal) {
1363
+ replySafe(item.reply, "Processing...");
1364
+ const controller = new AbortController();
1365
+ let timedOut = false;
1366
+ const onAbort = () => controller.abort();
1367
+ signal.addEventListener("abort", onAbort, { once: true });
1368
+ const timer = setTimeout(() => {
1369
+ timedOut = true;
1370
+ controller.abort();
1371
+ }, ctx.config.timeoutMs);
1372
+ try {
1373
+ const result = await ctx.getAgent().run({
1374
+ prompt: item.text,
1375
+ sessionId: ctx.state.acpSessionId ?? void 0,
1376
+ cwd: ctx.config.agent.cwd,
1377
+ model: ctx.config.agent.model,
1378
+ signal: controller.signal,
1379
+ onEvent: ctx.config.logPrompt ? logAgentEvent : void 0
1380
+ });
1381
+ ctx.state.acpSessionId = result.sessionId;
1382
+ ctx.state.usage = result.usage;
1383
+ ctx.saveState();
1384
+ for (const chunk of splitMessage(result.text || "(no output)")) {
1385
+ await item.reply(chunk);
1386
+ }
1387
+ } catch (error) {
1388
+ if (error instanceof AgentAbortError) {
1389
+ if (timedOut)
1390
+ replySafe(item.reply, "Execution timeout, please retry or simplify your request.");
1391
+ else if (!signal.aborted) replySafe(item.reply, "Request aborted.");
1392
+ return;
1393
+ }
1394
+ replyError(item.reply, error);
1395
+ } finally {
1396
+ clearTimeout(timer);
1397
+ signal.removeEventListener("abort", onAbort);
1398
+ }
1399
+ }
1400
+ function replyError(reply, error) {
1401
+ if (error instanceof AgentStartError) {
1402
+ console.error(`[server] agent start failed: ${error.message}
1403
+ ${error.stderr}`);
1404
+ replySafe(reply, `Error: ${error.message}`);
1405
+ return;
1406
+ }
1407
+ console.error("[server] agent run failed:", error);
1408
+ replySafe(reply, `Error: ${error instanceof Error ? error.message : String(error)}`);
1409
+ }
1410
+
1411
+ // src/bin-server.ts
1412
+ var configPath = process.argv[2];
1413
+ if (!configPath) {
1414
+ console.error("Usage: bin-server.js <config-path>");
1415
+ process.exit(1);
1416
+ }
1417
+ runBot((0, import_node_path4.resolve)(configPath)).catch((error) => {
1418
+ console.error(error instanceof Error ? error.message : String(error));
1419
+ const expected = error instanceof ConfigError || error instanceof LockError;
1420
+ process.exit(expected ? 2 : 1);
1421
+ });
1422
+ //# sourceMappingURL=bin-server.cjs.map