volute 0.4.0 → 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 (75) hide show
  1. package/README.md +22 -22
  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-5OCWMTVS.js → chunk-SMISE4SV.js} +77 -3
  12. package/dist/{chunk-SOZA2TLP.js → chunk-UAVD2AHX.js} +1 -1
  13. package/dist/{chunk-3C2XR4IY.js → chunk-UX25Z2ND.js} +113 -107
  14. package/dist/{chunk-GSPKUPKU.js → chunk-XUA3JUFK.js} +2 -1
  15. package/dist/chunk-ZYGKG6VC.js +22 -0
  16. package/dist/cli.js +86 -74
  17. package/dist/{connector-DKDJTLYZ.js → connector-LYEMXQEV.js} +11 -6
  18. package/dist/connectors/discord.js +3 -1
  19. package/dist/connectors/slack.js +14 -5
  20. package/dist/connectors/telegram.js +21 -2
  21. package/dist/conversation-ERXEQZTY.js +163 -0
  22. package/dist/create-RVCZN6HE.js +91 -0
  23. package/dist/{daemon-client-XR24PUJF.js → daemon-client-ZY6UUN2M.js} +2 -2
  24. package/dist/daemon.js +629 -177
  25. package/dist/{delete-55MXCEY5.js → delete-3QH7VYIN.js} +7 -8
  26. package/dist/{down-3OB6UVAJ.js → down-O7IFZLVJ.js} +1 -1
  27. package/dist/{env-JB27UAC3.js → env-4D4REPJF.js} +8 -5
  28. package/dist/{history-BKG74I43.js → history-OEONB53Z.js} +3 -3
  29. package/dist/{import-4CI2ZUTJ.js → import-MXJB2EII.js} +8 -8
  30. package/dist/{logs-NXFFGUKY.js → logs-DF342W4M.js} +2 -2
  31. package/dist/message-ADHWFHSI.js +32 -0
  32. package/dist/{package-Z2SFO2SV.js → package-VQOE7JNH.js} +1 -1
  33. package/dist/{schedule-A35SH4HT.js → schedule-NAG6F463.js} +10 -5
  34. package/dist/send-66QMKRUH.js +75 -0
  35. package/dist/{setup-2FDVN7OF.js → setup-RPRRGG2F.js} +5 -5
  36. package/dist/{start-LDPMCMYT.js → start-TUOXDSFL.js} +3 -3
  37. package/dist/{status-MVSQG54T.js → status-A36EHRO4.js} +3 -3
  38. package/dist/{stop-5PZTZCLL.js → stop-AOJZLQ5X.js} +6 -7
  39. package/dist/{up-F7TMTLRE.js → up-7ILD7GU7.js} +2 -2
  40. package/dist/update-LPSIAWQ2.js +140 -0
  41. package/dist/update-check-Y33QDCFL.js +17 -0
  42. package/dist/{upgrade-6ZW2RD64.js → upgrade-FX2TKJ2S.js} +16 -15
  43. package/dist/{variant-T64BKARF.js → variant-LAB67OC2.js} +15 -10
  44. package/dist/web-assets/assets/index-BbRmoxoA.js +308 -0
  45. package/dist/web-assets/index.html +2 -2
  46. package/drizzle/0003_clean_ego.sql +12 -0
  47. package/drizzle/meta/0003_snapshot.json +417 -0
  48. package/drizzle/meta/_journal.json +7 -0
  49. package/package.json +1 -1
  50. package/templates/_base/.init/.config/hooks/startup-context.sh +19 -1
  51. package/templates/_base/_skills/volute-agent/SKILL.md +110 -14
  52. package/templates/_base/home/.config/routes.json +10 -0
  53. package/templates/_base/home/VOLUTE.md +14 -35
  54. package/templates/_base/src/lib/format-prefix.ts +1 -1
  55. package/templates/_base/src/lib/router.ts +163 -16
  56. package/templates/_base/src/lib/routing.ts +55 -18
  57. package/templates/_base/src/lib/types.ts +3 -1
  58. package/templates/agent-sdk/.init/.config/routes.json +5 -0
  59. package/templates/agent-sdk/.init/CLAUDE.md +2 -2
  60. package/templates/agent-sdk/src/agent.ts +2 -1
  61. package/templates/agent-sdk/src/server.ts +8 -2
  62. package/templates/agent-sdk/volute-template.json +1 -1
  63. package/templates/pi/.init/.config/routes.json +5 -0
  64. package/templates/pi/.init/AGENTS.md +1 -1
  65. package/templates/pi/src/agent.ts +5 -3
  66. package/templates/pi/src/server.ts +1 -1
  67. package/templates/pi/volute-template.json +1 -1
  68. package/dist/channel-DQ6UY7QB.js +0 -67
  69. package/dist/chunk-ZHCE4DPY.js +0 -110
  70. package/dist/create-ILVOG75A.js +0 -79
  71. package/dist/send-3U6OTKG7.js +0 -57
  72. package/dist/web-assets/assets/index-NS621maO.js +0 -296
  73. package/templates/agent-sdk/.init/.config/sessions.json +0 -4
  74. package/templates/pi/.init/.config/sessions.json +0 -1
  75. package/dist/{service-SA4TTMDU.js → service-HZNIDNJF.js} +3 -3
