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