httpcat-cli 0.0.25 → 0.0.27-rc.1

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 (67) hide show
  1. package/.github/workflows/README.md +19 -2
  2. package/.github/workflows/ci.yml +31 -20
  3. package/.github/workflows/homebrew-tap.yml +1 -1
  4. package/.github/workflows/rc-publish.yml +169 -0
  5. package/.github/workflows/release.yml +223 -71
  6. package/README.md +113 -71
  7. package/bun.lock +2933 -0
  8. package/dist/commands/account.d.ts.map +1 -1
  9. package/dist/commands/account.js +14 -7
  10. package/dist/commands/account.js.map +1 -1
  11. package/dist/commands/balances.d.ts.map +1 -0
  12. package/dist/commands/balances.js +171 -0
  13. package/dist/commands/balances.js.map +1 -0
  14. package/dist/commands/buy.d.ts.map +1 -1
  15. package/dist/commands/buy.js +746 -24
  16. package/dist/commands/buy.js.map +1 -1
  17. package/dist/commands/chat.d.ts.map +1 -1
  18. package/dist/commands/chat.js +467 -906
  19. package/dist/commands/chat.js.map +1 -1
  20. package/dist/commands/claim.d.ts.map +1 -0
  21. package/dist/commands/claim.js +65 -0
  22. package/dist/commands/claim.js.map +1 -0
  23. package/dist/commands/create.d.ts.map +1 -1
  24. package/dist/commands/create.js +0 -1
  25. package/dist/commands/create.js.map +1 -1
  26. package/dist/commands/info.d.ts.map +1 -1
  27. package/dist/commands/info.js +128 -26
  28. package/dist/commands/info.js.map +1 -1
  29. package/dist/commands/list.d.ts.map +1 -1
  30. package/dist/commands/list.js +30 -23
  31. package/dist/commands/list.js.map +1 -1
  32. package/dist/commands/positions.d.ts.map +1 -1
  33. package/dist/commands/positions.js +178 -105
  34. package/dist/commands/positions.js.map +1 -1
  35. package/dist/commands/sell.d.ts.map +1 -1
  36. package/dist/commands/sell.js +713 -24
  37. package/dist/commands/sell.js.map +1 -1
  38. package/dist/index.js +417 -109
  39. package/dist/index.js.map +1 -1
  40. package/dist/interactive/shell.d.ts.map +1 -1
  41. package/dist/interactive/shell.js +328 -179
  42. package/dist/interactive/shell.js.map +1 -1
  43. package/dist/mcp/tools.d.ts.map +1 -1
  44. package/dist/mcp/tools.js +8 -8
  45. package/dist/mcp/tools.js.map +1 -1
  46. package/dist/utils/constants.d.ts.map +1 -0
  47. package/dist/utils/constants.js +66 -0
  48. package/dist/utils/constants.js.map +1 -0
  49. package/dist/utils/formatting.d.ts.map +1 -1
  50. package/dist/utils/formatting.js +3 -5
  51. package/dist/utils/formatting.js.map +1 -1
  52. package/dist/utils/token-resolver.d.ts.map +1 -1
  53. package/dist/utils/token-resolver.js +71 -40
  54. package/dist/utils/token-resolver.js.map +1 -1
  55. package/dist/utils/validation.d.ts.map +1 -1
  56. package/dist/utils/validation.js +4 -3
  57. package/dist/utils/validation.js.map +1 -1
  58. package/jest.config.js +1 -1
  59. package/package.json +19 -13
  60. package/.claude/settings.local.json +0 -41
  61. package/dist/commands/balance.d.ts.map +0 -1
  62. package/dist/commands/balance.js +0 -112
  63. package/dist/commands/balance.js.map +0 -1
  64. package/homebrew-httpcat/Formula/httpcat.rb +0 -18
  65. package/homebrew-httpcat/README.md +0 -31
  66. package/homebrew-httpcat/homebrew-httpcat/Formula/httpcat.rb +0 -18
  67. package/homebrew-httpcat/homebrew-httpcat/README.md +0 -31
package/dist/index.js CHANGED
@@ -2,9 +2,10 @@
2
2
  import { Command } from "commander";
3
3
  import chalk from "chalk";
4
4
  import { privateKeyToAccount } from "viem/accounts";
5
+ import { createRequire } from "module";
5
6
  import { config } from "./config.js";
6
- import { formatAddress } from "./utils/formatting.js";
7
- import { HttpcatClient } from "./client.js";
7
+ import { formatAddress, formatCurrency } from "./utils/formatting.js";
8
+ import { HttpcatClient, HttpcatError } from "./client.js";
8
9
  import { handleError, getExitCode } from "./utils/errors.js";
9
10
  import { startInteractiveShell } from "./interactive/shell.js";
10
11
  import { outputJson, outputError } from "./headless/json-output.js";
@@ -18,26 +19,29 @@ const __filename = fileURLToPath(import.meta.url);
18
19
  const __dirname = dirname(__filename);
19
20
  const packageJson = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
20
21
  const VERSION = packageJson.version;
22
+ const require = createRequire(import.meta.url);
23
+ const { version: PACKAGE_VERSION } = require("../package.json");
21
24
  // Import commands
22
25
  import { createToken, displayCreateResult } from "./commands/create.js";
