volute 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -29
- package/dist/agent-Z2B6EFEQ.js +75 -0
- package/dist/{agent-manager-AUCKMGPR.js → agent-manager-PXBKA2GK.js} +4 -4
- package/dist/channel-MK5OK2SI.js +113 -0
- package/dist/chunk-5X7HGB6L.js +107 -0
- package/dist/{chunk-YGFIWIOF.js → chunk-7L4AN5D4.js} +1 -1
- package/dist/{chunk-VRVVQIYY.js → chunk-AZEL2IEK.js} +1 -1
- package/dist/chunk-B3R6L2GW.js +24 -0
- package/dist/{chunk-DNOXHLE5.js → chunk-HE67X4T6.js} +1 -1
- package/dist/{chunk-I6OHXCMV.js → chunk-MW2KFO3B.js} +47 -9
- package/dist/chunk-MXUCNIBG.js +168 -0
- package/dist/chunk-SMISE4SV.js +226 -0
- package/dist/{chunk-SOZA2TLP.js → chunk-UAVD2AHX.js} +1 -1
- package/dist/{chunk-3C2XR4IY.js → chunk-UX25Z2ND.js} +113 -107
- package/dist/{chunk-GSPKUPKU.js → chunk-XUA3JUFK.js} +2 -1
- package/dist/chunk-ZYGKG6VC.js +22 -0
- package/dist/cli.js +98 -75
- package/dist/connector-LYEMXQEV.js +157 -0
- package/dist/connectors/discord.js +104 -161
- package/dist/connectors/slack.js +179 -0
- package/dist/connectors/telegram.js +175 -0
- package/dist/conversation-ERXEQZTY.js +163 -0
- package/dist/create-RVCZN6HE.js +91 -0
- package/dist/{daemon-client-XR24PUJF.js → daemon-client-ZY6UUN2M.js} +2 -2
- package/dist/daemon.js +824 -252
- package/dist/{delete-GQ7JEK2S.js → delete-3QH7VYIN.js} +8 -9
- package/dist/{down-3OB6UVAJ.js → down-O7IFZLVJ.js} +1 -1
- package/dist/{env-JB27UAC3.js → env-4D4REPJF.js} +8 -5
- package/dist/{history-3VRUBGGV.js → history-OEONB53Z.js} +5 -5
- package/dist/{import-K4MP2GX7.js → import-MXJB2EII.js} +23 -8
- package/dist/{logs-NXFFGUKY.js → logs-DF342W4M.js} +2 -2
- package/dist/message-ADHWFHSI.js +32 -0
- package/dist/package-VQOE7JNH.js +89 -0
- package/dist/{schedule-4I5TYHFH.js → schedule-NAG6F463.js} +12 -7
- package/dist/send-66QMKRUH.js +75 -0
- package/dist/{setup-SRS7AUAA.js → setup-RPRRGG2F.js} +6 -6
- package/dist/{start-LDPMCMYT.js → start-TUOXDSFL.js} +3 -3
- package/dist/{status-MVSQG54T.js → status-A36EHRO4.js} +3 -3
- package/dist/{stop-5PZTZCLL.js → stop-AOJZLQ5X.js} +6 -7
- package/dist/{up-UT3IMKCA.js → up-7ILD7GU7.js} +2 -2
- package/dist/update-LPSIAWQ2.js +140 -0
- package/dist/update-check-Y33QDCFL.js +17 -0
- package/dist/{upgrade-CDKECCGN.js → upgrade-FX2TKJ2S.js} +16 -15
- package/dist/{variant-CVYM3EQG.js → variant-LAB67OC2.js} +17 -12
- package/dist/web-assets/assets/index-BbRmoxoA.js +308 -0
- package/dist/web-assets/index.html +2 -2
- package/drizzle/0003_clean_ego.sql +12 -0
- package/drizzle/meta/0003_snapshot.json +417 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +3 -1
- package/templates/_base/.init/.config/hooks/startup-context.sh +19 -1
- package/templates/_base/_skills/volute-agent/SKILL.md +112 -16
- package/templates/_base/home/.config/routes.json +10 -0
- package/templates/_base/home/VOLUTE.md +19 -28
- package/templates/_base/src/lib/file-handler.ts +46 -0
- package/templates/_base/src/lib/format-prefix.ts +1 -1
- package/templates/_base/src/lib/router.ts +327 -0
- package/templates/_base/src/lib/routing.ts +137 -0
- package/templates/_base/src/lib/types.ts +16 -3
- package/templates/_base/src/lib/volute-server.ts +20 -48
- package/templates/agent-sdk/.init/.config/routes.json +5 -0
- package/templates/agent-sdk/.init/CLAUDE.md +2 -2
- package/templates/agent-sdk/src/agent.ts +269 -82
- package/templates/agent-sdk/src/server.ts +19 -4
- package/templates/agent-sdk/volute-template.json +1 -1
- package/templates/pi/.init/.config/routes.json +5 -0
- package/templates/pi/.init/AGENTS.md +1 -1
- package/templates/pi/src/agent.ts +279 -58
- package/templates/pi/src/server.ts +15 -4
- package/templates/pi/volute-template.json +1 -1
- package/dist/channel-7FZ6D25H.js +0 -90
- package/dist/chunk-N4YNKR3Q.js +0 -90
- package/dist/connector-TVJULIRT.js +0 -96
- package/dist/create-BRG2DBWI.js +0 -79
- package/dist/send-UK3JBZIB.js +0 -53
- package/dist/web-assets/assets/index-BC5eSqbY.js +0 -296
- package/templates/_base/src/lib/sessions.ts +0 -71
- package/templates/agent-sdk/.init/.config/sessions.json +0 -4
- package/templates/agent-sdk/src/lib/agent-sessions.ts +0 -204
- package/templates/pi/.init/.config/sessions.json +0 -1
- package/templates/pi/src/lib/agent-sessions.ts +0 -210
- package/dist/{service-SA4TTMDU.js → service-HZNIDNJF.js} +3 -3
package/dist/daemon.js
CHANGED
|
@@ -1,26 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
RotatingLog,
|
|
3
4
|
clearJsonMap,
|
|
4
5
|
getAgentManager,
|
|
5
6
|
initAgentManager,
|
|
6
7
|
loadJsonMap,
|
|
7
8
|
saveJsonMap
|
|
8
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-MW2KFO3B.js";
|
|
9
10
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from "./chunk-
|
|
11
|
+
checkForUpdate,
|
|
12
|
+
checkForUpdateCached,
|
|
13
|
+
getCurrentVersion
|
|
14
|
+
} from "./chunk-5X7HGB6L.js";
|
|
15
|
+
import {
|
|
16
|
+
CHANNELS
|
|
17
|
+
} from "./chunk-SMISE4SV.js";
|
|
18
|
+
import {
|
|
19
|
+
collectPart
|
|
20
|
+
} from "./chunk-B3R6L2GW.js";
|
|
14
21
|
import {
|
|
15
22
|
readVoluteConfig,
|
|
16
23
|
writeVoluteConfig
|
|
17
24
|
} from "./chunk-NETNFBA5.js";
|
|
18
25
|
import {
|
|
19
26
|
loadMergedEnv
|
|
20
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-HE67X4T6.js";
|
|
21
28
|
import {
|
|
22
29
|
applyIsolation
|
|
23
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-UAVD2AHX.js";
|
|
31
|
+
import {
|
|
32
|
+
resolveVoluteBin
|
|
33
|
+
} from "./chunk-5SKQ6J7T.js";
|
|
24
34
|
import {
|
|
25
35
|
agentDir,
|
|
26
36
|
checkHealth,
|
|
@@ -34,32 +44,101 @@ import {
|
|
|
34
44
|
setAgentRunning,
|
|
35
45
|
setVariantRunning,
|
|
36
46
|
voluteHome
|
|
37
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-UX25Z2ND.js";
|
|
38
48
|
import {
|
|
39
49
|
__export
|
|
40
50
|
} from "./chunk-K3NQKI34.js";
|
|
41
51
|
|
|
42
52
|
// src/daemon.ts
|
|
43
53
|
import { randomBytes } from "crypto";
|
|
44
|
-
import { mkdirSync as mkdirSync2, readFileSync as
|
|
45
|
-
import {
|
|
54
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
55
|
+
import { homedir } from "os";
|
|
56
|
+
import { resolve as resolve10 } from "path";
|
|
57
|
+
import { format } from "util";
|
|
46
58
|
|
|
47
59
|
// src/lib/connector-manager.ts
|
|
48
60
|
import { spawn } from "child_process";
|
|
49
|
-
import {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, unlinkSync, writeFileSync } from "fs";
|
|
62
|
+
import { dirname, resolve as resolve2 } from "path";
|
|
63
|
+
|
|
64
|
+
// src/lib/connector-defs.ts
|
|
65
|
+
import { existsSync, readFileSync } from "fs";
|
|
66
|
+
import { resolve } from "path";
|
|
67
|
+
var BUILTIN_DEFS = {
|
|
68
|
+
discord: {
|
|
69
|
+
displayName: "Discord",
|
|
70
|
+
description: "Connect to Discord as a bot",
|
|
71
|
+
envVars: [
|
|
72
|
+
{
|
|
73
|
+
name: "DISCORD_TOKEN",
|
|
74
|
+
required: true,
|
|
75
|
+
description: "Discord bot token",
|
|
76
|
+
scope: "agent"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "DISCORD_GUILD_ID",
|
|
80
|
+
required: false,
|
|
81
|
+
description: "Discord server ID (optional, for slash commands)",
|
|
82
|
+
scope: "agent"
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
slack: {
|
|
87
|
+
displayName: "Slack",
|
|
88
|
+
description: "Connect to Slack via Socket Mode",
|
|
89
|
+
envVars: [
|
|
90
|
+
{
|
|
91
|
+
name: "SLACK_BOT_TOKEN",
|
|
92
|
+
required: true,
|
|
93
|
+
description: "Slack bot token (xoxb-...)",
|
|
94
|
+
scope: "agent"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: "SLACK_APP_TOKEN",
|
|
98
|
+
required: true,
|
|
99
|
+
description: "Slack app-level token (xapp-...) for Socket Mode",
|
|
100
|
+
scope: "agent"
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
},
|
|
104
|
+
telegram: {
|
|
105
|
+
displayName: "Telegram",
|
|
106
|
+
description: "Connect to Telegram via long polling",
|
|
107
|
+
envVars: [
|
|
108
|
+
{
|
|
109
|
+
name: "TELEGRAM_BOT_TOKEN",
|
|
110
|
+
required: true,
|
|
111
|
+
description: "Telegram bot token from BotFather",
|
|
112
|
+
scope: "agent"
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
function getConnectorDef(type, connectorDir) {
|
|
118
|
+
if (BUILTIN_DEFS[type]) return BUILTIN_DEFS[type];
|
|
119
|
+
if (connectorDir) {
|
|
120
|
+
const jsonPath = resolve(connectorDir, "connector.json");
|
|
121
|
+
if (existsSync(jsonPath)) {
|
|
122
|
+
try {
|
|
123
|
+
return JSON.parse(readFileSync(jsonPath, "utf-8"));
|
|
124
|
+
} catch (err) {
|
|
125
|
+
console.warn(`Failed to parse ${jsonPath}: ${err}`);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
function checkMissingEnvVars(def, env) {
|
|
133
|
+
return def.envVars.filter((v) => v.required && !env[v.name]);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/lib/connector-manager.ts
|
|
58
137
|
function searchUpwards(...segments) {
|
|
59
138
|
let searchDir = dirname(new URL(import.meta.url).pathname);
|
|
60
139
|
for (let i = 0; i < 5; i++) {
|
|
61
|
-
const candidate =
|
|
62
|
-
if (
|
|
140
|
+
const candidate = resolve2(searchDir, ...segments);
|
|
141
|
+
if (existsSync2(candidate)) return candidate;
|
|
63
142
|
searchDir = dirname(searchDir);
|
|
64
143
|
}
|
|
65
144
|
return null;
|
|
@@ -85,6 +164,20 @@ var ConnectorManager = class {
|
|
|
85
164
|
}
|
|
86
165
|
}
|
|
87
166
|
}
|
|
167
|
+
checkConnectorEnv(type, agentDir2) {
|
|
168
|
+
const agentConnectorDir = resolve2(agentDir2, "connectors", type);
|
|
169
|
+
const userConnectorDir = resolve2(voluteHome(), "connectors", type);
|
|
170
|
+
const connectorDir = existsSync2(agentConnectorDir) ? agentConnectorDir : existsSync2(userConnectorDir) ? userConnectorDir : void 0;
|
|
171
|
+
const def = getConnectorDef(type, connectorDir);
|
|
172
|
+
if (!def) return null;
|
|
173
|
+
const env = loadMergedEnv(agentDir2);
|
|
174
|
+
const missing = checkMissingEnvVars(def, env);
|
|
175
|
+
if (missing.length === 0) return null;
|
|
176
|
+
return {
|
|
177
|
+
missing: missing.map((v) => ({ name: v.name, description: v.description })),
|
|
178
|
+
connectorName: def.displayName
|
|
179
|
+
};
|
|
180
|
+
}
|
|
88
181
|
async startConnector(agentName, agentDir2, agentPort, type, daemonPort) {
|
|
89
182
|
const existing = this.connectors.get(agentName)?.get(type);
|
|
90
183
|
if (existing) {
|
|
@@ -106,15 +199,15 @@ var ConnectorManager = class {
|
|
|
106
199
|
this.connectors.get(agentName)?.delete(type);
|
|
107
200
|
}
|
|
108
201
|
this.killOrphanConnector(agentDir2, type);
|
|
109
|
-
const agentConnector =
|
|
110
|
-
const userConnector =
|
|
202
|
+
const agentConnector = resolve2(agentDir2, "connectors", type, "index.ts");
|
|
203
|
+
const userConnector = resolve2(voluteHome(), "connectors", type, "index.ts");
|
|
111
204
|
const builtinConnector = this.resolveBuiltinConnector(type);
|
|
112
205
|
let connectorScript;
|
|
113
206
|
let runtime;
|
|
114
|
-
if (
|
|
207
|
+
if (existsSync2(agentConnector)) {
|
|
115
208
|
connectorScript = agentConnector;
|
|
116
|
-
runtime =
|
|
117
|
-
} else if (
|
|
209
|
+
runtime = resolve2(agentDir2, "node_modules", ".bin", "tsx");
|
|
210
|
+
} else if (existsSync2(userConnector)) {
|
|
118
211
|
connectorScript = userConnector;
|
|
119
212
|
runtime = this.resolveVoluteTsx();
|
|
120
213
|
} else if (builtinConnector) {
|
|
@@ -123,9 +216,9 @@ var ConnectorManager = class {
|
|
|
123
216
|
} else {
|
|
124
217
|
throw new Error(`No connector code found for type: ${type}`);
|
|
125
218
|
}
|
|
126
|
-
const logsDir =
|
|
219
|
+
const logsDir = resolve2(agentDir2, ".volute", "logs");
|
|
127
220
|
mkdirSync(logsDir, { recursive: true });
|
|
128
|
-
const logStream =
|
|
221
|
+
const logStream = new RotatingLog(resolve2(logsDir, `${type}.log`));
|
|
129
222
|
const agentEnv = loadMergedEnv(agentDir2);
|
|
130
223
|
const prefix = `${type.toUpperCase()}_`;
|
|
131
224
|
const connectorEnv = Object.fromEntries(
|
|
@@ -137,6 +230,7 @@ var ConnectorManager = class {
|
|
|
137
230
|
...process.env,
|
|
138
231
|
VOLUTE_AGENT_PORT: String(agentPort),
|
|
139
232
|
VOLUTE_AGENT_NAME: agentName,
|
|
233
|
+
VOLUTE_AGENT_DIR: agentDir2,
|
|
140
234
|
...daemonPort ? {
|
|
141
235
|
VOLUTE_DAEMON_URL: `http://127.0.0.1:${daemonPort}`,
|
|
142
236
|
VOLUTE_DAEMON_TOKEN: process.env.VOLUTE_DAEMON_TOKEN
|
|
@@ -146,8 +240,12 @@ var ConnectorManager = class {
|
|
|
146
240
|
};
|
|
147
241
|
await applyIsolation(spawnOpts, agentName);
|
|
148
242
|
const child = spawn(runtime, [connectorScript], spawnOpts);
|
|
243
|
+
let lastStderr = "";
|
|
149
244
|
child.stdout?.pipe(logStream);
|
|
150
|
-
child.stderr?.
|
|
245
|
+
child.stderr?.on("data", (chunk) => {
|
|
246
|
+
logStream.write(chunk);
|
|
247
|
+
lastStderr = chunk.toString().trim();
|
|
248
|
+
});
|
|
151
249
|
if (child.pid) {
|
|
152
250
|
this.saveConnectorPid(agentDir2, type, child.pid);
|
|
153
251
|
}
|
|
@@ -165,6 +263,7 @@ var ConnectorManager = class {
|
|
|
165
263
|
if (this.shuttingDown) return;
|
|
166
264
|
if (this.stopping.has(stopKey)) return;
|
|
167
265
|
console.error(`[daemon] connector ${type} for ${agentName} exited with code ${code}`);
|
|
266
|
+
if (lastStderr) console.error(`[daemon] last output: ${lastStderr}`);
|
|
168
267
|
const attempts = this.restartAttempts.get(stopKey) ?? 0;
|
|
169
268
|
if (attempts >= MAX_RESTART_ATTEMPTS) {
|
|
170
269
|
console.error(
|
|
@@ -194,19 +293,19 @@ var ConnectorManager = class {
|
|
|
194
293
|
const stopKey = `${agentName}:${type}`;
|
|
195
294
|
this.stopping.add(stopKey);
|
|
196
295
|
agentMap.delete(type);
|
|
197
|
-
await new Promise((
|
|
198
|
-
tracked.child.on("exit", () =>
|
|
296
|
+
await new Promise((resolve11) => {
|
|
297
|
+
tracked.child.on("exit", () => resolve11());
|
|
199
298
|
try {
|
|
200
299
|
tracked.child.kill("SIGTERM");
|
|
201
300
|
} catch {
|
|
202
|
-
|
|
301
|
+
resolve11();
|
|
203
302
|
}
|
|
204
303
|
setTimeout(() => {
|
|
205
304
|
try {
|
|
206
305
|
tracked.child.kill("SIGKILL");
|
|
207
306
|
} catch {
|
|
208
307
|
}
|
|
209
|
-
|
|
308
|
+
resolve11();
|
|
210
309
|
}, 5e3);
|
|
211
310
|
});
|
|
212
311
|
this.stopping.delete(stopKey);
|
|
@@ -238,7 +337,7 @@ var ConnectorManager = class {
|
|
|
238
337
|
}));
|
|
239
338
|
}
|
|
240
339
|
connectorPidPath(agentDir2, type) {
|
|
241
|
-
return
|
|
340
|
+
return resolve2(agentDir2, ".volute", "connectors", `${type}.pid`);
|
|
242
341
|
}
|
|
243
342
|
saveConnectorPid(agentDir2, type, pid) {
|
|
244
343
|
const pidPath = this.connectorPidPath(agentDir2, type);
|
|
@@ -253,9 +352,9 @@ var ConnectorManager = class {
|
|
|
253
352
|
}
|
|
254
353
|
killOrphanConnector(agentDir2, type) {
|
|
255
354
|
const pidPath = this.connectorPidPath(agentDir2, type);
|
|
256
|
-
if (!
|
|
355
|
+
if (!existsSync2(pidPath)) return;
|
|
257
356
|
try {
|
|
258
|
-
const pid = parseInt(
|
|
357
|
+
const pid = parseInt(readFileSync2(pidPath, "utf-8").trim(), 10);
|
|
259
358
|
if (pid > 0) {
|
|
260
359
|
process.kill(pid, "SIGTERM");
|
|
261
360
|
console.error(`[daemon] killed orphan connector ${type} (pid ${pid})`);
|
|
@@ -286,7 +385,7 @@ function getConnectorManager() {
|
|
|
286
385
|
}
|
|
287
386
|
|
|
288
387
|
// src/lib/scheduler.ts
|
|
289
|
-
import { resolve as
|
|
388
|
+
import { resolve as resolve3 } from "path";
|
|
290
389
|
import { CronExpressionParser } from "cron-parser";
|
|
291
390
|
var Scheduler = class {
|
|
292
391
|
schedules = /* @__PURE__ */ new Map();
|
|
@@ -296,7 +395,7 @@ var Scheduler = class {
|
|
|
296
395
|
daemonPort = null;
|
|
297
396
|
daemonToken = null;
|
|
298
397
|
get statePath() {
|
|
299
|
-
return
|
|
398
|
+
return resolve3(voluteHome(), "scheduler-state.json");
|
|
300
399
|
}
|
|
301
400
|
start(daemonPort, daemonToken) {
|
|
302
401
|
this.daemonPort = daemonPort ?? null;
|
|
@@ -374,6 +473,8 @@ var Scheduler = class {
|
|
|
374
473
|
channel: "system:scheduler",
|
|
375
474
|
sender: schedule.id
|
|
376
475
|
});
|
|
476
|
+
const controller = new AbortController();
|
|
477
|
+
const timeout = setTimeout(() => controller.abort(), 12e4);
|
|
377
478
|
try {
|
|
378
479
|
let res;
|
|
379
480
|
if (this.daemonPort && this.daemonToken) {
|
|
@@ -385,13 +486,15 @@ var Scheduler = class {
|
|
|
385
486
|
Authorization: `Bearer ${this.daemonToken}`,
|
|
386
487
|
Origin: daemonUrl
|
|
387
488
|
},
|
|
388
|
-
body
|
|
489
|
+
body,
|
|
490
|
+
signal: controller.signal
|
|
389
491
|
});
|
|
390
492
|
} else {
|
|
391
493
|
res = await fetch(`http://127.0.0.1:${entry.port}/message`, {
|
|
392
494
|
method: "POST",
|
|
393
495
|
headers: { "Content-Type": "application/json" },
|
|
394
|
-
body
|
|
496
|
+
body,
|
|
497
|
+
signal: controller.signal
|
|
395
498
|
});
|
|
396
499
|
}
|
|
397
500
|
if (!res.ok) {
|
|
@@ -399,8 +502,22 @@ var Scheduler = class {
|
|
|
399
502
|
} else {
|
|
400
503
|
console.error(`[scheduler] fired "${schedule.id}" for ${agentName}`);
|
|
401
504
|
}
|
|
505
|
+
try {
|
|
506
|
+
const reader = res.body?.getReader();
|
|
507
|
+
if (reader) {
|
|
508
|
+
try {
|
|
509
|
+
while (!(await reader.read()).done) {
|
|
510
|
+
}
|
|
511
|
+
} finally {
|
|
512
|
+
reader.releaseLock();
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
} catch {
|
|
516
|
+
}
|
|
402
517
|
} catch (err) {
|
|
403
518
|
console.error(`[scheduler] failed to fire "${schedule.id}" for ${agentName}:`, err);
|
|
519
|
+
} finally {
|
|
520
|
+
clearTimeout(timeout);
|
|
404
521
|
}
|
|
405
522
|
}
|
|
406
523
|
};
|
|
@@ -421,8 +538,8 @@ import { compareSync, hashSync } from "bcryptjs";
|
|
|
421
538
|
import { and, count, eq } from "drizzle-orm";
|
|
422
539
|
|
|
423
540
|
// src/lib/db.ts
|
|
424
|
-
import { chmodSync, existsSync as
|
|
425
|
-
import { dirname as dirname2, resolve as
|
|
541
|
+
import { chmodSync, existsSync as existsSync3 } from "fs";
|
|
542
|
+
import { dirname as dirname2, resolve as resolve4 } from "path";
|
|
426
543
|
import { fileURLToPath } from "url";
|
|
427
544
|
import { drizzle } from "drizzle-orm/libsql";
|
|
428
545
|
import { migrate } from "drizzle-orm/libsql/migrator";
|
|
@@ -431,18 +548,20 @@ import { migrate } from "drizzle-orm/libsql/migrator";
|
|
|
431
548
|
var schema_exports = {};
|
|
432
549
|
__export(schema_exports, {
|
|
433
550
|
agentMessages: () => agentMessages,
|
|
551
|
+
conversationParticipants: () => conversationParticipants,
|
|
434
552
|
conversations: () => conversations,
|
|
435
553
|
messages: () => messages,
|
|
436
554
|
sessions: () => sessions,
|
|
437
555
|
users: () => users
|
|
438
556
|
});
|
|
439
557
|
import { sql } from "drizzle-orm";
|
|
440
|
-
import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
558
|
+
import { index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
|
|
441
559
|
var users = sqliteTable("users", {
|
|
442
560
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
443
561
|
username: text("username").unique().notNull(),
|
|
444
562
|
password_hash: text("password_hash").notNull(),
|
|
445
563
|
role: text("role").notNull().default("pending"),
|
|
564
|
+
user_type: text("user_type").notNull().default("human"),
|
|
446
565
|
created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
|
|
447
566
|
});
|
|
448
567
|
var conversations = sqliteTable(
|
|
@@ -478,6 +597,19 @@ var agentMessages = sqliteTable(
|
|
|
478
597
|
index("idx_agent_messages_channel").on(table.agent, table.channel)
|
|
479
598
|
]
|
|
480
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
|
+
);
|
|
481
613
|
var sessions = sqliteTable("sessions", {
|
|
482
614
|
id: text("id").primaryKey(),
|
|
483
615
|
userId: integer("user_id").references(() => users.id, { onDelete: "cascade" }).notNull(),
|
|
@@ -498,11 +630,11 @@ var messages = sqliteTable(
|
|
|
498
630
|
|
|
499
631
|
// src/lib/db.ts
|
|
500
632
|
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
501
|
-
var migrationsFolder =
|
|
633
|
+
var migrationsFolder = existsSync3(resolve4(__dirname, "../drizzle")) ? resolve4(__dirname, "../drizzle") : resolve4(__dirname, "../../drizzle");
|
|
502
634
|
var db = null;
|
|
503
635
|
async function getDb() {
|
|
504
636
|
if (db) return db;
|
|
505
|
-
const dbPath = process.env.VOLUTE_DB_PATH ||
|
|
637
|
+
const dbPath = process.env.VOLUTE_DB_PATH || resolve4(voluteHome(), "volute.db");
|
|
506
638
|
db = drizzle({ connection: { url: `file:${dbPath}` }, schema: schema_exports });
|
|
507
639
|
await migrate(db, { migrationsFolder });
|
|
508
640
|
try {
|
|
@@ -520,12 +652,13 @@ async function getDb() {
|
|
|
520
652
|
async function createUser(username, password) {
|
|
521
653
|
const db2 = await getDb();
|
|
522
654
|
const hash = hashSync(password, 10);
|
|
523
|
-
const [{ value }] = await db2.select({ value: count() }).from(users);
|
|
655
|
+
const [{ value }] = await db2.select({ value: count() }).from(users).where(eq(users.user_type, "human"));
|
|
524
656
|
const role = value === 0 ? "admin" : "pending";
|
|
525
657
|
const [result] = await db2.insert(users).values({ username, password_hash: hash, role }).returning({
|
|
526
658
|
id: users.id,
|
|
527
659
|
username: users.username,
|
|
528
660
|
role: users.role,
|
|
661
|
+
user_type: users.user_type,
|
|
529
662
|
created_at: users.created_at
|
|
530
663
|
});
|
|
531
664
|
return result;
|
|
@@ -534,6 +667,7 @@ async function verifyUser(username, password) {
|
|
|
534
667
|
const db2 = await getDb();
|
|
535
668
|
const row = await db2.select().from(users).where(eq(users.username, username)).get();
|
|
536
669
|
if (!row) return null;
|
|
670
|
+
if (row.user_type === "agent") return null;
|
|
537
671
|
if (!compareSync(password, row.password_hash)) return null;
|
|
538
672
|
const { password_hash: _, ...user } = row;
|
|
539
673
|
return user;
|
|
@@ -544,6 +678,7 @@ async function getUser(id) {
|
|
|
544
678
|
id: users.id,
|
|
545
679
|
username: users.username,
|
|
546
680
|
role: users.role,
|
|
681
|
+
user_type: users.user_type,
|
|
547
682
|
created_at: users.created_at
|
|
548
683
|
}).from(users).where(eq(users.id, id)).get();
|
|
549
684
|
return row ?? null;
|
|
@@ -554,6 +689,7 @@ async function getUserByUsername(username) {
|
|
|
554
689
|
id: users.id,
|
|
555
690
|
username: users.username,
|
|
556
691
|
role: users.role,
|
|
692
|
+
user_type: users.user_type,
|
|
557
693
|
created_at: users.created_at
|
|
558
694
|
}).from(users).where(eq(users.username, username)).get();
|
|
559
695
|
return row ?? null;
|
|
@@ -564,6 +700,7 @@ async function listUsers() {
|
|
|
564
700
|
id: users.id,
|
|
565
701
|
username: users.username,
|
|
566
702
|
role: users.role,
|
|
703
|
+
user_type: users.user_type,
|
|
567
704
|
created_at: users.created_at
|
|
568
705
|
}).from(users).orderBy(users.created_at).all();
|
|
569
706
|
}
|
|
@@ -573,9 +710,58 @@ async function listPendingUsers() {
|
|
|
573
710
|
id: users.id,
|
|
574
711
|
username: users.username,
|
|
575
712
|
role: users.role,
|
|
713
|
+
user_type: users.user_type,
|
|
576
714
|
created_at: users.created_at
|
|
577
715
|
}).from(users).where(eq(users.role, "pending")).orderBy(users.created_at).all();
|
|
578
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
|
|
749
|
+
});
|
|
750
|
+
return result;
|
|
751
|
+
} catch (err) {
|
|
752
|
+
if (err instanceof Error && err.message.includes("UNIQUE constraint")) {
|
|
753
|
+
const retried = await db2.select({
|
|
754
|
+
id: users.id,
|
|
755
|
+
username: users.username,
|
|
756
|
+
role: users.role,
|
|
757
|
+
user_type: users.user_type,
|
|
758
|
+
created_at: users.created_at
|
|
759
|
+
}).from(users).where(and(eq(users.username, agentName), eq(users.user_type, "agent"))).get();
|
|
760
|
+
if (retried) return retried;
|
|
761
|
+
}
|
|
762
|
+
throw err;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
579
765
|
async function approveUser(id) {
|
|
580
766
|
const db2 = await getDb();
|
|
581
767
|
await db2.update(users).set({ role: "user" }).where(and(eq(users.id, id), eq(users.role, "pending")));
|
|
@@ -625,7 +811,7 @@ var authMiddleware = createMiddleware(async (c, next) => {
|
|
|
625
811
|
if (authHeader?.startsWith("Bearer ")) {
|
|
626
812
|
const token = authHeader.slice(7);
|
|
627
813
|
if (token && isValidDaemonToken(token)) {
|
|
628
|
-
c.set("user", { id: 0, username: "cli", role: "admin" });
|
|
814
|
+
c.set("user", { id: 0, username: "cli", role: "admin", user_type: "human" });
|
|
629
815
|
await next();
|
|
630
816
|
return;
|
|
631
817
|
}
|
|
@@ -642,37 +828,111 @@ var authMiddleware = createMiddleware(async (c, next) => {
|
|
|
642
828
|
});
|
|
643
829
|
|
|
644
830
|
// src/web/server.ts
|
|
645
|
-
import { existsSync as
|
|
831
|
+
import { existsSync as existsSync7 } from "fs";
|
|
646
832
|
import { readFile as readFile2, stat } from "fs/promises";
|
|
647
|
-
import { dirname as dirname3, extname, resolve as
|
|
833
|
+
import { dirname as dirname3, extname, resolve as resolve9 } from "path";
|
|
648
834
|
import { serve } from "@hono/node-server";
|
|
649
835
|
|
|
836
|
+
// src/lib/log-buffer.ts
|
|
837
|
+
var LogBuffer = class {
|
|
838
|
+
entries = [];
|
|
839
|
+
maxSize = 1e3;
|
|
840
|
+
subscribers = /* @__PURE__ */ new Set();
|
|
841
|
+
append(entry) {
|
|
842
|
+
this.entries.push(entry);
|
|
843
|
+
if (this.entries.length > this.maxSize) {
|
|
844
|
+
this.entries.shift();
|
|
845
|
+
}
|
|
846
|
+
for (const sub of this.subscribers) {
|
|
847
|
+
sub(entry);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
getEntries() {
|
|
851
|
+
return [...this.entries];
|
|
852
|
+
}
|
|
853
|
+
subscribe(fn) {
|
|
854
|
+
this.subscribers.add(fn);
|
|
855
|
+
return () => this.subscribers.delete(fn);
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
var logBuffer = new LogBuffer();
|
|
859
|
+
|
|
860
|
+
// src/lib/logger.ts
|
|
861
|
+
function write(level, msg, data) {
|
|
862
|
+
const entry = {
|
|
863
|
+
level,
|
|
864
|
+
msg,
|
|
865
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
866
|
+
...data ? { data } : {}
|
|
867
|
+
};
|
|
868
|
+
const line = JSON.stringify(entry);
|
|
869
|
+
process.stderr.write(`${line}
|
|
870
|
+
`);
|
|
871
|
+
logBuffer.append(entry);
|
|
872
|
+
}
|
|
873
|
+
var log = {
|
|
874
|
+
info: (msg, data) => write("info", msg, data),
|
|
875
|
+
warn: (msg, data) => write("warn", msg, data),
|
|
876
|
+
error: (msg, data) => write("error", msg, data)
|
|
877
|
+
};
|
|
878
|
+
var logger_default = log;
|
|
879
|
+
|
|
650
880
|
// src/web/app.ts
|
|
651
|
-
import { Hono as
|
|
881
|
+
import { Hono as Hono13 } from "hono";
|
|
652
882
|
import { bodyLimit } from "hono/body-limit";
|
|
653
883
|
import { csrf } from "hono/csrf";
|
|
654
884
|
import { HTTPException } from "hono/http-exception";
|
|
655
885
|
|
|
656
886
|
// src/web/routes/agents.ts
|
|
657
|
-
import { existsSync as
|
|
658
|
-
import { resolve as
|
|
887
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, rmSync } from "fs";
|
|
888
|
+
import { resolve as resolve5 } from "path";
|
|
659
889
|
import { and as and2, desc, eq as eq3 } from "drizzle-orm";
|
|
660
890
|
import { Hono } from "hono";
|
|
661
891
|
import { stream } from "hono/streaming";
|
|
662
892
|
|
|
663
|
-
// src/lib/
|
|
664
|
-
var
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
893
|
+
// src/lib/ndjson.ts
|
|
894
|
+
var MAX_BUFFER_SIZE = 1e6;
|
|
895
|
+
async function* readNdjson(body) {
|
|
896
|
+
const reader = body.getReader();
|
|
897
|
+
const decoder = new TextDecoder();
|
|
898
|
+
let buffer = "";
|
|
899
|
+
try {
|
|
900
|
+
while (true) {
|
|
901
|
+
const { done, value } = await reader.read();
|
|
902
|
+
if (done) break;
|
|
903
|
+
buffer += decoder.decode(value, { stream: true });
|
|
904
|
+
if (buffer.length > MAX_BUFFER_SIZE) {
|
|
905
|
+
logger_default.warn("ndjson: buffer exceeded 1MB, resetting");
|
|
906
|
+
buffer = "";
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
const lines = buffer.split("\n");
|
|
910
|
+
buffer = lines.pop() || "";
|
|
911
|
+
for (const line of lines) {
|
|
912
|
+
if (!line.trim()) continue;
|
|
913
|
+
try {
|
|
914
|
+
yield JSON.parse(line);
|
|
915
|
+
} catch {
|
|
916
|
+
logger_default.warn("ndjson: skipping invalid line", { line: line.slice(0, 100) });
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
if (buffer.trim()) {
|
|
921
|
+
try {
|
|
922
|
+
yield JSON.parse(buffer);
|
|
923
|
+
} catch {
|
|
924
|
+
logger_default.warn("ndjson: skipping invalid line", { line: buffer.slice(0, 100) });
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
} finally {
|
|
928
|
+
reader.releaseLock();
|
|
929
|
+
}
|
|
930
|
+
}
|
|
671
931
|
|
|
672
932
|
// src/web/routes/agents.ts
|
|
673
933
|
function getDaemonPort() {
|
|
674
934
|
try {
|
|
675
|
-
const data = JSON.parse(
|
|
935
|
+
const data = JSON.parse(readFileSync3(resolve5(voluteHome(), "daemon.json"), "utf-8"));
|
|
676
936
|
return data.port;
|
|
677
937
|
} catch {
|
|
678
938
|
return void 0;
|
|
@@ -687,10 +947,10 @@ async function getAgentStatus(name, port) {
|
|
|
687
947
|
}
|
|
688
948
|
const channels = [];
|
|
689
949
|
channels.push({
|
|
690
|
-
name: CHANNELS.
|
|
691
|
-
displayName: CHANNELS.
|
|
950
|
+
name: CHANNELS.volute.name,
|
|
951
|
+
displayName: CHANNELS.volute.displayName,
|
|
692
952
|
status: status === "running" ? "connected" : "disconnected",
|
|
693
|
-
showToolCalls: CHANNELS.
|
|
953
|
+
showToolCalls: CHANNELS.volute.showToolCalls
|
|
694
954
|
});
|
|
695
955
|
const connectorStatuses = getConnectorManager().getConnectorStatus(name);
|
|
696
956
|
for (const cs of connectorStatuses) {
|
|
@@ -717,7 +977,7 @@ var app = new Hono().get("/", async (c) => {
|
|
|
717
977
|
const name = c.req.param("name");
|
|
718
978
|
const entry = findAgent(name);
|
|
719
979
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
720
|
-
if (!
|
|
980
|
+
if (!existsSync4(agentDir(name))) return c.json({ error: "Agent directory missing" }, 404);
|
|
721
981
|
const { status, channels } = await getAgentStatus(name, entry.port);
|
|
722
982
|
const variants = readVariants(name);
|
|
723
983
|
const manager = getAgentManager();
|
|
@@ -743,7 +1003,7 @@ var app = new Hono().get("/", async (c) => {
|
|
|
743
1003
|
if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
|
|
744
1004
|
} else {
|
|
745
1005
|
const dir = agentDir(baseName);
|
|
746
|
-
if (!
|
|
1006
|
+
if (!existsSync4(dir)) return c.json({ error: "Agent directory missing" }, 404);
|
|
747
1007
|
}
|
|
748
1008
|
const manager = getAgentManager();
|
|
749
1009
|
if (manager.isRunning(name)) {
|
|
@@ -770,7 +1030,7 @@ var app = new Hono().get("/", async (c) => {
|
|
|
770
1030
|
if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
|
|
771
1031
|
} else {
|
|
772
1032
|
const dir = agentDir(baseName);
|
|
773
|
-
if (!
|
|
1033
|
+
if (!existsSync4(dir)) return c.json({ error: "Agent directory missing" }, 404);
|
|
774
1034
|
}
|
|
775
1035
|
const manager = getAgentManager();
|
|
776
1036
|
const connectorManager = getConnectorManager();
|
|
@@ -825,7 +1085,7 @@ var app = new Hono().get("/", async (c) => {
|
|
|
825
1085
|
}
|
|
826
1086
|
removeAllVariants(name);
|
|
827
1087
|
removeAgent(name);
|
|
828
|
-
if (force &&
|
|
1088
|
+
if (force && existsSync4(dir)) {
|
|
829
1089
|
rmSync(dir, { recursive: true, force: true });
|
|
830
1090
|
}
|
|
831
1091
|
return c.json({ ok: true });
|
|
@@ -847,14 +1107,22 @@ var app = new Hono().get("/", async (c) => {
|
|
|
847
1107
|
let parsed = null;
|
|
848
1108
|
try {
|
|
849
1109
|
parsed = JSON.parse(body);
|
|
850
|
-
} catch {
|
|
1110
|
+
} catch (err) {
|
|
1111
|
+
console.error(`[daemon] failed to parse message body for ${baseName}:`, err);
|
|
851
1112
|
}
|
|
852
1113
|
const channel = parsed?.channel ?? "unknown";
|
|
853
1114
|
const db2 = await getDb();
|
|
854
1115
|
if (parsed) {
|
|
855
1116
|
try {
|
|
856
1117
|
const sender = parsed.sender ?? null;
|
|
857
|
-
|
|
1118
|
+
let content;
|
|
1119
|
+
if (typeof parsed.content === "string") {
|
|
1120
|
+
content = parsed.content;
|
|
1121
|
+
} else if (Array.isArray(parsed.content)) {
|
|
1122
|
+
content = parsed.content.filter((p) => p.type === "text" && p.text).map((p) => p.text).join("\n");
|
|
1123
|
+
} else {
|
|
1124
|
+
content = JSON.stringify(parsed.content);
|
|
1125
|
+
}
|
|
858
1126
|
await db2.insert(agentMessages).values({
|
|
859
1127
|
agent: baseName,
|
|
860
1128
|
channel,
|
|
@@ -866,11 +1134,17 @@ var app = new Hono().get("/", async (c) => {
|
|
|
866
1134
|
console.error(`[daemon] failed to persist inbound message for ${baseName}:`, err);
|
|
867
1135
|
}
|
|
868
1136
|
}
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
1137
|
+
let res;
|
|
1138
|
+
try {
|
|
1139
|
+
res = await fetch(`http://127.0.0.1:${port}/message`, {
|
|
1140
|
+
method: "POST",
|
|
1141
|
+
headers: { "Content-Type": "application/json" },
|
|
1142
|
+
body
|
|
1143
|
+
});
|
|
1144
|
+
} catch (err) {
|
|
1145
|
+
console.error(`[daemon] agent ${name} unreachable on port ${port}:`, err);
|
|
1146
|
+
return c.json({ error: "Agent is not reachable" }, 502);
|
|
1147
|
+
}
|
|
874
1148
|
if (!res.ok) {
|
|
875
1149
|
return c.json({ error: `Agent responded with ${res.status}` }, res.status);
|
|
876
1150
|
}
|
|
@@ -878,57 +1152,39 @@ var app = new Hono().get("/", async (c) => {
|
|
|
878
1152
|
return c.json({ error: "No response body from agent" }, 502);
|
|
879
1153
|
}
|
|
880
1154
|
c.header("Content-Type", "application/x-ndjson");
|
|
1155
|
+
const encoder = new TextEncoder();
|
|
881
1156
|
return stream(c, async (s) => {
|
|
882
|
-
const reader = res.body.getReader();
|
|
883
|
-
const decoder = new TextDecoder();
|
|
884
|
-
let buffer = "";
|
|
885
1157
|
const textParts = [];
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
for (const line of lines) {
|
|
895
|
-
if (!line.trim()) continue;
|
|
896
|
-
try {
|
|
897
|
-
const event = JSON.parse(line);
|
|
898
|
-
if (event.type === "text") {
|
|
899
|
-
textParts.push(event.content);
|
|
900
|
-
}
|
|
901
|
-
} catch {
|
|
902
|
-
console.warn(`[daemon] malformed NDJSON line from ${baseName}`);
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
if (buffer.trim()) {
|
|
907
|
-
try {
|
|
908
|
-
const event = JSON.parse(buffer);
|
|
909
|
-
if (event.type === "text") {
|
|
910
|
-
textParts.push(event.content);
|
|
911
|
-
}
|
|
912
|
-
} catch {
|
|
913
|
-
console.warn(`[daemon] malformed NDJSON trailing data from ${baseName}`);
|
|
914
|
-
}
|
|
1158
|
+
const toolParts = [];
|
|
1159
|
+
for await (const event of readNdjson(res.body)) {
|
|
1160
|
+
await s.write(encoder.encode(`${JSON.stringify(event)}
|
|
1161
|
+
`));
|
|
1162
|
+
const part = collectPart(event);
|
|
1163
|
+
if (part != null) {
|
|
1164
|
+
if (event.type === "tool_use") toolParts.push(part);
|
|
1165
|
+
else textParts.push(part);
|
|
915
1166
|
}
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
}
|
|
1167
|
+
}
|
|
1168
|
+
const content = [textParts.join(""), ...toolParts].filter(Boolean).join("\n");
|
|
1169
|
+
if (content) {
|
|
1170
|
+
try {
|
|
1171
|
+
await db2.insert(agentMessages).values({
|
|
1172
|
+
agent: baseName,
|
|
1173
|
+
channel,
|
|
1174
|
+
role: "assistant",
|
|
1175
|
+
sender: baseName,
|
|
1176
|
+
content
|
|
1177
|
+
});
|
|
1178
|
+
} catch (err) {
|
|
1179
|
+
console.error(`[daemon] failed to persist assistant response for ${baseName}:`, err);
|
|
927
1180
|
}
|
|
928
|
-
} finally {
|
|
929
|
-
reader.releaseLock();
|
|
930
1181
|
}
|
|
931
1182
|
});
|
|
1183
|
+
}).get("/:name/history/channels", async (c) => {
|
|
1184
|
+
const name = c.req.param("name");
|
|
1185
|
+
const db2 = await getDb();
|
|
1186
|
+
const rows = await db2.selectDistinct({ channel: agentMessages.channel }).from(agentMessages).where(eq3(agentMessages.agent, name));
|
|
1187
|
+
return c.json(rows.map((r) => r.channel));
|
|
932
1188
|
}).get("/:name/history", async (c) => {
|
|
933
1189
|
const name = c.req.param("name");
|
|
934
1190
|
const channel = c.req.query("channel");
|
|
@@ -956,6 +1212,14 @@ var credentialsSchema = z.object({
|
|
|
956
1212
|
var admin = new Hono2().use(authMiddleware).get("/users", async (c) => {
|
|
957
1213
|
const user = c.get("user");
|
|
958
1214
|
if (user.role !== "admin") return c.json({ error: "Forbidden" }, 403);
|
|
1215
|
+
const agents = readRegistry();
|
|
1216
|
+
for (const agent of agents) {
|
|
1217
|
+
await getOrCreateAgentUser(agent.name);
|
|
1218
|
+
}
|
|
1219
|
+
const type = c.req.query("type");
|
|
1220
|
+
if (type === "human" || type === "agent") {
|
|
1221
|
+
return c.json(await listUsersByType(type));
|
|
1222
|
+
}
|
|
959
1223
|
return c.json(await listUsers());
|
|
960
1224
|
}).get("/users/pending", async (c) => {
|
|
961
1225
|
const user = c.get("user");
|
|
@@ -1008,6 +1272,8 @@ var app2 = new Hono2().post("/register", zValidator("json", credentialsSchema),
|
|
|
1008
1272
|
var auth_default = app2;
|
|
1009
1273
|
|
|
1010
1274
|
// src/web/routes/chat.ts
|
|
1275
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
1276
|
+
import { resolve as resolve6 } from "path";
|
|
1011
1277
|
import { zValidator as zValidator2 } from "@hono/zod-validator";
|
|
1012
1278
|
import { Hono as Hono3 } from "hono";
|
|
1013
1279
|
import { streamSSE } from "hono/streaming";
|
|
@@ -1015,7 +1281,7 @@ import { z as z2 } from "zod";
|
|
|
1015
1281
|
|
|
1016
1282
|
// src/lib/conversations.ts
|
|
1017
1283
|
import { randomUUID } from "crypto";
|
|
1018
|
-
import { and as and3, desc as desc2, eq as eq4, isNull, sql as sql2 } from "drizzle-orm";
|
|
1284
|
+
import { and as and3, desc as desc2, eq as eq4, inArray, isNull, sql as sql2 } from "drizzle-orm";
|
|
1019
1285
|
async function createConversation(agentName, channel, opts) {
|
|
1020
1286
|
const db2 = await getDb();
|
|
1021
1287
|
const id = randomUUID();
|
|
@@ -1026,6 +1292,15 @@ async function createConversation(agentName, channel, opts) {
|
|
|
1026
1292
|
user_id: opts?.userId ?? null,
|
|
1027
1293
|
title: opts?.title ?? null
|
|
1028
1294
|
});
|
|
1295
|
+
if (opts?.participantIds && opts.participantIds.length > 0) {
|
|
1296
|
+
await db2.insert(conversationParticipants).values(
|
|
1297
|
+
opts.participantIds.map((uid, i) => ({
|
|
1298
|
+
conversation_id: id,
|
|
1299
|
+
user_id: uid,
|
|
1300
|
+
role: i === 0 ? "owner" : "member"
|
|
1301
|
+
}))
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1029
1304
|
return {
|
|
1030
1305
|
id,
|
|
1031
1306
|
agent_name: agentName,
|
|
@@ -1036,24 +1311,44 @@ async function createConversation(agentName, channel, opts) {
|
|
|
1036
1311
|
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1037
1312
|
};
|
|
1038
1313
|
}
|
|
1039
|
-
async function
|
|
1314
|
+
async function getParticipants(conversationId) {
|
|
1040
1315
|
const db2 = await getDb();
|
|
1041
|
-
const
|
|
1042
|
-
|
|
1316
|
+
const rows = await db2.select({
|
|
1317
|
+
userId: conversationParticipants.user_id,
|
|
1318
|
+
username: users.username,
|
|
1319
|
+
userType: users.user_type,
|
|
1320
|
+
role: conversationParticipants.role
|
|
1321
|
+
}).from(conversationParticipants).innerJoin(users, eq4(conversationParticipants.user_id, users.id)).where(eq4(conversationParticipants.conversation_id, conversationId)).all();
|
|
1322
|
+
return rows;
|
|
1323
|
+
}
|
|
1324
|
+
async function isParticipant(conversationId, userId) {
|
|
1325
|
+
const db2 = await getDb();
|
|
1326
|
+
const row = await db2.select({ user_id: conversationParticipants.user_id }).from(conversationParticipants).where(
|
|
1327
|
+
and3(
|
|
1328
|
+
eq4(conversationParticipants.conversation_id, conversationId),
|
|
1329
|
+
eq4(conversationParticipants.user_id, userId)
|
|
1330
|
+
)
|
|
1331
|
+
).get();
|
|
1332
|
+
return row != null;
|
|
1333
|
+
}
|
|
1334
|
+
async function listConversationsForUser(userId) {
|
|
1335
|
+
const db2 = await getDb();
|
|
1336
|
+
const participantRows = await db2.select({ conversation_id: conversationParticipants.conversation_id }).from(conversationParticipants).where(eq4(conversationParticipants.user_id, userId)).all();
|
|
1337
|
+
if (participantRows.length === 0) return [];
|
|
1338
|
+
const convIds = participantRows.map((r) => r.conversation_id);
|
|
1339
|
+
return db2.select().from(conversations).where(inArray(conversations.id, convIds)).orderBy(desc2(conversations.updated_at)).all();
|
|
1340
|
+
}
|
|
1341
|
+
async function isParticipantOrOwner(conversationId, userId) {
|
|
1342
|
+
if (await isParticipant(conversationId, userId)) return true;
|
|
1343
|
+
const db2 = await getDb();
|
|
1344
|
+
const row = await db2.select().from(conversations).where(and3(eq4(conversations.id, conversationId), eq4(conversations.user_id, userId))).get();
|
|
1345
|
+
return row != null;
|
|
1043
1346
|
}
|
|
1044
1347
|
async function deleteConversationForUser(id, userId) {
|
|
1045
|
-
|
|
1046
|
-
if (!conv) return false;
|
|
1348
|
+
if (!await isParticipantOrOwner(id, userId)) return false;
|
|
1047
1349
|
await deleteConversation(id);
|
|
1048
1350
|
return true;
|
|
1049
1351
|
}
|
|
1050
|
-
async function listConversations(agentName, opts) {
|
|
1051
|
-
const db2 = await getDb();
|
|
1052
|
-
if (opts?.userId != null) {
|
|
1053
|
-
return db2.select().from(conversations).where(and3(eq4(conversations.agent_name, agentName), eq4(conversations.user_id, opts.userId))).orderBy(desc2(conversations.updated_at)).all();
|
|
1054
|
-
}
|
|
1055
|
-
return db2.select().from(conversations).where(eq4(conversations.agent_name, agentName)).orderBy(desc2(conversations.updated_at)).all();
|
|
1056
|
-
}
|
|
1057
1352
|
async function addMessage(conversationId, role, senderName, content) {
|
|
1058
1353
|
const db2 = await getDb();
|
|
1059
1354
|
const serialized = JSON.stringify(content);
|
|
@@ -1089,6 +1384,34 @@ async function getMessages(conversationId) {
|
|
|
1089
1384
|
return { ...row, content };
|
|
1090
1385
|
});
|
|
1091
1386
|
}
|
|
1387
|
+
async function listConversationsWithParticipants(userId) {
|
|
1388
|
+
const convs = await listConversationsForUser(userId);
|
|
1389
|
+
if (convs.length === 0) return [];
|
|
1390
|
+
const db2 = await getDb();
|
|
1391
|
+
const convIds = convs.map((c) => c.id);
|
|
1392
|
+
const rows = await db2.select({
|
|
1393
|
+
conversationId: conversationParticipants.conversation_id,
|
|
1394
|
+
userId: users.id,
|
|
1395
|
+
username: users.username,
|
|
1396
|
+
userType: users.user_type,
|
|
1397
|
+
role: conversationParticipants.role
|
|
1398
|
+
}).from(conversationParticipants).innerJoin(users, eq4(conversationParticipants.user_id, users.id)).where(inArray(conversationParticipants.conversation_id, convIds));
|
|
1399
|
+
const byConv = /* @__PURE__ */ new Map();
|
|
1400
|
+
for (const r of rows) {
|
|
1401
|
+
let arr = byConv.get(r.conversationId);
|
|
1402
|
+
if (!arr) {
|
|
1403
|
+
arr = [];
|
|
1404
|
+
byConv.set(r.conversationId, arr);
|
|
1405
|
+
}
|
|
1406
|
+
arr.push({
|
|
1407
|
+
userId: r.userId,
|
|
1408
|
+
username: r.username,
|
|
1409
|
+
userType: r.userType,
|
|
1410
|
+
role: r.role
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
return convs.map((c) => ({ ...c, participants: byConv.get(c.id) ?? [] }));
|
|
1414
|
+
}
|
|
1092
1415
|
async function deleteConversation(id) {
|
|
1093
1416
|
const db2 = await getDb();
|
|
1094
1417
|
await db2.delete(conversations).where(eq4(conversations.id, id));
|
|
@@ -1098,6 +1421,7 @@ async function deleteConversation(id) {
|
|
|
1098
1421
|
var chatSchema = z2.object({
|
|
1099
1422
|
message: z2.string().optional(),
|
|
1100
1423
|
conversationId: z2.string().optional(),
|
|
1424
|
+
sender: z2.string().optional(),
|
|
1101
1425
|
images: z2.array(
|
|
1102
1426
|
z2.object({
|
|
1103
1427
|
media_type: z2.string(),
|
|
@@ -1105,18 +1429,59 @@ var chatSchema = z2.object({
|
|
|
1105
1429
|
})
|
|
1106
1430
|
).optional()
|
|
1107
1431
|
});
|
|
1432
|
+
function getDaemonUrl() {
|
|
1433
|
+
const data = JSON.parse(readFileSync4(resolve6(voluteHome(), "daemon.json"), "utf-8"));
|
|
1434
|
+
return `http://127.0.0.1:${data.port}`;
|
|
1435
|
+
}
|
|
1436
|
+
function daemonFetchInternal(path, body) {
|
|
1437
|
+
const daemonUrl = getDaemonUrl();
|
|
1438
|
+
const token = process.env.VOLUTE_DAEMON_TOKEN;
|
|
1439
|
+
const headers = {
|
|
1440
|
+
"Content-Type": "application/json",
|
|
1441
|
+
Origin: daemonUrl
|
|
1442
|
+
};
|
|
1443
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
1444
|
+
return fetch(`${daemonUrl}${path}`, { method: "POST", headers, body });
|
|
1445
|
+
}
|
|
1446
|
+
function accumulateEvent(content, event) {
|
|
1447
|
+
if (event.type === "text") {
|
|
1448
|
+
const last = content[content.length - 1];
|
|
1449
|
+
if (last && last.type === "text") last.text += event.content;
|
|
1450
|
+
else content.push({ type: "text", text: event.content });
|
|
1451
|
+
} else if (event.type === "tool_use") {
|
|
1452
|
+
content.push({ type: "tool_use", name: event.name, input: event.input });
|
|
1453
|
+
} else if (event.type === "tool_result") {
|
|
1454
|
+
content.push({
|
|
1455
|
+
type: "tool_result",
|
|
1456
|
+
output: event.output,
|
|
1457
|
+
...event.is_error ? { is_error: true } : {}
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
async function consumeAndPersist(res, conversationId, agentName) {
|
|
1462
|
+
if (!res.body) {
|
|
1463
|
+
console.warn(`[chat] no response body from ${agentName}`);
|
|
1464
|
+
return [];
|
|
1465
|
+
}
|
|
1466
|
+
const assistantContent = [];
|
|
1467
|
+
for await (const event of readNdjson(res.body)) {
|
|
1468
|
+
accumulateEvent(assistantContent, event);
|
|
1469
|
+
if (event.type === "done") break;
|
|
1470
|
+
}
|
|
1471
|
+
if (assistantContent.length === 0) return [];
|
|
1472
|
+
try {
|
|
1473
|
+
await addMessage(conversationId, "assistant", agentName, assistantContent);
|
|
1474
|
+
} catch (err) {
|
|
1475
|
+
console.error(`[chat] failed to persist conversation message from ${agentName}:`, err);
|
|
1476
|
+
}
|
|
1477
|
+
return assistantContent;
|
|
1478
|
+
}
|
|
1108
1479
|
var app3 = new Hono3().post("/:name/chat", zValidator2("json", chatSchema), async (c) => {
|
|
1109
1480
|
const name = c.req.param("name");
|
|
1110
|
-
const [baseName
|
|
1481
|
+
const [baseName] = name.split("@", 2);
|
|
1111
1482
|
const entry = findAgent(baseName);
|
|
1112
1483
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1113
|
-
|
|
1114
|
-
if (variantName) {
|
|
1115
|
-
const variant = findVariant(baseName, variantName);
|
|
1116
|
-
if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
|
|
1117
|
-
port = variant.port;
|
|
1118
|
-
}
|
|
1119
|
-
const { getAgentManager: getAgentManager2 } = await import("./agent-manager-AUCKMGPR.js");
|
|
1484
|
+
const { getAgentManager: getAgentManager2 } = await import("./agent-manager-PXBKA2GK.js");
|
|
1120
1485
|
if (!getAgentManager2().isRunning(name)) {
|
|
1121
1486
|
return c.json({ error: "Agent is not running" }, 409);
|
|
1122
1487
|
}
|
|
@@ -1125,18 +1490,34 @@ var app3 = new Hono3().post("/:name/chat", zValidator2("json", chatSchema), asyn
|
|
|
1125
1490
|
return c.json({ error: "message or images required" }, 400);
|
|
1126
1491
|
}
|
|
1127
1492
|
const user = c.get("user");
|
|
1493
|
+
const agentUser = await getOrCreateAgentUser(baseName);
|
|
1494
|
+
const senderName = user.id === 0 && body.sender ? body.sender : user.username;
|
|
1128
1495
|
let conversationId = body.conversationId;
|
|
1129
1496
|
if (conversationId) {
|
|
1130
|
-
|
|
1131
|
-
|
|
1497
|
+
if (user.id !== 0 && !await isParticipantOrOwner(conversationId, user.id)) {
|
|
1498
|
+
return c.json({ error: "Conversation not found" }, 404);
|
|
1499
|
+
}
|
|
1132
1500
|
} else {
|
|
1133
1501
|
const title = body.message ? body.message.slice(0, 80) : "Image message";
|
|
1134
|
-
const
|
|
1135
|
-
|
|
1136
|
-
|
|
1502
|
+
const participantIds = [];
|
|
1503
|
+
if (user.id !== 0) {
|
|
1504
|
+
participantIds.push(user.id);
|
|
1505
|
+
} else if (body.sender) {
|
|
1506
|
+
const senderAgent = findAgent(body.sender);
|
|
1507
|
+
if (senderAgent) {
|
|
1508
|
+
const senderAgentUser = await getOrCreateAgentUser(body.sender);
|
|
1509
|
+
participantIds.push(senderAgentUser.id);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
participantIds.push(agentUser.id);
|
|
1513
|
+
const conv = await createConversation(baseName, "volute", {
|
|
1514
|
+
userId: user.id !== 0 ? user.id : void 0,
|
|
1515
|
+
title,
|
|
1516
|
+
participantIds
|
|
1137
1517
|
});
|
|
1138
1518
|
conversationId = conv.id;
|
|
1139
1519
|
}
|
|
1520
|
+
const channel = `volute:${conversationId}`;
|
|
1140
1521
|
const contentBlocks = [];
|
|
1141
1522
|
if (body.message) {
|
|
1142
1523
|
contentBlocks.push({ type: "text", text: body.message });
|
|
@@ -1146,73 +1527,87 @@ var app3 = new Hono3().post("/:name/chat", zValidator2("json", chatSchema), asyn
|
|
|
1146
1527
|
contentBlocks.push({ type: "image", media_type: img.media_type, data: img.data });
|
|
1147
1528
|
}
|
|
1148
1529
|
}
|
|
1149
|
-
await addMessage(conversationId, "user",
|
|
1150
|
-
const
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
});
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1530
|
+
await addMessage(conversationId, "user", senderName, contentBlocks);
|
|
1531
|
+
const participants = await getParticipants(conversationId);
|
|
1532
|
+
const agentParticipants = participants.filter((p) => p.userType === "agent");
|
|
1533
|
+
const participantNames = participants.map((p) => p.username);
|
|
1534
|
+
const manager = getAgentManager2();
|
|
1535
|
+
const runningAgents = agentParticipants.map((ap) => {
|
|
1536
|
+
const agentKey = ap.username === baseName ? name : ap.username;
|
|
1537
|
+
return manager.isRunning(agentKey) ? ap.username : null;
|
|
1538
|
+
}).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
|
+
const isDM = participants.length === 2;
|
|
1543
|
+
const payload = JSON.stringify({
|
|
1544
|
+
content: contentBlocks,
|
|
1545
|
+
channel,
|
|
1546
|
+
sender: senderName,
|
|
1547
|
+
participants: participantNames,
|
|
1548
|
+
participantCount: participants.length,
|
|
1549
|
+
isDM
|
|
1166
1550
|
});
|
|
1167
|
-
|
|
1168
|
-
|
|
1551
|
+
const responses = [];
|
|
1552
|
+
for (const agentName of runningAgents) {
|
|
1553
|
+
const targetName = agentName === baseName ? name : agentName;
|
|
1554
|
+
try {
|
|
1555
|
+
const res = await daemonFetchInternal(
|
|
1556
|
+
`/api/agents/${encodeURIComponent(targetName)}/message`,
|
|
1557
|
+
payload
|
|
1558
|
+
);
|
|
1559
|
+
if (res.ok && res.body) {
|
|
1560
|
+
responses.push({ name: agentName, res });
|
|
1561
|
+
} else {
|
|
1562
|
+
const errorBody = await res.text().catch(() => "");
|
|
1563
|
+
console.error(
|
|
1564
|
+
`[chat] agent ${agentName} responded with ${res.status}: ${errorBody.slice(0, 500)}`
|
|
1565
|
+
);
|
|
1566
|
+
}
|
|
1567
|
+
} catch (err) {
|
|
1568
|
+
console.error(`[chat] agent ${agentName} unreachable via daemon:`, err);
|
|
1569
|
+
}
|
|
1169
1570
|
}
|
|
1170
|
-
if (
|
|
1171
|
-
return c.json({ error: "No
|
|
1571
|
+
if (responses.length === 0) {
|
|
1572
|
+
return c.json({ error: "No agents reachable" }, 502);
|
|
1172
1573
|
}
|
|
1574
|
+
const primary = responses[0];
|
|
1575
|
+
const secondary = responses.slice(1);
|
|
1576
|
+
const secondaryPromises = secondary.map((s) => consumeAndPersist(s.res, conversationId, s.name));
|
|
1173
1577
|
return streamSSE(c, async (stream2) => {
|
|
1174
1578
|
await stream2.writeSSE({
|
|
1175
|
-
data: JSON.stringify({ type: "meta", conversationId })
|
|
1579
|
+
data: JSON.stringify({ type: "meta", conversationId, senderName: primary.name })
|
|
1176
1580
|
});
|
|
1177
1581
|
const assistantContent = [];
|
|
1178
|
-
|
|
1179
|
-
await
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
if (
|
|
1183
|
-
last.text += event.content;
|
|
1184
|
-
} else {
|
|
1185
|
-
assistantContent.push({ type: "text", text: event.content });
|
|
1186
|
-
}
|
|
1187
|
-
} else if (event.type === "tool_use") {
|
|
1188
|
-
assistantContent.push({
|
|
1189
|
-
type: "tool_use",
|
|
1190
|
-
name: event.name,
|
|
1191
|
-
input: event.input
|
|
1192
|
-
});
|
|
1193
|
-
} else if (event.type === "tool_result") {
|
|
1194
|
-
assistantContent.push({
|
|
1195
|
-
type: "tool_result",
|
|
1196
|
-
output: event.output,
|
|
1197
|
-
...event.is_error ? { is_error: true } : {}
|
|
1198
|
-
});
|
|
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;
|
|
1199
1587
|
}
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
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
|
+
);
|
|
1214
1608
|
}
|
|
1215
1609
|
}
|
|
1610
|
+
await stream2.writeSSE({ data: JSON.stringify({ type: "sync" }) });
|
|
1216
1611
|
});
|
|
1217
1612
|
});
|
|
1218
1613
|
var chat_default = app3;
|
|
@@ -1243,13 +1638,24 @@ var app4 = new Hono4().get("/:name/connectors", (c) => {
|
|
|
1243
1638
|
const entry = findAgent(name);
|
|
1244
1639
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1245
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
|
+
}
|
|
1246
1653
|
const config = readVoluteConfig(dir) ?? {};
|
|
1247
1654
|
const connectors = config.connectors ?? [];
|
|
1248
1655
|
if (!connectors.includes(type)) {
|
|
1249
1656
|
config.connectors = [...connectors, type];
|
|
1250
1657
|
writeVoluteConfig(dir, config);
|
|
1251
1658
|
}
|
|
1252
|
-
const manager = getConnectorManager();
|
|
1253
1659
|
try {
|
|
1254
1660
|
await manager.startConnector(name, dir, entry.port, type);
|
|
1255
1661
|
return c.json({ ok: true });
|
|
@@ -1278,19 +1684,79 @@ var app4 = new Hono4().get("/:name/connectors", (c) => {
|
|
|
1278
1684
|
var connectors_default = app4;
|
|
1279
1685
|
|
|
1280
1686
|
// src/web/routes/conversations.ts
|
|
1687
|
+
import { zValidator as zValidator3 } from "@hono/zod-validator";
|
|
1281
1688
|
import { Hono as Hono5 } from "hono";
|
|
1689
|
+
import { z as z3 } from "zod";
|
|
1690
|
+
var createConvSchema = z3.object({
|
|
1691
|
+
title: z3.string().optional(),
|
|
1692
|
+
participantIds: z3.array(z3.number()).optional(),
|
|
1693
|
+
participantNames: z3.array(z3.string()).optional()
|
|
1694
|
+
});
|
|
1282
1695
|
var app5 = new Hono5().get("/:name/conversations", async (c) => {
|
|
1283
1696
|
const name = c.req.param("name");
|
|
1284
1697
|
const user = c.get("user");
|
|
1285
|
-
|
|
1698
|
+
let lookupId = user.id;
|
|
1699
|
+
if (user.id === 0) {
|
|
1700
|
+
const agentUser = await getOrCreateAgentUser(name);
|
|
1701
|
+
lookupId = agentUser.id;
|
|
1702
|
+
}
|
|
1703
|
+
const all = await listConversationsForUser(lookupId);
|
|
1704
|
+
const convs = all.filter((c2) => c2.agent_name === name);
|
|
1286
1705
|
return c.json(convs);
|
|
1706
|
+
}).post("/:name/conversations", zValidator3("json", createConvSchema), async (c) => {
|
|
1707
|
+
const name = c.req.param("name");
|
|
1708
|
+
const user = c.get("user");
|
|
1709
|
+
const body = c.req.valid("json");
|
|
1710
|
+
if (!body.participantIds?.length && !body.participantNames?.length) {
|
|
1711
|
+
return c.json({ error: "participantIds or participantNames required" }, 400);
|
|
1712
|
+
}
|
|
1713
|
+
const agentUser = await getOrCreateAgentUser(name);
|
|
1714
|
+
const participantSet = /* @__PURE__ */ new Set();
|
|
1715
|
+
if (user.id !== 0) participantSet.add(user.id);
|
|
1716
|
+
participantSet.add(agentUser.id);
|
|
1717
|
+
for (const id of body.participantIds ?? []) participantSet.add(id);
|
|
1718
|
+
if (body.participantNames) {
|
|
1719
|
+
for (const pname of body.participantNames) {
|
|
1720
|
+
const existing = await getUserByUsername(pname);
|
|
1721
|
+
if (existing) {
|
|
1722
|
+
participantSet.add(existing.id);
|
|
1723
|
+
continue;
|
|
1724
|
+
}
|
|
1725
|
+
if (findAgent(pname)) {
|
|
1726
|
+
const au = await getOrCreateAgentUser(pname);
|
|
1727
|
+
participantSet.add(au.id);
|
|
1728
|
+
continue;
|
|
1729
|
+
}
|
|
1730
|
+
return c.json({ error: `User not found: ${pname}` }, 400);
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
for (const id of participantSet) {
|
|
1734
|
+
if (id === user.id || id === agentUser.id) continue;
|
|
1735
|
+
const u = await getUser(id);
|
|
1736
|
+
if (!u) return c.json({ error: `User ${id} not found` }, 400);
|
|
1737
|
+
}
|
|
1738
|
+
const conv = await createConversation(name, "volute", {
|
|
1739
|
+
userId: user.id !== 0 ? user.id : void 0,
|
|
1740
|
+
title: body.title,
|
|
1741
|
+
participantIds: [...participantSet]
|
|
1742
|
+
});
|
|
1743
|
+
return c.json(conv, 201);
|
|
1287
1744
|
}).get("/:name/conversations/:id/messages", async (c) => {
|
|
1288
1745
|
const id = c.req.param("id");
|
|
1289
1746
|
const user = c.get("user");
|
|
1290
|
-
|
|
1291
|
-
|
|
1747
|
+
if (user.id !== 0 && !await isParticipantOrOwner(id, user.id)) {
|
|
1748
|
+
return c.json({ error: "Conversation not found" }, 404);
|
|
1749
|
+
}
|
|
1292
1750
|
const msgs = await getMessages(id);
|
|
1293
1751
|
return c.json(msgs);
|
|
1752
|
+
}).get("/:name/conversations/:id/participants", async (c) => {
|
|
1753
|
+
const id = c.req.param("id");
|
|
1754
|
+
const user = c.get("user");
|
|
1755
|
+
if (!await isParticipantOrOwner(id, user.id)) {
|
|
1756
|
+
return c.json({ error: "Conversation not found" }, 404);
|
|
1757
|
+
}
|
|
1758
|
+
const participants = await getParticipants(id);
|
|
1759
|
+
return c.json(participants);
|
|
1294
1760
|
}).delete("/:name/conversations/:id", async (c) => {
|
|
1295
1761
|
const id = c.req.param("id");
|
|
1296
1762
|
const user = c.get("user");
|
|
@@ -1301,21 +1767,21 @@ var app5 = new Hono5().get("/:name/conversations", async (c) => {
|
|
|
1301
1767
|
var conversations_default = app5;
|
|
1302
1768
|
|
|
1303
1769
|
// src/web/routes/files.ts
|
|
1304
|
-
import { existsSync as
|
|
1770
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1305
1771
|
import { readdir, readFile, writeFile } from "fs/promises";
|
|
1306
|
-
import { resolve as
|
|
1307
|
-
import { zValidator as
|
|
1772
|
+
import { resolve as resolve7 } from "path";
|
|
1773
|
+
import { zValidator as zValidator4 } from "@hono/zod-validator";
|
|
1308
1774
|
import { Hono as Hono6 } from "hono";
|
|
1309
|
-
import { z as
|
|
1775
|
+
import { z as z4 } from "zod";
|
|
1310
1776
|
var ALLOWED_FILES = /* @__PURE__ */ new Set(["SOUL.md", "MEMORY.md", "CLAUDE.md", "VOLUTE.md"]);
|
|
1311
|
-
var saveFileSchema =
|
|
1777
|
+
var saveFileSchema = z4.object({ content: z4.string() });
|
|
1312
1778
|
var app6 = new Hono6().get("/:name/files", async (c) => {
|
|
1313
1779
|
const name = c.req.param("name");
|
|
1314
1780
|
const entry = findAgent(name);
|
|
1315
1781
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1316
1782
|
const dir = agentDir(name);
|
|
1317
|
-
const homeDir =
|
|
1318
|
-
if (!
|
|
1783
|
+
const homeDir = resolve7(dir, "home");
|
|
1784
|
+
if (!existsSync5(homeDir)) return c.json({ error: "Home directory missing" }, 404);
|
|
1319
1785
|
const allFiles = await readdir(homeDir);
|
|
1320
1786
|
const files = allFiles.filter((f) => f.endsWith(".md") && ALLOWED_FILES.has(f));
|
|
1321
1787
|
return c.json(files);
|
|
@@ -1328,13 +1794,13 @@ var app6 = new Hono6().get("/:name/files", async (c) => {
|
|
|
1328
1794
|
const entry = findAgent(name);
|
|
1329
1795
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1330
1796
|
const dir = agentDir(name);
|
|
1331
|
-
const filePath =
|
|
1332
|
-
if (!
|
|
1797
|
+
const filePath = resolve7(dir, "home", filename);
|
|
1798
|
+
if (!existsSync5(filePath)) {
|
|
1333
1799
|
return c.json({ error: "File not found" }, 404);
|
|
1334
1800
|
}
|
|
1335
1801
|
const content = await readFile(filePath, "utf-8");
|
|
1336
1802
|
return c.json({ filename, content });
|
|
1337
|
-
}).put("/:name/files/:filename",
|
|
1803
|
+
}).put("/:name/files/:filename", zValidator4("json", saveFileSchema), async (c) => {
|
|
1338
1804
|
const name = c.req.param("name");
|
|
1339
1805
|
const filename = c.req.param("filename");
|
|
1340
1806
|
if (!ALLOWED_FILES.has(filename)) {
|
|
@@ -1343,7 +1809,7 @@ var app6 = new Hono6().get("/:name/files", async (c) => {
|
|
|
1343
1809
|
const entry = findAgent(name);
|
|
1344
1810
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1345
1811
|
const dir = agentDir(name);
|
|
1346
|
-
const filePath =
|
|
1812
|
+
const filePath = resolve7(dir, "home", filename);
|
|
1347
1813
|
const { content } = c.req.valid("json");
|
|
1348
1814
|
await writeFile(filePath, content);
|
|
1349
1815
|
return c.json({ ok: true });
|
|
@@ -1352,8 +1818,8 @@ var files_default = app6;
|
|
|
1352
1818
|
|
|
1353
1819
|
// src/web/routes/logs.ts
|
|
1354
1820
|
import { spawn as spawn2 } from "child_process";
|
|
1355
|
-
import { existsSync as
|
|
1356
|
-
import { resolve as
|
|
1821
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1822
|
+
import { resolve as resolve8 } from "path";
|
|
1357
1823
|
import { Hono as Hono7 } from "hono";
|
|
1358
1824
|
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
1359
1825
|
var app7 = new Hono7().get("/:name/logs", async (c) => {
|
|
@@ -1361,8 +1827,8 @@ var app7 = new Hono7().get("/:name/logs", async (c) => {
|
|
|
1361
1827
|
const entry = findAgent(name);
|
|
1362
1828
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1363
1829
|
const dir = agentDir(name);
|
|
1364
|
-
const logFile =
|
|
1365
|
-
if (!
|
|
1830
|
+
const logFile = resolve8(dir, ".volute", "logs", "agent.log");
|
|
1831
|
+
if (!existsSync6(logFile)) {
|
|
1366
1832
|
return c.json({ error: "No log file found" }, 404);
|
|
1367
1833
|
}
|
|
1368
1834
|
return streamSSE2(c, async (stream2) => {
|
|
@@ -1380,9 +1846,9 @@ var app7 = new Hono7().get("/:name/logs", async (c) => {
|
|
|
1380
1846
|
stream2.onAbort(() => {
|
|
1381
1847
|
tail.kill();
|
|
1382
1848
|
});
|
|
1383
|
-
await new Promise((
|
|
1384
|
-
tail.on("exit",
|
|
1385
|
-
stream2.onAbort(
|
|
1849
|
+
await new Promise((resolve11) => {
|
|
1850
|
+
tail.on("exit", resolve11);
|
|
1851
|
+
stream2.onAbort(resolve11);
|
|
1386
1852
|
});
|
|
1387
1853
|
});
|
|
1388
1854
|
});
|
|
@@ -1484,19 +1950,99 @@ var app9 = new Hono9().get("/logs", async (c) => {
|
|
|
1484
1950
|
stream2.writeSSE({ data: JSON.stringify(entry) }).catch(() => {
|
|
1485
1951
|
});
|
|
1486
1952
|
});
|
|
1487
|
-
await new Promise((
|
|
1953
|
+
await new Promise((resolve11) => {
|
|
1488
1954
|
stream2.onAbort(() => {
|
|
1489
1955
|
unsubscribe();
|
|
1490
|
-
|
|
1956
|
+
resolve11();
|
|
1491
1957
|
});
|
|
1492
1958
|
});
|
|
1493
1959
|
});
|
|
1494
1960
|
});
|
|
1495
1961
|
var system_default = app9;
|
|
1496
1962
|
|
|
1497
|
-
// src/web/routes/
|
|
1963
|
+
// src/web/routes/update.ts
|
|
1964
|
+
import { spawn as spawn3 } from "child_process";
|
|
1498
1965
|
import { Hono as Hono10 } from "hono";
|
|
1499
|
-
var
|
|
1966
|
+
var bin;
|
|
1967
|
+
var app10 = new Hono10().get("/update", async (c) => {
|
|
1968
|
+
const result = await checkForUpdate();
|
|
1969
|
+
return c.json(result);
|
|
1970
|
+
}).post("/update", requireAdmin, async (c) => {
|
|
1971
|
+
bin ??= resolveVoluteBin();
|
|
1972
|
+
const child = spawn3(bin, ["update"], {
|
|
1973
|
+
stdio: "ignore",
|
|
1974
|
+
detached: true
|
|
1975
|
+
});
|
|
1976
|
+
child.on("error", (err) => {
|
|
1977
|
+
logger_default.error("Update process error", { error: err.message });
|
|
1978
|
+
});
|
|
1979
|
+
child.unref();
|
|
1980
|
+
return c.json({ ok: true, message: "Updating..." });
|
|
1981
|
+
});
|
|
1982
|
+
var update_default = app10;
|
|
1983
|
+
|
|
1984
|
+
// src/web/routes/user-conversations.ts
|
|
1985
|
+
import { zValidator as zValidator5 } from "@hono/zod-validator";
|
|
1986
|
+
import { Hono as Hono11 } from "hono";
|
|
1987
|
+
import { z as z5 } from "zod";
|
|
1988
|
+
var createSchema = z5.object({
|
|
1989
|
+
title: z5.string().optional(),
|
|
1990
|
+
participantNames: z5.array(z5.string()).min(1)
|
|
1991
|
+
});
|
|
1992
|
+
var app11 = new Hono11().use("*", authMiddleware).get("/", async (c) => {
|
|
1993
|
+
const user = c.get("user");
|
|
1994
|
+
const convs = await listConversationsWithParticipants(user.id);
|
|
1995
|
+
return c.json(convs);
|
|
1996
|
+
}).get("/:id/messages", async (c) => {
|
|
1997
|
+
const id = c.req.param("id");
|
|
1998
|
+
const user = c.get("user");
|
|
1999
|
+
if (user.id !== 0 && !await isParticipantOrOwner(id, user.id)) {
|
|
2000
|
+
return c.json({ error: "Conversation not found" }, 404);
|
|
2001
|
+
}
|
|
2002
|
+
const msgs = await getMessages(id);
|
|
2003
|
+
return c.json(msgs);
|
|
2004
|
+
}).post("/", zValidator5("json", createSchema), async (c) => {
|
|
2005
|
+
const user = c.get("user");
|
|
2006
|
+
const body = c.req.valid("json");
|
|
2007
|
+
const participantIds = /* @__PURE__ */ new Set();
|
|
2008
|
+
if (user.id !== 0) participantIds.add(user.id);
|
|
2009
|
+
let firstAgentName;
|
|
2010
|
+
for (const name of body.participantNames) {
|
|
2011
|
+
const existing = await getUserByUsername(name);
|
|
2012
|
+
if (existing) {
|
|
2013
|
+
participantIds.add(existing.id);
|
|
2014
|
+
if (!firstAgentName && existing.user_type === "agent") firstAgentName = name;
|
|
2015
|
+
continue;
|
|
2016
|
+
}
|
|
2017
|
+
if (findAgent(name)) {
|
|
2018
|
+
const au = await getOrCreateAgentUser(name);
|
|
2019
|
+
participantIds.add(au.id);
|
|
2020
|
+
if (!firstAgentName) firstAgentName = name;
|
|
2021
|
+
continue;
|
|
2022
|
+
}
|
|
2023
|
+
return c.json({ error: `User not found: ${name}` }, 400);
|
|
2024
|
+
}
|
|
2025
|
+
if (!firstAgentName) {
|
|
2026
|
+
return c.json({ error: "At least one agent participant is required" }, 400);
|
|
2027
|
+
}
|
|
2028
|
+
const conv = await createConversation(firstAgentName, "volute", {
|
|
2029
|
+
userId: user.id !== 0 ? user.id : void 0,
|
|
2030
|
+
title: body.title,
|
|
2031
|
+
participantIds: [...participantIds]
|
|
2032
|
+
});
|
|
2033
|
+
return c.json(conv, 201);
|
|
2034
|
+
}).delete("/:id", async (c) => {
|
|
2035
|
+
const id = c.req.param("id");
|
|
2036
|
+
const user = c.get("user");
|
|
2037
|
+
const deleted = await deleteConversationForUser(id, user.id);
|
|
2038
|
+
if (!deleted) return c.json({ error: "Conversation not found" }, 404);
|
|
2039
|
+
return c.json({ ok: true });
|
|
2040
|
+
});
|
|
2041
|
+
var user_conversations_default = app11;
|
|
2042
|
+
|
|
2043
|
+
// src/web/routes/variants.ts
|
|
2044
|
+
import { Hono as Hono12 } from "hono";
|
|
2045
|
+
var app12 = new Hono12().get("/:name/variants", async (c) => {
|
|
1500
2046
|
const name = c.req.param("name");
|
|
1501
2047
|
const entry = findAgent(name);
|
|
1502
2048
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
@@ -1510,11 +2056,11 @@ var app10 = new Hono10().get("/:name/variants", async (c) => {
|
|
|
1510
2056
|
);
|
|
1511
2057
|
return c.json(results);
|
|
1512
2058
|
});
|
|
1513
|
-
var variants_default =
|
|
2059
|
+
var variants_default = app12;
|
|
1514
2060
|
|
|
1515
2061
|
// src/web/app.ts
|
|
1516
|
-
var
|
|
1517
|
-
|
|
2062
|
+
var app13 = new Hono13();
|
|
2063
|
+
app13.onError((err, c) => {
|
|
1518
2064
|
if (err instanceof HTTPException) {
|
|
1519
2065
|
return err.getResponse();
|
|
1520
2066
|
}
|
|
@@ -1525,10 +2071,10 @@ app11.onError((err, c) => {
|
|
|
1525
2071
|
});
|
|
1526
2072
|
return c.json({ error: "Internal server error" }, 500);
|
|
1527
2073
|
});
|
|
1528
|
-
|
|
2074
|
+
app13.notFound((c) => {
|
|
1529
2075
|
return c.json({ error: "Not found" }, 404);
|
|
1530
2076
|
});
|
|
1531
|
-
|
|
2077
|
+
app13.use("*", async (c, next) => {
|
|
1532
2078
|
const start = Date.now();
|
|
1533
2079
|
await next();
|
|
1534
2080
|
const duration = Date.now() - start;
|
|
@@ -1539,15 +2085,28 @@ app11.use("*", async (c, next) => {
|
|
|
1539
2085
|
duration
|
|
1540
2086
|
});
|
|
1541
2087
|
});
|
|
1542
|
-
|
|
1543
|
-
|
|
2088
|
+
app13.get("/api/health", (c) => {
|
|
2089
|
+
let version = "unknown";
|
|
2090
|
+
let cached = null;
|
|
2091
|
+
try {
|
|
2092
|
+
version = getCurrentVersion();
|
|
2093
|
+
cached = checkForUpdateCached();
|
|
2094
|
+
} catch (err) {
|
|
2095
|
+
logger_default.error("Health check error", { error: err.message });
|
|
2096
|
+
}
|
|
2097
|
+
return c.json({
|
|
2098
|
+
ok: true,
|
|
2099
|
+
version,
|
|
2100
|
+
...cached?.updateAvailable ? { updateAvailable: true, latest: cached.latest } : {}
|
|
2101
|
+
});
|
|
1544
2102
|
});
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
var
|
|
2103
|
+
app13.use("/api/*", bodyLimit({ maxSize: 10 * 1024 * 1024 }));
|
|
2104
|
+
app13.use("/api/*", csrf());
|
|
2105
|
+
app13.use("/api/agents/*", authMiddleware);
|
|
2106
|
+
app13.use("/api/conversations/*", authMiddleware);
|
|
2107
|
+
app13.use("/api/system/*", authMiddleware);
|
|
2108
|
+
var routes = app13.route("/api/auth", auth_default).route("/api/system", system_default).route("/api/system", update_default).route("/api/agents", agents_default).route("/api/agents", chat_default).route("/api/agents", connectors_default).route("/api/agents", schedules_default).route("/api/agents", logs_default).route("/api/agents", variants_default).route("/api/agents", files_default).route("/api/agents", conversations_default).route("/api/conversations", user_conversations_default);
|
|
2109
|
+
var app_default = app13;
|
|
1551
2110
|
|
|
1552
2111
|
// src/web/server.ts
|
|
1553
2112
|
var MIME_TYPES = {
|
|
@@ -1566,8 +2125,8 @@ async function startServer({
|
|
|
1566
2125
|
let assetsDir = "";
|
|
1567
2126
|
let searchDir = dirname3(new URL(import.meta.url).pathname);
|
|
1568
2127
|
for (let i = 0; i < 5; i++) {
|
|
1569
|
-
const candidate =
|
|
1570
|
-
if (
|
|
2128
|
+
const candidate = resolve9(searchDir, "dist", "web-assets");
|
|
2129
|
+
if (existsSync7(candidate)) {
|
|
1571
2130
|
assetsDir = candidate;
|
|
1572
2131
|
break;
|
|
1573
2132
|
}
|
|
@@ -1576,7 +2135,8 @@ async function startServer({
|
|
|
1576
2135
|
if (assetsDir) {
|
|
1577
2136
|
app_default.get("*", async (c) => {
|
|
1578
2137
|
const urlPath = new URL(c.req.url).pathname;
|
|
1579
|
-
|
|
2138
|
+
if (urlPath.startsWith("/api/")) return c.notFound();
|
|
2139
|
+
const filePath = resolve9(assetsDir, urlPath.slice(1));
|
|
1580
2140
|
if (!filePath.startsWith(assetsDir)) return c.text("Forbidden", 403);
|
|
1581
2141
|
const s = await stat(filePath).catch(() => null);
|
|
1582
2142
|
if (s?.isFile()) {
|
|
@@ -1585,7 +2145,7 @@ async function startServer({
|
|
|
1585
2145
|
const body = await readFile2(filePath);
|
|
1586
2146
|
return c.body(body, 200, { "Content-Type": mime });
|
|
1587
2147
|
}
|
|
1588
|
-
const indexPath =
|
|
2148
|
+
const indexPath = resolve9(assetsDir, "index.html");
|
|
1589
2149
|
const indexStat = await stat(indexPath).catch(() => null);
|
|
1590
2150
|
if (indexStat?.isFile()) {
|
|
1591
2151
|
const body = await readFile2(indexPath, "utf-8");
|
|
@@ -1595,10 +2155,10 @@ async function startServer({
|
|
|
1595
2155
|
});
|
|
1596
2156
|
}
|
|
1597
2157
|
const server = serve({ fetch: app_default.fetch, port, hostname });
|
|
1598
|
-
await new Promise((
|
|
2158
|
+
await new Promise((resolve11, reject) => {
|
|
1599
2159
|
server.on("listening", () => {
|
|
1600
2160
|
logger_default.info("Volute UI running", { hostname, port });
|
|
1601
|
-
|
|
2161
|
+
resolve11();
|
|
1602
2162
|
});
|
|
1603
2163
|
server.on("error", (err) => {
|
|
1604
2164
|
reject(err);
|
|
@@ -1608,12 +2168,24 @@ async function startServer({
|
|
|
1608
2168
|
}
|
|
1609
2169
|
|
|
1610
2170
|
// src/daemon.ts
|
|
2171
|
+
if (!process.env.VOLUTE_HOME) {
|
|
2172
|
+
process.env.VOLUTE_HOME = resolve10(homedir(), ".volute");
|
|
2173
|
+
}
|
|
1611
2174
|
async function startDaemon(opts) {
|
|
1612
2175
|
const { port, hostname } = opts;
|
|
1613
2176
|
const myPid = String(process.pid);
|
|
1614
2177
|
const home = voluteHome();
|
|
1615
|
-
|
|
1616
|
-
|
|
2178
|
+
if (!opts.foreground) {
|
|
2179
|
+
const log2 = new RotatingLog(resolve10(home, "daemon.log"));
|
|
2180
|
+
const write2 = (...args) => log2.write(`${format(...args)}
|
|
2181
|
+
`);
|
|
2182
|
+
console.log = write2;
|
|
2183
|
+
console.error = write2;
|
|
2184
|
+
console.warn = write2;
|
|
2185
|
+
console.info = write2;
|
|
2186
|
+
}
|
|
2187
|
+
const DAEMON_PID_PATH = resolve10(home, "daemon.pid");
|
|
2188
|
+
const DAEMON_JSON_PATH = resolve10(home, "daemon.json");
|
|
1617
2189
|
mkdirSync2(home, { recursive: true });
|
|
1618
2190
|
const token = process.env.VOLUTE_DAEMON_TOKEN || randomBytes(32).toString("hex");
|
|
1619
2191
|
process.env.VOLUTE_DAEMON_TOKEN = token;
|
|
@@ -1666,13 +2238,13 @@ async function startDaemon(opts) {
|
|
|
1666
2238
|
console.error(`[daemon] running on ${hostname}:${port}, pid ${myPid}`);
|
|
1667
2239
|
function cleanup() {
|
|
1668
2240
|
try {
|
|
1669
|
-
if (
|
|
2241
|
+
if (readFileSync5(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
|
|
1670
2242
|
unlinkSync2(DAEMON_PID_PATH);
|
|
1671
2243
|
}
|
|
1672
2244
|
} catch {
|
|
1673
2245
|
}
|
|
1674
2246
|
try {
|
|
1675
|
-
const data = JSON.parse(
|
|
2247
|
+
const data = JSON.parse(readFileSync5(DAEMON_JSON_PATH, "utf-8"));
|
|
1676
2248
|
if (data.token === token) {
|
|
1677
2249
|
unlinkSync2(DAEMON_JSON_PATH);
|
|
1678
2250
|
}
|