upfynai-code 2.9.1 → 2.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -66
- package/client/dist/api-docs.html +838 -0
- package/client/dist/assets/AppContent-BXZDeSIC.js +545 -0
- package/client/dist/assets/CanvasFullScreen-mnpCnLZ9.js +1 -0
- package/client/dist/assets/CanvasWorkspace-4CqmjAVQ.js +163 -0
- package/client/dist/assets/DashboardPanel-zFIFlw56.js +1 -0
- package/client/dist/assets/FileTree-B0c_GaB3.js +1 -0
- package/client/dist/assets/GitPanel-DUP4zVU4.js +2 -0
- package/client/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/client/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/client/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/client/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/client/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/client/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/client/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/client/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/client/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/client/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/client/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/client/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/client/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/client/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/client/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/client/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/client/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/client/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/client/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/client/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/client/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/client/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/client/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/client/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/client/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/client/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/client/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/client/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/client/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/client/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/client/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/client/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/client/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/client/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/client/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/client/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/client/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/client/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/client/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/client/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/client/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/client/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/client/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/client/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/client/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/client/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/client/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/client/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/client/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/client/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/client/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/client/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/client/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/client/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/client/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/client/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/client/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/client/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/client/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/client/dist/assets/LoginModal-BRycfsyD.js +13 -0
- package/client/dist/assets/MarkdownPreview-DHmk3qzu.js +1 -0
- package/client/dist/assets/MermaidBlock-BuBc_G-F.js +2 -0
- package/client/dist/assets/Onboarding-BcnaZZ0o.js +1 -0
- package/client/dist/assets/PreviewPanel-CqCa92Tf.js +32 -0
- package/client/dist/assets/SetupForm-S0g6u5yT.js +1 -0
- package/client/dist/assets/WorkflowsPanel-CouH9JDO.js +1 -0
- package/client/dist/assets/index-BFuqS0tY.css +1 -0
- package/client/dist/assets/index-CNDcVl2g.js +68 -0
- package/client/dist/assets/pdf-CE_K4jFx.js +12 -0
- package/client/dist/assets/vendor-canvas-BZV40eAE.css +1 -0
- package/client/dist/assets/vendor-canvas-D39yWul6.js +49 -0
- package/client/dist/assets/vendor-codemirror-CbtmxxaB.js +35 -0
- package/client/dist/assets/vendor-diff-DNQpbhrT.js +69 -0
- package/client/dist/assets/vendor-i18n-DCFGyhQR.js +1 -0
- package/client/dist/assets/vendor-icons-BaD0x9SL.js +711 -0
- package/client/dist/assets/vendor-markdown-CimbIo6Y.js +296 -0
- package/client/dist/assets/vendor-mermaid-CH7SGc99.js +2556 -0
- package/client/dist/assets/vendor-react-96lCPsRK.js +67 -0
- package/client/dist/assets/vendor-syntax-DuHI9Ok6.js +16 -0
- package/client/dist/assets/vendor-xterm-CZq1hqo1.js +66 -0
- package/client/dist/assets/vendor-xterm-qxJ8_QYu.css +32 -0
- package/client/dist/clear-cache.html +85 -0
- package/client/dist/convert-icons.md +53 -0
- package/client/dist/favicon.png +0 -0
- package/client/dist/favicon.svg +5 -0
- package/client/dist/generate-icons.js +49 -0
- package/client/dist/icons/claude-ai-icon.svg +1 -0
- package/client/dist/icons/codex-white.svg +3 -0
- package/client/dist/icons/codex.svg +3 -0
- package/client/dist/icons/cursor-white.svg +12 -0
- package/client/dist/icons/cursor.svg +1 -0
- package/client/dist/icons/icon-128x128.png +0 -0
- package/client/dist/icons/icon-128x128.svg +5 -0
- package/client/dist/icons/icon-144x144.png +0 -0
- package/client/dist/icons/icon-144x144.svg +5 -0
- package/client/dist/icons/icon-152x152.png +0 -0
- package/client/dist/icons/icon-152x152.svg +5 -0
- package/client/dist/icons/icon-192x192.png +0 -0
- package/client/dist/icons/icon-192x192.svg +5 -0
- package/client/dist/icons/icon-384x384.png +0 -0
- package/client/dist/icons/icon-384x384.svg +5 -0
- package/client/dist/icons/icon-512x512.png +0 -0
- package/client/dist/icons/icon-512x512.svg +5 -0
- package/client/dist/icons/icon-72x72.png +0 -0
- package/client/dist/icons/icon-72x72.svg +5 -0
- package/client/dist/icons/icon-96x96.png +0 -0
- package/client/dist/icons/icon-96x96.svg +5 -0
- package/client/dist/icons/icon-template.svg +5 -0
- package/client/dist/index.html +119 -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/client/dist/logo.svg +14 -0
- package/client/dist/manifest.json +61 -0
- package/client/dist/mcp-docs.html +108 -0
- package/client/dist/offline.html +84 -0
- package/client/dist/screenshots/cli-selection.png +0 -0
- package/client/dist/screenshots/desktop-main.png +0 -0
- package/client/dist/screenshots/mobile-chat.png +0 -0
- package/client/dist/screenshots/tools-modal.png +0 -0
- package/client/dist/sw.js +82 -0
- package/commands/upfynai-connect.md +59 -0
- package/commands/upfynai-disconnect.md +31 -0
- package/commands/upfynai-doctor.md +99 -0
- package/commands/upfynai-export.md +49 -0
- package/commands/upfynai-local.md +82 -0
- package/commands/upfynai-status.md +75 -0
- package/commands/upfynai-stop.md +49 -0
- package/commands/upfynai-uninstall.md +58 -0
- package/commands/upfynai.md +69 -0
- package/package.json +143 -82
- package/scripts/build-client.js +17 -0
- package/scripts/fix-node-pty.js +67 -0
- package/scripts/install-commands.js +78 -0
- package/server/agent-loop.js +242 -0
- package/server/auto-compact.js +99 -0
- package/server/claude-sdk.js +797 -0
- package/server/cli-ui.js +785 -0
- package/server/cli.js +596 -0
- package/server/constants/config.js +31 -0
- package/server/cursor-cli.js +270 -0
- package/server/database/auth.db +0 -0
- package/server/database/db.js +1391 -0
- package/server/database/init.sql +70 -0
- package/server/index.js +3799 -0
- package/server/load-env.js +26 -0
- package/server/mcp-server.js +621 -0
- package/server/middleware/auth.js +176 -0
- package/server/middleware/relayHelpers.js +44 -0
- package/server/middleware/sandboxRouter.js +174 -0
- package/server/openai-codex.js +403 -0
- package/server/openrouter.js +137 -0
- package/server/projects.js +1807 -0
- package/server/provider-factory.js +174 -0
- package/server/relay-client.js +379 -0
- package/server/routes/agent.js +1226 -0
- package/server/routes/auth.js +554 -0
- package/server/routes/canvas.js +53 -0
- package/server/routes/cli-auth.js +263 -0
- package/server/routes/codex.js +396 -0
- package/server/routes/commands.js +707 -0
- package/server/routes/composio.js +176 -0
- package/server/routes/cursor.js +770 -0
- package/server/routes/dashboard.js +295 -0
- package/server/routes/git.js +1208 -0
- package/server/routes/keys.js +34 -0
- package/server/routes/mcp-utils.js +48 -0
- package/server/routes/mcp.js +661 -0
- package/server/routes/payments.js +227 -0
- package/server/routes/projects.js +655 -0
- package/server/routes/sessions.js +146 -0
- package/server/routes/settings.js +261 -0
- package/server/routes/taskmaster.js +1928 -0
- package/server/routes/user.js +106 -0
- package/server/routes/vapi-chat.js +624 -0
- package/server/routes/voice.js +235 -0
- package/server/routes/webhooks.js +166 -0
- package/server/routes/workflows.js +312 -0
- package/server/sandbox.js +120 -0
- package/server/services/composio.js +204 -0
- package/server/services/sessionRegistry.js +139 -0
- package/server/services/whisperService.js +84 -0
- package/server/services/workflowScheduler.js +206 -0
- package/server/tests/relay-flow.test.js +570 -0
- package/server/tests/sessions.test.js +259 -0
- package/server/utils/commandParser.js +303 -0
- package/server/utils/email.js +61 -0
- package/server/utils/gitConfig.js +24 -0
- package/server/utils/mcp-detector.js +198 -0
- package/server/utils/taskmaster-websocket.js +129 -0
- package/shared/integrationCatalog.d.ts +12 -0
- package/shared/integrationCatalog.js +172 -0
- package/shared/modelConstants.js +96 -0
- package/bin/cli.js +0 -97
- package/dist/agents/claude.js +0 -229
- package/dist/agents/codex.js +0 -48
- package/dist/agents/cursor.js +0 -48
- package/dist/agents/detect.js +0 -51
- package/dist/agents/exec.js +0 -31
- package/dist/agents/files.js +0 -105
- package/dist/agents/git.js +0 -18
- package/dist/agents/gitagent.js +0 -67
- package/dist/agents/index.js +0 -88
- package/dist/agents/shell.js +0 -38
- package/dist/agents/utils.js +0 -136
- package/scripts/postinstall.js +0 -9
- package/scripts/prepublish.js +0 -58
- package/src/animation.js +0 -228
- package/src/auth.js +0 -122
- package/src/config.js +0 -40
- package/src/connect.js +0 -416
- package/src/launch.js +0 -78
- package/src/mcp.js +0 -57
- package/src/permissions.js +0 -140
- package/src/persistent-shell.js +0 -261
- package/src/server.js +0 -54
- /package/{dist → shared}/gitagent/index.js +0 -0
- /package/{dist → shared}/gitagent/parser.js +0 -0
- /package/{dist → shared}/gitagent/prompt-builder.js +0 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-Provider Factory
|
|
3
|
+
* Ported from opencode-ai/opencode internal/llm/provider/provider.go
|
|
4
|
+
*
|
|
5
|
+
* Unified interface for all AI providers. Most providers reuse the
|
|
6
|
+
* OpenAI-compatible client with different base URLs (opencode pattern).
|
|
7
|
+
* Only Anthropic and Google need native SDKs.
|
|
8
|
+
*
|
|
9
|
+
* This factory abstracts provider differences so the caller only
|
|
10
|
+
* needs to call streamChat(messages, options) regardless of provider.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// OpenAI-compatible endpoints (opencode pattern: one client, many providers)
|
|
14
|
+
const OPENAI_COMPATIBLE_PROVIDERS = {
|
|
15
|
+
openrouter: {
|
|
16
|
+
baseUrl: 'https://openrouter.ai/api/v1',
|
|
17
|
+
keyEnv: 'OPENROUTER_API_KEY',
|
|
18
|
+
extraHeaders: (model) => ({
|
|
19
|
+
'HTTP-Referer': 'https://cli.upfyn.com',
|
|
20
|
+
'X-Title': 'Upfyn-Code',
|
|
21
|
+
}),
|
|
22
|
+
},
|
|
23
|
+
groq: {
|
|
24
|
+
baseUrl: 'https://api.groq.com/openai/v1',
|
|
25
|
+
keyEnv: 'GROQ_API_KEY',
|
|
26
|
+
},
|
|
27
|
+
deepseek: {
|
|
28
|
+
baseUrl: 'https://api.deepseek.com/v1',
|
|
29
|
+
keyEnv: 'DEEPSEEK_API_KEY',
|
|
30
|
+
},
|
|
31
|
+
xai: {
|
|
32
|
+
baseUrl: 'https://api.x.ai/v1',
|
|
33
|
+
keyEnv: 'XAI_API_KEY',
|
|
34
|
+
},
|
|
35
|
+
openai: {
|
|
36
|
+
baseUrl: 'https://api.openai.com/v1',
|
|
37
|
+
keyEnv: 'OPENAI_API_KEY',
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Cost per 1M tokens (opencode pattern: models.go cost tables)
|
|
42
|
+
const MODEL_COSTS = {
|
|
43
|
+
'anthropic/claude-sonnet-4': { input: 3.0, output: 15.0 },
|
|
44
|
+
'anthropic/claude-opus-4': { input: 15.0, output: 75.0 },
|
|
45
|
+
'openai/gpt-4o': { input: 2.5, output: 10.0 },
|
|
46
|
+
'openai/gpt-4o-mini': { input: 0.15, output: 0.6 },
|
|
47
|
+
'openai/o3': { input: 10.0, output: 40.0 },
|
|
48
|
+
'google/gemini-2.5-pro': { input: 1.25, output: 10.0 },
|
|
49
|
+
'deepseek/deepseek-r1': { input: 0.55, output: 2.19 },
|
|
50
|
+
'meta-llama/llama-4-maverick': { input: 0.5, output: 0.7 },
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Detect which provider to use based on model name.
|
|
55
|
+
* @param {string} model - Model name (e.g., 'anthropic/claude-sonnet-4')
|
|
56
|
+
* @returns {string} Provider name
|
|
57
|
+
*/
|
|
58
|
+
export function detectProvider(model) {
|
|
59
|
+
if (!model) return 'openrouter';
|
|
60
|
+
if (model.startsWith('anthropic/')) return 'openrouter'; // route through OpenRouter for BYOK
|
|
61
|
+
if (model.startsWith('openai/')) return 'openrouter';
|
|
62
|
+
if (model.startsWith('google/')) return 'openrouter';
|
|
63
|
+
if (model.startsWith('meta-llama/')) return 'openrouter';
|
|
64
|
+
if (model.startsWith('deepseek/')) return 'openrouter';
|
|
65
|
+
if (model.startsWith('mistralai/')) return 'openrouter';
|
|
66
|
+
// Direct provider keys
|
|
67
|
+
if (model.startsWith('gpt-')) return 'openai';
|
|
68
|
+
if (model.startsWith('claude-')) return 'anthropic';
|
|
69
|
+
return 'openrouter';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Create a streaming chat request to any provider.
|
|
74
|
+
* Uses the OpenAI-compatible API for most providers (opencode pattern).
|
|
75
|
+
*
|
|
76
|
+
* @param {string} model - Model name
|
|
77
|
+
* @param {Array} messages - Array of { role, content }
|
|
78
|
+
* @param {object} options - { apiKey, temperature, maxTokens }
|
|
79
|
+
* @returns {AsyncGenerator} Yields { type, content, usage } events
|
|
80
|
+
*/
|
|
81
|
+
export async function* streamChat(model, messages, options = {}) {
|
|
82
|
+
const provider = detectProvider(model);
|
|
83
|
+
const config = OPENAI_COMPATIBLE_PROVIDERS[provider];
|
|
84
|
+
|
|
85
|
+
if (!config) {
|
|
86
|
+
throw new Error(`Unsupported provider: ${provider}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const apiKey = options.apiKey || process.env[config.keyEnv];
|
|
90
|
+
if (!apiKey) {
|
|
91
|
+
throw new Error(`API key required for ${provider}. Set ${config.keyEnv} or provide via BYOK settings.`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const headers = {
|
|
95
|
+
'Content-Type': 'application/json',
|
|
96
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
97
|
+
...(config.extraHeaders ? config.extraHeaders(model) : {}),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const body = {
|
|
101
|
+
model,
|
|
102
|
+
messages,
|
|
103
|
+
stream: true,
|
|
104
|
+
...(options.temperature !== undefined && { temperature: options.temperature }),
|
|
105
|
+
...(options.maxTokens && { max_tokens: options.maxTokens }),
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const response = await fetch(`${config.baseUrl}/chat/completions`, {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers,
|
|
111
|
+
body: JSON.stringify(body),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
const errorText = await response.text();
|
|
116
|
+
throw new Error(`${provider} API error (${response.status}): ${errorText}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Parse SSE stream
|
|
120
|
+
const reader = response.body.getReader();
|
|
121
|
+
const decoder = new TextDecoder();
|
|
122
|
+
let buffer = '';
|
|
123
|
+
|
|
124
|
+
while (true) {
|
|
125
|
+
const { done, value } = await reader.read();
|
|
126
|
+
if (done) break;
|
|
127
|
+
|
|
128
|
+
buffer += decoder.decode(value, { stream: true });
|
|
129
|
+
const lines = buffer.split('\n');
|
|
130
|
+
buffer = lines.pop();
|
|
131
|
+
|
|
132
|
+
for (const line of lines) {
|
|
133
|
+
if (!line.startsWith('data: ')) continue;
|
|
134
|
+
const data = line.slice(6).trim();
|
|
135
|
+
if (data === '[DONE]') {
|
|
136
|
+
yield { type: 'complete' };
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const parsed = JSON.parse(data);
|
|
142
|
+
const delta = parsed.choices?.[0]?.delta;
|
|
143
|
+
|
|
144
|
+
if (delta?.content) {
|
|
145
|
+
yield { type: 'content_delta', content: delta.content };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Usage info (if provided at the end)
|
|
149
|
+
if (parsed.usage) {
|
|
150
|
+
yield {
|
|
151
|
+
type: 'usage',
|
|
152
|
+
promptTokens: parsed.usage.prompt_tokens || 0,
|
|
153
|
+
completionTokens: parsed.usage.completion_tokens || 0,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
// Skip malformed SSE chunks
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Calculate cost for a request.
|
|
165
|
+
* @param {string} model - Model name
|
|
166
|
+
* @param {number} promptTokens
|
|
167
|
+
* @param {number} completionTokens
|
|
168
|
+
* @returns {number} Cost in cents
|
|
169
|
+
*/
|
|
170
|
+
export function calculateCost(model, promptTokens, completionTokens) {
|
|
171
|
+
const costs = MODEL_COSTS[model];
|
|
172
|
+
if (!costs) return 0;
|
|
173
|
+
return (promptTokens / 1_000_000 * costs.input + completionTokens / 1_000_000 * costs.output) * 100;
|
|
174
|
+
}
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Upfyn-Code Relay Client
|
|
4
|
+
*
|
|
5
|
+
* Connects your local machine to the hosted Upfyn-Code server.
|
|
6
|
+
* Bridges Claude CLI, terminal, filesystem, and git to the web UI.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* upfynai-code connect --server https://upfynai.thinqmesh.com --key upfyn_xxx
|
|
10
|
+
* upfynai-code connect (uses saved config from ~/.upfynai/config.json)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import WebSocket from 'ws';
|
|
14
|
+
import os from 'os';
|
|
15
|
+
import fs from 'fs';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
import { execSync } from 'child_process';
|
|
18
|
+
import {
|
|
19
|
+
c,
|
|
20
|
+
showConnectStartup,
|
|
21
|
+
showConnectionBanner,
|
|
22
|
+
logRelayEvent,
|
|
23
|
+
createSpinner,
|
|
24
|
+
} from './cli-ui.js';
|
|
25
|
+
import { executeAction, isStreamingAction } from '../../packages/shared/agents/index.js';
|
|
26
|
+
|
|
27
|
+
// Load package.json for version
|
|
28
|
+
import { fileURLToPath } from 'url';
|
|
29
|
+
const __filename_rc = fileURLToPath(import.meta.url);
|
|
30
|
+
const __dirname_rc = path.dirname(__filename_rc);
|
|
31
|
+
let VERSION = '0.0.0';
|
|
32
|
+
try {
|
|
33
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname_rc, '../package.json'), 'utf8'));
|
|
34
|
+
VERSION = pkg.version;
|
|
35
|
+
} catch { /* ignore */ }
|
|
36
|
+
|
|
37
|
+
const CONFIG_DIR = path.join(os.homedir(), '.upfynai');
|
|
38
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
39
|
+
|
|
40
|
+
function loadConfig() {
|
|
41
|
+
try {
|
|
42
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
43
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
44
|
+
}
|
|
45
|
+
} catch (e) { /* ignore */ }
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function saveConfig(config) {
|
|
50
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
51
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Resolve a binary name to its full path, checking PATH first then local node_modules/.bin
|
|
58
|
+
*/
|
|
59
|
+
function resolveBinary(name) {
|
|
60
|
+
const isWindows = process.platform === 'win32';
|
|
61
|
+
try {
|
|
62
|
+
const result = execSync(`${isWindows ? 'where' : 'which'} ${name}`, { stdio: 'pipe', timeout: 5000 }).toString().trim();
|
|
63
|
+
return result.split('\n')[0].trim();
|
|
64
|
+
} catch {
|
|
65
|
+
const localPath = path.join(__dirname_rc, '../node_modules/.bin', name + (isWindows ? '.cmd' : ''));
|
|
66
|
+
if (fs.existsSync(localPath)) return localPath;
|
|
67
|
+
return name;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Detect which AI CLI agents are installed on this machine.
|
|
73
|
+
* Used for initial agent-capabilities report (separate from the shared detect agent
|
|
74
|
+
* which returns a different format via relay-response).
|
|
75
|
+
*/
|
|
76
|
+
function detectInstalledAgents() {
|
|
77
|
+
const isWindows = process.platform === 'win32';
|
|
78
|
+
const whichCmd = isWindows ? 'where' : 'which';
|
|
79
|
+
const localBinDir = path.join(__dirname_rc, '../node_modules/.bin');
|
|
80
|
+
|
|
81
|
+
const agentDefs = [
|
|
82
|
+
{ name: 'claude', binary: 'claude', label: 'Claude Code' },
|
|
83
|
+
{ name: 'codex', binary: 'codex', label: 'OpenAI Codex' },
|
|
84
|
+
{ name: 'cursor', binary: 'cursor-agent', label: 'Cursor Agent' },
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const detected = {};
|
|
88
|
+
for (const agent of agentDefs) {
|
|
89
|
+
try {
|
|
90
|
+
const result = execSync(`${whichCmd} ${agent.binary}`, { stdio: 'pipe', timeout: 5000 }).toString().trim();
|
|
91
|
+
detected[agent.name] = { installed: true, path: result.split('\n')[0].trim(), label: agent.label };
|
|
92
|
+
} catch {
|
|
93
|
+
const localPath = path.join(localBinDir, agent.binary + (isWindows ? '.cmd' : ''));
|
|
94
|
+
if (fs.existsSync(localPath)) {
|
|
95
|
+
detected[agent.name] = { installed: true, path: localPath, label: agent.label };
|
|
96
|
+
} else {
|
|
97
|
+
detected[agent.name] = { installed: false, label: agent.label };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return detected;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Build execution context for shared agents.
|
|
106
|
+
*/
|
|
107
|
+
function buildAgentContext(requestId, ws) {
|
|
108
|
+
return {
|
|
109
|
+
requestId,
|
|
110
|
+
streamMode: 'simple',
|
|
111
|
+
resolveBinary,
|
|
112
|
+
localBinDir: path.join(__dirname_rc, '../node_modules/.bin'),
|
|
113
|
+
stream: (data) => {
|
|
114
|
+
ws.send(JSON.stringify({ type: 'relay-stream', requestId, data }));
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Handle incoming relay commands from the server.
|
|
121
|
+
* Delegates to shared agent modules.
|
|
122
|
+
*/
|
|
123
|
+
async function handleRelayCommand(data, ws) {
|
|
124
|
+
const { requestId, action } = data;
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const ctx = buildAgentContext(requestId, ws);
|
|
128
|
+
|
|
129
|
+
if (isStreamingAction(action)) {
|
|
130
|
+
logRelayEvent('>', `${action}: ${(data.command || '').slice(0, 60)}...`, 'cyan');
|
|
131
|
+
const result = await executeAction(action, data, ctx);
|
|
132
|
+
ws.send(JSON.stringify({ type: 'relay-complete', requestId, exitCode: result.exitCode }));
|
|
133
|
+
} else {
|
|
134
|
+
if (action === 'shell-command') logRelayEvent('$', `Shell: ${(data.command || '').slice(0, 50)}`, 'dim');
|
|
135
|
+
else if (action === 'file-read') logRelayEvent('R', `Read: ${data.filePath}`, 'dim');
|
|
136
|
+
else if (action === 'file-write') logRelayEvent('W', `Write: ${data.filePath}`, 'dim');
|
|
137
|
+
else if (action === 'git-operation') logRelayEvent('G', `Git: ${data.gitCommand}`, 'dim');
|
|
138
|
+
else if (action === 'exec') logRelayEvent('$', `Exec: ${(data.command || '').slice(0, 50)}`, 'dim');
|
|
139
|
+
|
|
140
|
+
const result = await executeAction(action, data, ctx);
|
|
141
|
+
ws.send(JSON.stringify({ type: 'relay-response', requestId, data: result }));
|
|
142
|
+
}
|
|
143
|
+
} catch (err) {
|
|
144
|
+
ws.send(JSON.stringify({ type: 'relay-response', requestId, error: err.message }));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Create WebSocket connection with optional API key in handshake
|
|
150
|
+
*/
|
|
151
|
+
function createRelayConnection(wsUrl, config = {}) {
|
|
152
|
+
const headers = {};
|
|
153
|
+
if (config.anthropicApiKey) headers['x-anthropic-api-key'] = config.anthropicApiKey;
|
|
154
|
+
headers['x-upfyn-version'] = VERSION;
|
|
155
|
+
headers['x-upfyn-machine'] = os.hostname();
|
|
156
|
+
headers['x-upfyn-platform'] = process.platform;
|
|
157
|
+
headers['x-upfyn-cwd'] = process.cwd();
|
|
158
|
+
return new WebSocket(wsUrl, { headers });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Main connect function (interactive — with animation and logging)
|
|
163
|
+
*/
|
|
164
|
+
export async function connectToServer(options = {}) {
|
|
165
|
+
const config = loadConfig();
|
|
166
|
+
const serverUrl = options.server || config.server || 'https://upfynai-code-production.up.railway.app';
|
|
167
|
+
const relayKey = options.key || config.relayKey;
|
|
168
|
+
|
|
169
|
+
if (!relayKey) {
|
|
170
|
+
console.log('');
|
|
171
|
+
console.log(` ${c.red('FAIL')} No relay key provided.`);
|
|
172
|
+
console.log('');
|
|
173
|
+
console.log(` ${c.gray('Get your relay token from the web UI:')}`);
|
|
174
|
+
console.log(` ${c.dim('1.')} Sign in at ${c.cyan('https://cli.upfyn.com')}`);
|
|
175
|
+
console.log(` ${c.dim('2.')} Click ${c.bright('Connect')} button`);
|
|
176
|
+
console.log(` ${c.dim('3.')} Copy the command and run it here`);
|
|
177
|
+
console.log('');
|
|
178
|
+
console.log(` ${c.gray('Or run:')} ${c.bright('uc connect --server <url> --key upfyn_your_token')}`);
|
|
179
|
+
console.log('');
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
saveConfig({ ...config, server: serverUrl, relayKey });
|
|
184
|
+
|
|
185
|
+
await showConnectStartup(serverUrl, os.hostname(), os.userInfo().username, VERSION);
|
|
186
|
+
|
|
187
|
+
const wsUrl = serverUrl.replace(/^http/, 'ws') + '/relay?token=' + encodeURIComponent(relayKey);
|
|
188
|
+
|
|
189
|
+
const spinner = createSpinner('Connecting to server...');
|
|
190
|
+
spinner.start();
|
|
191
|
+
|
|
192
|
+
let reconnectAttempts = 0;
|
|
193
|
+
const MAX_RECONNECT = 10;
|
|
194
|
+
let lastPongTime = Date.now();
|
|
195
|
+
|
|
196
|
+
function connect() {
|
|
197
|
+
const ws = createRelayConnection(wsUrl, config);
|
|
198
|
+
lastPongTime = Date.now();
|
|
199
|
+
|
|
200
|
+
ws.on('open', () => {
|
|
201
|
+
reconnectAttempts = 0;
|
|
202
|
+
lastPongTime = Date.now();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
ws.on('message', (rawMessage) => {
|
|
206
|
+
try {
|
|
207
|
+
const data = JSON.parse(rawMessage);
|
|
208
|
+
|
|
209
|
+
if (data.type === 'relay-connected') {
|
|
210
|
+
spinner.stop('Connected!');
|
|
211
|
+
const nameMatch = data.message?.match(/Connected as (.+?)\./);
|
|
212
|
+
const username = nameMatch ? nameMatch[1] : 'Unknown';
|
|
213
|
+
showConnectionBanner(username, serverUrl);
|
|
214
|
+
|
|
215
|
+
const agents = detectInstalledAgents();
|
|
216
|
+
const installed = Object.entries(agents)
|
|
217
|
+
.filter(([, info]) => info.installed)
|
|
218
|
+
.map(([name, info]) => info.label);
|
|
219
|
+
const missing = Object.entries(agents)
|
|
220
|
+
.filter(([, info]) => !info.installed)
|
|
221
|
+
.map(([name, info]) => info.label);
|
|
222
|
+
|
|
223
|
+
if (installed.length > 0) logRelayEvent('+', `Agents found: ${installed.join(', ')}`, 'green');
|
|
224
|
+
if (missing.length > 0) logRelayEvent('~', `Not found: ${missing.join(', ')} (install to enable)`, 'yellow');
|
|
225
|
+
|
|
226
|
+
ws.send(JSON.stringify({
|
|
227
|
+
type: 'agent-capabilities',
|
|
228
|
+
agents,
|
|
229
|
+
machine: { hostname: os.hostname(), platform: process.platform, cwd: process.cwd() },
|
|
230
|
+
}));
|
|
231
|
+
|
|
232
|
+
logRelayEvent('*', 'Relay active -- waiting for commands...', 'green');
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (data.type === 'relay-command') {
|
|
237
|
+
handleRelayCommand(data, ws);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (data.type === 'pong' || data.type === 'server-ping') {
|
|
242
|
+
lastPongTime = Date.now();
|
|
243
|
+
if (data.type === 'server-ping') ws.send(JSON.stringify({ type: 'ping' }));
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (data.type === 'error') {
|
|
248
|
+
spinner.fail(`Server error: ${data.error}`);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
} catch (e) {
|
|
252
|
+
logRelayEvent('!', `Parse error: ${e.message}`, 'red');
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
ws.on('close', (code) => {
|
|
257
|
+
if (code === 1000) {
|
|
258
|
+
logRelayEvent('-', 'Disconnected gracefully.', 'dim');
|
|
259
|
+
process.exit(0);
|
|
260
|
+
}
|
|
261
|
+
reconnectAttempts++;
|
|
262
|
+
if (reconnectAttempts <= MAX_RECONNECT) {
|
|
263
|
+
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
|
|
264
|
+
logRelayEvent('~', `Connection lost. Reconnecting in ${delay / 1000}s... (${reconnectAttempts}/${MAX_RECONNECT})`, 'yellow');
|
|
265
|
+
setTimeout(connect, delay);
|
|
266
|
+
} else {
|
|
267
|
+
logRelayEvent('X', 'Max reconnection attempts reached. Exiting.', 'red');
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
ws.on('error', (err) => {
|
|
273
|
+
if (err.code === 'ECONNREFUSED') {
|
|
274
|
+
spinner.fail(`Cannot reach ${serverUrl}. Is the server running?`);
|
|
275
|
+
} else if (err.message?.includes('401')) {
|
|
276
|
+
spinner.fail('Authentication failed (401). Your relay token may be invalid or expired.');
|
|
277
|
+
logRelayEvent('!', 'Get a new token from the dashboard: https://cli.upfyn.com/dashboard', 'yellow');
|
|
278
|
+
logRelayEvent('!', 'Then run: uc connect --server <url> --key <new_token>', 'yellow');
|
|
279
|
+
} else if (err.message?.includes('403') || err.message?.includes('Forbidden')) {
|
|
280
|
+
spinner.fail('Access forbidden (403). Your account may be inactive.');
|
|
281
|
+
} else {
|
|
282
|
+
logRelayEvent('!', `WebSocket error: ${err.message || err.code || 'unknown'}`, 'red');
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const heartbeat = setInterval(() => {
|
|
287
|
+
if (ws.readyState !== 1) { clearInterval(heartbeat); return; }
|
|
288
|
+
if (Date.now() - lastPongTime > 75000) {
|
|
289
|
+
clearInterval(heartbeat);
|
|
290
|
+
logRelayEvent('!', 'No heartbeat response — connection stale, reconnecting...', 'yellow');
|
|
291
|
+
ws.terminate();
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
ws.send(JSON.stringify({ type: 'ping' }));
|
|
295
|
+
}, 30000);
|
|
296
|
+
|
|
297
|
+
ws.on('close', () => clearInterval(heartbeat));
|
|
298
|
+
ws.on('error', () => clearInterval(heartbeat));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
connect();
|
|
302
|
+
|
|
303
|
+
process.on('SIGINT', () => {
|
|
304
|
+
console.log('');
|
|
305
|
+
logRelayEvent('-', 'Disconnecting...', 'dim');
|
|
306
|
+
process.exit(0);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Background connect function (silent — used when uc launches Claude Code)
|
|
312
|
+
*/
|
|
313
|
+
export function connectToServerBackground(options = {}) {
|
|
314
|
+
const config = loadConfig();
|
|
315
|
+
const serverUrl = options.server || config.server;
|
|
316
|
+
const relayKey = options.key || config.relayKey;
|
|
317
|
+
|
|
318
|
+
if (!serverUrl || !relayKey) return;
|
|
319
|
+
|
|
320
|
+
const wsUrl = serverUrl.replace(/^http/, 'ws') + '/relay?token=' + encodeURIComponent(relayKey);
|
|
321
|
+
|
|
322
|
+
let reconnectAttempts = 0;
|
|
323
|
+
const MAX_RECONNECT = 5;
|
|
324
|
+
let lastPongTime = Date.now();
|
|
325
|
+
|
|
326
|
+
function connect() {
|
|
327
|
+
const ws = createRelayConnection(wsUrl, config);
|
|
328
|
+
lastPongTime = Date.now();
|
|
329
|
+
|
|
330
|
+
ws.on('message', (rawMessage) => {
|
|
331
|
+
try {
|
|
332
|
+
const data = JSON.parse(rawMessage);
|
|
333
|
+
if (data.type === 'relay-connected') {
|
|
334
|
+
// Detect installed agents and report to server (same as interactive connect)
|
|
335
|
+
const agents = detectInstalledAgents();
|
|
336
|
+
ws.send(JSON.stringify({
|
|
337
|
+
type: 'agent-capabilities',
|
|
338
|
+
agents,
|
|
339
|
+
machine: { hostname: os.hostname(), platform: process.platform, cwd: process.cwd() },
|
|
340
|
+
}));
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (data.type === 'pong' || data.type === 'server-ping') {
|
|
344
|
+
lastPongTime = Date.now();
|
|
345
|
+
if (data.type === 'server-ping') ws.send(JSON.stringify({ type: 'ping' }));
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (data.type === 'relay-command') {
|
|
349
|
+
handleRelayCommand(data, ws);
|
|
350
|
+
}
|
|
351
|
+
} catch { /* ignore */ }
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
ws.on('open', () => {
|
|
355
|
+
reconnectAttempts = 0;
|
|
356
|
+
lastPongTime = Date.now();
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
ws.on('close', (code) => {
|
|
360
|
+
clearInterval(heartbeat);
|
|
361
|
+
if (code === 1000) return;
|
|
362
|
+
reconnectAttempts++;
|
|
363
|
+
if (reconnectAttempts <= MAX_RECONNECT) {
|
|
364
|
+
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
|
|
365
|
+
setTimeout(connect, delay);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
ws.on('error', () => { clearInterval(heartbeat); });
|
|
370
|
+
|
|
371
|
+
const heartbeat = setInterval(() => {
|
|
372
|
+
if (ws.readyState !== 1) { clearInterval(heartbeat); return; }
|
|
373
|
+
if (Date.now() - lastPongTime > 75000) { clearInterval(heartbeat); ws.terminate(); return; }
|
|
374
|
+
ws.send(JSON.stringify({ type: 'ping' }));
|
|
375
|
+
}, 30000);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
connect();
|
|
379
|
+
}
|