volute 0.4.0 → 0.6.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 +22 -22
  2. package/dist/agent-X7GJLBLW.js +79 -0
  3. package/dist/{agent-manager-AUCKMGPR.js → agent-manager-JDVXU3ON.js} +4 -4
  4. package/dist/channel-SMCNOIVQ.js +262 -0
  5. package/dist/chunk-AOKAQGO4.js +107 -0
  6. package/dist/{chunk-VRVVQIYY.js → chunk-AZEL2IEK.js} +1 -1
  7. package/dist/chunk-B3R6L2GW.js +24 -0
  8. package/dist/{chunk-MXUCNIBG.js → chunk-BX7KI4S3.js} +68 -3
  9. package/dist/{chunk-I6OHXCMV.js → chunk-G6ZNGLUX.js} +47 -9
  10. package/dist/{chunk-DNOXHLE5.js → chunk-H7AMDUIA.js} +1 -1
  11. package/dist/{chunk-YGFIWIOF.js → chunk-JR4UXCTO.js} +1 -1
  12. package/dist/{chunk-3C2XR4IY.js → chunk-UWHWAPGO.js} +120 -107
  13. package/dist/{chunk-SOZA2TLP.js → chunk-W76KWE23.js} +1 -1
  14. package/dist/{chunk-GSPKUPKU.js → chunk-XUA3JUFK.js} +2 -1
  15. package/dist/chunk-ZYGKG6VC.js +22 -0
  16. package/dist/chunk-ZZOOTYXK.js +583 -0
  17. package/dist/cli.js +83 -74
  18. package/dist/{connector-DKDJTLYZ.js → connector-Y7JPNROO.js} +11 -6
  19. package/dist/connectors/discord.js +34 -5
  20. package/dist/connectors/slack.js +36 -8
  21. package/dist/connectors/telegram.js +55 -6
  22. package/dist/create-G525LWEA.js +91 -0
  23. package/dist/{daemon-client-XR24PUJF.js → daemon-client-442IV43D.js} +2 -2
  24. package/dist/daemon.js +1273 -384
  25. package/dist/{delete-55MXCEY5.js → delete-2PH2CGDY.js} +7 -8
  26. package/dist/{down-3OB6UVAJ.js → down-FXWAN66A.js} +1 -1
  27. package/dist/{env-JB27UAC3.js → env-7GLUJCWS.js} +8 -5
  28. package/dist/{history-BKG74I43.js → history-H72ZUIBN.js} +3 -3
  29. package/dist/{import-4CI2ZUTJ.js → import-AVKQJDYC.js} +8 -8
  30. package/dist/{logs-NXFFGUKY.js → logs-EDGK26AK.js} +2 -2
  31. package/dist/message-SCOQDR3P.js +32 -0
  32. package/dist/{package-Z2SFO2SV.js → package-4DP4Y4UO.js} +1 -1
  33. package/dist/restart-O4ETYLJF.js +29 -0
  34. package/dist/{schedule-A35SH4HT.js → schedule-S6QVC5ON.js} +10 -5
  35. package/dist/send-G7PE4DOJ.js +72 -0
  36. package/dist/{setup-2FDVN7OF.js → setup-F4TCWVSP.js} +5 -5
  37. package/dist/{start-LDPMCMYT.js → start-VHQ7LNWM.js} +3 -3
  38. package/dist/{status-MVSQG54T.js → status-QAJWXKMZ.js} +3 -3
  39. package/dist/{stop-5PZTZCLL.js → stop-CAGCT5NI.js} +6 -7
  40. package/dist/{up-F7TMTLRE.js → up-CSX3ZUIU.js} +16 -4
  41. package/dist/update-XSIX3GGP.js +140 -0
  42. package/dist/update-check-5ZADDHCK.js +17 -0
  43. package/dist/{upgrade-6ZW2RD64.js → upgrade-YXKPWDRU.js} +16 -15
  44. package/dist/{variant-T64BKARF.js → variant-4Z6W3PP6.js} +15 -10
  45. package/dist/web-assets/assets/index-D5PzIndO.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 +1 -1
  51. package/templates/_base/.init/.config/hooks/startup-context.sh +19 -1
  52. package/templates/_base/.init/.config/scripts/session-reader.ts +59 -0
  53. package/templates/_base/_skills/sessions/SKILL.md +49 -0
  54. package/templates/_base/_skills/volute-agent/SKILL.md +114 -14
  55. package/templates/_base/home/.config/routes.json +10 -0
  56. package/templates/_base/home/VOLUTE.md +14 -35
  57. package/templates/_base/src/lib/format-prefix.ts +7 -1
  58. package/templates/_base/src/lib/router.ts +193 -19
  59. package/templates/_base/src/lib/routing.ts +55 -18
  60. package/templates/_base/src/lib/session-monitor.ts +400 -0
  61. package/templates/_base/src/lib/types.ts +5 -1
  62. package/templates/agent-sdk/.init/.config/routes.json +5 -0
  63. package/templates/agent-sdk/.init/CLAUDE.md +2 -2
  64. package/templates/agent-sdk/src/agent.ts +18 -1
  65. package/templates/agent-sdk/src/lib/hooks/session-context.ts +32 -0
  66. package/templates/agent-sdk/src/server.ts +8 -2
  67. package/templates/agent-sdk/volute-template.json +1 -1
  68. package/templates/pi/.init/.config/routes.json +5 -0
  69. package/templates/pi/.init/AGENTS.md +1 -1
  70. package/templates/pi/src/agent.ts +12 -4
  71. package/templates/pi/src/lib/session-context-extension.ts +33 -0
  72. package/templates/pi/src/server.ts +1 -1
  73. package/templates/pi/volute-template.json +1 -1
  74. package/dist/channel-DQ6UY7QB.js +0 -67
  75. package/dist/chunk-5OCWMTVS.js +0 -152
  76. package/dist/chunk-ZHCE4DPY.js +0 -110
  77. package/dist/create-ILVOG75A.js +0 -79
  78. package/dist/send-3U6OTKG7.js +0 -57
  79. package/dist/web-assets/assets/index-NS621maO.js +0 -296
  80. package/templates/agent-sdk/.init/.config/sessions.json +0 -4
  81. package/templates/pi/.init/.config/sessions.json +0 -1
  82. package/dist/{service-SA4TTMDU.js → service-HZNIDNJF.js} +3 -3
package/dist/daemon.js CHANGED
@@ -1,33 +1,41 @@
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-G6ZNGLUX.js";
10
+ import {
11
+ checkForUpdate,
12
+ checkForUpdateCached,
13
+ getCurrentVersion
14
+ } from "./chunk-AOKAQGO4.js";
15
+ import {
16
+ collectPart
17
+ } from "./chunk-B3R6L2GW.js";
12
18
  import {
13
- collectPart,
14
- logBuffer,
15
- logger_default,
16
- readNdjson
17
- } from "./chunk-ZHCE4DPY.js";
19
+ CHANNELS
20
+ } from "./chunk-ZZOOTYXK.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-H7AMDUIA.js";
28
+ import "./chunk-BX7KI4S3.js";
25
29
  import {
26
30
  applyIsolation
27
- } from "./chunk-SOZA2TLP.js";
31
+ } from "./chunk-W76KWE23.js";
32
+ import {
33
+ resolveVoluteBin
34
+ } from "./chunk-5SKQ6J7T.js";
28
35
  import {
29
36
  agentDir,
30
37
  checkHealth,
38
+ daemonLoopback,
31
39
  findAgent,
32
40
  findVariant,
33
41
  getAllRunningVariants,
@@ -38,26 +46,21 @@ import {
38
46
  setAgentRunning,
39
47
  setVariantRunning,
40
48
  voluteHome
41
- } from "./chunk-3C2XR4IY.js";
49
+ } from "./chunk-UWHWAPGO.js";
42
50
  import {
43
51
  __export
44
52
  } from "./chunk-K3NQKI34.js";
45
53
 
46
54
  // src/daemon.ts
47
55
  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";
56
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
57
+ import { homedir } from "os";
58
+ import { resolve as resolve10 } from "path";
59
+ import { format } from "util";
50
60
 
51
61
  // src/lib/connector-manager.ts
52
62
  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";
63
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
61
64
  import { dirname, resolve as resolve2 } from "path";
62
65
 
63
66
  // src/lib/connector-defs.ts
@@ -217,7 +220,7 @@ var ConnectorManager = class {
217
220
  }
218
221
  const logsDir = resolve2(agentDir2, ".volute", "logs");
219
222
  mkdirSync(logsDir, { recursive: true });
220
- const logStream = createWriteStream(resolve2(logsDir, `${type}.log`), { flags: "a" });
223
+ const logStream = new RotatingLog(resolve2(logsDir, `${type}.log`));
221
224
  const agentEnv = loadMergedEnv(agentDir2);
222
225
  const prefix = `${type.toUpperCase()}_`;
