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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
// @ts-ignore - neo-blessed doesn't have types, but @types/blessed provides compatible types
|
|
3
3
|
import blessed from "neo-blessed";
|
|
4
|
+
import { HttpcatClient } from "../client.js";
|
|
4
5
|
import { config } from "../config.js";
|
|
5
6
|
import { printCat } from "./art.js";
|
|
6
7
|
import { validateAmount } from "../utils/validation.js";
|
|
@@ -23,7 +24,6 @@ import { getAccountInfo, switchAccount, addAccount, } from "../commands/account.
|
|
|
23
24
|
import { HttpcatError } from "../client.js";
|
|
24
25
|
import { createHttpcatAgent, chatWithAgent } from "../agent/ax-agent.js";
|
|
25
26
|
import { createLLM } from "../agent/llm-factory.js";
|
|
26
|
-
import { setupAIAgentWizard } from "../agent/setup-wizard.js";
|
|
27
27
|
// Detect terminal background color
|
|
28
28
|
function detectTerminalBackground() {
|
|
29
29
|
// Check COLORFGBG (format: "foreground;background")
|
|
@@ -646,7 +646,8 @@ async function handleCommand(client, command, args, log, logLines, logLinesSmoot
|
|
|
646
646
|
return;
|
|
647
647
|
}
|
|
648
648
|
const [identifier, amountInput] = args;
|
|
649
|
-
const
|
|
649
|
+
const network = client.getNetwork();
|
|
650
|
+
const isTestMode = network === "eip155:84532" || network === "eip155:11155111" || network.includes("sepolia");
|
|
650
651
|
const validAmounts = isTestMode ? TEST_AMOUNTS : PROD_AMOUNTS;
|
|
651
652
|
// Parse flags
|
|
652
653
|
const repeatCount = extractFlag(args, "--repeat")
|
|
@@ -675,10 +676,61 @@ async function handleCommand(client, command, args, log, logLines, logLinesSmoot
|
|
|
675
676
|
let stopReason = "";
|
|
676
677
|
for (let i = 1; i <= repeatCount; i++) {
|
|
677
678
|
try {
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
679
|
+
// Retry logic: try up to 10 times with exponential backoff on 402 errors
|
|
680
|
+
// Client is recreated on each attempt to ensure fresh signature for new nonce
|
|
681
|
+
let result = null;
|
|
682
|
+
let lastError = null;
|
|
683
|
+
const maxRetries = 10;
|
|
684
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
685
|
+
try {
|
|
686
|
+
if (attempt === 1) {
|
|
687
|
+
log(chalk.blue(`Buy ${i}/${repeatCount}...`));
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
log(chalk.yellow(` Retrying buy ${i}/${repeatCount} (attempt ${attempt}/${maxRetries})...`));
|
|
691
|
+
}
|
|
692
|
+
screen.render();
|
|
693
|
+
// Recreate client inside the operation to ensure fresh signature
|
|
694
|
+
// for each attempt (including retries). This fixes the issue where x402-fetch
|
|
695
|
+
// generates a new nonce but reuses the same signature, causing subsequent buys to fail
|
|
696
|
+
result = await (async () => {
|
|
697
|
+
// Recreate client for each attempt to ensure fresh signature for new nonce
|
|
698
|
+
const client = await HttpcatClient.create(privateKey);
|
|
699
|
+
return buyToken(client, identifier, amount, isTestMode, true, // silent=true to avoid console.log interference
|
|
700
|
+
privateKey);
|
|
701
|
+
})();
|
|
702
|
+
// Success! Break out of retry loop
|
|
703
|
+
break;
|
|
704
|
+
}
|
|
705
|
+
catch (error) {
|
|
706
|
+
lastError = error;
|
|
707
|
+
// Only retry on 402 errors (payment required)
|
|
708
|
+
const is402Error = error?.message?.includes("402") ||
|
|
709
|
+
error?.status === 402 ||
|
|
710
|
+
(typeof error === "string" && error.includes("402"));
|
|
711
|
+
if (is402Error && attempt < maxRetries) {
|
|
712
|
+
// Exponential backoff: 1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s (capped at 10s)
|
|
713
|
+
const backoffMs = Math.min(Math.pow(2, attempt - 1) * 1000, 10000);
|
|
714
|
+
log(chalk.yellow(` ⚠️ Payment required (attempt ${attempt}/${maxRetries}), retrying in ${backoffMs / 1000}s...`));
|
|
715
|
+
screen.render();
|
|
716
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs));
|
|
717
|
+
continue; // Retry
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
// Not a 402 error, or max retries reached - throw the error
|
|
721
|
+
throw error;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
// If we exhausted retries, throw the last error
|
|
726
|
+
if (!result) {
|
|
727
|
+
if (lastError) {
|
|
728
|
+
throw lastError;
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
throw new Error(`Failed to complete buy ${i}/${repeatCount} after ${maxRetries} attempts`);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
682
734
|
results.push(result);
|
|
683
735
|
totalSpent += parseFloat(result.amountSpent);
|
|
684
736
|
// Display compact result
|
|
@@ -690,9 +742,63 @@ async function handleCommand(client, command, args, log, logLines, logLinesSmoot
|
|
|
690
742
|
log(chalk.green("🎓 Token has graduated! Stopping buy loop."));
|
|
691
743
|
break;
|
|
692
744
|
}
|
|
745
|
+
// Wait for transaction confirmations if present
|
|
746
|
+
// This ensures both the buy transaction and payment transaction are confirmed
|
|
747
|
+
// before the next buy, preventing nonce/signature conflicts
|
|
748
|
+
if (i < repeatCount) {
|
|
749
|
+
const { createPublicClient, http } = await import("viem");
|
|
750
|
+
const { baseSepolia } = await import("viem/chains");
|
|
751
|
+
const publicClient = createPublicClient({
|
|
752
|
+
chain: baseSepolia,
|
|
753
|
+
transport: http(config.getRpcUrl()),
|
|
754
|
+
});
|
|
755
|
+
// Wait for payment transaction first (if present)
|
|
756
|
+
// This is critical to ensure the payment nonce is consumed before next request
|
|
757
|
+
if (result.paymentTxHash) {
|
|
758
|
+
log(chalk.dim(` Waiting for payment transaction confirmation...`));
|
|
759
|
+
screen.render();
|
|
760
|
+
try {
|
|
761
|
+
await publicClient.waitForTransactionReceipt({
|
|
762
|
+
hash: result.paymentTxHash,
|
|
763
|
+
});
|
|
764
|
+
log(chalk.dim(` ✅ Payment transaction confirmed`));
|
|
765
|
+
screen.render();
|
|
766
|
+
}
|
|
767
|
+
catch (txError) {
|
|
768
|
+
log(chalk.yellow(` ⚠️ Could not confirm payment transaction, proceeding...`));
|
|
769
|
+
screen.render();
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
// Wait for buy transaction (if present)
|
|
773
|
+
if (result.txHash) {
|
|
774
|
+
log(chalk.dim(` Waiting for buy transaction confirmation...`));
|
|
775
|
+
screen.render();
|
|
776
|
+
try {
|
|
777
|
+
await publicClient.waitForTransactionReceipt({
|
|
778
|
+
hash: result.txHash,
|
|
779
|
+
});
|
|
780
|
+
log(chalk.dim(` ✅ Buy transaction confirmed`));
|
|
781
|
+
screen.render();
|
|
782
|
+
}
|
|
783
|
+
catch (txError) {
|
|
784
|
+
log(chalk.yellow(` ⚠️ Could not confirm buy transaction, proceeding...`));
|
|
785
|
+
screen.render();
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
693
789
|
// Apply delay between iterations (except after the last one)
|
|
694
|
-
|
|
695
|
-
|
|
790
|
+
// For bonding curve buys, we need a minimum delay to allow backend
|
|
791
|
+
// to process the transaction and update balance state
|
|
792
|
+
if (i < repeatCount) {
|
|
793
|
+
const MIN_DELAY_MS = 2000; // 2 seconds minimum for backend processing
|
|
794
|
+
const totalDelay = Math.max(delayMs, MIN_DELAY_MS);
|
|
795
|
+
if (totalDelay > 0) {
|
|
796
|
+
if (totalDelay === MIN_DELAY_MS && delayMs === 0) {
|
|
797
|
+
log(chalk.dim(` Waiting ${totalDelay / 1000}s for backend to process...`));
|
|
798
|
+
screen.render();
|
|
799
|
+
}
|
|
800
|
+
await new Promise((resolve) => setTimeout(resolve, totalDelay));
|
|
801
|
+
}
|
|
696
802
|
}
|
|
697
803
|
}
|
|
698
804
|
catch (error) {
|
|
@@ -1117,7 +1223,7 @@ async function handleCommand(client, command, args, log, logLines, logLinesSmoot
|
|
|
1117
1223
|
log(chalk.red("Usage: env add <name> <agentUrl> [--network <network>]"));
|
|
1118
1224
|
return;
|
|
1119
1225
|
}
|
|
1120
|
-
const network = extractFlag(args, "--network") || "
|
|
1226
|
+
const network = extractFlag(args, "--network") || "eip155:84532";
|
|
1121
1227
|
config.addEnvironment(args[1], args[2], network);
|
|
1122
1228
|
log(chalk.green(`✅ Added environment: ${args[1]}`));
|
|
1123
1229
|
log(chalk.dim(` Agent URL: ${args[2]}`));
|
|
@@ -1128,7 +1234,7 @@ async function handleCommand(client, command, args, log, logLines, logLinesSmoot
|
|
|
1128
1234
|
log(chalk.red("Usage: env update <name> <agentUrl> [--network <network>]"));
|
|
1129
1235
|
return;
|
|
1130
1236
|
}
|
|
1131
|
-
const network = extractFlag(args, "--network") || "
|
|
1237
|
+
const network = extractFlag(args, "--network") || "eip155:84532";
|
|
1132
1238
|
config.updateEnvironment(args[1], args[2], network);
|
|
1133
1239
|
log(chalk.green(`✅ Updated environment: ${args[1]}`));
|
|
1134
1240
|
log(chalk.dim(` Agent URL: ${args[2]}`));
|
|
@@ -1142,94 +1248,10 @@ async function handleCommand(client, command, args, log, logLines, logLinesSmoot
|
|
|
1142
1248
|
case "agent":
|
|
1143
1249
|
case "ai":
|
|
1144
1250
|
case "cat": {
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
log("");
|
|
1150
|
-
break;
|
|
1151
|
-
}
|
|
1152
|
-
// Check if --setup flag is provided
|
|
1153
|
-
if (args[0] === "--setup") {
|
|
1154
|
-
try {
|
|
1155
|
-
await setupAIAgentWizard();
|
|
1156
|
-
log(chalk.green("✅ Agent configuration updated!"));
|
|
1157
|
-
log("");
|
|
1158
|
-
}
|
|
1159
|
-
catch (error) {
|
|
1160
|
-
log(chalk.red(`❌ Setup failed: ${error.message || String(error)}`));
|
|
1161
|
-
log("");
|
|
1162
|
-
}
|
|
1163
|
-
break;
|
|
1164
|
-
}
|
|
1165
|
-
// Get the query text (everything after the command)
|
|
1166
|
-
const query = args.join(" ").trim();
|
|
1167
|
-
if (!query) {
|
|
1168
|
-
log(chalk.yellow("Usage: agent <query>"));
|
|
1169
|
-
log(chalk.dim("Example: agent 'buy 0.1 USDC worth of WHALE'"));
|
|
1170
|
-
log(chalk.dim("Example: cat 'check my balance'"));
|
|
1171
|
-
log("");
|
|
1172
|
-
break;
|
|
1173
|
-
}
|
|
1174
|
-
// Check if agent is configured
|
|
1175
|
-
const agentConfig = config.getAIAgentConfig();
|
|
1176
|
-
if (!agentConfig) {
|
|
1177
|
-
log(chalk.yellow("⚠️ Agent not configured"));
|
|
1178
|
-
log(chalk.dim("Run 'agent --setup' to configure the AI agent."));
|
|
1179
|
-
log("");
|
|
1180
|
-
break;
|
|
1181
|
-
}
|
|
1182
|
-
const apiKey = config.getAIAgentApiKey();
|
|
1183
|
-
if (!apiKey) {
|
|
1184
|
-
log(chalk.red("❌ Failed to get API key"));
|
|
1185
|
-
log(chalk.dim("Run 'agent --setup' to configure the API key."));
|
|
1186
|
-
log("");
|
|
1187
|
-
break;
|
|
1188
|
-
}
|
|
1189
|
-
try {
|
|
1190
|
-
// Get account address for session ID
|
|
1191
|
-
const privateKey = config.getPrivateKey();
|
|
1192
|
-
const account = privateKeyToAccount(privateKey);
|
|
1193
|
-
const sessionId = account.address;
|
|
1194
|
-
// Show thinking indicator
|
|
1195
|
-
log(chalk.blue("🐱 Cat thinking..."));
|
|
1196
|
-
screen.render();
|
|
1197
|
-
// Create LLM and agent
|
|
1198
|
-
const llm = createLLM(agentConfig, apiKey);
|
|
1199
|
-
const agent = createHttpcatAgent(client, llm);
|
|
1200
|
-
// Chat with agent (pass session ID so it remembers)
|
|
1201
|
-
const response = await chatWithAgent(agent, llm, query, sessionId);
|
|
1202
|
-
// Display response
|
|
1203
|
-
log("");
|
|
1204
|
-
log(chalk.green("🐱 Cat:"));
|
|
1205
|
-
log(chalk.white(response));
|
|
1206
|
-
log("");
|
|
1207
|
-
}
|
|
1208
|
-
catch (error) {
|
|
1209
|
-
// Enhanced error logging
|
|
1210
|
-
if (process.env.HTTPCAT_DEBUG) {
|
|
1211
|
-
log(chalk.red(`❌ Error details: ${JSON.stringify(error, null, 2)}`));
|
|
1212
|
-
if (error.stack) {
|
|
1213
|
-
log(chalk.dim(error.stack));
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
// Check for authentication errors
|
|
1217
|
-
const errorMessage = error?.message || String(error);
|
|
1218
|
-
if (errorMessage.includes("Authentication failed") ||
|
|
1219
|
-
errorMessage.includes("API key") ||
|
|
1220
|
-
errorMessage.includes("401") ||
|
|
1221
|
-
errorMessage.includes("403") ||
|
|
1222
|
-
errorMessage.includes("Unauthorized") ||
|
|
1223
|
-
errorMessage.includes("Invalid API key")) {
|
|
1224
|
-
log(chalk.red("❌ Authentication failed"));
|
|
1225
|
-
log(chalk.dim("Your API key may be invalid or expired."));
|
|
1226
|
-
log(chalk.dim("Run 'agent --setup' to reconfigure."));
|
|
1227
|
-
}
|
|
1228
|
-
else {
|
|
1229
|
-
log(chalk.red(`❌ Error: ${errorMessage}`));
|
|
1230
|
-
}
|
|
1231
|
-
log("");
|
|
1232
|
-
}
|
|
1251
|
+
log("");
|
|
1252
|
+
log(chalk.yellow("⚠️ Agent command is only available in CLI mode"));
|
|
1253
|
+
log(chalk.dim("Run 'httpcat cat' from the command line to start agent mode."));
|
|
1254
|
+
log("");
|
|
1233
1255
|
break;
|
|
1234
1256
|
}
|
|
1235
1257
|
default:
|
|
@@ -1941,6 +1963,7 @@ async function startChatInShell(client, tokenIdentifier, log, logLines, screen,
|
|
|
1941
1963
|
leaseId: joinResult.leaseId,
|
|
1942
1964
|
leaseExpiresAt: new Date(joinResult.leaseExpiresAt),
|
|
1943
1965
|
};
|
|
1966
|
+
const tokenAddress = joinResult.tokenAddress; // Store token address from response
|
|
1944
1967
|
// Update header with lease info
|
|
1945
1968
|
await updateChatHeader(false);
|
|
1946
1969
|
// Display last messages
|
|
@@ -2108,7 +2131,7 @@ async function startChatInShell(client, tokenIdentifier, log, logLines, screen,
|
|
|
2108
2131
|
}
|
|
2109
2132
|
try {
|
|
2110
2133
|
// Send message - it will appear via WebSocket when received
|
|
2111
|
-
await sendChatMessage(client, trimmed, leaseInfo.leaseId, userAddress);
|
|
2134
|
+
await sendChatMessage(client, trimmed, leaseInfo.leaseId, userAddress, tokenAddress);
|
|
2112
2135
|
// Re-attach handler for next message
|
|
2113
2136
|
inputBox.removeAllListeners("submit");
|
|
2114
2137
|
inputBox.once("submit", chatInputHandler);
|
|
@@ -2157,6 +2180,386 @@ async function startChatInShell(client, tokenIdentifier, log, logLines, screen,
|
|
|
2157
2180
|
screen.render();
|
|
2158
2181
|
}
|
|
2159
2182
|
}
|
|
2183
|
+
/**
|
|
2184
|
+
* Start agent interactive mode (standalone, called from CLI)
|
|
2185
|
+
* This looks identical to the regular shell but starts in agent chat mode
|
|
2186
|
+
*/
|
|
2187
|
+
export async function startAgentInteractiveMode(client) {
|
|
2188
|
+
// Auto-detect terminal background and set default theme
|
|
2189
|
+
const detectedBg = detectTerminalBackground();
|
|
2190
|
+
let currentTheme = detectedBg === "dark" ? "dark" : "win95";
|
|
2191
|
+
// Helper function to get theme-appropriate cyan/blue color for blessed tags
|
|
2192
|
+
// For dark theme, use lighter colors (light-cyan-fg) for better visibility on black
|
|
2193
|
+
const getCyanColor = (theme) => theme === "dark" ? "light-cyan-fg" : "cyan-fg";
|
|
2194
|
+
const getBlueColor = (theme) => theme === "dark" ? "light-blue-fg" : "blue-fg";
|
|
2195
|
+
// Create blessed screen with optimized settings
|
|
2196
|
+
const screen = blessed.screen({
|
|
2197
|
+
smartCSR: true,
|
|
2198
|
+
title: "httpcat Agent Mode",
|
|
2199
|
+
fullUnicode: true, // Support double-width/surrogate/combining chars (emojis)
|
|
2200
|
+
fastCSR: false, // Disable fast CSR to prevent rendering issues
|
|
2201
|
+
cursor: {
|
|
2202
|
+
artificial: true,
|
|
2203
|
+
shape: "line",
|
|
2204
|
+
blink: true,
|
|
2205
|
+
color: "green",
|
|
2206
|
+
},
|
|
2207
|
+
// Force Unicode support for emojis
|
|
2208
|
+
forceUnicode: true,
|
|
2209
|
+
});
|
|
2210
|
+
const network = client.getNetwork();
|
|
2211
|
+
// Theme colors - no backgrounds, just borders
|
|
2212
|
+
const getThemeColors = (theme) => {
|
|
2213
|
+
switch (theme) {
|
|
2214
|
+
case "light":
|
|
2215
|
+
return {
|
|
2216
|
+
bg: "default", // Transparent/default
|
|
2217
|
+
fg: "black",
|
|
2218
|
+
border: "black",
|
|
2219
|
+
inputBg: "default",
|
|
2220
|
+
inputFg: "black", // Explicit black for visibility
|
|
2221
|
+
inputFocusBg: "default",
|
|
2222
|
+
inputFocusFg: "black",
|
|
2223
|
+
};
|
|
2224
|
+
case "win95":
|
|
2225
|
+
return {
|
|
2226
|
+
bg: "default",
|
|
2227
|
+
fg: "black",
|
|
2228
|
+
border: "black",
|
|
2229
|
+
inputBg: "default",
|
|
2230
|
+
inputFg: "black", // Explicit black for visibility
|
|
2231
|
+
inputFocusBg: "default",
|
|
2232
|
+
inputFocusFg: "black",
|
|
2233
|
+
};
|
|
2234
|
+
default: // dark
|
|
2235
|
+
return {
|
|
2236
|
+
bg: "default",
|
|
2237
|
+
fg: "green",
|
|
2238
|
+
border: "green",
|
|
2239
|
+
inputBg: "default",
|
|
2240
|
+
inputFg: "green", // Explicit green for visibility
|
|
2241
|
+
inputFocusBg: "default",
|
|
2242
|
+
inputFocusFg: "green",
|
|
2243
|
+
};
|
|
2244
|
+
}
|
|
2245
|
+
};
|
|
2246
|
+
let themeColors = getThemeColors(currentTheme);
|
|
2247
|
+
// Create header box with thick borders, transparent background - more compact
|
|
2248
|
+
const headerBox = blessed.box({
|
|
2249
|
+
top: 0,
|
|
2250
|
+
left: 0,
|
|
2251
|
+
width: "100%",
|
|
2252
|
+
height: 6, // Reduced to save vertical space
|
|
2253
|
+
content: "",
|
|
2254
|
+
tags: true,
|
|
2255
|
+
style: {
|
|
2256
|
+
fg: themeColors.fg,
|
|
2257
|
+
bg: "default", // Transparent
|
|
2258
|
+
bold: false,
|
|
2259
|
+
border: {
|
|
2260
|
+
fg: themeColors.border,
|
|
2261
|
+
bold: true,
|
|
2262
|
+
},
|
|
2263
|
+
},
|
|
2264
|
+
padding: {
|
|
2265
|
+
left: 1,
|
|
2266
|
+
right: 1,
|
|
2267
|
+
top: 0,
|
|
2268
|
+
bottom: 0,
|
|
2269
|
+
},
|
|
2270
|
+
border: {
|
|
2271
|
+
type: "line",
|
|
2272
|
+
fg: themeColors.border,
|
|
2273
|
+
ch: "═", // Double line for thicker border
|
|
2274
|
+
},
|
|
2275
|
+
});
|
|
2276
|
+
// Cat face variants for animation
|
|
2277
|
+
const catFaces = [
|
|
2278
|
+
{ name: "Sleepy", face: "[=^ -.- ^=]" },
|
|
2279
|
+
{ name: "Smug", face: "[=^‿^=]" },
|
|
2280
|
+
{ name: "Unhinged", face: "[=^◉_◉^=]" },
|
|
2281
|
+
{ name: "Judgy", face: "[=^ಠ‿ಠ^=]" },
|
|
2282
|
+
{ name: "Cute", face: "[=^。^=]" },
|
|
2283
|
+
{ name: "Menacing", face: "[=^>_<^=]" },
|
|
2284
|
+
{ name: "Loaf Mode", face: "[=^___^=]" },
|
|
2285
|
+
{ name: "Cosmic", face: "[=^✧_✧^=]" },
|
|
2286
|
+
];
|
|
2287
|
+
let currentCatIndex = 0;
|
|
2288
|
+
let catAnimationInterval = null;
|
|
2289
|
+
// Helper function to build welcome content with account info - more compact
|
|
2290
|
+
const buildWelcomeContent = async (theme, catFace) => {
|
|
2291
|
+
const welcomeLines = [];
|
|
2292
|
+
const colorTag = theme === "dark" ? "green-fg" : "black-fg";
|
|
2293
|
+
const cyanColor = getCyanColor(theme);
|
|
2294
|
+
// Use provided cat face or current one
|
|
2295
|
+
const displayCatFace = catFace || catFaces[currentCatIndex].face;
|
|
2296
|
+
// Cat face with breathing room, compact info below
|
|
2297
|
+
welcomeLines.push(`{${colorTag}}${displayCatFace}{/${colorTag}}`);
|
|
2298
|
+
welcomeLines.push(`{green-fg}🐱 Welcome to httpcat!{/green-fg} | {green-fg}🌐 {${cyanColor}}${network}{/${cyanColor}}{/green-fg}`);
|
|
2299
|
+
// Get account info
|
|
2300
|
+
let accountInfo = null;
|
|
2301
|
+
try {
|
|
2302
|
+
const accounts = config.getAllAccounts();
|
|
2303
|
+
const activeIndex = config.getActiveAccountIndex();
|
|
2304
|
+
const account = accounts.find((acc) => acc.index === activeIndex);
|
|
2305
|
+
if (account) {
|
|
2306
|
+
// Get balance info
|
|
2307
|
+
try {
|
|
2308
|
+
const privateKey = config.getAccountPrivateKey(activeIndex);
|
|
2309
|
+
const balance = await checkBalance(privateKey, true); // silent mode
|
|
2310
|
+
accountInfo = {
|
|
2311
|
+
account,
|
|
2312
|
+
balance,
|
|
2313
|
+
};
|
|
2314
|
+
}
|
|
2315
|
+
catch (error) {
|
|
2316
|
+
// If balance check fails, just show account info without balance
|
|
2317
|
+
accountInfo = { account };
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
catch (error) {
|
|
2322
|
+
// If account info fails, continue without it
|
|
2323
|
+
}
|
|
2324
|
+
// Compact account info - combined on fewer lines
|
|
2325
|
+
if (accountInfo) {
|
|
2326
|
+
const { account, balance } = accountInfo;
|
|
2327
|
+
const accountType = account.type === "custom" ? "Custom" : "Seed-Derived";
|
|
2328
|
+
const accountLabel = account.label ? ` (${account.label})` : "";
|
|
2329
|
+
if (balance) {
|
|
2330
|
+
const ethDisplay = balance.ethFormatted || balance.ethBalance || "0 ETH";
|
|
2331
|
+
const usdcDisplay = balance.usdcFormatted || balance.usdcBalance || "$0.00";
|
|
2332
|
+
// Combine account and balance on one line to save space
|
|
2333
|
+
welcomeLines.push(`{${cyanColor}}👤 Account #{green-fg}${account.index}{/green-fg} | {green-fg}${accountType}${accountLabel}{/green-fg} | 💰 {yellow-fg}${ethDisplay}{/yellow-fg} | {green-fg}${usdcDisplay}{/green-fg}{/${cyanColor}}`);
|
|
2334
|
+
}
|
|
2335
|
+
else {
|
|
2336
|
+
welcomeLines.push(`{${cyanColor}}👤 Account #{green-fg}${account.index}{/green-fg} | {green-fg}${accountType}${accountLabel}{/green-fg}{/${cyanColor}}`);
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
return welcomeLines.join("\n");
|
|
2340
|
+
};
|
|
2341
|
+
// Set initial header content
|
|
2342
|
+
buildWelcomeContent(currentTheme, catFaces[currentCatIndex].face).then((content) => {
|
|
2343
|
+
headerBox.setContent(content);
|
|
2344
|
+
screen.render();
|
|
2345
|
+
});
|
|
2346
|
+
// Start cat face animation (cycle through every minute)
|
|
2347
|
+
const startCatAnimation = () => {
|
|
2348
|
+
if (catAnimationInterval) {
|
|
2349
|
+
clearInterval(catAnimationInterval);
|
|
2350
|
+
}
|
|
2351
|
+
catAnimationInterval = setInterval(() => {
|
|
2352
|
+
currentCatIndex = (currentCatIndex + 1) % catFaces.length;
|
|
2353
|
+
buildWelcomeContent(currentTheme, catFaces[currentCatIndex].face).then((content) => {
|
|
2354
|
+
headerBox.setContent(content);
|
|
2355
|
+
screen.render();
|
|
2356
|
+
});
|
|
2357
|
+
}, 60000); // Change every minute (60 seconds)
|
|
2358
|
+
};
|
|
2359
|
+
// Start animation
|
|
2360
|
+
startCatAnimation();
|
|
2361
|
+
// Create output log box (scrollable) with thick borders, transparent background
|
|
2362
|
+
const outputBox = blessed.log({
|
|
2363
|
+
top: 6, // Adjusted to match new header height
|
|
2364
|
+
left: 0,
|
|
2365
|
+
width: "100%",
|
|
2366
|
+
bottom: 4, // Leave space for input box at bottom
|
|
2367
|
+
tags: true,
|
|
2368
|
+
scrollable: true,
|
|
2369
|
+
alwaysScroll: true,
|
|
2370
|
+
scrollbar: {
|
|
2371
|
+
ch: " ",
|
|
2372
|
+
inverse: currentTheme !== "dark",
|
|
2373
|
+
},
|
|
2374
|
+
style: {
|
|
2375
|
+
fg: themeColors.fg,
|
|
2376
|
+
bg: "default", // Transparent
|
|
2377
|
+
border: {
|
|
2378
|
+
fg: themeColors.border,
|
|
2379
|
+
bold: true,
|
|
2380
|
+
},
|
|
2381
|
+
},
|
|
2382
|
+
padding: {
|
|
2383
|
+
left: 0,
|
|
2384
|
+
right: 1,
|
|
2385
|
+
},
|
|
2386
|
+
mouse: true, // Enable mouse scrolling
|
|
2387
|
+
border: {
|
|
2388
|
+
type: "line",
|
|
2389
|
+
fg: themeColors.border,
|
|
2390
|
+
ch: "═", // Double line for thicker border
|
|
2391
|
+
},
|
|
2392
|
+
});
|
|
2393
|
+
// Create prompt label with bold font (appears larger) - positioned inside input box
|
|
2394
|
+
const promptLabel = blessed.text({
|
|
2395
|
+
bottom: 1,
|
|
2396
|
+
left: 2,
|
|
2397
|
+
width: 8, // Exactly "httpcat>" (8 characters)
|
|
2398
|
+
height: 1,
|
|
2399
|
+
content: "",
|
|
2400
|
+
tags: true,
|
|
2401
|
+
style: {
|
|
2402
|
+
fg: themeColors.fg,
|
|
2403
|
+
bg: "default", // Transparent
|
|
2404
|
+
bold: true,
|
|
2405
|
+
},
|
|
2406
|
+
});
|
|
2407
|
+
// Helper to update prompt label content
|
|
2408
|
+
const updatePromptLabel = (theme) => {
|
|
2409
|
+
const colorTag = theme === "dark" ? "green-fg" : "black-fg";
|
|
2410
|
+
promptLabel.content = `{${colorTag}}{bold}httpcat>{/bold}{/${colorTag}}`;
|
|
2411
|
+
};
|
|
2412
|
+
updatePromptLabel(currentTheme);
|
|
2413
|
+
// Create input box with visible cursor and stylish border
|
|
2414
|
+
const inputBox = blessed.textbox({
|
|
2415
|
+
bottom: 0,
|
|
2416
|
+
left: 0,
|
|
2417
|
+
width: "100%",
|
|
2418
|
+
height: 3,
|
|
2419
|
+
inputOnFocus: true,
|
|
2420
|
+
keys: true,
|
|
2421
|
+
vi: false, // Disabled to prevent double input issues in agent mode
|
|
2422
|
+
secret: false,
|
|
2423
|
+
tags: true,
|
|
2424
|
+
alwaysScroll: false,
|
|
2425
|
+
scrollable: false,
|
|
2426
|
+
padding: {
|
|
2427
|
+
left: 10, // Space for "httpcat>" prompt (8 chars) + 2 for spacing
|
|
2428
|
+
right: 1,
|
|
2429
|
+
top: 0,
|
|
2430
|
+
bottom: 0,
|
|
2431
|
+
},
|
|
2432
|
+
cursor: {
|
|
2433
|
+
artificial: true,
|
|
2434
|
+
shape: "block", // Block cursor is more visible than line
|
|
2435
|
+
blink: true,
|
|
2436
|
+
color: currentTheme === "dark" ? "green" : "black",
|
|
2437
|
+
},
|
|
2438
|
+
style: {
|
|
2439
|
+
fg: themeColors.fg,
|
|
2440
|
+
bg: "default",
|
|
2441
|
+
border: {
|
|
2442
|
+
fg: themeColors.border,
|
|
2443
|
+
bold: true,
|
|
2444
|
+
},
|
|
2445
|
+
focus: {
|
|
2446
|
+
fg: themeColors.fg,
|
|
2447
|
+
bg: "default",
|
|
2448
|
+
border: {
|
|
2449
|
+
fg: themeColors.border,
|
|
2450
|
+
bold: true,
|
|
2451
|
+
},
|
|
2452
|
+
},
|
|
2453
|
+
},
|
|
2454
|
+
border: {
|
|
2455
|
+
type: "line",
|
|
2456
|
+
fg: themeColors.border,
|
|
2457
|
+
ch: "─", // Single line border
|
|
2458
|
+
},
|
|
2459
|
+
});
|
|
2460
|
+
// Helper to update theme
|
|
2461
|
+
const updateTheme = (newTheme) => {
|
|
2462
|
+
currentTheme = newTheme;
|
|
2463
|
+
themeColors = getThemeColors(currentTheme);
|
|
2464
|
+
// Update screen cursor color
|
|
2465
|
+
screen.cursor.color =
|
|
2466
|
+
currentTheme === "dark" ? "green" : "black";
|
|
2467
|
+
// Update header content with new theme colors (keep current cat face)
|
|
2468
|
+
buildWelcomeContent(currentTheme, catFaces[currentCatIndex].face).then((content) => {
|
|
2469
|
+
headerBox.setContent(content);
|
|
2470
|
+
screen.render();
|
|
2471
|
+
});
|
|
2472
|
+
// Update all widget styles
|
|
2473
|
+
headerBox.style.fg = themeColors.fg;
|
|
2474
|
+
headerBox.style.bg = "default"; // Transparent
|
|
2475
|
+
headerBox.border = { type: "line", fg: themeColors.border, ch: "═" };
|
|
2476
|
+
outputBox.style.fg = themeColors.fg;
|
|
2477
|
+
outputBox.style.bg = "default"; // Transparent
|
|
2478
|
+
outputBox.scrollbar.inverse = currentTheme !== "dark";
|
|
2479
|
+
outputBox.border = { type: "line", fg: themeColors.border, ch: "═" };
|
|
2480
|
+
updatePromptLabel(currentTheme);
|
|
2481
|
+
promptLabel.style.fg = themeColors.fg;
|
|
2482
|
+
promptLabel.style.bg = "default"; // Transparent
|
|
2483
|
+
promptLabel.style.bold = true;
|
|
2484
|
+
// Update input box cursor, style, and border
|
|
2485
|
+
inputBox.style.fg = themeColors.fg;
|
|
2486
|
+
inputBox.style.bg = "default";
|
|
2487
|
+
inputBox.style.focus.fg = themeColors.fg;
|
|
2488
|
+
inputBox.style.focus.bg = "default";
|
|
2489
|
+
inputBox.style.border.fg = themeColors.border;
|
|
2490
|
+
inputBox.style.focus.border.fg = themeColors.border;
|
|
2491
|
+
inputBox.border = { type: "line", fg: themeColors.border, ch: "─" };
|
|
2492
|
+
if (inputBox.cursor) {
|
|
2493
|
+
inputBox.cursor.color =
|
|
2494
|
+
currentTheme === "dark" ? "green" : "black";
|
|
2495
|
+
}
|
|
2496
|
+
screen.cursor.color =
|
|
2497
|
+
currentTheme === "dark" ? "green" : "black";
|
|
2498
|
+
screen.render();
|
|
2499
|
+
};
|
|
2500
|
+
// Helper to log output (define before use)
|
|
2501
|
+
const log = (text) => {
|
|
2502
|
+
outputBox.log(text);
|
|
2503
|
+
outputBox.setScrollPerc(100);
|
|
2504
|
+
screen.render();
|
|
2505
|
+
};
|
|
2506
|
+
// Helper to log multiple lines
|
|
2507
|
+
const logLines = (lines) => {
|
|
2508
|
+
lines.forEach((line) => outputBox.log(line));
|
|
2509
|
+
outputBox.setScrollPerc(100);
|
|
2510
|
+
screen.render();
|
|
2511
|
+
};
|
|
2512
|
+
// Wrap toggleTheme to also log
|
|
2513
|
+
const toggleThemeWithLog = () => {
|
|
2514
|
+
const themes = ["win95", "dark", "light"];
|
|
2515
|
+
const currentIndex = themes.indexOf(currentTheme);
|
|
2516
|
+
const nextTheme = themes[(currentIndex + 1) % themes.length];
|
|
2517
|
+
updateTheme(nextTheme);
|
|
2518
|
+
log(chalk.blue(`Theme switched to: ${nextTheme}`));
|
|
2519
|
+
};
|
|
2520
|
+
// Append all widgets in correct z-order (last appended is on top)
|
|
2521
|
+
screen.append(headerBox);
|
|
2522
|
+
screen.append(outputBox);
|
|
2523
|
+
screen.append(inputBox);
|
|
2524
|
+
screen.append(promptLabel); // Prompt label on top so it's always visible
|
|
2525
|
+
// Handle F1 for theme toggle
|
|
2526
|
+
screen.key(["f1"], () => {
|
|
2527
|
+
toggleThemeWithLog();
|
|
2528
|
+
});
|
|
2529
|
+
// Store toggle function for command handler
|
|
2530
|
+
screen.toggleTheme = toggleThemeWithLog;
|
|
2531
|
+
screen.updateTheme = updateTheme;
|
|
2532
|
+
// Handle Ctrl+C - two-stage: first clears input if text exists, second quits
|
|
2533
|
+
const handleCtrlC = () => {
|
|
2534
|
+
const currentValue = inputBox.getValue();
|
|
2535
|
+
// If there's text in the input, clear it instead of quitting
|
|
2536
|
+
if (currentValue && currentValue.trim().length > 0) {
|
|
2537
|
+
inputBox.clearValue();
|
|
2538
|
+
screen.render();
|
|
2539
|
+
return;
|
|
2540
|
+
}
|
|
2541
|
+
// No text in input, so quit
|
|
2542
|
+
if (catAnimationInterval) {
|
|
2543
|
+
clearInterval(catAnimationInterval);
|
|
2544
|
+
catAnimationInterval = null;
|
|
2545
|
+
}
|
|
2546
|
+
screen.destroy();
|
|
2547
|
+
printCat("sleeping");
|
|
2548
|
+
console.log(chalk.cyan("Goodbye! 👋"));
|
|
2549
|
+
process.exit(0);
|
|
2550
|
+
};
|
|
2551
|
+
// Handle Ctrl+C on screen level
|
|
2552
|
+
screen.key(["C-c"], handleCtrlC);
|
|
2553
|
+
// Handle Ctrl+C on input box level
|
|
2554
|
+
inputBox.key(["C-c"], handleCtrlC);
|
|
2555
|
+
// Focus input and render
|
|
2556
|
+
inputBox.focus();
|
|
2557
|
+
screen.render();
|
|
2558
|
+
// Show welcome message with key commands on load
|
|
2559
|
+
displayWelcomeMessage(log, logLines, outputBox, screen, currentTheme);
|
|
2560
|
+
// Start agent chat mode
|
|
2561
|
+
await startAgentChatMode(client, log, logLines, screen, inputBox, headerBox, buildWelcomeContent, currentTheme, catFaces, currentCatIndex);
|
|
2562
|
+
}
|
|
2160
2563
|
/**
|
|
2161
2564
|
* Start agent chat mode (similar to startChatInShell)
|
|
2162
2565
|
*/
|
|
@@ -2217,17 +2620,10 @@ async function startAgentChatMode(client, log, logLines, screen, inputBox, heade
|
|
|
2217
2620
|
switch (cmd) {
|
|
2218
2621
|
case "/exit":
|
|
2219
2622
|
case "/quit":
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
headerBox.setContent(originalContent);
|
|
2225
|
-
// Restore original input handler
|
|
2226
|
-
inputBox.removeAllListeners("submit");
|
|
2227
|
-
inputBox.on("submit", originalSubmitHandler);
|
|
2228
|
-
isProcessing = false;
|
|
2229
|
-
inputBox.focus();
|
|
2230
|
-
screen.render();
|
|
2623
|
+
screen.destroy();
|
|
2624
|
+
printCat("sleeping");
|
|
2625
|
+
console.log(chalk.cyan("Goodbye! 👋"));
|
|
2626
|
+
process.exit(0);
|
|
2231
2627
|
return;
|
|
2232
2628
|
case "/help":
|
|
2233
2629
|
log(chalk.cyan("Cat commands: /exit, /quit, /help"));
|
|
@@ -2245,9 +2641,13 @@ async function startAgentChatMode(client, log, logLines, screen, inputBox, heade
|
|
|
2245
2641
|
}
|
|
2246
2642
|
// Send to agent
|
|
2247
2643
|
try {
|
|
2644
|
+
// Get account address for session ID
|
|
2645
|
+
const privateKey = config.getPrivateKey();
|
|
2646
|
+
const account = privateKeyToAccount(privateKey);
|
|
2647
|
+
const sessionId = account.address;
|
|
2248
2648
|
log(chalk.blue("🐱 Cat thinking..."));
|
|
2249
2649
|
screen.render();
|
|
2250
|
-
const response = await chatWithAgent(agent, llm, trimmed);
|
|
2650
|
+
const response = await chatWithAgent(agent, llm, trimmed, sessionId);
|
|
2251
2651
|
log(chalk.green("🐱 Cat:"));
|
|
2252
2652
|
log(response);
|
|
2253
2653
|
log("");
|