beecork 1.4.11 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/capabilities/index.d.ts +1 -1
- package/dist/capabilities/index.js +1 -1
- package/dist/capabilities/manager.js +13 -9
- package/dist/capabilities/packs.js +3 -1
- package/dist/channels/admin.d.ts +10 -0
- package/dist/channels/admin.js +20 -0
- package/dist/channels/command-handler.d.ts +2 -10
- package/dist/channels/command-handler.js +90 -84
- package/dist/channels/discord.d.ts +4 -9
- package/dist/channels/discord.js +59 -42
- package/dist/channels/index.d.ts +1 -1
- package/dist/channels/loader.js +13 -4
- package/dist/channels/pipeline.js +14 -5
- package/dist/channels/registry.d.ts +17 -1
- package/dist/channels/registry.js +33 -4
- package/dist/channels/send-helpers.d.ts +19 -0
- package/dist/channels/send-helpers.js +21 -0
- package/dist/channels/telegram.d.ts +21 -14
- package/dist/channels/telegram.js +214 -104
- package/dist/channels/types.d.ts +13 -38
- package/dist/channels/voice-state.d.ts +29 -0
- package/dist/channels/voice-state.js +45 -0
- package/dist/channels/webhook.d.ts +2 -5
- package/dist/channels/webhook.js +88 -29
- package/dist/channels/whatsapp.d.ts +9 -7
- package/dist/channels/whatsapp.js +141 -100
- package/dist/cli/capabilities.js +4 -4
- package/dist/cli/channel.js +16 -6
- package/dist/cli/commands.js +12 -9
- package/dist/cli/doctor.js +85 -27
- package/dist/cli/handoff.d.ts +7 -14
- package/dist/cli/handoff.js +9 -44
- package/dist/cli/mcp.js +5 -5
- package/dist/cli/media.js +21 -8
- package/dist/cli/setup.js +9 -8
- package/dist/cli/store.js +29 -12
- package/dist/config.d.ts +5 -1
- package/dist/config.js +20 -22
- package/dist/daemon.js +113 -51
- package/dist/dashboard/html.js +100 -20
- package/dist/dashboard/routes.d.ts +17 -0
- package/dist/dashboard/routes.js +623 -0
- package/dist/dashboard/server.js +38 -489
- package/dist/db/connection.d.ts +29 -0
- package/dist/db/connection.js +37 -0
- package/dist/db/index.js +43 -11
- package/dist/db/migrations.js +114 -22
- package/dist/delegation/manager.js +10 -4
- package/dist/index.js +39 -59
- package/dist/knowledge/manager.js +26 -12
- package/dist/mcp/handlers.d.ts +37 -0
- package/dist/mcp/handlers.js +520 -0
- package/dist/mcp/server.js +44 -858
- package/dist/mcp/tool-definitions.d.ts +1225 -0
- package/dist/mcp/tool-definitions.js +412 -0
- package/dist/mcp/validate.d.ts +23 -0
- package/dist/mcp/validate.js +65 -0
- package/dist/media/factory.js +18 -14
- package/dist/media/generators/dall-e.js +2 -2
- package/dist/media/generators/kling.js +4 -4
- package/dist/media/generators/lyria.js +1 -1
- package/dist/media/generators/nano-banana.d.ts +1 -1
- package/dist/media/generators/nano-banana.js +2 -2
- package/dist/media/generators/poll-util.js +4 -4
- package/dist/media/generators/recraft.js +3 -3
- package/dist/media/generators/runway.js +4 -4
- package/dist/media/generators/stable-diffusion.js +2 -2
- package/dist/media/generators/veo.js +1 -1
- package/dist/media/index.d.ts +2 -7
- package/dist/media/index.js +2 -2
- package/dist/media/store.d.ts +7 -0
- package/dist/media/store.js +18 -4
- package/dist/media/types.d.ts +22 -0
- package/dist/notifications/index.d.ts +2 -4
- package/dist/notifications/index.js +6 -19
- package/dist/notifications/ntfy.js +3 -3
- package/dist/observability/analytics.d.ts +1 -1
- package/dist/observability/analytics.js +41 -16
- package/dist/projects/index.d.ts +3 -2
- package/dist/projects/index.js +2 -2
- package/dist/projects/manager.d.ts +1 -7
- package/dist/projects/manager.js +66 -42
- package/dist/projects/router.d.ts +12 -0
- package/dist/projects/router.js +98 -45
- package/dist/service/install.js +15 -5
- package/dist/service/windows.js +1 -1
- package/dist/session/budget-guard.d.ts +20 -0
- package/dist/session/budget-guard.js +31 -0
- package/dist/session/circuit-breaker.d.ts +5 -3
- package/dist/session/circuit-breaker.js +45 -20
- package/dist/session/context-compactor.d.ts +32 -0
- package/dist/session/context-compactor.js +45 -0
- package/dist/session/context-monitor.js +2 -2
- package/dist/session/handoff.d.ts +21 -0
- package/dist/session/handoff.js +50 -0
- package/dist/session/manager.d.ts +21 -5
- package/dist/session/manager.js +166 -153
- package/dist/session/memory-store.d.ts +29 -0
- package/dist/session/memory-store.js +45 -0
- package/dist/session/message-queue.d.ts +28 -0
- package/dist/session/message-queue.js +52 -0
- package/dist/session/pending-dispatcher.d.ts +31 -0
- package/dist/session/pending-dispatcher.js +120 -0
- package/dist/session/pending-store.d.ts +60 -0
- package/dist/session/pending-store.js +118 -0
- package/dist/session/stale-session.d.ts +31 -0
- package/dist/session/stale-session.js +45 -0
- package/dist/session/subprocess.d.ts +3 -0
- package/dist/session/subprocess.js +54 -11
- package/dist/session/tab-store.d.ts +28 -0
- package/dist/session/tab-store.js +78 -0
- package/dist/tasks/scheduler.d.ts +13 -0
- package/dist/tasks/scheduler.js +97 -18
- package/dist/tasks/store.js +26 -12
- package/dist/timeline/logger.js +3 -1
- package/dist/timeline/query.js +15 -5
- package/dist/types.d.ts +49 -9
- package/dist/util/auto-heal.js +15 -5
- package/dist/util/install-info.js +3 -1
- package/dist/util/logger.d.ts +1 -1
- package/dist/util/logger.js +63 -24
- package/dist/util/paths.d.ts +2 -0
- package/dist/util/paths.js +16 -3
- package/dist/util/rate-limiter.js +8 -0
- package/dist/util/retry.js +1 -1
- package/dist/util/text.d.ts +21 -1
- package/dist/util/text.js +38 -8
- package/dist/voice/index.js +5 -1
- package/dist/voice/stt.js +14 -6
- package/dist/voice/tts.js +1 -1
- package/dist/watchers/scheduler.js +11 -5
- package/package.json +6 -1
- package/dist/session/tool-classifier.d.ts +0 -4
- package/dist/session/tool-classifier.js +0 -56
- package/dist/users/index.d.ts +0 -2
- package/dist/users/index.js +0 -1
- package/dist/users/service.d.ts +0 -17
- package/dist/users/service.js +0 -46
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { getDb } from '../db/index.js';
|
|
2
|
+
function rowToTab(row) {
|
|
3
|
+
return {
|
|
4
|
+
id: row.id,
|
|
5
|
+
name: row.name,
|
|
6
|
+
sessionId: row.session_id,
|
|
7
|
+
status: row.status,
|
|
8
|
+
workingDir: row.working_dir,
|
|
9
|
+
createdAt: row.created_at,
|
|
10
|
+
lastActivityAt: row.last_activity_at,
|
|
11
|
+
pid: row.pid,
|
|
12
|
+
systemPrompt: row.system_prompt,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* SQL helpers for the `tabs` table. Single place for any code that needs to
|
|
17
|
+
* read/write tab rows. TabManager owns subprocess lifecycle; TabStore owns
|
|
18
|
+
* the schema knowledge.
|
|
19
|
+
*
|
|
20
|
+
* All methods accept an optional `db` param so they're testable against an
|
|
21
|
+
* in-memory database. The no-arg form uses the singleton.
|
|
22
|
+
*/
|
|
23
|
+
export const TabStore = {
|
|
24
|
+
listAll(db = getDb()) {
|
|
25
|
+
return db.prepare('SELECT * FROM tabs ORDER BY last_activity_at DESC').all().map(rowToTab);
|
|
26
|
+
},
|
|
27
|
+
findByName(name, db = getDb()) {
|
|
28
|
+
const row = db.prepare('SELECT * FROM tabs WHERE name = ?').get(name);
|
|
29
|
+
return row ? rowToTab(row) : undefined;
|
|
30
|
+
},
|
|
31
|
+
getIdByName(name, db = getDb()) {
|
|
32
|
+
const row = db.prepare('SELECT id FROM tabs WHERE name = ?').get(name);
|
|
33
|
+
return row?.id;
|
|
34
|
+
},
|
|
35
|
+
countAll(db = getDb()) {
|
|
36
|
+
return db.prepare('SELECT COUNT(*) as c FROM tabs').get().c;
|
|
37
|
+
},
|
|
38
|
+
countRunning(db = getDb()) {
|
|
39
|
+
return db.prepare("SELECT COUNT(*) as c FROM tabs WHERE status = 'running'").get().c;
|
|
40
|
+
},
|
|
41
|
+
/** All tabs marked 'running' — used by daemon crash-recovery on startup. */
|
|
42
|
+
findRunning(db = getDb()) {
|
|
43
|
+
return db.prepare("SELECT * FROM tabs WHERE status = 'running'").all().map(rowToTab);
|
|
44
|
+
},
|
|
45
|
+
/** Most-recently-active tab — used by MCP to surface "current" context. */
|
|
46
|
+
mostRecent(db = getDb()) {
|
|
47
|
+
const row = db.prepare('SELECT * FROM tabs ORDER BY last_activity_at DESC LIMIT 1').get();
|
|
48
|
+
return row ? rowToTab(row) : undefined;
|
|
49
|
+
},
|
|
50
|
+
setStatus(name, status, db = getDb()) {
|
|
51
|
+
db.prepare('UPDATE tabs SET status = ?, last_activity_at = ?, pid = NULL WHERE name = ?').run(status, new Date().toISOString(), name);
|
|
52
|
+
},
|
|
53
|
+
setIdleById(id, db = getDb()) {
|
|
54
|
+
db.prepare("UPDATE tabs SET status = 'idle', pid = NULL WHERE id = ?").run(id);
|
|
55
|
+
},
|
|
56
|
+
/** Used by MCP close-tab to nudge daemon recovery loop. */
|
|
57
|
+
markRunningAsStopped(name, db = getDb()) {
|
|
58
|
+
db.prepare("UPDATE tabs SET status = 'stopped', pid = NULL WHERE name = ? AND status = 'running'").run(name);
|
|
59
|
+
},
|
|
60
|
+
setSystemPrompt(name, systemPrompt, db = getDb()) {
|
|
61
|
+
const result = db
|
|
62
|
+
.prepare('UPDATE tabs SET system_prompt = ? WHERE name = ?')
|
|
63
|
+
.run(systemPrompt, name);
|
|
64
|
+
return result.changes > 0;
|
|
65
|
+
},
|
|
66
|
+
/** Delete a tab and all of its messages atomically. Returns true if it existed. */
|
|
67
|
+
deleteWithMessages(name, db = getDb()) {
|
|
68
|
+
const id = this.getIdByName(name, db);
|
|
69
|
+
if (!id)
|
|
70
|
+
return false;
|
|
71
|
+
const tx = db.transaction(() => {
|
|
72
|
+
db.prepare('DELETE FROM messages WHERE tab_id = ?').run(id);
|
|
73
|
+
db.prepare('DELETE FROM tabs WHERE id = ?').run(id);
|
|
74
|
+
});
|
|
75
|
+
tx();
|
|
76
|
+
return true;
|
|
77
|
+
},
|
|
78
|
+
};
|
|
@@ -6,6 +6,7 @@ export declare class TaskScheduler {
|
|
|
6
6
|
private onNotify;
|
|
7
7
|
private nextRunAt;
|
|
8
8
|
private running;
|
|
9
|
+
private failureCounts;
|
|
9
10
|
private stopping;
|
|
10
11
|
private store;
|
|
11
12
|
constructor(tabManager: TabManager, onNotify: NotifyCallback | null);
|
|
@@ -24,9 +25,21 @@ export declare class TaskScheduler {
|
|
|
24
25
|
/** Compute next run time in ms epoch, given a "from" anchor. Returns null if invalid. */
|
|
25
26
|
private computeNextRun;
|
|
26
27
|
private fireJob;
|
|
28
|
+
/**
|
|
29
|
+
* Track consecutive failures per task. After MAX_CONSECUTIVE_FAILURES the
|
|
30
|
+
* task is auto-disabled in the DB and a single "auto-disabled" notification
|
|
31
|
+
* is sent. Counter resets on the next successful fire.
|
|
32
|
+
*/
|
|
33
|
+
private recordFailure;
|
|
27
34
|
private handleSystemEvent;
|
|
28
35
|
}
|
|
29
36
|
/** Convert human interval (30m, 2h, 1d, 1h30m, 2w) to milliseconds */
|
|
30
37
|
export declare function intervalToMs(interval: string): number | null;
|
|
31
38
|
/** Convert human interval (30m, 2h, 1d, 1h30m, 2w) to cron expression */
|
|
32
39
|
export declare function intervalToCron(interval: string): string | null;
|
|
40
|
+
/**
|
|
41
|
+
* Validate a schedule string for a given scheduleType. Returns an error string
|
|
42
|
+
* on invalid input or null when valid. Used by MCP + dashboard before insert
|
|
43
|
+
* so misconfigured tasks fail loud instead of silently never firing.
|
|
44
|
+
*/
|
|
45
|
+
export declare function validateSchedule(scheduleType: string, schedule: string): string | null;
|
package/dist/tasks/scheduler.js
CHANGED
|
@@ -7,6 +7,12 @@ import { TaskStore } from './store.js';
|
|
|
7
7
|
import { getCronReloadSignalPath, getLogsDir } from '../util/paths.js';
|
|
8
8
|
import { logger } from '../util/logger.js';
|
|
9
9
|
export const execAsync = promisify(exec);
|
|
10
|
+
/**
|
|
11
|
+
* Disable a task after this many consecutive failures so a misconfigured
|
|
12
|
+
* cron doesn't fire (and notify) every interval indefinitely. Reset on
|
|
13
|
+
* success.
|
|
14
|
+
*/
|
|
15
|
+
const MAX_CONSECUTIVE_FAILURES = 5;
|
|
10
16
|
export class TaskScheduler {
|
|
11
17
|
tabManager;
|
|
12
18
|
onNotify;
|
|
@@ -14,6 +20,8 @@ export class TaskScheduler {
|
|
|
14
20
|
nextRunAt = new Map();
|
|
15
21
|
// taskIds with an in-flight fireJob
|
|
16
22
|
running = new Set();
|
|
23
|
+
// taskId -> consecutive failure count, used for auto-disable
|
|
24
|
+
failureCounts = new Map();
|
|
17
25
|
stopping = false;
|
|
18
26
|
store = new TaskStore();
|
|
19
27
|
constructor(tabManager, onNotify) {
|
|
@@ -69,7 +77,9 @@ export class TaskScheduler {
|
|
|
69
77
|
try {
|
|
70
78
|
fs.unlinkSync(signalPath);
|
|
71
79
|
}
|
|
72
|
-
catch {
|
|
80
|
+
catch {
|
|
81
|
+
/* race condition, ok */
|
|
82
|
+
}
|
|
73
83
|
logger.info('Tasks: reload signal detected, reloading schedules');
|
|
74
84
|
this.loadAndSchedule();
|
|
75
85
|
}
|
|
@@ -124,11 +134,15 @@ export class TaskScheduler {
|
|
|
124
134
|
try {
|
|
125
135
|
switch (job.scheduleType) {
|
|
126
136
|
case 'cron':
|
|
127
|
-
return CronExpressionParser.parse(job.schedule, { currentDate: new Date(fromMs) })
|
|
137
|
+
return CronExpressionParser.parse(job.schedule, { currentDate: new Date(fromMs) })
|
|
138
|
+
.next()
|
|
139
|
+
.getTime();
|
|
128
140
|
case 'every': {
|
|
129
141
|
const expr = intervalToCron(job.schedule);
|
|
130
142
|
if (expr) {
|
|
131
|
-
return CronExpressionParser.parse(expr, { currentDate: new Date(fromMs) })
|
|
143
|
+
return CronExpressionParser.parse(expr, { currentDate: new Date(fromMs) })
|
|
144
|
+
.next()
|
|
145
|
+
.getTime();
|
|
132
146
|
}
|
|
133
147
|
const ms = intervalToMs(job.schedule);
|
|
134
148
|
return ms ? fromMs + ms : null;
|
|
@@ -169,6 +183,12 @@ export class TaskScheduler {
|
|
|
169
183
|
const status = result.error ? 'ERROR' : 'SUCCESS';
|
|
170
184
|
// Log result (status reflects subprocess exit / is_error, not just completion)
|
|
171
185
|
await fs.promises.appendFile(logFile, `[${new Date().toISOString()}] ${status}: ${firstLine}\n`);
|
|
186
|
+
if (result.error) {
|
|
187
|
+
this.recordFailure(job);
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
this.failureCounts.delete(job.id);
|
|
191
|
+
}
|
|
172
192
|
// Notify (separate try/catch -- notification failure shouldn't be reported as job failure)
|
|
173
193
|
try {
|
|
174
194
|
if (this.onNotify) {
|
|
@@ -188,10 +208,30 @@ export class TaskScheduler {
|
|
|
188
208
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
189
209
|
logger.error(`Task "${job.name}" failed:`, err);
|
|
190
210
|
await fs.promises.appendFile(logFile, `[${new Date().toISOString()}] ERROR: ${errMsg}\n`);
|
|
211
|
+
this.recordFailure(job);
|
|
191
212
|
try {
|
|
192
213
|
await this.onNotify?.(`[${job.name}] Failed -- ${errMsg}`);
|
|
193
214
|
}
|
|
194
|
-
catch {
|
|
215
|
+
catch {
|
|
216
|
+
/* notification best-effort */
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Track consecutive failures per task. After MAX_CONSECUTIVE_FAILURES the
|
|
222
|
+
* task is auto-disabled in the DB and a single "auto-disabled" notification
|
|
223
|
+
* is sent. Counter resets on the next successful fire.
|
|
224
|
+
*/
|
|
225
|
+
recordFailure(job) {
|
|
226
|
+
const count = (this.failureCounts.get(job.id) ?? 0) + 1;
|
|
227
|
+
this.failureCounts.set(job.id, count);
|
|
228
|
+
if (count >= MAX_CONSECUTIVE_FAILURES) {
|
|
229
|
+
logger.warn(`Task "${job.name}" hit ${count} consecutive failures — auto-disabling`);
|
|
230
|
+
this.store.update(job.id, { enabled: false });
|
|
231
|
+
this.nextRunAt.delete(job.id);
|
|
232
|
+
this.failureCounts.delete(job.id);
|
|
233
|
+
// Notify once. Subsequent enable + re-failure will trigger again.
|
|
234
|
+
this.onNotify?.(`Task "${job.name}" auto-disabled after ${count} consecutive failures. Re-enable from the dashboard or via beecork_task_update.`).catch(() => { });
|
|
195
235
|
}
|
|
196
236
|
}
|
|
197
237
|
async handleSystemEvent(job) {
|
|
@@ -207,28 +247,33 @@ export class TaskScheduler {
|
|
|
207
247
|
}
|
|
208
248
|
}
|
|
209
249
|
}
|
|
250
|
+
/** Parse a "1w2d3h45m"-style interval into its parts. Returns null on invalid input. */
|
|
251
|
+
function parseInterval(interval) {
|
|
252
|
+
const match = interval.match(/^(?:(\d+)w)?(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?$/);
|
|
253
|
+
if (!match || match.slice(1).every((g) => g === undefined))
|
|
254
|
+
return null;
|
|
255
|
+
return {
|
|
256
|
+
weeks: parseInt(match[1] || '0', 10),
|
|
257
|
+
days: parseInt(match[2] || '0', 10),
|
|
258
|
+
hours: parseInt(match[3] || '0', 10),
|
|
259
|
+
mins: parseInt(match[4] || '0', 10),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
210
262
|
/** Convert human interval (30m, 2h, 1d, 1h30m, 2w) to milliseconds */
|
|
211
263
|
export function intervalToMs(interval) {
|
|
212
|
-
const
|
|
213
|
-
if (!
|
|
264
|
+
const parts = parseInterval(interval);
|
|
265
|
+
if (!parts)
|
|
214
266
|
return null;
|
|
215
|
-
const weeks
|
|
216
|
-
const
|
|
217
|
-
const hours = parseInt(match[3] || '0', 10);
|
|
218
|
-
const mins = parseInt(match[4] || '0', 10);
|
|
219
|
-
const totalMs = ((weeks * 7 * 24 * 60) + (days * 24 * 60) + (hours * 60) + mins) * 60 * 1000;
|
|
267
|
+
const { weeks, days, hours, mins } = parts;
|
|
268
|
+
const totalMs = (weeks * 7 * 24 * 60 + days * 24 * 60 + hours * 60 + mins) * 60 * 1000;
|
|
220
269
|
return totalMs > 0 ? totalMs : null;
|
|
221
270
|
}
|
|
222
271
|
/** Convert human interval (30m, 2h, 1d, 1h30m, 2w) to cron expression */
|
|
223
272
|
export function intervalToCron(interval) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (!match || match.slice(1).every(g => g === undefined))
|
|
273
|
+
const parts = parseInterval(interval);
|
|
274
|
+
if (!parts)
|
|
227
275
|
return null;
|
|
228
|
-
const weeks
|
|
229
|
-
const days = parseInt(match[2] || '0', 10);
|
|
230
|
-
const hours = parseInt(match[3] || '0', 10);
|
|
231
|
-
const mins = parseInt(match[4] || '0', 10);
|
|
276
|
+
const { weeks, days, hours, mins } = parts;
|
|
232
277
|
// Convert to total minutes for simple intervals
|
|
233
278
|
const totalMins = weeks * 7 * 24 * 60 + days * 24 * 60 + hours * 60 + mins;
|
|
234
279
|
if (totalMins <= 0)
|
|
@@ -248,3 +293,37 @@ export function intervalToCron(interval) {
|
|
|
248
293
|
// Combined or large intervals -- return null, handled by intervalToMs fallback
|
|
249
294
|
return null;
|
|
250
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* Validate a schedule string for a given scheduleType. Returns an error string
|
|
298
|
+
* on invalid input or null when valid. Used by MCP + dashboard before insert
|
|
299
|
+
* so misconfigured tasks fail loud instead of silently never firing.
|
|
300
|
+
*/
|
|
301
|
+
export function validateSchedule(scheduleType, schedule) {
|
|
302
|
+
if (!schedule)
|
|
303
|
+
return 'schedule is required';
|
|
304
|
+
switch (scheduleType) {
|
|
305
|
+
case 'at': {
|
|
306
|
+
const ts = Date.parse(schedule);
|
|
307
|
+
if (Number.isNaN(ts))
|
|
308
|
+
return `"${schedule}" is not a valid ISO datetime`;
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
case 'every': {
|
|
312
|
+
if (intervalToMs(schedule) === null) {
|
|
313
|
+
return `"${schedule}" is not a valid interval. Use formats like "30m", "2h", "1d", "1h30m".`;
|
|
314
|
+
}
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
case 'cron': {
|
|
318
|
+
try {
|
|
319
|
+
CronExpressionParser.parse(schedule);
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
catch (err) {
|
|
323
|
+
return `"${schedule}" is not a valid cron expression: ${err instanceof Error ? err.message : String(err)}`;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
default:
|
|
327
|
+
return `unknown scheduleType "${scheduleType}"`;
|
|
328
|
+
}
|
|
329
|
+
}
|
package/dist/tasks/store.js
CHANGED
|
@@ -4,21 +4,33 @@ 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();
|
|
21
|
-
return db.prepare('SELECT * FROM tasks
|
|
33
|
+
return db.prepare('SELECT * FROM tasks ORDER BY created_at').all().map(rowToTask);
|
|
22
34
|
}
|
|
23
35
|
get(id) {
|
|
24
36
|
const db = getDb();
|
|
@@ -27,8 +39,8 @@ export class TaskStore {
|
|
|
27
39
|
}
|
|
28
40
|
add(job) {
|
|
29
41
|
const db = getDb();
|
|
30
|
-
db.prepare(`INSERT INTO tasks (id, name, schedule_type, schedule, tab_name, message, payload_type, enabled,
|
|
31
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
42
|
+
db.prepare(`INSERT INTO tasks (id, name, schedule_type, schedule, tab_name, message, payload_type, enabled, created_at, last_run_at, next_run_at)
|
|
43
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(job.id, job.name, job.scheduleType, job.schedule, job.tabName, job.message, job.payloadType || 'agentTurn', job.enabled ? 1 : 0, job.createdAt, job.lastRunAt, job.nextRunAt);
|
|
32
44
|
}
|
|
33
45
|
update(id, updates) {
|
|
34
46
|
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 {
|
|
@@ -71,11 +85,11 @@ export class TaskStore {
|
|
|
71
85
|
const jobs = data.jobs || [];
|
|
72
86
|
if (jobs.length === 0)
|
|
73
87
|
return;
|
|
74
|
-
const insert = db.prepare(`INSERT OR IGNORE INTO tasks (id, name, schedule_type, schedule, tab_name, message, enabled,
|
|
75
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
88
|
+
const insert = db.prepare(`INSERT OR IGNORE INTO tasks (id, name, schedule_type, schedule, tab_name, message, enabled, created_at, last_run_at, next_run_at)
|
|
89
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
76
90
|
const tx = db.transaction(() => {
|
|
77
91
|
for (const j of jobs) {
|
|
78
|
-
insert.run(j.id, j.name, j.scheduleType, j.schedule, j.tabName || 'default', j.message, j.enabled ? 1 : 0,
|
|
92
|
+
insert.run(j.id, j.name, j.scheduleType, j.schedule, j.tabName || 'default', j.message, j.enabled ? 1 : 0, j.createdAt, j.lastRunAt, j.nextRunAt);
|
|
79
93
|
}
|
|
80
94
|
});
|
|
81
95
|
tx();
|
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) {
|
|
@@ -12,8 +18,12 @@ export function getTimeline(options) {
|
|
|
12
18
|
const conditions = [];
|
|
13
19
|
const params = [];
|
|
14
20
|
if (options?.date) {
|
|
15
|
-
|
|
16
|
-
|
|
21
|
+
// Sargable range so idx_activity_log_created can serve the query.
|
|
22
|
+
// `date(created_at) = ?` is non-sargable and forces a full scan.
|
|
23
|
+
const dayStart = `${options.date} 00:00:00`;
|
|
24
|
+
const dayEnd = `${options.date} 23:59:59.999`;
|
|
25
|
+
conditions.push('created_at >= ? AND created_at <= ?');
|
|
26
|
+
params.push(dayStart, dayEnd);
|
|
17
27
|
}
|
|
18
28
|
if (options?.tabName) {
|
|
19
29
|
conditions.push('tab_name = ?');
|
package/dist/types.d.ts
CHANGED
|
@@ -8,10 +8,11 @@ export interface ClaudeCodeConfig {
|
|
|
8
8
|
defaultFlags: string[];
|
|
9
9
|
maxBudgetUsd?: number;
|
|
10
10
|
computerUse?: boolean;
|
|
11
|
+
/** Hard timeout per subprocess turn. Default 30 min. Set to 0 to disable. */
|
|
12
|
+
maxRuntimeMs?: number;
|
|
11
13
|
}
|
|
12
14
|
export interface MemoryConfig {
|
|
13
15
|
dbPath: string;
|
|
14
|
-
maxLongTermEntries: number;
|
|
15
16
|
}
|
|
16
17
|
export interface TabConfig {
|
|
17
18
|
workingDir: string;
|
|
@@ -25,6 +26,8 @@ export interface WhatsAppConfig {
|
|
|
25
26
|
mode: 'baileys';
|
|
26
27
|
sessionPath: string;
|
|
27
28
|
allowedNumbers: string[];
|
|
29
|
+
/** Optional admin phone number. Defaults to allowedNumbers[0]. */
|
|
30
|
+
adminNumber?: string;
|
|
28
31
|
}
|
|
29
32
|
export interface VoiceConfig {
|
|
30
33
|
sttProvider: 'whisper-api' | 'none';
|
|
@@ -37,12 +40,29 @@ export interface VoiceConfig {
|
|
|
37
40
|
export interface DiscordConfig {
|
|
38
41
|
token: string;
|
|
39
42
|
allowedUserIds?: string[];
|
|
40
|
-
|
|
43
|
+
/** Optional admin user ID. Defaults to allowedUserIds[0]. */
|
|
44
|
+
adminUserId?: string;
|
|
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
|
+
*/
|
|
41
59
|
export interface WebhookConfig {
|
|
42
60
|
enabled: boolean;
|
|
43
61
|
port: number;
|
|
44
62
|
authToken?: string;
|
|
45
63
|
hmacSecret?: string;
|
|
64
|
+
/** Set to true to skip the fail-secure auth check at start. NOT recommended on shared hosts. */
|
|
65
|
+
allowUnauthLocalhost?: boolean;
|
|
46
66
|
}
|
|
47
67
|
export interface GroupConfig {
|
|
48
68
|
activationMode: 'mention' | 'reply' | 'keyword' | 'always';
|
|
@@ -50,21 +70,39 @@ export interface GroupConfig {
|
|
|
50
70
|
tabPerGroup: boolean;
|
|
51
71
|
keywords?: string[];
|
|
52
72
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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;
|
|
58
86
|
server?: string;
|
|
59
|
-
|
|
87
|
+
} | {
|
|
88
|
+
type: 'webhook';
|
|
89
|
+
url: string;
|
|
60
90
|
headers?: Record<string, string>;
|
|
61
|
-
}
|
|
91
|
+
};
|
|
62
92
|
export interface MediaGeneratorConfig {
|
|
63
93
|
provider: string;
|
|
64
94
|
apiKey?: string;
|
|
65
95
|
model?: string;
|
|
66
96
|
style?: string;
|
|
67
97
|
}
|
|
98
|
+
/** A media file attached to a message. Shared by channels + util/text + media. */
|
|
99
|
+
export interface MediaAttachment {
|
|
100
|
+
type: 'image' | 'audio' | 'video' | 'document' | 'voice';
|
|
101
|
+
mimeType: string;
|
|
102
|
+
filePath: string;
|
|
103
|
+
fileName?: string;
|
|
104
|
+
caption?: string;
|
|
105
|
+
}
|
|
68
106
|
export interface BeecorkConfig {
|
|
69
107
|
telegram: TelegramConfig;
|
|
70
108
|
whatsapp?: WhatsAppConfig;
|
|
@@ -81,6 +119,8 @@ export interface BeecorkConfig {
|
|
|
81
119
|
notifications?: NotificationConfig[];
|
|
82
120
|
mediaGenerators?: MediaGeneratorConfig[];
|
|
83
121
|
communityChannels?: string[];
|
|
122
|
+
/** Capability packs enabled via `beecork capabilities enable`. Owned by src/capabilities. */
|
|
123
|
+
capabilities?: import('./capabilities/types.js').EnabledCapability[];
|
|
84
124
|
deployment: 'local' | 'vps';
|
|
85
125
|
}
|
|
86
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
|
}
|