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 CHANGED
@@ -25,7 +25,7 @@ Message Claude Code from Telegram at 2am — it wakes up, does the work, message
25
25
  - **Task scheduling** — Tell Claude Code to set up recurring tasks via MCP tools — it wakes up, runs the task, reports back.
26
26
  - **Memory** — Cross-session memory so Claude Code never loses context.
27
27
  - **MCP server** — 38 tools Claude Code can call to manage tabs, memory, cron jobs, watchers, media, folders, and more.
28
- - **Smart routing** — Pipe brain routes messages to the right folder and tab, tracks goals, learns from your usage.
28
+ - **Smart routing** — Messages are automatically routed to the right project folder and tab using project-name detection, sticky conversation context, and learned patterns. No API key required.
29
29
  - **Background service** — Runs as a launchd (macOS), systemd (Linux), or Task Scheduler (Windows) service. Starts on login, runs silently.
30
30
 
31
31
  ## Quick Start
@@ -90,7 +90,6 @@ beecork media setup # Configure media generators
90
90
  beecork activity # View activity summary
91
91
  beecork history # Show activity timeline
92
92
  beecork folders # List discovered folders
93
- beecork machines # List registered machines
94
93
  beecork templates # List tab templates
95
94
  beecork store search <q> # Search community extensions
96
95
  beecork store install <p> # Install a community package
@@ -113,9 +112,9 @@ Local install is `npm install -g beecork`. VPS requires the same plus a properly
113
112
  ## Architecture
114
113
 
