volute 0.3.1 → 0.5.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 (82) hide show
  1. package/README.md +29 -29
  2. package/dist/agent-Z2B6EFEQ.js +75 -0
  3. package/dist/{agent-manager-AUCKMGPR.js → agent-manager-PXBKA2GK.js} +4 -4
  4. package/dist/channel-MK5OK2SI.js +113 -0
  5. package/dist/chunk-5X7HGB6L.js +107 -0
  6. package/dist/{chunk-YGFIWIOF.js → chunk-7L4AN5D4.js} +1 -1
  7. package/dist/{chunk-VRVVQIYY.js → chunk-AZEL2IEK.js} +1 -1
  8. package/dist/chunk-B3R6L2GW.js +24 -0
  9. package/dist/{chunk-DNOXHLE5.js → chunk-HE67X4T6.js} +1 -1
  10. package/dist/{chunk-I6OHXCMV.js → chunk-MW2KFO3B.js} +47 -9
  11. package/dist/chunk-MXUCNIBG.js +168 -0
  12. package/dist/chunk-SMISE4SV.js +226 -0
  13. package/dist/{chunk-SOZA2TLP.js → chunk-UAVD2AHX.js} +1 -1
  14. package/dist/{chunk-3C2XR4IY.js → chunk-UX25Z2ND.js} +113 -107
  15. package/dist/{chunk-GSPKUPKU.js → chunk-XUA3JUFK.js} +2 -1
  16. package/dist/chunk-ZYGKG6VC.js +22 -0
  17. package/dist/cli.js +98 -75
  18. package/dist/connector-LYEMXQEV.js +157 -0
  19. package/dist/connectors/discord.js +104 -161
  20. package/dist/connectors/slack.js +179 -0
  21. package/dist/connectors/telegram.js +175 -0
  22. package/dist/conversation-ERXEQZTY.js +163 -0
  23. package/dist/create-RVCZN6HE.js +91 -0
  24. package/dist/{daemon-client-XR24PUJF.js → daemon-client-ZY6UUN2M.js} +2 -2
  25. package/dist/daemon.js +824 -252
  26. package/dist/{delete-GQ7JEK2S.js → delete-3QH7VYIN.js} +8 -9
  27. package/dist/{down-3OB6UVAJ.js → down-O7IFZLVJ.js} +1 -1
  28. package/dist/{env-JB27UAC3.js → env-4D4REPJF.js} +8 -5
  29. package/dist/{history-3VRUBGGV.js → history-OEONB53Z.js} +5 -5
  30. package/dist/{import-K4MP2GX7.js → import-MXJB2EII.js} +23 -8
  31. package/dist/{logs-NXFFGUKY.js → logs-DF342W4M.js} +2 -2
  32. package/dist/message-ADHWFHSI.js +32 -0
  33. package/dist/package-VQOE7JNH.js +89 -0
  34. package/dist/{schedule-4I5TYHFH.js → schedule-NAG6F463.js} +12 -7
  35. package/dist/send-66QMKRUH.js +75 -0
  36. package/dist/{setup-SRS7AUAA.js → setup-RPRRGG2F.js} +6 -6
  37. package/dist/{start-LDPMCMYT.js → start-TUOXDSFL.js} +3 -3
  38. package/dist/{status-MVSQG54T.js → status-A36EHRO4.js} +3 -3
  39. package/dist/{stop-5PZTZCLL.js → stop-AOJZLQ5X.js} +6 -7
  40. package/dist/{up-UT3IMKCA.js → up-7ILD7GU7.js} +2 -2
  41. package/dist/update-LPSIAWQ2.js +140 -0
  42. package/dist/update-check-Y33QDCFL.js +17 -0
  43. package/dist/{upgrade-CDKECCGN.js → upgrade-FX2TKJ2S.js} +16 -15
  44. package/dist/{variant-CVYM3EQG.js → variant-LAB67OC2.js} +17 -12
  45. package/dist/web-assets/assets/index-BbRmoxoA.js +308 -0
  46. package/dist/web-assets/index.html +2 -2
  47. package/drizzle/0003_clean_ego.sql +12 -0
  48. package/drizzle/meta/0003_snapshot.json +417 -0
  49. package/drizzle/meta/_journal.json +7 -0
  50. package/package.json +3 -1
  51. package/templates/_base/.init/.config/hooks/startup-context.sh +19 -1
  52. package/templates/_base/_skills/volute-agent/SKILL.md +112 -16
  53. package/templates/_base/home/.config/routes.json +10 -0
  54. package/templates/_base/home/VOLUTE.md +19 -28
  55. package/templates/_base/src/lib/file-handler.ts +46 -0
  56. package/templates/_base/src/lib/format-prefix.ts +1 -1
  57. package/templates/_base/src/lib/router.ts +327 -0
  58. package/templates/_base/src/lib/routing.ts +137 -0
  59. package/templates/_base/src/lib/types.ts +16 -3
  60. package/templates/_base/src/lib/volute-server.ts +20 -48
  61. package/templates/agent-sdk/.init/.config/routes.json +5 -0
  62. package/templates/agent-sdk/.init/CLAUDE.md +2 -2
  63. package/templates/agent-sdk/src/agent.ts +269 -82
  64. package/templates/agent-sdk/src/server.ts +19 -4
  65. package/templates/agent-sdk/volute-template.json +1 -1
  66. package/templates/pi/.init/.config/routes.json +5 -0
  67. package/templates/pi/.init/AGENTS.md +1 -1
  68. package/templates/pi/src/agent.ts +279 -58
  69. package/templates/pi/src/server.ts +15 -4
  70. package/templates/pi/volute-template.json +1 -1
  71. package/dist/channel-7FZ6D25H.js +0 -90
  72. package/dist/chunk-N4YNKR3Q.js +0 -90
  73. package/dist/connector-TVJULIRT.js +0 -96
  74. package/dist/create-BRG2DBWI.js +0 -79
  75. package/dist/send-UK3JBZIB.js +0 -53
  76. package/dist/web-assets/assets/index-BC5eSqbY.js +0 -296
  77. package/templates/_base/src/lib/sessions.ts +0 -71
  78. package/templates/agent-sdk/.init/.config/sessions.json +0 -4
  79. package/templates/agent-sdk/src/lib/agent-sessions.ts +0 -204
  80. package/templates/pi/.init/.config/sessions.json +0 -1
  81. package/templates/pi/src/lib/agent-sessions.ts +0 -210
  82. package/dist/{service-SA4TTMDU.js → service-HZNIDNJF.js} +3 -3
