@anastops/mcp-server 0.1.0 → 1.0.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 (105) hide show
  1. package/dist/formatters.d.ts.map +1 -1
  2. package/dist/formatters.js +12 -3
  3. package/dist/formatters.js.map +1 -1
  4. package/dist/handlers/agent-handlers.d.ts +8 -0
  5. package/dist/handlers/agent-handlers.d.ts.map +1 -0
  6. package/dist/handlers/agent-handlers.js +184 -0
  7. package/dist/handlers/agent-handlers.js.map +1 -0
  8. package/dist/handlers/artifact-handlers.d.ts +8 -0
  9. package/dist/handlers/artifact-handlers.d.ts.map +1 -0
  10. package/dist/handlers/artifact-handlers.js +122 -0
  11. package/dist/handlers/artifact-handlers.js.map +1 -0
  12. package/dist/handlers/cost-handlers.d.ts +8 -0
  13. package/dist/handlers/cost-handlers.d.ts.map +1 -0
  14. package/dist/handlers/cost-handlers.js +140 -0
  15. package/dist/handlers/cost-handlers.js.map +1 -0
  16. package/dist/handlers/handlers.agent.d.ts +10 -0
  17. package/dist/handlers/handlers.agent.d.ts.map +1 -0
  18. package/dist/handlers/handlers.agent.js +99 -0
  19. package/dist/handlers/handlers.agent.js.map +1 -0
  20. package/dist/handlers/handlers.base.d.ts +82 -0
  21. package/dist/handlers/handlers.base.d.ts.map +1 -0
  22. package/dist/handlers/handlers.base.js +337 -0
  23. package/dist/handlers/handlers.base.js.map +1 -0
  24. package/dist/handlers/handlers.lock.d.ts +8 -0
  25. package/dist/handlers/handlers.lock.d.ts.map +1 -0
  26. package/dist/handlers/handlers.lock.js +111 -0
  27. package/dist/handlers/handlers.lock.js.map +1 -0
  28. package/dist/handlers/handlers.memory.d.ts +11 -0
  29. package/dist/handlers/handlers.memory.d.ts.map +1 -0
  30. package/dist/handlers/handlers.memory.js +122 -0
  31. package/dist/handlers/handlers.memory.js.map +1 -0
  32. package/dist/handlers/handlers.monitoring.d.ts +8 -0
  33. package/dist/handlers/handlers.monitoring.d.ts.map +1 -0
  34. package/dist/handlers/handlers.monitoring.js +99 -0
  35. package/dist/handlers/handlers.monitoring.js.map +1 -0
  36. package/dist/handlers/handlers.orchestration.d.ts +9 -0
  37. package/dist/handlers/handlers.orchestration.d.ts.map +1 -0
  38. package/dist/handlers/handlers.orchestration.js +128 -0
  39. package/dist/handlers/handlers.orchestration.js.map +1 -0
  40. package/dist/handlers/handlers.session.d.ts +18 -0
  41. package/dist/handlers/handlers.session.d.ts.map +1 -0
  42. package/dist/handlers/handlers.session.js +286 -0
  43. package/dist/handlers/handlers.session.js.map +1 -0
  44. package/dist/handlers/handlers.task.d.ts +15 -0
  45. package/dist/handlers/handlers.task.d.ts.map +1 -0
  46. package/dist/handlers/handlers.task.js +753 -0
  47. package/dist/handlers/handlers.task.js.map +1 -0
  48. package/dist/handlers/handlers.utility.d.ts +10 -0
  49. package/dist/handlers/handlers.utility.d.ts.map +1 -0
  50. package/dist/handlers/handlers.utility.js +59 -0
  51. package/dist/handlers/handlers.utility.js.map +1 -0
  52. package/dist/handlers/index.d.ts +18 -0
  53. package/dist/handlers/index.d.ts.map +1 -0
  54. package/dist/handlers/index.js +209 -0
  55. package/dist/handlers/index.js.map +1 -0
  56. package/dist/handlers/lock-handlers.d.ts +8 -0
  57. package/dist/handlers/lock-handlers.d.ts.map +1 -0
  58. package/dist/handlers/lock-handlers.js +154 -0
  59. package/dist/handlers/lock-handlers.js.map +1 -0
  60. package/dist/handlers/memory-handlers.d.ts +8 -0
  61. package/dist/handlers/memory-handlers.d.ts.map +1 -0
  62. package/dist/handlers/memory-handlers.js +76 -0
  63. package/dist/handlers/memory-handlers.js.map +1 -0
  64. package/dist/handlers/orchestration-handlers.d.ts +8 -0
  65. package/dist/handlers/orchestration-handlers.d.ts.map +1 -0
  66. package/dist/handlers/orchestration-handlers.js +113 -0
  67. package/dist/handlers/orchestration-handlers.js.map +1 -0
  68. package/dist/handlers/session-handlers.d.ts +8 -0
  69. package/dist/handlers/session-handlers.d.ts.map +1 -0
  70. package/dist/handlers/session-handlers.js +558 -0
  71. package/dist/handlers/session-handlers.js.map +1 -0
  72. package/dist/handlers/task-handlers.d.ts +8 -0
  73. package/dist/handlers/task-handlers.d.ts.map +1 -0
  74. package/dist/handlers/task-handlers.js +677 -0
  75. package/dist/handlers/task-handlers.js.map +1 -0
  76. package/dist/handlers/tool-definitions.d.ts +2626 -0
  77. package/dist/handlers/tool-definitions.d.ts.map +1 -0
  78. package/dist/handlers/tool-definitions.js +641 -0
  79. package/dist/handlers/tool-definitions.js.map +1 -0
  80. package/dist/handlers/types.d.ts +90 -0
  81. package/dist/handlers/types.d.ts.map +1 -0
  82. package/dist/handlers/types.js +5 -0
  83. package/dist/handlers/types.js.map +1 -0
  84. package/dist/handlers/utility-handlers.d.ts +8 -0
  85. package/dist/handlers/utility-handlers.d.ts.map +1 -0
  86. package/dist/handlers/utility-handlers.js +113 -0
  87. package/dist/handlers/utility-handlers.js.map +1 -0
  88. package/dist/handlers/utils.d.ts +30 -0
  89. package/dist/handlers/utils.d.ts.map +1 -0
  90. package/dist/handlers/utils.js +95 -0
  91. package/dist/handlers/utils.js.map +1 -0
  92. package/dist/handlers.d.ts +17 -2260
  93. package/dist/handlers.d.ts.map +1 -1
  94. package/dist/handlers.js +17 -1836
  95. package/dist/handlers.js.map +1 -1
  96. package/dist/index.js +41 -7
  97. package/dist/index.js.map +1 -1
  98. package/dist/persistence.d.ts.map +1 -1
  99. package/dist/persistence.js +84 -47
  100. package/dist/persistence.js.map +1 -1
  101. package/dist/schemas.d.ts +299 -0
  102. package/dist/schemas.d.ts.map +1 -0
  103. package/dist/schemas.js +334 -0
  104. package/dist/schemas.js.map +1 -0
  105. package/package.json +8 -5