115
114
  ```
116
- Telegram/WhatsApp/Discord
115
+ Telegram/WhatsApp/Discord/Webhook
117
116
  |
118
- Pipe Brain (intelligent routing)
117
+ Shared pipeline (deterministic router)
119
118
  |
120
119
  Daemon (always-on)
121
120
  |
@@ -133,20 +133,6 @@ export async function handleSharedCommand(ctx, tabManager) {
133
133
  return { handled: true, response: `Tab "${tabName}" not found.` };
134
134
  return { handled: true, response: formatHandoffInfo(info) };
135
135
  }
136
- // /machines
137
- if (text === '/machines' || text.startsWith('/machines@')) {
138
- const { listMachines } = await import('../machines/index.js');
139
- const machines = listMachines();
140
- if (machines.length === 0)
141
- return { handled: true, response: 'No machines registered.' };
142
- const list = machines.map(m => {
143
- const primary = m.isPrimary ? ' ⭐' : '';
144
- const remote = m.host ? ` (${m.sshUser}@${m.host})` : ' (local)';
145
- const paths = m.projectPaths.slice(0, 3).join(', ');
146
- return `• ${m.name}${primary}${remote}\n Projects: ${paths}`;
147
- }).join('\n\n');
148
- return { handled: true, response: `🖥 ${machines.length} machine(s):\n\n${list}` };
149
- }
150
136
  // /folders (also accept legacy /projects)
151
137
  if (text === '/folders' || text === '/projects' || text.startsWith('/folders@') || text.startsWith('/projects@')) {
152
138
  const { listProjects } = await import('../projects/index.js');
@@ -299,7 +299,7 @@ export class TelegramChannel {
299
299
  await this.sendResponse(chatId, formatted.slice(0, 4000));
300
300
  return;
301
301
  }
302
- // Shared command handler (covers /tabs, /stop, /tab, /projects, /project, /newproject, /close, /fresh, /register, /link, /users, /cost, /activity, /handoff, /machines)
302
+ // Shared command handler (covers /tabs, /stop, /tab, /projects, /project, /newproject, /close, /fresh, /register, /link, /users, /cost, /activity, /handoff)
303
303
  const { handleSharedCommand } = await import('./command-handler.js');
304
304
  const result = await handleSharedCommand({
305
305
  userId: String(userId || 'default'),
@@ -348,109 +348,96 @@ export class TelegramChannel {
348
348
  let responseText;
349
349
  let responseError;
350
350
  let responseTab;
351
- if (this.ctx.pipeBrain) {
352
- // PipeBrain path is Telegram-specific (not shared in pipeline)
353
- const pipeResult = await this.ctx.pipeBrain.process(text, { chatId, userId: 0, messageId });
354
- responseText = pipeResult.response.text || '(empty response)';
355
- responseError = pipeResult.response.error;
356
- responseTab = pipeResult.tabName;
357
- if (pipeResult.decisions.length > 0) {
358
- const decisionText = pipeResult.decisions.join('\n');
359
- await this.bot.sendMessage(chatId, decisionText);
360
- }
361
- }
362
- else {
363
- // Telegram-specific: streaming message edits
364
- let streamMsgId = null;
365
- let streamBuffer = '';
366
- let lastEditTime = 0;
367
- // We need the effective tab name for the stream prefix, but it's determined
368
- // inside the pipeline. Use a mutable ref that the pipeline result will fill.
369
- let effectiveTabForStream = overrideTabName || tabName;
370
- const onTextChunk = async (chunk) => {
371
- streamBuffer += chunk;
372
- const now = Date.now();
373
- if (streamBuffer.length < 100 || now - lastEditTime < 1000)
374
- return;
375
- lastEditTime = now;
376
- try {
377
- const prefix = effectiveTabForStream !== 'default' ? `[${effectiveTabForStream}] ` : '';
378
- const preview = prefix + streamBuffer.slice(0, 4000) + (streamBuffer.length > 4000 ? '...' : '');
379
- if (!streamMsgId) {
380
- const sent = await this.bot.sendMessage(chatId, preview);
381
- streamMsgId = sent.message_id;
382
- }
383
- else {
384
- await this.bot.editMessageText(preview, { chat_id: chatId, message_id: streamMsgId });
385
- }
386
- }
387
- catch { /* edit failures are non-critical */ }
388
- };
389
- // Shared pipeline handles: routing, media prompt, progress, sendMessage, TTS
390
- const pipelineResult = await processInboundMessage({
391
- text,
392
- media,
393
- channelId: 'telegram',
394
- tabManager: this.ctx.tabManager,
395
- voiceReplyMode: this.ctx.config.voice?.replyMode,
396
- ttsProvider: this.ttsProvider,
397
- userId: String(chatId),
398
- sendProgress: (msg) => {
399
- this.bot.sendMessage(chatId, msg).catch(() => { });
400
- },
401
- overrideTabName,
402
- onTextChunk,
403
- });
404
- // Empty result means no prompt and no media
405
- if (!pipelineResult.responseText) {
406
- clearInterval(typingInterval);
407
- clearTimeout(stillWorkingTimeout);
351
+ // Telegram-specific: streaming message edits
352
+ let streamMsgId = null;
353
+ let streamBuffer = '';
354
+ let lastEditTime = 0;
355
+ // We need the effective tab name for the stream prefix, but it's determined
356
+ // inside the pipeline. Use a mutable ref that the pipeline result will fill.
357
+ let effectiveTabForStream = overrideTabName || tabName;
358
+ const onTextChunk = async (chunk) => {
359
+ streamBuffer += chunk;
360
+ const now = Date.now();
361
+ if (streamBuffer.length < 100 || now - lastEditTime < 1000)
408
362
  return;
409
- }
410
- // Update the effective tab for stream prefix (now known)
411
- effectiveTabForStream = pipelineResult.tabName;
412
- responseText = pipelineResult.responseText;
413
- responseError = pipelineResult.isError;
414
- responseTab = pipelineResult.tabName;
415
- // Telegram-specific: if streaming was active and no error, edit the final message
416
- if (streamMsgId && !responseError) {
417
- clearInterval(typingInterval);
418
- clearTimeout(stillWorkingTimeout);
419
- await this.setReaction(chatId, messageId, '✅');
420
- // Send voice if available (even with streaming)
421
- if (pipelineResult.audioPath) {
422
- await this.bot.sendVoice(chatId, pipelineResult.audioPath);
423
- if (pipelineResult.voiceOnly)
424
- return;
425
- }
426
- try {
427
- const prefix = responseTab !== 'default' ? `[${responseTab}] ` : '';
428
- const finalText = prefix + responseText;
429
- if (finalText.length <= 4096) {
430
- await this.bot.editMessageText(finalText, { chat_id: chatId, message_id: streamMsgId });
431
- }
432
- else {
433
- await this.sendResponse(chatId, responseText, responseTab);
434
- }
363
+ lastEditTime = now;
364
+ try {
365
+ const prefix = effectiveTabForStream !== 'default' ? `[${effectiveTabForStream}] ` : '';
366
+ const preview = prefix + streamBuffer.slice(0, 4000) + (streamBuffer.length > 4000 ? '...' : '');
367
+ if (!streamMsgId) {
368
+ const sent = await this.bot.sendMessage(chatId, preview);
369
+ streamMsgId = sent.message_id;
435
370
  }
436
- catch {
437
- await this.sendResponse(chatId, responseText, responseTab);
371
+ else {
372
+ await this.bot.editMessageText(preview, { chat_id: chatId, message_id: streamMsgId });
438
373
  }
439
- return;
440
374
  }
441
- // Send voice reply if TTS generated audio (non-streaming path)
375
+ catch { /* edit failures are non-critical */ }
376
+ };
377
+ // Shared pipeline handles: routing, media prompt, progress, sendMessage, TTS
378
+ const pipelineResult = await processInboundMessage({
379
+ text,
380
+ media,
381
+ channelId: 'telegram',
382
+ tabManager: this.ctx.tabManager,
383
+ voiceReplyMode: this.ctx.config.voice?.replyMode,
384
+ ttsProvider: this.ttsProvider,
385
+ userId: String(chatId),
386
+ sendProgress: (msg) => {
387
+ this.bot.sendMessage(chatId, msg).catch(() => { });
388
+ },
389
+ overrideTabName,
390
+ onTextChunk,
391
+ });
392
+ // Empty result means no prompt and no media
393
+ if (!pipelineResult.responseText) {
394
+ clearInterval(typingInterval);
395
+ clearTimeout(stillWorkingTimeout);
396
+ return;
397
+ }
398
+ // Update the effective tab for stream prefix (now known)
399
+ effectiveTabForStream = pipelineResult.tabName;
400
+ responseText = pipelineResult.responseText;
401
+ responseError = pipelineResult.isError;
402
+ responseTab = pipelineResult.tabName;
403
+ // Telegram-specific: if streaming was active and no error, edit the final message
404
+ if (streamMsgId && !responseError) {
405
+ clearInterval(typingInterval);
406
+ clearTimeout(stillWorkingTimeout);
407
+ await this.setReaction(chatId, messageId, '✅');
408
+ // Send voice if available (even with streaming)
442
409
  if (pipelineResult.audioPath) {
443
- clearInterval(typingInterval);
444
- clearTimeout(stillWorkingTimeout);
445
- await this.setReaction(chatId, messageId, responseError ? '❌' : '✅');
446
410
  await this.bot.sendVoice(chatId, pipelineResult.audioPath);
447
411
  if (pipelineResult.voiceOnly)
448
412
  return;
449
- if (!responseError) {
413
+ }
414
+ try {
415
+ const prefix = responseTab !== 'default' ? `[${responseTab}] ` : '';
416
+ const finalText = prefix + responseText;
417
+ if (finalText.length <= 4096) {
418
+ await this.bot.editMessageText(finalText, { chat_id: chatId, message_id: streamMsgId });
419
+ }
420
+ else {
450
421
  await this.sendResponse(chatId, responseText, responseTab);
451
- return;
452
422
  }
453
423
  }
424
+ catch {
425
+ await this.sendResponse(chatId, responseText, responseTab);
426
+ }
427
+ return;
428
+ }
429
+ // Send voice reply if TTS generated audio (non-streaming path)
430
+ if (pipelineResult.audioPath) {
431
+ clearInterval(typingInterval);
432
+ clearTimeout(stillWorkingTimeout);
433
+ await this.setReaction(chatId, messageId, responseError ? '❌' : '✅');
434
+ await this.bot.sendVoice(chatId, pipelineResult.audioPath);
435
+ if (pipelineResult.voiceOnly)
436
+ return;
437
+ if (!responseError) {
438
+ await this.sendResponse(chatId, responseText, responseTab);
439
+ return;
440
+ }
454
441
  }
455
442
  clearInterval(typingInterval);
456
443
  clearTimeout(stillWorkingTimeout);
@@ -1,5 +1,4 @@
1
1
  import type { TabManager } from '../session/manager.js';
2
- import type { PipeBrain } from '../pipe/brain.js';
3
2
  import type { BeecorkConfig } from '../types.js';
4
3
  /** A media file attached to a message */
5
4
  export interface MediaAttachment {
@@ -62,7 +61,6 @@ export interface Channel {
62
61
  export interface ChannelContext {
63
62
  config: BeecorkConfig;
64
63
  tabManager: TabManager;
65
- pipeBrain: PipeBrain | null;
66
64
  /** Broadcast notification to all channels + notification providers */
67
65
  notifyCallback?: (message: string) => Promise<void>;
68
66
  }
@@ -83,7 +83,6 @@ interface Channel {
83
83
  interface ChannelContext {
84
84
  config: any;
85
85
  tabManager: any;
86
- pipeBrain: any;
87
86
  }
88
87
 
89
88
  export default class ${className}Channel implements Channel {
package/dist/cli/setup.js CHANGED
@@ -124,24 +124,13 @@ export async function setupWizard() {
124
124
  tabs: {
125
125
  default: {
126
126
  workingDir: defaultDir,
127
- approvalMode: 'yolo',
128
- approvalTimeoutMinutes: 30,
129
127
  },
130
128
  },
131
129
  memory: {
132
- enabled: true,
133
130
  dbPath: '~/.beecork/memory.db',
134
131
  maxLongTermEntries: 1000,
135
132
  },
136
- pipe: {
137
- enabled: false,
138
- anthropicApiKey: '',
139
- routingModel: 'claude-haiku-4-5-20251001',
140
- complexModel: 'claude-sonnet-4-6-20250514',
141
- confidenceThreshold: 0.75,
142
- projectScanPaths: scanPaths,
143
- maxFollowUps: 5,
144
- },
133
+ projectScanPaths: scanPaths,
145
134
  deployment: 'local',
146
135
  };
147
136
  // Write everything
@@ -155,18 +144,10 @@ export async function setupWizard() {
155
144
  // Generate MCP config
156
145
  generateMcpConfig();
157
146
  console.log(`✓ MCP config generated at ${getMcpConfigPath()}`);
158
- // Inject Beecork instructions into global CLAUDE.md
159
- injectClaudeMd();
160
- console.log('✓ Beecork tools injected into ~/.claude/CLAUDE.md');
161
- // Scan for projects if pipe is enabled
162
- if (config.pipe.enabled) {
163
- const { scanForProjects } = await import('../pipe/project-scanner.js');
164
- const { PipeMemoryStore } = await import('../pipe/memory-store.js');
165
- const projects = scanForProjects(config.pipe.projectScanPaths);
166
- const store = new PipeMemoryStore();
167
- for (const p of projects) {
168
- store.upsertProject(p);
169
- }
147
+ // Discover projects on disk
148
+ {
149
+ const { discoverProjects } = await import('../projects/index.js');
150
+ const projects = discoverProjects(config.projectScanPaths);
170
151
  console.log(`✓ Discovered ${projects.length} projects`);
171
152
  closeDb();
172
153
  }
@@ -202,11 +183,6 @@ export async function setupWizard() {
202
183
  console.log(' beecork dashboard — open web control panel');
203
184
  console.log(' beecork quickstart — full getting-started checklist');
204
185
  console.log('');
205
- console.log(' ★ Recommended: Smart folder routing');
206
- console.log(' If you work in multiple folders, Beecork can auto-detect which');
207
- console.log(' folder you mean and route messages to the right tab.');
208
- console.log(' Run: beecork pipe setup');
209
- console.log('');
210
186
  console.log(' Add more channels:');
211
187
  console.log(' beecork whatsapp — connect WhatsApp');
212
188
  console.log(' beecork discord — connect Discord');
@@ -260,56 +236,3 @@ function generateMcpConfig() {
260
236
  };
261
237
  fs.writeFileSync(getMcpConfigPath(), JSON.stringify(mcpConfig, null, 2) + '\n');
262
238
  }
263
- const BEECORK_MARKER_START = '<!-- BEECORK START -->';
264
- const BEECORK_MARKER_END = '<!-- BEECORK END -->';
265
- const BEECORK_CLAUDE_MD = `${BEECORK_MARKER_START}
266
- ## Beecork — Always-On Tools
267
-
268
- You have Beecork MCP tools available when running inside Beecork:
269
-
270
- **Memory:** beecork_remember, beecork_recall — store and search long-term memory
271
- **Scheduling:** beecork_task_create/list/delete — schedule recurring tasks (aliases: beecork_cron_*)
272
- **Watchers:** beecork_watch_create/list/delete — monitor conditions and auto-respond
273
- **Tabs:** beecork_tab_create/list, beecork_send_message — manage virtual tabs
274
- **Communication:** beecork_notify, beecork_send_media — notify user, send files
275
- **Delegation:** beecork_delegate, beecork_delegation_status — delegate tasks to other tabs
276
- **Media:** beecork_generate_image/video/audio — generate media via AI providers
277
- **Projects:** beecork_project_create/list — manage projects
278
- **Observability:** beecork_cost, beecork_activity, beecork_status — track spending and activity
279
- **Handoff:** beecork_handoff — export session for terminal resume
280
-
281
- ### Self-Extension
282
-
283
- You can install new capabilities for yourself. If the user asks for a tool or media provider that isn't configured:
284
-
285
- 1. Check if a community package exists: \`beecork-media-<name>\` or \`beecork-channel-<name>\` on npm
286
- 2. Install it: \`npm install -g beecork-media-<name>\`
287
- 3. Or create a custom MCP server: write a Node.js script wrapping the API, register in \`~/.beecork/mcp-config.json\`
288
- 4. Tell the user to restart: \`beecork stop && beecork start\`
289
-
290
- ### Guidelines
291
-
292
- - Always call beecork_recall at the start of complex tasks
293
- - Always beecork_remember important outcomes and decisions
294
- - Use beecork_notify for progress on long tasks
295
- - Use beecork_delegate for independent subtasks that need their own workspace
296
- ${BEECORK_MARKER_END}`;
297
- function injectClaudeMd() {
298
- const claudeMdPath = path.join(os.homedir(), '.claude', 'CLAUDE.md');
299
- const claudeDir = path.dirname(claudeMdPath);
300
- fs.mkdirSync(claudeDir, { recursive: true });
301
- let content = '';
302
- if (fs.existsSync(claudeMdPath)) {
303
- content = fs.readFileSync(claudeMdPath, 'utf-8');
304
- // Remove old injection if present
305
- const startIdx = content.indexOf(BEECORK_MARKER_START);
306
- const endIdx = content.indexOf(BEECORK_MARKER_END);
307
- if (startIdx !== -1 && endIdx !== -1) {
308
- content = content.slice(0, startIdx) + content.slice(endIdx + BEECORK_MARKER_END.length);
309
- content = content.trim();
310
- }
311
- }
312
- // Append Beecork section
313
- content = content + '\n\n' + BEECORK_CLAUDE_MD + '\n';
314
- fs.writeFileSync(claudeMdPath, content);
315
- }
package/dist/config.js CHANGED
@@ -4,33 +4,18 @@ import path from 'node:path';
4
4
  import { getConfigPath, expandHome } from './util/paths.js';
