mcp-twin 1.2.0 → 1.4.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 (46) hide show
  1. package/.env.example +30 -0
  2. package/PRD.md +682 -0
  3. package/dist/cli.js +41 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/cloud/auth.d.ts +108 -0
  6. package/dist/cloud/auth.d.ts.map +1 -0
  7. package/dist/cloud/auth.js +199 -0
  8. package/dist/cloud/auth.js.map +1 -0
  9. package/dist/cloud/db.d.ts +21 -0
  10. package/dist/cloud/db.d.ts.map +1 -0
  11. package/dist/cloud/db.js +158 -0
  12. package/dist/cloud/db.js.map +1 -0
  13. package/dist/cloud/routes/auth.d.ts +7 -0
  14. package/dist/cloud/routes/auth.d.ts.map +1 -0
  15. package/dist/cloud/routes/auth.js +291 -0
  16. package/dist/cloud/routes/auth.js.map +1 -0
  17. package/dist/cloud/routes/billing.d.ts +7 -0
  18. package/dist/cloud/routes/billing.d.ts.map +1 -0
  19. package/dist/cloud/routes/billing.js +368 -0
  20. package/dist/cloud/routes/billing.js.map +1 -0
  21. package/dist/cloud/routes/twins.d.ts +7 -0
  22. package/dist/cloud/routes/twins.d.ts.map +1 -0
  23. package/dist/cloud/routes/twins.js +740 -0
  24. package/dist/cloud/routes/twins.js.map +1 -0
  25. package/dist/cloud/routes/usage.d.ts +7 -0
  26. package/dist/cloud/routes/usage.d.ts.map +1 -0
  27. package/dist/cloud/routes/usage.js +145 -0
  28. package/dist/cloud/routes/usage.js.map +1 -0
  29. package/dist/cloud/server.d.ts +10 -0
  30. package/dist/cloud/server.d.ts.map +1 -0
  31. package/dist/cloud/server.js +161 -0
  32. package/dist/cloud/server.js.map +1 -0
  33. package/dist/cloud/stripe.d.ts +60 -0
  34. package/dist/cloud/stripe.d.ts.map +1 -0
  35. package/dist/cloud/stripe.js +157 -0
  36. package/dist/cloud/stripe.js.map +1 -0
  37. package/package.json +25 -4
  38. package/src/cli.ts +10 -0
  39. package/src/cloud/auth.ts +269 -0
  40. package/src/cloud/db.ts +167 -0
  41. package/src/cloud/routes/auth.ts +355 -0
  42. package/src/cloud/routes/billing.ts +460 -0
  43. package/src/cloud/routes/twins.ts +908 -0
  44. package/src/cloud/routes/usage.ts +186 -0
  45. package/src/cloud/server.ts +171 -0
  46. package/src/cloud/stripe.ts +192 -0
