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/auth.js ADDED
@@ -0,0 +1,142 @@
1
+ import prompts from 'prompts';
2
+ import chalk from 'chalk';
3
+ import { readConfig, writeConfig, clearConfig, displayUrl } from './config.js';
4
+
5
+ async function apiCall(path, options = {}) {
6
+ const config = readConfig();
7
+ const url = `${config.serverUrl}${path}`;
8
+ const res = await fetch(url, {
9
+ ...options,
10
+ headers: {
11
+ 'Content-Type': 'application/json',
12
+ ...(options.headers || {}),
13
+ },
14
+ });
15
+ return res;
16
+ }
17
+
18
+ export function getToken() {
19
+ const config = readConfig();
20
+ return config.token || null;
21
+ }
22
+
23
+ export async function validateToken() {
24
+ const token = getToken();
25
+ if (!token) return null;
26
+
27
+ try {
28
+ const res = await apiCall('/api/auth/user', {
29
+ headers: { Authorization: `Bearer ${token}` },
30
+ });
31
+ if (!res.ok) return null;
32
+ const data = await res.json();
33
+ return data.user || null;
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ export async function login(options = {}) {
40
+ // Allow --server flag to override the server URL
41
+ if (options.server) {
42
+ writeConfig({ serverUrl: options.server.replace(/\/+$/, '') });
43
+ }
44
+
45
+ console.log(chalk.bold('\n Upfyn Code — Login\n'));
46
+
47
+ const response = await prompts([
48
+ {
49
+ type: 'text',
50
+ name: 'username',
51
+ message: 'Username',
52
+ validate: v => (v.trim() ? true : 'Username is required'),
53
+ },
54
+ {
55
+ type: 'password',
56
+ name: 'password',
57
+ message: 'Password',
58
+ validate: v => (v ? true : 'Password is required'),
59
+ },
60
+ ], {
61
+ onCancel: () => {
62
+ console.log(chalk.dim('\n Login cancelled.\n'));
63
+ process.exit(0);
64
+ },
65
+ });
66
+
67
+ const { username, password } = response;
68
+
69
+ try {
70
+ const res = await apiCall('/api/auth/login', {
71
+ method: 'POST',
72
+ body: JSON.stringify({ username: username.trim(), password }),
73
+ });
74
+
75
+ const data = await res.json();
76
+
77
+ if (!res.ok) {
78
+ console.log(chalk.red(`\n Login failed: ${data.error || 'Unknown error'}\n`));
79
+ process.exit(1);
80
+ }
81
+
82
+ writeConfig({
83
+ token: data.token,
84
+ user: data.user,
85
+ });
86
+
87
+ const name = data.user.first_name || data.user.username;
88
+ console.log(chalk.green(`\n Logged in as ${chalk.bold(name)}!`));
89
+
90
+ // Auto-fetch and save relay token for one-command connect
91
+ try {
92
+ const config = readConfig();
93
+ const tokenRes = await fetch(`${config.serverUrl}/api/auth/connect-token`, {
94
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${data.token}` },
95
+ });
96
+ if (tokenRes.ok) {
97
+ const tokenData = await tokenRes.json();
98
+ if (tokenData.token) {
99
+ writeConfig({ relayKey: tokenData.token });
100
+ console.log(chalk.dim(' Relay token saved.'));
101
+ console.log(chalk.cyan(`\n Run ${chalk.bold('uc web connect')} to bridge your machine to the web UI.\n`));
102
+ }
103
+ }
104
+ } catch { /* best-effort — relay token fetch is optional */ }
105
+
106
+ if (!readConfig().relayKey) {
107
+ console.log('');
108
+ }
109
+ } catch (err) {
110
+ const config = readConfig();
111
+ console.log(chalk.red(`\n Connection error. Could not reach ${displayUrl(config.serverUrl)}.`));
112
+ console.log(chalk.dim(' Check your network and try again.\n'));
113
+ process.exit(1);
114
+ }
115
+ }
116
+
117
+ export async function logout() {
118
+ clearConfig();
119
+ console.log(chalk.green('\n Logged out. Credentials cleared.\n'));
120
+ }
121
+
122
+ export async function status() {
123
+ const config = readConfig();
124
+
125
+ if (!config.token) {
126
+ console.log(chalk.yellow('\n Not logged in. Run `uc login` to authenticate.\n'));
127
+ return;
128
+ }
129
+
130
+ console.log(chalk.dim('\n Checking session...'));
131
+ const user = await validateToken();
132
+
133
+ if (!user) {
134
+ console.log(chalk.yellow(' Session expired. Run `uc login` to re-authenticate.\n'));
135
+ return;
136
+ }
137
+
138
+ const name = user.first_name || user.username;
139
+ console.log(chalk.bold(` Logged in as: ${chalk.cyan(name)} (${chalk.dim(user.username)})`));
140
+ console.log(chalk.dim(` Server: ${displayUrl(config.serverUrl)}`));
141
+ console.log(chalk.dim(` Local port: ${config.localPort}\n`));
142
+ }
package/src/config.js ADDED
@@ -0,0 +1,40 @@
1
+ import { readFileSync, writeFileSync, mkdirSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import { join, dirname } from 'path';
4
+
5
+ const CONFIG_PATH = join(homedir(), '.upfynrc');
6
+
7
+ const DEFAULTS = {
8
+ serverUrl: 'https://upfynai-code-production.up.railway.app',
9
+ localPort: 4200,
10
+ };
11
+
12
+ export function readConfig() {
13
+ try {
14
+ const raw = readFileSync(CONFIG_PATH, 'utf-8');
15
+ return { ...DEFAULTS, ...JSON.parse(raw) };
16
+ } catch {
17
+ return { ...DEFAULTS };
18
+ }
19
+ }
20
+
21
+ export function writeConfig(data) {
22
+ const existing = readConfig();
23
+ const merged = { ...existing, ...data };
24
+ mkdirSync(dirname(CONFIG_PATH), { recursive: true });
25
+ writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
26
+ return merged;
27
+ }
28
+
29
+ export function clearConfig() {
30
+ writeFileSync(CONFIG_PATH, JSON.stringify(DEFAULTS, null, 2) + '\n', 'utf-8');
31
+ }
32
+
33
+ /** Display-friendly server label — masks the internal URL */
34
+ export function displayUrl(url) {
35
+ if (!url || url === DEFAULTS.serverUrl) return 'Upfyn Cloud';
36
+ // Custom server — show the hostname only
37
+ try { return new URL(url).host; } catch { return url; }
38
+ }
39
+
40
+ export { CONFIG_PATH, DEFAULTS };
package/src/connect.js ADDED
@@ -0,0 +1,416 @@
1
+ import WebSocket from 'ws';
2
+ import os from 'os';
3
+ import { spawn } from 'child_process';
4
+ import { promises as fsPromises, existsSync } from 'fs';
5
+ import path, { dirname, join } from 'path';
6
+ import { fileURLToPath } from 'url';
7
+ import chalk from 'chalk';
8
+ import { readConfig, writeConfig, displayUrl } from './config.js';
9
+ import { getToken, validateToken } from './auth.js';
10
+ import { getPersistentShell } from './persistent-shell.js';
11
+ import { needsPermission, requestPermission, handlePermissionResponse } from './permissions.js';
12
+ import { playConnectAnimation } from './animation.js';
13
+
14
+ // Optional node-pty for proper terminal emulation (graceful fallback to spawn)
15
+ let pty;
16
+ try { pty = (await import('node-pty')).default; } catch { pty = null; }
17
+
18
+ // Resolve agents: dist/agents/ (npm package) or ../../shared/agents/ (monorepo)
19
+ const __connectDir = dirname(fileURLToPath(import.meta.url));
20
+ const _agentsPath = existsSync(join(__connectDir, '../dist/agents/index.js'))
21
+ ? '../dist/agents/index.js'
22
+ : '../../shared/agents/index.js';
23
+ const { executeAction, isStreamingAction } = await import(_agentsPath);
24
+
25
+ // Active process tracking (opencode pattern: activeRequests sync.Map)
26
+ const activeProcesses = new Map(); // requestId → { proc, action }
27
+
28
+ // Active shell sessions for relay terminal (shellSessionId → { proc })
29
+ const activeShellSessions = new Map();
30
+
31
+ /**
32
+ * Start an interactive shell session on the local machine, relayed to the browser.
33
+ * Uses node-pty for proper terminal emulation when available, falls back to spawn.
34
+ */
35
+ function handleShellSessionStart(data, ws) {
36
+ const shellSessionId = data.requestId;
37
+ const projectPath = data.projectPath || process.cwd();
38
+ const isWin = process.platform === 'win32';
39
+ const shellType = data.shellType;
40
+ const cols = data.cols || 80;
41
+ const rows = data.rows || 24;
42
+
43
+ console.log(chalk.cyan(` [relay] Starting shell session in ${projectPath}${pty ? ' (pty)' : ' (spawn)'}`));
44
+
45
+ let shellCmd, shellArgs;
46
+ const provider = data.provider || 'claude';
47
+ const isPlainShell = data.isPlainShell || (!!data.initialCommand && !data.hasSession) || provider === 'plain-shell';
48
+
49
+ function getInteractiveShell(projectDir) {
50
+ if (isWin) {
51
+ if (shellType === 'cmd') return { cmd: 'cmd.exe', args: [] };
52
+ return { cmd: 'powershell.exe', args: ['-NoExit'] };
53
+ }
54
+ const sh = shellType || process.env.SHELL || 'bash';
55
+ return { cmd: sh, args: ['--login'] };
56
+ }
57
+
58
+ function wrapCommand(command, projectDir) {
59
+ if (isWin) {
60
+ if (shellType === 'cmd') return { cmd: 'cmd.exe', args: ['/C', `cd /d "${projectDir}" && ${command}`] };
61
+ return { cmd: 'powershell.exe', args: ['-Command', `Set-Location -LiteralPath '${projectDir.replace(/'/g, "''")}'; ${command}`] };
62
+ }
63
+ const sh = shellType || 'bash';
64
+ return { cmd: sh, args: ['-c', `cd "${projectDir}" && ${command}`] };
65
+ }
66
+
67
+ if (isPlainShell && data.initialCommand) {
68
+ const s = wrapCommand(data.initialCommand, projectPath);
69
+ shellCmd = s.cmd; shellArgs = s.args;
70
+ } else if (isPlainShell) {
71
+ const s = getInteractiveShell(projectPath);
72
+ shellCmd = s.cmd; shellArgs = s.args;
73
+ } else if (provider === 'cursor') {
74
+ const cursorCmd = data.hasSession && data.sessionId
75
+ ? `cursor-agent --resume="${data.sessionId}"`
76
+ : 'cursor-agent';
77
+ const s = wrapCommand(cursorCmd, projectPath);
78
+ shellCmd = s.cmd; shellArgs = s.args;
79
+ } else {
80
+ const command = data.initialCommand || 'claude';
81
+ let claudeCmd;
82
+ if (isWin) {
83
+ claudeCmd = data.hasSession && data.sessionId
84
+ ? `claude --resume ${data.sessionId}`
85
+ : command;
86
+ } else {
87
+ claudeCmd = data.hasSession && data.sessionId
88
+ ? `claude --resume ${data.sessionId} || claude`
89
+ : command;
90
+ }
91
+ const s = wrapCommand(claudeCmd, projectPath);
92
+ shellCmd = s.cmd; shellArgs = s.args;
93
+ }
94
+
95
+ ws.send(JSON.stringify({ type: 'relay-shell-output', shellSessionId, data: '' }));
96
+
97
+ // Use node-pty for proper terminal emulation (resize, isatty, line discipline)
98
+ if (pty) {
99
+ const proc = pty.spawn(shellCmd, shellArgs, {
100
+ name: 'xterm-256color',
101
+ cols,
102
+ rows,
103
+ cwd: projectPath,
104
+ env: { ...process.env, TERM: 'xterm-256color', COLORTERM: 'truecolor', FORCE_COLOR: '3' },
105
+ });
106
+
107
+ activeShellSessions.set(shellSessionId, { proc, projectPath, isPty: true });
108
+
109
+ proc.onData((chunk) => {
110
+ if (ws.readyState === WebSocket.OPEN) {
111
+ ws.send(JSON.stringify({ type: 'relay-shell-output', shellSessionId, data: chunk }));
112
+ }
113
+ });
114
+
115
+ proc.onExit(({ exitCode }) => {
116
+ activeShellSessions.delete(shellSessionId);
117
+ if (ws.readyState === WebSocket.OPEN) {
118
+ ws.send(JSON.stringify({ type: 'relay-shell-exited', shellSessionId, exitCode }));
119
+ }
120
+ console.log(chalk.dim(` [relay] Shell session ended (code ${exitCode})`));
121
+ });
122
+ } else {
123
+ // Fallback: spawn without PTY (text wrapping may be incorrect)
124
+ const proc = spawn(shellCmd, shellArgs, {
125
+ cwd: projectPath,
126
+ env: { ...process.env, TERM: 'xterm-256color', COLORTERM: 'truecolor', FORCE_COLOR: '3', COLUMNS: String(cols), LINES: String(rows) },
127
+ stdio: ['pipe', 'pipe', 'pipe'],
128
+ });
129
+
130
+ activeShellSessions.set(shellSessionId, { proc, projectPath, isPty: false });
131
+
132
+ proc.stdout.on('data', (chunk) => {
133
+ if (ws.readyState === WebSocket.OPEN) {
134
+ ws.send(JSON.stringify({ type: 'relay-shell-output', shellSessionId, data: chunk.toString() }));
135
+ }
136
+ });
137
+
138
+ proc.stderr.on('data', (chunk) => {
139
+ if (ws.readyState === WebSocket.OPEN) {
140
+ ws.send(JSON.stringify({ type: 'relay-shell-output', shellSessionId, data: chunk.toString() }));
141
+ }
142
+ });
143
+
144
+ proc.on('close', (code) => {
145
+ activeShellSessions.delete(shellSessionId);
146
+ if (ws.readyState === WebSocket.OPEN) {
147
+ ws.send(JSON.stringify({ type: 'relay-shell-exited', shellSessionId, exitCode: code }));
148
+ }
149
+ console.log(chalk.dim(` [relay] Shell session ended (code ${code})`));
150
+ });
151
+
152
+ proc.on('error', (err) => {
153
+ activeShellSessions.delete(shellSessionId);
154
+ if (ws.readyState === WebSocket.OPEN) {
155
+ ws.send(JSON.stringify({ type: 'relay-shell-output', shellSessionId, data: `\r\n\x1b[31mError: ${err.message}\x1b[0m\r\n` }));
156
+ }
157
+ });
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Build execution context for shared agents.
163
+ * Provides CLI-specific capabilities (persistent shell, process tracking, streaming).
164
+ */
165
+ function buildAgentContext(requestId, ws) {
166
+ return {
167
+ requestId,
168
+ streamMode: 'structured',
169
+ getPersistentShell,
170
+ trackProcess: (id, entry) => activeProcesses.set(id, entry),
171
+ untrackProcess: (id) => activeProcesses.delete(id),
172
+ stream: (data) => {
173
+ if (ws.readyState === WebSocket.OPEN) {
174
+ ws.send(JSON.stringify({ type: 'relay-stream', requestId, data }));
175
+ }
176
+ },
177
+ };
178
+ }
179
+
180
+ /**
181
+ * Handle incoming relay commands from the server.
182
+ * Delegates to shared agent modules for action execution.
183
+ */
184
+ async function handleRelayCommand(data, ws) {
185
+ const { requestId, action } = data;
186
+
187
+ try {
188
+ // Permission gate (opencode pattern: dangerous actions require browser approval)
189
+ if (needsPermission(action, data)) {
190
+ const approved = await requestPermission(ws, requestId, action, data);
191
+ if (!approved) {
192
+ ws.send(JSON.stringify({ type: 'relay-response', requestId, error: 'Permission denied by user' }));
193
+ return;
194
+ }
195
+ }
196
+
197
+ const ctx = buildAgentContext(requestId, ws);
198
+
199
+ // For the exec action, params come from data.options in the old format
200
+ const params = action === 'exec'
201
+ ? { command: data.options?.command || data.command, timeout: data.options?.timeout || data.timeout, cwd: data.options?.cwd || data.cwd }
202
+ : data;
203
+
204
+ if (isStreamingAction(action)) {
205
+ // Streaming actions: agent calls ctx.stream() for chunks, returns on completion
206
+ const result = await executeAction(action, params, ctx);
207
+ ws.send(JSON.stringify({ type: 'relay-complete', requestId, exitCode: result.exitCode, sessionId: result.sessionId }));
208
+ } else {
209
+ // Sync actions: agent returns data directly
210
+ const result = await executeAction(action, params, ctx);
211
+ ws.send(JSON.stringify({ type: 'relay-response', requestId, data: result }));
212
+ }
213
+ } catch (err) {
214
+ ws.send(JSON.stringify({ type: 'relay-response', requestId, error: err.message }));
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Connect to the remote server via WebSocket relay.
220
+ * Bridges local Claude Code, shell, filesystem, and git to the web UI.
221
+ */
222
+ export async function connect(options = {}) {
223
+ const config = readConfig();
224
+ const serverUrl = options.server || config.serverUrl;
225
+ let relayKey = options.key;
226
+
227
+ if (!relayKey) {
228
+ const token = getToken();
229
+ if (!token) {
230
+ console.log(chalk.yellow('\n No account found. Run `uc login` first.\n'));
231
+ process.exit(1);
232
+ }
233
+
234
+ console.log(chalk.dim('\n Validating session...'));
235
+ const user = await validateToken();
236
+ if (!user) {
237
+ console.log(chalk.yellow(' Session expired. Run `uc login` to re-authenticate.\n'));
238
+ process.exit(1);
239
+ }
240
+
241
+ try {
242
+ const res = await fetch(`${serverUrl}/api/auth/connect-token`, {
243
+ headers: { Authorization: `Bearer ${token}` },
244
+ });
245
+ if (!res.ok) throw new Error('Failed to get connect token');
246
+ const data = await res.json();
247
+ relayKey = data.token;
248
+ } catch (err) {
249
+ console.log(chalk.red('\n Could not get connection token. Check your network and try again.\n'));
250
+ process.exit(1);
251
+ }
252
+ }
253
+
254
+ writeConfig({ relayKey });
255
+
256
+ const wsUrl = serverUrl.replace(/^http/, 'ws') + '/relay?token=' + encodeURIComponent(relayKey);
257
+
258
+ // Play spaceship launch animation
259
+ try { await playConnectAnimation(); } catch { /* cosmetic — don't block connect */ }
260
+
261
+ console.log(chalk.bold('\n Upfyn-Code Relay Client\n'));
262
+ console.log(` Server: ${chalk.cyan(displayUrl(serverUrl))}`);
263
+ console.log(` Machine: ${chalk.dim(os.hostname())}`);
264
+ console.log(` User: ${chalk.dim(os.userInfo().username)}\n`);
265
+
266
+ let reconnectAttempts = 0;
267
+ const MAX_RECONNECT = 10;
268
+
269
+ function doConnect() {
270
+ const ws = new WebSocket(wsUrl, {
271
+ headers: {
272
+ 'x-upfyn-machine': os.hostname(),
273
+ 'x-upfyn-platform': process.platform,
274
+ 'x-upfyn-cwd': process.cwd(),
275
+ },
276
+ });
277
+
278
+ // Respond to native WebSocket pings from server (Railway proxy keepalive)
279
+ ws.on('ping', () => { try { ws.pong(); } catch { /* ignore */ } });
280
+
281
+ ws.on('open', () => {
282
+ reconnectAttempts = 0;
283
+ console.log(chalk.green(' Connected! Your local machine is now bridged to the server.'));
284
+ console.log(chalk.dim(' Claude Code is the AI brain. Press Ctrl+C to disconnect.\n'));
285
+
286
+ const cwd = process.cwd();
287
+ const dirName = path.basename(cwd);
288
+ ws.send(JSON.stringify({
289
+ type: 'relay-init', cwd, dirName,
290
+ homedir: os.homedir(), platform: process.platform, hostname: os.hostname(),
291
+ }));
292
+ console.log(chalk.dim(` Default project: ${cwd}\n`));
293
+
294
+ const heartbeat = setInterval(() => {
295
+ if (ws.readyState === WebSocket.OPEN) {
296
+ ws.send(JSON.stringify({ type: 'ping' }));
297
+ try { ws.ping(); } catch { /* ignore */ }
298
+ }
299
+ }, 20000);
300
+ ws.on('close', () => clearInterval(heartbeat));
301
+ });
302
+
303
+ ws.on('message', (rawMessage) => {
304
+ try {
305
+ const data = JSON.parse(rawMessage);
306
+
307
+ if (data.type === 'relay-connected') {
308
+ console.log(chalk.green(` ${data.message}`));
309
+ return;
310
+ }
311
+ if (data.type === 'relay-command') {
312
+ if (data.action === 'shell-session-start') {
313
+ handleShellSessionStart(data, ws);
314
+ } else {
315
+ handleRelayCommand(data, ws);
316
+ }
317
+ return;
318
+ }
319
+ if (data.type === 'relay-abort') {
320
+ const entry = activeProcesses.get(data.requestId);
321
+ if (entry?.instance?.interrupt) {
322
+ // SDK query instance — use interrupt()
323
+ entry.instance.interrupt().catch(() => {});
324
+ activeProcesses.delete(data.requestId);
325
+ ws.send(JSON.stringify({ type: 'relay-complete', requestId: data.requestId, exitCode: -1, aborted: true }));
326
+ console.log(chalk.yellow(' [relay] SDK session interrupted by user'));
327
+ } else if (entry?.proc) {
328
+ // Legacy subprocess — use kill()
329
+ entry.proc.kill('SIGTERM');
330
+ activeProcesses.delete(data.requestId);
331
+ ws.send(JSON.stringify({ type: 'relay-complete', requestId: data.requestId, exitCode: -1, aborted: true }));
332
+ console.log(chalk.yellow(' [relay] Process aborted by user'));
333
+ }
334
+ return;
335
+ }
336
+ if (data.type === 'relay-permission-response') {
337
+ handlePermissionResponse(data);
338
+ return;
339
+ }
340
+ if (data.type === 'relay-shell-input') {
341
+ const session = activeShellSessions.get(data.shellSessionId);
342
+ if (session) {
343
+ if (session.isPty) {
344
+ session.proc.write(data.data);
345
+ } else if (session.proc?.stdin?.writable) {
346
+ session.proc.stdin.write(data.data);
347
+ }
348
+ }
349
+ return;
350
+ }
351
+ if (data.type === 'relay-shell-resize') {
352
+ const session = activeShellSessions.get(data.shellSessionId);
353
+ if (session?.isPty && session.proc?.resize) {
354
+ try { session.proc.resize(data.cols || 80, data.rows || 24); } catch { /* ignore resize errors */ }
355
+ }
356
+ return;
357
+ }
358
+ if (data.type === 'relay-shell-kill') {
359
+ const session = activeShellSessions.get(data.shellSessionId);
360
+ if (session?.proc) {
361
+ if (session.isPty) {
362
+ session.proc.kill();
363
+ } else {
364
+ session.proc.kill('SIGTERM');
365
+ }
366
+ activeShellSessions.delete(data.shellSessionId);
367
+ console.log(chalk.dim(' [relay] Shell session killed'));
368
+ }
369
+ return;
370
+ }
371
+ if (data.type === 'pong') return;
372
+ if (data.type === 'server-ping') {
373
+ if (ws.readyState === WebSocket.OPEN) {
374
+ ws.send(JSON.stringify({ type: 'server-pong' }));
375
+ }
376
+ return;
377
+ }
378
+ if (data.type === 'error') {
379
+ console.error(chalk.red(` Server error: ${data.error}`));
380
+ return;
381
+ }
382
+ } catch (e) {
383
+ // message parse error
384
+ }
385
+ });
386
+
387
+ ws.on('close', (code) => {
388
+ if (code === 1000) {
389
+ console.log(chalk.dim(' Disconnected.'));
390
+ process.exit(0);
391
+ }
392
+ reconnectAttempts++;
393
+ if (reconnectAttempts <= MAX_RECONNECT) {
394
+ const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
395
+ console.log(chalk.dim(` Connection lost. Reconnecting in ${delay / 1000}s... (${reconnectAttempts}/${MAX_RECONNECT})`));
396
+ setTimeout(doConnect, delay);
397
+ } else {
398
+ console.error(chalk.red(' Max reconnection attempts reached. Exiting.'));
399
+ process.exit(1);
400
+ }
401
+ });
402
+
403
+ ws.on('error', (err) => {
404
+ if (err.code === 'ECONNREFUSED') {
405
+ console.error(chalk.red(` Cannot reach ${displayUrl(serverUrl)}. Is the server running?`));
406
+ }
407
+ });
408
+ }
409
+
410
+ doConnect();
411
+
412
+ process.on('SIGINT', () => {
413
+ console.log(chalk.dim('\n Disconnecting...'));
414
+ process.exit(0);
415
+ });
416
+ }
package/src/launch.js ADDED
@@ -0,0 +1,81 @@
1
+ import open from 'open';
2
+ import chalk from 'chalk';
3
+ import { existsSync } from 'fs';
4
+ import { readConfig } from './config.js';
5
+ import { getToken, validateToken } from './auth.js';
6
+ import { startServer } from './server.js';
7
+ import { spawn } from 'child_process';
8
+ import { fileURLToPath } from 'url';
9
+ import { dirname, join } from 'path';
10
+
11
+ const __dirname = dirname(fileURLToPath(import.meta.url));
12
+
13
+ export async function openHosted() {
14
+ const token = getToken();
15
+
16
+ if (!token) {
17
+ console.log(chalk.yellow('\n No account found. Run `uc login` first.\n'));
18
+ process.exit(1);
19
+ }
20
+
21
+ console.log(chalk.dim('\n Validating session...'));
22
+ const user = await validateToken();
23
+
24
+ if (!user) {
25
+ console.log(chalk.yellow(' Session expired. Run `uc login` to re-authenticate.\n'));
26
+ process.exit(1);
27
+ }
28
+
29
+ const config = readConfig();
30
+ const url = `${config.serverUrl}?token=${encodeURIComponent(token)}`;
31
+
32
+ const name = user.first_name || user.username;
33
+ console.log(chalk.green(` Welcome back, ${chalk.bold(name)}!`));
34
+ console.log(chalk.dim(' Opening Upfyn-Code in your browser...\n'));
35
+
36
+ await open(url);
37
+ }
38
+
39
+ export async function startLocal() {
40
+ const config = readConfig();
41
+ const port = config.localPort || 3001;
42
+
43
+ console.log(chalk.dim('\n Starting local server...'));
44
+
45
+ // Try bundled local-server first (npm installs), then monorepo path
46
+ const bundledPath = join(__dirname, '../dist/local-server.js');
47
+ const monoRepoPath = join(__dirname, '../../local-server/index.js');
48
+ const localServerPath = existsSync(bundledPath) ? bundledPath : monoRepoPath;
49
+
50
+ try {
51
+ // Dynamically import the local server
52
+ process.env.PORT = String(port);
53
+ await import(localServerPath);
54
+
55
+ // Auto-open browser after server starts
56
+ setTimeout(async () => {
57
+ const url = `http://localhost:${port}/app/`;
58
+ console.log(chalk.dim(` Opening ${url} in browser...\n`));
59
+ await open(url);
60
+ }, 2000);
61
+ } catch (err) {
62
+ // Fallback: if local-server fails (missing deps), use the old proxy approach
63
+ console.log(chalk.dim(` Local server not available (${err.message}), falling back to proxy mode...`));
64
+
65
+ const token = getToken();
66
+ if (!token) {
67
+ console.log(chalk.yellow('\n No account found. Run `uc login` first.\n'));
68
+ process.exit(1);
69
+ }
70
+
71
+ const user = await validateToken();
72
+ if (!user) {
73
+ console.log(chalk.yellow(' Session expired. Run `uc login` to re-authenticate.\n'));
74
+ process.exit(1);
75
+ }
76
+
77
+ const name = user.first_name || user.username;
78
+ console.log(chalk.green(` Welcome back, ${chalk.bold(name)}!`));
79
+ await startServer(port, config.serverUrl, token);
80
+ }
81
+ }