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.
- package/dist/{agent-Z2B6EFEQ.js → agent-X7GJLBLW.js} +13 -9
- package/dist/{agent-manager-PXBKA2GK.js → agent-manager-JDVXU3ON.js} +4 -4
- package/dist/channel-SMCNOIVQ.js +262 -0
- package/dist/{chunk-5X7HGB6L.js → chunk-AOKAQGO4.js} +1 -1
- package/dist/{chunk-MXUCNIBG.js → chunk-BX7KI4S3.js} +68 -3
- package/dist/{chunk-MW2KFO3B.js → chunk-G6ZNGLUX.js} +3 -3
- package/dist/{chunk-HE67X4T6.js → chunk-H7AMDUIA.js} +1 -1
- package/dist/{chunk-7L4AN5D4.js → chunk-JR4UXCTO.js} +1 -1
- package/dist/{chunk-UX25Z2ND.js → chunk-UWHWAPGO.js} +7 -0
- package/dist/{chunk-UAVD2AHX.js → chunk-W76KWE23.js} +1 -1
- package/dist/chunk-ZZOOTYXK.js +583 -0
- package/dist/cli.js +18 -21
- package/dist/{connector-LYEMXQEV.js → connector-Y7JPNROO.js} +3 -3
- package/dist/connectors/discord.js +31 -4
- package/dist/connectors/slack.js +22 -3
- package/dist/connectors/telegram.js +34 -4
- package/dist/{create-RVCZN6HE.js → create-G525LWEA.js} +2 -2
- package/dist/{daemon-client-ZY6UUN2M.js → daemon-client-442IV43D.js} +2 -2
- package/dist/daemon.js +962 -525
- package/dist/{delete-3QH7VYIN.js → delete-2PH2CGDY.js} +3 -3
- package/dist/{down-O7IFZLVJ.js → down-FXWAN66A.js} +1 -1
- package/dist/{env-4D4REPJF.js → env-7GLUJCWS.js} +2 -2
- package/dist/{history-OEONB53Z.js → history-H72ZUIBN.js} +2 -2
- package/dist/{import-MXJB2EII.js → import-AVKQJDYC.js} +2 -2
- package/dist/{logs-DF342W4M.js → logs-EDGK26AK.js} +1 -1
- package/dist/{message-ADHWFHSI.js → message-SCOQDR3P.js} +2 -2
- package/dist/{package-VQOE7JNH.js → package-4DP4Y4UO.js} +1 -1
- package/dist/restart-O4ETYLJF.js +29 -0
- package/dist/{schedule-NAG6F463.js → schedule-S6QVC5ON.js} +2 -2
- package/dist/send-G7PE4DOJ.js +72 -0
- package/dist/{setup-RPRRGG2F.js → setup-F4TCWVSP.js} +2 -2
- package/dist/{start-TUOXDSFL.js → start-VHQ7LNWM.js} +2 -2
- package/dist/{status-A36EHRO4.js → status-QAJWXKMZ.js} +2 -2
- package/dist/{stop-AOJZLQ5X.js → stop-CAGCT5NI.js} +2 -2
- package/dist/{up-7ILD7GU7.js → up-CSX3ZUIU.js} +15 -3
- package/dist/{update-LPSIAWQ2.js → update-XSIX3GGP.js} +2 -2
- package/dist/{update-check-Y33QDCFL.js → update-check-5ZADDHCK.js} +2 -2
- package/dist/{upgrade-FX2TKJ2S.js → upgrade-YXKPWDRU.js} +2 -2
- package/dist/{variant-LAB67OC2.js → variant-4Z6W3PP6.js} +2 -2
- package/dist/web-assets/assets/index-D5PzIndO.js +308 -0
- package/dist/web-assets/index.html +1 -1
- package/package.json +1 -1
- package/templates/_base/.init/.config/scripts/session-reader.ts +59 -0
- package/templates/_base/_skills/sessions/SKILL.md +49 -0
- package/templates/_base/_skills/volute-agent/SKILL.md +13 -9
- package/templates/_base/src/lib/format-prefix.ts +6 -0
- package/templates/_base/src/lib/router.ts +30 -3
- package/templates/_base/src/lib/session-monitor.ts +400 -0
- package/templates/_base/src/lib/types.ts +2 -0
- package/templates/agent-sdk/src/agent.ts +16 -0
- package/templates/agent-sdk/src/lib/hooks/session-context.ts +32 -0
- package/templates/pi/src/agent.ts +7 -1
- package/templates/pi/src/lib/session-context-extension.ts +33 -0
- package/dist/channel-MK5OK2SI.js +0 -113
- package/dist/chunk-SMISE4SV.js +0 -226
- package/dist/conversation-ERXEQZTY.js +0 -163
- package/dist/send-66QMKRUH.js +0 -75
- 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-
|
|
9
|
+
} from "./chunk-G6ZNGLUX.js";
|
|
10
10
|
import {
|
|
11
11
|
checkForUpdate,
|
|
12
12
|
checkForUpdateCached,
|
|
13
13
|
getCurrentVersion
|
|
14
|
-
} from "./chunk-
|
|
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-
|
|
27
|
+
} from "./chunk-H7AMDUIA.js";
|
|
28
|
+
import "./chunk-BX7KI4S3.js";
|
|
28
29
|
import {
|
|
29
30
|
applyIsolation
|
|
30
|
-
} from "./chunk-
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
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
|
|
1204
|
+
const provider = CHANNELS[cs.type];
|
|
958
1205
|
channels.push({
|
|
959
|
-
name:
|
|
960
|
-
displayName:
|
|
1206
|
+
name: provider?.name ?? cs.type,
|
|
1207
|
+
displayName: provider?.displayName ?? cs.type,
|
|
961
1208
|
status: cs.running ? "connected" : "disconnected",
|
|
962
|
-
showToolCalls:
|
|
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)
|
|
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
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
await
|
|
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
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
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
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
})
|
|
1178
|
-
|
|
1179
|
-
|
|
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/
|
|
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
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
const
|
|
1287
|
-
const
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
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
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
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
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
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
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
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 =
|
|
1422
|
-
message:
|
|
1423
|
-
conversationId:
|
|
1424
|
-
sender:
|
|
1425
|
-
images:
|
|
1426
|
-
|
|
1427
|
-
media_type:
|
|
1428
|
-
data:
|
|
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(
|
|
1434
|
-
return `http
|
|
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
|
|
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
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
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
|
|
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
|
|
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
|
|
2333
|
+
var chat_default = app11;
|
|
1685
2334
|
|
|
1686
|
-
// src/web/routes/conversations.ts
|
|
1687
|
-
import { zValidator as
|
|
1688
|
-
import { Hono as
|
|
1689
|
-
import { z as
|
|
1690
|
-
var createConvSchema =
|
|
1691
|
-
title:
|
|
1692
|
-
participantIds:
|
|
1693
|
-
participantNames:
|
|
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
|
|
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",
|
|
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
|
|
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 =
|
|
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/
|
|
1940
|
-
import {
|
|
1941
|
-
import {
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
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
|
|
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("/",
|
|
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 =
|
|
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
|
|
2063
|
-
|
|
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
|
-
|
|
2499
|
+
app14.notFound((c) => {
|
|
2075
2500
|
return c.json({ error: "Not found" }, 404);
|
|
2076
2501
|
});
|
|
2077
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
var routes =
|
|
2109
|
-
var app_default =
|
|
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();
|