httpcat-cli 0.2.13 → 0.3.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 (63) hide show
  1. package/README.md +9 -9
  2. package/bun.lock +13 -1308
  3. package/dist/agent/tools.d.ts.map +1 -1
  4. package/dist/agent/tools.js +87 -5
  5. package/dist/agent/tools.js.map +1 -1
  6. package/dist/client.d.ts.map +1 -1
  7. package/dist/client.js +403 -46
  8. package/dist/client.js.map +1 -1
  9. package/dist/commands/account.d.ts.map +1 -1
  10. package/dist/commands/account.js +1 -0
  11. package/dist/commands/account.js.map +1 -1
  12. package/dist/commands/balances.d.ts.map +1 -1
  13. package/dist/commands/balances.js +39 -14
  14. package/dist/commands/balances.js.map +1 -1
  15. package/dist/commands/buy.d.ts.map +1 -1
  16. package/dist/commands/buy.js +29 -15
  17. package/dist/commands/buy.js.map +1 -1
  18. package/dist/commands/chat.d.ts.map +1 -1
  19. package/dist/commands/chat.js +34 -21
  20. package/dist/commands/chat.js.map +1 -1
  21. package/dist/commands/info.d.ts.map +1 -1
  22. package/dist/commands/info.js +12 -9
  23. package/dist/commands/info.js.map +1 -1
  24. package/dist/commands/positions.js +4 -4
  25. package/dist/commands/positions.js.map +1 -1
  26. package/dist/commands/sell.d.ts.map +1 -1
  27. package/dist/commands/sell.js +18 -11
  28. package/dist/commands/sell.js.map +1 -1
  29. package/dist/config.d.ts.map +1 -1
  30. package/dist/config.js +77 -10
  31. package/dist/config.js.map +1 -1
  32. package/dist/index.js +354 -118
  33. package/dist/index.js.map +1 -1
  34. package/dist/interactive/art.d.ts.map +1 -1
  35. package/dist/interactive/art.js +38 -0
  36. package/dist/interactive/art.js.map +1 -1
  37. package/dist/interactive/shell.d.ts.map +1 -1
  38. package/dist/interactive/shell.js +511 -111
  39. package/dist/interactive/shell.js.map +1 -1
  40. package/dist/mcp/chat-state.d.ts.map +1 -1
  41. package/dist/mcp/chat-state.js +2 -1
  42. package/dist/mcp/chat-state.js.map +1 -1
  43. package/dist/mcp/server.js +1 -1
  44. package/dist/mcp/tools.d.ts.map +1 -1
  45. package/dist/mcp/tools.js +108 -1
  46. package/dist/mcp/tools.js.map +1 -1
  47. package/dist/mcp/types.d.ts.map +1 -1
  48. package/dist/utils/constants.d.ts.map +1 -1
  49. package/dist/utils/constants.js +44 -2
  50. package/dist/utils/constants.js.map +1 -1
  51. package/dist/utils/errors.d.ts.map +1 -1
  52. package/dist/utils/errors.js +3 -3
  53. package/dist/utils/errors.js.map +1 -1
  54. package/dist/utils/privateKeyPrompt.d.ts.map +1 -1
  55. package/dist/utils/privateKeyPrompt.js +31 -7
  56. package/dist/utils/privateKeyPrompt.js.map +1 -1
  57. package/dist/utils/status.d.ts.map +1 -0
  58. package/dist/utils/status.js +67 -0
  59. package/dist/utils/status.js.map +1 -0
  60. package/dist/utils/token-resolver.d.ts.map +1 -1
  61. package/dist/utils/token-resolver.js +9 -0
  62. package/dist/utils/token-resolver.js.map +1 -1
  63. package/package.json +5 -4
package/dist/index.js CHANGED
@@ -1,8 +1,28 @@
1
1
  #!/usr/bin/env node
2
+ // Handle SIGINT (Ctrl+C) gracefully
3
+ let isExiting = false;
4
+ process.on("SIGINT", () => {
5
+ if (isExiting) {
6
+ // Force exit if already exiting
7
+ process.exit(130);
8
+ }
9
+ isExiting = true;
10
+ console.log("\n\nInterrupted by user. Exiting...");
11
+ process.exit(130);
12
+ });
13
+ process.on("SIGTERM", () => {
14
+ if (isExiting) {
15
+ process.exit(143);
16
+ }
17
+ isExiting = true;
18
+ console.log("\n\nTerminated. Exiting...");
19
+ process.exit(143);
20
+ });
2
21
  import { Command } from "commander";
3
22
  import chalk from "chalk";
4
23
  import { privateKeyToAccount } from "viem/accounts";
5
24
  import { createRequire } from "module";
25
+ import { randomUUID } from "crypto";
6
26
  import { config } from "./config.js";
7
27
  import { formatAddress, formatCurrency } from "./utils/formatting.js";
8
28
  import { HttpcatClient, HttpcatError } from "./client.js";
@@ -12,7 +32,8 @@ import { runCatSpinAnimation } from "./interactive/cat-spin.js";
12
32
  import { outputJson, outputError } from "./headless/json-output.js";
13
33
  import { promptForPrivateKey } from "./utils/privateKeyPrompt.js";
14
34
  import { withLoading } from "./utils/loading.js";
