upfynai-code 2.9.1 → 2.9.2

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 (229) hide show
  1. package/README.md +91 -66
  2. package/client/dist/api-docs.html +838 -0
  3. package/client/dist/assets/AppContent-BXZDeSIC.js +545 -0
  4. package/client/dist/assets/CanvasFullScreen-mnpCnLZ9.js +1 -0
  5. package/client/dist/assets/CanvasWorkspace-4CqmjAVQ.js +163 -0
  6. package/client/dist/assets/DashboardPanel-zFIFlw56.js +1 -0
  7. package/client/dist/assets/FileTree-B0c_GaB3.js +1 -0
  8. package/client/dist/assets/GitPanel-DUP4zVU4.js +2 -0
  9. package/client/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  10. package/client/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  11. package/client/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  12. package/client/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  13. package/client/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  14. package/client/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  15. package/client/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  16. package/client/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  17. package/client/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  18. package/client/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  19. package/client/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  20. package/client/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  21. package/client/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  22. package/client/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  23. package/client/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  24. package/client/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  25. package/client/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  26. package/client/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  27. package/client/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  28. package/client/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  29. package/client/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  30. package/client/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  31. package/client/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  32. package/client/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  33. package/client/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  34. package/client/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  35. package/client/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  36. package/client/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  37. package/client/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  38. package/client/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  39. package/client/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  40. package/client/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  41. package/client/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  42. package/client/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  43. package/client/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  44. package/client/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  45. package/client/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  46. package/client/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  47. package/client/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  48. package/client/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  49. package/client/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  50. package/client/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  51. package/client/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  52. package/client/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  53. package/client/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  54. package/client/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  55. package/client/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  56. package/client/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  57. package/client/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  58. package/client/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  59. package/client/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  60. package/client/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  61. package/client/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  62. package/client/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  63. package/client/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  64. package/client/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  65. package/client/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  66. package/client/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  67. package/client/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  68. package/client/dist/assets/LoginModal-BRycfsyD.js +13 -0
  69. package/client/dist/assets/MarkdownPreview-DHmk3qzu.js +1 -0
  70. package/client/dist/assets/MermaidBlock-BuBc_G-F.js +2 -0
  71. package/client/dist/assets/Onboarding-BcnaZZ0o.js +1 -0
  72. package/client/dist/assets/PreviewPanel-CqCa92Tf.js +32 -0
  73. package/client/dist/assets/SetupForm-S0g6u5yT.js +1 -0
  74. package/client/dist/assets/WorkflowsPanel-CouH9JDO.js +1 -0
  75. package/client/dist/assets/index-BFuqS0tY.css +1 -0
  76. package/client/dist/assets/index-CNDcVl2g.js +68 -0
  77. package/client/dist/assets/pdf-CE_K4jFx.js +12 -0
  78. package/client/dist/assets/vendor-canvas-BZV40eAE.css +1 -0
  79. package/client/dist/assets/vendor-canvas-D39yWul6.js +49 -0
  80. package/client/dist/assets/vendor-codemirror-CbtmxxaB.js +35 -0
  81. package/client/dist/assets/vendor-diff-DNQpbhrT.js +69 -0
  82. package/client/dist/assets/vendor-i18n-DCFGyhQR.js +1 -0
  83. package/client/dist/assets/vendor-icons-BaD0x9SL.js +711 -0
  84. package/client/dist/assets/vendor-markdown-CimbIo6Y.js +296 -0
  85. package/client/dist/assets/vendor-mermaid-CH7SGc99.js +2556 -0
  86. package/client/dist/assets/vendor-react-96lCPsRK.js +67 -0
  87. package/client/dist/assets/vendor-syntax-DuHI9Ok6.js +16 -0
  88. package/client/dist/assets/vendor-xterm-CZq1hqo1.js +66 -0
  89. package/client/dist/assets/vendor-xterm-qxJ8_QYu.css +32 -0
  90. package/client/dist/clear-cache.html +85 -0
  91. package/client/dist/convert-icons.md +53 -0
  92. package/client/dist/favicon.png +0 -0
  93. package/client/dist/favicon.svg +5 -0
  94. package/client/dist/generate-icons.js +49 -0
  95. package/client/dist/icons/claude-ai-icon.svg +1 -0
  96. package/client/dist/icons/codex-white.svg +3 -0
  97. package/client/dist/icons/codex.svg +3 -0
  98. package/client/dist/icons/cursor-white.svg +12 -0
  99. package/client/dist/icons/cursor.svg +1 -0
  100. package/client/dist/icons/icon-128x128.png +0 -0
  101. package/client/dist/icons/icon-128x128.svg +5 -0
  102. package/client/dist/icons/icon-144x144.png +0 -0
  103. package/client/dist/icons/icon-144x144.svg +5 -0
  104. package/client/dist/icons/icon-152x152.png +0 -0
  105. package/client/dist/icons/icon-152x152.svg +5 -0
  106. package/client/dist/icons/icon-192x192.png +0 -0
  107. package/client/dist/icons/icon-192x192.svg +5 -0
  108. package/client/dist/icons/icon-384x384.png +0 -0
  109. package/client/dist/icons/icon-384x384.svg +5 -0
  110. package/client/dist/icons/icon-512x512.png +0 -0
  111. package/client/dist/icons/icon-512x512.svg +5 -0
  112. package/client/dist/icons/icon-72x72.png +0 -0
  113. package/client/dist/icons/icon-72x72.svg +5 -0
  114. package/client/dist/icons/icon-96x96.png +0 -0
  115. package/client/dist/icons/icon-96x96.svg +5 -0
  116. package/client/dist/icons/icon-template.svg +5 -0
  117. package/client/dist/index.html +119 -0
  118. package/client/dist/logo-128.png +0 -0
  119. package/client/dist/logo-256.png +0 -0
  120. package/client/dist/logo-32.png +0 -0
  121. package/client/dist/logo-512.png +0 -0
  122. package/client/dist/logo-64.png +0 -0
  123. package/client/dist/logo.svg +14 -0
  124. package/client/dist/manifest.json +61 -0
  125. package/client/dist/mcp-docs.html +108 -0
  126. package/client/dist/offline.html +84 -0
  127. package/client/dist/screenshots/cli-selection.png +0 -0
  128. package/client/dist/screenshots/desktop-main.png +0 -0
  129. package/client/dist/screenshots/mobile-chat.png +0 -0
  130. package/client/dist/screenshots/tools-modal.png +0 -0
  131. package/client/dist/sw.js +82 -0
  132. package/commands/upfynai-connect.md +59 -0
  133. package/commands/upfynai-disconnect.md +31 -0
  134. package/commands/upfynai-doctor.md +99 -0
  135. package/commands/upfynai-export.md +49 -0
  136. package/commands/upfynai-local.md +82 -0
  137. package/commands/upfynai-status.md +75 -0
  138. package/commands/upfynai-stop.md +49 -0
  139. package/commands/upfynai-uninstall.md +58 -0
  140. package/commands/upfynai.md +69 -0
  141. package/package.json +143 -82
  142. package/scripts/build-client.js +17 -0
  143. package/scripts/fix-node-pty.js +67 -0
  144. package/scripts/install-commands.js +78 -0
  145. package/server/agent-loop.js +242 -0
  146. package/server/auto-compact.js +99 -0
  147. package/server/claude-sdk.js +797 -0
  148. package/server/cli-ui.js +785 -0
  149. package/server/cli.js +596 -0
  150. package/server/constants/config.js +31 -0
  151. package/server/cursor-cli.js +270 -0
  152. package/server/database/auth.db +0 -0
  153. package/server/database/db.js +1391 -0
  154. package/server/database/init.sql +70 -0
  155. package/server/index.js +3799 -0
  156. package/server/load-env.js +26 -0
  157. package/server/mcp-server.js +621 -0
  158. package/server/middleware/auth.js +176 -0
  159. package/server/middleware/relayHelpers.js +44 -0
  160. package/server/middleware/sandboxRouter.js +174 -0
  161. package/server/openai-codex.js +403 -0
  162. package/server/openrouter.js +137 -0
  163. package/server/projects.js +1807 -0
  164. package/server/provider-factory.js +174 -0
  165. package/server/relay-client.js +379 -0
  166. package/server/routes/agent.js +1226 -0
  167. package/server/routes/auth.js +554 -0
  168. package/server/routes/canvas.js +53 -0
  169. package/server/routes/cli-auth.js +263 -0
  170. package/server/routes/codex.js +396 -0
  171. package/server/routes/commands.js +707 -0
  172. package/server/routes/composio.js +176 -0
  173. package/server/routes/cursor.js +770 -0
  174. package/server/routes/dashboard.js +295 -0
  175. package/server/routes/git.js +1208 -0
  176. package/server/routes/keys.js +34 -0
  177. package/server/routes/mcp-utils.js +48 -0
  178. package/server/routes/mcp.js +661 -0
  179. package/server/routes/payments.js +227 -0
  180. package/server/routes/projects.js +655 -0
  181. package/server/routes/sessions.js +146 -0
  182. package/server/routes/settings.js +261 -0
  183. package/server/routes/taskmaster.js +1928 -0
  184. package/server/routes/user.js +106 -0
  185. package/server/routes/vapi-chat.js +624 -0
  186. package/server/routes/voice.js +235 -0
  187. package/server/routes/webhooks.js +166 -0
  188. package/server/routes/workflows.js +312 -0
  189. package/server/sandbox.js +120 -0
  190. package/server/services/composio.js +204 -0
  191. package/server/services/sessionRegistry.js +139 -0
  192. package/server/services/whisperService.js +84 -0
  193. package/server/services/workflowScheduler.js +206 -0
  194. package/server/tests/relay-flow.test.js +570 -0
  195. package/server/tests/sessions.test.js +259 -0
  196. package/server/utils/commandParser.js +303 -0
  197. package/server/utils/email.js +61 -0
  198. package/server/utils/gitConfig.js +24 -0
  199. package/server/utils/mcp-detector.js +198 -0
  200. package/server/utils/taskmaster-websocket.js +129 -0
  201. package/shared/integrationCatalog.d.ts +12 -0
  202. package/shared/integrationCatalog.js +172 -0
  203. package/shared/modelConstants.js +96 -0
  204. package/bin/cli.js +0 -97
  205. package/dist/agents/claude.js +0 -229
  206. package/dist/agents/codex.js +0 -48
  207. package/dist/agents/cursor.js +0 -48
  208. package/dist/agents/detect.js +0 -51
  209. package/dist/agents/exec.js +0 -31
  210. package/dist/agents/files.js +0 -105
  211. package/dist/agents/git.js +0 -18
  212. package/dist/agents/gitagent.js +0 -67
  213. package/dist/agents/index.js +0 -88
  214. package/dist/agents/shell.js +0 -38
  215. package/dist/agents/utils.js +0 -136
  216. package/scripts/postinstall.js +0 -9
  217. package/scripts/prepublish.js +0 -58
  218. package/src/animation.js +0 -228
  219. package/src/auth.js +0 -122
  220. package/src/config.js +0 -40
  221. package/src/connect.js +0 -416
  222. package/src/launch.js +0 -78
  223. package/src/mcp.js +0 -57
  224. package/src/permissions.js +0 -140
  225. package/src/persistent-shell.js +0 -261
  226. package/src/server.js +0 -54
  227. /package/{dist → shared}/gitagent/index.js +0 -0
  228. /package/{dist → shared}/gitagent/parser.js +0 -0
  229. /package/{dist → shared}/gitagent/prompt-builder.js +0 -0
