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
package/dist/util/logger.js
CHANGED
|
@@ -7,11 +7,24 @@ const LEVEL_PRIORITY = {
|
|
|
7
7
|
warn: 2,
|
|
8
8
|
error: 3,
|
|
9
9
|
};
|
|
10
|
+
// Patterns that should never reach disk or stdout. Each channel uses a per-call
|
|
11
|
+
// sanitizer too (defense-in-depth), but the Logger is the last line of defense
|
|
12
|
+
// for any third-party error object that happens to embed a secret in its message.
|
|
13
|
+
const REDACTION_PATTERNS = [
|
|
14
|
+
{ re: /bot\d+:[A-Za-z0-9_-]+/g, replacement: 'bot<REDACTED>' }, // Telegram bot token
|
|
15
|
+
];
|
|
16
|
+
function redact(s) {
|
|
17
|
+
let out = s;
|
|
18
|
+
for (const { re, replacement } of REDACTION_PATTERNS)
|
|
19
|
+
out = out.replace(re, replacement);
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
const ROTATE_BYTES = 10 * 1024 * 1024;
|
|
10
23
|
class Logger {
|
|
11
24
|
minLevel = 'info';
|
|
12
25
|
logFile = null;
|
|
13
26
|
stream = null;
|
|
14
|
-
|
|
27
|
+
bytesWritten = 0;
|
|
15
28
|
setLevel(level) {
|
|
16
29
|
this.minLevel = level;
|
|
17
30
|
}
|
|
@@ -20,18 +33,30 @@ class Logger {
|
|
|
20
33
|
fs.mkdirSync(dir, { recursive: true });
|
|
21
34
|
this.logFile = path.join(dir, name);
|
|
22
35
|
this.stream = fs.createWriteStream(this.logFile, { flags: 'a' });
|
|
36
|
+
// Seed bytesWritten from the existing file size so we don't reset the
|
|
37
|
+
// rotation counter on every daemon restart.
|
|
38
|
+
try {
|
|
39
|
+
this.bytesWritten = fs.statSync(this.logFile).size;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
this.bytesWritten = 0;
|
|
43
|
+
}
|
|
23
44
|
}
|
|
24
45
|
write(level, msg, ...args) {
|
|
25
46
|
if (LEVEL_PRIORITY[level] < LEVEL_PRIORITY[this.minLevel])
|
|
26
47
|
return;
|
|
27
48
|
const timestamp = new Date().toISOString();
|
|
28
49
|
const prefix = `[${timestamp}] [${level.toUpperCase()}]`;
|
|
29
|
-
const
|
|
30
|
-
? `${prefix} ${msg} ${args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' ')}`
|
|
50
|
+
const raw = args.length > 0
|
|
51
|
+
? `${prefix} ${msg} ${args.map((a) => (typeof a === 'string' ? a : JSON.stringify(a))).join(' ')}`
|
|
31
52
|
: `${prefix} ${msg}`;
|
|
53
|
+
const line = redact(raw);
|
|
32
54
|
if (this.stream) {
|
|
33
|
-
|
|
34
|
-
this.
|
|
55
|
+
const out = line + '\n';
|
|
56
|
+
this.stream.write(out);
|
|
57
|
+
this.bytesWritten += out.length;
|
|
58
|
+
if (this.bytesWritten > ROTATE_BYTES)
|
|
59
|
+
this.checkRotation();
|
|
35
60
|
}
|
|
36
61
|
if (level === 'error') {
|
|
37
62
|
console.error(line);
|
|
@@ -43,31 +68,38 @@ class Logger {
|
|
|
43
68
|
console.log(line);
|
|
44
69
|
}
|
|
45
70
|
}
|
|
46
|
-
debug(msg, ...args) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
71
|
+
debug(msg, ...args) {
|
|
72
|
+
this.write('debug', msg, ...args);
|
|
73
|
+
}
|
|
74
|
+
info(msg, ...args) {
|
|
75
|
+
this.write('info', msg, ...args);
|
|
76
|
+
}
|
|
77
|
+
warn(msg, ...args) {
|
|
78
|
+
this.write('warn', msg, ...args);
|
|
79
|
+
}
|
|
80
|
+
error(msg, ...args) {
|
|
81
|
+
this.write('error', msg, ...args);
|
|
82
|
+
}
|
|
50
83
|
checkRotation() {
|
|
51
84
|
if (!this.logFile || !this.stream)
|
|
52
85
|
return;
|
|
53
|
-
|
|
54
|
-
if (this.writeCount < 100)
|
|
55
|
-
return;
|
|
56
|
-
this.writeCount = 0;
|
|
86
|
+
// bytesWritten-based gate already triggered us; rotate without statSync.
|
|
57
87
|
try {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
try {
|
|
63
|
-
fs.unlinkSync(rotated);
|
|
64
|
-
}
|
|
65
|
-
catch { }
|
|
66
|
-
fs.renameSync(this.logFile, rotated);
|
|
67
|
-
this.stream = fs.createWriteStream(this.logFile, { flags: 'a' });
|
|
88
|
+
this.stream.end();
|
|
89
|
+
const rotated = this.logFile + '.1';
|
|
90
|
+
try {
|
|
91
|
+
fs.unlinkSync(rotated);
|
|
68
92
|
}
|
|
93
|
+
catch {
|
|
94
|
+
/* not fatal */
|
|
95
|
+
}
|
|
96
|
+
fs.renameSync(this.logFile, rotated);
|
|
97
|
+
this.stream = fs.createWriteStream(this.logFile, { flags: 'a' });
|
|
98
|
+
this.bytesWritten = 0;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
/* rotation failure shouldn't kill the daemon */
|
|
69
102
|
}
|
|
70
|
-
catch { }
|
|
71
103
|
}
|
|
72
104
|
close() {
|
|
73
105
|
if (this.stream) {
|
|
@@ -77,3 +109,10 @@ class Logger {
|
|
|
77
109
|
}
|
|
78
110
|
}
|
|
79
111
|
export const logger = new Logger();
|
|
112
|
+
// Allow operators to bump verbosity at runtime without recompiling.
|
|
113
|
+
// e.g. `BEECORK_LOG_LEVEL=debug beecork start` surfaces every claude subprocess
|
|
114
|
+
// stdout/stderr line via the logger.debug calls.
|
|
115
|
+
const envLevel = process.env.BEECORK_LOG_LEVEL?.toLowerCase();
|
|
116
|
+
if (envLevel === 'debug' || envLevel === 'info' || envLevel === 'warn' || envLevel === 'error') {
|
|
117
|
+
logger.setLevel(envLevel);
|
|
118
|
+
}
|
package/dist/util/paths.d.ts
CHANGED
|
@@ -7,5 +7,7 @@ export declare function getLogsDir(): string;
|
|
|
7
7
|
export declare function getPidPath(): string;
|
|
8
8
|
export declare function getRuntimeInfoPath(): string;
|
|
9
9
|
export declare function getCronReloadSignalPath(): string;
|
|
10
|
+
export declare function getWatcherReloadSignalPath(): string;
|
|
11
|
+
export declare function getWhatsappSessionPath(): string;
|
|
10
12
|
export declare function ensureBeecorkDirs(): void;
|
|
11
13
|
export declare function expandHome(p: string): string;
|
package/dist/util/paths.js
CHANGED
|
@@ -3,7 +3,7 @@ import os from 'node:os';
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
const BEECORK_DIR = '.beecork';
|
|
5
5
|
export function getBeecorkHome() {
|
|
6
|
-
return path.join(os.homedir(), BEECORK_DIR);
|
|
6
|
+
return process.env.BEECORK_HOME || path.join(os.homedir(), BEECORK_DIR);
|
|
7
7
|
}
|
|
8
8
|
export function getConfigPath() {
|
|
9
9
|
return path.join(getBeecorkHome(), 'config.json');
|
|
@@ -29,10 +29,23 @@ export function getRuntimeInfoPath() {
|
|
|
29
29
|
export function getCronReloadSignalPath() {
|
|
30
30
|
return path.join(getBeecorkHome(), '.cron-reload');
|
|
31
31
|
}
|
|
32
|
+
export function getWatcherReloadSignalPath() {
|
|
33
|
+
return path.join(getBeecorkHome(), '.watcher-reload');
|
|
34
|
+
}
|
|
35
|
+
export function getWhatsappSessionPath() {
|
|
36
|
+
return path.join(getBeecorkHome(), 'whatsapp-session');
|
|
37
|
+
}
|
|
32
38
|
export function ensureBeecorkDirs() {
|
|
33
39
|
const home = getBeecorkHome();
|
|
34
|
-
fs.mkdirSync(home, { recursive: true });
|
|
35
|
-
|
|
40
|
+
fs.mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
41
|
+
// Upgrade an existing dir that was created with a looser umask before this fix landed.
|
|
42
|
+
try {
|
|
43
|
+
fs.chmodSync(home, 0o700);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
/* not fatal if mode change fails */
|
|
47
|
+
}
|
|
48
|
+
fs.mkdirSync(getLogsDir(), { recursive: true, mode: 0o700 });
|
|
36
49
|
}
|
|
37
50
|
export function expandHome(p) {
|
|
38
51
|
if (p.startsWith('~/') || p === '~') {
|
|
@@ -21,6 +21,14 @@ export class RateLimiter {
|
|
|
21
21
|
logger.warn(`Rate limit: global limit reached (${this.globalLimit}/min)`);
|
|
22
22
|
return false;
|
|
23
23
|
}
|
|
24
|
+
// Bound the per-key map so a daemon that has seen many one-off keys
|
|
25
|
+
// (e.g. thousands of Telegram groups over months) doesn't leak memory.
|
|
26
|
+
if (this.perKey.size > 1000) {
|
|
27
|
+
for (const [k, w] of this.perKey) {
|
|
28
|
+
if (now > w.resetAt)
|
|
29
|
+
this.perKey.delete(k);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
24
32
|
// Reset per-key window
|
|
25
33
|
let keyWindow = this.perKey.get(key);
|
|
26
34
|
if (!keyWindow || now > keyWindow.resetAt) {
|
package/dist/util/retry.js
CHANGED
|
@@ -10,7 +10,7 @@ export async function retryWithBackoff(fn, delays = [1000, 5000, 15000], label =
|
|
|
10
10
|
lastError = err instanceof Error ? err : new Error(String(err));
|
|
11
11
|
if (attempt < delays.length) {
|
|
12
12
|
logger.warn(`${label} failed (attempt ${attempt + 1}/${delays.length + 1}), retrying in ${delays[attempt]}ms: ${lastError.message}`);
|
|
13
|
-
await new Promise(resolve => setTimeout(resolve, delays[attempt]));
|
|
13
|
+
await new Promise((resolve) => setTimeout(resolve, delays[attempt]));
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
}
|
package/dist/util/text.d.ts
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
import type { MediaAttachment } from '../
|
|
1
|
+
import type { MediaAttachment } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Shared message-size limits. Single source of truth so caps don't drift
|
|
4
|
+
* between channels, MCP, and dashboard.
|
|
5
|
+
*/
|
|
6
|
+
export declare const MESSAGE_LIMITS: {
|
|
7
|
+
/** Per-chunk send size (Telegram = 4096; other channels chunk to this too). */
|
|
8
|
+
readonly CHUNK: 4096;
|
|
9
|
+
/** HTTP request body cap (webhook + dashboard endpoints). */
|
|
10
|
+
readonly HTTP_BODY: number;
|
|
11
|
+
/** Webhook channel single-message payload. */
|
|
12
|
+
readonly WEBHOOK_PROMPT: 100000;
|
|
13
|
+
/** MCP tool content/message field. Tight because it goes through Claude's tool-result token budget. */
|
|
14
|
+
readonly MCP_CONTENT: 10240;
|
|
15
|
+
};
|
|
2
16
|
/** Split long text into chunks that fit within a message limit */
|
|
3
17
|
export declare function chunkText(text: string, maxLength?: number): string[];
|
|
4
18
|
/** Format an ISO date as a human-readable relative time */
|
|
@@ -8,5 +22,11 @@ export declare function parseTabMessage(text: string): {
|
|
|
8
22
|
tabName: string;
|
|
9
23
|
prompt: string;
|
|
10
24
|
};
|
|
25
|
+
/**
|
|
26
|
+
* Build the channel-side message text with an optional [tabName] prefix.
|
|
27
|
+
* Used by all three channels' sendResponse paths so the prefix format is
|
|
28
|
+
* consistent and the gate logic (skip "default", skip empty) lives in one place.
|
|
29
|
+
*/
|
|
30
|
+
export declare function formatTabbedResponse(text: string, tabName?: string): string;
|
|
11
31
|
/** Build prompt text from media attachments */
|
|
12
32
|
export declare function buildMediaPrompt(media: MediaAttachment[], textPrompt: string): string;
|
package/dist/util/text.js
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Shared message-size limits. Single source of truth so caps don't drift
|
|
3
|
+
* between channels, MCP, and dashboard.
|
|
4
|
+
*/
|
|
5
|
+
export const MESSAGE_LIMITS = {
|
|
6
|
+
/** Per-chunk send size (Telegram = 4096; other channels chunk to this too). */
|
|
7
|
+
CHUNK: 4096,
|
|
8
|
+
/** HTTP request body cap (webhook + dashboard endpoints). */
|
|
9
|
+
HTTP_BODY: 1024 * 1024, // 1 MB
|
|
10
|
+
/** Webhook channel single-message payload. */
|
|
11
|
+
WEBHOOK_PROMPT: 100_000, // 100 KB
|
|
12
|
+
/** MCP tool content/message field. Tight because it goes through Claude's tool-result token budget. */
|
|
13
|
+
MCP_CONTENT: 10_240, // 10 KB
|
|
14
|
+
};
|
|
15
|
+
const DEFAULT_MAX_LENGTH = MESSAGE_LIMITS.CHUNK;
|
|
2
16
|
/** Split long text into chunks that fit within a message limit */
|
|
3
17
|
export function chunkText(text, maxLength = DEFAULT_MAX_LENGTH) {
|
|
4
18
|
if (text.length <= maxLength)
|
|
@@ -50,20 +64,36 @@ export function parseTabMessage(text) {
|
|
|
50
64
|
}
|
|
51
65
|
return { tabName: 'default', prompt: text };
|
|
52
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Build the channel-side message text with an optional [tabName] prefix.
|
|
69
|
+
* Used by all three channels' sendResponse paths so the prefix format is
|
|
70
|
+
* consistent and the gate logic (skip "default", skip empty) lives in one place.
|
|
71
|
+
*/
|
|
72
|
+
export function formatTabbedResponse(text, tabName) {
|
|
73
|
+
if (!tabName || tabName === 'default')
|
|
74
|
+
return text;
|
|
75
|
+
return `[${tabName}] ${text}`;
|
|
76
|
+
}
|
|
53
77
|
/** Build prompt text from media attachments */
|
|
54
78
|
export function buildMediaPrompt(media, textPrompt) {
|
|
55
79
|
if (media.length === 0)
|
|
56
80
|
return textPrompt;
|
|
57
|
-
const descriptions = media.map(m => {
|
|
81
|
+
const descriptions = media.map((m) => {
|
|
58
82
|
if (m.type === 'voice' && m.caption?.startsWith('[Transcribed'))
|
|
59
83
|
return m.caption;
|
|
60
84
|
switch (m.type) {
|
|
61
|
-
case 'image':
|
|
62
|
-
|
|
63
|
-
case '
|
|
64
|
-
|
|
65
|
-
case '
|
|
66
|
-
|
|
85
|
+
case 'image':
|
|
86
|
+
return `User sent an image: ${m.filePath}`;
|
|
87
|
+
case 'voice':
|
|
88
|
+
return `User sent a voice message: ${m.filePath}`;
|
|
89
|
+
case 'audio':
|
|
90
|
+
return `User sent an audio file: ${m.filePath}${m.fileName ? ` (${m.fileName})` : ''}`;
|
|
91
|
+
case 'video':
|
|
92
|
+
return `User sent a video: ${m.filePath}`;
|
|
93
|
+
case 'document':
|
|
94
|
+
return `User sent a file: ${m.filePath}${m.fileName ? ` (${m.fileName})` : ''}`;
|
|
95
|
+
default:
|
|
96
|
+
return `User sent a file: ${m.filePath}`;
|
|
67
97
|
}
|
|
68
98
|
});
|
|
69
99
|
const mediaText = descriptions.join('\n');
|
package/dist/voice/index.js
CHANGED
|
@@ -10,7 +10,11 @@ export function initVoiceProviders(voice) {
|
|
|
10
10
|
stt = createSTTProvider({ provider: voice.sttProvider, apiKey: voice.sttApiKey });
|
|
11
11
|
}
|
|
12
12
|
if (voice?.ttsProvider && voice.ttsProvider !== 'none') {
|
|
13
|
-
tts = createTTSProvider({
|
|
13
|
+
tts = createTTSProvider({
|
|
14
|
+
provider: voice.ttsProvider,
|
|
15
|
+
apiKey: voice.ttsApiKey,
|
|
16
|
+
voice: voice.ttsVoice,
|
|
17
|
+
});
|
|
14
18
|
}
|
|
15
19
|
return { stt, tts };
|
|
16
20
|
}
|
package/dist/voice/stt.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { logger } from '../util/logger.js';
|
|
3
|
+
import { assertInsideMediaDir } from '../media/store.js';
|
|
3
4
|
/** OpenAI Whisper API provider */
|
|
4
5
|
export class WhisperAPIProvider {
|
|
5
6
|
apiKey;
|
|
@@ -10,26 +11,33 @@ export class WhisperAPIProvider {
|
|
|
10
11
|
try {
|
|
11
12
|
// Make a lightweight request to warm up the HTTPS connection
|
|
12
13
|
await fetch('https://api.openai.com/v1/models', {
|
|
13
|
-
headers: {
|
|
14
|
+
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
14
15
|
signal: AbortSignal.timeout(5000),
|
|
15
16
|
});
|
|
16
17
|
}
|
|
17
|
-
catch {
|
|
18
|
+
catch {
|
|
19
|
+
/* non-critical */
|
|
20
|
+
}
|
|
18
21
|
}
|
|
19
22
|
async transcribe(filePath) {
|
|
23
|
+
// Defense-in-depth: callers should already constrain filePath to the media dir,
|
|
24
|
+
// but verify here so a leaked/forged MediaAttachment.filePath cannot exfiltrate
|
|
25
|
+
// arbitrary files (e.g. ~/.ssh/id_rsa) to the Whisper API.
|
|
26
|
+
const safePath = assertInsideMediaDir(filePath);
|
|
27
|
+
const buffer = await readFile(safePath);
|
|
20
28
|
const formData = new FormData();
|
|
21
|
-
formData.append('file', new Blob([
|
|
29
|
+
formData.append('file', new Blob([buffer]), 'audio.ogg');
|
|
22
30
|
formData.append('model', 'whisper-1');
|
|
23
31
|
const response = await fetch('https://api.openai.com/v1/audio/transcriptions', {
|
|
24
32
|
method: 'POST',
|
|
25
|
-
headers: {
|
|
33
|
+
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
26
34
|
body: formData,
|
|
27
35
|
signal: AbortSignal.timeout(60000),
|
|
28
36
|
});
|
|
29
37
|
if (!response.ok) {
|
|
30
38
|
throw new Error(`Whisper API error: ${response.status} ${response.statusText}`);
|
|
31
39
|
}
|
|
32
|
-
const result = await response.json();
|
|
40
|
+
const result = (await response.json());
|
|
33
41
|
return result.text;
|
|
34
42
|
}
|
|
35
43
|
}
|
package/dist/voice/tts.js
CHANGED
|
@@ -14,7 +14,7 @@ export class OpenAITTSProvider {
|
|
|
14
14
|
const response = await fetch('https://api.openai.com/v1/audio/speech', {
|
|
15
15
|
method: 'POST',
|
|
16
16
|
headers: {
|
|
17
|
-
|
|
17
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
18
18
|
'Content-Type': 'application/json',
|
|
19
19
|
},
|
|
20
20
|
body: JSON.stringify({
|
|
@@ -3,9 +3,10 @@ import path from 'node:path';
|
|
|
3
3
|
import { WatcherStore } from './store.js';
|
|
4
4
|
import { evaluateWatcher } from './evaluator.js';
|
|
5
5
|
import { execAsync, intervalToMs } from '../tasks/scheduler.js';
|
|
6
|
-
import {
|
|
6
|
+
import { getLogsDir, getWatcherReloadSignalPath } from '../util/paths.js';
|
|
7
7
|
import { logger } from '../util/logger.js';
|
|
8
|
-
|
|
8
|
+
import { PendingMessageStore } from '../session/pending-store.js';
|
|
9
|
+
import { logActivity } from '../timeline/index.js';
|
|
9
10
|
export class WatcherScheduler {
|
|
10
11
|
store = new WatcherStore();
|
|
11
12
|
// watcherId -> due time in ms epoch for next check
|
|
@@ -48,12 +49,14 @@ export class WatcherScheduler {
|
|
|
48
49
|
}
|
|
49
50
|
/** Check for the reload signal file and reload if present */
|
|
50
51
|
checkForReload() {
|
|
51
|
-
const signalPath =
|
|
52
|
+
const signalPath = getWatcherReloadSignalPath();
|
|
52
53
|
if (fs.existsSync(signalPath)) {
|
|
53
54
|
try {
|
|
54
55
|
fs.unlinkSync(signalPath);
|
|
55
56
|
}
|
|
56
|
-
catch {
|
|
57
|
+
catch {
|
|
58
|
+
/* race condition, ok */
|
|
59
|
+
}
|
|
57
60
|
logger.info('Watchers: reload signal detected, reloading');
|
|
58
61
|
this.loadAndSchedule();
|
|
59
62
|
}
|
|
@@ -102,6 +105,9 @@ export class WatcherScheduler {
|
|
|
102
105
|
if (result.triggered) {
|
|
103
106
|
this.store.markTriggered(watcher.id);
|
|
104
107
|
await fs.promises.appendFile(logFile, `[${new Date().toISOString()}] TRIGGERED: ${result.output.slice(0, 500)}\n`);
|
|
108
|
+
logActivity('watcher_triggered', `Watcher "${watcher.name}" triggered`, {
|
|
109
|
+
details: result.output.slice(0, 500),
|
|
110
|
+
});
|
|
105
111
|
await this.executeAction(watcher, result.output);
|
|
106
112
|
}
|
|
107
113
|
else {
|
|
@@ -156,7 +162,7 @@ export class WatcherScheduler {
|
|
|
156
162
|
message = watcher.actionDetails.slice(colonIdx + 1).trim();
|
|
157
163
|
}
|
|
158
164
|
const fullMessage = `[Watcher "${watcher.name}" triggered] ${message}\n\nWatcher output:\n${output.slice(0, 500)}`;
|
|
159
|
-
|
|
165
|
+
PendingMessageStore.enqueueDelegation(tabName, fullMessage, db);
|
|
160
166
|
if (this.onNotify) {
|
|
161
167
|
await this.onNotify(`Watcher "${watcher.name}" triggered -- delegated to tab:${tabName}`);
|
|
162
168
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beecork",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Claude Code always-on infrastructure — a phone number, a memory, and an alarm clock",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
"dev:daemon": "tsx src/daemon.ts",
|
|
16
16
|
"test": "vitest",
|
|
17
17
|
"lint": "eslint src/",
|
|
18
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
19
|
+
"format:check": "prettier --check \"src/**/*.ts\"",
|
|
18
20
|
"build:css": "tailwindcss --content src/dashboard/html.ts --minify",
|
|
19
21
|
"prepublishOnly": "npm test && npm run build",
|
|
20
22
|
"postinstall": "node scripts/postinstall.mjs"
|
|
@@ -27,6 +29,7 @@
|
|
|
27
29
|
"cron-parser": "^5.5.0",
|
|
28
30
|
"discord.js": "^14.26.2",
|
|
29
31
|
"node-telegram-bot-api": "^0.67.0",
|
|
32
|
+
"pino": "^9.5.0",
|
|
30
33
|
"qrcode-terminal": "^0.12.0",
|
|
31
34
|
"uuid": "^13.0.0"
|
|
32
35
|
},
|
|
@@ -36,6 +39,8 @@
|
|
|
36
39
|
"@types/node": "^24.0.0",
|
|
37
40
|
"@types/node-telegram-bot-api": "^0.64.14",
|
|
38
41
|
"eslint": "^10.2.0",
|
|
42
|
+
"eslint-config-prettier": "^10.1.8",
|
|
43
|
+
"prettier": "^3.8.3",
|
|
39
44
|
"tailwindcss": "^4.2.2",
|
|
40
45
|
"tsx": "^4.21.0",
|
|
41
46
|
"typescript": "^6.0.2",
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
/** Reserved for future approval mode implementation. Not currently wired into the runtime. */
|
|
2
|
-
export type ToolRisk = 'safe' | 'dangerous';
|
|
3
|
-
/** Classify a tool call as safe or dangerous */
|
|
4
|
-
export declare function classifyTool(toolName: string, input: Record<string, unknown>): ToolRisk;
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
const SAFE_TOOLS = new Set([
|
|
2
|
-
'Read', 'Glob', 'Grep', 'LSP', 'WebFetch', 'WebSearch',
|
|
3
|
-
'ToolSearch', 'TaskGet', 'TaskList',
|
|
4
|
-
]);
|
|
5
|
-
const DANGEROUS_TOOLS = new Set([
|
|
6
|
-
'Write', 'Edit', 'NotebookEdit',
|
|
7
|
-
'TaskCreate', 'TaskUpdate', 'TaskStop',
|
|
8
|
-
]);
|
|
9
|
-
const SAFE_BASH_PATTERNS = [
|
|
10
|
-
/^(ls|cat|head|tail|wc|file|stat|which|type|echo|printf)\b/,
|
|
11
|
-
/^git\s+(status|log|diff|show|branch|tag|remote)\b/,
|
|
12
|
-
/^(pwd|whoami|hostname|date|uname|env|printenv)\b/,
|
|
13
|
-
/^(find|grep|rg|fd|ag)\b/,
|
|
14
|
-
/^(node|python|ruby|go)\s+--?(version|help)/,
|
|
15
|
-
/^npm\s+(list|ls|view|info|outdated|audit)\b/,
|
|
16
|
-
/^curl\s.*-X\s*GET\b/,
|
|
17
|
-
/^curl\s+(?!.*-X\s*(POST|PUT|DELETE|PATCH))(?!.*--data)(?!.*-d\s)/,
|
|
18
|
-
];
|
|
19
|
-
const DANGEROUS_BASH_PATTERNS = [
|
|
20
|
-
/^rm\b/,
|
|
21
|
-
/^(mv|cp)\b.*--?(force|f)\b/,
|
|
22
|
-
/^chmod\b/,
|
|
23
|
-
/^chown\b/,
|
|
24
|
-
/^git\s+(push|reset|rebase|merge|checkout\s+--)\b/,
|
|
25
|
-
/^(docker|kubectl|terraform|ansible)\b/,
|
|
26
|
-
/^(sudo|su)\b/,
|
|
27
|
-
/^npm\s+(publish|install|uninstall|link)\b/,
|
|
28
|
-
/^(kill|killall|pkill)\b/,
|
|
29
|
-
/^curl\s.*-X\s*(POST|PUT|DELETE|PATCH)\b/,
|
|
30
|
-
];
|
|
31
|
-
/** Classify a tool call as safe or dangerous */
|
|
32
|
-
export function classifyTool(toolName, input) {
|
|
33
|
-
if (SAFE_TOOLS.has(toolName))
|
|
34
|
-
return 'safe';
|
|
35
|
-
if (DANGEROUS_TOOLS.has(toolName))
|
|
36
|
-
return 'dangerous';
|
|
37
|
-
// Bash tool: inspect the command
|
|
38
|
-
if (toolName === 'Bash') {
|
|
39
|
-
const command = String(input.command || '').trim();
|
|
40
|
-
for (const pattern of SAFE_BASH_PATTERNS) {
|
|
41
|
-
if (pattern.test(command))
|
|
42
|
-
return 'safe';
|
|
43
|
-
}
|
|
44
|
-
for (const pattern of DANGEROUS_BASH_PATTERNS) {
|
|
45
|
-
if (pattern.test(command))
|
|
46
|
-
return 'dangerous';
|
|
47
|
-
}
|
|
48
|
-
// Default: unknown bash commands are dangerous
|
|
49
|
-
return 'dangerous';
|
|
50
|
-
}
|
|
51
|
-
// MCP tools from external servers: default to dangerous
|
|
52
|
-
if (toolName.startsWith('mcp__'))
|
|
53
|
-
return 'dangerous';
|
|
54
|
-
// Unknown tools: default to dangerous
|
|
55
|
-
return 'dangerous';
|
|
56
|
-
}
|
package/dist/users/index.d.ts
DELETED
package/dist/users/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { resolveUser, registerUser, linkIdentity, listUsers, hasAdmin } from './service.js';
|
package/dist/users/service.d.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export interface User {
|
|
2
|
-
id: string;
|
|
3
|
-
name: string;
|
|
4
|
-
role: 'admin' | 'user';
|
|
5
|
-
budgetUsd: number | null;
|
|
6
|
-
createdAt: string;
|
|
7
|
-
}
|
|
8
|
-
/** Get or create a user from a channel identity */
|
|
9
|
-
export declare function resolveUser(channelId: string, peerId: string): User | null;
|
|
10
|
-
/** Register a new user */
|
|
11
|
-
export declare function registerUser(name: string, channelId: string, peerId: string, role?: 'admin' | 'user'): User;
|
|
12
|
-
/** Link an additional channel identity to an existing user */
|
|
13
|
-
export declare function linkIdentity(userId: string, channelId: string, peerId: string): boolean;
|
|
14
|
-
/** Get all users */
|
|
15
|
-
export declare function listUsers(): User[];
|
|
16
|
-
/** Check if any admin exists */
|
|
17
|
-
export declare function hasAdmin(): boolean;
|
package/dist/users/service.js
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
-
import { getDb } from '../db/index.js';
|
|
3
|
-
import { logger } from '../util/logger.js';
|
|
4
|
-
function rowToUser(row) {
|
|
5
|
-
return { id: row.id, name: row.name, role: row.role, budgetUsd: row.budget_usd, createdAt: row.created_at };
|
|
6
|
-
}
|
|
7
|
-
/** Get or create a user from a channel identity */
|
|
8
|
-
export function resolveUser(channelId, peerId) {
|
|
9
|
-
const db = getDb();
|
|
10
|
-
const identity = db.prepare('SELECT user_id FROM identities WHERE channel_id = ? AND peer_id = ?').get(channelId, peerId);
|
|
11
|
-
if (!identity)
|
|
12
|
-
return null;
|
|
13
|
-
const row = db.prepare('SELECT * FROM users WHERE id = ?').get(identity.user_id);
|
|
14
|
-
return row ? rowToUser(row) : null;
|
|
15
|
-
}
|
|
16
|
-
/** Register a new user */
|
|
17
|
-
export function registerUser(name, channelId, peerId, role = 'user') {
|
|
18
|
-
const db = getDb();
|
|
19
|
-
const id = uuidv4();
|
|
20
|
-
db.prepare('INSERT INTO users (id, name, role) VALUES (?, ?, ?)').run(id, name, role);
|
|
21
|
-
db.prepare('INSERT INTO identities (user_id, channel_id, peer_id) VALUES (?, ?, ?)').run(id, channelId, peerId);
|
|
22
|
-
logger.info(`User registered: ${name} (${role}) via ${channelId}:${peerId}`);
|
|
23
|
-
return { id, name, role, budgetUsd: null, createdAt: new Date().toISOString() };
|
|
24
|
-
}
|
|
25
|
-
/** Link an additional channel identity to an existing user */
|
|
26
|
-
export function linkIdentity(userId, channelId, peerId) {
|
|
27
|
-
const db = getDb();
|
|
28
|
-
try {
|
|
29
|
-
db.prepare('INSERT INTO identities (user_id, channel_id, peer_id) VALUES (?, ?, ?)').run(userId, channelId, peerId);
|
|
30
|
-
return true;
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
return false; // Already linked or conflict
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
/** Get all users */
|
|
37
|
-
export function listUsers() {
|
|
38
|
-
const db = getDb();
|
|
39
|
-
return db.prepare('SELECT * FROM users ORDER BY created_at').all().map(rowToUser);
|
|
40
|
-
}
|
|
41
|
-
/** Check if any admin exists */
|
|
42
|
-
export function hasAdmin() {
|
|
43
|
-
const db = getDb();
|
|
44
|
-
const row = db.prepare("SELECT COUNT(*) as c FROM users WHERE role = 'admin'").get();
|
|
45
|
-
return row.c > 0;
|
|
46
|
-
}
|