experimental-ash 0.25.1 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/bin/ash.d.ts +4 -4
  3. package/bin/ash.js +12 -8
  4. package/dist/docs/public/channels/README.md +26 -2
  5. package/dist/docs/public/channels/discord.md +159 -0
  6. package/dist/docs/public/channels/slack.md +14 -2
  7. package/dist/src/channel/routes.d.ts +6 -1
  8. package/dist/src/channel/send.js +5 -2
  9. package/dist/src/channel/session-callback.d.ts +10 -0
  10. package/dist/src/channel/session-callback.js +65 -0
  11. package/dist/src/channel/types.d.ts +19 -0
  12. package/dist/src/chunks/{client-BShLWzR6.js → client-ZqNLLMZB.js} +3 -3
  13. package/dist/src/chunks/{compile-agent-CyP6FrL8.js → compile-agent-DrIyb818.js} +1 -1
  14. package/dist/src/chunks/{dev-authored-source-watcher-DIWfVUsu.js → dev-authored-source-watcher-C1WUVv9F.js} +1 -1
  15. package/dist/src/chunks/host-CwAcCrg7.js +70 -0
  16. package/dist/src/chunks/paths-CWZN-XRX.js +85 -0
  17. package/dist/src/chunks/{token-BOkIxJeV.js → token-YW4VSeBB.js} +1 -1
  18. package/dist/src/chunks/types-BJSR0JNV.js +1 -0
  19. package/dist/src/cli/commands/channels.d.ts +15 -0
  20. package/dist/src/cli/commands/channels.js +9 -0
  21. package/dist/src/cli/commands/info.js +1 -1
  22. package/dist/src/cli/dev/repl.js +3 -3
  23. package/dist/src/cli/run.js +1 -1
  24. package/dist/src/client/message-reducer.js +6 -0
  25. package/dist/src/context/keys.d.ts +1 -1
  26. package/dist/src/context/keys.js +1 -1
  27. package/dist/src/context/seed-keys.d.ts +5 -1
  28. package/dist/src/context/seed-keys.js +4 -0
  29. package/dist/src/evals/cli/eval.js +1 -1
  30. package/dist/src/execution/await-authorization-orchestrator.js +1 -1
  31. package/dist/src/execution/node-step.js +13 -0
  32. package/dist/src/execution/remote-agent-dispatch.d.ts +15 -0
  33. package/dist/src/execution/remote-agent-dispatch.js +79 -0
  34. package/dist/src/execution/runtime-context.js +4 -1
  35. package/dist/src/execution/session-callback-step.d.ts +16 -0
  36. package/dist/src/execution/session-callback-step.js +72 -0
  37. package/dist/src/execution/subagent-invocation.d.ts +16 -0
  38. package/dist/src/execution/subagent-invocation.js +16 -0
  39. package/dist/src/execution/subagent-tool.js +5 -8
  40. package/dist/src/execution/workflow-entry.js +21 -1
  41. package/dist/src/execution/workflow-steps.d.ts +6 -1
  42. package/dist/src/execution/workflow-steps.js +76 -25
  43. package/dist/src/harness/execute-tool.d.ts +3 -3
  44. package/dist/src/harness/runtime-actions.d.ts +1 -0
  45. package/dist/src/harness/runtime-actions.js +18 -1
  46. package/dist/src/internal/application/package.js +1 -1
  47. package/dist/src/internal/process/pnpm.d.ts +28 -0
  48. package/dist/src/internal/process/pnpm.js +50 -0
  49. package/dist/src/protocol/message.d.ts +6 -0
  50. package/dist/src/protocol/message.js +1 -0
  51. package/dist/src/protocol/routes.d.ts +11 -0
  52. package/dist/src/protocol/routes.js +13 -0
  53. package/dist/src/public/channels/ash.js +25 -1
  54. package/dist/src/public/channels/discord/api.d.ts +99 -0
  55. package/dist/src/public/channels/discord/api.js +167 -0
  56. package/dist/src/public/channels/discord/defaults.d.ts +9 -0
  57. package/dist/src/public/channels/discord/defaults.js +74 -0
  58. package/dist/src/public/channels/discord/discordChannel.d.ts +132 -0
  59. package/dist/src/public/channels/discord/discordChannel.js +402 -0
  60. package/dist/src/public/channels/discord/hitl.d.ts +34 -0
  61. package/dist/src/public/channels/discord/hitl.js +194 -0
  62. package/dist/src/public/channels/discord/inbound.d.ts +97 -0
  63. package/dist/src/public/channels/discord/inbound.js +238 -0
  64. package/dist/src/public/channels/discord/index.d.ts +7 -0
  65. package/dist/src/public/channels/discord/index.js +6 -0
  66. package/dist/src/public/channels/discord/responses.d.ts +11 -0
  67. package/dist/src/public/channels/discord/responses.js +40 -0
  68. package/dist/src/public/channels/discord/verify.d.ts +38 -0
  69. package/dist/src/public/channels/discord/verify.js +72 -0
  70. package/dist/src/public/channels/discord/verifyInbound.d.ts +6 -0
  71. package/dist/src/public/channels/discord/verifyInbound.js +19 -0
  72. package/dist/src/public/channels/slack/constants.d.ts +7 -0
  73. package/dist/src/public/channels/slack/constants.js +7 -0
  74. package/dist/src/public/channels/slack/slackChannel.js +2 -1
  75. package/dist/src/runtime/actions/keys.js +2 -0
  76. package/dist/src/runtime/actions/types.d.ts +47 -1
  77. package/dist/src/runtime/actions/types.js +23 -0
  78. package/dist/src/runtime/connections/callback-route.d.ts +1 -1
  79. package/dist/src/runtime/connections/callback-route.js +1 -1
  80. package/dist/src/runtime/connections/mcp-client.d.ts +1 -2
  81. package/dist/src/runtime/connections/mcp-client.js +69 -3
  82. package/dist/src/runtime/framework-channels/index.js +7 -2
  83. package/dist/src/runtime/session-callback-route.d.ts +6 -0
  84. package/dist/src/runtime/session-callback-route.js +87 -0
  85. package/package.json +9 -3
  86. package/dist/src/chunks/host-BxT35q6K.js +0 -70
  87. package/dist/src/chunks/paths-B2hLA0Fn.js +0 -85
  88. package/dist/src/chunks/types-CjIyrcYo.js +0 -1
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Discord HITL component rendering + decode helpers.
3
+ *
4
+ * Discord components carry a `custom_id` with a 100-character cap. Ash
5
+ * encodes only the request id and, for buttons, the selected option id.
6
+ */
7
+ import { DISCORD_INTERACTION_RESPONSE_TYPE, } from "#public/channels/discord/inbound.js";
8
+ /** Discord component type values used by the native channel. */
9
+ export const DISCORD_COMPONENT_TYPE = {
10
+ ACTION_ROW: 1,
11
+ BUTTON: 2,
12
+ STRING_SELECT: 3,
13
+ TEXT_INPUT: 4,
14
+ };
15
+ /** Custom id prefix for selectable HITL controls. */
16
+ export const DISCORD_HITL_CUSTOM_ID_PREFIX = "ash_input:";
17
+ /** Custom id prefix for the button/modal freeform flow. */
18
+ export const DISCORD_HITL_FREEFORM_CUSTOM_ID_PREFIX = "ash_input_freeform:";
19
+ /** Text-input id inside the freeform modal. */
20
+ export const DISCORD_HITL_FREEFORM_TEXT_INPUT_ID = "ash_freeform_text";
21
+ const DISCORD_CUSTOM_ID_MAX_LENGTH = 100;
22
+ const DISCORD_LABEL_MAX_LENGTH = 80;
23
+ const DISCORD_SELECT_OPTION_TEXT_MAX_LENGTH = 100;
24
+ const DISCORD_MODAL_TITLE_MAX_LENGTH = 45;
25
+ const DISCORD_ACTION_ROW_LIMIT = 5;
26
+ const DISCORD_SELECT_OPTION_LIMIT = 25;
27
+ /** Renders one input request as Discord message components. */
28
+ export function renderInputRequestComponents(request) {
29
+ const options = request.options;
30
+ const acceptsFreeform = request.allowFreeform === true || !options || options.length === 0;
31
+ if (options && options.length > 0 && request.display === "select") {
32
+ return [
33
+ {
34
+ components: [
35
+ {
36
+ custom_id: encodeHitlCustomId(DISCORD_HITL_CUSTOM_ID_PREFIX, {
37
+ requestId: request.requestId,
38
+ }),
39
+ options: options.slice(0, DISCORD_SELECT_OPTION_LIMIT).map((option) => {
40
+ const result = {
41
+ label: truncate(option.label, DISCORD_SELECT_OPTION_TEXT_MAX_LENGTH),
42
+ value: truncate(option.id, DISCORD_SELECT_OPTION_TEXT_MAX_LENGTH),
43
+ };
44
+ if (option.description !== undefined) {
45
+ result.description = truncate(option.description, DISCORD_SELECT_OPTION_TEXT_MAX_LENGTH);
46
+ }
47
+ return result;
48
+ }),
49
+ placeholder: "Choose an option",
50
+ type: DISCORD_COMPONENT_TYPE.STRING_SELECT,
51
+ },
52
+ ],
53
+ type: DISCORD_COMPONENT_TYPE.ACTION_ROW,
54
+ },
55
+ ];
56
+ }
57
+ if (options && options.length > 0) {
58
+ return chunk(options.slice(0, DISCORD_ACTION_ROW_LIMIT * DISCORD_ACTION_ROW_LIMIT), 5).map((row) => ({
59
+ components: row.map((option) => ({
60
+ custom_id: encodeHitlCustomId(DISCORD_HITL_CUSTOM_ID_PREFIX, {
61
+ optionId: option.id,
62
+ requestId: request.requestId,
63
+ }),
64
+ label: truncate(option.label, DISCORD_LABEL_MAX_LENGTH),
65
+ style: toDiscordButtonStyle(option.style),
66
+ type: DISCORD_COMPONENT_TYPE.BUTTON,
67
+ })),
68
+ type: DISCORD_COMPONENT_TYPE.ACTION_ROW,
69
+ }));
70
+ }
71
+ if (acceptsFreeform) {
72
+ return [
73
+ {
74
+ components: [
75
+ {
76
+ custom_id: encodeHitlCustomId(DISCORD_HITL_FREEFORM_CUSTOM_ID_PREFIX, {
77
+ requestId: request.requestId,
78
+ }),
79
+ label: "Type your answer",
80
+ style: 1,
81
+ type: DISCORD_COMPONENT_TYPE.BUTTON,
82
+ },
83
+ ],
84
+ type: DISCORD_COMPONENT_TYPE.ACTION_ROW,
85
+ },
86
+ ];
87
+ }
88
+ return [];
89
+ }
90
+ /** Builds a Discord modal response for one freeform HITL request. */
91
+ export function buildFreeformModalResponse(input) {
92
+ const payload = decodeHitlCustomId(input.customId, DISCORD_HITL_FREEFORM_CUSTOM_ID_PREFIX);
93
+ if (!payload) {
94
+ throw new Error("discordChannel: freeform custom_id is malformed.");
95
+ }
96
+ return {
97
+ data: {
98
+ components: [
99
+ {
100
+ components: [
101
+ {
102
+ custom_id: DISCORD_HITL_FREEFORM_TEXT_INPUT_ID,
103
+ label: "Answer",
104
+ max_length: 4000,
105
+ min_length: 1,
106
+ placeholder: "Type your answer here...",
107
+ required: true,
108
+ style: 2,
109
+ type: DISCORD_COMPONENT_TYPE.TEXT_INPUT,
110
+ },
111
+ ],
112
+ type: DISCORD_COMPONENT_TYPE.ACTION_ROW,
113
+ },
114
+ ],
115
+ custom_id: encodeHitlCustomId(DISCORD_HITL_FREEFORM_CUSTOM_ID_PREFIX, {
116
+ requestId: payload.requestId,
117
+ }),
118
+ title: truncate(input.prompt ?? "Your answer", DISCORD_MODAL_TITLE_MAX_LENGTH),
119
+ },
120
+ type: DISCORD_INTERACTION_RESPONSE_TYPE.MODAL,
121
+ };
122
+ }
123
+ /** Returns true when a component custom id starts the freeform modal flow. */
124
+ export function isDiscordFreeformComponent(customId) {
125
+ return decodeHitlCustomId(customId, DISCORD_HITL_FREEFORM_CUSTOM_ID_PREFIX) !== null;
126
+ }
127
+ /** Decodes one component interaction into HITL input responses. */
128
+ export function deriveComponentInputResponses(interaction) {
129
+ const payload = decodeHitlCustomId(interaction.customId, DISCORD_HITL_CUSTOM_ID_PREFIX);
130
+ if (!payload)
131
+ return [];
132
+ if (payload.optionId !== undefined) {
133
+ return [{ optionId: payload.optionId, requestId: payload.requestId }];
134
+ }
135
+ const selected = interaction.values[0];
136
+ if (selected !== undefined) {
137
+ return [{ optionId: selected, requestId: payload.requestId }];
138
+ }
139
+ return [];
140
+ }
141
+ /** Decodes one modal submission into HITL input responses. */
142
+ export function deriveModalInputResponses(interaction) {
143
+ const payload = decodeHitlCustomId(interaction.customId, DISCORD_HITL_FREEFORM_CUSTOM_ID_PREFIX);
144
+ const text = interaction.textInputs[DISCORD_HITL_FREEFORM_TEXT_INPUT_ID];
145
+ if (!payload || text === undefined)
146
+ return [];
147
+ return [{ requestId: payload.requestId, text }];
148
+ }
149
+ function encodeHitlCustomId(prefix, payload) {
150
+ const encoded = Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
151
+ const customId = `${prefix}${encoded}`;
152
+ if (customId.length > DISCORD_CUSTOM_ID_MAX_LENGTH) {
153
+ throw new Error("discordChannel: HITL custom_id exceeded Discord's 100-character limit.");
154
+ }
155
+ return customId;
156
+ }
157
+ function decodeHitlCustomId(customId, prefix) {
158
+ if (!customId.startsWith(prefix))
159
+ return null;
160
+ try {
161
+ const decoded = Buffer.from(customId.slice(prefix.length), "base64url").toString("utf8");
162
+ const parsed = JSON.parse(decoded);
163
+ if (typeof parsed.requestId !== "string" || parsed.requestId.length === 0)
164
+ return null;
165
+ const result = { requestId: parsed.requestId };
166
+ if (typeof parsed.optionId === "string") {
167
+ return { ...result, optionId: parsed.optionId };
168
+ }
169
+ return result;
170
+ }
171
+ catch {
172
+ return null;
173
+ }
174
+ }
175
+ function toDiscordButtonStyle(style) {
176
+ if (style === "primary")
177
+ return 1;
178
+ if (style === "danger")
179
+ return 4;
180
+ return 2;
181
+ }
182
+ function truncate(value, maxLength) {
183
+ if (value.length <= maxLength)
184
+ return value;
185
+ const sliceLength = Math.max(0, maxLength - 3);
186
+ return `${value.slice(0, sliceLength).trimEnd()}...`;
187
+ }
188
+ function chunk(values, size) {
189
+ const result = [];
190
+ for (let index = 0; index < values.length; index += size) {
191
+ result.push(values.slice(index, index + size));
192
+ }
193
+ return result;
194
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Inbound Discord interaction parsing and prompt shaping.
3
+ *
4
+ * The channel owns small, documented data shapes instead of exposing
5
+ * Discord's raw interaction payloads as the primary public API.
6
+ */
7
+ import type { UserContent } from "ai";
8
+ /** Discord interaction type values used by the native channel. */
9
+ export declare const DISCORD_INTERACTION_TYPE: {
10
+ readonly APPLICATION_COMMAND: 2;
11
+ readonly MESSAGE_COMPONENT: 3;
12
+ readonly MODAL_SUBMIT: 5;
13
+ readonly PING: 1;
14
+ };
15
+ /** Discord interaction callback type values used by the native channel. */
16
+ export declare const DISCORD_INTERACTION_RESPONSE_TYPE: {
17
+ readonly CHANNEL_MESSAGE_WITH_SOURCE: 4;
18
+ readonly DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE: 5;
19
+ readonly DEFERRED_UPDATE_MESSAGE: 6;
20
+ readonly MODAL: 9;
21
+ readonly PONG: 1;
22
+ };
23
+ /** Discord message flag for ephemeral responses. */
24
+ export declare const DISCORD_EPHEMERAL_MESSAGE_FLAG: number;
25
+ /** Discord user metadata surfaced by inbound interactions. */
26
+ export interface DiscordUser {
27
+ readonly avatar?: string;
28
+ readonly discriminator?: string;
29
+ readonly globalName?: string;
30
+ readonly id: string;
31
+ readonly isBot: boolean;
32
+ readonly username: string;
33
+ }
34
+ /** Discord guild member metadata surfaced by inbound interactions. */
35
+ export interface DiscordMember {
36
+ readonly nick?: string;
37
+ readonly roles: readonly string[];
38
+ readonly user: DiscordUser;
39
+ }
40
+ /** Common fields shared by parsed Discord interactions. */
41
+ export interface DiscordInteractionBase {
42
+ readonly applicationId: string;
43
+ readonly channelId: string;
44
+ readonly guildId?: string;
45
+ readonly id: string;
46
+ readonly member?: DiscordMember;
47
+ readonly token: string;
48
+ readonly user: DiscordUser;
49
+ readonly raw: Record<string, unknown>;
50
+ }
51
+ /** One slash-command option from Discord's interaction payload. */
52
+ export interface DiscordCommandOption {
53
+ readonly name: string;
54
+ readonly value?: string | number | boolean;
55
+ readonly options: readonly DiscordCommandOption[];
56
+ }
57
+ /** Parsed Discord slash/application command interaction. */
58
+ export interface DiscordCommandInteraction extends DiscordInteractionBase {
59
+ readonly commandId?: string;
60
+ readonly commandName: string;
61
+ readonly options: readonly DiscordCommandOption[];
62
+ readonly type: typeof DISCORD_INTERACTION_TYPE.APPLICATION_COMMAND;
63
+ }
64
+ /** Parsed Discord message-component interaction. */
65
+ export interface DiscordComponentInteraction extends DiscordInteractionBase {
66
+ readonly componentType: number;
67
+ readonly customId: string;
68
+ readonly messageId: string;
69
+ readonly type: typeof DISCORD_INTERACTION_TYPE.MESSAGE_COMPONENT;
70
+ readonly values: readonly string[];
71
+ }
72
+ /** Parsed Discord modal-submit interaction. */
73
+ export interface DiscordModalSubmitInteraction extends DiscordInteractionBase {
74
+ readonly customId: string;
75
+ readonly messageId?: string;
76
+ readonly textInputs: Readonly<Record<string, string>>;
77
+ readonly type: typeof DISCORD_INTERACTION_TYPE.MODAL_SUBMIT;
78
+ }
79
+ /** Parsed Discord interaction variants handled by the native channel. */
80
+ export type DiscordInteraction = DiscordCommandInteraction | DiscordComponentInteraction | DiscordModalSubmitInteraction;
81
+ /** Inbound context rendered into the model-visible `<discord_context>` block. */
82
+ export interface DiscordInboundContext {
83
+ readonly channelId: string;
84
+ readonly commandName?: string;
85
+ readonly guildId?: string;
86
+ readonly interactionId: string;
87
+ readonly userId: string;
88
+ readonly username?: string;
89
+ }
90
+ /** Parses one JSON-decoded Discord interaction payload. */
91
+ export declare function parseDiscordInteraction(value: unknown): DiscordInteraction | null;
92
+ /** Extracts the model-facing message from one Discord command. */
93
+ export declare function commandInteractionMessage(interaction: DiscordCommandInteraction): string;
94
+ /** Renders one {@link DiscordInboundContext} as a deterministic context block. */
95
+ export declare function formatDiscordContextBlock(context: DiscordInboundContext): string;
96
+ /** Prepends a `<discord_context>` block to the inbound turn message. */
97
+ export declare function prependDiscordContext(message: string | UserContent, context: DiscordInboundContext): string | UserContent;
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Inbound Discord interaction parsing and prompt shaping.
3
+ *
4
+ * The channel owns small, documented data shapes instead of exposing
5
+ * Discord's raw interaction payloads as the primary public API.
6
+ */
7
+ import { isNonEmptyString, isObject } from "#shared/guards.js";
8
+ /** Discord interaction type values used by the native channel. */
9
+ export const DISCORD_INTERACTION_TYPE = {
10
+ APPLICATION_COMMAND: 2,
11
+ MESSAGE_COMPONENT: 3,
12
+ MODAL_SUBMIT: 5,
13
+ PING: 1,
14
+ };
15
+ /** Discord interaction callback type values used by the native channel. */
16
+ export const DISCORD_INTERACTION_RESPONSE_TYPE = {
17
+ CHANNEL_MESSAGE_WITH_SOURCE: 4,
18
+ DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE: 5,
19
+ DEFERRED_UPDATE_MESSAGE: 6,
20
+ MODAL: 9,
21
+ PONG: 1,
22
+ };
23
+ /** Discord message flag for ephemeral responses. */
24
+ export const DISCORD_EPHEMERAL_MESSAGE_FLAG = 1 << 6;
25
+ const DISCORD_RESPONSE_INSTRUCTIONS = "Reply for Discord in concise Markdown. Avoid mass mentions, long tables, " +
26
+ "and messages that need more than a few short posts.";
27
+ /** Parses one JSON-decoded Discord interaction payload. */
28
+ export function parseDiscordInteraction(value) {
29
+ if (!isObject(value))
30
+ return null;
31
+ const type = value.type;
32
+ if (type === DISCORD_INTERACTION_TYPE.APPLICATION_COMMAND) {
33
+ return parseCommandInteraction(value);
34
+ }
35
+ if (type === DISCORD_INTERACTION_TYPE.MESSAGE_COMPONENT) {
36
+ return parseComponentInteraction(value);
37
+ }
38
+ if (type === DISCORD_INTERACTION_TYPE.MODAL_SUBMIT) {
39
+ return parseModalSubmitInteraction(value);
40
+ }
41
+ return null;
42
+ }
43
+ /** Extracts the model-facing message from one Discord command. */
44
+ export function commandInteractionMessage(interaction) {
45
+ const message = findOptionValue(interaction.options, "message");
46
+ if (typeof message === "string" && message.trim().length > 0)
47
+ return message;
48
+ const optionText = formatCommandOptions(interaction.options);
49
+ return optionText ? `/${interaction.commandName} ${optionText}` : `/${interaction.commandName}`;
50
+ }
51
+ /** Renders one {@link DiscordInboundContext} as a deterministic context block. */
52
+ export function formatDiscordContextBlock(context) {
53
+ const lines = [
54
+ "<discord_context>",
55
+ "response_medium: discord",
56
+ `response_instructions: ${DISCORD_RESPONSE_INSTRUCTIONS}`,
57
+ `user_id: ${context.userId}`,
58
+ ...(context.username ? [`username: ${context.username}`] : []),
59
+ `channel_id: ${context.channelId}`,
60
+ ...(context.guildId ? [`guild_id: ${context.guildId}`] : []),
61
+ `interaction_id: ${context.interactionId}`,
62
+ ...(context.commandName ? [`command_name: ${context.commandName}`] : []),
63
+ "</discord_context>",
64
+ ];
65
+ return lines.join("\n");
66
+ }
67
+ /** Prepends a `<discord_context>` block to the inbound turn message. */
68
+ export function prependDiscordContext(message, context) {
69
+ const block = formatDiscordContextBlock(context);
70
+ if (typeof message === "string") {
71
+ return message.length > 0 ? `${block}\n\n${message}` : block;
72
+ }
73
+ const contextPart = { type: "text", text: block };
74
+ return [contextPart, ...message];
75
+ }
76
+ function parseCommandInteraction(raw) {
77
+ const base = parseInteractionBase(raw);
78
+ const data = isObject(raw.data) ? raw.data : null;
79
+ if (!base || !data || !isNonEmptyString(data.name))
80
+ return null;
81
+ return {
82
+ ...base,
83
+ commandId: isNonEmptyString(data.id) ? data.id : undefined,
84
+ commandName: data.name,
85
+ options: parseOptions(data.options),
86
+ type: DISCORD_INTERACTION_TYPE.APPLICATION_COMMAND,
87
+ };
88
+ }
89
+ function parseComponentInteraction(raw) {
90
+ const base = parseInteractionBase(raw);
91
+ const data = isObject(raw.data) ? raw.data : null;
92
+ const message = isObject(raw.message) ? raw.message : null;
93
+ if (!base || !data || !isNonEmptyString(data.custom_id))
94
+ return null;
95
+ const messageId = isNonEmptyString(message?.id) ? message.id : "";
96
+ if (!messageId)
97
+ return null;
98
+ return {
99
+ ...base,
100
+ componentType: typeof data.component_type === "number" ? data.component_type : 0,
101
+ customId: data.custom_id,
102
+ messageId,
103
+ type: DISCORD_INTERACTION_TYPE.MESSAGE_COMPONENT,
104
+ values: Array.isArray(data.values)
105
+ ? data.values.filter((entry) => typeof entry === "string")
106
+ : [],
107
+ };
108
+ }
109
+ function parseModalSubmitInteraction(raw) {
110
+ const base = parseInteractionBase(raw);
111
+ const data = isObject(raw.data) ? raw.data : null;
112
+ if (!base || !data || !isNonEmptyString(data.custom_id))
113
+ return null;
114
+ const message = isObject(raw.message) ? raw.message : null;
115
+ return {
116
+ ...base,
117
+ customId: data.custom_id,
118
+ messageId: isNonEmptyString(message?.id) ? message.id : undefined,
119
+ textInputs: parseTextInputs(data.components),
120
+ type: DISCORD_INTERACTION_TYPE.MODAL_SUBMIT,
121
+ };
122
+ }
123
+ function parseInteractionBase(raw) {
124
+ if (!isNonEmptyString(raw.id) ||
125
+ !isNonEmptyString(raw.application_id) ||
126
+ !isNonEmptyString(raw.channel_id) ||
127
+ !isNonEmptyString(raw.token)) {
128
+ return null;
129
+ }
130
+ const user = parseInteractionUser(raw);
131
+ if (!user)
132
+ return null;
133
+ return {
134
+ applicationId: raw.application_id,
135
+ channelId: raw.channel_id,
136
+ guildId: isNonEmptyString(raw.guild_id) ? raw.guild_id : undefined,
137
+ id: raw.id,
138
+ member: parseMember(raw.member),
139
+ raw,
140
+ token: raw.token,
141
+ user,
142
+ };
143
+ }
144
+ function parseInteractionUser(raw) {
145
+ const directUser = parseUser(raw.user);
146
+ if (directUser)
147
+ return directUser;
148
+ const member = parseMember(raw.member);
149
+ return member?.user ?? null;
150
+ }
151
+ function parseMember(value) {
152
+ if (!isObject(value))
153
+ return undefined;
154
+ const user = parseUser(value.user);
155
+ if (!user)
156
+ return undefined;
157
+ return {
158
+ nick: isNonEmptyString(value.nick) ? value.nick : undefined,
159
+ roles: Array.isArray(value.roles)
160
+ ? value.roles.filter((entry) => typeof entry === "string")
161
+ : [],
162
+ user,
163
+ };
164
+ }
165
+ function parseUser(value) {
166
+ if (!isObject(value) || !isNonEmptyString(value.id) || !isNonEmptyString(value.username)) {
167
+ return null;
168
+ }
169
+ return {
170
+ avatar: isNonEmptyString(value.avatar) ? value.avatar : undefined,
171
+ discriminator: isNonEmptyString(value.discriminator) ? value.discriminator : undefined,
172
+ globalName: isNonEmptyString(value.global_name) ? value.global_name : undefined,
173
+ id: value.id,
174
+ isBot: value.bot === true,
175
+ username: value.username,
176
+ };
177
+ }
178
+ function parseOptions(value) {
179
+ if (!Array.isArray(value))
180
+ return [];
181
+ const options = [];
182
+ for (const item of value) {
183
+ if (!isObject(item) || !isNonEmptyString(item.name))
184
+ continue;
185
+ const option = {
186
+ name: item.name,
187
+ options: parseOptions(item.options),
188
+ value: parseOptionValue(item.value),
189
+ };
190
+ options.push(option);
191
+ }
192
+ return options;
193
+ }
194
+ function parseOptionValue(value) {
195
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
196
+ return value;
197
+ }
198
+ return undefined;
199
+ }
200
+ function parseTextInputs(value) {
201
+ const inputs = {};
202
+ if (!Array.isArray(value))
203
+ return inputs;
204
+ for (const row of value) {
205
+ if (!isObject(row) || !Array.isArray(row.components))
206
+ continue;
207
+ for (const component of row.components) {
208
+ if (!isObject(component))
209
+ continue;
210
+ if (isNonEmptyString(component.custom_id) && typeof component.value === "string") {
211
+ inputs[component.custom_id] = component.value;
212
+ }
213
+ }
214
+ }
215
+ return inputs;
216
+ }
217
+ function findOptionValue(options, name) {
218
+ for (const option of options) {
219
+ if (option.name === name && option.value !== undefined)
220
+ return option.value;
221
+ const nested = findOptionValue(option.options, name);
222
+ if (nested !== undefined)
223
+ return nested;
224
+ }
225
+ return undefined;
226
+ }
227
+ function formatCommandOptions(options) {
228
+ return options
229
+ .map(formatOption)
230
+ .filter((entry) => entry.length > 0)
231
+ .join(" ");
232
+ }
233
+ function formatOption(option) {
234
+ if (option.value !== undefined)
235
+ return `${option.name}:${String(option.value)}`;
236
+ const nested = formatCommandOptions(option.options);
237
+ return nested ? `${option.name} ${nested}` : option.name;
238
+ }
@@ -0,0 +1,7 @@
1
+ export type { ModelMessage } from "ai";
2
+ export { discordChannel, type DiscordChannel, type DiscordChannelConfig, type DiscordChannelCredentials, type DiscordChannelEvents, type DiscordChannelState, type DiscordCommandResult, type DiscordCommandResultOrPromise, type DiscordContext, type DiscordEventContext, type DiscordHandle, type DiscordReceiveArgs, type DiscordRequestOptions, } from "#public/channels/discord/discordChannel.js";
3
+ export { callDiscordApi, createDiscordFollowupMessage, discordContinuationToken, DISCORD_MESSAGE_CONTENT_MAX_LENGTH, DISCORD_NO_MENTIONS, editDiscordOriginalResponse, resolveDiscordApplicationId, resolveDiscordBotToken, resolveDiscordPublicKey, sendDiscordChannelMessage, splitDiscordMessageContent, triggerDiscordTypingIndicator, type DiscordApiOptions, type DiscordApiResponse, type DiscordApplicationId, type DiscordBotToken, type DiscordCredentials, type DiscordFetch, type DiscordMessageBody, type DiscordPostedMessage, } from "#public/channels/discord/api.js";
4
+ export { DISCORD_EPHEMERAL_MESSAGE_FLAG, DISCORD_INTERACTION_RESPONSE_TYPE, DISCORD_INTERACTION_TYPE, commandInteractionMessage, formatDiscordContextBlock, parseDiscordInteraction, prependDiscordContext, type DiscordCommandInteraction, type DiscordCommandOption, type DiscordComponentInteraction, type DiscordInboundContext, type DiscordInteraction, type DiscordInteractionBase, type DiscordMember, type DiscordModalSubmitInteraction, type DiscordUser, } from "#public/channels/discord/inbound.js";
5
+ export { DISCORD_COMPONENT_TYPE, DISCORD_HITL_CUSTOM_ID_PREFIX, DISCORD_HITL_FREEFORM_CUSTOM_ID_PREFIX, DISCORD_HITL_FREEFORM_TEXT_INPUT_ID, buildFreeformModalResponse, deriveComponentInputResponses, deriveModalInputResponses, isDiscordFreeformComponent, renderInputRequestComponents, } from "#public/channels/discord/hitl.js";
6
+ export { defaultDiscordAuth } from "#public/channels/discord/defaults.js";
7
+ export { verifyDiscordRequest, verifyDiscordSignature, type DiscordPublicKey, type DiscordVerifyOptions, type DiscordWebhookVerifier, } from "#public/channels/discord/verify.js";
@@ -0,0 +1,6 @@
1
+ export { discordChannel, } from "#public/channels/discord/discordChannel.js";
2
+ export { callDiscordApi, createDiscordFollowupMessage, discordContinuationToken, DISCORD_MESSAGE_CONTENT_MAX_LENGTH, DISCORD_NO_MENTIONS, editDiscordOriginalResponse, resolveDiscordApplicationId, resolveDiscordBotToken, resolveDiscordPublicKey, sendDiscordChannelMessage, splitDiscordMessageContent, triggerDiscordTypingIndicator, } from "#public/channels/discord/api.js";
3
+ export { DISCORD_EPHEMERAL_MESSAGE_FLAG, DISCORD_INTERACTION_RESPONSE_TYPE, DISCORD_INTERACTION_TYPE, commandInteractionMessage, formatDiscordContextBlock, parseDiscordInteraction, prependDiscordContext, } from "#public/channels/discord/inbound.js";
4
+ export { DISCORD_COMPONENT_TYPE, DISCORD_HITL_CUSTOM_ID_PREFIX, DISCORD_HITL_FREEFORM_CUSTOM_ID_PREFIX, DISCORD_HITL_FREEFORM_TEXT_INPUT_ID, buildFreeformModalResponse, deriveComponentInputResponses, deriveModalInputResponses, isDiscordFreeformComponent, renderInputRequestComponents, } from "#public/channels/discord/hitl.js";
5
+ export { defaultDiscordAuth } from "#public/channels/discord/defaults.js";
6
+ export { verifyDiscordRequest, verifyDiscordSignature, } from "#public/channels/discord/verify.js";
@@ -0,0 +1,11 @@
1
+ /** Builds the Discord acknowledgement response for deferred command handling. */
2
+ export declare function discordDeferredJson(ephemeral: boolean): Response;
3
+ /** Builds a Discord interaction callback JSON response. */
4
+ export declare function discordJson(input: {
5
+ readonly content: string;
6
+ readonly ephemeral?: boolean;
7
+ } | Record<string, unknown>): Response;
8
+ /** Serializes a Discord interaction callback body. */
9
+ export declare function discordJsonBody(body: Record<string, unknown>): Response;
10
+ /** Reads component or modal source message content from a raw Discord interaction. */
11
+ export declare function readMessageContent(raw: Record<string, unknown>): string | undefined;
@@ -0,0 +1,40 @@
1
+ import { DISCORD_EPHEMERAL_MESSAGE_FLAG, DISCORD_INTERACTION_RESPONSE_TYPE, } from "#public/channels/discord/inbound.js";
2
+ import { isNonEmptyString, isObject } from "#shared/guards.js";
3
+ import { parseJsonObject } from "#shared/json.js";
4
+ /** Builds the Discord acknowledgement response for deferred command handling. */
5
+ export function discordDeferredJson(ephemeral) {
6
+ const body = {
7
+ type: DISCORD_INTERACTION_RESPONSE_TYPE.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE,
8
+ };
9
+ if (ephemeral) {
10
+ body.data = { flags: DISCORD_EPHEMERAL_MESSAGE_FLAG };
11
+ }
12
+ return discordJsonBody(body);
13
+ }
14
+ /** Builds a Discord interaction callback JSON response. */
15
+ export function discordJson(input) {
16
+ if ("content" in input && typeof input.content === "string") {
17
+ const data = {
18
+ allowed_mentions: { parse: [] },
19
+ content: input.content,
20
+ };
21
+ if (input.ephemeral === true)
22
+ data.flags = DISCORD_EPHEMERAL_MESSAGE_FLAG;
23
+ return discordJsonBody({
24
+ data,
25
+ type: DISCORD_INTERACTION_RESPONSE_TYPE.CHANNEL_MESSAGE_WITH_SOURCE,
26
+ });
27
+ }
28
+ return discordJsonBody(input);
29
+ }
30
+ /** Serializes a Discord interaction callback body. */
31
+ export function discordJsonBody(body) {
32
+ return Response.json(parseJsonObject(body), {
33
+ headers: { "content-type": "application/json; charset=utf-8" },
34
+ });
35
+ }
36
+ /** Reads component or modal source message content from a raw Discord interaction. */
37
+ export function readMessageContent(raw) {
38
+ const message = isObject(raw.message) ? raw.message : null;
39
+ return isNonEmptyString(message?.content) ? message.content : undefined;
40
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Discord inbound-interaction verification.
3
+ *
4
+ * Discord signs interaction webhooks with Ed25519. The signed payload is
5
+ * the `X-Signature-Timestamp` header concatenated with the exact raw
6
+ * request body string.
7
+ */
8
+ /** Discord application public key, materialized directly or from an async secret provider. */
9
+ export type DiscordPublicKey = string | (() => string | Promise<string>);
10
+ /**
11
+ * Caller-supplied inbound webhook verifier. Used as an alternative to
12
+ * Ed25519 verification when an integration authenticates forwarded
13
+ * webhooks before they reach Ash.
14
+ */
15
+ export type DiscordWebhookVerifier = (request: Request, body: string) => unknown | Promise<unknown>;
16
+ /** Options for {@link verifyDiscordRequest}. */
17
+ export interface DiscordVerifyOptions {
18
+ readonly publicKey: DiscordPublicKey | undefined;
19
+ readonly webhookVerifier?: DiscordWebhookVerifier;
20
+ /** Max allowed clock skew, in seconds. Defaults to 5 minutes. */
21
+ readonly maxSkewSeconds?: number;
22
+ }
23
+ /** Resolves a Discord public key, falling back to `DISCORD_PUBLIC_KEY`. */
24
+ export declare function resolveDiscordPublicKey(publicKey?: DiscordPublicKey): Promise<string>;
25
+ /**
26
+ * Verifies an inbound Discord interaction request and returns the raw body.
27
+ *
28
+ * Throws when no public key/verifier is configured, required signature
29
+ * headers are missing, timestamp checks fail, or the signature check fails.
30
+ */
31
+ export declare function verifyDiscordRequest(request: Request, options: DiscordVerifyOptions): Promise<string>;
32
+ /** Verifies one Discord Ed25519 signature. */
33
+ export declare function verifyDiscordSignature(input: {
34
+ readonly body: string;
35
+ readonly publicKey: string;
36
+ readonly signature: string;
37
+ readonly timestamp: string;
38
+ }): boolean;