botinabox 2.9.2 → 2.9.4

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/bin/botinabox.mjs CHANGED
@@ -1,2 +1,2 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  import('../dist/cli.js').then(m => m.main(process.argv.slice(2)));
@@ -4,7 +4,7 @@ import {
4
4
  extractVoiceTranscript,
5
5
  parseSlackEvent,
6
6
  transcribeAudio
7
- } from "../../chunk-OEMM2LEA.js";
7
+ } from "../../chunk-QBBROFEL.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-5FKJBWPL.js");
75
- const { enrichVoiceMessage: enrichVoiceMessage2 } = await import("../../inbound-5FKJBWPL.js");
74
+ const { parseSlackEvent: parseSlackEvent2 } = await import("../../inbound-WAOVULIZ.js");
75
+ const { enrichVoiceMessage: enrichVoiceMessage2 } = await import("../../inbound-WAOVULIZ.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);
@@ -139,7 +139,7 @@ var SlackBoltAdapter = class {
139
139
  async start() {
140
140
  const boltModule = "@slack/bolt";
141
141
  const bolt = await import(boltModule);
142
- const { enrichVoiceMessage: enrichVoiceMessage2 } = await import("../../inbound-5FKJBWPL.js");
142
+ const { enrichVoiceMessage: enrichVoiceMessage2 } = await import("../../inbound-WAOVULIZ.js");
143
143
  const boltApp = new bolt.App({
144
144
  token: this.config.botToken,
145
145
  appToken: this.config.appToken,
@@ -0,0 +1,224 @@
1
+ // src/channels/slack/transcribe.ts
2
+ import { execFileSync } from "child_process";
3
+ import { writeFileSync, unlinkSync, mkdirSync } from "fs";
4
+ import { join } from "path";
5
+ import { randomUUID } from "crypto";
6
+ import os from "os";
7
+ import { createRequire } from "module";
8
+ var TEMP_DIR = join(os.tmpdir(), "botinabox-audio");
9
+ async function transcribeAudio(audioBuffer, filename, opts) {
10
+ let whisper;
11
+ try {
12
+ const require2 = createRequire(import.meta.url);
13
+ const mod = require2("whisper-node");
14
+ whisper = mod.whisper ?? mod.default ?? mod;
15
+ } catch {
16
+ console.warn("[botinabox] whisper-node not installed \u2014 voice transcription unavailable. Run: npm install whisper-node && npx whisper-node download");
17
+ return null;
18
+ }
19
+ try {
20
+ execFileSync("ffmpeg", ["-version"], { stdio: "ignore" });
21
+ } catch {
22
+ console.warn("[botinabox] ffmpeg not found \u2014 required for audio conversion. Install: brew install ffmpeg");
23
+ return null;
24
+ }
25
+ const id = randomUUID().slice(0, 8);
26
+ const ext = filename.split(".").pop() ?? "aac";
27
+ mkdirSync(TEMP_DIR, { recursive: true });
28
+ const inputPath = join(TEMP_DIR, `${id}.${ext}`);
29
+ const wavPath = join(TEMP_DIR, `${id}.wav`);
30
+ try {
31
+ writeFileSync(inputPath, audioBuffer);
32
+ execFileSync("ffmpeg", ["-y", "-i", inputPath, "-ar", "16000", "-ac", "1", "-c:a", "pcm_s16le", wavPath], {
33
+ stdio: "ignore",
34
+ timeout: 3e4
35
+ });
36
+ const segments = await whisper(wavPath, {
37
+ modelName: opts?.modelName ?? "base.en",
38
+ whisperOptions: {
39
+ language: opts?.language ?? "auto"
40
+ }
41
+ });
42
+ if (!segments || segments.length === 0) return null;
43
+ return segments.map((s) => s.speech).join(" ").trim();
44
+ } catch (err) {
45
+ console.error("[botinabox] Transcription failed:", err);
46
+ return null;
47
+ } finally {
48
+ try {
49
+ unlinkSync(inputPath);
50
+ } catch {
51
+ }
52
+ try {
53
+ unlinkSync(wavPath);
54
+ } catch {
55
+ }
56
+ }
57
+ }
58
+ async function downloadAudio(url, token) {
59
+ try {
60
+ const resp = await fetch(url, {
61
+ headers: { Authorization: `Bearer ${token}` }
62
+ });
63
+ if (!resp.ok) {
64
+ console.error(`[botinabox] Audio download failed: ${resp.status} ${resp.statusText}`);
65
+ return null;
66
+ }
67
+ return Buffer.from(await resp.arrayBuffer());
68
+ } catch (err) {
69
+ console.error("[botinabox] Audio download error:", err);
70
+ return null;
71
+ }
72
+ }
73
+
74
+ // src/channels/slack/media-type.ts
75
+ var FILETYPE_MAP = {
76
+ // image
77
+ jpg: "image",
78
+ jpeg: "image",
79
+ png: "image",
80
+ gif: "image",
81
+ webp: "image",
82
+ heic: "image",
83
+ svg: "image",
84
+ bmp: "image",
85
+ // video
86
+ mp4: "video",
87
+ mov: "video",
88
+ webm: "video",
89
+ avi: "video",
90
+ mkv: "video",
91
+ // audio (also handled by voice-message path — included for completeness)
92
+ aac: "audio",
93
+ m4a: "audio",
94
+ mp3: "audio",
95
+ wav: "audio",
96
+ ogg: "audio",
97
+ flac: "audio",
98
+ // pdf
99
+ pdf: "pdf",
100
+ // doc
101
+ gdoc: "doc",
102
+ docx: "doc",
103
+ doc: "doc",
104
+ md: "doc",
105
+ txt: "doc",
106
+ rtf: "doc",
107
+ // excel
108
+ gsheet: "excel",
109
+ xlsx: "excel",
110
+ xls: "excel",
111
+ csv: "excel",
112
+ tsv: "excel",
113
+ // presentation
114
+ gslide: "presentation",
115
+ pptx: "presentation",
116
+ ppt: "presentation",
117
+ key: "presentation",
118
+ // html
119
+ html: "html",
120
+ htm: "html"
121
+ };
122
+ function slackFiletypeToMediaType(filetype) {
123
+ if (!filetype) return "misc";
124
+ return FILETYPE_MAP[filetype.toLowerCase()] ?? "misc";
125
+ }
126
+ var URL_REGEX = /https?:\/\/[^\s<>"')]+/g;
127
+ function extractUrls(text) {
128
+ if (!text) return [];
129
+ const matches = text.match(URL_REGEX);
130
+ if (!matches) return [];
131
+ return Array.from(new Set(matches.map((u) => u.replace(/[.,;:!?)]+$/, ""))));
132
+ }
133
+
134
+ // src/channels/slack/inbound.ts
135
+ var AUDIO_TYPES = /* @__PURE__ */ new Set(["aac", "mp4", "m4a", "ogg", "webm", "mp3", "wav"]);
136
+ function extractVoiceTranscript(file) {
137
+ const isAudio = file.subtype === "slack_audio" || AUDIO_TYPES.has(file.filetype ?? "");
138
+ if (!isAudio) return null;
139
+ const transcript = file.transcription?.preview?.content ?? (typeof file.preview === "string" ? file.preview : null);
140
+ return transcript ?? null;
141
+ }
142
+ function parseSlackEvent(event) {
143
+ const id = event.client_msg_id ?? event.ts ?? event.event_ts ?? `slack-${Date.now()}`;
144
+ const channel = event.channel ?? "unknown";
145
+ const from = event.user ?? "unknown";
146
+ const isChannel = channel.startsWith("C") || channel.startsWith("G");
147
+ const threadId = event.thread_ts ?? (isChannel ? event.ts : void 0);
148
+ const receivedAt = event.ts ? new Date(parseFloat(event.ts) * 1e3).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
149
+ let body = event.text ?? "";
150
+ if (event.subtype === "file_share" && event.files?.length) {
151
+ for (const file of event.files) {
152
+ const transcript = extractVoiceTranscript(file);
153
+ if (transcript) {
154
+ body = body ? `${body}
155
+
156
+ [Voice message] ${transcript}` : `[Voice message] ${transcript}`;
157
+ break;
158
+ }
159
+ }
160
+ }
161
+ if (event.subtype === "file_share" && event.files?.length && !body) {
162
+ const hasAudio = event.files.some(
163
+ (f) => f.subtype === "slack_audio" || AUDIO_TYPES.has(f.filetype ?? "")
164
+ );
165
+ if (hasAudio) {
166
+ body = "[Voice message \u2014 no transcript available]";
167
+ }
168
+ }
169
+ const attachments = [];
170
+ if (event.subtype === "file_share" && event.files?.length) {
171
+ for (const file of event.files) {
172
+ const isAudio = file.subtype === "slack_audio" || AUDIO_TYPES.has(file.filetype ?? "");
173
+ if (isAudio) continue;
174
+ attachments.push({
175
+ type: slackFiletypeToMediaType(file.filetype),
176
+ url: file.url_private,
177
+ mimeType: file.mimetype,
178
+ filename: file.name ?? file.title,
179
+ size: file.size
180
+ });
181
+ }
182
+ }
183
+ const urls = extractUrls(body);
184
+ for (const url of urls) {
185
+ attachments.push({ type: "link", url });
186
+ }
187
+ return {
188
+ id,
189
+ channel,
190
+ from,
191
+ body,
192
+ threadId,
193
+ attachments: attachments.length > 0 ? attachments : void 0,
194
+ receivedAt,
195
+ raw: event
196
+ };
197
+ }
198
+ async function enrichVoiceMessage(msg, botToken) {
199
+ if (!msg.body.includes("[Voice message \u2014 no transcript available]")) return msg;
200
+ const raw = msg.raw;
201
+ const files = raw?.files;
202
+ if (!files?.length) return msg;
203
+ const audioFile = files.find(
204
+ (f) => f.subtype === "slack_audio" || AUDIO_TYPES.has(f.filetype ?? "")
205
+ );
206
+ if (!audioFile?.url_private) return msg;
207
+ const buffer = await downloadAudio(audioFile.url_private, botToken);
208
+ if (!buffer) return msg;
209
+ const filename = audioFile.name ?? `voice.${audioFile.filetype ?? "aac"}`;
210
+ const transcript = await transcribeAudio(buffer, filename);
211
+ if (!transcript) return msg;
212
+ return {
213
+ ...msg,
214
+ body: `[Voice message] ${transcript}`
215
+ };
216
+ }
217
+
218
+ export {
219
+ transcribeAudio,
220
+ downloadAudio,
221
+ extractVoiceTranscript,
222
+ parseSlackEvent,
223
+ enrichVoiceMessage
224
+ };
@@ -122,7 +122,7 @@ export declare class ChatPipelineV2 {
122
122
  */
123
123
  private think;
124
124
  /**
125
- * Build conversation history from channel messages.
125
+ * Build conversation history from thread messages.
126
126
  * Includes BOTH user and assistant messages (unlike v1 which excluded bot messages).
127
127
  */
128
128
  private buildHistory;
@@ -0,0 +1,11 @@
1
+ import {
2
+ enrichVoiceMessage,
3
+ extractVoiceTranscript,
4
+ parseSlackEvent
5
+ } from "./chunk-QBBROFEL.js";
6
+ import "./chunk-3RG5ZIWI.js";
7
+ export {
8
+ enrichVoiceMessage,
9
+ extractVoiceTranscript,
10
+ parseSlackEvent
11
+ };
package/dist/index.js CHANGED
@@ -2178,7 +2178,7 @@ var ChatPipelineV2 = class {
2178
2178
  if (this.messageFilter && !this.messageFilter(msg)) return;
2179
2179
  if (await this.isDuplicate(msg)) return;
2180
2180
  const channelId = msg.account ?? "";
2181
- const threadTs = channelId || msg.threadId || msg.id;
2181
+ const threadTs = msg.threadId || channelId || msg.id;
2182
2182
  if (threadTs && channelId) {
2183
2183
  this.threadChannelMap.set(threadTs, channelId);
2184
2184
  }
@@ -2186,7 +2186,7 @@ var ChatPipelineV2 = class {
2186
2186
  const { messageId } = await this.messageStore.storeInbound(msgWithThread);
2187
2187
  await this.hooks.emit("typing.start", { channel: this.channel, threadId: threadTs });
2188
2188
  try {
2189
- const history = await this.buildHistory(channelId);
2189
+ const history = await this.buildHistory(threadTs);
2190
2190
  let systemPrompt = this.config.systemPrompt;
2191
2191
  if (this.config.includeSystemContext !== false) {
2192
2192
  const ctx2 = await buildSystemContext(this.db, this.config.systemContextOptions);
@@ -2333,10 +2333,10 @@ ${ctx2}`;
2333
2333
  return { text: finalText, tasksDispatched };
2334
2334
  }
2335
2335
  /**
2336
- * Build conversation history from channel messages.
2336
+ * Build conversation history from thread messages.
2337
2337
  * Includes BOTH user and assistant messages (unlike v1 which excluded bot messages).
2338
2338
  */
2339
- async buildHistory(channelId) {
2339
+ async buildHistory(threadTs) {
2340
2340
  const maxMessages = this.config.history?.maxMessages ?? DEFAULT_MAX_MESSAGES;
2341
2341
  const maxAgeDays = this.config.history?.maxAgeDays ?? DEFAULT_MAX_AGE_DAYS;
2342
2342
  const includeAssistant = this.config.history?.includeAssistant !== false;
@@ -2344,7 +2344,7 @@ ${ctx2}`;
2344
2344
  let rows;
2345
2345
  try {
2346
2346
  rows = await this.db.query("messages", {
2347
- where: { channel: this.channel },
2347
+ where: { thread_id: threadTs },
2348
2348
  orderBy: "created_at",
2349
2349
  orderDir: "desc",
2350
2350
  limit: maxMessages
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botinabox",
3
- "version": "2.9.2",
3
+ "version": "2.9.4",
4
4
  "description": "Bot in a Box — framework for building multi-agent bots",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -60,7 +60,7 @@
60
60
  "@types/uuid": "^10.0.0",
61
61
  "ajv": "^8.17.1",
62
62
  "cron-parser": "^4.9.0",
63
- "latticesql": "^1.6.6",
63
+ "latticesql": "^1.6.10",
64
64
  "uuid": "^13.0.0",
65
65
  "yaml": "^2.7.0"
66
66
  },
@@ -1,82 +0,0 @@
1
- import { c as ContentBlock } from './provider-BHkqkSdq.js';
2
-
3
- /** Channel adapter types — Story 1.5 / 4.1 */
4
-
5
- type ChatType = "direct" | "group" | "channel";
6
- type FormattingMode = "markdown" | "mrkdwn" | "html" | "plain";
7
- interface ChannelCapabilities {
8
- chatTypes: ChatType[];
9
- threads: boolean;
10
- reactions: boolean;
11
- editing: boolean;
12
- media: boolean;
13
- polls: boolean;
14
- maxTextLength: number;
15
- formattingMode: FormattingMode;
16
- }
17
- interface ChannelMeta {
18
- displayName: string;
19
- icon?: string;
20
- homepage?: string;
21
- }
22
- interface InboundMessage {
23
- id: string;
24
- channel: string;
25
- account?: string;
26
- from: string;
27
- userId?: string;
28
- body: string;
29
- threadId?: string;
30
- replyToId?: string;
31
- attachments?: Attachment[];
32
- /**
33
- * Multimodal content blocks produced by attachment enrichers.
34
- * When set, ChatPipeline builds a multimodal user message that passes
35
- * these blocks through to the LLM provider alongside `body`.
36
- */
37
- attachmentBlocks?: ContentBlock[];
38
- receivedAt: string;
39
- raw?: unknown;
40
- }
41
- type AttachmentMediaType = "image" | "video" | "audio" | "pdf" | "doc" | "excel" | "presentation" | "html" | "link" | "misc";
42
- interface Attachment {
43
- type: AttachmentMediaType;
44
- url?: string;
45
- mimeType?: string;
46
- filename?: string;
47
- size?: number;
48
- }
49
- interface OutboundPayload {
50
- text: string;
51
- threadId?: string;
52
- replyToId?: string;
53
- attachments?: Attachment[];
54
- }
55
- interface SendResult {
56
- success: boolean;
57
- messageId?: string;
58
- error?: string;
59
- }
60
- interface HealthStatus {
61
- ok: boolean;
62
- latencyMs?: number;
63
- error?: string;
64
- }
65
- type ChannelConfig = Record<string, unknown>;
66
- interface ChannelAdapter {
67
- /** Unique identifier for this adapter instance */
68
- id: string;
69
- meta: ChannelMeta;
70
- capabilities: ChannelCapabilities;
71
- connect(config: ChannelConfig): Promise<void>;
72
- disconnect(): Promise<void>;
73
- healthCheck(): Promise<HealthStatus>;
74
- send(target: {
75
- peerId: string;
76
- threadId?: string;
77
- }, payload: OutboundPayload): Promise<SendResult>;
78
- /** Called when a message arrives — set by the framework */
79
- onMessage?: (message: InboundMessage) => Promise<void>;
80
- }
81
-
82
- 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,73 +0,0 @@
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 };