@gugu910/pi-slack-bridge 0.1.3

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 (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +299 -0
  3. package/dist/activity-log.js +304 -0
  4. package/dist/broker/adapters/slack.js +645 -0
  5. package/dist/broker/adapters/types.js +3 -0
  6. package/dist/broker/agent-messaging.js +154 -0
  7. package/dist/broker/auth.js +97 -0
  8. package/dist/broker/client.js +495 -0
  9. package/dist/broker/control-plane-canvas.js +357 -0
  10. package/dist/broker/index.js +125 -0
  11. package/dist/broker/leader.js +133 -0
  12. package/dist/broker/maintenance.js +135 -0
  13. package/dist/broker/paths.js +69 -0
  14. package/dist/broker/router.js +287 -0
  15. package/dist/broker/schema.js +1492 -0
  16. package/dist/broker/socket-server.js +665 -0
  17. package/dist/broker/types.js +12 -0
  18. package/dist/broker-delivery.js +34 -0
  19. package/dist/canvases.js +175 -0
  20. package/dist/deploy-manifest.js +238 -0
  21. package/dist/follower-delivery.js +83 -0
  22. package/dist/git-metadata.js +95 -0
  23. package/dist/guardrails.js +197 -0
  24. package/dist/helpers.js +2128 -0
  25. package/dist/home-tab.js +240 -0
  26. package/dist/index.js +3086 -0
  27. package/dist/pinet-commands.js +244 -0
  28. package/dist/ralph-loop.js +385 -0
  29. package/dist/reaction-triggers.js +160 -0
  30. package/dist/scheduled-wakeups.js +71 -0
  31. package/dist/slack-api.js +5 -0
  32. package/dist/slack-block-kit.js +425 -0
  33. package/dist/slack-export.js +214 -0
  34. package/dist/slack-modals.js +269 -0
  35. package/dist/slack-presence.js +98 -0
  36. package/dist/slack-socket-dedup.js +143 -0
  37. package/dist/slack-tools.js +1715 -0
  38. package/dist/slack-upload.js +147 -0
  39. package/dist/task-assignments.js +403 -0
  40. package/dist/ttl-cache.js +110 -0
  41. package/manifest.yaml +57 -0
  42. package/package.json +45 -0
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_REACTION_COMMANDS = void 0;
4
+ exports.normalizeReactionName = normalizeReactionName;
5
+ exports.resolveReactionCommands = resolveReactionCommands;
6
+ exports.formatReactionDisplay = formatReactionDisplay;
7
+ exports.buildReactionTriggerMessage = buildReactionTriggerMessage;
8
+ exports.buildReactionPromptGuidelines = buildReactionPromptGuidelines;
9
+ const REACTION_ALIASES = {
10
+ "📝": "memo",
11
+ memo: "memo",
12
+ "🐛": "bug",
13
+ bug: "bug",
14
+ "🔍": "mag",
15
+ mag: "mag",
16
+ mag_right: "mag_right",
17
+ "📌": "pushpin",
18
+ pushpin: "pushpin",
19
+ "🌐": "globe_with_meridians",
20
+ globe_with_meridians: "globe_with_meridians",
21
+ "🔄": "repeat",
22
+ repeat: "repeat",
23
+ "👀": "eyes",
24
+ eyes: "eyes",
25
+ "✅": "white_check_mark",
26
+ white_check_mark: "white_check_mark",
27
+ };
28
+ const REACTION_DISPLAY = {
29
+ memo: "📝",
30
+ bug: "🐛",
31
+ mag: "🔍",
32
+ mag_right: "🔎",
33
+ pushpin: "📌",
34
+ globe_with_meridians: "🌐",
35
+ repeat: "🔄",
36
+ eyes: "👀",
37
+ white_check_mark: "✅",
38
+ };
39
+ exports.DEFAULT_REACTION_COMMANDS = {
40
+ memo: {
41
+ action: "summarize",
42
+ prompt: "Summarize the reacted-to message and the surrounding thread. Focus on key decisions, action items, and open questions.",
43
+ },
44
+ bug: {
45
+ action: "file-issue",
46
+ prompt: "Turn the reacted-to message or thread into a GitHub issue. Capture the problem, context, reproduction clues, and a sensible next step.",
47
+ },
48
+ eyes: {
49
+ action: "review",
50
+ prompt: "Review the referenced message, code, or work item and report what needs attention. If the context points to a PR or diff, review that artifact.",
51
+ },
52
+ white_check_mark: {
53
+ action: "approve",
54
+ prompt: "Evaluate whether the referenced work should be approved. If it looks good, say so clearly and note any final caveats.",
55
+ },
56
+ repeat: {
57
+ action: "retry",
58
+ prompt: "Retry or regenerate the requested work using the reacted message and surrounding thread as the source of truth.",
59
+ },
60
+ mag: {
61
+ action: "search",
62
+ prompt: "Search the codebase for code related to the reacted message and report the most relevant files or findings.",
63
+ },
64
+ pushpin: {
65
+ action: "track",
66
+ prompt: "Treat the reacted message as something to preserve and track. Pin or record it in the most appropriate project artifact, such as a Slack canvas.",
67
+ },
68
+ globe_with_meridians: {
69
+ action: "fetch-url",
70
+ prompt: "If the reacted message contains a URL, fetch the linked page and summarize the important information for the user.",
71
+ },
72
+ };
73
+ function normalizeReactionNameOrNull(input) {
74
+ const trimmed = input.trim();
75
+ if (!trimmed)
76
+ return null;
77
+ const withoutColons = trimmed.replace(/^:+|:+$/g, "").toLowerCase();
78
+ return (REACTION_ALIASES[trimmed] ??
79
+ REACTION_ALIASES[withoutColons] ??
80
+ (/^[a-z0-9_+-]+$/.test(withoutColons) ? withoutColons : null));
81
+ }
82
+ function normalizeReactionName(input) {
83
+ const normalized = normalizeReactionNameOrNull(input);
84
+ if (!normalized) {
85
+ throw new Error(`Unsupported reaction ${JSON.stringify(input)}. Use a Slack reaction name like "eyes" or a supported emoji such as 👀, ✅, 🔄, 📝, or 🐛.`);
86
+ }
87
+ return normalized;
88
+ }
89
+ function buildDefaultPromptForAction(action) {
90
+ switch (action) {
91
+ case "summarize":
92
+ return exports.DEFAULT_REACTION_COMMANDS.memo.prompt;
93
+ case "file-issue":
94
+ return exports.DEFAULT_REACTION_COMMANDS.bug.prompt;
95
+ case "review":
96
+ return exports.DEFAULT_REACTION_COMMANDS.eyes.prompt;
97
+ case "approve":
98
+ return exports.DEFAULT_REACTION_COMMANDS.white_check_mark.prompt;
99
+ case "retry":
100
+ return exports.DEFAULT_REACTION_COMMANDS.repeat.prompt;
101
+ case "search":
102
+ return exports.DEFAULT_REACTION_COMMANDS.mag.prompt;
103
+ case "track":
104
+ return exports.DEFAULT_REACTION_COMMANDS.pushpin.prompt;
105
+ case "fetch-url":
106
+ return exports.DEFAULT_REACTION_COMMANDS.globe_with_meridians.prompt;
107
+ default:
108
+ return `Carry out the "${action}" action for the reacted Slack message or thread, using the included context before you decide what to do.`;
109
+ }
110
+ }
111
+ function resolveReactionCommands(settings) {
112
+ const resolved = new Map(Object.entries(exports.DEFAULT_REACTION_COMMANDS));
113
+ if (!settings) {
114
+ return resolved;
115
+ }
116
+ for (const [rawReaction, config] of Object.entries(settings)) {
117
+ const normalizedReaction = normalizeReactionNameOrNull(rawReaction);
118
+ if (!normalizedReaction)
119
+ continue;
120
+ if (typeof config === "string") {
121
+ resolved.set(normalizedReaction, {
122
+ action: config,
123
+ prompt: buildDefaultPromptForAction(config),
124
+ });
125
+ continue;
126
+ }
127
+ if (!config || typeof config !== "object")
128
+ continue;
129
+ const action = config.action?.trim() || resolved.get(normalizedReaction)?.action || normalizedReaction;
130
+ const prompt = config.prompt?.trim() || buildDefaultPromptForAction(action);
131
+ resolved.set(normalizedReaction, { action, prompt });
132
+ }
133
+ return resolved;
134
+ }
135
+ function formatReactionDisplay(reactionName) {
136
+ return `${REACTION_DISPLAY[reactionName] ?? `:${reactionName}:`} (:${reactionName}:)`;
137
+ }
138
+ function buildReactionTriggerMessage(input) {
139
+ const reactedMessageText = input.reactedMessageText.trim() || "(no text)";
140
+ return [
141
+ "Reaction trigger from Slack:",
142
+ `- reaction: ${formatReactionDisplay(input.reactionName)}`,
143
+ `- action: ${input.command.action}`,
144
+ `- reactor: ${input.reactorName}`,
145
+ `- channel: <#${input.channel}>`,
146
+ `- thread_ts: ${input.threadTs}`,
147
+ `- message_ts: ${input.messageTs}`,
148
+ `- reacted_message_author: ${input.reactedMessageAuthor}`,
149
+ `- reacted_message_text: ${reactedMessageText}`,
150
+ "",
151
+ `Requested action: ${input.command.prompt}`,
152
+ "Treat this reaction as an explicit user request, but still verify context before acting.",
153
+ ].join("\n");
154
+ }
155
+ function buildReactionPromptGuidelines() {
156
+ return [
157
+ "Reaction-triggered requests may appear in your Slack inbox as structured 'Reaction trigger from Slack:' messages.",
158
+ "Treat them as explicit user requests keyed off emoji reactions. Use the included action, reacted message text, and thread context to decide what to do.",
159
+ ];
160
+ }
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseScheduledWakeupDelay = parseScheduledWakeupDelay;
4
+ exports.resolveScheduledWakeupFireAt = resolveScheduledWakeupFireAt;
5
+ exports.buildScheduledWakeupThreadId = buildScheduledWakeupThreadId;
6
+ exports.buildScheduledWakeupMetadata = buildScheduledWakeupMetadata;
7
+ const DELAY_UNITS_MS = {
8
+ ms: 1,
9
+ s: 1_000,
10
+ m: 60_000,
11
+ h: 60 * 60_000,
12
+ d: 24 * 60 * 60_000,
13
+ };
14
+ function parseScheduledWakeupDelay(delay) {
15
+ const normalized = delay.trim().toLowerCase().replace(/\s+/g, "");
16
+ if (!normalized) {
17
+ return null;
18
+ }
19
+ const tokenRegex = /(\d+)(ms|s|m|h|d)/g;
20
+ let totalMs = 0;
21
+ let matchedLength = 0;
22
+ for (const match of normalized.matchAll(tokenRegex)) {
23
+ const [token, amountText, unit] = match;
24
+ if (!token || !amountText || !unit || match.index !== matchedLength) {
25
+ return null;
26
+ }
27
+ const amount = Number.parseInt(amountText, 10);
28
+ if (!Number.isFinite(amount) || amount < 0) {
29
+ return null;
30
+ }
31
+ totalMs += amount * DELAY_UNITS_MS[unit];
32
+ matchedLength += token.length;
33
+ }
34
+ if (matchedLength !== normalized.length || totalMs <= 0) {
35
+ return null;
36
+ }
37
+ return totalMs;
38
+ }
39
+ function resolveScheduledWakeupFireAt(input, now = Date.now()) {
40
+ const hasDelay = typeof input.delay === "string" && input.delay.trim().length > 0;
41
+ const hasAt = typeof input.at === "string" && input.at.trim().length > 0;
42
+ if (hasDelay === hasAt) {
43
+ throw new Error("Provide exactly one of delay or at.");
44
+ }
45
+ if (hasDelay) {
46
+ const delayMs = parseScheduledWakeupDelay(input.delay);
47
+ if (delayMs == null) {
48
+ throw new Error("Invalid delay. Use values like 5m, 30s, 1h30m, or 1d.");
49
+ }
50
+ return new Date(now + delayMs).toISOString();
51
+ }
52
+ const fireAtMs = Date.parse(input.at);
53
+ if (Number.isNaN(fireAtMs)) {
54
+ throw new Error("Invalid timestamp. Use an ISO-8601 time like 2026-04-02T14:30:00Z.");
55
+ }
56
+ if (fireAtMs <= now) {
57
+ throw new Error("Scheduled wake-up time must be in the future.");
58
+ }
59
+ return new Date(fireAtMs).toISOString();
60
+ }
61
+ function buildScheduledWakeupThreadId(agentId) {
62
+ return `wakeup:${agentId}`;
63
+ }
64
+ function buildScheduledWakeupMetadata(wakeupId, fireAt) {
65
+ return {
66
+ senderAgent: "Pinet Scheduler",
67
+ scheduledWakeup: true,
68
+ wakeupId,
69
+ fireAt,
70
+ };
71
+ }
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.callSlackApi = void 0;
4
+ var helpers_js_1 = require("./helpers.js");
5
+ Object.defineProperty(exports, "callSlackApi", { enumerable: true, get: function () { return helpers_js_1.callSlackAPI; } });
@@ -0,0 +1,425 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeSlackBlocksInput = normalizeSlackBlocksInput;
4
+ exports.summarizeSlackBlocksForPolicy = summarizeSlackBlocksForPolicy;
5
+ exports.encodeSlackBlockActionValue = encodeSlackBlockActionValue;
6
+ exports.buildSlackBlockKitPromptGuidelines = buildSlackBlockKitPromptGuidelines;
7
+ exports.buildSlackBlocksTemplate = buildSlackBlocksTemplate;
8
+ exports.extractSlackInteractivePayloadFromEnvelope = extractSlackInteractivePayloadFromEnvelope;
9
+ exports.extractSlackBlockActionsPayloadFromEnvelope = extractSlackBlockActionsPayloadFromEnvelope;
10
+ exports.normalizeSlackBlockActionPayload = normalizeSlackBlockActionPayload;
11
+ exports.normalizeSlackViewSubmissionPayload = normalizeSlackViewSubmissionPayload;
12
+ exports.normalizeSlackInteractivePayload = normalizeSlackInteractivePayload;
13
+ const slack_modals_js_1 = require("./slack-modals.js");
14
+ function isRecord(value) {
15
+ return typeof value === "object" && value !== null && !Array.isArray(value);
16
+ }
17
+ function asString(value) {
18
+ return typeof value === "string" && value.length > 0 ? value : undefined;
19
+ }
20
+ function asRecord(value) {
21
+ return isRecord(value) ? value : undefined;
22
+ }
23
+ function asRecordArray(value) {
24
+ if (!Array.isArray(value) || value.some((item) => !isRecord(item))) {
25
+ throw new Error("Slack blocks must be a JSON array of objects.");
26
+ }
27
+ return value;
28
+ }
29
+ function tryParseJson(value) {
30
+ if (!value)
31
+ return undefined;
32
+ try {
33
+ return JSON.parse(value);
34
+ }
35
+ catch {
36
+ return undefined;
37
+ }
38
+ }
39
+ function normalizeViewStateValue(value) {
40
+ const result = {};
41
+ if (typeof value.type === "string") {
42
+ result.type = value.type;
43
+ }
44
+ if (typeof value.value === "string") {
45
+ result.value = value.value;
46
+ }
47
+ if (typeof value.selected_date === "string") {
48
+ result.selectedDate = value.selected_date;
49
+ }
50
+ if (typeof value.selected_time === "string") {
51
+ result.selectedTime = value.selected_time;
52
+ }
53
+ if (typeof value.selected_conversation === "string") {
54
+ result.selectedConversation = value.selected_conversation;
55
+ }
56
+ if (typeof value.selected_channel === "string") {
57
+ result.selectedChannel = value.selected_channel;
58
+ }
59
+ if (typeof value.selected_user === "string") {
60
+ result.selectedUser = value.selected_user;
61
+ }
62
+ if (typeof value.selected_option === "object" && value.selected_option !== null) {
63
+ result.selectedOption = value.selected_option;
64
+ }
65
+ if (Array.isArray(value.selected_options)) {
66
+ result.selectedOptions = value.selected_options;
67
+ }
68
+ if (Array.isArray(value.selected_conversations)) {
69
+ result.selectedConversations = value.selected_conversations;
70
+ }
71
+ if (Array.isArray(value.selected_channels)) {
72
+ result.selectedChannels = value.selected_channels;
73
+ }
74
+ if (Array.isArray(value.selected_users)) {
75
+ result.selectedUsers = value.selected_users;
76
+ }
77
+ return result;
78
+ }
79
+ function joinNonEmpty(parts) {
80
+ return parts.filter((part) => Boolean(part && part.trim())).join("\n\n");
81
+ }
82
+ function buildHeaderBlock(title) {
83
+ if (!title?.trim())
84
+ return [];
85
+ return [
86
+ {
87
+ type: "header",
88
+ text: {
89
+ type: "plain_text",
90
+ text: title.trim().slice(0, 150),
91
+ },
92
+ },
93
+ ];
94
+ }
95
+ function buildSectionBlock(text) {
96
+ if (!text?.trim())
97
+ return [];
98
+ return [
99
+ {
100
+ type: "section",
101
+ text: {
102
+ type: "mrkdwn",
103
+ text: text.trim(),
104
+ },
105
+ },
106
+ ];
107
+ }
108
+ function buildFooterBlocks(footer) {
109
+ if (!footer?.trim())
110
+ return [];
111
+ return [
112
+ { type: "divider" },
113
+ {
114
+ type: "context",
115
+ elements: [{ type: "mrkdwn", text: footer.trim() }],
116
+ },
117
+ ];
118
+ }
119
+ function prefixDiffLines(text, prefix) {
120
+ return text
121
+ .split("\n")
122
+ .map((line) => `${prefix}${line}`)
123
+ .join("\n");
124
+ }
125
+ function getActionText(action) {
126
+ const text = asRecord(action.text);
127
+ return asString(text?.text);
128
+ }
129
+ function normalizeAction(action) {
130
+ const actionId = asString(action.action_id);
131
+ if (!actionId)
132
+ return null;
133
+ const value = asString(action.value);
134
+ return {
135
+ actionId,
136
+ blockId: asString(action.block_id),
137
+ text: getActionText(action),
138
+ type: asString(action.type),
139
+ style: asString(action.style),
140
+ value,
141
+ parsedValue: tryParseJson(value),
142
+ actionTs: asString(action.action_ts),
143
+ };
144
+ }
145
+ function sanitizeNormalizedActions(actions) {
146
+ return actions.map((action) => ({
147
+ actionId: action.actionId,
148
+ blockId: action.blockId ?? null,
149
+ text: action.text ?? null,
150
+ type: action.type ?? null,
151
+ style: action.style ?? null,
152
+ value: action.value ?? null,
153
+ parsedValue: action.parsedValue ?? null,
154
+ actionTs: action.actionTs ?? null,
155
+ }));
156
+ }
157
+ function normalizeSlackBlocksInput(blocks) {
158
+ return asRecordArray(blocks).map((block) => ({ ...block }));
159
+ }
160
+ function summarizeSlackBlocksForPolicy(blocks) {
161
+ if (!Array.isArray(blocks))
162
+ return "0";
163
+ return String(blocks.length);
164
+ }
165
+ function encodeSlackBlockActionValue(value) {
166
+ if (typeof value === "string")
167
+ return value;
168
+ return JSON.stringify(value);
169
+ }
170
+ function buildSlackBlockKitPromptGuidelines() {
171
+ return [
172
+ "Slack supports an optional blocks parameter for rich Block Kit messages. text stays required as fallback / notification text.",
173
+ "Use slack_blocks_build when you want a template-generated blocks array instead of hand-writing JSON.",
174
+ "A block kit payload is a JSON array of block objects, for example:",
175
+ '[{"type":"section","text":{"type":"mrkdwn","text":"*Deploy complete*\\nBranch: `main`"}},{"type":"actions","elements":[{"type":"button","text":{"type":"plain_text","text":"Rollback"},"action_id":"deploy.rollback","style":"danger","value":"rollback:prod"}]}]',
176
+ "Button actions should set stable action_id values. Put machine-readable context in the button value string (plain text or JSON string).",
177
+ ];
178
+ }
179
+ function buildSlackBlocksTemplate(input) {
180
+ switch (input.template) {
181
+ case "code_snippet": {
182
+ if (!input.code?.trim()) {
183
+ throw new Error('Template "code_snippet" requires a non-empty code value.');
184
+ }
185
+ const language = input.language?.trim() || "text";
186
+ const codeFence = `\`\`\`${language}\n${input.code.trim()}\n\`\`\``;
187
+ const blocks = [
188
+ ...buildHeaderBlock(input.title ?? "Code snippet"),
189
+ ...buildSectionBlock(joinNonEmpty([input.text, codeFence])),
190
+ ...buildFooterBlocks(input.footer),
191
+ ];
192
+ return {
193
+ blocks,
194
+ fallbackText: joinNonEmpty([input.title ?? "Code snippet", input.text, input.code]),
195
+ };
196
+ }
197
+ case "status_report": {
198
+ if (!input.title?.trim()) {
199
+ throw new Error('Template "status_report" requires a title.');
200
+ }
201
+ const fields = (input.fields ?? []).slice(0, 10).map((field) => ({
202
+ type: "mrkdwn",
203
+ text: `*${field.label.trim()}*\n${field.value.trim()}`,
204
+ }));
205
+ const blocks = [
206
+ ...buildHeaderBlock(input.title),
207
+ ...buildSectionBlock(input.text),
208
+ ];
209
+ if (fields.length > 0) {
210
+ blocks.push({ type: "section", fields });
211
+ }
212
+ blocks.push(...buildFooterBlocks(input.footer));
213
+ const fieldLines = (input.fields ?? []).map((field) => `${field.label}: ${field.value}`);
214
+ return {
215
+ blocks,
216
+ fallbackText: joinNonEmpty([input.title, input.text, fieldLines.join("\n"), input.footer]),
217
+ };
218
+ }
219
+ case "action_buttons": {
220
+ if (!input.text?.trim()) {
221
+ throw new Error('Template "action_buttons" requires text.');
222
+ }
223
+ const buttons = (input.buttons ?? []).map((button) => {
224
+ const element = {
225
+ type: "button",
226
+ text: { type: "plain_text", text: button.text.trim().slice(0, 75) },
227
+ action_id: button.action_id.trim(),
228
+ };
229
+ if (button.value)
230
+ element.value = button.value;
231
+ if (button.style)
232
+ element.style = button.style;
233
+ if (button.url)
234
+ element.url = button.url;
235
+ return element;
236
+ });
237
+ if (buttons.length === 0) {
238
+ throw new Error('Template "action_buttons" requires at least one button.');
239
+ }
240
+ const blocks = [
241
+ ...buildHeaderBlock(input.title),
242
+ ...buildSectionBlock(input.text),
243
+ { type: "actions", elements: buttons },
244
+ ...buildFooterBlocks(input.footer),
245
+ ];
246
+ return {
247
+ blocks,
248
+ fallbackText: joinNonEmpty([
249
+ input.title,
250
+ input.text,
251
+ `Actions: ${buttons
252
+ .map((button) => String(button.text.text ?? "action"))
253
+ .join(", ")}`,
254
+ ]),
255
+ };
256
+ }
257
+ case "diff_view": {
258
+ if (!input.before?.trim() && !input.after?.trim()) {
259
+ throw new Error('Template "diff_view" requires before and/or after text.');
260
+ }
261
+ const removed = input.before?.trim()
262
+ ? `*Removed*\n\`\`\`diff\n${prefixDiffLines(input.before.trim(), "-")}\n\`\`\``
263
+ : undefined;
264
+ const added = input.after?.trim()
265
+ ? `*Added*\n\`\`\`diff\n${prefixDiffLines(input.after.trim(), "+")}\n\`\`\``
266
+ : undefined;
267
+ const blocks = [
268
+ ...buildHeaderBlock(input.title ?? "Diff view"),
269
+ ...buildSectionBlock(joinNonEmpty([input.text, removed, added])),
270
+ ...buildFooterBlocks(input.footer),
271
+ ];
272
+ return {
273
+ blocks,
274
+ fallbackText: joinNonEmpty([
275
+ input.title ?? "Diff view",
276
+ input.text,
277
+ input.before,
278
+ input.after,
279
+ ]),
280
+ };
281
+ }
282
+ default:
283
+ throw new Error(`Unknown block template ${JSON.stringify(input.template)}. Use one of: code_snippet, status_report, action_buttons, diff_view.`);
284
+ }
285
+ }
286
+ function extractSlackInteractivePayloadFromEnvelope(envelope) {
287
+ if (envelope.type !== "interactive")
288
+ return null;
289
+ const payloadValue = envelope.payload;
290
+ let payload = payloadValue;
291
+ if (typeof payloadValue === "string") {
292
+ try {
293
+ payload = JSON.parse(payloadValue);
294
+ }
295
+ catch {
296
+ return null;
297
+ }
298
+ }
299
+ if (!isRecord(payload))
300
+ return null;
301
+ return payload;
302
+ }
303
+ function extractSlackBlockActionsPayloadFromEnvelope(envelope) {
304
+ const payload = extractSlackInteractivePayloadFromEnvelope(envelope);
305
+ return payload?.type === "block_actions" ? payload : null;
306
+ }
307
+ function normalizeSlackBlockActionPayload(payload) {
308
+ const user = asRecord(payload.user);
309
+ const container = asRecord(payload.container);
310
+ const channel = asRecord(payload.channel);
311
+ const message = asRecord(payload.message);
312
+ const view = asRecord(payload.view);
313
+ const actions = Array.isArray(payload.actions)
314
+ ? payload.actions
315
+ .map((entry) => asRecord(entry))
316
+ .filter((entry) => Boolean(entry))
317
+ : [];
318
+ const normalizedActions = actions
319
+ .map(normalizeAction)
320
+ .filter((action) => Boolean(action));
321
+ if (normalizedActions.length === 0)
322
+ return null;
323
+ const modalMetadata = (0, slack_modals_js_1.decodeSlackModalPrivateMetadata)(asString(view?.private_metadata));
324
+ const channelId = asString(container?.channel_id) ??
325
+ asString(channel?.id) ??
326
+ asString(message?.channel?.id) ??
327
+ modalMetadata.threadContext?.channel;
328
+ const messageTs = asString(container?.message_ts) ?? asString(message?.ts);
329
+ const threadTs = asString(container?.thread_ts) ??
330
+ asString(message?.thread_ts) ??
331
+ messageTs ??
332
+ modalMetadata.threadContext?.threadTs;
333
+ const userId = asString(user?.id);
334
+ if (!channelId || !threadTs || !userId)
335
+ return null;
336
+ const primaryAction = normalizedActions[0];
337
+ const label = primaryAction.text?.trim() ? `"${primaryAction.text.trim()}"` : "button";
338
+ const timestamp = primaryAction.actionTs ??
339
+ asString(payload.action_ts) ??
340
+ asString(view?.hash) ??
341
+ messageTs ??
342
+ threadTs;
343
+ return {
344
+ channel: channelId,
345
+ threadTs,
346
+ userId,
347
+ text: `Clicked Slack ${label} (action_id: ${primaryAction.actionId}).`,
348
+ timestamp,
349
+ metadata: {
350
+ kind: "slack_block_action",
351
+ triggerId: asString(payload.trigger_id) ?? null,
352
+ viewId: asString(view?.id) ?? null,
353
+ callbackId: asString(view?.callback_id) ?? null,
354
+ viewHash: asString(view?.hash) ?? null,
355
+ blockId: primaryAction.blockId ?? null,
356
+ actionId: primaryAction.actionId,
357
+ value: primaryAction.value ?? null,
358
+ parsedValue: primaryAction.parsedValue ?? null,
359
+ actionText: primaryAction.text ?? null,
360
+ channel: channelId,
361
+ threadTs,
362
+ messageTs: messageTs ?? null,
363
+ modalPrivateMetadata: modalMetadata.value ?? null,
364
+ actions: sanitizeNormalizedActions(normalizedActions),
365
+ },
366
+ };
367
+ }
368
+ function normalizeSlackViewSubmissionPayload(payload) {
369
+ if (payload.type !== "view_submission") {
370
+ return null;
371
+ }
372
+ const user = asRecord(payload.user);
373
+ const view = asRecord(payload.view);
374
+ const state = asRecord(view?.state);
375
+ const stateValues = asRecord(state?.values);
376
+ const userId = asString(user?.id);
377
+ const viewId = asString(view?.id);
378
+ const modalMetadata = (0, slack_modals_js_1.decodeSlackModalPrivateMetadata)(asString(view?.private_metadata));
379
+ const threadTs = modalMetadata.threadContext?.threadTs;
380
+ const channel = modalMetadata.threadContext?.channel;
381
+ if (!userId || !threadTs || !channel || !viewId || !stateValues) {
382
+ return null;
383
+ }
384
+ const normalizedValues = {};
385
+ for (const [blockId, rawActions] of Object.entries(stateValues)) {
386
+ const actionMap = asRecord(rawActions);
387
+ if (!actionMap)
388
+ continue;
389
+ normalizedValues[blockId] = Object.fromEntries(Object.entries(actionMap).map(([actionId, rawValue]) => [
390
+ actionId,
391
+ normalizeViewStateValue(asRecord(rawValue) ?? {}),
392
+ ]));
393
+ }
394
+ const callbackId = asString(view?.callback_id);
395
+ const titleText = asString((asRecord(view?.title) ?? {}).text);
396
+ const timestamp = asString(payload.hash) ?? viewId;
397
+ return {
398
+ channel,
399
+ threadTs,
400
+ userId,
401
+ text: `Submitted Slack modal ${callbackId ? `(${callbackId}) ` : ""}${titleText ? `"${titleText}"` : `view ${viewId}`}.`,
402
+ timestamp,
403
+ metadata: {
404
+ kind: "slack_view_submission",
405
+ triggerId: asString(payload.trigger_id) ?? null,
406
+ callbackId: callbackId ?? null,
407
+ viewId,
408
+ externalId: asString(view?.external_id) ?? null,
409
+ viewHash: asString(view?.hash) ?? null,
410
+ channel,
411
+ threadTs,
412
+ privateMetadata: modalMetadata.value ?? null,
413
+ stateValues: normalizedValues,
414
+ },
415
+ };
416
+ }
417
+ function normalizeSlackInteractivePayload(payload) {
418
+ if (payload.type === "block_actions") {
419
+ return normalizeSlackBlockActionPayload(payload);
420
+ }
421
+ if (payload.type === "view_submission") {
422
+ return normalizeSlackViewSubmissionPayload(payload);
423
+ }
424
+ return null;
425
+ }