httpcat-cli 0.2.12 → 0.2.13-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 (62) hide show
  1. package/README.md +346 -13
  2. package/Screenshot 2025-12-21 at 8.56.02/342/200/257PM.png +0 -0
  3. package/bun.lock +5 -0
  4. package/cat-spin.sh +417 -0
  5. package/dist/agent/ax-agent.d.ts.map +1 -0
  6. package/dist/agent/ax-agent.js +459 -0
  7. package/dist/agent/ax-agent.js.map +1 -0
  8. package/dist/agent/llm-factory.d.ts.map +1 -0
  9. package/dist/agent/llm-factory.js +82 -0
  10. package/dist/agent/llm-factory.js.map +1 -0
  11. package/dist/agent/setup-wizard.d.ts.map +1 -0
  12. package/dist/agent/setup-wizard.js +114 -0
  13. package/dist/agent/setup-wizard.js.map +1 -0
  14. package/dist/agent/tools.d.ts.map +1 -0
  15. package/dist/agent/tools.js +393 -0
  16. package/dist/agent/tools.js.map +1 -0
  17. package/dist/client.d.ts.map +1 -1
  18. package/dist/client.js +18 -0
  19. package/dist/client.js.map +1 -1
  20. package/dist/commands/balances.js +1 -1
  21. package/dist/commands/balances.js.map +1 -1
  22. package/dist/commands/buy.d.ts.map +1 -1
  23. package/dist/commands/buy.js +11 -6
  24. package/dist/commands/buy.js.map +1 -1
  25. package/dist/commands/chat.d.ts.map +1 -1
  26. package/dist/commands/chat.js +56 -46
  27. package/dist/commands/chat.js.map +1 -1
  28. package/dist/commands/create.d.ts.map +1 -1
  29. package/dist/commands/create.js +133 -5
  30. package/dist/commands/create.js.map +1 -1
  31. package/dist/commands/info.d.ts.map +1 -1
  32. package/dist/commands/info.js +3 -2
  33. package/dist/commands/info.js.map +1 -1
  34. package/dist/commands/positions.d.ts.map +1 -1
  35. package/dist/commands/positions.js +51 -54
  36. package/dist/commands/positions.js.map +1 -1
  37. package/dist/commands/sell.d.ts.map +1 -1
  38. package/dist/commands/sell.js +4 -3
  39. package/dist/commands/sell.js.map +1 -1
  40. package/dist/config.d.ts.map +1 -1
  41. package/dist/config.js +91 -0
  42. package/dist/config.js.map +1 -1
  43. package/dist/index.js +455 -37
  44. package/dist/index.js.map +1 -1
  45. package/dist/interactive/cat-spin.d.ts.map +1 -0
  46. package/dist/interactive/cat-spin.js +448 -0
  47. package/dist/interactive/cat-spin.js.map +1 -0
  48. package/dist/interactive/shell.d.ts.map +1 -1
  49. package/dist/interactive/shell.js +1647 -154
  50. package/dist/interactive/shell.js.map +1 -1
  51. package/dist/mcp/server.js +1 -1
  52. package/dist/mcp/tools.d.ts.map +1 -1
  53. package/dist/mcp/tools.js +107 -6
  54. package/dist/mcp/tools.js.map +1 -1
  55. package/dist/mcp/types.d.ts.map +1 -1
  56. package/dist/utils/loading.d.ts.map +1 -1
  57. package/dist/utils/loading.js +30 -0
  58. package/dist/utils/loading.js.map +1 -1
  59. package/dist/utils/token-resolver.d.ts.map +1 -1
  60. package/dist/utils/token-resolver.js +41 -0
  61. package/dist/utils/token-resolver.js.map +1 -1
  62. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -8,12 +8,14 @@ import { formatAddress, formatCurrency } from "./utils/formatting.js";
8
8
  import { HttpcatClient, HttpcatError } from "./client.js";
9
9
  import { handleError, getExitCode } from "./utils/errors.js";
10
10
  import { startInteractiveShell } from "./interactive/shell.js";