package/dist/handlers.js CHANGED
@@ -1,1840 +1,21 @@
1
1
  /**
2
- * MCP Tool Handlers - Simplified implementation for MVP
3
- */
4
- import { AdapterRegistry } from '@anastops/adapters';
5
- import { SessionManager, SessionForkService, ContextOptimizer, IntelligentRouter, } from '@anastops/core';
6
- import { LRUCache } from 'lru-cache';
7
- import { nanoid } from 'nanoid';
8
- import { getPersistence } from './persistence.js';
9
- import { formatSessionTable, formatTaskTable, formatAgentTable, getOutputFormat, } from './formatters.js';
10
- /**
11
- * Safe fire-and-forget wrapper for persistence operations
12
- * Catches errors to prevent uncaught promise rejections from crashing the server
13
- */
14
- function safePersist(operation) {
15
- operation.catch(error => {
16
- console.error('[Persistence Error]', error instanceof Error ? error.message : error);
17
- });
18
- }
19
- // TOON encoding constants
20
- const TOON_MIN_SIZE_BYTES = 500;
21
- /**
22
- * Estimate token count for text
23
- * Rough approximation: ~4 characters per token for English
24
- */
25
- function estimateTokens(text) {
26
- return Math.ceil(text.length / 4);
27
- }
28
- /**
29
- * Convert array of uniform objects to TOON tabular format
30
- * TOON format uses pipe-separated headers and values for compact representation
31
- */
32
- function toTOONTabular(arr, keys) {
33
- if (arr.length === 0 || keys.length === 0)
34
- return '';
35
- const lines = [];
36
- // Header row
37
- lines.push(keys.join(' | '));
38
- // Data rows
39
- for (const item of arr) {
40
- const values = keys.map(k => {
41
- const val = item[k];
42
- if (val === null || val === undefined)
43
- return '';
44
- if (typeof val === 'string')
45
- return val.slice(0, 50);
46
- if (val instanceof Date)
47
- return val.toISOString();
48
- return String(val);
49
- });
50
- lines.push(values.join(' | '));
51
- }
52
- return lines.join('\n');
53
- }
54
- /**
55
- * Check if array contains uniform objects (all objects have same keys)
56
- */
57
- function isUniformObjectArray(data) {
58
- if (!Array.isArray(data) || data.length === 0)
59
- return false;
60
- if (typeof data[0] !== 'object' || data[0] === null)
61
- return false;
62
- const firstKeys = Object.keys(data[0]).sort().join(',');
63
- return data.every(item => {
64
- if (typeof item !== 'object' || item === null)
65
- return false;
66
- return Object.keys(item).sort().join(',') === firstKeys;
67
- });
68
- }
69
- /**
70
- * Apply TOON encoding to response if beneficial
71
- * Returns original data if TOON doesn't provide savings
2
+ * MCP Tool Handlers - Main Export
72
3
  *
73
- * TOON (Token-Optimized Object Notation) is most effective for:
74
- * - Arrays of uniform objects (converted to tabular format)
75
- * - Large structured responses (> 500 bytes)
76
- */
77
- export function applyTOONEncodingToResponse(data) {
78
- // Only apply TOON to arrays or large objects
79
- const jsonStr = JSON.stringify(data);
80
- if (jsonStr.length < TOON_MIN_SIZE_BYTES) {
81
- return { data }; // Too small, not worth encoding
82
- }
83
- const originalTokens = estimateTokens(jsonStr);
84
- // Check if data structure is TOON-eligible
85
- // TOON works best with arrays of uniform objects
86
- if (typeof data !== 'object' || data === null) {
87
- return { data };
88
- }
89
- const dataObj = data;
90
- // Look for arrays within the response that could be TOON-encoded
91
- let hasEncodableArrays = false;
92
- const toonParts = [];
93
- const scalarParts = [];
94
- for (const [key, value] of Object.entries(dataObj)) {
95
- if (isUniformObjectArray(value)) {
96
- hasEncodableArrays = true;
97
- const keys = Object.keys(value[0]);
98
- toonParts.push(`${key}:\n${toTOONTabular(value, keys)}`);
99
- }
100
- else if (Array.isArray(value)) {
101
- // Non-uniform array, keep as JSON
102
- scalarParts.push(`${key}: ${JSON.stringify(value)}`);
103
- }
104
- else if (typeof value === 'object' && value !== null) {
105
- // Nested object, keep as compact JSON
106
- scalarParts.push(`${key}: ${JSON.stringify(value)}`);
107
- }
108
- else {
109
- // Scalar value
110
- scalarParts.push(`${key}: ${String(value)}`);
111
- }
112
- }
113
- if (!hasEncodableArrays) {
114
- return { data }; // No benefit from TOON encoding
115
- }
116
- // Build TOON-encoded representation
117
- const toonEncoded = [...scalarParts, ...toonParts].join('\n');
118
- const encodedTokens = estimateTokens(toonEncoded);
119
- // Only use TOON if we get meaningful savings (> 20%)
120
- const savingsPercent = ((originalTokens - encodedTokens) / originalTokens) * 100;
121
- if (savingsPercent < 20) {
122
- return { data }; // Not enough savings to justify encoding change
123
- }
124
- return {
125
- data: toonEncoded,
126
- encoding: 'toon',
127
- token_reduction: {
128
- original: originalTokens,
129
- encoded: encodedTokens,
130
- savings_percent: Math.round(savingsPercent * 100) / 100,
131
- },
132
- };
133
- }
134
- /**
135
- * Format a session report with all related data
136
- */
137
- function formatSessionReport(report, includeOutput, includeArtifactContent) {
138
- const { session, tasks: sessionTasks, agents: sessionAgents, artifacts: sessionArtifacts } = report;
139
- // Calculate statistics
140
- const taskStats = {
141
- total: sessionTasks.length,
142
- pending: sessionTasks.filter(t => t.status === 'pending').length,
143
- queued: sessionTasks.filter(t => t.status === 'queued').length,
144
- running: sessionTasks.filter(t => t.status === 'running').length,
145
- completed: sessionTasks.filter(t => t.status === 'completed').length,
146
- failed: sessionTasks.filter(t => t.status === 'failed').length,
147
- cancelled: sessionTasks.filter(t => t.status === 'cancelled').length,
148
- };
149
- const totalTokens = sessionTasks.reduce((sum, t) => sum + (t.token_usage?.total_tokens ?? 0), 0);
150
- const totalCost = sessionTasks.reduce((sum, t) => sum + (t.token_usage?.cost ?? 0), 0);
151
- // Format tasks with full details
152
- const formattedTasks = sessionTasks.map(t => {
153
- const taskData = {
154
- id: t.id,
155
- type: t.type,
156
- status: t.status,
157
- description: t.description,
158
- provider: t.provider,
159
- model: t.model,
160
- routing_tier: t.routing_tier,
161
- complexity_score: t.complexity_score,
162
- token_usage: t.token_usage,
163
- created_at: t.created_at,
164
- started_at: t.started_at,
165
- completed_at: t.completed_at,
166
- retry_count: t.retry_count,
167
- input: {
168
- prompt: t.input?.prompt?.slice(0, 500) ?? '',
169
- context_files: t.input?.context_files ?? [],
170
- agent: t.input?.agent,
171
- skills: t.input?.skills,
172
- },
173
- };
174
- if (t.error) {
175
- taskData.error = t.error;
176
- }
177
- if (includeOutput && t.output) {
178
- taskData.output = {
179
- content: t.output.content,
180
- artifacts: t.output.artifacts,
181
- files_modified: t.output.files_modified,
182
- };
183
- }
184
- else if (t.output) {
185
- taskData.output = {
186
- content_length: t.output.content?.length ?? 0,
187
- artifacts_count: t.output.artifacts?.length ?? 0,
188
- files_modified: t.output.files_modified,
189
- };
190
- }
191
- return taskData;
192
- });
193
- // Format agents
194
- const formattedAgents = sessionAgents.map(a => ({
195
- id: a.id,
196
- name: a.name,
197
- role: a.role,
198
- status: a.status,
199
- provider: a.provider,
200
- model: a.model,
201
- tasks_completed: a.tasks_completed,
202
- tasks_failed: a.tasks_failed,
203
- tokens_used: a.tokens_used,
204
- current_task_id: a.current_task_id,
205
- created_at: a.created_at,
206
- last_activity_at: a.last_activity_at,
207
- }));
208
- // Format artifacts
209
- const formattedArtifacts = sessionArtifacts.map(a => {
210
- const artifactData = {
211
- id: a.id,
212
- name: a.name,
213
- type: a.type,
214
- size_bytes: a.size_bytes,
215
- token_count: a.token_count,
216
- created_at: a.created_at,
217
- };
218
- if (includeArtifactContent) {
219
- artifactData.content = a.content;
220
- }
221
- else {
222
- artifactData.summary = a.summary;
223
- }
224
- return artifactData;
225
- });
226
- return {
227
- session: {
228
- id: session.id,
229
- objective: session.objective,
230
- status: session.status,
231
- created_at: session.created_at,
232
- updated_at: session.updated_at,
233
- metadata: session.metadata,
234
- },
235
- statistics: {
236
- tasks: taskStats,
237
- total_tokens: totalTokens,
238
- total_cost_usd: totalCost,
239
- agents_count: sessionAgents.length,
240
- artifacts_count: sessionArtifacts.length,
241
- },
242
- tasks: formattedTasks,
243
- agents: formattedAgents,
244
- artifacts: formattedArtifacts,
245
- };
246
- }
247
- // Shared instances
248
- const sessionManager = new SessionManager();
249
- const forkService = new SessionForkService(sessionManager);
250
- const contextOptimizer = new ContextOptimizer();
251
- const router = new IntelligentRouter();
252
- const registry = AdapterRegistry.getInstance();
253
- // LRU caches with size limits and TTL to prevent memory leaks
254
- const agents = new LRUCache({ max: 1000, ttl: 1000 * 60 * 60 }); // 1 hour TTL
255
- const tasks = new LRUCache({ max: 5000, ttl: 1000 * 60 * 60 * 24 }); // 24 hour TTL
256
- const artifacts = new LRUCache({ max: 1000, ttl: 1000 * 60 * 60 * 24 }); // 24 hour TTL
257
- const memoryStore = new LRUCache({ max: 10000, ttl: 1000 * 60 * 60 }); // 1 hour TTL
258
- /**
259
- * Get task from memory or MongoDB
260
- * Loads from MongoDB and caches in memory if not found
261
- */
262
- async function getTask(taskId) {
263
- // Check in-memory first
264
- const memTask = tasks.get(taskId);
265
- if (memTask !== undefined) {
266
- return memTask;
267
- }
268
- // Try to load from MongoDB
269
- const dbTask = await getPersistence().getTask(taskId);
270
- if (dbTask !== null) {
271
- // Cache in memory for subsequent access
272
- tasks.set(taskId, dbTask);
273
- return dbTask;
274
- }
275
- return null;
276
- }
277
- /**
278
- * Get agent from memory or MongoDB
279
- */
280
- async function getAgent(agentId) {
281
- const memAgent = agents.get(agentId);
282
- if (memAgent !== undefined) {
283
- return memAgent;
284
- }
285
- const dbAgent = await getPersistence().getAgent(agentId);
286
- if (dbAgent !== null) {
287
- agents.set(agentId, dbAgent);
288
- return dbAgent;
289
- }
290
- return null;
291
- }
292
- /**
293
- * Get artifact from memory or MongoDB
294
- */
295
- async function getArtifact(artifactId) {
296
- const memArtifact = artifacts.get(artifactId);
297
- if (memArtifact !== undefined) {
298
- return memArtifact;
299
- }
300
- const dbArtifact = await getPersistence().getArtifact(artifactId);
301
- if (dbArtifact !== null) {
302
- artifacts.set(artifactId, dbArtifact);
303
- return dbArtifact;
304
- }
305
- return null;
306
- }
307
- // Tool definitions
308
- export const toolDefinitions = [
309
- // Session tools (5)
310
- {
311
- name: 'session_spawn',
312
- description: 'Create a new AI orchestration session',
313
- inputSchema: {
314
- type: 'object',
315
- properties: {
316
- objective: { type: 'string', description: 'High-level objective' },
317
- context_files: { type: 'array', items: { type: 'string' } },
318
- parent_session: { type: 'string' },
319
- },
320
- required: ['objective'],
321
- },
322
- },
323
- {
324
- name: 'session_status',
325
- description: 'Get session status and metrics',
326
- inputSchema: {
327
- type: 'object',
328
- properties: {
329
- session_id: { type: 'string', description: 'Session ID' },
330
- },
331
- required: ['session_id'],
332
- },
333
- },
334
- {
335
- name: 'session_list',
336
- description: 'List sessions with filters. Use format="table" for beautiful ASCII table output.',
337
- inputSchema: {
338
- type: 'object',
339
- properties: {
340
- status: { type: 'string', enum: ['active', 'completed', 'archived'] },
341
- include_forks: { type: 'boolean' },
342
- limit: { type: 'number' },
343
- format: { type: 'string', enum: ['table', 'json'], description: 'Output format: "table" for ASCII table, "json" for raw JSON (default)' },
344
- },
345
- },
346
- },
347
- {
348
- name: 'session_fork',
349
- description: 'Fork a session to explore alternatives',
350
- inputSchema: {
351
- type: 'object',
352
- properties: {
353
- session_id: { type: 'string', description: 'Session ID to fork' },
354
- fork_point: { type: 'number' },
355
- reason: { type: 'string' },
356
- },
357
- required: ['session_id'],
358
- },
359
- },
360
- {
361
- name: 'session_archive',
362
- description: 'Archive a completed session',
363
- inputSchema: {
364
- type: 'object',
365
- properties: {
366
- session_id: { type: 'string' },
367
- },
368
- required: ['session_id'],
369
- },
370
- },
371
- {
372
- name: 'session_report',
373
- description: 'Get comprehensive report of sessions with all tasks, agents, artifacts, and full details. Use for observability and debugging.',
374
- inputSchema: {
375
- type: 'object',
376
- properties: {
377
- session_id: { type: 'string', description: 'Specific session ID (omit for all sessions)' },
378
- status: { type: 'string', enum: ['active', 'completed', 'archived'], description: 'Filter by status (when no session_id provided)' },
379
- include_output: { type: 'boolean', description: 'Include full task output content (default: true)' },
380
- include_artifacts: { type: 'boolean', description: 'Include artifact content (default: false, only metadata)' },
381
- limit: { type: 'number', description: 'Max sessions to return (default: 20)' },
382
- },
383
- },
384
- },
385
- {
386
- name: 'session_purge',
387
- description: 'Permanently delete sessions and all related data (tasks, agents, artifacts). Use with caution.',
388
- inputSchema: {
389
- type: 'object',
390
- properties: {
391
- session_id: { type: 'string', description: 'Specific session ID to delete' },
392
- session_ids: { type: 'array', items: { type: 'string' }, description: 'Array of session IDs to delete' },
393
- status: { type: 'string', enum: ['active', 'completed', 'archived'], description: 'Delete all sessions with this status' },
394
- older_than_days: { type: 'number', description: 'Delete sessions older than N days' },
395
- confirm: { type: 'boolean', description: 'Must be true to execute deletion' },
396
- },
397
- required: ['confirm'],
398
- },
399
- },
400
- // Agent tools (4)
401
- {
402
- name: 'agent_create',
403
- description: 'Create a new AI agent',
404
- inputSchema: {
405
- type: 'object',
406
- properties: {
407
- session_id: { type: 'string' },
408
- role: { type: 'string', enum: ['orchestrator', 'planner', 'implementer', 'reviewer', 'tester', 'debugger', 'documenter', 'specialist'] },
409
- name: { type: 'string' },
410
- provider: { type: 'string' },
411
- model: { type: 'string' },
412
- },
413
- required: ['session_id', 'role'],
414
- },
415
- },
416
- {
417
- name: 'agent_deploy',
418
- description: 'Deploy an agent to a task',
419
- inputSchema: {
420
- type: 'object',
421
- properties: {
422
- agent_id: { type: 'string' },
423
- task_id: { type: 'string' },
424
- },
425
- required: ['agent_id', 'task_id'],
426
- },
427
- },
428
- {
429
- name: 'agent_status',
430
- description: 'Get agent status',
431
- inputSchema: {
432
- type: 'object',
433
- properties: { agent_id: { type: 'string' } },
434
- required: ['agent_id'],
435
- },
436
- },
437
- {
438
- name: 'agent_list',
439
- description: 'List agents in a session. Use format="table" for beautiful ASCII table output.',
440
- inputSchema: {
441
- type: 'object',
442
- properties: {
443
- session_id: { type: 'string' },
444
- status: { type: 'string' },
445
- format: { type: 'string', enum: ['table', 'json'], description: 'Output format: "table" for ASCII table, "json" for raw JSON (default)' },
446
- },
447
- required: ['session_id'],
448
- },
449
- },
450
- {
451
- name: 'agent_retire',
452
- description: 'Retire an agent, marking it as completed',
453
- inputSchema: {
454
- type: 'object',
455
- properties: {
456
- agent_id: { type: 'string', description: 'ID of the agent to retire' },
457
- },
458
- required: ['agent_id'],
459
- },
460
- },
461
- // Task tools (5)
462
- {
463
- name: 'task_create',
464
- description: 'Create a task with intelligent routing. Supports agents and skills for Claude provider.',
465
- inputSchema: {
466
- type: 'object',
467
- properties: {
468
- session_id: { type: 'string' },
469
- type: { type: 'string' },
470
- description: { type: 'string' },
471
- prompt: { type: 'string' },
472
- context_files: { type: 'array', items: { type: 'string' } },
473
- force_provider: { type: 'string' },
474
- force_tier: { type: 'number' },
475
- agent: { type: 'string', description: 'Agent name to use (e.g., orchestration-specialist). Agent defines model, tools, and skills.' },
476
- skills: { type: 'array', items: { type: 'string' }, description: 'Specific skills to load (e.g., ["anastops-sessions", "anastops-tasks"])' },
477
- },
478
- required: ['session_id', 'type', 'description', 'prompt'],
479
- },
480
- },
481
- {
482
- name: 'task_queue',
483
- description: 'Queue a task for execution',
484
- inputSchema: {
485
- type: 'object',
486
- properties: { task_id: { type: 'string' } },
487
- required: ['task_id'],
488
- },
489
- },
490
- {
491
- name: 'task_status',
492
- description: 'Get task status',
493
- inputSchema: {
494
- type: 'object',
495
- properties: { task_id: { type: 'string' } },
496
- required: ['task_id'],
497
- },
498
- },
499
- {
500
- name: 'task_complete',
501
- description: 'Mark task as completed',
502
- inputSchema: {
503
- type: 'object',
504
- properties: {
505
- task_id: { type: 'string' },
506
- content: { type: 'string' },
507
- artifacts: { type: 'array', items: { type: 'string' } },
508
- },
509
- required: ['task_id', 'content'],
510
- },
511
- },
512
- {
513
- name: 'task_list',
514
- description: 'List tasks in a session. Use format="table" for beautiful ASCII table output.',
515
- inputSchema: {
516
- type: 'object',
517
- properties: {
518
- session_id: { type: 'string' },
519
- status: { type: 'string' },
520
- format: { type: 'string', enum: ['table', 'json'], description: 'Output format: "table" for ASCII table, "json" for raw JSON (default)' },
521
- },
522
- required: ['session_id'],
523
- },
524
- },
525
- {
526
- name: 'task_execute',
527
- description: 'Execute a pending or queued task',
528
- inputSchema: {
529
- type: 'object',
530
- properties: {
531
- task_id: { type: 'string', description: 'ID of the task to execute' },
532
- wait: { type: 'boolean', description: 'Wait for task completion (default: true)' },
533
- },
534
- required: ['task_id'],
535
- },
536
- },
537
- {
538
- name: 'task_cancel',
539
- description: 'Cancel a pending, queued, or running task',
540
- inputSchema: {
541
- type: 'object',
542
- properties: {
543
- task_id: { type: 'string', description: 'ID of the task to cancel' },
544
- },
545
- required: ['task_id'],
546
- },
547
- },
548
- {
549
- name: 'task_retry',
550
- description: 'Retry a failed task',
551
- inputSchema: {
552
- type: 'object',
553
- properties: {
554
- task_id: { type: 'string', description: 'ID of the failed task to retry' },
555
- },
556
- required: ['task_id'],
557
- },
558
- },
559
- // Lock Management tools (3)
560
- {
561
- name: 'lock_acquire',
562
- description: 'Acquire a file lock for a session to prevent conflicts',
563
- inputSchema: {
564
- type: 'object',
565
- properties: {
566
- session_id: { type: 'string' },
567
- file_path: { type: 'string' },
568
- ttl: { type: 'number', description: 'Lock TTL in milliseconds (default: 300000)' },
569
- },
570
- required: ['session_id', 'file_path'],
571
- },
572
- },
573
- {
574
- name: 'lock_release',
575
- description: 'Release a file lock held by a session',
576
- inputSchema: {
577
- type: 'object',
578
- properties: {
579
- session_id: { type: 'string' },
580
- file_path: { type: 'string' },
581
- },
582
- required: ['session_id', 'file_path'],
583
- },
584
- },
585
- {
586
- name: 'lock_status',
587
- description: 'Check lock status for a file or session',
588
- inputSchema: {
589
- type: 'object',
590
- properties: {
591
- session_id: { type: 'string', description: 'Session ID to check locks for' },
592
- file_path: { type: 'string', description: 'File path to check lock status' },
593
- },
594
- },
595
- },
596
- // Cost Monitoring tools (3)
597
- {
598
- name: 'session_cost',
599
- description: 'Get cost breakdown for a session',
600
- inputSchema: {
601
- type: 'object',
602
- properties: {
603
- session_id: { type: 'string' },
604
- },
605
- required: ['session_id'],
606
- },
607
- },
608
- {
609
- name: 'session_metrics',
610
- description: 'Get request and token usage metrics for a session',
611
- inputSchema: {
612
- type: 'object',
613
- properties: {
614
- session_id: { type: 'string' },
615
- },
616
- required: ['session_id'],
617
- },
618
- },
619
- {
620
- name: 'session_throttle',
621
- description: 'Check rate limit and throttle status for a session',
622
- inputSchema: {
623
- type: 'object',
624
- properties: {
625
- session_id: { type: 'string' },
626
- provider: { type: 'string', description: 'Specific provider to check' },
627
- },
628
- required: ['session_id'],
629
- },
630
- },
631
- // Memory tools (5)
632
- {
633
- name: 'memory_store',
634
- description: 'Store data in memory',
635
- inputSchema: {
636
- type: 'object',
637
- properties: {
638
- key: { type: 'string' },
639
- value: {},
640
- session_id: { type: 'string' },
641
- },
642
- required: ['key', 'value'],
643
- },
644
- },
645
- {
646
- name: 'memory_retrieve',
647
- description: 'Retrieve data from memory',
648
- inputSchema: {
649
- type: 'object',
650
- properties: { key: { type: 'string' } },
651
- required: ['key'],
652
- },
653
- },
654
- {
655
- name: 'artifact_save',
656
- description: 'Save an artifact',
657
- inputSchema: {
658
- type: 'object',
659
- properties: {
660
- session_id: { type: 'string' },
661
- type: { type: 'string', enum: ['code', 'document', 'data', 'config', 'test', 'other'] },
662
- name: { type: 'string' },
663
- content: { type: 'string' },
664
- },
665
- required: ['session_id', 'type', 'name', 'content'],
666
- },
667
- },
668
- {
669
- name: 'artifact_get',
670
- description: 'Get an artifact',
671
- inputSchema: {
672
- type: 'object',
673
- properties: { artifact_id: { type: 'string' } },
674
- required: ['artifact_id'],
675
- },
676
- },
677
- {
678
- name: 'artifact_list',
679
- description: 'List all artifacts for a session',
680
- inputSchema: {
681
- type: 'object',
682
- properties: {
683
- session_id: { type: 'string', description: 'Session ID to list artifacts for' },
684
- type: { type: 'string', enum: ['code', 'document', 'data', 'config', 'test', 'other'], description: 'Optional filter by artifact type' },
685
- },
686
- required: ['session_id'],
687
- },
688
- },
689
- {
690
- name: 'context_optimize',
691
- description: 'Optimize context for token reduction',
692
- inputSchema: {
693
- type: 'object',
694
- properties: {
695
- session_id: { type: 'string' },
696
- objective: { type: 'string' },
697
- phase: { type: 'string' },
698
- progress: { type: 'number' },
699
- messages: { type: 'array' },
700
- },
701
- required: ['session_id', 'objective', 'phase', 'progress', 'messages'],
702
- },
703
- },
704
- // Orchestration tools (4)
705
- {
706
- name: 'dispatch',
707
- description: 'Dispatch prompt to AI provider with routing',
708
- inputSchema: {
709
- type: 'object',
710
- properties: {
711
- session_id: { type: 'string' },
712
- prompt: { type: 'string' },
713
- type: { type: 'string' },
714
- force_provider: { type: 'string' },
715
- force_tier: { type: 'number' },
716
- },
717
- required: ['session_id', 'prompt'],
718
- },
719
- },
720
- {
721
- name: 'route',
722
- description: 'Get routing decision without executing',
723
- inputSchema: {
724
- type: 'object',
725
- properties: {
726
- type: { type: 'string' },
727
- description: { type: 'string' },
728
- },
729
- required: ['type', 'description'],
730
- },
731
- },
732
- {
733
- name: 'broadcast',
734
- description: 'Broadcast message to session agents',
735
- inputSchema: {
736
- type: 'object',
737
- properties: {
738
- session_id: { type: 'string' },
739
- message: { type: 'string' },
740
- message_type: { type: 'string', enum: ['info', 'warning', 'error', 'progress', 'complete'] },
741
- },
742
- required: ['session_id', 'message'],
743
- },
744
- },
745
- {
746
- name: 'aggregate',
747
- description: 'Aggregate results from multiple tasks',
748
- inputSchema: {
749
- type: 'object',
750
- properties: {
751
- task_ids: { type: 'array', items: { type: 'string' } },
752
- strategy: { type: 'string', enum: ['concat', 'merge', 'first', 'last', 'vote'] },
753
- },
754
- required: ['task_ids'],
755
- },
756
- },
757
- // Utility tools (5)
758
- {
759
- name: 'health_check',
760
- description: 'Check system health',
761
- inputSchema: {
762
- type: 'object',
763
- properties: { include_providers: { type: 'boolean' } },
764
- },
765
- },
766
- {
767
- name: 'provider_list',
768
- description: 'List available AI providers',
769
- inputSchema: {
770
- type: 'object',
771
- properties: {
772
- filter_healthy: { type: 'boolean' },
773
- include_capabilities: { type: 'boolean' },
774
- },
775
- },
776
- },
777
- {
778
- name: 'metrics_get',
779
- description: 'Get system metrics',
780
- inputSchema: {
781
- type: 'object',
782
- properties: { metric_type: { type: 'string', enum: ['routing', 'tokens', 'costs', 'all'] } },
783
- },
784
- },
785
- {
786
- name: 'config_get',
787
- description: 'Get configuration value',
788
- inputSchema: {
789
- type: 'object',
790
- properties: { key: { type: 'string' } },
791
- required: ['key'],
792
- },
793
- },
794
- {
795
- name: 'config_set',
796
- description: 'Set configuration value',
797
- inputSchema: {
798
- type: 'object',
799
- properties: { key: { type: 'string' }, value: {} },
800
- required: ['key', 'value'],
801
- },
802
- },
803
- ];
804
- // Tool handler
805
- export async function handleToolCall(name, args) {
806
- try {
807
- const result = await executeToolHandler(name, args);
808
- const format = getOutputFormat(args);
809
- // Apply table formatting for specific tools when format='table'
810
- if (format === 'table') {
811
- const formatted = formatResultAsTable(name, result);
812
- if (formatted !== null) {
813
- return {
814
- content: [{ type: 'text', text: formatted }],
815
- };
816
- }
817
- }
818
- // Return result directly as JSON (no pretty-printing - MCP messages must not contain embedded newlines)
819
- return {
820
- content: [{ type: 'text', text: JSON.stringify(result) }],
821
- isError: false,
822
- };
823
- }
824
- catch (error) {
825
- const message = error instanceof Error ? error.message : String(error);
826
- return {
827
- content: [{ type: 'text', text: JSON.stringify({ error: message }) }],
828
- isError: true,
829
- };
830
- }
831
- }
832
- /**
833
- * Format result as table for supported tools
834
- * Returns null if tool doesn't support table formatting
4
+ * This file re-exports the split handler modules for backward compatibility.
5
+ * The actual handlers are now split across the handlers/ directory:
6
+ * - handlers/handlers.base.ts - Shared utilities (TOON, formatting, cache)
7
+ * - handlers/handlers.session.ts - Session management
8
+ * - handlers/handlers.agent.ts - Agent management
9
+ * - handlers/handlers.task.ts - Task management
10
+ * - handlers/handlers.lock.ts - Lock management
11
+ * - handlers/handlers.monitoring.ts - Cost & metrics monitoring
12
+ * - handlers/handlers.memory.ts - Memory & artifacts
13
+ * - handlers/handlers.orchestration.ts - Dispatch, routing, broadcast
14
+ * - handlers/handlers.utility.ts - Health checks, config
15
+ * - handlers/index.ts - Main router
16
+ * - handlers/tool-definitions.ts - Tool schema definitions
17
+ *
18
+ * SECURITY: All inputs are validated using Zod schemas before processing.
835
19
  */
