httpcat-cli 0.2.12 → 0.2.13-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +355 -22
  2. package/Screenshot 2025-12-21 at 8.56.02/342/200/257PM.png +0 -0
  3. package/bun.lock +15 -1305
  4. package/cat-spin.sh +417 -0
  5. package/dist/agent/ax-agent.d.ts.map +1 -0
  6. package/dist/agent/ax-agent.js +459 -0
  7. package/dist/agent/ax-agent.js.map +1 -0
  8. package/dist/agent/llm-factory.d.ts.map +1 -0
  9. package/dist/agent/llm-factory.js +82 -0
  10. package/dist/agent/llm-factory.js.map +1 -0
  11. package/dist/agent/setup-wizard.d.ts.map +1 -0
  12. package/dist/agent/setup-wizard.js +114 -0
  13. package/dist/agent/setup-wizard.js.map +1 -0
  14. package/dist/agent/tools.d.ts.map +1 -0
  15. package/dist/agent/tools.js +394 -0
  16. package/dist/agent/tools.js.map +1 -0
  17. package/dist/client.d.ts.map +1 -1
  18. package/dist/client.js +403 -46
  19. package/dist/client.js.map +1 -1
  20. package/dist/commands/account.d.ts.map +1 -1
  21. package/dist/commands/account.js +1 -0
  22. package/dist/commands/account.js.map +1 -1
  23. package/dist/commands/balances.d.ts.map +1 -1
  24. package/dist/commands/balances.js +39 -14
  25. package/dist/commands/balances.js.map +1 -1
  26. package/dist/commands/buy.d.ts.map +1 -1
  27. package/dist/commands/buy.js +22 -14
  28. package/dist/commands/buy.js.map +1 -1
  29. package/dist/commands/chat.d.ts.map +1 -1
  30. package/dist/commands/chat.js +60 -62
  31. package/dist/commands/chat.js.map +1 -1
  32. package/dist/commands/create.d.ts.map +1 -1
  33. package/dist/commands/create.js +133 -5
  34. package/dist/commands/create.js.map +1 -1
  35. package/dist/commands/info.d.ts.map +1 -1
  36. package/dist/commands/info.js +12 -9
  37. package/dist/commands/info.js.map +1 -1
  38. package/dist/commands/positions.d.ts.map +1 -1
  39. package/dist/commands/positions.js +51 -54
  40. package/dist/commands/positions.js.map +1 -1
  41. package/dist/commands/sell.d.ts.map +1 -1
  42. package/dist/commands/sell.js +14 -10
  43. package/dist/commands/sell.js.map +1 -1
  44. package/dist/config.d.ts.map +1 -1
  45. package/dist/config.js +160 -10
  46. package/dist/config.js.map +1 -1
  47. package/dist/index.js +558 -45
  48. package/dist/index.js.map +1 -1
  49. package/dist/interactive/cat-spin.d.ts.map +1 -0
  50. package/dist/interactive/cat-spin.js +448 -0
  51. package/dist/interactive/cat-spin.js.map +1 -0
  52. package/dist/interactive/shell.d.ts.map +1 -1
  53. package/dist/interactive/shell.js +1651 -157
  54. package/dist/interactive/shell.js.map +1 -1
  55. package/dist/mcp/server.js +1 -1
  56. package/dist/mcp/tools.d.ts.map +1 -1
  57. package/dist/mcp/tools.js +109 -7
  58. package/dist/mcp/tools.js.map +1 -1
  59. package/dist/mcp/types.d.ts.map +1 -1
  60. package/dist/utils/constants.d.ts.map +1 -1
  61. package/dist/utils/constants.js +44 -2
  62. package/dist/utils/constants.js.map +1 -1
  63. package/dist/utils/errors.d.ts.map +1 -1
  64. package/dist/utils/errors.js +3 -3
  65. package/dist/utils/errors.js.map +1 -1
  66. package/dist/utils/loading.d.ts.map +1 -1
  67. package/dist/utils/loading.js +30 -0
  68. package/dist/utils/loading.js.map +1 -1
  69. package/dist/utils/privateKeyPrompt.d.ts.map +1 -1
  70. package/dist/utils/privateKeyPrompt.js +31 -7
  71. package/dist/utils/privateKeyPrompt.js.map +1 -1
  72. package/dist/utils/status.d.ts.map +1 -0
  73. package/dist/utils/status.js +67 -0
  74. package/dist/utils/status.js.map +1 -0
  75. package/dist/utils/token-resolver.d.ts.map +1 -1
  76. package/dist/utils/token-resolver.js +41 -0
  77. package/dist/utils/token-resolver.js.map +1 -1
  78. package/package.json +5 -3
@@ -1,25 +1,29 @@
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";
7
8
  // Import commands
8
- import { createToken } from "../commands/create.js";
9
- import { buyToken, TEST_AMOUNTS, PROD_AMOUNTS } from "../commands/buy.js";
9
+ import { createToken, processPhotoUrl, isFilePath, } from "../commands/create.js";
10
+ import { buyToken, TEST_AMOUNTS, PROD_AMOUNTS, } from "../commands/buy.js";
10
11
  import { sellToken, parseTokenAmount } from "../commands/sell.js";
11
12
  import { getTokenInfo } from "../commands/info.js";
12
13
  import { listTokens } from "../commands/list.js";
13
- import { formatCurrency } from "../utils/formatting.js";
14
+ import { formatCurrency, formatTokenAmount, formatAddress, } from "../utils/formatting.js";
14
15
  import { getPositions } from "../commands/positions.js";
15
16
  import { privateKeyToAccount } from "viem/accounts";
16
17
  import { checkHealth } from "../commands/health.js";
17
- import { startChatStream } from "../commands/chat.js";
18
+ import { joinChat, sendChatMessage, renewLease, normalizeWebSocketUrl, } from "../commands/chat.js";
19
+ import WebSocket from "ws";
18
20
  import { checkBalance } from "../commands/balances.js";
19
21
  import { viewFees, claimFees } from "../commands/claim.js";
20
22
  import { getTransactions } from "../commands/transactions.js";
21
- import { getAccountInfo, switchAccount, addAccount } from "../commands/account.js";
23
+ import { getAccountInfo, switchAccount, addAccount, } from "../commands/account.js";
22
24
  import { HttpcatError } from "../client.js";
25
+ import { createHttpcatAgent, chatWithAgent } from "../agent/ax-agent.js";
26
+ import { createLLM } from "../agent/llm-factory.js";
23
27
  // Detect terminal background color
