@miraj181/ipingyou 2.0.13 โ†’ 2.1.0

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/README.md CHANGED
@@ -22,6 +22,11 @@ No firewalls to configure. No port forwarding. No plaintext leakage.
22
22
  * ๐Ÿ“ก **Hardware Telemetry Verification**: Clients silently generate hardware footprint reports (OS, RAM, CPU, IP), encrypt them locally with the session password, and send them to the Host for authorization.
23
23
  * ๐Ÿšจ **Panic Kill-Switch**: Type `ipingyou panic` to instantly vaporize all associated keys, wipe all alias configs, and send a `SIGKILL` to every active tunnel and SSH shell.
24
24
  * ๐Ÿ‘ป **Daemonization**: Run `ipingyou service install` to quietly install and run the Host listener in the background (survives system reboots using PM2).
25
+ * ๐Ÿงญ **Approval Gate**: Require the Host to explicitly approve clients before they receive tunnel/key material.
26
+ * ๐Ÿ“ฆ **One-Time File Share**: Serve a single file/folder over SCP and revoke after use.
27
+ * ๐ŸŒ **HTTP & TCP Exposure**: Share a local web app or any TCP service (DB/RDP/VNC) beyond SSH.
28
+ * ๐Ÿ“‚ **Shared Drop Folder**: Auto-prepared dropbox folder for safe file transfers (macOS friendly).
29
+ * ๐Ÿงพ **Live Session Logs**: Host/client/broker write ephemeral per-session logs with actions and request/response status, removed on exit.
25
30
 
26
31
  ---
27
32
 
