@openacp/cli 0.5.3 → 0.6.1
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 +51 -16
- package/dist/action-detect-6M5GCGAU.js +15 -0
- package/dist/admin-IKPS5PFC.js +16 -0
- package/dist/agents-55NX3DHM.js +14 -0
- package/dist/{api-client-UN7BXQOQ.js → api-client-BH2JFHQW.js} +4 -2
- package/dist/{autostart-K73RQZVV.js → autostart-A7JRU4WJ.js} +6 -2
- package/dist/{chunk-ECBD5I5R.js → chunk-2KJC3ILH.js} +123 -16
- package/dist/chunk-2KJC3ILH.js.map +1 -0
- package/dist/{chunk-2Z2XPUD5.js → chunk-4LFDEW22.js} +53 -5
- package/dist/chunk-4LFDEW22.js.map +1 -0
- package/dist/{chunk-Z46LGZ7R.js → chunk-4TR5Y3MP.js} +18 -1
- package/dist/chunk-4TR5Y3MP.js.map +1 -0
- package/dist/chunk-7G5QKLLF.js +105 -0
- package/dist/chunk-7G5QKLLF.js.map +1 -0
- package/dist/{chunk-IURZ4QHG.js → chunk-7QJS2XBD.js} +2 -1
- package/dist/chunk-7QJS2XBD.js.map +1 -0
- package/dist/chunk-AKIU4JBF.js +145 -0
- package/dist/chunk-AKIU4JBF.js.map +1 -0
- package/dist/{chunk-KSIQZC3J.js → chunk-EVFJW45N.js} +1 -1
- package/dist/chunk-EVFJW45N.js.map +1 -0
- package/dist/chunk-GINCOFNW.js +134 -0
- package/dist/chunk-GINCOFNW.js.map +1 -0
- package/dist/chunk-H7ZMPBZC.js +203 -0
- package/dist/chunk-H7ZMPBZC.js.map +1 -0
- package/dist/chunk-I7WC6E5S.js +71 -0
- package/dist/chunk-I7WC6E5S.js.map +1 -0
- package/dist/{chunk-6DAZSKE5.js → chunk-IMILOCR5.js} +2 -2
- package/dist/chunk-LGQYTK55.js +442 -0
- package/dist/chunk-LGQYTK55.js.map +1 -0
- package/dist/{chunk-X6LLG7XN.js → chunk-PMGNLNSH.js} +15 -6
- package/dist/chunk-PMGNLNSH.js.map +1 -0
- package/dist/{chunk-LCJIPE5S.js → chunk-R3UJUOXI.js} +889 -591
- package/dist/chunk-R3UJUOXI.js.map +1 -0
- package/dist/chunk-SM3G6UAX.js +122 -0
- package/dist/chunk-SM3G6UAX.js.map +1 -0
- package/dist/chunk-T22OLSET.js +265 -0
- package/dist/chunk-T22OLSET.js.map +1 -0
- package/dist/chunk-THBR6OXH.js +62 -0
- package/dist/chunk-THBR6OXH.js.map +1 -0
- package/dist/{chunk-5KYLXEG3.js → chunk-TOZQ3JFN.js} +52 -9
- package/dist/chunk-TOZQ3JFN.js.map +1 -0
- package/dist/{chunk-IQIPQTQT.js → chunk-UB7XUO7C.js} +171 -26
- package/dist/chunk-UB7XUO7C.js.map +1 -0
- package/dist/{chunk-OORPX73T.js → chunk-W3EYKZNQ.js} +17 -2
- package/dist/chunk-W3EYKZNQ.js.map +1 -0
- package/dist/{chunk-K53OZH5Y.js → chunk-ZCHNAM3B.js} +76 -2
- package/dist/chunk-ZCHNAM3B.js.map +1 -0
- package/dist/cli.js +30 -29
- package/dist/cli.js.map +1 -1
- package/dist/{config-OH26EIWN.js → config-AK2W3E67.js} +2 -2
- package/dist/config-editor-VIA7A72X.js +12 -0
- package/dist/{config-registry-SNKA2EH2.js → config-registry-QQOJ2GQP.js} +2 -2
- package/dist/{daemon-VKCONJUY.js → daemon-G27YZUWB.js} +3 -3
- package/dist/discord-2DKRH45T.js +2044 -0
- package/dist/discord-2DKRH45T.js.map +1 -0
- package/dist/doctor-AN6AZ3PF.js +9 -0
- package/dist/doctor-CHCYUTV5.js +14 -0
- package/dist/doctor-CHCYUTV5.js.map +1 -0
- package/dist/index.d.ts +331 -6
- package/dist/index.js +21 -11
- package/dist/{main-NEYPQHB4.js → main-56SPFYW4.js} +32 -24
- package/dist/main-56SPFYW4.js.map +1 -0
- package/dist/{menu-J5YVH665.js → menu-XR2GET2B.js} +2 -2
- package/dist/menu-XR2GET2B.js.map +1 -0
- package/dist/new-session-DRRP2J7E.js +16 -0
- package/dist/new-session-DRRP2J7E.js.map +1 -0
- package/dist/session-FVFLBREJ.js +19 -0
- package/dist/session-FVFLBREJ.js.map +1 -0
- package/dist/settings-LPOLJ6SA.js +12 -0
- package/dist/settings-LPOLJ6SA.js.map +1 -0
- package/dist/{setup-ZCWGOEAH.js → setup-IPWJCIJM.js} +9 -5
- package/dist/setup-IPWJCIJM.js.map +1 -0
- package/dist/{version-VC5CPXBX.js → version-ALWGGVKM.js} +2 -2
- package/dist/version-ALWGGVKM.js.map +1 -0
- package/package.json +2 -1
- package/dist/chunk-2Z2XPUD5.js.map +0 -1
- package/dist/chunk-5KYLXEG3.js.map +0 -1
- package/dist/chunk-ECBD5I5R.js.map +0 -1
- package/dist/chunk-IQIPQTQT.js.map +0 -1
- package/dist/chunk-IURZ4QHG.js.map +0 -1
- package/dist/chunk-K53OZH5Y.js.map +0 -1
- package/dist/chunk-KSIQZC3J.js.map +0 -1
- package/dist/chunk-LCJIPE5S.js.map +0 -1
- package/dist/chunk-OORPX73T.js.map +0 -1
- package/dist/chunk-X6LLG7XN.js.map +0 -1
- package/dist/chunk-Z46LGZ7R.js.map +0 -1
- package/dist/config-editor-5TICUK3K.js +0 -12
- package/dist/doctor-X6UCE7GQ.js +0 -9
- package/dist/main-NEYPQHB4.js.map +0 -1
- /package/dist/{api-client-UN7BXQOQ.js.map → action-detect-6M5GCGAU.js.map} +0 -0
- /package/dist/{autostart-K73RQZVV.js.map → admin-IKPS5PFC.js.map} +0 -0
- /package/dist/{config-OH26EIWN.js.map → agents-55NX3DHM.js.map} +0 -0
- /package/dist/{config-editor-5TICUK3K.js.map → api-client-BH2JFHQW.js.map} +0 -0
- /package/dist/{config-registry-SNKA2EH2.js.map → autostart-A7JRU4WJ.js.map} +0 -0
- /package/dist/{chunk-6DAZSKE5.js.map → chunk-IMILOCR5.js.map} +0 -0
- /package/dist/{daemon-VKCONJUY.js.map → config-AK2W3E67.js.map} +0 -0
- /package/dist/{doctor-X6UCE7GQ.js.map → config-editor-VIA7A72X.js.map} +0 -0
- /package/dist/{menu-J5YVH665.js.map → config-registry-QQOJ2GQP.js.map} +0 -0
- /package/dist/{setup-ZCWGOEAH.js.map → daemon-G27YZUWB.js.map} +0 -0
- /package/dist/{version-VC5CPXBX.js.map → doctor-AN6AZ3PF.js.map} +0 -0
|
@@ -0,0 +1,2044 @@
|
|
|
1
|
+
import {
|
|
2
|
+
handleCancel,
|
|
3
|
+
handleCleanupButton,
|
|
4
|
+
handleHandoff,
|
|
5
|
+
handleSessions,
|
|
6
|
+
handleStatus
|
|
7
|
+
} from "./chunk-T22OLSET.js";
|
|
8
|
+
import {
|
|
9
|
+
handleAgentButton,
|
|
10
|
+
handleAgents,
|
|
11
|
+
handleInstall
|
|
12
|
+
} from "./chunk-H7ZMPBZC.js";
|
|
13
|
+
import {
|
|
14
|
+
handleSettings,
|
|
15
|
+
handleSettingsButton
|
|
16
|
+
} from "./chunk-THBR6OXH.js";
|
|
17
|
+
import {
|
|
18
|
+
handleDoctor,
|
|
19
|
+
handleDoctorButton
|
|
20
|
+
} from "./chunk-GINCOFNW.js";
|
|
21
|
+
import {
|
|
22
|
+
buildActionKeyboard,
|
|
23
|
+
detectAction,
|
|
24
|
+
storeAction
|
|
25
|
+
} from "./chunk-I7WC6E5S.js";
|
|
26
|
+
import {
|
|
27
|
+
handleNew,
|
|
28
|
+
handleNewChat,
|
|
29
|
+
handleNewSessionButton
|
|
30
|
+
} from "./chunk-AKIU4JBF.js";
|
|
31
|
+
import {
|
|
32
|
+
buildDeepLink,
|
|
33
|
+
createSessionThread,
|
|
34
|
+
deleteSessionThread,
|
|
35
|
+
ensureForums,
|
|
36
|
+
ensureUnarchived,
|
|
37
|
+
renameSessionThread
|
|
38
|
+
} from "./chunk-SM3G6UAX.js";
|
|
39
|
+
import {
|
|
40
|
+
handleDangerous,
|
|
41
|
+
handleDangerousButton,
|
|
42
|
+
handleRestart,
|
|
43
|
+
handleUpdate
|
|
44
|
+
} from "./chunk-7G5QKLLF.js";
|
|
45
|
+
import {
|
|
46
|
+
ChannelAdapter,
|
|
47
|
+
PRODUCT_GUIDE
|
|
48
|
+
} from "./chunk-LGQYTK55.js";
|
|
49
|
+
import "./chunk-ZCHNAM3B.js";
|
|
50
|
+
import "./chunk-4LFDEW22.js";
|
|
51
|
+
import {
|
|
52
|
+
log
|
|
53
|
+
} from "./chunk-ESOPMQAY.js";
|
|
54
|
+
|
|
55
|
+
// src/adapters/discord/adapter.ts
|
|
56
|
+
import { Client, GatewayIntentBits, MessageFlags } from "discord.js";
|
|
57
|
+
|
|
58
|
+
// src/adapters/discord/send-queue.ts
|
|
59
|
+
var DiscordSendQueue = class {
|
|
60
|
+
items = [];
|
|
61
|
+
processing = false;
|
|
62
|
+
lastExec = 0;
|
|
63
|
+
minInterval;
|
|
64
|
+
constructor(minInterval = 1e3) {
|
|
65
|
+
this.minInterval = minInterval;
|
|
66
|
+
}
|
|
67
|
+
enqueue(fn, opts) {
|
|
68
|
+
const type = opts.type;
|
|
69
|
+
const key = opts.key;
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
if (type === "text" && key) {
|
|
72
|
+
const idx = this.items.findIndex(
|
|
73
|
+
(item) => item.type === "text" && item.key === key
|
|
74
|
+
);
|
|
75
|
+
if (idx !== -1) {
|
|
76
|
+
this.items[idx].resolve(void 0);
|
|
77
|
+
this.items[idx] = { fn, type, key, resolve, reject };
|
|
78
|
+
this.scheduleProcess();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
this.items.push({ fn, type, key, resolve, reject });
|
|
83
|
+
this.scheduleProcess();
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
onRateLimited() {
|
|
87
|
+
log.warn("[DiscordSendQueue] Rate limited \u2014 dropping queued text items");
|
|
88
|
+
const remaining = [];
|
|
89
|
+
for (const item of this.items) {
|
|
90
|
+
if (item.type === "text") {
|
|
91
|
+
item.resolve(void 0);
|
|
92
|
+
} else {
|
|
93
|
+
remaining.push(item);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
this.items = remaining;
|
|
97
|
+
}
|
|
98
|
+
scheduleProcess() {
|
|
99
|
+
if (this.processing) return;
|
|
100
|
+
if (this.items.length === 0) return;
|
|
101
|
+
const elapsed = Date.now() - this.lastExec;
|
|
102
|
+
const delay = Math.max(0, this.minInterval - elapsed);
|
|
103
|
+
this.processing = true;
|
|
104
|
+
setTimeout(() => void this.processNext(), delay);
|
|
105
|
+
}
|
|
106
|
+
async processNext() {
|
|
107
|
+
const item = this.items.shift();
|
|
108
|
+
if (!item) {
|
|
109
|
+
this.processing = false;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const result = await item.fn();
|
|
114
|
+
item.resolve(result);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
item.reject(err);
|
|
117
|
+
} finally {
|
|
118
|
+
this.lastExec = Date.now();
|
|
119
|
+
this.processing = false;
|
|
120
|
+
this.scheduleProcess();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// src/adapters/discord/formatting.ts
|
|
126
|
+
var STATUS_ICON = {
|
|
127
|
+
running: "\u{1F504}",
|
|
128
|
+
completed: "\u2705",
|
|
129
|
+
failed: "\u274C",
|
|
130
|
+
pending: "\u23F3",
|
|
131
|
+
in_progress: "\u{1F504}"
|
|
132
|
+
};
|
|
133
|
+
var KIND_ICON = {
|
|
134
|
+
read: "\u{1F4D6}",
|
|
135
|
+
write: "\u270F\uFE0F",
|
|
136
|
+
command: "\u26A1",
|
|
137
|
+
search: "\u{1F50D}"
|
|
138
|
+
};
|
|
139
|
+
function extractContentText(content, depth = 0) {
|
|
140
|
+
if (!content || depth > 5) return "";
|
|
141
|
+
if (typeof content === "string") return content;
|
|
142
|
+
if (Array.isArray(content)) {
|
|
143
|
+
return content.map((c) => extractContentText(c, depth + 1)).filter(Boolean).join("\n");
|
|
144
|
+
}
|
|
145
|
+
if (typeof content === "object" && content !== null) {
|
|
146
|
+
const c = content;
|
|
147
|
+
if (c.type === "text" && typeof c.text === "string") return c.text;
|
|
148
|
+
if (typeof c.text === "string") return c.text;
|
|
149
|
+
if (typeof c.content === "string") return c.content;
|
|
150
|
+
if (c.content && typeof c.content === "object") return extractContentText(c.content, depth + 1);
|
|
151
|
+
if (c.input) return extractContentText(c.input, depth + 1);
|
|
152
|
+
if (c.output) return extractContentText(c.output, depth + 1);
|
|
153
|
+
const keys = Object.keys(c).filter((k) => k !== "type");
|
|
154
|
+
if (keys.length === 0) return "";
|
|
155
|
+
return JSON.stringify(c, null, 2);
|
|
156
|
+
}
|
|
157
|
+
return String(content);
|
|
158
|
+
}
|
|
159
|
+
function truncateContent(text, maxLen = 500) {
|
|
160
|
+
if (text.length <= maxLen) return text;
|
|
161
|
+
return text.slice(0, maxLen) + "\n\u2026 (truncated)";
|
|
162
|
+
}
|
|
163
|
+
function formatViewerLinks(links, filePath) {
|
|
164
|
+
if (!links) return "";
|
|
165
|
+
const fileName = filePath ? filePath.split("/").pop() || filePath : "";
|
|
166
|
+
let text = "\n";
|
|
167
|
+
if (links.file) text += `
|
|
168
|
+
[View ${fileName || "file"}](${links.file})`;
|
|
169
|
+
if (links.diff) text += `
|
|
170
|
+
[View diff${fileName ? ` \u2014 ${fileName}` : ""}](${links.diff})`;
|
|
171
|
+
return text;
|
|
172
|
+
}
|
|
173
|
+
function formatToolCall(tool) {
|
|
174
|
+
const si = STATUS_ICON[tool.status || ""] || "\u{1F527}";
|
|
175
|
+
const ki = KIND_ICON[tool.kind || ""] || "\u{1F6E0}\uFE0F";
|
|
176
|
+
let text = `${si} ${ki} **${tool.name || "Tool"}**`;
|
|
177
|
+
text += formatViewerLinks(tool.viewerLinks, tool.viewerFilePath);
|
|
178
|
+
if (!tool.viewerLinks) {
|
|
179
|
+
const details = extractContentText(tool.content);
|
|
180
|
+
if (details) {
|
|
181
|
+
text += `
|
|
182
|
+
\`\`\`
|
|
183
|
+
${truncateContent(details)}
|
|
184
|
+
\`\`\``;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return text;
|
|
188
|
+
}
|
|
189
|
+
function formatToolUpdate(update) {
|
|
190
|
+
return formatToolCall(update);
|
|
191
|
+
}
|
|
192
|
+
function formatPlan(entries) {
|
|
193
|
+
const statusIcon = {
|
|
194
|
+
pending: "\u23F3",
|
|
195
|
+
in_progress: "\u{1F504}",
|
|
196
|
+
completed: "\u2705"
|
|
197
|
+
};
|
|
198
|
+
const lines = entries.map(
|
|
199
|
+
(e, i) => `${statusIcon[e.status] || "\u2B1C"} ${i + 1}. ${e.content}`
|
|
200
|
+
);
|
|
201
|
+
return `**Plan:**
|
|
202
|
+
${lines.join("\n")}`;
|
|
203
|
+
}
|
|
204
|
+
function formatTokens(n) {
|
|
205
|
+
return n >= 1e3 ? `${Math.round(n / 1e3)}k` : String(n);
|
|
206
|
+
}
|
|
207
|
+
function progressBar(ratio) {
|
|
208
|
+
const filled = Math.round(Math.min(ratio, 1) * 10);
|
|
209
|
+
return "\u2593".repeat(filled) + "\u2591".repeat(10 - filled);
|
|
210
|
+
}
|
|
211
|
+
function formatUsage(usage) {
|
|
212
|
+
const { tokensUsed, contextSize } = usage;
|
|
213
|
+
if (tokensUsed == null) return "\u{1F4CA} Usage data unavailable";
|
|
214
|
+
if (contextSize == null) return `\u{1F4CA} ${formatTokens(tokensUsed)} tokens`;
|
|
215
|
+
const ratio = tokensUsed / contextSize;
|
|
216
|
+
const pct = Math.round(ratio * 100);
|
|
217
|
+
const bar = progressBar(ratio);
|
|
218
|
+
const emoji = pct >= 85 ? "\u26A0\uFE0F" : "\u{1F4CA}";
|
|
219
|
+
return `${emoji} ${formatTokens(tokensUsed)} / ${formatTokens(contextSize)} tokens
|
|
220
|
+
${bar} ${pct}%`;
|
|
221
|
+
}
|
|
222
|
+
function splitMessage(text, maxLength = 1800) {
|
|
223
|
+
if (text.length <= maxLength) return [text];
|
|
224
|
+
const chunks = [];
|
|
225
|
+
let remaining = text;
|
|
226
|
+
while (remaining.length > 0) {
|
|
227
|
+
if (remaining.length <= maxLength) {
|
|
228
|
+
chunks.push(remaining);
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
const wouldLeaveSmall = remaining.length < maxLength * 1.3;
|
|
232
|
+
const searchLimit = wouldLeaveSmall ? Math.floor(remaining.length / 2) + 300 : maxLength;
|
|
233
|
+
let splitAt = remaining.lastIndexOf("\n\n", searchLimit);
|
|
234
|
+
if (splitAt === -1 || splitAt < searchLimit * 0.2) {
|
|
235
|
+
splitAt = remaining.lastIndexOf("\n", searchLimit);
|
|
236
|
+
}
|
|
237
|
+
if (splitAt === -1 || splitAt < searchLimit * 0.2) {
|
|
238
|
+
splitAt = searchLimit;
|
|
239
|
+
}
|
|
240
|
+
const candidate = remaining.slice(0, splitAt);
|
|
241
|
+
const fences = candidate.match(/```/g);
|
|
242
|
+
if (fences && fences.length % 2 !== 0) {
|
|
243
|
+
const closingFence = remaining.indexOf("```", splitAt);
|
|
244
|
+
if (closingFence !== -1) {
|
|
245
|
+
const afterFence = remaining.indexOf("\n", closingFence + 3);
|
|
246
|
+
splitAt = afterFence !== -1 ? afterFence + 1 : closingFence + 3;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
chunks.push(remaining.slice(0, splitAt));
|
|
250
|
+
remaining = remaining.slice(splitAt).replace(/^\n+/, "");
|
|
251
|
+
}
|
|
252
|
+
return chunks;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/adapters/discord/tool-call-tracker.ts
|
|
256
|
+
var ToolCallTracker = class {
|
|
257
|
+
constructor(sendQueue) {
|
|
258
|
+
this.sendQueue = sendQueue;
|
|
259
|
+
}
|
|
260
|
+
sessions = /* @__PURE__ */ new Map();
|
|
261
|
+
async trackNewCall(sessionId, thread, tool) {
|
|
262
|
+
if (!this.sessions.has(sessionId)) {
|
|
263
|
+
this.sessions.set(sessionId, /* @__PURE__ */ new Map());
|
|
264
|
+
}
|
|
265
|
+
let resolveReady;
|
|
266
|
+
const ready = new Promise((r) => {
|
|
267
|
+
resolveReady = r;
|
|
268
|
+
});
|
|
269
|
+
const state = {
|
|
270
|
+
message: void 0,
|
|
271
|
+
name: tool.name,
|
|
272
|
+
kind: tool.kind,
|
|
273
|
+
viewerLinks: tool.viewerLinks,
|
|
274
|
+
viewerFilePath: tool.viewerFilePath,
|
|
275
|
+
ready
|
|
276
|
+
};
|
|
277
|
+
this.sessions.get(sessionId).set(tool.id, state);
|
|
278
|
+
const content = formatToolCall(tool);
|
|
279
|
+
try {
|
|
280
|
+
const msg = await this.sendQueue.enqueue(
|
|
281
|
+
() => thread.send({ content }),
|
|
282
|
+
{ type: "other" }
|
|
283
|
+
);
|
|
284
|
+
if (msg) state.message = msg;
|
|
285
|
+
} catch (err) {
|
|
286
|
+
log.warn({ err, toolId: tool.id }, "[ToolCallTracker] trackNewCall() send failed");
|
|
287
|
+
} finally {
|
|
288
|
+
resolveReady();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
async updateCall(sessionId, update) {
|
|
292
|
+
const toolState = this.sessions.get(sessionId)?.get(update.id);
|
|
293
|
+
if (!toolState) return;
|
|
294
|
+
if (update.viewerLinks) toolState.viewerLinks = update.viewerLinks;
|
|
295
|
+
if (update.viewerFilePath) toolState.viewerFilePath = update.viewerFilePath;
|
|
296
|
+
if (update.name) toolState.name = update.name;
|
|
297
|
+
if (update.kind) toolState.kind = update.kind;
|
|
298
|
+
const isTerminal = update.status === "completed" || update.status === "failed";
|
|
299
|
+
if (!isTerminal) return;
|
|
300
|
+
await toolState.ready;
|
|
301
|
+
if (!toolState.message) return;
|
|
302
|
+
log.debug(
|
|
303
|
+
{
|
|
304
|
+
toolId: update.id,
|
|
305
|
+
status: update.status,
|
|
306
|
+
hasViewerLinks: !!toolState.viewerLinks,
|
|
307
|
+
name: toolState.name,
|
|
308
|
+
msgId: toolState.message.id
|
|
309
|
+
},
|
|
310
|
+
"[ToolCallTracker] Tool completed, preparing edit"
|
|
311
|
+
);
|
|
312
|
+
const merged = {
|
|
313
|
+
...update,
|
|
314
|
+
name: toolState.name,
|
|
315
|
+
kind: toolState.kind,
|
|
316
|
+
viewerLinks: toolState.viewerLinks,
|
|
317
|
+
viewerFilePath: toolState.viewerFilePath
|
|
318
|
+
};
|
|
319
|
+
const content = formatToolUpdate(merged);
|
|
320
|
+
try {
|
|
321
|
+
await this.sendQueue.enqueue(
|
|
322
|
+
() => toolState.message.edit({ content }),
|
|
323
|
+
{ type: "other" }
|
|
324
|
+
);
|
|
325
|
+
} catch (err) {
|
|
326
|
+
log.warn(
|
|
327
|
+
{
|
|
328
|
+
err,
|
|
329
|
+
msgId: toolState.message.id,
|
|
330
|
+
contentLen: content.length,
|
|
331
|
+
hasViewerLinks: !!merged.viewerLinks
|
|
332
|
+
},
|
|
333
|
+
"[ToolCallTracker] Tool update edit failed"
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
cleanup(sessionId) {
|
|
338
|
+
this.sessions.delete(sessionId);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
// src/adapters/discord/streaming.ts
|
|
343
|
+
var FLUSH_INTERVAL = 5e3;
|
|
344
|
+
var MAX_DISPLAY_LENGTH = 1900;
|
|
345
|
+
var MessageDraft = class {
|
|
346
|
+
constructor(thread, sendQueue, sessionId) {
|
|
347
|
+
this.thread = thread;
|
|
348
|
+
this.sendQueue = sendQueue;
|
|
349
|
+
this.sessionId = sessionId;
|
|
350
|
+
}
|
|
351
|
+
buffer = "";
|
|
352
|
+
message;
|
|
353
|
+
flushTimer;
|
|
354
|
+
flushPromise = Promise.resolve();
|
|
355
|
+
lastSentBuffer = "";
|
|
356
|
+
displayTruncated = false;
|
|
357
|
+
firstFlushPending = false;
|
|
358
|
+
append(text) {
|
|
359
|
+
if (!text) return;
|
|
360
|
+
this.buffer += text;
|
|
361
|
+
this.scheduleFlush();
|
|
362
|
+
}
|
|
363
|
+
getBuffer() {
|
|
364
|
+
return this.buffer;
|
|
365
|
+
}
|
|
366
|
+
scheduleFlush() {
|
|
367
|
+
if (this.flushTimer) return;
|
|
368
|
+
this.flushTimer = setTimeout(() => {
|
|
369
|
+
this.flushTimer = void 0;
|
|
370
|
+
this.flushPromise = this.flushPromise.then(() => this.flush()).catch(() => {
|
|
371
|
+
});
|
|
372
|
+
}, FLUSH_INTERVAL);
|
|
373
|
+
}
|
|
374
|
+
async flush() {
|
|
375
|
+
if (!this.buffer) return;
|
|
376
|
+
if (this.firstFlushPending) return;
|
|
377
|
+
const snapshot = this.buffer;
|
|
378
|
+
let content = snapshot;
|
|
379
|
+
let truncated = false;
|
|
380
|
+
if (content.length > MAX_DISPLAY_LENGTH) {
|
|
381
|
+
content = snapshot.slice(0, MAX_DISPLAY_LENGTH) + "\u2026";
|
|
382
|
+
truncated = true;
|
|
383
|
+
}
|
|
384
|
+
if (!content) return;
|
|
385
|
+
if (!this.message) {
|
|
386
|
+
this.firstFlushPending = true;
|
|
387
|
+
try {
|
|
388
|
+
const result = await this.sendQueue.enqueue(
|
|
389
|
+
() => this.thread.send({ content }),
|
|
390
|
+
{ type: "other" }
|
|
391
|
+
);
|
|
392
|
+
if (result) {
|
|
393
|
+
this.message = result;
|
|
394
|
+
if (!truncated) {
|
|
395
|
+
this.lastSentBuffer = snapshot;
|
|
396
|
+
this.displayTruncated = false;
|
|
397
|
+
} else {
|
|
398
|
+
this.displayTruncated = true;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
} catch {
|
|
402
|
+
} finally {
|
|
403
|
+
this.firstFlushPending = false;
|
|
404
|
+
}
|
|
405
|
+
} else {
|
|
406
|
+
if (!truncated && snapshot === this.lastSentBuffer) return;
|
|
407
|
+
try {
|
|
408
|
+
const result = await this.sendQueue.enqueue(
|
|
409
|
+
() => this.message.edit({ content }),
|
|
410
|
+
{ type: "text", key: this.sessionId }
|
|
411
|
+
);
|
|
412
|
+
if (result !== void 0) {
|
|
413
|
+
if (!truncated) {
|
|
414
|
+
this.lastSentBuffer = snapshot;
|
|
415
|
+
this.displayTruncated = false;
|
|
416
|
+
} else {
|
|
417
|
+
this.displayTruncated = true;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
} catch {
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
async finalize() {
|
|
425
|
+
if (this.flushTimer) {
|
|
426
|
+
clearTimeout(this.flushTimer);
|
|
427
|
+
this.flushTimer = void 0;
|
|
428
|
+
}
|
|
429
|
+
await this.flushPromise;
|
|
430
|
+
if (!this.buffer) return;
|
|
431
|
+
if (this.message && this.buffer === this.lastSentBuffer && !this.displayTruncated) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (this.buffer.length <= MAX_DISPLAY_LENGTH) {
|
|
435
|
+
const content = this.buffer;
|
|
436
|
+
try {
|
|
437
|
+
if (this.message) {
|
|
438
|
+
await this.sendQueue.enqueue(
|
|
439
|
+
() => this.message.edit({ content }),
|
|
440
|
+
{ type: "other" }
|
|
441
|
+
);
|
|
442
|
+
} else {
|
|
443
|
+
await this.sendQueue.enqueue(
|
|
444
|
+
() => this.thread.send({ content }),
|
|
445
|
+
{ type: "other" }
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
return;
|
|
449
|
+
} catch {
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
const chunks = splitMessage(this.buffer, MAX_DISPLAY_LENGTH);
|
|
453
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
454
|
+
const content = chunks[i];
|
|
455
|
+
try {
|
|
456
|
+
if (i === 0 && this.message) {
|
|
457
|
+
await this.sendQueue.enqueue(
|
|
458
|
+
() => this.message.edit({ content }),
|
|
459
|
+
{ type: "other" }
|
|
460
|
+
);
|
|
461
|
+
} else {
|
|
462
|
+
const msg = await this.sendQueue.enqueue(
|
|
463
|
+
() => this.thread.send({ content }),
|
|
464
|
+
{ type: "other" }
|
|
465
|
+
);
|
|
466
|
+
if (msg) {
|
|
467
|
+
this.message = msg;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
} catch {
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
// src/adapters/discord/draft-manager.ts
|
|
477
|
+
var DraftManager = class {
|
|
478
|
+
constructor(sendQueue) {
|
|
479
|
+
this.sendQueue = sendQueue;
|
|
480
|
+
}
|
|
481
|
+
drafts = /* @__PURE__ */ new Map();
|
|
482
|
+
textBuffers = /* @__PURE__ */ new Map();
|
|
483
|
+
getOrCreate(sessionId, thread) {
|
|
484
|
+
let draft = this.drafts.get(sessionId);
|
|
485
|
+
if (!draft) {
|
|
486
|
+
draft = new MessageDraft(thread, this.sendQueue, sessionId);
|
|
487
|
+
this.drafts.set(sessionId, draft);
|
|
488
|
+
}
|
|
489
|
+
return draft;
|
|
490
|
+
}
|
|
491
|
+
hasDraft(sessionId) {
|
|
492
|
+
return this.drafts.has(sessionId);
|
|
493
|
+
}
|
|
494
|
+
appendText(sessionId, text) {
|
|
495
|
+
this.textBuffers.set(sessionId, (this.textBuffers.get(sessionId) ?? "") + text);
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Finalize the current draft.
|
|
499
|
+
* If isAssistant is true, detects action patterns in the accumulated text and sends
|
|
500
|
+
* action buttons as a follow-up message.
|
|
501
|
+
*/
|
|
502
|
+
async finalize(sessionId, thread, isAssistant) {
|
|
503
|
+
const draft = this.drafts.get(sessionId);
|
|
504
|
+
if (!draft) return;
|
|
505
|
+
this.drafts.delete(sessionId);
|
|
506
|
+
await draft.finalize();
|
|
507
|
+
if (isAssistant && thread) {
|
|
508
|
+
const fullText = this.textBuffers.get(sessionId);
|
|
509
|
+
this.textBuffers.delete(sessionId);
|
|
510
|
+
if (fullText) {
|
|
511
|
+
const detected = detectAction(fullText);
|
|
512
|
+
if (detected) {
|
|
513
|
+
const actionId = storeAction(detected);
|
|
514
|
+
const components = [buildActionKeyboard(actionId, detected)];
|
|
515
|
+
try {
|
|
516
|
+
await this.sendQueue.enqueue(
|
|
517
|
+
() => thread.send({ components }),
|
|
518
|
+
{ type: "other" }
|
|
519
|
+
);
|
|
520
|
+
} catch {
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
} else {
|
|
525
|
+
this.textBuffers.delete(sessionId);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
cleanup(sessionId) {
|
|
529
|
+
this.drafts.delete(sessionId);
|
|
530
|
+
this.textBuffers.delete(sessionId);
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
// src/adapters/discord/activity.ts
|
|
535
|
+
import { EmbedBuilder } from "discord.js";
|
|
536
|
+
var TYPING_REFRESH_MS = 8e3;
|
|
537
|
+
var ThinkingIndicator = class {
|
|
538
|
+
constructor(channel) {
|
|
539
|
+
this.channel = channel;
|
|
540
|
+
}
|
|
541
|
+
dismissed = false;
|
|
542
|
+
refreshTimer;
|
|
543
|
+
async show() {
|
|
544
|
+
if (this.dismissed) return;
|
|
545
|
+
try {
|
|
546
|
+
await this.channel.sendTyping();
|
|
547
|
+
this.startRefreshTimer();
|
|
548
|
+
} catch (err) {
|
|
549
|
+
log.warn({ err }, "[ThinkingIndicator] sendTyping() failed");
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
dismiss() {
|
|
553
|
+
this.dismissed = true;
|
|
554
|
+
this.stopRefreshTimer();
|
|
555
|
+
}
|
|
556
|
+
reset() {
|
|
557
|
+
this.dismissed = false;
|
|
558
|
+
}
|
|
559
|
+
startRefreshTimer() {
|
|
560
|
+
this.stopRefreshTimer();
|
|
561
|
+
this.refreshTimer = setInterval(() => {
|
|
562
|
+
if (this.dismissed) {
|
|
563
|
+
this.stopRefreshTimer();
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
this.channel.sendTyping().catch(() => {
|
|
567
|
+
});
|
|
568
|
+
}, TYPING_REFRESH_MS);
|
|
569
|
+
}
|
|
570
|
+
stopRefreshTimer() {
|
|
571
|
+
if (this.refreshTimer) {
|
|
572
|
+
clearInterval(this.refreshTimer);
|
|
573
|
+
this.refreshTimer = void 0;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
var UsageMessage = class {
|
|
578
|
+
constructor(thread, sendQueue) {
|
|
579
|
+
this.thread = thread;
|
|
580
|
+
this.sendQueue = sendQueue;
|
|
581
|
+
}
|
|
582
|
+
message;
|
|
583
|
+
async send(usage) {
|
|
584
|
+
const text = formatUsage(usage);
|
|
585
|
+
const embed = new EmbedBuilder().setDescription(text);
|
|
586
|
+
try {
|
|
587
|
+
if (this.message) {
|
|
588
|
+
await this.sendQueue.enqueue(
|
|
589
|
+
() => this.message.edit({ embeds: [embed] }),
|
|
590
|
+
{ type: "other" }
|
|
591
|
+
);
|
|
592
|
+
} else {
|
|
593
|
+
const result = await this.sendQueue.enqueue(
|
|
594
|
+
() => this.thread.send({ embeds: [embed] }),
|
|
595
|
+
{ type: "other" }
|
|
596
|
+
);
|
|
597
|
+
if (result) this.message = result;
|
|
598
|
+
}
|
|
599
|
+
} catch (err) {
|
|
600
|
+
log.warn({ err }, "[UsageMessage] send() failed");
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
async delete() {
|
|
604
|
+
if (!this.message) return;
|
|
605
|
+
const msg = this.message;
|
|
606
|
+
this.message = void 0;
|
|
607
|
+
try {
|
|
608
|
+
await this.sendQueue.enqueue(
|
|
609
|
+
() => msg.delete(),
|
|
610
|
+
{ type: "other" }
|
|
611
|
+
);
|
|
612
|
+
} catch (err) {
|
|
613
|
+
log.warn({ err }, "[UsageMessage] delete() failed");
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
var PLAN_DEBOUNCE_MS = 3500;
|
|
618
|
+
var PlanCard = class {
|
|
619
|
+
constructor(thread, sendQueue) {
|
|
620
|
+
this.thread = thread;
|
|
621
|
+
this.sendQueue = sendQueue;
|
|
622
|
+
}
|
|
623
|
+
message;
|
|
624
|
+
flushPromise = Promise.resolve();
|
|
625
|
+
latestEntries;
|
|
626
|
+
lastSentText;
|
|
627
|
+
flushTimer;
|
|
628
|
+
update(entries) {
|
|
629
|
+
this.latestEntries = entries;
|
|
630
|
+
if (this.flushTimer) clearTimeout(this.flushTimer);
|
|
631
|
+
this.flushTimer = setTimeout(() => {
|
|
632
|
+
this.flushTimer = void 0;
|
|
633
|
+
this.flushPromise = this.flushPromise.then(() => this._flush()).catch(() => {
|
|
634
|
+
});
|
|
635
|
+
}, PLAN_DEBOUNCE_MS);
|
|
636
|
+
}
|
|
637
|
+
async finalize() {
|
|
638
|
+
if (!this.latestEntries) return;
|
|
639
|
+
if (this.flushTimer) {
|
|
640
|
+
clearTimeout(this.flushTimer);
|
|
641
|
+
this.flushTimer = void 0;
|
|
642
|
+
}
|
|
643
|
+
await this.flushPromise;
|
|
644
|
+
this.flushPromise = this.flushPromise.then(() => this._flush()).catch(() => {
|
|
645
|
+
});
|
|
646
|
+
await this.flushPromise;
|
|
647
|
+
}
|
|
648
|
+
destroy() {
|
|
649
|
+
if (this.flushTimer) {
|
|
650
|
+
clearTimeout(this.flushTimer);
|
|
651
|
+
this.flushTimer = void 0;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
async _flush() {
|
|
655
|
+
if (!this.latestEntries) return;
|
|
656
|
+
const text = formatPlan(this.latestEntries);
|
|
657
|
+
if (this.message && text === this.lastSentText) return;
|
|
658
|
+
this.lastSentText = text;
|
|
659
|
+
const embed = new EmbedBuilder().setDescription(text);
|
|
660
|
+
try {
|
|
661
|
+
if (this.message) {
|
|
662
|
+
await this.sendQueue.enqueue(
|
|
663
|
+
() => this.message.edit({ embeds: [embed] }),
|
|
664
|
+
{ type: "other" }
|
|
665
|
+
);
|
|
666
|
+
} else {
|
|
667
|
+
const result = await this.sendQueue.enqueue(
|
|
668
|
+
() => this.thread.send({ embeds: [embed] }),
|
|
669
|
+
{ type: "other" }
|
|
670
|
+
);
|
|
671
|
+
if (result) this.message = result;
|
|
672
|
+
}
|
|
673
|
+
} catch (err) {
|
|
674
|
+
log.warn({ err }, "[PlanCard] flush failed");
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
var ActivityTracker = class {
|
|
679
|
+
constructor(thread, sendQueue) {
|
|
680
|
+
this.thread = thread;
|
|
681
|
+
this.sendQueue = sendQueue;
|
|
682
|
+
this.thinking = new ThinkingIndicator(thread);
|
|
683
|
+
this.planCard = new PlanCard(thread, sendQueue);
|
|
684
|
+
this.usage = new UsageMessage(thread, sendQueue);
|
|
685
|
+
}
|
|
686
|
+
isFirstEvent = true;
|
|
687
|
+
hasPlanCard = false;
|
|
688
|
+
thinking;
|
|
689
|
+
planCard;
|
|
690
|
+
usage;
|
|
691
|
+
async onNewPrompt() {
|
|
692
|
+
this.isFirstEvent = true;
|
|
693
|
+
this.hasPlanCard = false;
|
|
694
|
+
this.thinking.dismiss();
|
|
695
|
+
this.thinking.reset();
|
|
696
|
+
}
|
|
697
|
+
async onThought() {
|
|
698
|
+
await this._firstEventGuard();
|
|
699
|
+
await this.thinking.show();
|
|
700
|
+
}
|
|
701
|
+
async onTextStart() {
|
|
702
|
+
await this._firstEventGuard();
|
|
703
|
+
this.thinking.dismiss();
|
|
704
|
+
}
|
|
705
|
+
async onToolCall() {
|
|
706
|
+
await this._firstEventGuard();
|
|
707
|
+
this.thinking.dismiss();
|
|
708
|
+
this.thinking.reset();
|
|
709
|
+
}
|
|
710
|
+
async onPlan(entries) {
|
|
711
|
+
await this._firstEventGuard();
|
|
712
|
+
this.thinking.dismiss();
|
|
713
|
+
this.hasPlanCard = true;
|
|
714
|
+
this.planCard.update(entries);
|
|
715
|
+
}
|
|
716
|
+
async sendUsage(usage) {
|
|
717
|
+
await this.usage.send(usage);
|
|
718
|
+
}
|
|
719
|
+
async cleanup() {
|
|
720
|
+
this.thinking.dismiss();
|
|
721
|
+
this.planCard.destroy();
|
|
722
|
+
if (this.hasPlanCard) {
|
|
723
|
+
await this.planCard.finalize();
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
async _firstEventGuard() {
|
|
727
|
+
if (!this.isFirstEvent) return;
|
|
728
|
+
this.isFirstEvent = false;
|
|
729
|
+
await this.usage.delete();
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
// src/adapters/discord/skill-command-manager.ts
|
|
734
|
+
var DISCORD_MSG_LIMIT = 1900;
|
|
735
|
+
function buildSkillContent(commands) {
|
|
736
|
+
const sorted = [...commands].sort((a, b) => a.name.localeCompare(b.name));
|
|
737
|
+
const header = "**Available Skills**\n";
|
|
738
|
+
const lines = sorted.map((c) => `\`/${c.name}\``);
|
|
739
|
+
let content = header;
|
|
740
|
+
for (const line of lines) {
|
|
741
|
+
const candidate = content + "\n" + line;
|
|
742
|
+
if (candidate.length > DISCORD_MSG_LIMIT) break;
|
|
743
|
+
content = candidate;
|
|
744
|
+
}
|
|
745
|
+
return content;
|
|
746
|
+
}
|
|
747
|
+
var SkillCommandManager = class {
|
|
748
|
+
constructor(sendQueue, sessionManager) {
|
|
749
|
+
this.sendQueue = sendQueue;
|
|
750
|
+
this.sessionManager = sessionManager;
|
|
751
|
+
}
|
|
752
|
+
messages = /* @__PURE__ */ new Map();
|
|
753
|
+
async send(sessionId, thread, commands) {
|
|
754
|
+
if (!this.messages.has(sessionId)) {
|
|
755
|
+
const record = this.sessionManager.getSessionRecord(sessionId);
|
|
756
|
+
const platform = record?.platform;
|
|
757
|
+
if (platform?.skillMsgId) {
|
|
758
|
+
try {
|
|
759
|
+
const msg = await thread.messages.fetch(platform.skillMsgId);
|
|
760
|
+
if (msg) this.messages.set(sessionId, msg);
|
|
761
|
+
} catch {
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
if (commands.length === 0) {
|
|
766
|
+
await this.cleanup(sessionId);
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
const content = buildSkillContent(commands);
|
|
770
|
+
const existingMsg = this.messages.get(sessionId);
|
|
771
|
+
if (existingMsg) {
|
|
772
|
+
try {
|
|
773
|
+
await existingMsg.edit({ content });
|
|
774
|
+
return;
|
|
775
|
+
} catch (err) {
|
|
776
|
+
const msg = err instanceof Error ? err.message : "";
|
|
777
|
+
if (msg.includes("Unknown Message") || msg.includes("10008")) {
|
|
778
|
+
this.messages.delete(sessionId);
|
|
779
|
+
} else {
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
try {
|
|
785
|
+
const msg = await this.sendQueue.enqueue(
|
|
786
|
+
() => thread.send({ content }),
|
|
787
|
+
{ type: "other" }
|
|
788
|
+
);
|
|
789
|
+
if (!msg) return;
|
|
790
|
+
this.messages.set(sessionId, msg);
|
|
791
|
+
const record = this.sessionManager.getSessionRecord(sessionId);
|
|
792
|
+
if (record) {
|
|
793
|
+
await this.sessionManager.patchRecord(sessionId, {
|
|
794
|
+
platform: { ...record.platform, skillMsgId: msg.id }
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
try {
|
|
798
|
+
await msg.pin();
|
|
799
|
+
} catch (err) {
|
|
800
|
+
log.warn({ err, sessionId }, "[SkillCommandManager] Failed to pin skill message");
|
|
801
|
+
}
|
|
802
|
+
} catch (err) {
|
|
803
|
+
log.error({ err, sessionId }, "[SkillCommandManager] Failed to send skill commands");
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
async cleanup(sessionId) {
|
|
807
|
+
const msg = this.messages.get(sessionId);
|
|
808
|
+
this.messages.delete(sessionId);
|
|
809
|
+
if (msg) {
|
|
810
|
+
try {
|
|
811
|
+
await msg.edit({ content: "*Session ended*" });
|
|
812
|
+
await msg.unpin();
|
|
813
|
+
} catch {
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
const record = this.sessionManager.getSessionRecord(sessionId);
|
|
817
|
+
if (record) {
|
|
818
|
+
const platform = record.platform;
|
|
819
|
+
const { skillMsgId: _removed, ...rest } = platform;
|
|
820
|
+
await this.sessionManager.patchRecord(sessionId, { platform: rest });
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
// src/adapters/discord/permissions.ts
|
|
826
|
+
import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js";
|
|
827
|
+
import { nanoid } from "nanoid";
|
|
828
|
+
var PermissionHandler = class {
|
|
829
|
+
constructor(guildId, getSession, sendNotification) {
|
|
830
|
+
this.guildId = guildId;
|
|
831
|
+
this.getSession = getSession;
|
|
832
|
+
this.sendNotification = sendNotification;
|
|
833
|
+
}
|
|
834
|
+
pending = /* @__PURE__ */ new Map();
|
|
835
|
+
async sendPermissionRequest(session, request, thread) {
|
|
836
|
+
const callbackKey = nanoid(8);
|
|
837
|
+
this.pending.set(callbackKey, {
|
|
838
|
+
sessionId: session.id,
|
|
839
|
+
requestId: request.id,
|
|
840
|
+
options: request.options.map((o) => ({ id: o.id, isAllow: o.isAllow })),
|
|
841
|
+
guildId: this.guildId,
|
|
842
|
+
channelId: thread.id
|
|
843
|
+
});
|
|
844
|
+
const row = new ActionRowBuilder();
|
|
845
|
+
for (const option of request.options) {
|
|
846
|
+
const emoji = option.isAllow ? "\u2705" : "\u274C";
|
|
847
|
+
row.addComponents(
|
|
848
|
+
new ButtonBuilder().setCustomId(`p:${callbackKey}:${option.id}`).setLabel(`${emoji} ${option.label}`).setStyle(option.isAllow ? ButtonStyle.Success : ButtonStyle.Danger)
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
let messageId;
|
|
852
|
+
try {
|
|
853
|
+
const msg = await thread.send({
|
|
854
|
+
content: `\u{1F510} **Permission request:**
|
|
855
|
+
|
|
856
|
+
${request.description}`,
|
|
857
|
+
components: [row]
|
|
858
|
+
});
|
|
859
|
+
messageId = msg.id;
|
|
860
|
+
const pendingEntry = this.pending.get(callbackKey);
|
|
861
|
+
if (pendingEntry) pendingEntry.messageId = messageId;
|
|
862
|
+
} catch (err) {
|
|
863
|
+
log.warn({ err, sessionId: session.id }, "[PermissionHandler] Failed to send permission request");
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
const deepLink = buildDeepLink(this.guildId, thread.id, messageId);
|
|
867
|
+
void this.sendNotification({
|
|
868
|
+
sessionId: session.id,
|
|
869
|
+
sessionName: session.name,
|
|
870
|
+
type: "permission",
|
|
871
|
+
summary: request.description,
|
|
872
|
+
deepLink
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
async handleButtonInteraction(interaction) {
|
|
876
|
+
const data = interaction.customId;
|
|
877
|
+
if (!data.startsWith("p:")) return false;
|
|
878
|
+
const parts = data.split(":");
|
|
879
|
+
if (parts.length < 3) return false;
|
|
880
|
+
const [, callbackKey, optionId] = parts;
|
|
881
|
+
const pending = this.pending.get(callbackKey);
|
|
882
|
+
if (!pending) {
|
|
883
|
+
try {
|
|
884
|
+
await interaction.reply({ content: "\u274C Permission request expired", ephemeral: true });
|
|
885
|
+
} catch {
|
|
886
|
+
}
|
|
887
|
+
return true;
|
|
888
|
+
}
|
|
889
|
+
const session = this.getSession(pending.sessionId);
|
|
890
|
+
const option = pending.options.find((o) => o.id === optionId);
|
|
891
|
+
const isAllow = option?.isAllow ?? false;
|
|
892
|
+
log.info(
|
|
893
|
+
{ requestId: pending.requestId, optionId, isAllow },
|
|
894
|
+
"[PermissionHandler] Permission responded"
|
|
895
|
+
);
|
|
896
|
+
if (session?.permissionGate.requestId === pending.requestId) {
|
|
897
|
+
session.permissionGate.resolve(optionId);
|
|
898
|
+
}
|
|
899
|
+
this.pending.delete(callbackKey);
|
|
900
|
+
try {
|
|
901
|
+
await interaction.reply({ content: "\u2705 Responded", ephemeral: true });
|
|
902
|
+
} catch {
|
|
903
|
+
}
|
|
904
|
+
try {
|
|
905
|
+
await interaction.message.edit({ components: [] });
|
|
906
|
+
} catch {
|
|
907
|
+
}
|
|
908
|
+
return true;
|
|
909
|
+
}
|
|
910
|
+
};
|
|
911
|
+
|
|
912
|
+
// src/adapters/discord/commands/index.ts
|
|
913
|
+
import { SlashCommandBuilder } from "discord.js";
|
|
914
|
+
|
|
915
|
+
// src/adapters/discord/commands/menu.ts
|
|
916
|
+
import {
|
|
917
|
+
ActionRowBuilder as ActionRowBuilder2,
|
|
918
|
+
ButtonBuilder as ButtonBuilder2,
|
|
919
|
+
ButtonStyle as ButtonStyle2
|
|
920
|
+
} from "discord.js";
|
|
921
|
+
function buildMenuKeyboard() {
|
|
922
|
+
const row1 = new ActionRowBuilder2().addComponents(
|
|
923
|
+
new ButtonBuilder2().setCustomId("m:new").setLabel("\u{1F195} New Session").setStyle(ButtonStyle2.Primary),
|
|
924
|
+
new ButtonBuilder2().setCustomId("m:sessions").setLabel("\u{1F4CB} Sessions").setStyle(ButtonStyle2.Secondary),
|
|
925
|
+
new ButtonBuilder2().setCustomId("m:status").setLabel("\u{1F4CA} Status").setStyle(ButtonStyle2.Secondary)
|
|
926
|
+
);
|
|
927
|
+
const row2 = new ActionRowBuilder2().addComponents(
|
|
928
|
+
new ButtonBuilder2().setCustomId("m:agents").setLabel("\u{1F916} Agents").setStyle(ButtonStyle2.Secondary),
|
|
929
|
+
new ButtonBuilder2().setCustomId("m:settings").setLabel("\u2699\uFE0F Settings").setStyle(ButtonStyle2.Secondary),
|
|
930
|
+
new ButtonBuilder2().setCustomId("m:integrate").setLabel("\u{1F517} Integrate").setStyle(ButtonStyle2.Secondary)
|
|
931
|
+
);
|
|
932
|
+
const row3 = new ActionRowBuilder2().addComponents(
|
|
933
|
+
new ButtonBuilder2().setCustomId("m:restart").setLabel("\u{1F504} Restart").setStyle(ButtonStyle2.Secondary),
|
|
934
|
+
new ButtonBuilder2().setCustomId("m:update").setLabel("\u2B06\uFE0F Update").setStyle(ButtonStyle2.Secondary),
|
|
935
|
+
new ButtonBuilder2().setCustomId("m:help").setLabel("\u2753 Help").setStyle(ButtonStyle2.Secondary),
|
|
936
|
+
new ButtonBuilder2().setCustomId("m:doctor").setLabel("\u{1FA7A} Doctor").setStyle(ButtonStyle2.Secondary)
|
|
937
|
+
);
|
|
938
|
+
return [row1, row2, row3];
|
|
939
|
+
}
|
|
940
|
+
async function handleMenu(interaction, _adapter) {
|
|
941
|
+
await interaction.reply({
|
|
942
|
+
content: "**OpenACP Menu**\nChoose an action:",
|
|
943
|
+
components: buildMenuKeyboard(),
|
|
944
|
+
ephemeral: true
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
async function handleHelp(interaction, _adapter) {
|
|
948
|
+
await interaction.reply({
|
|
949
|
+
content: `\u{1F4D6} **OpenACP Help**
|
|
950
|
+
|
|
951
|
+
\u{1F680} **Getting Started**
|
|
952
|
+
Use \u{1F195} New Session or \`/new\` to start coding with AI.
|
|
953
|
+
Each session gets its own forum thread \u2014 chat there to work with the agent.
|
|
954
|
+
|
|
955
|
+
\u{1F4A1} **Common Commands**
|
|
956
|
+
\`/new [agent] [workspace]\` \u2014 Create new session
|
|
957
|
+
\`/newchat\` \u2014 New chat, same agent & workspace
|
|
958
|
+
\`/cancel\` \u2014 Cancel current session
|
|
959
|
+
\`/status\` \u2014 Show session or system status
|
|
960
|
+
\`/sessions\` \u2014 List all sessions
|
|
961
|
+
\`/agents\` \u2014 Browse & install agents
|
|
962
|
+
\`/install <name>\` \u2014 Install an agent
|
|
963
|
+
|
|
964
|
+
\u2699\uFE0F **System**
|
|
965
|
+
\`/restart\` \u2014 Restart OpenACP
|
|
966
|
+
\`/update\` \u2014 Update to latest version
|
|
967
|
+
\`/integrate\` \u2014 Manage agent integrations
|
|
968
|
+
\`/settings\` \u2014 View configuration
|
|
969
|
+
\`/menu\` \u2014 Show action menu
|
|
970
|
+
|
|
971
|
+
\u{1F512} **Session Options**
|
|
972
|
+
\`/dangerous\` \u2014 Toggle dangerous mode (auto-approve permissions)
|
|
973
|
+
\`/handoff\` \u2014 Continue session in your terminal
|
|
974
|
+
\`/clear\` \u2014 Clear assistant session history
|
|
975
|
+
|
|
976
|
+
\u{1FA7A} **Diagnostics**
|
|
977
|
+
\`/doctor\` \u2014 Run system diagnostics`,
|
|
978
|
+
ephemeral: true
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
async function handleClear(interaction, adapter) {
|
|
982
|
+
await interaction.deferReply({ ephemeral: true });
|
|
983
|
+
if (!adapter.assistant) {
|
|
984
|
+
await interaction.editReply("\u26A0\uFE0F Assistant is not available.");
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
try {
|
|
988
|
+
await adapter.assistant.respawn();
|
|
989
|
+
await interaction.editReply("\u2705 Assistant history cleared.");
|
|
990
|
+
} catch (err) {
|
|
991
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
992
|
+
await interaction.editReply(`\u274C Failed to clear: \`${message}\``);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
async function handleMenuButton(interaction, adapter) {
|
|
996
|
+
const { customId } = interaction;
|
|
997
|
+
try {
|
|
998
|
+
await interaction.deferUpdate();
|
|
999
|
+
} catch {
|
|
1000
|
+
}
|
|
1001
|
+
try {
|
|
1002
|
+
switch (customId) {
|
|
1003
|
+
case "m:new": {
|
|
1004
|
+
const { handleNew: handleNew2 } = await import("./new-session-DRRP2J7E.js");
|
|
1005
|
+
await interaction.followUp({ content: "Use `/new` to create a new session.", ephemeral: true });
|
|
1006
|
+
break;
|
|
1007
|
+
}
|
|
1008
|
+
case "m:sessions": {
|
|
1009
|
+
const { handleSessions: handleSessions2 } = await import("./session-FVFLBREJ.js");
|
|
1010
|
+
await showSessionsList(interaction, adapter);
|
|
1011
|
+
break;
|
|
1012
|
+
}
|
|
1013
|
+
case "m:status": {
|
|
1014
|
+
await showGlobalStatus(interaction, adapter);
|
|
1015
|
+
break;
|
|
1016
|
+
}
|
|
1017
|
+
case "m:agents": {
|
|
1018
|
+
const { showAgentsList } = await import("./agents-55NX3DHM.js");
|
|
1019
|
+
await showAgentsList(interaction, adapter);
|
|
1020
|
+
break;
|
|
1021
|
+
}
|
|
1022
|
+
case "m:settings": {
|
|
1023
|
+
const { showSettingsInfo } = await import("./settings-LPOLJ6SA.js");
|
|
1024
|
+
await showSettingsInfo(interaction, adapter);
|
|
1025
|
+
break;
|
|
1026
|
+
}
|
|
1027
|
+
case "m:integrate": {
|
|
1028
|
+
await interaction.followUp({ content: "Use `/integrate` to manage integrations.", ephemeral: true });
|
|
1029
|
+
break;
|
|
1030
|
+
}
|
|
1031
|
+
case "m:restart": {
|
|
1032
|
+
const { handleRestart: handleRestart2 } = await import("./admin-IKPS5PFC.js");
|
|
1033
|
+
if (!adapter.core.requestRestart) {
|
|
1034
|
+
await interaction.followUp({ content: "\u26A0\uFE0F Restart not available.", ephemeral: true });
|
|
1035
|
+
} else {
|
|
1036
|
+
await interaction.followUp({ content: "\u{1F504} Restarting OpenACP...", ephemeral: true });
|
|
1037
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1038
|
+
await adapter.core.requestRestart();
|
|
1039
|
+
}
|
|
1040
|
+
break;
|
|
1041
|
+
}
|
|
1042
|
+
case "m:update": {
|
|
1043
|
+
await interaction.followUp({ content: "\u26A0\uFE0F Update not implemented yet. Run `npm install -g @openacp/cli@latest` in your terminal.", ephemeral: true });
|
|
1044
|
+
break;
|
|
1045
|
+
}
|
|
1046
|
+
case "m:help": {
|
|
1047
|
+
await interaction.followUp({ content: "Use `/help` for command reference.", ephemeral: true });
|
|
1048
|
+
break;
|
|
1049
|
+
}
|
|
1050
|
+
case "m:doctor": {
|
|
1051
|
+
const { runDoctorInline } = await import("./doctor-CHCYUTV5.js");
|
|
1052
|
+
await runDoctorInline(interaction, adapter);
|
|
1053
|
+
break;
|
|
1054
|
+
}
|
|
1055
|
+
default:
|
|
1056
|
+
log.warn({ customId }, "[discord-menu] Unhandled menu button");
|
|
1057
|
+
}
|
|
1058
|
+
} catch (err) {
|
|
1059
|
+
log.error({ err, customId }, "[discord-menu] Menu button handler failed");
|
|
1060
|
+
try {
|
|
1061
|
+
await interaction.followUp({ content: `\u274C Action failed: ${err instanceof Error ? err.message : String(err)}`, ephemeral: true });
|
|
1062
|
+
} catch {
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
async function showGlobalStatus(interaction, adapter) {
|
|
1067
|
+
const sessions = adapter.core.sessionManager.listSessions("discord");
|
|
1068
|
+
const active = sessions.filter((s) => s.status === "active" || s.status === "initializing");
|
|
1069
|
+
await interaction.followUp({
|
|
1070
|
+
content: `**OpenACP Status**
|
|
1071
|
+
Active sessions: ${active.length}
|
|
1072
|
+
Total sessions: ${sessions.length}`,
|
|
1073
|
+
ephemeral: true
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
async function showSessionsList(interaction, adapter) {
|
|
1077
|
+
const allRecords = adapter.core.sessionManager.listRecords();
|
|
1078
|
+
if (allRecords.length === 0) {
|
|
1079
|
+
await interaction.followUp({ content: "No sessions found.", ephemeral: true });
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
const STATUS_EMOJI = {
|
|
1083
|
+
active: "\u{1F7E2}",
|
|
1084
|
+
initializing: "\u{1F7E1}",
|
|
1085
|
+
finished: "\u2705",
|
|
1086
|
+
error: "\u274C",
|
|
1087
|
+
cancelled: "\u26D4"
|
|
1088
|
+
};
|
|
1089
|
+
const STATUS_ORDER = {
|
|
1090
|
+
active: 0,
|
|
1091
|
+
initializing: 1,
|
|
1092
|
+
error: 2,
|
|
1093
|
+
finished: 3,
|
|
1094
|
+
cancelled: 4
|
|
1095
|
+
};
|
|
1096
|
+
allRecords.sort(
|
|
1097
|
+
(a, b) => (STATUS_ORDER[a.status] ?? 5) - (STATUS_ORDER[b.status] ?? 5)
|
|
1098
|
+
);
|
|
1099
|
+
const lines = allRecords.slice(0, 20).map((r) => {
|
|
1100
|
+
const emoji = STATUS_EMOJI[r.status] || "\u26AA";
|
|
1101
|
+
const name = r.name?.trim() || `${r.agentName} session`;
|
|
1102
|
+
return `${emoji} **${name}** \`[${r.status}]\``;
|
|
1103
|
+
});
|
|
1104
|
+
const truncated = allRecords.length > 20 ? `
|
|
1105
|
+
|
|
1106
|
+
*...and ${allRecords.length - 20} more*` : "";
|
|
1107
|
+
await interaction.followUp({
|
|
1108
|
+
content: `**Sessions: ${allRecords.length}**
|
|
1109
|
+
|
|
1110
|
+
${lines.join("\n")}${truncated}`,
|
|
1111
|
+
ephemeral: true
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// src/adapters/discord/commands/integrate.ts
|
|
1116
|
+
async function handleIntegrate(interaction, _adapter) {
|
|
1117
|
+
await interaction.deferReply({ ephemeral: true });
|
|
1118
|
+
await interaction.editReply(
|
|
1119
|
+
"\u{1F517} **Integrations**\n\nIntegration management via Discord is not yet implemented.\n\nUse the CLI: `openacp integrate`"
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
async function handleIntegrateButton(interaction, _adapter) {
|
|
1123
|
+
log.debug({ customId: interaction.customId }, "[discord-integrate] Button stub called");
|
|
1124
|
+
try {
|
|
1125
|
+
await interaction.reply({
|
|
1126
|
+
content: "\u{1F517} Integration management via Discord is not yet implemented. Use the CLI: `openacp integrate`",
|
|
1127
|
+
ephemeral: true
|
|
1128
|
+
});
|
|
1129
|
+
} catch {
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// src/adapters/discord/commands/router.ts
|
|
1134
|
+
async function handleSlashCommand(interaction, adapter) {
|
|
1135
|
+
const { commandName } = interaction;
|
|
1136
|
+
try {
|
|
1137
|
+
switch (commandName) {
|
|
1138
|
+
case "new":
|
|
1139
|
+
await handleNew(interaction, adapter);
|
|
1140
|
+
break;
|
|
1141
|
+
case "newchat":
|
|
1142
|
+
await handleNewChat(interaction, adapter);
|
|
1143
|
+
break;
|
|
1144
|
+
case "cancel":
|
|
1145
|
+
await handleCancel(interaction, adapter);
|
|
1146
|
+
break;
|
|
1147
|
+
case "status":
|
|
1148
|
+
await handleStatus(interaction, adapter);
|
|
1149
|
+
break;
|
|
1150
|
+
case "sessions":
|
|
1151
|
+
await handleSessions(interaction, adapter);
|
|
1152
|
+
break;
|
|
1153
|
+
case "agents":
|
|
1154
|
+
await handleAgents(interaction, adapter);
|
|
1155
|
+
break;
|
|
1156
|
+
case "install":
|
|
1157
|
+
await handleInstall(interaction, adapter);
|
|
1158
|
+
break;
|
|
1159
|
+
case "menu":
|
|
1160
|
+
await handleMenu(interaction, adapter);
|
|
1161
|
+
break;
|
|
1162
|
+
case "help":
|
|
1163
|
+
await handleHelp(interaction, adapter);
|
|
1164
|
+
break;
|
|
1165
|
+
case "dangerous":
|
|
1166
|
+
await handleDangerous(interaction, adapter);
|
|
1167
|
+
break;
|
|
1168
|
+
case "restart":
|
|
1169
|
+
await handleRestart(interaction, adapter);
|
|
1170
|
+
break;
|
|
1171
|
+
case "update":
|
|
1172
|
+
await handleUpdate(interaction, adapter);
|
|
1173
|
+
break;
|
|
1174
|
+
case "integrate":
|
|
1175
|
+
await handleIntegrate(interaction, adapter);
|
|
1176
|
+
break;
|
|
1177
|
+
case "settings":
|
|
1178
|
+
await handleSettings(interaction, adapter);
|
|
1179
|
+
break;
|
|
1180
|
+
case "doctor":
|
|
1181
|
+
await handleDoctor(interaction, adapter);
|
|
1182
|
+
break;
|
|
1183
|
+
case "handoff":
|
|
1184
|
+
await handleHandoff(interaction, adapter);
|
|
1185
|
+
break;
|
|
1186
|
+
case "clear":
|
|
1187
|
+
await handleClear(interaction, adapter);
|
|
1188
|
+
break;
|
|
1189
|
+
default:
|
|
1190
|
+
log.warn({ commandName }, "[discord-router] Unknown slash command");
|
|
1191
|
+
if (!interaction.replied && !interaction.deferred) {
|
|
1192
|
+
await interaction.reply({ content: `Unknown command: /${commandName}`, ephemeral: true });
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
} catch (err) {
|
|
1196
|
+
log.error({ err, commandName }, "[discord-router] Slash command handler failed");
|
|
1197
|
+
try {
|
|
1198
|
+
const errMsg = `\u274C Command failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
1199
|
+
if (interaction.deferred) {
|
|
1200
|
+
await interaction.editReply(errMsg);
|
|
1201
|
+
} else if (!interaction.replied) {
|
|
1202
|
+
await interaction.reply({ content: errMsg, ephemeral: true });
|
|
1203
|
+
}
|
|
1204
|
+
} catch {
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
async function setupButtonCallbacks(interaction, adapter) {
|
|
1209
|
+
const { customId } = interaction;
|
|
1210
|
+
try {
|
|
1211
|
+
if (customId.startsWith("a:dismiss:")) {
|
|
1212
|
+
try {
|
|
1213
|
+
await interaction.update({ components: [] });
|
|
1214
|
+
} catch {
|
|
1215
|
+
}
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
if (customId.startsWith("a:")) {
|
|
1219
|
+
const { getAction, removeAction } = await import("./action-detect-6M5GCGAU.js");
|
|
1220
|
+
const actionId = customId.slice(2);
|
|
1221
|
+
const action = getAction(actionId);
|
|
1222
|
+
if (!action) {
|
|
1223
|
+
await interaction.reply({ content: "\u274C Action expired.", ephemeral: true });
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
removeAction(actionId);
|
|
1227
|
+
if (action.type === "new_session") {
|
|
1228
|
+
await executeNewSession(interaction, adapter, action.agent, action.workspace);
|
|
1229
|
+
} else if (action.type === "cancel_session") {
|
|
1230
|
+
await executeCancelSession(interaction, adapter);
|
|
1231
|
+
}
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
if (customId.startsWith("d:")) {
|
|
1235
|
+
await handleDangerousButton(interaction, adapter);
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
if (customId.startsWith("m:new:")) {
|
|
1239
|
+
await handleNewSessionButton(interaction, adapter);
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
if (customId === "m:cleanup" || customId.startsWith("m:cleanup:")) {
|
|
1243
|
+
await handleCleanupButton(interaction, adapter);
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
if (customId === "m:doctor" || customId.startsWith("m:doctor:")) {
|
|
1247
|
+
await handleDoctorButton(interaction, adapter);
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
if (customId.startsWith("ag:")) {
|
|
1251
|
+
await handleAgentButton(interaction, adapter);
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
if (customId.startsWith("na:")) {
|
|
1255
|
+
const agentKey = customId.slice(3);
|
|
1256
|
+
try {
|
|
1257
|
+
await interaction.deferReply({ ephemeral: true });
|
|
1258
|
+
} catch {
|
|
1259
|
+
}
|
|
1260
|
+
await executeNewSession(interaction, adapter, agentKey, void 0);
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
if (customId.startsWith("s:")) {
|
|
1264
|
+
await handleSettingsButton(interaction, adapter);
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
if (customId.startsWith("i:")) {
|
|
1268
|
+
await handleIntegrateButton(interaction, adapter);
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
if (customId.startsWith("m:")) {
|
|
1272
|
+
await handleMenuButton(interaction, adapter);
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
log.warn({ customId }, "[discord-router] Unhandled button interaction");
|
|
1276
|
+
} catch (err) {
|
|
1277
|
+
log.error({ err, customId }, "[discord-router] Button callback failed");
|
|
1278
|
+
try {
|
|
1279
|
+
const errMsg = `\u274C Action failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
1280
|
+
if (!interaction.replied && !interaction.deferred) {
|
|
1281
|
+
await interaction.reply({ content: errMsg, ephemeral: true });
|
|
1282
|
+
} else {
|
|
1283
|
+
await interaction.followUp({ content: errMsg, ephemeral: true });
|
|
1284
|
+
}
|
|
1285
|
+
} catch {
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
async function executeNewSession(interaction, adapter, agentName, workspace) {
|
|
1290
|
+
const { executeNewSession: doExecute } = await import("./new-session-DRRP2J7E.js");
|
|
1291
|
+
await doExecute(interaction, adapter, agentName, workspace);
|
|
1292
|
+
}
|
|
1293
|
+
async function executeCancelSession(interaction, adapter) {
|
|
1294
|
+
const { executeCancelSession: doCancel } = await import("./session-FVFLBREJ.js");
|
|
1295
|
+
await doCancel(interaction, adapter);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// src/adapters/discord/commands/index.ts
|
|
1299
|
+
var SLASH_COMMANDS = [
|
|
1300
|
+
new SlashCommandBuilder().setName("new").setDescription("Create a new agent session").addStringOption((o) => o.setName("agent").setDescription("Agent to use").setRequired(false)).addStringOption((o) => o.setName("workspace").setDescription("Workspace directory").setRequired(false)),
|
|
1301
|
+
new SlashCommandBuilder().setName("newchat").setDescription("New chat in current thread, inheriting agent and workspace"),
|
|
1302
|
+
new SlashCommandBuilder().setName("cancel").setDescription("Cancel the current session"),
|
|
1303
|
+
new SlashCommandBuilder().setName("status").setDescription("Show session or global status"),
|
|
1304
|
+
new SlashCommandBuilder().setName("sessions").setDescription("List all sessions"),
|
|
1305
|
+
new SlashCommandBuilder().setName("agents").setDescription("List available agents"),
|
|
1306
|
+
new SlashCommandBuilder().setName("install").setDescription("Install an agent by name").addStringOption((o) => o.setName("name").setDescription("Agent name to install").setRequired(true)),
|
|
1307
|
+
new SlashCommandBuilder().setName("menu").setDescription("Show the action menu"),
|
|
1308
|
+
new SlashCommandBuilder().setName("help").setDescription("Show help"),
|
|
1309
|
+
new SlashCommandBuilder().setName("dangerous").setDescription("Toggle dangerous mode for the current session"),
|
|
1310
|
+
new SlashCommandBuilder().setName("restart").setDescription("Restart OpenACP"),
|
|
1311
|
+
new SlashCommandBuilder().setName("update").setDescription("Update to the latest version"),
|
|
1312
|
+
new SlashCommandBuilder().setName("integrate").setDescription("Manage agent integrations"),
|
|
1313
|
+
new SlashCommandBuilder().setName("settings").setDescription("Show configuration settings"),
|
|
1314
|
+
new SlashCommandBuilder().setName("doctor").setDescription("Run system diagnostics"),
|
|
1315
|
+
new SlashCommandBuilder().setName("handoff").setDescription("Generate a terminal resume command for this session"),
|
|
1316
|
+
new SlashCommandBuilder().setName("clear").setDescription("Reset the assistant session")
|
|
1317
|
+
];
|
|
1318
|
+
async function registerSlashCommands(guild) {
|
|
1319
|
+
await guild.commands.set(SLASH_COMMANDS.map((cmd) => cmd.toJSON()));
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// src/adapters/discord/assistant.ts
|
|
1323
|
+
async function spawnAssistant(core, threadId) {
|
|
1324
|
+
const config = core.configManager.get();
|
|
1325
|
+
log.info({ agent: config.defaultAgent, threadId }, "[discord-assistant] Creating assistant session...");
|
|
1326
|
+
const session = await core.createSession({
|
|
1327
|
+
channelId: "discord",
|
|
1328
|
+
agentName: config.defaultAgent,
|
|
1329
|
+
workingDirectory: core.configManager.resolveWorkspace(),
|
|
1330
|
+
initialName: "Assistant"
|
|
1331
|
+
// Prevent auto-naming from triggering after system prompt
|
|
1332
|
+
});
|
|
1333
|
+
session.threadId = threadId;
|
|
1334
|
+
log.info({ sessionId: session.id, threadId }, "[discord-assistant] Assistant agent spawned");
|
|
1335
|
+
const systemPrompt = buildAssistantSystemPrompt(core);
|
|
1336
|
+
const ready = session.enqueuePrompt(systemPrompt).then(() => {
|
|
1337
|
+
log.info({ sessionId: session.id }, "[discord-assistant] System prompt completed");
|
|
1338
|
+
}).catch((err) => {
|
|
1339
|
+
log.warn({ err }, "[discord-assistant] System prompt failed");
|
|
1340
|
+
});
|
|
1341
|
+
return { session, ready };
|
|
1342
|
+
}
|
|
1343
|
+
function buildWelcomeMessage(core) {
|
|
1344
|
+
const allRecords = core.sessionManager.listRecords();
|
|
1345
|
+
const activeCount = allRecords.filter(
|
|
1346
|
+
(r) => r.status === "active" || r.status === "initializing"
|
|
1347
|
+
).length;
|
|
1348
|
+
const errorCount = allRecords.filter((r) => r.status === "error").length;
|
|
1349
|
+
const totalCount = allRecords.length;
|
|
1350
|
+
const installedEntries = core.agentCatalog.getInstalledEntries();
|
|
1351
|
+
const agents = Object.keys(installedEntries);
|
|
1352
|
+
const config = core.configManager.get();
|
|
1353
|
+
const defaultAgent = config.defaultAgent;
|
|
1354
|
+
const agentList = agents.map((a) => `${a}${a === defaultAgent ? " (default)" : ""}`).join(", ");
|
|
1355
|
+
if (totalCount === 0) {
|
|
1356
|
+
return `\u{1F44B} **OpenACP is ready!**
|
|
1357
|
+
|
|
1358
|
+
No sessions yet. Use \`/new\` to start, or ask me anything!
|
|
1359
|
+
|
|
1360
|
+
Agents: ${agentList}`;
|
|
1361
|
+
}
|
|
1362
|
+
if (errorCount > 0) {
|
|
1363
|
+
return `\u{1F44B} **OpenACP is ready!**
|
|
1364
|
+
|
|
1365
|
+
\u{1F4CA} ${activeCount} active, ${errorCount} errors / ${totalCount} total
|
|
1366
|
+
\u26A0\uFE0F ${errorCount} session${errorCount > 1 ? "s have" : " has"} errors \u2014 ask me to check.
|
|
1367
|
+
|
|
1368
|
+
Agents: ${agentList}`;
|
|
1369
|
+
}
|
|
1370
|
+
return `\u{1F44B} **OpenACP is ready!**
|
|
1371
|
+
|
|
1372
|
+
\u{1F4CA} ${activeCount} active / ${totalCount} total
|
|
1373
|
+
Agents: ${agentList}`;
|
|
1374
|
+
}
|
|
1375
|
+
function buildAssistantSystemPrompt(core) {
|
|
1376
|
+
const config = core.configManager.get();
|
|
1377
|
+
const allRecords = core.sessionManager.listRecords();
|
|
1378
|
+
const activeCount = allRecords.filter(
|
|
1379
|
+
(r) => r.status === "active" || r.status === "initializing"
|
|
1380
|
+
).length;
|
|
1381
|
+
const statusCounts = /* @__PURE__ */ new Map();
|
|
1382
|
+
for (const r of allRecords) {
|
|
1383
|
+
statusCounts.set(r.status, (statusCounts.get(r.status) ?? 0) + 1);
|
|
1384
|
+
}
|
|
1385
|
+
const topicBreakdown = Array.from(statusCounts.entries()).map(([status, count]) => `${status}: ${count}`).join(", ") || "none";
|
|
1386
|
+
const installedEntries = core.agentCatalog.getInstalledEntries();
|
|
1387
|
+
const installedAgents = Object.keys(installedEntries);
|
|
1388
|
+
const agentNames = installedAgents.length ? installedAgents.join(", ") : Object.keys(config.agents).join(", ");
|
|
1389
|
+
const availableItems = core.agentCatalog.getAvailable();
|
|
1390
|
+
const availableAgentCount = availableItems.filter((i) => !i.installed).length;
|
|
1391
|
+
return `You are the OpenACP Assistant \u2014 a helpful guide for managing AI coding sessions on Discord.
|
|
1392
|
+
|
|
1393
|
+
## Current State
|
|
1394
|
+
- Active sessions: ${activeCount} / ${allRecords.length} total
|
|
1395
|
+
- Sessions by status: ${topicBreakdown}
|
|
1396
|
+
- Installed agents: ${agentNames}
|
|
1397
|
+
- Available in ACP Registry: ${availableAgentCount} more agents (use \`/agents\` to browse)
|
|
1398
|
+
- Default agent: ${config.defaultAgent}
|
|
1399
|
+
- Workspace base directory: ${config.workspace.baseDir}
|
|
1400
|
+
- Platform: Discord
|
|
1401
|
+
|
|
1402
|
+
## Discord Context
|
|
1403
|
+
- Each session gets its own forum thread in the OpenACP sessions channel
|
|
1404
|
+
- Users interact with sessions by chatting in those threads
|
|
1405
|
+
- Slash commands: /new, /cancel, /status, /sessions, /agents, /install, /menu, /help, /dangerous, /restart, /update, /integrate, /settings, /doctor, /handoff, /clear
|
|
1406
|
+
|
|
1407
|
+
## Action Playbook
|
|
1408
|
+
|
|
1409
|
+
### Create Session
|
|
1410
|
+
- The workspace is the project directory where the agent will work (read, write, execute code). It should be a specific project folder like \`~/code/my-project\` or \`${config.workspace.baseDir}/my-app\`.
|
|
1411
|
+
- Ask which agent to use (if multiple are installed). Installed: ${agentNames}
|
|
1412
|
+
- Ask which project directory to use as workspace. Suggest \`${config.workspace.baseDir}\` as the base.
|
|
1413
|
+
- Create via: \`openacp api new <agent> <workspace>\`
|
|
1414
|
+
|
|
1415
|
+
### Browse & Install Agents
|
|
1416
|
+
- Guide users to \`/agents\` command to see all available agents
|
|
1417
|
+
- For CLI users: \`openacp agents install <name>\`
|
|
1418
|
+
- Some agents need login/setup after install \u2014 guide users to \`openacp agents info <name>\`
|
|
1419
|
+
- To run agent CLI for login: \`openacp agents run <name> -- <args>\`
|
|
1420
|
+
|
|
1421
|
+
### Check Status / List Sessions
|
|
1422
|
+
- Run \`openacp api status\` for active sessions overview
|
|
1423
|
+
- Run \`openacp api topics\` for full list with statuses
|
|
1424
|
+
|
|
1425
|
+
### Cancel Session
|
|
1426
|
+
- Run \`openacp api status\` to see what's active
|
|
1427
|
+
- If 1 active session \u2192 ask user to confirm \u2192 \`openacp api cancel <id>\`
|
|
1428
|
+
- If multiple \u2192 list them, ask user which one to cancel
|
|
1429
|
+
|
|
1430
|
+
### Troubleshoot
|
|
1431
|
+
- Run \`openacp api health\` + \`openacp api status\` to diagnose
|
|
1432
|
+
- Small issue (stuck session) \u2192 suggest cancel + create new
|
|
1433
|
+
- Big issue (system-level) \u2192 suggest restart, ask for confirmation first
|
|
1434
|
+
|
|
1435
|
+
### Cleanup Old Sessions
|
|
1436
|
+
- Run \`openacp api topics --status finished,error\` to see what can be cleaned
|
|
1437
|
+
- Report the count, ask user to confirm
|
|
1438
|
+
- Execute: \`openacp api cleanup --status <statuses>\`
|
|
1439
|
+
|
|
1440
|
+
### Configuration
|
|
1441
|
+
- View: \`openacp config\`
|
|
1442
|
+
- Update: \`openacp config set <key> <value>\`
|
|
1443
|
+
|
|
1444
|
+
### Restart / Update
|
|
1445
|
+
- Always ask for confirmation \u2014 these are disruptive actions
|
|
1446
|
+
|
|
1447
|
+
### Toggle Dangerous Mode
|
|
1448
|
+
- Run \`openacp api dangerous <id> on|off\`
|
|
1449
|
+
- Explain: dangerous mode auto-approves all permission requests
|
|
1450
|
+
|
|
1451
|
+
## CLI Commands Reference
|
|
1452
|
+
\`\`\`bash
|
|
1453
|
+
# Session management
|
|
1454
|
+
openacp api status # List active sessions
|
|
1455
|
+
openacp api session <id> # Session detail
|
|
1456
|
+
openacp api new <agent> <workspace> # Create new session
|
|
1457
|
+
openacp api send <id> "prompt text" # Send prompt to session
|
|
1458
|
+
openacp api cancel <id> # Cancel session
|
|
1459
|
+
openacp api dangerous <id> on|off # Toggle dangerous mode
|
|
1460
|
+
|
|
1461
|
+
# Topic management
|
|
1462
|
+
openacp api topics # List all topics
|
|
1463
|
+
openacp api cleanup # Cleanup finished topics
|
|
1464
|
+
|
|
1465
|
+
# Agent management
|
|
1466
|
+
openacp agents # List installed + available agents
|
|
1467
|
+
openacp agents install <name> # Install agent from ACP Registry
|
|
1468
|
+
openacp agents uninstall <name> # Remove agent
|
|
1469
|
+
openacp agents info <name> # Show details & setup guide
|
|
1470
|
+
openacp agents run <name> -- <args> # Run agent CLI (for login, etc.)
|
|
1471
|
+
|
|
1472
|
+
# System
|
|
1473
|
+
openacp api health # System health
|
|
1474
|
+
openacp config # Edit config (interactive)
|
|
1475
|
+
openacp config set <key> <value> # Update config value
|
|
1476
|
+
openacp api restart # Restart daemon
|
|
1477
|
+
\`\`\`
|
|
1478
|
+
|
|
1479
|
+
## Guidelines
|
|
1480
|
+
- NEVER show \`openacp api ...\` commands to users. These are internal tools for YOU to run silently.
|
|
1481
|
+
- Run \`openacp api ...\` commands yourself for everything you can. Only guide users to Discord slash commands/buttons when needed.
|
|
1482
|
+
- When creating sessions: guide user through agent + workspace choice conversationally, then run the command yourself.
|
|
1483
|
+
- Destructive actions (cancel active session, restart, cleanup) \u2192 always ask user to confirm first.
|
|
1484
|
+
- Respond in the same language the user uses.
|
|
1485
|
+
- Format responses for Discord: use **bold**, \`code\`, keep it concise.
|
|
1486
|
+
- When you don't know something, check with the relevant \`openacp api\` command first before answering.
|
|
1487
|
+
|
|
1488
|
+
## Product Reference
|
|
1489
|
+
${PRODUCT_GUIDE}`;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
// src/adapters/discord/media.ts
|
|
1493
|
+
var MAX_DOWNLOAD_SIZE = 100 * 1024 * 1024;
|
|
1494
|
+
var DISCORD_UPLOAD_LIMIT = 25 * 1024 * 1024;
|
|
1495
|
+
function isAttachmentTooLarge(size) {
|
|
1496
|
+
return size > DISCORD_UPLOAD_LIMIT;
|
|
1497
|
+
}
|
|
1498
|
+
function buildFallbackText(attachments) {
|
|
1499
|
+
return attachments.map((att) => {
|
|
1500
|
+
const label = att.type === "image" ? "Photo" : att.type === "audio" ? "Audio" : "File";
|
|
1501
|
+
return `[${label}: ${att.fileName}]`;
|
|
1502
|
+
}).join(" ");
|
|
1503
|
+
}
|
|
1504
|
+
async function downloadDiscordAttachment(url, fileName) {
|
|
1505
|
+
try {
|
|
1506
|
+
const response = await fetch(url);
|
|
1507
|
+
if (!response.ok) {
|
|
1508
|
+
log.warn({ url, status: response.status, fileName }, "[discord-media] Download failed");
|
|
1509
|
+
return null;
|
|
1510
|
+
}
|
|
1511
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
1512
|
+
if (buffer.length > MAX_DOWNLOAD_SIZE) {
|
|
1513
|
+
log.warn({ fileName, size: buffer.length }, "[discord-media] File exceeds download size cap");
|
|
1514
|
+
return null;
|
|
1515
|
+
}
|
|
1516
|
+
return buffer;
|
|
1517
|
+
} catch (err) {
|
|
1518
|
+
log.error({ err, url, fileName }, "[discord-media] Download error");
|
|
1519
|
+
return null;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// src/adapters/discord/adapter.ts
|
|
1524
|
+
var DiscordAdapter = class extends ChannelAdapter {
|
|
1525
|
+
client;
|
|
1526
|
+
discordConfig;
|
|
1527
|
+
sendQueue;
|
|
1528
|
+
toolTracker;
|
|
1529
|
+
draftManager;
|
|
1530
|
+
skillManager;
|
|
1531
|
+
permissionHandler;
|
|
1532
|
+
sessionTrackers = /* @__PURE__ */ new Map();
|
|
1533
|
+
guild;
|
|
1534
|
+
forumChannel;
|
|
1535
|
+
notificationChannel;
|
|
1536
|
+
assistantSession = null;
|
|
1537
|
+
assistantInitializing = false;
|
|
1538
|
+
fileService;
|
|
1539
|
+
constructor(core, config) {
|
|
1540
|
+
super(core, config);
|
|
1541
|
+
this.discordConfig = config;
|
|
1542
|
+
this.client = new Client({
|
|
1543
|
+
intents: [
|
|
1544
|
+
GatewayIntentBits.Guilds,
|
|
1545
|
+
GatewayIntentBits.GuildMessages,
|
|
1546
|
+
GatewayIntentBits.MessageContent
|
|
1547
|
+
]
|
|
1548
|
+
});
|
|
1549
|
+
this.sendQueue = new DiscordSendQueue();
|
|
1550
|
+
this.toolTracker = new ToolCallTracker(this.sendQueue);
|
|
1551
|
+
this.draftManager = new DraftManager(this.sendQueue);
|
|
1552
|
+
this.fileService = core.fileService;
|
|
1553
|
+
this.client.rest.on("rateLimited", (info) => {
|
|
1554
|
+
log.warn({ route: info.route, timeToReset: info.timeToReset }, "[DiscordAdapter] Rate limited");
|
|
1555
|
+
this.sendQueue.onRateLimited();
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
// ─── start ────────────────────────────────────────────────────────────────
|
|
1559
|
+
start() {
|
|
1560
|
+
return new Promise((resolve, reject) => {
|
|
1561
|
+
this.client.once("ready", async () => {
|
|
1562
|
+
try {
|
|
1563
|
+
log.info({ guildId: this.discordConfig.guildId }, "[DiscordAdapter] Client ready, initializing...");
|
|
1564
|
+
const guild = this.client.guilds.cache.get(this.discordConfig.guildId) ?? await this.client.guilds.fetch(this.discordConfig.guildId).catch(() => null);
|
|
1565
|
+
if (!guild) {
|
|
1566
|
+
throw new Error(`Guild not found: ${this.discordConfig.guildId}`);
|
|
1567
|
+
}
|
|
1568
|
+
this.guild = guild;
|
|
1569
|
+
const saveConfig = (updates) => this.core.configManager.save(updates);
|
|
1570
|
+
const { forumChannel, notificationChannel } = await ensureForums(
|
|
1571
|
+
guild,
|
|
1572
|
+
{
|
|
1573
|
+
forumChannelId: this.discordConfig.forumChannelId,
|
|
1574
|
+
notificationChannelId: this.discordConfig.notificationChannelId
|
|
1575
|
+
},
|
|
1576
|
+
saveConfig
|
|
1577
|
+
);
|
|
1578
|
+
this.forumChannel = forumChannel;
|
|
1579
|
+
this.notificationChannel = notificationChannel;
|
|
1580
|
+
this.skillManager = new SkillCommandManager(this.sendQueue, this.core.sessionManager);
|
|
1581
|
+
this.permissionHandler = new PermissionHandler(
|
|
1582
|
+
guild.id,
|
|
1583
|
+
(sessionId) => this.core.sessionManager.getSession(sessionId),
|
|
1584
|
+
(notification) => this.sendNotification(notification)
|
|
1585
|
+
);
|
|
1586
|
+
await registerSlashCommands(guild);
|
|
1587
|
+
this.setupInteractionHandler();
|
|
1588
|
+
this.setupMessageHandler();
|
|
1589
|
+
const welcomeMsg = buildWelcomeMessage(this.core);
|
|
1590
|
+
try {
|
|
1591
|
+
await this.notificationChannel.send(welcomeMsg);
|
|
1592
|
+
} catch (err) {
|
|
1593
|
+
log.warn({ err }, "[DiscordAdapter] Failed to send welcome message");
|
|
1594
|
+
}
|
|
1595
|
+
await this.setupAssistant();
|
|
1596
|
+
log.info("[DiscordAdapter] Initialization complete");
|
|
1597
|
+
resolve();
|
|
1598
|
+
} catch (err) {
|
|
1599
|
+
log.error({ err }, "[DiscordAdapter] Initialization failed");
|
|
1600
|
+
reject(err);
|
|
1601
|
+
}
|
|
1602
|
+
});
|
|
1603
|
+
this.client.login(this.discordConfig.botToken).catch(reject);
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
// ─── stop ─────────────────────────────────────────────────────────────────
|
|
1607
|
+
async stop() {
|
|
1608
|
+
if (this.assistantSession) {
|
|
1609
|
+
try {
|
|
1610
|
+
await this.assistantSession.destroy();
|
|
1611
|
+
} catch (err) {
|
|
1612
|
+
log.warn({ err }, "[DiscordAdapter] Failed to destroy assistant session");
|
|
1613
|
+
}
|
|
1614
|
+
this.assistantSession = null;
|
|
1615
|
+
}
|
|
1616
|
+
this.client.destroy();
|
|
1617
|
+
log.info("[DiscordAdapter] Stopped");
|
|
1618
|
+
}
|
|
1619
|
+
// ─── Interaction handler ──────────────────────────────────────────────────
|
|
1620
|
+
setupInteractionHandler() {
|
|
1621
|
+
this.client.on("interactionCreate", async (interaction) => {
|
|
1622
|
+
try {
|
|
1623
|
+
if (interaction.isChatInputCommand()) {
|
|
1624
|
+
await handleSlashCommand(interaction, this);
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
if (interaction.isButton()) {
|
|
1628
|
+
const handled = await this.permissionHandler.handleButtonInteraction(interaction);
|
|
1629
|
+
if (!handled) {
|
|
1630
|
+
await setupButtonCallbacks(interaction, this);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
} catch (err) {
|
|
1634
|
+
log.error({ err }, "[DiscordAdapter] interactionCreate handler error");
|
|
1635
|
+
}
|
|
1636
|
+
});
|
|
1637
|
+
}
|
|
1638
|
+
// ─── Message handler ──────────────────────────────────────────────────────
|
|
1639
|
+
setupMessageHandler() {
|
|
1640
|
+
this.client.on("messageCreate", async (message) => {
|
|
1641
|
+
try {
|
|
1642
|
+
if (message.author.bot) return;
|
|
1643
|
+
if (!message.guild) return;
|
|
1644
|
+
if (message.guild.id !== this.guild.id) return;
|
|
1645
|
+
if (!message.channel.isThread()) return;
|
|
1646
|
+
const threadId = message.channel.id;
|
|
1647
|
+
const userId = message.author.id;
|
|
1648
|
+
let text = message.content;
|
|
1649
|
+
log.debug(
|
|
1650
|
+
{ threadId, userId, text: text.slice(0, 50), attachmentCount: message.attachments.size },
|
|
1651
|
+
"[DiscordAdapter] messageCreate received"
|
|
1652
|
+
);
|
|
1653
|
+
if (!text && message.attachments.size === 0) return;
|
|
1654
|
+
const sessionId = this.core.sessionManager.getSessionByThread("discord", threadId)?.id ?? "unknown";
|
|
1655
|
+
if (message.attachments.size > 0) {
|
|
1656
|
+
log.info(
|
|
1657
|
+
{
|
|
1658
|
+
sessionId,
|
|
1659
|
+
attachments: message.attachments.map((a) => ({
|
|
1660
|
+
name: a.name,
|
|
1661
|
+
size: a.size,
|
|
1662
|
+
contentType: a.contentType,
|
|
1663
|
+
url: a.url?.slice(0, 80)
|
|
1664
|
+
}))
|
|
1665
|
+
},
|
|
1666
|
+
"[discord-media] Processing incoming attachments"
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
const attachments = await this.processIncomingAttachments(message, sessionId);
|
|
1670
|
+
if (!text && attachments.length > 0) {
|
|
1671
|
+
text = buildFallbackText(attachments);
|
|
1672
|
+
}
|
|
1673
|
+
if (!text && attachments.length === 0 && message.attachments.size > 0) {
|
|
1674
|
+
try {
|
|
1675
|
+
await message.reply("Failed to process attachment(s)");
|
|
1676
|
+
} catch {
|
|
1677
|
+
}
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
if (this.discordConfig.assistantThreadId && threadId === this.discordConfig.assistantThreadId) {
|
|
1681
|
+
if (this.assistantSession && text) {
|
|
1682
|
+
await this.assistantSession.enqueuePrompt(text, attachments.length > 0 ? attachments : void 0);
|
|
1683
|
+
}
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
await this.core.handleMessage({
|
|
1687
|
+
channelId: "discord",
|
|
1688
|
+
threadId,
|
|
1689
|
+
userId,
|
|
1690
|
+
text,
|
|
1691
|
+
...attachments.length > 0 ? { attachments } : {}
|
|
1692
|
+
});
|
|
1693
|
+
} catch (err) {
|
|
1694
|
+
log.error({ err }, "[DiscordAdapter] messageCreate handler error");
|
|
1695
|
+
}
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
// ─── Assistant ────────────────────────────────────────────────────────────
|
|
1699
|
+
async setupAssistant() {
|
|
1700
|
+
let threadId = this.discordConfig.assistantThreadId;
|
|
1701
|
+
if (threadId) {
|
|
1702
|
+
try {
|
|
1703
|
+
const existing = this.guild.channels.cache.get(threadId) ?? await this.guild.channels.fetch(threadId);
|
|
1704
|
+
if (existing && existing.isThread()) {
|
|
1705
|
+
await ensureUnarchived(existing);
|
|
1706
|
+
log.info({ threadId }, "[DiscordAdapter] Reusing existing assistant thread");
|
|
1707
|
+
} else {
|
|
1708
|
+
log.warn({ threadId }, "[DiscordAdapter] Assistant thread not found, recreating...");
|
|
1709
|
+
threadId = null;
|
|
1710
|
+
}
|
|
1711
|
+
} catch {
|
|
1712
|
+
log.warn({ threadId }, "[DiscordAdapter] Assistant thread inaccessible, recreating...");
|
|
1713
|
+
threadId = null;
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
if (!threadId) {
|
|
1717
|
+
const thread = await createSessionThread(this.forumChannel, "Assistant");
|
|
1718
|
+
threadId = thread.id;
|
|
1719
|
+
await this.core.configManager.save({
|
|
1720
|
+
channels: { discord: { assistantThreadId: thread.id } }
|
|
1721
|
+
});
|
|
1722
|
+
log.info({ threadId }, "[DiscordAdapter] Created assistant thread");
|
|
1723
|
+
}
|
|
1724
|
+
this.assistantInitializing = true;
|
|
1725
|
+
try {
|
|
1726
|
+
const { session, ready } = await spawnAssistant(this.core, threadId);
|
|
1727
|
+
this.assistantSession = session;
|
|
1728
|
+
ready.finally(() => {
|
|
1729
|
+
this.assistantInitializing = false;
|
|
1730
|
+
});
|
|
1731
|
+
} catch (err) {
|
|
1732
|
+
this.assistantInitializing = false;
|
|
1733
|
+
log.error({ err }, "[DiscordAdapter] Failed to spawn assistant");
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
async respawnAssistant() {
|
|
1737
|
+
if (this.assistantSession) {
|
|
1738
|
+
try {
|
|
1739
|
+
await this.assistantSession.destroy();
|
|
1740
|
+
} catch {
|
|
1741
|
+
}
|
|
1742
|
+
this.assistantSession = null;
|
|
1743
|
+
}
|
|
1744
|
+
await this.setupAssistant();
|
|
1745
|
+
}
|
|
1746
|
+
// ─── Incoming media ──────────────────────────────────────────────────
|
|
1747
|
+
async processIncomingAttachments(message, sessionId) {
|
|
1748
|
+
if (message.attachments.size === 0) return [];
|
|
1749
|
+
const isVoiceMessage = message.flags.has(MessageFlags.IsVoiceMessage);
|
|
1750
|
+
const results = await Promise.allSettled(
|
|
1751
|
+
message.attachments.map(async (discordAtt) => {
|
|
1752
|
+
const buffer = await downloadDiscordAttachment(
|
|
1753
|
+
discordAtt.url,
|
|
1754
|
+
discordAtt.name ?? "attachment"
|
|
1755
|
+
);
|
|
1756
|
+
if (!buffer) return null;
|
|
1757
|
+
let data = buffer;
|
|
1758
|
+
let fileName = discordAtt.name ?? "attachment";
|
|
1759
|
+
let mimeType = discordAtt.contentType ?? "application/octet-stream";
|
|
1760
|
+
if (isVoiceMessage && mimeType.includes("ogg")) {
|
|
1761
|
+
try {
|
|
1762
|
+
data = await this.fileService.convertOggToWav(buffer);
|
|
1763
|
+
fileName = "voice.wav";
|
|
1764
|
+
mimeType = "audio/wav";
|
|
1765
|
+
} catch (err) {
|
|
1766
|
+
log.warn({ err }, "[discord-media] OGG\u2192WAV conversion failed, saving original");
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
return this.fileService.saveFile(sessionId, fileName, data, mimeType);
|
|
1770
|
+
})
|
|
1771
|
+
);
|
|
1772
|
+
const rejected = results.filter((r) => r.status === "rejected");
|
|
1773
|
+
if (rejected.length > 0) {
|
|
1774
|
+
log.warn({ rejected: rejected.map((r) => r.reason) }, "[discord-media] Some attachments failed");
|
|
1775
|
+
}
|
|
1776
|
+
const saved = results.filter((r) => r.status === "fulfilled").map((r) => r.value).filter((att) => att !== null);
|
|
1777
|
+
log.info({ count: saved.length, files: saved.map((a) => a.fileName) }, "[discord-media] Attachments processed");
|
|
1778
|
+
return saved;
|
|
1779
|
+
}
|
|
1780
|
+
// ─── Helper: resolve thread ───────────────────────────────────────────────
|
|
1781
|
+
async getThread(sessionId) {
|
|
1782
|
+
const session = this.core.sessionManager.getSession(sessionId);
|
|
1783
|
+
const threadId = session?.threadId;
|
|
1784
|
+
if (!threadId) {
|
|
1785
|
+
log.warn({ sessionId }, "[DiscordAdapter] No threadId for session");
|
|
1786
|
+
return null;
|
|
1787
|
+
}
|
|
1788
|
+
try {
|
|
1789
|
+
const channel = this.guild.channels.cache.get(threadId) ?? await this.guild.channels.fetch(threadId);
|
|
1790
|
+
if (channel && channel.isThread()) return channel;
|
|
1791
|
+
log.warn({ sessionId, threadId }, "[DiscordAdapter] Channel is not a thread");
|
|
1792
|
+
return null;
|
|
1793
|
+
} catch (err) {
|
|
1794
|
+
log.warn({ err, sessionId, threadId }, "[DiscordAdapter] Failed to fetch thread");
|
|
1795
|
+
return null;
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
// ─── sendMessage ──────────────────────────────────────────────────────────
|
|
1799
|
+
async sendMessage(sessionId, content) {
|
|
1800
|
+
if (this.assistantInitializing && this.assistantSession && sessionId === this.assistantSession.id) {
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
const thread = await this.getThread(sessionId);
|
|
1804
|
+
if (!thread) return;
|
|
1805
|
+
await ensureUnarchived(thread);
|
|
1806
|
+
const isAssistant = this.assistantSession != null && sessionId === this.assistantSession.id;
|
|
1807
|
+
if (!this.sessionTrackers.has(sessionId)) {
|
|
1808
|
+
this.sessionTrackers.set(sessionId, new ActivityTracker(thread, this.sendQueue));
|
|
1809
|
+
}
|
|
1810
|
+
const tracker = this.sessionTrackers.get(sessionId);
|
|
1811
|
+
switch (content.type) {
|
|
1812
|
+
case "thought": {
|
|
1813
|
+
await tracker.onThought();
|
|
1814
|
+
break;
|
|
1815
|
+
}
|
|
1816
|
+
case "text": {
|
|
1817
|
+
await tracker.onTextStart();
|
|
1818
|
+
const draft = this.draftManager.getOrCreate(sessionId, thread);
|
|
1819
|
+
draft.append(content.text);
|
|
1820
|
+
this.draftManager.appendText(sessionId, content.text);
|
|
1821
|
+
break;
|
|
1822
|
+
}
|
|
1823
|
+
case "tool_call": {
|
|
1824
|
+
await tracker.onToolCall();
|
|
1825
|
+
await this.draftManager.finalize(sessionId, thread, isAssistant);
|
|
1826
|
+
const meta = content.metadata ?? {};
|
|
1827
|
+
await this.toolTracker.trackNewCall(sessionId, thread, {
|
|
1828
|
+
id: String(meta.id ?? ""),
|
|
1829
|
+
name: content.text || String(meta.name ?? "Tool"),
|
|
1830
|
+
kind: meta.kind,
|
|
1831
|
+
status: String(meta.status ?? "running"),
|
|
1832
|
+
content: meta.content,
|
|
1833
|
+
viewerLinks: meta.viewerLinks,
|
|
1834
|
+
viewerFilePath: meta.viewerFilePath
|
|
1835
|
+
});
|
|
1836
|
+
break;
|
|
1837
|
+
}
|
|
1838
|
+
case "tool_update": {
|
|
1839
|
+
const meta = content.metadata ?? {};
|
|
1840
|
+
await this.toolTracker.updateCall(sessionId, {
|
|
1841
|
+
id: String(meta.id ?? ""),
|
|
1842
|
+
name: content.text || String(meta.name ?? ""),
|
|
1843
|
+
kind: meta.kind,
|
|
1844
|
+
status: String(meta.status ?? "completed"),
|
|
1845
|
+
content: meta.content,
|
|
1846
|
+
viewerLinks: meta.viewerLinks,
|
|
1847
|
+
viewerFilePath: meta.viewerFilePath
|
|
1848
|
+
});
|
|
1849
|
+
break;
|
|
1850
|
+
}
|
|
1851
|
+
case "plan": {
|
|
1852
|
+
const entries = content.metadata?.entries ?? [];
|
|
1853
|
+
await tracker.onPlan(entries);
|
|
1854
|
+
break;
|
|
1855
|
+
}
|
|
1856
|
+
case "usage": {
|
|
1857
|
+
await this.draftManager.finalize(sessionId, thread, isAssistant);
|
|
1858
|
+
const meta = content.metadata ?? {};
|
|
1859
|
+
await tracker.sendUsage({
|
|
1860
|
+
tokensUsed: meta.tokensUsed,
|
|
1861
|
+
contextSize: meta.contextSize
|
|
1862
|
+
});
|
|
1863
|
+
try {
|
|
1864
|
+
const deepLink = buildDeepLink(this.guild.id, thread.id);
|
|
1865
|
+
await this.sendNotification({
|
|
1866
|
+
sessionId,
|
|
1867
|
+
type: "completed",
|
|
1868
|
+
summary: content.text || "Session completed",
|
|
1869
|
+
deepLink
|
|
1870
|
+
});
|
|
1871
|
+
} catch {
|
|
1872
|
+
}
|
|
1873
|
+
break;
|
|
1874
|
+
}
|
|
1875
|
+
case "session_end": {
|
|
1876
|
+
await this.draftManager.finalize(sessionId, thread, isAssistant);
|
|
1877
|
+
await tracker.cleanup();
|
|
1878
|
+
this.toolTracker.cleanup(sessionId);
|
|
1879
|
+
this.sessionTrackers.delete(sessionId);
|
|
1880
|
+
await this.skillManager.cleanup(sessionId);
|
|
1881
|
+
try {
|
|
1882
|
+
await this.sendQueue.enqueue(
|
|
1883
|
+
() => thread.send({ content: "\u2705 Done" }),
|
|
1884
|
+
{ type: "other" }
|
|
1885
|
+
);
|
|
1886
|
+
} catch {
|
|
1887
|
+
}
|
|
1888
|
+
break;
|
|
1889
|
+
}
|
|
1890
|
+
case "error": {
|
|
1891
|
+
await this.draftManager.finalize(sessionId, thread, isAssistant);
|
|
1892
|
+
await tracker.cleanup();
|
|
1893
|
+
this.toolTracker.cleanup(sessionId);
|
|
1894
|
+
this.sessionTrackers.delete(sessionId);
|
|
1895
|
+
try {
|
|
1896
|
+
await this.sendQueue.enqueue(
|
|
1897
|
+
() => thread.send({ content: `\u274C Error: ${content.text}` }),
|
|
1898
|
+
{ type: "other" }
|
|
1899
|
+
);
|
|
1900
|
+
} catch {
|
|
1901
|
+
}
|
|
1902
|
+
break;
|
|
1903
|
+
}
|
|
1904
|
+
case "attachment": {
|
|
1905
|
+
if (!content.attachment) break;
|
|
1906
|
+
const { attachment } = content;
|
|
1907
|
+
await this.draftManager.finalize(sessionId, thread, isAssistant);
|
|
1908
|
+
if (isAttachmentTooLarge(attachment.size)) {
|
|
1909
|
+
log.warn({ sessionId, fileName: attachment.fileName, size: attachment.size }, "[discord-media] File too large (>25MB)");
|
|
1910
|
+
try {
|
|
1911
|
+
await this.sendQueue.enqueue(
|
|
1912
|
+
() => thread.send({ content: `\u26A0\uFE0F File too large to send (${Math.round(attachment.size / 1024 / 1024)}MB): ${attachment.fileName}` }),
|
|
1913
|
+
{ type: "other" }
|
|
1914
|
+
);
|
|
1915
|
+
} catch {
|
|
1916
|
+
}
|
|
1917
|
+
break;
|
|
1918
|
+
}
|
|
1919
|
+
try {
|
|
1920
|
+
await this.sendQueue.enqueue(
|
|
1921
|
+
() => thread.send({ files: [{ attachment: attachment.filePath, name: attachment.fileName }] }),
|
|
1922
|
+
{ type: "other" }
|
|
1923
|
+
);
|
|
1924
|
+
} catch (err) {
|
|
1925
|
+
log.error({ err, sessionId, fileName: attachment.fileName }, "[discord-media] Failed to send attachment");
|
|
1926
|
+
}
|
|
1927
|
+
break;
|
|
1928
|
+
}
|
|
1929
|
+
case "system_message": {
|
|
1930
|
+
try {
|
|
1931
|
+
await this.sendQueue.enqueue(
|
|
1932
|
+
() => thread.send({ content: content.text }),
|
|
1933
|
+
{ type: "other" }
|
|
1934
|
+
);
|
|
1935
|
+
} catch {
|
|
1936
|
+
}
|
|
1937
|
+
break;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
// ─── sendPermissionRequest ────────────────────────────────────────────────
|
|
1942
|
+
async sendPermissionRequest(sessionId, request) {
|
|
1943
|
+
const session = this.core.sessionManager.getSession(sessionId);
|
|
1944
|
+
if (!session) {
|
|
1945
|
+
log.warn({ sessionId }, "[DiscordAdapter] sendPermissionRequest: session not found");
|
|
1946
|
+
return;
|
|
1947
|
+
}
|
|
1948
|
+
const autoApprove = request.description.toLowerCase().includes("openacp") || session.dangerousMode;
|
|
1949
|
+
if (autoApprove) {
|
|
1950
|
+
const allowOption = request.options.find((o) => o.isAllow);
|
|
1951
|
+
if (allowOption && session.permissionGate.requestId === request.id) {
|
|
1952
|
+
session.permissionGate.resolve(allowOption.id);
|
|
1953
|
+
}
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1956
|
+
const thread = await this.getThread(sessionId);
|
|
1957
|
+
if (!thread) return;
|
|
1958
|
+
await this.permissionHandler.sendPermissionRequest(session, request, thread);
|
|
1959
|
+
}
|
|
1960
|
+
// ─── sendNotification ─────────────────────────────────────────────────────
|
|
1961
|
+
async sendNotification(notification) {
|
|
1962
|
+
if (!this.notificationChannel) return;
|
|
1963
|
+
const typeIcon = {
|
|
1964
|
+
completed: "\u2705",
|
|
1965
|
+
error: "\u274C",
|
|
1966
|
+
permission: "\u{1F510}",
|
|
1967
|
+
input_required: "\u{1F4AC}"
|
|
1968
|
+
};
|
|
1969
|
+
const icon = typeIcon[notification.type] ?? "\u2139\uFE0F";
|
|
1970
|
+
const name = notification.sessionName ? ` **${notification.sessionName}**` : "";
|
|
1971
|
+
let text = `${icon}${name}: ${notification.summary}`;
|
|
1972
|
+
if (notification.deepLink) {
|
|
1973
|
+
text += `
|
|
1974
|
+
${notification.deepLink}`;
|
|
1975
|
+
}
|
|
1976
|
+
try {
|
|
1977
|
+
await this.sendQueue.enqueue(
|
|
1978
|
+
() => this.notificationChannel.send({ content: text }),
|
|
1979
|
+
{ type: "other" }
|
|
1980
|
+
);
|
|
1981
|
+
} catch (err) {
|
|
1982
|
+
log.warn({ err }, "[DiscordAdapter] Failed to send notification");
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
// ─── createSessionThread ─────────────────────────────────────────────────
|
|
1986
|
+
async createSessionThread(sessionId, name) {
|
|
1987
|
+
const thread = await createSessionThread(this.forumChannel, name);
|
|
1988
|
+
const session = this.core.sessionManager.getSession(sessionId);
|
|
1989
|
+
if (session) {
|
|
1990
|
+
session.threadId = thread.id;
|
|
1991
|
+
}
|
|
1992
|
+
const record = this.core.sessionManager.getSessionRecord(sessionId);
|
|
1993
|
+
if (record) {
|
|
1994
|
+
await this.core.sessionManager.patchRecord(sessionId, {
|
|
1995
|
+
platform: { ...record.platform, threadId: thread.id }
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
return thread.id;
|
|
1999
|
+
}
|
|
2000
|
+
// ─── renameSessionThread ──────────────────────────────────────────────────
|
|
2001
|
+
async renameSessionThread(sessionId, newName) {
|
|
2002
|
+
const session = this.core.sessionManager.getSession(sessionId);
|
|
2003
|
+
const threadId = session?.threadId;
|
|
2004
|
+
if (!threadId) return;
|
|
2005
|
+
await renameSessionThread(this.guild, threadId, newName);
|
|
2006
|
+
}
|
|
2007
|
+
// ─── deleteSessionThread ──────────────────────────────────────────────────
|
|
2008
|
+
async deleteSessionThread(sessionId) {
|
|
2009
|
+
const session = this.core.sessionManager.getSession(sessionId);
|
|
2010
|
+
const threadId = session?.threadId;
|
|
2011
|
+
if (!threadId) return;
|
|
2012
|
+
await deleteSessionThread(this.guild, threadId);
|
|
2013
|
+
}
|
|
2014
|
+
// ─── sendSkillCommands ────────────────────────────────────────────────────
|
|
2015
|
+
async sendSkillCommands(sessionId, commands) {
|
|
2016
|
+
const thread = await this.getThread(sessionId);
|
|
2017
|
+
if (!thread) return;
|
|
2018
|
+
await this.skillManager.send(sessionId, thread, commands);
|
|
2019
|
+
}
|
|
2020
|
+
// ─── cleanupSkillCommands ─────────────────────────────────────────────────
|
|
2021
|
+
async cleanupSkillCommands(sessionId) {
|
|
2022
|
+
await this.skillManager.cleanup(sessionId);
|
|
2023
|
+
}
|
|
2024
|
+
// ─── Public helpers (for slash commands) ─────────────────────────────────
|
|
2025
|
+
getForumChannel() {
|
|
2026
|
+
return this.forumChannel;
|
|
2027
|
+
}
|
|
2028
|
+
getGuild() {
|
|
2029
|
+
return this.guild;
|
|
2030
|
+
}
|
|
2031
|
+
getGuildId() {
|
|
2032
|
+
return this.guild.id;
|
|
2033
|
+
}
|
|
2034
|
+
getAssistantSessionId() {
|
|
2035
|
+
return this.assistantSession?.id ?? null;
|
|
2036
|
+
}
|
|
2037
|
+
getAssistantThreadId() {
|
|
2038
|
+
return this.discordConfig.assistantThreadId;
|
|
2039
|
+
}
|
|
2040
|
+
};
|
|
2041
|
+
export {
|
|
2042
|
+
DiscordAdapter
|
|
2043
|
+
};
|
|
2044
|
+
//# sourceMappingURL=discord-2DKRH45T.js.map
|