beecork 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/capabilities/index.d.ts +1 -1
- package/dist/capabilities/index.js +1 -1
- package/dist/capabilities/manager.js +13 -9
- package/dist/capabilities/packs.js +3 -1
- package/dist/channels/command-handler.js +46 -14
- package/dist/channels/discord.d.ts +3 -6
- package/dist/channels/discord.js +40 -23
- package/dist/channels/index.d.ts +1 -1
- package/dist/channels/loader.js +13 -3
- package/dist/channels/pipeline.js +14 -5
- package/dist/channels/registry.d.ts +17 -1
- package/dist/channels/registry.js +33 -4
- package/dist/channels/telegram.d.ts +20 -5
- package/dist/channels/telegram.js +177 -42
- package/dist/channels/types.d.ts +11 -28
- package/dist/channels/voice-state.js +3 -1
- package/dist/channels/webhook.d.ts +1 -4
- package/dist/channels/webhook.js +26 -11
- package/dist/channels/whatsapp.d.ts +8 -4
- package/dist/channels/whatsapp.js +65 -29
- package/dist/cli/capabilities.js +4 -4
- package/dist/cli/channel.js +16 -6
- package/dist/cli/commands.js +12 -9
- package/dist/cli/doctor.js +80 -25
- package/dist/cli/handoff.d.ts +7 -14
- package/dist/cli/handoff.js +9 -44
- package/dist/cli/mcp.js +5 -5
- package/dist/cli/media.js +21 -8
- package/dist/cli/setup.js +9 -8
- package/dist/cli/store.js +29 -12
- package/dist/config.js +5 -10
- package/dist/daemon.js +88 -38
- package/dist/dashboard/html.js +80 -12
- package/dist/dashboard/routes.js +143 -79
- package/dist/dashboard/server.js +5 -1
- package/dist/db/connection.d.ts +29 -0
- package/dist/db/connection.js +37 -0
- package/dist/db/index.js +30 -12
- package/dist/db/migrations.js +84 -28
- package/dist/delegation/manager.js +10 -4
- package/dist/index.js +39 -59
- package/dist/knowledge/manager.js +26 -12
- package/dist/mcp/handlers.js +126 -57
- package/dist/mcp/server.js +20 -10
- package/dist/mcp/tool-definitions.js +68 -20
- package/dist/mcp/validate.d.ts +23 -0
- package/dist/mcp/validate.js +65 -0
- package/dist/media/factory.js +18 -14
- package/dist/media/generators/dall-e.js +2 -2
- package/dist/media/generators/kling.js +4 -4
- package/dist/media/generators/lyria.js +1 -1
- package/dist/media/generators/nano-banana.d.ts +1 -1
- package/dist/media/generators/nano-banana.js +2 -2
- package/dist/media/generators/poll-util.js +4 -4
- package/dist/media/generators/recraft.js +3 -3
- package/dist/media/generators/runway.js +4 -4
- package/dist/media/generators/stable-diffusion.js +2 -2
- package/dist/media/generators/veo.js +1 -1
- package/dist/media/index.js +1 -1
- package/dist/media/store.d.ts +7 -0
- package/dist/media/store.js +18 -4
- package/dist/media/types.d.ts +22 -0
- package/dist/notifications/index.d.ts +2 -4
- package/dist/notifications/index.js +6 -19
- package/dist/notifications/ntfy.js +3 -3
- package/dist/observability/analytics.js +35 -13
- package/dist/projects/index.d.ts +1 -1
- package/dist/projects/index.js +1 -1
- package/dist/projects/manager.d.ts +0 -4
- package/dist/projects/manager.js +51 -28
- package/dist/projects/router.d.ts +2 -0
- package/dist/projects/router.js +70 -45
- package/dist/service/install.js +15 -5
- package/dist/service/windows.js +1 -1
- package/dist/session/budget-guard.d.ts +20 -0
- package/dist/session/budget-guard.js +31 -0
- package/dist/session/circuit-breaker.d.ts +5 -3
- package/dist/session/circuit-breaker.js +45 -20
- package/dist/session/context-compactor.d.ts +32 -0
- package/dist/session/context-compactor.js +45 -0
- package/dist/session/context-monitor.js +2 -2
- package/dist/session/handoff.d.ts +21 -0
- package/dist/session/handoff.js +50 -0
- package/dist/session/manager.d.ts +17 -5
- package/dist/session/manager.js +153 -146
- package/dist/session/memory-store.d.ts +29 -0
- package/dist/session/memory-store.js +45 -0
- package/dist/session/message-queue.d.ts +28 -0
- package/dist/session/message-queue.js +52 -0
- package/dist/session/pending-dispatcher.d.ts +31 -0
- package/dist/session/pending-dispatcher.js +120 -0
- package/dist/session/pending-store.d.ts +60 -0
- package/dist/session/pending-store.js +118 -0
- package/dist/session/stale-session.d.ts +31 -0
- package/dist/session/stale-session.js +45 -0
- package/dist/session/subprocess.d.ts +2 -0
- package/dist/session/subprocess.js +33 -11
- package/dist/session/tab-store.js +4 -3
- package/dist/tasks/scheduler.d.ts +7 -0
- package/dist/tasks/scheduler.js +46 -6
- package/dist/tasks/store.js +20 -6
- package/dist/timeline/logger.js +3 -1
- package/dist/timeline/query.js +9 -3
- package/dist/types.d.ts +34 -9
- package/dist/util/auto-heal.js +15 -5
- package/dist/util/install-info.js +3 -1
- package/dist/util/logger.d.ts +1 -1
- package/dist/util/logger.js +63 -24
- package/dist/util/paths.d.ts +1 -0
- package/dist/util/paths.js +12 -2
- package/dist/util/retry.js +1 -1
- package/dist/util/text.js +13 -7
- package/dist/voice/index.js +5 -1
- package/dist/voice/stt.js +14 -6
- package/dist/voice/tts.js +1 -1
- package/dist/watchers/scheduler.js +9 -2
- package/package.json +18 -13
- package/dist/session/tool-classifier.d.ts +0 -4
- package/dist/session/tool-classifier.js +0 -56
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal MCP tool-input validator. Walks the declared `inputSchema` from
|
|
3
|
+
* `tool-definitions.ts` and rejects calls that violate `required`, `type`, or
|
|
4
|
+
* `enum`. Handlers previously had ad-hoc checks for some fields and silently
|
|
5
|
+
* accepted invalid inputs for others — this single validator converts the
|
|
6
|
+
* inconsistency into a uniform "Invalid input: ..." failure.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately handwritten (no Ajv/zod dep) to avoid pulling a runtime dep
|
|
9
|
+
* for one validation pass.
|
|
10
|
+
*/
|
|
11
|
+
interface PropertySchema {
|
|
12
|
+
type?: string | string[];
|
|
13
|
+
enum?: readonly unknown[];
|
|
14
|
+
items?: PropertySchema;
|
|
15
|
+
description?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface InputSchema {
|
|
18
|
+
type?: string;
|
|
19
|
+
properties?: Record<string, PropertySchema>;
|
|
20
|
+
required?: readonly string[];
|
|
21
|
+
}
|
|
22
|
+
export declare function validateToolArgs(schema: unknown, args: Record<string, unknown> | undefined): string | null;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal MCP tool-input validator. Walks the declared `inputSchema` from
|
|
3
|
+
* `tool-definitions.ts` and rejects calls that violate `required`, `type`, or
|
|
4
|
+
* `enum`. Handlers previously had ad-hoc checks for some fields and silently
|
|
5
|
+
* accepted invalid inputs for others — this single validator converts the
|
|
6
|
+
* inconsistency into a uniform "Invalid input: ..." failure.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately handwritten (no Ajv/zod dep) to avoid pulling a runtime dep
|
|
9
|
+
* for one validation pass.
|
|
10
|
+
*/
|
|
11
|
+
export function validateToolArgs(schema, args) {
|
|
12
|
+
if (!schema || typeof schema !== 'object')
|
|
13
|
+
return null;
|
|
14
|
+
const s = schema;
|
|
15
|
+
const a = args || {};
|
|
16
|
+
// Required fields
|
|
17
|
+
for (const field of s.required ?? []) {
|
|
18
|
+
const value = a[field];
|
|
19
|
+
if (value === undefined || value === null || value === '') {
|
|
20
|
+
return `Invalid input: "${field}" is required`;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Per-property type/enum checks
|
|
24
|
+
for (const [field, propSchema] of Object.entries(s.properties ?? {})) {
|
|
25
|
+
const value = a[field];
|
|
26
|
+
if (value === undefined || value === null)
|
|
27
|
+
continue;
|
|
28
|
+
const typeError = checkType(value, propSchema, field);
|
|
29
|
+
if (typeError)
|
|
30
|
+
return typeError;
|
|
31
|
+
if (propSchema.enum && !propSchema.enum.includes(value)) {
|
|
32
|
+
return `Invalid input: "${field}" must be one of ${JSON.stringify(propSchema.enum)} (got ${JSON.stringify(value)})`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
function checkType(value, schema, field) {
|
|
38
|
+
if (!schema.type)
|
|
39
|
+
return null;
|
|
40
|
+
const types = (Array.isArray(schema.type) ? schema.type : [schema.type]);
|
|
41
|
+
for (const t of types) {
|
|
42
|
+
if (matchesType(value, t))
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return `Invalid input: "${field}" must be ${types.join(' | ')} (got ${typeof value})`;
|
|
46
|
+
}
|
|
47
|
+
function matchesType(value, t) {
|
|
48
|
+
switch (t) {
|
|
49
|
+
case 'string':
|
|
50
|
+
return typeof value === 'string';
|
|
51
|
+
case 'number':
|
|
52
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
53
|
+
case 'integer':
|
|
54
|
+
return typeof value === 'number' && Number.isInteger(value);
|
|
55
|
+
case 'boolean':
|
|
56
|
+
return typeof value === 'boolean';
|
|
57
|
+
case 'object':
|
|
58
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
59
|
+
case 'array':
|
|
60
|
+
return Array.isArray(value);
|
|
61
|
+
default:
|
|
62
|
+
// Unknown types fall through to accept (lenient).
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
}
|
package/dist/media/factory.js
CHANGED
|
@@ -9,24 +9,28 @@ import { NanoBananaGenerator } from './generators/nano-banana.js';
|
|
|
9
9
|
import { ElevenLabsMusicGenerator } from './generators/elevenlabs-music.js';
|
|
10
10
|
import { LyriaGenerator } from './generators/lyria.js';
|
|
11
11
|
import { RecraftGenerator } from './generators/recraft.js';
|
|
12
|
+
// Lookup table — adding a new generator becomes one entry instead of one switch case.
|
|
13
|
+
const GENERATOR_BUILDERS = {
|
|
14
|
+
'dall-e': (k, m) => new DalleGenerator(k, m),
|
|
15
|
+
'stable-diffusion': (k) => new StableDiffusionGenerator(k),
|
|
16
|
+
runway: (k) => new RunwayGenerator(k),
|
|
17
|
+
veo: (k) => new VeoGenerator(k),
|
|
18
|
+
kling: (k) => new KlingGenerator(k),
|
|
19
|
+
'elevenlabs-sfx': (k) => new ElevenLabsSfxGenerator(k),
|
|
20
|
+
'nano-banana': (k, m) => new NanoBananaGenerator(k, m),
|
|
21
|
+
'elevenlabs-music': (k) => new ElevenLabsMusicGenerator(k),
|
|
22
|
+
lyria: (k, m) => new LyriaGenerator(k, m),
|
|
23
|
+
recraft: (k) => new RecraftGenerator(k),
|
|
24
|
+
};
|
|
12
25
|
export function createMediaGenerator(config) {
|
|
13
26
|
if (!config.apiKey) {
|
|
14
27
|
logger.warn(`Media generator ${config.provider}: missing API key`);
|
|
15
28
|
return null;
|
|
16
29
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
case 'veo': return new VeoGenerator(config.apiKey);
|
|
22
|
-
case 'kling': return new KlingGenerator(config.apiKey);
|
|
23
|
-
case 'elevenlabs-sfx': return new ElevenLabsSfxGenerator(config.apiKey);
|
|
24
|
-
case 'nano-banana': return new NanoBananaGenerator(config.apiKey, config.model);
|
|
25
|
-
case 'elevenlabs-music': return new ElevenLabsMusicGenerator(config.apiKey);
|
|
26
|
-
case 'lyria': return new LyriaGenerator(config.apiKey, config.model);
|
|
27
|
-
case 'recraft': return new RecraftGenerator(config.apiKey);
|
|
28
|
-
default:
|
|
29
|
-
logger.warn(`Unknown media generator: ${config.provider}`);
|
|
30
|
-
return null;
|
|
30
|
+
const build = GENERATOR_BUILDERS[config.provider];
|
|
31
|
+
if (!build) {
|
|
32
|
+
logger.warn(`Unknown media generator: ${config.provider}`);
|
|
33
|
+
return null;
|
|
31
34
|
}
|
|
35
|
+
return build(config.apiKey, config.model);
|
|
32
36
|
}
|
|
@@ -16,7 +16,7 @@ export class DalleGenerator {
|
|
|
16
16
|
const response = await fetch('https://api.openai.com/v1/images/generations', {
|
|
17
17
|
method: 'POST',
|
|
18
18
|
headers: {
|
|
19
|
-
|
|
19
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
20
20
|
'Content-Type': 'application/json',
|
|
21
21
|
},
|
|
22
22
|
body: JSON.stringify({
|
|
@@ -33,7 +33,7 @@ export class DalleGenerator {
|
|
|
33
33
|
const err = await response.text();
|
|
34
34
|
throw new Error(`DALL-E error ${response.status}: ${err.slice(0, 200)}`);
|
|
35
35
|
}
|
|
36
|
-
const data = await response.json();
|
|
36
|
+
const data = (await response.json());
|
|
37
37
|
const buffer = Buffer.from(data.data[0].b64_json, 'base64');
|
|
38
38
|
const filePath = saveMedia(buffer, 'png', 'generated-image.png');
|
|
39
39
|
return { filePath, mimeType: 'image/png' };
|
|
@@ -14,7 +14,7 @@ export class KlingGenerator {
|
|
|
14
14
|
const response = await fetch('https://api.klingai.com/v1/videos/text2video', {
|
|
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({
|
|
@@ -28,14 +28,14 @@ export class KlingGenerator {
|
|
|
28
28
|
const err = await response.text();
|
|
29
29
|
throw new Error(`Kling error ${response.status}: ${err.slice(0, 200)}`);
|
|
30
30
|
}
|
|
31
|
-
const { data } = await response.json();
|
|
31
|
+
const { data } = (await response.json());
|
|
32
32
|
// Poll for completion
|
|
33
|
-
const headers = {
|
|
33
|
+
const headers = { Authorization: `Bearer ${this.apiKey}` };
|
|
34
34
|
const videoUrl = await pollForCompletion({
|
|
35
35
|
statusUrl: `https://api.klingai.com/v1/videos/text2video/${data.task_id}`,
|
|
36
36
|
headers,
|
|
37
37
|
isComplete: (d) => d.data.task_status === 'succeed' && !!d.data.task_result?.videos[0],
|
|
38
|
-
isFailed: (d) => d.data.task_status === 'failed' ? 'generation failed' : null,
|
|
38
|
+
isFailed: (d) => (d.data.task_status === 'failed' ? 'generation failed' : null),
|
|
39
39
|
getResultUrl: (d) => d.data.task_result.videos[0].url,
|
|
40
40
|
label: 'Kling',
|
|
41
41
|
});
|
|
@@ -29,7 +29,7 @@ export class LyriaGenerator {
|
|
|
29
29
|
const err = await response.text();
|
|
30
30
|
throw new Error(`Lyria error ${response.status}: ${err.slice(0, 200)}`);
|
|
31
31
|
}
|
|
32
|
-
const data = await response.json();
|
|
32
|
+
const data = (await response.json());
|
|
33
33
|
const audioPart = data.candidates?.[0]?.content?.parts?.find((p) => p.inlineData);
|
|
34
34
|
if (!audioPart?.inlineData?.data) {
|
|
35
35
|
throw new Error('Lyria returned no audio data');
|
|
@@ -6,5 +6,5 @@ export declare class NanoBananaGenerator implements MediaGenerator {
|
|
|
6
6
|
readonly name = "Google Nano Banana";
|
|
7
7
|
readonly supportedTypes: MediaType[];
|
|
8
8
|
constructor(apiKey: string, model?: string);
|
|
9
|
-
generate(type: MediaType, prompt: string,
|
|
9
|
+
generate(type: MediaType, prompt: string, _options?: GenerateOptions): Promise<GenerateResult>;
|
|
10
10
|
}
|
|
@@ -9,7 +9,7 @@ export class NanoBananaGenerator {
|
|
|
9
9
|
this.apiKey = apiKey;
|
|
10
10
|
this.model = model;
|
|
11
11
|
}
|
|
12
|
-
async generate(type, prompt,
|
|
12
|
+
async generate(type, prompt, _options) {
|
|
13
13
|
if (type !== 'image')
|
|
14
14
|
throw new Error('Nano Banana only supports image generation');
|
|
15
15
|
// Gemini image generation API
|
|
@@ -31,7 +31,7 @@ export class NanoBananaGenerator {
|
|
|
31
31
|
const err = await response.text();
|
|
32
32
|
throw new Error(`Nano Banana error ${response.status}: ${err.slice(0, 200)}`);
|
|
33
33
|
}
|
|
34
|
-
const data = await response.json();
|
|
34
|
+
const data = (await response.json());
|
|
35
35
|
const imagePart = data.candidates?.[0]?.content?.parts?.find((p) => p.inlineData);
|
|
36
36
|
if (!imagePart?.inlineData?.data) {
|
|
37
37
|
throw new Error('Nano Banana returned no image data');
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { logger } from '../../util/logger.js';
|
|
2
2
|
export async function pollForCompletion(opts) {
|
|
3
|
-
const { statusUrl, headers, isComplete, isFailed, getResultUrl, interval = 5000, maxAttempts = 60, label = 'API' } = opts;
|
|
3
|
+
const { statusUrl, headers, isComplete, isFailed, getResultUrl, interval = 5000, maxAttempts = 60, label = 'API', } = opts;
|
|
4
4
|
for (let i = 0; i < maxAttempts; i++) {
|
|
5
|
-
await new Promise(r => setTimeout(r, interval));
|
|
5
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
6
6
|
const resp = await fetch(statusUrl, { headers, signal: AbortSignal.timeout(10000) });
|
|
7
7
|
if (!resp.ok) {
|
|
8
8
|
logger.warn(`[${label}] Poll failed: ${resp.status}`);
|
|
@@ -11,12 +11,12 @@ export async function pollForCompletion(opts) {
|
|
|
11
11
|
}
|
|
12
12
|
continue;
|
|
13
13
|
}
|
|
14
|
-
const data = await resp.json();
|
|
14
|
+
const data = (await resp.json());
|
|
15
15
|
const failMsg = isFailed(data);
|
|
16
16
|
if (failMsg)
|
|
17
17
|
throw new Error(`${label} generation failed: ${failMsg}`);
|
|
18
18
|
if (isComplete(data))
|
|
19
19
|
return getResultUrl(data);
|
|
20
20
|
}
|
|
21
|
-
throw new Error(`${label} generation timed out after ${maxAttempts * interval / 1000}s`);
|
|
21
|
+
throw new Error(`${label} generation timed out after ${(maxAttempts * interval) / 1000}s`);
|
|
22
22
|
}
|
|
@@ -17,14 +17,14 @@ export class RecraftGenerator {
|
|
|
17
17
|
const response = await fetch(endpoint, {
|
|
18
18
|
method: 'POST',
|
|
19
19
|
headers: {
|
|
20
|
-
|
|
20
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
21
21
|
'Content-Type': 'application/json',
|
|
22
22
|
},
|
|
23
23
|
body: JSON.stringify({
|
|
24
24
|
prompt: prompt.slice(0, 2000),
|
|
25
25
|
model: isVector ? 'recraftv4' : 'recraftv4',
|
|
26
26
|
response_format: isVector ? 'url' : 'b64_json',
|
|
27
|
-
style: isVector ? 'vector_illustration' :
|
|
27
|
+
style: isVector ? 'vector_illustration' : options?.style || 'realistic_image',
|
|
28
28
|
size: options?.width && options?.height ? `${options.width}x${options.height}` : '1024x1024',
|
|
29
29
|
}),
|
|
30
30
|
signal: AbortSignal.timeout(120000),
|
|
@@ -33,7 +33,7 @@ export class RecraftGenerator {
|
|
|
33
33
|
const err = await response.text();
|
|
34
34
|
throw new Error(`Recraft error ${response.status}: ${err.slice(0, 200)}`);
|
|
35
35
|
}
|
|
36
|
-
const data = await response.json();
|
|
36
|
+
const data = (await response.json());
|
|
37
37
|
const image = data.data?.[0];
|
|
38
38
|
if (isVector && image?.url) {
|
|
39
39
|
// Download SVG from URL
|
|
@@ -15,7 +15,7 @@ export class RunwayGenerator {
|
|
|
15
15
|
const startResponse = await fetch('https://api.runwayml.com/v1/image_to_video', {
|
|
16
16
|
method: 'POST',
|
|
17
17
|
headers: {
|
|
18
|
-
|
|
18
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
19
19
|
'Content-Type': 'application/json',
|
|
20
20
|
'X-Runway-Version': '2024-11-06',
|
|
21
21
|
},
|
|
@@ -30,14 +30,14 @@ export class RunwayGenerator {
|
|
|
30
30
|
const err = await startResponse.text();
|
|
31
31
|
throw new Error(`Runway error ${startResponse.status}: ${err.slice(0, 200)}`);
|
|
32
32
|
}
|
|
33
|
-
const { id: taskId } = await startResponse.json();
|
|
33
|
+
const { id: taskId } = (await startResponse.json());
|
|
34
34
|
// Poll for completion (max 5 minutes)
|
|
35
|
-
const headers = {
|
|
35
|
+
const headers = { Authorization: `Bearer ${this.apiKey}`, 'X-Runway-Version': '2024-11-06' };
|
|
36
36
|
const videoUrl = await pollForCompletion({
|
|
37
37
|
statusUrl: `https://api.runwayml.com/v1/tasks/${taskId}`,
|
|
38
38
|
headers,
|
|
39
39
|
isComplete: (data) => data.status === 'SUCCEEDED' && !!data.output?.[0],
|
|
40
|
-
isFailed: (data) => data.status === 'FAILED' ? 'generation failed' : null,
|
|
40
|
+
isFailed: (data) => (data.status === 'FAILED' ? 'generation failed' : null),
|
|
41
41
|
getResultUrl: (data) => data.output[0],
|
|
42
42
|
label: 'Runway',
|
|
43
43
|
});
|
|
@@ -18,8 +18,8 @@ export class StableDiffusionGenerator {
|
|
|
18
18
|
const response = await fetch('https://api.stability.ai/v2beta/stable-image/generate/core', {
|
|
19
19
|
method: 'POST',
|
|
20
20
|
headers: {
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
22
|
+
Accept: 'image/*',
|
|
23
23
|
},
|
|
24
24
|
body: formData,
|
|
25
25
|
signal: AbortSignal.timeout(120000),
|
|
@@ -24,7 +24,7 @@ export class VeoGenerator {
|
|
|
24
24
|
const err = await response.text();
|
|
25
25
|
throw new Error(`Veo error ${response.status}: ${err.slice(0, 200)}`);
|
|
26
26
|
}
|
|
27
|
-
const data = await response.json();
|
|
27
|
+
const data = (await response.json());
|
|
28
28
|
const buffer = Buffer.from(data.predictions[0].bytesBase64Encoded, 'base64');
|
|
29
29
|
const filePath = saveMedia(buffer, 'mp4', 'generated-video.mp4');
|
|
30
30
|
return { filePath, mimeType: 'video/mp4', durationMs: (options?.duration || 5) * 1000 };
|
package/dist/media/index.js
CHANGED
|
@@ -4,5 +4,5 @@ import { createMediaGenerator } from './factory.js';
|
|
|
4
4
|
export function initMediaGenerators(configs) {
|
|
5
5
|
if (!configs || configs.length === 0)
|
|
6
6
|
return [];
|
|
7
|
-
return configs.map(c => createMediaGenerator(c)).filter((g) => g !== null);
|
|
7
|
+
return configs.map((c) => createMediaGenerator(c)).filter((g) => g !== null);
|
|
8
8
|
}
|
package/dist/media/store.d.ts
CHANGED
|
@@ -8,3 +8,10 @@ export declare function isOversized(sizeBytes: number, maxSizeMb?: number): bool
|
|
|
8
8
|
export declare function cleanupMedia(ttlMs?: number): number;
|
|
9
9
|
/** Get the media directory path */
|
|
10
10
|
export declare function getMediaDir(): string;
|
|
11
|
+
/**
|
|
12
|
+
* Resolve a file path and confirm it lives inside the beecork media directory.
|
|
13
|
+
* Throws otherwise. Used at trust boundaries (MCP tools, voice transcription)
|
|
14
|
+
* where a caller could otherwise hand us an arbitrary path to read or upload.
|
|
15
|
+
* Returns the resolved (absolute) path on success.
|
|
16
|
+
*/
|
|
17
|
+
export declare function assertInsideMediaDir(filePath: string): string;
|
package/dist/media/store.js
CHANGED
|
@@ -17,9 +17,7 @@ export function saveMedia(buffer, extension, originalName) {
|
|
|
17
17
|
const safeName = originalName
|
|
18
18
|
? originalName.replace(/[\/\\]/g, '_').replace(/\.\./g, '_')
|
|
19
19
|
: undefined;
|
|
20
|
-
const name = safeName
|
|
21
|
-
? `${timestamp}-${safeName}`
|
|
22
|
-
: `${timestamp}.${extension}`;
|
|
20
|
+
const name = safeName ? `${timestamp}-${safeName}` : `${timestamp}.${extension}`;
|
|
23
21
|
const filePath = path.join(MEDIA_DIR, name);
|
|
24
22
|
fs.writeFileSync(filePath, buffer);
|
|
25
23
|
return filePath;
|
|
@@ -43,7 +41,9 @@ export function cleanupMedia(ttlMs = DEFAULT_TTL_MS) {
|
|
|
43
41
|
cleaned++;
|
|
44
42
|
}
|
|
45
43
|
}
|
|
46
|
-
catch {
|
|
44
|
+
catch {
|
|
45
|
+
/* file may have been deleted by another process */
|
|
46
|
+
}
|
|
47
47
|
}
|
|
48
48
|
if (cleaned > 0)
|
|
49
49
|
logger.info(`Media cleanup: removed ${cleaned} expired files`);
|
|
@@ -53,3 +53,17 @@ export function cleanupMedia(ttlMs = DEFAULT_TTL_MS) {
|
|
|
53
53
|
export function getMediaDir() {
|
|
54
54
|
return MEDIA_DIR;
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Resolve a file path and confirm it lives inside the beecork media directory.
|
|
58
|
+
* Throws otherwise. Used at trust boundaries (MCP tools, voice transcription)
|
|
59
|
+
* where a caller could otherwise hand us an arbitrary path to read or upload.
|
|
60
|
+
* Returns the resolved (absolute) path on success.
|
|
61
|
+
*/
|
|
62
|
+
export function assertInsideMediaDir(filePath) {
|
|
63
|
+
const resolved = path.resolve(filePath);
|
|
64
|
+
const root = path.resolve(MEDIA_DIR) + path.sep;
|
|
65
|
+
if (!resolved.startsWith(root)) {
|
|
66
|
+
throw new Error(`filePath must be inside the beecork media directory (${MEDIA_DIR})`);
|
|
67
|
+
}
|
|
68
|
+
return resolved;
|
|
69
|
+
}
|
package/dist/media/types.d.ts
CHANGED
|
@@ -19,3 +19,25 @@ export interface MediaGenerator {
|
|
|
19
19
|
readonly supportedTypes: MediaType[];
|
|
20
20
|
generate(type: MediaType, prompt: string, options?: GenerateOptions): Promise<GenerateResult>;
|
|
21
21
|
}
|
|
22
|
+
/** Gemini `:generateContent` response shape (used by lyria + nano-banana). */
|
|
23
|
+
export interface GeminiInlineDataPart {
|
|
24
|
+
inlineData?: {
|
|
25
|
+
mimeType?: string;
|
|
26
|
+
data: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export interface GeminiCandidate {
|
|
30
|
+
content?: {
|
|
31
|
+
parts?: GeminiInlineDataPart[];
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export interface GeminiGenerateContentResponse {
|
|
35
|
+
candidates?: GeminiCandidate[];
|
|
36
|
+
}
|
|
37
|
+
/** Recraft `/v1/images/generations` response. */
|
|
38
|
+
export interface RecraftGenerateResponse {
|
|
39
|
+
data?: Array<{
|
|
40
|
+
url?: string;
|
|
41
|
+
b64_json?: string;
|
|
42
|
+
}>;
|
|
43
|
+
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
export type { NotificationProvider } from './types.js';
|
|
2
|
-
export { PushoverProvider } from './pushover.js';
|
|
3
|
-
export { NtfyProvider } from './ntfy.js';
|
|
4
|
-
export { WebhookNotificationProvider } from './webhook-provider.js';
|
|
5
2
|
import type { NotificationProvider } from './types.js';
|
|
6
|
-
|
|
3
|
+
import type { NotificationConfig } from '../types.js';
|
|
4
|
+
export declare function createNotificationProvider(config: NotificationConfig): NotificationProvider | null;
|
|
@@ -1,34 +1,21 @@
|
|
|
1
|
-
export { PushoverProvider } from './pushover.js';
|
|
2
|
-
export { NtfyProvider } from './ntfy.js';
|
|
3
|
-
export { WebhookNotificationProvider } from './webhook-provider.js';
|
|
4
1
|
import { logger } from '../util/logger.js';
|
|
5
2
|
import { PushoverProvider } from './pushover.js';
|
|
6
3
|
import { NtfyProvider } from './ntfy.js';
|
|
7
4
|
import { WebhookNotificationProvider } from './webhook-provider.js';
|
|
8
5
|
export function createNotificationProvider(config) {
|
|
9
|
-
if (!config?.type)
|
|
10
|
-
return null;
|
|
11
6
|
switch (config.type) {
|
|
12
7
|
case 'pushover':
|
|
13
|
-
if (!config.userKey || !config.appToken) {
|
|
14
|
-
logger.warn('Pushover: missing userKey or appToken');
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
17
8
|
return new PushoverProvider(config.userKey, config.appToken);
|
|
18
9
|
case 'ntfy':
|
|
19
|
-
if (!config.topic) {
|
|
20
|
-
logger.warn('ntfy: missing topic');
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
10
|
return new NtfyProvider(config.topic, config.server);
|
|
24
11
|
case 'webhook':
|
|
25
|
-
if (!config.url) {
|
|
26
|
-
logger.warn('Webhook notification: missing url');
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
12
|
return new WebhookNotificationProvider(config.url, config.headers);
|
|
30
|
-
default:
|
|
31
|
-
|
|
13
|
+
default: {
|
|
14
|
+
// Exhaustiveness check — TS will error here if NotificationConfig gains
|
|
15
|
+
// a new variant without a matching case above.
|
|
16
|
+
const _exhaustive = config;
|
|
17
|
+
logger.warn(`Unknown notification provider: ${JSON.stringify(_exhaustive)}`);
|
|
32
18
|
return null;
|
|
19
|
+
}
|
|
33
20
|
}
|
|
34
21
|
}
|
|
@@ -11,9 +11,9 @@ export class NtfyProvider {
|
|
|
11
11
|
const response = await fetch(`${this.server}/${this.topic}`, {
|
|
12
12
|
method: 'POST',
|
|
13
13
|
headers: {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
Title: 'Beecork',
|
|
15
|
+
Priority: urgent ? '5' : '3',
|
|
16
|
+
Tags: urgent ? 'warning' : 'robot',
|
|
17
17
|
},
|
|
18
18
|
body: message,
|
|
19
19
|
signal: AbortSignal.timeout(10000),
|
|
@@ -1,28 +1,44 @@
|
|
|
1
1
|
import { getDb } from '../db/index.js';
|
|
2
2
|
export function getCostSummary() {
|
|
3
3
|
const db = getDb();
|
|
4
|
-
const today = db
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
const today = db
|
|
5
|
+
.prepare("SELECT COALESCE(SUM(cost_usd), 0) as total FROM messages WHERE created_at > date('now')")
|
|
6
|
+
.get().total;
|
|
7
|
+
const last7 = db
|
|
8
|
+
.prepare("SELECT COALESCE(SUM(cost_usd), 0) as total FROM messages WHERE created_at > date('now', '-7 days')")
|
|
9
|
+
.get().total;
|
|
10
|
+
const last30 = db
|
|
11
|
+
.prepare("SELECT COALESCE(SUM(cost_usd), 0) as total FROM messages WHERE created_at > date('now', '-30 days')")
|
|
12
|
+
.get().total;
|
|
7
13
|
// Per-tab summed over the last 30 days only — the unbounded version grew
|
|
8
14
|
// linearly with the messages table and made /cost noticeably slow over time.
|
|
9
|
-
const perTab = db
|
|
15
|
+
const perTab = db
|
|
16
|
+
.prepare(`
|
|
10
17
|
SELECT t.name, COALESCE(SUM(m.cost_usd), 0) as cost, COUNT(m.id) as messages
|
|
11
18
|
FROM tabs t LEFT JOIN messages m ON m.tab_id = t.id
|
|
12
19
|
AND m.created_at > date('now', '-30 days')
|
|
13
20
|
GROUP BY t.id ORDER BY cost DESC
|
|
14
|
-
`)
|
|
21
|
+
`)
|
|
22
|
+
.all();
|
|
15
23
|
return { today, last7Days: last7, last30Days: last30, perTab };
|
|
16
24
|
}
|
|
17
25
|
export function getActivitySummary(hours = 24) {
|
|
18
26
|
const db = getDb();
|
|
19
27
|
const sinceDate = new Date(Date.now() - hours * 3600000).toISOString();
|
|
20
|
-
const messagesReceived = db
|
|
21
|
-
|
|
28
|
+
const messagesReceived = db
|
|
29
|
+
.prepare('SELECT COUNT(*) as c FROM messages WHERE role = ? AND created_at > ?')
|
|
30
|
+
.get('user', sinceDate).c;
|
|
31
|
+
const messagesFromAssistant = db
|
|
32
|
+
.prepare('SELECT COUNT(*) as c FROM messages WHERE role = ? AND created_at > ?')
|
|
33
|
+
.get('assistant', sinceDate).c;
|
|
22
34
|
const tasksFired = db.prepare('SELECT COUNT(*) as c FROM tasks WHERE last_run_at > ?').get(sinceDate).c;
|
|
23
35
|
const memoriesCreated = db.prepare('SELECT COUNT(*) as c FROM memories WHERE created_at > ?').get(sinceDate).c;
|
|
24
|
-
const totalCost = db
|
|
25
|
-
|
|
36
|
+
const totalCost = db
|
|
37
|
+
.prepare('SELECT COALESCE(SUM(cost_usd), 0) as total FROM messages WHERE created_at > ?')
|
|
38
|
+
.get(sinceDate).total;
|
|
39
|
+
const activeTabsCount = db
|
|
40
|
+
.prepare('SELECT COUNT(DISTINCT tab_id) as c FROM messages WHERE created_at > ?')
|
|
41
|
+
.get(sinceDate).c;
|
|
26
42
|
return {
|
|
27
43
|
period: `Last ${hours} hours`,
|
|
28
44
|
messagesReceived,
|
|
@@ -40,19 +56,25 @@ const ANOMALY_STATE_KEY = 'anomaly_spend_state';
|
|
|
40
56
|
*/
|
|
41
57
|
export function checkAnomaliesWithDb(db) {
|
|
42
58
|
// Today's spend
|
|
43
|
-
const todaySpend = db
|
|
59
|
+
const todaySpend = db
|
|
60
|
+
.prepare("SELECT COALESCE(SUM(cost_usd), 0) as total FROM messages WHERE created_at > date('now')")
|
|
61
|
+
.get().total;
|
|
44
62
|
// 7-day rolling average (excluding today)
|
|
45
|
-
const avgSpend = db
|
|
63
|
+
const avgSpend = db
|
|
64
|
+
.prepare(`
|
|
46
65
|
SELECT COALESCE(AVG(daily_total), 0) as avg FROM (
|
|
47
66
|
SELECT date(created_at) as day, SUM(cost_usd) as daily_total
|
|
48
67
|
FROM messages
|
|
49
68
|
WHERE created_at > date('now', '-7 days') AND created_at < date('now')
|
|
50
69
|
GROUP BY date(created_at)
|
|
51
70
|
)
|
|
52
|
-
`)
|
|
71
|
+
`)
|
|
72
|
+
.get().avg;
|
|
53
73
|
const isBreach = avgSpend > 0 && todaySpend > avgSpend * 2;
|
|
54
74
|
const newState = isBreach ? 'breach' : 'ok';
|
|
55
|
-
const prevRow = db
|
|
75
|
+
const prevRow = db
|
|
76
|
+
.prepare('SELECT value FROM preferences WHERE key = ?')
|
|
77
|
+
.get(ANOMALY_STATE_KEY);
|
|
56
78
|
const prevState = prevRow?.value ?? 'ok';
|
|
57
79
|
if (newState === prevState)
|
|
58
80
|
return null;
|
package/dist/projects/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export type { Project, RouteDecision, RoutingContext } from './types.js';
|
|
2
|
-
export { discoverProjects, createProject, listProjects, getProject, ensureCategory, touchProject } from './manager.js';
|
|
2
|
+
export { discoverProjects, createProject, listProjects, getProject, ensureCategory, touchProject, } from './manager.js';
|
|
3
3
|
export { routeMessage, setUserContext, resolveProjectRoute } from './router.js';
|
|
4
4
|
export type { RouteResult } from './router.js';
|
package/dist/projects/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { discoverProjects, createProject, listProjects, getProject, ensureCategory, touchProject } from './manager.js';
|
|
1
|
+
export { discoverProjects, createProject, listProjects, getProject, ensureCategory, touchProject, } from './manager.js';
|
|
2
2
|
export { routeMessage, setUserContext, resolveProjectRoute } from './router.js';
|
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
import type { Project } from './types.js';
|
|
2
|
-
/** Get the workspace root from config */
|
|
3
|
-
export declare function getWorkspaceRoot(): string;
|
|
4
|
-
/** Get the managed workspace path (.beecork/ under workspace root) */
|
|
5
|
-
export declare function getManagedWorkspace(): string;
|
|
6
2
|
/** Discover projects in scan paths (look for git repos, package.json, etc.) */
|
|
7
3
|
export declare function discoverProjects(scanPaths?: string[]): Project[];
|
|
8
4
|
/** Create a new project. parentDir must resolve under an allowed root. */
|