volute 0.3.0 → 0.4.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 (54) hide show
  1. package/README.md +7 -7
  2. package/dist/{agent-manager-2LU6KULR.js → agent-manager-AUCKMGPR.js} +4 -4
  3. package/dist/{channel-H7N4SGR2.js → channel-DQ6UY7QB.js} +17 -40
  4. package/dist/{chunk-RALYNMHR.js → chunk-3C2XR4IY.js} +1 -1
  5. package/dist/chunk-5OCWMTVS.js +152 -0
  6. package/dist/{chunk-YEIHRP2J.js → chunk-DNOXHLE5.js} +1 -1
  7. package/dist/{chunk-IPIPLGME.js → chunk-I6OHXCMV.js} +4 -4
  8. package/dist/chunk-MXUCNIBG.js +168 -0
  9. package/dist/{chunk-DEUAVGSA.js → chunk-SOZA2TLP.js} +1 -1
  10. package/dist/{chunk-VVD3XO3E.js → chunk-YGFIWIOF.js} +1 -1
  11. package/dist/{chunk-N4YNKR3Q.js → chunk-ZHCE4DPY.js} +20 -0
  12. package/dist/cli.js +36 -24
  13. package/dist/connector-DKDJTLYZ.js +152 -0
  14. package/dist/connectors/discord.js +102 -158
  15. package/dist/connectors/slack.js +170 -0
  16. package/dist/connectors/telegram.js +156 -0
  17. package/dist/{create-RSWWMGKT.js → create-ILVOG75A.js} +5 -5
  18. package/dist/{daemon-client-27KMQQKX.js → daemon-client-XR24PUJF.js} +2 -2
  19. package/dist/daemon.js +271 -151
  20. package/dist/{delete-4ERL2QHH.js → delete-55MXCEY5.js} +5 -5
  21. package/dist/{down-HRC4MQCT.js → down-3OB6UVAJ.js} +1 -1
  22. package/dist/{env-DBWDTIP6.js → env-JB27UAC3.js} +2 -2
  23. package/dist/{history-W7BD2H74.js → history-BKG74I43.js} +4 -4
  24. package/dist/{import-6HTSSDFW.js → import-4CI2ZUTJ.js} +17 -2
  25. package/dist/{logs-NHWGHNBF.js → logs-NXFFGUKY.js} +1 -1
  26. package/dist/package-Z2SFO2SV.js +89 -0
  27. package/dist/{schedule-DKZ2E2CL.js → schedule-A35SH4HT.js} +4 -4
  28. package/dist/{send-5LEJXPYV.js → send-3U6OTKG7.js} +8 -4
  29. package/dist/{setup-ZMNTOJAV.js → setup-2FDVN7OF.js} +4 -4
  30. package/dist/{start-2BSXX6BS.js → start-LDPMCMYT.js} +2 -2
  31. package/dist/{status-N23CV27T.js → status-MVSQG54T.js} +2 -2
  32. package/dist/{stop-DSKBIJ2D.js → stop-5PZTZCLL.js} +2 -2
  33. package/dist/{up-4UGID4DM.js → up-F7TMTLRE.js} +1 -1
  34. package/dist/{upgrade-BGFVRCVP.js → upgrade-6ZW2RD64.js} +32 -19
  35. package/dist/{variant-JPLJTS2P.js → variant-T64BKARF.js} +130 -18
  36. package/dist/web-assets/assets/{index-BC5eSqbY.js → index-NS621maO.js} +23 -23
  37. package/dist/web-assets/index.html +1 -1
  38. package/package.json +3 -1
  39. package/templates/_base/_skills/volute-agent/SKILL.md +5 -4
  40. package/templates/_base/home/VOLUTE.md +18 -6
  41. package/templates/_base/src/lib/file-handler.ts +46 -0
  42. package/templates/_base/src/lib/router.ts +180 -0
  43. package/templates/_base/src/lib/routing.ts +100 -0
  44. package/templates/_base/src/lib/types.ts +13 -2
  45. package/templates/_base/src/lib/volute-server.ts +20 -48
  46. package/templates/agent-sdk/src/agent.ts +268 -82
  47. package/templates/agent-sdk/src/server.ts +12 -3
  48. package/templates/pi/src/agent.ts +277 -58
  49. package/templates/pi/src/server.ts +15 -4
  50. package/dist/chunk-MY74SUOL.js +0 -81
  51. package/dist/connector-6LWB5PRU.js +0 -96
  52. package/templates/_base/src/lib/sessions.ts +0 -71
  53. package/templates/agent-sdk/src/lib/agent-sessions.ts +0 -204
  54. package/templates/pi/src/lib/agent-sessions.ts +0 -210
package/dist/daemon.js CHANGED
@@ -1,26 +1,30 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ CHANNELS
4
+ } from "./chunk-5OCWMTVS.js";
2
5
  import {
3
6
  clearJsonMap,
4
7
  getAgentManager,
5
8
  initAgentManager,
6
9
  loadJsonMap,
7
10
  saveJsonMap
8
- } from "./chunk-IPIPLGME.js";
11
+ } from "./chunk-I6OHXCMV.js";
9
12
  import {
13
+ collectPart,
10
14
  logBuffer,
11
15
  logger_default,
12
16
  readNdjson
13
- } from "./chunk-N4YNKR3Q.js";
17
+ } from "./chunk-ZHCE4DPY.js";
14
18
  import {
15
19
  readVoluteConfig,
16
20
  writeVoluteConfig
17
21
  } from "./chunk-NETNFBA5.js";
