@miraj181/ipingyou 2.1.19 → 2.1.22

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/src/modes/ai.js CHANGED
@@ -8,16 +8,16 @@ import inquirer from 'inquirer';
8
8
  import fs from 'node:fs';
9
9
  import os from 'node:os';
10
10
  import path from 'node:path';
11
- import { getAlias } from '../lib/config.js';
12
- import { resolveUID } from '../lib/broker.js';
13
- import { buildSshArgs, extractHostname, quoteRemoteShell } from '../lib/ssh.js';
14
- import { addCleanupHook, cleanupAll } from '../lib/cleanup.js';
11
+ import { getAlias } from '../lib/mod/config.js';
12
+ import { resolveUID } from '../lib/client/broker.js';
13
+ import { buildSshArgs, extractHostname, quoteRemoteShell } from '../lib/services/ssh.js';
14
+ import { addCleanupHook, cleanupAll, trackPID, untrackPID } from '../lib/mod/cleanup.js';
15
15
  import { startHostMode } from './host.js';
16
16
  import { startClientMode } from './client.js';
17
17
  import { performSCPNonInteractive } from './client.js';
18
18
  import { DEFAULT_AI_MODEL, createGroqChatCompletion, getGroqApiKey, getRateLimitWarnings, listGroqModels, estimateTokensForMessages } from '../lib/ai/groq.js';
19
19
  import { assertSafeReadablePath, classifyCommand, redactSensitive, sanitizeUserTask, truncateForModel } from '../lib/ai/safety.js';
20
- import { recordEvent } from '../lib/session-log.js';
20
+ import { recordEvent } from '../lib/mod/session-log.js';
21
21
 
22
22
  let BROKER_URL = process.env.BROKER_URL || 'https://ipingyou.onrender.com';
23
23
 