@@ -100,13 +105,15 @@ These alerts (e.g., "AI-detected potential code anomaly", "Shell access", "Netwo
100
105
  | `ipingyou` | Interactive CLI dashboard wizard. |
101
106
  | `ipingyou host` | Start hosting and exposing your local machine securely. |
102
107
  | `ipingyou connect -u <UID>` | Connect directly to a specific UID. |
108
+ | `ipingyou ai` | Groq-powered task assistant with guarded local/remote tools. |
109
+ | `ipingyou doctor` | Diagnostics for dependencies, SSH, broker, SCP, AI, and tests. |
103
110
  | `ipingyou panic` | ๐Ÿšจ Self-destruct mode. Wipes configs, memory, and kills all processes. |
104
111
  | `ipingyou service install` | ๐Ÿ‘ป Installs Host mode as an always-on background daemon. |
105
112
  | `ipingyou service stop` | Stops and removes the background daemon. |
113
+ | `ipingyou service status` | Shows background daemon status. |
106
114
 
107
115
  ---
108
116
 
109
117
  ## ๐Ÿ“œ License
110
118
 
111
119
  [MIT License](LICENSE) ยฉ Sk Mirajul Islam
112
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miraj181/ipingyou",
3
- "version": "2.0.13",
3
+ "version": "2.1.0",
4
4
  "description": "SecureLink-CLI โ€” Secure peer-to-peer remote access via SSH & Cloudflare Tunnels",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
@@ -18,6 +18,7 @@
18
18
  "scripts": {
19
19
  "start": "node src/server.js",
20
20
  "dev:cli": "node src/cli.js",
21
+ "test": "node test/test_ai_helpers.js",
21
22
  "prepublishOnly": "node -e \"require('fs').accessSync('LICENSE')\" && node src/cli.js --help"
22
23
  },
23
24
  "keywords": [
@@ -44,16 +45,16 @@
44
45
  },
45
46
  "dependencies": {
46
47
  "chalk": "^5.3.0",
47
- "commander": "^12.1.0",
48
+ "commander": "^14.0.3",
48
49
  "execa": "^9.5.2",
49
- "inquirer": "^12.3.2",
50
+ "inquirer": "^13.4.3",
50
51
  "nanoid": "^5.0.9",
51
52
  "open": "^11.0.0",
52
- "ora": "^8.1.1",
53
+ "ora": "^9.4.0",
53
54
  "tree-kill": "^1.2.2",
54
55
  "ws": "^8.20.1",
55
- "express": "^4.19.2",
56
- "express-rate-limit": "^7.3.1",
56
+ "express": "^5.2.1",
57
+ "express-rate-limit": "^8.5.2",
57
58
  "helmet": "^7.1.0"
58
59
  },
59
60
  "devDependencies": {
package/src/cli.js CHANGED
@@ -29,6 +29,8 @@ import { detectOS, checkDependencies } from './lib/platform.js';
29
29
  import { cleanupAll, installShutdownHandlers, executePanicMode } from './lib/cleanup.js';
30
30
  import { startHostMode } from './modes/host.js';
31
31
  import { startClientMode } from './modes/client.js';
32
+ import { startAIMode } from './modes/ai.js';
33
+ import { startDoctorMode } from './modes/doctor.js';
32
34
 
33
35
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
34
36
  const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf8'));
@@ -112,6 +114,8 @@ function showRichHelp() {
112
114
  console.log(` ${chalk.green('host')} : Generates a secure session UID and exposes your local machine.`);
113
115
  console.log(` ${chalk.blue('connect')} : Prompts for a UID to connect to a remote host.`);
114
116
  console.log(` ${chalk.dim('Supports Interactive SSH Shell & SCP File Transfers')}`);
117
+ console.log(` ${chalk.magenta('ai')} : Groq-powered task assistant with guarded local/remote tools.`);
118
+ console.log(` ${chalk.yellow('doctor')} : Run diagnostics for dependencies, SSH, broker, SCP, AI, and tests.`);
115
119
  console.log('');
116
120
 
117
121
  console.log(chalk.bold.white(' ๐Ÿ”’ Security Architecture:'));
@@ -132,8 +136,12 @@ function showRichHelp() {
132
136
  console.log(` $ npx ipingyou ${chalk.dim('# Interactive wizard (Recommended)')}`);
133
137
  console.log(` $ npx ipingyou host ${chalk.dim('# Quick start as Host')}`);
134
138
  console.log(` $ npx ipingyou connect ${chalk.dim('# Quick start as Client')}`);
139
+ console.log(` $ npx ipingyou ai ${chalk.dim('# Start AI task assistant')}`);
140
+ console.log(` $ npx ipingyou doctor ${chalk.dim('# Diagnose local setup')}`);
135
141
  console.log(` $ npx ipingyou panic ${chalk.dim('# Self-destruct and wipe memory/traces')}`);
136
142
  console.log(` $ npx ipingyou service install ${chalk.dim('# Install Host mode as a background daemon')}`);
143
+ console.log(` $ npx ipingyou allowlist ${chalk.dim('# Manage AI command allowlist')}`);
144
+ console.log(` $ npx ipingyou history ${chalk.dim('# View session event logs')}`);
137
145
  console.log('');
138
146
  }
139
147
 
@@ -192,6 +200,14 @@ async function interactiveMode() {
192
200
  name: `${chalk.blue('๐ŸŒ Access a Remote Machine')} ${chalk.dim('โ€” Connect to a host via their UID (SSH/SCP)')}`,
193
201
  value: 'client',
194
202
  },
203
+ {
204
+ name: `${chalk.magenta('๐Ÿค– AI Task Assistant')} ${chalk.dim('โ€” Use Groq LLMs with guarded local/remote tools')}`,
205
+ value: 'ai',
206
+ },
207
+ {
208
+ name: `${chalk.yellow('๐Ÿฉบ Run Doctor')} ${chalk.dim('โ€” Diagnose setup and project health')}`,
209
+ value: 'doctor',
210
+ },
195
211
  new inquirer.Separator(),
196
212
  {
197
213
  name: `${chalk.magenta('๐Ÿ“– Help / Information')} ${chalk.dim('โ€” Learn how iPingYou works')}`,
@@ -208,6 +224,12 @@ async function interactiveMode() {
208
224
  case 'client':
209
225
  await startClientMode();
210
226
  break;
227
+ case 'ai':
228
+ await startAIMode();
229
+ break;
230
+ case 'doctor':
231
+ await startDoctorMode();
232
+ break;
211
233
  case 'help':
212
234
  showRichHelp();
213
235
  break;
@@ -265,6 +287,40 @@ program
265
287
  }
266
288
  });
267
289
 
290
+ program
291
+ .command('ai')
292
+ .description('Start AI mode โ€” Groq-powered local/remote task assistant with guarded tools')
293
+ .action(async () => {
294
+ try {
295
+ const opts = program.opts();
296
+ if (opts.broker) process.env.BROKER_URL = opts.broker;
297
+
298
+ showBanner();
299
+ showSystemInfo();
300
+ installShutdownHandlers();
301
+ await startAIMode();
302
+ } catch (err) {
303
+ fatal('ai', err);
304
+ }
305
+ });
306
+
307
+ program
308
+ .command('doctor')
309
+ .description('Run non-invasive diagnostics for dependencies, SSH, broker, SCP, AI, and tests')
310
+ .option('--full', 'Run full diagnostics, including broker integration if a local broker is running')
311
+ .action(async (commandOptions) => {
312
+ try {
313
+ const opts = program.opts();
314
+ if (opts.broker) process.env.BROKER_URL = opts.broker;
315
+
316
+ showBanner();
317
+ showSystemInfo();
318
+ await startDoctorMode({ full: commandOptions.full });
319
+ } catch (err) {
320
+ fatal('doctor', err);
321
+ }
322
+ });
323
+
268
324
  program
269
325
  .command('panic')
270
326
  .description('๐Ÿšจ Self-destruct mode: wipe all configs, kill tunnels, and remove traces')
@@ -310,6 +366,166 @@ program
310
366
  }
311
367
  });