18
22
  import {
19
23
  loadMergedEnv
20
- } from "./chunk-YEIHRP2J.js";
24
+ } from "./chunk-DNOXHLE5.js";
21
25
  import {
22
26
  applyIsolation
23
- } from "./chunk-DEUAVGSA.js";
27
+ } from "./chunk-SOZA2TLP.js";
24
28
  import {
25
29
  agentDir,
26
30
  checkHealth,
@@ -34,32 +38,106 @@ import {
34
38
  setAgentRunning,
35
39
  setVariantRunning,
36
40
  voluteHome
37
- } from "./chunk-RALYNMHR.js";
41
+ } from "./chunk-3C2XR4IY.js";
38
42
  import {
39
43
  __export
40
44
  } from "./chunk-K3NQKI34.js";
41
45
 
42
46
  // src/daemon.ts
43
47
  import { randomBytes } from "crypto";
44
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
45
- import { resolve as resolve8 } from "path";
48
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
49
+ import { resolve as resolve9 } from "path";
46
50
 
47
51
  // src/lib/connector-manager.ts
48
52
  import { spawn } from "child_process";
49
53
  import {
50
54
  createWriteStream,
51
- existsSync,
55
+ existsSync as existsSync2,
52
56
  mkdirSync,
53
- readFileSync,
57
+ readFileSync as readFileSync2,
54
58
  unlinkSync,
55
59
  writeFileSync
56
60
  } from "fs";
57
- import { dirname, resolve } from "path";
61
+ import { dirname, resolve as resolve2 } from "path";
62
+
63
+ // src/lib/connector-defs.ts
64
+ import { existsSync, readFileSync } from "fs";
65
+ import { resolve } from "path";
66
+ var BUILTIN_DEFS = {
67
+ discord: {
68
+ displayName: "Discord",
69
+ description: "Connect to Discord as a bot",
70
+ envVars: [
71
+ {
72
+ name: "DISCORD_TOKEN",
73
+ required: true,
74
+ description: "Discord bot token",
75
+ scope: "agent"
76
+ },
77
+ {
78
+ name: "DISCORD_GUILD_ID",
79
+ required: false,
80
+ description: "Discord server ID (optional, for slash commands)",
81
+ scope: "agent"
82
+ }
83
+ ]
84
+ },
85
+ slack: {
86
+ displayName: "Slack",
87
+ description: "Connect to Slack via Socket Mode",
88
+ envVars: [
89
+ {
90
+ name: "SLACK_BOT_TOKEN",
91
+ required: true,
92
+ description: "Slack bot token (xoxb-...)",
93
+ scope: "agent"
94
+ },
95
+ {
96
+ name: "SLACK_APP_TOKEN",
97
+ required: true,
98
+ description: "Slack app-level token (xapp-...) for Socket Mode",
99
+ scope: "agent"
100
+ }
101
+ ]
102
+ },
103
+ telegram: {
104
+ displayName: "Telegram",
105
+ description: "Connect to Telegram via long polling",
106
+ envVars: [
107
+ {
108
+ name: "TELEGRAM_BOT_TOKEN",
109
+ required: true,
110
+ description: "Telegram bot token from BotFather",
111
+ scope: "agent"
112
+ }
113
+ ]
114
+ }
115
+ };
116
+ function getConnectorDef(type, connectorDir) {
117
+ if (BUILTIN_DEFS[type]) return BUILTIN_DEFS[type];
118
+ if (connectorDir) {
119
+ const jsonPath = resolve(connectorDir, "connector.json");
120
+ if (existsSync(jsonPath)) {
121
+ try {
122
+ return JSON.parse(readFileSync(jsonPath, "utf-8"));
123
+ } catch (err) {
124
+ console.warn(`Failed to parse ${jsonPath}: ${err}`);
125
+ return null;
126
+ }
127
+ }
128
+ }
129
+ return null;
130
+ }
131
+ function checkMissingEnvVars(def, env) {
132
+ return def.envVars.filter((v) => v.required && !env[v.name]);
133
+ }
134
+
135
+ // src/lib/connector-manager.ts
58
136
  function searchUpwards(...segments) {
59
137
  let searchDir = dirname(new URL(import.meta.url).pathname);
60
138
  for (let i = 0; i < 5; i++) {
61
- const candidate = resolve(searchDir, ...segments);
62
- if (existsSync(candidate)) return candidate;
139
+ const candidate = resolve2(searchDir, ...segments);
140
+ if (existsSync2(candidate)) return candidate;
63
141
  searchDir = dirname(searchDir);
64
142
  }
65
143
  return null;
@@ -85,6 +163,20 @@ var ConnectorManager = class {
85
163
  }
86
164
  }
87
165
  }
