volute 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/LICENSE +21 -0
- package/README.md +227 -0
- package/dist/channel-Q642YUZE.js +90 -0
- package/dist/chunk-5YW4B7CG.js +181 -0
- package/dist/chunk-A5ZJEMHT.js +40 -0
- package/dist/chunk-D424ZQGI.js +31 -0
- package/dist/chunk-GSPKUPKU.js +120 -0
- package/dist/chunk-H5XQARAP.js +48 -0
- package/dist/chunk-KSMIWOCN.js +84 -0
- package/dist/chunk-N4QN44LC.js +74 -0
- package/dist/chunk-XZN4WPNC.js +34 -0
- package/dist/cli.js +95 -0
- package/dist/connect-LW6G23AV.js +48 -0
- package/dist/connectors/discord.js +213 -0
- package/dist/create-3K6O2SDC.js +62 -0
- package/dist/daemon-client-ZTHW7ROS.js +10 -0
- package/dist/daemon.js +1731 -0
- package/dist/delete-JNGY7ZFH.js +54 -0
- package/dist/disconnect-ACVTKTRE.js +30 -0
- package/dist/down-FYCUYC5H.js +71 -0
- package/dist/env-7SLRN3MG.js +159 -0
- package/dist/fork-BB3DZ426.js +112 -0
- package/dist/import-W2AMTEV5.js +410 -0
- package/dist/logs-BUHRIQ2L.js +35 -0
- package/dist/merge-446QTE7Q.js +219 -0
- package/dist/schedule-KKSOVUDF.js +113 -0
- package/dist/send-WQSVSRDD.js +50 -0
- package/dist/start-LKMWS6ZE.js +29 -0
- package/dist/status-CIEKUI3V.js +50 -0
- package/dist/stop-YTOAGYE4.js +29 -0
- package/dist/up-AJJ4GCXY.js +111 -0
- package/dist/upgrade-JACA6YMO.js +211 -0
- package/dist/variants-HPY4DEWU.js +60 -0
- package/dist/web-assets/assets/index-DNNPoxMn.js +158 -0
- package/dist/web-assets/index.html +15 -0
- package/package.json +76 -0
- package/templates/_base/.init/MEMORY.md +2 -0
- package/templates/_base/.init/SOUL.md +2 -0
- package/templates/_base/.init/memory/.gitkeep +0 -0
- package/templates/_base/_skills/memory/SKILL.md +30 -0
- package/templates/_base/_skills/volute-agent/SKILL.md +53 -0
- package/templates/_base/biome.json.tmpl +21 -0
- package/templates/_base/home/VOLUTE.md +19 -0
- package/templates/_base/src/lib/auto-commit.ts +46 -0
- package/templates/_base/src/lib/logger.ts +47 -0
- package/templates/_base/src/lib/types.ts +24 -0
- package/templates/_base/src/lib/volute-server.ts +98 -0
- package/templates/_base/tsconfig.json +13 -0
- package/templates/_base/volute.json.tmpl +3 -0
- package/templates/agent-sdk/.init/CLAUDE.md +36 -0
- package/templates/agent-sdk/package.json.tmpl +20 -0
- package/templates/agent-sdk/src/lib/agent.ts +199 -0
- package/templates/agent-sdk/src/lib/hooks/auto-commit.ts +14 -0
- package/templates/agent-sdk/src/lib/hooks/identity-reload.ts +26 -0
- package/templates/agent-sdk/src/lib/hooks/pre-compact.ts +20 -0
- package/templates/agent-sdk/src/lib/message-channel.ts +37 -0
- package/templates/agent-sdk/src/server.ts +158 -0
- package/templates/agent-sdk/volute-template.json +9 -0
- package/templates/pi/.init/AGENTS.md +26 -0
- package/templates/pi/package.json.tmpl +20 -0
- package/templates/pi/src/lib/agent.ts +205 -0
- package/templates/pi/src/server.ts +121 -0
- package/templates/pi/volute-template.json +9 -0
- package/templates/pi/volute.json.tmpl +3 -0
package/dist/daemon.js
ADDED
|
@@ -0,0 +1,1731 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
logBuffer,
|
|
4
|
+
logger_default,
|
|
5
|
+
readNdjson
|
|
6
|
+
} from "./chunk-KSMIWOCN.js";
|
|
7
|
+
import {
|
|
8
|
+
loadMergedEnv
|
|
9
|
+
} from "./chunk-A5ZJEMHT.js";
|
|
10
|
+
import {
|
|
11
|
+
VOLUTE_HOME,
|
|
12
|
+
__export,
|
|
13
|
+
agentDir,
|
|
14
|
+
checkHealth,
|
|
15
|
+
findAgent,
|
|
16
|
+
readRegistry,
|
|
17
|
+
readVariants,
|
|
18
|
+
removeAgent,
|
|
19
|
+
removeAllVariants,
|
|
20
|
+
setAgentRunning
|
|
21
|
+
} from "./chunk-5YW4B7CG.js";
|
|
22
|
+
|
|
23
|
+
// src/daemon.ts
|
|
24
|
+
import { randomBytes } from "crypto";
|
|
25
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
26
|
+
import { resolve as resolve8 } from "path";
|
|
27
|
+
|
|
28
|
+
// src/lib/agent-manager.ts
|
|
29
|
+
import { execFile, spawn } from "child_process";
|
|
30
|
+
import { createWriteStream, existsSync, mkdirSync, readFileSync, unlinkSync } from "fs";
|
|
31
|
+
import { resolve } from "path";
|
|
32
|
+
import { promisify } from "util";
|
|
33
|
+
var execFileAsync = promisify(execFile);
|
|
34
|
+
var AgentManager = class {
|
|
35
|
+
agents = /* @__PURE__ */ new Map();
|
|
36
|
+
stopping = /* @__PURE__ */ new Set();
|
|
37
|
+
shuttingDown = false;
|
|
38
|
+
async startAgent(name) {
|
|
39
|
+
if (this.agents.has(name)) {
|
|
40
|
+
throw new Error(`Agent ${name} is already running`);
|
|
41
|
+
}
|
|
42
|
+
const entry = findAgent(name);
|
|
43
|
+
if (!entry) throw new Error(`Unknown agent: ${name}`);
|
|
44
|
+
const dir = agentDir(name);
|
|
45
|
+
if (!existsSync(dir)) throw new Error(`Agent directory missing: ${dir}`);
|
|
46
|
+
const port = entry.port;
|
|
47
|
+
try {
|
|
48
|
+
const res = await fetch(`http://localhost:${port}/health`);
|
|
49
|
+
if (res.ok) {
|
|
50
|
+
console.error(`[daemon] killing orphan process on port ${port}`);
|
|
51
|
+
await killProcessOnPort(port);
|
|
52
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
}
|
|
56
|
+
const voluteDir = resolve(dir, ".volute");
|
|
57
|
+
const logsDir = resolve(voluteDir, "logs");
|
|
58
|
+
mkdirSync(logsDir, { recursive: true });
|
|
59
|
+
const logStream = createWriteStream(resolve(logsDir, "agent.log"), {
|
|
60
|
+
flags: "a"
|
|
61
|
+
});
|
|
62
|
+
const agentEnv = loadMergedEnv(dir);
|
|
63
|
+
const env = { ...process.env, ...agentEnv, VOLUTE_AGENT: name };
|
|
64
|
+
const tsxBin = resolve(dir, "node_modules", ".bin", "tsx");
|
|
65
|
+
const child = spawn(tsxBin, ["src/server.ts", "--port", String(port)], {
|
|
66
|
+
cwd: dir,
|
|
67
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
68
|
+
detached: true,
|
|
69
|
+
env
|
|
70
|
+
});
|
|
71
|
+
this.agents.set(name, { child, port });
|
|
72
|
+
child.stdout?.pipe(logStream);
|
|
73
|
+
child.stderr?.pipe(logStream);
|
|
74
|
+
try {
|
|
75
|
+
await new Promise((resolve9, reject) => {
|
|
76
|
+
const timeout = setTimeout(() => {
|
|
77
|
+
reject(new Error(`Agent ${name} did not start within 30s`));
|
|
78
|
+
}, 3e4);
|
|
79
|
+
function checkOutput(data) {
|
|
80
|
+
if (data.toString().match(/listening on :\d+/)) {
|
|
81
|
+
clearTimeout(timeout);
|
|
82
|
+
resolve9();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
child.stdout?.on("data", checkOutput);
|
|
86
|
+
child.stderr?.on("data", checkOutput);
|
|
87
|
+
child.on("error", (err) => {
|
|
88
|
+
clearTimeout(timeout);
|
|
89
|
+
reject(err);
|
|
90
|
+
});
|
|
91
|
+
child.on("exit", (code) => {
|
|
92
|
+
clearTimeout(timeout);
|
|
93
|
+
reject(new Error(`Agent ${name} exited with code ${code} during startup`));
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
} catch (err) {
|
|
97
|
+
this.agents.delete(name);
|
|
98
|
+
try {
|
|
99
|
+
child.kill();
|
|
100
|
+
} catch {
|
|
101
|
+
}
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
this.setupCrashRecovery(name, child, dir);
|
|
105
|
+
setAgentRunning(name, true);
|
|
106
|
+
console.error(`[daemon] started agent ${name} on port ${port}`);
|
|
107
|
+
}
|
|
108
|
+
setupCrashRecovery(name, child, dir) {
|
|
109
|
+
child.on("exit", async (code) => {
|
|
110
|
+
this.agents.delete(name);
|
|
111
|
+
if (this.shuttingDown || this.stopping.has(name)) return;
|
|
112
|
+
console.error(`[daemon] agent ${name} exited with code ${code}`);
|
|
113
|
+
const wasRestart = await this.handleRestart(name, dir);
|
|
114
|
+
if (wasRestart) {
|
|
115
|
+
console.error(`[daemon] restarting ${name} immediately after merge`);
|
|
116
|
+
this.startAgent(name).catch((err) => {
|
|
117
|
+
console.error(`[daemon] failed to restart ${name} after merge:`, err);
|
|
118
|
+
});
|
|
119
|
+
} else {
|
|
120
|
+
console.error(`[daemon] crash recovery for ${name} \u2014 restarting in 3s`);
|
|
121
|
+
setTimeout(() => {
|
|
122
|
+
if (this.shuttingDown) return;
|
|
123
|
+
this.startAgent(name).catch((err) => {
|
|
124
|
+
console.error(`[daemon] failed to restart ${name}:`, err);
|
|
125
|
+
});
|
|
126
|
+
}, 3e3);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
async handleRestart(name, dir) {
|
|
131
|
+
const restartPath = resolve(dir, ".volute", "restart.json");
|
|
132
|
+
if (!existsSync(restartPath)) return false;
|
|
133
|
+
try {
|
|
134
|
+
const signal = JSON.parse(readFileSync(restartPath, "utf-8"));
|
|
135
|
+
unlinkSync(restartPath);
|
|
136
|
+
if (signal.action === "merge" && signal.name) {
|
|
137
|
+
console.error(`[daemon] merging variant for ${name}: ${signal.name}`);
|
|
138
|
+
const mergeArgs = ["merge", name, signal.name];
|
|
139
|
+
if (signal.summary) mergeArgs.push("--summary", signal.summary);
|
|
140
|
+
if (signal.justification) mergeArgs.push("--justification", signal.justification);
|
|
141
|
+
if (signal.memory) mergeArgs.push("--memory", signal.memory);
|
|
142
|
+
await execFileAsync("volute", mergeArgs, {
|
|
143
|
+
cwd: dir,
|
|
144
|
+
env: { ...process.env, VOLUTE_SUPERVISOR: "1" }
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
return true;
|
|
148
|
+
} catch (e) {
|
|
149
|
+
console.error(`[daemon] failed to handle restart for ${name}:`, e);
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async stopAgent(name) {
|
|
154
|
+
const tracked = this.agents.get(name);
|
|
155
|
+
if (!tracked) return;
|
|
156
|
+
this.stopping.add(name);
|
|
157
|
+
const { child } = tracked;
|
|
158
|
+
this.agents.delete(name);
|
|
159
|
+
await new Promise((resolve9) => {
|
|
160
|
+
child.on("exit", () => resolve9());
|
|
161
|
+
try {
|
|
162
|
+
process.kill(-child.pid, "SIGTERM");
|
|
163
|
+
} catch {
|
|
164
|
+
resolve9();
|
|
165
|
+
}
|
|
166
|
+
setTimeout(() => {
|
|
167
|
+
try {
|
|
168
|
+
process.kill(-child.pid, "SIGKILL");
|
|
169
|
+
} catch {
|
|
170
|
+
}
|
|
171
|
+
resolve9();
|
|
172
|
+
}, 5e3);
|
|
173
|
+
});
|
|
174
|
+
this.stopping.delete(name);
|
|
175
|
+
setAgentRunning(name, false);
|
|
176
|
+
console.error(`[daemon] stopped agent ${name}`);
|
|
177
|
+
}
|
|
178
|
+
async restartAgent(name) {
|
|
179
|
+
await this.stopAgent(name);
|
|
180
|
+
await this.startAgent(name);
|
|
181
|
+
}
|
|
182
|
+
async stopAll() {
|
|
183
|
+
this.shuttingDown = true;
|
|
184
|
+
const names = [...this.agents.keys()];
|
|
185
|
+
await Promise.all(names.map((name) => this.stopAgent(name)));
|
|
186
|
+
}
|
|
187
|
+
isRunning(name) {
|
|
188
|
+
return this.agents.has(name);
|
|
189
|
+
}
|
|
190
|
+
getRunningAgents() {
|
|
191
|
+
return [...this.agents.keys()];
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
async function killProcessOnPort(port) {
|
|
195
|
+
try {
|
|
196
|
+
const { stdout } = await execFileAsync("lsof", ["-ti", `:${port}`, "-sTCP:LISTEN"]);
|
|
197
|
+
const pids = /* @__PURE__ */ new Set();
|
|
198
|
+
for (const line of stdout.trim().split("\n").filter(Boolean)) {
|
|
199
|
+
const pid = parseInt(line, 10);
|
|
200
|
+
pids.add(pid);
|
|
201
|
+
try {
|
|
202
|
+
const { stdout: psOut } = await execFileAsync("ps", ["-p", String(pid), "-o", "pgid="]);
|
|
203
|
+
const pgid = parseInt(psOut.trim(), 10);
|
|
204
|
+
if (pgid > 1) pids.add(pgid);
|
|
205
|
+
} catch {
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
for (const pid of pids) {
|
|
209
|
+
try {
|
|
210
|
+
process.kill(-pid, "SIGTERM");
|
|
211
|
+
} catch {
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
process.kill(pid, "SIGTERM");
|
|
215
|
+
} catch {
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} catch {
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
var instance = null;
|
|
222
|
+
function initAgentManager() {
|
|
223
|
+
if (instance) throw new Error("AgentManager already initialized");
|
|
224
|
+
instance = new AgentManager();
|
|
225
|
+
return instance;
|
|
226
|
+
}
|
|
227
|
+
function getAgentManager() {
|
|
228
|
+
if (!instance) instance = new AgentManager();
|
|
229
|
+
return instance;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/lib/connector-manager.ts
|
|
233
|
+
import { spawn as spawn2 } from "child_process";
|
|
234
|
+
import {
|
|
235
|
+
createWriteStream as createWriteStream2,
|
|
236
|
+
existsSync as existsSync3,
|
|
237
|
+
mkdirSync as mkdirSync2,
|
|
238
|
+
readFileSync as readFileSync3,
|
|
239
|
+
unlinkSync as unlinkSync2,
|
|
240
|
+
writeFileSync as writeFileSync2
|
|
241
|
+
} from "fs";
|
|
242
|
+
import { homedir } from "os";
|
|
243
|
+
import { dirname, resolve as resolve3 } from "path";
|
|
244
|
+
|
|
245
|
+
// src/lib/volute-config.ts
|
|
246
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
247
|
+
import { resolve as resolve2 } from "path";
|
|
248
|
+
function readVoluteConfig(agentDir2) {
|
|
249
|
+
const path = resolve2(agentDir2, "volute.json");
|
|
250
|
+
if (!existsSync2(path)) return {};
|
|
251
|
+
try {
|
|
252
|
+
return JSON.parse(readFileSync2(path, "utf-8"));
|
|
253
|
+
} catch {
|
|
254
|
+
return {};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function writeVoluteConfig(agentDir2, config) {
|
|
258
|
+
const path = resolve2(agentDir2, "volute.json");
|
|
259
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}
|
|
260
|
+
`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/lib/connector-manager.ts
|
|
264
|
+
var ConnectorManager = class {
|
|
265
|
+
connectors = /* @__PURE__ */ new Map();
|
|
266
|
+
stopping = /* @__PURE__ */ new Set();
|
|
267
|
+
// "agent:type" keys currently being explicitly stopped
|
|
268
|
+
shuttingDown = false;
|
|
269
|
+
async startConnectors(agentName, agentDir2, agentPort) {
|
|
270
|
+
const config = readVoluteConfig(agentDir2);
|
|
271
|
+
const types = config.connectors ?? [];
|
|
272
|
+
for (const type of types) {
|
|
273
|
+
try {
|
|
274
|
+
await this.startConnector(agentName, agentDir2, agentPort, type);
|
|
275
|
+
} catch (err) {
|
|
276
|
+
console.error(`[daemon] failed to start connector ${type} for ${agentName}:`, err);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async startConnector(agentName, agentDir2, agentPort, type) {
|
|
281
|
+
const existing = this.connectors.get(agentName)?.get(type);
|
|
282
|
+
if (existing) {
|
|
283
|
+
await new Promise((res) => {
|
|
284
|
+
existing.child.on("exit", () => res());
|
|
285
|
+
try {
|
|
286
|
+
existing.child.kill("SIGTERM");
|
|
287
|
+
} catch {
|
|
288
|
+
res();
|
|
289
|
+
}
|
|
290
|
+
setTimeout(() => {
|
|
291
|
+
try {
|
|
292
|
+
existing.child.kill("SIGKILL");
|
|
293
|
+
} catch {
|
|
294
|
+
}
|
|
295
|
+
res();
|
|
296
|
+
}, 3e3);
|
|
297
|
+
});
|
|
298
|
+
this.connectors.get(agentName)?.delete(type);
|
|
299
|
+
}
|
|
300
|
+
this.killOrphanConnector(agentDir2, type);
|
|
301
|
+
const agentConnector = resolve3(agentDir2, "connectors", type, "index.ts");
|
|
302
|
+
const userConnector = resolve3(homedir(), ".volute", "connectors", type, "index.ts");
|
|
303
|
+
const builtinConnector = this.resolveBuiltinConnector(type);
|
|
304
|
+
let connectorScript;
|
|
305
|
+
let runtime;
|
|
306
|
+
if (existsSync3(agentConnector)) {
|
|
307
|
+
connectorScript = agentConnector;
|
|
308
|
+
runtime = resolve3(agentDir2, "node_modules", ".bin", "tsx");
|
|
309
|
+
} else if (existsSync3(userConnector)) {
|
|
310
|
+
connectorScript = userConnector;
|
|
311
|
+
runtime = this.resolveVoluteTsx();
|
|
312
|
+
} else if (builtinConnector && existsSync3(builtinConnector)) {
|
|
313
|
+
connectorScript = builtinConnector;
|
|
314
|
+
runtime = process.execPath;
|
|
315
|
+
} else {
|
|
316
|
+
throw new Error(`No connector code found for type: ${type}`);
|
|
317
|
+
}
|
|
318
|
+
const logsDir = resolve3(agentDir2, ".volute", "logs");
|
|
319
|
+
mkdirSync2(logsDir, { recursive: true });
|
|
320
|
+
const logStream = createWriteStream2(resolve3(logsDir, `${type}.log`), { flags: "a" });
|
|
321
|
+
const agentEnv = loadMergedEnv(agentDir2);
|
|
322
|
+
const prefix = `${type.toUpperCase()}_`;
|
|
323
|
+
const connectorEnv = Object.fromEntries(
|
|
324
|
+
Object.entries(agentEnv).filter(([k]) => k.startsWith(prefix))
|
|
325
|
+
);
|
|
326
|
+
const child = spawn2(runtime, [connectorScript], {
|
|
327
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
328
|
+
env: {
|
|
329
|
+
...process.env,
|
|
330
|
+
VOLUTE_AGENT_PORT: String(agentPort),
|
|
331
|
+
VOLUTE_AGENT_NAME: agentName,
|
|
332
|
+
...connectorEnv
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
child.stdout?.pipe(logStream);
|
|
336
|
+
child.stderr?.pipe(logStream);
|
|
337
|
+
if (child.pid) {
|
|
338
|
+
this.saveConnectorPid(agentDir2, type, child.pid);
|
|
339
|
+
}
|
|
340
|
+
if (!this.connectors.has(agentName)) {
|
|
341
|
+
this.connectors.set(agentName, /* @__PURE__ */ new Map());
|
|
342
|
+
}
|
|
343
|
+
this.connectors.get(agentName).set(type, { child, type });
|
|
344
|
+
const stopKey = `${agentName}:${type}`;
|
|
345
|
+
child.on("exit", (code) => {
|
|
346
|
+
const agentMap = this.connectors.get(agentName);
|
|
347
|
+
if (agentMap?.get(type)?.child === child) {
|
|
348
|
+
agentMap.delete(type);
|
|
349
|
+
}
|
|
350
|
+
if (this.shuttingDown) return;
|
|
351
|
+
if (this.stopping.has(stopKey)) return;
|
|
352
|
+
console.error(`[daemon] connector ${type} for ${agentName} exited with code ${code}`);
|
|
353
|
+
console.error(`[daemon] restarting connector ${type} for ${agentName} in 3s`);
|
|
354
|
+
setTimeout(() => {
|
|
355
|
+
if (this.shuttingDown || this.stopping.has(stopKey)) return;
|
|
356
|
+
this.startConnector(agentName, agentDir2, agentPort, type).catch((err) => {
|
|
357
|
+
console.error(`[daemon] failed to restart connector ${type} for ${agentName}:`, err);
|
|
358
|
+
});
|
|
359
|
+
}, 3e3);
|
|
360
|
+
});
|
|
361
|
+
console.error(`[daemon] started connector ${type} for ${agentName}`);
|
|
362
|
+
}
|
|
363
|
+
async stopConnector(agentName, type) {
|
|
364
|
+
const agentMap = this.connectors.get(agentName);
|
|
365
|
+
if (!agentMap) return;
|
|
366
|
+
const tracked = agentMap.get(type);
|
|
367
|
+
if (!tracked) return;
|
|
368
|
+
const stopKey = `${agentName}:${type}`;
|
|
369
|
+
this.stopping.add(stopKey);
|
|
370
|
+
agentMap.delete(type);
|
|
371
|
+
await new Promise((resolve9) => {
|
|
372
|
+
tracked.child.on("exit", () => resolve9());
|
|
373
|
+
try {
|
|
374
|
+
tracked.child.kill("SIGTERM");
|
|
375
|
+
} catch {
|
|
376
|
+
resolve9();
|
|
377
|
+
}
|
|
378
|
+
setTimeout(() => {
|
|
379
|
+
try {
|
|
380
|
+
tracked.child.kill("SIGKILL");
|
|
381
|
+
} catch {
|
|
382
|
+
}
|
|
383
|
+
resolve9();
|
|
384
|
+
}, 5e3);
|
|
385
|
+
});
|
|
386
|
+
this.stopping.delete(stopKey);
|
|
387
|
+
try {
|
|
388
|
+
this.removeConnectorPid(agentDir(agentName), type);
|
|
389
|
+
} catch {
|
|
390
|
+
}
|
|
391
|
+
console.error(`[daemon] stopped connector ${type} for ${agentName}`);
|
|
392
|
+
}
|
|
393
|
+
async stopConnectors(agentName) {
|
|
394
|
+
const agentMap = this.connectors.get(agentName);
|
|
395
|
+
if (!agentMap) return;
|
|
396
|
+
const types = [...agentMap.keys()];
|
|
397
|
+
await Promise.all(types.map((type) => this.stopConnector(agentName, type)));
|
|
398
|
+
this.connectors.delete(agentName);
|
|
399
|
+
}
|
|
400
|
+
async stopAll() {
|
|
401
|
+
this.shuttingDown = true;
|
|
402
|
+
const agents = [...this.connectors.keys()];
|
|
403
|
+
await Promise.all(agents.map((name) => this.stopConnectors(name)));
|
|
404
|
+
}
|
|
405
|
+
getConnectorStatus(agentName) {
|
|
406
|
+
const agentMap = this.connectors.get(agentName);
|
|
407
|
+
if (!agentMap) return [];
|
|
408
|
+
return [...agentMap.entries()].map(([type, tracked]) => ({
|
|
409
|
+
type,
|
|
410
|
+
running: !tracked.child.killed
|
|
411
|
+
}));
|
|
412
|
+
}
|
|
413
|
+
connectorPidPath(agentDir2, type) {
|
|
414
|
+
return resolve3(agentDir2, ".volute", "connectors", `${type}.pid`);
|
|
415
|
+
}
|
|
416
|
+
saveConnectorPid(agentDir2, type, pid) {
|
|
417
|
+
const pidPath = this.connectorPidPath(agentDir2, type);
|
|
418
|
+
mkdirSync2(dirname(pidPath), { recursive: true });
|
|
419
|
+
writeFileSync2(pidPath, String(pid));
|
|
420
|
+
}
|
|
421
|
+
removeConnectorPid(agentDir2, type) {
|
|
422
|
+
try {
|
|
423
|
+
unlinkSync2(this.connectorPidPath(agentDir2, type));
|
|
424
|
+
} catch {
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
killOrphanConnector(agentDir2, type) {
|
|
428
|
+
const pidPath = this.connectorPidPath(agentDir2, type);
|
|
429
|
+
if (!existsSync3(pidPath)) return;
|
|
430
|
+
try {
|
|
431
|
+
const pid = parseInt(readFileSync3(pidPath, "utf-8").trim(), 10);
|
|
432
|
+
if (pid > 0) {
|
|
433
|
+
process.kill(pid, "SIGTERM");
|
|
434
|
+
console.error(`[daemon] killed orphan connector ${type} (pid ${pid})`);
|
|
435
|
+
}
|
|
436
|
+
} catch {
|
|
437
|
+
}
|
|
438
|
+
try {
|
|
439
|
+
unlinkSync2(pidPath);
|
|
440
|
+
} catch {
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
resolveBuiltinConnector(type) {
|
|
444
|
+
let searchDir = dirname(new URL(import.meta.url).pathname);
|
|
445
|
+
for (let i = 0; i < 5; i++) {
|
|
446
|
+
const candidate = resolve3(searchDir, "connectors", `${type}.js`);
|
|
447
|
+
if (existsSync3(candidate)) return candidate;
|
|
448
|
+
searchDir = dirname(searchDir);
|
|
449
|
+
}
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
resolveVoluteTsx() {
|
|
453
|
+
let searchDir = dirname(new URL(import.meta.url).pathname);
|
|
454
|
+
for (let i = 0; i < 5; i++) {
|
|
455
|
+
const candidate = resolve3(searchDir, "node_modules", ".bin", "tsx");
|
|
456
|
+
if (existsSync3(candidate)) return candidate;
|
|
457
|
+
searchDir = dirname(searchDir);
|
|
458
|
+
}
|
|
459
|
+
return "tsx";
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
var instance2 = null;
|
|
463
|
+
function initConnectorManager() {
|
|
464
|
+
if (instance2) throw new Error("ConnectorManager already initialized");
|
|
465
|
+
instance2 = new ConnectorManager();
|
|
466
|
+
return instance2;
|
|
467
|
+
}
|
|
468
|
+
function getConnectorManager() {
|
|
469
|
+
if (!instance2) instance2 = new ConnectorManager();
|
|
470
|
+
return instance2;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// src/lib/scheduler.ts
|
|
474
|
+
import { CronExpressionParser } from "cron-parser";
|
|
475
|
+
var Scheduler = class {
|
|
476
|
+
schedules = /* @__PURE__ */ new Map();
|
|
477
|
+
interval = null;
|
|
478
|
+
lastFired = /* @__PURE__ */ new Map();
|
|
479
|
+
// "agent:scheduleId" → epoch minute
|
|
480
|
+
start() {
|
|
481
|
+
this.interval = setInterval(() => this.tick(), 6e4);
|
|
482
|
+
}
|
|
483
|
+
stop() {
|
|
484
|
+
if (this.interval) clearInterval(this.interval);
|
|
485
|
+
}
|
|
486
|
+
loadSchedules(agentName) {
|
|
487
|
+
const dir = agentDir(agentName);
|
|
488
|
+
const config = readVoluteConfig(dir);
|
|
489
|
+
const schedules = config.schedules ?? [];
|
|
490
|
+
if (schedules.length > 0) {
|
|
491
|
+
this.schedules.set(agentName, schedules);
|
|
492
|
+
} else {
|
|
493
|
+
this.schedules.delete(agentName);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
unloadSchedules(agentName) {
|
|
497
|
+
this.schedules.delete(agentName);
|
|
498
|
+
}
|
|
499
|
+
tick() {
|
|
500
|
+
const now = /* @__PURE__ */ new Date();
|
|
501
|
+
for (const [agent, schedules] of this.schedules) {
|
|
502
|
+
for (const schedule of schedules) {
|
|
503
|
+
if (!schedule.enabled) continue;
|
|
504
|
+
if (this.shouldFire(schedule, now, agent)) {
|
|
505
|
+
this.fire(agent, schedule);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
shouldFire(schedule, now, agent) {
|
|
511
|
+
try {
|
|
512
|
+
const interval = CronExpressionParser.parse(schedule.cron);
|
|
513
|
+
const prev = interval.prev().toDate();
|
|
514
|
+
const epochMinute = Math.floor(now.getTime() / 6e4);
|
|
515
|
+
const key = `${agent}:${schedule.id}`;
|
|
516
|
+
if (this.lastFired.get(key) === epochMinute) return false;
|
|
517
|
+
const prevMinute = Math.floor(prev.getTime() / 6e4);
|
|
518
|
+
if (prevMinute === epochMinute) {
|
|
519
|
+
this.lastFired.set(key, epochMinute);
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
522
|
+
return false;
|
|
523
|
+
} catch {
|
|
524
|
+
return false;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
async fire(agentName, schedule) {
|
|
528
|
+
const entry = findAgent(agentName);
|
|
529
|
+
if (!entry) return;
|
|
530
|
+
try {
|
|
531
|
+
await fetch(`http://localhost:${entry.port}/message`, {
|
|
532
|
+
method: "POST",
|
|
533
|
+
headers: { "Content-Type": "application/json" },
|
|
534
|
+
body: JSON.stringify({
|
|
535
|
+
content: [{ type: "text", text: schedule.message }],
|
|
536
|
+
channel: "system:scheduler",
|
|
537
|
+
sender: "scheduler"
|
|
538
|
+
})
|
|
539
|
+
});
|
|
540
|
+
console.error(`[scheduler] fired "${schedule.id}" for ${agentName}`);
|
|
541
|
+
} catch (err) {
|
|
542
|
+
console.error(`[scheduler] failed to fire "${schedule.id}" for ${agentName}:`, err);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
var instance3 = null;
|
|
547
|
+
function getScheduler() {
|
|
548
|
+
if (!instance3) instance3 = new Scheduler();
|
|
549
|
+
return instance3;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// src/web/server.ts
|
|
553
|
+
import { existsSync as existsSync8 } from "fs";
|
|
554
|
+
import { readFile as readFile2, stat } from "fs/promises";
|
|
555
|
+
import { dirname as dirname3, extname, resolve as resolve7 } from "path";
|
|
556
|
+
import { serve } from "@hono/node-server";
|
|
557
|
+
|
|
558
|
+
// src/web/app.ts
|
|
559
|
+
import { Hono as Hono11 } from "hono";
|
|
560
|
+
import { csrf } from "hono/csrf";
|
|
561
|
+
import { HTTPException } from "hono/http-exception";
|
|
562
|
+
|
|
563
|
+
// src/web/middleware/auth.ts
|
|
564
|
+
import { getCookie } from "hono/cookie";
|
|
565
|
+
import { createMiddleware } from "hono/factory";
|
|
566
|
+
|
|
567
|
+
// src/lib/auth.ts
|
|
568
|
+
import { compareSync, hashSync } from "bcryptjs";
|
|
569
|
+
import { and, count, eq } from "drizzle-orm";
|
|
570
|
+
|
|
571
|
+
// src/lib/db.ts
|
|
572
|
+
import { existsSync as existsSync4 } from "fs";
|
|
573
|
+
import { dirname as dirname2, resolve as resolve4 } from "path";
|
|
574
|
+
import { fileURLToPath } from "url";
|
|
575
|
+
import { drizzle } from "drizzle-orm/libsql";
|
|
576
|
+
import { migrate } from "drizzle-orm/libsql/migrator";
|
|
577
|
+
|
|
578
|
+
// src/lib/schema.ts
|
|
579
|
+
var schema_exports = {};
|
|
580
|
+
__export(schema_exports, {
|
|
581
|
+
agentMessages: () => agentMessages,
|
|
582
|
+
conversations: () => conversations,
|
|
583
|
+
messages: () => messages,
|
|
584
|
+
users: () => users
|
|
585
|
+
});
|
|
586
|
+
import { sql } from "drizzle-orm";
|
|
587
|
+
import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
588
|
+
var users = sqliteTable("users", {
|
|
589
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
590
|
+
username: text("username").unique().notNull(),
|
|
591
|
+
password_hash: text("password_hash").notNull(),
|
|
592
|
+
role: text("role").notNull().default("pending"),
|
|
593
|
+
created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
|
|
594
|
+
});
|
|
595
|
+
var conversations = sqliteTable(
|
|
596
|
+
"conversations",
|
|
597
|
+
{
|
|
598
|
+
id: text("id").primaryKey(),
|
|
599
|
+
agent_name: text("agent_name").notNull(),
|
|
600
|
+
channel: text("channel").notNull(),
|
|
601
|
+
user_id: integer("user_id").references(() => users.id),
|
|
602
|
+
title: text("title"),
|
|
603
|
+
created_at: text("created_at").notNull().default(sql`(datetime('now'))`),
|
|
604
|
+
updated_at: text("updated_at").notNull().default(sql`(datetime('now'))`)
|
|
605
|
+
},
|
|
606
|
+
(table) => [
|
|
607
|
+
index("idx_conversations_agent_name").on(table.agent_name),
|
|
608
|
+
index("idx_conversations_user_id").on(table.user_id),
|
|
609
|
+
index("idx_conversations_updated_at").on(table.updated_at)
|
|
610
|
+
]
|
|
611
|
+
);
|
|
612
|
+
var agentMessages = sqliteTable(
|
|
613
|
+
"agent_messages",
|
|
614
|
+
{
|
|
615
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
616
|
+
agent: text("agent").notNull(),
|
|
617
|
+
channel: text("channel").notNull(),
|
|
618
|
+
role: text("role").notNull(),
|
|
619
|
+
sender: text("sender"),
|
|
620
|
+
content: text("content").notNull(),
|
|
621
|
+
created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
|
|
622
|
+
},
|
|
623
|
+
(table) => [
|
|
624
|
+
index("idx_agent_messages_agent").on(table.agent),
|
|
625
|
+
index("idx_agent_messages_channel").on(table.agent, table.channel)
|
|
626
|
+
]
|
|
627
|
+
);
|
|
628
|
+
var messages = sqliteTable(
|
|
629
|
+
"messages",
|
|
630
|
+
{
|
|
631
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
632
|
+
conversation_id: text("conversation_id").notNull().references(() => conversations.id, { onDelete: "cascade" }),
|
|
633
|
+
role: text("role").notNull(),
|
|
634
|
+
sender_name: text("sender_name"),
|
|
635
|
+
content: text("content").notNull(),
|
|
636
|
+
created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
|
|
637
|
+
},
|
|
638
|
+
(table) => [index("idx_messages_conversation_id").on(table.conversation_id)]
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
// src/lib/db.ts
|
|
642
|
+
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
643
|
+
var migrationsFolder = existsSync4(resolve4(__dirname, "../drizzle")) ? resolve4(__dirname, "../drizzle") : resolve4(__dirname, "../../drizzle");
|
|
644
|
+
var db = null;
|
|
645
|
+
async function getDb() {
|
|
646
|
+
if (db) return db;
|
|
647
|
+
const dbPath = process.env.VOLUTE_DB_PATH || resolve4(VOLUTE_HOME, "volute.db");
|
|
648
|
+
db = drizzle({ connection: { url: `file:${dbPath}` }, schema: schema_exports });
|
|
649
|
+
try {
|
|
650
|
+
await migrate(db, { migrationsFolder });
|
|
651
|
+
} catch (e) {
|
|
652
|
+
if (!(e instanceof Error) || !e.message.includes("already exists")) throw e;
|
|
653
|
+
}
|
|
654
|
+
return db;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// src/lib/auth.ts
|
|
658
|
+
async function createUser(username, password) {
|
|
659
|
+
const db2 = await getDb();
|
|
660
|
+
const hash = hashSync(password, 10);
|
|
661
|
+
const [{ value }] = await db2.select({ value: count() }).from(users);
|
|
662
|
+
const role = value === 0 ? "admin" : "pending";
|
|
663
|
+
const [result] = await db2.insert(users).values({ username, password_hash: hash, role }).returning({
|
|
664
|
+
id: users.id,
|
|
665
|
+
username: users.username,
|
|
666
|
+
role: users.role,
|
|
667
|
+
created_at: users.created_at
|
|
668
|
+
});
|
|
669
|
+
return result;
|
|
670
|
+
}
|
|
671
|
+
async function verifyUser(username, password) {
|
|
672
|
+
const db2 = await getDb();
|
|
673
|
+
const row = await db2.select().from(users).where(eq(users.username, username)).get();
|
|
674
|
+
if (!row) return null;
|
|
675
|
+
if (!compareSync(password, row.password_hash)) return null;
|
|
676
|
+
const { password_hash: _, ...user } = row;
|
|
677
|
+
return user;
|
|
678
|
+
}
|
|
679
|
+
async function getUser(id) {
|
|
680
|
+
const db2 = await getDb();
|
|
681
|
+
const row = await db2.select({
|
|
682
|
+
id: users.id,
|
|
683
|
+
username: users.username,
|
|
684
|
+
role: users.role,
|
|
685
|
+
created_at: users.created_at
|
|
686
|
+
}).from(users).where(eq(users.id, id)).get();
|
|
687
|
+
return row ?? null;
|
|
688
|
+
}
|
|
689
|
+
async function getUserByUsername(username) {
|
|
690
|
+
const db2 = await getDb();
|
|
691
|
+
const row = await db2.select({
|
|
692
|
+
id: users.id,
|
|
693
|
+
username: users.username,
|
|
694
|
+
role: users.role,
|
|
695
|
+
created_at: users.created_at
|
|
696
|
+
}).from(users).where(eq(users.username, username)).get();
|
|
697
|
+
return row ?? null;
|
|
698
|
+
}
|
|
699
|
+
async function listUsers() {
|
|
700
|
+
const db2 = await getDb();
|
|
701
|
+
return db2.select({
|
|
702
|
+
id: users.id,
|
|
703
|
+
username: users.username,
|
|
704
|
+
role: users.role,
|
|
705
|
+
created_at: users.created_at
|
|
706
|
+
}).from(users).orderBy(users.created_at).all();
|
|
707
|
+
}
|
|
708
|
+
async function listPendingUsers() {
|
|
709
|
+
const db2 = await getDb();
|
|
710
|
+
return db2.select({
|
|
711
|
+
id: users.id,
|
|
712
|
+
username: users.username,
|
|
713
|
+
role: users.role,
|
|
714
|
+
created_at: users.created_at
|
|
715
|
+
}).from(users).where(eq(users.role, "pending")).orderBy(users.created_at).all();
|
|
716
|
+
}
|
|
717
|
+
async function approveUser(id) {
|
|
718
|
+
const db2 = await getDb();
|
|
719
|
+
await db2.update(users).set({ role: "user" }).where(and(eq(users.id, id), eq(users.role, "pending")));
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// src/web/middleware/auth.ts
|
|
723
|
+
var SESSION_MAX_AGE = 864e5;
|
|
724
|
+
var sessions = /* @__PURE__ */ new Map();
|
|
725
|
+
function createSession(userId) {
|
|
726
|
+
const sessionId = crypto.randomUUID();
|
|
727
|
+
sessions.set(sessionId, { userId, createdAt: Date.now() });
|
|
728
|
+
return sessionId;
|
|
729
|
+
}
|
|
730
|
+
function deleteSession(sessionId) {
|
|
731
|
+
sessions.delete(sessionId);
|
|
732
|
+
}
|
|
733
|
+
function getSessionUserId(sessionId) {
|
|
734
|
+
const session = sessions.get(sessionId);
|
|
735
|
+
if (!session) return void 0;
|
|
736
|
+
if (Date.now() - session.createdAt > SESSION_MAX_AGE) {
|
|
737
|
+
sessions.delete(sessionId);
|
|
738
|
+
return void 0;
|
|
739
|
+
}
|
|
740
|
+
return session.userId;
|
|
741
|
+
}
|
|
742
|
+
var authMiddleware = createMiddleware(async (c, next) => {
|
|
743
|
+
const authHeader = c.req.header("Authorization");
|
|
744
|
+
if (authHeader?.startsWith("Bearer ")) {
|
|
745
|
+
const token = authHeader.slice(7);
|
|
746
|
+
if (token && token === process.env.VOLUTE_DAEMON_TOKEN) {
|
|
747
|
+
c.set("user", { id: 0, username: "cli", role: "admin" });
|
|
748
|
+
await next();
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
const sessionId = getCookie(c, "volute_session");
|
|
753
|
+
if (!sessionId) return c.json({ error: "Unauthorized" }, 401);
|
|
754
|
+
const userId = getSessionUserId(sessionId);
|
|
755
|
+
if (userId == null) return c.json({ error: "Unauthorized" }, 401);
|
|
756
|
+
const user = await getUser(userId);
|
|
757
|
+
if (!user) return c.json({ error: "Unauthorized" }, 401);
|
|
758
|
+
if (user.role === "pending") return c.json({ error: "Account pending approval" }, 403);
|
|
759
|
+
c.set("user", user);
|
|
760
|
+
await next();
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
// src/web/routes/agents.ts
|
|
764
|
+
import { existsSync as existsSync5, rmSync } from "fs";
|
|
765
|
+
import { and as and2, desc, eq as eq2 } from "drizzle-orm";
|
|
766
|
+
import { Hono } from "hono";
|
|
767
|
+
import { stream } from "hono/streaming";
|
|
768
|
+
|
|
769
|
+
// src/lib/channels.ts
|
|
770
|
+
var CHANNELS = {
|
|
771
|
+
web: { name: "web", displayName: "Web UI", showToolCalls: true },
|
|
772
|
+
discord: { name: "discord", displayName: "Discord", showToolCalls: false },
|
|
773
|
+
cli: { name: "cli", displayName: "CLI", showToolCalls: true },
|
|
774
|
+
system: { name: "system", displayName: "System", showToolCalls: false }
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
// src/web/routes/agents.ts
|
|
778
|
+
async function getAgentStatus(name, _dir, port) {
|
|
779
|
+
const manager = getAgentManager();
|
|
780
|
+
let status = "stopped";
|
|
781
|
+
if (manager.isRunning(name)) {
|
|
782
|
+
const health = await checkHealth(port);
|
|
783
|
+
status = health.ok ? "running" : "starting";
|
|
784
|
+
}
|
|
785
|
+
const channels = [];
|
|
786
|
+
channels.push({
|
|
787
|
+
name: CHANNELS.web.name,
|
|
788
|
+
displayName: CHANNELS.web.displayName,
|
|
789
|
+
status: status === "running" ? "connected" : "disconnected",
|
|
790
|
+
showToolCalls: CHANNELS.web.showToolCalls
|
|
791
|
+
});
|
|
792
|
+
const connectorManager = getConnectorManager();
|
|
793
|
+
const connectorStatuses = connectorManager.getConnectorStatus(name);
|
|
794
|
+
for (const cs of connectorStatuses) {
|
|
795
|
+
const channelConfig = CHANNELS[cs.type];
|
|
796
|
+
if (channelConfig) {
|
|
797
|
+
channels.push({
|
|
798
|
+
name: channelConfig.name,
|
|
799
|
+
displayName: channelConfig.displayName,
|
|
800
|
+
status: cs.running ? "connected" : "disconnected",
|
|
801
|
+
showToolCalls: channelConfig.showToolCalls
|
|
802
|
+
});
|
|
803
|
+
} else {
|
|
804
|
+
channels.push({
|
|
805
|
+
name: cs.type,
|
|
806
|
+
displayName: cs.type,
|
|
807
|
+
status: cs.running ? "connected" : "disconnected",
|
|
808
|
+
showToolCalls: false
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return { status, channels };
|
|
813
|
+
}
|
|
814
|
+
var app = new Hono().get("/", async (c) => {
|
|
815
|
+
const entries = readRegistry();
|
|
816
|
+
const agents = await Promise.all(
|
|
817
|
+
entries.map(async (entry) => {
|
|
818
|
+
const dir = agentDir(entry.name);
|
|
819
|
+
const { status, channels } = await getAgentStatus(entry.name, dir, entry.port);
|
|
820
|
+
return { ...entry, status, channels };
|
|
821
|
+
})
|
|
822
|
+
);
|
|
823
|
+
return c.json(agents);
|
|
824
|
+
}).get("/:name", async (c) => {
|
|
825
|
+
const name = c.req.param("name");
|
|
826
|
+
const entry = findAgent(name);
|
|
827
|
+
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
828
|
+
const dir = agentDir(name);
|
|
829
|
+
if (!existsSync5(dir)) return c.json({ error: "Agent directory missing" }, 404);
|
|
830
|
+
const { status, channels } = await getAgentStatus(name, dir, entry.port);
|
|
831
|
+
return c.json({ ...entry, status, channels });
|
|
832
|
+
}).post("/:name/start", async (c) => {
|
|
833
|
+
const name = c.req.param("name");
|
|
834
|
+
const entry = findAgent(name);
|
|
835
|
+
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
836
|
+
const dir = agentDir(name);
|
|
837
|
+
if (!existsSync5(dir)) return c.json({ error: "Agent directory missing" }, 404);
|
|
838
|
+
const manager = getAgentManager();
|
|
839
|
+
if (manager.isRunning(name)) {
|
|
840
|
+
return c.json({ error: "Agent already running" }, 409);
|
|
841
|
+
}
|
|
842
|
+
try {
|
|
843
|
+
await manager.startAgent(name);
|
|
844
|
+
setAgentRunning(name, true);
|
|
845
|
+
await getConnectorManager().startConnectors(name, dir, entry.port);
|
|
846
|
+
return c.json({ ok: true });
|
|
847
|
+
} catch (err) {
|
|
848
|
+
return c.json({ error: err instanceof Error ? err.message : "Failed to start agent" }, 500);
|
|
849
|
+
}
|
|
850
|
+
}).post("/:name/restart", async (c) => {
|
|
851
|
+
const name = c.req.param("name");
|
|
852
|
+
const entry = findAgent(name);
|
|
853
|
+
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
854
|
+
const dir = agentDir(name);
|
|
855
|
+
if (!existsSync5(dir)) return c.json({ error: "Agent directory missing" }, 404);
|
|
856
|
+
const manager = getAgentManager();
|
|
857
|
+
const connectorManager = getConnectorManager();
|
|
858
|
+
try {
|
|
859
|
+
if (manager.isRunning(name)) {
|
|
860
|
+
await connectorManager.stopConnectors(name);
|
|
861
|
+
await manager.stopAgent(name);
|
|
862
|
+
}
|
|
863
|
+
await manager.startAgent(name);
|
|
864
|
+
setAgentRunning(name, true);
|
|
865
|
+
await connectorManager.startConnectors(name, dir, entry.port);
|
|
866
|
+
return c.json({ ok: true });
|
|
867
|
+
} catch (err) {
|
|
868
|
+
return c.json({ error: err instanceof Error ? err.message : "Failed to restart agent" }, 500);
|
|
869
|
+
}
|
|
870
|
+
}).post("/:name/stop", async (c) => {
|
|
871
|
+
const name = c.req.param("name");
|
|
872
|
+
const entry = findAgent(name);
|
|
873
|
+
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
874
|
+
const manager = getAgentManager();
|
|
875
|
+
if (!manager.isRunning(name)) {
|
|
876
|
+
return c.json({ error: "Agent is not running" }, 409);
|
|
877
|
+
}
|
|
878
|
+
try {
|
|
879
|
+
await getConnectorManager().stopConnectors(name);
|
|
880
|
+
await manager.stopAgent(name);
|
|
881
|
+
setAgentRunning(name, false);
|
|
882
|
+
return c.json({ ok: true });
|
|
883
|
+
} catch (err) {
|
|
884
|
+
return c.json({ error: err instanceof Error ? err.message : "Failed to stop agent" }, 500);
|
|
885
|
+
}
|
|
886
|
+
}).delete("/:name", async (c) => {
|
|
887
|
+
const name = c.req.param("name");
|
|
888
|
+
const entry = findAgent(name);
|
|
889
|
+
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
890
|
+
const dir = agentDir(name);
|
|
891
|
+
const force = c.req.query("force") === "true";
|
|
892
|
+
const manager = getAgentManager();
|
|
893
|
+
if (manager.isRunning(name)) {
|
|
894
|
+
await getConnectorManager().stopConnectors(name);
|
|
895
|
+
await manager.stopAgent(name);
|
|
896
|
+
}
|
|
897
|
+
removeAllVariants(name);
|
|
898
|
+
removeAgent(name);
|
|
899
|
+
if (force && existsSync5(dir)) {
|
|
900
|
+
rmSync(dir, { recursive: true, force: true });
|
|
901
|
+
}
|
|
902
|
+
return c.json({ ok: true });
|
|
903
|
+
}).post("/:name/message", async (c) => {
|
|
904
|
+
const name = c.req.param("name");
|
|
905
|
+
const entry = findAgent(name);
|
|
906
|
+
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
907
|
+
const manager = getAgentManager();
|
|
908
|
+
if (!manager.isRunning(name)) {
|
|
909
|
+
return c.json({ error: "Agent is not running" }, 409);
|
|
910
|
+
}
|
|
911
|
+
const body = await c.req.text();
|
|
912
|
+
const db2 = await getDb();
|
|
913
|
+
try {
|
|
914
|
+
const parsed = JSON.parse(body);
|
|
915
|
+
const channel = parsed.channel ?? "unknown";
|
|
916
|
+
const sender = parsed.sender ?? null;
|
|
917
|
+
const content = typeof parsed.content === "string" ? parsed.content : JSON.stringify(parsed.content);
|
|
918
|
+
await db2.insert(agentMessages).values({
|
|
919
|
+
agent: name,
|
|
920
|
+
channel,
|
|
921
|
+
role: "user",
|
|
922
|
+
sender,
|
|
923
|
+
content
|
|
924
|
+
});
|
|
925
|
+
} catch {
|
|
926
|
+
}
|
|
927
|
+
const res = await fetch(`http://localhost:${entry.port}/message`, {
|
|
928
|
+
method: "POST",
|
|
929
|
+
headers: { "Content-Type": "application/json" },
|
|
930
|
+
body
|
|
931
|
+
});
|
|
932
|
+
if (!res.ok) {
|
|
933
|
+
return c.json({ error: `Agent responded with ${res.status}` }, res.status);
|
|
934
|
+
}
|
|
935
|
+
if (!res.body) {
|
|
936
|
+
return c.json({ error: "No response body from agent" }, 502);
|
|
937
|
+
}
|
|
938
|
+
c.header("Content-Type", "application/x-ndjson");
|
|
939
|
+
return stream(c, async (s) => {
|
|
940
|
+
const reader = res.body.getReader();
|
|
941
|
+
const decoder = new TextDecoder();
|
|
942
|
+
let buffer = "";
|
|
943
|
+
const textParts = [];
|
|
944
|
+
let channel = "unknown";
|
|
945
|
+
try {
|
|
946
|
+
try {
|
|
947
|
+
channel = JSON.parse(body).channel ?? "unknown";
|
|
948
|
+
} catch {
|
|
949
|
+
}
|
|
950
|
+
while (true) {
|
|
951
|
+
const { done, value } = await reader.read();
|
|
952
|
+
if (done) break;
|
|
953
|
+
await s.write(value);
|
|
954
|
+
buffer += decoder.decode(value, { stream: true });
|
|
955
|
+
const lines = buffer.split("\n");
|
|
956
|
+
buffer = lines.pop() || "";
|
|
957
|
+
for (const line of lines) {
|
|
958
|
+
if (!line.trim()) continue;
|
|
959
|
+
try {
|
|
960
|
+
const event = JSON.parse(line);
|
|
961
|
+
if (event.type === "text") {
|
|
962
|
+
textParts.push(event.content);
|
|
963
|
+
}
|
|
964
|
+
} catch {
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
if (buffer.trim()) {
|
|
969
|
+
try {
|
|
970
|
+
const event = JSON.parse(buffer);
|
|
971
|
+
if (event.type === "text") {
|
|
972
|
+
textParts.push(event.content);
|
|
973
|
+
}
|
|
974
|
+
} catch {
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
if (textParts.length > 0) {
|
|
978
|
+
const db3 = await getDb();
|
|
979
|
+
await db3.insert(agentMessages).values({
|
|
980
|
+
agent: name,
|
|
981
|
+
channel,
|
|
982
|
+
role: "assistant",
|
|
983
|
+
content: textParts.join("")
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
} finally {
|
|
987
|
+
reader.releaseLock();
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
}).get("/:name/history", async (c) => {
|
|
991
|
+
const name = c.req.param("name");
|
|
992
|
+
const channel = c.req.query("channel");
|
|
993
|
+
const limit = parseInt(c.req.query("limit") ?? "50", 10);
|
|
994
|
+
const offset = parseInt(c.req.query("offset") ?? "0", 10);
|
|
995
|
+
const db2 = await getDb();
|
|
996
|
+
const conditions = [eq2(agentMessages.agent, name)];
|
|
997
|
+
if (channel) {
|
|
998
|
+
conditions.push(eq2(agentMessages.channel, channel));
|
|
999
|
+
}
|
|
1000
|
+
const rows = await db2.select().from(agentMessages).where(and2(...conditions)).orderBy(desc(agentMessages.created_at)).limit(limit).offset(offset);
|
|
1001
|
+
return c.json(rows);
|
|
1002
|
+
});
|
|
1003
|
+
var agents_default = app;
|
|
1004
|
+
|
|
1005
|
+
// src/web/routes/auth.ts
|
|
1006
|
+
import { zValidator } from "@hono/zod-validator";
|
|
1007
|
+
import { Hono as Hono2 } from "hono";
|
|
1008
|
+
import { deleteCookie, getCookie as getCookie2, setCookie } from "hono/cookie";
|
|
1009
|
+
import { z } from "zod";
|
|
1010
|
+
var credentialsSchema = z.object({
|
|
1011
|
+
username: z.string().min(1),
|
|
1012
|
+
password: z.string().min(1)
|
|
1013
|
+
});
|
|
1014
|
+
var admin = new Hono2().use(authMiddleware).get("/users", async (c) => {
|
|
1015
|
+
const user = c.get("user");
|
|
1016
|
+
if (user.role !== "admin") return c.json({ error: "Forbidden" }, 403);
|
|
1017
|
+
return c.json(await listUsers());
|
|
1018
|
+
}).get("/users/pending", async (c) => {
|
|
1019
|
+
const user = c.get("user");
|
|
1020
|
+
if (user.role !== "admin") return c.json({ error: "Forbidden" }, 403);
|
|
1021
|
+
return c.json(await listPendingUsers());
|
|
1022
|
+
}).post("/users/:id/approve", async (c) => {
|
|
1023
|
+
const user = c.get("user");
|
|
1024
|
+
if (user.role !== "admin") return c.json({ error: "Forbidden" }, 403);
|
|
1025
|
+
const id = parseInt(c.req.param("id"), 10);
|
|
1026
|
+
await approveUser(id);
|
|
1027
|
+
return c.json({ ok: true });
|
|
1028
|
+
});
|
|
1029
|
+
var app2 = new Hono2().post("/register", zValidator("json", credentialsSchema), async (c) => {
|
|
1030
|
+
const { username, password } = c.req.valid("json");
|
|
1031
|
+
const existing = await getUserByUsername(username);
|
|
1032
|
+
if (existing) {
|
|
1033
|
+
return c.json({ error: "Username already taken" }, 409);
|
|
1034
|
+
}
|
|
1035
|
+
const user = await createUser(username, password);
|
|
1036
|
+
if (user.role === "admin") {
|
|
1037
|
+
const sessionId = createSession(user.id);
|
|
1038
|
+
setCookie(c, "volute_session", sessionId, { path: "/", httpOnly: true, sameSite: "Lax" });
|
|
1039
|
+
}
|
|
1040
|
+
return c.json({ id: user.id, username: user.username, role: user.role });
|
|
1041
|
+
}).post("/login", zValidator("json", credentialsSchema), async (c) => {
|
|
1042
|
+
const { username, password } = c.req.valid("json");
|
|
1043
|
+
const user = await verifyUser(username, password);
|
|
1044
|
+
if (!user) {
|
|
1045
|
+
return c.json({ error: "Invalid credentials" }, 401);
|
|
1046
|
+
}
|
|
1047
|
+
const sessionId = createSession(user.id);
|
|
1048
|
+
setCookie(c, "volute_session", sessionId, { path: "/", httpOnly: true, sameSite: "Lax" });
|
|
1049
|
+
return c.json({ id: user.id, username: user.username, role: user.role });
|
|
1050
|
+
}).post("/logout", (c) => {
|
|
1051
|
+
const sessionId = getCookie2(c, "volute_session");
|
|
1052
|
+
if (sessionId) {
|
|
1053
|
+
deleteSession(sessionId);
|
|
1054
|
+
deleteCookie(c, "volute_session", { path: "/" });
|
|
1055
|
+
}
|
|
1056
|
+
return c.json({ ok: true });
|
|
1057
|
+
}).get("/me", async (c) => {
|
|
1058
|
+
const sessionId = getCookie2(c, "volute_session");
|
|
1059
|
+
if (!sessionId) return c.json({ error: "Not logged in" }, 401);
|
|
1060
|
+
const userId = getSessionUserId(sessionId);
|
|
1061
|
+
if (userId == null) return c.json({ error: "Not logged in" }, 401);
|
|
1062
|
+
const user = await getUser(userId);
|
|
1063
|
+
if (!user) return c.json({ error: "Not logged in" }, 401);
|
|
1064
|
+
return c.json({ id: user.id, username: user.username, role: user.role });
|
|
1065
|
+
}).route("/", admin);
|
|
1066
|
+
var auth_default = app2;
|
|
1067
|
+
|
|
1068
|
+
// src/web/routes/chat.ts
|
|
1069
|
+
import { zValidator as zValidator2 } from "@hono/zod-validator";
|
|
1070
|
+
import { Hono as Hono3 } from "hono";
|
|
1071
|
+
import { streamSSE } from "hono/streaming";
|
|
1072
|
+
import { z as z2 } from "zod";
|
|
1073
|
+
|
|
1074
|
+
// src/lib/conversations.ts
|
|
1075
|
+
import { randomUUID } from "crypto";
|
|
1076
|
+
import { and as and3, desc as desc2, eq as eq3, isNull, sql as sql2 } from "drizzle-orm";
|
|
1077
|
+
async function createConversation(agentName, channel, opts) {
|
|
1078
|
+
const db2 = await getDb();
|
|
1079
|
+
const id = randomUUID();
|
|
1080
|
+
await db2.insert(conversations).values({
|
|
1081
|
+
id,
|
|
1082
|
+
agent_name: agentName,
|
|
1083
|
+
channel,
|
|
1084
|
+
user_id: opts?.userId ?? null,
|
|
1085
|
+
title: opts?.title ?? null
|
|
1086
|
+
});
|
|
1087
|
+
return {
|
|
1088
|
+
id,
|
|
1089
|
+
agent_name: agentName,
|
|
1090
|
+
channel,
|
|
1091
|
+
user_id: opts?.userId ?? null,
|
|
1092
|
+
title: opts?.title ?? null,
|
|
1093
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1094
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
async function getConversation(id) {
|
|
1098
|
+
const db2 = await getDb();
|
|
1099
|
+
const row = await db2.select().from(conversations).where(eq3(conversations.id, id)).get();
|
|
1100
|
+
return row ?? null;
|
|
1101
|
+
}
|
|
1102
|
+
async function listConversations(agentName, opts) {
|
|
1103
|
+
const db2 = await getDb();
|
|
1104
|
+
if (opts?.userId != null) {
|
|
1105
|
+
return db2.select().from(conversations).where(and3(eq3(conversations.agent_name, agentName), eq3(conversations.user_id, opts.userId))).orderBy(desc2(conversations.updated_at)).all();
|
|
1106
|
+
}
|
|
1107
|
+
return db2.select().from(conversations).where(eq3(conversations.agent_name, agentName)).orderBy(desc2(conversations.updated_at)).all();
|
|
1108
|
+
}
|
|
1109
|
+
async function addMessage(conversationId, role, senderName, content) {
|
|
1110
|
+
const db2 = await getDb();
|
|
1111
|
+
const serialized = JSON.stringify(content);
|
|
1112
|
+
const [result] = await db2.insert(messages).values({ conversation_id: conversationId, role, sender_name: senderName, content: serialized }).returning({ id: messages.id, created_at: messages.created_at });
|
|
1113
|
+
await db2.update(conversations).set({ updated_at: sql2`datetime('now')` }).where(eq3(conversations.id, conversationId));
|
|
1114
|
+
if (role === "user") {
|
|
1115
|
+
const firstText = content.find((b) => b.type === "text");
|
|
1116
|
+
const title = firstText ? firstText.text.slice(0, 80) : "";
|
|
1117
|
+
if (title) {
|
|
1118
|
+
await db2.update(conversations).set({ title }).where(and3(eq3(conversations.id, conversationId), isNull(conversations.title)));
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
return {
|
|
1122
|
+
id: result.id,
|
|
1123
|
+
conversation_id: conversationId,
|
|
1124
|
+
role,
|
|
1125
|
+
sender_name: senderName,
|
|
1126
|
+
content,
|
|
1127
|
+
created_at: result.created_at
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
async function getMessages(conversationId) {
|
|
1131
|
+
const db2 = await getDb();
|
|
1132
|
+
const rows = await db2.select().from(messages).where(eq3(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
|
|
1133
|
+
return rows.map((row) => {
|
|
1134
|
+
let content;
|
|
1135
|
+
try {
|
|
1136
|
+
const parsed = JSON.parse(row.content);
|
|
1137
|
+
content = Array.isArray(parsed) ? parsed : [{ type: "text", text: row.content }];
|
|
1138
|
+
} catch {
|
|
1139
|
+
content = [{ type: "text", text: row.content }];
|
|
1140
|
+
}
|
|
1141
|
+
return { ...row, content };
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
async function deleteConversation(id) {
|
|
1145
|
+
const db2 = await getDb();
|
|
1146
|
+
await db2.delete(conversations).where(eq3(conversations.id, id));
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// src/web/routes/chat.ts
|
|
1150
|
+
var chatSchema = z2.object({
|
|
1151
|
+
message: z2.string().optional(),
|
|
1152
|
+
conversationId: z2.string().optional(),
|
|
1153
|
+
images: z2.array(
|
|
1154
|
+
z2.object({
|
|
1155
|
+
media_type: z2.string(),
|
|
1156
|
+
data: z2.string()
|
|
1157
|
+
})
|
|
1158
|
+
).optional()
|
|
1159
|
+
});
|
|
1160
|
+
var app3 = new Hono3().post("/:name/chat", zValidator2("json", chatSchema), async (c) => {
|
|
1161
|
+
const name = c.req.param("name");
|
|
1162
|
+
const entry = findAgent(name);
|
|
1163
|
+
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1164
|
+
const body = c.req.valid("json");
|
|
1165
|
+
if (!body.message && (!body.images || body.images.length === 0)) {
|
|
1166
|
+
return c.json({ error: "message or images required" }, 400);
|
|
1167
|
+
}
|
|
1168
|
+
const user = c.get("user");
|
|
1169
|
+
let conversationId = body.conversationId;
|
|
1170
|
+
if (conversationId) {
|
|
1171
|
+
const conv = await getConversation(conversationId);
|
|
1172
|
+
if (!conv) return c.json({ error: "Conversation not found" }, 404);
|
|
1173
|
+
} else {
|
|
1174
|
+
const title = body.message ? body.message.slice(0, 80) : "Image message";
|
|
1175
|
+
const conv = await createConversation(name, "web", {
|
|
1176
|
+
userId: user.id,
|
|
1177
|
+
title
|
|
1178
|
+
});
|
|
1179
|
+
conversationId = conv.id;
|
|
1180
|
+
}
|
|
1181
|
+
const userContent = [];
|
|
1182
|
+
if (body.message) {
|
|
1183
|
+
userContent.push({ type: "text", text: body.message });
|
|
1184
|
+
}
|
|
1185
|
+
if (body.images) {
|
|
1186
|
+
for (const img of body.images) {
|
|
1187
|
+
userContent.push({ type: "image", media_type: img.media_type, data: img.data });
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
await addMessage(conversationId, "user", user.username, userContent);
|
|
1191
|
+
const userText = body.message ?? "[image]";
|
|
1192
|
+
const db2 = await getDb();
|
|
1193
|
+
await db2.insert(agentMessages).values({
|
|
1194
|
+
agent: name,
|
|
1195
|
+
channel: "web",
|
|
1196
|
+
role: "user",
|
|
1197
|
+
sender: user.username,
|
|
1198
|
+
content: userText
|
|
1199
|
+
});
|
|
1200
|
+
const agentContent = [];
|
|
1201
|
+
if (body.message) {
|
|
1202
|
+
agentContent.push({ type: "text", text: body.message });
|
|
1203
|
+
}
|
|
1204
|
+
if (body.images) {
|
|
1205
|
+
for (const img of body.images) {
|
|
1206
|
+
agentContent.push({ type: "image", media_type: img.media_type, data: img.data });
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
const res = await fetch(`http://localhost:${entry.port}/message`, {
|
|
1210
|
+
method: "POST",
|
|
1211
|
+
headers: { "Content-Type": "application/json" },
|
|
1212
|
+
body: JSON.stringify({
|
|
1213
|
+
content: agentContent,
|
|
1214
|
+
channel: "web",
|
|
1215
|
+
sender: user.username
|
|
1216
|
+
})
|
|
1217
|
+
});
|
|
1218
|
+
if (!res.ok) {
|
|
1219
|
+
return c.json({ error: `Agent responded with ${res.status}` }, res.status);
|
|
1220
|
+
}
|
|
1221
|
+
if (!res.body) {
|
|
1222
|
+
return c.json({ error: "No response body from agent" }, 502);
|
|
1223
|
+
}
|
|
1224
|
+
return streamSSE(c, async (stream2) => {
|
|
1225
|
+
await stream2.writeSSE({
|
|
1226
|
+
data: JSON.stringify({ type: "meta", conversationId })
|
|
1227
|
+
});
|
|
1228
|
+
const assistantContent = [];
|
|
1229
|
+
for await (const event of readNdjson(res.body)) {
|
|
1230
|
+
const voluteEvent = event;
|
|
1231
|
+
await stream2.writeSSE({ data: JSON.stringify(voluteEvent) });
|
|
1232
|
+
if (voluteEvent.type === "text") {
|
|
1233
|
+
const last = assistantContent[assistantContent.length - 1];
|
|
1234
|
+
if (last && last.type === "text") {
|
|
1235
|
+
last.text += voluteEvent.content;
|
|
1236
|
+
} else {
|
|
1237
|
+
assistantContent.push({ type: "text", text: voluteEvent.content });
|
|
1238
|
+
}
|
|
1239
|
+
} else if (voluteEvent.type === "tool_use") {
|
|
1240
|
+
assistantContent.push({
|
|
1241
|
+
type: "tool_use",
|
|
1242
|
+
name: voluteEvent.name,
|
|
1243
|
+
input: voluteEvent.input
|
|
1244
|
+
});
|
|
1245
|
+
} else if (voluteEvent.type === "tool_result") {
|
|
1246
|
+
assistantContent.push({
|
|
1247
|
+
type: "tool_result",
|
|
1248
|
+
output: voluteEvent.output,
|
|
1249
|
+
...voluteEvent.is_error ? { is_error: true } : {}
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
if (voluteEvent.type === "done") {
|
|
1253
|
+
if (assistantContent.length > 0) {
|
|
1254
|
+
await addMessage(conversationId, "assistant", name, assistantContent);
|
|
1255
|
+
const textParts = assistantContent.filter((b) => b.type === "text").map((b) => b.text);
|
|
1256
|
+
if (textParts.length > 0) {
|
|
1257
|
+
const db3 = await getDb();
|
|
1258
|
+
await db3.insert(agentMessages).values({
|
|
1259
|
+
agent: name,
|
|
1260
|
+
channel: "web",
|
|
1261
|
+
role: "assistant",
|
|
1262
|
+
content: textParts.join("")
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
break;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
});
|
|
1270
|
+
});
|
|
1271
|
+
var chat_default = app3;
|
|
1272
|
+
|
|
1273
|
+
// src/web/routes/connectors.ts
|
|
1274
|
+
import { Hono as Hono4 } from "hono";
|
|
1275
|
+
var app4 = new Hono4().get("/:name/connectors", (c) => {
|
|
1276
|
+
const name = c.req.param("name");
|
|
1277
|
+
const entry = findAgent(name);
|
|
1278
|
+
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1279
|
+
const dir = agentDir(name);
|
|
1280
|
+
const config = readVoluteConfig(dir);
|
|
1281
|
+
const configured = config.connectors ?? [];
|
|
1282
|
+
const manager = getConnectorManager();
|
|
1283
|
+
const runningStatus = manager.getConnectorStatus(name);
|
|
1284
|
+
const connectors = configured.map((type) => {
|
|
1285
|
+
const status = runningStatus.find((s) => s.type === type);
|
|
1286
|
+
return { type, running: status?.running ?? false };
|
|
1287
|
+
});
|
|
1288
|
+
return c.json(connectors);
|
|
1289
|
+
}).post("/:name/connectors/:type", async (c) => {
|
|
1290
|
+
const name = c.req.param("name");
|
|
1291
|
+
const type = c.req.param("type");
|
|
1292
|
+
const entry = findAgent(name);
|
|
1293
|
+
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1294
|
+
const dir = agentDir(name);
|
|
1295
|
+
const config = readVoluteConfig(dir);
|
|
1296
|
+
const connectors = config.connectors ?? [];
|
|
1297
|
+
if (!connectors.includes(type)) {
|
|
1298
|
+
config.connectors = [...connectors, type];
|
|
1299
|
+
writeVoluteConfig(dir, config);
|
|
1300
|
+
}
|
|
1301
|
+
const manager = getConnectorManager();
|
|
1302
|
+
try {
|
|
1303
|
+
await manager.startConnector(name, dir, entry.port, type);
|
|
1304
|
+
return c.json({ ok: true });
|
|
1305
|
+
} catch (err) {
|
|
1306
|
+
return c.json(
|
|
1307
|
+
{ error: err instanceof Error ? err.message : "Failed to start connector" },
|
|
1308
|
+
500
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
}).delete("/:name/connectors/:type", async (c) => {
|
|
1312
|
+
const name = c.req.param("name");
|
|
1313
|
+
const type = c.req.param("type");
|
|
1314
|
+
const entry = findAgent(name);
|
|
1315
|
+
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1316
|
+
const dir = agentDir(name);
|
|
1317
|
+
const manager = getConnectorManager();
|
|
1318
|
+
await manager.stopConnector(name, type);
|
|
1319
|
+
const config = readVoluteConfig(dir);
|
|
1320
|
+
config.connectors = (config.connectors ?? []).filter((t) => t !== type);
|
|
1321
|
+
writeVoluteConfig(dir, config);
|
|
1322
|
+
return c.json({ ok: true });
|
|
1323
|
+
});
|
|
1324
|
+
var connectors_default = app4;
|
|
1325
|
+
|
|
1326
|
+
// src/web/routes/conversations.ts
|
|
1327
|
+
import { Hono as Hono5 } from "hono";
|
|
1328
|
+
var app5 = new Hono5().get("/:name/conversations", async (c) => {
|
|
1329
|
+
const name = c.req.param("name");
|
|
1330
|
+
const user = c.get("user");
|
|
1331
|
+
const convs = await listConversations(name, { userId: user.id });
|
|
1332
|
+
return c.json(convs);
|
|
1333
|
+
}).get("/:name/conversations/:id/messages", async (c) => {
|
|
1334
|
+
const id = c.req.param("id");
|
|
1335
|
+
const msgs = await getMessages(id);
|
|
1336
|
+
return c.json(msgs);
|
|
1337
|
+
}).delete("/:name/conversations/:id", async (c) => {
|
|
1338
|
+
const id = c.req.param("id");
|
|
1339
|
+
await deleteConversation(id);
|
|
1340
|
+
return c.json({ ok: true });
|
|
1341
|
+
});
|
|
1342
|
+
var conversations_default = app5;
|
|
1343
|
+
|
|
1344
|
+
// src/web/routes/files.ts
|
|
1345
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1346
|
+
import { readdir, readFile, writeFile } from "fs/promises";
|
|
1347
|
+
import { resolve as resolve5 } from "path";
|
|
1348
|
+
import { zValidator as zValidator3 } from "@hono/zod-validator";
|
|
1349
|
+
import { Hono as Hono6 } from "hono";
|
|
1350
|
+
import { z as z3 } from "zod";
|
|
1351
|
+
var ALLOWED_FILES = /* @__PURE__ */ new Set(["SOUL.md", "MEMORY.md", "CLAUDE.md", "VOLUTE.md"]);
|
|
1352
|
+
var saveFileSchema = z3.object({ content: z3.string() });
|
|
1353
|
+
var app6 = new Hono6().get("/:name/files", async (c) => {
|
|
1354
|
+
const name = c.req.param("name");
|
|
1355
|
+
const entry = findAgent(name);
|
|
1356
|
+
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1357
|
+
const dir = agentDir(name);
|
|
1358
|
+
const homeDir = resolve5(dir, "home");
|
|
1359
|
+
if (!existsSync6(homeDir)) return c.json({ error: "Home directory missing" }, 404);
|
|
1360
|
+
const allFiles = await readdir(homeDir);
|
|
1361
|
+
const files = allFiles.filter((f) => f.endsWith(".md") && ALLOWED_FILES.has(f));
|
|
1362
|
+
return c.json(files);
|
|
1363
|
+
}).get("/:name/files/:filename", async (c) => {
|
|
1364
|
+
const name = c.req.param("name");
|
|
1365
|
+
const filename = c.req.param("filename");
|
|
1366
|
+
if (!ALLOWED_FILES.has(filename)) {
|
|
1367
|
+
return c.json({ error: "File not allowed" }, 403);
|
|
1368
|
+
}
|
|
1369
|
+
const entry = findAgent(name);
|
|
1370
|
+
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1371
|
+
const dir = agentDir(name);
|
|
1372
|
+
const filePath = resolve5(dir, "home", filename);
|
|
1373
|
+
if (!existsSync6(filePath)) {
|
|
1374
|
+
return c.json({ error: "File not found" }, 404);
|
|
1375
|
+
}
|
|
1376
|
+
const content = await readFile(filePath, "utf-8");
|
|
1377
|
+
return c.json({ filename, content });
|
|
1378
|
+
}).put("/:name/files/:filename", zValidator3("json", saveFileSchema), async (c) => {
|
|
1379
|
+
const name = c.req.param("name");
|
|
1380
|
+
const filename = c.req.param("filename");
|
|
1381
|
+
if (!ALLOWED_FILES.has(filename)) {
|
|
1382
|
+
return c.json({ error: "File not allowed" }, 403);
|
|
1383
|
+
}
|
|
1384
|
+
const entry = findAgent(name);
|
|
1385
|
+
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1386
|
+
const dir = agentDir(name);
|
|
1387
|
+
const filePath = resolve5(dir, "home", filename);
|
|
1388
|
+
const { content } = c.req.valid("json");
|
|
1389
|
+
await writeFile(filePath, content);
|
|
1390
|
+
return c.json({ ok: true });
|
|
1391
|
+
});
|
|
1392
|
+
var files_default = app6;
|
|
1393
|
+
|
|
1394
|
+
// src/web/routes/logs.ts
|
|
1395
|
+
import { spawn as spawn3 } from "child_process";
|
|
1396
|
+
import { existsSync as existsSync7 } from "fs";
|
|
1397
|
+
import { resolve as resolve6 } from "path";
|
|
1398
|
+
import { Hono as Hono7 } from "hono";
|
|
1399
|
+
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
1400
|
+
var app7 = new Hono7().get("/:name/logs", async (c) => {
|
|
1401
|
+
const name = c.req.param("name");
|
|
1402
|
+
const entry = findAgent(name);
|
|
1403
|
+
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1404
|
+
const dir = agentDir(name);
|
|
1405
|
+
const logFile = resolve6(dir, ".volute", "logs", "agent.log");
|
|
1406
|
+
if (!existsSync7(logFile)) {
|
|
1407
|
+
return c.json({ error: "No log file found" }, 404);
|
|
1408
|
+
}
|
|
1409
|
+
return streamSSE2(c, async (stream2) => {
|
|
1410
|
+
const tail = spawn3("tail", ["-n", "200", "-f", logFile]);
|
|
1411
|
+
const onData = (data) => {
|
|
1412
|
+
const lines = data.toString().split("\n");
|
|
1413
|
+
for (const line of lines) {
|
|
1414
|
+
if (line) {
|
|
1415
|
+
stream2.writeSSE({ data: line }).catch(() => {
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
};
|
|
1420
|
+
tail.stdout.on("data", onData);
|
|
1421
|
+
stream2.onAbort(() => {
|
|
1422
|
+
tail.kill();
|
|
1423
|
+
});
|
|
1424
|
+
await new Promise((resolve9) => {
|
|
1425
|
+
tail.on("exit", resolve9);
|
|
1426
|
+
stream2.onAbort(resolve9);
|
|
1427
|
+
});
|
|
1428
|
+
});
|
|
1429
|
+
});
|
|
1430
|
+
var logs_default = app7;
|
|
1431
|
+
|
|
1432
|
+
// src/web/routes/schedules.ts
|
|
1433
|
+
import { Hono as Hono8 } from "hono";
|
|
1434
|
+
function readSchedules(name) {
|
|
1435
|
+
return readVoluteConfig(agentDir(name)).schedules ?? [];
|
|
1436
|
+
}
|
|
1437
|
+
function writeSchedules(name, schedules) {
|
|
1438
|
+
const dir = agentDir(name);
|
|
1439
|
+
const config = readVoluteConfig(dir);
|
|
1440
|
+
config.schedules = schedules.length > 0 ? schedules : void 0;
|
|
1441
|
+
writeVoluteConfig(dir, config);
|
|
1442
|
+
getScheduler().loadSchedules(name);
|
|
1443
|
+
}
|
|
1444
|
+
var app8 = new Hono8().get("/:name/schedules", (c) => {
|
|
1445
|
+
const name = c.req.param("name");
|
|
1446
|
+
if (!findAgent(name)) return c.json({ error: "Agent not found" }, 404);
|
|
1447
|
+
return c.json(readSchedules(name));
|
|
1448
|
+
}).post("/:name/schedules", async (c) => {
|
|
1449
|
+
const name = c.req.param("name");
|
|
1450
|
+
if (!findAgent(name)) return c.json({ error: "Agent not found" }, 404);
|
|
1451
|
+
const body = await c.req.json();
|
|
1452
|
+
if (!body.cron || !body.message) {
|
|
1453
|
+
return c.json({ error: "cron and message are required" }, 400);
|
|
1454
|
+
}
|
|
1455
|
+
const schedules = readSchedules(name);
|
|
1456
|
+
const id = body.id || `schedule-${Date.now()}`;
|
|
1457
|
+
if (schedules.some((s) => s.id === id)) {
|
|
1458
|
+
return c.json({ error: `Schedule "${id}" already exists` }, 409);
|
|
1459
|
+
}
|
|
1460
|
+
schedules.push({ id, cron: body.cron, message: body.message, enabled: body.enabled ?? true });
|
|
1461
|
+
writeSchedules(name, schedules);
|
|
1462
|
+
return c.json({ ok: true, id }, 201);
|
|
1463
|
+
}).put("/:name/schedules/:id", async (c) => {
|
|
1464
|
+
const name = c.req.param("name");
|
|
1465
|
+
const id = c.req.param("id");
|
|
1466
|
+
if (!findAgent(name)) return c.json({ error: "Agent not found" }, 404);
|
|
1467
|
+
const schedules = readSchedules(name);
|
|
1468
|
+
const idx = schedules.findIndex((s) => s.id === id);
|
|
1469
|
+
if (idx === -1) return c.json({ error: "Schedule not found" }, 404);
|
|
1470
|
+
const body = await c.req.json();
|
|
1471
|
+
if (body.cron !== void 0) schedules[idx].cron = body.cron;
|
|
1472
|
+
if (body.message !== void 0) schedules[idx].message = body.message;
|
|
1473
|
+
if (body.enabled !== void 0) schedules[idx].enabled = body.enabled;
|
|
1474
|
+
writeSchedules(name, schedules);
|
|
1475
|
+
return c.json({ ok: true });
|
|
1476
|
+
}).delete("/:name/schedules/:id", (c) => {
|
|
1477
|
+
const name = c.req.param("name");
|
|
1478
|
+
const id = c.req.param("id");
|
|
1479
|
+
if (!findAgent(name)) return c.json({ error: "Agent not found" }, 404);
|
|
1480
|
+
const schedules = readSchedules(name);
|
|
1481
|
+
const filtered = schedules.filter((s) => s.id !== id);
|
|
1482
|
+
if (filtered.length === schedules.length) {
|
|
1483
|
+
return c.json({ error: "Schedule not found" }, 404);
|
|
1484
|
+
}
|
|
1485
|
+
writeSchedules(name, filtered);
|
|
1486
|
+
return c.json({ ok: true });
|
|
1487
|
+
}).post("/:name/webhook/:event", async (c) => {
|
|
1488
|
+
const name = c.req.param("name");
|
|
1489
|
+
const event = c.req.param("event");
|
|
1490
|
+
const entry = findAgent(name);
|
|
1491
|
+
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1492
|
+
const body = await c.req.text();
|
|
1493
|
+
const message = `[webhook: ${event}] ${body}`;
|
|
1494
|
+
try {
|
|
1495
|
+
const res = await fetch(`http://localhost:${entry.port}/message`, {
|
|
1496
|
+
method: "POST",
|
|
1497
|
+
headers: { "Content-Type": "application/json" },
|
|
1498
|
+
body: JSON.stringify({
|
|
1499
|
+
content: [{ type: "text", text: message }],
|
|
1500
|
+
channel: "system:webhook",
|
|
1501
|
+
sender: "webhook"
|
|
1502
|
+
})
|
|
1503
|
+
});
|
|
1504
|
+
if (!res.ok) {
|
|
1505
|
+
return c.json({ error: `Agent responded with ${res.status}` }, 502);
|
|
1506
|
+
}
|
|
1507
|
+
return c.json({ ok: true });
|
|
1508
|
+
} catch {
|
|
1509
|
+
return c.json({ error: "Failed to reach agent" }, 502);
|
|
1510
|
+
}
|
|
1511
|
+
});
|
|
1512
|
+
var schedules_default = app8;
|
|
1513
|
+
|
|
1514
|
+
// src/web/routes/system.ts
|
|
1515
|
+
import { Hono as Hono9 } from "hono";
|
|
1516
|
+
import { streamSSE as streamSSE3 } from "hono/streaming";
|
|
1517
|
+
var app9 = new Hono9().get("/logs", async (c) => {
|
|
1518
|
+
const user = c.get("user");
|
|
1519
|
+
if (user.role !== "admin") return c.json({ error: "Forbidden" }, 403);
|
|
1520
|
+
return streamSSE3(c, async (stream2) => {
|
|
1521
|
+
for (const entry of logBuffer.getEntries()) {
|
|
1522
|
+
await stream2.writeSSE({ data: JSON.stringify(entry) });
|
|
1523
|
+
}
|
|
1524
|
+
const unsubscribe = logBuffer.subscribe((entry) => {
|
|
1525
|
+
stream2.writeSSE({ data: JSON.stringify(entry) }).catch(() => {
|
|
1526
|
+
});
|
|
1527
|
+
});
|
|
1528
|
+
await new Promise((resolve9) => {
|
|
1529
|
+
stream2.onAbort(() => {
|
|
1530
|
+
unsubscribe();
|
|
1531
|
+
resolve9();
|
|
1532
|
+
});
|
|
1533
|
+
});
|
|
1534
|
+
});
|
|
1535
|
+
});
|
|
1536
|
+
var system_default = app9;
|
|
1537
|
+
|
|
1538
|
+
// src/web/routes/variants.ts
|
|
1539
|
+
import { Hono as Hono10 } from "hono";
|
|
1540
|
+
var app10 = new Hono10().get("/:name/variants", async (c) => {
|
|
1541
|
+
const name = c.req.param("name");
|
|
1542
|
+
const entry = findAgent(name);
|
|
1543
|
+
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1544
|
+
const variants = readVariants(name);
|
|
1545
|
+
const results = await Promise.all(
|
|
1546
|
+
variants.map(async (v) => {
|
|
1547
|
+
if (!v.port) return { ...v, status: "no-server" };
|
|
1548
|
+
const health = await checkHealth(v.port);
|
|
1549
|
+
return { ...v, status: health.ok ? "running" : "dead" };
|
|
1550
|
+
})
|
|
1551
|
+
);
|
|
1552
|
+
return c.json(results);
|
|
1553
|
+
});
|
|
1554
|
+
var variants_default = app10;
|
|
1555
|
+
|
|
1556
|
+
// src/web/app.ts
|
|
1557
|
+
var app11 = new Hono11();
|
|
1558
|
+
app11.onError((err, c) => {
|
|
1559
|
+
if (err instanceof HTTPException) {
|
|
1560
|
+
return err.getResponse();
|
|
1561
|
+
}
|
|
1562
|
+
logger_default.error("Unhandled error", {
|
|
1563
|
+
path: c.req.path,
|
|
1564
|
+
method: c.req.method,
|
|
1565
|
+
error: err.message
|
|
1566
|
+
});
|
|
1567
|
+
return c.json({ error: "Internal server error" }, 500);
|
|
1568
|
+
});
|
|
1569
|
+
app11.notFound((c) => {
|
|
1570
|
+
return c.json({ error: "Not found" }, 404);
|
|
1571
|
+
});
|
|
1572
|
+
app11.use("*", async (c, next) => {
|
|
1573
|
+
const start = Date.now();
|
|
1574
|
+
await next();
|
|
1575
|
+
const duration = Date.now() - start;
|
|
1576
|
+
logger_default.info("request", {
|
|
1577
|
+
method: c.req.method,
|
|
1578
|
+
path: c.req.path,
|
|
1579
|
+
status: c.res.status,
|
|
1580
|
+
duration
|
|
1581
|
+
});
|
|
1582
|
+
});
|
|
1583
|
+
app11.get("/api/health", (c) => {
|
|
1584
|
+
return c.json({ ok: true, version: "0.1.0" });
|
|
1585
|
+
});
|
|
1586
|
+
app11.use("/api/*", csrf());
|
|
1587
|
+
app11.use("/api/agents/*", authMiddleware);
|
|
1588
|
+
app11.use("/api/system/*", authMiddleware);
|
|
1589
|
+
var routes = app11.route("/api/auth", auth_default).route("/api/system", system_default).route("/api/agents", agents_default).route("/api/agents", chat_default).route("/api/agents", connectors_default).route("/api/agents", schedules_default).route("/api/agents", logs_default).route("/api/agents", variants_default).route("/api/agents", files_default).route("/api/agents", conversations_default);
|
|
1590
|
+
var app_default = app11;
|
|
1591
|
+
|
|
1592
|
+
// src/web/server.ts
|
|
1593
|
+
var MIME_TYPES = {
|
|
1594
|
+
".html": "text/html",
|
|
1595
|
+
".js": "application/javascript",
|
|
1596
|
+
".css": "text/css",
|
|
1597
|
+
".json": "application/json",
|
|
1598
|
+
".svg": "image/svg+xml",
|
|
1599
|
+
".png": "image/png",
|
|
1600
|
+
".ico": "image/x-icon"
|
|
1601
|
+
};
|
|
1602
|
+
async function startServer({ port }) {
|
|
1603
|
+
let assetsDir = "";
|
|
1604
|
+
let searchDir = dirname3(new URL(import.meta.url).pathname);
|
|
1605
|
+
for (let i = 0; i < 5; i++) {
|
|
1606
|
+
const candidate = resolve7(searchDir, "dist", "web-assets");
|
|
1607
|
+
if (existsSync8(candidate)) {
|
|
1608
|
+
assetsDir = candidate;
|
|
1609
|
+
break;
|
|
1610
|
+
}
|
|
1611
|
+
searchDir = dirname3(searchDir);
|
|
1612
|
+
}
|
|
1613
|
+
if (assetsDir) {
|
|
1614
|
+
app_default.get("*", async (c) => {
|
|
1615
|
+
const urlPath = new URL(c.req.url).pathname;
|
|
1616
|
+
const filePath = resolve7(assetsDir, urlPath.slice(1));
|
|
1617
|
+
if (!filePath.startsWith(assetsDir)) return c.text("Forbidden", 403);
|
|
1618
|
+
const s = await stat(filePath).catch(() => null);
|
|
1619
|
+
if (s?.isFile()) {
|
|
1620
|
+
const ext = extname(filePath);
|
|
1621
|
+
const mime = MIME_TYPES[ext] || "application/octet-stream";
|
|
1622
|
+
const body = await readFile2(filePath);
|
|
1623
|
+
return c.body(body, 200, { "Content-Type": mime });
|
|
1624
|
+
}
|
|
1625
|
+
const indexPath = resolve7(assetsDir, "index.html");
|
|
1626
|
+
const indexStat = await stat(indexPath).catch(() => null);
|
|
1627
|
+
if (indexStat?.isFile()) {
|
|
1628
|
+
const body = await readFile2(indexPath, "utf-8");
|
|
1629
|
+
return c.html(body);
|
|
1630
|
+
}
|
|
1631
|
+
return c.text("Not found", 404);
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
const server = serve({ fetch: app_default.fetch, port });
|
|
1635
|
+
await new Promise((resolve9, reject) => {
|
|
1636
|
+
server.on("listening", () => {
|
|
1637
|
+
logger_default.info("Volute UI running", { port });
|
|
1638
|
+
resolve9();
|
|
1639
|
+
});
|
|
1640
|
+
server.on("error", (err) => {
|
|
1641
|
+
reject(err);
|
|
1642
|
+
});
|
|
1643
|
+
});
|
|
1644
|
+
return server;
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
// src/daemon.ts
|
|
1648
|
+
var DAEMON_PID_PATH = resolve8(VOLUTE_HOME, "daemon.pid");
|
|
1649
|
+
var DAEMON_JSON_PATH = resolve8(VOLUTE_HOME, "daemon.json");
|
|
1650
|
+
async function startDaemon(opts) {
|
|
1651
|
+
const { port } = opts;
|
|
1652
|
+
const myPid = String(process.pid);
|
|
1653
|
+
mkdirSync3(VOLUTE_HOME, { recursive: true });
|
|
1654
|
+
const token = process.env.VOLUTE_DAEMON_TOKEN || randomBytes(32).toString("hex");
|
|
1655
|
+
process.env.VOLUTE_DAEMON_TOKEN = token;
|
|
1656
|
+
let server;
|
|
1657
|
+
try {
|
|
1658
|
+
server = await startServer({ port });
|
|
1659
|
+
} catch (err) {
|
|
1660
|
+
const e = err;
|
|
1661
|
+
if (e.code === "EADDRINUSE") {
|
|
1662
|
+
console.error(`[daemon] port ${port} is already in use`);
|
|
1663
|
+
process.exit(1);
|
|
1664
|
+
}
|
|
1665
|
+
throw err;
|
|
1666
|
+
}
|
|
1667
|
+
writeFileSync3(DAEMON_PID_PATH, myPid);
|
|
1668
|
+
writeFileSync3(DAEMON_JSON_PATH, `${JSON.stringify({ port, token }, null, 2)}
|
|
1669
|
+
`);
|
|
1670
|
+
const manager = initAgentManager();
|
|
1671
|
+
const connectors = initConnectorManager();
|
|
1672
|
+
const scheduler = getScheduler();
|
|
1673
|
+
scheduler.start();
|
|
1674
|
+
const registry = readRegistry();
|
|
1675
|
+
for (const entry of registry) {
|
|
1676
|
+
if (!entry.running) continue;
|
|
1677
|
+
try {
|
|
1678
|
+
await manager.startAgent(entry.name);
|
|
1679
|
+
const dir = agentDir(entry.name);
|
|
1680
|
+
await connectors.startConnectors(entry.name, dir, entry.port);
|
|
1681
|
+
scheduler.loadSchedules(entry.name);
|
|
1682
|
+
} catch (err) {
|
|
1683
|
+
console.error(`[daemon] failed to start agent ${entry.name}:`, err);
|
|
1684
|
+
setAgentRunning(entry.name, false);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
console.error(`[daemon] running on port ${port}, pid ${myPid}`);
|
|
1688
|
+
function cleanup() {
|
|
1689
|
+
try {
|
|
1690
|
+
if (readFileSync4(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
|
|
1691
|
+
unlinkSync3(DAEMON_PID_PATH);
|
|
1692
|
+
}
|
|
1693
|
+
} catch {
|
|
1694
|
+
}
|
|
1695
|
+
try {
|
|
1696
|
+
unlinkSync3(DAEMON_JSON_PATH);
|
|
1697
|
+
} catch {
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
let shuttingDown = false;
|
|
1701
|
+
async function shutdown() {
|
|
1702
|
+
if (shuttingDown) return;
|
|
1703
|
+
shuttingDown = true;
|
|
1704
|
+
console.error("[daemon] shutting down...");
|
|
1705
|
+
scheduler.stop();
|
|
1706
|
+
await connectors.stopAll();
|
|
1707
|
+
await manager.stopAll();
|
|
1708
|
+
server.close();
|
|
1709
|
+
cleanup();
|
|
1710
|
+
process.exit(0);
|
|
1711
|
+
}
|
|
1712
|
+
process.on("SIGINT", shutdown);
|
|
1713
|
+
process.on("SIGTERM", shutdown);
|
|
1714
|
+
process.on("exit", cleanup);
|
|
1715
|
+
}
|
|
1716
|
+
if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("daemon.ts")) {
|
|
1717
|
+
let port = 4200;
|
|
1718
|
+
let foreground = false;
|
|
1719
|
+
for (let i = 2; i < process.argv.length; i++) {
|
|
1720
|
+
if (process.argv[i] === "--port" && process.argv[i + 1]) {
|
|
1721
|
+
port = parseInt(process.argv[i + 1], 10);
|
|
1722
|
+
i++;
|
|
1723
|
+
} else if (process.argv[i] === "--foreground") {
|
|
1724
|
+
foreground = true;
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
startDaemon({ port, foreground });
|
|
1728
|
+
}
|
|
1729
|
+
export {
|
|
1730
|
+
startDaemon
|
|
1731
|
+
};
|