15
- import { readFileSync, writeFileSync, existsSync, statSync, unlinkSync, readlinkSync } from "fs";
35
+ import { writeStatusLine, clearStatusLine, formatRetryMessage, } from "./utils/status.js";
36
+ import { readFileSync, writeFileSync, existsSync, statSync, unlinkSync, readlinkSync, } from "fs";
16
37
  import { fileURLToPath } from "url";
17
38
  import { dirname, join } from "path";
18
39
  import { tmpdir } from "os";
@@ -24,7 +45,7 @@ const VERSION = packageJson.version;
24
45
  const require = createRequire(import.meta.url);
25
46
  const { version: PACKAGE_VERSION } = require("../package.json");
26
47
  // Import commands
27
- import { createToken, displayCreateResult, processPhotoUrl, isFilePath } from "./commands/create.js";
48
+ import { createToken, displayCreateResult, processPhotoUrl, isFilePath, } from "./commands/create.js";
28
49
  import { buyToken, displayBuyResult, displayBuyResultCompact, } from "./commands/buy.js";
29
50
  import { sellToken, displaySellResult, parseTokenAmount, } from "./commands/sell.js";
30
51
  import { getTokenInfo, displayTokenInfo } from "./commands/info.js";
@@ -84,6 +105,11 @@ function isConfigured(cliPrivateKey) {
84
105
  return true;
85
106
  return config.isConfigured();
86
107
  }
108
+ // Helper function to get global options properly
109
+ // IMPORTANT: Always use this instead of command.parent?.opts() to ensure global flags work
110
+ function getGlobalOpts() {
111
+ return program.opts();
112
+ }
87
113
  // Helper to ensure wallet is unlocked
88
114
  async function ensureWalletUnlocked() {
89
115
  const password = config.getPassword();
@@ -287,7 +313,20 @@ Examples:
287
313
  `)
288
314
  .action(async (name, symbol, options, command) => {
289
315
  try {
290
- const globalOpts = command.parent?.opts() || {};
316
+ // Fallback: get arguments from command object if not provided as parameters
317
+ // This handles cases where Commander.js doesn't parse arguments correctly
318
+ // In Commander.js v9+, use processedArgs; fallback to args for older versions
319
+ const args = command.processedArgs || command.args || [];
320
+ const actualName = name || args[0];
321
+ const actualSymbol = symbol || args[1];
322
+ // Validate required arguments
323
+ if (!actualName || typeof actualName !== "string") {
324
+ throw new Error("Token name is required. Usage: httpcat create <name> <symbol> [options]");
325
+ }
326
+ if (!actualSymbol || typeof actualSymbol !== "string") {
327
+ throw new Error("Token symbol is required. Usage: httpcat create <name> <symbol> [options]");
328
+ }
329
+ const globalOpts = getGlobalOpts();
291
330
  const accountIndex = globalOpts.account;
292
331
  // Ensure wallet is unlocked
293
332
  await ensureWalletUnlocked();
@@ -302,10 +341,16 @@ Examples:
302
341
  privateKey = await promptForPrivateKey();
303
342
  }
304
343
  const client = await HttpcatClient.create(privateKey);
344
+ // Generate default robohash URL if no photo provided
345
+ let photoToUse = options.photo;
346
+ if (!photoToUse) {
347
+ const uuid = randomUUID();
348
+ photoToUse = `https://robohash.org/${uuid}?set=set4`;
349
+ }
305
350
  // Process photo if it's a file path (show loading state)
