@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.
Files changed (179) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +570 -0
  3. package/dist/activity-log.d.ts +62 -0
  4. package/dist/activity-log.js +293 -0
  5. package/dist/agent-completion-runtime.d.ts +17 -0
  6. package/dist/agent-completion-runtime.js +26 -0
  7. package/dist/agent-event-runtime.d.ts +13 -0
  8. package/dist/agent-event-runtime.js +27 -0
  9. package/dist/agent-prompt-guidance.d.ts +22 -0
  10. package/dist/agent-prompt-guidance.js +46 -0
  11. package/dist/broker/adapters/slack.d.ts +74 -0
  12. package/dist/broker/adapters/slack.js +563 -0
  13. package/dist/broker/adapters/types.d.ts +1 -0
  14. package/dist/broker/adapters/types.js +1 -0
  15. package/dist/broker/agent-messaging.d.ts +1 -0
  16. package/dist/broker/agent-messaging.js +1 -0
  17. package/dist/broker/auth.d.ts +1 -0
  18. package/dist/broker/auth.js +1 -0
  19. package/dist/broker/client.d.ts +167 -0
  20. package/dist/broker/client.js +624 -0
  21. package/dist/broker/control-plane-dashboard.d.ts +98 -0
  22. package/dist/broker/control-plane-dashboard.js +213 -0
  23. package/dist/broker/ghost-reaper.d.ts +49 -0
  24. package/dist/broker/ghost-reaper.js +209 -0
  25. package/dist/broker/index.d.ts +36 -0
  26. package/dist/broker/index.js +90 -0
  27. package/dist/broker/leader.d.ts +1 -0
  28. package/dist/broker/leader.js +1 -0
  29. package/dist/broker/maintenance.d.ts +1 -0
  30. package/dist/broker/maintenance.js +1 -0
  31. package/dist/broker/message-send.d.ts +1 -0
  32. package/dist/broker/message-send.js +1 -0
  33. package/dist/broker/paths.d.ts +1 -0
  34. package/dist/broker/paths.js +1 -0
  35. package/dist/broker/raw-tcp-loopback.d.ts +1 -0
  36. package/dist/broker/raw-tcp-loopback.js +1 -0
  37. package/dist/broker/router.d.ts +1 -0
  38. package/dist/broker/router.js +1 -0
  39. package/dist/broker/schema.d.ts +9 -0
  40. package/dist/broker/schema.js +63 -0
  41. package/dist/broker/socket-server.d.ts +120 -0
  42. package/dist/broker/socket-server.js +1087 -0
  43. package/dist/broker/types.d.ts +1 -0
  44. package/dist/broker/types.js +1 -0
  45. package/dist/broker-delivery.d.ts +10 -0
  46. package/dist/broker-delivery.js +26 -0
  47. package/dist/broker-inbound-persistence.d.ts +11 -0
  48. package/dist/broker-inbound-persistence.js +22 -0
  49. package/dist/broker-prompt-loader.d.ts +34 -0
  50. package/dist/broker-prompt-loader.js +185 -0
  51. package/dist/broker-runtime-access.d.ts +14 -0
  52. package/dist/broker-runtime-access.js +20 -0
  53. package/dist/broker-runtime.d.ts +100 -0
  54. package/dist/broker-runtime.js +533 -0
  55. package/dist/broker-thread-owner-hints.d.ts +10 -0
  56. package/dist/broker-thread-owner-hints.js +14 -0
  57. package/dist/canvases.d.ts +80 -0
  58. package/dist/canvases.js +221 -0
  59. package/dist/command-registration-runtime.d.ts +9 -0
  60. package/dist/command-registration-runtime.js +9 -0
  61. package/dist/core-tool-guardrails.d.ts +24 -0
  62. package/dist/core-tool-guardrails.js +66 -0
  63. package/dist/deploy-manifest.d.ts +27 -0
  64. package/dist/deploy-manifest.js +197 -0
  65. package/dist/follower-delivery.d.ts +16 -0
  66. package/dist/follower-delivery.js +70 -0
  67. package/dist/follower-runtime.d.ts +61 -0
  68. package/dist/follower-runtime.js +350 -0
  69. package/dist/git-metadata.d.ts +35 -0
  70. package/dist/git-metadata.js +88 -0
  71. package/dist/guardrails.d.ts +69 -0
  72. package/dist/guardrails.js +283 -0
  73. package/dist/helpers.d.ts +550 -0
  74. package/dist/helpers.js +2711 -0
  75. package/dist/home-tab.d.ts +27 -0
  76. package/dist/home-tab.js +242 -0
  77. package/dist/imessage-tools.d.ts +33 -0
  78. package/dist/imessage-tools.js +96 -0
  79. package/dist/inbox-drain-runtime.d.ts +30 -0
  80. package/dist/inbox-drain-runtime.js +71 -0
  81. package/dist/index.d.ts +2 -0
  82. package/dist/index.js +1330 -0
  83. package/dist/persisted-runtime-state.d.ts +52 -0
  84. package/dist/persisted-runtime-state.js +98 -0
  85. package/dist/pinet-activity-formatting.d.ts +20 -0
  86. package/dist/pinet-activity-formatting.js +43 -0
  87. package/dist/pinet-agent-status.d.ts +36 -0
  88. package/dist/pinet-agent-status.js +48 -0
  89. package/dist/pinet-commands.d.ts +70 -0
  90. package/dist/pinet-commands.js +402 -0
  91. package/dist/pinet-confirmation-replies.d.ts +7 -0
  92. package/dist/pinet-confirmation-replies.js +30 -0
  93. package/dist/pinet-control-plane-dashboard.d.ts +34 -0
  94. package/dist/pinet-control-plane-dashboard.js +86 -0
  95. package/dist/pinet-home-tabs.d.ts +36 -0
  96. package/dist/pinet-home-tabs.js +101 -0
  97. package/dist/pinet-maintenance-delivery.d.ts +22 -0
  98. package/dist/pinet-maintenance-delivery.js +45 -0
  99. package/dist/pinet-mesh-ops.d.ts +97 -0
  100. package/dist/pinet-mesh-ops.js +266 -0
  101. package/dist/pinet-registration-gate.d.ts +14 -0
  102. package/dist/pinet-registration-gate.js +42 -0
  103. package/dist/pinet-remote-control-acks.d.ts +16 -0
  104. package/dist/pinet-remote-control-acks.js +46 -0
  105. package/dist/pinet-remote-control.d.ts +17 -0
  106. package/dist/pinet-remote-control.js +74 -0
  107. package/dist/pinet-runtime-composition.d.ts +27 -0
  108. package/dist/pinet-runtime-composition.js +20 -0
  109. package/dist/pinet-tools.d.ts +67 -0
  110. package/dist/pinet-tools.js +1211 -0
  111. package/dist/prompts/broker/default.md +41 -0
  112. package/dist/prompts/broker/tmux.md +49 -0
  113. package/dist/ralph-loop.d.ts +76 -0
  114. package/dist/ralph-loop.js +524 -0
  115. package/dist/reaction-triggers.d.ts +25 -0
  116. package/dist/reaction-triggers.js +171 -0
  117. package/dist/repo-tool-guardrails.d.ts +20 -0
  118. package/dist/repo-tool-guardrails.js +81 -0
  119. package/dist/runtime-agent-context.d.ts +93 -0
  120. package/dist/runtime-agent-context.js +302 -0
  121. package/dist/runtime-mode.d.ts +10 -0
  122. package/dist/runtime-mode.js +39 -0
  123. package/dist/scheduled-wakeups.d.ts +1 -0
  124. package/dist/scheduled-wakeups.js +1 -0
  125. package/dist/session-ui-runtime.d.ts +20 -0
  126. package/dist/session-ui-runtime.js +163 -0
  127. package/dist/single-player-runtime.d.ts +71 -0
  128. package/dist/single-player-runtime.js +436 -0
  129. package/dist/skins/cosmere.json +1571 -0
  130. package/dist/skins/foundation.json +1304 -0
  131. package/dist/slack-access.d.ts +186 -0
  132. package/dist/slack-access.js +462 -0
  133. package/dist/slack-api.d.ts +2 -0
  134. package/dist/slack-api.js +1 -0
  135. package/dist/slack-block-kit.d.ts +36 -0
  136. package/dist/slack-block-kit.js +242 -0
  137. package/dist/slack-export.d.ts +31 -0
  138. package/dist/slack-export.js +206 -0
  139. package/dist/slack-message-context.d.ts +15 -0
  140. package/dist/slack-message-context.js +207 -0
  141. package/dist/slack-modals.d.ts +13 -0
  142. package/dist/slack-modals.js +79 -0
  143. package/dist/slack-pinet-runtime-adapter.d.ts +16 -0
  144. package/dist/slack-pinet-runtime-adapter.js +72 -0
  145. package/dist/slack-presence.d.ts +36 -0
  146. package/dist/slack-presence.js +89 -0
  147. package/dist/slack-request-runtime.d.ts +8 -0
  148. package/dist/slack-request-runtime.js +19 -0
  149. package/dist/slack-runtime-access.d.ts +28 -0
  150. package/dist/slack-runtime-access.js +107 -0
  151. package/dist/slack-scope-diagnostics.d.ts +31 -0
  152. package/dist/slack-scope-diagnostics.js +235 -0
  153. package/dist/slack-socket-dedup.d.ts +7 -0
  154. package/dist/slack-socket-dedup.js +135 -0
  155. package/dist/slack-thread-status.d.ts +41 -0
  156. package/dist/slack-thread-status.js +156 -0
  157. package/dist/slack-tool-policy-runtime.d.ts +36 -0
  158. package/dist/slack-tool-policy-runtime.js +96 -0
  159. package/dist/slack-tools.d.ts +52 -0
  160. package/dist/slack-tools.js +2688 -0
  161. package/dist/slack-turn-guardrails.d.ts +19 -0
  162. package/dist/slack-turn-guardrails.js +36 -0
  163. package/dist/slack-upload.d.ts +49 -0
  164. package/dist/slack-upload.js +224 -0
  165. package/dist/task-assignments.d.ts +45 -0
  166. package/dist/task-assignments.js +527 -0
  167. package/dist/thread-confirmations.d.ts +22 -0
  168. package/dist/thread-confirmations.js +139 -0
  169. package/dist/tool-registration-runtime.d.ts +13 -0
  170. package/dist/tool-registration-runtime.js +13 -0
  171. package/dist/ttl-cache.d.ts +47 -0
  172. package/dist/ttl-cache.js +105 -0
  173. package/manifest.yaml +57 -0
  174. package/package.json +60 -0
  175. package/skills/pinet-skin-creator/SKILL.md +109 -0
  176. package/skills/pinet-skin-creator/references/descriptor-format.md +99 -0
  177. package/skills/pinet-skin-creator/references/safety-checklist.md +56 -0
  178. package/skills/pinet-skin-creator/templates/pinet-skin-descriptor.json +77 -0
  179. package/skills/slack-bridge/SKILL.md +360 -0
