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