volute 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +29 -29
  2. package/dist/agent-Z2B6EFEQ.js +75 -0
  3. package/dist/{agent-manager-AUCKMGPR.js → agent-manager-PXBKA2GK.js} +4 -4
  4. package/dist/channel-MK5OK2SI.js +113 -0
  5. package/dist/chunk-5X7HGB6L.js +107 -0
  6. package/dist/{chunk-YGFIWIOF.js → chunk-7L4AN5D4.js} +1 -1
  7. package/dist/{chunk-VRVVQIYY.js → chunk-AZEL2IEK.js} +1 -1
  8. package/dist/chunk-B3R6L2GW.js +24 -0
  9. package/dist/{chunk-DNOXHLE5.js → chunk-HE67X4T6.js} +1 -1
  10. package/dist/{chunk-I6OHXCMV.js → chunk-MW2KFO3B.js} +47 -9
  11. package/dist/chunk-MXUCNIBG.js +168 -0
  12. package/dist/chunk-SMISE4SV.js +226 -0
  13. package/dist/{chunk-SOZA2TLP.js → chunk-UAVD2AHX.js} +1 -1
  14. package/dist/{chunk-3C2XR4IY.js → chunk-UX25Z2ND.js} +113 -107
  15. package/dist/{chunk-GSPKUPKU.js → chunk-XUA3JUFK.js} +2 -1
  16. package/dist/chunk-ZYGKG6VC.js +22 -0
  17. package/dist/cli.js +98 -75
  18. package/dist/connector-LYEMXQEV.js +157 -0
  19. package/dist/connectors/discord.js +104 -161
  20. package/dist/connectors/slack.js +179 -0
  21. package/dist/connectors/telegram.js +175 -0
  22. package/dist/conversation-ERXEQZTY.js +163 -0
  23. package/dist/create-RVCZN6HE.js +91 -0
  24. package/dist/{daemon-client-XR24PUJF.js → daemon-client-ZY6UUN2M.js} +2 -2
  25. package/dist/daemon.js +824 -252
  26. package/dist/{delete-GQ7JEK2S.js → delete-3QH7VYIN.js} +8 -9
  27. package/dist/{down-3OB6UVAJ.js → down-O7IFZLVJ.js} +1 -1
  28. package/dist/{env-JB27UAC3.js → env-4D4REPJF.js} +8 -5
  29. package/dist/{history-3VRUBGGV.js → history-OEONB53Z.js} +5 -5
  30. package/dist/{import-K4MP2GX7.js → import-MXJB2EII.js} +23 -8
  31. package/dist/{logs-NXFFGUKY.js → logs-DF342W4M.js} +2 -2
  32. package/dist/message-ADHWFHSI.js +32 -0
  33. package/dist/package-VQOE7JNH.js +89 -0
  34. package/dist/{schedule-4I5TYHFH.js → schedule-NAG6F463.js} +12 -7
  35. package/dist/send-66QMKRUH.js +75 -0
  36. package/dist/{setup-SRS7AUAA.js → setup-RPRRGG2F.js} +6 -6
  37. package/dist/{start-LDPMCMYT.js → start-TUOXDSFL.js} +3 -3
  38. package/dist/{status-MVSQG54T.js → status-A36EHRO4.js} +3 -3
  39. package/dist/{stop-5PZTZCLL.js → stop-AOJZLQ5X.js} +6 -7
  40. package/dist/{up-UT3IMKCA.js → up-7ILD7GU7.js} +2 -2
  41. package/dist/update-LPSIAWQ2.js +140 -0
  42. package/dist/update-check-Y33QDCFL.js +17 -0
  43. package/dist/{upgrade-CDKECCGN.js → upgrade-FX2TKJ2S.js} +16 -15
  44. package/dist/{variant-CVYM3EQG.js → variant-LAB67OC2.js} +17 -12
  45. package/dist/web-assets/assets/index-BbRmoxoA.js +308 -0
  46. package/dist/web-assets/index.html +2 -2
  47. package/drizzle/0003_clean_ego.sql +12 -0
  48. package/drizzle/meta/0003_snapshot.json +417 -0
  49. package/drizzle/meta/_journal.json +7 -0
  50. package/package.json +3 -1
  51. package/templates/_base/.init/.config/hooks/startup-context.sh +19 -1
  52. package/templates/_base/_skills/volute-agent/SKILL.md +112 -16
  53. package/templates/_base/home/.config/routes.json +10 -0
  54. package/templates/_base/home/VOLUTE.md +19 -28
  55. package/templates/_base/src/lib/file-handler.ts +46 -0
  56. package/templates/_base/src/lib/format-prefix.ts +1 -1
  57. package/templates/_base/src/lib/router.ts +327 -0
  58. package/templates/_base/src/lib/routing.ts +137 -0
  59. package/templates/_base/src/lib/types.ts +16 -3
  60. package/templates/_base/src/lib/volute-server.ts +20 -48
  61. package/templates/agent-sdk/.init/.config/routes.json +5 -0
  62. package/templates/agent-sdk/.init/CLAUDE.md +2 -2
  63. package/templates/agent-sdk/src/agent.ts +269 -82
  64. package/templates/agent-sdk/src/server.ts +19 -4
  65. package/templates/agent-sdk/volute-template.json +1 -1
  66. package/templates/pi/.init/.config/routes.json +5 -0
  67. package/templates/pi/.init/AGENTS.md +1 -1
  68. package/templates/pi/src/agent.ts +279 -58
  69. package/templates/pi/src/server.ts +15 -4
  70. package/templates/pi/volute-template.json +1 -1
  71. package/dist/channel-7FZ6D25H.js +0 -90
  72. package/dist/chunk-N4YNKR3Q.js +0 -90
  73. package/dist/connector-TVJULIRT.js +0 -96
  74. package/dist/create-BRG2DBWI.js +0 -79
  75. package/dist/send-UK3JBZIB.js +0 -53
  76. package/dist/web-assets/assets/index-BC5eSqbY.js +0 -296
  77. package/templates/_base/src/lib/sessions.ts +0 -71
  78. package/templates/agent-sdk/.init/.config/sessions.json +0 -4
  79. package/templates/agent-sdk/src/lib/agent-sessions.ts +0 -204
  80. package/templates/pi/.init/.config/sessions.json +0 -1
  81. package/templates/pi/src/lib/agent-sessions.ts +0 -210
  82. package/dist/{service-SA4TTMDU.js → service-HZNIDNJF.js} +3 -3
package/dist/daemon.js CHANGED
@@ -1,26 +1,36 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ RotatingLog,
3
4
  clearJsonMap,
4
5
  getAgentManager,
5
6
  initAgentManager,
6
7
  loadJsonMap,
7
8
  saveJsonMap
8
- } from "./chunk-I6OHXCMV.js";
9
+ } from "./chunk-MW2KFO3B.js";
9
10
  import {
10
- logBuffer,
11
- logger_default,
12
- readNdjson
13
- } from "./chunk-N4YNKR3Q.js";
11
+ checkForUpdate,
12
+ checkForUpdateCached,
13
+ getCurrentVersion
14
+ } from "./chunk-5X7HGB6L.js";
15
+ import {
16
+ CHANNELS
17
+ } from "./chunk-SMISE4SV.js";
18
+ import {
19
+ collectPart
20
+ } from "./chunk-B3R6L2GW.js";
14
21
  import {
15
22
  readVoluteConfig,
16
23
  writeVoluteConfig
17
24
  } from "./chunk-NETNFBA5.js";
18
25
  import {
19
26
  loadMergedEnv
20
- } from "./chunk-DNOXHLE5.js";
27
+ } from "./chunk-HE67X4T6.js";
21
28
  import {
22
29
  applyIsolation
23
- } from "./chunk-SOZA2TLP.js";
30
+ } from "./chunk-UAVD2AHX.js";
31
+ import {
32
+ resolveVoluteBin
33
+ } from "./chunk-5SKQ6J7T.js";
24
34
  import {
25
35
  agentDir,
26
36
  checkHealth,
@@ -34,32 +44,101 @@ import {
34
44
  setAgentRunning,
35
45
  setVariantRunning,
36
46
  voluteHome
37
- } from "./chunk-3C2XR4IY.js";
47
+ } from "./chunk-UX25Z2ND.js";
38
48
  import {
39
49
  __export
40
50
  } from "./chunk-K3NQKI34.js";
41
51
 
42
52
  // src/daemon.ts
43
53
  import { randomBytes } from "crypto";
44
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
45
- import { resolve as resolve8 } from "path";
54
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
55
+ import { homedir } from "os";
56
+ import { resolve as resolve10 } from "path";
57
+ import { format } from "util";
46
58
 
47
59
  // src/lib/connector-manager.ts
48
60
  import { spawn } from "child_process";
49
- import {
50
- createWriteStream,
51
- existsSync,
52
- mkdirSync,
53
- readFileSync,
54
- unlinkSync,
55
- writeFileSync
56
- } from "fs";
57
- import { dirname, resolve } from "path";
61
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
62
+ import { dirname, resolve as resolve2 } from "path";
63
+
64
+ // src/lib/connector-defs.ts
65
+ import { existsSync, readFileSync } from "fs";
66
+ import { resolve } from "path";
67
+ var BUILTIN_DEFS = {
68
+ discord: {
69
+ displayName: "Discord",
70
+ description: "Connect to Discord as a bot",
71
+ envVars: [
72
+ {
73
+ name: "DISCORD_TOKEN",
74
+ required: true,
75
+ description: "Discord bot token",
76
+ scope: "agent"
77
+ },
78
+ {
79
+ name: "DISCORD_GUILD_ID",
80
+ required: false,
81
+ description: "Discord server ID (optional, for slash commands)",
82
+ scope: "agent"
83
+ }
84
+ ]
85
+ },
86
+ slack: {
87
+ displayName: "Slack",
88
+ description: "Connect to Slack via Socket Mode",
89
+ envVars: [
90
+ {
91
+ name: "SLACK_BOT_TOKEN",
92
+ required: true,
93
+ description: "Slack bot token (xoxb-...)",
94
+ scope: "agent"
95
+ },
96
+ {
97
+ name: "SLACK_APP_TOKEN",
98
+ required: true,
99
+ description: "Slack app-level token (xapp-...) for Socket Mode",
100
+ scope: "agent"
101
+ }
102
+ ]
103
+ },
104
+ telegram: {
105
+ displayName: "Telegram",
106
+ description: "Connect to Telegram via long polling",
107
+ envVars: [
108
+ {
109
+ name: "TELEGRAM_BOT_TOKEN",
110
+ required: true,
111
+ description: "Telegram bot token from BotFather",
112
+ scope: "agent"
113
+ }
114
+ ]
115
+ }
116
+ };
117
+ function getConnectorDef(type, connectorDir) {
118
+ if (BUILTIN_DEFS[type]) return BUILTIN_DEFS[type];
119
+ if (connectorDir) {
120
+ const jsonPath = resolve(connectorDir, "connector.json");
121
+ if (existsSync(jsonPath)) {
122
+ try {
123
+ return JSON.parse(readFileSync(jsonPath, "utf-8"));
124
+ } catch (err) {
125
+ console.warn(`Failed to parse ${jsonPath}: ${err}`);
126
+ return null;
127
+ }
128
+ }
129
+ }
130
+ return null;
131
+ }
132
+ function checkMissingEnvVars(def, env) {
133
+ return def.envVars.filter((v) => v.required && !env[v.name]);
134
+ }
135
+
136
+ // src/lib/connector-manager.ts
58
137
  function searchUpwards(...segments) {
59
138
  let searchDir = dirname(new URL(import.meta.url).pathname);
60
139
  for (let i = 0; i < 5; i++) {
61
- const candidate = resolve(searchDir, ...segments);
62
- if (existsSync(candidate)) return candidate;
140
+ const candidate = resolve2(searchDir, ...segments);
141
+ if (existsSync2(candidate)) return candidate;
63
142
  searchDir = dirname(searchDir);
64
143
  }
65
144
  return null;
@@ -85,6 +164,20 @@ var ConnectorManager = class {
85
164
  }
86
165
  }
