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