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
@@ -0,0 +1,364 @@
1
+ // MCP tool definitions for the Beecork MCP server.
2
+ // One array, one place to maintain the public tool surface. Handler logic
3
+ // lives in `./handlers.ts`; the server file (`./server.ts`) glues them.
4
+ export const TOOL_DEFINITIONS = [
5
+ {
6
+ name: 'beecork_remember',
7
+ description: 'Store a fact in Beecork\'s long-term memory. Use this for preferences, decisions, server addresses, outcomes, or anything the user might want recalled in future sessions.',
8
+ inputSchema: {
9
+ type: 'object',
10
+ properties: {
11
+ content: { type: 'string', description: 'The fact or information to remember' },
12
+ scope: { type: 'string', enum: ['global', 'project', 'tab', 'auto'], description: 'Where to store: global (about the user), project (about this folder), tab (about this conversation), or auto (Claude decides)' },
13
+ category: { type: 'string', description: 'For global scope: people, preferences, routines, or general' },
14
+ },
15
+ required: ['content'],
16
+ },
17
+ },
18
+ {
19
+ name: 'beecork_task_create',
20
+ description: 'Schedule a task that will run automatically. The task sends a message to a Beecork tab at the scheduled time.',
21
+ inputSchema: {
22
+ type: 'object',
23
+ properties: {
24
+ name: { type: 'string', description: 'Human-readable name for the task' },
25
+ scheduleType: {
26
+ type: 'string',
27
+ enum: ['at', 'every', 'cron'],
28
+ description: '"at" = one-time ISO datetime, "every" = interval like "30m"/"2h"/"1d", "cron" = cron expression like "0 9 * * 1"',
29
+ },
30
+ schedule: { type: 'string', description: 'The schedule value (ISO datetime, interval, or cron expression)' },
31
+ message: { type: 'string', description: 'The prompt/message to send when the task fires' },
32
+ tabName: { type: 'string', description: 'Which tab to send the message to (default: "default")' },
33
+ },
34
+ required: ['name', 'scheduleType', 'schedule', 'message'],
35
+ },
36
+ },
37
+ {
38
+ name: 'beecork_task_list',
39
+ description: 'List all scheduled tasks.',
40
+ inputSchema: { type: 'object', properties: {} },
41
+ },
42
+ {
43
+ name: 'beecork_task_delete',
44
+ description: 'Delete a scheduled task by ID.',
45
+ inputSchema: {
46
+ type: 'object',
47
+ properties: { id: { type: 'string', description: 'The ID of the task to delete' } },
48
+ required: ['id'],
49
+ },
50
+ },
51
+ {
52
+ name: 'beecork_cron_create',
53
+ description: '[DEPRECATED — use beecork_task_create] Schedule a task that will run automatically.',
54
+ inputSchema: {
55
+ type: 'object',
56
+ properties: {
57
+ name: { type: 'string', description: 'Human-readable name for the job' },
58
+ scheduleType: { type: 'string', enum: ['at', 'every', 'cron'] },
59
+ schedule: { type: 'string' },
60
+ message: { type: 'string' },
61
+ tabName: { type: 'string' },
62
+ },
63
+ required: ['name', 'scheduleType', 'schedule', 'message'],
64
+ },
65
+ },
66
+ {
67
+ name: 'beecork_cron_list',
68
+ description: '[DEPRECATED — use beecork_task_list] List all scheduled tasks.',
69
+ inputSchema: { type: 'object', properties: {} },
70
+ },
71
+ {
72
+ name: 'beecork_cron_delete',
73
+ description: '[DEPRECATED — use beecork_task_delete] Delete a scheduled task by ID.',
74
+ inputSchema: {
75
+ type: 'object',
76
+ properties: { id: { type: 'string' } },
77
+ required: ['id'],
78
+ },
79
+ },
80
+ {
81
+ name: 'beecork_watch_create',
82
+ description: 'Create a watcher that periodically runs a check command and triggers an action when a condition is met.',
83
+ inputSchema: {
84
+ type: 'object',
85
+ properties: {
86
+ name: { type: 'string', description: 'Human-readable name for the watcher' },
87
+ description: { type: 'string', description: 'What to watch (natural language)' },
88
+ checkCommand: { type: 'string', description: 'Shell command to run for checking' },
89
+ condition: { type: 'string', description: 'When to trigger: "contains X", "not contains X", "> N", "< N", "any", "error"' },
90
+ action: { type: 'string', enum: ['notify', 'fix', 'delegate'], description: 'What to do when triggered (default: notify)' },
91
+ actionDetails: { type: 'string', description: 'For fix: command to run. For delegate: tab name + message.' },
92
+ schedule: { type: 'string', description: 'How often to check: cron expression or interval like "5m", "1h"' },
93
+ },
94
+ required: ['name', 'checkCommand', 'condition', 'schedule'],
95
+ },
96
+ },
97
+ {
98
+ name: 'beecork_watch_list',
99
+ description: 'List all watchers.',
100
+ inputSchema: { type: 'object', properties: {} },
101
+ },
102
+ {
103
+ name: 'beecork_watch_delete',
104
+ description: 'Delete a watcher by ID.',
105
+ inputSchema: {
106
+ type: 'object',
107
+ properties: { id: { type: 'string' } },
108
+ required: ['id'],
109
+ },
110
+ },
111
+ {
112
+ name: 'beecork_tab_create',
113
+ description: 'Create a new Beecork tab (an isolated Claude session with its own working directory and history).',
114
+ inputSchema: {
115
+ type: 'object',
116
+ properties: {
117
+ name: { type: 'string', description: 'Tab name (alphanumeric + hyphens, max 32 chars). Cannot be "default".' },
118
+ workingDir: { type: 'string', description: 'Absolute path or ~/path for the tab\'s working directory' },
119
+ template: { type: 'string', description: 'Name of a tab template from config (sets workingDir + systemPrompt)' },
120
+ systemPrompt: { type: 'string', description: 'Tab-specific system prompt for Claude' },
121
+ },
122
+ required: ['name'],
123
+ },
124
+ },
125
+ {
126
+ name: 'beecork_tab_list',
127
+ description: 'List all Beecork tabs and their status.',
128
+ inputSchema: { type: 'object', properties: {} },
129
+ },
130
+ {
131
+ name: 'beecork_send_message',
132
+ description: 'Send a message to another Beecork tab. The message will be processed asynchronously by that tab\'s Claude subprocess.',
133
+ inputSchema: {
134
+ type: 'object',
135
+ properties: {
136
+ tabName: { type: 'string', description: 'Which tab to send to' },
137
+ message: { type: 'string', description: 'The message text' },
138
+ },
139
+ required: ['tabName', 'message'],
140
+ },
141
+ },
142
+ {
143
+ name: 'beecork_recall',
144
+ description: 'Search Beecork\'s memory and knowledge files for facts matching a query.',
145
+ inputSchema: {
146
+ type: 'object',
147
+ properties: {
148
+ query: { type: 'string', description: 'Search query (matches content substrings)' },
149
+ limit: { type: 'number', description: 'Max results (default 10, max 50)' },
150
+ },
151
+ required: ['query'],
152
+ },
153
+ },
154
+ {
155
+ name: 'beecork_notify',
156
+ description: 'Send a notification to the user via their configured channels (Telegram, Discord, etc.).',
157
+ inputSchema: {
158
+ type: 'object',
159
+ properties: {
160
+ message: { type: 'string', description: 'The notification text' },
161
+ urgent: { type: 'boolean', description: 'Prepend a 🚨 prefix' },
162
+ },
163
+ required: ['message'],
164
+ },
165
+ },
166
+ {
167
+ name: 'beecork_status',
168
+ description: 'Get a summary of Beecork state: tab/task/watcher/memory counts.',
169
+ inputSchema: { type: 'object', properties: {} },
170
+ },
171
+ {
172
+ name: 'beecork_send_media',
173
+ description: 'Send a generated or local media file (image/video/audio) through the user\'s channels.',
174
+ inputSchema: {
175
+ type: 'object',
176
+ properties: {
177
+ filePath: { type: 'string', description: 'Absolute path to the media file' },
178
+ caption: { type: 'string', description: 'Optional caption' },
179
+ tabName: { type: 'string', description: 'Tab to attribute the send to (default: "default")' },
180
+ },
181
+ required: ['filePath'],
182
+ },
183
+ },
184
+ {
185
+ name: 'beecork_channels',
186
+ description: 'List configured channels and their capabilities.',
187
+ inputSchema: { type: 'object', properties: {} },
188
+ },
189
+ {
190
+ name: 'beecork_cost',
191
+ description: 'Get cost summary (today, 7-day, 30-day, per-tab).',
192
+ inputSchema: {
193
+ type: 'object',
194
+ properties: { tabName: { type: 'string', description: 'Filter to a single tab' } },
195
+ },
196
+ },
197
+ {
198
+ name: 'beecork_failed_deliveries',
199
+ description: 'List recent messages that failed to deliver via channels.',
200
+ inputSchema: { type: 'object', properties: {} },
201
+ },
202
+ {
203
+ name: 'beecork_activity',
204
+ description: 'Get activity summary for the last N hours.',
205
+ inputSchema: {
206
+ type: 'object',
207
+ properties: { hours: { type: 'number', description: 'Lookback window (default 24)' } },
208
+ },
209
+ },
210
+ {
211
+ name: 'beecork_export_data',
212
+ description: 'Export historical data as JSON for analysis.',
213
+ inputSchema: {
214
+ type: 'object',
215
+ properties: {
216
+ type: { type: 'string', enum: ['costs', 'messages', 'crons'], description: 'What to export' },
217
+ days: { type: 'number', description: 'Lookback window in days (default 30)' },
218
+ },
219
+ required: ['type'],
220
+ },
221
+ },
222
+ {
223
+ name: 'beecork_handoff',
224
+ description: 'Get info to resume a tab\'s session in your terminal.',
225
+ inputSchema: {
226
+ type: 'object',
227
+ properties: { tabName: { type: 'string' } },
228
+ required: ['tabName'],
229
+ },
230
+ },
231
+ {
232
+ name: 'beecork_delegate',
233
+ description: 'Delegate a sub-task to another tab. The result will be sent back when complete.',
234
+ inputSchema: {
235
+ type: 'object',
236
+ properties: {
237
+ tabName: { type: 'string', description: 'Tab to delegate to' },
238
+ message: { type: 'string', description: 'The task description' },
239
+ returnToTab: { type: 'string', description: 'Tab to notify when complete (default: "default")' },
240
+ },
241
+ required: ['tabName', 'message'],
242
+ },
243
+ },
244
+ {
245
+ name: 'beecork_delegation_status',
246
+ description: 'List pending delegations.',
247
+ inputSchema: {
248
+ type: 'object',
249
+ properties: { tabName: { type: 'string', description: 'Filter by source or target tab' } },
250
+ },
251
+ },
252
+ {
253
+ name: 'beecork_project_create',
254
+ description: 'Register a project folder so the router can detect it from natural language.',
255
+ inputSchema: {
256
+ type: 'object',
257
+ properties: {
258
+ name: { type: 'string', description: 'Project name (also the folder name)' },
259
+ path: { type: 'string', description: 'Optional parent directory (must be under a scan path)' },
260
+ },
261
+ required: ['name'],
262
+ },
263
+ },
264
+ {
265
+ name: 'beecork_project_list',
266
+ description: 'List registered projects/folders.',
267
+ inputSchema: { type: 'object', properties: {} },
268
+ },
269
+ {
270
+ name: 'beecork_close_tab',
271
+ description: 'Permanently close a tab — stops its subprocess and deletes its message history.',
272
+ inputSchema: {
273
+ type: 'object',
274
+ properties: { tabName: { type: 'string' } },
275
+ required: ['tabName'],
276
+ },
277
+ },
278
+ {
279
+ name: 'beecork_generate_image',
280
+ description: 'Generate an image via a configured media provider (DALL-E, Recraft, etc.).',
281
+ inputSchema: {
282
+ type: 'object',
283
+ properties: {
284
+ prompt: { type: 'string' },
285
+ style: { type: 'string' },
286
+ provider: { type: 'string' },
287
+ },
288
+ required: ['prompt'],
289
+ },
290
+ },
291
+ {
292
+ name: 'beecork_generate_video',
293
+ description: 'Generate a video via a configured media provider.',
294
+ inputSchema: {
295
+ type: 'object',
296
+ properties: {
297
+ prompt: { type: 'string' },
298
+ duration: { type: 'number' },
299
+ provider: { type: 'string' },
300
+ },
301
+ required: ['prompt'],
302
+ },
303
+ },
304
+ {
305
+ name: 'beecork_generate_audio',
306
+ description: 'Generate audio via a configured media provider.',
307
+ inputSchema: {
308
+ type: 'object',
309
+ properties: {
310
+ prompt: { type: 'string' },
311
+ provider: { type: 'string' },
312
+ },
313
+ required: ['prompt'],
314
+ },
315
+ },
316
+ {
317
+ name: 'beecork_media_providers',
318
+ description: 'List configured media generators and what they support.',
319
+ inputSchema: { type: 'object', properties: {} },
320
+ },
321
+ {
322
+ name: 'beecork_knowledge',
323
+ description: 'List all stored knowledge (memories + knowledge files), optionally filtered by scope.',
324
+ inputSchema: {
325
+ type: 'object',
326
+ properties: { scope: { type: 'string', enum: ['global', 'project', 'tab', 'all'] } },
327
+ },
328
+ },
329
+ {
330
+ name: 'beecork_capabilities',
331
+ description: 'List available capability packs and their status.',
332
+ inputSchema: { type: 'object', properties: {} },
333
+ },
334
+ {
335
+ name: 'beecork_history',
336
+ description: 'Get the activity timeline for a given day/tab.',
337
+ inputSchema: {
338
+ type: 'object',
339
+ properties: {
340
+ date: { type: 'string', description: 'ISO date YYYY-MM-DD (default: today)' },
341
+ tabName: { type: 'string' },
342
+ limit: { type: 'number' },
343
+ },
344
+ },
345
+ },
346
+ {
347
+ name: 'beecork_replay',
348
+ description: 'Re-run a past activity event by ID.',
349
+ inputSchema: {
350
+ type: 'object',
351
+ properties: { eventId: { type: 'string' } },
352
+ required: ['eventId'],
353
+ },
354
+ },
355
+ {
356
+ name: 'beecork_store_search',
357
+ description: 'Search the Beecork community extension registry (npm packages starting with "beecork-").',
358
+ inputSchema: {
359
+ type: 'object',
360
+ properties: { query: { type: 'string' } },
361
+ required: ['query'],
362
+ },
363
+ },
364
+ ];
@@ -1,11 +1,6 @@
1
1
  export type { MediaGenerator, MediaType, GenerateOptions, GenerateResult } from './types.js';