package/dist/cli.js CHANGED
@@ -1,105 +1,128 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
+ import { homedir } from "os";
5
+ import { resolve } from "path";
6
+ if (!process.env.VOLUTE_HOME) {
7
+ process.env.VOLUTE_HOME = resolve(homedir(), ".volute");
8
+ }
4
9
  var command = process.argv[2];
5
10
  var args = process.argv.slice(3);
11
+ if (command === "--version" || command === "-v") {
12
+ const { default: pkg } = await import("./package-VQOE7JNH.js");
13
+ console.log(pkg.version);
14
+ process.exit(0);
15
+ }
6
16
  switch (command) {
7
- case "create":
8
- await import("./create-BRG2DBWI.js").then((m) => m.run(args));
9
- break;
10
- case "start":
11
- await import("./start-LDPMCMYT.js").then((m) => m.run(args));
12
- break;
13
- case "stop":
14
- await import("./stop-5PZTZCLL.js").then((m) => m.run(args));
15
- break;
16
- case "logs":
17
- await import("./logs-NXFFGUKY.js").then((m) => m.run(args));
17
+ case "agent":
18
+ await import("./agent-Z2B6EFEQ.js").then((m) => m.run(args));
18
19
  break;
19
- case "status":
20
- await import("./status-MVSQG54T.js").then((m) => m.run(args));
20
+ case "message":
21
+ await import("./message-ADHWFHSI.js").then((m) => m.run(args));
21
22
  break;
22
23
  case "variant":
23
- await import("./variant-CVYM3EQG.js").then((m) => m.run(args));
24
- break;
25
- case "send":
26
- await import("./send-UK3JBZIB.js").then((m) => m.run(args));
27
- break;
28
- case "import":
29
- await import("./import-K4MP2GX7.js").then((m) => m.run(args));
30
- break;
31
- case "delete":
32
- await import("./delete-GQ7JEK2S.js").then((m) => m.run(args));
33
- break;
34
- case "env":
35
- await import("./env-JB27UAC3.js").then((m) => m.run(args));
24
+ await import("./variant-LAB67OC2.js").then((m) => m.run(args));
36
25
  break;
37
26
  case "connector":
38
- await import("./connector-TVJULIRT.js").then((m) => m.run(args));
27
+ await import("./connector-LYEMXQEV.js").then((m) => m.run(args));
39
28
  break;
40
29
  case "channel":
41
- await import("./channel-7FZ6D25H.js").then((m) => m.run(args));
30
+ await import("./channel-MK5OK2SI.js").then((m) => m.run(args));
31
+ break;
32
+ case "schedule":
33
+ await import("./schedule-NAG6F463.js").then((m) => m.run(args));
34
+ break;
35
+ case "conversation":
36
+ await import("./conversation-ERXEQZTY.js").then((m) => m.run(args));
42
37
  break;
43
- case "upgrade":
44
- await import("./upgrade-CDKECCGN.js").then((m) => m.run(args));
38
+ case "env":
39
+ await import("./env-4D4REPJF.js").then((m) => m.run(args));
45
40
  break;
46
41
  case "up":
47
- await import("./up-UT3IMKCA.js").then((m) => m.run(args));
42
+ await import("./up-7ILD7GU7.js").then((m) => m.run(args));
48
43
  break;
49
44
  case "down":
50
- await import("./down-3OB6UVAJ.js").then((m) => m.run(args));
51
- break;
52
- case "schedule":
53
- await import("./schedule-4I5TYHFH.js").then((m) => m.run(args));
45
+ await import("./down-O7IFZLVJ.js").then((m) => m.run(args));
54
46
  break;
55
- case "history":
56
- await import("./history-3VRUBGGV.js").then((m) => m.run(args));
47
+ case "setup":
48
+ await import("./setup-RPRRGG2F.js").then((m) => m.run(args));
57
49
  break;
58
50
  case "service":
59
- await import("./service-SA4TTMDU.js").then((m) => m.run(args));
51
+ await import("./service-HZNIDNJF.js").then((m) => m.run(args));
60
52
  break;
61
- case "setup":
62
- await import("./setup-SRS7AUAA.js").then((m) => m.run(args));
53
+ case "update":
54
+ await import("./update-LPSIAWQ2.js").then((m) => m.run(args));
63
55
  break;
64
- default:
56
+ case "--help":
57
+ case "-h":
58
+ case void 0:
65
59
  console.log(`volute \u2014 create and manage AI agents
66
60
 
67
61
  Commands:
68
- volute create <name> Create a new agent
69
- volute start <name> Start an agent (daemonized)
70
- volute stop <name> Stop an agent
71
- volute status [<name>] Check agent status (or list all)
72
- volute logs [--agent <name>] Tail agent logs
73
- volute send <name> "<msg>" Send a message to an agent
74
- volute variant create <name> Create a variant (worktree + server)
75
- volute variant list List variants for an agent
76
- volute variant merge <name> Merge a variant back
77
- volute variant delete <name> Delete a variant
78
- volute import <path> Import an OpenClaw workspace
79
- volute env <set|get|list|remove> Manage environment variables
80
- volute connector connect <type> Enable a connector for an agent
81
- volute connector disconnect <type> Disable a connector for an agent
82
- volute channel read <uri> Read recent messages from a channel
83
- volute channel send <uri> "<msg>" Send a message to a channel
84
- volute schedule list List schedules for an agent
85
- volute schedule add ... Add a cron schedule
86
- volute schedule remove ... Remove a schedule
87
- volute history View message history
88
- volute up [--port N] Start the daemon (default: 4200)
89
- volute down Stop the daemon
90
- volute upgrade <name> Upgrade agent to latest template
91
- volute delete <name> [--force] Delete an agent (--force removes files)
92
- volute service install [--port N] Install as system service (auto-start)
93
- volute service uninstall Remove system service
94
- volute service status Check service status
95
- volute setup [--port N] [--host H] Install system service with user isolation
96
- volute setup uninstall [--force] Remove system service + isolation
62
+ volute agent create <name> Create a new agent
63
+ volute agent start <name> Start an agent (daemonized)
64
+ volute agent stop <name> Stop an agent
65
+ volute agent delete <name> [--force] Delete an agent (--force removes files)
66
+ volute agent list List all agents
67
+ volute agent status <name> Check agent status
68
+ volute agent logs <name> [--follow] Tail agent logs
69
+ volute agent upgrade <name> Upgrade agent to latest template
70
+ volute agent import <path> Import an OpenClaw workspace
71
+
72
+ volute message send <name> "<msg>" Send a message to an agent
73
+ volute message history [--agent <name>] View message history
74
+
75
+ volute variant create <name> Create a variant (worktree + server)
76
+ volute variant list List variants for an agent
77
+ volute variant merge <name> Merge a variant back
78
+ volute variant delete <name> Delete a variant
79
+
80
+ volute connector connect <type> Enable a connector for an agent
81
+ volute connector disconnect <type> Disable a connector for an agent
82
+
83
+ volute channel read <uri> Read recent messages from a channel
84
+ volute channel send <uri> "<msg>" Send a message to a channel
85
+
86
+ volute schedule list List schedules for an agent
87
+ volute schedule add ... Add a cron schedule
88
+ volute schedule remove ... Remove a schedule
89
+
90
+ volute conversation create ... Create a group conversation
91
+ volute conversation list List conversations
92
+ volute conversation send <id> "<msg>" Send a message to a conversation
97
93
 
98
- Agent commands (variant, connector, schedule, logs, history, channel) use
99
- --agent <name> or VOLUTE_AGENT env var to identify the agent.`);
100
- if (command) {
94
+ volute env <set|get|list|remove> Manage environment variables
95
+
96
+ volute up [--port N] Start the daemon (default: 4200)
97
+ volute down Stop the daemon
98
+
99
+ volute service install [--port N] Install as system service (auto-start)
100
+ volute service uninstall Remove system service
101
+ volute service status Check service status
102
+ volute setup [--port N] [--host H] Install system service with user isolation
103
+ volute setup uninstall [--force] Remove system service + isolation
104
+
105
+ volute update Update to latest version
106
+
107
+ Options:
108
+ --version, -v Show version number
109
+ --help, -h Show this help message
110
+
111
+ Agent-scoped commands (variant, connector, schedule, channel, conversation, message history)
112
+ use --agent <name> or VOLUTE_AGENT env var to identify the agent.`);
113
+ break;
114
+ default:
115
+ console.error(`Unknown command: ${command}
116
+ Run 'volute --help' for usage.`);
117
+ process.exit(1);
118
+ }
119
+ if (command !== "update") {
120
+ import("./update-check-Y33QDCFL.js").then((m) => m.checkForUpdate()).then((result) => {
121
+ if (result.updateAvailable) {
101
122
  console.error(`
102
- Unknown command: ${command}`);
103
- process.exit(1);
123
+ Update available: ${result.current} \u2192 ${result.latest}`);
124
+ console.error(" Run `volute update` to update\n");
104
125
  }
