upfynai-code 2.5.1 → 2.6.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.
- package/package.json +2 -8
- package/server/cli.js +1 -1
- package/server/database/db.js +16 -2
- package/server/index.js +2738 -2621
- package/server/middleware/auth.js +10 -2
- package/server/relay-client.js +73 -20
- package/server/routes/agent.js +1226 -1266
- package/server/routes/auth.js +32 -29
- package/server/routes/commands.js +598 -601
- package/server/routes/cursor.js +806 -807
- package/server/routes/dashboard.js +154 -1
- package/server/routes/git.js +1151 -1165
- package/server/routes/mcp.js +534 -551
- package/server/routes/settings.js +261 -269
- package/server/routes/taskmaster.js +1927 -1963
- package/server/routes/vapi-chat.js +94 -0
- package/server/routes/voice.js +0 -4
- package/server/sandbox.js +120 -0
package/server/routes/cursor.js
CHANGED
|
@@ -1,808 +1,807 @@
|
|
|
1
|
-
import express from 'express';
|
|
2
|
-
import { promises as fs } from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import os from 'os';
|
|
5
|
-
import { spawn } from 'child_process';
|
|
6
|
-
// sqlite3 is a native module — conditionally imported (not available on Vercel)
|
|
7
|
-
let sqlite3 = null;
|
|
8
|
-
let sqliteOpen = null;
|
|
9
|
-
try {
|
|
10
|
-
sqlite3 = (await import('sqlite3')).default;
|
|
11
|
-
sqliteOpen = (await import('sqlite')).open;
|
|
12
|
-
} catch (e) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
import
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
config.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
server.
|
|
152
|
-
server.config.
|
|
153
|
-
server.config.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
server.
|
|
157
|
-
server.config.
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
// MCP config
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
mcpConfig
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
mcpConfig
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
sessionData.
|
|
453
|
-
sessionData.
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const
|
|
505
|
-
const
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
if (!
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
const {
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
const
|
|
617
|
-
const
|
|
618
|
-
const
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
//
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
//
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { spawn } from 'child_process';
|
|
6
|
+
// sqlite3 is a native module — conditionally imported (not available on Vercel)
|
|
7
|
+
let sqlite3 = null;
|
|
8
|
+
let sqliteOpen = null;
|
|
9
|
+
try {
|
|
10
|
+
sqlite3 = (await import('sqlite3')).default;
|
|
11
|
+
sqliteOpen = (await import('sqlite')).open;
|
|
12
|
+
} catch (e) {
|
|
13
|
+
}
|
|
14
|
+
import crypto from 'crypto';
|
|
15
|
+
import { CURSOR_MODELS } from '../../shared/modelConstants.js';
|
|
16
|
+
|
|
17
|
+
const router = express.Router();
|
|
18
|
+
|
|
19
|
+
// GET /api/cursor/config - Read Cursor CLI configuration
|
|
20
|
+
router.get('/config', async (req, res) => {
|
|
21
|
+
try {
|
|
22
|
+
const configPath = path.join(os.homedir(), '.cursor', 'cli-config.json');
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const configContent = await fs.readFile(configPath, 'utf8');
|
|
26
|
+
const config = JSON.parse(configContent);
|
|
27
|
+
|
|
28
|
+
res.json({
|
|
29
|
+
success: true,
|
|
30
|
+
config: config,
|
|
31
|
+
path: configPath
|
|
32
|
+
});
|
|
33
|
+
} catch (error) {
|
|
34
|
+
// Config doesn't exist or is invalid
|
|
35
|
+
// cursor config not found
|
|
36
|
+
|
|
37
|
+
// Return default config
|
|
38
|
+
res.json({
|
|
39
|
+
success: true,
|
|
40
|
+
config: {
|
|
41
|
+
version: 1,
|
|
42
|
+
model: {
|
|
43
|
+
modelId: CURSOR_MODELS.DEFAULT,
|
|
44
|
+
displayName: "GPT-5"
|
|
45
|
+
},
|
|
46
|
+
permissions: {
|
|
47
|
+
allow: [],
|
|
48
|
+
deny: []
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
isDefault: true
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
// config read error
|
|
56
|
+
res.status(500).json({
|
|
57
|
+
error: 'Failed to read Cursor configuration',
|
|
58
|
+
details: 'An error occurred'
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// POST /api/cursor/config - Update Cursor CLI configuration
|
|
64
|
+
router.post('/config', async (req, res) => {
|
|
65
|
+
try {
|
|
66
|
+
const { permissions, model } = req.body;
|
|
67
|
+
const configPath = path.join(os.homedir(), '.cursor', 'cli-config.json');
|
|
68
|
+
|
|
69
|
+
// Read existing config or create default
|
|
70
|
+
let config = {
|
|
71
|
+
version: 1,
|
|
72
|
+
editor: {
|
|
73
|
+
vimMode: false
|
|
74
|
+
},
|
|
75
|
+
hasChangedDefaultModel: false,
|
|
76
|
+
privacyCache: {
|
|
77
|
+
ghostMode: false,
|
|
78
|
+
privacyMode: 3,
|
|
79
|
+
updatedAt: Date.now()
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const existing = await fs.readFile(configPath, 'utf8');
|
|
85
|
+
config = JSON.parse(existing);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
// Config doesn't exist, use defaults
|
|
88
|
+
// creating new config
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Update permissions if provided
|
|
92
|
+
if (permissions) {
|
|
93
|
+
config.permissions = {
|
|
94
|
+
allow: permissions.allow || [],
|
|
95
|
+
deny: permissions.deny || []
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Update model if provided
|
|
100
|
+
if (model) {
|
|
101
|
+
config.model = model;
|
|
102
|
+
config.hasChangedDefaultModel = true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Ensure directory exists
|
|
106
|
+
const configDir = path.dirname(configPath);
|
|
107
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
108
|
+
|
|
109
|
+
// Write updated config
|
|
110
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
111
|
+
|
|
112
|
+
res.json({
|
|
113
|
+
success: true,
|
|
114
|
+
config: config,
|
|
115
|
+
message: 'Cursor configuration updated successfully'
|
|
116
|
+
});
|
|
117
|
+
} catch (error) {
|
|
118
|
+
// config update error
|
|
119
|
+
res.status(500).json({
|
|
120
|
+
error: 'Failed to update Cursor configuration',
|
|
121
|
+
details: 'An error occurred'
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// GET /api/cursor/mcp - Read Cursor MCP servers configuration
|
|
127
|
+
router.get('/mcp', async (req, res) => {
|
|
128
|
+
try {
|
|
129
|
+
const mcpPath = path.join(os.homedir(), '.cursor', 'mcp.json');
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const mcpContent = await fs.readFile(mcpPath, 'utf8');
|
|
133
|
+
const mcpConfig = JSON.parse(mcpContent);
|
|
134
|
+
|
|
135
|
+
// Convert to UI-friendly format
|
|
136
|
+
const servers = [];
|
|
137
|
+
if (mcpConfig.mcpServers && typeof mcpConfig.mcpServers === 'object') {
|
|
138
|
+
for (const [name, config] of Object.entries(mcpConfig.mcpServers)) {
|
|
139
|
+
const server = {
|
|
140
|
+
id: name,
|
|
141
|
+
name: name,
|
|
142
|
+
type: 'stdio',
|
|
143
|
+
scope: 'cursor',
|
|
144
|
+
config: {},
|
|
145
|
+
raw: config
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Determine transport type and extract config
|
|
149
|
+
if (config.command) {
|
|
150
|
+
server.type = 'stdio';
|
|
151
|
+
server.config.command = config.command;
|
|
152
|
+
server.config.args = config.args || [];
|
|
153
|
+
server.config.env = config.env || {};
|
|
154
|
+
} else if (config.url) {
|
|
155
|
+
server.type = config.transport || 'http';
|
|
156
|
+
server.config.url = config.url;
|
|
157
|
+
server.config.headers = config.headers || {};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
servers.push(server);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
res.json({
|
|
165
|
+
success: true,
|
|
166
|
+
servers: servers,
|
|
167
|
+
path: mcpPath
|
|
168
|
+
});
|
|
169
|
+
} catch (error) {
|
|
170
|
+
// MCP config doesn't exist
|
|
171
|
+
// MCP config not found
|
|
172
|
+
res.json({
|
|
173
|
+
success: true,
|
|
174
|
+
servers: [],
|
|
175
|
+
isDefault: true
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
} catch (error) {
|
|
179
|
+
// MCP config read error
|
|
180
|
+
res.status(500).json({
|
|
181
|
+
error: 'Failed to read Cursor MCP configuration',
|
|
182
|
+
details: 'An error occurred'
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// POST /api/cursor/mcp/add - Add MCP server to Cursor configuration
|
|
188
|
+
router.post('/mcp/add', async (req, res) => {
|
|
189
|
+
try {
|
|
190
|
+
const { name, type = 'stdio', command, args = [], url, headers = {}, env = {} } = req.body;
|
|
191
|
+
const mcpPath = path.join(os.homedir(), '.cursor', 'mcp.json');
|
|
192
|
+
|
|
193
|
+
// adding MCP server
|
|
194
|
+
|
|
195
|
+
// Read existing config or create new
|
|
196
|
+
let mcpConfig = { mcpServers: {} };
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const existing = await fs.readFile(mcpPath, 'utf8');
|
|
200
|
+
mcpConfig = JSON.parse(existing);
|
|
201
|
+
if (!mcpConfig.mcpServers) {
|
|
202
|
+
mcpConfig.mcpServers = {};
|
|
203
|
+
}
|
|
204
|
+
} catch (error) {
|
|
205
|
+
// creating new MCP config
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Build server config based on type
|
|
209
|
+
let serverConfig = {};
|
|
210
|
+
|
|
211
|
+
if (type === 'stdio') {
|
|
212
|
+
serverConfig = {
|
|
213
|
+
command: command,
|
|
214
|
+
args: args,
|
|
215
|
+
env: env
|
|
216
|
+
};
|
|
217
|
+
} else if (type === 'http' || type === 'sse') {
|
|
218
|
+
serverConfig = {
|
|
219
|
+
url: url,
|
|
220
|
+
transport: type,
|
|
221
|
+
headers: headers
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Add server to config
|
|
226
|
+
mcpConfig.mcpServers[name] = serverConfig;
|
|
227
|
+
|
|
228
|
+
// Ensure directory exists
|
|
229
|
+
const mcpDir = path.dirname(mcpPath);
|
|
230
|
+
await fs.mkdir(mcpDir, { recursive: true });
|
|
231
|
+
|
|
232
|
+
// Write updated config
|
|
233
|
+
await fs.writeFile(mcpPath, JSON.stringify(mcpConfig, null, 2));
|
|
234
|
+
|
|
235
|
+
res.json({
|
|
236
|
+
success: true,
|
|
237
|
+
message: `MCP server "${name}" added to Cursor configuration`,
|
|
238
|
+
config: mcpConfig
|
|
239
|
+
});
|
|
240
|
+
} catch (error) {
|
|
241
|
+
// MCP server add error
|
|
242
|
+
res.status(500).json({
|
|
243
|
+
error: 'Failed to add MCP server',
|
|
244
|
+
details: 'An error occurred'
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// DELETE /api/cursor/mcp/:name - Remove MCP server from Cursor configuration
|
|
250
|
+
router.delete('/mcp/:name', async (req, res) => {
|
|
251
|
+
try {
|
|
252
|
+
const { name } = req.params;
|
|
253
|
+
const mcpPath = path.join(os.homedir(), '.cursor', 'mcp.json');
|
|
254
|
+
|
|
255
|
+
// removing MCP server
|
|
256
|
+
|
|
257
|
+
// Read existing config
|
|
258
|
+
let mcpConfig = { mcpServers: {} };
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const existing = await fs.readFile(mcpPath, 'utf8');
|
|
262
|
+
mcpConfig = JSON.parse(existing);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
return res.status(404).json({
|
|
265
|
+
error: 'Cursor MCP configuration not found'
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Check if server exists
|
|
270
|
+
if (!mcpConfig.mcpServers || !mcpConfig.mcpServers[name]) {
|
|
271
|
+
return res.status(404).json({
|
|
272
|
+
error: `MCP server "${name}" not found in Cursor configuration`
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Remove server from config
|
|
277
|
+
delete mcpConfig.mcpServers[name];
|
|
278
|
+
|
|
279
|
+
// Write updated config
|
|
280
|
+
await fs.writeFile(mcpPath, JSON.stringify(mcpConfig, null, 2));
|
|
281
|
+
|
|
282
|
+
res.json({
|
|
283
|
+
success: true,
|
|
284
|
+
message: `MCP server "${name}" removed from Cursor configuration`,
|
|
285
|
+
config: mcpConfig
|
|
286
|
+
});
|
|
287
|
+
} catch (error) {
|
|
288
|
+
// MCP server remove error
|
|
289
|
+
res.status(500).json({
|
|
290
|
+
error: 'Failed to remove MCP server',
|
|
291
|
+
details: 'An error occurred'
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// POST /api/cursor/mcp/add-json - Add MCP server using JSON format
|
|
297
|
+
router.post('/mcp/add-json', async (req, res) => {
|
|
298
|
+
try {
|
|
299
|
+
const { name, jsonConfig } = req.body;
|
|
300
|
+
const mcpPath = path.join(os.homedir(), '.cursor', 'mcp.json');
|
|
301
|
+
|
|
302
|
+
// adding MCP server via JSON
|
|
303
|
+
|
|
304
|
+
// Validate and parse JSON config
|
|
305
|
+
let parsedConfig;
|
|
306
|
+
try {
|
|
307
|
+
parsedConfig = typeof jsonConfig === 'string' ? JSON.parse(jsonConfig) : jsonConfig;
|
|
308
|
+
} catch (parseError) {
|
|
309
|
+
return res.status(400).json({
|
|
310
|
+
error: 'Invalid JSON configuration',
|
|
311
|
+
details: parseError.message
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Read existing config or create new
|
|
316
|
+
let mcpConfig = { mcpServers: {} };
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
const existing = await fs.readFile(mcpPath, 'utf8');
|
|
320
|
+
mcpConfig = JSON.parse(existing);
|
|
321
|
+
if (!mcpConfig.mcpServers) {
|
|
322
|
+
mcpConfig.mcpServers = {};
|
|
323
|
+
}
|
|
324
|
+
} catch (error) {
|
|
325
|
+
// creating new MCP config
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Add server to config
|
|
329
|
+
mcpConfig.mcpServers[name] = parsedConfig;
|
|
330
|
+
|
|
331
|
+
// Ensure directory exists
|
|
332
|
+
const mcpDir = path.dirname(mcpPath);
|
|
333
|
+
await fs.mkdir(mcpDir, { recursive: true });
|
|
334
|
+
|
|
335
|
+
// Write updated config
|
|
336
|
+
await fs.writeFile(mcpPath, JSON.stringify(mcpConfig, null, 2));
|
|
337
|
+
|
|
338
|
+
res.json({
|
|
339
|
+
success: true,
|
|
340
|
+
message: `MCP server "${name}" added to Cursor configuration via JSON`,
|
|
341
|
+
config: mcpConfig
|
|
342
|
+
});
|
|
343
|
+
} catch (error) {
|
|
344
|
+
// MCP server JSON add error
|
|
345
|
+
res.status(500).json({
|
|
346
|
+
error: 'Failed to add MCP server',
|
|
347
|
+
details: 'An error occurred'
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
// GET /api/cursor/sessions - Get Cursor sessions from SQLite database
|
|
353
|
+
router.get('/sessions', async (req, res) => {
|
|
354
|
+
try {
|
|
355
|
+
const { projectPath } = req.query;
|
|
356
|
+
|
|
357
|
+
// Calculate cwdID hash for the project path (Cursor uses MD5 hash)
|
|
358
|
+
const cwdId = crypto.createHash('md5').update(projectPath || process.cwd()).digest('hex');
|
|
359
|
+
const cursorChatsPath = path.join(os.homedir(), '.cursor', 'chats', cwdId);
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
// Check if the directory exists
|
|
363
|
+
try {
|
|
364
|
+
await fs.access(cursorChatsPath);
|
|
365
|
+
} catch (error) {
|
|
366
|
+
// No sessions for this project
|
|
367
|
+
return res.json({
|
|
368
|
+
success: true,
|
|
369
|
+
sessions: [],
|
|
370
|
+
cwdId: cwdId,
|
|
371
|
+
path: cursorChatsPath
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// List all session directories
|
|
376
|
+
const sessionDirs = await fs.readdir(cursorChatsPath);
|
|
377
|
+
const sessions = [];
|
|
378
|
+
|
|
379
|
+
for (const sessionId of sessionDirs) {
|
|
380
|
+
const sessionPath = path.join(cursorChatsPath, sessionId);
|
|
381
|
+
const storeDbPath = path.join(sessionPath, 'store.db');
|
|
382
|
+
let dbStatMtimeMs = null;
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
// Check if store.db exists
|
|
386
|
+
await fs.access(storeDbPath);
|
|
387
|
+
|
|
388
|
+
// Capture store.db mtime as a reliable fallback timestamp (last activity)
|
|
389
|
+
try {
|
|
390
|
+
const stat = await fs.stat(storeDbPath);
|
|
391
|
+
dbStatMtimeMs = stat.mtimeMs;
|
|
392
|
+
} catch (_) {}
|
|
393
|
+
|
|
394
|
+
// Open SQLite database
|
|
395
|
+
if (!sqliteOpen || !sqlite3) {
|
|
396
|
+
continue; // Skip on Vercel where native modules aren't available
|
|
397
|
+
}
|
|
398
|
+
const db = await sqliteOpen({
|
|
399
|
+
filename: storeDbPath,
|
|
400
|
+
driver: sqlite3.Database,
|
|
401
|
+
mode: sqlite3.OPEN_READONLY
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// Get metadata from meta table
|
|
405
|
+
const metaRows = await db.all(`
|
|
406
|
+
SELECT key, value FROM meta
|
|
407
|
+
`);
|
|
408
|
+
|
|
409
|
+
let sessionData = {
|
|
410
|
+
id: sessionId,
|
|
411
|
+
name: 'Untitled Session',
|
|
412
|
+
createdAt: null,
|
|
413
|
+
mode: null,
|
|
414
|
+
projectPath: projectPath,
|
|
415
|
+
lastMessage: null,
|
|
416
|
+
messageCount: 0
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
// Parse meta table entries
|
|
420
|
+
for (const row of metaRows) {
|
|
421
|
+
if (row.value) {
|
|
422
|
+
try {
|
|
423
|
+
// Try to decode as hex-encoded JSON
|
|
424
|
+
const hexMatch = row.value.toString().match(/^[0-9a-fA-F]+$/);
|
|
425
|
+
if (hexMatch) {
|
|
426
|
+
const jsonStr = Buffer.from(row.value, 'hex').toString('utf8');
|
|
427
|
+
const data = JSON.parse(jsonStr);
|
|
428
|
+
|
|
429
|
+
if (row.key === 'agent') {
|
|
430
|
+
sessionData.name = data.name || sessionData.name;
|
|
431
|
+
// Normalize createdAt to ISO string in milliseconds
|
|
432
|
+
let createdAt = data.createdAt;
|
|
433
|
+
if (typeof createdAt === 'number') {
|
|
434
|
+
if (createdAt < 1e12) {
|
|
435
|
+
createdAt = createdAt * 1000; // seconds -> ms
|
|
436
|
+
}
|
|
437
|
+
sessionData.createdAt = new Date(createdAt).toISOString();
|
|
438
|
+
} else if (typeof createdAt === 'string') {
|
|
439
|
+
const n = Number(createdAt);
|
|
440
|
+
if (!Number.isNaN(n)) {
|
|
441
|
+
const ms = n < 1e12 ? n * 1000 : n;
|
|
442
|
+
sessionData.createdAt = new Date(ms).toISOString();
|
|
443
|
+
} else {
|
|
444
|
+
// Assume it's already an ISO/date string
|
|
445
|
+
const d = new Date(createdAt);
|
|
446
|
+
sessionData.createdAt = isNaN(d.getTime()) ? null : d.toISOString();
|
|
447
|
+
}
|
|
448
|
+
} else {
|
|
449
|
+
sessionData.createdAt = sessionData.createdAt || null;
|
|
450
|
+
}
|
|
451
|
+
sessionData.mode = data.mode;
|
|
452
|
+
sessionData.agentId = data.agentId;
|
|
453
|
+
sessionData.latestRootBlobId = data.latestRootBlobId;
|
|
454
|
+
}
|
|
455
|
+
} else {
|
|
456
|
+
// If not hex, use raw value for simple keys
|
|
457
|
+
if (row.key === 'name') {
|
|
458
|
+
sessionData.name = row.value.toString();
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
} catch (e) {
|
|
462
|
+
// meta parse error
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Get message count from JSON blobs only (actual messages, not DAG structure)
|
|
468
|
+
try {
|
|
469
|
+
const blobCount = await db.get(`
|
|
470
|
+
SELECT COUNT(*) as count
|
|
471
|
+
FROM blobs
|
|
472
|
+
WHERE substr(data, 1, 1) = X'7B'
|
|
473
|
+
`);
|
|
474
|
+
sessionData.messageCount = blobCount.count;
|
|
475
|
+
|
|
476
|
+
// Get the most recent JSON blob for preview (actual message, not DAG structure)
|
|
477
|
+
const lastBlob = await db.get(`
|
|
478
|
+
SELECT data FROM blobs
|
|
479
|
+
WHERE substr(data, 1, 1) = X'7B'
|
|
480
|
+
ORDER BY rowid DESC
|
|
481
|
+
LIMIT 1
|
|
482
|
+
`);
|
|
483
|
+
|
|
484
|
+
if (lastBlob && lastBlob.data) {
|
|
485
|
+
try {
|
|
486
|
+
// Try to extract readable preview from blob (may contain binary with embedded JSON)
|
|
487
|
+
const raw = lastBlob.data.toString('utf8');
|
|
488
|
+
let preview = '';
|
|
489
|
+
// Attempt direct JSON parse
|
|
490
|
+
try {
|
|
491
|
+
const parsed = JSON.parse(raw);
|
|
492
|
+
if (parsed?.content) {
|
|
493
|
+
if (Array.isArray(parsed.content)) {
|
|
494
|
+
const firstText = parsed.content.find(p => p?.type === 'text' && p.text)?.text || '';
|
|
495
|
+
preview = firstText;
|
|
496
|
+
} else if (typeof parsed.content === 'string') {
|
|
497
|
+
preview = parsed.content;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
} catch (_) {}
|
|
501
|
+
if (!preview) {
|
|
502
|
+
// Strip non-printable and try to find JSON chunk
|
|
503
|
+
const cleaned = raw.replace(/[^\x09\x0A\x0D\x20-\x7E]/g, '');
|
|
504
|
+
const s = cleaned;
|
|
505
|
+
const start = s.indexOf('{');
|
|
506
|
+
const end = s.lastIndexOf('}');
|
|
507
|
+
if (start !== -1 && end > start) {
|
|
508
|
+
const jsonStr = s.slice(start, end + 1);
|
|
509
|
+
try {
|
|
510
|
+
const parsed = JSON.parse(jsonStr);
|
|
511
|
+
if (parsed?.content) {
|
|
512
|
+
if (Array.isArray(parsed.content)) {
|
|
513
|
+
const firstText = parsed.content.find(p => p?.type === 'text' && p.text)?.text || '';
|
|
514
|
+
preview = firstText;
|
|
515
|
+
} else if (typeof parsed.content === 'string') {
|
|
516
|
+
preview = parsed.content;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
} catch (_) {
|
|
520
|
+
preview = s;
|
|
521
|
+
}
|
|
522
|
+
} else {
|
|
523
|
+
preview = s;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (preview && preview.length > 0) {
|
|
527
|
+
sessionData.lastMessage = preview.substring(0, 100) + (preview.length > 100 ? '...' : '');
|
|
528
|
+
}
|
|
529
|
+
} catch (e) {
|
|
530
|
+
// blob parse error
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
} catch (e) {
|
|
534
|
+
// blobs read error
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
await db.close();
|
|
538
|
+
|
|
539
|
+
// Finalize createdAt: use parsed meta value when valid, else fall back to store.db mtime
|
|
540
|
+
if (!sessionData.createdAt) {
|
|
541
|
+
if (dbStatMtimeMs && Number.isFinite(dbStatMtimeMs)) {
|
|
542
|
+
sessionData.createdAt = new Date(dbStatMtimeMs).toISOString();
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
sessions.push(sessionData);
|
|
547
|
+
|
|
548
|
+
} catch (error) {
|
|
549
|
+
// session read error
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Fallback: ensure createdAt is a valid ISO string (use session directory mtime as last resort)
|
|
554
|
+
for (const s of sessions) {
|
|
555
|
+
if (!s.createdAt) {
|
|
556
|
+
try {
|
|
557
|
+
const sessionDir = path.join(cursorChatsPath, s.id);
|
|
558
|
+
const st = await fs.stat(sessionDir);
|
|
559
|
+
s.createdAt = new Date(st.mtimeMs).toISOString();
|
|
560
|
+
} catch {
|
|
561
|
+
s.createdAt = new Date().toISOString();
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
// Sort sessions by creation date (newest first)
|
|
566
|
+
sessions.sort((a, b) => {
|
|
567
|
+
if (!a.createdAt) return 1;
|
|
568
|
+
if (!b.createdAt) return -1;
|
|
569
|
+
return new Date(b.createdAt) - new Date(a.createdAt);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
res.json({
|
|
573
|
+
success: true,
|
|
574
|
+
sessions: sessions,
|
|
575
|
+
cwdId: cwdId,
|
|
576
|
+
path: cursorChatsPath
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
} catch (error) {
|
|
580
|
+
// sessions read error
|
|
581
|
+
res.status(500).json({
|
|
582
|
+
error: 'Failed to read Cursor sessions',
|
|
583
|
+
details: 'An error occurred'
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
// GET /api/cursor/sessions/:sessionId - Get specific Cursor session from SQLite
|
|
589
|
+
router.get('/sessions/:sessionId', async (req, res) => {
|
|
590
|
+
try {
|
|
591
|
+
const { sessionId } = req.params;
|
|
592
|
+
const { projectPath } = req.query;
|
|
593
|
+
|
|
594
|
+
// Calculate cwdID hash for the project path
|
|
595
|
+
const cwdId = crypto.createHash('md5').update(projectPath || process.cwd()).digest('hex');
|
|
596
|
+
const storeDbPath = path.join(os.homedir(), '.cursor', 'chats', cwdId, sessionId, 'store.db');
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
// Open SQLite database (requires native sqlite3 module)
|
|
600
|
+
if (!sqliteOpen || !sqlite3) {
|
|
601
|
+
return res.status(503).json({ error: 'SQLite not available on this deployment' });
|
|
602
|
+
}
|
|
603
|
+
const db = await sqliteOpen({
|
|
604
|
+
filename: storeDbPath,
|
|
605
|
+
driver: sqlite3.Database,
|
|
606
|
+
mode: sqlite3.OPEN_READONLY
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
// Get all blobs to build the DAG structure
|
|
610
|
+
const allBlobs = await db.all(`
|
|
611
|
+
SELECT rowid, id, data FROM blobs
|
|
612
|
+
`);
|
|
613
|
+
|
|
614
|
+
// Build the DAG structure from parent-child relationships
|
|
615
|
+
const blobMap = new Map(); // id -> blob data
|
|
616
|
+
const parentRefs = new Map(); // blob id -> [parent blob ids]
|
|
617
|
+
const childRefs = new Map(); // blob id -> [child blob ids]
|
|
618
|
+
const jsonBlobs = []; // Clean JSON messages
|
|
619
|
+
|
|
620
|
+
for (const blob of allBlobs) {
|
|
621
|
+
blobMap.set(blob.id, blob);
|
|
622
|
+
|
|
623
|
+
// Check if this is a JSON blob (actual message) or protobuf (DAG structure)
|
|
624
|
+
if (blob.data && blob.data[0] === 0x7B) { // Starts with '{' - JSON blob
|
|
625
|
+
try {
|
|
626
|
+
const parsed = JSON.parse(blob.data.toString('utf8'));
|
|
627
|
+
jsonBlobs.push({ ...blob, parsed });
|
|
628
|
+
} catch (e) {
|
|
629
|
+
// JSON blob parse failed
|
|
630
|
+
}
|
|
631
|
+
} else if (blob.data) { // Protobuf blob - extract parent references
|
|
632
|
+
const parents = [];
|
|
633
|
+
let i = 0;
|
|
634
|
+
|
|
635
|
+
// Scan for parent references (0x0A 0x20 followed by 32-byte hash)
|
|
636
|
+
while (i < blob.data.length - 33) {
|
|
637
|
+
if (blob.data[i] === 0x0A && blob.data[i+1] === 0x20) {
|
|
638
|
+
const parentHash = blob.data.slice(i+2, i+34).toString('hex');
|
|
639
|
+
if (blobMap.has(parentHash)) {
|
|
640
|
+
parents.push(parentHash);
|
|
641
|
+
}
|
|
642
|
+
i += 34;
|
|
643
|
+
} else {
|
|
644
|
+
i++;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (parents.length > 0) {
|
|
649
|
+
parentRefs.set(blob.id, parents);
|
|
650
|
+
// Update child references
|
|
651
|
+
for (const parentId of parents) {
|
|
652
|
+
if (!childRefs.has(parentId)) {
|
|
653
|
+
childRefs.set(parentId, []);
|
|
654
|
+
}
|
|
655
|
+
childRefs.get(parentId).push(blob.id);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Perform topological sort to get chronological order
|
|
662
|
+
const visited = new Set();
|
|
663
|
+
const sorted = [];
|
|
664
|
+
|
|
665
|
+
// DFS-based topological sort
|
|
666
|
+
function visit(nodeId) {
|
|
667
|
+
if (visited.has(nodeId)) return;
|
|
668
|
+
visited.add(nodeId);
|
|
669
|
+
|
|
670
|
+
// Visit all parents first (dependencies)
|
|
671
|
+
const parents = parentRefs.get(nodeId) || [];
|
|
672
|
+
for (const parentId of parents) {
|
|
673
|
+
visit(parentId);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Add this node after all its parents
|
|
677
|
+
const blob = blobMap.get(nodeId);
|
|
678
|
+
if (blob) {
|
|
679
|
+
sorted.push(blob);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Start with nodes that have no parents (roots)
|
|
684
|
+
for (const blob of allBlobs) {
|
|
685
|
+
if (!parentRefs.has(blob.id)) {
|
|
686
|
+
visit(blob.id);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Visit any remaining nodes (disconnected components)
|
|
691
|
+
for (const blob of allBlobs) {
|
|
692
|
+
visit(blob.id);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Now extract JSON messages in the order they appear in the sorted DAG
|
|
696
|
+
const messageOrder = new Map(); // JSON blob id -> order index
|
|
697
|
+
let orderIndex = 0;
|
|
698
|
+
|
|
699
|
+
for (const blob of sorted) {
|
|
700
|
+
// Check if this blob references any JSON messages
|
|
701
|
+
if (blob.data && blob.data[0] !== 0x7B) { // Protobuf blob
|
|
702
|
+
// Look for JSON blob references
|
|
703
|
+
for (const jsonBlob of jsonBlobs) {
|
|
704
|
+
try {
|
|
705
|
+
const jsonIdBytes = Buffer.from(jsonBlob.id, 'hex');
|
|
706
|
+
if (blob.data.includes(jsonIdBytes)) {
|
|
707
|
+
if (!messageOrder.has(jsonBlob.id)) {
|
|
708
|
+
messageOrder.set(jsonBlob.id, orderIndex++);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
} catch (e) {
|
|
712
|
+
// Skip if can't convert ID
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Sort JSON blobs by their appearance order in the DAG
|
|
719
|
+
const sortedJsonBlobs = jsonBlobs.sort((a, b) => {
|
|
720
|
+
const orderA = messageOrder.get(a.id) ?? Number.MAX_SAFE_INTEGER;
|
|
721
|
+
const orderB = messageOrder.get(b.id) ?? Number.MAX_SAFE_INTEGER;
|
|
722
|
+
if (orderA !== orderB) return orderA - orderB;
|
|
723
|
+
// Fallback to rowid if not in order map
|
|
724
|
+
return a.rowid - b.rowid;
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
// Use sorted JSON blobs
|
|
728
|
+
const blobs = sortedJsonBlobs.map((blob, idx) => ({
|
|
729
|
+
...blob,
|
|
730
|
+
sequence_num: idx + 1,
|
|
731
|
+
original_rowid: blob.rowid
|
|
732
|
+
}));
|
|
733
|
+
|
|
734
|
+
// Get metadata from meta table
|
|
735
|
+
const metaRows = await db.all(`
|
|
736
|
+
SELECT key, value FROM meta
|
|
737
|
+
`);
|
|
738
|
+
|
|
739
|
+
// Parse metadata
|
|
740
|
+
let metadata = {};
|
|
741
|
+
for (const row of metaRows) {
|
|
742
|
+
if (row.value) {
|
|
743
|
+
try {
|
|
744
|
+
// Try to decode as hex-encoded JSON
|
|
745
|
+
const hexMatch = row.value.toString().match(/^[0-9a-fA-F]+$/);
|
|
746
|
+
if (hexMatch) {
|
|
747
|
+
const jsonStr = Buffer.from(row.value, 'hex').toString('utf8');
|
|
748
|
+
metadata[row.key] = JSON.parse(jsonStr);
|
|
749
|
+
} else {
|
|
750
|
+
metadata[row.key] = row.value.toString();
|
|
751
|
+
}
|
|
752
|
+
} catch (e) {
|
|
753
|
+
metadata[row.key] = row.value.toString();
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Extract messages from sorted JSON blobs
|
|
759
|
+
const messages = [];
|
|
760
|
+
for (const blob of blobs) {
|
|
761
|
+
try {
|
|
762
|
+
// We already parsed JSON blobs earlier
|
|
763
|
+
const parsed = blob.parsed;
|
|
764
|
+
|
|
765
|
+
if (parsed) {
|
|
766
|
+
// Filter out ONLY system messages at the server level
|
|
767
|
+
// Check both direct role and nested message.role
|
|
768
|
+
const role = parsed?.role || parsed?.message?.role;
|
|
769
|
+
if (role === 'system') {
|
|
770
|
+
continue; // Skip only system messages
|
|
771
|
+
}
|
|
772
|
+
messages.push({
|
|
773
|
+
id: blob.id,
|
|
774
|
+
sequence: blob.sequence_num,
|
|
775
|
+
rowid: blob.original_rowid,
|
|
776
|
+
content: parsed
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
} catch (e) {
|
|
780
|
+
// Skip blobs that cause errors
|
|
781
|
+
// blob skipped
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
await db.close();
|
|
786
|
+
|
|
787
|
+
res.json({
|
|
788
|
+
success: true,
|
|
789
|
+
session: {
|
|
790
|
+
id: sessionId,
|
|
791
|
+
projectPath: projectPath,
|
|
792
|
+
messages: messages,
|
|
793
|
+
metadata: metadata,
|
|
794
|
+
cwdId: cwdId
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
} catch (error) {
|
|
799
|
+
// session read error
|
|
800
|
+
res.status(500).json({
|
|
801
|
+
error: 'Failed to read Cursor session',
|
|
802
|
+
details: 'An error occurred'
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
|
|
808
807
|
export default router;
|