@@ -0,0 +1,740 @@
1
+ "use strict";
2
+ /**
3
+ * Twins Routes
4
+ * MCP Twin Cloud
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const express_1 = require("express");
8
+ const db_1 = require("../db");
9
+ const auth_1 = require("../auth");
10
+ const twin_manager_1 = require("../../twin-manager");
11
+ const router = (0, express_1.Router)();
12
+ /**
13
+ * Middleware: Check quota
14
+ */
15
+ async function checkQuota(req, res, next) {
16
+ try {
17
+ const user = req.user;
18
+ const limits = (0, auth_1.getTierLimits)(user.tier);
19
+ // Get current month usage
20
+ const startOfMonth = new Date();
21
+ startOfMonth.setDate(1);
22
+ startOfMonth.setHours(0, 0, 0, 0);
23
+ const usage = await (0, db_1.queryOne)(`SELECT COALESCE(SUM(request_count), 0) as total
24
+ FROM usage_daily
25
+ WHERE user_id = $1 AND date >= $2`, [user.id, startOfMonth.toISOString().split('T')[0]]);
26
+ const usedRequests = parseInt(usage?.total || '0');
27
+ if (limits.requestsPerMonth !== -1 && usedRequests >= limits.requestsPerMonth) {
28
+ res.status(429).json({
29
+ error: {
30
+ code: 'QUOTA_EXCEEDED',
31
+ message: `Monthly request quota exceeded (${usedRequests}/${limits.requestsPerMonth}). Upgrade to Pro for more requests.`,
32
+ usage: {
33
+ used: usedRequests,
34
+ limit: limits.requestsPerMonth,
35
+ },
36
+ },
37
+ });
38
+ return;
39
+ }
40
+ next();
41
+ }
42
+ catch (error) {
43
+ console.error('[Twins] Quota check error:', error);
44
+ next(); // Don't block on quota check errors
45
+ }
46
+ }
47
+ /**
48
+ * POST /api/twins
49
+ * Create a new twin
50
+ */
51
+ router.post('/', auth_1.authenticateApiKey, async (req, res) => {
52
+ try {
53
+ const { name, config } = req.body;
54
+ const user = req.user;
55
+ // Validate input
56
+ if (!name || !config) {
57
+ res.status(400).json({
58
+ error: {
59
+ code: 'VALIDATION_ERROR',
60
+ message: 'Name and config are required',
61
+ },
62
+ });
63
+ return;
64
+ }
65
+ // Check twin limit
66
+ const limits = (0, auth_1.getTierLimits)(user.tier);
67
+ const twinCount = await (0, db_1.queryOne)('SELECT COUNT(*) as count FROM twins WHERE user_id = $1', [user.id]);
68
+ const currentCount = parseInt(twinCount?.count || '0');
69
+ if (limits.twins !== -1 && currentCount >= limits.twins) {
70
+ res.status(403).json({
71
+ error: {
72
+ code: 'TWIN_LIMIT_REACHED',
73
+ message: `Twin limit reached (${currentCount}/${limits.twins}). Upgrade to create more twins.`,
74
+ },
75
+ });
76
+ return;
77
+ }
78
+ // Assign ports (simple strategy: use twin count * 2 + base)
79
+ const basePort = 8100 + (currentCount * 2);
80
+ const portA = basePort;
81
+ const portB = basePort + 1;
82
+ // Create twin
83
+ const twins = await (0, db_1.query)(`INSERT INTO twins (user_id, name, config_json, port_a, port_b)
84
+ VALUES ($1, $2, $3, $4, $5)
85
+ RETURNING *`, [user.id, name, JSON.stringify(config), portA, portB]);
86
+ const twin = twins[0];
87
+ res.status(201).json({
88
+ message: 'Twin created',
89
+ twin: {
90
+ id: twin.id,
91
+ name: twin.name,
92
+ config: twin.config_json,
93
+ status: twin.status,
94
+ activeServer: twin.active_server,
95
+ ports: {
96
+ a: twin.port_a,
97
+ b: twin.port_b,
98
+ },
99
+ createdAt: twin.created_at,
100
+ },
101
+ });
102
+ }
103
+ catch (error) {
104
+ console.error('[Twins] Create error:', error);
105
+ res.status(500).json({
106
+ error: {
107
+ code: 'INTERNAL_ERROR',
108
+ message: 'Failed to create twin',
109
+ },
110
+ });
111
+ }
112
+ });
113
+ /**
114
+ * GET /api/twins
115
+ * List user's twins
116
+ */
117
+ router.get('/', auth_1.authenticateApiKey, async (req, res) => {
118
+ try {
119
+ const twins = await (0, db_1.query)(`SELECT * FROM twins WHERE user_id = $1 ORDER BY created_at DESC`, [req.user.id]);
120
+ res.json({
121
+ twins: twins.map((t) => ({
122
+ id: t.id,
123
+ name: t.name,
124
+ config: t.config_json,
125
+ status: t.status,
126
+ activeServer: t.active_server,
127
+ ports: {
128
+ a: t.port_a,
129
+ b: t.port_b,
130
+ },
131
+ createdAt: t.created_at,
132
+ updatedAt: t.updated_at,
133
+ })),
134
+ });
135
+ }
136
+ catch (error) {
137
+ console.error('[Twins] List error:', error);
138
+ res.status(500).json({
139
+ error: {
140
+ code: 'INTERNAL_ERROR',
141
+ message: 'Failed to list twins',
142
+ },
143
+ });
144
+ }
145
+ });
146
+ /**
147
+ * GET /api/twins/:id
148
+ * Get twin details
149
+ */
150
+ router.get('/:id', auth_1.authenticateApiKey, async (req, res) => {
151
+ try {
152
+ const twin = await (0, db_1.queryOne)('SELECT * FROM twins WHERE id = $1 AND user_id = $2', [req.params.id, req.user.id]);
153
+ if (!twin) {
154
+ res.status(404).json({
155
+ error: {
156
+ code: 'NOT_FOUND',
157
+ message: 'Twin not found',
158
+ },
159
+ });
160
+ return;
161
+ }
162
+ // Get recent logs count
163
+ const logCount = await (0, db_1.queryOne)('SELECT COUNT(*) as count FROM logs WHERE twin_id = $1', [twin.id]);
164
+ res.json({
165
+ twin: {
166
+ id: twin.id,
167
+ name: twin.name,
168
+ config: twin.config_json,
169
+ status: twin.status,
170
+ activeServer: twin.active_server,
171
+ ports: {
172
+ a: twin.port_a,
173
+ b: twin.port_b,
174
+ },
175
+ pids: {
176
+ a: twin.pid_a,
177
+ b: twin.pid_b,
178
+ },
179
+ createdAt: twin.created_at,
180
+ updatedAt: twin.updated_at,
181
+ },
182
+ stats: {
183
+ totalCalls: parseInt(logCount?.count || '0'),
184
+ },
185
+ });
186
+ }
187
+ catch (error) {
188
+ console.error('[Twins] Get error:', error);
189
+ res.status(500).json({
190
+ error: {
191
+ code: 'INTERNAL_ERROR',
192
+ message: 'Failed to get twin',
193
+ },
194
+ });
195
+ }
196
+ });
197
+ /**
198
+ * PATCH /api/twins/:id
199
+ * Update twin config
200
+ */
201
+ router.patch('/:id', auth_1.authenticateApiKey, async (req, res) => {
202
+ try {
203
+ const { name, config } = req.body;
204
+ const updates = [];
205
+ const values = [];
206
+ let paramIndex = 1;
207
+ if (name !== undefined) {
208
+ updates.push(`name = $${paramIndex++}`);
209
+ values.push(name);
210
+ }
211
+ if (config !== undefined) {
212
+ updates.push(`config_json = $${paramIndex++}`);
213
+ values.push(JSON.stringify(config));
214
+ }
215
+ if (updates.length === 0) {
216
+ res.status(400).json({
217
+ error: {
218
+ code: 'VALIDATION_ERROR',
219
+ message: 'No fields to update',
220
+ },
221
+ });
222
+ return;
223
+ }
224
+ updates.push(`updated_at = NOW()`);
225
+ values.push(req.params.id, req.user.id);
226
+ const twins = await (0, db_1.query)(`UPDATE twins
227
+ SET ${updates.join(', ')}
228
+ WHERE id = $${paramIndex++} AND user_id = $${paramIndex}
229
+ RETURNING *`, values);
230
+ if (twins.length === 0) {
231
+ res.status(404).json({
232
+ error: {
233
+ code: 'NOT_FOUND',
234
+ message: 'Twin not found',
235
+ },
236
+ });
237
+ return;
238
+ }
239
+ const twin = twins[0];
240
+ res.json({
241
+ message: 'Twin updated',
242
+ twin: {
243
+ id: twin.id,
244
+ name: twin.name,
245
+ config: twin.config_json,
246
+ status: twin.status,
247
+ updatedAt: twin.updated_at,
248
+ },
249
+ });
250
+ }
251
+ catch (error) {
252
+ console.error('[Twins] Update error:', error);
253
+ res.status(500).json({
254
+ error: {
255
+ code: 'INTERNAL_ERROR',
256
+ message: 'Failed to update twin',
257
+ },
258
+ });
259
+ }
260
+ });
261
+ /**
262
+ * DELETE /api/twins/:id
263
+ * Delete a twin
264
+ */
265
+ router.delete('/:id', auth_1.authenticateApiKey, async (req, res) => {
266
+ try {
267
+ // Stop twin if running
268
+ const twin = await (0, db_1.queryOne)('SELECT * FROM twins WHERE id = $1 AND user_id = $2', [req.params.id, req.user.id]);
269
+ if (!twin) {
270
+ res.status(404).json({
271
+ error: {
272
+ code: 'NOT_FOUND',
273
+ message: 'Twin not found',
274
+ },
275
+ });
276
+ return;
277
+ }
278
+ // Delete (cascade will remove logs)
279
+ await (0, db_1.query)('DELETE FROM twins WHERE id = $1', [twin.id]);
280
+ res.json({
281
+ message: 'Twin deleted',
282
+ });
283
+ }
284
+ catch (error) {
285
+ console.error('[Twins] Delete error:', error);
286
+ res.status(500).json({
287
+ error: {
288
+ code: 'INTERNAL_ERROR',
289
+ message: 'Failed to delete twin',
290
+ },
291
+ });
292
+ }
293
+ });
294
+ /**
295
+ * POST /api/twins/:id/start
296
+ * Start a twin
297
+ */
298
+ router.post('/:id/start', auth_1.authenticateApiKey, async (req, res) => {
299
+ try {
300
+ const twin = await (0, db_1.queryOne)('SELECT * FROM twins WHERE id = $1 AND user_id = $2', [req.params.id, req.user.id]);
301
+ if (!twin) {
302
+ res.status(404).json({
303
+ error: {
304
+ code: 'NOT_FOUND',
305
+ message: 'Twin not found',
306
+ },
307
+ });
308
+ return;
309
+ }
310
+ if (twin.status === 'running') {
311
+ res.status(400).json({
312
+ error: {
313
+ code: 'ALREADY_RUNNING',
314
+ message: 'Twin is already running',
315
+ },
316
+ });
317
+ return;
318
+ }
319
+ // Start using twin manager
320
+ const manager = (0, twin_manager_1.getTwinManager)();
321
+ const result = await manager.startTwins(twin.name);
322
+ if (!result.ok) {
323
+ res.status(500).json({
324
+ error: {
325
+ code: 'START_FAILED',
326
+ message: result.error || 'Failed to start twin',
327
+ },
328
+ });
329
+ return;
330
+ }
331
+ // Update twin status
332
+ await (0, db_1.query)(`UPDATE twins SET status = 'running', pid_a = $1, pid_b = $2, updated_at = NOW()
333
+ WHERE id = $3`, [result.pidA, result.pidB, twin.id]);
334
+ res.json({
335
+ message: 'Twin started',
336
+ status: 'running',
337
+ servers: {
338
+ a: { port: twin.port_a, pid: result.pidA, status: result.statusA },
339
+ b: { port: twin.port_b, pid: result.pidB, status: result.statusB },
340
+ },
341
+ });
342
+ }
343
+ catch (error) {
344
+ console.error('[Twins] Start error:', error);
345
+ res.status(500).json({
346
+ error: {
347
+ code: 'INTERNAL_ERROR',
348
+ message: 'Failed to start twin',
349
+ },
350
+ });
351
+ }
352
+ });
353
+ /**
354
+ * POST /api/twins/:id/stop
355
+ * Stop a twin
356
+ */
357
+ router.post('/:id/stop', auth_1.authenticateApiKey, async (req, res) => {
358
+ try {
359
+ const twin = await (0, db_1.queryOne)('SELECT * FROM twins WHERE id = $1 AND user_id = $2', [req.params.id, req.user.id]);
360
+ if (!twin) {
361
+ res.status(404).json({
362
+ error: {
363
+ code: 'NOT_FOUND',
364
+ message: 'Twin not found',
365
+ },
366
+ });
367
+ return;
368
+ }
369
+ // Stop using twin manager
370
+ const manager = (0, twin_manager_1.getTwinManager)();
371
+ await manager.stopTwins(twin.name);
372
+ // Update twin status
373
+ await (0, db_1.query)(`UPDATE twins SET status = 'stopped', pid_a = NULL, pid_b = NULL, updated_at = NOW()
374
+ WHERE id = $1`, [twin.id]);
375
+ res.json({
376
+ message: 'Twin stopped',
377
+ status: 'stopped',
378
+ });
379
+ }
380
+ catch (error) {
381
+ console.error('[Twins] Stop error:', error);
382
+ res.status(500).json({
383
+ error: {
384
+ code: 'INTERNAL_ERROR',
385
+ message: 'Failed to stop twin',
386
+ },
387
+ });
388
+ }
389
+ });
390
+ /**
391
+ * POST /api/twins/:id/reload
392
+ * Reload twin standby
393
+ */
394
+ router.post('/:id/reload', auth_1.authenticateApiKey, async (req, res) => {
395
+ try {
396
+ const twin = await (0, db_1.queryOne)('SELECT * FROM twins WHERE id = $1 AND user_id = $2', [req.params.id, req.user.id]);
397
+ if (!twin) {
398
+ res.status(404).json({
399
+ error: {
400
+ code: 'NOT_FOUND',
401
+ message: 'Twin not found',
402
+ },
403
+ });
404
+ return;
405
+ }
406
+ if (twin.status !== 'running') {
407
+ res.status(400).json({
408
+ error: {
409
+ code: 'NOT_RUNNING',
410
+ message: 'Twin must be running to reload',
411
+ },
412
+ });
413
+ return;
414
+ }
415
+ // Reload using twin manager
416
+ const manager = (0, twin_manager_1.getTwinManager)();
417
+ const result = await manager.reloadStandby(twin.name);
418
+ if (!result.ok) {
419
+ res.status(500).json({
420
+ error: {
421
+ code: 'RELOAD_FAILED',
422
+ message: result.error || 'Failed to reload twin',
423
+ },
424
+ });
425
+ return;
426
+ }
427
+ res.json({
428
+ message: 'Twin reloaded',
429
+ reloaded: result.reloaded,
430
+ healthy: result.healthy,
431
+ reloadCount: result.reloadCount,
432
+ });
433
+ }
434
+ catch (error) {
435
+ console.error('[Twins] Reload error:', error);
436
+ res.status(500).json({
437
+ error: {
438
+ code: 'INTERNAL_ERROR',
439
+ message: 'Failed to reload twin',
440
+ },
441
+ });
442
+ }
443
+ });
444
+ /**
445
+ * POST /api/twins/:id/swap
446
+ * Swap twin active server
447
+ */
448
+ router.post('/:id/swap', auth_1.authenticateApiKey, async (req, res) => {
449
+ try {
450
+ const twin = await (0, db_1.queryOne)('SELECT * FROM twins WHERE id = $1 AND user_id = $2', [req.params.id, req.user.id]);
451
+ if (!twin) {
452
+ res.status(404).json({
453
+ error: {
454
+ code: 'NOT_FOUND',
455
+ message: 'Twin not found',
456
+ },
457
+ });
458
+ return;
459
+ }
460
+ if (twin.status !== 'running') {
461
+ res.status(400).json({
462
+ error: {
463
+ code: 'NOT_RUNNING',
464
+ message: 'Twin must be running to swap',
465
+ },
466
+ });
467
+ return;
468
+ }
469
+ // Swap using twin manager
470
+ const manager = (0, twin_manager_1.getTwinManager)();
471
+ const result = await manager.swapActive(twin.name);
472
+ if (!result.ok) {
473
+ res.status(500).json({
474
+ error: {
475
+ code: 'SWAP_FAILED',
476
+ message: result.error || 'Failed to swap twin',
477
+ },
478
+ });
479
+ return;
480
+ }
481
+ // Update active server
482
+ const newActive = twin.active_server === 'a' ? 'b' : 'a';
483
+ await (0, db_1.query)(`UPDATE twins SET active_server = $1, updated_at = NOW() WHERE id = $2`, [newActive, twin.id]);
484
+ res.json({
485
+ message: 'Twin swapped',
486
+ previousActive: result.previousActive,
487
+ newActive: result.newActive,
488
+ });
489
+ }
490
+ catch (error) {
491
+ console.error('[Twins] Swap error:', error);
492
+ res.status(500).json({
493
+ error: {
494
+ code: 'INTERNAL_ERROR',
495
+ message: 'Failed to swap twin',
496
+ },
497
+ });
498
+ }
499
+ });
500
+ /**
501
+ * POST /api/twins/:id/call
502
+ * Execute a tool call through the twin
503
+ */
504
+ router.post('/:id/call', auth_1.authenticateApiKey, checkQuota, async (req, res) => {
505
+ const startTime = Date.now();
506
+ try {
507
+ const { toolName, args } = req.body;
508
+ if (!toolName) {
509
+ res.status(400).json({
510
+ error: {
511
+ code: 'VALIDATION_ERROR',
512
+ message: 'toolName is required',
513
+ },
514
+ });
515
+ return;
516
+ }
517
+ const twin = await (0, db_1.queryOne)('SELECT * FROM twins WHERE id = $1 AND user_id = $2', [req.params.id, req.user.id]);
518
+ if (!twin) {
519
+ res.status(404).json({
520
+ error: {
521
+ code: 'NOT_FOUND',
522
+ message: 'Twin not found',
523
+ },
524
+ });
525
+ return;
526
+ }
527
+ if (twin.status !== 'running') {
528
+ res.status(400).json({
529
+ error: {
530
+ code: 'NOT_RUNNING',
531
+ message: 'Twin must be running to execute calls',
532
+ },
533
+ });
534
+ return;
535
+ }
536
+ // Determine active port
537
+ const activePort = twin.active_server === 'a' ? twin.port_a : twin.port_b;
538
+ // Make HTTP call to MCP server
539
+ const response = await fetch(`http://localhost:${activePort}/mcp`, {
540
+ method: 'POST',
541
+ headers: { 'Content-Type': 'application/json' },
542
+ body: JSON.stringify({
543
+ jsonrpc: '2.0',
544
+ method: 'tools/call',
545
+ params: { name: toolName, arguments: args || {} },
546
+ id: Date.now(),
547
+ }),
548
+ });
549
+ const result = await response.json();
550
+ const durationMs = Date.now() - startTime;
551
+ const isError = !!result.error;
552
+ // Log the call
553
+ await (0, db_1.query)(`INSERT INTO logs (user_id, twin_id, tool_name, args, result, error, status, duration_ms, server)
554
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, [
555
+ req.user.id,
556
+ twin.id,
557
+ toolName,
558
+ JSON.stringify(args || {}),
559
+ isError ? null : JSON.stringify(result.result),
560
+ isError ? JSON.stringify(result.error) : null,
561
+ isError ? 'error' : 'success',
562
+ durationMs,
563
+ twin.active_server,
564
+ ]);
565
+ // Update usage
566
+ const today = new Date().toISOString().split('T')[0];
567
+ await (0, db_1.query)(`INSERT INTO usage_daily (user_id, date, request_count, error_count, total_duration_ms)
568
+ VALUES ($1, $2, 1, $3, $4)
569
+ ON CONFLICT (user_id, date)
570
+ DO UPDATE SET
571
+ request_count = usage_daily.request_count + 1,
572
+ error_count = usage_daily.error_count + $3,
573
+ total_duration_ms = usage_daily.total_duration_ms + $4`, [req.user.id, today, isError ? 1 : 0, durationMs]);
574
+ if (isError) {
575
+ res.status(500).json({
576
+ error: {
577
+ code: 'TOOL_ERROR',
578
+ message: result.error.message || 'Tool call failed',
579
+ details: result.error,
580
+ },
581
+ durationMs,
582
+ server: twin.active_server,
583
+ });
584
+ return;
585
+ }
586
+ res.json({
587
+ result: result.result,
588
+ durationMs,
589
+ server: twin.active_server,
590
+ requestId: `req_${Date.now()}`,
591
+ });
592
+ }
593
+ catch (error) {
594
+ const durationMs = Date.now() - startTime;
595
+ // Log the error
596
+ if (req.params.id && req.user) {
597
+ await (0, db_1.query)(`INSERT INTO logs (user_id, twin_id, tool_name, args, error, status, duration_ms, server)
598
+ VALUES ($1, $2, $3, $4, $5, 'error', $6, 'a')`, [
599
+ req.user.id,
600
+ req.params.id,
601
+ req.body?.toolName || 'unknown',
602
+ JSON.stringify(req.body?.args || {}),
603
+ error.message,
604
+ durationMs,
605
+ ]).catch(() => { }); // Don't fail on logging errors
606
+ }
607
+ console.error('[Twins] Call error:', error);
608
+ res.status(500).json({
609
+ error: {
610
+ code: 'INTERNAL_ERROR',
611
+ message: 'Failed to execute tool call',
612
+ details: error.message,
613
+ },
614
+ durationMs,
615
+ });
616
+ }
617
+ });
618
+ /**
619
+ * GET /api/twins/:id/logs
620
+ * Get twin execution logs
621
+ */
622
+ router.get('/:id/logs', auth_1.authenticateApiKey, async (req, res) => {
623
+ try {
624
+ const { limit = '50', offset = '0', status } = req.query;
625
+ const twin = await (0, db_1.queryOne)('SELECT id FROM twins WHERE id = $1 AND user_id = $2', [req.params.id, req.user.id]);
626
+ if (!twin) {
627
+ res.status(404).json({
628
+ error: {
629
+ code: 'NOT_FOUND',
630
+ message: 'Twin not found',
631
+ },
632
+ });
633
+ return;
634
+ }
635
+ let whereClause = 'WHERE twin_id = $1';
636
+ const params = [twin.id];
637
+ if (status) {
638
+ params.push(status);
639
+ whereClause += ` AND status = $${params.length}`;
640
+ }
641
+ params.push(parseInt(limit));
642
+ params.push(parseInt(offset));
643
+ const logs = await (0, db_1.query)(`SELECT * FROM logs
644
+ ${whereClause}
645
+ ORDER BY created_at DESC
646
+ LIMIT $${params.length - 1} OFFSET $${params.length}`, params);
647
+ res.json({
648
+ logs: logs.map((l) => ({
649
+ id: l.id,
650
+ toolName: l.tool_name,
651
+ args: l.args,
652
+ result: l.result,
653
+ error: l.error,
654
+ status: l.status,
655
+ durationMs: l.duration_ms,
656
+ server: l.server,
657
+ createdAt: l.created_at,
658
+ })),
659
+ });
660
+ }
661
+ catch (error) {
662
+ console.error('[Twins] Get logs error:', error);
663
+ res.status(500).json({
664
+ error: {
665
+ code: 'INTERNAL_ERROR',
666
+ message: 'Failed to get logs',
667
+ },
668
+ });
669
+ }
670
+ });
671
+ /**
672
+ * GET /api/twins/:id/status
673
+ * Get twin health status
674
+ */
675
+ router.get('/:id/status', auth_1.authenticateApiKey, async (req, res) => {
676
+ try {
677
+ const twin = await (0, db_1.queryOne)('SELECT * FROM twins WHERE id = $1 AND user_id = $2', [req.params.id, req.user.id]);
678
+ if (!twin) {
679
+ res.status(404).json({
680
+ error: {
681
+ code: 'NOT_FOUND',
682
+ message: 'Twin not found',
683
+ },
684
+ });
685
+ return;
686
+ }
687
+ // Check health if running
688
+ let healthA = false;
689
+ let healthB = false;
690
+ if (twin.status === 'running') {
691
+ try {
692
+ const respA = await fetch(`http://localhost:${twin.port_a}/health`, {
693
+ signal: AbortSignal.timeout(2000)
694
+ });
695
+ healthA = respA.ok;
696
+ }
697
+ catch { }
698
+ try {
699
+ const respB = await fetch(`http://localhost:${twin.port_b}/health`, {
700
+ signal: AbortSignal.timeout(2000)
701
+ });
702
+ healthB = respB.ok;
703
+ }
704
+ catch { }
705
+ }
706
+ res.json({
707
+ twin: {
708
+ id: twin.id,
709
+ name: twin.name,
710
+ status: twin.status,
711
+ activeServer: twin.active_server,
712
+ },
713
+ servers: {
714
+ a: {
715
+ port: twin.port_a,
716
+ pid: twin.pid_a,
717
+ healthy: healthA,
718
+ isActive: twin.active_server === 'a',
719
+ },
720
+ b: {
721
+ port: twin.port_b,
722
+ pid: twin.pid_b,
723
+ healthy: healthB,
724
+ isActive: twin.active_server === 'b',
725
+ },
726
+ },
727
+ });
728
+ }
729
+ catch (error) {
730
+ console.error('[Twins] Status error:', error);
731
+ res.status(500).json({
732
+ error: {
733
+ code: 'INTERNAL_ERROR',
734
+ message: 'Failed to get status',
735
+ },
736
+ });
737
+ }
738
+ });
739
+ exports.default = router;
740
+ //# sourceMappingURL=twins.js.map