126
+ }).catch(() => {
127
+ });
105
128
  }
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ resolveAgentName
4
+ } from "./chunk-AZEL2IEK.js";
5
+ import {
6
+ agentEnvPath,
7
+ readEnv,
8
+ writeEnv
9
+ } from "./chunk-HE67X4T6.js";
10
+ import {
11
+ parseArgs
12
+ } from "./chunk-D424ZQGI.js";
13
+ import {
14
+ daemonFetch
15
+ } from "./chunk-7L4AN5D4.js";
16
+ import {
17
+ agentDir
18
+ } from "./chunk-UX25Z2ND.js";
19
+ import "./chunk-K3NQKI34.js";
20
+
21
+ // src/commands/connector.ts
22
+ async function run(args) {
23
+ const subcommand = args[0];
24
+ switch (subcommand) {
25
+ case "connect":
26
+ await connectConnector(args.slice(1));
27
+ break;
28
+ case "disconnect":
29
+ await disconnectConnector(args.slice(1));
30
+ break;
31
+ case "--help":
32
+ case "-h":
33
+ case void 0:
34
+ printUsage();
35
+ break;
36
+ default:
37
+ printUsage();
38
+ process.exit(1);
39
+ }
40
+ }
41
+ function printUsage() {
42
+ console.log(`Usage:
43
+ volute connector connect <type> [--agent <name>]
44
+ volute connector disconnect <type> [--agent <name>]`);
45
+ }
46
+ async function promptValue(key, description) {
47
+ process.stderr.write(`${description}
48
+ Enter value for ${key}: `);
49
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
50
+ return new Promise((resolve) => {
51
+ let value = "";
52
+ const onData = (buf) => {
53
+ for (const byte of buf) {
54
+ if (byte === 3) {
55
+ process.stderr.write("\n");
56
+ process.exit(1);
57
+ }
58
+ if (byte === 13 || byte === 10) {
59
+ process.stderr.write("\n");
60
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
61
+ process.stdin.removeListener("data", onData);
62
+ process.stdin.pause();
63
+ resolve(value);
64
+ return;
65
+ }
66
+ if (byte === 127 || byte === 8) {
67
+ value = value.slice(0, -1);
68
+ } else {
69
+ value += String.fromCharCode(byte);
70
+ }
71
+ }
72
+ };
73
+ process.stdin.resume();
74
+ process.stdin.on("data", onData);
75
+ });
76
+ }
77
+ async function connectConnector(args) {
78
+ const { positional, flags } = parseArgs(args, {
79
+ agent: { type: "string" }
80
+ });
81
+ const agentName = resolveAgentName(flags);
82
+ const type = positional[0];
83
+ if (!type) {
84
+ console.error("Usage: volute connector connect <type> [--agent <name>]");
85
+ process.exit(1);
86
+ }
87
+ const url = `/api/agents/${encodeURIComponent(agentName)}/connectors/${encodeURIComponent(type)}`;
88
+ let res = await daemonFetch(url, { method: "POST" });
89
+ if (!res.ok) {
90
+ const body = await res.json().catch(() => ({ error: "Unknown error" }));
91
+ if (body.error === "missing_env" && "missing" in body) {
92
+ const { missing, connectorName } = body;
93
+ if (!process.stdin.isTTY) {
94
+ console.error(`Missing required environment variables for ${connectorName}:`);
95
+ for (const v of missing) {
96
+ console.error(` ${v.name} \u2014 ${v.description}`);
97
+ }
98
+ console.error(`
99
+ Set them with: volute env set <KEY> --agent ${agentName}`);
100
+ process.exit(1);
101
+ }
102
+ console.error(`${connectorName} connector requires some environment variables.
103
+ `);
104
+ const dir = agentDir(agentName);
105
+ const envPath = agentEnvPath(dir);
106
+ const env = readEnv(envPath);
107
+ for (const v of missing) {
108
+ const value = await promptValue(v.name, v.description);
109
+ if (!value) {
110
+ console.error(`No value provided for ${v.name}. Aborting.`);
111
+ process.exit(1);
112
+ }
113
+ env[v.name] = value;
114
+ }
115
+ writeEnv(envPath, env);
116
+ console.log("Environment variables saved.\n");
117
+ res = await daemonFetch(url, { method: "POST" });
118
+ if (!res.ok) {
119
+ const retryBody = await res.json().catch(() => ({ error: "Unknown error" }));
120
+ console.error(
121
+ `Failed to start ${type} connector: ${retryBody.error}`
122
+ );
123
+ process.exit(1);
124
+ }
125
+ } else {
126
+ console.error(`Failed to start ${type} connector: ${body.error}`);
127
+ process.exit(1);
128
+ }
129
+ }
130
+ console.log(`${type} connector for ${agentName} started.`);
131
+ }
132
+ async function disconnectConnector(args) {
133
+ const { positional, flags } = parseArgs(args, {
134
+ agent: { type: "string" }
135
+ });
136
+ const agentName = resolveAgentName(flags);
137
+ const type = positional[0];
138
+ if (!type) {
139
+ console.error("Usage: volute connector disconnect <type> [--agent <name>]");
140
+ process.exit(1);
141
+ }
142
+ const res = await daemonFetch(
143
+ `/api/agents/${encodeURIComponent(agentName)}/connectors/${encodeURIComponent(type)}`,
144
+ {
145
+ method: "DELETE"
146
+ }
147
+ );
148
+ if (!res.ok) {
149
+ const body = await res.json().catch(() => ({ error: "Unknown error" }));
150
+ console.error(`Failed to stop ${type} connector: ${body.error}`);
151
+ process.exit(1);
152
+ }
153
+ console.log(`${type} connector for ${agentName} stopped.`);
154
+ }
155
+ export {
156
+ run
157
+ };
@@ -1,8 +1,17 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ fireAndForget,
4
+ handleAgentMessage,
5
+ loadEnv,
6
+ loadFollowedChannels,
7
+ splitMessage
8
+ } from "../chunk-MXUCNIBG.js";
9
+ import "../chunk-K3NQKI34.js";
2
10
 
