volute 0.7.0 → 0.8.1

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 (77) hide show
  1. package/README.md +16 -14
  2. package/dist/{agent-7JF7MT73.js → agent-YORVRB6I.js} +10 -10
  3. package/dist/{agent-manager-IMZ7ZMBF.js → agent-manager-CMMH5KQQ.js} +4 -4
  4. package/dist/{channel-SMCNOIVQ.js → channel-RDGHBFSI.js} +16 -56
  5. package/dist/{chunk-JR4UXCTO.js → chunk-23L3MKEV.js} +1 -1
  6. package/dist/{chunk-5SKQ6J7T.js → chunk-5C5JWR2L.js} +15 -7
  7. package/dist/{chunk-UWHWAPGO.js → chunk-DP2DX4WV.js} +9 -1
  8. package/dist/{chunk-7ACDT3P2.js → chunk-ECPQXRLB.js} +1 -2
  9. package/dist/{chunk-LLJNZPCU.js → chunk-HZ5LTOEJ.js} +1 -1
  10. package/dist/{chunk-W76KWE23.js → chunk-IQXBMFZG.js} +6 -4
  11. package/dist/{chunk-ZZOOTYXK.js → chunk-LIPPXNIE.js} +60 -74
  12. package/dist/{chunk-BX7KI4S3.js → chunk-N6MLQ26B.js} +23 -96
  13. package/dist/{chunk-H7AMDUIA.js → chunk-QF22MYDJ.js} +6 -5
  14. package/dist/{chunk-NKXULRSW.js → chunk-RT6Y7AR3.js} +1 -1
  15. package/dist/{chunk-62X577Y7.js → chunk-W6TMWYU3.js} +126 -73
  16. package/dist/{chunk-EG45HBSJ.js → chunk-XSJ27WEM.js} +1 -1
  17. package/dist/cli.js +22 -20
  18. package/dist/{connector-Y7JPNROO.js → connector-ZP6MEFF4.js} +3 -3
  19. package/dist/connectors/discord.js +18 -59
  20. package/dist/connectors/slack.js +21 -38
  21. package/dist/connectors/telegram.js +31 -49
  22. package/dist/{create-G525LWEA.js → create-HGJHLABX.js} +22 -17
  23. package/dist/{daemon-client-442IV43D.js → daemon-client-54J3EIZD.js} +2 -2
  24. package/dist/{daemon-restart-4HVEKYFY.js → daemon-restart-CPBLMMRI.js} +3 -3
  25. package/dist/daemon.js +342 -402
  26. package/dist/{delete-UOU4AFQN.js → delete-45TGQC4N.js} +10 -5
  27. package/dist/{down-AZVH5TCD.js → down-O4EWZTVA.js} +2 -2
  28. package/dist/{env-7GLUJCWS.js → env-KMNYGVZ2.js} +7 -9
  29. package/dist/{history-H72ZUIBN.js → history-PXJVYLVY.js} +2 -2
  30. package/dist/{import-AVKQJDYC.js → import-CNEDF3TD.js} +6 -6
  31. package/dist/{logs-EDGK26AK.js → logs-TZB3MTLZ.js} +5 -4
  32. package/dist/{package-T2WAVJOU.js → package-RJSONENE.js} +1 -1
  33. package/dist/{restart-O4ETYLJF.js → restart-KVH3TK5N.js} +2 -2
  34. package/dist/{schedule-S6QVC5ON.js → schedule-HCUCBNQI.js} +2 -2
  35. package/dist/send-BNC2S5BY.js +162 -0
  36. package/dist/{service-HZNIDNJF.js → service-XCADRKIS.js} +8 -1
  37. package/dist/{setup-F4TCWVSP.js → setup-32KH5KLN.js} +85 -26
  38. package/dist/{start-VHQ7LNWM.js → start-QU73YTJW.js} +2 -2
  39. package/dist/{status-QAJWXKMZ.js → status-Q6ZQJXNI.js} +2 -2
  40. package/dist/{stop-CAGCT5NI.js → stop-N7U5N6A7.js} +2 -2
  41. package/dist/{up-RWZF6MLT.js → up-V6EAA7OZ.js} +2 -2
  42. package/dist/{update-F7QWV2LB.js → update-EUCZ7XGG.js} +3 -3
  43. package/dist/{update-check-B4J6IEQ4.js → update-check-SM4244SU.js} +2 -2
  44. package/dist/{upgrade-YXKPWDRU.js → upgrade-CZF6PN7Y.js} +4 -4
  45. package/dist/{variant-4Z6W3PP6.js → variant-RKXPN5DH.js} +20 -46
  46. package/dist/web-assets/assets/index-D-3zx6vs.js +307 -0
  47. package/dist/web-assets/index.html +1 -1
  48. package/drizzle/0004_magical_silverclaw.sql +1 -0
  49. package/drizzle/meta/0004_snapshot.json +410 -0
  50. package/drizzle/meta/_journal.json +7 -0
  51. package/package.json +1 -1
  52. package/templates/_base/_skills/volute-agent/SKILL.md +32 -16
  53. package/templates/_base/home/.config/routes.json +4 -8
  54. package/templates/_base/home/VOLUTE.md +16 -14
  55. package/templates/_base/src/lib/auto-reply.ts +38 -0
  56. package/templates/_base/src/lib/daemon-client.ts +53 -0
  57. package/templates/_base/src/lib/router.ts +66 -14
  58. package/templates/_base/src/lib/routing.ts +48 -9
  59. package/templates/_base/src/lib/startup.ts +1 -25
  60. package/templates/_base/src/lib/types.ts +2 -1
  61. package/templates/_base/src/lib/volute-server.ts +29 -14
  62. package/templates/agent-sdk/src/agent.ts +53 -111
  63. package/templates/agent-sdk/src/lib/content.ts +41 -0
  64. package/templates/agent-sdk/src/lib/session-store.ts +43 -0
  65. package/templates/agent-sdk/src/lib/stream-consumer.ts +66 -0
  66. package/templates/agent-sdk/src/server.ts +5 -13
  67. package/templates/pi/.init/AGENTS.md +5 -5
  68. package/templates/pi/src/agent.ts +32 -84
  69. package/templates/pi/src/lib/content.ts +15 -0
  70. package/templates/pi/src/lib/event-handler.ts +74 -0
  71. package/templates/pi/src/lib/resolve-model.ts +21 -0
  72. package/templates/pi/src/server.ts +3 -7
  73. package/dist/chunk-B3R6L2GW.js +0 -24
  74. package/dist/chunk-ZYGKG6VC.js +0 -22
  75. package/dist/message-SCOQDR3P.js +0 -32
  76. package/dist/send-G7PE4DOJ.js +0 -72
  77. package/dist/web-assets/assets/index-B1CqjUYD.js +0 -308
