volute 0.2.1 → 0.3.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 +46 -0
- package/dist/agent-manager-2LU6KULR.js +15 -0
- package/dist/{channel-2WJRM7PE.js → channel-H7N4SGR2.js} +7 -7
- package/dist/{chunk-XZN4WPNC.js → chunk-5SKQ6J7T.js} +9 -1
- package/dist/chunk-DEUAVGSA.js +81 -0
- package/dist/{chunk-L3BQEZ4Z.js → chunk-IPIPLGME.js} +74 -13
- package/dist/chunk-K3NQKI34.js +10 -0
- package/dist/chunk-NETNFBA5.js +28 -0
- package/dist/{chunk-6UCG6MIX.js → chunk-RALYNMHR.js} +1 -6
- package/dist/chunk-VRVVQIYY.js +15 -0
- package/dist/{chunk-4YXYAMFT.js → chunk-VVD3XO3E.js} +7 -6
- package/dist/{chunk-KFNNHQK7.js → chunk-YEIHRP2J.js} +1 -1
- package/dist/cli.js +56 -51
- package/dist/connector-6LWB5PRU.js +96 -0
- package/dist/connectors/discord.js +22 -1
- package/dist/{create-23AM7H5B.js → create-RSWWMGKT.js} +22 -5
- package/dist/daemon-client-27KMQQKX.js +9 -0
- package/dist/daemon.js +162 -132
- package/dist/{delete-GDMSOW3U.js → delete-4ERL2QHH.js} +7 -2
- package/dist/{down-WTF73FE7.js → down-HRC4MQCT.js} +10 -3
- package/dist/{env-YKUJOFHE.js → env-DBWDTIP6.js} +3 -2
- package/dist/{history-7WVVKMUY.js → history-W7BD2H74.js} +9 -8
- package/dist/{import-42DOLBDT.js → import-6HTSSDFW.js} +143 -36
- package/dist/{logs-SYRQOL6B.js → logs-NHWGHNBF.js} +8 -7
- package/dist/{schedule-J37XQM6E.js → schedule-DKZ2E2CL.js} +41 -41
- package/dist/{send-PLOYEYER.js → send-5LEJXPYV.js} +3 -2
- package/dist/service-SA4TTMDU.js +195 -0
- package/dist/setup-ZMNTOJAV.js +148 -0
- package/dist/{start-AG7QLULK.js → start-2BSXX6BS.js} +3 -2
- package/dist/{status-GCNU4M3K.js → status-N23CV27T.js} +3 -2
- package/dist/{stop-IL5Q6NER.js → stop-DSKBIJ2D.js} +3 -2
- package/dist/{up-ZC6G6K4K.js → up-4UGID4DM.js} +5 -3
- package/dist/{upgrade-DD5TNJWU.js → upgrade-BGFVRCVP.js} +4 -3
- package/dist/{merge-CSAVLSLY.js → variant-JPLJTS2P.js} +179 -10
- package/dist/web-assets/assets/index-BC5eSqbY.js +296 -0
- package/dist/web-assets/index.html +1 -1
- package/drizzle/0002_wealthy_the_call.sql +6 -0
- package/drizzle/meta/0002_snapshot.json +339 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +4 -1
- package/templates/_base/.init/SOUL.md +5 -1
- package/templates/_base/_skills/memory/SKILL.md +2 -2
- package/templates/_base/_skills/volute-agent/SKILL.md +28 -11
- package/templates/_base/home/VOLUTE.md +4 -2
- package/templates/_base/src/lib/auto-commit.ts +8 -3
- package/templates/_base/src/lib/types.ts +6 -2
- package/templates/_base/src/lib/volute-server.ts +5 -0
- package/templates/agent-sdk/.init/CLAUDE.md +15 -13
- package/templates/agent-sdk/src/agent.ts +12 -1
- package/templates/agent-sdk/src/lib/agent-sessions.ts +28 -4
- package/templates/pi/.init/AGENTS.md +11 -9
- package/templates/pi/src/agent.ts +16 -3
- package/templates/pi/src/lib/agent-sessions.ts +26 -4
- package/dist/agent-manager-SSJUZWOV.js +0 -13
- package/dist/connect-X5V5IMRW.js +0 -48
- package/dist/daemon-client-VN24HM5T.js +0 -10
- package/dist/disconnect-5JWFZ6RV.js +0 -30
- package/dist/fork-GRSVMBKI.js +0 -119
- package/dist/variants-QQIEKT6M.js +0 -60
- package/dist/web-assets/assets/index-DNNPoxMn.js +0 -158
package/dist/daemon.js
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
clearJsonMap,
|
|
3
4
|
getAgentManager,
|
|
4
|
-
initAgentManager
|
|
5
|
-
|
|
5
|
+
initAgentManager,
|
|
6
|
+
loadJsonMap,
|
|
7
|
+
saveJsonMap
|
|
8
|
+
} from "./chunk-IPIPLGME.js";
|
|
6
9
|
import {
|
|
7
10
|
logBuffer,
|
|
8
11
|
logger_default,
|
|
9
12
|
readNdjson
|
|
10
13
|
} from "./chunk-N4YNKR3Q.js";
|
|
14
|
+
import {
|
|
15
|
+
readVoluteConfig,
|
|
16
|
+
writeVoluteConfig
|
|
17
|
+
} from "./chunk-NETNFBA5.js";
|
|
11
18
|
import {
|
|
12
19
|
loadMergedEnv
|
|
13
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-YEIHRP2J.js";
|
|
21
|
+
import {
|
|
22
|
+
applyIsolation
|
|
23
|
+
} from "./chunk-DEUAVGSA.js";
|
|
14
24
|
import {
|
|
15
|
-
__export,
|
|
16
25
|
agentDir,
|
|
17
26
|
checkHealth,
|
|
18
27
|
findAgent,
|
|
@@ -25,59 +34,33 @@ import {
|
|
|
25
34
|
setAgentRunning,
|
|
26
35
|
setVariantRunning,
|
|
27
36
|
voluteHome
|
|
28
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-RALYNMHR.js";
|
|
38
|
+
import {
|
|
39
|
+
__export
|
|
40
|
+
} from "./chunk-K3NQKI34.js";
|
|
29
41
|
|
|
30
42
|
// src/daemon.ts
|
|
31
43
|
import { randomBytes } from "crypto";
|
|
32
|
-
import { mkdirSync as
|
|
44
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
33
45
|
import { resolve as resolve8 } from "path";
|
|
34
46
|
|
|
35
47
|
// src/lib/connector-manager.ts
|
|
36
48
|
import { spawn } from "child_process";
|
|
37
49
|
import {
|
|
38
50
|
createWriteStream,
|
|
39
|
-
existsSync
|
|
40
|
-
mkdirSync
|
|
41
|
-
readFileSync
|
|
51
|
+
existsSync,
|
|
52
|
+
mkdirSync,
|
|
53
|
+
readFileSync,
|
|
42
54
|
unlinkSync,
|
|
43
|
-
writeFileSync
|
|
55
|
+
writeFileSync
|
|
44
56
|
} from "fs";
|
|
45
|
-
import { homedir } from "os";
|
|
46
|
-
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
47
|
-
|
|
48
|
-
// src/lib/volute-config.ts
|
|
49
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
50
57
|
import { dirname, resolve } from "path";
|
|
51
|
-
function configPath(agentDir2) {
|
|
52
|
-
const newPath = resolve(agentDir2, "home/.config/volute.json");
|
|
53
|
-
if (existsSync(newPath)) return newPath;
|
|
54
|
-
const oldPath = resolve(agentDir2, "volute.json");
|
|
55
|
-
if (existsSync(oldPath)) return oldPath;
|
|
56
|
-
return newPath;
|
|
57
|
-
}
|
|
58
|
-
function readVoluteConfig(agentDir2) {
|
|
59
|
-
const path = configPath(agentDir2);
|
|
60
|
-
if (!existsSync(path)) return {};
|
|
61
|
-
try {
|
|
62
|
-
return JSON.parse(readFileSync(path, "utf-8"));
|
|
63
|
-
} catch {
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
function writeVoluteConfig(agentDir2, config) {
|
|
68
|
-
const path = resolve(agentDir2, "home/.config/volute.json");
|
|
69
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
70
|
-
writeFileSync(path, `${JSON.stringify(config, null, 2)}
|
|
71
|
-
`);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// src/lib/connector-manager.ts
|
|
75
58
|
function searchUpwards(...segments) {
|
|
76
|
-
let searchDir =
|
|
59
|
+
let searchDir = dirname(new URL(import.meta.url).pathname);
|
|
77
60
|
for (let i = 0; i < 5; i++) {
|
|
78
|
-
const candidate =
|
|
79
|
-
if (
|
|
80
|
-
searchDir =
|
|
61
|
+
const candidate = resolve(searchDir, ...segments);
|
|
62
|
+
if (existsSync(candidate)) return candidate;
|
|
63
|
+
searchDir = dirname(searchDir);
|
|
81
64
|
}
|
|
82
65
|
return null;
|
|
83
66
|
}
|
|
@@ -123,15 +106,15 @@ var ConnectorManager = class {
|
|
|
123
106
|
this.connectors.get(agentName)?.delete(type);
|
|
124
107
|
}
|
|
125
108
|
this.killOrphanConnector(agentDir2, type);
|
|
126
|
-
const agentConnector =
|
|
127
|
-
const userConnector =
|
|
109
|
+
const agentConnector = resolve(agentDir2, "connectors", type, "index.ts");
|
|
110
|
+
const userConnector = resolve(voluteHome(), "connectors", type, "index.ts");
|
|
128
111
|
const builtinConnector = this.resolveBuiltinConnector(type);
|
|
129
112
|
let connectorScript;
|
|
130
113
|
let runtime;
|
|
131
|
-
if (
|
|
114
|
+
if (existsSync(agentConnector)) {
|
|
132
115
|
connectorScript = agentConnector;
|
|
133
|
-
runtime =
|
|
134
|
-
} else if (
|
|
116
|
+
runtime = resolve(agentDir2, "node_modules", ".bin", "tsx");
|
|
117
|
+
} else if (existsSync(userConnector)) {
|
|
135
118
|
connectorScript = userConnector;
|
|
136
119
|
runtime = this.resolveVoluteTsx();
|
|
137
120
|
} else if (builtinConnector) {
|
|
@@ -140,15 +123,15 @@ var ConnectorManager = class {
|
|
|
140
123
|
} else {
|
|
141
124
|
throw new Error(`No connector code found for type: ${type}`);
|
|
142
125
|
}
|
|
143
|
-
const logsDir =
|
|
144
|
-
|
|
145
|
-
const logStream = createWriteStream(
|
|
126
|
+
const logsDir = resolve(agentDir2, ".volute", "logs");
|
|
127
|
+
mkdirSync(logsDir, { recursive: true });
|
|
128
|
+
const logStream = createWriteStream(resolve(logsDir, `${type}.log`), { flags: "a" });
|
|
146
129
|
const agentEnv = loadMergedEnv(agentDir2);
|
|
147
130
|
const prefix = `${type.toUpperCase()}_`;
|
|
148
131
|
const connectorEnv = Object.fromEntries(
|
|
149
132
|
Object.entries(agentEnv).filter(([k]) => k.startsWith(prefix))
|
|
150
133
|
);
|
|
151
|
-
const
|
|
134
|
+
const spawnOpts = {
|
|
152
135
|
stdio: ["ignore", "pipe", "pipe"],
|
|
153
136
|
env: {
|
|
154
137
|
...process.env,
|
|
@@ -160,7 +143,9 @@ var ConnectorManager = class {
|
|
|
160
143
|
} : {},
|
|
161
144
|
...connectorEnv
|
|
162
145
|
}
|
|
163
|
-
}
|
|
146
|
+
};
|
|
147
|
+
await applyIsolation(spawnOpts, agentName);
|
|
148
|
+
const child = spawn(runtime, [connectorScript], spawnOpts);
|
|
164
149
|
child.stdout?.pipe(logStream);
|
|
165
150
|
child.stderr?.pipe(logStream);
|
|
166
151
|
if (child.pid) {
|
|
@@ -253,12 +238,12 @@ var ConnectorManager = class {
|
|
|
253
238
|
}));
|
|
254
239
|
}
|
|
255
240
|
connectorPidPath(agentDir2, type) {
|
|
256
|
-
return
|
|
241
|
+
return resolve(agentDir2, ".volute", "connectors", `${type}.pid`);
|
|
257
242
|
}
|
|
258
243
|
saveConnectorPid(agentDir2, type, pid) {
|
|
259
244
|
const pidPath = this.connectorPidPath(agentDir2, type);
|
|
260
|
-
|
|
261
|
-
|
|
245
|
+
mkdirSync(dirname(pidPath), { recursive: true });
|
|
246
|
+
writeFileSync(pidPath, String(pid));
|
|
262
247
|
}
|
|
263
248
|
removeConnectorPid(agentDir2, type) {
|
|
264
249
|
try {
|
|
@@ -268,9 +253,9 @@ var ConnectorManager = class {
|
|
|
268
253
|
}
|
|
269
254
|
killOrphanConnector(agentDir2, type) {
|
|
270
255
|
const pidPath = this.connectorPidPath(agentDir2, type);
|
|
271
|
-
if (!
|
|
256
|
+
if (!existsSync(pidPath)) return;
|
|
272
257
|
try {
|
|
273
|
-
const pid = parseInt(
|
|
258
|
+
const pid = parseInt(readFileSync(pidPath, "utf-8").trim(), 10);
|
|
274
259
|
if (pid > 0) {
|
|
275
260
|
process.kill(pid, "SIGTERM");
|
|
276
261
|
console.error(`[daemon] killed orphan connector ${type} (pid ${pid})`);
|
|
@@ -301,6 +286,7 @@ function getConnectorManager() {
|
|
|
301
286
|
}
|
|
302
287
|
|
|
303
288
|
// src/lib/scheduler.ts
|
|
289
|
+
import { resolve as resolve2 } from "path";
|
|
304
290
|
import { CronExpressionParser } from "cron-parser";
|
|
305
291
|
var Scheduler = class {
|
|
306
292
|
schedules = /* @__PURE__ */ new Map();
|
|
@@ -309,14 +295,27 @@ var Scheduler = class {
|
|
|
309
295
|
// "agent:scheduleId" → epoch minute
|
|
310
296
|
daemonPort = null;
|
|
311
297
|
daemonToken = null;
|
|
298
|
+
get statePath() {
|
|
299
|
+
return resolve2(voluteHome(), "scheduler-state.json");
|
|
300
|
+
}
|
|
312
301
|
start(daemonPort, daemonToken) {
|
|
313
302
|
this.daemonPort = daemonPort ?? null;
|
|
314
303
|
this.daemonToken = daemonToken ?? null;
|
|
304
|
+
this.loadState();
|
|
315
305
|
this.interval = setInterval(() => this.tick(), 6e4);
|
|
316
306
|
}
|
|
317
307
|
stop() {
|
|
318
308
|
if (this.interval) clearInterval(this.interval);
|
|
319
309
|
}
|
|
310
|
+
loadState() {
|
|
311
|
+
this.lastFired = loadJsonMap(this.statePath);
|
|
312
|
+
}
|
|
313
|
+
saveState() {
|
|
314
|
+
saveJsonMap(this.statePath, this.lastFired);
|
|
315
|
+
}
|
|
316
|
+
clearState() {
|
|
317
|
+
clearJsonMap(this.statePath, this.lastFired);
|
|
318
|
+
}
|
|
320
319
|
loadSchedules(agentName) {
|
|
321
320
|
const dir = agentDir(agentName);
|
|
322
321
|
const config = readVoluteConfig(dir);
|
|
@@ -355,6 +354,7 @@ var Scheduler = class {
|
|
|
355
354
|
const prevMinute = Math.floor(prev.getTime() / 6e4);
|
|
356
355
|
if (prevMinute === epochMinute) {
|
|
357
356
|
this.lastFired.set(key, epochMinute);
|
|
357
|
+
this.saveState();
|
|
358
358
|
return true;
|
|
359
359
|
}
|
|
360
360
|
return false;
|
|
@@ -410,19 +410,9 @@ function getScheduler() {
|
|
|
410
410
|
return instance2;
|
|
411
411
|
}
|
|
412
412
|
|
|
413
|
-
// src/web/server.ts
|
|
414
|
-
import { existsSync as existsSync7 } from "fs";
|
|
415
|
-
import { readFile as readFile2, stat } from "fs/promises";
|
|
416
|
-
import { dirname as dirname4, extname, resolve as resolve7 } from "path";
|
|
417
|
-
import { serve } from "@hono/node-server";
|
|
418
|
-
|
|
419
|
-
// src/web/app.ts
|
|
420
|
-
import { Hono as Hono11 } from "hono";
|
|
421
|
-
import { bodyLimit } from "hono/body-limit";
|
|
422
|
-
import { csrf } from "hono/csrf";
|
|
423
|
-
import { HTTPException } from "hono/http-exception";
|
|
424
|
-
|
|
425
413
|
// src/web/middleware/auth.ts
|
|
414
|
+
import { timingSafeEqual } from "crypto";
|
|
415
|
+
import { eq as eq2, lt } from "drizzle-orm";
|
|
426
416
|
import { getCookie } from "hono/cookie";
|
|
427
417
|
import { createMiddleware } from "hono/factory";
|
|
428
418
|
|
|
@@ -431,8 +421,8 @@ import { compareSync, hashSync } from "bcryptjs";
|
|
|
431
421
|
import { and, count, eq } from "drizzle-orm";
|
|
432
422
|
|
|
433
423
|
// src/lib/db.ts
|
|
434
|
-
import { chmodSync, existsSync as
|
|
435
|
-
import { dirname as
|
|
424
|
+
import { chmodSync, existsSync as existsSync2 } from "fs";
|
|
425
|
+
import { dirname as dirname2, resolve as resolve3 } from "path";
|
|
436
426
|
import { fileURLToPath } from "url";
|
|
437
427
|
import { drizzle } from "drizzle-orm/libsql";
|
|
438
428
|
import { migrate } from "drizzle-orm/libsql/migrator";
|
|
@@ -443,6 +433,7 @@ __export(schema_exports, {
|
|
|
443
433
|
agentMessages: () => agentMessages,
|
|
444
434
|
conversations: () => conversations,
|
|
445
435
|
messages: () => messages,
|
|
436
|
+
sessions: () => sessions,
|
|
446
437
|
users: () => users
|
|
447
438
|
});
|
|
448
439
|
import { sql } from "drizzle-orm";
|
|
@@ -487,6 +478,11 @@ var agentMessages = sqliteTable(
|
|
|
487
478
|
index("idx_agent_messages_channel").on(table.agent, table.channel)
|
|
488
479
|
]
|
|
489
480
|
);
|
|
481
|
+
var sessions = sqliteTable("sessions", {
|
|
482
|
+
id: text("id").primaryKey(),
|
|
483
|
+
userId: integer("user_id").references(() => users.id, { onDelete: "cascade" }).notNull(),
|
|
484
|
+
createdAt: integer("created_at").notNull()
|
|
485
|
+
});
|
|
490
486
|
var messages = sqliteTable(
|
|
491
487
|
"messages",
|
|
492
488
|
{
|
|
@@ -501,18 +497,14 @@ var messages = sqliteTable(
|
|
|
501
497
|
);
|
|
502
498
|
|
|
503
499
|
// src/lib/db.ts
|
|
504
|
-
var __dirname =
|
|
505
|
-
var migrationsFolder =
|
|
500
|
+
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
501
|
+
var migrationsFolder = existsSync2(resolve3(__dirname, "../drizzle")) ? resolve3(__dirname, "../drizzle") : resolve3(__dirname, "../../drizzle");
|
|
506
502
|
var db = null;
|
|
507
503
|
async function getDb() {
|
|
508
504
|
if (db) return db;
|
|
509
505
|
const dbPath = process.env.VOLUTE_DB_PATH || resolve3(voluteHome(), "volute.db");
|
|
510
506
|
db = drizzle({ connection: { url: `file:${dbPath}` }, schema: schema_exports });
|
|
511
|
-
|
|
512
|
-
await migrate(db, { migrationsFolder });
|
|
513
|
-
} catch (e) {
|
|
514
|
-
if (!(e instanceof Error) || !e.message.includes("already exists")) throw e;
|
|
515
|
-
}
|
|
507
|
+
await migrate(db, { migrationsFolder });
|
|
516
508
|
try {
|
|
517
509
|
chmodSync(dbPath, 384);
|
|
518
510
|
} catch (err) {
|
|
@@ -590,24 +582,36 @@ async function approveUser(id) {
|
|
|
590
582
|
}
|
|
591
583
|
|
|
592
584
|
// src/web/middleware/auth.ts
|
|
585
|
+
function isValidDaemonToken(token) {
|
|
586
|
+
const expected = process.env.VOLUTE_DAEMON_TOKEN;
|
|
587
|
+
if (!expected || token.length !== expected.length) return false;
|
|
588
|
+
return timingSafeEqual(Buffer.from(token), Buffer.from(expected));
|
|
589
|
+
}
|
|
593
590
|
var SESSION_MAX_AGE = 864e5;
|
|
594
|
-
|
|
595
|
-
|
|
591
|
+
async function createSession(userId) {
|
|
592
|
+
const db2 = await getDb();
|
|
596
593
|
const sessionId = crypto.randomUUID();
|
|
597
|
-
sessions.
|
|
594
|
+
await db2.insert(sessions).values({ id: sessionId, userId, createdAt: Date.now() });
|
|
598
595
|
return sessionId;
|
|
599
596
|
}
|
|
600
|
-
function deleteSession(sessionId) {
|
|
601
|
-
|
|
597
|
+
async function deleteSession(sessionId) {
|
|
598
|
+
const db2 = await getDb();
|
|
599
|
+
await db2.delete(sessions).where(eq2(sessions.id, sessionId));
|
|
602
600
|
}
|
|
603
|
-
function getSessionUserId(sessionId) {
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
if (
|
|
607
|
-
|
|
601
|
+
async function getSessionUserId(sessionId) {
|
|
602
|
+
const db2 = await getDb();
|
|
603
|
+
const row = await db2.select().from(sessions).where(eq2(sessions.id, sessionId)).get();
|
|
604
|
+
if (!row) return void 0;
|
|
605
|
+
if (Date.now() - row.createdAt > SESSION_MAX_AGE) {
|
|
606
|
+
await db2.delete(sessions).where(eq2(sessions.id, sessionId));
|
|
608
607
|
return void 0;
|
|
609
608
|
}
|
|
610
|
-
return
|
|
609
|
+
return row.userId;
|
|
610
|
+
}
|
|
611
|
+
async function cleanExpiredSessions() {
|
|
612
|
+
const db2 = await getDb();
|
|
613
|
+
const cutoff = Date.now() - SESSION_MAX_AGE;
|
|
614
|
+
await db2.delete(sessions).where(lt(sessions.createdAt, cutoff));
|
|
611
615
|
}
|
|
612
616
|
var requireAdmin = createMiddleware(async (c, next) => {
|
|
613
617
|
const user = c.get("user");
|
|
@@ -620,7 +624,7 @@ var authMiddleware = createMiddleware(async (c, next) => {
|
|
|
620
624
|
const authHeader = c.req.header("Authorization");
|
|
621
625
|
if (authHeader?.startsWith("Bearer ")) {
|
|
622
626
|
const token = authHeader.slice(7);
|
|
623
|
-
if (token && token
|
|
627
|
+
if (token && isValidDaemonToken(token)) {
|
|
624
628
|
c.set("user", { id: 0, username: "cli", role: "admin" });
|
|
625
629
|
await next();
|
|
626
630
|
return;
|
|
@@ -628,7 +632,7 @@ var authMiddleware = createMiddleware(async (c, next) => {
|
|
|
628
632
|
}
|
|
629
633
|
const sessionId = getCookie(c, "volute_session");
|
|
630
634
|
if (!sessionId) return c.json({ error: "Unauthorized" }, 401);
|
|
631
|
-
const userId = getSessionUserId(sessionId);
|
|
635
|
+
const userId = await getSessionUserId(sessionId);
|
|
632
636
|
if (userId == null) return c.json({ error: "Unauthorized" }, 401);
|
|
633
637
|
const user = await getUser(userId);
|
|
634
638
|
if (!user) return c.json({ error: "Unauthorized" }, 401);
|
|
@@ -637,10 +641,22 @@ var authMiddleware = createMiddleware(async (c, next) => {
|
|
|
637
641
|
await next();
|
|
638
642
|
});
|
|
639
643
|
|
|
644
|
+
// src/web/server.ts
|
|
645
|
+
import { existsSync as existsSync6 } from "fs";
|
|
646
|
+
import { readFile as readFile2, stat } from "fs/promises";
|
|
647
|
+
import { dirname as dirname3, extname, resolve as resolve7 } from "path";
|
|
648
|
+
import { serve } from "@hono/node-server";
|
|
649
|
+
|
|
650
|
+
// src/web/app.ts
|
|
651
|
+
import { Hono as Hono11 } from "hono";
|
|
652
|
+
import { bodyLimit } from "hono/body-limit";
|
|
653
|
+
import { csrf } from "hono/csrf";
|
|
654
|
+
import { HTTPException } from "hono/http-exception";
|
|
655
|
+
|
|
640
656
|
// src/web/routes/agents.ts
|
|
641
|
-
import { existsSync as
|
|
657
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, rmSync } from "fs";
|
|
642
658
|
import { resolve as resolve4 } from "path";
|
|
643
|
-
import { and as and2, desc, eq as
|
|
659
|
+
import { and as and2, desc, eq as eq3 } from "drizzle-orm";
|
|
644
660
|
import { Hono } from "hono";
|
|
645
661
|
import { stream } from "hono/streaming";
|
|
646
662
|
|
|
@@ -656,7 +672,7 @@ var CHANNELS = {
|
|
|
656
672
|
// src/web/routes/agents.ts
|
|
657
673
|
function getDaemonPort() {
|
|
658
674
|
try {
|
|
659
|
-
const data = JSON.parse(
|
|
675
|
+
const data = JSON.parse(readFileSync2(resolve4(voluteHome(), "daemon.json"), "utf-8"));
|
|
660
676
|
return data.port;
|
|
661
677
|
} catch {
|
|
662
678
|
return void 0;
|
|
@@ -701,7 +717,7 @@ var app = new Hono().get("/", async (c) => {
|
|
|
701
717
|
const name = c.req.param("name");
|
|
702
718
|
const entry = findAgent(name);
|
|
703
719
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
704
|
-
if (!
|
|
720
|
+
if (!existsSync3(agentDir(name))) return c.json({ error: "Agent directory missing" }, 404);
|
|
705
721
|
const { status, channels } = await getAgentStatus(name, entry.port);
|
|
706
722
|
const variants = readVariants(name);
|
|
707
723
|
const manager = getAgentManager();
|
|
@@ -727,7 +743,7 @@ var app = new Hono().get("/", async (c) => {
|
|
|
727
743
|
if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
|
|
728
744
|
} else {
|
|
729
745
|
const dir = agentDir(baseName);
|
|
730
|
-
if (!
|
|
746
|
+
if (!existsSync3(dir)) return c.json({ error: "Agent directory missing" }, 404);
|
|
731
747
|
}
|
|
732
748
|
const manager = getAgentManager();
|
|
733
749
|
if (manager.isRunning(name)) {
|
|
@@ -738,6 +754,7 @@ var app = new Hono().get("/", async (c) => {
|
|
|
738
754
|
if (!variantName) {
|
|
739
755
|
const dir = agentDir(baseName);
|
|
740
756
|
await getConnectorManager().startConnectors(baseName, dir, entry.port, getDaemonPort());
|
|
757
|
+
getScheduler().loadSchedules(baseName);
|
|
741
758
|
}
|
|
742
759
|
return c.json({ ok: true });
|
|
743
760
|
} catch (err) {
|
|
@@ -753,7 +770,7 @@ var app = new Hono().get("/", async (c) => {
|
|
|
753
770
|
if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
|
|
754
771
|
} else {
|
|
755
772
|
const dir = agentDir(baseName);
|
|
756
|
-
if (!
|
|
773
|
+
if (!existsSync3(dir)) return c.json({ error: "Agent directory missing" }, 404);
|
|
757
774
|
}
|
|
758
775
|
const manager = getAgentManager();
|
|
759
776
|
const connectorManager = getConnectorManager();
|
|
@@ -766,6 +783,7 @@ var app = new Hono().get("/", async (c) => {
|
|
|
766
783
|
if (!variantName) {
|
|
767
784
|
const dir = agentDir(baseName);
|
|
768
785
|
await connectorManager.startConnectors(baseName, dir, entry.port, getDaemonPort());
|
|
786
|
+
getScheduler().loadSchedules(baseName);
|
|
769
787
|
}
|
|
770
788
|
return c.json({ ok: true });
|
|
771
789
|
} catch (err) {
|
|
@@ -785,7 +803,10 @@ var app = new Hono().get("/", async (c) => {
|
|
|
785
803
|
return c.json({ error: "Agent is not running" }, 409);
|
|
786
804
|
}
|
|
787
805
|
try {
|
|
788
|
-
if (!variantName)
|
|
806
|
+
if (!variantName) {
|
|
807
|
+
await getConnectorManager().stopConnectors(baseName);
|
|
808
|
+
getScheduler().unloadSchedules(baseName);
|
|
809
|
+
}
|
|
789
810
|
await manager.stopAgent(name);
|
|
790
811
|
return c.json({ ok: true });
|
|
791
812
|
} catch (err) {
|
|
@@ -804,7 +825,7 @@ var app = new Hono().get("/", async (c) => {
|
|
|
804
825
|
}
|
|
805
826
|
removeAllVariants(name);
|
|
806
827
|
removeAgent(name);
|
|
807
|
-
if (force &&
|
|
828
|
+
if (force && existsSync3(dir)) {
|
|
808
829
|
rmSync(dir, { recursive: true, force: true });
|
|
809
830
|
}
|
|
810
831
|
return c.json({ ok: true });
|
|
@@ -878,6 +899,7 @@ var app = new Hono().get("/", async (c) => {
|
|
|
878
899
|
textParts.push(event.content);
|
|
879
900
|
}
|
|
880
901
|
} catch {
|
|
902
|
+
console.warn(`[daemon] malformed NDJSON line from ${baseName}`);
|
|
881
903
|
}
|
|
882
904
|
}
|
|
883
905
|
}
|
|
@@ -888,6 +910,7 @@ var app = new Hono().get("/", async (c) => {
|
|
|
888
910
|
textParts.push(event.content);
|
|
889
911
|
}
|
|
890
912
|
} catch {
|
|
913
|
+
console.warn(`[daemon] malformed NDJSON trailing data from ${baseName}`);
|
|
891
914
|
}
|
|
892
915
|
}
|
|
893
916
|
if (textParts.length > 0) {
|
|
@@ -909,12 +932,12 @@ var app = new Hono().get("/", async (c) => {
|
|
|
909
932
|
}).get("/:name/history", async (c) => {
|
|
910
933
|
const name = c.req.param("name");
|
|
911
934
|
const channel = c.req.query("channel");
|
|
912
|
-
const limit = parseInt(c.req.query("limit") ?? "50", 10);
|
|
913
|
-
const offset = parseInt(c.req.query("offset") ?? "0", 10);
|
|
935
|
+
const limit = Math.min(Math.max(parseInt(c.req.query("limit") ?? "50", 10) || 50, 1), 200);
|
|
936
|
+
const offset = Math.max(parseInt(c.req.query("offset") ?? "0", 10) || 0, 0);
|
|
914
937
|
const db2 = await getDb();
|
|
915
|
-
const conditions = [
|
|
938
|
+
const conditions = [eq3(agentMessages.agent, name)];
|
|
916
939
|
if (channel) {
|
|
917
|
-
conditions.push(
|
|
940
|
+
conditions.push(eq3(agentMessages.channel, channel));
|
|
918
941
|
}
|
|
919
942
|
const rows = await db2.select().from(agentMessages).where(and2(...conditions)).orderBy(desc(agentMessages.created_at)).limit(limit).offset(offset);
|
|
920
943
|
return c.json(rows);
|
|
@@ -953,7 +976,7 @@ var app2 = new Hono2().post("/register", zValidator("json", credentialsSchema),
|
|
|
953
976
|
}
|
|
954
977
|
const user = await createUser(username, password);
|
|
955
978
|
if (user.role === "admin") {
|
|
956
|
-
const sessionId = createSession(user.id);
|
|
979
|
+
const sessionId = await createSession(user.id);
|
|
957
980
|
setCookie(c, "volute_session", sessionId, { path: "/", httpOnly: true, sameSite: "Lax" });
|
|
958
981
|
}
|
|
959
982
|
return c.json({ id: user.id, username: user.username, role: user.role });
|
|
@@ -963,20 +986,20 @@ var app2 = new Hono2().post("/register", zValidator("json", credentialsSchema),
|
|
|
963
986
|
if (!user) {
|
|
964
987
|
return c.json({ error: "Invalid credentials" }, 401);
|
|
965
988
|
}
|
|
966
|
-
const sessionId = createSession(user.id);
|
|
989
|
+
const sessionId = await createSession(user.id);
|
|
967
990
|
setCookie(c, "volute_session", sessionId, { path: "/", httpOnly: true, sameSite: "Lax" });
|
|
968
991
|
return c.json({ id: user.id, username: user.username, role: user.role });
|
|
969
|
-
}).post("/logout", (c) => {
|
|
992
|
+
}).post("/logout", async (c) => {
|
|
970
993
|
const sessionId = getCookie2(c, "volute_session");
|
|
971
994
|
if (sessionId) {
|
|
972
|
-
deleteSession(sessionId);
|
|
995
|
+
await deleteSession(sessionId);
|
|
973
996
|
deleteCookie(c, "volute_session", { path: "/" });
|
|
974
997
|
}
|
|
975
998
|
return c.json({ ok: true });
|
|
976
999
|
}).get("/me", async (c) => {
|
|
977
1000
|
const sessionId = getCookie2(c, "volute_session");
|
|
978
1001
|
if (!sessionId) return c.json({ error: "Not logged in" }, 401);
|
|
979
|
-
const userId = getSessionUserId(sessionId);
|
|
1002
|
+
const userId = await getSessionUserId(sessionId);
|
|
980
1003
|
if (userId == null) return c.json({ error: "Not logged in" }, 401);
|
|
981
1004
|
const user = await getUser(userId);
|
|
982
1005
|
if (!user) return c.json({ error: "Not logged in" }, 401);
|
|
@@ -992,7 +1015,7 @@ import { z as z2 } from "zod";
|
|
|
992
1015
|
|
|
993
1016
|
// src/lib/conversations.ts
|
|
994
1017
|
import { randomUUID } from "crypto";
|
|
995
|
-
import { and as and3, desc as desc2, eq as
|
|
1018
|
+
import { and as and3, desc as desc2, eq as eq4, isNull, sql as sql2 } from "drizzle-orm";
|
|
996
1019
|
async function createConversation(agentName, channel, opts) {
|
|
997
1020
|
const db2 = await getDb();
|
|
998
1021
|
const id = randomUUID();
|
|
@@ -1015,7 +1038,7 @@ async function createConversation(agentName, channel, opts) {
|
|
|
1015
1038
|
}
|
|
1016
1039
|
async function getConversationForUser(id, userId) {
|
|
1017
1040
|
const db2 = await getDb();
|
|
1018
|
-
const row = await db2.select().from(conversations).where(and3(
|
|
1041
|
+
const row = await db2.select().from(conversations).where(and3(eq4(conversations.id, id), eq4(conversations.user_id, userId))).get();
|
|
1019
1042
|
return row ?? null;
|
|
1020
1043
|
}
|
|
1021
1044
|
async function deleteConversationForUser(id, userId) {
|
|
@@ -1027,20 +1050,20 @@ async function deleteConversationForUser(id, userId) {
|
|
|
1027
1050
|
async function listConversations(agentName, opts) {
|
|
1028
1051
|
const db2 = await getDb();
|
|
1029
1052
|
if (opts?.userId != null) {
|
|
1030
|
-
return db2.select().from(conversations).where(and3(
|
|
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();
|
|
1031
1054
|
}
|
|
1032
|
-
return db2.select().from(conversations).where(
|
|
1055
|
+
return db2.select().from(conversations).where(eq4(conversations.agent_name, agentName)).orderBy(desc2(conversations.updated_at)).all();
|
|
1033
1056
|
}
|
|
1034
1057
|
async function addMessage(conversationId, role, senderName, content) {
|
|
1035
1058
|
const db2 = await getDb();
|
|
1036
1059
|
const serialized = JSON.stringify(content);
|
|
1037
1060
|
const [result] = await db2.insert(messages).values({ conversation_id: conversationId, role, sender_name: senderName, content: serialized }).returning({ id: messages.id, created_at: messages.created_at });
|
|
1038
|
-
await db2.update(conversations).set({ updated_at: sql2`datetime('now')` }).where(
|
|
1061
|
+
await db2.update(conversations).set({ updated_at: sql2`datetime('now')` }).where(eq4(conversations.id, conversationId));
|
|
1039
1062
|
if (role === "user") {
|
|
1040
1063
|
const firstText = content.find((b) => b.type === "text");
|
|
1041
1064
|
const title = firstText ? firstText.text.slice(0, 80) : "";
|
|
1042
1065
|
if (title) {
|
|
1043
|
-
await db2.update(conversations).set({ title }).where(and3(
|
|
1066
|
+
await db2.update(conversations).set({ title }).where(and3(eq4(conversations.id, conversationId), isNull(conversations.title)));
|
|
1044
1067
|
}
|
|
1045
1068
|
}
|
|
1046
1069
|
return {
|
|
@@ -1054,7 +1077,7 @@ async function addMessage(conversationId, role, senderName, content) {
|
|
|
1054
1077
|
}
|
|
1055
1078
|
async function getMessages(conversationId) {
|
|
1056
1079
|
const db2 = await getDb();
|
|
1057
|
-
const rows = await db2.select().from(messages).where(
|
|
1080
|
+
const rows = await db2.select().from(messages).where(eq4(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
|
|
1058
1081
|
return rows.map((row) => {
|
|
1059
1082
|
let content;
|
|
1060
1083
|
try {
|
|
@@ -1068,7 +1091,7 @@ async function getMessages(conversationId) {
|
|
|
1068
1091
|
}
|
|
1069
1092
|
async function deleteConversation(id) {
|
|
1070
1093
|
const db2 = await getDb();
|
|
1071
|
-
await db2.delete(conversations).where(
|
|
1094
|
+
await db2.delete(conversations).where(eq4(conversations.id, id));
|
|
1072
1095
|
}
|
|
1073
1096
|
|
|
1074
1097
|
// src/web/routes/chat.ts
|
|
@@ -1093,7 +1116,7 @@ var app3 = new Hono3().post("/:name/chat", zValidator2("json", chatSchema), asyn
|
|
|
1093
1116
|
if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
|
|
1094
1117
|
port = variant.port;
|
|
1095
1118
|
}
|
|
1096
|
-
const { getAgentManager: getAgentManager2 } = await import("./agent-manager-
|
|
1119
|
+
const { getAgentManager: getAgentManager2 } = await import("./agent-manager-2LU6KULR.js");
|
|
1097
1120
|
if (!getAgentManager2().isRunning(name)) {
|
|
1098
1121
|
return c.json({ error: "Agent is not running" }, 409);
|
|
1099
1122
|
}
|
|
@@ -1278,7 +1301,7 @@ var app5 = new Hono5().get("/:name/conversations", async (c) => {
|
|
|
1278
1301
|
var conversations_default = app5;
|
|
1279
1302
|
|
|
1280
1303
|
// src/web/routes/files.ts
|
|
1281
|
-
import { existsSync as
|
|
1304
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1282
1305
|
import { readdir, readFile, writeFile } from "fs/promises";
|
|
1283
1306
|
import { resolve as resolve5 } from "path";
|
|
1284
1307
|
import { zValidator as zValidator3 } from "@hono/zod-validator";
|
|
@@ -1292,7 +1315,7 @@ var app6 = new Hono6().get("/:name/files", async (c) => {
|
|
|
1292
1315
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1293
1316
|
const dir = agentDir(name);
|
|
1294
1317
|
const homeDir = resolve5(dir, "home");
|
|
1295
|
-
if (!
|
|
1318
|
+
if (!existsSync4(homeDir)) return c.json({ error: "Home directory missing" }, 404);
|
|
1296
1319
|
const allFiles = await readdir(homeDir);
|
|
1297
1320
|
const files = allFiles.filter((f) => f.endsWith(".md") && ALLOWED_FILES.has(f));
|
|
1298
1321
|
return c.json(files);
|
|
@@ -1306,7 +1329,7 @@ var app6 = new Hono6().get("/:name/files", async (c) => {
|
|
|
1306
1329
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1307
1330
|
const dir = agentDir(name);
|
|
1308
1331
|
const filePath = resolve5(dir, "home", filename);
|
|
1309
|
-
if (!
|
|
1332
|
+
if (!existsSync4(filePath)) {
|
|
1310
1333
|
return c.json({ error: "File not found" }, 404);
|
|
1311
1334
|
}
|
|
1312
1335
|
const content = await readFile(filePath, "utf-8");
|
|
@@ -1329,7 +1352,7 @@ var files_default = app6;
|
|
|
1329
1352
|
|
|
1330
1353
|
// src/web/routes/logs.ts
|
|
1331
1354
|
import { spawn as spawn2 } from "child_process";
|
|
1332
|
-
import { existsSync as
|
|
1355
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1333
1356
|
import { resolve as resolve6 } from "path";
|
|
1334
1357
|
import { Hono as Hono7 } from "hono";
|
|
1335
1358
|
import { streamSSE as streamSSE2 } from "hono/streaming";
|
|
@@ -1339,7 +1362,7 @@ var app7 = new Hono7().get("/:name/logs", async (c) => {
|
|
|
1339
1362
|
if (!entry) return c.json({ error: "Agent not found" }, 404);
|
|
1340
1363
|
const dir = agentDir(name);
|
|
1341
1364
|
const logFile = resolve6(dir, ".volute", "logs", "agent.log");
|
|
1342
|
-
if (!
|
|
1365
|
+
if (!existsSync5(logFile)) {
|
|
1343
1366
|
return c.json({ error: "No log file found" }, 404);
|
|
1344
1367
|
}
|
|
1345
1368
|
return streamSSE2(c, async (stream2) => {
|
|
@@ -1541,14 +1564,14 @@ async function startServer({
|
|
|
1541
1564
|
hostname = "127.0.0.1"
|
|
1542
1565
|
}) {
|
|
1543
1566
|
let assetsDir = "";
|
|
1544
|
-
let searchDir =
|
|
1567
|
+
let searchDir = dirname3(new URL(import.meta.url).pathname);
|
|
1545
1568
|
for (let i = 0; i < 5; i++) {
|
|
1546
1569
|
const candidate = resolve7(searchDir, "dist", "web-assets");
|
|
1547
|
-
if (
|
|
1570
|
+
if (existsSync6(candidate)) {
|
|
1548
1571
|
assetsDir = candidate;
|
|
1549
1572
|
break;
|
|
1550
1573
|
}
|
|
1551
|
-
searchDir =
|
|
1574
|
+
searchDir = dirname3(searchDir);
|
|
1552
1575
|
}
|
|
1553
1576
|
if (assetsDir) {
|
|
1554
1577
|
app_default.get("*", async (c) => {
|
|
@@ -1591,7 +1614,7 @@ async function startDaemon(opts) {
|
|
|
1591
1614
|
const home = voluteHome();
|
|
1592
1615
|
const DAEMON_PID_PATH = resolve8(home, "daemon.pid");
|
|
1593
1616
|
const DAEMON_JSON_PATH = resolve8(home, "daemon.json");
|
|
1594
|
-
|
|
1617
|
+
mkdirSync2(home, { recursive: true });
|
|
1595
1618
|
const token = process.env.VOLUTE_DAEMON_TOKEN || randomBytes(32).toString("hex");
|
|
1596
1619
|
process.env.VOLUTE_DAEMON_TOKEN = token;
|
|
1597
1620
|
let server;
|
|
@@ -1605,10 +1628,13 @@ async function startDaemon(opts) {
|
|
|
1605
1628
|
}
|
|
1606
1629
|
throw err;
|
|
1607
1630
|
}
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
`, {
|
|
1631
|
+
writeFileSync2(DAEMON_PID_PATH, myPid, { mode: 384 });
|
|
1632
|
+
writeFileSync2(DAEMON_JSON_PATH, `${JSON.stringify({ port, hostname, token }, null, 2)}
|
|
1633
|
+
`, {
|
|
1634
|
+
mode: 384
|
|
1635
|
+
});
|
|
1611
1636
|
const manager = initAgentManager();
|
|
1637
|
+
manager.loadCrashAttempts();
|
|
1612
1638
|
const connectors = initConnectorManager();
|
|
1613
1639
|
const scheduler = getScheduler();
|
|
1614
1640
|
scheduler.start(port, token);
|
|
@@ -1635,16 +1661,18 @@ async function startDaemon(opts) {
|
|
|
1635
1661
|
setVariantRunning(agentName, variant.name, false);
|
|
1636
1662
|
}
|
|
1637
1663
|
}
|
|
1664
|
+
cleanExpiredSessions().catch(() => {
|
|
1665
|
+
});
|
|
1638
1666
|
console.error(`[daemon] running on ${hostname}:${port}, pid ${myPid}`);
|
|
1639
1667
|
function cleanup() {
|
|
1640
1668
|
try {
|
|
1641
|
-
if (
|
|
1669
|
+
if (readFileSync3(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
|
|
1642
1670
|
unlinkSync2(DAEMON_PID_PATH);
|
|
1643
1671
|
}
|
|
1644
1672
|
} catch {
|
|
1645
1673
|
}
|
|
1646
1674
|
try {
|
|
1647
|
-
const data = JSON.parse(
|
|
1675
|
+
const data = JSON.parse(readFileSync3(DAEMON_JSON_PATH, "utf-8"));
|
|
1648
1676
|
if (data.token === token) {
|
|
1649
1677
|
unlinkSync2(DAEMON_JSON_PATH);
|
|
1650
1678
|
}
|
|
@@ -1657,8 +1685,10 @@ async function startDaemon(opts) {
|
|
|
1657
1685
|
shuttingDown = true;
|
|
1658
1686
|
console.error("[daemon] shutting down...");
|
|
1659
1687
|
scheduler.stop();
|
|
1688
|
+
scheduler.saveState();
|
|
1660
1689
|
await connectors.stopAll();
|
|
1661
1690
|
await manager.stopAll();
|
|
1691
|
+
manager.clearCrashAttempts();
|
|
1662
1692
|
server.close();
|
|
1663
1693
|
cleanup();
|
|
1664
1694
|
process.exit(0);
|