5
5
  const DEFAULT_TAB_CONFIG = {
6
6
  workingDir: os.homedir(),
7
- approvalMode: 'yolo',
8
- approvalTimeoutMinutes: 30,
9
- };
10
- const DEFAULT_PIPE_CONFIG = {
11
- enabled: false,
12
- anthropicApiKey: '',
13
- routingModel: 'claude-haiku-4-5-20251001',
14
- complexModel: 'claude-sonnet-4-6-20250514',
15
- confidenceThreshold: 0.75,
16
- projectScanPaths: ['~/Coding', '~/Projects', '~/code', '~/dev'],
17
- maxFollowUps: 5,
18
7
  };
8
+ const DEFAULT_PROJECT_SCAN_PATHS = ['~/Coding', '~/Projects', '~/code', '~/dev'];
19
9
  /**
20
- * INTENTIONAL: Beecork is designed for unattended AI agent operation.
21
- * 'yolo' mode allows all tool calls without user approval.
22
- * Users can override to 'auto-safe' or 'manual' in ~/.beecork/config.json.
10
+ * INTENTIONAL: --dangerously-skip-permissions is required for unattended operation.
11
+ * Without it, Claude Code would block on every tool call waiting for user input.
12
+ * Users can override defaultFlags in ~/.beecork/config.json.
23
13
  */
