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.
- package/README.md +69 -92
- package/bin/cli.js +191 -0
- package/dist/client/assets/AppContent-M14Au3SB.js +542 -0
- package/{client/dist/assets/BrowserPanel-0TLEl-IC.js → dist/client/assets/BrowserPanel-TFKm2NDJ.js} +2 -2
- package/dist/client/assets/DashboardPanel-C88HjsCh.js +1 -0
- package/dist/client/assets/FileTree-DvO1xnDE.js +1 -0
- package/{client/dist/assets/GitPanel-C_xFM-N2.js → dist/client/assets/GitPanel-D-slVlyy.js} +2 -2
- package/dist/client/assets/LoginModal-Chi4SYcr.js +21 -0
- package/{client/dist/assets/MarkdownPreview-CESjI261.js → dist/client/assets/MarkdownPreview-CuIix2u9.js} +1 -1
- package/dist/client/assets/MermaidBlock-Dq9uFv82.js +2 -0
- package/dist/client/assets/Onboarding-QYXx24dX.js +1 -0
- package/{client/dist/assets/PreviewPanel-CqCa92Tf.js → dist/client/assets/PreviewPanel-Dd8q-jo0.js} +1 -1
- package/dist/client/assets/SetupForm-CrspaUva.js +1 -0
- package/dist/client/assets/WorkflowsPanel-DIlYAdhB.js +1 -0
- package/dist/client/assets/index-CnNNzw9A.css +1 -0
- package/{client/dist/assets/index-HaY-3pK1.js → dist/client/assets/index-rUkK9FDP.js} +26 -26
- package/{client/dist/assets/vendor-codemirror-D2ALgpaX.js → dist/client/assets/vendor-codemirror-jc6nyJQg.js} +1 -1
- package/{client/dist/assets/vendor-diff-DNQpbhrT.js → dist/client/assets/vendor-diff-THJmAcEI.js} +1 -1
- package/{client/dist/assets/vendor-icons-GyYE35HP.js → dist/client/assets/vendor-icons-CfjIpdrD.js} +145 -155
- package/{client/dist/assets/vendor-markdown-CimbIo6Y.js → dist/client/assets/vendor-markdown-Cdm6NEGf.js} +1 -1
- package/dist/client/assets/vendor-mermaid-DTPaBx-U.js +2559 -0
- package/{client/dist/assets/vendor-react-96lCPsRK.js → dist/client/assets/vendor-react-wFkb6mSf.js} +1 -1
- package/{client/dist/assets/vendor-syntax-LS_Nt30I.js → dist/client/assets/vendor-syntax-C_UZR7tc.js} +1 -1
- package/dist/client/favicon.png +0 -0
- package/dist/client/icons/icon-128x128.png +0 -0
- package/dist/client/icons/icon-144x144.png +0 -0
- package/dist/client/icons/icon-152x152.png +0 -0
- package/dist/client/icons/icon-192x192.png +0 -0
- package/dist/client/icons/icon-384x384.png +0 -0
- package/dist/client/icons/icon-512x512.png +0 -0
- package/dist/client/icons/icon-72x72.png +0 -0
- package/dist/client/icons/icon-96x96.png +0 -0
- package/{client/dist → dist/client}/index.html +37 -36
- package/dist/client/logo-128.png +0 -0
- package/dist/client/logo-256.png +0 -0
- package/dist/client/logo-32.png +0 -0
- package/dist/client/logo-512.png +0 -0
- package/dist/client/logo-64.png +0 -0
- package/dist/client/logo.png +0 -0
- package/{client/dist → dist/client}/manifest.json +12 -12
- package/{client/dist → dist/client}/mcp-docs.html +1 -1
- package/{client/dist → dist/client}/sw.js +2 -2
- package/package.json +56 -105
- package/scripts/postinstall.js +9 -0
- package/scripts/prepublish.js +77 -0
- package/src/animation.js +228 -0
- package/src/auth.js +142 -0
- package/src/config.js +40 -0
- package/src/connect.js +416 -0
- package/src/launch.js +81 -0
- package/src/mcp.js +57 -0
- package/src/permissions.js +140 -0
- package/src/persistent-shell.js +261 -0
- package/src/server.js +54 -0
- package/client/dist/assets/AppContent-CwrTP6TW.js +0 -545
- package/client/dist/assets/CanvasFullScreen-D1GWQsGL.js +0 -1
- package/client/dist/assets/CanvasWorkspace-D7ORj358.js +0 -163
- package/client/dist/assets/DashboardPanel-BV7ybUDe.js +0 -1
- package/client/dist/assets/FileTree-5qfhBqdE.js +0 -1
- package/client/dist/assets/LoginModal-CImJHRjX.js +0 -13
- package/client/dist/assets/MermaidBlock-BFM21cwe.js +0 -2
- package/client/dist/assets/Onboarding-B3cteLu2.js +0 -1
- package/client/dist/assets/SetupForm-P6dsYgHO.js +0 -1
- package/client/dist/assets/WorkflowsPanel-CBoN80kc.js +0 -1
- package/client/dist/assets/index-46kkVu2i.css +0 -1
- package/client/dist/assets/pdf-CE_K4jFx.js +0 -12
- package/client/dist/assets/vendor-canvas-BZV40eAE.css +0 -1
- package/client/dist/assets/vendor-canvas-DvHJ_Pn2.js +0 -49
- package/client/dist/assets/vendor-mermaid-DucWyDEe.js +0 -2556
- package/client/dist/favicon.png +0 -0
- package/client/dist/icons/icon-128x128.png +0 -0
- package/client/dist/icons/icon-144x144.png +0 -0
- package/client/dist/icons/icon-152x152.png +0 -0
- package/client/dist/icons/icon-192x192.png +0 -0
- package/client/dist/icons/icon-384x384.png +0 -0
- package/client/dist/icons/icon-512x512.png +0 -0
- package/client/dist/icons/icon-72x72.png +0 -0
- package/client/dist/icons/icon-96x96.png +0 -0
- package/client/dist/logo-128.png +0 -0
- package/client/dist/logo-256.png +0 -0
- package/client/dist/logo-32.png +0 -0
- package/client/dist/logo-512.png +0 -0
- package/client/dist/logo-64.png +0 -0
- package/commands/upfynai-connect.md +0 -59
- package/commands/upfynai-disconnect.md +0 -31
- package/commands/upfynai-doctor.md +0 -99
- package/commands/upfynai-export.md +0 -49
- package/commands/upfynai-local.md +0 -82
- package/commands/upfynai-status.md +0 -75
- package/commands/upfynai-stop.md +0 -49
- package/commands/upfynai-uninstall.md +0 -58
- package/commands/upfynai.md +0 -69
- package/scripts/build-client.js +0 -17
- package/scripts/fix-node-pty.js +0 -67
- package/scripts/install-commands.js +0 -78
- package/server/agent-loop.js +0 -242
- package/server/auto-compact.js +0 -99
- package/server/browser.js +0 -131
- package/server/claude-sdk.js +0 -797
- package/server/cli-ui.js +0 -798
- package/server/cli.js +0 -751
- package/server/constants/config.js +0 -31
- package/server/cursor-cli.js +0 -270
- package/server/database/auth.db +0 -0
- package/server/database/db.js +0 -1547
- package/server/database/init.sql +0 -70
- package/server/index.js +0 -3813
- package/server/load-env.js +0 -26
- package/server/mcp-server.js +0 -621
- package/server/middleware/auth.js +0 -184
- package/server/middleware/relayHelpers.js +0 -44
- package/server/middleware/sandboxRouter.js +0 -174
- package/server/openai-codex.js +0 -403
- package/server/openrouter.js +0 -137
- package/server/projects.js +0 -1807
- package/server/provider-factory.js +0 -174
- package/server/relay-client.js +0 -390
- package/server/routes/agent.js +0 -1234
- package/server/routes/auth.js +0 -559
- package/server/routes/browser.js +0 -419
- package/server/routes/canvas.js +0 -53
- package/server/routes/cli-auth.js +0 -263
- package/server/routes/codex.js +0 -396
- package/server/routes/commands.js +0 -707
- package/server/routes/composio.js +0 -176
- package/server/routes/cursor.js +0 -770
- package/server/routes/dashboard.js +0 -295
- package/server/routes/git.js +0 -1208
- package/server/routes/keys.js +0 -34
- package/server/routes/mcp-utils.js +0 -48
- package/server/routes/mcp.js +0 -661
- package/server/routes/payments.js +0 -227
- package/server/routes/projects.js +0 -754
- package/server/routes/sessions.js +0 -146
- package/server/routes/settings.js +0 -261
- package/server/routes/taskmaster.js +0 -1928
- package/server/routes/user.js +0 -106
- package/server/routes/vapi-chat.js +0 -624
- package/server/routes/voice.js +0 -235
- package/server/routes/webhooks.js +0 -166
- package/server/routes/workflows.js +0 -312
- package/server/sandbox.js +0 -120
- package/server/services/browser-ai.js +0 -154
- package/server/services/composio.js +0 -204
- package/server/services/sessionRegistry.js +0 -139
- package/server/services/whisperService.js +0 -84
- package/server/services/workflowScheduler.js +0 -211
- package/server/tests/relay-flow.test.js +0 -570
- package/server/tests/sessions.test.js +0 -259
- package/server/utils/commandParser.js +0 -303
- package/server/utils/email.js +0 -66
- package/server/utils/gitConfig.js +0 -24
- package/server/utils/mcp-detector.js +0 -198
- package/server/utils/taskmaster-websocket.js +0 -129
- package/shared/integrationCatalog.d.ts +0 -12
- package/shared/integrationCatalog.js +0 -172
- package/shared/modelConstants.js +0 -96
- /package/{shared → dist}/agents/claude.js +0 -0
- /package/{shared → dist}/agents/codex.js +0 -0
- /package/{shared → dist}/agents/cursor.js +0 -0
- /package/{shared → dist}/agents/detect.js +0 -0
- /package/{shared → dist}/agents/exec.js +0 -0
- /package/{shared → dist}/agents/files.js +0 -0
- /package/{shared → dist}/agents/git.js +0 -0
- /package/{shared → dist}/agents/gitagent.js +0 -0
- /package/{shared → dist}/agents/index.js +0 -0
- /package/{shared → dist}/agents/shell.js +0 -0
- /package/{shared → dist}/agents/utils.js +0 -0
- /package/{client/dist → dist/client}/api-docs.html +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- /package/{client/dist → dist/client}/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- /package/{client/dist → dist/client}/assets/vendor-i18n-DCFGyhQR.js +0 -0
- /package/{client/dist → dist/client}/assets/vendor-xterm-CZq1hqo1.js +0 -0
- /package/{client/dist → dist/client}/assets/vendor-xterm-qxJ8_QYu.css +0 -0
- /package/{client/dist → dist/client}/clear-cache.html +0 -0
- /package/{client/dist → dist/client}/convert-icons.md +0 -0
- /package/{client/dist → dist/client}/favicon.svg +0 -0
- /package/{client/dist → dist/client}/generate-icons.js +0 -0
- /package/{client/dist → dist/client}/icons/claude-ai-icon.svg +0 -0
- /package/{client/dist → dist/client}/icons/codex-white.svg +0 -0
- /package/{client/dist → dist/client}/icons/codex.svg +0 -0
- /package/{client/dist → dist/client}/icons/cursor-white.svg +0 -0
- /package/{client/dist → dist/client}/icons/cursor.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-128x128.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-144x144.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-152x152.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-192x192.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-384x384.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-512x512.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-72x72.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-96x96.svg +0 -0
- /package/{client/dist → dist/client}/icons/icon-template.svg +0 -0
- /package/{client/dist → dist/client}/logo.svg +0 -0
- /package/{client/dist → dist/client}/offline.html +0 -0
- /package/{client/dist → dist/client}/screenshots/cli-selection.png +0 -0
- /package/{client/dist → dist/client}/screenshots/desktop-main.png +0 -0
- /package/{client/dist → dist/client}/screenshots/mobile-chat.png +0 -0
- /package/{client/dist → dist/client}/screenshots/tools-modal.png +0 -0
- /package/{shared → dist}/gitagent/index.js +0 -0
- /package/{shared → dist}/gitagent/parser.js +0 -0
- /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;
|