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.
- package/README.md +302 -10
- package/chat-automation.js +171 -0
- package/dist/commands/chat.d.ts.map +1 -0
- package/dist/commands/chat.js +1916 -0
- package/dist/commands/chat.js.map +1 -0
- package/dist/commands/mcp-server.d.ts.map +1 -0
- package/dist/commands/mcp-server.js +12 -0
- package/dist/commands/mcp-server.js.map +1 -0
- package/dist/config.js +4 -4
- package/dist/config.js.map +1 -1
- package/dist/index.js +188 -115
- package/dist/index.js.map +1 -1
- package/dist/interactive/shell.d.ts.map +1 -1
- package/dist/interactive/shell.js +7 -0
- package/dist/interactive/shell.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +42 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +292 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +3 -0
- package/dist/mcp/types.js.map +1 -0
- package/homebrew-httpcat/Formula/httpcat.rb +18 -0
- package/homebrew-httpcat/README.md +31 -0
- package/homebrew-httpcat/homebrew-httpcat/Formula/httpcat.rb +3 -3
- package/package.json +11 -3
|
@@ -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
|