11
+ import { runCatSpinAnimation } from "./interactive/cat-spin.js";
11
12
  import { outputJson, outputError } from "./headless/json-output.js";
12
13
  import { promptForPrivateKey } from "./utils/privateKeyPrompt.js";
13
14
  import { withLoading } from "./utils/loading.js";
14
- import { readFileSync } from "fs";
15
+ import { readFileSync, writeFileSync, existsSync, statSync, unlinkSync, readlinkSync, } from "fs";
15
16
  import { fileURLToPath } from "url";
16
17
  import { dirname, join } from "path";
18
+ import { tmpdir } from "os";
17
19
  // Get version from package.json
18
20
  const __filename = fileURLToPath(import.meta.url);
19
21
  const __dirname = dirname(__filename);
@@ -22,7 +24,7 @@ const VERSION = packageJson.version;
22
24
  const require = createRequire(import.meta.url);
23
25
  const { version: PACKAGE_VERSION } = require("../package.json");
24
26
  // Import commands
25
- import { createToken, displayCreateResult } from "./commands/create.js";
27
+ import { createToken, displayCreateResult, processPhotoUrl, isFilePath, } from "./commands/create.js";
26
28
  import { buyToken, displayBuyResult, displayBuyResultCompact, } from "./commands/buy.js";
27
29
  import { sellToken, displaySellResult, parseTokenAmount, } from "./commands/sell.js";
28
30
  import { getTokenInfo, displayTokenInfo } from "./commands/info.js";
@@ -273,18 +275,31 @@ program
273
275
  .description("Create a new token")
274
276
  .argument("<name>", 'Token name (e.g., "My Token")')
275
277
  .argument("<symbol>", 'Token symbol/ticker (e.g., "MTK", 3-10 characters)')
276
- .option("-p, --photo <url>", "Photo URL for token logo")
277
- .option("-b, --banner <url>", "Banner image URL")
278
+ .option("-p, --photo <url|path>", "Photo URL or file path for token logo")
278
279
  .option("-w, --website <url>", "Website URL")
279
280
  .addHelpText("after", `
280
281
  Examples:
281
282
  httpcat create "My Token" "MTK"
282
283
  httpcat create "Moon Cat" "MOON" --website https://mooncat.io
283
- httpcat create "Test" "TEST" --photo https://example.com/logo.png --banner https://example.com/banner.png
284
+ httpcat create "Test" "TEST" --photo https://example.com/logo.png
285
+ httpcat create "Test" "TEST" --photo ./logo.png
284
286
  httpcat --json create "AI Token" "AI"
285
287
  `)
