acp-discord 0.3.0 → 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.
- package/README.md +15 -5
- package/dist/daemon.js +122 -63
- package/dist/daemon.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,12 +6,15 @@ Send a message in Discord, get AI coding assistance back — with tool call visu
|
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
-
- **Slash commands & mentions** — `/ask <message
|
|
9
|
+
- **Slash commands & mentions** — `/ask <message>`, `/clear`, or `@bot message`
|
|
10
10
|
- **Real-time streaming** — agent responses stream into Discord with smart message splitting
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
11
|
+
- **File diffs** — see unified diffs in Discord when the agent modifies files
|
|
12
|
+
- **Tool call visualization** — see what the agent is doing (⏳ pending → 🔄 running → ✅ done / ❌ failed), with a ⏹️ stop button to cancel
|
|
13
|
+
- **Permission UI** — Discord buttons for approving/denying agent actions, with file diffs shown inline for review before approval
|
|
14
|
+
- **Auto-reply mode** — optionally respond to all messages in a channel, not just mentions
|
|
13
15
|
- **Multi-agent support** — different channels can use different agents
|
|
14
16
|
- **Daemon mode** — runs in background with auto-start (systemd/launchd)
|
|
17
|
+
- **Self-update** — `acp-discord update` to update in-place, auto-restarts the daemon
|
|
15
18
|
- **Interactive setup** — guided `init` wizard for first-time configuration
|
|
16
19
|
|
|
17
20
|
## Prerequisites
|
|
@@ -42,11 +45,12 @@ token = "your-discord-bot-token"
|
|
|
42
45
|
command = "claude-code"
|
|
43
46
|
args = ["--acp"]
|
|
44
47
|
cwd = "/path/to/your/project"
|
|
45
|
-
idle_timeout = 600 # seconds
|
|
48
|
+
idle_timeout = 600 # seconds before idle session is terminated (default: 600)
|
|
46
49
|
|
|
47
50
|
[channels.1234567890123456]
|
|
48
51
|
agent = "claude"
|
|
49
|
-
cwd = "/override/path"
|
|
52
|
+
cwd = "/override/path" # optional, per-channel working directory override
|
|
53
|
+
auto_reply = true # optional, respond to all messages (default: false, mention-only)
|
|
50
54
|
```
|
|
51
55
|
|
|
52
56
|
### Discord Bot Setup
|
|
@@ -74,6 +78,9 @@ acp-discord daemon status # Check if running
|
|
|
74
78
|
# Auto-start on boot
|
|
75
79
|
acp-discord daemon enable # Setup systemd (Linux) / launchd (macOS)
|
|
76
80
|
acp-discord daemon disable # Remove auto-start
|
|
81
|
+
|
|
82
|
+
# Self-update
|
|
83
|
+
acp-discord update # Update to latest version, auto-restarts daemon
|
|
77
84
|
```
|
|
78
85
|
|
|
79
86
|
### Discord Commands
|
|
@@ -81,8 +88,11 @@ acp-discord daemon disable # Remove auto-start
|
|
|
81
88
|
| Command | Description |
|
|
82
89
|
|---------|-------------|
|
|
83
90
|
| `/ask <message>` | Send a prompt to the coding agent |
|
|
91
|
+
| `/clear` | Clear the current session and start fresh |
|
|
84
92
|
| `@bot <message>` | Mention the bot to send a prompt |
|
|
85
93
|
|
|
94
|
+
If a prompt is sent while the agent is already working, it gets queued and processed after the current task completes.
|
|
95
|
+
|
|
86
96
|
### Development
|
|
87
97
|
|
|
88
98
|
```bash
|
package/dist/daemon.js
CHANGED
|
@@ -46,6 +46,7 @@ import { ClientSideConnection, ndJsonStream, PROTOCOL_VERSION } from "@agentclie
|
|
|
46
46
|
function createAcpClient(channelId, handlers, getRequestorId) {
|
|
47
47
|
return {
|
|
48
48
|
async requestPermission(params) {
|
|
49
|
+
const diffs = extractDiffs(params.toolCall.content);
|
|
49
50
|
const result = await handlers.onPermissionRequest(
|
|
50
51
|
channelId,
|
|
51
52
|
getRequestorId(),
|
|
@@ -58,7 +59,8 @@ function createAcpClient(channelId, handlers, getRequestorId) {
|
|
|
58
59
|
optionId: o.optionId,
|
|
59
60
|
name: o.name,
|
|
60
61
|
kind: o.kind
|
|
61
|
-
}))
|
|
62
|
+
})),
|
|
63
|
+
diffs
|
|
62
64
|
);
|
|
63
65
|
if (result.outcome === "selected") {
|
|
64
66
|
return { outcome: { outcome: "selected", optionId: result.optionId } };
|
|
@@ -76,23 +78,29 @@ function createAcpClient(channelId, handlers, getRequestorId) {
|
|
|
76
78
|
}
|
|
77
79
|
case "tool_call": {
|
|
78
80
|
const toolCallDiffs = extractDiffs(update.content);
|
|
81
|
+
const rawVal = update.rawInput;
|
|
82
|
+
const rawInput = typeof rawVal === "object" && rawVal !== null && !Array.isArray(rawVal) ? rawVal : void 0;
|
|
79
83
|
handlers.onToolCall(
|
|
80
84
|
channelId,
|
|
81
85
|
update.toolCallId,
|
|
82
86
|
update.title ?? "Unknown",
|
|
83
87
|
update.kind ?? "other",
|
|
84
88
|
update.status ?? "pending",
|
|
85
|
-
toolCallDiffs
|
|
89
|
+
toolCallDiffs,
|
|
90
|
+
rawInput
|
|
86
91
|
);
|
|
87
92
|
break;
|
|
88
93
|
}
|
|
89
94
|
case "tool_call_update": {
|
|
90
95
|
const updateDiffs = extractDiffs(update.content);
|
|
96
|
+
const updateRawVal = update.rawInput;
|
|
97
|
+
const updateRawInput = typeof updateRawVal === "object" && updateRawVal !== null && !Array.isArray(updateRawVal) ? updateRawVal : void 0;
|
|
91
98
|
handlers.onToolCallUpdate(
|
|
92
99
|
channelId,
|
|
93
100
|
update.toolCallId,
|
|
94
101
|
update.status ?? "in_progress",
|
|
95
|
-
updateDiffs
|
|
102
|
+
updateDiffs,
|
|
103
|
+
updateRawInput
|
|
96
104
|
);
|
|
97
105
|
break;
|
|
98
106
|
}
|
|
@@ -105,11 +113,10 @@ function extractDiffs(content) {
|
|
|
105
113
|
const diffs = [];
|
|
106
114
|
for (const item of content) {
|
|
107
115
|
if (item && typeof item === "object" && "type" in item && item.type === "diff") {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
});
|
|
116
|
+
const { path, oldText, newText } = item;
|
|
117
|
+
if (typeof path !== "string" || typeof newText !== "string") continue;
|
|
118
|
+
if (oldText !== void 0 && oldText !== null && typeof oldText !== "string") continue;
|
|
119
|
+
diffs.push({ path, oldText: oldText ?? null, newText });
|
|
113
120
|
}
|
|
114
121
|
}
|
|
115
122
|
return diffs;
|
|
@@ -268,51 +275,6 @@ import {
|
|
|
268
275
|
ButtonStyle,
|
|
269
276
|
EmbedBuilder
|
|
270
277
|
} from "discord.js";
|
|
271
|
-
var KIND_LABELS = {
|
|
272
|
-
allow_once: "\u2705 Allow",
|
|
273
|
-
allow_always: "\u2705 Always Allow",
|
|
274
|
-
reject_once: "\u274C Reject",
|
|
275
|
-
reject_always: "\u274C Never Allow"
|
|
276
|
-
};
|
|
277
|
-
var KIND_STYLES = {
|
|
278
|
-
allow_once: ButtonStyle.Success,
|
|
279
|
-
allow_always: ButtonStyle.Success,
|
|
280
|
-
reject_once: ButtonStyle.Danger,
|
|
281
|
-
reject_always: ButtonStyle.Danger
|
|
282
|
-
};
|
|
283
|
-
async function sendPermissionRequest(channel, toolTitle, toolKind, options, requestorId, timeoutMs = 14 * 60 * 1e3) {
|
|
284
|
-
if (options.length === 0) {
|
|
285
|
-
return { outcome: "cancelled" };
|
|
286
|
-
}
|
|
287
|
-
const embed = new EmbedBuilder().setColor(16753920).setTitle(`Permission: ${toolTitle}`).setDescription(`Tool type: \`${toolKind}\``).setTimestamp();
|
|
288
|
-
const buttons = options.map(
|
|
289
|
-
(opt) => new ButtonBuilder().setCustomId(`perm_${opt.optionId}`).setLabel(KIND_LABELS[opt.kind] ?? opt.name).setStyle(KIND_STYLES[opt.kind] ?? ButtonStyle.Secondary)
|
|
290
|
-
);
|
|
291
|
-
const rows = [];
|
|
292
|
-
for (let i = 0; i < buttons.length; i += 5) {
|
|
293
|
-
rows.push(new ActionRowBuilder().addComponents(buttons.slice(i, i + 5)));
|
|
294
|
-
}
|
|
295
|
-
const msg = await channel.send({ embeds: [embed], components: rows });
|
|
296
|
-
return new Promise((resolve) => {
|
|
297
|
-
const collector = msg.createMessageComponentCollector({
|
|
298
|
-
filter: (i) => i.user.id === requestorId,
|
|
299
|
-
time: timeoutMs
|
|
300
|
-
});
|
|
301
|
-
collector.on("collect", async (interaction) => {
|
|
302
|
-
const optionId = interaction.customId.replace("perm_", "");
|
|
303
|
-
await interaction.update({ components: [] });
|
|
304
|
-
collector.stop("selected");
|
|
305
|
-
resolve({ outcome: "selected", optionId });
|
|
306
|
-
});
|
|
307
|
-
collector.on("end", (_collected, reason) => {
|
|
308
|
-
if (reason === "time") {
|
|
309
|
-
msg.edit({ components: [] }).catch(() => {
|
|
310
|
-
});
|
|
311
|
-
resolve({ outcome: "cancelled" });
|
|
312
|
-
}
|
|
313
|
-
});
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
278
|
|
|
317
279
|
// src/daemon/message-bridge.ts
|
|
318
280
|
import { createTwoFilesPatch } from "diff";
|
|
@@ -369,10 +331,31 @@ var STATUS_ICONS = {
|
|
|
369
331
|
function formatToolSummary(tools) {
|
|
370
332
|
const lines = [];
|
|
371
333
|
for (const [, tool] of tools) {
|
|
372
|
-
|
|
334
|
+
const detail = extractToolDetail(tool.rawInput);
|
|
335
|
+
const suffix = detail ? ` \xB7 \`${detail}\`` : "";
|
|
336
|
+
lines.push(`${STATUS_ICONS[tool.status]} ${tool.title}${suffix}`);
|
|
373
337
|
}
|
|
374
338
|
return lines.join("\n");
|
|
375
339
|
}
|
|
340
|
+
var MAX_DETAIL_LENGTH = 80;
|
|
341
|
+
var SAFE_FIELDS = ["command", "file_path", "pattern", "query", "path", "url", "description"];
|
|
342
|
+
function extractToolDetail(rawInput) {
|
|
343
|
+
if (!rawInput) return null;
|
|
344
|
+
for (const field of SAFE_FIELDS) {
|
|
345
|
+
if (typeof rawInput[field] === "string" && rawInput[field]) {
|
|
346
|
+
return truncate(sanitizeDetail(rawInput[field]), MAX_DETAIL_LENGTH);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
function sanitizeDetail(text) {
|
|
352
|
+
return text.replace(/`/g, "'");
|
|
353
|
+
}
|
|
354
|
+
function truncate(text, max) {
|
|
355
|
+
const firstLine = text.split("\n")[0];
|
|
356
|
+
if (firstLine.length <= max) return firstLine;
|
|
357
|
+
return firstLine.slice(0, max - 1) + "\u2026";
|
|
358
|
+
}
|
|
376
359
|
function formatDiff(diffs, maxLines = MAX_DIFF_LINES) {
|
|
377
360
|
if (diffs.length === 0) return [];
|
|
378
361
|
const parts = [];
|
|
@@ -411,6 +394,65 @@ ${displayLines.join("\n")}
|
|
|
411
394
|
return splitMessage(fullMessage);
|
|
412
395
|
}
|
|
413
396
|
|
|
397
|
+
// src/daemon/permission-ui.ts
|
|
398
|
+
var KIND_LABELS = {
|
|
399
|
+
allow_once: "\u2705 Allow",
|
|
400
|
+
allow_always: "\u2705 Always Allow",
|
|
401
|
+
reject_once: "\u274C Reject",
|
|
402
|
+
reject_always: "\u274C Never Allow"
|
|
403
|
+
};
|
|
404
|
+
var KIND_STYLES = {
|
|
405
|
+
allow_once: ButtonStyle.Success,
|
|
406
|
+
allow_always: ButtonStyle.Success,
|
|
407
|
+
reject_once: ButtonStyle.Danger,
|
|
408
|
+
reject_always: ButtonStyle.Danger
|
|
409
|
+
};
|
|
410
|
+
async function sendPermissionRequest(channel, toolTitle, toolKind, options, requestorId, diffs = [], timeoutMs = 14 * 60 * 1e3) {
|
|
411
|
+
if (options.length === 0) {
|
|
412
|
+
return { outcome: "cancelled" };
|
|
413
|
+
}
|
|
414
|
+
let diffsSent = false;
|
|
415
|
+
if (diffs.length > 0) {
|
|
416
|
+
try {
|
|
417
|
+
const diffMessages = formatDiff(diffs);
|
|
418
|
+
for (const msg2 of diffMessages) {
|
|
419
|
+
await channel.send(msg2);
|
|
420
|
+
}
|
|
421
|
+
diffsSent = true;
|
|
422
|
+
} catch (err) {
|
|
423
|
+
console.error("Failed to send permission diffs:", err);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const embed = new EmbedBuilder().setColor(16753920).setTitle(`Permission: ${toolTitle}`).setDescription(`Tool type: \`${toolKind}\``).setTimestamp();
|
|
427
|
+
const buttons = options.map(
|
|
428
|
+
(opt) => new ButtonBuilder().setCustomId(`perm_${opt.optionId}`).setLabel(KIND_LABELS[opt.kind] ?? opt.name).setStyle(KIND_STYLES[opt.kind] ?? ButtonStyle.Secondary)
|
|
429
|
+
);
|
|
430
|
+
const rows = [];
|
|
431
|
+
for (let i = 0; i < buttons.length; i += 5) {
|
|
432
|
+
rows.push(new ActionRowBuilder().addComponents(buttons.slice(i, i + 5)));
|
|
433
|
+
}
|
|
434
|
+
const msg = await channel.send({ embeds: [embed], components: rows });
|
|
435
|
+
return new Promise((resolve) => {
|
|
436
|
+
const collector = msg.createMessageComponentCollector({
|
|
437
|
+
filter: (i) => i.user.id === requestorId,
|
|
438
|
+
time: timeoutMs
|
|
439
|
+
});
|
|
440
|
+
collector.on("collect", async (interaction) => {
|
|
441
|
+
const optionId = interaction.customId.replace("perm_", "");
|
|
442
|
+
await interaction.update({ components: [] });
|
|
443
|
+
collector.stop("selected");
|
|
444
|
+
resolve({ outcome: "selected", optionId, diffsSent });
|
|
445
|
+
});
|
|
446
|
+
collector.on("end", (_collected, reason) => {
|
|
447
|
+
if (reason === "time") {
|
|
448
|
+
msg.edit({ components: [] }).catch(() => {
|
|
449
|
+
});
|
|
450
|
+
resolve({ outcome: "cancelled", diffsSent });
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
414
456
|
// src/daemon/discord-bot.ts
|
|
415
457
|
async function startDiscordBot(config) {
|
|
416
458
|
const router = new ChannelRouter(config);
|
|
@@ -420,20 +462,22 @@ async function startDiscordBot(config) {
|
|
|
420
462
|
const replyMessages = /* @__PURE__ */ new Map();
|
|
421
463
|
const flushTimers = /* @__PURE__ */ new Map();
|
|
422
464
|
const pendingDiffs = /* @__PURE__ */ new Map();
|
|
465
|
+
const permissionDiffShown = /* @__PURE__ */ new Map();
|
|
423
466
|
let discordClient;
|
|
424
467
|
const handlers = {
|
|
425
|
-
onToolCall(channelId, toolCallId, title, _kind, status, diffs) {
|
|
468
|
+
onToolCall(channelId, toolCallId, title, _kind, status, diffs, rawInput) {
|
|
426
469
|
if (!toolStates.has(channelId)) toolStates.set(channelId, /* @__PURE__ */ new Map());
|
|
427
|
-
toolStates.get(channelId).set(toolCallId, { title, status });
|
|
470
|
+
toolStates.get(channelId).set(toolCallId, { title, status, rawInput });
|
|
428
471
|
accumulateDiffs(channelId, toolCallId, diffs);
|
|
429
472
|
updateToolSummaryMessage(channelId);
|
|
430
473
|
if (status === "completed") sendDiffsForTool(channelId, toolCallId);
|
|
431
474
|
},
|
|
432
|
-
onToolCallUpdate(channelId, toolCallId, status, diffs) {
|
|
475
|
+
onToolCallUpdate(channelId, toolCallId, status, diffs, rawInput) {
|
|
433
476
|
const tools = toolStates.get(channelId);
|
|
434
477
|
const tool = tools?.get(toolCallId);
|
|
435
478
|
if (tool) {
|
|
436
479
|
tool.status = status;
|
|
480
|
+
if (rawInput && !tool.rawInput) tool.rawInput = rawInput;
|
|
437
481
|
accumulateDiffs(channelId, toolCallId, diffs);
|
|
438
482
|
updateToolSummaryMessage(channelId);
|
|
439
483
|
if (status === "completed") sendDiffsForTool(channelId, toolCallId);
|
|
@@ -444,10 +488,15 @@ async function startDiscordBot(config) {
|
|
|
444
488
|
replyBuffers.set(channelId, current + text);
|
|
445
489
|
scheduleFlushReply(channelId);
|
|
446
490
|
},
|
|
447
|
-
async onPermissionRequest(channelId, requestorId, toolCall, options) {
|
|
491
|
+
async onPermissionRequest(channelId, requestorId, toolCall, options, diffs) {
|
|
448
492
|
const channel = await fetchChannel(channelId);
|
|
449
493
|
if (!channel) return { outcome: "cancelled" };
|
|
450
|
-
|
|
494
|
+
const result = await sendPermissionRequest(channel, toolCall.title, toolCall.kind, options, requestorId, diffs);
|
|
495
|
+
if (result.diffsSent) {
|
|
496
|
+
if (!permissionDiffShown.has(channelId)) permissionDiffShown.set(channelId, /* @__PURE__ */ new Set());
|
|
497
|
+
permissionDiffShown.get(channelId).add(toolCall.toolCallId);
|
|
498
|
+
}
|
|
499
|
+
return result;
|
|
451
500
|
},
|
|
452
501
|
onPromptComplete(channelId, _stopReason) {
|
|
453
502
|
flushReply(channelId, true);
|
|
@@ -457,6 +506,7 @@ async function startDiscordBot(config) {
|
|
|
457
506
|
replyBuffers.delete(channelId);
|
|
458
507
|
replyMessages.delete(channelId);
|
|
459
508
|
pendingDiffs.delete(channelId);
|
|
509
|
+
permissionDiffShown.delete(channelId);
|
|
460
510
|
}
|
|
461
511
|
};
|
|
462
512
|
const sessionManager = new SessionManager(handlers);
|
|
@@ -468,6 +518,12 @@ async function startDiscordBot(config) {
|
|
|
468
518
|
channelDiffs.set(toolCallId, existing.concat(diffs));
|
|
469
519
|
}
|
|
470
520
|
async function sendDiffsForTool(channelId, toolCallId) {
|
|
521
|
+
const shownSet = permissionDiffShown.get(channelId);
|
|
522
|
+
if (shownSet?.has(toolCallId)) {
|
|
523
|
+
shownSet.delete(toolCallId);
|
|
524
|
+
pendingDiffs.get(channelId)?.delete(toolCallId);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
471
527
|
const channelDiffs = pendingDiffs.get(channelId);
|
|
472
528
|
const diffs = channelDiffs?.get(toolCallId);
|
|
473
529
|
if (!diffs || diffs.length === 0) return;
|
|
@@ -475,7 +531,7 @@ async function startDiscordBot(config) {
|
|
|
475
531
|
if (!channel) return;
|
|
476
532
|
const messages = formatDiff(diffs);
|
|
477
533
|
for (const msg of messages) {
|
|
478
|
-
await channel.send(msg);
|
|
534
|
+
await channel.send({ content: msg, allowedMentions: { parse: [] } });
|
|
479
535
|
}
|
|
480
536
|
channelDiffs.delete(toolCallId);
|
|
481
537
|
}
|
|
@@ -498,12 +554,13 @@ async function startDiscordBot(config) {
|
|
|
498
554
|
const stopButton = new ActionRowBuilder2().addComponents(
|
|
499
555
|
new ButtonBuilder2().setCustomId(`stop_${channelId}`).setLabel("\u23F9 Stop").setStyle(ButtonStyle2.Secondary)
|
|
500
556
|
);
|
|
557
|
+
const noMentions = { parse: [] };
|
|
501
558
|
const existing = toolSummaryMessages.get(channelId);
|
|
502
559
|
if (existing) {
|
|
503
|
-
await existing.edit({ content, components: [stopButton] }).catch(() => {
|
|
560
|
+
await existing.edit({ content, components: [stopButton], allowedMentions: noMentions }).catch(() => {
|
|
504
561
|
});
|
|
505
562
|
} else {
|
|
506
|
-
const msg = await channel.send({ content, components: [stopButton] });
|
|
563
|
+
const msg = await channel.send({ content, components: [stopButton], allowedMentions: noMentions });
|
|
507
564
|
toolSummaryMessages.set(channelId, msg);
|
|
508
565
|
}
|
|
509
566
|
}
|
|
@@ -512,7 +569,7 @@ async function startDiscordBot(config) {
|
|
|
512
569
|
if (msg) {
|
|
513
570
|
const tools = toolStates.get(channelId);
|
|
514
571
|
const content = tools ? formatToolSummary(tools) : msg.content;
|
|
515
|
-
await msg.edit({ content, components: [] }).catch(() => {
|
|
572
|
+
await msg.edit({ content, components: [], allowedMentions: { parse: [] } }).catch(() => {
|
|
516
573
|
});
|
|
517
574
|
}
|
|
518
575
|
}
|
|
@@ -650,6 +707,8 @@ async function startDiscordBot(config) {
|
|
|
650
707
|
toolSummaryMessages.delete(channelId);
|
|
651
708
|
replyBuffers.delete(channelId);
|
|
652
709
|
replyMessages.delete(channelId);
|
|
710
|
+
pendingDiffs.delete(channelId);
|
|
711
|
+
permissionDiffShown.delete(channelId);
|
|
653
712
|
const timer = flushTimers.get(channelId);
|
|
654
713
|
if (timer) clearTimeout(timer);
|
|
655
714
|
flushTimers.delete(channelId);
|
package/dist/daemon.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/daemon/index.ts","../src/daemon/discord-bot.ts","../src/daemon/channel-router.ts","../src/daemon/session-manager.ts","../src/daemon/acp-client.ts","../src/daemon/permission-ui.ts","../src/daemon/message-bridge.ts"],"sourcesContent":["import { join } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { loadConfig } from \"../shared/config.js\";\nimport { writePid, removePid } from \"../cli/pid.js\";\nimport { startDiscordBot } from \"./discord-bot.js\";\n\nconst CONFIG_DIR = join(homedir(), \".acp-discord\");\nconst CONFIG_PATH = join(CONFIG_DIR, \"config.toml\");\nconst PID_PATH = join(CONFIG_DIR, \"daemon.pid\");\n\nexport async function runDaemon(): Promise<void> {\n // Load config first — if it fails, no stale PID file is left behind (#12)\n const config = loadConfig(CONFIG_PATH);\n\n writePid(PID_PATH, process.pid);\n process.on(\"exit\", () => removePid(PID_PATH));\n\n console.log(`acp-discord daemon started (PID: ${process.pid})`);\n console.log(`Loaded config: ${Object.keys(config.channels).length} channel(s)`);\n\n await startDiscordBot(config);\n}\n\nif (process.env.ACP_DISCORD_DAEMON === \"1\") {\n runDaemon().catch((err) => {\n console.error(\"Daemon failed:\", err);\n process.exit(1);\n });\n}\n","import {\n Client,\n GatewayIntentBits,\n Events,\n REST,\n Routes,\n SlashCommandBuilder,\n ActionRowBuilder,\n ButtonBuilder,\n ButtonStyle,\n type Message,\n type TextChannel,\n} from \"discord.js\";\nimport type { AppConfig } from \"../shared/types.js\";\nimport { ChannelRouter } from \"./channel-router.js\";\nimport { SessionManager } from \"./session-manager.js\";\nimport { sendPermissionRequest } from \"./permission-ui.js\";\nimport { splitMessage, formatToolSummary, formatDiff, type ToolStatus } from \"./message-bridge.js\";\nimport type { AcpEventHandlers, DiffContent } from \"./acp-client.js\";\n\nexport async function startDiscordBot(config: AppConfig): Promise<void> {\n const router = new ChannelRouter(config);\n\n // Per-channel state for display\n const toolStates = new Map<string, Map<string, { title: string; status: ToolStatus }>>();\n const toolSummaryMessages = new Map<string, Message>();\n const replyBuffers = new Map<string, string>();\n const replyMessages = new Map<string, Message>();\n const flushTimers = new Map<string, NodeJS.Timeout>();\n // channelId -> toolCallId -> DiffContent[]\n const pendingDiffs = new Map<string, Map<string, DiffContent[]>>();\n\n let discordClient: Client;\n\n const handlers: AcpEventHandlers = {\n onToolCall(channelId, toolCallId, title, _kind, status, diffs) {\n if (!toolStates.has(channelId)) toolStates.set(channelId, new Map());\n toolStates.get(channelId)!.set(toolCallId, { title, status: status as ToolStatus });\n accumulateDiffs(channelId, toolCallId, diffs);\n updateToolSummaryMessage(channelId);\n if (status === \"completed\") sendDiffsForTool(channelId, toolCallId);\n },\n\n onToolCallUpdate(channelId, toolCallId, status, diffs) {\n const tools = toolStates.get(channelId);\n const tool = tools?.get(toolCallId);\n if (tool) {\n tool.status = status as ToolStatus;\n accumulateDiffs(channelId, toolCallId, diffs);\n updateToolSummaryMessage(channelId);\n if (status === \"completed\") sendDiffsForTool(channelId, toolCallId);\n }\n },\n\n onAgentMessageChunk(channelId, text) {\n const current = replyBuffers.get(channelId) ?? \"\";\n replyBuffers.set(channelId, current + text);\n scheduleFlushReply(channelId);\n },\n\n async onPermissionRequest(channelId, requestorId, toolCall, options) {\n const channel = await fetchChannel(channelId);\n if (!channel) return { outcome: \"cancelled\" as const };\n return sendPermissionRequest(channel, toolCall.title, toolCall.kind, options, requestorId);\n },\n\n onPromptComplete(channelId, _stopReason) {\n // Final flush\n flushReply(channelId, true);\n // Remove stop button from tool summary\n removeStopButton(channelId);\n // Clear state for next turn\n toolStates.delete(channelId);\n toolSummaryMessages.delete(channelId);\n replyBuffers.delete(channelId);\n replyMessages.delete(channelId);\n pendingDiffs.delete(channelId);\n },\n };\n\n const sessionManager = new SessionManager(handlers);\n\n // --- Display helpers ---\n\n function accumulateDiffs(channelId: string, toolCallId: string, diffs: DiffContent[]) {\n if (diffs.length === 0) return;\n if (!pendingDiffs.has(channelId)) pendingDiffs.set(channelId, new Map());\n const channelDiffs = pendingDiffs.get(channelId)!;\n const existing = channelDiffs.get(toolCallId) ?? [];\n channelDiffs.set(toolCallId, existing.concat(diffs));\n }\n\n async function sendDiffsForTool(channelId: string, toolCallId: string) {\n const channelDiffs = pendingDiffs.get(channelId);\n const diffs = channelDiffs?.get(toolCallId);\n if (!diffs || diffs.length === 0) return;\n\n const channel = await fetchChannel(channelId);\n if (!channel) return;\n\n const messages = formatDiff(diffs);\n for (const msg of messages) {\n await channel.send(msg);\n }\n\n channelDiffs!.delete(toolCallId);\n }\n\n async function fetchChannel(channelId: string): Promise<TextChannel | null> {\n const cached = discordClient.channels.cache.get(channelId) as TextChannel | undefined;\n if (cached) return cached;\n try {\n const fetched = await discordClient.channels.fetch(channelId);\n return fetched as TextChannel;\n } catch {\n return null;\n }\n }\n\n async function updateToolSummaryMessage(channelId: string) {\n const tools = toolStates.get(channelId);\n if (!tools) return;\n\n const content = formatToolSummary(tools);\n const channel = await fetchChannel(channelId);\n if (!channel) return;\n\n const stopButton = new ActionRowBuilder<ButtonBuilder>().addComponents(\n new ButtonBuilder()\n .setCustomId(`stop_${channelId}`)\n .setLabel(\"\\u23F9 Stop\")\n .setStyle(ButtonStyle.Secondary),\n );\n\n const existing = toolSummaryMessages.get(channelId);\n if (existing) {\n await existing.edit({ content, components: [stopButton] }).catch(() => {});\n } else {\n const msg = await channel.send({ content, components: [stopButton] });\n toolSummaryMessages.set(channelId, msg);\n }\n }\n\n async function removeStopButton(channelId: string) {\n const msg = toolSummaryMessages.get(channelId);\n if (msg) {\n const tools = toolStates.get(channelId);\n const content = tools ? formatToolSummary(tools) : msg.content;\n await msg.edit({ content, components: [] }).catch(() => {});\n }\n }\n\n function scheduleFlushReply(channelId: string) {\n if (flushTimers.has(channelId)) return;\n flushTimers.set(\n channelId,\n setTimeout(() => {\n flushTimers.delete(channelId);\n flushReply(channelId, false);\n }, 500),\n );\n }\n\n async function flushReply(channelId: string, final: boolean) {\n const timer = flushTimers.get(channelId);\n if (timer) {\n clearTimeout(timer);\n flushTimers.delete(channelId);\n }\n\n const buffer = replyBuffers.get(channelId);\n if (!buffer) return;\n\n const channel = await fetchChannel(channelId);\n if (!channel) return;\n\n if (final) {\n // Send final reply as new message(s), delete streaming message\n const existing = replyMessages.get(channelId);\n if (existing) await existing.delete().catch(() => {});\n replyMessages.delete(channelId);\n\n const chunks = splitMessage(buffer);\n for (const chunk of chunks) {\n await channel.send(chunk);\n }\n replyBuffers.delete(channelId);\n } else {\n // Streaming update: edit existing message\n const truncated = buffer.length > 2000 ? buffer.slice(buffer.length - 1900) + \"...\" : buffer;\n const existing = replyMessages.get(channelId);\n if (existing) {\n await existing.edit(truncated).catch(() => {});\n } else {\n const msg = await channel.send(truncated);\n replyMessages.set(channelId, msg);\n }\n }\n }\n\n // --- Discord client setup ---\n\n discordClient = new Client({\n intents: [\n GatewayIntentBits.Guilds,\n GatewayIntentBits.GuildMessages,\n GatewayIntentBits.MessageContent,\n ],\n });\n\n discordClient.on(Events.ClientReady, async (c) => {\n console.log(`Discord bot ready: ${c.user.tag}`);\n\n // Register slash commands\n const askCommand = new SlashCommandBuilder()\n .setName(\"ask\")\n .setDescription(\"Ask the coding agent a question\")\n .addStringOption((opt) =>\n opt.setName(\"message\").setDescription(\"Your message\").setRequired(true),\n );\n\n const clearCommand = new SlashCommandBuilder()\n .setName(\"clear\")\n .setDescription(\"Clear the agent session and start fresh\");\n\n const rest = new REST().setToken(config.discord.token);\n try {\n await rest.put(Routes.applicationCommands(c.application.id), {\n body: [askCommand.toJSON(), clearCommand.toJSON()],\n });\n console.log(\"Registered /ask and /clear commands\");\n } catch (err) {\n console.error(\"Failed to register commands:\", err);\n }\n });\n\n // Handle @mention messages in configured channels\n discordClient.on(Events.MessageCreate, async (message: Message) => {\n if (message.author.bot) return;\n\n const channelId = message.channelId;\n const resolved = router.resolve(channelId);\n if (!resolved) return;\n\n const isMention = message.mentions.has(discordClient.user!);\n if (!resolved.autoReply && !isMention) return;\n\n // Strip mention prefix if present\n const text = message.content.replace(/<@!?\\d+>/g, \"\").trim();\n\n if (!text) {\n await message.reply(\"Please provide a message.\");\n return;\n }\n\n if (sessionManager.isPrompting(channelId)) {\n await message.reply(\"\\u23F3 Agent is working. Your message has been queued.\");\n }\n\n try {\n await sessionManager.prompt(channelId, text, resolved.agent, message.author.id);\n } catch (err) {\n console.error(`Prompt failed for channel ${channelId}:`, err);\n await message.reply(\"An error occurred while processing your request.\").catch(() => {});\n }\n });\n\n // Handle stop button clicks\n discordClient.on(Events.InteractionCreate, async (interaction) => {\n if (!interaction.isButton()) return;\n\n if (interaction.customId.startsWith(\"stop_\")) {\n const channelId = interaction.customId.replace(\"stop_\", \"\");\n const activeRequestor = sessionManager.getActiveRequestorId(channelId);\n\n // Only the user who triggered the current prompt can stop it\n if (activeRequestor && interaction.user.id !== activeRequestor) {\n await interaction.reply({ content: \"Only the user who started this prompt can stop it.\", ephemeral: true });\n return;\n }\n\n sessionManager.cancel(channelId);\n await interaction.update({ components: [] });\n }\n });\n\n // Handle /ask command\n discordClient.on(Events.InteractionCreate, async (interaction) => {\n if (!interaction.isChatInputCommand()) return;\n if (interaction.commandName !== \"ask\") return;\n\n const channelId = interaction.channelId;\n const resolved = router.resolve(channelId);\n if (!resolved) {\n await interaction.reply({ content: \"This channel is not configured for ACP.\", ephemeral: true });\n return;\n }\n\n const text = interaction.options.getString(\"message\", true);\n await interaction.deferReply();\n\n if (sessionManager.isPrompting(channelId)) {\n await interaction.editReply(\"\\u23F3 Agent is working. Your message has been queued.\");\n } else {\n await interaction.editReply(`\\uD83D\\uDCAC Processing: ${text.slice(0, 100)}...`);\n }\n\n try {\n await sessionManager.prompt(channelId, text, resolved.agent, interaction.user.id);\n } catch (err) {\n console.error(`Prompt failed for channel ${channelId}:`, err);\n await interaction.followUp({ content: \"An error occurred while processing your request.\", ephemeral: true }).catch(() => {});\n }\n });\n\n // Handle /clear command\n discordClient.on(Events.InteractionCreate, async (interaction) => {\n if (!interaction.isChatInputCommand()) return;\n if (interaction.commandName !== \"clear\") return;\n\n const channelId = interaction.channelId;\n sessionManager.teardown(channelId);\n\n // Clean up display state\n toolStates.delete(channelId);\n toolSummaryMessages.delete(channelId);\n replyBuffers.delete(channelId);\n replyMessages.delete(channelId);\n const timer = flushTimers.get(channelId);\n if (timer) clearTimeout(timer);\n flushTimers.delete(channelId);\n\n await interaction.reply(\"Session cleared. Next message will start a fresh agent.\");\n });\n\n // Graceful shutdown\n process.on(\"SIGTERM\", () => {\n sessionManager.teardownAll();\n discordClient.destroy();\n });\n\n process.on(\"SIGINT\", () => {\n sessionManager.teardownAll();\n discordClient.destroy();\n });\n\n try {\n await discordClient.login(config.discord.token);\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n if (message.includes(\"TOKEN_INVALID\") || message.includes(\"An invalid token was provided\")) {\n console.error(\"Error: Invalid Discord bot token. Check your config.toml.\");\n } else if (message.includes(\"ConnectTimeout\") || message.includes(\"ETIMEDOUT\") || message.includes(\"ECONNREFUSED\")) {\n console.error(\"Error: Cannot connect to Discord API. Check your network or proxy settings.\");\n console.error(\"Hint: Set HTTPS_PROXY=http://127.0.0.1:7890 if you need a proxy.\");\n } else {\n console.error(\"Error: Failed to connect to Discord:\", message);\n }\n process.exit(1);\n }\n}\n","import type { AppConfig, ResolvedChannelConfig } from \"../shared/types.js\";\nimport { resolveChannelConfig } from \"../shared/config.js\";\n\nexport class ChannelRouter {\n private config: AppConfig;\n\n constructor(config: AppConfig) {\n this.config = config;\n }\n\n resolve(channelId: string): ResolvedChannelConfig | null {\n return resolveChannelConfig(this.config, channelId);\n }\n\n isConfigured(channelId: string): boolean {\n return this.resolve(channelId) !== null;\n }\n}\n","import { spawn, type ChildProcess } from \"node:child_process\";\nimport { Readable, Writable } from \"node:stream\";\nimport { ClientSideConnection, ndJsonStream, PROTOCOL_VERSION } from \"@agentclientprotocol/sdk\";\nimport type { AgentConfig } from \"../shared/types.js\";\nimport { createAcpClient, type AcpEventHandlers } from \"./acp-client.js\";\n\ninterface ManagedSession {\n channelId: string;\n process: ChildProcess;\n connection: ClientSideConnection;\n sessionId: string;\n lastActivity: number;\n idleTimer: NodeJS.Timeout;\n prompting: boolean;\n queue: Array<{ text: string; requestorId: string }>;\n /** Set only when executePrompt begins — stable for the duration of the prompt */\n activePromptRequestorId: string;\n}\n\nexport class SessionManager {\n private sessions = new Map<string, ManagedSession>();\n private handlers: AcpEventHandlers;\n\n constructor(handlers: AcpEventHandlers) {\n this.handlers = handlers;\n }\n\n async prompt(channelId: string, text: string, agentConfig: AgentConfig, requestorId: string): Promise<string> {\n const session = await this.getOrCreate(channelId, agentConfig, requestorId);\n session.lastActivity = Date.now();\n this.resetIdleTimer(session, agentConfig.idle_timeout);\n\n if (session.prompting) {\n session.queue.push({ text, requestorId });\n return \"queued\";\n }\n\n return this.executePrompt(session, text, requestorId, agentConfig);\n }\n\n private async executePrompt(session: ManagedSession, text: string, requestorId: string, agentConfig: AgentConfig): Promise<string> {\n session.prompting = true;\n session.activePromptRequestorId = requestorId;\n try {\n const result = await session.connection.prompt({\n sessionId: session.sessionId,\n prompt: [{ type: \"text\", text }],\n });\n this.handlers.onPromptComplete(session.channelId, result.stopReason);\n return result.stopReason;\n } finally {\n session.prompting = false;\n // Process queue — await and catch to prevent unhandled rejections (#3)\n const next = session.queue.shift();\n if (next) {\n this.executePrompt(session, next.text, next.requestorId, agentConfig).catch((err) => {\n console.error(`Queued prompt failed for channel ${session.channelId}:`, err);\n });\n }\n }\n }\n\n cancel(channelId: string): void {\n const session = this.sessions.get(channelId);\n if (session) {\n session.connection.cancel({ sessionId: session.sessionId });\n }\n }\n\n private async getOrCreate(channelId: string, agentConfig: AgentConfig, requestorId: string): Promise<ManagedSession> {\n const existing = this.sessions.get(channelId);\n if (existing) return existing;\n return this.createSession(channelId, agentConfig, requestorId);\n }\n\n private async createSession(channelId: string, config: AgentConfig, requestorId: string): Promise<ManagedSession> {\n const proc = spawn(config.command, config.args, {\n stdio: [\"pipe\", \"pipe\", \"inherit\"],\n cwd: config.cwd,\n });\n\n // Handle spawn errors (ENOENT, permission denied, etc.) (#4)\n proc.on(\"error\", (err) => {\n console.error(`Agent process error for channel ${channelId}:`, err);\n const session = this.sessions.get(channelId);\n if (session?.process === proc) {\n clearTimeout(session.idleTimer);\n this.sessions.delete(channelId);\n }\n });\n\n proc.on(\"exit\", () => {\n const session = this.sessions.get(channelId);\n if (session?.process === proc) {\n this.sessions.delete(channelId);\n clearTimeout(session.idleTimer);\n }\n });\n\n // Wrap initialize/newSession in try/catch to clean up process on failure (#5)\n let connection: ClientSideConnection;\n let sessionId: string;\n try {\n const stream = ndJsonStream(\n Writable.toWeb(proc.stdin!) as WritableStream<Uint8Array>,\n Readable.toWeb(proc.stdout!) as ReadableStream<Uint8Array>,\n );\n\n const client = createAcpClient(channelId, this.handlers, () => {\n return this.sessions.get(channelId)?.activePromptRequestorId ?? requestorId;\n });\n connection = new ClientSideConnection((_agent) => client, stream);\n\n await connection.initialize({\n protocolVersion: PROTOCOL_VERSION,\n clientCapabilities: {\n fs: { readTextFile: true, writeTextFile: true },\n terminal: true,\n },\n clientInfo: {\n name: \"acp-discord\",\n title: \"ACP Discord Bot\",\n version: \"0.1.0\",\n },\n });\n\n const result = await connection.newSession({\n cwd: config.cwd,\n mcpServers: [],\n });\n sessionId = result.sessionId;\n } catch (err) {\n proc.kill();\n throw err;\n }\n\n const managed: ManagedSession = {\n channelId,\n process: proc,\n connection,\n sessionId,\n lastActivity: Date.now(),\n idleTimer: this.startIdleTimer(channelId, config.idle_timeout),\n prompting: false,\n queue: [],\n activePromptRequestorId: requestorId,\n };\n\n this.sessions.set(channelId, managed);\n return managed;\n }\n\n private startIdleTimer(channelId: string, timeoutSec: number): NodeJS.Timeout {\n return setTimeout(() => this.teardown(channelId), timeoutSec * 1000);\n }\n\n private resetIdleTimer(session: ManagedSession, timeoutSec: number): void {\n clearTimeout(session.idleTimer);\n session.idleTimer = this.startIdleTimer(session.channelId, timeoutSec);\n }\n\n teardown(channelId: string): void {\n const session = this.sessions.get(channelId);\n if (!session) return;\n clearTimeout(session.idleTimer);\n session.process.kill();\n this.sessions.delete(channelId);\n }\n\n teardownAll(): void {\n for (const channelId of this.sessions.keys()) {\n this.teardown(channelId);\n }\n }\n\n isPrompting(channelId: string): boolean {\n return this.sessions.get(channelId)?.prompting ?? false;\n }\n\n getActiveRequestorId(channelId: string): string | null {\n const session = this.sessions.get(channelId);\n if (!session?.prompting) return null;\n return session.activePromptRequestorId;\n }\n\n getActiveChannels(): string[] {\n return Array.from(this.sessions.keys());\n }\n}\n","import type {\n Client,\n RequestPermissionRequest,\n RequestPermissionResponse,\n SessionNotification,\n} from \"@agentclientprotocol/sdk\";\n\nexport interface DiffContent {\n path: string;\n oldText?: string | null;\n newText: string;\n}\n\nexport interface AcpEventHandlers {\n onToolCall(channelId: string, toolCallId: string, title: string, kind: string, status: string, diffs: DiffContent[]): void;\n onToolCallUpdate(channelId: string, toolCallId: string, status: string, diffs: DiffContent[]): void;\n onAgentMessageChunk(channelId: string, text: string): void;\n onPermissionRequest(\n channelId: string,\n requestorId: string,\n toolCall: { toolCallId: string; title: string; kind: string },\n options: Array<{ optionId: string; name: string; kind: string }>,\n ): Promise<{ outcome: \"selected\"; optionId: string } | { outcome: \"cancelled\" }>;\n onPromptComplete(channelId: string, stopReason: string): void;\n}\n\nexport function createAcpClient(\n channelId: string,\n handlers: AcpEventHandlers,\n getRequestorId: () => string,\n): Client {\n return {\n async requestPermission(params: RequestPermissionRequest): Promise<RequestPermissionResponse> {\n const result = await handlers.onPermissionRequest(\n channelId,\n getRequestorId(),\n {\n toolCallId: params.toolCall.toolCallId,\n title: params.toolCall.title ?? \"Unknown\",\n kind: params.toolCall.kind ?? \"other\",\n },\n params.options.map((o: { optionId: string; name: string; kind: string }) => ({\n optionId: o.optionId,\n name: o.name,\n kind: o.kind,\n })),\n );\n\n if (result.outcome === \"selected\") {\n return { outcome: { outcome: \"selected\", optionId: result.optionId } };\n }\n return { outcome: { outcome: \"cancelled\" } };\n },\n\n async sessionUpdate(params: SessionNotification): Promise<void> {\n const update = params.update;\n switch (update.sessionUpdate) {\n case \"agent_message_chunk\": {\n if (update.content.type === \"text\") {\n handlers.onAgentMessageChunk(channelId, update.content.text);\n }\n break;\n }\n case \"tool_call\": {\n const toolCallDiffs = extractDiffs(update.content);\n handlers.onToolCall(\n channelId,\n update.toolCallId,\n update.title ?? \"Unknown\",\n update.kind ?? \"other\",\n update.status ?? \"pending\",\n toolCallDiffs,\n );\n break;\n }\n case \"tool_call_update\": {\n const updateDiffs = extractDiffs(update.content);\n handlers.onToolCallUpdate(\n channelId,\n update.toolCallId,\n update.status ?? \"in_progress\",\n updateDiffs,\n );\n break;\n }\n }\n },\n };\n}\n\nfunction extractDiffs(content: unknown): DiffContent[] {\n if (!Array.isArray(content)) return [];\n const diffs: DiffContent[] = [];\n for (const item of content) {\n if (item && typeof item === \"object\" && \"type\" in item && item.type === \"diff\") {\n diffs.push({\n path: (item as { path: string }).path,\n oldText: (item as { oldText?: string | null }).oldText ?? null,\n newText: (item as { newText: string }).newText,\n });\n }\n }\n return diffs;\n}\n","import {\n ActionRowBuilder,\n ButtonBuilder,\n ButtonStyle,\n EmbedBuilder,\n type TextChannel,\n} from \"discord.js\";\n\nconst KIND_LABELS: Record<string, string> = {\n allow_once: \"\\u2705 Allow\",\n allow_always: \"\\u2705 Always Allow\",\n reject_once: \"\\u274C Reject\",\n reject_always: \"\\u274C Never Allow\",\n};\n\nconst KIND_STYLES: Record<string, ButtonStyle> = {\n allow_once: ButtonStyle.Success,\n allow_always: ButtonStyle.Success,\n reject_once: ButtonStyle.Danger,\n reject_always: ButtonStyle.Danger,\n};\n\nexport interface PermissionOption {\n optionId: string;\n name: string;\n kind: string;\n}\n\nexport async function sendPermissionRequest(\n channel: TextChannel,\n toolTitle: string,\n toolKind: string,\n options: PermissionOption[],\n requestorId: string,\n timeoutMs = 14 * 60 * 1000,\n): Promise<{ outcome: \"selected\"; optionId: string } | { outcome: \"cancelled\" }> {\n if (options.length === 0) {\n return { outcome: \"cancelled\" };\n }\n\n const embed = new EmbedBuilder()\n .setColor(0xffa500)\n .setTitle(`Permission: ${toolTitle}`)\n .setDescription(`Tool type: \\`${toolKind}\\``)\n .setTimestamp();\n\n const buttons = options.map((opt) =>\n new ButtonBuilder()\n .setCustomId(`perm_${opt.optionId}`)\n .setLabel(KIND_LABELS[opt.kind] ?? opt.name)\n .setStyle(KIND_STYLES[opt.kind] ?? ButtonStyle.Secondary),\n );\n\n // Discord allows max 5 buttons per ActionRow\n const rows: ActionRowBuilder<ButtonBuilder>[] = [];\n for (let i = 0; i < buttons.length; i += 5) {\n rows.push(new ActionRowBuilder<ButtonBuilder>().addComponents(buttons.slice(i, i + 5)));\n }\n\n const msg = await channel.send({ embeds: [embed], components: rows });\n\n return new Promise((resolve) => {\n const collector = msg.createMessageComponentCollector({\n filter: (i) => i.user.id === requestorId,\n time: timeoutMs,\n });\n\n collector.on(\"collect\", async (interaction) => {\n const optionId = interaction.customId.replace(\"perm_\", \"\");\n await interaction.update({ components: [] });\n collector.stop(\"selected\");\n resolve({ outcome: \"selected\", optionId });\n });\n\n collector.on(\"end\", (_collected, reason) => {\n if (reason === \"time\") {\n msg.edit({ components: [] }).catch(() => {});\n resolve({ outcome: \"cancelled\" });\n }\n });\n });\n}\n","import { createTwoFilesPatch } from \"diff\";\nimport type { DiffContent } from \"./acp-client.js\";\n\nconst DISCORD_MAX_LENGTH = 2000;\nconst MAX_DIFF_LINES = 150;\n\nexport function splitMessage(text: string, maxLength = DISCORD_MAX_LENGTH): string[] {\n if (text.length <= maxLength) return [text];\n\n const chunks: string[] = [];\n let remaining = text;\n let inCodeBlock = false;\n let codeFence = \"\";\n\n while (remaining.length > 0) {\n if (remaining.length <= maxLength) {\n chunks.push(remaining);\n break;\n }\n\n // Find split point: prefer newline before maxLength\n let splitAt = maxLength;\n const lastNewline = remaining.lastIndexOf(\"\\n\", maxLength);\n if (lastNewline > maxLength * 0.5) {\n splitAt = lastNewline + 1;\n }\n\n let chunk = remaining.slice(0, splitAt);\n remaining = remaining.slice(splitAt);\n\n // Handle code blocks: count fences in this chunk\n const fenceMatches = chunk.match(/```\\w*/g) || [];\n for (const fence of fenceMatches) {\n if (!inCodeBlock) {\n inCodeBlock = true;\n codeFence = fence;\n } else {\n inCodeBlock = false;\n codeFence = \"\";\n }\n }\n\n // If we're inside a code block at the split, close and reopen\n if (inCodeBlock) {\n chunk += \"\\n```\";\n remaining = codeFence + \"\\n\" + remaining;\n inCodeBlock = false;\n codeFence = \"\";\n }\n\n chunks.push(chunk);\n }\n\n return chunks;\n}\n\nexport type ToolStatus = \"pending\" | \"in_progress\" | \"completed\" | \"failed\";\n\nconst STATUS_ICONS: Record<ToolStatus, string> = {\n pending: \"\\u23F3\", // ⏳\n in_progress: \"\\uD83D\\uDD04\", // 🔄\n completed: \"\\u2705\", // ✅\n failed: \"\\u274C\", // ❌\n};\n\nexport function formatToolSummary(\n tools: Map<string, { title: string; status: ToolStatus }>,\n): string {\n const lines: string[] = [];\n for (const [, tool] of tools) {\n lines.push(`${STATUS_ICONS[tool.status]} ${tool.title}`);\n }\n return lines.join(\"\\n\");\n}\n\nexport function formatDiff(diffs: DiffContent[], maxLines = MAX_DIFF_LINES): string[] {\n if (diffs.length === 0) return [];\n\n const parts: string[] = [];\n\n for (const d of diffs) {\n const fileName = d.path.split(\"/\").pop() ?? d.path;\n const oldText = d.oldText ?? \"\";\n const patch = createTwoFilesPatch(\n d.oldText == null ? \"/dev/null\" : d.path,\n d.path,\n oldText,\n d.newText,\n undefined,\n undefined,\n { context: 3 },\n );\n\n // Remove the first two header lines (Index: and ===) if present, keep ---/+++ and hunks\n const patchLines = patch.split(\"\\n\");\n // Find the first --- line to start from\n const startIdx = patchLines.findIndex((l) => l.startsWith(\"---\"));\n const diffLines = startIdx >= 0 ? patchLines.slice(startIdx) : patchLines;\n\n let truncated = false;\n let displayLines = diffLines;\n if (diffLines.length > maxLines) {\n displayLines = diffLines.slice(0, maxLines);\n truncated = true;\n }\n\n let block = `**${fileName}**\\n\\`\\`\\`diff\\n${displayLines.join(\"\\n\")}\\n\\`\\`\\``;\n if (truncated) {\n block += `\\n*... ${diffLines.length - maxLines} more lines*`;\n }\n\n parts.push(block);\n }\n\n // Join all diff blocks and split for Discord's message limit\n const fullMessage = parts.join(\"\\n\\n\");\n return splitMessage(fullMessage);\n}\n"],"mappings":"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","names":["ActionRowBuilder","ButtonBuilder","ButtonStyle","ActionRowBuilder","ButtonBuilder","ButtonStyle"]}
|
|
1
|
+
{"version":3,"sources":["../src/daemon/index.ts","../src/daemon/discord-bot.ts","../src/daemon/channel-router.ts","../src/daemon/session-manager.ts","../src/daemon/acp-client.ts","../src/daemon/permission-ui.ts","../src/daemon/message-bridge.ts"],"sourcesContent":["import { join } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { loadConfig } from \"../shared/config.js\";\nimport { writePid, removePid } from \"../cli/pid.js\";\nimport { startDiscordBot } from \"./discord-bot.js\";\n\nconst CONFIG_DIR = join(homedir(), \".acp-discord\");\nconst CONFIG_PATH = join(CONFIG_DIR, \"config.toml\");\nconst PID_PATH = join(CONFIG_DIR, \"daemon.pid\");\n\nexport async function runDaemon(): Promise<void> {\n // Load config first — if it fails, no stale PID file is left behind (#12)\n const config = loadConfig(CONFIG_PATH);\n\n writePid(PID_PATH, process.pid);\n process.on(\"exit\", () => removePid(PID_PATH));\n\n console.log(`acp-discord daemon started (PID: ${process.pid})`);\n console.log(`Loaded config: ${Object.keys(config.channels).length} channel(s)`);\n\n await startDiscordBot(config);\n}\n\nif (process.env.ACP_DISCORD_DAEMON === \"1\") {\n runDaemon().catch((err) => {\n console.error(\"Daemon failed:\", err);\n process.exit(1);\n });\n}\n","import {\n Client,\n GatewayIntentBits,\n Events,\n REST,\n Routes,\n SlashCommandBuilder,\n ActionRowBuilder,\n ButtonBuilder,\n ButtonStyle,\n type Message,\n type TextChannel,\n} from \"discord.js\";\nimport type { AppConfig } from \"../shared/types.js\";\nimport { ChannelRouter } from \"./channel-router.js\";\nimport { SessionManager } from \"./session-manager.js\";\nimport { sendPermissionRequest } from \"./permission-ui.js\";\nimport { splitMessage, formatToolSummary, formatDiff, type ToolStatus } from \"./message-bridge.js\";\nimport type { AcpEventHandlers, DiffContent } from \"./acp-client.js\";\n\nexport async function startDiscordBot(config: AppConfig): Promise<void> {\n const router = new ChannelRouter(config);\n\n // Per-channel state for display\n const toolStates = new Map<string, Map<string, { title: string; status: ToolStatus; rawInput?: Record<string, unknown> }>>();\n const toolSummaryMessages = new Map<string, Message>();\n const replyBuffers = new Map<string, string>();\n const replyMessages = new Map<string, Message>();\n const flushTimers = new Map<string, NodeJS.Timeout>();\n // channelId -> toolCallId -> DiffContent[]\n const pendingDiffs = new Map<string, Map<string, DiffContent[]>>();\n // channelId -> Set of toolCallIds whose diffs were already shown at permission-request time\n const permissionDiffShown = new Map<string, Set<string>>();\n\n let discordClient: Client;\n\n const handlers: AcpEventHandlers = {\n onToolCall(channelId, toolCallId, title, _kind, status, diffs, rawInput) {\n if (!toolStates.has(channelId)) toolStates.set(channelId, new Map());\n toolStates.get(channelId)!.set(toolCallId, { title, status: status as ToolStatus, rawInput });\n accumulateDiffs(channelId, toolCallId, diffs);\n updateToolSummaryMessage(channelId);\n if (status === \"completed\") sendDiffsForTool(channelId, toolCallId);\n },\n\n onToolCallUpdate(channelId, toolCallId, status, diffs, rawInput) {\n const tools = toolStates.get(channelId);\n const tool = tools?.get(toolCallId);\n if (tool) {\n tool.status = status as ToolStatus;\n if (rawInput && !tool.rawInput) tool.rawInput = rawInput;\n accumulateDiffs(channelId, toolCallId, diffs);\n updateToolSummaryMessage(channelId);\n if (status === \"completed\") sendDiffsForTool(channelId, toolCallId);\n }\n },\n\n onAgentMessageChunk(channelId, text) {\n const current = replyBuffers.get(channelId) ?? \"\";\n replyBuffers.set(channelId, current + text);\n scheduleFlushReply(channelId);\n },\n\n async onPermissionRequest(channelId, requestorId, toolCall, options, diffs) {\n const channel = await fetchChannel(channelId);\n if (!channel) return { outcome: \"cancelled\" as const };\n const result = await sendPermissionRequest(channel, toolCall.title, toolCall.kind, options, requestorId, diffs);\n if (result.diffsSent) {\n if (!permissionDiffShown.has(channelId)) permissionDiffShown.set(channelId, new Set());\n permissionDiffShown.get(channelId)!.add(toolCall.toolCallId);\n }\n return result;\n },\n\n onPromptComplete(channelId, _stopReason) {\n // Final flush\n flushReply(channelId, true);\n // Remove stop button from tool summary\n removeStopButton(channelId);\n // Clear state for next turn\n toolStates.delete(channelId);\n toolSummaryMessages.delete(channelId);\n replyBuffers.delete(channelId);\n replyMessages.delete(channelId);\n pendingDiffs.delete(channelId);\n permissionDiffShown.delete(channelId);\n },\n };\n\n const sessionManager = new SessionManager(handlers);\n\n // --- Display helpers ---\n\n function accumulateDiffs(channelId: string, toolCallId: string, diffs: DiffContent[]) {\n if (diffs.length === 0) return;\n if (!pendingDiffs.has(channelId)) pendingDiffs.set(channelId, new Map());\n const channelDiffs = pendingDiffs.get(channelId)!;\n const existing = channelDiffs.get(toolCallId) ?? [];\n channelDiffs.set(toolCallId, existing.concat(diffs));\n }\n\n async function sendDiffsForTool(channelId: string, toolCallId: string) {\n // Skip if diffs were already shown at permission-request time\n const shownSet = permissionDiffShown.get(channelId);\n if (shownSet?.has(toolCallId)) {\n shownSet.delete(toolCallId);\n pendingDiffs.get(channelId)?.delete(toolCallId);\n return;\n }\n\n const channelDiffs = pendingDiffs.get(channelId);\n const diffs = channelDiffs?.get(toolCallId);\n if (!diffs || diffs.length === 0) return;\n\n const channel = await fetchChannel(channelId);\n if (!channel) return;\n\n const messages = formatDiff(diffs);\n for (const msg of messages) {\n await channel.send({ content: msg, allowedMentions: { parse: [] as const } });\n }\n\n channelDiffs!.delete(toolCallId);\n }\n\n async function fetchChannel(channelId: string): Promise<TextChannel | null> {\n const cached = discordClient.channels.cache.get(channelId) as TextChannel | undefined;\n if (cached) return cached;\n try {\n const fetched = await discordClient.channels.fetch(channelId);\n return fetched as TextChannel;\n } catch {\n return null;\n }\n }\n\n async function updateToolSummaryMessage(channelId: string) {\n const tools = toolStates.get(channelId);\n if (!tools) return;\n\n const content = formatToolSummary(tools);\n const channel = await fetchChannel(channelId);\n if (!channel) return;\n\n const stopButton = new ActionRowBuilder<ButtonBuilder>().addComponents(\n new ButtonBuilder()\n .setCustomId(`stop_${channelId}`)\n .setLabel(\"\\u23F9 Stop\")\n .setStyle(ButtonStyle.Secondary),\n );\n\n const noMentions = { parse: [] as const };\n const existing = toolSummaryMessages.get(channelId);\n if (existing) {\n await existing.edit({ content, components: [stopButton], allowedMentions: noMentions }).catch(() => {});\n } else {\n const msg = await channel.send({ content, components: [stopButton], allowedMentions: noMentions });\n toolSummaryMessages.set(channelId, msg);\n }\n }\n\n async function removeStopButton(channelId: string) {\n const msg = toolSummaryMessages.get(channelId);\n if (msg) {\n const tools = toolStates.get(channelId);\n const content = tools ? formatToolSummary(tools) : msg.content;\n await msg.edit({ content, components: [], allowedMentions: { parse: [] as const } }).catch(() => {});\n }\n }\n\n function scheduleFlushReply(channelId: string) {\n if (flushTimers.has(channelId)) return;\n flushTimers.set(\n channelId,\n setTimeout(() => {\n flushTimers.delete(channelId);\n flushReply(channelId, false);\n }, 500),\n );\n }\n\n async function flushReply(channelId: string, final: boolean) {\n const timer = flushTimers.get(channelId);\n if (timer) {\n clearTimeout(timer);\n flushTimers.delete(channelId);\n }\n\n const buffer = replyBuffers.get(channelId);\n if (!buffer) return;\n\n const channel = await fetchChannel(channelId);\n if (!channel) return;\n\n if (final) {\n // Send final reply as new message(s), delete streaming message\n const existing = replyMessages.get(channelId);\n if (existing) await existing.delete().catch(() => {});\n replyMessages.delete(channelId);\n\n const chunks = splitMessage(buffer);\n for (const chunk of chunks) {\n await channel.send(chunk);\n }\n replyBuffers.delete(channelId);\n } else {\n // Streaming update: edit existing message\n const truncated = buffer.length > 2000 ? buffer.slice(buffer.length - 1900) + \"...\" : buffer;\n const existing = replyMessages.get(channelId);\n if (existing) {\n await existing.edit(truncated).catch(() => {});\n } else {\n const msg = await channel.send(truncated);\n replyMessages.set(channelId, msg);\n }\n }\n }\n\n // --- Discord client setup ---\n\n discordClient = new Client({\n intents: [\n GatewayIntentBits.Guilds,\n GatewayIntentBits.GuildMessages,\n GatewayIntentBits.MessageContent,\n ],\n });\n\n discordClient.on(Events.ClientReady, async (c) => {\n console.log(`Discord bot ready: ${c.user.tag}`);\n\n // Register slash commands\n const askCommand = new SlashCommandBuilder()\n .setName(\"ask\")\n .setDescription(\"Ask the coding agent a question\")\n .addStringOption((opt) =>\n opt.setName(\"message\").setDescription(\"Your message\").setRequired(true),\n );\n\n const clearCommand = new SlashCommandBuilder()\n .setName(\"clear\")\n .setDescription(\"Clear the agent session and start fresh\");\n\n const rest = new REST().setToken(config.discord.token);\n try {\n await rest.put(Routes.applicationCommands(c.application.id), {\n body: [askCommand.toJSON(), clearCommand.toJSON()],\n });\n console.log(\"Registered /ask and /clear commands\");\n } catch (err) {\n console.error(\"Failed to register commands:\", err);\n }\n });\n\n // Handle @mention messages in configured channels\n discordClient.on(Events.MessageCreate, async (message: Message) => {\n if (message.author.bot) return;\n\n const channelId = message.channelId;\n const resolved = router.resolve(channelId);\n if (!resolved) return;\n\n const isMention = message.mentions.has(discordClient.user!);\n if (!resolved.autoReply && !isMention) return;\n\n // Strip mention prefix if present\n const text = message.content.replace(/<@!?\\d+>/g, \"\").trim();\n\n if (!text) {\n await message.reply(\"Please provide a message.\");\n return;\n }\n\n if (sessionManager.isPrompting(channelId)) {\n await message.reply(\"\\u23F3 Agent is working. Your message has been queued.\");\n }\n\n try {\n await sessionManager.prompt(channelId, text, resolved.agent, message.author.id);\n } catch (err) {\n console.error(`Prompt failed for channel ${channelId}:`, err);\n await message.reply(\"An error occurred while processing your request.\").catch(() => {});\n }\n });\n\n // Handle stop button clicks\n discordClient.on(Events.InteractionCreate, async (interaction) => {\n if (!interaction.isButton()) return;\n\n if (interaction.customId.startsWith(\"stop_\")) {\n const channelId = interaction.customId.replace(\"stop_\", \"\");\n const activeRequestor = sessionManager.getActiveRequestorId(channelId);\n\n // Only the user who triggered the current prompt can stop it\n if (activeRequestor && interaction.user.id !== activeRequestor) {\n await interaction.reply({ content: \"Only the user who started this prompt can stop it.\", ephemeral: true });\n return;\n }\n\n sessionManager.cancel(channelId);\n await interaction.update({ components: [] });\n }\n });\n\n // Handle /ask command\n discordClient.on(Events.InteractionCreate, async (interaction) => {\n if (!interaction.isChatInputCommand()) return;\n if (interaction.commandName !== \"ask\") return;\n\n const channelId = interaction.channelId;\n const resolved = router.resolve(channelId);\n if (!resolved) {\n await interaction.reply({ content: \"This channel is not configured for ACP.\", ephemeral: true });\n return;\n }\n\n const text = interaction.options.getString(\"message\", true);\n await interaction.deferReply();\n\n if (sessionManager.isPrompting(channelId)) {\n await interaction.editReply(\"\\u23F3 Agent is working. Your message has been queued.\");\n } else {\n await interaction.editReply(`\\uD83D\\uDCAC Processing: ${text.slice(0, 100)}...`);\n }\n\n try {\n await sessionManager.prompt(channelId, text, resolved.agent, interaction.user.id);\n } catch (err) {\n console.error(`Prompt failed for channel ${channelId}:`, err);\n await interaction.followUp({ content: \"An error occurred while processing your request.\", ephemeral: true }).catch(() => {});\n }\n });\n\n // Handle /clear command\n discordClient.on(Events.InteractionCreate, async (interaction) => {\n if (!interaction.isChatInputCommand()) return;\n if (interaction.commandName !== \"clear\") return;\n\n const channelId = interaction.channelId;\n sessionManager.teardown(channelId);\n\n // Clean up display state\n toolStates.delete(channelId);\n toolSummaryMessages.delete(channelId);\n replyBuffers.delete(channelId);\n replyMessages.delete(channelId);\n pendingDiffs.delete(channelId);\n permissionDiffShown.delete(channelId);\n const timer = flushTimers.get(channelId);\n if (timer) clearTimeout(timer);\n flushTimers.delete(channelId);\n\n await interaction.reply(\"Session cleared. Next message will start a fresh agent.\");\n });\n\n // Graceful shutdown\n process.on(\"SIGTERM\", () => {\n sessionManager.teardownAll();\n discordClient.destroy();\n });\n\n process.on(\"SIGINT\", () => {\n sessionManager.teardownAll();\n discordClient.destroy();\n });\n\n try {\n await discordClient.login(config.discord.token);\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n if (message.includes(\"TOKEN_INVALID\") || message.includes(\"An invalid token was provided\")) {\n console.error(\"Error: Invalid Discord bot token. Check your config.toml.\");\n } else if (message.includes(\"ConnectTimeout\") || message.includes(\"ETIMEDOUT\") || message.includes(\"ECONNREFUSED\")) {\n console.error(\"Error: Cannot connect to Discord API. Check your network or proxy settings.\");\n console.error(\"Hint: Set HTTPS_PROXY=http://127.0.0.1:7890 if you need a proxy.\");\n } else {\n console.error(\"Error: Failed to connect to Discord:\", message);\n }\n process.exit(1);\n }\n}\n","import type { AppConfig, ResolvedChannelConfig } from \"../shared/types.js\";\nimport { resolveChannelConfig } from \"../shared/config.js\";\n\nexport class ChannelRouter {\n private config: AppConfig;\n\n constructor(config: AppConfig) {\n this.config = config;\n }\n\n resolve(channelId: string): ResolvedChannelConfig | null {\n return resolveChannelConfig(this.config, channelId);\n }\n\n isConfigured(channelId: string): boolean {\n return this.resolve(channelId) !== null;\n }\n}\n","import { spawn, type ChildProcess } from \"node:child_process\";\nimport { Readable, Writable } from \"node:stream\";\nimport { ClientSideConnection, ndJsonStream, PROTOCOL_VERSION } from \"@agentclientprotocol/sdk\";\nimport type { AgentConfig } from \"../shared/types.js\";\nimport { createAcpClient, type AcpEventHandlers } from \"./acp-client.js\";\n\ninterface ManagedSession {\n channelId: string;\n process: ChildProcess;\n connection: ClientSideConnection;\n sessionId: string;\n lastActivity: number;\n idleTimer: NodeJS.Timeout;\n prompting: boolean;\n queue: Array<{ text: string; requestorId: string }>;\n /** Set only when executePrompt begins — stable for the duration of the prompt */\n activePromptRequestorId: string;\n}\n\nexport class SessionManager {\n private sessions = new Map<string, ManagedSession>();\n private handlers: AcpEventHandlers;\n\n constructor(handlers: AcpEventHandlers) {\n this.handlers = handlers;\n }\n\n async prompt(channelId: string, text: string, agentConfig: AgentConfig, requestorId: string): Promise<string> {\n const session = await this.getOrCreate(channelId, agentConfig, requestorId);\n session.lastActivity = Date.now();\n this.resetIdleTimer(session, agentConfig.idle_timeout);\n\n if (session.prompting) {\n session.queue.push({ text, requestorId });\n return \"queued\";\n }\n\n return this.executePrompt(session, text, requestorId, agentConfig);\n }\n\n private async executePrompt(session: ManagedSession, text: string, requestorId: string, agentConfig: AgentConfig): Promise<string> {\n session.prompting = true;\n session.activePromptRequestorId = requestorId;\n try {\n const result = await session.connection.prompt({\n sessionId: session.sessionId,\n prompt: [{ type: \"text\", text }],\n });\n this.handlers.onPromptComplete(session.channelId, result.stopReason);\n return result.stopReason;\n } finally {\n session.prompting = false;\n // Process queue — await and catch to prevent unhandled rejections (#3)\n const next = session.queue.shift();\n if (next) {\n this.executePrompt(session, next.text, next.requestorId, agentConfig).catch((err) => {\n console.error(`Queued prompt failed for channel ${session.channelId}:`, err);\n });\n }\n }\n }\n\n cancel(channelId: string): void {\n const session = this.sessions.get(channelId);\n if (session) {\n session.connection.cancel({ sessionId: session.sessionId });\n }\n }\n\n private async getOrCreate(channelId: string, agentConfig: AgentConfig, requestorId: string): Promise<ManagedSession> {\n const existing = this.sessions.get(channelId);\n if (existing) return existing;\n return this.createSession(channelId, agentConfig, requestorId);\n }\n\n private async createSession(channelId: string, config: AgentConfig, requestorId: string): Promise<ManagedSession> {\n const proc = spawn(config.command, config.args, {\n stdio: [\"pipe\", \"pipe\", \"inherit\"],\n cwd: config.cwd,\n });\n\n // Handle spawn errors (ENOENT, permission denied, etc.) (#4)\n proc.on(\"error\", (err) => {\n console.error(`Agent process error for channel ${channelId}:`, err);\n const session = this.sessions.get(channelId);\n if (session?.process === proc) {\n clearTimeout(session.idleTimer);\n this.sessions.delete(channelId);\n }\n });\n\n proc.on(\"exit\", () => {\n const session = this.sessions.get(channelId);\n if (session?.process === proc) {\n this.sessions.delete(channelId);\n clearTimeout(session.idleTimer);\n }\n });\n\n // Wrap initialize/newSession in try/catch to clean up process on failure (#5)\n let connection: ClientSideConnection;\n let sessionId: string;\n try {\n const stream = ndJsonStream(\n Writable.toWeb(proc.stdin!) as WritableStream<Uint8Array>,\n Readable.toWeb(proc.stdout!) as ReadableStream<Uint8Array>,\n );\n\n const client = createAcpClient(channelId, this.handlers, () => {\n return this.sessions.get(channelId)?.activePromptRequestorId ?? requestorId;\n });\n connection = new ClientSideConnection((_agent) => client, stream);\n\n await connection.initialize({\n protocolVersion: PROTOCOL_VERSION,\n clientCapabilities: {\n fs: { readTextFile: true, writeTextFile: true },\n terminal: true,\n },\n clientInfo: {\n name: \"acp-discord\",\n title: \"ACP Discord Bot\",\n version: \"0.1.0\",\n },\n });\n\n const result = await connection.newSession({\n cwd: config.cwd,\n mcpServers: [],\n });\n sessionId = result.sessionId;\n } catch (err) {\n proc.kill();\n throw err;\n }\n\n const managed: ManagedSession = {\n channelId,\n process: proc,\n connection,\n sessionId,\n lastActivity: Date.now(),\n idleTimer: this.startIdleTimer(channelId, config.idle_timeout),\n prompting: false,\n queue: [],\n activePromptRequestorId: requestorId,\n };\n\n this.sessions.set(channelId, managed);\n return managed;\n }\n\n private startIdleTimer(channelId: string, timeoutSec: number): NodeJS.Timeout {\n return setTimeout(() => this.teardown(channelId), timeoutSec * 1000);\n }\n\n private resetIdleTimer(session: ManagedSession, timeoutSec: number): void {\n clearTimeout(session.idleTimer);\n session.idleTimer = this.startIdleTimer(session.channelId, timeoutSec);\n }\n\n teardown(channelId: string): void {\n const session = this.sessions.get(channelId);\n if (!session) return;\n clearTimeout(session.idleTimer);\n session.process.kill();\n this.sessions.delete(channelId);\n }\n\n teardownAll(): void {\n for (const channelId of this.sessions.keys()) {\n this.teardown(channelId);\n }\n }\n\n isPrompting(channelId: string): boolean {\n return this.sessions.get(channelId)?.prompting ?? false;\n }\n\n getActiveRequestorId(channelId: string): string | null {\n const session = this.sessions.get(channelId);\n if (!session?.prompting) return null;\n return session.activePromptRequestorId;\n }\n\n getActiveChannels(): string[] {\n return Array.from(this.sessions.keys());\n }\n}\n","import type {\n Client,\n RequestPermissionRequest,\n RequestPermissionResponse,\n SessionNotification,\n} from \"@agentclientprotocol/sdk\";\n\nexport interface DiffContent {\n path: string;\n oldText?: string | null;\n newText: string;\n}\n\nexport interface AcpEventHandlers {\n onToolCall(channelId: string, toolCallId: string, title: string, kind: string, status: string, diffs: DiffContent[], rawInput?: Record<string, unknown>): void;\n onToolCallUpdate(channelId: string, toolCallId: string, status: string, diffs: DiffContent[], rawInput?: Record<string, unknown>): void;\n onAgentMessageChunk(channelId: string, text: string): void;\n onPermissionRequest(\n channelId: string,\n requestorId: string,\n toolCall: { toolCallId: string; title: string; kind: string },\n options: Array<{ optionId: string; name: string; kind: string }>,\n diffs: DiffContent[],\n ): Promise<{ outcome: \"selected\"; optionId: string } | { outcome: \"cancelled\" }>;\n onPromptComplete(channelId: string, stopReason: string): void;\n}\n\nexport function createAcpClient(\n channelId: string,\n handlers: AcpEventHandlers,\n getRequestorId: () => string,\n): Client {\n return {\n async requestPermission(params: RequestPermissionRequest): Promise<RequestPermissionResponse> {\n const diffs = extractDiffs((params.toolCall as { content?: unknown }).content);\n const result = await handlers.onPermissionRequest(\n channelId,\n getRequestorId(),\n {\n toolCallId: params.toolCall.toolCallId,\n title: params.toolCall.title ?? \"Unknown\",\n kind: params.toolCall.kind ?? \"other\",\n },\n params.options.map((o: { optionId: string; name: string; kind: string }) => ({\n optionId: o.optionId,\n name: o.name,\n kind: o.kind,\n })),\n diffs,\n );\n\n if (result.outcome === \"selected\") {\n return { outcome: { outcome: \"selected\", optionId: result.optionId } };\n }\n return { outcome: { outcome: \"cancelled\" } };\n },\n\n async sessionUpdate(params: SessionNotification): Promise<void> {\n const update = params.update;\n switch (update.sessionUpdate) {\n case \"agent_message_chunk\": {\n if (update.content.type === \"text\") {\n handlers.onAgentMessageChunk(channelId, update.content.text);\n }\n break;\n }\n case \"tool_call\": {\n const toolCallDiffs = extractDiffs(update.content);\n const rawVal = (update as Record<string, unknown>).rawInput;\n const rawInput = typeof rawVal === \"object\" && rawVal !== null && !Array.isArray(rawVal)\n ? (rawVal as Record<string, unknown>)\n : undefined;\n handlers.onToolCall(\n channelId,\n update.toolCallId,\n update.title ?? \"Unknown\",\n update.kind ?? \"other\",\n update.status ?? \"pending\",\n toolCallDiffs,\n rawInput,\n );\n break;\n }\n case \"tool_call_update\": {\n const updateDiffs = extractDiffs(update.content);\n const updateRawVal = (update as Record<string, unknown>).rawInput;\n const updateRawInput = typeof updateRawVal === \"object\" && updateRawVal !== null && !Array.isArray(updateRawVal)\n ? (updateRawVal as Record<string, unknown>)\n : undefined;\n handlers.onToolCallUpdate(\n channelId,\n update.toolCallId,\n update.status ?? \"in_progress\",\n updateDiffs,\n updateRawInput,\n );\n break;\n }\n }\n },\n };\n}\n\nfunction extractDiffs(content: unknown): DiffContent[] {\n if (!Array.isArray(content)) return [];\n const diffs: DiffContent[] = [];\n for (const item of content) {\n if (item && typeof item === \"object\" && \"type\" in item && item.type === \"diff\") {\n const { path, oldText, newText } = item as Record<string, unknown>;\n if (typeof path !== \"string\" || typeof newText !== \"string\") continue;\n if (oldText !== undefined && oldText !== null && typeof oldText !== \"string\") continue;\n diffs.push({ path, oldText: (oldText as string | null) ?? null, newText });\n }\n }\n return diffs;\n}\n","import {\n ActionRowBuilder,\n ButtonBuilder,\n ButtonStyle,\n EmbedBuilder,\n type TextChannel,\n} from \"discord.js\";\nimport type { DiffContent } from \"./acp-client.js\";\nimport { formatDiff } from \"./message-bridge.js\";\n\nconst KIND_LABELS: Record<string, string> = {\n allow_once: \"\\u2705 Allow\",\n allow_always: \"\\u2705 Always Allow\",\n reject_once: \"\\u274C Reject\",\n reject_always: \"\\u274C Never Allow\",\n};\n\nconst KIND_STYLES: Record<string, ButtonStyle> = {\n allow_once: ButtonStyle.Success,\n allow_always: ButtonStyle.Success,\n reject_once: ButtonStyle.Danger,\n reject_always: ButtonStyle.Danger,\n};\n\nexport interface PermissionOption {\n optionId: string;\n name: string;\n kind: string;\n}\n\nexport async function sendPermissionRequest(\n channel: TextChannel,\n toolTitle: string,\n toolKind: string,\n options: PermissionOption[],\n requestorId: string,\n diffs: DiffContent[] = [],\n timeoutMs = 14 * 60 * 1000,\n): Promise<{ outcome: \"selected\"; optionId: string; diffsSent?: boolean } | { outcome: \"cancelled\"; diffsSent?: boolean }> {\n if (options.length === 0) {\n return { outcome: \"cancelled\" };\n }\n\n // Send diffs before the permission embed so the user can review changes\n let diffsSent = false;\n if (diffs.length > 0) {\n try {\n const diffMessages = formatDiff(diffs);\n for (const msg of diffMessages) {\n await channel.send(msg);\n }\n diffsSent = true;\n } catch (err) {\n console.error(\"Failed to send permission diffs:\", err);\n }\n }\n\n const embed = new EmbedBuilder()\n .setColor(0xffa500)\n .setTitle(`Permission: ${toolTitle}`)\n .setDescription(`Tool type: \\`${toolKind}\\``)\n .setTimestamp();\n\n const buttons = options.map((opt) =>\n new ButtonBuilder()\n .setCustomId(`perm_${opt.optionId}`)\n .setLabel(KIND_LABELS[opt.kind] ?? opt.name)\n .setStyle(KIND_STYLES[opt.kind] ?? ButtonStyle.Secondary),\n );\n\n // Discord allows max 5 buttons per ActionRow\n const rows: ActionRowBuilder<ButtonBuilder>[] = [];\n for (let i = 0; i < buttons.length; i += 5) {\n rows.push(new ActionRowBuilder<ButtonBuilder>().addComponents(buttons.slice(i, i + 5)));\n }\n\n const msg = await channel.send({ embeds: [embed], components: rows });\n\n return new Promise((resolve) => {\n const collector = msg.createMessageComponentCollector({\n filter: (i) => i.user.id === requestorId,\n time: timeoutMs,\n });\n\n collector.on(\"collect\", async (interaction) => {\n const optionId = interaction.customId.replace(\"perm_\", \"\");\n await interaction.update({ components: [] });\n collector.stop(\"selected\");\n resolve({ outcome: \"selected\", optionId, diffsSent });\n });\n\n collector.on(\"end\", (_collected, reason) => {\n if (reason === \"time\") {\n msg.edit({ components: [] }).catch(() => {});\n resolve({ outcome: \"cancelled\", diffsSent });\n }\n });\n });\n}\n","import { createTwoFilesPatch } from \"diff\";\nimport type { DiffContent } from \"./acp-client.js\";\n\nconst DISCORD_MAX_LENGTH = 2000;\nconst MAX_DIFF_LINES = 150;\n\nexport function splitMessage(text: string, maxLength = DISCORD_MAX_LENGTH): string[] {\n if (text.length <= maxLength) return [text];\n\n const chunks: string[] = [];\n let remaining = text;\n let inCodeBlock = false;\n let codeFence = \"\";\n\n while (remaining.length > 0) {\n if (remaining.length <= maxLength) {\n chunks.push(remaining);\n break;\n }\n\n // Find split point: prefer newline before maxLength\n let splitAt = maxLength;\n const lastNewline = remaining.lastIndexOf(\"\\n\", maxLength);\n if (lastNewline > maxLength * 0.5) {\n splitAt = lastNewline + 1;\n }\n\n let chunk = remaining.slice(0, splitAt);\n remaining = remaining.slice(splitAt);\n\n // Handle code blocks: count fences in this chunk\n const fenceMatches = chunk.match(/```\\w*/g) || [];\n for (const fence of fenceMatches) {\n if (!inCodeBlock) {\n inCodeBlock = true;\n codeFence = fence;\n } else {\n inCodeBlock = false;\n codeFence = \"\";\n }\n }\n\n // If we're inside a code block at the split, close and reopen\n if (inCodeBlock) {\n chunk += \"\\n```\";\n remaining = codeFence + \"\\n\" + remaining;\n inCodeBlock = false;\n codeFence = \"\";\n }\n\n chunks.push(chunk);\n }\n\n return chunks;\n}\n\nexport type ToolStatus = \"pending\" | \"in_progress\" | \"completed\" | \"failed\";\n\nconst STATUS_ICONS: Record<ToolStatus, string> = {\n pending: \"\\u23F3\", // ⏳\n in_progress: \"\\uD83D\\uDD04\", // 🔄\n completed: \"\\u2705\", // ✅\n failed: \"\\u274C\", // ❌\n};\n\nexport function formatToolSummary(\n tools: Map<string, { title: string; status: ToolStatus; rawInput?: Record<string, unknown> }>,\n): string {\n const lines: string[] = [];\n for (const [, tool] of tools) {\n const detail = extractToolDetail(tool.rawInput);\n const suffix = detail ? ` · \\`${detail}\\`` : \"\";\n lines.push(`${STATUS_ICONS[tool.status]} ${tool.title}${suffix}`);\n }\n return lines.join(\"\\n\");\n}\n\nconst MAX_DETAIL_LENGTH = 80;\n\n// Only display values from known-safe fields to avoid leaking secrets\nconst SAFE_FIELDS = [\"command\", \"file_path\", \"pattern\", \"query\", \"path\", \"url\", \"description\"];\n\nfunction extractToolDetail(rawInput?: Record<string, unknown>): string | null {\n if (!rawInput) return null;\n\n for (const field of SAFE_FIELDS) {\n if (typeof rawInput[field] === \"string\" && rawInput[field]) {\n return truncate(sanitizeDetail(rawInput[field] as string), MAX_DETAIL_LENGTH);\n }\n }\n\n return null;\n}\n\nfunction sanitizeDetail(text: string): string {\n return text.replace(/`/g, \"'\");\n}\n\nfunction truncate(text: string, max: number): string {\n // Use first line only for multiline values\n const firstLine = text.split(\"\\n\")[0];\n if (firstLine.length <= max) return firstLine;\n return firstLine.slice(0, max - 1) + \"\\u2026\";\n}\n\nexport function formatDiff(diffs: DiffContent[], maxLines = MAX_DIFF_LINES): string[] {\n if (diffs.length === 0) return [];\n\n const parts: string[] = [];\n\n for (const d of diffs) {\n const fileName = d.path.split(\"/\").pop() ?? d.path;\n const oldText = d.oldText ?? \"\";\n const patch = createTwoFilesPatch(\n d.oldText == null ? \"/dev/null\" : d.path,\n d.path,\n oldText,\n d.newText,\n undefined,\n undefined,\n { context: 3 },\n );\n\n // Remove the first two header lines (Index: and ===) if present, keep ---/+++ and hunks\n const patchLines = patch.split(\"\\n\");\n // Find the first --- line to start from\n const startIdx = patchLines.findIndex((l) => l.startsWith(\"---\"));\n const diffLines = startIdx >= 0 ? patchLines.slice(startIdx) : patchLines;\n\n let truncated = false;\n let displayLines = diffLines;\n if (diffLines.length > maxLines) {\n displayLines = diffLines.slice(0, maxLines);\n truncated = true;\n }\n\n let block = `**${fileName}**\\n\\`\\`\\`diff\\n${displayLines.join(\"\\n\")}\\n\\`\\`\\``;\n if (truncated) {\n block += `\\n*... ${diffLines.length - maxLines} more lines*`;\n }\n\n parts.push(block);\n }\n\n // Join all diff blocks and split for Discord's message limit\n const fullMessage = parts.join(\"\\n\\n\");\n return splitMessage(fullMessage);\n}\n"],"mappings":"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","names":["ActionRowBuilder","ButtonBuilder","ButtonStyle","msg","ActionRowBuilder","ButtonBuilder","ButtonStyle"]}
|