botinabox 1.2.1 → 1.3.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.
- package/README.md +5 -1
- package/dist/channels/slack/index.d.ts +56 -1
- package/dist/channels/slack/index.js +15 -5
- package/dist/chunk-GS2JFL6I.js +144 -0
- package/dist/inbound-AFBUPSPG.js +10 -0
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -12,7 +12,11 @@ A modular TypeScript framework for building multi-agent bots with LLM orchestrat
|
|
|
12
12
|
- **Event-driven hooks** -- Priority-ordered, filter-based event bus for decoupled inter-layer communication.
|
|
13
13
|
- **Budget controls** -- Per-agent and global cost tracking with warning thresholds and hard stops.
|
|
14
14
|
- **Scheduling** -- Database-backed cron and one-time schedules that fire hook events.
|
|
15
|
-
- **
|
|
15
|
+
- **Connectors** -- Generic `Connector<T>` interface for external service integrations. Ships with Google Gmail and Calendar implementations (OAuth2 and service account auth).
|
|
16
|
+
- **Domain tables** -- `defineDomainTables()` and `defineDomainEntityContexts()` for standard multi-agent app schemas (org, project, client, invoice, repository, and more).
|
|
17
|
+
- **Auto-update** -- `autoUpdate()` checks npm for newer versions and installs them at startup.
|
|
18
|
+
- **Cursor persistence** -- `SecretStore.loadCursor()` / `saveCursor()` for persisting sync state across restarts.
|
|
19
|
+
- **Utilities** -- `truncateAtWord()` for word-boundary text truncation, `parseClaudeStream()`, `buildProcessEnv()`, and more.
|
|
16
20
|
- **Security** -- Input sanitization, field length enforcement, audit logging, and HMAC webhook verification.
|
|
17
21
|
|
|
18
22
|
## Install
|
|
@@ -80,6 +80,61 @@ declare function extractVoiceTranscript(file: SlackFile): string | null;
|
|
|
80
80
|
* with `[Voice message]`.
|
|
81
81
|
*/
|
|
82
82
|
declare function parseSlackEvent(event: SlackEvent): InboundMessage;
|
|
83
|
+
/**
|
|
84
|
+
* Enrich a voice message with local transcription when Slack's built-in
|
|
85
|
+
* transcription is unavailable.
|
|
86
|
+
*
|
|
87
|
+
* Downloads the audio file from Slack using the bot token, converts to WAV,
|
|
88
|
+
* and transcribes locally via whisper-node. Returns the original message
|
|
89
|
+
* unchanged if transcription fails or is not needed.
|
|
90
|
+
*
|
|
91
|
+
* @param msg - The parsed inbound message (from parseSlackEvent)
|
|
92
|
+
* @param botToken - Slack bot token for authenticated file download
|
|
93
|
+
* @returns The message with body replaced by transcript, or original on failure
|
|
94
|
+
*/
|
|
95
|
+
declare function enrichVoiceMessage(msg: InboundMessage, botToken: string): Promise<InboundMessage>;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Local voice transcription via whisper-node (whisper.cpp bindings).
|
|
99
|
+
*
|
|
100
|
+
* whisper-node is an optional dependency — if not installed, transcription
|
|
101
|
+
* degrades gracefully (returns null). Requires ffmpeg on the system PATH
|
|
102
|
+
* for audio format conversion.
|
|
103
|
+
*
|
|
104
|
+
* Setup:
|
|
105
|
+
* npm install whisper-node
|
|
106
|
+
* npx whisper-node download # download a model (e.g. base.en)
|
|
107
|
+
* brew install ffmpeg # or equivalent for your platform
|
|
108
|
+
*/
|
|
109
|
+
interface TranscribeOptions {
|
|
110
|
+
/** Whisper model name (default: "base.en"). Run `npx whisper-node download` to get models. */
|
|
111
|
+
modelName?: string;
|
|
112
|
+
/** Language code (default: "auto"). Use "en" for English-only models. */
|
|
113
|
+
language?: string;
|
|
114
|
+
}
|
|
115
|
+
interface TranscribeResult {
|
|
116
|
+
/** The full transcribed text */
|
|
117
|
+
text: string;
|
|
118
|
+
/** Individual segments with timestamps */
|
|
119
|
+
segments: Array<{
|
|
120
|
+
start: string;
|
|
121
|
+
end: string;
|
|
122
|
+
speech: string;
|
|
123
|
+
}>;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Transcribe an audio buffer using whisper-node.
|
|
127
|
+
*
|
|
128
|
+
* @param audioBuffer - Raw audio data (any format ffmpeg can decode)
|
|
129
|
+
* @param filename - Original filename (used for temp file extension)
|
|
130
|
+
* @param opts - Transcription options
|
|
131
|
+
* @returns Transcribed text, or null if transcription fails or whisper-node is not installed
|
|
132
|
+
*/
|
|
133
|
+
declare function transcribeAudio(audioBuffer: Buffer, filename: string, opts?: TranscribeOptions): Promise<string | null>;
|
|
134
|
+
/**
|
|
135
|
+
* Download an audio file from a URL with bearer token authentication.
|
|
136
|
+
*/
|
|
137
|
+
declare function downloadAudio(url: string, token: string): Promise<Buffer | null>;
|
|
83
138
|
|
|
84
139
|
/**
|
|
85
140
|
* Slack outbound formatting.
|
|
@@ -104,4 +159,4 @@ interface SlackConfig {
|
|
|
104
159
|
signingSecret?: string;
|
|
105
160
|
}
|
|
106
161
|
|
|
107
|
-
export { type BoltClient, SlackAdapter, type SlackConfig, type SlackEvent, type SlackFile, createSlackAdapter as default, extractVoiceTranscript, formatForSlack, parseSlackEvent };
|
|
162
|
+
export { type BoltClient, SlackAdapter, type SlackConfig, type SlackEvent, type SlackFile, type TranscribeOptions, type TranscribeResult, createSlackAdapter as default, downloadAudio, enrichVoiceMessage, extractVoiceTranscript, formatForSlack, parseSlackEvent, transcribeAudio };
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
+
downloadAudio,
|
|
3
|
+
enrichVoiceMessage,
|
|
2
4
|
extractVoiceTranscript,
|
|
3
|
-
parseSlackEvent
|
|
4
|
-
|
|
5
|
+
parseSlackEvent,
|
|
6
|
+
transcribeAudio
|
|
7
|
+
} from "../../chunk-GS2JFL6I.js";
|
|
5
8
|
|
|
6
9
|
// src/channels/slack/outbound.ts
|
|
7
10
|
function formatForSlack(text) {
|
|
@@ -64,8 +67,12 @@ var SlackAdapter = class {
|
|
|
64
67
|
/** Simulate receiving an inbound message (for testing/webhooks). */
|
|
65
68
|
async receive(event) {
|
|
66
69
|
if (this.onMessage) {
|
|
67
|
-
const { parseSlackEvent: parseSlackEvent2 } = await import("../../inbound-
|
|
68
|
-
const
|
|
70
|
+
const { parseSlackEvent: parseSlackEvent2 } = await import("../../inbound-AFBUPSPG.js");
|
|
71
|
+
const { enrichVoiceMessage: enrichVoiceMessage2 } = await import("../../inbound-AFBUPSPG.js");
|
|
72
|
+
let msg = parseSlackEvent2(event);
|
|
73
|
+
if (msg.body.includes("[Voice message") && this.config?.botToken) {
|
|
74
|
+
msg = await enrichVoiceMessage2(msg, this.config.botToken);
|
|
75
|
+
}
|
|
69
76
|
await this.onMessage(msg);
|
|
70
77
|
}
|
|
71
78
|
}
|
|
@@ -76,7 +83,10 @@ function createSlackAdapter(client) {
|
|
|
76
83
|
export {
|
|
77
84
|
SlackAdapter,
|
|
78
85
|
createSlackAdapter as default,
|
|
86
|
+
downloadAudio,
|
|
87
|
+
enrichVoiceMessage,
|
|
79
88
|
extractVoiceTranscript,
|
|
80
89
|
formatForSlack,
|
|
81
|
-
parseSlackEvent
|
|
90
|
+
parseSlackEvent,
|
|
91
|
+
transcribeAudio
|
|
82
92
|
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// src/channels/slack/transcribe.ts
|
|
2
|
+
import { execSync } 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
|
+
execSync("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
|
+
execSync(
|
|
33
|
+
`ffmpeg -y -i "${inputPath}" -ar 16000 -ac 1 -c:a pcm_s16le "${wavPath}"`,
|
|
34
|
+
{ stdio: "ignore", 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/inbound.ts
|
|
75
|
+
var AUDIO_TYPES = /* @__PURE__ */ new Set(["aac", "mp4", "m4a", "ogg", "webm", "mp3", "wav"]);
|
|
76
|
+
function extractVoiceTranscript(file) {
|
|
77
|
+
const isAudio = file.subtype === "slack_audio" || AUDIO_TYPES.has(file.filetype ?? "");
|
|
78
|
+
if (!isAudio) return null;
|
|
79
|
+
const transcript = file.transcription?.preview?.content ?? (typeof file.preview === "string" ? file.preview : null);
|
|
80
|
+
return transcript ?? null;
|
|
81
|
+
}
|
|
82
|
+
function parseSlackEvent(event) {
|
|
83
|
+
const id = event.client_msg_id ?? event.ts ?? event.event_ts ?? `slack-${Date.now()}`;
|
|
84
|
+
const channel = event.channel ?? "unknown";
|
|
85
|
+
const from = event.user ?? "unknown";
|
|
86
|
+
const threadId = event.thread_ts !== void 0 ? event.thread_ts : void 0;
|
|
87
|
+
const receivedAt = event.ts ? new Date(parseFloat(event.ts) * 1e3).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
|
|
88
|
+
let body = event.text ?? "";
|
|
89
|
+
if (event.subtype === "file_share" && event.files?.length) {
|
|
90
|
+
for (const file of event.files) {
|
|
91
|
+
const transcript = extractVoiceTranscript(file);
|
|
92
|
+
if (transcript) {
|
|
93
|
+
body = body ? `${body}
|
|
94
|
+
|
|
95
|
+
[Voice message] ${transcript}` : `[Voice message] ${transcript}`;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (event.subtype === "file_share" && event.files?.length && !body) {
|
|
101
|
+
const hasAudio = event.files.some(
|
|
102
|
+
(f) => f.subtype === "slack_audio" || AUDIO_TYPES.has(f.filetype ?? "")
|
|
103
|
+
);
|
|
104
|
+
if (hasAudio) {
|
|
105
|
+
body = "[Voice message \u2014 no transcript available]";
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
id,
|
|
110
|
+
channel,
|
|
111
|
+
from,
|
|
112
|
+
body,
|
|
113
|
+
threadId,
|
|
114
|
+
receivedAt,
|
|
115
|
+
raw: event
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
async function enrichVoiceMessage(msg, botToken) {
|
|
119
|
+
if (!msg.body.includes("[Voice message \u2014 no transcript available]")) return msg;
|
|
120
|
+
const raw = msg.raw;
|
|
121
|
+
const files = raw?.files;
|
|
122
|
+
if (!files?.length) return msg;
|
|
123
|
+
const audioFile = files.find(
|
|
124
|
+
(f) => f.subtype === "slack_audio" || AUDIO_TYPES.has(f.filetype ?? "")
|
|
125
|
+
);
|
|
126
|
+
if (!audioFile?.url_private) return msg;
|
|
127
|
+
const buffer = await downloadAudio(audioFile.url_private, botToken);
|
|
128
|
+
if (!buffer) return msg;
|
|
129
|
+
const filename = audioFile.name ?? `voice.${audioFile.filetype ?? "aac"}`;
|
|
130
|
+
const transcript = await transcribeAudio(buffer, filename);
|
|
131
|
+
if (!transcript) return msg;
|
|
132
|
+
return {
|
|
133
|
+
...msg,
|
|
134
|
+
body: `[Voice message] ${transcript}`
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export {
|
|
139
|
+
transcribeAudio,
|
|
140
|
+
downloadAudio,
|
|
141
|
+
extractVoiceTranscript,
|
|
142
|
+
parseSlackEvent,
|
|
143
|
+
enrichVoiceMessage
|
|
144
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "botinabox",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Bot in a Box — framework for building multi-agent bots",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -63,6 +63,9 @@
|
|
|
63
63
|
"uuid": "^13.0.0",
|
|
64
64
|
"yaml": "^2.7.0"
|
|
65
65
|
},
|
|
66
|
+
"optionalDependencies": {
|
|
67
|
+
"whisper-node": "^1.1.1"
|
|
68
|
+
},
|
|
66
69
|
"peerDependencies": {
|
|
67
70
|
"@anthropic-ai/sdk": "^0.52.0",
|
|
68
71
|
"googleapis": ">=140.0.0",
|