@miraj181/ipingyou 2.1.19 → 2.1.23

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,73 @@ 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
+ let consecutiveFailures = 0;
41
+ let warnedOnce = false;
42
+ const interval = setInterval(async () => {
43
+ if (isSyncing) return;
44
+ isSyncing = true;
45
+ try {
46
+ if (!fs.existsSync(localLogPath)) return;
47
+ const stats = fs.statSync(localLogPath);
48
+ if (stats.size === lastSize && stats.mtimeMs === lastMtime) return;
49
+
50
+ const scpArgs = [
51
+ '-O',
52
+ ...buildProxyCommandOption(hostname),
53
+ ...getKnownHostsOptions(persistKnownHosts),
54
+ '-o', 'IdentitiesOnly=yes',
55
+ ...getSshControlOptions(hostname)
56
+ ];
57
+ if (privateKeyPath) {
58
+ scpArgs.push('-i', privateKeyPath, '-o', 'IdentityAgent=none');
59
+ }
60
+
61
+ const clientName = `${os.userInfo().username}-${os.hostname()}`;
62
+ const remoteFilePath = `${remoteDropPath}/client-${clientName}.log`;
63
+
64
+ scpArgs.push(localLogPath, `${username}@${hostname}:${formatScpRemotePath(remoteFilePath)}`);
65
+
66
+ const result = await execa('scp', scpArgs, { reject: false });
67
+ if (result.exitCode === 0) {
68
+ lastSize = stats.size;
69
+ lastMtime = stats.mtimeMs;
70
+ consecutiveFailures = 0;
71
+ } else {
72
+ consecutiveFailures++;
73
+ if (consecutiveFailures >= 5 && !warnedOnce) {
74
+ warnedOnce = true;
75
+ logSessionEvent('client_log_sync_failing', { failures: consecutiveFailures, stderr: (result.stderr || '').slice(0, 200) }, 'warn');
76
+ }
77
+ }
78
+ } catch {
79
+ consecutiveFailures++;
80
+ } finally {
81
+ isSyncing = false;
82
+ }
83
+ }, 3000);
84
+
85
+ addCleanupHook(() => clearInterval(interval));
86
+ }
87
+
35
88
  async function promptUsername() {
36
89
  const { username } = await inquirer.prompt([
37
90
  {
@@ -94,18 +147,11 @@ async function connectSSH(username, hostname, privateKeyPath, persistKnownHosts
94
147
 
95
148
  const sshArgs = buildSshArgs(hostname, privateKeyPath, [
96
149
  '-o', 'ServerAliveInterval=30',
97
- '-o', 'ServerAliveCountMax=3'
150
+ '-o', 'ServerAliveCountMax=3',
151
+ '-t'
98
152
  ], { persistKnownHosts });
99
153
 
100
154
  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
155
 
110
156
  const child = execa('ssh', sshArgs, {
111
157
  stdio: 'inherit',
@@ -199,6 +245,7 @@ async function performSCP(username, hostname, direction, privateKeyPath, sharedD
199
245
  // Construct SCP args
200
246
  const scpArgs = [
201
247
  '-r', // recursive just in case
248
+ '-O', // Force legacy SCP protocol so that shell quoting in formatScpRemotePath works correctly
202
249
  ...buildProxyCommandOption(hostname),
203
250
  ...getKnownHostsOptions(persistKnownHosts),
204
251
  '-o', 'IdentitiesOnly=yes',
@@ -293,6 +340,7 @@ async function downloadSpecificRemotePath(username, hostname, privateKeyPath, re
293
340
  await showConnectionTrace('Local', 'Remote SCP');
294
341
  const scpArgs = [
295
342
  '-r',
343
+ '-O', // Force legacy SCP protocol
296
344
  ...buildProxyCommandOption(hostname),
297
345
  ...getKnownHostsOptions(persistKnownHosts),
298
346
  '-o', 'IdentitiesOnly=yes',
@@ -300,7 +348,10 @@ async function downloadSpecificRemotePath(username, hostname, privateKeyPath, re
300
348
  ];
301
349
  if (privateKeyPath) scpArgs.push('-i', privateKeyPath, '-o', 'IdentityAgent=none');
302
350
  scpArgs.push(`${username}@${hostname}:${formatScpRemotePath(remotePath)}`, localPath);
303
- const result = await execa('scp', scpArgs, { stdio: 'inherit', reject: false });
351
+ const child = execa('scp', scpArgs, { stdio: 'inherit', reject: false });
352
+ trackPID(child.pid);
353
+ const result = await child;
354
+ untrackPID(child.pid);
304
355
  return result.exitCode === 0;
305
356
  }
306
357
 
@@ -435,12 +486,27 @@ export async function startClientMode(options = {}) {
435
486
  console.log(chalk.dim(' The host has enabled approval gating. Submitting your access request...'));
436
487
 
437
488
  try {
489
+ let localIp = '127.0.0.1';
490
+ try {
491
+ const interfaces = os.networkInterfaces();
492
+ for (const devName in interfaces) {
493
+ const iface = interfaces[devName];
494
+ for (const alias of iface) {
495
+ if (alias.family === 'IPv4' && !alias.internal) {
496
+ localIp = alias.address;
497
+ break;
498
+ }
499
+ }
500
+ }
501
+ } catch {}
502
+
438
503
  const approvalDetails = {
439
504
  username: os.userInfo().username,
440
505
  hostname: os.hostname(),
441
506
  os: `${os.type()} ${os.release()} (${os.arch()})`,
442
507
  intent: 'connect',
443
508
  time: new Date().toISOString(),
509
+ localIp,
444
510
  };
445
511
 
446
512
  const { requestId, status: reqStatus, approvalRequired } = await requestHostApproval(
@@ -457,9 +523,9 @@ export async function startClientMode(options = {}) {
457
523
  console.log(chalk.dim(' This may take a few minutes. Press Ctrl+C to cancel.'));
458
524
  console.log('');
459
525
 
460
- const approved = await waitForApproval(BROKER_URL, targetUid, requestId, 300000);
526
+ const approvalResult = await waitForApproval(BROKER_URL, targetUid, requestId, 300000);
461
527
 
462
- if (approved) {
528
+ if (approvalResult && approvalResult.approved) {
463
529
  console.log(chalk.green(' ✅ Host approved your access request!'));
464
530
  logSessionEvent('client_approval_granted', { uid: targetUid, requestId });
465
531
  payload = await resolveUID(BROKER_URL, targetUid, targetPassword, false, requestId);
@@ -560,6 +626,15 @@ export async function startClientMode(options = {}) {
560
626
  }
561
627
  }
562
628
 
629
+ // Push telemetry immediately so host can see the client in "See detailed client telemetry"
630
+ // even before the user picks an action (SSH/SCP/etc.)
631
+ await pushTelemetry(BROKER_URL, targetUid, targetPassword, username, 'connected');
632
+
633
+ // Start background E2E client log sync if sharedDropPath is configured
634
+ if (payload.sharedDropPath && sessionLogPath) {
635
+ startLiveLogSync(username, hostname, privateKeyPath, payload.sharedDropPath, sessionLogPath, persistKnownHosts);
636
+ }
637
+
563
638
  // ─── One-Time File Share Auto-Download ────────────────────
564
639
  if (payload.oneTime && payload.oneTimeSharePath) {
565
640
  console.log('');
@@ -684,7 +759,7 @@ export async function performSCPNonInteractive(params = {}) {
684
759
  const privateKeyPath = payload.privateKey ? await writeEphemeralPrivateKey(payload.privateKey) : null;
685
760
 
686
761
  // Build scp args similar to performSCP
687
- const scpArgs = ['-r', ...buildProxyCommandOption(hostname), ...getKnownHostsOptions(persistKnownHosts), '-o', 'IdentitiesOnly=yes', ...getSshControlOptions(hostname)];
762
+ const scpArgs = ['-r', '-O', ...buildProxyCommandOption(hostname), ...getKnownHostsOptions(persistKnownHosts), '-o', 'IdentitiesOnly=yes', ...getSshControlOptions(hostname)];
688
763
  if (privateKeyPath) scpArgs.push('-i', privateKeyPath, '-o', 'IdentityAgent=none');
689
764
 
690
765
  const remoteSpec = `${username}@${hostname}:${formatScpRemotePath(remotePath)}`;
@@ -695,7 +770,10 @@ export async function performSCPNonInteractive(params = {}) {
695
770
  }
696
771
 
697
772
  try {
698
- const result = await execa('scp', scpArgs, { stdio: 'inherit', reject: false });
773
+ const child = execa('scp', scpArgs, { stdio: 'inherit', reject: false });
774
+ trackPID(child.pid);
775
+ const result = await child;
776
+ untrackPID(child.pid);
699
777
  if (result.exitCode === 0) {
700
778
  recordEvent('scp_transfer_success', { direction, localPath, remotePath, hostname, automated: true });
701
779
  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, getCloudflaredPath } 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
 
@@ -231,8 +230,8 @@ export async function startDoctorMode(options = {}) {
231
230
  await check('ssh client', () => commandVersion('ssh', ['-V']));
232
231
  await check('scp client', () => commandFound('scp'));
233
232
  await check('ssh-keygen', () => commandFound('ssh-keygen'));
234
- await check('cloudflared', () => commandVersion('cloudflared', ['--version']));
235
- await check('tmux', () => commandVersion('tmux', ['-V']));
233
+ const cfPath = await getCloudflaredPath();
234
+ await check('cloudflared', () => commandVersion(cfPath || 'cloudflared', ['--version']));
236
235
 
237
236
  console.log(chalk.bold('\n Host readiness'));
238
237
  await check('SSH service', checkSshService);