@tritard/waterbrother 0.16.143 → 0.16.144
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/package.json +1 -1
- package/src/channels.js +2 -1
- package/src/cli.js +23 -3
- package/src/discord.js +404 -2
package/package.json
CHANGED
package/src/channels.js
CHANGED
|
@@ -232,11 +232,12 @@ export function buildChannelOnboardingPayload(serviceId) {
|
|
|
232
232
|
"waterbrother config set-json channels '{\"discord\":{\"enabled\":true,\"botToken\":\"YOUR_BOT_TOKEN\",\"applicationId\":\"YOUR_APP_ID\",\"pairingMode\":\"manual\",\"allowedUserIds\":[]}}'",
|
|
233
233
|
"waterbrother channels status",
|
|
234
234
|
"waterbrother discord status",
|
|
235
|
+
"waterbrother discord sync-commands",
|
|
235
236
|
"waterbrother discord run"
|
|
236
237
|
],
|
|
237
238
|
notes: [
|
|
238
239
|
"Start with direct messages before any guild or channel workflow.",
|
|
239
|
-
"
|
|
240
|
+
"Waterbrother now syncs native Discord slash commands on startup, and you can force a sync with waterbrother discord sync-commands.",
|
|
240
241
|
"Prefer explicit user allowlists.",
|
|
241
242
|
"The interactive TUI onboarding wizard can store the Discord token and application id directly, just like Telegram."
|
|
242
243
|
]
|
package/src/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import { fileURLToPath } from "node:url";
|
|
|
8
8
|
import { promisify } from "node:util";
|
|
9
9
|
import { Agent } from "./agent.js";
|
|
10
10
|
import { getConfigPath, loadConfigLayers, resolveRuntimeConfig, saveConfig } from "./config.js";
|
|
11
|
-
import { getDiscordStatus, runDiscordGateway } from "./discord.js";
|
|
11
|
+
import { getDiscordStatus, runDiscordGateway, syncDiscordCommands } from "./discord.js";
|
|
12
12
|
import { createChatCompletion, createJsonCompletion, listModels } from "./model-client.js";
|
|
13
13
|
import { buildChannelOnboardingPayload, getChannelStatuses, getGatewayStatus } from "./channels.js";
|
|
14
14
|
import { runGatewayService } from "./gateway.js";
|
|
@@ -4226,8 +4226,8 @@ async function runDoctorCommand({ runtime, cwd, asJson = false }) {
|
|
|
4226
4226
|
|
|
4227
4227
|
async function runDiscordCommand(positional, runtime, { asJson = false } = {}) {
|
|
4228
4228
|
const sub = String(positional[1] || "status").trim().toLowerCase();
|
|
4229
|
-
if (!["status", "run"].includes(sub)) {
|
|
4230
|
-
throw new Error("Usage: waterbrother discord <status|run>");
|
|
4229
|
+
if (!["status", "run", "sync-commands"].includes(sub)) {
|
|
4230
|
+
throw new Error("Usage: waterbrother discord <status|run|sync-commands>");
|
|
4231
4231
|
}
|
|
4232
4232
|
|
|
4233
4233
|
if (sub === "status") {
|
|
@@ -4243,6 +4243,12 @@ async function runDiscordCommand(positional, runtime, { asJson = false } = {}) {
|
|
|
4243
4243
|
console.log(`token: ${status.tokenConfigured ? "configured" : "missing"}`);
|
|
4244
4244
|
console.log(`application id: ${status.applicationId || "missing"}`);
|
|
4245
4245
|
console.log(`intents: ${status.intents.join(", ") || "none"}`);
|
|
4246
|
+
if (status.commandScope) {
|
|
4247
|
+
console.log(`slash commands: ${status.commandCount || 0} (${status.commandScope})`);
|
|
4248
|
+
if (status.commandUpdatedAt) {
|
|
4249
|
+
console.log(`commands updated: ${status.commandUpdatedAt}`);
|
|
4250
|
+
}
|
|
4251
|
+
}
|
|
4246
4252
|
if (status.lastStatus) {
|
|
4247
4253
|
console.log(`last state: ${status.lastStatus.state || "unknown"}`);
|
|
4248
4254
|
console.log(`updated: ${status.lastStatus.updatedAt || "unknown"}`);
|
|
@@ -4253,6 +4259,20 @@ async function runDiscordCommand(positional, runtime, { asJson = false } = {}) {
|
|
|
4253
4259
|
return;
|
|
4254
4260
|
}
|
|
4255
4261
|
|
|
4262
|
+
if (sub === "sync-commands") {
|
|
4263
|
+
if (asJson) {
|
|
4264
|
+
const result = await syncDiscordCommands(runtime);
|
|
4265
|
+
printData(result, true);
|
|
4266
|
+
return;
|
|
4267
|
+
}
|
|
4268
|
+
const result = await syncDiscordCommands(runtime, { log: (line) => console.log(line) });
|
|
4269
|
+
console.log("Discord slash commands");
|
|
4270
|
+
console.log("");
|
|
4271
|
+
console.log(`scope: ${result.scope}`);
|
|
4272
|
+
console.log(`count: ${result.count}`);
|
|
4273
|
+
return;
|
|
4274
|
+
}
|
|
4275
|
+
|
|
4256
4276
|
if (asJson) {
|
|
4257
4277
|
throw new Error("waterbrother discord run does not support --json");
|
|
4258
4278
|
}
|
package/src/discord.js
CHANGED
|
@@ -35,6 +35,7 @@ const STATUS_PATH = path.join(os.homedir(), ".waterbrother", "discord-status.jso
|
|
|
35
35
|
const DISCORD_BRIDGE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
36
36
|
const DISCORD_BRIDGE_POLL_MS = 250;
|
|
37
37
|
const DISCORD_CONTINUATION_TTL_MS = 15 * 60 * 1000;
|
|
38
|
+
const DISCORD_COMMAND_VERSION = 1;
|
|
38
39
|
|
|
39
40
|
const INTENT_BITS = {
|
|
40
41
|
GUILDS: 1 << 0,
|
|
@@ -77,6 +78,14 @@ async function loadDiscordStatus() {
|
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
async function patchDiscordStatus(patch = {}) {
|
|
82
|
+
const current = await loadDiscordStatus().catch(() => null);
|
|
83
|
+
await saveDiscordStatus({
|
|
84
|
+
...(current || {}),
|
|
85
|
+
...patch
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
80
89
|
function normalizeDiscordRuntime(runtime = {}) {
|
|
81
90
|
const discord = runtime?.channels?.discord || {};
|
|
82
91
|
const intents = Array.isArray(discord.intents) && discord.intents.length > 0
|
|
@@ -93,6 +102,169 @@ function normalizeDiscordRuntime(runtime = {}) {
|
|
|
93
102
|
};
|
|
94
103
|
}
|
|
95
104
|
|
|
105
|
+
function buildDiscordSlashCommands() {
|
|
106
|
+
const roleChoices = [
|
|
107
|
+
{ name: "executor", value: "executor" },
|
|
108
|
+
{ name: "reviewer", value: "reviewer" },
|
|
109
|
+
{ name: "verifier", value: "verifier" },
|
|
110
|
+
{ name: "standby", value: "standby" }
|
|
111
|
+
];
|
|
112
|
+
const roomModeChoices = [
|
|
113
|
+
{ name: "chat", value: "chat" },
|
|
114
|
+
{ name: "plan", value: "plan" },
|
|
115
|
+
{ name: "execute", value: "execute" }
|
|
116
|
+
];
|
|
117
|
+
const taskStateChoices = [
|
|
118
|
+
{ name: "open", value: "open" },
|
|
119
|
+
{ name: "active", value: "active" },
|
|
120
|
+
{ name: "blocked", value: "blocked" },
|
|
121
|
+
{ name: "done", value: "done" }
|
|
122
|
+
];
|
|
123
|
+
const inviteRoleChoices = [
|
|
124
|
+
{ name: "owner", value: "owner" },
|
|
125
|
+
{ name: "editor", value: "editor" },
|
|
126
|
+
{ name: "observer", value: "observer" }
|
|
127
|
+
];
|
|
128
|
+
const commands = [
|
|
129
|
+
{ name: "help", description: "Show Waterbrother Discord help" },
|
|
130
|
+
{ name: "about", description: "Show Waterbrother identity and capabilities" },
|
|
131
|
+
{ name: "state", description: "Show Waterbrother self-awareness state" },
|
|
132
|
+
{ name: "status", description: "Show Discord gateway and remote session status" },
|
|
133
|
+
{ name: "project", description: "Show the current project and sharing state" },
|
|
134
|
+
{ name: "project-share", description: "Share the current project with this Discord room" },
|
|
135
|
+
{ name: "room", description: "Show shared room status for this project" },
|
|
136
|
+
{ name: "members", description: "List shared room members" },
|
|
137
|
+
{ name: "tasks", description: "List shared tasks" },
|
|
138
|
+
{ name: "people", description: "List recently seen Discord people" },
|
|
139
|
+
{ name: "terminals", description: "List live Waterbrother terminals" },
|
|
140
|
+
{ name: "executor", description: "Show the assigned executor" },
|
|
141
|
+
{ name: "reviewer", description: "Show the assigned reviewer" },
|
|
142
|
+
{ name: "verifier", description: "Show the assigned verifier" },
|
|
143
|
+
{ name: "verify", description: "Run verification through the assigned verifier" },
|
|
144
|
+
{ name: "verification", description: "Show the latest verification result" },
|
|
145
|
+
{ name: "rerun-verification", description: "Rerun verification" },
|
|
146
|
+
{ name: "override-verification", description: "Override blocking verification" },
|
|
147
|
+
{ name: "review-status", description: "Show reviewer blocking status and latest outcome" },
|
|
148
|
+
{ name: "accept-reviewer-concerns", description: "Accept reviewer concerns and clear blocking review" },
|
|
149
|
+
{ name: "override-reviewer", description: "Override the current reviewer outcome" },
|
|
150
|
+
{ name: "switch-executor-to-reviewer", description: "Make the reviewer the executor" },
|
|
151
|
+
{ name: "new", description: "Start a fresh remote session" },
|
|
152
|
+
{ name: "sessions", description: "List recent remote sessions" },
|
|
153
|
+
{ name: "clear", description: "Clear the current remote conversation" },
|
|
154
|
+
{
|
|
155
|
+
name: "resume",
|
|
156
|
+
description: "Resume a remote session",
|
|
157
|
+
options: [
|
|
158
|
+
{ type: 3, name: "session-id", description: "Remote session id", required: true }
|
|
159
|
+
]
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: "mode",
|
|
163
|
+
description: "Show or set the shared room mode",
|
|
164
|
+
options: [
|
|
165
|
+
{ type: 3, name: "mode", description: "Room mode", required: false, choices: roomModeChoices }
|
|
166
|
+
]
|
|
167
|
+
},
|
|
168
|
+
{ name: "events", description: "List recent shared room events" },
|
|
169
|
+
{ name: "invites", description: "List pending shared room invites" },
|
|
170
|
+
{
|
|
171
|
+
name: "invite",
|
|
172
|
+
description: "Create a shared room invite",
|
|
173
|
+
options: [
|
|
174
|
+
{ type: 3, name: "target", description: "Discord user id, @username, or recent display name", required: true },
|
|
175
|
+
{ type: 3, name: "role", description: "Invite role", required: false, choices: inviteRoleChoices }
|
|
176
|
+
]
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: "approve-invite",
|
|
180
|
+
description: "Approve a pending invite",
|
|
181
|
+
options: [
|
|
182
|
+
{ type: 3, name: "invite-id", description: "Invite id", required: true }
|
|
183
|
+
]
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: "accept-invite",
|
|
187
|
+
description: "Accept a pending invite",
|
|
188
|
+
options: [
|
|
189
|
+
{ type: 3, name: "invite-id", description: "Invite id", required: true }
|
|
190
|
+
]
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: "reject-invite",
|
|
194
|
+
description: "Reject a pending invite",
|
|
195
|
+
options: [
|
|
196
|
+
{ type: 3, name: "invite-id", description: "Invite id", required: true }
|
|
197
|
+
]
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: "remove-member",
|
|
201
|
+
description: "Remove a shared room member",
|
|
202
|
+
options: [
|
|
203
|
+
{ type: 3, name: "member-id", description: "Member id", required: true }
|
|
204
|
+
]
|
|
205
|
+
},
|
|
206
|
+
{ name: "claim", description: "Claim shared operator control" },
|
|
207
|
+
{ name: "release", description: "Release shared operator control" },
|
|
208
|
+
{
|
|
209
|
+
name: "assign-role",
|
|
210
|
+
description: "Assign a room role to a terminal or agent",
|
|
211
|
+
options: [
|
|
212
|
+
{ type: 3, name: "target", description: "Terminal label, this terminal, or agent label", required: true },
|
|
213
|
+
{ type: 3, name: "role", description: "Role to assign", required: true, choices: roleChoices }
|
|
214
|
+
]
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: "task-add",
|
|
218
|
+
description: "Add a shared task",
|
|
219
|
+
options: [
|
|
220
|
+
{ type: 3, name: "text", description: "Task text", required: true }
|
|
221
|
+
]
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: "task-move",
|
|
225
|
+
description: "Move a shared task",
|
|
226
|
+
options: [
|
|
227
|
+
{ type: 3, name: "task-id", description: "Task id", required: true },
|
|
228
|
+
{ type: 3, name: "state", description: "Next task state", required: true, choices: taskStateChoices }
|
|
229
|
+
]
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
name: "task-assign",
|
|
233
|
+
description: "Assign a shared task",
|
|
234
|
+
options: [
|
|
235
|
+
{ type: 3, name: "task-id", description: "Task id", required: true },
|
|
236
|
+
{ type: 3, name: "member-id", description: "Member id", required: true }
|
|
237
|
+
]
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
name: "task-claim",
|
|
241
|
+
description: "Claim a shared task",
|
|
242
|
+
options: [
|
|
243
|
+
{ type: 3, name: "task-id", description: "Task id", required: true }
|
|
244
|
+
]
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
name: "task-comment",
|
|
248
|
+
description: "Comment on a shared task",
|
|
249
|
+
options: [
|
|
250
|
+
{ type: 3, name: "task-id", description: "Task id", required: true },
|
|
251
|
+
{ type: 3, name: "text", description: "Comment text", required: true }
|
|
252
|
+
]
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: "task-history",
|
|
256
|
+
description: "Show shared task history",
|
|
257
|
+
options: [
|
|
258
|
+
{ type: 3, name: "task-id", description: "Task id", required: true }
|
|
259
|
+
]
|
|
260
|
+
}
|
|
261
|
+
];
|
|
262
|
+
return commands.map((command) => ({
|
|
263
|
+
...command,
|
|
264
|
+
dm_permission: true
|
|
265
|
+
}));
|
|
266
|
+
}
|
|
267
|
+
|
|
96
268
|
async function discordFetch(runtime, route, init = {}) {
|
|
97
269
|
const token = String(runtime?.token || "").trim();
|
|
98
270
|
if (!token) {
|
|
@@ -112,6 +284,55 @@ async function discordFetch(runtime, route, init = {}) {
|
|
|
112
284
|
return response.json();
|
|
113
285
|
}
|
|
114
286
|
|
|
287
|
+
function getDiscordCommandScope(discord = {}, status = null) {
|
|
288
|
+
const guildId = String(discord.guildId || status?.lastGuildId || "").trim();
|
|
289
|
+
if (guildId) {
|
|
290
|
+
return {
|
|
291
|
+
guildId,
|
|
292
|
+
scope: `guild:${guildId}`,
|
|
293
|
+
route: `/applications/${discord.applicationId}/guilds/${guildId}/commands`
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
guildId: "",
|
|
298
|
+
scope: "global",
|
|
299
|
+
route: `/applications/${discord.applicationId}/commands`
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export async function syncDiscordCommands(runtime = {}, { log = () => {} } = {}) {
|
|
304
|
+
const discord = normalizeDiscordRuntime(runtime);
|
|
305
|
+
if (!discord.enabled) {
|
|
306
|
+
throw new Error("Discord is not enabled in channels.discord.enabled");
|
|
307
|
+
}
|
|
308
|
+
if (!discord.token) {
|
|
309
|
+
throw new Error("Discord bot token is missing");
|
|
310
|
+
}
|
|
311
|
+
if (!discord.applicationId) {
|
|
312
|
+
throw new Error("Discord application id is missing");
|
|
313
|
+
}
|
|
314
|
+
const savedStatus = await loadDiscordStatus().catch(() => null);
|
|
315
|
+
const scope = getDiscordCommandScope(discord, savedStatus);
|
|
316
|
+
const commands = buildDiscordSlashCommands();
|
|
317
|
+
const synced = await discordFetch(discord, scope.route, {
|
|
318
|
+
method: "PUT",
|
|
319
|
+
body: JSON.stringify(commands)
|
|
320
|
+
});
|
|
321
|
+
const count = Array.isArray(synced) ? synced.length : commands.length;
|
|
322
|
+
await patchDiscordStatus({
|
|
323
|
+
commandVersion: DISCORD_COMMAND_VERSION,
|
|
324
|
+
commandScope: scope.scope,
|
|
325
|
+
commandCount: count,
|
|
326
|
+
commandUpdatedAt: new Date().toISOString()
|
|
327
|
+
});
|
|
328
|
+
log(`discord: slash commands synced (${scope.scope}, ${count})`);
|
|
329
|
+
return {
|
|
330
|
+
scope: scope.scope,
|
|
331
|
+
guildId: scope.guildId,
|
|
332
|
+
count
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
115
336
|
async function fetchGatewayInfo(runtime) {
|
|
116
337
|
return discordFetch(runtime, "/gateway/bot", { method: "GET" });
|
|
117
338
|
}
|
|
@@ -125,6 +346,27 @@ async function sendChannelMessage(runtime, channelId, content) {
|
|
|
125
346
|
});
|
|
126
347
|
}
|
|
127
348
|
|
|
349
|
+
async function createInteractionResponse(runtime, interaction, body) {
|
|
350
|
+
return discordFetch(runtime, `/interactions/${interaction.id}/${interaction.token}/callback`, {
|
|
351
|
+
method: "POST",
|
|
352
|
+
body: JSON.stringify(body)
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function deferInteractionResponse(runtime, interaction) {
|
|
357
|
+
return createInteractionResponse(runtime, interaction, {
|
|
358
|
+
type: 5
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function editInteractionResponse(runtime, interaction, content) {
|
|
363
|
+
const text = String(content || "").trim() || "(no content)";
|
|
364
|
+
return discordFetch(runtime, `/webhooks/${runtime.applicationId}/${interaction.token}/messages/@original`, {
|
|
365
|
+
method: "PATCH",
|
|
366
|
+
body: JSON.stringify({ content: text })
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
128
370
|
function extractMentionContent(content, botUserId) {
|
|
129
371
|
const raw = String(content || "");
|
|
130
372
|
if (!botUserId) return raw.trim();
|
|
@@ -348,6 +590,105 @@ function describeDiscordUser(message = {}) {
|
|
|
348
590
|
};
|
|
349
591
|
}
|
|
350
592
|
|
|
593
|
+
function getInteractionOptionValue(options = [], name) {
|
|
594
|
+
const match = Array.isArray(options) ? options.find((option) => String(option?.name || "").trim() === String(name || "").trim()) : null;
|
|
595
|
+
return match?.value;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function buildInteractionMessage(interaction = {}) {
|
|
599
|
+
const user = interaction?.member?.user || interaction?.user || {};
|
|
600
|
+
return {
|
|
601
|
+
id: String(interaction?.id || "").trim(),
|
|
602
|
+
channel_id: String(interaction?.channel_id || "").trim(),
|
|
603
|
+
guild_id: String(interaction?.guild_id || "").trim(),
|
|
604
|
+
content: "",
|
|
605
|
+
author: {
|
|
606
|
+
id: String(user?.id || "").trim(),
|
|
607
|
+
username: String(user?.username || "").trim(),
|
|
608
|
+
global_name: String(user?.global_name || "").trim(),
|
|
609
|
+
bot: Boolean(user?.bot)
|
|
610
|
+
},
|
|
611
|
+
mentions: []
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function buildDiscordCommandTextFromInteraction(interaction = {}) {
|
|
616
|
+
const data = interaction?.data && typeof interaction.data === "object" ? interaction.data : {};
|
|
617
|
+
const name = String(data.name || "").trim().toLowerCase();
|
|
618
|
+
const options = Array.isArray(data.options) ? data.options : [];
|
|
619
|
+
switch (name) {
|
|
620
|
+
case "help":
|
|
621
|
+
case "about":
|
|
622
|
+
case "state":
|
|
623
|
+
case "status":
|
|
624
|
+
case "project":
|
|
625
|
+
case "room":
|
|
626
|
+
case "members":
|
|
627
|
+
case "tasks":
|
|
628
|
+
case "people":
|
|
629
|
+
case "terminals":
|
|
630
|
+
case "executor":
|
|
631
|
+
case "reviewer":
|
|
632
|
+
case "verifier":
|
|
633
|
+
case "verify":
|
|
634
|
+
case "verification":
|
|
635
|
+
case "rerun-verification":
|
|
636
|
+
case "override-verification":
|
|
637
|
+
case "review-status":
|
|
638
|
+
case "accept-reviewer-concerns":
|
|
639
|
+
case "override-reviewer":
|
|
640
|
+
case "switch-executor-to-reviewer":
|
|
641
|
+
case "new":
|
|
642
|
+
case "sessions":
|
|
643
|
+
case "clear":
|
|
644
|
+
case "events":
|
|
645
|
+
case "invites":
|
|
646
|
+
case "claim":
|
|
647
|
+
case "release":
|
|
648
|
+
return `/${name}`;
|
|
649
|
+
case "project-share":
|
|
650
|
+
return "/project share";
|
|
651
|
+
case "resume":
|
|
652
|
+
return `/resume ${String(getInteractionOptionValue(options, "session-id") || "").trim()}`.trim();
|
|
653
|
+
case "mode": {
|
|
654
|
+
const mode = String(getInteractionOptionValue(options, "mode") || "").trim();
|
|
655
|
+
return mode ? `/mode ${mode}` : "/mode";
|
|
656
|
+
}
|
|
657
|
+
case "invite": {
|
|
658
|
+
const target = String(getInteractionOptionValue(options, "target") || "").trim();
|
|
659
|
+
const role = String(getInteractionOptionValue(options, "role") || "").trim();
|
|
660
|
+
return [`/invite`, target, role].filter(Boolean).join(" ");
|
|
661
|
+
}
|
|
662
|
+
case "approve-invite":
|
|
663
|
+
return `/approve-invite ${String(getInteractionOptionValue(options, "invite-id") || "").trim()}`.trim();
|
|
664
|
+
case "accept-invite":
|
|
665
|
+
return `/accept-invite ${String(getInteractionOptionValue(options, "invite-id") || "").trim()}`.trim();
|
|
666
|
+
case "reject-invite":
|
|
667
|
+
return `/reject-invite ${String(getInteractionOptionValue(options, "invite-id") || "").trim()}`.trim();
|
|
668
|
+
case "remove-member":
|
|
669
|
+
return `/remove-member ${String(getInteractionOptionValue(options, "member-id") || "").trim()}`.trim();
|
|
670
|
+
case "assign-role": {
|
|
671
|
+
const target = String(getInteractionOptionValue(options, "target") || "").trim();
|
|
672
|
+
const role = String(getInteractionOptionValue(options, "role") || "").trim();
|
|
673
|
+
return [`/assign-role`, target, role].filter(Boolean).join(" ");
|
|
674
|
+
}
|
|
675
|
+
case "task-add":
|
|
676
|
+
return `/task add ${String(getInteractionOptionValue(options, "text") || "").trim()}`.trim();
|
|
677
|
+
case "task-move":
|
|
678
|
+
return `/task move ${String(getInteractionOptionValue(options, "task-id") || "").trim()} ${String(getInteractionOptionValue(options, "state") || "").trim()}`.trim();
|
|
679
|
+
case "task-assign":
|
|
680
|
+
return `/task assign ${String(getInteractionOptionValue(options, "task-id") || "").trim()} ${String(getInteractionOptionValue(options, "member-id") || "").trim()}`.trim();
|
|
681
|
+
case "task-claim":
|
|
682
|
+
return `/task claim ${String(getInteractionOptionValue(options, "task-id") || "").trim()}`.trim();
|
|
683
|
+
case "task-comment":
|
|
684
|
+
return `/task comment ${String(getInteractionOptionValue(options, "task-id") || "").trim()} ${String(getInteractionOptionValue(options, "text") || "").trim()}`.trim();
|
|
685
|
+
case "task-history":
|
|
686
|
+
return `/task history ${String(getInteractionOptionValue(options, "task-id") || "").trim()}`.trim();
|
|
687
|
+
default:
|
|
688
|
+
return "";
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
351
692
|
function formatDiscordInvites(invites = []) {
|
|
352
693
|
if (!invites.length) return "Pending invites\n- none";
|
|
353
694
|
return [
|
|
@@ -976,6 +1317,37 @@ async function handleDiscordSessionCommand(runtime, state, message, command) {
|
|
|
976
1317
|
return null;
|
|
977
1318
|
}
|
|
978
1319
|
|
|
1320
|
+
async function handleDiscordSlashInteraction(runtime, interaction, { log = () => {} } = {}) {
|
|
1321
|
+
const discord = normalizeDiscordRuntime(runtime);
|
|
1322
|
+
const message = buildInteractionMessage(interaction);
|
|
1323
|
+
const rawText = buildDiscordCommandTextFromInteraction(interaction);
|
|
1324
|
+
if (!rawText) {
|
|
1325
|
+
await createInteractionResponse(discord, interaction, {
|
|
1326
|
+
type: 4,
|
|
1327
|
+
data: { content: "Unsupported slash command payload." }
|
|
1328
|
+
});
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
await deferInteractionResponse(discord, interaction);
|
|
1333
|
+
try {
|
|
1334
|
+
const state = await loadDiscordGatewayState();
|
|
1335
|
+
let reply = await handleDiscordControlCommand(runtime, state, message, rawText);
|
|
1336
|
+
if (!reply) {
|
|
1337
|
+
const command = parseDiscordSessionCommand(rawText);
|
|
1338
|
+
if (command) {
|
|
1339
|
+
reply = await handleDiscordSessionCommand(runtime, state, message, command);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
await editInteractionResponse(discord, interaction, reply || "Command completed.");
|
|
1343
|
+
log(`discord: interaction reply in ${message.channel_id}`);
|
|
1344
|
+
} catch (error) {
|
|
1345
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
1346
|
+
await editInteractionResponse(discord, interaction, `Slash command failed: ${text}`);
|
|
1347
|
+
throw error;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
|
|
979
1351
|
async function buildDiscordAbout(runtime, state, message) {
|
|
980
1352
|
const actor = describeDiscordUser(message);
|
|
981
1353
|
const sessionId = getPeerState(state, actor.userId)?.sessionId || "";
|
|
@@ -1807,6 +2179,9 @@ export async function getDiscordStatus(runtime = {}) {
|
|
|
1807
2179
|
guildId: discord.guildId || null,
|
|
1808
2180
|
intents: discord.intents,
|
|
1809
2181
|
intentMask: discord.intentMask,
|
|
2182
|
+
commandScope: saved?.commandScope || null,
|
|
2183
|
+
commandCount: Number(saved?.commandCount || 0),
|
|
2184
|
+
commandUpdatedAt: saved?.commandUpdatedAt || null,
|
|
1810
2185
|
lastStatus: saved
|
|
1811
2186
|
};
|
|
1812
2187
|
}
|
|
@@ -1823,6 +2198,12 @@ export async function runDiscordGateway(runtime = {}, { log = console.log, signa
|
|
|
1823
2198
|
throw new Error("Discord application id is missing");
|
|
1824
2199
|
}
|
|
1825
2200
|
|
|
2201
|
+
try {
|
|
2202
|
+
await syncDiscordCommands(runtime, { log });
|
|
2203
|
+
} catch (error) {
|
|
2204
|
+
log(`discord: slash command sync failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2205
|
+
}
|
|
2206
|
+
|
|
1826
2207
|
const gatewayInfo = await fetchGatewayInfo(discord);
|
|
1827
2208
|
const gatewayUrl = `${gatewayInfo.url}?v=10&encoding=json`;
|
|
1828
2209
|
const ws = new WebSocket(gatewayUrl);
|
|
@@ -1843,7 +2224,7 @@ export async function runDiscordGateway(runtime = {}, { log = console.log, signa
|
|
|
1843
2224
|
closed = true;
|
|
1844
2225
|
stopHeartbeat();
|
|
1845
2226
|
try { ws.close(1000, reason); } catch {}
|
|
1846
|
-
await
|
|
2227
|
+
await patchDiscordStatus({
|
|
1847
2228
|
state: reason,
|
|
1848
2229
|
updatedAt: new Date().toISOString(),
|
|
1849
2230
|
applicationId: discord.applicationId,
|
|
@@ -1911,7 +2292,7 @@ export async function runDiscordGateway(runtime = {}, { log = console.log, signa
|
|
|
1911
2292
|
if (packet.t === "READY") {
|
|
1912
2293
|
botUser = packet.d?.user || null;
|
|
1913
2294
|
log(`discord: ready as ${botUser?.username || "unknown"} (${botUser?.id || "n/a"})`);
|
|
1914
|
-
void
|
|
2295
|
+
void patchDiscordStatus({
|
|
1915
2296
|
state: "ready",
|
|
1916
2297
|
updatedAt: new Date().toISOString(),
|
|
1917
2298
|
applicationId: discord.applicationId,
|
|
@@ -1923,12 +2304,33 @@ export async function runDiscordGateway(runtime = {}, { log = console.log, signa
|
|
|
1923
2304
|
return;
|
|
1924
2305
|
}
|
|
1925
2306
|
|
|
2307
|
+
if (packet.t === "INTERACTION_CREATE") {
|
|
2308
|
+
try {
|
|
2309
|
+
if (packet.d?.guild_id) {
|
|
2310
|
+
await patchDiscordStatus({
|
|
2311
|
+
lastGuildId: String(packet.d.guild_id || "").trim(),
|
|
2312
|
+
updatedAt: new Date().toISOString()
|
|
2313
|
+
});
|
|
2314
|
+
}
|
|
2315
|
+
await handleDiscordSlashInteraction(runtime, packet.d, { log });
|
|
2316
|
+
} catch (error) {
|
|
2317
|
+
log(`discord: interaction failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2318
|
+
}
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
|
|
1926
2322
|
if (packet.t === "MESSAGE_CREATE") {
|
|
1927
2323
|
const msg = packet.d;
|
|
1928
2324
|
if (!msg || msg.author?.bot) return;
|
|
1929
2325
|
const scope = msg.guild_id ? `guild:${msg.guild_id}` : "dm";
|
|
1930
2326
|
log(`discord: ${scope} #${msg.channel_id} ${msg.author?.username || "unknown"} -> ${String(msg.content || "").trim()}`);
|
|
1931
2327
|
try {
|
|
2328
|
+
if (msg.guild_id) {
|
|
2329
|
+
await patchDiscordStatus({
|
|
2330
|
+
lastGuildId: String(msg.guild_id || "").trim(),
|
|
2331
|
+
updatedAt: new Date().toISOString()
|
|
2332
|
+
});
|
|
2333
|
+
}
|
|
1932
2334
|
const trackingState = await loadDiscordGatewayState();
|
|
1933
2335
|
await rememberKnownDiscordPeople(trackingState, msg);
|
|
1934
2336
|
const shouldReply = shouldReplyToMessage(msg, botUser?.id);
|