httpcat-cli 0.2.11 → 0.2.12-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/.github/workflows/sync-version.yml +19 -3
  2. package/README.md +346 -13
  3. package/Screenshot 2025-12-21 at 8.56.02/342/200/257PM.png +0 -0
  4. package/bun.lock +8 -1
  5. package/cat-spin.sh +417 -0
  6. package/dist/agent/ax-agent.d.ts.map +1 -0
  7. package/dist/agent/ax-agent.js +459 -0
  8. package/dist/agent/ax-agent.js.map +1 -0
  9. package/dist/agent/llm-factory.d.ts.map +1 -0
  10. package/dist/agent/llm-factory.js +82 -0
  11. package/dist/agent/llm-factory.js.map +1 -0
  12. package/dist/agent/setup-wizard.d.ts.map +1 -0
  13. package/dist/agent/setup-wizard.js +114 -0
  14. package/dist/agent/setup-wizard.js.map +1 -0
  15. package/dist/agent/tools.d.ts.map +1 -0
  16. package/dist/agent/tools.js +312 -0
  17. package/dist/agent/tools.js.map +1 -0
  18. package/dist/commands/balances.d.ts.map +1 -1
  19. package/dist/commands/balances.js +43 -41
  20. package/dist/commands/balances.js.map +1 -1
  21. package/dist/commands/chat.d.ts.map +1 -1
  22. package/dist/commands/chat.js +56 -46
  23. package/dist/commands/chat.js.map +1 -1
  24. package/dist/commands/create.d.ts.map +1 -1
  25. package/dist/commands/create.js +133 -5
  26. package/dist/commands/create.js.map +1 -1
  27. package/dist/commands/positions.d.ts.map +1 -1
  28. package/dist/commands/positions.js +51 -54
  29. package/dist/commands/positions.js.map +1 -1
  30. package/dist/config.d.ts.map +1 -1
  31. package/dist/config.js +296 -20
  32. package/dist/config.js.map +1 -1
  33. package/dist/index.js +301 -13
  34. package/dist/index.js.map +1 -1
  35. package/dist/interactive/cat-spin.d.ts.map +1 -0
  36. package/dist/interactive/cat-spin.js +448 -0
  37. package/dist/interactive/cat-spin.js.map +1 -0
  38. package/dist/interactive/shell.d.ts.map +1 -1
  39. package/dist/interactive/shell.js +2001 -180
  40. package/dist/interactive/shell.js.map +1 -1
  41. package/dist/mcp/tools.d.ts.map +1 -1
  42. package/dist/mcp/tools.js +1 -6
  43. package/dist/mcp/tools.js.map +1 -1
  44. package/dist/mcp/types.d.ts.map +1 -1
  45. package/dist/utils/loading.d.ts.map +1 -1
  46. package/dist/utils/loading.js +30 -0
  47. package/dist/utils/loading.js.map +1 -1
  48. package/dist/utils/privateKeyPrompt.d.ts.map +1 -1
  49. package/dist/utils/privateKeyPrompt.js +13 -9
  50. package/dist/utils/privateKeyPrompt.js.map +1 -1
  51. package/dist/utils/token-resolver.d.ts.map +1 -1
  52. package/dist/utils/token-resolver.js +32 -0
  53. package/dist/utils/token-resolver.js.map +1 -1
  54. package/package.json +3 -1
@@ -5,74 +5,288 @@ import { config } from "../config.js";
5
5
  import { printCat } from "./art.js";
6
6
  import { validateAmount } from "../utils/validation.js";
7
7
  // Import commands
8
- import { createToken } from "../commands/create.js";
9
- import { buyToken, TEST_AMOUNTS, PROD_AMOUNTS } from "../commands/buy.js";
8
+ import { createToken, processPhotoUrl, isFilePath, } from "../commands/create.js";
9
+ import { buyToken, TEST_AMOUNTS, PROD_AMOUNTS, } from "../commands/buy.js";
10
10
  import { sellToken, parseTokenAmount } from "../commands/sell.js";
11
11
  import { getTokenInfo } from "../commands/info.js";
12
12
  import { listTokens } from "../commands/list.js";
13
+ import { formatCurrency, formatTokenAmount, formatAddress, } from "../utils/formatting.js";
13
14
  import { getPositions } from "../commands/positions.js";
14
15
  import { privateKeyToAccount } from "viem/accounts";
15
16
  import { checkHealth } from "../commands/health.js";
16
- import { startChatStream } from "../commands/chat.js";
17
+ import { joinChat, sendChatMessage, renewLease, normalizeWebSocketUrl, } from "../commands/chat.js";
18
+ import WebSocket from "ws";
17
19
  import { checkBalance } from "../commands/balances.js";
18
- export async function startInteractiveShell(client) {
19
- // Create blessed screen
20
+ import { viewFees, claimFees } from "../commands/claim.js";
21
+ import { getTransactions } from "../commands/transactions.js";
22
+ import { getAccountInfo, switchAccount, addAccount, } from "../commands/account.js";
23
+ import { HttpcatError } from "../client.js";
24
+ import { createHttpcatAgent, chatWithAgent } from "../agent/ax-agent.js";
25
+ import { createLLM } from "../agent/llm-factory.js";
26
+ import { setupAIAgentWizard } from "../agent/setup-wizard.js";
27
+ // Detect terminal background color
28
+ function detectTerminalBackground() {
29
+ // Check COLORFGBG (format: "foreground;background")
30
+ // Background values: 0-7 are dark, 8-15 are light
31
+ const colorfgbg = process.env.COLORFGBG;
32
+ if (colorfgbg) {
33
+ const parts = colorfgbg.split(";");
34
+ if (parts.length >= 2) {
35
+ const bg = parseInt(parts[1], 10);
36
+ if (!isNaN(bg)) {
37
+ // 0-7 are dark backgrounds, 8-15 are light
38
+ return bg < 8 ? "dark" : "light";
39
+ }
40
+ }
41
+ }
42
+ // Check TERM_PROGRAM for common terminals
43
+ const termProgram = process.env.TERM_PROGRAM?.toLowerCase();
44
+ if (termProgram) {
45
+ // These terminals often have dark backgrounds by default
46
+ if (["iterm2", "vscode", "hyper", "alacritty", "kitty"].includes(termProgram)) {
47
+ return "dark";
48
+ }
49
+ // These often have light backgrounds
50
+ if (["apple_terminal"].includes(termProgram)) {
51
+ return "light";
52
+ }
53
+ }
54
+ // Check for common dark terminal indicators
55
+ const term = process.env.TERM?.toLowerCase() || "";
56
+ if (term.includes("256") || term.includes("xterm")) {
57
+ // Most modern terminals default to dark
58
+ return "dark";
59
+ }
60
+ // Default to dark (safer assumption for modern terminals)
61
+ return "dark";
62
+ }
63
+ export async function startInteractiveShell(client, autoChatToken) {
64
+ // Auto-detect terminal background and set default theme
65
+ const detectedBg = detectTerminalBackground();
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";
71
+ // Create blessed screen with optimized settings
20
72
  const screen = blessed.screen({
21
73
  smartCSR: true,
22
74
  title: "httpcat Interactive Shell",
23
- fullUnicode: true,
75
+ fullUnicode: true, // Support double-width/surrogate/combining chars (emojis)
76
+ fastCSR: false, // Disable fast CSR to prevent rendering issues
77
+ cursor: {
78
+ artificial: true,
79
+ shape: "line",
80
+ blink: true,
81
+ color: "green",
82
+ },
83
+ // Force Unicode support for emojis
84
+ forceUnicode: true,
24
85
  });
25
86
  const network = client.getNetwork();
26
- // Create header box
87
+ // Theme colors - no backgrounds, just borders
88
+ const getThemeColors = (theme) => {
89
+ switch (theme) {
90
+ case "light":
91
+ return {
92
+ bg: "default", // Transparent/default
93
+ fg: "black",
94
+ border: "black",
95
+ inputBg: "default",
96
+ inputFg: "black", // Explicit black for visibility
97
+ inputFocusBg: "default",
98
+ inputFocusFg: "black",
99
+ };
100
+ case "win95":
101
+ return {
102
+ bg: "default",
103
+ fg: "black",
104
+ border: "black",
105
+ inputBg: "default",
106
+ inputFg: "black", // Explicit black for visibility
107
+ inputFocusBg: "default",
108
+ inputFocusFg: "black",
109
+ };
110
+ default: // dark
111
+ return {
112
+ bg: "default",
113
+ fg: "green",
114
+ border: "green",
115
+ inputBg: "default",
116
+ inputFg: "green", // Explicit green for visibility
117
+ inputFocusBg: "default",
118
+ inputFocusFg: "green",
119
+ };
120
+ }
121
+ };
122
+ let themeColors = getThemeColors(currentTheme);
123
+ // Create header box with thick borders, transparent background - more compact
27
124
  const headerBox = blessed.box({
28
125
  top: 0,
29
126
  left: 0,
30
127
  width: "100%",
31
- height: 8,
128
+ height: 6, // Reduced to save vertical space
32
129
  content: "",
33
- tags: false,
130
+ tags: true,
34
131
  style: {
35
- fg: "white",
36
- bg: "black",
132
+ fg: themeColors.fg,
133
+ bg: "default", // Transparent
134
+ bold: false,
135
+ border: {
136
+ fg: themeColors.border,
137
+ bold: true,
138
+ },
37
139
  },
38
140
  padding: {
39
141
  left: 1,
40
142
  right: 1,
143
+ top: 0,
144
+ bottom: 0,
145
+ },
146
+ border: {
147
+ type: "line",
148
+ fg: themeColors.border,
149
+ ch: "═", // Double line for thicker border
41
150
  },
42
151
  });
43
- // Build welcome content
44
- const welcomeLines = [];
45
- welcomeLines.push(" /\\_/\\");
46
- welcomeLines.push(" ( ^.^ )");
47
- welcomeLines.push(" > ^ <");
48
- welcomeLines.push("");
49
- welcomeLines.push("Welcome to httpcat!");
50
- welcomeLines.push(`Connected to: ${network}`);
51
- welcomeLines.push('Type "help" for available commands or "exit" to quit');
52
- headerBox.setContent(welcomeLines.join("\n"));
53
- // Create output log box (scrollable)
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) => {
167
+ const welcomeLines = [];
168
+ const colorTag = theme === "dark" ? "green-fg" : "black-fg";
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}`);
175
+ // Get account info
176
+ let accountInfo = null;
177
+ try {
178
+ const accounts = config.getAllAccounts();
179
+ const activeIndex = config.getActiveAccountIndex();
180
+ const account = accounts.find((acc) => acc.index === activeIndex);
181
+ if (account) {
182
+ // Get balance info
183
+ try {
184
+ const privateKey = config.getAccountPrivateKey(activeIndex);
185
+ const balance = await checkBalance(privateKey, true); // silent mode
186
+ accountInfo = {
187
+ account,
188
+ balance,
189
+ };
190
+ }
191
+ catch (error) {
192
+ // If balance check fails, just show account info without balance
193
+ accountInfo = { account };
194
+ }
195
+ }
196
+ }
197
+ catch (error) {
198
+ // If account info fails, continue without it
199
+ }
200
+ // Compact account info - combined on fewer lines
201
+ if (accountInfo) {
202
+ const { account, balance } = accountInfo;
203
+ const accountType = account.type === "custom" ? "Custom" : "Seed-Derived";
204
+ const accountLabel = account.label ? ` (${account.label})` : "";
205
+ if (balance) {
206
+ const ethDisplay = balance.ethFormatted || balance.ethBalance || "0 ETH";
207
+ const usdcDisplay = balance.usdcFormatted || balance.usdcBalance || "$0.00";
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}}`);
213
+ }
214
+ }
215
+ return welcomeLines.join("\n");
216
+ };
217
+ // Set initial header content
218
+ buildWelcomeContent(currentTheme, catFaces[currentCatIndex].face).then((content) => {
219
+ headerBox.setContent(content);
220
+ screen.render();
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();
237
+ // Create output log box (scrollable) with thick borders, transparent background
54
238
  const outputBox = blessed.log({
55
- top: 8,
239
+ top: 6, // Adjusted to match new header height
56
240
  left: 0,
57
241
  width: "100%",
58
- height: "100%-11",
242
+ bottom: 4, // Leave space for input box at bottom
59
243
  tags: true,
60
244
  scrollable: true,
61
245
  alwaysScroll: true,
62
246
  scrollbar: {
63
247
  ch: " ",
64
- inverse: true,
248
+ inverse: currentTheme !== "dark",
65
249
  },
66
250
  style: {
67
- fg: "white",
68
- bg: "black",
251
+ fg: themeColors.fg,
252
+ bg: "default", // Transparent
253
+ border: {
254
+ fg: themeColors.border,
255
+ bold: true,
256
+ },
69
257
  },
70
258
  padding: {
71
- left: 1,
259
+ left: 0,
72
260
  right: 1,
73
261
  },
262
+ mouse: true, // Enable mouse scrolling
263
+ border: {
264
+ type: "line",
265
+ fg: themeColors.border,
266
+ ch: "═", // Double line for thicker border
267
+ },
74
268
  });
75
- // Create input box at bottom
269
+ // Create prompt label with bold font (appears larger) - positioned inside input box
270
+ const promptLabel = blessed.text({
271
+ bottom: 1,
272
+ left: 2,
273
+ width: 8, // Exactly "httpcat>" (8 characters)
274
+ height: 1,
275
+ content: "",
276
+ tags: true,
277
+ style: {
278
+ fg: themeColors.fg,
279
+ bg: "default", // Transparent
280
+ bold: true,
281
+ },
282
+ });
283
+ // Helper to update prompt label content
284
+ const updatePromptLabel = (theme) => {
285
+ const colorTag = theme === "dark" ? "green-fg" : "black-fg";
286
+ promptLabel.content = `{${colorTag}}{bold}httpcat>{/bold}{/${colorTag}}`;
287
+ };
288
+ updatePromptLabel(currentTheme);
289
+ // Create input box with visible cursor and stylish border
76
290
  const inputBox = blessed.textbox({
77
291
  bottom: 0,
78
292
  left: 0,
@@ -80,35 +294,86 @@ export async function startInteractiveShell(client) {
80
294
  height: 3,
81
295
  inputOnFocus: true,
82
296
  keys: true,
83
- style: {
84
- fg: "cyan",
85
- bg: "black",
86
- focus: {
87
- fg: "white",
88
- bg: "blue",
89
- },
90
- },
297
+ vi: false, // Disabled to prevent double input issues in agent mode
298
+ secret: false,
299
+ tags: true,
300
+ alwaysScroll: false,
301
+ scrollable: false,
91
302
  padding: {
92
- left: 1,
303
+ left: 10, // Space for "httpcat>" prompt (8 chars) + 2 for spacing
93
304
  right: 1,
305
+ top: 0,
306
+ bottom: 0,
307
+ },
308
+ cursor: {
309
+ artificial: true,
310
+ shape: "block", // Block cursor is more visible than line
311
+ blink: true,
312
+ color: currentTheme === "dark" ? "green" : "black",
94
313
  },
95
- });
96
- // Create a prompt label
97
- const promptLabel = blessed.text({
98
- bottom: 1,
99
- left: 1,
100
- content: "httpcat> ",
101
314
  style: {
102
- fg: "cyan",
103
- bg: "black",
315
+ fg: themeColors.fg,
316
+ bg: "default",
317
+ border: {
318
+ fg: themeColors.border,
319
+ bold: true,
320
+ },
321
+ focus: {
322
+ fg: themeColors.fg,
323
+ bg: "default",
324
+ border: {
325
+ fg: themeColors.border,
326
+ bold: true,
327
+ },
328
+ },
329
+ },
330
+ border: {
331
+ type: "line",
332
+ fg: themeColors.border,
333
+ ch: "─", // Single line border
104
334
  },
105
335
  });
106
- // Append all widgets
107
- screen.append(headerBox);
108
- screen.append(outputBox);
109
- screen.append(promptLabel);
110
- screen.append(inputBox);
111
- // Helper to log output
336
+ // Helper to update theme
337
+ const updateTheme = (newTheme) => {
338
+ currentTheme = newTheme;
339
+ themeColors = getThemeColors(currentTheme);
340
+ // Update screen cursor color
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) => {
345
+ headerBox.setContent(content);
346
+ screen.render();
347
+ });
348
+ // Update all widget styles
349
+ headerBox.style.fg = themeColors.fg;
350
+ headerBox.style.bg = "default"; // Transparent
351
+ headerBox.border = { type: "line", fg: themeColors.border, ch: "═" };
352
+ outputBox.style.fg = themeColors.fg;
353
+ outputBox.style.bg = "default"; // Transparent
354
+ outputBox.scrollbar.inverse = currentTheme !== "dark";
355
+ outputBox.border = { type: "line", fg: themeColors.border, ch: "═" };
356
+ updatePromptLabel(currentTheme);
357
+ promptLabel.style.fg = themeColors.fg;
358
+ promptLabel.style.bg = "default"; // Transparent
359
+ promptLabel.style.bold = true;
360
+ // Update input box cursor, style, and border
361
+ inputBox.style.fg = themeColors.fg;
362
+ inputBox.style.bg = "default";
363
+ inputBox.style.focus.fg = themeColors.fg;
364
+ inputBox.style.focus.bg = "default";
365
+ inputBox.style.border.fg = themeColors.border;
366
+ inputBox.style.focus.border.fg = themeColors.border;
367
+ inputBox.border = { type: "line", fg: themeColors.border, ch: "─" };
368
+ if (inputBox.cursor) {
369
+ inputBox.cursor.color =
370
+ currentTheme === "dark" ? "green" : "black";
371
+ }
372
+ screen.cursor.color =
373
+ currentTheme === "dark" ? "green" : "black";
374
+ screen.render();
375
+ };
376
+ // Helper to log output (define before use)
112
377
  const log = (text) => {
113
378
  outputBox.log(text);
114
379
  outputBox.setScrollPerc(100);
@@ -120,19 +385,166 @@ export async function startInteractiveShell(client) {
120
385
  outputBox.setScrollPerc(100);
121
386
  screen.render();
122
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
+ };
424
+ // Wrap toggleTheme to also log
425
+ const toggleThemeWithLog = () => {
426
+ const themes = ["win95", "dark", "light"];
427
+ const currentIndex = themes.indexOf(currentTheme);
428
+ const nextTheme = themes[(currentIndex + 1) % themes.length];
429
+ updateTheme(nextTheme);
430
+ log(chalk.blue(`Theme switched to: ${nextTheme}`));
431
+ };
432
+ // Append all widgets in correct z-order (last appended is on top)
433
+ screen.append(headerBox);
434
+ screen.append(outputBox);
435
+ screen.append(inputBox);
436
+ screen.append(promptLabel); // Prompt label on top so it's always visible
437
+ // Handle F1 for theme toggle
438
+ screen.key(["f1"], () => {
439
+ toggleThemeWithLog();
440
+ });
441
+ // Store toggle function for command handler
442
+ screen.toggleTheme = toggleThemeWithLog;
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
+ });
123
521
  // Handle input submission