24
14
  const DEFAULT_CONFIG = {
25
15
  telegram: {
26
16
  token: '',
27
17
  allowedUserIds: [],
28
18
  },
29
- /**
30
- * INTENTIONAL: --dangerously-skip-permissions is required for unattended operation.
31
- * Without it, Claude Code would block on every tool call waiting for user input.
32
- * Users can override defaultFlags in ~/.beecork/config.json.
33
- */
34
19
  claudeCode: {
35
20
  bin: 'claude',
36
21
  defaultFlags: ['--dangerously-skip-permissions'],
@@ -39,11 +24,10 @@ const DEFAULT_CONFIG = {
39
24
  default: { ...DEFAULT_TAB_CONFIG },
40
25
  },
41
26
  memory: {
42
- enabled: true,
43
27
  dbPath: '~/.beecork/memory.db',
44
28
  maxLongTermEntries: 1000,
45
29
  },
46
- pipe: { ...DEFAULT_PIPE_CONFIG },
30
+ projectScanPaths: [...DEFAULT_PROJECT_SCAN_PATHS],
47
31
  deployment: 'local',
48
32
  };
49
33
  let cachedConfig = null;
@@ -116,10 +100,10 @@ function mergeWithDefaults(raw) {
116
100
  ...DEFAULT_CONFIG.memory,
117
101
  ...raw.memory,
118
102
  },
119
- pipe: {
120
- ...DEFAULT_PIPE_CONFIG,
121
- ...raw.pipe,
122
- },
103
+ // Fall back to legacy pipe.projectScanPaths so old configs keep working
104
+ projectScanPaths: raw.projectScanPaths
105
+ ?? raw.pipe?.projectScanPaths
106
+ ?? [...DEFAULT_PROJECT_SCAN_PATHS],
123
107
  deployment: raw.deployment ?? DEFAULT_CONFIG.deployment,
124
108
  // Preserve optional config sections (no defaults needed)
125
109
  whatsapp: raw.whatsapp,
package/dist/daemon.js CHANGED
@@ -1,12 +1,10 @@
1
1
  import fs from 'node:fs';
2
- import os from 'node:os';
3
2
  import { getConfig } from './config.js';
4
3
  import { getDb, closeDb } from './db/index.js';
5
4
  import { TabManager } from './session/manager.js';
6
5
  import { ChannelRegistry, TelegramChannel, WhatsAppChannel } from './channels/index.js';
7
6
  import { TaskScheduler } from './tasks/scheduler.js';
8
7
  import { WatcherScheduler } from './watchers/scheduler.js';
9
- import { PipeBrain } from './pipe/brain.js';
10
8
  import { ensureBeecorkDirs, getPidPath, getBeecorkHome } from './util/paths.js';
11
9
  import { execSync } from 'node:child_process';
12
10
  import { logger } from './util/logger.js';
@@ -18,7 +16,6 @@ let tabManager;
18
16
  let channelRegistry;
19
17
  let taskScheduler;
20
18
  let watcherScheduler;
21
- let pipeBrain = null;
22
19
  let pollInterval;
23
20
  let shutdownFn = null;
24
21
  const notificationProviders = [];
@@ -52,14 +49,10 @@ async function main() {
52
49
  const config = getConfig();
53
50
  // 2. Initialize database
54
51
  getDb();
55
- // 2b. Register this machine
56
- const { registerThisMachine } = await import('./machines/index.js');
57
- const projectPaths = config.pipe?.projectScanPaths || [os.homedir()];
58
- registerThisMachine(projectPaths);
59
- // 2c. Discover projects in workspace
52
+ // 2a. Discover projects in workspace
60
53
  try {
61
54
  const { discoverProjects, ensureCategory } = await import('./projects/index.js');
62
- const projects = discoverProjects(config.pipe?.projectScanPaths);
55
+ const projects = discoverProjects(config.projectScanPaths);
63
56
  ensureCategory('general'); // Ensure default category exists
64
57
  logger.info(`Discovered ${projects.length} projects`);
65
58
  }
@@ -80,19 +73,13 @@ async function main() {
80
73
  logger.info(`PID file written: ${process.pid}`);
81
74
  // 4. Create TabManager
82
75
  tabManager = new TabManager(config);
83
- // 5. Initialize pipe brain (if API key configured)
84
- if (config.pipe?.enabled && config.pipe?.anthropicApiKey) {
85
- pipeBrain = new PipeBrain(config, tabManager);
86
- const projectCount = await pipeBrain.discoverProjects();
87
- logger.info(`Pipe brain initialized — ${projectCount} projects discovered`);
88
- }
89
- // 6. Ensure default tab
76
+ // 5. Ensure default tab
90
77
  tabManager.ensureTab('default');
91
- // 7. Recover crashed tabs
78
+ // 6. Recover crashed tabs
92
79
  await recoverCrashedTabs();
93
80
  // Start channels via registry
94
81
  channelRegistry = new ChannelRegistry();
95
- const channelCtx = { config, tabManager, pipeBrain, notifyCallback: broadcastNotify };
82
+ const channelCtx = { config, tabManager, notifyCallback: broadcastNotify };
96
83
  if (config.telegram?.token) {
97
84
  channelRegistry.register(new TelegramChannel(channelCtx));
98
85
  }
@@ -148,9 +135,6 @@ async function main() {
148
135
  }
149
136
  // Wire up broadcast notifications to all active channels
150
137
  tabManager.setNotifyCallback(broadcastNotify);
151
- if (pipeBrain) {
152
- pipeBrain.setNotifyCallback(broadcastNotify);
153
- }
154
138
  // 9. Start task scheduler
155
139
  taskScheduler = new TaskScheduler(tabManager, broadcastNotify);
156
140
  taskScheduler.loadAndSchedule();
package/dist/db/index.js CHANGED
@@ -63,8 +63,7 @@ export function getDb() {
63
63
  walInterval = setInterval(() => {
64
64
  try {
65
65
  db?.pragma('wal_checkpoint(PASSIVE)');
66
- // Prune old routing/permission history (keep last 1000 entries)
67
- db?.exec('DELETE FROM routing_history WHERE created_at < (SELECT created_at FROM routing_history ORDER BY created_at DESC LIMIT 1 OFFSET 999)');
66
+ // Prune old permission history (keep last 1000 entries)
68
67
  db?.exec('DELETE FROM permission_history WHERE created_at < (SELECT created_at FROM permission_history ORDER BY created_at DESC LIMIT 1 OFFSET 999)');
69
68
  db?.exec("DELETE FROM activity_log WHERE created_at < datetime('now', '-90 days')");
70
69
  }
@@ -142,17 +142,8 @@ const MIGRATIONS = [
142
142
  },
143
143
  {
144
144
  version: 12,
145
- description: 'Add machines table for multi-machine routing',
146
- up: `CREATE TABLE IF NOT EXISTS machines (
147
- id TEXT PRIMARY KEY,
148
- name TEXT NOT NULL,
149
- host TEXT,
150
- ssh_user TEXT,
151
- project_paths TEXT,
152
- is_primary INTEGER DEFAULT 0,
153
- last_seen_at TEXT DEFAULT (datetime('now')),
154
- created_at TEXT DEFAULT (datetime('now'))
155
- )`,
145
+ description: 'Removed machines table (retired multi-machine scaffolding)',
146
+ up: '',
156
147
  },
157
148
  {
158
149
  version: 13,
@@ -239,6 +230,14 @@ const MIGRATIONS = [
239
230
  description: 'Add index on activity_log.created_at',
240
231
  up: 'CREATE INDEX IF NOT EXISTS idx_activity_log_created ON activity_log(created_at)',
241
232
  },
233
+ {
234
+ version: 22,
235
+ description: 'Add indices on memories for dedup lookups and tab-scoped reads',
236
+ up: `
237
+ CREATE INDEX IF NOT EXISTS idx_memories_content ON memories(content);
238
+ CREATE INDEX IF NOT EXISTS idx_memories_tab_name ON memories(tab_name, created_at);
239
+ `,
240
+ },
242
241
  ];
243
242
  export function runMigrations(db) {
244
243
  // Ensure schema_version table exists
package/dist/index.js CHANGED
@@ -336,8 +336,6 @@ program
336
336
  console.log(` workingDir: ${tmpl.workingDir}`);
337
337
  if (tmpl.systemPrompt)
338
338
  console.log(` systemPrompt: "${tmpl.systemPrompt.slice(0, 80)}${tmpl.systemPrompt.length > 80 ? '...' : ''}"`);
339
- if (tmpl.approvalMode)
340
- console.log(` approvalMode: ${tmpl.approvalMode}`);
341
339
  }
342
340
  console.log('');
343
341
  });
@@ -407,27 +405,6 @@ program
407
405
  const { getActivitySummary, formatActivitySummary } = await import('./observability/analytics.js');
408
406
  console.log(formatActivitySummary(getActivitySummary(h)));
409
407
  });
410
- program
411
- .command('machines')
412
- .description('List registered machines')
413
- .action(async () => {
414
- const { listMachines } = await import('./machines/index.js');
415
- const machines = listMachines();
416
- if (machines.length === 0) {
417
- console.log('No machines registered. Start the daemon to register this machine.');
418
- return;
419
- }
420
- console.log(`\n${machines.length} machine(s):\n`);
421
- for (const m of machines) {
422
- const primary = m.isPrimary ? ' (primary)' : '';
423
- const remote = m.host ? ` — ${m.sshUser}@${m.host}` : ' — local';
424
- console.log(` ${m.name}${primary}${remote}`);
425
- for (const p of m.projectPaths) {
426
- console.log(` ${p}`);
427
- }
428
- }
429
- console.log('');
430
- });
431
408
  program
432
409
  .command('folders')
433
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>;