acp-discord 0.1.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/LICENSE +21 -0
- package/README.md +112 -0
- package/dist/chunk-SITRIFHO.js +122 -0
- package/dist/chunk-SITRIFHO.js.map +1 -0
- package/dist/daemon.d.ts +3 -0
- package/dist/daemon.js +606 -0
- package/dist/daemon.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +432 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -0
package/dist/daemon.js
ADDED
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
loadConfig,
|
|
4
|
+
removePid,
|
|
5
|
+
resolveChannelConfig,
|
|
6
|
+
writePid
|
|
7
|
+
} from "./chunk-SITRIFHO.js";
|
|
8
|
+
|
|
9
|
+
// src/daemon/index.ts
|
|
10
|
+
import { join } from "path";
|
|
11
|
+
import { homedir } from "os";
|
|
12
|
+
|
|
13
|
+
// src/daemon/discord-bot.ts
|
|
14
|
+
import {
|
|
15
|
+
Client,
|
|
16
|
+
GatewayIntentBits,
|
|
17
|
+
Events,
|
|
18
|
+
REST,
|
|
19
|
+
Routes,
|
|
20
|
+
SlashCommandBuilder,
|
|
21
|
+
ActionRowBuilder as ActionRowBuilder2,
|
|
22
|
+
ButtonBuilder as ButtonBuilder2,
|
|
23
|
+
ButtonStyle as ButtonStyle2
|
|
24
|
+
} from "discord.js";
|
|
25
|
+
|
|
26
|
+
// src/daemon/channel-router.ts
|
|
27
|
+
var ChannelRouter = class {
|
|
28
|
+
config;
|
|
29
|
+
constructor(config) {
|
|
30
|
+
this.config = config;
|
|
31
|
+
}
|
|
32
|
+
resolve(channelId) {
|
|
33
|
+
return resolveChannelConfig(this.config, channelId);
|
|
34
|
+
}
|
|
35
|
+
isConfigured(channelId) {
|
|
36
|
+
return this.resolve(channelId) !== null;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// src/daemon/session-manager.ts
|
|
41
|
+
import { spawn } from "child_process";
|
|
42
|
+
import { Readable, Writable } from "stream";
|
|
43
|
+
import { ClientSideConnection, ndJsonStream, PROTOCOL_VERSION } from "@agentclientprotocol/sdk";
|
|
44
|
+
|
|
45
|
+
// src/daemon/acp-client.ts
|
|
46
|
+
function createAcpClient(channelId, handlers, getRequestorId) {
|
|
47
|
+
return {
|
|
48
|
+
async requestPermission(params) {
|
|
49
|
+
const result = await handlers.onPermissionRequest(
|
|
50
|
+
channelId,
|
|
51
|
+
getRequestorId(),
|
|
52
|
+
{
|
|
53
|
+
toolCallId: params.toolCall.toolCallId,
|
|
54
|
+
title: params.toolCall.title ?? "Unknown",
|
|
55
|
+
kind: params.toolCall.kind ?? "other"
|
|
56
|
+
},
|
|
57
|
+
params.options.map((o) => ({
|
|
58
|
+
optionId: o.optionId,
|
|
59
|
+
name: o.name,
|
|
60
|
+
kind: o.kind
|
|
61
|
+
}))
|
|
62
|
+
);
|
|
63
|
+
if (result.outcome === "selected") {
|
|
64
|
+
return { outcome: { outcome: "selected", optionId: result.optionId } };
|
|
65
|
+
}
|
|
66
|
+
return { outcome: { outcome: "cancelled" } };
|
|
67
|
+
},
|
|
68
|
+
async sessionUpdate(params) {
|
|
69
|
+
const update = params.update;
|
|
70
|
+
switch (update.sessionUpdate) {
|
|
71
|
+
case "agent_message_chunk": {
|
|
72
|
+
if (update.content.type === "text") {
|
|
73
|
+
handlers.onAgentMessageChunk(channelId, update.content.text);
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
case "tool_call": {
|
|
78
|
+
handlers.onToolCall(
|
|
79
|
+
channelId,
|
|
80
|
+
update.toolCallId,
|
|
81
|
+
update.title ?? "Unknown",
|
|
82
|
+
update.kind ?? "other",
|
|
83
|
+
update.status ?? "pending"
|
|
84
|
+
);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
case "tool_call_update": {
|
|
88
|
+
handlers.onToolCallUpdate(
|
|
89
|
+
channelId,
|
|
90
|
+
update.toolCallId,
|
|
91
|
+
update.status ?? "in_progress"
|
|
92
|
+
);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/daemon/session-manager.ts
|
|
101
|
+
var SessionManager = class {
|
|
102
|
+
sessions = /* @__PURE__ */ new Map();
|
|
103
|
+
handlers;
|
|
104
|
+
constructor(handlers) {
|
|
105
|
+
this.handlers = handlers;
|
|
106
|
+
}
|
|
107
|
+
async prompt(channelId, text, agentConfig, requestorId) {
|
|
108
|
+
const session = await this.getOrCreate(channelId, agentConfig, requestorId);
|
|
109
|
+
session.lastActivity = Date.now();
|
|
110
|
+
this.resetIdleTimer(session, agentConfig.idle_timeout);
|
|
111
|
+
if (session.prompting) {
|
|
112
|
+
session.queue.push({ text, requestorId });
|
|
113
|
+
return "queued";
|
|
114
|
+
}
|
|
115
|
+
return this.executePrompt(session, text, requestorId, agentConfig);
|
|
116
|
+
}
|
|
117
|
+
async executePrompt(session, text, requestorId, agentConfig) {
|
|
118
|
+
session.prompting = true;
|
|
119
|
+
session.activePromptRequestorId = requestorId;
|
|
120
|
+
try {
|
|
121
|
+
const result = await session.connection.prompt({
|
|
122
|
+
sessionId: session.sessionId,
|
|
123
|
+
prompt: [{ type: "text", text }]
|
|
124
|
+
});
|
|
125
|
+
this.handlers.onPromptComplete(session.channelId, result.stopReason);
|
|
126
|
+
return result.stopReason;
|
|
127
|
+
} finally {
|
|
128
|
+
session.prompting = false;
|
|
129
|
+
const next = session.queue.shift();
|
|
130
|
+
if (next) {
|
|
131
|
+
this.executePrompt(session, next.text, next.requestorId, agentConfig).catch((err) => {
|
|
132
|
+
console.error(`Queued prompt failed for channel ${session.channelId}:`, err);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
cancel(channelId) {
|
|
138
|
+
const session = this.sessions.get(channelId);
|
|
139
|
+
if (session) {
|
|
140
|
+
session.connection.cancel({ sessionId: session.sessionId });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async getOrCreate(channelId, agentConfig, requestorId) {
|
|
144
|
+
const existing = this.sessions.get(channelId);
|
|
145
|
+
if (existing) return existing;
|
|
146
|
+
return this.createSession(channelId, agentConfig, requestorId);
|
|
147
|
+
}
|
|
148
|
+
async createSession(channelId, config, requestorId) {
|
|
149
|
+
const proc = spawn(config.command, config.args, {
|
|
150
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
151
|
+
cwd: config.cwd
|
|
152
|
+
});
|
|
153
|
+
proc.on("error", (err) => {
|
|
154
|
+
console.error(`Agent process error for channel ${channelId}:`, err);
|
|
155
|
+
const session = this.sessions.get(channelId);
|
|
156
|
+
if (session?.process === proc) {
|
|
157
|
+
clearTimeout(session.idleTimer);
|
|
158
|
+
this.sessions.delete(channelId);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
proc.on("exit", () => {
|
|
162
|
+
const session = this.sessions.get(channelId);
|
|
163
|
+
if (session?.process === proc) {
|
|
164
|
+
this.sessions.delete(channelId);
|
|
165
|
+
clearTimeout(session.idleTimer);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
let connection;
|
|
169
|
+
let sessionId;
|
|
170
|
+
try {
|
|
171
|
+
const stream = ndJsonStream(
|
|
172
|
+
Writable.toWeb(proc.stdin),
|
|
173
|
+
Readable.toWeb(proc.stdout)
|
|
174
|
+
);
|
|
175
|
+
const client = createAcpClient(channelId, this.handlers, () => {
|
|
176
|
+
return this.sessions.get(channelId)?.activePromptRequestorId ?? requestorId;
|
|
177
|
+
});
|
|
178
|
+
connection = new ClientSideConnection((_agent) => client, stream);
|
|
179
|
+
await connection.initialize({
|
|
180
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
181
|
+
clientCapabilities: {
|
|
182
|
+
fs: { readTextFile: true, writeTextFile: true },
|
|
183
|
+
terminal: true
|
|
184
|
+
},
|
|
185
|
+
clientInfo: {
|
|
186
|
+
name: "acp-discord",
|
|
187
|
+
title: "ACP Discord Bot",
|
|
188
|
+
version: "0.1.0"
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
const result = await connection.newSession({
|
|
192
|
+
cwd: config.cwd,
|
|
193
|
+
mcpServers: []
|
|
194
|
+
});
|
|
195
|
+
sessionId = result.sessionId;
|
|
196
|
+
} catch (err) {
|
|
197
|
+
proc.kill();
|
|
198
|
+
throw err;
|
|
199
|
+
}
|
|
200
|
+
const managed = {
|
|
201
|
+
channelId,
|
|
202
|
+
process: proc,
|
|
203
|
+
connection,
|
|
204
|
+
sessionId,
|
|
205
|
+
lastActivity: Date.now(),
|
|
206
|
+
idleTimer: this.startIdleTimer(channelId, config.idle_timeout),
|
|
207
|
+
prompting: false,
|
|
208
|
+
queue: [],
|
|
209
|
+
activePromptRequestorId: requestorId
|
|
210
|
+
};
|
|
211
|
+
this.sessions.set(channelId, managed);
|
|
212
|
+
return managed;
|
|
213
|
+
}
|
|
214
|
+
startIdleTimer(channelId, timeoutSec) {
|
|
215
|
+
return setTimeout(() => this.teardown(channelId), timeoutSec * 1e3);
|
|
216
|
+
}
|
|
217
|
+
resetIdleTimer(session, timeoutSec) {
|
|
218
|
+
clearTimeout(session.idleTimer);
|
|
219
|
+
session.idleTimer = this.startIdleTimer(session.channelId, timeoutSec);
|
|
220
|
+
}
|
|
221
|
+
teardown(channelId) {
|
|
222
|
+
const session = this.sessions.get(channelId);
|
|
223
|
+
if (!session) return;
|
|
224
|
+
clearTimeout(session.idleTimer);
|
|
225
|
+
session.process.kill();
|
|
226
|
+
this.sessions.delete(channelId);
|
|
227
|
+
}
|
|
228
|
+
teardownAll() {
|
|
229
|
+
for (const channelId of this.sessions.keys()) {
|
|
230
|
+
this.teardown(channelId);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
isPrompting(channelId) {
|
|
234
|
+
return this.sessions.get(channelId)?.prompting ?? false;
|
|
235
|
+
}
|
|
236
|
+
getActiveRequestorId(channelId) {
|
|
237
|
+
const session = this.sessions.get(channelId);
|
|
238
|
+
if (!session?.prompting) return null;
|
|
239
|
+
return session.activePromptRequestorId;
|
|
240
|
+
}
|
|
241
|
+
getActiveChannels() {
|
|
242
|
+
return Array.from(this.sessions.keys());
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// src/daemon/permission-ui.ts
|
|
247
|
+
import {
|
|
248
|
+
ActionRowBuilder,
|
|
249
|
+
ButtonBuilder,
|
|
250
|
+
ButtonStyle,
|
|
251
|
+
EmbedBuilder
|
|
252
|
+
} from "discord.js";
|
|
253
|
+
var KIND_LABELS = {
|
|
254
|
+
allow_once: "\u2705 Allow",
|
|
255
|
+
allow_always: "\u2705 Always Allow",
|
|
256
|
+
reject_once: "\u274C Reject",
|
|
257
|
+
reject_always: "\u274C Never Allow"
|
|
258
|
+
};
|
|
259
|
+
var KIND_STYLES = {
|
|
260
|
+
allow_once: ButtonStyle.Success,
|
|
261
|
+
allow_always: ButtonStyle.Success,
|
|
262
|
+
reject_once: ButtonStyle.Danger,
|
|
263
|
+
reject_always: ButtonStyle.Danger
|
|
264
|
+
};
|
|
265
|
+
async function sendPermissionRequest(channel, toolTitle, toolKind, options, requestorId, timeoutMs = 14 * 60 * 1e3) {
|
|
266
|
+
if (options.length === 0) {
|
|
267
|
+
return { outcome: "cancelled" };
|
|
268
|
+
}
|
|
269
|
+
const embed = new EmbedBuilder().setColor(16753920).setTitle(`Permission: ${toolTitle}`).setDescription(`Tool type: \`${toolKind}\``).setTimestamp();
|
|
270
|
+
const buttons = options.map(
|
|
271
|
+
(opt) => new ButtonBuilder().setCustomId(`perm_${opt.optionId}`).setLabel(KIND_LABELS[opt.kind] ?? opt.name).setStyle(KIND_STYLES[opt.kind] ?? ButtonStyle.Secondary)
|
|
272
|
+
);
|
|
273
|
+
const rows = [];
|
|
274
|
+
for (let i = 0; i < buttons.length; i += 5) {
|
|
275
|
+
rows.push(new ActionRowBuilder().addComponents(buttons.slice(i, i + 5)));
|
|
276
|
+
}
|
|
277
|
+
const msg = await channel.send({ embeds: [embed], components: rows });
|
|
278
|
+
return new Promise((resolve) => {
|
|
279
|
+
const collector = msg.createMessageComponentCollector({
|
|
280
|
+
filter: (i) => i.user.id === requestorId,
|
|
281
|
+
time: timeoutMs
|
|
282
|
+
});
|
|
283
|
+
collector.on("collect", async (interaction) => {
|
|
284
|
+
const optionId = interaction.customId.replace("perm_", "");
|
|
285
|
+
await interaction.update({ components: [] });
|
|
286
|
+
collector.stop("selected");
|
|
287
|
+
resolve({ outcome: "selected", optionId });
|
|
288
|
+
});
|
|
289
|
+
collector.on("end", (_collected, reason) => {
|
|
290
|
+
if (reason === "time") {
|
|
291
|
+
msg.edit({ components: [] }).catch(() => {
|
|
292
|
+
});
|
|
293
|
+
resolve({ outcome: "cancelled" });
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/daemon/message-bridge.ts
|
|
300
|
+
var DISCORD_MAX_LENGTH = 2e3;
|
|
301
|
+
function splitMessage(text, maxLength = DISCORD_MAX_LENGTH) {
|
|
302
|
+
if (text.length <= maxLength) return [text];
|
|
303
|
+
const chunks = [];
|
|
304
|
+
let remaining = text;
|
|
305
|
+
let inCodeBlock = false;
|
|
306
|
+
let codeFence = "";
|
|
307
|
+
while (remaining.length > 0) {
|
|
308
|
+
if (remaining.length <= maxLength) {
|
|
309
|
+
chunks.push(remaining);
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
let splitAt = maxLength;
|
|
313
|
+
const lastNewline = remaining.lastIndexOf("\n", maxLength);
|
|
314
|
+
if (lastNewline > maxLength * 0.5) {
|
|
315
|
+
splitAt = lastNewline + 1;
|
|
316
|
+
}
|
|
317
|
+
let chunk = remaining.slice(0, splitAt);
|
|
318
|
+
remaining = remaining.slice(splitAt);
|
|
319
|
+
const fenceMatches = chunk.match(/```\w*/g) || [];
|
|
320
|
+
for (const fence of fenceMatches) {
|
|
321
|
+
if (!inCodeBlock) {
|
|
322
|
+
inCodeBlock = true;
|
|
323
|
+
codeFence = fence;
|
|
324
|
+
} else {
|
|
325
|
+
inCodeBlock = false;
|
|
326
|
+
codeFence = "";
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (inCodeBlock) {
|
|
330
|
+
chunk += "\n```";
|
|
331
|
+
remaining = codeFence + "\n" + remaining;
|
|
332
|
+
inCodeBlock = false;
|
|
333
|
+
codeFence = "";
|
|
334
|
+
}
|
|
335
|
+
chunks.push(chunk);
|
|
336
|
+
}
|
|
337
|
+
return chunks;
|
|
338
|
+
}
|
|
339
|
+
var STATUS_ICONS = {
|
|
340
|
+
pending: "\u23F3",
|
|
341
|
+
// ⏳
|
|
342
|
+
in_progress: "\u{1F504}",
|
|
343
|
+
// 🔄
|
|
344
|
+
completed: "\u2705",
|
|
345
|
+
// ✅
|
|
346
|
+
failed: "\u274C"
|
|
347
|
+
// ❌
|
|
348
|
+
};
|
|
349
|
+
function formatToolSummary(tools) {
|
|
350
|
+
const lines = [];
|
|
351
|
+
for (const [, tool] of tools) {
|
|
352
|
+
lines.push(`${STATUS_ICONS[tool.status]} ${tool.title}`);
|
|
353
|
+
}
|
|
354
|
+
return lines.join("\n");
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/daemon/discord-bot.ts
|
|
358
|
+
async function startDiscordBot(config) {
|
|
359
|
+
const router = new ChannelRouter(config);
|
|
360
|
+
const toolStates = /* @__PURE__ */ new Map();
|
|
361
|
+
const toolSummaryMessages = /* @__PURE__ */ new Map();
|
|
362
|
+
const replyBuffers = /* @__PURE__ */ new Map();
|
|
363
|
+
const replyMessages = /* @__PURE__ */ new Map();
|
|
364
|
+
const flushTimers = /* @__PURE__ */ new Map();
|
|
365
|
+
let discordClient;
|
|
366
|
+
const handlers = {
|
|
367
|
+
onToolCall(channelId, toolCallId, title, _kind, status) {
|
|
368
|
+
if (!toolStates.has(channelId)) toolStates.set(channelId, /* @__PURE__ */ new Map());
|
|
369
|
+
toolStates.get(channelId).set(toolCallId, { title, status });
|
|
370
|
+
updateToolSummaryMessage(channelId);
|
|
371
|
+
},
|
|
372
|
+
onToolCallUpdate(channelId, toolCallId, status) {
|
|
373
|
+
const tools = toolStates.get(channelId);
|
|
374
|
+
const tool = tools?.get(toolCallId);
|
|
375
|
+
if (tool) {
|
|
376
|
+
tool.status = status;
|
|
377
|
+
updateToolSummaryMessage(channelId);
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
onAgentMessageChunk(channelId, text) {
|
|
381
|
+
const current = replyBuffers.get(channelId) ?? "";
|
|
382
|
+
replyBuffers.set(channelId, current + text);
|
|
383
|
+
scheduleFlushReply(channelId);
|
|
384
|
+
},
|
|
385
|
+
async onPermissionRequest(channelId, requestorId, toolCall, options) {
|
|
386
|
+
const channel = await fetchChannel(channelId);
|
|
387
|
+
if (!channel) return { outcome: "cancelled" };
|
|
388
|
+
return sendPermissionRequest(channel, toolCall.title, toolCall.kind, options, requestorId);
|
|
389
|
+
},
|
|
390
|
+
onPromptComplete(channelId, _stopReason) {
|
|
391
|
+
flushReply(channelId, true);
|
|
392
|
+
removeStopButton(channelId);
|
|
393
|
+
toolStates.delete(channelId);
|
|
394
|
+
toolSummaryMessages.delete(channelId);
|
|
395
|
+
replyBuffers.delete(channelId);
|
|
396
|
+
replyMessages.delete(channelId);
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
const sessionManager = new SessionManager(handlers);
|
|
400
|
+
async function fetchChannel(channelId) {
|
|
401
|
+
const cached = discordClient.channels.cache.get(channelId);
|
|
402
|
+
if (cached) return cached;
|
|
403
|
+
try {
|
|
404
|
+
const fetched = await discordClient.channels.fetch(channelId);
|
|
405
|
+
return fetched;
|
|
406
|
+
} catch {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async function updateToolSummaryMessage(channelId) {
|
|
411
|
+
const tools = toolStates.get(channelId);
|
|
412
|
+
if (!tools) return;
|
|
413
|
+
const content = formatToolSummary(tools);
|
|
414
|
+
const channel = await fetchChannel(channelId);
|
|
415
|
+
if (!channel) return;
|
|
416
|
+
const stopButton = new ActionRowBuilder2().addComponents(
|
|
417
|
+
new ButtonBuilder2().setCustomId(`stop_${channelId}`).setLabel("\u23F9 Stop").setStyle(ButtonStyle2.Secondary)
|
|
418
|
+
);
|
|
419
|
+
const existing = toolSummaryMessages.get(channelId);
|
|
420
|
+
if (existing) {
|
|
421
|
+
await existing.edit({ content, components: [stopButton] }).catch(() => {
|
|
422
|
+
});
|
|
423
|
+
} else {
|
|
424
|
+
const msg = await channel.send({ content, components: [stopButton] });
|
|
425
|
+
toolSummaryMessages.set(channelId, msg);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
async function removeStopButton(channelId) {
|
|
429
|
+
const msg = toolSummaryMessages.get(channelId);
|
|
430
|
+
if (msg) {
|
|
431
|
+
const tools = toolStates.get(channelId);
|
|
432
|
+
const content = tools ? formatToolSummary(tools) : msg.content;
|
|
433
|
+
await msg.edit({ content, components: [] }).catch(() => {
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
function scheduleFlushReply(channelId) {
|
|
438
|
+
if (flushTimers.has(channelId)) return;
|
|
439
|
+
flushTimers.set(
|
|
440
|
+
channelId,
|
|
441
|
+
setTimeout(() => {
|
|
442
|
+
flushTimers.delete(channelId);
|
|
443
|
+
flushReply(channelId, false);
|
|
444
|
+
}, 500)
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
async function flushReply(channelId, final) {
|
|
448
|
+
const timer = flushTimers.get(channelId);
|
|
449
|
+
if (timer) {
|
|
450
|
+
clearTimeout(timer);
|
|
451
|
+
flushTimers.delete(channelId);
|
|
452
|
+
}
|
|
453
|
+
const buffer = replyBuffers.get(channelId);
|
|
454
|
+
if (!buffer) return;
|
|
455
|
+
const channel = await fetchChannel(channelId);
|
|
456
|
+
if (!channel) return;
|
|
457
|
+
if (final) {
|
|
458
|
+
const existing = replyMessages.get(channelId);
|
|
459
|
+
if (existing) await existing.delete().catch(() => {
|
|
460
|
+
});
|
|
461
|
+
replyMessages.delete(channelId);
|
|
462
|
+
const chunks = splitMessage(buffer);
|
|
463
|
+
for (const chunk of chunks) {
|
|
464
|
+
await channel.send(chunk);
|
|
465
|
+
}
|
|
466
|
+
replyBuffers.delete(channelId);
|
|
467
|
+
} else {
|
|
468
|
+
const truncated = buffer.length > 2e3 ? buffer.slice(buffer.length - 1900) + "..." : buffer;
|
|
469
|
+
const existing = replyMessages.get(channelId);
|
|
470
|
+
if (existing) {
|
|
471
|
+
await existing.edit(truncated).catch(() => {
|
|
472
|
+
});
|
|
473
|
+
} else {
|
|
474
|
+
const msg = await channel.send(truncated);
|
|
475
|
+
replyMessages.set(channelId, msg);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
discordClient = new Client({
|
|
480
|
+
intents: [
|
|
481
|
+
GatewayIntentBits.Guilds,
|
|
482
|
+
GatewayIntentBits.GuildMessages,
|
|
483
|
+
GatewayIntentBits.MessageContent
|
|
484
|
+
]
|
|
485
|
+
});
|
|
486
|
+
discordClient.on(Events.ClientReady, async (c) => {
|
|
487
|
+
console.log(`Discord bot ready: ${c.user.tag}`);
|
|
488
|
+
const askCommand = new SlashCommandBuilder().setName("ask").setDescription("Ask the coding agent a question").addStringOption(
|
|
489
|
+
(opt) => opt.setName("message").setDescription("Your message").setRequired(true)
|
|
490
|
+
);
|
|
491
|
+
const rest = new REST().setToken(config.discord.token);
|
|
492
|
+
try {
|
|
493
|
+
await rest.put(Routes.applicationCommands(c.application.id), {
|
|
494
|
+
body: [askCommand.toJSON()]
|
|
495
|
+
});
|
|
496
|
+
console.log("Registered /ask command");
|
|
497
|
+
} catch (err) {
|
|
498
|
+
console.error("Failed to register commands:", err);
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
discordClient.on(Events.MessageCreate, async (message) => {
|
|
502
|
+
if (message.author.bot) return;
|
|
503
|
+
const channelId = message.channelId;
|
|
504
|
+
const resolved = router.resolve(channelId);
|
|
505
|
+
if (!resolved) return;
|
|
506
|
+
const isMention = message.mentions.has(discordClient.user);
|
|
507
|
+
if (!isMention) return;
|
|
508
|
+
const text = message.content.replace(/<@!?\d+>/g, "").trim();
|
|
509
|
+
if (!text) {
|
|
510
|
+
await message.reply("Please provide a message.");
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (sessionManager.isPrompting(channelId)) {
|
|
514
|
+
await message.reply("\u23F3 Agent is working. Your message has been queued.");
|
|
515
|
+
}
|
|
516
|
+
try {
|
|
517
|
+
await sessionManager.prompt(channelId, text, resolved.agent, message.author.id);
|
|
518
|
+
} catch (err) {
|
|
519
|
+
console.error(`Prompt failed for channel ${channelId}:`, err);
|
|
520
|
+
await message.reply("An error occurred while processing your request.").catch(() => {
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
discordClient.on(Events.InteractionCreate, async (interaction) => {
|
|
525
|
+
if (!interaction.isButton()) return;
|
|
526
|
+
if (interaction.customId.startsWith("stop_")) {
|
|
527
|
+
const channelId = interaction.customId.replace("stop_", "");
|
|
528
|
+
const activeRequestor = sessionManager.getActiveRequestorId(channelId);
|
|
529
|
+
if (activeRequestor && interaction.user.id !== activeRequestor) {
|
|
530
|
+
await interaction.reply({ content: "Only the user who started this prompt can stop it.", ephemeral: true });
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
sessionManager.cancel(channelId);
|
|
534
|
+
await interaction.update({ components: [] });
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
discordClient.on(Events.InteractionCreate, async (interaction) => {
|
|
538
|
+
if (!interaction.isChatInputCommand()) return;
|
|
539
|
+
if (interaction.commandName !== "ask") return;
|
|
540
|
+
const channelId = interaction.channelId;
|
|
541
|
+
const resolved = router.resolve(channelId);
|
|
542
|
+
if (!resolved) {
|
|
543
|
+
await interaction.reply({ content: "This channel is not configured for ACP.", ephemeral: true });
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
const text = interaction.options.getString("message", true);
|
|
547
|
+
await interaction.deferReply();
|
|
548
|
+
if (sessionManager.isPrompting(channelId)) {
|
|
549
|
+
await interaction.editReply("\u23F3 Agent is working. Your message has been queued.");
|
|
550
|
+
} else {
|
|
551
|
+
await interaction.editReply(`\u{1F4AC} Processing: ${text.slice(0, 100)}...`);
|
|
552
|
+
}
|
|
553
|
+
try {
|
|
554
|
+
await sessionManager.prompt(channelId, text, resolved.agent, interaction.user.id);
|
|
555
|
+
} catch (err) {
|
|
556
|
+
console.error(`Prompt failed for channel ${channelId}:`, err);
|
|
557
|
+
await interaction.followUp({ content: "An error occurred while processing your request.", ephemeral: true }).catch(() => {
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
process.on("SIGTERM", () => {
|
|
562
|
+
sessionManager.teardownAll();
|
|
563
|
+
discordClient.destroy();
|
|
564
|
+
});
|
|
565
|
+
process.on("SIGINT", () => {
|
|
566
|
+
sessionManager.teardownAll();
|
|
567
|
+
discordClient.destroy();
|
|
568
|
+
});
|
|
569
|
+
try {
|
|
570
|
+
await discordClient.login(config.discord.token);
|
|
571
|
+
} catch (err) {
|
|
572
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
573
|
+
if (message.includes("TOKEN_INVALID") || message.includes("An invalid token was provided")) {
|
|
574
|
+
console.error("Error: Invalid Discord bot token. Check your config.toml.");
|
|
575
|
+
} else if (message.includes("ConnectTimeout") || message.includes("ETIMEDOUT") || message.includes("ECONNREFUSED")) {
|
|
576
|
+
console.error("Error: Cannot connect to Discord API. Check your network or proxy settings.");
|
|
577
|
+
console.error("Hint: Set HTTPS_PROXY=http://127.0.0.1:7890 if you need a proxy.");
|
|
578
|
+
} else {
|
|
579
|
+
console.error("Error: Failed to connect to Discord:", message);
|
|
580
|
+
}
|
|
581
|
+
process.exit(1);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// src/daemon/index.ts
|
|
586
|
+
var CONFIG_DIR = join(homedir(), ".acp-discord");
|
|
587
|
+
var CONFIG_PATH = join(CONFIG_DIR, "config.toml");
|
|
588
|
+
var PID_PATH = join(CONFIG_DIR, "daemon.pid");
|
|
589
|
+
async function runDaemon() {
|
|
590
|
+
const config = loadConfig(CONFIG_PATH);
|
|
591
|
+
writePid(PID_PATH, process.pid);
|
|
592
|
+
process.on("exit", () => removePid(PID_PATH));
|
|
593
|
+
console.log(`acp-discord daemon started (PID: ${process.pid})`);
|
|
594
|
+
console.log(`Loaded config: ${Object.keys(config.channels).length} channel(s)`);
|
|
595
|
+
await startDiscordBot(config);
|
|
596
|
+
}
|
|
597
|
+
if (process.env.ACP_DISCORD_DAEMON === "1") {
|
|
598
|
+
runDaemon().catch((err) => {
|
|
599
|
+
console.error("Daemon failed:", err);
|
|
600
|
+
process.exit(1);
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
export {
|
|
604
|
+
runDaemon
|
|
605
|
+
};
|
|
606
|
+
//# sourceMappingURL=daemon.js.map
|