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.
- package/README.md +7 -7
- package/dist/{agent-manager-2LU6KULR.js → agent-manager-AUCKMGPR.js} +4 -4
- package/dist/{channel-H7N4SGR2.js → channel-DQ6UY7QB.js} +17 -40
- package/dist/{chunk-RALYNMHR.js → chunk-3C2XR4IY.js} +1 -1
- package/dist/chunk-5OCWMTVS.js +152 -0
- package/dist/{chunk-YEIHRP2J.js → chunk-DNOXHLE5.js} +1 -1
- package/dist/{chunk-IPIPLGME.js → chunk-I6OHXCMV.js} +4 -4
- package/dist/chunk-MXUCNIBG.js +168 -0
- package/dist/{chunk-DEUAVGSA.js → chunk-SOZA2TLP.js} +1 -1
- package/dist/{chunk-VVD3XO3E.js → chunk-YGFIWIOF.js} +1 -1
- package/dist/{chunk-N4YNKR3Q.js → chunk-ZHCE4DPY.js} +20 -0
- package/dist/cli.js +36 -24
- package/dist/connector-DKDJTLYZ.js +152 -0
- package/dist/connectors/discord.js +102 -158
- package/dist/connectors/slack.js +170 -0
- package/dist/connectors/telegram.js +156 -0
- package/dist/{create-RSWWMGKT.js → create-ILVOG75A.js} +5 -5
- package/dist/{daemon-client-27KMQQKX.js → daemon-client-XR24PUJF.js} +2 -2
- package/dist/daemon.js +271 -151
- package/dist/{delete-4ERL2QHH.js → delete-55MXCEY5.js} +5 -5
- package/dist/{down-HRC4MQCT.js → down-3OB6UVAJ.js} +1 -1
- package/dist/{env-DBWDTIP6.js → env-JB27UAC3.js} +2 -2
- package/dist/{history-W7BD2H74.js → history-BKG74I43.js} +4 -4
- package/dist/{import-6HTSSDFW.js → import-4CI2ZUTJ.js} +17 -2
- package/dist/{logs-NHWGHNBF.js → logs-NXFFGUKY.js} +1 -1
- package/dist/package-Z2SFO2SV.js +89 -0
- package/dist/{schedule-DKZ2E2CL.js → schedule-A35SH4HT.js} +4 -4
- package/dist/{send-5LEJXPYV.js → send-3U6OTKG7.js} +8 -4
- package/dist/{setup-ZMNTOJAV.js → setup-2FDVN7OF.js} +4 -4
- package/dist/{start-2BSXX6BS.js → start-LDPMCMYT.js} +2 -2
- package/dist/{status-N23CV27T.js → status-MVSQG54T.js} +2 -2
- package/dist/{stop-DSKBIJ2D.js → stop-5PZTZCLL.js} +2 -2
- package/dist/{up-4UGID4DM.js → up-F7TMTLRE.js} +1 -1
- package/dist/{upgrade-BGFVRCVP.js → upgrade-6ZW2RD64.js} +32 -19
- package/dist/{variant-JPLJTS2P.js → variant-T64BKARF.js} +130 -18
- package/dist/web-assets/assets/{index-BC5eSqbY.js → index-NS621maO.js} +23 -23
- package/dist/web-assets/index.html +1 -1
- package/package.json +3 -1
- package/templates/_base/_skills/volute-agent/SKILL.md +5 -4
- package/templates/_base/home/VOLUTE.md +18 -6
- package/templates/_base/src/lib/file-handler.ts +46 -0
- package/templates/_base/src/lib/router.ts +180 -0
- package/templates/_base/src/lib/routing.ts +100 -0
- package/templates/_base/src/lib/types.ts +13 -2
- package/templates/_base/src/lib/volute-server.ts +20 -48
- package/templates/agent-sdk/src/agent.ts +268 -82
- package/templates/agent-sdk/src/server.ts +12 -3
- package/templates/pi/src/agent.ts +277 -58
- package/templates/pi/src/server.ts +15 -4
- package/dist/chunk-MY74SUOL.js +0 -81
- package/dist/connector-6LWB5PRU.js +0 -96
- package/templates/_base/src/lib/sessions.ts +0 -71
- package/templates/agent-sdk/src/lib/agent-sessions.ts +0 -204
- 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
|
|
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
|
|
25
|
-
var
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
});
|