2
2
  export { createMediaGenerator } from './factory.js';
3
- export { saveMedia, cleanupMedia, ensureMediaDir, getMediaDir, isOversized } from './store.js';
3
+ export { saveMedia, cleanupMedia, ensureMediaDir, isOversized } from './store.js';
4
4
  import type { MediaGenerator } from './types.js';
5
- export interface MediaGeneratorConfig {
6
- provider: string;
7
- apiKey?: string;
8
- model?: string;
9
- style?: string;
10
- }
5
+ import type { MediaGeneratorConfig } from '../types.js';
11
6
  export declare function initMediaGenerators(configs?: MediaGeneratorConfig[]): MediaGenerator[];
@@ -1,5 +1,5 @@
1
1
  export { createMediaGenerator } from './factory.js';
2
- export { saveMedia, cleanupMedia, ensureMediaDir, getMediaDir, isOversized } from './store.js';
2
+ export { saveMedia, cleanupMedia, ensureMediaDir, isOversized } from './store.js';
3
3
  import { createMediaGenerator } from './factory.js';
4
4
  export function initMediaGenerators(configs) {
5
5
  if (!configs || configs.length === 0)
@@ -13,7 +13,7 @@ export interface ActivitySummary {
13
13
  period: string;
14
14
  messagesReceived: number;
15
15
  messagesFromAssistant: number;
16
- cronJobsFired: number;
16
+ tasksFired: number;
17
17
  memoriesCreated: number;
18
18
  totalCost: number;
19
19
  activeTabsCount: number;
@@ -4,9 +4,12 @@ export function getCostSummary() {
4
4
  const today = db.prepare("SELECT COALESCE(SUM(cost_usd), 0) as total FROM messages WHERE created_at > date('now')").get().total;
5
5
  const last7 = db.prepare("SELECT COALESCE(SUM(cost_usd), 0) as total FROM messages WHERE created_at > date('now', '-7 days')").get().total;
6
6
  const last30 = db.prepare("SELECT COALESCE(SUM(cost_usd), 0) as total FROM messages WHERE created_at > date('now', '-30 days')").get().total;
7
+ // Per-tab summed over the last 30 days only — the unbounded version grew
8
+ // linearly with the messages table and made /cost noticeably slow over time.
7
9
  const perTab = db.prepare(`
8
10
  SELECT t.name, COALESCE(SUM(m.cost_usd), 0) as cost, COUNT(m.id) as messages
9
11
  FROM tabs t LEFT JOIN messages m ON m.tab_id = t.id
12
+ AND m.created_at > date('now', '-30 days')
10
13
  GROUP BY t.id ORDER BY cost DESC
11
14
  `).all();
12
15
  return { today, last7Days: last7, last30Days: last30, perTab };
@@ -16,7 +19,7 @@ export function getActivitySummary(hours = 24) {
16
19
  const sinceDate = new Date(Date.now() - hours * 3600000).toISOString();
17
20
  const messagesReceived = db.prepare('SELECT COUNT(*) as c FROM messages WHERE role = ? AND created_at > ?').get('user', sinceDate).c;
18
21
  const messagesFromAssistant = db.prepare('SELECT COUNT(*) as c FROM messages WHERE role = ? AND created_at > ?').get('assistant', sinceDate).c;
19
- const cronJobsFired = db.prepare('SELECT COUNT(*) as c FROM tasks WHERE last_run_at > ?').get(sinceDate).c;
22
+ const tasksFired = db.prepare('SELECT COUNT(*) as c FROM tasks WHERE last_run_at > ?').get(sinceDate).c;
20
23
  const memoriesCreated = db.prepare('SELECT COUNT(*) as c FROM memories WHERE created_at > ?').get(sinceDate).c;
21
24
  const totalCost = db.prepare('SELECT COALESCE(SUM(cost_usd), 0) as total FROM messages WHERE created_at > ?').get(sinceDate).total;
22
25
  const activeTabsCount = db.prepare('SELECT COUNT(DISTINCT tab_id) as c FROM messages WHERE created_at > ?').get(sinceDate).c;
@@ -24,7 +27,7 @@ export function getActivitySummary(hours = 24) {
24
27
  period: `Last ${hours} hours`,
25
28
  messagesReceived,
26
29
  messagesFromAssistant,
27
- cronJobsFired,
30
+ tasksFired,
28
31
  memoriesCreated,
29
32
  totalCost,
30
33
  activeTabsCount,
@@ -85,7 +88,7 @@ export function formatActivitySummary(summary) {
85
88
  `📊 Activity (${summary.period})`,
86
89
  ` Messages in: ${summary.messagesReceived}`,
87
90
  ` Messages out: ${summary.messagesFromAssistant}`,
88
- ` Cron jobs fired: ${summary.cronJobsFired}`,
91
+ ` Tasks fired: ${summary.tasksFired}`,
89
92
  ` Memories created: ${summary.memoriesCreated}`,
90
93
  ` Active tabs: ${summary.activeTabsCount}`,
91
94
  ` Cost: $${summary.totalCost.toFixed(4)}`,
@@ -1,3 +1,4 @@
1
1
  export type { Project, RouteDecision, RoutingContext } from './types.js';
2
- export { discoverProjects, createProject, listProjects, getProject, ensureCategory, touchProject, closeTab, getWorkspaceRoot, getManagedWorkspace } from './manager.js';
3
- export { routeMessage, setUserContext } from './router.js';
2
+ export { discoverProjects, createProject, listProjects, getProject, ensureCategory, touchProject } from './manager.js';
3
+ export { routeMessage, setUserContext, resolveProjectRoute } from './router.js';
4
+ export type { RouteResult } from './router.js';
@@ -1,2 +1,2 @@
1
- export { discoverProjects, createProject, listProjects, getProject, ensureCategory, touchProject, closeTab, getWorkspaceRoot, getManagedWorkspace } from './manager.js';
2
- export { routeMessage, setUserContext } from './router.js';
1
+ export { discoverProjects, createProject, listProjects, getProject, ensureCategory, touchProject } from './manager.js';
2
+ export { routeMessage, setUserContext, resolveProjectRoute } from './router.js';
@@ -5,7 +5,7 @@ export declare function getWorkspaceRoot(): string;
5
5
  export declare function getManagedWorkspace(): string;
6
6
  /** Discover projects in scan paths (look for git repos, package.json, etc.) */
7
7
  export declare function discoverProjects(scanPaths?: string[]): Project[];
8
- /** Create a new project */
8
+ /** Create a new project. parentDir must resolve under an allowed root. */
9
9
  export declare function createProject(name: string, parentDir?: string): Project;
10
10
  /** Ensure a managed category exists (lazy creation) */
11
11
  export declare function ensureCategory(name: string): Project;
@@ -15,5 +15,3 @@ export declare function listProjects(): Project[];
15
15
  export declare function getProject(name: string): Project | null;
16
16
  /** Update last used timestamp */
17
17
  export declare function touchProject(name: string): void;
18
- /** Close (permanently delete) a tab */
19
- export declare function closeTab(tabName: string): boolean;
@@ -4,6 +4,9 @@ import { v4 as uuidv4 } from 'uuid';
4
4
  import { getDb } from '../db/index.js';
5
5
  import { getConfig } from '../config.js';
6
6
  import { logger } from '../util/logger.js';
7
+ function rowToProject(r) {
8
+ return { id: r.id, name: r.name, path: r.path, type: r.type, lastUsedAt: r.last_used_at, createdAt: r.created_at };
9
+ }
7
10
  /** Get the workspace root from config */
8
11
  export function getWorkspaceRoot() {
9
12
  const config = getConfig();
@@ -67,16 +70,29 @@ export function discoverProjects(scanPaths) {
67
70
  }
68
71
  return projects;
69
72
  }
70
- /** Create a new project */
73
+ /** Create a new project. parentDir must resolve under an allowed root. */
71
74
  export function createProject(name, parentDir) {
72
- const parent = parentDir || getWorkspaceRoot();
75
+ const requestedParent = parentDir || getWorkspaceRoot();
76
+ const resolvedParent = path.resolve(requestedParent.startsWith('~')
77
+ ? requestedParent.replace('~', process.env.HOME || '')
78
+ : requestedParent);
79
+ // Allowlist: parent must resolve under workspace root or one of the configured scan paths.
80
+ const config = getConfig();
81
+ const allowedRoots = [
82
+ getWorkspaceRoot(),
83
+ ...(config.projectScanPaths ?? []),
84
+ ].map(r => path.resolve(r.startsWith('~') ? r.replace('~', process.env.HOME || '') : r));
85
+ const isAllowed = allowedRoots.some(root => resolvedParent === root || resolvedParent.startsWith(root + path.sep));
86
+ if (!isAllowed) {
87
+ throw new Error(`Project parent directory must be under workspace root or a configured scan path. Allowed: ${allowedRoots.join(', ')}`);
88
+ }
73
89
  // Sanitize name to prevent path traversal
74
90
  const safeName = path.basename(name.replace(/\.\./g, ''));
75
91
  if (!safeName)
76
92
  throw new Error('Invalid project name');
77
- const projectPath = path.resolve(parent, safeName);
93
+ const projectPath = path.resolve(resolvedParent, safeName);
78
94
  // Ensure resolved path is within the parent directory
79
- if (!projectPath.startsWith(path.resolve(parent))) {
95
+ if (!projectPath.startsWith(resolvedParent)) {
80
96
  throw new Error('Project path must be within the parent directory');
81
97
  }
82
98
  if (fs.existsSync(projectPath)) {
@@ -101,9 +117,8 @@ export function ensureCategory(name) {
101
117
  fs.mkdirSync(categoryPath, { recursive: true });
102
118
  const db = getDb();
103
119
  const existing = db.prepare('SELECT * FROM projects WHERE name = ? AND type = ?').get(name, 'category');
104
- if (existing) {
105
- return { id: existing.id, name: existing.name, path: existing.path, type: 'category', lastUsedAt: existing.last_used_at, createdAt: existing.created_at };
106
- }
120
+ if (existing)
121
+ return rowToProject(existing);
107
122
  const id = uuidv4();
108
123
  db.prepare('INSERT INTO projects (id, name, path, type) VALUES (?, ?, ?, ?)').run(id, name, categoryPath, 'category');
109
124
  return { id, name, path: categoryPath, type: 'category', lastUsedAt: new Date().toISOString(), createdAt: new Date().toISOString() };
@@ -112,31 +127,17 @@ export function ensureCategory(name) {
112
127
  export function listProjects() {
113
128
  const db = getDb();
114
129
  const rows = db.prepare('SELECT * FROM projects ORDER BY type, last_used_at DESC').all();
115
- return rows.map(r => ({
116
- id: r.id, name: r.name, path: r.path, type: r.type,
117
- lastUsedAt: r.last_used_at, createdAt: r.created_at,
118
- }));
130
+ return rows.map(rowToProject);
119
131
  }
120
132
  /** Get a project by name */
121
133
  export function getProject(name) {
122
134
  const db = getDb();
123
135
  const row = db.prepare('SELECT * FROM projects WHERE name = ?').get(name);
124
- if (!row)
125
- return null;
126
- return { id: row.id, name: row.name, path: row.path, type: row.type, lastUsedAt: row.last_used_at, createdAt: row.created_at };
136
+ return row ? rowToProject(row) : null;
127
137
  }
128
138
  /** Update last used timestamp */
129
139
  export function touchProject(name) {
130
140
  getDb().prepare("UPDATE projects SET last_used_at = datetime('now') WHERE name = ?").run(name);
131
141
  }
132
- /** Close (permanently delete) a tab */
133
- export function closeTab(tabName) {
134
- const db = getDb();
135
- const tab = db.prepare('SELECT id FROM tabs WHERE name = ?').get(tabName);
136
- if (!tab)
137
- return false;
138
- db.prepare('DELETE FROM messages WHERE tab_id = ?').run(tab.id);
139
- db.prepare('DELETE FROM tabs WHERE id = ?').run(tab.id);
140
- logger.info(`Tab permanently closed: ${tabName}`);
141
- return true;
142
- }
142
+ // closeTab moved to TabManager.closeTab see src/session/manager.ts. It now kills
143
+ // the subprocess and deletes the rows in one place rather than the caller doing both.
@@ -3,3 +3,13 @@ import type { RouteDecision, RoutingContext } from './types.js';
3
3
  export declare function routeMessage(message: string, context?: RoutingContext): RouteDecision;
4
4
  /** Update current context for a user */
5
5
  export declare function setUserContext(userId: string, projectName: string, tabName: string): void;
6
+ export interface RouteResult {
7
+ effectiveTabName: string;
8
+ projectPath?: string;
9
+ confirmationMessage?: string;
10
+ }
11
+ /**
12
+ * Shared project routing logic — resolves which tab/project to use for a message.
13
+ * Pulled out of channels/ so all routing decisions live in one place.
14
+ */
15
+ export declare function resolveProjectRoute(rawPrompt: string, tabName: string, text: string, userId: string): Promise<RouteResult>;
@@ -191,3 +191,31 @@ function checkLearnedRouting(message) {
191
191
  }
192
192
  return null;
193
193
  }
194
+ /**
195
+ * Shared project routing logic — resolves which tab/project to use for a message.
196
+ * Pulled out of channels/ so all routing decisions live in one place.
197
+ */
198
+ export async function resolveProjectRoute(rawPrompt, tabName, text, userId) {
199
+ if (tabName !== 'default' || text.startsWith('/tab ')) {
200
+ return { effectiveTabName: tabName };
201
+ }
202
+ try {
203
+ const decision = routeMessage(rawPrompt, { userId });
204
+ if (decision.needsConfirmation) {
205
+ const projects = listProjects().filter((p) => p.type === 'user-project');
206
+ const options = projects.map((p, i) => `${i + 1}) ${p.name}`).join('\n');
207
+ return {
208
+ effectiveTabName: tabName,
209
+ confirmationMessage: `Which project?\n${options}\n\nReply with the number, or just send your message with /project <name> first.`,
210
+ };
211
+ }
212
+ setUserContext(userId, decision.project.name, decision.tabName);
213
+ return {
214
+ effectiveTabName: decision.tabName,
215
+ projectPath: decision.project.path,
216
+ };
217
+ }
218
+ catch {
219
+ return { effectiveTabName: tabName };
220
+ }
221
+ }
@@ -33,6 +33,10 @@ export declare class TabManager {
33
33
  private queryTab;
34
34
  /** Stop a tab's running subprocess */
35
35
  stopTab(tabName: string): void;
36
+ /** Update a tab's system_prompt. Returns true if the tab existed. */
37
+ setSystemPrompt(tabName: string, systemPrompt: string): boolean;
38
+ /** Close a tab — stop subprocess and delete its rows. Returns true if the tab existed. */
39
+ closeTab(tabName: string): boolean;
36
40
  /** Stop all running subprocesses (clean shutdown) */
37
41
  stopAll(): void;
38
42
  /** Process pending messages from MCP server IPC */