volute 0.6.0 → 0.8.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 (77) hide show
  1. package/README.md +13 -13
  2. package/dist/{agent-X7GJLBLW.js → agent-YORVRB6I.js} +10 -10
  3. package/dist/{agent-manager-JDVXU3ON.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-ECPQXRLB.js +264 -0
  9. package/dist/{down-FXWAN66A.js → chunk-HZ5LTOEJ.js} +48 -13
  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-AOKAQGO4.js → chunk-RT6Y7AR3.js} +2 -1
  15. package/dist/{chunk-G6ZNGLUX.js → chunk-W6TMWYU3.js} +133 -78
  16. package/dist/{up-CSX3ZUIU.js → chunk-XSJ27WEM.js} +2 -2
  17. package/dist/cli.js +25 -19
  18. package/dist/{connector-Y7JPNROO.js → connector-ZP6MEFF4.js} +3 -3
  19. package/dist/connectors/discord.js +24 -61
  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-CPBLMMRI.js +23 -0
  25. package/dist/daemon.js +397 -661
  26. package/dist/{delete-2PH2CGDY.js → delete-45TGQC4N.js} +13 -4
  27. package/dist/down-O4EWZTVA.js +11 -0
  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-4DP4Y4UO.js → package-5UCKNK6J.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-R4MCNBOA.js} +1 -1
  37. package/dist/{setup-F4TCWVSP.js → setup-JXDCJX7W.js} +25 -6
  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-V6EAA7OZ.js +12 -0
  42. package/dist/{update-XSIX3GGP.js → update-EUCZ7XGG.js} +3 -3
  43. package/dist/{update-check-5ZADDHCK.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-D5PzIndO.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);
196
258
  }
197
- setupCrashRecovery(name, child, dir, isVariant) {
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
+ }
286
+ }
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,11 +337,14 @@ var AgentManager = class {
285
337
  });
286
338
  this.stopping.delete(name);
287
339
  if (this.restartAttempts.delete(name)) this.saveCrashAttempts();
288
- const [baseName, variantName] = name.split("@", 2);
289
- if (variantName) {
290
- setVariantRunning(baseName, variantName, false);
291
- } else {
292
- setAgentRunning(name, false);
340
+ rmSync2(agentPidPath(name), { force: true });
341
+ if (!this.shuttingDown) {
342
+ const [baseName, variantName] = name.split("@", 2);
343
+ if (variantName) {
344
+ setVariantRunning(baseName, variantName, false);
345
+ } else {
346
+ setAgentRunning(name, false);
347
+ }
293
348
  }
294
349
  console.error(`[daemon] stopped agent ${name}`);
295
350
  }
@@ -4,8 +4,7 @@ import {
4
4
  } from "./chunk-D424ZQGI.js";
5
5
  import {
6
6
  voluteHome
7
- } from "./chunk-UWHWAPGO.js";
8
- import "./chunk-K3NQKI34.js";
7
+ } from "./chunk-DP2DX4WV.js";
9
8
 
10
9
  // src/commands/up.ts
11
10
  import { spawn } from "child_process";
@@ -103,6 +102,7 @@ async function run(args) {
103
102
  console.error(`Check logs: ${logFile}`);
104
103
  process.exit(1);
105
104
  }
105
+
106
106
  export {
107
107
  readGlobalConfig,
108
108
  run
package/dist/cli.js CHANGED
@@ -9,46 +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-4DP4Y4UO.js");
12
+ const { default: pkg } = await import("./package-5UCKNK6J.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-X7GJLBLW.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-CSX3ZUIU.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-FXWAN66A.js").then((m) => m.run(args));
45
+ await import("./down-O4EWZTVA.js").then((m) => m.run(args));
46
+ break;
47
+ case "restart":
48
+ await import("./daemon-restart-CPBLMMRI.js").then((m) => m.run(args));
43
49
  break;
44
50
  case "setup":
45
- await import("./setup-F4TCWVSP.js").then((m) => m.run(args));
51
+ await import("./setup-JXDCJX7W.js").then((m) => m.run(args));
46
52
  break;
47
53
  case "service":
48
- await import("./service-HZNIDNJF.js").then((m) => m.run(args));
54
+ await import("./service-R4MCNBOA.js").then((m) => m.run(args));
49
55
  break;
50
56
  case "update":
51
- await import("./update-XSIX3GGP.js").then((m) => m.run(args));
57
+ await import("./update-EUCZ7XGG.js").then((m) => m.run(args));
52
58
  break;
53
59
  case "--help":
54
60
  case "-h":
@@ -67,8 +73,8 @@ Commands:
67
73
  volute agent upgrade <name> Upgrade agent to latest template
68
74
  volute agent import <path> Import an OpenClaw workspace
69
75
 
70
- volute message send <name> "<msg>" Send a message to an agent
71
- 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
72
78
 
73
79
  volute variant create <name> Create a variant (worktree + server)
74
80
  volute variant list List variants for an agent
@@ -79,7 +85,6 @@ Commands:
79
85
  volute connector disconnect <type> Disable a connector for an agent
80
86
 
81
87
  volute channel read <uri> Read recent messages from a channel
82
- volute channel send <uri> "<msg>" Send a message to a channel
83
88
  volute channel list [<platform>] List conversations on a platform
84
89
  volute channel users <platform> List users on a platform
85
90
  volute channel create <platform> ... Create a conversation on a platform
@@ -92,6 +97,7 @@ Commands:
92
97
 
93
98
  volute up [--port N] Start the daemon (default: 4200)
94
99
  volute down Stop the daemon
100
+ volute restart [--port N] Restart the daemon
95
101
 
96
102
  volute service install [--port N] Install as system service (auto-start)
97
103
  volute service uninstall Remove system service
@@ -105,7 +111,7 @@ Options:
105
111
  --version, -v Show version number
106
112
  --help, -h Show this help message
107
113
 
108
- Agent-scoped commands (variant, connector, schedule, channel, message history)
114
+ Agent-scoped commands (send, history, variant, connector, schedule, channel)
109
115
  use --agent <name> or VOLUTE_AGENT env var to identify the agent.`);
110
116
  break;
111
117
  default:
@@ -114,7 +120,7 @@ Run 'volute --help' for usage.`);
114
120
  process.exit(1);
