httpcat-cli 0.2.13 → 0.3.0-rc.2

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 (195) hide show
  1. package/.github/workflows/release.yml +96 -0
  2. package/.github/workflows/sync-version.yml +31 -2
  3. package/README.md +185 -62
  4. package/additions.txt +3 -0
  5. package/bun.lock +180 -1276
  6. package/dist/agent/autonomous-trader.d.ts.map +1 -0
  7. package/dist/agent/autonomous-trader.js +362 -0
  8. package/dist/agent/autonomous-trader.js.map +1 -0
  9. package/dist/agent/ax-agent.d.ts.map +1 -1
  10. package/dist/agent/ax-agent.js +293 -18
  11. package/dist/agent/ax-agent.js.map +1 -1
  12. package/dist/agent/event-client.d.ts.map +1 -0
  13. package/dist/agent/event-client.js +82 -0
  14. package/dist/agent/event-client.js.map +1 -0
  15. package/dist/agent/log-stream.d.ts.map +1 -0
  16. package/dist/agent/log-stream.js +95 -0
  17. package/dist/agent/log-stream.js.map +1 -0
  18. package/dist/agent/memory/conversation-session.d.ts.map +1 -0
  19. package/dist/agent/memory/conversation-session.js +232 -0
  20. package/dist/agent/memory/conversation-session.js.map +1 -0
  21. package/dist/agent/memory/conversation-store.d.ts.map +1 -0
  22. package/dist/agent/memory/conversation-store.js +214 -0
  23. package/dist/agent/memory/conversation-store.js.map +1 -0
  24. package/dist/agent/memory/database-schema.d.ts.map +1 -0
  25. package/dist/agent/memory/database-schema.js +355 -0
  26. package/dist/agent/memory/database-schema.js.map +1 -0
  27. package/dist/agent/memory/decision-tracker.d.ts.map +1 -0
  28. package/dist/agent/memory/decision-tracker.js +274 -0
  29. package/dist/agent/memory/decision-tracker.js.map +1 -0
  30. package/dist/agent/memory/memory-manager.d.ts.map +1 -0
  31. package/dist/agent/memory/memory-manager.js +187 -0
  32. package/dist/agent/memory/memory-manager.js.map +1 -0
  33. package/dist/agent/memory/types.d.ts.map +1 -0
  34. package/dist/agent/memory/types.js +5 -0
  35. package/dist/agent/memory/types.js.map +1 -0
  36. package/dist/agent/message-formatter.d.ts.map +1 -0
  37. package/dist/agent/message-formatter.js +76 -0
  38. package/dist/agent/message-formatter.js.map +1 -0
  39. package/dist/agent/position-db.d.ts.map +1 -0
  40. package/dist/agent/position-db.js +154 -0
  41. package/dist/agent/position-db.js.map +1 -0
  42. package/dist/agent/simple-chat-ui-static.d.ts.map +1 -0
  43. package/dist/agent/simple-chat-ui-static.js +129 -0
  44. package/dist/agent/simple-chat-ui-static.js.map +1 -0
  45. package/dist/agent/simple-chat-ui.d.ts.map +1 -0
  46. package/dist/agent/simple-chat-ui.js +90 -0
  47. package/dist/agent/simple-chat-ui.js.map +1 -0
  48. package/dist/agent/tools.d.ts.map +1 -1
  49. package/dist/agent/tools.js +274 -5
  50. package/dist/agent/tools.js.map +1 -1
  51. package/dist/agent/ui.d.ts.map +1 -0
  52. package/dist/agent/ui.js +84 -0
  53. package/dist/agent/ui.js.map +1 -0
  54. package/dist/agent/unified-runtime.d.ts.map +1 -0
  55. package/dist/agent/unified-runtime.js +397 -0
  56. package/dist/agent/unified-runtime.js.map +1 -0
  57. package/dist/client.d.ts.map +1 -1
  58. package/dist/client.js +406 -46
  59. package/dist/client.js.map +1 -1
  60. package/dist/commands/account.d.ts.map +1 -1
  61. package/dist/commands/account.js +51 -22
  62. package/dist/commands/account.js.map +1 -1
  63. package/dist/commands/agent.d.ts.map +1 -0
  64. package/dist/commands/agent.js +258 -0
  65. package/dist/commands/agent.js.map +1 -0
  66. package/dist/commands/balances.d.ts.map +1 -1
  67. package/dist/commands/balances.js +122 -68
  68. package/dist/commands/balances.js.map +1 -1
  69. package/dist/commands/buy.d.ts.map +1 -1
  70. package/dist/commands/buy.js +73 -36
  71. package/dist/commands/buy.js.map +1 -1
  72. package/dist/commands/chat.d.ts.map +1 -1
  73. package/dist/commands/chat.js +200 -429
  74. package/dist/commands/chat.js.map +1 -1
  75. package/dist/commands/claim.d.ts.map +1 -1
  76. package/dist/commands/claim.js +84 -27
  77. package/dist/commands/claim.js.map +1 -1
  78. package/dist/commands/create.d.ts.map +1 -1
  79. package/dist/commands/create.js +65 -42
  80. package/dist/commands/create.js.map +1 -1
  81. package/dist/commands/health.d.ts.map +1 -1
  82. package/dist/commands/health.js +7 -5
  83. package/dist/commands/health.js.map +1 -1
  84. package/dist/commands/info.d.ts.map +1 -1
  85. package/dist/commands/info.js +125 -46
  86. package/dist/commands/info.js.map +1 -1
  87. package/dist/commands/list.d.ts.map +1 -1
  88. package/dist/commands/list.js +56 -22
  89. package/dist/commands/list.js.map +1 -1
  90. package/dist/commands/positions.d.ts.map +1 -1
  91. package/dist/commands/positions.js +76 -47
  92. package/dist/commands/positions.js.map +1 -1
  93. package/dist/commands/sell.d.ts.map +1 -1
  94. package/dist/commands/sell.js +62 -32
  95. package/dist/commands/sell.js.map +1 -1
  96. package/dist/commands/swap.d.ts.map +1 -0
  97. package/dist/commands/swap.js +392 -0
  98. package/dist/commands/swap.js.map +1 -0
  99. package/dist/commands/transactions.d.ts.map +1 -1
  100. package/dist/commands/transactions.js +28 -16
  101. package/dist/commands/transactions.js.map +1 -1
  102. package/dist/config.d.ts.map +1 -1
  103. package/dist/config.js +132 -11
  104. package/dist/config.js.map +1 -1
  105. package/dist/index.js +453 -145
  106. package/dist/index.js.map +1 -1
  107. package/dist/interactive/art.d.ts.map +1 -1
  108. package/dist/interactive/art.js +38 -1
  109. package/dist/interactive/art.js.map +1 -1
  110. package/dist/interactive/shell.d.ts.map +1 -1
  111. package/dist/interactive/shell.js +398 -2259
  112. package/dist/interactive/shell.js.map +1 -1
  113. package/dist/mcp/chat-state.d.ts.map +1 -1
  114. package/dist/mcp/chat-state.js +2 -1
  115. package/dist/mcp/chat-state.js.map +1 -1
  116. package/dist/mcp/server.js +1 -1
  117. package/dist/mcp/tools.d.ts.map +1 -1
  118. package/dist/mcp/tools.js +108 -1
  119. package/dist/mcp/tools.js.map +1 -1
  120. package/dist/mcp/types.d.ts.map +1 -1
  121. package/dist/types/agent-info.d.ts.map +1 -0
  122. package/dist/types/agent-info.js +11 -0
  123. package/dist/types/agent-info.js.map +1 -0
  124. package/dist/ui/components/ScrollableList.d.ts.map +1 -0
  125. package/dist/ui/components/ScrollableList.js +72 -0
  126. package/dist/ui/components/ScrollableList.js.map +1 -0
  127. package/dist/ui/components/ThemeProvider.d.ts.map +1 -0
  128. package/dist/ui/components/ThemeProvider.js +87 -0
  129. package/dist/ui/components/ThemeProvider.js.map +1 -0
  130. package/dist/ui/components/ThemedBox.d.ts.map +1 -0
  131. package/dist/ui/components/ThemedBox.js +24 -0
  132. package/dist/ui/components/ThemedBox.js.map +1 -0
  133. package/dist/ui/components/agent/ChatHeader.d.ts.map +1 -0
  134. package/dist/ui/components/agent/ChatHeader.js +39 -0
  135. package/dist/ui/components/agent/ChatHeader.js.map +1 -0
  136. package/dist/ui/components/agent/Header.d.ts.map +1 -0
  137. package/dist/ui/components/agent/Header.js +14 -0
  138. package/dist/ui/components/agent/Header.js.map +1 -0
  139. package/dist/ui/components/agent/Input.d.ts.map +1 -0
  140. package/dist/ui/components/agent/Input.js +23 -0
  141. package/dist/ui/components/agent/Input.js.map +1 -0
  142. package/dist/ui/components/agent/Output.d.ts.map +1 -0
  143. package/dist/ui/components/agent/Output.js +23 -0
  144. package/dist/ui/components/agent/Output.js.map +1 -0
  145. package/dist/ui/components/chat/TokenChatUI.d.ts.map +1 -0
  146. package/dist/ui/components/chat/TokenChatUI.js +133 -0
  147. package/dist/ui/components/chat/TokenChatUI.js.map +1 -0
  148. package/dist/ui/components/shell/ShellHeader.d.ts.map +1 -0
  149. package/dist/ui/components/shell/ShellHeader.js +31 -0
  150. package/dist/ui/components/shell/ShellHeader.js.map +1 -0
  151. package/dist/ui/components/shell/ShellInput.d.ts.map +1 -0
  152. package/dist/ui/components/shell/ShellInput.js +147 -0
  153. package/dist/ui/components/shell/ShellInput.js.map +1 -0
  154. package/dist/ui/components/shell/ShellOutput.d.ts.map +1 -0
  155. package/dist/ui/components/shell/ShellOutput.js +8 -0
  156. package/dist/ui/components/shell/ShellOutput.js.map +1 -0
  157. package/dist/ui/hooks/useChatWebSocket.d.ts.map +1 -0
  158. package/dist/ui/hooks/useChatWebSocket.js +76 -0
  159. package/dist/ui/hooks/useChatWebSocket.js.map +1 -0
  160. package/dist/ui/hooks/useCommandHistory.d.ts.map +1 -0
  161. package/dist/ui/hooks/useCommandHistory.js +70 -0
  162. package/dist/ui/hooks/useCommandHistory.js.map +1 -0
  163. package/dist/ui/hooks/useDebounce.d.ts.map +1 -0
  164. package/dist/ui/hooks/useDebounce.js +17 -0
  165. package/dist/ui/hooks/useDebounce.js.map +1 -0
  166. package/dist/ui/hooks/useLogStream.d.ts.map +1 -0
  167. package/dist/ui/hooks/useLogStream.js +20 -0
  168. package/dist/ui/hooks/useLogStream.js.map +1 -0
  169. package/dist/ui/hooks/useVirtualScroll.d.ts.map +1 -0
  170. package/dist/ui/hooks/useVirtualScroll.js +70 -0
  171. package/dist/ui/hooks/useVirtualScroll.js.map +1 -0
  172. package/dist/utils/constants.d.ts.map +1 -1
  173. package/dist/utils/constants.js +44 -2
  174. package/dist/utils/constants.js.map +1 -1
  175. package/dist/utils/errors.d.ts.map +1 -1
  176. package/dist/utils/errors.js +3 -3
  177. package/dist/utils/errors.js.map +1 -1
  178. package/dist/utils/formatting.d.ts.map +1 -1
  179. package/dist/utils/formatting.js +37 -6
  180. package/dist/utils/formatting.js.map +1 -1
  181. package/dist/utils/loading.d.ts.map +1 -1
  182. package/dist/utils/loading.js +23 -2
  183. package/dist/utils/loading.js.map +1 -1
  184. package/dist/utils/privateKeyPrompt.d.ts.map +1 -1
  185. package/dist/utils/privateKeyPrompt.js +31 -7
  186. package/dist/utils/privateKeyPrompt.js.map +1 -1
  187. package/dist/utils/status.d.ts.map +1 -0
  188. package/dist/utils/status.js +96 -0
  189. package/dist/utils/status.js.map +1 -0
  190. package/dist/utils/token-resolver.d.ts.map +1 -1
  191. package/dist/utils/token-resolver.js +17 -7
  192. package/dist/utils/token-resolver.js.map +1 -1
  193. package/issues.txt +2 -0
  194. package/package.json +16 -6
  195. package/Screenshot 2025-12-21 at 8.56.02/342/200/257PM.png +0 -0
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,9 +45,10 @@ 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";
51
+ import { swap, displaySwapResult } from "./commands/swap.js";
30
52
  import { getTokenInfo, displayTokenInfo } from "./commands/info.js";
