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,624 +0,0 @@
1
- import express from 'express';
2
- import crypto from 'crypto';
3
- import jwt from 'jsonwebtoken';
4
- import { userDb, subscriptionDb, voiceCallDb } from '../database/db.js';
5
-
6
- const router = express.Router();
7
-
8
- const VAPI_PRIVATE_KEY = process.env.VAPI_PRIVATE_KEY;
9
- const VAPI_PUBLIC_KEY = process.env.VAPI_PUBLIC_KEY;
10
- const VAPI_ASSISTANT_ID = process.env.VAPI_ASSISTANT_ID;
11
- const JWT_SECRET = process.env.JWT_SECRET?.trim();
12
-
13
- // ─── Session Context Store ──────────────────────────────────────────────────
14
- // Reliable user identification for VAPI webhook callbacks.
15
- // Problem: VAPI doesn't reliably propagate assistantOverrides.metadata back to
16
- // the webhook for tool-calls (especially for Chat API). So we store user context
17
- // server-side keyed by a short-lived sessionId, and pass that sessionId in metadata.
18
- // The webhook reads sessionId from metadata and looks up the full user context here.
19
-
20
- const sessionStore = new Map();
21
- const SESSION_TTL_MS = 60 * 60 * 1000; // 1 hour
22
-
23
- function createSession(userCtx) {
24
- const sessionId = `vs_${crypto.randomBytes(12).toString('hex')}`;
25
- sessionStore.set(sessionId, { userCtx, createdAt: Date.now() });
26
- return sessionId;
27
- }
28
-
29
- function getSessionContext(sessionId) {
30
- if (!sessionId) return null;
31
- const entry = sessionStore.get(sessionId);
32
- if (!entry) return null;
33
- if (Date.now() - entry.createdAt > SESSION_TTL_MS) {
34
- sessionStore.delete(sessionId);
35
- return null;
36
- }
37
- return entry.userCtx;
38
- }
39
-
40
- // Cleanup expired sessions every 15 minutes
41
- setInterval(() => {
42
- const now = Date.now();
43
- for (const [key, entry] of sessionStore) {
44
- if (now - entry.createdAt > SESSION_TTL_MS) sessionStore.delete(key);
45
- }
46
- }, 15 * 60 * 1000).unref();
47
-
48
- // ─── Helpers ────────────────────────────────────────────────────────────────
49
-
50
- function friendlyPlan(planId) {
51
- if (!planId) return 'free';
52
- const p = planId.toLowerCase();
53
- if (p.includes('month')) return 'Monthly';
54
- if (p.includes('half') || p.includes('6')) return 'Half-Yearly';
55
- if (p.includes('year') || p.includes('annual')) return 'Annual';
56
- return planId;
57
- }
58
-
59
- function daysBetween(a, b) {
60
- return Math.ceil((new Date(b) - new Date(a)) / 86400000);
61
- }
62
-
63
- async function tryGetUser(req) {
64
- try {
65
- let token = req.cookies?.session;
66
- if (!token) {
67
- const auth = req.headers['authorization'];
68
- if (auth?.startsWith('Bearer ')) token = auth.slice(7);
69
- }
70
- if (!token || !JWT_SECRET) return null;
71
-
72
- const decoded = jwt.verify(token, JWT_SECRET);
73
- const user = await userDb.getUserById(decoded.userId);
74
- if (!user) return null;
75
-
76
- const sub = await subscriptionDb.getActiveSub(user.id);
77
-
78
- const fullName = [user.first_name, user.last_name].filter(Boolean).join(' ') || user.username || 'AI Enthusiast';
79
- const accountAgeDays = daysBetween(user.created_at, new Date());
80
-
81
- const ctx = {
82
- userId: String(user.id),
83
- userName: user.first_name || user.username || 'AI Enthusiast',
84
- fullName,
85
- userEmail: user.email || null,
86
- phone: user.phone || null,
87
- accountAgeDays,
88
- memberSince: user.created_at ? new Date(user.created_at).toLocaleDateString('en-IN', { month: 'long', year: 'numeric' }) : null,
89
- };
90
-
91
- if (sub) {
92
- const daysLeft = daysBetween(new Date(), sub.expires_at);
93
- ctx.userPlan = friendlyPlan(sub.plan_id);
94
- ctx.planStatus = sub.status;
95
- ctx.planExpiresAt = sub.expires_at;
96
- ctx.planDaysLeft = daysLeft;
97
- ctx.isPro = true;
98
- } else {
99
- ctx.userPlan = 'free';
100
- ctx.isPro = false;
101
- }
102
-
103
- return ctx;
104
- } catch {
105
- return null;
106
- }
107
- }
108
-
109
- const GUEST_CONTEXT = { userName: 'AI Enthusiast', userPlan: 'not logged in' };
110
-
111
- // ─── Metadata extraction: check every possible path VAPI might use ──────────
112
-
113
- function extractMetadata(message) {
114
- // VAPI places metadata in different locations depending on voice vs chat
115
- return message.call?.assistantOverrides?.metadata
116
- || message.call?.metadata
117
- || message.chat?.assistantOverrides?.metadata
118
- || message.chat?.metadata
119
- || message.metadata
120
- || {};
121
- }
122
-
123
- // Resolve user context from metadata — tries session store first, then userId fallback
124
- async function resolveUserFromMetadata(metadata) {
125
- // Priority 1: Session store (most reliable — we stored context server-side)
126
- if (metadata.sessionId) {
127
- const ctx = getSessionContext(metadata.sessionId);
128
- if (ctx) return ctx;
129
- }
130
-
131
- // Priority 2: Direct userId in metadata (fallback if session expired)
132
- if (metadata.userId) {
133
- return { userId: metadata.userId, ...metadata };
134
- }
135
-
136
- return null;
137
- }
138
-
139
- // ─── POST /api/vapi/webhook — VAPI Server URL handler ──────────────────────
140
- // VAPI calls this during conversations for tool-calls.
141
- // Tools handled:
142
- // - getUserContext: fetches user name/plan/email
143
- // - troubleshootConnection: checks CLI relay status
144
- // - getSubscriptionDetails: detailed subscription info
145
- // - getPricingInfo: current pricing plans
146
- // - getFeatureComparison: free vs pro features
147
- // - endCall: ends the current call
148
-
149
- router.post('/webhook', async (req, res) => {
150
- const { message } = req.body;
151
- if (!message?.type) {
152
- return res.status(400).json({ error: 'Invalid VAPI server event' });
153
- }
154
-
155
- switch (message.type) {
156
- case 'tool-calls': {
157
- const metadata = extractMetadata(message);
158
-
159
- const results = await Promise.all(
160
- (message.toolCallList || []).map(async (toolCall) => {
161
- const fn = toolCall.function;
162
-
163
- if (fn.name === 'getUserContext') {
164
- return {
165
- toolCallId: toolCall.id,
166
- result: JSON.stringify(await handleGetUserContext(metadata)),
167
- };
168
- }
169
-
170
- if (fn.name === 'troubleshootConnection') {
171
- return {
172
- toolCallId: toolCall.id,
173
- result: JSON.stringify(await handleTroubleshootConnection(metadata)),
174
- };
175
- }
176
-
177
- if (fn.name === 'getSubscriptionDetails') {
178
- return {
179
- toolCallId: toolCall.id,
180
- result: JSON.stringify(await handleGetSubscriptionDetails(metadata)),
181
- };
182
- }
183
-
184
- if (fn.name === 'getPricingInfo') {
185
- return {
186
- toolCallId: toolCall.id,
187
- result: JSON.stringify(handleGetPricingInfo()),
188
- };
189
- }
190
-
191
- if (fn.name === 'getFeatureComparison') {
192
- return {
193
- toolCallId: toolCall.id,
194
- result: JSON.stringify(handleGetFeatureComparison()),
195
- };
196
- }
197
-
198
- if (fn.name === 'endCall') {
199
- return {
200
- toolCallId: toolCall.id,
201
- result: JSON.stringify({ action: 'end_call', message: 'Call ended by assistant.' }),
202
- };
203
- }
204
-
205
- return { toolCallId: toolCall.id, result: '{}' };
206
- })
207
- );
208
- return res.json({ results });
209
- }
210
-
211
- case 'assistant-request': {
212
- if (VAPI_ASSISTANT_ID) {
213
- return res.json({ assistantId: VAPI_ASSISTANT_ID });
214
- }
215
- return res.json({ error: 'No assistant configured' });
216
- }
217
-
218
- case 'end-of-call-report': {
219
- // Persist call data to Turso, then clean up session
220
- const metadata = extractMetadata(message);
221
- const userCtx = metadata.sessionId ? getSessionContext(metadata.sessionId) : null;
222
- const userId = userCtx?.userId || metadata.userId;
223
-
224
- if (userId) {
225
- try {
226
- const call = message.call || message;
227
- const vapiCallId = call.id || call.callId || null;
228
-
229
- // Build transcript from messages array
230
- const msgs = message.artifact?.messages || call.messages || [];
231
- const transcriptLines = msgs
232
- .filter(m => m.role && m.message)
233
- .map(m => `${m.role}: ${m.message}`)
234
- .join('\n');
235
-
236
- // Collect tools used
237
- const toolsUsed = msgs
238
- .filter(m => m.role === 'tool_calls' || m.toolCalls)
239
- .flatMap(m => (m.toolCalls || []).map(t => t.function?.name))
240
- .filter(Boolean);
241
-
242
- await voiceCallDb.complete(userId, vapiCallId, {
243
- status: call.status || 'completed',
244
- endedReason: call.endedReason || message.endedReason || null,
245
- durationSeconds: call.duration ? Math.round(call.duration) : (message.durationSeconds || null),
246
- cost: call.cost || message.cost || null,
247
- transcript: transcriptLines || null,
248
- summary: message.artifact?.summary || message.summary || null,
249
- toolsUsed: toolsUsed.length > 0 ? JSON.stringify(toolsUsed) : null,
250
- messagesCount: msgs.length || 0,
251
- });
252
- } catch { /* Best-effort persistence — don't block webhook response */ }
253
- }
254
-
255
- if (metadata.sessionId) sessionStore.delete(metadata.sessionId);
256
- return res.json({});
257
- }
258
-
259
- case 'status-update': {
260
- // Track call start — create initial record in Turso
261
- const metadata2 = extractMetadata(message);
262
- const userCtx2 = metadata2.sessionId ? getSessionContext(metadata2.sessionId) : null;
263
- const userId2 = userCtx2?.userId || metadata2.userId;
264
- const callStatus = message.status || message.call?.status;
265
-
266
- if (userId2 && callStatus === 'in-progress') {
267
- try {
268
- const vapiCallId = message.call?.id || null;
269
- const callId = await voiceCallDb.create(userId2, 'voice', metadata2.sessionId || null);
270
- if (vapiCallId) await voiceCallDb.setVapiCallId(callId, vapiCallId);
271
- } catch { /* Best-effort */ }
272
- }
273
- return res.json({});
274
- }
275
-
276
- case 'conversation-update':
277
- case 'transcript':
278
- case 'speech-update':
279
- case 'hang':
280
- return res.json({});
281
-
282
- default:
283
- return res.json({});
284
- }
285
- });
286
-
287
- // ─── Tool handlers ──────────────────────────────────────────────────────────
288
-
289
- async function handleGetUserContext(metadata) {
290
- try {
291
- const resolved = await resolveUserFromMetadata(metadata);
292
-
293
- if (!resolved || !resolved.userId) {
294
- return {
295
- name: 'AI Enthusiast',
296
- plan: 'not logged in',
297
- isPro: false,
298
- note: 'User is not signed in — treat them warmly as a curious visitor and encourage sign-up.',
299
- };
300
- }
301
-
302
- const userId = resolved.userId;
303
- const user = await userDb.getUserById(userId);
304
- if (!user) {
305
- return { name: 'AI Enthusiast', plan: 'not logged in', isPro: false, note: 'User not found.' };
306
- }
307
-
308
- const sub = await subscriptionDb.getActiveSub(user.id);
309
- const fullName = [user.first_name, user.last_name].filter(Boolean).join(' ') || user.username;
310
- const accountAgeDays = daysBetween(user.created_at, new Date());
311
-
312
- const ctx = {
313
- name: user.first_name || user.username,
314
- fullName,
315
- email: user.email || null,
316
- phone: user.phone || null,
317
- accountAgeDays,
318
- memberSince: user.created_at ? new Date(user.created_at).toLocaleDateString('en-IN', { month: 'long', year: 'numeric' }) : null,
319
- };
320
-
321
- if (sub) {
322
- const daysLeft = daysBetween(new Date(), sub.expires_at);
323
- ctx.plan = friendlyPlan(sub.plan_id);
324
- ctx.planStatus = sub.status;
325
- ctx.planExpiresAt = sub.expires_at;
326
- ctx.planDaysLeft = daysLeft;
327
- ctx.isPro = true;
328
- ctx.note = daysLeft <= 7
329
- ? `Subscription expires in ${daysLeft} days — gently remind them to renew.`
330
- : `Active ${friendlyPlan(sub.plan_id)} subscriber.`;
331
- } else {
332
- ctx.plan = 'free';
333
- ctx.isPro = false;
334
- ctx.note = 'Free tier user — if relevant, mention the benefits of upgrading.';
335
- }
336
-
337
- return ctx;
338
- } catch {
339
- return { name: 'AI Enthusiast', plan: 'unknown', isPro: false, note: 'Lookup failed.' };
340
- }
341
- }
342
-
343
- async function handleGetSubscriptionDetails(metadata) {
344
- try {
345
- const resolved = await resolveUserFromMetadata(metadata);
346
- if (!resolved?.userId) {
347
- return {
348
- hasSubscription: false,
349
- message: 'User is not logged in. They need to sign in to view subscription details.',
350
- suggestion: 'Encourage the user to sign up or log in at cli.upfyn.com',
351
- };
352
- }
353
-
354
- const sub = await subscriptionDb.getActiveSub(resolved.userId);
355
- if (!sub) {
356
- return {
357
- hasSubscription: false,
358
- currentPlan: 'free',
359
- message: 'User is on the free tier (BYOK — Bring Your Own Key). No active paid subscription.',
360
- suggestion: 'Mention the Pro plan benefits if they ask about upgrading.',
361
- };
362
- }
363
-
364
- const daysLeft = daysBetween(new Date(), sub.expires_at);
365
- return {
366
- hasSubscription: true,
367
- currentPlan: friendlyPlan(sub.plan_id),
368
- status: sub.status,
369
- expiresAt: sub.expires_at,
370
- daysRemaining: daysLeft,
371
- autoRenew: sub.status === 'active',
372
- message: daysLeft <= 7
373
- ? `Subscription expires in ${daysLeft} days. Remind them to renew.`
374
- : `Active ${friendlyPlan(sub.plan_id)} subscription with ${daysLeft} days remaining.`,
375
- };
376
- } catch {
377
- return { hasSubscription: false, message: 'Could not fetch subscription details.' };
378
- }
379
- }
380
-
381
- function handleGetPricingInfo() {
382
- return {
383
- currency: 'INR',
384
- plans: [
385
- {
386
- name: 'Free',
387
- price: 0,
388
- billing: 'forever',
389
- features: [
390
- 'Bring Your Own API Key (BYOK)',
391
- 'Connect to Claude Code, Codex, Cursor',
392
- 'Web-based IDE with terminal & file explorer',
393
- 'AI chat with your own API key',
394
- 'Basic canvas/whiteboard',
395
- ],
396
- bestFor: 'Developers who already have API keys and want a visual interface',
397
- },
398
- {
399
- name: 'Monthly Pro',
400
- price: 499,
401
- billing: 'per month',
402
- features: [
403
- 'Everything in Free',
404
- 'Server-provided AI (no API key needed)',
405
- 'Priority support',
406
- 'Advanced canvas features',
407
- 'Voice assistant (Upfyn)',
408
- 'Unlimited projects',
409
- ],
410
- bestFor: 'Developers who want a hassle-free experience',
411
- },
412
- {
413
- name: '6-Month Pro',
414
- price: 2499,
415
- billing: 'every 6 months',
416
- savingsVsMonthly: '17% savings vs monthly',
417
- features: ['Everything in Monthly Pro', 'Lower per-month cost'],
418
- bestFor: 'Committed users who want to save',
419
- },
420
- {
421
- name: 'Annual Pro',
422
- price: 4999,
423
- billing: 'per year',
424
- savingsVsMonthly: '17% savings vs monthly',
425
- currentOffer: 'Launch offer: first year at just ₹499!',
426
- features: ['Everything in Monthly Pro', 'Best value', 'Launch discount available'],
427
- bestFor: 'Best deal — especially with the launch offer',
428
- },
429
- ],
430
- note: 'All prices in INR. Pro plans include server-provided AI so users don\'t need their own API keys.',
431
- };
432
- }
433
-
434
- function handleGetFeatureComparison() {
435
- return {
436
- comparison: {
437
- 'Web IDE (terminal, files, git)': { free: true, pro: true },
438
- 'Connect to Claude Code / Codex / Cursor': { free: true, pro: true },
439
- 'BYOK AI Chat (your own API key)': { free: true, pro: true },
440
- 'Canvas / Whiteboard': { free: 'basic', pro: 'advanced' },
441
- 'Server-provided AI (no API key needed)': { free: false, pro: true },
442
- 'Voice Assistant (Upfyn)': { free: false, pro: true },
443
- 'Priority Support': { free: false, pro: true },
444
- 'Multiple simultaneous projects': { free: 'limited', pro: 'unlimited' },
445
- 'MCP Integration (Claude Desktop, ChatGPT, Cursor)': { free: true, pro: true },
446
- },
447
- summary: 'Free tier is great for developers who have their own API keys. Pro adds server-provided AI, voice assistant, and premium features.',
448
- upgradeUrl: 'https://cli.upfyn.com/pricing',
449
- };
450
- }
451
-
452
- async function handleTroubleshootConnection(metadata) {
453
- try {
454
- const resolved = await resolveUserFromMetadata(metadata);
455
- if (!resolved?.userId) {
456
- return {
457
- connected: false,
458
- message: 'User is not logged in. They need to sign in first, then run "uc connect" on their machine.',
459
- };
460
- }
461
-
462
- const userId = resolved.userId;
463
- const port = process.env.PORT || 3001;
464
- const baseUrl = `http://localhost:${port}`;
465
-
466
- const token = jwt.sign({ userId: Number(userId) }, JWT_SECRET, { expiresIn: '1m' });
467
-
468
- const res = await fetch(`${baseUrl}/api/relay/status`, {
469
- headers: { 'Authorization': `Bearer ${token}` },
470
- });
471
-
472
- if (!res.ok) {
473
- return { connected: false, message: 'Could not check connection status.' };
474
- }
475
-
476
- const data = await res.json();
477
-
478
- if (data.connected) {
479
- return {
480
- connected: true,
481
- message: 'Machine is connected and online! Everything looks good.',
482
- local: data.local || false,
483
- connectedAt: data.connectedAt || null,
484
- };
485
- }
486
-
487
- return {
488
- connected: false,
489
- message: 'Machine is NOT connected. Tell the user to open a terminal and run: uc connect',
490
- troubleshooting: [
491
- 'Make sure upfynai-code is installed: npm install -g @upfynai-code/app',
492
- 'Run: uc connect',
493
- 'Check if their firewall is blocking WebSocket connections',
494
- 'Try: uc doctor — to diagnose issues',
495
- ],
496
- };
497
- } catch {
498
- return { connected: false, message: 'Could not check connection status.' };
499
- }
500
- }
501
-
502
- // ─── POST /api/vapi/chat — Proxy to VAPI Chat API ──────────────────────────
503
- // Body: { message?, chatId? }
504
- // Returns: { reply, chatId }
505
-
506
- router.post('/chat', async (req, res) => {
507
- if (!VAPI_PRIVATE_KEY || !VAPI_ASSISTANT_ID) {
508
- return res.status(503).json({ error: 'Chat not configured' });
509
- }
510
-
511
- const { message, chatId } = req.body;
512
-
513
- try {
514
- const userCtx = await tryGetUser(req);
515
- const vars = userCtx || GUEST_CONTEXT;
516
-
517
- // Create session so webhook can identify this user
518
- const sessionId = userCtx ? createSession(userCtx) : null;
519
-
520
- const metadata = userCtx
521
- ? { ...userCtx, sessionId }
522
- : {};
523
-
524
- const body = {
525
- assistantId: VAPI_ASSISTANT_ID,
526
- assistantOverrides: {
527
- metadata,
528
- variableValues: { userName: vars.userName },
529
- },
530
- };
531
-
532
- if (message && typeof message === 'string') {
533
- body.input = message.slice(0, 2000);
534
- } else {
535
- body.input = `Greet me briefly. My name is ${vars.userName}.`;
536
- }
537
- if (chatId) body.previousChatId = chatId;
538
-
539
- const controller = new AbortController();
540
- const timeout = setTimeout(() => controller.abort(), 30000);
541
-
542
- const response = await fetch('https://api.vapi.ai/chat', {
543
- method: 'POST',
544
- headers: {
545
- 'Authorization': `Bearer ${VAPI_PRIVATE_KEY}`,
546
- 'Content-Type': 'application/json',
547
- },
548
- body: JSON.stringify(body),
549
- signal: controller.signal,
550
- });
551
-
552
- clearTimeout(timeout);
553
-
554
- if (!response.ok) {
555
- const err = await response.text().catch(() => '');
556
- return res.status(response.status).json({ error: 'VAPI chat request failed', details: err });
557
- }
558
-
559
- const data = await response.json();
560
-
561
- const assistantMsg = [...(data.output || [])].reverse().find(o => o.role === 'assistant' && o.content);
562
- const reply = assistantMsg?.content || 'No response';
563
-
564
- const responseChatId = data.chat?.id || chatId || null;
565
-
566
- // Persist chat call to Turso
567
- const chatUserCtx = await tryGetUser(req);
568
- if (chatUserCtx?.userId) {
569
- try {
570
- await voiceCallDb.saveChat(chatUserCtx.userId, sessionId, responseChatId, 1);
571
- } catch { /* Best-effort */ }
572
- }
573
-
574
- res.json({
575
- reply,
576
- chatId: responseChatId,
577
- });
578
- } catch (error) {
579
- if (error.name === 'AbortError') {
580
- return res.status(504).json({ error: 'Chat request timed out' });
581
- }
582
- res.status(500).json({ error: 'Chat request failed' });
583
- }
584
- });
585
-
586
- // ─── POST /api/assistant/call-context — Return assistantOverrides for VAPI SDK
587
- // Frontend calls this to get user-specific overrides, then starts call via VAPI Web SDK
588
- router.post('/call-context', async (req, res) => {
589
- try {
590
- const userCtx = await tryGetUser(req);
591
- const vars = userCtx || GUEST_CONTEXT;
592
-
593
- // Create session so webhook can identify this user during the voice call
594
- const sessionId = userCtx ? createSession(userCtx) : null;
595
-
596
- let greeting;
597
- if (userCtx) {
598
- const name = vars.userName;
599
- if (vars.accountAgeDays <= 1) {
600
- greeting = `Welcome to Upfyn, ${name}! Great to have you on board. How can I help you get started?`;
601
- } else if (vars.isPro) {
602
- greeting = `Hey ${name}! What can I do for you today?`;
603
- } else {
604
- greeting = `Hey ${name}! What can I help you with?`;
605
- }
606
- } else {
607
- greeting = 'Hey there! Welcome to Upfyn. What can I help you with?';
608
- }
609
-
610
- res.json({
611
- metadata: userCtx ? { ...userCtx, sessionId } : {},
612
- variableValues: { userName: vars.userName },
613
- firstMessage: greeting,
614
- });
615
- } catch {
616
- res.json({
617
- metadata: {},
618
- variableValues: { userName: GUEST_CONTEXT.userName },
619
- firstMessage: 'Hey there! Welcome to Upfyn. What can I help you with?',
620
- });
621
- }
622
- });
623
-
624
- export default router;