115
121
  }
116
122
  if (command !== "update") {
117
- import("./update-check-5ZADDHCK.js").then((m) => m.checkForUpdate()).then((result) => {
123
+ import("./update-check-SM4244SU.js").then((m) => m.checkForUpdate()).then((result) => {
118
124
  if (result.updateAvailable) {
119
125
  console.error(`
120
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 = {
@@ -122,15 +115,20 @@ client.on(Events.MessageCreate, async (message) => {
122
115
  ...message.guild?.name ? { serverName: message.guild.name } : {},
123
116
  ...participantCount ? { participantCount } : {}
124
117
  };
118
+ reportTyping(env, channelKey, senderName, false);
125
119
  if (isFollowedChannel && !isMentioned) {
126
- 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
+ });
127
125
  return;
128
126
  }
129
127
  await handleDiscordMessage(message, payload);
130
128
  });
131
129
  client.on(Events.TypingStart, (typing) => {
132
130
  if (typing.user.bot) return;
133
- const sender = typing.member?.displayName ?? typing.user.username ?? typing.user.id ?? "unknown";
131
+ const sender = typing.user.displayName || typing.user.username || typing.user.id || "unknown";
134
132
  const typingChannel = typing.guild ? `discord:${slugify(typing.guild.name)}/${slugify("name" in typing.channel ? String(typing.channel.name) : typing.channel.id)}` : `discord:@${slugify(typing.user.username ?? typing.user.id)}`;
135
133
  reportTyping(env, typingChannel, sender, true);
136
134
  });
@@ -138,54 +136,19 @@ async function handleDiscordMessage(message, payload) {
138
136
  const channel = message.channel;
139
137
  if (!("sendTyping" in channel)) return;
140
138
  const typingInterval = setInterval(() => {
141
- channel.sendTyping().catch(() => {
139
+ channel.sendTyping().catch((err) => {
140
+ console.warn(`[discord] sendTyping failed: ${err}`);
142
141
  });
143
142
  }, TYPING_INTERVAL_MS);
144
- channel.sendTyping().catch(() => {
143
+ channel.sendTyping().catch((err) => {
144
+ console.warn(`[discord] sendTyping failed: ${err}`);
145
145
  });
146
- let replied = false;
147
146
  try {
148
- await handleAgentMessage(env, payload, {
149
- onFlush: async (text, images) => {
150
- if (!text && images.length === 0) return;
151
- const chunks = text ? splitMessage(text, DISCORD_MAX_LENGTH) : [];
152
- const imageFiles = images.map((img, i) => {
153
- const ext = img.media_type.split("/")[1] || "png";
154
- return new AttachmentBuilder(Buffer.from(img.data, "base64"), {
155
- name: `image-${i}.${ext}`
156
- });
157
- });
158
- if (chunks.length === 0 && imageFiles.length > 0) {
159
- const sendFn = replied ? channel.send.bind(channel) : message.reply.bind(message);
160
- await sendFn({ content: "\u200B", files: imageFiles }).catch((err) => {
161
- console.error(`Failed to send message: ${err}`);
162
- });
163
- replied = true;
164
- return;
165
- }
166
- for (let i = 0; i < chunks.length; i++) {
167
- const isLast = i === chunks.length - 1;
168
- const opts = {
169
- content: chunks[i]
170
- };
171
- if (isLast && imageFiles.length > 0) opts.files = imageFiles;
172
- try {
173
- if (!replied) {
174
- await message.reply(opts);
175
- replied = true;
176
- } else {
177
- await channel.send(opts);
178
- }
179
- } catch (err) {
180
- console.error(`Failed to send message: ${err}`);
181
- }
182
- }
183
- },
184
- onError: async (msg) => {
185
- await message.reply(msg).catch(() => {
186
- });
187
- }
188
- });
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
+ });
189
152
  } finally {
190
153
  clearInterval(typingInterval);
191
154
  }