upfynai-code 3.0.4 → 3.1.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 (246) hide show
  1. package/README.md +66 -91
  2. package/bin/cli.js +191 -0
  3. package/{client/dist/assets/AppContent-CwrTP6TW.js → dist/client/assets/AppContent-BofJquUs.js} +4 -4
  4. package/{client/dist/assets/BrowserPanel-0TLEl-IC.js → dist/client/assets/BrowserPanel-CSvD4jOX.js} +2 -2
  5. package/dist/client/assets/CanvasFullScreen-onRfarpc.js +1 -0
  6. package/dist/client/assets/CanvasWorkspace-DvGKdL-k.js +259 -0
  7. package/dist/client/assets/DashboardPanel-DqAHbXDO.js +1 -0
  8. package/dist/client/assets/FileTree-BE0h-9M9.js +1 -0
  9. package/{client/dist/assets/GitPanel-C_xFM-N2.js → dist/client/assets/GitPanel-DdeJ0bp5.js} +2 -2
  10. package/{client/dist/assets/LoginModal-CImJHRjX.js → dist/client/assets/LoginModal-BP0pCTrH.js} +3 -3
  11. package/dist/client/assets/MermaidBlock-D0rfEhrT.js +2 -0
  12. package/dist/client/assets/Onboarding-B2zQy-_6.js +1 -0
  13. package/dist/client/assets/SetupForm-Be7-WBe-.js +1 -0
  14. package/dist/client/assets/WorkflowsPanel-CusLbVJ6.js +1 -0
  15. package/{client/dist/assets/index-HaY-3pK1.js → dist/client/assets/index-BQy15irW.js} +24 -24
  16. package/dist/client/assets/index-CS0fDqEC.js +1 -0
  17. package/dist/client/assets/index-DYLSCCCp.css +1 -0
  18. package/dist/client/assets/vendor-canvas-QWTduIvM.js +23 -0
  19. package/{client/dist/assets/vendor-icons-GyYE35HP.js → dist/client/assets/vendor-icons-kix3Gb31.js} +1 -1
  20. package/{client/dist/assets/vendor-mermaid-DucWyDEe.js → dist/client/assets/vendor-mermaid-CS3J4_Bz.js} +329 -326
  21. package/dist/client/favicon.png +0 -0
  22. package/dist/client/favicon.svg +15 -0
  23. package/{client/dist → dist/client}/index.html +3 -3
  24. package/{client/dist → dist/client}/manifest.json +12 -12
  25. package/package.json +55 -104
  26. package/scripts/postinstall.js +9 -0
  27. package/scripts/prepublish.js +77 -0
  28. package/src/animation.js +228 -0
  29. package/src/auth.js +142 -0
  30. package/src/config.js +40 -0
  31. package/src/connect.js +416 -0
  32. package/src/launch.js +81 -0
  33. package/src/mcp.js +57 -0
  34. package/src/permissions.js +140 -0
  35. package/src/persistent-shell.js +261 -0
  36. package/src/server.js +54 -0
  37. package/client/dist/assets/CanvasFullScreen-D1GWQsGL.js +0 -1
  38. package/client/dist/assets/CanvasWorkspace-D7ORj358.js +0 -163
  39. package/client/dist/assets/DashboardPanel-BV7ybUDe.js +0 -1
  40. package/client/dist/assets/FileTree-5qfhBqdE.js +0 -1
  41. package/client/dist/assets/MermaidBlock-BFM21cwe.js +0 -2
  42. package/client/dist/assets/Onboarding-B3cteLu2.js +0 -1
  43. package/client/dist/assets/SetupForm-P6dsYgHO.js +0 -1
  44. package/client/dist/assets/WorkflowsPanel-CBoN80kc.js +0 -1
  45. package/client/dist/assets/index-46kkVu2i.css +0 -1
  46. package/client/dist/assets/vendor-canvas-DvHJ_Pn2.js +0 -49
  47. package/client/dist/favicon.png +0 -0
  48. package/client/dist/favicon.svg +0 -5
  49. package/commands/upfynai-connect.md +0 -59
  50. package/commands/upfynai-disconnect.md +0 -31
  51. package/commands/upfynai-doctor.md +0 -99
  52. package/commands/upfynai-export.md +0 -49
  53. package/commands/upfynai-local.md +0 -82
  54. package/commands/upfynai-status.md +0 -75
  55. package/commands/upfynai-stop.md +0 -49
  56. package/commands/upfynai-uninstall.md +0 -58
  57. package/commands/upfynai.md +0 -69
  58. package/scripts/build-client.js +0 -17
  59. package/scripts/fix-node-pty.js +0 -67
  60. package/scripts/install-commands.js +0 -78
  61. package/server/agent-loop.js +0 -242
  62. package/server/auto-compact.js +0 -99
  63. package/server/browser.js +0 -131
  64. package/server/claude-sdk.js +0 -797
  65. package/server/cli-ui.js +0 -798
  66. package/server/cli.js +0 -751
  67. package/server/constants/config.js +0 -31
  68. package/server/cursor-cli.js +0 -270
  69. package/server/database/auth.db +0 -0
  70. package/server/database/db.js +0 -1547
  71. package/server/database/init.sql +0 -70
  72. package/server/index.js +0 -3813
  73. package/server/load-env.js +0 -26
  74. package/server/mcp-server.js +0 -621
  75. package/server/middleware/auth.js +0 -184
  76. package/server/middleware/relayHelpers.js +0 -44
  77. package/server/middleware/sandboxRouter.js +0 -174
  78. package/server/openai-codex.js +0 -403
  79. package/server/openrouter.js +0 -137
  80. package/server/projects.js +0 -1807
  81. package/server/provider-factory.js +0 -174
  82. package/server/relay-client.js +0 -390
  83. package/server/routes/agent.js +0 -1234
  84. package/server/routes/auth.js +0 -559
  85. package/server/routes/browser.js +0 -419
  86. package/server/routes/canvas.js +0 -53
  87. package/server/routes/cli-auth.js +0 -263
  88. package/server/routes/codex.js +0 -396
  89. package/server/routes/commands.js +0 -707
  90. package/server/routes/composio.js +0 -176
  91. package/server/routes/cursor.js +0 -770
  92. package/server/routes/dashboard.js +0 -295
  93. package/server/routes/git.js +0 -1208
  94. package/server/routes/keys.js +0 -34
  95. package/server/routes/mcp-utils.js +0 -48
  96. package/server/routes/mcp.js +0 -661
  97. package/server/routes/payments.js +0 -227
  98. package/server/routes/projects.js +0 -754
  99. package/server/routes/sessions.js +0 -146
  100. package/server/routes/settings.js +0 -261
  101. package/server/routes/taskmaster.js +0 -1928
  102. package/server/routes/user.js +0 -106
  103. package/server/routes/vapi-chat.js +0 -624
  104. package/server/routes/voice.js +0 -235
  105. package/server/routes/webhooks.js +0 -166
  106. package/server/routes/workflows.js +0 -312
  107. package/server/sandbox.js +0 -120
  108. package/server/services/browser-ai.js +0 -154
  109. package/server/services/composio.js +0 -204
  110. package/server/services/sessionRegistry.js +0 -139
  111. package/server/services/whisperService.js +0 -84
  112. package/server/services/workflowScheduler.js +0 -211
  113. package/server/tests/relay-flow.test.js +0 -570
  114. package/server/tests/sessions.test.js +0 -259
  115. package/server/utils/commandParser.js +0 -303
  116. package/server/utils/email.js +0 -66
  117. package/server/utils/gitConfig.js +0 -24
  118. package/server/utils/mcp-detector.js +0 -198
  119. package/server/utils/taskmaster-websocket.js +0 -129
  120. package/shared/integrationCatalog.d.ts +0 -12
  121. package/shared/integrationCatalog.js +0 -172
  122. package/shared/modelConstants.js +0 -96
  123. /package/{shared → dist}/agents/claude.js +0 -0
  124. /package/{shared → dist}/agents/codex.js +0 -0
  125. /package/{shared → dist}/agents/cursor.js +0 -0
  126. /package/{shared → dist}/agents/detect.js +0 -0
  127. /package/{shared → dist}/agents/exec.js +0 -0
  128. /package/{shared → dist}/agents/files.js +0 -0
  129. /package/{shared → dist}/agents/git.js +0 -0
  130. /package/{shared → dist}/agents/gitagent.js +0 -0
  131. /package/{shared → dist}/agents/index.js +0 -0
  132. /package/{shared → dist}/agents/shell.js +0 -0
  133. /package/{shared → dist}/agents/utils.js +0 -0
  134. /package/{client/dist → dist/client}/api-docs.html +0 -0
  135. /package/{client/dist → dist/client}/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  136. /package/{client/dist → dist/client}/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  137. /package/{client/dist → dist/client}/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  138. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  139. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  140. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  141. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  142. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  143. /package/{client/dist → dist/client}/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  144. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  145. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  146. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  147. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  148. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  149. /package/{client/dist → dist/client}/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  150. /package/{client/dist → dist/client}/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  151. /package/{client/dist → dist/client}/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  152. /package/{client/dist → dist/client}/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  153. /package/{client/dist → dist/client}/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  154. /package/{client/dist → dist/client}/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  155. /package/{client/dist → dist/client}/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  156. /package/{client/dist → dist/client}/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  157. /package/{client/dist → dist/client}/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  158. /package/{client/dist → dist/client}/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  159. /package/{client/dist → dist/client}/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  160. /package/{client/dist → dist/client}/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  161. /package/{client/dist → dist/client}/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  162. /package/{client/dist → dist/client}/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  163. /package/{client/dist → dist/client}/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  164. /package/{client/dist → dist/client}/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  165. /package/{client/dist → dist/client}/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  166. /package/{client/dist → dist/client}/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  167. /package/{client/dist → dist/client}/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  168. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  169. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  170. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  171. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  172. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  173. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  174. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  175. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  176. /package/{client/dist → dist/client}/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  177. /package/{client/dist → dist/client}/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  178. /package/{client/dist → dist/client}/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  179. /package/{client/dist → dist/client}/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  180. /package/{client/dist → dist/client}/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  181. /package/{client/dist → dist/client}/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  182. /package/{client/dist → dist/client}/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  183. /package/{client/dist → dist/client}/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  184. /package/{client/dist → dist/client}/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  185. /package/{client/dist → dist/client}/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  186. /package/{client/dist → dist/client}/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  187. /package/{client/dist → dist/client}/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  188. /package/{client/dist → dist/client}/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  189. /package/{client/dist → dist/client}/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  190. /package/{client/dist → dist/client}/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  191. /package/{client/dist → dist/client}/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  192. /package/{client/dist → dist/client}/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  193. /package/{client/dist → dist/client}/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  194. /package/{client/dist → dist/client}/assets/MarkdownPreview-CESjI261.js +0 -0
  195. /package/{client/dist → dist/client}/assets/PreviewPanel-CqCa92Tf.js +0 -0
  196. /package/{client/dist → dist/client}/assets/pdf-CE_K4jFx.js +0 -0
  197. /package/{client/dist → dist/client}/assets/vendor-canvas-BZV40eAE.css +0 -0
  198. /package/{client/dist → dist/client}/assets/vendor-codemirror-D2ALgpaX.js +0 -0
  199. /package/{client/dist → dist/client}/assets/vendor-diff-DNQpbhrT.js +0 -0
  200. /package/{client/dist → dist/client}/assets/vendor-i18n-DCFGyhQR.js +0 -0
  201. /package/{client/dist → dist/client}/assets/vendor-markdown-CimbIo6Y.js +0 -0
  202. /package/{client/dist → dist/client}/assets/vendor-react-96lCPsRK.js +0 -0
  203. /package/{client/dist → dist/client}/assets/vendor-syntax-LS_Nt30I.js +0 -0
  204. /package/{client/dist → dist/client}/assets/vendor-xterm-CZq1hqo1.js +0 -0
  205. /package/{client/dist → dist/client}/assets/vendor-xterm-qxJ8_QYu.css +0 -0
  206. /package/{client/dist → dist/client}/clear-cache.html +0 -0
  207. /package/{client/dist → dist/client}/convert-icons.md +0 -0
  208. /package/{client/dist → dist/client}/generate-icons.js +0 -0
  209. /package/{client/dist → dist/client}/icons/claude-ai-icon.svg +0 -0
  210. /package/{client/dist → dist/client}/icons/codex-white.svg +0 -0
  211. /package/{client/dist → dist/client}/icons/codex.svg +0 -0
  212. /package/{client/dist → dist/client}/icons/cursor-white.svg +0 -0
  213. /package/{client/dist → dist/client}/icons/cursor.svg +0 -0
  214. /package/{client/dist → dist/client}/icons/icon-128x128.png +0 -0
  215. /package/{client/dist → dist/client}/icons/icon-128x128.svg +0 -0
  216. /package/{client/dist → dist/client}/icons/icon-144x144.png +0 -0
  217. /package/{client/dist → dist/client}/icons/icon-144x144.svg +0 -0
  218. /package/{client/dist → dist/client}/icons/icon-152x152.png +0 -0
  219. /package/{client/dist → dist/client}/icons/icon-152x152.svg +0 -0
  220. /package/{client/dist → dist/client}/icons/icon-192x192.png +0 -0
  221. /package/{client/dist → dist/client}/icons/icon-192x192.svg +0 -0
  222. /package/{client/dist → dist/client}/icons/icon-384x384.png +0 -0
  223. /package/{client/dist → dist/client}/icons/icon-384x384.svg +0 -0
  224. /package/{client/dist → dist/client}/icons/icon-512x512.png +0 -0
  225. /package/{client/dist → dist/client}/icons/icon-512x512.svg +0 -0
  226. /package/{client/dist → dist/client}/icons/icon-72x72.png +0 -0
  227. /package/{client/dist → dist/client}/icons/icon-72x72.svg +0 -0
  228. /package/{client/dist → dist/client}/icons/icon-96x96.png +0 -0
  229. /package/{client/dist → dist/client}/icons/icon-96x96.svg +0 -0
  230. /package/{client/dist → dist/client}/icons/icon-template.svg +0 -0
  231. /package/{client/dist → dist/client}/logo-128.png +0 -0
  232. /package/{client/dist → dist/client}/logo-256.png +0 -0
  233. /package/{client/dist → dist/client}/logo-32.png +0 -0
  234. /package/{client/dist → dist/client}/logo-512.png +0 -0
  235. /package/{client/dist → dist/client}/logo-64.png +0 -0
  236. /package/{client/dist → dist/client}/logo.svg +0 -0
  237. /package/{client/dist → dist/client}/mcp-docs.html +0 -0
  238. /package/{client/dist → dist/client}/offline.html +0 -0
  239. /package/{client/dist → dist/client}/screenshots/cli-selection.png +0 -0
  240. /package/{client/dist → dist/client}/screenshots/desktop-main.png +0 -0
  241. /package/{client/dist → dist/client}/screenshots/mobile-chat.png +0 -0
  242. /package/{client/dist → dist/client}/screenshots/tools-modal.png +0 -0
  243. /package/{client/dist → dist/client}/sw.js +0 -0
  244. /package/{shared → dist}/gitagent/index.js +0 -0
  245. /package/{shared → dist}/gitagent/parser.js +0 -0
  246. /package/{shared → dist}/gitagent/prompt-builder.js +0 -0
