beecork 1.5.0 → 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/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 +6 -1
- package/dist/session/tool-classifier.d.ts +0 -4
- package/dist/session/tool-classifier.js +0 -56
package/dist/tasks/store.js
CHANGED
|
@@ -4,17 +4,29 @@ import { getCrontabPath } from '../util/paths.js';
|
|
|
4
4
|
import { logger } from '../util/logger.js';
|
|
5
5
|
function rowToTask(row) {
|
|
6
6
|
return {
|
|
7
|
-
id: row.id,
|
|
7
|
+
id: row.id,
|
|
8
|
+
name: row.name,
|
|
8
9
|
scheduleType: row.schedule_type,
|
|
9
|
-
schedule: row.schedule,
|
|
10
|
+
schedule: row.schedule,
|
|
11
|
+
tabName: row.tab_name,
|
|
12
|
+
message: row.message,
|
|
10
13
|
payloadType: row.payload_type || 'agentTurn',
|
|
11
|
-
enabled: row.enabled === 1,
|
|
12
|
-
|
|
14
|
+
enabled: row.enabled === 1,
|
|
15
|
+
createdAt: row.created_at,
|
|
16
|
+
lastRunAt: row.last_run_at,
|
|
17
|
+
nextRunAt: row.next_run_at,
|
|
13
18
|
};
|
|
14
19
|
}
|
|
20
|
+
// One-shot JSON migration only needs to run once per process lifetime, but the
|
|
21
|
+
// dashboard creates a fresh TaskStore per request and MCP creates one per task
|
|
22
|
+
// call. Without this flag, every instantiation re-fired existsSync + COUNT(*).
|
|
23
|
+
let migrationChecked = false;
|
|
15
24
|
export class TaskStore {
|
|
16
25
|
constructor() {
|
|
17
|
-
|
|
26
|
+
if (!migrationChecked) {
|
|
27
|
+
migrationChecked = true;
|
|
28
|
+
this.migrateFromJson();
|
|
29
|
+
}
|
|
18
30
|
}
|
|
19
31
|
list() {
|
|
20
32
|
const db = getDb();
|
|
@@ -63,7 +75,9 @@ export class TaskStore {
|
|
|
63
75
|
try {
|
|
64
76
|
fs.renameSync(jsonPath, jsonPath + '.bak');
|
|
65
77
|
}
|
|
66
|
-
catch {
|
|
78
|
+
catch {
|
|
79
|
+
/* ok */
|
|
80
|
+
}
|
|
67
81
|
return;
|
|
68
82
|
}
|
|
69
83
|
try {
|
package/dist/timeline/logger.js
CHANGED
|
@@ -5,5 +5,7 @@ export function logActivity(eventType, summary, options) {
|
|
|
5
5
|
const db = getDb();
|
|
6
6
|
db.prepare('INSERT INTO activity_log (id, event_type, project_name, tab_name, summary, details, duration_ms, cost_usd) VALUES (?, ?, ?, ?, ?, ?, ?, ?)').run(uuidv4(), eventType, options?.projectName || null, options?.tabName || null, summary, options?.details || null, options?.durationMs || null, options?.costUsd || null);
|
|
7
7
|
}
|
|
8
|
-
catch {
|
|
8
|
+
catch {
|
|
9
|
+
/* non-critical — don't crash if logging fails */
|
|
10
|
+
}
|
|
9
11
|
}
|
package/dist/timeline/query.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { getDb } from '../db/index.js';
|
|
2
2
|
function rowToEvent(r) {
|
|
3
3
|
return {
|
|
4
|
-
id: r.id,
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
id: r.id,
|
|
5
|
+
eventType: r.event_type,
|
|
6
|
+
projectName: r.project_name,
|
|
7
|
+
tabName: r.tab_name,
|
|
8
|
+
summary: r.summary,
|
|
9
|
+
details: r.details,
|
|
10
|
+
durationMs: r.duration_ms,
|
|
11
|
+
costUsd: r.cost_usd,
|
|
12
|
+
createdAt: r.created_at,
|
|
7
13
|
};
|
|
8
14
|
}
|
|
9
15
|
export function getTimeline(options) {
|
package/dist/types.d.ts
CHANGED
|
@@ -13,7 +13,6 @@ export interface ClaudeCodeConfig {
|
|
|
13
13
|
}
|
|
14
14
|
export interface MemoryConfig {
|
|
15
15
|
dbPath: string;
|
|
16
|
-
maxLongTermEntries: number;
|
|
17
16
|
}
|
|
18
17
|
export interface TabConfig {
|
|
19
18
|
workingDir: string;
|
|
@@ -44,11 +43,26 @@ export interface DiscordConfig {
|
|
|
44
43
|
/** Optional admin user ID. Defaults to allowedUserIds[0]. */
|
|
45
44
|
adminUserId?: string;
|
|
46
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Webhook channel auth semantics:
|
|
48
|
+
*
|
|
49
|
+
* - When both `authToken` and `hmacSecret` are configured, EITHER mode
|
|
50
|
+
* satisfies authentication (OR semantics). A valid Bearer token is enough
|
|
51
|
+
* even if the HMAC signature is missing. There is no AND mode today.
|
|
52
|
+
* - Replay protection is NOT implemented. Callers must keep their payloads
|
|
53
|
+
* idempotent — see L10 in the audit. Acceptable while the server binds to
|
|
54
|
+
* 127.0.0.1 only; revisit if remote exposure becomes a thing.
|
|
55
|
+
* - `allowUnauthLocalhost` is the explicit opt-in to run with no auth at all.
|
|
56
|
+
* Useful for `curl localhost` from a trusted shell, but on a multi-user
|
|
57
|
+
* host any local process can inject prompts into your tabs.
|
|
58
|
+
*/
|
|
47
59
|
export interface WebhookConfig {
|
|
48
60
|
enabled: boolean;
|
|
49
61
|
port: number;
|
|
50
62
|
authToken?: string;
|
|
51
63
|
hmacSecret?: string;
|
|
64
|
+
/** Set to true to skip the fail-secure auth check at start. NOT recommended on shared hosts. */
|
|
65
|
+
allowUnauthLocalhost?: boolean;
|
|
52
66
|
}
|
|
53
67
|
export interface GroupConfig {
|
|
54
68
|
activationMode: 'mention' | 'reply' | 'keyword' | 'always';
|
|
@@ -56,15 +70,25 @@ export interface GroupConfig {
|
|
|
56
70
|
tabPerGroup: boolean;
|
|
57
71
|
keywords?: string[];
|
|
58
72
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Discriminated union of notification provider configs. The `type` field
|
|
75
|
+
* narrows which fields are required so callers get full type safety instead
|
|
76
|
+
* of treating every field as optional. createNotificationProvider switches
|
|
77
|
+
* exhaustively on `type`.
|
|
78
|
+
*/
|
|
79
|
+
export type NotificationConfig = {
|
|
80
|
+
type: 'pushover';
|
|
81
|
+
userKey: string;
|
|
82
|
+
appToken: string;
|
|
83
|
+
} | {
|
|
84
|
+
type: 'ntfy';
|
|
85
|
+
topic: string;
|
|
64
86
|
server?: string;
|
|
65
|
-
|
|
87
|
+
} | {
|
|
88
|
+
type: 'webhook';
|
|
89
|
+
url: string;
|
|
66
90
|
headers?: Record<string, string>;
|
|
67
|
-
}
|
|
91
|
+
};
|
|
68
92
|
export interface MediaGeneratorConfig {
|
|
69
93
|
provider: string;
|
|
70
94
|
apiKey?: string;
|
|
@@ -77,7 +101,6 @@ export interface MediaAttachment {
|
|
|
77
101
|
mimeType: string;
|
|
78
102
|
filePath: string;
|
|
79
103
|
fileName?: string;
|
|
80
|
-
duration?: number;
|
|
81
104
|
caption?: string;
|
|
82
105
|
}
|
|
83
106
|
export interface BeecorkConfig {
|
|
@@ -96,6 +119,8 @@ export interface BeecorkConfig {
|
|
|
96
119
|
notifications?: NotificationConfig[];
|
|
97
120
|
mediaGenerators?: MediaGeneratorConfig[];
|
|
98
121
|
communityChannels?: string[];
|
|
122
|
+
/** Capability packs enabled via `beecork capabilities enable`. Owned by src/capabilities. */
|
|
123
|
+
capabilities?: import('./capabilities/types.js').EnabledCapability[];
|
|
99
124
|
deployment: 'local' | 'vps';
|
|
100
125
|
}
|
|
101
126
|
export type TabStatus = 'idle' | 'running' | 'error' | 'stopped';
|
package/dist/util/auto-heal.js
CHANGED
|
@@ -41,10 +41,20 @@ export function autoHealInstall(fromFileUrl) {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
if (rewroteUnit && signaledDaemon) {
|
|
44
|
-
return {
|
|
44
|
+
return {
|
|
45
|
+
action: 'rewrote-and-signaled',
|
|
46
|
+
unitPath,
|
|
47
|
+
oldDaemonScript: oldDaemonScript,
|
|
48
|
+
newDaemonScript: currentDaemonScript,
|
|
49
|
+
};
|
|
45
50
|
}
|
|
46
51
|
if (rewroteUnit)
|
|
47
|
-
return {
|
|
52
|
+
return {
|
|
53
|
+
action: 'rewrote-unit',
|
|
54
|
+
unitPath,
|
|
55
|
+
oldDaemonScript: oldDaemonScript,
|
|
56
|
+
newDaemonScript: currentDaemonScript,
|
|
57
|
+
};
|
|
48
58
|
if (signaledDaemon)
|
|
49
59
|
return { action: 'signaled-daemon', unitPath };
|
|
50
60
|
return { action: 'noop' };
|
|
@@ -64,8 +74,8 @@ export function extractDaemonScript(content) {
|
|
|
64
74
|
// launchd plist: pull the second <string> inside ProgramArguments
|
|
65
75
|
const launchdMatch = content.match(/<key>\s*ProgramArguments\s*<\/key>\s*<array>([\s\S]*?)<\/array>/);
|
|
66
76
|
if (launchdMatch) {
|
|
67
|
-
const args = Array.from(launchdMatch[1].matchAll(/<string>([^<]+)<\/string>/g)).map(m => m[1]);
|
|
68
|
-
const jsArg = args.find(a => a.endsWith('daemon.js'));
|
|
77
|
+
const args = Array.from(launchdMatch[1].matchAll(/<string>([^<]+)<\/string>/g)).map((m) => m[1]);
|
|
78
|
+
const jsArg = args.find((a) => a.endsWith('daemon.js'));
|
|
69
79
|
if (jsArg)
|
|
70
80
|
return jsArg;
|
|
71
81
|
}
|
|
@@ -73,7 +83,7 @@ export function extractDaemonScript(content) {
|
|
|
73
83
|
const systemdMatch = content.match(/^ExecStart\s*=\s*(.+)$/m);
|
|
74
84
|
if (systemdMatch) {
|
|
75
85
|
const parts = systemdMatch[1].split(/\s+/);
|
|
76
|
-
const jsArg = parts.find(p => p.endsWith('daemon.js'));
|
|
86
|
+
const jsArg = parts.find((p) => p.endsWith('daemon.js'));
|
|
77
87
|
if (jsArg)
|
|
78
88
|
return jsArg;
|
|
79
89
|
}
|
package/dist/util/logger.d.ts
CHANGED
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
|
@@ -8,5 +8,6 @@ export declare function getPidPath(): string;
|
|
|
8
8
|
export declare function getRuntimeInfoPath(): string;
|
|
9
9
|
export declare function getCronReloadSignalPath(): string;
|
|
10
10
|
export declare function getWatcherReloadSignalPath(): string;
|
|
11
|
+
export declare function getWhatsappSessionPath(): string;
|
|
11
12
|
export declare function ensureBeecorkDirs(): void;
|
|
12
13
|
export declare function expandHome(p: string): string;
|
package/dist/util/paths.js
CHANGED
|
@@ -32,10 +32,20 @@ export function getCronReloadSignalPath() {
|
|
|
32
32
|
export function getWatcherReloadSignalPath() {
|
|
33
33
|
return path.join(getBeecorkHome(), '.watcher-reload');
|
|
34
34
|
}
|
|
35
|
+
export function getWhatsappSessionPath() {
|
|
36
|
+
return path.join(getBeecorkHome(), 'whatsapp-session');
|
|
37
|
+
}
|
|
35
38
|
export function ensureBeecorkDirs() {
|
|
36
39
|
const home = getBeecorkHome();
|
|
37
|
-
fs.mkdirSync(home, { recursive: true });
|
|
38
|
-
|
|
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 });
|
|
39
49
|
}
|
|
40
50
|
export function expandHome(p) {
|
|
41
51
|
if (p.startsWith('~/') || p === '~') {
|
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.js
CHANGED
|
@@ -78,16 +78,22 @@ export function formatTabbedResponse(text, tabName) {
|
|
|
78
78
|
export function buildMediaPrompt(media, textPrompt) {
|
|
79
79
|
if (media.length === 0)
|
|
80
80
|
return textPrompt;
|
|
81
|
-
const descriptions = media.map(m => {
|
|
81
|
+
const descriptions = media.map((m) => {
|
|
82
82
|
if (m.type === 'voice' && m.caption?.startsWith('[Transcribed'))
|
|
83
83
|
return m.caption;
|
|
84
84
|
switch (m.type) {
|
|
85
|
-
case 'image':
|
|
86
|
-
|
|
87
|
-
case '
|
|
88
|
-
|
|
89
|
-
case '
|
|
90
|
-
|
|
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}`;
|
|
91
97
|
}
|
|
92
98
|
});
|
|
93
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({
|
|
@@ -5,6 +5,8 @@ import { evaluateWatcher } from './evaluator.js';
|
|
|
5
5
|
import { execAsync, intervalToMs } from '../tasks/scheduler.js';
|
|
6
6
|
import { getLogsDir, getWatcherReloadSignalPath } from '../util/paths.js';
|
|
7
7
|
import { logger } from '../util/logger.js';
|
|
8
|
+
import { PendingMessageStore } from '../session/pending-store.js';
|
|
9
|
+
import { logActivity } from '../timeline/index.js';
|
|
8
10
|
export class WatcherScheduler {
|
|
9
11
|
store = new WatcherStore();
|
|
10
12
|
// watcherId -> due time in ms epoch for next check
|
|
@@ -52,7 +54,9 @@ export class WatcherScheduler {
|
|
|
52
54
|
try {
|
|
53
55
|
fs.unlinkSync(signalPath);
|
|
54
56
|
}
|
|
55
|
-
catch {
|
|
57
|
+
catch {
|
|
58
|
+
/* race condition, ok */
|
|
59
|
+
}
|
|
56
60
|
logger.info('Watchers: reload signal detected, reloading');
|
|
57
61
|
this.loadAndSchedule();
|
|
58
62
|
}
|
|
@@ -101,6 +105,9 @@ export class WatcherScheduler {
|
|
|
101
105
|
if (result.triggered) {
|
|
102
106
|
this.store.markTriggered(watcher.id);
|
|
103
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
|
+
});
|
|
104
111
|
await this.executeAction(watcher, result.output);
|
|
105
112
|
}
|
|
106
113
|
else {
|
|
@@ -155,7 +162,7 @@ export class WatcherScheduler {
|
|
|
155
162
|
message = watcher.actionDetails.slice(colonIdx + 1).trim();
|
|
156
163
|
}
|
|
157
164
|
const fullMessage = `[Watcher "${watcher.name}" triggered] ${message}\n\nWatcher output:\n${output.slice(0, 500)}`;
|
|
158
|
-
|
|
165
|
+
PendingMessageStore.enqueueDelegation(tabName, fullMessage, db);
|
|
159
166
|
if (this.onNotify) {
|
|
160
167
|
await this.onNotify(`Watcher "${watcher.name}" triggered -- delegated to tab:${tabName}`);
|
|
161
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
|
-
}
|