botinabox 2.7.11 → 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,73 @@
1
+ /** Channel adapter types — Story 1.5 / 4.1 */
2
+ type ChatType = "direct" | "group" | "channel";
3
+ type FormattingMode = "markdown" | "mrkdwn" | "html" | "plain";
4
+ interface ChannelCapabilities {
5
+ chatTypes: ChatType[];
6
+ threads: boolean;
7
+ reactions: boolean;
8
+ editing: boolean;
9
+ media: boolean;
10
+ polls: boolean;
11
+ maxTextLength: number;
12
+ formattingMode: FormattingMode;
13
+ }
14
+ interface ChannelMeta {
15
+ displayName: string;
16
+ icon?: string;
17
+ homepage?: string;
18
+ }
19
+ interface InboundMessage {
20
+ id: string;
21
+ channel: string;
22
+ account?: string;
23
+ from: string;
24
+ userId?: string;
25
+ body: string;
26
+ threadId?: string;
27
+ replyToId?: string;
28
+ attachments?: Attachment[];
29
+ receivedAt: string;
30
+ raw?: unknown;
31
+ }
32
+ type AttachmentMediaType = "image" | "video" | "audio" | "pdf" | "doc" | "excel" | "presentation" | "html" | "link" | "misc";
33
+ interface Attachment {
34
+ type: AttachmentMediaType;
35
+ url?: string;
36
+ mimeType?: string;
37
+ filename?: string;
38
+ size?: number;
39
+ }
40
+ interface OutboundPayload {
41
+ text: string;
42
+ threadId?: string;
43
+ replyToId?: string;
44
+ attachments?: Attachment[];
45
+ }
46
+ interface SendResult {
47
+ success: boolean;
48
+ messageId?: string;
49
+ error?: string;
50
+ }
51
+ interface HealthStatus {
52
+ ok: boolean;
53
+ latencyMs?: number;
54
+ error?: string;
55
+ }
56
+ type ChannelConfig = Record<string, unknown>;
57
+ interface ChannelAdapter {
58
+ /** Unique identifier for this adapter instance */
59
+ id: string;
60
+ meta: ChannelMeta;
61
+ capabilities: ChannelCapabilities;
62
+ connect(config: ChannelConfig): Promise<void>;
63
+ disconnect(): Promise<void>;
64
+ healthCheck(): Promise<HealthStatus>;
65
+ send(target: {
66
+ peerId: string;
67
+ threadId?: string;
68
+ }, payload: OutboundPayload): Promise<SendResult>;
69
+ /** Called when a message arrives — set by the framework */
70
+ onMessage?: (message: InboundMessage) => Promise<void>;
71
+ }
72
+
73
+ export type { Attachment as A, ChannelAdapter as C, FormattingMode as F, HealthStatus as H, InboundMessage as I, OutboundPayload as O, SendResult as S, AttachmentMediaType as a, ChannelCapabilities as b, ChannelConfig as c, ChannelMeta as d, ChatType as e };
@@ -1,4 +1,4 @@
1
- import { C as ChannelAdapter, c as ChannelMeta, a as ChannelCapabilities, I as InboundMessage, b as ChannelConfig, H as HealthStatus, O as OutboundPayload, S as SendResult } from '../../channel-06G0vbIn.js';
1
+ import { C as ChannelAdapter, d as ChannelMeta, b as ChannelCapabilities, I as InboundMessage, c as ChannelConfig, H as HealthStatus, O as OutboundPayload, S as SendResult } from '../../channel-DziSPayj.js';
2
2
 
