volute 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/{agent-Z2B6EFEQ.js → agent-X7GJLBLW.js} +13 -9
  2. package/dist/{agent-manager-PXBKA2GK.js → agent-manager-JDVXU3ON.js} +4 -4
  3. package/dist/channel-SMCNOIVQ.js +262 -0
  4. package/dist/{chunk-5X7HGB6L.js → chunk-AOKAQGO4.js} +1 -1
  5. package/dist/{chunk-MXUCNIBG.js → chunk-BX7KI4S3.js} +68 -3
  6. package/dist/{chunk-MW2KFO3B.js → chunk-G6ZNGLUX.js} +3 -3
  7. package/dist/{chunk-HE67X4T6.js → chunk-H7AMDUIA.js} +1 -1
  8. package/dist/{chunk-7L4AN5D4.js → chunk-JR4UXCTO.js} +1 -1
  9. package/dist/{chunk-UX25Z2ND.js → chunk-UWHWAPGO.js} +7 -0
  10. package/dist/{chunk-UAVD2AHX.js → chunk-W76KWE23.js} +1 -1
  11. package/dist/chunk-ZZOOTYXK.js +583 -0
  12. package/dist/cli.js +18 -21
  13. package/dist/{connector-LYEMXQEV.js → connector-Y7JPNROO.js} +3 -3
  14. package/dist/connectors/discord.js +31 -4
  15. package/dist/connectors/slack.js +22 -3
  16. package/dist/connectors/telegram.js +34 -4
  17. package/dist/{create-RVCZN6HE.js → create-G525LWEA.js} +2 -2
  18. package/dist/{daemon-client-ZY6UUN2M.js → daemon-client-442IV43D.js} +2 -2
  19. package/dist/daemon.js +962 -525
  20. package/dist/{delete-3QH7VYIN.js → delete-2PH2CGDY.js} +3 -3
  21. package/dist/{down-O7IFZLVJ.js → down-FXWAN66A.js} +1 -1
  22. package/dist/{env-4D4REPJF.js → env-7GLUJCWS.js} +2 -2
  23. package/dist/{history-OEONB53Z.js → history-H72ZUIBN.js} +2 -2
  24. package/dist/{import-MXJB2EII.js → import-AVKQJDYC.js} +2 -2
  25. package/dist/{logs-DF342W4M.js → logs-EDGK26AK.js} +1 -1
  26. package/dist/{message-ADHWFHSI.js → message-SCOQDR3P.js} +2 -2
  27. package/dist/{package-VQOE7JNH.js → package-4DP4Y4UO.js} +1 -1
  28. package/dist/restart-O4ETYLJF.js +29 -0
  29. package/dist/{schedule-NAG6F463.js → schedule-S6QVC5ON.js} +2 -2
  30. package/dist/send-G7PE4DOJ.js +72 -0
  31. package/dist/{setup-RPRRGG2F.js → setup-F4TCWVSP.js} +2 -2
  32. package/dist/{start-TUOXDSFL.js → start-VHQ7LNWM.js} +2 -2
  33. package/dist/{status-A36EHRO4.js → status-QAJWXKMZ.js} +2 -2
  34. package/dist/{stop-AOJZLQ5X.js → stop-CAGCT5NI.js} +2 -2
  35. package/dist/{up-7ILD7GU7.js → up-CSX3ZUIU.js} +15 -3
  36. package/dist/{update-LPSIAWQ2.js → update-XSIX3GGP.js} +2 -2
  37. package/dist/{update-check-Y33QDCFL.js → update-check-5ZADDHCK.js} +2 -2
  38. package/dist/{upgrade-FX2TKJ2S.js → upgrade-YXKPWDRU.js} +2 -2
  39. package/dist/{variant-LAB67OC2.js → variant-4Z6W3PP6.js} +2 -2
  40. package/dist/web-assets/assets/index-D5PzIndO.js +308 -0
  41. package/dist/web-assets/index.html +1 -1
  42. package/package.json +1 -1
  43. package/templates/_base/.init/.config/scripts/session-reader.ts +59 -0
  44. package/templates/_base/_skills/sessions/SKILL.md +49 -0
  45. package/templates/_base/_skills/volute-agent/SKILL.md +13 -9
  46. package/templates/_base/src/lib/format-prefix.ts +6 -0
  47. package/templates/_base/src/lib/router.ts +30 -3
  48. package/templates/_base/src/lib/session-monitor.ts +400 -0
  49. package/templates/_base/src/lib/types.ts +2 -0
  50. package/templates/agent-sdk/src/agent.ts +16 -0
  51. package/templates/agent-sdk/src/lib/hooks/session-context.ts +32 -0
  52. package/templates/pi/src/agent.ts +7 -1
  53. package/templates/pi/src/lib/session-context-extension.ts +33 -0
  54. package/dist/channel-MK5OK2SI.js +0 -113
  55. package/dist/chunk-SMISE4SV.js +0 -226
  56. package/dist/conversation-ERXEQZTY.js +0 -163
  57. package/dist/send-66QMKRUH.js +0 -75
  58. package/dist/web-assets/assets/index-BbRmoxoA.js +0 -308
package/dist/daemon.js CHANGED
@@ -6,34 +6,36 @@ import {
6
6
  initAgentManager,
7
7
  loadJsonMap,
8
8
  saveJsonMap
9
- } from "./chunk-MW2KFO3B.js";
9
+ } from "./chunk-G6ZNGLUX.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-AOKAQGO4.js";
18
15
  import {
19
16
  collectPart
20
17
  } from "./chunk-B3R6L2GW.js";
18
+ import {
19
+ CHANNELS
20
+ } from "./chunk-ZZOOTYXK.js";
21
21
  import {
22
22
  readVoluteConfig,
23
23
  writeVoluteConfig
24
24
  } from "./chunk-NETNFBA5.js";
25
25
  import {
26
26
  loadMergedEnv
27
- } from "./chunk-HE67X4T6.js";
27
+ } from "./chunk-H7AMDUIA.js";
28
+ import "./chunk-BX7KI4S3.js";
28
29
  import {
29
30
  applyIsolation
30
- } from "./chunk-UAVD2AHX.js";
31
+ } from "./chunk-W76KWE23.js";
31
32
  import {
32
33
  resolveVoluteBin
33
34
  } from "./chunk-5SKQ6J7T.js";
34
35
  import {
35
36
  agentDir,
36
37
  checkHealth,
38
+ daemonLoopback,
37
39
  findAgent,
38
40
  findVariant,
39
41
  getAllRunningVariants,
@@ -44,7 +46,7 @@ import {
44
46
  setAgentRunning,
45
47
  setVariantRunning,
46
48
  voluteHome
47
- } from "./chunk-UX25Z2ND.js";
49
+ } from "./chunk-UWHWAPGO.js";
48
50
  import {
49
51
  __export
50
52
  } from "./chunk-K3NQKI34.js";