124
522
  inputBox.on("submit", async (value) => {
125
523
  const trimmed = value.trim();
126
524
  inputBox.clearValue();
525
+ screen.render(); // Clear input immediately
526
+ // Reset history navigation
527
+ historyIndex = -1;
528
+ currentInputBeforeHistory = "";
127
529
  if (!trimmed) {
128
530
  inputBox.focus();
129
531
  screen.render();
130
532
  return;
131
533
  }
132
- log(`httpcat> ${trimmed}`);
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
+ }
543
+ // Log the command with prompt
544
+ log(`{green-fg}httpcat>{/green-fg} ${trimmed}`);
133
545
  const [command, ...args] = trimmed.split(/\s+/);
134
546
  try {
135
- await handleCommand(client, command.toLowerCase(), args, log, logLines, screen, inputBox);
547
+ await handleCommand(client, command.toLowerCase(), args, log, logLines, logLinesSmooth, screen, inputBox, currentTheme, buildWelcomeContent, headerBox, catFaces, currentCatIndex);
136
548
  }
137
549
  catch (error) {
138
550
  log(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
@@ -141,43 +553,86 @@ export async function startInteractiveShell(client) {
141
553
  inputBox.focus();
142
554
  screen.render();
143
555
  });
144
- // Handle Ctrl+C
145
- screen.key(["C-c"], () => {
146
- screen.destroy();
147
- printCat("sleeping");
148
- console.log(chalk.cyan("Goodbye! 👋"));
149
- process.exit(0);
150
- });
151
556
  // Handle escape to clear input
152
557
  inputBox.key(["escape"], () => {
153
558
  inputBox.clearValue();
154
559
  screen.render();
155
560
  });
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
+ }
575
+ screen.destroy();
576
+ printCat("sleeping");
577
+ console.log(chalk.cyan("Goodbye! 👋"));
578
+ process.exit(0);
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);
156
584
  // Focus input and render
157
585
  inputBox.focus();
158
586
  screen.render();
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
+ }
159
599
  }