312
368
 
369
+ program
370
+ .command('allowlist')
371
+ .description('Manage AI command allowlist โ€” add, remove, or list safe regex patterns')
372
+ .argument('[action]', 'Action: list, add, remove', 'list')
373
+ .argument('[pattern]', 'Regex pattern to add or remove')
374
+ .action(async (action, pattern) => {
375
+ try {
376
+ showBanner();
377
+ const fs = await import('node:fs');
378
+ const os = await import('node:os');
379
+ const path = await import('node:path');
380
+ const { ensureAllowlistFile, getAllowlistRegexes } = await import('./lib/allowlist.js');
381
+
382
+ const allowlistPath = ensureAllowlistFile();
383
+
384
+ if (action === 'list' || !action) {
385
+ const patterns = getAllowlistRegexes();
386
+ console.log(chalk.bold.cyan(' ๐Ÿ“‹ AI Command Allowlist'));
387
+ console.log(chalk.dim(' โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€'));
388
+ console.log(chalk.dim(` File: ${allowlistPath}`));
389
+ console.log('');
390
+ if (patterns.length === 0) {
391
+ console.log(chalk.dim(' No patterns configured.'));
392
+ } else {
393
+ try {
394
+ const raw = JSON.parse(fs.readFileSync(allowlistPath, 'utf8'));
395
+ raw.forEach((p, i) => {
396
+ console.log(` ${chalk.green(i + 1)}. ${chalk.white(p)}`);
397
+ });
398
+ } catch (e) {
399
+ console.log(chalk.red(' โŒ Allowlist file is corrupted.'));
400
+ }
401
+ }
402
+ console.log('');
403
+ console.log(chalk.dim(' Usage: ipingyou allowlist add "^my-safe-command"'));
404
+ console.log(chalk.dim(' ipingyou allowlist remove "^my-safe-command"'));
405
+ } else if (action === 'add') {
406
+ if (!pattern) {
407
+ console.log(chalk.red(' โŒ Missing pattern. Usage: ipingyou allowlist add "^my-command"'));
408
+ process.exit(1);
409
+ }
410
+ // Validate regex
411
+ try { new RegExp(pattern); } catch (err) {
412
+ console.log(chalk.red(` โŒ Invalid regex: ${err.message}`));
413
+ process.exit(1);
414
+ }
415
+ let raw = [];
416
+ try { raw = JSON.parse(fs.readFileSync(allowlistPath, 'utf8')); } catch {}
417
+ if (raw.includes(pattern)) {
418
+ console.log(chalk.yellow(` โš ๏ธ Pattern already exists: ${pattern}`));
419
+ } else {
420
+ raw.push(pattern);
421
+ fs.writeFileSync(allowlistPath, JSON.stringify(raw, null, 2), { mode: 0o600 });
422
+ console.log(chalk.green(` โœ… Added: ${chalk.white(pattern)}`));
423
+ }
424
+ } else if (action === 'remove') {
425
+ if (!pattern) {
426
+ console.log(chalk.red(' โŒ Missing pattern. Usage: ipingyou allowlist remove "^my-command"'));
427
+ process.exit(1);
428
+ }
429
+ let raw = [];
430
+ try { raw = JSON.parse(fs.readFileSync(allowlistPath, 'utf8')); } catch {}
431
+ const idx = raw.indexOf(pattern);
432
+ if (idx === -1) {
433
+ console.log(chalk.yellow(` โš ๏ธ Pattern not found: ${pattern}`));
434
+ } else {
435
+ raw.splice(idx, 1);
436
+ fs.writeFileSync(allowlistPath, JSON.stringify(raw, null, 2), { mode: 0o600 });
437
+ console.log(chalk.green(` โœ… Removed: ${chalk.white(pattern)}`));
438
+ }
439
+ } else {
440
+ console.log(chalk.red(` โŒ Unknown action: ${action}. Use list, add, or remove.`));
441
+ }
442
+ } catch (err) {
443
+ fatal('allowlist', err);
444
+ }
445
+ });
446
+
447
+ program
448
+ .command('history')
449
+ .description('View session event logs from ~/.ipingyou/logs')
450
+ .option('-n, --lines <count>', 'Number of recent events to show', '25')
451
+ .option('--type <type>', 'Filter by event type (e.g. ssh, scp, ai)')
452
+ .option('--json', 'Output raw JSON lines')
453
+ .action(async (commandOptions) => {
454
+ try {
455
+ showBanner();
456
+ const fs = await import('node:fs');
457
+ const os = await import('node:os');
458
+ const pathMod = await import('node:path');
459
+
460
+ const logFile = pathMod.default.join(os.default.homedir(), '.ipingyou', 'logs', 'session-events.jsonl');
461
+
462
+ console.log(chalk.bold.cyan(' ๐Ÿ“œ Session Event History'));
463
+ console.log(chalk.dim(' โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€'));
464
+
465
+ if (!fs.existsSync(logFile)) {
466
+ console.log(chalk.dim(' No session history found yet.'));
467
+ console.log(chalk.dim(` Events are recorded to: ${logFile}`));
468
+ return;
469
+ }
470
+
471
+ const raw = fs.readFileSync(logFile, 'utf8').trim();
472
+ if (!raw) {
473
+ console.log(chalk.dim(' Log file is empty.'));
474
+ return;
475
+ }
476
+
477
+ let events = raw.split('\n').map(line => {
478
+ try { return JSON.parse(line); } catch { return null; }
479
+ }).filter(Boolean);
480
+
481
+ // Filter by type if specified
482
+ if (commandOptions.type) {
483
+ const filter = commandOptions.type.toLowerCase();
484
+ events = events.filter(e => (e.type || '').toLowerCase().includes(filter));
485
+ }
486
+
487
+ // Take last N events
488
+ const count = parseInt(commandOptions.lines) || 25;
489
+ events = events.slice(-count);
490
+
491
+ if (events.length === 0) {
492
+ console.log(chalk.dim(' No matching events found.'));
493
+ return;
494
+ }
495
+
496
+ console.log(chalk.dim(` Showing last ${events.length} event(s)${commandOptions.type ? ` matching "${commandOptions.type}"` : ''}:`));
497
+ console.log('');
498
+
499
+ if (commandOptions.json) {
500
+ events.forEach(e => console.log(JSON.stringify(e)));
501
+ } else {
502
+ const levelColors = {
503
+ info: chalk.blue,
504
+ warn: chalk.yellow,
505
+ error: chalk.red,
506
+ };
507
+
508
+ events.forEach(e => {
509
+ const time = e.time ? new Date(e.time).toLocaleString() : '?';
510
+ const level = (e.level || 'info').toUpperCase().padEnd(5);
511
+ const colorFn = levelColors[e.level] || chalk.white;
512
+ const type = chalk.cyan((e.type || 'unknown').padEnd(35));
513
+ const details = e.details && Object.keys(e.details).length > 0
514
+ ? chalk.dim(JSON.stringify(e.details))
515
+ : '';
516
+
517
+ console.log(` ${chalk.dim(time)} ${colorFn(level)} ${type} ${details}`);
518
+ });
519
+ }
520
+
521
+ console.log('');
522
+ console.log(chalk.dim(` Log file: ${logFile}`));
523
+ console.log(chalk.dim(` Total events in file: ${raw.split('\n').length}`));
524
+ } catch (err) {
525
+ fatal('history', err);
526
+ }
527
+ });
528
+
313
529
  // โ”€โ”€โ”€ Default: interactive mode โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