286
288
  .action(async (name, symbol, options, command) => {
287
289
  try {
290
+ // Fallback: get arguments from command object if not provided as parameters
291
+ // This handles cases where Commander.js doesn't parse arguments correctly
292
+ // In Commander.js v9+, use processedArgs; fallback to args for older versions
293
+ const args = command.processedArgs || command.args || [];
294
+ const actualName = name || args[0];
295
+ const actualSymbol = symbol || args[1];
296
+ // Validate required arguments
297
+ if (!actualName || typeof actualName !== "string") {
298
+ throw new Error("Token name is required. Usage: httpcat create <name> <symbol> [options]");
299
+ }
300
+ if (!actualSymbol || typeof actualSymbol !== "string") {
301
+ throw new Error("Token symbol is required. Usage: httpcat create <name> <symbol> [options]");
302
+ }
288
303
  const globalOpts = command.parent?.opts() || {};
289
304
  const accountIndex = globalOpts.account;
290
305
  // Ensure wallet is unlocked
@@ -300,11 +315,21 @@ Examples:
300
315
  privateKey = await promptForPrivateKey();
301
316
  }
302
317
  const client = await HttpcatClient.create(privateKey);
318
+ // Process photo if it's a file path (show loading state)
319
+ let processedPhotoUrl = options.photo;
320
+ if (options.photo && isFilePath(options.photo)) {
321
+ processedPhotoUrl = await withLoading(async () => processPhotoUrl(options.photo), {
322
+ message: "Uploading image...",
323
+ json: globalOpts.json,
324
+ quiet: globalOpts.quiet,
325
+ spinner: "cat",
326
+ });
327
+ }
328
+ // Create token (show loading state)
303
329
  const result = await withLoading(() => createToken(client, {
304
- name,
305
- symbol,
306
- photoUrl: options.photo,
307
- bannerUrl: options.banner,
330
+ name: actualName.trim(),
331
+ symbol: actualSymbol.trim(),
332
+ photoUrl: processedPhotoUrl,
308
333
  websiteUrl: options.website,
309
334
  }), {
310
335
  message: "Creating token...",
@@ -401,16 +426,66 @@ Examples:
401
426
  let stopReason = "";
402
427
  for (let i = 1; i <= repeatCount; i++) {
403
428
  try {
404
- // Show progress for non-JSON mode
405
- if (!globalOpts.json && !globalOpts.quiet) {
406
- console.log(chalk.dim(`Buy ${i}/${repeatCount}...`));
429
+ // Retry logic: try up to 10 times with exponential backoff on 402 errors
430
+ // Client is recreated on each attempt to ensure fresh signature for new nonce
431
+ let result = null;
432
+ let lastError = null;
433
+ const maxRetries = 10;
434
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
435
+ try {
436
+ // Show progress for non-JSON mode
437
+ if (!globalOpts.json && !globalOpts.quiet) {
438
+ if (attempt === 1) {
439
+ console.log(chalk.dim(`Buy ${i}/${repeatCount}...`));
440
+ }
441
+ else {
442
+ console.log(chalk.yellow(` Retrying buy ${i}/${repeatCount} (attempt ${attempt}/${maxRetries})...`));
443
+ }
444
+ }
445
+ // Recreate client inside the operation function to ensure fresh signature
446
+ // for each attempt (including retries). This fixes the issue where x402-fetch
447
+ // generates a new nonce but reuses the same signature, causing subsequent buys to fail
448
+ result = await withLoading(async () => {
449
+ // Recreate client for each attempt to ensure fresh signature for new nonce
450
+ const client = await HttpcatClient.create(privateKey);
451
+ return buyToken(client, tokenId, finalAmount, isTestMode, silent || i > 1, privateKey);
452
+ }, {
453
+ message: `Buying tokens (${i}/${repeatCount})...`,
454
+ json: globalOpts.json,
455
+ quiet: globalOpts.quiet || i > 1,
456
+ spinner: "cat",
457
+ });
458
+ // Success! Break out of retry loop
459
+ break;
460
+ }
461
+ catch (error) {
462
+ lastError = error;
463
+ // Only retry on 402 errors (payment required)
464
+ const is402Error = error instanceof HttpcatError && error.status === 402;
465
+ if (is402Error && attempt < maxRetries) {
466
+ // Exponential backoff: 1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s (capped at 10s)
467
+ const backoffMs = Math.min(Math.pow(2, attempt - 1) * 1000, 10000);
468
+ if (!globalOpts.json && !globalOpts.quiet) {
469
+ console.log(chalk.yellow(` ⚠️ Payment required (attempt ${attempt}/${maxRetries}), retrying in ${backoffMs / 1000}s...`));
470
+ }
471
+ await new Promise((resolve) => setTimeout(resolve, backoffMs));
472
+ continue; // Retry
473
+ }
474
+ else {
475
+ // Not a 402 error, or max retries reached - throw the error
476
+ throw error;
477
+ }
478
+ }
479
+ }
480
+ // If we exhausted retries, throw the last error
481
+ if (!result) {
482
+ if (lastError) {
483
+ throw lastError;
484
+ }
485
+ else {
486
+ throw new Error(`Failed to complete buy ${i}/${repeatCount} after ${maxRetries} attempts`);
487
+ }
407
488
  }
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
489
  results.push(result);
415
490
  totalSpent += parseFloat(result.amountSpent);
416
491
  // Display compact result for each buy
@@ -431,23 +506,68 @@ Examples:
431
506
  }
432
507
  break;
433
508
  }
509
+ // Wait for transaction confirmations if present
510
+ // This ensures both the buy transaction and payment transaction are confirmed
511
+ // before the next buy, preventing nonce/signature conflicts
512
+ if (i < repeatCount) {
513
+ const { createPublicClient, http } = await import("viem");
514
+ const { baseSepolia } = await import("viem/chains");
515
+ const publicClient = createPublicClient({
516
+ chain: baseSepolia,
517
+ transport: http(config.getRpcUrl()),
518
+ });
519
+ // Wait for payment transaction first (if present)
520
+ // This is critical to ensure the payment nonce is consumed before next request
521
+ if (result.paymentTxHash) {
522
+ if (!globalOpts.json && !globalOpts.quiet) {
523
+ console.log(chalk.dim(` Waiting for payment transaction confirmation...`));
524
+ }
525
+ try {
526
+ await publicClient.waitForTransactionReceipt({
527
+ hash: result.paymentTxHash,
528
+ });
529
+ if (!globalOpts.json && !globalOpts.quiet) {
530
+ console.log(chalk.dim(` ✅ Payment transaction confirmed`));
531
+ }
532
+ }
533
+ catch (txError) {
534
+ // If we can't wait for confirmation, log but continue
535
+ if (!globalOpts.json && !globalOpts.quiet) {
536
+ console.log(chalk.yellow(` ⚠️ Could not confirm payment transaction, proceeding...`));
537
+ }
538
+ }
539
+ }
540
+ // Wait for buy transaction (if present)
541
+ if (result.txHash) {
542
+ if (!globalOpts.json && !globalOpts.quiet) {
543
+ console.log(chalk.dim(` Waiting for buy transaction confirmation...`));
544
+ }
545
+ try {
546
+ await publicClient.waitForTransactionReceipt({
547
+ hash: result.txHash,
548
+ });
549
+ if (!globalOpts.json && !globalOpts.quiet) {
550
+ console.log(chalk.dim(` ✅ Buy transaction confirmed`));
551
+ }
552
+ }
553
+ catch (txError) {
554
+ // If we can't wait for confirmation, log but continue
555
+ if (!globalOpts.json && !globalOpts.quiet) {
556
+ console.log(chalk.yellow(` ⚠️ Could not confirm buy transaction, proceeding...`));
557
+ }
558
+ }
559
+ }
560
+ }
434
561
  // Apply delay between iterations (except after the last one)
435
562
  if (i < repeatCount && delayMs > 0) {
436
563
  await new Promise((resolve) => setTimeout(resolve, delayMs));
437
564
  }
438
565
  }
439
566
  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
567
+ // Don't catch 402 errors - x402-fetch handles payment automatically
568
+ // 402 is the normal payment flow on first call, not an error
569
+ // Let withLoading and x402-fetch handle payment retries
570
+ // Only re-throw to let outer handler deal with persistent errors
451
571
  throw error;
452
572
  }
453
573
  }
@@ -565,7 +685,7 @@ Examples:
565
685
  const { baseSepolia } = await import("viem/chains");
566
686
  const publicClient = createPublicClient({
567
687
  chain: baseSepolia,
568
- transport: http(),
688
+ transport: http(config.getRpcUrl()),
569
689
  });
570
690
  const ERC20_ABI = parseAbi([
571
691
  "function balanceOf(address owner) view returns (uint256)",
@@ -606,7 +726,7 @@ Examples:
606
726
  const { baseSepolia } = await import("viem/chains");
607
727
  const publicClient = createPublicClient({
608
728
  chain: baseSepolia,
609
- transport: http(),
729
+ transport: http(config.getRpcUrl()),
610
730
  });
611
731
  const ERC20_ABI = parseAbi([
612
732
  "function balanceOf(address owner) view returns (uint256)",
@@ -1276,10 +1396,16 @@ Examples:
1276
1396
  privateKey = await promptForPrivateKey();
1277
1397
  }
1278
1398
  const client = await HttpcatClient.create(privateKey);
1279
- const inputFormat = globalOpts.json
1280
- ? chatOpts.inputFormat || "text"
1281
- : "text";
1282
- await startChatStream(client, globalOpts.json || false, tokenIdentifier || undefined, inputFormat);
1399
+ // If JSON mode, use the original chat stream (for non-interactive usage)
1400
+ if (globalOpts.json) {
1401
+ const inputFormat = chatOpts.inputFormat || "text";
1402
+ await startChatStream(client, true, // jsonMode
1403
+ tokenIdentifier || undefined, inputFormat);
1404
+ }
1405
+ else {
1406
+ // Interactive mode: launch shell and auto-enter chat
1407
+ await startInteractiveShell(client, tokenIdentifier || undefined);
1408
+ }
1283
1409
  }
1284
1410
  catch (error) {
1285
1411
  const globalOpts = command.parent?.opts() || {};
@@ -1292,6 +1418,108 @@ Examples:
1292
1418
  process.exit(getExitCode(error));
1293
1419
  }
1294
1420
  });
1421
+ // Agent setup command
1422
+ program
1423
+ .command("agent")
1424
+ .description("Configure AI agent for trading assistance")
1425
+ .option("-s, --setup", "Run setup wizard to configure API key")
1426
+ .addHelpText("after", `
1427
+ Examples:
1428
+ httpcat agent --setup # Configure AI agent
1429
+
1430
+ The AI agent helps you:
1431
+ • Trade tokens using natural language
1432
+ • Check balances and positions
1433
+ • Get market information
1434
+ • Execute commands hands-free
1435
+
1436
+ Use 'httpcat cat' to start interactive agent mode.
1437
+ `)
1438
+ .action(async (options) => {
1439
+ try {
1440
+ const { setupAIAgentWizard } = await import("./agent/setup-wizard.js");
1441
+ if (options.setup) {
1442
+ await setupAIAgentWizard();
1443
+ process.exit(0);
1444
+ }
1445
+ else {
1446
+ // Show help for agent command
1447
+ console.log();
1448
+ console.log(chalk.cyan.bold("🐱 AI Agent Setup"));
1449
+ console.log();
1450
+ console.log(chalk.bold("To configure:"));
1451
+ console.log(chalk.dim(" httpcat agent --setup"));
1452
+ console.log();
1453
+ console.log(chalk.bold("To start agent mode:"));
1454
+ console.log(chalk.dim(" httpcat cat"));
1455
+ console.log();
1456
+ process.exit(0);
1457
+ }
1458
+ }
1459
+ catch (error) {
1460
+ handleError(error, program.opts().verbose);
1461
+ process.exit(getExitCode(error));
1462
+ }
1463
+ });
1464
+ // Cat command - starts agent interactive mode
1465
+ program
1466
+ .command("cat")
1467
+ .description("Start interactive AI agent mode")
1468
+ .addHelpText("after", `
1469
+ Examples:
1470
+ httpcat cat # Start interactive agent mode
1471
+
1472
+ The AI agent helps you:
1473
+ • Trade tokens using natural language
1474
+ • Check balances and positions
1475
+ • Get market information
1476
+ • Execute commands hands-free
1477
+
1478
+ Type /exit to quit agent mode.
1479
+ `)
1480
+ .action(async (options, command) => {
1481
+ try {
1482
+ const globalOpts = command.parent?.opts() || {};
1483
+ const accountIndex = globalOpts.account;
1484
+ // Ensure wallet is unlocked
1485
+ await ensureWalletUnlocked();
1486
+ let privateKey = getPrivateKey(globalOpts.privateKey, accountIndex);
1487
+ // If not configured and not in JSON mode, prompt interactively
1488
+ if (!isConfigured(globalOpts.privateKey)) {
1489
+ if (globalOpts.json) {
1490
+ console.error('❌ Not configured. Run "httpcat config" first or use --private-key flag.');
1491
+ process.exit(2);
1492
+ }
1493
+ // Interactive prompt for private key
1494
+ privateKey = await promptForPrivateKey();
1495
+ }
1496
+ const client = await HttpcatClient.create(privateKey);
1497
+ // Check if agent is configured
1498
+ const agentConfig = config.getAIAgentConfig();
1499
+ if (!agentConfig) {
1500
+ console.log();
1501
+ console.log(chalk.yellow("⚠️ Agent not configured"));
1502
+ console.log(chalk.dim("Run 'httpcat agent --setup' to configure the AI agent."));
1503
+ console.log();
1504
+ process.exit(1);
1505
+ }
1506
+ const apiKey = config.getAIAgentApiKey();
1507
+ if (!apiKey) {
1508
+ console.log();
1509
+ console.log(chalk.red("❌ Failed to get API key"));
1510
+ console.log(chalk.dim("Run 'httpcat agent --setup' to configure the API key."));
1511
+ console.log();
1512
+ process.exit(1);
1513
+ }
1514
+ // Start agent interactive mode
1515
+ const { startAgentInteractiveMode } = await import("./interactive/shell.js");
1516
+ await startAgentInteractiveMode(client);
1517
+ }
1518
+ catch (error) {
1519
+ handleError(error, program.opts().verbose);
1520
+ process.exit(getExitCode(error));
1521
+ }
1522
+ });
1295
1523
  // MCP Server command
1296
1524
  program
1297
1525
  .command("mcp-server")
@@ -1305,7 +1533,7 @@ The server communicates via stdio and can be used with MCP clients like Cursor o
1305
1533
 
1306
1534
  Configuration:
1307
1535
  Add to your MCP client config (e.g., Cursor settings):
1308
-
1536
+
1309
1537
  {
1310
1538
  "mcpServers": {
1311
1539
  "httpcat": {
@@ -1321,14 +1549,182 @@ Configuration:
1321
1549
  .action(async () => {
1322
1550
  await runMcpServer();
1323
1551
  });
1552
+ // Custom help formatting with grouped commands
1553
+ function displayGroupedHelp(program) {
1554
+ console.log();
1555
+ console.log(chalk.cyan.bold("httpcat - CLI tool for interacting with httpcat agent"));
1556
+ console.log();
1557
+ console.log(chalk.bold("Usage:"));
1558
+ console.log(chalk.dim(" httpcat [options] <command>"));
1559
+ console.log();
1560
+ console.log(chalk.bold("Global Options:"));
1561
+ program.options.forEach((opt) => {
1562
+ const flags = opt.flags;
1563
+ const description = opt.description || "";
1564
+ console.log(` ${chalk.green(flags.padEnd(30))} ${chalk.dim(description)}`);
1565
+ });
1566
+ console.log();
1567
+ // Group commands
1568
+ const commandGroups = {
1569
+ "Token Operations": ["create", "buy", "sell", "claim"],
1570
+ "Token Information": ["info", "list"],
1571
+ Portfolio: ["positions", "transactions", "balances"],
1572
+ "Account Management": ["account", "env"],
1573
+ "AI & Social": ["agent", "chat"],
1574
+ System: ["health", "config", "mcp-server"],
1575
+ };
1576
+ // Display grouped commands
1577
+ for (const [groupName, commandNames] of Object.entries(commandGroups)) {
1578
+ const commands = program.commands.filter((cmd) => commandNames.includes(cmd.name()));
1579
+ if (commands.length > 0) {
1580
+ console.log(chalk.bold(chalk.cyan(`${groupName}:`)));
1581
+ commands.forEach((cmd) => {
1582
+ // Build usage string from command name and arguments
1583
+ const args = cmd.registeredArguments
1584
+ .map((arg) => {
1585
+ const nameOut = arg.name() + (arg.variadic ? "..." : "");
1586
+ return arg.required ? `<${nameOut}>` : `[${nameOut}]`;
1587
+ })
1588
+ .join(" ");
1589
+ const usage = args ? `${cmd.name()} ${args}` : cmd.name();
1590
+ const description = cmd.description() || "";
1591
+ console.log(` ${chalk.green(usage.padEnd(30))} ${chalk.dim(description)}`);
1592
+ });
1593
+ console.log();
1594
+ }
1595
+ }
1596
+ // Show subcommands for account and env
1597
+ const accountCmd = program.commands.find((c) => c.name() === "account");
1598
+ if (accountCmd) {
1599
+ const subcommands = accountCmd.commands;
1600
+ if (subcommands.length > 0) {
1601
+ console.log(chalk.bold(chalk.cyan("Account Subcommands:")));
1602
+ subcommands.forEach((cmd) => {
1603
+ const args = cmd.registeredArguments
1604
+ .map((arg) => {
1605
+ const nameOut = arg.name() + (arg.variadic ? "..." : "");
1606
+ return arg.required ? `<${nameOut}>` : `[${nameOut}]`;
1607
+ })
1608
+ .join(" ");
1609
+ const usage = args
1610
+ ? `account ${cmd.name()} ${args}`
1611
+ : `account ${cmd.name()}`;
1612
+ const description = cmd.description() || "";
1613
+ console.log(` ${chalk.green(usage.padEnd(30))} ${chalk.dim(description)}`);
1614
+ });
1615
+ console.log();
1616
+ }
1617
+ }
1618
+ const envCmd = program.commands.find((c) => c.name() === "env");
1619
+ if (envCmd) {
1620
+ const subcommands = envCmd.commands;
1621
+ if (subcommands.length > 0) {
1622
+ console.log(chalk.bold(chalk.cyan("Environment Subcommands:")));
1623
+ subcommands.forEach((cmd) => {
1624
+ const args = cmd.registeredArguments
1625
+ .map((arg) => {
1626
+ const nameOut = arg.name() + (arg.variadic ? "..." : "");
1627
+ return arg.required ? `<${nameOut}>` : `[${nameOut}]`;
1628
+ })
1629
+ .join(" ");
1630
+ const usage = args ? `env ${cmd.name()} ${args}` : `env ${cmd.name()}`;
1631
+ const description = cmd.description() || "";
1632
+ console.log(` ${chalk.green(usage.padEnd(30))} ${chalk.dim(description)}`);
1633
+ });
1634
+ console.log();
1635
+ }
1636
+ }
1637
+ console.log(chalk.dim("Run 'httpcat <command> --help' for more information on a command."));
1638
+ console.log();
1639
+ }
1640
+ // Override the default help output using configureHelp
1641
+ program.configureHelp({
1642
+ commandUsage: (cmd) => {
1643
+ const name = cmd.name();
1644
+ const args = cmd.registeredArguments
1645
+ .map((arg) => {
1646
+ const nameOut = arg.name() + (arg.variadic ? "..." : "");
1647
+ return arg.required ? `<${nameOut}>` : `[${nameOut}]`;
1648
+ })
1649
+ .join(" ");
1650
+ return `${cmd.parent?.name() || "httpcat"} ${name}${args ? ` ${args}` : ""}`;
1651
+ },
1652
+ subcommandTerm: (cmd) => cmd.name(),
1653
+ });
1324
1654
  // Help command
1325
1655
  program
1326
1656
  .command("help")
1327
1657
  .description("Display help for httpcat")
1328
1658
  .action(() => {
1329
- program.outputHelp();
1659
+ displayGroupedHelp(program);
1330
1660
  process.exit(0);
1331
1661
  });
1662
+ // Helper function to get a unique terminal session identifier
1663
+ function getTerminalSessionId() {
1664
+ // Try to get terminal TTY device (most unique per terminal window)
1665
+ // This works on Linux/Unix systems
1666
+ try {
1667
+ if (process.stdin.isTTY && process.stdin.fd !== undefined) {
1668
+ const ttyPath = readlinkSync(`/proc/self/fd/${process.stdin.fd}`).replace(/^\/dev\//, "");
1669
+ if (ttyPath && ttyPath !== "stdin") {
1670
+ return `${process.env.USER || "default"}-${ttyPath}`;
1671
+ }
1672
+ }
1673
+ }
1674
+ catch {
1675
+ // Fall through to other methods (e.g., on macOS or if /proc doesn't exist)
1676
+ }
1677
+ // Fall back to environment variables or user/term combination
1678
+ return (process.env.TERM_SESSION_ID ||
1679
+ process.env.SESSION_ID ||
1680
+ `${process.env.USER || "default"}-${process.env.TERM || "unknown"}`);
1681
+ }
1682
+ // Helper function to check if animation has been shown in this terminal session
1683
+ function hasAnimationBeenShown() {
1684
+ // Check environment variable first (user can set this manually)
1685
+ if (process.env.HTTPCAT_ANIMATION_SHOWN === "1") {
1686
+ return true;
1687
+ }
1688
+ // Check for marker file in temp directory
1689
+ const sessionId = getTerminalSessionId();
1690
+ const markerFile = join(tmpdir(), `httpcat-animation-${sessionId.replace(/[^a-zA-Z0-9]/g, "-")}.marker`);
1691
+ if (existsSync(markerFile)) {
1692
+ // Check if file is recent (within last hour) to handle stale files
1693
+ try {
1694
+ const stats = statSync(markerFile);
1695
+ const ageMs = Date.now() - stats.mtimeMs;
1696
+ const oneHour = 60 * 60 * 1000;
1697
+ if (ageMs < oneHour) {
1698
+ return true;
1699
+ }
1700
+ // File is stale, remove it
1701
+ try {
1702
+ unlinkSync(markerFile);
1703
+ }
1704
+ catch {
1705
+ // Ignore errors when removing stale file
1706
+ }
1707
+ }
1708
+ catch {
1709
+ // If we can't read the file, assume animation hasn't been shown
1710
+ }
1711
+ }
1712
+ return false;
1713
+ }
1714
+ // Helper function to mark animation as shown
1715
+ function markAnimationAsShown() {
1716
+ // Set environment variable for current process
1717
+ process.env.HTTPCAT_ANIMATION_SHOWN = "1";
1718
+ // Create marker file for terminal session persistence
1719
+ try {
1720
+ const sessionId = getTerminalSessionId();
1721
+ const markerFile = join(tmpdir(), `httpcat-animation-${sessionId.replace(/[^a-zA-Z0-9]/g, "-")}.marker`);
1722
+ writeFileSync(markerFile, Date.now().toString(), { flag: "w" });
1723
+ }
1724
+ catch {
1725
+ // If we can't write the marker file, that's okay - env var will still work for this process
1726
+ }
1727
+ }
1332
1728
  // Interactive shell (default when no command)
1333
1729
  program.action(async (options) => {
1334
1730
  try {
@@ -1343,6 +1739,28 @@ program.action(async (options) => {
1343
1739
  await config.runSetupWizard();
1344
1740
  console.log();
1345
1741
  }
1742
+ // Run cat spin animation if conditions are met
1743
+ // Only show animation once per terminal session
1744
+ const animationShown = hasAnimationBeenShown();
1745
+ const shouldShowAnimation = !animationShown &&
1746
+ isConfigured(options.privateKey) &&
1747
+ options.art !== false &&
1748
+ !options.json &&
1749
+ !options.quiet;
1750
+ if (shouldShowAnimation) {
1751
+ const prefs = config.get("preferences");
1752
+ if (prefs?.enableAsciiArt !== false) {
1753
+ try {
1754
+ await runCatSpinAnimation();
1755
+ // Mark animation as shown for this terminal session
1756
+ markAnimationAsShown();
1757
+ }
1758
+ catch (error) {
1759
+ // If animation is interrupted or fails, continue to shell
1760
+ // Don't show error to user, just proceed
1761
+ }
1762
+ }
1763
+ }
1346
1764
  const client = await HttpcatClient.create(privateKey);
1347
1765
  await startInteractiveShell(client);
1348
1766
  }