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.
Files changed (60) hide show
  1. package/README.md +46 -0
  2. package/dist/agent-manager-2LU6KULR.js +15 -0
  3. package/dist/{channel-2WJRM7PE.js → channel-H7N4SGR2.js} +7 -7
  4. package/dist/{chunk-XZN4WPNC.js → chunk-5SKQ6J7T.js} +9 -1
  5. package/dist/chunk-DEUAVGSA.js +81 -0
  6. package/dist/{chunk-L3BQEZ4Z.js → chunk-IPIPLGME.js} +74 -13
  7. package/dist/chunk-K3NQKI34.js +10 -0
  8. package/dist/chunk-NETNFBA5.js +28 -0
  9. package/dist/{chunk-6UCG6MIX.js → chunk-RALYNMHR.js} +1 -6
  10. package/dist/chunk-VRVVQIYY.js +15 -0
  11. package/dist/{chunk-4YXYAMFT.js → chunk-VVD3XO3E.js} +7 -6
  12. package/dist/{chunk-KFNNHQK7.js → chunk-YEIHRP2J.js} +1 -1
  13. package/dist/cli.js +56 -51
  14. package/dist/connector-6LWB5PRU.js +96 -0
  15. package/dist/connectors/discord.js +22 -1
  16. package/dist/{create-23AM7H5B.js → create-RSWWMGKT.js} +22 -5
  17. package/dist/daemon-client-27KMQQKX.js +9 -0
  18. package/dist/daemon.js +162 -132
  19. package/dist/{delete-GDMSOW3U.js → delete-4ERL2QHH.js} +7 -2
  20. package/dist/{down-WTF73FE7.js → down-HRC4MQCT.js} +10 -3
  21. package/dist/{env-YKUJOFHE.js → env-DBWDTIP6.js} +3 -2
  22. package/dist/{history-7WVVKMUY.js → history-W7BD2H74.js} +9 -8
  23. package/dist/{import-42DOLBDT.js → import-6HTSSDFW.js} +143 -36
  24. package/dist/{logs-SYRQOL6B.js → logs-NHWGHNBF.js} +8 -7
  25. package/dist/{schedule-J37XQM6E.js → schedule-DKZ2E2CL.js} +41 -41
  26. package/dist/{send-PLOYEYER.js → send-5LEJXPYV.js} +3 -2
  27. package/dist/service-SA4TTMDU.js +195 -0
  28. package/dist/setup-ZMNTOJAV.js +148 -0
  29. package/dist/{start-AG7QLULK.js → start-2BSXX6BS.js} +3 -2
  30. package/dist/{status-GCNU4M3K.js → status-N23CV27T.js} +3 -2
  31. package/dist/{stop-IL5Q6NER.js → stop-DSKBIJ2D.js} +3 -2
  32. package/dist/{up-ZC6G6K4K.js → up-4UGID4DM.js} +5 -3
  33. package/dist/{upgrade-DD5TNJWU.js → upgrade-BGFVRCVP.js} +4 -3
  34. package/dist/{merge-CSAVLSLY.js → variant-JPLJTS2P.js} +179 -10
  35. package/dist/web-assets/assets/index-BC5eSqbY.js +296 -0
  36. package/dist/web-assets/index.html +1 -1
  37. package/drizzle/0002_wealthy_the_call.sql +6 -0
  38. package/drizzle/meta/0002_snapshot.json +339 -0
  39. package/drizzle/meta/_journal.json +7 -0
  40. package/package.json +4 -1
  41. package/templates/_base/.init/SOUL.md +5 -1
  42. package/templates/_base/_skills/memory/SKILL.md +2 -2
  43. package/templates/_base/_skills/volute-agent/SKILL.md +28 -11
  44. package/templates/_base/home/VOLUTE.md +4 -2
  45. package/templates/_base/src/lib/auto-commit.ts +8 -3
  46. package/templates/_base/src/lib/types.ts +6 -2
  47. package/templates/_base/src/lib/volute-server.ts +5 -0
  48. package/templates/agent-sdk/.init/CLAUDE.md +15 -13
  49. package/templates/agent-sdk/src/agent.ts +12 -1
  50. package/templates/agent-sdk/src/lib/agent-sessions.ts +28 -4
  51. package/templates/pi/.init/AGENTS.md +11 -9
  52. package/templates/pi/src/agent.ts +16 -3
  53. package/templates/pi/src/lib/agent-sessions.ts +26 -4
  54. package/dist/agent-manager-SSJUZWOV.js +0 -13
  55. package/dist/connect-X5V5IMRW.js +0 -48
  56. package/dist/daemon-client-VN24HM5T.js +0 -10
  57. package/dist/disconnect-5JWFZ6RV.js +0 -30
  58. package/dist/fork-GRSVMBKI.js +0 -119
  59. package/dist/variants-QQIEKT6M.js +0 -60
  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
- } from "./chunk-L3BQEZ4Z.js";
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-KFNNHQK7.js";
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-6UCG6MIX.js";
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 mkdirSync3, readFileSync as readFileSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
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 as existsSync2,
40
- mkdirSync as mkdirSync2,
41
- readFileSync as readFileSync2,
51
+ existsSync,
52
+ mkdirSync,
53
+ readFileSync,
42
54
  unlinkSync,
