volute 0.5.0 → 0.7.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 (62) hide show
  1. package/dist/{agent-Z2B6EFEQ.js → agent-7JF7MT73.js} +13 -9
  2. package/dist/{agent-manager-PXBKA2GK.js → agent-manager-IMZ7ZMBF.js} +4 -4
  3. package/dist/channel-SMCNOIVQ.js +262 -0
  4. package/dist/{chunk-MW2KFO3B.js → chunk-62X577Y7.js} +10 -8
  5. package/dist/chunk-7ACDT3P2.js +265 -0
  6. package/dist/{chunk-MXUCNIBG.js → chunk-BX7KI4S3.js} +68 -3
  7. package/dist/{up-7ILD7GU7.js → chunk-EG45HBSJ.js} +16 -4
  8. package/dist/{chunk-HE67X4T6.js → chunk-H7AMDUIA.js} +1 -1
  9. package/dist/{chunk-7L4AN5D4.js → chunk-JR4UXCTO.js} +1 -1
  10. package/dist/{down-O7IFZLVJ.js → chunk-LLJNZPCU.js} +48 -13
  11. package/dist/{chunk-5X7HGB6L.js → chunk-NKXULRSW.js} +2 -1
  12. package/dist/{chunk-UX25Z2ND.js → chunk-UWHWAPGO.js} +7 -0
  13. package/dist/{chunk-UAVD2AHX.js → chunk-W76KWE23.js} +1 -1
  14. package/dist/chunk-ZZOOTYXK.js +583 -0
  15. package/dist/cli.js +22 -21
  16. package/dist/{connector-LYEMXQEV.js → connector-Y7JPNROO.js} +3 -3
  17. package/dist/connectors/discord.js +38 -7
  18. package/dist/connectors/slack.js +22 -3
  19. package/dist/connectors/telegram.js +34 -4
  20. package/dist/{create-RVCZN6HE.js → create-G525LWEA.js} +2 -2
  21. package/dist/{daemon-client-ZY6UUN2M.js → daemon-client-442IV43D.js} +2 -2
  22. package/dist/daemon-restart-4HVEKYFY.js +23 -0
  23. package/dist/daemon.js +1042 -809
  24. package/dist/{delete-3QH7VYIN.js → delete-UOU4AFQN.js} +7 -3
  25. package/dist/down-AZVH5TCD.js +11 -0
  26. package/dist/{env-4D4REPJF.js → env-7GLUJCWS.js} +2 -2
  27. package/dist/{history-OEONB53Z.js → history-H72ZUIBN.js} +2 -2
  28. package/dist/{import-MXJB2EII.js → import-AVKQJDYC.js} +2 -2
  29. package/dist/{logs-DF342W4M.js → logs-EDGK26AK.js} +1 -1
  30. package/dist/{message-ADHWFHSI.js → message-SCOQDR3P.js} +2 -2
  31. package/dist/{package-VQOE7JNH.js → package-T2WAVJOU.js} +1 -1
  32. package/dist/restart-O4ETYLJF.js +29 -0
  33. package/dist/{schedule-NAG6F463.js → schedule-S6QVC5ON.js} +2 -2
  34. package/dist/send-G7PE4DOJ.js +72 -0
  35. package/dist/{setup-RPRRGG2F.js → setup-F4TCWVSP.js} +2 -2
  36. package/dist/{start-TUOXDSFL.js → start-VHQ7LNWM.js} +2 -2
  37. package/dist/{status-A36EHRO4.js → status-QAJWXKMZ.js} +2 -2
  38. package/dist/{stop-AOJZLQ5X.js → stop-CAGCT5NI.js} +2 -2
  39. package/dist/up-RWZF6MLT.js +12 -0
  40. package/dist/{update-LPSIAWQ2.js → update-F7QWV2LB.js} +2 -2
  41. package/dist/{update-check-Y33QDCFL.js → update-check-B4J6IEQ4.js} +2 -2
  42. package/dist/{upgrade-FX2TKJ2S.js → upgrade-YXKPWDRU.js} +2 -2
  43. package/dist/{variant-LAB67OC2.js → variant-4Z6W3PP6.js} +2 -2
  44. package/dist/web-assets/assets/index-B1CqjUYD.js +308 -0
  45. package/dist/web-assets/index.html +1 -1
  46. package/package.json +1 -1
  47. package/templates/_base/.init/.config/scripts/session-reader.ts +59 -0
  48. package/templates/_base/_skills/sessions/SKILL.md +49 -0
  49. package/templates/_base/_skills/volute-agent/SKILL.md +13 -9
  50. package/templates/_base/src/lib/format-prefix.ts +6 -0
  51. package/templates/_base/src/lib/router.ts +30 -3
  52. package/templates/_base/src/lib/session-monitor.ts +400 -0
  53. package/templates/_base/src/lib/types.ts +2 -0
  54. package/templates/agent-sdk/src/agent.ts +16 -0
  55. package/templates/agent-sdk/src/lib/hooks/session-context.ts +32 -0
  56. package/templates/pi/src/agent.ts +7 -1
  57. package/templates/pi/src/lib/session-context-extension.ts +33 -0
  58. package/dist/channel-MK5OK2SI.js +0 -113
  59. package/dist/chunk-SMISE4SV.js +0 -226
  60. package/dist/conversation-ERXEQZTY.js +0 -163
  61. package/dist/send-66QMKRUH.js +0 -75
  62. package/dist/web-assets/assets/index-BbRmoxoA.js +0 -308
package/dist/daemon.js CHANGED
@@ -6,34 +6,58 @@ import {
6
6
  initAgentManager,
7
7
  loadJsonMap,
8
8
  saveJsonMap
9
- } from "./chunk-MW2KFO3B.js";
9
+ } from "./chunk-62X577Y7.js";
10
10
  import {
11
11
  checkForUpdate,
12
12
  checkForUpdateCached,
13
13
  getCurrentVersion
14
- } from "./chunk-5X7HGB6L.js";
15
- import {
16
- CHANNELS
17
- } from "./chunk-SMISE4SV.js";
14
+ } from "./chunk-NKXULRSW.js";
18
15
  import {
19
16
  collectPart
20
17
  } from "./chunk-B3R6L2GW.js";
18
+ import {
19
+ CHANNELS
20
+ } from "./chunk-ZZOOTYXK.js";
21
+ import {
22
+ agentMessages,
23
+ approveUser,
24
+ conversationParticipants,
25
+ conversations,
26
+ createUser,
27
+ deleteAgentUser,
28
+ getDb,
29
+ getOrCreateAgentUser,
30
+ getUser,
31
+ getUserByUsername,
32
+ listPendingUsers,
33
+ listUsers,
34
+ listUsersByType,
35
+ messages,
36
+ sessions,
37
+ users,
38
+ verifyUser
39
+ } from "./chunk-7ACDT3P2.js";
21
40
  import {
22
41
  readVoluteConfig,
23
42
  writeVoluteConfig
24
43
  } from "./chunk-NETNFBA5.js";
25
44
  import {
26
45
  loadMergedEnv
27
- } from "./chunk-HE67X4T6.js";
46
+ } from "./chunk-H7AMDUIA.js";
47
+ import {
48
+ slugify,
49
+ writeChannelEntry
50
+ } from "./chunk-BX7KI4S3.js";
28
51
  import {
29
52
  applyIsolation
30
- } from "./chunk-UAVD2AHX.js";
53
+ } from "./chunk-W76KWE23.js";
31
54
  import {
32
55
  resolveVoluteBin
33
56
  } from "./chunk-5SKQ6J7T.js";
34
57
  import {
35
58
  agentDir,
36
59
  checkHealth,
60
+ daemonLoopback,
37
61
  findAgent,
38
62
  findVariant,
39
63
  getAllRunningVariants,
@@ -44,16 +68,14 @@ import {
44
68
  setAgentRunning,
45
69
  setVariantRunning,
46
70
  voluteHome
47
- } from "./chunk-UX25Z2ND.js";
48
- import {
49
- __export
50
- } from "./chunk-K3NQKI34.js";
71
+ } from "./chunk-UWHWAPGO.js";
72
+ import "./chunk-K3NQKI34.js";
51
73
 
52
74
  // src/daemon.ts
53
75
  import { randomBytes } from "crypto";
54
76
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
55
77
  import { homedir } from "os";
56
- import { resolve as resolve10 } from "path";
78
+ import { resolve as resolve9 } from "path";
57
79
  import { format } from "util";
58
80
 
59
81
  // src/lib/connector-manager.ts