24
28
  function detectTerminalBackground() {
25
29
  // Check COLORFGBG (format: "foreground;background")
@@ -56,15 +60,19 @@ function detectTerminalBackground() {
56
60
  // Default to dark (safer assumption for modern terminals)
57
61
  return "dark";
58
62
  }
59
- export async function startInteractiveShell(client) {
63
+ export async function startInteractiveShell(client, autoChatToken) {
60
64
  // Auto-detect terminal background and set default theme
61
65
  const detectedBg = detectTerminalBackground();
62
66
  let currentTheme = detectedBg === "dark" ? "dark" : "win95";
67
+ // Helper function to get theme-appropriate cyan/blue color for blessed tags
68
+ // For dark theme, use lighter colors (light-cyan-fg) for better visibility on black
69
+ const getCyanColor = (theme) => theme === "dark" ? "light-cyan-fg" : "cyan-fg";
70
+ const getBlueColor = (theme) => theme === "dark" ? "light-blue-fg" : "blue-fg";
63
71
  // Create blessed screen with optimized settings
64
72
  const screen = blessed.screen({
65
73
  smartCSR: true,
66
74
  title: "httpcat Interactive Shell",
67
- fullUnicode: true,
75
+ fullUnicode: true, // Support double-width/surrogate/combining chars (emojis)
68
76
  fastCSR: false, // Disable fast CSR to prevent rendering issues
69
77
  cursor: {
70
78
  artificial: true,
@@ -72,8 +80,8 @@ export async function startInteractiveShell(client) {
72
80
  blink: true,
73
81
  color: "green",
74
82
  },
75
- // Ensure cursor is always visible
76
- forceUnicode: false,
83
+ // Force Unicode support for emojis
84
+ forceUnicode: true,
77
85
  });
78
86
  const network = client.getNetwork();
79
87
  // Theme colors - no backgrounds, just borders
@@ -112,12 +120,12 @@ export async function startInteractiveShell(client) {
112
120
  }
113
121
  };
114
122
  let themeColors = getThemeColors(currentTheme);
115
- // Create header box with thick borders, transparent background
123
+ // Create header box with thick borders, transparent background - more compact
116
124
  const headerBox = blessed.box({
117
125
  top: 0,
118
126
  left: 0,
119
127
  width: "100%",
120
- height: 16,
128
+ height: 6, // Reduced to save vertical space
121
129
  content: "",
122
130
  tags: true,
123
131
  style: {
@@ -130,10 +138,10 @@ export async function startInteractiveShell(client) {
130
138
  },
131
139
  },
132
140
  padding: {
133
- left: 0,
141
+ left: 1,
134
142
  right: 1,
135
- top: 1,
136
- bottom: 1,
143
+ top: 0,
144
+ bottom: 0,
137
145
  },
138
146
  border: {
139
147
  type: "line",
@@ -141,19 +149,29 @@ export async function startInteractiveShell(client) {
141
149
  ch: "═", // Double line for thicker border
142
150
  },
143
151
  });
144
- // Helper function to build welcome content with account info
145
- const buildWelcomeContent = async (theme) => {
152
+ // Cat face variants for animation
153
+ const catFaces = [
154
+ { name: "Sleepy", face: "[=^ -.- ^=]" },
155
+ { name: "Smug", face: "[=^‿^=]" },
156
+ { name: "Unhinged", face: "[=^◉_◉^=]" },
157
+ { name: "Judgy", face: "[=^ಠ‿ಠ^=]" },
158
+ { name: "Cute", face: "[=^。^=]" },
159
+ { name: "Menacing", face: "[=^>_<^=]" },
160
+ { name: "Loaf Mode", face: "[=^___^=]" },
161
+ { name: "Cosmic", face: "[=^✧_✧^=]" },
162
+ ];
163
+ let currentCatIndex = 0;
164
+ let catAnimationInterval = null;
165
+ // Helper function to build welcome content with account info - more compact
166
+ const buildWelcomeContent = async (theme, catFace) => {
146
167
  const welcomeLines = [];
147
168
  const colorTag = theme === "dark" ? "green-fg" : "black-fg";
148
- // Old-school pattern in top bar
149
- const pattern = "═" + "─".repeat(78) + "═";
150
- welcomeLines.push(`{${colorTag}}${pattern}{/${colorTag}}`);
151
- // Single cat ASCII art
152
- welcomeLines.push(`{${colorTag}} /\\_/\\{/${colorTag}}`);
153
- welcomeLines.push(`{${colorTag}} ( ^.^ ){/${colorTag}}`);
154
- welcomeLines.push(`{${colorTag}} > ^ <{/${colorTag}}`);
155
- welcomeLines.push(`{${colorTag}} / \\{/${colorTag}}`);
156
- welcomeLines.push("");
169
+ const cyanColor = getCyanColor(theme);
170
+ // Use provided cat face or current one
171
+ const displayCatFace = catFace || catFaces[currentCatIndex].face;
172
+ // Cat face with breathing room, compact info below
173
+ welcomeLines.push(`{${colorTag}}${displayCatFace}{/${colorTag}}`);
174
+ welcomeLines.push(`{green-fg}🐱 Welcome to httpcat!{/green-fg} | {green-fg}🌐 {${cyanColor}}${network}{/${cyanColor}}{/green-fg}`);
157
175
  // Get account info
158
176
  let accountInfo = null;
159
177
  try {
@@ -179,39 +197,46 @@ export async function startInteractiveShell(client) {
179
197
  catch (error) {
180
198
  // If account info fails, continue without it
181
199
  }
182
- // Playful cat-like greetings with account info
183
- const greetings = [
184
- `{green-fg}Meow! Welcome to httpcat!{/green-fg}`,
185
- `{green-fg}*purrs* Ready to play with some tokens?{/green-fg}`,
186
- `{green-fg}Connected to: {cyan-fg}${network}{/cyan-fg}{/green-fg}`,
187
- ];
188
- // Add account information
200
+ // Compact account info - combined on fewer lines
189
201
  if (accountInfo) {
190
202
  const { account, balance } = accountInfo;
191
203
  const accountType = account.type === "custom" ? "Custom" : "Seed-Derived";
192
204
  const accountLabel = account.label ? ` (${account.label})` : "";
193
- greetings.push("");
194
- greetings.push(`{cyan-fg}Account #{green-fg}${account.index}{/green-fg} | {green-fg}${accountType}${accountLabel}{/green-fg}{/cyan-fg}`);
195
205
  if (balance) {
196
206
  const ethDisplay = balance.ethFormatted || balance.ethBalance || "0 ETH";
197
207
  const usdcDisplay = balance.usdcFormatted || balance.usdcBalance || "$0.00";
198
- greetings.push(`{cyan-fg}Balance: {yellow-fg}${ethDisplay}{/yellow-fg} | {green-fg}${usdcDisplay}{/green-fg}{/cyan-fg}`);
208
+ // Combine account and balance on one line to save space
209
+ 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}}`);
210
+ }
211
+ else {
212
+ welcomeLines.push(`{${cyanColor}}👤 Account #{green-fg}${account.index}{/green-fg} | {green-fg}${accountType}${accountLabel}{/green-fg}{/${cyanColor}}`);
199
213
  }
200
214
  }
201
- greetings.push("");
202
- greetings.push(`{yellow-fg}Pssst... type {bold}help{/bold} if you want to see what I can do!{/yellow-fg}`);
203
- greetings.push(`{yellow-fg}Or just start playing - I'm curious like a cat!{/yellow-fg}`);
204
- welcomeLines.push(...greetings);
205
215
  return welcomeLines.join("\n");
206
216
  };
207
217
  // Set initial header content
208
- buildWelcomeContent(currentTheme).then((content) => {
218
+ buildWelcomeContent(currentTheme, catFaces[currentCatIndex].face).then((content) => {
209
219
  headerBox.setContent(content);
210
220
  screen.render();
211
221
  });
222
+ // Start cat face animation (cycle through every minute)
223
+ const startCatAnimation = () => {
224
+ if (catAnimationInterval) {
225
+ clearInterval(catAnimationInterval);
226
+ }
227
+ catAnimationInterval = setInterval(() => {
228
+ currentCatIndex = (currentCatIndex + 1) % catFaces.length;
229
+ buildWelcomeContent(currentTheme, catFaces[currentCatIndex].face).then((content) => {
230
+ headerBox.setContent(content);
231
+ screen.render();
232
+ });
233
+ }, 60000); // Change every minute (60 seconds)
234
+ };
235
+ // Start animation
236
+ startCatAnimation();
212
237
  // Create output log box (scrollable) with thick borders, transparent background
213
238
  const outputBox = blessed.log({
214
- top: 16,
239
+ top: 6, // Adjusted to match new header height
215
240
  left: 0,
216
241
  width: "100%",
217
242
  bottom: 4, // Leave space for input box at bottom
@@ -269,6 +294,7 @@ export async function startInteractiveShell(client) {
269
294
  height: 3,
270
295
  inputOnFocus: true,
271
296
  keys: true,
297
+ vi: false, // Disabled to prevent double input issues in agent mode
272
298
  secret: false,
273
299
  tags: true,
274
300
  alwaysScroll: false,
@@ -312,9 +338,10 @@ export async function startInteractiveShell(client) {
312
338
  currentTheme = newTheme;
313
339
  themeColors = getThemeColors(currentTheme);
314
340
  // Update screen cursor color
315
- screen.cursor.color = currentTheme === "dark" ? "green" : "black";
316
- // Update header content with new theme colors
317
- buildWelcomeContent(currentTheme).then((content) => {
341
+ screen.cursor.color =
342
+ currentTheme === "dark" ? "green" : "black";
343
+ // Update header content with new theme colors (keep current cat face)
344
+ buildWelcomeContent(currentTheme, catFaces[currentCatIndex].face).then((content) => {
318
345
  headerBox.setContent(content);
319
346
  screen.render();
320
347
  });
@@ -339,9 +366,11 @@ export async function startInteractiveShell(client) {
339
366
  inputBox.style.focus.border.fg = themeColors.border;
340
367
  inputBox.border = { type: "line", fg: themeColors.border, ch: "─" };
341
368
  if (inputBox.cursor) {
342
- inputBox.cursor.color = currentTheme === "dark" ? "green" : "black";
369
+ inputBox.cursor.color =
370
+ currentTheme === "dark" ? "green" : "black";
343
371
  }
344
- screen.cursor.color = currentTheme === "dark" ? "green" : "black";
372
+ screen.cursor.color =
373
+ currentTheme === "dark" ? "green" : "black";
345
374
  screen.render();
346
375
  };
347
376
  // Helper to log output (define before use)
@@ -356,6 +385,42 @@ export async function startInteractiveShell(client) {
356
385
  outputBox.setScrollPerc(100);
357
386
  screen.render();
358
387
  };
388
+ // Helper to log multiple lines with smooth scrolling (for tables/lists)
389
+ const logLinesSmooth = async (lines, scrollToTop = false) => {
390
+ // Disable auto-scroll temporarily to batch output (prevents rapid scrolling)
391
+ const originalAlwaysScroll = outputBox.alwaysScroll;
392
+ outputBox.alwaysScroll = false;
393
+ // Get line count before adding new lines
394
+ const linesBefore = outputBox.lines?.length || 0;
395
+ // Add all lines at once (no rendering between lines = smooth, no rapid scrolling)
396
+ lines.forEach((line) => outputBox.log(line));
397
+ // Calculate scroll position after adding lines
398
+ const linesAfter = outputBox.lines?.length || 0;
399
+ const visibleHeight = outputBox.height || 20;
400
+ if (scrollToTop && linesAfter > visibleHeight) {
401
+ // Scroll to show the beginning of the new content
402
+ // Calculate which line the new content starts at
403
+ const newContentStartLine = linesBefore;
404
+ // Calculate percentage to show that line near the top
405
+ // We want to show the new content starting from the top of visible area
406
+ const totalScrollable = Math.max(1, linesAfter - visibleHeight);
407
+ const scrollPerc = Math.max(0, Math.min(100, (newContentStartLine / totalScrollable) * 100));
408
+ try {
409
+ outputBox.setScrollPerc(scrollPerc);
410
+ }
411
+ catch (e) {
412
+ // Fallback if method doesn't exist
413
+ outputBox.setScrollPerc(0);
414
+ }
415
+ }
416
+ else {
417
+ // Scroll to bottom
418
+ outputBox.setScrollPerc(100);
419
+ }
420
+ // Re-enable auto-scroll
421
+ outputBox.alwaysScroll = originalAlwaysScroll;
422
+ screen.render();
423
+ };
359
424
  // Wrap toggleTheme to also log
360
425
  const toggleThemeWithLog = () => {
361
426
  const themes = ["win95", "dark", "light"];
@@ -376,21 +441,110 @@ export async function startInteractiveShell(client) {
376
441
  // Store toggle function for command handler
377
442
  screen.toggleTheme = toggleThemeWithLog;
378
443
  screen.updateTheme = updateTheme;
444
+ // Command history
445
+ const commandHistory = [];
446
+ let historyIndex = -1; // -1 means not navigating history (showing current input)
447
+ let currentInputBeforeHistory = ""; // Store current input when starting to navigate history
448
+ // Handle up arrow - navigate to previous command in history
449
+ inputBox.key(["up"], () => {
450
+ if (commandHistory.length === 0) {
451
+ return;
452
+ }
453
+ // If we're not currently navigating history, save the current input
454
+ if (historyIndex === -1) {
455
+ currentInputBeforeHistory = inputBox.getValue();
456
+ historyIndex = commandHistory.length - 1; // Start at the most recent command
457
+ }
458
+ else {
459
+ // Move to previous command (earlier in history)
460
+ if (historyIndex > 0) {
461
+ historyIndex--;
462
+ }
463
+ }
464
+ // Set the input to the command at current history index
465
+ inputBox.setValue(commandHistory[historyIndex]);
466
+ screen.render();
467
+ });
468
+ // Handle down arrow - navigate to next command in history (or back to current input)
469
+ inputBox.key(["down"], () => {
470
+ if (commandHistory.length === 0 || historyIndex === -1) {
471
+ return;
472
+ }
473
+ // Move to next command (more recent in history)
474
+ if (historyIndex < commandHistory.length - 1) {
475
+ historyIndex++;
476
+ inputBox.setValue(commandHistory[historyIndex]);
477
+ }
478
+ else {
479
+ // We're at the most recent command, go back to the input that was there before
480
+ historyIndex = -1;
481
+ inputBox.setValue(currentInputBeforeHistory);
482
+ currentInputBeforeHistory = "";
483
+ }
484
+ screen.render();
485
+ });
486
+ // Handle left arrow - move cursor left within input
487
+ // Only intercept if we're not navigating history (blessed handles cursor movement automatically with vi: true)
488
+ inputBox.key(["left"], () => {
489
+ // Reset history navigation when user starts editing with arrow keys
490
+ if (historyIndex !== -1) {
491
+ historyIndex = -1;
492
+ currentInputBeforeHistory = "";
493
+ }
494
+ // Blessed will handle cursor movement automatically with vi: true
495
+ screen.render();
496
+ });
497
+ // Handle right arrow - move cursor right within input
498
+ inputBox.key(["right"], () => {
499
+ // Reset history navigation when user starts editing with arrow keys
500
+ if (historyIndex !== -1) {
501
+ historyIndex = -1;
502
+ currentInputBeforeHistory = "";
503
+ }
504
+ // Blessed will handle cursor movement automatically with vi: true
505
+ screen.render();
506
+ });
507
+ // Handle Home key - move cursor to beginning of line
508
+ inputBox.key(["home"], () => {
509
+ if (historyIndex === -1) {
510
+ // Blessed handles this automatically with vi: true
511
+ screen.render();
512
+ }
513
+ });
514
+ // Handle End key - move cursor to end of line
515
+ inputBox.key(["end"], () => {
516
+ if (historyIndex === -1) {
517
+ // Blessed handles this automatically with vi: true
518
+ screen.render();
519
+ }
520
+ });
379
521
  // Handle input submission
380
522
  inputBox.on("submit", async (value) => {
381
523
  const trimmed = value.trim();
382
524
  inputBox.clearValue();
383
525
  screen.render(); // Clear input immediately
526
+ // Reset history navigation
527
+ historyIndex = -1;
528
+ currentInputBeforeHistory = "";
384
529
  if (!trimmed) {
385
530
  inputBox.focus();
386
531
  screen.render();
387
532
  return;
388
533
  }
534
+ // Add to history (avoid duplicates - don't add if same as last command)
535
+ if (commandHistory.length === 0 ||
536
+ commandHistory[commandHistory.length - 1] !== trimmed) {
537
+ commandHistory.push(trimmed);
538
+ // Limit history size to prevent memory issues (keep last 100 commands)
539
+ if (commandHistory.length > 100) {
540
+ commandHistory.shift();
541
+ }
542
+ }
389
543
  // Log the command with prompt
390
544
  log(`{green-fg}httpcat>{/green-fg} ${trimmed}`);
391
545
  const [command, ...args] = trimmed.split(/\s+/);
392
546
  try {
393
- await handleCommand(client, command.toLowerCase(), args, log, logLines, screen, inputBox, currentTheme, buildWelcomeContent, headerBox);
547
+ await handleCommand(client, command.toLowerCase(), args, log, logLines, logLinesSmooth, screen, inputBox, currentTheme, buildWelcomeContent, headerBox, catFaces, currentCatIndex);
394
548
  }
395
549
  catch (error) {
396
550
  log(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
@@ -399,52 +553,86 @@ export async function startInteractiveShell(client) {
399
553
  inputBox.focus();
400
554
  screen.render();
401
555
  });
402
- // Handle Ctrl+C
403
- screen.key(["C-c"], () => {
404
- screen.destroy();
405
- printCat("sleeping");
406
- console.log(chalk.cyan("Goodbye! 👋"));
407
- process.exit(0);
408
- });
409
556
  // Handle escape to clear input
410
557
  inputBox.key(["escape"], () => {
411
558
  inputBox.clearValue();
412
559
  screen.render();
413
560
  });
414
- // Handle Ctrl+C to quit (also works from input box)
415
- inputBox.key(["C-c"], () => {
561
+ // Handle Ctrl+C - two-stage: first clears input if text exists, second quits
562
+ const handleCtrlC = () => {
563
+ const currentValue = inputBox.getValue();
564
+ // If there's text in the input, clear it instead of quitting
565
+ if (currentValue && currentValue.trim().length > 0) {
566
+ inputBox.clearValue();
567
+ screen.render();
568
+ return;
569
+ }
570
+ // No text in input, so quit
571
+ if (catAnimationInterval) {
572
+ clearInterval(catAnimationInterval);
573
+ catAnimationInterval = null;
574
+ }
416
575
  screen.destroy();
417
576
  printCat("sleeping");
418
577
  console.log(chalk.cyan("Goodbye! 👋"));
419
578
  process.exit(0);
420
- });
579
+ };
580
+ // Handle Ctrl+C on screen level
581
+ screen.key(["C-c"], handleCtrlC);
582
+ // Handle Ctrl+C on input box level
583
+ inputBox.key(["C-c"], handleCtrlC);
421
584
  // Focus input and render
422
585
  inputBox.focus();
423
586
  screen.render();
424
- // Show available commands on load
425
- displayHelp(log, logLines);
587
+ // Show welcome message with key commands on load
588
+ displayWelcomeMessage(log, logLines, outputBox, screen, currentTheme);
589
+ // Auto-enter chat mode if token identifier provided
590
+ if (autoChatToken !== undefined) {
591
+ // Store original submit handler
592
+ const originalHandlers = inputBox.listeners("submit");
593
+ const originalSubmitHandler = originalHandlers[0];
594
+ // Remove original handler temporarily
595
+ inputBox.removeAllListeners("submit");
596
+ // Enter chat mode automatically
597
+ await startChatInShell(client, autoChatToken, log, logLines, screen, inputBox, originalSubmitHandler, headerBox, buildWelcomeContent, currentTheme, catFaces, currentCatIndex);
598
+ }
426
599
  }
427
- async function handleCommand(client, command, args, log, logLines, screen, inputBox, currentTheme, buildWelcomeContent, headerBox) {
600
+ async function handleCommand(client, command, args, log, logLines, logLinesSmooth, screen, inputBox, currentTheme, buildWelcomeContent, headerBox, catFaces, currentCatIndex) {
428
601
  switch (command) {
429
602
  case "help":
430
- displayHelp(log, logLines);
603
+ // Get outputBox from screen children (it's the second child after headerBox)
604
+ const outputBox = screen.children.find((child) => child.type === "log");
605
+ displayHelp(log, logLines, outputBox, screen, currentTheme);
431
606
  break;
432
607
  case "create": {
433
608
  if (args.length < 2) {
434
- log(chalk.red("Usage: create <name> <symbol> [--photo URL] [--banner URL] [--website URL]"));
609
+ log(chalk.red("Usage: create <name> <symbol> [--photo URL|path] [--website URL]"));
435
610
  return;
436
611
  }
437
612
  const [name, symbol] = args;
438
- const photoUrl = extractFlag(args, "--photo");
439
- const bannerUrl = extractFlag(args, "--banner");
613
+ let photoUrl = extractFlag(args, "--photo");
440
614
  const websiteUrl = extractFlag(args, "--website");
615
+ // Process photo if it's a file path (show loading state)
616
+ if (photoUrl && isFilePath(photoUrl)) {
617
+ log(chalk.blue("Uploading image..."));
618
+ screen.render();
619
+ try {
620
+ photoUrl = processPhotoUrl(photoUrl);
621
+ log(chalk.green("✓ Image uploaded"));
622
+ screen.render();
623
+ }
624
+ catch (error) {
625
+ log(chalk.red(`Failed to upload image: ${error instanceof Error ? error.message : String(error)}`));
626
+ screen.render();
627
+ return;
628
+ }
629
+ }
441
630
  log(chalk.blue("Creating token..."));
442
631
  screen.render();
443
632
  const result = await createToken(client, {
444
633
  name,
445
634
  symbol,
446
635
  photoUrl,
447
- bannerUrl,
448
636
  websiteUrl,
449
637
  });
450
638
  displayCreateResultToLog(result, log, logLines);
@@ -458,7 +646,8 @@ async function handleCommand(client, command, args, log, logLines, screen, input
458
646
  return;
459
647
  }
460
648
  const [identifier, amountInput] = args;
461
- const isTestMode = client.getNetwork().includes("sepolia");
649
+ const network = client.getNetwork();
650
+ const isTestMode = network === "eip155:84532" || network === "eip155:11155111" || network.includes("sepolia");
462
651
  const validAmounts = isTestMode ? TEST_AMOUNTS : PROD_AMOUNTS;
463
652
  // Parse flags
464
653
  const repeatCount = extractFlag(args, "--repeat")
@@ -487,10 +676,61 @@ async function handleCommand(client, command, args, log, logLines, screen, input
487
676
  let stopReason = "";
488
677
  for (let i = 1; i <= repeatCount; i++) {
489
678
  try {
490
- log(chalk.blue(`Buy ${i}/${repeatCount}...`));
491
- screen.render();
492
- const result = await buyToken(client, identifier, amount, isTestMode, i > 1, // Silent after first buy
493
- 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
+ }
494
734
  results.push(result);
495
735
  totalSpent += parseFloat(result.amountSpent);
496
736
  // Display compact result
@@ -502,9 +742,63 @@ async function handleCommand(client, command, args, log, logLines, screen, input
502
742
  log(chalk.green("🎓 Token has graduated! Stopping buy loop."));
503
743
  break;
504
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
+ }
505
789
  // Apply delay between iterations (except after the last one)
506
- if (i < repeatCount && delayMs > 0) {
507
- 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
+ }
508
802
  }
509
803
  }
510
804
  catch (error) {
@@ -513,6 +807,38 @@ async function handleCommand(client, command, args, log, logLines, screen, input
513
807
  stoppedEarly = true;
514
808
  stopReason = "Insufficient funds";
515
809
  log(chalk.yellow("💡 Insufficient funds. Stopping buy loop."));
810
+ log("");
811
+ // Show current balance to help diagnose the issue
812
+ try {
813
+ const balance = await checkBalance(privateKey, true);
814
+ log(chalk.cyan("💰 Current Wallet Balances:"));
815
+ log(chalk.dim(` ETH: ${balance.ethFormatted}`));
816
+ log(chalk.dim(` USDC: ${balance.usdcFormatted}`));
817
+ if (balance.cat402Formatted) {
818
+ log(chalk.dim(` CAT: ${balance.cat402Formatted}`));
819
+ }
820
+ log("");
821
+ // Provide guidance based on what might be insufficient
822
+ const usdcBalance = parseFloat(balance.usdcFormatted.replace("$", "").replace(",", ""));
823
+ const ethBalance = parseFloat(balance.ethFormatted.replace(" ETH", "").replace(",", ""));
824
+ const buyAmount = parseFloat(amount);
825
+ log(chalk.yellow("💡 Possible issues:"));
826
+ if (usdcBalance < buyAmount) {
827
+ log(chalk.dim(` • Insufficient USDC for purchase: Need $${buyAmount.toFixed(6)}, have $${usdcBalance.toFixed(6)}`));
828
+ }
829
+ if (usdcBalance < 0.01) {
830
+ log(chalk.dim(` • Low USDC for API payments: x402 protocol requires USDC for API calls`));
831
+ }
832
+ if (ethBalance < 0.001) {
833
+ log(chalk.dim(` • Low ETH for gas: Need ETH to pay for transaction fees`));
834
+ }
835
+ log("");
836
+ log(chalk.dim(" Check your balance with: balances"));
837
+ }
838
+ catch (balanceError) {
839
+ // If we can't fetch balance, just show generic message
840
+ log(chalk.dim(" Check your balance with: balances"));
841
+ }
516
842
  break;
517
843
  }
518
844
  // For other errors, re-throw to be handled by outer catch
@@ -541,7 +867,8 @@ async function handleCommand(client, command, args, log, logLines, screen, input
541
867
  // Normal single buy execution
542
868
  log(chalk.blue("Buying tokens..."));
543
869
  screen.render();
544
- const result = await buyToken(client, identifier, amount, isTestMode, false, privateKey);
870
+ const result = await buyToken(client, identifier, amount, isTestMode, true, // silent=true to avoid console.log interference
871
+ privateKey);
545
872
  displayBuyResultToLog(result, log, logLines);
546
873
  }
547
874
  break;
@@ -553,9 +880,13 @@ async function handleCommand(client, command, args, log, logLines, screen, input
553
880
  return;
554
881
  }
555
882
  const [identifier, amountInput] = args;
883
+ // Get user address from private key for position checking
884
+ const privateKey = config.getPrivateKey();
885
+ const account = privateKeyToAccount(privateKey);
886
+ const userAddress = account.address;
556
887
  log(chalk.blue("Checking token info..."));
557
888
  screen.render();
558
- const info = await getTokenInfo(client, identifier);
889
+ const info = await getTokenInfo(client, identifier, userAddress, true); // silent=true to avoid console.log interference
559
890
  if (!info.userPosition || info.userPosition.tokensOwned === "0") {
560
891
  log(chalk.yellow("You do not own any of this token."));
561
892
  return;
@@ -563,7 +894,7 @@ async function handleCommand(client, command, args, log, logLines, screen, input
563
894
  const tokenAmount = parseTokenAmount(amountInput, info.userPosition.tokensOwned);
564
895
  log(chalk.blue("Selling tokens..."));
565
896
  screen.render();
566
- const result = await sellToken(client, identifier, tokenAmount, false);
897
+ const result = await sellToken(client, identifier, tokenAmount, true, privateKey); // silent=true to avoid console.log interference
567
898
  displaySellResultToLog(result, log, logLines);
568
899
  break;
569
900
  }
@@ -580,7 +911,7 @@ async function handleCommand(client, command, args, log, logLines, screen, input
580
911
  const userAddress = account.address;
581
912
  log(chalk.blue("Fetching token info..."));
582
913
  screen.render();
583
- const info = await getTokenInfo(client, identifier, userAddress);
914
+ const info = await getTokenInfo(client, identifier, userAddress, true); // silent=true to avoid console.log interference
584
915
  displayTokenInfoToLog(info, log, logLines);
585
916
  break;
586
917
  }
@@ -591,7 +922,7 @@ async function handleCommand(client, command, args, log, logLines, screen, input
591
922
  log(chalk.blue("Fetching token list..."));
592
923
  screen.render();
593
924
  const result = await listTokens(client, page, limit, sortBy);
594
- displayTokenListToLog(result, log, logLines);
925
+ displayTokenListToLog(result, log, logLines, logLinesSmooth);
595
926
  break;
596
927
  }
597
928
  case "positions": {
@@ -664,14 +995,17 @@ async function handleCommand(client, command, args, log, logLines, screen, input
664
995
  break;
665
996
  }
666
997
  case "chat": {
667
- // For chat, we need to exit the blessed shell and start chat stream
668
- log(chalk.yellow("Starting chat mode..."));
669
- screen.destroy();
670
998
  const tokenIdentifier = args.length > 0 && !args[0].startsWith("--") ? args[0] : undefined;
671
- const inputFormatRaw = extractFlag(args, "--input-format") || "text";
672
- const inputFormat = (inputFormatRaw === "stream-json" ? "stream-json" : "text");
673
- await startChatStream(client, false, tokenIdentifier, inputFormat);
674
- process.exit(0);
999
+ log(chalk.yellow("💬 Entering chat mode..."));
1000
+ log(chalk.dim("Type /exit to return to shell, or /help for chat commands"));
1001
+ log("");
1002
+ // Start chat mode within the shell
1003
+ // Store original submit handler
1004
+ const originalHandlers = inputBox.listeners("submit");
1005
+ const originalSubmitHandler = originalHandlers[0];
1006
+ // Remove original handler temporarily
1007
+ inputBox.removeAllListeners("submit");
1008
+ await startChatInShell(client, tokenIdentifier, log, logLines, screen, inputBox, originalSubmitHandler, headerBox, buildWelcomeContent, currentTheme, catFaces, currentCatIndex);
675
1009
  break;
676
1010
  }
677
1011
  case "exit":
@@ -713,13 +1047,13 @@ async function handleCommand(client, command, args, log, logLines, screen, input
713
1047
  const address = callerAddress || account.address;
714
1048
  log(chalk.blue("Claiming fees..."));
715
1049
  screen.render();
716
- const result = await claimFees(client, identifier, address, false);
1050
+ const result = await claimFees(client, identifier, address, true); // silent=true to avoid console.log interference
717
1051
  displayClaimResultToLog(result, log, logLines);
718
1052
  }
719
1053
  else {
720
1054
  log(chalk.blue("Fetching fee information..."));
721
1055
  screen.render();
722
- const result = await viewFees(client, identifier, false);
1056
+ const result = await viewFees(client, identifier, true); // silent=true to avoid console.log interference
723
1057
  displayFeesToLog(result, log, logLines);
724
1058
  }
725
1059
  break;
@@ -786,7 +1120,9 @@ async function handleCommand(client, command, args, log, logLines, screen, input
786
1120
  }
787
1121
  for (const account of accounts) {
788
1122
  const isActive = account.index === activeIndex;
789
- const status = isActive ? chalk.green("● Active") : chalk.blue("○ Inactive");
1123
+ const status = isActive
1124
+ ? chalk.green("● Active")
1125
+ : chalk.blue("○ Inactive");
790
1126
  const type = account.type === "custom" ? "Custom" : "Seed-Derived";
791
1127
  const address = account.address.slice(0, 6) + "..." + account.address.slice(-4);
792
1128
  log(` ${chalk.green(account.index.toString().padEnd(5))} ${chalk.blue(type.padEnd(12))} ${chalk.green(address.padEnd(15))} ${status}`);
@@ -812,7 +1148,7 @@ async function handleCommand(client, command, args, log, logLines, screen, input
812
1148
  log(chalk.blue(` Address: ${account.address.slice(0, 6)}...${account.address.slice(-4)}`));
813
1149
  }
814
1150
  // Refresh header with new account info
815
- buildWelcomeContent(currentTheme).then((content) => {
1151
+ buildWelcomeContent(currentTheme, catFaces[currentCatIndex].face).then((content) => {
816
1152
  headerBox.setContent(content);
817
1153
  screen.render();
818
1154
  });
@@ -823,7 +1159,7 @@ async function handleCommand(client, command, args, log, logLines, screen, input
823
1159
  await addAccount();
824
1160
  log(chalk.green("✅ Account added"));
825
1161
  // Refresh header with new account info
826
- buildWelcomeContent(currentTheme).then((content) => {
1162
+ buildWelcomeContent(currentTheme, catFaces[currentCatIndex].face).then((content) => {
827
1163
  headerBox.setContent(content);
828
1164
  screen.render();
829
1165
  });
@@ -857,7 +1193,9 @@ async function handleCommand(client, command, args, log, logLines, screen, input
857
1193
  for (const [name, env] of Object.entries(envs)) {
858
1194
  const isCurrent = name === current;
859
1195
  const prefix = isCurrent ? chalk.green("→ ") : " ";
860
- const nameDisplay = isCurrent ? chalk.green.bold(name) : chalk.bold(name);
1196
+ const nameDisplay = isCurrent
1197
+ ? chalk.green.bold(name)
1198
+ : chalk.bold(name);
861
1199
  log(`${prefix}${nameDisplay}`);
862
1200
  log(chalk.dim(` Agent URL: ${env.agentUrl}`));
863
1201
  log(chalk.dim(` Network: ${env.network}`));
@@ -885,7 +1223,7 @@ async function handleCommand(client, command, args, log, logLines, screen, input
885
1223
  log(chalk.red("Usage: env add <name> <agentUrl> [--network <network>]"));
886
1224
  return;
887
1225
  }
888
- const network = extractFlag(args, "--network") || "base-sepolia";
1226
+ const network = extractFlag(args, "--network") || "eip155:84532";
889
1227
  config.addEnvironment(args[1], args[2], network);
890
1228
  log(chalk.green(`✅ Added environment: ${args[1]}`));
891
1229
  log(chalk.dim(` Agent URL: ${args[2]}`));
@@ -896,7 +1234,7 @@ async function handleCommand(client, command, args, log, logLines, screen, input
896
1234
  log(chalk.red("Usage: env update <name> <agentUrl> [--network <network>]"));
897
1235
  return;
898
1236
  }
899
- const network = extractFlag(args, "--network") || "base-sepolia";
1237
+ const network = extractFlag(args, "--network") || "eip155:84532";
900
1238
  config.updateEnvironment(args[1], args[2], network);
901
1239
  log(chalk.green(`✅ Updated environment: ${args[1]}`));
902
1240
  log(chalk.dim(` Agent URL: ${args[2]}`));
@@ -907,35 +1245,215 @@ async function handleCommand(client, command, args, log, logLines, screen, input
907
1245
  }
908
1246
  break;
909
1247
  }
1248
+ case "agent":
1249
+ case "ai":
1250
+ case "cat": {
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("");
1255
+ break;
1256
+ }
910
1257
  default:
911
1258
  log(chalk.red(`Unknown command: ${command}`));
912
1259
  log(chalk.dim('Type "help" for available commands'));
913
1260
  }
914
1261
  }
915
- function displayHelp(log, logLines) {
916
- log("");
917
- log(chalk.green.bold("Available Commands:"));
918
- log("");
919
- log(chalk.green(" create <name> <symbol>"));
920
- log(chalk.green(" buy <id> <amount> [--repeat N] [--delay MS]"));
921
- log(chalk.green(" sell <id> <amount|all>"));
922
- log(chalk.green(" info <id>"));
923
- log(chalk.green(" list [--sort mcap|created|name] [--page N] [--limit N]"));
924
- log(chalk.green(" positions [--active|--graduated]"));
925
- log(chalk.green(" balances"));
926
- log(chalk.green(" claim <id> [--execute] [--address ADDR]"));
927
- log(chalk.green(" transactions [--user ADDR] [--token ID] [--type TYPE] [--limit N] [--offset N]"));
928
- log(chalk.green(" account [list|switch <index>|add]"));
929
- log(chalk.green(" env [list|use <name>|show|add|update]"));
930
- log(chalk.green(" chat [token] [--input-format FORMAT]"));
931
- log(chalk.green(" health"));
932
- log(chalk.green(" config [--show|--set|--reset]"));
933
- log(chalk.green(" network"));
934
- log(chalk.green(" clear"));
935
- log(chalk.green(" help"));
936
- log(chalk.green(" exit"));
937
- log("");
938
- log(chalk.blue("Tip: Run any command without arguments to see detailed help"));
1262
+ function displayWelcomeMessage(log, logLines, outputBox, screen, theme = "dark") {
1263
+ // Helper to get theme-appropriate colors
1264
+ const getCyanColor = (t) => t === "dark" ? "light-cyan-fg" : "cyan-fg";
1265
+ const getBlueColor = (t) => t === "dark" ? "light-blue-fg" : "blue-fg";
1266
+ const cyanColor = getCyanColor(theme);
1267
+ const blueColor = getBlueColor(theme);
1268
+ // If we have outputBox, use blessed tags instead of chalk to avoid question marks
1269
+ // Otherwise fall back to the log function with chalk
1270
+ if (outputBox && screen) {
1271
+ const welcomeLines = [
1272
+ "",
1273
+ `{bold}{${cyanColor}}🐱 Welcome to httpcat!{/${cyanColor}}{/bold}`,
1274
+ "",
1275
+ "{yellow-fg}✨ Here are some commands to get you started:{/yellow-fg}",
1276
+ "",
1277
+ "{green-fg} 📋 list{/green-fg} - Browse all available tokens",
1278
+ "{green-fg} ℹ️ info <token>{/green-fg} - Get detailed info about a token",
1279
+ "{green-fg} 💰 buy <token> <amount>{/green-fg} - Buy tokens (e.g., buy MTK 0.10)",
1280
+ "{green-fg} 💼 positions{/green-fg} - View your portfolio",
1281
+ "{green-fg} 💵 balances{/green-fg} - Check your wallet balance",
1282
+ "",
1283
+ `{${blueColor}}💡 Type {bold}help{/bold} to see all available commands!{/${blueColor}}`,
1284
+ "",
1285
+ ];
1286
+ // Log all welcome lines at once
1287
+ welcomeLines.forEach((line) => outputBox.log(line));
1288
+ // Scroll to top so welcome message is visible
1289
+ outputBox.setScrollPerc(0);
1290
+ screen.render();
1291
+ }
1292
+ else {
1293
+ // Fallback to regular log function if outputBox not available
1294
+ log("");
1295
+ log(chalk.cyan.bold("🐱 Welcome to httpcat!"));
1296
+ log("");
1297
+ log(chalk.yellow("✨ Here are some commands to get you started:"));
1298
+ log("");
1299
+ log(chalk.green(" 📋 list") +
1300
+ chalk.dim(" - Browse all available tokens"));
1301
+ log(chalk.green(" ℹ️ info <token>") +
1302
+ chalk.dim(" - Get detailed info about a token"));
1303
+ log(chalk.green(" 💰 buy <token> <amount>") +
1304
+ chalk.dim(" - Buy tokens (e.g., buy MTK 0.10)"));
1305
+ log(chalk.green(" 💼 positions") +
1306
+ chalk.dim(" - View your portfolio"));
1307
+ log(chalk.green(" 💵 balances") +
1308
+ chalk.dim(" - Check your wallet balance"));
1309
+ log("");
1310
+ log(chalk.blue("💡 Type ") +
1311
+ chalk.bold("help") +
1312
+ chalk.blue(" to see all available commands!"));
1313
+ log("");
1314
+ }
1315
+ }
1316
+ function displayHelp(log, logLines, outputBox, screen, theme = "dark") {
1317
+ // Helper to get theme-appropriate colors
1318
+ const getCyanColor = (t) => t === "dark" ? "light-cyan-fg" : "cyan-fg";
1319
+ const getBlueColor = (t) => t === "dark" ? "light-blue-fg" : "blue-fg";
1320
+ const cyanColor = getCyanColor(theme);
1321
+ const blueColor = getBlueColor(theme);
1322
+ // If we have outputBox, use blessed tags instead of chalk to avoid question marks
1323
+ // Otherwise fall back to the log function
1324
+ if (outputBox && screen) {
1325
+ // Get the line number where help will start (before logging)
1326
+ const linesBeforeHelp = outputBox.lines?.length || 0;
1327
+ // Collect all help lines using blessed tags
1328
+ const helpLines = [
1329
+ "",
1330
+ `{${cyanColor}}{bold}Available Commands:{/bold}{/${cyanColor}}`,
1331
+ "",
1332
+ // Token Operations
1333
+ `{${cyanColor}}{bold}Token Operations:{/bold}{/${cyanColor}}`,
1334
+ "{green-fg} create <name> <symbol>{/green-fg}",
1335
+ "{green-fg} buy <id> <amount> [--repeat N] [--delay MS]{/green-fg}",
1336
+ "{green-fg} sell <id> <amount|all>{/green-fg}",
1337
+ "{green-fg} claim <id> [--execute] [--address ADDR]{/green-fg}",
1338
+ "",
1339
+ // Token Information
1340
+ `{${cyanColor}}{bold}Token Information:{/bold}{/${cyanColor}}`,
1341
+ "{green-fg} info <id>{/green-fg}",
1342
+ "{green-fg} list [--sort mcap|created|name] [--page N] [--limit N]{/green-fg}",
1343
+ "",
1344
+ // Portfolio
1345
+ `{${cyanColor}}{bold}Portfolio:{/bold}{/${cyanColor}}`,
1346
+ "{green-fg} positions [--active|--graduated]{/green-fg}",
1347
+ "{green-fg} transactions [--user ADDR] [--token ID] [--type TYPE] [--limit N] [--offset N]{/green-fg}",
1348
+ "{green-fg} balances{/green-fg}",
1349
+ "",
1350
+ // Account Management
1351
+ `{${cyanColor}}{bold}Account Management:{/bold}{/${cyanColor}}`,
1352
+ "{green-fg} account [list|switch <index>|add]{/green-fg}",
1353
+ "{green-fg} env [list|use <name>|show|add|update]{/green-fg}",
1354
+ "",
1355
+ // Social
1356
+ `{${cyanColor}}{bold}Social:{/bold}{/${cyanColor}}`,
1357
+ "{green-fg} chat [token] [--input-format FORMAT]{/green-fg}",
1358
+ "",
1359
+ // Cat (agent and cat are synonyms)
1360
+ `{${cyanColor}}{bold}Cat (or 'agent'):{/bold}{/${cyanColor}}`,
1361
+ "{green-fg} agent <query>{/green-fg} - Ask the cat to do something (or use 'cat')",
1362
+ "{green-fg} agent --chat{/green-fg} - Enter interactive chat mode (or 'cat --chat')",
1363
+ "{green-fg} agent --setup{/green-fg} - Configure API key/provider (or 'cat --setup')",
1364
+ "",
1365
+ // System
1366
+ `{${cyanColor}}{bold}System:{/bold}{/${cyanColor}}`,
1367
+ "{green-fg} health{/green-fg}",
1368
+ "{green-fg} config [--show|--set|--reset]{/green-fg}",
1369
+ "{green-fg} network{/green-fg}",
1370
+ "",
1371
+ // Shell Commands
1372
+ `{${cyanColor}}{bold}Shell Commands:{/bold}{/${cyanColor}}`,
1373
+ "{green-fg} clear{/green-fg}",
1374
+ "{green-fg} help{/green-fg}",
1375
+ "{green-fg} exit{/green-fg}",
1376
+ "",
1377
+ `{${blueColor}}Tip: Run any command without arguments to see detailed help{/${blueColor}}`,
1378
+ ];
1379
+ // Log all help lines at once
1380
+ helpLines.forEach((line) => outputBox.log(line));
1381
+ // Scroll to show "Available Commands" at the top of the visible area
1382
+ // Get total lines after logging
1383
+ const totalLines = outputBox.lines?.length || 0;
1384
+ const visibleHeight = outputBox.height || 20;
1385
+ if (totalLines > visibleHeight && linesBeforeHelp >= 0) {
1386
+ // Calculate which line "Available Commands" is on (after the empty line)
1387
+ const availableCommandsLine = linesBeforeHelp + 1;
1388
+ // Calculate scroll percentage to show that line at the top
1389
+ // Percentage = (line number / total lines) * 100
1390
+ // But we want the line to be at the top, so we need to account for visible height
1391
+ const scrollPerc = Math.max(0, Math.min(100, ((availableCommandsLine - 1) /
1392
+ Math.max(1, totalLines - visibleHeight)) *
1393
+ 100));
1394
+ outputBox.setScrollPerc(scrollPerc);
1395
+ }
1396
+ else {
1397
+ // If all content fits in visible area or no previous content, scroll to top
1398
+ outputBox.setScrollPerc(0);
1399
+ }
1400
+ screen.render();
1401
+ }
1402
+ else {
1403
+ // Fallback to regular log function if outputBox not available
1404
+ log("");
1405
+ log(chalk.cyan.bold("Available Commands:"));
1406
+ log("");
1407
+ // Token Operations
1408
+ log(chalk.bold(chalk.cyan("Token Operations:")));
1409
+ log(chalk.green(" create <name> <symbol>"));
1410
+ log(chalk.green(" buy <id> <amount> [--repeat N] [--delay MS]"));
1411
+ log(chalk.green(" sell <id> <amount|all>"));
1412
+ log(chalk.green(" claim <id> [--execute] [--address ADDR]"));
1413
+ log("");
1414
+ // Token Information
1415
+ log(chalk.bold(chalk.cyan("Token Information:")));
1416
+ log(chalk.green(" info <id>"));
1417
+ log(chalk.green(" list [--sort mcap|created|name] [--page N] [--limit N]"));
1418
+ log("");
1419
+ // Portfolio
1420
+ log(chalk.bold(chalk.cyan("Portfolio:")));
1421
+ log(chalk.green(" positions [--active|--graduated]"));
1422
+ log(chalk.green(" transactions [--user ADDR] [--token ID] [--type TYPE] [--limit N] [--offset N]"));
1423
+ log(chalk.green(" balances"));
1424
+ log("");
1425
+ // Account Management
1426
+ log(chalk.bold(chalk.cyan("Account Management:")));
1427
+ log(chalk.green(" account [list|switch <index>|add]"));
1428
+ log(chalk.green(" env [list|use <name>|show|add|update]"));
1429
+ log("");
1430
+ // Social
1431
+ log(chalk.bold(chalk.cyan("Social:")));
1432
+ log(chalk.green(" chat [token] [--input-format FORMAT]"));
1433
+ log("");
1434
+ // Cat (agent and cat are synonyms)
1435
+ log(chalk.bold(chalk.cyan("Cat (or 'agent'):")));
1436
+ log(chalk.green(" agent <query>") +
1437
+ chalk.dim(" - Ask the cat to do something (or use 'cat')"));
1438
+ log(chalk.green(" agent --chat") +
1439
+ chalk.dim(" - Enter interactive chat mode (or 'cat --chat')"));
1440
+ log(chalk.green(" agent --setup") +
1441
+ chalk.dim(" - Configure API key/provider (or 'cat --setup')"));
1442
+ log("");
1443
+ // System
1444
+ log(chalk.bold(chalk.cyan("System:")));
1445
+ log(chalk.green(" health"));
1446
+ log(chalk.green(" config [--show|--set|--reset]"));
1447
+ log(chalk.green(" network"));
1448
+ log("");
1449
+ // Shell Commands
1450
+ log(chalk.bold(chalk.cyan("Shell Commands:")));
1451
+ log(chalk.green(" clear"));
1452
+ log(chalk.green(" help"));
1453
+ log(chalk.green(" exit"));
1454
+ log("");
1455
+ log(chalk.blue("Tip: Run any command without arguments to see detailed help"));
1456
+ }
939
1457
  }
940
1458
  function displayConfigToLog(log, logLines) {
941
1459
  const cfg = config.getAll();
@@ -972,43 +1490,141 @@ function displayBuyResultToLog(result, log, logLines) {
972
1490
  }
973
1491
  function displaySellResultToLog(result, log, logLines) {
974
1492
  log(chalk.green("Sale successful!"));
975
- log(chalk.blue("Tokens sold: ") + chalk.green(result.tokensSold));
976
- log(chalk.blue("USDC received: ") + chalk.yellow(result.usdcReceived));
977
- log(chalk.blue("New price: ") + chalk.green(result.newPrice));
978
- log(chalk.blue("Graduation: ") +
979
- chalk.magenta(result.graduationProgress.toFixed(2) + "%"));
1493
+ log(chalk.blue("Tokens sold: ") +
1494
+ chalk.green(formatTokenAmount(result.tokensSold || "0")));
1495
+ log(chalk.blue("USDC received: ") +
1496
+ chalk.yellow(formatCurrency(result.usdcReceived || "0", 6)));
1497
+ if (result.fee) {
1498
+ log(chalk.blue("Fee (1%): ") + chalk.yellow(formatCurrency(result.fee, 6)));
1499
+ }
1500
+ if (result.newPrice) {
1501
+ log(chalk.blue("New price: ") + chalk.green(formatCurrency(result.newPrice)));
1502
+ }
1503
+ if (result.newMcap) {
1504
+ log(chalk.blue("New market cap: ") +
1505
+ chalk.yellow(formatCurrency(result.newMcap)));
1506
+ }
1507
+ if (result.graduationProgress !== undefined) {
1508
+ log(chalk.blue("Graduation: ") +
1509
+ chalk.magenta(result.graduationProgress.toFixed(2) + "%"));
1510
+ }
1511
+ if (result.txHash) {
1512
+ log(chalk.blue("Transaction: ") + chalk.green(result.txHash));
1513
+ }
1514
+ else if (result.usdcTransferTx) {
1515
+ log(chalk.blue("Transaction: ") + chalk.green(result.usdcTransferTx));
1516
+ }
1517
+ // Graduation progress bar
1518
+ if (result.graduationProgress !== undefined) {
1519
+ const barLength = 20;
1520
+ const filled = Math.floor((result.graduationProgress / 100) * barLength);
1521
+ const empty = barLength - filled;
1522
+ const bar = "🐱" + "=".repeat(filled) + ">" + " ".repeat(empty);
1523
+ log(chalk.yellow(`[${bar}] ${chalk.bold(result.graduationProgress.toFixed(2) + "%")} to moon!`));
1524
+ }
1525
+ // Display price impact warning for Uniswap swaps
1526
+ if (result.source === "uniswap_v2" && result.priceImpact !== undefined) {
1527
+ if (result.priceImpact < 0.5) {
1528
+ log(chalk.green(`Price Impact: ${result.priceImpact.toFixed(2)}%`));
1529
+ }
1530
+ else if (result.priceImpact < 1) {
1531
+ log(chalk.yellow(`Price Impact: ${result.priceImpact.toFixed(2)}%`));
1532
+ }
1533
+ else if (result.priceImpact < 3) {
1534
+ log(chalk.red(`⚠️ HIGH Price Impact: ${result.priceImpact.toFixed(2)}%`));
1535
+ }
1536
+ else {
1537
+ log(chalk.red(`🚨 VERY HIGH Price Impact: ${result.priceImpact.toFixed(2)}%`));
1538
+ }
1539
+ }
1540
+ // Display route info for Uniswap swaps
1541
+ if (result.source === "uniswap_v2" && result.route) {
1542
+ const feePercent = result.routeFee
1543
+ ? (result.routeFee / 10000).toFixed(2)
1544
+ : "N/A";
1545
+ log(chalk.cyan(`Route: ${result.route} (${feePercent}% fee)`));
1546
+ }
980
1547
  }
981
1548
  function displayTokenInfoToLog(info, log, logLines) {
982
- log(chalk.green.bold(info.name) + ` (${info.symbol})`);
983
- if (info.tokenAddress) {
984
- log(chalk.blue("Address: ") + chalk.green(info.tokenAddress));
985
- }
986
- log(chalk.blue("Price: ") + chalk.green(info.currentPrice));
987
- log(chalk.blue("Market Cap: ") + chalk.yellow(info.marketCap));
988
- log(chalk.blue("Supply: ") + chalk.green(info.totalSupply));
1549
+ log("");
1550
+ // Token header box
1551
+ const priceDisplay = info.status === "graduated"
1552
+ ? chalk.dim("--")
1553
+ : info.price
1554
+ ? formatCurrency(info.price)
1555
+ : chalk.dim("N/A");
1556
+ const mcapDisplay = info.status === "graduated"
1557
+ ? chalk.dim("--")
1558
+ : info.mcap
1559
+ ? formatCurrency(info.mcap)
1560
+ : chalk.dim("N/A");
1561
+ // Create box-like display
1562
+ log(chalk.green.bold(`📊 ${info.name} (${info.symbol})`));
1563
+ log(chalk.dim("─".repeat(60)));
1564
+ log(chalk.blue("Address: ") + chalk.green(formatAddress(info.address || "")));
1565
+ log(chalk.blue("Status: ") + chalk.green(info.status || "unknown"));
1566
+ log(chalk.blue("Price: ") + chalk.green(priceDisplay));
1567
+ log(chalk.blue("Market Cap: ") + chalk.yellow(mcapDisplay));
1568
+ log(chalk.blue("Total Supply: ") +
1569
+ chalk.green(formatTokenAmount(info.totalSupply || "0")));
1570
+ log(chalk.blue("Created: ") +
1571
+ chalk.green(new Date(info.createdAt || Date.now()).toLocaleString()));
1572
+ log("");
1573
+ // Graduation progress
989
1574
  if (info.graduationProgress !== undefined) {
990
- log(chalk.blue("Graduation: ") +
991
- chalk.magenta(info.graduationProgress.toFixed(2) + "%"));
1575
+ const barLength = 20;
1576
+ const filled = Math.floor((info.graduationProgress / 100) * barLength);
1577
+ const empty = barLength - filled;
1578
+ const bar = "🐱" + "=".repeat(filled) + ">" + " ".repeat(empty);
1579
+ log(chalk.yellow(`[${bar}] ${chalk.bold(info.graduationProgress.toFixed(2) + "%")} to moon!`));
1580
+ log("");
992
1581
  }
1582
+ // Position display
993
1583
  if (info.userPosition && info.userPosition.tokensOwned !== "0") {
994
- log(chalk.blue("Your position: ") +
995
- chalk.green(info.userPosition.tokensOwned + " tokens"));
1584
+ log(chalk.green.bold("💼 Your Position"));
1585
+ log(chalk.dim("".repeat(60)));
1586
+ log(chalk.blue("Tokens Owned: ") +
1587
+ chalk.green(formatTokenAmount(info.userPosition.tokensOwned)));
1588
+ log(chalk.blue("Invested: ") +
1589
+ chalk.green(formatCurrency(info.userPosition.usdcInvested || "0")));
1590
+ if (info.userPosition.currentValue) {
1591
+ log(chalk.blue("Current Value: ") +
1592
+ chalk.green(formatCurrency(info.userPosition.currentValue)));
1593
+ }
1594
+ if (info.userPosition.pnl) {
1595
+ const pnl = parseFloat(info.userPosition.pnl);
1596
+ const pnlColor = pnl >= 0 ? chalk.green : chalk.red;
1597
+ const pnlSign = pnl >= 0 ? "+" : "";
1598
+ const pnlIcon = pnl >= 0 ? "🟢" : "🔴";
1599
+ log(chalk.blue("P&L: ") +
1600
+ `${pnlIcon} ${pnlColor(pnlSign + formatCurrency(info.userPosition.pnl))}`);
1601
+ }
1602
+ log("");
996
1603
  }
997
1604
  }
998
- function displayTokenListToLog(result, log, logLines) {
999
- log("");
1000
- log(chalk.green.bold(`Tokens (${result.total || result.tokens.length} total):`));
1605
+ function displayTokenListToLog(result, log, logLines, logLinesSmooth) {
1606
+ // Collect all lines first for smooth scrolling
1607
+ const allLines = [];
1608
+ allLines.push("");
1609
+ allLines.push(chalk.green.bold(`Tokens (${result.total || result.tokens.length} total):`));
1001
1610
  if (result.page && result.pages) {
1002
- log(chalk.dim(`Page ${result.page} of ${result.pages}`));
1611
+ allLines.push(chalk.dim(`Page ${result.page} of ${result.pages}`));
1003
1612
  }
1004
- log("");
1613
+ allLines.push("");
1005
1614
  if (result.tokens.length === 0) {
1006
- log(chalk.yellow("No tokens found."));
1615
+ allLines.push(chalk.yellow("No tokens found."));
1616
+ // Use smooth scrolling if available, otherwise regular logLines
1617
+ if (logLinesSmooth) {
1618
+ logLinesSmooth(allLines, false);
1619
+ }
1620
+ else {
1621
+ logLines(allLines);
1622
+ }
1007
1623
  return;
1008
1624
  }
1009
1625
  // Show header
1010
- log(` ${chalk.cyan.bold("Name".padEnd(20))} ${chalk.cyan.bold("Symbol".padEnd(10))} ${chalk.cyan.bold("Market Cap".padEnd(15))} ${chalk.cyan.bold("Price".padEnd(12))} ${chalk.cyan.bold("Graduation".padEnd(12))} ${chalk.cyan.bold("Status")}`);
1011
- log(chalk.dim(" " + "─".repeat(90)));
1626
+ allLines.push(` ${chalk.cyan.bold("Name".padEnd(20))} ${chalk.cyan.bold("Symbol".padEnd(10))} ${chalk.cyan.bold("Market Cap".padEnd(15))} ${chalk.cyan.bold("Price".padEnd(12))} ${chalk.cyan.bold("Graduation".padEnd(12))} ${chalk.cyan.bold("Status")}`);
1627
+ allLines.push(chalk.dim(" " + "─".repeat(90)));
1012
1628
  // Show tokens
1013
1629
  for (const token of result.tokens) {
1014
1630
  const graduationIcon = token.status === "graduated"
@@ -1028,21 +1644,28 @@ function displayTokenListToLog(result, log, logLines) {
1028
1644
  const statusDisplay = token.status === "graduated"
1029
1645
  ? chalk.green("✓ Graduated")
1030
1646
  : chalk.yellow("Active");
1031
- log(` ${chalk.bold(token.name.padEnd(20))} ${chalk.green(token.symbol.padEnd(10))} ${mcapDisplay.padEnd(15)} ${priceDisplay.padEnd(12)} ${graduationDisplay.padEnd(12)} ${statusDisplay}`);
1647
+ allLines.push(` ${chalk.bold(token.name.padEnd(20))} ${chalk.green(token.symbol.padEnd(10))} ${mcapDisplay.padEnd(15)} ${priceDisplay.padEnd(12)} ${graduationDisplay.padEnd(12)} ${statusDisplay}`);
1032
1648
  }
1033
- log("");
1649
+ allLines.push("");
1034
1650
  if (result.page && result.pages && result.page < result.pages) {
1035
- log(chalk.dim(`Use --page ${result.page + 1} to see more tokens`));
1036
- log("");
1651
+ allLines.push(chalk.dim(`Use --page ${result.page + 1} to see more tokens`));
1652
+ allLines.push("");
1037
1653
  }
1038
1654
  // Show example commands
1039
1655
  if (result.tokens.length > 0) {
1040
1656
  const firstToken = result.tokens[0];
1041
- log(chalk.dim("Example commands:"));
1042
- log(chalk.dim(` buy ${firstToken.symbol}`));
1043
- log(chalk.dim(` sell ${firstToken.symbol}`));
1044
- log(chalk.dim(` info ${firstToken.symbol}`));
1045
- log("");
1657
+ allLines.push(chalk.dim("Example commands:"));
1658
+ allLines.push(chalk.dim(` buy ${firstToken.symbol}`));
1659
+ allLines.push(chalk.dim(` sell ${firstToken.symbol}`));
1660
+ allLines.push(chalk.dim(` info ${firstToken.symbol}`));
1661
+ allLines.push("");
1662
+ }
1663
+ // Use smooth scrolling if available (scrolls to show the list header), otherwise regular logLines
1664
+ if (logLinesSmooth) {
1665
+ logLinesSmooth(allLines, true); // scrollToTop=true to show the list header
1666
+ }
1667
+ else {
1668
+ logLines(allLines);
1046
1669
  }
1047
1670
  }
1048
1671
  function displayPositionsToLog(result, log, logLines) {
@@ -1053,11 +1676,19 @@ function displayPositionsToLog(result, log, logLines) {
1053
1676
  log(chalk.green.bold(`Your Positions (${result.positions.length}):`));
1054
1677
  log("");
1055
1678
  for (const pos of result.positions) {
1056
- log(` ${chalk.green(pos.symbol || "???")} - ${chalk.green(pos.tokensOwned + " tokens")} @ ${chalk.yellow(pos.currentValue || "N/A")}`);
1679
+ const tokenName = pos.token?.name || "???";
1680
+ const tokenSymbol = pos.token?.symbol || "???";
1681
+ const tokensDisplay = formatTokenAmount(pos.tokensOwned || "0");
1682
+ const valueDisplay = pos.currentValue
1683
+ ? formatCurrency(pos.currentValue)
1684
+ : "N/A";
1685
+ log(` ${chalk.green.bold(`${tokenName} (${tokenSymbol})`)} - ${chalk.green(`${tokensDisplay} tokens`)} @ ${chalk.yellow(valueDisplay)}`);
1057
1686
  }
1058
1687
  }
1059
1688
  function displayHealthStatusToLog(health, log, logLines) {
1060
- const statusColor = health.status === "ok" || health.status === "healthy" ? chalk.green : chalk.red;
1689
+ const statusColor = health.status === "ok" || health.status === "healthy"
1690
+ ? chalk.green
1691
+ : chalk.red;
1061
1692
  log(statusColor(`Agent Status: ${health.status}`));
1062
1693
  if (health.version) {
1063
1694
  log(chalk.blue("Version: ") + chalk.green(health.version));
@@ -1070,15 +1701,19 @@ function displayBalanceToLog(balance, log, logLines) {
1070
1701
  log("");
1071
1702
  log(chalk.green.bold("Wallet Balance:"));
1072
1703
  log(chalk.blue("Address: ") + chalk.green(balance.address));
1073
- log(chalk.blue("ETH: ") + chalk.yellow(balance.ethFormatted || balance.ethBalance));
1074
- log(chalk.blue("USDC: ") + chalk.green(balance.usdcFormatted || balance.usdcBalance));
1704
+ log(chalk.blue("ETH: ") +
1705
+ chalk.yellow(balance.ethFormatted || balance.ethBalance));
1706
+ log(chalk.blue("USDC: ") +
1707
+ chalk.green(balance.usdcFormatted || balance.usdcBalance));
1075
1708
  if (balance.cat402Formatted) {
1076
1709
  log(chalk.blue("CAT: ") + chalk.cyan(balance.cat402Formatted));
1077
1710
  }
1078
1711
  log("");
1079
1712
  // Show warnings if balances are low
1080
1713
  const ethNum = Number(balance.ethFormatted || balance.ethBalance);
1081
- const usdcNum = Number((balance.usdcFormatted || balance.usdcBalance).replace("$", "").replace(",", ""));
1714
+ const usdcNum = Number((balance.usdcFormatted || balance.usdcBalance)
1715
+ .replace("$", "")
1716
+ .replace(",", ""));
1082
1717
  if (ethNum < 0.001 && usdcNum < 1) {
1083
1718
  log(chalk.yellow("⚠️ Low balances detected!"));
1084
1719
  log(chalk.dim(" You need Base Sepolia ETH for gas fees"));
@@ -1109,7 +1744,8 @@ function displayFeesToLog(fees, log, logLines) {
1109
1744
  log(chalk.green.bold(`💰 Accumulated Fees: ${fees.tokenName} (${fees.tokenSymbol})`));
1110
1745
  log(chalk.blue("━".repeat(50)));
1111
1746
  log(chalk.blue("Contract: ") + chalk.green(fees.tokenAddress));
1112
- log(chalk.blue("LP Status: ") + chalk.green(fees.isLocked ? "🔒 Locked" : "❌ Not Locked"));
1747
+ log(chalk.blue("LP Status: ") +
1748
+ chalk.green(fees.isLocked ? "🔒 Locked" : "❌ Not Locked"));
1113
1749
  if (typeof fees.v4TickLower === "number" &&
1114
1750
  typeof fees.v4TickUpper === "number" &&
1115
1751
  typeof fees.v4TickCurrent === "number" &&
@@ -1120,7 +1756,8 @@ function displayFeesToLog(fees, log, logLines) {
1120
1756
  log("");
1121
1757
  log(chalk.green("Total Fees:"));
1122
1758
  log(chalk.blue(" Tokens: ") + chalk.green(hasFeesToken ? fees.feeToken : "0"));
1123
- log(chalk.blue(" USDC: ") + chalk.green(hasFeesPaired ? fees.feePaired : "$0.00"));
1759
+ log(chalk.blue(" USDC: ") +
1760
+ chalk.green(hasFeesPaired ? fees.feePaired : "$0.00"));
1124
1761
  log("");
1125
1762
  log(chalk.green("Creator Share (80%):"));
1126
1763
  log(chalk.blue(" Tokens: ") + chalk.green(fees.creatorToken || "0"));
@@ -1185,7 +1822,8 @@ function displayAccountInfoToLog(data, log, logLines) {
1185
1822
  log(chalk.blue("=".repeat(50)));
1186
1823
  log("");
1187
1824
  log(chalk.blue("Account Index: ") + chalk.green(account.index.toString()));
1188
- log(chalk.blue("Type: ") + chalk.green(account.type === "custom" ? "Custom" : "Seed-Derived"));
1825
+ log(chalk.blue("Type: ") +
1826
+ chalk.green(account.type === "custom" ? "Custom" : "Seed-Derived"));
1189
1827
  log(chalk.blue("Address: ") + chalk.green(account.address));
1190
1828
  if (account.label) {
1191
1829
  log(chalk.blue("Label: ") + chalk.green(account.label));
@@ -1204,4 +1842,860 @@ function displayAccountInfoToLog(data, log, logLines) {
1204
1842
  displayPositionsToLog(data.positions, log, logLines);
1205
1843
  }
1206
1844
  }
1845
+ // Helper function to format time remaining for lease
1846
+ function formatTimeRemaining(expiresAt) {
1847
+ const now = new Date();
1848
+ const diffMs = expiresAt.getTime() - now.getTime();
1849
+ if (diffMs <= 0) {
1850
+ return "expired";
1851
+ }
1852
+ const minutes = Math.floor(diffMs / 60000);
1853
+ const seconds = Math.floor((diffMs % 60000) / 1000);
1854
+ if (minutes > 0) {
1855
+ return `${minutes}m ${seconds}s`;
1856
+ }
1857
+ return `${seconds}s`;
1858
+ }
1859
+ // Helper function to build chat header content
1860
+ async function buildChatHeaderContent(theme, tokenInfo, tokenIdentifier, isConnected, leaseInfo, getCyanColor) {
1861
+ const colorTag = theme === "dark" ? "green-fg" : "black-fg";
1862
+ const cyanColor = getCyanColor(theme);
1863
+ const lines = [];
1864
+ // Title line
1865
+ if (tokenInfo) {
1866
+ lines.push(`{${colorTag}}💬 httpcat Chat: {${cyanColor}}${tokenInfo.name} (${tokenInfo.symbol}){/${cyanColor}}{/${colorTag}}`);
1867
+ }
1868
+ else if (tokenIdentifier) {
1869
+ // Fallback to identifier if token lookup failed
1870
+ const displayId = tokenIdentifier.length > 20
1871
+ ? `${tokenIdentifier.slice(0, 10)}...${tokenIdentifier.slice(-6)}`
1872
+ : tokenIdentifier;
1873
+ lines.push(`{${colorTag}}💬 httpcat Chat: {${cyanColor}}${displayId}{/${cyanColor}}{/${colorTag}}`);
1874
+ }
1875
+ else {
1876
+ lines.push(`{${colorTag}}💬 httpcat Chat: General{/${colorTag}}`);
1877
+ }
1878
+ lines.push(`{${colorTag}}${"═".repeat(60)}{/${colorTag}}`);
1879
+ lines.push("");
1880
+ // Connection status
1881
+ if (isConnected) {
1882
+ lines.push(`{green-fg}✅ Connected to chat stream{/green-fg}`);
1883
+ lines.push("");
1884
+ }
1885
+ // Lease info
1886
+ if (leaseInfo) {
1887
+ const timeRemaining = formatTimeRemaining(leaseInfo.leaseExpiresAt);
1888
+ const isExpired = leaseInfo.leaseExpiresAt.getTime() <= Date.now();
1889
+ const timePrefix = isExpired ? "⚠️ " : "⏱️ ";
1890
+ lines.push(`{yellow-fg}${timePrefix}Lease expires in: ${timeRemaining}{/yellow-fg}`);
1891
+ }
1892
+ lines.push("");
1893
+ lines.push(`{${cyanColor}}💡 Type your message and press Enter to send{/${cyanColor}}`);
1894
+ lines.push(`{${cyanColor}}💡 Type /exit to return to shell{/${cyanColor}}`);
1895
+ lines.push(`{${cyanColor}}💡 Type /renew to renew your lease{/${cyanColor}}`);
1896
+ if (tokenIdentifier) {
1897
+ lines.push(`{${cyanColor}}💡 Type /buy <amount> to buy tokens{/${cyanColor}}`);
1898
+ lines.push(`{${cyanColor}}💡 Type /sell <amount> to sell tokens{/${cyanColor}}`);
1899
+ }
1900
+ return lines.join("\n");
1901
+ }
1902
+ // Simplified chat mode that works within the shell
1903
+ async function startChatInShell(client, tokenIdentifier, log, logLines, screen, inputBox, originalSubmitHandler, headerBox, buildWelcomeContent, currentTheme, catFaces, currentCatIndex) {
1904
+ const privateKey = config.getPrivateKey();
1905
+ const account = privateKeyToAccount(privateKey);
1906
+ const userAddress = account.address;
1907
+ let leaseInfo = null;
1908
+ let ws = null;
1909
+ let isInChatMode = true;
1910
+ let isProcessing = false; // Flag to prevent duplicate processing
1911
+ const displayedMessageIds = new Set();
1912
+ // Helper to format chat messages
1913
+ const formatChatMessage = (msg, isOwn = false) => {
1914
+ const date = new Date(msg.timestamp);
1915
+ const now = new Date();
1916
+ const diffMs = now.getTime() - date.getTime();
1917
+ const diffSec = Math.floor(diffMs / 1000);
1918
+ const diffMin = Math.floor(diffSec / 60);
1919
+ let timeStr = "just now";
1920
+ if (diffSec >= 60) {
1921
+ timeStr =
1922
+ diffMin < 60
1923
+ ? `${diffMin}m ago`
1924
+ : date.toLocaleTimeString("en-US", {
1925
+ hour: "2-digit",
1926
+ minute: "2-digit",
1927
+ });
1928
+ }
1929
+ const authorColor = isOwn ? chalk.green : chalk.cyan;
1930
+ const authorShort = msg.authorShort || formatAddress(msg.author, 6);
1931
+ return `${chalk.dim(`[${timeStr}]`)} ${authorColor(authorShort)}: ${msg.message}`;
1932
+ };
1933
+ // Get token information if it's a token chat
1934
+ let tokenInfo = null;
1935
+ if (tokenIdentifier) {
1936
+ try {
1937
+ const info = await getTokenInfo(client, tokenIdentifier, userAddress, true);
1938
+ tokenInfo = { name: info.name, symbol: info.symbol };
1939
+ }
1940
+ catch (error) {
1941
+ // Token lookup failed - will use identifier in header
1942
+ tokenInfo = null;
1943
+ }
1944
+ }
1945
+ // Helper to get cyan color based on theme
1946
+ const getCyanColor = (theme) => theme === "dark" ? "light-cyan-fg" : "cyan-fg";
1947
+ // Helper to update header
1948
+ const updateChatHeader = async (isConnected = false) => {
1949
+ const content = await buildChatHeaderContent(currentTheme, tokenInfo, tokenIdentifier, isConnected, leaseInfo, getCyanColor);
1950
+ headerBox.setContent(content);
1951
+ screen.render();
1952
+ };
1953
+ // Update header to show chat mode
1954
+ await updateChatHeader(false);
1955
+ // Set up header update interval for lease countdown
1956
+ let headerUpdateInterval = null;
1957
+ try {
1958
+ // Join chat
1959
+ log(chalk.blue("Joining chat..."));
1960
+ screen.render();
1961
+ const joinResult = await joinChat(client, userAddress, tokenIdentifier, true);
1962
+ leaseInfo = {
1963
+ leaseId: joinResult.leaseId,
1964
+ leaseExpiresAt: new Date(joinResult.leaseExpiresAt),
1965
+ };
1966
+ // Update header with lease info
1967
+ await updateChatHeader(false);
1968
+ // Display last messages
1969
+ if (joinResult.lastMessages.length > 0) {
1970
+ log(chalk.dim("─".repeat(60)));
1971
+ log(chalk.cyan.bold("Recent messages:"));
1972
+ for (const msg of joinResult.lastMessages) {
1973
+ displayedMessageIds.add(msg.messageId);
1974
+ const isOwn = msg.author.toLowerCase() === userAddress.toLowerCase();
1975
+ log(formatChatMessage(msg, isOwn));
1976
+ }
1977
+ log(chalk.dim("─".repeat(60)));
1978
+ }
1979
+ // Connect to WebSocket
1980
+ const agentUrl = client.getAgentUrl();
1981
+ const normalizedWsUrl = normalizeWebSocketUrl(joinResult.wsUrl, agentUrl);
1982
+ const wsUrlObj = new URL(normalizedWsUrl);
1983
+ wsUrlObj.searchParams.set("leaseId", joinResult.leaseId);
1984
+ await new Promise((resolve) => setTimeout(resolve, 500));
1985
+ ws = new WebSocket(wsUrlObj.toString());
1986
+ ws.on("open", () => {
1987
+ log(chalk.green("✅ Connected to chat stream"));
1988
+ updateChatHeader(true);
1989
+ screen.render();
1990
+ });
1991
+ ws.on("message", (data) => {
1992
+ try {
1993
+ const event = JSON.parse(data.toString());
1994
+ if (event.type === "message" && event.data) {
1995
+ const msg = {
1996
+ ...event.data,
1997
+ authorShort: formatAddress(event.data.author, 6),
1998
+ };
1999
+ if (!displayedMessageIds.has(msg.messageId)) {
2000
+ displayedMessageIds.add(msg.messageId);
2001
+ const isOwn = msg.author.toLowerCase() === userAddress.toLowerCase();
2002
+ log(formatChatMessage(msg, isOwn));
2003
+ screen.render();
2004
+ }
2005
+ }
2006
+ else if (event.type === "lease_expired") {
2007
+ log(chalk.yellow("⏱️ Your lease has expired. Type /renew to continue."));
2008
+ leaseInfo = null;
2009
+ updateChatHeader(true);
2010
+ screen.render();
2011
+ }
2012
+ }
2013
+ catch (error) {
2014
+ // Ignore parse errors
2015
+ }
2016
+ });
2017
+ ws.on("error", (error) => {
2018
+ log(chalk.red(`❌ WebSocket error: ${error.message}`));
2019
+ screen.render();
2020
+ });
2021
+ ws.on("close", () => {
2022
+ if (isInChatMode) {
2023
+ log(chalk.yellow("⚠️ WebSocket connection closed"));
2024
+ updateChatHeader(false);
2025
+ screen.render();
2026
+ }
2027
+ });
2028
+ // Start header update interval
2029
+ headerUpdateInterval = setInterval(async () => {
2030
+ if (leaseInfo && isInChatMode) {
2031
+ await updateChatHeader(true);
2032
+ }
2033
+ }, 1000);
2034
+ // Wait for connection
2035
+ await new Promise((resolve, reject) => {
2036
+ const timeout = setTimeout(() => reject(new Error("Connection timeout")), 10000);
2037
+ ws.once("open", () => {
2038
+ clearTimeout(timeout);
2039
+ resolve();
2040
+ });
2041
+ ws.once("error", (err) => {
2042
+ clearTimeout(timeout);
2043
+ reject(err);
2044
+ });
2045
+ });
2046
+ // Set up chat input handler
2047
+ const chatInputHandler = async (value) => {
2048
+ const trimmed = value.trim();
2049
+ // Clear input immediately
2050
+ inputBox.clearValue();
2051
+ screen.render();
2052
+ if (!trimmed) {
2053
+ isProcessing = false;
2054
+ inputBox.focus();
2055
+ screen.render();
2056
+ return;
2057
+ }
2058
+ // Handle commands
2059
+ if (trimmed.startsWith("/")) {
2060
+ const [cmd, ...cmdArgs] = trimmed.split(" ");
2061
+ switch (cmd) {
2062
+ case "/exit":
2063
+ case "/quit":
2064
+ isInChatMode = false;
2065
+ if (headerUpdateInterval) {
2066
+ clearInterval(headerUpdateInterval);
2067
+ headerUpdateInterval = null;
2068
+ }
2069
+ if (ws) {
2070
+ ws.close();
2071
+ ws = null;
2072
+ }
2073
+ log(chalk.yellow("Exited chat mode. Back to shell."));
2074
+ log("");
2075
+ // Restore original header
2076
+ const originalContent = await buildWelcomeContent(currentTheme, catFaces[currentCatIndex].face);
2077
+ headerBox.setContent(originalContent);
2078
+ // Restore original input handler
2079
+ inputBox.removeAllListeners("submit");
2080
+ inputBox.on("submit", originalSubmitHandler);
2081
+ isProcessing = false;
2082
+ inputBox.focus();
2083
+ screen.render();
2084
+ return;
2085
+ case "/renew":
2086
+ try {
2087
+ log(chalk.blue("Renewing lease..."));
2088
+ screen.render();
2089
+ const renewal = await renewLease(client, userAddress, leaseInfo?.leaseId);
2090
+ leaseInfo = {
2091
+ leaseId: renewal.leaseId,
2092
+ leaseExpiresAt: new Date(renewal.leaseExpiresAt),
2093
+ };
2094
+ await updateChatHeader(true);
2095
+ log(chalk.green("✅ Lease renewed!"));
2096
+ screen.render();
2097
+ }
2098
+ catch (error) {
2099
+ log(chalk.red(`❌ Renew failed: ${error instanceof Error ? error.message : String(error)}`));
2100
+ screen.render();
2101
+ }
2102
+ isProcessing = false;
2103
+ inputBox.focus();
2104
+ screen.render();
2105
+ return;
2106
+ case "/help":
2107
+ log(chalk.cyan("Chat commands: /exit, /quit, /renew, /help"));
2108
+ screen.render();
2109
+ isProcessing = false;
2110
+ inputBox.focus();
2111
+ screen.render();
2112
+ return;
2113
+ default:
2114
+ log(chalk.yellow(`Unknown command: ${cmd}. Type /help for commands.`));
2115
+ screen.render();
2116
+ isProcessing = false;
2117
+ inputBox.focus();
2118
+ screen.render();
2119
+ return;
2120
+ }
2121
+ }
2122
+ // Send message
2123
+ if (!leaseInfo || leaseInfo.leaseExpiresAt.getTime() <= Date.now()) {
2124
+ log(chalk.yellow("⏱️ Your lease has expired. Type /renew to continue."));
2125
+ screen.render();
2126
+ isProcessing = false;
2127
+ inputBox.focus();
2128
+ screen.render();
2129
+ return;
2130
+ }
2131
+ try {
2132
+ // Send message - it will appear via WebSocket when received
2133
+ await sendChatMessage(client, trimmed, leaseInfo.leaseId, userAddress);
2134
+ // Re-attach handler for next message
2135
+ inputBox.removeAllListeners("submit");
2136
+ inputBox.once("submit", chatInputHandler);
2137
+ isProcessing = false;
2138
+ inputBox.focus();
2139
+ screen.render();
2140
+ }
2141
+ catch (error) {
2142
+ log(chalk.red(`❌ Send failed: ${error instanceof Error ? error.message : String(error)}`));
2143
+ screen.render();
2144
+ // Re-attach handler for next message
2145
+ inputBox.removeAllListeners("submit");
2146
+ inputBox.once("submit", chatInputHandler);
2147
+ isProcessing = false;
2148
+ inputBox.focus();
2149
+ screen.render();
2150
+ }
2151
+ };
2152
+ // Temporarily replace input handler
2153
+ // Remove all listeners first to ensure clean state
2154
+ inputBox.removeAllListeners("submit");
2155
+ // Clear any existing input value to prevent state issues
2156
+ inputBox.clearValue();
2157
+ // Add the chat handler - only one handler should be active
2158
+ inputBox.once("submit", chatInputHandler);
2159
+ // Ensure input box has focus and is ready
2160
+ inputBox.focus();
2161
+ screen.render();
2162
+ }
2163
+ catch (error) {
2164
+ log(chalk.red(`❌ Chat error: ${error instanceof Error ? error.message : String(error)}`));
2165
+ if (headerUpdateInterval) {
2166
+ clearInterval(headerUpdateInterval);
2167
+ headerUpdateInterval = null;
2168
+ }
2169
+ if (ws)
2170
+ ws.close();
2171
+ // Restore original header on error
2172
+ const originalContent = await buildWelcomeContent(currentTheme, catFaces[currentCatIndex].face);
2173
+ headerBox.setContent(originalContent);
2174
+ // Restore original handler on error
2175
+ inputBox.removeAllListeners("submit");
2176
+ inputBox.on("submit", originalSubmitHandler);
2177
+ screen.render();
2178
+ inputBox.focus();
2179
+ screen.render();
2180
+ }
2181
+ }
2182
+ /**
2183
+ * Start agent interactive mode (standalone, called from CLI)
2184
+ * This looks identical to the regular shell but starts in agent chat mode
2185
+ */
2186
+ export async function startAgentInteractiveMode(client) {
2187
+ // Auto-detect terminal background and set default theme
2188
+ const detectedBg = detectTerminalBackground();
2189
+ let currentTheme = detectedBg === "dark" ? "dark" : "win95";
2190
+ // Helper function to get theme-appropriate cyan/blue color for blessed tags
2191
+ // For dark theme, use lighter colors (light-cyan-fg) for better visibility on black
2192
+ const getCyanColor = (theme) => theme === "dark" ? "light-cyan-fg" : "cyan-fg";
2193
+ const getBlueColor = (theme) => theme === "dark" ? "light-blue-fg" : "blue-fg";
2194
+ // Create blessed screen with optimized settings
2195
+ const screen = blessed.screen({
2196
+ smartCSR: true,
2197
+ title: "httpcat Agent Mode",
2198
+ fullUnicode: true, // Support double-width/surrogate/combining chars (emojis)
2199
+ fastCSR: false, // Disable fast CSR to prevent rendering issues
2200
+ cursor: {
2201
+ artificial: true,
2202
+ shape: "line",
2203
+ blink: true,
2204
+ color: "green",
2205
+ },
2206
+ // Force Unicode support for emojis
2207
+ forceUnicode: true,
2208
+ });
2209
+ const network = client.getNetwork();
2210
+ // Theme colors - no backgrounds, just borders
2211
+ const getThemeColors = (theme) => {
2212
+ switch (theme) {
2213
+ case "light":
2214
+ return {
2215
+ bg: "default", // Transparent/default
2216
+ fg: "black",
2217
+ border: "black",
2218
+ inputBg: "default",
2219
+ inputFg: "black", // Explicit black for visibility
2220
+ inputFocusBg: "default",
2221
+ inputFocusFg: "black",
2222
+ };
2223
+ case "win95":
2224
+ return {
2225
+ bg: "default",
2226
+ fg: "black",
2227
+ border: "black",
2228
+ inputBg: "default",
2229
+ inputFg: "black", // Explicit black for visibility
2230
+ inputFocusBg: "default",
2231
+ inputFocusFg: "black",
2232
+ };
2233
+ default: // dark
2234
+ return {
2235
+ bg: "default",
2236
+ fg: "green",
2237
+ border: "green",
2238
+ inputBg: "default",
2239
+ inputFg: "green", // Explicit green for visibility
2240
+ inputFocusBg: "default",
2241
+ inputFocusFg: "green",
2242
+ };
2243
+ }
2244
+ };
2245
+ let themeColors = getThemeColors(currentTheme);
2246
+ // Create header box with thick borders, transparent background - more compact
2247
+ const headerBox = blessed.box({
2248
+ top: 0,
2249
+ left: 0,
2250
+ width: "100%",
2251
+ height: 6, // Reduced to save vertical space
2252
+ content: "",
2253
+ tags: true,
2254
+ style: {
2255
+ fg: themeColors.fg,
2256
+ bg: "default", // Transparent
2257
+ bold: false,
2258
+ border: {
2259
+ fg: themeColors.border,
2260
+ bold: true,
2261
+ },
2262
+ },
2263
+ padding: {
2264
+ left: 1,
2265
+ right: 1,
2266
+ top: 0,
2267
+ bottom: 0,
2268
+ },
2269
+ border: {
2270
+ type: "line",
2271
+ fg: themeColors.border,
2272
+ ch: "═", // Double line for thicker border
2273
+ },
2274
+ });
2275
+ // Cat face variants for animation
2276
+ const catFaces = [
2277
+ { name: "Sleepy", face: "[=^ -.- ^=]" },
2278
+ { name: "Smug", face: "[=^‿^=]" },
2279
+ { name: "Unhinged", face: "[=^◉_◉^=]" },
2280
+ { name: "Judgy", face: "[=^ಠ‿ಠ^=]" },
2281
+ { name: "Cute", face: "[=^。^=]" },
2282
+ { name: "Menacing", face: "[=^>_<^=]" },
2283
+ { name: "Loaf Mode", face: "[=^___^=]" },
2284
+ { name: "Cosmic", face: "[=^✧_✧^=]" },
2285
+ ];
2286
+ let currentCatIndex = 0;
2287
+ let catAnimationInterval = null;
2288
+ // Helper function to build welcome content with account info - more compact
2289
+ const buildWelcomeContent = async (theme, catFace) => {
2290
+ const welcomeLines = [];
2291
+ const colorTag = theme === "dark" ? "green-fg" : "black-fg";
2292
+ const cyanColor = getCyanColor(theme);
2293
+ // Use provided cat face or current one
2294
+ const displayCatFace = catFace || catFaces[currentCatIndex].face;
2295
+ // Cat face with breathing room, compact info below
2296
+ welcomeLines.push(`{${colorTag}}${displayCatFace}{/${colorTag}}`);
2297
+ welcomeLines.push(`{green-fg}🐱 Welcome to httpcat!{/green-fg} | {green-fg}🌐 {${cyanColor}}${network}{/${cyanColor}}{/green-fg}`);
2298
+ // Get account info
2299
+ let accountInfo = null;
2300
+ try {
2301
+ const accounts = config.getAllAccounts();
2302
+ const activeIndex = config.getActiveAccountIndex();
2303
+ const account = accounts.find((acc) => acc.index === activeIndex);
2304
+ if (account) {
2305
+ // Get balance info
2306
+ try {
2307
+ const privateKey = config.getAccountPrivateKey(activeIndex);
2308
+ const balance = await checkBalance(privateKey, true); // silent mode
2309
+ accountInfo = {
2310
+ account,
2311
+ balance,
2312
+ };
2313
+ }
2314
+ catch (error) {
2315
+ // If balance check fails, just show account info without balance
2316
+ accountInfo = { account };
2317
+ }
2318
+ }
2319
+ }
2320
+ catch (error) {
2321
+ // If account info fails, continue without it
2322
+ }
2323
+ // Compact account info - combined on fewer lines
2324
+ if (accountInfo) {
2325
+ const { account, balance } = accountInfo;
2326
+ const accountType = account.type === "custom" ? "Custom" : "Seed-Derived";
2327
+ const accountLabel = account.label ? ` (${account.label})` : "";
2328
+ if (balance) {
2329
+ const ethDisplay = balance.ethFormatted || balance.ethBalance || "0 ETH";
2330
+ const usdcDisplay = balance.usdcFormatted || balance.usdcBalance || "$0.00";
2331
+ // Combine account and balance on one line to save space
2332
+ 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}}`);
2333
+ }
2334
+ else {
2335
+ welcomeLines.push(`{${cyanColor}}👤 Account #{green-fg}${account.index}{/green-fg} | {green-fg}${accountType}${accountLabel}{/green-fg}{/${cyanColor}}`);
2336
+ }
2337
+ }
2338
+ return welcomeLines.join("\n");
2339
+ };
2340
+ // Set initial header content
2341
+ buildWelcomeContent(currentTheme, catFaces[currentCatIndex].face).then((content) => {
2342
+ headerBox.setContent(content);
2343
+ screen.render();
2344
+ });
2345
+ // Start cat face animation (cycle through every minute)
2346
+ const startCatAnimation = () => {
2347
+ if (catAnimationInterval) {
2348
+ clearInterval(catAnimationInterval);
2349
+ }
2350
+ catAnimationInterval = setInterval(() => {
2351
+ currentCatIndex = (currentCatIndex + 1) % catFaces.length;
2352
+ buildWelcomeContent(currentTheme, catFaces[currentCatIndex].face).then((content) => {
2353
+ headerBox.setContent(content);
2354
+ screen.render();
2355
+ });
2356
+ }, 60000); // Change every minute (60 seconds)
2357
+ };
2358
+ // Start animation
2359
+ startCatAnimation();
2360
+ // Create output log box (scrollable) with thick borders, transparent background
2361
+ const outputBox = blessed.log({
2362
+ top: 6, // Adjusted to match new header height
2363
+ left: 0,
2364
+ width: "100%",
2365
+ bottom: 4, // Leave space for input box at bottom
2366
+ tags: true,
2367
+ scrollable: true,
2368
+ alwaysScroll: true,
2369
+ scrollbar: {
2370
+ ch: " ",
2371
+ inverse: currentTheme !== "dark",
2372
+ },
2373
+ style: {
2374
+ fg: themeColors.fg,
2375
+ bg: "default", // Transparent
2376
+ border: {
2377
+ fg: themeColors.border,
2378
+ bold: true,
2379
+ },
2380
+ },
2381
+ padding: {
2382
+ left: 0,
2383
+ right: 1,
2384
+ },
2385
+ mouse: true, // Enable mouse scrolling
2386
+ border: {
2387
+ type: "line",
2388
+ fg: themeColors.border,
2389
+ ch: "═", // Double line for thicker border
2390
+ },
2391
+ });
2392
+ // Create prompt label with bold font (appears larger) - positioned inside input box
2393
+ const promptLabel = blessed.text({
2394
+ bottom: 1,
2395
+ left: 2,
2396
+ width: 8, // Exactly "httpcat>" (8 characters)
2397
+ height: 1,
2398
+ content: "",
2399
+ tags: true,
2400
+ style: {
2401
+ fg: themeColors.fg,
2402
+ bg: "default", // Transparent
2403
+ bold: true,
2404
+ },
2405
+ });
2406
+ // Helper to update prompt label content
2407
+ const updatePromptLabel = (theme) => {
2408
+ const colorTag = theme === "dark" ? "green-fg" : "black-fg";
2409
+ promptLabel.content = `{${colorTag}}{bold}httpcat>{/bold}{/${colorTag}}`;
2410
+ };
2411
+ updatePromptLabel(currentTheme);
2412
+ // Create input box with visible cursor and stylish border
2413
+ const inputBox = blessed.textbox({
2414
+ bottom: 0,
2415
+ left: 0,
2416
+ width: "100%",
2417
+ height: 3,
2418
+ inputOnFocus: true,
2419
+ keys: true,
2420
+ vi: false, // Disabled to prevent double input issues in agent mode
2421
+ secret: false,
2422
+ tags: true,
2423
+ alwaysScroll: false,
2424
+ scrollable: false,
2425
+ padding: {
2426
+ left: 10, // Space for "httpcat>" prompt (8 chars) + 2 for spacing
2427
+ right: 1,
2428
+ top: 0,
2429
+ bottom: 0,
2430
+ },
2431
+ cursor: {
2432
+ artificial: true,
2433
+ shape: "block", // Block cursor is more visible than line
2434
+ blink: true,
2435
+ color: currentTheme === "dark" ? "green" : "black",
2436
+ },
2437
+ style: {
2438
+ fg: themeColors.fg,
2439
+ bg: "default",
2440
+ border: {
2441
+ fg: themeColors.border,
2442
+ bold: true,
2443
+ },
2444
+ focus: {
2445
+ fg: themeColors.fg,
2446
+ bg: "default",
2447
+ border: {
2448
+ fg: themeColors.border,
2449
+ bold: true,
2450
+ },
2451
+ },
2452
+ },
2453
+ border: {
2454
+ type: "line",
2455
+ fg: themeColors.border,
2456
+ ch: "─", // Single line border
2457
+ },
2458
+ });
2459
+ // Helper to update theme
2460
+ const updateTheme = (newTheme) => {
2461
+ currentTheme = newTheme;
2462
+ themeColors = getThemeColors(currentTheme);
2463
+ // Update screen cursor color
2464
+ screen.cursor.color =
2465
+ currentTheme === "dark" ? "green" : "black";
2466
+ // Update header content with new theme colors (keep current cat face)
2467
+ buildWelcomeContent(currentTheme, catFaces[currentCatIndex].face).then((content) => {
2468
+ headerBox.setContent(content);
2469
+ screen.render();
2470
+ });
2471
+ // Update all widget styles
2472
+ headerBox.style.fg = themeColors.fg;
2473
+ headerBox.style.bg = "default"; // Transparent
2474
+ headerBox.border = { type: "line", fg: themeColors.border, ch: "═" };
2475
+ outputBox.style.fg = themeColors.fg;
2476
+ outputBox.style.bg = "default"; // Transparent
2477
+ outputBox.scrollbar.inverse = currentTheme !== "dark";
2478
+ outputBox.border = { type: "line", fg: themeColors.border, ch: "═" };
2479
+ updatePromptLabel(currentTheme);
2480
+ promptLabel.style.fg = themeColors.fg;
2481
+ promptLabel.style.bg = "default"; // Transparent
2482
+ promptLabel.style.bold = true;
2483
+ // Update input box cursor, style, and border
2484
+ inputBox.style.fg = themeColors.fg;
2485
+ inputBox.style.bg = "default";
2486
+ inputBox.style.focus.fg = themeColors.fg;
2487
+ inputBox.style.focus.bg = "default";
2488
+ inputBox.style.border.fg = themeColors.border;
2489
+ inputBox.style.focus.border.fg = themeColors.border;
2490
+ inputBox.border = { type: "line", fg: themeColors.border, ch: "─" };
2491
+ if (inputBox.cursor) {
2492
+ inputBox.cursor.color =
2493
+ currentTheme === "dark" ? "green" : "black";
2494
+ }
2495
+ screen.cursor.color =
2496
+ currentTheme === "dark" ? "green" : "black";
2497
+ screen.render();
2498
+ };
2499
+ // Helper to log output (define before use)
2500
+ const log = (text) => {
2501
+ outputBox.log(text);
2502
+ outputBox.setScrollPerc(100);
2503
+ screen.render();
2504
+ };
2505
+ // Helper to log multiple lines
2506
+ const logLines = (lines) => {
2507
+ lines.forEach((line) => outputBox.log(line));
2508
+ outputBox.setScrollPerc(100);
2509
+ screen.render();
2510
+ };
2511
+ // Wrap toggleTheme to also log
2512
+ const toggleThemeWithLog = () => {
2513
+ const themes = ["win95", "dark", "light"];
2514
+ const currentIndex = themes.indexOf(currentTheme);
2515
+ const nextTheme = themes[(currentIndex + 1) % themes.length];
2516
+ updateTheme(nextTheme);
2517
+ log(chalk.blue(`Theme switched to: ${nextTheme}`));
2518
+ };
2519
+ // Append all widgets in correct z-order (last appended is on top)
2520
+ screen.append(headerBox);
2521
+ screen.append(outputBox);
2522
+ screen.append(inputBox);
2523
+ screen.append(promptLabel); // Prompt label on top so it's always visible
2524
+ // Handle F1 for theme toggle
2525
+ screen.key(["f1"], () => {
2526
+ toggleThemeWithLog();
2527
+ });
2528
+ // Store toggle function for command handler
2529
+ screen.toggleTheme = toggleThemeWithLog;
2530
+ screen.updateTheme = updateTheme;
2531
+ // Handle Ctrl+C - two-stage: first clears input if text exists, second quits
2532
+ const handleCtrlC = () => {
2533
+ const currentValue = inputBox.getValue();
2534
+ // If there's text in the input, clear it instead of quitting
2535
+ if (currentValue && currentValue.trim().length > 0) {
2536
+ inputBox.clearValue();
2537
+ screen.render();
2538
+ return;
2539
+ }
2540
+ // No text in input, so quit
2541
+ if (catAnimationInterval) {
2542
+ clearInterval(catAnimationInterval);
2543
+ catAnimationInterval = null;
2544
+ }
2545
+ screen.destroy();
2546
+ printCat("sleeping");
2547
+ console.log(chalk.cyan("Goodbye! 👋"));
2548
+ process.exit(0);
2549
+ };
2550
+ // Handle Ctrl+C on screen level
2551
+ screen.key(["C-c"], handleCtrlC);
2552
+ // Handle Ctrl+C on input box level
2553
+ inputBox.key(["C-c"], handleCtrlC);
2554
+ // Focus input and render
2555
+ inputBox.focus();
2556
+ screen.render();
2557
+ // Show welcome message with key commands on load
2558
+ displayWelcomeMessage(log, logLines, outputBox, screen, currentTheme);
2559
+ // Start agent chat mode
2560
+ await startAgentChatMode(client, log, logLines, screen, inputBox, headerBox, buildWelcomeContent, currentTheme, catFaces, currentCatIndex);
2561
+ }
2562
+ /**
2563
+ * Start agent chat mode (similar to startChatInShell)
2564
+ */
2565
+ async function startAgentChatMode(client, log, logLines, screen, inputBox, headerBox, buildWelcomeContent, currentTheme, catFaces, currentCatIndex) {
2566
+ let isProcessing = false;
2567
+ // Get agent config
2568
+ const agentConfig = config.getAIAgentConfig();
2569
+ if (!agentConfig) {
2570
+ throw new Error("Cat not configured");
2571
+ }
2572
+ const apiKey = config.getAIAgentApiKey();
2573
+ if (!apiKey) {
2574
+ throw new Error("Failed to get API key");
2575
+ }
2576
+ // Create LLM and agent
2577
+ const llm = createLLM(agentConfig, apiKey);
2578
+ const agent = createHttpcatAgent(client, llm);
2579
+ // Helper to update header
2580
+ const updateAgentHeader = async () => {
2581
+ const colorTag = currentTheme === "dark" ? "green-fg" : "black-fg";
2582
+ const cyanColor = currentTheme === "dark" ? "light-cyan-fg" : "cyan-fg";
2583
+ const lines = [];
2584
+ lines.push(`{${colorTag}}🐱 Cat Chat Mode{/${colorTag}}`);
2585
+ lines.push(`{${colorTag}}${"═".repeat(60)}{/${colorTag}}`);
2586
+ lines.push("");
2587
+ lines.push(`{${cyanColor}}💡 Ask me to buy tokens, check balances, list tokens, and more!{/${cyanColor}}`);
2588
+ lines.push(`{${cyanColor}}💡 Type /exit to return to shell{/${cyanColor}}`);
2589
+ lines.push("");
2590
+ const content = lines.join("\n");
2591
+ headerBox.setContent(content);
2592
+ screen.render();
2593
+ };
2594
+ // Update header to show agent mode
2595
+ await updateAgentHeader();
2596
+ // Store original submit handler
2597
+ const originalHandlers = inputBox.listeners("submit");
2598
+ const originalSubmitHandler = originalHandlers[0];
2599
+ // Set up agent input handler (similar to chat mode)
2600
+ const agentInputHandler = async (value) => {
2601
+ // Guard against double processing
2602
+ if (isProcessing) {
2603
+ return;
2604
+ }
2605
+ isProcessing = true;
2606
+ const trimmed = value.trim();
2607
+ // Clear input immediately
2608
+ inputBox.clearValue();
2609
+ screen.render();
2610
+ if (!trimmed) {
2611
+ isProcessing = false;
2612
+ inputBox.focus();
2613
+ screen.render();
2614
+ return;
2615
+ }
2616
+ // Handle commands
2617
+ if (trimmed.startsWith("/")) {
2618
+ const [cmd] = trimmed.split(" ");
2619
+ switch (cmd) {
2620
+ case "/exit":
2621
+ case "/quit":
2622
+ screen.destroy();
2623
+ printCat("sleeping");
2624
+ console.log(chalk.cyan("Goodbye! 👋"));
2625
+ process.exit(0);
2626
+ return;
2627
+ case "/help":
2628
+ log(chalk.cyan("Cat commands: /exit, /quit, /help"));
2629
+ isProcessing = false;
2630
+ inputBox.focus();
2631
+ screen.render();
2632
+ return;
2633
+ default:
2634
+ log(chalk.yellow(`Unknown command: ${cmd}. Type /help for commands.`));
2635
+ isProcessing = false;
2636
+ inputBox.focus();
2637
+ screen.render();
2638
+ return;
2639
+ }
2640
+ }
2641
+ // Send to agent
2642
+ try {
2643
+ // Get account address for session ID
2644
+ const privateKey = config.getPrivateKey();
2645
+ const account = privateKeyToAccount(privateKey);
2646
+ const sessionId = account.address;
2647
+ log(chalk.blue("🐱 Cat thinking..."));
2648
+ screen.render();
2649
+ const response = await chatWithAgent(agent, llm, trimmed, sessionId);
2650
+ log(chalk.green("🐱 Cat:"));
2651
+ log(response);
2652
+ log("");
2653
+ }
2654
+ catch (error) {
2655
+ const errorMsg = error.message || String(error);
2656
+ const errorStack = error.stack || "";
2657
+ // Log full error details for debugging
2658
+ if (process.env.HTTPCAT_DEBUG) {
2659
+ log(chalk.dim(`Debug: Full error: ${JSON.stringify(error, null, 2)}`));
2660
+ if (errorStack) {
2661
+ log(chalk.dim(`Debug: Stack trace: ${errorStack}`));
2662
+ }
2663
+ }
2664
+ log(chalk.red(`❌ Agent error: ${errorMsg}`));
2665
+ // Check for authentication errors
2666
+ const isAuthError = errorMsg.includes("Authentication failed") ||
2667
+ errorMsg.includes("API key") ||
2668
+ errorMsg.includes("401") ||
2669
+ errorMsg.includes("403") ||
2670
+ errorMsg.includes("Unauthorized") ||
2671
+ errorMsg.includes("Invalid API key") ||
2672
+ errorMsg.includes("authentication") ||
2673
+ errorMsg.includes("Invalid API Key") ||
2674
+ errorStack.includes("401") ||
2675
+ errorStack.includes("403");
2676
+ if (isAuthError) {
2677
+ log("");
2678
+ log(chalk.yellow("🔑 Authentication Error Detected"));
2679
+ log(chalk.dim("Your API key may be invalid, expired, or incorrectly configured."));
2680
+ log(chalk.dim("Exit cat mode and run 'agent' (or 'cat') again to re-run the setup wizard."));
2681
+ }
2682
+ log("");
2683
+ }
2684
+ isProcessing = false;
2685
+ inputBox.focus();
2686
+ screen.render();
2687
+ };
2688
+ // Remove original handler and add agent handler
2689
+ inputBox.removeAllListeners("submit");
2690
+ inputBox.on("submit", agentInputHandler);
2691
+ // Clear and focus input
2692
+ inputBox.clearValue();
2693
+ inputBox.focus();
2694
+ screen.render();
2695
+ // Show welcome message
2696
+ log(chalk.green("🐱 Cat chat mode activated!"));
2697
+ log(chalk.dim("Ask me anything about tokens, trading, or your portfolio."));
2698
+ log(chalk.dim("Type /exit to return to shell."));
2699
+ log("");
2700
+ }
1207
2701
  //# sourceMappingURL=shell.js.map