beecork 1.3.10 → 1.4.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/index.js CHANGED
@@ -166,12 +166,13 @@ program
166
166
  .description('Set up WhatsApp — enter phone number, then scan QR code to pair')
167
167
  .action(async () => {
168
168
  const readline = await import('node:readline');
169
+ const fs = await import('node:fs');
169
170
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
170
171
  const ask = (q, def) => new Promise(r => rl.question(def ? `${q} [${def}]: ` : `${q}: `, a => r(a.trim() || def || '')));
171
172
  console.log('\nWhatsApp Setup\n');
172
173
  console.log(' WhatsApp connects via QR code scanning (like WhatsApp Web).');
173
- console.log(' After setup, run "beecork start" and the QR code will appear');
174
- console.log(' in your terminal. Scan it with your phone to pair.\n');
174
+ console.log(' After entering your phone number, a QR code will appear.');
175
+ console.log(' Scan it with your phone to pair.\n');
175
176
  const number = await ask('Your WhatsApp phone number (e.g., 14155551234)');
176
177
  if (!number) {
177
178
  console.log('No number provided. Cancelled.');
@@ -181,18 +182,49 @@ program
181
182
  const { getConfig, saveConfig } = await import('./config.js');
182
183
  const { getBeecorkHome } = await import('./util/paths.js');
183
184
  const config = getConfig();
185
+ const sessionPath = `${getBeecorkHome()}/whatsapp-session`;
184
186
  config.whatsapp = {
185
187
  enabled: true,
186
188
  mode: 'baileys',
187
- sessionPath: `${getBeecorkHome()}/whatsapp-session`,
189
+ sessionPath,
188
190
  allowedNumbers: [number],
189
191
  };
190
192
  saveConfig(config);
191
- console.log('\n✓ WhatsApp configured.');
192
- console.log(' Next: run "beecork start" — the QR code will appear in your terminal.');
193
- console.log(' Scan it with your phone to pair. Once paired, press Ctrl+C and');
194
- console.log(' run "beecork start" again for background operation.\n');
193
+ console.log('\n✓ Config saved. Connecting to WhatsApp...\n');
195
194
  rl.close();
195
+ // Pair immediately — show QR code in this terminal
196
+ try {
197
+ const { default: makeWASocket, useMultiFileAuthState, DisconnectReason } = await import('@whiskeysockets/baileys');
198
+ fs.mkdirSync(sessionPath, { recursive: true, mode: 0o700 });
199
+ const { state, saveCreds } = await useMultiFileAuthState(sessionPath);
200
+ const sock = makeWASocket({
201
+ auth: state,
202
+ printQRInTerminal: true,
203
+ });
204
+ sock.ev.on('creds.update', saveCreds);
205
+ sock.ev.on('connection.update', (update) => {
206
+ if (update.connection === 'open') {
207
+ console.log('\n✓ WhatsApp paired successfully!');
208
+ console.log(' You can now start the daemon: beecork start\n');
209
+ sock.end(undefined);
210
+ process.exit(0);
211
+ }
212
+ if (update.connection === 'close') {
213
+ const reason = update.lastDisconnect?.error?.output?.statusCode;
214
+ if (reason === DisconnectReason.loggedOut) {
215
+ console.log('\n✗ WhatsApp logged out. Please try again.\n');
216
+ process.exit(1);
217
+ }
218
+ }
219
+ });
220
+ console.log('Scan the QR code above with your phone (WhatsApp → Linked Devices → Link a Device)');
221
+ console.log('Waiting for pairing... (Ctrl+C to cancel)\n');
222
+ }
223
+ catch (err) {
224
+ console.error('Failed to connect to WhatsApp:', err instanceof Error ? err.message : err);
225
+ console.log('\nConfig is saved. You can try pairing later by running: beecork whatsapp');
226
+ process.exit(1);
227
+ }
196
228
  });
197
229
  program
198
230
  .command('webhook')
@@ -304,8 +336,6 @@ program
304
336
  console.log(` workingDir: ${tmpl.workingDir}`);
305
337
  if (tmpl.systemPrompt)
306
338
  console.log(` systemPrompt: "${tmpl.systemPrompt.slice(0, 80)}${tmpl.systemPrompt.length > 80 ? '...' : ''}"`);
307
- if (tmpl.approvalMode)
308
- console.log(` approvalMode: ${tmpl.approvalMode}`);
309
339
  }