@@ -223,11 +223,14 @@ async function runLocalCommand(command) {
223
223
  if (args.length === 0) {
224
224
  return { exitCode: 1, stdout: '', stderr: 'Empty or unsafe command after parsing' };
225
225
  }
226
- const result = await execa(args[0], args.slice(1), {
226
+ const child = execa(args[0], args.slice(1), {
227
227
  reject: false,
228
228
  timeout: 30000,
229
229
  maxBuffer: 1024 * 1024,
230
230
  });
231
+ trackPID(child.pid);
232
+ const result = await child;
233
+ untrackPID(child.pid);
231
234
 
232
235
  return {
233
236
  exitCode: result.exitCode,
@@ -290,11 +293,14 @@ export function parseLocalCommand(command) {
290
293
  async function runRemoteCommand(context, command) {
291
294
  const sshArgs = buildSshArgs(context.hostname, context.privateKeyPath);
292
295
  sshArgs.push(`${context.username}@${context.hostname}`, command);
293
- const result = await execa('ssh', sshArgs, {
296
+ const child = execa('ssh', sshArgs, {
294
297
  reject: false,
295
298
  timeout: 30000,
296
299
  maxBuffer: 1024 * 1024,
297
300
  });
301
+ trackPID(child.pid);
302
+ const result = await child;
303
+ untrackPID(child.pid);
298
304
 
299
305
  return {
300
306
  exitCode: result.exitCode,
@@ -650,7 +656,7 @@ async function tryAITransfer(task, context) {
650
656
  if (context && context.scope === 'remote' && context.hostname && context.username) {
651
657
  console.log(chalk.dim(` Using active remote session: ${context.username}@${context.hostname}`));
652
658
 
653
- const { buildProxyCommandOption, getSshControlOptions, formatScpRemotePath } = await import('../lib/ssh.js');
659
+ const { buildProxyCommandOption, getSshControlOptions, formatScpRemotePath } = await import('../lib/services/ssh.js');
654
660
  const scpArgs = [
655
661
  '-r',
656
662
  ...buildProxyCommandOption(context.hostname),
@@ -670,7 +676,10 @@ async function tryAITransfer(task, context) {
670
676
  }
671
677
 
672
678
  try {
673
- const result = await execa('scp', scpArgs, { stdio: 'inherit', reject: false });
679
+ const child = execa('scp', scpArgs, { stdio: 'inherit', reject: false });
680
+ trackPID(child.pid);
681
+ const result = await child;
682
+ untrackPID(child.pid);
674
683
  if (result.exitCode === 0) {
675
684
  console.log(chalk.green(' ✅ Transfer completed via active remote session.'));
676
685
  recordEvent('ai_transfer_success', { direction, localPath, remotePath, hostname: context.hostname, reusedContext: true });
@@ -18,20 +18,64 @@ import inquirer from 'inquirer';
18
18
  import fs from 'node:fs';
19
19
  import path from 'node:path';
20
20
  import os from 'node:os';
21
- import { cleanupAll, trackPID, untrackPID, addCleanupHook } from '../lib/cleanup.js';
22
- import { createSpinner, sshSpinner, networkSpinner, fileTransferSpinner, showConnectionTrace, simulateTransferProgress } from '../lib/animations.js';
23
- import { getConfig, saveAlias } from '../lib/config.js';
24
- import { pushTelemetry, requestHostApproval, resolveUID, revokeUID, waitForApproval } from '../lib/broker.js';
25
- import { calculateChecksum } from '../lib/checksum.js';
26
- import { promptLocalPath, promptRemotePath } from '../lib/path-browser.js';
27
- import { buildProxyCommandOption, buildSshArgs, extractHostname, formatScpRemotePath, getKnownHostsOptions, getSshControlOptions, quoteRemoteShell } from '../lib/ssh.js';
28
- import { buildTmuxSessionName, TMUX_SOCKET_PATH } from '../lib/tmux.js';
29
- import { openUrl } from '../lib/open-url.js';
30
- import { secureSensitiveUrl } from '../lib/secure-print.js';
31
- import { cleanupSessionLog, initSessionLog, logSessionEvent, recordEvent } from '../lib/session-log.js';
21
+ import { cleanupAll, trackPID, untrackPID, addCleanupHook } from '../lib/mod/cleanup.js';
22
+ import { createSpinner, sshSpinner, networkSpinner, fileTransferSpinner, showConnectionTrace, simulateTransferProgress } from '../lib/mod/animations.js';
23
+ import { getConfig, saveAlias } from '../lib/mod/config.js';
24
+ import { pushTelemetry, requestHostApproval, resolveUID, revokeUID, waitForApproval } from '../lib/client/broker.js';
25
+ import { calculateChecksum } from '../lib/mod/checksum.js';
26
+ import { promptLocalPath, promptRemotePath } from '../lib/client/path-browser.js';
27
+ import { buildProxyCommandOption, buildSshArgs, extractHostname, formatScpRemotePath, getKnownHostsOptions, getSshControlOptions, quoteRemoteShell } from '../lib/services/ssh.js';
28
+ import { openUrl } from '../lib/mod/open-url.js';
29
+ import { secureSensitiveUrl } from '../lib/mod/secure-print.js';
30
+ import { cleanupSessionLog, initSessionLog, logSessionEvent, recordEvent } from '../lib/mod/session-log.js';
32
31
 
33
32
  let BROKER_URL = process.env.BROKER_URL || 'https://ipingyou.onrender.com';
34
33
 
34
+ function startLiveLogSync(username, hostname, privateKeyPath, remoteDropPath, localLogPath, persistKnownHosts = true) {
35
+ if (!remoteDropPath || !localLogPath) return;
36
+
37
+ let lastSize = -1;
38
+ let lastMtime = 0;
39
+ let isSyncing = false;
40
+ const interval = setInterval(async () => {
41
+ if (isSyncing) return;
42
+ isSyncing = true;
43
+ try {
44
+ if (!fs.existsSync(localLogPath)) return;
45
+ const stats = fs.statSync(localLogPath);
46
+ if (stats.size === lastSize && stats.mtimeMs === lastMtime) return;
47
+
48
+ const scpArgs = [
49
+ '-O',
50
+ ...buildProxyCommandOption(hostname),
51
+ ...getKnownHostsOptions(persistKnownHosts),
52
+ '-o', 'IdentitiesOnly=yes',
53
+ ...getSshControlOptions(hostname)
54
+ ];
55
+ if (privateKeyPath) {
56
+ scpArgs.push('-i', privateKeyPath, '-o', 'IdentityAgent=none');
57
+ }
58
+
59
+ const clientName = `${os.userInfo().username}-${os.hostname()}`;
60
+ const remoteFilePath = `${remoteDropPath}/client-${clientName}.log`;
61
+
62
+ scpArgs.push(localLogPath, `${username}@${hostname}:${formatScpRemotePath(remoteFilePath)}`);
63
+
64
+ const result = await execa('scp', scpArgs, { reject: false });
65
+ if (result.exitCode === 0) {
66
+ lastSize = stats.size;
67
+ lastMtime = stats.mtimeMs;
68
+ }
69
+ } catch {
70
+ // Ignore background sync failures silently
71
+ } finally {
72
+ isSyncing = false;
73
+ }
74
+ }, 3000);
75
+
76
+ addCleanupHook(() => clearInterval(interval));
77
+ }
78
+
35
79
  async function promptUsername() {
36
80
  const { username } = await inquirer.prompt([
37
81
  {
@@ -94,18 +138,11 @@ async function connectSSH(username, hostname, privateKeyPath, persistKnownHosts
94
138
 
95
139
  const sshArgs = buildSshArgs(hostname, privateKeyPath, [
96
140
  '-o', 'ServerAliveInterval=30',
97
- '-o', 'ServerAliveCountMax=3'
141
+ '-o', 'ServerAliveCountMax=3',
142
+ '-t'
98
143
  ], { persistKnownHosts });
99
144
 
100
145
  sshArgs.push(`${username}@${hostname}`);
101
- const tmuxSession = buildTmuxSessionName(username);
102
- const quotedSocket = quoteRemoteShell(TMUX_SOCKET_PATH);
103
- const quotedSession = quoteRemoteShell(tmuxSession);
104
- const tmuxSocketCmdStr = `tmux -S ${quotedSocket}`;
105
- const tmuxPrepare = `${tmuxSocketCmdStr} has-session -t ${quotedSession} 2>/dev/null || ${tmuxSocketCmdStr} new-session -d -s ${quotedSession}`;
106
- const tmuxAttach = `${tmuxSocketCmdStr} attach -t ${quotedSession}`;
107
- const tmuxCommand = `if command -v tmux >/dev/null 2>&1; then (${tmuxPrepare} && ${tmuxAttach}) || exec $SHELL -l; else exec $SHELL -l; fi`;
108
- sshArgs.push('-t', tmuxCommand);
109
146
 
110
147
  const child = execa('ssh', sshArgs, {
111
148
  stdio: 'inherit',
@@ -199,6 +236,7 @@ async function performSCP(username, hostname, direction, privateKeyPath, sharedD
199
236
  // Construct SCP args
200
237
  const scpArgs = [
201
238
  '-r', // recursive just in case
239
+ '-O', // Force legacy SCP protocol so that shell quoting in formatScpRemotePath works correctly
202
240
  ...buildProxyCommandOption(hostname),
203
241
  ...getKnownHostsOptions(persistKnownHosts),
204
242
  '-o', 'IdentitiesOnly=yes',
@@ -293,6 +331,7 @@ async function downloadSpecificRemotePath(username, hostname, privateKeyPath, re
293
331
  await showConnectionTrace('Local', 'Remote SCP');
294
332
  const scpArgs = [
295
333
  '-r',
334
+ '-O', // Force legacy SCP protocol
296
335
  ...buildProxyCommandOption(hostname),
297
336
  ...getKnownHostsOptions(persistKnownHosts),
298
337
  '-o', 'IdentitiesOnly=yes',
@@ -300,7 +339,10 @@ async function downloadSpecificRemotePath(username, hostname, privateKeyPath, re
300
339
  ];
301
340
  if (privateKeyPath) scpArgs.push('-i', privateKeyPath, '-o', 'IdentityAgent=none');
302
341
  scpArgs.push(`${username}@${hostname}:${formatScpRemotePath(remotePath)}`, localPath);
303
- const result = await execa('scp', scpArgs, { stdio: 'inherit', reject: false });
342
+ const child = execa('scp', scpArgs, { stdio: 'inherit', reject: false });
343
+ trackPID(child.pid);
344
+ const result = await child;
345
+ untrackPID(child.pid);
304
346
  return result.exitCode === 0;
305
347
  }
306
348
 
@@ -435,12 +477,27 @@ export async function startClientMode(options = {}) {
435
477
  console.log(chalk.dim(' The host has enabled approval gating. Submitting your access request...'));
436
478
 
437
479
  try {
480
+ let localIp = '127.0.0.1';
481
+ try {
482
+ const interfaces = os.networkInterfaces();
483
+ for (const devName in interfaces) {
484
+ const iface = interfaces[devName];
485
+ for (const alias of iface) {
486
+ if (alias.family === 'IPv4' && !alias.internal) {
487
+ localIp = alias.address;
488
+ break;
489
+ }
490
+ }
491
+ }
492
+ } catch {}
493
+
438
494
  const approvalDetails = {
439
495
  username: os.userInfo().username,
440
496
  hostname: os.hostname(),
441
497
  os: `${os.type()} ${os.release()} (${os.arch()})`,
442
498
  intent: 'connect',
443
499
  time: new Date().toISOString(),
500
+ localIp,
444
501
  };
445
502
 
446
503
  const { requestId, status: reqStatus, approvalRequired } = await requestHostApproval(
@@ -457,9 +514,9 @@ export async function startClientMode(options = {}) {
457
514
  console.log(chalk.dim(' This may take a few minutes. Press Ctrl+C to cancel.'));
458
515
  console.log('');
459
516
 
460
- const approved = await waitForApproval(BROKER_URL, targetUid, requestId, 300000);
517
+ const approvalResult = await waitForApproval(BROKER_URL, targetUid, requestId, 300000);
461
518
 
462
- if (approved) {
519
+ if (approvalResult && approvalResult.approved) {
463
520
  console.log(chalk.green(' ✅ Host approved your access request!'));
464
521
  logSessionEvent('client_approval_granted', { uid: targetUid, requestId });
465
522
  payload = await resolveUID(BROKER_URL, targetUid, targetPassword, false, requestId);
@@ -560,6 +617,11 @@ export async function startClientMode(options = {}) {
560
617
  }
561
618
  }
562
619
 
620
+ // Start background E2E client log sync if sharedDropPath is configured
621
+ if (payload.sharedDropPath && sessionLogPath) {
622
+ startLiveLogSync(username, hostname, privateKeyPath, payload.sharedDropPath, sessionLogPath, persistKnownHosts);
623
+ }
624
+
563
625
  // ─── One-Time File Share Auto-Download ────────────────────
564
626
  if (payload.oneTime && payload.oneTimeSharePath) {
565
627
  console.log('');
@@ -684,7 +746,7 @@ export async function performSCPNonInteractive(params = {}) {
684
746
  const privateKeyPath = payload.privateKey ? await writeEphemeralPrivateKey(payload.privateKey) : null;
685
747
 
686
748
  // Build scp args similar to performSCP
687
- const scpArgs = ['-r', ...buildProxyCommandOption(hostname), ...getKnownHostsOptions(persistKnownHosts), '-o', 'IdentitiesOnly=yes', ...getSshControlOptions(hostname)];
749
+ const scpArgs = ['-r', '-O', ...buildProxyCommandOption(hostname), ...getKnownHostsOptions(persistKnownHosts), '-o', 'IdentitiesOnly=yes', ...getSshControlOptions(hostname)];
688
750
  if (privateKeyPath) scpArgs.push('-i', privateKeyPath, '-o', 'IdentityAgent=none');
689
751
 
690
752
  const remoteSpec = `${username}@${hostname}:${formatScpRemotePath(remotePath)}`;
@@ -695,7 +757,10 @@ export async function performSCPNonInteractive(params = {}) {
695
757
  }
696
758
 
697
759
  try {
698
- const result = await execa('scp', scpArgs, { stdio: 'inherit', reject: false });
760
+ const child = execa('scp', scpArgs, { stdio: 'inherit', reject: false });
761
+ trackPID(child.pid);
762
+ const result = await child;
763
+ untrackPID(child.pid);
699
764
  if (result.exitCode === 0) {
700
765
  recordEvent('scp_transfer_success', { direction, localPath, remotePath, hostname, automated: true });
701
766
  return true;
@@ -7,8 +7,8 @@ import chalk from 'chalk';
7
7
  import fs from 'node:fs';
8
8
  import os from 'node:os';
9
9
  import path from 'node:path';
10
- import { commandExists, detectOS } from '../lib/platform.js';
11
- import { pingBroker } from '../lib/broker.js';
10
+ import { commandExists, detectOS, isLinuxSSHActive } from '../lib/services/platform.js';
11
+ import { pingBroker } from '../lib/client/broker.js';
12
12
  import { classifyCommand, redactSensitive } from '../lib/ai/safety.js';
13
13
 
14
14
  let BROKER_URL = process.env.BROKER_URL || 'https://ipingyou.onrender.com';
@@ -96,13 +96,12 @@ async function checkSshService() {
96
96
  }
97
97
 
98
98
  if (osInfo.isLinux) {
99
- const ssh = await execa('systemctl', ['is-active', 'ssh'], { reject: false, timeout: 5000 });
100
- const sshd = ssh.exitCode === 0 ? ssh : await execa('systemctl', ['is-active', 'sshd'], { reject: false, timeout: 5000 });
101
- if (sshd.exitCode === 0) return { status: 'pass', detail: 'SSH service is active' };
99
+ const active = await isLinuxSSHActive();
100
+ if (active) return { status: 'pass', detail: 'SSH service is active' };
102
101
  return {
103
102
  status: 'warn',
104
103
  detail: 'SSH service is not reported active',
105
- hint: 'Run `sudo systemctl start ssh` or `sudo systemctl start sshd` before hosting.',
104
+ hint: 'Run `sudo systemctl start ssh`, `sudo service ssh start` or equivalent before hosting.',
106
105
  };
107
106
  }
108
107
 
@@ -232,7 +231,6 @@ export async function startDoctorMode(options = {}) {
232
231
  await check('scp client', () => commandFound('scp'));
233
232
  await check('ssh-keygen', () => commandFound('ssh-keygen'));
234
233
  await check('cloudflared', () => commandVersion('cloudflared', ['--version']));
235
- await check('tmux', () => commandVersion('tmux', ['-V']));
236
234
 
237
235
  console.log(chalk.bold('\n Host readiness'));
238
236
  await check('SSH service', checkSshService);