@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.
- package/dist/agents/agent-loop.d.ts.map +1 -1
- package/dist/agents/agent-loop.js +51 -1
- package/dist/agents/agent-loop.js.map +1 -1
- package/dist/agents/channels/bundled-channel-metas.d.ts +2 -0
- package/dist/agents/channels/bundled-channel-metas.d.ts.map +1 -1
- package/dist/agents/channels/bundled-channel-metas.js +11 -0
- package/dist/agents/channels/bundled-channel-metas.js.map +1 -1
- package/dist/agents/channels/discord/account-config.d.ts +117 -0
- package/dist/agents/channels/discord/account-config.d.ts.map +1 -0
- package/dist/agents/channels/discord/account-config.js +260 -0
- package/dist/agents/channels/discord/account-config.js.map +1 -0
- package/dist/agents/channels/discord/adapter.d.ts +56 -0
- package/dist/agents/channels/discord/adapter.d.ts.map +1 -0
- package/dist/agents/channels/discord/adapter.js +526 -0
- package/dist/agents/channels/discord/adapter.js.map +1 -0
- package/dist/agents/channels/discord/approval-authorize.d.ts +43 -0
- package/dist/agents/channels/discord/approval-authorize.d.ts.map +1 -0
- package/dist/agents/channels/discord/approval-authorize.js +71 -0
- package/dist/agents/channels/discord/approval-authorize.js.map +1 -0
- package/dist/agents/channels/discord/approval-native.d.ts +68 -0
- package/dist/agents/channels/discord/approval-native.d.ts.map +1 -0
- package/dist/agents/channels/discord/approval-native.js +81 -0
- package/dist/agents/channels/discord/approval-native.js.map +1 -0
- package/dist/agents/channels/discord/command-menu.d.ts +49 -0
- package/dist/agents/channels/discord/command-menu.d.ts.map +1 -0
- package/dist/agents/channels/discord/command-menu.js +73 -0
- package/dist/agents/channels/discord/command-menu.js.map +1 -0
- package/dist/agents/channels/discord/components.d.ts +97 -0
- package/dist/agents/channels/discord/components.d.ts.map +1 -0
- package/dist/agents/channels/discord/components.js +131 -0
- package/dist/agents/channels/discord/components.js.map +1 -0
- package/dist/agents/channels/discord/connection.d.ts +387 -0
- package/dist/agents/channels/discord/connection.d.ts.map +1 -0
- package/dist/agents/channels/discord/connection.js +786 -0
- package/dist/agents/channels/discord/connection.js.map +1 -0
- package/dist/agents/channels/discord/draft-stream.d.ts +92 -0
- package/dist/agents/channels/discord/draft-stream.d.ts.map +1 -0
- package/dist/agents/channels/discord/draft-stream.js +213 -0
- package/dist/agents/channels/discord/draft-stream.js.map +1 -0
- package/dist/agents/channels/discord/format.d.ts +55 -0
- package/dist/agents/channels/discord/format.d.ts.map +1 -0
- package/dist/agents/channels/discord/format.js +247 -0
- package/dist/agents/channels/discord/format.js.map +1 -0
- package/dist/agents/channels/discord/inbound-extras.d.ts +220 -0
- package/dist/agents/channels/discord/inbound-extras.d.ts.map +1 -0
- package/dist/agents/channels/discord/inbound-extras.js +351 -0
- package/dist/agents/channels/discord/inbound-extras.js.map +1 -0
- package/dist/agents/channels/discord/index.d.ts +14 -0
- package/dist/agents/channels/discord/index.d.ts.map +1 -0
- package/dist/agents/channels/discord/index.js +14 -0
- package/dist/agents/channels/discord/index.js.map +1 -0
- package/dist/agents/channels/discord/media.d.ts +85 -0
- package/dist/agents/channels/discord/media.d.ts.map +1 -0
- package/dist/agents/channels/discord/media.js +242 -0
- package/dist/agents/channels/discord/media.js.map +1 -0
- package/dist/agents/channels/discord/module.d.ts +15 -0
- package/dist/agents/channels/discord/module.d.ts.map +1 -0
- package/dist/agents/channels/discord/module.js +22 -0
- package/dist/agents/channels/discord/module.js.map +1 -0
- package/dist/agents/channels/discord/plugin.d.ts +73 -0
- package/dist/agents/channels/discord/plugin.d.ts.map +1 -0
- package/dist/agents/channels/discord/plugin.js +303 -0
- package/dist/agents/channels/discord/plugin.js.map +1 -0
- package/dist/agents/channels/discord/probe.d.ts +93 -0
- package/dist/agents/channels/discord/probe.d.ts.map +1 -0
- package/dist/agents/channels/discord/probe.js +158 -0
- package/dist/agents/channels/discord/probe.js.map +1 -0
- package/dist/agents/channels/discord/reasoning-lane.d.ts +42 -0
- package/dist/agents/channels/discord/reasoning-lane.d.ts.map +1 -0
- package/dist/agents/channels/discord/reasoning-lane.js +68 -0
- package/dist/agents/channels/discord/reasoning-lane.js.map +1 -0
- package/dist/agents/channels/inbound-pipeline.d.ts.map +1 -1
- package/dist/agents/channels/inbound-pipeline.js +65 -7
- package/dist/agents/channels/inbound-pipeline.js.map +1 -1
- package/dist/agents/channels/sdk.d.ts +2 -0
- package/dist/agents/channels/sdk.d.ts.map +1 -1
- package/dist/agents/channels/sdk.js +2 -0
- package/dist/agents/channels/sdk.js.map +1 -1
- package/dist/agents/extensions/modules/index.d.ts.map +1 -1
- package/dist/agents/extensions/modules/index.js +5 -0
- package/dist/agents/extensions/modules/index.js.map +1 -1
- package/dist/agents/tools/cron-tool.d.ts.map +1 -1
- package/dist/agents/tools/cron-tool.js +4 -1
- package/dist/agents/tools/cron-tool.js.map +1 -1
- package/dist/buildstamp.json +1 -1
- package/dist/core/auth-bridge.d.ts +1 -0
- package/dist/core/auth-bridge.d.ts.map +1 -1
- package/dist/core/auth-bridge.js +46 -1
- package/dist/core/auth-bridge.js.map +1 -1
- package/dist/core/server.d.ts.map +1 -1
- package/dist/core/server.js +18 -2
- package/dist/core/server.js.map +1 -1
- package/dist/cron/isolated-agent/run-executor.d.ts +11 -0
- package/dist/cron/isolated-agent/run-executor.d.ts.map +1 -1
- package/dist/cron/isolated-agent/run-executor.js +20 -4
- package/dist/cron/isolated-agent/run-executor.js.map +1 -1
- package/dist/cron/types.d.ts +8 -0
- package/dist/cron/types.d.ts.map +1 -1
- package/dist/system-prompt/assembler.d.ts.map +1 -1
- package/dist/system-prompt/assembler.js +4 -2
- package/dist/system-prompt/assembler.js.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord inline-approval authorization.
|
|
3
|
+
*
|
|
4
|
+
* When an approval prompt is rendered as buttons, ANY member who can see the
|
|
5
|
+
* message could press a button. Brigade's central inbound pipeline already runs
|
|
6
|
+
* the access-control gate before a button press reaches the approval-callback
|
|
7
|
+
* path, so only an admitted (allow-listed / owner) peer gets here at all — but a
|
|
8
|
+
* SHARED Discord guild channel is the edge case: in a channel the bot is in, an
|
|
9
|
+
* admitted member's button press should still be allowed only when that presser
|
|
10
|
+
* is an approved approver, not merely present in the room. Discord's
|
|
11
|
+
* multi-member guilds make this gate more load-bearing than a 1:1 DM.
|
|
12
|
+
*
|
|
13
|
+
* This predicate is the channel's `approvalCapability.authorizeApprover`. It is
|
|
14
|
+
* invoked CENTRALLY by `tryConsumeChannelApprovalCallback` with the presser's
|
|
15
|
+
* `senderId` (the Discord user id, a snowflake); returning `{ authorized: false,
|
|
16
|
+
* reason }` refuses the press without consuming the operator's pending approval
|
|
17
|
+
* (so the real operator can still answer). Policy:
|
|
18
|
+
*
|
|
19
|
+
* - When the channel has an explicit allow-from list configured (the approved
|
|
20
|
+
* senders), only those ids may approve.
|
|
21
|
+
* - When NO allow-from list is configured, defer to the access gate that
|
|
22
|
+
* already admitted the inbound and authorize the press (matches the text-
|
|
23
|
+
* reply path, which has no extra approver gate).
|
|
24
|
+
*
|
|
25
|
+
* Pure + deterministic over its `cfg` + `senderId` inputs — no I/O. Discord
|
|
26
|
+
* mirror of `slack/approval-authorize.ts`.
|
|
27
|
+
*/
|
|
28
|
+
/** Read the channel's configured allow-from sender ids (string-normalized). */
|
|
29
|
+
function configuredAllowFrom(cfg, accountId) {
|
|
30
|
+
const channels = cfg.channels;
|
|
31
|
+
const slot = channels?.discord;
|
|
32
|
+
if (!slot)
|
|
33
|
+
return [];
|
|
34
|
+
const ids = [];
|
|
35
|
+
const push = (list) => {
|
|
36
|
+
for (const v of list ?? []) {
|
|
37
|
+
const s = String(v).trim();
|
|
38
|
+
if (s)
|
|
39
|
+
ids.push(s);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
push(slot.allowFrom);
|
|
43
|
+
// Per-account allow-from (multi-account shape) when an accountId is supplied.
|
|
44
|
+
if (accountId && Array.isArray(slot.accounts)) {
|
|
45
|
+
for (const entry of slot.accounts) {
|
|
46
|
+
if (entry && typeof entry.id === "string" && entry.id.trim() === accountId.trim())
|
|
47
|
+
push(entry.allowFrom);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return ids;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Resolve whether `senderId` is allowed to answer a Discord inline approval.
|
|
54
|
+
* Returns `{ authorized: true }` when no explicit allow-from gate applies (the
|
|
55
|
+
* central access gate already admitted the inbound), or when the presser is on
|
|
56
|
+
* the configured allow-from list. Otherwise refuses with a reason.
|
|
57
|
+
*/
|
|
58
|
+
export function resolveDiscordApprover(args) {
|
|
59
|
+
const allow = configuredAllowFrom(args.cfg, args.accountId);
|
|
60
|
+
// No explicit allow list → defer to the access gate that already ran.
|
|
61
|
+
if (allow.length === 0)
|
|
62
|
+
return { authorized: true };
|
|
63
|
+
const sender = (args.senderId ?? "").trim();
|
|
64
|
+
if (sender && allow.includes(sender))
|
|
65
|
+
return { authorized: true };
|
|
66
|
+
return {
|
|
67
|
+
authorized: false,
|
|
68
|
+
reason: "Only an approved sender can answer that approval.",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=approval-authorize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approval-authorize.js","sourceRoot":"","sources":["../../../../src/agents/channels/discord/approval-authorize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAIH,+EAA+E;AAC/E,SAAS,mBAAmB,CAAC,GAAkB,EAAE,SAAkB;IAClE,MAAM,QAAQ,GAAI,GAA8C,CAAC,QAAQ,CAAC;IAC1E,MAAM,IAAI,GAAG,QAAQ,EAAE,OAEX,CAAC;IACb,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,CAAC;IACrB,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,MAAM,IAAI,GAAG,CAAC,IAA6B,EAAE,EAAE;QAC9C,KAAK,MAAM,CAAC,IAAI,IAAI,IAAI,EAAE,EAAE,CAAC;YAC5B,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YAC3B,IAAI,CAAC;gBAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACpB,CAAC;IACF,CAAC,CAAC;IACF,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACrB,8EAA8E;IAC9E,IAAI,SAAS,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC/C,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnC,IAAI,KAAK,IAAI,OAAO,KAAK,CAAC,EAAE,KAAK,QAAQ,IAAI,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,SAAS,CAAC,IAAI,EAAE;gBAAE,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAC1G,CAAC;IACF,CAAC;IACD,OAAO,GAAG,CAAC;AACZ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CAAC,IAItC;IACA,MAAM,KAAK,GAAG,mBAAmB,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IAC5D,sEAAsE;IACtE,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC;IACpD,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAC5C,IAAI,MAAM,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC;IAClE,OAAO;QACN,UAAU,EAAE,KAAK;QACjB,MAAM,EAAE,mDAAmD;KAC3D,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord message-component rendering for native approval prompts.
|
|
3
|
+
*
|
|
4
|
+
* When a channel-routed turn raises an exec/plugin approval AND the Discord
|
|
5
|
+
* adapter has opted into `approvalCapability.sendApprovalPrompt`, the central
|
|
6
|
+
* approval-router asks this channel to render the question as native BUTTONS
|
|
7
|
+
* instead of the default "reply yes/no" text card. The button payloads are
|
|
8
|
+
* produced by the CENTRAL codec (`buildApprovalCallbackButtons`) so the press
|
|
9
|
+
* comes back as an `InboundMessage.callbackQuery` the central
|
|
10
|
+
* `tryConsumeChannelApprovalCallback` decodes + resolves — this file only maps
|
|
11
|
+
* the codec's `{ label, data }` specs onto Discord's component-row shape (via
|
|
12
|
+
* `components.ts`) and assembles the prompt text.
|
|
13
|
+
*
|
|
14
|
+
* SAFETY: every button `custom_id` here is the codec's output — versioned,
|
|
15
|
+
* base64url + printable-ASCII, already proven `<= 64` UTF-8 bytes by
|
|
16
|
+
* `encodeApprovalCallback` (and well under Discord's 100-char custom_id cap).
|
|
17
|
+
* This module never mints its own payloads.
|
|
18
|
+
*
|
|
19
|
+
* Discord mirror of `slack/approval-native.ts`.
|
|
20
|
+
*/
|
|
21
|
+
import { type DiscordActionRow } from "./components.js";
|
|
22
|
+
/** The Discord message payload an approval prompt sends: prompt `text` + button `rows`. */
|
|
23
|
+
export interface DiscordApprovalMessage {
|
|
24
|
+
/** Prompt body rendered above the buttons. */
|
|
25
|
+
text: string;
|
|
26
|
+
/** The component rows (a row of up to 5 buttons; up to 5 rows). */
|
|
27
|
+
rows: DiscordActionRow[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Build the component approval message for an approval prompt from the central
|
|
31
|
+
* codec. Returns `null` when fewer than two byte-safe buttons could be minted (a
|
|
32
|
+
* pathologically long approval id) — the caller then falls back to the text
|
|
33
|
+
* prompt rather than ship a half-rendered prompt.
|
|
34
|
+
*
|
|
35
|
+
* `allowAlways: false` drops the "Allow always" button (approvals where
|
|
36
|
+
* persisting an allowlist entry doesn't apply).
|
|
37
|
+
*/
|
|
38
|
+
export declare function buildDiscordApprovalMessage(args: {
|
|
39
|
+
approvalId: string;
|
|
40
|
+
command: string;
|
|
41
|
+
approvalKind: "exec" | "plugin";
|
|
42
|
+
toolName?: string;
|
|
43
|
+
allowAlways?: boolean;
|
|
44
|
+
}): DiscordApprovalMessage | null;
|
|
45
|
+
/**
|
|
46
|
+
* Compose the operator-facing approval question text rendered ABOVE the buttons.
|
|
47
|
+
* Kept short + control-char-scrubbed; the buttons carry the action, so the text
|
|
48
|
+
* only needs the command preview + a one-line ask. The 🦁 mark is the Brigade
|
|
49
|
+
* brand-stamp so the operator recognises this as a Brigade prompt. The command
|
|
50
|
+
* preview is wrapped in a Discord inline code span (single backticks) so it
|
|
51
|
+
* renders verbatim.
|
|
52
|
+
*/
|
|
53
|
+
export declare function buildDiscordApprovalText(args: {
|
|
54
|
+
command: string;
|
|
55
|
+
approvalKind: "exec" | "plugin";
|
|
56
|
+
toolName?: string;
|
|
57
|
+
agentId?: string;
|
|
58
|
+
}): string;
|
|
59
|
+
/**
|
|
60
|
+
* Parse a pressed button's `custom_id` into the pending-approval id + decision,
|
|
61
|
+
* via the CENTRAL codec. Returns `null` when the press wasn't an approval button
|
|
62
|
+
* (a general button, or a foreign id) so the caller can fall through.
|
|
63
|
+
*/
|
|
64
|
+
export declare function parseDiscordApprovalAction(customId: string | undefined): {
|
|
65
|
+
approvalId: string;
|
|
66
|
+
decision: "allow-once" | "allow-always" | "deny";
|
|
67
|
+
} | null;
|
|
68
|
+
//# sourceMappingURL=approval-native.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approval-native.d.ts","sourceRoot":"","sources":["../../../../src/agents/channels/discord/approval-native.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,EAA4B,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAKlF,2FAA2F;AAC3F,MAAM,WAAW,sBAAsB;IACtC,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,IAAI,EAAE,gBAAgB,EAAE,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,GAAG,QAAQ,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,OAAO,CAAC;CACtB,GAAG,sBAAsB,GAAG,IAAI,CAchC;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,GAAG,QAAQ,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,MAAM,CAWT;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CACzC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC1B;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,YAAY,GAAG,cAAc,GAAG,MAAM,CAAA;CAAE,GAAG,IAAI,CAGjF"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord message-component rendering for native approval prompts.
|
|
3
|
+
*
|
|
4
|
+
* When a channel-routed turn raises an exec/plugin approval AND the Discord
|
|
5
|
+
* adapter has opted into `approvalCapability.sendApprovalPrompt`, the central
|
|
6
|
+
* approval-router asks this channel to render the question as native BUTTONS
|
|
7
|
+
* instead of the default "reply yes/no" text card. The button payloads are
|
|
8
|
+
* produced by the CENTRAL codec (`buildApprovalCallbackButtons`) so the press
|
|
9
|
+
* comes back as an `InboundMessage.callbackQuery` the central
|
|
10
|
+
* `tryConsumeChannelApprovalCallback` decodes + resolves — this file only maps
|
|
11
|
+
* the codec's `{ label, data }` specs onto Discord's component-row shape (via
|
|
12
|
+
* `components.ts`) and assembles the prompt text.
|
|
13
|
+
*
|
|
14
|
+
* SAFETY: every button `custom_id` here is the codec's output — versioned,
|
|
15
|
+
* base64url + printable-ASCII, already proven `<= 64` UTF-8 bytes by
|
|
16
|
+
* `encodeApprovalCallback` (and well under Discord's 100-char custom_id cap).
|
|
17
|
+
* This module never mints its own payloads.
|
|
18
|
+
*
|
|
19
|
+
* Discord mirror of `slack/approval-native.ts`.
|
|
20
|
+
*/
|
|
21
|
+
import { buildApprovalCallbackButtons, decodeApprovalCallback } from "../sdk.js";
|
|
22
|
+
import { buildDiscordApprovalRows } from "./components.js";
|
|
23
|
+
/** C0/C1 control-character class (incl. NUL) — never a raw control byte in source. */
|
|
24
|
+
const CONTROL_CHARS_RE = /[\x00-\x1f\x7f-\x9f]/g;
|
|
25
|
+
/**
|
|
26
|
+
* Build the component approval message for an approval prompt from the central
|
|
27
|
+
* codec. Returns `null` when fewer than two byte-safe buttons could be minted (a
|
|
28
|
+
* pathologically long approval id) — the caller then falls back to the text
|
|
29
|
+
* prompt rather than ship a half-rendered prompt.
|
|
30
|
+
*
|
|
31
|
+
* `allowAlways: false` drops the "Allow always" button (approvals where
|
|
32
|
+
* persisting an allowlist entry doesn't apply).
|
|
33
|
+
*/
|
|
34
|
+
export function buildDiscordApprovalMessage(args) {
|
|
35
|
+
const specs = buildApprovalCallbackButtons({
|
|
36
|
+
approvalId: args.approvalId,
|
|
37
|
+
...(args.allowAlways === false ? { allowAlways: false } : {}),
|
|
38
|
+
});
|
|
39
|
+
if (specs.length < 2)
|
|
40
|
+
return null; // not enough buttons → caller uses text prompt
|
|
41
|
+
const rows = buildDiscordApprovalRows(specs);
|
|
42
|
+
if (rows.length === 0)
|
|
43
|
+
return null;
|
|
44
|
+
const text = buildDiscordApprovalText({
|
|
45
|
+
command: args.command,
|
|
46
|
+
approvalKind: args.approvalKind,
|
|
47
|
+
...(args.toolName !== undefined ? { toolName: args.toolName } : {}),
|
|
48
|
+
});
|
|
49
|
+
return { text, rows };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Compose the operator-facing approval question text rendered ABOVE the buttons.
|
|
53
|
+
* Kept short + control-char-scrubbed; the buttons carry the action, so the text
|
|
54
|
+
* only needs the command preview + a one-line ask. The 🦁 mark is the Brigade
|
|
55
|
+
* brand-stamp so the operator recognises this as a Brigade prompt. The command
|
|
56
|
+
* preview is wrapped in a Discord inline code span (single backticks) so it
|
|
57
|
+
* renders verbatim.
|
|
58
|
+
*/
|
|
59
|
+
export function buildDiscordApprovalText(args) {
|
|
60
|
+
const flat = args.command
|
|
61
|
+
.replace(/[\r\n]+/g, " ")
|
|
62
|
+
// oxlint-disable-next-line no-control-regex
|
|
63
|
+
.replace(CONTROL_CHARS_RE, " ")
|
|
64
|
+
.replace(/\s+/g, " ")
|
|
65
|
+
.trim();
|
|
66
|
+
const preview = flat.length <= 180 ? flat : `${flat.slice(0, 177)}…`;
|
|
67
|
+
const what = args.approvalKind === "plugin" ? "run a plugin action" : "run a shell command";
|
|
68
|
+
const lines = [`🦁 Brigade wants to ${what}:`, `\`${preview}\``, "", "Choose below — times out in 5 minutes."];
|
|
69
|
+
return lines.join("\n");
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Parse a pressed button's `custom_id` into the pending-approval id + decision,
|
|
73
|
+
* via the CENTRAL codec. Returns `null` when the press wasn't an approval button
|
|
74
|
+
* (a general button, or a foreign id) so the caller can fall through.
|
|
75
|
+
*/
|
|
76
|
+
export function parseDiscordApprovalAction(customId) {
|
|
77
|
+
if (!customId)
|
|
78
|
+
return null;
|
|
79
|
+
return decodeApprovalCallback(customId);
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=approval-native.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approval-native.js","sourceRoot":"","sources":["../../../../src/agents/channels/discord/approval-native.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,4BAA4B,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AACjF,OAAO,EAAE,wBAAwB,EAAyB,MAAM,iBAAiB,CAAC;AAElF,sFAAsF;AACtF,MAAM,gBAAgB,GAAG,uBAAuB,CAAC;AAUjD;;;;;;;;GAQG;AACH,MAAM,UAAU,2BAA2B,CAAC,IAM3C;IACA,MAAM,KAAK,GAAG,4BAA4B,CAAC;QAC1C,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,GAAG,CAAC,IAAI,CAAC,WAAW,KAAK,KAAK,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC7D,CAAC,CAAC;IACH,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC,CAAC,+CAA+C;IAClF,MAAM,IAAI,GAAG,wBAAwB,CAAC,KAAK,CAAC,CAAC;IAC7C,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,MAAM,IAAI,GAAG,wBAAwB,CAAC;QACrC,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,YAAY,EAAE,IAAI,CAAC,YAAY;QAC/B,GAAG,CAAC,IAAI,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACnE,CAAC,CAAC;IACH,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;AACvB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,wBAAwB,CAAC,IAKxC;IACA,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO;SACvB,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC;QACzB,4CAA4C;SAC3C,OAAO,CAAC,gBAAgB,EAAE,GAAG,CAAC;SAC9B,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,IAAI,EAAE,CAAC;IACT,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC;IACrE,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,qBAAqB,CAAC;IAC5F,MAAM,KAAK,GAAG,CAAC,uBAAuB,IAAI,GAAG,EAAE,KAAK,OAAO,IAAI,EAAE,EAAE,EAAE,wCAAwC,CAAC,CAAC;IAC/G,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACzB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,0BAA0B,CACzC,QAA4B;IAE5B,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC3B,OAAO,sBAAsB,CAAC,QAAQ,CAAC,CAAC;AACzC,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord native command menu — map Brigade's central channel commands onto the
|
|
3
|
+
* application-command payload that Discord's REST `PUT /applications/{id}/commands`
|
|
4
|
+
* (or the per-guild variant) registers, so the operator sees `/help`, `/status`,
|
|
5
|
+
* … as native Discord slash commands.
|
|
6
|
+
*
|
|
7
|
+
* Brigade owns the command set centrally (`buildBundledCommands` → `/help`,
|
|
8
|
+
* `/status`, `/allowlist`, `/agent`, `/agents`, `/whoami`, `/org`, plus any
|
|
9
|
+
* module-registered channel commands). Discord — like Telegram's `setMyCommands`
|
|
10
|
+
* but UNLIKE Slack (whose slash commands are configured by hand in the app UI) —
|
|
11
|
+
* accepts a programmatic registration, so on connect the connection PUTs this
|
|
12
|
+
* manifest. (Discord slash commands carry no positional args by default, so the
|
|
13
|
+
* agent receives `/status` etc. as a bare command; an inbound interaction is
|
|
14
|
+
* normalized back into a `/command` text turn.)
|
|
15
|
+
*
|
|
16
|
+
* Discord's constraints (enforced here so a malformed command never makes the
|
|
17
|
+
* whole `PUT` reject):
|
|
18
|
+
* - command name: 1–32 chars, lowercase, `[a-z0-9_-]` only (leading `/` stripped).
|
|
19
|
+
* - description: 1–100 chars (clamped; a non-empty fallback is always emitted
|
|
20
|
+
* since Discord rejects an empty description).
|
|
21
|
+
* - at most 100 global commands.
|
|
22
|
+
* - CHAT_INPUT command type = 1.
|
|
23
|
+
*
|
|
24
|
+
* Pure / deterministic — no I/O. Output command names are printable ASCII
|
|
25
|
+
* (`[a-z0-9_-]`), so no NUL / control byte can appear.
|
|
26
|
+
*/
|
|
27
|
+
import type { ChannelCommand } from "../sdk.js";
|
|
28
|
+
/** A Discord application-command (CHAT_INPUT) registration entry. */
|
|
29
|
+
export interface DiscordApplicationCommand {
|
|
30
|
+
/** Command WITHOUT the leading slash, e.g. `status`. */
|
|
31
|
+
name: string;
|
|
32
|
+
/** Short description shown in the Discord command hint (1–100 chars). */
|
|
33
|
+
description: string;
|
|
34
|
+
/** CHAT_INPUT command type (1). */
|
|
35
|
+
type: 1;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Normalize a command word to Discord's `[a-z0-9_-]{1,32}` shape, or null if
|
|
39
|
+
* unusable. Strips a leading `/`, lowercases, and drops disallowed chars.
|
|
40
|
+
*/
|
|
41
|
+
export declare function normalizeDiscordCommandName(raw: string): string | null;
|
|
42
|
+
/**
|
|
43
|
+
* Build the Discord application-command manifest from Brigade's central channel
|
|
44
|
+
* commands. De-dupes by normalized name (first wins), drops unusable names, and
|
|
45
|
+
* caps at Discord's 100-command ceiling. Returns `[]` when nothing maps (the
|
|
46
|
+
* connection then skips the REST `PUT` entirely).
|
|
47
|
+
*/
|
|
48
|
+
export declare function buildDiscordCommandManifest(commands: ReadonlyArray<ChannelCommand>): DiscordApplicationCommand[];
|
|
49
|
+
//# sourceMappingURL=command-menu.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command-menu.d.ts","sourceRoot":"","sources":["../../../../src/agents/channels/discord/command-menu.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEhD,qEAAqE;AACrE,MAAM,WAAW,yBAAyB;IACzC,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,WAAW,EAAE,MAAM,CAAC;IACpB,mCAAmC;IACnC,IAAI,EAAE,CAAC,CAAC;CACR;AAWD;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKtE;AAUD;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,QAAQ,EAAE,aAAa,CAAC,cAAc,CAAC,GAAG,yBAAyB,EAAE,CAWhH"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord native command menu — map Brigade's central channel commands onto the
|
|
3
|
+
* application-command payload that Discord's REST `PUT /applications/{id}/commands`
|
|
4
|
+
* (or the per-guild variant) registers, so the operator sees `/help`, `/status`,
|
|
5
|
+
* … as native Discord slash commands.
|
|
6
|
+
*
|
|
7
|
+
* Brigade owns the command set centrally (`buildBundledCommands` → `/help`,
|
|
8
|
+
* `/status`, `/allowlist`, `/agent`, `/agents`, `/whoami`, `/org`, plus any
|
|
9
|
+
* module-registered channel commands). Discord — like Telegram's `setMyCommands`
|
|
10
|
+
* but UNLIKE Slack (whose slash commands are configured by hand in the app UI) —
|
|
11
|
+
* accepts a programmatic registration, so on connect the connection PUTs this
|
|
12
|
+
* manifest. (Discord slash commands carry no positional args by default, so the
|
|
13
|
+
* agent receives `/status` etc. as a bare command; an inbound interaction is
|
|
14
|
+
* normalized back into a `/command` text turn.)
|
|
15
|
+
*
|
|
16
|
+
* Discord's constraints (enforced here so a malformed command never makes the
|
|
17
|
+
* whole `PUT` reject):
|
|
18
|
+
* - command name: 1–32 chars, lowercase, `[a-z0-9_-]` only (leading `/` stripped).
|
|
19
|
+
* - description: 1–100 chars (clamped; a non-empty fallback is always emitted
|
|
20
|
+
* since Discord rejects an empty description).
|
|
21
|
+
* - at most 100 global commands.
|
|
22
|
+
* - CHAT_INPUT command type = 1.
|
|
23
|
+
*
|
|
24
|
+
* Pure / deterministic — no I/O. Output command names are printable ASCII
|
|
25
|
+
* (`[a-z0-9_-]`), so no NUL / control byte can appear.
|
|
26
|
+
*/
|
|
27
|
+
/** Discord application-command limits. */
|
|
28
|
+
const MAX_COMMANDS = 100;
|
|
29
|
+
const MAX_NAME_LEN = 32;
|
|
30
|
+
const MAX_DESC_LEN = 100;
|
|
31
|
+
/** Discord allows lowercase `[a-z0-9_-]` (1–32) in a CHAT_INPUT command name. */
|
|
32
|
+
const COMMAND_NAME_RE = /^[a-z0-9_-]{1,32}$/;
|
|
33
|
+
/** CHAT_INPUT application-command type. */
|
|
34
|
+
const CHAT_INPUT = 1;
|
|
35
|
+
/**
|
|
36
|
+
* Normalize a command word to Discord's `[a-z0-9_-]{1,32}` shape, or null if
|
|
37
|
+
* unusable. Strips a leading `/`, lowercases, and drops disallowed chars.
|
|
38
|
+
*/
|
|
39
|
+
export function normalizeDiscordCommandName(raw) {
|
|
40
|
+
const stripped = raw.trim().replace(/^\/+/, "").toLowerCase();
|
|
41
|
+
const cleaned = stripped.replace(/[^a-z0-9_-]/g, "").slice(0, MAX_NAME_LEN);
|
|
42
|
+
if (!cleaned || !COMMAND_NAME_RE.test(cleaned))
|
|
43
|
+
return null;
|
|
44
|
+
return cleaned;
|
|
45
|
+
}
|
|
46
|
+
/** Clamp + flatten a description to a single printable line within Discord's cap (never empty). */
|
|
47
|
+
function normalizeDescription(desc, fallback) {
|
|
48
|
+
const raw = (desc ?? "").replace(/\s+/g, " ").trim() || fallback;
|
|
49
|
+
const clamped = raw.length > MAX_DESC_LEN ? `${raw.slice(0, MAX_DESC_LEN - 1)}…` : raw;
|
|
50
|
+
// Discord rejects an empty description — guarantee at least the fallback name.
|
|
51
|
+
return clamped || fallback;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Build the Discord application-command manifest from Brigade's central channel
|
|
55
|
+
* commands. De-dupes by normalized name (first wins), drops unusable names, and
|
|
56
|
+
* caps at Discord's 100-command ceiling. Returns `[]` when nothing maps (the
|
|
57
|
+
* connection then skips the REST `PUT` entirely).
|
|
58
|
+
*/
|
|
59
|
+
export function buildDiscordCommandManifest(commands) {
|
|
60
|
+
const out = [];
|
|
61
|
+
const seen = new Set();
|
|
62
|
+
for (const cmd of commands) {
|
|
63
|
+
if (out.length >= MAX_COMMANDS)
|
|
64
|
+
break;
|
|
65
|
+
const name = normalizeDiscordCommandName(cmd.name);
|
|
66
|
+
if (!name || seen.has(name))
|
|
67
|
+
continue;
|
|
68
|
+
seen.add(name);
|
|
69
|
+
out.push({ name, description: normalizeDescription(cmd.description, name), type: CHAT_INPUT });
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=command-menu.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command-menu.js","sourceRoot":"","sources":["../../../../src/agents/channels/discord/command-menu.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAcH,0CAA0C;AAC1C,MAAM,YAAY,GAAG,GAAG,CAAC;AACzB,MAAM,YAAY,GAAG,EAAE,CAAC;AACxB,MAAM,YAAY,GAAG,GAAG,CAAC;AACzB,iFAAiF;AACjF,MAAM,eAAe,GAAG,oBAAoB,CAAC;AAC7C,2CAA2C;AAC3C,MAAM,UAAU,GAAG,CAAU,CAAC;AAE9B;;;GAGG;AACH,MAAM,UAAU,2BAA2B,CAAC,GAAW;IACtD,MAAM,QAAQ,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IAC9D,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,YAAY,CAAC,CAAC;IAC5E,IAAI,CAAC,OAAO,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5D,OAAO,OAAO,CAAC;AAChB,CAAC;AAED,mGAAmG;AACnG,SAAS,oBAAoB,CAAC,IAAwB,EAAE,QAAgB;IACvE,MAAM,GAAG,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,IAAI,QAAQ,CAAC;IACjE,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,YAAY,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;IACvF,+EAA+E;IAC/E,OAAO,OAAO,IAAI,QAAQ,CAAC;AAC5B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,2BAA2B,CAAC,QAAuC;IAClF,MAAM,GAAG,GAAgC,EAAE,CAAC;IAC5C,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;QAC5B,IAAI,GAAG,CAAC,MAAM,IAAI,YAAY;YAAE,MAAM;QACtC,MAAM,IAAI,GAAG,2BAA2B,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACnD,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,SAAS;QACtC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACf,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,oBAAoB,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;IAChG,CAAC;IACD,OAAO,GAAG,CAAC;AACZ,CAAC"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord message-component button rendering + the custom_id callback codec.
|
|
3
|
+
*
|
|
4
|
+
* Discord's analogue of `slack/blocks.ts`. Two button lanes share one builder,
|
|
5
|
+
* exactly as Slack's do:
|
|
6
|
+
*
|
|
7
|
+
* - APPROVAL buttons — payloads come from the CENTRAL approval codec
|
|
8
|
+
* (`buildApprovalCallbackButtons`), so a press resolves through the central
|
|
9
|
+
* `tryConsumeChannelApprovalCallback`. Built in `approval-native.ts`; this
|
|
10
|
+
* module only supplies the low-level row + button shapes.
|
|
11
|
+
* - GENERAL buttons — the agent attaches arbitrary buttons via the
|
|
12
|
+
* `message_action` `buttons` kind; a press routes back through the inbound
|
|
13
|
+
* pipeline as a normal turn. Every general payload is namespaced with
|
|
14
|
+
* {@link GENERAL_CALLBACK_PREFIX} so the pipeline can tell it apart from an
|
|
15
|
+
* approval press (which it consumes FIRST).
|
|
16
|
+
*
|
|
17
|
+
* WHERE THE PAYLOAD RIDES. A Discord button carries its opaque codec payload in
|
|
18
|
+
* `custom_id` (Discord caps it at 100 chars). The central approval codec already
|
|
19
|
+
* guarantees ≤64 UTF-8 bytes, so an approval payload always fits; a general
|
|
20
|
+
* payload that exceeds the budget after prefixing is DROPPED (rather than ship a
|
|
21
|
+
* truncated id that decodes to the wrong action on press). On press, discord.js
|
|
22
|
+
* delivers the pressed `interaction.customId` verbatim, which the central
|
|
23
|
+
* pipeline decodes — there is no separate routing id (unlike Slack's action_id).
|
|
24
|
+
*
|
|
25
|
+
* These builders emit PLAIN serializable specs (a `DiscordButtonSpec` grid); the
|
|
26
|
+
* connection turns each into a discord.js `ActionRowBuilder<ButtonBuilder>` at
|
|
27
|
+
* send time, so this module stays pure + dependency-light + unit-testable
|
|
28
|
+
* without importing discord.js.
|
|
29
|
+
*
|
|
30
|
+
* Pure / deterministic — no I/O, no globals.
|
|
31
|
+
*/
|
|
32
|
+
/** Discord caps a component `custom_id` at 100 chars. */
|
|
33
|
+
export declare const DISCORD_CUSTOM_ID_MAX_CHARS = 100;
|
|
34
|
+
/** Discord allows at most 5 buttons per action row. */
|
|
35
|
+
export declare const DISCORD_BUTTONS_PER_ROW = 5;
|
|
36
|
+
/** Discord allows at most 5 action rows per message. */
|
|
37
|
+
export declare const DISCORD_MAX_ROWS = 5;
|
|
38
|
+
/** Discord ButtonStyle enum values (mirrors discord.js `ButtonStyle`). */
|
|
39
|
+
export declare const DISCORD_BUTTON_STYLE: {
|
|
40
|
+
readonly Primary: 1;
|
|
41
|
+
readonly Secondary: 2;
|
|
42
|
+
readonly Success: 3;
|
|
43
|
+
readonly Danger: 4;
|
|
44
|
+
};
|
|
45
|
+
export type DiscordButtonStyleValue = (typeof DISCORD_BUTTON_STYLE)[keyof typeof DISCORD_BUTTON_STYLE];
|
|
46
|
+
/** A single serializable Discord button spec (the connection turns it into a ButtonBuilder). */
|
|
47
|
+
export interface DiscordButtonSpec {
|
|
48
|
+
/** Button label shown to the user (≤ 80 chars). */
|
|
49
|
+
label: string;
|
|
50
|
+
/** Opaque codec payload carried in `custom_id` (≤ 100 chars), read back on press. */
|
|
51
|
+
customId: string;
|
|
52
|
+
/** Discord button style (default Secondary). */
|
|
53
|
+
style: DiscordButtonStyleValue;
|
|
54
|
+
}
|
|
55
|
+
/** One action row — up to 5 buttons. */
|
|
56
|
+
export type DiscordActionRow = DiscordButtonSpec[];
|
|
57
|
+
/** One button spec before it's shaped + validated. */
|
|
58
|
+
export interface DiscordButtonInput {
|
|
59
|
+
/** Button label shown to the user. */
|
|
60
|
+
text: string;
|
|
61
|
+
/** Opaque codec payload delivered to the handler on press. */
|
|
62
|
+
value: string;
|
|
63
|
+
/** Optional Discord button style. */
|
|
64
|
+
style?: DiscordButtonStyleValue;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Strip control bytes from a custom_id. The central codec already guarantees a
|
|
68
|
+
* printable, short payload, so this is purely defensive for a value that didn't
|
|
69
|
+
* come from the codec — and we never truncate a payload (we DROP an over-budget
|
|
70
|
+
* one at the call site), since a truncated id would decode to the wrong action.
|
|
71
|
+
*/
|
|
72
|
+
export declare function sanitizeDiscordCustomId(value: string): string;
|
|
73
|
+
/**
|
|
74
|
+
* Build the action rows for an APPROVAL prompt from the central codec's
|
|
75
|
+
* `{ label, data, decision }` specs (the payload rides in `custom_id`). Returns
|
|
76
|
+
* `[]` when fewer than two byte-safe buttons could be shaped — the caller then
|
|
77
|
+
* falls back to a text prompt rather than ship a half-rendered prompt. `Deny` is
|
|
78
|
+
* styled Danger; the first (allow-once) button Success.
|
|
79
|
+
*/
|
|
80
|
+
export declare function buildDiscordApprovalRows(specs: ReadonlyArray<{
|
|
81
|
+
label: string;
|
|
82
|
+
data: string;
|
|
83
|
+
decision?: string;
|
|
84
|
+
}>): DiscordActionRow[];
|
|
85
|
+
/**
|
|
86
|
+
* Build the action rows for a GENERAL button keyboard from a grid of specs. Each
|
|
87
|
+
* spec's `data` is prefixed with {@link GENERAL_CALLBACK_PREFIX} + sanitized; a
|
|
88
|
+
* button whose prefixed id exceeds the budget OR whose label is empty is
|
|
89
|
+
* DROPPED. Returns `null` when no usable button remains (the caller then sends a
|
|
90
|
+
* plain message instead). The grid is flattened + re-chunked into Discord's
|
|
91
|
+
* 5-per-row / 5-row limits.
|
|
92
|
+
*/
|
|
93
|
+
export declare function buildDiscordButtonRows(grid: Array<Array<{
|
|
94
|
+
text: string;
|
|
95
|
+
data: string;
|
|
96
|
+
}>>): DiscordActionRow[] | null;
|
|
97
|
+
//# sourceMappingURL=components.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../../../../src/agents/channels/discord/components.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAIH,yDAAyD;AACzD,eAAO,MAAM,2BAA2B,MAAM,CAAC;AAE/C,uDAAuD;AACvD,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAEzC,wDAAwD;AACxD,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAKlC,0EAA0E;AAC1E,eAAO,MAAM,oBAAoB;;;;;CAKvB,CAAC;AAEX,MAAM,MAAM,uBAAuB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,OAAO,oBAAoB,CAAC,CAAC;AAEvG,gGAAgG;AAChG,MAAM,WAAW,iBAAiB;IACjC,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,qFAAqF;IACrF,QAAQ,EAAE,MAAM,CAAC;IACjB,gDAAgD;IAChD,KAAK,EAAE,uBAAuB,CAAC;CAC/B;AAED,wCAAwC;AACxC,MAAM,MAAM,gBAAgB,GAAG,iBAAiB,EAAE,CAAC;AAEnD,sDAAsD;AACtD,MAAM,WAAW,kBAAkB;IAClC,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,8DAA8D;IAC9D,KAAK,EAAE,MAAM,CAAC;IACd,qCAAqC;IACrC,KAAK,CAAC,EAAE,uBAAuB,CAAC;CAChC;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAG7D;AA0BD;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACvC,KAAK,EAAE,aAAa,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GACtE,gBAAgB,EAAE,CAcpB;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,GAAG,gBAAgB,EAAE,GAAG,IAAI,CAcpH"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord message-component button rendering + the custom_id callback codec.
|
|
3
|
+
*
|
|
4
|
+
* Discord's analogue of `slack/blocks.ts`. Two button lanes share one builder,
|
|
5
|
+
* exactly as Slack's do:
|
|
6
|
+
*
|
|
7
|
+
* - APPROVAL buttons — payloads come from the CENTRAL approval codec
|
|
8
|
+
* (`buildApprovalCallbackButtons`), so a press resolves through the central
|
|
9
|
+
* `tryConsumeChannelApprovalCallback`. Built in `approval-native.ts`; this
|
|
10
|
+
* module only supplies the low-level row + button shapes.
|
|
11
|
+
* - GENERAL buttons — the agent attaches arbitrary buttons via the
|
|
12
|
+
* `message_action` `buttons` kind; a press routes back through the inbound
|
|
13
|
+
* pipeline as a normal turn. Every general payload is namespaced with
|
|
14
|
+
* {@link GENERAL_CALLBACK_PREFIX} so the pipeline can tell it apart from an
|
|
15
|
+
* approval press (which it consumes FIRST).
|
|
16
|
+
*
|
|
17
|
+
* WHERE THE PAYLOAD RIDES. A Discord button carries its opaque codec payload in
|
|
18
|
+
* `custom_id` (Discord caps it at 100 chars). The central approval codec already
|
|
19
|
+
* guarantees ≤64 UTF-8 bytes, so an approval payload always fits; a general
|
|
20
|
+
* payload that exceeds the budget after prefixing is DROPPED (rather than ship a
|
|
21
|
+
* truncated id that decodes to the wrong action on press). On press, discord.js
|
|
22
|
+
* delivers the pressed `interaction.customId` verbatim, which the central
|
|
23
|
+
* pipeline decodes — there is no separate routing id (unlike Slack's action_id).
|
|
24
|
+
*
|
|
25
|
+
* These builders emit PLAIN serializable specs (a `DiscordButtonSpec` grid); the
|
|
26
|
+
* connection turns each into a discord.js `ActionRowBuilder<ButtonBuilder>` at
|
|
27
|
+
* send time, so this module stays pure + dependency-light + unit-testable
|
|
28
|
+
* without importing discord.js.
|
|
29
|
+
*
|
|
30
|
+
* Pure / deterministic — no I/O, no globals.
|
|
31
|
+
*/
|
|
32
|
+
import { GENERAL_CALLBACK_PREFIX } from "../general-callback.js";
|
|
33
|
+
/** Discord caps a component `custom_id` at 100 chars. */
|
|
34
|
+
export const DISCORD_CUSTOM_ID_MAX_CHARS = 100;
|
|
35
|
+
/** Discord allows at most 5 buttons per action row. */
|
|
36
|
+
export const DISCORD_BUTTONS_PER_ROW = 5;
|
|
37
|
+
/** Discord allows at most 5 action rows per message. */
|
|
38
|
+
export const DISCORD_MAX_ROWS = 5;
|
|
39
|
+
/** Discord button label cap (chars). */
|
|
40
|
+
const DISCORD_BUTTON_LABEL_MAX = 80;
|
|
41
|
+
/** Discord ButtonStyle enum values (mirrors discord.js `ButtonStyle`). */
|
|
42
|
+
export const DISCORD_BUTTON_STYLE = {
|
|
43
|
+
Primary: 1,
|
|
44
|
+
Secondary: 2,
|
|
45
|
+
Success: 3,
|
|
46
|
+
Danger: 4,
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Strip control bytes from a custom_id. The central codec already guarantees a
|
|
50
|
+
* printable, short payload, so this is purely defensive for a value that didn't
|
|
51
|
+
* come from the codec — and we never truncate a payload (we DROP an over-budget
|
|
52
|
+
* one at the call site), since a truncated id would decode to the wrong action.
|
|
53
|
+
*/
|
|
54
|
+
export function sanitizeDiscordCustomId(value) {
|
|
55
|
+
// oxlint-disable-next-line no-control-regex
|
|
56
|
+
return value.replace(/[\x00-\x1f\x7f-\x9f]/g, "");
|
|
57
|
+
}
|
|
58
|
+
/** Shape one button input into a validated spec, or null when it can't fit. */
|
|
59
|
+
function toButton(input) {
|
|
60
|
+
const label = (input?.text ?? "").trim();
|
|
61
|
+
const customId = sanitizeDiscordCustomId(input?.value ?? "");
|
|
62
|
+
if (!label || !customId)
|
|
63
|
+
return null;
|
|
64
|
+
// Reject (don't truncate) an id that won't fit — a truncated payload would
|
|
65
|
+
// decode to the wrong action on press.
|
|
66
|
+
if (customId.length > DISCORD_CUSTOM_ID_MAX_CHARS)
|
|
67
|
+
return null;
|
|
68
|
+
return {
|
|
69
|
+
label: label.slice(0, DISCORD_BUTTON_LABEL_MAX),
|
|
70
|
+
customId,
|
|
71
|
+
style: input.style ?? DISCORD_BUTTON_STYLE.Secondary,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/** Group a flat button list into rows of at most 5, capped at 5 rows total. */
|
|
75
|
+
function chunkIntoRows(buttons) {
|
|
76
|
+
const rows = [];
|
|
77
|
+
for (let i = 0; i < buttons.length && rows.length < DISCORD_MAX_ROWS; i += DISCORD_BUTTONS_PER_ROW) {
|
|
78
|
+
rows.push(buttons.slice(i, i + DISCORD_BUTTONS_PER_ROW));
|
|
79
|
+
}
|
|
80
|
+
return rows;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Build the action rows for an APPROVAL prompt from the central codec's
|
|
84
|
+
* `{ label, data, decision }` specs (the payload rides in `custom_id`). Returns
|
|
85
|
+
* `[]` when fewer than two byte-safe buttons could be shaped — the caller then
|
|
86
|
+
* falls back to a text prompt rather than ship a half-rendered prompt. `Deny` is
|
|
87
|
+
* styled Danger; the first (allow-once) button Success.
|
|
88
|
+
*/
|
|
89
|
+
export function buildDiscordApprovalRows(specs) {
|
|
90
|
+
const buttons = [];
|
|
91
|
+
for (const s of specs) {
|
|
92
|
+
const style = s.decision === "deny"
|
|
93
|
+
? DISCORD_BUTTON_STYLE.Danger
|
|
94
|
+
: s.decision === "allow-once"
|
|
95
|
+
? DISCORD_BUTTON_STYLE.Success
|
|
96
|
+
: DISCORD_BUTTON_STYLE.Secondary;
|
|
97
|
+
const btn = toButton({ text: s.label, value: s.data, style });
|
|
98
|
+
if (btn)
|
|
99
|
+
buttons.push(btn);
|
|
100
|
+
}
|
|
101
|
+
if (buttons.length < 2)
|
|
102
|
+
return []; // not enough buttons → caller uses text prompt
|
|
103
|
+
return chunkIntoRows(buttons);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Build the action rows for a GENERAL button keyboard from a grid of specs. Each
|
|
107
|
+
* spec's `data` is prefixed with {@link GENERAL_CALLBACK_PREFIX} + sanitized; a
|
|
108
|
+
* button whose prefixed id exceeds the budget OR whose label is empty is
|
|
109
|
+
* DROPPED. Returns `null` when no usable button remains (the caller then sends a
|
|
110
|
+
* plain message instead). The grid is flattened + re-chunked into Discord's
|
|
111
|
+
* 5-per-row / 5-row limits.
|
|
112
|
+
*/
|
|
113
|
+
export function buildDiscordButtonRows(grid) {
|
|
114
|
+
const buttons = [];
|
|
115
|
+
for (const row of grid) {
|
|
116
|
+
for (const spec of row) {
|
|
117
|
+
const label = (spec?.text ?? "").trim();
|
|
118
|
+
const token = spec?.data ?? "";
|
|
119
|
+
if (!label || !token)
|
|
120
|
+
continue;
|
|
121
|
+
const prefixed = `${GENERAL_CALLBACK_PREFIX}${token}`;
|
|
122
|
+
const btn = toButton({ text: label, value: prefixed });
|
|
123
|
+
if (btn)
|
|
124
|
+
buttons.push(btn);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (buttons.length === 0)
|
|
128
|
+
return null;
|
|
129
|
+
return chunkIntoRows(buttons);
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=components.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"components.js","sourceRoot":"","sources":["../../../../src/agents/channels/discord/components.ts"],"names":[],"mappings":"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"}
|