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
|
@@ -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.d.ts
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
export type { MediaGenerator, MediaType, GenerateOptions, GenerateResult } from './types.js';
|
|
2
2
|
export { createMediaGenerator } from './factory.js';
|
|
3
|
-
export { saveMedia, cleanupMedia, ensureMediaDir,
|
|
3
|
+
export { saveMedia, cleanupMedia, ensureMediaDir, isOversized } from './store.js';
|
|
4
4
|
import type { MediaGenerator } from './types.js';
|
|
5
|
-
|
|
6
|
-
provider: string;
|
|
7
|
-
apiKey?: string;
|
|
8
|
-
model?: string;
|
|
9
|
-
style?: string;
|
|
10
|
-
}
|
|
5
|
+
import type { MediaGeneratorConfig } from '../types.js';
|
|
11
6
|
export declare function initMediaGenerators(configs?: MediaGeneratorConfig[]): MediaGenerator[];
|
package/dist/media/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export { createMediaGenerator } from './factory.js';
|
|
2
|
-
export { saveMedia, cleanupMedia, ensureMediaDir,
|
|
2
|
+
export { saveMedia, cleanupMedia, ensureMediaDir, isOversized } from './store.js';
|
|
3
3
|
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,30 +1,49 @@
|
|
|
1
1
|
import { getDb } from '../db/index.js';
|
|
2
2
|
export function getCostSummary() {
|
|
3
3
|
const db = getDb();
|
|
4
|
-
const today = db
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
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;
|
|
13
|
+
// Per-tab summed over the last 30 days only — the unbounded version grew
|
|
14
|
+
// linearly with the messages table and made /cost noticeably slow over time.
|
|
15
|
+
const perTab = db
|
|
16
|
+
.prepare(`
|
|
8
17
|
SELECT t.name, COALESCE(SUM(m.cost_usd), 0) as cost, COUNT(m.id) as messages
|
|
9
18
|
FROM tabs t LEFT JOIN messages m ON m.tab_id = t.id
|
|
19
|
+
AND m.created_at > date('now', '-30 days')
|
|
10
20
|
GROUP BY t.id ORDER BY cost DESC
|
|
11
|
-
`)
|
|
21
|
+
`)
|
|
22
|
+
.all();
|
|
12
23
|
return { today, last7Days: last7, last30Days: last30, perTab };
|
|
13
24
|
}
|
|
14
25
|
export function getActivitySummary(hours = 24) {
|
|
15
26
|
const db = getDb();
|
|
16
27
|
const sinceDate = new Date(Date.now() - hours * 3600000).toISOString();
|
|
17
|
-
const messagesReceived = db
|
|
18
|
-
|
|
19
|
-
|
|
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;
|
|
34
|
+
const tasksFired = db.prepare('SELECT COUNT(*) as c FROM tasks WHERE last_run_at > ?').get(sinceDate).c;
|
|
20
35
|
const memoriesCreated = db.prepare('SELECT COUNT(*) as c FROM memories WHERE created_at > ?').get(sinceDate).c;
|
|
21
|
-
const totalCost = db
|
|
22
|
-
|
|
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;
|
|
23
42
|
return {
|
|
24
43
|
period: `Last ${hours} hours`,
|
|
25
44
|
messagesReceived,
|
|
26
45
|
messagesFromAssistant,
|
|
27
|
-
|
|
46
|
+
tasksFired,
|
|
28
47
|
memoriesCreated,
|
|
29
48
|
totalCost,
|
|
30
49
|
activeTabsCount,
|
|
@@ -37,19 +56,25 @@ const ANOMALY_STATE_KEY = 'anomaly_spend_state';
|
|
|
37
56
|
*/
|
|
38
57
|
export function checkAnomaliesWithDb(db) {
|
|
39
58
|
// Today's spend
|
|
40
|
-
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;
|
|
41
62
|
// 7-day rolling average (excluding today)
|
|
42
|
-
const avgSpend = db
|
|
63
|
+
const avgSpend = db
|
|
64
|
+
.prepare(`
|
|
43
65
|
SELECT COALESCE(AVG(daily_total), 0) as avg FROM (
|
|
44
66
|
SELECT date(created_at) as day, SUM(cost_usd) as daily_total
|
|
45
67
|
FROM messages
|
|
46
68
|
WHERE created_at > date('now', '-7 days') AND created_at < date('now')
|
|
47
69
|
GROUP BY date(created_at)
|
|
48
70
|
)
|
|
49
|
-
`)
|
|
71
|
+
`)
|
|
72
|
+
.get().avg;
|
|
50
73
|
const isBreach = avgSpend > 0 && todaySpend > avgSpend * 2;
|
|
51
74
|
const newState = isBreach ? 'breach' : 'ok';
|
|
52
|
-
const prevRow = db
|
|
75
|
+
const prevRow = db
|
|
76
|
+
.prepare('SELECT value FROM preferences WHERE key = ?')
|
|
77
|
+
.get(ANOMALY_STATE_KEY);
|
|
53
78
|
const prevState = prevRow?.value ?? 'ok';
|
|
54
79
|
if (newState === prevState)
|
|
55
80
|
return null;
|
|
@@ -85,7 +110,7 @@ export function formatActivitySummary(summary) {
|
|
|
85
110
|
`📊 Activity (${summary.period})`,
|
|
86
111
|
` Messages in: ${summary.messagesReceived}`,
|
|
87
112
|
` Messages out: ${summary.messagesFromAssistant}`,
|
|
88
|
-
`
|
|
113
|
+
` Tasks fired: ${summary.tasksFired}`,
|
|
89
114
|
` Memories created: ${summary.memoriesCreated}`,
|
|
90
115
|
` Active tabs: ${summary.activeTabsCount}`,
|
|
91
116
|
` Cost: $${summary.totalCost.toFixed(4)}`,
|
package/dist/projects/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export type { Project, RouteDecision, RoutingContext } from './types.js';
|
|
2
|
-
export { discoverProjects, createProject, listProjects, getProject, ensureCategory, touchProject,
|
|
3
|
-
export { routeMessage, setUserContext } from './router.js';
|
|
2
|
+
export { discoverProjects, createProject, listProjects, getProject, ensureCategory, touchProject, } from './manager.js';
|
|
3
|
+
export { routeMessage, setUserContext, resolveProjectRoute } from './router.js';
|
|
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,
|
|
2
|
-
export { routeMessage, setUserContext } from './router.js';
|
|
1
|
+
export { discoverProjects, createProject, listProjects, getProject, ensureCategory, touchProject, } from './manager.js';
|
|
2
|
+
export { routeMessage, setUserContext, resolveProjectRoute } from './router.js';
|
|
@@ -1,11 +1,7 @@
|
|
|
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
|
-
/** Create a new project */
|
|
4
|
+
/** Create a new project. parentDir must resolve under an allowed root. */
|
|
9
5
|
export declare function createProject(name: string, parentDir?: string): Project;
|
|
10
6
|
/** Ensure a managed category exists (lazy creation) */
|
|
11
7
|
export declare function ensureCategory(name: string): Project;
|
|
@@ -15,5 +11,3 @@ export declare function listProjects(): Project[];
|
|
|
15
11
|
export declare function getProject(name: string): Project | null;
|
|
16
12
|
/** Update last used timestamp */
|
|
17
13
|
export declare function touchProject(name: string): void;
|
|
18
|
-
/** Close (permanently delete) a tab */
|
|
19
|
-
export declare function closeTab(tabName: string): boolean;
|
package/dist/projects/manager.js
CHANGED
|
@@ -3,16 +3,28 @@ import path from 'node:path';
|
|
|
3
3
|
import { v4 as uuidv4 } from 'uuid';
|
|
4
4
|
import { getDb } from '../db/index.js';
|
|
5
5
|
import { getConfig } from '../config.js';
|
|
6
|
+
import { expandHome } from '../util/paths.js';
|
|
6
7
|
import { logger } from '../util/logger.js';
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
import { invalidateProjectCache } from './router.js';
|
|
9
|
+
function rowToProject(r) {
|
|
10
|
+
return {
|
|
11
|
+
id: r.id,
|
|
12
|
+
name: r.name,
|
|
13
|
+
path: r.path,
|
|
14
|
+
type: r.type,
|
|
15
|
+
lastUsedAt: r.last_used_at,
|
|
16
|
+
createdAt: r.created_at,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Get the workspace root from config (module-local) */
|
|
20
|
+
function getWorkspaceRoot() {
|
|
9
21
|
const config = getConfig();
|
|
10
22
|
// Use the default tab's workingDir as workspace root
|
|
11
23
|
const root = config.tabs?.default?.workingDir || process.env.HOME || '';
|
|
12
|
-
return
|
|
24
|
+
return expandHome(root);
|
|
13
25
|
}
|
|
14
|
-
/** Get the managed workspace path (.beecork/ under workspace root) */
|
|
15
|
-
|
|
26
|
+
/** Get the managed workspace path (.beecork/ under workspace root) (module-local) */
|
|
27
|
+
function getManagedWorkspace() {
|
|
16
28
|
return path.join(getWorkspaceRoot(), '.beecork');
|
|
17
29
|
}
|
|
18
30
|
/** Discover projects in scan paths (look for git repos, package.json, etc.) */
|
|
@@ -21,7 +33,7 @@ export function discoverProjects(scanPaths) {
|
|
|
21
33
|
const projects = [];
|
|
22
34
|
const db = getDb();
|
|
23
35
|
for (let scanPath of paths) {
|
|
24
|
-
scanPath =
|
|
36
|
+
scanPath = expandHome(scanPath);
|
|
25
37
|
if (!fs.existsSync(scanPath))
|
|
26
38
|
continue;
|
|
27
39
|
try {
|
|
@@ -35,13 +47,13 @@ export function discoverProjects(scanPaths) {
|
|
|
35
47
|
continue;
|
|
36
48
|
const dirPath = path.join(scanPath, entry.name);
|
|
37
49
|
// Check if it looks like a project (has .git, package.json, or similar)
|
|
38
|
-
const isProject = fs.existsSync(path.join(dirPath, '.git'))
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
50
|
+
const isProject = fs.existsSync(path.join(dirPath, '.git')) ||
|
|
51
|
+
fs.existsSync(path.join(dirPath, 'package.json')) ||
|
|
52
|
+
fs.existsSync(path.join(dirPath, 'Cargo.toml')) ||
|
|
53
|
+
fs.existsSync(path.join(dirPath, 'go.mod')) ||
|
|
54
|
+
fs.existsSync(path.join(dirPath, 'requirements.txt')) ||
|
|
55
|
+
fs.existsSync(path.join(dirPath, 'pyproject.toml')) ||
|
|
56
|
+
fs.existsSync(path.join(dirPath, 'CLAUDE.md'));
|
|
45
57
|
if (isProject) {
|
|
46
58
|
projects.push({
|
|
47
59
|
id: uuidv4(),
|
|
@@ -65,18 +77,27 @@ export function discoverProjects(scanPaths) {
|
|
|
65
77
|
ON CONFLICT(name) DO UPDATE SET path = excluded.path, last_used_at = datetime('now')
|
|
66
78
|
`).run(project.id, project.name, project.path, project.type);
|
|
67
79
|
}
|
|
80
|
+
invalidateProjectCache();
|
|
68
81
|
return projects;
|
|
69
82
|
}
|
|
70
|
-
/** Create a new project */
|
|
83
|
+
/** Create a new project. parentDir must resolve under an allowed root. */
|
|
71
84
|
export function createProject(name, parentDir) {
|
|
72
|
-
const
|
|
85
|
+
const requestedParent = parentDir || getWorkspaceRoot();
|
|
86
|
+
const resolvedParent = path.resolve(expandHome(requestedParent));
|
|
87
|
+
// Allowlist: parent must resolve under workspace root or one of the configured scan paths.
|
|
88
|
+
const config = getConfig();
|
|
89
|
+
const allowedRoots = [getWorkspaceRoot(), ...(config.projectScanPaths ?? [])].map((r) => path.resolve(expandHome(r)));
|
|
90
|
+
const isAllowed = allowedRoots.some((root) => resolvedParent === root || resolvedParent.startsWith(root + path.sep));
|
|
91
|
+
if (!isAllowed) {
|
|
92
|
+
throw new Error(`Project parent directory must be under workspace root or a configured scan path. Allowed: ${allowedRoots.join(', ')}`);
|
|
93
|
+
}
|
|
73
94
|
// Sanitize name to prevent path traversal
|
|
74
95
|
const safeName = path.basename(name.replace(/\.\./g, ''));
|
|
75
96
|
if (!safeName)
|
|
76
97
|
throw new Error('Invalid project name');
|
|
77
|
-
const projectPath = path.resolve(
|
|
98
|
+
const projectPath = path.resolve(resolvedParent, safeName);
|
|
78
99
|
// Ensure resolved path is within the parent directory
|
|
79
|
-
if (!projectPath.startsWith(
|
|
100
|
+
if (!projectPath.startsWith(resolvedParent)) {
|
|
80
101
|
throw new Error('Project path must be within the parent directory');
|
|
81
102
|
}
|
|
82
103
|
if (fs.existsSync(projectPath)) {
|
|
@@ -93,50 +114,53 @@ export function createProject(name, parentDir) {
|
|
|
93
114
|
INSERT INTO projects (id, name, path, type) VALUES (?, ?, ?, ?)
|
|
94
115
|
ON CONFLICT(name) DO UPDATE SET path = excluded.path
|
|
95
116
|
`).run(id, name, projectPath, 'user-project');
|
|
96
|
-
|
|
117
|
+
invalidateProjectCache();
|
|
118
|
+
return {
|
|
119
|
+
id,
|
|
120
|
+
name,
|
|
121
|
+
path: projectPath,
|
|
122
|
+
type: 'user-project',
|
|
123
|
+
lastUsedAt: new Date().toISOString(),
|
|
124
|
+
createdAt: new Date().toISOString(),
|
|
125
|
+
};
|
|
97
126
|
}
|
|
98
127
|
/** Ensure a managed category exists (lazy creation) */
|
|
99
128
|
export function ensureCategory(name) {
|
|
100
129
|
const categoryPath = path.join(getManagedWorkspace(), name);
|
|
101
130
|
fs.mkdirSync(categoryPath, { recursive: true });
|
|
102
131
|
const db = getDb();
|
|
103
|
-
const existing = db
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
132
|
+
const existing = db
|
|
133
|
+
.prepare('SELECT * FROM projects WHERE name = ? AND type = ?')
|
|
134
|
+
.get(name, 'category');
|
|
135
|
+
if (existing)
|
|
136
|
+
return rowToProject(existing);
|
|
107
137
|
const id = uuidv4();
|
|
108
138
|
db.prepare('INSERT INTO projects (id, name, path, type) VALUES (?, ?, ?, ?)').run(id, name, categoryPath, 'category');
|
|
109
|
-
|
|
139
|
+
invalidateProjectCache();
|
|
140
|
+
return {
|
|
141
|
+
id,
|
|
142
|
+
name,
|
|
143
|
+
path: categoryPath,
|
|
144
|
+
type: 'category',
|
|
145
|
+
lastUsedAt: new Date().toISOString(),
|
|
146
|
+
createdAt: new Date().toISOString(),
|
|
147
|
+
};
|
|
110
148
|
}
|
|
111
149
|
/** List all projects */
|
|
112
150
|
export function listProjects() {
|
|
113
151
|
const db = getDb();
|
|
114
|
-
const rows = db
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}));
|
|
152
|
+
const rows = db
|
|
153
|
+
.prepare('SELECT * FROM projects ORDER BY type, last_used_at DESC')
|
|
154
|
+
.all();
|
|
155
|
+
return rows.map(rowToProject);
|
|
119
156
|
}
|
|
120
157
|
/** Get a project by name */
|
|
121
158
|
export function getProject(name) {
|
|
122
159
|
const db = getDb();
|
|
123
160
|
const row = db.prepare('SELECT * FROM projects WHERE name = ?').get(name);
|
|
124
|
-
|
|
125
|
-
return null;
|
|
126
|
-
return { id: row.id, name: row.name, path: row.path, type: row.type, lastUsedAt: row.last_used_at, createdAt: row.created_at };
|
|
161
|
+
return row ? rowToProject(row) : null;
|
|
127
162
|
}
|
|
128
163
|
/** Update last used timestamp */
|
|
129
164
|
export function touchProject(name) {
|
|
130
165
|
getDb().prepare("UPDATE projects SET last_used_at = datetime('now') WHERE name = ?").run(name);
|
|
131
166
|
}
|
|
132
|
-
/** Close (permanently delete) a tab */
|
|
133
|
-
export function closeTab(tabName) {
|
|
134
|
-
const db = getDb();
|
|
135
|
-
const tab = db.prepare('SELECT id FROM tabs WHERE name = ?').get(tabName);
|
|
136
|
-
if (!tab)
|
|
137
|
-
return false;
|
|
138
|
-
db.prepare('DELETE FROM messages WHERE tab_id = ?').run(tab.id);
|
|
139
|
-
db.prepare('DELETE FROM tabs WHERE id = ?').run(tab.id);
|
|
140
|
-
logger.info(`Tab permanently closed: ${tabName}`);
|
|
141
|
-
return true;
|
|
142
|
-
}
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import type { RouteDecision, RoutingContext } from './types.js';
|
|
2
|
+
/** Invalidate the project cache. Call after createProject/discoverProjects/etc. */
|
|
3
|
+
export declare function invalidateProjectCache(): void;
|
|
2
4
|
/** Route a message to the right project and tab */
|
|
3
5
|
export declare function routeMessage(message: string, context?: RoutingContext): RouteDecision;
|
|
4
6
|
/** Update current context for a user */
|
|
5
7
|
export declare function setUserContext(userId: string, projectName: string, tabName: string): void;
|
|
8
|
+
export interface RouteResult {
|
|
9
|
+
effectiveTabName: string;
|
|
10
|
+
projectPath?: string;
|
|
11
|
+
confirmationMessage?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Shared project routing logic — resolves which tab/project to use for a message.
|
|
15
|
+
* Pulled out of channels/ so all routing decisions live in one place.
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveProjectRoute(rawPrompt: string, tabName: string, text: string, userId: string): Promise<RouteResult>;
|