@@ -1,23 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  loadMergedEnv
4
- } from "./chunk-H7AMDUIA.js";
4
+ } from "./chunk-QF22MYDJ.js";
5
5
  import {
6
6
  applyIsolation
7
- } from "./chunk-W76KWE23.js";
7
+ } from "./chunk-IQXBMFZG.js";
8
8
  import {
9
9
  agentDir,
10
10
  findAgent,
11
11
  findVariant,
12
12
  setAgentRunning,
13
13
  setVariantRunning,
14
- validateBranchName,
14
+ stateDir,
15
15
  voluteHome
16
- } from "./chunk-UWHWAPGO.js";
16
+ } from "./chunk-DP2DX4WV.js";
17
17
 
18
18
  // src/lib/agent-manager.ts
19
19
  import { execFile, spawn } from "child_process";
20
- import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, unlinkSync as unlinkSync2 } from "fs";
20
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
21
21
  import { resolve } from "path";
22
22
  import { promisify } from "util";
23
23
 
@@ -59,14 +59,21 @@ function clearJsonMap(path, map) {
59
59
  }
60
60
 
61
61
  // src/lib/rotating-log.ts
62
- import { createWriteStream, existsSync as existsSync2, renameSync, statSync } from "fs";
62
+ import {
63
+ createWriteStream,
64
+ existsSync as existsSync2,
65
+ renameSync,
66
+ rmSync,
67
+ statSync
68
+ } from "fs";
63
69
  import { Writable } from "stream";
64
70
  var MAX_SIZE = 10 * 1024 * 1024;
