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
|
@@ -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)',
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared admin check for channel commands. Single source of truth so the
|
|
3
|
+
* "who can run admin commands?" rule doesn't drift between channels.
|
|
4
|
+
*
|
|
5
|
+
* Policy:
|
|
6
|
+
* - If an explicit admin peer ID is configured, only they are admin.
|
|
7
|
+
* - Else the first allowed peer in the allowlist is admin.
|
|
8
|
+
* - If the allowlist is empty, no one is admin (fail closed).
|
|
9
|
+
*/
|
|
10
|
+
export declare function isChannelAdmin(allowList: Iterable<string | number>, peerId: string | number | undefined, explicitAdmin?: string | number): boolean;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared admin check for channel commands. Single source of truth so the
|
|
3
|
+
* "who can run admin commands?" rule doesn't drift between channels.
|
|
4
|
+
*
|
|
5
|
+
* Policy:
|
|
6
|
+
* - If an explicit admin peer ID is configured, only they are admin.
|
|
7
|
+
* - Else the first allowed peer in the allowlist is admin.
|
|
8
|
+
* - If the allowlist is empty, no one is admin (fail closed).
|
|
9
|
+
*/
|
|
10
|
+
export function isChannelAdmin(allowList, peerId, explicitAdmin) {
|
|
11
|
+
if (peerId === undefined || peerId === null)
|
|
12
|
+
return false;
|
|
13
|
+
if (explicitAdmin !== undefined && explicitAdmin !== null) {
|
|
14
|
+
return String(peerId) === String(explicitAdmin);
|
|
15
|
+
}
|
|
16
|
+
const first = [...allowList][0];
|
|
17
|
+
if (first === undefined)
|
|
18
|
+
return false;
|
|
19
|
+
return String(peerId) === String(first);
|
|
20
|
+
}
|
|
@@ -9,19 +9,11 @@ export interface CommandResult {
|
|
|
9
9
|
handled: boolean;
|
|
10
10
|
response?: string;
|
|
11
11
|
}
|
|
12
|
-
export
|
|
13
|
-
effectiveTabName: string;
|
|
14
|
-
projectPath?: string;
|
|
15
|
-
confirmationMessage?: string;
|
|
16
|
-
}
|
|
12
|
+
export type { RouteResult } from '../projects/router.js';
|
|
17
13
|
/**
|
|
18
14
|
* Handle shared commands that work identically across all channels.
|
|
19
15
|
* Returns { handled: true, response } if a command was matched.
|
|
20
16
|
* The channel is responsible for sending the response via its own API.
|
|
21
17
|
*/
|
|
22
18
|
export declare function handleSharedCommand(ctx: CommandContext, tabManager: TabManager): Promise<CommandResult>;
|
|
23
|
-
|
|
24
|
-
* Shared project routing logic — resolves which tab/project to use for a message.
|
|
25
|
-
* Extracted from the identical blocks in Telegram, WhatsApp, and Discord channels.
|
|
26
|
-
*/
|
|
27
|
-
export declare function resolveProjectRoute(rawPrompt: string, tabName: string, text: string, userId: string): Promise<RouteResult>;
|
|
19
|
+
export { resolveProjectRoute } from '../projects/router.js';
|
|
@@ -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>
|
|
@@ -32,12 +36,22 @@ export async function handleSharedCommand(ctx, tabManager) {
|
|
|
32
36
|
const rest = text.slice(5);
|
|
33
37
|
const setPromptMatch = rest.match(/^(\S+)\s+--set-prompt\s+"([^"]+)"/);
|
|
34
38
|
if (setPromptMatch) {
|
|
39
|
+
if (!isAdmin)
|
|
40
|
+
return { handled: true, response: 'Only admin can change system prompts.' };
|
|
35
41
|
const tabName = setPromptMatch[1];
|
|
42
|
+
if (tabName !== 'default') {
|
|
43
|
+
const nameErr = validateTabName(tabName);
|
|
44
|
+
if (nameErr)
|
|
45
|
+
return { handled: true, response: `Invalid tab name: ${nameErr}` };
|
|
46
|
+
}
|
|
36
47
|
const systemPrompt = setPromptMatch[2];
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
48
|
+
const updated = tabManager.setSystemPrompt(tabName, systemPrompt);
|
|
49
|
+
return {
|
|
50
|
+
handled: true,
|
|
51
|
+
response: updated
|
|
52
|
+
? `System prompt updated for tab "${tabName}"`
|
|
53
|
+
: `Tab "${tabName}" not found.`,
|
|
54
|
+
};
|
|
41
55
|
}
|
|
42
56
|
const spaceIdx = rest.indexOf(' ');
|
|
43
57
|
if (spaceIdx === -1) {
|
|
@@ -51,42 +65,9 @@ export async function handleSharedCommand(ctx, tabManager) {
|
|
|
51
65
|
// /tab with a valid name + message — not handled here, falls through to message handling
|
|
52
66
|
return { handled: false };
|
|
53
67
|
}
|
|
54
|
-
// /register
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const existing = resolveUser(ctx.channelId, userId);
|
|
58
|
-
if (existing) {
|
|
59
|
-
return { handled: true, response: `You're already registered as "${existing.name}" (${existing.role}).` };
|
|
60
|
-
}
|
|
61
|
-
const name = text.slice(10).trim() || `user-${userId}`;
|
|
62
|
-
const role = hasAdmin() ? 'user' : 'admin';
|
|
63
|
-
const user = registerUser(name, ctx.channelId, userId, role);
|
|
64
|
-
return { handled: true, response: `Registered as "${user.name}" (${user.role}).${role === 'admin' ? ' You are the admin.' : ''}` };
|
|
65
|
-
}
|
|
66
|
-
// /link channel:peerId
|
|
67
|
-
if (text.startsWith('/link ')) {
|
|
68
|
-
const { resolveUser, linkIdentity } = await import('../users/index.js');
|
|
69
|
-
const user = resolveUser(ctx.channelId, userId);
|
|
70
|
-
if (!user)
|
|
71
|
-
return { handled: true, response: 'Register first: /register' };
|
|
72
|
-
const parts = text.slice(6).trim().split(':');
|
|
73
|
-
if (parts.length !== 2) {
|
|
74
|
-
return { handled: true, response: 'Usage: /link channel:peerId (e.g., /link discord:123456789)' };
|
|
75
|
-
}
|
|
76
|
-
const success = linkIdentity(user.id, parts[0], parts[1]);
|
|
77
|
-
return { handled: true, response: success ? `Linked ${parts[0]} identity.` : 'Failed to link — already linked or invalid.' };
|
|
78
|
-
}
|
|
79
|
-
// /users (admin only)
|
|
80
|
-
if (text === '/users') {
|
|
81
|
-
if (!isAdmin)
|
|
82
|
-
return { handled: true, response: 'Admin only.' };
|
|
83
|
-
const { listUsers } = await import('../users/index.js');
|
|
84
|
-
const users = listUsers();
|
|
85
|
-
if (users.length === 0)
|
|
86
|
-
return { handled: true, response: 'No registered users.' };
|
|
87
|
-
const list = users.map(u => `• ${u.name} [${u.role}] — ${u.id.slice(0, 8)}`).join('\n');
|
|
88
|
-
return { handled: true, response: `${users.length} user(s):\n${list}` };
|
|
89
|
-
}
|
|
68
|
+
// /register, /link, /users were part of unused multi-user scaffolding —
|
|
69
|
+
// removed in the audit fix pass. Beecork is single-user; admin is the first
|
|
70
|
+
// allowedUserId on Telegram (or config.telegram.adminUserId).
|
|
90
71
|
// /watches
|
|
91
72
|
if (text === '/watches' || text.startsWith('/watches@')) {
|
|
92
73
|
const { getDb } = await import('../db/index.js');
|
|
@@ -94,23 +75,27 @@ export async function handleSharedCommand(ctx, tabManager) {
|
|
|
94
75
|
const watchers = db.prepare('SELECT * FROM watchers ORDER BY created_at').all();
|
|
95
76
|
if (watchers.length === 0)
|
|
96
77
|
return { handled: true, response: 'No watchers configured.' };
|
|
97
|
-
const watchList = watchers
|
|
78
|
+
const watchList = watchers
|
|
79
|
+
.map((w) => {
|
|
98
80
|
const status = w.enabled ? 'active' : 'disabled';
|
|
99
81
|
return `[${status}] ${w.name} -- ${w.schedule} (triggers: ${w.trigger_count})`;
|
|
100
|
-
})
|
|
82
|
+
})
|
|
83
|
+
.join('\n');
|
|
101
84
|
return { handled: true, response: `Watchers:\n${watchList}` };
|
|
102
85
|
}
|
|
103
86
|
// /tasks
|
|
104
87
|
if (text === '/tasks' || text.startsWith('/tasks@')) {
|
|
105
88
|
const { getDb } = await import('../db/index.js');
|
|
106
89
|
const db = getDb();
|
|
107
|
-
const tasks = db.prepare('SELECT * FROM tasks
|
|
90
|
+
const tasks = db.prepare('SELECT * FROM tasks ORDER BY created_at').all();
|
|
108
91
|
if (tasks.length === 0)
|
|
109
92
|
return { handled: true, response: 'No tasks scheduled.' };
|
|
110
|
-
const taskList = tasks
|
|
93
|
+
const taskList = tasks
|
|
94
|
+
.map((t) => {
|
|
111
95
|
const status = t.enabled ? 'enabled' : 'disabled';
|
|
112
96
|
return `[${status}] ${t.name} (${t.schedule_type}: ${t.schedule}) -> tab:${t.tab_name}`;
|
|
113
|
-
})
|
|
97
|
+
})
|
|
98
|
+
.join('\n');
|
|
114
99
|
return { handled: true, response: `Tasks:\n${taskList}` };
|
|
115
100
|
}
|
|
116
101
|
// /cost
|
|
@@ -127,14 +112,17 @@ export async function handleSharedCommand(ctx, tabManager) {
|
|
|
127
112
|
// /handoff [tab]
|
|
128
113
|
if (text.startsWith('/handoff')) {
|
|
129
114
|
const tabName = text.slice(9).trim() || 'default';
|
|
130
|
-
const { exportTab, formatHandoffInfo } = await import('../cli/handoff.js');
|
|
131
115
|
const info = exportTab(tabName);
|
|
132
116
|
if (!info)
|
|
133
117
|
return { handled: true, response: `Tab "${tabName}" not found.` };
|
|
118
|
+
logActivity('user_command', '/handoff', { tabName });
|
|
134
119
|
return { handled: true, response: formatHandoffInfo(info) };
|
|
135
120
|
}
|
|
136
121
|
// /folders (also accept legacy /projects)
|
|
137
|
-
if (text === '/folders' ||
|
|
122
|
+
if (text === '/folders' ||
|
|
123
|
+
text === '/projects' ||
|
|
124
|
+
text.startsWith('/folders@') ||
|
|
125
|
+
text.startsWith('/projects@')) {
|
|
138
126
|
const { listProjects } = await import('../projects/index.js');
|
|
139
127
|
const projects = listProjects();
|
|
140
128
|
if (projects.length === 0)
|
|
@@ -151,15 +139,22 @@ export async function handleSharedCommand(ctx, tabManager) {
|
|
|
151
139
|
return { handled: true, response: msg };
|
|
152
140
|
}
|
|
153
141
|
// /folder <name> (also accept legacy /project <name>)
|
|
154
|
-
if ((text.startsWith('/folder ') && !text.startsWith('/folders')) ||
|
|
142
|
+
if ((text.startsWith('/folder ') && !text.startsWith('/folders')) ||
|
|
143
|
+
(text.startsWith('/project ') && !text.startsWith('/projects'))) {
|
|
155
144
|
const prefix = text.startsWith('/folder ') ? '/folder ' : '/project ';
|
|
156
145
|
const name = text.slice(prefix.length).trim();
|
|
157
146
|
const { getProject, setUserContext } = await import('../projects/index.js');
|
|
158
147
|
const project = getProject(name);
|
|
159
148
|
if (!project)
|
|
160
|
-
return {
|
|
149
|
+
return {
|
|
150
|
+
handled: true,
|
|
151
|
+
response: `Folder "${name}" not found. Use /folders to list or /newfolder to create.`,
|
|
152
|
+
};
|
|
161
153
|
setUserContext(userId, project.name, project.name);
|
|
162
|
-
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
|
+
};
|
|
163
158
|
}
|
|
164
159
|
// /newfolder <name> [path] (also accept legacy /newproject)
|
|
165
160
|
if (text.startsWith('/newfolder ') || text.startsWith('/newproject ')) {
|
|
@@ -172,18 +167,23 @@ export async function handleSharedCommand(ctx, tabManager) {
|
|
|
172
167
|
const { createProject, setUserContext } = await import('../projects/index.js');
|
|
173
168
|
const project = createProject(name, customPath);
|
|
174
169
|
setUserContext(userId, project.name, project.name);
|
|
175
|
-
return {
|
|
170
|
+
return {
|
|
171
|
+
handled: true,
|
|
172
|
+
response: `✓ Folder "${name}" created at ${project.path}\nSwitched to this folder.`,
|
|
173
|
+
};
|
|
176
174
|
}
|
|
177
175
|
// /close <tab>
|
|
178
176
|
if (text.startsWith('/close ')) {
|
|
179
177
|
const tabNameToClose = text.slice(7).trim();
|
|
180
178
|
if (!tabNameToClose)
|
|
181
179
|
return { handled: true, response: 'Usage: /close <tabname>' };
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
180
|
+
const closed = tabManager.closeTab(tabNameToClose);
|
|
181
|
+
return {
|
|
182
|
+
handled: true,
|
|
183
|
+
response: closed
|
|
184
|
+
? `Tab "${tabNameToClose}" permanently closed. History deleted.`
|
|
185
|
+
: `Tab "${tabNameToClose}" not found.`,
|
|
186
|
+
};
|
|
187
187
|
}
|
|
188
188
|
// /fresh <folder>
|
|
189
189
|
if (text.startsWith('/fresh ')) {
|
|
@@ -194,36 +194,42 @@ export async function handleSharedCommand(ctx, tabManager) {
|
|
|
194
194
|
return { handled: true, response: `Folder "${folderName}" not found.` };
|
|
195
195
|
const freshTabName = `${folderName}-${Date.now().toString(36).slice(-4)}`;
|
|
196
196
|
setUserContext(userId, project.name, freshTabName);
|
|
197
|
-
return {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
/**
|
|
202
|
-
* Shared project routing logic — resolves which tab/project to use for a message.
|
|
203
|
-
* Extracted from the identical blocks in Telegram, WhatsApp, and Discord channels.
|
|
204
|
-
*/
|
|
205
|
-
export async function resolveProjectRoute(rawPrompt, tabName, text, userId) {
|
|
206
|
-
if (tabName !== 'default' || text.startsWith('/tab ')) {
|
|
207
|
-
return { effectiveTabName: tabName };
|
|
197
|
+
return {
|
|
198
|
+
handled: true,
|
|
199
|
+
response: `Fresh start in "${folderName}" (tab: ${freshTabName})\nSend your message now.`,
|
|
200
|
+
};
|
|
208
201
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
202
|
+
// /history [date|yesterday]
|
|
203
|
+
if (text === '/history' || text.startsWith('/history ')) {
|
|
204
|
+
const dateArg = text.slice(9).trim();
|
|
205
|
+
const { getTimeline, formatTimeline } = await import('../timeline/index.js');
|
|
206
|
+
let date;
|
|
207
|
+
if (dateArg === 'yesterday') {
|
|
208
|
+
date = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
|
|
209
|
+
}
|
|
210
|
+
else if (dateArg) {
|
|
211
|
+
date = dateArg;
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
date = new Date().toISOString().slice(0, 10);
|
|
215
|
+
}
|
|
216
|
+
const events = getTimeline({ date, limit: 30 });
|
|
217
|
+
return { handled: true, response: formatTimeline(events) };
|
|
218
|
+
}
|
|
219
|
+
// /knowledge
|
|
220
|
+
if (text === '/knowledge') {
|
|
221
|
+
const { getAllKnowledge, formatKnowledgeForContext } = await import('../knowledge/index.js');
|
|
222
|
+
const entries = getAllKnowledge();
|
|
223
|
+
if (entries.length === 0) {
|
|
215
224
|
return {
|
|
216
|
-
|
|
217
|
-
|
|
225
|
+
handled: true,
|
|
226
|
+
response: 'No knowledge stored yet. Beecork learns from your conversations.',
|
|
218
227
|
};
|
|
219
228
|
}
|
|
220
|
-
|
|
221
|
-
return {
|
|
222
|
-
effectiveTabName: decision.tabName,
|
|
223
|
-
projectPath: decision.project.path,
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
catch {
|
|
227
|
-
return { effectiveTabName: tabName };
|
|
229
|
+
return { handled: true, response: formatKnowledgeForContext(entries).slice(0, 4000) };
|
|
228
230
|
}
|
|
231
|
+
return { handled: false };
|
|
229
232
|
}
|
|
233
|
+
// resolveProjectRoute lives at src/projects/router.ts — re-exported here so
|
|
234
|
+
// existing callers (channels/pipeline.ts) don't need to update their import path.
|
|
235
|
+
export { resolveProjectRoute } from '../projects/router.js';
|
|
@@ -1,22 +1,17 @@
|
|
|
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;
|
|
11
|
-
private
|
|
12
|
-
private ttsProvider;
|
|
13
|
-
private sttWarmedUp;
|
|
9
|
+
private voice;
|
|
14
10
|
constructor(ctx: ChannelContext);
|
|
15
11
|
start(): Promise<void>;
|
|
16
12
|
stop(): void;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
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>;
|
|
20
15
|
setTyping(peerId: string, active: boolean): Promise<void>;
|
|
21
16
|
private sendResponse;
|
|
22
17
|
}
|
package/dist/channels/discord.js
CHANGED
|
@@ -1,22 +1,30 @@
|
|
|
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
|
-
import {
|
|
17
|
+
import { VoiceState } from './voice-state.js';
|
|
7
18
|
import { processInboundMessage } from './pipeline.js';
|
|
19
|
+
import { isChannelAdmin } from './admin.js';
|
|
8
20
|
export class DiscordChannel {
|
|
9
21
|
id = 'discord';
|
|
10
22
|
name = 'Discord';
|
|
11
23
|
maxMessageLength = 2000;
|
|
12
|
-
supportsStreaming = false; // Discord message editing is rate-limited
|
|
13
|
-
supportsMedia = true;
|
|
14
24
|
client = null; // Discord.js Client
|
|
15
25
|
ctx;
|
|
16
26
|
allowedUserIds;
|
|
17
|
-
|
|
18
|
-
ttsProvider = null;
|
|
19
|
-
sttWarmedUp = false;
|
|
27
|
+
voice = new VoiceState('discord');
|
|
20
28
|
constructor(ctx) {
|
|
21
29
|
this.ctx = ctx;
|
|
22
30
|
this.allowedUserIds = new Set((ctx.config.discord?.allowedUserIds ?? []).map(String));
|
|
@@ -37,10 +45,8 @@ export class DiscordChannel {
|
|
|
37
45
|
GatewayIntentBits.MessageContent,
|
|
38
46
|
],
|
|
39
47
|
});
|
|
40
|
-
// Voice providers
|
|
41
|
-
|
|
42
|
-
this.sttProvider = stt;
|
|
43
|
-
this.ttsProvider = tts;
|
|
48
|
+
// Voice providers (STT + TTS)
|
|
49
|
+
this.voice.init(this.ctx.config);
|
|
44
50
|
this.client.on(Events.MessageCreate, async (message) => {
|
|
45
51
|
// Ignore bot messages
|
|
46
52
|
if (message.author.bot)
|
|
@@ -59,17 +65,18 @@ export class DiscordChannel {
|
|
|
59
65
|
}
|
|
60
66
|
// Rate limit
|
|
61
67
|
if (!inboundLimiter.check(this.id)) {
|
|
62
|
-
await message
|
|
68
|
+
await message
|
|
69
|
+
.reply("I'm receiving too many messages right now. Please wait a moment.")
|
|
70
|
+
.catch(() => { });
|
|
63
71
|
return;
|
|
64
72
|
}
|
|
65
73
|
const text = message.content
|
|
66
74
|
.replace(/<@!?\d+>/g, '') // Remove mentions
|
|
67
75
|
.trim();
|
|
68
|
-
// Warm up STT connection on first message with attachments
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
this.
|
|
72
|
-
}
|
|
76
|
+
// Warm up STT connection on first message with attachments.
|
|
77
|
+
// (Discord intentionally only warms up; it doesn't transcribe like Telegram/WhatsApp.)
|
|
78
|
+
if (message.attachments.size > 0)
|
|
79
|
+
await this.voice.warmup();
|
|
73
80
|
// Download attachments
|
|
74
81
|
const media = [];
|
|
75
82
|
for (const attachment of message.attachments.values()) {
|
|
@@ -115,7 +122,7 @@ export class DiscordChannel {
|
|
|
115
122
|
const cmdResult = await handleSharedCommand({
|
|
116
123
|
userId: message.author.id,
|
|
117
124
|
text,
|
|
118
|
-
isAdmin: this.allowedUserIds
|
|
125
|
+
isAdmin: isChannelAdmin(this.allowedUserIds, message.author.id, this.ctx.config.discord?.adminUserId),
|
|
119
126
|
channelId: 'discord',
|
|
120
127
|
}, this.ctx.tabManager);
|
|
121
128
|
if (cmdResult.handled) {
|
|
@@ -127,12 +134,16 @@ export class DiscordChannel {
|
|
|
127
134
|
// Discord-specific: use thread name as tab if in a thread
|
|
128
135
|
let overrideTabName;
|
|
129
136
|
if (message.channel.isThread?.()) {
|
|
130
|
-
const
|
|
137
|
+
const sanitized = (message.channel.name || '')
|
|
131
138
|
.replace(/[^a-zA-Z0-9-]/g, '-')
|
|
132
139
|
.replace(/^-+|-+$/g, '')
|
|
133
140
|
.slice(0, 32);
|
|
134
|
-
|
|
135
|
-
|
|
141
|
+
// Run the synthesized name through validateTabName so weird thread
|
|
142
|
+
// names (empty, starts with hyphen, "default") don't blow up downstream.
|
|
143
|
+
const { validateTabName } = await import('../config.js');
|
|
144
|
+
if (sanitized && tabName === 'default' && !validateTabName(sanitized)) {
|
|
145
|
+
overrideTabName = sanitized;
|
|
146
|
+
}
|
|
136
147
|
}
|
|
137
148
|
// Typing indicator refresh
|
|
138
149
|
const typingInterval = setInterval(() => {
|
|
@@ -146,7 +157,7 @@ export class DiscordChannel {
|
|
|
146
157
|
channelId: 'discord',
|
|
147
158
|
tabManager: this.ctx.tabManager,
|
|
148
159
|
voiceReplyMode: this.ctx.config.voice?.replyMode,
|
|
149
|
-
ttsProvider: this.
|
|
160
|
+
ttsProvider: this.voice.tts,
|
|
150
161
|
userId: message.author.id,
|
|
151
162
|
sendProgress: (msg) => {
|
|
152
163
|
message.channel.send(msg).catch(() => { });
|
|
@@ -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
|
|
@@ -235,15 +245,22 @@ export class DiscordChannel {
|
|
|
235
245
|
catch { }
|
|
236
246
|
}
|
|
237
247
|
async sendResponse(message, text, tabName) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
+
// Discord quirk: first chunk uses message.reply so it threads under the
|
|
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';
|