31
53
  import { listTokens, displayTokenList } from "./commands/list.js";
32
54
  import { getPositions, displayPositions } from "./commands/positions.js";
@@ -35,6 +57,7 @@ import { checkHealth, displayHealthStatus } from "./commands/health.js";
35
57
  import { checkBalance, displayBalance } from "./commands/balances.js";
36
58
  import { startChatStream } from "./commands/chat.js";
37
59
  import { runMcpServer } from "./commands/mcp-server.js";
60
+ import { setupAgentCommands } from "./commands/agent.js";
38
61
  import { getAccountInfo, displayAccountInfo, listAccounts, switchAccount, addAccount, } from "./commands/account.js";
39
62
  import { viewFees, claimFees, displayFees, displayClaimResult, } from "./commands/claim.js";
40
63
  // Check for --version --json before parsing
@@ -84,6 +107,11 @@ function isConfigured(cliPrivateKey) {
84
107
  return true;
85
108
  return config.isConfigured();
86
109
  }
110
+ // Helper function to get global options properly
111
+ // IMPORTANT: Always use this instead of command.parent?.opts() to ensure global flags work
112
+ function getGlobalOpts() {
113
+ return program.opts();
114
+ }
87
115
  // Helper to ensure wallet is unlocked
88
116
  async function ensureWalletUnlocked() {
89
117
  const password = config.getPassword();
@@ -194,7 +222,7 @@ envCommand
194
222
  .description("Add a custom environment")
195
223
  .argument("<name>", "Environment name")
196
224
  .argument("<agentUrl>", "Agent URL (e.g., http://localhost:8787)")
197
- .option("-n, --network <network>", "Network (default: base-sepolia)", "base-sepolia")
225
+ .option("-n, --network <network>", "Network: base-sepolia (eip155:84532) or base (eip155:8453) (default: base-sepolia)", "base-sepolia")
198
226
  .action((name, agentUrl, options) => {
199
227
  try {
200
228
  config.addEnvironment(name, agentUrl, options.network);
@@ -215,7 +243,7 @@ envCommand
215
243
  .description("Update an existing environment")
216
244
  .argument("<name>", "Environment name")
217
245
  .argument("<agentUrl>", "Agent URL (e.g., http://localhost:8787)")
218
- .option("-n, --network <network>", "Network (default: base-sepolia)", "base-sepolia")
246
+ .option("-n, --network <network>", "Network: base-sepolia (eip155:84532) or base (eip155:8453) (default: base-sepolia)", "base-sepolia")
219
247
  .action((name, agentUrl, options) => {
220
248
  try {
221
249
  config.updateEnvironment(name, agentUrl, options.network);
@@ -287,7 +315,20 @@ Examples:
287
315
  `)
288
316
  .action(async (name, symbol, options, command) => {
289
317
  try {
290
- const globalOpts = command.parent?.opts() || {};
318
+ // Fallback: get arguments from command object if not provided as parameters
319
+ // This handles cases where Commander.js doesn't parse arguments correctly
320
+ // In Commander.js v9+, use processedArgs; fallback to args for older versions
321
+ const args = command.processedArgs || command.args || [];
322
+ const actualName = name || args[0];
323
+ const actualSymbol = symbol || args[1];
324
+ // Validate required arguments
325
+ if (!actualName || typeof actualName !== "string") {
326
+ throw new Error("Token name is required. Usage: httpcat create <name> <symbol> [options]");
327
+ }
328
+ if (!actualSymbol || typeof actualSymbol !== "string") {
329
+ throw new Error("Token symbol is required. Usage: httpcat create <name> <symbol> [options]");
330
+ }
331
+ const globalOpts = getGlobalOpts();
291
332
  const accountIndex = globalOpts.account;
292
333
  // Ensure wallet is unlocked
293
334
  await ensureWalletUnlocked();
@@ -302,20 +343,27 @@ Examples:
302
343
  privateKey = await promptForPrivateKey();
303
344
  }
304
345
  const client = await HttpcatClient.create(privateKey);
346
+ // Generate default robohash URL if no photo provided
347
+ let photoToUse = options.photo;
348
+ if (!photoToUse) {
349
+ const uuid = randomUUID();
350
+ photoToUse = `https://robohash.org/${uuid}?set=set4`;
351
+ }
305
352
  // 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), {
353
+ let processedPhotoUrl = photoToUse;
354
+ if (photoToUse && isFilePath(photoToUse)) {
355
+ processedPhotoUrl = await withLoading(async () => processPhotoUrl(photoToUse), {
309
356
  message: "Uploading image...",
310
357
  json: globalOpts.json,
311
358
  quiet: globalOpts.quiet,
312
359
  spinner: "cat",
360
+ clearOnSuccess: true,
313
361
  });
314
362
  }
315
363
  // Create token (show loading state)
316
364
  const result = await withLoading(() => createToken(client, {
317
- name,
318
- symbol,
365
+ name: actualName.trim(),
366
+ symbol: actualSymbol.trim(),
319
367
  photoUrl: processedPhotoUrl,
320
368
  websiteUrl: options.website,
321
369
  }), {
@@ -323,6 +371,7 @@ Examples:
323
371
  json: globalOpts.json,
324
372
  quiet: globalOpts.quiet,
325
373
  spinner: "cat",
374
+ clearOnSuccess: true,
326
375
  });
327
376
  if (globalOpts.json) {
328
377
  outputJson("create_token", result);
@@ -333,7 +382,26 @@ Examples:
333
382
  process.exit(0);
334
383
  }
335
384
  catch (error) {
336
- const globalOpts = command.parent?.opts() || {};
385
+ const globalOpts = getGlobalOpts();
386
+ // Check if platform hasn't graduated - if so, provide a clearer error message
387
+ try {
388
+ const agentInfo = await config.getAgentInfo();
389
+ if (agentInfo.catGraduated !== true) {
390
+ const graduationError = new Error("Token creation is currently disabled. The platform token must graduate before new tokens can be created. Please try again later.");
391
+ if (globalOpts.json) {
392
+ outputError("create_token", graduationError, getExitCode(graduationError));
393
+ }
394
+ else {
395
+ handleError(graduationError, globalOpts.verbose);
396
+ }
397
+ process.exit(getExitCode(graduationError));
398
+ return;
399
+ }
400
+ }
401
+ catch (infoError) {
402
+ // If we can't fetch agent info, fall through to normal error handling
403
+ }
404
+ // Handle other errors normally
337
405
  if (globalOpts.json) {
338
406
  outputError("create_token", error, getExitCode(error));
339
407
  }
@@ -365,7 +433,7 @@ Examples:
365
433
  `)
366
434
  .action(async (tokenId, amount, options, command) => {
367
435
  try {
368
- const globalOpts = command.parent?.opts() || {};
436
+ const globalOpts = getGlobalOpts();
369
437
  const accountIndex = globalOpts.account;
370
438
  const repeatCount = options.repeat;
371
439
  const delayMs = options.delay || 0;
@@ -382,15 +450,19 @@ Examples:
382
450
  privateKey = await promptForPrivateKey();
383
451
  }
384
452
  const client = await HttpcatClient.create(privateKey);
385
- const isTestMode = client.getNetwork().includes("sepolia");
453
+ const network = client.getNetwork();
454
+ const isTestMode = network === "eip155:84532" || network === "eip155:11155111" || network.includes("sepolia");
386
455
  const silent = globalOpts.json || globalOpts.quiet;
387
456
  // Handle percentage amounts for graduated tokens
388
457
  let finalAmount = amount;
389
458
  if (amount.endsWith("%")) {
390
- if (!silent) {
391
- console.log("💰 Checking USDC balance for percentage buy...");
459
+ if (!silent && !globalOpts.json && !globalOpts.quiet) {
460
+ writeStatusLine(chalk.dim("💰 Checking USDC balance for percentage buy..."));
392
461
  }
393
462
  const balance = await checkBalance(privateKey);
463
+ if (!silent && !globalOpts.json && !globalOpts.quiet) {
464
+ clearStatusLine();
465
+ }
394
466
  const usdcBalance = parseFloat(balance.usdcFormatted.replace("$", "").replace(",", ""));
395
467
  const percentage = parseFloat(amount.replace("%", ""));
396
468
  if (isNaN(percentage) || percentage <= 0 || percentage > 100) {
@@ -413,18 +485,83 @@ Examples:
413
485
  let stopReason = "";
414
486
  for (let i = 1; i <= repeatCount; i++) {
415
487
  try {
416
- // Show progress for non-JSON mode
417
- if (!globalOpts.json && !globalOpts.quiet) {
418
- console.log(chalk.dim(`Buy ${i}/${repeatCount}...`));
488
+ // Retry logic: try up to 10 times with exponential backoff on 402 errors
489
+ // Client is recreated on each attempt to ensure fresh signature for new nonce
490
+ let result = null;
491
+ let lastError = null;
492
+ const maxRetries = 10;
493
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
494
+ try {
495
+ // Show progress for non-JSON mode
496
+ if (!globalOpts.json && !globalOpts.quiet) {
497
+ if (attempt === 1) {
498
+ console.log(chalk.dim(`Buy ${i}/${repeatCount}...`));
499
+ }
500
+ else {
501
+ console.log(chalk.yellow(` Retrying buy ${i}/${repeatCount} (attempt ${attempt}/${maxRetries})...`));
502
+ }
503
+ }
504
+ // Recreate client inside the operation function to ensure fresh signature
505
+ // for each attempt (including retries). This fixes the issue where x402-fetch
506
+ // generates a new nonce but reuses the same signature, causing subsequent buys to fail
507
+ result = await withLoading(async () => {
508
+ // Recreate client for each attempt to ensure fresh signature for new nonce
509
+ const client = await HttpcatClient.create(privateKey);
510
+ return buyToken(client, tokenId, finalAmount, isTestMode, silent || i > 1, privateKey);
511
+ }, {
512
+ message: `Buying tokens (${i}/${repeatCount})...`,
513
+ json: globalOpts.json,
514
+ quiet: globalOpts.quiet || i > 1,
515
+ spinner: "cat",
516
+ clearOnSuccess: true,
517
+ });
518
+ // Success! Break out of retry loop
519
+ break;
520
+ }
521
+ catch (error) {
522
+ lastError = error;
523
+ // Only retry on 402 errors (payment required)
524
+ const is402Error = error instanceof HttpcatError && error.status === 402;
525
+ if (is402Error && attempt < maxRetries) {
526
+ // Exponential backoff: 1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s (capped at 10s)
527
+ const backoffMs = Math.min(Math.pow(2, attempt - 1) * 1000, 10000);
528
+ const backoffSeconds = backoffMs / 1000;
529
+ if (!globalOpts.json && !globalOpts.quiet) {
530
+ // Write status on same line (overwrites previous)
531
+ const statusMessage = formatRetryMessage(attempt, maxRetries, backoffSeconds);
532
+ writeStatusLine(statusMessage);
533
+ }
534
+ await new Promise((resolve) => setTimeout(resolve, backoffMs));
535
+ continue; // Retry
536
+ }
537
+ else {
538
+ // Not a 402 error, or max retries reached - clear status and throw
539
+ if (!globalOpts.json && !globalOpts.quiet) {
540
+ clearStatusLine();
541
+ }
542
+ throw error;
543
+ }
544
+ }
545
+ }
546
+ // If we exhausted retries, throw the last error
547
+ if (!result) {
548
+ // Clear status line before throwing
549
+ if (!globalOpts.json && !globalOpts.quiet) {
550
+ clearStatusLine();
551
+ }
552
+ if (lastError) {
553
+ throw lastError;
554
+ }
555
+ else {
556
+ throw new Error(`Failed to complete buy ${i}/${repeatCount} after ${maxRetries} attempts`);
557
+ }
419
558
  }
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
559
  results.push(result);
427
560
  totalSpent += parseFloat(result.amountSpent);
561
+ // Clear status line before showing result
562
+ if (!globalOpts.json && !globalOpts.quiet) {
563
+ clearStatusLine();
564
+ }
428
565
  // Display compact result for each buy
429
566
  if (globalOpts.json) {
430
567
  // In JSON mode, output each result
@@ -443,58 +580,79 @@ Examples:
443
580
  }
444
581
  break;
445
582
  }
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
583
+ // Wait for transaction confirmations if present
584
+ // This ensures both the buy transaction and payment transaction are confirmed
585
+ // before the next buy, preventing nonce/signature conflicts
586
+ if (i < repeatCount) {
587
+ const { createPublicClient, http } = await import("viem");
588
+ const { baseSepolia } = await import("viem/chains");
589
+ const publicClient = createPublicClient({
590
+ chain: baseSepolia,
591
+ transport: http(config.getRpcUrl()),
592
+ });
593
+ // Wait for payment transaction first (if present)
594
+ // This is critical to ensure the payment nonce is consumed before next request
595
+ if (result.paymentTxHash) {
596
+ if (!globalOpts.json && !globalOpts.quiet) {
597
+ console.log(chalk.dim(` Waiting for payment transaction confirmation...`));
598
+ }
461
599
  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}`));
600
+ await publicClient.waitForTransactionReceipt({
601
+ hash: result.paymentTxHash,
602
+ });
603
+ if (!globalOpts.json && !globalOpts.quiet) {
604
+ console.log(chalk.dim(` ✅ Payment transaction confirmed`));
468
605
  }
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)}`));
477
- }
478
- if (usdcBalance < 0.01) {
479
- console.log(chalk.dim(` • Low USDC for API payments: x402 protocol requires USDC for API calls`));
606
+ }
607
+ catch (txError) {
608
+ // If we can't wait for confirmation, log but continue
609
+ if (!globalOpts.json && !globalOpts.quiet) {
610
+ console.log(chalk.yellow(` ⚠️ Could not confirm payment transaction, proceeding...`));
480
611
  }
481
- if (ethBalance < 0.001) {
482
- console.log(chalk.dim(` • Low ETH for gas: Need ETH to pay for transaction fees`));
612
+ }
613
+ }
614
+ // Wait for buy transaction (if present)
615
+ if (result.txHash) {
616
+ if (!globalOpts.json && !globalOpts.quiet) {
617
+ console.log(chalk.dim(` Waiting for buy transaction confirmation...`));
618
+ }
619
+ try {
620
+ await publicClient.waitForTransactionReceipt({
621
+ hash: result.txHash,
622
+ });
623
+ if (!globalOpts.json && !globalOpts.quiet) {
624
+ console.log(chalk.dim(` ✅ Buy transaction confirmed`));
483
625
  }
484
- console.log();
485
- console.log(chalk.dim(" Check your balance with: httpcat balances"));
486
626
  }
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"));
627
+ catch (txError) {
628
+ // If we can't wait for confirmation, log but continue
629
+ if (!globalOpts.json && !globalOpts.quiet) {
630
+ console.log(chalk.yellow(` ⚠️ Could not confirm buy transaction, proceeding...`));
631
+ }
490
632
  }
491
633
  }
492
- break;
493
634
  }
494
- // For other errors, re-throw to be handled by outer catch
635
+ // Apply delay between iterations (except after the last one)
636
+ if (i < repeatCount && delayMs > 0) {
637
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
638
+ }
639
+ }
640
+ catch (error) {
641
+ // Clear status line before throwing
642
+ if (!globalOpts.json && !globalOpts.quiet) {
643
+ clearStatusLine();
644
+ }
645
+ // Don't catch 402 errors - x402-fetch handles payment automatically
646
+ // 402 is the normal payment flow on first call, not an error
647
+ // Let withLoading and x402-fetch handle payment retries
648
+ // Only re-throw to let outer handler deal with persistent errors
495
649
  throw error;
496
650
  }
497
651
  }
652
+ // Clear status line before showing summary
653
+ if (!globalOpts.json && !globalOpts.quiet) {
654
+ clearStatusLine();
655
+ }
498
656
  // Show final summary
499
657
  if (!globalOpts.json && !globalOpts.quiet && results.length > 0) {
500
658
  const lastResult = results[results.length - 1];
@@ -517,13 +675,60 @@ Examples:
517
675
  process.exit(0);
518
676
  }
519
677
  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
- });
678
+ // Normal single buy execution with retry logic
679
+ let result = null;
680
+ let lastError = null;
681
+ let attempt = 0;
682
+ const maxRetries = null; // Retry indefinitely for single buys
683
+ while (!result) {
684
+ attempt++;
685
+ try {
686
+ // Show initial attempt message only on first try
687
+ if (attempt === 1 && !globalOpts.json && !globalOpts.quiet) {
688
+ writeStatusLine(chalk.dim("Buying tokens..."));
689
+ }
690
+ // Recreate client for each attempt to ensure fresh signature
691
+ const freshClient = await HttpcatClient.create(privateKey);
692
+ result = await buyToken(freshClient, tokenId, finalAmount, isTestMode, silent, privateKey);
693
+ // Success! Clear any status line and break
694
+ if (!globalOpts.json && !globalOpts.quiet) {
695
+ clearStatusLine();
696
+ }
697
+ break;
698
+ }
699
+ catch (error) {
700
+ lastError = error;
701
+ // Only retry on 402 errors (payment required)
702
+ const is402Error = error instanceof HttpcatError && error.status === 402;
703
+ if (is402Error) {
704
+ // Exponential backoff: 1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s (capped at 10s)
705
+ const backoffMs = Math.min(Math.pow(2, attempt - 1) * 1000, 10000);
706
+ const backoffSeconds = backoffMs / 1000;
707
+ if (!globalOpts.json && !globalOpts.quiet) {
708
+ // Write status on same line (overwrites previous)
709
+ const statusMessage = formatRetryMessage(attempt, maxRetries, backoffSeconds);
710
+ writeStatusLine(statusMessage);
711
+ }
712
+ await new Promise((resolve) => setTimeout(resolve, backoffMs));
713
+ continue; // Retry
714
+ }
715
+ else {
716
+ // Not a 402 error - throw immediately
717
+ if (!globalOpts.json && !globalOpts.quiet) {
718
+ clearStatusLine();
719
+ }
720
+ throw error;
721
+ }
722
+ }
723
+ }
724
+ if (!result) {
725
+ if (lastError) {
726
+ throw lastError;
727
+ }
728
+ else {
729
+ throw new Error("Failed to complete buy");
730
+ }
731
+ }
527
732
  if (globalOpts.json) {
528
733
  outputJson("token_buy", result);
529
734
  }
@@ -534,7 +739,11 @@ Examples:
534
739
  }
535
740
  }
536
741
  catch (error) {
537
- const globalOpts = command.parent?.opts() || {};
742
+ const globalOpts = getGlobalOpts();
743
+ // Clear status line before showing error
744
+ if (!globalOpts.json && !globalOpts.quiet) {
745
+ clearStatusLine();
746
+ }
538
747
  if (globalOpts.json) {
539
748
  outputError("token_buy", error, getExitCode(error));
540
749
  }
@@ -559,15 +768,13 @@ Examples:
559
768
  `)
560
769
  .action(async (tokenId, amountInput, command) => {
561
770
  try {
562
- const globalOpts = command.parent?.opts() || {};
771
+ const globalOpts = getGlobalOpts();
563
772
  const accountIndex = globalOpts.account;
564
773
  // Ensure wallet is unlocked
565
774
  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);
775
+ let privateKey = getPrivateKey(globalOpts.privateKey, accountIndex);
569
776
  // If not configured and not in JSON mode, prompt interactively
570
- if (!isConfigured(programOpts.privateKey || globalOpts.privateKey)) {
777
+ if (!isConfigured(globalOpts.privateKey)) {
571
778
  if (globalOpts.json) {
572
779
  console.error('❌ Not configured. Run "httpcat config" first or use --private-key flag.');
573
780
  process.exit(2);
@@ -594,6 +801,7 @@ Examples:
594
801
  json: globalOpts.json,
595
802
  quiet: globalOpts.quiet,
596
803
  spinner: "cat",
804
+ clearOnSuccess: true,
597
805
  });
598
806
  // Found in database! Continue with normal flow below
599
807
  actualBalance = info.userPosition?.tokensOwned || "0";
@@ -602,14 +810,14 @@ Examples:
602
810
  // Not in database - treat as external token
603
811
  if (error.message?.includes("not found") ||
604
812
  error.message?.includes("Invalid UUID")) {
605
- if (!silent) {
606
- console.log("📊 Checking on-chain balance for external token...");
813
+ if (!silent && !globalOpts.json && !globalOpts.quiet) {
814
+ writeStatusLine(chalk.dim("📊 Checking on-chain balance for external token..."));
607
815
  }
608
816
  const { createPublicClient, http, parseAbi } = await import("viem");
609
817
  const { baseSepolia } = await import("viem/chains");
610
818
  const publicClient = createPublicClient({
611
819
  chain: baseSepolia,
612
- transport: http(),
820
+ transport: http(config.getRpcUrl()),
613
821
  });
614
822
  const ERC20_ABI = parseAbi([
615
823
  "function balanceOf(address owner) view returns (uint256)",
@@ -621,6 +829,9 @@ Examples:
621
829
  args: [userAddress],
622
830
  });
623
831
  actualBalance = balance.toString();
832
+ if (!silent && !globalOpts.json && !globalOpts.quiet) {
833
+ clearStatusLine();
834
+ }
624
835
  if (!silent) {
625
836
  const { formatUnits } = await import("viem");
626
837
  console.log(`💼 On-chain balance: ${formatUnits(balance, 18)} tokens`);
@@ -643,14 +854,14 @@ Examples:
643
854
  actualBalance = info.userPosition?.tokensOwned || "0";
644
855
  // For graduated tokens, check actual on-chain balance
645
856
  if (info && info.status === "graduated") {
646
- if (!silent) {
647
- console.log("📊 Checking on-chain balance for graduated token...");
857
+ if (!silent && !globalOpts.json && !globalOpts.quiet) {
858
+ writeStatusLine(chalk.dim("📊 Checking on-chain balance for graduated token..."));
648
859
  }
649
860
  const { createPublicClient, http, parseAbi } = await import("viem");
650
861
  const { baseSepolia } = await import("viem/chains");
651
862
  const publicClient = createPublicClient({
652
863
  chain: baseSepolia,
653
- transport: http(),
864
+ transport: http(config.getRpcUrl()),
654
865
  });
655
866
  const ERC20_ABI = parseAbi([
656
867
  "function balanceOf(address owner) view returns (uint256)",
@@ -662,6 +873,9 @@ Examples:
662
873
  args: [userAddress],
663
874
  });
664
875
  actualBalance = balance.toString();
876
+ if (!silent && !globalOpts.json && !globalOpts.quiet) {
877
+ clearStatusLine();
878
+ }
665
879
  if (!silent) {
666
880
  const { formatUnits } = await import("viem");
667
881
  console.log(`💼 On-chain balance: ${formatUnits(balance, 18)} tokens`);
@@ -677,6 +891,7 @@ Examples:
677
891
  json: globalOpts.json,
678
892
  quiet: globalOpts.quiet,
679
893
  spinner: "cat",
894
+ clearOnSuccess: true,
680
895
  });
681
896
  if (globalOpts.json) {
682
897
  outputJson("token_sell", result);
@@ -687,7 +902,7 @@ Examples:
687
902
  process.exit(0);
688
903
  }
689
904
  catch (error) {
690
- const globalOpts = command.parent?.opts() || {};
905
+ const globalOpts = getGlobalOpts();
691
906
  if (globalOpts.json) {
692
907
  outputError("token_sell", error, getExitCode(error));
693
908
  }
@@ -697,6 +912,72 @@ Examples:
697
912
  process.exit(getExitCode(error));
698
913
  }
699
914
  });
915
+ // Swap command (Pro - any token to any token via DEX aggregation)
916
+ program
917
+ .command("swap")
918
+ .description("Swap any token to any token ($0.10 - pro DEX aggregation)")
919
+ .argument("<tokenIn>", "Token to sell (address)")
920
+ .argument("<tokenOut>", "Token to buy (address)")
921
+ .argument("<amount>", "Amount to sell (in smallest unit, e.g., wei)")
922
+ .option("-s, --slippage <bps>", "Slippage tolerance in basis points (default: 50 = 0.5%)", "50")
923
+ .addHelpText("after", `
924
+ Examples:
925
+ # Swap 1 USDC for WETH
926
+ httpcat swap 0x036cbd53842c5426634e7929541ec2318f3dcf7e 0x4200000000000000000000000000000000000006 1000000
927
+
928
+ # Swap with 1% slippage
929
+ httpcat swap 0x036c... 0x4200... 1000000 --slippage 100
930
+ `)
931
+ .action(async (tokenIn, tokenOut, amount, options, command) => {
932
+ try {
933
+ const globalOpts = getGlobalOpts();
934
+ const accountIndex = globalOpts.account;
935
+ // Ensure wallet is unlocked
936
+ await ensureWalletUnlocked();
937
+ let privateKey = getPrivateKey(globalOpts.privateKey, accountIndex);
938
+ // If not configured and not in JSON mode, prompt interactively
939
+ if (!isConfigured(globalOpts.privateKey)) {
940
+ if (globalOpts.json) {
941
+ console.error('❌ Not configured. Run "httpcat config" first or use --private-key flag.');
942
+ process.exit(2);
943
+ }
944
+ privateKey = await promptForPrivateKey();
945
+ }
946
+ const client = await HttpcatClient.create(privateKey);
947
+ const silent = globalOpts.json || globalOpts.quiet;
948
+ const slippageBps = parseInt(options.slippage, 10) || 50;
949
+ if (!silent && !globalOpts.json && !globalOpts.quiet) {
950
+ writeStatusLine(chalk.dim("Swapping tokens..."));
951
+ }
952
+ const result = await withLoading(() => swap(client, tokenIn, tokenOut, amount, slippageBps, silent, privateKey), {
953
+ message: "Processing swap...",
954
+ json: globalOpts.json,
955
+ quiet: globalOpts.quiet,
956
+ spinner: "cat",
957
+ clearOnSuccess: true,
958
+ });
959
+ if (!silent && !globalOpts.json && !globalOpts.quiet) {
960
+ clearStatusLine();
961
+ }
962
+ if (globalOpts.json) {
963
+ console.log(JSON.stringify(result, null, 2));
964
+ }
965
+ else if (!globalOpts.quiet) {
966
+ displaySwapResult(result);
967
+ }
968
+ process.exit(0);
969
+ }
970
+ catch (error) {
971
+ const globalOpts = getGlobalOpts();
972
+ if (globalOpts.json) {
973
+ console.log(JSON.stringify({ error: error.message }, null, 2));
974
+ }
975
+ else if (!globalOpts.quiet) {
976
+ handleError(error, globalOpts.verbose);
977
+ }
978
+ process.exit(getExitCode(error));
979
+ }
980
+ });
700
981
  // Claim command
701
982
  program
702
983
  .command("claim")
@@ -713,7 +994,7 @@ Examples:
713
994
  `)
714
995
  .action(async (identifier, options, command) => {
715
996
  try {
716
- const globalOpts = command.parent?.opts() || {};
997
+ const globalOpts = getGlobalOpts();
717
998
  const accountIndex = globalOpts.account;
718
999
  // Ensure wallet is unlocked
719
1000
  await ensureWalletUnlocked();
@@ -739,6 +1020,7 @@ Examples:
739
1020
  json: globalOpts.json,
740
1021
  quiet: globalOpts.quiet,
741
1022
  spinner: "cat",
1023
+ clearOnSuccess: true,
742
1024
  });
743
1025
  if (globalOpts.json) {
744
1026
  outputJson("claim_fees", result);
@@ -754,6 +1036,7 @@ Examples:
754
1036
  json: globalOpts.json,
755
1037
  quiet: globalOpts.quiet,
756
1038
  spinner: "cat",
1039
+ clearOnSuccess: true,
757
1040
  });
758
1041
  if (globalOpts.json) {
759
1042
  outputJson("view_fees", result);
@@ -765,7 +1048,7 @@ Examples:
765
1048
  process.exit(0);
766
1049
  }
767
1050
  catch (error) {
768
- const globalOpts = command.parent?.opts() || {};
1051
+ const globalOpts = getGlobalOpts();
769
1052
  if (globalOpts.json) {
770
1053
  outputError("claim", error, getExitCode(error));
771
1054
  }
@@ -789,7 +1072,7 @@ Examples:
789
1072
  `)
790
1073
  .action(async (tokenId, command) => {
791
1074
  try {
792
- const globalOpts = command.parent?.opts() || {};
1075
+ const globalOpts = getGlobalOpts();
793
1076
  const accountIndex = globalOpts.account;
794
1077
  // Ensure wallet is unlocked
795
1078
  await ensureWalletUnlocked();
@@ -813,17 +1096,19 @@ Examples:
813
1096
  json: globalOpts.json,
814
1097
  quiet: globalOpts.quiet,
815
1098
  spinner: "cat",
1099
+ clearOnSuccess: true,
816
1100
  });
817
1101
  if (globalOpts.json) {
818
1102
  outputJson("token_info", result);
819
1103
  }
820
1104
  else if (!globalOpts.quiet) {
821
- await displayTokenInfo(result, userAddress);
1105
+ const client = await HttpcatClient.create(privateKey);
1106
+ await displayTokenInfo(result, userAddress, client.getNetwork());
822
1107
  }
823
1108
  process.exit(0);
824
1109
  }
825
1110
  catch (error) {
826
- const globalOpts = command.parent?.opts() || {};
1111
+ const globalOpts = getGlobalOpts();
827
1112
  if (globalOpts.json) {
828
1113
  outputError("token_info", error, getExitCode(error));
829
1114
  }
@@ -849,7 +1134,7 @@ Examples:
849
1134
  `)
850
1135
  .action(async (options, command) => {
851
1136
  try {
852
- const globalOpts = command.parent?.opts() || {};
1137
+ const globalOpts = getGlobalOpts();
853
1138
  const accountIndex = globalOpts.account;
854
1139
  // Ensure wallet is unlocked
855
1140
  await ensureWalletUnlocked();
@@ -869,6 +1154,7 @@ Examples:
869
1154
  json: globalOpts.json,
870
1155
  quiet: globalOpts.quiet,
871
1156
  spinner: "cat",
1157
+ clearOnSuccess: true,
872
1158
  });
873
1159
  if (globalOpts.json) {
874
1160
  outputJson("list_tokens", result);
@@ -879,7 +1165,7 @@ Examples:
879
1165
  process.exit(0);
880
1166
  }
881
1167
  catch (error) {
882
- const globalOpts = command.parent?.opts() || {};
1168
+ const globalOpts = getGlobalOpts();
883
1169
  if (globalOpts.json) {
884
1170
  outputError("list_tokens", error, getExitCode(error));
885
1171
  }
@@ -908,7 +1194,7 @@ Examples:
908
1194
  `)
909
1195
  .action(async (options, command) => {
910
1196
  try {
911
- const globalOpts = command.parent?.opts() || {};
1197
+ const globalOpts = getGlobalOpts();
912
1198
  const accountIndex = globalOpts.account;
913
1199
  // Ensure wallet is unlocked
914
1200
  await ensureWalletUnlocked();
@@ -971,6 +1257,7 @@ Examples:
971
1257
  json: globalOpts.json,
972
1258
  quiet: globalOpts.quiet,
973
1259
  spinner: "cat",
1260
+ clearOnSuccess: true,
974
1261
  });
975
1262
  if (globalOpts.json) {
976
1263
  outputJson("transactions", result);
@@ -981,7 +1268,7 @@ Examples:
981
1268
  process.exit(0);
982
1269
  }
983
1270
  catch (error) {
984
- const globalOpts = command.parent?.opts() || {};
1271
+ const globalOpts = getGlobalOpts();
985
1272
  if (globalOpts.json) {
986
1273
  outputError("transactions", error, getExitCode(error));
987
1274
  }
@@ -1007,7 +1294,7 @@ Examples:
1007
1294
  `)
1008
1295
  .action(async (command) => {
1009
1296
  try {
1010
- const globalOpts = command.parent?.opts() || {};
1297
+ const globalOpts = getGlobalOpts();
1011
1298
  const accountIndex = globalOpts.account;
1012
1299
  // Ensure wallet is unlocked
1013
1300
  await ensureWalletUnlocked();
@@ -1049,6 +1336,7 @@ Examples:
1049
1336
  json: globalOpts.json,
1050
1337
  quiet: globalOpts.quiet,
1051
1338
  spinner: "cat",
1339
+ clearOnSuccess: true,
1052
1340
  });
1053
1341
  const filter = command.active
1054
1342
  ? "active"
@@ -1075,7 +1363,7 @@ Examples:
1075
1363
  process.exit(0);
1076
1364
  }
1077
1365
  catch (error) {
1078
- const globalOpts = command.parent?.opts() || {};
1366
+ const globalOpts = getGlobalOpts();
1079
1367
  if (globalOpts.json) {
1080
1368
  outputError("positions", error, getExitCode(error));
1081
1369
  }
@@ -1097,7 +1385,7 @@ Examples:
1097
1385
  `)
1098
1386
  .action(async (command) => {
1099
1387
  try {
1100
- const globalOpts = command.parent?.opts() || {};
1388
+ const globalOpts = getGlobalOpts();
1101
1389
  const privateKey = getPrivateKey(globalOpts.privateKey);
1102
1390
  if (!isConfigured(globalOpts.privateKey)) {
1103
1391
  console.error('❌ Not configured. Run "httpcat config" first or use --private-key flag.');
@@ -1109,6 +1397,7 @@ Examples:
1109
1397
  json: globalOpts.json,
1110
1398
  quiet: globalOpts.quiet,
1111
1399
  spinner: "cat",
1400
+ clearOnSuccess: true,
1112
1401
  });
1113
1402
  if (globalOpts.json) {
1114
1403
  outputJson("health", result);
@@ -1119,7 +1408,7 @@ Examples:
1119
1408
  process.exit(result.status === "ok" ? 0 : 1);
1120
1409
  }
1121
1410
  catch (error) {
1122
- const globalOpts = command.parent?.opts() || {};
1411
+ const globalOpts = getGlobalOpts();
1123
1412
  if (globalOpts.json) {
1124
1413
  outputError("health", error, getExitCode(error));
1125
1414
  }
@@ -1141,14 +1430,15 @@ Examples:
1141
1430
  `)
1142
1431
  .action(async (command) => {
1143
1432
  try {
1144
- const globalOpts = command.parent?.opts() || {};
1145
- const accountIndex = globalOpts.account;
1433
+ // Use program.opts() directly since command.parent?.opts() doesn't include global options
1434
+ const programOpts = program.opts();
1435
+ const accountIndex = programOpts.account;
1146
1436
  // Ensure wallet is unlocked
1147
1437
  await ensureWalletUnlocked();
1148
- let privateKey = getPrivateKey(globalOpts.privateKey, accountIndex);
1438
+ let privateKey = getPrivateKey(programOpts.privateKey, accountIndex);
1149
1439
  // If not configured and not in JSON mode, prompt interactively
1150
- if (!isConfigured(globalOpts.privateKey)) {
1151
- if (globalOpts.json) {
1440
+ if (!isConfigured(programOpts.privateKey)) {
1441
+ if (programOpts.json) {
1152
1442
  console.error('❌ Not configured. Run "httpcat config" first or use --private-key flag.');
1153
1443
  process.exit(2);
1154
1444
  }
@@ -1157,25 +1447,26 @@ Examples:
1157
1447
  }
1158
1448
  const result = await withLoading(() => checkBalance(privateKey), {
1159
1449
  message: "Checking balances...",
1160
- json: globalOpts.json,
1161
- quiet: globalOpts.quiet,
1450
+ json: programOpts.json,
1451
+ quiet: programOpts.quiet,
1162
1452
  spinner: "cat",
1453
+ clearOnSuccess: true,
1163
1454
  });
1164
- if (globalOpts.json) {
1455
+ if (programOpts.json) {
1165
1456
  outputJson("balances", result);
1166
1457
  }
1167
- else if (!globalOpts.quiet) {
1458
+ else if (!programOpts.quiet) {
1168
1459
  displayBalance(result);
1169
1460
  }
1170
1461
  process.exit(0);
1171
1462
  }
1172
1463
  catch (error) {
1173
- const globalOpts = command.parent?.opts() || {};
1174
- if (globalOpts.json) {
1464
+ const programOpts = program.opts();
1465
+ if (programOpts.json) {
1175
1466
  outputError("balances", error, getExitCode(error));
1176
1467
  }
1177
1468
  else {
1178
- handleError(error, globalOpts.verbose);
1469
+ handleError(error, programOpts.verbose);
1179
1470
  }
1180
1471
  process.exit(getExitCode(error));
1181
1472
  }
@@ -1203,6 +1494,7 @@ Examples:
1203
1494
  json: globalOpts.json,
1204
1495
  quiet: globalOpts.quiet,
1205
1496
  spinner: "cat",
1497
+ clearOnSuccess: true,
1206
1498
  });
1207
1499
  if (globalOpts.json) {
1208
1500
  outputJson("account", result);
@@ -1304,7 +1596,7 @@ Examples:
1304
1596
  `)
1305
1597
  .action(async (tokenIdentifier, options, command) => {
1306
1598
  try {
1307
- const globalOpts = command.parent?.opts() || {};
1599
+ const globalOpts = getGlobalOpts();
1308
1600
  const chatOpts = options;
1309
1601
  const accountIndex = globalOpts.account;
1310
1602
  // Ensure wallet is unlocked
@@ -1324,15 +1616,18 @@ Examples:
1324
1616
  if (globalOpts.json) {
1325
1617
  const inputFormat = chatOpts.inputFormat || "text";
1326
1618
  await startChatStream(client, true, // jsonMode
1327
- tokenIdentifier || undefined, inputFormat);
1619
+ tokenIdentifier || undefined, inputFormat, false // returnToShell
1620
+ );
1328
1621
  }
1329
1622
  else {
1330
- // Interactive mode: launch shell and auto-enter chat
1331
- await startInteractiveShell(client, tokenIdentifier || undefined);
1623
+ // Interactive mode: use Ink-based chat UI
1624
+ await startChatStream(client, false, // jsonMode
1625
+ tokenIdentifier || undefined, "text", false // returnToShell (not from shell, so exit on quit)
1626
+ );
1332
1627
  }
1333
1628
  }
1334
1629
  catch (error) {
1335
- const globalOpts = command.parent?.opts() || {};
1630
+ const globalOpts = getGlobalOpts();
1336
1631
  if (globalOpts.json) {
1337
1632
  outputError("chat", error, getExitCode(error));
1338
1633
  }
@@ -1342,16 +1637,13 @@ Examples:
1342
1637
  process.exit(getExitCode(error));
1343
1638
  }
1344
1639
  });
1345
- // Agent command
1640
+ // Cat command - starts agent interactive mode
1346
1641
  program
1347
- .command("agent")
1348
- .alias("cat")
1349
- .description("Configure AI agent for trading assistance")
1350
- .option("-s, --setup", "Run setup wizard to configure API key")
1642
+ .command("cat")
1643
+ .description("Start interactive AI agent mode")
1351
1644
  .addHelpText("after", `
1352
1645
  Examples:
1353
- httpcat agent --setup # Configure AI agent
1354
- httpcat cat --setup # Same, using 'cat' alias
1646
+ httpcat cat # Start interactive agent mode
1355
1647
 
1356
1648
  The AI agent helps you:
1357
1649
  • Trade tokens using natural language
@@ -1359,37 +1651,53 @@ The AI agent helps you:
1359
1651
  • Get market information
1360
1652
  • Execute commands hands-free
1361
1653
 
1362
- You can use 'agent' or 'cat' interchangeably.
1654
+ Type /exit to quit agent mode.
1363
1655
  `)
1364
- .action(async (options) => {
1656
+ .action(async (options, command) => {
1365
1657
  try {
1366
- const { setupAIAgentWizard } = await import("./agent/setup-wizard.js");
1367
- if (options.setup) {
1368
- await setupAIAgentWizard();
1369
- process.exit(0);
1658
+ const globalOpts = getGlobalOpts();
1659
+ const accountIndex = globalOpts.account;
1660
+ // Ensure wallet is unlocked
1661
+ await ensureWalletUnlocked();
1662
+ let privateKey = getPrivateKey(globalOpts.privateKey, accountIndex);
1663
+ // If not configured and not in JSON mode, prompt interactively
1664
+ if (!isConfigured(globalOpts.privateKey)) {
1665
+ if (globalOpts.json) {
1666
+ console.error('❌ Not configured. Run "httpcat config" first or use --private-key flag.');
1667
+ process.exit(2);
1668
+ }
1669
+ // Interactive prompt for private key
1670
+ privateKey = await promptForPrivateKey();
1370
1671
  }
1371
- else {
1372
- // Show help for agent command
1373
- console.log();
1374
- console.log(chalk.cyan.bold("🐱 AI Agent"));
1672
+ const client = await HttpcatClient.create(privateKey);
1673
+ // Check if agent is configured
1674
+ const agentConfig = config.getAIAgentConfig();
1675
+ if (!agentConfig) {
1375
1676
  console.log();
1376
- console.log(chalk.yellow("The AI agent feature is currently available only in interactive mode."));
1677
+ console.log(chalk.yellow("⚠️ Agent not configured"));
1678
+ console.log(chalk.dim("Run 'httpcat agent --setup' to configure the AI agent."));
1377
1679
  console.log();
1378
- console.log(chalk.bold("To configure:"));
1379
- console.log(chalk.dim(" httpcat agent --setup"));
1680
+ process.exit(1);
1681
+ }
1682
+ const apiKey = config.getAIAgentApiKey();
1683
+ if (!apiKey) {
1380
1684
  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."));
1685
+ console.log(chalk.red("❌ Failed to get API key"));
1686
+ console.log(chalk.dim("Run 'httpcat agent --setup' to configure the API key."));
1384
1687
  console.log();
1385
- process.exit(0);
1688
+ process.exit(1);
1386
1689
  }
1690
+ // Start agent interactive mode
1691
+ const { startAgentInteractiveMode } = await import("./interactive/shell.js");
1692
+ await startAgentInteractiveMode(client);
1387
1693
  }
1388
1694
  catch (error) {
1389
1695
  handleError(error, program.opts().verbose);
1390
1696
  process.exit(getExitCode(error));
1391
1697
  }
1392
1698
  });
1699
+ // Autonomous Trading Agent commands
1700
+ setupAgentCommands(program);
1393
1701
  // MCP Server command
1394
1702
  program
1395
1703
  .command("mcp-server")
@@ -1436,12 +1744,12 @@ function displayGroupedHelp(program) {
1436
1744
  console.log();
1437
1745
  // Group commands
1438
1746
  const commandGroups = {
1439
- "Token Operations": ["create", "buy", "sell", "claim"],
1747
+ "Token Operations": ["create", "buy", "sell", "swap", "claim"],
1440
1748
  "Token Information": ["info", "list"],
1441
- "Portfolio": ["positions", "transactions", "balances"],
1749
+ Portfolio: ["positions", "transactions", "balances"],
1442
1750
  "Account Management": ["account", "env"],
1443
- "AI & Social": ["agent", "chat"],
1444
- "System": ["health", "config", "mcp-server"],
1751
+ "AI & Social": ["agent", "cat", "chat"],
1752
+ System: ["health", "config", "mcp-server"],
1445
1753
  };
1446
1754
  // Display grouped commands
1447
1755
  for (const [groupName, commandNames] of Object.entries(commandGroups)) {
@@ -1545,9 +1853,9 @@ function getTerminalSessionId() {
1545
1853
  // Fall through to other methods (e.g., on macOS or if /proc doesn't exist)
1546
1854
  }
1547
1855
  // Fall back to environment variables or user/term combination
1548
- return process.env.TERM_SESSION_ID ||
1856
+ return (process.env.TERM_SESSION_ID ||
1549
1857
  process.env.SESSION_ID ||
1550
- `${process.env.USER || "default"}-${process.env.TERM || "unknown"}`;
1858
+ `${process.env.USER || "default"}-${process.env.TERM || "unknown"}`);
1551
1859
  }
1552
1860
  // Helper function to check if animation has been shown in this terminal session
1553
1861
  function hasAnimationBeenShown() {