@@ -232,7 +234,7 @@ var ConnectorManager = class {
232
234
  VOLUTE_AGENT_NAME: agentName,
233
235
  VOLUTE_AGENT_DIR: agentDir2,
234
236
  ...daemonPort ? {
235
- VOLUTE_DAEMON_URL: `http://127.0.0.1:${daemonPort}`,
237
+ VOLUTE_DAEMON_URL: `http://${daemonLoopback()}:${daemonPort}`,
236
238
  VOLUTE_DAEMON_TOKEN: process.env.VOLUTE_DAEMON_TOKEN
237
239
  } : {},
238
240
  ...connectorEnv
@@ -478,7 +480,7 @@ var Scheduler = class {
478
480
  try {
479
481
  let res;
480
482
  if (this.daemonPort && this.daemonToken) {
481
- const daemonUrl = `http://127.0.0.1:${this.daemonPort}`;
483
+ const daemonUrl = `http://${daemonLoopback()}:${this.daemonPort}`;
482
484
  res = await fetch(`${daemonUrl}/api/agents/${encodeURIComponent(agentName)}/message`, {
483
485
  method: "POST",
484
486
  headers: {
@@ -527,6 +529,180 @@ function getScheduler() {
527
529
  return instance2;
528
530
  }
529
531
 
532
+ // src/lib/token-budget.ts
533
+ var DEFAULT_BUDGET_PERIOD_MINUTES = 60;
534
+ var MAX_QUEUE_SIZE = 100;
535
+ var TokenBudget = class {
536
+ budgets = /* @__PURE__ */ new Map();
537
+ interval = null;
538
+ daemonPort = null;
539
+ daemonToken = null;
540
+ start(daemonPort, daemonToken) {
541
+ this.daemonPort = daemonPort ?? null;
542
+ this.daemonToken = daemonToken ?? null;
543
+ this.interval = setInterval(() => this.tick(), 6e4);
544
+ }
545
+ stop() {
546
+ if (this.interval) clearInterval(this.interval);
547
+ this.interval = null;
548
+ }
549
+ setBudget(agent, tokenLimit, periodMinutes) {
550
+ if (tokenLimit <= 0) return;
551
+ const existing = this.budgets.get(agent);
552
+ if (existing) {
553
+ existing.tokenLimit = tokenLimit;
554
+ existing.periodMinutes = periodMinutes;
555
+ } else {
556
+ this.budgets.set(agent, {
557
+ tokensUsed: 0,
558
+ periodStart: Date.now(),
559
+ periodMinutes,
560
+ tokenLimit,
561
+ queue: [],
562
+ warningInjected: false
563
+ });
564
+ }
565
+ }
566
+ removeBudget(agent) {
567
+ this.budgets.delete(agent);
568
+ }
569
+ recordUsage(agent, inputTokens, outputTokens) {
570
+ const state = this.budgets.get(agent);
571
+ if (!state) return;
572
+ state.tokensUsed += inputTokens + outputTokens;
573
+ }
574
+ /** Returns current budget status. Does not mutate state — call acknowledgeWarning() after delivering a warning. */
575
+ checkBudget(agent) {
576
+ const state = this.budgets.get(agent);
577
+ if (!state) return "ok";
578
+ const pct = state.tokensUsed / state.tokenLimit;
579
+ if (pct >= 1) return "exceeded";
580
+ if (pct >= 0.8 && !state.warningInjected) return "warning";
581
+ return "ok";
582
+ }
583
+ /** Mark warning as delivered for this period. Call after successfully injecting the warning. */
584
+ acknowledgeWarning(agent) {
585
+ const state = this.budgets.get(agent);
586
+ if (state) state.warningInjected = true;
587
+ }
588
+ enqueue(agent, message) {
589
+ const state = this.budgets.get(agent);
590
+ if (!state) return;
591
+ if (state.queue.length >= MAX_QUEUE_SIZE) {
592
+ state.queue.shift();
593
+ }
594
+ state.queue.push(message);
595
+ }
596
+ drain(agent) {
597
+ const state = this.budgets.get(agent);
598
+ if (!state) return [];
599
+ const messages2 = state.queue;
600
+ state.queue = [];
601
+ return messages2;
602
+ }
603
+ getUsage(agent) {
604
+ const state = this.budgets.get(agent);
605
+ if (!state) return null;
606
+ return {
607
+ tokensUsed: state.tokensUsed,
608
+ tokenLimit: state.tokenLimit,
609
+ periodMinutes: state.periodMinutes,
610
+ periodStart: state.periodStart,
611
+ queueLength: state.queue.length,
612
+ percentUsed: Math.round(state.tokensUsed / state.tokenLimit * 100)
613
+ };
614
+ }
615
+ tick() {
616
+ const now = Date.now();
617
+ for (const [agent, state] of this.budgets) {
618
+ const elapsed = now - state.periodStart;
619
+ if (elapsed >= state.periodMinutes * 6e4) {
620
+ state.tokensUsed = 0;
621
+ state.periodStart = now;
622
+ state.warningInjected = false;
623
+ const queued = this.drain(agent);
624
+ if (queued.length > 0) {
625
+ this.replay(agent, queued).catch((err) => {
626
+ console.error(`[token-budget] replay error for ${agent}:`, err);
627
+ });
628
+ }
629
+ }
630
+ }
631
+ }
632
+ async replay(agentName, messages2) {
633
+ if (!this.daemonPort || !this.daemonToken) {
634
+ console.error(
635
+ `[token-budget] cannot replay ${messages2.length} message(s) for ${agentName}: daemon not configured`
636
+ );
637
+ const state = this.budgets.get(agentName);
638
+ if (state) state.queue.push(...messages2);
639
+ return;
640
+ }
641
+ const summary = messages2.map((m) => {
642
+ const from = m.sender ? `[${m.sender}]` : "";
643
+ const ch = m.channel ? `(${m.channel})` : "";
644
+ return `${from}${ch} ${m.textContent}`;
645
+ }).join("\n");
646
+ const body = JSON.stringify({
647
+ content: [
648
+ {
649
+ type: "text",
650
+ text: `[Budget replay] ${messages2.length} queued message(s) from the previous budget period:
651
+
652
+ ${summary}`
653
+ }
654
+ ],
655
+ channel: "system:budget-replay",
656
+ sender: "system"
657
+ });
658
+ const daemonUrl = `http://${daemonLoopback()}:${this.daemonPort}`;
659
+ const controller = new AbortController();
660
+ const timeout = setTimeout(() => controller.abort(), 12e4);
661
+ try {
662
+ const res = await fetch(`${daemonUrl}/api/agents/${encodeURIComponent(agentName)}/message`, {
663
+ method: "POST",
664
+ headers: {
665
+ "Content-Type": "application/json",
666
+ Authorization: `Bearer ${this.daemonToken}`,
667
+ Origin: daemonUrl
668
+ },
669
+ body,
670
+ signal: controller.signal
671
+ });
672
+ if (!res.ok) {
673
+ console.error(`[token-budget] replay for ${agentName} got HTTP ${res.status}`);
674
+ } else {
675
+ console.error(
676
+ `[token-budget] replayed ${messages2.length} queued message(s) for ${agentName}`
677
+ );
678
+ }
679
+ try {
680
+ const reader = res.body?.getReader();
681
+ if (reader) {
682
+ try {
683
+ while (!(await reader.read()).done) {
684
+ }
685
+ } finally {
686
+ reader.releaseLock();
687
+ }
688
+ }
689
+ } catch {
690
+ }
691
+ } catch (err) {
692
+ console.error(`[token-budget] failed to replay for ${agentName}:`, err);
693
+ const state = this.budgets.get(agentName);
694
+ if (state) state.queue.push(...messages2);
695
+ } finally {
696
+ clearTimeout(timeout);
697
+ }
698
+ }
699
+ };
700
+ var instance3 = null;
701
+ function getTokenBudget() {
702
+ if (!instance3) instance3 = new TokenBudget();
703
+ return instance3;
704
+ }
705
+
530
706
  // src/web/middleware/auth.ts
531
707
  import { timingSafeEqual } from "crypto";
532
708
  import { eq as eq2, lt } from "drizzle-orm";
@@ -878,7 +1054,7 @@ var log = {
878
1054
  var logger_default = log;
879
1055
 
880
1056
  // src/web/app.ts
881
- import { Hono as Hono13 } from "hono";
1057
+ import { Hono as Hono14 } from "hono";
882
1058
  import { bodyLimit } from "hono/body-limit";
883
1059
  import { csrf } from "hono/csrf";
884
1060
  import { HTTPException } from "hono/http-exception";
@@ -929,6 +1105,73 @@ async function* readNdjson(body) {
929
1105
  }
930
1106
  }
931
1107
 
1108
+ // src/lib/typing.ts
1109
+ var DEFAULT_TTL_MS = 1e4;
1110
+ var SWEEP_INTERVAL_MS = 5e3;
1111
+ var TypingMap = class {
1112
+ channels = /* @__PURE__ */ new Map();
1113
+ sweepTimer;
1114
+ constructor() {
1115
+ this.sweepTimer = setInterval(() => this.sweep(), SWEEP_INTERVAL_MS);
1116
+ this.sweepTimer.unref();
1117
+ }
1118
+ set(channel, sender, opts) {
1119
+ const expiresAt = opts?.persistent ? Infinity : Date.now() + (opts?.ttlMs ?? DEFAULT_TTL_MS);
1120
+ let senders = this.channels.get(channel);
1121
+ if (!senders) {
1122
+ senders = /* @__PURE__ */ new Map();
1123
+ this.channels.set(channel, senders);
1124
+ }
1125
+ senders.set(sender, { expiresAt });
1126
+ }
1127
+ delete(channel, sender) {
1128
+ const senders = this.channels.get(channel);
1129
+ if (senders) {
1130
+ senders.delete(sender);
1131
+ if (senders.size === 0) {
1132
+ this.channels.delete(channel);
1133
+ }
1134
+ }
1135
+ }
1136
+ get(channel) {
1137
+ const senders = this.channels.get(channel);
1138
+ if (!senders) return [];
1139
+ const now = Date.now();
1140
+ const result = [];
1141
+ for (const [sender, entry] of senders) {
1142
+ if (entry.expiresAt > now) {
1143
+ result.push(sender);
1144
+ }
1145
+ }
1146
+ return result;
1147
+ }
1148
+ dispose() {
1149
+ clearInterval(this.sweepTimer);
1150
+ this.channels.clear();
1151
+ if (instance4 === this) instance4 = void 0;
1152
+ }
1153
+ sweep() {
1154
+ const now = Date.now();
1155
+ for (const [channel, senders] of this.channels) {
1156
+ for (const [sender, entry] of senders) {
1157
+ if (entry.expiresAt <= now) {
1158
+ senders.delete(sender);
1159
+ }
1160
+ }
1161
+ if (senders.size === 0) {
1162
+ this.channels.delete(channel);
1163
+ }
1164
+ }
1165
+ }
1166
+ };
1167
+ var instance4;
1168
+ function getTypingMap() {
1169
+ if (!instance4) {
1170
+ instance4 = new TypingMap();
1171
+ }
1172
+ return instance4;
1173
+ }
1174
+
932
1175
  // src/web/routes/agents.ts
933
1176
  function getDaemonPort() {
934
1177
  try {
@@ -945,21 +1188,25 @@ async function getAgentStatus(name, port) {
945
1188
  const health = await checkHealth(port);
946
1189
  status = health.ok ? "running" : "starting";
947
1190
  }
1191
+ const channelConfig = readVoluteConfig(agentDir(name))?.channels;
948
1192
  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
- });
1193
+ for (const [, provider] of Object.entries(CHANNELS)) {
1194
+ if (!provider.builtIn) continue;
1195
+ channels.push({
1196
+ name: provider.name,
1197
+ displayName: provider.displayName,
1198
+ status: status === "running" ? "connected" : "disconnected",
1199
+ showToolCalls: channelConfig?.[provider.name]?.showToolCalls ?? provider.showToolCalls
1200
+ });
1201
+ }
955
1202
  const connectorStatuses = getConnectorManager().getConnectorStatus(name);
956
1203
  for (const cs of connectorStatuses) {
957
- const config = CHANNELS[cs.type];
1204
+ const provider = CHANNELS[cs.type];
958
1205
  channels.push({
959
- name: config?.name ?? cs.type,
960
- displayName: config?.displayName ?? cs.type,
1206
+ name: provider?.name ?? cs.type,
1207
+ displayName: provider?.displayName ?? cs.type,
961
1208
  status: cs.running ? "connected" : "disconnected",
962
- showToolCalls: config?.showToolCalls ?? false
1209
+ showToolCalls: channelConfig?.[cs.type]?.showToolCalls ?? provider?.showToolCalls ?? false
963
1210
  });
964
1211
  }
965
1212
  return { status, channels };
@@ -1015,6 +1262,14 @@ var app = new Hono().get("/", async (c) => {
1015
1262
  const dir = agentDir(baseName);
1016
1263
  await getConnectorManager().startConnectors(baseName, dir, entry.port, getDaemonPort());
1017
1264
  getScheduler().loadSchedules(baseName);
1265
+ const config = readVoluteConfig(dir);
1266
+ if (config?.tokenBudget) {
1267
+ getTokenBudget().setBudget(
1268
+ baseName,
1269
+ config.tokenBudget,
1270
+ config.tokenBudgetPeriodMinutes ?? DEFAULT_BUDGET_PERIOD_MINUTES
1271
+ );
1272
+ }
1018
1273
  }
1019
1274
  return c.json({ ok: true });
1020
1275
  } catch (err) {
@@ -1036,7 +1291,10 @@ var app = new Hono().get("/", async (c) => {
1036
1291
  const connectorManager = getConnectorManager();
1037
1292
  try {
1038
1293
  if (manager.isRunning(name)) {
1039
- if (!variantName) await connectorManager.stopConnectors(baseName);
1294
+ if (!variantName) {
1295
+ await connectorManager.stopConnectors(baseName);
1296
+ getTokenBudget().removeBudget(baseName);
1297
+ }
1040
1298
  await manager.stopAgent(name);
1041
1299
  }
1042
1300
  await manager.startAgent(name);
@@ -1044,6 +1302,14 @@ var app = new Hono().get("/", async (c) => {
1044
1302
  const dir = agentDir(baseName);
1045
1303
  await connectorManager.startConnectors(baseName, dir, entry.port, getDaemonPort());
1046
1304
  getScheduler().loadSchedules(baseName);
1305
+ const config = readVoluteConfig(dir);
1306
+ if (config?.tokenBudget) {
1307
+ getTokenBudget().setBudget(
1308
+ baseName,
1309
+ config.tokenBudget,
1310
+ config.tokenBudgetPeriodMinutes ?? DEFAULT_BUDGET_PERIOD_MINUTES
1311
+ );
1312
+ }
1047
1313
  }
1048
1314
  return c.json({ ok: true });
1049
1315
  } catch (err) {
@@ -1066,6 +1332,7 @@ var app = new Hono().get("/", async (c) => {
1066
1332
  if (!variantName) {
1067
1333
  await getConnectorManager().stopConnectors(baseName);
1068
1334
  getScheduler().unloadSchedules(baseName);
1335
+ getTokenBudget().removeBudget(baseName);
1069
1336
  }
1070
1337
  await manager.stopAgent(name);
1071
1338
  return c.json({ ok: true });
@@ -1081,6 +1348,7 @@ var app = new Hono().get("/", async (c) => {
1081
1348
  const manager = getAgentManager();
1082
1349
  if (manager.isRunning(name)) {
1083
1350
  await getConnectorManager().stopConnectors(name);
1351
+ getTokenBudget().removeBudget(name);
1084
1352
  await manager.stopAgent(name);
1085
1353
  }
1086
1354
  removeAllVariants(name);
@@ -1134,12 +1402,61 @@ var app = new Hono().get("/", async (c) => {
1134
1402
  console.error(`[daemon] failed to persist inbound message for ${baseName}:`, err);
1135
1403
  }
1136
1404
  }
1405
+ const budget = getTokenBudget();
1406
+ const budgetStatus = budget.checkBudget(baseName);
1407
+ if (budgetStatus === "exceeded") {
1408
+ let textContent = "";
1409
+ if (parsed) {
1410
+ if (typeof parsed.content === "string") {
1411
+ textContent = parsed.content;
1412
+ } else if (Array.isArray(parsed.content)) {
1413
+ textContent = parsed.content.filter((p) => p.type === "text" && p.text).map((p) => p.text).join("\n");
1414
+ }
1415
+ }
1416
+ budget.enqueue(baseName, {
1417
+ channel,
1418
+ sender: parsed?.sender ?? null,
1419
+ textContent
1420
+ });
1421
+ c.header("Content-Type", "application/x-ndjson");
1422
+ const encoder2 = new TextEncoder();
1423
+ return stream(c, async (s) => {
1424
+ await s.write(
1425
+ encoder2.encode(
1426
+ `${JSON.stringify({ type: "text", content: "[Token budget exceeded \u2014 message queued for next period]" })}
1427
+ `
1428
+ )
1429
+ );
1430
+ await s.write(encoder2.encode(`${JSON.stringify({ type: "done" })}
1431
+ `));
1432
+ });
1433
+ }
1434
+ const typingMap = getTypingMap();
1435
+ const currentlyTyping = typingMap.get(channel).filter((s) => s !== baseName);
1436
+ let forwardBody = body;
1437
+ if (parsed && currentlyTyping.length > 0) {
1438
+ parsed.typing = currentlyTyping;
1439
+ forwardBody = JSON.stringify(parsed);
1440
+ }
1441
+ if (budgetStatus === "warning" && parsed) {
1442
+ const usage = budget.getUsage(baseName);
1443
+ const pct = usage?.percentUsed ?? 80;
1444
+ const warningText = `
1445
+ [System: Token budget is at ${pct}% \u2014 conserve tokens to avoid message queuing]`;
1446
+ if (typeof parsed.content === "string") {
1447
+ parsed.content = parsed.content + warningText;
1448
+ } else if (Array.isArray(parsed.content)) {
1449
+ parsed.content = [...parsed.content, { type: "text", text: warningText }];
1450
+ }
1451
+ budget.acknowledgeWarning(baseName);
1452
+ forwardBody = JSON.stringify(parsed);
1453
+ }
1137
1454
  let res;
1138
1455
  try {
1139
1456
  res = await fetch(`http://127.0.0.1:${port}/message`, {
1140
1457
  method: "POST",
1141
1458
  headers: { "Content-Type": "application/json" },
1142
- body
1459
+ body: forwardBody
1143
1460
  });
1144
1461
  } catch (err) {
1145
1462
  console.error(`[daemon] agent ${name} unreachable on port ${port}:`, err);
@@ -1153,33 +1470,76 @@ var app = new Hono().get("/", async (c) => {
1153
1470
  }
1154
1471
  c.header("Content-Type", "application/x-ndjson");
1155
1472
  const encoder = new TextEncoder();
1473
+ typingMap.set(channel, baseName, { persistent: true });
1156
1474
  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)}
1475
+ try {
1476
+ const textParts = [];
1477
+ const toolParts = [];
1478
+ for await (const event of readNdjson(res.body)) {
1479
+ if (event.type === "usage") {
1480
+ const input = typeof event.input_tokens === "number" ? event.input_tokens : 0;
1481
+ const output = typeof event.output_tokens === "number" ? event.output_tokens : 0;
1482
+ budget.recordUsage(baseName, input, output);
1483
+ continue;
1484
+ }
1485
+ await s.write(encoder.encode(`${JSON.stringify(event)}
1161
1486
  `));
1162
- const part = collectPart(event);
1163
- if (part != null) {
1164
- if (event.type === "tool_use") toolParts.push(part);
1165
- else textParts.push(part);
1487
+ const part = collectPart(event);
1488
+ if (part != null) {
1489
+ if (event.type === "tool_use") toolParts.push(part);
1490
+ else textParts.push(part);
1491
+ }
1166
1492
  }
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);
1493
+ const content = [textParts.join(""), ...toolParts].filter(Boolean).join("\n");
1494
+ if (content) {
1495
+ try {
1496
+ await db2.insert(agentMessages).values({
1497
+ agent: baseName,
1498
+ channel,
1499
+ role: "assistant",
1500
+ sender: baseName,
1501
+ content
1502
+ });
1503
+ } catch (err) {
1504
+ console.error(`[daemon] failed to persist assistant response for ${baseName}:`, err);
1505
+ }
1180
1506
  }
1507
+ } finally {
1508
+ typingMap.delete(channel, baseName);
1181
1509
  }
1182
1510
  });
1511
+ }).get("/:name/budget", async (c) => {
1512
+ const name = c.req.param("name");
1513
+ const [baseName] = name.split("@", 2);
1514
+ const usage = getTokenBudget().getUsage(baseName);
1515
+ if (!usage) return c.json({ error: "No budget configured" }, 404);
1516
+ return c.json(usage);
1517
+ }).post("/:name/history", async (c) => {
1518
+ const name = c.req.param("name");
1519
+ const [baseName] = name.split("@", 2);
1520
+ let body;
1521
+ try {
1522
+ body = await c.req.json();
1523
+ } catch {
1524
+ return c.json({ error: "Invalid JSON" }, 400);
1525
+ }
1526
+ if (!body.channel || !body.content) {
1527
+ return c.json({ error: "channel and content required" }, 400);
1528
+ }
1529
+ const db2 = await getDb();
1530
+ try {
1531
+ await db2.insert(agentMessages).values({
1532
+ agent: baseName,
1533
+ channel: body.channel,
1534
+ role: "assistant",
1535
+ sender: baseName,
1536
+ content: body.content
1537
+ });
1538
+ } catch (err) {
1539
+ console.error(`[daemon] failed to persist external send for ${baseName}:`, err);
1540
+ return c.json({ error: "Failed to persist" }, 500);
1541
+ }
1542
+ return c.json({ ok: true });
1183
1543
  }).get("/:name/history/channels", async (c) => {
1184
1544
  const name = c.req.param("name");
1185
1545
  const db2 = await getDb();
@@ -1271,100 +1631,437 @@ var app2 = new Hono2().post("/register", zValidator("json", credentialsSchema),
1271
1631
  }).route("/", admin);
1272
1632
  var auth_default = app2;
1273
1633
 
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";
1634
+ // src/web/routes/connectors.ts
1278
1635
  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
1636
+ var CONNECTOR_TYPE_RE = /^[a-z][a-z0-9-]*$/;
1637
+ var app3 = new Hono3().get("/:name/connectors", (c) => {
1638
+ const name = c.req.param("name");
1639
+ const entry = findAgent(name);
1640
+ if (!entry) return c.json({ error: "Agent not found" }, 404);
1641
+ const dir = agentDir(name);
1642
+ const config = readVoluteConfig(dir) ?? {};
1643
+ const configured = config.connectors ?? [];
1644
+ const manager = getConnectorManager();
1645
+ const runningStatus = manager.getConnectorStatus(name);
1646
+ const connectors = configured.map((type) => {
1647
+ const status = runningStatus.find((s) => s.type === type);
1648
+ return { type, running: status?.running ?? false };
1294
1649
  });
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
- }))
1650
+ return c.json(connectors);
1651
+ }).post("/:name/connectors/:type", requireAdmin, async (c) => {
1652
+ const name = c.req.param("name");
1653
+ const type = c.req.param("type");
1654
+ if (!CONNECTOR_TYPE_RE.test(type)) {
1655
+ return c.json({ error: "Invalid connector type" }, 400);
1656
+ }
1657
+ const entry = findAgent(name);
1658
+ if (!entry) return c.json({ error: "Agent not found" }, 404);
1659
+ const dir = agentDir(name);
1660
+ const manager = getConnectorManager();
1661
+ const envCheck = manager.checkConnectorEnv(type, dir);
1662
+ if (envCheck) {
1663
+ return c.json(
1664
+ {
1665
+ error: "missing_env",
1666
+ missing: envCheck.missing,
1667
+ connectorName: envCheck.connectorName
1668
+ },
1669
+ 400
1302
1670
  );
1303
1671
  }
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) {
1348
- if (!await isParticipantOrOwner(id, userId)) return false;
1349
- await deleteConversation(id);
1350
- return true;
1351
- }
1352
- async function addMessage(conversationId, role, senderName, content) {
1353
- const db2 = await getDb();
1354
- 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));
1357
- if (role === "user") {
1358
- const firstText = content.find((b) => b.type === "text");
1359
- const title = firstText ? firstText.text.slice(0, 80) : "";
1360
- if (title) {
1361
- await db2.update(conversations).set({ title }).where(and3(eq4(conversations.id, conversationId), isNull(conversations.title)));
1362
- }
1672
+ const config = readVoluteConfig(dir) ?? {};
1673
+ const connectors = config.connectors ?? [];
1674
+ if (!connectors.includes(type)) {
1675
+ config.connectors = [...connectors, type];
1676
+ writeVoluteConfig(dir, config);
1363
1677
  }
1364
- return {
1365
- id: result.id,
1366
- conversation_id: conversationId,
1367
- role,
1678
+ try {
1679
+ await manager.startConnector(name, dir, entry.port, type);
1680
+ return c.json({ ok: true });
1681
+ } catch (err) {
1682
+ return c.json(
1683
+ { error: err instanceof Error ? err.message : "Failed to start connector" },
1684
+ 500
1685
+ );
1686
+ }
1687
+ }).delete("/:name/connectors/:type", requireAdmin, async (c) => {
1688
+ const name = c.req.param("name");
1689
+ const type = c.req.param("type");
1690
+ if (!CONNECTOR_TYPE_RE.test(type)) {
1691
+ return c.json({ error: "Invalid connector type" }, 400);
1692
+ }
1693
+ const entry = findAgent(name);
1694
+ if (!entry) return c.json({ error: "Agent not found" }, 404);
1695
+ const dir = agentDir(name);
1696
+ const manager = getConnectorManager();
1697
+ await manager.stopConnector(name, type);
1698
+ const config = readVoluteConfig(dir) ?? {};
1699
+ config.connectors = (config.connectors ?? []).filter((t) => t !== type);
1700
+ writeVoluteConfig(dir, config);
1701
+ return c.json({ ok: true });
1702
+ });
1703
+ var connectors_default = app3;
1704
+
1705
+ // src/web/routes/files.ts
1706
+ import { existsSync as existsSync5 } from "fs";
1707
+ import { readdir, readFile, writeFile } from "fs/promises";
1708
+ import { resolve as resolve6 } from "path";
1709
+ import { zValidator as zValidator2 } from "@hono/zod-validator";
1710
+ import { Hono as Hono4 } from "hono";
1711
+ import { z as z2 } from "zod";
1712
+ var ALLOWED_FILES = /* @__PURE__ */ new Set(["SOUL.md", "MEMORY.md", "CLAUDE.md", "VOLUTE.md"]);
1713
+ var saveFileSchema = z2.object({ content: z2.string() });
1714
+ var app4 = new Hono4().get("/:name/files", async (c) => {
1715
+ const name = c.req.param("name");
1716
+ const entry = findAgent(name);
1717
+ if (!entry) return c.json({ error: "Agent not found" }, 404);
1718
+ const dir = agentDir(name);
1719
+ const homeDir = resolve6(dir, "home");
1720
+ if (!existsSync5(homeDir)) return c.json({ error: "Home directory missing" }, 404);
1721
+ const allFiles = await readdir(homeDir);
1722
+ const files = allFiles.filter((f) => f.endsWith(".md") && ALLOWED_FILES.has(f));
1723
+ return c.json(files);
1724
+ }).get("/:name/files/:filename", async (c) => {
1725
+ const name = c.req.param("name");
1726
+ const filename = c.req.param("filename");
1727
+ if (!ALLOWED_FILES.has(filename)) {
1728
+ return c.json({ error: "File not allowed" }, 403);
1729
+ }
1730
+ const entry = findAgent(name);
1731
+ if (!entry) return c.json({ error: "Agent not found" }, 404);
1732
+ const dir = agentDir(name);
1733
+ const filePath = resolve6(dir, "home", filename);
1734
+ if (!existsSync5(filePath)) {
1735
+ return c.json({ error: "File not found" }, 404);
1736
+ }
1737
+ const content = await readFile(filePath, "utf-8");
1738
+ return c.json({ filename, content });
1739
+ }).put("/:name/files/:filename", zValidator2("json", saveFileSchema), async (c) => {
1740
+ const name = c.req.param("name");
1741
+ const filename = c.req.param("filename");
1742
+ if (!ALLOWED_FILES.has(filename)) {
1743
+ return c.json({ error: "File not allowed" }, 403);
1744
+ }
1745
+ const entry = findAgent(name);
1746
+ if (!entry) return c.json({ error: "Agent not found" }, 404);
1747
+ const dir = agentDir(name);
1748
+ const filePath = resolve6(dir, "home", filename);
1749
+ const { content } = c.req.valid("json");
1750
+ await writeFile(filePath, content);
1751
+ return c.json({ ok: true });
1752
+ });
1753
+ var files_default = app4;
1754
+
1755
+ // src/web/routes/logs.ts
1756
+ import { spawn as spawn2 } from "child_process";
1757
+ import { existsSync as existsSync6 } from "fs";
1758
+ import { resolve as resolve7 } from "path";
1759
+ import { Hono as Hono5 } from "hono";
1760
+ import { streamSSE } from "hono/streaming";
1761
+ var app5 = new Hono5().get("/:name/logs", async (c) => {
1762
+ const name = c.req.param("name");
1763
+ const entry = findAgent(name);
1764
+ if (!entry) return c.json({ error: "Agent not found" }, 404);
1765
+ const dir = agentDir(name);
1766
+ const logFile = resolve7(dir, ".volute", "logs", "agent.log");
1767
+ if (!existsSync6(logFile)) {
1768
+ return c.json({ error: "No log file found" }, 404);
1769
+ }
1770
+ return streamSSE(c, async (stream2) => {
1771
+ const tail = spawn2("tail", ["-n", "200", "-f", logFile]);
1772
+ const onData = (data) => {
1773
+ const lines = data.toString().split("\n");
1774
+ for (const line of lines) {
1775
+ if (line) {
1776
+ stream2.writeSSE({ data: line }).catch(() => {
1777
+ });
1778
+ }
1779
+ }
1780
+ };
1781
+ tail.stdout.on("data", onData);
1782
+ stream2.onAbort(() => {
1783
+ tail.kill();
1784
+ });
1785
+ await new Promise((resolve11) => {
1786
+ tail.on("exit", resolve11);
1787
+ stream2.onAbort(resolve11);
1788
+ });
1789
+ });
1790
+ });
1791
+ var logs_default = app5;
1792
+
1793
+ // src/web/routes/schedules.ts
1794
+ import { Hono as Hono6 } from "hono";
1795
+ function readSchedules(name) {
1796
+ return readVoluteConfig(agentDir(name))?.schedules ?? [];
1797
+ }
1798
+ function writeSchedules(name, schedules) {
1799
+ const dir = agentDir(name);
1800
+ const config = readVoluteConfig(dir) ?? {};
1801
+ config.schedules = schedules.length > 0 ? schedules : void 0;
1802
+ writeVoluteConfig(dir, config);
1803
+ getScheduler().loadSchedules(name);
1804
+ }
1805
+ var app6 = new Hono6().get("/:name/schedules", (c) => {
1806
+ const name = c.req.param("name");
1807
+ if (!findAgent(name)) return c.json({ error: "Agent not found" }, 404);
1808
+ return c.json(readSchedules(name));
1809
+ }).post("/:name/schedules", requireAdmin, async (c) => {
1810
+ const name = c.req.param("name");
1811
+ if (!findAgent(name)) return c.json({ error: "Agent not found" }, 404);
1812
+ const body = await c.req.json();
1813
+ if (!body.cron || !body.message) {
1814
+ return c.json({ error: "cron and message are required" }, 400);
1815
+ }
1816
+ const schedules = readSchedules(name);
1817
+ const id = body.id || `schedule-${Date.now()}`;
1818
+ if (schedules.some((s) => s.id === id)) {
1819
+ return c.json({ error: `Schedule "${id}" already exists` }, 409);
1820
+ }
1821
+ schedules.push({ id, cron: body.cron, message: body.message, enabled: body.enabled ?? true });
1822
+ writeSchedules(name, schedules);
1823
+ return c.json({ ok: true, id }, 201);
1824
+ }).put("/:name/schedules/:id", requireAdmin, async (c) => {
1825
+ const name = c.req.param("name");
1826
+ const id = c.req.param("id");
1827
+ if (!findAgent(name)) return c.json({ error: "Agent not found" }, 404);
1828
+ const schedules = readSchedules(name);
1829
+ const idx = schedules.findIndex((s) => s.id === id);
1830
+ if (idx === -1) return c.json({ error: "Schedule not found" }, 404);
1831
+ const body = await c.req.json();
1832
+ if (body.cron !== void 0) schedules[idx].cron = body.cron;
1833
+ if (body.message !== void 0) schedules[idx].message = body.message;
1834
+ if (body.enabled !== void 0) schedules[idx].enabled = body.enabled;
1835
+ writeSchedules(name, schedules);
1836
+ return c.json({ ok: true });
1837
+ }).delete("/:name/schedules/:id", requireAdmin, (c) => {
1838
+ const name = c.req.param("name");
1839
+ const id = c.req.param("id");
1840
+ if (!findAgent(name)) return c.json({ error: "Agent not found" }, 404);
1841
+ const schedules = readSchedules(name);
1842
+ const filtered = schedules.filter((s) => s.id !== id);
1843
+ if (filtered.length === schedules.length) {
1844
+ return c.json({ error: "Schedule not found" }, 404);
1845
+ }
1846
+ writeSchedules(name, filtered);
1847
+ return c.json({ ok: true });
1848
+ }).post("/:name/webhook/:event", async (c) => {
1849
+ const name = c.req.param("name");
1850
+ const event = c.req.param("event");
1851
+ const entry = findAgent(name);
1852
+ if (!entry) return c.json({ error: "Agent not found" }, 404);
1853
+ const body = await c.req.text();
1854
+ const message = `[webhook: ${event}] ${body}`;
1855
+ try {
1856
+ const res = await fetch(`http://127.0.0.1:${entry.port}/message`, {
1857
+ method: "POST",
1858
+ headers: { "Content-Type": "application/json" },
1859
+ body: JSON.stringify({
1860
+ content: [{ type: "text", text: message }],
1861
+ channel: "system:webhook",
1862
+ sender: "webhook"
1863
+ })
1864
+ });
1865
+ if (!res.ok) {
1866
+ return c.json({ error: `Agent responded with ${res.status}` }, 502);
1867
+ }
1868
+ return c.json({ ok: true });
1869
+ } catch {
1870
+ return c.json({ error: "Failed to reach agent" }, 502);
1871
+ }
1872
+ });
1873
+ var schedules_default = app6;
1874
+
1875
+ // src/web/routes/system.ts
1876
+ import { Hono as Hono7 } from "hono";
1877
+ import { streamSSE as streamSSE2 } from "hono/streaming";
1878
+ var app7 = new Hono7().get("/logs", async (c) => {
1879
+ const user = c.get("user");
1880
+ if (user.role !== "admin") return c.json({ error: "Forbidden" }, 403);
1881
+ return streamSSE2(c, async (stream2) => {
1882
+ for (const entry of logBuffer.getEntries()) {
1883
+ await stream2.writeSSE({ data: JSON.stringify(entry) });
1884
+ }
1885
+ const unsubscribe = logBuffer.subscribe((entry) => {
1886
+ stream2.writeSSE({ data: JSON.stringify(entry) }).catch(() => {
1887
+ });
1888
+ });
1889
+ await new Promise((resolve11) => {
1890
+ stream2.onAbort(() => {
1891
+ unsubscribe();
1892
+ resolve11();
1893
+ });
1894
+ });
1895
+ });
1896
+ });
1897
+ var system_default = app7;
1898
+
1899
+ // src/web/routes/typing.ts
1900
+ import { zValidator as zValidator3 } from "@hono/zod-validator";
1901
+ import { Hono as Hono8 } from "hono";
1902
+ import { z as z3 } from "zod";
1903
+ var typingSchema = z3.object({
1904
+ channel: z3.string().min(1),
1905
+ sender: z3.string().min(1),
1906
+ active: z3.boolean()
1907
+ });
1908
+ var app8 = new Hono8().post("/:name/typing", zValidator3("json", typingSchema), (c) => {
1909
+ const { channel, sender, active } = c.req.valid("json");
1910
+ const map = getTypingMap();
1911
+ if (active) {
1912
+ map.set(channel, sender);
1913
+ } else {
1914
+ map.delete(channel, sender);
1915
+ }
1916
+ return c.json({ ok: true });
1917
+ }).get("/:name/typing", (c) => {
1918
+ const channel = c.req.query("channel");
1919
+ if (!channel) {
1920
+ return c.json({ error: "channel query param is required" }, 400);
1921
+ }
1922
+ const map = getTypingMap();
1923
+ return c.json({ typing: map.get(channel) });
1924
+ });
1925
+ var typing_default = app8;
1926
+
1927
+ // src/web/routes/update.ts
1928
+ import { spawn as spawn3 } from "child_process";
1929
+ import { Hono as Hono9 } from "hono";
1930
+ var bin;
1931
+ var app9 = new Hono9().get("/update", async (c) => {
1932
+ const result = await checkForUpdate();
1933
+ return c.json(result);
1934
+ }).post("/update", requireAdmin, async (c) => {
1935
+ bin ??= resolveVoluteBin();
1936
+ const child = spawn3(bin, ["update"], {
1937
+ stdio: "ignore",
1938
+ detached: true
1939
+ });
1940
+ child.on("error", (err) => {
1941
+ logger_default.error("Update process error", { error: err.message });
1942
+ });
1943
+ child.unref();
1944
+ return c.json({ ok: true, message: "Updating..." });
1945
+ });
1946
+ var update_default = app9;
1947
+
1948
+ // src/web/routes/variants.ts
1949
+ import { Hono as Hono10 } from "hono";
1950
+ var app10 = new Hono10().get("/:name/variants", async (c) => {
1951
+ const name = c.req.param("name");
1952
+ const entry = findAgent(name);
1953
+ if (!entry) return c.json({ error: "Agent not found" }, 404);
1954
+ const variants = readVariants(name);
1955
+ const results = await Promise.all(
1956
+ variants.map(async (v) => {
1957
+ if (!v.port) return { ...v, status: "no-server" };
1958
+ const health = await checkHealth(v.port);
1959
+ return { ...v, status: health.ok ? "running" : "dead" };
1960
+ })
1961
+ );
1962
+ return c.json(results);
1963
+ });
1964
+ var variants_default = app10;
1965
+
1966
+ // src/web/routes/volute/chat.ts
1967
+ import { readFileSync as readFileSync4 } from "fs";
1968
+ import { resolve as resolve8 } from "path";
1969
+ import { zValidator as zValidator4 } from "@hono/zod-validator";
1970
+ import { Hono as Hono11 } from "hono";
1971
+ import { streamSSE as streamSSE3 } from "hono/streaming";
1972
+ import { z as z4 } from "zod";
1973
+
1974
+ // src/lib/conversations.ts
1975
+ import { randomUUID } from "crypto";
1976
+ import { and as and3, desc as desc2, eq as eq4, inArray, isNull, sql as sql2 } from "drizzle-orm";
1977
+ async function createConversation(agentName, channel, opts) {
1978
+ const db2 = await getDb();
1979
+ const id = randomUUID();
1980
+ await db2.insert(conversations).values({
1981
+ id,
1982
+ agent_name: agentName,
1983
+ channel,
1984
+ user_id: opts?.userId ?? null,
1985
+ title: opts?.title ?? null
1986
+ });
1987
+ if (opts?.participantIds && opts.participantIds.length > 0) {
1988
+ await db2.insert(conversationParticipants).values(
1989
+ opts.participantIds.map((uid, i) => ({
1990
+ conversation_id: id,
1991
+ user_id: uid,
1992
+ role: i === 0 ? "owner" : "member"
1993
+ }))
1994
+ );
1995
+ }
1996
+ return {
1997
+ id,
1998
+ agent_name: agentName,
1999
+ channel,
2000
+ user_id: opts?.userId ?? null,
2001
+ title: opts?.title ?? null,
2002
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
2003
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
2004
+ };
2005
+ }
2006
+ async function getConversation(id) {
2007
+ const db2 = await getDb();
2008
+ const row = await db2.select().from(conversations).where(eq4(conversations.id, id)).get();
2009
+ return row ?? null;
2010
+ }
2011
+ async function getParticipants(conversationId) {
2012
+ const db2 = await getDb();
2013
+ const rows = await db2.select({
2014
+ userId: conversationParticipants.user_id,
2015
+ username: users.username,
2016
+ userType: users.user_type,
2017
+ role: conversationParticipants.role
2018
+ }).from(conversationParticipants).innerJoin(users, eq4(conversationParticipants.user_id, users.id)).where(eq4(conversationParticipants.conversation_id, conversationId)).all();
2019
+ return rows;
2020
+ }
2021
+ async function isParticipant(conversationId, userId) {
2022
+ const db2 = await getDb();
2023
+ const row = await db2.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(
2024
+ and3(
2025
+ eq4(conversationParticipants.conversation_id, conversationId),
2026
+ eq4(conversationParticipants.user_id, userId)
2027
+ )
2028
+ ).get();
2029
+ return row != null;
2030
+ }
2031
+ async function listConversationsForUser(userId) {
2032
+ const db2 = await getDb();
2033
+ const participantRows = await db2.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(eq4(conversationParticipants.user_id, userId)).all();
2034
+ if (participantRows.length === 0) return [];
2035
+ const convIds = participantRows.map((r) => r.conversation_id);
2036
+ return db2.select().from(conversations).where(inArray(conversations.id, convIds)).orderBy(desc2(conversations.updated_at)).all();
2037
+ }
2038
+ async function isParticipantOrOwner(conversationId, userId) {
2039
+ if (await isParticipant(conversationId, userId)) return true;
2040
+ const db2 = await getDb();
2041
+ const row = await db2.select().from(conversations).where(and3(eq4(conversations.id, conversationId), eq4(conversations.user_id, userId))).get();
2042
+ return row != null;
2043
+ }
2044
+ async function deleteConversationForUser(id, userId) {
2045
+ if (!await isParticipantOrOwner(id, userId)) return false;
2046
+ await deleteConversation(id);
2047
+ return true;
2048
+ }
2049
+ async function addMessage(conversationId, role, senderName, content) {
2050
+ const db2 = await getDb();
2051
+ const serialized = JSON.stringify(content);
2052
+ const [result] = await db2.insert(messages).values({ conversation_id: conversationId, role, sender_name: senderName, content: serialized }).returning({ id: messages.id, created_at: messages.created_at });
2053
+ await db2.update(conversations).set({ updated_at: sql2`datetime('now')` }).where(eq4(conversations.id, conversationId));
2054
+ if (role === "user") {
2055
+ const firstText = content.find((b) => b.type === "text");
2056
+ const title = firstText ? firstText.text.slice(0, 80) : "";
2057
+ if (title) {
2058
+ await db2.update(conversations).set({ title }).where(and3(eq4(conversations.id, conversationId), isNull(conversations.title)));
2059
+ }
2060
+ }
2061
+ return {
2062
+ id: result.id,
2063
+ conversation_id: conversationId,
2064
+ role,
1368
2065
  sender_name: senderName,
1369
2066
  content,
1370
2067
  created_at: result.created_at
@@ -1412,26 +2109,39 @@ async function listConversationsWithParticipants(userId) {
1412
2109
  }
1413
2110
  return convs.map((c) => ({ ...c, participants: byConv.get(c.id) ?? [] }));
1414
2111
  }
2112
+ async function findDMConversation(agentName, participantIds) {
2113
+ const db2 = await getDb();
2114
+ const agentConvs = await db2.select({ id: conversations.id }).from(conversations).where(eq4(conversations.agent_name, agentName)).all();
2115
+ for (const conv of agentConvs) {
2116
+ const rows = await db2.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(eq4(conversationParticipants.conversation_id, conv.id)).all();
2117
+ if (rows.length !== 2) continue;
2118
+ const ids = new Set(rows.map((r) => r.user_id));
2119
+ if (ids.has(participantIds[0]) && ids.has(participantIds[1])) {
2120
+ return conv.id;
2121
+ }
2122
+ }
2123
+ return null;
2124
+ }
1415
2125
  async function deleteConversation(id) {
1416
2126
  const db2 = await getDb();
1417
2127
  await db2.delete(conversations).where(eq4(conversations.id, id));
1418
2128
  }
1419
2129
 
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()
2130
+ // src/web/routes/volute/chat.ts
2131
+ var chatSchema = z4.object({
2132
+ message: z4.string().optional(),
2133
+ conversationId: z4.string().optional(),
2134
+ sender: z4.string().optional(),
2135
+ images: z4.array(
2136
+ z4.object({
2137
+ media_type: z4.string(),
2138
+ data: z4.string()
1429
2139
  })
1430
2140
  ).optional()
1431
2141
  });
1432
2142
  function getDaemonUrl() {
1433
- const data = JSON.parse(readFileSync4(resolve6(voluteHome(), "daemon.json"), "utf-8"));
1434
- return `http://127.0.0.1:${data.port}`;
2143
+ const data = JSON.parse(readFileSync4(resolve8(voluteHome(), "daemon.json"), "utf-8"));
2144
+ return `http://${daemonLoopback()}:${data.port}`;
1435
2145
  }
1436
2146
  function daemonFetchInternal(path, body) {
1437
2147
  const daemonUrl = getDaemonUrl();
@@ -1476,15 +2186,11 @@ async function consumeAndPersist(res, conversationId, agentName) {
1476
2186
  }
1477
2187
  return assistantContent;
1478
2188
  }
1479
- var app3 = new Hono3().post("/:name/chat", zValidator2("json", chatSchema), async (c) => {
2189
+ var app11 = new Hono11().post("/:name/chat", zValidator4("json", chatSchema), async (c) => {
1480
2190
  const name = c.req.param("name");
1481
2191
  const [baseName] = name.split("@", 2);
1482
2192
  const entry = findAgent(baseName);
1483
2193
  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
2194
  const body = c.req.valid("json");
1489
2195
  if (!body.message && (!body.images || body.images.length === 0)) {
1490
2196
  return c.json({ error: "message or images required" }, 400);
@@ -1510,12 +2216,20 @@ var app3 = new Hono3().post("/:name/chat", zValidator2("json", chatSchema), asyn
1510
2216
  }
1511
2217
  }
1512
2218
  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;
2219
+ if (participantIds.length === 2) {
2220
+ const existing = await findDMConversation(baseName, participantIds);
2221
+ if (existing) {
2222
+ conversationId = existing;
2223
+ }
2224
+ }
2225
+ if (!conversationId) {
2226
+ const conv = await createConversation(baseName, "volute", {
2227
+ userId: user.id !== 0 ? user.id : void 0,
2228
+ title,
2229
+ participantIds
2230
+ });
2231
+ conversationId = conv.id;
2232
+ }
1519
2233
  }
1520
2234
  const channel = `volute:${conversationId}`;
1521
2235
  const contentBlocks = [];
@@ -1531,22 +2245,23 @@ var app3 = new Hono3().post("/:name/chat", zValidator2("json", chatSchema), asyn
1531
2245
  const participants = await getParticipants(conversationId);
1532
2246
  const agentParticipants = participants.filter((p) => p.userType === "agent");
1533
2247
  const participantNames = participants.map((p) => p.username);
2248
+ const { getAgentManager: getAgentManager2 } = await import("./agent-manager-JDVXU3ON.js");
1534
2249
  const manager = getAgentManager2();
1535
2250
  const runningAgents = agentParticipants.map((ap) => {
1536
2251
  const agentKey = ap.username === baseName ? name : ap.username;
1537
2252
  return manager.isRunning(agentKey) ? ap.username : null;
1538
2253
  }).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
2254
  const isDM = participants.length === 2;
2255
+ const typingMap = getTypingMap();
2256
+ const currentlyTyping = typingMap.get(channel);
1543
2257
  const payload = JSON.stringify({
1544
2258
  content: contentBlocks,
1545
2259
  channel,
1546
2260
  sender: senderName,
1547
2261
  participants: participantNames,
1548
2262
  participantCount: participants.length,
1549
- isDM
2263
+ isDM,
2264
+ ...currentlyTyping.length > 0 ? { typing: currentlyTyping } : {}
1550
2265
  });
1551
2266
  const responses = [];
1552
2267
  for (const agentName of runningAgents) {
@@ -1569,12 +2284,17 @@ var app3 = new Hono3().post("/:name/chat", zValidator2("json", chatSchema), asyn
1569
2284
  }
1570
2285
  }
1571
2286
  if (responses.length === 0) {
1572
- return c.json({ error: "No agents reachable" }, 502);
2287
+ return streamSSE3(c, async (stream2) => {
2288
+ await stream2.writeSSE({
2289
+ data: JSON.stringify({ type: "meta", conversationId })
2290
+ });
2291
+ await stream2.writeSSE({ data: JSON.stringify({ type: "sync" }) });
2292
+ });
1573
2293
  }
1574
2294
  const primary = responses[0];
1575
2295
  const secondary = responses.slice(1);
1576
2296
  const secondaryPromises = secondary.map((s) => consumeAndPersist(s.res, conversationId, s.name));
1577
- return streamSSE(c, async (stream2) => {
2297
+ return streamSSE3(c, async (stream2) => {
1578
2298
  await stream2.writeSSE({
1579
2299
  data: JSON.stringify({ type: "meta", conversationId, senderName: primary.name })
1580
2300
  });
@@ -1585,114 +2305,43 @@ var app3 = new Hono3().post("/:name/chat", zValidator2("json", chatSchema), asyn
1585
2305
  accumulateEvent(assistantContent, event);
1586
2306
  if (event.type === "done") break;
1587
2307
  }
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 });
2308
+ } catch (err) {
2309
+ console.error(`[chat] error streaming response from ${primary.name}:`, err);
2310
+ await stream2.writeSSE({
2311
+ data: JSON.stringify({ type: "error", message: "Stream interrupted" })
2312
+ });
2313
+ }
2314
+ if (assistantContent.length > 0) {
2315
+ try {
2316
+ await addMessage(conversationId, "assistant", primary.name, assistantContent);
2317
+ } catch (err) {
2318
+ console.error(`[chat] failed to persist response from ${primary.name}:`, err);
2319
+ }
2320
+ }
2321
+ const results = await Promise.allSettled(secondaryPromises);
2322
+ for (let i = 0; i < results.length; i++) {
2323
+ if (results[i].status === "rejected") {
2324
+ console.error(
2325
+ `[chat] secondary agent ${secondary[i].name} response failed:`,
2326
+ results[i].reason
2327
+ );
2328
+ }
2329
+ }
2330
+ await stream2.writeSSE({ data: JSON.stringify({ type: "sync" }) });
2331
+ });
1683
2332
  });
1684
- var connectors_default = app4;
2333
+ var chat_default = app11;
1685
2334
 
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()
2335
+ // src/web/routes/volute/conversations.ts
2336
+ import { zValidator as zValidator5 } from "@hono/zod-validator";
2337
+ import { Hono as Hono12 } from "hono";
2338
+ import { z as z5 } from "zod";
2339
+ var createConvSchema = z5.object({
2340
+ title: z5.string().optional(),
2341
+ participantIds: z5.array(z5.number()).optional(),
2342
+ participantNames: z5.array(z5.string()).optional()
1694
2343
  });
1695
- var app5 = new Hono5().get("/:name/conversations", async (c) => {
2344
+ var app12 = new Hono12().get("/:name/conversations", async (c) => {
1696
2345
  const name = c.req.param("name");
1697
2346
  const user = c.get("user");
1698
2347
  let lookupId = user.id;
@@ -1703,7 +2352,7 @@ var app5 = new Hono5().get("/:name/conversations", async (c) => {
1703
2352
  const all = await listConversationsForUser(lookupId);
1704
2353
  const convs = all.filter((c2) => c2.agent_name === name);
1705
2354
  return c.json(convs);
1706
- }).post("/:name/conversations", zValidator3("json", createConvSchema), async (c) => {
2355
+ }).post("/:name/conversations", zValidator5("json", createConvSchema), async (c) => {
1707
2356
  const name = c.req.param("name");
1708
2357
  const user = c.get("user");
1709
2358
  const body = c.req.valid("json");
@@ -1735,10 +2384,19 @@ var app5 = new Hono5().get("/:name/conversations", async (c) => {
1735
2384
  const u = await getUser(id);
1736
2385
  if (!u) return c.json({ error: `User ${id} not found` }, 400);
1737
2386
  }
2387
+ const participantIds = [...participantSet];
2388
+ if (participantIds.length === 2) {
2389
+ const existingId = await findDMConversation(name, participantIds);
2390
+ if (existingId) {
2391
+ const conv2 = await getConversation(existingId);
2392
+ if (conv2) return c.json(conv2);
2393
+ console.warn(`[conversations] DM conversation ${existingId} found but not retrievable`);
2394
+ }
2395
+ }
1738
2396
  const conv = await createConversation(name, "volute", {
1739
2397
  userId: user.id !== 0 ? user.id : void 0,
1740
2398
  title: body.title,
1741
- participantIds: [...participantSet]
2399
+ participantIds
1742
2400
  });
1743
2401
  return c.json(conv, 201);
1744
2402
  }).get("/:name/conversations/:id/messages", async (c) => {
@@ -1752,7 +2410,7 @@ var app5 = new Hono5().get("/:name/conversations", async (c) => {
1752
2410
  }).get("/:name/conversations/:id/participants", async (c) => {
1753
2411
  const id = c.req.param("id");
1754
2412
  const user = c.get("user");
1755
- if (!await isParticipantOrOwner(id, user.id)) {
2413
+ if (user.id !== 0 && !await isParticipantOrOwner(id, user.id)) {
1756
2414
  return c.json({ error: "Conversation not found" }, 404);
1757
2415
  }
1758
2416
  const participants = await getParticipants(id);
@@ -1764,232 +2422,17 @@ var app5 = new Hono5().get("/:name/conversations", async (c) => {
1764
2422
  if (!deleted) return c.json({ error: "Conversation not found" }, 404);
1765
2423
  return c.json({ ok: true });
1766
2424
  });
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;
2425
+ var conversations_default = app12;
1938
2426
 
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;
1983
-
1984
- // src/web/routes/user-conversations.ts
1985
- import { zValidator as zValidator5 } from "@hono/zod-validator";
1986
- import { Hono as Hono11 } from "hono";
1987
- import { z as z5 } from "zod";
1988
- var createSchema = z5.object({
1989
- title: z5.string().optional(),
1990
- participantNames: z5.array(z5.string()).min(1)
2427
+ // src/web/routes/volute/user-conversations.ts
2428
+ import { zValidator as zValidator6 } from "@hono/zod-validator";
2429
+ import { Hono as Hono13 } from "hono";
2430
+ import { z as z6 } from "zod";
2431
+ var createSchema = z6.object({
2432
+ title: z6.string().optional(),
2433
+ participantNames: z6.array(z6.string()).min(1)
1991
2434
  });
1992
- var app11 = new Hono11().use("*", authMiddleware).get("/", async (c) => {
2435
+ var app13 = new Hono13().use("*", authMiddleware).get("/", async (c) => {
1993
2436
  const user = c.get("user");
1994
2437
  const convs = await listConversationsWithParticipants(user.id);
1995
2438
  return c.json(convs);
@@ -2001,7 +2444,7 @@ var app11 = new Hono11().use("*", authMiddleware).get("/", async (c) => {
2001
2444
  }
2002
2445
  const msgs = await getMessages(id);
2003
2446
  return c.json(msgs);
2004
- }).post("/", zValidator5("json", createSchema), async (c) => {
2447
+ }).post("/", zValidator6("json", createSchema), async (c) => {
2005
2448
  const user = c.get("user");
2006
2449
  const body = c.req.valid("json");
2007
2450
  const participantIds = /* @__PURE__ */ new Set();
@@ -2038,29 +2481,11 @@ var app11 = new Hono11().use("*", authMiddleware).get("/", async (c) => {
2038
2481
  if (!deleted) return c.json({ error: "Conversation not found" }, 404);
2039
2482
  return c.json({ ok: true });
2040
2483
  });
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;
2484
+ var user_conversations_default = app13;
2060
2485
 
2061
2486
  // src/web/app.ts
2062
- var app13 = new Hono13();
2063
- app13.onError((err, c) => {
2487
+ var app14 = new Hono14();
2488
+ app14.onError((err, c) => {
2064
2489
  if (err instanceof HTTPException) {
2065
2490
  return err.getResponse();
2066
2491
  }
@@ -2071,10 +2496,10 @@ app13.onError((err, c) => {
2071
2496
  });
2072
2497
  return c.json({ error: "Internal server error" }, 500);
2073
2498
  });
2074
- app13.notFound((c) => {
2499
+ app14.notFound((c) => {
2075
2500
  return c.json({ error: "Not found" }, 404);
2076
2501
  });
2077
- app13.use("*", async (c, next) => {
2502
+ app14.use("*", async (c, next) => {
2078
2503
  const start = Date.now();
2079
2504
  await next();
2080
2505
  const duration = Date.now() - start;
@@ -2085,7 +2510,7 @@ app13.use("*", async (c, next) => {
2085
2510
  duration
2086
2511
  });
2087
2512
  });
2088
- app13.get("/api/health", (c) => {
2513
+ app14.get("/api/health", (c) => {
2089
2514
  let version = "unknown";
2090
2515
  let cached = null;
2091
2516
  try {
@@ -2100,13 +2525,13 @@ app13.get("/api/health", (c) => {
2100
2525
  ...cached?.updateAvailable ? { updateAvailable: true, latest: cached.latest } : {}
2101
2526
  });
2102
2527
  });
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;
2528
+ app14.use("/api/*", bodyLimit({ maxSize: 10 * 1024 * 1024 }));
2529
+ app14.use("/api/*", csrf());
2530
+ app14.use("/api/agents/*", authMiddleware);
2531
+ app14.use("/api/conversations/*", authMiddleware);
2532
+ app14.use("/api/system/*", authMiddleware);
2533
+ var routes = app14.route("/api/auth", auth_default).route("/api/system", system_default).route("/api/system", update_default).route("/api/agents", agents_default).route("/api/agents", chat_default).route("/api/agents", connectors_default).route("/api/agents", schedules_default).route("/api/agents", logs_default).route("/api/agents", typing_default).route("/api/agents", variants_default).route("/api/agents", files_default).route("/api/agents", conversations_default).route("/api/conversations", user_conversations_default);
2534
+ var app_default = app14;
2110
2535
 
2111
2536
  // src/web/server.ts
2112
2537
  var MIME_TYPES = {
@@ -2189,6 +2614,7 @@ async function startDaemon(opts) {
2189
2614
  mkdirSync2(home, { recursive: true });
2190
2615
  const token = process.env.VOLUTE_DAEMON_TOKEN || randomBytes(32).toString("hex");
2191
2616
  process.env.VOLUTE_DAEMON_TOKEN = token;
2617
+ process.env.VOLUTE_DAEMON_HOSTNAME = hostname;
2192
2618
  let server;
2193
2619
  try {
2194
2620
  server = await startServer({ port, hostname });
@@ -2210,6 +2636,8 @@ async function startDaemon(opts) {
2210
2636
  const connectors = initConnectorManager();
2211
2637
  const scheduler = getScheduler();
2212
2638
  scheduler.start(port, token);
2639
+ const tokenBudget = getTokenBudget();
2640
+ tokenBudget.start(port, token);
2213
2641
  const registry = readRegistry();
2214
2642
  for (const entry of registry) {
2215
2643
  if (!entry.running) continue;
@@ -2218,6 +2646,14 @@ async function startDaemon(opts) {
2218
2646
  const dir = agentDir(entry.name);
2219
2647
  await connectors.startConnectors(entry.name, dir, entry.port, port);
2220
2648
  scheduler.loadSchedules(entry.name);
2649
+ const config = readVoluteConfig(dir);
2650
+ if (config?.tokenBudget) {
2651
+ tokenBudget.setBudget(
2652
+ entry.name,
2653
+ config.tokenBudget,
2654
+ config.tokenBudgetPeriodMinutes ?? DEFAULT_BUDGET_PERIOD_MINUTES
2655
+ );
2656
+ }
2221
2657
  } catch (err) {
2222
2658
  console.error(`[daemon] failed to start agent ${entry.name}:`, err);
2223
2659
  setAgentRunning(entry.name, false);
@@ -2258,6 +2694,7 @@ async function startDaemon(opts) {
2258
2694
  console.error("[daemon] shutting down...");
2259
2695
  scheduler.stop();
2260
2696
  scheduler.saveState();
2697
+ tokenBudget.stop();
2261
2698
  await connectors.stopAll();
2262
2699
  await manager.stopAll();
2263
2700
  manager.clearCrashAttempts();