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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. 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.0",
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
- if (isPlainShell && data.initialCommand) {
35
- // Run a specific command
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
- shellCmd = 'powershell.exe';
38
- shellArgs = ['-Command', `Set-Location -Path "${projectPath}"; ${data.initialCommand}`];
39
- } else {
40
- shellCmd = 'bash';
41
- shellArgs = ['-c', `cd "${projectPath}" && ${data.initialCommand}`];
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
- } else if (isPlainShell) {
44
- // Interactive shell
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
- shellCmd = 'powershell.exe';
47
- shellArgs = ['-NoExit', '-Command', `Set-Location -Path "${projectPath}"`];
48
- } else {
49
- shellCmd = process.env.SHELL || 'bash';
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
- if (isWin) {
54
- shellCmd = 'powershell.exe';
55
- const cursorCmd = data.hasSession && data.sessionId
56
- ? `cursor-agent --resume="${data.sessionId}"`
57
- : 'cursor-agent';
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
- shellCmd = 'powershell.exe';
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
- shellCmd = 'bash';
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, 100)) {
187
- if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
188
- const item = { name: entry.name, type: entry.isDirectory() ? 'directory' : 'file' };
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(path.join(dirPath, entry.name), maxDepth, currentDepth + 1);
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
- const { filePath } = data;
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
- ws.send(JSON.stringify({ type: 'relay-response', requestId, data: { content } }));
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
- const { filePath: fp, content: fileContent } = data;
342
- if (!fp || typeof fp !== 'string') throw new Error('Invalid file path');
343
- const normalizedFp = path.resolve(fp);
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 = 3 } = data;
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, depth);
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',