65
71
  var RotatingLog = class extends Writable {
66
- constructor(path, maxSize = MAX_SIZE) {
72
+ constructor(path, maxSize = MAX_SIZE, maxFiles = 5) {
67
73
  super();
68
74
  this.path = path;
69
75
  this.maxSize = maxSize;
76
+ this.maxFiles = maxFiles;
70
77
  this.on("error", () => {
71
78
  });
72
79
  try {
@@ -82,6 +89,13 @@ var RotatingLog = class extends Writable {
82
89
  this.size += chunk.length;
83
90
  if (this.size > this.maxSize) {
84
91
  try {
92
+ const oldest = `${this.path}.${this.maxFiles}`;
93
+ if (existsSync2(oldest)) rmSync(oldest);
94
+ for (let i = this.maxFiles - 1; i >= 1; i--) {
95
+ const from = `${this.path}.${i}`;
96
+ const to = `${this.path}.${i + 1}`;
97
+ if (existsSync2(from)) renameSync(from, to);
98
+ }
85
99
  renameSync(this.path, `${this.path}.1`);
86
100
  const oldStream = this.stream;
87
101
  this.stream = createWriteStream(this.path);
@@ -99,6 +113,9 @@ var RotatingLog = class extends Writable {
99
113
 
100
114
  // src/lib/agent-manager.ts
101
115
  var execFileAsync = promisify(execFile);
116
+ function agentPidPath(name) {
117
+ return resolve(stateDir(name), "agent.pid");
118
+ }
102
119
  var MAX_RESTART_ATTEMPTS = 5;
103
120
  var BASE_RESTART_DELAY = 3e3;
104
121
  var MAX_RESTART_DELAY = 6e4;
@@ -107,6 +124,7 @@ var AgentManager = class {
107
124
  stopping = /* @__PURE__ */ new Set();
108
125
  shuttingDown = false;
109
126
  restartAttempts = /* @__PURE__ */ new Map();
127
+ pendingContext = /* @__PURE__ */ new Map();
110
128
  resolveTarget(name) {
111
129
  const [baseName, variantName] = name.split("@", 2);
112
130
  const entry = findAgent(baseName);
@@ -127,6 +145,34 @@ var AgentManager = class {
127
145
  const target = this.resolveTarget(name);
128
146
  const { dir, isVariant, baseName, variantName } = target;
129
147
  const port = target.port;
148
+ const pidFile = agentPidPath(name);
149
+ try {
150
+ if (existsSync3(pidFile)) {
151
+ const stalePid = parseInt(readFileSync2(pidFile, "utf-8").trim(), 10);
152
+ if (stalePid > 0) {
153
+ try {
154
+ process.kill(stalePid, 0);
155
+ const { stdout } = await execFileAsync("ps", ["-p", String(stalePid), "-o", "args="]);
156
+ if (stdout.includes("server.ts")) {
157
+ console.error(`[daemon] killing stale agent process ${stalePid} for ${name}`);
158
+ process.kill(-stalePid, "SIGTERM");
159
+ await new Promise((r) => setTimeout(r, 500));
160
+ } else {
161
+ console.error(
162
+ `[daemon] stale PID ${stalePid} for ${name} is not an agent process, skipping`
163
+ );
164
+ }
165
+ } catch (err) {
166
+ if (err.code !== "ESRCH") {
167
+ console.error(`[daemon] failed to check/kill stale process for ${name}:`, err);
168
+ }
169
+ }
170
+ }
171
+ rmSync2(pidFile, { force: true });
172
+ }
173
+ } catch (err) {
174
+ console.error(`[daemon] failed to read PID file for ${name}:`, err);
175
+ }
130
176
  try {
131
177
  const res = await fetch(`http://127.0.0.1:${port}/health`);
132
178
  if (res.ok) {
@@ -136,13 +182,18 @@ var AgentManager = class {
136
182
  }
137
183
  } catch {
138
184
  }
139
- const voluteDir = resolve(dir, ".volute");
140
- const logsDir = resolve(voluteDir, "logs");
185
+ const logsDir = resolve(stateDir(name), "logs");
141
186
  mkdirSync(logsDir, { recursive: true });
142
187
  const logStream = new RotatingLog(resolve(logsDir, "agent.log"));
143
- const agentEnv = loadMergedEnv(dir);
144
- const { VOLUTE_DAEMON_TOKEN: _, ...parentEnv } = process.env;
145
- const env = { ...parentEnv, ...agentEnv, VOLUTE_AGENT: name };
188
+ const agentEnv = loadMergedEnv(name);
189
+ const env = {
190
+ ...process.env,
191
+ ...agentEnv,
192
+ VOLUTE_AGENT: name,
193
+ VOLUTE_STATE_DIR: stateDir(name),
194
+ VOLUTE_AGENT_DIR: dir,
195
+ VOLUTE_AGENT_PORT: String(port)
196
+ };
146
197
  const tsxBin = resolve(dir, "node_modules", ".bin", "tsx");
147
198
  const spawnOpts = {
148
199
  cwd: dir,
@@ -185,82 +236,83 @@ var AgentManager = class {
185
236
  }
186
237
  throw err;
187
238
  }
239
+ if (child.pid) {
240
+ try {
241
+ writeFileSync2(pidFile, String(child.pid));
242
+ } catch (err) {
243
+ console.error(`[daemon] failed to write PID file for ${name}:`, err);
244
+ }
245
+ }
188
246
  if (this.restartAttempts.delete(name)) this.saveCrashAttempts();
189
- this.setupCrashRecovery(name, child, dir, isVariant);
247
+ this.setupCrashRecovery(name, child);
190
248
  if (isVariant) {
191
249
  setVariantRunning(baseName, variantName, true);
192
250
  } else {
193
251
  setAgentRunning(name, true);
194
252
  }
195
253
  console.error(`[daemon] started agent ${name} on port ${port}`);
254
+ await this.deliverPendingContext(name);
255
+ }
256
+ setPendingContext(name, context) {
257
+ this.pendingContext.set(name, context);
258
+ }
259
+ async deliverPendingContext(name) {
260
+ const context = this.pendingContext.get(name);
261
+ if (!context) return;
262
+ const tracked = this.agents.get(name);
263
+ if (!tracked) return;
264
+ this.pendingContext.delete(name);
265
+ const parts = [];
266
+ if (context.type === "merge" || context.type === "merged") {
267
+ parts.push(`[system] Variant "${context.name}" has been merged and you have been restarted.`);
268
+ } else {
269
+ parts.push("[system] You have been restarted.");
270
+ }
271
+ if (context.summary) parts.push(`Changes: ${context.summary}`);
272
+ if (context.justification) parts.push(`Why: ${context.justification}`);
273
+ if (context.memory) parts.push(`Context: ${context.memory}`);
274
+ try {
275
+ await fetch(`http://127.0.0.1:${tracked.port}/message`, {
276
+ method: "POST",
277
+ headers: { "Content-Type": "application/json" },
278
+ body: JSON.stringify({
279
+ content: [{ type: "text", text: parts.join("\n") }],
280
+ channel: "system"
281
+ })
282
+ });
283
+ } catch (err) {
284
+ console.error(`[daemon] failed to deliver pending context to ${name}:`, err);
285
+ }
196
286
  }
197
- setupCrashRecovery(name, child, dir, isVariant) {
287
+ setupCrashRecovery(name, child) {
198
288
  child.on("exit", async (code) => {
199
289
  this.agents.delete(name);
200
290
  if (this.shuttingDown || this.stopping.has(name)) return;
201
291
  console.error(`[daemon] agent ${name} exited with code ${code}`);
202
- const wasRestart = isVariant ? false : await this.handleRestart(name, dir);
203
- if (wasRestart) {
204
- console.error(`[daemon] restarting ${name} immediately after merge`);
205
- if (this.restartAttempts.delete(name)) this.saveCrashAttempts();
206
- this.startAgent(name).catch((err) => {
207
- console.error(`[daemon] failed to restart ${name} after merge:`, err);
208
- });
209
- } else {
210
- const attempts = this.restartAttempts.get(name) ?? 0;
211
- if (attempts >= MAX_RESTART_ATTEMPTS) {
212
- console.error(`[daemon] ${name} crashed ${attempts} times \u2014 giving up on restart`);
213
- const [base, variant] = name.split("@", 2);
214
- if (variant) {
215
- setVariantRunning(base, variant, false);
216
- } else {
217
- setAgentRunning(name, false);
218
- }
219
- return;
292
+ const attempts = this.restartAttempts.get(name) ?? 0;
293
+ if (attempts >= MAX_RESTART_ATTEMPTS) {
294
+ console.error(`[daemon] ${name} crashed ${attempts} times \u2014 giving up on restart`);
295
+ const [base, variant] = name.split("@", 2);
296
+ if (variant) {
297
+ setVariantRunning(base, variant, false);
298
+ } else {
299
+ setAgentRunning(name, false);
220
300
  }
221
- const delay = Math.min(BASE_RESTART_DELAY * 2 ** attempts, MAX_RESTART_DELAY);
222
- this.restartAttempts.set(name, attempts + 1);
223
- this.saveCrashAttempts();
224
- console.error(
225
- `[daemon] crash recovery for ${name} \u2014 attempt ${attempts + 1}/${MAX_RESTART_ATTEMPTS}, restarting in ${delay}ms`
226
- );
227
- setTimeout(() => {
228
- if (this.shuttingDown) return;
229
- this.startAgent(name).catch((err) => {
230
- console.error(`[daemon] failed to restart ${name}:`, err);
231
- });
232
- }, delay);
301
+ return;
233
302
  }
234
- });
235
- }
236
- async handleRestart(name, dir) {
237
- const restartPath = resolve(dir, ".volute", "restart.json");
238
- if (!existsSync3(restartPath)) return false;
239
- try {
240
- const signal = JSON.parse(readFileSync2(restartPath, "utf-8"));
241
- unlinkSync2(restartPath);
242
- if (signal.action === "merge" && signal.name) {
243
- const err = validateBranchName(signal.name);
244
- if (err) {
245
- console.error(`[daemon] invalid variant name in restart.json for ${name}: ${err}`);
246
- return false;
247
- }
248
- console.error(`[daemon] merging variant for ${name}: ${signal.name}`);
249
- const mergeArgs = ["merge", name, signal.name];
250
- if (signal.summary) mergeArgs.push("--summary", signal.summary);
251
- if (signal.justification) mergeArgs.push("--justification", signal.justification);
252
- if (signal.memory) mergeArgs.push("--memory", signal.memory);
253
- const { VOLUTE_DAEMON_TOKEN: _t, ...mergeEnv } = process.env;
254
- await execFileAsync("volute", mergeArgs, {
255
- cwd: dir,
256
- env: { ...mergeEnv, VOLUTE_SUPERVISOR: "1" }
303
+ const delay = Math.min(BASE_RESTART_DELAY * 2 ** attempts, MAX_RESTART_DELAY);
304
+ this.restartAttempts.set(name, attempts + 1);
305
+ this.saveCrashAttempts();
306
+ console.error(
307
+ `[daemon] crash recovery for ${name} \u2014 attempt ${attempts + 1}/${MAX_RESTART_ATTEMPTS}, restarting in ${delay}ms`
308
+ );
309
+ setTimeout(() => {
310
+ if (this.shuttingDown) return;
311
+ this.startAgent(name).catch((err) => {
312
+ console.error(`[daemon] failed to restart ${name}:`, err);
257
313
  });
258
- }
259
- return true;
260
- } catch (e) {
261
- console.error(`[daemon] failed to handle restart for ${name}:`, e);
262
- return false;
263
- }
314
+ }, delay);
315
+ });
264
316
  }
265
317
  async stopAgent(name) {
266
318
  const tracked = this.agents.get(name);
@@ -285,6 +337,7 @@ var AgentManager = class {
285
337
  });
286
338
  this.stopping.delete(name);
287
339
  if (this.restartAttempts.delete(name)) this.saveCrashAttempts();
340
+ rmSync2(agentPidPath(name), { force: true });
288
341
  if (!this.shuttingDown) {
289
342
  const [baseName, variantName] = name.split("@", 2);
290
343
  if (variantName) {
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-D424ZQGI.js";
5
5
  import {
6
6
  voluteHome
7
- } from "./chunk-UWHWAPGO.js";
7
+ } from "./chunk-DP2DX4WV.js";
8
8
 
9
9
  // src/commands/up.ts
10
10
  import { spawn } from "child_process";
package/dist/cli.js CHANGED
@@ -9,49 +9,52 @@ if (!process.env.VOLUTE_HOME) {
9
9
  var command = process.argv[2];
10
10
  var args = process.argv.slice(3);
11
11
  if (command === "--version" || command === "-v") {
12
- const { default: pkg } = await import("./package-T2WAVJOU.js");
12
+ const { default: pkg } = await import("./package-RJSONENE.js");
13
13
  console.log(pkg.version);
14
14
  process.exit(0);
15
15
  }
16
16
  switch (command) {
17
17
  case "agent":
18
- await import("./agent-7JF7MT73.js").then((m) => m.run(args));
18
+ await import("./agent-YORVRB6I.js").then((m) => m.run(args));
19
19
  break;
20
- case "message":
21
- await import("./message-SCOQDR3P.js").then((m) => m.run(args));
20
+ case "send":
21
+ await import("./send-BNC2S5BY.js").then((m) => m.run(args));
22
+ break;
23
+ case "history":
24
+ await import("./history-PXJVYLVY.js").then((m) => m.run(args));
22
25
  break;
23
26
  case "variant":
24
- await import("./variant-4Z6W3PP6.js").then((m) => m.run(args));
27
+ await import("./variant-RKXPN5DH.js").then((m) => m.run(args));
25
28
  break;
26
29
  case "connector":
27
- await import("./connector-Y7JPNROO.js").then((m) => m.run(args));
30
+ await import("./connector-ZP6MEFF4.js").then((m) => m.run(args));
28
31
  break;
29
32
  case "channel":
30
- await import("./channel-SMCNOIVQ.js").then((m) => m.run(args));
33
+ await import("./channel-RDGHBFSI.js").then((m) => m.run(args));
31
34
  break;
32
35
  case "schedule":
33
- await import("./schedule-S6QVC5ON.js").then((m) => m.run(args));
36
+ await import("./schedule-HCUCBNQI.js").then((m) => m.run(args));
34
37
  break;
35
38
  case "env":
36
- await import("./env-7GLUJCWS.js").then((m) => m.run(args));
39
+ await import("./env-KMNYGVZ2.js").then((m) => m.run(args));
37
40
  break;
38
41
  case "up":
39
- await import("./up-RWZF6MLT.js").then((m) => m.run(args));
42
+ await import("./up-V6EAA7OZ.js").then((m) => m.run(args));
40
43
  break;
41
44
  case "down":
42
- await import("./down-AZVH5TCD.js").then((m) => m.run(args));
45
+ await import("./down-O4EWZTVA.js").then((m) => m.run(args));
43
46
  break;
44
47
  case "restart":
45
- await import("./daemon-restart-4HVEKYFY.js").then((m) => m.run(args));
48
+ await import("./daemon-restart-CPBLMMRI.js").then((m) => m.run(args));
46
49
  break;
47
50
  case "setup":
48
- await import("./setup-F4TCWVSP.js").then((m) => m.run(args));
51
+ await import("./setup-32KH5KLN.js").then((m) => m.run(args));
49
52
  break;
50
53
  case "service":
51
- await import("./service-HZNIDNJF.js").then((m) => m.run(args));
54
+ await import("./service-XCADRKIS.js").then((m) => m.run(args));
52
55
  break;
53
56
  case "update":
54
- await import("./update-F7QWV2LB.js").then((m) => m.run(args));
57
+ await import("./update-EUCZ7XGG.js").then((m) => m.run(args));
55
58
  break;
56
59
  case "--help":
57
60
  case "-h":
@@ -70,8 +73,8 @@ Commands:
70
73
  volute agent upgrade <name> Upgrade agent to latest template
71
74
  volute agent import <path> Import an OpenClaw workspace
72
75
 
73
- volute message send <name> "<msg>" Send a message to an agent
74
- volute message history [--agent <name>] View message history
76
+ volute send <target> "<msg>" Send a message (agent DM, channel, etc.)
77
+ volute history [--agent <name>] View message history
75
78
 
76
79
  volute variant create <name> Create a variant (worktree + server)
77
80
  volute variant list List variants for an agent
@@ -82,7 +85,6 @@ Commands:
82
85
  volute connector disconnect <type> Disable a connector for an agent
83
86
 
84
87
  volute channel read <uri> Read recent messages from a channel
85
- volute channel send <uri> "<msg>" Send a message to a channel
86
88
  volute channel list [<platform>] List conversations on a platform
87
89
  volute channel users <platform> List users on a platform
88
90
  volute channel create <platform> ... Create a conversation on a platform
@@ -109,7 +111,7 @@ Options:
109
111
  --version, -v Show version number
110
112
  --help, -h Show this help message
111
113
 
112
- Agent-scoped commands (variant, connector, schedule, channel, message history)
114
+ Agent-scoped commands (send, history, variant, connector, schedule, channel)
113
115
  use --agent <name> or VOLUTE_AGENT env var to identify the agent.`);
114
116
  break;
115
117
  default:
@@ -118,7 +120,7 @@ Run 'volute --help' for usage.`);
118
120
  process.exit(1);
119
121
  }
120
122
  if (command !== "update") {
121
- import("./update-check-B4J6IEQ4.js").then((m) => m.checkForUpdate()).then((result) => {
123
+ import("./update-check-SM4244SU.js").then((m) => m.checkForUpdate()).then((result) => {
122
124
  if (result.updateAvailable) {
123
125
  console.error(`
124
126
  Update available: ${result.current} \u2192 ${result.latest}`);
@@ -6,16 +6,16 @@ import {
6
6
  agentEnvPath,
7
7
  readEnv,
8
8
  writeEnv
9
- } from "./chunk-H7AMDUIA.js";
9
+ } from "./chunk-QF22MYDJ.js";
10
10
  import {
11
11
  parseArgs
12
12
  } from "./chunk-D424ZQGI.js";
13
13
  import {
14
14
  daemonFetch
15
- } from "./chunk-JR4UXCTO.js";
15
+ } from "./chunk-23L3MKEV.js";
16
16
  import {
17
17
  agentDir
18
- } from "./chunk-UWHWAPGO.js";
18
+ } from "./chunk-DP2DX4WV.js";
19
19
  import "./chunk-K3NQKI34.js";
20
20
 
21
21
  // src/commands/connector.ts
@@ -1,27 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  buildChannelSlug,
4
- fireAndForget,
5
- handleAgentMessage,
6
4
  loadEnv,
7
5
  loadFollowedChannels,
8
6
  reportTyping,
7
+ sendToAgent,
9
8
  slugify,
10
- splitMessage,
11
9
  writeChannelEntry
12
- } from "../chunk-BX7KI4S3.js";
10
+ } from "../chunk-N6MLQ26B.js";
11
+ import "../chunk-DP2DX4WV.js";
13
12
  import "../chunk-K3NQKI34.js";
14
13
 
15
14
  // src/connectors/discord.ts
16
- import {
17
- AttachmentBuilder,
18
- ChannelType,
19
- Client,
20
- Events,
21
- GatewayIntentBits,
22
- Partials
23
- } from "discord.js";
24
- var DISCORD_MAX_LENGTH = 2e3;
15
+ import { ChannelType, Client, Events, GatewayIntentBits, Partials } from "discord.js";
25
16
  var TYPING_INTERVAL_MS = 8e3;
26
17
  var env = loadEnv();
27
18
  var token = process.env.DISCORD_TOKEN;
@@ -102,14 +93,16 @@ client.on(Events.MessageCreate, async (message) => {
102
93
  channelName: channelName ?? message.channelId,
103
94
  serverName: message.guild?.name
104
95
  });
105
- if (env.agentDir) {
106
- writeChannelEntry(env.agentDir, channelKey, {
96
+ try {
97
+ writeChannelEntry(env.agentName, channelKey, {
107
98
  platformId: message.channelId,
108
99
  platform: "discord",
109
100
  name: channelName ? `#${channelName}` : void 0,
110
101
  server: message.guild?.name,
111
102
  type: isDM ? "dm" : "channel"
112
103
  });
104
+ } catch (err) {
105
+ console.error(`[discord] failed to write channel entry for ${channelKey}:`, err);
113
106
  }
114
107
  const participantCount = isDM ? 2 : message.guild?.memberCount;
115
108
  const payload = {
@@ -124,7 +117,11 @@ client.on(Events.MessageCreate, async (message) => {
124
117
  };
125
118
  reportTyping(env, channelKey, senderName, false);
126
119
  if (isFollowedChannel && !isMentioned) {
127
- await fireAndForget(env, payload);
120
+ const result = await sendToAgent(env, payload);
121
+ if (!result.ok)
122
+ message.reply(result.error ?? "Failed to process message").catch((err) => {
123
+ console.warn(`[discord] failed to send error reply: ${err}`);
124
+ });
128
125
  return;
129
126
  }
130
127
  await handleDiscordMessage(message, payload);
@@ -146,50 +143,12 @@ async function handleDiscordMessage(message, payload) {
146
143
  channel.sendTyping().catch((err) => {
147
144
  console.warn(`[discord] sendTyping failed: ${err}`);
148
145
  });
149
- let replied = false;
150
146
  try {
151
- await handleAgentMessage(env, payload, {
152
- onFlush: async (text, images) => {
153
- if (!text && images.length === 0) return;
154
- const chunks = text ? splitMessage(text, DISCORD_MAX_LENGTH) : [];
155
- const imageFiles = images.map((img, i) => {
156
- const ext = img.media_type.split("/")[1] || "png";
157
- return new AttachmentBuilder(Buffer.from(img.data, "base64"), {
158
- name: `image-${i}.${ext}`
159
- });
160
- });
161
- if (chunks.length === 0 && imageFiles.length > 0) {
162
- const sendFn = replied ? channel.send.bind(channel) : message.reply.bind(message);
163
- await sendFn({ content: "\u200B", files: imageFiles }).catch((err) => {
164
- console.error(`Failed to send message: ${err}`);
165
- });
166
- replied = true;
167
- return;
168
- }
169
- for (let i = 0; i < chunks.length; i++) {
170
- const isLast = i === chunks.length - 1;
171
- const opts = {
172
- content: chunks[i]
173
- };
174
- if (isLast && imageFiles.length > 0) opts.files = imageFiles;
175
- try {
176
- if (!replied) {
177
- await message.reply(opts);
178
- replied = true;
179
- } else {
180
- await channel.send(opts);
181
- }
182
- } catch (err) {
183
- console.error(`Failed to send message: ${err}`);
184
- }
185
- }
186
- },
187
- onError: async (msg) => {
188
- await message.reply(msg).catch((err) => {
189
- console.error(`[discord] failed to send error reply: ${err}`);
190
- });
191
- }
192
- });
147
+ const result = await sendToAgent(env, payload);
148
+ if (!result.ok)
149
+ message.reply(result.error ?? "Failed to process message").catch((err) => {
150
+ console.warn(`[discord] failed to send error reply: ${err}`);
151
+ });
193
152
  } finally {
194
153
  clearInterval(typingInterval);
195
154
  }
@@ -1,19 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  buildChannelSlug,
4
- fireAndForget,
5
- handleAgentMessage,
6
4
  loadEnv,
7
5
  loadFollowedChannels,
8
6
  onShutdown,
9
- splitMessage,
7
+ sendToAgent,
10
8
  writeChannelEntry
11
- } from "../chunk-BX7KI4S3.js";
9
+ } from "../chunk-N6MLQ26B.js";
10
+ import "../chunk-DP2DX4WV.js";
12
11
  import "../chunk-K3NQKI34.js";
13
12
 
14
13
  // src/connectors/slack.ts
15
14
  import { App } from "@slack/bolt";
16
- var SLACK_MAX_LENGTH = 4e3;
17
15
  var env = loadEnv();
18
16
  var botToken = process.env.SLACK_BOT_TOKEN;
19
17
  var appToken = process.env.SLACK_APP_TOKEN;
@@ -30,7 +28,7 @@ var app = new App({
30
28
  });
31
29
  var botUserId;
32
30
  var serverName;
33
- app.message(async ({ message, say }) => {
31
+ app.message(async ({ message }) => {
34
32
  if (message.subtype) return;
35
33
  if (!("user" in message) || !("text" in message)) return;
36
34
  if ("bot_id" in message && message.bot_id) return;
@@ -98,14 +96,16 @@ app.message(async ({ message, say }) => {
98
96
  channelName: channelName ?? message.channel,
99
97
  serverName
100
98
  });
101
- if (env.agentDir) {
102
- writeChannelEntry(env.agentDir, channelKey, {
99
+ try {
100
+ writeChannelEntry(env.agentName, channelKey, {
103
101
  platformId: message.channel,
104
102
  platform: "slack",
105
103
  name: channelName ? `#${channelName}` : void 0,
106
104
  server: serverName,
107
105
  type: isDM ? "dm" : "channel"
108
106
  });
107
+ } catch (err) {
108
+ console.error(`[slack] failed to write channel entry for ${channelKey}:`, err);
109
109
  }
110
110
  const participantCount = message.channel_type === "im" ? 2 : numMembers;
111
111
  const payload = {
@@ -119,38 +119,21 @@ app.message(async ({ message, say }) => {
119
119
  ...participantCount ? { participantCount } : {}
120
120
  };
121
121
  if (isFollowedChannel && !isMentioned) {
122
- await fireAndForget(env, payload);
122
+ const result2 = await sendToAgent(env, payload);
123
+ if (!result2.ok)
124
+ app.client.chat.postMessage({
125
+ channel: message.channel,
126
+ text: result2.error ?? "Failed to process message"
127
+ }).catch((err) => {
128
+ console.warn(`[slack] failed to send error reply: ${err}`);
129
+ });
123
130
  return;
124
131
  }
125
- await handleAgentMessage(env, payload, {
126
- onFlush: async (text2, images) => {
127
- for (const img of images) {
128
- const ext = img.media_type.split("/")[1] || "png";
129
- try {
130
- await app.client.filesUploadV2({
131
- channel_id: message.channel,
132
- file: Buffer.from(img.data, "base64"),
133
- filename: `image.${ext}`
134
- });
135
- } catch (err) {
136
- console.error(`Failed to upload image: ${err}`);
137
- }
138
- }
139
- if (!text2) return;
140
- const chunks = splitMessage(text2, SLACK_MAX_LENGTH);
141
- for (const chunk of chunks) {
142
- try {
143
- await say(chunk);
144
- } catch (err) {
145
- console.error(`Failed to send message: ${err}`);
146
- }
147
- }
148
- },
149
- onError: async (msg) => {
150
- await say(msg).catch(() => {
151
- });
152
- }
153
- });
132
+ const result = await sendToAgent(env, payload);
133
+ if (!result.ok)
134
+ app.client.chat.postMessage({ channel: message.channel, text: result.error ?? "Failed to process message" }).catch((err) => {
135
+ console.warn(`[slack] failed to send error reply: ${err}`);
136
+ });
154
137
  });
155
138
  async function start() {
156
139
  await app.start();