httpcat-cli 0.0.9 → 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/chat-automation.js +171 -0
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +685 -138
- package/dist/commands/chat.js.map +1 -1
- 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 +1 -1
package/dist/commands/chat.js
CHANGED
|
@@ -3,16 +3,58 @@ import WebSocket from "ws";
|
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
// @ts-ignore - neo-blessed doesn't have types, but @types/blessed provides compatible types
|
|
5
5
|
import blessed from "neo-blessed";
|
|
6
|
-
import { formatAddress } from "../utils/formatting.js";
|
|
6
|
+
import { formatAddress, formatTokenAmount, formatCurrency } from "../utils/formatting.js";
|
|
7
7
|
import { handleError } from "../utils/errors.js";
|
|
8
8
|
import { config } from "../config.js";
|
|
9
9
|
import { printCat } from "../interactive/art.js";
|
|
10
10
|
import { resolveTokenId } from "../utils/token-resolver.js";
|
|
11
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";
|
|
12
15
|
const CHAT_JOIN_ENTRYPOINT = "chat_join";
|
|
13
16
|
const CHAT_MESSAGE_ENTRYPOINT = "chat_message";
|
|
14
17
|
const CHAT_RENEW_LEASE_ENTRYPOINT = "chat_renew_lease";
|
|
15
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
|
+
}
|
|
16
58
|
export async function joinChat(client, tokenIdentifier, silent = false) {
|
|
17
59
|
let tokenId;
|
|
18
60
|
// If token identifier provided, resolve it to tokenId
|
|
@@ -128,14 +170,42 @@ function formatTimeRemaining(expiresAt) {
|
|
|
128
170
|
}
|
|
129
171
|
return `${seconds}s`;
|
|
130
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
|
+
}
|
|
131
196
|
function updateHeaderBox(headerBox, leaseInfo, tokenName, isConnected = false) {
|
|
132
197
|
const title = tokenName
|
|
133
198
|
? `💬 httpcat Chat: ${tokenName}`
|
|
134
199
|
: "💬 httpcat Chat Stream";
|
|
135
200
|
// Get terminal width, default to 80 if not available
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
const
|
|
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);
|
|
139
209
|
// Build content line by line - use plain text (blessed will handle wrapping)
|
|
140
210
|
const lines = [];
|
|
141
211
|
lines.push(title);
|
|
@@ -157,8 +227,13 @@ function updateHeaderBox(headerBox, leaseInfo, tokenName, isConnected = false) {
|
|
|
157
227
|
lines.push("💡 Type your message and press Enter to send");
|
|
158
228
|
lines.push("💡 Type /exit or Ctrl+C to quit");
|
|
159
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
|
+
}
|
|
160
234
|
lines.push("");
|
|
161
235
|
lines.push(lineSeparator);
|
|
236
|
+
// Clear and set content to prevent overlapping
|
|
162
237
|
headerBox.setContent(lines.join("\n"));
|
|
163
238
|
}
|
|
164
239
|
async function ensureLeaseValid(client, leaseInfo, messageLogBox, screen) {
|
|
@@ -244,9 +319,16 @@ export async function startChatStream(client, jsonMode = false, tokenIdentifier)
|
|
|
244
319
|
if (!leaseInfo || leaseInfo.leaseExpiresAt.getTime() <= Date.now()) {
|
|
245
320
|
throw new Error("Lease is invalid or expired. Please try again.");
|
|
246
321
|
}
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
const
|
|
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);
|
|
250
332
|
wsUrlObj.searchParams.set("leaseId", joinResult.leaseId);
|
|
251
333
|
wsUrl = wsUrlObj.toString();
|
|
252
334
|
// Helper function to attach websocket handlers
|
|
@@ -317,8 +399,9 @@ export async function startChatStream(client, jsonMode = false, tokenIdentifier)
|
|
|
317
399
|
process.stdout.write("\r\x1b[K");
|
|
318
400
|
}
|
|
319
401
|
// Display the message (only if not already displayed)
|
|
402
|
+
// In JSON mode, displayMessage should never be called, but add explicit check
|
|
320
403
|
if (!displayedMessageIds.has(msg.messageId)) {
|
|
321
|
-
displayMessage(msg, isOwn, false, messageLogBox || undefined, rl || undefined);
|
|
404
|
+
displayMessage(msg, isOwn, false, messageLogBox || undefined, jsonMode ? undefined : (rl || undefined));
|
|
322
405
|
}
|
|
323
406
|
// Re-enable input - clear the input line completely
|
|
324
407
|
isSending = false;
|
|
@@ -338,8 +421,9 @@ export async function startChatStream(client, jsonMode = false, tokenIdentifier)
|
|
|
338
421
|
}
|
|
339
422
|
else {
|
|
340
423
|
// Not our message, just display it normally
|
|
424
|
+
// In JSON mode, displayMessage should never be called, but add explicit check
|
|
341
425
|
if (!displayedMessageIds.has(msg.messageId)) {
|
|
342
|
-
displayMessage(msg, isOwn, false, messageLogBox || undefined, rl || undefined);
|
|
426
|
+
displayMessage(msg, isOwn, false, messageLogBox || undefined, jsonMode ? undefined : (rl || undefined));
|
|
343
427
|
}
|
|
344
428
|
}
|
|
345
429
|
}
|
|
@@ -596,112 +680,40 @@ export async function startChatStream(client, jsonMode = false, tokenIdentifier)
|
|
|
596
680
|
if (!connected) {
|
|
597
681
|
throw new Error("Failed to establish WebSocket connection");
|
|
598
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);
|
|
599
707
|
// Set up blessed UI or readline interface (only in interactive mode)
|
|
600
708
|
if (!jsonMode) {
|
|
601
709
|
let useBlessed = true;
|
|
602
|
-
//
|
|
603
|
-
|
|
604
|
-
|
|
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
|
|
710
|
+
// Check if terminal supports blessed (basic compatibility check)
|
|
711
|
+
// If not a TTY, silently fall back to readline
|
|
712
|
+
if (!process.stdout.isTTY) {
|
|
688
713
|
useBlessed = false;
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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
|
|
714
|
+
}
|
|
715
|
+
// Helper function to set up readline interface
|
|
716
|
+
const setupReadline = () => {
|
|
705
717
|
rl = createInterface({
|
|
706
718
|
input: process.stdin,
|
|
707
719
|
output: process.stdout,
|
|
@@ -725,8 +737,123 @@ export async function startChatStream(client, jsonMode = false, tokenIdentifier)
|
|
|
725
737
|
console.log(chalk.dim("💡 Type your message and press Enter to send"));
|
|
726
738
|
console.log(chalk.dim("💡 Type /exit or Ctrl+C to quit"));
|
|
727
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
|
+
}
|
|
728
744
|
console.log(chalk.cyan("─".repeat(80)));
|
|
729
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();
|
|
730
857
|
}
|
|
731
858
|
// Display last messages
|
|
732
859
|
if (joinResult.lastMessages.length > 0) {
|
|
@@ -738,7 +865,8 @@ export async function startChatStream(client, jsonMode = false, tokenIdentifier)
|
|
|
738
865
|
sortedMessages.forEach((msg) => {
|
|
739
866
|
displayedMessageIds.add(msg.messageId);
|
|
740
867
|
const isOwn = msg.author === userAddress;
|
|
741
|
-
|
|
868
|
+
// This is inside !jsonMode block, but add explicit check for safety
|
|
869
|
+
displayMessage(msg, isOwn, false, messageLogBox || undefined, jsonMode ? undefined : (rl || undefined));
|
|
742
870
|
});
|
|
743
871
|
}
|
|
744
872
|
// Only set up blessed-specific handlers if using blessed
|
|
@@ -749,30 +877,12 @@ export async function startChatStream(client, jsonMode = false, tokenIdentifier)
|
|
|
749
877
|
});
|
|
750
878
|
// Update header with lease countdown every second
|
|
751
879
|
headerUpdateInterval = setInterval(() => {
|
|
752
|
-
if (leaseInfo && headerBox) {
|
|
880
|
+
if (leaseInfo && headerBox && screen && !isExiting) {
|
|
753
881
|
updateHeaderBox(headerBox, leaseInfo, tokenIdentifier, true);
|
|
754
|
-
screen
|
|
882
|
+
screen.render();
|
|
755
883
|
}
|
|
756
884
|
}, 1000);
|
|
757
885
|
}
|
|
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
886
|
// Handle input submission
|
|
777
887
|
if (useBlessed && inputBox) {
|
|
778
888
|
// Blessed input handling
|
|
@@ -841,7 +951,10 @@ export async function startChatStream(client, jsonMode = false, tokenIdentifier)
|
|
|
841
951
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
842
952
|
// Reconnect with new lease - with retry logic
|
|
843
953
|
if (wsUrl) {
|
|
844
|
-
|
|
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);
|
|
845
958
|
wsUrlObj.searchParams.set("leaseId", leaseInfo.leaseId);
|
|
846
959
|
wsUrl = wsUrlObj.toString(); // Update stored URL
|
|
847
960
|
// Retry logic with exponential backoff
|
|
@@ -952,9 +1065,130 @@ export async function startChatStream(client, jsonMode = false, tokenIdentifier)
|
|
|
952
1065
|
}
|
|
953
1066
|
return;
|
|
954
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
|
+
}
|
|
955
1186
|
case "/help":
|
|
956
1187
|
if (messageLogBox) {
|
|
957
|
-
|
|
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);
|
|
958
1192
|
messageLogBox.setScrollPerc(100);
|
|
959
1193
|
screen?.render();
|
|
960
1194
|
}
|
|
@@ -1116,7 +1350,10 @@ export async function startChatStream(client, jsonMode = false, tokenIdentifier)
|
|
|
1116
1350
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
1117
1351
|
// Reconnect with new lease
|
|
1118
1352
|
if (wsUrl) {
|
|
1119
|
-
|
|
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);
|
|
1120
1357
|
wsUrlObj.searchParams.set("leaseId", leaseInfo.leaseId);
|
|
1121
1358
|
wsUrl = wsUrlObj.toString();
|
|
1122
1359
|
// Retry logic
|
|
@@ -1200,8 +1437,87 @@ export async function startChatStream(client, jsonMode = false, tokenIdentifier)
|
|
|
1200
1437
|
}
|
|
1201
1438
|
return;
|
|
1202
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
|
+
}
|
|
1203
1516
|
case "/help":
|
|
1204
|
-
|
|
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);
|
|
1205
1521
|
if (readlineInterface)
|
|
1206
1522
|
readlineInterface.prompt();
|
|
1207
1523
|
return;
|
|
@@ -1306,11 +1622,241 @@ export async function startChatStream(client, jsonMode = false, tokenIdentifier)
|
|
|
1306
1622
|
}
|
|
1307
1623
|
}
|
|
1308
1624
|
else {
|
|
1309
|
-
// JSON mode:
|
|
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
|
+
});
|
|
1310
1850
|
process.on("SIGINT", () => {
|
|
1311
1851
|
isExiting = true;
|
|
1852
|
+
if (leaseCheckInterval)
|
|
1853
|
+
clearInterval(leaseCheckInterval);
|
|
1854
|
+
if (headerUpdateInterval)
|
|
1855
|
+
clearInterval(headerUpdateInterval);
|
|
1312
1856
|
if (ws)
|
|
1313
1857
|
ws.close();
|
|
1858
|
+
// Don't try to use readline in JSON mode - it should never exist
|
|
1859
|
+
console.log(JSON.stringify({ type: 'exiting' }));
|
|
1314
1860
|
process.exit(0);
|
|
1315
1861
|
});
|
|
1316
1862
|
}
|
|
@@ -1340,7 +1886,8 @@ export async function startChatStream(client, jsonMode = false, tokenIdentifier)
|
|
|
1340
1886
|
if (screen) {
|
|
1341
1887
|
screen.destroy();
|
|
1342
1888
|
}
|
|
1343
|
-
if (
|
|
1889
|
+
// Only close readline if not in JSON mode (readline should never exist in JSON mode)
|
|
1890
|
+
if (rl && !jsonMode) {
|
|
1344
1891
|
rl.close();
|
|
1345
1892
|
}
|
|
1346
1893
|
}
|