beecork 1.5.0 → 1.6.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.
Files changed (119) hide show
  1. package/dist/capabilities/index.d.ts +1 -1
  2. package/dist/capabilities/index.js +1 -1
  3. package/dist/capabilities/manager.js +13 -9
  4. package/dist/capabilities/packs.js +3 -1
  5. package/dist/channels/command-handler.js +46 -14
  6. package/dist/channels/discord.d.ts +3 -6
  7. package/dist/channels/discord.js +40 -23
  8. package/dist/channels/index.d.ts +1 -1
  9. package/dist/channels/loader.js +13 -3
  10. package/dist/channels/pipeline.js +14 -5
  11. package/dist/channels/registry.d.ts +17 -1
  12. package/dist/channels/registry.js +33 -4
  13. package/dist/channels/telegram.d.ts +20 -5
  14. package/dist/channels/telegram.js +177 -42
  15. package/dist/channels/types.d.ts +11 -28
  16. package/dist/channels/voice-state.js +3 -1
  17. package/dist/channels/webhook.d.ts +1 -4
  18. package/dist/channels/webhook.js +26 -11
  19. package/dist/channels/whatsapp.d.ts +8 -4
  20. package/dist/channels/whatsapp.js +65 -29
  21. package/dist/cli/capabilities.js +4 -4
  22. package/dist/cli/channel.js +16 -6
  23. package/dist/cli/commands.js +12 -9
  24. package/dist/cli/doctor.js +80 -25
  25. package/dist/cli/handoff.d.ts +7 -14
  26. package/dist/cli/handoff.js +9 -44
  27. package/dist/cli/mcp.js +5 -5
  28. package/dist/cli/media.js +21 -8
  29. package/dist/cli/setup.js +9 -8
  30. package/dist/cli/store.js +29 -12
  31. package/dist/config.js +5 -10
  32. package/dist/daemon.js +88 -38
  33. package/dist/dashboard/html.js +80 -12
  34. package/dist/dashboard/routes.js +143 -79
  35. package/dist/dashboard/server.js +5 -1
  36. package/dist/db/connection.d.ts +29 -0
  37. package/dist/db/connection.js +37 -0
  38. package/dist/db/index.js +30 -12
  39. package/dist/db/migrations.js +84 -28
  40. package/dist/delegation/manager.js +10 -4
  41. package/dist/index.js +39 -59
  42. package/dist/knowledge/manager.js +26 -12
  43. package/dist/mcp/handlers.js +126 -57
  44. package/dist/mcp/server.js +20 -10
  45. package/dist/mcp/tool-definitions.js +68 -20
  46. package/dist/mcp/validate.d.ts +23 -0
  47. package/dist/mcp/validate.js +65 -0
  48. package/dist/media/factory.js +18 -14
  49. package/dist/media/generators/dall-e.js +2 -2
  50. package/dist/media/generators/kling.js +4 -4
  51. package/dist/media/generators/lyria.js +1 -1
  52. package/dist/media/generators/nano-banana.d.ts +1 -1
  53. package/dist/media/generators/nano-banana.js +2 -2
  54. package/dist/media/generators/poll-util.js +4 -4
  55. package/dist/media/generators/recraft.js +3 -3
  56. package/dist/media/generators/runway.js +4 -4
  57. package/dist/media/generators/stable-diffusion.js +2 -2
  58. package/dist/media/generators/veo.js +1 -1
  59. package/dist/media/index.js +1 -1
  60. package/dist/media/store.d.ts +7 -0
  61. package/dist/media/store.js +18 -4
  62. package/dist/media/types.d.ts +22 -0
  63. package/dist/notifications/index.d.ts +2 -4
  64. package/dist/notifications/index.js +6 -19
  65. package/dist/notifications/ntfy.js +3 -3
  66. package/dist/observability/analytics.js +35 -13
  67. package/dist/projects/index.d.ts +1 -1
  68. package/dist/projects/index.js +1 -1
  69. package/dist/projects/manager.d.ts +0 -4
  70. package/dist/projects/manager.js +51 -28
  71. package/dist/projects/router.d.ts +2 -0
  72. package/dist/projects/router.js +70 -45
  73. package/dist/service/install.js +15 -5
  74. package/dist/service/windows.js +1 -1
  75. package/dist/session/budget-guard.d.ts +20 -0
  76. package/dist/session/budget-guard.js +31 -0
  77. package/dist/session/circuit-breaker.d.ts +5 -3
  78. package/dist/session/circuit-breaker.js +45 -20
  79. package/dist/session/context-compactor.d.ts +32 -0
  80. package/dist/session/context-compactor.js +45 -0
  81. package/dist/session/context-monitor.js +2 -2
  82. package/dist/session/handoff.d.ts +21 -0
  83. package/dist/session/handoff.js +50 -0
  84. package/dist/session/manager.d.ts +17 -5
  85. package/dist/session/manager.js +153 -146
  86. package/dist/session/memory-store.d.ts +29 -0
  87. package/dist/session/memory-store.js +45 -0
  88. package/dist/session/message-queue.d.ts +28 -0
  89. package/dist/session/message-queue.js +52 -0
  90. package/dist/session/pending-dispatcher.d.ts +31 -0
  91. package/dist/session/pending-dispatcher.js +120 -0
  92. package/dist/session/pending-store.d.ts +60 -0
  93. package/dist/session/pending-store.js +118 -0
  94. package/dist/session/stale-session.d.ts +31 -0
  95. package/dist/session/stale-session.js +45 -0
  96. package/dist/session/subprocess.d.ts +2 -0
  97. package/dist/session/subprocess.js +33 -11
  98. package/dist/session/tab-store.js +4 -3
  99. package/dist/tasks/scheduler.d.ts +7 -0
  100. package/dist/tasks/scheduler.js +46 -6
  101. package/dist/tasks/store.js +20 -6
  102. package/dist/timeline/logger.js +3 -1
  103. package/dist/timeline/query.js +9 -3
  104. package/dist/types.d.ts +34 -9
  105. package/dist/util/auto-heal.js +15 -5
  106. package/dist/util/install-info.js +3 -1
  107. package/dist/util/logger.d.ts +1 -1
  108. package/dist/util/logger.js +63 -24
  109. package/dist/util/paths.d.ts +1 -0
  110. package/dist/util/paths.js +12 -2
  111. package/dist/util/retry.js +1 -1
  112. package/dist/util/text.js +13 -7
  113. package/dist/voice/index.js +5 -1
  114. package/dist/voice/stt.js +14 -6
  115. package/dist/voice/tts.js +1 -1
  116. package/dist/watchers/scheduler.js +9 -2
  117. package/package.json +6 -1
  118. package/dist/session/tool-classifier.d.ts +0 -4
  119. package/dist/session/tool-classifier.js +0 -56
