beecork 1.4.11 → 1.5.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 (66) hide show
  1. package/dist/channels/admin.d.ts +10 -0
  2. package/dist/channels/admin.js +20 -0
  3. package/dist/channels/command-handler.d.ts +2 -10
  4. package/dist/channels/command-handler.js +47 -73
  5. package/dist/channels/discord.d.ts +1 -3
  6. package/dist/channels/discord.js +28 -28
  7. package/dist/channels/loader.js +0 -1
  8. package/dist/channels/send-helpers.d.ts +19 -0
  9. package/dist/channels/send-helpers.js +21 -0
  10. package/dist/channels/telegram.d.ts +1 -9
  11. package/dist/channels/telegram.js +46 -71
  12. package/dist/channels/types.d.ts +2 -10
  13. package/dist/channels/voice-state.d.ts +29 -0
  14. package/dist/channels/voice-state.js +43 -0
  15. package/dist/channels/webhook.d.ts +1 -1
  16. package/dist/channels/webhook.js +68 -24
  17. package/dist/channels/whatsapp.d.ts +1 -3
  18. package/dist/channels/whatsapp.js +79 -74
  19. package/dist/cli/doctor.js +5 -2
  20. package/dist/cli/handoff.js +6 -6
  21. package/dist/config.d.ts +5 -1
  22. package/dist/config.js +17 -14
  23. package/dist/daemon.js +29 -17
  24. package/dist/dashboard/html.js +20 -8
  25. package/dist/dashboard/routes.d.ts +17 -0
  26. package/dist/dashboard/routes.js +559 -0
  27. package/dist/dashboard/server.js +33 -488
  28. package/dist/db/index.js +16 -2
  29. package/dist/db/migrations.js +44 -8
  30. package/dist/mcp/handlers.d.ts +37 -0
  31. package/dist/mcp/handlers.js +451 -0
  32. package/dist/mcp/server.js +25 -849
  33. package/dist/mcp/tool-definitions.d.ts +1225 -0
  34. package/dist/mcp/tool-definitions.js +364 -0
  35. package/dist/media/index.d.ts +2 -7
  36. package/dist/media/index.js +1 -1
  37. package/dist/observability/analytics.d.ts +1 -1
  38. package/dist/observability/analytics.js +6 -3
  39. package/dist/projects/index.d.ts +3 -2
  40. package/dist/projects/index.js +2 -2
  41. package/dist/projects/manager.d.ts +1 -3
  42. package/dist/projects/manager.js +26 -25
  43. package/dist/projects/router.d.ts +10 -0
  44. package/dist/projects/router.js +28 -0
  45. package/dist/session/manager.d.ts +4 -0
  46. package/dist/session/manager.js +48 -42
  47. package/dist/session/subprocess.d.ts +1 -0
  48. package/dist/session/subprocess.js +21 -0
  49. package/dist/session/tab-store.d.ts +28 -0
  50. package/dist/session/tab-store.js +77 -0
  51. package/dist/tasks/scheduler.d.ts +6 -0
  52. package/dist/tasks/scheduler.js +52 -13
  53. package/dist/tasks/store.js +6 -6
  54. package/dist/timeline/query.js +6 -2
  55. package/dist/types.d.ts +15 -0
  56. package/dist/util/paths.d.ts +1 -0
  57. package/dist/util/paths.js +4 -1
  58. package/dist/util/rate-limiter.js +8 -0
  59. package/dist/util/text.d.ts +21 -1
  60. package/dist/util/text.js +25 -1
  61. package/dist/watchers/scheduler.js +2 -3
  62. package/package.json +1 -1
  63. package/dist/users/index.d.ts +0 -2
  64. package/dist/users/index.js +0 -1
  65. package/dist/users/service.d.ts +0 -17
  66. package/dist/users/service.js +0 -46
package/dist/daemon.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import { getConfig } from './config.js';
3
3
  import { getDb, closeDb } from './db/index.js';
4
+ import { TabStore } from './session/tab-store.js';
4
5
  import { TabManager } from './session/manager.js';
5
6
  import { ChannelRegistry, TelegramChannel, WhatsAppChannel } from './channels/index.js';
6
7
  import { TaskScheduler } from './tasks/scheduler.js';
@@ -188,7 +189,11 @@ async function main() {
188
189
  }
189
190
  }, 5000);
190
191
  // 11. Handle shutdown