@@ -0,0 +1,770 @@
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
+ // Cloud mode: read config from user's machine via relay
23
+ if (req.isCloud) {
24
+ if (!req.requireRelay()) return;
25
+ try {
26
+ const result = await req.sendRelay('file-read', { filePath: '~/.cursor/cli-config.json' }, 15000);
27
+ const config = JSON.parse(result.content);
28
+ return res.json({ success: true, config, path: '~/.cursor/cli-config.json' });
29
+ } catch {
30
+ return res.json({
31
+ success: true,
32
+ config: { version: 1, model: { modelId: CURSOR_MODELS.DEFAULT, displayName: "GPT-5" }, permissions: { allow: [], deny: [] } },
33
+ isDefault: true
34
+ });
35
+ }
36
+ }
37
+
38
+ // Local mode
39
+ const configPath = path.join(os.homedir(), '.cursor', 'cli-config.json');
40
+ try {
41
+ const configContent = await fs.readFile(configPath, 'utf8');
42
+ const config = JSON.parse(configContent);
43
+ res.json({ success: true, config, path: configPath });
44
+ } catch (error) {
45
+ res.json({
46
+ success: true,
47
+ config: { version: 1, model: { modelId: CURSOR_MODELS.DEFAULT, displayName: "GPT-5" }, permissions: { allow: [], deny: [] } },
48
+ isDefault: true
49
+ });
50
+ }
51
+ } catch (error) {
52
+ res.status(500).json({ error: 'Failed to read Cursor configuration', details: 'An error occurred' });
53
+ }
54
+ });
55
+
56
+ // POST /api/cursor/config - Update Cursor CLI configuration
57
+ router.post('/config', async (req, res) => {
58
+ try {
59
+ const { permissions, model } = req.body;
60
+
61
+ // Cloud mode: read/write config on user's machine via relay
62
+ if (req.isCloud) {
63
+ if (!req.requireRelay()) return;
64
+ let config = { version: 1, editor: { vimMode: false }, hasChangedDefaultModel: false, privacyCache: { ghostMode: false, privacyMode: 3, updatedAt: Date.now() } };
65
+ try {
66
+ const result = await req.sendRelay('file-read', { filePath: '~/.cursor/cli-config.json' }, 15000);
67
+ config = JSON.parse(result.content);
68
+ } catch { /* use defaults */ }
69
+ if (permissions) config.permissions = { allow: permissions.allow || [], deny: permissions.deny || [] };
70
+ if (model) { config.model = model; config.hasChangedDefaultModel = true; }
71
+ await req.sendRelay('create-folder', { folderPath: '~/.cursor' }, 10000);
72
+ await req.sendRelay('file-write', { filePath: '~/.cursor/cli-config.json', content: JSON.stringify(config, null, 2) }, 15000);
73
+ return res.json({ success: true, config, message: 'Cursor configuration updated successfully' });
74
+ }
75
+
76
+ // Local mode
77
+ const configPath = path.join(os.homedir(), '.cursor', 'cli-config.json');
78
+ let config = { version: 1, editor: { vimMode: false }, hasChangedDefaultModel: false, privacyCache: { ghostMode: false, privacyMode: 3, updatedAt: Date.now() } };
79
+ try {
80
+ const existing = await fs.readFile(configPath, 'utf8');
81
+ config = JSON.parse(existing);
82
+ } catch (error) { /* defaults */ }
83
+ if (permissions) config.permissions = { allow: permissions.allow || [], deny: permissions.deny || [] };
84
+ if (model) { config.model = model; config.hasChangedDefaultModel = true; }
85
+ const configDir = path.dirname(configPath);
86
+ await fs.mkdir(configDir, { recursive: true });
87
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2));
88
+ res.json({ success: true, config, message: 'Cursor configuration updated successfully' });
89
+ } catch (error) {
90
+ res.status(500).json({ error: 'Failed to update Cursor configuration', details: 'An error occurred' });
91
+ }
92
+ });
93
+
94
+ // GET /api/cursor/mcp - Read Cursor MCP servers configuration
95
+ router.get('/mcp', async (req, res) => {
96
+ try {
97
+ // Cloud mode: read from user's machine
98
+ if (req.isCloud) {
99
+ if (!req.requireRelay()) return;
100
+ try {
101
+ const result = await req.sendRelay('file-read', { filePath: '~/.cursor/mcp.json' }, 15000);
102
+ const mcpConfig = JSON.parse(result.content);
103
+ const servers = [];
104
+ if (mcpConfig.mcpServers && typeof mcpConfig.mcpServers === 'object') {
105
+ for (const [name, config] of Object.entries(mcpConfig.mcpServers)) {
106
+ const server = { id: name, name, type: 'stdio', scope: 'cursor', config: {}, raw: config };
107
+ if (config.command) { server.type = 'stdio'; server.config.command = config.command; server.config.args = config.args || []; server.config.env = config.env || {}; }
108
+ else if (config.url) { server.type = config.transport || 'http'; server.config.url = config.url; server.config.headers = config.headers || {}; }
109
+ servers.push(server);
110
+ }
111
+ }
112
+ return res.json({ success: true, servers, path: '~/.cursor/mcp.json' });
113
+ } catch {
114
+ return res.json({ success: true, servers: [], isDefault: true });
115
+ }
116
+ }
117
+
118
+ // Local mode
119
+ const mcpPath = path.join(os.homedir(), '.cursor', 'mcp.json');
120
+
121
+ try {
122
+ const mcpContent = await fs.readFile(mcpPath, 'utf8');
123
+ const mcpConfig = JSON.parse(mcpContent);
124
+
125
+ // Convert to UI-friendly format
126
+ const servers = [];
127
+ if (mcpConfig.mcpServers && typeof mcpConfig.mcpServers === 'object') {
128
+ for (const [name, config] of Object.entries(mcpConfig.mcpServers)) {
129
+ const server = {
130
+ id: name,
131
+ name: name,
132
+ type: 'stdio',
133
+ scope: 'cursor',
134
+ config: {},
135
+ raw: config
136
+ };
137
+
138
+ // Determine transport type and extract config
139
+ if (config.command) {
140
+ server.type = 'stdio';
141
+ server.config.command = config.command;
142
+ server.config.args = config.args || [];
143
+ server.config.env = config.env || {};
144
+ } else if (config.url) {
145
+ server.type = config.transport || 'http';
146
+ server.config.url = config.url;
147
+ server.config.headers = config.headers || {};
148
+ }
149
+
150
+ servers.push(server);
151
+ }
152
+ }
153
+
154
+ res.json({
155
+ success: true,
156
+ servers: servers,
157
+ path: mcpPath
158
+ });
159
+ } catch (error) {
160
+ // MCP config doesn't exist
161
+ // MCP config not found
162
+ res.json({
163
+ success: true,
164
+ servers: [],
165
+ isDefault: true
166
+ });
167
+ }
168
+ } catch (error) {
169
+ // MCP config read error
170
+ res.status(500).json({
171
+ error: 'Failed to read Cursor MCP configuration',
172
+ details: 'An error occurred'
173
+ });
174
+ }
175
+ });
176
+
177
+ // POST /api/cursor/mcp/add - Add MCP server to Cursor configuration
178
+ router.post('/mcp/add', async (req, res) => {
179
+ try {
180
+ const { name, type = 'stdio', command, args = [], url, headers = {}, env = {} } = req.body;
181
+
182
+ // Build server config
183
+ let serverConfig = {};
184
+ if (type === 'stdio') { serverConfig = { command, args, env }; }
185
+ else if (type === 'http' || type === 'sse') { serverConfig = { url, transport: type, headers }; }
186
+
187
+ // Cloud mode: read/write on user's machine
188
+ if (req.isCloud) {
189
+ if (!req.requireRelay()) return;
190
+ let mcpConfig = { mcpServers: {} };
191
+ try {
192
+ const result = await req.sendRelay('file-read', { filePath: '~/.cursor/mcp.json' }, 15000);
193
+ mcpConfig = JSON.parse(result.content);
194
+ if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
195
+ } catch { /* new config */ }
196
+ mcpConfig.mcpServers[name] = serverConfig;
197
+ await req.sendRelay('create-folder', { folderPath: '~/.cursor' }, 10000);
198
+ await req.sendRelay('file-write', { filePath: '~/.cursor/mcp.json', content: JSON.stringify(mcpConfig, null, 2) }, 15000);
199
+ return res.json({ success: true, message: `MCP server "${name}" added to Cursor configuration`, config: mcpConfig });
200
+ }
201
+
202
+ // Local mode
203
+ const mcpPath = path.join(os.homedir(), '.cursor', 'mcp.json');
204
+ let mcpConfig = { mcpServers: {} };
205
+ try {
206
+ const existing = await fs.readFile(mcpPath, 'utf8');
207
+ mcpConfig = JSON.parse(existing);
208
+ if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
209
+ } catch (error) { /* new config */ }
210
+ mcpConfig.mcpServers[name] = serverConfig;
211
+ await fs.mkdir(path.dirname(mcpPath), { recursive: true });
212
+ await fs.writeFile(mcpPath, JSON.stringify(mcpConfig, null, 2));
213
+ res.json({ success: true, message: `MCP server "${name}" added to Cursor configuration`, config: mcpConfig });
214
+ } catch (error) {
215
+ res.status(500).json({ error: 'Failed to add MCP server', details: 'An error occurred' });
216
+ }
217
+ });
218
+
219
+ // DELETE /api/cursor/mcp/:name - Remove MCP server from Cursor configuration
220
+ router.delete('/mcp/:name', async (req, res) => {
221
+ try {
222
+ const { name } = req.params;
223
+
224
+ // Cloud mode
225
+ if (req.isCloud) {
226
+ if (!req.requireRelay()) return;
227
+ let mcpConfig;
228
+ try {
229
+ const result = await req.sendRelay('file-read', { filePath: '~/.cursor/mcp.json' }, 15000);
230
+ mcpConfig = JSON.parse(result.content);
231
+ } catch {
232
+ return res.status(404).json({ error: 'Cursor MCP configuration not found' });
233
+ }
234
+ if (!mcpConfig.mcpServers || !mcpConfig.mcpServers[name]) {
235
+ return res.status(404).json({ error: `MCP server "${name}" not found in Cursor configuration` });
236
+ }
237
+ delete mcpConfig.mcpServers[name];
238
+ await req.sendRelay('file-write', { filePath: '~/.cursor/mcp.json', content: JSON.stringify(mcpConfig, null, 2) }, 15000);
239
+ return res.json({ success: true, message: `MCP server "${name}" removed from Cursor configuration`, config: mcpConfig });
240
+ }
241
+
242
+ // Local mode
243
+ const mcpPath = path.join(os.homedir(), '.cursor', 'mcp.json');
244
+ let mcpConfig;
245
+ try {
246
+ const existing = await fs.readFile(mcpPath, 'utf8');
247
+ mcpConfig = JSON.parse(existing);
248
+ } catch (error) {
249
+ return res.status(404).json({ error: 'Cursor MCP configuration not found' });
250
+ }
251
+ if (!mcpConfig.mcpServers || !mcpConfig.mcpServers[name]) {
252
+ return res.status(404).json({ error: `MCP server "${name}" not found in Cursor configuration` });
253
+ }
254
+ delete mcpConfig.mcpServers[name];
255
+ await fs.writeFile(mcpPath, JSON.stringify(mcpConfig, null, 2));
256
+ res.json({ success: true, message: `MCP server "${name}" removed from Cursor configuration`, config: mcpConfig });
257
+ } catch (error) {
258
+ res.status(500).json({ error: 'Failed to remove MCP server', details: 'An error occurred' });
259
+ }
260
+ });
261
+
262
+ // POST /api/cursor/mcp/add-json - Add MCP server using JSON format
263
+ router.post('/mcp/add-json', async (req, res) => {
264
+ try {
265
+ const { name, jsonConfig } = req.body;
266
+
267
+ let parsedConfig;
268
+ try {
269
+ parsedConfig = typeof jsonConfig === 'string' ? JSON.parse(jsonConfig) : jsonConfig;
270
+ } catch (parseError) {
271
+ return res.status(400).json({ error: 'Invalid JSON configuration', details: parseError.message });
272
+ }
273
+
274
+ // Cloud mode
275
+ if (req.isCloud) {
276
+ if (!req.requireRelay()) return;
277
+ let mcpConfig = { mcpServers: {} };
278
+ try {
279
+ const result = await req.sendRelay('file-read', { filePath: '~/.cursor/mcp.json' }, 15000);
280
+ mcpConfig = JSON.parse(result.content);
281
+ if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
282
+ } catch { /* new config */ }
283
+ mcpConfig.mcpServers[name] = parsedConfig;
284
+ await req.sendRelay('create-folder', { folderPath: '~/.cursor' }, 10000);
285
+ await req.sendRelay('file-write', { filePath: '~/.cursor/mcp.json', content: JSON.stringify(mcpConfig, null, 2) }, 15000);
286
+ return res.json({ success: true, message: `MCP server "${name}" added to Cursor configuration`, config: mcpConfig });
287
+ }
288
+
289
+ // Local mode
290
+ const mcpPath = path.join(os.homedir(), '.cursor', 'mcp.json');
291
+ let mcpConfig = { mcpServers: {} };
292
+ try {
293
+ const existing = await fs.readFile(mcpPath, 'utf8');
294
+ mcpConfig = JSON.parse(existing);
295
+ if (!mcpConfig.mcpServers) mcpConfig.mcpServers = {};
296
+ } catch (error) { /* new config */ }
297
+ mcpConfig.mcpServers[name] = parsedConfig;
298
+ await fs.mkdir(path.dirname(mcpPath), { recursive: true });
299
+ await fs.writeFile(mcpPath, JSON.stringify(mcpConfig, null, 2));
300
+
301
+ res.json({
302
+ success: true,
303
+ message: `MCP server "${name}" added to Cursor configuration via JSON`,
304
+ config: mcpConfig
305
+ });
306
+ } catch (error) {
307
+ // MCP server JSON add error
308
+ res.status(500).json({
309
+ error: 'Failed to add MCP server',
310
+ details: 'An error occurred'
311
+ });
312
+ }
313
+ });
314
+
315
+ // GET /api/cursor/sessions - Get Cursor sessions from SQLite database
316
+ router.get('/sessions', async (req, res) => {
317
+ try {
318
+ const { projectPath } = req.query;
319
+
320
+ // Calculate cwdID hash for the project path (Cursor uses MD5 hash)
321
+ const cwdId = crypto.createHash('md5').update(projectPath || process.cwd()).digest('hex');
322
+ const cursorChatsPath = path.join(os.homedir(), '.cursor', 'chats', cwdId);
323
+
324
+
325
+ // Check if the directory exists
326
+ try {
327
+ await fs.access(cursorChatsPath);
328
+ } catch (error) {
329
+ // No sessions for this project
330
+ return res.json({
331
+ success: true,
332
+ sessions: [],
333
+ cwdId: cwdId,
334
+ path: cursorChatsPath
335
+ });
336
+ }
337
+
338
+ // List all session directories
339
+ const sessionDirs = await fs.readdir(cursorChatsPath);
340
+ const sessions = [];
341
+
342
+ for (const sessionId of sessionDirs) {
343
+ const sessionPath = path.join(cursorChatsPath, sessionId);
344
+ const storeDbPath = path.join(sessionPath, 'store.db');
345
+ let dbStatMtimeMs = null;
346
+
347
+ try {
348
+ // Check if store.db exists
349
+ await fs.access(storeDbPath);
350
+
351
+ // Capture store.db mtime as a reliable fallback timestamp (last activity)
352
+ try {
353
+ const stat = await fs.stat(storeDbPath);
354
+ dbStatMtimeMs = stat.mtimeMs;
355
+ } catch (_) {}
356
+
357
+ // Open SQLite database
358
+ if (!sqliteOpen || !sqlite3) {
359
+ continue; // Skip on Vercel where native modules aren't available
360
+ }
361
+ const db = await sqliteOpen({
362
+ filename: storeDbPath,
363
+ driver: sqlite3.Database,
364
+ mode: sqlite3.OPEN_READONLY
365
+ });
366
+
367
+ // Get metadata from meta table
368
+ const metaRows = await db.all(`
369
+ SELECT key, value FROM meta
370
+ `);
371
+
372
+ let sessionData = {
373
+ id: sessionId,
374
+ name: 'Untitled Session',
375
+ createdAt: null,
376
+ mode: null,
377
+ projectPath: projectPath,
378
+ lastMessage: null,
379
+ messageCount: 0
380
+ };
381
+
382
+ // Parse meta table entries
383
+ for (const row of metaRows) {
384
+ if (row.value) {
385
+ try {
386
+ // Try to decode as hex-encoded JSON
387
+ const hexMatch = row.value.toString().match(/^[0-9a-fA-F]+$/);
388
+ if (hexMatch) {
389
+ const jsonStr = Buffer.from(row.value, 'hex').toString('utf8');
390
+ const data = JSON.parse(jsonStr);
391
+
392
+ if (row.key === 'agent') {
393
+ sessionData.name = data.name || sessionData.name;
394
+ // Normalize createdAt to ISO string in milliseconds
395
+ let createdAt = data.createdAt;
396
+ if (typeof createdAt === 'number') {
397
+ if (createdAt < 1e12) {
398
+ createdAt = createdAt * 1000; // seconds -> ms
399
+ }
400
+ sessionData.createdAt = new Date(createdAt).toISOString();
401
+ } else if (typeof createdAt === 'string') {
402
+ const n = Number(createdAt);
403
+ if (!Number.isNaN(n)) {
404
+ const ms = n < 1e12 ? n * 1000 : n;
405
+ sessionData.createdAt = new Date(ms).toISOString();
406
+ } else {
407
+ // Assume it's already an ISO/date string
408
+ const d = new Date(createdAt);
409
+ sessionData.createdAt = isNaN(d.getTime()) ? null : d.toISOString();
410
+ }
411
+ } else {
412
+ sessionData.createdAt = sessionData.createdAt || null;
413
+ }
414
+ sessionData.mode = data.mode;
415
+ sessionData.agentId = data.agentId;
416
+ sessionData.latestRootBlobId = data.latestRootBlobId;
417
+ }
418
+ } else {
419
+ // If not hex, use raw value for simple keys
420
+ if (row.key === 'name') {
421
+ sessionData.name = row.value.toString();
422
+ }
423
+ }
424
+ } catch (e) {
425
+ // meta parse error
426
+ }
427
+ }
428
+ }
429
+
430
+ // Get message count from JSON blobs only (actual messages, not DAG structure)
431
+ try {
432
+ const blobCount = await db.get(`
433
+ SELECT COUNT(*) as count
434
+ FROM blobs
435
+ WHERE substr(data, 1, 1) = X'7B'
436
+ `);
437
+ sessionData.messageCount = blobCount.count;
438
+
439
+ // Get the most recent JSON blob for preview (actual message, not DAG structure)
440
+ const lastBlob = await db.get(`
441
+ SELECT data FROM blobs
442
+ WHERE substr(data, 1, 1) = X'7B'
443
+ ORDER BY rowid DESC
444
+ LIMIT 1
445
+ `);
446
+
447
+ if (lastBlob && lastBlob.data) {
448
+ try {
449
+ // Try to extract readable preview from blob (may contain binary with embedded JSON)
450
+ const raw = lastBlob.data.toString('utf8');
451
+ let preview = '';
452
+ // Attempt direct JSON parse
453
+ try {
454
+ const parsed = JSON.parse(raw);
455
+ if (parsed?.content) {
456
+ if (Array.isArray(parsed.content)) {
457
+ const firstText = parsed.content.find(p => p?.type === 'text' && p.text)?.text || '';
458
+ preview = firstText;
459
+ } else if (typeof parsed.content === 'string') {
460
+ preview = parsed.content;
461
+ }
462
+ }
463
+ } catch (_) {}
464
+ if (!preview) {
465
+ // Strip non-printable and try to find JSON chunk
466
+ const cleaned = raw.replace(/[^\x09\x0A\x0D\x20-\x7E]/g, '');
467
+ const s = cleaned;
468
+ const start = s.indexOf('{');
469
+ const end = s.lastIndexOf('}');
470
+ if (start !== -1 && end > start) {
471
+ const jsonStr = s.slice(start, end + 1);
472
+ try {
473
+ const parsed = JSON.parse(jsonStr);
474
+ if (parsed?.content) {
475
+ if (Array.isArray(parsed.content)) {
476
+ const firstText = parsed.content.find(p => p?.type === 'text' && p.text)?.text || '';
477
+ preview = firstText;
478
+ } else if (typeof parsed.content === 'string') {
479
+ preview = parsed.content;
480
+ }
481
+ }
482
+ } catch (_) {
483
+ preview = s;
484
+ }
485
+ } else {
486
+ preview = s;
487
+ }
488
+ }
489
+ if (preview && preview.length > 0) {
490
+ sessionData.lastMessage = preview.substring(0, 100) + (preview.length > 100 ? '...' : '');
491
+ }
492
+ } catch (e) {
493
+ // blob parse error
494
+ }
495
+ }
496
+ } catch (e) {
497
+ // blobs read error
498
+ }
499
+
500
+ await db.close();
501
+
502
+ // Finalize createdAt: use parsed meta value when valid, else fall back to store.db mtime
503
+ if (!sessionData.createdAt) {
504
+ if (dbStatMtimeMs && Number.isFinite(dbStatMtimeMs)) {
505
+ sessionData.createdAt = new Date(dbStatMtimeMs).toISOString();
506
+ }
507
+ }
508
+
509
+ sessions.push(sessionData);
510
+
511
+ } catch (error) {
512
+ // session read error
513
+ }
514
+ }
515
+
516
+ // Fallback: ensure createdAt is a valid ISO string (use session directory mtime as last resort)
517
+ for (const s of sessions) {
518
+ if (!s.createdAt) {
519
+ try {
520
+ const sessionDir = path.join(cursorChatsPath, s.id);
521
+ const st = await fs.stat(sessionDir);
522
+ s.createdAt = new Date(st.mtimeMs).toISOString();
523
+ } catch {
524
+ s.createdAt = new Date().toISOString();
525
+ }
526
+ }
527
+ }
528
+ // Sort sessions by creation date (newest first)
529
+ sessions.sort((a, b) => {
530
+ if (!a.createdAt) return 1;
531
+ if (!b.createdAt) return -1;
532
+ return new Date(b.createdAt) - new Date(a.createdAt);
533
+ });
534
+
535
+ res.json({
536
+ success: true,
537
+ sessions: sessions,
538
+ cwdId: cwdId,
539
+ path: cursorChatsPath
540
+ });
541
+
542
+ } catch (error) {
543
+ // sessions read error
544
+ res.status(500).json({
545
+ error: 'Failed to read Cursor sessions',
546
+ details: 'An error occurred'
547
+ });
548
+ }
549
+ });
550
+
551
+ // GET /api/cursor/sessions/:sessionId - Get specific Cursor session from SQLite
552
+ router.get('/sessions/:sessionId', async (req, res) => {
553
+ try {
554
+ const { sessionId } = req.params;
555
+ const { projectPath } = req.query;
556
+
557
+ // Calculate cwdID hash for the project path
558
+ const cwdId = crypto.createHash('md5').update(projectPath || process.cwd()).digest('hex');
559
+ const storeDbPath = path.join(os.homedir(), '.cursor', 'chats', cwdId, sessionId, 'store.db');
560
+
561
+
562
+ // Open SQLite database (requires native sqlite3 module)
563
+ if (!sqliteOpen || !sqlite3) {
564
+ return res.status(503).json({ error: 'SQLite not available on this deployment' });
565
+ }
566
+ const db = await sqliteOpen({
567
+ filename: storeDbPath,
568
+ driver: sqlite3.Database,
569
+ mode: sqlite3.OPEN_READONLY
570
+ });
571
+
572
+ // Get all blobs to build the DAG structure
573
+ const allBlobs = await db.all(`
574
+ SELECT rowid, id, data FROM blobs
575
+ `);
576
+
577
+ // Build the DAG structure from parent-child relationships
578
+ const blobMap = new Map(); // id -> blob data
579
+ const parentRefs = new Map(); // blob id -> [parent blob ids]
580
+ const childRefs = new Map(); // blob id -> [child blob ids]
581
+ const jsonBlobs = []; // Clean JSON messages
582
+
583
+ for (const blob of allBlobs) {
584
+ blobMap.set(blob.id, blob);
585
+
586
+ // Check if this is a JSON blob (actual message) or protobuf (DAG structure)
587
+ if (blob.data && blob.data[0] === 0x7B) { // Starts with '{' - JSON blob
588
+ try {
589
+ const parsed = JSON.parse(blob.data.toString('utf8'));
590
+ jsonBlobs.push({ ...blob, parsed });
591
+ } catch (e) {
592
+ // JSON blob parse failed
593
+ }
594
+ } else if (blob.data) { // Protobuf blob - extract parent references
595
+ const parents = [];
596
+ let i = 0;
597
+
598
+ // Scan for parent references (0x0A 0x20 followed by 32-byte hash)
599
+ while (i < blob.data.length - 33) {
600
+ if (blob.data[i] === 0x0A && blob.data[i+1] === 0x20) {
601
+ const parentHash = blob.data.slice(i+2, i+34).toString('hex');
602
+ if (blobMap.has(parentHash)) {
603
+ parents.push(parentHash);
604
+ }
605
+ i += 34;
606
+ } else {
607
+ i++;
608
+ }
609
+ }
610
+
611
+ if (parents.length > 0) {
612
+ parentRefs.set(blob.id, parents);
613
+ // Update child references
614
+ for (const parentId of parents) {
615
+ if (!childRefs.has(parentId)) {
616
+ childRefs.set(parentId, []);
617
+ }
618
+ childRefs.get(parentId).push(blob.id);
619
+ }
620
+ }
621
+ }
622
+ }
623
+
624
+ // Perform topological sort to get chronological order
625
+ const visited = new Set();
626
+ const sorted = [];
627
+
628
+ // DFS-based topological sort
629
+ function visit(nodeId) {
630
+ if (visited.has(nodeId)) return;
631
+ visited.add(nodeId);
632
+
633
+ // Visit all parents first (dependencies)
634
+ const parents = parentRefs.get(nodeId) || [];
635
+ for (const parentId of parents) {
636
+ visit(parentId);
637
+ }
638
+
639
+ // Add this node after all its parents
640
+ const blob = blobMap.get(nodeId);
641
+ if (blob) {
642
+ sorted.push(blob);
643
+ }
644
+ }
645
+
646
+ // Start with nodes that have no parents (roots)
647
+ for (const blob of allBlobs) {
648
+ if (!parentRefs.has(blob.id)) {
649
+ visit(blob.id);
650
+ }
651
+ }
652
+
653
+ // Visit any remaining nodes (disconnected components)
654
+ for (const blob of allBlobs) {
655
+ visit(blob.id);
656
+ }
657
+
658
+ // Now extract JSON messages in the order they appear in the sorted DAG
659
+ const messageOrder = new Map(); // JSON blob id -> order index
660
+ let orderIndex = 0;
661
+
662
+ for (const blob of sorted) {
663
+ // Check if this blob references any JSON messages
664
+ if (blob.data && blob.data[0] !== 0x7B) { // Protobuf blob
665
+ // Look for JSON blob references
666
+ for (const jsonBlob of jsonBlobs) {
667
+ try {
668
+ const jsonIdBytes = Buffer.from(jsonBlob.id, 'hex');
669
+ if (blob.data.includes(jsonIdBytes)) {
670
+ if (!messageOrder.has(jsonBlob.id)) {
671
+ messageOrder.set(jsonBlob.id, orderIndex++);
672
+ }
673
+ }
674
+ } catch (e) {
675
+ // Skip if can't convert ID
676
+ }
677
+ }
678
+ }
679
+ }
680
+
681
+ // Sort JSON blobs by their appearance order in the DAG
682
+ const sortedJsonBlobs = jsonBlobs.sort((a, b) => {
683
+ const orderA = messageOrder.get(a.id) ?? Number.MAX_SAFE_INTEGER;
684
+ const orderB = messageOrder.get(b.id) ?? Number.MAX_SAFE_INTEGER;
685
+ if (orderA !== orderB) return orderA - orderB;
686
+ // Fallback to rowid if not in order map
687
+ return a.rowid - b.rowid;
688
+ });
689
+
690
+ // Use sorted JSON blobs
691
+ const blobs = sortedJsonBlobs.map((blob, idx) => ({
692
+ ...blob,
693
+ sequence_num: idx + 1,
694
+ original_rowid: blob.rowid
695
+ }));
696
+
697
+ // Get metadata from meta table
698
+ const metaRows = await db.all(`
699
+ SELECT key, value FROM meta
700
+ `);
701
+
702
+ // Parse metadata
703
+ let metadata = {};
704
+ for (const row of metaRows) {
705
+ if (row.value) {
706
+ try {
707
+ // Try to decode as hex-encoded JSON
708
+ const hexMatch = row.value.toString().match(/^[0-9a-fA-F]+$/);
709
+ if (hexMatch) {
710
+ const jsonStr = Buffer.from(row.value, 'hex').toString('utf8');
711
+ metadata[row.key] = JSON.parse(jsonStr);
712
+ } else {
713
+ metadata[row.key] = row.value.toString();
714
+ }
715
+ } catch (e) {
716
+ metadata[row.key] = row.value.toString();
717
+ }
718
+ }
719
+ }
720
+
721
+ // Extract messages from sorted JSON blobs
722
+ const messages = [];
723
+ for (const blob of blobs) {
724
+ try {
725
+ // We already parsed JSON blobs earlier
726
+ const parsed = blob.parsed;
727
+
728
+ if (parsed) {
729
+ // Filter out ONLY system messages at the server level
730
+ // Check both direct role and nested message.role
731
+ const role = parsed?.role || parsed?.message?.role;
732
+ if (role === 'system') {
733
+ continue; // Skip only system messages
734
+ }
735
+ messages.push({
736
+ id: blob.id,
737
+ sequence: blob.sequence_num,
738
+ rowid: blob.original_rowid,
739
+ content: parsed
740
+ });
741
+ }
742
+ } catch (e) {
743
+ // Skip blobs that cause errors
744
+ // blob skipped
745
+ }
746
+ }
747
+
748
+ await db.close();
749
+
750
+ res.json({
751
+ success: true,
752
+ session: {
753
+ id: sessionId,
754
+ projectPath: projectPath,
755
+ messages: messages,
756
+ metadata: metadata,
757
+ cwdId: cwdId
758
+ }
759
+ });
760
+
761
+ } catch (error) {
762
+ // session read error
763
+ res.status(500).json({
764
+ error: 'Failed to read Cursor session',
765
+ details: 'An error occurred'
766
+ });
767
+ }
768
+ });
769
+
770
+ export default router;