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.
Files changed (66) hide show
  1. package/README.md +1 -2
  2. package/dist/agent-manager-SSJUZWOV.js +13 -0
  3. package/dist/{channel-Q642YUZE.js → channel-2WJRM7PE.js} +2 -2
  4. package/dist/{chunk-H5XQARAP.js → chunk-4YXYAMFT.js} +3 -3
  5. package/dist/{chunk-5YW4B7CG.js → chunk-6UCG6MIX.js} +72 -23
  6. package/dist/{chunk-A5ZJEMHT.js → chunk-KFNNHQK7.js} +4 -4
  7. package/dist/chunk-L3BQEZ4Z.js +271 -0
  8. package/dist/{chunk-N4QN44LC.js → chunk-MY74SUOL.js} +29 -22
  9. package/dist/{chunk-KSMIWOCN.js → chunk-N4YNKR3Q.js} +6 -0
  10. package/dist/cli.js +23 -19
  11. package/dist/{connect-LW6G23AV.js → connect-X5V5IMRW.js} +3 -3
  12. package/dist/connectors/discord.js +9 -2
  13. package/dist/{create-3K6O2SDC.js → create-23AM7H5B.js} +1 -1
  14. package/dist/{daemon-client-ZTHW7ROS.js → daemon-client-VN24HM5T.js} +2 -2
  15. package/dist/daemon.js +394 -436
  16. package/dist/{delete-JNGY7ZFH.js → delete-GDMSOW3U.js} +2 -2
  17. package/dist/{disconnect-ACVTKTRE.js → disconnect-5JWFZ6RV.js} +2 -2
  18. package/dist/{down-FYCUYC5H.js → down-WTF73FE7.js} +5 -4
  19. package/dist/{env-7SLRN3MG.js → env-YKUJOFHE.js} +12 -5
  20. package/dist/{fork-BB3DZ426.js → fork-GRSVMBKI.js} +39 -32
  21. package/dist/history-7WVVKMUY.js +46 -0
  22. package/dist/{import-W2AMTEV5.js → import-42DOLBDT.js} +1 -1
  23. package/dist/{logs-BUHRIQ2L.js → logs-SYRQOL6B.js} +1 -1
  24. package/dist/{merge-446QTE7Q.js → merge-CSAVLSLY.js} +33 -36
  25. package/dist/{schedule-KKSOVUDF.js → schedule-J37XQM6E.js} +2 -2
  26. package/dist/{send-WQSVSRDD.js → send-PLOYEYER.js} +7 -5
  27. package/dist/{start-LKMWS6ZE.js → start-AG7QLULK.js} +2 -2
  28. package/dist/{status-CIEKUI3V.js → status-GCNU4M3K.js} +9 -2
  29. package/dist/{stop-YTOAGYE4.js → stop-IL5Q6NER.js} +2 -2
  30. package/dist/{up-AJJ4GCXY.js → up-ZC6G6K4K.js} +21 -37
  31. package/dist/{upgrade-JACA6YMO.js → upgrade-DD5TNJWU.js} +3 -5
  32. package/dist/{variants-HPY4DEWU.js → variants-QQIEKT6M.js} +2 -2
  33. package/drizzle/0000_flaky_mariko_yashida.sql +34 -0
  34. package/drizzle/0001_careless_warpath.sql +12 -0
  35. package/drizzle/meta/0000_snapshot.json +227 -0
  36. package/drizzle/meta/0001_snapshot.json +298 -0
  37. package/drizzle/meta/_journal.json +20 -0
  38. package/package.json +2 -1
  39. package/templates/_base/.init/.config/hooks/startup-context.sh +28 -0
  40. package/templates/_base/_skills/memory/SKILL.md +56 -13
  41. package/templates/_base/_skills/volute-agent/SKILL.md +27 -3
  42. package/templates/_base/home/VOLUTE.md +25 -0
  43. package/templates/_base/src/lib/format-prefix.ts +24 -0
  44. package/templates/_base/src/lib/sessions.ts +71 -0
  45. package/templates/_base/src/lib/startup.ts +132 -0
  46. package/templates/_base/src/lib/types.ts +3 -0
  47. package/templates/_base/src/lib/volute-server.ts +18 -2
  48. package/templates/agent-sdk/.init/.claude/settings.json +14 -0
  49. package/templates/agent-sdk/.init/.config/sessions.json +4 -0
  50. package/templates/agent-sdk/.init/CLAUDE.md +3 -2
  51. package/templates/agent-sdk/package.json.tmpl +1 -1
  52. package/templates/agent-sdk/src/agent.ts +101 -0
  53. package/templates/agent-sdk/src/lib/agent-sessions.ts +180 -0
  54. package/templates/agent-sdk/src/server.ts +33 -129
  55. package/templates/agent-sdk/volute-template.json +1 -1
  56. package/templates/pi/.init/.config/sessions.json +1 -0
  57. package/templates/pi/.init/AGENTS.md +2 -1
  58. package/templates/pi/src/agent.ts +61 -0
  59. package/templates/pi/src/lib/agent-sessions.ts +188 -0
  60. package/templates/pi/src/server.ts +28 -102
  61. package/templates/pi/volute-template.json +1 -1
  62. package/templates/agent-sdk/src/lib/agent.ts +0 -199
  63. package/templates/pi/src/lib/agent.ts +0 -205
  64. /package/templates/_base/.init/memory/{.gitkeep → journal/.gitkeep} +0 -0
  65. /package/templates/_base/{volute.json.tmpl → home/.config/volute.json.tmpl} +0 -0
  66. /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-KSMIWOCN.js";