package/dist/daemon.js CHANGED
@@ -1,30 +1,36 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- CHANNELS
4
- } from "./chunk-5OCWMTVS.js";
5
- import {
3
+ RotatingLog,
6
4
  clearJsonMap,
7
5
  getAgentManager,
8
6
  initAgentManager,
9
7
  loadJsonMap,
10
8
  saveJsonMap
11
- } from "./chunk-I6OHXCMV.js";
9
+ } from "./chunk-MW2KFO3B.js";
12
10
  import {
13
- collectPart,
14
- logBuffer,
15
- logger_default,
16
- readNdjson
17
- } from "./chunk-ZHCE4DPY.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";
18
21
  import {
19
22
  readVoluteConfig,
20
23
  writeVoluteConfig
21
24
  } from "./chunk-NETNFBA5.js";
22
25
  import {
23
26
  loadMergedEnv
24
- } from "./chunk-DNOXHLE5.js";
27
+ } from "./chunk-HE67X4T6.js";
25
28
  import {
26
29
  applyIsolation
27
- } from "./chunk-SOZA2TLP.js";
30
+ } from "./chunk-UAVD2AHX.js";
31
+ import {
32
+ resolveVoluteBin
33
+ } from "./chunk-5SKQ6J7T.js";
28
34
  import {
29
35
  agentDir,
30
36
  checkHealth,
@@ -38,26 +44,21 @@ import {
38
44
  setAgentRunning,
39
45
  setVariantRunning,
40
46
  voluteHome
41
- } from "./chunk-3C2XR4IY.js";
47
+ } from "./chunk-UX25Z2ND.js";
42
48
  import {
43
49
  __export
44
50
  } from "./chunk-K3NQKI34.js";
45
51
 
46
52
  // src/daemon.ts
47
53
  import { randomBytes } from "crypto";
48
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
49
- import { resolve as resolve9 } 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";
50
58
 
51
59
  // src/lib/connector-manager.ts
52
60
  import { spawn } from "child_process";
53
- import {
54
- createWriteStream,
55
- existsSync as existsSync2,
56
- mkdirSync,
57
- readFileSync as readFileSync2,
58
- unlinkSync,
59
- writeFileSync
60
- } from "fs";
61
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
61
62
  import { dirname, resolve as resolve2 } from "path";
62
63
 
63
64
  // src/lib/connector-defs.ts
@@ -217,7 +218,7 @@ var ConnectorManager = class {
217
218
  }
218
219
  const logsDir = resolve2(agentDir2, ".volute", "logs");
219
220
  mkdirSync(logsDir, { recursive: true });
220
- const logStream = createWriteStream(resolve2(logsDir, `${type}.log`), { flags: "a" });
221
+ const logStream = new RotatingLog(resolve2(logsDir, `${type}.log`));
221
222
  const agentEnv = loadMergedEnv(agentDir2);
222
223
  const prefix = `${type.toUpperCase()}_`;
