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.
- package/README.md +346 -13
- package/Screenshot 2025-12-21 at 8.56.02/342/200/257PM.png +0 -0
- package/bun.lock +5 -0
- package/cat-spin.sh +417 -0
- package/dist/agent/ax-agent.d.ts.map +1 -0
- package/dist/agent/ax-agent.js +459 -0
- package/dist/agent/ax-agent.js.map +1 -0
- package/dist/agent/llm-factory.d.ts.map +1 -0
- package/dist/agent/llm-factory.js +82 -0
- package/dist/agent/llm-factory.js.map +1 -0
- package/dist/agent/setup-wizard.d.ts.map +1 -0
- package/dist/agent/setup-wizard.js +114 -0
- package/dist/agent/setup-wizard.js.map +1 -0
- package/dist/agent/tools.d.ts.map +1 -0
- package/dist/agent/tools.js +393 -0
- package/dist/agent/tools.js.map +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +18 -0
- package/dist/client.js.map +1 -1
- package/dist/commands/balances.js +1 -1
- package/dist/commands/balances.js.map +1 -1
- package/dist/commands/buy.d.ts.map +1 -1
- package/dist/commands/buy.js +11 -6
- package/dist/commands/buy.js.map +1 -1
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +56 -46
- package/dist/commands/chat.js.map +1 -1
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +133 -5
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/info.d.ts.map +1 -1
- package/dist/commands/info.js +3 -2
- package/dist/commands/info.js.map +1 -1
- package/dist/commands/positions.d.ts.map +1 -1
- package/dist/commands/positions.js +51 -54
- package/dist/commands/positions.js.map +1 -1
- package/dist/commands/sell.d.ts.map +1 -1
- package/dist/commands/sell.js +4 -3
- package/dist/commands/sell.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +91 -0
- package/dist/config.js.map +1 -1
- package/dist/index.js +455 -37
- package/dist/index.js.map +1 -1
- package/dist/interactive/cat-spin.d.ts.map +1 -0
- package/dist/interactive/cat-spin.js +448 -0
- package/dist/interactive/cat-spin.js.map +1 -0
- package/dist/interactive/shell.d.ts.map +1 -1
- package/dist/interactive/shell.js +1647 -154
- package/dist/interactive/shell.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 +107 -6
- package/dist/mcp/tools.js.map +1 -1
- package/dist/mcp/types.d.ts.map +1 -1
- package/dist/utils/loading.d.ts.map +1 -1
- package/dist/utils/loading.js +30 -0
- package/dist/utils/loading.js.map +1 -1
- package/dist/utils/token-resolver.d.ts.map +1 -1
- package/dist/utils/token-resolver.js +41 -0
- package/dist/utils/token-resolver.js.map +1 -1
- 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
|
|
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:
|
|
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
|
-
//
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
//
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
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
|
|
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
|
}
|