httpcat-cli 0.0.7 → 0.0.10

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.
@@ -0,0 +1,1916 @@
1
+ import { createInterface } from "readline";
2
+ import WebSocket from "ws";
3
+ import chalk from "chalk";
4
+ // @ts-ignore - neo-blessed doesn't have types, but @types/blessed provides compatible types
5
+ import blessed from "neo-blessed";
6
+ import { formatAddress, formatTokenAmount, formatCurrency } from "../utils/formatting.js";
7
+ import { handleError } from "../utils/errors.js";
8
+ import { config } from "../config.js";
9
+ import { printCat } from "../interactive/art.js";
10
+ import { resolveTokenId } from "../utils/token-resolver.js";
11
+ import { privateKeyToAccount } from "viem/accounts";
12
+ import { buyToken } from "./buy.js";
13
+ import { sellToken, parseTokenAmount } from "./sell.js";
14
+ import { getTokenInfo } from "./info.js";
15
+ const CHAT_JOIN_ENTRYPOINT = "chat_join";
16
+ const CHAT_MESSAGE_ENTRYPOINT = "chat_message";
17
+ const CHAT_RENEW_LEASE_ENTRYPOINT = "chat_renew_lease";
18
+ const LEASE_DURATION_MS = 10 * 60 * 1000; // 10 minutes
19
+ /**
20
+ * Normalize WebSocket URL based on agent URL
21
+ * - Converts protocol (ws:// <-> wss://) to match agent URL
22
+ * - Replaces hostname with agent URL's hostname (handles localhost URLs from backend)
23
+ * - Preserves path and query parameters from WebSocket URL
24
+ */
25
+ function normalizeWebSocketUrl(wsUrl, agentUrl) {
26
+ try {
27
+ const agentUrlObj = new URL(agentUrl);
28
+ const wsUrlObj = new URL(wsUrl);
29
+ // Determine correct protocol based on agent URL
30
+ const shouldUseWss = agentUrlObj.protocol === "https:";
31
+ const shouldUseWs = agentUrlObj.protocol === "http:";
32
+ // Replace hostname with agent URL's hostname
33
+ // This handles cases where backend returns localhost URLs
34
+ wsUrlObj.hostname = agentUrlObj.hostname;
35
+ // Use agent URL's port if it's explicitly set, otherwise use default for protocol
36
+ if (agentUrlObj.port) {
37
+ wsUrlObj.port = agentUrlObj.port;
38
+ }
39
+ else {
40
+ // Remove port to use default (80 for ws, 443 for wss)
41
+ wsUrlObj.port = "";
42
+ }
43
+ // Convert protocol to match agent URL
44
+ if (shouldUseWss) {
45
+ wsUrlObj.protocol = "wss:";
46
+ }
47
+ else if (shouldUseWs) {
48
+ wsUrlObj.protocol = "ws:";
49
+ }
50
+ return wsUrlObj.toString();
51
+ }
52
+ catch (error) {
53
+ // If URL parsing fails, return original
54
+ // This is safer than trying to guess
55
+ return wsUrl;
56
+ }
57
+ }
58
+ export async function joinChat(client, tokenIdentifier, silent = false) {
59
+ let tokenId;
60
+ // If token identifier provided, resolve it to tokenId
61
+ if (tokenIdentifier) {
62
+ // Check if it's an Ethereum address (starts with 0x and is 42 chars)
63
+ const isAddress = /^0x[a-fA-F0-9]{40}$/.test(tokenIdentifier);
64
+ if (isAddress) {
65
+ // Pass address directly - backend should handle it
66
+ tokenId = tokenIdentifier;
67
+ }
68
+ else {
69
+ // Resolve symbol/name/ticker to tokenId
70
+ tokenId = await resolveTokenId(tokenIdentifier, client, silent);
71
+ }
72
+ }
73
+ const input = tokenId ? { tokenId } : {};
74
+ const { data } = await client.invoke(CHAT_JOIN_ENTRYPOINT, input);
75
+ // Format author addresses for last messages
76
+ data.lastMessages = data.lastMessages.map((msg) => ({
77
+ ...msg,
78
+ authorShort: formatAddress(msg.author, 6),
79
+ }));
80
+ return data;
81
+ }
82
+ export async function sendChatMessage(client, message, leaseId) {
83
+ if (!message.trim()) {
84
+ throw new Error("Message cannot be empty");
85
+ }
86
+ const input = { message, leaseId };
87
+ const { data } = await client.invoke(CHAT_MESSAGE_ENTRYPOINT, input);
88
+ return data;
89
+ }
90
+ export async function renewLease(client, leaseId) {
91
+ const input = leaseId ? { leaseId } : {};
92
+ const { data } = await client.invoke(CHAT_RENEW_LEASE_ENTRYPOINT, input);
93
+ return data;
94
+ }
95
+ function formatTimestamp(timestamp) {
96
+ try {
97
+ const date = new Date(timestamp);
98
+ const now = new Date();
99
+ const diffMs = now.getTime() - date.getTime();
100
+ const diffSec = Math.floor(diffMs / 1000);
101
+ const diffMin = Math.floor(diffSec / 60);
102
+ const diffHour = Math.floor(diffMin / 60);
103
+ if (diffSec < 60) {
104
+ return "just now";
105
+ }
106
+ else if (diffMin < 60) {
107
+ return `${diffMin}m ago`;
108
+ }
109
+ else if (diffHour < 24) {
110
+ return `${diffHour}h ago`;
111
+ }
112
+ else {
113
+ return date.toLocaleTimeString("en-US", {
114
+ hour: "2-digit",
115
+ minute: "2-digit",
116
+ });
117
+ }
118
+ }
119
+ catch {
120
+ return "";
121
+ }
122
+ }
123
+ // Track displayed messages to avoid duplicates
124
+ const displayedMessageIds = new Set();
125
+ function formatMessageText(msg, isOwn = false) {
126
+ const timeStr = formatTimestamp(msg.timestamp);
127
+ const authorColor = isOwn ? chalk.green : chalk.cyan;
128
+ const messageColor = isOwn ? chalk.white : chalk.gray;
129
+ const authorShort = msg.authorShort || formatAddress(msg.author, 6);
130
+ return `${chalk.dim(`[${timeStr}]`)} ${authorColor(authorShort)}: ${messageColor(msg.message)}`;
131
+ }
132
+ function displayMessage(msg, isOwn = false, isPending = false, messageLogBox, rl) {
133
+ // Skip if we've already displayed this message
134
+ if (displayedMessageIds.has(msg.messageId)) {
135
+ return;
136
+ }
137
+ displayedMessageIds.add(msg.messageId);
138
+ const messageText = formatMessageText(msg, isOwn);
139
+ if (messageLogBox) {
140
+ // Use blessed log box - automatically handles scrolling
141
+ messageLogBox.log(messageText);
142
+ messageLogBox.setScrollPerc(100); // Auto-scroll to bottom
143
+ }
144
+ else if (rl) {
145
+ // Fallback to readline approach (for JSON mode or edge cases)
146
+ process.stdout.write("\r\x1b[K");
147
+ process.stdout.write(messageText);
148
+ process.stdout.write("\n");
149
+ if (rl)
150
+ rl.prompt(true);
151
+ }
152
+ else {
153
+ // Simple console.log when no UI active
154
+ console.log(messageText);
155
+ }
156
+ }
157
+ function clearLine() {
158
+ process.stdout.write("\r\x1b[K");
159
+ }
160
+ function formatTimeRemaining(expiresAt) {
161
+ const now = new Date();
162
+ const diffMs = expiresAt.getTime() - now.getTime();
163
+ if (diffMs <= 0) {
164
+ return "expired";
165
+ }
166
+ const minutes = Math.floor(diffMs / 60000);
167
+ const seconds = Math.floor((diffMs % 60000) / 1000);
168
+ if (minutes > 0) {
169
+ return `${minutes}m ${seconds}s`;
170
+ }
171
+ return `${seconds}s`;
172
+ }
173
+ /**
174
+ * Format buy result compactly for chat log
175
+ */
176
+ function formatBuyResultCompact(result) {
177
+ const graduationStatus = result.graduationReached
178
+ ? "✅ GRADUATED!"
179
+ : `${result.graduationProgress.toFixed(2)}%`;
180
+ return chalk.green("✅ Buy successful! ") +
181
+ `Received ${chalk.cyan(formatTokenAmount(result.tokensReceived))} tokens, ` +
182
+ `spent ${chalk.yellow(formatCurrency(result.amountSpent))}, ` +
183
+ `new price ${chalk.cyan(formatCurrency(result.newPrice))}, ` +
184
+ `graduation ${chalk.magenta(graduationStatus)}`;
185
+ }
186
+ /**
187
+ * Format sell result compactly for chat log
188
+ */
189
+ function formatSellResultCompact(result) {
190
+ return chalk.green("✅ Sell successful! ") +
191
+ `Sold ${chalk.cyan(formatTokenAmount(result.tokensSold))} tokens, ` +
192
+ `received ${chalk.yellow(formatCurrency(result.usdcReceived))}, ` +
193
+ `new price ${chalk.cyan(formatCurrency(result.newPrice))}, ` +
194
+ `graduation ${chalk.magenta(result.graduationProgress.toFixed(2) + "%")}`;
195
+ }
196
+ function updateHeaderBox(headerBox, leaseInfo, tokenName, isConnected = false) {
197
+ const title = tokenName
198
+ ? `💬 httpcat Chat: ${tokenName}`
199
+ : "💬 httpcat Chat Stream";
200
+ // Get terminal width, default to 80 if not available
201
+ // Account for padding (left + right = 2)
202
+ const terminalWidth = process.stdout.columns || 80;
203
+ const boxWidth = headerBox.width;
204
+ const availableWidth = typeof boxWidth === "number" ? boxWidth - 2 : terminalWidth - 2;
205
+ const separatorWidth = Math.max(20, Math.min(availableWidth, terminalWidth - 2));
206
+ // Use safe separator that won't overflow
207
+ const separator = "═".repeat(separatorWidth);
208
+ const lineSeparator = "─".repeat(separatorWidth);
209
+ // Build content line by line - use plain text (blessed will handle wrapping)
210
+ const lines = [];
211
+ lines.push(title);
212
+ lines.push(separator);
213
+ lines.push("");
214
+ if (isConnected) {
215
+ lines.push("✅ Connected to chat stream");
216
+ lines.push("");
217
+ }
218
+ lines.push("💰 Entry fee: $0.01 USDC (10 min lease)");
219
+ lines.push("💬 Per message: $0.01 USDC");
220
+ if (leaseInfo) {
221
+ const timeRemaining = formatTimeRemaining(leaseInfo.leaseExpiresAt);
222
+ const isExpired = leaseInfo.leaseExpiresAt.getTime() <= Date.now();
223
+ const timePrefix = isExpired ? "⚠️ " : "⏱️ ";
224
+ lines.push(`${timePrefix}Lease expires in: ${timeRemaining}`);
225
+ }
226
+ lines.push("");
227
+ lines.push("💡 Type your message and press Enter to send");
228
+ lines.push("💡 Type /exit or Ctrl+C to quit");
229
+ lines.push("💡 Type /renew to renew your lease");
230
+ if (tokenName) {
231
+ lines.push("💡 Type /buy <amount> to buy tokens");
232
+ lines.push("💡 Type /sell <amount> to sell tokens");
233
+ }
234
+ lines.push("");
235
+ lines.push(lineSeparator);
236
+ // Clear and set content to prevent overlapping
237
+ headerBox.setContent(lines.join("\n"));
238
+ }
239
+ async function ensureLeaseValid(client, leaseInfo, messageLogBox, screen) {
240
+ if (!leaseInfo || leaseInfo.leaseExpiresAt.getTime() <= Date.now()) {
241
+ // Lease expired or doesn't exist, renew it
242
+ if (messageLogBox) {
243
+ messageLogBox.log(chalk.yellow("⏱️ Lease expired. Renewing..."));
244
+ messageLogBox.setScrollPerc(100);
245
+ screen?.render();
246
+ }
247
+ else {
248
+ clearLine();
249
+ console.log(chalk.yellow("⏱️ Lease expired. Renewing..."));
250
+ }
251
+ const renewal = await renewLease(client, leaseInfo?.leaseId);
252
+ return {
253
+ leaseId: renewal.leaseId,
254
+ leaseExpiresAt: new Date(renewal.leaseExpiresAt),
255
+ };
256
+ }
257
+ return leaseInfo;
258
+ }
259
+ export async function startChatStream(client, jsonMode = false, tokenIdentifier) {
260
+ let leaseInfo = null;
261
+ let userAddress;
262
+ let ws = null;
263
+ let wsUrl = null; // Store websocket URL for reconnection
264
+ let rl = null;
265
+ let leaseCheckInterval = null;
266
+ let headerUpdateInterval = null;
267
+ let isExiting = false;
268
+ let isSending = false; // Track sending state across handlers
269
+ let currentInput = ""; // Track current input across handlers
270
+ let pendingMessages = new Map(); // messageId -> tempMessageId (shared between send and receive)
271
+ let pulseIntervals = new Map(); // tempMessageId -> interval
272
+ let isCleaningUp = false; // Flag to prevent intervals from writing during cleanup
273
+ let messageCount = 0; // Track number of messages displayed (for positioning)
274
+ // Blessed UI components (only used in non-JSON mode)
275
+ let screen = null;
276
+ let headerBox = null;
277
+ let messageLogBox = null;
278
+ let inputBox = null;
279
+ // Join chat
280
+ try {
281
+ if (!jsonMode) {
282
+ if (tokenIdentifier) {
283
+ console.log(chalk.dim(`Joining chat room for token: ${tokenIdentifier}...`));
284
+ }
285
+ else {
286
+ console.log(chalk.dim("Joining general chat room..."));
287
+ }
288
+ }
289
+ const joinResult = await joinChat(client, tokenIdentifier, jsonMode);
290
+ leaseInfo = {
291
+ leaseId: joinResult.leaseId,
292
+ leaseExpiresAt: new Date(joinResult.leaseExpiresAt),
293
+ };
294
+ // Initialize userAddress from config if available (fallback until first message)
295
+ if (!userAddress) {
296
+ try {
297
+ const privateKey = config.getPrivateKey();
298
+ if (privateKey) {
299
+ const account = privateKeyToAccount(privateKey);
300
+ userAddress = account.address;
301
+ }
302
+ }
303
+ catch (error) {
304
+ // Ignore - will be set from first message send
305
+ }
306
+ }
307
+ if (jsonMode) {
308
+ // JSON mode: output join result and stream messages as JSON
309
+ console.log(JSON.stringify({
310
+ type: "joined",
311
+ leaseId: joinResult.leaseId,
312
+ leaseExpiresAt: joinResult.leaseExpiresAt,
313
+ tokenId: tokenIdentifier || null,
314
+ lastMessages: joinResult.lastMessages,
315
+ }));
316
+ }
317
+ // Connect to WebSocket
318
+ // Ensure we have a valid lease before connecting
319
+ if (!leaseInfo || leaseInfo.leaseExpiresAt.getTime() <= Date.now()) {
320
+ throw new Error("Lease is invalid or expired. Please try again.");
321
+ }
322
+ // Adjust delay based on whether agent is remote or local
323
+ // Remote servers may need more time for database replication/consistency
324
+ const agentUrl = client.getAgentUrl();
325
+ const isLocalhost = agentUrl.includes("localhost") || agentUrl.includes("127.0.0.1");
326
+ const delay = isLocalhost ? 500 : 1500; // 500ms for local, 1.5s for remote
327
+ await new Promise((resolve) => setTimeout(resolve, delay));
328
+ // Normalize WebSocket URL protocol (ws:// vs wss://) based on agent URL
329
+ // Replaces hostname with agent URL's hostname and converts protocol
330
+ const normalizedWsUrl = normalizeWebSocketUrl(joinResult.wsUrl, agentUrl);
331
+ const wsUrlObj = new URL(normalizedWsUrl);
332
+ wsUrlObj.searchParams.set("leaseId", joinResult.leaseId);
333
+ wsUrl = wsUrlObj.toString();
334
+ // Helper function to attach websocket handlers
335
+ const attachWebSocketHandlers = (websocket) => {
336
+ websocket.on("open", () => {
337
+ // Connection established - update header if using blessed UI
338
+ if (!jsonMode && headerBox) {
339
+ updateHeaderBox(headerBox, leaseInfo, tokenIdentifier, true);
340
+ screen?.render();
341
+ }
342
+ });
343
+ websocket.on("message", async (data) => {
344
+ try {
345
+ const event = JSON.parse(data.toString());
346
+ if (event.type === "message" && event.data) {
347
+ const msg = {
348
+ ...event.data,
349
+ authorShort: formatAddress(event.data.author, 6),
350
+ };
351
+ if (jsonMode) {
352
+ console.log(JSON.stringify({
353
+ type: "message",
354
+ data: msg,
355
+ }));
356
+ }
357
+ else {
358
+ // Normalize addresses to lowercase for case-insensitive comparison
359
+ const normalizedAuthor = msg.author?.toLowerCase();
360
+ const normalizedUserAddress = userAddress?.toLowerCase();
361
+ const isOwn = normalizedAuthor === normalizedUserAddress;
362
+ // Check if this is a message we just sent
363
+ // Method 1: Check if messageId is in pendingMessages (most reliable)
364
+ const hasPendingId = pendingMessages.has(msg.messageId);
365
+ // Method 2: Check if message text is in pendingMessages (fallback if messageId doesn't match)
366
+ const hasPendingText = pendingMessages.has(`text:${msg.message}`);
367
+ // Method 3: Check if author matches AND we're currently sending (fallback)
368
+ const isAuthorMatch = isOwn && isSending;
369
+ // Combine all methods
370
+ const isOurPendingMessage = hasPendingId || hasPendingText || isAuthorMatch;
371
+ if (isOurPendingMessage) {
372
+ // Set cleanup flag to prevent intervals from writing
373
+ isCleaningUp = true;
374
+ // FIRST: Stop ALL pulsing animations immediately (before anything else)
375
+ // This prevents the interval from overwriting our clear
376
+ pulseIntervals.forEach((interval, id) => {
377
+ clearInterval(interval);
378
+ });
379
+ pulseIntervals.clear(); // Clear the map completely
380
+ // Clean up tracking FIRST (so intervals know to stop)
381
+ if (pendingMessages.has(msg.messageId)) {
382
+ pendingMessages.delete(msg.messageId);
383
+ }
384
+ // Also clean up text-based tracking
385
+ if (pendingMessages.has(`text:${msg.message}`)) {
386
+ pendingMessages.delete(`text:${msg.message}`);
387
+ }
388
+ // Small delay to ensure intervals have stopped
389
+ await new Promise((resolve) => setTimeout(resolve, 20));
390
+ // Stop pulsing and clear the grey pulsing input line
391
+ if (inputBox) {
392
+ // Clear input box
393
+ inputBox.clearValue();
394
+ screen?.render();
395
+ }
396
+ else if (rl) {
397
+ // Fallback to readline
398
+ process.stdout.write("\n");
399
+ process.stdout.write("\r\x1b[K");
400
+ }
401
+ // Display the message (only if not already displayed)
402
+ // In JSON mode, displayMessage should never be called, but add explicit check
403
+ if (!displayedMessageIds.has(msg.messageId)) {
404
+ displayMessage(msg, isOwn, false, messageLogBox || undefined, jsonMode ? undefined : (rl || undefined));
405
+ }
406
+ // Re-enable input - clear the input line completely
407
+ isSending = false;
408
+ currentInput = "";
409
+ isCleaningUp = false; // Clear cleanup flag
410
+ if (inputBox && screen) {
411
+ // Clear input box and refocus
412
+ inputBox.clearValue();
413
+ inputBox.focus();
414
+ screen.render();
415
+ }
416
+ else if (rl) {
417
+ // Fallback to readline
418
+ process.stdout.write("\r\x1b[K");
419
+ rl.prompt();
420
+ }
421
+ }
422
+ else {
423
+ // Not our message, just display it normally
424
+ // In JSON mode, displayMessage should never be called, but add explicit check
425
+ if (!displayedMessageIds.has(msg.messageId)) {
426
+ displayMessage(msg, isOwn, false, messageLogBox || undefined, jsonMode ? undefined : (rl || undefined));
427
+ }
428
+ }
429
+ }
430
+ }
431
+ else if (event.type === "lease_expired") {
432
+ if (jsonMode) {
433
+ console.log(JSON.stringify({
434
+ type: "lease_expired",
435
+ }));
436
+ }
437
+ else {
438
+ if (messageLogBox) {
439
+ messageLogBox.log(chalk.yellow("⏱️ Your lease has expired. Type /renew to continue chatting."));
440
+ messageLogBox.setScrollPerc(100);
441
+ screen?.render();
442
+ }
443
+ else if (rl) {
444
+ process.stdout.write("\r\x1b[K");
445
+ console.log(chalk.yellow("⏱️ Your lease has expired. Type /renew to continue chatting."));
446
+ rl.prompt(true);
447
+ }
448
+ else {
449
+ console.log(chalk.yellow("⏱️ Your lease has expired. Type /renew to continue chatting."));
450
+ }
451
+ }
452
+ leaseInfo = null;
453
+ if (headerBox) {
454
+ updateHeaderBox(headerBox, null, tokenIdentifier, true);
455
+ screen?.render();
456
+ }
457
+ }
458
+ else if (event.type === "error") {
459
+ if (jsonMode) {
460
+ console.log(JSON.stringify({
461
+ type: "error",
462
+ error: event.error,
463
+ }));
464
+ }
465
+ else {
466
+ if (messageLogBox) {
467
+ messageLogBox.log(chalk.red(`❌ Error: ${event.error || "Unknown error"}`));
468
+ messageLogBox.setScrollPerc(100);
469
+ screen?.render();
470
+ }
471
+ else if (rl) {
472
+ process.stdout.write("\r\x1b[K");
473
+ console.log(chalk.red(`❌ Error: ${event.error || "Unknown error"}`));
474
+ rl.prompt(true);
475
+ }
476
+ else {
477
+ console.log(chalk.red(`❌ Error: ${event.error || "Unknown error"}`));
478
+ }
479
+ }
480
+ }
481
+ }
482
+ catch (error) {
483
+ // Ignore parse errors
484
+ }
485
+ });
486
+ websocket.on("error", (error) => {
487
+ // Only log errors if not exiting and connection is still relevant
488
+ if (isExiting)
489
+ return;
490
+ if (jsonMode) {
491
+ console.log(JSON.stringify({
492
+ type: "error",
493
+ error: error.message,
494
+ }));
495
+ }
496
+ else {
497
+ const errorMsg = error.message || "Unknown WebSocket error";
498
+ if (messageLogBox) {
499
+ messageLogBox.log(chalk.red(`❌ WebSocket error: ${errorMsg}`));
500
+ messageLogBox.setScrollPerc(100);
501
+ screen?.render();
502
+ }
503
+ else if (rl) {
504
+ process.stdout.write("\r\x1b[K");
505
+ console.log(chalk.red(`❌ WebSocket error: ${errorMsg}`));
506
+ rl.prompt(true);
507
+ }
508
+ else {
509
+ console.log(chalk.red(`❌ WebSocket error: ${errorMsg}`));
510
+ }
511
+ }
512
+ });
513
+ websocket.on("close", (code, reason) => {
514
+ // Only show warning for unexpected closes (not normal closure or going away)
515
+ // Code 1000 = Normal Closure
516
+ // Code 1001 = Going Away (server restart, etc.)
517
+ // Code 1006 = Abnormal Closure (connection lost)
518
+ // Code 1008 = Policy Violation (e.g., invalid/expired lease)
519
+ const isNormalClose = code === 1000 || code === 1001;
520
+ const shouldShowWarning = !isExiting && !jsonMode && !isNormalClose;
521
+ if (shouldShowWarning) {
522
+ const reasonStr = reason.toString() || "Connection lost";
523
+ let message = `⚠️ WebSocket connection closed`;
524
+ // Special handling for lease-related errors
525
+ if (code === 1008 && reasonStr.toLowerCase().includes("lease")) {
526
+ // Check if lease is actually expired or if this might be a timing issue
527
+ const isLeaseExpired = !leaseInfo || leaseInfo.leaseExpiresAt.getTime() <= Date.now();
528
+ if (isLeaseExpired) {
529
+ message = `⏱️ Your lease has expired. Type /renew to continue chatting.`;
530
+ }
531
+ else {
532
+ // Lease should still be valid - this might be a server-side issue
533
+ message = `⚠️ Lease validation failed on server. Your lease appears valid. Type /renew to get a fresh lease.`;
534
+ }
535
+ leaseInfo = null; // Mark lease as invalid
536
+ if (headerBox) {
537
+ updateHeaderBox(headerBox, null, tokenIdentifier, false);
538
+ screen?.render();
539
+ }
540
+ }
541
+ else if (code === 1006) {
542
+ message = `⚠️ Connection lost. Type /renew to reconnect.`;
543
+ }
544
+ else {
545
+ message = `⚠️ WebSocket connection closed (code: ${code}): ${reasonStr}`;
546
+ }
547
+ if (messageLogBox) {
548
+ messageLogBox.log(chalk.yellow(message));
549
+ messageLogBox.setScrollPerc(100);
550
+ screen?.render();
551
+ }
552
+ else if (rl) {
553
+ process.stdout.write("\r\x1b[K");
554
+ console.log(chalk.yellow(message));
555
+ rl.prompt(true);
556
+ }
557
+ else {
558
+ console.log(chalk.yellow(message));
559
+ }
560
+ }
561
+ });
562
+ };
563
+ // Connect to WebSocket with retry logic for timing issues
564
+ if (!wsUrl) {
565
+ throw new Error("WebSocket URL is not available");
566
+ }
567
+ let retryCount = 0;
568
+ const maxRetries = 3;
569
+ let connected = false;
570
+ while (!connected && retryCount < maxRetries) {
571
+ try {
572
+ if (retryCount > 0) {
573
+ // Exponential backoff: 500ms, 1000ms, 2000ms
574
+ const backoffDelay = Math.min(500 * Math.pow(2, retryCount - 1), 2000);
575
+ if (!jsonMode) {
576
+ if (messageLogBox) {
577
+ messageLogBox.log(chalk.yellow(`🔄 Retrying WebSocket connection (attempt ${retryCount + 1}/${maxRetries})...`));
578
+ messageLogBox.setScrollPerc(100);
579
+ screen?.render();
580
+ }
581
+ else if (rl) {
582
+ console.log(chalk.yellow(`🔄 Retrying WebSocket connection (attempt ${retryCount + 1}/${maxRetries})...`));
583
+ }
584
+ }
585
+ await new Promise((resolve) => setTimeout(resolve, backoffDelay));
586
+ }
587
+ // Create WebSocket connection and wait for it to open or fail
588
+ await new Promise((resolve, reject) => {
589
+ const newWs = new WebSocket(wsUrl);
590
+ // Attach permanent handlers FIRST (before temporary handlers)
591
+ attachWebSocketHandlers(newWs);
592
+ // Set up temporary handlers for connection establishment
593
+ let timeout = null;
594
+ let openHandler = null;
595
+ let closeHandler = null;
596
+ let errorHandler = null;
597
+ const cleanup = () => {
598
+ if (timeout)
599
+ clearTimeout(timeout);
600
+ if (openHandler)
601
+ newWs.removeListener("open", openHandler);
602
+ if (closeHandler)
603
+ newWs.removeListener("close", closeHandler);
604
+ if (errorHandler)
605
+ newWs.removeListener("error", errorHandler);
606
+ };
607
+ timeout = setTimeout(() => {
608
+ cleanup();
609
+ newWs.close();
610
+ reject(new Error("Connection timeout - lease may not be ready yet"));
611
+ }, 10000);
612
+ openHandler = () => {
613
+ cleanup();
614
+ ws = newWs; // Assign to outer variable only after successful connection
615
+ resolve();
616
+ };
617
+ closeHandler = (code, reason) => {
618
+ cleanup();
619
+ const reasonStr = reason.toString();
620
+ // Only retry on code 1008 (lease validation) errors
621
+ if (code === 1008 && reasonStr.toLowerCase().includes("lease")) {
622
+ reject(new Error("Lease validation failed - may need more time to commit"));
623
+ }
624
+ else {
625
+ reject(new Error(`Connection closed: ${code} - ${reasonStr}`));
626
+ }
627
+ };
628
+ errorHandler = (error) => {
629
+ cleanup();
630
+ reject(error);
631
+ };
632
+ // Attach temporary handlers AFTER permanent handlers
633
+ newWs.once("open", openHandler);
634
+ newWs.once("close", closeHandler);
635
+ newWs.once("error", errorHandler);
636
+ });
637
+ connected = true;
638
+ // Connection established successfully
639
+ if (!jsonMode && messageLogBox && retryCount > 0) {
640
+ messageLogBox.log(chalk.green("✅ WebSocket connected successfully"));
641
+ messageLogBox.setScrollPerc(100);
642
+ screen?.render();
643
+ }
644
+ }
645
+ catch (error) {
646
+ retryCount++;
647
+ const errorMsg = error instanceof Error ? error.message : String(error);
648
+ // Check if this is a retryable error (lease validation timing issue)
649
+ const isRetryable = errorMsg.includes("Lease validation failed") ||
650
+ errorMsg.includes("lease may not be ready");
651
+ if (retryCount >= maxRetries || !isRetryable) {
652
+ // Final failure or non-retryable error
653
+ const finalError = `Failed to connect after ${retryCount} attempts: ${errorMsg}`;
654
+ if (jsonMode) {
655
+ console.log(JSON.stringify({
656
+ type: "error",
657
+ error: finalError,
658
+ }));
659
+ }
660
+ else {
661
+ if (messageLogBox) {
662
+ messageLogBox.log(chalk.red(`❌ ${finalError}`));
663
+ messageLogBox.log(chalk.yellow("💡 Try typing /renew to get a fresh lease"));
664
+ messageLogBox.setScrollPerc(100);
665
+ screen?.render();
666
+ }
667
+ else if (rl) {
668
+ console.log(chalk.red(`❌ ${finalError}`));
669
+ console.log(chalk.yellow("💡 Try typing /renew to get a fresh lease"));
670
+ }
671
+ else {
672
+ console.log(chalk.red(`❌ ${finalError}`));
673
+ }
674
+ }
675
+ throw new Error(finalError);
676
+ }
677
+ // Will retry on next iteration
678
+ }
679
+ }
680
+ if (!connected) {
681
+ throw new Error("Failed to establish WebSocket connection");
682
+ }
683
+ // Check lease expiration every 30 seconds (works for both JSON and interactive modes)
684
+ leaseCheckInterval = setInterval(() => {
685
+ if (leaseInfo && leaseInfo.leaseExpiresAt.getTime() <= Date.now()) {
686
+ if (jsonMode) {
687
+ console.log(JSON.stringify({
688
+ type: 'lease_expired',
689
+ message: 'Your lease has expired. Use /renew to continue chatting.',
690
+ }));
691
+ }
692
+ else if (messageLogBox) {
693
+ messageLogBox.log(chalk.yellow("⏱️ Your lease has expired. Type /renew to continue chatting."));
694
+ messageLogBox.setScrollPerc(100);
695
+ screen?.render();
696
+ }
697
+ else if (rl) {
698
+ console.log(chalk.yellow("⏱️ Your lease has expired. Type /renew to continue chatting."));
699
+ }
700
+ leaseInfo = null;
701
+ if (headerBox) {
702
+ updateHeaderBox(headerBox, null, tokenIdentifier, true);
703
+ screen?.render();
704
+ }
705
+ }
706
+ }, 30000);
707
+ // Set up blessed UI or readline interface (only in interactive mode)
708
+ if (!jsonMode) {
709
+ let useBlessed = true;
710
+ // Check if terminal supports blessed (basic compatibility check)
711
+ // If not a TTY, silently fall back to readline
712
+ if (!process.stdout.isTTY) {
713
+ useBlessed = false;
714
+ }
715
+ // Helper function to set up readline interface
716
+ const setupReadline = () => {
717
+ rl = createInterface({
718
+ input: process.stdin,
719
+ output: process.stdout,
720
+ prompt: "",
721
+ });
722
+ // Display header information via console
723
+ console.log();
724
+ console.log(chalk.cyan("═".repeat(80)));
725
+ console.log(chalk.cyan("💬 httpcat Chat Stream"));
726
+ console.log(chalk.cyan("═".repeat(80)));
727
+ console.log();
728
+ console.log(chalk.green("✅ Connected to chat stream"));
729
+ console.log();
730
+ console.log(chalk.dim("💰 Entry fee: $0.01 USDC (10 min lease)"));
731
+ console.log(chalk.dim("💬 Per message: $0.01 USDC"));
732
+ if (leaseInfo) {
733
+ const timeRemaining = formatTimeRemaining(leaseInfo.leaseExpiresAt);
734
+ console.log(chalk.dim(`⏱️ Lease expires in: ${timeRemaining}`));
735
+ }
736
+ console.log();
737
+ console.log(chalk.dim("💡 Type your message and press Enter to send"));
738
+ console.log(chalk.dim("💡 Type /exit or Ctrl+C to quit"));
739
+ console.log(chalk.dim("💡 Type /renew to renew your lease"));
740
+ if (tokenIdentifier) {
741
+ console.log(chalk.dim("💡 Type /buy <amount> to buy tokens"));
742
+ console.log(chalk.dim("💡 Type /sell <amount> to sell tokens"));
743
+ }
744
+ console.log(chalk.cyan("─".repeat(80)));
745
+ console.log();
746
+ };
747
+ // Try to create blessed screen with error handling
748
+ if (useBlessed) {
749
+ try {
750
+ // Create blessed screen and widgets
751
+ screen = blessed.screen({
752
+ smartCSR: true,
753
+ title: "httpcat Chat",
754
+ fullUnicode: true,
755
+ // Add error handling for terminal compatibility
756
+ fastCSR: false, // Disable fast CSR to avoid rendering issues
757
+ });
758
+ // Calculate header height (approximately 10 lines)
759
+ const headerHeight = 10;
760
+ const inputHeight = 3;
761
+ // Create header box (fixed at top)
762
+ headerBox = blessed.box({
763
+ top: 0,
764
+ left: 0,
765
+ width: "100%",
766
+ height: headerHeight,
767
+ content: "",
768
+ tags: false, // Disable tags to avoid rendering issues
769
+ wrap: true,
770
+ scrollable: false,
771
+ alwaysScroll: false,
772
+ padding: {
773
+ left: 1,
774
+ right: 1,
775
+ top: 0,
776
+ bottom: 0,
777
+ },
778
+ style: {
779
+ fg: "white",
780
+ bg: "black",
781
+ },
782
+ });
783
+ // Create message log box (scrollable, middle section)
784
+ messageLogBox = blessed.log({
785
+ top: headerHeight,
786
+ left: 0,
787
+ width: "100%",
788
+ height: `100%-${headerHeight + inputHeight}`,
789
+ tags: true,
790
+ scrollable: true,
791
+ alwaysScroll: true,
792
+ scrollbar: {
793
+ ch: " ",
794
+ inverse: true,
795
+ },
796
+ style: {
797
+ fg: "white",
798
+ bg: "black",
799
+ },
800
+ });
801
+ // Create input box (fixed at bottom)
802
+ inputBox = blessed.textbox({
803
+ bottom: 0,
804
+ left: 0,
805
+ width: "100%",
806
+ height: inputHeight,
807
+ content: "",
808
+ inputOnFocus: true,
809
+ tags: true,
810
+ keys: true,
811
+ style: {
812
+ fg: "cyan",
813
+ bg: "black",
814
+ focus: {
815
+ fg: "white",
816
+ bg: "blue",
817
+ },
818
+ },
819
+ });
820
+ // Append widgets to screen
821
+ screen.append(headerBox);
822
+ screen.append(messageLogBox);
823
+ screen.append(inputBox);
824
+ // Test render to catch early errors
825
+ screen.render();
826
+ // Initial header update
827
+ updateHeaderBox(headerBox, leaseInfo, tokenIdentifier, false);
828
+ screen.render();
829
+ }
830
+ catch (blessedError) {
831
+ // Blessed failed - fallback to readline
832
+ useBlessed = false;
833
+ // Only show error if it's not a TTY issue (which we already handled silently)
834
+ if (process.stdout.isTTY) {
835
+ console.error(chalk.yellow("⚠️ Blessed UI initialization failed, falling back to readline interface"));
836
+ console.error(chalk.dim(` Error: ${blessedError instanceof Error ? blessedError.message : String(blessedError)}`));
837
+ }
838
+ // Clean up any partial blessed setup
839
+ if (screen) {
840
+ try {
841
+ screen.destroy();
842
+ }
843
+ catch {
844
+ // Ignore cleanup errors
845
+ }
846
+ screen = null;
847
+ headerBox = null;
848
+ messageLogBox = null;
849
+ inputBox = null;
850
+ }
851
+ setupReadline();
852
+ }
853
+ }
854
+ else {
855
+ // Not a TTY or blessed disabled - use readline directly
856
+ setupReadline();
857
+ }
858
+ // Display last messages
859
+ if (joinResult.lastMessages.length > 0) {
860
+ const sortedMessages = [...joinResult.lastMessages].sort((a, b) => {
861
+ const timeA = new Date(a.timestamp).getTime();
862
+ const timeB = new Date(b.timestamp).getTime();
863
+ return timeA - timeB; // Oldest first
864
+ });
865
+ sortedMessages.forEach((msg) => {
866
+ displayedMessageIds.add(msg.messageId);
867
+ const isOwn = msg.author === userAddress;
868
+ // This is inside !jsonMode block, but add explicit check for safety
869
+ displayMessage(msg, isOwn, false, messageLogBox || undefined, jsonMode ? undefined : (rl || undefined));
870
+ });
871
+ }
872
+ // Only set up blessed-specific handlers if using blessed
873
+ if (useBlessed && screen) {
874
+ // Handle terminal resize
875
+ screen.on("resize", () => {
876
+ screen?.render();
877
+ });
878
+ // Update header with lease countdown every second
879
+ headerUpdateInterval = setInterval(() => {
880
+ if (leaseInfo && headerBox && screen && !isExiting) {
881
+ updateHeaderBox(headerBox, leaseInfo, tokenIdentifier, true);
882
+ screen.render();
883
+ }
884
+ }, 1000);
885
+ }
886
+ // Handle input submission
887
+ if (useBlessed && inputBox) {
888
+ // Blessed input handling
889
+ inputBox.on("submit", async (value) => {
890
+ const trimmed = value.trim();
891
+ // Ignore input while sending
892
+ if (isSending || !trimmed) {
893
+ inputBox?.clearValue();
894
+ inputBox?.focus();
895
+ screen?.render();
896
+ return;
897
+ }
898
+ // Handle commands
899
+ if (trimmed.startsWith("/")) {
900
+ const [cmd] = trimmed.split(" ");
901
+ switch (cmd) {
902
+ case "/exit":
903
+ case "/quit":
904
+ isExiting = true;
905
+ if (leaseCheckInterval)
906
+ clearInterval(leaseCheckInterval);
907
+ if (headerUpdateInterval)
908
+ clearInterval(headerUpdateInterval);
909
+ if (ws)
910
+ ws.close();
911
+ screen?.destroy();
912
+ console.log();
913
+ printCat("sleeping");
914
+ console.log(chalk.cyan("Chat disconnected. Goodbye! 👋"));
915
+ process.exit(0);
916
+ return;
917
+ case "/renew": {
918
+ try {
919
+ inputBox?.setValue("⏱️ Renewing lease...");
920
+ screen?.render();
921
+ const renewal = await renewLease(client, leaseInfo?.leaseId);
922
+ leaseInfo = {
923
+ leaseId: renewal.leaseId,
924
+ leaseExpiresAt: new Date(renewal.leaseExpiresAt),
925
+ };
926
+ // Update header first
927
+ if (headerBox) {
928
+ updateHeaderBox(headerBox, leaseInfo, tokenIdentifier, false);
929
+ }
930
+ if (messageLogBox) {
931
+ messageLogBox.log(chalk.green(`✅ Lease renewed! Expires in ${formatTimeRemaining(leaseInfo.leaseExpiresAt)}`));
932
+ messageLogBox.log(chalk.yellow("🔄 Reconnecting to chat stream..."));
933
+ messageLogBox.setScrollPerc(100);
934
+ screen?.render();
935
+ }
936
+ // Close old connection properly
937
+ if (ws) {
938
+ // Remove all listeners to prevent conflicts
939
+ ws.removeAllListeners();
940
+ // Ensure connection is fully closed
941
+ // WebSocket.OPEN = 1, WebSocket.CONNECTING = 0
942
+ if (ws.readyState === 1 || ws.readyState === 0) {
943
+ ws.close();
944
+ }
945
+ ws = null;
946
+ }
947
+ // Wait for database transaction to commit and old connection to fully close
948
+ // The server validates the lease in the open handler, so we need to ensure
949
+ // the database update is visible before connecting
950
+ // Increased delay to 2 seconds for better database consistency
951
+ await new Promise((resolve) => setTimeout(resolve, 2000));
952
+ // Reconnect with new lease - with retry logic
953
+ if (wsUrl) {
954
+ // Normalize WebSocket URL protocol based on agent URL
955
+ const agentUrl = client.getAgentUrl();
956
+ const normalizedWsUrl = normalizeWebSocketUrl(wsUrl, agentUrl);
957
+ const wsUrlObj = new URL(normalizedWsUrl);
958
+ wsUrlObj.searchParams.set("leaseId", leaseInfo.leaseId);
959
+ wsUrl = wsUrlObj.toString(); // Update stored URL
960
+ // Retry logic with exponential backoff
961
+ let retryCount = 0;
962
+ const maxRetries = 3;
963
+ let connected = false;
964
+ while (!connected && retryCount < maxRetries) {
965
+ try {
966
+ if (retryCount > 0) {
967
+ const backoffDelay = Math.min(1000 * Math.pow(2, retryCount - 1), 5000);
968
+ if (messageLogBox) {
969
+ messageLogBox.log(chalk.yellow(`🔄 Retry ${retryCount}/${maxRetries} in ${backoffDelay}ms...`));
970
+ messageLogBox.setScrollPerc(100);
971
+ screen?.render();
972
+ }
973
+ await new Promise((resolve) => setTimeout(resolve, backoffDelay));
974
+ }
975
+ // Create new connection and wait for it to open
976
+ const newWsUrl = wsUrl; // TypeScript: ensure it's not null
977
+ await new Promise((resolve, reject) => {
978
+ const newWs = new WebSocket(newWsUrl);
979
+ // Attach permanent handlers FIRST (before temporary handlers)
980
+ attachWebSocketHandlers(newWs);
981
+ // Set up temporary handlers for connection establishment
982
+ let timeout = null;
983
+ let openHandler = null;
984
+ let closeHandler = null;
985
+ let errorHandler = null;
986
+ const cleanup = () => {
987
+ if (timeout)
988
+ clearTimeout(timeout);
989
+ if (openHandler)
990
+ newWs.removeListener("open", openHandler);
991
+ if (closeHandler)
992
+ newWs.removeListener("close", closeHandler);
993
+ if (errorHandler)
994
+ newWs.removeListener("error", errorHandler);
995
+ };
996
+ timeout = setTimeout(() => {
997
+ cleanup();
998
+ newWs.close();
999
+ reject(new Error("Connection timeout - lease may not be ready yet"));
1000
+ }, 10000);
1001
+ openHandler = () => {
1002
+ cleanup();
1003
+ ws = newWs; // Assign to outer variable only after successful connection
1004
+ resolve();
1005
+ };
1006
+ closeHandler = (code, reason) => {
1007
+ cleanup();
1008
+ const reasonStr = reason.toString();
1009
+ if (code === 1008 && reasonStr.includes("lease")) {
1010
+ reject(new Error("Lease validation failed - please try /renew again"));
1011
+ }
1012
+ else {
1013
+ reject(new Error(`Connection closed: ${code} - ${reasonStr}`));
1014
+ }
1015
+ };
1016
+ errorHandler = (error) => {
1017
+ cleanup();
1018
+ reject(error);
1019
+ };
1020
+ // Attach temporary handlers AFTER permanent handlers
1021
+ newWs.once("open", openHandler);
1022
+ newWs.once("close", closeHandler);
1023
+ newWs.once("error", errorHandler);
1024
+ });
1025
+ connected = true;
1026
+ // Connection established
1027
+ if (headerBox) {
1028
+ updateHeaderBox(headerBox, leaseInfo, tokenIdentifier, true);
1029
+ }
1030
+ if (messageLogBox) {
1031
+ messageLogBox.log(chalk.green("✅ Reconnected to chat stream"));
1032
+ messageLogBox.setScrollPerc(100);
1033
+ }
1034
+ screen?.render();
1035
+ }
1036
+ catch (error) {
1037
+ retryCount++;
1038
+ const errorMsg = error instanceof Error ? error.message : String(error);
1039
+ if (retryCount >= maxRetries) {
1040
+ // Final failure
1041
+ throw new Error(`Failed to reconnect after ${maxRetries} attempts: ${errorMsg}`);
1042
+ }
1043
+ // Log retry attempt
1044
+ if (messageLogBox) {
1045
+ messageLogBox.log(chalk.yellow(`⚠️ Connection attempt ${retryCount} failed: ${errorMsg}`));
1046
+ messageLogBox.setScrollPerc(100);
1047
+ screen?.render();
1048
+ }
1049
+ }
1050
+ }
1051
+ }
1052
+ inputBox?.clearValue();
1053
+ inputBox?.focus();
1054
+ screen?.render();
1055
+ }
1056
+ catch (error) {
1057
+ const errorMsg = error instanceof Error ? error.message : String(error);
1058
+ if (messageLogBox) {
1059
+ messageLogBox.log(chalk.red(`❌ ${errorMsg}`));
1060
+ messageLogBox.setScrollPerc(100);
1061
+ }
1062
+ inputBox?.clearValue();
1063
+ inputBox?.focus();
1064
+ screen?.render();
1065
+ }
1066
+ return;
1067
+ }
1068
+ case "/buy": {
1069
+ // Only allow in token-specific chats
1070
+ if (!tokenIdentifier) {
1071
+ if (messageLogBox) {
1072
+ messageLogBox.log(chalk.yellow("⚠️ /buy command only works in token-specific chats"));
1073
+ messageLogBox.setScrollPerc(100);
1074
+ screen?.render();
1075
+ }
1076
+ inputBox?.clearValue();
1077
+ inputBox?.focus();
1078
+ screen?.render();
1079
+ return;
1080
+ }
1081
+ try {
1082
+ const parts = trimmed.split(" ");
1083
+ if (parts.length < 2) {
1084
+ if (messageLogBox) {
1085
+ messageLogBox.log(chalk.yellow("⚠️ Usage: /buy <amount> (e.g., /buy 0.05)"));
1086
+ messageLogBox.setScrollPerc(100);
1087
+ screen?.render();
1088
+ }
1089
+ inputBox?.clearValue();
1090
+ inputBox?.focus();
1091
+ screen?.render();
1092
+ return;
1093
+ }
1094
+ const amount = parts[1];
1095
+ inputBox?.setValue(`💰 Buying tokens...`);
1096
+ screen?.render();
1097
+ const isTestMode = client.getNetwork().includes("sepolia");
1098
+ const result = await buyToken(client, tokenIdentifier, amount, isTestMode, true // silent mode
1099
+ );
1100
+ if (messageLogBox) {
1101
+ messageLogBox.log(formatBuyResultCompact(result));
1102
+ messageLogBox.setScrollPerc(100);
1103
+ screen?.render();
1104
+ }
1105
+ inputBox?.clearValue();
1106
+ inputBox?.focus();
1107
+ screen?.render();
1108
+ }
1109
+ catch (error) {
1110
+ const errorMsg = error instanceof Error ? error.message : String(error);
1111
+ if (messageLogBox) {
1112
+ messageLogBox.log(chalk.red(`❌ Buy failed: ${errorMsg}`));
1113
+ messageLogBox.setScrollPerc(100);
1114
+ screen?.render();
1115
+ }
1116
+ inputBox?.clearValue();
1117
+ inputBox?.focus();
1118
+ screen?.render();
1119
+ }
1120
+ return;
1121
+ }
1122
+ case "/sell": {
1123
+ // Only allow in token-specific chats
1124
+ if (!tokenIdentifier) {
1125
+ if (messageLogBox) {
1126
+ messageLogBox.log(chalk.yellow("⚠️ /sell command only works in token-specific chats"));
1127
+ messageLogBox.setScrollPerc(100);
1128
+ screen?.render();
1129
+ }
1130
+ inputBox?.clearValue();
1131
+ inputBox?.focus();
1132
+ screen?.render();
1133
+ return;
1134
+ }
1135
+ try {
1136
+ const parts = trimmed.split(" ");
1137
+ if (parts.length < 2) {
1138
+ if (messageLogBox) {
1139
+ messageLogBox.log(chalk.yellow("⚠️ Usage: /sell <amount|all|percentage> (e.g., /sell 0.05, /sell all, /sell 50%)"));
1140
+ messageLogBox.setScrollPerc(100);
1141
+ screen?.render();
1142
+ }
1143
+ inputBox?.clearValue();
1144
+ inputBox?.focus();
1145
+ screen?.render();
1146
+ return;
1147
+ }
1148
+ const amountStr = parts[1];
1149
+ inputBox?.setValue(`💸 Selling tokens...`);
1150
+ screen?.render();
1151
+ // Get token info to check balance
1152
+ if (!userAddress) {
1153
+ throw new Error("User address not available");
1154
+ }
1155
+ const tokenInfo = await getTokenInfo(client, tokenIdentifier, userAddress, true // silent mode
1156
+ );
1157
+ if (!tokenInfo.userPosition || tokenInfo.userPosition.tokensOwned === "0") {
1158
+ throw new Error("You do not own any of this token");
1159
+ }
1160
+ // Parse sell amount
1161
+ const tokenAmount = parseTokenAmount(amountStr, tokenInfo.userPosition.tokensOwned);
1162
+ const result = await sellToken(client, tokenIdentifier, tokenAmount, true // silent mode
1163
+ );
1164
+ if (messageLogBox) {
1165
+ messageLogBox.log(formatSellResultCompact(result));
1166
+ messageLogBox.setScrollPerc(100);
1167
+ screen?.render();
1168
+ }
1169
+ inputBox?.clearValue();
1170
+ inputBox?.focus();
1171
+ screen?.render();
1172
+ }
1173
+ catch (error) {
1174
+ const errorMsg = error instanceof Error ? error.message : String(error);
1175
+ if (messageLogBox) {
1176
+ messageLogBox.log(chalk.red(`❌ Sell failed: ${errorMsg}`));
1177
+ messageLogBox.setScrollPerc(100);
1178
+ screen?.render();
1179
+ }
1180
+ inputBox?.clearValue();
1181
+ inputBox?.focus();
1182
+ screen?.render();
1183
+ }
1184
+ return;
1185
+ }
1186
+ case "/help":
1187
+ if (messageLogBox) {
1188
+ const helpText = tokenIdentifier
1189
+ ? chalk.dim("Commands: /exit, /quit, /renew, /buy, /sell, /help")
1190
+ : chalk.dim("Commands: /exit, /quit, /renew, /help");
1191
+ messageLogBox.log(helpText);
1192
+ messageLogBox.setScrollPerc(100);
1193
+ screen?.render();
1194
+ }
1195
+ inputBox?.clearValue();
1196
+ inputBox?.focus();
1197
+ screen?.render();
1198
+ return;
1199
+ default:
1200
+ if (messageLogBox) {
1201
+ messageLogBox.log(chalk.yellow(`Unknown command: ${cmd}. Type /help for commands.`));
1202
+ messageLogBox.setScrollPerc(100);
1203
+ screen?.render();
1204
+ }
1205
+ inputBox?.clearValue();
1206
+ inputBox?.focus();
1207
+ screen?.render();
1208
+ return;
1209
+ }
1210
+ }
1211
+ // Check if websocket is still connected
1212
+ // WebSocket.OPEN = 1
1213
+ if (!ws || ws.readyState !== 1) {
1214
+ const stateMsg = ws
1215
+ ? ws.readyState === 0 ? "connecting"
1216
+ : ws.readyState === 2 ? "closing"
1217
+ : ws.readyState === 3 ? "closed"
1218
+ : "unknown"
1219
+ : "not initialized";
1220
+ if (messageLogBox) {
1221
+ messageLogBox.log(chalk.red(`❌ WebSocket is not connected (state: ${stateMsg}). Please wait for connection or type /renew.`));
1222
+ messageLogBox.setScrollPerc(100);
1223
+ screen?.render();
1224
+ }
1225
+ inputBox?.clearValue();
1226
+ inputBox?.focus();
1227
+ screen?.render();
1228
+ return;
1229
+ }
1230
+ // Check if lease is valid
1231
+ if (!leaseInfo || leaseInfo.leaseExpiresAt.getTime() <= Date.now()) {
1232
+ if (messageLogBox) {
1233
+ messageLogBox.log(chalk.yellow("⏱️ Your lease has expired. Type /renew to continue chatting."));
1234
+ messageLogBox.setScrollPerc(100);
1235
+ screen?.render();
1236
+ }
1237
+ inputBox?.clearValue();
1238
+ inputBox?.focus();
1239
+ screen?.render();
1240
+ return;
1241
+ }
1242
+ // Send message
1243
+ isSending = true;
1244
+ currentInput = trimmed;
1245
+ // Show pulsing animation in input box
1246
+ let pulseCount = 0;
1247
+ let pulseInterval = null;
1248
+ const tempMessageId = `pending-${Date.now()}-${Math.random()}`;
1249
+ const updatePulse = () => {
1250
+ if (!inputBox || isCleaningUp)
1251
+ return;
1252
+ pulseCount++;
1253
+ const pulseChar = pulseCount % 2 === 0 ? "●" : "○";
1254
+ inputBox.setValue(`${trimmed} ${pulseChar}`);
1255
+ screen?.render();
1256
+ };
1257
+ // Initial pulse
1258
+ updatePulse();
1259
+ pulseInterval = setInterval(updatePulse, 500);
1260
+ pulseIntervals.set(tempMessageId, pulseInterval);
1261
+ try {
1262
+ // Ensure lease is valid before sending
1263
+ leaseInfo = await ensureLeaseValid(client, leaseInfo, messageLogBox || undefined, screen || undefined);
1264
+ if (headerBox) {
1265
+ updateHeaderBox(headerBox, leaseInfo, tokenIdentifier, true);
1266
+ screen?.render();
1267
+ }
1268
+ // Double-check websocket is still connected after lease renewal
1269
+ // WebSocket.OPEN = 1
1270
+ if (!ws || ws.readyState !== 1) {
1271
+ throw new Error("WebSocket connection lost. Please wait for reconnection.");
1272
+ }
1273
+ const result = await sendChatMessage(client, trimmed, leaseInfo.leaseId);
1274
+ userAddress = result.author;
1275
+ // Track this message
1276
+ pendingMessages.set(result.messageId, tempMessageId);
1277
+ pendingMessages.set(`text:${trimmed}`, tempMessageId);
1278
+ // Keep pulsing until WebSocket confirms
1279
+ }
1280
+ catch (error) {
1281
+ // Stop pulsing
1282
+ if (pulseInterval) {
1283
+ clearInterval(pulseInterval);
1284
+ pulseIntervals.delete(tempMessageId);
1285
+ }
1286
+ const errorMsg = error instanceof Error ? error.message : String(error);
1287
+ if (messageLogBox) {
1288
+ messageLogBox.log(chalk.red(`❌ ${errorMsg}`));
1289
+ messageLogBox.setScrollPerc(100);
1290
+ }
1291
+ inputBox?.clearValue();
1292
+ inputBox?.focus();
1293
+ isSending = false;
1294
+ currentInput = "";
1295
+ screen?.render();
1296
+ }
1297
+ });
1298
+ }
1299
+ else if (rl) {
1300
+ // Readline input handling
1301
+ const readlineInterface = rl; // Store reference for TypeScript
1302
+ readlineInterface.setPrompt("");
1303
+ readlineInterface.prompt();
1304
+ readlineInterface.on("line", async (line) => {
1305
+ const trimmed = line.trim();
1306
+ // Ignore empty input
1307
+ if (!trimmed) {
1308
+ readlineInterface.prompt();
1309
+ return;
1310
+ }
1311
+ // Handle commands
1312
+ if (trimmed.startsWith("/")) {
1313
+ const [cmd] = trimmed.split(" ");
1314
+ switch (cmd) {
1315
+ case "/exit":
1316
+ case "/quit":
1317
+ isExiting = true;
1318
+ if (leaseCheckInterval)
1319
+ clearInterval(leaseCheckInterval);
1320
+ if (headerUpdateInterval)
1321
+ clearInterval(headerUpdateInterval);
1322
+ if (ws)
1323
+ ws.close();
1324
+ readlineInterface.close();
1325
+ console.log();
1326
+ printCat("sleeping");
1327
+ console.log(chalk.cyan("Chat disconnected. Goodbye! 👋"));
1328
+ process.exit(0);
1329
+ return;
1330
+ case "/renew": {
1331
+ try {
1332
+ console.log(chalk.yellow("⏱️ Renewing lease..."));
1333
+ const renewal = await renewLease(client, leaseInfo?.leaseId);
1334
+ leaseInfo = {
1335
+ leaseId: renewal.leaseId,
1336
+ leaseExpiresAt: new Date(renewal.leaseExpiresAt),
1337
+ };
1338
+ console.log(chalk.green(`✅ Lease renewed! Expires in ${formatTimeRemaining(leaseInfo.leaseExpiresAt)}`));
1339
+ console.log(chalk.yellow("🔄 Reconnecting to chat stream..."));
1340
+ // Close old connection properly
1341
+ if (ws) {
1342
+ ws.removeAllListeners();
1343
+ // WebSocket.OPEN = 1, WebSocket.CONNECTING = 0
1344
+ if (ws.readyState === 1 || ws.readyState === 0) {
1345
+ ws.close();
1346
+ }
1347
+ ws = null;
1348
+ }
1349
+ // Wait for database transaction to commit
1350
+ await new Promise((resolve) => setTimeout(resolve, 2000));
1351
+ // Reconnect with new lease
1352
+ if (wsUrl) {
1353
+ // Normalize WebSocket URL protocol based on agent URL
1354
+ const agentUrl = client.getAgentUrl();
1355
+ const normalizedWsUrl = normalizeWebSocketUrl(wsUrl, agentUrl);
1356
+ const wsUrlObj = new URL(normalizedWsUrl);
1357
+ wsUrlObj.searchParams.set("leaseId", leaseInfo.leaseId);
1358
+ wsUrl = wsUrlObj.toString();
1359
+ // Retry logic
1360
+ let retryCount = 0;
1361
+ const maxRetries = 3;
1362
+ let connected = false;
1363
+ while (!connected && retryCount < maxRetries) {
1364
+ try {
1365
+ if (retryCount > 0) {
1366
+ const backoffDelay = Math.min(1000 * Math.pow(2, retryCount - 1), 5000);
1367
+ console.log(chalk.yellow(`🔄 Retry ${retryCount}/${maxRetries} in ${backoffDelay}ms...`));
1368
+ await new Promise((resolve) => setTimeout(resolve, backoffDelay));
1369
+ }
1370
+ const newWsUrl = wsUrl;
1371
+ await new Promise((resolve, reject) => {
1372
+ const newWs = new WebSocket(newWsUrl);
1373
+ attachWebSocketHandlers(newWs);
1374
+ let timeout = null;
1375
+ let openHandler = null;
1376
+ let closeHandler = null;
1377
+ let errorHandler = null;
1378
+ const cleanup = () => {
1379
+ if (timeout)
1380
+ clearTimeout(timeout);
1381
+ if (openHandler)
1382
+ newWs.removeListener("open", openHandler);
1383
+ if (closeHandler)
1384
+ newWs.removeListener("close", closeHandler);
1385
+ if (errorHandler)
1386
+ newWs.removeListener("error", errorHandler);
1387
+ };
1388
+ timeout = setTimeout(() => {
1389
+ cleanup();
1390
+ newWs.close();
1391
+ reject(new Error("Connection timeout"));
1392
+ }, 10000);
1393
+ openHandler = () => {
1394
+ cleanup();
1395
+ ws = newWs;
1396
+ resolve();
1397
+ };
1398
+ closeHandler = (code, reason) => {
1399
+ cleanup();
1400
+ const reasonStr = reason.toString();
1401
+ if (code === 1008 && reasonStr.includes("lease")) {
1402
+ reject(new Error("Lease validation failed"));
1403
+ }
1404
+ else {
1405
+ reject(new Error(`Connection closed: ${code} - ${reasonStr}`));
1406
+ }
1407
+ };
1408
+ errorHandler = (error) => {
1409
+ cleanup();
1410
+ reject(error);
1411
+ };
1412
+ newWs.once("open", openHandler);
1413
+ newWs.once("close", closeHandler);
1414
+ newWs.once("error", errorHandler);
1415
+ });
1416
+ connected = true;
1417
+ console.log(chalk.green("✅ Reconnected to chat stream"));
1418
+ }
1419
+ catch (error) {
1420
+ retryCount++;
1421
+ const errorMsg = error instanceof Error ? error.message : String(error);
1422
+ if (retryCount >= maxRetries) {
1423
+ throw new Error(`Failed to reconnect after ${maxRetries} attempts: ${errorMsg}`);
1424
+ }
1425
+ console.log(chalk.yellow(`⚠️ Connection attempt ${retryCount} failed: ${errorMsg}`));
1426
+ }
1427
+ }
1428
+ }
1429
+ if (readlineInterface)
1430
+ readlineInterface.prompt();
1431
+ }
1432
+ catch (error) {
1433
+ const errorMsg = error instanceof Error ? error.message : String(error);
1434
+ console.log(chalk.red(`❌ ${errorMsg}`));
1435
+ if (readlineInterface)
1436
+ readlineInterface.prompt();
1437
+ }
1438
+ return;
1439
+ }
1440
+ case "/buy": {
1441
+ // Only allow in token-specific chats
1442
+ if (!tokenIdentifier) {
1443
+ console.log(chalk.yellow("⚠️ /buy command only works in token-specific chats"));
1444
+ if (readlineInterface)
1445
+ readlineInterface.prompt();
1446
+ return;
1447
+ }
1448
+ try {
1449
+ const parts = trimmed.split(" ");
1450
+ if (parts.length < 2) {
1451
+ console.log(chalk.yellow("⚠️ Usage: /buy <amount> (e.g., /buy 0.05)"));
1452
+ if (readlineInterface)
1453
+ readlineInterface.prompt();
1454
+ return;
1455
+ }
1456
+ const amount = parts[1];
1457
+ console.log(chalk.yellow("💰 Buying tokens..."));
1458
+ const isTestMode = client.getNetwork().includes("sepolia");
1459
+ const result = await buyToken(client, tokenIdentifier, amount, isTestMode, true // silent mode
1460
+ );
1461
+ console.log(formatBuyResultCompact(result));
1462
+ if (readlineInterface)
1463
+ readlineInterface.prompt();
1464
+ }
1465
+ catch (error) {
1466
+ const errorMsg = error instanceof Error ? error.message : String(error);
1467
+ console.log(chalk.red(`❌ Buy failed: ${errorMsg}`));
1468
+ if (readlineInterface)
1469
+ readlineInterface.prompt();
1470
+ }
1471
+ return;
1472
+ }
1473
+ case "/sell": {
1474
+ // Only allow in token-specific chats
1475
+ if (!tokenIdentifier) {
1476
+ console.log(chalk.yellow("⚠️ /sell command only works in token-specific chats"));
1477
+ if (readlineInterface)
1478
+ readlineInterface.prompt();
1479
+ return;
1480
+ }
1481
+ try {
1482
+ const parts = trimmed.split(" ");
1483
+ if (parts.length < 2) {
1484
+ console.log(chalk.yellow("⚠️ Usage: /sell <amount|all|percentage> (e.g., /sell 0.05, /sell all, /sell 50%)"));
1485
+ if (readlineInterface)
1486
+ readlineInterface.prompt();
1487
+ return;
1488
+ }
1489
+ const amountStr = parts[1];
1490
+ console.log(chalk.yellow("💸 Selling tokens..."));
1491
+ // Get token info to check balance
1492
+ if (!userAddress) {
1493
+ throw new Error("User address not available");
1494
+ }
1495
+ const tokenInfo = await getTokenInfo(client, tokenIdentifier, userAddress, true // silent mode
1496
+ );
1497
+ if (!tokenInfo.userPosition || tokenInfo.userPosition.tokensOwned === "0") {
1498
+ throw new Error("You do not own any of this token");
1499
+ }
1500
+ // Parse sell amount
1501
+ const tokenAmount = parseTokenAmount(amountStr, tokenInfo.userPosition.tokensOwned);
1502
+ const result = await sellToken(client, tokenIdentifier, tokenAmount, true // silent mode
1503
+ );
1504
+ console.log(formatSellResultCompact(result));
1505
+ if (readlineInterface)
1506
+ readlineInterface.prompt();
1507
+ }
1508
+ catch (error) {
1509
+ const errorMsg = error instanceof Error ? error.message : String(error);
1510
+ console.log(chalk.red(`❌ Sell failed: ${errorMsg}`));
1511
+ if (readlineInterface)
1512
+ readlineInterface.prompt();
1513
+ }
1514
+ return;
1515
+ }
1516
+ case "/help":
1517
+ const helpText = tokenIdentifier
1518
+ ? chalk.dim("Commands: /exit, /quit, /renew, /buy, /sell, /help")
1519
+ : chalk.dim("Commands: /exit, /quit, /renew, /help");
1520
+ console.log(helpText);
1521
+ if (readlineInterface)
1522
+ readlineInterface.prompt();
1523
+ return;
1524
+ default:
1525
+ console.log(chalk.yellow(`Unknown command: ${cmd}. Type /help for commands.`));
1526
+ if (readlineInterface)
1527
+ readlineInterface.prompt();
1528
+ return;
1529
+ }
1530
+ }
1531
+ // Check if websocket is still connected
1532
+ if (!ws || ws.readyState !== 1) {
1533
+ const stateMsg = ws
1534
+ ? ws.readyState === 0 ? "connecting"
1535
+ : ws.readyState === 2 ? "closing"
1536
+ : ws.readyState === 3 ? "closed"
1537
+ : "unknown"
1538
+ : "not initialized";
1539
+ console.log(chalk.red(`❌ WebSocket is not connected (state: ${stateMsg}). Please wait for connection or type /renew.`));
1540
+ if (readlineInterface)
1541
+ readlineInterface.prompt();
1542
+ return;
1543
+ }
1544
+ // Check if lease is valid
1545
+ if (!leaseInfo || leaseInfo.leaseExpiresAt.getTime() <= Date.now()) {
1546
+ console.log(chalk.yellow("⏱️ Your lease has expired. Type /renew to continue chatting."));
1547
+ if (readlineInterface)
1548
+ readlineInterface.prompt();
1549
+ return;
1550
+ }
1551
+ // Send message
1552
+ isSending = true;
1553
+ currentInput = trimmed;
1554
+ try {
1555
+ // Ensure lease is valid before sending
1556
+ leaseInfo = await ensureLeaseValid(client, leaseInfo, undefined, undefined);
1557
+ // Double-check websocket is still connected
1558
+ if (!ws || ws.readyState !== 1) {
1559
+ throw new Error("WebSocket connection lost. Please wait for reconnection.");
1560
+ }
1561
+ const result = await sendChatMessage(client, trimmed, leaseInfo.leaseId);
1562
+ userAddress = result.author;
1563
+ // Track this message
1564
+ const tempMessageId = `pending-${Date.now()}-${Math.random()}`;
1565
+ pendingMessages.set(result.messageId, tempMessageId);
1566
+ pendingMessages.set(`text:${trimmed}`, tempMessageId);
1567
+ // Message sent - will be displayed when received via WebSocket
1568
+ }
1569
+ catch (error) {
1570
+ const errorMsg = error instanceof Error ? error.message : String(error);
1571
+ console.log(chalk.red(`❌ ${errorMsg}`));
1572
+ isSending = false;
1573
+ currentInput = "";
1574
+ }
1575
+ if (readlineInterface)
1576
+ readlineInterface.prompt();
1577
+ });
1578
+ // Handle Ctrl+C for readline
1579
+ readlineInterface.on("SIGINT", () => {
1580
+ isExiting = true;
1581
+ isSending = false;
1582
+ pulseIntervals.forEach((interval) => clearInterval(interval));
1583
+ pulseIntervals.clear();
1584
+ if (leaseCheckInterval)
1585
+ clearInterval(leaseCheckInterval);
1586
+ if (headerUpdateInterval)
1587
+ clearInterval(headerUpdateInterval);
1588
+ if (ws)
1589
+ ws.close();
1590
+ if (readlineInterface)
1591
+ readlineInterface.close();
1592
+ console.log();
1593
+ printCat("sleeping");
1594
+ console.log(chalk.cyan("Chat disconnected. Goodbye! 👋"));
1595
+ process.exit(0);
1596
+ });
1597
+ }
1598
+ // Handle Ctrl+C (for blessed mode)
1599
+ if (useBlessed && screen) {
1600
+ screen.key(["C-c"], () => {
1601
+ isExiting = true;
1602
+ isSending = false;
1603
+ pulseIntervals.forEach((interval) => clearInterval(interval));
1604
+ pulseIntervals.clear();
1605
+ if (leaseCheckInterval)
1606
+ clearInterval(leaseCheckInterval);
1607
+ if (headerUpdateInterval)
1608
+ clearInterval(headerUpdateInterval);
1609
+ if (ws)
1610
+ ws.close();
1611
+ screen?.destroy();
1612
+ console.log();
1613
+ printCat("sleeping");
1614
+ console.log(chalk.cyan("Chat disconnected. Goodbye! 👋"));
1615
+ process.exit(0);
1616
+ });
1617
+ // Focus input box and render (only for blessed)
1618
+ if (inputBox) {
1619
+ inputBox.focus();
1620
+ screen.render();
1621
+ }
1622
+ }
1623
+ }
1624
+ else {
1625
+ // JSON mode: read messages from stdin and output JSON to stdout
1626
+ // Set up stdin reading for non-interactive mode
1627
+ process.stdin.setEncoding('utf8');
1628
+ process.stdin.setRawMode(false);
1629
+ process.stdin.resume();
1630
+ let stdinBuffer = '';
1631
+ process.stdin.on('data', async (chunk) => {
1632
+ stdinBuffer += chunk.toString();
1633
+ const lines = stdinBuffer.split('\n');
1634
+ stdinBuffer = lines.pop() || ''; // Keep incomplete line in buffer
1635
+ for (const line of lines) {
1636
+ const trimmed = line.trim();
1637
+ if (!trimmed)
1638
+ continue;
1639
+ // Handle commands
1640
+ if (trimmed.startsWith('/')) {
1641
+ const [cmd] = trimmed.split(' ');
1642
+ switch (cmd) {
1643
+ case '/exit':
1644
+ case '/quit':
1645
+ isExiting = true;
1646
+ if (leaseCheckInterval)
1647
+ clearInterval(leaseCheckInterval);
1648
+ if (headerUpdateInterval)
1649
+ clearInterval(headerUpdateInterval);
1650
+ if (ws)
1651
+ ws.close();
1652
+ console.log(JSON.stringify({ type: 'exiting' }));
1653
+ process.exit(0);
1654
+ return;
1655
+ case '/renew': {
1656
+ try {
1657
+ console.log(JSON.stringify({ type: 'renewing_lease' }));
1658
+ const renewal = await renewLease(client, leaseInfo?.leaseId);
1659
+ leaseInfo = {
1660
+ leaseId: renewal.leaseId,
1661
+ leaseExpiresAt: new Date(renewal.leaseExpiresAt),
1662
+ };
1663
+ console.log(JSON.stringify({
1664
+ type: 'lease_renewed',
1665
+ leaseId: renewal.leaseId,
1666
+ leaseExpiresAt: renewal.leaseExpiresAt,
1667
+ }));
1668
+ // Close old connection
1669
+ if (ws) {
1670
+ ws.removeAllListeners();
1671
+ if (ws.readyState === 1 || ws.readyState === 0) {
1672
+ ws.close();
1673
+ }
1674
+ ws = null;
1675
+ }
1676
+ // Wait for database transaction
1677
+ await new Promise((resolve) => setTimeout(resolve, 2000));
1678
+ // Reconnect with new lease
1679
+ if (wsUrl) {
1680
+ const agentUrl = client.getAgentUrl();
1681
+ const normalizedWsUrl = normalizeWebSocketUrl(wsUrl, agentUrl);
1682
+ const wsUrlObj = new URL(normalizedWsUrl);
1683
+ wsUrlObj.searchParams.set("leaseId", leaseInfo.leaseId);
1684
+ wsUrl = wsUrlObj.toString();
1685
+ // Retry logic
1686
+ let retryCount = 0;
1687
+ const maxRetries = 3;
1688
+ let connected = false;
1689
+ while (!connected && retryCount < maxRetries) {
1690
+ try {
1691
+ if (retryCount > 0) {
1692
+ const backoffDelay = Math.min(1000 * Math.pow(2, retryCount - 1), 5000);
1693
+ await new Promise((resolve) => setTimeout(resolve, backoffDelay));
1694
+ }
1695
+ await new Promise((resolve, reject) => {
1696
+ const newWs = new WebSocket(wsUrl);
1697
+ attachWebSocketHandlers(newWs);
1698
+ let timeout = null;
1699
+ let openHandler = null;
1700
+ let closeHandler = null;
1701
+ let errorHandler = null;
1702
+ const cleanup = () => {
1703
+ if (timeout)
1704
+ clearTimeout(timeout);
1705
+ if (openHandler)
1706
+ newWs.removeListener("open", openHandler);
1707
+ if (closeHandler)
1708
+ newWs.removeListener("close", closeHandler);
1709
+ if (errorHandler)
1710
+ newWs.removeListener("error", errorHandler);
1711
+ };
1712
+ timeout = setTimeout(() => {
1713
+ cleanup();
1714
+ newWs.close();
1715
+ reject(new Error("Connection timeout"));
1716
+ }, 10000);
1717
+ openHandler = () => {
1718
+ cleanup();
1719
+ ws = newWs;
1720
+ resolve();
1721
+ };
1722
+ closeHandler = (code, reason) => {
1723
+ cleanup();
1724
+ const reasonStr = reason.toString();
1725
+ if (code === 1008 && reasonStr.includes("lease")) {
1726
+ reject(new Error("Lease validation failed"));
1727
+ }
1728
+ else {
1729
+ reject(new Error(`Connection closed: ${code} - ${reasonStr}`));
1730
+ }
1731
+ };
1732
+ errorHandler = (error) => {
1733
+ cleanup();
1734
+ reject(error);
1735
+ };
1736
+ newWs.once("open", openHandler);
1737
+ newWs.once("close", closeHandler);
1738
+ newWs.once("error", errorHandler);
1739
+ });
1740
+ connected = true;
1741
+ console.log(JSON.stringify({ type: 'reconnected' }));
1742
+ }
1743
+ catch (error) {
1744
+ retryCount++;
1745
+ const errorMsg = error instanceof Error ? error.message : String(error);
1746
+ if (retryCount >= maxRetries) {
1747
+ console.log(JSON.stringify({
1748
+ type: 'error',
1749
+ error: `Failed to reconnect after ${maxRetries} attempts: ${errorMsg}`,
1750
+ }));
1751
+ return;
1752
+ }
1753
+ }
1754
+ }
1755
+ }
1756
+ }
1757
+ catch (error) {
1758
+ const errorMsg = error instanceof Error ? error.message : String(error);
1759
+ console.log(JSON.stringify({
1760
+ type: 'error',
1761
+ error: errorMsg,
1762
+ }));
1763
+ }
1764
+ return;
1765
+ }
1766
+ default:
1767
+ console.log(JSON.stringify({
1768
+ type: 'error',
1769
+ error: `Unknown command: ${cmd}. Supported commands: /exit, /renew`,
1770
+ }));
1771
+ return;
1772
+ }
1773
+ }
1774
+ // Send message
1775
+ if (isSending) {
1776
+ console.log(JSON.stringify({
1777
+ type: 'error',
1778
+ error: 'Please wait for the previous message to be sent',
1779
+ }));
1780
+ return;
1781
+ }
1782
+ // Check if websocket is connected
1783
+ if (!ws || ws.readyState !== 1) {
1784
+ console.log(JSON.stringify({
1785
+ type: 'error',
1786
+ error: 'WebSocket is not connected. Please wait for connection or use /renew.',
1787
+ }));
1788
+ return;
1789
+ }
1790
+ // Check if lease is valid
1791
+ if (!leaseInfo || leaseInfo.leaseExpiresAt.getTime() <= Date.now()) {
1792
+ console.log(JSON.stringify({
1793
+ type: 'error',
1794
+ error: 'Lease has expired. Use /renew to continue chatting.',
1795
+ }));
1796
+ return;
1797
+ }
1798
+ // Send message
1799
+ isSending = true;
1800
+ try {
1801
+ leaseInfo = await ensureLeaseValid(client, leaseInfo, undefined, undefined);
1802
+ if (!ws || ws.readyState !== 1) {
1803
+ throw new Error("WebSocket connection lost. Please wait for reconnection.");
1804
+ }
1805
+ const result = await sendChatMessage(client, trimmed, leaseInfo.leaseId);
1806
+ userAddress = result.author;
1807
+ // Track this message
1808
+ const tempMessageId = `pending-${Date.now()}-${Math.random()}`;
1809
+ pendingMessages.set(result.messageId, tempMessageId);
1810
+ pendingMessages.set(`text:${trimmed}`, tempMessageId);
1811
+ // Message sent - will be displayed when received via WebSocket
1812
+ console.log(JSON.stringify({
1813
+ type: 'message_sent',
1814
+ messageId: result.messageId,
1815
+ }));
1816
+ }
1817
+ catch (error) {
1818
+ const errorMsg = error instanceof Error ? error.message : String(error);
1819
+ console.log(JSON.stringify({
1820
+ type: 'error',
1821
+ error: errorMsg,
1822
+ }));
1823
+ }
1824
+ finally {
1825
+ isSending = false;
1826
+ }
1827
+ }
1828
+ });
1829
+ process.stdin.on('end', () => {
1830
+ // Stdin closed - this is normal in JSON mode when input pipe ends
1831
+ isExiting = true;
1832
+ if (leaseCheckInterval)
1833
+ clearInterval(leaseCheckInterval);
1834
+ if (headerUpdateInterval)
1835
+ clearInterval(headerUpdateInterval);
1836
+ if (ws)
1837
+ ws.close();
1838
+ // Don't try to use readline in JSON mode - it should never exist
1839
+ process.exit(0);
1840
+ });
1841
+ process.stdin.on('error', (error) => {
1842
+ // Handle stdin errors gracefully in JSON mode
1843
+ if (!isExiting) {
1844
+ console.log(JSON.stringify({
1845
+ type: 'error',
1846
+ error: `Stdin error: ${error.message}`,
1847
+ }));
1848
+ }
1849
+ });
1850
+ process.on("SIGINT", () => {
1851
+ isExiting = true;
1852
+ if (leaseCheckInterval)
1853
+ clearInterval(leaseCheckInterval);
1854
+ if (headerUpdateInterval)
1855
+ clearInterval(headerUpdateInterval);
1856
+ if (ws)
1857
+ ws.close();
1858
+ // Don't try to use readline in JSON mode - it should never exist
1859
+ console.log(JSON.stringify({ type: 'exiting' }));
1860
+ process.exit(0);
1861
+ });
1862
+ }
1863
+ }
1864
+ catch (error) {
1865
+ // Cleanup on error
1866
+ try {
1867
+ if (leaseCheckInterval)
1868
+ clearInterval(leaseCheckInterval);
1869
+ if (headerUpdateInterval)
1870
+ clearInterval(headerUpdateInterval);
1871
+ pulseIntervals.forEach((interval) => clearInterval(interval));
1872
+ pulseIntervals.clear();
1873
+ if (ws !== null) {
1874
+ try {
1875
+ ws.removeAllListeners();
1876
+ // WebSocket.OPEN = 1, WebSocket.CONNECTING = 0
1877
+ const wsState = ws.readyState;
1878
+ if (wsState === 1 || wsState === 0) {
1879
+ ws.close();
1880
+ }
1881
+ }
1882
+ catch {
1883
+ // Ignore cleanup errors
1884
+ }
1885
+ }
1886
+ if (screen) {
1887
+ screen.destroy();
1888
+ }
1889
+ // Only close readline if not in JSON mode (readline should never exist in JSON mode)
1890
+ if (rl && !jsonMode) {
1891
+ rl.close();
1892
+ }
1893
+ }
1894
+ catch (cleanupError) {
1895
+ // Ignore cleanup errors
1896
+ }
1897
+ if (jsonMode) {
1898
+ console.log(JSON.stringify({
1899
+ type: "error",
1900
+ error: error instanceof Error ? error.message : String(error),
1901
+ }));
1902
+ }
1903
+ else {
1904
+ handleError(error, config.get("preferences")?.verboseLogging);
1905
+ }
1906
+ process.exit(1);
1907
+ }
1908
+ }
1909
+ export function displayChatResult(result) {
1910
+ console.log();
1911
+ console.log(chalk.green("✅ Message sent!"));
1912
+ console.log(chalk.dim(` ID: ${result.messageId}`));
1913
+ console.log(chalk.dim(` Time: ${new Date(result.timestamp).toLocaleString()}`));
1914
+ console.log();
1915
+ }
1916
+ //# sourceMappingURL=chat.js.map