upfynai-code 3.0.4 → 3.2.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 (258) hide show
  1. package/README.md +69 -92
  2. package/bin/cli.js +191 -0
  3. package/dist/client/assets/AppContent-M14Au3SB.js +542 -0
  4. package/{client/dist/assets/BrowserPanel-0TLEl-IC.js → dist/client/assets/BrowserPanel-TFKm2NDJ.js} +2 -2
  5. package/dist/client/assets/DashboardPanel-C88HjsCh.js +1 -0
  6. package/dist/client/assets/FileTree-DvO1xnDE.js +1 -0
  7. package/{client/dist/assets/GitPanel-C_xFM-N2.js → dist/client/assets/GitPanel-D-slVlyy.js} +2 -2
  8. package/dist/client/assets/LoginModal-Chi4SYcr.js +21 -0
  9. package/{client/dist/assets/MarkdownPreview-CESjI261.js → dist/client/assets/MarkdownPreview-CuIix2u9.js} +1 -1
  10. package/dist/client/assets/MermaidBlock-Dq9uFv82.js +2 -0
  11. package/dist/client/assets/Onboarding-QYXx24dX.js +1 -0
  12. package/{client/dist/assets/PreviewPanel-CqCa92Tf.js → dist/client/assets/PreviewPanel-Dd8q-jo0.js} +1 -1
  13. package/dist/client/assets/SetupForm-CrspaUva.js +1 -0
  14. package/dist/client/assets/WorkflowsPanel-DIlYAdhB.js +1 -0
  15. package/dist/client/assets/index-CnNNzw9A.css +1 -0
  16. package/{client/dist/assets/index-HaY-3pK1.js → dist/client/assets/index-rUkK9FDP.js} +26 -26
  17. package/{client/dist/assets/vendor-codemirror-D2ALgpaX.js → dist/client/assets/vendor-codemirror-jc6nyJQg.js} +1 -1
  18. package/{client/dist/assets/vendor-diff-DNQpbhrT.js → dist/client/assets/vendor-diff-THJmAcEI.js} +1 -1
  19. package/{client/dist/assets/vendor-icons-GyYE35HP.js → dist/client/assets/vendor-icons-CfjIpdrD.js} +145 -155
  20. package/{client/dist/assets/vendor-markdown-CimbIo6Y.js → dist/client/assets/vendor-markdown-Cdm6NEGf.js} +1 -1
  21. package/dist/client/assets/vendor-mermaid-DTPaBx-U.js +2559 -0
  22. package/{client/dist/assets/vendor-react-96lCPsRK.js → dist/client/assets/vendor-react-wFkb6mSf.js} +1 -1
  23. package/{client/dist/assets/vendor-syntax-LS_Nt30I.js → dist/client/assets/vendor-syntax-C_UZR7tc.js} +1 -1
  24. package/dist/client/favicon.png +0 -0
  25. package/dist/client/icons/icon-128x128.png +0 -0
  26. package/dist/client/icons/icon-144x144.png +0 -0
  27. package/dist/client/icons/icon-152x152.png +0 -0
  28. package/dist/client/icons/icon-192x192.png +0 -0
  29. package/dist/client/icons/icon-384x384.png +0 -0
  30. package/dist/client/icons/icon-512x512.png +0 -0
  31. package/dist/client/icons/icon-72x72.png +0 -0
  32. package/dist/client/icons/icon-96x96.png +0 -0
  33. package/{client/dist → dist/client}/index.html +37 -36
  34. package/dist/client/logo-128.png +0 -0
  35. package/dist/client/logo-256.png +0 -0
  36. package/dist/client/logo-32.png +0 -0
  37. package/dist/client/logo-512.png +0 -0
  38. package/dist/client/logo-64.png +0 -0
  39. package/dist/client/logo.png +0 -0
  40. package/{client/dist → dist/client}/manifest.json +12 -12
  41. package/{client/dist → dist/client}/mcp-docs.html +1 -1
  42. package/{client/dist → dist/client}/sw.js +2 -2
  43. package/package.json +56 -105
  44. package/scripts/postinstall.js +9 -0
  45. package/scripts/prepublish.js +77 -0
  46. package/src/animation.js +228 -0
  47. package/src/auth.js +142 -0
  48. package/src/config.js +40 -0
  49. package/src/connect.js +416 -0
  50. package/src/launch.js +81 -0
  51. package/src/mcp.js +57 -0
  52. package/src/permissions.js +140 -0
  53. package/src/persistent-shell.js +261 -0
  54. package/src/server.js +54 -0
  55. package/client/dist/assets/AppContent-CwrTP6TW.js +0 -545
  56. package/client/dist/assets/CanvasFullScreen-D1GWQsGL.js +0 -1
  57. package/client/dist/assets/CanvasWorkspace-D7ORj358.js +0 -163
  58. package/client/dist/assets/DashboardPanel-BV7ybUDe.js +0 -1
  59. package/client/dist/assets/FileTree-5qfhBqdE.js +0 -1
  60. package/client/dist/assets/LoginModal-CImJHRjX.js +0 -13
  61. package/client/dist/assets/MermaidBlock-BFM21cwe.js +0 -2
  62. package/client/dist/assets/Onboarding-B3cteLu2.js +0 -1
  63. package/client/dist/assets/SetupForm-P6dsYgHO.js +0 -1
  64. package/client/dist/assets/WorkflowsPanel-CBoN80kc.js +0 -1
  65. package/client/dist/assets/index-46kkVu2i.css +0 -1
  66. package/client/dist/assets/pdf-CE_K4jFx.js +0 -12
  67. package/client/dist/assets/vendor-canvas-BZV40eAE.css +0 -1
  68. package/client/dist/assets/vendor-canvas-DvHJ_Pn2.js +0 -49
  69. package/client/dist/assets/vendor-mermaid-DucWyDEe.js +0 -2556
  70. package/client/dist/favicon.png +0 -0
  71. package/client/dist/icons/icon-128x128.png +0 -0
  72. package/client/dist/icons/icon-144x144.png +0 -0
  73. package/client/dist/icons/icon-152x152.png +0 -0
  74. package/client/dist/icons/icon-192x192.png +0 -0
  75. package/client/dist/icons/icon-384x384.png +0 -0
  76. package/client/dist/icons/icon-512x512.png +0 -0
  77. package/client/dist/icons/icon-72x72.png +0 -0
  78. package/client/dist/icons/icon-96x96.png +0 -0
  79. package/client/dist/logo-128.png +0 -0
  80. package/client/dist/logo-256.png +0 -0
  81. package/client/dist/logo-32.png +0 -0
  82. package/client/dist/logo-512.png +0 -0
  83. package/client/dist/logo-64.png +0 -0
  84. package/commands/upfynai-connect.md +0 -59
  85. package/commands/upfynai-disconnect.md +0 -31
  86. package/commands/upfynai-doctor.md +0 -99
  87. package/commands/upfynai-export.md +0 -49
  88. package/commands/upfynai-local.md +0 -82
  89. package/commands/upfynai-status.md +0 -75
  90. package/commands/upfynai-stop.md +0 -49
  91. package/commands/upfynai-uninstall.md +0 -58
  92. package/commands/upfynai.md +0 -69
  93. package/scripts/build-client.js +0 -17
  94. package/scripts/fix-node-pty.js +0 -67
  95. package/scripts/install-commands.js +0 -78
  96. package/server/agent-loop.js +0 -242
  97. package/server/auto-compact.js +0 -99
  98. package/server/browser.js +0 -131
  99. package/server/claude-sdk.js +0 -797
  100. package/server/cli-ui.js +0 -798
  101. package/server/cli.js +0 -751
  102. package/server/constants/config.js +0 -31
  103. package/server/cursor-cli.js +0 -270
  104. package/server/database/auth.db +0 -0
  105. package/server/database/db.js +0 -1547
  106. package/server/database/init.sql +0 -70
  107. package/server/index.js +0 -3813
  108. package/server/load-env.js +0 -26
  109. package/server/mcp-server.js +0 -621
  110. package/server/middleware/auth.js +0 -184
  111. package/server/middleware/relayHelpers.js +0 -44
  112. package/server/middleware/sandboxRouter.js +0 -174
  113. package/server/openai-codex.js +0 -403
  114. package/server/openrouter.js +0 -137
  115. package/server/projects.js +0 -1807
  116. package/server/provider-factory.js +0 -174
  117. package/server/relay-client.js +0 -390
  118. package/server/routes/agent.js +0 -1234
  119. package/server/routes/auth.js +0 -559
  120. package/server/routes/browser.js +0 -419
  121. package/server/routes/canvas.js +0 -53
  122. package/server/routes/cli-auth.js +0 -263
  123. package/server/routes/codex.js +0 -396
  124. package/server/routes/commands.js +0 -707
  125. package/server/routes/composio.js +0 -176
  126. package/server/routes/cursor.js +0 -770
  127. package/server/routes/dashboard.js +0 -295
  128. package/server/routes/git.js +0 -1208
  129. package/server/routes/keys.js +0 -34
  130. package/server/routes/mcp-utils.js +0 -48
  131. package/server/routes/mcp.js +0 -661
  132. package/server/routes/payments.js +0 -227
  133. package/server/routes/projects.js +0 -754
  134. package/server/routes/sessions.js +0 -146
  135. package/server/routes/settings.js +0 -261
  136. package/server/routes/taskmaster.js +0 -1928
  137. package/server/routes/user.js +0 -106
  138. package/server/routes/vapi-chat.js +0 -624
  139. package/server/routes/voice.js +0 -235
  140. package/server/routes/webhooks.js +0 -166
  141. package/server/routes/workflows.js +0 -312
  142. package/server/sandbox.js +0 -120
  143. package/server/services/browser-ai.js +0 -154
  144. package/server/services/composio.js +0 -204
  145. package/server/services/sessionRegistry.js +0 -139
  146. package/server/services/whisperService.js +0 -84
  147. package/server/services/workflowScheduler.js +0 -211
  148. package/server/tests/relay-flow.test.js +0 -570
  149. package/server/tests/sessions.test.js +0 -259
  150. package/server/utils/commandParser.js +0 -303
  151. package/server/utils/email.js +0 -66
  152. package/server/utils/gitConfig.js +0 -24
  153. package/server/utils/mcp-detector.js +0 -198
  154. package/server/utils/taskmaster-websocket.js +0 -129
  155. package/shared/integrationCatalog.d.ts +0 -12
  156. package/shared/integrationCatalog.js +0 -172
  157. package/shared/modelConstants.js +0 -96
  158. /package/{shared → dist}/agents/claude.js +0 -0
  159. /package/{shared → dist}/agents/codex.js +0 -0
  160. /package/{shared → dist}/agents/cursor.js +0 -0
  161. /package/{shared → dist}/agents/detect.js +0 -0
  162. /package/{shared → dist}/agents/exec.js +0 -0
  163. /package/{shared → dist}/agents/files.js +0 -0
  164. /package/{shared → dist}/agents/git.js +0 -0
  165. /package/{shared → dist}/agents/gitagent.js +0 -0
  166. /package/{shared → dist}/agents/index.js +0 -0
  167. /package/{shared → dist}/agents/shell.js +0 -0
  168. /package/{shared → dist}/agents/utils.js +0 -0
  169. /package/{client/dist → dist/client}/api-docs.html +0 -0
  170. /package/{client/dist → dist/client}/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  171. /package/{client/dist → dist/client}/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  172. /package/{client/dist → dist/client}/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  173. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  174. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  175. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  176. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  177. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  178. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  179. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  180. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  181. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  182. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  183. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  184. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  185. /package/{client/dist → dist/client}/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  186. /package/{client/dist → dist/client}/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  187. /package/{client/dist → dist/client}/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  188. /package/{client/dist → dist/client}/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  189. /package/{client/dist → dist/client}/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  190. /package/{client/dist → dist/client}/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  191. /package/{client/dist → dist/client}/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  192. /package/{client/dist → dist/client}/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  193. /package/{client/dist → dist/client}/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  194. /package/{client/dist → dist/client}/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  195. /package/{client/dist → dist/client}/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  196. /package/{client/dist → dist/client}/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  197. /package/{client/dist → dist/client}/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  198. /package/{client/dist → dist/client}/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  199. /package/{client/dist → dist/client}/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  200. /package/{client/dist → dist/client}/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  201. /package/{client/dist → dist/client}/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  202. /package/{client/dist → dist/client}/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  203. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  204. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  205. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  206. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  207. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  208. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  209. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  210. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  211. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  212. /package/{client/dist → dist/client}/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  213. /package/{client/dist → dist/client}/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  214. /package/{client/dist → dist/client}/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  215. /package/{client/dist → dist/client}/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  216. /package/{client/dist → dist/client}/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  217. /package/{client/dist → dist/client}/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  218. /package/{client/dist → dist/client}/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  219. /package/{client/dist → dist/client}/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  220. /package/{client/dist → dist/client}/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  221. /package/{client/dist → dist/client}/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  222. /package/{client/dist → dist/client}/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  223. /package/{client/dist → dist/client}/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  224. /package/{client/dist → dist/client}/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  225. /package/{client/dist → dist/client}/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  226. /package/{client/dist → dist/client}/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  227. /package/{client/dist → dist/client}/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  228. /package/{client/dist → dist/client}/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  229. /package/{client/dist → dist/client}/assets/vendor-i18n-DCFGyhQR.js +0 -0
  230. /package/{client/dist → dist/client}/assets/vendor-xterm-CZq1hqo1.js +0 -0
  231. /package/{client/dist → dist/client}/assets/vendor-xterm-qxJ8_QYu.css +0 -0
  232. /package/{client/dist → dist/client}/clear-cache.html +0 -0
  233. /package/{client/dist → dist/client}/convert-icons.md +0 -0
  234. /package/{client/dist → dist/client}/favicon.svg +0 -0
  235. /package/{client/dist → dist/client}/generate-icons.js +0 -0
  236. /package/{client/dist → dist/client}/icons/claude-ai-icon.svg +0 -0
  237. /package/{client/dist → dist/client}/icons/codex-white.svg +0 -0
  238. /package/{client/dist → dist/client}/icons/codex.svg +0 -0
  239. /package/{client/dist → dist/client}/icons/cursor-white.svg +0 -0
  240. /package/{client/dist → dist/client}/icons/cursor.svg +0 -0
  241. /package/{client/dist → dist/client}/icons/icon-128x128.svg +0 -0
  242. /package/{client/dist → dist/client}/icons/icon-144x144.svg +0 -0
  243. /package/{client/dist → dist/client}/icons/icon-152x152.svg +0 -0
  244. /package/{client/dist → dist/client}/icons/icon-192x192.svg +0 -0
  245. /package/{client/dist → dist/client}/icons/icon-384x384.svg +0 -0
  246. /package/{client/dist → dist/client}/icons/icon-512x512.svg +0 -0
  247. /package/{client/dist → dist/client}/icons/icon-72x72.svg +0 -0
  248. /package/{client/dist → dist/client}/icons/icon-96x96.svg +0 -0
  249. /package/{client/dist → dist/client}/icons/icon-template.svg +0 -0
  250. /package/{client/dist → dist/client}/logo.svg +0 -0
  251. /package/{client/dist → dist/client}/offline.html +0 -0
  252. /package/{client/dist → dist/client}/screenshots/cli-selection.png +0 -0
  253. /package/{client/dist → dist/client}/screenshots/desktop-main.png +0 -0
  254. /package/{client/dist → dist/client}/screenshots/mobile-chat.png +0 -0
  255. /package/{client/dist → dist/client}/screenshots/tools-modal.png +0 -0
  256. /package/{shared → dist}/gitagent/index.js +0 -0
  257. /package/{shared → dist}/gitagent/parser.js +0 -0
  258. /package/{shared → dist}/gitagent/prompt-builder.js +0 -0