223
226
  const connectorEnv = Object.fromEntries(
@@ -231,7 +234,7 @@ var ConnectorManager = class {
231
234
  VOLUTE_AGENT_NAME: agentName,
232
235
  VOLUTE_AGENT_DIR: agentDir2,
233
236
  ...daemonPort ? {
234
- VOLUTE_DAEMON_URL: `http://127.0.0.1:${daemonPort}`,
237
+ VOLUTE_DAEMON_URL: `http://${daemonLoopback()}:${daemonPort}`,
235
238
  VOLUTE_DAEMON_TOKEN: process.env.VOLUTE_DAEMON_TOKEN
236
239
  } : {},
237
240
  ...connectorEnv
@@ -292,19 +295,19 @@ var ConnectorManager = class {
292
295
  const stopKey = `${agentName}:${type}`;
293
296
  this.stopping.add(stopKey);
294
297
  agentMap.delete(type);
295
- await new Promise((resolve10) => {
296
- tracked.child.on("exit", () => resolve10());
298
+ await new Promise((resolve11) => {
299
+ tracked.child.on("exit", () => resolve11());
297
300
  try {
298
301
  tracked.child.kill("SIGTERM");
299
302
  } catch {
300
- resolve10();
303
+ resolve11();
301
304
  }
302
305
  setTimeout(() => {
303
306
  try {
304
307
  tracked.child.kill("SIGKILL");
305
308
  } catch {
306
309
  }
307
- resolve10();
310
+ resolve11();
308
311
  }, 5e3);
309
312
  });
310
313
  this.stopping.delete(stopKey);
@@ -477,7 +480,7 @@ var Scheduler = class {
477
480
  try {
478
481
  let res;
479
482
  if (this.daemonPort && this.daemonToken) {
480
- const daemonUrl = `http://127.0.0.1:${this.daemonPort}`;
483
+ const daemonUrl = `http://${daemonLoopback()}:${this.daemonPort}`;
481
484
  res = await fetch(`${daemonUrl}/api/agents/${encodeURIComponent(agentName)}/message`, {
482
485
  method: "POST",
483
486
  headers: {
@@ -502,7 +505,15 @@ var Scheduler = class {
502
505
  console.error(`[scheduler] fired "${schedule.id}" for ${agentName}`);
503
506
  }
504
507
  try {
505
- await res.body?.cancel();
508
+ const reader = res.body?.getReader();
509
+ if (reader) {
510
+ try {
511
+ while (!(await reader.read()).done) {
512
+ }
513
+ } finally {
514
+ reader.releaseLock();
515
+ }
516
+ }
506
517
  } catch {
507
518
  }
508
519
  } catch (err) {
@@ -518,6 +529,180 @@ function getScheduler() {
518
529
  return instance2;
519
530
  }
520
531
 
532
+ // src/lib/token-budget.ts
533
+ var DEFAULT_BUDGET_PERIOD_MINUTES = 60;
534
+ var MAX_QUEUE_SIZE = 100;
535
+ var TokenBudget = class {
536
+ budgets = /* @__PURE__ */ new Map();
537
+ interval = null;
538
+ daemonPort = null;
539
+ daemonToken = null;
540
+ start(daemonPort, daemonToken) {
541
+ this.daemonPort = daemonPort ?? null;
542
+ this.daemonToken = daemonToken ?? null;
543
+ this.interval = setInterval(() => this.tick(), 6e4);
544
+ }
545
+ stop() {
546
+ if (this.interval) clearInterval(this.interval);
547
+ this.interval = null;
548
+ }
549
+ setBudget(agent, tokenLimit, periodMinutes) {
550
+ if (tokenLimit <= 0) return;
551
+ const existing = this.budgets.get(agent);
552
+ if (existing) {
553
+ existing.tokenLimit = tokenLimit;
554
+ existing.periodMinutes = periodMinutes;
555
+ } else {
556
+ this.budgets.set(agent, {
557
+ tokensUsed: 0,
558
+ periodStart: Date.now(),
559
+ periodMinutes,
560
+ tokenLimit,
561
+ queue: [],
562
+ warningInjected: false
563
+ });
564
+ }
565
+ }
566
+ removeBudget(agent) {
567
+ this.budgets.delete(agent);
568
+ }
569
+ recordUsage(agent, inputTokens, outputTokens) {
570
+ const state = this.budgets.get(agent);
571
+ if (!state) return;
572
+ state.tokensUsed += inputTokens + outputTokens;
573
+ }
574
+ /** Returns current budget status. Does not mutate state — call acknowledgeWarning() after delivering a warning. */
575
+ checkBudget(agent) {
576
+ const state = this.budgets.get(agent);
577
+ if (!state) return "ok";
578
+ const pct = state.tokensUsed / state.tokenLimit;
579
+ if (pct >= 1) return "exceeded";
580
+ if (pct >= 0.8 && !state.warningInjected) return "warning";
581
+ return "ok";
582
+ }
583
+ /** Mark warning as delivered for this period. Call after successfully injecting the warning. */
584
+ acknowledgeWarning(agent) {
585
+ const state = this.budgets.get(agent);
586
+ if (state) state.warningInjected = true;
587
+ }
588
+ enqueue(agent, message) {
589
+ const state = this.budgets.get(agent);
590
+ if (!state) return;
591
+ if (state.queue.length >= MAX_QUEUE_SIZE) {
592
+ state.queue.shift();
593
+ }
594
+ state.queue.push(message);
595
+ }
596
+ drain(agent) {
597
+ const state = this.budgets.get(agent);
598
+ if (!state) return [];
599
+ const messages2 = state.queue;
600
+ state.queue = [];
601
+ return messages2;
602
+ }
603
+ getUsage(agent) {
604
+ const state = this.budgets.get(agent);
605
+ if (!state) return null;
606
+ return {
607
+ tokensUsed: state.tokensUsed,
608
+ tokenLimit: state.tokenLimit,
609
+ periodMinutes: state.periodMinutes,
610
+ periodStart: state.periodStart,
611
+ queueLength: state.queue.length,
612
+ percentUsed: Math.round(state.tokensUsed / state.tokenLimit * 100)
613
+ };
614
+ }
615
+ tick() {
616
+ const now = Date.now();
617
+ for (const [agent, state] of this.budgets) {
618
+ const elapsed = now - state.periodStart;
619
+ if (elapsed >= state.periodMinutes * 6e4) {
620
+ state.tokensUsed = 0;
621
+ state.periodStart = now;
622
+ state.warningInjected = false;
623
+ const queued = this.drain(agent);
624
+ if (queued.length > 0) {
625
+ this.replay(agent, queued).catch((err) => {
626
+ console.error(`[token-budget] replay error for ${agent}:`, err);
627
+ });
628
+ }
629
+ }
630
+ }
631
+ }
632
+ async replay(agentName, messages2) {
633
+ if (!this.daemonPort || !this.daemonToken) {
634
+ console.error(
635
+ `[token-budget] cannot replay ${messages2.length} message(s) for ${agentName}: daemon not configured`
636
+ );
637
+ const state = this.budgets.get(agentName);
638
+ if (state) state.queue.push(...messages2);
639
+ return;
640
+ }
641
+ const summary = messages2.map((m) => {
642
+ const from = m.sender ? `[${m.sender}]` : "";
643
+ const ch = m.channel ? `(${m.channel})` : "";
644
+ return `${from}${ch} ${m.textContent}`;
645
+ }).join("\n");
646
+ const body = JSON.stringify({
647
+ content: [
648
+ {
649
+ type: "text",
650
+ text: `[Budget replay] ${messages2.length} queued message(s) from the previous budget period:
651
+
652
+ ${summary}`
653
+ }
654
+ ],
655
+ channel: "system:budget-replay",
656
+ sender: "system"
657
+ });
658
+ const daemonUrl = `http://${daemonLoopback()}:${this.daemonPort}`;
659
+ const controller = new AbortController();
660
+ const timeout = setTimeout(() => controller.abort(), 12e4);
661
+ try {
662
+ const res = await fetch(`${daemonUrl}/api/agents/${encodeURIComponent(agentName)}/message`, {
663
+ method: "POST",
664
+ headers: {
665
+ "Content-Type": "application/json",
666
+ Authorization: `Bearer ${this.daemonToken}`,
667
+ Origin: daemonUrl
668
+ },
669
+ body,
670
+ signal: controller.signal
671
+ });
672
+ if (!res.ok) {
673
+ console.error(`[token-budget] replay for ${agentName} got HTTP ${res.status}`);
674
+ } else {
675
+ console.error(
676
+ `[token-budget] replayed ${messages2.length} queued message(s) for ${agentName}`
677
+ );
678
+ }
679
+ try {
680
+ const reader = res.body?.getReader();
681
+ if (reader) {
682
+ try {
683
+ while (!(await reader.read()).done) {
684
+ }
685
+ } finally {
686
+ reader.releaseLock();
687
+ }
688
+ }
689
+ } catch {
690
+ }
691
+ } catch (err) {
692
+ console.error(`[token-budget] failed to replay for ${agentName}:`, err);
693
+ const state = this.budgets.get(agentName);
694
+ if (state) state.queue.push(...messages2);
695
+ } finally {
696
+ clearTimeout(timeout);
697
+ }
698
+ }
699
+ };
700
+ var instance3 = null;
701
+ function getTokenBudget() {
702
+ if (!instance3) instance3 = new TokenBudget();
703
+ return instance3;
704
+ }
705
+
521
706
  // src/web/middleware/auth.ts
522
707
  import { timingSafeEqual } from "crypto";
523
708
  import { eq as eq2, lt } from "drizzle-orm";
@@ -539,18 +724,20 @@ import { migrate } from "drizzle-orm/libsql/migrator";
539
724
  var schema_exports = {};
540
725
  __export(schema_exports, {
541
726
  agentMessages: () => agentMessages,
727
+ conversationParticipants: () => conversationParticipants,
542
728
  conversations: () => conversations,
543
729
  messages: () => messages,
544
730
  sessions: () => sessions,
545
731
  users: () => users
546
732
  });
547
733
  import { sql } from "drizzle-orm";
548
- import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
734
+ import { index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
549
735
  var users = sqliteTable("users", {
550
736
  id: integer("id").primaryKey({ autoIncrement: true }),
551
737
  username: text("username").unique().notNull(),
552
738
  password_hash: text("password_hash").notNull(),
553
739
  role: text("role").notNull().default("pending"),
740
+ user_type: text("user_type").notNull().default("human"),
554
741
  created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
555
742
  });
556
743
  var conversations = sqliteTable(
@@ -586,6 +773,19 @@ var agentMessages = sqliteTable(
586
773
  index("idx_agent_messages_channel").on(table.agent, table.channel)
587
774
  ]
588
775
  );
776
+ var conversationParticipants = sqliteTable(
777
+ "conversation_participants",
778
+ {
779
+ conversation_id: text("conversation_id").notNull().references(() => conversations.id, { onDelete: "cascade" }),
780
+ user_id: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
781
+ role: text("role").notNull().default("member"),
782
+ joined_at: text("joined_at").notNull().default(sql`(datetime('now'))`)
783
+ },
784
+ (table) => [
785
+ uniqueIndex("idx_cp_unique").on(table.conversation_id, table.user_id),
786
+ index("idx_cp_user_id").on(table.user_id)
787
+ ]
788
+ );
589
789
  var sessions = sqliteTable("sessions", {
590
790
  id: text("id").primaryKey(),
591
791
  userId: integer("user_id").references(() => users.id, { onDelete: "cascade" }).notNull(),
@@ -628,12 +828,13 @@ async function getDb() {
628
828
  async function createUser(username, password) {
629
829
  const db2 = await getDb();
630
830
  const hash = hashSync(password, 10);
631
- const [{ value }] = await db2.select({ value: count() }).from(users);
831
+ const [{ value }] = await db2.select({ value: count() }).from(users).where(eq(users.user_type, "human"));
632
832
  const role = value === 0 ? "admin" : "pending";
633
833
  const [result] = await db2.insert(users).values({ username, password_hash: hash, role }).returning({
634
834
  id: users.id,
635
835
  username: users.username,
636
836
  role: users.role,
837
+ user_type: users.user_type,
637
838
  created_at: users.created_at
638
839
  });
639
840
  return result;
@@ -642,6 +843,7 @@ async function verifyUser(username, password) {
642
843
  const db2 = await getDb();
643
844
  const row = await db2.select().from(users).where(eq(users.username, username)).get();
644
845
  if (!row) return null;
846
+ if (row.user_type === "agent") return null;
645
847
  if (!compareSync(password, row.password_hash)) return null;
646
848
  const { password_hash: _, ...user } = row;
647
849
  return user;
@@ -652,6 +854,7 @@ async function getUser(id) {
652
854
  id: users.id,
653
855
  username: users.username,
654
856
  role: users.role,
857
+ user_type: users.user_type,
655
858
  created_at: users.created_at
656
859
  }).from(users).where(eq(users.id, id)).get();
657
860
  return row ?? null;
@@ -662,6 +865,7 @@ async function getUserByUsername(username) {
662
865
  id: users.id,
663
866
  username: users.username,
664
867
  role: users.role,
868
+ user_type: users.user_type,
665
869
  created_at: users.created_at
666
870
  }).from(users).where(eq(users.username, username)).get();
667
871
  return row ?? null;
@@ -672,6 +876,7 @@ async function listUsers() {
672
876
  id: users.id,
673
877
  username: users.username,
674
878
  role: users.role,
879
+ user_type: users.user_type,
675
880
  created_at: users.created_at
676
881
  }).from(users).orderBy(users.created_at).all();
677
882
  }
@@ -681,9 +886,58 @@ async function listPendingUsers() {
681
886
  id: users.id,
682
887
  username: users.username,
683
888
  role: users.role,
889
+ user_type: users.user_type,
684
890
  created_at: users.created_at
685
891
  }).from(users).where(eq(users.role, "pending")).orderBy(users.created_at).all();
686
892
  }
893
+ async function listUsersByType(userType) {
894
+ const db2 = await getDb();
895
+ return db2.select({
896
+ id: users.id,
897
+ username: users.username,
898
+ role: users.role,
899
+ user_type: users.user_type,
900
+ created_at: users.created_at
901
+ }).from(users).where(eq(users.user_type, userType)).orderBy(users.created_at).all();
902
+ }
903
+ async function getOrCreateAgentUser(agentName) {
904
+ const db2 = await getDb();
905
+ const existing = await db2.select({
906
+ id: users.id,
907
+ username: users.username,
908
+ role: users.role,
909
+ user_type: users.user_type,
910
+ created_at: users.created_at
911
+ }).from(users).where(and(eq(users.username, agentName), eq(users.user_type, "agent"))).get();
912
+ if (existing) return existing;
913
+ try {
914
+ const [result] = await db2.insert(users).values({
915
+ username: agentName,
916
+ password_hash: "!agent",
917
+ role: "agent",
918
+ user_type: "agent"
919
+ }).returning({
920
+ id: users.id,
921
+ username: users.username,
922
+ role: users.role,
923
+ user_type: users.user_type,
924
+ created_at: users.created_at
925
+ });
926
+ return result;
927
+ } catch (err) {
928
+ if (err instanceof Error && err.message.includes("UNIQUE constraint")) {
929
+ const retried = await db2.select({
930
+ id: users.id,
931
+ username: users.username,
932
+ role: users.role,
933
+ user_type: users.user_type,
934
+ created_at: users.created_at
935
+ }).from(users).where(and(eq(users.username, agentName), eq(users.user_type, "agent"))).get();
936
+ if (retried) return retried;
937
+ }
938
+ throw err;
939
+ }
940
+ }
687
941
  async function approveUser(id) {
688
942
  const db2 = await getDb();
689
943
  await db2.update(users).set({ role: "user" }).where(and(eq(users.id, id), eq(users.role, "pending")));
@@ -733,7 +987,7 @@ var authMiddleware = createMiddleware(async (c, next) => {
733
987
  if (authHeader?.startsWith("Bearer ")) {
734
988
  const token = authHeader.slice(7);
735
989
  if (token && isValidDaemonToken(token)) {
736
- c.set("user", { id: 0, username: "cli", role: "admin" });
990
+ c.set("user", { id: 0, username: "cli", role: "admin", user_type: "human" });
737
991
  await next();
738
992
  return;
739
993
  }
@@ -752,11 +1006,55 @@ var authMiddleware = createMiddleware(async (c, next) => {
752
1006
  // src/web/server.ts
753
1007
  import { existsSync as existsSync7 } from "fs";
754
1008
  import { readFile as readFile2, stat } from "fs/promises";
755
- import { dirname as dirname3, extname, resolve as resolve8 } from "path";
1009
+ import { dirname as dirname3, extname, resolve as resolve9 } from "path";
756
1010
  import { serve } from "@hono/node-server";
757
1011
 
1012
+ // src/lib/log-buffer.ts
1013
+ var LogBuffer = class {
1014
+ entries = [];
1015
+ maxSize = 1e3;
1016
+ subscribers = /* @__PURE__ */ new Set();
1017
+ append(entry) {
1018
+ this.entries.push(entry);
1019
+ if (this.entries.length > this.maxSize) {
1020
+ this.entries.shift();
1021
+ }
1022
+ for (const sub of this.subscribers) {
1023
+ sub(entry);
1024
+ }
1025
+ }
1026
+ getEntries() {
1027
+ return [...this.entries];
1028
+ }
1029
+ subscribe(fn) {
1030
+ this.subscribers.add(fn);
1031
+ return () => this.subscribers.delete(fn);
1032
+ }
1033
+ };
1034
+ var logBuffer = new LogBuffer();
1035
+
1036
+ // src/lib/logger.ts
1037
+ function write(level, msg, data) {
1038
+ const entry = {
1039
+ level,
1040
+ msg,
1041
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1042
+ ...data ? { data } : {}
1043
+ };
1044
+ const line = JSON.stringify(entry);
1045
+ process.stderr.write(`${line}
1046
+ `);
1047
+ logBuffer.append(entry);
1048
+ }
1049
+ var log = {
1050
+ info: (msg, data) => write("info", msg, data),
1051
+ warn: (msg, data) => write("warn", msg, data),
1052
+ error: (msg, data) => write("error", msg, data)
1053
+ };
1054
+ var logger_default = log;
1055
+
758
1056
  // src/web/app.ts
759
- import { Hono as Hono11 } from "hono";
1057
+ import { Hono as Hono14 } from "hono";
760
1058
  import { bodyLimit } from "hono/body-limit";
761
1059
  import { csrf } from "hono/csrf";
762
1060
  import { HTTPException } from "hono/http-exception";
@@ -767,6 +1065,114 @@ import { resolve as resolve5 } from "path";
767
1065
  import { and as and2, desc, eq as eq3 } from "drizzle-orm";
768
1066
  import { Hono } from "hono";
769
1067
  import { stream } from "hono/streaming";
1068
+
1069
+ // src/lib/ndjson.ts
1070
+ var MAX_BUFFER_SIZE = 1e6;
1071
+ async function* readNdjson(body) {
1072
+ const reader = body.getReader();
1073
+ const decoder = new TextDecoder();
1074
+ let buffer = "";
1075
+ try {
1076
+ while (true) {
1077
+ const { done, value } = await reader.read();
1078
+ if (done) break;
1079
+ buffer += decoder.decode(value, { stream: true });
1080
+ if (buffer.length > MAX_BUFFER_SIZE) {
1081
+ logger_default.warn("ndjson: buffer exceeded 1MB, resetting");
1082
+ buffer = "";
1083
+ continue;
1084
+ }
1085
+ const lines = buffer.split("\n");
1086
+ buffer = lines.pop() || "";
1087
+ for (const line of lines) {
1088
+ if (!line.trim()) continue;
1089
+ try {
1090
+ yield JSON.parse(line);
1091
+ } catch {
1092
+ logger_default.warn("ndjson: skipping invalid line", { line: line.slice(0, 100) });
1093
+ }
1094
+ }
1095
+ }
1096
+ if (buffer.trim()) {
1097
+ try {
1098
+ yield JSON.parse(buffer);
1099
+ } catch {
1100
+ logger_default.warn("ndjson: skipping invalid line", { line: buffer.slice(0, 100) });
1101
+ }
1102
+ }
1103
+ } finally {
1104
+ reader.releaseLock();
1105
+ }
1106
+ }
1107
+
1108
+ // src/lib/typing.ts
1109
+ var DEFAULT_TTL_MS = 1e4;
1110
+ var SWEEP_INTERVAL_MS = 5e3;
1111
+ var TypingMap = class {
1112
+ channels = /* @__PURE__ */ new Map();
1113
+ sweepTimer;
1114
+ constructor() {
1115
+ this.sweepTimer = setInterval(() => this.sweep(), SWEEP_INTERVAL_MS);
1116
+ this.sweepTimer.unref();
1117
+ }
1118
+ set(channel, sender, opts) {
1119
+ const expiresAt = opts?.persistent ? Infinity : Date.now() + (opts?.ttlMs ?? DEFAULT_TTL_MS);
1120
+ let senders = this.channels.get(channel);
1121
+ if (!senders) {
1122
+ senders = /* @__PURE__ */ new Map();
1123
+ this.channels.set(channel, senders);
1124
+ }
1125
+ senders.set(sender, { expiresAt });
1126
+ }
1127
+ delete(channel, sender) {
1128
+ const senders = this.channels.get(channel);
1129
+ if (senders) {
1130
+ senders.delete(sender);
1131
+ if (senders.size === 0) {
1132
+ this.channels.delete(channel);
1133
+ }
1134
+ }
1135
+ }
1136
+ get(channel) {
1137
+ const senders = this.channels.get(channel);
1138
+ if (!senders) return [];
1139
+ const now = Date.now();
1140
+ const result = [];
1141
+ for (const [sender, entry] of senders) {
1142
+ if (entry.expiresAt > now) {
1143
+ result.push(sender);
1144
+ }
1145
+ }
1146
+ return result;
1147
+ }
1148
+ dispose() {
1149
+ clearInterval(this.sweepTimer);
1150
+ this.channels.clear();
1151
+ if (instance4 === this) instance4 = void 0;
1152
+ }
1153
+ sweep() {
1154
+ const now = Date.now();
1155
+ for (const [channel, senders] of this.channels) {
1156
+ for (const [sender, entry] of senders) {
1157
+ if (entry.expiresAt <= now) {
1158
+ senders.delete(sender);
1159
+ }
1160
+ }
1161
+ if (senders.size === 0) {
1162
+ this.channels.delete(channel);
1163
+ }
1164
+ }
1165
+ }
1166
+ };
1167
+ var instance4;
1168
+ function getTypingMap() {
1169
+ if (!instance4) {
1170
+ instance4 = new TypingMap();
1171
+ }
1172
+ return instance4;
1173
+ }
1174
+
1175
+ // src/web/routes/agents.ts
770
1176
  function getDaemonPort() {
771
1177
  try {
772
1178
  const data = JSON.parse(readFileSync3(resolve5(voluteHome(), "daemon.json"), "utf-8"));
@@ -782,21 +1188,25 @@ async function getAgentStatus(name, port) {
782
1188
  const health = await checkHealth(port);
783
1189
  status = health.ok ? "running" : "starting";
784
1190
  }
1191
+ const channelConfig = readVoluteConfig(agentDir(name))?.channels;
785
1192
  const channels = [];
786
- channels.push({
787
- name: CHANNELS.web.name,
788
- displayName: CHANNELS.web.displayName,
789
- status: status === "running" ? "connected" : "disconnected",
790
- showToolCalls: CHANNELS.web.showToolCalls
791
- });
1193
+ for (const [, provider] of Object.entries(CHANNELS)) {
1194
+ if (!provider.builtIn) continue;
1195
+ channels.push({
1196
+ name: provider.name,
1197
+ displayName: provider.displayName,
1198
+ status: status === "running" ? "connected" : "disconnected",
1199
+ showToolCalls: channelConfig?.[provider.name]?.showToolCalls ?? provider.showToolCalls
1200
+ });
1201
+ }
792
1202
  const connectorStatuses = getConnectorManager().getConnectorStatus(name);
793
1203
  for (const cs of connectorStatuses) {
794
- const config = CHANNELS[cs.type];
1204
+ const provider = CHANNELS[cs.type];
795
1205
  channels.push({
796
- name: config?.name ?? cs.type,
797
- displayName: config?.displayName ?? cs.type,
1206
+ name: provider?.name ?? cs.type,
1207
+ displayName: provider?.displayName ?? cs.type,
798
1208
  status: cs.running ? "connected" : "disconnected",
799
- showToolCalls: config?.showToolCalls ?? false
1209
+ showToolCalls: channelConfig?.[cs.type]?.showToolCalls ?? provider?.showToolCalls ?? false
800
1210
  });
801
1211
  }
802
1212
  return { status, channels };
@@ -852,6 +1262,14 @@ var app = new Hono().get("/", async (c) => {
852
1262
  const dir = agentDir(baseName);
853
1263
  await getConnectorManager().startConnectors(baseName, dir, entry.port, getDaemonPort());
854
1264
  getScheduler().loadSchedules(baseName);
1265
+ const config = readVoluteConfig(dir);
1266
+ if (config?.tokenBudget) {
1267
+ getTokenBudget().setBudget(
1268
+ baseName,
1269
+ config.tokenBudget,
1270
+ config.tokenBudgetPeriodMinutes ?? DEFAULT_BUDGET_PERIOD_MINUTES
1271
+ );
1272
+ }
855
1273
  }
856
1274
  return c.json({ ok: true });
857
1275
  } catch (err) {
@@ -873,7 +1291,10 @@ var app = new Hono().get("/", async (c) => {
873
1291
  const connectorManager = getConnectorManager();
874
1292
  try {
875
1293
  if (manager.isRunning(name)) {
876
- if (!variantName) await connectorManager.stopConnectors(baseName);
1294
+ if (!variantName) {
1295
+ await connectorManager.stopConnectors(baseName);
1296
+ getTokenBudget().removeBudget(baseName);
1297
+ }
877
1298
  await manager.stopAgent(name);
878
1299
  }
879
1300
  await manager.startAgent(name);
@@ -881,6 +1302,14 @@ var app = new Hono().get("/", async (c) => {
881
1302
  const dir = agentDir(baseName);
882
1303
  await connectorManager.startConnectors(baseName, dir, entry.port, getDaemonPort());
883
1304
  getScheduler().loadSchedules(baseName);
1305
+ const config = readVoluteConfig(dir);
1306
+ if (config?.tokenBudget) {
1307
+ getTokenBudget().setBudget(
1308
+ baseName,
1309
+ config.tokenBudget,
1310
+ config.tokenBudgetPeriodMinutes ?? DEFAULT_BUDGET_PERIOD_MINUTES
1311
+ );
1312
+ }
884
1313
  }
885
1314
  return c.json({ ok: true });
886
1315
  } catch (err) {
@@ -903,6 +1332,7 @@ var app = new Hono().get("/", async (c) => {
903
1332
  if (!variantName) {
904
1333
  await getConnectorManager().stopConnectors(baseName);
905
1334
  getScheduler().unloadSchedules(baseName);
1335
+ getTokenBudget().removeBudget(baseName);
906
1336
  }
907
1337
  await manager.stopAgent(name);
908
1338
  return c.json({ ok: true });
@@ -918,6 +1348,7 @@ var app = new Hono().get("/", async (c) => {
918
1348
  const manager = getAgentManager();
919
1349
  if (manager.isRunning(name)) {
920
1350
  await getConnectorManager().stopConnectors(name);
1351
+ getTokenBudget().removeBudget(name);
921
1352
  await manager.stopAgent(name);
922
1353
  }
923
1354
  removeAllVariants(name);
@@ -971,12 +1402,61 @@ var app = new Hono().get("/", async (c) => {
971
1402
  console.error(`[daemon] failed to persist inbound message for ${baseName}:`, err);
972
1403
  }
973
1404
  }
1405
+ const budget = getTokenBudget();
1406
+ const budgetStatus = budget.checkBudget(baseName);
1407
+ if (budgetStatus === "exceeded") {
1408
+ let textContent = "";
1409
+ if (parsed) {
1410
+ if (typeof parsed.content === "string") {
1411
+ textContent = parsed.content;
1412
+ } else if (Array.isArray(parsed.content)) {
1413
+ textContent = parsed.content.filter((p) => p.type === "text" && p.text).map((p) => p.text).join("\n");
1414
+ }
1415
+ }
1416
+ budget.enqueue(baseName, {
1417
+ channel,
1418
+ sender: parsed?.sender ?? null,
1419
+ textContent
1420
+ });
1421
+ c.header("Content-Type", "application/x-ndjson");
1422
+ const encoder2 = new TextEncoder();
1423
+ return stream(c, async (s) => {
1424
+ await s.write(
1425
+ encoder2.encode(
1426
+ `${JSON.stringify({ type: "text", content: "[Token budget exceeded \u2014 message queued for next period]" })}
1427
+ `
1428
+ )
1429
+ );
1430
+ await s.write(encoder2.encode(`${JSON.stringify({ type: "done" })}
1431
+ `));
1432
+ });
1433
+ }
1434
+ const typingMap = getTypingMap();
1435
+ const currentlyTyping = typingMap.get(channel).filter((s) => s !== baseName);
1436
+ let forwardBody = body;
1437
+ if (parsed && currentlyTyping.length > 0) {
1438
+ parsed.typing = currentlyTyping;
1439
+ forwardBody = JSON.stringify(parsed);
1440
+ }
1441
+ if (budgetStatus === "warning" && parsed) {
1442
+ const usage = budget.getUsage(baseName);
1443
+ const pct = usage?.percentUsed ?? 80;
1444
+ const warningText = `
1445
+ [System: Token budget is at ${pct}% \u2014 conserve tokens to avoid message queuing]`;
1446
+ if (typeof parsed.content === "string") {
1447
+ parsed.content = parsed.content + warningText;
1448
+ } else if (Array.isArray(parsed.content)) {
1449
+ parsed.content = [...parsed.content, { type: "text", text: warningText }];
1450
+ }
1451
+ budget.acknowledgeWarning(baseName);
1452
+ forwardBody = JSON.stringify(parsed);
1453
+ }
974
1454
  let res;
975
1455
  try {
976
1456
  res = await fetch(`http://127.0.0.1:${port}/message`, {
977
1457
  method: "POST",
978
1458
  headers: { "Content-Type": "application/json" },
979
- body
1459
+ body: forwardBody
980
1460
  });
981
1461
  } catch (err) {
982
1462
  console.error(`[daemon] agent ${name} unreachable on port ${port}:`, err);
@@ -990,33 +1470,76 @@ var app = new Hono().get("/", async (c) => {
990
1470
  }
991
1471
  c.header("Content-Type", "application/x-ndjson");
992
1472
  const encoder = new TextEncoder();
1473
+ typingMap.set(channel, baseName, { persistent: true });
993
1474
  return stream(c, async (s) => {
994
- const textParts = [];
995
- const toolParts = [];
996
- for await (const event of readNdjson(res.body)) {
997
- await s.write(encoder.encode(`${JSON.stringify(event)}
1475
+ try {
1476
+ const textParts = [];
1477
+ const toolParts = [];
1478
+ for await (const event of readNdjson(res.body)) {
1479
+ if (event.type === "usage") {
1480
+ const input = typeof event.input_tokens === "number" ? event.input_tokens : 0;
1481
+ const output = typeof event.output_tokens === "number" ? event.output_tokens : 0;
1482
+ budget.recordUsage(baseName, input, output);
1483
+ continue;
1484
+ }
1485
+ await s.write(encoder.encode(`${JSON.stringify(event)}
998
1486
  `));
999
- const part = collectPart(event);
1000
- if (part != null) {
1001
- if (event.type === "tool_use") toolParts.push(part);
1002
- else textParts.push(part);
1487
+ const part = collectPart(event);
1488
+ if (part != null) {
1489
+ if (event.type === "tool_use") toolParts.push(part);
1490
+ else textParts.push(part);
1491
+ }
1003
1492
  }
1004
- }
1005
- const content = [textParts.join(""), ...toolParts].filter(Boolean).join("\n");
1006
- if (content) {
1007
- try {
1008
- await db2.insert(agentMessages).values({
1009
- agent: baseName,
1010
- channel,
1011
- role: "assistant",
1012
- sender: baseName,
1013
- content
1014
- });
1015
- } catch (err) {
1016
- console.error(`[daemon] failed to persist assistant response for ${baseName}:`, err);
1493
+ const content = [textParts.join(""), ...toolParts].filter(Boolean).join("\n");
1494
+ if (content) {
1495
+ try {
1496
+ await db2.insert(agentMessages).values({
1497
+ agent: baseName,
1498
+ channel,
1499
+ role: "assistant",
1500
+ sender: baseName,
1501
+ content
1502
+ });
1503
+ } catch (err) {
1504
+ console.error(`[daemon] failed to persist assistant response for ${baseName}:`, err);
1505
+ }
1017
1506
  }
1507
+ } finally {
1508
+ typingMap.delete(channel, baseName);
1018
1509
  }
1019
1510
  });
1511
+ }).get("/:name/budget", async (c) => {
1512
+ const name = c.req.param("name");
1513
+ const [baseName] = name.split("@", 2);
1514
+ const usage = getTokenBudget().getUsage(baseName);
1515
+ if (!usage) return c.json({ error: "No budget configured" }, 404);
1516
+ return c.json(usage);
1517
+ }).post("/:name/history", async (c) => {
1518
+ const name = c.req.param("name");
1519
+ const [baseName] = name.split("@", 2);
1520
+ let body;
1521
+ try {
1522
+ body = await c.req.json();
1523
+ } catch {
1524
+ return c.json({ error: "Invalid JSON" }, 400);
1525
+ }
1526
+ if (!body.channel || !body.content) {
1527
+ return c.json({ error: "channel and content required" }, 400);
1528
+ }
1529
+ const db2 = await getDb();
1530
+ try {
1531
+ await db2.insert(agentMessages).values({
1532
+ agent: baseName,
1533
+ channel: body.channel,
1534
+ role: "assistant",
1535
+ sender: baseName,
1536
+ content: body.content
1537
+ });
1538
+ } catch (err) {
1539
+ console.error(`[daemon] failed to persist external send for ${baseName}:`, err);
1540
+ return c.json({ error: "Failed to persist" }, 500);
1541
+ }
1542
+ return c.json({ ok: true });
1020
1543
  }).get("/:name/history/channels", async (c) => {
1021
1544
  const name = c.req.param("name");
1022
1545
  const db2 = await getDb();
@@ -1049,6 +1572,14 @@ var credentialsSchema = z.object({
1049
1572
  var admin = new Hono2().use(authMiddleware).get("/users", async (c) => {
1050
1573
  const user = c.get("user");
1051
1574
  if (user.role !== "admin") return c.json({ error: "Forbidden" }, 403);
1575
+ const agents = readRegistry();
1576
+ for (const agent of agents) {
1577
+ await getOrCreateAgentUser(agent.name);
1578
+ }
1579
+ const type = c.req.query("type");
1580
+ if (type === "human" || type === "agent") {
1581
+ return c.json(await listUsersByType(type));
1582
+ }
1052
1583
  return c.json(await listUsers());
1053
1584
  }).get("/users/pending", async (c) => {
1054
1585
  const user = c.get("user");
@@ -1100,254 +1631,28 @@ var app2 = new Hono2().post("/register", zValidator("json", credentialsSchema),
1100
1631
  }).route("/", admin);
1101
1632
  var auth_default = app2;
1102
1633
 
1103
- // src/web/routes/chat.ts
1104
- import { zValidator as zValidator2 } from "@hono/zod-validator";
1634
+ // src/web/routes/connectors.ts
1105
1635
  import { Hono as Hono3 } from "hono";
1106
- import { streamSSE } from "hono/streaming";
1107
- import { z as z2 } from "zod";
1108
-
1109
- // src/lib/conversations.ts
1110
- import { randomUUID } from "crypto";
1111
- import { and as and3, desc as desc2, eq as eq4, isNull, sql as sql2 } from "drizzle-orm";
1112
- async function createConversation(agentName, channel, opts) {
1113
- const db2 = await getDb();
1114
- const id = randomUUID();
1115
- await db2.insert(conversations).values({
1116
- id,
1117
- agent_name: agentName,
1118
- channel,
1119
- user_id: opts?.userId ?? null,
1120
- title: opts?.title ?? null
1636
+ var CONNECTOR_TYPE_RE = /^[a-z][a-z0-9-]*$/;
1637
+ var app3 = new Hono3().get("/:name/connectors", (c) => {
1638
+ const name = c.req.param("name");
1639
+ const entry = findAgent(name);
1640
+ if (!entry) return c.json({ error: "Agent not found" }, 404);
1641
+ const dir = agentDir(name);
1642
+ const config = readVoluteConfig(dir) ?? {};
1643
+ const configured = config.connectors ?? [];
1644
+ const manager = getConnectorManager();
1645
+ const runningStatus = manager.getConnectorStatus(name);
1646
+ const connectors = configured.map((type) => {
1647
+ const status = runningStatus.find((s) => s.type === type);
1648
+ return { type, running: status?.running ?? false };
1121
1649
  });
1122
- return {
1123
- id,
1124
- agent_name: agentName,
1125
- channel,
1126
- user_id: opts?.userId ?? null,
1127
- title: opts?.title ?? null,
1128
- created_at: (/* @__PURE__ */ new Date()).toISOString(),
1129
- updated_at: (/* @__PURE__ */ new Date()).toISOString()
1130
- };
1131
- }
1132
- async function getConversationForUser(id, userId) {
1133
- 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;
1136
- }
1137
- async function deleteConversationForUser(id, userId) {
1138
- const conv = await getConversationForUser(id, userId);
1139
- if (!conv) return false;
1140
- await deleteConversation(id);
1141
- return true;
1142
- }
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
- async function addMessage(conversationId, role, senderName, content) {
1151
- const db2 = await getDb();
1152
- const serialized = JSON.stringify(content);
1153
- const [result] = await db2.insert(messages).values({ conversation_id: conversationId, role, sender_name: senderName, content: serialized }).returning({ id: messages.id, created_at: messages.created_at });
1154
- await db2.update(conversations).set({ updated_at: sql2`datetime('now')` }).where(eq4(conversations.id, conversationId));
1155
- if (role === "user") {
1156
- const firstText = content.find((b) => b.type === "text");
1157
- const title = firstText ? firstText.text.slice(0, 80) : "";
1158
- if (title) {
1159
- await db2.update(conversations).set({ title }).where(and3(eq4(conversations.id, conversationId), isNull(conversations.title)));
1160
- }
1161
- }
1162
- return {
1163
- id: result.id,
1164
- conversation_id: conversationId,
1165
- role,
1166
- sender_name: senderName,
1167
- content,
1168
- created_at: result.created_at
1169
- };
1170
- }
1171
- async function getMessages(conversationId) {
1172
- const db2 = await getDb();
1173
- const rows = await db2.select().from(messages).where(eq4(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
1174
- return rows.map((row) => {
1175
- let content;
1176
- try {
1177
- const parsed = JSON.parse(row.content);
1178
- content = Array.isArray(parsed) ? parsed : [{ type: "text", text: row.content }];
1179
- } catch {
1180
- content = [{ type: "text", text: row.content }];
1181
- }
1182
- return { ...row, content };
1183
- });
1184
- }
1185
- async function deleteConversation(id) {
1186
- const db2 = await getDb();
1187
- await db2.delete(conversations).where(eq4(conversations.id, id));
1188
- }
1189
-
1190
- // src/web/routes/chat.ts
1191
- var chatSchema = z2.object({
1192
- message: z2.string().optional(),
1193
- conversationId: z2.string().optional(),
1194
- images: z2.array(
1195
- z2.object({
1196
- media_type: z2.string(),
1197
- data: z2.string()
1198
- })
1199
- ).optional()
1200
- });
1201
- var app3 = new Hono3().post("/:name/chat", zValidator2("json", chatSchema), async (c) => {
1202
- const name = c.req.param("name");
1203
- const [baseName, variantName] = name.split("@", 2);
1204
- const entry = findAgent(baseName);
1205
- 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");
1213
- if (!getAgentManager2().isRunning(name)) {
1214
- return c.json({ error: "Agent is not running" }, 409);
1215
- }
1216
- const body = c.req.valid("json");
1217
- if (!body.message && (!body.images || body.images.length === 0)) {
1218
- return c.json({ error: "message or images required" }, 400);
1219
- }
1220
- const user = c.get("user");
1221
- let conversationId = body.conversationId;
1222
- if (conversationId) {
1223
- const conv = await getConversationForUser(conversationId, user.id);
1224
- if (!conv) return c.json({ error: "Conversation not found" }, 404);
1225
- } else {
1226
- const title = body.message ? body.message.slice(0, 80) : "Image message";
1227
- const conv = await createConversation(baseName, "web", {
1228
- userId: user.id,
1229
- title
1230
- });
1231
- conversationId = conv.id;
1232
- }
1233
- const contentBlocks = [];
1234
- if (body.message) {
1235
- contentBlocks.push({ type: "text", text: body.message });
1236
- }
1237
- if (body.images) {
1238
- for (const img of body.images) {
1239
- contentBlocks.push({ type: "image", media_type: img.media_type, data: img.data });
1240
- }
1241
- }
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]"
1250
- });
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);
1268
- }
1269
- if (!res.body) {
1270
- return c.json({ error: "No response body from agent" }, 502);
1271
- }
1272
- return streamSSE(c, async (stream2) => {
1273
- await stream2.writeSSE({
1274
- data: JSON.stringify({ type: "meta", conversationId })
1275
- });
1276
- 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
- });
1298
- }
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;
1323
- }
1324
- }
1325
- });
1326
- });
1327
- var chat_default = app3;
1328
-
1329
- // src/web/routes/connectors.ts
1330
- import { Hono as Hono4 } from "hono";
1331
- var CONNECTOR_TYPE_RE = /^[a-z][a-z0-9-]*$/;
1332
- var app4 = new Hono4().get("/:name/connectors", (c) => {
1333
- const name = c.req.param("name");
1334
- const entry = findAgent(name);
1335
- if (!entry) return c.json({ error: "Agent not found" }, 404);
1336
- const dir = agentDir(name);
1337
- const config = readVoluteConfig(dir) ?? {};
1338
- const configured = config.connectors ?? [];
1339
- const manager = getConnectorManager();
1340
- const runningStatus = manager.getConnectorStatus(name);
1341
- const connectors = configured.map((type) => {
1342
- const status = runningStatus.find((s) => s.type === type);
1343
- return { type, running: status?.running ?? false };
1344
- });
1345
- return c.json(connectors);
1346
- }).post("/:name/connectors/:type", requireAdmin, async (c) => {
1347
- const name = c.req.param("name");
1348
- const type = c.req.param("type");
1349
- if (!CONNECTOR_TYPE_RE.test(type)) {
1350
- return c.json({ error: "Invalid connector type" }, 400);
1650
+ return c.json(connectors);
1651
+ }).post("/:name/connectors/:type", requireAdmin, async (c) => {
1652
+ const name = c.req.param("name");
1653
+ const type = c.req.param("type");
1654
+ if (!CONNECTOR_TYPE_RE.test(type)) {
1655
+ return c.json({ error: "Invalid connector type" }, 400);
1351
1656
  }
1352
1657
  const entry = findAgent(name);
1353
1658
  if (!entry) return c.json({ error: "Agent not found" }, 404);
@@ -1395,41 +1700,18 @@ var app4 = new Hono4().get("/:name/connectors", (c) => {
1395
1700
  writeVoluteConfig(dir, config);
1396
1701
  return c.json({ ok: true });
1397
1702
  });
1398
- var connectors_default = app4;
1399
-
1400
- // src/web/routes/conversations.ts
1401
- import { Hono as Hono5 } from "hono";
1402
- var app5 = new Hono5().get("/:name/conversations", async (c) => {
1403
- const name = c.req.param("name");
1404
- const user = c.get("user");
1405
- const convs = await listConversations(name, { userId: user.id });
1406
- return c.json(convs);
1407
- }).get("/:name/conversations/:id/messages", async (c) => {
1408
- const id = c.req.param("id");
1409
- const user = c.get("user");
1410
- const conv = await getConversationForUser(id, user.id);
1411
- if (!conv) return c.json({ error: "Conversation not found" }, 404);
1412
- const msgs = await getMessages(id);
1413
- return c.json(msgs);
1414
- }).delete("/:name/conversations/:id", async (c) => {
1415
- const id = c.req.param("id");
1416
- const user = c.get("user");
1417
- const deleted = await deleteConversationForUser(id, user.id);
1418
- if (!deleted) return c.json({ error: "Conversation not found" }, 404);
1419
- return c.json({ ok: true });
1420
- });
1421
- var conversations_default = app5;
1703
+ var connectors_default = app3;
1422
1704
 
1423
1705
  // src/web/routes/files.ts
1424
1706
  import { existsSync as existsSync5 } from "fs";
1425
1707
  import { readdir, readFile, writeFile } from "fs/promises";
1426
1708
  import { resolve as resolve6 } from "path";
1427
- import { zValidator as zValidator3 } from "@hono/zod-validator";
1428
- import { Hono as Hono6 } from "hono";
1429
- import { z as z3 } from "zod";
1709
+ import { zValidator as zValidator2 } from "@hono/zod-validator";
1710
+ import { Hono as Hono4 } from "hono";
1711
+ import { z as z2 } from "zod";
1430
1712
  var ALLOWED_FILES = /* @__PURE__ */ new Set(["SOUL.md", "MEMORY.md", "CLAUDE.md", "VOLUTE.md"]);
1431
- var saveFileSchema = z3.object({ content: z3.string() });
1432
- var app6 = new Hono6().get("/:name/files", async (c) => {
1713
+ var saveFileSchema = z2.object({ content: z2.string() });
1714
+ var app4 = new Hono4().get("/:name/files", async (c) => {
1433
1715
  const name = c.req.param("name");
1434
1716
  const entry = findAgent(name);
1435
1717
  if (!entry) return c.json({ error: "Agent not found" }, 404);
@@ -1454,7 +1736,7 @@ var app6 = new Hono6().get("/:name/files", async (c) => {
1454
1736
  }
1455
1737
  const content = await readFile(filePath, "utf-8");
1456
1738
  return c.json({ filename, content });
1457
- }).put("/:name/files/:filename", zValidator3("json", saveFileSchema), async (c) => {
1739
+ }).put("/:name/files/:filename", zValidator2("json", saveFileSchema), async (c) => {
1458
1740
  const name = c.req.param("name");
1459
1741
  const filename = c.req.param("filename");
1460
1742
  if (!ALLOWED_FILES.has(filename)) {
@@ -1468,15 +1750,15 @@ var app6 = new Hono6().get("/:name/files", async (c) => {
1468
1750
  await writeFile(filePath, content);
1469
1751
  return c.json({ ok: true });
1470
1752
  });
1471
- var files_default = app6;
1753
+ var files_default = app4;
1472
1754
 
1473
1755
  // src/web/routes/logs.ts
1474
1756
  import { spawn as spawn2 } from "child_process";
1475
1757
  import { existsSync as existsSync6 } from "fs";
1476
1758
  import { resolve as resolve7 } from "path";
1477
- import { Hono as Hono7 } from "hono";
1478
- import { streamSSE as streamSSE2 } from "hono/streaming";
1479
- var app7 = new Hono7().get("/:name/logs", async (c) => {
1759
+ import { Hono as Hono5 } from "hono";
1760
+ import { streamSSE } from "hono/streaming";
1761
+ var app5 = new Hono5().get("/:name/logs", async (c) => {
1480
1762
  const name = c.req.param("name");
1481
1763
  const entry = findAgent(name);
1482
1764
  if (!entry) return c.json({ error: "Agent not found" }, 404);
@@ -1485,7 +1767,7 @@ var app7 = new Hono7().get("/:name/logs", async (c) => {
1485
1767
  if (!existsSync6(logFile)) {
1486
1768
  return c.json({ error: "No log file found" }, 404);
1487
1769
  }
1488
- return streamSSE2(c, async (stream2) => {
1770
+ return streamSSE(c, async (stream2) => {
1489
1771
  const tail = spawn2("tail", ["-n", "200", "-f", logFile]);
1490
1772
  const onData = (data) => {
1491
1773
  const lines = data.toString().split("\n");
@@ -1500,16 +1782,16 @@ var app7 = new Hono7().get("/:name/logs", async (c) => {
1500
1782
  stream2.onAbort(() => {
1501
1783
  tail.kill();
1502
1784
  });
1503
- await new Promise((resolve10) => {
1504
- tail.on("exit", resolve10);
1505
- stream2.onAbort(resolve10);
1785
+ await new Promise((resolve11) => {
1786
+ tail.on("exit", resolve11);
1787
+ stream2.onAbort(resolve11);
1506
1788
  });
1507
1789
  });
1508
1790
  });
1509
- var logs_default = app7;
1791
+ var logs_default = app5;
1510
1792
 
1511
1793
  // src/web/routes/schedules.ts
1512
- import { Hono as Hono8 } from "hono";
1794
+ import { Hono as Hono6 } from "hono";
1513
1795
  function readSchedules(name) {
1514
1796
  return readVoluteConfig(agentDir(name))?.schedules ?? [];
1515
1797
  }
@@ -1520,7 +1802,7 @@ function writeSchedules(name, schedules) {
1520
1802
  writeVoluteConfig(dir, config);
1521
1803
  getScheduler().loadSchedules(name);
1522
1804
  }
1523
- var app8 = new Hono8().get("/:name/schedules", (c) => {
1805
+ var app6 = new Hono6().get("/:name/schedules", (c) => {
1524
1806
  const name = c.req.param("name");
1525
1807
  if (!findAgent(name)) return c.json({ error: "Agent not found" }, 404);
1526
1808
  return c.json(readSchedules(name));
@@ -1588,15 +1870,15 @@ var app8 = new Hono8().get("/:name/schedules", (c) => {
1588
1870
  return c.json({ error: "Failed to reach agent" }, 502);
1589
1871
  }
1590
1872
  });
1591
- var schedules_default = app8;
1873
+ var schedules_default = app6;
1592
1874
 
1593
1875
  // src/web/routes/system.ts
1594
- import { Hono as Hono9 } from "hono";
1595
- import { streamSSE as streamSSE3 } from "hono/streaming";
1596
- var app9 = new Hono9().get("/logs", async (c) => {
1876
+ import { Hono as Hono7 } from "hono";
1877
+ import { streamSSE as streamSSE2 } from "hono/streaming";
1878
+ var app7 = new Hono7().get("/logs", async (c) => {
1597
1879
  const user = c.get("user");
1598
1880
  if (user.role !== "admin") return c.json({ error: "Forbidden" }, 403);
1599
- return streamSSE3(c, async (stream2) => {
1881
+ return streamSSE2(c, async (stream2) => {
1600
1882
  for (const entry of logBuffer.getEntries()) {
1601
1883
  await stream2.writeSSE({ data: JSON.stringify(entry) });
1602
1884
  }
@@ -1604,15 +1886,64 @@ var app9 = new Hono9().get("/logs", async (c) => {
1604
1886
  stream2.writeSSE({ data: JSON.stringify(entry) }).catch(() => {
1605
1887
  });
1606
1888
  });
1607
- await new Promise((resolve10) => {
1889
+ await new Promise((resolve11) => {
1608
1890
  stream2.onAbort(() => {
1609
1891
  unsubscribe();
1610
- resolve10();
1892
+ resolve11();
1611
1893
  });
1612
1894
  });
1613
1895
  });
1614
1896
  });
1615
- var system_default = app9;
1897
+ var system_default = app7;
1898
+
1899
+ // src/web/routes/typing.ts
1900
+ import { zValidator as zValidator3 } from "@hono/zod-validator";
1901
+ import { Hono as Hono8 } from "hono";
1902
+ import { z as z3 } from "zod";
1903
+ var typingSchema = z3.object({
1904
+ channel: z3.string().min(1),
1905
+ sender: z3.string().min(1),
1906
+ active: z3.boolean()
1907
+ });
1908
+ var app8 = new Hono8().post("/:name/typing", zValidator3("json", typingSchema), (c) => {
1909
+ const { channel, sender, active } = c.req.valid("json");
1910
+ const map = getTypingMap();
1911
+ if (active) {
1912
+ map.set(channel, sender);
1913
+ } else {
1914
+ map.delete(channel, sender);
1915
+ }
1916
+ return c.json({ ok: true });
1917
+ }).get("/:name/typing", (c) => {
1918
+ const channel = c.req.query("channel");
1919
+ if (!channel) {
1920
+ return c.json({ error: "channel query param is required" }, 400);
1921
+ }
1922
+ const map = getTypingMap();
1923
+ return c.json({ typing: map.get(channel) });
1924
+ });
1925
+ var typing_default = app8;
1926
+
1927
+ // src/web/routes/update.ts
1928
+ import { spawn as spawn3 } from "child_process";
1929
+ import { Hono as Hono9 } from "hono";
1930
+ var bin;
1931
+ var app9 = new Hono9().get("/update", async (c) => {
1932
+ const result = await checkForUpdate();
1933
+ return c.json(result);
1934
+ }).post("/update", requireAdmin, async (c) => {
1935
+ bin ??= resolveVoluteBin();
1936
+ const child = spawn3(bin, ["update"], {
1937
+ stdio: "ignore",
1938
+ detached: true
1939
+ });
1940
+ child.on("error", (err) => {
1941
+ logger_default.error("Update process error", { error: err.message });
1942
+ });
1943
+ child.unref();
1944
+ return c.json({ ok: true, message: "Updating..." });
1945
+ });
1946
+ var update_default = app9;
1616
1947
 
1617
1948
  // src/web/routes/variants.ts
1618
1949
  import { Hono as Hono10 } from "hono";
@@ -1632,9 +1963,529 @@ var app10 = new Hono10().get("/:name/variants", async (c) => {
1632
1963
  });
1633
1964
  var variants_default = app10;
1634
1965
 
1966
+ // src/web/routes/volute/chat.ts
1967
+ import { readFileSync as readFileSync4 } from "fs";
1968
+ import { resolve as resolve8 } from "path";
1969
+ import { zValidator as zValidator4 } from "@hono/zod-validator";
1970
+ import { Hono as Hono11 } from "hono";
1971
+ import { streamSSE as streamSSE3 } from "hono/streaming";
1972
+ import { z as z4 } from "zod";
1973
+
1974
+ // src/lib/conversations.ts
1975
+ import { randomUUID } from "crypto";
1976
+ import { and as and3, desc as desc2, eq as eq4, inArray, isNull, sql as sql2 } from "drizzle-orm";
1977
+ async function createConversation(agentName, channel, opts) {
1978
+ const db2 = await getDb();
1979
+ const id = randomUUID();
1980
+ await db2.insert(conversations).values({
1981
+ id,
1982
+ agent_name: agentName,
1983
+ channel,
1984
+ user_id: opts?.userId ?? null,
1985
+ title: opts?.title ?? null
1986
+ });
1987
+ if (opts?.participantIds && opts.participantIds.length > 0) {
1988
+ await db2.insert(conversationParticipants).values(
1989
+ opts.participantIds.map((uid, i) => ({
1990
+ conversation_id: id,
1991
+ user_id: uid,
1992
+ role: i === 0 ? "owner" : "member"
1993
+ }))
1994
+ );
1995
+ }
1996
+ return {
1997
+ id,
1998
+ agent_name: agentName,
1999
+ channel,
2000
+ user_id: opts?.userId ?? null,
2001
+ title: opts?.title ?? null,
2002
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
2003
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
2004
+ };
2005
+ }
2006
+ async function getConversation(id) {
2007
+ const db2 = await getDb();
2008
+ const row = await db2.select().from(conversations).where(eq4(conversations.id, id)).get();
2009
+ return row ?? null;
2010
+ }
2011
+ async function getParticipants(conversationId) {
2012
+ const db2 = await getDb();
2013
+ const rows = await db2.select({
2014
+ userId: conversationParticipants.user_id,
2015
+ username: users.username,
2016
+ userType: users.user_type,
2017
+ role: conversationParticipants.role
2018
+ }).from(conversationParticipants).innerJoin(users, eq4(conversationParticipants.user_id, users.id)).where(eq4(conversationParticipants.conversation_id, conversationId)).all();
2019
+ return rows;
2020
+ }
2021
+ async function isParticipant(conversationId, userId) {
2022
+ const db2 = await getDb();
2023
+ const row = await db2.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(
2024
+ and3(
2025
+ eq4(conversationParticipants.conversation_id, conversationId),
2026
+ eq4(conversationParticipants.user_id, userId)
2027
+ )
2028
+ ).get();
2029
+ return row != null;
2030
+ }
2031
+ async function listConversationsForUser(userId) {
2032
+ const db2 = await getDb();
2033
+ const participantRows = await db2.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(eq4(conversationParticipants.user_id, userId)).all();
2034
+ if (participantRows.length === 0) return [];
2035
+ const convIds = participantRows.map((r) => r.conversation_id);
2036
+ return db2.select().from(conversations).where(inArray(conversations.id, convIds)).orderBy(desc2(conversations.updated_at)).all();
2037
+ }
2038
+ async function isParticipantOrOwner(conversationId, userId) {
2039
+ if (await isParticipant(conversationId, userId)) return true;
2040
+ const db2 = await getDb();
2041
+ const row = await db2.select().from(conversations).where(and3(eq4(conversations.id, conversationId), eq4(conversations.user_id, userId))).get();
2042
+ return row != null;
2043
+ }
2044
+ async function deleteConversationForUser(id, userId) {
2045
+ if (!await isParticipantOrOwner(id, userId)) return false;
2046
+ await deleteConversation(id);
2047
+ return true;
2048
+ }
2049
+ async function addMessage(conversationId, role, senderName, content) {
2050
+ const db2 = await getDb();
2051
+ const serialized = JSON.stringify(content);
2052
+ const [result] = await db2.insert(messages).values({ conversation_id: conversationId, role, sender_name: senderName, content: serialized }).returning({ id: messages.id, created_at: messages.created_at });
2053
+ await db2.update(conversations).set({ updated_at: sql2`datetime('now')` }).where(eq4(conversations.id, conversationId));
2054
+ if (role === "user") {
2055
+ const firstText = content.find((b) => b.type === "text");
2056
+ const title = firstText ? firstText.text.slice(0, 80) : "";
2057
+ if (title) {
2058
+ await db2.update(conversations).set({ title }).where(and3(eq4(conversations.id, conversationId), isNull(conversations.title)));
2059
+ }
2060
+ }
2061
+ return {
2062
+ id: result.id,
2063
+ conversation_id: conversationId,
2064
+ role,
2065
+ sender_name: senderName,
2066
+ content,
2067
+ created_at: result.created_at
2068
+ };
2069
+ }
2070
+ async function getMessages(conversationId) {
2071
+ const db2 = await getDb();
2072
+ const rows = await db2.select().from(messages).where(eq4(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
2073
+ return rows.map((row) => {
2074
+ let content;
2075
+ try {
2076
+ const parsed = JSON.parse(row.content);
2077
+ content = Array.isArray(parsed) ? parsed : [{ type: "text", text: row.content }];
2078
+ } catch {
2079
+ content = [{ type: "text", text: row.content }];
2080
+ }
2081
+ return { ...row, content };
2082
+ });
2083
+ }
2084
+ async function listConversationsWithParticipants(userId) {
2085
+ const convs = await listConversationsForUser(userId);
2086
+ if (convs.length === 0) return [];
2087
+ const db2 = await getDb();
2088
+ const convIds = convs.map((c) => c.id);
2089
+ const rows = await db2.select({
2090
+ conversationId: conversationParticipants.conversation_id,
2091
+ userId: users.id,
2092
+ username: users.username,
2093
+ userType: users.user_type,
2094
+ role: conversationParticipants.role
2095
+ }).from(conversationParticipants).innerJoin(users, eq4(conversationParticipants.user_id, users.id)).where(inArray(conversationParticipants.conversation_id, convIds));
2096
+ const byConv = /* @__PURE__ */ new Map();
2097
+ for (const r of rows) {
2098
+ let arr = byConv.get(r.conversationId);
2099
+ if (!arr) {
2100
+ arr = [];
2101
+ byConv.set(r.conversationId, arr);
2102
+ }
2103
+ arr.push({
2104
+ userId: r.userId,
2105
+ username: r.username,
2106
+ userType: r.userType,
2107
+ role: r.role
2108
+ });
2109
+ }
2110
+ return convs.map((c) => ({ ...c, participants: byConv.get(c.id) ?? [] }));
2111
+ }
2112
+ async function findDMConversation(agentName, participantIds) {
2113
+ const db2 = await getDb();
2114
+ const agentConvs = await db2.select({ id: conversations.id }).from(conversations).where(eq4(conversations.agent_name, agentName)).all();
2115
+ for (const conv of agentConvs) {
2116
+ const rows = await db2.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(eq4(conversationParticipants.conversation_id, conv.id)).all();
2117
+ if (rows.length !== 2) continue;
2118
+ const ids = new Set(rows.map((r) => r.user_id));
2119
+ if (ids.has(participantIds[0]) && ids.has(participantIds[1])) {
2120
+ return conv.id;
2121
+ }
2122
+ }
2123
+ return null;
2124
+ }
2125
+ async function deleteConversation(id) {
2126
+ const db2 = await getDb();
2127
+ await db2.delete(conversations).where(eq4(conversations.id, id));
2128
+ }
2129
+
2130
+ // src/web/routes/volute/chat.ts
2131
+ var chatSchema = z4.object({
2132
+ message: z4.string().optional(),
2133
+ conversationId: z4.string().optional(),
2134
+ sender: z4.string().optional(),
2135
+ images: z4.array(
2136
+ z4.object({
2137
+ media_type: z4.string(),
2138
+ data: z4.string()
2139
+ })
2140
+ ).optional()
2141
+ });
2142
+ function getDaemonUrl() {
2143
+ const data = JSON.parse(readFileSync4(resolve8(voluteHome(), "daemon.json"), "utf-8"));
2144
+ return `http://${daemonLoopback()}:${data.port}`;
2145
+ }
2146
+ function daemonFetchInternal(path, body) {
2147
+ const daemonUrl = getDaemonUrl();
2148
+ const token = process.env.VOLUTE_DAEMON_TOKEN;
2149
+ const headers = {
2150
+ "Content-Type": "application/json",
2151
+ Origin: daemonUrl
2152
+ };
2153
+ if (token) headers.Authorization = `Bearer ${token}`;
2154
+ return fetch(`${daemonUrl}${path}`, { method: "POST", headers, body });
2155
+ }
2156
+ function accumulateEvent(content, event) {
2157
+ if (event.type === "text") {
2158
+ const last = content[content.length - 1];
2159
+ if (last && last.type === "text") last.text += event.content;
2160
+ else content.push({ type: "text", text: event.content });
2161
+ } else if (event.type === "tool_use") {
2162
+ content.push({ type: "tool_use", name: event.name, input: event.input });
2163
+ } else if (event.type === "tool_result") {
2164
+ content.push({
2165
+ type: "tool_result",
2166
+ output: event.output,
2167
+ ...event.is_error ? { is_error: true } : {}
2168
+ });
2169
+ }
2170
+ }
2171
+ async function consumeAndPersist(res, conversationId, agentName) {
2172
+ if (!res.body) {
2173
+ console.warn(`[chat] no response body from ${agentName}`);
2174
+ return [];
2175
+ }
2176
+ const assistantContent = [];
2177
+ for await (const event of readNdjson(res.body)) {
2178
+ accumulateEvent(assistantContent, event);
2179
+ if (event.type === "done") break;
2180
+ }
2181
+ if (assistantContent.length === 0) return [];
2182
+ try {
2183
+ await addMessage(conversationId, "assistant", agentName, assistantContent);
2184
+ } catch (err) {
2185
+ console.error(`[chat] failed to persist conversation message from ${agentName}:`, err);
2186
+ }
2187
+ return assistantContent;
2188
+ }
2189
+ var app11 = new Hono11().post("/:name/chat", zValidator4("json", chatSchema), async (c) => {
2190
+ const name = c.req.param("name");
2191
+ const [baseName] = name.split("@", 2);
2192
+ const entry = findAgent(baseName);
2193
+ if (!entry) return c.json({ error: "Agent not found" }, 404);
2194
+ const body = c.req.valid("json");
2195
+ if (!body.message && (!body.images || body.images.length === 0)) {
2196
+ return c.json({ error: "message or images required" }, 400);
2197
+ }
2198
+ const user = c.get("user");
2199
+ const agentUser = await getOrCreateAgentUser(baseName);
2200
+ const senderName = user.id === 0 && body.sender ? body.sender : user.username;
2201
+ let conversationId = body.conversationId;
2202
+ if (conversationId) {
2203
+ if (user.id !== 0 && !await isParticipantOrOwner(conversationId, user.id)) {
2204
+ return c.json({ error: "Conversation not found" }, 404);
2205
+ }
2206
+ } else {
2207
+ const title = body.message ? body.message.slice(0, 80) : "Image message";
2208
+ const participantIds = [];
2209
+ if (user.id !== 0) {
2210
+ participantIds.push(user.id);
2211
+ } else if (body.sender) {
2212
+ const senderAgent = findAgent(body.sender);
2213
+ if (senderAgent) {
2214
+ const senderAgentUser = await getOrCreateAgentUser(body.sender);
2215
+ participantIds.push(senderAgentUser.id);
2216
+ }
2217
+ }
2218
+ participantIds.push(agentUser.id);
2219
+ if (participantIds.length === 2) {
2220
+ const existing = await findDMConversation(baseName, participantIds);
2221
+ if (existing) {
2222
+ conversationId = existing;
2223
+ }
2224
+ }
2225
+ if (!conversationId) {
2226
+ const conv = await createConversation(baseName, "volute", {
2227
+ userId: user.id !== 0 ? user.id : void 0,
2228
+ title,
2229
+ participantIds
2230
+ });
2231
+ conversationId = conv.id;
2232
+ }
2233
+ }
2234
+ const channel = `volute:${conversationId}`;
2235
+ const contentBlocks = [];
2236
+ if (body.message) {
2237
+ contentBlocks.push({ type: "text", text: body.message });
2238
+ }
2239
+ if (body.images) {
2240
+ for (const img of body.images) {
2241
+ contentBlocks.push({ type: "image", media_type: img.media_type, data: img.data });
2242
+ }
2243
+ }
2244
+ await addMessage(conversationId, "user", senderName, contentBlocks);
2245
+ const participants = await getParticipants(conversationId);
2246
+ const agentParticipants = participants.filter((p) => p.userType === "agent");
2247
+ const participantNames = participants.map((p) => p.username);
2248
+ const { getAgentManager: getAgentManager2 } = await import("./agent-manager-JDVXU3ON.js");
2249
+ const manager = getAgentManager2();
2250
+ const runningAgents = agentParticipants.map((ap) => {
2251
+ const agentKey = ap.username === baseName ? name : ap.username;
2252
+ return manager.isRunning(agentKey) ? ap.username : null;
2253
+ }).filter((n) => n !== null && n !== senderName);
2254
+ const isDM = participants.length === 2;
2255
+ const typingMap = getTypingMap();
2256
+ const currentlyTyping = typingMap.get(channel);
2257
+ const payload = JSON.stringify({
2258
+ content: contentBlocks,
2259
+ channel,
2260
+ sender: senderName,
2261
+ participants: participantNames,
2262
+ participantCount: participants.length,
2263
+ isDM,
2264
+ ...currentlyTyping.length > 0 ? { typing: currentlyTyping } : {}
2265
+ });
2266
+ const responses = [];
2267
+ for (const agentName of runningAgents) {
2268
+ const targetName = agentName === baseName ? name : agentName;
2269
+ try {
2270
+ const res = await daemonFetchInternal(
2271
+ `/api/agents/${encodeURIComponent(targetName)}/message`,
2272
+ payload
2273
+ );
2274
+ if (res.ok && res.body) {
2275
+ responses.push({ name: agentName, res });
2276
+ } else {
2277
+ const errorBody = await res.text().catch(() => "");
2278
+ console.error(
2279
+ `[chat] agent ${agentName} responded with ${res.status}: ${errorBody.slice(0, 500)}`
2280
+ );
2281
+ }
2282
+ } catch (err) {
2283
+ console.error(`[chat] agent ${agentName} unreachable via daemon:`, err);
2284
+ }
2285
+ }
2286
+ if (responses.length === 0) {
2287
+ return streamSSE3(c, async (stream2) => {
2288
+ await stream2.writeSSE({
2289
+ data: JSON.stringify({ type: "meta", conversationId })
2290
+ });
2291
+ await stream2.writeSSE({ data: JSON.stringify({ type: "sync" }) });
2292
+ });
2293
+ }
2294
+ const primary = responses[0];
2295
+ const secondary = responses.slice(1);
2296
+ const secondaryPromises = secondary.map((s) => consumeAndPersist(s.res, conversationId, s.name));
2297
+ return streamSSE3(c, async (stream2) => {
2298
+ await stream2.writeSSE({
2299
+ data: JSON.stringify({ type: "meta", conversationId, senderName: primary.name })
2300
+ });
2301
+ const assistantContent = [];
2302
+ try {
2303
+ for await (const event of readNdjson(primary.res.body)) {
2304
+ await stream2.writeSSE({ data: JSON.stringify(event) });
2305
+ accumulateEvent(assistantContent, event);
2306
+ if (event.type === "done") break;
2307
+ }
2308
+ } catch (err) {
2309
+ console.error(`[chat] error streaming response from ${primary.name}:`, err);
2310
+ await stream2.writeSSE({
2311
+ data: JSON.stringify({ type: "error", message: "Stream interrupted" })
2312
+ });
2313
+ }
2314
+ if (assistantContent.length > 0) {
2315
+ try {
2316
+ await addMessage(conversationId, "assistant", primary.name, assistantContent);
2317
+ } catch (err) {
2318
+ console.error(`[chat] failed to persist response from ${primary.name}:`, err);
2319
+ }
2320
+ }
2321
+ const results = await Promise.allSettled(secondaryPromises);
2322
+ for (let i = 0; i < results.length; i++) {
2323
+ if (results[i].status === "rejected") {
2324
+ console.error(
2325
+ `[chat] secondary agent ${secondary[i].name} response failed:`,
2326
+ results[i].reason
2327
+ );
2328
+ }
2329
+ }
2330
+ await stream2.writeSSE({ data: JSON.stringify({ type: "sync" }) });
2331
+ });
2332
+ });
2333
+ var chat_default = app11;
2334
+
2335
+ // src/web/routes/volute/conversations.ts
2336
+ import { zValidator as zValidator5 } from "@hono/zod-validator";
2337
+ import { Hono as Hono12 } from "hono";
2338
+ import { z as z5 } from "zod";
2339
+ var createConvSchema = z5.object({
2340
+ title: z5.string().optional(),
2341
+ participantIds: z5.array(z5.number()).optional(),
2342
+ participantNames: z5.array(z5.string()).optional()
2343
+ });
2344
+ var app12 = new Hono12().get("/:name/conversations", async (c) => {
2345
+ const name = c.req.param("name");
2346
+ const user = c.get("user");
2347
+ let lookupId = user.id;
2348
+ if (user.id === 0) {
2349
+ const agentUser = await getOrCreateAgentUser(name);
2350
+ lookupId = agentUser.id;
2351
+ }
2352
+ const all = await listConversationsForUser(lookupId);
2353
+ const convs = all.filter((c2) => c2.agent_name === name);
2354
+ return c.json(convs);
2355
+ }).post("/:name/conversations", zValidator5("json", createConvSchema), async (c) => {
2356
+ const name = c.req.param("name");
2357
+ const user = c.get("user");
2358
+ const body = c.req.valid("json");
2359
+ if (!body.participantIds?.length && !body.participantNames?.length) {
2360
+ return c.json({ error: "participantIds or participantNames required" }, 400);
2361
+ }
2362
+ const agentUser = await getOrCreateAgentUser(name);
2363
+ const participantSet = /* @__PURE__ */ new Set();
2364
+ if (user.id !== 0) participantSet.add(user.id);
2365
+ participantSet.add(agentUser.id);
2366
+ for (const id of body.participantIds ?? []) participantSet.add(id);
2367
+ if (body.participantNames) {
2368
+ for (const pname of body.participantNames) {
2369
+ const existing = await getUserByUsername(pname);
2370
+ if (existing) {
2371
+ participantSet.add(existing.id);
2372
+ continue;
2373
+ }
2374
+ if (findAgent(pname)) {
2375
+ const au = await getOrCreateAgentUser(pname);
2376
+ participantSet.add(au.id);
2377
+ continue;
2378
+ }
2379
+ return c.json({ error: `User not found: ${pname}` }, 400);
2380
+ }
2381
+ }
2382
+ for (const id of participantSet) {
2383
+ if (id === user.id || id === agentUser.id) continue;
2384
+ const u = await getUser(id);
2385
+ if (!u) return c.json({ error: `User ${id} not found` }, 400);
2386
+ }
2387
+ const participantIds = [...participantSet];
2388
+ if (participantIds.length === 2) {
2389
+ const existingId = await findDMConversation(name, participantIds);
2390
+ if (existingId) {
2391
+ const conv2 = await getConversation(existingId);
2392
+ if (conv2) return c.json(conv2);
2393
+ console.warn(`[conversations] DM conversation ${existingId} found but not retrievable`);
2394
+ }
2395
+ }
2396
+ const conv = await createConversation(name, "volute", {
2397
+ userId: user.id !== 0 ? user.id : void 0,
2398
+ title: body.title,
2399
+ participantIds
2400
+ });
2401
+ return c.json(conv, 201);
2402
+ }).get("/:name/conversations/:id/messages", async (c) => {
2403
+ const id = c.req.param("id");
2404
+ const user = c.get("user");
2405
+ if (user.id !== 0 && !await isParticipantOrOwner(id, user.id)) {
2406
+ return c.json({ error: "Conversation not found" }, 404);
2407
+ }
2408
+ const msgs = await getMessages(id);
2409
+ return c.json(msgs);
2410
+ }).get("/:name/conversations/:id/participants", async (c) => {
2411
+ const id = c.req.param("id");
2412
+ const user = c.get("user");
2413
+ if (user.id !== 0 && !await isParticipantOrOwner(id, user.id)) {
2414
+ return c.json({ error: "Conversation not found" }, 404);
2415
+ }
2416
+ const participants = await getParticipants(id);
2417
+ return c.json(participants);
2418
+ }).delete("/:name/conversations/:id", async (c) => {
2419
+ const id = c.req.param("id");
2420
+ const user = c.get("user");
2421
+ const deleted = await deleteConversationForUser(id, user.id);
2422
+ if (!deleted) return c.json({ error: "Conversation not found" }, 404);
2423
+ return c.json({ ok: true });
2424
+ });
2425
+ var conversations_default = app12;
2426
+
2427
+ // src/web/routes/volute/user-conversations.ts
2428
+ import { zValidator as zValidator6 } from "@hono/zod-validator";
2429
+ import { Hono as Hono13 } from "hono";
2430
+ import { z as z6 } from "zod";
2431
+ var createSchema = z6.object({
2432
+ title: z6.string().optional(),
2433
+ participantNames: z6.array(z6.string()).min(1)
2434
+ });
2435
+ var app13 = new Hono13().use("*", authMiddleware).get("/", async (c) => {
2436
+ const user = c.get("user");
2437
+ const convs = await listConversationsWithParticipants(user.id);
2438
+ return c.json(convs);
2439
+ }).get("/:id/messages", async (c) => {
2440
+ const id = c.req.param("id");
2441
+ const user = c.get("user");
2442
+ if (user.id !== 0 && !await isParticipantOrOwner(id, user.id)) {
2443
+ return c.json({ error: "Conversation not found" }, 404);
2444
+ }
2445
+ const msgs = await getMessages(id);
2446
+ return c.json(msgs);
2447
+ }).post("/", zValidator6("json", createSchema), async (c) => {
2448
+ const user = c.get("user");
2449
+ const body = c.req.valid("json");
2450
+ const participantIds = /* @__PURE__ */ new Set();
2451
+ if (user.id !== 0) participantIds.add(user.id);
2452
+ let firstAgentName;
2453
+ for (const name of body.participantNames) {
2454
+ const existing = await getUserByUsername(name);
2455
+ if (existing) {
2456
+ participantIds.add(existing.id);
2457
+ if (!firstAgentName && existing.user_type === "agent") firstAgentName = name;
2458
+ continue;
2459
+ }
2460
+ if (findAgent(name)) {
2461
+ const au = await getOrCreateAgentUser(name);
2462
+ participantIds.add(au.id);
2463
+ if (!firstAgentName) firstAgentName = name;
2464
+ continue;
2465
+ }
2466
+ return c.json({ error: `User not found: ${name}` }, 400);
2467
+ }
2468
+ if (!firstAgentName) {
2469
+ return c.json({ error: "At least one agent participant is required" }, 400);
2470
+ }
2471
+ const conv = await createConversation(firstAgentName, "volute", {
2472
+ userId: user.id !== 0 ? user.id : void 0,
2473
+ title: body.title,
2474
+ participantIds: [...participantIds]
2475
+ });
2476
+ return c.json(conv, 201);
2477
+ }).delete("/:id", async (c) => {
2478
+ const id = c.req.param("id");
2479
+ const user = c.get("user");
2480
+ const deleted = await deleteConversationForUser(id, user.id);
2481
+ if (!deleted) return c.json({ error: "Conversation not found" }, 404);
2482
+ return c.json({ ok: true });
2483
+ });
2484
+ var user_conversations_default = app13;
2485
+
1635
2486
  // src/web/app.ts
1636
- var app11 = new Hono11();
1637
- app11.onError((err, c) => {
2487
+ var app14 = new Hono14();
2488
+ app14.onError((err, c) => {
1638
2489
  if (err instanceof HTTPException) {
1639
2490
  return err.getResponse();
1640
2491
  }
@@ -1645,10 +2496,10 @@ app11.onError((err, c) => {
1645
2496
  });
1646
2497
  return c.json({ error: "Internal server error" }, 500);
1647
2498
  });
1648
- app11.notFound((c) => {
2499
+ app14.notFound((c) => {
1649
2500
  return c.json({ error: "Not found" }, 404);
1650
2501
  });
1651
- app11.use("*", async (c, next) => {
2502
+ app14.use("*", async (c, next) => {
1652
2503
  const start = Date.now();
1653
2504
  await next();
1654
2505
  const duration = Date.now() - start;
@@ -1659,15 +2510,28 @@ app11.use("*", async (c, next) => {
1659
2510
  duration
1660
2511
  });
1661
2512
  });
1662
- app11.get("/api/health", (c) => {
1663
- return c.json({ ok: true, version: "0.1.0" });
2513
+ app14.get("/api/health", (c) => {
2514
+ let version = "unknown";
2515
+ let cached = null;
2516
+ try {
2517
+ version = getCurrentVersion();
2518
+ cached = checkForUpdateCached();
2519
+ } catch (err) {
2520
+ logger_default.error("Health check error", { error: err.message });
2521
+ }
2522
+ return c.json({
2523
+ ok: true,
2524
+ version,
2525
+ ...cached?.updateAvailable ? { updateAvailable: true, latest: cached.latest } : {}
2526
+ });
1664
2527
  });
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;
2528
+ app14.use("/api/*", bodyLimit({ maxSize: 10 * 1024 * 1024 }));
2529
+ app14.use("/api/*", csrf());
2530
+ app14.use("/api/agents/*", authMiddleware);
2531
+ app14.use("/api/conversations/*", authMiddleware);
2532
+ app14.use("/api/system/*", authMiddleware);
2533
+ var routes = app14.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", typing_default).route("/api/agents", variants_default).route("/api/agents", files_default).route("/api/agents", conversations_default).route("/api/conversations", user_conversations_default);
2534
+ var app_default = app14;
1671
2535
 
1672
2536
  // src/web/server.ts
1673
2537
  var MIME_TYPES = {
@@ -1686,7 +2550,7 @@ async function startServer({
1686
2550
  let assetsDir = "";
1687
2551
  let searchDir = dirname3(new URL(import.meta.url).pathname);
1688
2552
  for (let i = 0; i < 5; i++) {
1689
- const candidate = resolve8(searchDir, "dist", "web-assets");
2553
+ const candidate = resolve9(searchDir, "dist", "web-assets");
1690
2554
  if (existsSync7(candidate)) {
1691
2555
  assetsDir = candidate;
1692
2556
  break;
@@ -1696,7 +2560,8 @@ async function startServer({
1696
2560
  if (assetsDir) {
1697
2561
  app_default.get("*", async (c) => {
1698
2562
  const urlPath = new URL(c.req.url).pathname;
1699
- const filePath = resolve8(assetsDir, urlPath.slice(1));
2563
+ if (urlPath.startsWith("/api/")) return c.notFound();
2564
+ const filePath = resolve9(assetsDir, urlPath.slice(1));
1700
2565
  if (!filePath.startsWith(assetsDir)) return c.text("Forbidden", 403);
1701
2566
  const s = await stat(filePath).catch(() => null);
1702
2567
  if (s?.isFile()) {
@@ -1705,7 +2570,7 @@ async function startServer({
1705
2570
  const body = await readFile2(filePath);
1706
2571
  return c.body(body, 200, { "Content-Type": mime });
1707
2572
  }
1708
- const indexPath = resolve8(assetsDir, "index.html");
2573
+ const indexPath = resolve9(assetsDir, "index.html");
1709
2574
  const indexStat = await stat(indexPath).catch(() => null);
1710
2575
  if (indexStat?.isFile()) {
1711
2576
  const body = await readFile2(indexPath, "utf-8");
@@ -1715,10 +2580,10 @@ async function startServer({
1715
2580
  });
1716
2581
  }
1717
2582
  const server = serve({ fetch: app_default.fetch, port, hostname });
1718
- await new Promise((resolve10, reject) => {
2583
+ await new Promise((resolve11, reject) => {
1719
2584
  server.on("listening", () => {
1720
2585
  logger_default.info("Volute UI running", { hostname, port });
1721
- resolve10();
2586
+ resolve11();
1722
2587
  });
1723
2588
  server.on("error", (err) => {
1724
2589
  reject(err);
@@ -1728,15 +2593,28 @@ async function startServer({
1728
2593
  }
1729
2594
 
1730
2595
  // src/daemon.ts
2596
+ if (!process.env.VOLUTE_HOME) {
2597
+ process.env.VOLUTE_HOME = resolve10(homedir(), ".volute");
2598
+ }
1731
2599
  async function startDaemon(opts) {
1732
2600
  const { port, hostname } = opts;
1733
2601
  const myPid = String(process.pid);
1734
2602
  const home = voluteHome();
1735
- const DAEMON_PID_PATH = resolve9(home, "daemon.pid");
1736
- const DAEMON_JSON_PATH = resolve9(home, "daemon.json");
2603
+ if (!opts.foreground) {
2604
+ const log2 = new RotatingLog(resolve10(home, "daemon.log"));
2605
+ const write2 = (...args) => log2.write(`${format(...args)}
2606
+ `);
2607
+ console.log = write2;
2608
+ console.error = write2;
2609
+ console.warn = write2;
2610
+ console.info = write2;
2611
+ }
2612
+ const DAEMON_PID_PATH = resolve10(home, "daemon.pid");
2613
+ const DAEMON_JSON_PATH = resolve10(home, "daemon.json");
1737
2614
  mkdirSync2(home, { recursive: true });
1738
2615
  const token = process.env.VOLUTE_DAEMON_TOKEN || randomBytes(32).toString("hex");
1739
2616
  process.env.VOLUTE_DAEMON_TOKEN = token;
2617
+ process.env.VOLUTE_DAEMON_HOSTNAME = hostname;
1740
2618
  let server;
1741
2619
  try {
1742
2620
  server = await startServer({ port, hostname });
@@ -1758,6 +2636,8 @@ async function startDaemon(opts) {
1758
2636
  const connectors = initConnectorManager();
1759
2637
  const scheduler = getScheduler();
1760
2638
  scheduler.start(port, token);
2639
+ const tokenBudget = getTokenBudget();
2640
+ tokenBudget.start(port, token);
1761
2641
  const registry = readRegistry();
1762
2642
  for (const entry of registry) {
1763
2643
  if (!entry.running) continue;
@@ -1766,6 +2646,14 @@ async function startDaemon(opts) {
1766
2646
  const dir = agentDir(entry.name);
1767
2647
  await connectors.startConnectors(entry.name, dir, entry.port, port);
1768
2648
  scheduler.loadSchedules(entry.name);
2649
+ const config = readVoluteConfig(dir);
2650
+ if (config?.tokenBudget) {
2651
+ tokenBudget.setBudget(
2652
+ entry.name,
2653
+ config.tokenBudget,
2654
+ config.tokenBudgetPeriodMinutes ?? DEFAULT_BUDGET_PERIOD_MINUTES
2655
+ );
2656
+ }
1769
2657
  } catch (err) {
1770
2658
  console.error(`[daemon] failed to start agent ${entry.name}:`, err);
1771
2659
  setAgentRunning(entry.name, false);
@@ -1786,13 +2674,13 @@ async function startDaemon(opts) {
1786
2674
  console.error(`[daemon] running on ${hostname}:${port}, pid ${myPid}`);
1787
2675
  function cleanup() {
1788
2676
  try {
1789
- if (readFileSync4(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
2677
+ if (readFileSync5(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
1790
2678
  unlinkSync2(DAEMON_PID_PATH);
1791
2679
  }
1792
2680
  } catch {
1793
2681
  }
1794
2682
  try {
1795
- const data = JSON.parse(readFileSync4(DAEMON_JSON_PATH, "utf-8"));
2683
+ const data = JSON.parse(readFileSync5(DAEMON_JSON_PATH, "utf-8"));
1796
2684
  if (data.token === token) {
1797
2685
  unlinkSync2(DAEMON_JSON_PATH);
1798
2686
  }
@@ -1806,6 +2694,7 @@ async function startDaemon(opts) {
1806
2694
  console.error("[daemon] shutting down...");
1807
2695
  scheduler.stop();
1808
2696
  scheduler.saveState();
2697
+ tokenBudget.stop();
1809
2698
  await connectors.stopAll();
1810
2699
  await manager.stopAll();
1811
2700
  manager.clearCrashAttempts();