beecork 1.3.11 → 1.4.1
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/README.md +3 -4
- package/dist/channels/command-handler.js +0 -14
- package/dist/channels/telegram.js +80 -93
- package/dist/channels/types.d.ts +0 -2
- package/dist/cli/channel.js +0 -1
- package/dist/cli/setup.js +5 -82
- package/dist/config.js +9 -25
- package/dist/daemon.js +5 -21
- package/dist/db/index.js +1 -2
- package/dist/db/migrations.js +10 -11
- package/dist/index.js +0 -23
- package/dist/mcp/server.js +5 -10
- package/dist/session/manager.d.ts +0 -1
- package/dist/session/manager.js +5 -32
- package/dist/tasks/scheduler.js +0 -3
- package/dist/types.d.ts +2 -17
- package/package.json +16 -18
- package/dist/machines/index.d.ts +0 -1
- package/dist/machines/index.js +0 -1
- package/dist/machines/registry.d.ts +0 -15
- package/dist/machines/registry.js +0 -46
- package/dist/memory/extractor.d.ts +0 -5
- package/dist/memory/extractor.js +0 -157
- package/dist/pipe/anthropic-client.d.ts +0 -14
- package/dist/pipe/anthropic-client.js +0 -98
- package/dist/pipe/brain.d.ts +0 -22
- package/dist/pipe/brain.js +0 -160
- package/dist/pipe/memory-store.d.ts +0 -10
- package/dist/pipe/memory-store.js +0 -50
- package/dist/pipe/project-scanner.d.ts +0 -6
- package/dist/pipe/project-scanner.js +0 -26
- package/dist/pipe/types.d.ts +0 -46
- package/dist/pipe/types.js +0 -1
package/dist/session/manager.js
CHANGED
|
@@ -5,7 +5,6 @@ import { ContextMonitor } from './context-monitor.js';
|
|
|
5
5
|
import { getDb } from '../db/index.js';
|
|
6
6
|
import { resolveWorkingDir, validateTabName } from '../config.js';
|
|
7
7
|
import { logger } from '../util/logger.js';
|
|
8
|
-
import { extractMemories, getRelevantMemories } from '../memory/extractor.js';
|
|
9
8
|
import { logActivity } from '../timeline/index.js';
|
|
10
9
|
import { getAllKnowledge, formatKnowledgeForContext } from '../knowledge/index.js';
|
|
11
10
|
function rowToTab(row) {
|
|
@@ -85,7 +84,7 @@ export class TabManager {
|
|
|
85
84
|
logger.info(`[${tabName}] Message queued (queue size: ${this.messageQueues.get(tabName).length})`);
|
|
86
85
|
});
|
|
87
86
|
}
|
|
88
|
-
return this.executeMessage(tab, prompt, options?.resume ?? false, options?.onTextChunk, options?.
|
|
87
|
+
return this.executeMessage(tab, prompt, options?.resume ?? false, options?.onTextChunk, options?.onToolUse, options?._compactionDepth);
|
|
89
88
|
}
|
|
90
89
|
/** Get all tabs from the database */
|
|
91
90
|
listTabs() {
|
|
@@ -147,7 +146,7 @@ export class TabManager {
|
|
|
147
146
|
}
|
|
148
147
|
}
|
|
149
148
|
}
|
|
150
|
-
async executeMessage(tab, prompt, resume, onTextChunk,
|
|
149
|
+
async executeMessage(tab, prompt, resume, onTextChunk, onToolUse, compactionDepth) {
|
|
151
150
|
const db = getDb();
|
|
152
151
|
logActivity('task_started', 'Processing message', { tabName: tab.name, details: prompt.slice(0, 500) });
|
|
153
152
|
// Budget check before spawning
|
|
@@ -163,26 +162,10 @@ export class TabManager {
|
|
|
163
162
|
this.onNotify?.(`⚠️ Budget warning: tab "${tab.name}" at $${tabSpend.toFixed(2)} / $${this.config.claudeCode.maxBudgetUsd.toFixed(2)} (80%)`).catch(() => { });
|
|
164
163
|
}
|
|
165
164
|
}
|
|
166
|
-
//
|
|
167
|
-
const tabConfig = this.config.tabs[tab.name] || this.config.tabs['default'];
|
|
168
|
-
if (tabConfig?.approvalMode && tabConfig.approvalMode !== 'yolo') {
|
|
169
|
-
logger.warn(`Tab "${tab.name}" has approvalMode="${tabConfig.approvalMode}" — interactive approval not yet implemented, running in yolo mode`);
|
|
170
|
-
}
|
|
171
|
-
// Inject knowledge from all three layers
|
|
165
|
+
// Inject knowledge (global markdown + project markdown + tab facts)
|
|
172
166
|
const knowledge = getAllKnowledge(tab.workingDir, tab.name);
|
|
173
167
|
const knowledgeContext = formatKnowledgeForContext(knowledge);
|
|
174
|
-
|
|
175
|
-
if (knowledgeContext) {
|
|
176
|
-
enrichedPrompt = `${knowledgeContext}\n\n${prompt}`;
|
|
177
|
-
}
|
|
178
|
-
// Also inject relevant memories as fallback (additive)
|
|
179
|
-
const memories = getRelevantMemories(tab.name);
|
|
180
|
-
if (memories.length > 0) {
|
|
181
|
-
const memoryContext = memories.map(m => `- ${m}`).join('\n');
|
|
182
|
-
if (!knowledgeContext) {
|
|
183
|
-
enrichedPrompt = `[Context from memory:\n${memoryContext}\n]\n\n${prompt}`;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
168
|
+
const enrichedPrompt = knowledgeContext ? `${knowledgeContext}\n\n${prompt}` : prompt;
|
|
186
169
|
// Store user message
|
|
187
170
|
db.prepare('INSERT INTO messages (tab_id, role, content) VALUES (?, ?, ?)')
|
|
188
171
|
.run(tab.id, 'user', prompt);
|
|
@@ -289,14 +272,11 @@ export class TabManager {
|
|
|
289
272
|
// Ask Claude for a structured summary
|
|
290
273
|
const summaryPrompt = 'Summarize your progress in this session concisely: completed steps, current state, remaining steps, and all important identifiers (file paths, URLs, variable names). Output ONLY the summary.';
|
|
291
274
|
this.sendMessage(tab.name, summaryPrompt, { _compactionDepth: currentDepth + 1 }).then(summaryResult => {
|
|
292
|
-
// Store summary as checkpoint memory
|
|
293
|
-
db.prepare('INSERT INTO memories (content, tab_name, source) VALUES (?, ?, ?)')
|
|
294
|
-
.run(`[checkpoint] ${summaryResult.text}`, tab.name, 'auto');
|
|
295
275
|
// Reset session: new session ID so next message starts fresh with summary context
|
|
296
276
|
const newSessionId = uuidv4();
|
|
297
277
|
db.prepare('UPDATE tabs SET session_id = ? WHERE id = ?').run(newSessionId, tab.id);
|
|
298
278
|
logger.info(`[${tab.name}] Context compacted — new session ${newSessionId.slice(0, 8)}...`);
|
|
299
|
-
// Continue with original goal using the summary as context
|
|
279
|
+
// Continue with original goal using the summary as in-prompt context (not persisted)
|
|
300
280
|
const continuationPrompt = `[CONTEXT RESTORED FROM PREVIOUS SESSION]\n${summaryResult.text}\n\n[Continue the original task: "${enrichedPrompt.slice(0, 500)}"]`;
|
|
301
281
|
this.sendMessage(tab.name, continuationPrompt, { onTextChunk, _compactionDepth: currentDepth + 1 }).then(resolve).catch(reject);
|
|
302
282
|
}).catch(err => {
|
|
@@ -318,13 +298,6 @@ export class TabManager {
|
|
|
318
298
|
logger.warn('Delegation completion check failed:', err);
|
|
319
299
|
});
|
|
320
300
|
resolve(result);
|
|
321
|
-
// Auto-extract memories from completed sessions (fire and forget)
|
|
322
|
-
// Skip if pipe brain already handles extraction via PipeBrain.learn()
|
|
323
|
-
if (!result.error && result.text && !skipExtraction) {
|
|
324
|
-
extractMemories(this.config, tab.name, result.text, result.durationMs).catch(err => {
|
|
325
|
-
logger.error(`[${tab.name}] Memory extraction error:`, err);
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
301
|
// Process next queued message (prepend loop warning if needed)
|
|
329
302
|
if (loopWarningPending && this.messageQueues.get(tab.name)?.length) {
|
|
330
303
|
const next = this.messageQueues.get(tab.name)[0];
|
package/dist/tasks/scheduler.js
CHANGED
|
@@ -171,9 +171,6 @@ export class TaskScheduler {
|
|
|
171
171
|
await this.onNotify('Beecork health check: all systems operational');
|
|
172
172
|
}
|
|
173
173
|
break;
|
|
174
|
-
case 'memory_compaction':
|
|
175
|
-
logger.info('System event: memory compaction (not yet implemented)');
|
|
176
|
-
break;
|
|
177
174
|
default:
|
|
178
175
|
logger.warn(`Unknown system event: ${job.message}`);
|
|
179
176
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -10,22 +10,15 @@ export interface ClaudeCodeConfig {
|
|
|
10
10
|
computerUse?: boolean;
|
|
11
11
|
}
|
|
12
12
|
export interface MemoryConfig {
|
|
13
|
-
enabled: boolean;
|
|
14
13
|
dbPath: string;
|
|
15
14
|
maxLongTermEntries: number;
|
|
16
15
|
}
|
|
17
16
|
export interface TabConfig {
|
|
18
17
|
workingDir: string;
|
|
19
|
-
/** Reserved for future use. Currently has no effect — all sessions run in 'yolo' mode. */
|
|
20
|
-
approvalMode: ApprovalMode;
|
|
21
|
-
/** Reserved for future use. Currently has no effect. */
|
|
22
|
-
approvalTimeoutMinutes: number;
|
|
23
18
|
}
|
|
24
|
-
export type ApprovalMode = 'yolo' | 'ask' | 'auto-safe';
|
|
25
19
|
export interface TabTemplate {
|
|
26
20
|
workingDir?: string;
|
|
27
21
|
systemPrompt?: string;
|
|
28
|
-
approvalMode?: ApprovalMode;
|
|
29
22
|
}
|
|
30
23
|
export interface WhatsAppConfig {
|
|
31
24
|
enabled: boolean;
|
|
@@ -33,15 +26,6 @@ export interface WhatsAppConfig {
|
|
|
33
26
|
sessionPath: string;
|
|
34
27
|
allowedNumbers: string[];
|
|
35
28
|
}
|
|
36
|
-
export interface PipeConfig {
|
|
37
|
-
enabled: boolean;
|
|
38
|
-
anthropicApiKey: string;
|
|
39
|
-
routingModel: string;
|
|
40
|
-
complexModel: string;
|
|
41
|
-
confidenceThreshold: number;
|
|
42
|
-
projectScanPaths: string[];
|
|
43
|
-
maxFollowUps: number;
|
|
44
|
-
}
|
|
45
29
|
export interface VoiceConfig {
|
|
46
30
|
sttProvider: 'whisper-api' | 'none';
|
|
47
31
|
sttApiKey?: string;
|
|
@@ -90,7 +74,8 @@ export interface BeecorkConfig {
|
|
|
90
74
|
tabs: Record<string, TabConfig>;
|
|
91
75
|
tabTemplates?: Record<string, TabTemplate>;
|
|
92
76
|
memory: MemoryConfig;
|
|
93
|
-
|
|
77
|
+
/** Directories to scan for projects on startup (defaults to ~/Coding, ~/Projects, etc.) */
|
|
78
|
+
projectScanPaths: string[];
|
|
94
79
|
voice?: VoiceConfig;
|
|
95
80
|
groups?: GroupConfig;
|
|
96
81
|
notifications?: NotificationConfig[];
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beecork",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "Claude Code always-on infrastructure — a phone number, a memory, and an alarm clock",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"beecork": "dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"engines": {
|
|
10
|
-
"node": ">=
|
|
10
|
+
"node": ">=22"
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
13
|
"build": "tsc -p tsconfig.build.json && node -e \"require('fs').chmodSync('dist/index.js', 0o755)\"",
|
|
@@ -19,29 +19,27 @@
|
|
|
19
19
|
"prepublishOnly": "npm test && npm run build"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@
|
|
23
|
-
"@modelcontextprotocol/sdk": "^1.28.0",
|
|
22
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
24
23
|
"@whiskeysockets/baileys": "^6.7.0",
|
|
25
24
|
"better-sqlite3": "^12.8.0",
|
|
26
|
-
"commander": "^
|
|
27
|
-
"discord.js": "^14.
|
|
28
|
-
"node-cron": "^4.
|
|
25
|
+
"commander": "^14.0.3",
|
|
26
|
+
"discord.js": "^14.26.2",
|
|
27
|
+
"node-cron": "^4.2.1",
|
|
29
28
|
"node-telegram-bot-api": "^0.67.0",
|
|
30
|
-
"uuid": "^
|
|
29
|
+
"uuid": "^13.0.0"
|
|
31
30
|
},
|
|
32
31
|
"devDependencies": {
|
|
33
32
|
"@tailwindcss/cli": "^4.2.2",
|
|
34
|
-
"@types/better-sqlite3": "^7.6.
|
|
35
|
-
"@types/node": "^
|
|
36
|
-
"@types/node-cron": "^3.0.
|
|
37
|
-
"@types/node-telegram-bot-api": "^0.64.
|
|
38
|
-
"
|
|
39
|
-
"eslint": "^10.1.0",
|
|
33
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
34
|
+
"@types/node": "^24.0.0",
|
|
35
|
+
"@types/node-cron": "^3.0.11",
|
|
36
|
+
"@types/node-telegram-bot-api": "^0.64.14",
|
|
37
|
+
"eslint": "^10.2.0",
|
|
40
38
|
"tailwindcss": "^4.2.2",
|
|
41
|
-
"tsx": "^4.
|
|
42
|
-
"typescript": "^
|
|
43
|
-
"typescript-eslint": "^8.58.
|
|
44
|
-
"vitest": "^
|
|
39
|
+
"tsx": "^4.21.0",
|
|
40
|
+
"typescript": "^6.0.2",
|
|
41
|
+
"typescript-eslint": "^8.58.1",
|
|
42
|
+
"vitest": "^4.1.4"
|
|
45
43
|
},
|
|
46
44
|
"files": [
|
|
47
45
|
"dist/"
|
package/dist/machines/index.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { getMachineId, registerThisMachine, listMachines, type Machine } from './registry.js';
|
package/dist/machines/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { getMachineId, registerThisMachine, listMachines } from './registry.js';
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export interface Machine {
|
|
2
|
-
id: string;
|
|
3
|
-
name: string;
|
|
4
|
-
host: string | null;
|
|
5
|
-
sshUser: string | null;
|
|
6
|
-
projectPaths: string[];
|
|
7
|
-
isPrimary: boolean;
|
|
8
|
-
lastSeenAt: string;
|
|
9
|
-
}
|
|
10
|
-
/** Get or create this machine's unique ID */
|
|
11
|
-
export declare function getMachineId(): string;
|
|
12
|
-
/** Register this machine in the database */
|
|
13
|
-
export declare function registerThisMachine(projectPaths: string[]): Machine;
|
|
14
|
-
/** List all machines */
|
|
15
|
-
export declare function listMachines(): Machine[];
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import os from 'node:os';
|
|
2
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
-
import { getDb } from '../db/index.js';
|
|
4
|
-
import { getBeecorkHome } from '../util/paths.js';
|
|
5
|
-
import { logger } from '../util/logger.js';
|
|
6
|
-
import fs from 'node:fs';
|
|
7
|
-
const MACHINE_ID_PATH = `${getBeecorkHome()}/machine-id`;
|
|
8
|
-
/** Get or create this machine's unique ID */
|
|
9
|
-
export function getMachineId() {
|
|
10
|
-
if (fs.existsSync(MACHINE_ID_PATH)) {
|
|
11
|
-
return fs.readFileSync(MACHINE_ID_PATH, 'utf-8').trim();
|
|
12
|
-
}
|
|
13
|
-
const id = uuidv4();
|
|
14
|
-
fs.writeFileSync(MACHINE_ID_PATH, id, { mode: 0o600 });
|
|
15
|
-
return id;
|
|
16
|
-
}
|
|
17
|
-
/** Register this machine in the database */
|
|
18
|
-
export function registerThisMachine(projectPaths) {
|
|
19
|
-
const db = getDb();
|
|
20
|
-
const id = getMachineId();
|
|
21
|
-
const name = os.hostname();
|
|
22
|
-
db.prepare(`
|
|
23
|
-
INSERT INTO machines (id, name, project_paths, last_seen_at)
|
|
24
|
-
VALUES (?, ?, ?, datetime('now'))
|
|
25
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
26
|
-
name = excluded.name,
|
|
27
|
-
project_paths = excluded.project_paths,
|
|
28
|
-
last_seen_at = datetime('now')
|
|
29
|
-
`).run(id, name, JSON.stringify(projectPaths));
|
|
30
|
-
logger.info(`Machine registered: ${name} (${id.slice(0, 8)}) with ${projectPaths.length} project paths`);
|
|
31
|
-
return { id, name, host: null, sshUser: null, projectPaths, isPrimary: false, lastSeenAt: new Date().toISOString() };
|
|
32
|
-
}
|
|
33
|
-
/** List all machines */
|
|
34
|
-
export function listMachines() {
|
|
35
|
-
const db = getDb();
|
|
36
|
-
const rows = db.prepare('SELECT * FROM machines ORDER BY is_primary DESC, name').all();
|
|
37
|
-
return rows.map(r => ({
|
|
38
|
-
id: r.id,
|
|
39
|
-
name: r.name,
|
|
40
|
-
host: r.host,
|
|
41
|
-
sshUser: r.ssh_user,
|
|
42
|
-
projectPaths: JSON.parse(r.project_paths || '[]'),
|
|
43
|
-
isPrimary: !!r.is_primary,
|
|
44
|
-
lastSeenAt: r.last_seen_at,
|
|
45
|
-
}));
|
|
46
|
-
}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import type { BeecorkConfig } from '../types.js';
|
|
2
|
-
/** Auto-extract memories from a completed session */
|
|
3
|
-
export declare function extractMemories(config: BeecorkConfig, tabName: string, sessionText: string, durationMs: number): Promise<void>;
|
|
4
|
-
/** Inject relevant memories into a prompt */
|
|
5
|
-
export declare function getRelevantMemories(tabName: string): string[];
|
package/dist/memory/extractor.js
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import { getDb } from '../db/index.js';
|
|
3
|
-
import { logger } from '../util/logger.js';
|
|
4
|
-
const EXTRACTION_PROMPT = `Extract 0-5 key facts, decisions, or outcomes from this session transcript that would be useful in future sessions. Output ONLY a JSON array of strings. If nothing is worth remembering, output an empty array [].
|
|
5
|
-
|
|
6
|
-
Examples of what to extract:
|
|
7
|
-
- Server addresses, credentials, file paths
|
|
8
|
-
- User preferences ("prefers deployments after 11pm")
|
|
9
|
-
- Decisions made ("chose PostgreSQL over MySQL for X reason")
|
|
10
|
-
- Outcomes ("deploy succeeded", "bug was in auth middleware")
|
|
11
|
-
|
|
12
|
-
Session transcript:
|
|
13
|
-
`;
|
|
14
|
-
/** Auto-extract memories from a completed session */
|
|
15
|
-
export async function extractMemories(config, tabName, sessionText, durationMs) {
|
|
16
|
-
// Only extract from non-trivial sessions
|
|
17
|
-
if (durationMs < 10000 || sessionText.length < 200)
|
|
18
|
-
return;
|
|
19
|
-
// Rate limit: check if we extracted recently for this tab
|
|
20
|
-
const db = getDb();
|
|
21
|
-
const recent = db.prepare(`SELECT COUNT(*) as count FROM memories
|
|
22
|
-
WHERE tab_name = ? AND source = 'auto'
|
|
23
|
-
AND created_at > datetime('now', '-5 minutes')`).get(tabName);
|
|
24
|
-
if (recent.count > 0) {
|
|
25
|
-
logger.debug(`[${tabName}] Skipping memory extraction — too recent`);
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
try {
|
|
29
|
-
const prompt = EXTRACTION_PROMPT + sessionText.slice(0, 5000); // Limit transcript size
|
|
30
|
-
const facts = await runExtractionSession(config, prompt);
|
|
31
|
-
if (facts.length === 0)
|
|
32
|
-
return;
|
|
33
|
-
for (const fact of facts) {
|
|
34
|
-
db.prepare('INSERT INTO memories (content, tab_name, source) VALUES (?, ?, ?)').run(fact, tabName, 'auto');
|
|
35
|
-
}
|
|
36
|
-
// Enforce maxLongTermEntries limit
|
|
37
|
-
const maxEntries = config.memory.maxLongTermEntries ?? 1000;
|
|
38
|
-
const count = db.prepare('SELECT COUNT(*) as c FROM memories').get().c;
|
|
39
|
-
if (count > maxEntries) {
|
|
40
|
-
const excess = count - maxEntries;
|
|
41
|
-
db.prepare('DELETE FROM memories WHERE rowid IN (SELECT rowid FROM memories ORDER BY created_at ASC LIMIT ?)').run(excess);
|
|
42
|
-
logger.info(`[${tabName}] Evicted ${excess} oldest memories (limit: ${maxEntries})`);
|
|
43
|
-
}
|
|
44
|
-
logger.info(`[${tabName}] Auto-extracted ${facts.length} memories`);
|
|
45
|
-
}
|
|
46
|
-
catch (err) {
|
|
47
|
-
logger.error(`[${tabName}] Memory extraction failed:`, err);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
/** Inject relevant memories into a prompt */
|
|
51
|
-
export function getRelevantMemories(tabName) {
|
|
52
|
-
const db = getDb();
|
|
53
|
-
// Get recent global memories + tab-specific memories
|
|
54
|
-
const memories = db.prepare(`SELECT content FROM memories
|
|
55
|
-
WHERE tab_name IS NULL OR tab_name = ?
|
|
56
|
-
ORDER BY created_at DESC LIMIT 20`).all(tabName);
|
|
57
|
-
return memories.map(m => m.content);
|
|
58
|
-
}
|
|
59
|
-
async function runExtractionSession(config, prompt) {
|
|
60
|
-
// Prefer direct API call (cheaper, faster) when API key is available
|
|
61
|
-
if (config.pipe?.anthropicApiKey) {
|
|
62
|
-
return runExtractionViaApi(config.pipe.anthropicApiKey, config.pipe.routingModel, prompt);
|
|
63
|
-
}
|
|
64
|
-
// Fallback: spawn Claude Code subprocess
|
|
65
|
-
return runExtractionViaSubprocess(config, prompt);
|
|
66
|
-
}
|
|
67
|
-
function parseFactsFromText(text) {
|
|
68
|
-
const jsonMatch = text.match(/\[[\s\S]*?\]/);
|
|
69
|
-
if (jsonMatch) {
|
|
70
|
-
const facts = JSON.parse(jsonMatch[0]);
|
|
71
|
-
if (Array.isArray(facts) && facts.every(f => typeof f === 'string')) {
|
|
72
|
-
return facts.slice(0, 5);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return [];
|
|
76
|
-
}
|
|
77
|
-
let cachedClient = null;
|
|
78
|
-
let cachedApiKey = '';
|
|
79
|
-
async function runExtractionViaApi(apiKey, model, prompt) {
|
|
80
|
-
const { default: Anthropic } = await import('@anthropic-ai/sdk');
|
|
81
|
-
if (!cachedClient || cachedApiKey !== apiKey) {
|
|
82
|
-
cachedClient = new Anthropic({ apiKey });
|
|
83
|
-
cachedApiKey = apiKey;
|
|
84
|
-
}
|
|
85
|
-
const client = cachedClient;
|
|
86
|
-
try {
|
|
87
|
-
const response = await client.messages.create({
|
|
88
|
-
model,
|
|
89
|
-
max_tokens: 500,
|
|
90
|
-
system: 'Extract 0-5 key facts from this session transcript worth remembering across sessions. Output ONLY a JSON array of strings. If nothing is worth remembering, output [].',
|
|
91
|
-
messages: [{ role: 'user', content: prompt }],
|
|
92
|
-
}, { timeout: 15000 });
|
|
93
|
-
const text = response.content.find((b) => b.type === 'text')?.text ?? '[]';
|
|
94
|
-
return parseFactsFromText(text);
|
|
95
|
-
}
|
|
96
|
-
catch (err) {
|
|
97
|
-
logger.warn('API-based memory extraction failed, falling back to subprocess:', err);
|
|
98
|
-
return [];
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
async function runExtractionViaSubprocess(config, prompt) {
|
|
102
|
-
return new Promise((resolve, reject) => {
|
|
103
|
-
let resolved = false;
|
|
104
|
-
const safeResolve = (val) => { if (!resolved) {
|
|
105
|
-
resolved = true;
|
|
106
|
-
resolve(val);
|
|
107
|
-
} };
|
|
108
|
-
const safeReject = (err) => { if (!resolved) {
|
|
109
|
-
resolved = true;
|
|
110
|
-
reject(err);
|
|
111
|
-
} };
|
|
112
|
-
const args = [
|
|
113
|
-
'-p',
|
|
114
|
-
'--output-format', 'stream-json',
|
|
115
|
-
'--verbose',
|
|
116
|
-
...config.claudeCode.defaultFlags,
|
|
117
|
-
'--no-session-persistence',
|
|
118
|
-
prompt,
|
|
119
|
-
];
|
|
120
|
-
const proc = spawn(config.claudeCode.bin, args, {
|
|
121
|
-
cwd: process.cwd(),
|
|
122
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
123
|
-
});
|
|
124
|
-
let output = '';
|
|
125
|
-
let stdoutBuffer = '';
|
|
126
|
-
proc.stdout.on('data', (chunk) => {
|
|
127
|
-
stdoutBuffer += chunk.toString();
|
|
128
|
-
const lines = stdoutBuffer.split('\n');
|
|
129
|
-
stdoutBuffer = lines.pop() || '';
|
|
130
|
-
for (const line of lines) {
|
|
131
|
-
if (!line.trim())
|
|
132
|
-
continue;
|
|
133
|
-
try {
|
|
134
|
-
const event = JSON.parse(line);
|
|
135
|
-
if (event.type === 'result' && event.result) {
|
|
136
|
-
output = event.result;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
catch { /* skip non-JSON */ }
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
proc.on('exit', () => {
|
|
143
|
-
clearTimeout(timer);
|
|
144
|
-
try {
|
|
145
|
-
safeResolve(parseFactsFromText(output));
|
|
146
|
-
}
|
|
147
|
-
catch {
|
|
148
|
-
safeResolve([]);
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
proc.on('error', safeReject);
|
|
152
|
-
const timer = setTimeout(() => {
|
|
153
|
-
proc.kill('SIGTERM');
|
|
154
|
-
safeResolve([]);
|
|
155
|
-
}, 30000);
|
|
156
|
-
});
|
|
157
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { RouteDecision, GoalEvaluation, KnowledgeEntry, Project } from './types.js';
|
|
2
|
-
export declare class PipeAnthropicClient {
|
|
3
|
-
private client;
|
|
4
|
-
private routingModel;
|
|
5
|
-
private complexModel;
|
|
6
|
-
constructor(apiKey: string, routingModel: string, complexModel: string);
|
|
7
|
-
/** Route a message to the right project/tab (Haiku — fast, cheap) */
|
|
8
|
-
route(message: string, projects: Project[], recentRouting: string[]): Promise<RouteDecision>;
|
|
9
|
-
/** Evaluate if a goal was achieved (Sonnet — needs reasoning) */
|
|
10
|
-
evaluateGoal(originalGoal: string, response: string): Promise<GoalEvaluation>;
|
|
11
|
-
/** Extract knowledge from a conversation (Haiku — fast) */
|
|
12
|
-
extractKnowledge(conversation: string, existingFacts: string[]): Promise<KnowledgeEntry[]>;
|
|
13
|
-
private complete;
|
|
14
|
-
}
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import Anthropic from '@anthropic-ai/sdk';
|
|
2
|
-
import { logger } from '../util/logger.js';
|
|
3
|
-
import { retryWithBackoff } from '../util/retry.js';
|
|
4
|
-
export class PipeAnthropicClient {
|
|
5
|
-
client;
|
|
6
|
-
routingModel;
|
|
7
|
-
complexModel;
|
|
8
|
-
constructor(apiKey, routingModel, complexModel) {
|
|
9
|
-
this.client = new Anthropic({ apiKey });
|
|
10
|
-
this.routingModel = routingModel;
|
|
11
|
-
this.complexModel = complexModel;
|
|
12
|
-
}
|
|
13
|
-
/** Route a message to the right project/tab (Haiku — fast, cheap) */
|
|
14
|
-
async route(message, projects, recentRouting) {
|
|
15
|
-
const projectList = projects.map(p => `- ${p.name}: ${p.path}${p.languages?.length ? ` (${p.languages.join(', ')})` : ''}${p.description ? ` — ${p.description}` : ''}`).join('\n');
|
|
16
|
-
const response = await this.complete(`You are a message router. Given a user message, determine which project it relates to.
|
|
17
|
-
|
|
18
|
-
Available projects:
|
|
19
|
-
${projectList || '(no projects discovered yet)'}
|
|
20
|
-
|
|
21
|
-
Recent routing decisions:
|
|
22
|
-
${recentRouting.slice(0, 5).join('\n') || '(none)'}
|
|
23
|
-
|
|
24
|
-
Respond with ONLY valid JSON: {"tabName": "project-name", "projectPath": "/path", "confidence": 0.0-1.0, "reason": "brief explanation", "needsConfirmation": false}
|
|
25
|
-
|
|
26
|
-
If the message is a general question not related to any project, use tabName "default" with the user's home directory.
|
|
27
|
-
If unsure which project, set confidence below 0.5 and needsConfirmation to true.`, message, 'haiku');
|
|
28
|
-
try {
|
|
29
|
-
return JSON.parse(response);
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
logger.warn('Failed to parse routing response:', response);
|
|
33
|
-
return { tabName: 'default', projectPath: null, confidence: 0.3, reason: 'Could not parse routing', needsConfirmation: true };
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
/** Evaluate if a goal was achieved (Sonnet — needs reasoning) */
|
|
37
|
-
async evaluateGoal(originalGoal, response) {
|
|
38
|
-
const result = await this.complete(`You evaluate whether a coding assistant achieved a user's goal.
|
|
39
|
-
|
|
40
|
-
Respond with ONLY valid JSON: {"status": "done|partial|failed", "reason": "brief explanation", "followUp": "what to do next" or null}
|
|
41
|
-
|
|
42
|
-
Consider:
|
|
43
|
-
- Did it actually make changes, or just describe what to do?
|
|
44
|
-
- Did it complete the full task or just one step?
|
|
45
|
-
- Are there obvious remaining steps?
|
|
46
|
-
|
|
47
|
-
If the response is a simple answer to a question (not a task), status is "done".`, `Original goal: "${originalGoal}"\n\nAssistant's response:\n${response.slice(0, 3000)}`, 'sonnet');
|
|
48
|
-
try {
|
|
49
|
-
return JSON.parse(result);
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
logger.warn('Failed to parse routing response:', result);
|
|
53
|
-
return { status: 'done', reason: 'Could not evaluate', followUp: null };
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
/** Extract knowledge from a conversation (Haiku — fast) */
|
|
57
|
-
async extractKnowledge(conversation, existingFacts) {
|
|
58
|
-
const response = await this.complete(`Extract structured knowledge from this conversation worth remembering across sessions.
|
|
59
|
-
|
|
60
|
-
Already known facts:
|
|
61
|
-
${existingFacts.slice(0, 10).join('\n') || '(none)'}
|
|
62
|
-
|
|
63
|
-
Respond with ONLY a valid JSON array: [{"content": "...", "category": "project|preference|decision|fact"}]
|
|
64
|
-
Return [] if nothing new is worth remembering. Max 5 entries.`, conversation.slice(0, 5000), 'haiku');
|
|
65
|
-
try {
|
|
66
|
-
const entries = JSON.parse(response);
|
|
67
|
-
if (Array.isArray(entries)) {
|
|
68
|
-
return entries.slice(0, 5).map((e) => ({
|
|
69
|
-
content: e.content,
|
|
70
|
-
category: (e.category || 'fact'),
|
|
71
|
-
tabName: null,
|
|
72
|
-
source: 'pipe',
|
|
73
|
-
}));
|
|
74
|
-
}
|
|
75
|
-
return [];
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
return [];
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
async complete(systemPrompt, userMessage, model) {
|
|
82
|
-
const modelId = model === 'haiku' ? this.routingModel : this.complexModel;
|
|
83
|
-
try {
|
|
84
|
-
const response = await retryWithBackoff(() => this.client.messages.create({
|
|
85
|
-
model: modelId,
|
|
86
|
-
max_tokens: 500,
|
|
87
|
-
system: systemPrompt,
|
|
88
|
-
messages: [{ role: 'user', content: userMessage }],
|
|
89
|
-
}, { timeout: 30000 }), [1000, 5000, 15000], `Pipe API call (${model})`);
|
|
90
|
-
const textBlock = response.content.find(b => b.type === 'text');
|
|
91
|
-
return textBlock?.text ?? '';
|
|
92
|
-
}
|
|
93
|
-
catch (err) {
|
|
94
|
-
logger.error(`Pipe API call failed (${model}):`, err);
|
|
95
|
-
throw err;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
package/dist/pipe/brain.d.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { TabManager } from '../session/manager.js';
|
|
2
|
-
import type { BeecorkConfig } from '../types.js';
|
|
3
|
-
import type { ChatContext, PipeResult } from './types.js';
|
|
4
|
-
export declare class PipeBrain {
|
|
5
|
-
private client;
|
|
6
|
-
private memory;
|
|
7
|
-
private config;
|
|
8
|
-
private tabManager;
|
|
9
|
-
private notifyCallback;
|
|
10
|
-
constructor(config: BeecorkConfig, tabManager: TabManager);
|
|
11
|
-
setNotifyCallback(cb: (text: string) => Promise<void>): void;
|
|
12
|
-
/** Main entry point: process a user message with intelligence */
|
|
13
|
-
process(message: string, context: ChatContext): Promise<PipeResult>;
|
|
14
|
-
/** Route a message to the right project/tab */
|
|
15
|
-
private route;
|
|
16
|
-
/** Evaluate if the goal was achieved and send follow-ups if needed */
|
|
17
|
-
private evaluateAndFollowUp;
|
|
18
|
-
/** Learn from a completed conversation */
|
|
19
|
-
private learn;
|
|
20
|
-
/** Discover projects on the filesystem */
|
|
21
|
-
discoverProjects(): Promise<number>;
|
|
22
|
-
}
|