3
3
  /**
4
4
  * DiscordAdapter — ChannelAdapter implementation for Discord.
@@ -1,7 +1,7 @@
1
- import { C as ChannelAdapter, c as ChannelMeta, a as ChannelCapabilities, I as InboundMessage, b as ChannelConfig, H as HealthStatus, O as OutboundPayload, S as SendResult } from '../../channel-06G0vbIn.js';
2
- import { H as HookBus, c as ChatPipeline } from '../../chat-pipeline-DuNX5WoL.js';
1
+ import { C as ChannelAdapter, d as ChannelMeta, b as ChannelCapabilities, I as InboundMessage, c as ChannelConfig, H as HealthStatus, O as OutboundPayload, S as SendResult, A as Attachment, a as AttachmentMediaType } from '../../channel-DziSPayj.js';
2
+ import { H as HookBus, c as ChatPipeline } from '../../chat-pipeline-BGgmH_ap.js';
3
3
  import 'better-sqlite3';
4
- import '../../provider-DLGUfnNx.js';
4
+ import '../../provider-BHkqkSdq.js';
5
5
 
6
6
  /**
7
7
  * SlackAdapter — ChannelAdapter implementation for Slack.
@@ -162,6 +162,27 @@ interface SlackConfig {
162
162
  signingSecret?: string;
163
163
  }
164
164
 
165
+ /**
166
+ * An AttachmentEnricher downloads an attachment and extracts its textual content.
167
+ *
168
+ * Returns the extracted text on success, or `null` on failure. The framework
169
+ * surfaces the filename/URL as a fallback when the enricher returns null.
170
+ *
171
+ * @param attachment - The attachment metadata (type, url, filename, etc.)
172
+ * @param botToken - Slack bot token, required for authenticated downloads from url_private
173
+ */
174
+ type AttachmentEnricher = (attachment: Attachment, botToken: string) => Promise<string | null>;
175
+ /**
176
+ * A map from attachment type to the enricher that handles it.
177
+ * Types without an entry fall through to the framework's default behavior
178
+ * (surfacing filename + URL in the message body).
179
+ */
180
+ type AttachmentEnricherMap = Partial<Record<AttachmentMediaType, AttachmentEnricher>>;
181
+ /** Internal: the result of running enrichAttachments on a message. */
182
+ interface EnrichedMessage extends InboundMessage {
183
+ body: string;
184
+ }
185
+
165
186
  /**
166
187
  * SlackBoltAdapter — real Bolt Socket Mode integration for botinabox.
167
188
  *
@@ -181,6 +202,8 @@ interface SlackBoltAdapterConfig {
181
202
  appToken: string;
182
203
  hooks: HookBus;
183
204
  pipeline: ChatPipeline;
205
+ /** Optional per-type attachment enrichers. */
206
+ attachmentEnrichers?: AttachmentEnricherMap;
184
207
  }
185
208
  declare class SlackBoltAdapter {
186
209
  private app;
@@ -190,4 +213,52 @@ declare class SlackBoltAdapter {
190
213
  stop(): Promise<void>;
191
214
  }
192
215
 
193
- export { type BoltClient, SlackAdapter, SlackBoltAdapter, type SlackBoltAdapterConfig, type SlackConfig, type SlackEvent, type SlackFile, type TranscribeOptions, type TranscribeResult, createSlackAdapter as default, downloadAudio, enrichVoiceMessage, extractVoiceTranscript, formatForSlack, parseSlackEvent, transcribeAudio };
216
+ /**
217
+ * Run enrichers over each attachment on an InboundMessage and append extracted
218
+ * content to the message body.
219
+ *
220
+ * Enrichers are run sequentially. If an enricher throws or returns null, the
221
+ * attachment is surfaced as `[Attached: <filename>]` with no content — the
222
+ * LLM still sees that a file was present.
223
+ *
224
+ * Extracted text is appended to the body in this format:
225
+ *
226
+ * <original body>
227
+ *
228
+ * [Attached: invoice.pdf]
229
+ * <extracted content>
230
+ */
231
+ declare function enrichAttachments(msg: InboundMessage, botToken: string, enrichers: AttachmentEnricherMap): Promise<InboundMessage>;
232
+
233
+ interface ImageEnricherConfig {
234
+ /** Anthropic API key for vision calls. */
235
+ apiKey: string;
236
+ /** Model to use. Defaults to claude-sonnet-4-6. */
237
+ model?: string;
238
+ /** Max tokens for the description. Defaults to 1024. */
239
+ maxTokens?: number;
240
+ /** Prompt used for image description. */
241
+ prompt?: string;
242
+ }
243
+ /**
244
+ * Build an enricher that sends Slack-hosted images to Claude's vision API
245
+ * and returns a text description.
246
+ *
247
+ * Consumer must provide an Anthropic API key. The Anthropic SDK is a peer
248
+ * dependency of botinabox — the consumer must have it installed.
249
+ */
250
+ declare function createImageEnricher(config: ImageEnricherConfig): AttachmentEnricher;
251
+
252
+ interface PdfEnricherConfig {
253
+ apiKey: string;
254
+ model?: string;
255
+ maxTokens?: number;
256
+ prompt?: string;
257
+ }
258
+ /**
259
+ * Build an enricher that sends Slack-hosted PDFs to Claude's document API
260
+ * and returns a text summary. Requires Claude 3.5+.
261
+ */
262
+ declare function createPdfEnricher(config: PdfEnricherConfig): AttachmentEnricher;
263
+
264
+ export { type AttachmentEnricher, type AttachmentEnricherMap, type BoltClient, type EnrichedMessage, type ImageEnricherConfig, type PdfEnricherConfig, SlackAdapter, SlackBoltAdapter, type SlackBoltAdapterConfig, type SlackConfig, type SlackEvent, type SlackFile, type TranscribeOptions, type TranscribeResult, createImageEnricher, createPdfEnricher, createSlackAdapter as default, downloadAudio, enrichAttachments, enrichVoiceMessage, extractVoiceTranscript, formatForSlack, parseSlackEvent, transcribeAudio };
@@ -4,7 +4,7 @@ import {
4
4
  extractVoiceTranscript,
5
5
  parseSlackEvent,
6
6
  transcribeAudio
7
- } from "../../chunk-NNPCKR6G.js";
7
+ } from "../../chunk-OEMM2LEA.js";
8
8
  import {
9
9
  chunkText
10
10
  } from "../../chunk-ZTZFPTOQ.js";