@@ -1,3 +1,16 @@
1
+ /*
2
+ * WhatsApp integration via @whiskeysockets/baileys.
3
+ *
4
+ * baileys is a peer-optional dependency loaded by dynamic import, and its
5
+ * runtime types are intentionally loose (the lib uses heavy union/index
6
+ * types that don't survive serialization across the dynamic-import boundary).
7
+ * Trying to type every variant of Baileys' message/connection shapes would
8
+ * either pull baileys into the static graph (bloating non-WhatsApp installs)
9
+ * or require maintaining a parallel shim. We accept `any` at this trust
10
+ * boundary; runtime validation lives in the descriptors table inside
11
+ * messages.upsert.
12
+ */
13
+ /* eslint-disable @typescript-eslint/no-explicit-any */
1
14
  import fs from 'node:fs';
2
15
  import { logger } from '../util/logger.js';
3
16
  import { saveMedia, isOversized } from '../media/store.js';
@@ -11,8 +24,6 @@ export class WhatsAppChannel {
11
24
  id = 'whatsapp';
12
25
  name = 'WhatsApp';
13
26
  maxMessageLength = WHATSAPP_MAX_LENGTH;
14
- supportsStreaming = false;
15
- supportsMedia = true;
16
27
  sock = null;
17
28
  ctx;
18
29
  allowedNumbers;
@@ -24,12 +35,42 @@ export class WhatsAppChannel {
24
35
  this.ctx = ctx;
25
36
  this.allowedNumbers = new Set(ctx.config.whatsapp?.allowedNumbers ?? []);
26
37
  }
38
+ /**
39
+ * Schedule the next reconnect with exponential backoff. Unlike the previous
40
+ * inline setTimeout, this path retries when `start()` itself rejects (auth
41
+ * failure, baileys init throw, etc.) instead of going permanently silent
42
+ * after a single failed attempt.
43
+ */
44
+ scheduleReconnect() {
45
+ this.reconnectAttempts++;
46
+ if (this.reconnectAttempts > this.maxReconnectAttempts) {
47
+ logger.error(`WhatsApp reconnect failed after ${this.maxReconnectAttempts} attempts, giving up`);
48
+ this.ctx
49
+ .notifyCallback?.('⚠️ WhatsApp disconnected after 10 reconnection attempts. Restart daemon to reconnect.')
50
+ .catch((err) => logger.error('Failed to send WhatsApp disconnect notification:', err));
51
+ return;
52
+ }
53
+ const delayIdx = Math.min(this.reconnectAttempts - 1, this.backoffDelays.length - 1);
54
+ const delay = this.backoffDelays[delayIdx];
55
+ logger.warn(`WhatsApp connection closed, reconnecting in ${delay / 1000}s (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
56
+ setTimeout(() => {
57
+ this.start().catch((err) => {
58
+ logger.error('WhatsApp reconnect attempt failed:', err);
59
+ // Recurse via scheduleReconnect so backoff keeps escalating instead of
60
+ // silently dropping the reconnect chain after one failed start().
61
+ this.scheduleReconnect();
62
+ });
63
+ }, delay);
64
+ }
27
65
  async start() {
28
66
  // Initialize voice providers (STT + TTS)
29
67
  this.voice.init(this.ctx.config);
30
68
  try {
31
- const { default: makeWASocket, useMultiFileAuthState, DisconnectReason, downloadMediaMessage, fetchLatestBaileysVersion } = await import('@whiskeysockets/baileys');
32
- const sessionPath = this.ctx.config.whatsapp?.sessionPath ?? `${process.env.HOME}/.beecork/whatsapp-session`;
69
+ const { default: makeWASocket, useMultiFileAuthState, DisconnectReason, downloadMediaMessage, fetchLatestBaileysVersion, } = await import('@whiskeysockets/baileys');
70
+ const { getWhatsappSessionPath } = await import('../util/paths.js');
71
+ // Use the centralized path helper — the previous fallback hard-coded
72
+ // process.env.HOME which bypassed BEECORK_HOME for tests/isolation.
73
+ const sessionPath = this.ctx.config.whatsapp?.sessionPath ?? getWhatsappSessionPath();
33
74
  fs.mkdirSync(sessionPath, { recursive: true, mode: 0o700 });
34
75
  const { state, saveCreds } = await useMultiFileAuthState(sessionPath);
35
76
  const { version } = await fetchLatestBaileysVersion().catch(() => ({ version: undefined }));
@@ -56,21 +97,7 @@ export class WhatsAppChannel {
56
97
  if (connection === 'close') {
57
98
  const reason = lastDisconnect?.error?.output?.statusCode;
58
99
  if (reason !== DisconnectReason.loggedOut) {
59
- this.reconnectAttempts++;
60
- if (this.reconnectAttempts > this.maxReconnectAttempts) {
61
- logger.error(`WhatsApp reconnect failed after ${this.maxReconnectAttempts} attempts, giving up`);
62
- this.ctx.notifyCallback?.('⚠️ WhatsApp disconnected after 10 reconnection attempts. Restart daemon to reconnect.')
63
- .catch(err => logger.error('Failed to send WhatsApp disconnect notification:', err));
64
- return;
65
- }
66
- const delayIdx = Math.min(this.reconnectAttempts - 1, this.backoffDelays.length - 1);
67
- const delay = this.backoffDelays[delayIdx];
68
- logger.warn(`WhatsApp connection closed, reconnecting in ${delay / 1000}s (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
69
- setTimeout(() => {
70
- this.start().catch(err => {
71
- logger.error('WhatsApp reconnect failed:', err);
72
- });
73
- }, delay);
100
+ this.scheduleReconnect();
74
101
  }
75
102
  else {
76
103
  logger.error('WhatsApp logged out. Please re-scan QR code.');
@@ -90,13 +117,18 @@ export class WhatsAppChannel {
90
117
  return;
91
118
  // Rate limit check
92
119
  if (!inboundLimiter.check(this.id)) {
93
- await sock.sendMessage(sender, { text: "I'm receiving too many messages right now. Please wait a moment." }).catch(() => { });
120
+ await sock
121
+ .sendMessage(sender, {
122
+ text: "I'm receiving too many messages right now. Please wait a moment.",
123
+ })
124
+ .catch(() => { });
94
125
  return;
95
126
  }
96
127
  const text = msg.message.conversation ||
97
128
  msg.message.extendedTextMessage?.text ||
98
129
  msg.message.imageMessage?.caption ||
99
- msg.message.videoMessage?.caption || '';
130
+ msg.message.videoMessage?.caption ||
131
+ '';
100
132
  const descriptors = [
101
133
  {
102
134
  key: 'imageMessage',
@@ -160,7 +192,7 @@ export class WhatsAppChannel {
160
192
  const waResults = await Promise.allSettled(waDownloadTasks);
161
193
  const media = waResults
162
194
  .filter((r) => r.status === 'fulfilled' && r.value !== null)
163
- .map(r => r.value);
195
+ .map((r) => r.value);
164
196
  // Transcribe voice messages if STT is configured
165
197
  await this.voice.transcribe(media);
166
198
  if (!text && media.length === 0)
@@ -202,7 +234,11 @@ export class WhatsAppChannel {
202
234
  return;
203
235
  // Send voice reply if TTS generated audio
204
236
  if (pipelineResult.audioPath) {
205
- await sock.sendMessage(sender, { audio: { url: pipelineResult.audioPath }, mimetype: 'audio/ogg; codecs=opus', ptt: true });
237
+ await sock.sendMessage(sender, {
238
+ audio: { url: pipelineResult.audioPath },
239
+ mimetype: 'audio/ogg; codecs=opus',
240
+ ptt: true,
241
+ });
206
242
  if (pipelineResult.voiceOnly)
207
243
  return;
208
244
  }
@@ -210,7 +246,10 @@ export class WhatsAppChannel {
210
246
  }
211
247
  catch (err) {
212
248
  logger.error('WhatsApp message handler error:', err);
213
- await sock.sendMessage(sender, { text: 'Something went wrong processing your message. Check daemon logs for details.' })
249
+ await sock
250
+ .sendMessage(sender, {
251
+ text: 'Something went wrong processing your message. Check daemon logs for details.',
252
+ })
214
253
  .catch((sendErr) => logger.error('WhatsApp: failed to send fallback error message:', sendErr));
215
254
  }
216
255
  });
@@ -236,7 +275,7 @@ export class WhatsAppChannel {
236
275
  text,
237
276
  maxLength: WHATSAPP_MAX_LENGTH,
238
277
  retryLabel: 'whatsapp-send',
239
- sendChunk: chunk => sock.sendMessage(peerId, { text: chunk }),
278
+ sendChunk: (chunk) => sock.sendMessage(peerId, { text: chunk }),
240
279
  });
241
280
  }
242
281
  async sendNotification(message, _urgent) {
@@ -259,9 +298,6 @@ export class WhatsAppChannel {
259
298
  const status = active ? 'composing' : 'paused';
260
299
  await sock.sendPresenceUpdate(status, peerId).catch(() => { });
261
300
  }
262
- onMessage(_handler) {
263
- // Messages are handled directly in start()
264
- }
265
301
  // ─── Private ───
266
302
  async sendResponse(jid, text, tabName) {
267
303
  const sock = this.sock;
@@ -271,7 +307,7 @@ export class WhatsAppChannel {
271
307
  tabName,
272
308
  maxLength: WHATSAPP_MAX_LENGTH,
273
309
  retryLabel: 'whatsapp-send',
274
- sendChunk: chunk => sock.sendMessage(jid, { text: chunk }),
310
+ sendChunk: (chunk) => sock.sendMessage(jid, { text: chunk }),
275
311
  });
276
312
  }
277
313
  catch (err) {
@@ -1,15 +1,15 @@
1
1
  import readline from 'node:readline';
2
2
  function ask(rl, question, defaultValue) {
3
3
  const prompt = defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `;
4
- return new Promise(r => rl.question(prompt, a => r(a.trim() || defaultValue || '')));
4
+ return new Promise((r) => rl.question(prompt, (a) => r(a.trim() || defaultValue || '')));
5
5
  }
6
6
  export async function enableCapability(packId) {
7
7
  const { getAvailablePacks, isEnabled, enablePack } = await import('../capabilities/index.js');
8
8
  const packs = getAvailablePacks();
9
- const pack = packs.find(p => p.id === packId);
9
+ const pack = packs.find((p) => p.id === packId);
10
10
  if (!pack) {
11
11
  console.log(`Unknown capability: "${packId}"`);
12
- console.log('Available: ' + packs.map(p => p.id).join(', '));
12
+ console.log('Available: ' + packs.map((p) => p.id).join(', '));
13
13
  return;
14
14
  }
15
15
  if (isEnabled(packId)) {
@@ -46,7 +46,7 @@ export async function listCapabilities() {
46
46
  web: 'Web',
47
47
  };
48
48
  for (const category of categories) {
49
- const categoryPacks = packs.filter(p => p.category === category);
49
+ const categoryPacks = packs.filter((p) => p.category === category);
50
50
  if (categoryPacks.length === 0)
51
51
  continue;
52
52
  console.log(` ${categoryNames[category]}:`);
@@ -1,17 +1,24 @@
1
- import { execSync } from 'node:child_process';
1
+ import { execFileSync } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  const CHANNEL_PREFIX = 'beecork-channel-';
5
+ const SAFE_NPM_PACKAGE = /^[@a-zA-Z0-9_/.-]+$/;
5
6
  export function channelInstall(packageName) {
6
7
  // Normalize name
7
- const fullName = packageName.startsWith(CHANNEL_PREFIX) ? packageName : `${CHANNEL_PREFIX}${packageName}`;
8
+ const fullName = packageName.startsWith(CHANNEL_PREFIX)
9
+ ? packageName
10
+ : `${CHANNEL_PREFIX}${packageName}`;
11
+ if (!SAFE_NPM_PACKAGE.test(fullName)) {
12
+ console.error(`Invalid package name: ${fullName}`);
13
+ process.exit(1);
14
+ }
8
15
  console.log(`Installing channel: ${fullName}...`);
9
16
  try {
10
- execSync(`npm install -g ${fullName}`, { stdio: 'inherit' });
17
+ execFileSync('npm', ['install', '-g', fullName], { stdio: 'inherit' });
11
18
  console.log(`\nChannel "${fullName}" installed.`);
12
19
  console.log('Restart the daemon to activate: beecork stop && beecork start');
13
20
  }
14
- catch (err) {
21
+ catch {
15
22
  console.error(`Failed to install ${fullName}. Check the package name and try again.`);
16
23
  process.exit(1);
17
24
  }
@@ -173,8 +180,11 @@ npm publish
173
180
  }
174
181
  export function channelList() {
175
182
  try {
176
- const output = execSync('npm list -g --depth=0', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
177
- const lines = output.split('\n').filter(line => line.includes(CHANNEL_PREFIX));
183
+ const output = execFileSync('npm', ['list', '-g', '--depth=0'], {
184
+ encoding: 'utf-8',
185
+ stdio: ['pipe', 'pipe', 'pipe'],
186
+ });
187
+ const lines = output.split('\n').filter((line) => line.includes(CHANNEL_PREFIX));
178
188
  if (lines.length === 0) {
179
189
  console.log('No community channels installed.');
180
190
  console.log(`Install one: beecork channel install <name>`);
@@ -4,6 +4,7 @@ import { spawn, execSync } from 'node:child_process';
4
4
  import { getDb, closeDb } from '../db/index.js';
5
5
  import { getConfig } from '../config.js';
6
6
  import { TaskStore } from '../tasks/store.js';
7
+ import { TabStore } from '../session/tab-store.js';
7
8
  import { getDaemonPid, timeAgo } from './helpers.js';
8
9
  import { startService, stopService } from '../service/install.js';
9
10
  import { getPidPath, getLogsDir } from '../util/paths.js';
@@ -71,17 +72,17 @@ export async function showStatus() {
71
72
  console.log(`Daemon: ${pid ? `running (PID ${pid})` : 'stopped'}`);
72
73
  console.log(`Deployment: ${config.deployment}`);
73
74
  try {
74
- const db = getDb();
75
- const tabs = db.prepare('SELECT * FROM tabs ORDER BY last_activity_at DESC').all();
75
+ getDb(); // ensure DB initialized
76
+ const tabs = TabStore.listAll();
76
77
  console.log(`\nTabs (${tabs.length}):`);
77
78
  for (const tab of tabs) {
78
- const ago = timeAgo(tab.last_activity_at);
79
+ const ago = timeAgo(tab.lastActivityAt);
79
80
  const pidInfo = tab.pid ? ` (PID ${tab.pid})` : '';
80
81
  console.log(` ${tab.name.padEnd(20)} ${tab.status.padEnd(12)} last active: ${ago}${pidInfo}`);
81
82
  }
82
83
  const store = new TaskStore();
83
84
  const jobs = store.list();
84
- const activeJobs = jobs.filter(j => j.enabled);
85
+ const activeJobs = jobs.filter((j) => j.enabled);
85
86
  console.log(`\nTasks: ${activeJobs.length} active (${jobs.length} total)`);
86
87
  if (activeJobs.length > 0) {
87
88
  for (const job of activeJobs.slice(0, 5)) {
@@ -97,8 +98,8 @@ export async function showStatus() {
97
98
  console.log('');
98
99
  }
99
100
  export async function listTabs() {
100
- const db = requireDb();
101
- const tabs = db.prepare('SELECT * FROM tabs ORDER BY last_activity_at DESC').all();
101
+ requireDb();
102
+ const tabs = TabStore.listAll();
102
103
  closeDb();
103
104
  if (tabs.length === 0) {
104
105
  console.log('No tabs.');
@@ -106,8 +107,8 @@ export async function listTabs() {
106
107
  }
107
108
  console.log(`\nTabs (${tabs.length}):\n`);
108
109
  for (const tab of tabs) {
109
- const ago = timeAgo(tab.last_activity_at);
110
- console.log(` ${tab.name.padEnd(20)} [${tab.status}] dir:${tab.working_dir} — ${ago}`);
110
+ const ago = timeAgo(tab.lastActivityAt);
111
+ console.log(` ${tab.name.padEnd(20)} [${tab.status}] dir:${tab.workingDir} — ${ago}`);
111
112
  }
112
113
  console.log('');
113
114
  }
@@ -186,7 +187,9 @@ export async function deleteWatcher(id) {
186
187
  }
187
188
  export async function listMemories() {
188
189
  const db = requireDb();
189
- const memories = db.prepare('SELECT * FROM memories ORDER BY created_at DESC LIMIT 50').all();
190
+ const memories = db
191
+ .prepare('SELECT * FROM memories ORDER BY created_at DESC LIMIT 50')
192
+ .all();
190
193
  closeDb();
191
194
  if (memories.length === 0) {
192
195
  console.log('No memories stored.');
@@ -1,7 +1,8 @@
1
1
  import fs from 'node:fs';
2
2
  import { execSync } from 'node:child_process';
3
3
  import { getConfig } from '../config.js';
4
- import { getDbPath, getPidPath, getBeecorkHome } from '../util/paths.js';
4
+ import { getDbPath, getPidPath, getBeecorkHome, getConfigPath, getMcpConfigPath, getWhatsappSessionPath, } from '../util/paths.js';
5
+ import { getMediaDir } from '../media/store.js';
5
6
  export async function runDoctor() {
6
7
  const checks = [];
7
8
  // 1. Check Claude binary
@@ -19,10 +20,14 @@ export async function runDoctor() {
19
20
  }
20
21
  }
21
22
  catch {
22
- checks.push({ name: 'Claude Code', status: 'fail', message: 'Claude Code binary not found. Install: npm install -g @anthropic-ai/claude-code' });
23
+ checks.push({
24
+ name: 'Claude Code',
25
+ status: 'fail',
26
+ message: 'Claude Code binary not found. Install: npm install -g @anthropic-ai/claude-code',
27
+ });
23
28
  }
24
29
  // 2. Check config file
25
- const configPath = `${getBeecorkHome()}/config.json`;
30
+ const configPath = getConfigPath();
26
31
  if (fs.existsSync(configPath)) {
27
32
  try {
28
33
  const config = getConfig();
@@ -30,17 +35,31 @@ export async function runDoctor() {
30
35
  // 3. Check Telegram token
31
36
  if (config.telegram?.token) {
32
37
  try {
33
- const resp = await fetch(`https://api.telegram.org/bot${config.telegram.token}/getMe`, { signal: AbortSignal.timeout(10000) });
38
+ const resp = await fetch(`https://api.telegram.org/bot${config.telegram.token}/getMe`, {
39
+ signal: AbortSignal.timeout(10000),
40
+ });
34
41
  if (resp.ok) {
35
- const data = await resp.json();
36
- checks.push({ name: 'Telegram bot', status: 'pass', message: `@${data.result.username}` });
42
+ const data = (await resp.json());
43
+ checks.push({
44
+ name: 'Telegram bot',
45
+ status: 'pass',
46
+ message: `@${data.result.username}`,
47
+ });
37
48
  }
38
49
  else {
39
- checks.push({ name: 'Telegram bot', status: 'fail', message: 'Invalid token — getMe returned error' });
50
+ checks.push({
51
+ name: 'Telegram bot',
52
+ status: 'fail',
53
+ message: 'Invalid token — getMe returned error',
54
+ });
40
55
  }
41
56
  }
42
- catch (err) {
43
- checks.push({ name: 'Telegram bot', status: 'warn', message: 'Could not reach Telegram API' });
57
+ catch {
58
+ checks.push({
59
+ name: 'Telegram bot',
60
+ status: 'warn',
61
+ message: 'Could not reach Telegram API',
62
+ });
44
63
  }
45
64
  }
46
65
  else {
@@ -48,12 +67,16 @@ export async function runDoctor() {
48
67
  }
49
68
  // 4. Check WhatsApp session
50
69
  if (config.whatsapp?.enabled) {
51
- const sessionPath = config.whatsapp.sessionPath || `${getBeecorkHome()}/whatsapp-session`;
70
+ const sessionPath = config.whatsapp.sessionPath || getWhatsappSessionPath();
52
71
  if (fs.existsSync(sessionPath) && fs.readdirSync(sessionPath).length > 0) {
53
72
  checks.push({ name: 'WhatsApp session', status: 'pass', message: sessionPath });
54
73
  }
55
74
  else {
56
- checks.push({ name: 'WhatsApp session', status: 'warn', message: 'No session data — QR scan needed' });
75
+ checks.push({
76
+ name: 'WhatsApp session',
77
+ status: 'warn',
78
+ message: 'No session data — QR scan needed',
79
+ });
57
80
  }
58
81
  }
59
82
  }
@@ -62,14 +85,18 @@ export async function runDoctor() {
62
85
  }
63
86
  }
64
87
  else {
65
- checks.push({ name: 'Config', status: 'fail', message: `Not found at ${configPath}. Run: beecork setup` });
88
+ checks.push({
89
+ name: 'Config',
90
+ status: 'fail',
91
+ message: `Not found at ${configPath}. Run: beecork setup`,
92
+ });
66
93
  }
67
94
  // 5. Check database
68
95
  const dbPath = getDbPath();
69
96
  if (fs.existsSync(dbPath)) {
70
97
  try {
71
- const Database = (await import('better-sqlite3')).default;
72
- const db = new Database(dbPath, { readonly: true });
98
+ const { openDb } = await import('../db/connection.js');
99
+ const db = openDb(dbPath, { readonly: true });
73
100
  const integrity = db.pragma('integrity_check');
74
101
  if (integrity[0]?.integrity_check === 'ok') {
75
102
  const size = (fs.statSync(dbPath).size / 1024).toFixed(0);
@@ -85,7 +112,11 @@ export async function runDoctor() {
85
112
  }
86
113
  }
87
114
  else {
88
- checks.push({ name: 'Database', status: 'warn', message: 'No database yet — starts on first run' });
115
+ checks.push({
116
+ name: 'Database',
117
+ status: 'warn',
118
+ message: 'No database yet — starts on first run',
119
+ });
89
120
  }
90
121
  // 6. Check daemon
91
122
  const pidPath = getPidPath();
@@ -96,7 +127,11 @@ export async function runDoctor() {
96
127
  checks.push({ name: 'Daemon', status: 'pass', message: `Running (PID ${pid})` });
97
128
  }
98
129
  catch {
99
- checks.push({ name: 'Daemon', status: 'warn', message: `Stale PID file (PID ${pid} not running)` });
130
+ checks.push({
131
+ name: 'Daemon',
132
+ status: 'warn',
133
+ message: `Stale PID file (PID ${pid} not running)`,
134
+ });
100
135
  }
101
136
  }
102
137
  else {
@@ -104,7 +139,7 @@ export async function runDoctor() {
104
139
  }
105
140
  // 7. Check disk space for media
106
141
  try {
107
- const mediaDir = `${getBeecorkHome()}/media`;
142
+ const mediaDir = getMediaDir();
108
143
  const homeDir = getBeecorkHome();
109
144
  // Simple check: can we write a temp file?
110
145
  const testPath = `${homeDir}/.doctor-test`;
@@ -112,25 +147,45 @@ export async function runDoctor() {
112
147
  fs.unlinkSync(testPath);
113
148
  if (fs.existsSync(mediaDir)) {
114
149
  const files = fs.readdirSync(mediaDir);
115
- checks.push({ name: 'Media dir', status: 'pass', message: `${files.length} files in ${mediaDir}` });
150
+ checks.push({
151
+ name: 'Media dir',
152
+ status: 'pass',
153
+ message: `${files.length} files in ${mediaDir}`,
154
+ });
116
155
  }
117
156
  else {
118
- checks.push({ name: 'Media dir', status: 'pass', message: 'Not created yet (created on first media)' });
157
+ checks.push({
158
+ name: 'Media dir',
159
+ status: 'pass',
160
+ message: 'Not created yet (created on first media)',
161
+ });
119
162
  }
120
163
  }
121
164
  catch {
122
- checks.push({ name: 'Disk space', status: 'fail', message: 'Cannot write to beecork home directory' });
165
+ checks.push({
166
+ name: 'Disk space',
167
+ status: 'fail',
168
+ message: 'Cannot write to beecork home directory',
169
+ });
123
170
  }
124
171
  // 8. Check MCP config
125
- const mcpConfigPath = `${getBeecorkHome()}/mcp-config.json`;
172
+ const mcpConfigPath = getMcpConfigPath();
126
173
  if (fs.existsSync(mcpConfigPath)) {
127
174
  try {
128
175
  const mcpConfig = JSON.parse(fs.readFileSync(mcpConfigPath, 'utf-8'));
129
176
  const serverCount = Object.keys(mcpConfig.mcpServers || {}).length;
130
- checks.push({ name: 'MCP config', status: 'pass', message: `${serverCount} server(s) configured` });
177
+ checks.push({
178
+ name: 'MCP config',
179
+ status: 'pass',
180
+ message: `${serverCount} server(s) configured`,
181
+ });
131
182
  }
132
183
  catch {
133
- checks.push({ name: 'MCP config', status: 'fail', message: 'Invalid JSON in mcp-config.json' });
184
+ checks.push({
185
+ name: 'MCP config',
186
+ status: 'fail',
187
+ message: 'Invalid JSON in mcp-config.json',
188
+ });
134
189
  }
135
190
  }
136
191
  else {
@@ -145,8 +200,8 @@ export async function runDoctor() {
145
200
  for (const check of checks) {
146
201
  console.log(` ${icons[check.status]} ${check.name}: ${check.message}`);
147
202
  }
148
- const fails = checks.filter(c => c.status === 'fail').length;
149
- const warns = checks.filter(c => c.status === 'warn').length;
203
+ const fails = checks.filter((c) => c.status === 'fail').length;
204
+ const warns = checks.filter((c) => c.status === 'warn').length;
150
205
  console.log(`\n ${checks.length} checks: ${checks.length - fails - warns} passed, ${warns} warnings, ${fails} failures\n`);
151
206
  if (fails > 0)
152
207
  process.exit(1);
@@ -1,15 +1,8 @@
1
- interface TabInfo {
2
- name: string;
3
- sessionId: string;
4
- workingDir: string;
5
- status: string;
6
- lastActivity: string;
7
- recentMessages: Array<{
8
- role: string;
9
- content: string;
10
- }>;
11
- }
12
- export declare function exportTab(tabName: string): TabInfo | null;
1
+ import { exportTab, formatHandoffInfo, type TabHandoffInfo } from '../session/handoff.js';
2
+ export { exportTab, formatHandoffInfo, type TabHandoffInfo };
3
+ /**
4
+ * CLI-only flow: print the handoff info, spawn claude with `--resume`, and
5
+ * inherit stdio so the user is dropped into an interactive session. Calls
6
+ * `process.exit` on subprocess exit — only safe from the CLI entry point.
7
+ */
13
8
  export declare function attachTab(tabName: string): void;
14
- export declare function formatHandoffInfo(info: TabInfo): string;
15
- export {};
@@ -1,21 +1,13 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { getDb } from '../db/index.js';
3
2
  import { getConfig } from '../config.js';
4
- import { TabStore } from '../session/tab-store.js';
5
- export function exportTab(tabName) {
6
- const tab = TabStore.findByName(tabName);
7
- if (!tab)
8
- return null;
9
- const messages = getDb().prepare('SELECT role, content FROM messages WHERE tab_id = ? ORDER BY created_at DESC LIMIT 5').all(tab.id);
10
- return {
11
- name: tab.name,
12
- sessionId: tab.sessionId,
13
- workingDir: tab.workingDir,
14
- status: tab.status,
15
- lastActivity: tab.lastActivityAt,
16
- recentMessages: messages.reverse(),
17
- };
18
- }
3
+ import { exportTab, formatHandoffInfo } from '../session/handoff.js';
4
+ // Re-export the daemon-shared helpers so existing CLI callers keep working.
5
+ export { exportTab, formatHandoffInfo };
6
+ /**
7
+ * CLI-only flow: print the handoff info, spawn claude with `--resume`, and
8
+ * inherit stdio so the user is dropped into an interactive session. Calls
9
+ * `process.exit` on subprocess exit — only safe from the CLI entry point.
10
+ */
19
11
  export function attachTab(tabName) {
20
12
  const info = exportTab(tabName);
21
13
  if (!info) {
@@ -30,10 +22,7 @@ export function attachTab(tabName) {
30
22
  console.log(` Status: ${info.status}`);
31
23
  console.log('');
32
24
  // Spawn Claude Code in the terminal, resuming the session
33
- const child = spawn(bin, [
34
- '--session-id', info.sessionId,
35
- '--resume',
36
- ], {
25
+ const child = spawn(bin, ['--session-id', info.sessionId, '--resume'], {
37
26
  cwd: info.workingDir,
38
27
  stdio: 'inherit', // Attach to terminal
39
28
  env: { ...process.env },
@@ -42,27 +31,3 @@ export function attachTab(tabName) {
42
31
  process.exit(code ?? 0);
43
32
  });
44
33
  }
45
- export function formatHandoffInfo(info) {
46
- const lines = [
47
- `Session Handoff — tab "${info.name}"`,
48
- '',
49
- `Session ID: ${info.sessionId}`,
50
- `Working dir: ${info.workingDir}`,
51
- `Status: ${info.status}`,
52
- `Last activity: ${info.lastActivity}`,
53
- '',
54
- 'To resume in terminal:',
55
- ` beecork attach ${info.name}`,
56
- '',
57
- 'Or manually:',
58
- ` cd ${info.workingDir}`,
59
- ` claude --session-id ${info.sessionId} --resume`,
60
- ];
61
- if (info.recentMessages.length > 0) {
62
- lines.push('', 'Recent context:');
63
- for (const msg of info.recentMessages) {
64
- lines.push(` [${msg.role}] ${msg.content.slice(0, 150)}${msg.content.length > 150 ? '...' : ''}`);
65
- }
66
- }
67
- return lines.join('\n');
68
- }
package/dist/cli/mcp.js CHANGED
@@ -1,14 +1,14 @@
1
1
  import fs from 'node:fs';
2
- import { getBeecorkHome } from '../util/paths.js';
3
- const MCP_CONFIG_PATH = `${getBeecorkHome()}/mcp-config.json`;
2
+ import { getMcpConfigPath } from '../util/paths.js';
4
3
  function loadMcpConfig() {
5
- if (fs.existsSync(MCP_CONFIG_PATH)) {
6
- return JSON.parse(fs.readFileSync(MCP_CONFIG_PATH, 'utf-8'));
4
+ const path = getMcpConfigPath();
5
+ if (fs.existsSync(path)) {
6
+ return JSON.parse(fs.readFileSync(path, 'utf-8'));
7
7
  }
8
8
  return { mcpServers: {} };
9
9
  }
10
10
  function saveMcpConfig(config) {
11
- fs.writeFileSync(MCP_CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
11
+ fs.writeFileSync(getMcpConfigPath(), JSON.stringify(config, null, 2), { mode: 0o600 });
12
12
  }
13
13
  export function mcpAdd(name, command, args) {
14
14
  const config = loadMcpConfig();