beecork 1.5.0 → 1.7.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/command-handler.js +46 -14
- package/dist/channels/discord.d.ts +3 -6
- package/dist/channels/discord.js +40 -23
- package/dist/channels/index.d.ts +1 -1
- package/dist/channels/loader.js +13 -3
- 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/telegram.d.ts +20 -5
- package/dist/channels/telegram.js +177 -42
- package/dist/channels/types.d.ts +11 -28
- package/dist/channels/voice-state.js +3 -1
- package/dist/channels/webhook.d.ts +1 -4
- package/dist/channels/webhook.js +26 -11
- package/dist/channels/whatsapp.d.ts +8 -4
- package/dist/channels/whatsapp.js +65 -29
- 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 +80 -25
- 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.js +5 -10
- package/dist/daemon.js +88 -38
- package/dist/dashboard/html.js +80 -12
- package/dist/dashboard/routes.js +143 -79
- package/dist/dashboard/server.js +5 -1
- package/dist/db/connection.d.ts +29 -0
- package/dist/db/connection.js +37 -0
- package/dist/db/index.js +30 -12
- package/dist/db/migrations.js +84 -28
- package/dist/delegation/manager.js +10 -4
- package/dist/index.js +39 -59
- package/dist/knowledge/manager.js +26 -12
- package/dist/mcp/handlers.js +126 -57
- package/dist/mcp/server.js +20 -10
- package/dist/mcp/tool-definitions.js +68 -20
- 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.js +1 -1
- 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.js +35 -13
- package/dist/projects/index.d.ts +1 -1
- package/dist/projects/index.js +1 -1
- package/dist/projects/manager.d.ts +0 -4
- package/dist/projects/manager.js +51 -28
- package/dist/projects/router.d.ts +2 -0
- package/dist/projects/router.js +70 -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 +17 -5
- package/dist/session/manager.js +153 -146
- 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 +2 -0
- package/dist/session/subprocess.js +33 -11
- package/dist/session/tab-store.js +4 -3
- package/dist/tasks/scheduler.d.ts +7 -0
- package/dist/tasks/scheduler.js +46 -6
- package/dist/tasks/store.js +20 -6
- package/dist/timeline/logger.js +3 -1
- package/dist/timeline/query.js +9 -3
- package/dist/types.d.ts +34 -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 +1 -0
- package/dist/util/paths.js +12 -2
- package/dist/util/retry.js +1 -1
- package/dist/util/text.js +13 -7
- 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 +9 -2
- package/package.json +18 -13
- package/dist/session/tool-classifier.d.ts +0 -4
- package/dist/session/tool-classifier.js +0 -56
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export type { CapabilityPack, EnabledCapability } from './types.js';
|
|
2
2
|
export { CAPABILITY_PACKS } from './packs.js';
|
|
3
|
-
export { getAvailablePacks, getEnabledCapabilities, isEnabled, enablePack, disablePack, updateMcpConfig } from './manager.js';
|
|
3
|
+
export { getAvailablePacks, getEnabledCapabilities, isEnabled, enablePack, disablePack, updateMcpConfig, } from './manager.js';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { CAPABILITY_PACKS } from './packs.js';
|
|
2
|
-
export { getAvailablePacks, getEnabledCapabilities, isEnabled, enablePack, disablePack, updateMcpConfig } from './manager.js';
|
|
2
|
+
export { getAvailablePacks, getEnabledCapabilities, isEnabled, enablePack, disablePack, updateMcpConfig, } from './manager.js';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
|
-
import {
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
3
|
import { getConfig, saveConfig } from '../config.js';
|
|
4
|
+
const SAFE_NPM_PACKAGE = /^[@a-zA-Z0-9_/.-]+$/;
|
|
4
5
|
import { getMcpConfigPath } from '../util/paths.js';
|
|
5
6
|
import { logger } from '../util/logger.js';
|
|
6
7
|
import { CAPABILITY_PACKS } from './packs.js';
|
|
@@ -15,28 +16,31 @@ export function getEnabledCapabilities() {
|
|
|
15
16
|
}
|
|
16
17
|
/** Check if a pack is enabled */
|
|
17
18
|
export function isEnabled(packId) {
|
|
18
|
-
return getEnabledCapabilities().some(c => c.packId === packId);
|
|
19
|
+
return getEnabledCapabilities().some((c) => c.packId === packId);
|
|
19
20
|
}
|
|
20
21
|
/** Enable a capability pack */
|
|
21
22
|
export function enablePack(packId, apiKey) {
|
|
22
|
-
const pack = CAPABILITY_PACKS.find(p => p.id === packId);
|
|
23
|
+
const pack = CAPABILITY_PACKS.find((p) => p.id === packId);
|
|
23
24
|
if (!pack)
|
|
24
25
|
throw new Error(`Unknown capability: ${packId}. Use 'beecork capabilities' to list.`);
|
|
25
26
|
if (pack.requiresApiKey && !apiKey) {
|
|
26
27
|
throw new Error(`${pack.name} requires an API key. Hint: ${pack.apiKeyHint}`);
|
|
27
28
|
}
|
|
28
29
|
// Install the MCP server package
|
|
30
|
+
if (!SAFE_NPM_PACKAGE.test(pack.mcpServer.package)) {
|
|
31
|
+
throw new Error(`Invalid package name in capability pack: ${pack.mcpServer.package}`);
|
|
32
|
+
}
|
|
29
33
|
try {
|
|
30
34
|
console.log(`Installing ${pack.mcpServer.package}...`);
|
|
31
|
-
|
|
35
|
+
execFileSync('npm', ['install', '-g', pack.mcpServer.package], { stdio: 'pipe' });
|
|
32
36
|
}
|
|
33
|
-
catch
|
|
37
|
+
catch {
|
|
34
38
|
logger.warn(`Package install skipped (may already be available via npx): ${pack.mcpServer.package}`);
|
|
35
39
|
}
|
|
36
40
|
// Update config
|
|
37
41
|
const config = getConfig();
|
|
38
42
|
const capabilities = config.capabilities || [];
|
|
39
|
-
const existing = capabilities.findIndex(c => c.packId === packId);
|
|
43
|
+
const existing = capabilities.findIndex((c) => c.packId === packId);
|
|
40
44
|
const entry = { packId, apiKey, enabledAt: new Date().toISOString() };
|
|
41
45
|
if (existing >= 0) {
|
|
42
46
|
capabilities[existing] = entry;
|
|
@@ -54,7 +58,7 @@ export function enablePack(packId, apiKey) {
|
|
|
54
58
|
export function disablePack(packId) {
|
|
55
59
|
const config = getConfig();
|
|
56
60
|
const capabilities = config.capabilities || [];
|
|
57
|
-
config.capabilities = capabilities.filter(c => c.packId !== packId);
|
|
61
|
+
config.capabilities = capabilities.filter((c) => c.packId !== packId);
|
|
58
62
|
saveConfig(config);
|
|
59
63
|
updateMcpConfig();
|
|
60
64
|
logger.info(`Capability disabled: ${packId}`);
|
|
@@ -78,7 +82,7 @@ export function updateMcpConfig() {
|
|
|
78
82
|
// Add enabled capability servers
|
|
79
83
|
const capabilities = getEnabledCapabilities();
|
|
80
84
|
for (const cap of capabilities) {
|
|
81
|
-
const pack = CAPABILITY_PACKS.find(p => p.id === cap.packId);
|
|
85
|
+
const pack = CAPABILITY_PACKS.find((p) => p.id === cap.packId);
|
|
82
86
|
if (!pack)
|
|
83
87
|
continue;
|
|
84
88
|
// Resolve env vars
|
|
@@ -96,7 +100,7 @@ export function updateMcpConfig() {
|
|
|
96
100
|
env[pack.apiKeyEnvVar] = cap.apiKey;
|
|
97
101
|
}
|
|
98
102
|
// Resolve args templates
|
|
99
|
-
const args = (pack.mcpServer.args || []).map(arg => arg.replace(/\$\{(\w+)\}/g, (_, varName) => {
|
|
103
|
+
const args = (pack.mcpServer.args || []).map((arg) => arg.replace(/\$\{(\w+)\}/g, (_, varName) => {
|
|
100
104
|
if (varName === pack.apiKeyEnvVar)
|
|
101
105
|
return cap.apiKey || '';
|
|
102
106
|
return process.env[varName] || '';
|
|
@@ -9,7 +9,9 @@ export const CAPABILITY_PACKS = [
|
|
|
9
9
|
package: '@notionhq/notion-mcp-server',
|
|
10
10
|
command: 'npx',
|
|
11
11
|
args: ['-y', '@notionhq/notion-mcp-server'],
|
|
12
|
-
env: {
|
|
12
|
+
env: {
|
|
13
|
+
OPENAPI_MCP_HEADERS: '{"Authorization":"Bearer ${NOTION_API_KEY}","Notion-Version":"2022-06-28"}',
|
|
14
|
+
},
|
|
13
15
|
},
|
|
14
16
|
requiresApiKey: true,
|
|
15
17
|
apiKeyHint: 'Notion integration token (from notion.so/my-integrations)',
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { timeAgo } from '../util/text.js';
|
|
6
6
|
import { validateTabName } from '../config.js';
|
|
7
|
+
import { exportTab, formatHandoffInfo } from '../session/handoff.js';
|
|
8
|
+
import { logActivity } from '../timeline/index.js';
|
|
7
9
|
/**
|
|
8
10
|
* Handle shared commands that work identically across all channels.
|
|
9
11
|
* Returns { handled: true, response } if a command was matched.
|
|
@@ -16,7 +18,9 @@ export async function handleSharedCommand(ctx, tabManager) {
|
|
|
16
18
|
const tabs = tabManager.listTabs();
|
|
17
19
|
if (tabs.length === 0)
|
|
18
20
|
return { handled: true, response: 'No tabs.' };
|
|
19
|
-
const list = tabs
|
|
21
|
+
const list = tabs
|
|
22
|
+
.map((t) => `• ${t.name} [${t.status}] — ${timeAgo(t.lastActivityAt)}`)
|
|
23
|
+
.join('\n');
|
|
20
24
|
return { handled: true, response: list };
|
|
21
25
|
}
|
|
22
26
|
// /stop <name>
|
|
@@ -71,10 +75,12 @@ export async function handleSharedCommand(ctx, tabManager) {
|
|
|
71
75
|
const watchers = db.prepare('SELECT * FROM watchers ORDER BY created_at').all();
|
|
72
76
|
if (watchers.length === 0)
|
|
73
77
|
return { handled: true, response: 'No watchers configured.' };
|
|
74
|
-
const watchList = watchers
|
|
78
|
+
const watchList = watchers
|
|
79
|
+
.map((w) => {
|
|
75
80
|
const status = w.enabled ? 'active' : 'disabled';
|
|
76
81
|
return `[${status}] ${w.name} -- ${w.schedule} (triggers: ${w.trigger_count})`;
|
|
77
|
-
})
|
|
82
|
+
})
|
|
83
|
+
.join('\n');
|
|
78
84
|
return { handled: true, response: `Watchers:\n${watchList}` };
|
|
79
85
|
}
|
|
80
86
|
// /tasks
|
|
@@ -84,10 +90,12 @@ export async function handleSharedCommand(ctx, tabManager) {
|
|
|
84
90
|
const tasks = db.prepare('SELECT * FROM tasks ORDER BY created_at').all();
|
|
85
91
|
if (tasks.length === 0)
|
|
86
92
|
return { handled: true, response: 'No tasks scheduled.' };
|
|
87
|
-
const taskList = tasks
|
|
93
|
+
const taskList = tasks
|
|
94
|
+
.map((t) => {
|
|
88
95
|
const status = t.enabled ? 'enabled' : 'disabled';
|
|
89
96
|
return `[${status}] ${t.name} (${t.schedule_type}: ${t.schedule}) -> tab:${t.tab_name}`;
|
|
90
|
-
})
|
|
97
|
+
})
|
|
98
|
+
.join('\n');
|
|
91
99
|
return { handled: true, response: `Tasks:\n${taskList}` };
|
|
92
100
|
}
|
|
93
101
|
// /cost
|
|
@@ -104,14 +112,17 @@ export async function handleSharedCommand(ctx, tabManager) {
|
|
|
104
112
|
// /handoff [tab]
|
|
105
113
|
if (text.startsWith('/handoff')) {
|
|
106
114
|
const tabName = text.slice(9).trim() || 'default';
|
|
107
|
-
const { exportTab, formatHandoffInfo } = await import('../cli/handoff.js');
|
|
108
115
|
const info = exportTab(tabName);
|
|
109
116
|
if (!info)
|
|
110
117
|
return { handled: true, response: `Tab "${tabName}" not found.` };
|
|
118
|
+
logActivity('user_command', '/handoff', { tabName });
|
|
111
119
|
return { handled: true, response: formatHandoffInfo(info) };
|
|
112
120
|
}
|
|
113
121
|
// /folders (also accept legacy /projects)
|
|
114
|
-
if (text === '/folders' ||
|
|
122
|
+
if (text === '/folders' ||
|
|
123
|
+
text === '/projects' ||
|
|
124
|
+
text.startsWith('/folders@') ||
|
|
125
|
+
text.startsWith('/projects@')) {
|
|
115
126
|
const { listProjects } = await import('../projects/index.js');
|
|
116
127
|
const projects = listProjects();
|
|
117
128
|
if (projects.length === 0)
|
|
@@ -128,15 +139,22 @@ export async function handleSharedCommand(ctx, tabManager) {
|
|
|
128
139
|
return { handled: true, response: msg };
|
|
129
140
|
}
|
|
130
141
|
// /folder <name> (also accept legacy /project <name>)
|
|
131
|
-
if ((text.startsWith('/folder ') && !text.startsWith('/folders')) ||
|
|
142
|
+
if ((text.startsWith('/folder ') && !text.startsWith('/folders')) ||
|
|
143
|
+
(text.startsWith('/project ') && !text.startsWith('/projects'))) {
|
|
132
144
|
const prefix = text.startsWith('/folder ') ? '/folder ' : '/project ';
|
|
133
145
|
const name = text.slice(prefix.length).trim();
|
|
134
146
|
const { getProject, setUserContext } = await import('../projects/index.js');
|
|
135
147
|
const project = getProject(name);
|
|
136
148
|
if (!project)
|
|
137
|
-
return {
|
|
149
|
+
return {
|
|
150
|
+
handled: true,
|
|
151
|
+
response: `Folder "${name}" not found. Use /folders to list or /newfolder to create.`,
|
|
152
|
+
};
|
|
138
153
|
setUserContext(userId, project.name, project.name);
|
|
139
|
-
return {
|
|
154
|
+
return {
|
|
155
|
+
handled: true,
|
|
156
|
+
response: `Switched to folder: ${project.name}\nPath: ${project.path}\n\nNext messages will work in this folder.`,
|
|
157
|
+
};
|
|
140
158
|
}
|
|
141
159
|
// /newfolder <name> [path] (also accept legacy /newproject)
|
|
142
160
|
if (text.startsWith('/newfolder ') || text.startsWith('/newproject ')) {
|
|
@@ -149,7 +167,10 @@ export async function handleSharedCommand(ctx, tabManager) {
|
|
|
149
167
|
const { createProject, setUserContext } = await import('../projects/index.js');
|
|
150
168
|
const project = createProject(name, customPath);
|
|
151
169
|
setUserContext(userId, project.name, project.name);
|
|
152
|
-
return {
|
|
170
|
+
return {
|
|
171
|
+
handled: true,
|
|
172
|
+
response: `✓ Folder "${name}" created at ${project.path}\nSwitched to this folder.`,
|
|
173
|
+
};
|
|
153
174
|
}
|
|
154
175
|
// /close <tab>
|
|
155
176
|
if (text.startsWith('/close ')) {
|
|
@@ -157,7 +178,12 @@ export async function handleSharedCommand(ctx, tabManager) {
|
|
|
157
178
|
if (!tabNameToClose)
|
|
158
179
|
return { handled: true, response: 'Usage: /close <tabname>' };
|
|
159
180
|
const closed = tabManager.closeTab(tabNameToClose);
|
|
160
|
-
return {
|
|
181
|
+
return {
|
|
182
|
+
handled: true,
|
|
183
|
+
response: closed
|
|
184
|
+
? `Tab "${tabNameToClose}" permanently closed. History deleted.`
|
|
185
|
+
: `Tab "${tabNameToClose}" not found.`,
|
|
186
|
+
};
|
|
161
187
|
}
|
|
162
188
|
// /fresh <folder>
|
|
163
189
|
if (text.startsWith('/fresh ')) {
|
|
@@ -168,7 +194,10 @@ export async function handleSharedCommand(ctx, tabManager) {
|
|
|
168
194
|
return { handled: true, response: `Folder "${folderName}" not found.` };
|
|
169
195
|
const freshTabName = `${folderName}-${Date.now().toString(36).slice(-4)}`;
|
|
170
196
|
setUserContext(userId, project.name, freshTabName);
|
|
171
|
-
return {
|
|
197
|
+
return {
|
|
198
|
+
handled: true,
|
|
199
|
+
response: `Fresh start in "${folderName}" (tab: ${freshTabName})\nSend your message now.`,
|
|
200
|
+
};
|
|
172
201
|
}
|
|
173
202
|
// /history [date|yesterday]
|
|
174
203
|
if (text === '/history' || text.startsWith('/history ')) {
|
|
@@ -192,7 +221,10 @@ export async function handleSharedCommand(ctx, tabManager) {
|
|
|
192
221
|
const { getAllKnowledge, formatKnowledgeForContext } = await import('../knowledge/index.js');
|
|
193
222
|
const entries = getAllKnowledge();
|
|
194
223
|
if (entries.length === 0) {
|
|
195
|
-
return {
|
|
224
|
+
return {
|
|
225
|
+
handled: true,
|
|
226
|
+
response: 'No knowledge stored yet. Beecork learns from your conversations.',
|
|
227
|
+
};
|
|
196
228
|
}
|
|
197
229
|
return { handled: true, response: formatKnowledgeForContext(entries).slice(0, 4000) };
|
|
198
230
|
}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import type { Channel, ChannelContext,
|
|
1
|
+
import type { Channel, ChannelContext, SendOptions } from './types.js';
|
|
2
2
|
export declare class DiscordChannel implements Channel {
|
|
3
3
|
readonly id = "discord";
|
|
4
4
|
readonly name = "Discord";
|
|
5
5
|
readonly maxMessageLength = 2000;
|
|
6
|
-
readonly supportsStreaming = false;
|
|
7
|
-
readonly supportsMedia = true;
|
|
8
6
|
private client;
|
|
9
7
|
private ctx;
|
|
10
8
|
private allowedUserIds;
|
|
@@ -12,9 +10,8 @@ export declare class DiscordChannel implements Channel {
|
|
|
12
10
|
constructor(ctx: ChannelContext);
|
|
13
11
|
start(): Promise<void>;
|
|
14
12
|
stop(): void;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
sendNotification(message: string, urgent?: boolean): Promise<void>;
|
|
13
|
+
sendMessage(peerId: string, text: string, _options?: SendOptions): Promise<void>;
|
|
14
|
+
sendNotification(message: string, _urgent?: boolean): Promise<void>;
|
|
18
15
|
setTyping(peerId: string, active: boolean): Promise<void>;
|
|
19
16
|
private sendResponse;
|
|
20
17
|
}
|
package/dist/channels/discord.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Discord integration via discord.js (peer-optional, dynamic-imported).
|
|
3
|
+
*
|
|
4
|
+
* discord.js's strict union types (PartialGroupDMChannel, TextChannel, etc.)
|
|
5
|
+
* require narrowing at every send/sendTyping callsite. The runtime channel
|
|
6
|
+
* objects we receive in handlers all support these methods, but expressing
|
|
7
|
+
* that to TypeScript via the published types is a significant refactor with
|
|
8
|
+
* no runtime benefit. We accept `any` at this library boundary — same pattern
|
|
9
|
+
* as the WhatsApp/baileys integration.
|
|
10
|
+
*/
|
|
11
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
1
12
|
import { logger } from '../util/logger.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
13
|
+
import { parseTabMessage } from '../util/text.js';
|
|
14
|
+
import { sendChunkedResponse } from './send-helpers.js';
|
|
4
15
|
import { inboundLimiter } from '../util/rate-limiter.js';
|
|
5
16
|
import { saveMedia, isOversized } from '../media/store.js';
|
|
6
17
|
import { VoiceState } from './voice-state.js';
|
|
@@ -10,8 +21,6 @@ export class DiscordChannel {
|
|
|
10
21
|
id = 'discord';
|
|
11
22
|
name = 'Discord';
|
|
12
23
|
maxMessageLength = 2000;
|
|
13
|
-
supportsStreaming = false; // Discord message editing is rate-limited
|
|
14
|
-
supportsMedia = true;
|
|
15
24
|
client = null; // Discord.js Client
|
|
16
25
|
ctx;
|
|
17
26
|
allowedUserIds;
|
|
@@ -56,7 +65,9 @@ export class DiscordChannel {
|
|
|
56
65
|
}
|
|
57
66
|
// Rate limit
|
|
58
67
|
if (!inboundLimiter.check(this.id)) {
|
|
59
|
-
await message
|
|
68
|
+
await message
|
|
69
|
+
.reply("I'm receiving too many messages right now. Please wait a moment.")
|
|
70
|
+
.catch(() => { });
|
|
60
71
|
return;
|
|
61
72
|
}
|
|
62
73
|
const text = message.content
|
|
@@ -188,26 +199,25 @@ export class DiscordChannel {
|
|
|
188
199
|
}
|
|
189
200
|
logger.info('Discord bot stopped');
|
|
190
201
|
}
|
|
191
|
-
|
|
192
|
-
// Messages are handled directly in start()
|
|
193
|
-
}
|
|
194
|
-
async sendMessage(peerId, text, options) {
|
|
202
|
+
async sendMessage(peerId, text, _options) {
|
|
195
203
|
if (!this.client)
|
|
196
204
|
return;
|
|
197
205
|
try {
|
|
198
206
|
const channel = await this.client.channels.fetch(peerId);
|
|
199
207
|
if (!channel?.isTextBased())
|
|
200
208
|
return;
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
209
|
+
await sendChunkedResponse({
|
|
210
|
+
text,
|
|
211
|
+
maxLength: this.maxMessageLength,
|
|
212
|
+
retryLabel: 'discord-send',
|
|
213
|
+
sendChunk: (chunk) => channel.send(chunk),
|
|
214
|
+
});
|
|
205
215
|
}
|
|
206
216
|
catch (err) {
|
|
207
217
|
logger.error(`Discord send failed for ${peerId}:`, err);
|
|
208
218
|
}
|
|
209
219
|
}
|
|
210
|
-
async sendNotification(message,
|
|
220
|
+
async sendNotification(message, _urgent) {
|
|
211
221
|
if (!this.client)
|
|
212
222
|
return;
|
|
213
223
|
// Send to all allowed users via DM
|
|
@@ -236,14 +246,21 @@ export class DiscordChannel {
|
|
|
236
246
|
}
|
|
237
247
|
async sendResponse(message, text, tabName) {
|
|
238
248
|
// Discord quirk: first chunk uses message.reply so it threads under the
|
|
239
|
-
// original user message; follow-ups use channel.send.
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
249
|
+
// original user message; follow-ups use channel.send. sendChunkedResponse
|
|
250
|
+
// handles prefix + chunking + retry envelope; the closure-tracked chunkIdx
|
|
251
|
+
// keeps the "first chunk replies, rest send" behavior.
|
|
252
|
+
let chunkIdx = 0;
|
|
253
|
+
await sendChunkedResponse({
|
|
254
|
+
text,
|
|
255
|
+
tabName,
|
|
256
|
+
maxLength: this.maxMessageLength,
|
|
257
|
+
retryDelays: [1000, 5000],
|
|
258
|
+
retryLabel: 'discord-send',
|
|
259
|
+
sendChunk: (chunk) => {
|
|
260
|
+
const isFirst = chunkIdx === 0;
|
|
261
|
+
chunkIdx++;
|
|
262
|
+
return isFirst ? message.reply(chunk) : message.channel.send(chunk);
|
|
263
|
+
},
|
|
264
|
+
});
|
|
248
265
|
}
|
|
249
266
|
}
|
package/dist/channels/index.d.ts
CHANGED
|
@@ -4,4 +4,4 @@ export { WhatsAppChannel } from './whatsapp.js';
|
|
|
4
4
|
export { WebhookChannel } from './webhook.js';
|
|
5
5
|
export { DiscordChannel } from './discord.js';
|
|
6
6
|
export { loadCommunityChannels } from './loader.js';
|
|
7
|
-
export type { Channel, ChannelContext,
|
|
7
|
+
export type { Channel, ChannelContext, MediaAttachment, SendOptions } from './types.js';
|
package/dist/channels/loader.js
CHANGED
|
@@ -47,8 +47,15 @@ export async function loadCommunityChannels(ctx) {
|
|
|
47
47
|
logger.warn(`Community channel ${dir}: entry point not found at ${entryPath}`);
|
|
48
48
|
continue;
|
|
49
49
|
}
|
|
50
|
-
// Dynamic import — community channels run with full daemon access
|
|
51
|
-
|
|
50
|
+
// Dynamic import — community channels run with full daemon access:
|
|
51
|
+
// they can read every config token (Telegram, Discord, WhatsApp,
|
|
52
|
+
// notification provider keys) AND every environment variable the
|
|
53
|
+
// daemon inherits (including SSH-agent sockets, GitHub tokens,
|
|
54
|
+
// anything sourced from your shell profile). A compromised
|
|
55
|
+
// community-channel package is effectively a root credential drop.
|
|
56
|
+
// Pin versions in package.json, audit before opting in, and treat
|
|
57
|
+
// every entry in `communityChannels` as a trust anchor.
|
|
58
|
+
logger.warn(`Loading community channel "${dir}" — this package will have FULL access to your channel tokens (Telegram/Discord/WhatsApp) and every environment variable inherited by the daemon (including SSH keys). Only enable if you trust the package author and have audited the version.`);
|
|
52
59
|
const module = await import(entryPath);
|
|
53
60
|
const ChannelClass = module.default || module[Object.keys(module)[0]];
|
|
54
61
|
if (!ChannelClass || typeof ChannelClass !== 'function') {
|
|
@@ -57,7 +64,10 @@ export async function loadCommunityChannels(ctx) {
|
|
|
57
64
|
}
|
|
58
65
|
const instance = new ChannelClass(ctx);
|
|
59
66
|
// Validate it implements Channel interface (duck typing)
|
|
60
|
-
if (!instance.id ||
|
|
67
|
+
if (!instance.id ||
|
|
68
|
+
!instance.name ||
|
|
69
|
+
typeof instance.start !== 'function' ||
|
|
70
|
+
typeof instance.stop !== 'function') {
|
|
61
71
|
logger.warn(`Community channel ${dir}: does not implement Channel interface`);
|
|
62
72
|
continue;
|
|
63
73
|
}
|
|
@@ -22,9 +22,20 @@ import { logger } from '../util/logger.js';
|
|
|
22
22
|
export async function processInboundMessage(opts) {
|
|
23
23
|
const { text, media, channelId, tabManager, voiceReplyMode, ttsProvider, userId, sendProgress, overrideTabName, onTextChunk, } = opts;
|
|
24
24
|
// 1. Parse tab name and prompt from the message text
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
const parsed = parseTabMessage(text);
|
|
26
|
+
const rawPrompt = parsed.prompt;
|
|
27
|
+
let tabName = parsed.tabName;
|
|
28
|
+
// Allow channel to override tab name (group tabs, thread tabs, etc.).
|
|
29
|
+
// Validate centrally so each channel doesn't need its own check — keeps the
|
|
30
|
+
// CLAUDE.md "validation is centralized" rule honest. `default` is allowed.
|
|
27
31
|
if (overrideTabName) {
|
|
32
|
+
if (overrideTabName !== 'default') {
|
|
33
|
+
const { validateTabName } = await import('../config.js');
|
|
34
|
+
const overrideErr = validateTabName(overrideTabName);
|
|
35
|
+
if (overrideErr) {
|
|
36
|
+
return { responseText: `Invalid tab name: ${overrideErr}`, tabName, isError: true };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
28
39
|
tabName = overrideTabName;
|
|
29
40
|
}
|
|
30
41
|
if (!rawPrompt && media.length === 0) {
|
|
@@ -56,9 +67,7 @@ export async function processInboundMessage(opts) {
|
|
|
56
67
|
progressTracker.stop();
|
|
57
68
|
// 6. Build response text
|
|
58
69
|
const isError = result.error;
|
|
59
|
-
const responseText = isError
|
|
60
|
-
? `Error: ${result.text}`
|
|
61
|
-
: result.text || '(empty response)';
|
|
70
|
+
const responseText = isError ? `Error: ${result.text}` : result.text || '(empty response)';
|
|
62
71
|
// 7. TTS voice reply (shared logic)
|
|
63
72
|
let audioPath;
|
|
64
73
|
let voiceOnly = false;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Channel } from './types.js';
|
|
2
|
+
import type { MediaAttachment } from '../types.js';
|
|
2
3
|
/**
|
|
3
4
|
* ChannelRegistry manages the lifecycle of all channels.
|
|
4
5
|
* Channels register themselves, and the registry handles start/stop/broadcast.
|
|
@@ -7,7 +8,12 @@ export declare class ChannelRegistry {
|
|
|
7
8
|
private channels;
|
|
8
9
|
/** Register a channel */
|
|
9
10
|
register(channel: Channel): void;
|
|
10
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Start all registered channels in parallel. Each channel's start() is an
|
|
13
|
+
* independent network handshake (Telegram getMe, Discord login, WhatsApp
|
|
14
|
+
* Baileys boot) — running them sequentially adds 2-5s to cold start with no
|
|
15
|
+
* coordination benefit.
|
|
16
|
+
*/
|
|
11
17
|
start(): Promise<void>;
|
|
12
18
|
/** Stop all channels */
|
|
13
19
|
stop(): void;
|
|
@@ -17,4 +23,14 @@ export declare class ChannelRegistry {
|
|
|
17
23
|
getAll(): Channel[];
|
|
18
24
|
/** Broadcast a notification to all channels */
|
|
19
25
|
broadcastNotify(text: string, urgent?: boolean): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Broadcast a media attachment to every channel that supports it. Used by
|
|
28
|
+
* the pending-message dispatcher to deliver media queued by
|
|
29
|
+
* `beecork_send_media` and the `beecork_generate_*` tools — without this,
|
|
30
|
+
* those tools queue a row whose JSON envelope is never rendered.
|
|
31
|
+
*
|
|
32
|
+
* Channels that don't implement broadcastMedia get a sendNotification
|
|
33
|
+
* fallback so the user at least learns a file was generated.
|
|
34
|
+
*/
|
|
35
|
+
broadcastMedia(media: MediaAttachment): Promise<void>;
|
|
20
36
|
}
|
|
@@ -13,9 +13,14 @@ export class ChannelRegistry {
|
|
|
13
13
|
this.channels.set(channel.id, channel);
|
|
14
14
|
logger.info(`Channel registered: ${channel.name} (${channel.id})`);
|
|
15
15
|
}
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Start all registered channels in parallel. Each channel's start() is an
|
|
18
|
+
* independent network handshake (Telegram getMe, Discord login, WhatsApp
|
|
19
|
+
* Baileys boot) — running them sequentially adds 2-5s to cold start with no
|
|
20
|
+
* coordination benefit.
|
|
21
|
+
*/
|
|
17
22
|
async start() {
|
|
18
|
-
|
|
23
|
+
await Promise.all(Array.from(this.channels.entries()).map(async ([id, channel]) => {
|
|
19
24
|
try {
|
|
20
25
|
await channel.start();
|
|
21
26
|
logger.info(`Channel started: ${channel.name}`);
|
|
@@ -23,7 +28,7 @@ export class ChannelRegistry {
|
|
|
23
28
|
catch (err) {
|
|
24
29
|
logger.error(`Failed to start channel ${id}:`, err);
|
|
25
30
|
}
|
|
26
|
-
}
|
|
31
|
+
}));
|
|
27
32
|
}
|
|
28
33
|
/** Stop all channels */
|
|
29
34
|
stop() {
|
|
@@ -46,6 +51,30 @@ export class ChannelRegistry {
|
|
|
46
51
|
}
|
|
47
52
|
/** Broadcast a notification to all channels */
|
|
48
53
|
async broadcastNotify(text, urgent) {
|
|
49
|
-
await Promise.all(Array.from(this.channels.values()).map(channel
|
|
54
|
+
await Promise.all(Array.from(this.channels.values()).map((channel) => channel
|
|
55
|
+
.sendNotification(text, urgent)
|
|
56
|
+
.catch((err) => logger.warn(`${channel.name} notify failed:`, err))));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Broadcast a media attachment to every channel that supports it. Used by
|
|
60
|
+
* the pending-message dispatcher to deliver media queued by
|
|
61
|
+
* `beecork_send_media` and the `beecork_generate_*` tools — without this,
|
|
62
|
+
* those tools queue a row whose JSON envelope is never rendered.
|
|
63
|
+
*
|
|
64
|
+
* Channels that don't implement broadcastMedia get a sendNotification
|
|
65
|
+
* fallback so the user at least learns a file was generated.
|
|
66
|
+
*/
|
|
67
|
+
async broadcastMedia(media) {
|
|
68
|
+
await Promise.all(Array.from(this.channels.values()).map((channel) => {
|
|
69
|
+
if (channel.broadcastMedia) {
|
|
70
|
+
return channel
|
|
71
|
+
.broadcastMedia(media)
|
|
72
|
+
.catch((err) => logger.warn(`${channel.name} broadcastMedia failed:`, err));
|
|
73
|
+
}
|
|
74
|
+
const fallback = `📎 Media generated: ${media.filePath}${media.caption ? `\n${media.caption}` : ''}`;
|
|
75
|
+
return channel
|
|
76
|
+
.sendNotification(fallback)
|
|
77
|
+
.catch((err) => logger.warn(`${channel.name} media-fallback notify failed:`, err));
|
|
78
|
+
}));
|
|
50
79
|
}
|
|
51
80
|
}
|
|
@@ -1,30 +1,45 @@
|
|
|
1
|
-
import type { Channel, ChannelContext,
|
|
1
|
+
import type { Channel, ChannelContext, MediaAttachment, SendOptions } from './types.js';
|
|
2
2
|
export declare class TelegramChannel implements Channel {
|
|
3
3
|
readonly id = "telegram";
|
|
4
4
|
readonly name = "Telegram";
|
|
5
5
|
readonly maxMessageLength = 4096;
|
|
6
|
-
readonly supportsStreaming = true;
|
|
7
|
-
readonly supportsMedia = true;
|
|
8
6
|
private bot;
|
|
9
7
|
private ctx;
|
|
10
8
|
private activeChatIds;
|
|
9
|
+
private allowedUserIdSet;
|
|
11
10
|
private voice;
|
|
12
11
|
private botUserId;
|
|
13
12
|
private botUsername;
|
|
14
13
|
private mutedGroups;
|
|
15
14
|
private welcomeSent;
|
|
15
|
+
private pollingErrorTimes;
|
|
16
|
+
private pollingDegradedNotified;
|
|
16
17
|
constructor(ctx: ChannelContext);
|
|
17
18
|
start(): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Track polling errors over a 60s rolling window. If we see 5+ errors in 60s
|
|
21
|
+
* we surface "polling degraded" exactly once via the notify callback so the
|
|
22
|
+
* user knows Telegram is broken instead of silently failing.
|
|
23
|
+
*/
|
|
24
|
+
private recordPollingError;
|
|
18
25
|
stop(): void;
|
|
19
|
-
sendMessage(peerId: string, text: string,
|
|
26
|
+
sendMessage(peerId: string, text: string, _options?: SendOptions): Promise<void>;
|
|
20
27
|
sendNotification(message: string, _urgent?: boolean): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Send a media file to every recipient the bot would notify. Used by the
|
|
30
|
+
* pending-message dispatcher to deliver media queued by MCP tools.
|
|
31
|
+
* Routes by attachment type to the right Telegram primitive (sendVoice for
|
|
32
|
+
* voice messages, sendPhoto for images, sendVideo for video, sendDocument
|
|
33
|
+
* for everything else).
|
|
34
|
+
*/
|
|
35
|
+
broadcastMedia(media: MediaAttachment): Promise<void>;
|
|
21
36
|
setTyping(peerId: string, active: boolean): Promise<void>;
|
|
22
|
-
onMessage(_handler: InboundMessageHandler): void;
|
|
23
37
|
private setupHandlers;
|
|
24
38
|
private handleCommand;
|
|
25
39
|
private handleMessage;
|
|
26
40
|
private sendResponse;
|
|
27
41
|
private sendWithRetry;
|
|
42
|
+
private sendWithRetryRaw;
|
|
28
43
|
private downloadTelegramFile;
|
|
29
44
|
private isAllowed;
|
|
30
45
|
private isAdmin;
|