160
- async function handleCommand(client, command, args, log, logLines, screen, inputBox) {
600
+ async function handleCommand(client, command, args, log, logLines, logLinesSmooth, screen, inputBox, currentTheme, buildWelcomeContent, headerBox, catFaces, currentCatIndex) {
161
601
  switch (command) {
162
602
  case "help":
163
- 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);
164
606
  break;
165
607
  case "create": {
166
608
  if (args.length < 2) {
167
- 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]"));
168
610
  return;
169
611
  }
170
612
  const [name, symbol] = args;
171
- const photoUrl = extractFlag(args, "--photo");
172
- const bannerUrl = extractFlag(args, "--banner");
613
+ let photoUrl = extractFlag(args, "--photo");
173
614
  const websiteUrl = extractFlag(args, "--website");
174
- log(chalk.dim("Creating token..."));
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
+ }
630
+ log(chalk.blue("Creating token..."));
175
631
  screen.render();
176
632
  const result = await createToken(client, {
177
633
  name,
178
634
  symbol,
179
635
  photoUrl,
180
- bannerUrl,
181
636
  websiteUrl,
182
637
  });
183
638
  displayCreateResultToLog(result, log, logLines);
@@ -185,14 +640,21 @@ async function handleCommand(client, command, args, log, logLines, screen, input
185
640
  }
186
641
  case "buy": {
187
642
  if (args.length < 2) {
188
- log(chalk.red("Usage: buy <address|name|symbol> <amount>"));
643
+ log(chalk.red("Usage: buy <address|name|symbol> <amount> [--repeat <count>] [--delay <ms>]"));
189
644
  log(chalk.dim(" amount: 0.05, 0.10, or 0.20 (test mode) or 50, 100, 200 (production)"));
190
- log(chalk.dim(' Examples: buy 0x1234... 0.20, buy "My Token" 0.10, buy MTK 0.05'));
645
+ log(chalk.dim(' Examples: buy 0x1234... 0.20, buy "My Token" 0.10, buy MTK 0.05 --repeat 10'));
191
646
  return;
192
647
  }
193
648
  const [identifier, amountInput] = args;
194
649
  const isTestMode = client.getNetwork().includes("sepolia");
195
650
  const validAmounts = isTestMode ? TEST_AMOUNTS : PROD_AMOUNTS;
651
+ // Parse flags
652
+ const repeatCount = extractFlag(args, "--repeat")
653
+ ? parseInt(extractFlag(args, "--repeat") || "1", 10)
654
+ : undefined;
655
+ const delayMs = extractFlag(args, "--delay")
656
+ ? parseInt(extractFlag(args, "--delay") || "0", 10)
657
+ : 0;
196
658
  // Try to validate and normalize the amount
197
659
  let amount;
198
660
  try {
@@ -204,10 +666,105 @@ async function handleCommand(client, command, args, log, logLines, screen, input
204
666
  log(chalk.dim(`Valid amounts: ${validAmounts.join(", ")}`));
205
667
  return;
206
668
  }
207
- log(chalk.dim("Buying tokens..."));
208
- screen.render();
209
- const result = await buyToken(client, identifier, amount, isTestMode, false);
210
- displayBuyResultToLog(result, log, logLines);
669
+ const privateKey = config.getPrivateKey();
670
+ // Handle repeat mode
671
+ if (repeatCount && repeatCount > 0) {
672
+ const results = [];
673
+ let totalSpent = 0;
674
+ let stoppedEarly = false;
675
+ let stopReason = "";
676
+ for (let i = 1; i <= repeatCount; i++) {
677
+ try {
678
+ log(chalk.blue(`Buy ${i}/${repeatCount}...`));
679
+ screen.render();
680
+ const result = await buyToken(client, identifier, amount, isTestMode, true, // silent=true to avoid console.log interference
681
+ privateKey);
682
+ results.push(result);
683
+ totalSpent += parseFloat(result.amountSpent);
684
+ // Display compact result
685
+ displayBuyResultToLog(result, log, logLines);
686
+ // Check if token graduated
687
+ if (result.graduationReached) {
688
+ stoppedEarly = true;
689
+ stopReason = "Token graduated";
690
+ log(chalk.green("🎓 Token has graduated! Stopping buy loop."));
691
+ break;
692
+ }
693
+ // Apply delay between iterations (except after the last one)
694
+ if (i < repeatCount && delayMs > 0) {
695
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
696
+ }
697
+ }
698
+ catch (error) {
699
+ // Handle insufficient funds (402) gracefully
700
+ if (error instanceof HttpcatError && error.status === 402) {
701
+ stoppedEarly = true;
702
+ stopReason = "Insufficient funds";
703
+ log(chalk.yellow("💡 Insufficient funds. Stopping buy loop."));
704
+ log("");
705
+ // Show current balance to help diagnose the issue
706
+ try {
707
+ const balance = await checkBalance(privateKey, true);
708
+ log(chalk.cyan("💰 Current Wallet Balances:"));
709
+ log(chalk.dim(` ETH: ${balance.ethFormatted}`));
710
+ log(chalk.dim(` USDC: ${balance.usdcFormatted}`));
711
+ if (balance.cat402Formatted) {
712
+ log(chalk.dim(` CAT: ${balance.cat402Formatted}`));
713
+ }
714
+ log("");
715
+ // Provide guidance based on what might be insufficient
716
+ const usdcBalance = parseFloat(balance.usdcFormatted.replace("$", "").replace(",", ""));
717
+ const ethBalance = parseFloat(balance.ethFormatted.replace(" ETH", "").replace(",", ""));
718
+ const buyAmount = parseFloat(amount);
719
+ log(chalk.yellow("💡 Possible issues:"));
720
+ if (usdcBalance < buyAmount) {
721
+ log(chalk.dim(` • Insufficient USDC for purchase: Need $${buyAmount.toFixed(6)}, have $${usdcBalance.toFixed(6)}`));
722
+ }
723
+ if (usdcBalance < 0.01) {
724
+ log(chalk.dim(` • Low USDC for API payments: x402 protocol requires USDC for API calls`));
725
+ }
726
+ if (ethBalance < 0.001) {
727
+ log(chalk.dim(` • Low ETH for gas: Need ETH to pay for transaction fees`));
728
+ }
729
+ log("");
730
+ log(chalk.dim(" Check your balance with: balances"));
731
+ }
732
+ catch (balanceError) {
733
+ // If we can't fetch balance, just show generic message
734
+ log(chalk.dim(" Check your balance with: balances"));
735
+ }
736
+ break;
737
+ }
738
+ // For other errors, re-throw to be handled by outer catch
739
+ throw error;
740
+ }
741
+ }
742
+ // Show final summary
743
+ if (results.length > 0) {
744
+ const lastResult = results[results.length - 1];
745
+ const graduationStatus = lastResult.graduationReached
746
+ ? "✅ GRADUATED!"
747
+ : `${(lastResult.graduationProgress || 0).toFixed(2)}%`;
748
+ log("");
749
+ log(chalk.cyan.bold("📊 Repeat Buy Summary"));
750
+ log(chalk.dim("─".repeat(50)));
751
+ log(`Total buys completed: ${chalk.bold(results.length.toString())}${repeatCount ? `/${repeatCount}` : ""}`);
752
+ log(`Total amount spent: ${chalk.bold(`$${totalSpent.toFixed(2)}`)}`);
753
+ log(`Final token price: ${chalk.bold(lastResult.newPrice)}`);
754
+ log(`Final graduation: ${chalk.bold(graduationStatus)}`);
755
+ if (stoppedEarly) {
756
+ log(`Stopped early: ${chalk.yellow(stopReason)}`);
757
+ }
758
+ }
759
+ }
760
+ else {
761
+ // Normal single buy execution
762
+ log(chalk.blue("Buying tokens..."));
763
+ screen.render();
764
+ const result = await buyToken(client, identifier, amount, isTestMode, true, // silent=true to avoid console.log interference
765
+ privateKey);
766
+ displayBuyResultToLog(result, log, logLines);
767
+ }
211
768
  break;
212
769
  }
213
770
  case "sell": {
@@ -217,17 +774,21 @@ async function handleCommand(client, command, args, log, logLines, screen, input
217
774
  return;
218
775
  }
219
776
  const [identifier, amountInput] = args;
220
- log(chalk.dim("Checking token info..."));
777
+ // Get user address from private key for position checking
778
+ const privateKey = config.getPrivateKey();
779
+ const account = privateKeyToAccount(privateKey);
780
+ const userAddress = account.address;
781
+ log(chalk.blue("Checking token info..."));
221
782
  screen.render();
222
- const info = await getTokenInfo(client, identifier);
783
+ const info = await getTokenInfo(client, identifier, userAddress, true); // silent=true to avoid console.log interference
223
784
  if (!info.userPosition || info.userPosition.tokensOwned === "0") {
224
785
  log(chalk.yellow("You do not own any of this token."));
225
786
  return;
226
787
  }
227
788
  const tokenAmount = parseTokenAmount(amountInput, info.userPosition.tokensOwned);
228
- log(chalk.dim("Selling tokens..."));
789
+ log(chalk.blue("Selling tokens..."));
229
790
  screen.render();
230
- const result = await sellToken(client, identifier, tokenAmount, false);
791
+ const result = await sellToken(client, identifier, tokenAmount, true, privateKey); // silent=true to avoid console.log interference
231
792
  displaySellResultToLog(result, log, logLines);
232
793
  break;
233
794
  }
@@ -242,9 +803,9 @@ async function handleCommand(client, command, args, log, logLines, screen, input
242
803
  const privateKey = config.getPrivateKey();
243
804
  const account = privateKeyToAccount(privateKey);
244
805
  const userAddress = account.address;
245
- log(chalk.dim("Fetching token info..."));
806
+ log(chalk.blue("Fetching token info..."));
246
807
  screen.render();
247
- const info = await getTokenInfo(client, identifier, userAddress);
808
+ const info = await getTokenInfo(client, identifier, userAddress, true); // silent=true to avoid console.log interference
248
809
  displayTokenInfoToLog(info, log, logLines);
249
810
  break;
250
811
  }
@@ -252,20 +813,34 @@ async function handleCommand(client, command, args, log, logLines, screen, input
252
813
  const page = parseInt(extractFlag(args, "--page") || "1");
253
814
  const limit = parseInt(extractFlag(args, "--limit") || "20");
254
815
  const sortBy = (extractFlag(args, "--sort") || "mcap");
255
- log(chalk.dim("Fetching token list..."));
816
+ log(chalk.blue("Fetching token list..."));
256
817
  screen.render();
257
818
  const result = await listTokens(client, page, limit, sortBy);
258
- displayTokenListToLog(result, log, logLines);
819
+ displayTokenListToLog(result, log, logLines, logLinesSmooth);
259
820
  break;
260
821
  }
261
822
  case "positions": {
262
823
  const privateKey = config.getPrivateKey();
263
824
  const account = privateKeyToAccount(privateKey);
264
825
  const userAddress = account.address;
265
- log(chalk.dim("Fetching positions..."));
826
+ // Parse flags
827
+ const activeOnly = args.includes("--active");
828
+ const graduatedOnly = args.includes("--graduated");
829
+ log(chalk.blue("Fetching positions..."));
266
830
  screen.render();
267
831
  const result = await getPositions(client, userAddress);
268
- displayPositionsToLog(result, log, logLines);
832
+ // Filter positions if flags are set
833
+ let filteredResult = result;
834
+ if (activeOnly || graduatedOnly) {
835
+ filteredResult = {
836
+ ...result,
837
+ positions: result.positions.filter((p) => activeOnly
838
+ ? p.token.status !== "graduated"
839
+ : p.token.status === "graduated"),
840
+ };
841
+ filteredResult.total = filteredResult.positions.length;
842
+ }
843
+ displayPositionsToLog(filteredResult, log, logLines);
269
844
  break;
270
845
  }
271
846
  case "health": {
@@ -278,15 +853,15 @@ async function handleCommand(client, command, args, log, logLines, screen, input
278
853
  case "balances": {
279
854
  try {
280
855
  const privateKey = config.getPrivateKey();
281
- log(chalk.dim("Checking balance..."));
856
+ log(chalk.blue("Checking balance..."));
282
857
  screen.render();
283
- const balance = await checkBalance(privateKey);
858
+ const balance = await checkBalance(privateKey, true); // silent mode
284
859
  displayBalanceToLog(balance, log, logLines);
285
860
  }
286
861
  catch (error) {
287
- log(chalk.dim("Checking balance..."));
862
+ log(chalk.blue("Checking balance..."));
288
863
  screen.render();
289
- const balance = await checkBalance();
864
+ const balance = await checkBalance(undefined, true); // silent mode
290
865
  displayBalanceToLog(balance, log, logLines);
291
866
  }
292
867
  break;
@@ -309,17 +884,22 @@ async function handleCommand(client, command, args, log, logLines, screen, input
309
884
  break;
310
885
  }
311
886
  case "network": {
312
- log(chalk.yellow("Current network: ") + chalk.cyan(config.get("network")));
313
- log(chalk.dim("Note: Only base-sepolia (testnet) is currently supported"));
887
+ log(chalk.yellow("Current network: ") + chalk.green(config.get("network")));
888
+ log(chalk.blue("Note: Only base-sepolia (testnet) is currently supported"));
314
889
  break;
315
890
  }
316
891
  case "chat": {
317
- // For chat, we need to exit the blessed shell and start chat stream
318
- log(chalk.yellow("Starting chat mode..."));
319
- screen.destroy();
320
- const tokenIdentifier = args.length > 0 ? args[0] : undefined;
321
- await startChatStream(client, false, tokenIdentifier);
322
- process.exit(0);
892
+ const tokenIdentifier = args.length > 0 && !args[0].startsWith("--") ? args[0] : undefined;
893
+ log(chalk.yellow("💬 Entering chat mode..."));
894
+ log(chalk.dim("Type /exit to return to shell, or /help for chat commands"));
895
+ log("");
896
+ // Start chat mode within the shell
897
+ // Store original submit handler
898
+ const originalHandlers = inputBox.listeners("submit");
899
+ const originalSubmitHandler = originalHandlers[0];
900
+ // Remove original handler temporarily
901
+ inputBox.removeAllListeners("submit");
902
+ await startChatInShell(client, tokenIdentifier, log, logLines, screen, inputBox, originalSubmitHandler, headerBox, buildWelcomeContent, currentTheme, catFaces, currentCatIndex);
323
903
  break;
324
904
  }
325
905
  case "exit":
@@ -333,115 +913,737 @@ async function handleCommand(client, command, args, log, logLines, screen, input
333
913
  // Clear the output box
334
914
  screen.children[1].setContent("");
335
915
  screen.render();
916
+ // Show playful message with ASCII cat
917
+ const catArt = currentTheme === "dark"
918
+ ? `{green-fg} /\\_/\\{/green-fg}
919
+ {green-fg} ( ^.^ ){/green-fg}
920
+ {green-fg} > ^ <{/green-fg}`
921
+ : `{black-fg} /\\_/\\{/black-fg}
922
+ {black-fg} ( ^.^ ){/black-fg}
923
+ {black-fg} > ^ <{/black-fg}`;
924
+ log("");
925
+ log(catArt);
926
+ log(chalk.green.bold("Terminal cleared!"));
927
+ log("");
336
928
  break;
929
+ case "claim": {
930
+ if (args.length < 1) {
931
+ log(chalk.red("Usage: claim <address|name|symbol> [--execute] [--address <address>]"));
932
+ log(chalk.dim(' Examples: claim MTK, claim "My Token" --execute'));
933
+ return;
934
+ }
935
+ const [identifier] = args;
936
+ const execute = args.includes("--execute");
937
+ const callerAddress = extractFlag(args, "--address");
938
+ if (execute) {
939
+ const privateKey = config.getPrivateKey();
940
+ const account = privateKeyToAccount(privateKey);
941
+ const address = callerAddress || account.address;
942
+ log(chalk.blue("Claiming fees..."));
943
+ screen.render();
944
+ const result = await claimFees(client, identifier, address, true); // silent=true to avoid console.log interference
945
+ displayClaimResultToLog(result, log, logLines);
946
+ }
947
+ else {
948
+ log(chalk.blue("Fetching fee information..."));
949
+ screen.render();
950
+ const result = await viewFees(client, identifier, true); // silent=true to avoid console.log interference
951
+ displayFeesToLog(result, log, logLines);
952
+ }
953
+ break;
954
+ }
955
+ case "transactions": {
956
+ const privateKey = config.getPrivateKey();
957
+ const account = privateKeyToAccount(privateKey);
958
+ const userAddress = account.address;
959
+ const input = {};
960
+ if (extractFlag(args, "--user")) {
961
+ input.userAddress = extractFlag(args, "--user");
962
+ }
963
+ else {
964
+ input.userAddress = userAddress;
965
+ }
966
+ if (extractFlag(args, "--token")) {
967
+ input.tokenId = extractFlag(args, "--token");
968
+ }
969
+ if (extractFlag(args, "--type")) {
970
+ const type = extractFlag(args, "--type");
971
+ if (!["buy", "sell", "airdrop"].includes(type || "")) {
972
+ log(chalk.red("Invalid type. Must be: buy, sell, or airdrop"));
973
+ return;
974
+ }
975
+ input.type = type;
976
+ }
977
+ if (extractFlag(args, "--limit")) {
978
+ input.limit = parseInt(extractFlag(args, "--limit") || "50", 10);
979
+ }
980
+ else {
981
+ input.limit = 50;
982
+ }
983
+ if (extractFlag(args, "--offset")) {
984
+ input.offset = parseInt(extractFlag(args, "--offset") || "0", 10);
985
+ }
986
+ else {
987
+ input.offset = 0;
988
+ }
989
+ log(chalk.blue("Fetching transactions..."));
990
+ screen.render();
991
+ const result = await getTransactions(client, input);
992
+ displayTransactionsToLog(result, log, logLines);
993
+ break;
994
+ }
995
+ case "account": {
996
+ if (args.length === 0) {
997
+ // Show account info
998
+ log(chalk.blue("Fetching account information..."));
999
+ screen.render();
1000
+ const result = await getAccountInfo();
1001
+ displayAccountInfoToLog(result, log, logLines);
1002
+ }
1003
+ else if (args[0] === "list") {
1004
+ // For list, we need to capture console output or recreate the display
1005
+ const accounts = config.getAllAccounts();
1006
+ const activeIndex = config.getActiveAccountIndex();
1007
+ log("");
1008
+ log(chalk.green.bold("📋 All Accounts"));
1009
+ log("");
1010
+ if (accounts.length === 0) {
1011
+ log(chalk.yellow("No accounts configured."));
1012
+ log(chalk.blue('Run "httpcat config" to set up your wallet.'));
1013
+ return;
1014
+ }
1015
+ for (const account of accounts) {
1016
+ const isActive = account.index === activeIndex;
1017
+ const status = isActive
1018
+ ? chalk.green("● Active")
1019
+ : chalk.blue("○ Inactive");
1020
+ const type = account.type === "custom" ? "Custom" : "Seed-Derived";
1021
+ const address = account.address.slice(0, 6) + "..." + account.address.slice(-4);
1022
+ log(` ${chalk.green(account.index.toString().padEnd(5))} ${chalk.blue(type.padEnd(12))} ${chalk.green(address.padEnd(15))} ${status}`);
1023
+ }
1024
+ log("");
1025
+ log(chalk.blue(`Active account: ${chalk.green(activeIndex.toString())}`));
1026
+ }
1027
+ else if (args[0] === "switch") {
1028
+ if (args.length < 2) {
1029
+ log(chalk.red("Usage: account switch <index>"));
1030
+ return;
1031
+ }
1032
+ const index = parseInt(args[1], 10);
1033
+ if (isNaN(index)) {
1034
+ log(chalk.red("Invalid account index"));
1035
+ return;
1036
+ }
1037
+ switchAccount(index);
1038
+ const accounts = config.getAllAccounts();
1039
+ const account = accounts.find((acc) => acc.index === index);
1040
+ log(chalk.green(`✅ Switched to account ${index}`));
1041
+ if (account) {
1042
+ log(chalk.blue(` Address: ${account.address.slice(0, 6)}...${account.address.slice(-4)}`));
1043
+ }
1044
+ // Refresh header with new account info
1045
+ buildWelcomeContent(currentTheme, catFaces[currentCatIndex].face).then((content) => {
1046
+ headerBox.setContent(content);
1047
+ screen.render();
1048
+ });
1049
+ }
1050
+ else if (args[0] === "add") {
1051
+ log(chalk.blue("Adding new account..."));
1052
+ screen.render();
1053
+ await addAccount();
1054
+ log(chalk.green("✅ Account added"));
1055
+ // Refresh header with new account info
1056
+ buildWelcomeContent(currentTheme, catFaces[currentCatIndex].face).then((content) => {
1057
+ headerBox.setContent(content);
1058
+ screen.render();
1059
+ });
1060
+ }
1061
+ else {
1062
+ log(chalk.red("Usage: account [list|switch <index>|add]"));
1063
+ }
1064
+ break;
1065
+ }
1066
+ case "env": {
1067
+ if (args.length === 0 || args[0] === "show") {
1068
+ const current = config.getCurrentEnvironment();
1069
+ if (!current) {
1070
+ log(chalk.yellow("No environment selected"));
1071
+ return;
1072
+ }
1073
+ const env = config.getEnvironmentConfig(current);
1074
+ if (!env) {
1075
+ log(chalk.yellow(`Environment "${current}" not found`));
1076
+ return;
1077
+ }
1078
+ log(chalk.cyan.bold(`Current Environment: ${chalk.green(current)}`));
1079
+ log(chalk.dim(`Agent URL: ${env.agentUrl}`));
1080
+ log(chalk.dim(`Network: ${env.network}`));
1081
+ }
1082
+ else if (args[0] === "list") {
1083
+ const envs = config.getEnvironments();
1084
+ const current = config.getCurrentEnvironment();
1085
+ log(chalk.cyan.bold("Available Environments:"));
1086
+ log("");
1087
+ for (const [name, env] of Object.entries(envs)) {
1088
+ const isCurrent = name === current;
1089
+ const prefix = isCurrent ? chalk.green("→ ") : " ";
1090
+ const nameDisplay = isCurrent
1091
+ ? chalk.green.bold(name)
1092
+ : chalk.bold(name);
1093
+ log(`${prefix}${nameDisplay}`);
1094
+ log(chalk.dim(` Agent URL: ${env.agentUrl}`));
1095
+ log(chalk.dim(` Network: ${env.network}`));
1096
+ log("");
1097
+ }
1098
+ if (current) {
1099
+ log(chalk.dim(`Current environment: ${chalk.green(current)}`));
1100
+ }
1101
+ }
1102
+ else if (args[0] === "use") {
1103
+ if (args.length < 2) {
1104
+ log(chalk.red("Usage: env use <name>"));
1105
+ return;
1106
+ }
1107
+ config.setEnvironment(args[1]);
1108
+ const env = config.getEnvironmentConfig(args[1]);
1109
+ log(chalk.green(`✅ Switched to environment: ${args[1]}`));
1110
+ if (env) {
1111
+ log(chalk.dim(` Agent URL: ${env.agentUrl}`));
1112
+ log(chalk.dim(` Network: ${env.network}`));
1113
+ }
1114
+ }
1115
+ else if (args[0] === "add") {
1116
+ if (args.length < 3) {
1117
+ log(chalk.red("Usage: env add <name> <agentUrl> [--network <network>]"));
1118
+ return;
1119
+ }
1120
+ const network = extractFlag(args, "--network") || "base-sepolia";
1121
+ config.addEnvironment(args[1], args[2], network);
1122
+ log(chalk.green(`✅ Added environment: ${args[1]}`));
1123
+ log(chalk.dim(` Agent URL: ${args[2]}`));
1124
+ log(chalk.dim(` Network: ${network}`));
1125
+ }
1126
+ else if (args[0] === "update") {
1127
+ if (args.length < 3) {
1128
+ log(chalk.red("Usage: env update <name> <agentUrl> [--network <network>]"));
1129
+ return;
1130
+ }
1131
+ const network = extractFlag(args, "--network") || "base-sepolia";
1132
+ config.updateEnvironment(args[1], args[2], network);
1133
+ log(chalk.green(`✅ Updated environment: ${args[1]}`));
1134
+ log(chalk.dim(` Agent URL: ${args[2]}`));
1135
+ log(chalk.dim(` Network: ${network}`));
1136
+ }
1137
+ else {
1138
+ log(chalk.red("Usage: env [list|use <name>|show|add <name> <url>|update <name> <url>]"));
1139
+ }
1140
+ break;
1141
+ }
1142
+ case "agent":
1143
+ case "ai":
1144
+ case "cat": {
1145
+ // Check if --chat flag is provided (for future chat mode)
1146
+ if (args[0] === "--chat") {
1147
+ log(chalk.yellow("⚠️ Agent chat mode is currently disabled"));
1148
+ log(chalk.dim("Use 'agent <query>' or 'cat <query>' to ask the cat directly."));
1149
+ log("");
1150
+ break;
1151
+ }
1152
+ // Check if --setup flag is provided
1153
+ if (args[0] === "--setup") {
1154
+ try {
1155
+ await setupAIAgentWizard();
1156
+ log(chalk.green("✅ Agent configuration updated!"));
1157
+ log("");
1158
+ }
1159
+ catch (error) {
1160
+ log(chalk.red(`❌ Setup failed: ${error.message || String(error)}`));
1161
+ log("");
1162
+ }
1163
+ break;
1164
+ }
1165
+ // Get the query text (everything after the command)
1166
+ const query = args.join(" ").trim();
1167
+ if (!query) {
1168
+ log(chalk.yellow("Usage: agent <query>"));
1169
+ log(chalk.dim("Example: agent 'buy 0.1 USDC worth of WHALE'"));
1170
+ log(chalk.dim("Example: cat 'check my balance'"));
1171
+ log("");
1172
+ break;
1173
+ }
1174
+ // Check if agent is configured
1175
+ const agentConfig = config.getAIAgentConfig();
1176
+ if (!agentConfig) {
1177
+ log(chalk.yellow("⚠️ Agent not configured"));
1178
+ log(chalk.dim("Run 'agent --setup' to configure the AI agent."));
1179
+ log("");
1180
+ break;
1181
+ }
1182
+ const apiKey = config.getAIAgentApiKey();
1183
+ if (!apiKey) {
1184
+ log(chalk.red("❌ Failed to get API key"));
1185
+ log(chalk.dim("Run 'agent --setup' to configure the API key."));
1186
+ log("");
1187
+ break;
1188
+ }
1189
+ try {
1190
+ // Get account address for session ID
1191
+ const privateKey = config.getPrivateKey();
1192
+ const account = privateKeyToAccount(privateKey);
1193
+ const sessionId = account.address;
1194
+ // Show thinking indicator
1195
+ log(chalk.blue("🐱 Cat thinking..."));
1196
+ screen.render();
1197
+ // Create LLM and agent
1198
+ const llm = createLLM(agentConfig, apiKey);
1199
+ const agent = createHttpcatAgent(client, llm);
1200
+ // Chat with agent (pass session ID so it remembers)
1201
+ const response = await chatWithAgent(agent, llm, query, sessionId);
1202
+ // Display response
1203
+ log("");
1204
+ log(chalk.green("🐱 Cat:"));
1205
+ log(chalk.white(response));
1206
+ log("");
1207
+ }
1208
+ catch (error) {
1209
+ // Enhanced error logging
1210
+ if (process.env.HTTPCAT_DEBUG) {
1211
+ log(chalk.red(`❌ Error details: ${JSON.stringify(error, null, 2)}`));
1212
+ if (error.stack) {
1213
+ log(chalk.dim(error.stack));
1214
+ }
1215
+ }
1216
+ // Check for authentication errors
1217
+ const errorMessage = error?.message || String(error);
1218
+ if (errorMessage.includes("Authentication failed") ||
1219
+ errorMessage.includes("API key") ||
1220
+ errorMessage.includes("401") ||
1221
+ errorMessage.includes("403") ||
1222
+ errorMessage.includes("Unauthorized") ||
1223
+ errorMessage.includes("Invalid API key")) {
1224
+ log(chalk.red("❌ Authentication failed"));
1225
+ log(chalk.dim("Your API key may be invalid or expired."));
1226
+ log(chalk.dim("Run 'agent --setup' to reconfigure."));
1227
+ }
1228
+ else {
1229
+ log(chalk.red(`❌ Error: ${errorMessage}`));
1230
+ }
1231
+ log("");
1232
+ }
1233
+ break;
1234
+ }
337
1235
  default:
338
1236
  log(chalk.red(`Unknown command: ${command}`));
339
1237
  log(chalk.dim('Type "help" for available commands'));
340
1238
  }
341
1239
  }
342
- function displayHelp(log, logLines) {
343
- log("");
344
- log(chalk.cyan.bold("Available Commands:"));
345
- log("");
346
- const commands = [
347
- ["create <name> <symbol>", "Create a new token"],
348
- ["buy <address|name|symbol> <amount>", "Buy tokens ($0.05, $0.10, or $0.20)"],
349
- ["sell <address|name|symbol> <amount|all>", "Sell tokens"],
350
- ["info <address|name|symbol>", "Get token information"],
351
- ["list [--sort mcap|created]", "List all tokens"],
352
- ["positions", "Get all your positions with comprehensive info"],
353
- ["balances", "Check wallet balances (ETH and USDC)"],
354
- [
355
- "chat [token]",
356
- "Start streaming chat (optional: token symbol/name/address)",
357
- ],
358
- ["health", "Check agent health"],
359
- ["config [--show|--set|--reset]", "Manage configuration"],
360
- ["network", "Show current network"],
361
- ["clear", "Clear the output"],
362
- ["help", "Show this help message"],
363
- ["exit", "Exit the shell"],
364
- ];
365
- for (const [cmd, desc] of commands) {
366
- log(` ${chalk.cyan(cmd.padEnd(30))} ${chalk.dim(desc)}`);
1240
+ function displayWelcomeMessage(log, logLines, outputBox, screen, theme = "dark") {
1241
+ // Helper to get theme-appropriate colors
1242
+ const getCyanColor = (t) => t === "dark" ? "light-cyan-fg" : "cyan-fg";
1243
+ const getBlueColor = (t) => t === "dark" ? "light-blue-fg" : "blue-fg";
1244
+ const cyanColor = getCyanColor(theme);
1245
+ const blueColor = getBlueColor(theme);
1246
+ // If we have outputBox, use blessed tags instead of chalk to avoid question marks
1247
+ // Otherwise fall back to the log function with chalk
1248
+ if (outputBox && screen) {
1249
+ const welcomeLines = [
1250
+ "",
1251
+ `{bold}{${cyanColor}}🐱 Welcome to httpcat!{/${cyanColor}}{/bold}`,
1252
+ "",
1253
+ "{yellow-fg}✨ Here are some commands to get you started:{/yellow-fg}",
1254
+ "",
1255
+ "{green-fg} 📋 list{/green-fg} - Browse all available tokens",
1256
+ "{green-fg} ℹ️ info <token>{/green-fg} - Get detailed info about a token",
1257
+ "{green-fg} 💰 buy <token> <amount>{/green-fg} - Buy tokens (e.g., buy MTK 0.10)",
1258
+ "{green-fg} 💼 positions{/green-fg} - View your portfolio",
1259
+ "{green-fg} 💵 balances{/green-fg} - Check your wallet balance",
1260
+ "",
1261
+ `{${blueColor}}💡 Type {bold}help{/bold} to see all available commands!{/${blueColor}}`,
1262
+ "",
1263
+ ];
1264
+ // Log all welcome lines at once
1265
+ welcomeLines.forEach((line) => outputBox.log(line));
1266
+ // Scroll to top so welcome message is visible
1267
+ outputBox.setScrollPerc(0);
1268
+ screen.render();
1269
+ }
1270
+ else {
1271
+ // Fallback to regular log function if outputBox not available
1272
+ log("");
1273
+ log(chalk.cyan.bold("🐱 Welcome to httpcat!"));
1274
+ log("");
1275
+ log(chalk.yellow("✨ Here are some commands to get you started:"));
1276
+ log("");
1277
+ log(chalk.green(" 📋 list") +
1278
+ chalk.dim(" - Browse all available tokens"));
1279
+ log(chalk.green(" ℹ️ info <token>") +
1280
+ chalk.dim(" - Get detailed info about a token"));
1281
+ log(chalk.green(" 💰 buy <token> <amount>") +
1282
+ chalk.dim(" - Buy tokens (e.g., buy MTK 0.10)"));
1283
+ log(chalk.green(" 💼 positions") +
1284
+ chalk.dim(" - View your portfolio"));
1285
+ log(chalk.green(" 💵 balances") +
1286
+ chalk.dim(" - Check your wallet balance"));
1287
+ log("");
1288
+ log(chalk.blue("💡 Type ") +
1289
+ chalk.bold("help") +
1290
+ chalk.blue(" to see all available commands!"));
1291
+ log("");
1292
+ }
1293
+ }
1294
+ function displayHelp(log, logLines, outputBox, screen, theme = "dark") {
1295
+ // Helper to get theme-appropriate colors
1296
+ const getCyanColor = (t) => t === "dark" ? "light-cyan-fg" : "cyan-fg";
1297
+ const getBlueColor = (t) => t === "dark" ? "light-blue-fg" : "blue-fg";
1298
+ const cyanColor = getCyanColor(theme);
1299
+ const blueColor = getBlueColor(theme);
1300
+ // If we have outputBox, use blessed tags instead of chalk to avoid question marks
1301
+ // Otherwise fall back to the log function
1302
+ if (outputBox && screen) {
1303
+ // Get the line number where help will start (before logging)
1304
+ const linesBeforeHelp = outputBox.lines?.length || 0;
1305
+ // Collect all help lines using blessed tags
1306
+ const helpLines = [
1307
+ "",
1308
+ `{${cyanColor}}{bold}Available Commands:{/bold}{/${cyanColor}}`,
1309
+ "",
1310
+ // Token Operations
1311
+ `{${cyanColor}}{bold}Token Operations:{/bold}{/${cyanColor}}`,
1312
+ "{green-fg} create <name> <symbol>{/green-fg}",
1313
+ "{green-fg} buy <id> <amount> [--repeat N] [--delay MS]{/green-fg}",
1314
+ "{green-fg} sell <id> <amount|all>{/green-fg}",
1315
+ "{green-fg} claim <id> [--execute] [--address ADDR]{/green-fg}",
1316
+ "",
1317
+ // Token Information
1318
+ `{${cyanColor}}{bold}Token Information:{/bold}{/${cyanColor}}`,
1319
+ "{green-fg} info <id>{/green-fg}",
1320
+ "{green-fg} list [--sort mcap|created|name] [--page N] [--limit N]{/green-fg}",
1321
+ "",
1322
+ // Portfolio
1323
+ `{${cyanColor}}{bold}Portfolio:{/bold}{/${cyanColor}}`,
1324
+ "{green-fg} positions [--active|--graduated]{/green-fg}",
1325
+ "{green-fg} transactions [--user ADDR] [--token ID] [--type TYPE] [--limit N] [--offset N]{/green-fg}",
1326
+ "{green-fg} balances{/green-fg}",
1327
+ "",
1328
+ // Account Management
1329
+ `{${cyanColor}}{bold}Account Management:{/bold}{/${cyanColor}}`,
1330
+ "{green-fg} account [list|switch <index>|add]{/green-fg}",
1331
+ "{green-fg} env [list|use <name>|show|add|update]{/green-fg}",
1332
+ "",
1333
+ // Social
1334
+ `{${cyanColor}}{bold}Social:{/bold}{/${cyanColor}}`,
1335
+ "{green-fg} chat [token] [--input-format FORMAT]{/green-fg}",
1336
+ "",
1337
+ // Cat (agent and cat are synonyms)
1338
+ `{${cyanColor}}{bold}Cat (or 'agent'):{/bold}{/${cyanColor}}`,
1339
+ "{green-fg} agent <query>{/green-fg} - Ask the cat to do something (or use 'cat')",
1340
+ "{green-fg} agent --chat{/green-fg} - Enter interactive chat mode (or 'cat --chat')",
1341
+ "{green-fg} agent --setup{/green-fg} - Configure API key/provider (or 'cat --setup')",
1342
+ "",
1343
+ // System
1344
+ `{${cyanColor}}{bold}System:{/bold}{/${cyanColor}}`,
1345
+ "{green-fg} health{/green-fg}",
1346
+ "{green-fg} config [--show|--set|--reset]{/green-fg}",
1347
+ "{green-fg} network{/green-fg}",
1348
+ "",
1349
+ // Shell Commands
1350
+ `{${cyanColor}}{bold}Shell Commands:{/bold}{/${cyanColor}}`,
1351
+ "{green-fg} clear{/green-fg}",
1352
+ "{green-fg} help{/green-fg}",
1353
+ "{green-fg} exit{/green-fg}",
1354
+ "",
1355
+ `{${blueColor}}Tip: Run any command without arguments to see detailed help{/${blueColor}}`,
1356
+ ];
1357
+ // Log all help lines at once
1358
+ helpLines.forEach((line) => outputBox.log(line));
1359
+ // Scroll to show "Available Commands" at the top of the visible area
1360
+ // Get total lines after logging
1361
+ const totalLines = outputBox.lines?.length || 0;
1362
+ const visibleHeight = outputBox.height || 20;
1363
+ if (totalLines > visibleHeight && linesBeforeHelp >= 0) {
1364
+ // Calculate which line "Available Commands" is on (after the empty line)
1365
+ const availableCommandsLine = linesBeforeHelp + 1;
1366
+ // Calculate scroll percentage to show that line at the top
1367
+ // Percentage = (line number / total lines) * 100
1368
+ // But we want the line to be at the top, so we need to account for visible height
1369
+ const scrollPerc = Math.max(0, Math.min(100, ((availableCommandsLine - 1) /
1370
+ Math.max(1, totalLines - visibleHeight)) *
1371
+ 100));
1372
+ outputBox.setScrollPerc(scrollPerc);
1373
+ }
1374
+ else {
1375
+ // If all content fits in visible area or no previous content, scroll to top
1376
+ outputBox.setScrollPerc(0);
1377
+ }
1378
+ screen.render();
1379
+ }
1380
+ else {
1381
+ // Fallback to regular log function if outputBox not available
1382
+ log("");
1383
+ log(chalk.cyan.bold("Available Commands:"));
1384
+ log("");
1385
+ // Token Operations
1386
+ log(chalk.bold(chalk.cyan("Token Operations:")));
1387
+ log(chalk.green(" create <name> <symbol>"));
1388
+ log(chalk.green(" buy <id> <amount> [--repeat N] [--delay MS]"));
1389
+ log(chalk.green(" sell <id> <amount|all>"));
1390
+ log(chalk.green(" claim <id> [--execute] [--address ADDR]"));
1391
+ log("");
1392
+ // Token Information
1393
+ log(chalk.bold(chalk.cyan("Token Information:")));
1394
+ log(chalk.green(" info <id>"));
1395
+ log(chalk.green(" list [--sort mcap|created|name] [--page N] [--limit N]"));
1396
+ log("");
1397
+ // Portfolio
1398
+ log(chalk.bold(chalk.cyan("Portfolio:")));
1399
+ log(chalk.green(" positions [--active|--graduated]"));
1400
+ log(chalk.green(" transactions [--user ADDR] [--token ID] [--type TYPE] [--limit N] [--offset N]"));
1401
+ log(chalk.green(" balances"));
1402
+ log("");
1403
+ // Account Management
1404
+ log(chalk.bold(chalk.cyan("Account Management:")));
1405
+ log(chalk.green(" account [list|switch <index>|add]"));
1406
+ log(chalk.green(" env [list|use <name>|show|add|update]"));
1407
+ log("");
1408
+ // Social
1409
+ log(chalk.bold(chalk.cyan("Social:")));
1410
+ log(chalk.green(" chat [token] [--input-format FORMAT]"));
1411
+ log("");
1412
+ // Cat (agent and cat are synonyms)
1413
+ log(chalk.bold(chalk.cyan("Cat (or 'agent'):")));
1414
+ log(chalk.green(" agent <query>") +
1415
+ chalk.dim(" - Ask the cat to do something (or use 'cat')"));
1416
+ log(chalk.green(" agent --chat") +
1417
+ chalk.dim(" - Enter interactive chat mode (or 'cat --chat')"));
1418
+ log(chalk.green(" agent --setup") +
1419
+ chalk.dim(" - Configure API key/provider (or 'cat --setup')"));
1420
+ log("");
1421
+ // System
1422
+ log(chalk.bold(chalk.cyan("System:")));
1423
+ log(chalk.green(" health"));
1424
+ log(chalk.green(" config [--show|--set|--reset]"));
1425
+ log(chalk.green(" network"));
1426
+ log("");
1427
+ // Shell Commands
1428
+ log(chalk.bold(chalk.cyan("Shell Commands:")));
1429
+ log(chalk.green(" clear"));
1430
+ log(chalk.green(" help"));
1431
+ log(chalk.green(" exit"));
1432
+ log("");
1433
+ log(chalk.blue("Tip: Run any command without arguments to see detailed help"));
367
1434
  }
368
- log("");
369
- log(chalk.dim("Examples:"));
370
- log(chalk.dim(' create "My Token" "MTK"'));
371
- log(chalk.dim(" buy 0x1234... 5"));
372
- log(chalk.dim(' buy "My Token" 2'));
373
- log(chalk.dim(" buy MTK 1"));
374
- log(chalk.dim(" sell 0x1234... 50%"));
375
- log(chalk.dim(' info "My Token"'));
376
- log(chalk.dim(" list --sort mcap --limit 10"));
377
- log(chalk.dim(" positions"));
378
1435
  }
379
1436
  function displayConfigToLog(log, logLines) {
380
1437
  const cfg = config.getAll();
381
1438
  log("");
382
- log(chalk.cyan.bold("Current Configuration:"));
1439
+ log(chalk.green.bold("Current Configuration:"));
383
1440
  log("");
384
- log(chalk.dim("Network: ") + chalk.cyan(cfg.network));
385
- log(chalk.dim("Agent URL: ") + chalk.cyan(cfg.agentUrl));
386
- log(chalk.dim("Facilitator: ") + chalk.cyan(cfg.facilitatorUrl));
387
- log(chalk.dim("Max Payment: ") + chalk.cyan("$" + cfg.defaultMaxPayment));
388
- log(chalk.dim("ASCII Art: ") +
389
- chalk.cyan(cfg.preferences.enableAsciiArt ? "enabled" : "disabled"));
390
- log(chalk.dim("Private Key: ") +
391
- chalk.dim(cfg.privateKey ? "configured" : "not configured"));
1441
+ log(chalk.blue("Network: ") + chalk.green(cfg.network));
1442
+ log(chalk.blue("Agent URL: ") + chalk.green(cfg.agentUrl));
1443
+ log(chalk.blue("Facilitator: ") + chalk.green(cfg.facilitatorUrl));
1444
+ log(chalk.blue("Max Payment: ") + chalk.green("$" + cfg.defaultMaxPayment));
1445
+ log(chalk.blue("ASCII Art: ") +
1446
+ chalk.green(cfg.preferences.enableAsciiArt ? "enabled" : "disabled"));
1447
+ log(chalk.blue("Private Key: ") +
1448
+ chalk.blue(cfg.privateKey ? "configured" : "not configured"));
392
1449
  log("");
393
- log(chalk.dim(`Config file: ${config.getConfigPath()}`));
1450
+ log(chalk.blue(`Config file: ${config.getConfigPath()}`));
394
1451
  }
395
1452
  // Simplified display functions that log to blessed output
396
1453
  function displayCreateResultToLog(result, log, logLines) {
397
1454
  log(chalk.green("Token created successfully!"));
398
- log(chalk.dim("Name: ") + result.name);
399
- log(chalk.dim("Symbol: ") + result.symbol);
1455
+ log(chalk.blue("Name: ") + chalk.green(result.name));
1456
+ log(chalk.blue("Symbol: ") + chalk.green(result.symbol));
400
1457
  if (result.tokenAddress) {
401
- log(chalk.dim("Address: ") + chalk.cyan(result.tokenAddress));
1458
+ log(chalk.blue("Address: ") + chalk.green(result.tokenAddress));
402
1459
  }
403
1460
  }
404
1461
  function displayBuyResultToLog(result, log, logLines) {
405
1462
  log(chalk.green("Purchase successful!"));
406
- log(chalk.dim("Tokens received: ") + chalk.cyan(result.tokensReceived));
407
- log(chalk.dim("Amount spent: ") + chalk.yellow(result.amountSpent));
408
- log(chalk.dim("New price: ") + chalk.cyan(result.newPrice));
409
- log(chalk.dim("Graduation: ") +
1463
+ log(chalk.blue("Tokens received: ") + chalk.green(result.tokensReceived));
1464
+ log(chalk.blue("Amount spent: ") + chalk.yellow(result.amountSpent));
1465
+ log(chalk.blue("New price: ") + chalk.green(result.newPrice));
1466
+ log(chalk.blue("Graduation: ") +
410
1467
  chalk.magenta(result.graduationProgress.toFixed(2) + "%"));
411
1468
  }
412
1469
  function displaySellResultToLog(result, log, logLines) {
413
1470
  log(chalk.green("Sale successful!"));
414
- log(chalk.dim("Tokens sold: ") + chalk.cyan(result.tokensSold));
415
- log(chalk.dim("USDC received: ") + chalk.yellow(result.usdcReceived));
416
- log(chalk.dim("New price: ") + chalk.cyan(result.newPrice));
417
- log(chalk.dim("Graduation: ") +
418
- chalk.magenta(result.graduationProgress.toFixed(2) + "%"));
1471
+ log(chalk.blue("Tokens sold: ") +
1472
+ chalk.green(formatTokenAmount(result.tokensSold || "0")));
1473
+ log(chalk.blue("USDC received: ") +
1474
+ chalk.yellow(formatCurrency(result.usdcReceived || "0", 6)));
1475
+ if (result.fee) {
1476
+ log(chalk.blue("Fee (1%): ") + chalk.yellow(formatCurrency(result.fee, 6)));
1477
+ }
1478
+ if (result.newPrice) {
1479
+ log(chalk.blue("New price: ") + chalk.green(formatCurrency(result.newPrice)));
1480
+ }
1481
+ if (result.newMcap) {
1482
+ log(chalk.blue("New market cap: ") +
1483
+ chalk.yellow(formatCurrency(result.newMcap)));
1484
+ }
1485
+ if (result.graduationProgress !== undefined) {
1486
+ log(chalk.blue("Graduation: ") +
1487
+ chalk.magenta(result.graduationProgress.toFixed(2) + "%"));
1488
+ }
1489
+ if (result.txHash) {
1490
+ log(chalk.blue("Transaction: ") + chalk.green(result.txHash));
1491
+ }
1492
+ else if (result.usdcTransferTx) {
1493
+ log(chalk.blue("Transaction: ") + chalk.green(result.usdcTransferTx));
1494
+ }
1495
+ // Graduation progress bar
1496
+ if (result.graduationProgress !== undefined) {
1497
+ const barLength = 20;
1498
+ const filled = Math.floor((result.graduationProgress / 100) * barLength);
1499
+ const empty = barLength - filled;
1500
+ const bar = "🐱" + "=".repeat(filled) + ">" + " ".repeat(empty);
1501
+ log(chalk.yellow(`[${bar}] ${chalk.bold(result.graduationProgress.toFixed(2) + "%")} to moon!`));
1502
+ }
1503
+ // Display price impact warning for Uniswap swaps
1504
+ if (result.source === "uniswap_v2" && result.priceImpact !== undefined) {
1505
+ if (result.priceImpact < 0.5) {
1506
+ log(chalk.green(`Price Impact: ${result.priceImpact.toFixed(2)}%`));
1507
+ }
1508
+ else if (result.priceImpact < 1) {
1509
+ log(chalk.yellow(`Price Impact: ${result.priceImpact.toFixed(2)}%`));
1510
+ }
1511
+ else if (result.priceImpact < 3) {
1512
+ log(chalk.red(`⚠️ HIGH Price Impact: ${result.priceImpact.toFixed(2)}%`));
1513
+ }
1514
+ else {
1515
+ log(chalk.red(`🚨 VERY HIGH Price Impact: ${result.priceImpact.toFixed(2)}%`));
1516
+ }
1517
+ }
1518
+ // Display route info for Uniswap swaps
1519
+ if (result.source === "uniswap_v2" && result.route) {
1520
+ const feePercent = result.routeFee
1521
+ ? (result.routeFee / 10000).toFixed(2)
1522
+ : "N/A";
1523
+ log(chalk.cyan(`Route: ${result.route} (${feePercent}% fee)`));
1524
+ }
419
1525
  }
420
1526
  function displayTokenInfoToLog(info, log, logLines) {
421
- log(chalk.cyan.bold(info.name) + ` (${info.symbol})`);
422
- if (info.tokenAddress) {
423
- log(chalk.dim("Address: ") + info.tokenAddress);
424
- }
425
- log(chalk.dim("Price: ") + chalk.cyan(info.currentPrice));
426
- log(chalk.dim("Market Cap: ") + chalk.yellow(info.marketCap));
427
- log(chalk.dim("Supply: ") + info.totalSupply);
1527
+ log("");
1528
+ // Token header box
1529
+ const priceDisplay = info.status === "graduated"
1530
+ ? chalk.dim("--")
1531
+ : info.price
1532
+ ? formatCurrency(info.price)
1533
+ : chalk.dim("N/A");
1534
+ const mcapDisplay = info.status === "graduated"
1535
+ ? chalk.dim("--")
1536
+ : info.mcap
1537
+ ? formatCurrency(info.mcap)
1538
+ : chalk.dim("N/A");
1539
+ // Create box-like display
1540
+ log(chalk.green.bold(`📊 ${info.name} (${info.symbol})`));
1541
+ log(chalk.dim("─".repeat(60)));
1542
+ log(chalk.blue("Address: ") + chalk.green(formatAddress(info.address || "")));
1543
+ log(chalk.blue("Status: ") + chalk.green(info.status || "unknown"));
1544
+ log(chalk.blue("Price: ") + chalk.green(priceDisplay));
1545
+ log(chalk.blue("Market Cap: ") + chalk.yellow(mcapDisplay));
1546
+ log(chalk.blue("Total Supply: ") +
1547
+ chalk.green(formatTokenAmount(info.totalSupply || "0")));
1548
+ log(chalk.blue("Created: ") +
1549
+ chalk.green(new Date(info.createdAt || Date.now()).toLocaleString()));
1550
+ log("");
1551
+ // Graduation progress
428
1552
  if (info.graduationProgress !== undefined) {
429
- log(chalk.dim("Graduation: ") +
430
- chalk.magenta(info.graduationProgress.toFixed(2) + "%"));
1553
+ const barLength = 20;
1554
+ const filled = Math.floor((info.graduationProgress / 100) * barLength);
1555
+ const empty = barLength - filled;
1556
+ const bar = "🐱" + "=".repeat(filled) + ">" + " ".repeat(empty);
1557
+ log(chalk.yellow(`[${bar}] ${chalk.bold(info.graduationProgress.toFixed(2) + "%")} to moon!`));
1558
+ log("");
431
1559
  }
1560
+ // Position display
432
1561
  if (info.userPosition && info.userPosition.tokensOwned !== "0") {
433
- log(chalk.dim("Your position: ") +
434
- chalk.green(info.userPosition.tokensOwned + " tokens"));
1562
+ log(chalk.green.bold("💼 Your Position"));
1563
+ log(chalk.dim("".repeat(60)));
1564
+ log(chalk.blue("Tokens Owned: ") +
1565
+ chalk.green(formatTokenAmount(info.userPosition.tokensOwned)));
1566
+ log(chalk.blue("Invested: ") +
1567
+ chalk.green(formatCurrency(info.userPosition.usdcInvested || "0")));
1568
+ if (info.userPosition.currentValue) {
1569
+ log(chalk.blue("Current Value: ") +
1570
+ chalk.green(formatCurrency(info.userPosition.currentValue)));
1571
+ }
1572
+ if (info.userPosition.pnl) {
1573
+ const pnl = parseFloat(info.userPosition.pnl);
1574
+ const pnlColor = pnl >= 0 ? chalk.green : chalk.red;
1575
+ const pnlSign = pnl >= 0 ? "+" : "";
1576
+ const pnlIcon = pnl >= 0 ? "🟢" : "🔴";
1577
+ log(chalk.blue("P&L: ") +
1578
+ `${pnlIcon} ${pnlColor(pnlSign + formatCurrency(info.userPosition.pnl))}`);
1579
+ }
1580
+ log("");
435
1581
  }
436
1582
  }
437
- function displayTokenListToLog(result, log, logLines) {
438
- log(chalk.cyan.bold(`Tokens (${result.tokens.length} total):`));
439
- log("");
440
- for (const token of result.tokens.slice(0, 10)) {
441
- log(` ${chalk.cyan(token.symbol.padEnd(8))} ${token.name.padEnd(20)} ${chalk.yellow(token.marketCap || "N/A")}`);
1583
+ function displayTokenListToLog(result, log, logLines, logLinesSmooth) {
1584
+ // Collect all lines first for smooth scrolling
1585
+ const allLines = [];
1586
+ allLines.push("");
1587
+ allLines.push(chalk.green.bold(`Tokens (${result.total || result.tokens.length} total):`));
1588
+ if (result.page && result.pages) {
1589
+ allLines.push(chalk.dim(`Page ${result.page} of ${result.pages}`));
1590
+ }
1591
+ allLines.push("");
1592
+ if (result.tokens.length === 0) {
1593
+ allLines.push(chalk.yellow("No tokens found."));
1594
+ // Use smooth scrolling if available, otherwise regular logLines
1595
+ if (logLinesSmooth) {
1596
+ logLinesSmooth(allLines, false);
1597
+ }
1598
+ else {
1599
+ logLines(allLines);
1600
+ }
1601
+ return;
442
1602
  }
443
- if (result.tokens.length > 10) {
444
- log(chalk.dim(` ... and ${result.tokens.length - 10} more`));
1603
+ // Show header
1604
+ 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")}`);
1605
+ allLines.push(chalk.dim(" " + "─".repeat(90)));
1606
+ // Show tokens
1607
+ for (const token of result.tokens) {
1608
+ const graduationIcon = token.status === "graduated"
1609
+ ? "[G]"
1610
+ : token.graduationProgress >= 90
1611
+ ? "[H]"
1612
+ : token.graduationProgress >= 50
1613
+ ? "[M]"
1614
+ : "[L]";
1615
+ const mcapDisplay = token.status === "graduated"
1616
+ ? chalk.dim("--")
1617
+ : formatCurrency(token.mcap || "0");
1618
+ const priceDisplay = token.status === "graduated"
1619
+ ? chalk.dim("--")
1620
+ : formatCurrency(token.price || "0");
1621
+ const graduationDisplay = `${graduationIcon} ${(token.graduationProgress || 0).toFixed(1)}%`;
1622
+ const statusDisplay = token.status === "graduated"
1623
+ ? chalk.green("✓ Graduated")
1624
+ : chalk.yellow("Active");
1625
+ allLines.push(` ${chalk.bold(token.name.padEnd(20))} ${chalk.green(token.symbol.padEnd(10))} ${mcapDisplay.padEnd(15)} ${priceDisplay.padEnd(12)} ${graduationDisplay.padEnd(12)} ${statusDisplay}`);
1626
+ }
1627
+ allLines.push("");
1628
+ if (result.page && result.pages && result.page < result.pages) {
1629
+ allLines.push(chalk.dim(`Use --page ${result.page + 1} to see more tokens`));
1630
+ allLines.push("");
1631
+ }
1632
+ // Show example commands
1633
+ if (result.tokens.length > 0) {
1634
+ const firstToken = result.tokens[0];
1635
+ allLines.push(chalk.dim("Example commands:"));
1636
+ allLines.push(chalk.dim(` buy ${firstToken.symbol}`));
1637
+ allLines.push(chalk.dim(` sell ${firstToken.symbol}`));
1638
+ allLines.push(chalk.dim(` info ${firstToken.symbol}`));
1639
+ allLines.push("");
1640
+ }
1641
+ // Use smooth scrolling if available (scrolls to show the list header), otherwise regular logLines
1642
+ if (logLinesSmooth) {
1643
+ logLinesSmooth(allLines, true); // scrollToTop=true to show the list header
1644
+ }
1645
+ else {
1646
+ logLines(allLines);
445
1647
  }
446
1648
  }
447
1649
  function displayPositionsToLog(result, log, logLines) {
@@ -449,27 +1651,61 @@ function displayPositionsToLog(result, log, logLines) {
449
1651
  log(chalk.yellow("No positions found."));
450
1652
  return;
451
1653
  }
452
- log(chalk.cyan.bold(`Your Positions (${result.positions.length}):`));
1654
+ log(chalk.green.bold(`Your Positions (${result.positions.length}):`));
453
1655
  log("");
454
1656
  for (const pos of result.positions) {
455
- log(` ${chalk.cyan(pos.symbol || "???")} - ${chalk.green(pos.tokensOwned + " tokens")} @ ${chalk.yellow(pos.currentValue || "N/A")}`);
1657
+ const tokenName = pos.token?.name || "???";
1658
+ const tokenSymbol = pos.token?.symbol || "???";
1659
+ const tokensDisplay = formatTokenAmount(pos.tokensOwned || "0");
1660
+ const valueDisplay = pos.currentValue
1661
+ ? formatCurrency(pos.currentValue)
1662
+ : "N/A";
1663
+ log(` ${chalk.green.bold(`${tokenName} (${tokenSymbol})`)} - ${chalk.green(`${tokensDisplay} tokens`)} @ ${chalk.yellow(valueDisplay)}`);
456
1664
  }
457
1665
  }
458
1666
  function displayHealthStatusToLog(health, log, logLines) {
459
- const statusColor = health.status === "healthy" ? chalk.green : chalk.red;
1667
+ const statusColor = health.status === "ok" || health.status === "healthy"
1668
+ ? chalk.green
1669
+ : chalk.red;
460
1670
  log(statusColor(`Agent Status: ${health.status}`));
461
1671
  if (health.version) {
462
- log(chalk.dim("Version: ") + health.version);
1672
+ log(chalk.blue("Version: ") + chalk.green(health.version));
463
1673
  }
464
1674
  if (health.uptime) {
465
- log(chalk.dim("Uptime: ") + health.uptime);
1675
+ log(chalk.blue("Uptime: ") + chalk.green(health.uptime));
466
1676
  }
467
1677
  }
468
1678
  function displayBalanceToLog(balance, log, logLines) {
469
- log(chalk.cyan.bold("Wallet Balance:"));
470
- log(chalk.dim("Address: ") + chalk.cyan(balance.address));
471
- log(chalk.dim("ETH: ") + chalk.yellow(balance.ethBalance));
472
- log(chalk.dim("USDC: ") + chalk.green(balance.usdcBalance));
1679
+ log("");
1680
+ log(chalk.green.bold("Wallet Balance:"));
1681
+ log(chalk.blue("Address: ") + chalk.green(balance.address));
1682
+ log(chalk.blue("ETH: ") +
1683
+ chalk.yellow(balance.ethFormatted || balance.ethBalance));
1684
+ log(chalk.blue("USDC: ") +
1685
+ chalk.green(balance.usdcFormatted || balance.usdcBalance));
1686
+ if (balance.cat402Formatted) {
1687
+ log(chalk.blue("CAT: ") + chalk.cyan(balance.cat402Formatted));
1688
+ }
1689
+ log("");
1690
+ // Show warnings if balances are low
1691
+ const ethNum = Number(balance.ethFormatted || balance.ethBalance);
1692
+ const usdcNum = Number((balance.usdcFormatted || balance.usdcBalance)
1693
+ .replace("$", "")
1694
+ .replace(",", ""));
1695
+ if (ethNum < 0.001 && usdcNum < 1) {
1696
+ log(chalk.yellow("⚠️ Low balances detected!"));
1697
+ log(chalk.dim(" You need Base Sepolia ETH for gas fees"));
1698
+ log(chalk.dim(" You need Base Sepolia USDC for trading"));
1699
+ log("");
1700
+ }
1701
+ else if (ethNum < 0.001) {
1702
+ log(chalk.yellow("⚠️ Low ETH balance - you may not have enough for gas fees"));
1703
+ log("");
1704
+ }
1705
+ else if (usdcNum < 1) {
1706
+ log(chalk.yellow("⚠️ Low USDC balance - you may not have enough for trades"));
1707
+ log("");
1708
+ }
473
1709
  }
474
1710
  function extractFlag(args, flag) {
475
1711
  const index = args.findIndex((arg) => arg === flag);
@@ -478,4 +1714,589 @@ function extractFlag(args, flag) {
478
1714
  }
479
1715
  return undefined;
480
1716
  }
1717
+ function displayFeesToLog(fees, log, logLines) {
1718
+ const hasFeesToken = BigInt(fees.feeToken || "0") > 0n;
1719
+ const hasFeesPaired = BigInt(fees.feePaired || "0") > 0n;
1720
+ const hasFees = hasFeesToken || hasFeesPaired;
1721
+ log("");
1722
+ log(chalk.green.bold(`💰 Accumulated Fees: ${fees.tokenName} (${fees.tokenSymbol})`));
1723
+ log(chalk.blue("━".repeat(50)));
1724
+ log(chalk.blue("Contract: ") + chalk.green(fees.tokenAddress));
1725
+ log(chalk.blue("LP Status: ") +
1726
+ chalk.green(fees.isLocked ? "🔒 Locked" : "❌ Not Locked"));
1727
+ if (typeof fees.v4TickLower === "number" &&
1728
+ typeof fees.v4TickUpper === "number" &&
1729
+ typeof fees.v4TickCurrent === "number" &&
1730
+ typeof fees.v4InRange === "boolean") {
1731
+ log(chalk.blue("V4 Range: ") +
1732
+ chalk.green(`[${fees.v4TickLower}, ${fees.v4TickUpper}) | Current: ${fees.v4TickCurrent} | In Range: ${fees.v4InRange ? "✅" : "❌"}`));
1733
+ }
1734
+ log("");
1735
+ log(chalk.green("Total Fees:"));
1736
+ log(chalk.blue(" Tokens: ") + chalk.green(hasFeesToken ? fees.feeToken : "0"));
1737
+ log(chalk.blue(" USDC: ") +
1738
+ chalk.green(hasFeesPaired ? fees.feePaired : "$0.00"));
1739
+ log("");
1740
+ log(chalk.green("Creator Share (80%):"));
1741
+ log(chalk.blue(" Tokens: ") + chalk.green(fees.creatorToken || "0"));
1742
+ log(chalk.blue(" USDC: ") + chalk.green(fees.creatorPaired || "$0.00"));
1743
+ log("");
1744
+ log(chalk.green("Platform Share (20%):"));
1745
+ log(chalk.blue(" Tokens: ") + chalk.green(fees.platformToken || "0"));
1746
+ log(chalk.blue(" USDC: ") + chalk.green(fees.platformPaired || "$0.00"));
1747
+ log(chalk.blue("━".repeat(50)));
1748
+ if (!hasFees) {
1749
+ if (fees.v4InRange === false) {
1750
+ log(chalk.yellow("\n⚠️ No fees accrued: position is out of range"));
1751
+ }
1752
+ else {
1753
+ log(chalk.yellow("\n⚠️ No fees accumulated yet. Trade volume needed."));
1754
+ }
1755
+ }
1756
+ else {
1757
+ log(chalk.blue("\n💡 Run with --execute to claim fees"));
1758
+ }
1759
+ }
1760
+ function displayClaimResultToLog(result, log, logLines) {
1761
+ log(chalk.green("✅ Fees claimed successfully!"));
1762
+ log(chalk.blue("Token: ") + chalk.green(result.tokenAddress));
1763
+ log(chalk.blue("Transaction: ") + chalk.green(result.txHash));
1764
+ log(chalk.blue("Claimed:"));
1765
+ log(chalk.blue(" Tokens: ") + chalk.green(result.creatorToken || "0"));
1766
+ log(chalk.blue(" USDC: ") + chalk.green(result.creatorPaired || "$0.00"));
1767
+ }
1768
+ function displayTransactionsToLog(result, log, logLines) {
1769
+ log(chalk.green.bold(`📜 Transactions (${result.total} total)`));
1770
+ log("");
1771
+ if (result.transactions.length === 0) {
1772
+ log(chalk.yellow("No transactions found."));
1773
+ return;
1774
+ }
1775
+ for (let i = 0; i < result.transactions.length; i++) {
1776
+ const tx = result.transactions[i];
1777
+ const typeIcon = tx.type === "buy" ? "🟢" : tx.type === "sell" ? "🔴" : "🎁";
1778
+ const statusIcon = tx.status === "success" ? "✅" : tx.status === "pending" ? "⏳" : "❌";
1779
+ log(chalk.green.bold(`${i + 1}. ${typeIcon} ${tx.type.toUpperCase()} ${statusIcon} ${tx.status.toUpperCase()}`));
1780
+ if (tx.token) {
1781
+ log(chalk.blue(` Token: ${tx.token.name} (${tx.token.symbol})`));
1782
+ }
1783
+ log(chalk.blue(` Amount: ${tx.amount}`));
1784
+ log(chalk.blue(` Fee: ${tx.fee}`));
1785
+ if (tx.txHash) {
1786
+ log(chalk.blue(` TX: ${tx.txHash}`));
1787
+ }
1788
+ log(chalk.blue(` Date: ${new Date(tx.createdAt).toLocaleString()}`));
1789
+ log("");
1790
+ }
1791
+ if (result.hasMore) {
1792
+ log(chalk.blue(`Showing ${result.offset + 1}-${result.offset + result.transactions.length} of ${result.total}`));
1793
+ log(chalk.blue(`Use --offset ${result.offset + result.limit} to see more`));
1794
+ }
1795
+ }
1796
+ function displayAccountInfoToLog(data, log, logLines) {
1797
+ const { account, balance } = data;
1798
+ log("");
1799
+ log(chalk.green.bold("👤 Account Information"));
1800
+ log(chalk.blue("=".repeat(50)));
1801
+ log("");
1802
+ log(chalk.blue("Account Index: ") + chalk.green(account.index.toString()));
1803
+ log(chalk.blue("Type: ") +
1804
+ chalk.green(account.type === "custom" ? "Custom" : "Seed-Derived"));
1805
+ log(chalk.blue("Address: ") + chalk.green(account.address));
1806
+ if (account.label) {
1807
+ log(chalk.blue("Label: ") + chalk.green(account.label));
1808
+ }
1809
+ log("");
1810
+ log(chalk.green.bold("💰 Balances"));
1811
+ log(chalk.blue("ETH: ") + chalk.yellow(balance.ethBalance));
1812
+ log(chalk.blue("USDC: ") + chalk.green(balance.usdcBalance));
1813
+ if (balance.cat402Formatted) {
1814
+ log(chalk.blue("CAT: ") + chalk.green(balance.cat402Formatted));
1815
+ }
1816
+ log("");
1817
+ if (data.positions && data.positions.positions.length > 0) {
1818
+ log(chalk.green.bold("💼 Positions"));
1819
+ log("");
1820
+ displayPositionsToLog(data.positions, log, logLines);
1821
+ }
1822
+ }
1823
+ // Helper function to format time remaining for lease
1824
+ function formatTimeRemaining(expiresAt) {
1825
+ const now = new Date();
1826
+ const diffMs = expiresAt.getTime() - now.getTime();
1827
+ if (diffMs <= 0) {
1828
+ return "expired";
1829
+ }
1830
+ const minutes = Math.floor(diffMs / 60000);
1831
+ const seconds = Math.floor((diffMs % 60000) / 1000);
1832
+ if (minutes > 0) {
1833
+ return `${minutes}m ${seconds}s`;
1834
+ }
1835
+ return `${seconds}s`;
1836
+ }
1837
+ // Helper function to build chat header content
1838
+ async function buildChatHeaderContent(theme, tokenInfo, tokenIdentifier, isConnected, leaseInfo, getCyanColor) {
1839
+ const colorTag = theme === "dark" ? "green-fg" : "black-fg";
1840
+ const cyanColor = getCyanColor(theme);
1841
+ const lines = [];
1842
+ // Title line
1843
+ if (tokenInfo) {
1844
+ lines.push(`{${colorTag}}💬 httpcat Chat: {${cyanColor}}${tokenInfo.name} (${tokenInfo.symbol}){/${cyanColor}}{/${colorTag}}`);
1845
+ }
1846
+ else if (tokenIdentifier) {
1847
+ // Fallback to identifier if token lookup failed
1848
+ const displayId = tokenIdentifier.length > 20
1849
+ ? `${tokenIdentifier.slice(0, 10)}...${tokenIdentifier.slice(-6)}`
1850
+ : tokenIdentifier;
1851
+ lines.push(`{${colorTag}}💬 httpcat Chat: {${cyanColor}}${displayId}{/${cyanColor}}{/${colorTag}}`);
1852
+ }
1853
+ else {
1854
+ lines.push(`{${colorTag}}💬 httpcat Chat: General{/${colorTag}}`);
1855
+ }
1856
+ lines.push(`{${colorTag}}${"═".repeat(60)}{/${colorTag}}`);
1857
+ lines.push("");
1858
+ // Connection status
1859
+ if (isConnected) {
1860
+ lines.push(`{green-fg}✅ Connected to chat stream{/green-fg}`);
1861
+ lines.push("");
1862
+ }
1863
+ // Lease info
1864
+ if (leaseInfo) {
1865
+ const timeRemaining = formatTimeRemaining(leaseInfo.leaseExpiresAt);
1866
+ const isExpired = leaseInfo.leaseExpiresAt.getTime() <= Date.now();
1867
+ const timePrefix = isExpired ? "⚠️ " : "⏱️ ";
1868
+ lines.push(`{yellow-fg}${timePrefix}Lease expires in: ${timeRemaining}{/yellow-fg}`);
1869
+ }
1870
+ lines.push("");
1871
+ lines.push(`{${cyanColor}}💡 Type your message and press Enter to send{/${cyanColor}}`);
1872
+ lines.push(`{${cyanColor}}💡 Type /exit to return to shell{/${cyanColor}}`);
1873
+ lines.push(`{${cyanColor}}💡 Type /renew to renew your lease{/${cyanColor}}`);
1874
+ if (tokenIdentifier) {
1875
+ lines.push(`{${cyanColor}}💡 Type /buy <amount> to buy tokens{/${cyanColor}}`);
1876
+ lines.push(`{${cyanColor}}💡 Type /sell <amount> to sell tokens{/${cyanColor}}`);
1877
+ }
1878
+ return lines.join("\n");
1879
+ }
1880
+ // Simplified chat mode that works within the shell
1881
+ async function startChatInShell(client, tokenIdentifier, log, logLines, screen, inputBox, originalSubmitHandler, headerBox, buildWelcomeContent, currentTheme, catFaces, currentCatIndex) {
1882
+ const privateKey = config.getPrivateKey();
1883
+ const account = privateKeyToAccount(privateKey);
1884
+ const userAddress = account.address;
1885
+ let leaseInfo = null;
1886
+ let ws = null;
1887
+ let isInChatMode = true;
1888
+ let isProcessing = false; // Flag to prevent duplicate processing
1889
+ const displayedMessageIds = new Set();
1890
+ // Helper to format chat messages
1891
+ const formatChatMessage = (msg, isOwn = false) => {
1892
+ const date = new Date(msg.timestamp);
1893
+ const now = new Date();
1894
+ const diffMs = now.getTime() - date.getTime();
1895
+ const diffSec = Math.floor(diffMs / 1000);
1896
+ const diffMin = Math.floor(diffSec / 60);
1897
+ let timeStr = "just now";
1898
+ if (diffSec >= 60) {
1899
+ timeStr =
1900
+ diffMin < 60
1901
+ ? `${diffMin}m ago`
1902
+ : date.toLocaleTimeString("en-US", {
1903
+ hour: "2-digit",
1904
+ minute: "2-digit",
1905
+ });
1906
+ }
1907
+ const authorColor = isOwn ? chalk.green : chalk.cyan;
1908
+ const authorShort = msg.authorShort || formatAddress(msg.author, 6);
1909
+ return `${chalk.dim(`[${timeStr}]`)} ${authorColor(authorShort)}: ${msg.message}`;
1910
+ };
1911
+ // Get token information if it's a token chat
1912
+ let tokenInfo = null;
1913
+ if (tokenIdentifier) {
1914
+ try {
1915
+ const info = await getTokenInfo(client, tokenIdentifier, userAddress, true);
1916
+ tokenInfo = { name: info.name, symbol: info.symbol };
1917
+ }
1918
+ catch (error) {
1919
+ // Token lookup failed - will use identifier in header
1920
+ tokenInfo = null;
1921
+ }
1922
+ }
1923
+ // Helper to get cyan color based on theme
1924
+ const getCyanColor = (theme) => theme === "dark" ? "light-cyan-fg" : "cyan-fg";
1925
+ // Helper to update header
1926
+ const updateChatHeader = async (isConnected = false) => {
1927
+ const content = await buildChatHeaderContent(currentTheme, tokenInfo, tokenIdentifier, isConnected, leaseInfo, getCyanColor);
1928
+ headerBox.setContent(content);
1929
+ screen.render();
1930
+ };
1931
+ // Update header to show chat mode
1932
+ await updateChatHeader(false);
1933
+ // Set up header update interval for lease countdown
1934
+ let headerUpdateInterval = null;
1935
+ try {
1936
+ // Join chat
1937
+ log(chalk.blue("Joining chat..."));
1938
+ screen.render();
1939
+ const joinResult = await joinChat(client, userAddress, tokenIdentifier, true);
1940
+ leaseInfo = {
1941
+ leaseId: joinResult.leaseId,
1942
+ leaseExpiresAt: new Date(joinResult.leaseExpiresAt),
1943
+ };
1944
+ // Update header with lease info
1945
+ await updateChatHeader(false);
1946
+ // Display last messages
1947
+ if (joinResult.lastMessages.length > 0) {
1948
+ log(chalk.dim("─".repeat(60)));
1949
+ log(chalk.cyan.bold("Recent messages:"));
1950
+ for (const msg of joinResult.lastMessages) {
1951
+ displayedMessageIds.add(msg.messageId);
1952
+ const isOwn = msg.author.toLowerCase() === userAddress.toLowerCase();
1953
+ log(formatChatMessage(msg, isOwn));
1954
+ }
1955
+ log(chalk.dim("─".repeat(60)));
1956
+ }
1957
+ // Connect to WebSocket
1958
+ const agentUrl = client.getAgentUrl();
1959
+ const normalizedWsUrl = normalizeWebSocketUrl(joinResult.wsUrl, agentUrl);
1960
+ const wsUrlObj = new URL(normalizedWsUrl);
1961
+ wsUrlObj.searchParams.set("leaseId", joinResult.leaseId);
1962
+ await new Promise((resolve) => setTimeout(resolve, 500));
1963
+ ws = new WebSocket(wsUrlObj.toString());
1964
+ ws.on("open", () => {
1965
+ log(chalk.green("✅ Connected to chat stream"));
1966
+ updateChatHeader(true);
1967
+ screen.render();
1968
+ });
1969
+ ws.on("message", (data) => {
1970
+ try {
1971
+ const event = JSON.parse(data.toString());
1972
+ if (event.type === "message" && event.data) {
1973
+ const msg = {
1974
+ ...event.data,
1975
+ authorShort: formatAddress(event.data.author, 6),
1976
+ };
1977
+ if (!displayedMessageIds.has(msg.messageId)) {
1978
+ displayedMessageIds.add(msg.messageId);
1979
+ const isOwn = msg.author.toLowerCase() === userAddress.toLowerCase();
1980
+ log(formatChatMessage(msg, isOwn));
1981
+ screen.render();
1982
+ }
1983
+ }
1984
+ else if (event.type === "lease_expired") {
1985
+ log(chalk.yellow("⏱️ Your lease has expired. Type /renew to continue."));
1986
+ leaseInfo = null;
1987
+ updateChatHeader(true);
1988
+ screen.render();
1989
+ }
1990
+ }
1991
+ catch (error) {
1992
+ // Ignore parse errors
1993
+ }
1994
+ });
1995
+ ws.on("error", (error) => {
1996
+ log(chalk.red(`❌ WebSocket error: ${error.message}`));
1997
+ screen.render();
1998
+ });
1999
+ ws.on("close", () => {
2000
+ if (isInChatMode) {
2001
+ log(chalk.yellow("⚠️ WebSocket connection closed"));
2002
+ updateChatHeader(false);
2003
+ screen.render();
2004
+ }
2005
+ });
2006
+ // Start header update interval
2007
+ headerUpdateInterval = setInterval(async () => {
2008
+ if (leaseInfo && isInChatMode) {
2009
+ await updateChatHeader(true);
2010
+ }
2011
+ }, 1000);
2012
+ // Wait for connection
2013
+ await new Promise((resolve, reject) => {
2014
+ const timeout = setTimeout(() => reject(new Error("Connection timeout")), 10000);
2015
+ ws.once("open", () => {
2016
+ clearTimeout(timeout);
2017
+ resolve();
2018
+ });
2019
+ ws.once("error", (err) => {
2020
+ clearTimeout(timeout);
2021
+ reject(err);
2022
+ });
2023
+ });
2024
+ // Set up chat input handler
2025
+ const chatInputHandler = async (value) => {
2026
+ const trimmed = value.trim();
2027
+ // Clear input immediately
2028
+ inputBox.clearValue();
2029
+ screen.render();
2030
+ if (!trimmed) {
2031
+ isProcessing = false;
2032
+ inputBox.focus();
2033
+ screen.render();
2034
+ return;
2035
+ }
2036
+ // Handle commands
2037
+ if (trimmed.startsWith("/")) {
2038
+ const [cmd, ...cmdArgs] = trimmed.split(" ");
2039
+ switch (cmd) {
2040
+ case "/exit":
2041
+ case "/quit":
2042
+ isInChatMode = false;
2043
+ if (headerUpdateInterval) {
2044
+ clearInterval(headerUpdateInterval);
2045
+ headerUpdateInterval = null;
2046
+ }
2047
+ if (ws) {
2048
+ ws.close();
2049
+ ws = null;
2050
+ }
2051
+ log(chalk.yellow("Exited chat mode. Back to shell."));
2052
+ log("");
2053
+ // Restore original header
2054
+ const originalContent = await buildWelcomeContent(currentTheme, catFaces[currentCatIndex].face);
2055
+ headerBox.setContent(originalContent);
2056
+ // Restore original input handler
2057
+ inputBox.removeAllListeners("submit");
2058
+ inputBox.on("submit", originalSubmitHandler);
2059
+ isProcessing = false;
2060
+ inputBox.focus();
2061
+ screen.render();
2062
+ return;
2063
+ case "/renew":
2064
+ try {
2065
+ log(chalk.blue("Renewing lease..."));
2066
+ screen.render();
2067
+ const renewal = await renewLease(client, userAddress, leaseInfo?.leaseId);
2068
+ leaseInfo = {
2069
+ leaseId: renewal.leaseId,
2070
+ leaseExpiresAt: new Date(renewal.leaseExpiresAt),
2071
+ };
2072
+ await updateChatHeader(true);
2073
+ log(chalk.green("✅ Lease renewed!"));
2074
+ screen.render();
2075
+ }
2076
+ catch (error) {
2077
+ log(chalk.red(`❌ Renew failed: ${error instanceof Error ? error.message : String(error)}`));
2078
+ screen.render();
2079
+ }
2080
+ isProcessing = false;
2081
+ inputBox.focus();
2082
+ screen.render();
2083
+ return;
2084
+ case "/help":
2085
+ log(chalk.cyan("Chat commands: /exit, /quit, /renew, /help"));
2086
+ screen.render();
2087
+ isProcessing = false;
2088
+ inputBox.focus();
2089
+ screen.render();
2090
+ return;
2091
+ default:
2092
+ log(chalk.yellow(`Unknown command: ${cmd}. Type /help for commands.`));
2093
+ screen.render();
2094
+ isProcessing = false;
2095
+ inputBox.focus();
2096
+ screen.render();
2097
+ return;
2098
+ }
2099
+ }
2100
+ // Send message
2101
+ if (!leaseInfo || leaseInfo.leaseExpiresAt.getTime() <= Date.now()) {
2102
+ log(chalk.yellow("⏱️ Your lease has expired. Type /renew to continue."));
2103
+ screen.render();
2104
+ isProcessing = false;
2105
+ inputBox.focus();
2106
+ screen.render();
2107
+ return;
2108
+ }
2109
+ try {
2110
+ // Send message - it will appear via WebSocket when received
2111
+ await sendChatMessage(client, trimmed, leaseInfo.leaseId, userAddress);
2112
+ // Re-attach handler for next message
2113
+ inputBox.removeAllListeners("submit");
2114
+ inputBox.once("submit", chatInputHandler);
2115
+ isProcessing = false;
2116
+ inputBox.focus();
2117
+ screen.render();
2118
+ }
2119
+ catch (error) {
2120
+ log(chalk.red(`❌ Send failed: ${error instanceof Error ? error.message : String(error)}`));
2121
+ screen.render();
2122
+ // Re-attach handler for next message
2123
+ inputBox.removeAllListeners("submit");
2124
+ inputBox.once("submit", chatInputHandler);
2125
+ isProcessing = false;
2126
+ inputBox.focus();
2127
+ screen.render();
2128
+ }
2129
+ };
2130
+ // Temporarily replace input handler
2131
+ // Remove all listeners first to ensure clean state
2132
+ inputBox.removeAllListeners("submit");
2133
+ // Clear any existing input value to prevent state issues
2134
+ inputBox.clearValue();
2135
+ // Add the chat handler - only one handler should be active
2136
+ inputBox.once("submit", chatInputHandler);
2137
+ // Ensure input box has focus and is ready
2138
+ inputBox.focus();
2139
+ screen.render();
2140
+ }
2141
+ catch (error) {
2142
+ log(chalk.red(`❌ Chat error: ${error instanceof Error ? error.message : String(error)}`));
2143
+ if (headerUpdateInterval) {
2144
+ clearInterval(headerUpdateInterval);
2145
+ headerUpdateInterval = null;
2146
+ }
2147
+ if (ws)
2148
+ ws.close();
2149
+ // Restore original header on error
2150
+ const originalContent = await buildWelcomeContent(currentTheme, catFaces[currentCatIndex].face);
2151
+ headerBox.setContent(originalContent);
2152
+ // Restore original handler on error
2153
+ inputBox.removeAllListeners("submit");
2154
+ inputBox.on("submit", originalSubmitHandler);
2155
+ screen.render();
2156
+ inputBox.focus();
2157
+ screen.render();
2158
+ }
2159
+ }
2160
+ /**
2161
+ * Start agent chat mode (similar to startChatInShell)
2162
+ */
2163
+ async function startAgentChatMode(client, log, logLines, screen, inputBox, headerBox, buildWelcomeContent, currentTheme, catFaces, currentCatIndex) {
2164
+ let isProcessing = false;
2165
+ // Get agent config
2166
+ const agentConfig = config.getAIAgentConfig();
2167
+ if (!agentConfig) {
2168
+ throw new Error("Cat not configured");
2169
+ }
2170
+ const apiKey = config.getAIAgentApiKey();
2171
+ if (!apiKey) {
2172
+ throw new Error("Failed to get API key");
2173
+ }
2174
+ // Create LLM and agent
2175
+ const llm = createLLM(agentConfig, apiKey);
2176
+ const agent = createHttpcatAgent(client, llm);
2177
+ // Helper to update header
2178
+ const updateAgentHeader = async () => {
2179
+ const colorTag = currentTheme === "dark" ? "green-fg" : "black-fg";
2180
+ const cyanColor = currentTheme === "dark" ? "light-cyan-fg" : "cyan-fg";
2181
+ const lines = [];
2182
+ lines.push(`{${colorTag}}🐱 Cat Chat Mode{/${colorTag}}`);
2183
+ lines.push(`{${colorTag}}${"═".repeat(60)}{/${colorTag}}`);
2184
+ lines.push("");
2185
+ lines.push(`{${cyanColor}}💡 Ask me to buy tokens, check balances, list tokens, and more!{/${cyanColor}}`);
2186
+ lines.push(`{${cyanColor}}💡 Type /exit to return to shell{/${cyanColor}}`);
2187
+ lines.push("");
2188
+ const content = lines.join("\n");
2189
+ headerBox.setContent(content);
2190
+ screen.render();
2191
+ };
2192
+ // Update header to show agent mode
2193
+ await updateAgentHeader();
2194
+ // Store original submit handler
2195
+ const originalHandlers = inputBox.listeners("submit");
2196
+ const originalSubmitHandler = originalHandlers[0];
2197
+ // Set up agent input handler (similar to chat mode)
2198
+ const agentInputHandler = async (value) => {
2199
+ // Guard against double processing
2200
+ if (isProcessing) {
2201
+ return;
2202
+ }
2203
+ isProcessing = true;
2204
+ const trimmed = value.trim();
2205
+ // Clear input immediately
2206
+ inputBox.clearValue();
2207
+ screen.render();
2208
+ if (!trimmed) {
2209
+ isProcessing = false;
2210
+ inputBox.focus();
2211
+ screen.render();
2212
+ return;
2213
+ }
2214
+ // Handle commands
2215
+ if (trimmed.startsWith("/")) {
2216
+ const [cmd] = trimmed.split(" ");
2217
+ switch (cmd) {
2218
+ case "/exit":
2219
+ case "/quit":
2220
+ log(chalk.yellow("Exited cat mode. Back to shell."));
2221
+ log("");
2222
+ // Restore original header
2223
+ const originalContent = await buildWelcomeContent(currentTheme, catFaces[currentCatIndex].face);
2224
+ headerBox.setContent(originalContent);
2225
+ // Restore original input handler
2226
+ inputBox.removeAllListeners("submit");
2227
+ inputBox.on("submit", originalSubmitHandler);
2228
+ isProcessing = false;
2229
+ inputBox.focus();
2230
+ screen.render();
2231
+ return;
2232
+ case "/help":
2233
+ log(chalk.cyan("Cat commands: /exit, /quit, /help"));
2234
+ isProcessing = false;
2235
+ inputBox.focus();
2236
+ screen.render();
2237
+ return;
2238
+ default:
2239
+ log(chalk.yellow(`Unknown command: ${cmd}. Type /help for commands.`));
2240
+ isProcessing = false;
2241
+ inputBox.focus();
2242
+ screen.render();
2243
+ return;
2244
+ }
2245
+ }
2246
+ // Send to agent
2247
+ try {
2248
+ log(chalk.blue("🐱 Cat thinking..."));
2249
+ screen.render();
2250
+ const response = await chatWithAgent(agent, llm, trimmed);
2251
+ log(chalk.green("🐱 Cat:"));
2252
+ log(response);
2253
+ log("");
2254
+ }
2255
+ catch (error) {
2256
+ const errorMsg = error.message || String(error);
2257
+ const errorStack = error.stack || "";
2258
+ // Log full error details for debugging
2259
+ if (process.env.HTTPCAT_DEBUG) {
2260
+ log(chalk.dim(`Debug: Full error: ${JSON.stringify(error, null, 2)}`));
2261
+ if (errorStack) {
2262
+ log(chalk.dim(`Debug: Stack trace: ${errorStack}`));
2263
+ }
2264
+ }
2265
+ log(chalk.red(`❌ Agent error: ${errorMsg}`));
2266
+ // Check for authentication errors
2267
+ const isAuthError = errorMsg.includes("Authentication failed") ||
2268
+ errorMsg.includes("API key") ||
2269
+ errorMsg.includes("401") ||
2270
+ errorMsg.includes("403") ||
2271
+ errorMsg.includes("Unauthorized") ||
2272
+ errorMsg.includes("Invalid API key") ||
2273
+ errorMsg.includes("authentication") ||
2274
+ errorMsg.includes("Invalid API Key") ||
2275
+ errorStack.includes("401") ||
2276
+ errorStack.includes("403");
2277
+ if (isAuthError) {
2278
+ log("");
2279
+ log(chalk.yellow("🔑 Authentication Error Detected"));
2280
+ log(chalk.dim("Your API key may be invalid, expired, or incorrectly configured."));
2281
+ log(chalk.dim("Exit cat mode and run 'agent' (or 'cat') again to re-run the setup wizard."));
2282
+ }
2283
+ log("");
2284
+ }
2285
+ isProcessing = false;
2286
+ inputBox.focus();
2287
+ screen.render();
2288
+ };
2289
+ // Remove original handler and add agent handler
2290
+ inputBox.removeAllListeners("submit");
2291
+ inputBox.on("submit", agentInputHandler);
2292
+ // Clear and focus input
2293
+ inputBox.clearValue();
2294
+ inputBox.focus();
2295
+ screen.render();
2296
+ // Show welcome message
2297
+ log(chalk.green("🐱 Cat chat mode activated!"));
2298
+ log(chalk.dim("Ask me anything about tokens, trading, or your portfolio."));
2299
+ log(chalk.dim("Type /exit to return to shell."));
2300
+ log("");
2301
+ }
481
2302
  //# sourceMappingURL=shell.js.map