23
- import { buyToken, displayBuyResult, } from "./commands/buy.js";
26
+ import { buyToken, displayBuyResult, displayBuyResultCompact, } from "./commands/buy.js";
24
27
  import { sellToken, displaySellResult, parseTokenAmount, } from "./commands/sell.js";
25
28
  import { getTokenInfo, displayTokenInfo } from "./commands/info.js";
26
29
  import { listTokens, displayTokenList } from "./commands/list.js";
27
30
  import { getPositions, displayPositions } from "./commands/positions.js";
28
31
  import { getTransactions, displayTransactions, } from "./commands/transactions.js";
29
32
  import { checkHealth, displayHealthStatus } from "./commands/health.js";
30
- import { checkBalance, displayBalance } from "./commands/balance.js";
33
+ import { checkBalance, displayBalance } from "./commands/balances.js";
31
34
  import { startChatStream } from "./commands/chat.js";
32
35
  import { runMcpServer } from "./commands/mcp-server.js";
33
- import { getAccountInfo, displayAccountInfo, listAccounts, switchAccount, addAccount } from "./commands/account.js";
36
+ import { getAccountInfo, displayAccountInfo, listAccounts, switchAccount, addAccount, } from "./commands/account.js";
37
+ import { viewFees, claimFees, displayFees, displayClaimResult, } from "./commands/claim.js";
34
38
  // Check for --version --json before parsing
35
39
  const args = process.argv;
