@spinabot/brigade 1.6.0 → 1.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.
Files changed (102) hide show
  1. package/dist/agents/agent-loop.d.ts.map +1 -1
  2. package/dist/agents/agent-loop.js +51 -1
  3. package/dist/agents/agent-loop.js.map +1 -1
  4. package/dist/agents/channels/bundled-channel-metas.d.ts +2 -0
  5. package/dist/agents/channels/bundled-channel-metas.d.ts.map +1 -1
  6. package/dist/agents/channels/bundled-channel-metas.js +11 -0
  7. package/dist/agents/channels/bundled-channel-metas.js.map +1 -1
  8. package/dist/agents/channels/discord/account-config.d.ts +117 -0
  9. package/dist/agents/channels/discord/account-config.d.ts.map +1 -0
  10. package/dist/agents/channels/discord/account-config.js +260 -0
  11. package/dist/agents/channels/discord/account-config.js.map +1 -0
  12. package/dist/agents/channels/discord/adapter.d.ts +56 -0
  13. package/dist/agents/channels/discord/adapter.d.ts.map +1 -0
  14. package/dist/agents/channels/discord/adapter.js +526 -0
  15. package/dist/agents/channels/discord/adapter.js.map +1 -0
  16. package/dist/agents/channels/discord/approval-authorize.d.ts +43 -0
  17. package/dist/agents/channels/discord/approval-authorize.d.ts.map +1 -0
  18. package/dist/agents/channels/discord/approval-authorize.js +71 -0
  19. package/dist/agents/channels/discord/approval-authorize.js.map +1 -0
  20. package/dist/agents/channels/discord/approval-native.d.ts +68 -0
  21. package/dist/agents/channels/discord/approval-native.d.ts.map +1 -0
  22. package/dist/agents/channels/discord/approval-native.js +81 -0
  23. package/dist/agents/channels/discord/approval-native.js.map +1 -0
  24. package/dist/agents/channels/discord/command-menu.d.ts +49 -0
  25. package/dist/agents/channels/discord/command-menu.d.ts.map +1 -0
  26. package/dist/agents/channels/discord/command-menu.js +73 -0
  27. package/dist/agents/channels/discord/command-menu.js.map +1 -0
  28. package/dist/agents/channels/discord/components.d.ts +97 -0
  29. package/dist/agents/channels/discord/components.d.ts.map +1 -0
  30. package/dist/agents/channels/discord/components.js +131 -0
  31. package/dist/agents/channels/discord/components.js.map +1 -0
  32. package/dist/agents/channels/discord/connection.d.ts +387 -0
  33. package/dist/agents/channels/discord/connection.d.ts.map +1 -0
  34. package/dist/agents/channels/discord/connection.js +786 -0
  35. package/dist/agents/channels/discord/connection.js.map +1 -0
  36. package/dist/agents/channels/discord/draft-stream.d.ts +92 -0
  37. package/dist/agents/channels/discord/draft-stream.d.ts.map +1 -0
  38. package/dist/agents/channels/discord/draft-stream.js +213 -0
  39. package/dist/agents/channels/discord/draft-stream.js.map +1 -0
  40. package/dist/agents/channels/discord/format.d.ts +55 -0
  41. package/dist/agents/channels/discord/format.d.ts.map +1 -0
  42. package/dist/agents/channels/discord/format.js +247 -0
  43. package/dist/agents/channels/discord/format.js.map +1 -0
  44. package/dist/agents/channels/discord/inbound-extras.d.ts +220 -0
  45. package/dist/agents/channels/discord/inbound-extras.d.ts.map +1 -0
  46. package/dist/agents/channels/discord/inbound-extras.js +351 -0
  47. package/dist/agents/channels/discord/inbound-extras.js.map +1 -0
  48. package/dist/agents/channels/discord/index.d.ts +14 -0
  49. package/dist/agents/channels/discord/index.d.ts.map +1 -0
  50. package/dist/agents/channels/discord/index.js +14 -0
  51. package/dist/agents/channels/discord/index.js.map +1 -0
  52. package/dist/agents/channels/discord/media.d.ts +85 -0
  53. package/dist/agents/channels/discord/media.d.ts.map +1 -0
  54. package/dist/agents/channels/discord/media.js +242 -0
  55. package/dist/agents/channels/discord/media.js.map +1 -0
  56. package/dist/agents/channels/discord/module.d.ts +15 -0
  57. package/dist/agents/channels/discord/module.d.ts.map +1 -0
  58. package/dist/agents/channels/discord/module.js +22 -0
  59. package/dist/agents/channels/discord/module.js.map +1 -0
  60. package/dist/agents/channels/discord/plugin.d.ts +73 -0
  61. package/dist/agents/channels/discord/plugin.d.ts.map +1 -0
  62. package/dist/agents/channels/discord/plugin.js +303 -0
  63. package/dist/agents/channels/discord/plugin.js.map +1 -0
  64. package/dist/agents/channels/discord/probe.d.ts +93 -0
  65. package/dist/agents/channels/discord/probe.d.ts.map +1 -0
  66. package/dist/agents/channels/discord/probe.js +158 -0
  67. package/dist/agents/channels/discord/probe.js.map +1 -0
  68. package/dist/agents/channels/discord/reasoning-lane.d.ts +42 -0
  69. package/dist/agents/channels/discord/reasoning-lane.d.ts.map +1 -0
  70. package/dist/agents/channels/discord/reasoning-lane.js +68 -0
  71. package/dist/agents/channels/discord/reasoning-lane.js.map +1 -0
  72. package/dist/agents/channels/inbound-pipeline.d.ts.map +1 -1
  73. package/dist/agents/channels/inbound-pipeline.js +65 -7
  74. package/dist/agents/channels/inbound-pipeline.js.map +1 -1
  75. package/dist/agents/channels/sdk.d.ts +2 -0
  76. package/dist/agents/channels/sdk.d.ts.map +1 -1
  77. package/dist/agents/channels/sdk.js +2 -0
  78. package/dist/agents/channels/sdk.js.map +1 -1
  79. package/dist/agents/extensions/modules/index.d.ts.map +1 -1
  80. package/dist/agents/extensions/modules/index.js +5 -0
  81. package/dist/agents/extensions/modules/index.js.map +1 -1
  82. package/dist/agents/tools/cron-tool.d.ts.map +1 -1
  83. package/dist/agents/tools/cron-tool.js +4 -1
  84. package/dist/agents/tools/cron-tool.js.map +1 -1
  85. package/dist/buildstamp.json +1 -1
  86. package/dist/core/auth-bridge.d.ts +1 -0
  87. package/dist/core/auth-bridge.d.ts.map +1 -1
  88. package/dist/core/auth-bridge.js +46 -1
  89. package/dist/core/auth-bridge.js.map +1 -1
  90. package/dist/core/server.d.ts.map +1 -1
  91. package/dist/core/server.js +18 -2
  92. package/dist/core/server.js.map +1 -1
  93. package/dist/cron/isolated-agent/run-executor.d.ts +11 -0
  94. package/dist/cron/isolated-agent/run-executor.d.ts.map +1 -1
  95. package/dist/cron/isolated-agent/run-executor.js +20 -4
  96. package/dist/cron/isolated-agent/run-executor.js.map +1 -1
  97. package/dist/cron/types.d.ts +8 -0
  98. package/dist/cron/types.d.ts.map +1 -1
  99. package/dist/system-prompt/assembler.d.ts.map +1 -1
  100. package/dist/system-prompt/assembler.js +4 -2
  101. package/dist/system-prompt/assembler.js.map +1 -1
  102. package/package.json +2 -1
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Discord media helpers — inbound download + outbound upload construction.
3
+ *
4
+ * INBOUND: a Discord message attachment carries a public CDN `url`
5
+ * (cdn.discordapp.com / media.discordapp.net). We GET that URL (NO auth header —
6
+ * Discord attachment URLs are public, unlike Slack's `url_private`) and save the
7
+ * bytes under `~/.brigade/channels/discord/media/<YYYY-MM-DD>/<id>.<ext>` so the
8
+ * agent can `read` the attachment by path. In convex mode the cache relocates to
9
+ * the OS cache dir (never under ~/.brigade, to respect the strict-zero guard).
10
+ *
11
+ * OUTBOUND: `buildDiscordAttachment` validates a local path through Brigade's
12
+ * outbound media-path guard (so a prompt-injected "send ~/.ssh/id_rsa" can't
13
+ * exfiltrate a secret), then returns the `{ path, name }` the connection wraps
14
+ * in a discord.js `AttachmentBuilder`. The builder itself lives in the
15
+ * connection (which imports discord.js); this module stays dependency-light +
16
+ * unit-testable.
17
+ *
18
+ * SSRF GUARD: even though Discord URLs are public, we REQUIRE https + a Discord
19
+ * CDN host before fetching. Without this a prompt-injected / spoofed message
20
+ * could carry `http://169.254.169.254/…` (cloud metadata) or any attacker host
21
+ * and Brigade would fetch it — a classic SSRF. Mirrors `slack/media.ts`.
22
+ */
23
+ import { mkdirSync, writeFileSync } from "node:fs";
24
+ import path from "node:path";
25
+ import { resolveChannelStateDir, resolveOsCacheDir } from "../../../config/paths.js";
26
+ import { tryGetRuntimeContext } from "../../../storage/runtime-context.js";
27
+ // Channel SDK barrel — the outbound-media exfil guard + the contract types. All
28
+ // contract types come from the channel SDK barrel so the channel is built
29
+ // entirely on `../sdk.js`.
30
+ import { validateOutboundMediaPath } from "../sdk.js";
31
+ import { resolveDiscordAttachmentKind } from "./inbound-extras.js";
32
+ const CHANNEL_ID = "discord";
33
+ /**
34
+ * Defensive ceiling on an inbound attachment download. Discord's own per-file
35
+ * limit is generous (and tiered by server boost); we cap at 50 MB so a huge
36
+ * upload can't blow out memory. Anything larger is skipped (the message still
37
+ * reaches the agent without the attachment).
38
+ */
39
+ const MAX_BYTES = 50 * 1024 * 1024;
40
+ /**
41
+ * Discord's attachment-CDN hosts. An inbound attachment `url` only ever points
42
+ * at one of these — before fetching we REQUIRE https + a Discord host so a
43
+ * spoofed message can't make Brigade fetch an arbitrary internal URL (SSRF).
44
+ * Subdomains of these hosts are allowed.
45
+ */
46
+ const DISCORD_CDN_HOSTS = ["cdn.discordapp.com", "media.discordapp.net", "cdn.discord.com"];
47
+ /**
48
+ * True when `rawUrl` is an https URL whose host is a Discord CDN host (or a
49
+ * subdomain of one). Anything else (non-https, a non-Discord host, or an
50
+ * unparseable URL) returns false so the caller refuses to fetch.
51
+ */
52
+ export function isAllowedDiscordAttachmentUrl(rawUrl) {
53
+ let parsed;
54
+ try {
55
+ parsed = new URL(rawUrl);
56
+ }
57
+ catch {
58
+ return false;
59
+ }
60
+ if (parsed.protocol !== "https:")
61
+ return false;
62
+ const host = parsed.hostname.toLowerCase();
63
+ return DISCORD_CDN_HOSTS.some((h) => host === h || host.endsWith(`.${h}`));
64
+ }
65
+ /** YYYY-MM-DD (UTC) bucket — stable filename grouping for grep / review. */
66
+ function dayBucket() {
67
+ const d = new Date();
68
+ const pad = (x) => String(x).padStart(2, "0");
69
+ return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}`;
70
+ }
71
+ /** Derive a file extension from a Discord attachment (filename / content-type / kind). */
72
+ function extFromAttachment(att, kind) {
73
+ const name = (att.name ?? "").toLowerCase();
74
+ const fromName = name.includes(".") ? name.slice(name.lastIndexOf(".") + 1).replace(/[^a-z0-9]/g, "") : "";
75
+ if (fromName && /^[a-z0-9]+$/.test(fromName))
76
+ return fromName;
77
+ const mime = (att.contentType ?? "").toLowerCase();
78
+ const fromMime = mime.includes("/") ? mime.slice(mime.lastIndexOf("/") + 1).split(";")[0]?.replace(/[^a-z0-9]/g, "") : "";
79
+ if (fromMime && /^[a-z0-9]+$/.test(fromMime))
80
+ return fromMime;
81
+ // Sensible default by kind when nothing carried an extension.
82
+ switch (kind) {
83
+ case "image":
84
+ return "png";
85
+ case "video":
86
+ return "mp4";
87
+ case "voice":
88
+ return "ogg";
89
+ case "audio":
90
+ return "mp3";
91
+ default:
92
+ return "bin";
93
+ }
94
+ }
95
+ /** Where downloaded media lands — OS cache in convex mode, channel-state dir otherwise. */
96
+ function mediaBaseDir() {
97
+ return tryGetRuntimeContext()?.mode === "convex"
98
+ ? path.join(resolveOsCacheDir(), "channels", CHANNEL_ID)
99
+ : resolveChannelStateDir(CHANNEL_ID);
100
+ }
101
+ /**
102
+ * Bounded retry for the transient attachment fetch. Discord's CDN occasionally
103
+ * blips on a 5xx or a network reset; one or two quick retries turn a dropped
104
+ * attachment into a delivered one. The caller still wraps the whole thing in a
105
+ * try/catch that degrades to `null`, so an exhausted retry never breaks message
106
+ * delivery. Mirrors `slack/media.ts`'s `withSlackRetry`.
107
+ */
108
+ export async function withDiscordRetry(fn, attempts = 3) {
109
+ let lastErr;
110
+ for (let i = 0; i < attempts; i++) {
111
+ try {
112
+ return await fn();
113
+ }
114
+ catch (err) {
115
+ lastErr = err;
116
+ if (i < attempts - 1)
117
+ await new Promise((r) => setTimeout(r, 200 * 2 ** i));
118
+ }
119
+ }
120
+ throw lastErr;
121
+ }
122
+ /**
123
+ * Download one inbound Discord attachment to disk and return its normalized
124
+ * descriptor, or `null` when it couldn't be fetched (no url / too big / network
125
+ * error / non-Discord host). Never throws — a download glitch must not break
126
+ * message delivery.
127
+ */
128
+ export async function downloadDiscordAttachment(args) {
129
+ const { attachment, log } = args;
130
+ const doFetch = args.fetchImpl ?? fetch;
131
+ const url = attachment.url || attachment.proxyURL;
132
+ if (!url) {
133
+ log?.("discord attachment skipped — no url", { id: attachment.id });
134
+ return null;
135
+ }
136
+ // SSRF guard: REFUSE any url that isn't https on a Discord CDN host — a spoofed
137
+ // message pointing at `http://169.254.169.254/…` (or any attacker host) must
138
+ // never be fetched. Checked BEFORE the fetch.
139
+ if (!isAllowedDiscordAttachmentUrl(url)) {
140
+ log?.("discord attachment skipped — url is not an allowed Discord CDN host (SSRF guard)", { id: attachment.id });
141
+ return null;
142
+ }
143
+ if (typeof attachment.size === "number" && attachment.size > MAX_BYTES) {
144
+ log?.("discord attachment skipped — exceeds size cap", { id: attachment.id, bytes: attachment.size, cap: MAX_BYTES });
145
+ return null;
146
+ }
147
+ const kind = resolveDiscordAttachmentKind(attachment);
148
+ try {
149
+ const res = await withDiscordRetry(async () => {
150
+ // `redirect: "manual"` so a cross-origin 30x can't carry the request off to
151
+ // a non-Discord host. A redirect surfaces as a non-ok response and falls
152
+ // through to the `!r.ok` handler below.
153
+ const r = await doFetch(url, { redirect: "manual" });
154
+ // Retry 5xx (transient CDN blip); a 4xx falls through to the !ok handler.
155
+ if (!r.ok && r.status >= 500)
156
+ throw new Error(`discord attachment fetch failed (${r.status})`);
157
+ return r;
158
+ });
159
+ if (!res.ok) {
160
+ log?.("discord attachment download failed", { id: attachment.id, status: res.status });
161
+ return null;
162
+ }
163
+ const buf = Buffer.from(await res.arrayBuffer());
164
+ if (buf.length === 0)
165
+ return null;
166
+ if (buf.length > MAX_BYTES) {
167
+ log?.("discord attachment skipped — exceeds size cap", { id: attachment.id, bytes: buf.length, cap: MAX_BYTES });
168
+ return null;
169
+ }
170
+ const dir = path.join(mediaBaseDir(), "media", dayBucket());
171
+ mkdirSync(dir, { recursive: true });
172
+ // attachment.id is stable; use it as the filename so the same media resolves
173
+ // idempotently. Fall back to a timestamp.
174
+ const baseName = (attachment.id || `discord_${Date.now()}`).replace(/[^A-Za-z0-9_-]/g, "_");
175
+ const dest = path.join(dir, `${baseName}.${extFromAttachment(attachment, kind)}`);
176
+ writeFileSync(dest, buf, { mode: 0o600 });
177
+ return {
178
+ kind,
179
+ path: dest,
180
+ ...(attachment.contentType ? { mimeType: attachment.contentType } : {}),
181
+ ...(attachment.name ? { fileName: attachment.name } : {}),
182
+ };
183
+ }
184
+ catch (err) {
185
+ log?.("discord attachment download failed", {
186
+ id: attachment.id,
187
+ error: err instanceof Error ? err.message : String(err),
188
+ });
189
+ return null;
190
+ }
191
+ }
192
+ /** Default outbound file extension by media kind (used when the path has none). */
193
+ function outboundExtForKind(kind) {
194
+ switch (kind) {
195
+ case "image":
196
+ return "png";
197
+ case "video":
198
+ return "mp4";
199
+ case "voice":
200
+ return "ogg";
201
+ case "audio":
202
+ return "mp3";
203
+ default:
204
+ // document / sticker / anything else: leave extensionless (Discord falls
205
+ // back to the content the byte-sniff detects).
206
+ return "";
207
+ }
208
+ }
209
+ /**
210
+ * Validate + shape a local file for outbound upload. Runs the path through
211
+ * Brigade's outbound media-path guard and throws a clear operator-facing error
212
+ * when the guard refuses it (the `send_media` tool surfaces it). Returns the
213
+ * `{ path, name, caption? }` the connection turns into a discord.js
214
+ * `AttachmentBuilder`.
215
+ *
216
+ * Filename precedence: an explicit `media.fileName` wins; otherwise the path's
217
+ * basename. When the chosen name has NO extension we append one inferred from
218
+ * `media.kind` (image→png, video→mp4, audio→mp3, voice→ogg) so Discord detects
219
+ * the file type instead of treating it as an opaque blob (an extensionless
220
+ * `image` upload otherwise renders as a generic file, not an inline preview).
221
+ */
222
+ export function buildDiscordAttachment(media) {
223
+ const verdict = validateOutboundMediaPath(media.path);
224
+ if (!verdict.ok) {
225
+ throw new Error(`Discord: ${verdict.reason ?? "refusing to attach this file"}`);
226
+ }
227
+ let name = media.fileName || path.basename(media.path) || "file";
228
+ // No extension on the resolved name → infer one from the media kind so Discord
229
+ // type-detects the attachment. `path.extname` returns "" when there's no dot.
230
+ if (!path.extname(name)) {
231
+ const ext = outboundExtForKind(media.kind);
232
+ if (ext)
233
+ name = `${name}.${ext}`;
234
+ }
235
+ return {
236
+ path: media.path,
237
+ name,
238
+ ...(media.caption ? { caption: media.caption } : {}),
239
+ };
240
+ }
241
+ export { MAX_BYTES as DISCORD_MEDIA_MAX_BYTES };
242
+ //# sourceMappingURL=media.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media.js","sourceRoot":"","sources":["../../../../src/agents/channels/discord/media.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Discord extension module.
3
+ *
4
+ * Registers the Discord channel adapter through the seam. The loader gates it by
5
+ * the usual extension config (`extensions.disabled` / `entries`), and the
6
+ * adapter itself only starts when `channels.discord.enabled` is true AND a bot
7
+ * token resolves — so bundling this module is inert until the operator opts in.
8
+ *
9
+ * Unlike Slack's module, Discord registers NO gateway HTTP route: the Gateway
10
+ * (WebSocket) is the only inbound transport (no public URL needed, analogous to
11
+ * Slack Socket Mode / Telegram long-polling), so there is no events-mode webhook
12
+ * to wire. Discord mirror of `telegram/module.ts` (the polling-only shape).
13
+ */
14
+ export declare const discordModule: import("../sdk.js").BrigadeModule;
15
+ //# sourceMappingURL=module.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../../../../src/agents/channels/discord/module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,eAAO,MAAM,aAAa,mCAKxB,CAAC"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Discord extension module.
3
+ *
4
+ * Registers the Discord channel adapter through the seam. The loader gates it by
5
+ * the usual extension config (`extensions.disabled` / `entries`), and the
6
+ * adapter itself only starts when `channels.discord.enabled` is true AND a bot
7
+ * token resolves — so bundling this module is inert until the operator opts in.
8
+ *
9
+ * Unlike Slack's module, Discord registers NO gateway HTTP route: the Gateway
10
+ * (WebSocket) is the only inbound transport (no public URL needed, analogous to
11
+ * Slack Socket Mode / Telegram long-polling), so there is no events-mode webhook
12
+ * to wire. Discord mirror of `telegram/module.ts` (the polling-only shape).
13
+ */
14
+ import { defineModule } from "../sdk.js";
15
+ import { createDiscordAdapter } from "./adapter.js";
16
+ export const discordModule = defineModule({
17
+ id: "discord",
18
+ register(b) {
19
+ b.channel(createDiscordAdapter());
20
+ },
21
+ });
22
+ //# sourceMappingURL=module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"module.js","sourceRoot":"","sources":["../../../../src/agents/channels/discord/module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEpD,MAAM,CAAC,MAAM,aAAa,GAAG,YAAY,CAAC;IACzC,EAAE,EAAE,SAAS;IACb,QAAQ,CAAC,CAAC;QACT,CAAC,CAAC,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAAC;IACnC,CAAC;CACD,CAAC,CAAC"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Discord `ChannelPlugin` — the multi-ACCOUNT contract surface.
3
+ *
4
+ * Mirrors `slack/plugin.ts`: wraps `createDiscordAdapter()` (the per-connection
5
+ * implementation) with the lifecycle adapters the `ChannelPluginManager`
6
+ * consumes, so an operator can run MORE THAN ONE Discord bot at once via:
7
+ *
8
+ * channels.discord = {
9
+ * enabled: true,
10
+ * accounts: [
11
+ * { id: "main", botToken: "…AAA" },
12
+ * { id: "labs", botToken: "…BBB" },
13
+ * ],
14
+ * }
15
+ *
16
+ * - `config.listAccountIds` / `resolveAccount` → multi-account discovery
17
+ * - `gateway.startAccount` / `stopAccount` → per-account bot lifecycle
18
+ * - `outbound.sendText` / `sendMedia` → routes by `target.accountId`
19
+ * - per-account approval-dispatcher registration → an exec-gate prompt raised
20
+ * by a turn on (discord, labs) replies on (discord, labs), not the default
21
+ *
22
+ * Per-account state lives in a `Map<accountId, AccountRuntime>` held in this
23
+ * closure — one Gateway connection per account, partitioned token resolution per
24
+ * `channels.discord.accounts[].botToken`. Inbound dispatch reuses the shared
25
+ * `runChannelInboundPipeline` so the multi-account path carries the identical
26
+ * ACL + debounce + abort + approval-reply + approval-callback surface as the
27
+ * legacy single-adapter manager.
28
+ *
29
+ * The legacy single-account `createDiscordAdapter` (started by the legacy
30
+ * `startChannels` manager) STEPS ASIDE when >1 account is configured — its
31
+ * `isConfigured` returns false for the default account in that case (mirrors
32
+ * Slack), so the two paths never double-start a bot.
33
+ *
34
+ * Discord has no events-mode HTTP route (the Gateway is the only inbound
35
+ * transport), so — unlike Slack's plugin — there is NO per-account webhook-sink
36
+ * registry to populate.
37
+ */
38
+ import type { BrigadeConfig } from "../../../config/types.js";
39
+ import { type ChannelAdapter, type ChannelOutboundTarget, type ChannelPlugin, type StartChannelsArgs } from "../sdk.js";
40
+ import { type ResolvedDiscordAccount } from "./account-config.js";
41
+ import { type DiscordProbeResult } from "./probe.js";
42
+ /** Dependencies the gateway hands the plugin to drive turns + replies. */
43
+ export interface DiscordPluginDeps {
44
+ /** Boot-time default agent for routing fallbacks. */
45
+ defaultAgentId: string;
46
+ /** Active gateway config — re-read fresh per inbound for live policy. */
47
+ loadConfig: () => BrigadeConfig;
48
+ /** Run one agent turn (the gateway's serialised turn executor). */
49
+ runTurn: StartChannelsArgs["runTurn"];
50
+ /**
51
+ * Optional adapter factory — tests inject a fake; production uses
52
+ * `createDiscordAdapter`. Receives the per-account scope.
53
+ */
54
+ adapterFactory?: (args: {
55
+ accountId: string;
56
+ }) => ChannelAdapter;
57
+ }
58
+ /** Operator-grade view of a per-account bot — exposed via attached helpers. */
59
+ export interface DiscordPluginRuntimeView {
60
+ /** Currently-running account ids. */
61
+ startedAccountIds(): string[];
62
+ /** Look up the per-account adapter (or undefined when the account isn't started). */
63
+ getAdapter(accountId: string): ChannelAdapter | undefined;
64
+ /** Run a `/users/@me` probe for an account (for status / doctor). */
65
+ probeAccount(accountId: string, cfg: BrigadeConfig): Promise<DiscordProbeResult>;
66
+ }
67
+ /** Plugin handle with the extra per-account introspection surface attached. */
68
+ export type DiscordPluginHandle = ChannelPlugin<ResolvedDiscordAccount> & DiscordPluginRuntimeView;
69
+ /** Construct the plugin instance, capturing per-account runtime state in closure. */
70
+ export declare function createDiscordPlugin(deps: DiscordPluginDeps): DiscordPluginHandle;
71
+ /** Outbound dispatch helper for callers reaching the plugin directly. */
72
+ export type DiscordOutboundTarget = ChannelOutboundTarget;
73
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../../../src/agents/channels/discord/plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAK9D,OAAO,EAON,KAAK,cAAc,EAOnB,KAAK,qBAAqB,EAC1B,KAAK,aAAa,EAKlB,KAAK,iBAAiB,EAEtB,MAAM,WAAW,CAAC;AACnB,OAAO,EAMN,KAAK,sBAAsB,EAC3B,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAgB,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAkBnE,0EAA0E;AAC1E,MAAM,WAAW,iBAAiB;IACjC,qDAAqD;IACrD,cAAc,EAAE,MAAM,CAAC;IACvB,yEAAyE;IACzE,UAAU,EAAE,MAAM,aAAa,CAAC;IAChC,mEAAmE;IACnE,OAAO,EAAE,iBAAiB,CAAC,SAAS,CAAC,CAAC;IACtC;;;OAGG;IACH,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,KAAK,cAAc,CAAC;CACjE;AAED,+EAA+E;AAC/E,MAAM,WAAW,wBAAwB;IACxC,qCAAqC;IACrC,iBAAiB,IAAI,MAAM,EAAE,CAAC;IAC9B,qFAAqF;IACrF,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAAC;IAC1D,qEAAqE;IACrE,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;CACjF;AAED,+EAA+E;AAC/E,MAAM,MAAM,mBAAmB,GAAG,aAAa,CAAC,sBAAsB,CAAC,GAAG,wBAAwB,CAAC;AAqBnG,qFAAqF;AACrF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,iBAAiB,GAAG,mBAAmB,CA6NhF;AAOD,yEAAyE;AACzE,MAAM,MAAM,qBAAqB,GAAG,qBAAqB,CAAC"}