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