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/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/commands.js +0 -16
- 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 +39 -30
- 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 +1 -2
- 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/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
|
|
174
|
-
console.log('
|
|
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
|
|
189
|
+
sessionPath,
|
|
188
190
|
allowedNumbers: [number],
|
|
189
191
|
};
|
|
190
192
|
saveConfig(config);
|
|
191
|
-
console.log('\n✓
|
|
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')
|
package/dist/mcp/server.js
CHANGED
|
@@ -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>;
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beecork",
|
|
3
|
-
"version": "1.
|
|
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",
|
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
|
-
}
|