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
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ resolveAgentName
4
+ } from "./chunk-VRVVQIYY.js";
5
+ import {
6
+ agentEnvPath,
7
+ readEnv,
8
+ writeEnv
9
+ } from "./chunk-DNOXHLE5.js";
10
+ import {
11
+ parseArgs
12
+ } from "./chunk-D424ZQGI.js";
13
+ import {
14
+ daemonFetch
15
+ } from "./chunk-YGFIWIOF.js";
16
+ import {
17
+ agentDir
18
+ } from "./chunk-3C2XR4IY.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
+ default:
32
+ printUsage();
33
+ process.exit(subcommand ? 1 : 0);
34
+ }
35
+ }
36
+ function printUsage() {
37
+ console.error(`Usage:
38
+ volute connector connect <type> [--agent <name>]
39
+ volute connector disconnect <type> [--agent <name>]`);
40
+ }
41
+ async function promptValue(key, description) {
42
+ process.stderr.write(`${description}
43
+ Enter value for ${key}: `);
44
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
45
+ return new Promise((resolve) => {
46
+ let value = "";
47
+ const onData = (buf) => {
48
+ for (const byte of buf) {
49
+ if (byte === 3) {
50
+ process.stderr.write("\n");
51
+ process.exit(1);
52
+ }
53
+ if (byte === 13 || byte === 10) {
54
+ process.stderr.write("\n");
55
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
56
+ process.stdin.removeListener("data", onData);
57
+ process.stdin.pause();
58
+ resolve(value);
59
+ return;
60
+ }
61
+ if (byte === 127 || byte === 8) {
62
+ value = value.slice(0, -1);
63
+ } else {
64
+ value += String.fromCharCode(byte);
65
+ }
66
+ }
67
+ };
68
+ process.stdin.resume();
69
+ process.stdin.on("data", onData);
70
+ });
71
+ }
72
+ async function connectConnector(args) {
73
+ const { positional, flags } = parseArgs(args, {
74
+ agent: { type: "string" }
75
+ });
76
+ const agentName = resolveAgentName(flags);
77
+ const type = positional[0];
78
+ if (!type) {
79
+ console.error("Usage: volute connector connect <type> [--agent <name>]");
80
+ process.exit(1);
81
+ }
82
+ const url = `/api/agents/${encodeURIComponent(agentName)}/connectors/${encodeURIComponent(type)}`;
83
+ let res = await daemonFetch(url, { method: "POST" });
84
+ if (!res.ok) {
85
+ const body = await res.json().catch(() => ({ error: "Unknown error" }));
86
+ if (body.error === "missing_env" && "missing" in body) {
87
+ const { missing, connectorName } = body;
88
+ if (!process.stdin.isTTY) {
89
+ console.error(`Missing required environment variables for ${connectorName}:`);
90
+ for (const v of missing) {
91
+ console.error(` ${v.name} \u2014 ${v.description}`);
92
+ }
93
+ console.error(`
94
+ Set them with: volute env set <KEY> --agent ${agentName}`);
95
+ process.exit(1);
96
+ }
97
+ console.error(`${connectorName} connector requires some environment variables.
98
+ `);
99
+ const dir = agentDir(agentName);
100
+ const envPath = agentEnvPath(dir);
101
+ const env = readEnv(envPath);
102
+ for (const v of missing) {
103
+ const value = await promptValue(v.name, v.description);
104
+ if (!value) {
105
+ console.error(`No value provided for ${v.name}. Aborting.`);
106
+ process.exit(1);
107
+ }
108
+ env[v.name] = value;
109
+ }
110
+ writeEnv(envPath, env);
111
+ console.log("Environment variables saved.\n");
112
+ res = await daemonFetch(url, { method: "POST" });
113
+ if (!res.ok) {
114
+ const retryBody = await res.json().catch(() => ({ error: "Unknown error" }));
115
+ console.error(
116
+ `Failed to start ${type} connector: ${retryBody.error}`
117
+ );
118
+ process.exit(1);
119
+ }
120
+ } else {
121
+ console.error(`Failed to start ${type} connector: ${body.error}`);
122
+ process.exit(1);
123
+ }
124
+ }
125
+ console.log(`${type} connector for ${agentName} started.`);
126
+ }
127
+ async function disconnectConnector(args) {
128
+ const { positional, flags } = parseArgs(args, {
129
+ agent: { type: "string" }
130
+ });
131
+ const agentName = resolveAgentName(flags);
132
+ const type = positional[0];
133
+ if (!type) {
134
+ console.error("Usage: volute connector disconnect <type> [--agent <name>]");
135
+ process.exit(1);
136
+ }
137
+ const res = await daemonFetch(
138
+ `/api/agents/${encodeURIComponent(agentName)}/connectors/${encodeURIComponent(type)}`,
139
+ {
140
+ method: "DELETE"
141
+ }
142
+ );
143
+ if (!res.ok) {
144
+ const body = await res.json().catch(() => ({ error: "Unknown error" }));
145
+ console.error(`Failed to stop ${type} connector: ${body.error}`);
146
+ process.exit(1);
147
+ }
148
+ console.log(`${type} connector for ${agentName} stopped.`);
149
+ }
150
+ export {
151
+ run
152
+ };
@@ -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://localhost:${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} (port ${agentPort})`);
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,80 @@ 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 payload = {
94
+ content,
95
+ channel: channelKey,
96
+ sender: senderName,
97
+ platform: "Discord",
98
+ ...isDM ? { isDM: true } : {},
99
+ ...channelName ? { channelName } : {},
100
+ ...message.guild?.name ? { guildName: message.guild.name } : {}
101
+ };
102
+ if (isFollowedChannel && !isMentioned) {
103
+ await fireAndForget(env, payload);
104
+ return;
105
+ }
106
+ await handleDiscordMessage(message, payload);
74
107
  });