836
- function formatResultAsTable(toolName, result) {
837
- if (result === null || typeof result !== 'object') {
838
- return null;
839
- }
840
- const data = result;
841
- switch (toolName) {
842
- case 'session_list': {
843
- const sessions = data['sessions'];
844
- if (sessions !== undefined) {
845
- // Add created_at if not present (use current time as fallback)
846
- const sessionsWithDate = sessions.map(s => ({
847
- ...s,
848
- created_at: s.created_at ?? new Date(),
849
- }));
850
- return formatSessionTable(sessionsWithDate);
851
- }
852
- break;
853
- }
854
- case 'task_list': {
855
- const tasks = data['tasks'];
856
- if (tasks !== undefined) {
857
- return formatTaskTable(tasks);
858
- }
859
- break;
860
- }
861
- case 'agent_list': {
862
- const agents = data['agents'];
863
- if (agents !== undefined) {
864
- return formatAgentTable(agents);
865
- }
866
- break;
867
- }
868
- }
869
- return null;
870
- }
871
- async function executeToolHandler(name, args) {
872
- switch (name) {
873
- // Session tools
874
- case 'session_spawn': {
875
- const session = sessionManager.spawn({
876
- objective: args['objective'],
877
- ...(args['context_files'] !== undefined && { context_files: args['context_files'] }),
878
- ...(args['parent_session'] !== undefined && { parent_session: args['parent_session'] }),
879
- });
880
- // Persist to MongoDB
881
- safePersist(getPersistence().saveSession(session));
882
- return { session_id: session.id, objective: session.objective, status: session.status };
883
- }
884
- case 'session_status': {
885
- const status = sessionManager.getStatus(args['session_id']);
886
- // TOON encoding applied in handleToolCall
887
- return status;
888
- }
889
- case 'session_list': {
890
- // Get from in-memory first
891
- const inMemorySessions = sessionManager.list({
892
- ...(args['status'] !== undefined && { status: args['status'] }),
893
- ...(args['include_forks'] !== undefined && { include_forks: args['include_forks'] }),
894
- ...(args['limit'] !== undefined && { limit: args['limit'] }),
895
- });
896
- // Also get from MongoDB for persistence
897
- const persistedSessions = await getPersistence().listSessions({
898
- ...(args['status'] !== undefined && { status: args['status'] }),
899
- ...(args['limit'] !== undefined && { limit: args['limit'] }),
900
- });
901
- // Merge: in-memory takes precedence, then add persisted ones not in memory
902
- const inMemoryIds = new Set(inMemorySessions.map(s => s.id));
903
- const sessions = [
904
- ...inMemorySessions,
905
- ...persistedSessions.filter(s => !inMemoryIds.has(s.id)),
906
- ];
907
- // Return raw result for table formatting to be applied in handleToolCall
908
- const result = {
909
- count: sessions.length,
910
- sessions: sessions.map(s => ({
911
- id: s.id,
912
- objective: s.objective,
913
- status: s.status,
914
- created_at: s.created_at,
915
- })),
916
- };
917
- // Don't apply TOON encoding here - let handleToolCall handle formatting
918
- return result;
919
- }
920
- case 'session_fork': {
921
- const forked = forkService.fork({
922
- session_id: args['session_id'],
923
- ...(args['fork_point'] !== undefined && { fork_point: args['fork_point'] }),
924
- ...(args['reason'] !== undefined && { reason: args['reason'] }),
925
- });
926
- // Persist to MongoDB
927
- void getPersistence().saveSession(forked);
928
- return { forked_session_id: forked.id, parent_id: forked.parent_session_id, fork_point: forked.fork_point };
929
- }
930
- case 'session_archive': {
931
- const session = sessionManager.updateStatus(args['session_id'], 'archived');
932
- // Persist to MongoDB
933
- safePersist(getPersistence().saveSession(session));
934
- return { session_id: session.id, status: session.status };
935
- }
936
- case 'session_report': {
937
- const sessionId = args['session_id'];
938
- const statusFilter = args['status'];
939
- const includeOutput = args['include_output'] ?? true;
940
- const includeArtifactContent = args['include_artifacts'] ?? false;
941
- const limit = args['limit'] ?? 20;
942
- const persistence = getPersistence();
943
- if (sessionId) {
944
- // Single session report
945
- const report = await persistence.getSessionReport(sessionId);
946
- if (!report) {
947
- throw new Error(`Session not found: ${sessionId}`);
948
- }
949
- return formatSessionReport(report, includeOutput, includeArtifactContent);
950
- }
951
- // All sessions report
952
- const reportFilters = { limit };
953
- if (statusFilter !== undefined) {
954
- reportFilters.status = statusFilter;
955
- }
956
- const reports = await persistence.getAllSessionReports(reportFilters);
957
- return {
958
- total_sessions: reports.length,
959
- sessions: reports.map(r => formatSessionReport(r, includeOutput, includeArtifactContent)),
960
- };
961
- }
962
- case 'session_purge': {
963
- const confirm = args['confirm'];
964
- if (!confirm) {
965
- return { error: 'Must set confirm: true to execute deletion' };
966
- }
967
- const sessionId = args['session_id'];
968
- const sessionIds = args['session_ids'];
969
- const statusFilter = args['status'];
970
- const olderThanDays = args['older_than_days'];
971
- const persistence = getPersistence();
972
- // Single session deletion
973
- if (sessionId) {
974
- // Remove from in-memory cache
975
- sessionManager.remove(sessionId);
976
- // Remove related tasks from cache
977
- for (const [taskId, task] of tasks.entries()) {
978
- if (task.session_id === sessionId) {
979
- tasks.delete(taskId);
980
- }
981
- }
982
- // Remove related agents from cache
983
- for (const [agentId, agent] of agents.entries()) {
984
- if (agent.session_id === sessionId) {
985
- agents.delete(agentId);
986
- }
987
- }
988
- // Remove related artifacts from cache
989
- for (const [artifactId, artifact] of artifacts.entries()) {
990
- if (artifact.session_id === sessionId) {
991
- artifacts.delete(artifactId);
992
- }
993
- }
994
- const deleted = await persistence.deleteSession(sessionId);
995
- return { deleted: deleted, session_id: sessionId };
996
- }
997
- // Build filters for bulk deletion
998
- const filters = {};
999
- if (sessionIds && sessionIds.length > 0) {
1000
- filters.session_ids = sessionIds;
1001
- }
1002
- if (statusFilter) {
1003
- filters.status = statusFilter;
1004
- }
1005
- if (olderThanDays !== undefined) {
1006
- const cutoffDate = new Date();
1007
- cutoffDate.setDate(cutoffDate.getDate() - olderThanDays);
1008
- filters.older_than = cutoffDate;
1009
- }
1010
- if (Object.keys(filters).length === 0) {
1011
- return { error: 'Must provide at least one filter: session_id, session_ids, status, or older_than_days' };
1012
- }
1013
- const result = await persistence.deleteSessions(filters);
1014
- // Clean up in-memory caches for deleted sessions
1015
- for (const deletedId of result.session_ids) {
1016
- sessionManager.remove(deletedId);
1017
- for (const [taskId, task] of tasks.entries()) {
1018
- if (task.session_id === deletedId) {
1019
- tasks.delete(taskId);
1020
- }
1021
- }
1022
- for (const [agentId, agent] of agents.entries()) {
1023
- if (agent.session_id === deletedId) {
1024
- agents.delete(agentId);
1025
- }
1026
- }
1027
- for (const [artifactId, artifact] of artifacts.entries()) {
1028
- if (artifact.session_id === deletedId) {
1029
- artifacts.delete(artifactId);
1030
- }
1031
- }
1032
- }
1033
- return {
1034
- deleted_count: result.deleted_count,
1035
- session_ids: result.session_ids,
1036
- };
1037
- }
1038
- // Agent tools
1039
- case 'agent_create': {
1040
- const agentId = nanoid(12);
1041
- const now = new Date();
1042
- const agent = {
1043
- id: agentId,
1044
- session_id: args['session_id'],
1045
- role: args['role'] ?? 'implementer',
1046
- name: args['name'] ?? `agent-${agentId.slice(0, 4)}`,
1047
- status: 'idle',
1048
- provider: args['provider'] ?? 'claude',
1049
- model: args['model'] ?? 'claude-sonnet',
1050
- current_task_id: null,
1051
- tasks_completed: 0,
1052
- tasks_failed: 0,
1053
- tokens_used: 0,
1054
- created_at: now,
1055
- last_activity_at: now,
1056
- config: {},
1057
- };
1058
- agents.set(agentId, agent);
1059
- // Persist to MongoDB
1060
- safePersist(getPersistence().saveAgent(agent));
1061
- return { agent_id: agent.id, name: agent.name, role: agent.role, status: agent.status };
1062
- }
1063
- case 'agent_deploy': {
1064
- const agent = await getAgent(args['agent_id']);
1065
- if (agent === null)
1066
- throw new Error('Agent not found');
1067
- agent.current_task_id = args['task_id'];
1068
- agent.status = 'working';
1069
- agent.last_activity_at = new Date();
1070
- agents.set(agent.id, agent);
1071
- safePersist(getPersistence().saveAgent(agent));
1072
- return { agent_id: agent.id, task_id: agent.current_task_id, status: agent.status };
1073
- }
1074
- case 'agent_status': {
1075
- const agent = await getAgent(args['agent_id']);
1076
- if (agent === null)
1077
- throw new Error('Agent not found');
1078
- return agent;
1079
- }
1080
- case 'agent_list': {
1081
- // Get from in-memory cache first
1082
- const inMemoryAgents = [];
1083
- for (const [, agent] of agents.entries()) {
1084
- if (!args['session_id'] || agent.session_id === args['session_id']) {
1085
- inMemoryAgents.push(agent);
1086
- }
1087
- }
1088
- // Also get from MongoDB for persistence
1089
- const persistedAgents = await getPersistence().listAgents({
1090
- ...(args['session_id'] !== undefined && { session_id: args['session_id'] }),
1091
- });
1092
- // Merge: in-memory takes precedence
1093
- const inMemoryIds = new Set(inMemoryAgents.map(a => a.id));
1094
- const sessionAgents = [
1095
- ...inMemoryAgents,
1096
- ...persistedAgents.filter(a => !inMemoryIds.has(a.id)),
1097
- ];
1098
- return { count: sessionAgents.length, agents: sessionAgents.map(a => ({ id: a.id, name: a.name, role: a.role, status: a.status })) };
1099
- }
1100
- case 'agent_retire': {
1101
- const agentId = args['agent_id'];
1102
- const agent = await getAgent(agentId);
1103
- if (agent === null) {
1104
- throw new Error(`Agent not found: ${agentId}`);
1105
- }
1106
- // Retire the agent
1107
- agent.status = 'completed';
1108
- agent.last_activity_at = new Date();
1109
- agent.current_task_id = null;
1110
- agents.set(agentId, agent);
1111
- // Persist to MongoDB
1112
- safePersist(getPersistence().saveAgent(agent));
1113
- return {
1114
- agent_id: agent.id,
1115
- name: agent.name,
1116
- role: agent.role,
1117
- status: agent.status,
1118
- tasks_completed: agent.tasks_completed,
1119
- retired: true,
1120
- };
1121
- }
1122
- // Task tools
1123
- case 'task_create': {
1124
- const taskId = nanoid(12);
1125
- const now = new Date();
1126
- const taskType = args['type'] ?? 'other';
1127
- // Store agent and skills in task input for execution
1128
- const taskInput = {
1129
- prompt: args['prompt'],
1130
- context_files: args['context_files'] ?? [],
1131
- };
1132
- if (args['agent'] !== undefined) {
1133
- taskInput.agent = args['agent'];
1134
- }
1135
- if (args['skills'] !== undefined) {
1136
- taskInput.skills = args['skills'];
1137
- }
1138
- const task = {
1139
- id: taskId,
1140
- session_id: args['session_id'],
1141
- agent_id: null,
1142
- type: taskType,
1143
- status: 'pending',
1144
- description: args['description'],
1145
- input: taskInput,
1146
- output: null,
1147
- error: null,
1148
- complexity_score: 0,
1149
- routing_tier: 3,
1150
- provider: 'claude',
1151
- model: 'claude-sonnet',
1152
- token_usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0, cost: 0 },
1153
- created_at: now,
1154
- started_at: null,
1155
- completed_at: null,
1156
- dependencies: [],
1157
- priority: 5,
1158
- retry_count: 0,
1159
- max_retries: 3,
1160
- };
1161
- // Route the task with optional overrides
1162
- const routingOverride = {};
1163
- if (args['force_provider'] !== undefined) {
1164
- routingOverride.force_provider = args['force_provider'];
1165
- }
1166
- if (args['force_tier'] !== undefined) {
1167
- routingOverride.force_tier = args['force_tier'];
1168
- }
1169
- const routing = router.route(task, routingOverride);
1170
- task.routing_tier = routing.tier;
1171
- task.provider = routing.provider;
1172
- task.model = routing.model;
1173
- task.complexity_score = routing.complexity_score;
1174
- tasks.set(taskId, task);
1175
- // Persist to MongoDB
1176
- safePersist(getPersistence().saveTask(task));
1177
- const response = {
1178
- task_id: task.id,
1179
- type: task.type,
1180
- routing: { tier: routing.tier, provider: routing.provider, model: routing.model },
1181
- };
1182
- if (taskInput.agent) {
1183
- response.agent = taskInput.agent;
1184
- }
1185
- return response;
1186
- }
1187
- case 'task_queue': {
1188
- const task = await getTask(args['task_id']);
1189
- if (task === null)
1190
- throw new Error('Task not found');
1191
- task.status = 'queued';
1192
- tasks.set(task.id, task);
1193
- safePersist(getPersistence().saveTask(task));
1194
- return { task_id: task.id, status: task.status };
1195
- }
1196
- case 'task_status': {
1197
- const task = await getTask(args['task_id']);
1198
- if (task === null)
1199
- throw new Error('Task not found');
1200
- return task;
1201
- }
1202
- case 'task_complete': {
1203
- const task = await getTask(args['task_id']);
1204
- if (task === null)
1205
- throw new Error('Task not found');
1206
- task.status = 'completed';
1207
- task.completed_at = new Date();
1208
- task.output = { content: args['content'], artifacts: args['artifacts'] ?? [], files_modified: [], metadata: {} };
1209
- tasks.set(task.id, task);
1210
- // Persist to MongoDB
1211
- safePersist(getPersistence().saveTask(task));
1212
- return { task_id: task.id, status: task.status };
1213
- }
1214
- case 'task_list': {
1215
- // Get from in-memory cache first
1216
- const inMemoryTasks = [];
1217
- for (const [, task] of tasks.entries()) {
1218
- if (!args['session_id'] || task.session_id === args['session_id']) {
1219
- inMemoryTasks.push(task);
1220
- }
1221
- }
1222
- // Also get from MongoDB for persistence
1223
- const persistedTasks = await getPersistence().listTasks({
1224
- ...(args['session_id'] !== undefined && { session_id: args['session_id'] }),
1225
- });
1226
- // Merge: in-memory takes precedence
1227
- const inMemoryIds = new Set(inMemoryTasks.map(t => t.id));
1228
- const sessionTasks = [
1229
- ...inMemoryTasks,
1230
- ...persistedTasks.filter(t => !inMemoryIds.has(t.id)),
1231
- ];
1232
- return { count: sessionTasks.length, tasks: sessionTasks.map(t => ({ id: t.id, type: t.type, status: t.status, description: t.description.slice(0, 50) })) };
1233
- }
1234
- case 'task_execute': {
1235
- const taskId = args['task_id'];
1236
- const wait = args['wait'] ?? true;
1237
- const task = await getTask(taskId);
1238
- if (task === null) {
1239
- throw new Error(`Task not found: ${taskId}`);
1240
- }
1241
- if (task.status !== 'pending' && task.status !== 'queued') {
1242
- return {
1243
- task_id: taskId,
1244
- status: task.status,
1245
- error: `Task cannot be executed - current status is '${task.status}'`,
1246
- };
1247
- }
1248
- // Update task status to running
1249
- task.status = 'running';
1250
- task.started_at = new Date();
1251
- tasks.set(taskId, task);
1252
- // Persist running status
1253
- safePersist(getPersistence().saveTask(task));
1254
- try {
1255
- // Get adapter for the routed provider
1256
- const adapter = registry.get(task.provider);
1257
- if (adapter === undefined) {
1258
- throw new Error(`Provider adapter not found: ${task.provider}`);
1259
- }
1260
- // Execute task with adapter
1261
- // Use workspace root from environment or cwd
1262
- const workingDir = process.env['ANASTOPS_WORKSPACE'] ?? process.cwd();
1263
- // Build request with optional agent and skills
1264
- const taskInput = task.input;
1265
- const executeRequest = {
1266
- model: task.model,
1267
- prompt: taskInput?.prompt ?? task.description,
1268
- working_dir: workingDir,
1269
- };
1270
- // Pass agent and skills if specified
1271
- if (taskInput?.agent) {
1272
- executeRequest.agent = taskInput.agent;
1273
- }
1274
- if (taskInput?.skills) {
1275
- executeRequest.skills = taskInput.skills;
1276
- }
1277
- const response = await adapter.execute(executeRequest, { workingDir });
1278
- // Update task with result
1279
- task.status = 'completed';
1280
- task.completed_at = new Date();
1281
- task.output = {
1282
- content: response.content,
1283
- artifacts: [],
1284
- files_modified: [],
1285
- metadata: { usage: response.usage },
1286
- };
1287
- tasks.set(taskId, task);
1288
- // Persist completed task
1289
- safePersist(getPersistence().saveTask(task));
1290
- // Update session metadata via sessionManager
1291
- if (sessionManager.exists(task.session_id)) {
1292
- const session = sessionManager.getSession(task.session_id);
1293
- session.metadata = session.metadata ?? { total_tokens: 0, total_cost: 0, agents_used: [], files_affected: [], tasks_completed: 0, tasks_failed: 0 };
1294
- session.metadata.tasks_completed = (session.metadata.tasks_completed ?? 0) + 1;
1295
- session.metadata.total_tokens = (session.metadata.total_tokens ?? 0) + (response.usage?.total_tokens ?? 0);
1296
- session.metadata.total_cost = (session.metadata.total_cost ?? 0) + (response.usage?.cost ?? 0);
1297
- session.updated_at = new Date();
1298
- safePersist(getPersistence().saveSession(session));
1299
- }
1300
- return {
1301
- task_id: taskId,
1302
- status: 'completed',
1303
- result: response,
1304
- waited: wait,
1305
- };
1306
- }
1307
- catch (error) {
1308
- task.status = 'failed';
1309
- task.completed_at = new Date();
1310
- task.error = error instanceof Error ? error.message : String(error);
1311
- tasks.set(taskId, task);
1312
- // Persist failed task
1313
- safePersist(getPersistence().saveTask(task));
1314
- // Update session metadata for failure via sessionManager
1315
- if (sessionManager.exists(task.session_id)) {
1316
- const session = sessionManager.getSession(task.session_id);
1317
- session.metadata = session.metadata ?? { total_tokens: 0, total_cost: 0, agents_used: [], files_affected: [], tasks_completed: 0, tasks_failed: 0 };
1318
- session.metadata.tasks_failed = (session.metadata.tasks_failed ?? 0) + 1;
1319
- session.updated_at = new Date();
1320
- safePersist(getPersistence().saveSession(session));
1321
- }
1322
- throw error;
1323
- }
1324
- }
1325
- case 'task_cancel': {
1326
- const taskId = args['task_id'];
1327
- const task = await getTask(taskId);
1328
- if (task === null) {
1329
- throw new Error(`Task not found: ${taskId}`);
1330
- }
1331
- // Check if task is cancelable (pending, queued, or running)
1332
- const cancelableStatuses = ['pending', 'queued', 'running'];
1333
- if (!cancelableStatuses.includes(task.status)) {
1334
- return {
1335
- task_id: taskId,
1336
- status: task.status,
1337
- cancelled: false,
1338
- error: `Task cannot be cancelled - current status is '${task.status}'`,
1339
- };
1340
- }
1341
- // Cancel the task
1342
- task.status = 'cancelled';
1343
- task.completed_at = new Date();
1344
- tasks.set(taskId, task);
1345
- // Persist to MongoDB
1346
- safePersist(getPersistence().saveTask(task));
1347
- return {
1348
- task_id: taskId,
1349
- status: task.status,
1350
- cancelled: true,
1351
- };
1352
- }
1353
- case 'task_retry': {
1354
- const taskId = args['task_id'];
1355
- const task = await getTask(taskId);
1356
- if (task === null) {
1357
- throw new Error(`Task not found: ${taskId}`);
1358
- }
1359
- // Check if task is in failed status
1360
- if (task.status !== 'failed') {
1361
- return {
1362
- task_id: taskId,
1363
- status: task.status,
1364
- retried: false,
1365
- error: `Task cannot be retried - current status is '${task.status}' (must be 'failed')`,
1366
- };
1367
- }
1368
- // Check if max retries exceeded
1369
- if (task.retry_count >= task.max_retries) {
1370
- return {
1371
- task_id: taskId,
1372
- status: task.status,
1373
- retried: false,
1374
- error: `Task has exceeded max retries (${task.max_retries})`,
1375
- retry_count: task.retry_count,
1376
- };
1377
- }
1378
- // Retry the task
1379
- task.status = 'pending';
1380
- task.retry_count += 1;
1381
- task.error = null;
1382
- task.started_at = null;
1383
- task.completed_at = null;
1384
- tasks.set(taskId, task);
1385
- // Persist to MongoDB
1386
- safePersist(getPersistence().saveTask(task));
1387
- return {
1388
- task_id: taskId,
1389
- status: task.status,
1390
- retried: true,
1391
- retry_count: task.retry_count,
1392
- max_retries: task.max_retries,
1393
- };
1394
- }
1395
- // Lock Management tools
1396
- case 'lock_acquire': {
1397
- const sessionId = args['session_id'];
1398
- const filePath = args['file_path'];
1399
- const ttl = args['ttl'] ?? 300000;
1400
- // In-memory lock implementation (Redis-based implementation would be used in production)
1401
- const lockKey = `lock:${filePath}`;
1402
- const existingLock = memoryStore.get(lockKey);
1403
- if (existingLock !== undefined) {
1404
- const lockData = existingLock.value;
1405
- // Check if lock is expired
1406
- if (lockData.expires_at > Date.now() && lockData.session_id !== sessionId) {
1407
- return {
1408
- session_id: sessionId,
1409
- file_path: filePath,
1410
- acquired: false,
1411
- owner: lockData.session_id,
1412
- ttl,
1413
- };
1414
- }
1415
- }
1416
- // Acquire lock
1417
- memoryStore.set(lockKey, {
1418
- value: { session_id: sessionId, expires_at: Date.now() + ttl },
1419
- stored_at: new Date(),
1420
- });
1421
- return {
1422
- session_id: sessionId,
1423
- file_path: filePath,
1424
- acquired: true,
1425
- ttl,
1426
- };
1427
- }
1428
- case 'lock_release': {
1429
- const sessionId = args['session_id'];
1430
- const filePath = args['file_path'];
1431
- const lockKey = `lock:${filePath}`;
1432
- const existingLock = memoryStore.get(lockKey);
1433
- if (existingLock !== undefined) {
1434
- const lockData = existingLock.value;
1435
- if (lockData.session_id === sessionId) {
1436
- memoryStore.delete(lockKey);
1437
- return {
1438
- session_id: sessionId,
1439
- file_path: filePath,
1440
- released: true,
1441
- };
1442
- }
1443
- return {
1444
- session_id: sessionId,
1445
- file_path: filePath,
1446
- released: false,
1447
- error: 'Lock is held by another session',
1448
- };
1449
- }
1450
- return {
1451
- session_id: sessionId,
1452
- file_path: filePath,
1453
- released: true,
1454
- message: 'Lock did not exist',
1455
- };
1456
- }
1457
- case 'lock_status': {
1458
- const sessionId = args['session_id'];
1459
- const filePath = args['file_path'];
1460
- if (filePath !== undefined) {
1461
- const lockKey = `lock:${filePath}`;
1462
- const existingLock = memoryStore.get(lockKey);
1463
- if (existingLock !== undefined) {
1464
- const lockData = existingLock.value;
1465
- const isExpired = lockData.expires_at <= Date.now();
1466
- return {
1467
- file_path: filePath,
1468
- is_locked: !isExpired,
1469
- owner: isExpired ? null : lockData.session_id,
1470
- expires_at: lockData.expires_at,
1471
- };
1472
- }
1473
- return {
1474
- file_path: filePath,
1475
- is_locked: false,
1476
- owner: null,
1477
- };
1478
- }
1479
- if (sessionId !== undefined) {
1480
- // List all locks held by this session
1481
- const sessionLocks = [];
1482
- for (const [key, data] of memoryStore.entries()) {
1483
- if (key.startsWith('lock:')) {
1484
- const lockData = data.value;
1485
- if (lockData.session_id === sessionId && lockData.expires_at > Date.now()) {
1486
- sessionLocks.push({
1487
- file_path: key.replace('lock:', ''),
1488
- expires_at: lockData.expires_at,
1489
- });
1490
- }
1491
- }
1492
- }
1493
- return {
1494
- session_id: sessionId,
1495
- lock_count: sessionLocks.length,
1496
- locks: sessionLocks,
1497
- };
1498
- }
1499
- return { error: 'Must provide either session_id or file_path' };
1500
- }
1501
- // Cost Monitoring tools
1502
- case 'session_cost': {
1503
- const sessionId = args['session_id'];
1504
- // Aggregate cost from task token_usage
1505
- let totalCost = 0;
1506
- for (const [, task] of tasks.entries()) {
1507
- if (task.session_id === sessionId && task.token_usage !== undefined) {
1508
- totalCost += task.token_usage.cost ?? 0;
1509
- }
1510
- }
1511
- return {
1512
- session_id: sessionId,
1513
- total_cost_usd: totalCost,
1514
- currency: 'USD',
1515
- };
1516
- }
1517
- case 'session_metrics': {
1518
- const sessionId = args['session_id'];
1519
- // Aggregate metrics from tasks
1520
- let requestCount = 0;
1521
- let inputTokens = 0;
1522
- let outputTokens = 0;
1523
- let totalTokens = 0;
1524
- for (const [, task] of tasks.entries()) {
1525
- if (task.session_id === sessionId) {
1526
- requestCount++;
1527
- if (task.token_usage !== undefined) {
1528
- inputTokens += task.token_usage.prompt_tokens ?? 0;
1529
- outputTokens += task.token_usage.completion_tokens ?? 0;
1530
- totalTokens += task.token_usage.total_tokens ?? 0;
1531
- }
1532
- }
1533
- }
1534
- return {
1535
- session_id: sessionId,
1536
- request_count: requestCount,
1537
- token_usage: {
1538
- input: inputTokens,
1539
- output: outputTokens,
1540
- total: totalTokens,
1541
- },
1542
- };
1543
- }
1544
- case 'session_throttle': {
1545
- const sessionId = args['session_id'];
1546
- const provider = args['provider'] ?? 'claude';
1547
- // In-memory rate limit tracking (Redis-based in production)
1548
- const rateKey = `rate:${sessionId}:${provider}`;
1549
- const rateData = memoryStore.get(rateKey);
1550
- // Default limits per provider (requests per minute)
1551
- const defaultLimit = 30;
1552
- const limits = {
1553
- claude: 50,
1554
- openai: 60,
1555
- gemini: 60,
1556
- };
1557
- const limit = limits[provider] ?? defaultLimit;
1558
- if (rateData !== undefined) {
1559
- const data = rateData.value;
1560
- const windowSize = 60000; // 1 minute
1561
- const now = Date.now();
1562
- if (now - data.window_start > windowSize) {
1563
- // Window expired, reset
1564
- return {
1565
- session_id: sessionId,
1566
- provider,
1567
- usage_percent: 0,
1568
- current_count: 0,
1569
- limit,
1570
- throttled: false,
1571
- blocked: false,
1572
- };
1573
- }
1574
- const usagePercent = (data.count / limit) * 100;
1575
- return {
1576
- session_id: sessionId,
1577
- provider,
1578
- usage_percent: Math.min(100, usagePercent),
1579
- current_count: data.count,
1580
- limit,
1581
- throttled: usagePercent >= 85,
1582
- blocked: usagePercent >= 95,
1583
- };
1584
- }
1585
- return {
1586
- session_id: sessionId,
1587
- provider,
1588
- usage_percent: 0,
1589
- current_count: 0,
1590
- limit,
1591
- throttled: false,
1592
- blocked: false,
1593
- };
1594
- }
1595
- // Memory tools
1596
- case 'memory_store': {
1597
- const key = args['session_id'] !== undefined
1598
- ? `${String(args['session_id'])}:${String(args['key'])}`
1599
- : String(args['key']);
1600
- memoryStore.set(key, { value: args['value'], stored_at: new Date() });
1601
- return { key, stored: true };
1602
- }
1603
- case 'memory_retrieve': {
1604
- const data = memoryStore.get(args['key']);
1605
- if (data === undefined)
1606
- throw new Error('Key not found');
1607
- return { key: args['key'], data };
1608
- }
1609
- case 'artifact_save': {
1610
- const artifactId = nanoid(12);
1611
- const now = new Date();
1612
- const content = args['content'];
1613
- const artifact = {
1614
- id: artifactId,
1615
- session_id: args['session_id'],
1616
- task_id: null,
1617
- type: args['type'] ?? 'other',
1618
- name: args['name'],
1619
- extension: '',
1620
- content,
1621
- content_hash: '',
1622
- size_bytes: content.length,
1623
- summary: content.slice(0, 200),
1624
- token_count: Math.ceil(content.length / 4),
1625
- relevance_score: 50,
1626
- metadata: {},
1627
- created_at: now,
1628
- updated_at: now,
1629
- };
1630
- artifacts.set(artifactId, artifact);
1631
- // Persist to MongoDB
1632
- safePersist(getPersistence().saveArtifact(artifact));
1633
- return { artifact_id: artifact.id, name: artifact.name, size_bytes: artifact.size_bytes };
1634
- }
1635
- case 'artifact_get': {
1636
- const artifact = await getArtifact(args['artifact_id']);
1637
- if (artifact === null)
1638
- throw new Error('Artifact not found');
1639
- return artifact;
1640
- }
1641
- case 'artifact_list': {
1642
- const sessionId = args['session_id'];
1643
- const typeFilter = args['type'];
1644
- // Get from in-memory cache first
1645
- const inMemoryArtifacts = [];
1646
- for (const [, artifact] of artifacts.entries()) {
1647
- if (artifact.session_id === sessionId) {
1648
- if (typeFilter === undefined || artifact.type === typeFilter) {
1649
- inMemoryArtifacts.push(artifact);
1650
- }
1651
- }
1652
- }
1653
- // Also get from MongoDB for persistence
1654
- const persistedArtifacts = await getPersistence().listArtifacts({
1655
- session_id: sessionId,
1656
- ...(typeFilter !== undefined && { type: typeFilter }),
1657
- });
1658
- // Merge: in-memory takes precedence
1659
- const inMemoryIds = new Set(inMemoryArtifacts.map(a => a.id));
1660
- const sessionArtifacts = [
1661
- ...inMemoryArtifacts,
1662
- ...persistedArtifacts.filter(a => !inMemoryIds.has(a.id)),
1663
- ];
1664
- return {
1665
- session_id: sessionId,
1666
- count: sessionArtifacts.length,
1667
- ...(typeFilter !== undefined && { type_filter: typeFilter }),
1668
- artifacts: sessionArtifacts.map(a => ({
1669
- id: a.id,
1670
- name: a.name,
1671
- type: a.type,
1672
- size_bytes: a.size_bytes,
1673
- created_at: a.created_at,
1674
- })),
1675
- };
1676
- }
1677
- case 'context_optimize': {
1678
- const sessionArtifacts = [];
1679
- for (const [, artifact] of artifacts.entries()) {
1680
- if (artifact.session_id === args['session_id']) {
1681
- sessionArtifacts.push(artifact);
1682
- }
1683
- }
1684
- const result = contextOptimizer.optimize({
1685
- session_id: args['session_id'],
1686
- objective: args['objective'],
1687
- phase: args['phase'],
1688
- progress: args['progress'],
1689
- current_task: null,
1690
- artifacts: sessionArtifacts,
1691
- messages: args['messages'],
1692
- decisions: [],
1693
- completed_tasks: [],
1694
- active_work: [],
1695
- blockers: [],
1696
- });
1697
- return {
1698
- original_tokens: result.original_tokens,
1699
- optimized_tokens: result.optimized_tokens,
1700
- reduction_percent: result.reduction_percent,
1701
- minimal_context: result.context,
1702
- };
1703
- }
1704
- // Orchestration tools
1705
- case 'dispatch': {
1706
- const taskId = nanoid(12);
1707
- const task = {
1708
- id: taskId,
1709
- session_id: args['session_id'],
1710
- agent_id: null,
1711
- type: args['type'] ?? 'other',
1712
- status: 'pending',
1713
- description: args['prompt'].slice(0, 200),
1714
- input: { prompt: args['prompt'] },
1715
- output: null,
1716
- error: null,
1717
- complexity_score: 0,
1718
- routing_tier: 3,
1719
- provider: 'claude',
1720
- model: 'claude-sonnet',
1721
- token_usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0, cost: 0 },
1722
- created_at: new Date(),
1723
- started_at: null,
1724
- completed_at: null,
1725
- dependencies: [],
1726
- priority: 5,
1727
- retry_count: 0,
1728
- max_retries: 3,
1729
- };
1730
- const routingOverride = {};
1731
- if (args['force_provider'] !== undefined) {
1732
- routingOverride.force_provider = args['force_provider'];
1733
- }
1734
- if (args['force_tier'] !== undefined) {
1735
- routingOverride.force_tier = args['force_tier'];
1736
- }
1737
- const routing = router.route(task, routingOverride);
1738
- // Get adapter and execute
1739
- const adapter = registry.get(routing.provider);
1740
- if (adapter === undefined) {
1741
- throw new Error(`Provider not available: ${routing.provider}`);
1742
- }
1743
- // Use workspace root for dispatch calls
1744
- const workingDir = process.env['ANASTOPS_WORKSPACE'] ?? process.cwd();
1745
- const response = await adapter.execute({ prompt: args['prompt'], model: routing.model, working_dir: workingDir }, { workingDir });
1746
- const dispatchResult = {
1747
- task_id: taskId,
1748
- routing: { tier: routing.tier, provider: routing.provider, model: routing.model },
1749
- response: { content: response.content, usage: response.usage },
1750
- };
1751
- // TOON encoding applied in handleToolCall
1752
- return dispatchResult;
1753
- }
1754
- case 'route': {
1755
- const task = {
1756
- id: 'route-check',
1757
- session_id: 'route-check',
1758
- agent_id: null,
1759
- type: args['type'] ?? 'other',
1760
- status: 'pending',
1761
- description: args['description'],
1762
- input: { prompt: args['description'] },
1763
- output: null,
1764
- error: null,
1765
- complexity_score: 0,
1766
- routing_tier: 3,
1767
- provider: 'claude',
1768
- model: 'claude-sonnet',
1769
- token_usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0, cost: 0 },
1770
- created_at: new Date(),
1771
- started_at: null,
1772
- completed_at: null,
1773
- dependencies: [],
1774
- priority: 5,
1775
- retry_count: 0,
1776
- max_retries: 3,
1777
- };
1778
- const routing = router.route(task, {});
1779
- return routing;
1780
- }
1781
- case 'broadcast': {
1782
- return { session_id: args['session_id'], message: args['message'], message_type: args['message_type'] ?? 'info', broadcast_at: new Date() };
1783
- }
1784
- case 'aggregate': {
1785
- const taskIds = args['task_ids'];
1786
- const taskResults = taskIds.map(id => tasks.get(id)).filter(t => t !== undefined);
1787
- return { task_count: taskResults.length, strategy: args['strategy'] ?? 'concat' };
1788
- }
1789
- // Utility tools
1790
- case 'health_check': {
1791
- const result = {
1792
- status: 'healthy',
1793
- timestamp: new Date(),
1794
- components: { core: 'healthy', router: 'healthy', memory: 'in-memory' },
1795
- };
1796
- if (args['include_providers'] === true) {
1797
- const providerHealth = {};
1798
- for (const adapter of registry.list()) {
1799
- const health = await adapter.healthCheck();
1800
- providerHealth[adapter.type] = { healthy: health.healthy, error: health.error };
1801
- }
1802
- result['providers'] = providerHealth;
1803
- }
1804
- return result;
1805
- }
1806
- case 'provider_list': {
1807
- const providers = [];
1808
- for (const adapter of registry.list()) {
1809
- const health = await adapter.healthCheck();
1810
- if (args['filter_healthy'] === true && !health.healthy)
1811
- continue;
1812
- const info = { type: adapter.type, name: adapter.name, healthy: health.healthy };
1813
- if (args['include_capabilities'] === true) {
1814
- const caps = await adapter.getCapabilities();
1815
- info['capabilities'] = caps;
1816
- }
1817
- providers.push(info);
1818
- }
1819
- return { count: providers.length, providers };
1820
- }
1821
- case 'metrics_get': {
1822
- return router.getMetrics();
1823
- }
1824
- case 'config_get': {
1825
- // Placeholder config
1826
- const config = {
1827
- default_provider: 'claude',
1828
- default_timeout: 600000, // 10 minutes
1829
- context_budget: 1600,
1830
- };
1831
- return { key: args['key'], value: config[args['key']] };
1832
- }
1833
- case 'config_set': {
1834
- return { key: args['key'], value: args['value'], updated: true };
1835
- }
1836
- default:
1837
- throw new Error(`Unknown tool: ${name}`);
1838
- }
1839
- }
20
+ export { handleToolCall, toolDefinitions } from './handlers/index.js';
1840
21
  //# sourceMappingURL=handlers.js.map