cursor-agent-a2a 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.
@@ -0,0 +1,722 @@
1
+ /**
2
+ * Cursor Agent A2A Routes
3
+ *
4
+ * Independent A2A-compatible service wrapping Cursor CLI agent command.
5
+ * This service is separate from AgentStudio's agent system.
6
+ *
7
+ * Endpoints:
8
+ * - GET / - Homepage with usage instructions and Agent Card display
9
+ * - GET /.well-known/agent-card.json - Agent Card discovery
10
+ * - POST /messages - Send synchronous message
11
+ * - POST /tasks - Create asynchronous task
12
+ * - GET /tasks/:taskId - Query task status
13
+ * - DELETE /tasks/:taskId - Cancel task
14
+ *
15
+ * Authentication: API key via Authorization header (Bearer token)
16
+ */
17
+ import express from 'express';
18
+ import { generateCursorAgentCard, executeCursorAgent, executeCursorAgentStream } from '../services/cursorAgentService.js';
19
+ import { v4 as uuidv4 } from 'uuid';
20
+ const router = express.Router({ mergeParams: true });
21
+ const tasks = new Map();
22
+ const sessions = new Map();
23
+ // Simple API key validation (for demo - in production use proper key management)
24
+ const VALID_API_KEY = process.env.CURSOR_AGENT_API_KEY || 'cursor-agent-demo-key';
25
+ /**
26
+ * Simple API key authentication middleware
27
+ */
28
+ function apiKeyAuth(req, res, next) {
29
+ const authHeader = req.headers.authorization;
30
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
31
+ res.status(401).json({
32
+ error: 'Missing or invalid Authorization header',
33
+ code: 'UNAUTHORIZED',
34
+ message: 'Include: Authorization: Bearer <api-key>',
35
+ });
36
+ return;
37
+ }
38
+ const apiKey = authHeader.substring(7);
39
+ if (apiKey !== VALID_API_KEY) {
40
+ res.status(401).json({
41
+ error: 'Invalid API key',
42
+ code: 'INVALID_API_KEY',
43
+ });
44
+ return;
45
+ }
46
+ next();
47
+ }
48
+ // ============================================================================
49
+ // GET / - Root endpoint with usage instructions and Agent Card display
50
+ // ============================================================================
51
+ router.get('/', async (req, res) => {
52
+ const baseUrl = `${req.protocol}://${req.get('host')}`;
53
+ const agentCardUrl = `${baseUrl}/.well-known/agent-card.json`;
54
+ // Fetch agent card for display
55
+ let agentCard = null;
56
+ try {
57
+ const agentCardResponse = await fetch(agentCardUrl);
58
+ if (agentCardResponse.ok) {
59
+ agentCard = await agentCardResponse.json();
60
+ }
61
+ }
62
+ catch (error) {
63
+ console.error('[CursorAgent] Failed to fetch agent card:', error);
64
+ }
65
+ const skillsHtml = agentCard?.skills?.map((skill) => `
66
+ <div class="skill-card">
67
+ <h4>${skill.name}</h4>
68
+ <p>${skill.description}</p>
69
+ </div>
70
+ `).join('') || '';
71
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
72
+ res.send(`
73
+ <!DOCTYPE html>
74
+ <html lang="en">
75
+ <head>
76
+ <meta charset="UTF-8">
77
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
78
+ <title>Cursor Agent A2A Service</title>
79
+ <style>
80
+ * { box-sizing: border-box; }
81
+ body {
82
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
83
+ max-width: 1200px;
84
+ margin: 0 auto;
85
+ padding: 40px 20px;
86
+ line-height: 1.6;
87
+ color: #1f2937;
88
+ background: #f9fafb;
89
+ }
90
+ .header {
91
+ background: linear-gradient(135deg, #2563eb 0%, #1e40af 100%);
92
+ color: white;
93
+ padding: 40px;
94
+ border-radius: 12px;
95
+ margin-bottom: 30px;
96
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
97
+ }
98
+ .header h1 { margin: 0 0 10px 0; font-size: 2.5em; }
99
+ .header p { margin: 0; opacity: 0.9; font-size: 1.1em; }
100
+ .card {
101
+ background: white;
102
+ border-radius: 8px;
103
+ padding: 24px;
104
+ margin-bottom: 24px;
105
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
106
+ }
107
+ .card h2 {
108
+ color: #1e40af;
109
+ margin-top: 0;
110
+ border-bottom: 2px solid #e5e7eb;
111
+ padding-bottom: 12px;
112
+ }
113
+ .agent-card-display {
114
+ background: #f3f4f6;
115
+ border-radius: 8px;
116
+ padding: 20px;
117
+ margin: 20px 0;
118
+ }
119
+ .agent-info {
120
+ display: grid;
121
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
122
+ gap: 16px;
123
+ margin: 16px 0;
124
+ }
125
+ .info-item {
126
+ background: white;
127
+ padding: 12px;
128
+ border-radius: 6px;
129
+ border-left: 3px solid #2563eb;
130
+ }
131
+ .info-item strong { display: block; color: #6b7280; font-size: 0.875em; margin-bottom: 4px; }
132
+ .info-item span { color: #1f2937; font-size: 1em; }
133
+ .skills-grid {
134
+ display: grid;
135
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
136
+ gap: 16px;
137
+ margin: 16px 0;
138
+ }
139
+ .skill-card {
140
+ background: white;
141
+ padding: 16px;
142
+ border-radius: 6px;
143
+ border: 1px solid #e5e7eb;
144
+ }
145
+ .skill-card h4 {
146
+ margin: 0 0 8px 0;
147
+ color: #2563eb;
148
+ font-size: 1.1em;
149
+ }
150
+ .skill-card p {
151
+ margin: 0;
152
+ color: #6b7280;
153
+ font-size: 0.9em;
154
+ }
155
+ .endpoint {
156
+ background: #f9fafb;
157
+ border-left: 4px solid #2563eb;
158
+ padding: 16px;
159
+ margin: 12px 0;
160
+ border-radius: 4px;
161
+ }
162
+ .endpoint-url {
163
+ background: #1f2937;
164
+ color: #10b981;
165
+ padding: 8px 12px;
166
+ border-radius: 4px;
167
+ font-family: 'Monaco', 'Courier New', monospace;
168
+ font-size: 0.9em;
169
+ margin: 8px 0;
170
+ word-break: break-all;
171
+ }
172
+ .auth-note {
173
+ background: #fef3c7;
174
+ border-left: 4px solid #f59e0b;
175
+ padding: 12px 16px;
176
+ margin: 16px 0;
177
+ border-radius: 4px;
178
+ }
179
+ code {
180
+ background: #f3f4f6;
181
+ padding: 2px 6px;
182
+ border-radius: 4px;
183
+ font-family: 'Monaco', 'Courier New', monospace;
184
+ font-size: 0.9em;
185
+ }
186
+ pre {
187
+ background: #1f2937;
188
+ color: #f9fafb;
189
+ padding: 16px;
190
+ border-radius: 8px;
191
+ overflow-x: auto;
192
+ font-size: 0.9em;
193
+ }
194
+ a {
195
+ color: #2563eb;
196
+ text-decoration: none;
197
+ }
198
+ a:hover {
199
+ text-decoration: underline;
200
+ }
201
+ .copy-btn {
202
+ background: #2563eb;
203
+ color: white;
204
+ border: none;
205
+ padding: 6px 12px;
206
+ border-radius: 4px;
207
+ cursor: pointer;
208
+ font-size: 0.85em;
209
+ margin-left: 8px;
210
+ }
211
+ .copy-btn:hover {
212
+ background: #1e40af;
213
+ }
214
+ </style>
215
+ </head>
216
+ <body>
217
+ <div class="header">
218
+ <h1>🚀 Cursor Agent A2A Service</h1>
219
+ <p>Independent A2A-compatible service wrapping Cursor CLI agent command</p>
220
+ </div>
221
+
222
+ <div class="card">
223
+ <h2>📋 Agent Card</h2>
224
+ ${agentCard ? `
225
+ <div class="agent-card-display">
226
+ <div class="agent-info">
227
+ <div class="info-item">
228
+ <strong>Name</strong>
229
+ <span>${agentCard.name}</span>
230
+ </div>
231
+ <div class="info-item">
232
+ <strong>Version</strong>
233
+ <span>${agentCard.version}</span>
234
+ </div>
235
+ <div class="info-item">
236
+ <strong>Agent ID</strong>
237
+ <span>${agentCard.context?.a2aAgentId || 'N/A'}</span>
238
+ </div>
239
+ </div>
240
+ <p><strong>Description:</strong> ${agentCard.description}</p>
241
+ <h3 style="margin-top: 24px; margin-bottom: 12px;">Skills</h3>
242
+ <div class="skills-grid">
243
+ ${skillsHtml}
244
+ </div>
245
+ <p style="margin-top: 16px;">
246
+ <a href="${agentCardUrl}" target="_blank">View full Agent Card JSON →</a>
247
+ </p>
248
+ </div>
249
+ ` : '<p>Loading agent card...</p>'}
250
+ </div>
251
+
252
+ <div class="card">
253
+ <h2>🔑 API Key Configuration</h2>
254
+ <div class="auth-note">
255
+ <strong>Note:</strong> All API endpoints (except Agent Card and homepage) require authentication via API key.
256
+ </div>
257
+ <p>Set your API key via environment variable:</p>
258
+ <pre>export CURSOR_AGENT_API_KEY=your-secure-api-key</pre>
259
+ <p>Default API key (for development): <code>cursor-agent-demo-key</code></p>
260
+ </div>
261
+
262
+ <div class="card">
263
+ <h2>📡 API Endpoints</h2>
264
+
265
+ <h3>1. Send Message (Synchronous)</h3>
266
+ <div class="endpoint">
267
+ <strong>POST</strong> <code>${baseUrl}/messages</code>
268
+ <div class="endpoint-url">curl -X POST "${baseUrl}/messages" \\
269
+ -H "Authorization: Bearer cursor-agent-demo-key" \\
270
+ -H "Content-Type: application/json" \\
271
+ -d '{"message": "Say hello", "workspace": "/path/to/project"}'</div>
272
+ <p><strong>Request Body:</strong></p>
273
+ <pre>{
274
+ "message": "Say hello",
275
+ "workspace": "/path/to/project",
276
+ "sessionId": "optional-session-id"
277
+ }</pre>
278
+ <p><strong>Response:</strong></p>
279
+ <pre>{
280
+ "response": "Hello! I'm Claude...",
281
+ "sessionId": "chat-12345",
282
+ "metadata": {
283
+ "processingTimeMs": 1234
284
+ }
285
+ }</pre>
286
+ </div>
287
+
288
+ <h3>2. Send Message (Streaming)</h3>
289
+ <div class="endpoint">
290
+ <strong>POST</strong> <code>${baseUrl}/messages?stream=true</code>
291
+ <div class="endpoint-url">curl -X POST "${baseUrl}/messages?stream=true" \\
292
+ -H "Authorization: Bearer cursor-agent-demo-key" \\
293
+ -H "Accept: text/event-stream" \\
294
+ -H "Content-Type: application/json" \\
295
+ -d '{"message": "Say hello", "workspace": "/path/to/project"}'</div>
296
+ <p>Returns Server-Sent Events (SSE) stream.</p>
297
+ </div>
298
+
299
+ <h3>3. Create Async Task</h3>
300
+ <div class="endpoint">
301
+ <strong>POST</strong> <code>${baseUrl}/tasks</code>
302
+ <div class="endpoint-url">curl -X POST "${baseUrl}/tasks" \\
303
+ -H "Authorization: Bearer cursor-agent-demo-key" \\
304
+ -H "Content-Type: application/json" \\
305
+ -d '{
306
+ "message": "Long running task",
307
+ "workspace": "/path/to/project",
308
+ "timeout": 600000,
309
+ "pushNotificationConfig": {
310
+ "url": "https://your-webhook-url.com/callback",
311
+ "token": "optional-verification-token"
312
+ }
313
+ }'</div>
314
+ <p><strong>Request Body:</strong></p>
315
+ <pre>{
316
+ "message": "Long running task",
317
+ "workspace": "/path/to/project",
318
+ "timeout": 600000,
319
+ "pushNotificationConfig": {
320
+ "url": "https://your-webhook-url.com/callback",
321
+ "token": "optional-verification-token"
322
+ }
323
+ }</pre>
324
+ <p><strong>Response (202 Accepted):</strong></p>
325
+ <pre>{
326
+ "taskId": "task-uuid",
327
+ "status": "pending",
328
+ "checkUrl": "/tasks/task-uuid"
329
+ }</pre>
330
+ <p><strong>Note:</strong> Task completion will POST to the webhook URL (if provided) with task status and result.</p>
331
+ </div>
332
+
333
+ <h3>4. Query Task Status</h3>
334
+ <div class="endpoint">
335
+ <strong>GET</strong> <code>${baseUrl}/tasks/:taskId</code>
336
+ <div class="endpoint-url">curl -X GET "${baseUrl}/tasks/task-uuid" \\
337
+ -H "Authorization: Bearer cursor-agent-demo-key"</div>
338
+ <p><strong>Response:</strong></p>
339
+ <pre>{
340
+ "taskId": "task-uuid",
341
+ "status": "completed",
342
+ "output": {
343
+ "result": "Task completed successfully"
344
+ },
345
+ "createdAt": "2026-01-24T...",
346
+ "completedAt": "2026-01-24T..."
347
+ }</pre>
348
+ <p><strong>Status values:</strong> <code>pending</code>, <code>running</code>, <code>completed</code>, <code>failed</code>, <code>canceled</p>
349
+ </div>
350
+
351
+ <h3>5. Cancel Task</h3>
352
+ <div class="endpoint">
353
+ <strong>DELETE</strong> <code>${baseUrl}/tasks/:taskId</code>
354
+ <div class="endpoint-url">curl -X DELETE "${baseUrl}/tasks/task-uuid" \\
355
+ -H "Authorization: Bearer cursor-agent-demo-key"</div>
356
+ </div>
357
+ </div>
358
+
359
+ <div class="card">
360
+ <h2>📚 Documentation</h2>
361
+ <p>Full documentation available at: <a href="https://github.com/jeffkit/cursor-agent-a2a" target="_blank">GitHub Repository</a></p>
362
+ <p>Service status: <a href="/health">/health</a></p>
363
+ </div>
364
+ </body>
365
+ </html>
366
+ `);
367
+ });
368
+ // ============================================================================
369
+ // GET /.well-known/agent-card.json - Agent Card Discovery
370
+ // ============================================================================
371
+ router.get('/.well-known/agent-card.json', (req, res) => {
372
+ try {
373
+ const baseUrl = `${req.protocol}://${req.get('host')}`;
374
+ const agentCard = generateCursorAgentCard(baseUrl);
375
+ res.json(agentCard);
376
+ }
377
+ catch (error) {
378
+ console.error('[CursorAgent] Error generating agent card:', error);
379
+ res.status(500).json({
380
+ error: 'Failed to generate agent card',
381
+ code: 'AGENT_CARD_ERROR',
382
+ });
383
+ }
384
+ });
385
+ // ============================================================================
386
+ // POST /messages - Synchronous Message (Protected)
387
+ // ============================================================================
388
+ router.post('/messages', apiKeyAuth, async (req, res) => {
389
+ try {
390
+ const { message, workspace, sessionId } = req.body;
391
+ const stream = req.query.stream === 'true' || req.headers.accept === 'text/event-stream';
392
+ if (!message) {
393
+ return res.status(400).json({
394
+ error: 'Missing message field',
395
+ code: 'VALIDATION_ERROR',
396
+ });
397
+ }
398
+ // Determine workspace: use provided one, or get from session, or use current directory
399
+ let finalWorkspace = workspace;
400
+ let finalSessionId = sessionId;
401
+ if (sessionId) {
402
+ // Try to get workspace from existing session
403
+ const session = sessions.get(sessionId);
404
+ if (session) {
405
+ finalWorkspace = session.workspace;
406
+ session.lastAccessedAt = new Date().toISOString();
407
+ }
408
+ else if (!workspace) {
409
+ // Session not found and no workspace provided
410
+ return res.status(400).json({
411
+ error: 'Session not found and workspace not provided',
412
+ code: 'SESSION_NOT_FOUND',
413
+ message: 'Either provide a valid sessionId with existing session, or provide workspace for new session',
414
+ });
415
+ }
416
+ }
417
+ else if (!workspace) {
418
+ // No session and no workspace - use current directory
419
+ finalWorkspace = process.cwd();
420
+ }
421
+ console.info('[CursorAgent] Message received:', {
422
+ messageLength: message.length,
423
+ workspace: finalWorkspace,
424
+ sessionId: finalSessionId,
425
+ stream,
426
+ });
427
+ if (stream) {
428
+ // Streaming Mode (SSE)
429
+ res.setHeader('Content-Type', 'text/event-stream');
430
+ res.setHeader('Cache-Control', 'no-cache');
431
+ res.setHeader('Connection', 'keep-alive');
432
+ res.flushHeaders();
433
+ let capturedSessionId = sessionId;
434
+ try {
435
+ await executeCursorAgentStream(message, { workspace: finalWorkspace, sessionId: finalSessionId }, (event) => {
436
+ // Capture session ID from events
437
+ if (event.sessionId && !capturedSessionId) {
438
+ capturedSessionId = event.sessionId;
439
+ // Store session with workspace
440
+ if (capturedSessionId && finalWorkspace) {
441
+ sessions.set(capturedSessionId, {
442
+ sessionId: capturedSessionId,
443
+ workspace: finalWorkspace,
444
+ createdAt: new Date().toISOString(),
445
+ lastAccessedAt: new Date().toISOString(),
446
+ });
447
+ }
448
+ }
449
+ const eventData = {
450
+ ...event,
451
+ sessionId: capturedSessionId || finalSessionId,
452
+ };
453
+ res.write(`data: ${JSON.stringify(eventData)}\n\n`);
454
+ });
455
+ // Ensure session is stored even if no sessionId was captured from events
456
+ if (!capturedSessionId && finalSessionId && finalWorkspace) {
457
+ sessions.set(finalSessionId, {
458
+ sessionId: finalSessionId,
459
+ workspace: finalWorkspace,
460
+ createdAt: new Date().toISOString(),
461
+ lastAccessedAt: new Date().toISOString(),
462
+ });
463
+ }
464
+ res.write(`data: ${JSON.stringify({ type: 'done', sessionId: capturedSessionId || finalSessionId })}\n\n`);
465
+ res.end();
466
+ }
467
+ catch (error) {
468
+ console.error('[CursorAgent] Error in streaming:', error);
469
+ const errorEvent = {
470
+ type: 'error',
471
+ error: error instanceof Error ? error.message : String(error),
472
+ sessionId: capturedSessionId || sessionId,
473
+ };
474
+ res.write(`data: ${JSON.stringify(errorEvent)}\n\n`);
475
+ res.end();
476
+ }
477
+ }
478
+ else {
479
+ // Synchronous Mode
480
+ const startTime = Date.now();
481
+ const result = await executeCursorAgent(message, { workspace: finalWorkspace, sessionId: finalSessionId });
482
+ const processingTimeMs = Date.now() - startTime;
483
+ if (!result.success) {
484
+ return res.status(500).json({
485
+ error: result.error || 'Failed to execute cursor agent',
486
+ code: 'EXECUTION_ERROR',
487
+ });
488
+ }
489
+ // Use returned sessionId or keep the provided one for continuation
490
+ const returnedSessionId = result.sessionId || finalSessionId;
491
+ // Store session with workspace
492
+ if (returnedSessionId && finalWorkspace) {
493
+ sessions.set(returnedSessionId, {
494
+ sessionId: returnedSessionId,
495
+ workspace: finalWorkspace,
496
+ createdAt: new Date().toISOString(),
497
+ lastAccessedAt: new Date().toISOString(),
498
+ });
499
+ }
500
+ console.info('[CursorAgent] Message processed successfully:', {
501
+ processingTimeMs,
502
+ responseLength: result.response?.length || 0,
503
+ sessionId: returnedSessionId,
504
+ });
505
+ res.json({
506
+ response: result.response || 'No response generated',
507
+ sessionId: returnedSessionId,
508
+ metadata: {
509
+ processingTimeMs,
510
+ ...result.metadata,
511
+ },
512
+ });
513
+ }
514
+ }
515
+ catch (error) {
516
+ console.error('[CursorAgent] Error processing message:', error);
517
+ if (!res.headersSent) {
518
+ res.status(500).json({
519
+ error: 'Failed to process message',
520
+ code: 'MESSAGE_PROCESSING_ERROR',
521
+ details: error instanceof Error ? error.message : String(error),
522
+ });
523
+ }
524
+ }
525
+ });
526
+ // ============================================================================
527
+ // POST /tasks - Create Asynchronous Task (Protected)
528
+ // ============================================================================
529
+ router.post('/tasks', apiKeyAuth, async (req, res) => {
530
+ try {
531
+ const { message, workspace, timeout, sessionId, pushNotificationConfig } = req.body;
532
+ if (!message) {
533
+ return res.status(400).json({
534
+ error: 'Missing message field',
535
+ code: 'VALIDATION_ERROR',
536
+ });
537
+ }
538
+ const taskId = uuidv4();
539
+ const now = new Date().toISOString();
540
+ // Create task
541
+ const task = {
542
+ id: taskId,
543
+ status: 'pending',
544
+ message,
545
+ workspace,
546
+ createdAt: now,
547
+ updatedAt: now,
548
+ pushNotificationConfig,
549
+ };
550
+ tasks.set(taskId, task);
551
+ console.info('[CursorAgent] Task created:', {
552
+ taskId,
553
+ workspace,
554
+ timeout,
555
+ hasWebhook: !!pushNotificationConfig?.url,
556
+ });
557
+ // Execute task asynchronously
558
+ setImmediate(async () => {
559
+ try {
560
+ task.status = 'running';
561
+ task.startedAt = new Date().toISOString();
562
+ task.updatedAt = new Date().toISOString();
563
+ const result = await executeCursorAgent(message, {
564
+ workspace,
565
+ timeout: timeout || 600000, // 10 minutes default
566
+ sessionId,
567
+ });
568
+ task.status = result.success ? 'completed' : 'failed';
569
+ task.response = result.response;
570
+ task.error = result.error;
571
+ task.completedAt = new Date().toISOString();
572
+ task.updatedAt = new Date().toISOString();
573
+ // Send webhook notification if configured
574
+ if (pushNotificationConfig?.url && (task.status === 'completed' || task.status === 'failed')) {
575
+ try {
576
+ const webhookPayload = {
577
+ taskId: task.id,
578
+ status: task.status,
579
+ output: task.status === 'completed' ? { result: task.response } : undefined,
580
+ errorDetails: task.status === 'failed' ? {
581
+ message: task.error,
582
+ code: 'EXECUTION_ERROR',
583
+ } : undefined,
584
+ createdAt: task.createdAt,
585
+ completedAt: task.completedAt,
586
+ };
587
+ const headers = {
588
+ 'Content-Type': 'application/json',
589
+ };
590
+ if (pushNotificationConfig.token) {
591
+ headers['Authorization'] = `Bearer ${pushNotificationConfig.token}`;
592
+ }
593
+ await fetch(pushNotificationConfig.url, {
594
+ method: 'POST',
595
+ headers,
596
+ body: JSON.stringify(webhookPayload),
597
+ });
598
+ console.info('[CursorAgent] Webhook notification sent:', {
599
+ taskId: task.id,
600
+ url: pushNotificationConfig.url,
601
+ });
602
+ }
603
+ catch (webhookError) {
604
+ console.error('[CursorAgent] Failed to send webhook notification:', webhookError);
605
+ }
606
+ }
607
+ }
608
+ catch (error) {
609
+ task.status = 'failed';
610
+ task.error = error instanceof Error ? error.message : String(error);
611
+ task.completedAt = new Date().toISOString();
612
+ task.updatedAt = new Date().toISOString();
613
+ }
614
+ });
615
+ // Return task ID immediately
616
+ res.status(202).json({
617
+ taskId,
618
+ status: task.status,
619
+ checkUrl: `${req.protocol}://${req.get('host')}/tasks/${taskId}`,
620
+ });
621
+ }
622
+ catch (error) {
623
+ console.error('[CursorAgent] Error creating task:', error);
624
+ res.status(500).json({
625
+ error: 'Failed to create task',
626
+ code: 'TASK_CREATION_ERROR',
627
+ details: error instanceof Error ? error.message : String(error),
628
+ });
629
+ }
630
+ });
631
+ // ============================================================================
632
+ // GET /tasks/:taskId - Query Task Status (Protected)
633
+ // ============================================================================
634
+ router.get('/tasks/:taskId', apiKeyAuth, (req, res) => {
635
+ try {
636
+ const { taskId } = req.params;
637
+ const task = tasks.get(taskId);
638
+ if (!task) {
639
+ return res.status(404).json({
640
+ error: `Task not found: ${taskId}`,
641
+ code: 'TASK_NOT_FOUND',
642
+ });
643
+ }
644
+ const response = {
645
+ taskId: task.id,
646
+ status: task.status,
647
+ createdAt: task.createdAt,
648
+ updatedAt: task.updatedAt,
649
+ };
650
+ if (task.startedAt) {
651
+ response.startedAt = task.startedAt;
652
+ }
653
+ if (task.completedAt) {
654
+ response.completedAt = task.completedAt;
655
+ }
656
+ if (task.response) {
657
+ response.output = {
658
+ result: task.response,
659
+ };
660
+ }
661
+ if (task.error) {
662
+ response.errorDetails = {
663
+ message: task.error,
664
+ code: 'EXECUTION_ERROR',
665
+ };
666
+ }
667
+ if (task.status === 'running') {
668
+ response.progress = {
669
+ currentStep: 'Processing',
670
+ percentComplete: 50,
671
+ };
672
+ }
673
+ res.json(response);
674
+ }
675
+ catch (error) {
676
+ console.error('[CursorAgent] Error querying task status:', error);
677
+ res.status(500).json({
678
+ error: 'Failed to query task status',
679
+ code: 'TASK_STATUS_ERROR',
680
+ details: error instanceof Error ? error.message : String(error),
681
+ });
682
+ }
683
+ });
684
+ // ============================================================================
685
+ // DELETE /tasks/:taskId - Cancel Task (Protected)
686
+ // ============================================================================
687
+ router.delete('/tasks/:taskId', apiKeyAuth, (req, res) => {
688
+ try {
689
+ const { taskId } = req.params;
690
+ const task = tasks.get(taskId);
691
+ if (!task) {
692
+ return res.status(404).json({
693
+ error: `Task not found: ${taskId}`,
694
+ code: 'TASK_NOT_FOUND',
695
+ });
696
+ }
697
+ if (task.status === 'completed' || task.status === 'failed') {
698
+ return res.status(400).json({
699
+ error: `Cannot cancel task in ${task.status} state`,
700
+ code: 'TASK_CANNOT_BE_CANCELED',
701
+ });
702
+ }
703
+ task.status = 'canceled';
704
+ task.updatedAt = new Date().toISOString();
705
+ task.completedAt = new Date().toISOString();
706
+ res.json({
707
+ taskId: task.id,
708
+ status: task.status,
709
+ message: 'Task canceled successfully',
710
+ });
711
+ }
712
+ catch (error) {
713
+ console.error('[CursorAgent] Error canceling task:', error);
714
+ res.status(500).json({
715
+ error: 'Failed to cancel task',
716
+ code: 'TASK_CANCELLATION_ERROR',
717
+ details: error instanceof Error ? error.message : String(error),
718
+ });
719
+ }
720
+ });
721
+ export default router;
722
+ //# sourceMappingURL=cursorAgent.js.map