package/src/mcp.js ADDED
@@ -0,0 +1,57 @@
1
+ import chalk from 'chalk';
2
+ import { readConfig } from './config.js';
3
+ import { getToken, validateToken } from './auth.js';
4
+
5
+ /**
6
+ * Print MCP configuration for Claude / Cursor / other AI tools.
7
+ * Optionally accepts --server and --key overrides.
8
+ */
9
+ export async function mcp(options = {}) {
10
+ const config = readConfig();
11
+ const serverUrl = options.server || config.serverUrl;
12
+ let relayKey = options.key;
13
+
14
+ // If no key provided, fetch one using auth token
15
+ if (!relayKey) {
16
+ const token = getToken();
17
+ if (!token) {
18
+ console.log(chalk.yellow('\n No account found. Run `uc login` first.\n'));
19
+ process.exit(1);
20
+ }
21
+
22
+ const user = await validateToken();
23
+ if (!user) {
24
+ console.log(chalk.yellow('\n Session expired. Run `uc login` to re-authenticate.\n'));
25
+ process.exit(1);
26
+ }
27
+
28
+ try {
29
+ const res = await fetch(`${serverUrl}/api/auth/connect-token`, {
30
+ headers: { Authorization: `Bearer ${token}` },
31
+ });
32
+ if (!res.ok) throw new Error('Failed to get connect token');
33
+ const data = await res.json();
34
+ relayKey = data.token;
35
+ } catch (err) {
36
+ console.log(chalk.red('\n Could not get connection token. Check your network and try again.\n'));
37
+ process.exit(1);
38
+ }
39
+ }
40
+
41
+ const mcpConfig = {
42
+ mcpServers: {
43
+ 'upfynai-code': {
44
+ url: `${serverUrl}/mcp`,
45
+ headers: {
46
+ Authorization: `Bearer ${relayKey}`,
47
+ },
48
+ },
49
+ },
50
+ };
51
+
52
+ console.log(chalk.bold('\n Upfyn-Code — MCP Integration\n'));
53
+ console.log(chalk.dim(' Add this to your Claude / Cursor MCP settings:\n'));
54
+ console.log(chalk.white(JSON.stringify(mcpConfig, null, 2)));
55
+ console.log('');
56
+ console.log(chalk.dim(' The JSON above contains your MCP URL and authorization header.\n'));
57
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Permission-Gated Tool Execution
3
+ * Ported from opencode-ai/opencode internal/permission/permission.go
4
+ *
5
+ * Splits relay actions into safe (auto-approve) and dangerous (require browser approval).
6
+ * Permission requests are sent to the server → browser, and we wait for the response.
7
+ */
8
+
9
+ // Safe actions — auto-approved, read-only (opencode pattern: tools without permission flag)
10
+ const SAFE_ACTIONS = new Set([
11
+ 'file-read',
12
+ 'file-tree',
13
+ 'browse-dirs',
14
+ 'validate-path',
15
+ 'detect-agents',
16
+ ]);
17
+
18
+ // Dangerous actions — require user approval (opencode pattern: tools with permission flag)
19
+ const DANGEROUS_ACTIONS = new Set([
20
+ 'shell-command',
21
+ 'file-write',
22
+ 'create-folder',
23
+ 'git-operation',
24
+ ]);
25
+
26
+ // Safe shell command prefixes — auto-approved even within shell-command
27
+ // (opencode pattern: safe bash commands bypass permission)
28
+ const SAFE_SHELL_PREFIXES = [
29
+ 'ls', 'dir', 'pwd', 'echo', 'cat', 'head', 'tail', 'wc',
30
+ 'git status', 'git log', 'git diff', 'git branch', 'git remote',
31
+ 'node --version', 'npm --version', 'python --version',
32
+ 'which', 'where', 'type', 'whoami', 'hostname',
33
+ 'date', 'uptime', 'df', 'free',
34
+ ];
35
+
36
+ // Pending permission requests: requestId → { resolve }
37
+ const pendingPermissions = new Map();
38
+
39
+ /**
40
+ * Check if an action needs user permission.
41
+ * @param {string} action - Relay action type
42
+ * @param {object} payload - Action payload
43
+ * @returns {boolean}
44
+ */
45
+ export function needsPermission(action, payload) {
46
+ if (SAFE_ACTIONS.has(action)) return false;
47
+ if (!DANGEROUS_ACTIONS.has(action)) return false; // unknown actions handled elsewhere
48
+
49
+ // Shell commands: check if it's a safe read-only command
50
+ if (action === 'shell-command' && payload?.command) {
51
+ const cmd = payload.command.trim().toLowerCase();
52
+ if (SAFE_SHELL_PREFIXES.some(prefix => cmd.startsWith(prefix))) {
53
+ return false;
54
+ }
55
+ }
56
+
57
+ return true;
58
+ }
59
+
60
+ /**
61
+ * Request permission from the browser via the relay WebSocket.
62
+ * Blocks until the user approves or denies.
63
+ *
64
+ * @param {WebSocket} ws - Relay WebSocket connection
65
+ * @param {string} requestId - Relay request ID
66
+ * @param {string} action - Action being requested
67
+ * @param {object} payload - Action payload
68
+ * @param {number} timeoutMs - Timeout for waiting (default 60s)
69
+ * @returns {Promise<boolean>} - true if approved, false if denied
70
+ */
71
+ export function requestPermission(ws, requestId, action, payload, timeoutMs = 60000) {
72
+ return new Promise((resolve) => {
73
+ const permId = `perm-${requestId}`;
74
+
75
+ const timeout = setTimeout(() => {
76
+ pendingPermissions.delete(permId);
77
+ resolve(false); // Timeout = deny
78
+ }, timeoutMs);
79
+
80
+ pendingPermissions.set(permId, {
81
+ resolve: (approved) => {
82
+ clearTimeout(timeout);
83
+ pendingPermissions.delete(permId);
84
+ resolve(approved);
85
+ },
86
+ });
87
+
88
+ // Send permission request to server → browser
89
+ ws.send(JSON.stringify({
90
+ type: 'relay-permission-request',
91
+ requestId,
92
+ permissionId: permId,
93
+ action,
94
+ description: describeAction(action, payload),
95
+ payload: sanitizePayload(action, payload),
96
+ }));
97
+ });
98
+ }
99
+
100
+ /**
101
+ * Handle a permission response from the server.
102
+ * @param {object} data - { permissionId, approved }
103
+ */
104
+ export function handlePermissionResponse(data) {
105
+ const pending = pendingPermissions.get(data.permissionId);
106
+ if (pending) {
107
+ pending.resolve(Boolean(data.approved));
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Generate a human-readable description of the action.
113
+ */
114
+ function describeAction(action, payload) {
115
+ switch (action) {
116
+ case 'shell-command':
117
+ return `Execute command: ${payload?.command || '(unknown)'}`;
118
+ case 'file-write':
119
+ return `Write to file: ${payload?.filePath || '(unknown)'}`;
120
+ case 'create-folder':
121
+ return `Create folder: ${payload?.folderPath || '(unknown)'}`;
122
+ case 'git-operation':
123
+ return `Run git: ${payload?.gitCommand || '(unknown)'}`;
124
+ default:
125
+ return `${action}: ${JSON.stringify(payload || {}).slice(0, 200)}`;
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Sanitize payload for display — remove large content fields.
131
+ */
132
+ function sanitizePayload(action, payload) {
133
+ if (!payload) return {};
134
+ const safe = { ...payload };
135
+ // Don't send file content to browser for permission display
136
+ if (safe.content && safe.content.length > 500) {
137
+ safe.content = safe.content.slice(0, 500) + `... (${safe.content.length} chars total)`;
138
+ }
139
+ return safe;
140
+ }
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Persistent Shell Singleton
3
+ * Ported from opencode-ai/opencode internal/llm/tools/shell/shell.go
4
+ *
5
+ * Maintains a single long-running shell process across commands.
6
+ * - Commands are queued and executed sequentially
7
+ * - Environment and cwd persist between commands
8
+ * - Child processes can be killed on abort
9
+ * - Shell respawns automatically if it dies
10
+ */
11
+ import { spawn, execSync } from 'child_process';
12
+ import os from 'os';
13
+ import path from 'path';
14
+ import { promises as fs } from 'fs';
15
+
16
+ let shellInstance = null;
17
+
18
+ /**
19
+ * Get or create the persistent shell singleton.
20
+ * @param {string} workingDir - Initial working directory
21
+ * @returns {PersistentShell}
22
+ */
23
+ export function getPersistentShell(workingDir) {
24
+ if (!shellInstance || !shellInstance.isAlive) {
25
+ shellInstance = new PersistentShell(workingDir || os.homedir());
26
+ }
27
+ return shellInstance;
28
+ }
29
+
30
+ class PersistentShell {
31
+ constructor(cwd) {
32
+ this.cwd = cwd;
33
+ this.isAlive = false;
34
+ this.commandQueue = [];
35
+ this.processing = false;
36
+ this.proc = null;
37
+ this.stdin = null;
38
+ this._start();
39
+ }
40
+
41
+ _start() {
42
+ const isWin = process.platform === 'win32';
43
+ const shellPath = isWin
44
+ ? process.env.COMSPEC || 'cmd.exe'
45
+ : process.env.SHELL || '/bin/bash';
46
+ const shellArgs = isWin ? ['/Q'] : ['-l'];
47
+
48
+ this.proc = spawn(shellPath, shellArgs, {
49
+ cwd: this.cwd,
50
+ env: { ...process.env, GIT_EDITOR: 'true' },
51
+ stdio: ['pipe', 'pipe', 'pipe'],
52
+ shell: false,
53
+ });
54
+
55
+ this.stdin = this.proc.stdin;
56
+ this.isAlive = true;
57
+
58
+ this.proc.on('close', () => {
59
+ this.isAlive = false;
60
+ // Reject any queued commands
61
+ for (const queued of this.commandQueue) {
62
+ queued.reject(new Error('Shell process died'));
63
+ }
64
+ this.commandQueue = [];
65
+ });
66
+
67
+ this.proc.on('error', () => {
68
+ this.isAlive = false;
69
+ });
70
+ }
71
+
72
+ /**
73
+ * Execute a command in the persistent shell.
74
+ * Queued and executed sequentially (opencode pattern: commandQueue channel).
75
+ *
76
+ * @param {string} command - Shell command to execute
77
+ * @param {object} opts - { timeoutMs, abortSignal }
78
+ * @returns {Promise<{ stdout, stderr, exitCode, interrupted, cwd }>}
79
+ */
80
+ exec(command, opts = {}) {
81
+ return new Promise((resolve, reject) => {
82
+ this.commandQueue.push({ command, opts, resolve, reject });
83
+ this._processNext();
84
+ });
85
+ }
86
+
87
+ async _processNext() {
88
+ if (this.processing || this.commandQueue.length === 0) return;
89
+ this.processing = true;
90
+
91
+ const { command, opts, resolve, reject } = this.commandQueue.shift();
92
+
93
+ try {
94
+ const result = await this._execCommand(command, opts);
95
+ resolve(result);
96
+ } catch (err) {
97
+ reject(err);
98
+ } finally {
99
+ this.processing = false;
100
+ // Process next in queue
101
+ if (this.commandQueue.length > 0) {
102
+ this._processNext();
103
+ }
104
+ }
105
+ }
106
+
107
+ async _execCommand(command, { timeoutMs = 60000, abortSignal } = {}) {
108
+ if (!this.isAlive) {
109
+ throw new Error('Shell is not alive');
110
+ }
111
+
112
+ const isWin = process.platform === 'win32';
113
+ const tmpDir = os.tmpdir();
114
+ const ts = Date.now() + '-' + Math.random().toString(36).slice(2, 8);
115
+ const stdoutFile = path.join(tmpDir, `uc-stdout-${ts}`);
116
+ const stderrFile = path.join(tmpDir, `uc-stderr-${ts}`);
117
+ const statusFile = path.join(tmpDir, `uc-status-${ts}`);
118
+ const cwdFile = path.join(tmpDir, `uc-cwd-${ts}`);
119
+
120
+ try {
121
+ // Build the shell command that redirects output to temp files
122
+ // Exact same pattern as opencode's shell.go execCommand
123
+ let fullCommand;
124
+ if (isWin) {
125
+ // Windows cmd.exe variant
126
+ fullCommand = [
127
+ `${command} > "${stdoutFile}" 2> "${stderrFile}"`,
128
+ `echo %ERRORLEVEL% > "${statusFile}"`,
129
+ `cd > "${cwdFile}"`,
130
+ ].join(' & ');
131
+ } else {
132
+ // Unix bash variant (identical to opencode)
133
+ const escaped = command.replace(/'/g, "'\\''");
134
+ fullCommand = [
135
+ `eval '${escaped}' < /dev/null > '${stdoutFile}' 2> '${stderrFile}'`,
136
+ `EXEC_EXIT_CODE=$?`,
137
+ `pwd > '${cwdFile}'`,
138
+ `echo $EXEC_EXIT_CODE > '${statusFile}'`,
139
+ ].join('\n');
140
+ }
141
+
142
+ this.stdin.write(fullCommand + '\n');
143
+
144
+ // Poll for status file (same polling pattern as opencode)
145
+ let interrupted = false;
146
+ const startTime = Date.now();
147
+
148
+ await new Promise((done, fail) => {
149
+ const poll = setInterval(async () => {
150
+ // Check abort
151
+ if (abortSignal?.aborted) {
152
+ this.killChildren();
153
+ interrupted = true;
154
+ clearInterval(poll);
155
+ done();
156
+ return;
157
+ }
158
+
159
+ // Check timeout
160
+ if (timeoutMs > 0 && Date.now() - startTime > timeoutMs) {
161
+ this.killChildren();
162
+ interrupted = true;
163
+ clearInterval(poll);
164
+ done();
165
+ return;
166
+ }
167
+
168
+ // Check if status file exists and has content
169
+ try {
170
+ const stat = await fs.stat(statusFile);
171
+ if (stat.size > 0) {
172
+ clearInterval(poll);
173
+ done();
174
+ }
175
+ } catch {
176
+ // File doesn't exist yet
177
+ }
178
+ }, 50);
179
+ });
180
+
181
+ // Read results from temp files
182
+ const stdout = await this._readFileOrEmpty(stdoutFile);
183
+ const stderr = await this._readFileOrEmpty(stderrFile);
184
+ const exitCodeStr = await this._readFileOrEmpty(statusFile);
185
+ const newCwd = await this._readFileOrEmpty(cwdFile);
186
+
187
+ let exitCode = 0;
188
+ if (exitCodeStr.trim()) {
189
+ exitCode = parseInt(exitCodeStr.trim(), 10) || 0;
190
+ } else if (interrupted) {
191
+ exitCode = 143;
192
+ }
193
+
194
+ if (newCwd.trim()) {
195
+ this.cwd = newCwd.trim();
196
+ }
197
+
198
+ return {
199
+ stdout: stdout,
200
+ stderr: interrupted ? stderr + '\nCommand execution timed out or was interrupted' : stderr,
201
+ exitCode,
202
+ interrupted,
203
+ cwd: this.cwd,
204
+ };
205
+ } finally {
206
+ // Cleanup temp files
207
+ await Promise.all([
208
+ fs.unlink(stdoutFile).catch(() => {}),
209
+ fs.unlink(stderrFile).catch(() => {}),
210
+ fs.unlink(statusFile).catch(() => {}),
211
+ fs.unlink(cwdFile).catch(() => {}),
212
+ ]);
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Kill child processes of the shell (opencode pattern: killChildren).
218
+ * On Unix: pgrep -P <pid> → SIGTERM each child.
219
+ * On Windows: taskkill /PID /T.
220
+ */
221
+ killChildren() {
222
+ if (!this.proc?.pid) return;
223
+ try {
224
+ if (process.platform === 'win32') {
225
+ execSync(`taskkill /PID ${this.proc.pid} /T /F`, { stdio: 'ignore', timeout: 5000 });
226
+ // Respawn since taskkill kills the parent too on Windows
227
+ this._start();
228
+ } else {
229
+ const output = execSync(`pgrep -P ${this.proc.pid}`, { encoding: 'utf8', timeout: 5000 });
230
+ for (const line of output.split('\n')) {
231
+ const pid = parseInt(line.trim(), 10);
232
+ if (pid > 0) {
233
+ try { process.kill(pid, 'SIGTERM'); } catch { /* already dead */ }
234
+ }
235
+ }
236
+ }
237
+ } catch {
238
+ // pgrep/taskkill failed — no children to kill
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Close the persistent shell.
244
+ */
245
+ close() {
246
+ if (!this.isAlive) return;
247
+ try {
248
+ this.stdin.write('exit\n');
249
+ this.proc.kill('SIGTERM');
250
+ } catch { /* ignore */ }
251
+ this.isAlive = false;
252
+ }
253
+
254
+ async _readFileOrEmpty(filePath) {
255
+ try {
256
+ return await fs.readFile(filePath, 'utf8');
257
+ } catch {
258
+ return '';
259
+ }
260
+ }
261
+ }
package/src/server.js ADDED
@@ -0,0 +1,54 @@
1
+ import { fileURLToPath } from 'url';
2
+ import { dirname, join } from 'path';
3
+ import { existsSync } from 'fs';
4
+ import express from 'express';
5
+ import { createProxyMiddleware } from 'http-proxy-middleware';
6
+ import open from 'open';
7
+ import chalk from 'chalk';
8
+
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const DIST_DIR = join(__dirname, '..', 'dist');
11
+
12
+ export async function startServer(port, serverUrl, token) {
13
+ if (!existsSync(DIST_DIR)) {
14
+ console.log(chalk.red('\n Error: No built frontend found at dist/.'));
15
+ console.log(chalk.dim(' Run the build-frontend script first, or use the default mode (no --local flag).\n'));
16
+ process.exit(1);
17
+ }
18
+
19
+ const app = express();
20
+
21
+ // Proxy API calls to the remote Vercel backend
22
+ app.use('/api', createProxyMiddleware({
23
+ target: serverUrl,
24
+ changeOrigin: true,
25
+ headers: {
26
+ Authorization: `Bearer ${token}`,
27
+ },
28
+ }));
29
+
30
+ // Serve the built React frontend
31
+ app.use(express.static(DIST_DIR));
32
+
33
+ // SPA fallback — serve index.html for all non-API, non-static routes
34
+ app.get('*', (req, res) => {
35
+ res.sendFile(join(DIST_DIR, 'index.html'));
36
+ });
37
+
38
+ return new Promise((resolve) => {
39
+ const server = app.listen(port, async () => {
40
+ const url = `http://localhost:${port}?token=${encodeURIComponent(token)}`;
41
+ console.log(chalk.green(`\n Local server running at ${chalk.bold(`http://localhost:${port}`)}`));
42
+ console.log(chalk.dim(' API proxy active. Press Ctrl+C to stop.\n'));
43
+ await open(url);
44
+ resolve(server);
45
+ });
46
+
47
+ const shutdown = () => {
48
+ console.log(chalk.dim('\n Shutting down...'));
49
+ server.close(() => process.exit(0));
50
+ };
51
+ process.on('SIGINT', shutdown);
52
+ process.on('SIGTERM', shutdown);
53
+ });
54
+ }
@@ -1 +0,0 @@
1
- import{u as x,h as p,r as u,j as e}from"./vendor-react-96lCPsRK.js";import{u as f}from"./index-HaY-3pK1.js";import h from"./CanvasWorkspace-D7ORj358.js";import"./vendor-syntax-LS_Nt30I.js";import"./vendor-markdown-CimbIo6Y.js";import"./vendor-icons-GyYE35HP.js";import"./vendor-i18n-DCFGyhQR.js";import"./vendor-canvas-DvHJ_Pn2.js";import"./vendor-mermaid-DucWyDEe.js";function E(){const{projectName:s}=x(),r=p(),{sendMessage:l,latestMessage:m,connectionState:t}=f();if(u.useEffect(()=>{const n=o=>{var c,d;const i=(d=(c=o.target)==null?void 0:c.tagName)==null?void 0:d.toLowerCase();o.key==="Escape"&&i!=="input"&&i!=="textarea"&&r("/")};return window.addEventListener("keydown",n),()=>window.removeEventListener("keydown",n)},[r]),!s)return e.jsx("div",{className:"flex items-center justify-center h-screen bg-background text-muted-foreground",children:"No project selected"});const a=decodeURIComponent(s);return e.jsxs("div",{className:"fixed inset-0 bg-background flex flex-col z-50",children:[e.jsxs("div",{className:"flex items-center justify-between px-4 py-2 border-b border-border/50 bg-card/50 backdrop-blur-sm shrink-0",children:[e.jsxs("div",{className:"flex items-center gap-3",children:[e.jsxs("button",{onClick:()=>r("/"),className:"flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors",children:[e.jsx("svg",{className:"w-4 h-4",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:e.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M15 19l-7-7 7-7"})}),"Back"]}),e.jsx("div",{className:"h-4 w-px bg-border/50"}),e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx("svg",{className:"w-4 h-4 text-primary",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:e.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z"})}),e.jsx("span",{className:"text-sm font-medium text-foreground",children:a}),e.jsx("span",{className:"text-[10px] px-1.5 py-0.5 rounded-full bg-primary/10 text-primary border border-primary/20 font-medium",children:"Canvas"})]})]}),e.jsxs("div",{className:"flex items-center gap-2",children:[t!=="connected"&&e.jsxs("div",{className:"flex items-center gap-1.5 text-[10px] px-2 py-0.5 rounded-full border border-amber-500/20 bg-amber-500/5 text-amber-400",children:[e.jsx("div",{className:"w-1.5 h-1.5 bg-amber-400 rounded-full animate-pulse"}),t==="reconnecting"?"Reconnecting...":"Offline"]}),e.jsx("span",{className:"text-[10px] text-muted-foreground",children:"Press Esc to exit"})]})]}),e.jsx("div",{className:"flex-1 min-h-0",children:e.jsx(h,{projectName:a,sendMessage:l,latestMessage:m,isFullScreen:!0})})]})}export{E as default};