314
530
  program.action(async () => {
315
531
  try {
@@ -0,0 +1,116 @@
1
+ const GROQ_API_BASE = 'https://api.groq.com/openai/v1';
2
+
3
+ export const DEFAULT_AI_MODEL = 'qwen/qwen3-32b';
4
+
5
+ export function getGroqApiKey() {
6
+ return process.env.GROQ_API_KEY || null;
7
+ }
8
+
9
+ export async function listGroqModels(apiKey) {
10
+ const res = await fetch(`${GROQ_API_BASE}/models`, {
11
+ headers: {
12
+ Authorization: `Bearer ${apiKey}`,
13
+ 'Content-Type': 'application/json',
14
+ },
15
+ });
16
+
17
+ if (!res.ok) {
18
+ const message = await res.text().catch(() => '');
19
+ throw new Error(`Groq model lookup failed: HTTP ${res.status} ${message}`.trim());
20
+ }
21
+
22
+ const data = await res.json();
23
+ return Array.isArray(data.data) ? data.data : [];
24
+ }
25
+
26
+ export async function createGroqChatCompletion(apiKey, body) {
27
+ const res = await fetch(`${GROQ_API_BASE}/chat/completions`, {
28
+ method: 'POST',
29
+ headers: {
30
+ Authorization: `Bearer ${apiKey}`,
31
+ 'Content-Type': 'application/json',
32
+ },
33
+ body: JSON.stringify(body),
34
+ });
35
+
36
+ if (!res.ok) {
37
+ const message = await res.text().catch(() => '');
38
+ const err = new Error(`Groq chat completion failed: HTTP ${res.status} ${message}`.trim());
39
+ err.status = res.status;
40
+ err.rateLimit = parseGroqRateLimitHeaders(res.headers);
41
+ throw err;
42
+ }
43
+
44
+ const data = await res.json();
45
+ data._rateLimit = parseGroqRateLimitHeaders(res.headers);
46
+ return data;
47
+ }
48
+
49
+ export function parseGroqRateLimitHeaders(headers) {
50
+ const readNumber = (name) => {
51
+ const value = headers.get(name);
52
+ if (!value) return null;
53
+ const parsed = Number(value);
54
+ return Number.isFinite(parsed) ? parsed : null;
55
+ };
56
+
57
+ return {
58
+ limitRequests: readNumber('x-ratelimit-limit-requests'),
59
+ remainingRequests: readNumber('x-ratelimit-remaining-requests'),
60
+ resetRequests: headers.get('x-ratelimit-reset-requests') || null,
61
+ limitTokens: readNumber('x-ratelimit-limit-tokens'),
62
+ remainingTokens: readNumber('x-ratelimit-remaining-tokens'),
63
+ resetTokens: headers.get('x-ratelimit-reset-tokens') || null,
64
+ retryAfter: headers.get('retry-after') || null,
65
+ };
66
+ }
67
+
68
+ // Lightweight token estimator: approximate tokens by characters/4
69
+ export function estimateTokensForMessages(messages = [], completionText = '') {
70
+ try {
71
+ let chars = 0;
72
+ for (const m of messages || []) {
73
+ if (!m) continue;
74
+ chars += String(m).length;
75
+ }
76
+ chars += String(completionText || '').length;
77
+ return Math.max(0, Math.ceil(chars / 4));
78
+ } catch {
79
+ return 0;
80
+ }
81
+ }
82
+
83
+ export function getRateLimitWarnings(rateLimit, threshold = 0.8) {
84
+ if (!rateLimit) return [];
85
+
86
+ const warnings = [];
87
+ const checks = [
88
+ {
89
+ label: 'daily request',
90
+ limit: rateLimit.limitRequests,
91
+ remaining: rateLimit.remainingRequests,
92
+ reset: rateLimit.resetRequests,
93
+ },
94
+ {
95
+ label: 'per-minute token',
96
+ limit: rateLimit.limitTokens,
97
+ remaining: rateLimit.remainingTokens,
98
+ reset: rateLimit.resetTokens,
99
+ },
100
+ ];
101
+
102
+ for (const item of checks) {
103
+ if (!item.limit || item.remaining === null || item.remaining === undefined) continue;
104
+ const used = Math.max(0, item.limit - item.remaining);
105
+ const ratio = used / item.limit;
106
+ if (ratio >= threshold) {
107
+ warnings.push({
108
+ ...item,
109
+ used,
110
+ percent: Math.round(ratio * 100),
111
+ });
112
+ }
113
+ }
114
+
115
+ return warnings;
116
+ }
@@ -0,0 +1,114 @@
1
+ const SECRET_PATTERNS = [
2
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
3
+ /\b(gsk_[A-Za-z0-9_-]{20,})\b/g,
4
+ /\b(sk-[A-Za-z0-9_-]{20,})\b/g,
5
+ /\b(ghp_[A-Za-z0-9_]{20,})\b/g,
6
+ /\b(xox[baprs]-[A-Za-z0-9-]{20,})\b/g,
7
+ /(["']?(?:api[_-]?key|token|password|secret|private[_-]?key)["']?\s*[:=]\s*["']?)[^\s"',}]+/gi,
8
+ ];
9
+
10
+ const BLOCKED_PATH_PATTERNS = [
11
+ /(^|[\s~/])\.ssh(\/|$)/,
12
+ /(^|[\s~/])\.gnupg(\/|$)/,
13
+ /(^|[\s~/])\.aws(\/|$)/,
14
+ /(^|[\s~/])\.config\/gh(\/|$)/,
15
+ /(^|[\s~/])\.ipingyou(\/|$)/,
16
+ /(^|[\s~/])\.npmrc($|\s)/,
17
+ /(^|[\s~/])\.netrc($|\s)/,
18
+ /(^|[\s~/])\.pypirc($|\s)/,
19
+ /(^|[\s~/])\.env($|[\s.]|\/)/,
20
+ /id_(rsa|dsa|ecdsa|ed25519)(\.pub)?$/,
21
+ ];
22
+
23
+ const BLOCKED_COMMAND_PATTERNS = [
24
+ /^\s*(env|printenv|set)\b/,
25
+ /\b(export|declare)\s+-p\b/,
26
+ /\bsecurity\s+find-generic-password\b/,
27
+ /\bpass\s+show\b/,
28
+ ];
29
+
30
+ const DANGEROUS_COMMAND_PATTERNS = [
31
+ /\brm\s+(-[^\s]*[rf][^\s]*|--recursive|--force)/,
32
+ /\bsudo\b/,
33
+ /\bchmod\b/,
34
+ /\bchown\b/,
35
+ /\bmv\b/,
36
+ /\bcp\b/,
37
+ /\binstall\b/,
38
+ /\bnpm\s+(i|install|uninstall|remove|rm)\b/,
39
+ /\b(pnpm|yarn)\s+(add|remove|install)\b/,
40
+ /\b(git\s+(reset|clean|checkout|switch|merge|rebase|push|commit))\b/,
41
+ /\b(curl|wget)\b/,
42
+ />|>>|\btee\b/,
43
+ ];
44
+
45
+ import { getAllowlistRegexes } from '../allowlist.js';
46
+
47
+ const READ_ONLY_COMMAND_PATTERNS = [
48
+ /^\s*(pwd|ls|find|rg|grep|sed|cat|head|tail|wc|git status|git diff|git log|git show|node --version|npm --version|which|date|uname)\b/,
49
+ ];
50
+
51
+ export function redactSensitive(value) {
52
+ let output = String(value ?? '');
53
+ for (const pattern of SECRET_PATTERNS) {
54
+ output = output.replace(pattern, (match, prefix) => prefix ? `${prefix}[REDACTED]` : '[REDACTED]');
55
+ }
56
+ return output;
57
+ }
58
+
59
+ export function truncateForModel(value, maxChars = 12000) {
60
+ const text = redactSensitive(value);
61
+ if (text.length <= maxChars) return text;
62
+ return `${text.slice(0, maxChars)}\n[TRUNCATED ${text.length - maxChars} chars]`;
63
+ }
64
+
65
+ export function commandTouchesBlockedPath(command) {
66
+ return BLOCKED_PATH_PATTERNS.some(pattern => pattern.test(command));
67
+ }
68
+
69
+ export function classifyCommand(command) {
70
+ const text = String(command || '').trim();
71
+ if (!text) return { blocked: true, needsApproval: true, reason: 'Empty command' };
72
+
73
+ if (commandTouchesBlockedPath(text)) {
74
+ return {
75
+ blocked: true,
76
+ needsApproval: true,
77
+ reason: 'Command references a protected secret/config path',
78
+ };
79
+ }
80
+
81
+ // Check user-provided allowlist first (if present)
82
+ try {
83
+ const userPatterns = getAllowlistRegexes();
84
+ if (Array.isArray(userPatterns) && userPatterns.some(p => p.test(text))) {
85
+ return { blocked: false, needsApproval: false, reason: 'Matched user allowlist' };
86
+ }
87
+ } catch {
88
+ // ignore allowlist errors and fall back to defaults
89
+ }
90
+
91
+ if (BLOCKED_COMMAND_PATTERNS.some(pattern => pattern.test(text))) {
92
+ return {
93
+ blocked: true,
94
+ needsApproval: true,
95
+ reason: 'Command could dump environment variables or credential stores',
96
+ };
97
+ }
98
+
99
+ if (DANGEROUS_COMMAND_PATTERNS.some(pattern => pattern.test(text))) {
100
+ return { blocked: false, needsApproval: true, reason: 'Command may modify files, install packages, or access the network' };
101
+ }
102
+
103
+ if (READ_ONLY_COMMAND_PATTERNS.some(pattern => pattern.test(text))) {
104
+ return { blocked: false, needsApproval: false, reason: 'Read-only command' };
105
+ }
106
+
107
+ return { blocked: false, needsApproval: true, reason: 'Command is not known to be read-only' };
108
+ }
109
+
110
+ export function sanitizeUserTask(task) {
111
+ return redactSensitive(task)
112
+ .replace(/session password\s*[:=]\s*\S+/gi, 'session password: [REDACTED]')
113
+ .replace(/private key\s*[:=]\s*\S+/gi, 'private key: [REDACTED]');
114
+ }
@@ -0,0 +1,35 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ const ALLOWLIST_PATH = path.join(os.homedir(), '.ipingyou', 'allowlist.json');
6
+
7
+ export function getAllowlistRegexes() {
8
+ try {
9
+ if (!fs.existsSync(ALLOWLIST_PATH)) return [];
10
+ const raw = fs.readFileSync(ALLOWLIST_PATH, 'utf8');
11
+ const data = JSON.parse(raw || '[]');
12
+ if (!Array.isArray(data)) return [];
13
+ return data.map(s => {
14
+ try { return new RegExp(s); } catch { return null; }
15
+ }).filter(Boolean);
16
+ } catch {
17
+ return [];
18
+ }
19
+ }
20
+
21
+ export function ensureAllowlistFile() {
22
+ try {
23
+ if (!fs.existsSync(path.dirname(ALLOWLIST_PATH))) {
24
+ fs.mkdirSync(path.dirname(ALLOWLIST_PATH), { recursive: true, mode: 0o700 });
25
+ }
26
+ if (!fs.existsSync(ALLOWLIST_PATH)) {
27
+ fs.writeFileSync(ALLOWLIST_PATH, JSON.stringify([
28
+ '^\\s*(pwd|ls|find|rg|grep|sed|cat|head|tail|wc|git status|git diff|git log|git show|node --version|npm --version|which|date|uname)\\b'
29
+ ], null, 2), { mode: 0o600 });
30
+ }
31
+ return ALLOWLIST_PATH;
32
+ } catch {
33
+ return ALLOWLIST_PATH;
34
+ }
35
+ }