310
340
  console.log('');
311
341
  });
@@ -375,27 +405,6 @@ program
375
405
  const { getActivitySummary, formatActivitySummary } = await import('./observability/analytics.js');
376
406
  console.log(formatActivitySummary(getActivitySummary(h)));
377
407
  });
378
- program
379
- .command('machines')
380
- .description('List registered machines')
381
- .action(async () => {
382
- const { listMachines } = await import('./machines/index.js');
383
- const machines = listMachines();
384
- if (machines.length === 0) {
385
- console.log('No machines registered. Start the daemon to register this machine.');
386
- return;
387
- }
388
- console.log(`\n${machines.length} machine(s):\n`);
389
- for (const m of machines) {
390
- const primary = m.isPrimary ? ' (primary)' : '';
391
- const remote = m.host ? ` — ${m.sshUser}@${m.host}` : ' — local';
392
- console.log(` ${m.name}${primary}${remote}`);
393
- for (const p of m.projectPaths) {
394
- console.log(` ${p}`);
395
- }
396
- }
397
- console.log('');
398
- });
399
408
  program
400
409
  .command('folders')
401
410
  .alias('projects')
@@ -322,11 +322,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
322
322
  required: ['tabName'],
323
323
  },
324
324
  },
325
- {
326
- name: 'beecork_machines',
327
- description: 'List registered machines and their folder paths. Shows which machine handles which folders.',
328
- inputSchema: { type: 'object', properties: {} },
329
- },
330
325
  {
331
326
  name: 'beecork_delegate',
332
327
  description: 'Delegate a task to another tab. The target tab runs independently and the result is automatically sent back to the source tab when complete. Use this for tasks that need their own working directory or context.',
@@ -493,6 +488,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
493
488
  }
494
489
  // Default: existing tab memory behavior
495
490
  const fullContent = category ? `[${category}] ${content}` : content;
491
+ // Dedup: skip insert if an identical fact already exists
492
+ const existing = db.prepare('SELECT id FROM memories WHERE content = ? AND tab_name IS NULL LIMIT 1').get(fullContent);
493
+ if (existing) {
494
+ return ok(`Already remembered: "${fullContent}"`);
495
+ }
496
496
  db.prepare('INSERT INTO memories (content, source) VALUES (?, ?)').run(fullContent, 'tool');
497
497
  return ok(`Remembered: "${fullContent}"`);
498
498
  }
@@ -761,11 +761,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
761
761
  };
762
762
  return ok(JSON.stringify(info, null, 2));
763
763
  }
764
- case 'beecork_machines': {
765
- const { listMachines } = await import('../machines/index.js');
766
- const machines = listMachines();
767
- return ok(JSON.stringify(machines, null, 2));
768
- }
769
764
  case 'beecork_delegate': {
770
765
  const { tabName, message, returnToTab } = args;
771
766
  try {
@@ -23,7 +23,6 @@ export declare class TabManager {
23
23
  resume?: boolean;
24
24
  onTextChunk?: (text: string) => void;
25
25
  onToolUse?: (toolName: string, toolInput: Record<string, unknown>) => void;
26
- skipExtraction?: boolean;
27
26
  projectPath?: string;
28
27
  _compactionDepth?: number;
29
28
  }): Promise<SendResult>;
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "beecork",
3
- "version": "1.3.10",
3
+ "version": "1.4.0",
4
4
  "description": "Claude Code always-on infrastructure — a phone number, a memory, and an alarm clock",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,7 +19,6 @@
19
19
  "prepublishOnly": "npm test && npm run build"
20
20
  },
21
21
  "dependencies": {
22
- "@anthropic-ai/sdk": "^0.80.0",
23
22
  "@modelcontextprotocol/sdk": "^1.28.0",
24
23
  "@whiskeysockets/baileys": "^6.7.0",
25
24
  "better-sqlite3": "^12.8.0",
@@ -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
- }