@miraj181/ipingyou 2.1.18 → 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
  {
@@ -54,6 +98,15 @@ async function writeEphemeralPrivateKey(privateKey) {
54
98
  const keyPath = path.join(os.tmpdir(), `ipingyou_client_${Date.now()}`);
55
99
  fs.writeFileSync(keyPath, normalizePrivateKey(privateKey), { mode: 0o600 });
56
100
 
101
+ // On Windows, NTFS ignores POSIX mode bits — fix ACLs with icacls
102
+ if (process.platform === 'win32') {
103
+ const currentUser = os.userInfo().username;
104
+ // Remove all inherited permissions first
105
+ await execa('icacls', [keyPath, '/inheritance:r'], { reject: false });
106
+ // Grant only the current user full control
107
+ await execa('icacls', [keyPath, '/grant:r', `${currentUser}:(F)`], { reject: false });
108
+ }
109
+
57
110
  const result = await execa('ssh-keygen', ['-y', '-f', keyPath], {
58
111
  reject: false,
59
112
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -85,18 +138,11 @@ async function connectSSH(username, hostname, privateKeyPath, persistKnownHosts
85
138
 
86
139
  const sshArgs = buildSshArgs(hostname, privateKeyPath, [
87
140
  '-o', 'ServerAliveInterval=30',
88
- '-o', 'ServerAliveCountMax=3'
141
+ '-o', 'ServerAliveCountMax=3',
142
+ '-t'
89
143
  ], { persistKnownHosts });
90
144
 
91
145
  sshArgs.push(`${username}@${hostname}`);
92
- const tmuxSession = buildTmuxSessionName(username);
93
- const quotedSocket = quoteRemoteShell(TMUX_SOCKET_PATH);
94
- const quotedSession = quoteRemoteShell(tmuxSession);
95
- const tmuxSocketCmdStr = `tmux -S ${quotedSocket}`;
96
- const tmuxPrepare = `${tmuxSocketCmdStr} has-session -t ${quotedSession} 2>/dev/null || ${tmuxSocketCmdStr} new-session -d -s ${quotedSession}`;
97
- const tmuxAttach = `${tmuxSocketCmdStr} attach -t ${quotedSession}`;
98
- const tmuxCommand = `if command -v tmux >/dev/null 2>&1; then (${tmuxPrepare} && ${tmuxAttach}) || exec $SHELL -l; else exec $SHELL -l; fi`;
99
- sshArgs.push('-t', tmuxCommand);
100
146
 
101
147
  const child = execa('ssh', sshArgs, {
102
148
  stdio: 'inherit',
@@ -190,6 +236,7 @@ async function performSCP(username, hostname, direction, privateKeyPath, sharedD
190
236
  // Construct SCP args
191
237
  const scpArgs = [
192
238
  '-r', // recursive just in case
239
+ '-O', // Force legacy SCP protocol so that shell quoting in formatScpRemotePath works correctly
193
240
  ...buildProxyCommandOption(hostname),
194
241
  ...getKnownHostsOptions(persistKnownHosts),
195
242
  '-o', 'IdentitiesOnly=yes',
@@ -284,6 +331,7 @@ async function downloadSpecificRemotePath(username, hostname, privateKeyPath, re
284
331
  await showConnectionTrace('Local', 'Remote SCP');
285
332
  const scpArgs = [
286
333
  '-r',
334
+ '-O', // Force legacy SCP protocol
287
335
  ...buildProxyCommandOption(hostname),
288
336
  ...getKnownHostsOptions(persistKnownHosts),
289
337
  '-o', 'IdentitiesOnly=yes',
@@ -291,7 +339,10 @@ async function downloadSpecificRemotePath(username, hostname, privateKeyPath, re
291
339
  ];
292
340
  if (privateKeyPath) scpArgs.push('-i', privateKeyPath, '-o', 'IdentityAgent=none');
293
341
  scpArgs.push(`${username}@${hostname}:${formatScpRemotePath(remotePath)}`, localPath);
294
- 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);
295
346
  return result.exitCode === 0;
296
347
  }
297
348
 
@@ -426,12 +477,27 @@ export async function startClientMode(options = {}) {
426
477
  console.log(chalk.dim(' The host has enabled approval gating. Submitting your access request...'));
427
478
 
428
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
+
429
494
  const approvalDetails = {
430
495
  username: os.userInfo().username,
431
496
  hostname: os.hostname(),
432
497
  os: `${os.type()} ${os.release()} (${os.arch()})`,
433
498
  intent: 'connect',
434
499
  time: new Date().toISOString(),
500
+ localIp,
435
501
  };
436
502
 
437
503
  const { requestId, status: reqStatus, approvalRequired } = await requestHostApproval(
@@ -448,9 +514,9 @@ export async function startClientMode(options = {}) {
448
514
  console.log(chalk.dim(' This may take a few minutes. Press Ctrl+C to cancel.'));
449
515
  console.log('');
450
516
 
451
- const approved = await waitForApproval(BROKER_URL, targetUid, requestId, 300000);
517
+ const approvalResult = await waitForApproval(BROKER_URL, targetUid, requestId, 300000);
452
518
 
453
- if (approved) {
519
+ if (approvalResult && approvalResult.approved) {
454
520
  console.log(chalk.green(' ✅ Host approved your access request!'));
455
521
  logSessionEvent('client_approval_granted', { uid: targetUid, requestId });
456
522
  payload = await resolveUID(BROKER_URL, targetUid, targetPassword, false, requestId);
@@ -551,6 +617,11 @@ export async function startClientMode(options = {}) {
551
617
  }
552
618
  }
553
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
+
554
625
  // ─── One-Time File Share Auto-Download ────────────────────
555
626
  if (payload.oneTime && payload.oneTimeSharePath) {
556
627
  console.log('');
@@ -675,7 +746,7 @@ export async function performSCPNonInteractive(params = {}) {
675
746
  const privateKeyPath = payload.privateKey ? await writeEphemeralPrivateKey(payload.privateKey) : null;
676
747
 
677
748
  // Build scp args similar to performSCP
678
- 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)];
679
750
  if (privateKeyPath) scpArgs.push('-i', privateKeyPath, '-o', 'IdentityAgent=none');
680
751
 
681
752
  const remoteSpec = `${username}@${hostname}:${formatScpRemotePath(remotePath)}`;
@@ -686,7 +757,10 @@ export async function performSCPNonInteractive(params = {}) {
686
757
  }
687
758
 
688
759
  try {
689
- 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);
690
764
  if (result.exitCode === 0) {
691
765
  recordEvent('scp_transfer_success', { direction, localPath, remotePath, hostname, automated: true });
692
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);