36
40
  if (args.includes("--version") || args.includes("-V")) {
37
41
  if (args.includes("--json")) {
38
42
  console.log(JSON.stringify({
39
43
  name: "httpcat-cli",
40
- version: VERSION,
44
+ version: PACKAGE_VERSION,
41
45
  }, null, 2));
42
46
  process.exit(0);
43
47
  }
@@ -46,13 +50,13 @@ const program = new Command();
46
50
  program
47
51
  .name("httpcat")
48
52
  .description("CLI tool for interacting with httpcat agent")
49
- .version(VERSION)
50
- .option("--json", "Output in JSON format")
51
- .option("--quiet", "Minimal output (exit codes only)")
52
- .option("--verbose", "Verbose error messages")
53
+ .version(PACKAGE_VERSION)
54
+ .option("-j, --json", "Output in JSON format")
55
+ .option("-q, --quiet", "Minimal output (exit codes only)")
56
+ .option("-v, --verbose", "Verbose error messages")
53
57
  .option("--no-art", "Disable ASCII art")
54
- .option("--private-key <key>", "Private key (overrides config and env var)")
55
- .option("--account <index>", "Account index to use (overrides active account)", (value) => parseInt(value, 10));
58
+ .option("-k, --private-key <key>", "Private key (overrides config and env var)")
59
+ .option("-a, --account <index>", "Account index to use (overrides active account)", (value) => parseInt(value, 10));
56
60
  // Helper function to get private key with priority: CLI flag > env var > config
57
61
  function getPrivateKey(cliPrivateKey, accountIndex) {
58
62
  if (cliPrivateKey)
@@ -62,7 +66,9 @@ function getPrivateKey(cliPrivateKey, accountIndex) {
62
66
  return envKey;
63
67
  // Use account system
64
68
  try {
65
- const index = accountIndex !== undefined ? accountIndex : config.getActiveAccountIndex();
69
+ const index = accountIndex !== undefined
70
+ ? accountIndex
71
+ : config.getActiveAccountIndex();
66
72
  return config.getAccountPrivateKey(index);
67
73
  }
68
74
  catch (error) {
@@ -186,7 +192,7 @@ envCommand
186
192
  .description("Add a custom environment")
187
193
  .argument("<name>", "Environment name")
188
194
  .argument("<agentUrl>", "Agent URL (e.g., http://localhost:8787)")
189
- .option("--network <network>", "Network (default: base-sepolia)", "base-sepolia")
195
+ .option("-n, --network <network>", "Network (default: base-sepolia)", "base-sepolia")
190
196
  .action((name, agentUrl, options) => {
191
197
  try {
192
198
  config.addEnvironment(name, agentUrl, options.network);
@@ -207,7 +213,7 @@ envCommand
207
213
  .description("Update an existing environment")
208
214
  .argument("<name>", "Environment name")
209
215
  .argument("<agentUrl>", "Agent URL (e.g., http://localhost:8787)")
210
- .option("--network <network>", "Network (default: base-sepolia)", "base-sepolia")
216
+ .option("-n, --network <network>", "Network (default: base-sepolia)", "base-sepolia")
211
217
  .action((name, agentUrl, options) => {
212
218
  try {
213
219
  config.updateEnvironment(name, agentUrl, options.network);
@@ -225,9 +231,9 @@ envCommand
225
231
  program
226
232
  .command("config")
227
233
  .description("Configure httpcat")
228
- .option("--show", "Show current configuration")
229
- .option("--set <key=value>", "Set configuration value")
230
- .option("--reset", "Reset configuration")
234
+ .option("-s, --show", "Show current configuration")
235
+ .option("-S, --set <key=value>", "Set configuration value")
236
+ .option("-r, --reset", "Reset configuration")
231
237
  .addHelpText("after", `
232
238
  Examples:
233
239
  httpcat config # Run setup wizard
@@ -239,17 +245,21 @@ Examples:
239
245
  try {
240
246
  if (options.show) {
241
247
  console.log(JSON.stringify(config.getAll(), null, 2));
248
+ process.exit(0);
242
249
  }
243
250
  else if (options.set) {
244
251
  const [key, value] = options.set.split("=");
245
252
  config.set(key, value);
246
253
  console.log(`✅ ${key} set to ${value}`);
254
+ process.exit(0);
247
255
  }
248
256
  else if (options.reset) {
249
257
  await config.runSetupWizard();
258
+ process.exit(0);
250
259
  }
251
260
  else {
252
261
  await config.runSetupWizard();
262
+ process.exit(0);
253
263
  }
254
264
  }
255
265
  catch (error) {
@@ -263,9 +273,9 @@ program
263
273
  .description("Create a new token")
264
274
  .argument("<name>", 'Token name (e.g., "My Token")')
265
275
  .argument("<symbol>", 'Token symbol/ticker (e.g., "MTK", 3-10 characters)')
266
- .option("--photo <url>", "Photo URL for token logo")
267
- .option("--banner <url>", "Banner image URL")
268
- .option("--website <url>", "Website URL")
276
+ .option("-p, --photo <url>", "Photo URL for token logo")
277
+ .option("-b, --banner <url>", "Banner image URL")
278
+ .option("-w, --website <url>", "Website URL")
269
279
  .addHelpText("after", `
270
280
  Examples:
271
281
  httpcat create "My Token" "MTK"
@@ -324,21 +334,29 @@ Examples:
324
334
  // Buy command
325
335
  program
326
336
  .command("buy")
327
- .description("Buy tokens from the bonding curve")
328
- .argument("<identifier>", 'Token ID (UUID), name, or symbol (e.g., abc123-..., "My Token", or MTK)')
329
- .argument("<amount>", "Amount in USDC: 0.05, 0.10, or 0.20 (testnet) | 50, 100, or 200 (mainnet)")
337
+ .description("Buy tokens (auto-routes to bonding curve or Uniswap V2)")
338
+ .argument("<identifier>", 'Address, name, or symbol (e.g., 0x1234..., "My Token", or MTK)')
339
+ .argument("<amount>", "Amount in USDC or percentage: 0.05, 0.10, 0.20, 10, 50% (% of your USDC balance)")
340
+ .option("-r, --repeat <count>", "Number of times to repeat the buy operation", (value) => parseInt(value, 10))
341
+ .option("-d, --delay <ms>", "Delay in milliseconds between repeat operations (default: 0)", (value) => parseInt(value, 10), 0)
330
342
  .addHelpText("after", `
331
343
  Examples:
332
344
  httpcat buy abc123-4567-89ab-cdef-0123456789ab 0.20
333
345
  httpcat buy "My Token" 0.10
334
- httpcat buy MTK 0.05
346
+ httpcat buy MTK 10 # Buy with $10 USDC
347
+ httpcat buy MTK 50% # Buy with 50% of your USDC balance
348
+ httpcat buy MTK 0.05 --repeat 10
335
349
  httpcat --json buy abc123-4567-89ab-cdef-0123456789ab 0.10
336
350
  httpcat --private-key 0x... buy "PurrfecTT" 0.20
351
+ httpcat buy MTK 0.10 --repeat 10
352
+ httpcat buy MTK 0.10 --repeat 10 --delay 500
337
353
  `)
338
- .action(async (tokenId, amount, command) => {
354
+ .action(async (tokenId, amount, options, command) => {
339
355
  try {
340
356
  const globalOpts = command.parent?.opts() || {};
341
357
  const accountIndex = globalOpts.account;
358
+ const repeatCount = options.repeat;
359
+ const delayMs = options.delay || 0;
342
360
  // Ensure wallet is unlocked
343
361
  await ensureWalletUnlocked();
344
362
  let privateKey = getPrivateKey(globalOpts.privateKey, accountIndex);
@@ -354,19 +372,122 @@ Examples:
354
372
  const client = await HttpcatClient.create(privateKey);
355
373
  const isTestMode = client.getNetwork().includes("sepolia");
356
374
  const silent = globalOpts.json || globalOpts.quiet;
357
- const result = await withLoading(() => buyToken(client, tokenId, amount, isTestMode, silent), {
358
- message: "Buying tokens...",
359
- json: globalOpts.json,
360
- quiet: globalOpts.quiet,
361
- spinner: "cat",
362
- });
363
- if (globalOpts.json) {
364
- outputJson("token_buy", result);
375
+ // Handle percentage amounts for graduated tokens
376
+ let finalAmount = amount;
377
+ if (amount.endsWith("%")) {
378
+ if (!silent) {
379
+ console.log("💰 Checking USDC balance for percentage buy...");
380
+ }
381
+ const balance = await checkBalance(privateKey);
382
+ const usdcBalance = parseFloat(balance.usdcFormatted.replace("$", "").replace(",", ""));
383
+ const percentage = parseFloat(amount.replace("%", ""));
384
+ if (isNaN(percentage) || percentage <= 0 || percentage > 100) {
385
+ throw new Error("Percentage must be between 0 and 100");
386
+ }
387
+ finalAmount = ((usdcBalance * percentage) / 100).toFixed(6);
388
+ if (!silent) {
389
+ console.log(`💵 USDC Balance: $${usdcBalance}`);
390
+ console.log(`📊 Using ${percentage}% = $${finalAmount}`);
391
+ }
392
+ if (parseFloat(finalAmount) === 0) {
393
+ throw new Error("Insufficient USDC balance");
394
+ }
365
395
  }
366
- else if (!globalOpts.quiet) {
367
- displayBuyResult(result);
396
+ // If repeat is specified, execute multiple buys
397
+ if (repeatCount && repeatCount > 0) {
398
+ const results = [];
399
+ let totalSpent = 0;
400
+ let stoppedEarly = false;
401
+ let stopReason = "";
402
+ for (let i = 1; i <= repeatCount; i++) {
403
+ try {
404
+ // Show progress for non-JSON mode
405
+ if (!globalOpts.json && !globalOpts.quiet) {
406
+ console.log(chalk.dim(`Buy ${i}/${repeatCount}...`));
407
+ }
408
+ const result = await withLoading(() => buyToken(client, tokenId, finalAmount, isTestMode, silent || i > 1, privateKey), {
409
+ message: `Buying tokens (${i}/${repeatCount})...`,
410
+ json: globalOpts.json,
411
+ quiet: globalOpts.quiet || i > 1,
412
+ spinner: "cat",
413
+ });
414
+ results.push(result);
415
+ totalSpent += parseFloat(result.amountSpent);
416
+ // Display compact result for each buy
417
+ if (globalOpts.json) {
418
+ // In JSON mode, output each result
419
+ outputJson("token_buy", result);
420
+ }
421
+ else if (!globalOpts.quiet) {
422
+ displayBuyResultCompact(result, i, repeatCount);
423
+ }
424
+ // Check if token graduated
425
+ if (result.graduationReached) {
426
+ stoppedEarly = true;
427
+ stopReason = "Token graduated";
428
+ if (!globalOpts.json && !globalOpts.quiet) {
429
+ console.log();
430
+ console.log(chalk.green("🎓 Token has graduated! Stopping buy loop."));
431
+ }
432
+ break;
433
+ }
434
+ // Apply delay between iterations (except after the last one)
435
+ if (i < repeatCount && delayMs > 0) {
436
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
437
+ }
438
+ }
439
+ catch (error) {
440
+ // Handle insufficient funds (402) gracefully
441
+ if (error instanceof HttpcatError && error.status === 402) {
442
+ stoppedEarly = true;
443
+ stopReason = "Insufficient funds";
444
+ if (!globalOpts.json && !globalOpts.quiet) {
445
+ console.log();
446
+ console.log(chalk.yellow("💡 Insufficient funds. Stopping buy loop."));
447
+ }
448
+ break;
449
+ }
450
+ // For other errors, re-throw to be handled by outer catch
451
+ throw error;
452
+ }
453
+ }
454
+ // Show final summary
455
+ if (!globalOpts.json && !globalOpts.quiet && results.length > 0) {
456
+ const lastResult = results[results.length - 1];
457
+ const graduationStatus = lastResult.graduationReached
458
+ ? "✅ GRADUATED!"
459
+ : `${(lastResult.graduationProgress || 0).toFixed(2)}%`;
460
+ console.log();
461
+ console.log(chalk.cyan.bold("📊 Repeat Buy Summary"));
462
+ console.log(chalk.dim("─".repeat(50)));
463
+ console.log(`Total buys completed: ${chalk.bold(results.length)}${repeatCount ? `/${repeatCount}` : ""}`);
464
+ console.log(`Total amount spent: ${chalk.bold(formatCurrency(totalSpent.toString()))}`);
465
+ console.log(`Final token price: ${chalk.bold(formatCurrency(lastResult.newPrice))}`);
466
+ console.log(`Final graduation: ${chalk.bold(graduationStatus)}`);
467
+ if (stoppedEarly) {
468
+ console.log(`Stopped early: ${chalk.yellow(stopReason)}`);
469
+ }
470
+ console.log();
471
+ }
472
+ // Exit with success code (0) even if stopped early due to graduation or insufficient funds
473
+ process.exit(0);
474
+ }
475
+ else {
476
+ // Normal single buy execution
477
+ const result = await withLoading(() => buyToken(client, tokenId, finalAmount, isTestMode, silent, privateKey), {
478
+ message: "Buying tokens...",
479
+ json: globalOpts.json,
480
+ quiet: globalOpts.quiet,
481
+ spinner: "cat",
482
+ });
483
+ if (globalOpts.json) {
484
+ outputJson("token_buy", result);
485
+ }
486
+ else if (!globalOpts.quiet) {
487
+ displayBuyResult(result);
488
+ }
489
+ process.exit(0);
368
490
  }
369
- process.exit(0);
370
491
  }
371
492
  catch (error) {
372
493
  const globalOpts = command.parent?.opts() || {};
@@ -382,8 +503,8 @@ Examples:
382
503
  // Sell command
383
504
  program
384
505
  .command("sell")
385
- .description("Sell tokens back to the bonding curve")
386
- .argument("<identifier>", 'Token ID (UUID), name, or symbol (e.g., abc123-..., "My Token", or MTK)')
506
+ .description("Sell tokens (auto-routes to bonding curve or Uniswap V2)")
507
+ .argument("<identifier>", 'Address, name, or symbol (e.g., 0x1234..., "My Token", or MTK)')
387
508
  .argument("<amount>", 'Amount: number (e.g., 1000), percentage (e.g., 50%), or "all"')
388
509
  .addHelpText("after", `
389
510
  Examples:
@@ -413,23 +534,99 @@ Examples:
413
534
  // Derive user address from private key
414
535
  const account = privateKeyToAccount(privateKey);
415
536
  const userAddress = account.address;
416
- // Get current holdings (this also resolves the identifier to tokenId)
417
- const info = await withLoading(() => getTokenInfo(client, tokenId, userAddress, silent), {
418
- message: "Checking token info...",
419
- json: globalOpts.json,
420
- quiet: globalOpts.quiet,
421
- spinner: "cat",
422
- });
423
- if (!info.userPosition || info.userPosition.tokensOwned === "0") {
424
- throw new Error("You do not own any of this token");
537
+ // Check if it's a contract address
538
+ const addressRegex = /^0x[0-9a-f]{40}$/i;
539
+ const isContractAddress = addressRegex.test(tokenId);
540
+ let actualBalance = "0";
541
+ let info = null;
542
+ // If it's a contract address, try to look it up in database first
543
+ if (isContractAddress) {
544
+ try {
545
+ // Try to get token info - if it exists in our DB, use normal flow
546
+ info = await withLoading(() => getTokenInfo(client, tokenId, userAddress, silent), {
547
+ message: "Checking token info...",
548
+ json: globalOpts.json,
549
+ quiet: globalOpts.quiet,
550
+ spinner: "cat",
551
+ });
552
+ // Found in database! Continue with normal flow below
553
+ actualBalance = info.userPosition?.tokensOwned || "0";
554
+ }
555
+ catch (error) {
556
+ // Not in database - treat as external token
557
+ if (error.message?.includes("not found") ||
558
+ error.message?.includes("Invalid UUID")) {
559
+ if (!silent) {
560
+ console.log("📊 Checking on-chain balance for external token...");
561
+ }
562
+ const { createPublicClient, http, parseAbi } = await import("viem");
563
+ const { baseSepolia } = await import("viem/chains");
564
+ const publicClient = createPublicClient({
565
+ chain: baseSepolia,
566
+ transport: http(),
567
+ });
568
+ const ERC20_ABI = parseAbi([
569
+ "function balanceOf(address owner) view returns (uint256)",
570
+ ]);
571
+ const balance = await publicClient.readContract({
572
+ address: tokenId,
573
+ abi: ERC20_ABI,
574
+ functionName: "balanceOf",
575
+ args: [userAddress],
576
+ });
577
+ actualBalance = balance.toString();
578
+ if (!silent) {
579
+ const { formatUnits } = await import("viem");
580
+ console.log(`💼 On-chain balance: ${formatUnits(balance, 18)} tokens`);
581
+ }
582
+ }
583
+ else {
584
+ // Some other error - rethrow
585
+ throw error;
586
+ }
587
+ }
425
588
  }
426
- // Use the resolved tokenId from info instead of resolving again
427
- const resolvedTokenId = info.tokenId;
428
- if (!resolvedTokenId) {
429
- throw new Error(`Failed to get token ID from token info. Original identifier: ${tokenId}`);
589
+ else {
590
+ // UUID token - get token info
591
+ info = await withLoading(() => getTokenInfo(client, tokenId, userAddress, silent), {
592
+ message: "Checking token info...",
593
+ json: globalOpts.json,
594
+ quiet: globalOpts.quiet,
595
+ spinner: "cat",
596
+ });
597
+ actualBalance = info.userPosition?.tokensOwned || "0";
598
+ // For graduated tokens, check actual on-chain balance
599
+ if (info && info.status === "graduated") {
600
+ if (!silent) {
601
+ console.log("📊 Checking on-chain balance for graduated token...");
602
+ }
603
+ const { createPublicClient, http, parseAbi } = await import("viem");
604
+ const { baseSepolia } = await import("viem/chains");
605
+ const publicClient = createPublicClient({
606
+ chain: baseSepolia,
607
+ transport: http(),
608
+ });
609
+ const ERC20_ABI = parseAbi([
610
+ "function balanceOf(address owner) view returns (uint256)",
611
+ ]);
612
+ const balance = await publicClient.readContract({
613
+ address: info.address,
614
+ abi: ERC20_ABI,
615
+ functionName: "balanceOf",
616
+ args: [userAddress],
617
+ });
618
+ actualBalance = balance.toString();
619
+ if (!silent) {
620
+ const { formatUnits } = await import("viem");
621
+ console.log(`💼 On-chain balance: ${formatUnits(balance, 18)} tokens`);
622
+ }
623
+ }
430
624
  }
431
- const tokenAmount = parseTokenAmount(amountInput, info.userPosition.tokensOwned);
432
- const result = await withLoading(() => sellToken(client, resolvedTokenId, tokenAmount, silent), {
625
+ if (actualBalance === "0") {
626
+ throw new Error("You do not own any of this token");
627
+ }
628
+ const tokenAmount = parseTokenAmount(amountInput, actualBalance);
629
+ const result = await withLoading(() => sellToken(client, tokenId, tokenAmount, silent, privateKey), {
433
630
  message: "Selling tokens...",
434
631
  json: globalOpts.json,
435
632
  quiet: globalOpts.quiet,
@@ -454,11 +651,89 @@ Examples:
454
651
  process.exit(getExitCode(error));
455
652
  }
456
653
  });
654
+ // Claim command
655
+ program
656
+ .command("claim")
657
+ .description("View and claim accumulated LP fees for graduated tokens")
658
+ .argument("<identifier>", 'Address, name, or symbol (e.g., 0x1234..., "My Token", or MTK)')
659
+ .option("-e, --execute", "Execute the claim (default: view only)")
660
+ .option("-A, --address <address>", "Caller address (defaults to wallet address)")
661
+ .addHelpText("after", `
662
+ Examples:
663
+ httpcat claim "MyToken" # View accumulated fees
664
+ httpcat claim MTK --execute # Claim fees
665
+ httpcat claim 0x4C64... --execute # Claim using contract address
666
+ httpcat --json claim "MyToken"
667
+ `)
668
+ .action(async (identifier, options, command) => {
669
+ try {
670
+ const globalOpts = command.parent?.opts() || {};
671
+ const accountIndex = globalOpts.account;
672
+ // Ensure wallet is unlocked
673
+ await ensureWalletUnlocked();
674
+ let privateKey = getPrivateKey(globalOpts.privateKey, accountIndex);
675
+ // If not configured and not in JSON mode, prompt interactively
676
+ if (!isConfigured(globalOpts.privateKey)) {
677
+ if (globalOpts.json) {
678
+ console.error('❌ Not configured. Run "httpcat config" first or use --private-key flag.');
679
+ process.exit(2);
680
+ }
681
+ // Interactive prompt for private key
682
+ privateKey = await promptForPrivateKey();
683
+ }
684
+ const client = await HttpcatClient.create(privateKey);
685
+ const silent = globalOpts.json || globalOpts.quiet;
686
+ if (options.execute) {
687
+ // Claim fees
688
+ // Derive caller address from private key if not provided
689
+ const account = privateKeyToAccount(privateKey);
690
+ const callerAddress = options.address || account.address;
691
+ const result = await withLoading(() => claimFees(client, identifier, callerAddress, silent), {
692
+ message: "Claiming fees...",
693
+ json: globalOpts.json,
694
+ quiet: globalOpts.quiet,
695
+ spinner: "cat",
696
+ });
697
+ if (globalOpts.json) {
698
+ outputJson("claim_fees", result);
699
+ }
700
+ else if (!globalOpts.quiet) {
701
+ displayClaimResult(result);
702
+ }
703
+ }
704
+ else {
705
+ // View fees only
706
+ const result = await withLoading(() => viewFees(client, identifier, silent), {
707
+ message: "Fetching fee information...",
708
+ json: globalOpts.json,
709
+ quiet: globalOpts.quiet,
710
+ spinner: "cat",
711
+ });
712
+ if (globalOpts.json) {
713
+ outputJson("view_fees", result);
714
+ }
715
+ else if (!globalOpts.quiet) {
716
+ displayFees(result);
717
+ }
718
+ }
719
+ process.exit(0);
720
+ }
721
+ catch (error) {
722
+ const globalOpts = command.parent?.opts() || {};
723
+ if (globalOpts.json) {
724
+ outputError("claim", error, getExitCode(error));
725
+ }
726
+ else {
727
+ handleError(error, globalOpts.verbose);
728
+ }
729
+ process.exit(getExitCode(error));
730
+ }
731
+ });
457
732
  // Info command
458
733
  program
459
734
  .command("info")
460
735
  .description("Get detailed information about a token")
461
- .argument("<identifier>", 'Token ID (UUID), name, or symbol (e.g., abc123-..., "My Token", or MTK)')
736
+ .argument("<identifier>", 'Address, name, or symbol (e.g., 0x1234..., "My Token", or MTK)')
462
737
  .addHelpText("after", `
463
738
  Examples:
464
739
  httpcat info abc123-4567-89ab-cdef-0123456789ab
@@ -497,7 +772,7 @@ Examples:
497
772
  outputJson("token_info", result);
498
773
  }
499
774
  else if (!globalOpts.quiet) {
500
- displayTokenInfo(result);
775
+ await displayTokenInfo(result, userAddress);
501
776
  }
502
777
  process.exit(0);
503
778
  }
@@ -516,9 +791,9 @@ Examples:
516
791
  program
517
792
  .command("list")
518
793
  .description("List all tokens with pagination and sorting")
519
- .option("--page <number>", "Page number (default: 1)", "1")
520
- .option("--limit <number>", "Items per page (default: 20, max: 100)", "20")
521
- .option("--sort <field>", "Sort by: mcap (market cap), created (date), or name (alphabetical)", "mcap")
794
+ .option("-p, --page <number>", "Page number (default: 1)", "1")
795
+ .option("-l, --limit <number>", "Items per page (default: 20, max: 100)", "20")
796
+ .option("-s, --sort <field>", "Sort by: mcap (market cap), created (date), or name (alphabetical)", "mcap")
522
797
  .addHelpText("after", `
523
798
  Examples:
524
799
  httpcat list
@@ -571,18 +846,18 @@ Examples:
571
846
  // Transactions command
572
847
  program
573
848
  .command("transactions")
574
- .description("Get paginated list of transactions with optional filtering")
575
- .option("--user <address>", "Filter by user address")
576
- .option("--token <tokenId>", "Filter by token ID")
577
- .option("--type <type>", "Filter by type: buy, sell, or airdrop")
578
- .option("--limit <number>", "Number of results (default: 50, max: 100)", "50")
579
- .option("--offset <number>", "Pagination offset (default: 0)", "0")
849
+ .description("Get paginated list of transactions (defaults to selected account)")
850
+ .option("-u, --user <address>", "Filter by user address (defaults to selected account if not provided)")
851
+ .option("-t, --token <tokenId>", "Filter by token ID")
852
+ .option("-T, --type <type>", "Filter by type: buy, sell, or airdrop")
853
+ .option("-l, --limit <number>", "Number of results (default: 50, max: 100)", "50")
854
+ .option("-o, --offset <number>", "Pagination offset (default: 0)", "0")
580
855
  .addHelpText("after", `
581
856
  Examples:
582
- httpcat transactions
583
- httpcat transactions --user 0x1234...
584
- httpcat transactions --token abc123-...
585
- httpcat transactions --type buy --limit 20
857
+ httpcat transactions # Get transactions for selected account
858
+ httpcat transactions --user 0x1234... # Get transactions for specific address
859
+ httpcat transactions --token abc123-... # Get transactions for a token
860
+ httpcat transactions --type buy --limit 20 # Get buy transactions for selected account
586
861
  httpcat --json transactions --user 0x1234... --offset 50
587
862
  `)
588
863
  .action(async (options, command) => {
@@ -602,9 +877,37 @@ Examples:
602
877
  privateKey = await promptForPrivateKey();
603
878
  }
604
879
  const client = await HttpcatClient.create(privateKey);
880
+ // Derive user address from private key (default to selected account if no --user option)
881
+ const account = privateKeyToAccount(privateKey);
882
+ const userAddress = account.address;
883
+ // Show which account is being used (if not quiet and not json)
884
+ if (!globalOpts.quiet && !globalOpts.json) {
885
+ const activeIndex = accountIndex !== undefined
886
+ ? accountIndex
887
+ : config.getActiveAccountIndex();
888
+ const accounts = config.getAllAccounts();
889
+ const accountInfo = accounts.find((acc) => acc.index === activeIndex);
890
+ if (accountInfo) {
891
+ // Verify the address matches
892
+ if (accountInfo.address.toLowerCase() !== userAddress.toLowerCase()) {
893
+ console.log(chalk.red(`⚠️ Warning: Account address mismatch!`));
894
+ console.log(chalk.red(` Expected: ${accountInfo.address}`));
895
+ console.log(chalk.red(` Got: ${userAddress}`));
896
+ console.log();
897
+ }
898
+ console.log(chalk.dim(`Using account ${activeIndex} (${accountInfo.type === "custom" ? "Custom" : "Seed-Derived"}): ${formatAddress(userAddress, 12)}`));
899
+ console.log();
900
+ }
901
+ }
605
902
  const input = {};
606
- if (options.user)
903
+ // Use selected account's address by default, unless --user is explicitly provided
904
+ if (options.user) {
607
905
  input.userAddress = options.user;
906
+ }
907
+ else {
908
+ // Default to selected account's address
909
+ input.userAddress = userAddress;
910
+ }
608
911
  if (options.token)
609
912
  input.tokenId = options.token;
610
913
  if (options.type) {
@@ -646,9 +949,13 @@ Examples:
646
949
  program
647
950
  .command("positions")
648
951
  .description("Get all your positions with comprehensive information")
952
+ .option("-a, --active", "Show only active (non-graduated) positions")
953
+ .option("-g, --graduated", "Show only graduated positions")
649
954
  .addHelpText("after", `
650
955
  Examples:
651
956
  httpcat positions
957
+ httpcat positions --active
958
+ httpcat positions --graduated
652
959
  httpcat --json positions
653
960
  httpcat --private-key 0x... positions
654
961
  `)
@@ -674,7 +981,9 @@ Examples:
674
981
  const userAddress = account.address;
675
982
  // Show which account is being used and verify it matches (if not quiet and not json)
676
983
  if (!globalOpts.quiet && !globalOpts.json) {
677
- const activeIndex = accountIndex !== undefined ? accountIndex : config.getActiveAccountIndex();
984
+ const activeIndex = accountIndex !== undefined
985
+ ? accountIndex
986
+ : config.getActiveAccountIndex();
678
987
  const accounts = config.getAllAccounts();
679
988
  const accountInfo = accounts.find((acc) => acc.index === activeIndex);
680
989
  if (accountInfo) {
@@ -689,17 +998,33 @@ Examples:
689
998
  console.log();
690
999
  }
691
1000
  }
692
- const result = await withLoading(() => getPositions(client, userAddress), {
1001
+ let result = await withLoading(() => getPositions(client, userAddress), {
693
1002
  message: "Fetching positions...",
694
1003
  json: globalOpts.json,
695
1004
  quiet: globalOpts.quiet,
696
1005
  spinner: "cat",
697
1006
  });
1007
+ const filter = command.active
1008
+ ? "active"
1009
+ : command.graduated
1010
+ ? "graduated"
1011
+ : "all";
698
1012
  if (globalOpts.json) {
1013
+ // For JSON output, filter the positions
1014
+ if (filter !== "all") {
1015
+ const filteredPositions = result.positions.filter((p) => filter === "active"
1016
+ ? p.token.status !== "graduated"
1017
+ : p.token.status === "graduated");
1018
+ result = {
1019
+ ...result,
1020
+ positions: filteredPositions,
1021
+ total: filteredPositions.length,
1022
+ };
1023
+ }
699
1024
  outputJson("positions", result);
700
1025
  }
701
1026
  else if (!globalOpts.quiet) {
702
- displayPositions(result);
1027
+ displayPositions(result, filter);
703
1028
  }
704
1029
  process.exit(0);
705
1030
  }
@@ -758,15 +1083,15 @@ Examples:
758
1083
  process.exit(getExitCode(error));
759
1084
  }
760
1085
  });
761
- // Balance command
1086
+ // Balances command
762
1087
  program
763
- .command("balance")
764
- .description("Check wallet balance (ETH and USDC)")
1088
+ .command("balances")
1089
+ .description("Check wallet balances (ETH and USDC)")
765
1090
  .addHelpText("after", `
766
1091
  Examples:
767
- httpcat balance
768
- httpcat balance --private-key 0x...
769
- httpcat --json balance
1092
+ httpcat balances
1093
+ httpcat balances --private-key 0x...
1094
+ httpcat --json balances
770
1095
  `)
771
1096
  .action(async (command) => {
772
1097
  try {
@@ -785,13 +1110,13 @@ Examples:
785
1110
  privateKey = undefined;
786
1111
  }
787
1112
  const result = await withLoading(() => checkBalance(privateKey), {
788
- message: "Checking balance...",
1113
+ message: "Checking balances...",
789
1114
  json: globalOpts.json,
790
1115
  quiet: globalOpts.quiet,
791
1116
  spinner: "cat",
792
1117
  });
793
1118
  if (globalOpts.json) {
794
- outputJson("balance", result);
1119
+ outputJson("balances", result);
795
1120
  }
796
1121
  else if (!globalOpts.quiet) {
797
1122
  displayBalance(result);
@@ -801,7 +1126,7 @@ Examples:
801
1126
  catch (error) {
802
1127
  const globalOpts = command.parent?.opts() || {};
803
1128
  if (globalOpts.json) {
804
- outputError("balance", error, getExitCode(error));
1129
+ outputError("balances", error, getExitCode(error));
805
1130
  }
806
1131
  else {
807
1132
  handleError(error, globalOpts.verbose);
@@ -837,7 +1162,7 @@ Examples:
837
1162
  outputJson("account", result);
838
1163
  }
839
1164
  else if (!globalOpts.quiet) {
840
- displayAccountInfo(result);
1165
+ await displayAccountInfo(result);
841
1166
  }
842
1167
  process.exit(0);
843
1168
  }
@@ -891,31 +1216,6 @@ accountCommand
891
1216
  process.exit(getExitCode(error));
892
1217
  }
893
1218
  });
894
- accountCommand
895
- .command("add")
896
- .description("Add a new seed-derived account")
897
- .addHelpText("after", `
898
- Examples:
899
- httpcat account add # Add a new account from existing seed phrase
900
- httpcat account add # If no seed phrase, prompts to generate/import one
901
- `)
902
- .action(async () => {
903
- try {
904
- const { addAccount } = await import("./commands/account.js");
905
- await addAccount();
906
- process.exit(0);
907
- }
908
- catch (error) {
909
- const globalOpts = accountCommand.parent?.opts() || {};
910
- if (globalOpts.json) {
911
- outputError("account_add", error, getExitCode(error));
912
- }
913
- else {
914
- handleError(error, globalOpts.verbose);
915
- }
916
- process.exit(getExitCode(error));
917
- }
918
- });
919
1219
  accountCommand
920
1220
  .command("add")
921
1221
  .description("Add a new seed-derived account")
@@ -945,7 +1245,7 @@ program
945
1245
  .command("chat")
946
1246
  .description("Start streaming chat ($0.01 to join, $0.0001 per message, 10 min lease)")
947
1247
  .argument("[token]", "Optional: Token symbol, name, or address for token-specific chat")
948
- .option("--input-format <format>", 'Input format (only works with --json): "text" (default), or "stream-json" (realtime streaming input)', "text")
1248
+ .option("-f, --input-format <format>", 'Input format (only works with --json): "text" (default), or "stream-json" (realtime streaming input)', "text")
949
1249
  .addHelpText("after", `
950
1250
  Examples:
951
1251
  httpcat chat # Join general chat
@@ -1019,6 +1319,14 @@ Configuration:
1019
1319
  .action(async () => {
1020
1320
  await runMcpServer();
1021
1321
  });
1322
+ // Help command
1323
+ program
1324
+ .command("help")
1325
+ .description("Display help for httpcat")
1326
+ .action(() => {
1327
+ program.outputHelp();
1328
+ process.exit(0);
1329
+ });
1022
1330
  // Interactive shell (default when no command)
1023
1331
  program.action(async (options) => {
1024
1332
  try {