191
- const shutdown = async () => {
192
+ let shuttingDown = false;
193
+ const shutdown = async (exitCode = 0) => {
194
+ if (shuttingDown)
195
+ return;
196
+ shuttingDown = true;
192
197
  logger.info('Beecork daemon shutting down...');
193
198
  // Send shutdown notification before stopping (with timeout to prevent hanging)
194
199
  try {
@@ -202,17 +207,20 @@ async function main() {
202
207
  watcherScheduler.stopAll();
203
208
  closeDb();
204
209
  const pidPath = getPidPath();
205
- if (fs.existsSync(pidPath))
206
- fs.unlinkSync(pidPath);
210
+ try {
211
+ if (fs.existsSync(pidPath))
212
+ fs.unlinkSync(pidPath);
213
+ }
214
+ catch { /* race or already gone */ }
207
215
  removeRuntimeInfo();
208
216
  logActivity('system_event', 'Beecork daemon stopped');
209
217
  logger.info('Beecork daemon stopped.');
210
218
  logger.close();
211
- process.exit(0);
219
+ process.exit(exitCode);
212
220
  };
213
221
  shutdownFn = shutdown;
214
- process.on('SIGTERM', shutdown);
215
- process.on('SIGINT', shutdown);
222
+ process.on('SIGTERM', () => shutdown(0));
223
+ process.on('SIGINT', () => shutdown(0));
216
224
  // Resilience: catch unhandled errors to prevent silent daemon death
217
225
  process.on('unhandledRejection', (reason) => {
218
226
  logger.error('Unhandled rejection:', reason);
@@ -220,9 +228,8 @@ async function main() {
220
228
  });
221
229
  process.on('uncaughtException', async (err) => {
222
230
  logger.error('Uncaught exception — shutting down gracefully:', err);
223
- if (shutdownFn)
224
- await shutdownFn();
225
- process.exit(1);
231
+ // Exit non-zero so the supervisor (launchd/systemd/Task Scheduler) restarts us.
232
+ await shutdown(1);
226
233
  });
227
234
  logger.info(`Beecork daemon ready (home: ${getBeecorkHome()})`);
228
235
  logActivity('system_event', 'Beecork daemon started');
@@ -247,12 +254,13 @@ async function main() {
247
254
  }
248
255
  async function recoverCrashedTabs() {
249
256
  const db = getDb();
250
- const crashedRows = db.prepare(`SELECT * FROM tabs WHERE status = 'running'`).all();
257
+ // Find tabs that were running when daemon stopped
258
+ const crashedRows = TabStore.findRunning(db);
251
259
  if (crashedRows.length === 0)
252
260
  return;
253
261
  logger.info(`Found ${crashedRows.length} tabs that were running when daemon stopped`);
254
262
  for (const row of crashedRows) {
255
- logger.info(`Recovering tab: ${row.name} (session: ${row.session_id})`);
263
+ logger.info(`Recovering tab: ${row.name} (session: ${row.sessionId})`);
256
264
  // Get last few messages for context
257
265
  const recentMessages = db.prepare(`SELECT role, content FROM messages
258
266
  WHERE tab_id = ? ORDER BY created_at DESC LIMIT 5`).all(row.id);
@@ -267,13 +275,17 @@ async function recoverCrashedTabs() {
267
275
  `[SYSTEM: Please acknowledge you are back and ready for new instructions.]`,
268
276
  ].join('\n');
269
277
  // Reset status so TabManager can use it
270
- db.prepare(`UPDATE tabs SET status = 'idle', pid = NULL WHERE id = ?`).run(row.id);
271
- // Resume the session
272
- tabManager.sendMessage(row.name, recoveryPrompt, { resume: true }).catch(err => {
278
+ TabStore.setIdleById(row.id, db);
279
+ // Await the resume + notify on outcome. The previous fire-and-forget
280
+ // pattern told users "recovered" before the resume actually succeeded.
281
+ try {
282
+ await tabManager.sendMessage(row.name, recoveryPrompt, { resume: true });
283
+ await broadcastNotify(`Beecork restarted. Recovered tab "${row.name}" — session resumed.`).catch(() => { });
284
+ }
285
+ catch (err) {
273
286
  logger.error(`Failed to recover tab ${row.name}:`, err);
274
- });
275
- // Notify via all channels
276
- await broadcastNotify(`Beecork restarted. Recovered tab "${row.name}" — session resumed.`).catch(() => { });
287
+ await broadcastNotify(`Beecork restarted. Tab "${row.name}" recovery FAILED — ${err instanceof Error ? err.message : String(err)}.`).catch(() => { });
288
+ }
277
289
  }
278
290
  }
279
291
  main().catch(err => {
@@ -314,8 +314,10 @@ export function getDashboardHtml(token) {
314
314
  dot.className = 'status-dot status-error';
315
315
  status.textContent = 'stopped';
316
316
  }
317
+ // Server returns both new "tasks" and legacy "cronJobs" — prefer the new one.
318
+ const taskCount = s.tasks ?? s.cronJobs ?? 0;
317
319
  document.getElementById('stats').textContent =
318
- s.tabs + ' tabs | ' + s.cronJobs + ' crons | ' + s.memories + ' mem';
320
+ s.tabs + ' tabs | ' + taskCount + ' tasks | ' + s.memories + ' mem';
319
321
  } catch {}
320
322
  }
321
323
 
@@ -349,7 +351,8 @@ export function getDashboardHtml(token) {
349
351
  }).join('');
350
352
  }
351
353
 
352
- async function selectTab(name) {
354
+ async function selectTab(name, opts) {
355
+ const fromUser = !opts || opts.fromUser !== false;
353
356
  selectedTab = name;
354
357
  document.getElementById('msg-title').textContent = name;
355
358
  document.getElementById('btn-delete-tab').classList.remove('hidden');
@@ -382,19 +385,28 @@ export function getDashboardHtml(token) {
382
385
  if (m.tokens_in) meta.push(m.tokens_in.toLocaleString() + ' in');
383
386
  if (m.tokens_out) meta.push(m.tokens_out.toLocaleString() + ' out');
384
387
  const metaStr = meta.length ? '<span class="text-xs text-gray-600 ml-2">' + meta.join(' | ') + '</span>' : '';
385
- const content = m.content.length > 2000 ? m.content.slice(0, 2000) + '\\n\\n... (' + m.content.length.toLocaleString() + ' chars total)' : m.content;
388
+ const truncated = m.content.length > 2000;
389
+ const preview = truncated ? m.content.slice(0, 2000) : m.content;
390
+ const rest = truncated ? m.content.slice(2000) : '';
391
+ const body = truncated
392
+ ? esc(preview) + '<details class="mt-1"><summary class="text-xs text-honey-400 cursor-pointer">Show ' + (m.content.length - 2000).toLocaleString() + ' more chars</summary>' + esc(rest) + '</details>'
393
+ : esc(preview);
386
394
 
387
395
  return '<div class="' + cls + ' rounded-lg p-3">' +
388
396
  '<div class="flex items-center justify-between mb-1">' +
389
397
  '<span class="text-xs font-semibold ' + (m.role === 'user' ? 'text-honey-400' : 'text-gray-400') + '">' + label + metaStr + '</span>' +
390
398
  '<span class="text-xs text-gray-600">' + timeAgo(m.created_at) + '</span>' +
391
399
  '</div>' +
392
- '<pre class="text-sm text-gray-300 whitespace-pre-wrap break-words font-sans leading-relaxed">' + esc(content) + '</pre>' +
400
+ '<pre class="text-sm text-gray-300 whitespace-pre-wrap break-words font-sans leading-relaxed">' + body + '</pre>' +
393
401
  '</div>';
394
402
  }).join('');
395
403
 
396
- list.scrollTop = list.scrollHeight;
397
- document.getElementById('msg-input').focus();
404
+ // Only jump to bottom + steal focus on user-initiated selection,
405
+ // not on the 8s background refresh, so typing isn't interrupted.
406
+ if (fromUser) {
407
+ list.scrollTop = list.scrollHeight;
408
+ document.getElementById('msg-input').focus();
409
+ }
398
410
  }
399
411
 
400
412
  // --- Send message ---
@@ -683,7 +695,7 @@ export function getDashboardHtml(token) {
683
695
  const day = c.day.slice(5);
684
696
  return '<div class="flex-1 flex flex-col items-center gap-1">' +
685
697
  '<span class="text-xs text-gray-500 font-mono">$' + c.total_cost.toFixed(3) + '</span>' +
686
- '<div class="w-full cost-bar" style="height:' + Math.max(pct, 2) + '%" title="' + c.day + ': $' + c.total_cost.toFixed(4) + ' (' + c.message_count + ' msgs)"></div>' +
698
+ '<div class="w-full cost-bar" style="height:' + Math.max(pct, 2) + '%" title="' + esc(c.day) + ': $' + c.total_cost.toFixed(4) + ' (' + c.message_count + ' msgs)"></div>' +
687
699
  '<span class="text-xs text-gray-600 font-mono">' + day + '</span>' +
688
700
  '</div>';
689
701
  }).join('') +
@@ -762,7 +774,7 @@ export function getDashboardHtml(token) {
762
774
  loadTabs();
763
775
  setInterval(loadStatus, 10000);
764
776
  // Periodically reload messages for selected tab
765
- setInterval(() => { if (selectedTab) selectTab(selectedTab); }, 8000);
777
+ setInterval(() => { if (selectedTab) selectTab(selectedTab, { fromUser: false }); }, 8000);
766
778
  </script>
767
779
  </body>
768
780
  </html>`;
@@ -0,0 +1,17 @@
1
+ import http from 'node:http';
2
+ declare function json(res: http.ServerResponse, data: unknown, status?: number): void;
3
+ export interface RouteCtx {
4
+ req: http.IncomingMessage;
5
+ res: http.ServerResponse;
6
+ url: URL;
7
+ path: string;
8
+ }
9
+ type RouteHandler = (ctx: RouteCtx) => Promise<void> | void;
10
+ interface RouteEntry {
11
+ method: string;
12
+ test: (path: string) => boolean;
13
+ handler: RouteHandler;
14
+ }
15
+ export declare const ROUTES: RouteEntry[];
16
+ export declare function dispatch(method: string, path: string): RouteEntry | null;
17
+ export { json };