87
166
  }
167
+ checkConnectorEnv(type, agentDir2) {
168
+ const agentConnectorDir = resolve2(agentDir2, "connectors", type);
169
+ const userConnectorDir = resolve2(voluteHome(), "connectors", type);
170
+ const connectorDir = existsSync2(agentConnectorDir) ? agentConnectorDir : existsSync2(userConnectorDir) ? userConnectorDir : void 0;
171
+ const def = getConnectorDef(type, connectorDir);
172
+ if (!def) return null;
173
+ const env = loadMergedEnv(agentDir2);
174
+ const missing = checkMissingEnvVars(def, env);
175
+ if (missing.length === 0) return null;
176
+ return {
177
+ missing: missing.map((v) => ({ name: v.name, description: v.description })),
178
+ connectorName: def.displayName
179
+ };
180
+ }
88
181
  async startConnector(agentName, agentDir2, agentPort, type, daemonPort) {
89
182
  const existing = this.connectors.get(agentName)?.get(type);
90
183
  if (existing) {
@@ -106,15 +199,15 @@ var ConnectorManager = class {
106
199
  this.connectors.get(agentName)?.delete(type);
107
200
  }
108
201
  this.killOrphanConnector(agentDir2, type);
109
- const agentConnector = resolve(agentDir2, "connectors", type, "index.ts");
110
- const userConnector = resolve(voluteHome(), "connectors", type, "index.ts");
202
+ const agentConnector = resolve2(agentDir2, "connectors", type, "index.ts");
203
+ const userConnector = resolve2(voluteHome(), "connectors", type, "index.ts");
111
204
  const builtinConnector = this.resolveBuiltinConnector(type);
112
205
  let connectorScript;
113
206
  let runtime;
114
- if (existsSync(agentConnector)) {
207
+ if (existsSync2(agentConnector)) {
115
208
  connectorScript = agentConnector;
116
- runtime = resolve(agentDir2, "node_modules", ".bin", "tsx");
117
- } else if (existsSync(userConnector)) {
209
+ runtime = resolve2(agentDir2, "node_modules", ".bin", "tsx");
210
+ } else if (existsSync2(userConnector)) {
118
211
  connectorScript = userConnector;
119
212
  runtime = this.resolveVoluteTsx();
120
213
  } else if (builtinConnector) {
@@ -123,9 +216,9 @@ var ConnectorManager = class {
123
216
  } else {
124
217
  throw new Error(`No connector code found for type: ${type}`);
125
218
  }
126
- const logsDir = resolve(agentDir2, ".volute", "logs");
219
+ const logsDir = resolve2(agentDir2, ".volute", "logs");
127
220
  mkdirSync(logsDir, { recursive: true });
128
- const logStream = createWriteStream(resolve(logsDir, `${type}.log`), { flags: "a" });
221
+ const logStream = new RotatingLog(resolve2(logsDir, `${type}.log`));
129
222
  const agentEnv = loadMergedEnv(agentDir2);
130
223
  const prefix = `${type.toUpperCase()}_`;
131
224
  const connectorEnv = Object.fromEntries(
@@ -137,6 +230,7 @@ var ConnectorManager = class {
137
230
  ...process.env,
138
231
  VOLUTE_AGENT_PORT: String(agentPort),
139
232
  VOLUTE_AGENT_NAME: agentName,
233
+ VOLUTE_AGENT_DIR: agentDir2,
140
234
  ...daemonPort ? {
141
235
  VOLUTE_DAEMON_URL: `http://127.0.0.1:${daemonPort}`,
142
236
  VOLUTE_DAEMON_TOKEN: process.env.VOLUTE_DAEMON_TOKEN
@@ -146,8 +240,12 @@ var ConnectorManager = class {
146
240
  };
147
241
  await applyIsolation(spawnOpts, agentName);
148
242
  const child = spawn(runtime, [connectorScript], spawnOpts);
243
+ let lastStderr = "";
149
244
  child.stdout?.pipe(logStream);
150
- child.stderr?.pipe(logStream);
245
+ child.stderr?.on("data", (chunk) => {
246
+ logStream.write(chunk);
247
+ lastStderr = chunk.toString().trim();
248
+ });
151
249
  if (child.pid) {
152
250
  this.saveConnectorPid(agentDir2, type, child.pid);
153
251
  }
@@ -165,6 +263,7 @@ var ConnectorManager = class {
165
263
  if (this.shuttingDown) return;
166
264
  if (this.stopping.has(stopKey)) return;
167
265
  console.error(`[daemon] connector ${type} for ${agentName} exited with code ${code}`);
266
+ if (lastStderr) console.error(`[daemon] last output: ${lastStderr}`);
168
267
  const attempts = this.restartAttempts.get(stopKey) ?? 0;
169
268
  if (attempts >= MAX_RESTART_ATTEMPTS) {
170
269
  console.error(
@@ -194,19 +293,19 @@ var ConnectorManager = class {
194
293
  const stopKey = `${agentName}:${type}`;
195
294
  this.stopping.add(stopKey);
196
295
  agentMap.delete(type);
197
- await new Promise((resolve9) => {
198
- tracked.child.on("exit", () => resolve9());
296
+ await new Promise((resolve11) => {
297
+ tracked.child.on("exit", () => resolve11());
199
298
  try {
200
299
  tracked.child.kill("SIGTERM");
201
300
  } catch {
202
- resolve9();
301
+ resolve11();
203
302
  }
204
303
  setTimeout(() => {
205
304
  try {
206
305
  tracked.child.kill("SIGKILL");
207
306
  } catch {
208
307
  }
209
- resolve9();
308
+ resolve11();
210
309
  }, 5e3);
211
310
  });
212
311
  this.stopping.delete(stopKey);
@@ -238,7 +337,7 @@ var ConnectorManager = class {
238
337
  }));
239
338
  }
240
339
  connectorPidPath(agentDir2, type) {
241
- return resolve(agentDir2, ".volute", "connectors", `${type}.pid`);
340
+ return resolve2(agentDir2, ".volute", "connectors", `${type}.pid`);
242
341
  }
243
342
  saveConnectorPid(agentDir2, type, pid) {
244
343
  const pidPath = this.connectorPidPath(agentDir2, type);
@@ -253,9 +352,9 @@ var ConnectorManager = class {
253
352
  }
254
353
  killOrphanConnector(agentDir2, type) {
255
354
  const pidPath = this.connectorPidPath(agentDir2, type);
256
- if (!existsSync(pidPath)) return;
355
+ if (!existsSync2(pidPath)) return;
257
356
  try {
258
- const pid = parseInt(readFileSync(pidPath, "utf-8").trim(), 10);
357
+ const pid = parseInt(readFileSync2(pidPath, "utf-8").trim(), 10);
259
358
  if (pid > 0) {
260
359
  process.kill(pid, "SIGTERM");
261
360
  console.error(`[daemon] killed orphan connector ${type} (pid ${pid})`);
@@ -286,7 +385,7 @@ function getConnectorManager() {
286
385
  }
287
386
 
288
387
  // src/lib/scheduler.ts
289
- import { resolve as resolve2 } from "path";
388
+ import { resolve as resolve3 } from "path";
290
389
  import { CronExpressionParser } from "cron-parser";
291
390
  var Scheduler = class {
292
391
  schedules = /* @__PURE__ */ new Map();
@@ -296,7 +395,7 @@ var Scheduler = class {
296
395
  daemonPort = null;
297
396
  daemonToken = null;
298
397
  get statePath() {
299
- return resolve2(voluteHome(), "scheduler-state.json");
398
+ return resolve3(voluteHome(), "scheduler-state.json");
300
399
  }
301
400
  start(daemonPort, daemonToken) {
302
401
  this.daemonPort = daemonPort ?? null;
@@ -374,6 +473,8 @@ var Scheduler = class {
374
473
  channel: "system:scheduler",
375
474
  sender: schedule.id
376
475
  });
476
+ const controller = new AbortController();
477
+ const timeout = setTimeout(() => controller.abort(), 12e4);
377
478
  try {
378
479
  let res;
379
480
  if (this.daemonPort && this.daemonToken) {
@@ -385,13 +486,15 @@ var Scheduler = class {
385
486
  Authorization: `Bearer ${this.daemonToken}`,
386
487
  Origin: daemonUrl
387
488
  },
388
- body
489
+ body,
490
+ signal: controller.signal
389
491
  });
390
492
  } else {
391
493
  res = await fetch(`http://127.0.0.1:${entry.port}/message`, {
392
494
  method: "POST",
393
495
  headers: { "Content-Type": "application/json" },
394
- body
496
+ body,
497
+ signal: controller.signal
395
498
  });
396
499
  }
397
500
  if (!res.ok) {
@@ -399,8 +502,22 @@ var Scheduler = class {
399
502
  } else {
400
503
  console.error(`[scheduler] fired "${schedule.id}" for ${agentName}`);
401
504
  }
505
+ try {
506
+ const reader = res.body?.getReader();
507
+ if (reader) {
508
+ try {
509
+ while (!(await reader.read()).done) {
510
+ }
511
+ } finally {
512
+ reader.releaseLock();
513
+ }
514
+ }
515
+ } catch {
516
+ }
402
517
  } catch (err) {
403
518
  console.error(`[scheduler] failed to fire "${schedule.id}" for ${agentName}:`, err);
519
+ } finally {
520
+ clearTimeout(timeout);
404
521
  }
405
522
  }
406
523
  };
@@ -421,8 +538,8 @@ import { compareSync, hashSync } from "bcryptjs";
421
538
  import { and, count, eq } from "drizzle-orm";
422
539
 
423
540
  // src/lib/db.ts
424
- import { chmodSync, existsSync as existsSync2 } from "fs";
425
- import { dirname as dirname2, resolve as resolve3 } from "path";
541
+ import { chmodSync, existsSync as existsSync3 } from "fs";
542
+ import { dirname as dirname2, resolve as resolve4 } from "path";
426
543
  import { fileURLToPath } from "url";
427
544
  import { drizzle } from "drizzle-orm/libsql";
428
545
  import { migrate } from "drizzle-orm/libsql/migrator";
@@ -431,18 +548,20 @@ import { migrate } from "drizzle-orm/libsql/migrator";
431
548
  var schema_exports = {};
432
549
  __export(schema_exports, {
433
550
  agentMessages: () => agentMessages,
551
+ conversationParticipants: () => conversationParticipants,
434
552
  conversations: () => conversations,
435
553
  messages: () => messages,
436
554
  sessions: () => sessions,
437
555
  users: () => users
438
556
  });
439
557
  import { sql } from "drizzle-orm";
440
- import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
558
+ import { index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
441
559
  var users = sqliteTable("users", {
442
560
  id: integer("id").primaryKey({ autoIncrement: true }),
443
561
  username: text("username").unique().notNull(),
444
562
  password_hash: text("password_hash").notNull(),
445
563
  role: text("role").notNull().default("pending"),
564
+ user_type: text("user_type").notNull().default("human"),
446
565
  created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
447
566
  });
448
567
  var conversations = sqliteTable(
@@ -478,6 +597,19 @@ var agentMessages = sqliteTable(
478
597
  index("idx_agent_messages_channel").on(table.agent, table.channel)
479
598
  ]
480
599
  );
600
+ var conversationParticipants = sqliteTable(
601
+ "conversation_participants",
602
+ {
603
+ conversation_id: text("conversation_id").notNull().references(() => conversations.id, { onDelete: "cascade" }),
604
+ user_id: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
605
+ role: text("role").notNull().default("member"),
606
+ joined_at: text("joined_at").notNull().default(sql`(datetime('now'))`)
607
+ },
608
+ (table) => [
609
+ uniqueIndex("idx_cp_unique").on(table.conversation_id, table.user_id),
610
+ index("idx_cp_user_id").on(table.user_id)
611
+ ]
612
+ );
481
613
  var sessions = sqliteTable("sessions", {
482
614
  id: text("id").primaryKey(),
483
615
  userId: integer("user_id").references(() => users.id, { onDelete: "cascade" }).notNull(),
@@ -498,11 +630,11 @@ var messages = sqliteTable(
498
630
 
499
631
  // src/lib/db.ts
500
632
  var __dirname = dirname2(fileURLToPath(import.meta.url));
501
- var migrationsFolder = existsSync2(resolve3(__dirname, "../drizzle")) ? resolve3(__dirname, "../drizzle") : resolve3(__dirname, "../../drizzle");
633
+ var migrationsFolder = existsSync3(resolve4(__dirname, "../drizzle")) ? resolve4(__dirname, "../drizzle") : resolve4(__dirname, "../../drizzle");
502
634
  var db = null;
503
635
  async function getDb() {
504
636
  if (db) return db;
505
- const dbPath = process.env.VOLUTE_DB_PATH || resolve3(voluteHome(), "volute.db");
637
+ const dbPath = process.env.VOLUTE_DB_PATH || resolve4(voluteHome(), "volute.db");
506
638
  db = drizzle({ connection: { url: `file:${dbPath}` }, schema: schema_exports });
507
639
  await migrate(db, { migrationsFolder });
508
640
  try {
@@ -520,12 +652,13 @@ async function getDb() {
520
652
  async function createUser(username, password) {
521
653
  const db2 = await getDb();
522
654
  const hash = hashSync(password, 10);
523
- const [{ value }] = await db2.select({ value: count() }).from(users);
655
+ const [{ value }] = await db2.select({ value: count() }).from(users).where(eq(users.user_type, "human"));
524
656
  const role = value === 0 ? "admin" : "pending";
525
657
  const [result] = await db2.insert(users).values({ username, password_hash: hash, role }).returning({
526
658
  id: users.id,
527
659
  username: users.username,
528
660
  role: users.role,
661
+ user_type: users.user_type,
529
662
  created_at: users.created_at
530
663
  });
531
664
  return result;
@@ -534,6 +667,7 @@ async function verifyUser(username, password) {
534
667
  const db2 = await getDb();
535
668
  const row = await db2.select().from(users).where(eq(users.username, username)).get();
536
669
  if (!row) return null;
670
+ if (row.user_type === "agent") return null;
537
671
  if (!compareSync(password, row.password_hash)) return null;
538
672
  const { password_hash: _, ...user } = row;
539
673
  return user;
@@ -544,6 +678,7 @@ async function getUser(id) {
544
678
  id: users.id,
545
679
  username: users.username,
546
680
  role: users.role,
681
+ user_type: users.user_type,
547
682
  created_at: users.created_at
548
683
  }).from(users).where(eq(users.id, id)).get();
549
684
  return row ?? null;
@@ -554,6 +689,7 @@ async function getUserByUsername(username) {
554
689
  id: users.id,
555
690
  username: users.username,
556
691
  role: users.role,
692
+ user_type: users.user_type,
557
693
  created_at: users.created_at
558
694
  }).from(users).where(eq(users.username, username)).get();
559
695
  return row ?? null;
@@ -564,6 +700,7 @@ async function listUsers() {
564
700
  id: users.id,
565
701
  username: users.username,
566
702
  role: users.role,
703
+ user_type: users.user_type,
567
704
  created_at: users.created_at
568
705
  }).from(users).orderBy(users.created_at).all();
569
706
  }
@@ -573,9 +710,58 @@ async function listPendingUsers() {
573
710
  id: users.id,
574
711
  username: users.username,
575
712
  role: users.role,
713
+ user_type: users.user_type,
576
714
  created_at: users.created_at
577
715
  }).from(users).where(eq(users.role, "pending")).orderBy(users.created_at).all();
578
716
  }
717
+ async function listUsersByType(userType) {
718
+ const db2 = await getDb();
719
+ return db2.select({
720
+ id: users.id,
721
+ username: users.username,
722
+ role: users.role,
723
+ user_type: users.user_type,
724
+ created_at: users.created_at
725
+ }).from(users).where(eq(users.user_type, userType)).orderBy(users.created_at).all();
726
+ }
727
+ async function getOrCreateAgentUser(agentName) {
728
+ const db2 = await getDb();
729
+ const existing = await db2.select({
730
+ id: users.id,
731
+ username: users.username,
732
+ role: users.role,
733
+ user_type: users.user_type,
734
+ created_at: users.created_at
735
+ }).from(users).where(and(eq(users.username, agentName), eq(users.user_type, "agent"))).get();
736
+ if (existing) return existing;
737
+ try {
738
+ const [result] = await db2.insert(users).values({
739
+ username: agentName,
740
+ password_hash: "!agent",
741
+ role: "agent",
742
+ user_type: "agent"
743
+ }).returning({
744
+ id: users.id,
745
+ username: users.username,
746
+ role: users.role,
747
+ user_type: users.user_type,
748
+ created_at: users.created_at
749
+ });
750
+ return result;
751
+ } catch (err) {
752
+ if (err instanceof Error && err.message.includes("UNIQUE constraint")) {
753
+ const retried = await db2.select({
754
+ id: users.id,
755
+ username: users.username,
756
+ role: users.role,
757
+ user_type: users.user_type,
758
+ created_at: users.created_at
759
+ }).from(users).where(and(eq(users.username, agentName), eq(users.user_type, "agent"))).get();
760
+ if (retried) return retried;
761
+ }
762
+ throw err;
763
+ }
764
+ }
579
765
  async function approveUser(id) {
580
766
  const db2 = await getDb();
581
767
  await db2.update(users).set({ role: "user" }).where(and(eq(users.id, id), eq(users.role, "pending")));
@@ -625,7 +811,7 @@ var authMiddleware = createMiddleware(async (c, next) => {
625
811
  if (authHeader?.startsWith("Bearer ")) {
626
812
  const token = authHeader.slice(7);
627
813
  if (token && isValidDaemonToken(token)) {
628
- c.set("user", { id: 0, username: "cli", role: "admin" });
814
+ c.set("user", { id: 0, username: "cli", role: "admin", user_type: "human" });
629
815
  await next();
630
816
  return;
631
817
  }
@@ -642,37 +828,111 @@ var authMiddleware = createMiddleware(async (c, next) => {
642
828
  });
643
829
 
644
830
  // src/web/server.ts
645
- import { existsSync as existsSync6 } from "fs";
831
+ import { existsSync as existsSync7 } from "fs";
646
832
  import { readFile as readFile2, stat } from "fs/promises";
647
- import { dirname as dirname3, extname, resolve as resolve7 } from "path";
833
+ import { dirname as dirname3, extname, resolve as resolve9 } from "path";
648
834
  import { serve } from "@hono/node-server";
649
835
 
836
+ // src/lib/log-buffer.ts
837
+ var LogBuffer = class {
838
+ entries = [];
839
+ maxSize = 1e3;
840
+ subscribers = /* @__PURE__ */ new Set();
841
+ append(entry) {
842
+ this.entries.push(entry);
843
+ if (this.entries.length > this.maxSize) {
844
+ this.entries.shift();
845
+ }
846
+ for (const sub of this.subscribers) {
847
+ sub(entry);
848
+ }
849
+ }
850
+ getEntries() {
851
+ return [...this.entries];
852
+ }
853
+ subscribe(fn) {
854
+ this.subscribers.add(fn);
855
+ return () => this.subscribers.delete(fn);
856
+ }
857
+ };
858
+ var logBuffer = new LogBuffer();
859
+
860
+ // src/lib/logger.ts
861
+ function write(level, msg, data) {
862
+ const entry = {
863
+ level,
864
+ msg,
865
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
866
+ ...data ? { data } : {}
867
+ };
868
+ const line = JSON.stringify(entry);
869
+ process.stderr.write(`${line}
870
+ `);
871
+ logBuffer.append(entry);
872
+ }
873
+ var log = {
874
+ info: (msg, data) => write("info", msg, data),
875
+ warn: (msg, data) => write("warn", msg, data),
876
+ error: (msg, data) => write("error", msg, data)
877
+ };
878
+ var logger_default = log;
879
+
650
880
  // src/web/app.ts
651
- import { Hono as Hono11 } from "hono";
881
+ import { Hono as Hono13 } from "hono";
652
882
  import { bodyLimit } from "hono/body-limit";
653
883
  import { csrf } from "hono/csrf";
654
884
  import { HTTPException } from "hono/http-exception";
655
885
 
656
886
  // src/web/routes/agents.ts
657
- import { existsSync as existsSync3, readFileSync as readFileSync2, rmSync } from "fs";
658
- import { resolve as resolve4 } from "path";
887
+ import { existsSync as existsSync4, readFileSync as readFileSync3, rmSync } from "fs";
888
+ import { resolve as resolve5 } from "path";
659
889
  import { and as and2, desc, eq as eq3 } from "drizzle-orm";
660
890
  import { Hono } from "hono";
661
891
  import { stream } from "hono/streaming";
662
892
 
663
- // src/lib/channels.ts
664
- var CHANNELS = {
665
- web: { name: "web", displayName: "Web UI", showToolCalls: true },
666
- discord: { name: "discord", displayName: "Discord", showToolCalls: false },
667
- cli: { name: "cli", displayName: "CLI", showToolCalls: true },
668
- agent: { name: "agent", displayName: "Agent", showToolCalls: true },
669
- system: { name: "system", displayName: "System", showToolCalls: false }
670
- };
893
+ // src/lib/ndjson.ts
894
+ var MAX_BUFFER_SIZE = 1e6;
895
+ async function* readNdjson(body) {
896
+ const reader = body.getReader();
897
+ const decoder = new TextDecoder();
898
+ let buffer = "";
899
+ try {
900
+ while (true) {
901
+ const { done, value } = await reader.read();
902
+ if (done) break;
903
+ buffer += decoder.decode(value, { stream: true });
904
+ if (buffer.length > MAX_BUFFER_SIZE) {
905
+ logger_default.warn("ndjson: buffer exceeded 1MB, resetting");
906
+ buffer = "";
907
+ continue;
908
+ }
909
+ const lines = buffer.split("\n");
910
+ buffer = lines.pop() || "";
911
+ for (const line of lines) {
912
+ if (!line.trim()) continue;
913
+ try {
914
+ yield JSON.parse(line);
915
+ } catch {
916
+ logger_default.warn("ndjson: skipping invalid line", { line: line.slice(0, 100) });
917
+ }
918
+ }
919
+ }
920
+ if (buffer.trim()) {
921
+ try {
922
+ yield JSON.parse(buffer);
923
+ } catch {
924
+ logger_default.warn("ndjson: skipping invalid line", { line: buffer.slice(0, 100) });
925
+ }
926
+ }
927
+ } finally {
928
+ reader.releaseLock();
929
+ }
930
+ }
671
931
 
672
932
  // src/web/routes/agents.ts
673
933
  function getDaemonPort() {
674
934
  try {
675
- const data = JSON.parse(readFileSync2(resolve4(voluteHome(), "daemon.json"), "utf-8"));
935
+ const data = JSON.parse(readFileSync3(resolve5(voluteHome(), "daemon.json"), "utf-8"));
676
936
  return data.port;
677
937
  } catch {
678
938
  return void 0;
@@ -687,10 +947,10 @@ async function getAgentStatus(name, port) {
687
947
  }
688
948
  const channels = [];
689
949
  channels.push({
690
- name: CHANNELS.web.name,
691
- displayName: CHANNELS.web.displayName,
950
+ name: CHANNELS.volute.name,
951
+ displayName: CHANNELS.volute.displayName,
692
952
  status: status === "running" ? "connected" : "disconnected",
693
- showToolCalls: CHANNELS.web.showToolCalls
953
+ showToolCalls: CHANNELS.volute.showToolCalls
694
954
  });
695
955
  const connectorStatuses = getConnectorManager().getConnectorStatus(name);
696
956
  for (const cs of connectorStatuses) {
@@ -717,7 +977,7 @@ var app = new Hono().get("/", async (c) => {
717
977
  const name = c.req.param("name");
718
978
  const entry = findAgent(name);
719
979
  if (!entry) return c.json({ error: "Agent not found" }, 404);
720
- if (!existsSync3(agentDir(name))) return c.json({ error: "Agent directory missing" }, 404);
980
+ if (!existsSync4(agentDir(name))) return c.json({ error: "Agent directory missing" }, 404);
721
981
  const { status, channels } = await getAgentStatus(name, entry.port);
722
982
  const variants = readVariants(name);
723
983
  const manager = getAgentManager();
@@ -743,7 +1003,7 @@ var app = new Hono().get("/", async (c) => {
743
1003
  if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
744
1004
  } else {
745
1005
  const dir = agentDir(baseName);
746
- if (!existsSync3(dir)) return c.json({ error: "Agent directory missing" }, 404);
1006
+ if (!existsSync4(dir)) return c.json({ error: "Agent directory missing" }, 404);
747
1007
  }
748
1008
  const manager = getAgentManager();
749
1009
  if (manager.isRunning(name)) {
@@ -770,7 +1030,7 @@ var app = new Hono().get("/", async (c) => {
770
1030
  if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
771
1031
  } else {
772
1032
  const dir = agentDir(baseName);
773
- if (!existsSync3(dir)) return c.json({ error: "Agent directory missing" }, 404);
1033
+ if (!existsSync4(dir)) return c.json({ error: "Agent directory missing" }, 404);
774
1034
  }
775
1035
  const manager = getAgentManager();
776
1036
  const connectorManager = getConnectorManager();
@@ -825,7 +1085,7 @@ var app = new Hono().get("/", async (c) => {
825
1085
  }
826
1086
  removeAllVariants(name);
827
1087
  removeAgent(name);
828
- if (force && existsSync3(dir)) {
1088
+ if (force && existsSync4(dir)) {
829
1089
  rmSync(dir, { recursive: true, force: true });
830
1090
  }
831
1091
  return c.json({ ok: true });
@@ -847,14 +1107,22 @@ var app = new Hono().get("/", async (c) => {
847
1107
  let parsed = null;
848
1108
  try {
849
1109
  parsed = JSON.parse(body);
850
- } catch {
1110
+ } catch (err) {
1111
+ console.error(`[daemon] failed to parse message body for ${baseName}:`, err);
851
1112
  }
852
1113
  const channel = parsed?.channel ?? "unknown";
853
1114
  const db2 = await getDb();
854
1115
  if (parsed) {
855
1116
  try {
856
1117
  const sender = parsed.sender ?? null;
857
- const content = typeof parsed.content === "string" ? parsed.content : JSON.stringify(parsed.content);
1118
+ let content;
1119
+ if (typeof parsed.content === "string") {
1120
+ content = parsed.content;
1121
+ } else if (Array.isArray(parsed.content)) {
1122
+ content = parsed.content.filter((p) => p.type === "text" && p.text).map((p) => p.text).join("\n");
1123
+ } else {
1124
+ content = JSON.stringify(parsed.content);
1125
+ }
858
1126
  await db2.insert(agentMessages).values({
859
1127
  agent: baseName,
860
1128
  channel,
@@ -866,11 +1134,17 @@ var app = new Hono().get("/", async (c) => {
866
1134
  console.error(`[daemon] failed to persist inbound message for ${baseName}:`, err);
867
1135
  }
868
1136
  }
869
- const res = await fetch(`http://127.0.0.1:${port}/message`, {
870
- method: "POST",
871
- headers: { "Content-Type": "application/json" },
872
- body
873
- });
1137
+ let res;
1138
+ try {
1139
+ res = await fetch(`http://127.0.0.1:${port}/message`, {
1140
+ method: "POST",
1141
+ headers: { "Content-Type": "application/json" },
1142
+ body
1143
+ });
1144
+ } catch (err) {
1145
+ console.error(`[daemon] agent ${name} unreachable on port ${port}:`, err);
1146
+ return c.json({ error: "Agent is not reachable" }, 502);
1147
+ }
874
1148
  if (!res.ok) {
875
1149
  return c.json({ error: `Agent responded with ${res.status}` }, res.status);
876
1150
  }
@@ -878,57 +1152,39 @@ var app = new Hono().get("/", async (c) => {
878
1152
  return c.json({ error: "No response body from agent" }, 502);
879
1153
  }
880
1154
  c.header("Content-Type", "application/x-ndjson");
1155
+ const encoder = new TextEncoder();
881
1156
  return stream(c, async (s) => {
882
- const reader = res.body.getReader();
883
- const decoder = new TextDecoder();
884
- let buffer = "";
885
1157
  const textParts = [];
886
- try {
887
- while (true) {
888
- const { done, value } = await reader.read();
889
- if (done) break;
890
- await s.write(value);
891
- buffer += decoder.decode(value, { stream: true });
892
- const lines = buffer.split("\n");
893
- buffer = lines.pop() || "";
894
- for (const line of lines) {
895
- if (!line.trim()) continue;
896
- try {
897
- const event = JSON.parse(line);
898
- if (event.type === "text") {
899
- textParts.push(event.content);
900
- }
901
- } catch {
902
- console.warn(`[daemon] malformed NDJSON line from ${baseName}`);
903
- }
904
- }
905
- }
906
- if (buffer.trim()) {
907
- try {
908
- const event = JSON.parse(buffer);
909
- if (event.type === "text") {
910
- textParts.push(event.content);
911
- }
912
- } catch {
913
- console.warn(`[daemon] malformed NDJSON trailing data from ${baseName}`);
914
- }
1158
+ const toolParts = [];
1159
+ for await (const event of readNdjson(res.body)) {
1160
+ await s.write(encoder.encode(`${JSON.stringify(event)}
1161
+ `));
1162
+ const part = collectPart(event);
1163
+ if (part != null) {
1164
+ if (event.type === "tool_use") toolParts.push(part);
1165
+ else textParts.push(part);
915
1166
  }
916
- if (textParts.length > 0) {
917
- try {
918
- await db2.insert(agentMessages).values({
919
- agent: baseName,
920
- channel,
921
- role: "assistant",
922
- content: textParts.join("")
923
- });
924
- } catch (err) {
925
- console.error(`[daemon] failed to persist assistant response for ${baseName}:`, err);
926
- }
1167
+ }
1168
+ const content = [textParts.join(""), ...toolParts].filter(Boolean).join("\n");
1169
+ if (content) {
1170
+ try {
1171
+ await db2.insert(agentMessages).values({
1172
+ agent: baseName,
1173
+ channel,
1174
+ role: "assistant",
1175
+ sender: baseName,
1176
+ content
1177
+ });
1178
+ } catch (err) {
1179
+ console.error(`[daemon] failed to persist assistant response for ${baseName}:`, err);
927
1180
  }
928
- } finally {
929
- reader.releaseLock();
930
1181
  }
931
1182
  });
1183
+ }).get("/:name/history/channels", async (c) => {
1184
+ const name = c.req.param("name");
1185
+ const db2 = await getDb();
1186
+ const rows = await db2.selectDistinct({ channel: agentMessages.channel }).from(agentMessages).where(eq3(agentMessages.agent, name));
1187
+ return c.json(rows.map((r) => r.channel));
932
1188
  }).get("/:name/history", async (c) => {
933
1189
  const name = c.req.param("name");
934
1190
  const channel = c.req.query("channel");
@@ -956,6 +1212,14 @@ var credentialsSchema = z.object({
956
1212
  var admin = new Hono2().use(authMiddleware).get("/users", async (c) => {
957
1213
  const user = c.get("user");
958
1214
  if (user.role !== "admin") return c.json({ error: "Forbidden" }, 403);
1215
+ const agents = readRegistry();
1216
+ for (const agent of agents) {
1217
+ await getOrCreateAgentUser(agent.name);
1218
+ }
1219
+ const type = c.req.query("type");
1220
+ if (type === "human" || type === "agent") {
1221
+ return c.json(await listUsersByType(type));
1222
+ }
959
1223
  return c.json(await listUsers());
960
1224
  }).get("/users/pending", async (c) => {
961
1225
  const user = c.get("user");
@@ -1008,6 +1272,8 @@ var app2 = new Hono2().post("/register", zValidator("json", credentialsSchema),
1008
1272
  var auth_default = app2;
1009
1273
 
1010
1274
  // src/web/routes/chat.ts
1275
+ import { readFileSync as readFileSync4 } from "fs";
1276
+ import { resolve as resolve6 } from "path";
1011
1277
  import { zValidator as zValidator2 } from "@hono/zod-validator";
1012
1278
  import { Hono as Hono3 } from "hono";
1013
1279
  import { streamSSE } from "hono/streaming";
@@ -1015,7 +1281,7 @@ import { z as z2 } from "zod";
1015
1281
 
1016
1282
  // src/lib/conversations.ts
1017
1283
  import { randomUUID } from "crypto";
1018
- import { and as and3, desc as desc2, eq as eq4, isNull, sql as sql2 } from "drizzle-orm";
1284
+ import { and as and3, desc as desc2, eq as eq4, inArray, isNull, sql as sql2 } from "drizzle-orm";
1019
1285
  async function createConversation(agentName, channel, opts) {
1020
1286
  const db2 = await getDb();
1021
1287
  const id = randomUUID();
@@ -1026,6 +1292,15 @@ async function createConversation(agentName, channel, opts) {
1026
1292
  user_id: opts?.userId ?? null,
1027
1293
  title: opts?.title ?? null
1028
1294
  });
1295
+ if (opts?.participantIds && opts.participantIds.length > 0) {
1296
+ await db2.insert(conversationParticipants).values(
1297
+ opts.participantIds.map((uid, i) => ({
1298
+ conversation_id: id,
1299
+ user_id: uid,
1300
+ role: i === 0 ? "owner" : "member"
1301
+ }))
1302
+ );
1303
+ }
1029
1304
  return {
1030
1305
  id,
1031
1306
  agent_name: agentName,
@@ -1036,24 +1311,44 @@ async function createConversation(agentName, channel, opts) {
1036
1311
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
1037
1312
  };
1038
1313
  }
1039
- async function getConversationForUser(id, userId) {
1314
+ async function getParticipants(conversationId) {
1040
1315
  const db2 = await getDb();
1041
- const row = await db2.select().from(conversations).where(and3(eq4(conversations.id, id), eq4(conversations.user_id, userId))).get();
1042
- return row ?? null;
1316
+ const rows = await db2.select({
1317
+ userId: conversationParticipants.user_id,
1318
+ username: users.username,
1319
+ userType: users.user_type,
1320
+ role: conversationParticipants.role
1321
+ }).from(conversationParticipants).innerJoin(users, eq4(conversationParticipants.user_id, users.id)).where(eq4(conversationParticipants.conversation_id, conversationId)).all();
1322
+ return rows;
1323
+ }
1324
+ async function isParticipant(conversationId, userId) {
1325
+ const db2 = await getDb();
1326
+ const row = await db2.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(
1327
+ and3(
1328
+ eq4(conversationParticipants.conversation_id, conversationId),
1329
+ eq4(conversationParticipants.user_id, userId)
1330
+ )
1331
+ ).get();
1332
+ return row != null;
1333
+ }
1334
+ async function listConversationsForUser(userId) {
1335
+ const db2 = await getDb();
1336
+ const participantRows = await db2.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(eq4(conversationParticipants.user_id, userId)).all();
1337
+ if (participantRows.length === 0) return [];
1338
+ const convIds = participantRows.map((r) => r.conversation_id);
1339
+ return db2.select().from(conversations).where(inArray(conversations.id, convIds)).orderBy(desc2(conversations.updated_at)).all();
1340
+ }
1341
+ async function isParticipantOrOwner(conversationId, userId) {
1342
+ if (await isParticipant(conversationId, userId)) return true;
1343
+ const db2 = await getDb();
1344
+ const row = await db2.select().from(conversations).where(and3(eq4(conversations.id, conversationId), eq4(conversations.user_id, userId))).get();
1345
+ return row != null;
1043
1346
  }
1044
1347
  async function deleteConversationForUser(id, userId) {
1045
- const conv = await getConversationForUser(id, userId);
1046
- if (!conv) return false;
1348
+ if (!await isParticipantOrOwner(id, userId)) return false;
1047
1349
  await deleteConversation(id);
1048
1350
  return true;
1049
1351
  }
1050
- async function listConversations(agentName, opts) {
1051
- const db2 = await getDb();
1052
- if (opts?.userId != null) {
1053
- return db2.select().from(conversations).where(and3(eq4(conversations.agent_name, agentName), eq4(conversations.user_id, opts.userId))).orderBy(desc2(conversations.updated_at)).all();
1054
- }
1055
- return db2.select().from(conversations).where(eq4(conversations.agent_name, agentName)).orderBy(desc2(conversations.updated_at)).all();
1056
- }
1057
1352
  async function addMessage(conversationId, role, senderName, content) {
1058
1353
  const db2 = await getDb();
1059
1354
  const serialized = JSON.stringify(content);
@@ -1089,6 +1384,34 @@ async function getMessages(conversationId) {
1089
1384
  return { ...row, content };
1090
1385
  });
1091
1386
  }
1387
+ async function listConversationsWithParticipants(userId) {
1388
+ const convs = await listConversationsForUser(userId);
1389
+ if (convs.length === 0) return [];
1390
+ const db2 = await getDb();
1391
+ const convIds = convs.map((c) => c.id);
1392
+ const rows = await db2.select({
1393
+ conversationId: conversationParticipants.conversation_id,
1394
+ userId: users.id,
1395
+ username: users.username,
1396
+ userType: users.user_type,
1397
+ role: conversationParticipants.role
1398
+ }).from(conversationParticipants).innerJoin(users, eq4(conversationParticipants.user_id, users.id)).where(inArray(conversationParticipants.conversation_id, convIds));
1399
+ const byConv = /* @__PURE__ */ new Map();
1400
+ for (const r of rows) {
1401
+ let arr = byConv.get(r.conversationId);
1402
+ if (!arr) {
1403
+ arr = [];
1404
+ byConv.set(r.conversationId, arr);
1405
+ }
1406
+ arr.push({
1407
+ userId: r.userId,
1408
+ username: r.username,
1409
+ userType: r.userType,
1410
+ role: r.role
1411
+ });
1412
+ }
1413
+ return convs.map((c) => ({ ...c, participants: byConv.get(c.id) ?? [] }));
1414
+ }
1092
1415
  async function deleteConversation(id) {
1093
1416
  const db2 = await getDb();
1094
1417
  await db2.delete(conversations).where(eq4(conversations.id, id));
@@ -1098,6 +1421,7 @@ async function deleteConversation(id) {
1098
1421
  var chatSchema = z2.object({
1099
1422
  message: z2.string().optional(),
1100
1423
  conversationId: z2.string().optional(),
1424
+ sender: z2.string().optional(),
1101
1425
  images: z2.array(
1102
1426
  z2.object({
1103
1427
  media_type: z2.string(),
@@ -1105,18 +1429,59 @@ var chatSchema = z2.object({
1105
1429
  })
1106
1430
  ).optional()
1107
1431
  });
1432
+ function getDaemonUrl() {
1433
+ const data = JSON.parse(readFileSync4(resolve6(voluteHome(), "daemon.json"), "utf-8"));
1434
+ return `http://127.0.0.1:${data.port}`;
1435
+ }
1436
+ function daemonFetchInternal(path, body) {
1437
+ const daemonUrl = getDaemonUrl();
1438
+ const token = process.env.VOLUTE_DAEMON_TOKEN;
1439
+ const headers = {
1440
+ "Content-Type": "application/json",
1441
+ Origin: daemonUrl
1442
+ };
1443
+ if (token) headers.Authorization = `Bearer ${token}`;
1444
+ return fetch(`${daemonUrl}${path}`, { method: "POST", headers, body });
1445
+ }
1446
+ function accumulateEvent(content, event) {
1447
+ if (event.type === "text") {
1448
+ const last = content[content.length - 1];
1449
+ if (last && last.type === "text") last.text += event.content;
1450
+ else content.push({ type: "text", text: event.content });
1451
+ } else if (event.type === "tool_use") {
1452
+ content.push({ type: "tool_use", name: event.name, input: event.input });
1453
+ } else if (event.type === "tool_result") {
1454
+ content.push({
1455
+ type: "tool_result",
1456
+ output: event.output,
1457
+ ...event.is_error ? { is_error: true } : {}
1458
+ });
1459
+ }
1460
+ }
1461
+ async function consumeAndPersist(res, conversationId, agentName) {
1462
+ if (!res.body) {
1463
+ console.warn(`[chat] no response body from ${agentName}`);
1464
+ return [];
1465
+ }
1466
+ const assistantContent = [];
1467
+ for await (const event of readNdjson(res.body)) {
1468
+ accumulateEvent(assistantContent, event);
1469
+ if (event.type === "done") break;
1470
+ }
1471
+ if (assistantContent.length === 0) return [];
1472
+ try {
1473
+ await addMessage(conversationId, "assistant", agentName, assistantContent);
1474
+ } catch (err) {
1475
+ console.error(`[chat] failed to persist conversation message from ${agentName}:`, err);
1476
+ }
1477
+ return assistantContent;
1478
+ }
1108
1479
  var app3 = new Hono3().post("/:name/chat", zValidator2("json", chatSchema), async (c) => {
1109
1480
  const name = c.req.param("name");
1110
- const [baseName, variantName] = name.split("@", 2);
1481
+ const [baseName] = name.split("@", 2);
1111
1482
  const entry = findAgent(baseName);
1112
1483
  if (!entry) return c.json({ error: "Agent not found" }, 404);
1113
- let port = entry.port;
1114
- if (variantName) {
1115
- const variant = findVariant(baseName, variantName);
1116
- if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
1117
- port = variant.port;
1118
- }
1119
- const { getAgentManager: getAgentManager2 } = await import("./agent-manager-AUCKMGPR.js");
1484
+ const { getAgentManager: getAgentManager2 } = await import("./agent-manager-PXBKA2GK.js");
1120
1485
  if (!getAgentManager2().isRunning(name)) {
1121
1486
  return c.json({ error: "Agent is not running" }, 409);
1122
1487
  }
@@ -1125,18 +1490,34 @@ var app3 = new Hono3().post("/:name/chat", zValidator2("json", chatSchema), asyn
1125
1490
  return c.json({ error: "message or images required" }, 400);
1126
1491
  }
1127
1492
  const user = c.get("user");
1493
+ const agentUser = await getOrCreateAgentUser(baseName);
1494
+ const senderName = user.id === 0 && body.sender ? body.sender : user.username;
1128
1495
  let conversationId = body.conversationId;
1129
1496
  if (conversationId) {
1130
- const conv = await getConversationForUser(conversationId, user.id);
1131
- if (!conv) return c.json({ error: "Conversation not found" }, 404);
1497
+ if (user.id !== 0 && !await isParticipantOrOwner(conversationId, user.id)) {
1498
+ return c.json({ error: "Conversation not found" }, 404);
1499
+ }
1132
1500
  } else {
1133
1501
  const title = body.message ? body.message.slice(0, 80) : "Image message";
1134
- const conv = await createConversation(baseName, "web", {
1135
- userId: user.id,
1136
- title
1502
+ const participantIds = [];
1503
+ if (user.id !== 0) {
1504
+ participantIds.push(user.id);
1505
+ } else if (body.sender) {
1506
+ const senderAgent = findAgent(body.sender);
1507
+ if (senderAgent) {
1508
+ const senderAgentUser = await getOrCreateAgentUser(body.sender);
1509
+ participantIds.push(senderAgentUser.id);
1510
+ }
1511
+ }
1512
+ participantIds.push(agentUser.id);
1513
+ const conv = await createConversation(baseName, "volute", {
1514
+ userId: user.id !== 0 ? user.id : void 0,
1515
+ title,
1516
+ participantIds
1137
1517
  });
1138
1518
  conversationId = conv.id;
1139
1519
  }
1520
+ const channel = `volute:${conversationId}`;
1140
1521
  const contentBlocks = [];
1141
1522
  if (body.message) {
1142
1523
  contentBlocks.push({ type: "text", text: body.message });
@@ -1146,73 +1527,87 @@ var app3 = new Hono3().post("/:name/chat", zValidator2("json", chatSchema), asyn
1146
1527
  contentBlocks.push({ type: "image", media_type: img.media_type, data: img.data });
1147
1528
  }
1148
1529
  }
1149
- await addMessage(conversationId, "user", user.username, contentBlocks);
1150
- const db2 = await getDb();
1151
- await db2.insert(agentMessages).values({
1152
- agent: baseName,
1153
- channel: "web",
1154
- role: "user",
1155
- sender: user.username,
1156
- content: body.message ?? "[image]"
1157
- });
1158
- const res = await fetch(`http://127.0.0.1:${port}/message`, {
1159
- method: "POST",
1160
- headers: { "Content-Type": "application/json" },
1161
- body: JSON.stringify({
1162
- content: contentBlocks,
1163
- channel: "web",
1164
- sender: user.username
1165
- })
1530
+ await addMessage(conversationId, "user", senderName, contentBlocks);
1531
+ const participants = await getParticipants(conversationId);
1532
+ const agentParticipants = participants.filter((p) => p.userType === "agent");
1533
+ const participantNames = participants.map((p) => p.username);
1534
+ const manager = getAgentManager2();
1535
+ const runningAgents = agentParticipants.map((ap) => {
1536
+ const agentKey = ap.username === baseName ? name : ap.username;
1537
+ return manager.isRunning(agentKey) ? ap.username : null;
1538
+ }).filter((n) => n !== null && n !== senderName);
1539
+ if (runningAgents.length === 0) {
1540
+ return c.json({ error: "No running agents in this conversation" }, 409);
1541
+ }
1542
+ const isDM = participants.length === 2;
1543
+ const payload = JSON.stringify({
1544
+ content: contentBlocks,
1545
+ channel,
1546
+ sender: senderName,
1547
+ participants: participantNames,
1548
+ participantCount: participants.length,
1549
+ isDM
1166
1550
  });
1167
- if (!res.ok) {
1168
- return c.json({ error: `Agent responded with ${res.status}` }, res.status);
1551
+ const responses = [];
1552
+ for (const agentName of runningAgents) {
1553
+ const targetName = agentName === baseName ? name : agentName;
1554
+ try {
1555
+ const res = await daemonFetchInternal(
1556
+ `/api/agents/${encodeURIComponent(targetName)}/message`,
1557
+ payload
1558
+ );
1559
+ if (res.ok && res.body) {
1560
+ responses.push({ name: agentName, res });
1561
+ } else {
1562
+ const errorBody = await res.text().catch(() => "");
1563
+ console.error(
1564
+ `[chat] agent ${agentName} responded with ${res.status}: ${errorBody.slice(0, 500)}`
1565
+ );
1566
+ }
1567
+ } catch (err) {
1568
+ console.error(`[chat] agent ${agentName} unreachable via daemon:`, err);
1569
+ }
1169
1570
  }
1170
- if (!res.body) {
1171
- return c.json({ error: "No response body from agent" }, 502);
1571
+ if (responses.length === 0) {
1572
+ return c.json({ error: "No agents reachable" }, 502);
1172
1573
  }
1574
+ const primary = responses[0];
1575
+ const secondary = responses.slice(1);
1576
+ const secondaryPromises = secondary.map((s) => consumeAndPersist(s.res, conversationId, s.name));
1173
1577
  return streamSSE(c, async (stream2) => {
1174
1578
  await stream2.writeSSE({
1175
- data: JSON.stringify({ type: "meta", conversationId })
1579
+ data: JSON.stringify({ type: "meta", conversationId, senderName: primary.name })
1176
1580
  });
1177
1581
  const assistantContent = [];
1178
- for await (const event of readNdjson(res.body)) {
1179
- await stream2.writeSSE({ data: JSON.stringify(event) });
1180
- if (event.type === "text") {
1181
- const last = assistantContent[assistantContent.length - 1];
1182
- if (last && last.type === "text") {
1183
- last.text += event.content;
1184
- } else {
1185
- assistantContent.push({ type: "text", text: event.content });
1186
- }
1187
- } else if (event.type === "tool_use") {
1188
- assistantContent.push({
1189
- type: "tool_use",
1190
- name: event.name,
1191
- input: event.input
1192
- });
1193
- } else if (event.type === "tool_result") {
1194
- assistantContent.push({
1195
- type: "tool_result",
1196
- output: event.output,
1197
- ...event.is_error ? { is_error: true } : {}
1198
- });
1582
+ try {
1583
+ for await (const event of readNdjson(primary.res.body)) {
1584
+ await stream2.writeSSE({ data: JSON.stringify(event) });
1585
+ accumulateEvent(assistantContent, event);
1586
+ if (event.type === "done") break;
1199
1587
  }
1200
- if (event.type === "done") {
1201
- if (assistantContent.length > 0) {
1202
- await addMessage(conversationId, "assistant", baseName, assistantContent);
1203
- const textParts = assistantContent.filter((b) => b.type === "text").map((b) => b.text);
1204
- if (textParts.length > 0) {
1205
- await db2.insert(agentMessages).values({
1206
- agent: baseName,
1207
- channel: "web",
1208
- role: "assistant",
1209
- content: textParts.join("")
1210
- });
1211
- }
1212
- }
1213
- break;
1588
+ } catch (err) {
1589
+ console.error(`[chat] error streaming response from ${primary.name}:`, err);
1590
+ await stream2.writeSSE({
1591
+ data: JSON.stringify({ type: "error", message: "Stream interrupted" })
1592
+ });
1593
+ }
1594
+ if (assistantContent.length > 0) {
1595
+ try {
1596
+ await addMessage(conversationId, "assistant", primary.name, assistantContent);
1597
+ } catch (err) {
1598
+ console.error(`[chat] failed to persist response from ${primary.name}:`, err);
1599
+ }
1600
+ }
1601
+ const results = await Promise.allSettled(secondaryPromises);
1602
+ for (let i = 0; i < results.length; i++) {
1603
+ if (results[i].status === "rejected") {
1604
+ console.error(
1605
+ `[chat] secondary agent ${secondary[i].name} response failed:`,
1606
+ results[i].reason
1607
+ );
1214
1608
  }
1215
1609
  }
1610
+ await stream2.writeSSE({ data: JSON.stringify({ type: "sync" }) });
1216
1611
  });
1217
1612
  });
1218
1613
  var chat_default = app3;
@@ -1243,13 +1638,24 @@ var app4 = new Hono4().get("/:name/connectors", (c) => {
1243
1638
  const entry = findAgent(name);
1244
1639
  if (!entry) return c.json({ error: "Agent not found" }, 404);
1245
1640
  const dir = agentDir(name);
1641
+ const manager = getConnectorManager();
1642
+ const envCheck = manager.checkConnectorEnv(type, dir);
1643
+ if (envCheck) {
1644
+ return c.json(
1645
+ {
1646
+ error: "missing_env",
1647
+ missing: envCheck.missing,
1648
+ connectorName: envCheck.connectorName
1649
+ },
1650
+ 400
1651
+ );
1652
+ }
1246
1653
  const config = readVoluteConfig(dir) ?? {};
1247
1654
  const connectors = config.connectors ?? [];
1248
1655
  if (!connectors.includes(type)) {
1249
1656
  config.connectors = [...connectors, type];
1250
1657
  writeVoluteConfig(dir, config);
1251
1658
  }
1252
- const manager = getConnectorManager();
1253
1659
  try {
1254
1660
  await manager.startConnector(name, dir, entry.port, type);
1255
1661
  return c.json({ ok: true });
@@ -1278,19 +1684,79 @@ var app4 = new Hono4().get("/:name/connectors", (c) => {
1278
1684
  var connectors_default = app4;
1279
1685
 
1280
1686
  // src/web/routes/conversations.ts
1687
+ import { zValidator as zValidator3 } from "@hono/zod-validator";
1281
1688
  import { Hono as Hono5 } from "hono";
1689
+ import { z as z3 } from "zod";
1690
+ var createConvSchema = z3.object({
1691
+ title: z3.string().optional(),
1692
+ participantIds: z3.array(z3.number()).optional(),
1693
+ participantNames: z3.array(z3.string()).optional()
1694
+ });
1282
1695
  var app5 = new Hono5().get("/:name/conversations", async (c) => {
1283
1696
  const name = c.req.param("name");
1284
1697
  const user = c.get("user");
1285
- const convs = await listConversations(name, { userId: user.id });
1698
+ let lookupId = user.id;
1699
+ if (user.id === 0) {
1700
+ const agentUser = await getOrCreateAgentUser(name);
1701
+ lookupId = agentUser.id;
1702
+ }
1703
+ const all = await listConversationsForUser(lookupId);
1704
+ const convs = all.filter((c2) => c2.agent_name === name);
1286
1705
  return c.json(convs);
1706
+ }).post("/:name/conversations", zValidator3("json", createConvSchema), async (c) => {
1707
+ const name = c.req.param("name");
1708
+ const user = c.get("user");
1709
+ const body = c.req.valid("json");
1710
+ if (!body.participantIds?.length && !body.participantNames?.length) {
1711
+ return c.json({ error: "participantIds or participantNames required" }, 400);
1712
+ }
1713
+ const agentUser = await getOrCreateAgentUser(name);
1714
+ const participantSet = /* @__PURE__ */ new Set();
1715
+ if (user.id !== 0) participantSet.add(user.id);
1716
+ participantSet.add(agentUser.id);
1717
+ for (const id of body.participantIds ?? []) participantSet.add(id);
1718
+ if (body.participantNames) {
1719
+ for (const pname of body.participantNames) {
1720
+ const existing = await getUserByUsername(pname);
1721
+ if (existing) {
1722
+ participantSet.add(existing.id);
1723
+ continue;
1724
+ }
1725
+ if (findAgent(pname)) {
1726
+ const au = await getOrCreateAgentUser(pname);
1727
+ participantSet.add(au.id);
1728
+ continue;
1729
+ }
1730
+ return c.json({ error: `User not found: ${pname}` }, 400);
1731
+ }
1732
+ }
1733
+ for (const id of participantSet) {
1734
+ if (id === user.id || id === agentUser.id) continue;
1735
+ const u = await getUser(id);
1736
+ if (!u) return c.json({ error: `User ${id} not found` }, 400);
1737
+ }
1738
+ const conv = await createConversation(name, "volute", {
1739
+ userId: user.id !== 0 ? user.id : void 0,
1740
+ title: body.title,
1741
+ participantIds: [...participantSet]
1742
+ });
1743
+ return c.json(conv, 201);
1287
1744
  }).get("/:name/conversations/:id/messages", async (c) => {
1288
1745
  const id = c.req.param("id");
1289
1746
  const user = c.get("user");
1290
- const conv = await getConversationForUser(id, user.id);
1291
- if (!conv) return c.json({ error: "Conversation not found" }, 404);
1747
+ if (user.id !== 0 && !await isParticipantOrOwner(id, user.id)) {
1748
+ return c.json({ error: "Conversation not found" }, 404);
1749
+ }
1292
1750
  const msgs = await getMessages(id);
1293
1751
  return c.json(msgs);
1752
+ }).get("/:name/conversations/:id/participants", async (c) => {
1753
+ const id = c.req.param("id");
1754
+ const user = c.get("user");
1755
+ if (!await isParticipantOrOwner(id, user.id)) {
1756
+ return c.json({ error: "Conversation not found" }, 404);
1757
+ }
1758
+ const participants = await getParticipants(id);
1759
+ return c.json(participants);
1294
1760
  }).delete("/:name/conversations/:id", async (c) => {
1295
1761
  const id = c.req.param("id");
1296
1762
  const user = c.get("user");
@@ -1301,21 +1767,21 @@ var app5 = new Hono5().get("/:name/conversations", async (c) => {
1301
1767
  var conversations_default = app5;
1302
1768
 
1303
1769
  // src/web/routes/files.ts
1304
- import { existsSync as existsSync4 } from "fs";
1770
+ import { existsSync as existsSync5 } from "fs";
1305
1771
  import { readdir, readFile, writeFile } from "fs/promises";
1306
- import { resolve as resolve5 } from "path";
1307
- import { zValidator as zValidator3 } from "@hono/zod-validator";
1772
+ import { resolve as resolve7 } from "path";
1773
+ import { zValidator as zValidator4 } from "@hono/zod-validator";
1308
1774
  import { Hono as Hono6 } from "hono";
1309
- import { z as z3 } from "zod";
1775
+ import { z as z4 } from "zod";
1310
1776
  var ALLOWED_FILES = /* @__PURE__ */ new Set(["SOUL.md", "MEMORY.md", "CLAUDE.md", "VOLUTE.md"]);
1311
- var saveFileSchema = z3.object({ content: z3.string() });
1777
+ var saveFileSchema = z4.object({ content: z4.string() });
1312
1778
  var app6 = new Hono6().get("/:name/files", async (c) => {
1313
1779
  const name = c.req.param("name");
1314
1780
  const entry = findAgent(name);
1315
1781
  if (!entry) return c.json({ error: "Agent not found" }, 404);
1316
1782
  const dir = agentDir(name);
1317
- const homeDir = resolve5(dir, "home");
1318
- if (!existsSync4(homeDir)) return c.json({ error: "Home directory missing" }, 404);
1783
+ const homeDir = resolve7(dir, "home");
1784
+ if (!existsSync5(homeDir)) return c.json({ error: "Home directory missing" }, 404);
1319
1785
  const allFiles = await readdir(homeDir);
1320
1786
  const files = allFiles.filter((f) => f.endsWith(".md") && ALLOWED_FILES.has(f));
1321
1787
  return c.json(files);
@@ -1328,13 +1794,13 @@ var app6 = new Hono6().get("/:name/files", async (c) => {
1328
1794
  const entry = findAgent(name);
1329
1795
  if (!entry) return c.json({ error: "Agent not found" }, 404);
1330
1796
  const dir = agentDir(name);
1331
- const filePath = resolve5(dir, "home", filename);
1332
- if (!existsSync4(filePath)) {
1797
+ const filePath = resolve7(dir, "home", filename);
1798
+ if (!existsSync5(filePath)) {
1333
1799
  return c.json({ error: "File not found" }, 404);
1334
1800
  }
1335
1801
  const content = await readFile(filePath, "utf-8");
1336
1802
  return c.json({ filename, content });
1337
- }).put("/:name/files/:filename", zValidator3("json", saveFileSchema), async (c) => {
1803
+ }).put("/:name/files/:filename", zValidator4("json", saveFileSchema), async (c) => {
1338
1804
  const name = c.req.param("name");
1339
1805
  const filename = c.req.param("filename");
1340
1806
  if (!ALLOWED_FILES.has(filename)) {
@@ -1343,7 +1809,7 @@ var app6 = new Hono6().get("/:name/files", async (c) => {
1343
1809
  const entry = findAgent(name);
1344
1810
  if (!entry) return c.json({ error: "Agent not found" }, 404);
1345
1811
  const dir = agentDir(name);
1346
- const filePath = resolve5(dir, "home", filename);
1812
+ const filePath = resolve7(dir, "home", filename);
1347
1813
  const { content } = c.req.valid("json");
1348
1814
  await writeFile(filePath, content);
1349
1815
  return c.json({ ok: true });
@@ -1352,8 +1818,8 @@ var files_default = app6;
1352
1818
 
1353
1819
  // src/web/routes/logs.ts
1354
1820
  import { spawn as spawn2 } from "child_process";
1355
- import { existsSync as existsSync5 } from "fs";
1356
- import { resolve as resolve6 } from "path";
1821
+ import { existsSync as existsSync6 } from "fs";
1822
+ import { resolve as resolve8 } from "path";
1357
1823
  import { Hono as Hono7 } from "hono";
1358
1824
  import { streamSSE as streamSSE2 } from "hono/streaming";
1359
1825
  var app7 = new Hono7().get("/:name/logs", async (c) => {
@@ -1361,8 +1827,8 @@ var app7 = new Hono7().get("/:name/logs", async (c) => {
1361
1827
  const entry = findAgent(name);
1362
1828
  if (!entry) return c.json({ error: "Agent not found" }, 404);
1363
1829
  const dir = agentDir(name);
1364
- const logFile = resolve6(dir, ".volute", "logs", "agent.log");
1365
- if (!existsSync5(logFile)) {
1830
+ const logFile = resolve8(dir, ".volute", "logs", "agent.log");
1831
+ if (!existsSync6(logFile)) {
1366
1832
  return c.json({ error: "No log file found" }, 404);
1367
1833
  }
1368
1834
  return streamSSE2(c, async (stream2) => {
@@ -1380,9 +1846,9 @@ var app7 = new Hono7().get("/:name/logs", async (c) => {
1380
1846
  stream2.onAbort(() => {
1381
1847
  tail.kill();
1382
1848
  });
1383
- await new Promise((resolve9) => {
1384
- tail.on("exit", resolve9);
1385
- stream2.onAbort(resolve9);
1849
+ await new Promise((resolve11) => {
1850
+ tail.on("exit", resolve11);
1851
+ stream2.onAbort(resolve11);
1386
1852
  });
1387
1853
  });
1388
1854
  });
@@ -1484,19 +1950,99 @@ var app9 = new Hono9().get("/logs", async (c) => {
1484
1950
  stream2.writeSSE({ data: JSON.stringify(entry) }).catch(() => {
1485
1951
  });
1486
1952
  });
1487
- await new Promise((resolve9) => {
1953
+ await new Promise((resolve11) => {
1488
1954
  stream2.onAbort(() => {
1489
1955
  unsubscribe();
1490
- resolve9();
1956
+ resolve11();
1491
1957
  });
1492
1958
  });
1493
1959
  });
1494
1960
  });
1495
1961
  var system_default = app9;
1496
1962
 
1497
- // src/web/routes/variants.ts
1963
+ // src/web/routes/update.ts
1964
+ import { spawn as spawn3 } from "child_process";
1498
1965
  import { Hono as Hono10 } from "hono";
1499
- var app10 = new Hono10().get("/:name/variants", async (c) => {
1966
+ var bin;
1967
+ var app10 = new Hono10().get("/update", async (c) => {
1968
+ const result = await checkForUpdate();
1969
+ return c.json(result);
1970
+ }).post("/update", requireAdmin, async (c) => {
1971
+ bin ??= resolveVoluteBin();
1972
+ const child = spawn3(bin, ["update"], {
1973
+ stdio: "ignore",
1974
+ detached: true
1975
+ });
1976
+ child.on("error", (err) => {
1977
+ logger_default.error("Update process error", { error: err.message });
1978
+ });
1979
+ child.unref();
1980
+ return c.json({ ok: true, message: "Updating..." });
1981
+ });
1982
+ var update_default = app10;
1983
+
1984
+ // src/web/routes/user-conversations.ts
1985
+ import { zValidator as zValidator5 } from "@hono/zod-validator";
1986
+ import { Hono as Hono11 } from "hono";
1987
+ import { z as z5 } from "zod";
1988
+ var createSchema = z5.object({
1989
+ title: z5.string().optional(),
1990
+ participantNames: z5.array(z5.string()).min(1)
1991
+ });
1992
+ var app11 = new Hono11().use("*", authMiddleware).get("/", async (c) => {
1993
+ const user = c.get("user");
1994
+ const convs = await listConversationsWithParticipants(user.id);
1995
+ return c.json(convs);
1996
+ }).get("/:id/messages", async (c) => {
1997
+ const id = c.req.param("id");
1998
+ const user = c.get("user");
1999
+ if (user.id !== 0 && !await isParticipantOrOwner(id, user.id)) {
2000
+ return c.json({ error: "Conversation not found" }, 404);
2001
+ }
2002
+ const msgs = await getMessages(id);
2003
+ return c.json(msgs);
2004
+ }).post("/", zValidator5("json", createSchema), async (c) => {
2005
+ const user = c.get("user");
2006
+ const body = c.req.valid("json");
2007
+ const participantIds = /* @__PURE__ */ new Set();
2008
+ if (user.id !== 0) participantIds.add(user.id);
2009
+ let firstAgentName;
2010
+ for (const name of body.participantNames) {
2011
+ const existing = await getUserByUsername(name);
2012
+ if (existing) {
2013
+ participantIds.add(existing.id);
2014
+ if (!firstAgentName && existing.user_type === "agent") firstAgentName = name;
2015
+ continue;
2016
+ }
2017
+ if (findAgent(name)) {
2018
+ const au = await getOrCreateAgentUser(name);
2019
+ participantIds.add(au.id);
2020
+ if (!firstAgentName) firstAgentName = name;
2021
+ continue;
2022
+ }
2023
+ return c.json({ error: `User not found: ${name}` }, 400);
2024
+ }
2025
+ if (!firstAgentName) {
2026
+ return c.json({ error: "At least one agent participant is required" }, 400);
2027
+ }
2028
+ const conv = await createConversation(firstAgentName, "volute", {
2029
+ userId: user.id !== 0 ? user.id : void 0,
2030
+ title: body.title,
2031
+ participantIds: [...participantIds]
2032
+ });
2033
+ return c.json(conv, 201);
2034
+ }).delete("/:id", async (c) => {
2035
+ const id = c.req.param("id");
2036
+ const user = c.get("user");
2037
+ const deleted = await deleteConversationForUser(id, user.id);
2038
+ if (!deleted) return c.json({ error: "Conversation not found" }, 404);
2039
+ return c.json({ ok: true });
2040
+ });
2041
+ var user_conversations_default = app11;
2042
+
2043
+ // src/web/routes/variants.ts
2044
+ import { Hono as Hono12 } from "hono";
2045
+ var app12 = new Hono12().get("/:name/variants", async (c) => {
1500
2046
  const name = c.req.param("name");
1501
2047
  const entry = findAgent(name);
1502
2048
  if (!entry) return c.json({ error: "Agent not found" }, 404);
@@ -1510,11 +2056,11 @@ var app10 = new Hono10().get("/:name/variants", async (c) => {
1510
2056
  );
1511
2057
  return c.json(results);
1512
2058
  });
1513
- var variants_default = app10;
2059
+ var variants_default = app12;
1514
2060
 
1515
2061
  // src/web/app.ts
1516
- var app11 = new Hono11();
1517
- app11.onError((err, c) => {
2062
+ var app13 = new Hono13();
2063
+ app13.onError((err, c) => {
1518
2064
  if (err instanceof HTTPException) {
1519
2065
  return err.getResponse();
1520
2066
  }
@@ -1525,10 +2071,10 @@ app11.onError((err, c) => {
1525
2071
  });
1526
2072
  return c.json({ error: "Internal server error" }, 500);
1527
2073
  });
1528
- app11.notFound((c) => {
2074
+ app13.notFound((c) => {
1529
2075
  return c.json({ error: "Not found" }, 404);
1530
2076
  });
1531
- app11.use("*", async (c, next) => {
2077
+ app13.use("*", async (c, next) => {
1532
2078
  const start = Date.now();
1533
2079
  await next();
1534
2080
  const duration = Date.now() - start;
@@ -1539,15 +2085,28 @@ app11.use("*", async (c, next) => {
1539
2085
  duration
1540
2086
  });
1541
2087
  });
1542
- app11.get("/api/health", (c) => {
1543
- return c.json({ ok: true, version: "0.1.0" });
2088
+ app13.get("/api/health", (c) => {
2089
+ let version = "unknown";
2090
+ let cached = null;
2091
+ try {
2092
+ version = getCurrentVersion();
2093
+ cached = checkForUpdateCached();
2094
+ } catch (err) {
2095
+ logger_default.error("Health check error", { error: err.message });
2096
+ }
2097
+ return c.json({
2098
+ ok: true,
2099
+ version,
2100
+ ...cached?.updateAvailable ? { updateAvailable: true, latest: cached.latest } : {}
2101
+ });
1544
2102
  });
1545
- app11.use("/api/*", bodyLimit({ maxSize: 10 * 1024 * 1024 }));
1546
- app11.use("/api/*", csrf());
1547
- app11.use("/api/agents/*", authMiddleware);
1548
- app11.use("/api/system/*", authMiddleware);
1549
- var routes = app11.route("/api/auth", auth_default).route("/api/system", system_default).route("/api/agents", agents_default).route("/api/agents", chat_default).route("/api/agents", connectors_default).route("/api/agents", schedules_default).route("/api/agents", logs_default).route("/api/agents", variants_default).route("/api/agents", files_default).route("/api/agents", conversations_default);
1550
- var app_default = app11;
2103
+ app13.use("/api/*", bodyLimit({ maxSize: 10 * 1024 * 1024 }));
2104
+ app13.use("/api/*", csrf());
2105
+ app13.use("/api/agents/*", authMiddleware);
2106
+ app13.use("/api/conversations/*", authMiddleware);
2107
+ app13.use("/api/system/*", authMiddleware);
2108
+ var routes = app13.route("/api/auth", auth_default).route("/api/system", system_default).route("/api/system", update_default).route("/api/agents", agents_default).route("/api/agents", chat_default).route("/api/agents", connectors_default).route("/api/agents", schedules_default).route("/api/agents", logs_default).route("/api/agents", variants_default).route("/api/agents", files_default).route("/api/agents", conversations_default).route("/api/conversations", user_conversations_default);
2109
+ var app_default = app13;
1551
2110
 
1552
2111
  // src/web/server.ts
1553
2112
  var MIME_TYPES = {
@@ -1566,8 +2125,8 @@ async function startServer({
1566
2125
  let assetsDir = "";
1567
2126
  let searchDir = dirname3(new URL(import.meta.url).pathname);
1568
2127
  for (let i = 0; i < 5; i++) {
1569
- const candidate = resolve7(searchDir, "dist", "web-assets");
1570
- if (existsSync6(candidate)) {
2128
+ const candidate = resolve9(searchDir, "dist", "web-assets");
2129
+ if (existsSync7(candidate)) {
1571
2130
  assetsDir = candidate;
1572
2131
  break;
1573
2132
  }
@@ -1576,7 +2135,8 @@ async function startServer({
1576
2135
  if (assetsDir) {
1577
2136
  app_default.get("*", async (c) => {
1578
2137
  const urlPath = new URL(c.req.url).pathname;
1579
- const filePath = resolve7(assetsDir, urlPath.slice(1));
2138
+ if (urlPath.startsWith("/api/")) return c.notFound();
2139
+ const filePath = resolve9(assetsDir, urlPath.slice(1));
1580
2140
  if (!filePath.startsWith(assetsDir)) return c.text("Forbidden", 403);
1581
2141
  const s = await stat(filePath).catch(() => null);
1582
2142
  if (s?.isFile()) {
@@ -1585,7 +2145,7 @@ async function startServer({
1585
2145
  const body = await readFile2(filePath);
1586
2146
  return c.body(body, 200, { "Content-Type": mime });
1587
2147
  }
1588
- const indexPath = resolve7(assetsDir, "index.html");
2148
+ const indexPath = resolve9(assetsDir, "index.html");
1589
2149
  const indexStat = await stat(indexPath).catch(() => null);
1590
2150
  if (indexStat?.isFile()) {
1591
2151
  const body = await readFile2(indexPath, "utf-8");
@@ -1595,10 +2155,10 @@ async function startServer({
1595
2155
  });
1596
2156
  }
1597
2157
  const server = serve({ fetch: app_default.fetch, port, hostname });
1598
- await new Promise((resolve9, reject) => {
2158
+ await new Promise((resolve11, reject) => {
1599
2159
  server.on("listening", () => {
1600
2160
  logger_default.info("Volute UI running", { hostname, port });
1601
- resolve9();
2161
+ resolve11();
1602
2162
  });
1603
2163
  server.on("error", (err) => {
1604
2164
  reject(err);
@@ -1608,12 +2168,24 @@ async function startServer({
1608
2168
  }
1609
2169
 
1610
2170
  // src/daemon.ts
2171
+ if (!process.env.VOLUTE_HOME) {
2172
+ process.env.VOLUTE_HOME = resolve10(homedir(), ".volute");
2173
+ }
1611
2174
  async function startDaemon(opts) {
1612
2175
  const { port, hostname } = opts;
1613
2176
  const myPid = String(process.pid);
1614
2177
  const home = voluteHome();
1615
- const DAEMON_PID_PATH = resolve8(home, "daemon.pid");
1616
- const DAEMON_JSON_PATH = resolve8(home, "daemon.json");
2178
+ if (!opts.foreground) {
2179
+ const log2 = new RotatingLog(resolve10(home, "daemon.log"));
2180
+ const write2 = (...args) => log2.write(`${format(...args)}
2181
+ `);
2182
+ console.log = write2;
2183
+ console.error = write2;
2184
+ console.warn = write2;
2185
+ console.info = write2;
2186
+ }
2187
+ const DAEMON_PID_PATH = resolve10(home, "daemon.pid");
2188
+ const DAEMON_JSON_PATH = resolve10(home, "daemon.json");
1617
2189
  mkdirSync2(home, { recursive: true });
1618
2190
  const token = process.env.VOLUTE_DAEMON_TOKEN || randomBytes(32).toString("hex");
1619
2191
  process.env.VOLUTE_DAEMON_TOKEN = token;
@@ -1666,13 +2238,13 @@ async function startDaemon(opts) {
1666
2238
  console.error(`[daemon] running on ${hostname}:${port}, pid ${myPid}`);
1667
2239
  function cleanup() {
1668
2240
  try {
1669
- if (readFileSync3(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
2241
+ if (readFileSync5(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
1670
2242
  unlinkSync2(DAEMON_PID_PATH);
1671
2243
  }
1672
2244
  } catch {
1673
2245
  }
1674
2246
  try {
1675
- const data = JSON.parse(readFileSync3(DAEMON_JSON_PATH, "utf-8"));
2247
+ const data = JSON.parse(readFileSync5(DAEMON_JSON_PATH, "utf-8"));
1676
2248
  if (data.token === token) {
1677
2249
  unlinkSync2(DAEMON_JSON_PATH);
1678
2250
  }