166
+ checkConnectorEnv(type, agentDir2) {
167
+ const agentConnectorDir = resolve2(agentDir2, "connectors", type);
168
+ const userConnectorDir = resolve2(voluteHome(), "connectors", type);
169
+ const connectorDir = existsSync2(agentConnectorDir) ? agentConnectorDir : existsSync2(userConnectorDir) ? userConnectorDir : void 0;
170
+ const def = getConnectorDef(type, connectorDir);
171
+ if (!def) return null;
172
+ const env = loadMergedEnv(agentDir2);
173
+ const missing = checkMissingEnvVars(def, env);
174
+ if (missing.length === 0) return null;
175
+ return {
176
+ missing: missing.map((v) => ({ name: v.name, description: v.description })),
177
+ connectorName: def.displayName
178
+ };
179
+ }
88
180
  async startConnector(agentName, agentDir2, agentPort, type, daemonPort) {
89
181
  const existing = this.connectors.get(agentName)?.get(type);
90
182
  if (existing) {
@@ -106,15 +198,15 @@ var ConnectorManager = class {
106
198
  this.connectors.get(agentName)?.delete(type);
107
199
  }
108
200
  this.killOrphanConnector(agentDir2, type);
109
- const agentConnector = resolve(agentDir2, "connectors", type, "index.ts");
110
- const userConnector = resolve(voluteHome(), "connectors", type, "index.ts");
201
+ const agentConnector = resolve2(agentDir2, "connectors", type, "index.ts");
202
+ const userConnector = resolve2(voluteHome(), "connectors", type, "index.ts");
111
203
  const builtinConnector = this.resolveBuiltinConnector(type);
112
204
  let connectorScript;
113
205
  let runtime;
114
- if (existsSync(agentConnector)) {
206
+ if (existsSync2(agentConnector)) {
115
207
  connectorScript = agentConnector;
116
- runtime = resolve(agentDir2, "node_modules", ".bin", "tsx");
117
- } else if (existsSync(userConnector)) {
208
+ runtime = resolve2(agentDir2, "node_modules", ".bin", "tsx");
209
+ } else if (existsSync2(userConnector)) {
118
210
  connectorScript = userConnector;
119
211
  runtime = this.resolveVoluteTsx();
120
212
  } else if (builtinConnector) {
@@ -123,9 +215,9 @@ var ConnectorManager = class {
123
215
  } else {
124
216
  throw new Error(`No connector code found for type: ${type}`);
125
217
  }
126
- const logsDir = resolve(agentDir2, ".volute", "logs");
218
+ const logsDir = resolve2(agentDir2, ".volute", "logs");
127
219
  mkdirSync(logsDir, { recursive: true });
128
- const logStream = createWriteStream(resolve(logsDir, `${type}.log`), { flags: "a" });
220
+ const logStream = createWriteStream(resolve2(logsDir, `${type}.log`), { flags: "a" });
129
221
  const agentEnv = loadMergedEnv(agentDir2);
130
222
  const prefix = `${type.toUpperCase()}_`;
131
223
  const connectorEnv = Object.fromEntries(
@@ -137,8 +229,9 @@ var ConnectorManager = class {
137
229
  ...process.env,
138
230
  VOLUTE_AGENT_PORT: String(agentPort),
139
231
  VOLUTE_AGENT_NAME: agentName,
232
+ VOLUTE_AGENT_DIR: agentDir2,
140
233
  ...daemonPort ? {
141
- VOLUTE_DAEMON_URL: `http://localhost:${daemonPort}`,
234
+ VOLUTE_DAEMON_URL: `http://127.0.0.1:${daemonPort}`,
142
235
  VOLUTE_DAEMON_TOKEN: process.env.VOLUTE_DAEMON_TOKEN
143
236
  } : {},
144
237
  ...connectorEnv
@@ -146,8 +239,12 @@ var ConnectorManager = class {
146
239
  };
147
240
  await applyIsolation(spawnOpts, agentName);
148
241
  const child = spawn(runtime, [connectorScript], spawnOpts);
242
+ let lastStderr = "";
149
243
  child.stdout?.pipe(logStream);
150
- child.stderr?.pipe(logStream);
244
+ child.stderr?.on("data", (chunk) => {
245
+ logStream.write(chunk);
246
+ lastStderr = chunk.toString().trim();
247
+ });
151
248
  if (child.pid) {
152
249
  this.saveConnectorPid(agentDir2, type, child.pid);
153
250
  }
@@ -165,6 +262,7 @@ var ConnectorManager = class {
165
262
  if (this.shuttingDown) return;
166
263
  if (this.stopping.has(stopKey)) return;
167
264
  console.error(`[daemon] connector ${type} for ${agentName} exited with code ${code}`);
265
+ if (lastStderr) console.error(`[daemon] last output: ${lastStderr}`);
168
266
  const attempts = this.restartAttempts.get(stopKey) ?? 0;
169
267
  if (attempts >= MAX_RESTART_ATTEMPTS) {
170
268
  console.error(
@@ -194,19 +292,19 @@ var ConnectorManager = class {
194
292
  const stopKey = `${agentName}:${type}`;
195
293
  this.stopping.add(stopKey);
196
294
  agentMap.delete(type);
197
- await new Promise((resolve9) => {
198
- tracked.child.on("exit", () => resolve9());
295
+ await new Promise((resolve10) => {
296
+ tracked.child.on("exit", () => resolve10());
199
297
  try {
200
298
  tracked.child.kill("SIGTERM");
201
299
  } catch {
202
- resolve9();
300
+ resolve10();
203
301
  }
204
302
  setTimeout(() => {
205
303
  try {
206
304
  tracked.child.kill("SIGKILL");
207
305
  } catch {
208
306
  }
209
- resolve9();
307
+ resolve10();
210
308
  }, 5e3);
211
309
  });
212
310
  this.stopping.delete(stopKey);
@@ -238,7 +336,7 @@ var ConnectorManager = class {
238
336
  }));
239
337
  }
240
338
  connectorPidPath(agentDir2, type) {
241
- return resolve(agentDir2, ".volute", "connectors", `${type}.pid`);
339
+ return resolve2(agentDir2, ".volute", "connectors", `${type}.pid`);
242
340
  }
243
341
  saveConnectorPid(agentDir2, type, pid) {
244
342
  const pidPath = this.connectorPidPath(agentDir2, type);
@@ -253,9 +351,9 @@ var ConnectorManager = class {
253
351
  }
254
352
  killOrphanConnector(agentDir2, type) {
255
353
  const pidPath = this.connectorPidPath(agentDir2, type);
256
- if (!existsSync(pidPath)) return;
354
+ if (!existsSync2(pidPath)) return;
257
355
  try {
258
- const pid = parseInt(readFileSync(pidPath, "utf-8").trim(), 10);
356
+ const pid = parseInt(readFileSync2(pidPath, "utf-8").trim(), 10);
259
357
  if (pid > 0) {
260
358
  process.kill(pid, "SIGTERM");
261
359
  console.error(`[daemon] killed orphan connector ${type} (pid ${pid})`);
@@ -286,7 +384,7 @@ function getConnectorManager() {
286
384
  }
287
385
 
288
386
  // src/lib/scheduler.ts
289
- import { resolve as resolve2 } from "path";
387
+ import { resolve as resolve3 } from "path";
290
388
  import { CronExpressionParser } from "cron-parser";
291
389
  var Scheduler = class {
292
390
  schedules = /* @__PURE__ */ new Map();
@@ -296,7 +394,7 @@ var Scheduler = class {
296
394
  daemonPort = null;
297
395
  daemonToken = null;
298
396
  get statePath() {
299
- return resolve2(voluteHome(), "scheduler-state.json");
397
+ return resolve3(voluteHome(), "scheduler-state.json");
300
398
  }
301
399
  start(daemonPort, daemonToken) {
302
400
  this.daemonPort = daemonPort ?? null;
@@ -374,10 +472,12 @@ var Scheduler = class {
374
472
  channel: "system:scheduler",
375
473
  sender: schedule.id
376
474
  });
475
+ const controller = new AbortController();
476
+ const timeout = setTimeout(() => controller.abort(), 12e4);
377
477
  try {
378
478
  let res;
379
479
  if (this.daemonPort && this.daemonToken) {
380
- const daemonUrl = `http://localhost:${this.daemonPort}`;
480
+ const daemonUrl = `http://127.0.0.1:${this.daemonPort}`;
381
481
  res = await fetch(`${daemonUrl}/api/agents/${encodeURIComponent(agentName)}/message`, {
382
482
  method: "POST",
383
483
  headers: {
@@ -385,13 +485,15 @@ var Scheduler = class {
385
485
  Authorization: `Bearer ${this.daemonToken}`,
386
486
  Origin: daemonUrl
387
487
  },
388
- body
488
+ body,
489
+ signal: controller.signal
389
490
  });
390
491
  } else {
391
- res = await fetch(`http://localhost:${entry.port}/message`, {
492
+ res = await fetch(`http://127.0.0.1:${entry.port}/message`, {
392
493
  method: "POST",
393
494
  headers: { "Content-Type": "application/json" },
394
- body
495
+ body,
496
+ signal: controller.signal
395
497
  });
396
498
  }
397
499
  if (!res.ok) {
@@ -399,8 +501,14 @@ var Scheduler = class {
399
501
  } else {
400
502
  console.error(`[scheduler] fired "${schedule.id}" for ${agentName}`);
401
503
  }
504
+ try {
505
+ await res.body?.cancel();
506
+ } catch {
507
+ }
402
508
  } catch (err) {
403
509
  console.error(`[scheduler] failed to fire "${schedule.id}" for ${agentName}:`, err);
510
+ } finally {
511
+ clearTimeout(timeout);
404
512
  }
405
513
  }
406
514
  };
@@ -421,8 +529,8 @@ import { compareSync, hashSync } from "bcryptjs";
421
529
  import { and, count, eq } from "drizzle-orm";
422
530
 
423
531
  // src/lib/db.ts
424
- import { chmodSync, existsSync as existsSync2 } from "fs";
425
- import { dirname as dirname2, resolve as resolve3 } from "path";
532
+ import { chmodSync, existsSync as existsSync3 } from "fs";
533
+ import { dirname as dirname2, resolve as resolve4 } from "path";
426
534
  import { fileURLToPath } from "url";
427
535
  import { drizzle } from "drizzle-orm/libsql";
428
536
  import { migrate } from "drizzle-orm/libsql/migrator";
@@ -498,11 +606,11 @@ var messages = sqliteTable(
498
606
 
499
607
  // src/lib/db.ts
500
608
  var __dirname = dirname2(fileURLToPath(import.meta.url));
501
- var migrationsFolder = existsSync2(resolve3(__dirname, "../drizzle")) ? resolve3(__dirname, "../drizzle") : resolve3(__dirname, "../../drizzle");
609
+ var migrationsFolder = existsSync3(resolve4(__dirname, "../drizzle")) ? resolve4(__dirname, "../drizzle") : resolve4(__dirname, "../../drizzle");
502
610
  var db = null;
503
611
  async function getDb() {
504
612
  if (db) return db;
505
- const dbPath = process.env.VOLUTE_DB_PATH || resolve3(voluteHome(), "volute.db");
613
+ const dbPath = process.env.VOLUTE_DB_PATH || resolve4(voluteHome(), "volute.db");
506
614
  db = drizzle({ connection: { url: `file:${dbPath}` }, schema: schema_exports });
507
615
  await migrate(db, { migrationsFolder });
508
616
  try {
@@ -642,9 +750,9 @@ var authMiddleware = createMiddleware(async (c, next) => {
642
750
  });
643
751
 
644
752
  // src/web/server.ts
645
- import { existsSync as existsSync6 } from "fs";
753
+ import { existsSync as existsSync7 } from "fs";
646
754
  import { readFile as readFile2, stat } from "fs/promises";
647
- import { dirname as dirname3, extname, resolve as resolve7 } from "path";
755
+ import { dirname as dirname3, extname, resolve as resolve8 } from "path";
648
756
  import { serve } from "@hono/node-server";
649
757
 
650
758
  // src/web/app.ts
@@ -654,25 +762,14 @@ import { csrf } from "hono/csrf";
654
762
  import { HTTPException } from "hono/http-exception";
655
763
 
656
764
  // src/web/routes/agents.ts
657
- import { existsSync as existsSync3, readFileSync as readFileSync2, rmSync } from "fs";
658
- import { resolve as resolve4 } from "path";
765
+ import { existsSync as existsSync4, readFileSync as readFileSync3, rmSync } from "fs";
766
+ import { resolve as resolve5 } from "path";
659
767
  import { and as and2, desc, eq as eq3 } from "drizzle-orm";
660
768
  import { Hono } from "hono";
661
769
  import { stream } from "hono/streaming";
662
-
663
- // src/lib/channels.ts
664
- var CHANNELS = {
665
- web: { name: "web", displayName: "Web UI", showToolCalls: true },
666
- discord: { name: "discord", displayName: "Discord", showToolCalls: false },
667
- cli: { name: "cli", displayName: "CLI", showToolCalls: true },
668
- agent: { name: "agent", displayName: "Agent", showToolCalls: true },
669
- system: { name: "system", displayName: "System", showToolCalls: false }
670
- };
671
-
672
- // src/web/routes/agents.ts
673
770
  function getDaemonPort() {
674
771
  try {
675
- const data = JSON.parse(readFileSync2(resolve4(voluteHome(), "daemon.json"), "utf-8"));
772
+ const data = JSON.parse(readFileSync3(resolve5(voluteHome(), "daemon.json"), "utf-8"));
676
773
  return data.port;
677
774
  } catch {
678
775
  return void 0;
@@ -717,7 +814,7 @@ var app = new Hono().get("/", async (c) => {
717
814
  const name = c.req.param("name");
718
815
  const entry = findAgent(name);
719
816
  if (!entry) return c.json({ error: "Agent not found" }, 404);
720
- if (!existsSync3(agentDir(name))) return c.json({ error: "Agent directory missing" }, 404);
817
+ if (!existsSync4(agentDir(name))) return c.json({ error: "Agent directory missing" }, 404);
721
818
  const { status, channels } = await getAgentStatus(name, entry.port);
722
819
  const variants = readVariants(name);
723
820
  const manager = getAgentManager();
@@ -743,7 +840,7 @@ var app = new Hono().get("/", async (c) => {
743
840
  if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
744
841
  } else {
745
842
  const dir = agentDir(baseName);
746
- if (!existsSync3(dir)) return c.json({ error: "Agent directory missing" }, 404);
843
+ if (!existsSync4(dir)) return c.json({ error: "Agent directory missing" }, 404);
747
844
  }
748
845
  const manager = getAgentManager();
749
846
  if (manager.isRunning(name)) {
@@ -770,7 +867,7 @@ var app = new Hono().get("/", async (c) => {
770
867
  if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
771
868
  } else {
772
869
  const dir = agentDir(baseName);
773
- if (!existsSync3(dir)) return c.json({ error: "Agent directory missing" }, 404);
870
+ if (!existsSync4(dir)) return c.json({ error: "Agent directory missing" }, 404);
774
871
  }
775
872
  const manager = getAgentManager();
776
873
  const connectorManager = getConnectorManager();
@@ -825,7 +922,7 @@ var app = new Hono().get("/", async (c) => {
825
922
  }
826
923
  removeAllVariants(name);
827
924
  removeAgent(name);
828
- if (force && existsSync3(dir)) {
925
+ if (force && existsSync4(dir)) {
829
926
  rmSync(dir, { recursive: true, force: true });
830
927
  }
831
928
  return c.json({ ok: true });
@@ -847,14 +944,22 @@ var app = new Hono().get("/", async (c) => {
847
944
  let parsed = null;
848
945
  try {
849
946
  parsed = JSON.parse(body);
850
- } catch {
947
+ } catch (err) {
948
+ console.error(`[daemon] failed to parse message body for ${baseName}:`, err);
851
949
  }
852
950
  const channel = parsed?.channel ?? "unknown";
853
951
  const db2 = await getDb();
854
952
  if (parsed) {
855
953
  try {
856
954
  const sender = parsed.sender ?? null;
857
- const content = typeof parsed.content === "string" ? parsed.content : JSON.stringify(parsed.content);
955
+ let content;
956
+ if (typeof parsed.content === "string") {
957
+ content = parsed.content;
958
+ } else if (Array.isArray(parsed.content)) {
959
+ content = parsed.content.filter((p) => p.type === "text" && p.text).map((p) => p.text).join("\n");
960
+ } else {
961
+ content = JSON.stringify(parsed.content);
962
+ }
858
963
  await db2.insert(agentMessages).values({
859
964
  agent: baseName,
860
965
  channel,
@@ -866,11 +971,17 @@ var app = new Hono().get("/", async (c) => {
866
971
  console.error(`[daemon] failed to persist inbound message for ${baseName}:`, err);
867
972
  }
868
973
  }
869
- const res = await fetch(`http://localhost:${port}/message`, {
870
- method: "POST",
871
- headers: { "Content-Type": "application/json" },
872
- body
873
- });
974
+ let res;
975
+ try {
976
+ res = await fetch(`http://127.0.0.1:${port}/message`, {
977
+ method: "POST",
978
+ headers: { "Content-Type": "application/json" },
979
+ body
980
+ });
981
+ } catch (err) {
982
+ console.error(`[daemon] agent ${name} unreachable on port ${port}:`, err);
983
+ return c.json({ error: "Agent is not reachable" }, 502);
984
+ }
874
985
  if (!res.ok) {
875
986
  return c.json({ error: `Agent responded with ${res.status}` }, res.status);
876
987
  }
@@ -878,57 +989,39 @@ var app = new Hono().get("/", async (c) => {
878
989
  return c.json({ error: "No response body from agent" }, 502);
879
990
  }
880
991
  c.header("Content-Type", "application/x-ndjson");
992
+ const encoder = new TextEncoder();
881
993
  return stream(c, async (s) => {
882
- const reader = res.body.getReader();
883
- const decoder = new TextDecoder();
884
- let buffer = "";
885
994
  const textParts = [];
886
- try {
887
- while (true) {
888
- const { done, value } = await reader.read();
889
- if (done) break;
890
- await s.write(value);
891
- buffer += decoder.decode(value, { stream: true });
892
- const lines = buffer.split("\n");
893
- buffer = lines.pop() || "";
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
- }
995
+ const toolParts = [];
996
+ for await (const event of readNdjson(res.body)) {
997
+ await s.write(encoder.encode(`${JSON.stringify(event)}
998
+ `));
999
+ const part = collectPart(event);
1000
+ if (part != null) {
1001
+ if (event.type === "tool_use") toolParts.push(part);
1002
+ else textParts.push(part);
915
1003
  }
916
- if (textParts.length > 0) {
917
- try {
918
- await db2.insert(agentMessages).values({
919
- agent: baseName,
920
- channel,
921
- role: "assistant",
922
- content: textParts.join("")
923
- });
924
- } catch (err) {
925
- console.error(`[daemon] failed to persist assistant response for ${baseName}:`, err);
926
- }
1004
+ }
1005
+ const content = [textParts.join(""), ...toolParts].filter(Boolean).join("\n");
1006
+ if (content) {
1007
+ try {
1008
+ await db2.insert(agentMessages).values({
1009
+ agent: baseName,
1010
+ channel,
1011
+ role: "assistant",
1012
+ sender: baseName,
1013
+ content
1014
+ });
1015
+ } catch (err) {
1016
+ console.error(`[daemon] failed to persist assistant response for ${baseName}:`, err);
927
1017
  }
928
- } finally {
929
- reader.releaseLock();
930
1018
  }
931
1019
  });
1020
+ }).get("/:name/history/channels", async (c) => {
1021
+ const name = c.req.param("name");
1022
+ const db2 = await getDb();
1023
+ const rows = await db2.selectDistinct({ channel: agentMessages.channel }).from(agentMessages).where(eq3(agentMessages.agent, name));
1024
+ return c.json(rows.map((r) => r.channel));
932
1025
  }).get("/:name/history", async (c) => {
933
1026
  const name = c.req.param("name");
934
1027
  const channel = c.req.query("channel");
@@ -1116,7 +1209,7 @@ var app3 = new Hono3().post("/:name/chat", zValidator2("json", chatSchema), asyn
1116
1209
  if (!variant) return c.json({ error: `Unknown variant: ${variantName}` }, 404);
1117
1210
  port = variant.port;
1118
1211
  }
1119
- const { getAgentManager: getAgentManager2 } = await import("./agent-manager-2LU6KULR.js");
1212
+ const { getAgentManager: getAgentManager2 } = await import("./agent-manager-AUCKMGPR.js");
1120
1213
  if (!getAgentManager2().isRunning(name)) {
1121
1214
  return c.json({ error: "Agent is not running" }, 409);
1122
1215
  }
@@ -1155,15 +1248,21 @@ var app3 = new Hono3().post("/:name/chat", zValidator2("json", chatSchema), asyn
1155
1248
  sender: user.username,
1156
1249
  content: body.message ?? "[image]"
1157
1250
  });
1158
- const res = await fetch(`http://localhost:${port}/message`, {
1159
- method: "POST",
1160
- headers: { "Content-Type": "application/json" },
1161
- body: JSON.stringify({
1162
- content: contentBlocks,
1163
- channel: "web",
1164
- sender: user.username
1165
- })
1166
- });
1251
+ let res;
1252
+ try {
1253
+ res = await fetch(`http://127.0.0.1:${port}/message`, {
1254
+ method: "POST",
1255
+ headers: { "Content-Type": "application/json" },
1256
+ body: JSON.stringify({
1257
+ content: contentBlocks,
1258
+ channel: "web",
1259
+ sender: user.username
1260
+ })
1261
+ });
1262
+ } catch (err) {
1263
+ console.error(`[chat] agent ${name} unreachable on port ${port}:`, err);
1264
+ return c.json({ error: "Agent is not reachable" }, 502);
1265
+ }
1167
1266
  if (!res.ok) {
1168
1267
  return c.json({ error: `Agent responded with ${res.status}` }, res.status);
1169
1268
  }
@@ -1200,13 +1299,23 @@ var app3 = new Hono3().post("/:name/chat", zValidator2("json", chatSchema), asyn
1200
1299
  if (event.type === "done") {
1201
1300
  if (assistantContent.length > 0) {
1202
1301
  await addMessage(conversationId, "assistant", baseName, assistantContent);
1203
- const textParts = assistantContent.filter((b) => b.type === "text").map((b) => b.text);
1204
- if (textParts.length > 0) {
1302
+ const textParts = [];
1303
+ const toolParts = [];
1304
+ for (const b of assistantContent) {
1305
+ const part = collectPart(b);
1306
+ if (part != null) {
1307
+ if (b.type === "tool_use") toolParts.push(part);
1308
+ else textParts.push(part);
1309
+ }
1310
+ }
1311
+ const summary = [textParts.join(""), ...toolParts].filter(Boolean).join("\n");
1312
+ if (summary) {
1205
1313
  await db2.insert(agentMessages).values({
1206
1314
  agent: baseName,
1207
1315
  channel: "web",
1208
1316
  role: "assistant",
1209
- content: textParts.join("")
1317
+ sender: baseName,
1318
+ content: summary
1210
1319
  });
1211
1320
  }
1212
1321
  }
@@ -1243,13 +1352,24 @@ var app4 = new Hono4().get("/:name/connectors", (c) => {
1243
1352
  const entry = findAgent(name);
1244
1353
  if (!entry) return c.json({ error: "Agent not found" }, 404);
1245
1354
  const dir = agentDir(name);
1355
+ const manager = getConnectorManager();
1356
+ const envCheck = manager.checkConnectorEnv(type, dir);
1357
+ if (envCheck) {
1358
+ return c.json(
1359
+ {
1360
+ error: "missing_env",
1361
+ missing: envCheck.missing,
1362
+ connectorName: envCheck.connectorName
1363
+ },
1364
+ 400
1365
+ );
1366
+ }
1246
1367
  const config = readVoluteConfig(dir) ?? {};
1247
1368
  const connectors = config.connectors ?? [];
1248
1369
  if (!connectors.includes(type)) {
1249
1370
  config.connectors = [...connectors, type];
1250
1371
  writeVoluteConfig(dir, config);
1251
1372
  }
1252
- const manager = getConnectorManager();
1253
1373
  try {
1254
1374
  await manager.startConnector(name, dir, entry.port, type);
1255
1375
  return c.json({ ok: true });
@@ -1301,9 +1421,9 @@ var app5 = new Hono5().get("/:name/conversations", async (c) => {
1301
1421
  var conversations_default = app5;
1302
1422
 
1303
1423
  // src/web/routes/files.ts
1304
- import { existsSync as existsSync4 } from "fs";
1424
+ import { existsSync as existsSync5 } from "fs";
1305
1425
  import { readdir, readFile, writeFile } from "fs/promises";
1306
- import { resolve as resolve5 } from "path";
1426
+ import { resolve as resolve6 } from "path";
1307
1427
  import { zValidator as zValidator3 } from "@hono/zod-validator";
1308
1428
  import { Hono as Hono6 } from "hono";
1309
1429
  import { z as z3 } from "zod";
@@ -1314,8 +1434,8 @@ var app6 = new Hono6().get("/:name/files", async (c) => {
1314
1434
  const entry = findAgent(name);
1315
1435
  if (!entry) return c.json({ error: "Agent not found" }, 404);
1316
1436
  const dir = agentDir(name);
1317
- const homeDir = resolve5(dir, "home");
1318
- if (!existsSync4(homeDir)) return c.json({ error: "Home directory missing" }, 404);
1437
+ const homeDir = resolve6(dir, "home");
1438
+ if (!existsSync5(homeDir)) return c.json({ error: "Home directory missing" }, 404);
1319
1439
  const allFiles = await readdir(homeDir);
1320
1440
  const files = allFiles.filter((f) => f.endsWith(".md") && ALLOWED_FILES.has(f));
1321
1441
  return c.json(files);
@@ -1328,8 +1448,8 @@ var app6 = new Hono6().get("/:name/files", async (c) => {
1328
1448
  const entry = findAgent(name);
1329
1449
  if (!entry) return c.json({ error: "Agent not found" }, 404);
1330
1450
  const dir = agentDir(name);
1331
- const filePath = resolve5(dir, "home", filename);
1332
- if (!existsSync4(filePath)) {
1451
+ const filePath = resolve6(dir, "home", filename);
1452
+ if (!existsSync5(filePath)) {
1333
1453
  return c.json({ error: "File not found" }, 404);
1334
1454
  }
1335
1455
  const content = await readFile(filePath, "utf-8");
@@ -1343,7 +1463,7 @@ var app6 = new Hono6().get("/:name/files", async (c) => {
1343
1463
  const entry = findAgent(name);
1344
1464
  if (!entry) return c.json({ error: "Agent not found" }, 404);
1345
1465
  const dir = agentDir(name);
1346
- const filePath = resolve5(dir, "home", filename);
1466
+ const filePath = resolve6(dir, "home", filename);
1347
1467
  const { content } = c.req.valid("json");
1348
1468
  await writeFile(filePath, content);
1349
1469
  return c.json({ ok: true });
@@ -1352,8 +1472,8 @@ var files_default = app6;
1352
1472
 
1353
1473
  // src/web/routes/logs.ts
1354
1474
  import { spawn as spawn2 } from "child_process";
1355
- import { existsSync as existsSync5 } from "fs";
1356
- import { resolve as resolve6 } from "path";
1475
+ import { existsSync as existsSync6 } from "fs";
1476
+ import { resolve as resolve7 } from "path";
1357
1477
  import { Hono as Hono7 } from "hono";
1358
1478
  import { streamSSE as streamSSE2 } from "hono/streaming";
1359
1479
  var app7 = new Hono7().get("/:name/logs", async (c) => {
@@ -1361,8 +1481,8 @@ var app7 = new Hono7().get("/:name/logs", async (c) => {
1361
1481
  const entry = findAgent(name);
1362
1482
  if (!entry) return c.json({ error: "Agent not found" }, 404);
1363
1483
  const dir = agentDir(name);
1364
- const logFile = resolve6(dir, ".volute", "logs", "agent.log");
1365
- if (!existsSync5(logFile)) {
1484
+ const logFile = resolve7(dir, ".volute", "logs", "agent.log");
1485
+ if (!existsSync6(logFile)) {
1366
1486
  return c.json({ error: "No log file found" }, 404);
1367
1487
  }
1368
1488
  return streamSSE2(c, async (stream2) => {
@@ -1380,9 +1500,9 @@ var app7 = new Hono7().get("/:name/logs", async (c) => {
1380
1500
  stream2.onAbort(() => {
1381
1501
  tail.kill();
1382
1502
  });
1383
- await new Promise((resolve9) => {
1384
- tail.on("exit", resolve9);
1385
- stream2.onAbort(resolve9);
1503
+ await new Promise((resolve10) => {
1504
+ tail.on("exit", resolve10);
1505
+ stream2.onAbort(resolve10);
1386
1506
  });
1387
1507
  });
1388
1508
  });
@@ -1451,7 +1571,7 @@ var app8 = new Hono8().get("/:name/schedules", (c) => {
1451
1571
  const body = await c.req.text();
1452
1572
  const message = `[webhook: ${event}] ${body}`;
1453
1573
  try {
1454
- const res = await fetch(`http://localhost:${entry.port}/message`, {
1574
+ const res = await fetch(`http://127.0.0.1:${entry.port}/message`, {
1455
1575
  method: "POST",
1456
1576
  headers: { "Content-Type": "application/json" },
1457
1577
  body: JSON.stringify({
@@ -1484,10 +1604,10 @@ var app9 = new Hono9().get("/logs", async (c) => {
1484
1604
  stream2.writeSSE({ data: JSON.stringify(entry) }).catch(() => {
1485
1605
  });
1486
1606
  });
1487
- await new Promise((resolve9) => {
1607
+ await new Promise((resolve10) => {
1488
1608
  stream2.onAbort(() => {
1489
1609
  unsubscribe();
1490
- resolve9();
1610
+ resolve10();
1491
1611
  });
1492
1612
  });
1493
1613
  });
@@ -1566,8 +1686,8 @@ async function startServer({
1566
1686
  let assetsDir = "";
1567
1687
  let searchDir = dirname3(new URL(import.meta.url).pathname);
1568
1688
  for (let i = 0; i < 5; i++) {
1569
- const candidate = resolve7(searchDir, "dist", "web-assets");
1570
- if (existsSync6(candidate)) {
1689
+ const candidate = resolve8(searchDir, "dist", "web-assets");
1690
+ if (existsSync7(candidate)) {
1571
1691
  assetsDir = candidate;
1572
1692
  break;
1573
1693
  }
@@ -1576,7 +1696,7 @@ async function startServer({
1576
1696
  if (assetsDir) {
1577
1697
  app_default.get("*", async (c) => {
1578
1698
  const urlPath = new URL(c.req.url).pathname;
1579
- const filePath = resolve7(assetsDir, urlPath.slice(1));
1699
+ const filePath = resolve8(assetsDir, urlPath.slice(1));
1580
1700
  if (!filePath.startsWith(assetsDir)) return c.text("Forbidden", 403);
1581
1701
  const s = await stat(filePath).catch(() => null);
1582
1702
  if (s?.isFile()) {
@@ -1585,7 +1705,7 @@ async function startServer({
1585
1705
  const body = await readFile2(filePath);
1586
1706
  return c.body(body, 200, { "Content-Type": mime });
1587
1707
  }
1588
- const indexPath = resolve7(assetsDir, "index.html");
1708
+ const indexPath = resolve8(assetsDir, "index.html");
1589
1709
  const indexStat = await stat(indexPath).catch(() => null);
1590
1710
  if (indexStat?.isFile()) {
1591
1711
  const body = await readFile2(indexPath, "utf-8");
@@ -1595,10 +1715,10 @@ async function startServer({
1595
1715
  });
1596
1716
  }
1597
1717
  const server = serve({ fetch: app_default.fetch, port, hostname });
1598
- await new Promise((resolve9, reject) => {
1718
+ await new Promise((resolve10, reject) => {
1599
1719
  server.on("listening", () => {
1600
1720
  logger_default.info("Volute UI running", { hostname, port });
1601
- resolve9();
1721
+ resolve10();
1602
1722
  });
1603
1723
  server.on("error", (err) => {
1604
1724
  reject(err);
@@ -1612,8 +1732,8 @@ async function startDaemon(opts) {
1612
1732
  const { port, hostname } = opts;
1613
1733
  const myPid = String(process.pid);
1614
1734
  const home = voluteHome();
1615
- const DAEMON_PID_PATH = resolve8(home, "daemon.pid");
1616
- const DAEMON_JSON_PATH = resolve8(home, "daemon.json");
1735
+ const DAEMON_PID_PATH = resolve9(home, "daemon.pid");
1736
+ const DAEMON_JSON_PATH = resolve9(home, "daemon.json");
1617
1737
  mkdirSync2(home, { recursive: true });
1618
1738
  const token = process.env.VOLUTE_DAEMON_TOKEN || randomBytes(32).toString("hex");
1619
1739
  process.env.VOLUTE_DAEMON_TOKEN = token;
@@ -1666,13 +1786,13 @@ async function startDaemon(opts) {
1666
1786
  console.error(`[daemon] running on ${hostname}:${port}, pid ${myPid}`);
1667
1787
  function cleanup() {
1668
1788
  try {
1669
- if (readFileSync3(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
1789
+ if (readFileSync4(DAEMON_PID_PATH, "utf-8").trim() === myPid) {
1670
1790
  unlinkSync2(DAEMON_PID_PATH);
1671
1791
  }
1672
1792
  } catch {
1673
1793
  }
1674
1794
  try {
1675
- const data = JSON.parse(readFileSync3(DAEMON_JSON_PATH, "utf-8"));
1795
+ const data = JSON.parse(readFileSync4(DAEMON_JSON_PATH, "utf-8"));
1676
1796
  if (data.token === token) {
1677
1797
  unlinkSync2(DAEMON_JSON_PATH);
1678
1798
  }