upfynai-code 2.7.0 → 2.7.1
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 +1 -1
- package/src/connect.js +109 -46
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "upfynai-code",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.1",
|
|
4
4
|
"description": "Unified AI coding interface — access AI chat, terminal, file explorer, git, and visual canvas from any browser. Connect your local machine and code from anywhere.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/connect.js
CHANGED
|
@@ -23,6 +23,8 @@ function handleShellSessionStart(data, ws) {
|
|
|
23
23
|
const shellSessionId = data.requestId;
|
|
24
24
|
const projectPath = data.projectPath || process.cwd();
|
|
25
25
|
const isWin = process.platform === 'win32';
|
|
26
|
+
const isMac = process.platform === 'darwin';
|
|
27
|
+
const shellType = data.shellType; // 'cmd', 'powershell', 'bash', 'zsh', etc.
|
|
26
28
|
|
|
27
29
|
console.log(chalk.cyan(` [relay] Starting shell session in ${projectPath}`));
|
|
28
30
|
|
|
@@ -31,56 +33,76 @@ function handleShellSessionStart(data, ws) {
|
|
|
31
33
|
const provider = data.provider || 'claude';
|
|
32
34
|
const isPlainShell = data.isPlainShell || (!!data.initialCommand && !data.hasSession) || provider === 'plain-shell';
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Helper: get the right shell command + args for interactive shell based on platform + shellType.
|
|
38
|
+
* Returns { cmd, args } that cd into projectPath.
|
|
39
|
+
*/
|
|
40
|
+
function getInteractiveShell(projectDir) {
|
|
36
41
|
if (isWin) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
if (shellType === 'cmd') {
|
|
43
|
+
return { cmd: 'cmd.exe', args: ['/K', `cd /d "${projectDir}"`] };
|
|
44
|
+
}
|
|
45
|
+
// Default to PowerShell on Windows
|
|
46
|
+
return { cmd: 'powershell.exe', args: ['-NoExit', '-Command', `Set-Location -LiteralPath '${projectDir.replace(/'/g, "''")}'`] };
|
|
42
47
|
}
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
// Unix: use shellType if specified, else $SHELL or bash
|
|
49
|
+
const sh = shellType || process.env.SHELL || 'bash';
|
|
50
|
+
return { cmd: sh, args: ['--login'] };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Helper: wrap a command to execute in the right shell for the platform + shellType.
|
|
55
|
+
* Returns { cmd, args }.
|
|
56
|
+
*/
|
|
57
|
+
function wrapCommand(command, projectDir) {
|
|
45
58
|
if (isWin) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
shellArgs = ['--login'];
|
|
59
|
+
if (shellType === 'cmd') {
|
|
60
|
+
return { cmd: 'cmd.exe', args: ['/C', `cd /d "${projectDir}" && ${command}`] };
|
|
61
|
+
}
|
|
62
|
+
return { cmd: 'powershell.exe', args: ['-Command', `Set-Location -LiteralPath '${projectDir.replace(/'/g, "''")}'; ${command}`] };
|
|
51
63
|
}
|
|
64
|
+
const sh = shellType || 'bash';
|
|
65
|
+
return { cmd: sh, args: ['-c', `cd "${projectDir}" && ${command}`] };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (isPlainShell && data.initialCommand) {
|
|
69
|
+
// Run a specific command
|
|
70
|
+
const s = wrapCommand(data.initialCommand, projectPath);
|
|
71
|
+
shellCmd = s.cmd; shellArgs = s.args;
|
|
72
|
+
} else if (isPlainShell) {
|
|
73
|
+
// Interactive shell
|
|
74
|
+
const s = getInteractiveShell(projectPath);
|
|
75
|
+
shellCmd = s.cmd; shellArgs = s.args;
|
|
52
76
|
} else if (provider === 'cursor') {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
shellArgs = ['-Command', `Set-Location -Path "${projectPath}"; ${cursorCmd}`];
|
|
59
|
-
} else {
|
|
60
|
-
shellCmd = 'bash';
|
|
61
|
-
const cursorCmd = data.hasSession && data.sessionId
|
|
62
|
-
? `cursor-agent --resume="${data.sessionId}"`
|
|
63
|
-
: 'cursor-agent';
|
|
64
|
-
shellArgs = ['-c', `cd "${projectPath}" && ${cursorCmd}`];
|
|
65
|
-
}
|
|
77
|
+
const cursorCmd = data.hasSession && data.sessionId
|
|
78
|
+
? `cursor-agent --resume="${data.sessionId}"`
|
|
79
|
+
: 'cursor-agent';
|
|
80
|
+
const s = wrapCommand(cursorCmd, projectPath);
|
|
81
|
+
shellCmd = s.cmd; shellArgs = s.args;
|
|
66
82
|
} else {
|
|
67
83
|
// Claude (default)
|
|
68
84
|
const command = data.initialCommand || 'claude';
|
|
85
|
+
let claudeCmd;
|
|
69
86
|
if (isWin) {
|
|
70
|
-
|
|
71
|
-
const claudeCmd = data.hasSession && data.sessionId
|
|
87
|
+
claudeCmd = data.hasSession && data.sessionId
|
|
72
88
|
? `claude --resume ${data.sessionId}`
|
|
73
89
|
: command;
|
|
74
|
-
shellArgs = ['-Command', `Set-Location -Path "${projectPath}"; ${claudeCmd}`];
|
|
75
90
|
} else {
|
|
76
|
-
|
|
77
|
-
const claudeCmd = data.hasSession && data.sessionId
|
|
91
|
+
claudeCmd = data.hasSession && data.sessionId
|
|
78
92
|
? `claude --resume ${data.sessionId} || claude`
|
|
79
93
|
: command;
|
|
80
|
-
shellArgs = ['-c', `cd "${projectPath}" && ${claudeCmd}`];
|
|
81
94
|
}
|
|
95
|
+
const s = wrapCommand(claudeCmd, projectPath);
|
|
96
|
+
shellCmd = s.cmd; shellArgs = s.args;
|
|
82
97
|
}
|
|
83
98
|
|
|
99
|
+
// Send ack so browser knows spawn is starting
|
|
100
|
+
ws.send(JSON.stringify({
|
|
101
|
+
type: 'relay-shell-output',
|
|
102
|
+
shellSessionId,
|
|
103
|
+
data: '',
|
|
104
|
+
}));
|
|
105
|
+
|
|
84
106
|
const proc = spawn(shellCmd, shellArgs, {
|
|
85
107
|
cwd: isPlainShell && !data.initialCommand ? projectPath : undefined,
|
|
86
108
|
env: {
|
|
@@ -183,11 +205,25 @@ async function buildFileTree(dirPath, maxDepth, currentDepth = 0) {
|
|
|
183
205
|
try {
|
|
184
206
|
const entries = await fsPromises.readdir(dirPath, { withFileTypes: true });
|
|
185
207
|
const items = [];
|
|
186
|
-
for (const entry of entries.slice(0,
|
|
187
|
-
|
|
188
|
-
|
|
208
|
+
for (const entry of entries.slice(0, 200)) {
|
|
209
|
+
// Skip heavy/hidden directories
|
|
210
|
+
if (entry.name === 'node_modules' || entry.name === '.git' ||
|
|
211
|
+
entry.name === 'dist' || entry.name === 'build' ||
|
|
212
|
+
entry.name === '.svn' || entry.name === '.hg') continue;
|
|
213
|
+
if (entry.name.startsWith('.') && currentDepth === 0) continue; // hide dotfiles at root only
|
|
214
|
+
|
|
215
|
+
const itemPath = path.join(dirPath, entry.name);
|
|
216
|
+
const item = { name: entry.name, path: itemPath, type: entry.isDirectory() ? 'directory' : 'file' };
|
|
217
|
+
|
|
218
|
+
// Get file stats for metadata (size, modified)
|
|
219
|
+
try {
|
|
220
|
+
const stats = await fsPromises.stat(itemPath);
|
|
221
|
+
item.size = stats.size;
|
|
222
|
+
item.modified = stats.mtime.toISOString();
|
|
223
|
+
} catch { /* ignore stat errors */ }
|
|
224
|
+
|
|
189
225
|
if (entry.isDirectory() && currentDepth < maxDepth - 1) {
|
|
190
|
-
item.children = await buildFileTree(
|
|
226
|
+
item.children = await buildFileTree(itemPath, maxDepth, currentDepth + 1);
|
|
191
227
|
}
|
|
192
228
|
items.push(item);
|
|
193
229
|
}
|
|
@@ -326,21 +362,29 @@ async function handleRelayCommand(data, ws) {
|
|
|
326
362
|
}
|
|
327
363
|
|
|
328
364
|
case 'file-read': {
|
|
329
|
-
|
|
365
|
+
let { filePath, encoding } = data;
|
|
330
366
|
if (!filePath || typeof filePath !== 'string') throw new Error('Invalid file path');
|
|
367
|
+
// Resolve ~ to home directory (routes send paths like ~/.cursor/config.json)
|
|
368
|
+
if (filePath === '~') filePath = os.homedir();
|
|
369
|
+
else if (filePath.startsWith('~/') || filePath.startsWith('~\\')) filePath = path.join(os.homedir(), filePath.slice(2));
|
|
331
370
|
const normalizedPath = path.resolve(filePath);
|
|
332
371
|
const normLower = normalizedPath.toLowerCase().replace(/\\/g, '/');
|
|
333
372
|
const blockedRead = ['/etc/shadow', '/etc/passwd', '.ssh/id_rsa', '.ssh/id_ed25519', '/.env'];
|
|
334
373
|
if (blockedRead.some(b => normLower.includes(b))) throw new Error('Access denied');
|
|
335
|
-
const content = await fsPromises.readFile(normalizedPath, 'utf8');
|
|
336
|
-
|
|
374
|
+
const content = await fsPromises.readFile(normalizedPath, encoding === 'base64' ? null : 'utf8');
|
|
375
|
+
const result = encoding === 'base64' ? content.toString('base64') : content;
|
|
376
|
+
ws.send(JSON.stringify({ type: 'relay-response', requestId, data: { content: result } }));
|
|
337
377
|
break;
|
|
338
378
|
}
|
|
339
379
|
|
|
340
380
|
case 'file-write': {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
381
|
+
let fp2 = data.filePath;
|
|
382
|
+
const fileContent = data.content;
|
|
383
|
+
if (!fp2 || typeof fp2 !== 'string') throw new Error('Invalid file path');
|
|
384
|
+
// Resolve ~ to home directory
|
|
385
|
+
if (fp2 === '~') fp2 = os.homedir();
|
|
386
|
+
else if (fp2.startsWith('~/') || fp2.startsWith('~\\')) fp2 = path.join(os.homedir(), fp2.slice(2));
|
|
387
|
+
const normalizedFp = path.resolve(fp2);
|
|
344
388
|
const fpLower = normalizedFp.toLowerCase().replace(/\\/g, '/');
|
|
345
389
|
const blockedWrite = [
|
|
346
390
|
'/etc/', '/usr/bin/', '/usr/sbin/',
|
|
@@ -356,10 +400,11 @@ async function handleRelayCommand(data, ws) {
|
|
|
356
400
|
}
|
|
357
401
|
|
|
358
402
|
case 'file-tree': {
|
|
359
|
-
const { dirPath, depth
|
|
403
|
+
const { dirPath, depth, maxDepth } = data;
|
|
404
|
+
const treeDepth = depth || maxDepth || 3;
|
|
360
405
|
const resolvedDir = dirPath ? path.resolve(dirPath) : process.cwd();
|
|
361
|
-
const tree = await buildFileTree(resolvedDir,
|
|
362
|
-
ws.send(JSON.stringify({ type: 'relay-response', requestId, data: { tree } }));
|
|
406
|
+
const tree = await buildFileTree(resolvedDir, treeDepth);
|
|
407
|
+
ws.send(JSON.stringify({ type: 'relay-response', requestId, data: { files: tree } }));
|
|
363
408
|
break;
|
|
364
409
|
}
|
|
365
410
|
|
|
@@ -498,6 +543,24 @@ async function handleRelayCommand(data, ws) {
|
|
|
498
543
|
break;
|
|
499
544
|
}
|
|
500
545
|
|
|
546
|
+
case 'detect-agents': {
|
|
547
|
+
// Detect installed AI CLI agents on user's machine
|
|
548
|
+
const agents = {};
|
|
549
|
+
const checkCmd = process.platform === 'win32' ? 'where' : 'which';
|
|
550
|
+
const agentBins = { claude: 'claude', cursor: 'cursor-agent', codex: 'codex' };
|
|
551
|
+
for (const [name, bin] of Object.entries(agentBins)) {
|
|
552
|
+
try {
|
|
553
|
+
const { execSync } = await import('child_process');
|
|
554
|
+
execSync(`${checkCmd} ${bin}`, { encoding: 'utf8', timeout: 5000, stdio: 'pipe' });
|
|
555
|
+
agents[name] = true;
|
|
556
|
+
} catch {
|
|
557
|
+
agents[name] = false;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
ws.send(JSON.stringify({ type: 'relay-response', requestId, data: { agents } }));
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
|
|
501
564
|
default:
|
|
502
565
|
ws.send(JSON.stringify({
|
|
503
566
|
type: 'relay-response',
|