beecork 1.4.11 → 1.6.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/dist/capabilities/index.d.ts +1 -1
- package/dist/capabilities/index.js +1 -1
- package/dist/capabilities/manager.js +13 -9
- package/dist/capabilities/packs.js +3 -1
- package/dist/channels/admin.d.ts +10 -0
- package/dist/channels/admin.js +20 -0
- package/dist/channels/command-handler.d.ts +2 -10
- package/dist/channels/command-handler.js +90 -84
- package/dist/channels/discord.d.ts +4 -9
- package/dist/channels/discord.js +59 -42
- package/dist/channels/index.d.ts +1 -1
- package/dist/channels/loader.js +13 -4
- package/dist/channels/pipeline.js +14 -5
- package/dist/channels/registry.d.ts +17 -1
- package/dist/channels/registry.js +33 -4
- package/dist/channels/send-helpers.d.ts +19 -0
- package/dist/channels/send-helpers.js +21 -0
- package/dist/channels/telegram.d.ts +21 -14
- package/dist/channels/telegram.js +214 -104
- package/dist/channels/types.d.ts +13 -38
- package/dist/channels/voice-state.d.ts +29 -0
- package/dist/channels/voice-state.js +45 -0
- package/dist/channels/webhook.d.ts +2 -5
- package/dist/channels/webhook.js +88 -29
- package/dist/channels/whatsapp.d.ts +9 -7
- package/dist/channels/whatsapp.js +141 -100
- package/dist/cli/capabilities.js +4 -4
- package/dist/cli/channel.js +16 -6
- package/dist/cli/commands.js +12 -9
- package/dist/cli/doctor.js +85 -27
- package/dist/cli/handoff.d.ts +7 -14
- package/dist/cli/handoff.js +9 -44
- package/dist/cli/mcp.js +5 -5
- package/dist/cli/media.js +21 -8
- package/dist/cli/setup.js +9 -8
- package/dist/cli/store.js +29 -12
- package/dist/config.d.ts +5 -1
- package/dist/config.js +20 -22
- package/dist/daemon.js +113 -51
- package/dist/dashboard/html.js +100 -20
- package/dist/dashboard/routes.d.ts +17 -0
- package/dist/dashboard/routes.js +623 -0
- package/dist/dashboard/server.js +38 -489
- package/dist/db/connection.d.ts +29 -0
- package/dist/db/connection.js +37 -0
- package/dist/db/index.js +43 -11
- package/dist/db/migrations.js +114 -22
- package/dist/delegation/manager.js +10 -4
- package/dist/index.js +39 -59
- package/dist/knowledge/manager.js +26 -12
- package/dist/mcp/handlers.d.ts +37 -0
- package/dist/mcp/handlers.js +520 -0
- package/dist/mcp/server.js +44 -858
- package/dist/mcp/tool-definitions.d.ts +1225 -0
- package/dist/mcp/tool-definitions.js +412 -0
- package/dist/mcp/validate.d.ts +23 -0
- package/dist/mcp/validate.js +65 -0
- package/dist/media/factory.js +18 -14
- package/dist/media/generators/dall-e.js +2 -2
- package/dist/media/generators/kling.js +4 -4
- package/dist/media/generators/lyria.js +1 -1
- package/dist/media/generators/nano-banana.d.ts +1 -1
- package/dist/media/generators/nano-banana.js +2 -2
- package/dist/media/generators/poll-util.js +4 -4
- package/dist/media/generators/recraft.js +3 -3
- package/dist/media/generators/runway.js +4 -4
- package/dist/media/generators/stable-diffusion.js +2 -2
- package/dist/media/generators/veo.js +1 -1
- package/dist/media/index.d.ts +2 -7
- package/dist/media/index.js +2 -2
- package/dist/media/store.d.ts +7 -0
- package/dist/media/store.js +18 -4
- package/dist/media/types.d.ts +22 -0
- package/dist/notifications/index.d.ts +2 -4
- package/dist/notifications/index.js +6 -19
- package/dist/notifications/ntfy.js +3 -3
- package/dist/observability/analytics.d.ts +1 -1
- package/dist/observability/analytics.js +41 -16
- package/dist/projects/index.d.ts +3 -2
- package/dist/projects/index.js +2 -2
- package/dist/projects/manager.d.ts +1 -7
- package/dist/projects/manager.js +66 -42
- package/dist/projects/router.d.ts +12 -0
- package/dist/projects/router.js +98 -45
- package/dist/service/install.js +15 -5
- package/dist/service/windows.js +1 -1
- package/dist/session/budget-guard.d.ts +20 -0
- package/dist/session/budget-guard.js +31 -0
- package/dist/session/circuit-breaker.d.ts +5 -3
- package/dist/session/circuit-breaker.js +45 -20
- package/dist/session/context-compactor.d.ts +32 -0
- package/dist/session/context-compactor.js +45 -0
- package/dist/session/context-monitor.js +2 -2
- package/dist/session/handoff.d.ts +21 -0
- package/dist/session/handoff.js +50 -0
- package/dist/session/manager.d.ts +21 -5
- package/dist/session/manager.js +166 -153
- package/dist/session/memory-store.d.ts +29 -0
- package/dist/session/memory-store.js +45 -0
- package/dist/session/message-queue.d.ts +28 -0
- package/dist/session/message-queue.js +52 -0
- package/dist/session/pending-dispatcher.d.ts +31 -0
- package/dist/session/pending-dispatcher.js +120 -0
- package/dist/session/pending-store.d.ts +60 -0
- package/dist/session/pending-store.js +118 -0
- package/dist/session/stale-session.d.ts +31 -0
- package/dist/session/stale-session.js +45 -0
- package/dist/session/subprocess.d.ts +3 -0
- package/dist/session/subprocess.js +54 -11
- package/dist/session/tab-store.d.ts +28 -0
- package/dist/session/tab-store.js +78 -0
- package/dist/tasks/scheduler.d.ts +13 -0
- package/dist/tasks/scheduler.js +97 -18
- package/dist/tasks/store.js +26 -12
- package/dist/timeline/logger.js +3 -1
- package/dist/timeline/query.js +15 -5
- package/dist/types.d.ts +49 -9
- package/dist/util/auto-heal.js +15 -5
- package/dist/util/install-info.js +3 -1
- package/dist/util/logger.d.ts +1 -1
- package/dist/util/logger.js +63 -24
- package/dist/util/paths.d.ts +2 -0
- package/dist/util/paths.js +16 -3
- package/dist/util/rate-limiter.js +8 -0
- package/dist/util/retry.js +1 -1
- package/dist/util/text.d.ts +21 -1
- package/dist/util/text.js +38 -8
- package/dist/voice/index.js +5 -1
- package/dist/voice/stt.js +14 -6
- package/dist/voice/tts.js +1 -1
- package/dist/watchers/scheduler.js +11 -5
- package/package.json +6 -1
- package/dist/session/tool-classifier.d.ts +0 -4
- package/dist/session/tool-classifier.js +0 -56
- package/dist/users/index.d.ts +0 -2
- package/dist/users/index.js +0 -1
- package/dist/users/service.d.ts +0 -17
- package/dist/users/service.js +0 -46
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { initVoiceProviders } from '../voice/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Per-channel STT/TTS state. Used to be duplicated across Telegram, WhatsApp,
|
|
4
|
+
* and Discord; consolidated here so init + warmup + transcription happen the
|
|
5
|
+
* same way across all 3.
|
|
6
|
+
*
|
|
7
|
+
* Discord historically only called warmup() and did NOT transcribe — preserve
|
|
8
|
+
* that behavior unless the caller explicitly asks for transcription.
|
|
9
|
+
*/
|
|
10
|
+
export class VoiceState {
|
|
11
|
+
channelId;
|
|
12
|
+
stt = null;
|
|
13
|
+
tts = null;
|
|
14
|
+
warmedUp = false;
|
|
15
|
+
constructor(channelId) {
|
|
16
|
+
this.channelId = channelId;
|
|
17
|
+
}
|
|
18
|
+
init(config) {
|
|
19
|
+
const { stt, tts } = initVoiceProviders(config.voice);
|
|
20
|
+
this.stt = stt;
|
|
21
|
+
this.tts = tts;
|
|
22
|
+
}
|
|
23
|
+
/** One-shot warmup (no media). Used by Discord today. */
|
|
24
|
+
async warmup() {
|
|
25
|
+
if (this.warmedUp || !this.stt)
|
|
26
|
+
return;
|
|
27
|
+
try {
|
|
28
|
+
this.stt.warmup?.();
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
/* warmup is best-effort */
|
|
32
|
+
}
|
|
33
|
+
this.warmedUp = true;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Transcribe voice attachments in-place (mutates the caption fields).
|
|
37
|
+
* Returns the updated warmed-up flag. No-op if STT isn't configured.
|
|
38
|
+
*/
|
|
39
|
+
async transcribe(media) {
|
|
40
|
+
if (!this.stt)
|
|
41
|
+
return;
|
|
42
|
+
const { transcribeVoiceMessages } = await import('../voice/index.js');
|
|
43
|
+
this.warmedUp = await transcribeVoiceMessages(media, this.stt, this.channelId, this.warmedUp);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -1,16 +1,13 @@
|
|
|
1
|
-
import type { Channel, ChannelContext,
|
|
1
|
+
import type { Channel, ChannelContext, SendOptions } from './types.js';
|
|
2
2
|
export declare class WebhookChannel implements Channel {
|
|
3
3
|
readonly id = "webhook";
|
|
4
4
|
readonly name = "Webhook";
|
|
5
|
-
readonly maxMessageLength
|
|
6
|
-
readonly supportsStreaming = false;
|
|
7
|
-
readonly supportsMedia = false;
|
|
5
|
+
readonly maxMessageLength: 100000;
|
|
8
6
|
private server;
|
|
9
7
|
private ctx;
|
|
10
8
|
constructor(ctx: ChannelContext);
|
|
11
9
|
start(): Promise<void>;
|
|
12
10
|
stop(): void;
|
|
13
|
-
onMessage(_handler: InboundMessageHandler): void;
|
|
14
11
|
sendMessage(_peerId: string, _text: string, _options?: SendOptions): Promise<void>;
|
|
15
12
|
sendNotification(_message: string, _urgent?: boolean): Promise<void>;
|
|
16
13
|
setTyping(_peerId: string, _active: boolean): Promise<void>;
|
package/dist/channels/webhook.js
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
2
|
import crypto from 'node:crypto';
|
|
3
3
|
import { logger } from '../util/logger.js';
|
|
4
|
-
import {
|
|
4
|
+
import { validateTabNameOrDefault } from '../config.js';
|
|
5
|
+
import { inboundLimiter } from '../util/rate-limiter.js';
|
|
6
|
+
import { MESSAGE_LIMITS } from '../util/text.js';
|
|
7
|
+
import { processInboundMessage } from './pipeline.js';
|
|
8
|
+
function safeEqualString(a, b) {
|
|
9
|
+
const ab = Buffer.from(a);
|
|
10
|
+
const bb = Buffer.from(b);
|
|
11
|
+
if (ab.length !== bb.length)
|
|
12
|
+
return false;
|
|
13
|
+
try {
|
|
14
|
+
return crypto.timingSafeEqual(ab, bb);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
5
20
|
export class WebhookChannel {
|
|
6
21
|
id = 'webhook';
|
|
7
22
|
name = 'Webhook';
|
|
8
|
-
maxMessageLength =
|
|
9
|
-
supportsStreaming = false;
|
|
10
|
-
supportsMedia = false;
|
|
23
|
+
maxMessageLength = MESSAGE_LIMITS.WEBHOOK_PROMPT;
|
|
11
24
|
server = null;
|
|
12
25
|
ctx;
|
|
13
26
|
constructor(ctx) {
|
|
@@ -17,6 +30,16 @@ export class WebhookChannel {
|
|
|
17
30
|
const config = this.getConfig();
|
|
18
31
|
if (!config?.enabled)
|
|
19
32
|
return;
|
|
33
|
+
// Fail-secure: a webhook running with no auth turns localhost-injected
|
|
34
|
+
// prompts (from any local process or any user on a shared host) into
|
|
35
|
+
// arbitrary claude --dangerously-skip-permissions runs. Require either
|
|
36
|
+
// an authToken or hmacSecret, OR an explicit allowUnauthLocalhost opt-in.
|
|
37
|
+
if (!config.authToken && !config.hmacSecret && !config.allowUnauthLocalhost) {
|
|
38
|
+
logger.error('Webhook channel refusing to start: no authToken or hmacSecret configured. ' +
|
|
39
|
+
'Set one in ~/.beecork/config.json under webhook.authToken/hmacSecret, or ' +
|
|
40
|
+
'explicitly opt in with webhook.allowUnauthLocalhost=true (NOT recommended on shared hosts).');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
20
43
|
const port = config.port || 8374;
|
|
21
44
|
this.server = http.createServer(async (req, res) => {
|
|
22
45
|
// CORS headers for API clients
|
|
@@ -40,22 +63,21 @@ export class WebhookChannel {
|
|
|
40
63
|
return;
|
|
41
64
|
}
|
|
42
65
|
const tabName = decodeURIComponent(match[1]);
|
|
43
|
-
// Validate tab name
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
66
|
+
// Validate tab name (allow "default" — it's a reference, not a creation)
|
|
67
|
+
const tabError = validateTabNameOrDefault(tabName);
|
|
68
|
+
if (tabError) {
|
|
69
|
+
res.writeHead(400);
|
|
70
|
+
res.end(JSON.stringify({ error: tabError }));
|
|
71
|
+
return;
|
|
51
72
|
}
|
|
52
73
|
// Read body first (needed for both JSON parsing and HMAC verification)
|
|
53
74
|
let body = '';
|
|
54
75
|
for await (const chunk of req) {
|
|
55
76
|
body += chunk;
|
|
56
|
-
if (body.length >
|
|
77
|
+
if (body.length > MESSAGE_LIMITS.HTTP_BODY) {
|
|
57
78
|
res.writeHead(413);
|
|
58
79
|
res.end(JSON.stringify({ error: 'Payload too large' }));
|
|
80
|
+
req.destroy();
|
|
59
81
|
return;
|
|
60
82
|
}
|
|
61
83
|
}
|
|
@@ -65,6 +87,12 @@ export class WebhookChannel {
|
|
|
65
87
|
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
66
88
|
return;
|
|
67
89
|
}
|
|
90
|
+
// Rate-limit AFTER auth so unauthenticated callers don't burn the budget
|
|
91
|
+
if (!inboundLimiter.check(this.id)) {
|
|
92
|
+
res.writeHead(429);
|
|
93
|
+
res.end(JSON.stringify({ error: 'Rate limit exceeded' }));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
68
96
|
let payload;
|
|
69
97
|
try {
|
|
70
98
|
payload = JSON.parse(body);
|
|
@@ -81,23 +109,57 @@ export class WebhookChannel {
|
|
|
81
109
|
return;
|
|
82
110
|
}
|
|
83
111
|
const isSync = payload.sync ?? false;
|
|
112
|
+
const remote = req.socket.remoteAddress ?? 'webhook';
|
|
84
113
|
try {
|
|
85
114
|
if (isSync) {
|
|
86
|
-
// Sync mode:
|
|
87
|
-
const result = await
|
|
88
|
-
|
|
115
|
+
// Sync mode: route through the shared pipeline so routing/enrichment apply.
|
|
116
|
+
const result = await processInboundMessage({
|
|
117
|
+
text: prompt,
|
|
118
|
+
media: [],
|
|
119
|
+
channelId: this.id,
|
|
120
|
+
tabManager: this.ctx.tabManager,
|
|
121
|
+
userId: remote,
|
|
122
|
+
sendProgress: () => {
|
|
123
|
+
/* webhook has no progress channel */
|
|
124
|
+
},
|
|
125
|
+
overrideTabName: tabName,
|
|
126
|
+
});
|
|
127
|
+
res.writeHead(result.isError ? 500 : 200);
|
|
89
128
|
res.end(JSON.stringify({
|
|
90
|
-
text: result.
|
|
91
|
-
tab: tabName,
|
|
92
|
-
|
|
93
|
-
durationMs: result.durationMs,
|
|
94
|
-
error: result.error,
|
|
129
|
+
text: result.responseText,
|
|
130
|
+
tab: result.tabName,
|
|
131
|
+
error: result.isError ? result.responseText : undefined,
|
|
95
132
|
}));
|
|
96
133
|
}
|
|
97
134
|
else {
|
|
98
|
-
// Async mode:
|
|
99
|
-
|
|
135
|
+
// Async mode: fire-and-forget through the pipeline.
|
|
136
|
+
// Surface failures to the user via broadcastNotify since the HTTP response
|
|
137
|
+
// is already 202 and the caller has no other way to learn.
|
|
138
|
+
processInboundMessage({
|
|
139
|
+
text: prompt,
|
|
140
|
+
media: [],
|
|
141
|
+
channelId: this.id,
|
|
142
|
+
tabManager: this.ctx.tabManager,
|
|
143
|
+
userId: remote,
|
|
144
|
+
sendProgress: () => {
|
|
145
|
+
/* webhook has no progress channel */
|
|
146
|
+
},
|
|
147
|
+
overrideTabName: tabName,
|
|
148
|
+
})
|
|
149
|
+
.then((result) => {
|
|
150
|
+
if (result.isError && this.ctx.notifyCallback) {
|
|
151
|
+
this.ctx
|
|
152
|
+
.notifyCallback(`Webhook async failed for "${tabName}": ${result.responseText}`)
|
|
153
|
+
.catch(() => { });
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
.catch((err) => {
|
|
100
157
|
logger.error(`Webhook async processing failed for tab ${tabName}:`, err);
|
|
158
|
+
if (this.ctx.notifyCallback) {
|
|
159
|
+
this.ctx
|
|
160
|
+
.notifyCallback(`Webhook async failed for "${tabName}": ${err instanceof Error ? err.message : String(err)}`)
|
|
161
|
+
.catch(() => { });
|
|
162
|
+
}
|
|
101
163
|
});
|
|
102
164
|
res.writeHead(202);
|
|
103
165
|
res.end(JSON.stringify({ accepted: true, tab: tabName }));
|
|
@@ -120,9 +182,6 @@ export class WebhookChannel {
|
|
|
120
182
|
}
|
|
121
183
|
logger.info('Webhook channel stopped');
|
|
122
184
|
}
|
|
123
|
-
onMessage(_handler) {
|
|
124
|
-
// Webhooks handle messages directly in the HTTP handler
|
|
125
|
-
}
|
|
126
185
|
async sendMessage(_peerId, _text, _options) {
|
|
127
186
|
// Webhooks are request-response — responses are sent in the HTTP handler
|
|
128
187
|
}
|
|
@@ -136,10 +195,10 @@ export class WebhookChannel {
|
|
|
136
195
|
// No auth configured = allow all (localhost only)
|
|
137
196
|
if (!config.authToken && !config.hmacSecret)
|
|
138
197
|
return true;
|
|
139
|
-
// Bearer token auth
|
|
198
|
+
// Bearer token auth (constant-time compare)
|
|
140
199
|
if (config.authToken) {
|
|
141
|
-
const authHeader = req.headers.authorization;
|
|
142
|
-
if (authHeader
|
|
200
|
+
const authHeader = req.headers.authorization || '';
|
|
201
|
+
if (safeEqualString(authHeader, `Bearer ${config.authToken}`))
|
|
143
202
|
return true;
|
|
144
203
|
}
|
|
145
204
|
// HMAC signature auth (for GitHub-style webhooks)
|
|
@@ -1,26 +1,28 @@
|
|
|
1
|
-
import type { Channel, ChannelContext,
|
|
1
|
+
import type { Channel, ChannelContext, SendOptions } from './types.js';
|
|
2
2
|
export declare class WhatsAppChannel implements Channel {
|
|
3
3
|
readonly id = "whatsapp";
|
|
4
4
|
readonly name = "WhatsApp";
|
|
5
5
|
readonly maxMessageLength = 8192;
|
|
6
|
-
readonly supportsStreaming = false;
|
|
7
|
-
readonly supportsMedia = true;
|
|
8
6
|
private sock;
|
|
9
7
|
private ctx;
|
|
10
8
|
private allowedNumbers;
|
|
11
9
|
private reconnectAttempts;
|
|
12
10
|
private readonly maxReconnectAttempts;
|
|
13
11
|
private readonly backoffDelays;
|
|
14
|
-
private
|
|
15
|
-
private ttsProvider;
|
|
16
|
-
private sttWarmedUp;
|
|
12
|
+
private voice;
|
|
17
13
|
constructor(ctx: ChannelContext);
|
|
14
|
+
/**
|
|
15
|
+
* Schedule the next reconnect with exponential backoff. Unlike the previous
|
|
16
|
+
* inline setTimeout, this path retries when `start()` itself rejects (auth
|
|
17
|
+
* failure, baileys init throw, etc.) instead of going permanently silent
|
|
18
|
+
* after a single failed attempt.
|
|
19
|
+
*/
|
|
20
|
+
private scheduleReconnect;
|
|
18
21
|
start(): Promise<void>;
|
|
19
22
|
stop(): void;
|
|
20
23
|
sendMessage(peerId: string, text: string, _options?: SendOptions): Promise<void>;
|
|
21
24
|
sendNotification(message: string, _urgent?: boolean): Promise<void>;
|
|
22
25
|
setTyping(peerId: string, active: boolean): Promise<void>;
|
|
23
|
-
onMessage(_handler: InboundMessageHandler): void;
|
|
24
26
|
private sendResponse;
|
|
25
27
|
private isAllowed;
|
|
26
28
|
}
|
|
@@ -1,39 +1,76 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WhatsApp integration via @whiskeysockets/baileys.
|
|
3
|
+
*
|
|
4
|
+
* baileys is a peer-optional dependency loaded by dynamic import, and its
|
|
5
|
+
* runtime types are intentionally loose (the lib uses heavy union/index
|
|
6
|
+
* types that don't survive serialization across the dynamic-import boundary).
|
|
7
|
+
* Trying to type every variant of Baileys' message/connection shapes would
|
|
8
|
+
* either pull baileys into the static graph (bloating non-WhatsApp installs)
|
|
9
|
+
* or require maintaining a parallel shim. We accept `any` at this trust
|
|
10
|
+
* boundary; runtime validation lives in the descriptors table inside
|
|
11
|
+
* messages.upsert.
|
|
12
|
+
*/
|
|
13
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
1
14
|
import fs from 'node:fs';
|
|
2
15
|
import { logger } from '../util/logger.js';
|
|
3
16
|
import { saveMedia, isOversized } from '../media/store.js';
|
|
4
|
-
import {
|
|
5
|
-
import { chunkText } from '../util/text.js';
|
|
17
|
+
import { sendChunkedResponse } from './send-helpers.js';
|
|
6
18
|
import { inboundLimiter } from '../util/rate-limiter.js';
|
|
7
19
|
import { processInboundMessage } from './pipeline.js';
|
|
8
|
-
import {
|
|
20
|
+
import { isChannelAdmin } from './admin.js';
|
|
21
|
+
import { VoiceState } from './voice-state.js';
|
|
9
22
|
const WHATSAPP_MAX_LENGTH = 8192;
|
|
10
23
|
export class WhatsAppChannel {
|
|
11
24
|
id = 'whatsapp';
|
|
12
25
|
name = 'WhatsApp';
|
|
13
26
|
maxMessageLength = WHATSAPP_MAX_LENGTH;
|
|
14
|
-
supportsStreaming = false;
|
|
15
|
-
supportsMedia = true;
|
|
16
27
|
sock = null;
|
|
17
28
|
ctx;
|
|
18
29
|
allowedNumbers;
|
|
19
30
|
reconnectAttempts = 0;
|
|
20
31
|
maxReconnectAttempts = 10;
|
|
21
32
|
backoffDelays = [1000, 5000, 15000, 30000, 60000];
|
|
22
|
-
|
|
23
|
-
ttsProvider = null;
|
|
24
|
-
sttWarmedUp = false;
|
|
33
|
+
voice = new VoiceState('whatsapp');
|
|
25
34
|
constructor(ctx) {
|
|
26
35
|
this.ctx = ctx;
|
|
27
36
|
this.allowedNumbers = new Set(ctx.config.whatsapp?.allowedNumbers ?? []);
|
|
28
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Schedule the next reconnect with exponential backoff. Unlike the previous
|
|
40
|
+
* inline setTimeout, this path retries when `start()` itself rejects (auth
|
|
41
|
+
* failure, baileys init throw, etc.) instead of going permanently silent
|
|
42
|
+
* after a single failed attempt.
|
|
43
|
+
*/
|
|
44
|
+
scheduleReconnect() {
|
|
45
|
+
this.reconnectAttempts++;
|
|
46
|
+
if (this.reconnectAttempts > this.maxReconnectAttempts) {
|
|
47
|
+
logger.error(`WhatsApp reconnect failed after ${this.maxReconnectAttempts} attempts, giving up`);
|
|
48
|
+
this.ctx
|
|
49
|
+
.notifyCallback?.('⚠️ WhatsApp disconnected after 10 reconnection attempts. Restart daemon to reconnect.')
|
|
50
|
+
.catch((err) => logger.error('Failed to send WhatsApp disconnect notification:', err));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const delayIdx = Math.min(this.reconnectAttempts - 1, this.backoffDelays.length - 1);
|
|
54
|
+
const delay = this.backoffDelays[delayIdx];
|
|
55
|
+
logger.warn(`WhatsApp connection closed, reconnecting in ${delay / 1000}s (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
|
|
56
|
+
setTimeout(() => {
|
|
57
|
+
this.start().catch((err) => {
|
|
58
|
+
logger.error('WhatsApp reconnect attempt failed:', err);
|
|
59
|
+
// Recurse via scheduleReconnect so backoff keeps escalating instead of
|
|
60
|
+
// silently dropping the reconnect chain after one failed start().
|
|
61
|
+
this.scheduleReconnect();
|
|
62
|
+
});
|
|
63
|
+
}, delay);
|
|
64
|
+
}
|
|
29
65
|
async start() {
|
|
30
|
-
// Initialize voice providers
|
|
31
|
-
|
|
32
|
-
this.sttProvider = stt;
|
|
33
|
-
this.ttsProvider = tts;
|
|
66
|
+
// Initialize voice providers (STT + TTS)
|
|
67
|
+
this.voice.init(this.ctx.config);
|
|
34
68
|
try {
|
|
35
|
-
const { default: makeWASocket, useMultiFileAuthState, DisconnectReason, downloadMediaMessage, fetchLatestBaileysVersion } = await import('@whiskeysockets/baileys');
|
|
36
|
-
const
|
|
69
|
+
const { default: makeWASocket, useMultiFileAuthState, DisconnectReason, downloadMediaMessage, fetchLatestBaileysVersion, } = await import('@whiskeysockets/baileys');
|
|
70
|
+
const { getWhatsappSessionPath } = await import('../util/paths.js');
|
|
71
|
+
// Use the centralized path helper — the previous fallback hard-coded
|
|
72
|
+
// process.env.HOME which bypassed BEECORK_HOME for tests/isolation.
|
|
73
|
+
const sessionPath = this.ctx.config.whatsapp?.sessionPath ?? getWhatsappSessionPath();
|
|
37
74
|
fs.mkdirSync(sessionPath, { recursive: true, mode: 0o700 });
|
|
38
75
|
const { state, saveCreds } = await useMultiFileAuthState(sessionPath);
|
|
39
76
|
const { version } = await fetchLatestBaileysVersion().catch(() => ({ version: undefined }));
|
|
@@ -60,21 +97,7 @@ export class WhatsAppChannel {
|
|
|
60
97
|
if (connection === 'close') {
|
|
61
98
|
const reason = lastDisconnect?.error?.output?.statusCode;
|
|
62
99
|
if (reason !== DisconnectReason.loggedOut) {
|
|
63
|
-
this.
|
|
64
|
-
if (this.reconnectAttempts > this.maxReconnectAttempts) {
|
|
65
|
-
logger.error(`WhatsApp reconnect failed after ${this.maxReconnectAttempts} attempts, giving up`);
|
|
66
|
-
this.ctx.notifyCallback?.('⚠️ WhatsApp disconnected after 10 reconnection attempts. Restart daemon to reconnect.')
|
|
67
|
-
.catch(err => logger.error('Failed to send WhatsApp disconnect notification:', err));
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
const delayIdx = Math.min(this.reconnectAttempts - 1, this.backoffDelays.length - 1);
|
|
71
|
-
const delay = this.backoffDelays[delayIdx];
|
|
72
|
-
logger.warn(`WhatsApp connection closed, reconnecting in ${delay / 1000}s (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
|
|
73
|
-
setTimeout(() => {
|
|
74
|
-
this.start().catch(err => {
|
|
75
|
-
logger.error('WhatsApp reconnect failed:', err);
|
|
76
|
-
});
|
|
77
|
-
}, delay);
|
|
100
|
+
this.scheduleReconnect();
|
|
78
101
|
}
|
|
79
102
|
else {
|
|
80
103
|
logger.error('WhatsApp logged out. Please re-scan QR code.');
|
|
@@ -94,75 +117,84 @@ export class WhatsAppChannel {
|
|
|
94
117
|
return;
|
|
95
118
|
// Rate limit check
|
|
96
119
|
if (!inboundLimiter.check(this.id)) {
|
|
97
|
-
await sock
|
|
120
|
+
await sock
|
|
121
|
+
.sendMessage(sender, {
|
|
122
|
+
text: "I'm receiving too many messages right now. Please wait a moment.",
|
|
123
|
+
})
|
|
124
|
+
.catch(() => { });
|
|
98
125
|
return;
|
|
99
126
|
}
|
|
100
127
|
const text = msg.message.conversation ||
|
|
101
128
|
msg.message.extendedTextMessage?.text ||
|
|
102
129
|
msg.message.imageMessage?.caption ||
|
|
103
|
-
msg.message.videoMessage?.caption ||
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
waDownloadTasks.push(downloadMediaMessage(msg, 'buffer', {})
|
|
119
|
-
.then((buffer) => {
|
|
120
|
-
if (buffer && !isOversized(buffer.length)) {
|
|
121
|
-
const ext = msg.message.audioMessage.ptt ? 'ogg' : 'mp3';
|
|
122
|
-
const filePath = saveMedia(buffer, ext);
|
|
130
|
+
msg.message.videoMessage?.caption ||
|
|
131
|
+
'';
|
|
132
|
+
const descriptors = [
|
|
133
|
+
{
|
|
134
|
+
key: 'imageMessage',
|
|
135
|
+
build: (m, buf) => ({
|
|
136
|
+
type: 'image',
|
|
137
|
+
mimeType: m.imageMessage.mimetype || 'image/jpeg',
|
|
138
|
+
filePath: saveMedia(buf, 'jpg'),
|
|
139
|
+
}),
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
key: 'audioMessage',
|
|
143
|
+
build: (m, buf) => {
|
|
144
|
+
const ext = m.audioMessage.ptt ? 'ogg' : 'mp3';
|
|
123
145
|
return {
|
|
124
|
-
type:
|
|
125
|
-
mimeType:
|
|
126
|
-
filePath,
|
|
127
|
-
duration:
|
|
146
|
+
type: m.audioMessage.ptt ? 'voice' : 'audio',
|
|
147
|
+
mimeType: m.audioMessage.mimetype || 'audio/ogg',
|
|
148
|
+
filePath: saveMedia(buf, ext),
|
|
149
|
+
duration: m.audioMessage.seconds ?? undefined,
|
|
128
150
|
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
key: 'documentMessage',
|
|
155
|
+
build: (m, buf) => {
|
|
156
|
+
const ext = m.documentMessage.fileName?.split('.').pop() || 'bin';
|
|
157
|
+
return {
|
|
158
|
+
type: 'document',
|
|
159
|
+
mimeType: m.documentMessage.mimetype || 'application/octet-stream',
|
|
160
|
+
filePath: saveMedia(buf, ext, m.documentMessage.fileName ?? undefined),
|
|
161
|
+
fileName: m.documentMessage.fileName ?? undefined,
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
key: 'videoMessage',
|
|
167
|
+
build: (m, buf) => ({
|
|
168
|
+
type: 'video',
|
|
169
|
+
mimeType: m.videoMessage.mimetype || 'video/mp4',
|
|
170
|
+
filePath: saveMedia(buf, 'mp4'),
|
|
171
|
+
duration: m.videoMessage.seconds ?? undefined,
|
|
172
|
+
}),
|
|
173
|
+
},
|
|
174
|
+
];
|
|
175
|
+
const waDownloadTasks = [];
|
|
176
|
+
for (const d of descriptors) {
|
|
177
|
+
if (!msg.message[d.key])
|
|
178
|
+
continue;
|
|
135
179
|
waDownloadTasks.push(downloadMediaMessage(msg, 'buffer', {})
|
|
136
180
|
.then((buffer) => {
|
|
137
|
-
if (buffer
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return
|
|
181
|
+
if (!buffer || isOversized(buffer.length))
|
|
182
|
+
return null;
|
|
183
|
+
try {
|
|
184
|
+
return d.build(msg.message, buffer);
|
|
141
185
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
.catch(() => null));
|
|
145
|
-
}
|
|
146
|
-
if (msg.message.videoMessage) {
|
|
147
|
-
waDownloadTasks.push(downloadMediaMessage(msg, 'buffer', {})
|
|
148
|
-
.then((buffer) => {
|
|
149
|
-
if (buffer && !isOversized(buffer.length)) {
|
|
150
|
-
const filePath = saveMedia(buffer, 'mp4');
|
|
151
|
-
return { type: 'video', mimeType: msg.message.videoMessage.mimetype || 'video/mp4', filePath, duration: msg.message.videoMessage.seconds ?? undefined };
|
|
186
|
+
catch {
|
|
187
|
+
return null;
|
|
152
188
|
}
|
|
153
|
-
return null;
|
|
154
189
|
})
|
|
155
190
|
.catch(() => null));
|
|
156
191
|
}
|
|
157
192
|
const waResults = await Promise.allSettled(waDownloadTasks);
|
|
158
193
|
const media = waResults
|
|
159
194
|
.filter((r) => r.status === 'fulfilled' && r.value !== null)
|
|
160
|
-
.map(r => r.value);
|
|
195
|
+
.map((r) => r.value);
|
|
161
196
|
// Transcribe voice messages if STT is configured
|
|
162
|
-
|
|
163
|
-
const { transcribeVoiceMessages } = await import('../voice/index.js');
|
|
164
|
-
this.sttWarmedUp = await transcribeVoiceMessages(media, this.sttProvider, 'whatsapp', this.sttWarmedUp);
|
|
165
|
-
}
|
|
197
|
+
await this.voice.transcribe(media);
|
|
166
198
|
if (!text && media.length === 0)
|
|
167
199
|
return;
|
|
168
200
|
try {
|
|
@@ -173,7 +205,7 @@ export class WhatsAppChannel {
|
|
|
173
205
|
const cmdResult = await handleSharedCommand({
|
|
174
206
|
userId: waUserId,
|
|
175
207
|
text,
|
|
176
|
-
isAdmin: this.allowedNumbers
|
|
208
|
+
isAdmin: isChannelAdmin(this.allowedNumbers, waUserId, this.ctx.config.whatsapp?.adminNumber),
|
|
177
209
|
channelId: 'whatsapp',
|
|
178
210
|
}, this.ctx.tabManager);
|
|
179
211
|
if (cmdResult.handled) {
|
|
@@ -190,7 +222,7 @@ export class WhatsAppChannel {
|
|
|
190
222
|
channelId: 'whatsapp',
|
|
191
223
|
tabManager: this.ctx.tabManager,
|
|
192
224
|
voiceReplyMode: this.ctx.config.voice?.replyMode,
|
|
193
|
-
ttsProvider: this.
|
|
225
|
+
ttsProvider: this.voice.tts,
|
|
194
226
|
userId: waUserId,
|
|
195
227
|
sendProgress: (msg) => {
|
|
196
228
|
sock.sendMessage(sender, { text: msg }).catch(() => { });
|
|
@@ -202,7 +234,11 @@ export class WhatsAppChannel {
|
|
|
202
234
|
return;
|
|
203
235
|
// Send voice reply if TTS generated audio
|
|
204
236
|
if (pipelineResult.audioPath) {
|
|
205
|
-
await sock.sendMessage(sender, {
|
|
237
|
+
await sock.sendMessage(sender, {
|
|
238
|
+
audio: { url: pipelineResult.audioPath },
|
|
239
|
+
mimetype: 'audio/ogg; codecs=opus',
|
|
240
|
+
ptt: true,
|
|
241
|
+
});
|
|
206
242
|
if (pipelineResult.voiceOnly)
|
|
207
243
|
return;
|
|
208
244
|
}
|
|
@@ -210,7 +246,11 @@ export class WhatsAppChannel {
|
|
|
210
246
|
}
|
|
211
247
|
catch (err) {
|
|
212
248
|
logger.error('WhatsApp message handler error:', err);
|
|
213
|
-
await sock
|
|
249
|
+
await sock
|
|
250
|
+
.sendMessage(sender, {
|
|
251
|
+
text: 'Something went wrong processing your message. Check daemon logs for details.',
|
|
252
|
+
})
|
|
253
|
+
.catch((sendErr) => logger.error('WhatsApp: failed to send fallback error message:', sendErr));
|
|
214
254
|
}
|
|
215
255
|
});
|
|
216
256
|
}
|
|
@@ -231,10 +271,12 @@ export class WhatsAppChannel {
|
|
|
231
271
|
const sock = this.sock;
|
|
232
272
|
if (!sock)
|
|
233
273
|
return;
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
274
|
+
await sendChunkedResponse({
|
|
275
|
+
text,
|
|
276
|
+
maxLength: WHATSAPP_MAX_LENGTH,
|
|
277
|
+
retryLabel: 'whatsapp-send',
|
|
278
|
+
sendChunk: (chunk) => sock.sendMessage(peerId, { text: chunk }),
|
|
279
|
+
});
|
|
238
280
|
}
|
|
239
281
|
async sendNotification(message, _urgent) {
|
|
240
282
|
const sock = this.sock;
|
|
@@ -256,21 +298,20 @@ export class WhatsAppChannel {
|
|
|
256
298
|
const status = active ? 'composing' : 'paused';
|
|
257
299
|
await sock.sendPresenceUpdate(status, peerId).catch(() => { });
|
|
258
300
|
}
|
|
259
|
-
onMessage(_handler) {
|
|
260
|
-
// Messages are handled directly in start()
|
|
261
|
-
}
|
|
262
301
|
// ─── Private ───
|
|
263
302
|
async sendResponse(jid, text, tabName) {
|
|
264
|
-
const prefix = tabName && tabName !== 'default' ? `[${tabName}] ` : '';
|
|
265
|
-
const chunks = chunkText(prefix + text, WHATSAPP_MAX_LENGTH);
|
|
266
303
|
const sock = this.sock;
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
304
|
+
try {
|
|
305
|
+
await sendChunkedResponse({
|
|
306
|
+
text,
|
|
307
|
+
tabName,
|
|
308
|
+
maxLength: WHATSAPP_MAX_LENGTH,
|
|
309
|
+
retryLabel: 'whatsapp-send',
|
|
310
|
+
sendChunk: (chunk) => sock.sendMessage(jid, { text: chunk }),
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
logger.error(`WhatsApp delivery failed for ${jid}:`, err);
|
|
274
315
|
}
|
|
275
316
|
}
|
|
276
317
|
isAllowed(jid) {
|
package/dist/cli/capabilities.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import readline from 'node:readline';
|
|
2
2
|
function ask(rl, question, defaultValue) {
|
|
3
3
|
const prompt = defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `;
|
|
4
|
-
return new Promise(r => rl.question(prompt, a => r(a.trim() || defaultValue || '')));
|
|
4
|
+
return new Promise((r) => rl.question(prompt, (a) => r(a.trim() || defaultValue || '')));
|
|
5
5
|
}
|
|
6
6
|
export async function enableCapability(packId) {
|
|
7
7
|
const { getAvailablePacks, isEnabled, enablePack } = await import('../capabilities/index.js');
|
|
8
8
|
const packs = getAvailablePacks();
|
|
9
|
-
const pack = packs.find(p => p.id === packId);
|
|
9
|
+
const pack = packs.find((p) => p.id === packId);
|
|
10
10
|
if (!pack) {
|
|
11
11
|
console.log(`Unknown capability: "${packId}"`);
|
|
12
|
-
console.log('Available: ' + packs.map(p => p.id).join(', '));
|
|
12
|
+
console.log('Available: ' + packs.map((p) => p.id).join(', '));
|
|
13
13
|
return;
|
|
14
14
|
}
|
|
15
15
|
if (isEnabled(packId)) {
|
|
@@ -46,7 +46,7 @@ export async function listCapabilities() {
|
|
|
46
46
|
web: 'Web',
|
|
47
47
|
};
|
|
48
48
|
for (const category of categories) {
|
|
49
|
-
const categoryPacks = packs.filter(p => p.category === category);
|
|
49
|
+
const categoryPacks = packs.filter((p) => p.category === category);
|
|
50
50
|
if (categoryPacks.length === 0)
|
|
51
51
|
continue;
|
|
52
52
|
console.log(` ${categoryNames[category]}:`);
|