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,312 @@
1
+ import express from 'express';
2
+ import { workflowDb, webhookDb, credentialsDb } from '../database/db.js';
3
+ import { refreshWorkflowSchedule, stopWorkflowSchedule, executeWorkflow } from '../services/workflowScheduler.js';
4
+ import { INTEGRATION_CATALOG } from '../../shared/integrationCatalog.js';
5
+
6
+ const router = express.Router();
7
+
8
+ // ── BYOK helpers ────────────────────────────────────────────────────
9
+ async function getUserProviderKey(userId, providerType) {
10
+ if (!userId) return null;
11
+ try {
12
+ const creds = await credentialsDb.getCredentials(userId, providerType);
13
+ const active = creds.find(c => c.is_active);
14
+ return active?.credential_value || null;
15
+ } catch { return null; }
16
+ }
17
+
18
+ // ── AI workflow generation prompt ───────────────────────────────────
19
+ function buildWorkflowGenerationPrompt(description, availableWebhooks, connectedIntegrations) {
20
+ const integrationsList = INTEGRATION_CATALOG.map(i => {
21
+ const connected = connectedIntegrations.includes(i.id);
22
+ const actions = i.popularActions.map(a => ` - ${a.slug}: ${a.label} (params: ${a.params.join(', ')})`).join('\n');
23
+ return `${i.name} (${i.id})${connected ? ' [CONNECTED]' : ''}:\n${actions}`;
24
+ }).join('\n\n');
25
+
26
+ const webhooksList = availableWebhooks.length
27
+ ? availableWebhooks.map(w => `- ID ${w.id}: "${w.name}" (${w.method} ${w.url})`).join('\n')
28
+ : 'No webhooks configured.';
29
+
30
+ return `You are a workflow automation builder. Given a natural language description, generate a workflow JSON.
31
+
32
+ AVAILABLE STEP TYPES:
33
+ 1. "ai-prompt" — Run an AI prompt. Config: { prompt: string }
34
+ 2. "webhook" — Call an HTTP endpoint. Config: { webhookId: string, payloadTemplate?: string }
35
+ 3. "delay" — Wait N seconds (max 30). Config: { seconds: number }
36
+ 4. "condition" — Branch on expression. Config: { expression: string }
37
+ 5. "integration" — Use a connected app via Composio. Config: { integrationId: string, toolSlug: string, arguments: { param: value } }
38
+
39
+ AVAILABLE INTEGRATIONS:
40
+ ${integrationsList}
41
+
42
+ AVAILABLE WEBHOOKS:
43
+ ${webhooksList}
44
+
45
+ RULES:
46
+ - Each step needs: id (unique string), type, label (human-readable), config, order (0-indexed)
47
+ - Step IDs should be like "step_1", "step_2", etc.
48
+ - For integration steps, only use integrations marked [CONNECTED] or suggest connecting them
49
+ - Use {{prev.field}} syntax in config values to reference previous step output
50
+ - If the user mentions a schedule, include schedule (cron) and schedule_enabled: true
51
+ - Keep workflows focused and practical
52
+
53
+ Respond with ONLY valid JSON in this exact format (no markdown, no explanation):
54
+ {
55
+ "name": "Workflow Name",
56
+ "description": "What this workflow does",
57
+ "steps": [...],
58
+ "schedule": null,
59
+ "schedule_enabled": false,
60
+ "schedule_timezone": "UTC"
61
+ }
62
+
63
+ USER REQUEST: ${description}`;
64
+ }
65
+
66
+ // ── Routes ──────────────────────────────────────────────────────────
67
+
68
+ // GET /api/workflows — list all workflows for the user
69
+ router.get('/', async (req, res) => {
70
+ try {
71
+ const workflows = await workflowDb.getAll(req.user.id);
72
+ const parsed = workflows.map(w => ({
73
+ ...w,
74
+ steps: typeof w.steps === 'string' ? JSON.parse(w.steps) : w.steps
75
+ }));
76
+ res.json({ workflows: parsed });
77
+ } catch (error) {
78
+ res.status(500).json({ error: 'Failed to fetch workflows' });
79
+ }
80
+ });
81
+
82
+ // POST /api/workflows — create a workflow
83
+ router.post('/', async (req, res) => {
84
+ try {
85
+ const { name, description, steps, schedule, schedule_enabled, schedule_timezone } = req.body;
86
+ if (!name || !name.trim()) return res.status(400).json({ error: 'Name is required' });
87
+ if (!Array.isArray(steps)) return res.status(400).json({ error: 'Steps must be an array' });
88
+
89
+ const workflow = await workflowDb.create(req.user.id, {
90
+ name: name.trim(),
91
+ description: description?.trim() || null,
92
+ steps,
93
+ schedule: schedule || null,
94
+ schedule_enabled: !!schedule_enabled,
95
+ schedule_timezone: schedule_timezone || 'UTC'
96
+ });
97
+
98
+ // Sync cron scheduler
99
+ if (workflow.id) refreshWorkflowSchedule(workflow.id, req.user.id);
100
+
101
+ res.json({ success: true, workflow });
102
+ } catch (error) {
103
+ res.status(500).json({ error: 'Failed to create workflow' });
104
+ }
105
+ });
106
+
107
+ // POST /api/workflows/generate — AI-powered workflow generation from natural language
108
+ router.post('/generate', async (req, res) => {
109
+ try {
110
+ const { description } = req.body;
111
+ if (!description || !description.trim()) {
112
+ return res.status(400).json({ error: 'Description is required' });
113
+ }
114
+
115
+ // Get user's API key (try Anthropic first, then OpenRouter)
116
+ let apiKey = await getUserProviderKey(req.user.id, 'anthropic_key');
117
+ let provider = 'anthropic';
118
+
119
+ if (!apiKey) {
120
+ apiKey = await getUserProviderKey(req.user.id, 'openrouter_key');
121
+ provider = 'openrouter';
122
+ }
123
+
124
+ // Fall back to server key
125
+ if (!apiKey) {
126
+ apiKey = process.env.ANTHROPIC_API_KEY;
127
+ provider = 'anthropic';
128
+ }
129
+
130
+ if (!apiKey) {
131
+ return res.status(400).json({ error: 'No AI provider key available. Add an API key in Settings > AI Providers.' });
132
+ }
133
+
134
+ // Get available webhooks and connected integrations for context
135
+ const webhooks = await webhookDb.getAll(req.user.id);
136
+ const connectedIntegrations = []; // Will be populated if Composio is available
137
+
138
+ const prompt = buildWorkflowGenerationPrompt(description.trim(), webhooks, connectedIntegrations);
139
+
140
+ let generatedJson;
141
+
142
+ if (provider === 'anthropic') {
143
+ const response = await fetch('https://api.anthropic.com/v1/messages', {
144
+ method: 'POST',
145
+ headers: {
146
+ 'x-api-key': apiKey,
147
+ 'anthropic-version': '2023-06-01',
148
+ 'content-type': 'application/json',
149
+ },
150
+ body: JSON.stringify({
151
+ model: 'claude-sonnet-4-20250514',
152
+ max_tokens: 2048,
153
+ messages: [{ role: 'user', content: prompt }],
154
+ }),
155
+ });
156
+
157
+ if (!response.ok) {
158
+ return res.status(502).json({ error: 'AI provider returned an error' });
159
+ }
160
+
161
+ const data = await response.json();
162
+ const text = data.content?.[0]?.text || '';
163
+ generatedJson = text.trim();
164
+ } else {
165
+ // OpenRouter
166
+ const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
167
+ method: 'POST',
168
+ headers: {
169
+ 'Authorization': `Bearer ${apiKey}`,
170
+ 'Content-Type': 'application/json',
171
+ 'HTTP-Referer': 'https://cli.upfyn.com',
172
+ 'X-Title': 'Upfyn-Code',
173
+ },
174
+ body: JSON.stringify({
175
+ model: 'anthropic/claude-sonnet-4',
176
+ messages: [{ role: 'user', content: prompt }],
177
+ max_tokens: 2048,
178
+ }),
179
+ });
180
+
181
+ if (!response.ok) {
182
+ return res.status(502).json({ error: 'AI provider returned an error' });
183
+ }
184
+
185
+ const data = await response.json();
186
+ generatedJson = data.choices?.[0]?.message?.content?.trim() || '';
187
+ }
188
+
189
+ // Parse the AI response — strip markdown fences if present
190
+ let cleaned = generatedJson;
191
+ if (cleaned.startsWith('```')) {
192
+ cleaned = cleaned.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '');
193
+ }
194
+
195
+ let workflow;
196
+ try {
197
+ workflow = JSON.parse(cleaned);
198
+ } catch {
199
+ return res.status(422).json({ error: 'AI generated invalid workflow format. Try rephrasing your description.' });
200
+ }
201
+
202
+ // Validate required fields
203
+ if (!workflow.name || !Array.isArray(workflow.steps)) {
204
+ return res.status(422).json({ error: 'AI generated incomplete workflow. Try being more specific.' });
205
+ }
206
+
207
+ // Ensure step IDs are unique
208
+ workflow.steps = workflow.steps.map((step, i) => ({
209
+ ...step,
210
+ id: step.id || `step_${Date.now()}_${i}`,
211
+ order: i,
212
+ }));
213
+
214
+ // Save to Turso
215
+ const saved = await workflowDb.create(req.user.id, {
216
+ name: workflow.name.trim(),
217
+ description: workflow.description?.trim() || description.trim(),
218
+ steps: workflow.steps,
219
+ schedule: workflow.schedule || null,
220
+ schedule_enabled: !!workflow.schedule_enabled,
221
+ schedule_timezone: workflow.schedule_timezone || 'UTC',
222
+ });
223
+
224
+ if (saved.id && workflow.schedule_enabled) {
225
+ refreshWorkflowSchedule(saved.id, req.user.id);
226
+ }
227
+
228
+ res.json({
229
+ success: true,
230
+ workflow: {
231
+ ...saved,
232
+ steps: workflow.steps,
233
+ },
234
+ });
235
+ } catch (error) {
236
+ res.status(500).json({ error: 'Failed to generate workflow' });
237
+ }
238
+ });
239
+
240
+ // PUT /api/workflows/:id — update a workflow
241
+ router.put('/:id', async (req, res) => {
242
+ try {
243
+ const { name, description, steps, schedule, schedule_enabled, schedule_timezone } = req.body;
244
+ if (!name || !name.trim()) return res.status(400).json({ error: 'Name is required' });
245
+
246
+ const wfId = Number(req.params.id);
247
+ const updated = await workflowDb.update(wfId, req.user.id, {
248
+ name: name.trim(),
249
+ description: description?.trim() || null,
250
+ steps: steps || [],
251
+ schedule: schedule || null,
252
+ schedule_enabled: !!schedule_enabled,
253
+ schedule_timezone: schedule_timezone || 'UTC'
254
+ });
255
+
256
+ if (!updated) return res.status(404).json({ error: 'Workflow not found' });
257
+
258
+ // Sync cron scheduler
259
+ refreshWorkflowSchedule(wfId, req.user.id);
260
+
261
+ res.json({ success: true });
262
+ } catch (error) {
263
+ res.status(500).json({ error: 'Failed to update workflow' });
264
+ }
265
+ });
266
+
267
+ // DELETE /api/workflows/:id — delete a workflow
268
+ router.delete('/:id', async (req, res) => {
269
+ try {
270
+ const wfId = Number(req.params.id);
271
+ const deleted = await workflowDb.delete(wfId, req.user.id);
272
+ if (!deleted) return res.status(404).json({ error: 'Workflow not found' });
273
+
274
+ stopWorkflowSchedule(wfId);
275
+
276
+ res.json({ success: true });
277
+ } catch (error) {
278
+ res.status(500).json({ error: 'Failed to delete workflow' });
279
+ }
280
+ });
281
+
282
+ // POST /api/workflows/:id/run — execute a workflow (manual trigger)
283
+ router.post('/:id/run', async (req, res) => {
284
+ try {
285
+ const workflow = await workflowDb.getById(Number(req.params.id), req.user.id);
286
+ if (!workflow) return res.status(404).json({ error: 'Workflow not found' });
287
+
288
+ const steps = typeof workflow.steps === 'string' ? JSON.parse(workflow.steps) : workflow.steps;
289
+ if (!steps.length) return res.status(400).json({ error: 'Workflow has no steps' });
290
+
291
+ const result = await executeWorkflow({ ...workflow, steps });
292
+ res.json(result);
293
+ } catch (error) {
294
+ res.status(500).json({ error: 'Failed to run workflow' });
295
+ }
296
+ });
297
+
298
+ // GET /api/workflows/:id/runs — list execution history for a workflow
299
+ router.get('/:id/runs', async (req, res) => {
300
+ try {
301
+ const runs = await workflowDb.getRuns(Number(req.params.id), req.user.id);
302
+ const parsed = runs.map(r => ({
303
+ ...r,
304
+ result: typeof r.result === 'string' ? (() => { try { return JSON.parse(r.result); } catch { return r.result; } })() : r.result
305
+ }));
306
+ res.json({ runs: parsed });
307
+ } catch (error) {
308
+ res.status(500).json({ error: 'Failed to fetch workflow runs' });
309
+ }
310
+ });
311
+
312
+ export default router;
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Sandbox Client — connects the backend to the separate sandbox-service on Railway.
3
+ * All sandbox operations are proxied to the sandbox service via HTTP.
4
+ */
5
+
6
+ const SANDBOX_SERVICE_URL = process.env.SANDBOX_SERVICE_URL || 'http://localhost:4300';
7
+ const SANDBOX_SERVICE_SECRET = process.env.SANDBOX_SERVICE_SECRET || 'dev-sandbox-secret';
8
+
9
+ async function sandboxFetch(path, userId, body = null) {
10
+ const opts = {
11
+ method: body ? 'POST' : 'GET',
12
+ headers: {
13
+ 'Content-Type': 'application/json',
14
+ 'x-sandbox-secret': SANDBOX_SERVICE_SECRET,
15
+ 'x-user-id': String(userId),
16
+ },
17
+ };
18
+ if (body) opts.body = JSON.stringify(body);
19
+
20
+ const res = await fetch(`${SANDBOX_SERVICE_URL}${path}`, opts);
21
+ const data = await res.json();
22
+ if (!res.ok) throw new Error(data.error || `Sandbox service error: ${res.status}`);
23
+ return data;
24
+ }
25
+
26
+ const sandboxClient = {
27
+
28
+ /**
29
+ * Check if the sandbox service is reachable.
30
+ */
31
+ async isAvailable() {
32
+ try {
33
+ const res = await fetch(`${SANDBOX_SERVICE_URL}/health`, { signal: AbortSignal.timeout(3000) });
34
+ return res.ok;
35
+ } catch {
36
+ return false;
37
+ }
38
+ },
39
+
40
+ /**
41
+ * Initialize a user's sandbox (creates if doesn't exist).
42
+ */
43
+ async initSandbox(userId) {
44
+ return sandboxFetch('/api/sandbox/init', userId, {});
45
+ },
46
+
47
+ /**
48
+ * Get sandbox status.
49
+ */
50
+ async getStatus(userId) {
51
+ return sandboxFetch('/api/sandbox/status', userId);
52
+ },
53
+
54
+ /**
55
+ * Destroy a user's sandbox.
56
+ */
57
+ async destroySandbox(userId) {
58
+ const res = await fetch(`${SANDBOX_SERVICE_URL}/api/sandbox`, {
59
+ method: 'DELETE',
60
+ headers: {
61
+ 'Content-Type': 'application/json',
62
+ 'x-sandbox-secret': SANDBOX_SERVICE_SECRET,
63
+ 'x-user-id': String(userId),
64
+ },
65
+ });
66
+ const data = await res.json();
67
+ if (!res.ok) throw new Error(data.error || 'Failed to destroy sandbox');
68
+ return data;
69
+ },
70
+
71
+ /**
72
+ * Execute a command in the user's sandbox.
73
+ */
74
+ async exec(userId, command, opts = {}) {
75
+ return sandboxFetch('/api/exec', userId, {
76
+ command,
77
+ cwd: opts.cwd,
78
+ timeout: opts.timeout,
79
+ userKeys: opts.userKeys,
80
+ });
81
+ },
82
+
83
+ /**
84
+ * Read a file from the user's sandbox.
85
+ */
86
+ async readFile(userId, filePath) {
87
+ return sandboxFetch('/api/file/read', userId, { filePath });
88
+ },
89
+
90
+ /**
91
+ * Write a file to the user's sandbox.
92
+ */
93
+ async writeFile(userId, filePath, content) {
94
+ return sandboxFetch('/api/file/write', userId, { filePath, content });
95
+ },
96
+
97
+ /**
98
+ * Get file tree from the user's sandbox.
99
+ */
100
+ async getFileTree(userId, dirPath, depth = 3) {
101
+ return sandboxFetch('/api/file/tree', userId, { dirPath, depth });
102
+ },
103
+
104
+ /**
105
+ * Run a git command in the user's sandbox.
106
+ */
107
+ async gitOperation(userId, gitCommand, cwd) {
108
+ return sandboxFetch('/api/git', userId, { gitCommand, cwd });
109
+ },
110
+
111
+ /**
112
+ * Get the WebSocket URL for an interactive shell session.
113
+ */
114
+ getShellWsUrl(userId, sessionId) {
115
+ const wsBase = SANDBOX_SERVICE_URL.replace(/^http/, 'ws');
116
+ return `${wsBase}/shell?secret=${encodeURIComponent(SANDBOX_SERVICE_SECRET)}&userId=${userId}&sessionId=${sessionId || 'default'}`;
117
+ },
118
+ };
119
+
120
+ export { sandboxClient, SANDBOX_SERVICE_URL };
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Composio Service — wraps the Composio SDK for OAuth management + tool execution.
3
+ * Single API key for the app, user isolation via entity mapping.
4
+ */
5
+
6
+
7
+ let Composio = null;
8
+ let composioClient = null;
9
+ let sdkReady = null; // resolved promise once SDK is loaded
10
+
11
+ // Eagerly attempt to load the SDK — store the promise so getClient can await it
12
+ sdkReady = (async () => {
13
+ try {
14
+ const mod = await import('composio-core');
15
+ Composio = mod.Composio || mod.default;
16
+ } catch {
17
+ // SDK not installed — composio features will be unavailable
18
+ }
19
+ })();
20
+
21
+ /**
22
+ * Lazy-init the Composio client singleton.
23
+ * Returns null if COMPOSIO_API_KEY is not set.
24
+ */
25
+ async function getClient() {
26
+ if (composioClient) return composioClient;
27
+ if (!process.env.COMPOSIO_API_KEY) return null;
28
+
29
+ // Wait for SDK to finish loading
30
+ await sdkReady;
31
+
32
+ try {
33
+ if (!Composio) return null;
34
+ composioClient = new Composio({ apiKey: process.env.COMPOSIO_API_KEY });
35
+ return composioClient;
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Map internal user ID to Composio entity ID.
43
+ */
44
+ function composioUserId(internalId) {
45
+ return `upfyn_user_${internalId}`;
46
+ }
47
+
48
+ /**
49
+ * Get or create an entity for the given user.
50
+ */
51
+ async function getEntity(userId) {
52
+ const client = await getClient();
53
+ if (!client) throw new Error('Composio not configured');
54
+ return client.getEntity(composioUserId(userId));
55
+ }
56
+
57
+ /**
58
+ * Initiate an OAuth connection for a user.
59
+ * OAuth credentials are managed on the Composio dashboard — not injected here.
60
+ *
61
+ * @param {number} userId - Internal user ID
62
+ * @param {string} appName - Composio app name (e.g., 'GMAIL', 'SLACK')
63
+ * @param {string|null} authConfigId - Optional specific auth config
64
+ * @returns {{ redirectUrl: string, connectedAccountId: string }}
65
+ */
66
+ async function initiateConnection(userId, appName, authConfigId = null) {
67
+ const entity = await getEntity(userId);
68
+
69
+ const params = { appName };
70
+ if (authConfigId) params.authConfigId = authConfigId;
71
+
72
+ const connectionRequest = await entity.initiateConnection(params);
73
+ return {
74
+ redirectUrl: connectionRequest.redirectUrl,
75
+ connectedAccountId: connectionRequest.connectedAccountId,
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Wait/poll for a connection to complete.
81
+ * @param {string} connectedAccountId - The connection ID from initiateConnection
82
+ * @returns {{ status: string, appName: string }}
83
+ */
84
+ async function waitForConnection(connectedAccountId) {
85
+ const client = await getClient();
86
+ if (!client) throw new Error('Composio not configured');
87
+
88
+ const connection = await client.connectedAccounts.get({ connectedAccountId });
89
+ return {
90
+ status: connection.status,
91
+ appName: connection.appName,
92
+ id: connection.id,
93
+ };
94
+ }
95
+
96
+ /**
97
+ * List connected accounts for a user.
98
+ * @param {number} userId
99
+ * @returns {Array<{ id, appName, status, createdAt }>}
100
+ */
101
+ async function listConnectedAccounts(userId) {
102
+ const client = await getClient();
103
+ if (!client) throw new Error('Composio not configured');
104
+
105
+ const entityId = composioUserId(userId);
106
+ try {
107
+ const accounts = await client.connectedAccounts.list({ entityId });
108
+ return (accounts.items || accounts || []).map(a => ({
109
+ id: a.id,
110
+ appName: a.appName,
111
+ status: a.status,
112
+ createdAt: a.createdAt,
113
+ }));
114
+ } catch {
115
+ return [];
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Get available tools for given toolkits.
121
+ * @param {string[]} apps - e.g., ['GMAIL', 'SLACK']
122
+ * @returns {Array<{ name, description, parameters }>}
123
+ */
124
+ async function getTools(apps = []) {
125
+ const client = await getClient();
126
+ if (!client) throw new Error('Composio not configured');
127
+
128
+ const tools = await client.actions.list({ apps });
129
+ return (tools.items || tools || []).map(t => ({
130
+ name: t.name,
131
+ displayName: t.displayName || t.name,
132
+ description: t.description,
133
+ parameters: t.parameters,
134
+ appName: t.appName,
135
+ }));
136
+ }
137
+
138
+ /**
139
+ * Get schema for a specific tool.
140
+ * @param {string} actionName - e.g., 'GMAIL_SEND_EMAIL'
141
+ * @returns {{ name, description, parameters }}
142
+ */
143
+ async function getToolSchema(actionName) {
144
+ const client = await getClient();
145
+ if (!client) throw new Error('Composio not configured');
146
+
147
+ const action = await client.actions.get({ actionName });
148
+ return {
149
+ name: action.name,
150
+ displayName: action.displayName || action.name,
151
+ description: action.description,
152
+ parameters: action.parameters,
153
+ appName: action.appName,
154
+ };
155
+ }
156
+
157
+ /**
158
+ * Execute a Composio tool action.
159
+ * @param {number} userId - Internal user ID
160
+ * @param {string} actionName - e.g., 'GMAIL_SEND_EMAIL'
161
+ * @param {object} params - Action parameters
162
+ * @returns {{ success, data, error }}
163
+ */
164
+ async function executeTool(userId, actionName, params = {}) {
165
+ const entity = await getEntity(userId);
166
+
167
+ try {
168
+ const result = await entity.execute(actionName, params);
169
+ return { success: true, data: result };
170
+ } catch (err) {
171
+ return { success: false, error: err.message };
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Disconnect an account.
177
+ * @param {string} connectedAccountId
178
+ */
179
+ async function disconnectAccount(connectedAccountId) {
180
+ const client = await getClient();
181
+ if (!client) throw new Error('Composio not configured');
182
+
183
+ await client.connectedAccounts.delete({ connectedAccountId });
184
+ }
185
+
186
+ /**
187
+ * Check if Composio is available and configured.
188
+ */
189
+ async function isAvailable() {
190
+ await sdkReady;
191
+ return !!(Composio && process.env.COMPOSIO_API_KEY);
192
+ }
193
+
194
+ export {
195
+ isAvailable,
196
+ composioUserId,
197
+ initiateConnection,
198
+ waitForConnection,
199
+ listConnectedAccounts,
200
+ getTools,
201
+ getToolSchema,
202
+ executeTool,
203
+ disconnectAccount,
204
+ };