@kevinlilili/agent-wallet 0.1.0 → 0.2.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.
Files changed (2) hide show
  1. package/index.js +342 -48
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -10,7 +10,7 @@
10
10
  * Security: ONLY reads public addresses. Never reads private keys.
11
11
  */
12
12
 
13
- const { readFileSync, existsSync, readdirSync } = require("fs");
13
+ const { readFileSync, existsSync, readdirSync, statSync } = require("fs");
14
14
  const { homedir } = require("os");
15
15
  const { join } = require("path");
16
16
  const { execSync } = require("child_process");
@@ -44,37 +44,74 @@ function scanAgentCash() {
44
44
  return wallets;
45
45
  }
46
46
 
47
- function scanSponge() {
47
+ function scanMCPWallets() {
48
48
  const wallets = [];
49
+ const seen = new Set();
49
50
 
50
- // Sponge stores wallet in ~/.sponge/ or via MCP config env vars
51
- const spongeWallet = readJson(join(HOME, ".sponge", "wallet.json"));
52
- if (spongeWallet && typeof spongeWallet.address === "string") {
53
- wallets.push({ provider: "Sponge", address: spongeWallet.address, chain: "evm" });
54
- }
55
-
56
- // Check MCP configs for Sponge server env vars
51
+ // All known MCP config file locations and their display labels
57
52
  const mcpConfigs = [
58
- join(HOME, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
59
- join(HOME, ".cursor", "mcp.json"),
53
+ { path: join(HOME, "Library", "Application Support", "Claude", "claude_desktop_config.json"), label: "Claude Desktop" },
54
+ { path: join(HOME, ".cursor", "mcp.json"), label: "Cursor" },
55
+ { path: join(HOME, ".claude.json"), label: "Claude Code" },
60
56
  ];
61
57
 
62
- for (const configPath of mcpConfigs) {
58
+ // Helpers to classify addresses
59
+ const isEvmAddress = (val) =>
60
+ typeof val === "string" && /^0x[a-fA-F0-9]{40}$/.test(val);
61
+
62
+ const isSolanaAddress = (val) =>
63
+ typeof val === "string" && !val.startsWith("0x") &&
64
+ /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(val);
65
+
66
+ const isPrivateKeyVar = (key) =>
67
+ key.toUpperCase().includes("PRIVATE_KEY");
68
+
69
+ const isWalletVar = (key) => {
70
+ const upper = key.toUpperCase();
71
+ return upper.endsWith("_ADDRESS") || upper.endsWith("_WALLET") || upper.endsWith("_WALLET_ADDRESS");
72
+ };
73
+
74
+ function addWallet(provider, address, chain) {
75
+ if (seen.has(address)) return;
76
+ seen.add(address);
77
+ wallets.push({ provider, address, chain });
78
+ }
79
+
80
+ for (const { path: configPath, label } of mcpConfigs) {
63
81
  const data = readJson(configPath);
64
82
  if (!data) continue;
65
83
  const servers = data.mcpServers;
66
- if (!servers) continue;
67
-
68
- for (const [name, config] of Object.entries(servers)) {
69
- if (!name.toLowerCase().includes("sponge")) continue;
70
- const env = config.env;
71
- if (!env) continue;
72
-
73
- // Look for wallet address in env vars
74
- for (const [key, val] of Object.entries(env)) {
75
- if (typeof val === "string" && val.startsWith("0x") && val.length === 42 &&
76
- (key.includes("ADDRESS") || key.includes("WALLET"))) {
77
- wallets.push({ provider: `Sponge (${name})`, address: val, chain: "evm" });
84
+ if (!servers || typeof servers !== "object") continue;
85
+
86
+ for (const [serverName, config] of Object.entries(servers)) {
87
+ if (!config || typeof config !== "object") continue;
88
+ const providerLabel = `${serverName} (${label})`;
89
+
90
+ // Scan env vars for wallet addresses
91
+ if (config.env && typeof config.env === "object") {
92
+ for (const [key, val] of Object.entries(config.env)) {
93
+ // SECURITY: never read private keys
94
+ if (isPrivateKeyVar(key)) continue;
95
+
96
+ if (isWalletVar(key)) {
97
+ if (isEvmAddress(val)) {
98
+ addWallet(providerLabel, val, "evm");
99
+ } else if (isSolanaAddress(val)) {
100
+ addWallet(providerLabel, val, "solana");
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ // Scan args array for wallet addresses passed as CLI arguments
107
+ if (Array.isArray(config.args)) {
108
+ for (const arg of config.args) {
109
+ if (typeof arg !== "string") continue;
110
+ if (isEvmAddress(arg)) {
111
+ addWallet(providerLabel, arg, "evm");
112
+ } else if (isSolanaAddress(arg)) {
113
+ addWallet(providerLabel, arg, "solana");
114
+ }
78
115
  }
79
116
  }
80
117
  }
@@ -179,6 +216,193 @@ function scanEnvFiles() {
179
216
  return wallets;
180
217
  }
181
218
 
219
+ // ── Security Scanner ────────────────────────────────────────────
220
+
221
+ const SEVERITY = { CRITICAL: "critical", WARNING: "warning", INFO: "info" };
222
+ const RED = "\x1b[31m";
223
+
224
+ function securityScan(wallets) {
225
+ const findings = [];
226
+
227
+ // ── 1. Plaintext private keys in wallet files ──
228
+ const walletFiles = [
229
+ { path: join(HOME, ".agentcash", "wallet.json"), provider: "AgentCash", keyFields: ["privateKey", "private_key", "secretKey"] },
230
+ { path: join(HOME, ".agentcash", "solana-wallet.json"), provider: "AgentCash (SOL)", keyFields: ["privateKey", "private_key", "secretKey"] },
231
+ { path: join(HOME, ".cdp", "wallet_data.json"), provider: "Coinbase CDP", keyFields: ["privateKey", "private_key", "seed"] },
232
+ { path: join(HOME, ".latinum", "config.json"), provider: "Latinum", keyFields: ["privateKey", "private_key"] },
233
+ ];
234
+
235
+ // Also check wallet_data_*.txt files
236
+ const searchDirs = [HOME, join(HOME, "Projects"), join(HOME, "projects"), join(HOME, "code"), join(HOME, "dev")];
237
+ for (const dir of searchDirs) {
238
+ if (!existsSync(dir)) continue;
239
+ try {
240
+ for (const file of readdirSync(dir)) {
241
+ if (file.startsWith("wallet_data_") && file.endsWith(".txt")) {
242
+ walletFiles.push({ path: join(dir, file), provider: `AgentKit (${file})`, keyFields: ["privateKey", "private_key", "seed", "export_data"] });
243
+ }
244
+ }
245
+ } catch { /* skip */ }
246
+ }
247
+
248
+ for (const { path: fp, provider, keyFields } of walletFiles) {
249
+ if (!existsSync(fp)) continue;
250
+ const data = readJson(fp);
251
+ if (!data || typeof data !== "object") continue;
252
+
253
+ const exposedKeys = keyFields.filter((k) => data[k] && typeof data[k] === "string" && data[k].length > 10);
254
+ if (exposedKeys.length > 0) {
255
+ findings.push({
256
+ severity: SEVERITY.CRITICAL,
257
+ title: `Plaintext private key in ${provider} wallet file`,
258
+ detail: fp.replace(HOME, "~"),
259
+ fix: "Encrypt this file or move private key to a hardware wallet / secure vault",
260
+ });
261
+ }
262
+
263
+ // Check file permissions (should be 600, not world/group readable)
264
+ try {
265
+ const mode = statSync(fp).mode;
266
+ const groupRead = mode & 0o040;
267
+ const otherRead = mode & 0o004;
268
+ if (groupRead || otherRead) {
269
+ findings.push({
270
+ severity: SEVERITY.WARNING,
271
+ title: `${provider} wallet file is readable by other users`,
272
+ detail: `${fp.replace(HOME, "~")} — permissions too open`,
273
+ fix: `Run: chmod 600 "${fp}"`,
274
+ });
275
+ }
276
+ } catch { /* skip */ }
277
+ }
278
+
279
+ // ── 2. Private keys in .env files ──
280
+ const envDirs = [process.cwd(), HOME, join(HOME, "Projects"), join(HOME, "projects")];
281
+ for (const dir of envDirs) {
282
+ const envPath = join(dir, ".env");
283
+ if (!existsSync(envPath)) continue;
284
+ try {
285
+ const lines = readFileSync(envPath, "utf-8").split("\n");
286
+ for (const line of lines) {
287
+ if (line.trim().startsWith("#")) continue;
288
+ const match = line.match(/^([A-Za-z_]*(?:PRIVATE_KEY|SECRET_KEY|MNEMONIC|SEED_PHRASE)[A-Za-z_]*)=/i);
289
+ if (match) {
290
+ findings.push({
291
+ severity: SEVERITY.CRITICAL,
292
+ title: `Private key variable in .env file`,
293
+ detail: `${envPath.replace(HOME, "~")} → ${match[1]}`,
294
+ fix: "Move to encrypted secrets manager or hardware wallet",
295
+ });
296
+ }
297
+ }
298
+ } catch { /* skip */ }
299
+ }
300
+
301
+ // ── 3. Private keys leaked into MCP configs ──
302
+ const mcpConfigs = [
303
+ { path: join(HOME, "Library", "Application Support", "Claude", "claude_desktop_config.json"), label: "Claude Desktop" },
304
+ { path: join(HOME, ".cursor", "mcp.json"), label: "Cursor" },
305
+ { path: join(HOME, ".claude.json"), label: "Claude Code" },
306
+ ];
307
+ for (const { path: configPath, label } of mcpConfigs) {
308
+ const data = readJson(configPath);
309
+ if (!data || !data.mcpServers) continue;
310
+ for (const [serverName, config] of Object.entries(data.mcpServers)) {
311
+ if (!config || typeof config !== "object") continue;
312
+ const env = config.env || {};
313
+ for (const key of Object.keys(env)) {
314
+ const upper = key.toUpperCase();
315
+ if (upper.includes("PRIVATE_KEY") || upper.includes("SECRET_KEY") || upper.includes("MNEMONIC") || upper.includes("SEED_PHRASE")) {
316
+ findings.push({
317
+ severity: SEVERITY.CRITICAL,
318
+ title: `Private key in MCP config (${label})`,
319
+ detail: `Server "${serverName}" → env.${key}`,
320
+ fix: "Remove from config, use encrypted storage instead",
321
+ });
322
+ }
323
+ }
324
+ }
325
+ }
326
+
327
+ // ── 4. Wallet files in dangerous locations ──
328
+ const dangerousDirs = ["Downloads", "Desktop", "Documents", "tmp"];
329
+ for (const dirName of dangerousDirs) {
330
+ const dir = join(HOME, dirName);
331
+ if (!existsSync(dir)) continue;
332
+ try {
333
+ for (const file of readdirSync(dir)) {
334
+ const lower = file.toLowerCase();
335
+ if (lower.includes("private_key") || lower.includes("keystore") || lower.includes("seed_phrase") ||
336
+ (lower.includes("wallet") && (lower.endsWith(".json") || lower.endsWith(".txt") || lower.endsWith(".csv")) && lower.includes("key"))) {
337
+ findings.push({
338
+ severity: SEVERITY.WARNING,
339
+ title: `Suspicious wallet/key file in ~/${dirName}`,
340
+ detail: `~/${dirName}/${file}`,
341
+ fix: "Move to a secure location or delete if no longer needed",
342
+ });
343
+ }
344
+ }
345
+ } catch { /* skip */ }
346
+ }
347
+
348
+ // ── 5. Git exposure — wallet dirs not gitignored ──
349
+ const sensitiveDirs = [
350
+ { path: join(HOME, ".agentcash"), name: ".agentcash" },
351
+ { path: join(HOME, ".cdp"), name: ".cdp" },
352
+ { path: join(HOME, ".wallet-agent"), name: ".wallet-agent" },
353
+ { path: join(HOME, ".openclaw"), name: ".openclaw" },
354
+ { path: join(HOME, ".latinum"), name: ".latinum" },
355
+ ];
356
+ for (const { path: dp, name } of sensitiveDirs) {
357
+ if (!existsSync(dp)) continue;
358
+ try {
359
+ const result = execSync(`git -C "${HOME}" check-ignore -q "${dp}" 2>/dev/null`, { stdio: "pipe" });
360
+ } catch {
361
+ // Non-zero exit = not ignored
362
+ findings.push({
363
+ severity: SEVERITY.WARNING,
364
+ title: `~/${name}/ is not git-ignored`,
365
+ detail: "Could accidentally be committed to a repository",
366
+ fix: `Add "${name}/" to your global .gitignore`,
367
+ });
368
+ }
369
+ }
370
+
371
+ // ── 6. wallet-agent: check if keys are actually encrypted ──
372
+ const waPath = join(HOME, ".wallet-agent", "auth", "encrypted-keys.json");
373
+ if (existsSync(waPath)) {
374
+ const data = readJson(waPath);
375
+ if (data && data.keys) {
376
+ const allEncrypted = Object.values(data.keys).every((v) => typeof v === "string" && v.length > 100);
377
+ if (allEncrypted) {
378
+ findings.push({
379
+ severity: SEVERITY.INFO,
380
+ title: "wallet-agent keys appear encrypted",
381
+ detail: waPath.replace(HOME, "~"),
382
+ fix: null,
383
+ });
384
+ }
385
+ }
386
+ }
387
+
388
+ // ── Calculate security score ──
389
+ const criticalCount = findings.filter((f) => f.severity === SEVERITY.CRITICAL).length;
390
+ const warningCount = findings.filter((f) => f.severity === SEVERITY.WARNING).length;
391
+ let score = 100;
392
+ score -= criticalCount * 25;
393
+ score -= warningCount * 10;
394
+ score = Math.max(0, Math.min(100, score));
395
+
396
+ let grade;
397
+ if (score >= 90) grade = "A";
398
+ else if (score >= 75) grade = "B";
399
+ else if (score >= 50) grade = "C";
400
+ else if (score >= 25) grade = "D";
401
+ else grade = "F";
402
+
403
+ return { findings, score, grade, criticalCount, warningCount };
404
+ }
405
+
182
406
  // ── Output helpers ──────────────────────────────────────────────
183
407
 
184
408
  const RESET = "\x1b[0m";
@@ -194,6 +418,56 @@ function shortAddr(addr) {
194
418
 
195
419
  // ── Main ────────────────────────────────────────────────────────
196
420
 
421
+ function printSecurityReport(security) {
422
+ const { findings, score, grade, criticalCount, warningCount } = security;
423
+
424
+ console.log(` ${BOLD}SECURITY SCAN${RESET}`);
425
+ console.log("");
426
+
427
+ if (findings.length === 0) {
428
+ console.log(` ${GREEN}✓ No security issues found${RESET}`);
429
+ } else {
430
+ for (const f of findings) {
431
+ let icon, color;
432
+ if (f.severity === SEVERITY.CRITICAL) { icon = "✗"; color = RED; }
433
+ else if (f.severity === SEVERITY.WARNING) { icon = "!"; color = YELLOW; }
434
+ else { icon = "✓"; color = GREEN; }
435
+
436
+ console.log(` ${color}${icon}${RESET} ${f.title}`);
437
+ console.log(` ${DIM}${f.detail}${RESET}`);
438
+ if (f.fix) {
439
+ console.log(` ${DIM}Fix: ${f.fix}${RESET}`);
440
+ }
441
+ console.log("");
442
+ }
443
+ }
444
+
445
+ // Score bar
446
+ const barLen = 20;
447
+ const filled = Math.round((score / 100) * barLen);
448
+ const empty = barLen - filled;
449
+ let barColor = score >= 75 ? GREEN : score >= 50 ? YELLOW : RED;
450
+ const bar = barColor + "█".repeat(filled) + DIM + "░".repeat(empty) + RESET;
451
+
452
+ console.log(` Security Score: ${BOLD}${grade}${RESET} (${score}/100) ${bar}`);
453
+ if (criticalCount > 0) console.log(` ${RED}${criticalCount} critical${RESET} · ${warningCount} warnings`);
454
+ else if (warningCount > 0) console.log(` ${YELLOW}${warningCount} warning(s)${RESET}`);
455
+ console.log("");
456
+ }
457
+
458
+ function generateShareableText(unique, security) {
459
+ const chainSet = new Set(unique.map((w) => w.chain === "solana" ? "SOL" : "EVM"));
460
+ const lines = [
461
+ `Agent Wallet Security: ${security.grade} (${security.score}/100)`,
462
+ `${unique.length} wallets · ${chainSet.size} chain types`,
463
+ ];
464
+ if (security.criticalCount > 0) lines.push(`⚠ ${security.criticalCount} critical issues found`);
465
+ else lines.push(`✓ No critical issues`);
466
+ lines.push("");
467
+ lines.push("Scan yours: npx @kevinlilili/agent-wallet");
468
+ return lines.join("\n");
469
+ }
470
+
197
471
  function main() {
198
472
  const args = process.argv.slice(2);
199
473
  const dashboardUrl = args.find((a) => a.startsWith("--url="))?.split("=")[1]
@@ -201,16 +475,19 @@ function main() {
201
475
  || "https://agent-wallet-dashboard.vercel.app";
202
476
  const noBrowser = args.includes("--no-open");
203
477
  const jsonOutput = args.includes("--json");
478
+ const securityOnly = args.includes("--security");
479
+ const noSecurity = args.includes("--no-security");
204
480
 
205
481
  console.log("");
206
482
  console.log(` ${BOLD}Agent Wallet Scanner${RESET}`);
207
- console.log(` ${DIM}Scanning local configs for agent wallet addresses...${RESET}`);
483
+ console.log(` ${DIM}Scanning local configs for agent wallets & security issues...${RESET}`);
208
484
  console.log("");
209
485
 
486
+ // ── Wallet Discovery ──
210
487
  const allWallets = [];
211
488
  const scanners = [
212
489
  { name: "AgentCash", fn: scanAgentCash },
213
- { name: "Sponge", fn: scanSponge },
490
+ { name: "MCP Wallets", fn: scanMCPWallets },
214
491
  { name: "Coinbase AgentKit", fn: scanCoinbaseAgentKit },
215
492
  { name: "wallet-agent", fn: scanWalletAgent },
216
493
  { name: "OpenClaw", fn: scanOpenClaw },
@@ -218,39 +495,57 @@ function main() {
218
495
  { name: ".env file", fn: scanEnvFiles },
219
496
  ];
220
497
 
221
- for (const scanner of scanners) {
222
- const found = scanner.fn();
223
- if (found.length > 0) {
224
- console.log(` ${GREEN}✓${RESET} ${scanner.name}: ${found.length} wallet(s)`);
225
- allWallets.push(...found);
498
+ if (!securityOnly) {
499
+ for (const scanner of scanners) {
500
+ const found = scanner.fn();
501
+ if (found.length > 0) {
502
+ console.log(` ${GREEN}✓${RESET} ${scanner.name}: ${found.length} wallet(s)`);
503
+ allWallets.push(...found);
504
+ }
226
505
  }
227
506
  }
228
507
 
229
- // Deduplicate by address
230
508
  const unique = Array.from(new Map(allWallets.map((w) => [w.address, w])).values());
231
509
 
232
- if (unique.length === 0) {
233
- console.log(` ${DIM}No agent wallets found.${RESET}`);
510
+ if (!securityOnly && unique.length > 0) {
234
511
  console.log("");
235
- console.log(` ${DIM}Tip: Install an agent wallet provider (AgentCash, Sponge, etc.)${RESET}`);
236
- console.log(` ${DIM}or open the dashboard manually: ${dashboardUrl}${RESET}`);
512
+ console.log(` ${BOLD}${unique.length} wallet(s) found${RESET}`);
513
+ console.log("");
514
+
515
+ for (const w of unique) {
516
+ const badge = w.chain === "solana" ? `${YELLOW}SOL${RESET}` : `${CYAN}EVM${RESET}`;
517
+ console.log(` ${w.provider.padEnd(20)} ${DIM}${shortAddr(w.address)}${RESET} ${badge}`);
518
+ }
519
+ console.log("");
520
+ } else if (!securityOnly && unique.length === 0) {
521
+ console.log(` ${DIM}No agent wallets found.${RESET}`);
237
522
  console.log("");
238
- process.exit(0);
239
523
  }
240
524
 
241
- console.log("");
242
- console.log(` ${BOLD}${unique.length} wallet(s) found${RESET}`);
243
- console.log("");
525
+ // ── Security Scan ──
526
+ if (!noSecurity) {
527
+ const security = securityScan(unique);
528
+ printSecurityReport(security);
529
+
530
+ // Shareable text
531
+ if (unique.length > 0) {
532
+ const shareText = generateShareableText(unique, security);
533
+ console.log(` ${DIM}── Share ──${RESET}`);
534
+ console.log("");
535
+ shareText.split("\n").forEach((l) => console.log(` ${l}`));
536
+ console.log("");
537
+ }
244
538
 
245
- for (const w of unique) {
246
- const badge = w.chain === "solana" ? `${YELLOW}SOL${RESET}` : `${CYAN}EVM${RESET}`;
247
- console.log(` ${w.provider.padEnd(20)} ${DIM}${shortAddr(w.address)}${RESET} ${badge}`);
539
+ if (jsonOutput) {
540
+ console.log(JSON.stringify({ wallets: unique, security: { score: security.score, grade: security.grade, findings: security.findings } }, null, 2));
541
+ process.exit(0);
542
+ }
543
+ } else if (jsonOutput) {
544
+ console.log(JSON.stringify(unique, null, 2));
545
+ process.exit(0);
248
546
  }
249
547
 
250
- // JSON output mode (for piping)
251
- if (jsonOutput) {
252
- console.log("");
253
- console.log(JSON.stringify(unique, null, 2));
548
+ if (securityOnly || unique.length === 0) {
254
549
  process.exit(0);
255
550
  }
256
551
 
@@ -260,7 +555,6 @@ function main() {
260
555
  .join("&");
261
556
  const url = `${dashboardUrl}?${params}`;
262
557
 
263
- console.log("");
264
558
  console.log(` ${DIM}Dashboard:${RESET} ${url}`);
265
559
  console.log("");
266
560
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kevinlilili/agent-wallet",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Scan your machine for AI agent wallets and see them in one dashboard",
5
5
  "bin": {
6
6
  "agent-wallet": "index.js"