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