code-graph-context 2.4.5 → 2.5.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.
- package/README.md +462 -622
- package/dist/mcp/constants.js +274 -0
- package/dist/mcp/handlers/swarm-worker.handler.js +251 -0
- package/dist/mcp/handlers/task-decomposition.handler.js +294 -0
- package/dist/mcp/tools/index.js +13 -1
- package/dist/mcp/tools/swarm-claim-task.tool.js +331 -0
- package/dist/mcp/tools/swarm-complete-task.tool.js +421 -0
- package/dist/mcp/tools/swarm-constants.js +113 -0
- package/dist/mcp/tools/swarm-get-tasks.tool.js +419 -0
- package/dist/mcp/tools/swarm-orchestrate.tool.js +410 -0
- package/dist/mcp/tools/swarm-post-task.tool.js +235 -0
- package/package.json +1 -1
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swarm Get Tasks Tool
|
|
3
|
+
* Query tasks from the blackboard with various filters
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { Neo4jService } from '../../storage/neo4j/neo4j.service.js';
|
|
7
|
+
import { TOOL_NAMES, TOOL_METADATA } from '../constants.js';
|
|
8
|
+
import { createErrorResponse, createSuccessResponse, resolveProjectIdOrError, debugLog } from '../utils.js';
|
|
9
|
+
import { TASK_STATUSES, TASK_TYPES, TASK_PRIORITIES } from './swarm-constants.js';
|
|
10
|
+
/**
|
|
11
|
+
* Main query to get tasks with filters
|
|
12
|
+
*/
|
|
13
|
+
const GET_TASKS_QUERY = `
|
|
14
|
+
MATCH (t:SwarmTask {projectId: $projectId})
|
|
15
|
+
WHERE ($swarmId IS NULL OR t.swarmId = $swarmId)
|
|
16
|
+
AND ($statuses IS NULL OR size($statuses) = 0 OR t.status IN $statuses)
|
|
17
|
+
AND ($types IS NULL OR size($types) = 0 OR t.type IN $types)
|
|
18
|
+
AND ($claimedBy IS NULL OR t.claimedBy = $claimedBy)
|
|
19
|
+
AND ($createdBy IS NULL OR t.createdBy = $createdBy)
|
|
20
|
+
AND ($minPriority IS NULL OR t.priorityScore >= $minPriority)
|
|
21
|
+
|
|
22
|
+
// Get dependency info
|
|
23
|
+
OPTIONAL MATCH (t)-[:DEPENDS_ON]->(dep:SwarmTask)
|
|
24
|
+
WITH t, collect({id: dep.id, title: dep.title, status: dep.status}) as dependencies
|
|
25
|
+
|
|
26
|
+
// Get tasks blocked by this one
|
|
27
|
+
OPTIONAL MATCH (blocked:SwarmTask)-[:DEPENDS_ON]->(t)
|
|
28
|
+
WITH t, dependencies, collect({id: blocked.id, title: blocked.title, status: blocked.status}) as blockedTasks
|
|
29
|
+
|
|
30
|
+
// Get target code nodes
|
|
31
|
+
OPTIONAL MATCH (t)-[:TARGETS]->(target)
|
|
32
|
+
WITH t, dependencies, blockedTasks,
|
|
33
|
+
collect(DISTINCT {id: target.id, type: labels(target)[0], name: target.name, filePath: target.filePath}) as targets
|
|
34
|
+
|
|
35
|
+
RETURN t.id as id,
|
|
36
|
+
t.projectId as projectId,
|
|
37
|
+
t.swarmId as swarmId,
|
|
38
|
+
t.title as title,
|
|
39
|
+
t.description as description,
|
|
40
|
+
t.type as type,
|
|
41
|
+
t.priority as priority,
|
|
42
|
+
t.priorityScore as priorityScore,
|
|
43
|
+
t.status as status,
|
|
44
|
+
t.targetNodeIds as targetNodeIds,
|
|
45
|
+
t.targetFilePaths as targetFilePaths,
|
|
46
|
+
t.claimedBy as claimedBy,
|
|
47
|
+
t.claimedAt as claimedAt,
|
|
48
|
+
t.startedAt as startedAt,
|
|
49
|
+
t.completedAt as completedAt,
|
|
50
|
+
t.createdBy as createdBy,
|
|
51
|
+
t.createdAt as createdAt,
|
|
52
|
+
t.summary as summary,
|
|
53
|
+
t.metadata as metadata,
|
|
54
|
+
dependencies,
|
|
55
|
+
blockedTasks,
|
|
56
|
+
[target IN targets WHERE target.id IS NOT NULL] as targets
|
|
57
|
+
|
|
58
|
+
ORDER BY
|
|
59
|
+
CASE WHEN $orderBy = 'priority' THEN t.priorityScore END DESC,
|
|
60
|
+
CASE WHEN $orderBy = 'created' THEN t.createdAt END DESC,
|
|
61
|
+
CASE WHEN $orderBy = 'updated' THEN t.updatedAt END DESC,
|
|
62
|
+
t.priorityScore DESC,
|
|
63
|
+
t.createdAt ASC
|
|
64
|
+
|
|
65
|
+
SKIP toInteger($skip)
|
|
66
|
+
LIMIT toInteger($limit)
|
|
67
|
+
`;
|
|
68
|
+
/**
|
|
69
|
+
* Query to get task statistics for a swarm
|
|
70
|
+
*/
|
|
71
|
+
const GET_TASK_STATS_QUERY = `
|
|
72
|
+
MATCH (t:SwarmTask {projectId: $projectId})
|
|
73
|
+
WHERE ($swarmId IS NULL OR t.swarmId = $swarmId)
|
|
74
|
+
|
|
75
|
+
WITH t.status as status, t.type as type, t.priority as priority,
|
|
76
|
+
t.claimedBy as agent, count(t) as count
|
|
77
|
+
|
|
78
|
+
RETURN status, type, priority, agent, count
|
|
79
|
+
ORDER BY count DESC
|
|
80
|
+
`;
|
|
81
|
+
/**
|
|
82
|
+
* Query to get a single task by ID with full details
|
|
83
|
+
*/
|
|
84
|
+
const GET_TASK_BY_ID_QUERY = `
|
|
85
|
+
MATCH (t:SwarmTask {id: $taskId, projectId: $projectId})
|
|
86
|
+
|
|
87
|
+
// Get dependencies
|
|
88
|
+
OPTIONAL MATCH (t)-[:DEPENDS_ON]->(dep:SwarmTask)
|
|
89
|
+
WITH t, collect({
|
|
90
|
+
id: dep.id,
|
|
91
|
+
title: dep.title,
|
|
92
|
+
status: dep.status,
|
|
93
|
+
claimedBy: dep.claimedBy
|
|
94
|
+
}) as dependencies
|
|
95
|
+
|
|
96
|
+
// Get tasks blocked by this one
|
|
97
|
+
OPTIONAL MATCH (blocked:SwarmTask)-[:DEPENDS_ON]->(t)
|
|
98
|
+
WITH t, dependencies, collect({
|
|
99
|
+
id: blocked.id,
|
|
100
|
+
title: blocked.title,
|
|
101
|
+
status: blocked.status
|
|
102
|
+
}) as blockedTasks
|
|
103
|
+
|
|
104
|
+
// Get target code nodes with more detail
|
|
105
|
+
OPTIONAL MATCH (t)-[:TARGETS]->(target)
|
|
106
|
+
WITH t, dependencies, blockedTasks,
|
|
107
|
+
collect(DISTINCT {
|
|
108
|
+
id: target.id,
|
|
109
|
+
type: labels(target)[0],
|
|
110
|
+
name: target.name,
|
|
111
|
+
filePath: target.filePath,
|
|
112
|
+
coreType: target.coreType,
|
|
113
|
+
semanticType: target.semanticType
|
|
114
|
+
}) as targets
|
|
115
|
+
|
|
116
|
+
RETURN t {
|
|
117
|
+
.*,
|
|
118
|
+
dependencies: dependencies,
|
|
119
|
+
blockedTasks: blockedTasks,
|
|
120
|
+
targets: [target IN targets WHERE target.id IS NOT NULL]
|
|
121
|
+
} as task
|
|
122
|
+
`;
|
|
123
|
+
/**
|
|
124
|
+
* Query to get active workers from pheromones
|
|
125
|
+
*/
|
|
126
|
+
const GET_ACTIVE_WORKERS_QUERY = `
|
|
127
|
+
MATCH (p:Pheromone {projectId: $projectId})
|
|
128
|
+
WHERE ($swarmId IS NULL OR p.swarmId = $swarmId)
|
|
129
|
+
AND p.type IN ['modifying', 'claiming']
|
|
130
|
+
WITH p.agentId as agentId, p.type as type,
|
|
131
|
+
max(p.timestamp) as lastActivity,
|
|
132
|
+
count(p) as nodeCount
|
|
133
|
+
RETURN agentId, type,
|
|
134
|
+
lastActivity,
|
|
135
|
+
nodeCount,
|
|
136
|
+
duration.between(datetime({epochMillis: lastActivity}), datetime()).minutes as minutesSinceActivity
|
|
137
|
+
ORDER BY lastActivity DESC
|
|
138
|
+
`;
|
|
139
|
+
/**
|
|
140
|
+
* Query to get the dependency graph for visualization
|
|
141
|
+
*/
|
|
142
|
+
const GET_DEPENDENCY_GRAPH_QUERY = `
|
|
143
|
+
MATCH (t:SwarmTask {projectId: $projectId})
|
|
144
|
+
WHERE ($swarmId IS NULL OR t.swarmId = $swarmId)
|
|
145
|
+
|
|
146
|
+
OPTIONAL MATCH (t)-[r:DEPENDS_ON]->(dep:SwarmTask)
|
|
147
|
+
|
|
148
|
+
RETURN collect(DISTINCT {
|
|
149
|
+
id: t.id,
|
|
150
|
+
title: t.title,
|
|
151
|
+
status: t.status,
|
|
152
|
+
priority: t.priority,
|
|
153
|
+
type: t.type,
|
|
154
|
+
claimedBy: t.claimedBy
|
|
155
|
+
}) as nodes,
|
|
156
|
+
collect(DISTINCT CASE WHEN dep IS NOT NULL THEN {from: t.id, to: dep.id} END) as edges
|
|
157
|
+
`;
|
|
158
|
+
export const createSwarmGetTasksTool = (server) => {
|
|
159
|
+
server.registerTool(TOOL_NAMES.swarmGetTasks, {
|
|
160
|
+
title: TOOL_METADATA[TOOL_NAMES.swarmGetTasks].title,
|
|
161
|
+
description: TOOL_METADATA[TOOL_NAMES.swarmGetTasks].description,
|
|
162
|
+
inputSchema: {
|
|
163
|
+
projectId: z.string().describe('Project ID, name, or path'),
|
|
164
|
+
swarmId: z.string().optional().describe('Filter by swarm ID'),
|
|
165
|
+
taskId: z.string().optional().describe('Get a specific task by ID (returns full details)'),
|
|
166
|
+
statuses: z
|
|
167
|
+
.array(z.enum(TASK_STATUSES))
|
|
168
|
+
.optional()
|
|
169
|
+
.describe('Filter by task statuses (e.g., ["available", "in_progress"])'),
|
|
170
|
+
types: z
|
|
171
|
+
.array(z.enum(TASK_TYPES))
|
|
172
|
+
.optional()
|
|
173
|
+
.describe('Filter by task types (e.g., ["implement", "fix"])'),
|
|
174
|
+
claimedBy: z.string().optional().describe('Filter tasks claimed by a specific agent'),
|
|
175
|
+
createdBy: z.string().optional().describe('Filter tasks created by a specific agent'),
|
|
176
|
+
minPriority: z
|
|
177
|
+
.enum(Object.keys(TASK_PRIORITIES))
|
|
178
|
+
.optional()
|
|
179
|
+
.describe('Minimum priority level'),
|
|
180
|
+
orderBy: z
|
|
181
|
+
.enum(['priority', 'created', 'updated'])
|
|
182
|
+
.optional()
|
|
183
|
+
.default('priority')
|
|
184
|
+
.describe('Sort order: priority (highest first), created (newest first), updated'),
|
|
185
|
+
limit: z
|
|
186
|
+
.number()
|
|
187
|
+
.int()
|
|
188
|
+
.min(1)
|
|
189
|
+
.max(100)
|
|
190
|
+
.optional()
|
|
191
|
+
.default(20)
|
|
192
|
+
.describe('Maximum tasks to return (default: 20)'),
|
|
193
|
+
skip: z.number().int().min(0).optional().default(0).describe('Number of tasks to skip for pagination'),
|
|
194
|
+
includeStats: z
|
|
195
|
+
.boolean()
|
|
196
|
+
.optional()
|
|
197
|
+
.default(false)
|
|
198
|
+
.describe('Include aggregate statistics by status/type/agent'),
|
|
199
|
+
includeDependencyGraph: z
|
|
200
|
+
.boolean()
|
|
201
|
+
.optional()
|
|
202
|
+
.default(false)
|
|
203
|
+
.describe('Include dependency graph for visualization'),
|
|
204
|
+
},
|
|
205
|
+
}, async ({ projectId, swarmId, taskId, statuses, types, claimedBy, createdBy, minPriority, orderBy = 'priority', limit = 20, skip = 0, includeStats = false, includeDependencyGraph = false, }) => {
|
|
206
|
+
const neo4jService = new Neo4jService();
|
|
207
|
+
// Resolve project ID
|
|
208
|
+
const projectResult = await resolveProjectIdOrError(projectId, neo4jService);
|
|
209
|
+
if (!projectResult.success) {
|
|
210
|
+
await neo4jService.close();
|
|
211
|
+
return projectResult.error;
|
|
212
|
+
}
|
|
213
|
+
const resolvedProjectId = projectResult.projectId;
|
|
214
|
+
try {
|
|
215
|
+
await debugLog('Swarm get tasks', {
|
|
216
|
+
projectId: resolvedProjectId,
|
|
217
|
+
swarmId,
|
|
218
|
+
taskId,
|
|
219
|
+
statuses,
|
|
220
|
+
types,
|
|
221
|
+
claimedBy,
|
|
222
|
+
limit,
|
|
223
|
+
});
|
|
224
|
+
// If taskId is provided, get single task with full details
|
|
225
|
+
if (taskId) {
|
|
226
|
+
const result = await neo4jService.run(GET_TASK_BY_ID_QUERY, {
|
|
227
|
+
taskId,
|
|
228
|
+
projectId: resolvedProjectId,
|
|
229
|
+
});
|
|
230
|
+
if (result.length === 0) {
|
|
231
|
+
return createErrorResponse(`Task ${taskId} not found`);
|
|
232
|
+
}
|
|
233
|
+
const task = result[0].task;
|
|
234
|
+
// Parse metadata if present
|
|
235
|
+
if (task.metadata) {
|
|
236
|
+
try {
|
|
237
|
+
task.metadata = JSON.parse(task.metadata);
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
// Keep as string if not valid JSON
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Parse artifacts if present
|
|
244
|
+
if (task.artifacts) {
|
|
245
|
+
try {
|
|
246
|
+
task.artifacts = JSON.parse(task.artifacts);
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
// Keep as string if not valid JSON
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// Convert Neo4j integers
|
|
253
|
+
const convertTimestamp = (ts) => typeof ts === 'object' && ts?.toNumber ? ts.toNumber() : ts;
|
|
254
|
+
task.createdAt = convertTimestamp(task.createdAt);
|
|
255
|
+
task.updatedAt = convertTimestamp(task.updatedAt);
|
|
256
|
+
task.claimedAt = convertTimestamp(task.claimedAt);
|
|
257
|
+
task.startedAt = convertTimestamp(task.startedAt);
|
|
258
|
+
task.completedAt = convertTimestamp(task.completedAt);
|
|
259
|
+
return createSuccessResponse(JSON.stringify({ success: true, task }));
|
|
260
|
+
}
|
|
261
|
+
// Get list of tasks
|
|
262
|
+
const minPriorityScore = minPriority
|
|
263
|
+
? TASK_PRIORITIES[minPriority]
|
|
264
|
+
: null;
|
|
265
|
+
const tasksResult = await neo4jService.run(GET_TASKS_QUERY, {
|
|
266
|
+
projectId: resolvedProjectId,
|
|
267
|
+
swarmId: swarmId || null,
|
|
268
|
+
statuses: statuses || null,
|
|
269
|
+
types: types || null,
|
|
270
|
+
claimedBy: claimedBy || null,
|
|
271
|
+
createdBy: createdBy || null,
|
|
272
|
+
minPriority: minPriorityScore,
|
|
273
|
+
orderBy,
|
|
274
|
+
limit: Math.floor(limit),
|
|
275
|
+
skip: Math.floor(skip),
|
|
276
|
+
});
|
|
277
|
+
const convertTimestamp = (ts) => typeof ts === 'object' && ts?.toNumber ? ts.toNumber() : ts;
|
|
278
|
+
const tasks = tasksResult.map((t) => {
|
|
279
|
+
// Parse metadata if present
|
|
280
|
+
let metadata = t.metadata;
|
|
281
|
+
if (metadata) {
|
|
282
|
+
try {
|
|
283
|
+
metadata = JSON.parse(metadata);
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
// Keep as string
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
id: t.id,
|
|
291
|
+
projectId: t.projectId,
|
|
292
|
+
swarmId: t.swarmId,
|
|
293
|
+
title: t.title,
|
|
294
|
+
description: t.description,
|
|
295
|
+
type: t.type,
|
|
296
|
+
priority: t.priority,
|
|
297
|
+
priorityScore: t.priorityScore,
|
|
298
|
+
status: t.status,
|
|
299
|
+
targetNodeIds: t.targetNodeIds,
|
|
300
|
+
targetFilePaths: t.targetFilePaths,
|
|
301
|
+
claimedBy: t.claimedBy,
|
|
302
|
+
claimedAt: convertTimestamp(t.claimedAt),
|
|
303
|
+
startedAt: convertTimestamp(t.startedAt),
|
|
304
|
+
completedAt: convertTimestamp(t.completedAt),
|
|
305
|
+
createdBy: t.createdBy,
|
|
306
|
+
createdAt: convertTimestamp(t.createdAt),
|
|
307
|
+
summary: t.summary,
|
|
308
|
+
metadata,
|
|
309
|
+
dependencies: t.dependencies?.filter((d) => d.id !== null) || [],
|
|
310
|
+
blockedTasks: t.blockedTasks?.filter((d) => d.id !== null) || [],
|
|
311
|
+
targets: t.targets || [],
|
|
312
|
+
};
|
|
313
|
+
});
|
|
314
|
+
const response = {
|
|
315
|
+
success: true,
|
|
316
|
+
tasks,
|
|
317
|
+
pagination: {
|
|
318
|
+
skip,
|
|
319
|
+
limit,
|
|
320
|
+
returned: tasks.length,
|
|
321
|
+
hasMore: tasks.length === limit,
|
|
322
|
+
},
|
|
323
|
+
filters: {
|
|
324
|
+
swarmId: swarmId || 'all',
|
|
325
|
+
statuses: statuses || 'all',
|
|
326
|
+
types: types || 'all',
|
|
327
|
+
claimedBy: claimedBy || 'any',
|
|
328
|
+
minPriority: minPriority || 'any',
|
|
329
|
+
},
|
|
330
|
+
};
|
|
331
|
+
// Include statistics if requested
|
|
332
|
+
if (includeStats) {
|
|
333
|
+
const statsResult = await neo4jService.run(GET_TASK_STATS_QUERY, {
|
|
334
|
+
projectId: resolvedProjectId,
|
|
335
|
+
swarmId: swarmId || null,
|
|
336
|
+
});
|
|
337
|
+
const stats = {
|
|
338
|
+
byStatus: {},
|
|
339
|
+
byType: {},
|
|
340
|
+
byPriority: {},
|
|
341
|
+
byAgent: {},
|
|
342
|
+
total: 0,
|
|
343
|
+
};
|
|
344
|
+
for (const row of statsResult) {
|
|
345
|
+
const count = typeof row.count === 'object' ? row.count.toNumber() : row.count;
|
|
346
|
+
if (row.status) {
|
|
347
|
+
stats.byStatus[row.status] = (stats.byStatus[row.status] || 0) + count;
|
|
348
|
+
}
|
|
349
|
+
if (row.type) {
|
|
350
|
+
stats.byType[row.type] = (stats.byType[row.type] || 0) + count;
|
|
351
|
+
}
|
|
352
|
+
if (row.priority) {
|
|
353
|
+
stats.byPriority[row.priority] = (stats.byPriority[row.priority] || 0) + count;
|
|
354
|
+
}
|
|
355
|
+
if (row.agent) {
|
|
356
|
+
stats.byAgent[row.agent] = (stats.byAgent[row.agent] || 0) + count;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
stats.total = Object.values(stats.byStatus).reduce((a, b) => a + b, 0);
|
|
360
|
+
// Calculate progress metrics
|
|
361
|
+
const completed = stats.byStatus['completed'] ?? 0;
|
|
362
|
+
const failed = stats.byStatus['failed'] ?? 0;
|
|
363
|
+
const inProgress = stats.byStatus['in_progress'] ?? 0;
|
|
364
|
+
const available = stats.byStatus['available'] ?? 0;
|
|
365
|
+
const blocked = stats.byStatus['blocked'] ?? 0;
|
|
366
|
+
const done = completed + failed;
|
|
367
|
+
response.stats = stats;
|
|
368
|
+
response.progress = {
|
|
369
|
+
completed,
|
|
370
|
+
failed,
|
|
371
|
+
inProgress,
|
|
372
|
+
available,
|
|
373
|
+
blocked,
|
|
374
|
+
total: stats.total,
|
|
375
|
+
percentComplete: stats.total > 0 ? Math.round((done / stats.total) * 100) : 0,
|
|
376
|
+
isComplete: done === stats.total && stats.total > 0,
|
|
377
|
+
summary: stats.total === 0
|
|
378
|
+
? 'No tasks'
|
|
379
|
+
: `${completed}/${stats.total} completed (${Math.round((done / stats.total) * 100)}%)`,
|
|
380
|
+
};
|
|
381
|
+
// Get active workers from pheromones
|
|
382
|
+
const workersResult = await neo4jService.run(GET_ACTIVE_WORKERS_QUERY, {
|
|
383
|
+
projectId: resolvedProjectId,
|
|
384
|
+
swarmId: swarmId || null,
|
|
385
|
+
});
|
|
386
|
+
response.activeWorkers = workersResult.map((w) => ({
|
|
387
|
+
agentId: w.agentId,
|
|
388
|
+
status: w.type === 'modifying' ? 'working' : 'claiming',
|
|
389
|
+
lastActivity: typeof w.lastActivity === 'object' ? w.lastActivity.toNumber() : w.lastActivity,
|
|
390
|
+
nodesBeingWorked: typeof w.nodeCount === 'object' ? w.nodeCount.toNumber() : w.nodeCount,
|
|
391
|
+
minutesSinceActivity: typeof w.minutesSinceActivity === 'object'
|
|
392
|
+
? w.minutesSinceActivity.toNumber()
|
|
393
|
+
: w.minutesSinceActivity,
|
|
394
|
+
}));
|
|
395
|
+
}
|
|
396
|
+
// Include dependency graph if requested
|
|
397
|
+
if (includeDependencyGraph) {
|
|
398
|
+
const graphResult = await neo4jService.run(GET_DEPENDENCY_GRAPH_QUERY, {
|
|
399
|
+
projectId: resolvedProjectId,
|
|
400
|
+
swarmId: swarmId || null,
|
|
401
|
+
});
|
|
402
|
+
if (graphResult.length > 0) {
|
|
403
|
+
response.dependencyGraph = {
|
|
404
|
+
nodes: graphResult[0].nodes || [],
|
|
405
|
+
edges: (graphResult[0].edges || []).filter((e) => e !== null),
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return createSuccessResponse(JSON.stringify(response, null, 2));
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
await debugLog('Swarm get tasks error', { error: String(error) });
|
|
413
|
+
return createErrorResponse(error instanceof Error ? error : String(error));
|
|
414
|
+
}
|
|
415
|
+
finally {
|
|
416
|
+
await neo4jService.close();
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
};
|