@anastops/mcp-server 1.1.2 → 2.0.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.
Files changed (57) hide show
  1. package/dist/handlers/handlers.base.d.ts +20 -4
  2. package/dist/handlers/handlers.base.d.ts.map +1 -1
  3. package/dist/handlers/handlers.base.js +32 -17
  4. package/dist/handlers/handlers.base.js.map +1 -1
  5. package/dist/handlers/index.d.ts +5 -4
  6. package/dist/handlers/index.d.ts.map +1 -1
  7. package/dist/handlers/index.js +126 -135
  8. package/dist/handlers/index.js.map +1 -1
  9. package/dist/handlers/tool-definitions.d.ts +1016 -1564
  10. package/dist/handlers/tool-definitions.d.ts.map +1 -1
  11. package/dist/handlers/tool-definitions.js +617 -388
  12. package/dist/handlers/tool-definitions.js.map +1 -1
  13. package/dist/handlers/types.d.ts +34 -0
  14. package/dist/handlers/types.d.ts.map +1 -1
  15. package/dist/handlers/v3/agent-tools.d.ts +280 -0
  16. package/dist/handlers/v3/agent-tools.d.ts.map +1 -0
  17. package/dist/handlers/v3/agent-tools.js +460 -0
  18. package/dist/handlers/v3/agent-tools.js.map +1 -0
  19. package/dist/handlers/v3/index.d.ts +31 -0
  20. package/dist/handlers/v3/index.d.ts.map +1 -0
  21. package/dist/handlers/v3/index.js +56 -0
  22. package/dist/handlers/v3/index.js.map +1 -0
  23. package/dist/handlers/v3/routing-integration-example.d.ts +258 -0
  24. package/dist/handlers/v3/routing-integration-example.d.ts.map +1 -0
  25. package/dist/handlers/v3/routing-integration-example.js +454 -0
  26. package/dist/handlers/v3/routing-integration-example.js.map +1 -0
  27. package/dist/handlers/v3/routing-middleware.d.ts +191 -0
  28. package/dist/handlers/v3/routing-middleware.d.ts.map +1 -0
  29. package/dist/handlers/v3/routing-middleware.js +425 -0
  30. package/dist/handlers/v3/routing-middleware.js.map +1 -0
  31. package/dist/handlers/v3/session-tools.d.ts +303 -0
  32. package/dist/handlers/v3/session-tools.d.ts.map +1 -0
  33. package/dist/handlers/v3/session-tools.js +945 -0
  34. package/dist/handlers/v3/session-tools.js.map +1 -0
  35. package/dist/handlers/v3/task-tools.d.ts +18 -0
  36. package/dist/handlers/v3/task-tools.d.ts.map +1 -0
  37. package/dist/handlers/v3/task-tools.js +1947 -0
  38. package/dist/handlers/v3/task-tools.js.map +1 -0
  39. package/dist/handlers/v3/utility-tools.d.ts +99 -0
  40. package/dist/handlers/v3/utility-tools.d.ts.map +1 -0
  41. package/dist/handlers/v3/utility-tools.js +878 -0
  42. package/dist/handlers/v3/utility-tools.js.map +1 -0
  43. package/dist/handlers.d.ts +1 -1
  44. package/dist/handlers.d.ts.map +1 -1
  45. package/dist/handlers.js +1 -1
  46. package/dist/handlers.js.map +1 -1
  47. package/dist/index.js +12 -2
  48. package/dist/index.js.map +1 -1
  49. package/dist/persistence.d.ts +11 -0
  50. package/dist/persistence.d.ts.map +1 -1
  51. package/dist/persistence.js +77 -0
  52. package/dist/persistence.js.map +1 -1
  53. package/dist/schemas.d.ts +95 -8
  54. package/dist/schemas.d.ts.map +1 -1
  55. package/dist/schemas.js +97 -2
  56. package/dist/schemas.js.map +1 -1
  57. package/package.json +3 -3