3
11
  // src/connectors/discord.ts
4
12
  import {
5
13
  AttachmentBuilder,
14
+ ChannelType,
6
15
  Client,
7
16
  Events,
8
17
  GatewayIntentBits,
@@ -10,21 +19,14 @@ import {
10
19
  } from "discord.js";
11
20
  var DISCORD_MAX_LENGTH = 2e3;
12
21
  var TYPING_INTERVAL_MS = 8e3;
13
- var agentPort = process.env.VOLUTE_AGENT_PORT;
14
- var agentName = process.env.VOLUTE_AGENT_NAME;
22
+ var env = loadEnv();
15
23
  var token = process.env.DISCORD_TOKEN;
16
- if (!agentPort || !agentName) {
17
- console.error("Missing required env vars: VOLUTE_AGENT_PORT, VOLUTE_AGENT_NAME");
18
- process.exit(1);
19
- }
20
24
  if (!token) {
21
25
  console.error("Missing required env var: DISCORD_TOKEN");
22
26
  process.exit(1);
23
27
  }
24
- var guildId = process.env.DISCORD_GUILD_ID;
25
- var daemonUrl = process.env.VOLUTE_DAEMON_URL;
26
- var daemonToken = process.env.VOLUTE_DAEMON_TOKEN;
27
- var baseUrl = daemonUrl ? `${daemonUrl}/api/agents/${encodeURIComponent(agentName)}` : `http://127.0.0.1:${agentPort}`;
28
+ var followedChannelNames = loadFollowedChannels(env, "discord");
29
+ var followedChannelIds = /* @__PURE__ */ new Set();
28
30
  var client = new Client({
29
31
  intents: [
30
32
  GatewayIntentBits.Guilds,
@@ -42,13 +44,28 @@ process.on("SIGINT", shutdown);
42
44
  process.on("SIGTERM", shutdown);
43
45
  client.once(Events.ClientReady, (c) => {
44
46
  console.log(`Connected to Discord as ${c.user.tag}`);
45
- console.log(`Bridging to agent: ${agentName} via ${baseUrl}/message`);
47
+ console.log(`Bridging to agent: ${env.agentName} via ${env.baseUrl}/message`);
48
+ if (followedChannelNames.length > 0) {
49
+ for (const guild of c.guilds.cache.values()) {
50
+ for (const ch of guild.channels.cache.values()) {
51
+ if (ch.type !== ChannelType.GuildText) continue;
52
+ if (followedChannelNames.includes(ch.name)) {
53
+ followedChannelIds.add(ch.id);
54
+ console.log(`Following #${ch.name} (${ch.id}) in ${guild.name}`);
55
+ }
56
+ }
57
+ }
58
+ if (followedChannelIds.size === 0) {
59
+ console.warn(`No channels found matching: ${followedChannelNames.join(", ")}`);
60
+ }
61
+ }
46
62
  });
47
63
  client.on(Events.MessageCreate, async (message) => {
48
64
  if (message.author.bot) return;
49
65
  const isDM = !message.guild;
50
66
  const isMentioned = !isDM && message.mentions.has(client.user);
51
- if (!isDM && !isMentioned) return;
67
+ const isFollowedChannel = !isDM && followedChannelIds.has(message.channelId);
68
+ if (!isDM && !isMentioned && !isFollowedChannel) return;
52
69
  let text = message.content;
53
70
  if (isMentioned) {
54
71
  text = text.replace(new RegExp(`<@!?${client.user.id}>`, "g"), "").trim();
@@ -70,8 +87,82 @@ client.on(Events.MessageCreate, async (message) => {
70
87
  }
71
88
  }
72
89
  if (content.length === 0) return;
73
- await handleAgentRequest(message, content);
90
+ const senderName = message.author.displayName || message.author.username;
91
+ const channelKey = `discord:${message.channelId}`;
92
+ const channelName = !isDM && "name" in message.channel ? message.channel.name : void 0;
93
+ const participantCount = isDM ? 2 : message.guild?.memberCount;
94
+ const payload = {
95
+ content,
96
+ channel: channelKey,
97
+ sender: senderName,
98
+ platform: "Discord",
99
+ ...isDM ? { isDM: true } : {},
100
+ ...channelName ? { channelName } : {},
101
+ ...message.guild?.name ? { serverName: message.guild.name } : {},
102
+ ...participantCount ? { participantCount } : {}
103
+ };
104
+ if (isFollowedChannel && !isMentioned) {
105
+ await fireAndForget(env, payload);
106
+ return;
107
+ }
108
+ await handleDiscordMessage(message, payload);
74
109
  });
110
+ async function handleDiscordMessage(message, payload) {
111
+ const channel = message.channel;
112
+ if (!("sendTyping" in channel)) return;
113
+ const typingInterval = setInterval(() => {
114
+ channel.sendTyping().catch(() => {
115
+ });
116
+ }, TYPING_INTERVAL_MS);
117
+ channel.sendTyping().catch(() => {
118
+ });
119
+ let replied = false;
120
+ try {
121
+ await handleAgentMessage(env, payload, {
122
+ onFlush: async (text, images) => {
123
+ if (!text && images.length === 0) return;
124
+ const chunks = text ? splitMessage(text, DISCORD_MAX_LENGTH) : [];
125
+ const imageFiles = images.map((img, i) => {
126
+ const ext = img.media_type.split("/")[1] || "png";
127
+ return new AttachmentBuilder(Buffer.from(img.data, "base64"), {
128
+ name: `image-${i}.${ext}`
129
+ });
130
+ });
131
+ if (chunks.length === 0 && imageFiles.length > 0) {
132
+ const sendFn = replied ? channel.send.bind(channel) : message.reply.bind(message);
133
+ await sendFn({ content: "\u200B", files: imageFiles }).catch((err) => {
134
+ console.error(`Failed to send message: ${err}`);
135
+ });
136
+ replied = true;
137
+ return;
138
+ }
139
+ for (let i = 0; i < chunks.length; i++) {
140
+ const isLast = i === chunks.length - 1;
141
+ const opts = {
142
+ content: chunks[i]
143
+ };
144
+ if (isLast && imageFiles.length > 0) opts.files = imageFiles;
145
+ try {
146
+ if (!replied) {
147
+ await message.reply(opts);
148
+ replied = true;
149
+ } else {
150
+ await channel.send(opts);
151
+ }
152
+ } catch (err) {
153
+ console.error(`Failed to send message: ${err}`);
154
+ }
155
+ }
156
+ },
157
+ onError: async (msg) => {
158
+ await message.reply(msg).catch(() => {
159
+ });
160
+ }
161
+ });
162
+ } finally {
163
+ clearInterval(typingInterval);
164
+ }
165
+ }
75
166
  async function loginWithRetry() {
76
167
  try {
77
168
  await client.login(token);
@@ -94,151 +185,3 @@ loginWithRetry().catch((err) => {
94
185
  console.error("Failed to connect to Discord:", err);
95
186
  process.exit(1);
96
187
  });
97
- function splitMessage(text) {
98
- const chunks = [];
99
- while (text.length > DISCORD_MAX_LENGTH) {
100
- let splitAt = text.lastIndexOf("\n", DISCORD_MAX_LENGTH);
101
- if (splitAt < DISCORD_MAX_LENGTH / 2) splitAt = DISCORD_MAX_LENGTH;
102
- chunks.push(text.slice(0, splitAt));
103
- text = text.slice(splitAt).replace(/^\n/, "");
104
- }
105
- if (text) chunks.push(text);
106
- return chunks;
107
- }
108
- async function* readNdjson(body) {
109
- const reader = body.getReader();
110
- const decoder = new TextDecoder();
111
- let buffer = "";
112
- try {
113
- while (true) {
114
- const { done, value } = await reader.read();
115
- if (done) break;
116
- buffer += decoder.decode(value, { stream: true });
117
- const lines = buffer.split("\n");
118
- buffer = lines.pop() || "";
119
- for (const line of lines) {
120
- if (!line.trim()) continue;
121
- try {
122
- yield JSON.parse(line);
123
- } catch {
124
- }
125
- }
126
- }
127
- if (buffer.trim()) {
128
- try {
129
- yield JSON.parse(buffer);
130
- } catch {
131
- }
132
- }
133
- } finally {
134
- reader.releaseLock();
135
- }
136
- }
137
- async function handleAgentRequest(message, content) {
138
- const channel = message.channel;
139
- if (!("sendTyping" in channel)) return;
140
- const typingInterval = setInterval(() => {
141
- channel.sendTyping().catch(() => {
142
- });
143
- }, TYPING_INTERVAL_MS);
144
- channel.sendTyping().catch(() => {
145
- });
146
- let accumulated = "";
147
- const pendingImages = [];
148
- let replied = false;
149
- async function flush() {
150
- const text = accumulated.trim();
151
- accumulated = "";
152
- if (!text && pendingImages.length === 0) return;
153
- const chunks = text ? splitMessage(text) : [];
154
- const imageFiles = pendingImages.splice(0).map((img, i) => {
155
- const ext = img.media_type.split("/")[1] || "png";
156
- return new AttachmentBuilder(Buffer.from(img.data, "base64"), {
157
- name: `image-${i}.${ext}`
158
- });
159
- });
160
- if (chunks.length === 0 && imageFiles.length > 0) {
161
- const sendFn = replied ? channel.send.bind(channel) : message.reply.bind(message);
162
- await sendFn({ content: "\u200B", files: imageFiles }).catch((err) => {
163
- console.error(`Failed to send message: ${err}`);
164
- });
165
- replied = true;
166
- return;
167
- }
168
- for (let i = 0; i < chunks.length; i++) {
169
- const isLast = i === chunks.length - 1;
170
- const opts = {
171
- content: chunks[i]
172
- };
173
- if (isLast && imageFiles.length > 0) opts.files = imageFiles;
174
- try {
175
- if (!replied) {
176
- await message.reply(opts);
177
- replied = true;
178
- } else {
179
- await channel.send(opts);
180
- }
181
- } catch (err) {
182
- console.error(`Failed to send message: ${err}`);
183
- }
184
- }
185
- }
186
- const senderName = message.author.displayName || message.author.username;
187
- const channelKey = `discord:${message.channelId}`;
188
- const isDM = !message.guild;
189
- const channelName = !isDM && "name" in message.channel ? message.channel.name : null;
190
- try {
191
- const headers = { "Content-Type": "application/json" };
192
- if (daemonUrl && daemonToken) {
193
- headers.Authorization = `Bearer ${daemonToken}`;
194
- headers.Origin = daemonUrl;
195
- }
196
- const res = await fetch(`${baseUrl}/message`, {
197
- method: "POST",
198
- headers,
199
- body: JSON.stringify({
200
- content,
201
- channel: channelKey,
202
- sender: senderName,
203
- platform: "Discord",
204
- ...isDM ? { isDM: true } : {},
205
- ...channelName ? { channelName } : {},
206
- ...message.guild?.name ? { guildName: message.guild.name } : {}
207
- })
208
- });
209
- if (!res.ok) {
210
- const body = await res.text().catch(() => "");
211
- console.error(`Agent returned ${res.status}: ${body}`);
212
- await message.reply(`Error: agent returned ${res.status}`);
213
- clearInterval(typingInterval);
214
- return;
215
- }
216
- if (!res.body) {
217
- await message.reply("Error: no response from agent");
218
- clearInterval(typingInterval);
219
- return;
220
- }
221
- for await (const event of readNdjson(res.body)) {
222
- if (event.type === "text") {
223
- accumulated += event.content;
224
- } else if (event.type === "image") {
225
- pendingImages.push({
226
- data: event.data,
227
- media_type: event.media_type
228
- });
229
- } else if (event.type === "tool_use") {
230
- await flush();
231
- } else if (event.type === "done") {
232
- break;
233
- }
234
- }
235
- await flush();
236
- } catch (err) {
237
- console.error(`Failed to reach agent at ${baseUrl}/message:`, err);
238
- const errMsg = err instanceof TypeError && err.cause?.code === "ECONNREFUSED" ? "Agent is not running" : `Error: ${err}`;
239
- await message.reply(errMsg).catch(() => {
240
- });
241
- } finally {
242
- clearInterval(typingInterval);
243
- }
244
- }