@jishankai/solid-cli 1.0.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.
@@ -0,0 +1,453 @@
1
+ import { BaseAgent } from './BaseAgent.js';
2
+ import { executeShellCommand } from '../utils/commander.js';
3
+ import fs from 'fs/promises';
4
+ import path from 'path';
5
+
6
+ /**
7
+ * BlockchainAgent - Analyzes blockchain, wallet, and DeFi security threats
8
+ */
9
+ export class BlockchainAgent extends BaseAgent {
10
+ constructor() {
11
+ super('BlockchainAgent');
12
+
13
+ // Known wallet applications and processes
14
+ this.knownWallets = new Set([
15
+ 'MetaMask', 'phantom', 'solana', 'trust-wallet', 'coinbase-wallet',
16
+ 'ledger-live', 'trezor-bridge', 'exodus', 'atomic-wallet', 'myetherwallet',
17
+ 'metamask', 'phantom-wallet', 'brave-wallet', 'rainbow', 'argent',
18
+ 'gnosis-safe', 'imtoken', 'tokenpocket', 'mathwallet', 'safepal'
19
+ ]);
20
+
21
+ // Known DeFi platforms and protocols
22
+ this.knownDeFi = new Set([
23
+ 'uniswap', 'sushiswap', 'pancakeswap', 'curve', 'balancer',
24
+ 'compound', 'aave', 'makerdao', 'yearn-finance', 'lido',
25
+ 'opensea', 'rarible', 'superrare', 'foundation', 'zora',
26
+ '1inch', '0x', 'paraswap', 'dydx', 'perpetual-protocol',
27
+ 'synthetix', 'uma', 'kyber', 'bancor', 'thorchain'
28
+ ]);
29
+
30
+ // Suspicious blockchain-related processes
31
+ this.suspiciousProcesses = new Set([
32
+ 'crypto-miner', 'coin-miner', 'xmr-miner', 'eth-miner',
33
+ 'bitcoin-miner', 'crypto-hijack', 'blockchain-malware',
34
+ 'wallet-stealer', 'seed-phrase', 'private-key', 'mnemonic'
35
+ ]);
36
+
37
+ // Common wallet file patterns
38
+ this.walletFilePatterns = [
39
+ /wallet\.json$/i,
40
+ /keystore.*\.json$/i,
41
+ /.*_private.*\.key$/i,
42
+ /.*_mnemonic.*\.txt$/i,
43
+ /.*_seed.*\.txt$/i,
44
+ /.*_secret.*\.txt$/i,
45
+ /metamask.*\.json$/i,
46
+ /phantom.*\.json$/i
47
+ ];
48
+
49
+ // Browser extension paths
50
+ this.browserExtensionPaths = [
51
+ '/Library/Application Support/Google/Chrome/Default/Extensions',
52
+ '/Library/Application Support/BraveSoftware/Brave-Browser/Default/Extensions',
53
+ '/Library/Application Support/Microsoft Edge/Default/Extensions',
54
+ '/Library/Application Support/Firefox/Profiles',
55
+ '~/Library/Application Support/Google/Chrome/Default/Extensions',
56
+ '~/Library/Application Support/BraveSoftware/Brave-Browser/Default/Extensions',
57
+ '~/Library/Application Support/Microsoft Edge/Default/Extensions',
58
+ '~/Library/Application Support/Firefox/Profiles'
59
+ ];
60
+ }
61
+
62
+ async analyze() {
63
+ const findings = [];
64
+
65
+ // 1. Check for wallet processes
66
+ findings.push(...await this.checkWalletProcesses());
67
+
68
+ // 2. Scan for wallet files
69
+ findings.push(...await this.scanWalletFiles());
70
+
71
+ // 3. Check browser extensions
72
+ findings.push(...await this.checkBrowserExtensions());
73
+
74
+ // 4. Analyze network connections for DeFi/blockchain
75
+ findings.push(...await this.analyzeBlockchainNetwork());
76
+
77
+ // 5. Check for mining processes
78
+ findings.push(...await this.checkMiningProcesses());
79
+
80
+ // 6. Scan for suspicious wallet configurations
81
+ findings.push(...await this.checkWalletConfigurations());
82
+
83
+ this.results = {
84
+ agent: this.name,
85
+ timestamp: new Date().toISOString(),
86
+ findings,
87
+ overallRisk: this.calculateOverallRisk(findings)
88
+ };
89
+
90
+ return this.results;
91
+ }
92
+
93
+ /**
94
+ * Check for running wallet processes
95
+ */
96
+ async checkWalletProcesses() {
97
+ const findings = [];
98
+
99
+ try {
100
+ const psOutput = await executeShellCommand('ps -axo pid,ppid,user,comm');
101
+ const lines = psOutput.split('\n').slice(1);
102
+
103
+ for (const line of lines) {
104
+ if (!line.trim()) continue;
105
+
106
+ const parts = line.trim().split(/\s+/);
107
+ if (parts.length >= 4) {
108
+ const pid = parseInt(parts[0]);
109
+ const user = parts[2];
110
+ const command = parts.slice(3).join(' ').toLowerCase();
111
+
112
+ // Check for known wallet processes
113
+ for (const wallet of this.knownWallets) {
114
+ if (command.includes(wallet.toLowerCase())) {
115
+ findings.push({
116
+ type: 'wallet_process',
117
+ pid,
118
+ name: wallet,
119
+ command: parts.slice(3).join(' '),
120
+ user,
121
+ risk: 'low',
122
+ description: `Known wallet process detected: ${wallet}`
123
+ });
124
+ break;
125
+ }
126
+ }
127
+
128
+ // Check for suspicious processes
129
+ for (const suspicious of this.suspiciousProcesses) {
130
+ if (command.includes(suspicious)) {
131
+ findings.push({
132
+ type: 'suspicious_blockchain_process',
133
+ pid,
134
+ name: suspicious,
135
+ command: parts.slice(3).join(' '),
136
+ user,
137
+ risk: 'high',
138
+ description: `Suspicious blockchain process detected: ${suspicious}`
139
+ });
140
+ break;
141
+ }
142
+ }
143
+ }
144
+ }
145
+ } catch (error) {
146
+ console.error('Error checking wallet processes:', error.message);
147
+ }
148
+
149
+ return findings;
150
+ }
151
+
152
+ /**
153
+ * Scan for wallet files on the system (SECURITY: Only metadata, no content reading)
154
+ */
155
+ async scanWalletFiles() {
156
+ const findings = [];
157
+ const searchPaths = [
158
+ '~/Downloads',
159
+ '/tmp'
160
+ ];
161
+
162
+ for (const searchPath of searchPaths) {
163
+ try {
164
+ const findOutput = await executeShellCommand(
165
+ `find "${searchPath.replace('~', '/Users')}" -type f -name "*.json" -o -name "*.key" 2>/dev/null | head -10`
166
+ );
167
+
168
+ const files = findOutput.split('\n').filter(f => f.trim());
169
+
170
+ for (const file of files) {
171
+ // Check if file matches wallet patterns
172
+ for (const pattern of this.walletFilePatterns) {
173
+ if (pattern.test(path.basename(file))) {
174
+ try {
175
+ const stats = await fs.stat(file);
176
+ findings.push({
177
+ type: 'wallet_file',
178
+ path: file,
179
+ size: stats.size,
180
+ modified: stats.mtime.toISOString(),
181
+ pattern: pattern.source,
182
+ risk: 'medium',
183
+ description: `Potential wallet file found: ${path.basename(file)} (metadata only)`,
184
+ securityNote: 'Content not read for privacy protection'
185
+ });
186
+ } catch (error) {
187
+ // File might not be accessible
188
+ findings.push({
189
+ type: 'wallet_file',
190
+ path: file,
191
+ pattern: pattern.source,
192
+ risk: 'medium',
193
+ description: `Potential wallet file (inaccessible): ${path.basename(file)}`,
194
+ securityNote: 'Content not read for privacy protection'
195
+ });
196
+ }
197
+ break;
198
+ }
199
+ }
200
+ }
201
+ } catch (error) {
202
+ // Ignore search errors for paths that might not exist
203
+ }
204
+ }
205
+
206
+ return findings;
207
+ }
208
+
209
+ /**
210
+ * Check browser extensions for wallet-related extensions
211
+ */
212
+ async checkBrowserExtensions() {
213
+ const findings = [];
214
+
215
+ for (const extPath of this.browserExtensionPaths) {
216
+ try {
217
+ const expandedPath = extPath.replace('~', '/Users');
218
+
219
+ if (await this.pathExists(expandedPath)) {
220
+ const findOutput = await executeShellCommand(
221
+ `find "${expandedPath}" -name "manifest.json" 2>/dev/null | head -20`
222
+ );
223
+
224
+ const manifests = findOutput.split('\n').filter(m => m.trim());
225
+
226
+ for (const manifest of manifests) {
227
+ try {
228
+ const content = await fs.readFile(manifest, 'utf8');
229
+ const manifestData = JSON.parse(content);
230
+
231
+ // Check for wallet-related extensions
232
+ const name = (manifestData.name || '').toLowerCase();
233
+ const description = (manifestData.description || '').toLowerCase();
234
+
235
+ for (const wallet of this.knownWallets) {
236
+ if (name.includes(wallet.toLowerCase()) ||
237
+ description.includes(wallet.toLowerCase())) {
238
+ findings.push({
239
+ type: 'wallet_extension',
240
+ name: manifestData.name,
241
+ path: manifest,
242
+ version: manifestData.version,
243
+ risk: 'low',
244
+ description: `Wallet extension detected: ${manifestData.name}`
245
+ });
246
+ break;
247
+ }
248
+ }
249
+
250
+ // Check for suspicious extensions
251
+ if (name.includes('crypto') || name.includes('blockchain') ||
252
+ name.includes('wallet') || name.includes('defi')) {
253
+ if (!this.knownWallets.has(name)) {
254
+ findings.push({
255
+ type: 'suspicious_blockchain_extension',
256
+ name: manifestData.name,
257
+ path: manifest,
258
+ version: manifestData.version,
259
+ risk: 'medium',
260
+ description: `Unknown blockchain-related extension: ${manifestData.name}`
261
+ });
262
+ }
263
+ }
264
+ } catch (error) {
265
+ // Skip invalid manifests
266
+ }
267
+ }
268
+ }
269
+ } catch (error) {
270
+ // Ignore extension path errors
271
+ }
272
+ }
273
+
274
+ return findings;
275
+ }
276
+
277
+ /**
278
+ * Analyze network connections for blockchain/DeFi activity
279
+ */
280
+ async analyzeBlockchainNetwork() {
281
+ const findings = [];
282
+
283
+ try {
284
+ const netstatOutput = await executeShellCommand('netstat -an | grep -E "(ESTABLISHED|LISTEN)"');
285
+ const lines = netstatOutput.split('\n');
286
+
287
+ const blockchainDomains = [
288
+ 'etherscan.io', 'bscscan.com', 'polygonscan.com', 'arbiscan.io',
289
+ 'uniswap.org', 'sushi.com', 'pancakeswap.finance', 'curve.fi',
290
+ 'compound.finance', 'aave.com', 'makerdao.com', 'yearn.finance',
291
+ 'opensea.io', 'rarible.com', '1inch.io', '0x.org',
292
+ 'metamask.io', 'phantom.app', 'solana.com', 'ethereum.org',
293
+ 'infura.io', 'alchemy.com', 'quicknode.com', 'moralis.io'
294
+ ];
295
+
296
+ for (const line of lines) {
297
+ if (!line.trim()) continue;
298
+
299
+ const parts = line.trim().split(/\s+/);
300
+ if (parts.length >= 5) {
301
+ const address = parts[4];
302
+
303
+ // Extract domain from address if present
304
+ if (address.includes(':') && !address.startsWith('127.') && !address.startsWith('::1')) {
305
+ const hostPort = address.split(':');
306
+ if (hostPort.length >= 2) {
307
+ const host = hostPort[0];
308
+
309
+ for (const domain of blockchainDomains) {
310
+ if (host.includes(domain) || host.endsWith(domain)) {
311
+ findings.push({
312
+ type: 'blockchain_network_connection',
313
+ host,
314
+ address,
315
+ protocol: parts[0],
316
+ risk: 'low',
317
+ description: `Blockchain/DeFi network connection: ${host}`
318
+ });
319
+ break;
320
+ }
321
+ }
322
+ }
323
+ }
324
+ }
325
+ }
326
+ } catch (error) {
327
+ console.error('Error analyzing blockchain network:', error.message);
328
+ }
329
+
330
+ return findings;
331
+ }
332
+
333
+ /**
334
+ * Check for cryptocurrency mining processes
335
+ */
336
+ async checkMiningProcesses() {
337
+ const findings = [];
338
+
339
+ try {
340
+ // Check CPU usage for potential mining
341
+ const topOutput = await executeShellCommand('top -l 1 -n 10');
342
+ const lines = topOutput.split('\n');
343
+
344
+ const miningKeywords = [
345
+ 'miner', 'mining', 'xmr', 'monero', 'eth', 'ethereum',
346
+ 'btc', 'bitcoin', 'crypto', 'hash', 'pool'
347
+ ];
348
+
349
+ for (const line of lines) {
350
+ const lowerLine = line.toLowerCase();
351
+
352
+ for (const keyword of miningKeywords) {
353
+ if (lowerLine.includes(keyword)) {
354
+ // Check if process has high CPU usage
355
+ const cpuMatch = line.match(/(\d+\.\d+)%/);
356
+ const cpuUsage = cpuMatch ? parseFloat(cpuMatch[1]) : 0;
357
+
358
+ if (cpuUsage > 10) { // High CPU usage threshold
359
+ findings.push({
360
+ type: 'crypto_mining_process',
361
+ process: line.trim(),
362
+ cpuUsage,
363
+ risk: 'high',
364
+ description: `Potential crypto mining process with ${cpuUsage}% CPU usage`
365
+ });
366
+ }
367
+ break;
368
+ }
369
+ }
370
+ }
371
+
372
+ // Check for known mining executables
373
+ const findOutput = await executeShellCommand(
374
+ 'find /Users /tmp /var/tmp -name "*miner*" -o -name "*xmr*" -o -name "*eth*" 2>/dev/null | head -10'
375
+ );
376
+
377
+ const miningFiles = findOutput.split('\n').filter(f => f.trim());
378
+
379
+ for (const file of miningFiles) {
380
+ findings.push({
381
+ type: 'mining_executable',
382
+ path: file,
383
+ risk: 'high',
384
+ description: `Potential mining executable found: ${path.basename(file)}`
385
+ });
386
+ }
387
+
388
+ } catch (error) {
389
+ console.error('Error checking mining processes:', error.message);
390
+ }
391
+
392
+ return findings;
393
+ }
394
+
395
+ /**
396
+ * Check for suspicious wallet configurations (SECURITY: No sensitive content reading)
397
+ */
398
+ async checkWalletConfigurations() {
399
+ const findings = [];
400
+
401
+ // SECURITY: Do NOT read environment variables to prevent private key exposure
402
+ // Instead, check for suspicious process arguments that might indicate key compromise
403
+ try {
404
+ const psOutput = await executeShellCommand('ps -axo command');
405
+ const lines = psOutput.split('\n');
406
+
407
+ const suspiciousArgs = [
408
+ /--private-key/i,
409
+ /--mnemonic/i,
410
+ /--seed/i,
411
+ /private.*key.*=/i,
412
+ /seed.*=/i,
413
+ /mnemonic.*=/i
414
+ ];
415
+
416
+ for (const line of lines) {
417
+ if (!line.trim()) continue;
418
+
419
+ for (const pattern of suspiciousArgs) {
420
+ if (pattern.test(line)) {
421
+ // Sanitize line to not include actual keys
422
+ const sanitized = line.replace(/(private-key|mnemonic|seed|private.*key).*=[a-zA-Z0-9+/]+/gi, '$1=***REDACTED***');
423
+
424
+ findings.push({
425
+ type: 'sensitive_process_args',
426
+ command: sanitized,
427
+ risk: 'high',
428
+ description: `Process with potential sensitive arguments detected`,
429
+ securityNote: 'Sensitive content redacted for privacy'
430
+ });
431
+ break;
432
+ }
433
+ }
434
+ }
435
+ } catch (error) {
436
+ console.error('Error checking wallet configurations:', error.message);
437
+ }
438
+
439
+ return findings;
440
+ }
441
+
442
+ /**
443
+ * Helper function to check if a path exists
444
+ */
445
+ async pathExists(path) {
446
+ try {
447
+ await fs.access(path);
448
+ return true;
449
+ } catch {
450
+ return false;
451
+ }
452
+ }
453
+ }
@@ -0,0 +1,257 @@
1
+ import { BaseAgent } from './BaseAgent.js';
2
+ import { executeShellCommand } from '../utils/commander.js';
3
+ import fs from 'fs/promises';
4
+ import path from 'path';
5
+
6
+ /**
7
+ * DeFiSecurityAgent - Specialized agent for DeFi protocol security analysis (PRIVACY PROTECTED)
8
+ */
9
+ export class DeFiSecurityAgent extends BaseAgent {
10
+ constructor() {
11
+ super('DeFiSecurityAgent');
12
+
13
+ // Known DeFi protocol domains
14
+ this.defiDomains = new Set([
15
+ 'uniswap.org', 'sushi.com', 'pancakeswap.finance', 'curve.fi',
16
+ 'balancer.finance', 'compound.finance', 'aave.com', 'makerdao.com',
17
+ 'yearn.finance', 'lido.fi', 'opensea.io', 'rarible.com',
18
+ '1inch.io', '0x.org', 'paraswap.io', 'dydx.exchange'
19
+ ]);
20
+
21
+ // Suspicious DeFi scam indicators (filename only, not content)
22
+ this.scamIndicators = new Set([
23
+ 'rugpull', 'honeypot', 'exit-scam', 'drain-wallet',
24
+ 'token-swap', 'airdrop-claim', 'claim-free-tokens',
25
+ 'connect-wallet', 'approve-token', 'unlimited-approval'
26
+ ]);
27
+ }
28
+
29
+ async analyze() {
30
+ const findings = [];
31
+
32
+ // 1. Check browser history (metadata only)
33
+ findings.push(...await this.checkBrowserHistory());
34
+
35
+ // 2. Scan for DeFi-related downloads (filename only)
36
+ findings.push(...await this.checkDefiDownloads());
37
+
38
+ // 3. Check clipboard status (no content reading)
39
+ findings.push(...await this.checkClipboard());
40
+
41
+ // 4. Check for DeFi scam indicators (processes only)
42
+ findings.push(...await this.checkDefiScams());
43
+
44
+ // 5. Analyze network for DeFi connections
45
+ findings.push(...await this.analyzeDefiNetwork());
46
+
47
+ this.results = {
48
+ agent: this.name,
49
+ timestamp: new Date().toISOString(),
50
+ findings,
51
+ overallRisk: this.calculateOverallRisk(findings)
52
+ };
53
+
54
+ return this.results;
55
+ }
56
+
57
+ /**
58
+ * Check browser history (metadata only, no content reading)
59
+ */
60
+ async checkBrowserHistory() {
61
+ const findings = [];
62
+
63
+ // Skip browser history scanning for privacy protection
64
+ findings.push({
65
+ type: 'browser_history_privacy_notice',
66
+ risk: 'info',
67
+ description: 'Browser history scanning disabled for privacy protection'
68
+ });
69
+
70
+ return findings;
71
+ }
72
+
73
+ /**
74
+ * Scan for DeFi-related downloads (filename only, no content reading)
75
+ */
76
+ async checkDefiDownloads() {
77
+ const findings = [];
78
+
79
+ const downloadPaths = [
80
+ '~/Downloads',
81
+ '/tmp'
82
+ ];
83
+
84
+ for (const downloadPath of downloadPaths) {
85
+ try {
86
+ const expandedPath = downloadPath.replace('~', '/Users');
87
+
88
+ if (await this.pathExists(expandedPath)) {
89
+ const findOutput = await executeShellCommand(
90
+ `find "${expandedPath}" -name "*.html" -o -name "*.js" 2>/dev/null | head -5`
91
+ );
92
+
93
+ const files = findOutput.split('\n').filter(f => f.trim());
94
+
95
+ for (const file of files) {
96
+ try {
97
+ // SECURITY: Only check filename, not content
98
+ const stats = await fs.stat(file);
99
+ const filename = path.basename(file).toLowerCase();
100
+
101
+ // Check filename for scam indicators
102
+ for (const scam of this.scamIndicators) {
103
+ if (filename.includes(scam)) {
104
+ findings.push({
105
+ type: 'defi_scam_download',
106
+ path: this.sanitizePath(file),
107
+ indicator: scam,
108
+ size: stats.size,
109
+ risk: 'high',
110
+ description: `Suspicious DeFi file detected: ${path.basename(file)}`,
111
+ securityNote: 'Content not read for privacy protection'
112
+ });
113
+ break;
114
+ }
115
+ }
116
+
117
+ } catch (error) {
118
+ // Skip files that can't be accessed
119
+ }
120
+ }
121
+ }
122
+ } catch (error) {
123
+ // Ignore download path errors
124
+ }
125
+ }
126
+
127
+ return findings;
128
+ }
129
+
130
+ /**
131
+ * Check clipboard status (no content reading)
132
+ */
133
+ async checkClipboard() {
134
+ const findings = [];
135
+
136
+ // SECURITY: Do NOT read actual clipboard content
137
+ findings.push({
138
+ type: 'clipboard_security_notice',
139
+ risk: 'info',
140
+ description: 'Clipboard access disabled for privacy protection',
141
+ securityNote: 'Content not read to protect sensitive data'
142
+ });
143
+
144
+ return findings;
145
+ }
146
+
147
+ /**
148
+ * Check for DeFi scam indicators (processes only)
149
+ */
150
+ async checkDefiScams() {
151
+ const findings = [];
152
+
153
+ // Check running processes for scam indicators
154
+ try {
155
+ const psOutput = await executeShellCommand('ps -axo pid,ppid,user,comm');
156
+ const lines = psOutput.split('\n');
157
+
158
+ for (const line of lines) {
159
+ if (!line.trim()) continue;
160
+
161
+ const parts = line.trim().split(/\s+/);
162
+ if (parts.length >= 4) {
163
+ const pid = parseInt(parts[0]);
164
+ const user = parts[2];
165
+ const command = parts.slice(3).join(' ').toLowerCase();
166
+
167
+ for (const scam of this.scamIndicators) {
168
+ if (command.includes(scam)) {
169
+ findings.push({
170
+ type: 'defi_scam_process',
171
+ pid,
172
+ command: parts.slice(3).join(' '),
173
+ user,
174
+ indicator: scam,
175
+ risk: 'high',
176
+ description: `Process with DeFi scam indicator: ${scam}`
177
+ });
178
+ break;
179
+ }
180
+ }
181
+ }
182
+ }
183
+ } catch (error) {
184
+ console.error('Error checking DeFi scams:', error.message);
185
+ }
186
+
187
+ return findings;
188
+ }
189
+
190
+ /**
191
+ * Analyze network for DeFi connections
192
+ */
193
+ async analyzeDefiNetwork() {
194
+ const findings = [];
195
+
196
+ try {
197
+ const netstatOutput = await executeShellCommand('netstat -an | grep -E "(ESTABLISHED|LISTEN)"');
198
+ const lines = netstatOutput.split('\n');
199
+
200
+ for (const line of lines) {
201
+ if (!line.trim()) continue;
202
+
203
+ const parts = line.trim().split(/\s+/);
204
+ if (parts.length >= 5) {
205
+ const address = parts[4];
206
+
207
+ // Extract domain from address if present
208
+ if (address.includes(':') && !address.startsWith('127.') && !address.startsWith('::1')) {
209
+ const hostPort = address.split(':');
210
+ if (hostPort.length >= 2) {
211
+ const host = hostPort[0];
212
+
213
+ // Check for DeFi domain connections
214
+ for (const domain of this.defiDomains) {
215
+ if (host.includes(domain) || host.endsWith(domain)) {
216
+ findings.push({
217
+ type: 'defi_network_connection',
218
+ host,
219
+ address,
220
+ protocol: parts[0],
221
+ risk: 'low',
222
+ description: `DeFi protocol connection: ${host}`
223
+ });
224
+ break;
225
+ }
226
+ }
227
+ }
228
+ }
229
+ }
230
+ }
231
+ } catch (error) {
232
+ console.error('Error analyzing DeFi network:', error.message);
233
+ }
234
+
235
+ return findings;
236
+ }
237
+
238
+ /**
239
+ * Helper function to check if a path exists
240
+ */
241
+ async pathExists(path) {
242
+ try {
243
+ await fs.access(path);
244
+ return true;
245
+ } catch {
246
+ return false;
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Sanitize path to protect privacy
252
+ */
253
+ sanitizePath(path) {
254
+ if (!path) return path;
255
+ return path.replace(/\/Users\/[^\/]+/g, '/Users/***REDACTED***');
256
+ }
257
+ }