@@ -0,0 +1,945 @@
1
+ /**
2
+ * MCP v3.0 Session Management Tools
3
+ * Consolidated session management following aggressive consolidation plan
4
+ *
5
+ * Tools:
6
+ * - session_manage: Create, fork, archive, delete operations
7
+ * - session_query: Status, list, report query operations
8
+ * - session_queue_config: Configure and query queue settings
9
+ *
10
+ * @see docs/MCP-V3-AGGRESSIVE-CONSOLIDATION.md
11
+ */
12
+ import { SessionManager, SessionForkService, addBreadcrumb, } from '@anastops/core';
13
+ import { getPersistence } from '../../persistence.js';
14
+ import { safePersist, formatSessionReport, tasks, agents, artifacts } from '../handlers.base.js';
15
+ // Shared instances
16
+ const sessionManager = new SessionManager();
17
+ const forkService = new SessionForkService(sessionManager);
18
+ // ============================================================================
19
+ // TOOL SCHEMAS
20
+ // ============================================================================
21
+ /**
22
+ * session_manage tool schema
23
+ *
24
+ * Handles create, fork, archive, and delete operations on sessions.
25
+ *
26
+ * @example Create a new session
27
+ * ```typescript
28
+ * session_manage({
29
+ * operation: 'create',
30
+ * objective: 'Build authentication system',
31
+ * concurrency: 3,
32
+ * auto_execute: true
33
+ * })
34
+ * ```
35
+ *
36
+ * @example Fork a session
37
+ * ```typescript
38
+ * session_manage({
39
+ * operation: 'fork',
40
+ * session_id: 'abc123',
41
+ * reason: 'Explore OAuth approach'
42
+ * })
43
+ * ```
44
+ *
45
+ * @example Archive a session
46
+ * ```typescript
47
+ * session_manage({
48
+ * operation: 'archive',
49
+ * session_id: 'abc123'
50
+ * })
51
+ * ```
52
+ *
53
+ * @example Delete a session
54
+ * ```typescript
55
+ * session_manage({
56
+ * operation: 'delete',
57
+ * session_id: 'abc123',
58
+ * confirm: true
59
+ * })
60
+ * ```
61
+ */
62
+ export const sessionManageSchema = {
63
+ name: 'session_manage',
64
+ description: 'Manage sessions: create new sessions, fork existing sessions, archive completed sessions, or delete sessions permanently',
65
+ inputSchema: {
66
+ type: 'object',
67
+ properties: {
68
+ operation: {
69
+ type: 'string',
70
+ enum: ['create', 'fork', 'archive', 'delete'],
71
+ description: 'Operation to perform: create=spawn new session, fork=create alternative branch, archive=mark completed, delete=permanently remove',
72
+ },
73
+ // CREATE operation parameters
74
+ objective: {
75
+ type: 'string',
76
+ description: 'Required for create: High-level objective for the session',
77
+ },
78
+ context_files: {
79
+ type: 'array',
80
+ items: { type: 'string' },
81
+ description: 'For create: Array of file paths to include in session context',
82
+ },
83
+ parent_session: {
84
+ type: 'string',
85
+ description: 'For create: Parent session ID if this is a child session',
86
+ },
87
+ concurrency: {
88
+ type: 'number',
89
+ description: 'For create: Max concurrent running tasks (1-10, default: 1)',
90
+ },
91
+ auto_execute: {
92
+ type: 'boolean',
93
+ description: 'For create: Auto-execute queued tasks when slots available (default: false)',
94
+ },
95
+ // FORK operation parameters
96
+ session_id: {
97
+ type: 'string',
98
+ description: 'Required for fork/archive/delete: Session ID to operate on',
99
+ },
100
+ fork_point: {
101
+ type: 'number',
102
+ description: 'For fork: Task number to fork from (optional)',
103
+ },
104
+ reason: {
105
+ type: 'string',
106
+ description: 'For fork: Reason for forking (e.g., "Explore OAuth approach")',
107
+ },
108
+ // DELETE operation parameters
109
+ session_ids: {
110
+ type: 'array',
111
+ items: { type: 'string' },
112
+ description: 'For delete: Array of session IDs to delete (bulk deletion)',
113
+ },
114
+ status: {
115
+ type: 'string',
116
+ enum: ['active', 'completed', 'archived'],
117
+ description: 'For delete: Delete all sessions with this status',
118
+ },
119
+ older_than_days: {
120
+ type: 'number',
121
+ description: 'For delete: Delete sessions older than N days',
122
+ },
123
+ confirm: {
124
+ type: 'boolean',
125
+ description: 'Required for delete: Must be true to execute deletion (safety check)',
126
+ },
127
+ },
128
+ required: ['operation'],
129
+ },
130
+ };
131
+ /**
132
+ * session_query tool schema
133
+ *
134
+ * Queries session information: get status, list sessions, or generate reports.
135
+ *
136
+ * @example Get session status
137
+ * ```typescript
138
+ * session_query({
139
+ * operation: 'status',
140
+ * session_id: 'abc123'
141
+ * })
142
+ * ```
143
+ *
144
+ * @example List active sessions
145
+ * ```typescript
146
+ * session_query({
147
+ * operation: 'list',
148
+ * status_filter: 'active',
149
+ * format: 'table'
150
+ * })
151
+ * ```
152
+ *
153
+ * @example Generate session report
154
+ * ```typescript
155
+ * session_query({
156
+ * operation: 'report',
157
+ * session_id: 'abc123',
158
+ * include_output: true,
159
+ * include_artifacts: false
160
+ * })
161
+ * ```
162
+ */
163
+ export const sessionQuerySchema = {
164
+ name: 'session_query',
165
+ description: 'Query session information: get status for a specific session, list all sessions with filters, or generate comprehensive reports',
166
+ inputSchema: {
167
+ type: 'object',
168
+ properties: {
169
+ operation: {
170
+ type: 'string',
171
+ enum: ['status', 'list', 'report'],
172
+ description: 'Type of query: status=get session details, list=list sessions with filters, report=generate comprehensive report',
173
+ },
174
+ // STATUS operation parameters
175
+ session_id: {
176
+ type: 'string',
177
+ description: 'Required for status: Session ID to query. Optional for report: omit to get all sessions',
178
+ },
179
+ // LIST operation parameters
180
+ status_filter: {
181
+ type: 'string',
182
+ enum: ['active', 'completed', 'archived'],
183
+ description: 'For list/report: Filter sessions by status',
184
+ },
185
+ include_forks: {
186
+ type: 'boolean',
187
+ description: 'For list: Include forked sessions in results (default: false)',
188
+ },
189
+ limit: {
190
+ type: 'number',
191
+ description: 'For list/report: Maximum number of sessions to return (default: 20 for report)',
192
+ },
193
+ format: {
194
+ type: 'string',
195
+ enum: ['table', 'json'],
196
+ description: 'For list: Output format (table=ASCII table, json=raw JSON, default: json)',
197
+ },
198
+ // REPORT operation parameters
199
+ include_output: {
200
+ type: 'boolean',
201
+ description: 'For report: Include full task output content (default: true)',
202
+ },
203
+ include_artifacts: {
204
+ type: 'boolean',
205
+ description: 'For report: Include artifact content, not just metadata (default: false)',
206
+ },
207
+ },
208
+ required: ['operation'],
209
+ },
210
+ };
211
+ /**
212
+ * session_queue_config tool schema
213
+ *
214
+ * Configure queue settings or query queue status for a session.
215
+ *
216
+ * @example Configure queue settings
217
+ * ```typescript
218
+ * session_queue_config({
219
+ * operation: 'configure',
220
+ * session_id: 'abc123',
221
+ * concurrency: 5,
222
+ * auto_execute: true
223
+ * })
224
+ * ```
225
+ *
226
+ * @example Query queue status
227
+ * ```typescript
228
+ * session_queue_config({
229
+ * operation: 'status',
230
+ * session_id: 'abc123'
231
+ * })
232
+ * ```
233
+ */
234
+ export const sessionQueueConfigSchema = {
235
+ name: 'session_queue_config',
236
+ description: 'Configure session queue settings (concurrency, auto-execution) or query queue status (running/queued counts, available slots)',
237
+ inputSchema: {
238
+ type: 'object',
239
+ properties: {
240
+ operation: {
241
+ type: 'string',
242
+ enum: ['configure', 'status'],
243
+ description: 'Operation: configure=update queue settings, status=get queue status and ready tasks',
244
+ },
245
+ session_id: {
246
+ type: 'string',
247
+ description: 'Required: Session ID to configure or query',
248
+ },
249
+ // CONFIGURE operation parameters
250
+ concurrency: {
251
+ type: 'number',
252
+ description: 'For configure: Max concurrent running tasks (1-10)',
253
+ },
254
+ auto_execute: {
255
+ type: 'boolean',
256
+ description: 'For configure: Auto-execute queued tasks when slots available',
257
+ },
258
+ },
259
+ required: ['operation', 'session_id'],
260
+ },
261
+ };
262
+ /**
263
+ * session_resume tool schema
264
+ *
265
+ * Discover active sessions with incomplete tasks and display them as a checklist.
266
+ *
267
+ * @example Resume specific session
268
+ * ```typescript
269
+ * session_resume({
270
+ * session_id: 'abc123'
271
+ * })
272
+ * ```
273
+ *
274
+ * @example Discover all resumable sessions
275
+ * ```typescript
276
+ * session_resume({})
277
+ * ```
278
+ */
279
+ export const sessionResumeSchema = {
280
+ name: 'session_resume',
281
+ description: 'Discover active sessions with incomplete tasks and display them in a Claude-style checklist format. Returns sessions with pending, queued, running, or failed tasks.',
282
+ inputSchema: {
283
+ type: 'object',
284
+ properties: {
285
+ session_id: {
286
+ type: 'string',
287
+ description: 'Optional: Specific session ID to resume. If omitted, discovers all active sessions with incomplete tasks.',
288
+ },
289
+ },
290
+ },
291
+ };
292
+ // ============================================================================
293
+ // HANDLER IMPLEMENTATIONS
294
+ // ============================================================================
295
+ /**
296
+ * Validates operation-specific required fields
297
+ * Throws error if required fields are missing for the operation
298
+ */
299
+ function validateSessionManageArgs(operation, args) {
300
+ switch (operation) {
301
+ case 'create':
302
+ if (typeof args['objective'] !== 'string' || args['objective'] === '') {
303
+ throw new Error('operation=create requires: objective (string)');
304
+ }
305
+ break;
306
+ case 'fork':
307
+ if (typeof args['session_id'] !== 'string' || args['session_id'] === '') {
308
+ throw new Error('operation=fork requires: session_id (string)');
309
+ }
310
+ break;
311
+ case 'archive':
312
+ if (typeof args['session_id'] !== 'string' || args['session_id'] === '') {
313
+ throw new Error('operation=archive requires: session_id (string)');
314
+ }
315
+ break;
316
+ case 'delete': {
317
+ if (args['confirm'] !== true) {
318
+ throw new Error('operation=delete requires: confirm=true (safety check)');
319
+ }
320
+ // Must provide at least one filter: session_id, session_ids, status, or older_than_days
321
+ const hasFilter = (typeof args['session_id'] === 'string' && args['session_id'] !== '') ||
322
+ (Array.isArray(args['session_ids']) && args['session_ids'].length > 0) ||
323
+ (typeof args['status'] === 'string' && args['status'] !== '') ||
324
+ typeof args['older_than_days'] === 'number';
325
+ if (!hasFilter) {
326
+ throw new Error('operation=delete requires at least one filter: session_id, session_ids, status, or older_than_days');
327
+ }
328
+ break;
329
+ }
330
+ default:
331
+ throw new Error(`Unknown operation: ${operation}`);
332
+ }
333
+ }
334
+ function validateSessionQueryArgs(operation, args) {
335
+ switch (operation) {
336
+ case 'status':
337
+ if (typeof args['session_id'] !== 'string' || args['session_id'] === '') {
338
+ throw new Error('operation=status requires: session_id (string)');
339
+ }
340
+ break;
341
+ case 'list':
342
+ // No required fields for list - all optional
343
+ break;
344
+ case 'report':
345
+ // session_id is optional for report (omit to get all sessions)
346
+ break;
347
+ default:
348
+ throw new Error(`Unknown operation: ${operation}`);
349
+ }
350
+ }
351
+ function validateQueueConfigArgs(operation, args) {
352
+ // session_id is always required (enforced in schema)
353
+ if (typeof args['session_id'] !== 'string' || args['session_id'] === '') {
354
+ throw new Error('session_id (string) is required');
355
+ }
356
+ switch (operation) {
357
+ case 'configure':
358
+ // Validate concurrency if provided
359
+ if (args['concurrency'] !== undefined) {
360
+ const concurrency = args['concurrency'];
361
+ if (concurrency < 1 || concurrency > 10) {
362
+ throw new Error('concurrency must be between 1 and 10');
363
+ }
364
+ }
365
+ break;
366
+ case 'status':
367
+ // No additional validation needed
368
+ break;
369
+ default:
370
+ throw new Error(`Unknown operation: ${operation}`);
371
+ }
372
+ }
373
+ /**
374
+ * session_manage handler
375
+ * Routes to appropriate handler based on operation parameter
376
+ */
377
+ export async function handleSessionManage(args) {
378
+ const operation = args['operation'];
379
+ // Validate operation-specific required fields
380
+ validateSessionManageArgs(operation, args);
381
+ switch (operation) {
382
+ case 'create':
383
+ return handleSessionCreate(args);
384
+ case 'fork':
385
+ return handleSessionFork(args);
386
+ case 'archive':
387
+ return handleSessionArchive(args);
388
+ case 'delete':
389
+ return handleSessionDelete(args);
390
+ default:
391
+ throw new Error(`Unknown operation: ${operation}`);
392
+ }
393
+ }
394
+ /**
395
+ * session_query handler
396
+ * Routes to appropriate handler based on operation parameter
397
+ */
398
+ export async function handleSessionQuery(args) {
399
+ const operation = args['operation'];
400
+ // Validate operation-specific required fields
401
+ validateSessionQueryArgs(operation, args);
402
+ switch (operation) {
403
+ case 'status':
404
+ return handleSessionStatus(args);
405
+ case 'list':
406
+ return handleSessionList(args);
407
+ case 'report':
408
+ return handleSessionReport(args);
409
+ default:
410
+ throw new Error(`Unknown operation: ${operation}`);
411
+ }
412
+ }
413
+ /**
414
+ * session_queue_config handler
415
+ * Routes to configure or status operation
416
+ */
417
+ export async function handleSessionQueueConfig(args) {
418
+ const operation = args['operation'];
419
+ // Validate operation-specific required fields
420
+ validateQueueConfigArgs(operation, args);
421
+ switch (operation) {
422
+ case 'configure':
423
+ return handleQueueConfigure(args);
424
+ case 'status':
425
+ return handleQueueStatus(args);
426
+ default:
427
+ throw new Error(`Unknown operation: ${operation}`);
428
+ }
429
+ }
430
+ // ============================================================================
431
+ // OPERATION IMPLEMENTATIONS
432
+ // ============================================================================
433
+ /**
434
+ * Create a new session
435
+ */
436
+ function handleSessionCreate(args) {
437
+ const objective = args['objective'];
438
+ addBreadcrumb('Creating session', { objective }, 'info', 'session');
439
+ const session = sessionManager.spawn({
440
+ objective,
441
+ ...(args['context_files'] !== undefined && {
442
+ context_files: args['context_files'],
443
+ }),
444
+ ...(args['parent_session'] !== undefined && {
445
+ parent_session: args['parent_session'],
446
+ }),
447
+ ...(args['concurrency'] !== undefined && {
448
+ concurrency: args['concurrency'],
449
+ }),
450
+ ...(args['auto_execute'] !== undefined && {
451
+ auto_execute: args['auto_execute'],
452
+ }),
453
+ });
454
+ addBreadcrumb('Session created', { session_id: session.id }, 'info', 'session');
455
+ // Persist to MongoDB
456
+ safePersist(getPersistence().saveSession(session));
457
+ return Promise.resolve({
458
+ session_id: session.id,
459
+ objective: session.objective,
460
+ status: session.status,
461
+ queue_config: session.queue_config,
462
+ });
463
+ }
464
+ /**
465
+ * Fork an existing session
466
+ */
467
+ function handleSessionFork(args) {
468
+ const sessionId = args['session_id'];
469
+ const reason = args['reason'] ?? 'unknown';
470
+ addBreadcrumb('Forking session', { session_id: sessionId, reason }, 'info', 'session');
471
+ const forked = forkService.fork({
472
+ session_id: sessionId,
473
+ ...(args['fork_point'] !== undefined && {
474
+ fork_point: args['fork_point'],
475
+ }),
476
+ ...(args['reason'] !== undefined && { reason }),
477
+ });
478
+ addBreadcrumb('Session forked', { forked_session_id: forked.id, parent_id: forked.parent_session_id }, 'info', 'session');
479
+ // Persist to MongoDB
480
+ safePersist(getPersistence().saveSession(forked));
481
+ return Promise.resolve({
482
+ forked_session_id: forked.id,
483
+ parent_id: forked.parent_session_id,
484
+ fork_point: forked.fork_point,
485
+ });
486
+ }
487
+ /**
488
+ * Archive a session
489
+ */
490
+ function handleSessionArchive(args) {
491
+ const sessionId = args['session_id'];
492
+ const session = sessionManager.updateStatus(sessionId, 'archived');
493
+ // Persist to MongoDB
494
+ safePersist(getPersistence().saveSession(session));
495
+ return Promise.resolve({
496
+ session_id: session.id,
497
+ status: session.status,
498
+ });
499
+ }
500
+ /**
501
+ * Delete session(s) permanently
502
+ */
503
+ async function handleSessionDelete(args) {
504
+ const sessionId = args['session_id'];
505
+ const sessionIds = args['session_ids'];
506
+ const statusFilter = args['status'];
507
+ const olderThanDays = args['older_than_days'];
508
+ const persistence = getPersistence();
509
+ // Single session deletion
510
+ if (sessionId !== undefined && sessionId !== '') {
511
+ // Remove from in-memory cache
512
+ sessionManager.remove(sessionId);
513
+ // Remove related tasks from cache
514
+ for (const [taskId, task] of tasks.entries()) {
515
+ if (task.session_id === sessionId) {
516
+ tasks.delete(taskId);
517
+ }
518
+ }
519
+ // Remove related agents from cache
520
+ for (const [agentId, agent] of agents.entries()) {
521
+ if (agent.session_id === sessionId) {
522
+ agents.delete(agentId);
523
+ }
524
+ }
525
+ // Remove related artifacts from cache
526
+ for (const [artifactId, artifact] of artifacts.entries()) {
527
+ if (artifact.session_id === sessionId) {
528
+ artifacts.delete(artifactId);
529
+ }
530
+ }
531
+ const deleted = await persistence.deleteSession(sessionId);
532
+ return { deleted: deleted, session_id: sessionId };
533
+ }
534
+ // Build filters for bulk deletion
535
+ const filters = {};
536
+ if (sessionIds !== undefined && sessionIds.length > 0) {
537
+ filters.session_ids = sessionIds;
538
+ }
539
+ if (statusFilter !== undefined && statusFilter !== '') {
540
+ filters.status = statusFilter;
541
+ }
542
+ if (olderThanDays !== undefined) {
543
+ const cutoffDate = new Date();
544
+ cutoffDate.setDate(cutoffDate.getDate() - olderThanDays);
545
+ filters.older_than = cutoffDate;
546
+ }
547
+ const result = await persistence.deleteSessions(filters);
548
+ // Clean up in-memory caches for deleted sessions
549
+ for (const deletedId of result.session_ids) {
550
+ sessionManager.remove(deletedId);
551
+ for (const [taskId, task] of tasks.entries()) {
552
+ if (task.session_id === deletedId) {
553
+ tasks.delete(taskId);
554
+ }
555
+ }
556
+ for (const [agentId, agent] of agents.entries()) {
557
+ if (agent.session_id === deletedId) {
558
+ agents.delete(agentId);
559
+ }
560
+ }
561
+ for (const [artifactId, artifact] of artifacts.entries()) {
562
+ if (artifact.session_id === deletedId) {
563
+ artifacts.delete(artifactId);
564
+ }
565
+ }
566
+ }
567
+ return {
568
+ deleted_count: result.deleted_count,
569
+ session_ids: result.session_ids,
570
+ };
571
+ }
572
+ /**
573
+ * Get session status
574
+ */
575
+ function handleSessionStatus(args) {
576
+ const sessionId = args['session_id'];
577
+ const status = sessionManager.getStatus(sessionId);
578
+ // TOON encoding applied in handleToolCall
579
+ return Promise.resolve(status);
580
+ }
581
+ /**
582
+ * List sessions with filters
583
+ */
584
+ async function handleSessionList(args) {
585
+ // Get from in-memory first
586
+ const inMemorySessions = sessionManager.list({
587
+ ...(args['status_filter'] !== undefined && {
588
+ status: args['status_filter'],
589
+ }),
590
+ ...(args['include_forks'] !== undefined && {
591
+ include_forks: args['include_forks'],
592
+ }),
593
+ ...(args['limit'] !== undefined && {
594
+ limit: args['limit'],
595
+ }),
596
+ });
597
+ // Also get from MongoDB for persistence
598
+ const persistedSessions = await getPersistence().listSessions({
599
+ ...(args['status_filter'] !== undefined && {
600
+ status: args['status_filter'],
601
+ }),
602
+ ...(args['limit'] !== undefined && {
603
+ limit: args['limit'],
604
+ }),
605
+ });
606
+ // Merge: in-memory takes precedence, then add persisted ones not in memory
607
+ const inMemoryIds = new Set(inMemorySessions.map((s) => s.id));
608
+ const sessions = [
609
+ ...inMemorySessions,
610
+ ...persistedSessions.filter((s) => !inMemoryIds.has(s.id)),
611
+ ];
612
+ // Return raw result for table formatting to be applied in handleToolCall
613
+ const result = {
614
+ count: sessions.length,
615
+ sessions: sessions.map((s) => ({
616
+ id: s.id,
617
+ objective: s.objective,
618
+ status: s.status,
619
+ created_at: s.created_at,
620
+ })),
621
+ };
622
+ // Don't apply TOON encoding here - let handleToolCall handle formatting
623
+ return result;
624
+ }
625
+ /**
626
+ * Generate session report
627
+ */
628
+ async function handleSessionReport(args) {
629
+ const sessionId = args['session_id'];
630
+ const statusFilter = args['status_filter'];
631
+ const includeOutput = args['include_output'] ?? true;
632
+ const includeArtifactContent = args['include_artifacts'] ?? false;
633
+ const limit = args['limit'] ?? 20;
634
+ const persistence = getPersistence();
635
+ if (sessionId !== undefined && sessionId !== '') {
636
+ // Single session report
637
+ const report = await persistence.getSessionReport(sessionId);
638
+ if (report === null) {
639
+ throw new Error(`Session not found: ${sessionId}`);
640
+ }
641
+ return formatSessionReport(report, includeOutput, includeArtifactContent);
642
+ }
643
+ // All sessions report
644
+ const reportFilters = { limit };
645
+ if (statusFilter !== undefined) {
646
+ reportFilters.status = statusFilter;
647
+ }
648
+ const reports = await persistence.getAllSessionReports(reportFilters);
649
+ return {
650
+ total_sessions: reports.length,
651
+ sessions: reports.map((r) => formatSessionReport(r, includeOutput, includeArtifactContent)),
652
+ };
653
+ }
654
+ /**
655
+ * Configure queue settings
656
+ */
657
+ function handleQueueConfigure(args) {
658
+ const sessionId = args['session_id'];
659
+ const concurrency = args['concurrency'];
660
+ const autoExecute = args['auto_execute'];
661
+ // Build updates object
662
+ const updates = {};
663
+ if (concurrency !== undefined) {
664
+ updates.concurrency = concurrency;
665
+ }
666
+ if (autoExecute !== undefined) {
667
+ updates.auto_execute = autoExecute;
668
+ }
669
+ if (Object.keys(updates).length === 0) {
670
+ // No updates provided, just return current config
671
+ const session = sessionManager.getSession(sessionId);
672
+ return Promise.resolve({
673
+ session_id: sessionId,
674
+ queue_config: session.queue_config,
675
+ });
676
+ }
677
+ // Update queue config
678
+ const session = sessionManager.updateQueueConfig(sessionId, updates);
679
+ safePersist(getPersistence().saveSession(session));
680
+ return Promise.resolve({
681
+ session_id: sessionId,
682
+ queue_config: session.queue_config,
683
+ updated: true,
684
+ });
685
+ }
686
+ /**
687
+ * Get queue status
688
+ */
689
+ async function handleQueueStatus(args) {
690
+ const sessionId = args['session_id'];
691
+ const session = sessionManager.getSession(sessionId);
692
+ // Get tasks for the session
693
+ const sessionTasks = [];
694
+ for (const [, task] of tasks.entries()) {
695
+ if (task.session_id === sessionId) {
696
+ sessionTasks.push(task);
697
+ }
698
+ }
699
+ // Also get from MongoDB
700
+ const persistedTasks = await getPersistence().listTasks({ session_id: sessionId });
701
+ const inMemoryIds = new Set(sessionTasks.map((t) => t.id));
702
+ const allTasks = [...sessionTasks, ...persistedTasks.filter((t) => !inMemoryIds.has(t.id))];
703
+ // Count by status
704
+ const runningCount = allTasks.filter((t) => t.status === 'running').length;
705
+ const queuedCount = allTasks.filter((t) => t.status === 'queued').length;
706
+ const pendingCount = allTasks.filter((t) => t.status === 'pending').length;
707
+ const completedCount = allTasks.filter((t) => t.status === 'completed').length;
708
+ const failedCount = allTasks.filter((t) => t.status === 'failed').length;
709
+ const availableSlots = Math.max(0, session.queue_config.concurrency - runningCount);
710
+ // Find ready tasks (queued with met dependencies)
711
+ const queuedTasks = allTasks.filter((t) => t.status === 'queued');
712
+ const taskMap = new Map(allTasks.map((t) => [t.id, t]));
713
+ const readyTasks = queuedTasks
714
+ .filter((task) => {
715
+ if (task.dependencies.length === 0)
716
+ return true;
717
+ return task.dependencies.every((depId) => {
718
+ const depTask = taskMap.get(depId);
719
+ return depTask !== undefined && depTask.status === 'completed';
720
+ });
721
+ })
722
+ .sort((a, b) => {
723
+ // Sort by priority (higher first), then by created_at (earlier first)
724
+ if (a.priority !== b.priority)
725
+ return b.priority - a.priority;
726
+ return a.created_at.getTime() - b.created_at.getTime();
727
+ })
728
+ .slice(0, availableSlots)
729
+ .map((t) => t.id);
730
+ return {
731
+ session_id: sessionId,
732
+ queue_config: session.queue_config,
733
+ running_count: runningCount,
734
+ queued_count: queuedCount,
735
+ pending_count: pendingCount,
736
+ completed_count: completedCount,
737
+ failed_count: failedCount,
738
+ available_slots: availableSlots,
739
+ ready_tasks: readyTasks,
740
+ };
741
+ }
742
+ /**
743
+ * Resume a session - discover active sessions with incomplete tasks
744
+ */
745
+ async function handleSessionResume(args) {
746
+ const sessionId = args['session_id'];
747
+ const persistence = getPersistence();
748
+ let sessionsToCheck = [];
749
+ if (sessionId !== undefined && sessionId !== '') {
750
+ // Resume specific session
751
+ const session = sessionManager.getSession(sessionId);
752
+ sessionsToCheck = [session];
753
+ }
754
+ else {
755
+ // Discover all active sessions
756
+ const inMemorySessions = sessionManager.list({ status: 'active' });
757
+ const persistedSessions = await persistence.listSessions({ status: 'active' });
758
+ // Merge sessions
759
+ const inMemoryIds = new Set(inMemorySessions.map((s) => s.id));
760
+ sessionsToCheck = [
761
+ ...inMemorySessions,
762
+ ...persistedSessions.filter((s) => !inMemoryIds.has(s.id)),
763
+ ];
764
+ }
765
+ // Filter sessions with incomplete tasks
766
+ const resumableSessions = [];
767
+ for (const session of sessionsToCheck) {
768
+ // Get tasks for the session from both memory and persistence
769
+ const sessionTasks = [];
770
+ for (const [, task] of tasks.entries()) {
771
+ if (task.session_id === session.id) {
772
+ sessionTasks.push(task);
773
+ }
774
+ }
775
+ const persistedTasks = await persistence.listTasks({ session_id: session.id });
776
+ const inMemoryIds = new Set(sessionTasks.map((t) => t.id));
777
+ const allTasks = [...sessionTasks, ...persistedTasks.filter((t) => !inMemoryIds.has(t.id))];
778
+ // Count by status
779
+ const completed = allTasks.filter((t) => t.status === 'completed').length;
780
+ const pending = allTasks.filter((t) => t.status === 'pending').length;
781
+ const running = allTasks.filter((t) => t.status === 'running').length;
782
+ const failed = allTasks.filter((t) => t.status === 'failed').length;
783
+ const queued = allTasks.filter((t) => t.status === 'queued').length;
784
+ // Only include sessions with incomplete tasks
785
+ const incompleteTasks = pending + running + failed + queued;
786
+ if (incompleteTasks === 0) {
787
+ continue;
788
+ }
789
+ // Build task map for dependency checking
790
+ const taskMap = new Map(allTasks.map((t) => [t.id, t]));
791
+ // Build task info array
792
+ const taskInfos = allTasks
793
+ .filter((t) => t.status !== 'completed' && t.status !== 'cancelled')
794
+ .map((task) => {
795
+ // Check if task is blocked by dependencies
796
+ const blocked = task.dependencies.length > 0 &&
797
+ task.dependencies.some((depId) => {
798
+ const depTask = taskMap.get(depId);
799
+ return depTask === undefined || depTask.status !== 'completed';
800
+ });
801
+ return {
802
+ id: task.id,
803
+ description: task.description,
804
+ status: task.status,
805
+ blocked,
806
+ dependencies: task.dependencies,
807
+ };
808
+ });
809
+ resumableSessions.push({
810
+ session_id: session.id,
811
+ objective: session.objective,
812
+ total_tasks: allTasks.length,
813
+ completed,
814
+ pending,
815
+ running,
816
+ failed,
817
+ queued,
818
+ tasks: taskInfos,
819
+ });
820
+ }
821
+ // Format output
822
+ if (resumableSessions.length === 0) {
823
+ return {
824
+ message: sessionId !== null && sessionId !== undefined
825
+ ? `Session ${sessionId} has no incomplete tasks to resume`
826
+ : 'No active sessions with incomplete tasks found',
827
+ sessions: [],
828
+ };
829
+ }
830
+ if (resumableSessions.length === 1) {
831
+ // Single session - show full checklist
832
+ const firstSession = resumableSessions[0];
833
+ if (firstSession === undefined) {
834
+ throw new Error('Unexpected error: session not found in array');
835
+ }
836
+ return {
837
+ message: formatSingleSessionResume(firstSession),
838
+ sessions: resumableSessions,
839
+ };
840
+ }
841
+ // Multiple sessions - show summary cards
842
+ return {
843
+ message: formatMultiSessionResume(resumableSessions),
844
+ sessions: resumableSessions,
845
+ };
846
+ }
847
+ /**
848
+ * Format a single session as a full checklist (Claude-style ASCII box)
849
+ */
850
+ function formatSingleSessionResume(session) {
851
+ const lines = [];
852
+ // Header
853
+ lines.push('┌─────────────────────────────────────────────────────────────────────┐');
854
+ lines.push(`│ Resume Session: ${session.session_id.padEnd(47)} │`);
855
+ lines.push('├─────────────────────────────────────────────────────────────────────┤');
856
+ lines.push(`│ ${session.objective.slice(0, 67).padEnd(67)} │`);
857
+ lines.push('├─────────────────────────────────────────────────────────────────────┤');
858
+ // Progress summary
859
+ const totalPercent = Math.round((session.completed / session.total_tasks) * 100).toString();
860
+ const progressBar = createProgressBar(session.completed, session.total_tasks, 50);
861
+ lines.push(`│ Progress: ${progressBar} ${totalPercent.padStart(3)}% │`);
862
+ lines.push(`│ Tasks: ${session.total_tasks} total, ${session.completed} completed, ${session.pending + session.running + session.failed + session.queued} remaining${' '.repeat(Math.max(0, 67 - `Tasks: ${session.total_tasks} total, ${session.completed} completed, ${session.pending + session.running + session.failed + session.queued} remaining`.length))} │`);
863
+ lines.push('├─────────────────────────────────────────────────────────────────────┤');
864
+ // Task list
865
+ for (const task of session.tasks) {
866
+ const icon = getTaskIcon(task.status, task.blocked);
867
+ const statusLabel = task.blocked ? 'blocked' : task.status;
868
+ const taskLine = `${icon} [${statusLabel}] ${task.description.slice(0, 50)}`;
869
+ lines.push(`│ ${taskLine.padEnd(67)} │`);
870
+ // Show dependencies for blocked tasks
871
+ if (task.blocked && task.dependencies.length > 0) {
872
+ const depLine = ` ↳ Waiting on: ${task.dependencies.join(', ').slice(0, 45)}`;
873
+ lines.push(`│ ${depLine.padEnd(67)} │`);
874
+ }
875
+ }
876
+ lines.push('└─────────────────────────────────────────────────────────────────────┘');
877
+ return lines.join('\n');
878
+ }
879
+ /**
880
+ * Format multiple sessions as summary cards
881
+ */
882
+ function formatMultiSessionResume(sessions) {
883
+ const lines = [];
884
+ lines.push('┌─────────────────────────────────────────────────────────────────────┐');
885
+ lines.push(`│ Found ${sessions.length} active sessions with incomplete tasks${' '.repeat(Math.max(0, 67 - `Found ${sessions.length} active sessions with incomplete tasks`.length))} │`);
886
+ lines.push('└─────────────────────────────────────────────────────────────────────┘');
887
+ lines.push('');
888
+ for (const session of sessions) {
889
+ lines.push('┌─────────────────────────────────────────────────────────────────────┐');
890
+ lines.push(`│ Session: ${session.session_id.padEnd(58)} │`);
891
+ lines.push('├─────────────────────────────────────────────────────────────────────┤');
892
+ lines.push(`│ ${session.objective.slice(0, 67).padEnd(67)} │`);
893
+ lines.push('├─────────────────────────────────────────────────────────────────────┤');
894
+ const totalPercent = Math.round((session.completed / session.total_tasks) * 100).toString();
895
+ const progressBar = createProgressBar(session.completed, session.total_tasks, 50);
896
+ lines.push(`│ Progress: ${progressBar} ${totalPercent.padStart(3)}% │`);
897
+ lines.push(`│ ${session.completed}/${session.total_tasks} completed | ${session.running} running | ${session.pending} pending | ${session.failed} failed${' '.repeat(Math.max(0, 67 - `${session.completed}/${session.total_tasks} completed | ${session.running} running | ${session.pending} pending | ${session.failed} failed`.length))} │`);
898
+ lines.push('├─────────────────────────────────────────────────────────────────────┤');
899
+ // Show first 3-4 incomplete tasks
900
+ const tasksToShow = session.tasks.slice(0, 4);
901
+ for (const task of tasksToShow) {
902
+ const icon = getTaskIcon(task.status, task.blocked);
903
+ const statusLabel = task.blocked ? 'blocked' : task.status;
904
+ const taskLine = `${icon} [${statusLabel}] ${task.description.slice(0, 50)}`;
905
+ lines.push(`│ ${taskLine.padEnd(67)} │`);
906
+ }
907
+ if (session.tasks.length > 4) {
908
+ const remaining = session.tasks.length - 4;
909
+ lines.push(`│ ... and ${remaining} more task${remaining > 1 ? 's' : ''}${' '.repeat(Math.max(0, 67 - `... and ${remaining} more task${remaining > 1 ? 's' : ''}`.length))} │`);
910
+ }
911
+ lines.push('└─────────────────────────────────────────────────────────────────────┘');
912
+ lines.push('');
913
+ }
914
+ return lines.join('\n');
915
+ }
916
+ /**
917
+ * Get icon for task status
918
+ */
919
+ function getTaskIcon(status, blocked) {
920
+ if (blocked)
921
+ return '⏸️';
922
+ switch (status) {
923
+ case 'completed':
924
+ return '✅';
925
+ case 'running':
926
+ return '🔧';
927
+ case 'failed':
928
+ return '❗';
929
+ default:
930
+ return '❌';
931
+ }
932
+ }
933
+ /**
934
+ * Create a progress bar string
935
+ */
936
+ function createProgressBar(completed, total, width) {
937
+ const filledWidth = Math.round((completed / total) * width);
938
+ const emptyWidth = width - filledWidth;
939
+ return '█'.repeat(filledWidth) + '░'.repeat(emptyWidth);
940
+ }
941
+ // ============================================================================
942
+ // EXPORTS
943
+ // ============================================================================
944
+ export { sessionManager, forkService, handleSessionResume };
945
+ //# sourceMappingURL=session-tools.js.map