@@ -232,7 +254,7 @@ var ConnectorManager = class {
232
254
  VOLUTE_AGENT_NAME: agentName,
233
255
  VOLUTE_AGENT_DIR: agentDir2,
234
256
  ...daemonPort ? {
235
- VOLUTE_DAEMON_URL: `http://127.0.0.1:${daemonPort}`,
257
+ VOLUTE_DAEMON_URL: `http://${daemonLoopback()}:${daemonPort}`,
236
258
  VOLUTE_DAEMON_TOKEN: process.env.VOLUTE_DAEMON_TOKEN
237
259
  } : {},
238
260
  ...connectorEnv
@@ -293,19 +315,19 @@ var ConnectorManager = class {
293
315
  const stopKey = `${agentName}:${type}`;
294
316
  this.stopping.add(stopKey);
295
317
  agentMap.delete(type);
296
- await new Promise((resolve11) => {
297
- tracked.child.on("exit", () => resolve11());
318
+ await new Promise((resolve10) => {
319
+ tracked.child.on("exit", () => resolve10());
298
320
  try {
299
321
  tracked.child.kill("SIGTERM");
300
322
  } catch {
301
- resolve11();
323
+ resolve10();
302
324
  }
303
325
  setTimeout(() => {
304
326
  try {
305
327
  tracked.child.kill("SIGKILL");
306
328
  } catch {
307
329
  }
308
- resolve11();
330
+ resolve10();
309
331
  }, 5e3);
310
332
  });
311
333
  this.stopping.delete(stopKey);
@@ -478,7 +500,7 @@ var Scheduler = class {
478
500
  try {
479
501
  let res;
480
502
  if (this.daemonPort && this.daemonToken) {
481
- const daemonUrl = `http://127.0.0.1:${this.daemonPort}`;
503
+ const daemonUrl = `http://${daemonLoopback()}:${this.daemonPort}`;
482
504
  res = await fetch(`${daemonUrl}/api/agents/${encodeURIComponent(agentName)}/message`, {
483
505
  method: "POST",
484
506
  headers: {
@@ -527,247 +549,185 @@ function getScheduler() {
527
549
  return instance2;
528
550
  }
529
551
 
530
- // src/web/middleware/auth.ts
531
- import { timingSafeEqual } from "crypto";
532
- import { eq as eq2, lt } from "drizzle-orm";
533
- import { getCookie } from "hono/cookie";
534
- import { createMiddleware } from "hono/factory";
535
-
536
- // src/lib/auth.ts
537
- import { compareSync, hashSync } from "bcryptjs";
538
- import { and, count, eq } from "drizzle-orm";
539
-
540
- // src/lib/db.ts
541
- import { chmodSync, existsSync as existsSync3 } from "fs";
542
- import { dirname as dirname2, resolve as resolve4 } from "path";
543
- import { fileURLToPath } from "url";
544
- import { drizzle } from "drizzle-orm/libsql";
545
- import { migrate } from "drizzle-orm/libsql/migrator";
546
-
547
- // src/lib/schema.ts
548
- var schema_exports = {};
549
- __export(schema_exports, {
550
- agentMessages: () => agentMessages,
551
- conversationParticipants: () => conversationParticipants,
552
- conversations: () => conversations,
553
- messages: () => messages,
554
- sessions: () => sessions,
555
- users: () => users
556
- });
557
- import { sql } from "drizzle-orm";
558
- import { index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
559
- var users = sqliteTable("users", {
560
- id: integer("id").primaryKey({ autoIncrement: true }),
561
- username: text("username").unique().notNull(),
562
- password_hash: text("password_hash").notNull(),
563
- role: text("role").notNull().default("pending"),
564
- user_type: text("user_type").notNull().default("human"),
565
- created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
566
- });
567
- var conversations = sqliteTable(
568
- "conversations",
569
- {
570
- id: text("id").primaryKey(),
571
- agent_name: text("agent_name").notNull(),
572
- channel: text("channel").notNull(),
573
- user_id: integer("user_id").references(() => users.id),
574
- title: text("title"),
575
- created_at: text("created_at").notNull().default(sql`(datetime('now'))`),
576
- updated_at: text("updated_at").notNull().default(sql`(datetime('now'))`)
577
- },
578
- (table) => [
579
- index("idx_conversations_agent_name").on(table.agent_name),
580
- index("idx_conversations_user_id").on(table.user_id),
581
- index("idx_conversations_updated_at").on(table.updated_at)
582
- ]
583
- );
584
- var agentMessages = sqliteTable(
585
- "agent_messages",
586
- {
587
- id: integer("id").primaryKey({ autoIncrement: true }),
588
- agent: text("agent").notNull(),
589
- channel: text("channel").notNull(),
590
- role: text("role").notNull(),
591
- sender: text("sender"),
592
- content: text("content").notNull(),
593
- created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
594
- },
595
- (table) => [
596
- index("idx_agent_messages_agent").on(table.agent),
597
- index("idx_agent_messages_channel").on(table.agent, table.channel)
598
- ]
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
- );
613
- var sessions = sqliteTable("sessions", {
614
- id: text("id").primaryKey(),
615
- userId: integer("user_id").references(() => users.id, { onDelete: "cascade" }).notNull(),
616
- createdAt: integer("created_at").notNull()
617
- });
618
- var messages = sqliteTable(
619
- "messages",
620
- {
621
- id: integer("id").primaryKey({ autoIncrement: true }),
622
- conversation_id: text("conversation_id").notNull().references(() => conversations.id, { onDelete: "cascade" }),
623
- role: text("role").notNull(),
624
- sender_name: text("sender_name"),
625
- content: text("content").notNull(),
626
- created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
627
- },
628
- (table) => [index("idx_messages_conversation_id").on(table.conversation_id)]
629
- );
630
-
631
- // src/lib/db.ts
632
- var __dirname = dirname2(fileURLToPath(import.meta.url));
633
- var migrationsFolder = existsSync3(resolve4(__dirname, "../drizzle")) ? resolve4(__dirname, "../drizzle") : resolve4(__dirname, "../../drizzle");
634
- var db = null;
635
- async function getDb() {
636
- if (db) return db;
637
- const dbPath = process.env.VOLUTE_DB_PATH || resolve4(voluteHome(), "volute.db");
638
- db = drizzle({ connection: { url: `file:${dbPath}` }, schema: schema_exports });
639
- await migrate(db, { migrationsFolder });
640
- try {
641
- chmodSync(dbPath, 384);
642
- } catch (err) {
643
- console.error(
644
- `[volute] WARNING: Failed to restrict database file permissions on ${dbPath}:`,
645
- err
646
- );
552
+ // src/lib/token-budget.ts
553
+ var DEFAULT_BUDGET_PERIOD_MINUTES = 60;
554
+ var MAX_QUEUE_SIZE = 100;
555
+ var TokenBudget = class {
556
+ budgets = /* @__PURE__ */ new Map();
557
+ interval = null;
558
+ daemonPort = null;
559
+ daemonToken = null;
560
+ start(daemonPort, daemonToken) {
561
+ this.daemonPort = daemonPort ?? null;
562
+ this.daemonToken = daemonToken ?? null;
563
+ this.interval = setInterval(() => this.tick(), 6e4);
647
564
  }
648
- return db;
649
- }
565
+ stop() {
566
+ if (this.interval) clearInterval(this.interval);
567
+ this.interval = null;
568
+ }
569
+ setBudget(agent, tokenLimit, periodMinutes) {
570
+ if (tokenLimit <= 0) return;
571
+ const existing = this.budgets.get(agent);
572
+ if (existing) {
573
+ existing.tokenLimit = tokenLimit;
574
+ existing.periodMinutes = periodMinutes;
575
+ } else {
576
+ this.budgets.set(agent, {
577
+ tokensUsed: 0,
578
+ periodStart: Date.now(),
579
+ periodMinutes,
580
+ tokenLimit,
581
+ queue: [],
582
+ warningInjected: false
583
+ });
584
+ }
585
+ }
586
+ removeBudget(agent) {
587
+ this.budgets.delete(agent);
588
+ }
589
+ recordUsage(agent, inputTokens, outputTokens) {
590
+ const state = this.budgets.get(agent);
591
+ if (!state) return;
592
+ state.tokensUsed += inputTokens + outputTokens;
593
+ }
594
+ /** Returns current budget status. Does not mutate state — call acknowledgeWarning() after delivering a warning. */
595
+ checkBudget(agent) {
596
+ const state = this.budgets.get(agent);
597
+ if (!state) return "ok";
598
+ const pct = state.tokensUsed / state.tokenLimit;
599
+ if (pct >= 1) return "exceeded";
600
+ if (pct >= 0.8 && !state.warningInjected) return "warning";
601
+ return "ok";
602
+ }
603
+ /** Mark warning as delivered for this period. Call after successfully injecting the warning. */
604
+ acknowledgeWarning(agent) {
605
+ const state = this.budgets.get(agent);
606
+ if (state) state.warningInjected = true;
607
+ }
608
+ enqueue(agent, message) {
609
+ const state = this.budgets.get(agent);
610
+ if (!state) return;
611
+ if (state.queue.length >= MAX_QUEUE_SIZE) {
612
+ state.queue.shift();
613
+ }
614
+ state.queue.push(message);
615
+ }
616
+ drain(agent) {
617
+ const state = this.budgets.get(agent);
618
+ if (!state) return [];
619
+ const messages2 = state.queue;
620
+ state.queue = [];
621
+ return messages2;
622
+ }
623
+ getUsage(agent) {
624
+ const state = this.budgets.get(agent);
625
+ if (!state) return null;
626
+ return {
627
+ tokensUsed: state.tokensUsed,
628
+ tokenLimit: state.tokenLimit,
629
+ periodMinutes: state.periodMinutes,
630
+ periodStart: state.periodStart,
631
+ queueLength: state.queue.length,
632
+ percentUsed: Math.round(state.tokensUsed / state.tokenLimit * 100)
633
+ };
634
+ }
635
+ tick() {
636
+ const now = Date.now();
637
+ for (const [agent, state] of this.budgets) {
638
+ const elapsed = now - state.periodStart;
639
+ if (elapsed >= state.periodMinutes * 6e4) {
640
+ state.tokensUsed = 0;
641
+ state.periodStart = now;
642
+ state.warningInjected = false;
643
+ const queued = this.drain(agent);
644
+ if (queued.length > 0) {
645
+ this.replay(agent, queued).catch((err) => {
646
+ console.error(`[token-budget] replay error for ${agent}:`, err);
647
+ });
648
+ }
649
+ }
650
+ }
651
+ }
652
+ async replay(agentName, messages2) {
653
+ if (!this.daemonPort || !this.daemonToken) {
654
+ console.error(
655
+ `[token-budget] cannot replay ${messages2.length} message(s) for ${agentName}: daemon not configured`
656
+ );
657
+ const state = this.budgets.get(agentName);
658
+ if (state) state.queue.push(...messages2);
659
+ return;
660
+ }
661
+ const summary = messages2.map((m) => {
662
+ const from = m.sender ? `[${m.sender}]` : "";
663
+ const ch = m.channel ? `(${m.channel})` : "";
664
+ return `${from}${ch} ${m.textContent}`;
665
+ }).join("\n");
666
+ const body = JSON.stringify({
667
+ content: [
668
+ {
669
+ type: "text",
670
+ text: `[Budget replay] ${messages2.length} queued message(s) from the previous budget period:
650
671
 
651
- // src/lib/auth.ts
652
- async function createUser(username, password) {
653
- const db2 = await getDb();
654
- const hash = hashSync(password, 10);
655
- const [{ value }] = await db2.select({ value: count() }).from(users).where(eq(users.user_type, "human"));
656
- const role = value === 0 ? "admin" : "pending";
657
- const [result] = await db2.insert(users).values({ username, password_hash: hash, role }).returning({
658
- id: users.id,
659
- username: users.username,
660
- role: users.role,
661
- user_type: users.user_type,
662
- created_at: users.created_at
663
- });
664
- return result;
665
- }
666
- async function verifyUser(username, password) {
667
- const db2 = await getDb();
668
- const row = await db2.select().from(users).where(eq(users.username, username)).get();
669
- if (!row) return null;
670
- if (row.user_type === "agent") return null;
671
- if (!compareSync(password, row.password_hash)) return null;
672
- const { password_hash: _, ...user } = row;
673
- return user;
674
- }
675
- async function getUser(id) {
676
- const db2 = await getDb();
677
- const row = await db2.select({
678
- id: users.id,
679
- username: users.username,
680
- role: users.role,
681
- user_type: users.user_type,
682
- created_at: users.created_at
683
- }).from(users).where(eq(users.id, id)).get();
684
- return row ?? null;
685
- }
686
- async function getUserByUsername(username) {
687
- const db2 = await getDb();
688
- const row = await db2.select({
689
- id: users.id,
690
- username: users.username,
691
- role: users.role,
692
- user_type: users.user_type,
693
- created_at: users.created_at
694
- }).from(users).where(eq(users.username, username)).get();
695
- return row ?? null;
696
- }
697
- async function listUsers() {
698
- const db2 = await getDb();
699
- return db2.select({
700
- id: users.id,
701
- username: users.username,
702
- role: users.role,
703
- user_type: users.user_type,
704
- created_at: users.created_at
705
- }).from(users).orderBy(users.created_at).all();
706
- }
707
- async function listPendingUsers() {
708
- const db2 = await getDb();
709
- return db2.select({
710
- id: users.id,
711
- username: users.username,
712
- role: users.role,
713
- user_type: users.user_type,
714
- created_at: users.created_at
715
- }).from(users).where(eq(users.role, "pending")).orderBy(users.created_at).all();
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
672
+ ${summary}`
673
+ }
674
+ ],
675
+ channel: "system:budget-replay",
676
+ sender: "system"
749
677
  });
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;
678
+ const daemonUrl = `http://${daemonLoopback()}:${this.daemonPort}`;
679
+ const controller = new AbortController();
680
+ const timeout = setTimeout(() => controller.abort(), 12e4);
681
+ try {
682
+ const res = await fetch(`${daemonUrl}/api/agents/${encodeURIComponent(agentName)}/message`, {
683
+ method: "POST",
684
+ headers: {
685
+ "Content-Type": "application/json",
686
+ Authorization: `Bearer ${this.daemonToken}`,
687
+ Origin: daemonUrl
688
+ },
689
+ body,
690
+ signal: controller.signal
691
+ });
692
+ if (!res.ok) {
693
+ console.error(`[token-budget] replay for ${agentName} got HTTP ${res.status}`);
694
+ } else {
695
+ console.error(
696
+ `[token-budget] replayed ${messages2.length} queued message(s) for ${agentName}`
697
+ );
698
+ }
699
+ try {
700
+ const reader = res.body?.getReader();
701
+ if (reader) {
702
+ try {
703
+ while (!(await reader.read()).done) {
704
+ }
705
+ } finally {
706
+ reader.releaseLock();
707
+ }
708
+ }
709
+ } catch {
710
+ }
711
+ } catch (err) {
712
+ console.error(`[token-budget] failed to replay for ${agentName}:`, err);
713
+ const state = this.budgets.get(agentName);
714
+ if (state) state.queue.push(...messages2);
715
+ } finally {
716
+ clearTimeout(timeout);
761
717
  }
762
- throw err;
763
718
  }
764
- }
765
- async function approveUser(id) {
766
- const db2 = await getDb();
767
- await db2.update(users).set({ role: "user" }).where(and(eq(users.id, id), eq(users.role, "pending")));
719
+ };
720
+ var instance3 = null;
721
+ function getTokenBudget() {
722
+ if (!instance3) instance3 = new TokenBudget();
723
+ return instance3;
768
724
  }
769
725
 
770
726
  // src/web/middleware/auth.ts
727
+ import { timingSafeEqual } from "crypto";
728
+ import { eq, lt } from "drizzle-orm";
729
+ import { getCookie } from "hono/cookie";
730
+ import { createMiddleware } from "hono/factory";
771
731
  function isValidDaemonToken(token) {
772
732
  const expected = process.env.VOLUTE_DAEMON_TOKEN;
773
733
  if (!expected || token.length !== expected.length) return false;
@@ -775,29 +735,29 @@ function isValidDaemonToken(token) {
775
735
  }
776
736
  var SESSION_MAX_AGE = 864e5;
777
737
  async function createSession(userId) {
778
- const db2 = await getDb();
738
+ const db = await getDb();
779
739
  const sessionId = crypto.randomUUID();
780
- await db2.insert(sessions).values({ id: sessionId, userId, createdAt: Date.now() });
740
+ await db.insert(sessions).values({ id: sessionId, userId, createdAt: Date.now() });
781
741
  return sessionId;
782
742
  }
783
743
  async function deleteSession(sessionId) {
784
- const db2 = await getDb();
785
- await db2.delete(sessions).where(eq2(sessions.id, sessionId));
744
+ const db = await getDb();
745
+ await db.delete(sessions).where(eq(sessions.id, sessionId));
786
746
  }
787
747
  async function getSessionUserId(sessionId) {
788
- const db2 = await getDb();
789
- const row = await db2.select().from(sessions).where(eq2(sessions.id, sessionId)).get();
748
+ const db = await getDb();
749
+ const row = await db.select().from(sessions).where(eq(sessions.id, sessionId)).get();
790
750
  if (!row) return void 0;
791
751
  if (Date.now() - row.createdAt > SESSION_MAX_AGE) {
792
- await db2.delete(sessions).where(eq2(sessions.id, sessionId));
752
+ await db.delete(sessions).where(eq(sessions.id, sessionId));
793
753
  return void 0;
794
754
  }
795
755
  return row.userId;
796
756
  }
797
757
  async function cleanExpiredSessions() {
798
- const db2 = await getDb();
758
+ const db = await getDb();
799
759
  const cutoff = Date.now() - SESSION_MAX_AGE;
800
- await db2.delete(sessions).where(lt(sessions.createdAt, cutoff));
760
+ await db.delete(sessions).where(lt(sessions.createdAt, cutoff));
801
761
  }
802
762
  var requireAdmin = createMiddleware(async (c, next) => {
803
763
  const user = c.get("user");
@@ -828,9 +788,9 @@ var authMiddleware = createMiddleware(async (c, next) => {
828
788
  });
829
789
 
830
790
  // src/web/server.ts
831
- import { existsSync as existsSync7 } from "fs";
791
+ import { existsSync as existsSync6 } from "fs";
832
792
  import { readFile as readFile2, stat } from "fs/promises";
833
- import { dirname as dirname3, extname, resolve as resolve9 } from "path";
793
+ import { dirname as dirname2, extname, resolve as resolve8 } from "path";
834
794
  import { serve } from "@hono/node-server";
835
795
 
836
796
  // src/lib/log-buffer.ts
@@ -878,15 +838,15 @@ var log = {
878
838
  var logger_default = log;
879
839
 
880
840
  // src/web/app.ts
881
- import { Hono as Hono13 } from "hono";
841
+ import { Hono as Hono14 } from "hono";
882
842
  import { bodyLimit } from "hono/body-limit";
883
843
  import { csrf } from "hono/csrf";
884
844
  import { HTTPException } from "hono/http-exception";
885
845
 
886
846
  // src/web/routes/agents.ts
887
- import { existsSync as existsSync4, readFileSync as readFileSync3, rmSync } from "fs";
888
- import { resolve as resolve5 } from "path";
889
- import { and as and2, desc, eq as eq3 } from "drizzle-orm";
847
+ import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync } from "fs";
848
+ import { resolve as resolve4 } from "path";
849
+ import { and, desc, eq as eq2 } from "drizzle-orm";
890
850
  import { Hono } from "hono";
891
851
  import { stream } from "hono/streaming";
892
852
 
@@ -929,10 +889,77 @@ async function* readNdjson(body) {
929
889
  }
930
890
  }
931
891
 
892
+ // src/lib/typing.ts
893
+ var DEFAULT_TTL_MS = 1e4;
894
+ var SWEEP_INTERVAL_MS = 5e3;
895
+ var TypingMap = class {
896
+ channels = /* @__PURE__ */ new Map();
897
+ sweepTimer;
898
+ constructor() {
899
+ this.sweepTimer = setInterval(() => this.sweep(), SWEEP_INTERVAL_MS);
900
+ this.sweepTimer.unref();
901
+ }
902
+ set(channel, sender, opts) {
903
+ const expiresAt = opts?.persistent ? Infinity : Date.now() + (opts?.ttlMs ?? DEFAULT_TTL_MS);
904
+ let senders = this.channels.get(channel);
905
+ if (!senders) {
906
+ senders = /* @__PURE__ */ new Map();
907
+ this.channels.set(channel, senders);
908
+ }
909
+ senders.set(sender, { expiresAt });
910
+ }
911
+ delete(channel, sender) {
912
+ const senders = this.channels.get(channel);
913
+ if (senders) {
914
+ senders.delete(sender);
915
+ if (senders.size === 0) {
916
+ this.channels.delete(channel);
917
+ }
918
+ }
919
+ }
920
+ get(channel) {
921
+ const senders = this.channels.get(channel);
922
+ if (!senders) return [];
923
+ const now = Date.now();
924
+ const result = [];
925
+ for (const [sender, entry] of senders) {
926
+ if (entry.expiresAt > now) {
927
+ result.push(sender);
928
+ }
929
+ }
930
+ return result;
931
+ }
932
+ dispose() {
933
+ clearInterval(this.sweepTimer);
934
+ this.channels.clear();
935
+ if (instance4 === this) instance4 = void 0;
936
+ }
937
+ sweep() {
938
+ const now = Date.now();
939
+ for (const [channel, senders] of this.channels) {
940
+ for (const [sender, entry] of senders) {
941
+ if (entry.expiresAt <= now) {
942
+ senders.delete(sender);
943
+ }
944
+ }
945
+ if (senders.size === 0) {
946
+ this.channels.delete(channel);
947
+ }
948
+ }
949
+ }
950
+ };
951
+ var instance4;
952
+ function getTypingMap() {
953
+ if (!instance4) {
954
+ instance4 = new TypingMap();
955
+ }
956
+ return instance4;
957
+ }
958
+
932
959
  // src/web/routes/agents.ts
933
960
  function getDaemonPort() {
934
961
  try {
935
- const data = JSON.parse(readFileSync3(resolve5(voluteHome(), "daemon.json"), "utf-8"));
962
+ const data = JSON.parse(readFileSync3(resolve4(voluteHome(), "daemon.json"), "utf-8"));
936
963
  return data.port;
937
964
  } catch {
938
965
  return void 0;
@@ -945,21 +972,25 @@ async function getAgentStatus(name, port) {
945
972
  const health = await checkHealth(port);
946
973
  status = health.ok ? "running" : "starting";
947
974
  }
975
+ const channelConfig = readVoluteConfig(agentDir(name))?.channels;
948
976
  const channels = [];
949
- channels.push({
950
- name: CHANNELS.volute.name,
951
- displayName: CHANNELS.volute.displayName,
952
- status: status === "running" ? "connected" : "disconnected",
953
- showToolCalls: CHANNELS.volute.showToolCalls
954
- });
977
+ for (const [, provider] of Object.entries(CHANNELS)) {
978
+ if (!provider.builtIn) continue;
979
+ channels.push({
980
+ name: provider.name,
981
+ displayName: provider.displayName,
982
+ status: status === "running" ? "connected" : "disconnected",
983
+ showToolCalls: channelConfig?.[provider.name]?.showToolCalls ?? provider.showToolCalls
984
+ });
985
+ }
955
986
  const connectorStatuses = getConnectorManager().getConnectorStatus(name);
956
987
  for (const cs of connectorStatuses) {
957
- const config = CHANNELS[cs.type];
988
+ const provider = CHANNELS[cs.type];
958
989
  channels.push({
959
- name: config?.name ?? cs.type,
960
- displayName: config?.displayName ?? cs.type,
990
+ name: provider?.name ?? cs.type,
991
+ displayName: provider?.displayName ?? cs.type,
961
992
  status: cs.running ? "connected" : "disconnected",
962
- showToolCalls: config?.showToolCalls ?? false
993
+ showToolCalls: channelConfig?.[cs.type]?.showToolCalls ?? provider?.showToolCalls ?? false
963
994
  });
964
995
  }
965
996
  return { status, channels };
@@ -977,7 +1008,7 @@ var app = new Hono().get("/", async (c) => {
977
1008
  const name = c.req.param("name");
978
1009
  const entry = findAgent(name);
979
1010
  if (!entry) return c.json({ error: "Agent not found" }, 404);
980
- if (!existsSync4(agentDir(name))) return c.json({ error: "Agent directory missing" }, 404);
1011
+ if (!existsSync3(agentDir(name))) return c.json({ error: "Agent directory missing" }, 404);
981
1012
  const { status, channels } = await getAgentStatus(name, entry.port);
982
1013
  const variants = readVariants(name);
983
1014
  const manager = getAgentManager();
@@ -1003,7 +1034,7 @@ var app = new Hono().get("/", async (c) => {
1003
1034
  if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
1004
1035
  } else {
1005
1036
  const dir = agentDir(baseName);
1006
- if (!existsSync4(dir)) return c.json({ error: "Agent directory missing" }, 404);
1037
+ if (!existsSync3(dir)) return c.json({ error: "Agent directory missing" }, 404);
1007
1038
  }
1008
1039
  const manager = getAgentManager();
1009
1040
  if (manager.isRunning(name)) {
@@ -1015,6 +1046,14 @@ var app = new Hono().get("/", async (c) => {
1015
1046
  const dir = agentDir(baseName);
1016
1047
  await getConnectorManager().startConnectors(baseName, dir, entry.port, getDaemonPort());
1017
1048
  getScheduler().loadSchedules(baseName);
1049
+ const config = readVoluteConfig(dir);
1050
+ if (config?.tokenBudget) {
1051
+ getTokenBudget().setBudget(
1052
+ baseName,
1053
+ config.tokenBudget,
1054
+ config.tokenBudgetPeriodMinutes ?? DEFAULT_BUDGET_PERIOD_MINUTES
1055
+ );
1056
+ }
1018
1057
  }
1019
1058
  return c.json({ ok: true });
1020
1059
  } catch (err) {
@@ -1030,13 +1069,16 @@ var app = new Hono().get("/", async (c) => {
1030
1069
  if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
1031
1070
  } else {
1032
1071
  const dir = agentDir(baseName);
1033
- if (!existsSync4(dir)) return c.json({ error: "Agent directory missing" }, 404);
1072
+ if (!existsSync3(dir)) return c.json({ error: "Agent directory missing" }, 404);
1034
1073
  }
1035
1074
  const manager = getAgentManager();
1036
1075
  const connectorManager = getConnectorManager();
1037
1076
  try {
1038
1077
  if (manager.isRunning(name)) {
1039
- if (!variantName) await connectorManager.stopConnectors(baseName);
1078
+ if (!variantName) {
1079
+ await connectorManager.stopConnectors(baseName);
1080
+ getTokenBudget().removeBudget(baseName);
1081
+ }
1040
1082
  await manager.stopAgent(name);
1041
1083
  }
1042
1084
  await manager.startAgent(name);
@@ -1044,6 +1086,14 @@ var app = new Hono().get("/", async (c) => {
1044
1086
  const dir = agentDir(baseName);
1045
1087
  await connectorManager.startConnectors(baseName, dir, entry.port, getDaemonPort());
1046
1088
  getScheduler().loadSchedules(baseName);
1089
+ const config = readVoluteConfig(dir);
1090
+ if (config?.tokenBudget) {
1091
+ getTokenBudget().setBudget(
1092
+ baseName,
1093
+ config.tokenBudget,
1094
+ config.tokenBudgetPeriodMinutes ?? DEFAULT_BUDGET_PERIOD_MINUTES
1095
+ );
1096
+ }
1047
1097
  }
1048
1098
  return c.json({ ok: true });
1049
1099
  } catch (err) {
@@ -1066,6 +1116,7 @@ var app = new Hono().get("/", async (c) => {
1066
1116
  if (!variantName) {
1067
1117
  await getConnectorManager().stopConnectors(baseName);
1068
1118
  getScheduler().unloadSchedules(baseName);
1119
+ getTokenBudget().removeBudget(baseName);
1069
1120
  }
1070
1121
  await manager.stopAgent(name);
1071
1122
  return c.json({ ok: true });
@@ -1081,11 +1132,13 @@ var app = new Hono().get("/", async (c) => {
1081
1132
  const manager = getAgentManager();
1082
1133
  if (manager.isRunning(name)) {
1083
1134
  await getConnectorManager().stopConnectors(name);
1135
+ getTokenBudget().removeBudget(name);
1084
1136
  await manager.stopAgent(name);
1085
1137
  }
1086
1138
  removeAllVariants(name);
1087
1139
  removeAgent(name);
1088
- if (force && existsSync4(dir)) {
1140
+ await deleteAgentUser(name);
1141
+ if (force && existsSync3(dir)) {
1089
1142
  rmSync(dir, { recursive: true, force: true });
1090
1143
  }
1091
1144
  return c.json({ ok: true });
@@ -1111,10 +1164,10 @@ var app = new Hono().get("/", async (c) => {
1111
1164
  console.error(`[daemon] failed to parse message body for ${baseName}:`, err);
1112
1165
  }
1113
1166
  const channel = parsed?.channel ?? "unknown";
1114
- const db2 = await getDb();
1167
+ const db = await getDb();
1115
1168
  if (parsed) {
1116
1169
  try {
1117
- const sender = parsed.sender ?? null;
1170
+ const sender2 = parsed.sender ?? null;
1118
1171
  let content;
1119
1172
  if (typeof parsed.content === "string") {
1120
1173
  content = parsed.content;
@@ -1123,23 +1176,74 @@ var app = new Hono().get("/", async (c) => {
1123
1176
  } else {
1124
1177
  content = JSON.stringify(parsed.content);
1125
1178
  }
1126
- await db2.insert(agentMessages).values({
1179
+ await db.insert(agentMessages).values({
1127
1180
  agent: baseName,
1128
1181
  channel,
1129
1182
  role: "user",
1130
- sender,
1183
+ sender: sender2,
1131
1184
  content
1132
1185
  });
1133
1186
  } catch (err) {
1134
1187
  console.error(`[daemon] failed to persist inbound message for ${baseName}:`, err);
1135
1188
  }
1136
1189
  }
1190
+ const budget = getTokenBudget();
1191
+ const budgetStatus = budget.checkBudget(baseName);
1192
+ if (budgetStatus === "exceeded") {
1193
+ let textContent = "";
1194
+ if (parsed) {
1195
+ if (typeof parsed.content === "string") {
1196
+ textContent = parsed.content;
1197
+ } else if (Array.isArray(parsed.content)) {
1198
+ textContent = parsed.content.filter((p) => p.type === "text" && p.text).map((p) => p.text).join("\n");
1199
+ }
1200
+ }
1201
+ budget.enqueue(baseName, {
1202
+ channel,
1203
+ sender: parsed?.sender ?? null,
1204
+ textContent
1205
+ });
1206
+ c.header("Content-Type", "application/x-ndjson");
1207
+ const encoder2 = new TextEncoder();
1208
+ return stream(c, async (s) => {
1209
+ await s.write(
1210
+ encoder2.encode(
1211
+ `${JSON.stringify({ type: "text", content: "[Token budget exceeded \u2014 message queued for next period]" })}
1212
+ `
1213
+ )
1214
+ );
1215
+ await s.write(encoder2.encode(`${JSON.stringify({ type: "done" })}
1216
+ `));
1217
+ });
1218
+ }
1219
+ const typingMap = getTypingMap();
1220
+ const sender = parsed?.sender ?? "";
1221
+ if (sender) typingMap.delete(channel, sender);
1222
+ const currentlyTyping = typingMap.get(channel).filter((s) => s !== baseName);
1223
+ let forwardBody = body;
1224
+ if (parsed && currentlyTyping.length > 0) {
1225
+ parsed.typing = currentlyTyping;
1226
+ forwardBody = JSON.stringify(parsed);
1227
+ }
1228
+ if (budgetStatus === "warning" && parsed) {
1229
+ const usage = budget.getUsage(baseName);
1230
+ const pct = usage?.percentUsed ?? 80;
1231
+ const warningText = `
1232
+ [System: Token budget is at ${pct}% \u2014 conserve tokens to avoid message queuing]`;
1233
+ if (typeof parsed.content === "string") {
1234
+ parsed.content = parsed.content + warningText;
1235
+ } else if (Array.isArray(parsed.content)) {
1236
+ parsed.content = [...parsed.content, { type: "text", text: warningText }];
1237
+ }
1238
+ budget.acknowledgeWarning(baseName);
1239
+ forwardBody = JSON.stringify(parsed);
1240
+ }
1137
1241
  let res;
1138
1242
  try {
1139
1243
  res = await fetch(`http://127.0.0.1:${port}/message`, {
1140
1244
  method: "POST",
1141
1245
  headers: { "Content-Type": "application/json" },
1142
- body
1246
+ body: forwardBody
1143
1247
  });
1144
1248
  } catch (err) {
1145
1249
  console.error(`[daemon] agent ${name} unreachable on port ${port}:`, err);
@@ -1153,49 +1257,92 @@ var app = new Hono().get("/", async (c) => {
1153
1257
  }
1154
1258
  c.header("Content-Type", "application/x-ndjson");
1155
1259
  const encoder = new TextEncoder();
1260
+ typingMap.set(channel, baseName, { persistent: true });
1156
1261
  return stream(c, async (s) => {
1157
- const textParts = [];
1158
- const toolParts = [];
1159
- for await (const event of readNdjson(res.body)) {
1160
- await s.write(encoder.encode(`${JSON.stringify(event)}
1262
+ try {
1263
+ const textParts = [];
1264
+ const toolParts = [];
1265
+ for await (const event of readNdjson(res.body)) {
1266
+ if (event.type === "usage") {
1267
+ const input = typeof event.input_tokens === "number" ? event.input_tokens : 0;
1268
+ const output = typeof event.output_tokens === "number" ? event.output_tokens : 0;
1269
+ budget.recordUsage(baseName, input, output);
1270
+ continue;
1271
+ }
1272
+ await s.write(encoder.encode(`${JSON.stringify(event)}
1161
1273
  `));
1162
- const part = collectPart(event);
1163
- if (part != null) {
1164
- if (event.type === "tool_use") toolParts.push(part);
1165
- else textParts.push(part);
1274
+ const part = collectPart(event);
1275
+ if (part != null) {
1276
+ if (event.type === "tool_use") toolParts.push(part);
1277
+ else textParts.push(part);
1278
+ }
1166
1279
  }
1167
- }
1168
- const content = [textParts.join(""), ...toolParts].filter(Boolean).join("\n");
1169
- if (content) {
1170
- try {
1171
- await db2.insert(agentMessages).values({
1172
- agent: baseName,
1173
- channel,
1174
- role: "assistant",
1175
- sender: baseName,
1176
- content
1177
- });
1178
- } catch (err) {
1179
- console.error(`[daemon] failed to persist assistant response for ${baseName}:`, err);
1280
+ const content = [textParts.join(""), ...toolParts].filter(Boolean).join("\n");
1281
+ if (content) {
1282
+ try {
1283
+ await db.insert(agentMessages).values({
1284
+ agent: baseName,
1285
+ channel,
1286
+ role: "assistant",
1287
+ sender: baseName,
1288
+ content
1289
+ });
1290
+ } catch (err) {
1291
+ console.error(`[daemon] failed to persist assistant response for ${baseName}:`, err);
1292
+ }
1180
1293
  }
1294
+ } finally {
1295
+ typingMap.delete(channel, baseName);
1181
1296
  }
1182
1297
  });
1298
+ }).get("/:name/budget", async (c) => {
1299
+ const name = c.req.param("name");
1300
+ const [baseName] = name.split("@", 2);
1301
+ const usage = getTokenBudget().getUsage(baseName);
1302
+ if (!usage) return c.json({ error: "No budget configured" }, 404);
1303
+ return c.json(usage);
1304
+ }).post("/:name/history", async (c) => {
1305
+ const name = c.req.param("name");
1306
+ const [baseName] = name.split("@", 2);
1307
+ let body;
1308
+ try {
1309
+ body = await c.req.json();
1310
+ } catch {
1311
+ return c.json({ error: "Invalid JSON" }, 400);
1312
+ }
1313
+ if (!body.channel || !body.content) {
1314
+ return c.json({ error: "channel and content required" }, 400);
1315
+ }
1316
+ const db = await getDb();
1317
+ try {
1318
+ await db.insert(agentMessages).values({
1319
+ agent: baseName,
1320
+ channel: body.channel,
1321
+ role: "assistant",
1322
+ sender: baseName,
1323
+ content: body.content
1324
+ });
1325
+ } catch (err) {
1326
+ console.error(`[daemon] failed to persist external send for ${baseName}:`, err);
1327
+ return c.json({ error: "Failed to persist" }, 500);
1328
+ }
1329
+ return c.json({ ok: true });
1183
1330
  }).get("/:name/history/channels", async (c) => {
1184
1331
  const name = c.req.param("name");
1185
- const db2 = await getDb();
1186
- const rows = await db2.selectDistinct({ channel: agentMessages.channel }).from(agentMessages).where(eq3(agentMessages.agent, name));
1332
+ const db = await getDb();
1333
+ const rows = await db.selectDistinct({ channel: agentMessages.channel }).from(agentMessages).where(eq2(agentMessages.agent, name));
1187
1334
  return c.json(rows.map((r) => r.channel));
1188
1335
  }).get("/:name/history", async (c) => {
1189
1336
  const name = c.req.param("name");
1190
1337
  const channel = c.req.query("channel");
1191
1338
  const limit = Math.min(Math.max(parseInt(c.req.query("limit") ?? "50", 10) || 50, 1), 200);
1192
1339
  const offset = Math.max(parseInt(c.req.query("offset") ?? "0", 10) || 0, 0);
1193
- const db2 = await getDb();
1194
- const conditions = [eq3(agentMessages.agent, name)];
1340
+ const db = await getDb();
1341
+ const conditions = [eq2(agentMessages.agent, name)];
1195
1342
  if (channel) {
1196
- conditions.push(eq3(agentMessages.channel, channel));
1343
+ conditions.push(eq2(agentMessages.channel, channel));
1197
1344
  }
1198
- const rows = await db2.select().from(agentMessages).where(and2(...conditions)).orderBy(desc(agentMessages.created_at)).limit(limit).offset(offset);
1345
+ const rows = await db.select().from(agentMessages).where(and(...conditions)).orderBy(desc(agentMessages.created_at)).limit(limit).offset(offset);
1199
1346
  return c.json(rows);
1200
1347
  });
1201
1348
  var agents_default = app;
@@ -1271,94 +1418,431 @@ var app2 = new Hono2().post("/register", zValidator("json", credentialsSchema),
1271
1418
  }).route("/", admin);
1272
1419
  var auth_default = app2;
1273
1420
 
1274
- // src/web/routes/chat.ts
1275
- import { readFileSync as readFileSync4 } from "fs";
1276
- import { resolve as resolve6 } from "path";
1277
- import { zValidator as zValidator2 } from "@hono/zod-validator";
1421
+ // src/web/routes/connectors.ts
1278
1422
  import { Hono as Hono3 } from "hono";
1279
- import { streamSSE } from "hono/streaming";
1280
- import { z as z2 } from "zod";
1281
-
1282
- // src/lib/conversations.ts
1283
- import { randomUUID } from "crypto";
1284
- import { and as and3, desc as desc2, eq as eq4, inArray, isNull, sql as sql2 } from "drizzle-orm";
1285
- async function createConversation(agentName, channel, opts) {
1286
- const db2 = await getDb();
1287
- const id = randomUUID();
1288
- await db2.insert(conversations).values({
1289
- id,
1290
- agent_name: agentName,
1291
- channel,
1292
- user_id: opts?.userId ?? null,
1293
- title: opts?.title ?? null
1423
+ var CONNECTOR_TYPE_RE = /^[a-z][a-z0-9-]*$/;
1424
+ var app3 = new Hono3().get("/:name/connectors", (c) => {
1425
+ const name = c.req.param("name");
1426
+ const entry = findAgent(name);
1427
+ if (!entry) return c.json({ error: "Agent not found" }, 404);
1428
+ const dir = agentDir(name);
1429
+ const config = readVoluteConfig(dir) ?? {};
1430
+ const configured = config.connectors ?? [];
1431
+ const manager = getConnectorManager();
1432
+ const runningStatus = manager.getConnectorStatus(name);
1433
+ const connectors = configured.map((type) => {
1434
+ const status = runningStatus.find((s) => s.type === type);
1435
+ return { type, running: status?.running ?? false };
1294
1436
  });
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
- }))
1437
+ return c.json(connectors);
1438
+ }).post("/:name/connectors/:type", requireAdmin, async (c) => {
1439
+ const name = c.req.param("name");
1440
+ const type = c.req.param("type");
1441
+ if (!CONNECTOR_TYPE_RE.test(type)) {
1442
+ return c.json({ error: "Invalid connector type" }, 400);
1443
+ }
1444
+ const entry = findAgent(name);
1445
+ if (!entry) return c.json({ error: "Agent not found" }, 404);
1446
+ const dir = agentDir(name);
1447
+ const manager = getConnectorManager();
1448
+ const envCheck = manager.checkConnectorEnv(type, dir);
1449
+ if (envCheck) {
1450
+ return c.json(
1451
+ {
1452
+ error: "missing_env",
1453
+ missing: envCheck.missing,
1454
+ connectorName: envCheck.connectorName
1455
+ },
1456
+ 400
1302
1457
  );
1303
1458
  }
1304
- return {
1305
- id,
1306
- agent_name: agentName,
1307
- channel,
1308
- user_id: opts?.userId ?? null,
1309
- title: opts?.title ?? null,
1310
- created_at: (/* @__PURE__ */ new Date()).toISOString(),
1311
- updated_at: (/* @__PURE__ */ new Date()).toISOString()
1312
- };
1313
- }
1314
- async function getParticipants(conversationId) {
1315
- const db2 = await getDb();
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;
1346
- }
1347
- async function deleteConversationForUser(id, userId) {
1459
+ const config = readVoluteConfig(dir) ?? {};
1460
+ const connectors = config.connectors ?? [];
1461
+ if (!connectors.includes(type)) {
1462
+ config.connectors = [...connectors, type];
1463
+ writeVoluteConfig(dir, config);
1464
+ }
1465
+ try {
1466
+ await manager.startConnector(name, dir, entry.port, type);
1467
+ return c.json({ ok: true });
1468
+ } catch (err) {
1469
+ return c.json(
1470
+ { error: err instanceof Error ? err.message : "Failed to start connector" },
1471
+ 500
1472
+ );
1473
+ }
1474
+ }).delete("/:name/connectors/:type", requireAdmin, async (c) => {
1475
+ const name = c.req.param("name");
1476
+ const type = c.req.param("type");
1477
+ if (!CONNECTOR_TYPE_RE.test(type)) {
1478
+ return c.json({ error: "Invalid connector type" }, 400);
1479
+ }
1480
+ const entry = findAgent(name);
1481
+ if (!entry) return c.json({ error: "Agent not found" }, 404);
1482
+ const dir = agentDir(name);
1483
+ const manager = getConnectorManager();
1484
+ await manager.stopConnector(name, type);
1485
+ const config = readVoluteConfig(dir) ?? {};
1486
+ config.connectors = (config.connectors ?? []).filter((t) => t !== type);
1487
+ writeVoluteConfig(dir, config);
1488
+ return c.json({ ok: true });
1489
+ });
1490
+ var connectors_default = app3;
1491
+
1492
+ // src/web/routes/files.ts
1493
+ import { existsSync as existsSync4 } from "fs";
1494
+ import { readdir, readFile, writeFile } from "fs/promises";
1495
+ import { resolve as resolve5 } from "path";
1496
+ import { zValidator as zValidator2 } from "@hono/zod-validator";
1497
+ import { Hono as Hono4 } from "hono";
1498
+ import { z as z2 } from "zod";
1499
+ var ALLOWED_FILES = /* @__PURE__ */ new Set(["SOUL.md", "MEMORY.md", "CLAUDE.md", "VOLUTE.md"]);
1500
+ var saveFileSchema = z2.object({ content: z2.string() });
1501
+ var app4 = new Hono4().get("/:name/files", async (c) => {
1502
+ const name = c.req.param("name");
1503
+ const entry = findAgent(name);
1504
+ if (!entry) return c.json({ error: "Agent not found" }, 404);
1505
+ const dir = agentDir(name);
1506
+ const homeDir = resolve5(dir, "home");
1507
+ if (!existsSync4(homeDir)) return c.json({ error: "Home directory missing" }, 404);
1508
+ const allFiles = await readdir(homeDir);
1509
+ const files = allFiles.filter((f) => f.endsWith(".md") && ALLOWED_FILES.has(f));
1510
+ return c.json(files);
1511
+ }).get("/:name/files/:filename", async (c) => {
1512
+ const name = c.req.param("name");
1513
+ const filename = c.req.param("filename");
1514
+ if (!ALLOWED_FILES.has(filename)) {
1515
+ return c.json({ error: "File not allowed" }, 403);
1516
+ }
1517
+ const entry = findAgent(name);
1518
+ if (!entry) return c.json({ error: "Agent not found" }, 404);
1519
+ const dir = agentDir(name);
1520
+ const filePath = resolve5(dir, "home", filename);
1521
+ if (!existsSync4(filePath)) {
1522
+ return c.json({ error: "File not found" }, 404);
1523
+ }
1524
+ const content = await readFile(filePath, "utf-8");
1525
+ return c.json({ filename, content });
1526
+ }).put("/:name/files/:filename", zValidator2("json", saveFileSchema), async (c) => {
1527
+ const name = c.req.param("name");
1528
+ const filename = c.req.param("filename");
1529
+ if (!ALLOWED_FILES.has(filename)) {
1530
+ return c.json({ error: "File not allowed" }, 403);
1531
+ }
1532
+ const entry = findAgent(name);
1533
+ if (!entry) return c.json({ error: "Agent not found" }, 404);
1534
+ const dir = agentDir(name);
1535
+ const filePath = resolve5(dir, "home", filename);
1536
+ const { content } = c.req.valid("json");
1537
+ await writeFile(filePath, content);
1538
+ return c.json({ ok: true });
1539
+ });
1540
+ var files_default = app4;
1541
+
1542
+ // src/web/routes/logs.ts
1543
+ import { spawn as spawn2 } from "child_process";
1544
+ import { existsSync as existsSync5 } from "fs";
1545
+ import { resolve as resolve6 } from "path";
1546
+ import { Hono as Hono5 } from "hono";
1547
+ import { streamSSE } from "hono/streaming";
1548
+ var app5 = new Hono5().get("/:name/logs", async (c) => {
1549
+ const name = c.req.param("name");
1550
+ const entry = findAgent(name);
1551
+ if (!entry) return c.json({ error: "Agent not found" }, 404);
1552
+ const dir = agentDir(name);
1553
+ const logFile = resolve6(dir, ".volute", "logs", "agent.log");
1554
+ if (!existsSync5(logFile)) {
1555
+ return c.json({ error: "No log file found" }, 404);
1556
+ }
1557
+ return streamSSE(c, async (stream2) => {
1558
+ const tail = spawn2("tail", ["-n", "200", "-f", logFile]);
1559
+ const onData = (data) => {
1560
+ const lines = data.toString().split("\n");
1561
+ for (const line of lines) {
1562
+ if (line) {
1563
+ stream2.writeSSE({ data: line }).catch(() => {
1564
+ });
1565
+ }
1566
+ }
1567
+ };
1568
+ tail.stdout.on("data", onData);
1569
+ stream2.onAbort(() => {
1570
+ tail.kill();
1571
+ });
1572
+ await new Promise((resolve10) => {
1573
+ tail.on("exit", resolve10);
1574
+ stream2.onAbort(resolve10);
1575
+ });
1576
+ });
1577
+ });
1578
+ var logs_default = app5;
1579
+
1580
+ // src/web/routes/schedules.ts
1581
+ import { Hono as Hono6 } from "hono";
1582
+ function readSchedules(name) {
1583
+ return readVoluteConfig(agentDir(name))?.schedules ?? [];
1584
+ }
1585
+ function writeSchedules(name, schedules) {
1586
+ const dir = agentDir(name);
1587
+ const config = readVoluteConfig(dir) ?? {};
1588
+ config.schedules = schedules.length > 0 ? schedules : void 0;
1589
+ writeVoluteConfig(dir, config);
1590
+ getScheduler().loadSchedules(name);
1591
+ }
1592
+ var app6 = new Hono6().get("/:name/schedules", (c) => {
1593
+ const name = c.req.param("name");
1594
+ if (!findAgent(name)) return c.json({ error: "Agent not found" }, 404);
1595
+ return c.json(readSchedules(name));
1596
+ }).post("/:name/schedules", requireAdmin, async (c) => {
1597
+ const name = c.req.param("name");
1598
+ if (!findAgent(name)) return c.json({ error: "Agent not found" }, 404);
1599
+ const body = await c.req.json();
1600
+ if (!body.cron || !body.message) {
1601
+ return c.json({ error: "cron and message are required" }, 400);
1602
+ }
1603
+ const schedules = readSchedules(name);
1604
+ const id = body.id || `schedule-${Date.now()}`;
1605
+ if (schedules.some((s) => s.id === id)) {
1606
+ return c.json({ error: `Schedule "${id}" already exists` }, 409);
1607
+ }
1608
+ schedules.push({ id, cron: body.cron, message: body.message, enabled: body.enabled ?? true });
1609
+ writeSchedules(name, schedules);
1610
+ return c.json({ ok: true, id }, 201);
1611
+ }).put("/:name/schedules/:id", requireAdmin, async (c) => {
1612
+ const name = c.req.param("name");
1613
+ const id = c.req.param("id");
1614
+ if (!findAgent(name)) return c.json({ error: "Agent not found" }, 404);
1615
+ const schedules = readSchedules(name);
1616
+ const idx = schedules.findIndex((s) => s.id === id);
1617
+ if (idx === -1) return c.json({ error: "Schedule not found" }, 404);
1618
+ const body = await c.req.json();
1619
+ if (body.cron !== void 0) schedules[idx].cron = body.cron;
1620
+ if (body.message !== void 0) schedules[idx].message = body.message;
1621
+ if (body.enabled !== void 0) schedules[idx].enabled = body.enabled;
1622
+ writeSchedules(name, schedules);
1623
+ return c.json({ ok: true });
1624
+ }).delete("/:name/schedules/:id", requireAdmin, (c) => {
1625
+ const name = c.req.param("name");
1626
+ const id = c.req.param("id");
1627
+ if (!findAgent(name)) return c.json({ error: "Agent not found" }, 404);
1628
+ const schedules = readSchedules(name);
1629
+ const filtered = schedules.filter((s) => s.id !== id);
1630
+ if (filtered.length === schedules.length) {
1631
+ return c.json({ error: "Schedule not found" }, 404);
1632
+ }
1633
+ writeSchedules(name, filtered);
1634
+ return c.json({ ok: true });
1635
+ }).post("/:name/webhook/:event", async (c) => {
1636
+ const name = c.req.param("name");
1637
+ const event = c.req.param("event");
1638
+ const entry = findAgent(name);
1639
+ if (!entry) return c.json({ error: "Agent not found" }, 404);
1640
+ const body = await c.req.text();
1641
+ const message = `[webhook: ${event}] ${body}`;
1642
+ try {
1643
+ const res = await fetch(`http://127.0.0.1:${entry.port}/message`, {
1644
+ method: "POST",
1645
+ headers: { "Content-Type": "application/json" },
1646
+ body: JSON.stringify({
1647
+ content: [{ type: "text", text: message }],
1648
+ channel: "system:webhook",
1649
+ sender: "webhook"
1650
+ })
1651
+ });
1652
+ if (!res.ok) {
1653
+ return c.json({ error: `Agent responded with ${res.status}` }, 502);
1654
+ }
1655
+ return c.json({ ok: true });
1656
+ } catch {
1657
+ return c.json({ error: "Failed to reach agent" }, 502);
1658
+ }
1659
+ });
1660
+ var schedules_default = app6;
1661
+
1662
+ // src/web/routes/system.ts
1663
+ import { Hono as Hono7 } from "hono";
1664
+ import { streamSSE as streamSSE2 } from "hono/streaming";
1665
+ var app7 = new Hono7().get("/logs", async (c) => {
1666
+ const user = c.get("user");
1667
+ if (user.role !== "admin") return c.json({ error: "Forbidden" }, 403);
1668
+ return streamSSE2(c, async (stream2) => {
1669
+ for (const entry of logBuffer.getEntries()) {
1670
+ await stream2.writeSSE({ data: JSON.stringify(entry) });
1671
+ }
1672
+ const unsubscribe = logBuffer.subscribe((entry) => {
1673
+ stream2.writeSSE({ data: JSON.stringify(entry) }).catch(() => {
1674
+ });
1675
+ });
1676
+ await new Promise((resolve10) => {
1677
+ stream2.onAbort(() => {
1678
+ unsubscribe();
1679
+ resolve10();
1680
+ });
1681
+ });
1682
+ });
1683
+ });
1684
+ var system_default = app7;
1685
+
1686
+ // src/web/routes/typing.ts
1687
+ import { zValidator as zValidator3 } from "@hono/zod-validator";
1688
+ import { Hono as Hono8 } from "hono";
1689
+ import { z as z3 } from "zod";
1690
+ var typingSchema = z3.object({
1691
+ channel: z3.string().min(1),
1692
+ sender: z3.string().min(1),
1693
+ active: z3.boolean()
1694
+ });
1695
+ var app8 = new Hono8().post("/:name/typing", zValidator3("json", typingSchema), (c) => {
1696
+ const { channel, sender, active } = c.req.valid("json");
1697
+ const map = getTypingMap();
1698
+ if (active) {
1699
+ map.set(channel, sender);
1700
+ } else {
1701
+ map.delete(channel, sender);
1702
+ }
1703
+ return c.json({ ok: true });
1704
+ }).get("/:name/typing", (c) => {
1705
+ const channel = c.req.query("channel");
1706
+ if (!channel) {
1707
+ return c.json({ error: "channel query param is required" }, 400);
1708
+ }
1709
+ const map = getTypingMap();
1710
+ return c.json({ typing: map.get(channel) });
1711
+ });
1712
+ var typing_default = app8;
1713
+
1714
+ // src/web/routes/update.ts
1715
+ import { spawn as spawn3 } from "child_process";
1716
+ import { Hono as Hono9 } from "hono";
1717
+ var bin;
1718
+ var app9 = new Hono9().get("/update", async (c) => {
1719
+ const result = await checkForUpdate();
1720
+ return c.json(result);
1721
+ }).post("/update", requireAdmin, async (c) => {
1722
+ bin ??= resolveVoluteBin();
1723
+ const child = spawn3(bin, ["update"], {
1724
+ stdio: "ignore",
1725
+ detached: true
1726
+ });
1727
+ child.on("error", (err) => {
1728
+ logger_default.error("Update process error", { error: err.message });
1729
+ });
1730
+ child.unref();
1731
+ return c.json({ ok: true, message: "Updating..." });
1732
+ });
1733
+ var update_default = app9;
1734
+
1735
+ // src/web/routes/variants.ts
1736
+ import { Hono as Hono10 } from "hono";
1737
+ var app10 = new Hono10().get("/:name/variants", async (c) => {
1738
+ const name = c.req.param("name");
1739
+ const entry = findAgent(name);
1740
+ if (!entry) return c.json({ error: "Agent not found" }, 404);
1741
+ const variants = readVariants(name);
1742
+ const results = await Promise.all(
1743
+ variants.map(async (v) => {
1744
+ if (!v.port) return { ...v, status: "no-server" };
1745
+ const health = await checkHealth(v.port);
1746
+ return { ...v, status: health.ok ? "running" : "dead" };
1747
+ })
1748
+ );
1749
+ return c.json(results);
1750
+ });
1751
+ var variants_default = app10;
1752
+
1753
+ // src/web/routes/volute/chat.ts
1754
+ import { readFileSync as readFileSync4 } from "fs";
1755
+ import { resolve as resolve7 } from "path";
1756
+ import { zValidator as zValidator4 } from "@hono/zod-validator";
1757
+ import { Hono as Hono11 } from "hono";
1758
+ import { streamSSE as streamSSE3 } from "hono/streaming";
1759
+ import { z as z4 } from "zod";
1760
+
1761
+ // src/lib/conversations.ts
1762
+ import { randomUUID } from "crypto";
1763
+ import { and as and2, desc as desc2, eq as eq3, inArray, isNull, sql } from "drizzle-orm";
1764
+ async function createConversation(agentName, channel, opts) {
1765
+ const db = await getDb();
1766
+ const id = randomUUID();
1767
+ await db.insert(conversations).values({
1768
+ id,
1769
+ agent_name: agentName,
1770
+ channel,
1771
+ user_id: opts?.userId ?? null,
1772
+ title: opts?.title ?? null
1773
+ });
1774
+ if (opts?.participantIds && opts.participantIds.length > 0) {
1775
+ await db.insert(conversationParticipants).values(
1776
+ opts.participantIds.map((uid, i) => ({
1777
+ conversation_id: id,
1778
+ user_id: uid,
1779
+ role: i === 0 ? "owner" : "member"
1780
+ }))
1781
+ );
1782
+ }
1783
+ return {
1784
+ id,
1785
+ agent_name: agentName,
1786
+ channel,
1787
+ user_id: opts?.userId ?? null,
1788
+ title: opts?.title ?? null,
1789
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
1790
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1791
+ };
1792
+ }
1793
+ async function getConversation(id) {
1794
+ const db = await getDb();
1795
+ const row = await db.select().from(conversations).where(eq3(conversations.id, id)).get();
1796
+ return row ?? null;
1797
+ }
1798
+ async function getParticipants(conversationId) {
1799
+ const db = await getDb();
1800
+ const rows = await db.select({
1801
+ userId: conversationParticipants.user_id,
1802
+ username: users.username,
1803
+ userType: users.user_type,
1804
+ role: conversationParticipants.role
1805
+ }).from(conversationParticipants).innerJoin(users, eq3(conversationParticipants.user_id, users.id)).where(eq3(conversationParticipants.conversation_id, conversationId)).all();
1806
+ return rows;
1807
+ }
1808
+ async function isParticipant(conversationId, userId) {
1809
+ const db = await getDb();
1810
+ const row = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(
1811
+ and2(
1812
+ eq3(conversationParticipants.conversation_id, conversationId),
1813
+ eq3(conversationParticipants.user_id, userId)
1814
+ )
1815
+ ).get();
1816
+ return row != null;
1817
+ }
1818
+ async function listConversationsForUser(userId) {
1819
+ const db = await getDb();
1820
+ const participantRows = await db.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(eq3(conversationParticipants.user_id, userId)).all();
1821
+ if (participantRows.length === 0) return [];
1822
+ const convIds = participantRows.map((r) => r.conversation_id);
1823
+ return db.select().from(conversations).where(inArray(conversations.id, convIds)).orderBy(desc2(conversations.updated_at)).all();
1824
+ }
1825
+ async function isParticipantOrOwner(conversationId, userId) {
1826
+ if (await isParticipant(conversationId, userId)) return true;
1827
+ const db = await getDb();
1828
+ const row = await db.select().from(conversations).where(and2(eq3(conversations.id, conversationId), eq3(conversations.user_id, userId))).get();
1829
+ return row != null;
1830
+ }
1831
+ async function deleteConversationForUser(id, userId) {
1348
1832
  if (!await isParticipantOrOwner(id, userId)) return false;
1349
1833
  await deleteConversation(id);
1350
1834
  return true;
1351
1835
  }
1352
1836
  async function addMessage(conversationId, role, senderName, content) {
1353
- const db2 = await getDb();
1837
+ const db = await getDb();
1354
1838
  const serialized = JSON.stringify(content);
1355
- 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 });
1356
- await db2.update(conversations).set({ updated_at: sql2`datetime('now')` }).where(eq4(conversations.id, conversationId));
1839
+ const [result] = await db.insert(messages).values({ conversation_id: conversationId, role, sender_name: senderName, content: serialized }).returning({ id: messages.id, created_at: messages.created_at });
1840
+ await db.update(conversations).set({ updated_at: sql`datetime('now')` }).where(eq3(conversations.id, conversationId));
1357
1841
  if (role === "user") {
1358
1842
  const firstText = content.find((b) => b.type === "text");
1359
1843
  const title = firstText ? firstText.text.slice(0, 80) : "";
1360
1844
  if (title) {
1361
- await db2.update(conversations).set({ title }).where(and3(eq4(conversations.id, conversationId), isNull(conversations.title)));
1845
+ await db.update(conversations).set({ title }).where(and2(eq3(conversations.id, conversationId), isNull(conversations.title)));
1362
1846
  }
1363
1847
  }
1364
1848
  return {
@@ -1371,8 +1855,8 @@ async function addMessage(conversationId, role, senderName, content) {
1371
1855
  };
1372
1856
  }
1373
1857
  async function getMessages(conversationId) {
1374
- const db2 = await getDb();
1375
- const rows = await db2.select().from(messages).where(eq4(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
1858
+ const db = await getDb();
1859
+ const rows = await db.select().from(messages).where(eq3(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
1376
1860
  return rows.map((row) => {
1377
1861
  let content;
1378
1862
  try {
@@ -1387,15 +1871,15 @@ async function getMessages(conversationId) {
1387
1871
  async function listConversationsWithParticipants(userId) {
1388
1872
  const convs = await listConversationsForUser(userId);
1389
1873
  if (convs.length === 0) return [];
1390
- const db2 = await getDb();
1874
+ const db = await getDb();
1391
1875
  const convIds = convs.map((c) => c.id);
1392
- const rows = await db2.select({
1876
+ const rows = await db.select({
1393
1877
  conversationId: conversationParticipants.conversation_id,
1394
1878
  userId: users.id,
1395
1879
  username: users.username,
1396
1880
  userType: users.user_type,
1397
1881
  role: conversationParticipants.role
1398
- }).from(conversationParticipants).innerJoin(users, eq4(conversationParticipants.user_id, users.id)).where(inArray(conversationParticipants.conversation_id, convIds));
1882
+ }).from(conversationParticipants).innerJoin(users, eq3(conversationParticipants.user_id, users.id)).where(inArray(conversationParticipants.conversation_id, convIds));
1399
1883
  const byConv = /* @__PURE__ */ new Map();
1400
1884
  for (const r of rows) {
1401
1885
  let arr = byConv.get(r.conversationId);
@@ -1412,26 +1896,39 @@ async function listConversationsWithParticipants(userId) {
1412
1896
  }
1413
1897
  return convs.map((c) => ({ ...c, participants: byConv.get(c.id) ?? [] }));
1414
1898
  }
1899
+ async function findDMConversation(agentName, participantIds) {
1900
+ const db = await getDb();
1901
+ const agentConvs = await db.select({ id: conversations.id }).from(conversations).where(eq3(conversations.agent_name, agentName)).all();
1902
+ for (const conv of agentConvs) {
1903
+ const rows = await db.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(eq3(conversationParticipants.conversation_id, conv.id)).all();
1904
+ if (rows.length !== 2) continue;
1905
+ const ids = new Set(rows.map((r) => r.user_id));
1906
+ if (ids.has(participantIds[0]) && ids.has(participantIds[1])) {
1907
+ return conv.id;
1908
+ }
1909
+ }
1910
+ return null;
1911
+ }
1415
1912
  async function deleteConversation(id) {
1416
- const db2 = await getDb();
1417
- await db2.delete(conversations).where(eq4(conversations.id, id));
1913
+ const db = await getDb();
1914
+ await db.delete(conversations).where(eq3(conversations.id, id));
1418
1915
  }
1419
1916
 
1420
- // src/web/routes/chat.ts
1421
- var chatSchema = z2.object({
1422
- message: z2.string().optional(),
1423
- conversationId: z2.string().optional(),
1424
- sender: z2.string().optional(),
1425
- images: z2.array(
1426
- z2.object({
1427
- media_type: z2.string(),
1428
- data: z2.string()
1917
+ // src/web/routes/volute/chat.ts
1918
+ var chatSchema = z4.object({
1919
+ message: z4.string().optional(),
1920
+ conversationId: z4.string().optional(),
1921
+ sender: z4.string().optional(),
1922
+ images: z4.array(
1923
+ z4.object({
1924
+ media_type: z4.string(),
1925
+ data: z4.string()
1429
1926
  })
1430
1927
  ).optional()
1431
1928
  });
1432
1929
  function getDaemonUrl() {
1433
- const data = JSON.parse(readFileSync4(resolve6(voluteHome(), "daemon.json"), "utf-8"));
1434
- return `http://127.0.0.1:${data.port}`;
1930
+ const data = JSON.parse(readFileSync4(resolve7(voluteHome(), "daemon.json"), "utf-8"));
1931
+ return `http://${daemonLoopback()}:${data.port}`;
1435
1932
  }
1436
1933
  function daemonFetchInternal(path, body) {
1437
1934
  const daemonUrl = getDaemonUrl();
@@ -1476,15 +1973,11 @@ async function consumeAndPersist(res, conversationId, agentName) {
1476
1973
  }
1477
1974
  return assistantContent;
1478
1975
  }
1479
- var app3 = new Hono3().post("/:name/chat", zValidator2("json", chatSchema), async (c) => {
1976
+ var app11 = new Hono11().post("/:name/chat", zValidator4("json", chatSchema), async (c) => {
1480
1977
  const name = c.req.param("name");
1481
1978
  const [baseName] = name.split("@", 2);
1482
1979
  const entry = findAgent(baseName);
1483
1980
  if (!entry) return c.json({ error: "Agent not found" }, 404);
1484
- const { getAgentManager: getAgentManager2 } = await import("./agent-manager-PXBKA2GK.js");
1485
- if (!getAgentManager2().isRunning(name)) {
1486
- return c.json({ error: "Agent is not running" }, 409);
1487
- }
1488
1981
  const body = c.req.valid("json");
1489
1982
  if (!body.message && (!body.images || body.images.length === 0)) {
1490
1983
  return c.json({ error: "message or images required" }, 400);
@@ -1510,14 +2003,24 @@ var app3 = new Hono3().post("/:name/chat", zValidator2("json", chatSchema), asyn
1510
2003
  }
1511
2004
  }
1512
2005
  participantIds.push(agentUser.id);
1513
- const conv = await createConversation(baseName, "volute", {
1514
- userId: user.id !== 0 ? user.id : void 0,
1515
- title,
1516
- participantIds
1517
- });
1518
- conversationId = conv.id;
2006
+ if (participantIds.length === 2) {
2007
+ const existing = await findDMConversation(baseName, participantIds);
2008
+ if (existing) {
2009
+ conversationId = existing;
2010
+ }
2011
+ }
2012
+ if (!conversationId) {
2013
+ const conv2 = await createConversation(baseName, "volute", {
2014
+ userId: user.id !== 0 ? user.id : void 0,
2015
+ title,
2016
+ participantIds
2017
+ });
2018
+ conversationId = conv2.id;
2019
+ }
1519
2020
  }
1520
- const channel = `volute:${conversationId}`;
2021
+ const conv = await getConversation(conversationId);
2022
+ const convTitle = conv?.title;
2023
+ const channel = convTitle ? `volute:${slugify(convTitle)}` : `volute:${conversationId}`;
1521
2024
  const contentBlocks = [];
1522
2025
  if (body.message) {
1523
2026
  contentBlocks.push({ type: "text", text: body.message });
@@ -1531,22 +2034,30 @@ var app3 = new Hono3().post("/:name/chat", zValidator2("json", chatSchema), asyn
1531
2034
  const participants = await getParticipants(conversationId);
1532
2035
  const agentParticipants = participants.filter((p) => p.userType === "agent");
1533
2036
  const participantNames = participants.map((p) => p.username);
2037
+ const { getAgentManager: getAgentManager2 } = await import("./agent-manager-IMZ7ZMBF.js");
1534
2038
  const manager = getAgentManager2();
1535
2039
  const runningAgents = agentParticipants.map((ap) => {
1536
2040
  const agentKey = ap.username === baseName ? name : ap.username;
1537
2041
  return manager.isRunning(agentKey) ? ap.username : null;
1538
2042
  }).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
2043
  const isDM = participants.length === 2;
2044
+ const dir = agentDir(baseName);
2045
+ writeChannelEntry(dir, channel, {
2046
+ platformId: conversationId,
2047
+ platform: "volute",
2048
+ name: convTitle ?? void 0,
2049
+ type: isDM ? "dm" : "group"
2050
+ });
2051
+ const typingMap = getTypingMap();
2052
+ const currentlyTyping = typingMap.get(channel);
1543
2053
  const payload = JSON.stringify({
1544
2054
  content: contentBlocks,
1545
2055
  channel,
1546
2056
  sender: senderName,
1547
2057
  participants: participantNames,
1548
2058
  participantCount: participants.length,
1549
- isDM
2059
+ isDM,
2060
+ ...currentlyTyping.length > 0 ? { typing: currentlyTyping } : {}
1550
2061
  });
1551
2062
  const responses = [];
1552
2063
  for (const agentName of runningAgents) {
@@ -1569,130 +2080,64 @@ var app3 = new Hono3().post("/:name/chat", zValidator2("json", chatSchema), asyn
1569
2080
  }
1570
2081
  }
1571
2082
  if (responses.length === 0) {
1572
- return c.json({ error: "No agents reachable" }, 502);
2083
+ return streamSSE3(c, async (stream2) => {
2084
+ await stream2.writeSSE({
2085
+ data: JSON.stringify({ type: "meta", conversationId })
2086
+ });
2087
+ await stream2.writeSSE({ data: JSON.stringify({ type: "sync" }) });
2088
+ });
1573
2089
  }
1574
2090
  const primary = responses[0];
1575
2091
  const secondary = responses.slice(1);
1576
2092
  const secondaryPromises = secondary.map((s) => consumeAndPersist(s.res, conversationId, s.name));
1577
- return streamSSE(c, async (stream2) => {
2093
+ return streamSSE3(c, async (stream2) => {
1578
2094
  await stream2.writeSSE({
1579
- data: JSON.stringify({ type: "meta", conversationId, senderName: primary.name })
1580
- });
1581
- const assistantContent = [];
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;
1587
- }
1588
- } catch (err) {
1589
- console.error(`[chat] error streaming response from ${primary.name}:`, err);
1590
- await stream2.writeSSE({
1591
- data: JSON.stringify({ type: "error", message: "Stream interrupted" })
1592
- });
1593
- }
1594
- if (assistantContent.length > 0) {
1595
- try {
1596
- await addMessage(conversationId, "assistant", primary.name, assistantContent);
1597
- } catch (err) {
1598
- console.error(`[chat] failed to persist response from ${primary.name}:`, err);
1599
- }
1600
- }
1601
- const results = await Promise.allSettled(secondaryPromises);
1602
- for (let i = 0; i < results.length; i++) {
1603
- if (results[i].status === "rejected") {
1604
- console.error(
1605
- `[chat] secondary agent ${secondary[i].name} response failed:`,
1606
- results[i].reason
1607
- );
1608
- }
1609
- }
1610
- await stream2.writeSSE({ data: JSON.stringify({ type: "sync" }) });
1611
- });
1612
- });
1613
- var chat_default = app3;
1614
-
1615
- // src/web/routes/connectors.ts
1616
- import { Hono as Hono4 } from "hono";
1617
- var CONNECTOR_TYPE_RE = /^[a-z][a-z0-9-]*$/;
1618
- var app4 = new Hono4().get("/:name/connectors", (c) => {
1619
- const name = c.req.param("name");
1620
- const entry = findAgent(name);
1621
- if (!entry) return c.json({ error: "Agent not found" }, 404);
1622
- const dir = agentDir(name);
1623
- const config = readVoluteConfig(dir) ?? {};
1624
- const configured = config.connectors ?? [];
1625
- const manager = getConnectorManager();
1626
- const runningStatus = manager.getConnectorStatus(name);
1627
- const connectors = configured.map((type) => {
1628
- const status = runningStatus.find((s) => s.type === type);
1629
- return { type, running: status?.running ?? false };
1630
- });
1631
- return c.json(connectors);
1632
- }).post("/:name/connectors/:type", requireAdmin, async (c) => {
1633
- const name = c.req.param("name");
1634
- const type = c.req.param("type");
1635
- if (!CONNECTOR_TYPE_RE.test(type)) {
1636
- return c.json({ error: "Invalid connector type" }, 400);
1637
- }
1638
- const entry = findAgent(name);
1639
- if (!entry) return c.json({ error: "Agent not found" }, 404);
1640
- const dir = agentDir(name);
1641
- const manager = getConnectorManager();
1642
- const envCheck = manager.checkConnectorEnv(type, dir);
1643
- if (envCheck) {
1644
- return c.json(
1645
- {
1646
- error: "missing_env",
1647
- missing: envCheck.missing,
1648
- connectorName: envCheck.connectorName
1649
- },
1650
- 400
1651
- );
1652
- }
1653
- const config = readVoluteConfig(dir) ?? {};
1654
- const connectors = config.connectors ?? [];
1655
- if (!connectors.includes(type)) {
1656
- config.connectors = [...connectors, type];
1657
- writeVoluteConfig(dir, config);
1658
- }
1659
- try {
1660
- await manager.startConnector(name, dir, entry.port, type);
1661
- return c.json({ ok: true });
1662
- } catch (err) {
1663
- return c.json(
1664
- { error: err instanceof Error ? err.message : "Failed to start connector" },
1665
- 500
1666
- );
1667
- }
1668
- }).delete("/:name/connectors/:type", requireAdmin, async (c) => {
1669
- const name = c.req.param("name");
1670
- const type = c.req.param("type");
1671
- if (!CONNECTOR_TYPE_RE.test(type)) {
1672
- return c.json({ error: "Invalid connector type" }, 400);
1673
- }
1674
- const entry = findAgent(name);
1675
- if (!entry) return c.json({ error: "Agent not found" }, 404);
1676
- const dir = agentDir(name);
1677
- const manager = getConnectorManager();
1678
- await manager.stopConnector(name, type);
1679
- const config = readVoluteConfig(dir) ?? {};
1680
- config.connectors = (config.connectors ?? []).filter((t) => t !== type);
1681
- writeVoluteConfig(dir, config);
1682
- return c.json({ ok: true });
2095
+ data: JSON.stringify({ type: "meta", conversationId, senderName: primary.name })
2096
+ });
2097
+ const assistantContent = [];
2098
+ try {
2099
+ for await (const event of readNdjson(primary.res.body)) {
2100
+ await stream2.writeSSE({ data: JSON.stringify(event) });
2101
+ accumulateEvent(assistantContent, event);
2102
+ if (event.type === "done") break;
2103
+ }
2104
+ } catch (err) {
2105
+ console.error(`[chat] error streaming response from ${primary.name}:`, err);
2106
+ await stream2.writeSSE({
2107
+ data: JSON.stringify({ type: "error", message: "Stream interrupted" })
2108
+ });
2109
+ }
2110
+ if (assistantContent.length > 0) {
2111
+ try {
2112
+ await addMessage(conversationId, "assistant", primary.name, assistantContent);
2113
+ } catch (err) {
2114
+ console.error(`[chat] failed to persist response from ${primary.name}:`, err);
2115
+ }
2116
+ }
2117
+ const results = await Promise.allSettled(secondaryPromises);
2118
+ for (let i = 0; i < results.length; i++) {
2119
+ if (results[i].status === "rejected") {
2120
+ console.error(
2121
+ `[chat] secondary agent ${secondary[i].name} response failed:`,
2122
+ results[i].reason
2123
+ );
2124
+ }
2125
+ }
2126
+ await stream2.writeSSE({ data: JSON.stringify({ type: "sync" }) });
2127
+ });
1683
2128
  });
1684
- var connectors_default = app4;
2129
+ var chat_default = app11;
1685
2130
 
1686
- // src/web/routes/conversations.ts
1687
- import { zValidator as zValidator3 } from "@hono/zod-validator";
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()
2131
+ // src/web/routes/volute/conversations.ts
2132
+ import { zValidator as zValidator5 } from "@hono/zod-validator";
2133
+ import { Hono as Hono12 } from "hono";
2134
+ import { z as z5 } from "zod";
2135
+ var createConvSchema = z5.object({
2136
+ title: z5.string().optional(),
2137
+ participantIds: z5.array(z5.number()).optional(),
2138
+ participantNames: z5.array(z5.string()).optional()
1694
2139
  });
1695
- var app5 = new Hono5().get("/:name/conversations", async (c) => {
2140
+ var app12 = new Hono12().get("/:name/conversations", async (c) => {
1696
2141
  const name = c.req.param("name");
1697
2142
  const user = c.get("user");
1698
2143
  let lookupId = user.id;
@@ -1703,7 +2148,7 @@ var app5 = new Hono5().get("/:name/conversations", async (c) => {
1703
2148
  const all = await listConversationsForUser(lookupId);
1704
2149
  const convs = all.filter((c2) => c2.agent_name === name);
1705
2150
  return c.json(convs);
1706
- }).post("/:name/conversations", zValidator3("json", createConvSchema), async (c) => {
2151
+ }).post("/:name/conversations", zValidator5("json", createConvSchema), async (c) => {
1707
2152
  const name = c.req.param("name");
1708
2153
  const user = c.get("user");
1709
2154
  const body = c.req.valid("json");
@@ -1735,10 +2180,19 @@ var app5 = new Hono5().get("/:name/conversations", async (c) => {
1735
2180
  const u = await getUser(id);
1736
2181
  if (!u) return c.json({ error: `User ${id} not found` }, 400);
1737
2182
  }
2183
+ const participantIds = [...participantSet];
2184
+ if (participantIds.length === 2) {
2185
+ const existingId = await findDMConversation(name, participantIds);
2186
+ if (existingId) {
2187
+ const conv2 = await getConversation(existingId);
2188
+ if (conv2) return c.json(conv2);
2189
+ console.warn(`[conversations] DM conversation ${existingId} found but not retrievable`);
2190
+ }
2191
+ }
1738
2192
  const conv = await createConversation(name, "volute", {
1739
2193
  userId: user.id !== 0 ? user.id : void 0,
1740
2194
  title: body.title,
1741
- participantIds: [...participantSet]
2195
+ participantIds
1742
2196
  });
1743
2197
  return c.json(conv, 201);
1744
2198
  }).get("/:name/conversations/:id/messages", async (c) => {
@@ -1752,7 +2206,7 @@ var app5 = new Hono5().get("/:name/conversations", async (c) => {
1752
2206
  }).get("/:name/conversations/:id/participants", async (c) => {
1753
2207
  const id = c.req.param("id");
1754
2208
  const user = c.get("user");
1755
- if (!await isParticipantOrOwner(id, user.id)) {
2209
+ if (user.id !== 0 && !await isParticipantOrOwner(id, user.id)) {
1756
2210
  return c.json({ error: "Conversation not found" }, 404);
1757
2211
  }
1758
2212
  const participants = await getParticipants(id);
@@ -1764,232 +2218,17 @@ var app5 = new Hono5().get("/:name/conversations", async (c) => {
1764
2218
  if (!deleted) return c.json({ error: "Conversation not found" }, 404);
1765
2219
  return c.json({ ok: true });
1766
2220
  });
1767
- var conversations_default = app5;
1768
-
1769
- // src/web/routes/files.ts
1770
- import { existsSync as existsSync5 } from "fs";
1771
- import { readdir, readFile, writeFile } from "fs/promises";
1772
- import { resolve as resolve7 } from "path";
1773
- import { zValidator as zValidator4 } from "@hono/zod-validator";
1774
- import { Hono as Hono6 } from "hono";
1775
- import { z as z4 } from "zod";
1776
- var ALLOWED_FILES = /* @__PURE__ */ new Set(["SOUL.md", "MEMORY.md", "CLAUDE.md", "VOLUTE.md"]);
1777
- var saveFileSchema = z4.object({ content: z4.string() });
1778
- var app6 = new Hono6().get("/:name/files", async (c) => {
1779
- const name = c.req.param("name");
1780
- const entry = findAgent(name);
1781
- if (!entry) return c.json({ error: "Agent not found" }, 404);
1782
- const dir = agentDir(name);
1783
- const homeDir = resolve7(dir, "home");
1784
- if (!existsSync5(homeDir)) return c.json({ error: "Home directory missing" }, 404);
1785
- const allFiles = await readdir(homeDir);
1786
- const files = allFiles.filter((f) => f.endsWith(".md") && ALLOWED_FILES.has(f));
1787
- return c.json(files);
1788
- }).get("/:name/files/:filename", async (c) => {
1789
- const name = c.req.param("name");
1790
- const filename = c.req.param("filename");
1791
- if (!ALLOWED_FILES.has(filename)) {
1792
- return c.json({ error: "File not allowed" }, 403);
1793
- }
1794
- const entry = findAgent(name);
1795
- if (!entry) return c.json({ error: "Agent not found" }, 404);
1796
- const dir = agentDir(name);
1797
- const filePath = resolve7(dir, "home", filename);
1798
- if (!existsSync5(filePath)) {
1799
- return c.json({ error: "File not found" }, 404);
1800
- }
1801
- const content = await readFile(filePath, "utf-8");
1802
- return c.json({ filename, content });
1803
- }).put("/:name/files/:filename", zValidator4("json", saveFileSchema), async (c) => {
1804
- const name = c.req.param("name");
1805
- const filename = c.req.param("filename");
1806
- if (!ALLOWED_FILES.has(filename)) {
1807
- return c.json({ error: "File not allowed" }, 403);
1808
- }
1809
- const entry = findAgent(name);
1810
- if (!entry) return c.json({ error: "Agent not found" }, 404);
1811
- const dir = agentDir(name);
1812
- const filePath = resolve7(dir, "home", filename);
1813
- const { content } = c.req.valid("json");
1814
- await writeFile(filePath, content);
1815
- return c.json({ ok: true });
1816
- });
1817
- var files_default = app6;
1818
-
1819
- // src/web/routes/logs.ts
1820
- import { spawn as spawn2 } from "child_process";
1821
- import { existsSync as existsSync6 } from "fs";
1822
- import { resolve as resolve8 } from "path";
1823
- import { Hono as Hono7 } from "hono";
1824
- import { streamSSE as streamSSE2 } from "hono/streaming";
1825
- var app7 = new Hono7().get("/:name/logs", async (c) => {
1826
- const name = c.req.param("name");
1827
- const entry = findAgent(name);
1828
- if (!entry) return c.json({ error: "Agent not found" }, 404);
1829
- const dir = agentDir(name);
1830
- const logFile = resolve8(dir, ".volute", "logs", "agent.log");
1831
- if (!existsSync6(logFile)) {
1832
- return c.json({ error: "No log file found" }, 404);
1833
- }
1834
- return streamSSE2(c, async (stream2) => {
1835
- const tail = spawn2("tail", ["-n", "200", "-f", logFile]);
1836
- const onData = (data) => {
1837
- const lines = data.toString().split("\n");
1838
- for (const line of lines) {
1839
- if (line) {
1840
- stream2.writeSSE({ data: line }).catch(() => {
1841
- });
1842
- }
1843
- }
1844
- };
1845
- tail.stdout.on("data", onData);
1846
- stream2.onAbort(() => {
1847
- tail.kill();
1848
- });
1849
- await new Promise((resolve11) => {
1850
- tail.on("exit", resolve11);
1851
- stream2.onAbort(resolve11);
1852
- });
1853
- });
1854
- });
1855
- var logs_default = app7;
1856
-
1857
- // src/web/routes/schedules.ts
1858
- import { Hono as Hono8 } from "hono";
1859
- function readSchedules(name) {
1860
- return readVoluteConfig(agentDir(name))?.schedules ?? [];
1861
- }
1862
- function writeSchedules(name, schedules) {
1863
- const dir = agentDir(name);
1864
- const config = readVoluteConfig(dir) ?? {};
1865
- config.schedules = schedules.length > 0 ? schedules : void 0;
1866
- writeVoluteConfig(dir, config);
1867
- getScheduler().loadSchedules(name);
1868
- }
1869
- var app8 = new Hono8().get("/:name/schedules", (c) => {
1870
- const name = c.req.param("name");
1871
- if (!findAgent(name)) return c.json({ error: "Agent not found" }, 404);
1872
- return c.json(readSchedules(name));
1873
- }).post("/:name/schedules", requireAdmin, async (c) => {
1874
- const name = c.req.param("name");
1875
- if (!findAgent(name)) return c.json({ error: "Agent not found" }, 404);
1876
- const body = await c.req.json();
1877
- if (!body.cron || !body.message) {
1878
- return c.json({ error: "cron and message are required" }, 400);
1879
- }
1880
- const schedules = readSchedules(name);
1881
- const id = body.id || `schedule-${Date.now()}`;
1882
- if (schedules.some((s) => s.id === id)) {
1883
- return c.json({ error: `Schedule "${id}" already exists` }, 409);
1884
- }
1885
- schedules.push({ id, cron: body.cron, message: body.message, enabled: body.enabled ?? true });
1886
- writeSchedules(name, schedules);
1887
- return c.json({ ok: true, id }, 201);
1888
- }).put("/:name/schedules/:id", requireAdmin, async (c) => {
1889
- const name = c.req.param("name");
1890
- const id = c.req.param("id");
1891
- if (!findAgent(name)) return c.json({ error: "Agent not found" }, 404);
1892
- const schedules = readSchedules(name);
1893
- const idx = schedules.findIndex((s) => s.id === id);
1894
- if (idx === -1) return c.json({ error: "Schedule not found" }, 404);
1895
- const body = await c.req.json();
1896
- if (body.cron !== void 0) schedules[idx].cron = body.cron;
1897
- if (body.message !== void 0) schedules[idx].message = body.message;
1898
- if (body.enabled !== void 0) schedules[idx].enabled = body.enabled;
1899
- writeSchedules(name, schedules);
1900
- return c.json({ ok: true });
1901
- }).delete("/:name/schedules/:id", requireAdmin, (c) => {
1902
- const name = c.req.param("name");
1903
- const id = c.req.param("id");
1904
- if (!findAgent(name)) return c.json({ error: "Agent not found" }, 404);
1905
- const schedules = readSchedules(name);
1906
- const filtered = schedules.filter((s) => s.id !== id);
1907
- if (filtered.length === schedules.length) {
1908
- return c.json({ error: "Schedule not found" }, 404);
1909
- }
1910
- writeSchedules(name, filtered);
1911
- return c.json({ ok: true });
1912
- }).post("/:name/webhook/:event", async (c) => {
1913
- const name = c.req.param("name");
1914
- const event = c.req.param("event");
1915
- const entry = findAgent(name);
1916
- if (!entry) return c.json({ error: "Agent not found" }, 404);
1917
- const body = await c.req.text();
1918
- const message = `[webhook: ${event}] ${body}`;
1919
- try {
1920
- const res = await fetch(`http://127.0.0.1:${entry.port}/message`, {
1921
- method: "POST",
1922
- headers: { "Content-Type": "application/json" },
1923
- body: JSON.stringify({
1924
- content: [{ type: "text", text: message }],
1925
- channel: "system:webhook",
1926
- sender: "webhook"
1927
- })
1928
- });
1929
- if (!res.ok) {
1930
- return c.json({ error: `Agent responded with ${res.status}` }, 502);
1931
- }
1932
- return c.json({ ok: true });
1933
- } catch {
1934
- return c.json({ error: "Failed to reach agent" }, 502);
1935
- }
1936
- });
1937
- var schedules_default = app8;
1938
-
1939
- // src/web/routes/system.ts
1940
- import { Hono as Hono9 } from "hono";
1941
- import { streamSSE as streamSSE3 } from "hono/streaming";
1942
- var app9 = new Hono9().get("/logs", async (c) => {
1943
- const user = c.get("user");
1944
- if (user.role !== "admin") return c.json({ error: "Forbidden" }, 403);
1945
- return streamSSE3(c, async (stream2) => {
1946
- for (const entry of logBuffer.getEntries()) {
1947
- await stream2.writeSSE({ data: JSON.stringify(entry) });
1948
- }
1949
- const unsubscribe = logBuffer.subscribe((entry) => {
1950
- stream2.writeSSE({ data: JSON.stringify(entry) }).catch(() => {
1951
- });
1952
- });
1953
- await new Promise((resolve11) => {
1954
- stream2.onAbort(() => {
1955
- unsubscribe();
1956
- resolve11();
1957
- });
1958
- });
1959
- });
1960
- });
1961
- var system_default = app9;
1962
-
1963
- // src/web/routes/update.ts
1964
- import { spawn as spawn3 } from "child_process";
1965
- import { Hono as Hono10 } from "hono";
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;
2221
+ var conversations_default = app12;
1983
2222
 
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)
2223
+ // src/web/routes/volute/user-conversations.ts
2224
+ import { zValidator as zValidator6 } from "@hono/zod-validator";
2225
+ import { Hono as Hono13 } from "hono";
2226
+ import { z as z6 } from "zod";
2227
+ var createSchema = z6.object({
2228
+ title: z6.string().optional(),
2229
+ participantNames: z6.array(z6.string()).min(1)
1991
2230
  });
1992
- var app11 = new Hono11().use("*", authMiddleware).get("/", async (c) => {
2231
+ var app13 = new Hono13().use("*", authMiddleware).get("/", async (c) => {
1993
2232
  const user = c.get("user");
1994
2233
  const convs = await listConversationsWithParticipants(user.id);
1995
2234
  return c.json(convs);
@@ -2001,7 +2240,7 @@ var app11 = new Hono11().use("*", authMiddleware).get("/", async (c) => {
2001
2240
  }
2002
2241
  const msgs = await getMessages(id);
2003
2242
  return c.json(msgs);
2004
- }).post("/", zValidator5("json", createSchema), async (c) => {
2243
+ }).post("/", zValidator6("json", createSchema), async (c) => {
2005
2244
  const user = c.get("user");
2006
2245
  const body = c.req.valid("json");
2007
2246
  const participantIds = /* @__PURE__ */ new Set();
@@ -2038,29 +2277,11 @@ var app11 = new Hono11().use("*", authMiddleware).get("/", async (c) => {
2038
2277
  if (!deleted) return c.json({ error: "Conversation not found" }, 404);
2039
2278
  return c.json({ ok: true });
2040
2279
  });
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) => {
2046
- const name = c.req.param("name");
2047
- const entry = findAgent(name);
2048
- if (!entry) return c.json({ error: "Agent not found" }, 404);
2049
- const variants = readVariants(name);
2050
- const results = await Promise.all(
2051
- variants.map(async (v) => {
2052
- if (!v.port) return { ...v, status: "no-server" };
2053
- const health = await checkHealth(v.port);
2054
- return { ...v, status: health.ok ? "running" : "dead" };
2055
- })
2056
- );
2057
- return c.json(results);
2058
- });
2059
- var variants_default = app12;
2280
+ var user_conversations_default = app13;
2060
2281
 
2061
2282
  // src/web/app.ts
2062
- var app13 = new Hono13();
2063
- app13.onError((err, c) => {
2283
+ var app14 = new Hono14();
2284
+ app14.onError((err, c) => {
2064
2285
  if (err instanceof HTTPException) {
2065
2286
  return err.getResponse();
2066
2287
  }
@@ -2071,10 +2292,10 @@ app13.onError((err, c) => {
2071
2292
  });
2072
2293
  return c.json({ error: "Internal server error" }, 500);
2073
2294
  });
2074
- app13.notFound((c) => {
2295
+ app14.notFound((c) => {
2075
2296
  return c.json({ error: "Not found" }, 404);
2076
2297
  });
2077
- app13.use("*", async (c, next) => {
2298
+ app14.use("*", async (c, next) => {
2078
2299
  const start = Date.now();
2079
2300
  await next();
2080
2301
  const duration = Date.now() - start;
@@ -2085,7 +2306,7 @@ app13.use("*", async (c, next) => {
2085
2306
  duration
2086
2307
  });
2087
2308
  });
2088
- app13.get("/api/health", (c) => {
2309
+ app14.get("/api/health", (c) => {
2089
2310
  let version = "unknown";
2090
2311
  let cached = null;
2091
2312
  try {
@@ -2100,13 +2321,13 @@ app13.get("/api/health", (c) => {
2100
2321
  ...cached?.updateAvailable ? { updateAvailable: true, latest: cached.latest } : {}
2101
2322
  });
2102
2323
  });
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;
2324
+ app14.use("/api/*", bodyLimit({ maxSize: 10 * 1024 * 1024 }));
2325
+ app14.use("/api/*", csrf());
2326
+ app14.use("/api/agents/*", authMiddleware);
2327
+ app14.use("/api/conversations/*", authMiddleware);
2328
+ app14.use("/api/system/*", authMiddleware);
2329
+ 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);
2330
+ var app_default = app14;
2110
2331
 
2111
2332
  // src/web/server.ts
2112
2333
  var MIME_TYPES = {
@@ -2123,20 +2344,20 @@ async function startServer({
2123
2344
  hostname = "127.0.0.1"
2124
2345
  }) {
2125
2346
  let assetsDir = "";
2126
- let searchDir = dirname3(new URL(import.meta.url).pathname);
2347
+ let searchDir = dirname2(new URL(import.meta.url).pathname);
2127
2348
  for (let i = 0; i < 5; i++) {
2128
- const candidate = resolve9(searchDir, "dist", "web-assets");
2129
- if (existsSync7(candidate)) {
2349
+ const candidate = resolve8(searchDir, "dist", "web-assets");
2350
+ if (existsSync6(candidate)) {
2130
2351
  assetsDir = candidate;
2131
2352
  break;
2132
2353
  }
2133
- searchDir = dirname3(searchDir);
2354
+ searchDir = dirname2(searchDir);
2134
2355
  }
2135
2356
  if (assetsDir) {
2136
2357
  app_default.get("*", async (c) => {
2137
2358
  const urlPath = new URL(c.req.url).pathname;
2138
2359
  if (urlPath.startsWith("/api/")) return c.notFound();
2139
- const filePath = resolve9(assetsDir, urlPath.slice(1));
2360
+ const filePath = resolve8(assetsDir, urlPath.slice(1));
2140
2361
  if (!filePath.startsWith(assetsDir)) return c.text("Forbidden", 403);
2141
2362
  const s = await stat(filePath).catch(() => null);
2142
2363
  if (s?.isFile()) {
@@ -2145,7 +2366,7 @@ async function startServer({
2145
2366
  const body = await readFile2(filePath);
2146
2367
  return c.body(body, 200, { "Content-Type": mime });
2147
2368
  }
2148
- const indexPath = resolve9(assetsDir, "index.html");
2369
+ const indexPath = resolve8(assetsDir, "index.html");
2149
2370
  const indexStat = await stat(indexPath).catch(() => null);
2150
2371
  if (indexStat?.isFile()) {
2151
2372
  const body = await readFile2(indexPath, "utf-8");
@@ -2155,10 +2376,10 @@ async function startServer({
2155
2376
  });
2156
2377
  }
2157
2378
  const server = serve({ fetch: app_default.fetch, port, hostname });
2158
- await new Promise((resolve11, reject) => {
2379
+ await new Promise((resolve10, reject) => {
2159
2380
  server.on("listening", () => {
2160
2381
  logger_default.info("Volute UI running", { hostname, port });
2161
- resolve11();
2382
+ resolve10();
2162
2383
  });
2163
2384
  server.on("error", (err) => {
2164
2385
  reject(err);
@@ -2169,14 +2390,14 @@ async function startServer({
2169
2390
 
2170
2391
  // src/daemon.ts
2171
2392
  if (!process.env.VOLUTE_HOME) {
2172
- process.env.VOLUTE_HOME = resolve10(homedir(), ".volute");
2393
+ process.env.VOLUTE_HOME = resolve9(homedir(), ".volute");
2173
2394
  }
2174
2395
  async function startDaemon(opts) {
2175
2396
  const { port, hostname } = opts;
2176
2397
  const myPid = String(process.pid);
2177
2398
  const home = voluteHome();
2178
2399
  if (!opts.foreground) {
2179
- const log2 = new RotatingLog(resolve10(home, "daemon.log"));
2400
+ const log2 = new RotatingLog(resolve9(home, "daemon.log"));
2180
2401
  const write2 = (...args) => log2.write(`${format(...args)}
2181
2402
  `);
2182
2403
  console.log = write2;
@@ -2184,11 +2405,12 @@ async function startDaemon(opts) {
2184
2405
  console.warn = write2;
2185
2406
  console.info = write2;
2186
2407
  }
2187
- const DAEMON_PID_PATH = resolve10(home, "daemon.pid");
2188
- const DAEMON_JSON_PATH = resolve10(home, "daemon.json");
2408
+ const DAEMON_PID_PATH = resolve9(home, "daemon.pid");
2409
+ const DAEMON_JSON_PATH = resolve9(home, "daemon.json");
2189
2410
  mkdirSync2(home, { recursive: true });
2190
2411
  const token = process.env.VOLUTE_DAEMON_TOKEN || randomBytes(32).toString("hex");
2191
2412
  process.env.VOLUTE_DAEMON_TOKEN = token;
2413
+ process.env.VOLUTE_DAEMON_HOSTNAME = hostname;
2192
2414
  let server;
2193
2415
  try {
2194
2416
  server = await startServer({ port, hostname });
@@ -2210,6 +2432,8 @@ async function startDaemon(opts) {
2210
2432
  const connectors = initConnectorManager();
2211
2433
  const scheduler = getScheduler();
2212
2434
  scheduler.start(port, token);
2435
+ const tokenBudget = getTokenBudget();
2436
+ tokenBudget.start(port, token);
2213
2437
  const registry = readRegistry();
2214
2438
  for (const entry of registry) {
2215
2439
  if (!entry.running) continue;
@@ -2218,6 +2442,14 @@ async function startDaemon(opts) {
2218
2442
  const dir = agentDir(entry.name);
2219
2443
  await connectors.startConnectors(entry.name, dir, entry.port, port);
2220
2444
  scheduler.loadSchedules(entry.name);
2445
+ const config = readVoluteConfig(dir);
2446
+ if (config?.tokenBudget) {
2447
+ tokenBudget.setBudget(
2448
+ entry.name,
2449
+ config.tokenBudget,
2450
+ config.tokenBudgetPeriodMinutes ?? DEFAULT_BUDGET_PERIOD_MINUTES
2451
+ );
2452
+ }
2221
2453
  } catch (err) {
2222
2454
  console.error(`[daemon] failed to start agent ${entry.name}:`, err);
2223
2455
  setAgentRunning(entry.name, false);
@@ -2258,6 +2490,7 @@ async function startDaemon(opts) {
2258
2490
  console.error("[daemon] shutting down...");
2259
2491
  scheduler.stop();
2260
2492
  scheduler.saveState();
2493
+ tokenBudget.stop();
2261
2494
  await connectors.stopAll();
2262
2495
  await manager.stopAll();
2263
2496
  manager.clearCrashAttempts();