httpcat-cli 0.0.7 → 0.0.9
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/dist/commands/chat.d.ts.map +1 -0
- package/dist/commands/chat.js +1369 -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/package.json +11 -3
|
@@ -0,0 +1,1369 @@
|
|
|
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 } 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
|
+
const CHAT_JOIN_ENTRYPOINT = "chat_join";
|
|
13
|
+
const CHAT_MESSAGE_ENTRYPOINT = "chat_message";
|
|
14
|
+
const CHAT_RENEW_LEASE_ENTRYPOINT = "chat_renew_lease";
|
|
15
|
+
const LEASE_DURATION_MS = 10 * 60 * 1000; // 10 minutes
|
|
16
|
+
export async function joinChat(client, tokenIdentifier, silent = false) {
|
|
17
|
+
let tokenId;
|
|
18
|
+
// If token identifier provided, resolve it to tokenId
|
|
19
|
+
if (tokenIdentifier) {
|
|
20
|
+
// Check if it's an Ethereum address (starts with 0x and is 42 chars)
|
|
21
|
+
const isAddress = /^0x[a-fA-F0-9]{40}$/.test(tokenIdentifier);
|
|
22
|
+
if (isAddress) {
|
|
23
|
+
// Pass address directly - backend should handle it
|
|
24
|
+
tokenId = tokenIdentifier;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
// Resolve symbol/name/ticker to tokenId
|
|
28
|
+
tokenId = await resolveTokenId(tokenIdentifier, client, silent);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const input = tokenId ? { tokenId } : {};
|
|
32
|
+
const { data } = await client.invoke(CHAT_JOIN_ENTRYPOINT, input);
|
|
33
|
+
// Format author addresses for last messages
|
|
34
|
+
data.lastMessages = data.lastMessages.map((msg) => ({
|
|
35
|
+
...msg,
|
|
36
|
+
authorShort: formatAddress(msg.author, 6),
|
|
37
|
+
}));
|
|
38
|
+
return data;
|
|
39
|
+
}
|
|
40
|
+
export async function sendChatMessage(client, message, leaseId) {
|
|
41
|
+
if (!message.trim()) {
|
|
42
|
+
throw new Error("Message cannot be empty");
|
|
43
|
+
}
|
|
44
|
+
const input = { message, leaseId };
|
|
45
|
+
const { data } = await client.invoke(CHAT_MESSAGE_ENTRYPOINT, input);
|
|
46
|
+
return data;
|
|
47
|
+
}
|
|
48
|
+
export async function renewLease(client, leaseId) {
|
|
49
|
+
const input = leaseId ? { leaseId } : {};
|
|
50
|
+
const { data } = await client.invoke(CHAT_RENEW_LEASE_ENTRYPOINT, input);
|
|
51
|
+
return data;
|
|
52
|
+
}
|
|
53
|
+
function formatTimestamp(timestamp) {
|
|
54
|
+
try {
|
|
55
|
+
const date = new Date(timestamp);
|
|
56
|
+
const now = new Date();
|
|
57
|
+
const diffMs = now.getTime() - date.getTime();
|
|
58
|
+
const diffSec = Math.floor(diffMs / 1000);
|
|
59
|
+
const diffMin = Math.floor(diffSec / 60);
|
|
60
|
+
const diffHour = Math.floor(diffMin / 60);
|
|
61
|
+
if (diffSec < 60) {
|
|
62
|
+
return "just now";
|
|
63
|
+
}
|
|
64
|
+
else if (diffMin < 60) {
|
|
65
|
+
return `${diffMin}m ago`;
|
|
66
|
+
}
|
|
67
|
+
else if (diffHour < 24) {
|
|
68
|
+
return `${diffHour}h ago`;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
return date.toLocaleTimeString("en-US", {
|
|
72
|
+
hour: "2-digit",
|
|
73
|
+
minute: "2-digit",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return "";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Track displayed messages to avoid duplicates
|
|
82
|
+
const displayedMessageIds = new Set();
|
|
83
|
+
function formatMessageText(msg, isOwn = false) {
|
|
84
|
+
const timeStr = formatTimestamp(msg.timestamp);
|
|
85
|
+
const authorColor = isOwn ? chalk.green : chalk.cyan;
|
|
86
|
+
const messageColor = isOwn ? chalk.white : chalk.gray;
|
|
87
|
+
const authorShort = msg.authorShort || formatAddress(msg.author, 6);
|
|
88
|
+
return `${chalk.dim(`[${timeStr}]`)} ${authorColor(authorShort)}: ${messageColor(msg.message)}`;
|
|
89
|
+
}
|
|
90
|
+
function displayMessage(msg, isOwn = false, isPending = false, messageLogBox, rl) {
|
|
91
|
+
// Skip if we've already displayed this message
|
|
92
|
+
if (displayedMessageIds.has(msg.messageId)) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
displayedMessageIds.add(msg.messageId);
|
|
96
|
+
const messageText = formatMessageText(msg, isOwn);
|
|
97
|
+
if (messageLogBox) {
|
|
98
|
+
// Use blessed log box - automatically handles scrolling
|
|
99
|
+
messageLogBox.log(messageText);
|
|
100
|
+
messageLogBox.setScrollPerc(100); // Auto-scroll to bottom
|
|
101
|
+
}
|
|
102
|
+
else if (rl) {
|
|
103
|
+
// Fallback to readline approach (for JSON mode or edge cases)
|
|
104
|
+
process.stdout.write("\r\x1b[K");
|
|
105
|
+
process.stdout.write(messageText);
|
|
106
|
+
process.stdout.write("\n");
|
|
107
|
+
if (rl)
|
|
108
|
+
rl.prompt(true);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// Simple console.log when no UI active
|
|
112
|
+
console.log(messageText);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function clearLine() {
|
|
116
|
+
process.stdout.write("\r\x1b[K");
|
|
117
|
+
}
|
|
118
|
+
function formatTimeRemaining(expiresAt) {
|
|
119
|
+
const now = new Date();
|
|
120
|
+
const diffMs = expiresAt.getTime() - now.getTime();
|
|
121
|
+
if (diffMs <= 0) {
|
|
122
|
+
return "expired";
|
|
123
|
+
}
|
|
124
|
+
const minutes = Math.floor(diffMs / 60000);
|
|
125
|
+
const seconds = Math.floor((diffMs % 60000) / 1000);
|
|
126
|
+
if (minutes > 0) {
|
|
127
|
+
return `${minutes}m ${seconds}s`;
|
|
128
|
+
}
|
|
129
|
+
return `${seconds}s`;
|
|
130
|
+
}
|
|
131
|
+
function updateHeaderBox(headerBox, leaseInfo, tokenName, isConnected = false) {
|
|
132
|
+
const title = tokenName
|
|
133
|
+
? `💬 httpcat Chat: ${tokenName}`
|
|
134
|
+
: "💬 httpcat Chat Stream";
|
|
135
|
+
// Get terminal width, default to 80 if not available
|
|
136
|
+
const width = process.stdout.columns || 80;
|
|
137
|
+
const separator = "═".repeat(Math.max(20, width - 2));
|
|
138
|
+
const lineSeparator = "─".repeat(Math.max(20, width - 2));
|
|
139
|
+
// Build content line by line - use plain text (blessed will handle wrapping)
|
|
140
|
+
const lines = [];
|
|
141
|
+
lines.push(title);
|
|
142
|
+
lines.push(separator);
|
|
143
|
+
lines.push("");
|
|
144
|
+
if (isConnected) {
|
|
145
|
+
lines.push("✅ Connected to chat stream");
|
|
146
|
+
lines.push("");
|
|
147
|
+
}
|
|
148
|
+
lines.push("💰 Entry fee: $0.01 USDC (10 min lease)");
|
|
149
|
+
lines.push("💬 Per message: $0.01 USDC");
|
|
150
|
+
if (leaseInfo) {
|
|
151
|
+
const timeRemaining = formatTimeRemaining(leaseInfo.leaseExpiresAt);
|
|
152
|
+
const isExpired = leaseInfo.leaseExpiresAt.getTime() <= Date.now();
|
|
153
|
+
const timePrefix = isExpired ? "⚠️ " : "⏱️ ";
|
|
154
|
+
lines.push(`${timePrefix}Lease expires in: ${timeRemaining}`);
|
|
155
|
+
}
|
|
156
|
+
lines.push("");
|
|
157
|
+
lines.push("💡 Type your message and press Enter to send");
|
|
158
|
+
lines.push("💡 Type /exit or Ctrl+C to quit");
|
|
159
|
+
lines.push("💡 Type /renew to renew your lease");
|
|
160
|
+
lines.push("");
|
|
161
|
+
lines.push(lineSeparator);
|
|
162
|
+
headerBox.setContent(lines.join("\n"));
|
|
163
|
+
}
|
|
164
|
+
async function ensureLeaseValid(client, leaseInfo, messageLogBox, screen) {
|
|
165
|
+
if (!leaseInfo || leaseInfo.leaseExpiresAt.getTime() <= Date.now()) {
|
|
166
|
+
// Lease expired or doesn't exist, renew it
|
|
167
|
+
if (messageLogBox) {
|
|
168
|
+
messageLogBox.log(chalk.yellow("⏱️ Lease expired. Renewing..."));
|
|
169
|
+
messageLogBox.setScrollPerc(100);
|
|
170
|
+
screen?.render();
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
clearLine();
|
|
174
|
+
console.log(chalk.yellow("⏱️ Lease expired. Renewing..."));
|
|
175
|
+
}
|
|
176
|
+
const renewal = await renewLease(client, leaseInfo?.leaseId);
|
|
177
|
+
return {
|
|
178
|
+
leaseId: renewal.leaseId,
|
|
179
|
+
leaseExpiresAt: new Date(renewal.leaseExpiresAt),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return leaseInfo;
|
|
183
|
+
}
|
|
184
|
+
export async function startChatStream(client, jsonMode = false, tokenIdentifier) {
|
|
185
|
+
let leaseInfo = null;
|
|
186
|
+
let userAddress;
|
|
187
|
+
let ws = null;
|
|
188
|
+
let wsUrl = null; // Store websocket URL for reconnection
|
|
189
|
+
let rl = null;
|
|
190
|
+
let leaseCheckInterval = null;
|
|
191
|
+
let headerUpdateInterval = null;
|
|
192
|
+
let isExiting = false;
|
|
193
|
+
let isSending = false; // Track sending state across handlers
|
|
194
|
+
let currentInput = ""; // Track current input across handlers
|
|
195
|
+
let pendingMessages = new Map(); // messageId -> tempMessageId (shared between send and receive)
|
|
196
|
+
let pulseIntervals = new Map(); // tempMessageId -> interval
|
|
197
|
+
let isCleaningUp = false; // Flag to prevent intervals from writing during cleanup
|
|
198
|
+
let messageCount = 0; // Track number of messages displayed (for positioning)
|
|
199
|
+
// Blessed UI components (only used in non-JSON mode)
|
|
200
|
+
let screen = null;
|
|
201
|
+
let headerBox = null;
|
|
202
|
+
let messageLogBox = null;
|
|
203
|
+
let inputBox = null;
|
|
204
|
+
// Join chat
|
|
205
|
+
try {
|
|
206
|
+
if (!jsonMode) {
|
|
207
|
+
if (tokenIdentifier) {
|
|
208
|
+
console.log(chalk.dim(`Joining chat room for token: ${tokenIdentifier}...`));
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
console.log(chalk.dim("Joining general chat room..."));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const joinResult = await joinChat(client, tokenIdentifier, jsonMode);
|
|
215
|
+
leaseInfo = {
|
|
216
|
+
leaseId: joinResult.leaseId,
|
|
217
|
+
leaseExpiresAt: new Date(joinResult.leaseExpiresAt),
|
|
218
|
+
};
|
|
219
|
+
// Initialize userAddress from config if available (fallback until first message)
|
|
220
|
+
if (!userAddress) {
|
|
221
|
+
try {
|
|
222
|
+
const privateKey = config.getPrivateKey();
|
|
223
|
+
if (privateKey) {
|
|
224
|
+
const account = privateKeyToAccount(privateKey);
|
|
225
|
+
userAddress = account.address;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
// Ignore - will be set from first message send
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (jsonMode) {
|
|
233
|
+
// JSON mode: output join result and stream messages as JSON
|
|
234
|
+
console.log(JSON.stringify({
|
|
235
|
+
type: "joined",
|
|
236
|
+
leaseId: joinResult.leaseId,
|
|
237
|
+
leaseExpiresAt: joinResult.leaseExpiresAt,
|
|
238
|
+
tokenId: tokenIdentifier || null,
|
|
239
|
+
lastMessages: joinResult.lastMessages,
|
|
240
|
+
}));
|
|
241
|
+
}
|
|
242
|
+
// Connect to WebSocket
|
|
243
|
+
// Ensure we have a valid lease before connecting
|
|
244
|
+
if (!leaseInfo || leaseInfo.leaseExpiresAt.getTime() <= Date.now()) {
|
|
245
|
+
throw new Error("Lease is invalid or expired. Please try again.");
|
|
246
|
+
}
|
|
247
|
+
// Small delay to ensure database transaction commits before WebSocket connection
|
|
248
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
249
|
+
const wsUrlObj = new URL(joinResult.wsUrl);
|
|
250
|
+
wsUrlObj.searchParams.set("leaseId", joinResult.leaseId);
|
|
251
|
+
wsUrl = wsUrlObj.toString();
|
|
252
|
+
// Helper function to attach websocket handlers
|
|
253
|
+
const attachWebSocketHandlers = (websocket) => {
|
|
254
|
+
websocket.on("open", () => {
|
|
255
|
+
// Connection established - update header if using blessed UI
|
|
256
|
+
if (!jsonMode && headerBox) {
|
|
257
|
+
updateHeaderBox(headerBox, leaseInfo, tokenIdentifier, true);
|
|
258
|
+
screen?.render();
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
websocket.on("message", async (data) => {
|
|
262
|
+
try {
|
|
263
|
+
const event = JSON.parse(data.toString());
|
|
264
|
+
if (event.type === "message" && event.data) {
|
|
265
|
+
const msg = {
|
|
266
|
+
...event.data,
|
|
267
|
+
authorShort: formatAddress(event.data.author, 6),
|
|
268
|
+
};
|
|
269
|
+
if (jsonMode) {
|
|
270
|
+
console.log(JSON.stringify({
|
|
271
|
+
type: "message",
|
|
272
|
+
data: msg,
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
// Normalize addresses to lowercase for case-insensitive comparison
|
|
277
|
+
const normalizedAuthor = msg.author?.toLowerCase();
|
|
278
|
+
const normalizedUserAddress = userAddress?.toLowerCase();
|
|
279
|
+
const isOwn = normalizedAuthor === normalizedUserAddress;
|
|
280
|
+
// Check if this is a message we just sent
|
|
281
|
+
// Method 1: Check if messageId is in pendingMessages (most reliable)
|
|
282
|
+
const hasPendingId = pendingMessages.has(msg.messageId);
|
|
283
|
+
// Method 2: Check if message text is in pendingMessages (fallback if messageId doesn't match)
|
|
284
|
+
const hasPendingText = pendingMessages.has(`text:${msg.message}`);
|
|
285
|
+
// Method 3: Check if author matches AND we're currently sending (fallback)
|
|
286
|
+
const isAuthorMatch = isOwn && isSending;
|
|
287
|
+
// Combine all methods
|
|
288
|
+
const isOurPendingMessage = hasPendingId || hasPendingText || isAuthorMatch;
|
|
289
|
+
if (isOurPendingMessage) {
|
|
290
|
+
// Set cleanup flag to prevent intervals from writing
|
|
291
|
+
isCleaningUp = true;
|
|
292
|
+
// FIRST: Stop ALL pulsing animations immediately (before anything else)
|
|
293
|
+
// This prevents the interval from overwriting our clear
|
|
294
|
+
pulseIntervals.forEach((interval, id) => {
|
|
295
|
+
clearInterval(interval);
|
|
296
|
+
});
|
|
297
|
+
pulseIntervals.clear(); // Clear the map completely
|
|
298
|
+
// Clean up tracking FIRST (so intervals know to stop)
|
|
299
|
+
if (pendingMessages.has(msg.messageId)) {
|
|
300
|
+
pendingMessages.delete(msg.messageId);
|
|
301
|
+
}
|
|
302
|
+
// Also clean up text-based tracking
|
|
303
|
+
if (pendingMessages.has(`text:${msg.message}`)) {
|
|
304
|
+
pendingMessages.delete(`text:${msg.message}`);
|
|
305
|
+
}
|
|
306
|
+
// Small delay to ensure intervals have stopped
|
|
307
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
308
|
+
// Stop pulsing and clear the grey pulsing input line
|
|
309
|
+
if (inputBox) {
|
|
310
|
+
// Clear input box
|
|
311
|
+
inputBox.clearValue();
|
|
312
|
+
screen?.render();
|
|
313
|
+
}
|
|
314
|
+
else if (rl) {
|
|
315
|
+
// Fallback to readline
|
|
316
|
+
process.stdout.write("\n");
|
|
317
|
+
process.stdout.write("\r\x1b[K");
|
|
318
|
+
}
|
|
319
|
+
// Display the message (only if not already displayed)
|
|
320
|
+
if (!displayedMessageIds.has(msg.messageId)) {
|
|
321
|
+
displayMessage(msg, isOwn, false, messageLogBox || undefined, rl || undefined);
|
|
322
|
+
}
|
|
323
|
+
// Re-enable input - clear the input line completely
|
|
324
|
+
isSending = false;
|
|
325
|
+
currentInput = "";
|
|
326
|
+
isCleaningUp = false; // Clear cleanup flag
|
|
327
|
+
if (inputBox && screen) {
|
|
328
|
+
// Clear input box and refocus
|
|
329
|
+
inputBox.clearValue();
|
|
330
|
+
inputBox.focus();
|
|
331
|
+
screen.render();
|
|
332
|
+
}
|
|
333
|
+
else if (rl) {
|
|
334
|
+
// Fallback to readline
|
|
335
|
+
process.stdout.write("\r\x1b[K");
|
|
336
|
+
rl.prompt();
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
// Not our message, just display it normally
|
|
341
|
+
if (!displayedMessageIds.has(msg.messageId)) {
|
|
342
|
+
displayMessage(msg, isOwn, false, messageLogBox || undefined, rl || undefined);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
else if (event.type === "lease_expired") {
|
|
348
|
+
if (jsonMode) {
|
|
349
|
+
console.log(JSON.stringify({
|
|
350
|
+
type: "lease_expired",
|
|
351
|
+
}));
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
if (messageLogBox) {
|
|
355
|
+
messageLogBox.log(chalk.yellow("⏱️ Your lease has expired. Type /renew to continue chatting."));
|
|
356
|
+
messageLogBox.setScrollPerc(100);
|
|
357
|
+
screen?.render();
|
|
358
|
+
}
|
|
359
|
+
else if (rl) {
|
|
360
|
+
process.stdout.write("\r\x1b[K");
|
|
361
|
+
console.log(chalk.yellow("⏱️ Your lease has expired. Type /renew to continue chatting."));
|
|
362
|
+
rl.prompt(true);
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
console.log(chalk.yellow("⏱️ Your lease has expired. Type /renew to continue chatting."));
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
leaseInfo = null;
|
|
369
|
+
if (headerBox) {
|
|
370
|
+
updateHeaderBox(headerBox, null, tokenIdentifier, true);
|
|
371
|
+
screen?.render();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
else if (event.type === "error") {
|
|
375
|
+
if (jsonMode) {
|
|
376
|
+
console.log(JSON.stringify({
|
|
377
|
+
type: "error",
|
|
378
|
+
error: event.error,
|
|
379
|
+
}));
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
if (messageLogBox) {
|
|
383
|
+
messageLogBox.log(chalk.red(`❌ Error: ${event.error || "Unknown error"}`));
|
|
384
|
+
messageLogBox.setScrollPerc(100);
|
|
385
|
+
screen?.render();
|
|
386
|
+
}
|
|
387
|
+
else if (rl) {
|
|
388
|
+
process.stdout.write("\r\x1b[K");
|
|
389
|
+
console.log(chalk.red(`❌ Error: ${event.error || "Unknown error"}`));
|
|
390
|
+
rl.prompt(true);
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
console.log(chalk.red(`❌ Error: ${event.error || "Unknown error"}`));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
// Ignore parse errors
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
websocket.on("error", (error) => {
|
|
403
|
+
// Only log errors if not exiting and connection is still relevant
|
|
404
|
+
if (isExiting)
|
|
405
|
+
return;
|
|
406
|
+
if (jsonMode) {
|
|
407
|
+
console.log(JSON.stringify({
|
|
408
|
+
type: "error",
|
|
409
|
+
error: error.message,
|
|
410
|
+
}));
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
const errorMsg = error.message || "Unknown WebSocket error";
|
|
414
|
+
if (messageLogBox) {
|
|
415
|
+
messageLogBox.log(chalk.red(`❌ WebSocket error: ${errorMsg}`));
|
|
416
|
+
messageLogBox.setScrollPerc(100);
|
|
417
|
+
screen?.render();
|
|
418
|
+
}
|
|
419
|
+
else if (rl) {
|
|
420
|
+
process.stdout.write("\r\x1b[K");
|
|
421
|
+
console.log(chalk.red(`❌ WebSocket error: ${errorMsg}`));
|
|
422
|
+
rl.prompt(true);
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
console.log(chalk.red(`❌ WebSocket error: ${errorMsg}`));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
websocket.on("close", (code, reason) => {
|
|
430
|
+
// Only show warning for unexpected closes (not normal closure or going away)
|
|
431
|
+
// Code 1000 = Normal Closure
|
|
432
|
+
// Code 1001 = Going Away (server restart, etc.)
|
|
433
|
+
// Code 1006 = Abnormal Closure (connection lost)
|
|
434
|
+
// Code 1008 = Policy Violation (e.g., invalid/expired lease)
|
|
435
|
+
const isNormalClose = code === 1000 || code === 1001;
|
|
436
|
+
const shouldShowWarning = !isExiting && !jsonMode && !isNormalClose;
|
|
437
|
+
if (shouldShowWarning) {
|
|
438
|
+
const reasonStr = reason.toString() || "Connection lost";
|
|
439
|
+
let message = `⚠️ WebSocket connection closed`;
|
|
440
|
+
// Special handling for lease-related errors
|
|
441
|
+
if (code === 1008 && reasonStr.toLowerCase().includes("lease")) {
|
|
442
|
+
// Check if lease is actually expired or if this might be a timing issue
|
|
443
|
+
const isLeaseExpired = !leaseInfo || leaseInfo.leaseExpiresAt.getTime() <= Date.now();
|
|
444
|
+
if (isLeaseExpired) {
|
|
445
|
+
message = `⏱️ Your lease has expired. Type /renew to continue chatting.`;
|
|
446
|
+
}
|
|
447
|
+
else {
|
|
448
|
+
// Lease should still be valid - this might be a server-side issue
|
|
449
|
+
message = `⚠️ Lease validation failed on server. Your lease appears valid. Type /renew to get a fresh lease.`;
|
|
450
|
+
}
|
|
451
|
+
leaseInfo = null; // Mark lease as invalid
|
|
452
|
+
if (headerBox) {
|
|
453
|
+
updateHeaderBox(headerBox, null, tokenIdentifier, false);
|
|
454
|
+
screen?.render();
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
else if (code === 1006) {
|
|
458
|
+
message = `⚠️ Connection lost. Type /renew to reconnect.`;
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
message = `⚠️ WebSocket connection closed (code: ${code}): ${reasonStr}`;
|
|
462
|
+
}
|
|
463
|
+
if (messageLogBox) {
|
|
464
|
+
messageLogBox.log(chalk.yellow(message));
|
|
465
|
+
messageLogBox.setScrollPerc(100);
|
|
466
|
+
screen?.render();
|
|
467
|
+
}
|
|
468
|
+
else if (rl) {
|
|
469
|
+
process.stdout.write("\r\x1b[K");
|
|
470
|
+
console.log(chalk.yellow(message));
|
|
471
|
+
rl.prompt(true);
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
console.log(chalk.yellow(message));
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
};
|
|
479
|
+
// Connect to WebSocket with retry logic for timing issues
|
|
480
|
+
if (!wsUrl) {
|
|
481
|
+
throw new Error("WebSocket URL is not available");
|
|
482
|
+
}
|
|
483
|
+
let retryCount = 0;
|
|
484
|
+
const maxRetries = 3;
|
|
485
|
+
let connected = false;
|
|
486
|
+
while (!connected && retryCount < maxRetries) {
|
|
487
|
+
try {
|
|
488
|
+
if (retryCount > 0) {
|
|
489
|
+
// Exponential backoff: 500ms, 1000ms, 2000ms
|
|
490
|
+
const backoffDelay = Math.min(500 * Math.pow(2, retryCount - 1), 2000);
|
|
491
|
+
if (!jsonMode) {
|
|
492
|
+
if (messageLogBox) {
|
|
493
|
+
messageLogBox.log(chalk.yellow(`🔄 Retrying WebSocket connection (attempt ${retryCount + 1}/${maxRetries})...`));
|
|
494
|
+
messageLogBox.setScrollPerc(100);
|
|
495
|
+
screen?.render();
|
|
496
|
+
}
|
|
497
|
+
else if (rl) {
|
|
498
|
+
console.log(chalk.yellow(`🔄 Retrying WebSocket connection (attempt ${retryCount + 1}/${maxRetries})...`));
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
await new Promise((resolve) => setTimeout(resolve, backoffDelay));
|
|
502
|
+
}
|
|
503
|
+
// Create WebSocket connection and wait for it to open or fail
|
|
504
|
+
await new Promise((resolve, reject) => {
|
|
505
|
+
const newWs = new WebSocket(wsUrl);
|
|
506
|
+
// Attach permanent handlers FIRST (before temporary handlers)
|
|
507
|
+
attachWebSocketHandlers(newWs);
|
|
508
|
+
// Set up temporary handlers for connection establishment
|
|
509
|
+
let timeout = null;
|
|
510
|
+
let openHandler = null;
|
|
511
|
+
let closeHandler = null;
|
|
512
|
+
let errorHandler = null;
|
|
513
|
+
const cleanup = () => {
|
|
514
|
+
if (timeout)
|
|
515
|
+
clearTimeout(timeout);
|
|
516
|
+
if (openHandler)
|
|
517
|
+
newWs.removeListener("open", openHandler);
|
|
518
|
+
if (closeHandler)
|
|
519
|
+
newWs.removeListener("close", closeHandler);
|
|
520
|
+
if (errorHandler)
|
|
521
|
+
newWs.removeListener("error", errorHandler);
|
|
522
|
+
};
|
|
523
|
+
timeout = setTimeout(() => {
|
|
524
|
+
cleanup();
|
|
525
|
+
newWs.close();
|
|
526
|
+
reject(new Error("Connection timeout - lease may not be ready yet"));
|
|
527
|
+
}, 10000);
|
|
528
|
+
openHandler = () => {
|
|
529
|
+
cleanup();
|
|
530
|
+
ws = newWs; // Assign to outer variable only after successful connection
|
|
531
|
+
resolve();
|
|
532
|
+
};
|
|
533
|
+
closeHandler = (code, reason) => {
|
|
534
|
+
cleanup();
|
|
535
|
+
const reasonStr = reason.toString();
|
|
536
|
+
// Only retry on code 1008 (lease validation) errors
|
|
537
|
+
if (code === 1008 && reasonStr.toLowerCase().includes("lease")) {
|
|
538
|
+
reject(new Error("Lease validation failed - may need more time to commit"));
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
reject(new Error(`Connection closed: ${code} - ${reasonStr}`));
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
errorHandler = (error) => {
|
|
545
|
+
cleanup();
|
|
546
|
+
reject(error);
|
|
547
|
+
};
|
|
548
|
+
// Attach temporary handlers AFTER permanent handlers
|
|
549
|
+
newWs.once("open", openHandler);
|
|
550
|
+
newWs.once("close", closeHandler);
|
|
551
|
+
newWs.once("error", errorHandler);
|
|
552
|
+
});
|
|
553
|
+
connected = true;
|
|
554
|
+
// Connection established successfully
|
|
555
|
+
if (!jsonMode && messageLogBox && retryCount > 0) {
|
|
556
|
+
messageLogBox.log(chalk.green("✅ WebSocket connected successfully"));
|
|
557
|
+
messageLogBox.setScrollPerc(100);
|
|
558
|
+
screen?.render();
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
catch (error) {
|
|
562
|
+
retryCount++;
|
|
563
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
564
|
+
// Check if this is a retryable error (lease validation timing issue)
|
|
565
|
+
const isRetryable = errorMsg.includes("Lease validation failed") ||
|
|
566
|
+
errorMsg.includes("lease may not be ready");
|
|
567
|
+
if (retryCount >= maxRetries || !isRetryable) {
|
|
568
|
+
// Final failure or non-retryable error
|
|
569
|
+
const finalError = `Failed to connect after ${retryCount} attempts: ${errorMsg}`;
|
|
570
|
+
if (jsonMode) {
|
|
571
|
+
console.log(JSON.stringify({
|
|
572
|
+
type: "error",
|
|
573
|
+
error: finalError,
|
|
574
|
+
}));
|
|
575
|
+
}
|
|
576
|
+
else {
|
|
577
|
+
if (messageLogBox) {
|
|
578
|
+
messageLogBox.log(chalk.red(`❌ ${finalError}`));
|
|
579
|
+
messageLogBox.log(chalk.yellow("💡 Try typing /renew to get a fresh lease"));
|
|
580
|
+
messageLogBox.setScrollPerc(100);
|
|
581
|
+
screen?.render();
|
|
582
|
+
}
|
|
583
|
+
else if (rl) {
|
|
584
|
+
console.log(chalk.red(`❌ ${finalError}`));
|
|
585
|
+
console.log(chalk.yellow("💡 Try typing /renew to get a fresh lease"));
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
console.log(chalk.red(`❌ ${finalError}`));
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
throw new Error(finalError);
|
|
592
|
+
}
|
|
593
|
+
// Will retry on next iteration
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (!connected) {
|
|
597
|
+
throw new Error("Failed to establish WebSocket connection");
|
|
598
|
+
}
|
|
599
|
+
// Set up blessed UI or readline interface (only in interactive mode)
|
|
600
|
+
if (!jsonMode) {
|
|
601
|
+
let useBlessed = true;
|
|
602
|
+
// Try to create blessed screen with error handling
|
|
603
|
+
try {
|
|
604
|
+
// Check if terminal supports blessed (basic compatibility check)
|
|
605
|
+
if (!process.stdout.isTTY) {
|
|
606
|
+
throw new Error("Not a TTY - falling back to readline");
|
|
607
|
+
}
|
|
608
|
+
// Create blessed screen and widgets
|
|
609
|
+
screen = blessed.screen({
|
|
610
|
+
smartCSR: true,
|
|
611
|
+
title: "httpcat Chat",
|
|
612
|
+
fullUnicode: true,
|
|
613
|
+
// Add error handling for terminal compatibility
|
|
614
|
+
fastCSR: false, // Disable fast CSR to avoid rendering issues
|
|
615
|
+
});
|
|
616
|
+
// Calculate header height (approximately 10 lines)
|
|
617
|
+
const headerHeight = 10;
|
|
618
|
+
const inputHeight = 3;
|
|
619
|
+
// Create header box (fixed at top)
|
|
620
|
+
headerBox = blessed.box({
|
|
621
|
+
top: 0,
|
|
622
|
+
left: 0,
|
|
623
|
+
width: "100%",
|
|
624
|
+
height: headerHeight,
|
|
625
|
+
content: "",
|
|
626
|
+
tags: false, // Disable tags to avoid rendering issues
|
|
627
|
+
wrap: true,
|
|
628
|
+
scrollable: false,
|
|
629
|
+
alwaysScroll: false,
|
|
630
|
+
padding: {
|
|
631
|
+
left: 1,
|
|
632
|
+
right: 1,
|
|
633
|
+
},
|
|
634
|
+
style: {
|
|
635
|
+
fg: "white",
|
|
636
|
+
bg: "black",
|
|
637
|
+
},
|
|
638
|
+
});
|
|
639
|
+
// Create message log box (scrollable, middle section)
|
|
640
|
+
messageLogBox = blessed.log({
|
|
641
|
+
top: headerHeight,
|
|
642
|
+
left: 0,
|
|
643
|
+
width: "100%",
|
|
644
|
+
height: `100%-${headerHeight + inputHeight}`,
|
|
645
|
+
tags: true,
|
|
646
|
+
scrollable: true,
|
|
647
|
+
alwaysScroll: true,
|
|
648
|
+
scrollbar: {
|
|
649
|
+
ch: " ",
|
|
650
|
+
inverse: true,
|
|
651
|
+
},
|
|
652
|
+
style: {
|
|
653
|
+
fg: "white",
|
|
654
|
+
bg: "black",
|
|
655
|
+
},
|
|
656
|
+
});
|
|
657
|
+
// Create input box (fixed at bottom)
|
|
658
|
+
inputBox = blessed.textbox({
|
|
659
|
+
bottom: 0,
|
|
660
|
+
left: 0,
|
|
661
|
+
width: "100%",
|
|
662
|
+
height: inputHeight,
|
|
663
|
+
content: "",
|
|
664
|
+
inputOnFocus: true,
|
|
665
|
+
tags: true,
|
|
666
|
+
keys: true,
|
|
667
|
+
style: {
|
|
668
|
+
fg: "cyan",
|
|
669
|
+
bg: "black",
|
|
670
|
+
focus: {
|
|
671
|
+
fg: "white",
|
|
672
|
+
bg: "blue",
|
|
673
|
+
},
|
|
674
|
+
},
|
|
675
|
+
});
|
|
676
|
+
// Append widgets to screen
|
|
677
|
+
screen.append(headerBox);
|
|
678
|
+
screen.append(messageLogBox);
|
|
679
|
+
screen.append(inputBox);
|
|
680
|
+
// Test render to catch early errors
|
|
681
|
+
screen.render();
|
|
682
|
+
// Initial header update
|
|
683
|
+
updateHeaderBox(headerBox, leaseInfo, tokenIdentifier, false);
|
|
684
|
+
screen.render();
|
|
685
|
+
}
|
|
686
|
+
catch (blessedError) {
|
|
687
|
+
// Blessed failed - fallback to readline
|
|
688
|
+
useBlessed = false;
|
|
689
|
+
console.error(chalk.yellow("⚠️ Blessed UI initialization failed, falling back to readline interface"));
|
|
690
|
+
console.error(chalk.dim(` Error: ${blessedError instanceof Error ? blessedError.message : String(blessedError)}`));
|
|
691
|
+
// Clean up any partial blessed setup
|
|
692
|
+
if (screen) {
|
|
693
|
+
try {
|
|
694
|
+
screen.destroy();
|
|
695
|
+
}
|
|
696
|
+
catch {
|
|
697
|
+
// Ignore cleanup errors
|
|
698
|
+
}
|
|
699
|
+
screen = null;
|
|
700
|
+
headerBox = null;
|
|
701
|
+
messageLogBox = null;
|
|
702
|
+
inputBox = null;
|
|
703
|
+
}
|
|
704
|
+
// Create readline interface as fallback
|
|
705
|
+
rl = createInterface({
|
|
706
|
+
input: process.stdin,
|
|
707
|
+
output: process.stdout,
|
|
708
|
+
prompt: "",
|
|
709
|
+
});
|
|
710
|
+
// Display header information via console
|
|
711
|
+
console.log();
|
|
712
|
+
console.log(chalk.cyan("═".repeat(80)));
|
|
713
|
+
console.log(chalk.cyan("💬 httpcat Chat Stream"));
|
|
714
|
+
console.log(chalk.cyan("═".repeat(80)));
|
|
715
|
+
console.log();
|
|
716
|
+
console.log(chalk.green("✅ Connected to chat stream"));
|
|
717
|
+
console.log();
|
|
718
|
+
console.log(chalk.dim("💰 Entry fee: $0.01 USDC (10 min lease)"));
|
|
719
|
+
console.log(chalk.dim("💬 Per message: $0.01 USDC"));
|
|
720
|
+
if (leaseInfo) {
|
|
721
|
+
const timeRemaining = formatTimeRemaining(leaseInfo.leaseExpiresAt);
|
|
722
|
+
console.log(chalk.dim(`⏱️ Lease expires in: ${timeRemaining}`));
|
|
723
|
+
}
|
|
724
|
+
console.log();
|
|
725
|
+
console.log(chalk.dim("💡 Type your message and press Enter to send"));
|
|
726
|
+
console.log(chalk.dim("💡 Type /exit or Ctrl+C to quit"));
|
|
727
|
+
console.log(chalk.dim("💡 Type /renew to renew your lease"));
|
|
728
|
+
console.log(chalk.cyan("─".repeat(80)));
|
|
729
|
+
console.log();
|
|
730
|
+
}
|
|
731
|
+
// Display last messages
|
|
732
|
+
if (joinResult.lastMessages.length > 0) {
|
|
733
|
+
const sortedMessages = [...joinResult.lastMessages].sort((a, b) => {
|
|
734
|
+
const timeA = new Date(a.timestamp).getTime();
|
|
735
|
+
const timeB = new Date(b.timestamp).getTime();
|
|
736
|
+
return timeA - timeB; // Oldest first
|
|
737
|
+
});
|
|
738
|
+
sortedMessages.forEach((msg) => {
|
|
739
|
+
displayedMessageIds.add(msg.messageId);
|
|
740
|
+
const isOwn = msg.author === userAddress;
|
|
741
|
+
displayMessage(msg, isOwn, false, messageLogBox || undefined, rl || undefined);
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
// Only set up blessed-specific handlers if using blessed
|
|
745
|
+
if (useBlessed && screen) {
|
|
746
|
+
// Handle terminal resize
|
|
747
|
+
screen.on("resize", () => {
|
|
748
|
+
screen?.render();
|
|
749
|
+
});
|
|
750
|
+
// Update header with lease countdown every second
|
|
751
|
+
headerUpdateInterval = setInterval(() => {
|
|
752
|
+
if (leaseInfo && headerBox) {
|
|
753
|
+
updateHeaderBox(headerBox, leaseInfo, tokenIdentifier, true);
|
|
754
|
+
screen?.render();
|
|
755
|
+
}
|
|
756
|
+
}, 1000);
|
|
757
|
+
}
|
|
758
|
+
// Check lease expiration every 30 seconds (works for both blessed and readline)
|
|
759
|
+
leaseCheckInterval = setInterval(() => {
|
|
760
|
+
if (leaseInfo && leaseInfo.leaseExpiresAt.getTime() <= Date.now()) {
|
|
761
|
+
if (messageLogBox) {
|
|
762
|
+
messageLogBox.log(chalk.yellow("⏱️ Your lease has expired. Type /renew to continue chatting."));
|
|
763
|
+
messageLogBox.setScrollPerc(100);
|
|
764
|
+
screen?.render();
|
|
765
|
+
}
|
|
766
|
+
else if (rl) {
|
|
767
|
+
console.log(chalk.yellow("⏱️ Your lease has expired. Type /renew to continue chatting."));
|
|
768
|
+
}
|
|
769
|
+
leaseInfo = null;
|
|
770
|
+
if (headerBox) {
|
|
771
|
+
updateHeaderBox(headerBox, null, tokenIdentifier, true);
|
|
772
|
+
screen?.render();
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}, 30000);
|
|
776
|
+
// Handle input submission
|
|
777
|
+
if (useBlessed && inputBox) {
|
|
778
|
+
// Blessed input handling
|
|
779
|
+
inputBox.on("submit", async (value) => {
|
|
780
|
+
const trimmed = value.trim();
|
|
781
|
+
// Ignore input while sending
|
|
782
|
+
if (isSending || !trimmed) {
|
|
783
|
+
inputBox?.clearValue();
|
|
784
|
+
inputBox?.focus();
|
|
785
|
+
screen?.render();
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
// Handle commands
|
|
789
|
+
if (trimmed.startsWith("/")) {
|
|
790
|
+
const [cmd] = trimmed.split(" ");
|
|
791
|
+
switch (cmd) {
|
|
792
|
+
case "/exit":
|
|
793
|
+
case "/quit":
|
|
794
|
+
isExiting = true;
|
|
795
|
+
if (leaseCheckInterval)
|
|
796
|
+
clearInterval(leaseCheckInterval);
|
|
797
|
+
if (headerUpdateInterval)
|
|
798
|
+
clearInterval(headerUpdateInterval);
|
|
799
|
+
if (ws)
|
|
800
|
+
ws.close();
|
|
801
|
+
screen?.destroy();
|
|
802
|
+
console.log();
|
|
803
|
+
printCat("sleeping");
|
|
804
|
+
console.log(chalk.cyan("Chat disconnected. Goodbye! 👋"));
|
|
805
|
+
process.exit(0);
|
|
806
|
+
return;
|
|
807
|
+
case "/renew": {
|
|
808
|
+
try {
|
|
809
|
+
inputBox?.setValue("⏱️ Renewing lease...");
|
|
810
|
+
screen?.render();
|
|
811
|
+
const renewal = await renewLease(client, leaseInfo?.leaseId);
|
|
812
|
+
leaseInfo = {
|
|
813
|
+
leaseId: renewal.leaseId,
|
|
814
|
+
leaseExpiresAt: new Date(renewal.leaseExpiresAt),
|
|
815
|
+
};
|
|
816
|
+
// Update header first
|
|
817
|
+
if (headerBox) {
|
|
818
|
+
updateHeaderBox(headerBox, leaseInfo, tokenIdentifier, false);
|
|
819
|
+
}
|
|
820
|
+
if (messageLogBox) {
|
|
821
|
+
messageLogBox.log(chalk.green(`✅ Lease renewed! Expires in ${formatTimeRemaining(leaseInfo.leaseExpiresAt)}`));
|
|
822
|
+
messageLogBox.log(chalk.yellow("🔄 Reconnecting to chat stream..."));
|
|
823
|
+
messageLogBox.setScrollPerc(100);
|
|
824
|
+
screen?.render();
|
|
825
|
+
}
|
|
826
|
+
// Close old connection properly
|
|
827
|
+
if (ws) {
|
|
828
|
+
// Remove all listeners to prevent conflicts
|
|
829
|
+
ws.removeAllListeners();
|
|
830
|
+
// Ensure connection is fully closed
|
|
831
|
+
// WebSocket.OPEN = 1, WebSocket.CONNECTING = 0
|
|
832
|
+
if (ws.readyState === 1 || ws.readyState === 0) {
|
|
833
|
+
ws.close();
|
|
834
|
+
}
|
|
835
|
+
ws = null;
|
|
836
|
+
}
|
|
837
|
+
// Wait for database transaction to commit and old connection to fully close
|
|
838
|
+
// The server validates the lease in the open handler, so we need to ensure
|
|
839
|
+
// the database update is visible before connecting
|
|
840
|
+
// Increased delay to 2 seconds for better database consistency
|
|
841
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
842
|
+
// Reconnect with new lease - with retry logic
|
|
843
|
+
if (wsUrl) {
|
|
844
|
+
const wsUrlObj = new URL(wsUrl);
|
|
845
|
+
wsUrlObj.searchParams.set("leaseId", leaseInfo.leaseId);
|
|
846
|
+
wsUrl = wsUrlObj.toString(); // Update stored URL
|
|
847
|
+
// Retry logic with exponential backoff
|
|
848
|
+
let retryCount = 0;
|
|
849
|
+
const maxRetries = 3;
|
|
850
|
+
let connected = false;
|
|
851
|
+
while (!connected && retryCount < maxRetries) {
|
|
852
|
+
try {
|
|
853
|
+
if (retryCount > 0) {
|
|
854
|
+
const backoffDelay = Math.min(1000 * Math.pow(2, retryCount - 1), 5000);
|
|
855
|
+
if (messageLogBox) {
|
|
856
|
+
messageLogBox.log(chalk.yellow(`🔄 Retry ${retryCount}/${maxRetries} in ${backoffDelay}ms...`));
|
|
857
|
+
messageLogBox.setScrollPerc(100);
|
|
858
|
+
screen?.render();
|
|
859
|
+
}
|
|
860
|
+
await new Promise((resolve) => setTimeout(resolve, backoffDelay));
|
|
861
|
+
}
|
|
862
|
+
// Create new connection and wait for it to open
|
|
863
|
+
const newWsUrl = wsUrl; // TypeScript: ensure it's not null
|
|
864
|
+
await new Promise((resolve, reject) => {
|
|
865
|
+
const newWs = new WebSocket(newWsUrl);
|
|
866
|
+
// Attach permanent handlers FIRST (before temporary handlers)
|
|
867
|
+
attachWebSocketHandlers(newWs);
|
|
868
|
+
// Set up temporary handlers for connection establishment
|
|
869
|
+
let timeout = null;
|
|
870
|
+
let openHandler = null;
|
|
871
|
+
let closeHandler = null;
|
|
872
|
+
let errorHandler = null;
|
|
873
|
+
const cleanup = () => {
|
|
874
|
+
if (timeout)
|
|
875
|
+
clearTimeout(timeout);
|
|
876
|
+
if (openHandler)
|
|
877
|
+
newWs.removeListener("open", openHandler);
|
|
878
|
+
if (closeHandler)
|
|
879
|
+
newWs.removeListener("close", closeHandler);
|
|
880
|
+
if (errorHandler)
|
|
881
|
+
newWs.removeListener("error", errorHandler);
|
|
882
|
+
};
|
|
883
|
+
timeout = setTimeout(() => {
|
|
884
|
+
cleanup();
|
|
885
|
+
newWs.close();
|
|
886
|
+
reject(new Error("Connection timeout - lease may not be ready yet"));
|
|
887
|
+
}, 10000);
|
|
888
|
+
openHandler = () => {
|
|
889
|
+
cleanup();
|
|
890
|
+
ws = newWs; // Assign to outer variable only after successful connection
|
|
891
|
+
resolve();
|
|
892
|
+
};
|
|
893
|
+
closeHandler = (code, reason) => {
|
|
894
|
+
cleanup();
|
|
895
|
+
const reasonStr = reason.toString();
|
|
896
|
+
if (code === 1008 && reasonStr.includes("lease")) {
|
|
897
|
+
reject(new Error("Lease validation failed - please try /renew again"));
|
|
898
|
+
}
|
|
899
|
+
else {
|
|
900
|
+
reject(new Error(`Connection closed: ${code} - ${reasonStr}`));
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
errorHandler = (error) => {
|
|
904
|
+
cleanup();
|
|
905
|
+
reject(error);
|
|
906
|
+
};
|
|
907
|
+
// Attach temporary handlers AFTER permanent handlers
|
|
908
|
+
newWs.once("open", openHandler);
|
|
909
|
+
newWs.once("close", closeHandler);
|
|
910
|
+
newWs.once("error", errorHandler);
|
|
911
|
+
});
|
|
912
|
+
connected = true;
|
|
913
|
+
// Connection established
|
|
914
|
+
if (headerBox) {
|
|
915
|
+
updateHeaderBox(headerBox, leaseInfo, tokenIdentifier, true);
|
|
916
|
+
}
|
|
917
|
+
if (messageLogBox) {
|
|
918
|
+
messageLogBox.log(chalk.green("✅ Reconnected to chat stream"));
|
|
919
|
+
messageLogBox.setScrollPerc(100);
|
|
920
|
+
}
|
|
921
|
+
screen?.render();
|
|
922
|
+
}
|
|
923
|
+
catch (error) {
|
|
924
|
+
retryCount++;
|
|
925
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
926
|
+
if (retryCount >= maxRetries) {
|
|
927
|
+
// Final failure
|
|
928
|
+
throw new Error(`Failed to reconnect after ${maxRetries} attempts: ${errorMsg}`);
|
|
929
|
+
}
|
|
930
|
+
// Log retry attempt
|
|
931
|
+
if (messageLogBox) {
|
|
932
|
+
messageLogBox.log(chalk.yellow(`⚠️ Connection attempt ${retryCount} failed: ${errorMsg}`));
|
|
933
|
+
messageLogBox.setScrollPerc(100);
|
|
934
|
+
screen?.render();
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
inputBox?.clearValue();
|
|
940
|
+
inputBox?.focus();
|
|
941
|
+
screen?.render();
|
|
942
|
+
}
|
|
943
|
+
catch (error) {
|
|
944
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
945
|
+
if (messageLogBox) {
|
|
946
|
+
messageLogBox.log(chalk.red(`❌ ${errorMsg}`));
|
|
947
|
+
messageLogBox.setScrollPerc(100);
|
|
948
|
+
}
|
|
949
|
+
inputBox?.clearValue();
|
|
950
|
+
inputBox?.focus();
|
|
951
|
+
screen?.render();
|
|
952
|
+
}
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
case "/help":
|
|
956
|
+
if (messageLogBox) {
|
|
957
|
+
messageLogBox.log(chalk.dim("Commands: /exit, /quit, /renew, /help"));
|
|
958
|
+
messageLogBox.setScrollPerc(100);
|
|
959
|
+
screen?.render();
|
|
960
|
+
}
|
|
961
|
+
inputBox?.clearValue();
|
|
962
|
+
inputBox?.focus();
|
|
963
|
+
screen?.render();
|
|
964
|
+
return;
|
|
965
|
+
default:
|
|
966
|
+
if (messageLogBox) {
|
|
967
|
+
messageLogBox.log(chalk.yellow(`Unknown command: ${cmd}. Type /help for commands.`));
|
|
968
|
+
messageLogBox.setScrollPerc(100);
|
|
969
|
+
screen?.render();
|
|
970
|
+
}
|
|
971
|
+
inputBox?.clearValue();
|
|
972
|
+
inputBox?.focus();
|
|
973
|
+
screen?.render();
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
// Check if websocket is still connected
|
|
978
|
+
// WebSocket.OPEN = 1
|
|
979
|
+
if (!ws || ws.readyState !== 1) {
|
|
980
|
+
const stateMsg = ws
|
|
981
|
+
? ws.readyState === 0 ? "connecting"
|
|
982
|
+
: ws.readyState === 2 ? "closing"
|
|
983
|
+
: ws.readyState === 3 ? "closed"
|
|
984
|
+
: "unknown"
|
|
985
|
+
: "not initialized";
|
|
986
|
+
if (messageLogBox) {
|
|
987
|
+
messageLogBox.log(chalk.red(`❌ WebSocket is not connected (state: ${stateMsg}). Please wait for connection or type /renew.`));
|
|
988
|
+
messageLogBox.setScrollPerc(100);
|
|
989
|
+
screen?.render();
|
|
990
|
+
}
|
|
991
|
+
inputBox?.clearValue();
|
|
992
|
+
inputBox?.focus();
|
|
993
|
+
screen?.render();
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
// Check if lease is valid
|
|
997
|
+
if (!leaseInfo || leaseInfo.leaseExpiresAt.getTime() <= Date.now()) {
|
|
998
|
+
if (messageLogBox) {
|
|
999
|
+
messageLogBox.log(chalk.yellow("⏱️ Your lease has expired. Type /renew to continue chatting."));
|
|
1000
|
+
messageLogBox.setScrollPerc(100);
|
|
1001
|
+
screen?.render();
|
|
1002
|
+
}
|
|
1003
|
+
inputBox?.clearValue();
|
|
1004
|
+
inputBox?.focus();
|
|
1005
|
+
screen?.render();
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
// Send message
|
|
1009
|
+
isSending = true;
|
|
1010
|
+
currentInput = trimmed;
|
|
1011
|
+
// Show pulsing animation in input box
|
|
1012
|
+
let pulseCount = 0;
|
|
1013
|
+
let pulseInterval = null;
|
|
1014
|
+
const tempMessageId = `pending-${Date.now()}-${Math.random()}`;
|
|
1015
|
+
const updatePulse = () => {
|
|
1016
|
+
if (!inputBox || isCleaningUp)
|
|
1017
|
+
return;
|
|
1018
|
+
pulseCount++;
|
|
1019
|
+
const pulseChar = pulseCount % 2 === 0 ? "●" : "○";
|
|
1020
|
+
inputBox.setValue(`${trimmed} ${pulseChar}`);
|
|
1021
|
+
screen?.render();
|
|
1022
|
+
};
|
|
1023
|
+
// Initial pulse
|
|
1024
|
+
updatePulse();
|
|
1025
|
+
pulseInterval = setInterval(updatePulse, 500);
|
|
1026
|
+
pulseIntervals.set(tempMessageId, pulseInterval);
|
|
1027
|
+
try {
|
|
1028
|
+
// Ensure lease is valid before sending
|
|
1029
|
+
leaseInfo = await ensureLeaseValid(client, leaseInfo, messageLogBox || undefined, screen || undefined);
|
|
1030
|
+
if (headerBox) {
|
|
1031
|
+
updateHeaderBox(headerBox, leaseInfo, tokenIdentifier, true);
|
|
1032
|
+
screen?.render();
|
|
1033
|
+
}
|
|
1034
|
+
// Double-check websocket is still connected after lease renewal
|
|
1035
|
+
// WebSocket.OPEN = 1
|
|
1036
|
+
if (!ws || ws.readyState !== 1) {
|
|
1037
|
+
throw new Error("WebSocket connection lost. Please wait for reconnection.");
|
|
1038
|
+
}
|
|
1039
|
+
const result = await sendChatMessage(client, trimmed, leaseInfo.leaseId);
|
|
1040
|
+
userAddress = result.author;
|
|
1041
|
+
// Track this message
|
|
1042
|
+
pendingMessages.set(result.messageId, tempMessageId);
|
|
1043
|
+
pendingMessages.set(`text:${trimmed}`, tempMessageId);
|
|
1044
|
+
// Keep pulsing until WebSocket confirms
|
|
1045
|
+
}
|
|
1046
|
+
catch (error) {
|
|
1047
|
+
// Stop pulsing
|
|
1048
|
+
if (pulseInterval) {
|
|
1049
|
+
clearInterval(pulseInterval);
|
|
1050
|
+
pulseIntervals.delete(tempMessageId);
|
|
1051
|
+
}
|
|
1052
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1053
|
+
if (messageLogBox) {
|
|
1054
|
+
messageLogBox.log(chalk.red(`❌ ${errorMsg}`));
|
|
1055
|
+
messageLogBox.setScrollPerc(100);
|
|
1056
|
+
}
|
|
1057
|
+
inputBox?.clearValue();
|
|
1058
|
+
inputBox?.focus();
|
|
1059
|
+
isSending = false;
|
|
1060
|
+
currentInput = "";
|
|
1061
|
+
screen?.render();
|
|
1062
|
+
}
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
else if (rl) {
|
|
1066
|
+
// Readline input handling
|
|
1067
|
+
const readlineInterface = rl; // Store reference for TypeScript
|
|
1068
|
+
readlineInterface.setPrompt("");
|
|
1069
|
+
readlineInterface.prompt();
|
|
1070
|
+
readlineInterface.on("line", async (line) => {
|
|
1071
|
+
const trimmed = line.trim();
|
|
1072
|
+
// Ignore empty input
|
|
1073
|
+
if (!trimmed) {
|
|
1074
|
+
readlineInterface.prompt();
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
// Handle commands
|
|
1078
|
+
if (trimmed.startsWith("/")) {
|
|
1079
|
+
const [cmd] = trimmed.split(" ");
|
|
1080
|
+
switch (cmd) {
|
|
1081
|
+
case "/exit":
|
|
1082
|
+
case "/quit":
|
|
1083
|
+
isExiting = true;
|
|
1084
|
+
if (leaseCheckInterval)
|
|
1085
|
+
clearInterval(leaseCheckInterval);
|
|
1086
|
+
if (headerUpdateInterval)
|
|
1087
|
+
clearInterval(headerUpdateInterval);
|
|
1088
|
+
if (ws)
|
|
1089
|
+
ws.close();
|
|
1090
|
+
readlineInterface.close();
|
|
1091
|
+
console.log();
|
|
1092
|
+
printCat("sleeping");
|
|
1093
|
+
console.log(chalk.cyan("Chat disconnected. Goodbye! 👋"));
|
|
1094
|
+
process.exit(0);
|
|
1095
|
+
return;
|
|
1096
|
+
case "/renew": {
|
|
1097
|
+
try {
|
|
1098
|
+
console.log(chalk.yellow("⏱️ Renewing lease..."));
|
|
1099
|
+
const renewal = await renewLease(client, leaseInfo?.leaseId);
|
|
1100
|
+
leaseInfo = {
|
|
1101
|
+
leaseId: renewal.leaseId,
|
|
1102
|
+
leaseExpiresAt: new Date(renewal.leaseExpiresAt),
|
|
1103
|
+
};
|
|
1104
|
+
console.log(chalk.green(`✅ Lease renewed! Expires in ${formatTimeRemaining(leaseInfo.leaseExpiresAt)}`));
|
|
1105
|
+
console.log(chalk.yellow("🔄 Reconnecting to chat stream..."));
|
|
1106
|
+
// Close old connection properly
|
|
1107
|
+
if (ws) {
|
|
1108
|
+
ws.removeAllListeners();
|
|
1109
|
+
// WebSocket.OPEN = 1, WebSocket.CONNECTING = 0
|
|
1110
|
+
if (ws.readyState === 1 || ws.readyState === 0) {
|
|
1111
|
+
ws.close();
|
|
1112
|
+
}
|
|
1113
|
+
ws = null;
|
|
1114
|
+
}
|
|
1115
|
+
// Wait for database transaction to commit
|
|
1116
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1117
|
+
// Reconnect with new lease
|
|
1118
|
+
if (wsUrl) {
|
|
1119
|
+
const wsUrlObj = new URL(wsUrl);
|
|
1120
|
+
wsUrlObj.searchParams.set("leaseId", leaseInfo.leaseId);
|
|
1121
|
+
wsUrl = wsUrlObj.toString();
|
|
1122
|
+
// Retry logic
|
|
1123
|
+
let retryCount = 0;
|
|
1124
|
+
const maxRetries = 3;
|
|
1125
|
+
let connected = false;
|
|
1126
|
+
while (!connected && retryCount < maxRetries) {
|
|
1127
|
+
try {
|
|
1128
|
+
if (retryCount > 0) {
|
|
1129
|
+
const backoffDelay = Math.min(1000 * Math.pow(2, retryCount - 1), 5000);
|
|
1130
|
+
console.log(chalk.yellow(`🔄 Retry ${retryCount}/${maxRetries} in ${backoffDelay}ms...`));
|
|
1131
|
+
await new Promise((resolve) => setTimeout(resolve, backoffDelay));
|
|
1132
|
+
}
|
|
1133
|
+
const newWsUrl = wsUrl;
|
|
1134
|
+
await new Promise((resolve, reject) => {
|
|
1135
|
+
const newWs = new WebSocket(newWsUrl);
|
|
1136
|
+
attachWebSocketHandlers(newWs);
|
|
1137
|
+
let timeout = null;
|
|
1138
|
+
let openHandler = null;
|
|
1139
|
+
let closeHandler = null;
|
|
1140
|
+
let errorHandler = null;
|
|
1141
|
+
const cleanup = () => {
|
|
1142
|
+
if (timeout)
|
|
1143
|
+
clearTimeout(timeout);
|
|
1144
|
+
if (openHandler)
|
|
1145
|
+
newWs.removeListener("open", openHandler);
|
|
1146
|
+
if (closeHandler)
|
|
1147
|
+
newWs.removeListener("close", closeHandler);
|
|
1148
|
+
if (errorHandler)
|
|
1149
|
+
newWs.removeListener("error", errorHandler);
|
|
1150
|
+
};
|
|
1151
|
+
timeout = setTimeout(() => {
|
|
1152
|
+
cleanup();
|
|
1153
|
+
newWs.close();
|
|
1154
|
+
reject(new Error("Connection timeout"));
|
|
1155
|
+
}, 10000);
|
|
1156
|
+
openHandler = () => {
|
|
1157
|
+
cleanup();
|
|
1158
|
+
ws = newWs;
|
|
1159
|
+
resolve();
|
|
1160
|
+
};
|
|
1161
|
+
closeHandler = (code, reason) => {
|
|
1162
|
+
cleanup();
|
|
1163
|
+
const reasonStr = reason.toString();
|
|
1164
|
+
if (code === 1008 && reasonStr.includes("lease")) {
|
|
1165
|
+
reject(new Error("Lease validation failed"));
|
|
1166
|
+
}
|
|
1167
|
+
else {
|
|
1168
|
+
reject(new Error(`Connection closed: ${code} - ${reasonStr}`));
|
|
1169
|
+
}
|
|
1170
|
+
};
|
|
1171
|
+
errorHandler = (error) => {
|
|
1172
|
+
cleanup();
|
|
1173
|
+
reject(error);
|
|
1174
|
+
};
|
|
1175
|
+
newWs.once("open", openHandler);
|
|
1176
|
+
newWs.once("close", closeHandler);
|
|
1177
|
+
newWs.once("error", errorHandler);
|
|
1178
|
+
});
|
|
1179
|
+
connected = true;
|
|
1180
|
+
console.log(chalk.green("✅ Reconnected to chat stream"));
|
|
1181
|
+
}
|
|
1182
|
+
catch (error) {
|
|
1183
|
+
retryCount++;
|
|
1184
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1185
|
+
if (retryCount >= maxRetries) {
|
|
1186
|
+
throw new Error(`Failed to reconnect after ${maxRetries} attempts: ${errorMsg}`);
|
|
1187
|
+
}
|
|
1188
|
+
console.log(chalk.yellow(`⚠️ Connection attempt ${retryCount} failed: ${errorMsg}`));
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
if (readlineInterface)
|
|
1193
|
+
readlineInterface.prompt();
|
|
1194
|
+
}
|
|
1195
|
+
catch (error) {
|
|
1196
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1197
|
+
console.log(chalk.red(`❌ ${errorMsg}`));
|
|
1198
|
+
if (readlineInterface)
|
|
1199
|
+
readlineInterface.prompt();
|
|
1200
|
+
}
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
case "/help":
|
|
1204
|
+
console.log(chalk.dim("Commands: /exit, /quit, /renew, /help"));
|
|
1205
|
+
if (readlineInterface)
|
|
1206
|
+
readlineInterface.prompt();
|
|
1207
|
+
return;
|
|
1208
|
+
default:
|
|
1209
|
+
console.log(chalk.yellow(`Unknown command: ${cmd}. Type /help for commands.`));
|
|
1210
|
+
if (readlineInterface)
|
|
1211
|
+
readlineInterface.prompt();
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
// Check if websocket is still connected
|
|
1216
|
+
if (!ws || ws.readyState !== 1) {
|
|
1217
|
+
const stateMsg = ws
|
|
1218
|
+
? ws.readyState === 0 ? "connecting"
|
|
1219
|
+
: ws.readyState === 2 ? "closing"
|
|
1220
|
+
: ws.readyState === 3 ? "closed"
|
|
1221
|
+
: "unknown"
|
|
1222
|
+
: "not initialized";
|
|
1223
|
+
console.log(chalk.red(`❌ WebSocket is not connected (state: ${stateMsg}). Please wait for connection or type /renew.`));
|
|
1224
|
+
if (readlineInterface)
|
|
1225
|
+
readlineInterface.prompt();
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
// Check if lease is valid
|
|
1229
|
+
if (!leaseInfo || leaseInfo.leaseExpiresAt.getTime() <= Date.now()) {
|
|
1230
|
+
console.log(chalk.yellow("⏱️ Your lease has expired. Type /renew to continue chatting."));
|
|
1231
|
+
if (readlineInterface)
|
|
1232
|
+
readlineInterface.prompt();
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
// Send message
|
|
1236
|
+
isSending = true;
|
|
1237
|
+
currentInput = trimmed;
|
|
1238
|
+
try {
|
|
1239
|
+
// Ensure lease is valid before sending
|
|
1240
|
+
leaseInfo = await ensureLeaseValid(client, leaseInfo, undefined, undefined);
|
|
1241
|
+
// Double-check websocket is still connected
|
|
1242
|
+
if (!ws || ws.readyState !== 1) {
|
|
1243
|
+
throw new Error("WebSocket connection lost. Please wait for reconnection.");
|
|
1244
|
+
}
|
|
1245
|
+
const result = await sendChatMessage(client, trimmed, leaseInfo.leaseId);
|
|
1246
|
+
userAddress = result.author;
|
|
1247
|
+
// Track this message
|
|
1248
|
+
const tempMessageId = `pending-${Date.now()}-${Math.random()}`;
|
|
1249
|
+
pendingMessages.set(result.messageId, tempMessageId);
|
|
1250
|
+
pendingMessages.set(`text:${trimmed}`, tempMessageId);
|
|
1251
|
+
// Message sent - will be displayed when received via WebSocket
|
|
1252
|
+
}
|
|
1253
|
+
catch (error) {
|
|
1254
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1255
|
+
console.log(chalk.red(`❌ ${errorMsg}`));
|
|
1256
|
+
isSending = false;
|
|
1257
|
+
currentInput = "";
|
|
1258
|
+
}
|
|
1259
|
+
if (readlineInterface)
|
|
1260
|
+
readlineInterface.prompt();
|
|
1261
|
+
});
|
|
1262
|
+
// Handle Ctrl+C for readline
|
|
1263
|
+
readlineInterface.on("SIGINT", () => {
|
|
1264
|
+
isExiting = true;
|
|
1265
|
+
isSending = false;
|
|
1266
|
+
pulseIntervals.forEach((interval) => clearInterval(interval));
|
|
1267
|
+
pulseIntervals.clear();
|
|
1268
|
+
if (leaseCheckInterval)
|
|
1269
|
+
clearInterval(leaseCheckInterval);
|
|
1270
|
+
if (headerUpdateInterval)
|
|
1271
|
+
clearInterval(headerUpdateInterval);
|
|
1272
|
+
if (ws)
|
|
1273
|
+
ws.close();
|
|
1274
|
+
if (readlineInterface)
|
|
1275
|
+
readlineInterface.close();
|
|
1276
|
+
console.log();
|
|
1277
|
+
printCat("sleeping");
|
|
1278
|
+
console.log(chalk.cyan("Chat disconnected. Goodbye! 👋"));
|
|
1279
|
+
process.exit(0);
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
// Handle Ctrl+C (for blessed mode)
|
|
1283
|
+
if (useBlessed && screen) {
|
|
1284
|
+
screen.key(["C-c"], () => {
|
|
1285
|
+
isExiting = true;
|
|
1286
|
+
isSending = false;
|
|
1287
|
+
pulseIntervals.forEach((interval) => clearInterval(interval));
|
|
1288
|
+
pulseIntervals.clear();
|
|
1289
|
+
if (leaseCheckInterval)
|
|
1290
|
+
clearInterval(leaseCheckInterval);
|
|
1291
|
+
if (headerUpdateInterval)
|
|
1292
|
+
clearInterval(headerUpdateInterval);
|
|
1293
|
+
if (ws)
|
|
1294
|
+
ws.close();
|
|
1295
|
+
screen?.destroy();
|
|
1296
|
+
console.log();
|
|
1297
|
+
printCat("sleeping");
|
|
1298
|
+
console.log(chalk.cyan("Chat disconnected. Goodbye! 👋"));
|
|
1299
|
+
process.exit(0);
|
|
1300
|
+
});
|
|
1301
|
+
// Focus input box and render (only for blessed)
|
|
1302
|
+
if (inputBox) {
|
|
1303
|
+
inputBox.focus();
|
|
1304
|
+
screen.render();
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
else {
|
|
1309
|
+
// JSON mode: keep process alive and stream messages
|
|
1310
|
+
process.on("SIGINT", () => {
|
|
1311
|
+
isExiting = true;
|
|
1312
|
+
if (ws)
|
|
1313
|
+
ws.close();
|
|
1314
|
+
process.exit(0);
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
catch (error) {
|
|
1319
|
+
// Cleanup on error
|
|
1320
|
+
try {
|
|
1321
|
+
if (leaseCheckInterval)
|
|
1322
|
+
clearInterval(leaseCheckInterval);
|
|
1323
|
+
if (headerUpdateInterval)
|
|
1324
|
+
clearInterval(headerUpdateInterval);
|
|
1325
|
+
pulseIntervals.forEach((interval) => clearInterval(interval));
|
|
1326
|
+
pulseIntervals.clear();
|
|
1327
|
+
if (ws !== null) {
|
|
1328
|
+
try {
|
|
1329
|
+
ws.removeAllListeners();
|
|
1330
|
+
// WebSocket.OPEN = 1, WebSocket.CONNECTING = 0
|
|
1331
|
+
const wsState = ws.readyState;
|
|
1332
|
+
if (wsState === 1 || wsState === 0) {
|
|
1333
|
+
ws.close();
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
catch {
|
|
1337
|
+
// Ignore cleanup errors
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
if (screen) {
|
|
1341
|
+
screen.destroy();
|
|
1342
|
+
}
|
|
1343
|
+
if (rl) {
|
|
1344
|
+
rl.close();
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
catch (cleanupError) {
|
|
1348
|
+
// Ignore cleanup errors
|
|
1349
|
+
}
|
|
1350
|
+
if (jsonMode) {
|
|
1351
|
+
console.log(JSON.stringify({
|
|
1352
|
+
type: "error",
|
|
1353
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1354
|
+
}));
|
|
1355
|
+
}
|
|
1356
|
+
else {
|
|
1357
|
+
handleError(error, config.get("preferences")?.verboseLogging);
|
|
1358
|
+
}
|
|
1359
|
+
process.exit(1);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
export function displayChatResult(result) {
|
|
1363
|
+
console.log();
|
|
1364
|
+
console.log(chalk.green("✅ Message sent!"));
|
|
1365
|
+
console.log(chalk.dim(` ID: ${result.messageId}`));
|
|
1366
|
+
console.log(chalk.dim(` Time: ${new Date(result.timestamp).toLocaleString()}`));
|
|
1367
|
+
console.log();
|
|
1368
|
+
}
|
|
1369
|
+
//# sourceMappingURL=chat.js.map
|