@@ -0,0 +1,436 @@
1
+ import { agentOwnsThread, normalizeOwnedThreads } from "./helpers.js";
2
+ import { buildReactionTriggerMessage, normalizeReactionName, } from "./reaction-triggers.js";
3
+ import { buildSlackThreadRuntimeScope, classifyMessage, resolveSlackThreadOwnerHint, SlackSocketModeClient, } from "./slack-access.js";
4
+ import { isAbortError } from "./helpers.js";
5
+ function interruptSinglePlayerTurn(ctx) {
6
+ if (ctx.isIdle?.() ?? true)
7
+ return;
8
+ try {
9
+ ctx.abort?.();
10
+ }
11
+ catch {
12
+ /* best effort — interrupt controls must not crash reaction handling */
13
+ }
14
+ }
15
+ export function createSinglePlayerRuntime(deps) {
16
+ let slackSocket = null;
17
+ let shuttingDown = false;
18
+ function getCurrentBotUserId() {
19
+ return slackSocket?.getBotUserId() ?? deps.getBotUserId();
20
+ }
21
+ function getAgentState() {
22
+ return {
23
+ agentName: deps.getAgentName(),
24
+ agentAliases: deps.getAgentAliases(),
25
+ agentOwnerToken: deps.getAgentOwnerToken(),
26
+ };
27
+ }
28
+ function trackOwnedThread(threadTs, channelId, source = "slack") {
29
+ const threads = deps.getThreads();
30
+ const agentOwnerToken = deps.getAgentOwnerToken();
31
+ if (!threads.has(threadTs)) {
32
+ threads.set(threadTs, {
33
+ channelId,
34
+ threadTs,
35
+ userId: "",
36
+ source,
37
+ owner: agentOwnerToken,
38
+ });
39
+ }
40
+ else {
41
+ const thread = threads.get(threadTs);
42
+ if (!thread.owner)
43
+ thread.owner = agentOwnerToken;
44
+ if (!thread.source) {
45
+ thread.source = source;
46
+ }
47
+ }
48
+ deps.getUnclaimedThreads().delete(threadTs);
49
+ deps.persistState();
50
+ }
51
+ function clearPendingAttention(threadTs) {
52
+ const pending = deps.getPendingEyes().get(threadTs);
53
+ if (!pending)
54
+ return;
55
+ for (const entry of pending) {
56
+ void deps.removeReaction(entry.channel, entry.messageTs, "eyes");
57
+ }
58
+ deps.getPendingEyes().delete(threadTs);
59
+ }
60
+ async function resolveThreadOwner(channel, threadTs) {
61
+ const hint = await resolveSlackThreadOwnerHint({
62
+ slack: deps.slack,
63
+ token: deps.getBotToken(),
64
+ channel,
65
+ threadTs,
66
+ limit: 50,
67
+ });
68
+ return hint?.agentOwner ?? hint?.agentName ?? null;
69
+ }
70
+ async function ensureLocalThreadOwnership(channel, threadTs) {
71
+ const threads = deps.getThreads();
72
+ const { agentName, agentAliases, agentOwnerToken } = getAgentState();
73
+ const localOwner = threads.get(threadTs)?.owner;
74
+ if (localOwner && !agentOwnsThread(localOwner, agentName, agentAliases, agentOwnerToken)) {
75
+ return "skip";
76
+ }
77
+ if (localOwner) {
78
+ const thread = threads.get(threadTs);
79
+ if (thread) {
80
+ normalizeOwnedThreads([thread], agentName, agentOwnerToken, agentAliases);
81
+ }
82
+ return "continue";
83
+ }
84
+ const unclaimedThreads = deps.getUnclaimedThreads();
85
+ if (unclaimedThreads.has(threadTs)) {
86
+ return "continue";
87
+ }
88
+ const remoteOwner = await resolveThreadOwner(channel, threadTs);
89
+ if (shuttingDown) {
90
+ return "shutdown";
91
+ }
92
+ const thread = threads.get(threadTs);
93
+ if (remoteOwner && !agentOwnsThread(remoteOwner, agentName, agentAliases, agentOwnerToken)) {
94
+ if (thread)
95
+ thread.owner = remoteOwner;
96
+ return "skip";
97
+ }
98
+ if (agentOwnsThread(remoteOwner ?? undefined, agentName, agentAliases, agentOwnerToken)) {
99
+ if (thread)
100
+ thread.owner = agentOwnerToken;
101
+ }
102
+ if (!remoteOwner) {
103
+ unclaimedThreads.add(threadTs);
104
+ }
105
+ return "continue";
106
+ }
107
+ async function onThreadStarted(event) {
108
+ if (shuttingDown)
109
+ return;
110
+ const info = {
111
+ channelId: event.channelId,
112
+ threadTs: event.threadTs,
113
+ userId: event.userId,
114
+ source: "slack",
115
+ };
116
+ if (event.context) {
117
+ info.context = event.context;
118
+ }
119
+ deps.getThreads().set(info.threadTs, info);
120
+ deps.setLastDmChannel(info.channelId);
121
+ deps.persistState();
122
+ await deps.setSuggestedPrompts(info.channelId, info.threadTs);
123
+ }
124
+ function onContextChanged(event) {
125
+ if (shuttingDown)
126
+ return;
127
+ const existing = deps.getThreads().get(event.threadTs);
128
+ if (!existing || !event.context)
129
+ return;
130
+ existing.context = event.context;
131
+ deps.persistState();
132
+ }
133
+ async function onAppHomeOpened(event, ctx) {
134
+ if (shuttingDown)
135
+ return;
136
+ await deps.publishCurrentPinetHomeTab(event.userId, ctx);
137
+ }
138
+ async function onReactionAdded(evt, ctx) {
139
+ if (shuttingDown)
140
+ return;
141
+ const item = evt.item;
142
+ const user = evt.user;
143
+ const rawReactionName = evt.reaction;
144
+ if (!item ||
145
+ item.type !== "message" ||
146
+ !item.channel ||
147
+ !item.ts ||
148
+ !user ||
149
+ !rawReactionName) {
150
+ return;
151
+ }
152
+ if (user === getCurrentBotUserId()) {
153
+ return;
154
+ }
155
+ let reactionName;
156
+ try {
157
+ reactionName = normalizeReactionName(rawReactionName);
158
+ }
159
+ catch {
160
+ return;
161
+ }
162
+ const command = deps.getReactionCommand(reactionName);
163
+ if (!command || !deps.isUserAllowed(user)) {
164
+ return;
165
+ }
166
+ try {
167
+ const isInterruptReaction = command.action === "interrupt";
168
+ const reactedMessage = await deps.fetchSlackMessageByTs(item.channel, item.ts);
169
+ if (!reactedMessage) {
170
+ const reason = isInterruptReaction
171
+ ? "identify Slack thread for interrupt reaction"
172
+ : "fetch reacted message";
173
+ throw new Error(`Unable to ${reason} ${item.ts} in channel ${item.channel}`);
174
+ }
175
+ const threadTs = reactedMessage?.thread_ts ??
176
+ reactedMessage?.ts ??
177
+ item.ts;
178
+ const threads = deps.getThreads();
179
+ if (!threads.has(threadTs)) {
180
+ threads.set(threadTs, {
181
+ channelId: item.channel,
182
+ threadTs,
183
+ userId: reactedMessage?.user ?? user,
184
+ source: "slack",
185
+ });
186
+ }
187
+ const ownership = await ensureLocalThreadOwnership(item.channel, threadTs);
188
+ if (ownership !== "continue") {
189
+ return;
190
+ }
191
+ const reactorName = isInterruptReaction
192
+ ? await deps.resolveUser(user).catch(() => user)
193
+ : await deps.resolveUser(user);
194
+ if (shuttingDown)
195
+ return;
196
+ if (isInterruptReaction) {
197
+ interruptSinglePlayerTurn(ctx);
198
+ ctx.ui.notify(`${reactorName} requested an interrupt with :${reactionName}:`, "warning");
199
+ await deps.addReaction(item.channel, item.ts, "white_check_mark");
200
+ return;
201
+ }
202
+ const reactedMessageAuthorId = reactedMessage?.user ?? evt.item_user;
203
+ const reactedMessageAuthor = reactedMessageAuthorId
204
+ ? await deps.resolveUser(reactedMessageAuthorId)
205
+ : reactedMessage?.bot_id
206
+ ? "bot"
207
+ : "unknown";
208
+ if (shuttingDown)
209
+ return;
210
+ const reactedMessageText = typeof reactedMessage?.text === "string" && reactedMessage.text.trim().length > 0
211
+ ? reactedMessage.text
212
+ : "(no text)";
213
+ const reactionMessage = buildReactionTriggerMessage({
214
+ reactionName,
215
+ command,
216
+ reactorName,
217
+ channel: item.channel,
218
+ threadTs,
219
+ messageTs: item.ts,
220
+ reactedMessageText,
221
+ reactedMessageAuthor,
222
+ });
223
+ ctx.ui.notify(`${reactorName} reacted with :${reactionName}:`, "info");
224
+ const threadInfo = threads.get(threadTs);
225
+ deps.pushInboxMessage({
226
+ channel: item.channel,
227
+ threadTs,
228
+ userId: user,
229
+ text: reactionMessage,
230
+ timestamp: evt.event_ts ?? item.ts,
231
+ scope: buildSlackThreadRuntimeScope({
232
+ channelId: item.channel,
233
+ context: threadInfo?.context,
234
+ }),
235
+ });
236
+ deps.persistState();
237
+ deps.updateBadge();
238
+ await deps.addReaction(item.channel, item.ts, "white_check_mark");
239
+ deps.maybeDrainInboxIfIdle(ctx);
240
+ }
241
+ catch (err) {
242
+ console.error(`[slack-bridge] reaction trigger failed: ${deps.formatError(err)}`);
243
+ await deps.addReaction(item.channel, item.ts, "x");
244
+ }
245
+ }
246
+ async function onMessage(evt, ctx) {
247
+ if (shuttingDown)
248
+ return;
249
+ const threads = deps.getThreads();
250
+ const classified = classifyMessage(evt, getCurrentBotUserId(), new Set(threads.keys()));
251
+ if (!classified.relevant)
252
+ return;
253
+ const { threadTs, channel, userId, text, isDM, isChannelMention, messageTs, metadata } = classified;
254
+ if (!threads.has(threadTs)) {
255
+ threads.set(threadTs, { channelId: channel, threadTs, userId, source: "slack" });
256
+ }
257
+ const ownership = await ensureLocalThreadOwnership(channel, threadTs);
258
+ if (ownership !== "continue") {
259
+ return;
260
+ }
261
+ if (!deps.isUserAllowed(userId)) {
262
+ await deps.slack("chat.postMessage", deps.getBotToken(), {
263
+ channel,
264
+ thread_ts: threadTs,
265
+ text: "Sorry, I can only respond to authorized users. Please contact an admin if you need access.",
266
+ });
267
+ return;
268
+ }
269
+ if (isDM) {
270
+ deps.setLastDmChannel(channel);
271
+ }
272
+ deps.persistState();
273
+ const confirmationResult = deps.consumeConfirmationReply(threadTs, text);
274
+ const messageText = confirmationResult === null
275
+ ? text
276
+ : confirmationResult.approved
277
+ ? `${text}\n\nâś… User approved security confirmation request in this thread.`
278
+ : `${text}\n\n❌ User denied security confirmation request in this thread.`;
279
+ const name = await deps.resolveUser(userId);
280
+ if (shuttingDown)
281
+ return;
282
+ ctx.ui.notify(`${name}: ${text.slice(0, 100)}`, "info");
283
+ void deps.beginThreadStatus?.(channel, threadTs, "is thinking…");
284
+ void deps.addReaction(channel, messageTs, "eyes");
285
+ const pending = deps.getPendingEyes().get(threadTs) ?? [];
286
+ pending.push({ channel, messageTs });
287
+ deps.getPendingEyes().set(threadTs, pending);
288
+ const threadInfo = threads.get(threadTs);
289
+ deps.pushInboxMessage({
290
+ channel,
291
+ threadTs,
292
+ userId,
293
+ text: messageText,
294
+ timestamp: messageTs,
295
+ scope: buildSlackThreadRuntimeScope({
296
+ channelId: channel,
297
+ context: threadInfo?.context,
298
+ }),
299
+ ...(isChannelMention && { isChannelMention: true }),
300
+ ...(metadata ? { metadata } : {}),
301
+ });
302
+ deps.updateBadge();
303
+ deps.maybeDrainInboxIfIdle(ctx);
304
+ }
305
+ async function queueInteractiveInboxEvent(normalized, ctx) {
306
+ const threads = deps.getThreads();
307
+ if (!threads.has(normalized.threadTs)) {
308
+ threads.set(normalized.threadTs, {
309
+ channelId: normalized.channel,
310
+ threadTs: normalized.threadTs,
311
+ userId: normalized.userId,
312
+ source: "slack",
313
+ });
314
+ }
315
+ const ownership = await ensureLocalThreadOwnership(normalized.channel, normalized.threadTs);
316
+ if (ownership !== "continue") {
317
+ return;
318
+ }
319
+ if (!deps.isUserAllowed(normalized.userId)) {
320
+ await deps.slack("chat.postMessage", deps.getBotToken(), {
321
+ channel: normalized.channel,
322
+ thread_ts: normalized.threadTs,
323
+ text: "Sorry, I can only respond to authorized users. Please contact an admin if you need access.",
324
+ });
325
+ return;
326
+ }
327
+ if (normalized.channel.startsWith("D")) {
328
+ deps.setLastDmChannel(normalized.channel);
329
+ }
330
+ deps.persistState();
331
+ void deps.beginThreadStatus?.(normalized.channel, normalized.threadTs, "is thinking…");
332
+ const name = await deps.resolveUser(normalized.userId);
333
+ if (shuttingDown)
334
+ return;
335
+ ctx.ui.notify(`${name}: ${normalized.text.slice(0, 100)}`, "info");
336
+ const threadInfo = threads.get(normalized.threadTs);
337
+ deps.pushInboxMessage({
338
+ channel: normalized.channel,
339
+ threadTs: normalized.threadTs,
340
+ userId: normalized.userId,
341
+ text: normalized.text,
342
+ timestamp: normalized.timestamp,
343
+ metadata: normalized.metadata,
344
+ scope: buildSlackThreadRuntimeScope({
345
+ channelId: normalized.channel,
346
+ context: threadInfo?.context,
347
+ }),
348
+ });
349
+ deps.updateBadge();
350
+ deps.maybeDrainInboxIfIdle(ctx);
351
+ }
352
+ const threadContextPort = {
353
+ resolveThreadChannel: (threadTs) => deps.resolveThreadChannel(threadTs),
354
+ noteThreadReply: (threadTs, channelId) => {
355
+ trackOwnedThread(threadTs, channelId, "slack");
356
+ deps.claimOwnedThread(threadTs, channelId, "slack");
357
+ },
358
+ clearPendingAttention: (threadTs) => {
359
+ clearPendingAttention(threadTs);
360
+ },
361
+ };
362
+ return {
363
+ async connect(ctx) {
364
+ shuttingDown = false;
365
+ const socket = new SlackSocketModeClient({
366
+ slack: deps.slack,
367
+ botToken: deps.getBotToken(),
368
+ appToken: deps.getAppToken(),
369
+ resolveBotUserIdOnConnect: false,
370
+ dedup: deps.dedup,
371
+ abortAndWait: deps.abortSlackRequests,
372
+ onOpen: () => deps.setExtStatus(ctx, "ok"),
373
+ onReconnectScheduled: () => {
374
+ if (!shuttingDown && deps.isSingleRuntimeActive()) {
375
+ deps.setExtStatus(ctx, "reconnecting");
376
+ }
377
+ },
378
+ onError: (error) => {
379
+ if (!isAbortError(error)) {
380
+ console.error(`[slack-bridge] Slack access: ${deps.formatError(error)}`);
381
+ }
382
+ },
383
+ onThreadStarted: (event) => onThreadStarted(event),
384
+ onThreadContextChanged: (event) => onContextChanged(event),
385
+ onAppHomeOpened: (event) => onAppHomeOpened(event, ctx),
386
+ onMessage: (event) => onMessage(event, ctx),
387
+ onReactionAdded: (event) => onReactionAdded(event, ctx),
388
+ onMemberJoinedChannel: async ({ channel, isSelf }) => {
389
+ if (!isSelf)
390
+ return;
391
+ ctx.ui.notify(`Pinet added to channel ${channel}`, "info");
392
+ deps.pushInboxMessage({
393
+ channel,
394
+ threadTs: "",
395
+ userId: "system",
396
+ text: `Pinet was added to channel <#${channel}>. You can now post messages there.`,
397
+ timestamp: String(Date.now() / 1000),
398
+ });
399
+ deps.updateBadge();
400
+ deps.maybeDrainInboxIfIdle(ctx);
401
+ },
402
+ onInteractive: (event) => queueInteractiveInboxEvent(event, ctx),
403
+ });
404
+ slackSocket = socket;
405
+ await socket.connect();
406
+ },
407
+ async disconnect() {
408
+ shuttingDown = true;
409
+ const socket = slackSocket;
410
+ slackSocket = null;
411
+ if (socket) {
412
+ await socket.disconnect();
413
+ return;
414
+ }
415
+ await deps.abortSlackRequests();
416
+ },
417
+ getBotUserId() {
418
+ return slackSocket?.getBotUserId() ?? null;
419
+ },
420
+ isConnected() {
421
+ return slackSocket?.isConnected() ?? false;
422
+ },
423
+ isShuttingDown() {
424
+ return shuttingDown;
425
+ },
426
+ resetShutdownState() {
427
+ shuttingDown = false;
428
+ },
429
+ trackOwnedThread(threadTs, channelId, source = "slack") {
430
+ trackOwnedThread(threadTs, channelId, source);
431
+ },
432
+ getThreadContextPort() {
433
+ return threadContextPort;
434
+ },
435
+ };
436
+ }