223
224
  const connectorEnv = Object.fromEntries(
@@ -292,19 +293,19 @@ var ConnectorManager = class {
292
293
  const stopKey = `${agentName}:${type}`;
293
294
  this.stopping.add(stopKey);
294
295
  agentMap.delete(type);
295
- await new Promise((resolve10) => {
296
- tracked.child.on("exit", () => resolve10());
296
+ await new Promise((resolve11) => {
297
+ tracked.child.on("exit", () => resolve11());
297
298
  try {
298
299
  tracked.child.kill("SIGTERM");
299
300
  } catch {
300
- resolve10();
301
+ resolve11();
301
302
  }
302
303
  setTimeout(() => {
303
304
  try {
304
305
  tracked.child.kill("SIGKILL");
305
306
  } catch {
306
307
  }
307
- resolve10();
308
+ resolve11();
308
309
  }, 5e3);
309
310
  });
310
311
  this.stopping.delete(stopKey);
@@ -502,7 +503,15 @@ var Scheduler = class {
502
503
  console.error(`[scheduler] fired "${schedule.id}" for ${agentName}`);
503
504
  }
504
505
  try {
505
- await res.body?.cancel();
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
+ }
506
515
  } catch {
507
516
  }
508
517
  } catch (err) {
@@ -539,18 +548,20 @@ import { migrate } from "drizzle-orm/libsql/migrator";
539
548
  var schema_exports = {};
540
549
  __export(schema_exports, {
541
550
  agentMessages: () => agentMessages,
551
+ conversationParticipants: () => conversationParticipants,
542
552
  conversations: () => conversations,
543
553
  messages: () => messages,
544
554
  sessions: () => sessions,
545
555
  users: () => users
546
556
  });
547
557
  import { sql } from "drizzle-orm";
548
- import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
558
+ import { index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
549
559
  var users = sqliteTable("users", {
550
560
  id: integer("id").primaryKey({ autoIncrement: true }),
551
561
  username: text("username").unique().notNull(),
552
562
  password_hash: text("password_hash").notNull(),
553
563
  role: text("role").notNull().default("pending"),
564
+ user_type: text("user_type").notNull().default("human"),
554
565
  created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
555
566
  });
556
567
  var conversations = sqliteTable(
@@ -586,6 +597,19 @@ var agentMessages = sqliteTable(
586
597
  index("idx_agent_messages_channel").on(table.agent, table.channel)
587
598
  ]
588
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
+ );
589
613
  var sessions = sqliteTable("sessions", {
590
614
  id: text("id").primaryKey(),
591
615
  userId: integer("user_id").references(() => users.id, { onDelete: "cascade" }).notNull(),
@@ -628,12 +652,13 @@ async function getDb() {
628
652
  async function createUser(username, password) {
629
653
  const db2 = await getDb();
630
654
  const hash = hashSync(password, 10);
631
- 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"));
632
656
  const role = value === 0 ? "admin" : "pending";
633
657
  const [result] = await db2.insert(users).values({ username, password_hash: hash, role }).returning({
634
658
  id: users.id,
635
659
  username: users.username,
636
660
  role: users.role,
661
+ user_type: users.user_type,
637
662
  created_at: users.created_at
638
663
  });
639
664
  return result;
@@ -642,6 +667,7 @@ async function verifyUser(username, password) {
642
667
  const db2 = await getDb();
643
668
  const row = await db2.select().from(users).where(eq(users.username, username)).get();
644
669
  if (!row) return null;
670
+ if (row.user_type === "agent") return null;
645
671
  if (!compareSync(password, row.password_hash)) return null;
646
672
  const { password_hash: _, ...user } = row;
647
673
  return user;
@@ -652,6 +678,7 @@ async function getUser(id) {
652
678
  id: users.id,
653
679
  username: users.username,
654
680
  role: users.role,
681
+ user_type: users.user_type,
655
682
  created_at: users.created_at
656
683
  }).from(users).where(eq(users.id, id)).get();
657
684
  return row ?? null;
@@ -662,6 +689,7 @@ async function getUserByUsername(username) {
662
689
  id: users.id,
663
690
  username: users.username,
664
691
  role: users.role,
692
+ user_type: users.user_type,
665
693
  created_at: users.created_at
666
694
  }).from(users).where(eq(users.username, username)).get();
667
695
  return row ?? null;
@@ -672,6 +700,7 @@ async function listUsers() {
672
700
  id: users.id,
673
701
  username: users.username,
674
702
  role: users.role,
703
+ user_type: users.user_type,
675
704
  created_at: users.created_at
676
705
  }).from(users).orderBy(users.created_at).all();
677
706
  }
@@ -681,9 +710,58 @@ async function listPendingUsers() {
681
710
  id: users.id,
682
711
  username: users.username,
683
712
  role: users.role,
713
+ user_type: users.user_type,
684
714
  created_at: users.created_at
685
715
  }).from(users).where(eq(users.role, "pending")).orderBy(users.created_at).all();
686
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
+ }
687
765
  async function approveUser(id) {
688
766
  const db2 = await getDb();
689
767
  await db2.update(users).set({ role: "user" }).where(and(eq(users.id, id), eq(users.role, "pending")));
@@ -733,7 +811,7 @@ var authMiddleware = createMiddleware(async (c, next) => {
733
811
  if (authHeader?.startsWith("Bearer ")) {
734
812
  const token = authHeader.slice(7);
735
813
  if (token && isValidDaemonToken(token)) {
736
- c.set("user", { id: 0, username: "cli", role: "admin" });
814
+ c.set("user", { id: 0, username: "cli", role: "admin", user_type: "human" });
737
815
  await next();
738
816
  return;
739
817
  }
@@ -752,11 +830,55 @@ var authMiddleware = createMiddleware(async (c, next) => {
752
830
  // src/web/server.ts
753
831
  import { existsSync as existsSync7 } from "fs";
754
832
  import { readFile as readFile2, stat } from "fs/promises";
755
- import { dirname as dirname3, extname, resolve as resolve8 } from "path";
833
+ import { dirname as dirname3, extname, resolve as resolve9 } from "path";
756
834
  import { serve } from "@hono/node-server";
757
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
+
758
880
  // src/web/app.ts
759
- import { Hono as Hono11 } from "hono";
881
+ import { Hono as Hono13 } from "hono";
760
882
  import { bodyLimit } from "hono/body-limit";
761
883
  import { csrf } from "hono/csrf";
762
884
  import { HTTPException } from "hono/http-exception";
@@ -767,6 +889,47 @@ import { resolve as resolve5 } from "path";
767
889
  import { and as and2, desc, eq as eq3 } from "drizzle-orm";
768
890
  import { Hono } from "hono";
769
891
  import { stream } from "hono/streaming";
892
+
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
+ }
931
+
932
+ // src/web/routes/agents.ts
770
933
  function getDaemonPort() {
771
934
  try {
772
935
  const data = JSON.parse(readFileSync3(resolve5(voluteHome(), "daemon.json"), "utf-8"));
@@ -784,10 +947,10 @@ async function getAgentStatus(name, port) {
784
947
  }
785
948
  const channels = [];
786
949
  channels.push({
787
- name: CHANNELS.web.name,
788
- displayName: CHANNELS.web.displayName,
950
+ name: CHANNELS.volute.name,
951
+ displayName: CHANNELS.volute.displayName,
789
952
  status: status === "running" ? "connected" : "disconnected",
790
- showToolCalls: CHANNELS.web.showToolCalls
953
+ showToolCalls: CHANNELS.volute.showToolCalls
791
954
  });
792
955
  const connectorStatuses = getConnectorManager().getConnectorStatus(name);
793
956
  for (const cs of connectorStatuses) {
@@ -1049,6 +1212,14 @@ var credentialsSchema = z.object({
1049
1212
  var admin = new Hono2().use(authMiddleware).get("/users", async (c) => {
1050
1213
  const user = c.get("user");
1051
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
+ }
1052
1223
  return c.json(await listUsers());
1053
1224
  }).get("/users/pending", async (c) => {
1054
1225
  const user = c.get("user");
@@ -1101,6 +1272,8 @@ var app2 = new Hono2().post("/register", zValidator("json", credentialsSchema),
1101
1272
  var auth_default = app2;
1102
1273
 
1103
1274
  // src/web/routes/chat.ts
1275
+ import { readFileSync as readFileSync4 } from "fs";
1276
+ import { resolve as resolve6 } from "path";
1104
1277
  import { zValidator as zValidator2 } from "@hono/zod-validator";
1105
1278
  import { Hono as Hono3 } from "hono";
1106
1279
  import { streamSSE } from "hono/streaming";
@@ -1108,7 +1281,7 @@ import { z as z2 } from "zod";
1108
1281
 
1109
1282
  // src/lib/conversations.ts
1110
1283
  import { randomUUID } from "crypto";
1111
- 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";
1112
1285
  async function createConversation(agentName, channel, opts) {
1113
1286
  const db2 = await getDb();
1114
1287
  const id = randomUUID();
@@ -1119,6 +1292,15 @@ async function createConversation(agentName, channel, opts) {
1119
1292
  user_id: opts?.userId ?? null,
1120
1293
  title: opts?.title ?? null
1121
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
+ }
1122
1304
  return {
1123
1305
  id,
1124
1306
  agent_name: agentName,
@@ -1129,24 +1311,44 @@ async function createConversation(agentName, channel, opts) {
1129
1311
  updated_at: (/* @__PURE__ */ new Date()).toISOString()
1130
1312
  };
1131
1313
  }
1132
- async function getConversationForUser(id, userId) {
1314
+ async function getParticipants(conversationId) {
1133
1315
  const db2 = await getDb();
1134
- const row = await db2.select().from(conversations).where(and3(eq4(conversations.id, id), eq4(conversations.user_id, userId))).get();
1135
- 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;
1136
1346
  }
1137
1347
  async function deleteConversationForUser(id, userId) {
1138
- const conv = await getConversationForUser(id, userId);
1139
- if (!conv) return false;
1348
+ if (!await isParticipantOrOwner(id, userId)) return false;
1140
1349
  await deleteConversation(id);
1141
1350
  return true;
1142
1351
  }
1143
- async function listConversations(agentName, opts) {
1144
- const db2 = await getDb();
1145
- if (opts?.userId != null) {
1146
- return db2.select().from(conversations).where(and3(eq4(conversations.agent_name, agentName), eq4(conversations.user_id, opts.userId))).orderBy(desc2(conversations.updated_at)).all();
1147
- }
1148
- return db2.select().from(conversations).where(eq4(conversations.agent_name, agentName)).orderBy(desc2(conversations.updated_at)).all();
1149
- }
1150
1352
  async function addMessage(conversationId, role, senderName, content) {
1151
1353
  const db2 = await getDb();
1152
1354
  const serialized = JSON.stringify(content);
@@ -1182,6 +1384,34 @@ async function getMessages(conversationId) {
1182
1384
  return { ...row, content };
1183
1385
  });
1184
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
+ }
1185
1415
  async function deleteConversation(id) {
1186
1416
  const db2 = await getDb();
1187
1417
  await db2.delete(conversations).where(eq4(conversations.id, id));
@@ -1191,6 +1421,7 @@ async function deleteConversation(id) {
1191
1421
  var chatSchema = z2.object({
1192
1422
  message: z2.string().optional(),
1193
1423
  conversationId: z2.string().optional(),
1424
+ sender: z2.string().optional(),
1194
1425
  images: z2.array(
1195
1426
  z2.object({
1196
1427
  media_type: z2.string(),
@@ -1198,18 +1429,59 @@ var chatSchema = z2.object({
1198
1429
  })
1199
1430
  ).optional()
1200
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
+ }
1201
1479
  var app3 = new Hono3().post("/:name/chat", zValidator2("json", chatSchema), async (c) => {
1202
1480
  const name = c.req.param("name");
1203
- const [baseName, variantName] = name.split("@", 2);
1481
+ const [baseName] = name.split("@", 2);
1204
1482
  const entry = findAgent(baseName);
1205
1483
  if (!entry) return c.json({ error: "Agent not found" }, 404);
1206
- let port = entry.port;
1207
- if (variantName) {
1208
- const variant = findVariant(baseName, variantName);
1209
- if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
1210
- port = variant.port;
1211
- }
1212
- const { getAgentManager: getAgentManager2 } = await import("./agent-manager-AUCKMGPR.js");
1484
+ const { getAgentManager: getAgentManager2 } = await import("./agent-manager-PXBKA2GK.js");
1213
1485
  if (!getAgentManager2().isRunning(name)) {
1214
1486
  return c.json({ error: "Agent is not running" }, 409);
1215
1487
  }
@@ -1218,18 +1490,34 @@ var app3 = new Hono3().post("/:name/chat", zValidator2("json", chatSchema), asyn
1218
1490
  return c.json({ error: "message or images required" }, 400);
1219
1491
  }
1220
1492
  const user = c.get("user");
1493
+ const agentUser = await getOrCreateAgentUser(baseName);
1494
+ const senderName = user.id === 0 && body.sender ? body.sender : user.username;
1221
1495
  let conversationId = body.conversationId;
1222
1496
  if (conversationId) {
1223
- const conv = await getConversationForUser(conversationId, user.id);
1224
- 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
+ }
1225
1500
  } else {
1226
1501
  const title = body.message ? body.message.slice(0, 80) : "Image message";
1227
- const conv = await createConversation(baseName, "web", {
1228
- userId: user.id,
1229
- 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
1230
1517
  });
1231
1518
  conversationId = conv.id;
1232
1519
  }
1520
+ const channel = `volute:${conversationId}`;
1233
1521
  const contentBlocks = [];
1234
1522
  if (body.message) {
1235
1523
  contentBlocks.push({ type: "text", text: body.message });
@@ -1239,89 +1527,87 @@ var app3 = new Hono3().post("/:name/chat", zValidator2("json", chatSchema), asyn
1239
1527
  contentBlocks.push({ type: "image", media_type: img.media_type, data: img.data });
1240
1528
  }
1241
1529
  }
1242
- await addMessage(conversationId, "user", user.username, contentBlocks);
1243
- const db2 = await getDb();
1244
- await db2.insert(agentMessages).values({
1245
- agent: baseName,
1246
- channel: "web",
1247
- role: "user",
1248
- sender: user.username,
1249
- content: body.message ?? "[image]"
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
1250
1550
  });
1251
- let res;
1252
- try {
1253
- res = await fetch(`http://127.0.0.1:${port}/message`, {
1254
- method: "POST",
1255
- headers: { "Content-Type": "application/json" },
1256
- body: JSON.stringify({
1257
- content: contentBlocks,
1258
- channel: "web",
1259
- sender: user.username
1260
- })
1261
- });
1262
- } catch (err) {
1263
- console.error(`[chat] agent ${name} unreachable on port ${port}:`, err);
1264
- return c.json({ error: "Agent is not reachable" }, 502);
1265
- }
1266
- if (!res.ok) {
1267
- 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
+ }
1268
1570
  }
1269
- if (!res.body) {
1270
- return c.json({ error: "No response body from agent" }, 502);
1571
+ if (responses.length === 0) {
1572
+ return c.json({ error: "No agents reachable" }, 502);
1271
1573
  }
1574
+ const primary = responses[0];
1575
+ const secondary = responses.slice(1);
1576
+ const secondaryPromises = secondary.map((s) => consumeAndPersist(s.res, conversationId, s.name));
1272
1577
  return streamSSE(c, async (stream2) => {
1273
1578
  await stream2.writeSSE({
1274
- data: JSON.stringify({ type: "meta", conversationId })
1579
+ data: JSON.stringify({ type: "meta", conversationId, senderName: primary.name })
1275
1580
  });
1276
1581
  const assistantContent = [];
1277
- for await (const event of readNdjson(res.body)) {
1278
- await stream2.writeSSE({ data: JSON.stringify(event) });
1279
- if (event.type === "text") {
1280
- const last = assistantContent[assistantContent.length - 1];
1281
- if (last && last.type === "text") {
1282
- last.text += event.content;
1283
- } else {
1284
- assistantContent.push({ type: "text", text: event.content });
1285
- }
1286
- } else if (event.type === "tool_use") {
1287
- assistantContent.push({
1288
- type: "tool_use",
1289
- name: event.name,
1290
- input: event.input
1291
- });
1292
- } else if (event.type === "tool_result") {
1293
- assistantContent.push({
1294
- type: "tool_result",
1295
- output: event.output,
1296
- ...event.is_error ? { is_error: true } : {}
1297
- });
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;
1298
1587
  }
1299
- if (event.type === "done") {
1300
- if (assistantContent.length > 0) {
1301
- await addMessage(conversationId, "assistant", baseName, assistantContent);
1302
- const textParts = [];
1303
- const toolParts = [];
1304
- for (const b of assistantContent) {
1305
- const part = collectPart(b);
1306
- if (part != null) {
1307
- if (b.type === "tool_use") toolParts.push(part);
1308
- else textParts.push(part);
1309
- }
1310
- }
1311
- const summary = [textParts.join(""), ...toolParts].filter(Boolean).join("\n");
1312
- if (summary) {
1313
- await db2.insert(agentMessages).values({
1314
- agent: baseName,
1315
- channel: "web",
1316
- role: "assistant",
1317
- sender: baseName,
1318
- content: summary
1319
- });
1320
- }
1321
- }
1322
- 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);
1323
1599
  }
1324
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
+ );
1608
+ }
1609
+ }
1610
+ await stream2.writeSSE({ data: JSON.stringify({ type: "sync" }) });
1325
1611
  });
1326
1612
  });
1327
1613
  var chat_default = app3;
@@ -1398,19 +1684,79 @@ var app4 = new Hono4().get("/:name/connectors", (c) => {
1398
1684
  var connectors_default = app4;
1399
1685
 
1400
1686
  // src/web/routes/conversations.ts
1687
+ import { zValidator as zValidator3 } from "@hono/zod-validator";
1401
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
+ });
1402
1695
  var app5 = new Hono5().get("/:name/conversations", async (c) => {
1403
1696
  const name = c.req.param("name");
1404
1697
  const user = c.get("user");
1405
- 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);
1406
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);
1407
1744
  }).get("/:name/conversations/:id/messages", async (c) => {
1408
1745
  const id = c.req.param("id");
1409
1746
  const user = c.get("user");
1410
- const conv = await getConversationForUser(id, user.id);
1411
- 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
+ }
1412
1750
  const msgs = await getMessages(id);
1413
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);
1414
1760
  }).delete("/:name/conversations/:id", async (c) => {
1415
1761
  const id = c.req.param("id");
1416
1762
  const user = c.get("user");
@@ -1423,18 +1769,18 @@ var conversations_default = app5;
1423
1769
  // src/web/routes/files.ts
1424
1770
  import { existsSync as existsSync5 } from "fs";
1425
1771
  import { readdir, readFile, writeFile } from "fs/promises";
1426
- import { resolve as resolve6 } from "path";
1427
- import { zValidator as zValidator3 } from "@hono/zod-validator";
1772
+ import { resolve as resolve7 } from "path";
1773
+ import { zValidator as zValidator4 } from "@hono/zod-validator";
1428
1774
  import { Hono as Hono6 } from "hono";
1429
- import { z as z3 } from "zod";
1775
+ import { z as z4 } from "zod";
1430
1776
  var ALLOWED_FILES = /* @__PURE__ */ new Set(["SOUL.md", "MEMORY.md", "CLAUDE.md", "VOLUTE.md"]);
1431
- var saveFileSchema = z3.object({ content: z3.string() });
1777
+ var saveFileSchema = z4.object({ content: z4.string() });
1432
1778
  var app6 = new Hono6().get("/:name/files", async (c) => {
1433
1779
  const name = c.req.param("name");
1434
1780
  const entry = findAgent(name);
1435
1781
  if (!entry) return c.json({ error: "Agent not found" }, 404);
1436
1782
  const dir = agentDir(name);
1437
- const homeDir = resolve6(dir, "home");
1783
+ const homeDir = resolve7(dir, "home");
1438
1784
  if (!existsSync5(homeDir)) return c.json({ error: "Home directory missing" }, 404);
1439
1785
  const allFiles = await readdir(homeDir);
1440
1786
  const files = allFiles.filter((f) => f.endsWith(".md") && ALLOWED_FILES.has(f));
@@ -1448,13 +1794,13 @@ var app6 = new Hono6().get("/:name/files", async (c) => {
1448
1794
  const entry = findAgent(name);
1449
1795
  if (!entry) return c.json({ error: "Agent not found" }, 404);
1450
1796
  const dir = agentDir(name);
1451
- const filePath = resolve6(dir, "home", filename);
1797
+ const filePath = resolve7(dir, "home", filename);
1452
1798
  if (!existsSync5(filePath)) {
1453
1799
  return c.json({ error: "File not found" }, 404);
1454
1800
  }
1455
1801
  const content = await readFile(filePath, "utf-8");
1456
1802
  return c.json({ filename, content });
1457
- }).put("/:name/files/:filename", zValidator3("json", saveFileSchema), async (c) => {
1803
+ }).put("/:name/files/:filename", zValidator4("json", saveFileSchema), async (c) => {
1458
1804
  const name = c.req.param("name");
1459
1805
  const filename = c.req.param("filename");
1460
1806
  if (!ALLOWED_FILES.has(filename)) {
@@ -1463,7 +1809,7 @@ var app6 = new Hono6().get("/:name/files", async (c) => {
1463
1809
  const entry = findAgent(name);
1464
1810
  if (!entry) return c.json({ error: "Agent not found" }, 404);
1465
1811
  const dir = agentDir(name);
1466
- const filePath = resolve6(dir, "home", filename);
1812
+ const filePath = resolve7(dir, "home", filename);
1467
1813
  const { content } = c.req.valid("json");
1468
1814
  await writeFile(filePath, content);
1469
1815
  return c.json({ ok: true });
@@ -1473,7 +1819,7 @@ var files_default = app6;
1473
1819
  // src/web/routes/logs.ts
1474
1820
  import { spawn as spawn2 } from "child_process";
1475
1821
  import { existsSync as existsSync6 } from "fs";
1476
- import { resolve as resolve7 } from "path";
1822
+ import { resolve as resolve8 } from "path";
1477
1823
  import { Hono as Hono7 } from "hono";
1478
1824
  import { streamSSE as streamSSE2 } from "hono/streaming";
1479
1825
  var app7 = new Hono7().get("/:name/logs", async (c) => {
@@ -1481,7 +1827,7 @@ var app7 = new Hono7().get("/:name/logs", async (c) => {
1481
1827
  const entry = findAgent(name);
1482
1828
  if (!entry) return c.json({ error: "Agent not found" }, 404);
1483
1829
  const dir = agentDir(name);
1484
- const logFile = resolve7(dir, ".volute", "logs", "agent.log");
1830
+ const logFile = resolve8(dir, ".volute", "logs", "agent.log");
1485
1831
  if (!existsSync6(logFile)) {
1486
1832
  return c.json({ error: "No log file found" }, 404);
1487
1833
  }
@@ -1500,9 +1846,9 @@ var app7 = new Hono7().get("/:name/logs", async (c) => {
1500
1846
  stream2.onAbort(() => {
1501
1847
  tail.kill();
1502
1848
  });
1503
- await new Promise((resolve10) => {
1504
- tail.on("exit", resolve10);
1505
- stream2.onAbort(resolve10);
1849
+ await new Promise((resolve11) => {
1850
+ tail.on("exit", resolve11);
1851
+ stream2.onAbort(resolve11);
1506
1852
  });
1507
1853
  });
1508
1854
  });
@@ -1604,19 +1950,99 @@ var app9 = new Hono9().get("/logs", async (c) => {
1604
1950
  stream2.writeSSE({ data: JSON.stringify(entry) }).catch(() => {
1605
1951
  });
1606
1952
  });
1607
- await new Promise((resolve10) => {
1953
+ await new Promise((resolve11) => {
1608
1954
  stream2.onAbort(() => {
1609
1955
  unsubscribe();
1610
- resolve10();
1956
+ resolve11();
1611
1957
  });
1612
1958
  });
1613
1959
  });
1614
1960
  });
1615
1961
  var system_default = app9;
1616
1962
 
1617
- // src/web/routes/variants.ts
1963
+ // src/web/routes/update.ts
1964
+ import { spawn as spawn3 } from "child_process";
1618
1965
  import { Hono as Hono10 } from "hono";
1619
- 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) => {
1620
2046
  const name = c.req.param("name");
1621
2047
  const entry = findAgent(name);
1622
2048
  if (!entry) return c.json({ error: "Agent not found" }, 404);
@@ -1630,11 +2056,11 @@ var app10 = new Hono10().get("/:name/variants", async (c) => {
1630
2056
  );
1631
2057
  return c.json(results);
1632
2058
  });
1633
- var variants_default = app10;
2059
+ var variants_default = app12;
1634
2060
 
1635
2061
  // src/web/app.ts
1636
- var app11 = new Hono11();
1637
- app11.onError((err, c) => {
2062
+ var app13 = new Hono13();
2063
+ app13.onError((err, c) => {
1638
2064
  if (err instanceof HTTPException) {
1639
2065
  return err.getResponse();
1640
2066
  }
@@ -1645,10 +2071,10 @@ app11.onError((err, c) => {
1645
2071
  });
1646
2072
  return c.json({ error: "Internal server error" }, 500);
1647
2073
  });
1648
- app11.notFound((c) => {
2074
+ app13.notFound((c) => {
1649
2075
  return c.json({ error: "Not found" }, 404);
1650
2076
  });
1651
- app11.use("*", async (c, next) => {
2077
+ app13.use("*", async (c, next) => {
1652
2078
  const start = Date.now();
1653
2079
  await next();
1654
2080
  const duration = Date.now() - start;
@@ -1659,15 +2085,28 @@ app11.use("*", async (c, next) => {
1659
2085
  duration
1660
2086
  });
1661
2087
  });
1662
- app11.get("/api/health", (c) => {
1663
- 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
+ });
1664
2102
  });
1665
- app11.use("/api/*", bodyLimit({ maxSize: 10 * 1024 * 1024 }));
1666
- app11.use("/api/*", csrf());
1667
- app11.use("/api/agents/*", authMiddleware);
1668
- app11.use("/api/system/*", authMiddleware);
1669
- 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);
1670
- 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;
1671
2110
 
1672
2111
  // src/web/server.ts
1673
2112
  var MIME_TYPES = {
@@ -1686,7 +2125,7 @@ async function startServer({
1686
2125
  let assetsDir = "";
1687
2126
  let searchDir = dirname3(new URL(import.meta.url).pathname);
1688
2127
  for (let i = 0; i < 5; i++) {
1689
- const candidate = resolve8(searchDir, "dist", "web-assets");
2128
+ const candidate = resolve9(searchDir, "dist", "web-assets");
1690
2129
  if (existsSync7(candidate)) {
1691
2130
  assetsDir = candidate;
1692
2131
  break;
@@ -1696,7 +2135,8 @@ async function startServer({
1696
2135
  if (assetsDir) {
1697
2136
  app_default.get("*", async (c) => {
1698
2137
  const urlPath = new URL(c.req.url).pathname;
1699
- const filePath = resolve8(assetsDir, urlPath.slice(1));
2138
+ if (urlPath.startsWith("/api/")) return c.notFound();
2139
+ const filePath = resolve9(assetsDir, urlPath.slice(1));
1700
2140
  if (!filePath.startsWith(assetsDir)) return c.text("Forbidden", 403);
1701
2141
  const s = await stat(filePath).catch(() => null);
1702
2142
  if (s?.isFile()) {
@@ -1705,7 +2145,7 @@ async function startServer({
1705
2145
  const body = await readFile2(filePath);
1706
2146
  return c.body(body, 200, { "Content-Type": mime });
1707
2147
  }
1708
- const indexPath = resolve8(assetsDir, "index.html");
2148
+ const indexPath = resolve9(assetsDir, "index.html");
1709
2149
  const indexStat = await stat(indexPath).catch(() => null);
1710
2150
  if (indexStat?.isFile()) {
1711
2151
  const body = await readFile2(indexPath, "utf-8");
@@ -1715,10 +2155,10 @@ async function startServer({
1715
2155
  });
1716
2156
  }
1717
2157
  const server = serve({ fetch: app_default.fetch, port, hostname });
1718
- await new Promise((resolve10, reject) => {
2158
+ await new Promise((resolve11, reject) => {
1719
2159
  server.on("listening", () => {
1720
2160
  logger_default.info("Volute UI running", { hostname, port });
1721
- resolve10();
2161
+ resolve11();
1722
2162
  });
1723
2163
  server.on("error", (err) => {
1724
2164
  reject(err);
@@ -1728,12 +2168,24 @@ async function startServer({
1728
2168
  }
1729
2169
 
1730
2170
  // src/daemon.ts
2171
+ if (!process.env.VOLUTE_HOME) {
2172
+ process.env.VOLUTE_HOME = resolve10(homedir(), ".volute");
2173
+ }
1731
2174
  async function startDaemon(opts) {
1732
2175
  const { port, hostname } = opts;
1733
2176
  const myPid = String(process.pid);
1734
2177
  const home = voluteHome();
1735
- const DAEMON_PID_PATH = resolve9(home, "daemon.pid");
1736
- const DAEMON_JSON_PATH = resolve9(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");
1737
2189
  mkdirSync2(home, { recursive: true });
1738
2190
  const token = process.env.VOLUTE_DAEMON_TOKEN || randomBytes(32).toString("hex");
1739
2191
  process.env.VOLUTE_DAEMON_TOKEN = token;
@@ -1786,13 +2238,13 @@ async function startDaemon(opts) {
1786
2238
  console.error(`[daemon] running on ${hostname}:${port}, pid ${myPid}`);
1787
2239
  function cleanup() {
1788
2240
  try {
1789
- if (readFileSync4(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
2241
+ if (readFileSync5(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
1790
2242
  unlinkSync2(DAEMON_PID_PATH);
1791
2243
  }
1792
2244
  } catch {
1793
2245
  }
1794
2246
  try {
1795
- const data = JSON.parse(readFileSync4(DAEMON_JSON_PATH, "utf-8"));
2247
+ const data = JSON.parse(readFileSync5(DAEMON_JSON_PATH, "utf-8"));
1796
2248
  if (data.token === token) {
1797
2249
  unlinkSync2(DAEMON_JSON_PATH);
1798
2250
  }