@@ -1,1547 +0,0 @@
1
- // ─── Database Client Resolution ──────────────────────────────────────────────
2
- // Cloud (Railway): uses @libsql/client for Turso remote databases
3
- // Local (npm install): uses libsql (native SQLite binding) with async adapter
4
- // This avoids shipping deprecated transitive deps (node-domexception) to npm users
5
-
6
- let createClient;
7
-
8
- // 1. Try @libsql/client (cloud — installed on Railway via nixpacks)
9
- try {
10
- createClient = (await import('@libsql/client')).createClient;
11
- } catch {
12
- // 2. Fallback: use libsql native binding with async adapter (local installs)
13
- try {
14
- const DatabaseMod = await import('libsql');
15
- const Database = DatabaseMod.default || DatabaseMod;
16
- createClient = function createLocalClient({ url, authToken }) {
17
- const isRemote = url.startsWith('libsql://') || url.startsWith('https://');
18
- const dbPath = isRemote ? url : url.replace(/^file:/, '');
19
- const sqliteDb = authToken ? new Database(dbPath, { authToken }) : new Database(dbPath);
20
- // WAL mode only for local files, not remote Turso
21
- if (!isRemote) sqliteDb.pragma('journal_mode = WAL');
22
- return {
23
- execute: async (query) => {
24
- const sql = typeof query === 'string' ? query : query.sql;
25
- const args = typeof query === 'string' ? [] : (query.args || []);
26
- const stmt = sqliteDb.prepare(sql);
27
- if (sql.trim().match(/^(SELECT|PRAGMA|EXPLAIN)/i)) {
28
- const rows = args.length ? stmt.all(...args) : stmt.all();
29
- return { rows, columns: rows.length > 0 ? Object.keys(rows[0]) : [] };
30
- }
31
- const info = args.length ? stmt.run(...args) : stmt.run();
32
- return { rows: [], columns: [], rowsAffected: info.changes, lastInsertRowid: info.lastInsertRowid };
33
- },
34
- batch: async (queries) => {
35
- const results = [];
36
- for (const q of queries) {
37
- // Re-use the adapter's own execute method
38
- const sql = typeof q === 'string' ? q : q.sql;
39
- const args = typeof q === 'string' ? [] : (q.args || []);
40
- const stmt = sqliteDb.prepare(sql);
41
- if (sql.trim().match(/^(SELECT|PRAGMA|EXPLAIN)/i)) {
42
- const rows = args.length ? stmt.all(...args) : stmt.all();
43
- results.push({ rows, columns: rows.length > 0 ? Object.keys(rows[0]) : [] });
44
- } else {
45
- const info = args.length ? stmt.run(...args) : stmt.run();
46
- results.push({ rows: [], columns: [], rowsAffected: info.changes, lastInsertRowid: info.lastInsertRowid });
47
- }
48
- }
49
- return results;
50
- },
51
- close: () => sqliteDb.close(),
52
- };
53
- };
54
- } catch {
55
- // Neither available
56
- }
57
- }
58
-
59
- import path from 'path';
60
- import fs from 'fs';
61
- import crypto from 'crypto';
62
- import { fileURLToPath } from 'url';
63
- import { dirname } from 'path';
64
-
65
- const __filename = fileURLToPath(import.meta.url);
66
- const __dirname = dirname(__filename);
67
-
68
- // ANSI color codes
69
- const colors = { reset: '\x1b[0m', bright: '\x1b[1m', cyan: '\x1b[36m', dim: '\x1b[2m' };
70
- const c = {
71
- info: (t) => `${colors.cyan}${t}${colors.reset}`,
72
- bright: (t) => `${colors.bright}${t}${colors.reset}`,
73
- dim: (t) => `${colors.dim}${t}${colors.reset}`,
74
- };
75
-
76
- // Database URL resolution (lazy — resolved on first access)
77
- let _db = null;
78
- let _dbUrl = null;
79
- let _dbAuthToken = null;
80
-
81
- function resolveDbConfig() {
82
- if (_dbUrl) return;
83
-
84
- if (process.env.TURSO_DATABASE_URL) {
85
- _dbUrl = process.env.TURSO_DATABASE_URL.trim();
86
- _dbAuthToken = process.env.TURSO_AUTH_TOKEN?.trim();
87
- console.log(`${c.info('[DB]')} Using Turso: ${_dbUrl}`);
88
- } else if (process.env.DATABASE_PATH) {
89
- const dbPath = process.env.DATABASE_PATH.trim();
90
- try { if (!fs.existsSync(path.dirname(dbPath))) fs.mkdirSync(path.dirname(dbPath), { recursive: true }); } catch { /* Vercel read-only */ }
91
- _dbUrl = `file:${dbPath}`;
92
- console.log(`${c.info('[DB]')} Using custom path: ${dbPath}`);
93
- } else if (process.env.VERCEL) {
94
- _dbUrl = 'file:/tmp/auth.db';
95
- console.warn(`${c.info('[DB]')} WARNING: Ephemeral /tmp on Vercel. Set TURSO_DATABASE_URL for persistent data.`);
96
- } else {
97
- _dbUrl = `file:${path.join(__dirname, 'auth.db')}`;
98
- console.log(`${c.info('[DB]')} Using local: ${_dbUrl}`);
99
- }
100
- }
101
-
102
- function getDb() {
103
- if (!_db) {
104
- if (!createClient) {
105
- throw new Error('[DB] No database driver available. Install libsql or @libsql/client.');
106
- }
107
- resolveDbConfig();
108
- _db = createClient({ url: _dbUrl, ...(_dbAuthToken ? { authToken: _dbAuthToken } : {}) });
109
- }
110
- return _db;
111
- }
112
-
113
- // Lazy Proxy — defers createClient() until first DB call
114
- const db = new Proxy({}, {
115
- get(_, prop) {
116
- const client = getDb();
117
- const val = client[prop];
118
- return typeof val === 'function' ? val.bind(client) : val;
119
- }
120
- });
121
-
122
- // ─── Schema ─────────────────────────────────────────────────────────────────────
123
-
124
- const INIT_SQL = `
125
- CREATE TABLE IF NOT EXISTS users (
126
- id INTEGER PRIMARY KEY AUTOINCREMENT,
127
- username TEXT UNIQUE NOT NULL,
128
- password_hash TEXT NOT NULL,
129
- first_name TEXT,
130
- last_name TEXT,
131
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
132
- last_login DATETIME,
133
- is_active BOOLEAN DEFAULT 1,
134
- email TEXT,
135
- phone TEXT,
136
- git_name TEXT,
137
- git_email TEXT,
138
- has_completed_onboarding BOOLEAN DEFAULT 0,
139
- access_override TEXT DEFAULT NULL,
140
- user_code TEXT UNIQUE,
141
- google_id TEXT
142
- );
143
-
144
- CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
145
- CREATE INDEX IF NOT EXISTS idx_users_active ON users(is_active);
146
- CREATE INDEX IF NOT EXISTS idx_users_user_code ON users(user_code);
147
-
148
- CREATE TABLE IF NOT EXISTS api_keys (
149
- id INTEGER PRIMARY KEY AUTOINCREMENT,
150
- user_id INTEGER NOT NULL,
151
- key_name TEXT NOT NULL,
152
- api_key TEXT UNIQUE NOT NULL,
153
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
154
- last_used DATETIME,
155
- is_active BOOLEAN DEFAULT 1,
156
- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
157
- );
158
-
159
- CREATE INDEX IF NOT EXISTS idx_api_keys_key ON api_keys(api_key);
160
- CREATE INDEX IF NOT EXISTS idx_api_keys_user_id ON api_keys(user_id);
161
- CREATE INDEX IF NOT EXISTS idx_api_keys_active ON api_keys(is_active);
162
-
163
- CREATE TABLE IF NOT EXISTS user_credentials (
164
- id INTEGER PRIMARY KEY AUTOINCREMENT,
165
- user_id INTEGER NOT NULL,
166
- credential_name TEXT NOT NULL,
167
- credential_type TEXT NOT NULL,
168
- credential_value TEXT NOT NULL,
169
- description TEXT,
170
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
171
- is_active BOOLEAN DEFAULT 1,
172
- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
173
- );
174
-
175
- CREATE INDEX IF NOT EXISTS idx_user_credentials_user_id ON user_credentials(user_id);
176
- CREATE INDEX IF NOT EXISTS idx_user_credentials_type ON user_credentials(credential_type);
177
- CREATE INDEX IF NOT EXISTS idx_user_credentials_active ON user_credentials(is_active);
178
-
179
- CREATE TABLE IF NOT EXISTS relay_tokens (
180
- id INTEGER PRIMARY KEY AUTOINCREMENT,
181
- user_id INTEGER NOT NULL,
182
- token TEXT UNIQUE NOT NULL,
183
- name TEXT NOT NULL DEFAULT 'default',
184
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
185
- last_connected DATETIME,
186
- last_connected_ip TEXT,
187
- is_active BOOLEAN DEFAULT 1,
188
- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
189
- );
190
-
191
- CREATE INDEX IF NOT EXISTS idx_relay_tokens_token ON relay_tokens(token);
192
- CREATE INDEX IF NOT EXISTS idx_relay_tokens_user_id ON relay_tokens(user_id);
193
- CREATE INDEX IF NOT EXISTS idx_relay_tokens_active ON relay_tokens(is_active);
194
-
195
- CREATE TABLE IF NOT EXISTS subscriptions (
196
- id INTEGER PRIMARY KEY AUTOINCREMENT,
197
- user_id INTEGER NOT NULL,
198
- plan_id TEXT NOT NULL,
199
- status TEXT NOT NULL DEFAULT 'active',
200
- amount INTEGER NOT NULL,
201
- currency TEXT NOT NULL DEFAULT 'INR',
202
- starts_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
203
- expires_at DATETIME NOT NULL,
204
- razorpay_order_id TEXT,
205
- razorpay_payment_id TEXT,
206
- razorpay_signature TEXT,
207
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
208
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
209
- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
210
- );
211
-
212
- CREATE INDEX IF NOT EXISTS idx_subscriptions_user_id ON subscriptions(user_id);
213
- CREATE INDEX IF NOT EXISTS idx_subscriptions_status ON subscriptions(status);
214
- CREATE INDEX IF NOT EXISTS idx_subscriptions_expires ON subscriptions(expires_at);
215
-
216
- CREATE TABLE IF NOT EXISTS payments (
217
- id INTEGER PRIMARY KEY AUTOINCREMENT,
218
- user_id INTEGER NOT NULL,
219
- subscription_id INTEGER,
220
- plan_id TEXT NOT NULL,
221
- amount INTEGER NOT NULL,
222
- currency TEXT NOT NULL DEFAULT 'INR',
223
- status TEXT NOT NULL DEFAULT 'pending',
224
- razorpay_order_id TEXT UNIQUE,
225
- razorpay_payment_id TEXT,
226
- razorpay_signature TEXT,
227
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
228
- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
229
- FOREIGN KEY (subscription_id) REFERENCES subscriptions(id) ON DELETE SET NULL
230
- );
231
-
232
- CREATE INDEX IF NOT EXISTS idx_payments_user_id ON payments(user_id);
233
- CREATE INDEX IF NOT EXISTS idx_payments_order_id ON payments(razorpay_order_id);
234
- CREATE INDEX IF NOT EXISTS idx_payments_status ON payments(status);
235
-
236
- CREATE TABLE IF NOT EXISTS webhooks (
237
- id INTEGER PRIMARY KEY AUTOINCREMENT,
238
- user_id INTEGER NOT NULL,
239
- name TEXT NOT NULL,
240
- url TEXT NOT NULL,
241
- method TEXT NOT NULL DEFAULT 'POST',
242
- headers TEXT DEFAULT '{}',
243
- description TEXT,
244
- is_active BOOLEAN DEFAULT 1,
245
- last_triggered DATETIME,
246
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
247
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
248
- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
249
- );
250
-
251
- CREATE INDEX IF NOT EXISTS idx_webhooks_user_id ON webhooks(user_id);
252
- CREATE INDEX IF NOT EXISTS idx_webhooks_active ON webhooks(is_active);
253
-
254
- CREATE TABLE IF NOT EXISTS workflows (
255
- id INTEGER PRIMARY KEY AUTOINCREMENT,
256
- user_id INTEGER NOT NULL,
257
- name TEXT NOT NULL,
258
- description TEXT,
259
- steps TEXT NOT NULL DEFAULT '[]',
260
- schedule TEXT DEFAULT NULL,
261
- schedule_enabled BOOLEAN DEFAULT 0,
262
- schedule_timezone TEXT DEFAULT 'UTC',
263
- is_active BOOLEAN DEFAULT 1,
264
- last_run DATETIME,
265
- next_run DATETIME,
266
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
267
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
268
- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
269
- );
270
-
271
- CREATE INDEX IF NOT EXISTS idx_workflows_user_id ON workflows(user_id);
272
- CREATE INDEX IF NOT EXISTS idx_workflows_active ON workflows(is_active);
273
-
274
- CREATE TABLE IF NOT EXISTS workflow_runs (
275
- id INTEGER PRIMARY KEY AUTOINCREMENT,
276
- workflow_id INTEGER NOT NULL,
277
- user_id INTEGER NOT NULL,
278
- status TEXT NOT NULL DEFAULT 'pending',
279
- steps_completed INTEGER DEFAULT 0,
280
- total_steps INTEGER DEFAULT 0,
281
- result TEXT,
282
- error TEXT,
283
- started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
284
- completed_at DATETIME,
285
- FOREIGN KEY (workflow_id) REFERENCES workflows(id) ON DELETE CASCADE,
286
- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
287
- );
288
-
289
- CREATE INDEX IF NOT EXISTS idx_workflow_runs_workflow_id ON workflow_runs(workflow_id);
290
- CREATE INDEX IF NOT EXISTS idx_workflow_runs_user_id ON workflow_runs(user_id);
291
- CREATE INDEX IF NOT EXISTS idx_workflow_runs_status ON workflow_runs(status);
292
-
293
- CREATE TABLE IF NOT EXISTS user_sandboxes (
294
- id INTEGER PRIMARY KEY AUTOINCREMENT,
295
- user_id INTEGER NOT NULL UNIQUE,
296
- sandbox_path TEXT NOT NULL,
297
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
298
- last_accessed DATETIME DEFAULT CURRENT_TIMESTAMP,
299
- disk_usage_bytes INTEGER DEFAULT 0,
300
- is_active BOOLEAN DEFAULT 1,
301
- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
302
- );
303
-
304
- CREATE INDEX IF NOT EXISTS idx_user_sandboxes_user_id ON user_sandboxes(user_id);
305
- CREATE INDEX IF NOT EXISTS idx_user_sandboxes_active ON user_sandboxes(is_active);
306
-
307
- CREATE TABLE IF NOT EXISTS file_versions (
308
- id INTEGER PRIMARY KEY AUTOINCREMENT,
309
- user_id INTEGER NOT NULL,
310
- session_id TEXT NOT NULL,
311
- file_path TEXT NOT NULL,
312
- content TEXT NOT NULL,
313
- version INTEGER NOT NULL DEFAULT 1,
314
- action TEXT NOT NULL DEFAULT 'write',
315
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
316
- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
317
- );
318
-
319
- CREATE INDEX IF NOT EXISTS idx_file_versions_session ON file_versions(session_id);
320
- CREATE INDEX IF NOT EXISTS idx_file_versions_path ON file_versions(file_path, session_id);
321
- CREATE INDEX IF NOT EXISTS idx_file_versions_user ON file_versions(user_id);
322
-
323
- CREATE TABLE IF NOT EXISTS session_usage (
324
- id INTEGER PRIMARY KEY AUTOINCREMENT,
325
- user_id INTEGER NOT NULL,
326
- session_id TEXT NOT NULL,
327
- provider TEXT NOT NULL DEFAULT 'claude',
328
- prompt_tokens INTEGER NOT NULL DEFAULT 0,
329
- completion_tokens INTEGER NOT NULL DEFAULT 0,
330
- cost_cents REAL NOT NULL DEFAULT 0.0,
331
- model TEXT,
332
- message_count INTEGER NOT NULL DEFAULT 0,
333
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
334
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
335
- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
336
- );
337
-
338
- CREATE INDEX IF NOT EXISTS idx_session_usage_user ON session_usage(user_id);
339
- CREATE INDEX IF NOT EXISTS idx_session_usage_session ON session_usage(session_id);
340
-
341
- CREATE TABLE IF NOT EXISTS project_canvases (
342
- id INTEGER PRIMARY KEY AUTOINCREMENT,
343
- user_id INTEGER NOT NULL,
344
- project_name TEXT NOT NULL,
345
- elements TEXT DEFAULT '[]',
346
- app_state TEXT DEFAULT '{}',
347
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
348
- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
349
- UNIQUE(user_id, project_name)
350
- );
351
-
352
- CREATE INDEX IF NOT EXISTS idx_project_canvases_user ON project_canvases(user_id);
353
-
354
- CREATE TABLE IF NOT EXISTS user_projects (
355
- id INTEGER PRIMARY KEY AUTOINCREMENT,
356
- user_id INTEGER NOT NULL,
357
- project_name TEXT NOT NULL,
358
- original_path TEXT NOT NULL,
359
- display_name TEXT,
360
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
361
- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
362
- UNIQUE(user_id, original_path)
363
- );
364
-
365
- CREATE INDEX IF NOT EXISTS idx_user_projects_user_id ON user_projects(user_id);
366
-
367
- CREATE TABLE IF NOT EXISTS password_reset_tokens (
368
- id INTEGER PRIMARY KEY AUTOINCREMENT,
369
- user_id INTEGER NOT NULL,
370
- token TEXT UNIQUE NOT NULL,
371
- expires_at DATETIME NOT NULL,
372
- used BOOLEAN DEFAULT 0,
373
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
374
- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
375
- );
376
-
377
- CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_token ON password_reset_tokens(token);
378
- CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_user_id ON password_reset_tokens(user_id);
379
-
380
- CREATE TABLE IF NOT EXISTS active_connections (
381
- id INTEGER PRIMARY KEY AUTOINCREMENT,
382
- user_id TEXT NOT NULL,
383
- project_name TEXT,
384
- connection_type TEXT DEFAULT 'relay',
385
- sandbox_id TEXT,
386
- last_cwd TEXT,
387
- last_machine TEXT,
388
- last_platform TEXT,
389
- connected_at DATETIME DEFAULT CURRENT_TIMESTAMP,
390
- disconnected_at DATETIME,
391
- UNIQUE(user_id, connection_type)
392
- );
393
-
394
- CREATE INDEX IF NOT EXISTS idx_active_connections_user ON active_connections(user_id);
395
-
396
- CREATE TABLE IF NOT EXISTS voice_calls (
397
- id INTEGER PRIMARY KEY AUTOINCREMENT,
398
- user_id INTEGER NOT NULL,
399
- vapi_call_id TEXT,
400
- session_id TEXT,
401
- call_type TEXT DEFAULT 'voice',
402
- status TEXT DEFAULT 'in_progress',
403
- started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
404
- ended_at DATETIME,
405
- duration_seconds INTEGER,
406
- ended_reason TEXT,
407
- cost REAL,
408
- transcript TEXT,
409
- summary TEXT,
410
- tools_used TEXT,
411
- messages_count INTEGER DEFAULT 0,
412
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
413
- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
414
- );
415
-
416
- CREATE INDEX IF NOT EXISTS idx_voice_calls_user ON voice_calls(user_id);
417
- CREATE INDEX IF NOT EXISTS idx_voice_calls_vapi ON voice_calls(vapi_call_id);
418
-
419
- CREATE TABLE IF NOT EXISTS browser_sessions (
420
- id INTEGER PRIMARY KEY AUTOINCREMENT,
421
- user_id INTEGER NOT NULL,
422
- steel_session_id TEXT NOT NULL UNIQUE,
423
- status TEXT NOT NULL DEFAULT 'active',
424
- viewer_url TEXT,
425
- cdp_url TEXT,
426
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
427
- last_accessed DATETIME DEFAULT CURRENT_TIMESTAMP,
428
- metadata TEXT DEFAULT '{}',
429
- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
430
- );
431
-
432
- CREATE INDEX IF NOT EXISTS idx_browser_sessions_user ON browser_sessions(user_id);
433
- CREATE INDEX IF NOT EXISTS idx_browser_sessions_status ON browser_sessions(status);
434
- `;
435
-
436
- // ─── Migrations ─────────────────────────────────────────────────────────────────
437
-
438
- const runMigrations = async () => {
439
- try {
440
- const result = await db.execute("PRAGMA table_info(users)");
441
- const cols = result.rows.map(r => r.name);
442
-
443
- const addCol = async (col, def) => {
444
- if (!cols.includes(col)) {
445
- await db.execute(`ALTER TABLE users ADD COLUMN ${col} ${def}`);
446
- }
447
- };
448
-
449
- await addCol('git_name', 'TEXT');
450
- await addCol('git_email', 'TEXT');
451
- await addCol('has_completed_onboarding', 'BOOLEAN DEFAULT 0');
452
- await addCol('email', 'TEXT');
453
- await addCol('phone', 'TEXT');
454
- await addCol('first_name', 'TEXT');
455
- await addCol('last_name', 'TEXT');
456
- await addCol('access_override', 'TEXT DEFAULT NULL');
457
- await addCol('user_code', 'TEXT');
458
- await addCol('google_id', 'TEXT');
459
- try { await db.execute('CREATE UNIQUE INDEX IF NOT EXISTS idx_users_user_code ON users(user_code)'); } catch { /* ignore */ }
460
-
461
- // Backfill user_code (upc-001, upc-002, ...) for users missing it
462
- try {
463
- const noCode = await db.execute("SELECT id FROM users WHERE user_code IS NULL ORDER BY id ASC");
464
- for (const row of noCode.rows) {
465
- const code = `upc-${String(row.id).padStart(3, '0')}`;
466
- await db.execute({ sql: 'UPDATE users SET user_code = ? WHERE id = ?', args: [code, row.id] });
467
- }
468
- if (noCode.rows.length > 0) console.log(`${c.info('[DB]')} Assigned user_code to ${noCode.rows.length} users`);
469
- } catch (e) { console.warn('[DB] user_code backfill:', e.message); }
470
-
471
- // Migrate old ck_ API keys → up-cli- prefix
472
- try {
473
- const migrated = await db.execute("UPDATE api_keys SET api_key = 'up-cli-' || SUBSTR(api_key, 4) WHERE api_key LIKE 'ck_%'");
474
- if (migrated.rowsAffected > 0) console.log(`${c.info('[DB]')} Migrated ${migrated.rowsAffected} API keys: ck_ → up-cli-`);
475
- } catch { /* empty table or already migrated */ }
476
-
477
- // Backfill: ensure every user has records in all tables (relay_tokens, api_keys, subscriptions, payments, access_override)
478
- try {
479
- const allUsers = await db.execute('SELECT id, username, access_override FROM users WHERE is_active = 1');
480
- let backfilled = 0;
481
- for (const user of allUsers.rows) {
482
- // Relay token
483
- const tokens = await db.execute({ sql: 'SELECT id FROM relay_tokens WHERE user_id = ? LIMIT 1', args: [user.id] });
484
- if (tokens.rows.length === 0) {
485
- const token = 'upfyn_' + crypto.randomBytes(32).toString('hex');
486
- await db.execute({ sql: 'INSERT INTO relay_tokens (user_id, token, name) VALUES (?, ?, ?)', args: [user.id, token, 'default'] });
487
- backfilled++;
488
- }
489
- // API key
490
- const keys = await db.execute({ sql: 'SELECT id FROM api_keys WHERE user_id = ? LIMIT 1', args: [user.id] });
491
- if (keys.rows.length === 0) {
492
- const apiKey = 'up-cli-' + crypto.randomBytes(32).toString('hex');
493
- await db.execute({ sql: 'INSERT INTO api_keys (user_id, key_name, api_key) VALUES (?, ?, ?)', args: [user.id, 'default', apiKey] });
494
- backfilled++;
495
- }
496
- // Yearly subscription (if none exists)
497
- const subs = await db.execute({ sql: 'SELECT id FROM subscriptions WHERE user_id = ? LIMIT 1', args: [user.id] });
498
- if (subs.rows.length === 0) {
499
- const now = new Date();
500
- const expiresAt = new Date(now.getTime() + 365 * 24 * 60 * 60 * 1000).toISOString();
501
- const subResult = await db.execute({
502
- sql: `INSERT INTO subscriptions (user_id, plan_id, status, amount, currency, starts_at, expires_at) VALUES (?, 'yearly', 'active', 49900, 'INR', ?, ?)`,
503
- args: [user.id, now.toISOString(), expiresAt]
504
- });
505
- // Payment record for the subscription
506
- const subId = Number(subResult.lastInsertRowid);
507
- await db.execute({
508
- sql: `INSERT INTO payments (user_id, subscription_id, plan_id, amount, currency, status, razorpay_order_id) VALUES (?, ?, 'yearly', 49900, 'INR', 'paid', ?)`,
509
- args: [user.id, subId, `seed_order_${user.id}_${Date.now()}`]
510
- });
511
- backfilled++;
512
- }
513
- // Set access_override = 'paid' if not already set
514
- if (!user.access_override || user.access_override !== 'paid') {
515
- await db.execute({ sql: "UPDATE users SET access_override = 'paid' WHERE id = ?", args: [user.id] });
516
- backfilled++;
517
- }
518
- }
519
- if (backfilled > 0) console.log(`${c.info('[DB]')} Backfilled ${backfilled} records for existing users`);
520
- } catch (e) { console.warn('[DB] Backfill warning:', e.message); }
521
-
522
- // Add last_connected_ip column to relay_tokens if missing
523
- try {
524
- const rtCols = await db.execute("PRAGMA table_info(relay_tokens)");
525
- const rtColNames = rtCols.rows.map(r => r.name);
526
- if (!rtColNames.includes('last_connected_ip')) {
527
- await db.execute('ALTER TABLE relay_tokens ADD COLUMN last_connected_ip TEXT');
528
- }
529
- } catch { /* column may already exist */ }
530
-
531
- // Add github_origin column to user_projects if missing
532
- try {
533
- const upCols = await db.execute("PRAGMA table_info(user_projects)");
534
- const upColNames = upCols.rows.map(r => r.name);
535
- if (!upColNames.includes('github_origin')) {
536
- await db.execute('ALTER TABLE user_projects ADD COLUMN github_origin TEXT');
537
- }
538
- } catch { /* column may already exist */ }
539
-
540
- console.log(`${c.info('[DB]')} Migrations complete`);
541
- } catch (error) {
542
- console.error('Migration error:', error.message);
543
- throw error;
544
- }
545
- };
546
-
547
- // ─── Initialize ─────────────────────────────────────────────────────────────────
548
-
549
- const initializeDatabase = async () => {
550
- try {
551
- try { await db.execute('PRAGMA foreign_keys = ON'); } catch (e) { console.warn('[DB] PRAGMA foreign_keys not supported:', e.message); }
552
-
553
- const stmts = INIT_SQL.split(';').map(s => s.trim()).filter(s => s.length > 0 && !s.startsWith('--'));
554
- for (const stmt of stmts) {
555
- try { await db.execute(stmt); } catch (e) { console.error('[DB] Statement failed:', stmt.substring(0, 80), e.message); throw e; }
556
- }
557
-
558
- console.log(`${c.info('[DB]')} Initialized`);
559
- await runMigrations();
560
- } catch (error) {
561
- console.error('DB init error:', error.message);
562
- throw error;
563
- }
564
- };
565
-
566
- // ─── Helpers ────────────────────────────────────────────────────────────────────
567
-
568
- const getRow = (result) => result.rows.length > 0 ? result.rows[0] : null;
569
- const ensureForeignKeys = async () => { try { await db.execute('PRAGMA foreign_keys = ON'); } catch { /* Turso */ } };
570
-
571
- // ─── User DB ────────────────────────────────────────────────────────────────────
572
-
573
- const userDb = {
574
- hasUsers: async () => {
575
- const result = await db.execute('SELECT COUNT(*) as count FROM users');
576
- return Number(getRow(result).count) > 0;
577
- },
578
-
579
- createUser: async (username, passwordHash, email = null, phone = null, firstName = null, lastName = null) => {
580
- const result = await db.execute({
581
- sql: 'INSERT INTO users (username, password_hash, email, phone, first_name, last_name) VALUES (?, ?, ?, ?, ?, ?)',
582
- args: [username, passwordHash, email, phone, firstName, lastName]
583
- });
584
- const userId = Number(result.lastInsertRowid);
585
- // Auto-assign user_code (upc-001, upc-002, ...)
586
- const userCode = `upc-${String(userId).padStart(3, '0')}`;
587
- try { await db.execute({ sql: 'UPDATE users SET user_code = ? WHERE id = ?', args: [userCode, userId] }); } catch { /* non-critical */ }
588
- return { id: userId, username, first_name: firstName, last_name: lastName, user_code: userCode };
589
- },
590
-
591
- getUserByUsername: async (username) => {
592
- const lower = (username || '').toLowerCase();
593
- const result = await db.execute({
594
- sql: 'SELECT * FROM users WHERE (LOWER(username) = ? OR LOWER(email) = ? OR phone = ? OR LOWER(user_code) = ?) AND is_active = 1',
595
- args: [lower, lower, username, lower]
596
- });
597
- return getRow(result);
598
- },
599
-
600
- updateLastLogin: async (userId) => {
601
- await db.execute({ sql: 'UPDATE users SET last_login = CURRENT_TIMESTAMP WHERE id = ?', args: [userId] });
602
- },
603
-
604
- getUserById: async (userId) => {
605
- const result = await db.execute({
606
- sql: 'SELECT id, username, first_name, last_name, email, phone, created_at, last_login, access_override, user_code FROM users WHERE id = ? AND is_active = 1',
607
- args: [userId]
608
- });
609
- return getRow(result);
610
- },
611
-
612
- getFirstUser: async () => {
613
- const result = await db.execute('SELECT id, username, first_name, last_name, email, phone, created_at, last_login, access_override, user_code FROM users WHERE is_active = 1 LIMIT 1');
614
- return getRow(result);
615
- },
616
-
617
- updateGitConfig: async (userId, gitName, gitEmail) => {
618
- await db.execute({ sql: 'UPDATE users SET git_name = ?, git_email = ? WHERE id = ?', args: [gitName, gitEmail, userId] });
619
- },
620
-
621
- getGitConfig: async (userId) => {
622
- const result = await db.execute({ sql: 'SELECT git_name, git_email FROM users WHERE id = ?', args: [userId] });
623
- return getRow(result);
624
- },
625
-
626
- completeOnboarding: async (userId) => {
627
- await db.execute({ sql: 'UPDATE users SET has_completed_onboarding = 1 WHERE id = ?', args: [userId] });
628
- },
629
-
630
- hasCompletedOnboarding: async (userId) => {
631
- const result = await db.execute({ sql: 'SELECT has_completed_onboarding FROM users WHERE id = ?', args: [userId] });
632
- const row = getRow(result);
633
- return row?.has_completed_onboarding === 1;
634
- },
635
-
636
- setAccessOverride: async (userId, value) => {
637
- await db.execute({
638
- sql: 'UPDATE users SET access_override = ? WHERE id = ?',
639
- args: [value, userId]
640
- });
641
- },
642
-
643
- getUserByGoogleId: async (googleId) => {
644
- const result = await db.execute({
645
- sql: 'SELECT * FROM users WHERE google_id = ? AND is_active = 1',
646
- args: [googleId]
647
- });
648
- return getRow(result);
649
- },
650
-
651
- setUserGoogleId: async (userId, googleId) => {
652
- await db.execute({
653
- sql: 'UPDATE users SET google_id = ? WHERE id = ?',
654
- args: [googleId, userId]
655
- });
656
- },
657
-
658
- getUserByEmail: async (email) => {
659
- const result = await db.execute({
660
- sql: 'SELECT * FROM users WHERE LOWER(email) = ? AND is_active = 1',
661
- args: [(email || '').toLowerCase()]
662
- });
663
- return getRow(result);
664
- },
665
-
666
- updatePasswordHash: async (userId, passwordHash) => {
667
- await db.execute({
668
- sql: 'UPDATE users SET password_hash = ? WHERE id = ?',
669
- args: [passwordHash, userId]
670
- });
671
- }
672
- };
673
-
674
- // ─── API Keys DB ────────────────────────────────────────────────────────────────
675
-
676
- const apiKeysDb = {
677
- generateApiKey: () => 'up-cli-' + crypto.randomBytes(32).toString('hex'),
678
-
679
- createApiKey: async (userId, keyName) => {
680
- const apiKey = apiKeysDb.generateApiKey();
681
- const result = await db.execute({
682
- sql: 'INSERT INTO api_keys (user_id, key_name, api_key) VALUES (?, ?, ?)',
683
- args: [userId, keyName, apiKey]
684
- });
685
- return { id: Number(result.lastInsertRowid), keyName, apiKey };
686
- },
687
-
688
- getApiKeys: async (userId) => {
689
- const result = await db.execute({
690
- sql: 'SELECT id, key_name, api_key, created_at, last_used, is_active FROM api_keys WHERE user_id = ? ORDER BY created_at DESC',
691
- args: [userId]
692
- });
693
- return result.rows;
694
- },
695
-
696
- validateApiKey: async (apiKey) => {
697
- const result = await db.execute({
698
- sql: `SELECT u.id, u.username, ak.id as api_key_id
699
- FROM api_keys ak JOIN users u ON ak.user_id = u.id
700
- WHERE ak.api_key = ? AND ak.is_active = 1 AND u.is_active = 1`,
701
- args: [apiKey]
702
- });
703
- const row = getRow(result);
704
- if (row) {
705
- await db.execute({ sql: 'UPDATE api_keys SET last_used = CURRENT_TIMESTAMP WHERE id = ?', args: [row.api_key_id] });
706
- }
707
- return row;
708
- },
709
-
710
- deleteApiKey: async (userId, apiKeyId) => {
711
- await ensureForeignKeys();
712
- const result = await db.execute({ sql: 'DELETE FROM api_keys WHERE id = ? AND user_id = ?', args: [apiKeyId, userId] });
713
- return result.rowsAffected > 0;
714
- },
715
-
716
- toggleApiKey: async (userId, apiKeyId, isActive) => {
717
- const result = await db.execute({
718
- sql: 'UPDATE api_keys SET is_active = ? WHERE id = ? AND user_id = ?',
719
- args: [isActive ? 1 : 0, apiKeyId, userId]
720
- });
721
- return result.rowsAffected > 0;
722
- },
723
-
724
- deactivateAll: async (userId) => {
725
- await db.execute({ sql: 'UPDATE api_keys SET is_active = 0 WHERE user_id = ?', args: [userId] });
726
- }
727
- };
728
-
729
- // ─── Credentials DB ─────────────────────────────────────────────────────────────
730
-
731
- const credentialsDb = {
732
- createCredential: async (userId, credentialName, credentialType, credentialValue, description = null) => {
733
- const result = await db.execute({
734
- sql: 'INSERT INTO user_credentials (user_id, credential_name, credential_type, credential_value, description) VALUES (?, ?, ?, ?, ?)',
735
- args: [userId, credentialName, credentialType, credentialValue, description]
736
- });
737
- return { id: Number(result.lastInsertRowid), credentialName, credentialType };
738
- },
739
-
740
- getCredentials: async (userId, credentialType = null) => {
741
- let sql = 'SELECT id, credential_name, credential_type, description, created_at, is_active FROM user_credentials WHERE user_id = ?';
742
- const args = [userId];
743
- if (credentialType) { sql += ' AND credential_type = ?'; args.push(credentialType); }
744
- sql += ' ORDER BY created_at DESC';
745
- return (await db.execute({ sql, args })).rows;
746
- },
747
-
748
- getActiveCredential: async (userId, credentialType) => {
749
- const result = await db.execute({
750
- sql: 'SELECT credential_value FROM user_credentials WHERE user_id = ? AND credential_type = ? AND is_active = 1 ORDER BY created_at DESC LIMIT 1',
751
- args: [userId, credentialType]
752
- });
753
- return getRow(result)?.credential_value || null;
754
- },
755
-
756
- deleteCredential: async (userId, credentialId) => {
757
- await ensureForeignKeys();
758
- const result = await db.execute({ sql: 'DELETE FROM user_credentials WHERE id = ? AND user_id = ?', args: [credentialId, userId] });
759
- return result.rowsAffected > 0;
760
- },
761
-
762
- toggleCredential: async (userId, credentialId, isActive) => {
763
- const result = await db.execute({
764
- sql: 'UPDATE user_credentials SET is_active = ? WHERE id = ? AND user_id = ?',
765
- args: [isActive ? 1 : 0, credentialId, userId]
766
- });
767
- return result.rowsAffected > 0;
768
- }
769
- };
770
-
771
- // Backward compat
772
- const githubTokensDb = {
773
- createGithubToken: (userId, tokenName, githubToken, description = null) => credentialsDb.createCredential(userId, tokenName, 'github_token', githubToken, description),
774
- getGithubTokens: (userId) => credentialsDb.getCredentials(userId, 'github_token'),
775
- getActiveGithubToken: (userId) => credentialsDb.getActiveCredential(userId, 'github_token'),
776
- deleteGithubToken: (userId, tokenId) => credentialsDb.deleteCredential(userId, tokenId),
777
- toggleGithubToken: (userId, tokenId, isActive) => credentialsDb.toggleCredential(userId, tokenId, isActive)
778
- };
779
-
780
- // ─── Plan Durations ─────────────────────────────────────────────────────────────
781
-
782
- const PLAN_DURATIONS = {
783
- monthly: 30,
784
- 'half-yearly': 180,
785
- yearly: 365,
786
- };
787
-
788
- // ─── Subscription DB ────────────────────────────────────────────────────────────
789
-
790
- const subscriptionDb = {
791
- getActiveSub: async (userId) => {
792
- const result = await db.execute({
793
- sql: `SELECT * FROM subscriptions WHERE user_id = ? AND status = 'active' AND expires_at > CURRENT_TIMESTAMP ORDER BY expires_at DESC LIMIT 1`,
794
- args: [userId]
795
- });
796
- return getRow(result);
797
- },
798
-
799
- getAllSubs: async (userId) => {
800
- const result = await db.execute({
801
- sql: 'SELECT * FROM subscriptions WHERE user_id = ? ORDER BY created_at DESC',
802
- args: [userId]
803
- });
804
- return result.rows;
805
- },
806
-
807
- createSub: async (userId, planId, amount, currency, razorpayOrderId, razorpayPaymentId, razorpaySignature) => {
808
- const days = PLAN_DURATIONS[planId] || 30;
809
-
810
- // Check if user has an active sub — extend from its expiry, otherwise start now
811
- const existing = await subscriptionDb.getActiveSub(userId);
812
- const startsAt = existing ? existing.expires_at : new Date().toISOString();
813
- const startsDate = new Date(startsAt);
814
- const expiresAt = new Date(startsDate.getTime() + days * 24 * 60 * 60 * 1000).toISOString();
815
-
816
- const result = await db.execute({
817
- sql: `INSERT INTO subscriptions (user_id, plan_id, status, amount, currency, starts_at, expires_at, razorpay_order_id, razorpay_payment_id, razorpay_signature) VALUES (?, ?, 'active', ?, ?, ?, ?, ?, ?, ?)`,
818
- args: [userId, planId, amount, currency, startsAt, expiresAt, razorpayOrderId, razorpayPaymentId, razorpaySignature]
819
- });
820
- return { id: Number(result.lastInsertRowid), planId, status: 'active', startsAt, expiresAt };
821
- },
822
-
823
- cancelSub: async (userId, subId) => {
824
- const result = await db.execute({
825
- sql: `UPDATE subscriptions SET status = 'cancelled', updated_at = CURRENT_TIMESTAMP WHERE id = ? AND user_id = ?`,
826
- args: [subId, userId]
827
- });
828
- return result.rowsAffected > 0;
829
- },
830
-
831
- expireOverdue: async () => {
832
- const result = await db.execute(
833
- `UPDATE subscriptions SET status = 'expired', updated_at = CURRENT_TIMESTAMP WHERE status = 'active' AND expires_at <= CURRENT_TIMESTAMP`
834
- );
835
- return result.rowsAffected;
836
- },
837
-
838
- // Cancel current active sub and create a new one starting now (used for downgrades)
839
- changePlan: async (userId, newPlanId, amount, currency, razorpayOrderId, razorpayPaymentId, razorpaySignature) => {
840
- // Cancel existing active subscription
841
- const existing = await subscriptionDb.getActiveSub(userId);
842
- if (existing) {
843
- await db.execute({
844
- sql: `UPDATE subscriptions SET status = 'changed', updated_at = CURRENT_TIMESTAMP WHERE id = ? AND user_id = ?`,
845
- args: [existing.id, userId]
846
- });
847
- }
848
-
849
- // Create new subscription starting now (not extending from old expiry)
850
- const days = PLAN_DURATIONS[newPlanId] || 30;
851
- const startsAt = new Date().toISOString();
852
- const expiresAt = new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString();
853
-
854
- const result = await db.execute({
855
- sql: `INSERT INTO subscriptions (user_id, plan_id, status, amount, currency, starts_at, expires_at, razorpay_order_id, razorpay_payment_id, razorpay_signature) VALUES (?, ?, 'active', ?, ?, ?, ?, ?, ?, ?)`,
856
- args: [userId, newPlanId, amount, currency, startsAt, expiresAt, razorpayOrderId, razorpayPaymentId, razorpaySignature]
857
- });
858
- return { id: Number(result.lastInsertRowid), planId: newPlanId, status: 'active', startsAt, expiresAt };
859
- },
860
- };
861
-
862
- // ─── Payment DB ─────────────────────────────────────────────────────────────────
863
-
864
- const paymentDb = {
865
- createPayment: async (userId, planId, amount, currency, razorpayOrderId) => {
866
- const result = await db.execute({
867
- sql: `INSERT INTO payments (user_id, plan_id, amount, currency, razorpay_order_id, status) VALUES (?, ?, ?, ?, ?, 'pending')`,
868
- args: [userId, planId, amount, currency, razorpayOrderId]
869
- });
870
- return { id: Number(result.lastInsertRowid), razorpayOrderId };
871
- },
872
-
873
- getByOrderId: async (razorpayOrderId) => {
874
- const result = await db.execute({
875
- sql: 'SELECT * FROM payments WHERE razorpay_order_id = ?',
876
- args: [razorpayOrderId]
877
- });
878
- return getRow(result);
879
- },
880
-
881
- markPaid: async (razorpayOrderId, razorpayPaymentId, razorpaySignature, subscriptionId) => {
882
- await db.execute({
883
- sql: `UPDATE payments SET status = 'paid', razorpay_payment_id = ?, razorpay_signature = ?, subscription_id = ? WHERE razorpay_order_id = ?`,
884
- args: [razorpayPaymentId, razorpaySignature, subscriptionId, razorpayOrderId]
885
- });
886
- },
887
-
888
- markFailed: async (razorpayOrderId) => {
889
- await db.execute({
890
- sql: `UPDATE payments SET status = 'failed' WHERE razorpay_order_id = ?`,
891
- args: [razorpayOrderId]
892
- });
893
- },
894
-
895
- getUserPayments: async (userId) => {
896
- const result = await db.execute({
897
- sql: 'SELECT * FROM payments WHERE user_id = ? ORDER BY created_at DESC',
898
- args: [userId]
899
- });
900
- return result.rows;
901
- },
902
- };
903
-
904
- // ─── Relay Tokens DB ────────────────────────────────────────────────────────────
905
-
906
- const relayTokensDb = {
907
- generateToken: () => 'upfyn_' + crypto.randomBytes(32).toString('hex'),
908
-
909
- createToken: async (userId, name = 'default') => {
910
- const token = relayTokensDb.generateToken();
911
- await db.execute({ sql: 'INSERT INTO relay_tokens (user_id, token, name) VALUES (?, ?, ?)', args: [userId, token, name] });
912
- return { token, name };
913
- },
914
-
915
- validateToken: async (token, clientIp = null) => {
916
- try {
917
- const result = await db.execute({
918
- sql: `SELECT rt.id, rt.user_id, rt.name, u.id as uid, u.username
919
- FROM relay_tokens rt JOIN users u ON rt.user_id = u.id
920
- WHERE rt.token = ? AND rt.is_active = 1 AND u.is_active = 1`,
921
- args: [token]
922
- });
923
- const row = getRow(result);
924
- if (row) {
925
- const updateSql = clientIp
926
- ? 'UPDATE relay_tokens SET last_connected = CURRENT_TIMESTAMP, last_connected_ip = ? WHERE id = ?'
927
- : 'UPDATE relay_tokens SET last_connected = CURRENT_TIMESTAMP WHERE id = ?';
928
- const updateArgs = clientIp ? [clientIp, row.id] : [row.id];
929
- await db.execute({ sql: updateSql, args: updateArgs });
930
- }
931
- return row;
932
- } catch { return null; }
933
- },
934
-
935
- getTokens: async (userId) => {
936
- return (await db.execute({ sql: 'SELECT id, name, token, created_at, last_connected, is_active FROM relay_tokens WHERE user_id = ? ORDER BY created_at DESC', args: [userId] })).rows;
937
- },
938
-
939
- deleteToken: async (userId, tokenId) => {
940
- await ensureForeignKeys();
941
- return (await db.execute({ sql: 'DELETE FROM relay_tokens WHERE id = ? AND user_id = ?', args: [tokenId, userId] })).rowsAffected > 0;
942
- },
943
-
944
- toggleToken: async (userId, tokenId, isActive) => {
945
- return (await db.execute({ sql: 'UPDATE relay_tokens SET is_active = ? WHERE id = ? AND user_id = ?', args: [isActive ? 1 : 0, tokenId, userId] })).rowsAffected > 0;
946
- },
947
-
948
- deactivateAll: async (userId) => {
949
- await db.execute({ sql: 'UPDATE relay_tokens SET is_active = 0 WHERE user_id = ?', args: [userId] });
950
- }
951
- };
952
-
953
- // ─── Webhook DB ──────────────────────────────────────────────────────────────
954
-
955
- const webhookDb = {
956
- getAll: async (userId) => {
957
- const result = await db.execute({
958
- sql: 'SELECT * FROM webhooks WHERE user_id = ? ORDER BY created_at DESC',
959
- args: [userId]
960
- });
961
- return result.rows;
962
- },
963
-
964
- getById: async (id, userId) => {
965
- const result = await db.execute({
966
- sql: 'SELECT * FROM webhooks WHERE id = ? AND user_id = ?',
967
- args: [id, userId]
968
- });
969
- return getRow(result);
970
- },
971
-
972
- create: async (userId, { name, url, method, headers, description }) => {
973
- const result = await db.execute({
974
- sql: 'INSERT INTO webhooks (user_id, name, url, method, headers, description) VALUES (?, ?, ?, ?, ?, ?)',
975
- args: [userId, name, url, method || 'POST', headers || '{}', description || null]
976
- });
977
- return { id: Number(result.lastInsertRowid), name, url, method: method || 'POST' };
978
- },
979
-
980
- update: async (id, userId, { name, url, method, headers, description }) => {
981
- const result = await db.execute({
982
- sql: 'UPDATE webhooks SET name = ?, url = ?, method = ?, headers = ?, description = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND user_id = ?',
983
- args: [name, url, method, headers || '{}', description || null, id, userId]
984
- });
985
- return result.rowsAffected > 0;
986
- },
987
-
988
- delete: async (id, userId) => {
989
- await ensureForeignKeys();
990
- const result = await db.execute({
991
- sql: 'DELETE FROM webhooks WHERE id = ? AND user_id = ?',
992
- args: [id, userId]
993
- });
994
- return result.rowsAffected > 0;
995
- },
996
-
997
- updateLastTriggered: async (id) => {
998
- await db.execute({
999
- sql: 'UPDATE webhooks SET last_triggered = CURRENT_TIMESTAMP WHERE id = ?',
1000
- args: [id]
1001
- });
1002
- }
1003
- };
1004
-
1005
- // ─── Workflow DB ─────────────────────────────────────────────────────────────
1006
-
1007
- const workflowDb = {
1008
- getAll: async (userId) => {
1009
- const result = await db.execute({
1010
- sql: 'SELECT * FROM workflows WHERE user_id = ? ORDER BY created_at DESC',
1011
- args: [userId]
1012
- });
1013
- return result.rows;
1014
- },
1015
-
1016
- getById: async (id, userId) => {
1017
- const result = await db.execute({
1018
- sql: 'SELECT * FROM workflows WHERE id = ? AND user_id = ?',
1019
- args: [id, userId]
1020
- });
1021
- return getRow(result);
1022
- },
1023
-
1024
- create: async (userId, { name, description, steps, schedule, schedule_enabled, schedule_timezone }) => {
1025
- const result = await db.execute({
1026
- sql: 'INSERT INTO workflows (user_id, name, description, steps, schedule, schedule_enabled, schedule_timezone) VALUES (?, ?, ?, ?, ?, ?, ?)',
1027
- args: [userId, name, description || null, JSON.stringify(steps || []), schedule || null, schedule_enabled ? 1 : 0, schedule_timezone || 'UTC']
1028
- });
1029
- return { id: Number(result.lastInsertRowid), name };
1030
- },
1031
-
1032
- update: async (id, userId, { name, description, steps, schedule, schedule_enabled, schedule_timezone }) => {
1033
- const result = await db.execute({
1034
- sql: 'UPDATE workflows SET name = ?, description = ?, steps = ?, schedule = ?, schedule_enabled = ?, schedule_timezone = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND user_id = ?',
1035
- args: [name, description || null, JSON.stringify(steps || []), schedule || null, schedule_enabled ? 1 : 0, schedule_timezone || 'UTC', id, userId]
1036
- });
1037
- return result.rowsAffected > 0;
1038
- },
1039
-
1040
- delete: async (id, userId) => {
1041
- await ensureForeignKeys();
1042
- const result = await db.execute({
1043
- sql: 'DELETE FROM workflows WHERE id = ? AND user_id = ?',
1044
- args: [id, userId]
1045
- });
1046
- return result.rowsAffected > 0;
1047
- },
1048
-
1049
- updateLastRun: async (id) => {
1050
- await db.execute({
1051
- sql: 'UPDATE workflows SET last_run = CURRENT_TIMESTAMP WHERE id = ?',
1052
- args: [id]
1053
- });
1054
- },
1055
-
1056
- getScheduled: async () => {
1057
- const result = await db.execute(
1058
- 'SELECT * FROM workflows WHERE schedule IS NOT NULL AND schedule_enabled = 1 AND is_active = 1'
1059
- );
1060
- return result.rows;
1061
- },
1062
-
1063
- updateNextRun: async (id, nextRun) => {
1064
- await db.execute({
1065
- sql: 'UPDATE workflows SET next_run = ? WHERE id = ?',
1066
- args: [nextRun, id]
1067
- });
1068
- },
1069
-
1070
- createRun: async (workflowId, userId, totalSteps) => {
1071
- const result = await db.execute({
1072
- sql: 'INSERT INTO workflow_runs (workflow_id, user_id, status, total_steps) VALUES (?, ?, ?, ?)',
1073
- args: [workflowId, userId, 'running', totalSteps]
1074
- });
1075
- return { id: Number(result.lastInsertRowid) };
1076
- },
1077
-
1078
- updateRun: async (runId, { status, stepsCompleted, result: runResult, error }) => {
1079
- const sets = ['status = ?'];
1080
- const args = [status];
1081
- if (stepsCompleted !== undefined) { sets.push('steps_completed = ?'); args.push(stepsCompleted); }
1082
- if (runResult !== undefined) { sets.push('result = ?'); args.push(typeof runResult === 'string' ? runResult : JSON.stringify(runResult)); }
1083
- if (error !== undefined) { sets.push('error = ?'); args.push(error); }
1084
- if (status === 'completed' || status === 'failed') { sets.push('completed_at = CURRENT_TIMESTAMP'); }
1085
- args.push(runId);
1086
- await db.execute({ sql: `UPDATE workflow_runs SET ${sets.join(', ')} WHERE id = ?`, args });
1087
- },
1088
-
1089
- getRuns: async (workflowId, userId) => {
1090
- const result = await db.execute({
1091
- sql: 'SELECT * FROM workflow_runs WHERE workflow_id = ? AND user_id = ? ORDER BY started_at DESC LIMIT 20',
1092
- args: [workflowId, userId]
1093
- });
1094
- return result.rows;
1095
- }
1096
- };
1097
-
1098
- // ─── File Version History (opencode pattern: files table for undo/redo) ───────
1099
-
1100
- const fileVersionDb = {
1101
- save: async (userId, sessionId, filePath, content, action = 'write') => {
1102
- // Get next version number for this file+session
1103
- const existing = await db.execute({
1104
- sql: 'SELECT MAX(version) as maxVer FROM file_versions WHERE session_id = ? AND file_path = ?',
1105
- args: [sessionId, filePath]
1106
- });
1107
- const nextVersion = (existing.rows[0]?.maxVer || 0) + 1;
1108
- await db.execute({
1109
- sql: 'INSERT INTO file_versions (user_id, session_id, file_path, content, version, action) VALUES (?, ?, ?, ?, ?, ?)',
1110
- args: [userId, sessionId, filePath, content, nextVersion, action]
1111
- });
1112
- return nextVersion;
1113
- },
1114
-
1115
- getVersions: async (sessionId, filePath, limit = 50) => {
1116
- const result = await db.execute({
1117
- sql: 'SELECT id, version, action, created_at FROM file_versions WHERE session_id = ? AND file_path = ? ORDER BY version DESC LIMIT ?',
1118
- args: [sessionId, filePath, limit]
1119
- });
1120
- return result.rows;
1121
- },
1122
-
1123
- getVersion: async (sessionId, filePath, version) => {
1124
- const result = await db.execute({
1125
- sql: 'SELECT * FROM file_versions WHERE session_id = ? AND file_path = ? AND version = ?',
1126
- args: [sessionId, filePath, version]
1127
- });
1128
- return result.rows[0] || null;
1129
- },
1130
-
1131
- getLatest: async (sessionId, filePath) => {
1132
- const result = await db.execute({
1133
- sql: 'SELECT * FROM file_versions WHERE session_id = ? AND file_path = ? ORDER BY version DESC LIMIT 1',
1134
- args: [sessionId, filePath]
1135
- });
1136
- return result.rows[0] || null;
1137
- },
1138
-
1139
- getSessionFiles: async (sessionId) => {
1140
- const result = await db.execute({
1141
- sql: 'SELECT DISTINCT file_path, MAX(version) as latest_version, COUNT(*) as total_versions FROM file_versions WHERE session_id = ? GROUP BY file_path',
1142
- args: [sessionId]
1143
- });
1144
- return result.rows;
1145
- },
1146
- };
1147
-
1148
- // ─── Session Usage / Cost Tracking (opencode pattern: per-session token+cost tracking) ───
1149
-
1150
- const sessionUsageDb = {
1151
- upsert: async (userId, sessionId, { provider, promptTokens, completionTokens, costCents, model }) => {
1152
- // Try update first, then insert
1153
- const existing = await db.execute({
1154
- sql: 'SELECT id FROM session_usage WHERE session_id = ? AND user_id = ?',
1155
- args: [sessionId, userId]
1156
- });
1157
- if (existing.rows.length > 0) {
1158
- await db.execute({
1159
- sql: `UPDATE session_usage SET
1160
- prompt_tokens = prompt_tokens + ?,
1161
- completion_tokens = completion_tokens + ?,
1162
- cost_cents = cost_cents + ?,
1163
- message_count = message_count + 1,
1164
- model = COALESCE(?, model),
1165
- updated_at = CURRENT_TIMESTAMP
1166
- WHERE session_id = ? AND user_id = ?`,
1167
- args: [promptTokens || 0, completionTokens || 0, costCents || 0, model, sessionId, userId]
1168
- });
1169
- } else {
1170
- await db.execute({
1171
- sql: 'INSERT INTO session_usage (user_id, session_id, provider, prompt_tokens, completion_tokens, cost_cents, model, message_count) VALUES (?, ?, ?, ?, ?, ?, ?, 1)',
1172
- args: [userId, sessionId, provider || 'claude', promptTokens || 0, completionTokens || 0, costCents || 0, model]
1173
- });
1174
- }
1175
- },
1176
-
1177
- getSession: async (sessionId) => {
1178
- const result = await db.execute({
1179
- sql: 'SELECT * FROM session_usage WHERE session_id = ?',
1180
- args: [sessionId]
1181
- });
1182
- return result.rows[0] || null;
1183
- },
1184
-
1185
- getUserUsage: async (userId, days = 30) => {
1186
- const result = await db.execute({
1187
- sql: `SELECT
1188
- SUM(prompt_tokens) as total_prompt_tokens,
1189
- SUM(completion_tokens) as total_completion_tokens,
1190
- SUM(cost_cents) as total_cost_cents,
1191
- SUM(message_count) as total_messages,
1192
- COUNT(*) as total_sessions
1193
- FROM session_usage
1194
- WHERE user_id = ? AND created_at >= datetime('now', '-' || ? || ' days')`,
1195
- args: [userId, days]
1196
- });
1197
- return result.rows[0] || null;
1198
- },
1199
-
1200
- getUserSessions: async (userId, limit = 20) => {
1201
- const result = await db.execute({
1202
- sql: 'SELECT * FROM session_usage WHERE user_id = ? ORDER BY updated_at DESC LIMIT ?',
1203
- args: [userId, limit]
1204
- });
1205
- return result.rows;
1206
- },
1207
- };
1208
-
1209
- // ─── Active Connections DB ────────────────────────────────────────────────────────
1210
-
1211
- const connectionDb = {
1212
- connect: async (userId, projectName, type = 'relay', sandboxId = null, extra = {}) => {
1213
- const { cwd, machine, platform } = extra;
1214
- // Upsert: if user already has this connection type, update it
1215
- const existing = await db.execute({
1216
- sql: 'SELECT id FROM active_connections WHERE user_id = ? AND connection_type = ?',
1217
- args: [String(userId), type]
1218
- });
1219
- if (existing.rows.length > 0) {
1220
- await db.execute({
1221
- sql: 'UPDATE active_connections SET project_name = ?, sandbox_id = ?, last_cwd = COALESCE(?, last_cwd), last_machine = COALESCE(?, last_machine), last_platform = COALESCE(?, last_platform), connected_at = CURRENT_TIMESTAMP, disconnected_at = NULL WHERE user_id = ? AND connection_type = ?',
1222
- args: [projectName, sandboxId, cwd, machine, platform, String(userId), type]
1223
- });
1224
- } else {
1225
- await db.execute({
1226
- sql: 'INSERT INTO active_connections (user_id, project_name, connection_type, sandbox_id, last_cwd, last_machine, last_platform) VALUES (?, ?, ?, ?, ?, ?, ?)',
1227
- args: [String(userId), projectName, type, sandboxId, cwd, machine, platform]
1228
- });
1229
- }
1230
- },
1231
-
1232
- disconnect: async (userId, type = 'relay') => {
1233
- await db.execute({
1234
- sql: 'UPDATE active_connections SET disconnected_at = CURRENT_TIMESTAMP WHERE user_id = ? AND connection_type = ? AND disconnected_at IS NULL',
1235
- args: [String(userId), type]
1236
- });
1237
- },
1238
-
1239
- getActive: async (userId) => {
1240
- const result = await db.execute({
1241
- sql: 'SELECT * FROM active_connections WHERE user_id = ? AND disconnected_at IS NULL',
1242
- args: [String(userId)]
1243
- });
1244
- return result.rows;
1245
- },
1246
-
1247
- getAllActive: async () => {
1248
- const result = await db.execute({
1249
- sql: 'SELECT * FROM active_connections WHERE disconnected_at IS NULL ORDER BY connected_at DESC'
1250
- });
1251
- return result.rows;
1252
- },
1253
- };
1254
-
1255
- // ─── Canvas DB ───────────────────────────────────────────────────────────────────
1256
-
1257
- const canvasDb = {
1258
- save: async (userId, projectName, elements, appState) => {
1259
- const elementsJson = typeof elements === 'string' ? elements : JSON.stringify(elements);
1260
- const appStateJson = typeof appState === 'string' ? appState : JSON.stringify(appState);
1261
- const existing = await db.execute({
1262
- sql: 'SELECT id FROM project_canvases WHERE user_id = ? AND project_name = ?',
1263
- args: [userId, projectName]
1264
- });
1265
- if (existing.rows.length > 0) {
1266
- await db.execute({
1267
- sql: 'UPDATE project_canvases SET elements = ?, app_state = ?, updated_at = CURRENT_TIMESTAMP WHERE user_id = ? AND project_name = ?',
1268
- args: [elementsJson, appStateJson, userId, projectName]
1269
- });
1270
- } else {
1271
- await db.execute({
1272
- sql: 'INSERT INTO project_canvases (user_id, project_name, elements, app_state) VALUES (?, ?, ?, ?)',
1273
- args: [userId, projectName, elementsJson, appStateJson]
1274
- });
1275
- }
1276
- },
1277
-
1278
- load: async (userId, projectName) => {
1279
- const result = await db.execute({
1280
- sql: 'SELECT elements, app_state, updated_at FROM project_canvases WHERE user_id = ? AND project_name = ?',
1281
- args: [userId, projectName]
1282
- });
1283
- const row = getRow(result);
1284
- if (!row) return null;
1285
- return {
1286
- elements: JSON.parse(row.elements || '[]'),
1287
- appState: JSON.parse(row.app_state || '{}'),
1288
- updatedAt: row.updated_at,
1289
- };
1290
- },
1291
-
1292
- delete: async (userId, projectName) => {
1293
- const result = await db.execute({
1294
- sql: 'DELETE FROM project_canvases WHERE user_id = ? AND project_name = ?',
1295
- args: [userId, projectName]
1296
- });
1297
- return result.rowsAffected > 0;
1298
- },
1299
- };
1300
-
1301
- // ─── Password Reset Tokens DB ────────────────────────────────────────────────
1302
-
1303
- const resetTokenDb = {
1304
- create: async (userId, token, expiresAt) => {
1305
- const result = await db.execute({
1306
- sql: 'INSERT INTO password_reset_tokens (user_id, token, expires_at) VALUES (?, ?, ?)',
1307
- args: [userId, token, expiresAt]
1308
- });
1309
- return { id: Number(result.lastInsertRowid), token };
1310
- },
1311
-
1312
- getValid: async (token) => {
1313
- const result = await db.execute({
1314
- sql: 'SELECT * FROM password_reset_tokens WHERE token = ? AND used = 0 AND expires_at > datetime(\'now\')',
1315
- args: [token]
1316
- });
1317
- return getRow(result);
1318
- },
1319
-
1320
- markUsed: async (tokenId) => {
1321
- await db.execute({
1322
- sql: 'UPDATE password_reset_tokens SET used = 1 WHERE id = ?',
1323
- args: [tokenId]
1324
- });
1325
- },
1326
-
1327
- cleanExpired: async () => {
1328
- await db.execute("DELETE FROM password_reset_tokens WHERE expires_at < datetime('now') OR used = 1");
1329
- }
1330
- };
1331
-
1332
- // ─── Projects DB (cloud mode) ─────────────────────────────────────────────────
1333
-
1334
- const projectDb = {
1335
- upsert: async (userId, originalPath, displayName = null, githubOrigin = null) => {
1336
- const projectName = originalPath.replace(/[\\/:\s~_]/g, '-');
1337
- const existing = await db.execute({
1338
- sql: 'SELECT id FROM user_projects WHERE user_id = ? AND original_path = ?',
1339
- args: [userId, originalPath]
1340
- });
1341
- if (existing.rows.length > 0) {
1342
- if (displayName || githubOrigin) {
1343
- const updates = [];
1344
- const args = [];
1345
- if (displayName) { updates.push('display_name = ?'); args.push(displayName); }
1346
- if (githubOrigin) { updates.push('github_origin = ?'); args.push(githubOrigin); }
1347
- args.push(existing.rows[0].id);
1348
- await db.execute({ sql: `UPDATE user_projects SET ${updates.join(', ')} WHERE id = ?`, args });
1349
- }
1350
- return { id: Number(existing.rows[0].id), projectName, originalPath, alreadyExists: true };
1351
- }
1352
- const result = await db.execute({
1353
- sql: 'INSERT INTO user_projects (user_id, project_name, original_path, display_name, github_origin) VALUES (?, ?, ?, ?, ?)',
1354
- args: [userId, projectName, originalPath, displayName, githubOrigin]
1355
- });
1356
- return { id: Number(result.lastInsertRowid), projectName, originalPath };
1357
- },
1358
-
1359
- getAll: async (userId) => {
1360
- const result = await db.execute({
1361
- sql: 'SELECT * FROM user_projects WHERE user_id = ? ORDER BY created_at DESC',
1362
- args: [userId]
1363
- });
1364
- return result.rows;
1365
- },
1366
-
1367
- remove: async (userId, originalPath) => {
1368
- const result = await db.execute({
1369
- sql: 'DELETE FROM user_projects WHERE user_id = ? AND original_path = ?',
1370
- args: [userId, originalPath]
1371
- });
1372
- return result.rowsAffected > 0;
1373
- },
1374
-
1375
- rename: async (userId, originalPath, displayName) => {
1376
- const result = await db.execute({
1377
- sql: 'UPDATE user_projects SET display_name = ? WHERE user_id = ? AND original_path = ?',
1378
- args: [displayName, userId, originalPath]
1379
- });
1380
- return result.rowsAffected > 0;
1381
- },
1382
-
1383
- getByName: async (userId, projectName) => {
1384
- const result = await db.execute({
1385
- sql: 'SELECT * FROM user_projects WHERE user_id = ? AND project_name = ?',
1386
- args: [userId, projectName]
1387
- });
1388
- return result.rows[0] || null;
1389
- }
1390
- };
1391
-
1392
- // ─── Voice Calls DB ───────────────────────────────────────────────────────────
1393
-
1394
- const voiceCallDb = {
1395
- // Create a call record when call starts (or when we first get a webhook)
1396
- create: async (userId, callType = 'voice', sessionId = null) => {
1397
- const result = await db.execute({
1398
- sql: 'INSERT INTO voice_calls (user_id, call_type, session_id, status) VALUES (?, ?, ?, ?)',
1399
- args: [userId, callType, sessionId, 'in_progress']
1400
- });
1401
- return Number(result.lastInsertRowid);
1402
- },
1403
-
1404
- // Update call with VAPI call ID (from first webhook event)
1405
- setVapiCallId: async (id, vapiCallId) => {
1406
- await db.execute({
1407
- sql: 'UPDATE voice_calls SET vapi_call_id = ? WHERE id = ?',
1408
- args: [vapiCallId, id]
1409
- });
1410
- },
1411
-
1412
- // Complete a call with end-of-call-report data
1413
- complete: async (userId, vapiCallId, data) => {
1414
- const { status, endedReason, durationSeconds, cost, transcript, summary, toolsUsed, messagesCount } = data;
1415
- // Try to update existing row by vapi_call_id, or by most recent in-progress call
1416
- const existing = vapiCallId
1417
- ? await db.execute({ sql: 'SELECT id FROM voice_calls WHERE vapi_call_id = ?', args: [vapiCallId] })
1418
- : await db.execute({ sql: 'SELECT id FROM voice_calls WHERE user_id = ? AND status = ? ORDER BY started_at DESC LIMIT 1', args: [userId, 'in_progress'] });
1419
-
1420
- if (existing.rows.length > 0) {
1421
- await db.execute({
1422
- sql: `UPDATE voice_calls SET status = ?, ended_reason = ?, ended_at = CURRENT_TIMESTAMP,
1423
- duration_seconds = ?, cost = ?, transcript = ?, summary = ?, tools_used = ?,
1424
- messages_count = ? WHERE id = ?`,
1425
- args: [status || 'completed', endedReason || null, durationSeconds || null, cost || null,
1426
- transcript || null, summary || null, toolsUsed || null, messagesCount || 0,
1427
- existing.rows[0].id]
1428
- });
1429
- return Number(existing.rows[0].id);
1430
- }
1431
-
1432
- // No existing row — insert complete record
1433
- const result = await db.execute({
1434
- sql: `INSERT INTO voice_calls (user_id, vapi_call_id, call_type, status, ended_reason, ended_at,
1435
- duration_seconds, cost, transcript, summary, tools_used, messages_count)
1436
- VALUES (?, ?, 'voice', ?, ?, CURRENT_TIMESTAMP, ?, ?, ?, ?, ?, ?)`,
1437
- args: [userId, vapiCallId || null, status || 'completed', endedReason || null,
1438
- durationSeconds || null, cost || null, transcript || null, summary || null,
1439
- toolsUsed || null, messagesCount || 0]
1440
- });
1441
- return Number(result.lastInsertRowid);
1442
- },
1443
-
1444
- // Save a chat call (non-voice, from VAPI Chat API)
1445
- saveChat: async (userId, sessionId, chatId, messagesCount = 1) => {
1446
- const result = await db.execute({
1447
- sql: `INSERT INTO voice_calls (user_id, vapi_call_id, session_id, call_type, status, ended_at, messages_count)
1448
- VALUES (?, ?, ?, 'chat', 'completed', CURRENT_TIMESTAMP, ?)`,
1449
- args: [userId, chatId || null, sessionId || null, messagesCount]
1450
- });
1451
- return Number(result.lastInsertRowid);
1452
- },
1453
-
1454
- // Get user's call history
1455
- getByUser: async (userId, limit = 20, offset = 0) => {
1456
- const result = await db.execute({
1457
- sql: 'SELECT * FROM voice_calls WHERE user_id = ? ORDER BY started_at DESC LIMIT ? OFFSET ?',
1458
- args: [userId, limit, offset]
1459
- });
1460
- return result.rows;
1461
- },
1462
-
1463
- // Get user stats (total calls, total duration, etc.)
1464
- getUserStats: async (userId) => {
1465
- const result = await db.execute({
1466
- sql: `SELECT
1467
- COUNT(*) as total_calls,
1468
- SUM(CASE WHEN call_type = 'voice' THEN 1 ELSE 0 END) as voice_calls,
1469
- SUM(CASE WHEN call_type = 'chat' THEN 1 ELSE 0 END) as chat_calls,
1470
- SUM(COALESCE(duration_seconds, 0)) as total_duration_seconds,
1471
- SUM(COALESCE(messages_count, 0)) as total_messages,
1472
- MAX(started_at) as last_call_at
1473
- FROM voice_calls WHERE user_id = ?`,
1474
- args: [userId]
1475
- });
1476
- return getRow(result) || { total_calls: 0, voice_calls: 0, chat_calls: 0, total_duration_seconds: 0, total_messages: 0, last_call_at: null };
1477
- },
1478
-
1479
- // Get call by VAPI call ID
1480
- getByVapiId: async (vapiCallId) => {
1481
- const result = await db.execute({
1482
- sql: 'SELECT * FROM voice_calls WHERE vapi_call_id = ?',
1483
- args: [vapiCallId]
1484
- });
1485
- return getRow(result);
1486
- },
1487
- };
1488
-
1489
- // ─── Browser Sessions DB ─────────────────────────────────────────────────────
1490
-
1491
- const browserSessionDb = {
1492
- create: async (userId, steelSessionId, viewerUrl = null, cdpUrl = null, metadata = {}) => {
1493
- const result = await db.execute({
1494
- sql: 'INSERT INTO browser_sessions (user_id, steel_session_id, viewer_url, cdp_url, metadata) VALUES (?, ?, ?, ?, ?)',
1495
- args: [userId, steelSessionId, viewerUrl, cdpUrl, JSON.stringify(metadata)]
1496
- });
1497
- return { id: Number(result.lastInsertRowid), steelSessionId };
1498
- },
1499
-
1500
- getActive: async (userId) => {
1501
- const result = await db.execute({
1502
- sql: "SELECT * FROM browser_sessions WHERE user_id = ? AND status = 'active' ORDER BY created_at DESC LIMIT 1",
1503
- args: [userId]
1504
- });
1505
- return getRow(result);
1506
- },
1507
-
1508
- getBySessionId: async (userId, steelSessionId) => {
1509
- const result = await db.execute({
1510
- sql: 'SELECT * FROM browser_sessions WHERE user_id = ? AND steel_session_id = ?',
1511
- args: [userId, steelSessionId]
1512
- });
1513
- return getRow(result);
1514
- },
1515
-
1516
- updateAccess: async (userId, steelSessionId) => {
1517
- await db.execute({
1518
- sql: "UPDATE browser_sessions SET last_accessed = datetime('now') WHERE user_id = ? AND steel_session_id = ?",
1519
- args: [userId, steelSessionId]
1520
- });
1521
- },
1522
-
1523
- deactivate: async (userId, steelSessionId) => {
1524
- await db.execute({
1525
- sql: "UPDATE browser_sessions SET status = 'closed' WHERE user_id = ? AND steel_session_id = ?",
1526
- args: [userId, steelSessionId]
1527
- });
1528
- },
1529
-
1530
- listByUser: async (userId) => {
1531
- const result = await db.execute({
1532
- sql: 'SELECT * FROM browser_sessions WHERE user_id = ? ORDER BY created_at DESC LIMIT 20',
1533
- args: [userId]
1534
- });
1535
- return result.rows;
1536
- },
1537
-
1538
- cleanupStale: async (maxAgeMinutes = 30) => {
1539
- const result = await db.execute({
1540
- sql: `UPDATE browser_sessions SET status = 'expired' WHERE status = 'active' AND last_accessed < datetime('now', '-' || ? || ' minutes')`,
1541
- args: [maxAgeMinutes]
1542
- });
1543
- return result.rowsAffected || 0;
1544
- },
1545
- };
1546
-
1547
- export { db, initializeDatabase, userDb, apiKeysDb, credentialsDb, relayTokensDb, githubTokensDb, subscriptionDb, paymentDb, webhookDb, workflowDb, fileVersionDb, sessionUsageDb, connectionDb, canvasDb, resetTokenDb, projectDb, voiceCallDb, browserSessionDb, PLAN_DURATIONS };