43
- writeFileSync as writeFileSync2
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 = dirname2(new URL(import.meta.url).pathname);
59
+ let searchDir = dirname(new URL(import.meta.url).pathname);
77
60
  for (let i = 0; i < 5; i++) {
78
- const candidate = resolve2(searchDir, ...segments);
79
- if (existsSync2(candidate)) return candidate;
80
- searchDir = dirname2(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 = resolve2(agentDir2, "connectors", type, "index.ts");
127
- const userConnector = resolve2(homedir(), ".volute", "connectors", type, "index.ts");
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 (existsSync2(agentConnector)) {
114
+ if (existsSync(agentConnector)) {
132
115
  connectorScript = agentConnector;
133
- runtime = resolve2(agentDir2, "node_modules", ".bin", "tsx");
134
- } else if (existsSync2(userConnector)) {
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 = resolve2(agentDir2, ".volute", "logs");
144
- mkdirSync2(logsDir, { recursive: true });
145
- const logStream = createWriteStream(resolve2(logsDir, `${type}.log`), { flags: "a" });
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 child = spawn(runtime, [connectorScript], {
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 resolve2(agentDir2, ".volute", "connectors", `${type}.pid`);
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
- mkdirSync2(dirname2(pidPath), { recursive: true });
261
- writeFileSync2(pidPath, String(pid));
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 (!existsSync2(pidPath)) return;
256
+ if (!existsSync(pidPath)) return;
272
257
  try {
273
- const pid = parseInt(readFileSync2(pidPath, "utf-8").trim(), 10);
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 existsSync3 } from "fs";
435
- import { dirname as dirname3, resolve as resolve3 } from "path";
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 = dirname3(fileURLToPath(import.meta.url));
505
- var migrationsFolder = existsSync3(resolve3(__dirname, "../drizzle")) ? resolve3(__dirname, "../drizzle") : resolve3(__dirname, "../../drizzle");
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
- try {
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
- var sessions = /* @__PURE__ */ new Map();
595
- function createSession(userId) {
591
+ async function createSession(userId) {
592
+ const db2 = await getDb();
596
593
  const sessionId = crypto.randomUUID();
597
- sessions.set(sessionId, { userId, createdAt: Date.now() });
594
+ await db2.insert(sessions).values({ id: sessionId, userId, createdAt: Date.now() });
598
595
  return sessionId;
599
596
  }
600
- function deleteSession(sessionId) {
601
- sessions.delete(sessionId);
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 session = sessions.get(sessionId);
605
- if (!session) return void 0;
606
- if (Date.now() - session.createdAt > SESSION_MAX_AGE) {
607
- sessions.delete(sessionId);
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 session.userId;
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 === process.env.VOLUTE_DAEMON_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 existsSync4, readFileSync as readFileSync3, rmSync } from "fs";
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 eq2 } from "drizzle-orm";
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(readFileSync3(resolve4(voluteHome(), "daemon.json"), "utf-8"));
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 (!existsSync4(agentDir(name))) return c.json({ error: "Agent directory missing" }, 404);
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 (!existsSync4(dir)) return c.json({ error: "Agent directory missing" }, 404);
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 (!existsSync4(dir)) return c.json({ error: "Agent directory missing" }, 404);
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) await getConnectorManager().stopConnectors(baseName);
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 && existsSync4(dir)) {
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 = [eq2(agentMessages.agent, name)];
938
+ const conditions = [eq3(agentMessages.agent, name)];
916
939
  if (channel) {
917
- conditions.push(eq2(agentMessages.channel, channel));
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 eq3, isNull, sql as sql2 } from "drizzle-orm";
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(eq3(conversations.id, id), eq3(conversations.user_id, userId))).get();
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(eq3(conversations.agent_name, agentName), eq3(conversations.user_id, opts.userId))).orderBy(desc2(conversations.updated_at)).all();
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(eq3(conversations.agent_name, agentName)).orderBy(desc2(conversations.updated_at)).all();
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(eq3(conversations.id, conversationId));
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(eq3(conversations.id, conversationId), isNull(conversations.title)));
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(eq3(messages.conversation_id, conversationId)).orderBy(messages.created_at).all();
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(eq3(conversations.id, id));
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-SSJUZWOV.js");
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 existsSync5 } from "fs";
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 (!existsSync5(homeDir)) return c.json({ error: "Home directory missing" }, 404);
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 (!existsSync5(filePath)) {
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 existsSync6 } from "fs";
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 (!existsSync6(logFile)) {
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 = dirname4(new URL(import.meta.url).pathname);
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 (existsSync7(candidate)) {
1570
+ if (existsSync6(candidate)) {
1548
1571
  assetsDir = candidate;
1549
1572
  break;
1550
1573
  }
1551
- searchDir = dirname4(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
- mkdirSync3(home, { recursive: true });
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
- writeFileSync3(DAEMON_PID_PATH, myPid, { mode: 384 });
1609
- writeFileSync3(DAEMON_JSON_PATH, `${JSON.stringify({ port, token }, null, 2)}
1610
- `, { mode: 384 });
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 (readFileSync4(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
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(readFileSync4(DAEMON_JSON_PATH, "utf-8"));
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);