volute 0.1.0 → 0.2.1
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 +1 -2
- package/dist/agent-manager-SSJUZWOV.js +13 -0
- package/dist/{channel-Q642YUZE.js → channel-2WJRM7PE.js} +2 -2
- package/dist/{chunk-H5XQARAP.js → chunk-4YXYAMFT.js} +3 -3
- package/dist/{chunk-5YW4B7CG.js → chunk-6UCG6MIX.js} +72 -23
- package/dist/{chunk-A5ZJEMHT.js → chunk-KFNNHQK7.js} +4 -4
- package/dist/chunk-L3BQEZ4Z.js +271 -0
- package/dist/{chunk-N4QN44LC.js → chunk-MY74SUOL.js} +29 -22
- package/dist/{chunk-KSMIWOCN.js → chunk-N4YNKR3Q.js} +6 -0
- package/dist/cli.js +23 -19
- package/dist/{connect-LW6G23AV.js → connect-X5V5IMRW.js} +3 -3
- package/dist/connectors/discord.js +9 -2
- package/dist/{create-3K6O2SDC.js → create-23AM7H5B.js} +1 -1
- package/dist/{daemon-client-ZTHW7ROS.js → daemon-client-VN24HM5T.js} +2 -2
- package/dist/daemon.js +394 -436
- package/dist/{delete-JNGY7ZFH.js → delete-GDMSOW3U.js} +2 -2
- package/dist/{disconnect-ACVTKTRE.js → disconnect-5JWFZ6RV.js} +2 -2
- package/dist/{down-FYCUYC5H.js → down-WTF73FE7.js} +5 -4
- package/dist/{env-7SLRN3MG.js → env-YKUJOFHE.js} +12 -5
- package/dist/{fork-BB3DZ426.js → fork-GRSVMBKI.js} +39 -32
- package/dist/history-7WVVKMUY.js +46 -0
- package/dist/{import-W2AMTEV5.js → import-42DOLBDT.js} +1 -1
- package/dist/{logs-BUHRIQ2L.js → logs-SYRQOL6B.js} +1 -1
- package/dist/{merge-446QTE7Q.js → merge-CSAVLSLY.js} +33 -36
- package/dist/{schedule-KKSOVUDF.js → schedule-J37XQM6E.js} +2 -2
- package/dist/{send-WQSVSRDD.js → send-PLOYEYER.js} +7 -5
- package/dist/{start-LKMWS6ZE.js → start-AG7QLULK.js} +2 -2
- package/dist/{status-CIEKUI3V.js → status-GCNU4M3K.js} +9 -2
- package/dist/{stop-YTOAGYE4.js → stop-IL5Q6NER.js} +2 -2
- package/dist/{up-AJJ4GCXY.js → up-ZC6G6K4K.js} +21 -37
- package/dist/{upgrade-JACA6YMO.js → upgrade-DD5TNJWU.js} +3 -5
- package/dist/{variants-HPY4DEWU.js → variants-QQIEKT6M.js} +2 -2
- package/drizzle/0000_flaky_mariko_yashida.sql +34 -0
- package/drizzle/0001_careless_warpath.sql +12 -0
- package/drizzle/meta/0000_snapshot.json +227 -0
- package/drizzle/meta/0001_snapshot.json +298 -0
- package/drizzle/meta/_journal.json +20 -0
- package/package.json +2 -1
- package/templates/_base/.init/.config/hooks/startup-context.sh +28 -0
- package/templates/_base/_skills/memory/SKILL.md +56 -13
- package/templates/_base/_skills/volute-agent/SKILL.md +27 -3
- package/templates/_base/home/VOLUTE.md +25 -0
- package/templates/_base/src/lib/format-prefix.ts +24 -0
- package/templates/_base/src/lib/sessions.ts +71 -0
- package/templates/_base/src/lib/startup.ts +132 -0
- package/templates/_base/src/lib/types.ts +3 -0
- package/templates/_base/src/lib/volute-server.ts +18 -2
- package/templates/agent-sdk/.init/.claude/settings.json +14 -0
- package/templates/agent-sdk/.init/.config/sessions.json +4 -0
- package/templates/agent-sdk/.init/CLAUDE.md +3 -2
- package/templates/agent-sdk/package.json.tmpl +1 -1
- package/templates/agent-sdk/src/agent.ts +101 -0
- package/templates/agent-sdk/src/lib/agent-sessions.ts +180 -0
- package/templates/agent-sdk/src/server.ts +33 -129
- package/templates/agent-sdk/volute-template.json +1 -1
- package/templates/pi/.init/.config/sessions.json +1 -0
- package/templates/pi/.init/AGENTS.md +2 -1
- package/templates/pi/src/agent.ts +61 -0
- package/templates/pi/src/lib/agent-sessions.ts +188 -0
- package/templates/pi/src/server.ts +28 -102
- package/templates/pi/volute-template.json +1 -1
- package/templates/agent-sdk/src/lib/agent.ts +0 -199
- package/templates/pi/src/lib/agent.ts +0 -205
- /package/templates/_base/.init/memory/{.gitkeep → journal/.gitkeep} +0 -0
- /package/templates/_base/{volute.json.tmpl → home/.config/volute.json.tmpl} +0 -0
- /package/templates/pi/{volute.json.tmpl → home/.config/volute.json.tmpl} +0 -0
package/dist/daemon.js
CHANGED
|
@@ -1,283 +1,108 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getAgentManager,
|
|
4
|
+
initAgentManager
|
|
5
|
+
} from "./chunk-L3BQEZ4Z.js";
|
|
2
6
|
import {
|
|
3
7
|
logBuffer,
|
|
4
8
|
logger_default,
|
|
5
9
|
readNdjson
|
|
6
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-N4YNKR3Q.js";
|
|
7
11
|
import {
|
|
8
12
|
loadMergedEnv
|
|
9
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-KFNNHQK7.js";
|
|
10
14
|
import {
|
|
11
|
-
VOLUTE_HOME,
|
|
12
15
|
__export,
|
|
13
16
|
agentDir,
|
|
14
17
|
checkHealth,
|
|
15
18
|
findAgent,
|
|
19
|
+
findVariant,
|
|
20
|
+
getAllRunningVariants,
|
|
16
21
|
readRegistry,
|
|
17
22
|
readVariants,
|
|
18
23
|
removeAgent,
|
|
19
24
|
removeAllVariants,
|
|
20
|
-
setAgentRunning
|
|
21
|
-
|
|
25
|
+
setAgentRunning,
|
|
26
|
+
setVariantRunning,
|
|
27
|
+
voluteHome
|
|
28
|
+
} from "./chunk-6UCG6MIX.js";
|
|
22
29
|
|
|
23
30
|
// src/daemon.ts
|
|
24
31
|
import { randomBytes } from "crypto";
|
|
25
|
-
import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, unlinkSync as
|
|
32
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
26
33
|
import { resolve as resolve8 } from "path";
|
|
27
34
|
|
|
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
35
|
// src/lib/connector-manager.ts
|
|
233
|
-
import { spawn
|
|
36
|
+
import { spawn } from "child_process";
|
|
234
37
|
import {
|
|
235
|
-
createWriteStream
|
|
236
|
-
existsSync as
|
|
38
|
+
createWriteStream,
|
|
39
|
+
existsSync as existsSync2,
|
|
237
40
|
mkdirSync as mkdirSync2,
|
|
238
|
-
readFileSync as
|
|
239
|
-
unlinkSync
|
|
41
|
+
readFileSync as readFileSync2,
|
|
42
|
+
unlinkSync,
|
|
240
43
|
writeFileSync as writeFileSync2
|
|
241
44
|
} from "fs";
|
|
242
45
|
import { homedir } from "os";
|
|
243
|
-
import { dirname, resolve as
|
|
46
|
+
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
244
47
|
|
|
245
48
|
// src/lib/volute-config.ts
|
|
246
|
-
import { existsSync
|
|
247
|
-
import { resolve
|
|
49
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
50
|
+
import { dirname, resolve } from "path";
|
|
51
|
+
function configPath(agentDir2) {
|
|
52
|
+
const newPath = resolve(agentDir2, "home/.config/volute.json");
|
|
53
|
+
if (existsSync(newPath)) return newPath;
|
|
54
|
+
const oldPath = resolve(agentDir2, "volute.json");
|
|
55
|
+
if (existsSync(oldPath)) return oldPath;
|
|
56
|
+
return newPath;
|
|
57
|
+
}
|
|
248
58
|
function readVoluteConfig(agentDir2) {
|
|
249
|
-
const path =
|
|
250
|
-
if (!
|
|
59
|
+
const path = configPath(agentDir2);
|
|
60
|
+
if (!existsSync(path)) return {};
|
|
251
61
|
try {
|
|
252
|
-
return JSON.parse(
|
|
62
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
253
63
|
} catch {
|
|
254
|
-
return
|
|
64
|
+
return null;
|
|
255
65
|
}
|
|
256
66
|
}
|
|
257
67
|
function writeVoluteConfig(agentDir2, config) {
|
|
258
|
-
const path =
|
|
68
|
+
const path = resolve(agentDir2, "home/.config/volute.json");
|
|
69
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
259
70
|
writeFileSync(path, `${JSON.stringify(config, null, 2)}
|
|
260
71
|
`);
|
|
261
72
|
}
|
|
262
73
|
|
|
263
74
|
// src/lib/connector-manager.ts
|
|
75
|
+
function searchUpwards(...segments) {
|
|
76
|
+
let searchDir = dirname2(new URL(import.meta.url).pathname);
|
|
77
|
+
for (let i = 0; i < 5; i++) {
|
|
78
|
+
const candidate = resolve2(searchDir, ...segments);
|
|
79
|
+
if (existsSync2(candidate)) return candidate;
|
|
80
|
+
searchDir = dirname2(searchDir);
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
var MAX_RESTART_ATTEMPTS = 5;
|
|
85
|
+
var BASE_RESTART_DELAY = 3e3;
|
|
86
|
+
var MAX_RESTART_DELAY = 6e4;
|
|
264
87
|
var ConnectorManager = class {
|
|
265
88
|
connectors = /* @__PURE__ */ new Map();
|
|
266
89
|
stopping = /* @__PURE__ */ new Set();
|
|
267
90
|
// "agent:type" keys currently being explicitly stopped
|
|
268
91
|
shuttingDown = false;
|
|
269
|
-
|
|
270
|
-
|
|
92
|
+
restartAttempts = /* @__PURE__ */ new Map();
|
|
93
|
+
// "agent:type" -> count
|
|
94
|
+
async startConnectors(agentName, agentDir2, agentPort, daemonPort) {
|
|
95
|
+
const config = readVoluteConfig(agentDir2) ?? {};
|
|
271
96
|
const types = config.connectors ?? [];
|
|
272
97
|
for (const type of types) {
|
|
273
98
|
try {
|
|
274
|
-
await this.startConnector(agentName, agentDir2, agentPort, type);
|
|
99
|
+
await this.startConnector(agentName, agentDir2, agentPort, type, daemonPort);
|
|
275
100
|
} catch (err) {
|
|
276
101
|
console.error(`[daemon] failed to start connector ${type} for ${agentName}:`, err);
|
|
277
102
|
}
|
|
278
103
|
}
|
|
279
104
|
}
|
|
280
|
-
async startConnector(agentName, agentDir2, agentPort, type) {
|
|
105
|
+
async startConnector(agentName, agentDir2, agentPort, type, daemonPort) {
|
|
281
106
|
const existing = this.connectors.get(agentName)?.get(type);
|
|
282
107
|
if (existing) {
|
|
283
108
|
await new Promise((res) => {
|
|
@@ -298,37 +123,41 @@ var ConnectorManager = class {
|
|
|
298
123
|
this.connectors.get(agentName)?.delete(type);
|
|
299
124
|
}
|
|
300
125
|
this.killOrphanConnector(agentDir2, type);
|
|
301
|
-
const agentConnector =
|
|
302
|
-
const userConnector =
|
|
126
|
+
const agentConnector = resolve2(agentDir2, "connectors", type, "index.ts");
|
|
127
|
+
const userConnector = resolve2(homedir(), ".volute", "connectors", type, "index.ts");
|
|
303
128
|
const builtinConnector = this.resolveBuiltinConnector(type);
|
|
304
129
|
let connectorScript;
|
|
305
130
|
let runtime;
|
|
306
|
-
if (
|
|
131
|
+
if (existsSync2(agentConnector)) {
|
|
307
132
|
connectorScript = agentConnector;
|
|
308
|
-
runtime =
|
|
309
|
-
} else if (
|
|
133
|
+
runtime = resolve2(agentDir2, "node_modules", ".bin", "tsx");
|
|
134
|
+
} else if (existsSync2(userConnector)) {
|
|
310
135
|
connectorScript = userConnector;
|
|
311
136
|
runtime = this.resolveVoluteTsx();
|
|
312
|
-
} else if (builtinConnector
|
|
137
|
+
} else if (builtinConnector) {
|
|
313
138
|
connectorScript = builtinConnector;
|
|
314
139
|
runtime = process.execPath;
|
|
315
140
|
} else {
|
|
316
141
|
throw new Error(`No connector code found for type: ${type}`);
|
|
317
142
|
}
|
|
318
|
-
const logsDir =
|
|
143
|
+
const logsDir = resolve2(agentDir2, ".volute", "logs");
|
|
319
144
|
mkdirSync2(logsDir, { recursive: true });
|
|
320
|
-
const logStream =
|
|
145
|
+
const logStream = createWriteStream(resolve2(logsDir, `${type}.log`), { flags: "a" });
|
|
321
146
|
const agentEnv = loadMergedEnv(agentDir2);
|
|
322
147
|
const prefix = `${type.toUpperCase()}_`;
|
|
323
148
|
const connectorEnv = Object.fromEntries(
|
|
324
149
|
Object.entries(agentEnv).filter(([k]) => k.startsWith(prefix))
|
|
325
150
|
);
|
|
326
|
-
const child =
|
|
151
|
+
const child = spawn(runtime, [connectorScript], {
|
|
327
152
|
stdio: ["ignore", "pipe", "pipe"],
|
|
328
153
|
env: {
|
|
329
154
|
...process.env,
|
|
330
155
|
VOLUTE_AGENT_PORT: String(agentPort),
|
|
331
156
|
VOLUTE_AGENT_NAME: agentName,
|
|
157
|
+
...daemonPort ? {
|
|
158
|
+
VOLUTE_DAEMON_URL: `http://localhost:${daemonPort}`,
|
|
159
|
+
VOLUTE_DAEMON_TOKEN: process.env.VOLUTE_DAEMON_TOKEN
|
|
160
|
+
} : {},
|
|
332
161
|
...connectorEnv
|
|
333
162
|
}
|
|
334
163
|
});
|
|
@@ -342,6 +171,7 @@ var ConnectorManager = class {
|
|
|
342
171
|
}
|
|
343
172
|
this.connectors.get(agentName).set(type, { child, type });
|
|
344
173
|
const stopKey = `${agentName}:${type}`;
|
|
174
|
+
this.restartAttempts.delete(stopKey);
|
|
345
175
|
child.on("exit", (code) => {
|
|
346
176
|
const agentMap = this.connectors.get(agentName);
|
|
347
177
|
if (agentMap?.get(type)?.child === child) {
|
|
@@ -350,13 +180,24 @@ var ConnectorManager = class {
|
|
|
350
180
|
if (this.shuttingDown) return;
|
|
351
181
|
if (this.stopping.has(stopKey)) return;
|
|
352
182
|
console.error(`[daemon] connector ${type} for ${agentName} exited with code ${code}`);
|
|
353
|
-
|
|
183
|
+
const attempts = this.restartAttempts.get(stopKey) ?? 0;
|
|
184
|
+
if (attempts >= MAX_RESTART_ATTEMPTS) {
|
|
185
|
+
console.error(
|
|
186
|
+
`[daemon] connector ${type} for ${agentName} crashed ${attempts} times \u2014 giving up`
|
|
187
|
+
);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const delay = Math.min(BASE_RESTART_DELAY * 2 ** attempts, MAX_RESTART_DELAY);
|
|
191
|
+
this.restartAttempts.set(stopKey, attempts + 1);
|
|
192
|
+
console.error(
|
|
193
|
+
`[daemon] restarting connector ${type} for ${agentName} \u2014 attempt ${attempts + 1}/${MAX_RESTART_ATTEMPTS}, in ${delay}ms`
|
|
194
|
+
);
|
|
354
195
|
setTimeout(() => {
|
|
355
196
|
if (this.shuttingDown || this.stopping.has(stopKey)) return;
|
|
356
|
-
this.startConnector(agentName, agentDir2, agentPort, type).catch((err) => {
|
|
197
|
+
this.startConnector(agentName, agentDir2, agentPort, type, daemonPort).catch((err) => {
|
|
357
198
|
console.error(`[daemon] failed to restart connector ${type} for ${agentName}:`, err);
|
|
358
199
|
});
|
|
359
|
-
},
|
|
200
|
+
}, delay);
|
|
360
201
|
});
|
|
361
202
|
console.error(`[daemon] started connector ${type} for ${agentName}`);
|
|
362
203
|
}
|
|
@@ -384,6 +225,7 @@ var ConnectorManager = class {
|
|
|
384
225
|
}, 5e3);
|
|
385
226
|
});
|
|
386
227
|
this.stopping.delete(stopKey);
|
|
228
|
+
this.restartAttempts.delete(stopKey);
|
|
387
229
|
try {
|
|
388
230
|
this.removeConnectorPid(agentDir(agentName), type);
|
|
389
231
|
} catch {
|
|
@@ -411,24 +253,24 @@ var ConnectorManager = class {
|
|
|
411
253
|
}));
|
|
412
254
|
}
|
|
413
255
|
connectorPidPath(agentDir2, type) {
|
|
414
|
-
return
|
|
256
|
+
return resolve2(agentDir2, ".volute", "connectors", `${type}.pid`);
|
|
415
257
|
}
|
|
416
258
|
saveConnectorPid(agentDir2, type, pid) {
|
|
417
259
|
const pidPath = this.connectorPidPath(agentDir2, type);
|
|
418
|
-
mkdirSync2(
|
|
260
|
+
mkdirSync2(dirname2(pidPath), { recursive: true });
|
|
419
261
|
writeFileSync2(pidPath, String(pid));
|
|
420
262
|
}
|
|
421
263
|
removeConnectorPid(agentDir2, type) {
|
|
422
264
|
try {
|
|
423
|
-
|
|
265
|
+
unlinkSync(this.connectorPidPath(agentDir2, type));
|
|
424
266
|
} catch {
|
|
425
267
|
}
|
|
426
268
|
}
|
|
427
269
|
killOrphanConnector(agentDir2, type) {
|
|
428
270
|
const pidPath = this.connectorPidPath(agentDir2, type);
|
|
429
|
-
if (!
|
|
271
|
+
if (!existsSync2(pidPath)) return;
|
|
430
272
|
try {
|
|
431
|
-
const pid = parseInt(
|
|
273
|
+
const pid = parseInt(readFileSync2(pidPath, "utf-8").trim(), 10);
|
|
432
274
|
if (pid > 0) {
|
|
433
275
|
process.kill(pid, "SIGTERM");
|
|
434
276
|
console.error(`[daemon] killed orphan connector ${type} (pid ${pid})`);
|
|
@@ -436,38 +278,26 @@ var ConnectorManager = class {
|
|
|
436
278
|
} catch {
|
|
437
279
|
}
|
|
438
280
|
try {
|
|
439
|
-
|
|
281
|
+
unlinkSync(pidPath);
|
|
440
282
|
} catch {
|
|
441
283
|
}
|
|
442
284
|
}
|
|
443
285
|
resolveBuiltinConnector(type) {
|
|
444
|
-
|
|
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;
|
|
286
|
+
return searchUpwards("connectors", `${type}.js`);
|
|
451
287
|
}
|
|
452
288
|
resolveVoluteTsx() {
|
|
453
|
-
|
|
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";
|
|
289
|
+
return searchUpwards("node_modules", ".bin", "tsx") ?? "tsx";
|
|
460
290
|
}
|
|
461
291
|
};
|
|
462
|
-
var
|
|
292
|
+
var instance = null;
|
|
463
293
|
function initConnectorManager() {
|
|
464
|
-
if (
|
|
465
|
-
|
|
466
|
-
return
|
|
294
|
+
if (instance) throw new Error("ConnectorManager already initialized");
|
|
295
|
+
instance = new ConnectorManager();
|
|
296
|
+
return instance;
|
|
467
297
|
}
|
|
468
298
|
function getConnectorManager() {
|
|
469
|
-
if (!
|
|
470
|
-
return
|
|
299
|
+
if (!instance) instance = new ConnectorManager();
|
|
300
|
+
return instance;
|
|
471
301
|
}
|
|
472
302
|
|
|
473
303
|
// src/lib/scheduler.ts
|
|
@@ -477,7 +307,11 @@ var Scheduler = class {
|
|
|
477
307
|
interval = null;
|
|
478
308
|
lastFired = /* @__PURE__ */ new Map();
|
|
479
309
|
// "agent:scheduleId" → epoch minute
|
|
480
|
-
|
|
310
|
+
daemonPort = null;
|
|
311
|
+
daemonToken = null;
|
|
312
|
+
start(daemonPort, daemonToken) {
|
|
313
|
+
this.daemonPort = daemonPort ?? null;
|
|
314
|
+
this.daemonToken = daemonToken ?? null;
|
|
481
315
|
this.interval = setInterval(() => this.tick(), 6e4);
|
|
482
316
|
}
|
|
483
317
|
stop() {
|
|
@@ -486,6 +320,7 @@ var Scheduler = class {
|
|
|
486
320
|
loadSchedules(agentName) {
|
|
487
321
|
const dir = agentDir(agentName);
|
|
488
322
|
const config = readVoluteConfig(dir);
|
|
323
|
+
if (!config) return;
|
|
489
324
|
const schedules = config.schedules ?? [];
|
|
490
325
|
if (schedules.length > 0) {
|
|
491
326
|
this.schedules.set(agentName, schedules);
|
|
@@ -497,6 +332,9 @@ var Scheduler = class {
|
|
|
497
332
|
this.schedules.delete(agentName);
|
|
498
333
|
}
|
|
499
334
|
tick() {
|
|
335
|
+
for (const agent of this.schedules.keys()) {
|
|
336
|
+
this.loadSchedules(agent);
|
|
337
|
+
}
|
|
500
338
|
const now = /* @__PURE__ */ new Date();
|
|
501
339
|
for (const [agent, schedules] of this.schedules) {
|
|
502
340
|
for (const schedule of schedules) {
|
|
@@ -520,43 +358,67 @@ var Scheduler = class {
|
|
|
520
358
|
return true;
|
|
521
359
|
}
|
|
522
360
|
return false;
|
|
523
|
-
} catch {
|
|
361
|
+
} catch (err) {
|
|
362
|
+
console.error(
|
|
363
|
+
`[scheduler] invalid cron "${schedule.cron}" for ${agent}:${schedule.id}:`,
|
|
364
|
+
err
|
|
365
|
+
);
|
|
524
366
|
return false;
|
|
525
367
|
}
|
|
526
368
|
}
|
|
527
369
|
async fire(agentName, schedule) {
|
|
528
370
|
const entry = findAgent(agentName);
|
|
529
371
|
if (!entry) return;
|
|
372
|
+
const body = JSON.stringify({
|
|
373
|
+
content: [{ type: "text", text: schedule.message }],
|
|
374
|
+
channel: "system:scheduler",
|
|
375
|
+
sender: schedule.id
|
|
376
|
+
});
|
|
530
377
|
try {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
378
|
+
let res;
|
|
379
|
+
if (this.daemonPort && this.daemonToken) {
|
|
380
|
+
const daemonUrl = `http://localhost:${this.daemonPort}`;
|
|
381
|
+
res = await fetch(`${daemonUrl}/api/agents/${encodeURIComponent(agentName)}/message`, {
|
|
382
|
+
method: "POST",
|
|
383
|
+
headers: {
|
|
384
|
+
"Content-Type": "application/json",
|
|
385
|
+
Authorization: `Bearer ${this.daemonToken}`,
|
|
386
|
+
Origin: daemonUrl
|
|
387
|
+
},
|
|
388
|
+
body
|
|
389
|
+
});
|
|
390
|
+
} else {
|
|
391
|
+
res = await fetch(`http://localhost:${entry.port}/message`, {
|
|
392
|
+
method: "POST",
|
|
393
|
+
headers: { "Content-Type": "application/json" },
|
|
394
|
+
body
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
if (!res.ok) {
|
|
398
|
+
console.error(`[scheduler] "${schedule.id}" for ${agentName} got HTTP ${res.status}`);
|
|
399
|
+
} else {
|
|
400
|
+
console.error(`[scheduler] fired "${schedule.id}" for ${agentName}`);
|
|
401
|
+
}
|
|
541
402
|
} catch (err) {
|
|
542
403
|
console.error(`[scheduler] failed to fire "${schedule.id}" for ${agentName}:`, err);
|
|
543
404
|
}
|
|
544
405
|
}
|
|
545
406
|
};
|
|
546
|
-
var
|
|
407
|
+
var instance2 = null;
|
|
547
408
|
function getScheduler() {
|
|
548
|
-
if (!
|
|
549
|
-
return
|
|
409
|
+
if (!instance2) instance2 = new Scheduler();
|
|
410
|
+
return instance2;
|
|
550
411
|
}
|
|
551
412
|
|
|
552
413
|
// src/web/server.ts
|
|
553
|
-
import { existsSync as
|
|
414
|
+
import { existsSync as existsSync7 } from "fs";
|
|
554
415
|
import { readFile as readFile2, stat } from "fs/promises";
|
|
555
|
-
import { dirname as
|
|
416
|
+
import { dirname as dirname4, extname, resolve as resolve7 } from "path";
|
|
556
417
|
import { serve } from "@hono/node-server";
|
|
557
418
|
|
|
558
419
|
// src/web/app.ts
|
|
559
420
|
import { Hono as Hono11 } from "hono";
|
|
421
|
+
import { bodyLimit } from "hono/body-limit";
|
|
560
422
|
import { csrf } from "hono/csrf";
|
|
561
423
|
import { HTTPException } from "hono/http-exception";
|
|
562
424
|
|
|
@@ -569,8 +431,8 @@ import { compareSync, hashSync } from "bcryptjs";
|
|
|
569
431
|
import { and, count, eq } from "drizzle-orm";
|
|
570
432
|
|
|
571
433
|
// src/lib/db.ts
|
|
572
|
-
import { existsSync as
|
|
573
|
-
import { dirname as
|
|
434
|
+
import { chmodSync, existsSync as existsSync3 } from "fs";
|
|
435
|
+
import { dirname as dirname3, resolve as resolve3 } from "path";
|
|
574
436
|
import { fileURLToPath } from "url";
|
|
575
437
|
import { drizzle } from "drizzle-orm/libsql";
|
|
576
438
|
import { migrate } from "drizzle-orm/libsql/migrator";
|
|
@@ -639,18 +501,26 @@ var messages = sqliteTable(
|
|
|
639
501
|
);
|
|
640
502
|
|
|
641
503
|
// src/lib/db.ts
|
|
642
|
-
var __dirname =
|
|
643
|
-
var migrationsFolder =
|
|
504
|
+
var __dirname = dirname3(fileURLToPath(import.meta.url));
|
|
505
|
+
var migrationsFolder = existsSync3(resolve3(__dirname, "../drizzle")) ? resolve3(__dirname, "../drizzle") : resolve3(__dirname, "../../drizzle");
|
|
644
506
|
var db = null;
|
|
645
507
|
async function getDb() {
|
|
646
508
|
if (db) return db;
|
|
647
|
-
const dbPath = process.env.VOLUTE_DB_PATH ||
|
|
509
|
+
const dbPath = process.env.VOLUTE_DB_PATH || resolve3(voluteHome(), "volute.db");
|
|
648
510
|
db = drizzle({ connection: { url: `file:${dbPath}` }, schema: schema_exports });
|
|
649
511
|
try {
|
|
650
512
|
await migrate(db, { migrationsFolder });
|
|
651
513
|
} catch (e) {
|
|
652
514
|
if (!(e instanceof Error) || !e.message.includes("already exists")) throw e;
|
|
653
515
|
}
|
|
516
|
+
try {
|
|
517
|
+
chmodSync(dbPath, 384);
|
|
518
|
+
} catch (err) {
|
|
519
|
+
console.error(
|
|
520
|
+
`[volute] WARNING: Failed to restrict database file permissions on ${dbPath}:`,
|
|
521
|
+
err
|
|
522
|
+
);
|
|
523
|
+
}
|
|
654
524
|
return db;
|
|
655
525
|
}
|
|
656
526
|
|
|
@@ -739,6 +609,13 @@ function getSessionUserId(sessionId) {
|
|
|
739
609
|
}
|
|
740
610
|
return session.userId;
|
|
741
611
|
}
|
|
612
|
+
var requireAdmin = createMiddleware(async (c, next) => {
|
|
613
|
+
const user = c.get("user");
|
|
614
|
+
if (user.role !== "admin") {
|
|
615
|
+
return c.json({ error: "Forbidden" }, 403);
|
|
616
|
+
}
|
|
617
|
+
await next();
|
|
618
|
+
});
|
|
742
619
|
var authMiddleware = createMiddleware(async (c, next) => {
|
|
743
620
|
const authHeader = c.req.header("Authorization");
|
|
744
621
|
if (authHeader?.startsWith("Bearer ")) {
|
|
@@ -761,7 +638,8 @@ var authMiddleware = createMiddleware(async (c, next) => {
|
|
|
761
638
|
});
|
|
762
639
|
|
|
763
640
|
// src/web/routes/agents.ts
|
|
764
|
-
import { existsSync as
|
|
641
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, rmSync } from "fs";
|
|
642
|
+
import { resolve as resolve4 } from "path";
|
|
765
643
|
import { and as and2, desc, eq as eq2 } from "drizzle-orm";
|
|
766
644
|
import { Hono } from "hono";
|
|
767
645
|
import { stream } from "hono/streaming";
|
|
@@ -771,11 +649,20 @@ var CHANNELS = {
|
|
|
771
649
|
web: { name: "web", displayName: "Web UI", showToolCalls: true },
|
|
772
650
|
discord: { name: "discord", displayName: "Discord", showToolCalls: false },
|
|
773
651
|
cli: { name: "cli", displayName: "CLI", showToolCalls: true },
|
|
652
|
+
agent: { name: "agent", displayName: "Agent", showToolCalls: true },
|
|
774
653
|
system: { name: "system", displayName: "System", showToolCalls: false }
|
|
775
654
|
};
|
|
776
655
|
|
|
777
656
|
// src/web/routes/agents.ts
|
|
778
|
-
|
|
657
|
+
function getDaemonPort() {
|
|
658
|
+
try {
|
|
659
|
+
const data = JSON.parse(readFileSync3(resolve4(voluteHome(), "daemon.json"), "utf-8"));
|
|
660
|
+
return data.port;
|
|
661
|
+
} catch {
|
|
662
|
+
return void 0;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
async function getAgentStatus(name, port) {
|
|
779
666
|
const manager = getAgentManager();
|
|
780
667
|
let status = "stopped";
|
|
781
668
|
if (manager.isRunning(name)) {
|
|
@@ -789,25 +676,15 @@ async function getAgentStatus(name, _dir, port) {
|
|
|
789
676
|
status: status === "running" ? "connected" : "disconnected",
|
|
790
677
|
showToolCalls: CHANNELS.web.showToolCalls
|
|
791
678
|
});
|
|
792
|
-
const
|
|
793
|
-
const connectorStatuses = connectorManager.getConnectorStatus(name);
|
|
679
|
+
const connectorStatuses = getConnectorManager().getConnectorStatus(name);
|
|
794
680
|
for (const cs of connectorStatuses) {
|
|
795
|
-
const
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
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
|
-
}
|
|
681
|
+
const config = CHANNELS[cs.type];
|
|
682
|
+
channels.push({
|
|
683
|
+
name: config?.name ?? cs.type,
|
|
684
|
+
displayName: config?.displayName ?? cs.type,
|
|
685
|
+
status: cs.running ? "connected" : "disconnected",
|
|
686
|
+
showToolCalls: config?.showToolCalls ?? false
|
|
687
|
+
});
|
|
811
688
|
}
|
|
812
689
|
return { status, channels };
|
|
813
690
|
}
|
|
@@ -815,8 +692,7 @@ var app = new Hono().get("/", async (c) => {
|
|
|
815
692
|
const entries = readRegistry();
|
|
816
693
|
const agents = await Promise.all(
|
|
817
694
|
entries.map(async (entry) => {
|
|
818
|
-
const
|
|
819
|
-
const { status, channels } = await getAgentStatus(entry.name, dir, entry.port);
|
|
695
|
+
const { status, channels } = await getAgentStatus(entry.name, entry.port);
|
|
820
696
|
return { ...entry, status, channels };
|
|
821
697
|
})
|
|
822
698
|
);
|
|
@@ -825,65 +701,97 @@ var app = new Hono().get("/", async (c) => {
|
|
|
825
701
|
const name = c.req.param("name");
|
|
826
702
|
const entry = findAgent(name);
|
|
827
703
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
const
|
|
831
|
-
|
|
832
|
-
|
|
704
|
+
if (!existsSync4(agentDir(name))) return c.json({ error: "Agent directory missing" }, 404);
|
|
705
|
+
const { status, channels } = await getAgentStatus(name, entry.port);
|
|
706
|
+
const variants = readVariants(name);
|
|
707
|
+
const manager = getAgentManager();
|
|
708
|
+
const variantStatuses = await Promise.all(
|
|
709
|
+
variants.map(async (v) => {
|
|
710
|
+
const compositeKey = `${name}@${v.name}`;
|
|
711
|
+
let variantStatus = "stopped";
|
|
712
|
+
if (manager.isRunning(compositeKey)) {
|
|
713
|
+
const health = await checkHealth(v.port);
|
|
714
|
+
variantStatus = health.ok ? "running" : "starting";
|
|
715
|
+
}
|
|
716
|
+
return { name: v.name, port: v.port, status: variantStatus };
|
|
717
|
+
})
|
|
718
|
+
);
|
|
719
|
+
return c.json({ ...entry, status, channels, variants: variantStatuses });
|
|
720
|
+
}).post("/:name/start", requireAdmin, async (c) => {
|
|
833
721
|
const name = c.req.param("name");
|
|
834
|
-
const
|
|
722
|
+
const [baseName, variantName] = name.split("@", 2);
|
|
723
|
+
const entry = findAgent(baseName);
|
|
835
724
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
836
|
-
|
|
837
|
-
|
|
725
|
+
if (variantName) {
|
|
726
|
+
const variant = findVariant(baseName, variantName);
|
|
727
|
+
if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
|
|
728
|
+
} else {
|
|
729
|
+
const dir = agentDir(baseName);
|
|
730
|
+
if (!existsSync4(dir)) return c.json({ error: "Agent directory missing" }, 404);
|
|
731
|
+
}
|
|
838
732
|
const manager = getAgentManager();
|
|
839
733
|
if (manager.isRunning(name)) {
|
|
840
734
|
return c.json({ error: "Agent already running" }, 409);
|
|
841
735
|
}
|
|
842
736
|
try {
|
|
843
737
|
await manager.startAgent(name);
|
|
844
|
-
|
|
845
|
-
|
|
738
|
+
if (!variantName) {
|
|
739
|
+
const dir = agentDir(baseName);
|
|
740
|
+
await getConnectorManager().startConnectors(baseName, dir, entry.port, getDaemonPort());
|
|
741
|
+
}
|
|
846
742
|
return c.json({ ok: true });
|
|
847
743
|
} catch (err) {
|
|
848
744
|
return c.json({ error: err instanceof Error ? err.message : "Failed to start agent" }, 500);
|
|
849
745
|
}
|
|
850
|
-
}).post("/:name/restart", async (c) => {
|
|
746
|
+
}).post("/:name/restart", requireAdmin, async (c) => {
|
|
851
747
|
const name = c.req.param("name");
|
|
852
|
-
const
|
|
748
|
+
const [baseName, variantName] = name.split("@", 2);
|
|
749
|
+
const entry = findAgent(baseName);
|
|
853
750
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
854
|
-
|
|
855
|
-
|
|
751
|
+
if (variantName) {
|
|
752
|
+
const variant = findVariant(baseName, variantName);
|
|
753
|
+
if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
|
|
754
|
+
} else {
|
|
755
|
+
const dir = agentDir(baseName);
|
|
756
|
+
if (!existsSync4(dir)) return c.json({ error: "Agent directory missing" }, 404);
|
|
757
|
+
}
|
|
856
758
|
const manager = getAgentManager();
|
|
857
759
|
const connectorManager = getConnectorManager();
|
|
858
760
|
try {
|
|
859
761
|
if (manager.isRunning(name)) {
|
|
860
|
-
await connectorManager.stopConnectors(
|
|
762
|
+
if (!variantName) await connectorManager.stopConnectors(baseName);
|
|
861
763
|
await manager.stopAgent(name);
|
|
862
764
|
}
|
|
863
765
|
await manager.startAgent(name);
|
|
864
|
-
|
|
865
|
-
|
|
766
|
+
if (!variantName) {
|
|
767
|
+
const dir = agentDir(baseName);
|
|
768
|
+
await connectorManager.startConnectors(baseName, dir, entry.port, getDaemonPort());
|
|
769
|
+
}
|
|
866
770
|
return c.json({ ok: true });
|
|
867
771
|
} catch (err) {
|
|
868
772
|
return c.json({ error: err instanceof Error ? err.message : "Failed to restart agent" }, 500);
|
|
869
773
|
}
|
|
870
|
-
}).post("/:name/stop", async (c) => {
|
|
774
|
+
}).post("/:name/stop", requireAdmin, async (c) => {
|
|
871
775
|
const name = c.req.param("name");
|
|
872
|
-
const
|
|
776
|
+
const [baseName, variantName] = name.split("@", 2);
|
|
777
|
+
const entry = findAgent(baseName);
|
|
873
778
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
779
|
+
if (variantName) {
|
|
780
|
+
const variant = findVariant(baseName, variantName);
|
|
781
|
+
if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
|
|
782
|
+
}
|
|
874
783
|
const manager = getAgentManager();
|
|
875
784
|
if (!manager.isRunning(name)) {
|
|
876
785
|
return c.json({ error: "Agent is not running" }, 409);
|
|
877
786
|
}
|
|
878
787
|
try {
|
|
879
|
-
await getConnectorManager().stopConnectors(
|
|
788
|
+
if (!variantName) await getConnectorManager().stopConnectors(baseName);
|
|
880
789
|
await manager.stopAgent(name);
|
|
881
|
-
setAgentRunning(name, false);
|
|
882
790
|
return c.json({ ok: true });
|
|
883
791
|
} catch (err) {
|
|
884
792
|
return c.json({ error: err instanceof Error ? err.message : "Failed to stop agent" }, 500);
|
|
885
793
|
}
|
|
886
|
-
}).delete("/:name", async (c) => {
|
|
794
|
+
}).delete("/:name", requireAdmin, async (c) => {
|
|
887
795
|
const name = c.req.param("name");
|
|
888
796
|
const entry = findAgent(name);
|
|
889
797
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
@@ -896,35 +804,48 @@ var app = new Hono().get("/", async (c) => {
|
|
|
896
804
|
}
|
|
897
805
|
removeAllVariants(name);
|
|
898
806
|
removeAgent(name);
|
|
899
|
-
if (force &&
|
|
807
|
+
if (force && existsSync4(dir)) {
|
|
900
808
|
rmSync(dir, { recursive: true, force: true });
|
|
901
809
|
}
|
|
902
810
|
return c.json({ ok: true });
|
|
903
811
|
}).post("/:name/message", async (c) => {
|
|
904
812
|
const name = c.req.param("name");
|
|
905
|
-
const
|
|
813
|
+
const [baseName, variantName] = name.split("@", 2);
|
|
814
|
+
const entry = findAgent(baseName);
|
|
906
815
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
907
|
-
|
|
908
|
-
if (
|
|
816
|
+
let port = entry.port;
|
|
817
|
+
if (variantName) {
|
|
818
|
+
const variant = findVariant(baseName, variantName);
|
|
819
|
+
if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
|
|
820
|
+
port = variant.port;
|
|
821
|
+
}
|
|
822
|
+
if (!getAgentManager().isRunning(name)) {
|
|
909
823
|
return c.json({ error: "Agent is not running" }, 409);
|
|
910
824
|
}
|
|
911
825
|
const body = await c.req.text();
|
|
912
|
-
|
|
826
|
+
let parsed = null;
|
|
913
827
|
try {
|
|
914
|
-
|
|
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
|
-
});
|
|
828
|
+
parsed = JSON.parse(body);
|
|
925
829
|
} catch {
|
|
926
830
|
}
|
|
927
|
-
const
|
|
831
|
+
const channel = parsed?.channel ?? "unknown";
|
|
832
|
+
const db2 = await getDb();
|
|
833
|
+
if (parsed) {
|
|
834
|
+
try {
|
|
835
|
+
const sender = parsed.sender ?? null;
|
|
836
|
+
const content = typeof parsed.content === "string" ? parsed.content : JSON.stringify(parsed.content);
|
|
837
|
+
await db2.insert(agentMessages).values({
|
|
838
|
+
agent: baseName,
|
|
839
|
+
channel,
|
|
840
|
+
role: "user",
|
|
841
|
+
sender,
|
|
842
|
+
content
|
|
843
|
+
});
|
|
844
|
+
} catch (err) {
|
|
845
|
+
console.error(`[daemon] failed to persist inbound message for ${baseName}:`, err);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
const res = await fetch(`http://localhost:${port}/message`, {
|
|
928
849
|
method: "POST",
|
|
929
850
|
headers: { "Content-Type": "application/json" },
|
|
930
851
|
body
|
|
@@ -941,12 +862,7 @@ var app = new Hono().get("/", async (c) => {
|
|
|
941
862
|
const decoder = new TextDecoder();
|
|
942
863
|
let buffer = "";
|
|
943
864
|
const textParts = [];
|
|
944
|
-
let channel = "unknown";
|
|
945
865
|
try {
|
|
946
|
-
try {
|
|
947
|
-
channel = JSON.parse(body).channel ?? "unknown";
|
|
948
|
-
} catch {
|
|
949
|
-
}
|
|
950
866
|
while (true) {
|
|
951
867
|
const { done, value } = await reader.read();
|
|
952
868
|
if (done) break;
|
|
@@ -975,13 +891,16 @@ var app = new Hono().get("/", async (c) => {
|
|
|
975
891
|
}
|
|
976
892
|
}
|
|
977
893
|
if (textParts.length > 0) {
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
894
|
+
try {
|
|
895
|
+
await db2.insert(agentMessages).values({
|
|
896
|
+
agent: baseName,
|
|
897
|
+
channel,
|
|
898
|
+
role: "assistant",
|
|
899
|
+
content: textParts.join("")
|
|
900
|
+
});
|
|
901
|
+
} catch (err) {
|
|
902
|
+
console.error(`[daemon] failed to persist assistant response for ${baseName}:`, err);
|
|
903
|
+
}
|
|
985
904
|
}
|
|
986
905
|
} finally {
|
|
987
906
|
reader.releaseLock();
|
|
@@ -1094,11 +1013,17 @@ async function createConversation(agentName, channel, opts) {
|
|
|
1094
1013
|
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1095
1014
|
};
|
|
1096
1015
|
}
|
|
1097
|
-
async function
|
|
1016
|
+
async function getConversationForUser(id, userId) {
|
|
1098
1017
|
const db2 = await getDb();
|
|
1099
|
-
const row = await db2.select().from(conversations).where(eq3(conversations.id, id)).get();
|
|
1018
|
+
const row = await db2.select().from(conversations).where(and3(eq3(conversations.id, id), eq3(conversations.user_id, userId))).get();
|
|
1100
1019
|
return row ?? null;
|
|
1101
1020
|
}
|
|
1021
|
+
async function deleteConversationForUser(id, userId) {
|
|
1022
|
+
const conv = await getConversationForUser(id, userId);
|
|
1023
|
+
if (!conv) return false;
|
|
1024
|
+
await deleteConversation(id);
|
|
1025
|
+
return true;
|
|
1026
|
+
}
|
|
1102
1027
|
async function listConversations(agentName, opts) {
|
|
1103
1028
|
const db2 = await getDb();
|
|
1104
1029
|
if (opts?.userId != null) {
|
|
@@ -1159,8 +1084,19 @@ var chatSchema = z2.object({
|
|
|
1159
1084
|
});
|
|
1160
1085
|
var app3 = new Hono3().post("/:name/chat", zValidator2("json", chatSchema), async (c) => {
|
|
1161
1086
|
const name = c.req.param("name");
|
|
1162
|
-
const
|
|
1087
|
+
const [baseName, variantName] = name.split("@", 2);
|
|
1088
|
+
const entry = findAgent(baseName);
|
|
1163
1089
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1090
|
+
let port = entry.port;
|
|
1091
|
+
if (variantName) {
|
|
1092
|
+
const variant = findVariant(baseName, variantName);
|
|
1093
|
+
if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
|
|
1094
|
+
port = variant.port;
|
|
1095
|
+
}
|
|
1096
|
+
const { getAgentManager: getAgentManager2 } = await import("./agent-manager-SSJUZWOV.js");
|
|
1097
|
+
if (!getAgentManager2().isRunning(name)) {
|
|
1098
|
+
return c.json({ error: "Agent is not running" }, 409);
|
|
1099
|
+
}
|
|
1164
1100
|
const body = c.req.valid("json");
|
|
1165
1101
|
if (!body.message && (!body.images || body.images.length === 0)) {
|
|
1166
1102
|
return c.json({ error: "message or images required" }, 400);
|
|
@@ -1168,49 +1104,39 @@ var app3 = new Hono3().post("/:name/chat", zValidator2("json", chatSchema), asyn
|
|
|
1168
1104
|
const user = c.get("user");
|
|
1169
1105
|
let conversationId = body.conversationId;
|
|
1170
1106
|
if (conversationId) {
|
|
1171
|
-
const conv = await
|
|
1107
|
+
const conv = await getConversationForUser(conversationId, user.id);
|
|
1172
1108
|
if (!conv) return c.json({ error: "Conversation not found" }, 404);
|
|
1173
1109
|
} else {
|
|
1174
1110
|
const title = body.message ? body.message.slice(0, 80) : "Image message";
|
|
1175
|
-
const conv = await createConversation(
|
|
1111
|
+
const conv = await createConversation(baseName, "web", {
|
|
1176
1112
|
userId: user.id,
|
|
1177
1113
|
title
|
|
1178
1114
|
});
|
|
1179
1115
|
conversationId = conv.id;
|
|
1180
1116
|
}
|
|
1181
|
-
const
|
|
1117
|
+
const contentBlocks = [];
|
|
1182
1118
|
if (body.message) {
|
|
1183
|
-
|
|
1119
|
+
contentBlocks.push({ type: "text", text: body.message });
|
|
1184
1120
|
}
|
|
1185
1121
|
if (body.images) {
|
|
1186
1122
|
for (const img of body.images) {
|
|
1187
|
-
|
|
1123
|
+
contentBlocks.push({ type: "image", media_type: img.media_type, data: img.data });
|
|
1188
1124
|
}
|
|
1189
1125
|
}
|
|
1190
|
-
await addMessage(conversationId, "user", user.username,
|
|
1191
|
-
const userText = body.message ?? "[image]";
|
|
1126
|
+
await addMessage(conversationId, "user", user.username, contentBlocks);
|
|
1192
1127
|
const db2 = await getDb();
|
|
1193
1128
|
await db2.insert(agentMessages).values({
|
|
1194
|
-
agent:
|
|
1129
|
+
agent: baseName,
|
|
1195
1130
|
channel: "web",
|
|
1196
1131
|
role: "user",
|
|
1197
1132
|
sender: user.username,
|
|
1198
|
-
content:
|
|
1133
|
+
content: body.message ?? "[image]"
|
|
1199
1134
|
});
|
|
1200
|
-
const
|
|
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`, {
|
|
1135
|
+
const res = await fetch(`http://localhost:${port}/message`, {
|
|
1210
1136
|
method: "POST",
|
|
1211
1137
|
headers: { "Content-Type": "application/json" },
|
|
1212
1138
|
body: JSON.stringify({
|
|
1213
|
-
content:
|
|
1139
|
+
content: contentBlocks,
|
|
1214
1140
|
channel: "web",
|
|
1215
1141
|
sender: user.username
|
|
1216
1142
|
})
|
|
@@ -1227,36 +1153,34 @@ var app3 = new Hono3().post("/:name/chat", zValidator2("json", chatSchema), asyn
|
|
|
1227
1153
|
});
|
|
1228
1154
|
const assistantContent = [];
|
|
1229
1155
|
for await (const event of readNdjson(res.body)) {
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
if (voluteEvent.type === "text") {
|
|
1156
|
+
await stream2.writeSSE({ data: JSON.stringify(event) });
|
|
1157
|
+
if (event.type === "text") {
|
|
1233
1158
|
const last = assistantContent[assistantContent.length - 1];
|
|
1234
1159
|
if (last && last.type === "text") {
|
|
1235
|
-
last.text +=
|
|
1160
|
+
last.text += event.content;
|
|
1236
1161
|
} else {
|
|
1237
|
-
assistantContent.push({ type: "text", text:
|
|
1162
|
+
assistantContent.push({ type: "text", text: event.content });
|
|
1238
1163
|
}
|
|
1239
|
-
} else if (
|
|
1164
|
+
} else if (event.type === "tool_use") {
|
|
1240
1165
|
assistantContent.push({
|
|
1241
1166
|
type: "tool_use",
|
|
1242
|
-
name:
|
|
1243
|
-
input:
|
|
1167
|
+
name: event.name,
|
|
1168
|
+
input: event.input
|
|
1244
1169
|
});
|
|
1245
|
-
} else if (
|
|
1170
|
+
} else if (event.type === "tool_result") {
|
|
1246
1171
|
assistantContent.push({
|
|
1247
1172
|
type: "tool_result",
|
|
1248
|
-
output:
|
|
1249
|
-
...
|
|
1173
|
+
output: event.output,
|
|
1174
|
+
...event.is_error ? { is_error: true } : {}
|
|
1250
1175
|
});
|
|
1251
1176
|
}
|
|
1252
|
-
if (
|
|
1177
|
+
if (event.type === "done") {
|
|
1253
1178
|
if (assistantContent.length > 0) {
|
|
1254
|
-
await addMessage(conversationId, "assistant",
|
|
1179
|
+
await addMessage(conversationId, "assistant", baseName, assistantContent);
|
|
1255
1180
|
const textParts = assistantContent.filter((b) => b.type === "text").map((b) => b.text);
|
|
1256
1181
|
if (textParts.length > 0) {
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
agent: name,
|
|
1182
|
+
await db2.insert(agentMessages).values({
|
|
1183
|
+
agent: baseName,
|
|
1260
1184
|
channel: "web",
|
|
1261
1185
|
role: "assistant",
|
|
1262
1186
|
content: textParts.join("")
|
|
@@ -1272,12 +1196,13 @@ var chat_default = app3;
|
|
|
1272
1196
|
|
|
1273
1197
|
// src/web/routes/connectors.ts
|
|
1274
1198
|
import { Hono as Hono4 } from "hono";
|
|
1199
|
+
var CONNECTOR_TYPE_RE = /^[a-z][a-z0-9-]*$/;
|
|
1275
1200
|
var app4 = new Hono4().get("/:name/connectors", (c) => {
|
|
1276
1201
|
const name = c.req.param("name");
|
|
1277
1202
|
const entry = findAgent(name);
|
|
1278
1203
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1279
1204
|
const dir = agentDir(name);
|
|
1280
|
-
const config = readVoluteConfig(dir);
|
|
1205
|
+
const config = readVoluteConfig(dir) ?? {};
|
|
1281
1206
|
const configured = config.connectors ?? [];
|
|
1282
1207
|
const manager = getConnectorManager();
|
|
1283
1208
|
const runningStatus = manager.getConnectorStatus(name);
|
|
@@ -1286,13 +1211,16 @@ var app4 = new Hono4().get("/:name/connectors", (c) => {
|
|
|
1286
1211
|
return { type, running: status?.running ?? false };
|
|
1287
1212
|
});
|
|
1288
1213
|
return c.json(connectors);
|
|
1289
|
-
}).post("/:name/connectors/:type", async (c) => {
|
|
1214
|
+
}).post("/:name/connectors/:type", requireAdmin, async (c) => {
|
|
1290
1215
|
const name = c.req.param("name");
|
|
1291
1216
|
const type = c.req.param("type");
|
|
1217
|
+
if (!CONNECTOR_TYPE_RE.test(type)) {
|
|
1218
|
+
return c.json({ error: "Invalid connector type" }, 400);
|
|
1219
|
+
}
|
|
1292
1220
|
const entry = findAgent(name);
|
|
1293
1221
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1294
1222
|
const dir = agentDir(name);
|
|
1295
|
-
const config = readVoluteConfig(dir);
|
|
1223
|
+
const config = readVoluteConfig(dir) ?? {};
|
|
1296
1224
|
const connectors = config.connectors ?? [];
|
|
1297
1225
|
if (!connectors.includes(type)) {
|
|
1298
1226
|
config.connectors = [...connectors, type];
|
|
@@ -1308,15 +1236,18 @@ var app4 = new Hono4().get("/:name/connectors", (c) => {
|
|
|
1308
1236
|
500
|
|
1309
1237
|
);
|
|
1310
1238
|
}
|
|
1311
|
-
}).delete("/:name/connectors/:type", async (c) => {
|
|
1239
|
+
}).delete("/:name/connectors/:type", requireAdmin, async (c) => {
|
|
1312
1240
|
const name = c.req.param("name");
|
|
1313
1241
|
const type = c.req.param("type");
|
|
1242
|
+
if (!CONNECTOR_TYPE_RE.test(type)) {
|
|
1243
|
+
return c.json({ error: "Invalid connector type" }, 400);
|
|
1244
|
+
}
|
|
1314
1245
|
const entry = findAgent(name);
|
|
1315
1246
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1316
1247
|
const dir = agentDir(name);
|
|
1317
1248
|
const manager = getConnectorManager();
|
|
1318
1249
|
await manager.stopConnector(name, type);
|
|
1319
|
-
const config = readVoluteConfig(dir);
|
|
1250
|
+
const config = readVoluteConfig(dir) ?? {};
|
|
1320
1251
|
config.connectors = (config.connectors ?? []).filter((t) => t !== type);
|
|
1321
1252
|
writeVoluteConfig(dir, config);
|
|
1322
1253
|
return c.json({ ok: true });
|
|
@@ -1332,17 +1263,22 @@ var app5 = new Hono5().get("/:name/conversations", async (c) => {
|
|
|
1332
1263
|
return c.json(convs);
|
|
1333
1264
|
}).get("/:name/conversations/:id/messages", async (c) => {
|
|
1334
1265
|
const id = c.req.param("id");
|
|
1266
|
+
const user = c.get("user");
|
|
1267
|
+
const conv = await getConversationForUser(id, user.id);
|
|
1268
|
+
if (!conv) return c.json({ error: "Conversation not found" }, 404);
|
|
1335
1269
|
const msgs = await getMessages(id);
|
|
1336
1270
|
return c.json(msgs);
|
|
1337
1271
|
}).delete("/:name/conversations/:id", async (c) => {
|
|
1338
1272
|
const id = c.req.param("id");
|
|
1339
|
-
|
|
1273
|
+
const user = c.get("user");
|
|
1274
|
+
const deleted = await deleteConversationForUser(id, user.id);
|
|
1275
|
+
if (!deleted) return c.json({ error: "Conversation not found" }, 404);
|
|
1340
1276
|
return c.json({ ok: true });
|
|
1341
1277
|
});
|
|
1342
1278
|
var conversations_default = app5;
|
|
1343
1279
|
|
|
1344
1280
|
// src/web/routes/files.ts
|
|
1345
|
-
import { existsSync as
|
|
1281
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1346
1282
|
import { readdir, readFile, writeFile } from "fs/promises";
|
|
1347
1283
|
import { resolve as resolve5 } from "path";
|
|
1348
1284
|
import { zValidator as zValidator3 } from "@hono/zod-validator";
|
|
@@ -1356,7 +1292,7 @@ var app6 = new Hono6().get("/:name/files", async (c) => {
|
|
|
1356
1292
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1357
1293
|
const dir = agentDir(name);
|
|
1358
1294
|
const homeDir = resolve5(dir, "home");
|
|
1359
|
-
if (!
|
|
1295
|
+
if (!existsSync5(homeDir)) return c.json({ error: "Home directory missing" }, 404);
|
|
1360
1296
|
const allFiles = await readdir(homeDir);
|
|
1361
1297
|
const files = allFiles.filter((f) => f.endsWith(".md") && ALLOWED_FILES.has(f));
|
|
1362
1298
|
return c.json(files);
|
|
@@ -1370,7 +1306,7 @@ var app6 = new Hono6().get("/:name/files", async (c) => {
|
|
|
1370
1306
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1371
1307
|
const dir = agentDir(name);
|
|
1372
1308
|
const filePath = resolve5(dir, "home", filename);
|
|
1373
|
-
if (!
|
|
1309
|
+
if (!existsSync5(filePath)) {
|
|
1374
1310
|
return c.json({ error: "File not found" }, 404);
|
|
1375
1311
|
}
|
|
1376
1312
|
const content = await readFile(filePath, "utf-8");
|
|
@@ -1392,8 +1328,8 @@ var app6 = new Hono6().get("/:name/files", async (c) => {
|
|
|
1392
1328
|
var files_default = app6;
|
|
1393
1329
|
|
|
1394
1330
|
// src/web/routes/logs.ts
|
|
1395
|
-
import { spawn as
|
|
1396
|
-
import { existsSync as
|
|
1331
|
+
import { spawn as spawn2 } from "child_process";
|
|
1332
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1397
1333
|
import { resolve as resolve6 } from "path";
|
|
1398
1334
|
import { Hono as Hono7 } from "hono";
|
|
1399
1335
|
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
@@ -1403,11 +1339,11 @@ var app7 = new Hono7().get("/:name/logs", async (c) => {
|
|
|
1403
1339
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1404
1340
|
const dir = agentDir(name);
|
|
1405
1341
|
const logFile = resolve6(dir, ".volute", "logs", "agent.log");
|
|
1406
|
-
if (!
|
|
1342
|
+
if (!existsSync6(logFile)) {
|
|
1407
1343
|
return c.json({ error: "No log file found" }, 404);
|
|
1408
1344
|
}
|
|
1409
1345
|
return streamSSE2(c, async (stream2) => {
|
|
1410
|
-
const tail =
|
|
1346
|
+
const tail = spawn2("tail", ["-n", "200", "-f", logFile]);
|
|
1411
1347
|
const onData = (data) => {
|
|
1412
1348
|
const lines = data.toString().split("\n");
|
|
1413
1349
|
for (const line of lines) {
|
|
@@ -1432,11 +1368,11 @@ var logs_default = app7;
|
|
|
1432
1368
|
// src/web/routes/schedules.ts
|
|
1433
1369
|
import { Hono as Hono8 } from "hono";
|
|
1434
1370
|
function readSchedules(name) {
|
|
1435
|
-
return readVoluteConfig(agentDir(name))
|
|
1371
|
+
return readVoluteConfig(agentDir(name))?.schedules ?? [];
|
|
1436
1372
|
}
|
|
1437
1373
|
function writeSchedules(name, schedules) {
|
|
1438
1374
|
const dir = agentDir(name);
|
|
1439
|
-
const config = readVoluteConfig(dir);
|
|
1375
|
+
const config = readVoluteConfig(dir) ?? {};
|
|
1440
1376
|
config.schedules = schedules.length > 0 ? schedules : void 0;
|
|
1441
1377
|
writeVoluteConfig(dir, config);
|
|
1442
1378
|
getScheduler().loadSchedules(name);
|
|
@@ -1445,7 +1381,7 @@ var app8 = new Hono8().get("/:name/schedules", (c) => {
|
|
|
1445
1381
|
const name = c.req.param("name");
|
|
1446
1382
|
if (!findAgent(name)) return c.json({ error: "Agent not found" }, 404);
|
|
1447
1383
|
return c.json(readSchedules(name));
|
|
1448
|
-
}).post("/:name/schedules", async (c) => {
|
|
1384
|
+
}).post("/:name/schedules", requireAdmin, async (c) => {
|
|
1449
1385
|
const name = c.req.param("name");
|
|
1450
1386
|
if (!findAgent(name)) return c.json({ error: "Agent not found" }, 404);
|
|
1451
1387
|
const body = await c.req.json();
|
|
@@ -1460,7 +1396,7 @@ var app8 = new Hono8().get("/:name/schedules", (c) => {
|
|
|
1460
1396
|
schedules.push({ id, cron: body.cron, message: body.message, enabled: body.enabled ?? true });
|
|
1461
1397
|
writeSchedules(name, schedules);
|
|
1462
1398
|
return c.json({ ok: true, id }, 201);
|
|
1463
|
-
}).put("/:name/schedules/:id", async (c) => {
|
|
1399
|
+
}).put("/:name/schedules/:id", requireAdmin, async (c) => {
|
|
1464
1400
|
const name = c.req.param("name");
|
|
1465
1401
|
const id = c.req.param("id");
|
|
1466
1402
|
if (!findAgent(name)) return c.json({ error: "Agent not found" }, 404);
|
|
@@ -1473,7 +1409,7 @@ var app8 = new Hono8().get("/:name/schedules", (c) => {
|
|
|
1473
1409
|
if (body.enabled !== void 0) schedules[idx].enabled = body.enabled;
|
|
1474
1410
|
writeSchedules(name, schedules);
|
|
1475
1411
|
return c.json({ ok: true });
|
|
1476
|
-
}).delete("/:name/schedules/:id", (c) => {
|
|
1412
|
+
}).delete("/:name/schedules/:id", requireAdmin, (c) => {
|
|
1477
1413
|
const name = c.req.param("name");
|
|
1478
1414
|
const id = c.req.param("id");
|
|
1479
1415
|
if (!findAgent(name)) return c.json({ error: "Agent not found" }, 404);
|
|
@@ -1583,6 +1519,7 @@ app11.use("*", async (c, next) => {
|
|
|
1583
1519
|
app11.get("/api/health", (c) => {
|
|
1584
1520
|
return c.json({ ok: true, version: "0.1.0" });
|
|
1585
1521
|
});
|
|
1522
|
+
app11.use("/api/*", bodyLimit({ maxSize: 10 * 1024 * 1024 }));
|
|
1586
1523
|
app11.use("/api/*", csrf());
|
|
1587
1524
|
app11.use("/api/agents/*", authMiddleware);
|
|
1588
1525
|
app11.use("/api/system/*", authMiddleware);
|
|
@@ -1599,16 +1536,19 @@ var MIME_TYPES = {
|
|
|
1599
1536
|
".png": "image/png",
|
|
1600
1537
|
".ico": "image/x-icon"
|
|
1601
1538
|
};
|
|
1602
|
-
async function startServer({
|
|
1539
|
+
async function startServer({
|
|
1540
|
+
port,
|
|
1541
|
+
hostname = "127.0.0.1"
|
|
1542
|
+
}) {
|
|
1603
1543
|
let assetsDir = "";
|
|
1604
|
-
let searchDir =
|
|
1544
|
+
let searchDir = dirname4(new URL(import.meta.url).pathname);
|
|
1605
1545
|
for (let i = 0; i < 5; i++) {
|
|
1606
1546
|
const candidate = resolve7(searchDir, "dist", "web-assets");
|
|
1607
|
-
if (
|
|
1547
|
+
if (existsSync7(candidate)) {
|
|
1608
1548
|
assetsDir = candidate;
|
|
1609
1549
|
break;
|
|
1610
1550
|
}
|
|
1611
|
-
searchDir =
|
|
1551
|
+
searchDir = dirname4(searchDir);
|
|
1612
1552
|
}
|
|
1613
1553
|
if (assetsDir) {
|
|
1614
1554
|
app_default.get("*", async (c) => {
|
|
@@ -1631,10 +1571,10 @@ async function startServer({ port }) {
|
|
|
1631
1571
|
return c.text("Not found", 404);
|
|
1632
1572
|
});
|
|
1633
1573
|
}
|
|
1634
|
-
const server = serve({ fetch: app_default.fetch, port });
|
|
1574
|
+
const server = serve({ fetch: app_default.fetch, port, hostname });
|
|
1635
1575
|
await new Promise((resolve9, reject) => {
|
|
1636
1576
|
server.on("listening", () => {
|
|
1637
|
-
logger_default.info("Volute UI running", { port });
|
|
1577
|
+
logger_default.info("Volute UI running", { hostname, port });
|
|
1638
1578
|
resolve9();
|
|
1639
1579
|
});
|
|
1640
1580
|
server.on("error", (err) => {
|
|
@@ -1645,17 +1585,18 @@ async function startServer({ port }) {
|
|
|
1645
1585
|
}
|
|
1646
1586
|
|
|
1647
1587
|
// src/daemon.ts
|
|
1648
|
-
var DAEMON_PID_PATH = resolve8(VOLUTE_HOME, "daemon.pid");
|
|
1649
|
-
var DAEMON_JSON_PATH = resolve8(VOLUTE_HOME, "daemon.json");
|
|
1650
1588
|
async function startDaemon(opts) {
|
|
1651
|
-
const { port } = opts;
|
|
1589
|
+
const { port, hostname } = opts;
|
|
1652
1590
|
const myPid = String(process.pid);
|
|
1653
|
-
|
|
1591
|
+
const home = voluteHome();
|
|
1592
|
+
const DAEMON_PID_PATH = resolve8(home, "daemon.pid");
|
|
1593
|
+
const DAEMON_JSON_PATH = resolve8(home, "daemon.json");
|
|
1594
|
+
mkdirSync3(home, { recursive: true });
|
|
1654
1595
|
const token = process.env.VOLUTE_DAEMON_TOKEN || randomBytes(32).toString("hex");
|
|
1655
1596
|
process.env.VOLUTE_DAEMON_TOKEN = token;
|
|
1656
1597
|
let server;
|
|
1657
1598
|
try {
|
|
1658
|
-
server = await startServer({ port });
|
|
1599
|
+
server = await startServer({ port, hostname });
|
|
1659
1600
|
} catch (err) {
|
|
1660
1601
|
const e = err;
|
|
1661
1602
|
if (e.code === "EADDRINUSE") {
|
|
@@ -1664,36 +1605,49 @@ async function startDaemon(opts) {
|
|
|
1664
1605
|
}
|
|
1665
1606
|
throw err;
|
|
1666
1607
|
}
|
|
1667
|
-
writeFileSync3(DAEMON_PID_PATH, myPid);
|
|
1608
|
+
writeFileSync3(DAEMON_PID_PATH, myPid, { mode: 384 });
|
|
1668
1609
|
writeFileSync3(DAEMON_JSON_PATH, `${JSON.stringify({ port, token }, null, 2)}
|
|
1669
|
-
|
|
1610
|
+
`, { mode: 384 });
|
|
1670
1611
|
const manager = initAgentManager();
|
|
1671
1612
|
const connectors = initConnectorManager();
|
|
1672
1613
|
const scheduler = getScheduler();
|
|
1673
|
-
scheduler.start();
|
|
1614
|
+
scheduler.start(port, token);
|
|
1674
1615
|
const registry = readRegistry();
|
|
1675
1616
|
for (const entry of registry) {
|
|
1676
1617
|
if (!entry.running) continue;
|
|
1677
1618
|
try {
|
|
1678
1619
|
await manager.startAgent(entry.name);
|
|
1679
1620
|
const dir = agentDir(entry.name);
|
|
1680
|
-
await connectors.startConnectors(entry.name, dir, entry.port);
|
|
1621
|
+
await connectors.startConnectors(entry.name, dir, entry.port, port);
|
|
1681
1622
|
scheduler.loadSchedules(entry.name);
|
|
1682
1623
|
} catch (err) {
|
|
1683
1624
|
console.error(`[daemon] failed to start agent ${entry.name}:`, err);
|
|
1684
1625
|
setAgentRunning(entry.name, false);
|
|
1685
1626
|
}
|
|
1686
1627
|
}
|
|
1687
|
-
|
|
1628
|
+
const runningVariants = getAllRunningVariants();
|
|
1629
|
+
for (const { agentName, variant } of runningVariants) {
|
|
1630
|
+
const compositeKey = `${agentName}@${variant.name}`;
|
|
1631
|
+
try {
|
|
1632
|
+
await manager.startAgent(compositeKey);
|
|
1633
|
+
} catch (err) {
|
|
1634
|
+
console.error(`[daemon] failed to start variant ${compositeKey}:`, err);
|
|
1635
|
+
setVariantRunning(agentName, variant.name, false);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
console.error(`[daemon] running on ${hostname}:${port}, pid ${myPid}`);
|
|
1688
1639
|
function cleanup() {
|
|
1689
1640
|
try {
|
|
1690
1641
|
if (readFileSync4(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
|
|
1691
|
-
|
|
1642
|
+
unlinkSync2(DAEMON_PID_PATH);
|
|
1692
1643
|
}
|
|
1693
1644
|
} catch {
|
|
1694
1645
|
}
|
|
1695
1646
|
try {
|
|
1696
|
-
|
|
1647
|
+
const data = JSON.parse(readFileSync4(DAEMON_JSON_PATH, "utf-8"));
|
|
1648
|
+
if (data.token === token) {
|
|
1649
|
+
unlinkSync2(DAEMON_JSON_PATH);
|
|
1650
|
+
}
|
|
1697
1651
|
} catch {
|
|
1698
1652
|
}
|
|
1699
1653
|
}
|
|
@@ -1715,16 +1669,20 @@ async function startDaemon(opts) {
|
|
|
1715
1669
|
}
|
|
1716
1670
|
if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith("daemon.ts")) {
|
|
1717
1671
|
let port = 4200;
|
|
1672
|
+
let hostname = "127.0.0.1";
|
|
1718
1673
|
let foreground = false;
|
|
1719
1674
|
for (let i = 2; i < process.argv.length; i++) {
|
|
1720
1675
|
if (process.argv[i] === "--port" && process.argv[i + 1]) {
|
|
1721
1676
|
port = parseInt(process.argv[i + 1], 10);
|
|
1722
1677
|
i++;
|
|
1678
|
+
} else if (process.argv[i] === "--host" && process.argv[i + 1]) {
|
|
1679
|
+
hostname = process.argv[i + 1];
|
|
1680
|
+
i++;
|
|
1723
1681
|
} else if (process.argv[i] === "--foreground") {
|
|
1724
1682
|
foreground = true;
|
|
1725
1683
|
}
|
|
1726
1684
|
}
|
|
1727
|
-
startDaemon({ port, foreground });
|
|
1685
|
+
startDaemon({ port, hostname, foreground });
|
|
1728
1686
|
}
|
|
1729
1687
|
export {
|
|
1730
1688
|
startDaemon
|