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