@@ -71,8 +71,8 @@ var SlackAdapter = class {
71
71
  /** Simulate receiving an inbound message (for testing/webhooks). */
72
72
  async receive(event) {
73
73
  if (this.onMessage) {
74
- const { parseSlackEvent: parseSlackEvent2 } = await import("../../inbound-IBKXBFZF.js");
75
- const { enrichVoiceMessage: enrichVoiceMessage2 } = await import("../../inbound-IBKXBFZF.js");
74
+ const { parseSlackEvent: parseSlackEvent2 } = await import("../../inbound-5FKJBWPL.js");
75
+ const { enrichVoiceMessage: enrichVoiceMessage2 } = await import("../../inbound-5FKJBWPL.js");
76
76
  let msg = parseSlackEvent2(event);
77
77
  if (msg.body.includes("[Voice message") && this.config?.botToken) {
78
78
  msg = await enrichVoiceMessage2(msg, this.config.botToken);
@@ -85,6 +85,33 @@ function createSlackAdapter(client) {
85
85
  return new SlackAdapter(client);
86
86
  }
87
87
 
88
+ // src/channels/slack/enrichers/enrich.ts
89
+ async function enrichAttachments(msg, botToken, enrichers) {
90
+ if (!msg.attachments?.length) return msg;
91
+ const parts = msg.body ? [msg.body] : [];
92
+ for (const att of msg.attachments) {
93
+ const enricher = enrichers[att.type];
94
+ const label = att.filename ?? att.url ?? att.type;
95
+ if (!enricher) {
96
+ parts.push(`[Attached: ${label}]`);
97
+ continue;
98
+ }
99
+ let extracted = null;
100
+ try {
101
+ extracted = await enricher(att, botToken);
102
+ } catch {
103
+ extracted = null;
104
+ }
105
+ if (extracted) {
106
+ parts.push(`[Attached: ${label}]
107
+ ${extracted}`);
108
+ } else {
109
+ parts.push(`[Attached: ${label}]`);
110
+ }
111
+ }
112
+ return { ...msg, body: parts.join("\n\n") };
113
+ }
114
+
88
115
  // src/channels/slack/bolt-adapter.ts
89
116
  var SlackBoltAdapter = class {
90
117
  app = null;
@@ -95,7 +122,7 @@ var SlackBoltAdapter = class {
95
122
  async start() {
96
123
  const boltModule = "@slack/bolt";
97
124
  const bolt = await import(boltModule);
98
- const { enrichVoiceMessage: enrichVoiceMessage2 } = await import("../../inbound-IBKXBFZF.js");
125
+ const { enrichVoiceMessage: enrichVoiceMessage2 } = await import("../../inbound-5FKJBWPL.js");
99
126
  const boltApp = new bolt.App({
100
127
  token: this.config.botToken,
101
128
  appToken: this.config.appToken,
@@ -113,6 +140,9 @@ var SlackBoltAdapter = class {
113
140
  if (inbound.body.includes("[Voice message \u2014 no transcript available]")) {
114
141
  inbound = await enrichVoiceMessage2(inbound, botToken);
115
142
  }
143
+ if (inbound.attachments?.length && this.config.attachmentEnrichers) {
144
+ inbound = await enrichAttachments(inbound, botToken, this.config.attachmentEnrichers);
145
+ }
116
146
  await hooks.emit("message.inbound", inbound);
117
147
  });
118
148
  hooks.register("response.ready", async (ctx) => {
@@ -164,11 +194,113 @@ var SlackBoltAdapter = class {
164
194
  }
165
195
  }
166
196
  };
197
+
198
+ // src/channels/slack/enrichers/image-enricher.ts
199
+ var DEFAULT_PROMPT = "Describe this image in detail. Include any visible text (OCR), objects, people, layout, and context. Be thorough \u2014 the description will be used as input to another agent that cannot see the image.";
200
+ async function downloadAsBase64(urlPrivate, botToken) {
201
+ try {
202
+ const response = await fetch(urlPrivate, {
203
+ headers: { Authorization: `Bearer ${botToken}` },
204
+ signal: AbortSignal.timeout(3e4)
205
+ });
206
+ if (!response.ok) return null;
207
+ const buffer = Buffer.from(await response.arrayBuffer());
208
+ return buffer.toString("base64");
209
+ } catch {
210
+ return null;
211
+ }
212
+ }
213
+ function createImageEnricher(config) {
214
+ const { apiKey, model = "claude-sonnet-4-6", maxTokens = 1024, prompt = DEFAULT_PROMPT } = config;
215
+ return async (att, botToken) => {
216
+ if (!att.url) return null;
217
+ const mediaType = att.mimeType ?? "image/jpeg";
218
+ const base64 = await downloadAsBase64(att.url, botToken);
219
+ if (!base64) return null;
220
+ const anthropicModule = "@anthropic-ai/sdk";
221
+ const sdk = await import(anthropicModule);
222
+ const client = new sdk.default({ apiKey });
223
+ try {
224
+ const message = await client.messages.create({
225
+ model,
226
+ max_tokens: maxTokens,
227
+ messages: [
228
+ {
229
+ role: "user",
230
+ content: [
231
+ {
232
+ type: "image",
233
+ source: { type: "base64", media_type: mediaType, data: base64 }
234
+ },
235
+ { type: "text", text: prompt }
236
+ ]
237
+ }
238
+ ]
239
+ });
240
+ const textBlock = message.content.find((c) => c.type === "text");
241
+ return textBlock?.text ?? null;
242
+ } catch {
243
+ return null;
244
+ }
245
+ };
246
+ }
247
+
248
+ // src/channels/slack/enrichers/pdf-enricher.ts
249
+ var DEFAULT_PROMPT2 = "Summarize this PDF in detail. Include all key facts, numbers, dates, names, and any structured content (tables, lists). The summary will be used as input to another agent that cannot see the PDF.";
250
+ async function downloadAsBase642(urlPrivate, botToken) {
251
+ try {
252
+ const response = await fetch(urlPrivate, {
253
+ headers: { Authorization: `Bearer ${botToken}` },
254
+ signal: AbortSignal.timeout(6e4)
255
+ });
256
+ if (!response.ok) return null;
257
+ const buffer = Buffer.from(await response.arrayBuffer());
258
+ return buffer.toString("base64");
259
+ } catch {
260
+ return null;
261
+ }
262
+ }
263
+ function createPdfEnricher(config) {
264
+ const { apiKey, model = "claude-sonnet-4-6", maxTokens = 4096, prompt = DEFAULT_PROMPT2 } = config;
265
+ return async (att, botToken) => {
266
+ if (!att.url) return null;
267
+ const base64 = await downloadAsBase642(att.url, botToken);
268
+ if (!base64) return null;
269
+ const anthropicModule = "@anthropic-ai/sdk";
270
+ const sdk = await import(anthropicModule);
271
+ const client = new sdk.default({ apiKey });
272
+ try {
273
+ const message = await client.messages.create({
274
+ model,
275
+ max_tokens: maxTokens,
276
+ messages: [
277
+ {
278
+ role: "user",
279
+ content: [
280
+ {
281
+ type: "document",
282
+ source: { type: "base64", media_type: "application/pdf", data: base64 }
283
+ },
284
+ { type: "text", text: prompt }
285
+ ]
286
+ }
287
+ ]
288
+ });
289
+ const textBlock = message.content.find((c) => c.type === "text");
290
+ return textBlock?.text ?? null;
291
+ } catch {
292
+ return null;
293
+ }
294
+ };
295
+ }
167
296
  export {
168
297
  SlackAdapter,
169
298
  SlackBoltAdapter,
299
+ createImageEnricher,
300
+ createPdfEnricher,
170
301
  createSlackAdapter as default,
171
302
  downloadAudio,
303
+ enrichAttachments,
172
304
  enrichVoiceMessage,
173
305
  extractVoiceTranscript,
174
306
  formatForSlack,
@@ -1,4 +1,4 @@
1
- import { C as ChannelAdapter, c as ChannelMeta, a as ChannelCapabilities, I as InboundMessage, b as ChannelConfig, H as HealthStatus, O as OutboundPayload, S as SendResult } from '../../channel-06G0vbIn.js';
1
+ import { C as ChannelAdapter, d as ChannelMeta, b as ChannelCapabilities, I as InboundMessage, c as ChannelConfig, H as HealthStatus, O as OutboundPayload, S as SendResult } from '../../channel-DziSPayj.js';
2
2
 
3
3
  /**
4
4
  * WebhookAdapter — ChannelAdapter implementation for webhook-based channels.