306
- let processedPhotoUrl = options.photo;
307
- if (options.photo && isFilePath(options.photo)) {
308
- processedPhotoUrl = await withLoading(async () => processPhotoUrl(options.photo), {
351
+ let processedPhotoUrl = photoToUse;
352
+ if (photoToUse && isFilePath(photoToUse)) {
353
+ processedPhotoUrl = await withLoading(async () => processPhotoUrl(photoToUse), {
309
354
  message: "Uploading image...",
310
355
  json: globalOpts.json,
311
356
  quiet: globalOpts.quiet,
@@ -314,8 +359,8 @@ Examples:
314
359
  }
315
360
  // Create token (show loading state)
316
361
  const result = await withLoading(() => createToken(client, {
317
- name,
318
- symbol,
362
+ name: actualName.trim(),
363
+ symbol: actualSymbol.trim(),
319
364
  photoUrl: processedPhotoUrl,
320
365
  websiteUrl: options.website,
321
366
  }), {
@@ -333,7 +378,7 @@ Examples:
333
378
  process.exit(0);
334
379
  }
335
380
  catch (error) {
336
- const globalOpts = command.parent?.opts() || {};
381
+ const globalOpts = getGlobalOpts();
337
382
  if (globalOpts.json) {
338
383
  outputError("create_token", error, getExitCode(error));
339
384
  }
@@ -365,7 +410,7 @@ Examples:
365
410
  `)
366
411
  .action(async (tokenId, amount, options, command) => {
367
412
  try {
368
- const globalOpts = command.parent?.opts() || {};
413
+ const globalOpts = getGlobalOpts();
369
414
  const accountIndex = globalOpts.account;
370
415
  const repeatCount = options.repeat;
371
416
  const delayMs = options.delay || 0;
@@ -382,7 +427,8 @@ Examples:
382
427
  privateKey = await promptForPrivateKey();
383
428
  }
384
429
  const client = await HttpcatClient.create(privateKey);
385
- const isTestMode = client.getNetwork().includes("sepolia");
430
+ const network = client.getNetwork();
431
+ const isTestMode = network === "eip155:84532" || network === "eip155:11155111" || network.includes("sepolia");
386
432
  const silent = globalOpts.json || globalOpts.quiet;
387
433
  // Handle percentage amounts for graduated tokens
388
434
  let finalAmount = amount;
@@ -413,18 +459,82 @@ Examples:
413
459
  let stopReason = "";
414
460
  for (let i = 1; i <= repeatCount; i++) {
415
461
  try {
416
- // Show progress for non-JSON mode
417
- if (!globalOpts.json && !globalOpts.quiet) {
418
- console.log(chalk.dim(`Buy ${i}/${repeatCount}...`));
462
+ // Retry logic: try up to 10 times with exponential backoff on 402 errors
463
+ // Client is recreated on each attempt to ensure fresh signature for new nonce
464
+ let result = null;
465
+ let lastError = null;
466
+ const maxRetries = 10;
467
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
468
+ try {
469
+ // Show progress for non-JSON mode
470
+ if (!globalOpts.json && !globalOpts.quiet) {
471
+ if (attempt === 1) {
472
+ console.log(chalk.dim(`Buy ${i}/${repeatCount}...`));
473
+ }
474
+ else {
475
+ console.log(chalk.yellow(` Retrying buy ${i}/${repeatCount} (attempt ${attempt}/${maxRetries})...`));
476
+ }
477
+ }
478
+ // Recreate client inside the operation function to ensure fresh signature
479
+ // for each attempt (including retries). This fixes the issue where x402-fetch
480
+ // generates a new nonce but reuses the same signature, causing subsequent buys to fail
481
+ result = await withLoading(async () => {
482
+ // Recreate client for each attempt to ensure fresh signature for new nonce
483
+ const client = await HttpcatClient.create(privateKey);
484
+ return buyToken(client, tokenId, finalAmount, isTestMode, silent || i > 1, privateKey);
485
+ }, {
486
+ message: `Buying tokens (${i}/${repeatCount})...`,
487
+ json: globalOpts.json,
488
+ quiet: globalOpts.quiet || i > 1,
489
+ spinner: "cat",
490
+ });
491
+ // Success! Break out of retry loop
492
+ break;
493
+ }
494
+ catch (error) {
495
+ lastError = error;
496
+ // Only retry on 402 errors (payment required)
497
+ const is402Error = error instanceof HttpcatError && error.status === 402;
498
+ if (is402Error && attempt < maxRetries) {
499
+ // Exponential backoff: 1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s (capped at 10s)
500
+ const backoffMs = Math.min(Math.pow(2, attempt - 1) * 1000, 10000);
501
+ const backoffSeconds = backoffMs / 1000;
502
+ if (!globalOpts.json && !globalOpts.quiet) {
503
+ // Write status on same line (overwrites previous)
504
+ const statusMessage = formatRetryMessage(attempt, maxRetries, backoffSeconds);
505
+ writeStatusLine(statusMessage);
506
+ }
507
+ await new Promise((resolve) => setTimeout(resolve, backoffMs));
508
+ continue; // Retry
509
+ }
510
+ else {
511
+ // Not a 402 error, or max retries reached - clear status and throw
512
+ if (!globalOpts.json && !globalOpts.quiet) {
513
+ clearStatusLine();
514
+ }
515
+ throw error;
516
+ }
517
+ }
518
+ }
519
+ // If we exhausted retries, throw the last error
520
+ if (!result) {
521
+ // Clear status line before throwing
522
+ if (!globalOpts.json && !globalOpts.quiet) {
523
+ clearStatusLine();
524
+ }
525
+ if (lastError) {
526
+ throw lastError;
527
+ }
528
+ else {
529
+ throw new Error(`Failed to complete buy ${i}/${repeatCount} after ${maxRetries} attempts`);
530
+ }
419
531
  }
420
- const result = await withLoading(() => buyToken(client, tokenId, finalAmount, isTestMode, silent || i > 1, privateKey), {
421
- message: `Buying tokens (${i}/${repeatCount})...`,
422
- json: globalOpts.json,
423
- quiet: globalOpts.quiet || i > 1,
424
- spinner: "cat",
425
- });
426
532
  results.push(result);
427
533
  totalSpent += parseFloat(result.amountSpent);
534
+ // Clear status line before showing result
535
+ if (!globalOpts.json && !globalOpts.quiet) {
536
+ clearStatusLine();
537
+ }
428
538
  // Display compact result for each buy
429
539
  if (globalOpts.json) {
430
540
  // In JSON mode, output each result
@@ -443,58 +553,79 @@ Examples:
443
553
  }
444
554
  break;
445
555
  }
446
- // Apply delay between iterations (except after the last one)
447
- if (i < repeatCount && delayMs > 0) {
448
- await new Promise((resolve) => setTimeout(resolve, delayMs));
449
- }
450
- }
451
- catch (error) {
452
- // Handle insufficient funds (402) gracefully
453
- if (error instanceof HttpcatError && error.status === 402) {
454
- stoppedEarly = true;
455
- stopReason = "Insufficient funds";
456
- if (!globalOpts.json && !globalOpts.quiet) {
457
- console.log();
458
- console.log(chalk.yellow("💡 Insufficient funds. Stopping buy loop."));
459
- console.log();
460
- // Show current balance to help diagnose the issue
556
+ // Wait for transaction confirmations if present
557
+ // This ensures both the buy transaction and payment transaction are confirmed
558
+ // before the next buy, preventing nonce/signature conflicts
559
+ if (i < repeatCount) {
560
+ const { createPublicClient, http } = await import("viem");
561
+ const { baseSepolia } = await import("viem/chains");
562
+ const publicClient = createPublicClient({
563
+ chain: baseSepolia,
564
+ transport: http(config.getRpcUrl()),
565
+ });
566
+ // Wait for payment transaction first (if present)
567
+ // This is critical to ensure the payment nonce is consumed before next request
568
+ if (result.paymentTxHash) {
569
+ if (!globalOpts.json && !globalOpts.quiet) {
570
+ console.log(chalk.dim(` Waiting for payment transaction confirmation...`));
571
+ }
461
572
  try {
462
- const balance = await checkBalance(privateKey, true);
463
- console.log(chalk.cyan("💰 Current Wallet Balances:"));
464
- console.log(chalk.dim(` ETH: ${balance.ethFormatted}`));
465
- console.log(chalk.dim(` USDC: ${balance.usdcFormatted}`));
466
- if (balance.cat402Formatted) {
467
- console.log(chalk.dim(` CAT: ${balance.cat402Formatted}`));
468
- }
469
- console.log();
470
- // Provide guidance based on what might be insufficient
471
- const usdcBalance = parseFloat(balance.usdcFormatted.replace("$", "").replace(",", ""));
472
- const ethBalance = parseFloat(balance.ethFormatted.replace(" ETH", "").replace(",", ""));
473
- const buyAmount = parseFloat(finalAmount);
474
- console.log(chalk.yellow("💡 Possible issues:"));
475
- if (usdcBalance < buyAmount) {
476
- console.log(chalk.dim(` • Insufficient USDC for purchase: Need $${buyAmount.toFixed(6)}, have $${usdcBalance.toFixed(6)}`));
573
+ await publicClient.waitForTransactionReceipt({
574
+ hash: result.paymentTxHash,
575
+ });
576
+ if (!globalOpts.json && !globalOpts.quiet) {
577
+ console.log(chalk.dim(` ✅ Payment transaction confirmed`));
477
578
  }
478
- if (usdcBalance < 0.01) {
479
- console.log(chalk.dim(` • Low USDC for API payments: x402 protocol requires USDC for API calls`));
579
+ }
580
+ catch (txError) {
581
+ // If we can't wait for confirmation, log but continue
582
+ if (!globalOpts.json && !globalOpts.quiet) {
583
+ console.log(chalk.yellow(` ⚠️ Could not confirm payment transaction, proceeding...`));
480
584
  }
481
- if (ethBalance < 0.001) {
482
- console.log(chalk.dim(` • Low ETH for gas: Need ETH to pay for transaction fees`));
585
+ }
586
+ }
587
+ // Wait for buy transaction (if present)
588
+ if (result.txHash) {
589
+ if (!globalOpts.json && !globalOpts.quiet) {
590
+ console.log(chalk.dim(` Waiting for buy transaction confirmation...`));
591
+ }
592
+ try {
593
+ await publicClient.waitForTransactionReceipt({
594
+ hash: result.txHash,
595
+ });
596
+ if (!globalOpts.json && !globalOpts.quiet) {
597
+ console.log(chalk.dim(` ✅ Buy transaction confirmed`));
483
598
  }
484
- console.log();
485
- console.log(chalk.dim(" Check your balance with: httpcat balances"));
486
599
  }
487
- catch (balanceError) {
488
- // If we can't fetch balance, just show generic message
489
- console.log(chalk.dim(" Check your balance with: httpcat balances"));
600
+ catch (txError) {
601
+ // If we can't wait for confirmation, log but continue
602
+ if (!globalOpts.json && !globalOpts.quiet) {
603
+ console.log(chalk.yellow(` ⚠️ Could not confirm buy transaction, proceeding...`));
604
+ }
490
605
  }
491
606
  }
492
- break;
493
607
  }
494
- // For other errors, re-throw to be handled by outer catch
608
+ // Apply delay between iterations (except after the last one)
609
+ if (i < repeatCount && delayMs > 0) {
610
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
611
+ }
612
+ }
613
+ catch (error) {
614
+ // Clear status line before throwing
615
+ if (!globalOpts.json && !globalOpts.quiet) {
616
+ clearStatusLine();
617
+ }
618
+ // Don't catch 402 errors - x402-fetch handles payment automatically
619
+ // 402 is the normal payment flow on first call, not an error
620
+ // Let withLoading and x402-fetch handle payment retries
621
+ // Only re-throw to let outer handler deal with persistent errors
495
622
  throw error;
496
623
  }
497
624
  }
625
+ // Clear status line before showing summary
626
+ if (!globalOpts.json && !globalOpts.quiet) {
627
+ clearStatusLine();
628
+ }
498
629
  // Show final summary
499
630
  if (!globalOpts.json && !globalOpts.quiet && results.length > 0) {
500
631
  const lastResult = results[results.length - 1];
@@ -517,13 +648,60 @@ Examples:
517
648
  process.exit(0);
518
649
  }
519
650
  else {
520
- // Normal single buy execution
521
- const result = await withLoading(() => buyToken(client, tokenId, finalAmount, isTestMode, silent, privateKey), {
522
- message: "Buying tokens...",
523
- json: globalOpts.json,
524
- quiet: globalOpts.quiet,
525
- spinner: "cat",
526
- });
651
+ // Normal single buy execution with retry logic
652
+ let result = null;
653
+ let lastError = null;
654
+ let attempt = 0;
655
+ const maxRetries = null; // Retry indefinitely for single buys
656
+ while (!result) {
657
+ attempt++;
658
+ try {
659
+ // Show initial attempt message only on first try
660
+ if (attempt === 1 && !globalOpts.json && !globalOpts.quiet) {
661
+ console.log(chalk.dim("Buying tokens..."));
662
+ }
663
+ // Recreate client for each attempt to ensure fresh signature
664
+ const freshClient = await HttpcatClient.create(privateKey);
665
+ result = await buyToken(freshClient, tokenId, finalAmount, isTestMode, silent, privateKey);
666
+ // Success! Clear any status line and break
667
+ if (!globalOpts.json && !globalOpts.quiet) {
668
+ clearStatusLine();
669
+ }
670
+ break;
671
+ }
672
+ catch (error) {
673
+ lastError = error;
674
+ // Only retry on 402 errors (payment required)
675
+ const is402Error = error instanceof HttpcatError && error.status === 402;
676
+ if (is402Error) {
677
+ // Exponential backoff: 1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s (capped at 10s)
678
+ const backoffMs = Math.min(Math.pow(2, attempt - 1) * 1000, 10000);
679
+ const backoffSeconds = backoffMs / 1000;
680
+ if (!globalOpts.json && !globalOpts.quiet) {
681
+ // Write status on same line (overwrites previous)
682
+ const statusMessage = formatRetryMessage(attempt, maxRetries, backoffSeconds);
683
+ writeStatusLine(statusMessage);
684
+ }
685
+ await new Promise((resolve) => setTimeout(resolve, backoffMs));
686
+ continue; // Retry
687
+ }
688
+ else {
689
+ // Not a 402 error - throw immediately
690
+ if (!globalOpts.json && !globalOpts.quiet) {
691
+ clearStatusLine();
692
+ }
693
+ throw error;
694
+ }
695
+ }
696
+ }
697
+ if (!result) {
698
+ if (lastError) {
699
+ throw lastError;
700
+ }
701
+ else {
702
+ throw new Error("Failed to complete buy");
703
+ }
704
+ }
527
705
  if (globalOpts.json) {
528
706
  outputJson("token_buy", result);
529
707
  }
@@ -534,7 +712,11 @@ Examples:
534
712
  }
535
713
  }
536
714
  catch (error) {
537
- const globalOpts = command.parent?.opts() || {};
715
+ const globalOpts = getGlobalOpts();
716
+ // Clear status line before showing error
717
+ if (!globalOpts.json && !globalOpts.quiet) {
718
+ clearStatusLine();
719
+ }
538
720
  if (globalOpts.json) {
539
721
  outputError("token_buy", error, getExitCode(error));
540
722
  }
@@ -559,15 +741,13 @@ Examples:
559
741
  `)
560
742
  .action(async (tokenId, amountInput, command) => {
561
743
  try {
562
- const globalOpts = command.parent?.opts() || {};
744
+ const globalOpts = getGlobalOpts();
563
745
  const accountIndex = globalOpts.account;
564
746
  // Ensure wallet is unlocked
565
747
  await ensureWalletUnlocked();
566
- // Use program.opts() directly since command.parent?.opts() doesn't include global options
567
- const programOpts = program.opts();
568
- let privateKey = getPrivateKey(programOpts.privateKey || globalOpts.privateKey, accountIndex);
748
+ let privateKey = getPrivateKey(globalOpts.privateKey, accountIndex);
569
749
  // If not configured and not in JSON mode, prompt interactively
570
- if (!isConfigured(programOpts.privateKey || globalOpts.privateKey)) {
750
+ if (!isConfigured(globalOpts.privateKey)) {
571
751
  if (globalOpts.json) {
572
752
  console.error('❌ Not configured. Run "httpcat config" first or use --private-key flag.');
573
753
  process.exit(2);
@@ -609,7 +789,7 @@ Examples:
609
789
  const { baseSepolia } = await import("viem/chains");
610
790
  const publicClient = createPublicClient({
611
791
  chain: baseSepolia,
612
- transport: http(),
792
+ transport: http(config.getRpcUrl()),
613
793
  });
614
794
  const ERC20_ABI = parseAbi([
615
795
  "function balanceOf(address owner) view returns (uint256)",
@@ -650,7 +830,7 @@ Examples:
650
830
  const { baseSepolia } = await import("viem/chains");
651
831
  const publicClient = createPublicClient({
652
832
  chain: baseSepolia,
653
- transport: http(),
833
+ transport: http(config.getRpcUrl()),
654
834
  });
655
835
  const ERC20_ABI = parseAbi([
656
836
  "function balanceOf(address owner) view returns (uint256)",
@@ -687,7 +867,7 @@ Examples:
687
867
  process.exit(0);
688
868
  }
689
869
  catch (error) {
690
- const globalOpts = command.parent?.opts() || {};
870
+ const globalOpts = getGlobalOpts();
691
871
  if (globalOpts.json) {
692
872
  outputError("token_sell", error, getExitCode(error));
693
873
  }
@@ -713,7 +893,7 @@ Examples:
713
893
  `)
714
894
  .action(async (identifier, options, command) => {
715
895
  try {
716
- const globalOpts = command.parent?.opts() || {};
896
+ const globalOpts = getGlobalOpts();
717
897
  const accountIndex = globalOpts.account;
718
898
  // Ensure wallet is unlocked
719
899
  await ensureWalletUnlocked();
@@ -765,7 +945,7 @@ Examples:
765
945
  process.exit(0);
766
946
  }
767
947
  catch (error) {
768
- const globalOpts = command.parent?.opts() || {};
948
+ const globalOpts = getGlobalOpts();
769
949
  if (globalOpts.json) {
770
950
  outputError("claim", error, getExitCode(error));
771
951
  }
@@ -789,7 +969,7 @@ Examples:
789
969
  `)
790
970
  .action(async (tokenId, command) => {
791
971
  try {
792
- const globalOpts = command.parent?.opts() || {};
972
+ const globalOpts = getGlobalOpts();
793
973
  const accountIndex = globalOpts.account;
794
974
  // Ensure wallet is unlocked
795
975
  await ensureWalletUnlocked();
@@ -818,12 +998,13 @@ Examples:
818
998
  outputJson("token_info", result);
819
999
  }
820
1000
  else if (!globalOpts.quiet) {
821
- await displayTokenInfo(result, userAddress);
1001
+ const client = await HttpcatClient.create(privateKey);
1002
+ await displayTokenInfo(result, userAddress, client.getNetwork());
822
1003
  }
823
1004
  process.exit(0);
824
1005
  }
825
1006
  catch (error) {
826
- const globalOpts = command.parent?.opts() || {};
1007
+ const globalOpts = getGlobalOpts();
827
1008
  if (globalOpts.json) {
828
1009
  outputError("token_info", error, getExitCode(error));
829
1010
  }
@@ -849,7 +1030,7 @@ Examples:
849
1030
  `)
850
1031
  .action(async (options, command) => {
851
1032
  try {
852
- const globalOpts = command.parent?.opts() || {};
1033
+ const globalOpts = getGlobalOpts();
853
1034
  const accountIndex = globalOpts.account;
854
1035
  // Ensure wallet is unlocked
855
1036
  await ensureWalletUnlocked();
@@ -879,7 +1060,7 @@ Examples:
879
1060
  process.exit(0);
880
1061
  }
881
1062
  catch (error) {
882
- const globalOpts = command.parent?.opts() || {};
1063
+ const globalOpts = getGlobalOpts();
883
1064
  if (globalOpts.json) {
884
1065
  outputError("list_tokens", error, getExitCode(error));
885
1066
  }
@@ -908,7 +1089,7 @@ Examples:
908
1089
  `)
909
1090
  .action(async (options, command) => {
910
1091
  try {
911
- const globalOpts = command.parent?.opts() || {};
1092
+ const globalOpts = getGlobalOpts();
912
1093
  const accountIndex = globalOpts.account;
913
1094
  // Ensure wallet is unlocked
914
1095
  await ensureWalletUnlocked();
@@ -981,7 +1162,7 @@ Examples:
981
1162
  process.exit(0);
982
1163
  }
983
1164
  catch (error) {
984
- const globalOpts = command.parent?.opts() || {};
1165
+ const globalOpts = getGlobalOpts();
985
1166
  if (globalOpts.json) {
986
1167
  outputError("transactions", error, getExitCode(error));
987
1168
  }
@@ -1007,7 +1188,7 @@ Examples:
1007
1188
  `)
1008
1189
  .action(async (command) => {
1009
1190
  try {
1010
- const globalOpts = command.parent?.opts() || {};
1191
+ const globalOpts = getGlobalOpts();
1011
1192
  const accountIndex = globalOpts.account;
1012
1193
  // Ensure wallet is unlocked
1013
1194
  await ensureWalletUnlocked();
@@ -1075,7 +1256,7 @@ Examples:
1075
1256
  process.exit(0);
1076
1257
  }
1077
1258
  catch (error) {
1078
- const globalOpts = command.parent?.opts() || {};
1259
+ const globalOpts = getGlobalOpts();
1079
1260
  if (globalOpts.json) {
1080
1261
  outputError("positions", error, getExitCode(error));
1081
1262
  }
@@ -1097,7 +1278,7 @@ Examples:
1097
1278
  `)
1098
1279
  .action(async (command) => {
1099
1280
  try {
1100
- const globalOpts = command.parent?.opts() || {};
1281
+ const globalOpts = getGlobalOpts();
1101
1282
  const privateKey = getPrivateKey(globalOpts.privateKey);
1102
1283
  if (!isConfigured(globalOpts.privateKey)) {
1103
1284
  console.error('❌ Not configured. Run "httpcat config" first or use --private-key flag.');
@@ -1119,7 +1300,7 @@ Examples:
1119
1300
  process.exit(result.status === "ok" ? 0 : 1);
1120
1301
  }
1121
1302
  catch (error) {
1122
- const globalOpts = command.parent?.opts() || {};
1303
+ const globalOpts = getGlobalOpts();
1123
1304
  if (globalOpts.json) {
1124
1305
  outputError("health", error, getExitCode(error));
1125
1306
  }
@@ -1141,14 +1322,15 @@ Examples:
1141
1322
  `)
1142
1323
  .action(async (command) => {
1143
1324
  try {
1144
- const globalOpts = command.parent?.opts() || {};
1145
- const accountIndex = globalOpts.account;
1325
+ // Use program.opts() directly since command.parent?.opts() doesn't include global options
1326
+ const programOpts = program.opts();
1327
+ const accountIndex = programOpts.account;
1146
1328
  // Ensure wallet is unlocked
1147
1329
  await ensureWalletUnlocked();
1148
- let privateKey = getPrivateKey(globalOpts.privateKey, accountIndex);
1330
+ let privateKey = getPrivateKey(programOpts.privateKey, accountIndex);
1149
1331
  // If not configured and not in JSON mode, prompt interactively
1150
- if (!isConfigured(globalOpts.privateKey)) {
1151
- if (globalOpts.json) {
1332
+ if (!isConfigured(programOpts.privateKey)) {
1333
+ if (programOpts.json) {
1152
1334
  console.error('❌ Not configured. Run "httpcat config" first or use --private-key flag.');
1153
1335
  process.exit(2);
1154
1336
  }
@@ -1157,25 +1339,25 @@ Examples:
1157
1339
  }
1158
1340
  const result = await withLoading(() => checkBalance(privateKey), {
1159
1341
  message: "Checking balances...",
1160
- json: globalOpts.json,
1161
- quiet: globalOpts.quiet,
1342
+ json: programOpts.json,
1343
+ quiet: programOpts.quiet,
1162
1344
  spinner: "cat",
1163
1345
  });
1164
- if (globalOpts.json) {
1346
+ if (programOpts.json) {
1165
1347
  outputJson("balances", result);
1166
1348
  }
1167
- else if (!globalOpts.quiet) {
1349
+ else if (!programOpts.quiet) {
1168
1350
  displayBalance(result);
1169
1351
  }
1170
1352
  process.exit(0);
1171
1353
  }
1172
1354
  catch (error) {
1173
- const globalOpts = command.parent?.opts() || {};
1174
- if (globalOpts.json) {
1355
+ const programOpts = program.opts();
1356
+ if (programOpts.json) {
1175
1357
  outputError("balances", error, getExitCode(error));
1176
1358
  }
1177
1359
  else {
1178
- handleError(error, globalOpts.verbose);
1360
+ handleError(error, programOpts.verbose);
1179
1361
  }
1180
1362
  process.exit(getExitCode(error));
1181
1363
  }
@@ -1304,7 +1486,7 @@ Examples:
1304
1486
  `)
1305
1487
  .action(async (tokenIdentifier, options, command) => {
1306
1488
  try {
1307
- const globalOpts = command.parent?.opts() || {};
1489
+ const globalOpts = getGlobalOpts();
1308
1490
  const chatOpts = options;
1309
1491
  const accountIndex = globalOpts.account;
1310
1492
  // Ensure wallet is unlocked
@@ -1332,7 +1514,7 @@ Examples:
1332
1514
  }
1333
1515
  }
1334
1516
  catch (error) {
1335
- const globalOpts = command.parent?.opts() || {};
1517
+ const globalOpts = getGlobalOpts();
1336
1518
  if (globalOpts.json) {
1337
1519
  outputError("chat", error, getExitCode(error));
1338
1520
  }
@@ -1342,16 +1524,14 @@ Examples:
1342
1524
  process.exit(getExitCode(error));
1343
1525
  }
1344
1526
  });
1345
- // Agent command
1527
+ // Agent setup command
1346
1528
  program
1347
1529
  .command("agent")
1348
- .alias("cat")
1349
1530
  .description("Configure AI agent for trading assistance")
1350
1531
  .option("-s, --setup", "Run setup wizard to configure API key")
1351
1532
  .addHelpText("after", `
1352
1533
  Examples:
1353
1534
  httpcat agent --setup # Configure AI agent
1354
- httpcat cat --setup # Same, using 'cat' alias
1355
1535
 
1356
1536
  The AI agent helps you:
1357
1537
  • Trade tokens using natural language
@@ -1359,7 +1539,7 @@ The AI agent helps you:
1359
1539
  • Get market information
1360
1540
  • Execute commands hands-free
1361
1541
 
1362
- You can use 'agent' or 'cat' interchangeably.
1542
+ Use 'httpcat cat' to start interactive agent mode.
1363
1543
  `)
1364
1544
  .action(async (options) => {
1365
1545
  try {
@@ -1371,16 +1551,13 @@ You can use 'agent' or 'cat' interchangeably.
1371
1551
  else {
1372
1552
  // Show help for agent command
1373
1553
  console.log();
1374
- console.log(chalk.cyan.bold("🐱 AI Agent"));
1375
- console.log();
1376
- console.log(chalk.yellow("The AI agent feature is currently available only in interactive mode."));
1554
+ console.log(chalk.cyan.bold("🐱 AI Agent Setup"));
1377
1555
  console.log();
1378
1556
  console.log(chalk.bold("To configure:"));
1379
1557
  console.log(chalk.dim(" httpcat agent --setup"));
1380
1558
  console.log();
1381
- console.log(chalk.bold("Note:"));
1382
- console.log(chalk.dim(" The agent command has been temporarily disabled in interactive mode."));
1383
- console.log(chalk.dim(" Use the CLI setup above to configure your API keys for future use."));
1559
+ console.log(chalk.bold("To start agent mode:"));
1560
+ console.log(chalk.dim(" httpcat cat"));
1384
1561
  console.log();
1385
1562
  process.exit(0);
1386
1563
  }
@@ -1390,6 +1567,65 @@ You can use 'agent' or 'cat' interchangeably.
1390
1567
  process.exit(getExitCode(error));
1391
1568
  }
1392
1569
  });
1570
+ // Cat command - starts agent interactive mode
1571
+ program
1572
+ .command("cat")
1573
+ .description("Start interactive AI agent mode")
1574
+ .addHelpText("after", `
1575
+ Examples:
1576
+ httpcat cat # Start interactive agent mode
1577
+
1578
+ The AI agent helps you:
1579
+ • Trade tokens using natural language
1580
+ • Check balances and positions
1581
+ • Get market information
1582
+ • Execute commands hands-free
1583
+
1584
+ Type /exit to quit agent mode.
1585
+ `)
1586
+ .action(async (options, command) => {
1587
+ try {
1588
+ const globalOpts = getGlobalOpts();
1589
+ const accountIndex = globalOpts.account;
1590
+ // Ensure wallet is unlocked
1591
+ await ensureWalletUnlocked();
1592
+ let privateKey = getPrivateKey(globalOpts.privateKey, accountIndex);
1593
+ // If not configured and not in JSON mode, prompt interactively
1594
+ if (!isConfigured(globalOpts.privateKey)) {
1595
+ if (globalOpts.json) {
1596
+ console.error('❌ Not configured. Run "httpcat config" first or use --private-key flag.');
1597
+ process.exit(2);
1598
+ }
1599
+ // Interactive prompt for private key
1600
+ privateKey = await promptForPrivateKey();
1601
+ }
1602
+ const client = await HttpcatClient.create(privateKey);
1603
+ // Check if agent is configured
1604
+ const agentConfig = config.getAIAgentConfig();
1605
+ if (!agentConfig) {
1606
+ console.log();
1607
+ console.log(chalk.yellow("⚠️ Agent not configured"));
1608
+ console.log(chalk.dim("Run 'httpcat agent --setup' to configure the AI agent."));
1609
+ console.log();
1610
+ process.exit(1);
1611
+ }
1612
+ const apiKey = config.getAIAgentApiKey();
1613
+ if (!apiKey) {
1614
+ console.log();
1615
+ console.log(chalk.red("❌ Failed to get API key"));
1616
+ console.log(chalk.dim("Run 'httpcat agent --setup' to configure the API key."));
1617
+ console.log();
1618
+ process.exit(1);
1619
+ }
1620
+ // Start agent interactive mode
1621
+ const { startAgentInteractiveMode } = await import("./interactive/shell.js");
1622
+ await startAgentInteractiveMode(client);
1623
+ }
1624
+ catch (error) {
1625
+ handleError(error, program.opts().verbose);
1626
+ process.exit(getExitCode(error));
1627
+ }
1628
+ });
1393
1629
  // MCP Server command
1394
1630
  program
1395
1631
  .command("mcp-server")
@@ -1438,10 +1674,10 @@ function displayGroupedHelp(program) {
1438
1674
  const commandGroups = {
1439
1675
  "Token Operations": ["create", "buy", "sell", "claim"],
1440
1676
  "Token Information": ["info", "list"],
1441
- "Portfolio": ["positions", "transactions", "balances"],
1677
+ Portfolio: ["positions", "transactions", "balances"],
1442
1678
  "Account Management": ["account", "env"],
1443
1679
  "AI & Social": ["agent", "chat"],
1444
- "System": ["health", "config", "mcp-server"],
1680
+ System: ["health", "config", "mcp-server"],
1445
1681
  };
1446
1682
  // Display grouped commands
1447
1683
  for (const [groupName, commandNames] of Object.entries(commandGroups)) {
@@ -1545,9 +1781,9 @@ function getTerminalSessionId() {
1545
1781
  // Fall through to other methods (e.g., on macOS or if /proc doesn't exist)
1546
1782
  }
1547
1783
  // Fall back to environment variables or user/term combination
1548
- return process.env.TERM_SESSION_ID ||
1784
+ return (process.env.TERM_SESSION_ID ||
1549
1785
  process.env.SESSION_ID ||
1550
- `${process.env.USER || "default"}-${process.env.TERM || "unknown"}`;
1786
+ `${process.env.USER || "default"}-${process.env.TERM || "unknown"}`);
1551
1787
  }
1552
1788
  // Helper function to check if animation has been shown in this terminal session
1553
1789
  function hasAnimationBeenShown() {