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