10
+ } from "./chunk-N4YNKR3Q.js";
7
11
  import {
8
12
  loadMergedEnv
9
- } from "./chunk-A5ZJEMHT.js";
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
- } from "./chunk-5YW4B7CG.js";
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 unlinkSync3, writeFileSync as writeFileSync3 } from "fs";
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 as spawn2 } from "child_process";
36
+ import { spawn } from "child_process";
234
37
  import {
235
- createWriteStream as createWriteStream2,
236
- existsSync as existsSync3,
38
+ createWriteStream,
39
+ existsSync as existsSync2,
237
40
  mkdirSync as mkdirSync2,
238
- readFileSync as readFileSync3,
239
- unlinkSync as unlinkSync2,
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 resolve3 } from "path";
46
+ import { dirname as dirname2, resolve as resolve2 } from "path";
244
47
 
245
48
  // src/lib/volute-config.ts
246
- import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
247
- import { resolve as resolve2 } from "path";
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 = resolve2(agentDir2, "volute.json");
250
- if (!existsSync2(path)) return {};
59
+ const path = configPath(agentDir2);
60
+ if (!existsSync(path)) return {};
251
61
  try {
252
- return JSON.parse(readFileSync2(path, "utf-8"));
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 = resolve2(agentDir2, "volute.json");
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
- async startConnectors(agentName, agentDir2, agentPort) {
270
- const config = readVoluteConfig(agentDir2);
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 = resolve3(agentDir2, "connectors", type, "index.ts");
302
- const userConnector = resolve3(homedir(), ".volute", "connectors", type, "index.ts");
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 (existsSync3(agentConnector)) {
131
+ if (existsSync2(agentConnector)) {
307
132
  connectorScript = agentConnector;
308
- runtime = resolve3(agentDir2, "node_modules", ".bin", "tsx");
309
- } else if (existsSync3(userConnector)) {
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 && existsSync3(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 = resolve3(agentDir2, ".volute", "logs");
143
+ const logsDir = resolve2(agentDir2, ".volute", "logs");
319
144
  mkdirSync2(logsDir, { recursive: true });
320
- const logStream = createWriteStream2(resolve3(logsDir, `${type}.log`), { flags: "a" });
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 = spawn2(runtime, [connectorScript], {
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
- console.error(`[daemon] restarting connector ${type} for ${agentName} in 3s`);
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
- }, 3e3);
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 resolve3(agentDir2, ".volute", "connectors", `${type}.pid`);
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(dirname(pidPath), { recursive: true });
260
+ mkdirSync2(dirname2(pidPath), { recursive: true });
419
261
  writeFileSync2(pidPath, String(pid));
420
262
  }
421
263
  removeConnectorPid(agentDir2, type) {
422
264
  try {
423
- unlinkSync2(this.connectorPidPath(agentDir2, type));
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 (!existsSync3(pidPath)) return;
271
+ if (!existsSync2(pidPath)) return;
430
272
  try {
431
- const pid = parseInt(readFileSync3(pidPath, "utf-8").trim(), 10);
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
- unlinkSync2(pidPath);
281
+ unlinkSync(pidPath);
440
282
  } catch {
441
283
  }
442
284
  }
443
285
  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;
286
+ return searchUpwards("connectors", `${type}.js`);
451
287
  }
452
288
  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";
289
+ return searchUpwards("node_modules", ".bin", "tsx") ?? "tsx";
460
290
  }
461
291
  };
462
- var instance2 = null;
292
+ var instance = null;
463
293
  function initConnectorManager() {
464
- if (instance2) throw new Error("ConnectorManager already initialized");
465
- instance2 = new ConnectorManager();
466
- return instance2;
294
+ if (instance) throw new Error("ConnectorManager already initialized");
295
+ instance = new ConnectorManager();
296
+ return instance;
467
297
  }
468
298
  function getConnectorManager() {
469
- if (!instance2) instance2 = new ConnectorManager();
470
- return instance2;
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
- start() {
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
- 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}`);
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 instance3 = null;
407
+ var instance2 = null;
547
408
  function getScheduler() {
548
- if (!instance3) instance3 = new Scheduler();
549
- return instance3;
409
+ if (!instance2) instance2 = new Scheduler();
410
+ return instance2;
550
411
  }
551
412
 
552
413
  // src/web/server.ts
553
- import { existsSync as existsSync8 } from "fs";
414
+ import { existsSync as existsSync7 } from "fs";
554
415
  import { readFile as readFile2, stat } from "fs/promises";
555
- import { dirname as dirname3, extname, resolve as resolve7 } from "path";
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 existsSync4 } from "fs";
573
- import { dirname as dirname2, resolve as resolve4 } from "path";
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 = dirname2(fileURLToPath(import.meta.url));
643
- var migrationsFolder = existsSync4(resolve4(__dirname, "../drizzle")) ? resolve4(__dirname, "../drizzle") : resolve4(__dirname, "../../drizzle");
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 || resolve4(VOLUTE_HOME, "volute.db");
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 existsSync5, rmSync } from "fs";
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
- async function getAgentStatus(name, _dir, port) {
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 connectorManager = getConnectorManager();
793
- const connectorStatuses = connectorManager.getConnectorStatus(name);
679
+ const connectorStatuses = getConnectorManager().getConnectorStatus(name);
794
680
  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
- }
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 dir = agentDir(entry.name);
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
- 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) => {
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 entry = findAgent(name);
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
- const dir = agentDir(name);
837
- if (!existsSync5(dir)) return c.json({ error: "Agent directory missing" }, 404);
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
- setAgentRunning(name, true);
845
- await getConnectorManager().startConnectors(name, dir, entry.port);
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 entry = findAgent(name);
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
- const dir = agentDir(name);
855
- if (!existsSync5(dir)) return c.json({ error: "Agent directory missing" }, 404);
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(name);
762
+ if (!variantName) await connectorManager.stopConnectors(baseName);
861
763
  await manager.stopAgent(name);
862
764
  }
863
765
  await manager.startAgent(name);
864
- setAgentRunning(name, true);
865
- await connectorManager.startConnectors(name, dir, entry.port);
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 entry = findAgent(name);
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(name);
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 && existsSync5(dir)) {
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 entry = findAgent(name);
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
- const manager = getAgentManager();
908
- if (!manager.isRunning(name)) {
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
- const db2 = await getDb();
826
+ let parsed = null;
913
827
  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
- });
828
+ parsed = JSON.parse(body);
925
829
  } catch {
926
830
  }
927
- const res = await fetch(`http://localhost:${entry.port}/message`, {
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
- const db3 = await getDb();
979
- await db3.insert(agentMessages).values({
980
- agent: name,
981
- channel,
982
- role: "assistant",
983
- content: textParts.join("")
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 getConversation(id) {
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 entry = findAgent(name);
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 getConversation(conversationId);
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(name, "web", {
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 userContent = [];
1117
+ const contentBlocks = [];
1182
1118
  if (body.message) {
1183
- userContent.push({ type: "text", text: body.message });
1119
+ contentBlocks.push({ type: "text", text: body.message });
1184
1120
  }
1185
1121
  if (body.images) {
1186
1122
  for (const img of body.images) {
1187
- userContent.push({ type: "image", media_type: img.media_type, data: img.data });
1123
+ contentBlocks.push({ type: "image", media_type: img.media_type, data: img.data });
1188
1124
  }
1189
1125
  }
1190
- await addMessage(conversationId, "user", user.username, userContent);
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: name,
1129
+ agent: baseName,
1195
1130
  channel: "web",
1196
1131
  role: "user",
1197
1132
  sender: user.username,
1198
- content: userText
1133
+ content: body.message ?? "[image]"
1199
1134
  });
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`, {
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: agentContent,
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
- const voluteEvent = event;
1231
- await stream2.writeSSE({ data: JSON.stringify(voluteEvent) });
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 += voluteEvent.content;
1160
+ last.text += event.content;
1236
1161
  } else {
1237
- assistantContent.push({ type: "text", text: voluteEvent.content });
1162
+ assistantContent.push({ type: "text", text: event.content });
1238
1163
  }
1239
- } else if (voluteEvent.type === "tool_use") {
1164
+ } else if (event.type === "tool_use") {
1240
1165
  assistantContent.push({
1241
1166
  type: "tool_use",
1242
- name: voluteEvent.name,
1243
- input: voluteEvent.input
1167
+ name: event.name,
1168
+ input: event.input
1244
1169
  });
1245
- } else if (voluteEvent.type === "tool_result") {
1170
+ } else if (event.type === "tool_result") {
1246
1171
  assistantContent.push({
1247
1172
  type: "tool_result",
1248
- output: voluteEvent.output,
1249
- ...voluteEvent.is_error ? { is_error: true } : {}
1173
+ output: event.output,
1174
+ ...event.is_error ? { is_error: true } : {}
1250
1175
  });
1251
1176
  }
1252
- if (voluteEvent.type === "done") {
1177
+ if (event.type === "done") {
1253
1178
  if (assistantContent.length > 0) {
1254
- await addMessage(conversationId, "assistant", name, assistantContent);
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
- const db3 = await getDb();
1258
- await db3.insert(agentMessages).values({
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
- await deleteConversation(id);
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 existsSync6 } from "fs";
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 (!existsSync6(homeDir)) return c.json({ error: "Home directory missing" }, 404);
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 (!existsSync6(filePath)) {
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 spawn3 } from "child_process";
1396
- import { existsSync as existsSync7 } from "fs";
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 (!existsSync7(logFile)) {
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 = spawn3("tail", ["-n", "200", "-f", logFile]);
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)).schedules ?? [];
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({ port }) {
1539
+ async function startServer({
1540
+ port,
1541
+ hostname = "127.0.0.1"
1542
+ }) {
1603
1543
  let assetsDir = "";
1604
- let searchDir = dirname3(new URL(import.meta.url).pathname);
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 (existsSync8(candidate)) {
1547
+ if (existsSync7(candidate)) {
1608
1548
  assetsDir = candidate;
1609
1549
  break;
1610
1550
  }
1611
- searchDir = dirname3(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
- mkdirSync3(VOLUTE_HOME, { recursive: true });
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
- console.error(`[daemon] running on port ${port}, pid ${myPid}`);
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
- unlinkSync3(DAEMON_PID_PATH);
1642
+ unlinkSync2(DAEMON_PID_PATH);
1692
1643
  }
1693
1644
  } catch {
1694
1645
  }
1695
1646
  try {
1696
- unlinkSync3(DAEMON_JSON_PATH);
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