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.
Files changed (63) hide show
  1. package/README.md +9 -9
  2. package/bun.lock +13 -1308
  3. package/dist/agent/tools.d.ts.map +1 -1
  4. package/dist/agent/tools.js +87 -5
  5. package/dist/agent/tools.js.map +1 -1
  6. package/dist/client.d.ts.map +1 -1
  7. package/dist/client.js +403 -46
  8. package/dist/client.js.map +1 -1
  9. package/dist/commands/account.d.ts.map +1 -1
  10. package/dist/commands/account.js +1 -0
  11. package/dist/commands/account.js.map +1 -1
  12. package/dist/commands/balances.d.ts.map +1 -1
  13. package/dist/commands/balances.js +39 -14
  14. package/dist/commands/balances.js.map +1 -1
  15. package/dist/commands/buy.d.ts.map +1 -1
  16. package/dist/commands/buy.js +29 -15
  17. package/dist/commands/buy.js.map +1 -1
  18. package/dist/commands/chat.d.ts.map +1 -1
  19. package/dist/commands/chat.js +34 -21
  20. package/dist/commands/chat.js.map +1 -1
  21. package/dist/commands/info.d.ts.map +1 -1
  22. package/dist/commands/info.js +12 -9
  23. package/dist/commands/info.js.map +1 -1
  24. package/dist/commands/positions.js +4 -4
  25. package/dist/commands/positions.js.map +1 -1
  26. package/dist/commands/sell.d.ts.map +1 -1
  27. package/dist/commands/sell.js +18 -11
  28. package/dist/commands/sell.js.map +1 -1
  29. package/dist/config.d.ts.map +1 -1
  30. package/dist/config.js +77 -10
  31. package/dist/config.js.map +1 -1
  32. package/dist/index.js +354 -118
  33. package/dist/index.js.map +1 -1
  34. package/dist/interactive/art.d.ts.map +1 -1
  35. package/dist/interactive/art.js +38 -0
  36. package/dist/interactive/art.js.map +1 -1
  37. package/dist/interactive/shell.d.ts.map +1 -1
  38. package/dist/interactive/shell.js +511 -111
  39. package/dist/interactive/shell.js.map +1 -1
  40. package/dist/mcp/chat-state.d.ts.map +1 -1
  41. package/dist/mcp/chat-state.js +2 -1
  42. package/dist/mcp/chat-state.js.map +1 -1
  43. package/dist/mcp/server.js +1 -1
  44. package/dist/mcp/tools.d.ts.map +1 -1
  45. package/dist/mcp/tools.js +108 -1
  46. package/dist/mcp/tools.js.map +1 -1
  47. package/dist/mcp/types.d.ts.map +1 -1
  48. package/dist/utils/constants.d.ts.map +1 -1
  49. package/dist/utils/constants.js +44 -2
  50. package/dist/utils/constants.js.map +1 -1
  51. package/dist/utils/errors.d.ts.map +1 -1
  52. package/dist/utils/errors.js +3 -3
  53. package/dist/utils/errors.js.map +1 -1
  54. package/dist/utils/privateKeyPrompt.d.ts.map +1 -1
  55. package/dist/utils/privateKeyPrompt.js +31 -7
  56. package/dist/utils/privateKeyPrompt.js.map +1 -1
  57. package/dist/utils/status.d.ts.map +1 -0
  58. package/dist/utils/status.js +67 -0
  59. package/dist/utils/status.js.map +1 -0
  60. package/dist/utils/token-resolver.d.ts.map +1 -1
  61. package/dist/utils/token-resolver.js +9 -0
  62. package/dist/utils/token-resolver.js.map +1 -1
  63. 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 isTestMode = client.getNetwork().includes("sepolia");
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
- log(chalk.blue(`Buy ${i}/${repeatCount}...`));
679
- screen.render();
680
- const result = await buyToken(client, identifier, amount, isTestMode, true, // silent=true to avoid console.log interference
681
- privateKey);
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
- if (i < repeatCount && delayMs > 0) {
695
- await new Promise((resolve) => setTimeout(resolve, delayMs));
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") || "base-sepolia";
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") || "base-sepolia";
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
- // Check if --chat flag is provided (for future chat mode)
1146
- if (args[0] === "--chat") {
1147
- log(chalk.yellow("⚠️ Agent chat mode is currently disabled"));
1148
- log(chalk.dim("Use 'agent <query>' or 'cat <query>' to ask the cat directly."));
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
- log(chalk.yellow("Exited cat mode. Back to shell."));
2221
- log("");
2222
- // Restore original header
2223
- const originalContent = await buildWelcomeContent(currentTheme, catFaces[currentCatIndex].face);
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("");