108
+ async function handleDiscordMessage(message, payload) {
109
+ const channel = message.channel;
110
+ if (!("sendTyping" in channel)) return;
111
+ const typingInterval = setInterval(() => {
112
+ channel.sendTyping().catch(() => {
113
+ });
114
+ }, TYPING_INTERVAL_MS);
115
+ channel.sendTyping().catch(() => {
116
+ });
117
+ let replied = false;
118
+ try {
119
+ await handleAgentMessage(env, payload, {
120
+ onFlush: async (text, images) => {
121
+ if (!text && images.length === 0) return;
122
+ const chunks = text ? splitMessage(text, DISCORD_MAX_LENGTH) : [];
123
+ const imageFiles = images.map((img, i) => {
124
+ const ext = img.media_type.split("/")[1] || "png";
125
+ return new AttachmentBuilder(Buffer.from(img.data, "base64"), {
126
+ name: `image-${i}.${ext}`
127
+ });
128
+ });
129
+ if (chunks.length === 0 && imageFiles.length > 0) {
130
+ const sendFn = replied ? channel.send.bind(channel) : message.reply.bind(message);
131
+ await sendFn({ content: "\u200B", files: imageFiles }).catch((err) => {
132
+ console.error(`Failed to send message: ${err}`);
133
+ });
134
+ replied = true;
135
+ return;
136
+ }
137
+ for (let i = 0; i < chunks.length; i++) {
138
+ const isLast = i === chunks.length - 1;
139
+ const opts = {
140
+ content: chunks[i]
141
+ };
142
+ if (isLast && imageFiles.length > 0) opts.files = imageFiles;
143
+ try {
144
+ if (!replied) {
145
+ await message.reply(opts);
146
+ replied = true;
147
+ } else {
148
+ await channel.send(opts);
149
+ }
150
+ } catch (err) {
151
+ console.error(`Failed to send message: ${err}`);
152
+ }
153
+ }
154
+ },
155
+ onError: async (msg) => {
156
+ await message.reply(msg).catch(() => {
157
+ });
158
+ }
159
+ });
160
+ } finally {
161
+ clearInterval(typingInterval);
162
+ }
163
+ }
75
164
  async function loginWithRetry() {
76
165
  try {
77
166
  await client.login(token);
@@ -94,148 +183,3 @@ loginWithRetry().catch((err) => {
94
183
  console.error("Failed to connect to Discord:", err);
95
184
  process.exit(1);
96
185
  });
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
- await message.reply(`Error: agent returned ${res.status}`);
211
- clearInterval(typingInterval);
212
- return;
213
- }
214
- if (!res.body) {
215
- await message.reply("Error: no response from agent");
216
- clearInterval(typingInterval);
217
- return;
218
- }
219
- for await (const event of readNdjson(res.body)) {
220
- if (event.type === "text") {
221
- accumulated += event.content;
222
- } else if (event.type === "image") {
223
- pendingImages.push({
224
- data: event.data,
225
- media_type: event.media_type
226
- });
227
- } else if (event.type === "tool_use") {
228
- await flush();
229
- } else if (event.type === "done") {
230
- break;
231
- }
232
- }
233
- await flush();
234
- } catch (err) {
235
- const errMsg = err instanceof TypeError && err.cause?.code === "ECONNREFUSED" ? "Agent is not running" : `Error: ${err}`;
236
- await message.reply(errMsg).catch(() => {
237
- });
238
- } finally {
239
- clearInterval(typingInterval);
240
- }
241
- }
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ fireAndForget,
4
+ handleAgentMessage,
5
+ loadEnv,
6
+ loadFollowedChannels,
7
+ onShutdown,
8
+ splitMessage
9
+ } from "../chunk-MXUCNIBG.js";
10
+ import "../chunk-K3NQKI34.js";
11
+
12
+ // src/connectors/slack.ts
13
+ import { App } from "@slack/bolt";
14
+ var SLACK_MAX_LENGTH = 4e3;
15
+ var env = loadEnv();
16
+ var botToken = process.env.SLACK_BOT_TOKEN;
17
+ var appToken = process.env.SLACK_APP_TOKEN;
18
+ if (!botToken || !appToken) {
19
+ console.error("Missing required env vars: SLACK_BOT_TOKEN, SLACK_APP_TOKEN");
20
+ process.exit(1);
21
+ }
22
+ var followedChannelNames = loadFollowedChannels(env, "slack");
23
+ var followedChannelIds = /* @__PURE__ */ new Set();
24
+ var app = new App({
25
+ token: botToken,
26
+ socketMode: true,
27
+ appToken
28
+ });
29
+ var botUserId;
30
+ app.message(async ({ message, say }) => {
31
+ if (message.subtype) return;
32
+ if (!("user" in message) || !("text" in message)) return;
33
+ if ("bot_id" in message && message.bot_id) return;
34
+ const isDM = message.channel_type === "im";
35
+ const isMentioned = !isDM && botUserId && message.text?.includes(`<@${botUserId}>`);
36
+ const isFollowedChannel = !isDM && followedChannelIds.has(message.channel);
37
+ if (!isDM && !isMentioned && !isFollowedChannel) return;
38
+ let text = message.text ?? "";
39
+ if (isMentioned && botUserId) {
40
+ text = text.replace(new RegExp(`<@${botUserId}>`, "g"), "").trim();
41
+ }
42
+ const content = [];
43
+ if (text) content.push({ type: "text", text });
44
+ if ("files" in message && message.files) {
45
+ for (const file of message.files) {
46
+ if (!file.mimetype?.startsWith("image/") || !file.url_private) continue;
47
+ try {
48
+ const res = await fetch(file.url_private, {
49
+ headers: { Authorization: `Bearer ${botToken}` }
50
+ });
51
+ if (!res.ok) {
52
+ console.error(`Failed to download attachment: HTTP ${res.status}`);
53
+ continue;
54
+ }
55
+ const buffer = Buffer.from(await res.arrayBuffer());
56
+ content.push({
57
+ type: "image",
58
+ media_type: file.mimetype,
59
+ data: buffer.toString("base64")
60
+ });
61
+ } catch (err) {
62
+ console.error(`Failed to download attachment: ${err}`);
63
+ }
64
+ }
65
+ }
66
+ if (content.length === 0) return;
67
+ let channelName;
68
+ if (!isDM) {
69
+ try {
70
+ const info = await app.client.conversations.info({
71
+ channel: message.channel
72
+ });
73
+ channelName = info.channel?.name;
74
+ } catch (err) {
75
+ console.warn(`Failed to get channel name: ${err}`);
76
+ }
77
+ }
78
+ let senderName = message.user;
79
+ try {
80
+ const userInfo = await app.client.users.info({
81
+ user: message.user
82
+ });
83
+ senderName = userInfo.user?.profile?.display_name || userInfo.user?.profile?.real_name || message.user;
84
+ } catch (err) {
85
+ console.warn(`Failed to get user info: ${err}`);
86
+ }
87
+ const channelKey = `slack:${message.channel}`;
88
+ const payload = {
89
+ content,
90
+ channel: channelKey,
91
+ sender: senderName,
92
+ platform: "Slack",
93
+ ...isDM ? { isDM: true } : {},
94
+ ...channelName ? { channelName } : {}
95
+ };
96
+ if (isFollowedChannel && !isMentioned) {
97
+ await fireAndForget(env, payload);
98
+ return;
99
+ }
100
+ await handleAgentMessage(env, payload, {
101
+ onFlush: async (text2, images) => {
102
+ for (const img of images) {
103
+ const ext = img.media_type.split("/")[1] || "png";
104
+ try {
105
+ await app.client.filesUploadV2({
106
+ channel_id: message.channel,
107
+ file: Buffer.from(img.data, "base64"),
108
+ filename: `image.${ext}`
109
+ });
110
+ } catch (err) {
111
+ console.error(`Failed to upload image: ${err}`);
112
+ }
113
+ }
114
+ if (!text2) return;
115
+ const chunks = splitMessage(text2, SLACK_MAX_LENGTH);
116
+ for (const chunk of chunks) {
117
+ try {
118
+ await say(chunk);
119
+ } catch (err) {
120
+ console.error(`Failed to send message: ${err}`);
121
+ }
122
+ }
123
+ },
124
+ onError: async (msg) => {
125
+ await say(msg).catch(() => {
126
+ });
127
+ }
128
+ });
129
+ });
130
+ async function start() {
131
+ await app.start();
132
+ const auth = await app.client.auth.test();
133
+ if (!auth.user_id) {
134
+ throw new Error("auth.test succeeded but returned no user_id");
135
+ }
136
+ botUserId = auth.user_id;
137
+ console.log(`Connected to Slack as bot user ${botUserId}`);
138
+ console.log(`Bridging to agent: ${env.agentName} via ${env.baseUrl}/message`);
139
+ if (followedChannelNames.length > 0) {
140
+ try {
141
+ let cursor;
142
+ do {
143
+ const result = await app.client.conversations.list({
144
+ types: "public_channel,private_channel",
145
+ limit: 200,
146
+ ...cursor ? { cursor } : {}
147
+ });
148
+ for (const ch of result.channels ?? []) {
149
+ if (followedChannelNames.includes(ch.name)) {
150
+ followedChannelIds.add(ch.id);
151
+ console.log(`Following #${ch.name} (${ch.id})`);
152
+ }
153
+ }
154
+ cursor = result.response_metadata?.next_cursor || void 0;
155
+ } while (cursor);
156
+ } catch (err) {
157
+ console.error(`Failed to resolve channel names: ${err}`);
158
+ }
159
+ if (followedChannelIds.size === 0 && followedChannelNames.length > 0) {
160
+ console.warn(`No channels found matching: ${followedChannelNames.join(", ")}`);
161
+ }
162
+ }
163
+ }
164
+ onShutdown(async () => {
165
+ await app.stop();
166
+ });
167
+ start().catch((err) => {
168
+ console.error("Failed to start Slack connector:", err);
169
+ process.exit(1);
170
+ });