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.
@@ -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?.skipExtraction, options?.onToolUse, options?._compactionDepth);
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, skipExtraction, onToolUse, compactionDepth) {
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
- // Log approval mode (full interactive approval coming in a future release)
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
- let enrichedPrompt = prompt;
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];
@@ -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
- pipe: PipeConfig;
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.11",
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": ">=18"
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
- "@anthropic-ai/sdk": "^0.80.0",
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": "^13.0.0",
27
- "discord.js": "^14.25.1",
28
- "node-cron": "^4.0.0",
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": "^11.0.0"
29
+ "uuid": "^13.0.0"
31
30
  },
32
31
  "devDependencies": {
33
32
  "@tailwindcss/cli": "^4.2.2",
34
- "@types/better-sqlite3": "^7.6.0",
35
- "@types/node": "^22.0.0",
36
- "@types/node-cron": "^3.0.0",
37
- "@types/node-telegram-bot-api": "^0.64.0",
38
- "@types/uuid": "^10.0.0",
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.0.0",
42
- "typescript": "^5.7.0",
43
- "typescript-eslint": "^8.58.0",
44
- "vitest": "^3.0.0"
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/"
@@ -1 +0,0 @@
1
- export { getMachineId, registerThisMachine, listMachines, type Machine } from './registry.js';
@@ -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[];
@@ -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
- }
@@ -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
- }