@pinet/slack-bridge 0.1.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.
- package/LICENSE +21 -0
- package/README.md +570 -0
- package/dist/activity-log.d.ts +62 -0
- package/dist/activity-log.js +293 -0
- package/dist/agent-completion-runtime.d.ts +17 -0
- package/dist/agent-completion-runtime.js +26 -0
- package/dist/agent-event-runtime.d.ts +13 -0
- package/dist/agent-event-runtime.js +27 -0
- package/dist/agent-prompt-guidance.d.ts +22 -0
- package/dist/agent-prompt-guidance.js +46 -0
- package/dist/broker/adapters/slack.d.ts +74 -0
- package/dist/broker/adapters/slack.js +563 -0
- package/dist/broker/adapters/types.d.ts +1 -0
- package/dist/broker/adapters/types.js +1 -0
- package/dist/broker/agent-messaging.d.ts +1 -0
- package/dist/broker/agent-messaging.js +1 -0
- package/dist/broker/auth.d.ts +1 -0
- package/dist/broker/auth.js +1 -0
- package/dist/broker/client.d.ts +167 -0
- package/dist/broker/client.js +624 -0
- package/dist/broker/control-plane-dashboard.d.ts +98 -0
- package/dist/broker/control-plane-dashboard.js +213 -0
- package/dist/broker/ghost-reaper.d.ts +49 -0
- package/dist/broker/ghost-reaper.js +209 -0
- package/dist/broker/index.d.ts +36 -0
- package/dist/broker/index.js +90 -0
- package/dist/broker/leader.d.ts +1 -0
- package/dist/broker/leader.js +1 -0
- package/dist/broker/maintenance.d.ts +1 -0
- package/dist/broker/maintenance.js +1 -0
- package/dist/broker/message-send.d.ts +1 -0
- package/dist/broker/message-send.js +1 -0
- package/dist/broker/paths.d.ts +1 -0
- package/dist/broker/paths.js +1 -0
- package/dist/broker/raw-tcp-loopback.d.ts +1 -0
- package/dist/broker/raw-tcp-loopback.js +1 -0
- package/dist/broker/router.d.ts +1 -0
- package/dist/broker/router.js +1 -0
- package/dist/broker/schema.d.ts +9 -0
- package/dist/broker/schema.js +63 -0
- package/dist/broker/socket-server.d.ts +120 -0
- package/dist/broker/socket-server.js +1087 -0
- package/dist/broker/types.d.ts +1 -0
- package/dist/broker/types.js +1 -0
- package/dist/broker-delivery.d.ts +10 -0
- package/dist/broker-delivery.js +26 -0
- package/dist/broker-inbound-persistence.d.ts +11 -0
- package/dist/broker-inbound-persistence.js +22 -0
- package/dist/broker-prompt-loader.d.ts +34 -0
- package/dist/broker-prompt-loader.js +185 -0
- package/dist/broker-runtime-access.d.ts +14 -0
- package/dist/broker-runtime-access.js +20 -0
- package/dist/broker-runtime.d.ts +100 -0
- package/dist/broker-runtime.js +533 -0
- package/dist/broker-thread-owner-hints.d.ts +10 -0
- package/dist/broker-thread-owner-hints.js +14 -0
- package/dist/canvases.d.ts +80 -0
- package/dist/canvases.js +221 -0
- package/dist/command-registration-runtime.d.ts +9 -0
- package/dist/command-registration-runtime.js +9 -0
- package/dist/core-tool-guardrails.d.ts +24 -0
- package/dist/core-tool-guardrails.js +66 -0
- package/dist/deploy-manifest.d.ts +27 -0
- package/dist/deploy-manifest.js +197 -0
- package/dist/follower-delivery.d.ts +16 -0
- package/dist/follower-delivery.js +70 -0
- package/dist/follower-runtime.d.ts +61 -0
- package/dist/follower-runtime.js +350 -0
- package/dist/git-metadata.d.ts +35 -0
- package/dist/git-metadata.js +88 -0
- package/dist/guardrails.d.ts +69 -0
- package/dist/guardrails.js +283 -0
- package/dist/helpers.d.ts +550 -0
- package/dist/helpers.js +2711 -0
- package/dist/home-tab.d.ts +27 -0
- package/dist/home-tab.js +242 -0
- package/dist/imessage-tools.d.ts +33 -0
- package/dist/imessage-tools.js +96 -0
- package/dist/inbox-drain-runtime.d.ts +30 -0
- package/dist/inbox-drain-runtime.js +71 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1330 -0
- package/dist/persisted-runtime-state.d.ts +52 -0
- package/dist/persisted-runtime-state.js +98 -0
- package/dist/pinet-activity-formatting.d.ts +20 -0
- package/dist/pinet-activity-formatting.js +43 -0
- package/dist/pinet-agent-status.d.ts +36 -0
- package/dist/pinet-agent-status.js +48 -0
- package/dist/pinet-commands.d.ts +70 -0
- package/dist/pinet-commands.js +402 -0
- package/dist/pinet-confirmation-replies.d.ts +7 -0
- package/dist/pinet-confirmation-replies.js +30 -0
- package/dist/pinet-control-plane-dashboard.d.ts +34 -0
- package/dist/pinet-control-plane-dashboard.js +86 -0
- package/dist/pinet-home-tabs.d.ts +36 -0
- package/dist/pinet-home-tabs.js +101 -0
- package/dist/pinet-maintenance-delivery.d.ts +22 -0
- package/dist/pinet-maintenance-delivery.js +45 -0
- package/dist/pinet-mesh-ops.d.ts +97 -0
- package/dist/pinet-mesh-ops.js +266 -0
- package/dist/pinet-registration-gate.d.ts +14 -0
- package/dist/pinet-registration-gate.js +42 -0
- package/dist/pinet-remote-control-acks.d.ts +16 -0
- package/dist/pinet-remote-control-acks.js +46 -0
- package/dist/pinet-remote-control.d.ts +17 -0
- package/dist/pinet-remote-control.js +74 -0
- package/dist/pinet-runtime-composition.d.ts +27 -0
- package/dist/pinet-runtime-composition.js +20 -0
- package/dist/pinet-tools.d.ts +67 -0
- package/dist/pinet-tools.js +1211 -0
- package/dist/prompts/broker/default.md +41 -0
- package/dist/prompts/broker/tmux.md +49 -0
- package/dist/ralph-loop.d.ts +76 -0
- package/dist/ralph-loop.js +524 -0
- package/dist/reaction-triggers.d.ts +25 -0
- package/dist/reaction-triggers.js +171 -0
- package/dist/repo-tool-guardrails.d.ts +20 -0
- package/dist/repo-tool-guardrails.js +81 -0
- package/dist/runtime-agent-context.d.ts +93 -0
- package/dist/runtime-agent-context.js +302 -0
- package/dist/runtime-mode.d.ts +10 -0
- package/dist/runtime-mode.js +39 -0
- package/dist/scheduled-wakeups.d.ts +1 -0
- package/dist/scheduled-wakeups.js +1 -0
- package/dist/session-ui-runtime.d.ts +20 -0
- package/dist/session-ui-runtime.js +163 -0
- package/dist/single-player-runtime.d.ts +71 -0
- package/dist/single-player-runtime.js +436 -0
- package/dist/skins/cosmere.json +1571 -0
- package/dist/skins/foundation.json +1304 -0
- package/dist/slack-access.d.ts +186 -0
- package/dist/slack-access.js +462 -0
- package/dist/slack-api.d.ts +2 -0
- package/dist/slack-api.js +1 -0
- package/dist/slack-block-kit.d.ts +36 -0
- package/dist/slack-block-kit.js +242 -0
- package/dist/slack-export.d.ts +31 -0
- package/dist/slack-export.js +206 -0
- package/dist/slack-message-context.d.ts +15 -0
- package/dist/slack-message-context.js +207 -0
- package/dist/slack-modals.d.ts +13 -0
- package/dist/slack-modals.js +79 -0
- package/dist/slack-pinet-runtime-adapter.d.ts +16 -0
- package/dist/slack-pinet-runtime-adapter.js +72 -0
- package/dist/slack-presence.d.ts +36 -0
- package/dist/slack-presence.js +89 -0
- package/dist/slack-request-runtime.d.ts +8 -0
- package/dist/slack-request-runtime.js +19 -0
- package/dist/slack-runtime-access.d.ts +28 -0
- package/dist/slack-runtime-access.js +107 -0
- package/dist/slack-scope-diagnostics.d.ts +31 -0
- package/dist/slack-scope-diagnostics.js +235 -0
- package/dist/slack-socket-dedup.d.ts +7 -0
- package/dist/slack-socket-dedup.js +135 -0
- package/dist/slack-thread-status.d.ts +41 -0
- package/dist/slack-thread-status.js +156 -0
- package/dist/slack-tool-policy-runtime.d.ts +36 -0
- package/dist/slack-tool-policy-runtime.js +96 -0
- package/dist/slack-tools.d.ts +52 -0
- package/dist/slack-tools.js +2688 -0
- package/dist/slack-turn-guardrails.d.ts +19 -0
- package/dist/slack-turn-guardrails.js +36 -0
- package/dist/slack-upload.d.ts +49 -0
- package/dist/slack-upload.js +224 -0
- package/dist/task-assignments.d.ts +45 -0
- package/dist/task-assignments.js +527 -0
- package/dist/thread-confirmations.d.ts +22 -0
- package/dist/thread-confirmations.js +139 -0
- package/dist/tool-registration-runtime.d.ts +13 -0
- package/dist/tool-registration-runtime.js +13 -0
- package/dist/ttl-cache.d.ts +47 -0
- package/dist/ttl-cache.js +105 -0
- package/manifest.yaml +57 -0
- package/package.json +60 -0
- package/skills/pinet-skin-creator/SKILL.md +109 -0
- package/skills/pinet-skin-creator/references/descriptor-format.md +99 -0
- package/skills/pinet-skin-creator/references/safety-checklist.md +56 -0
- package/skills/pinet-skin-creator/templates/pinet-skin-descriptor.json +77 -0
- package/skills/slack-bridge/SKILL.md +360 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { decodeSlackModalPrivateMetadata } from "./slack-modals.js";
|
|
2
|
+
function isRecord(value) {
|
|
3
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
function asString(value) {
|
|
6
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
7
|
+
}
|
|
8
|
+
function asRecord(value) {
|
|
9
|
+
return isRecord(value) ? value : undefined;
|
|
10
|
+
}
|
|
11
|
+
function asRecordArray(value) {
|
|
12
|
+
if (!Array.isArray(value) || value.some((item) => !isRecord(item))) {
|
|
13
|
+
throw new Error("Slack blocks must be a JSON array of objects.");
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
function tryParseJson(value) {
|
|
18
|
+
if (!value)
|
|
19
|
+
return undefined;
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(value);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function normalizeViewStateValue(value) {
|
|
28
|
+
const result = {};
|
|
29
|
+
if (typeof value.type === "string") {
|
|
30
|
+
result.type = value.type;
|
|
31
|
+
}
|
|
32
|
+
if (typeof value.value === "string") {
|
|
33
|
+
result.value = value.value;
|
|
34
|
+
}
|
|
35
|
+
if (typeof value.selected_date === "string") {
|
|
36
|
+
result.selectedDate = value.selected_date;
|
|
37
|
+
}
|
|
38
|
+
if (typeof value.selected_time === "string") {
|
|
39
|
+
result.selectedTime = value.selected_time;
|
|
40
|
+
}
|
|
41
|
+
if (typeof value.selected_conversation === "string") {
|
|
42
|
+
result.selectedConversation = value.selected_conversation;
|
|
43
|
+
}
|
|
44
|
+
if (typeof value.selected_channel === "string") {
|
|
45
|
+
result.selectedChannel = value.selected_channel;
|
|
46
|
+
}
|
|
47
|
+
if (typeof value.selected_user === "string") {
|
|
48
|
+
result.selectedUser = value.selected_user;
|
|
49
|
+
}
|
|
50
|
+
if (typeof value.selected_option === "object" && value.selected_option !== null) {
|
|
51
|
+
result.selectedOption = value.selected_option;
|
|
52
|
+
}
|
|
53
|
+
if (Array.isArray(value.selected_options)) {
|
|
54
|
+
result.selectedOptions = value.selected_options;
|
|
55
|
+
}
|
|
56
|
+
if (Array.isArray(value.selected_conversations)) {
|
|
57
|
+
result.selectedConversations = value.selected_conversations;
|
|
58
|
+
}
|
|
59
|
+
if (Array.isArray(value.selected_channels)) {
|
|
60
|
+
result.selectedChannels = value.selected_channels;
|
|
61
|
+
}
|
|
62
|
+
if (Array.isArray(value.selected_users)) {
|
|
63
|
+
result.selectedUsers = value.selected_users;
|
|
64
|
+
}
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
function getActionText(action) {
|
|
68
|
+
const text = asRecord(action.text);
|
|
69
|
+
return asString(text?.text);
|
|
70
|
+
}
|
|
71
|
+
function normalizeAction(action) {
|
|
72
|
+
const actionId = asString(action.action_id);
|
|
73
|
+
if (!actionId)
|
|
74
|
+
return null;
|
|
75
|
+
const value = asString(action.value);
|
|
76
|
+
return {
|
|
77
|
+
actionId,
|
|
78
|
+
blockId: asString(action.block_id),
|
|
79
|
+
text: getActionText(action),
|
|
80
|
+
type: asString(action.type),
|
|
81
|
+
style: asString(action.style),
|
|
82
|
+
value,
|
|
83
|
+
parsedValue: tryParseJson(value),
|
|
84
|
+
actionTs: asString(action.action_ts),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function sanitizeNormalizedActions(actions) {
|
|
88
|
+
return actions.map((action) => ({
|
|
89
|
+
actionId: action.actionId,
|
|
90
|
+
blockId: action.blockId ?? null,
|
|
91
|
+
text: action.text ?? null,
|
|
92
|
+
type: action.type ?? null,
|
|
93
|
+
style: action.style ?? null,
|
|
94
|
+
value: action.value ?? null,
|
|
95
|
+
parsedValue: action.parsedValue ?? null,
|
|
96
|
+
actionTs: action.actionTs ?? null,
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
export function normalizeSlackBlocksInput(blocks) {
|
|
100
|
+
return asRecordArray(blocks).map((block) => ({ ...block }));
|
|
101
|
+
}
|
|
102
|
+
export function summarizeSlackBlocksForPolicy(blocks) {
|
|
103
|
+
if (!Array.isArray(blocks))
|
|
104
|
+
return "0";
|
|
105
|
+
return String(blocks.length);
|
|
106
|
+
}
|
|
107
|
+
export function encodeSlackBlockActionValue(value) {
|
|
108
|
+
if (typeof value === "string")
|
|
109
|
+
return value;
|
|
110
|
+
return JSON.stringify(value);
|
|
111
|
+
}
|
|
112
|
+
export function extractSlackInteractivePayloadFromEnvelope(envelope) {
|
|
113
|
+
if (envelope.type !== "interactive")
|
|
114
|
+
return null;
|
|
115
|
+
const payloadValue = envelope.payload;
|
|
116
|
+
let payload = payloadValue;
|
|
117
|
+
if (typeof payloadValue === "string") {
|
|
118
|
+
try {
|
|
119
|
+
payload = JSON.parse(payloadValue);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (!isRecord(payload))
|
|
126
|
+
return null;
|
|
127
|
+
return payload;
|
|
128
|
+
}
|
|
129
|
+
export function extractSlackBlockActionsPayloadFromEnvelope(envelope) {
|
|
130
|
+
const payload = extractSlackInteractivePayloadFromEnvelope(envelope);
|
|
131
|
+
return payload?.type === "block_actions" ? payload : null;
|
|
132
|
+
}
|
|
133
|
+
export function normalizeSlackBlockActionPayload(payload) {
|
|
134
|
+
const user = asRecord(payload.user);
|
|
135
|
+
const container = asRecord(payload.container);
|
|
136
|
+
const channel = asRecord(payload.channel);
|
|
137
|
+
const message = asRecord(payload.message);
|
|
138
|
+
const view = asRecord(payload.view);
|
|
139
|
+
const actions = Array.isArray(payload.actions)
|
|
140
|
+
? payload.actions
|
|
141
|
+
.map((entry) => asRecord(entry))
|
|
142
|
+
.filter((entry) => Boolean(entry))
|
|
143
|
+
: [];
|
|
144
|
+
const normalizedActions = actions
|
|
145
|
+
.map(normalizeAction)
|
|
146
|
+
.filter((action) => Boolean(action));
|
|
147
|
+
if (normalizedActions.length === 0)
|
|
148
|
+
return null;
|
|
149
|
+
const modalMetadata = decodeSlackModalPrivateMetadata(asString(view?.private_metadata));
|
|
150
|
+
const channelId = asString(container?.channel_id) ??
|
|
151
|
+
asString(channel?.id) ??
|
|
152
|
+
asString(message?.channel?.id) ??
|
|
153
|
+
modalMetadata.threadContext?.channel;
|
|
154
|
+
const messageTs = asString(container?.message_ts) ?? asString(message?.ts);
|
|
155
|
+
const threadTs = asString(container?.thread_ts) ??
|
|
156
|
+
asString(message?.thread_ts) ??
|
|
157
|
+
messageTs ??
|
|
158
|
+
modalMetadata.threadContext?.threadTs;
|
|
159
|
+
const userId = asString(user?.id);
|
|
160
|
+
if (!channelId || !threadTs || !userId)
|
|
161
|
+
return null;
|
|
162
|
+
const primaryAction = normalizedActions[0];
|
|
163
|
+
const label = primaryAction.text?.trim() ? `"${primaryAction.text.trim()}"` : "button";
|
|
164
|
+
const timestamp = primaryAction.actionTs ??
|
|
165
|
+
asString(payload.action_ts) ??
|
|
166
|
+
asString(view?.hash) ??
|
|
167
|
+
messageTs ??
|
|
168
|
+
threadTs;
|
|
169
|
+
return {
|
|
170
|
+
channel: channelId,
|
|
171
|
+
threadTs,
|
|
172
|
+
userId,
|
|
173
|
+
text: `Clicked Slack ${label} (action_id: ${primaryAction.actionId}).`,
|
|
174
|
+
timestamp,
|
|
175
|
+
metadata: {
|
|
176
|
+
kind: "slack_block_action",
|
|
177
|
+
triggerId: asString(payload.trigger_id) ?? null,
|
|
178
|
+
viewId: asString(view?.id) ?? null,
|
|
179
|
+
callbackId: asString(view?.callback_id) ?? null,
|
|
180
|
+
viewHash: asString(view?.hash) ?? null,
|
|
181
|
+
blockId: primaryAction.blockId ?? null,
|
|
182
|
+
actionId: primaryAction.actionId,
|
|
183
|
+
value: primaryAction.value ?? null,
|
|
184
|
+
parsedValue: primaryAction.parsedValue ?? null,
|
|
185
|
+
actionText: primaryAction.text ?? null,
|
|
186
|
+
channel: channelId,
|
|
187
|
+
threadTs,
|
|
188
|
+
messageTs: messageTs ?? null,
|
|
189
|
+
modalPrivateMetadata: modalMetadata.value ?? null,
|
|
190
|
+
actions: sanitizeNormalizedActions(normalizedActions),
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
export function normalizeSlackViewSubmissionPayload(payload) {
|
|
195
|
+
if (payload.type !== "view_submission") {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
const user = asRecord(payload.user);
|
|
199
|
+
const view = asRecord(payload.view);
|
|
200
|
+
const state = asRecord(view?.state);
|
|
201
|
+
const stateValues = asRecord(state?.values);
|
|
202
|
+
const userId = asString(user?.id);
|
|
203
|
+
const viewId = asString(view?.id);
|
|
204
|
+
const modalMetadata = decodeSlackModalPrivateMetadata(asString(view?.private_metadata));
|
|
205
|
+
const threadTs = modalMetadata.threadContext?.threadTs;
|
|
206
|
+
const channel = modalMetadata.threadContext?.channel;
|
|
207
|
+
if (!userId || !threadTs || !channel || !viewId || !stateValues) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
const normalizedValues = {};
|
|
211
|
+
for (const [blockId, rawActions] of Object.entries(stateValues)) {
|
|
212
|
+
const actionMap = asRecord(rawActions);
|
|
213
|
+
if (!actionMap)
|
|
214
|
+
continue;
|
|
215
|
+
normalizedValues[blockId] = Object.fromEntries(Object.entries(actionMap).map(([actionId, rawValue]) => [
|
|
216
|
+
actionId,
|
|
217
|
+
normalizeViewStateValue(asRecord(rawValue) ?? {}),
|
|
218
|
+
]));
|
|
219
|
+
}
|
|
220
|
+
const callbackId = asString(view?.callback_id);
|
|
221
|
+
const titleText = asString((asRecord(view?.title) ?? {}).text);
|
|
222
|
+
const timestamp = asString(payload.hash) ?? viewId;
|
|
223
|
+
return {
|
|
224
|
+
channel,
|
|
225
|
+
threadTs,
|
|
226
|
+
userId,
|
|
227
|
+
text: `Submitted Slack modal ${callbackId ? `(${callbackId}) ` : ""}${titleText ? `"${titleText}"` : `view ${viewId}`}.`,
|
|
228
|
+
timestamp,
|
|
229
|
+
metadata: {
|
|
230
|
+
kind: "slack_view_submission",
|
|
231
|
+
triggerId: asString(payload.trigger_id) ?? null,
|
|
232
|
+
callbackId: callbackId ?? null,
|
|
233
|
+
viewId,
|
|
234
|
+
externalId: asString(view?.external_id) ?? null,
|
|
235
|
+
viewHash: asString(view?.hash) ?? null,
|
|
236
|
+
channel,
|
|
237
|
+
threadTs,
|
|
238
|
+
privateMetadata: modalMetadata.value ?? null,
|
|
239
|
+
stateValues: normalizedValues,
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type SlackExportFormat = "markdown" | "json" | "plain";
|
|
2
|
+
export interface SlackExportFileInput {
|
|
3
|
+
name?: string;
|
|
4
|
+
title?: string;
|
|
5
|
+
mimetype?: string;
|
|
6
|
+
filetype?: string;
|
|
7
|
+
permalink?: string;
|
|
8
|
+
urlPrivate?: string;
|
|
9
|
+
preview?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface SlackExportMessageInput {
|
|
12
|
+
ts?: string;
|
|
13
|
+
authorName?: string;
|
|
14
|
+
text?: string;
|
|
15
|
+
files?: SlackExportFileInput[];
|
|
16
|
+
}
|
|
17
|
+
export interface BuildSlackThreadExportOptions {
|
|
18
|
+
format?: string;
|
|
19
|
+
includeMetadata?: boolean;
|
|
20
|
+
threadTs: string;
|
|
21
|
+
channelId: string;
|
|
22
|
+
channelLabel?: string;
|
|
23
|
+
messages: SlackExportMessageInput[];
|
|
24
|
+
mentionNames?: Record<string, string>;
|
|
25
|
+
}
|
|
26
|
+
export declare function convertSlackMrkdwnToMarkdown(text: string, mentionNames?: Record<string, string>): string;
|
|
27
|
+
export declare function normalizeSlackExportFormat(format?: string): SlackExportFormat;
|
|
28
|
+
export declare function parseSlackExportBoundaryTs(value: string): number;
|
|
29
|
+
export declare function filterSlackExportMessagesByRange(messages: SlackExportMessageInput[], oldestTs?: number, latestTs?: number): SlackExportMessageInput[];
|
|
30
|
+
export declare function formatSlackExportTimestamp(ts?: string): string;
|
|
31
|
+
export declare function buildSlackThreadExport(options: BuildSlackThreadExportOptions): string;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
function decodeSlackEntities(text) {
|
|
2
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
3
|
+
}
|
|
4
|
+
function formatSlackToken(token, mentionNames) {
|
|
5
|
+
if (token.startsWith("@")) {
|
|
6
|
+
const userId = token.slice(1);
|
|
7
|
+
return `@${mentionNames[userId] ?? userId}`;
|
|
8
|
+
}
|
|
9
|
+
if (token.startsWith("#")) {
|
|
10
|
+
const [, fallback] = token.split("|");
|
|
11
|
+
return `#${fallback ?? token.slice(1)}`;
|
|
12
|
+
}
|
|
13
|
+
if (token.startsWith("!")) {
|
|
14
|
+
if (token === "!here" || token === "!channel" || token === "!everyone") {
|
|
15
|
+
return `@${token.slice(1)}`;
|
|
16
|
+
}
|
|
17
|
+
if (token.startsWith("!subteam^")) {
|
|
18
|
+
const [, handle] = token.split("|");
|
|
19
|
+
return `@${handle ?? "group"}`;
|
|
20
|
+
}
|
|
21
|
+
if (token.startsWith("!date^")) {
|
|
22
|
+
const [, fallback] = token.split("|");
|
|
23
|
+
return fallback ?? token;
|
|
24
|
+
}
|
|
25
|
+
return token.slice(1);
|
|
26
|
+
}
|
|
27
|
+
const [target, label] = token.split("|");
|
|
28
|
+
if (!target) {
|
|
29
|
+
return token;
|
|
30
|
+
}
|
|
31
|
+
if (target.startsWith("mailto:")) {
|
|
32
|
+
const visible = label ?? target.slice("mailto:".length);
|
|
33
|
+
return `[${visible}](${target})`;
|
|
34
|
+
}
|
|
35
|
+
if (target.startsWith("http://") || target.startsWith("https://")) {
|
|
36
|
+
const visible = label ?? target;
|
|
37
|
+
return `[${visible}](${target})`;
|
|
38
|
+
}
|
|
39
|
+
return label ?? target;
|
|
40
|
+
}
|
|
41
|
+
export function convertSlackMrkdwnToMarkdown(text, mentionNames = {}) {
|
|
42
|
+
const decoded = decodeSlackEntities(text);
|
|
43
|
+
const withTokens = decoded.replace(/<([^>]+)>/g, (_match, token) => formatSlackToken(token, mentionNames));
|
|
44
|
+
return withTokens
|
|
45
|
+
.replace(/(^|[\s(])\*(\S(?:[\s\S]*?\S)?)\*(?=$|[\s).,!?:;])/g, "$1**$2**")
|
|
46
|
+
.replace(/(^|[\s(])~(\S(?:[\s\S]*?\S)?)~(?=$|[\s).,!?:;])/g, "$1~~$2~~");
|
|
47
|
+
}
|
|
48
|
+
export function normalizeSlackExportFormat(format) {
|
|
49
|
+
const normalized = format?.trim().toLowerCase() ?? "markdown";
|
|
50
|
+
if (normalized === "markdown" || normalized === "json" || normalized === "plain") {
|
|
51
|
+
return normalized;
|
|
52
|
+
}
|
|
53
|
+
throw new Error("format must be 'markdown', 'json', or 'plain'.");
|
|
54
|
+
}
|
|
55
|
+
export function parseSlackExportBoundaryTs(value) {
|
|
56
|
+
const trimmed = value.trim();
|
|
57
|
+
if (!trimmed) {
|
|
58
|
+
throw new Error("Timestamp boundary cannot be empty.");
|
|
59
|
+
}
|
|
60
|
+
if (/^\d+(?:\.\d+)?$/.test(trimmed)) {
|
|
61
|
+
return Number.parseFloat(trimmed);
|
|
62
|
+
}
|
|
63
|
+
const parsed = Date.parse(trimmed);
|
|
64
|
+
if (Number.isNaN(parsed)) {
|
|
65
|
+
throw new Error(`Invalid timestamp boundary: ${value}`);
|
|
66
|
+
}
|
|
67
|
+
return parsed / 1000;
|
|
68
|
+
}
|
|
69
|
+
export function filterSlackExportMessagesByRange(messages, oldestTs, latestTs) {
|
|
70
|
+
return messages.filter((message) => {
|
|
71
|
+
const ts = message.ts ? Number.parseFloat(message.ts) : Number.NaN;
|
|
72
|
+
if (!Number.isFinite(ts)) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
if (oldestTs != null && ts < oldestTs) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
if (latestTs != null && ts > latestTs) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
export function formatSlackExportTimestamp(ts) {
|
|
85
|
+
if (!ts) {
|
|
86
|
+
return "unknown time";
|
|
87
|
+
}
|
|
88
|
+
const numeric = Number.parseFloat(ts);
|
|
89
|
+
if (!Number.isFinite(numeric)) {
|
|
90
|
+
return ts;
|
|
91
|
+
}
|
|
92
|
+
return new Date(numeric * 1000).toISOString();
|
|
93
|
+
}
|
|
94
|
+
function buildSlackFileSummary(file) {
|
|
95
|
+
const label = file.title ?? file.name ?? "attachment";
|
|
96
|
+
const url = file.permalink ?? file.urlPrivate;
|
|
97
|
+
const type = file.filetype ?? file.mimetype;
|
|
98
|
+
const parts = [`- ${label}`];
|
|
99
|
+
if (type) {
|
|
100
|
+
parts.push(`(${type})`);
|
|
101
|
+
}
|
|
102
|
+
if (url) {
|
|
103
|
+
parts.push(`— ${url}`);
|
|
104
|
+
}
|
|
105
|
+
let line = parts.join(" ");
|
|
106
|
+
const preview = file.preview?.trim();
|
|
107
|
+
if (preview) {
|
|
108
|
+
line += `\n Preview: ${preview}`;
|
|
109
|
+
}
|
|
110
|
+
return line;
|
|
111
|
+
}
|
|
112
|
+
function buildSlackExportHeader(options, participants) {
|
|
113
|
+
const header = ["# Slack Thread Export", ""];
|
|
114
|
+
header.push(`- Thread: \`${options.threadTs}\``);
|
|
115
|
+
header.push(options.channelLabel
|
|
116
|
+
? `- Channel: ${options.channelLabel} (\`${options.channelId}\`)`
|
|
117
|
+
: `- Channel: \`${options.channelId}\``);
|
|
118
|
+
header.push(`- Messages: ${options.messages.length}`);
|
|
119
|
+
if (participants.length > 0) {
|
|
120
|
+
header.push(`- Participants: ${participants.join(", ")}`);
|
|
121
|
+
}
|
|
122
|
+
header.push("");
|
|
123
|
+
return header;
|
|
124
|
+
}
|
|
125
|
+
function buildMarkdownExport(options) {
|
|
126
|
+
const mentionNames = options.mentionNames ?? {};
|
|
127
|
+
const participants = [
|
|
128
|
+
...new Set(options.messages.map((message) => message.authorName).filter(Boolean)),
|
|
129
|
+
];
|
|
130
|
+
const lines = buildSlackExportHeader(options, participants);
|
|
131
|
+
options.messages.forEach((message, index) => {
|
|
132
|
+
const heading = options.includeMetadata !== false
|
|
133
|
+
? `## ${formatSlackExportTimestamp(message.ts)} — ${message.authorName ?? "unknown"}`
|
|
134
|
+
: `## Message ${index + 1}`;
|
|
135
|
+
lines.push(heading, "");
|
|
136
|
+
const body = convertSlackMrkdwnToMarkdown(message.text ?? "", mentionNames).trim();
|
|
137
|
+
lines.push(body || "(no text)");
|
|
138
|
+
if ((message.files?.length ?? 0) > 0) {
|
|
139
|
+
lines.push("", "Attachments:");
|
|
140
|
+
for (const file of message.files ?? []) {
|
|
141
|
+
lines.push(buildSlackFileSummary(file));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
lines.push("", "---", "");
|
|
145
|
+
});
|
|
146
|
+
return lines.join("\n").trim();
|
|
147
|
+
}
|
|
148
|
+
function buildPlainExport(options) {
|
|
149
|
+
const mentionNames = options.mentionNames ?? {};
|
|
150
|
+
const lines = [];
|
|
151
|
+
for (const message of options.messages) {
|
|
152
|
+
if (options.includeMetadata !== false) {
|
|
153
|
+
lines.push(`[${formatSlackExportTimestamp(message.ts)}] ${message.authorName ?? "unknown"}`);
|
|
154
|
+
}
|
|
155
|
+
lines.push(convertSlackMrkdwnToMarkdown(message.text ?? "", mentionNames).trim() || "(no text)");
|
|
156
|
+
if ((message.files?.length ?? 0) > 0) {
|
|
157
|
+
lines.push("Attachments:");
|
|
158
|
+
for (const file of message.files ?? []) {
|
|
159
|
+
lines.push(buildSlackFileSummary(file));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
lines.push("");
|
|
163
|
+
}
|
|
164
|
+
return lines.join("\n").trim();
|
|
165
|
+
}
|
|
166
|
+
function buildJsonExport(options) {
|
|
167
|
+
const mentionNames = options.mentionNames ?? {};
|
|
168
|
+
const payload = {
|
|
169
|
+
format: "json",
|
|
170
|
+
thread_ts: options.threadTs,
|
|
171
|
+
channel: options.channelId,
|
|
172
|
+
channel_label: options.channelLabel,
|
|
173
|
+
include_metadata: options.includeMetadata !== false,
|
|
174
|
+
message_count: options.messages.length,
|
|
175
|
+
messages: options.messages.map((message, index) => ({
|
|
176
|
+
index: index + 1,
|
|
177
|
+
...(options.includeMetadata !== false
|
|
178
|
+
? {
|
|
179
|
+
ts: message.ts,
|
|
180
|
+
timestamp: formatSlackExportTimestamp(message.ts),
|
|
181
|
+
author: message.authorName ?? "unknown",
|
|
182
|
+
}
|
|
183
|
+
: {}),
|
|
184
|
+
text: convertSlackMrkdwnToMarkdown(message.text ?? "", mentionNames),
|
|
185
|
+
files: (message.files ?? []).map((file) => ({
|
|
186
|
+
title: file.title,
|
|
187
|
+
name: file.name,
|
|
188
|
+
type: file.filetype ?? file.mimetype,
|
|
189
|
+
permalink: file.permalink,
|
|
190
|
+
url_private: file.urlPrivate,
|
|
191
|
+
preview: file.preview,
|
|
192
|
+
})),
|
|
193
|
+
})),
|
|
194
|
+
};
|
|
195
|
+
return JSON.stringify(payload, null, 2);
|
|
196
|
+
}
|
|
197
|
+
export function buildSlackThreadExport(options) {
|
|
198
|
+
const format = normalizeSlackExportFormat(options.format);
|
|
199
|
+
if (format === "json") {
|
|
200
|
+
return buildJsonExport(options);
|
|
201
|
+
}
|
|
202
|
+
if (format === "plain") {
|
|
203
|
+
return buildPlainExport(options);
|
|
204
|
+
}
|
|
205
|
+
return buildMarkdownExport(options);
|
|
206
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface SlackMessageFileMetadata {
|
|
2
|
+
id?: string;
|
|
3
|
+
name?: string;
|
|
4
|
+
title?: string;
|
|
5
|
+
mimetype?: string;
|
|
6
|
+
filetype?: string;
|
|
7
|
+
prettyType?: string;
|
|
8
|
+
permalink?: string;
|
|
9
|
+
urlPrivate?: string;
|
|
10
|
+
mode?: string;
|
|
11
|
+
size?: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function extractSlackMessageFileMetadata(files: unknown): SlackMessageFileMetadata[];
|
|
14
|
+
export declare function extractSlackMessageContextLines(evt: Record<string, unknown>, baseText?: string): string[];
|
|
15
|
+
export declare function buildSlackInboundMessageText(baseText: string, evt: Record<string, unknown>): string;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
function asString(value) {
|
|
2
|
+
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
|
3
|
+
}
|
|
4
|
+
function asRecord(value) {
|
|
5
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
6
|
+
? value
|
|
7
|
+
: null;
|
|
8
|
+
}
|
|
9
|
+
function asRecordArray(value) {
|
|
10
|
+
return Array.isArray(value)
|
|
11
|
+
? value.filter((item) => typeof item === "object" && item !== null)
|
|
12
|
+
: [];
|
|
13
|
+
}
|
|
14
|
+
function normalizeWhitespace(value) {
|
|
15
|
+
return value.replace(/\s+/g, " ").trim();
|
|
16
|
+
}
|
|
17
|
+
function clipLine(value, maxLength = 220) {
|
|
18
|
+
if (value.length <= maxLength)
|
|
19
|
+
return value;
|
|
20
|
+
return `${value.slice(0, maxLength - 1).trimEnd()}…`;
|
|
21
|
+
}
|
|
22
|
+
function pushContextLine(lines, rawValue) {
|
|
23
|
+
if (!rawValue)
|
|
24
|
+
return;
|
|
25
|
+
const normalized = clipLine(normalizeWhitespace(rawValue));
|
|
26
|
+
if (!normalized)
|
|
27
|
+
return;
|
|
28
|
+
lines.push(normalized);
|
|
29
|
+
}
|
|
30
|
+
function extractTextObject(value) {
|
|
31
|
+
const record = asRecord(value);
|
|
32
|
+
if (!record)
|
|
33
|
+
return [];
|
|
34
|
+
const text = asString(record.text);
|
|
35
|
+
return text ? [text] : [];
|
|
36
|
+
}
|
|
37
|
+
function extractRichTextElementText(element) {
|
|
38
|
+
const type = asString(element.type) ?? "";
|
|
39
|
+
switch (type) {
|
|
40
|
+
case "text": {
|
|
41
|
+
const text = asString(element.text);
|
|
42
|
+
return text ? [text] : [];
|
|
43
|
+
}
|
|
44
|
+
case "link": {
|
|
45
|
+
const text = asString(element.text);
|
|
46
|
+
const url = asString(element.url);
|
|
47
|
+
if (text && url && text !== url) {
|
|
48
|
+
return [`${text} (${url})`];
|
|
49
|
+
}
|
|
50
|
+
return text ? [text] : url ? [url] : [];
|
|
51
|
+
}
|
|
52
|
+
case "emoji": {
|
|
53
|
+
const name = asString(element.name);
|
|
54
|
+
return name ? [`:${name}:`] : [];
|
|
55
|
+
}
|
|
56
|
+
case "user": {
|
|
57
|
+
const userId = asString(element.user_id);
|
|
58
|
+
return userId ? [`<@${userId}>`] : [];
|
|
59
|
+
}
|
|
60
|
+
case "channel": {
|
|
61
|
+
const channelId = asString(element.channel_id);
|
|
62
|
+
return channelId ? [`<#${channelId}>`] : [];
|
|
63
|
+
}
|
|
64
|
+
case "rich_text_section":
|
|
65
|
+
case "rich_text_list":
|
|
66
|
+
case "rich_text_quote":
|
|
67
|
+
case "rich_text_preformatted": {
|
|
68
|
+
return asRecordArray(element.elements).flatMap((child) => extractRichTextElementText(child));
|
|
69
|
+
}
|
|
70
|
+
default:
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function extractBlockContextLines(blocks) {
|
|
75
|
+
const lines = [];
|
|
76
|
+
for (const block of asRecordArray(blocks)) {
|
|
77
|
+
const type = asString(block.type) ?? "";
|
|
78
|
+
switch (type) {
|
|
79
|
+
case "header":
|
|
80
|
+
case "section": {
|
|
81
|
+
for (const value of extractTextObject(block.text)) {
|
|
82
|
+
pushContextLine(lines, value);
|
|
83
|
+
}
|
|
84
|
+
for (const field of asRecordArray(block.fields)) {
|
|
85
|
+
for (const value of extractTextObject(field)) {
|
|
86
|
+
pushContextLine(lines, value);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
case "context": {
|
|
92
|
+
for (const element of asRecordArray(block.elements)) {
|
|
93
|
+
for (const value of extractTextObject(element)) {
|
|
94
|
+
pushContextLine(lines, value);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
case "image": {
|
|
100
|
+
pushContextLine(lines, asString(block.alt_text));
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
case "rich_text": {
|
|
104
|
+
for (const element of asRecordArray(block.elements)) {
|
|
105
|
+
const text = extractRichTextElementText(element).join("");
|
|
106
|
+
pushContextLine(lines, text);
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
default:
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return lines;
|
|
115
|
+
}
|
|
116
|
+
function extractAttachmentContextLines(attachments) {
|
|
117
|
+
const lines = [];
|
|
118
|
+
for (const attachment of asRecordArray(attachments)) {
|
|
119
|
+
pushContextLine(lines, asString(attachment.pretext));
|
|
120
|
+
pushContextLine(lines, asString(attachment.title));
|
|
121
|
+
pushContextLine(lines, asString(attachment.text));
|
|
122
|
+
pushContextLine(lines, asString(attachment.fallback));
|
|
123
|
+
pushContextLine(lines, asString(attachment.footer));
|
|
124
|
+
lines.push(...extractBlockContextLines(attachment.blocks));
|
|
125
|
+
}
|
|
126
|
+
return lines;
|
|
127
|
+
}
|
|
128
|
+
export function extractSlackMessageFileMetadata(files) {
|
|
129
|
+
const extracted = [];
|
|
130
|
+
for (const file of asRecordArray(files)) {
|
|
131
|
+
const metadata = {
|
|
132
|
+
...(asString(file.id) ? { id: asString(file.id) ?? undefined } : {}),
|
|
133
|
+
...(asString(file.name) ? { name: asString(file.name) ?? undefined } : {}),
|
|
134
|
+
...(asString(file.title) ? { title: asString(file.title) ?? undefined } : {}),
|
|
135
|
+
...(asString(file.mimetype) ? { mimetype: asString(file.mimetype) ?? undefined } : {}),
|
|
136
|
+
...(asString(file.filetype) ? { filetype: asString(file.filetype) ?? undefined } : {}),
|
|
137
|
+
...(asString(file.pretty_type)
|
|
138
|
+
? { prettyType: asString(file.pretty_type) ?? undefined }
|
|
139
|
+
: {}),
|
|
140
|
+
...(asString(file.permalink) ? { permalink: asString(file.permalink) ?? undefined } : {}),
|
|
141
|
+
...(asString(file.url_private_download)
|
|
142
|
+
? { urlPrivate: asString(file.url_private_download) ?? undefined }
|
|
143
|
+
: asString(file.url_private)
|
|
144
|
+
? { urlPrivate: asString(file.url_private) ?? undefined }
|
|
145
|
+
: {}),
|
|
146
|
+
...(asString(file.mode) ? { mode: asString(file.mode) ?? undefined } : {}),
|
|
147
|
+
...(typeof file.size === "number" ? { size: file.size } : {}),
|
|
148
|
+
};
|
|
149
|
+
if (Object.keys(metadata).length === 0)
|
|
150
|
+
continue;
|
|
151
|
+
extracted.push(metadata);
|
|
152
|
+
}
|
|
153
|
+
return extracted;
|
|
154
|
+
}
|
|
155
|
+
function extractFileContextLines(files) {
|
|
156
|
+
const lines = [];
|
|
157
|
+
for (const file of extractSlackMessageFileMetadata(files)) {
|
|
158
|
+
const title = file.title ?? file.name;
|
|
159
|
+
const prettyType = file.prettyType ?? file.filetype ?? file.mimetype;
|
|
160
|
+
const parts = [
|
|
161
|
+
title,
|
|
162
|
+
prettyType,
|
|
163
|
+
file.mode,
|
|
164
|
+
file.id ? `id=${file.id}` : null,
|
|
165
|
+
file.permalink ?? file.urlPrivate ?? null,
|
|
166
|
+
].filter((part) => Boolean(part));
|
|
167
|
+
if (parts.length === 0)
|
|
168
|
+
continue;
|
|
169
|
+
pushContextLine(lines, parts.join(" — "));
|
|
170
|
+
}
|
|
171
|
+
return lines;
|
|
172
|
+
}
|
|
173
|
+
function dedupeContextLines(baseText, lines) {
|
|
174
|
+
const baseNormalized = normalizeWhitespace(baseText).toLowerCase();
|
|
175
|
+
const seen = new Set();
|
|
176
|
+
const deduped = [];
|
|
177
|
+
for (const line of lines) {
|
|
178
|
+
const normalized = normalizeWhitespace(line).toLowerCase();
|
|
179
|
+
if (!normalized)
|
|
180
|
+
continue;
|
|
181
|
+
if (baseNormalized && normalized === baseNormalized) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (seen.has(normalized))
|
|
185
|
+
continue;
|
|
186
|
+
seen.add(normalized);
|
|
187
|
+
deduped.push(line);
|
|
188
|
+
}
|
|
189
|
+
return deduped;
|
|
190
|
+
}
|
|
191
|
+
export function extractSlackMessageContextLines(evt, baseText = "") {
|
|
192
|
+
const lines = [
|
|
193
|
+
...extractBlockContextLines(evt.blocks),
|
|
194
|
+
...extractAttachmentContextLines(evt.attachments),
|
|
195
|
+
...extractFileContextLines(evt.files),
|
|
196
|
+
];
|
|
197
|
+
return dedupeContextLines(baseText, lines).slice(0, 4);
|
|
198
|
+
}
|
|
199
|
+
export function buildSlackInboundMessageText(baseText, evt) {
|
|
200
|
+
const trimmedBase = baseText.trim();
|
|
201
|
+
const contextLines = extractSlackMessageContextLines(evt, trimmedBase);
|
|
202
|
+
if (contextLines.length === 0) {
|
|
203
|
+
return trimmedBase;
|
|
204
|
+
}
|
|
205
|
+
const prefix = trimmedBase.length > 0 ? trimmedBase : "(Slack message had no plain-text body)";
|
|
206
|
+
return `${prefix}\n\nSlack message context:\n${contextLines.map((line) => `- ${line}`).join("\n")}`;
|
|
207
|
+
}
|