upfynai-code 2.5.1 → 2.6.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/package.json +2 -8
- package/server/cli.js +1 -1
- package/server/database/db.js +16 -2
- package/server/index.js +2738 -2621
- package/server/middleware/auth.js +10 -2
- package/server/relay-client.js +73 -20
- package/server/routes/agent.js +1226 -1266
- package/server/routes/auth.js +32 -29
- package/server/routes/commands.js +598 -601
- package/server/routes/cursor.js +806 -807
- package/server/routes/dashboard.js +154 -1
- package/server/routes/git.js +1151 -1165
- package/server/routes/mcp.js +534 -551
- package/server/routes/settings.js +261 -269
- package/server/routes/taskmaster.js +1927 -1963
- package/server/routes/vapi-chat.js +94 -0
- package/server/routes/voice.js +0 -4
- package/server/sandbox.js +120 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
|
|
3
|
+
const router = express.Router();
|
|
4
|
+
|
|
5
|
+
const VAPI_PRIVATE_KEY = process.env.VAPI_PRIVATE_KEY;
|
|
6
|
+
const VAPI_PUBLIC_KEY = process.env.VAPI_PUBLIC_KEY;
|
|
7
|
+
const VAPI_ASSISTANT_ID = process.env.VAPI_ASSISTANT_ID;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* POST /api/vapi/chat — Proxy to VAPI Chat API
|
|
11
|
+
* Body: { message, chatId? }
|
|
12
|
+
* Returns: { reply, chatId }
|
|
13
|
+
*/
|
|
14
|
+
router.post('/chat', async (req, res) => {
|
|
15
|
+
if (!VAPI_PRIVATE_KEY || !VAPI_ASSISTANT_ID) {
|
|
16
|
+
return res.status(503).json({ error: 'VAPI chat not configured' });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { message, chatId } = req.body;
|
|
20
|
+
if (!message || typeof message !== 'string') {
|
|
21
|
+
return res.status(400).json({ error: 'Message is required' });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const body = {
|
|
26
|
+
assistantId: VAPI_ASSISTANT_ID,
|
|
27
|
+
input: message.slice(0, 2000),
|
|
28
|
+
};
|
|
29
|
+
if (chatId) body.previousChatId = chatId;
|
|
30
|
+
|
|
31
|
+
const response = await fetch('https://api.vapi.ai/chat', {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: {
|
|
34
|
+
'Authorization': `Bearer ${VAPI_PRIVATE_KEY}`,
|
|
35
|
+
'Content-Type': 'application/json',
|
|
36
|
+
},
|
|
37
|
+
body: JSON.stringify(body),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
const err = await response.text().catch(() => '');
|
|
42
|
+
return res.status(response.status).json({ error: 'VAPI chat request failed' });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const data = await response.json();
|
|
46
|
+
|
|
47
|
+
// Extract the assistant's reply from the output array
|
|
48
|
+
const assistantMsg = data.output?.find(o => o.role === 'assistant');
|
|
49
|
+
const reply = assistantMsg?.content || data.output?.[0]?.content || 'No response';
|
|
50
|
+
|
|
51
|
+
res.json({
|
|
52
|
+
reply,
|
|
53
|
+
chatId: data.chat?.id || chatId || null,
|
|
54
|
+
});
|
|
55
|
+
} catch (error) {
|
|
56
|
+
res.status(500).json({ error: 'Chat request failed' });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* POST /api/vapi/call — Create a VAPI web call server-side
|
|
62
|
+
* Returns: { webCallUrl, callId }
|
|
63
|
+
*/
|
|
64
|
+
router.post('/call', async (req, res) => {
|
|
65
|
+
if (!VAPI_PUBLIC_KEY || !VAPI_ASSISTANT_ID) {
|
|
66
|
+
return res.status(503).json({ error: 'VAPI voice not configured' });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const response = await fetch('https://api.vapi.ai/call/web', {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: {
|
|
73
|
+
'Authorization': `Bearer ${VAPI_PUBLIC_KEY}`,
|
|
74
|
+
'Content-Type': 'application/json',
|
|
75
|
+
},
|
|
76
|
+
body: JSON.stringify({ assistantId: VAPI_ASSISTANT_ID }),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
const err = await response.text().catch(() => '');
|
|
81
|
+
return res.status(response.status).json({ error: 'Failed to create call', details: err });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const data = await response.json();
|
|
85
|
+
res.json({
|
|
86
|
+
webCallUrl: data.webCallUrl || data.transport?.callUrl,
|
|
87
|
+
callId: data.id,
|
|
88
|
+
});
|
|
89
|
+
} catch (error) {
|
|
90
|
+
res.status(500).json({ error: 'Call creation failed' });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
export default router;
|
package/server/routes/voice.js
CHANGED
|
@@ -75,7 +75,6 @@ router.post('/stt', upload.single('audio'), async (req, res) => {
|
|
|
75
75
|
return res.json({ text, source: 'openai' });
|
|
76
76
|
} catch (err) {
|
|
77
77
|
// If cloud fails, try local fallback
|
|
78
|
-
console.warn('[Voice STT] OpenAI API failed, trying local:', err.message);
|
|
79
78
|
}
|
|
80
79
|
}
|
|
81
80
|
|
|
@@ -95,7 +94,6 @@ router.post('/stt', upload.single('audio'), async (req, res) => {
|
|
|
95
94
|
|
|
96
95
|
return res.status(500).json({ error: 'No STT engine available. Add your OpenAI key in Settings > AI Providers, or install nodejs-whisper + ffmpeg for local transcription.' });
|
|
97
96
|
} catch (error) {
|
|
98
|
-
console.error('[Voice STT] Error:', error.message);
|
|
99
97
|
res.status(500).json({ error: 'Transcription failed' });
|
|
100
98
|
}
|
|
101
99
|
});
|
|
@@ -123,7 +121,6 @@ router.post('/tts', async (req, res) => {
|
|
|
123
121
|
res.setHeader('Content-Length', audioBuffer.length);
|
|
124
122
|
res.send(audioBuffer);
|
|
125
123
|
} catch (error) {
|
|
126
|
-
console.error('[Voice TTS] Error:', error.message);
|
|
127
124
|
res.status(500).json({ error: 'TTS synthesis failed' });
|
|
128
125
|
}
|
|
129
126
|
});
|
|
@@ -148,7 +145,6 @@ router.get('/voices', async (req, res) => {
|
|
|
148
145
|
|
|
149
146
|
res.json({ voices: englishVoices });
|
|
150
147
|
} catch (error) {
|
|
151
|
-
console.error('[Voice Voices] Error:', error.message);
|
|
152
148
|
res.status(500).json({ error: 'Failed to fetch voices' });
|
|
153
149
|
}
|
|
154
150
|
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox Client — connects the backend to the separate sandbox-service on Railway.
|
|
3
|
+
* All sandbox operations are proxied to the sandbox service via HTTP.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const SANDBOX_SERVICE_URL = process.env.SANDBOX_SERVICE_URL || 'http://localhost:4300';
|
|
7
|
+
const SANDBOX_SERVICE_SECRET = process.env.SANDBOX_SERVICE_SECRET || 'dev-sandbox-secret';
|
|
8
|
+
|
|
9
|
+
async function sandboxFetch(path, userId, body = null) {
|
|
10
|
+
const opts = {
|
|
11
|
+
method: body ? 'POST' : 'GET',
|
|
12
|
+
headers: {
|
|
13
|
+
'Content-Type': 'application/json',
|
|
14
|
+
'x-sandbox-secret': SANDBOX_SERVICE_SECRET,
|
|
15
|
+
'x-user-id': String(userId),
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
if (body) opts.body = JSON.stringify(body);
|
|
19
|
+
|
|
20
|
+
const res = await fetch(`${SANDBOX_SERVICE_URL}${path}`, opts);
|
|
21
|
+
const data = await res.json();
|
|
22
|
+
if (!res.ok) throw new Error(data.error || `Sandbox service error: ${res.status}`);
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const sandboxClient = {
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if the sandbox service is reachable.
|
|
30
|
+
*/
|
|
31
|
+
async isAvailable() {
|
|
32
|
+
try {
|
|
33
|
+
const res = await fetch(`${SANDBOX_SERVICE_URL}/health`, { signal: AbortSignal.timeout(3000) });
|
|
34
|
+
return res.ok;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Initialize a user's sandbox (creates if doesn't exist).
|
|
42
|
+
*/
|
|
43
|
+
async initSandbox(userId) {
|
|
44
|
+
return sandboxFetch('/api/sandbox/init', userId, {});
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get sandbox status.
|
|
49
|
+
*/
|
|
50
|
+
async getStatus(userId) {
|
|
51
|
+
return sandboxFetch('/api/sandbox/status', userId);
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Destroy a user's sandbox.
|
|
56
|
+
*/
|
|
57
|
+
async destroySandbox(userId) {
|
|
58
|
+
const res = await fetch(`${SANDBOX_SERVICE_URL}/api/sandbox`, {
|
|
59
|
+
method: 'DELETE',
|
|
60
|
+
headers: {
|
|
61
|
+
'Content-Type': 'application/json',
|
|
62
|
+
'x-sandbox-secret': SANDBOX_SERVICE_SECRET,
|
|
63
|
+
'x-user-id': String(userId),
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
const data = await res.json();
|
|
67
|
+
if (!res.ok) throw new Error(data.error || 'Failed to destroy sandbox');
|
|
68
|
+
return data;
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Execute a command in the user's sandbox.
|
|
73
|
+
*/
|
|
74
|
+
async exec(userId, command, opts = {}) {
|
|
75
|
+
return sandboxFetch('/api/exec', userId, {
|
|
76
|
+
command,
|
|
77
|
+
cwd: opts.cwd,
|
|
78
|
+
timeout: opts.timeout,
|
|
79
|
+
userKeys: opts.userKeys,
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Read a file from the user's sandbox.
|
|
85
|
+
*/
|
|
86
|
+
async readFile(userId, filePath) {
|
|
87
|
+
return sandboxFetch('/api/file/read', userId, { filePath });
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Write a file to the user's sandbox.
|
|
92
|
+
*/
|
|
93
|
+
async writeFile(userId, filePath, content) {
|
|
94
|
+
return sandboxFetch('/api/file/write', userId, { filePath, content });
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get file tree from the user's sandbox.
|
|
99
|
+
*/
|
|
100
|
+
async getFileTree(userId, dirPath, depth = 3) {
|
|
101
|
+
return sandboxFetch('/api/file/tree', userId, { dirPath, depth });
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Run a git command in the user's sandbox.
|
|
106
|
+
*/
|
|
107
|
+
async gitOperation(userId, gitCommand, cwd) {
|
|
108
|
+
return sandboxFetch('/api/git', userId, { gitCommand, cwd });
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get the WebSocket URL for an interactive shell session.
|
|
113
|
+
*/
|
|
114
|
+
getShellWsUrl(userId, sessionId) {
|
|
115
|
+
const wsBase = SANDBOX_SERVICE_URL.replace(/^http/, 'ws');
|
|
116
|
+
return `${wsBase}/shell?secret=${encodeURIComponent(SANDBOX_SERVICE_SECRET)}&userId=${userId}&sessionId=${sessionId || 'default'}`;
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export { sandboxClient, SANDBOX_SERVICE_URL };
|