httpcat-cli 0.0.27 → 0.1.1-rc.1

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.
Files changed (70) hide show
  1. package/.github/dependabot.yml +2 -0
  2. package/.github/workflows/README.md +37 -4
  3. package/.github/workflows/ci.yml +31 -20
  4. package/.github/workflows/homebrew-tap.yml +1 -1
  5. package/.github/workflows/publish-switch.yml +41 -0
  6. package/.github/workflows/rc-publish.yml +196 -0
  7. package/.github/workflows/release.yml +267 -85
  8. package/README.md +107 -108
  9. package/bun.lock +2933 -0
  10. package/dist/commands/account.d.ts.map +1 -1
  11. package/dist/commands/account.js +14 -7
  12. package/dist/commands/account.js.map +1 -1
  13. package/dist/commands/balances.d.ts.map +1 -0
  14. package/dist/commands/balances.js +171 -0
  15. package/dist/commands/balances.js.map +1 -0
  16. package/dist/commands/buy.d.ts.map +1 -1
  17. package/dist/commands/buy.js +743 -35
  18. package/dist/commands/buy.js.map +1 -1
  19. package/dist/commands/chat.d.ts.map +1 -1
  20. package/dist/commands/chat.js +467 -906
  21. package/dist/commands/chat.js.map +1 -1
  22. package/dist/commands/claim.d.ts.map +1 -0
  23. package/dist/commands/claim.js +65 -0
  24. package/dist/commands/claim.js.map +1 -0
  25. package/dist/commands/create.d.ts.map +1 -1
  26. package/dist/commands/create.js +0 -1
  27. package/dist/commands/create.js.map +1 -1
  28. package/dist/commands/info.d.ts.map +1 -1
  29. package/dist/commands/info.js +143 -38
  30. package/dist/commands/info.js.map +1 -1
  31. package/dist/commands/list.d.ts.map +1 -1
  32. package/dist/commands/list.js +31 -27
  33. package/dist/commands/list.js.map +1 -1
  34. package/dist/commands/positions.d.ts.map +1 -1
  35. package/dist/commands/positions.js +178 -106
  36. package/dist/commands/positions.js.map +1 -1
  37. package/dist/commands/sell.d.ts.map +1 -1
  38. package/dist/commands/sell.js +720 -28
  39. package/dist/commands/sell.js.map +1 -1
  40. package/dist/index.js +321 -104
  41. package/dist/index.js.map +1 -1
  42. package/dist/interactive/shell.d.ts.map +1 -1
  43. package/dist/interactive/shell.js +328 -179
  44. package/dist/interactive/shell.js.map +1 -1
  45. package/dist/mcp/tools.d.ts.map +1 -1
  46. package/dist/mcp/tools.js +8 -8
  47. package/dist/mcp/tools.js.map +1 -1
  48. package/dist/utils/constants.d.ts.map +1 -0
  49. package/dist/utils/constants.js +66 -0
  50. package/dist/utils/constants.js.map +1 -0
  51. package/dist/utils/formatting.d.ts.map +1 -1
  52. package/dist/utils/formatting.js +3 -5
  53. package/dist/utils/formatting.js.map +1 -1
  54. package/dist/utils/token-resolver.d.ts.map +1 -1
  55. package/dist/utils/token-resolver.js +70 -68
  56. package/dist/utils/token-resolver.js.map +1 -1
  57. package/dist/utils/validation.d.ts.map +1 -1
  58. package/dist/utils/validation.js +4 -3
  59. package/dist/utils/validation.js.map +1 -1
  60. package/jest.config.js +1 -1
  61. package/package.json +19 -13
  62. package/tests/README.md +0 -1
  63. package/.claude/settings.local.json +0 -41
  64. package/dist/commands/balance.d.ts.map +0 -1
  65. package/dist/commands/balance.js +0 -112
  66. package/dist/commands/balance.js.map +0 -1
  67. package/homebrew-httpcat/Formula/httpcat.rb +0 -18
  68. package/homebrew-httpcat/README.md +0 -31
  69. package/homebrew-httpcat/homebrew-httpcat/Formula/httpcat.rb +0 -18
  70. package/homebrew-httpcat/homebrew-httpcat/README.md +0 -31
@@ -1,4 +1,3 @@
1
- import { createInterface } from "readline";
2
1
  import WebSocket from "ws";
3
2
  import chalk from "chalk";
4
3
  // @ts-ignore - neo-blessed doesn't have types, but @types/blessed provides compatible types
@@ -133,7 +132,7 @@ function formatMessageText(msg, isOwn = false) {
133
132
  const authorShort = msg.authorShort || formatAddress(msg.author, 6);
134
133
  return `${chalk.dim(`[${timeStr}]`)} ${authorColor(authorShort)}: ${messageColor(msg.message)}`;
135
134
  }
136
- function displayMessage(msg, isOwn = false, isPending = false, messageLogBox, rl) {
135
+ function displayMessage(msg, isOwn = false, isPending = false, messageLogBox) {
137
136
  // Skip if we've already displayed this message
138
137
  if (displayedMessageIds.has(msg.messageId)) {
139
138
  return;
@@ -145,16 +144,8 @@ function displayMessage(msg, isOwn = false, isPending = false, messageLogBox, rl
145
144
  messageLogBox.log(messageText);
146
145
  messageLogBox.setScrollPerc(100); // Auto-scroll to bottom
147
146
  }
148
- else if (rl) {
149
- // Fallback to readline approach (for JSON mode or edge cases)
150
- process.stdout.write("\r\x1b[K");
151
- process.stdout.write(messageText);
152
- process.stdout.write("\n");
153
- if (rl)
154
- rl.prompt(true);
155
- }
156
147
  else {
157
- // Simple console.log when no UI active
148
+ // Simple console.log when no UI active (JSON mode)
158
149
  console.log(messageText);
159
150
  }
160
151
  }
@@ -180,7 +171,9 @@ function formatTimeRemaining(expiresAt) {
180
171
  function formatBuyResultCompact(result) {
181
172
  const graduationStatus = result.graduationReached
182
173
  ? "✅ GRADUATED!"
183
- : `${result.graduationProgress.toFixed(2)}%`;
174
+ : result.graduationProgress !== undefined
175
+ ? `${result.graduationProgress.toFixed(2)}%`
176
+ : "N/A";
184
177
  return chalk.green("✅ Buy successful! ") +
185
178
  `Received ${chalk.cyan(formatTokenAmount(result.tokensReceived))} tokens, ` +
186
179
  `spent ${chalk.yellow(formatCurrency(result.amountSpent))}, ` +
@@ -191,11 +184,14 @@ function formatBuyResultCompact(result) {
191
184
  * Format sell result compactly for chat log
192
185
  */
193
186
  function formatSellResultCompact(result) {
187
+ const graduationStatus = result.graduationProgress !== undefined
188
+ ? `${result.graduationProgress.toFixed(2)}%`
189
+ : "N/A";
194
190
  return chalk.green("✅ Sell successful! ") +
195
191
  `Sold ${chalk.cyan(formatTokenAmount(result.tokensSold))} tokens, ` +
196
192
  `received ${chalk.yellow(formatCurrency(result.usdcReceived))}, ` +
197
193
  `new price ${chalk.cyan(formatCurrency(result.newPrice))}, ` +
198
- `graduation ${chalk.magenta(result.graduationProgress.toFixed(2) + "%")}`;
194
+ `graduation ${chalk.magenta(graduationStatus)}`;
199
195
  }
200
196
  function updateHeaderBox(headerBox, leaseInfo, tokenName, isConnected = false) {
201
197
  const title = tokenName
@@ -265,7 +261,6 @@ export async function startChatStream(client, jsonMode = false, tokenIdentifier,
265
261
  let userAddress;
266
262
  let ws = null;
267
263
  let wsUrl = null; // Store websocket URL for reconnection
268
- let rl = null;
269
264
  let leaseCheckInterval = null;
270
265
  let headerUpdateInterval = null;
271
266
  let isExiting = false;
@@ -401,15 +396,10 @@ export async function startChatStream(client, jsonMode = false, tokenIdentifier,
401
396
  inputBox.clearValue();
402
397
  screen?.render();
403
398
  }
404
- else if (rl) {
405
- // Fallback to readline
406
- process.stdout.write("\n");
407
- process.stdout.write("\r\x1b[K");
408
- }
409
399
  // Display the message (only if not already displayed)
410
400
  // In JSON mode, displayMessage should never be called, but add explicit check
411
401
  if (!displayedMessageIds.has(msg.messageId)) {
412
- displayMessage(msg, isOwn, false, messageLogBox || undefined, jsonMode ? undefined : (rl || undefined));
402
+ displayMessage(msg, isOwn, false, messageLogBox || undefined);
413
403
  }
414
404
  // Re-enable input - clear the input line completely
415
405
  isSending = false;
@@ -421,17 +411,12 @@ export async function startChatStream(client, jsonMode = false, tokenIdentifier,
421
411
  inputBox.focus();
422
412
  screen.render();
423
413
  }
424
- else if (rl) {
425
- // Fallback to readline
426
- process.stdout.write("\r\x1b[K");
427
- rl.prompt();
428
- }
429
414
  }
430
415
  else {
431
416
  // Not our message, just display it normally
432
417
  // In JSON mode, displayMessage should never be called, but add explicit check
433
418
  if (!displayedMessageIds.has(msg.messageId)) {
434
- displayMessage(msg, isOwn, false, messageLogBox || undefined, jsonMode ? undefined : (rl || undefined));
419
+ displayMessage(msg, isOwn, false, messageLogBox || undefined);
435
420
  }
436
421
  }
437
422
  }
@@ -448,14 +433,6 @@ export async function startChatStream(client, jsonMode = false, tokenIdentifier,
448
433
  messageLogBox.setScrollPerc(100);
449
434
  screen?.render();
450
435
  }
451
- else if (rl) {
452
- process.stdout.write("\r\x1b[K");
453
- console.log(chalk.yellow("⏱️ Your lease has expired. Type /renew to continue chatting."));
454
- rl.prompt(true);
455
- }
456
- else {
457
- console.log(chalk.yellow("⏱️ Your lease has expired. Type /renew to continue chatting."));
458
- }
459
436
  }
460
437
  leaseInfo = null;
461
438
  if (headerBox) {
@@ -476,14 +453,6 @@ export async function startChatStream(client, jsonMode = false, tokenIdentifier,
476
453
  messageLogBox.setScrollPerc(100);
477
454
  screen?.render();
478
455
  }
479
- else if (rl) {
480
- process.stdout.write("\r\x1b[K");
481
- console.log(chalk.red(`❌ Error: ${event.error || "Unknown error"}`));
482
- rl.prompt(true);
483
- }
484
- else {
485
- console.log(chalk.red(`❌ Error: ${event.error || "Unknown error"}`));
486
- }
487
456
  }
488
457
  }
489
458
  }
@@ -508,14 +477,6 @@ export async function startChatStream(client, jsonMode = false, tokenIdentifier,
508
477
  messageLogBox.setScrollPerc(100);
509
478
  screen?.render();
510
479
  }
511
- else if (rl) {
512
- process.stdout.write("\r\x1b[K");
513
- console.log(chalk.red(`❌ WebSocket error: ${errorMsg}`));
514
- rl.prompt(true);
515
- }
516
- else {
517
- console.log(chalk.red(`❌ WebSocket error: ${errorMsg}`));
518
- }
519
480
  }
520
481
  });
521
482
  websocket.on("close", (code, reason) => {
@@ -557,14 +518,6 @@ export async function startChatStream(client, jsonMode = false, tokenIdentifier,
557
518
  messageLogBox.setScrollPerc(100);
558
519
  screen?.render();
559
520
  }
560
- else if (rl) {
561
- process.stdout.write("\r\x1b[K");
562
- console.log(chalk.yellow(message));
563
- rl.prompt(true);
564
- }
565
- else {
566
- console.log(chalk.yellow(message));
567
- }
568
521
  }
569
522
  });
570
523
  };
@@ -586,9 +539,6 @@ export async function startChatStream(client, jsonMode = false, tokenIdentifier,
586
539
  messageLogBox.setScrollPerc(100);
587
540
  screen?.render();
588
541
  }
589
- else if (rl) {
590
- console.log(chalk.yellow(`🔄 Retrying WebSocket connection (attempt ${retryCount + 1}/${maxRetries})...`));
591
- }
592
542
  }
593
543
  await new Promise((resolve) => setTimeout(resolve, backoffDelay));
594
544
  }
@@ -672,13 +622,6 @@ export async function startChatStream(client, jsonMode = false, tokenIdentifier,
672
622
  messageLogBox.setScrollPerc(100);
673
623
  screen?.render();
674
624
  }
675
- else if (rl) {
676
- console.log(chalk.red(`❌ ${finalError}`));
677
- console.log(chalk.yellow("💡 Try typing /renew to get a fresh lease"));
678
- }
679
- else {
680
- console.log(chalk.red(`❌ ${finalError}`));
681
- }
682
625
  }
683
626
  throw new Error(finalError);
684
627
  }
@@ -702,9 +645,6 @@ export async function startChatStream(client, jsonMode = false, tokenIdentifier,
702
645
  messageLogBox.setScrollPerc(100);
703
646
  screen?.render();
704
647
  }
705
- else if (rl) {
706
- console.log(chalk.yellow("⏱️ Your lease has expired. Type /renew to continue chatting."));
707
- }
708
648
  leaseInfo = null;
709
649
  if (headerBox) {
710
650
  updateHeaderBox(headerBox, null, tokenIdentifier, true);
@@ -712,157 +652,90 @@ export async function startChatStream(client, jsonMode = false, tokenIdentifier,
712
652
  }
713
653
  }
714
654
  }, 30000);
715
- // Set up blessed UI or readline interface (only in interactive mode)
655
+ // Set up blessed UI (only in interactive mode)
716
656
  if (!jsonMode) {
717
- let useBlessed = true;
718
- // Check if terminal supports blessed (basic compatibility check)
719
- // If not a TTY, silently fall back to readline
657
+ // Check if terminal supports blessed
720
658
  if (!process.stdout.isTTY) {
721
- useBlessed = false;
722
- }
723
- // Helper function to set up readline interface
724
- const setupReadline = () => {
725
- rl = createInterface({
726
- input: process.stdin,
727
- output: process.stdout,
728
- prompt: "",
729
- });
730
- // Display header information via console
731
- console.log();
732
- console.log(chalk.cyan("═".repeat(80)));
733
- console.log(chalk.cyan("💬 httpcat Chat Stream"));
734
- console.log(chalk.cyan("═".repeat(80)));
735
- console.log();
736
- console.log(chalk.green("✅ Connected to chat stream"));
737
- console.log();
738
- console.log(chalk.dim("💰 Entry fee: $0.01 USDC (10 min lease)"));
739
- console.log(chalk.dim("💬 Per message: $0.01 USDC"));
740
- if (leaseInfo) {
741
- const timeRemaining = formatTimeRemaining(leaseInfo.leaseExpiresAt);
742
- console.log(chalk.dim(`⏱️ Lease expires in: ${timeRemaining}`));
743
- }
744
- console.log();
745
- console.log(chalk.dim("💡 Type your message and press Enter to send"));
746
- console.log(chalk.dim("💡 Type /exit or Ctrl+C to quit"));
747
- console.log(chalk.dim("💡 Type /renew to renew your lease"));
748
- if (tokenIdentifier) {
749
- console.log(chalk.dim("💡 Type /buy <amount> to buy tokens"));
750
- console.log(chalk.dim("💡 Type /sell <amount> to sell tokens"));
751
- }
752
- console.log(chalk.cyan("─".repeat(80)));
753
- console.log();
754
- };
755
- // Try to create blessed screen with error handling
756
- if (useBlessed) {
757
- try {
758
- // Create blessed screen and widgets
759
- screen = blessed.screen({
760
- smartCSR: true,
761
- title: "httpcat Chat",
762
- fullUnicode: true,
763
- // Add error handling for terminal compatibility
764
- fastCSR: false, // Disable fast CSR to avoid rendering issues
765
- });
766
- // Calculate header height (approximately 10 lines)
767
- const headerHeight = 10;
768
- const inputHeight = 3;
769
- // Create header box (fixed at top)
770
- headerBox = blessed.box({
771
- top: 0,
772
- left: 0,
773
- width: "100%",
774
- height: headerHeight,
775
- content: "",
776
- tags: false, // Disable tags to avoid rendering issues
777
- wrap: true,
778
- scrollable: false,
779
- alwaysScroll: false,
780
- padding: {
781
- left: 1,
782
- right: 1,
783
- top: 0,
784
- bottom: 0,
785
- },
786
- style: {
787
- fg: "white",
788
- bg: "black",
789
- },
790
- });
791
- // Create message log box (scrollable, middle section)
792
- messageLogBox = blessed.log({
793
- top: headerHeight,
794
- left: 0,
795
- width: "100%",
796
- height: `100%-${headerHeight + inputHeight}`,
797
- tags: true,
798
- scrollable: true,
799
- alwaysScroll: true,
800
- scrollbar: {
801
- ch: " ",
802
- inverse: true,
803
- },
804
- style: {
805
- fg: "white",
806
- bg: "black",
807
- },
808
- });
809
- // Create input box (fixed at bottom)
810
- inputBox = blessed.textbox({
811
- bottom: 0,
812
- left: 0,
813
- width: "100%",
814
- height: inputHeight,
815
- content: "",
816
- inputOnFocus: true,
817
- tags: true,
818
- keys: true,
819
- style: {
820
- fg: "cyan",
821
- bg: "black",
822
- focus: {
823
- fg: "white",
824
- bg: "blue",
825
- },
826
- },
827
- });
828
- // Append widgets to screen
829
- screen.append(headerBox);
830
- screen.append(messageLogBox);
831
- screen.append(inputBox);
832
- // Test render to catch early errors
833
- screen.render();
834
- // Initial header update
835
- updateHeaderBox(headerBox, leaseInfo, tokenIdentifier, false);
836
- screen.render();
837
- }
838
- catch (blessedError) {
839
- // Blessed failed - fallback to readline
840
- useBlessed = false;
841
- // Only show error if it's not a TTY issue (which we already handled silently)
842
- if (process.stdout.isTTY) {
843
- console.error(chalk.yellow("⚠️ Blessed UI initialization failed, falling back to readline interface"));
844
- console.error(chalk.dim(` Error: ${blessedError instanceof Error ? blessedError.message : String(blessedError)}`));
845
- }
846
- // Clean up any partial blessed setup
847
- if (screen) {
848
- try {
849
- screen.destroy();
850
- }
851
- catch {
852
- // Ignore cleanup errors
853
- }
854
- screen = null;
855
- headerBox = null;
856
- messageLogBox = null;
857
- inputBox = null;
858
- }
859
- setupReadline();
860
- }
861
- }
862
- else {
863
- // Not a TTY or blessed disabled - use readline directly
864
- setupReadline();
659
+ throw new Error("Chat requires an interactive terminal (TTY). Use --json mode for non-interactive usage.");
865
660
  }
661
+ // Create blessed screen and widgets
662
+ screen = blessed.screen({
663
+ smartCSR: true,
664
+ title: "httpcat Chat",
665
+ fullUnicode: true,
666
+ fastCSR: false, // Disable fast CSR to avoid rendering issues
667
+ });
668
+ // Calculate header height (approximately 10 lines)
669
+ const headerHeight = 10;
670
+ const inputHeight = 3;
671
+ // Create header box (fixed at top)
672
+ headerBox = blessed.box({
673
+ top: 0,
674
+ left: 0,
675
+ width: "100%",
676
+ height: headerHeight,
677
+ content: "",
678
+ tags: false, // Disable tags to avoid rendering issues
679
+ wrap: true,
680
+ scrollable: false,
681
+ alwaysScroll: false,
682
+ padding: {
683
+ left: 1,
684
+ right: 1,
685
+ top: 0,
686
+ bottom: 0,
687
+ },
688
+ style: {
689
+ fg: "white",
690
+ bg: "black",
691
+ },
692
+ });
693
+ // Create message log box (scrollable, middle section)
694
+ messageLogBox = blessed.log({
695
+ top: headerHeight,
696
+ left: 0,
697
+ width: "100%",
698
+ height: `100%-${headerHeight + inputHeight}`,
699
+ tags: true,
700
+ scrollable: true,
701
+ alwaysScroll: true,
702
+ scrollbar: {
703
+ ch: " ",
704
+ inverse: true,
705
+ },
706
+ style: {
707
+ fg: "white",
708
+ bg: "black",
709
+ },
710
+ });
711
+ // Create input box (fixed at bottom)
712
+ inputBox = blessed.textbox({
713
+ bottom: 0,
714
+ left: 0,
715
+ width: "100%",
716
+ height: inputHeight,
717
+ content: "",
718
+ inputOnFocus: true,
719
+ tags: true,
720
+ keys: true,
721
+ style: {
722
+ fg: "cyan",
723
+ bg: "black",
724
+ focus: {
725
+ fg: "white",
726
+ bg: "blue",
727
+ },
728
+ },
729
+ });
730
+ // Append widgets to screen
731
+ screen.append(headerBox);
732
+ screen.append(messageLogBox);
733
+ screen.append(inputBox);
734
+ // Initial render
735
+ screen.render();
736
+ // Initial header update
737
+ updateHeaderBox(headerBox, leaseInfo, tokenIdentifier, false);
738
+ screen.render();
866
739
  // Display last messages
867
740
  if (joinResult.lastMessages.length > 0) {
868
741
  const sortedMessages = [...joinResult.lastMessages].sort((a, b) => {
@@ -874,210 +747,218 @@ export async function startChatStream(client, jsonMode = false, tokenIdentifier,
874
747
  displayedMessageIds.add(msg.messageId);
875
748
  const isOwn = msg.author === userAddress;
876
749
  // This is inside !jsonMode block, but add explicit check for safety
877
- displayMessage(msg, isOwn, false, messageLogBox || undefined, jsonMode ? undefined : (rl || undefined));
878
- });
879
- }
880
- // Only set up blessed-specific handlers if using blessed
881
- if (useBlessed && screen) {
882
- // Handle terminal resize
883
- screen.on("resize", () => {
884
- screen?.render();
750
+ displayMessage(msg, isOwn, false, messageLogBox || undefined);
885
751
  });
886
- // Update header with lease countdown every second
887
- headerUpdateInterval = setInterval(() => {
888
- if (leaseInfo && headerBox && screen && !isExiting) {
889
- updateHeaderBox(headerBox, leaseInfo, tokenIdentifier, true);
890
- screen.render();
891
- }
892
- }, 1000);
893
752
  }
753
+ // Handle terminal resize
754
+ screen.on("resize", () => {
755
+ screen?.render();
756
+ });
757
+ // Update header with lease countdown every second
758
+ headerUpdateInterval = setInterval(() => {
759
+ if (leaseInfo && headerBox && screen && !isExiting) {
760
+ updateHeaderBox(headerBox, leaseInfo, tokenIdentifier, true);
761
+ screen.render();
762
+ }
763
+ }, 1000);
894
764
  // Handle input submission
895
- if (useBlessed && inputBox) {
896
- // Blessed input handling
897
- inputBox.on("submit", async (value) => {
898
- const trimmed = value.trim();
899
- // Ignore input while sending
900
- if (isSending || !trimmed) {
901
- inputBox?.clearValue();
902
- inputBox?.focus();
903
- screen?.render();
904
- return;
905
- }
906
- // Handle commands
907
- if (trimmed.startsWith("/")) {
908
- const [cmd] = trimmed.split(" ");
909
- switch (cmd) {
910
- case "/exit":
911
- case "/quit":
912
- isExiting = true;
913
- if (leaseCheckInterval)
914
- clearInterval(leaseCheckInterval);
915
- if (headerUpdateInterval)
916
- clearInterval(headerUpdateInterval);
917
- if (ws)
918
- ws.close();
919
- screen?.destroy();
920
- console.log();
921
- printCat("sleeping");
922
- console.log(chalk.cyan("Chat disconnected. Goodbye! 👋"));
923
- process.exit(0);
924
- return;
925
- case "/renew": {
926
- try {
927
- inputBox?.setValue("⏱️ Renewing lease...");
765
+ inputBox.on("submit", async (value) => {
766
+ const trimmed = value.trim();
767
+ // Ignore input while sending
768
+ if (isSending || !trimmed) {
769
+ inputBox?.clearValue();
770
+ inputBox?.focus();
771
+ screen?.render();
772
+ return;
773
+ }
774
+ // Handle commands
775
+ if (trimmed.startsWith("/")) {
776
+ const [cmd] = trimmed.split(" ");
777
+ switch (cmd) {
778
+ case "/exit":
779
+ case "/quit":
780
+ isExiting = true;
781
+ if (leaseCheckInterval)
782
+ clearInterval(leaseCheckInterval);
783
+ if (headerUpdateInterval)
784
+ clearInterval(headerUpdateInterval);
785
+ if (ws)
786
+ ws.close();
787
+ screen?.destroy();
788
+ console.log();
789
+ printCat("sleeping");
790
+ console.log(chalk.cyan("Chat disconnected. Goodbye! 👋"));
791
+ process.exit(0);
792
+ return;
793
+ case "/renew": {
794
+ try {
795
+ inputBox?.setValue("⏱️ Renewing lease...");
796
+ screen?.render();
797
+ const renewal = await renewLease(client, userAddress, leaseInfo?.leaseId);
798
+ leaseInfo = {
799
+ leaseId: renewal.leaseId,
800
+ leaseExpiresAt: new Date(renewal.leaseExpiresAt),
801
+ };
802
+ // Update header first
803
+ if (headerBox) {
804
+ updateHeaderBox(headerBox, leaseInfo, tokenIdentifier, false);
805
+ }
806
+ if (messageLogBox) {
807
+ messageLogBox.log(chalk.green(`✅ Lease renewed! Expires in ${formatTimeRemaining(leaseInfo.leaseExpiresAt)}`));
808
+ messageLogBox.log(chalk.yellow("🔄 Reconnecting to chat stream..."));
809
+ messageLogBox.setScrollPerc(100);
928
810
  screen?.render();
929
- const renewal = await renewLease(client, userAddress, leaseInfo?.leaseId);
930
- leaseInfo = {
931
- leaseId: renewal.leaseId,
932
- leaseExpiresAt: new Date(renewal.leaseExpiresAt),
933
- };
934
- // Update header first
935
- if (headerBox) {
936
- updateHeaderBox(headerBox, leaseInfo, tokenIdentifier, false);
937
- }
938
- if (messageLogBox) {
939
- messageLogBox.log(chalk.green(`✅ Lease renewed! Expires in ${formatTimeRemaining(leaseInfo.leaseExpiresAt)}`));
940
- messageLogBox.log(chalk.yellow("🔄 Reconnecting to chat stream..."));
941
- messageLogBox.setScrollPerc(100);
942
- screen?.render();
943
- }
944
- // Close old connection properly
945
- if (ws) {
946
- // Remove all listeners to prevent conflicts
947
- ws.removeAllListeners();
948
- // Ensure connection is fully closed
949
- // WebSocket.OPEN = 1, WebSocket.CONNECTING = 0
950
- if (ws.readyState === 1 || ws.readyState === 0) {
951
- ws.close();
952
- }
953
- ws = null;
811
+ }
812
+ // Close old connection properly
813
+ if (ws) {
814
+ // Remove all listeners to prevent conflicts
815
+ ws.removeAllListeners();
816
+ // Ensure connection is fully closed
817
+ // WebSocket.OPEN = 1, WebSocket.CONNECTING = 0
818
+ if (ws.readyState === 1 || ws.readyState === 0) {
819
+ ws.close();
954
820
  }
955
- // Wait for database transaction to commit and old connection to fully close
956
- // The server validates the lease in the open handler, so we need to ensure
957
- // the database update is visible before connecting
958
- // Increased delay to 2 seconds for better database consistency
959
- await new Promise((resolve) => setTimeout(resolve, 2000));
960
- // Reconnect with new lease - with retry logic
961
- if (wsUrl) {
962
- // Normalize WebSocket URL protocol based on agent URL
963
- const agentUrl = client.getAgentUrl();
964
- const normalizedWsUrl = normalizeWebSocketUrl(wsUrl, agentUrl);
965
- const wsUrlObj = new URL(normalizedWsUrl);
966
- wsUrlObj.searchParams.set("leaseId", leaseInfo.leaseId);
967
- wsUrl = wsUrlObj.toString(); // Update stored URL
968
- // Retry logic with exponential backoff
969
- let retryCount = 0;
970
- const maxRetries = 3;
971
- let connected = false;
972
- while (!connected && retryCount < maxRetries) {
973
- try {
974
- if (retryCount > 0) {
975
- const backoffDelay = Math.min(1000 * Math.pow(2, retryCount - 1), 5000);
976
- if (messageLogBox) {
977
- messageLogBox.log(chalk.yellow(`🔄 Retry ${retryCount}/${maxRetries} in ${backoffDelay}ms...`));
978
- messageLogBox.setScrollPerc(100);
979
- screen?.render();
980
- }
981
- await new Promise((resolve) => setTimeout(resolve, backoffDelay));
982
- }
983
- // Create new connection and wait for it to open
984
- const newWsUrl = wsUrl; // TypeScript: ensure it's not null
985
- await new Promise((resolve, reject) => {
986
- const newWs = new WebSocket(newWsUrl);
987
- // Attach permanent handlers FIRST (before temporary handlers)
988
- attachWebSocketHandlers(newWs);
989
- // Set up temporary handlers for connection establishment
990
- let timeout = null;
991
- let openHandler = null;
992
- let closeHandler = null;
993
- let errorHandler = null;
994
- const cleanup = () => {
995
- if (timeout)
996
- clearTimeout(timeout);
997
- if (openHandler)
998
- newWs.removeListener("open", openHandler);
999
- if (closeHandler)
1000
- newWs.removeListener("close", closeHandler);
1001
- if (errorHandler)
1002
- newWs.removeListener("error", errorHandler);
1003
- };
1004
- timeout = setTimeout(() => {
1005
- cleanup();
1006
- newWs.close();
1007
- reject(new Error("Connection timeout - lease may not be ready yet"));
1008
- }, 10000);
1009
- openHandler = () => {
1010
- cleanup();
1011
- ws = newWs; // Assign to outer variable only after successful connection
1012
- resolve();
1013
- };
1014
- closeHandler = (code, reason) => {
1015
- cleanup();
1016
- const reasonStr = reason.toString();
1017
- if (code === 1008 && reasonStr.includes("lease")) {
1018
- reject(new Error("Lease validation failed - please try /renew again"));
1019
- }
1020
- else {
1021
- reject(new Error(`Connection closed: ${code} - ${reasonStr}`));
1022
- }
1023
- };
1024
- errorHandler = (error) => {
1025
- cleanup();
1026
- reject(error);
1027
- };
1028
- // Attach temporary handlers AFTER permanent handlers
1029
- newWs.once("open", openHandler);
1030
- newWs.once("close", closeHandler);
1031
- newWs.once("error", errorHandler);
1032
- });
1033
- connected = true;
1034
- // Connection established
1035
- if (headerBox) {
1036
- updateHeaderBox(headerBox, leaseInfo, tokenIdentifier, true);
1037
- }
1038
- if (messageLogBox) {
1039
- messageLogBox.log(chalk.green("✅ Reconnected to chat stream"));
1040
- messageLogBox.setScrollPerc(100);
1041
- }
1042
- screen?.render();
1043
- }
1044
- catch (error) {
1045
- retryCount++;
1046
- const errorMsg = error instanceof Error ? error.message : String(error);
1047
- if (retryCount >= maxRetries) {
1048
- // Final failure
1049
- throw new Error(`Failed to reconnect after ${maxRetries} attempts: ${errorMsg}`);
1050
- }
1051
- // Log retry attempt
821
+ ws = null;
822
+ }
823
+ // Wait for database transaction to commit and old connection to fully close
824
+ // The server validates the lease in the open handler, so we need to ensure
825
+ // the database update is visible before connecting
826
+ // Increased delay to 2 seconds for better database consistency
827
+ await new Promise((resolve) => setTimeout(resolve, 2000));
828
+ // Reconnect with new lease - with retry logic
829
+ if (wsUrl) {
830
+ // Normalize WebSocket URL protocol based on agent URL
831
+ const agentUrl = client.getAgentUrl();
832
+ const normalizedWsUrl = normalizeWebSocketUrl(wsUrl, agentUrl);
833
+ const wsUrlObj = new URL(normalizedWsUrl);
834
+ wsUrlObj.searchParams.set("leaseId", leaseInfo.leaseId);
835
+ wsUrl = wsUrlObj.toString(); // Update stored URL
836
+ // Retry logic with exponential backoff
837
+ let retryCount = 0;
838
+ const maxRetries = 3;
839
+ let connected = false;
840
+ while (!connected && retryCount < maxRetries) {
841
+ try {
842
+ if (retryCount > 0) {
843
+ const backoffDelay = Math.min(1000 * Math.pow(2, retryCount - 1), 5000);
1052
844
  if (messageLogBox) {
1053
- messageLogBox.log(chalk.yellow(`⚠️ Connection attempt ${retryCount} failed: ${errorMsg}`));
845
+ messageLogBox.log(chalk.yellow(`🔄 Retry ${retryCount}/${maxRetries} in ${backoffDelay}ms...`));
1054
846
  messageLogBox.setScrollPerc(100);
1055
847
  screen?.render();
1056
848
  }
849
+ await new Promise((resolve) => setTimeout(resolve, backoffDelay));
850
+ }
851
+ // Create new connection and wait for it to open
852
+ const newWsUrl = wsUrl; // TypeScript: ensure it's not null
853
+ await new Promise((resolve, reject) => {
854
+ const newWs = new WebSocket(newWsUrl);
855
+ // Attach permanent handlers FIRST (before temporary handlers)
856
+ attachWebSocketHandlers(newWs);
857
+ // Set up temporary handlers for connection establishment
858
+ let timeout = null;
859
+ let openHandler = null;
860
+ let closeHandler = null;
861
+ let errorHandler = null;
862
+ const cleanup = () => {
863
+ if (timeout)
864
+ clearTimeout(timeout);
865
+ if (openHandler)
866
+ newWs.removeListener("open", openHandler);
867
+ if (closeHandler)
868
+ newWs.removeListener("close", closeHandler);
869
+ if (errorHandler)
870
+ newWs.removeListener("error", errorHandler);
871
+ };
872
+ timeout = setTimeout(() => {
873
+ cleanup();
874
+ newWs.close();
875
+ reject(new Error("Connection timeout - lease may not be ready yet"));
876
+ }, 10000);
877
+ openHandler = () => {
878
+ cleanup();
879
+ ws = newWs; // Assign to outer variable only after successful connection
880
+ resolve();
881
+ };
882
+ closeHandler = (code, reason) => {
883
+ cleanup();
884
+ const reasonStr = reason.toString();
885
+ if (code === 1008 && reasonStr.includes("lease")) {
886
+ reject(new Error("Lease validation failed - please try /renew again"));
887
+ }
888
+ else {
889
+ reject(new Error(`Connection closed: ${code} - ${reasonStr}`));
890
+ }
891
+ };
892
+ errorHandler = (error) => {
893
+ cleanup();
894
+ reject(error);
895
+ };
896
+ // Attach temporary handlers AFTER permanent handlers
897
+ newWs.once("open", openHandler);
898
+ newWs.once("close", closeHandler);
899
+ newWs.once("error", errorHandler);
900
+ });
901
+ connected = true;
902
+ // Connection established
903
+ if (headerBox) {
904
+ updateHeaderBox(headerBox, leaseInfo, tokenIdentifier, true);
905
+ }
906
+ if (messageLogBox) {
907
+ messageLogBox.log(chalk.green("✅ Reconnected to chat stream"));
908
+ messageLogBox.setScrollPerc(100);
909
+ }
910
+ screen?.render();
911
+ }
912
+ catch (error) {
913
+ retryCount++;
914
+ const errorMsg = error instanceof Error ? error.message : String(error);
915
+ if (retryCount >= maxRetries) {
916
+ // Final failure
917
+ throw new Error(`Failed to reconnect after ${maxRetries} attempts: ${errorMsg}`);
918
+ }
919
+ // Log retry attempt
920
+ if (messageLogBox) {
921
+ messageLogBox.log(chalk.yellow(`⚠️ Connection attempt ${retryCount} failed: ${errorMsg}`));
922
+ messageLogBox.setScrollPerc(100);
923
+ screen?.render();
1057
924
  }
1058
925
  }
1059
926
  }
1060
- inputBox?.clearValue();
1061
- inputBox?.focus();
1062
- screen?.render();
1063
927
  }
1064
- catch (error) {
1065
- const errorMsg = error instanceof Error ? error.message : String(error);
1066
- if (messageLogBox) {
1067
- messageLogBox.log(chalk.red(`❌ ${errorMsg}`));
1068
- messageLogBox.setScrollPerc(100);
1069
- }
1070
- inputBox?.clearValue();
1071
- inputBox?.focus();
928
+ inputBox?.clearValue();
929
+ inputBox?.focus();
930
+ screen?.render();
931
+ }
932
+ catch (error) {
933
+ const errorMsg = error instanceof Error ? error.message : String(error);
934
+ if (messageLogBox) {
935
+ messageLogBox.log(chalk.red(`❌ ${errorMsg}`));
936
+ messageLogBox.setScrollPerc(100);
937
+ }
938
+ inputBox?.clearValue();
939
+ inputBox?.focus();
940
+ screen?.render();
941
+ }
942
+ return;
943
+ }
944
+ case "/buy": {
945
+ // Only allow in token-specific chats
946
+ if (!tokenIdentifier) {
947
+ if (messageLogBox) {
948
+ messageLogBox.log(chalk.yellow("⚠️ /buy command only works in token-specific chats"));
949
+ messageLogBox.setScrollPerc(100);
1072
950
  screen?.render();
1073
951
  }
952
+ inputBox?.clearValue();
953
+ inputBox?.focus();
954
+ screen?.render();
1074
955
  return;
1075
956
  }
1076
- case "/buy": {
1077
- // Only allow in token-specific chats
1078
- if (!tokenIdentifier) {
957
+ try {
958
+ const parts = trimmed.split(" ");
959
+ if (parts.length < 2) {
1079
960
  if (messageLogBox) {
1080
- messageLogBox.log(chalk.yellow("⚠️ /buy command only works in token-specific chats"));
961
+ messageLogBox.log(chalk.yellow("⚠️ Usage: /buy <amount> (e.g., /buy 0.05)"));
1081
962
  messageLogBox.setScrollPerc(100);
1082
963
  screen?.render();
1083
964
  }
@@ -1086,52 +967,52 @@ export async function startChatStream(client, jsonMode = false, tokenIdentifier,
1086
967
  screen?.render();
1087
968
  return;
1088
969
  }
1089
- try {
1090
- const parts = trimmed.split(" ");
1091
- if (parts.length < 2) {
1092
- if (messageLogBox) {
1093
- messageLogBox.log(chalk.yellow("⚠️ Usage: /buy <amount> (e.g., /buy 0.05)"));
1094
- messageLogBox.setScrollPerc(100);
1095
- screen?.render();
1096
- }
1097
- inputBox?.clearValue();
1098
- inputBox?.focus();
1099
- screen?.render();
1100
- return;
1101
- }
1102
- const amount = parts[1];
1103
- inputBox?.setValue(`💰 Buying tokens...`);
970
+ const amount = parts[1];
971
+ inputBox?.setValue(`💰 Buying tokens...`);
972
+ screen?.render();
973
+ const isTestMode = client.getNetwork().includes("sepolia");
974
+ const result = await buyToken(client, tokenIdentifier, amount, isTestMode, true // silent mode
975
+ );
976
+ if (messageLogBox) {
977
+ messageLogBox.log(formatBuyResultCompact(result));
978
+ messageLogBox.setScrollPerc(100);
1104
979
  screen?.render();
1105
- const isTestMode = client.getNetwork().includes("sepolia");
1106
- const result = await buyToken(client, tokenIdentifier, amount, isTestMode, true // silent mode
1107
- );
1108
- if (messageLogBox) {
1109
- messageLogBox.log(formatBuyResultCompact(result));
1110
- messageLogBox.setScrollPerc(100);
1111
- screen?.render();
1112
- }
1113
- inputBox?.clearValue();
1114
- inputBox?.focus();
980
+ }
981
+ inputBox?.clearValue();
982
+ inputBox?.focus();
983
+ screen?.render();
984
+ }
985
+ catch (error) {
986
+ const errorMsg = error instanceof Error ? error.message : String(error);
987
+ if (messageLogBox) {
988
+ messageLogBox.log(chalk.red(`❌ Buy failed: ${errorMsg}`));
989
+ messageLogBox.setScrollPerc(100);
1115
990
  screen?.render();
1116
991
  }
1117
- catch (error) {
1118
- const errorMsg = error instanceof Error ? error.message : String(error);
1119
- if (messageLogBox) {
1120
- messageLogBox.log(chalk.red(`❌ Buy failed: ${errorMsg}`));
1121
- messageLogBox.setScrollPerc(100);
1122
- screen?.render();
1123
- }
1124
- inputBox?.clearValue();
1125
- inputBox?.focus();
992
+ inputBox?.clearValue();
993
+ inputBox?.focus();
994
+ screen?.render();
995
+ }
996
+ return;
997
+ }
998
+ case "/sell": {
999
+ // Only allow in token-specific chats
1000
+ if (!tokenIdentifier) {
1001
+ if (messageLogBox) {
1002
+ messageLogBox.log(chalk.yellow("⚠️ /sell command only works in token-specific chats"));
1003
+ messageLogBox.setScrollPerc(100);
1126
1004
  screen?.render();
1127
1005
  }
1006
+ inputBox?.clearValue();
1007
+ inputBox?.focus();
1008
+ screen?.render();
1128
1009
  return;
1129
1010
  }
1130
- case "/sell": {
1131
- // Only allow in token-specific chats
1132
- if (!tokenIdentifier) {
1011
+ try {
1012
+ const parts = trimmed.split(" ");
1013
+ if (parts.length < 2) {
1133
1014
  if (messageLogBox) {
1134
- messageLogBox.log(chalk.yellow("⚠️ /sell command only works in token-specific chats"));
1015
+ messageLogBox.log(chalk.yellow("⚠️ Usage: /sell <amount|all|percentage> (e.g., /sell 0.05, /sell all, /sell 50%)"));
1135
1016
  messageLogBox.setScrollPerc(100);
1136
1017
  screen?.render();
1137
1018
  }
@@ -1140,493 +1021,177 @@ export async function startChatStream(client, jsonMode = false, tokenIdentifier,
1140
1021
  screen?.render();
1141
1022
  return;
1142
1023
  }
1143
- try {
1144
- const parts = trimmed.split(" ");
1145
- if (parts.length < 2) {
1146
- if (messageLogBox) {
1147
- messageLogBox.log(chalk.yellow("⚠️ Usage: /sell <amount|all|percentage> (e.g., /sell 0.05, /sell all, /sell 50%)"));
1148
- messageLogBox.setScrollPerc(100);
1149
- screen?.render();
1150
- }
1151
- inputBox?.clearValue();
1152
- inputBox?.focus();
1153
- screen?.render();
1154
- return;
1155
- }
1156
- const amountStr = parts[1];
1157
- inputBox?.setValue(`💸 Selling tokens...`);
1158
- screen?.render();
1159
- // Get token info to check balance
1160
- if (!userAddress) {
1161
- throw new Error("User address not available");
1162
- }
1163
- const tokenInfo = await getTokenInfo(client, tokenIdentifier, userAddress, true // silent mode
1164
- );
1165
- if (!tokenInfo.userPosition || tokenInfo.userPosition.tokensOwned === "0") {
1166
- throw new Error("You do not own any of this token");
1167
- }
1168
- // Parse sell amount
1169
- const tokenAmount = parseTokenAmount(amountStr, tokenInfo.userPosition.tokensOwned);
1170
- const result = await sellToken(client, tokenIdentifier, tokenAmount, true // silent mode
1171
- );
1172
- if (messageLogBox) {
1173
- messageLogBox.log(formatSellResultCompact(result));
1174
- messageLogBox.setScrollPerc(100);
1175
- screen?.render();
1176
- }
1177
- inputBox?.clearValue();
1178
- inputBox?.focus();
1179
- screen?.render();
1180
- }
1181
- catch (error) {
1182
- const errorMsg = error instanceof Error ? error.message : String(error);
1183
- if (messageLogBox) {
1184
- messageLogBox.log(chalk.red(`❌ Sell failed: ${errorMsg}`));
1185
- messageLogBox.setScrollPerc(100);
1186
- screen?.render();
1187
- }
1188
- inputBox?.clearValue();
1189
- inputBox?.focus();
1190
- screen?.render();
1191
- }
1192
- return;
1193
- }
1194
- case "/help":
1195
- if (messageLogBox) {
1196
- const helpText = tokenIdentifier
1197
- ? chalk.dim("Commands: /exit, /quit, /renew, /buy, /sell, /help")
1198
- : chalk.dim("Commands: /exit, /quit, /renew, /help");
1199
- messageLogBox.log(helpText);
1200
- messageLogBox.setScrollPerc(100);
1024
+ const amountStr = parts[1];
1025
+ inputBox?.setValue(`💸 Selling tokens...`);
1026
+ screen?.render();
1027
+ // Get token info to check balance
1028
+ if (!userAddress) {
1029
+ throw new Error("User address not available");
1030
+ }
1031
+ const tokenInfo = await getTokenInfo(client, tokenIdentifier, userAddress, true // silent mode
1032
+ );
1033
+ if (!tokenInfo.userPosition || tokenInfo.userPosition.tokensOwned === "0") {
1034
+ throw new Error("You do not own any of this token");
1035
+ }
1036
+ // Parse sell amount
1037
+ const tokenAmount = parseTokenAmount(amountStr, tokenInfo.userPosition.tokensOwned);
1038
+ const result = await sellToken(client, tokenIdentifier, tokenAmount, true // silent mode
1039
+ );
1040
+ if (messageLogBox) {
1041
+ messageLogBox.log(formatSellResultCompact(result));
1042
+ messageLogBox.setScrollPerc(100);
1201
1043
  screen?.render();
1202
1044
  }
1203
1045
  inputBox?.clearValue();
1204
1046
  inputBox?.focus();
1205
1047
  screen?.render();
1206
- return;
1207
- default:
1048
+ }
1049
+ catch (error) {
1050
+ const errorMsg = error instanceof Error ? error.message : String(error);
1208
1051
  if (messageLogBox) {
1209
- messageLogBox.log(chalk.yellow(`Unknown command: ${cmd}. Type /help for commands.`));
1052
+ messageLogBox.log(chalk.red(`❌ Sell failed: ${errorMsg}`));
1210
1053
  messageLogBox.setScrollPerc(100);
1211
1054
  screen?.render();
1212
1055
  }
1213
1056
  inputBox?.clearValue();
1214
1057
  inputBox?.focus();
1215
1058
  screen?.render();
1216
- return;
1059
+ }
1060
+ return;
1217
1061
  }
1218
- }
1219
- // Check if websocket is still connected
1220
- // WebSocket.OPEN = 1
1221
- if (!ws || ws.readyState !== 1) {
1222
- const stateMsg = ws
1223
- ? ws.readyState === 0 ? "connecting"
1224
- : ws.readyState === 2 ? "closing"
1225
- : ws.readyState === 3 ? "closed"
1226
- : "unknown"
1227
- : "not initialized";
1228
- if (messageLogBox) {
1229
- messageLogBox.log(chalk.red(`❌ WebSocket is not connected (state: ${stateMsg}). Please wait for connection or type /renew.`));
1230
- messageLogBox.setScrollPerc(100);
1062
+ case "/help":
1063
+ if (messageLogBox) {
1064
+ const helpText = tokenIdentifier
1065
+ ? chalk.dim("Commands: /exit, /quit, /renew, /buy, /sell, /help")
1066
+ : chalk.dim("Commands: /exit, /quit, /renew, /help");
1067
+ messageLogBox.log(helpText);
1068
+ messageLogBox.setScrollPerc(100);
1069
+ screen?.render();
1070
+ }
1071
+ inputBox?.clearValue();
1072
+ inputBox?.focus();
1231
1073
  screen?.render();
1232
- }
1233
- inputBox?.clearValue();
1234
- inputBox?.focus();
1235
- screen?.render();
1236
- return;
1237
- }
1238
- // Check if lease is valid
1239
- if (!leaseInfo || leaseInfo.leaseExpiresAt.getTime() <= Date.now()) {
1240
- if (messageLogBox) {
1241
- messageLogBox.log(chalk.yellow("⏱️ Your lease has expired. Type /renew to continue chatting."));
1242
- messageLogBox.setScrollPerc(100);
1074
+ return;
1075
+ default:
1076
+ if (messageLogBox) {
1077
+ messageLogBox.log(chalk.yellow(`Unknown command: ${cmd}. Type /help for commands.`));
1078
+ messageLogBox.setScrollPerc(100);
1079
+ screen?.render();
1080
+ }
1081
+ inputBox?.clearValue();
1082
+ inputBox?.focus();
1243
1083
  screen?.render();
1244
- }
1245
- inputBox?.clearValue();
1246
- inputBox?.focus();
1247
- screen?.render();
1248
- return;
1249
- }
1250
- // Send message
1251
- isSending = true;
1252
- currentInput = trimmed;
1253
- // Show pulsing animation in input box
1254
- let pulseCount = 0;
1255
- let pulseInterval = null;
1256
- const tempMessageId = `pending-${Date.now()}-${Math.random()}`;
1257
- const updatePulse = () => {
1258
- if (!inputBox || isCleaningUp)
1259
1084
  return;
1260
- pulseCount++;
1261
- const pulseChar = pulseCount % 2 === 0 ? "●" : "○";
1262
- inputBox.setValue(`${trimmed} ${pulseChar}`);
1085
+ }
1086
+ }
1087
+ // Check if websocket is still connected
1088
+ // WebSocket.OPEN = 1
1089
+ if (!ws || ws.readyState !== 1) {
1090
+ const stateMsg = ws
1091
+ ? ws.readyState === 0 ? "connecting"
1092
+ : ws.readyState === 2 ? "closing"
1093
+ : ws.readyState === 3 ? "closed"
1094
+ : "unknown"
1095
+ : "not initialized";
1096
+ if (messageLogBox) {
1097
+ messageLogBox.log(chalk.red(`❌ WebSocket is not connected (state: ${stateMsg}). Please wait for connection or type /renew.`));
1098
+ messageLogBox.setScrollPerc(100);
1263
1099
  screen?.render();
1264
- };
1265
- // Initial pulse
1266
- updatePulse();
1267
- pulseInterval = setInterval(updatePulse, 500);
1268
- pulseIntervals.set(tempMessageId, pulseInterval);
1269
- try {
1270
- // Ensure lease is valid before sending
1271
- leaseInfo = await ensureLeaseValid(client, userAddress, leaseInfo, messageLogBox || undefined, screen || undefined);
1272
- if (headerBox) {
1273
- updateHeaderBox(headerBox, leaseInfo, tokenIdentifier, true);
1274
- screen?.render();
1275
- }
1276
- // Double-check websocket is still connected after lease renewal
1277
- // WebSocket.OPEN = 1
1278
- if (!ws || ws.readyState !== 1) {
1279
- throw new Error("WebSocket connection lost. Please wait for reconnection.");
1280
- }
1281
- const result = await sendChatMessage(client, trimmed, leaseInfo.leaseId, userAddress);
1282
- userAddress = result.author;
1283
- // Track this message
1284
- pendingMessages.set(result.messageId, tempMessageId);
1285
- pendingMessages.set(`text:${trimmed}`, tempMessageId);
1286
- // Keep pulsing until WebSocket confirms
1287
1100
  }
1288
- catch (error) {
1289
- // Stop pulsing
1290
- if (pulseInterval) {
1291
- clearInterval(pulseInterval);
1292
- pulseIntervals.delete(tempMessageId);
1293
- }
1294
- const errorMsg = error instanceof Error ? error.message : String(error);
1295
- if (messageLogBox) {
1296
- messageLogBox.log(chalk.red(`❌ ${errorMsg}`));
1297
- messageLogBox.setScrollPerc(100);
1298
- }
1299
- inputBox?.clearValue();
1300
- inputBox?.focus();
1301
- isSending = false;
1302
- currentInput = "";
1101
+ inputBox?.clearValue();
1102
+ inputBox?.focus();
1103
+ screen?.render();
1104
+ return;
1105
+ }
1106
+ // Check if lease is valid
1107
+ if (!leaseInfo || leaseInfo.leaseExpiresAt.getTime() <= Date.now()) {
1108
+ if (messageLogBox) {
1109
+ messageLogBox.log(chalk.yellow("⏱️ Your lease has expired. Type /renew to continue chatting."));
1110
+ messageLogBox.setScrollPerc(100);
1303
1111
  screen?.render();
1304
1112
  }
1305
- });
1306
- }
1307
- else if (rl) {
1308
- // Readline input handling
1309
- const readlineInterface = rl; // Store reference for TypeScript
1310
- readlineInterface.setPrompt("");
1311
- readlineInterface.prompt();
1312
- readlineInterface.on("line", async (line) => {
1313
- const trimmed = line.trim();
1314
- // Ignore empty input
1315
- if (!trimmed) {
1316
- readlineInterface.prompt();
1113
+ inputBox?.clearValue();
1114
+ inputBox?.focus();
1115
+ screen?.render();
1116
+ return;
1117
+ }
1118
+ // Send message
1119
+ isSending = true;
1120
+ currentInput = trimmed;
1121
+ // Show pulsing animation in input box
1122
+ let pulseCount = 0;
1123
+ let pulseInterval = null;
1124
+ const tempMessageId = `pending-${Date.now()}-${Math.random()}`;
1125
+ const updatePulse = () => {
1126
+ if (!inputBox || isCleaningUp)
1317
1127
  return;
1128
+ pulseCount++;
1129
+ const pulseChar = pulseCount % 2 === 0 ? "●" : "○";
1130
+ inputBox.setValue(`${trimmed} ${pulseChar}`);
1131
+ screen?.render();
1132
+ };
1133
+ // Initial pulse
1134
+ updatePulse();
1135
+ pulseInterval = setInterval(updatePulse, 500);
1136
+ pulseIntervals.set(tempMessageId, pulseInterval);
1137
+ try {
1138
+ // Ensure lease is valid before sending
1139
+ leaseInfo = await ensureLeaseValid(client, userAddress, leaseInfo, messageLogBox || undefined, screen || undefined);
1140
+ if (headerBox) {
1141
+ updateHeaderBox(headerBox, leaseInfo, tokenIdentifier, true);
1142
+ screen?.render();
1318
1143
  }
1319
- // Handle commands
1320
- if (trimmed.startsWith("/")) {
1321
- const [cmd] = trimmed.split(" ");
1322
- switch (cmd) {
1323
- case "/exit":
1324
- case "/quit":
1325
- isExiting = true;
1326
- if (leaseCheckInterval)
1327
- clearInterval(leaseCheckInterval);
1328
- if (headerUpdateInterval)
1329
- clearInterval(headerUpdateInterval);
1330
- if (ws)
1331
- ws.close();
1332
- readlineInterface.close();
1333
- console.log();
1334
- printCat("sleeping");
1335
- console.log(chalk.cyan("Chat disconnected. Goodbye! 👋"));
1336
- process.exit(0);
1337
- return;
1338
- case "/renew": {
1339
- try {
1340
- console.log(chalk.yellow("⏱️ Renewing lease..."));
1341
- const renewal = await renewLease(client, userAddress, leaseInfo?.leaseId);
1342
- leaseInfo = {
1343
- leaseId: renewal.leaseId,
1344
- leaseExpiresAt: new Date(renewal.leaseExpiresAt),
1345
- };
1346
- console.log(chalk.green(`✅ Lease renewed! Expires in ${formatTimeRemaining(leaseInfo.leaseExpiresAt)}`));
1347
- console.log(chalk.yellow("🔄 Reconnecting to chat stream..."));
1348
- // Close old connection properly
1349
- if (ws) {
1350
- ws.removeAllListeners();
1351
- // WebSocket.OPEN = 1, WebSocket.CONNECTING = 0
1352
- if (ws.readyState === 1 || ws.readyState === 0) {
1353
- ws.close();
1354
- }
1355
- ws = null;
1356
- }
1357
- // Wait for database transaction to commit
1358
- await new Promise((resolve) => setTimeout(resolve, 2000));
1359
- // Reconnect with new lease
1360
- if (wsUrl) {
1361
- // Normalize WebSocket URL protocol based on agent URL
1362
- const agentUrl = client.getAgentUrl();
1363
- const normalizedWsUrl = normalizeWebSocketUrl(wsUrl, agentUrl);
1364
- const wsUrlObj = new URL(normalizedWsUrl);
1365
- wsUrlObj.searchParams.set("leaseId", leaseInfo.leaseId);
1366
- wsUrl = wsUrlObj.toString();
1367
- // Retry logic
1368
- let retryCount = 0;
1369
- const maxRetries = 3;
1370
- let connected = false;
1371
- while (!connected && retryCount < maxRetries) {
1372
- try {
1373
- if (retryCount > 0) {
1374
- const backoffDelay = Math.min(1000 * Math.pow(2, retryCount - 1), 5000);
1375
- console.log(chalk.yellow(`🔄 Retry ${retryCount}/${maxRetries} in ${backoffDelay}ms...`));
1376
- await new Promise((resolve) => setTimeout(resolve, backoffDelay));
1377
- }
1378
- const newWsUrl = wsUrl;
1379
- await new Promise((resolve, reject) => {
1380
- const newWs = new WebSocket(newWsUrl);
1381
- attachWebSocketHandlers(newWs);
1382
- let timeout = null;
1383
- let openHandler = null;
1384
- let closeHandler = null;
1385
- let errorHandler = null;
1386
- const cleanup = () => {
1387
- if (timeout)
1388
- clearTimeout(timeout);
1389
- if (openHandler)
1390
- newWs.removeListener("open", openHandler);
1391
- if (closeHandler)
1392
- newWs.removeListener("close", closeHandler);
1393
- if (errorHandler)
1394
- newWs.removeListener("error", errorHandler);
1395
- };
1396
- timeout = setTimeout(() => {
1397
- cleanup();
1398
- newWs.close();
1399
- reject(new Error("Connection timeout"));
1400
- }, 10000);
1401
- openHandler = () => {
1402
- cleanup();
1403
- ws = newWs;
1404
- resolve();
1405
- };
1406
- closeHandler = (code, reason) => {
1407
- cleanup();
1408
- const reasonStr = reason.toString();
1409
- if (code === 1008 && reasonStr.includes("lease")) {
1410
- reject(new Error("Lease validation failed"));
1411
- }
1412
- else {
1413
- reject(new Error(`Connection closed: ${code} - ${reasonStr}`));
1414
- }
1415
- };
1416
- errorHandler = (error) => {
1417
- cleanup();
1418
- reject(error);
1419
- };
1420
- newWs.once("open", openHandler);
1421
- newWs.once("close", closeHandler);
1422
- newWs.once("error", errorHandler);
1423
- });
1424
- connected = true;
1425
- console.log(chalk.green("✅ Reconnected to chat stream"));
1426
- }
1427
- catch (error) {
1428
- retryCount++;
1429
- const errorMsg = error instanceof Error ? error.message : String(error);
1430
- if (retryCount >= maxRetries) {
1431
- throw new Error(`Failed to reconnect after ${maxRetries} attempts: ${errorMsg}`);
1432
- }
1433
- console.log(chalk.yellow(`⚠️ Connection attempt ${retryCount} failed: ${errorMsg}`));
1434
- }
1435
- }
1436
- }
1437
- if (readlineInterface)
1438
- readlineInterface.prompt();
1439
- }
1440
- catch (error) {
1441
- const errorMsg = error instanceof Error ? error.message : String(error);
1442
- console.log(chalk.red(`❌ ${errorMsg}`));
1443
- if (readlineInterface)
1444
- readlineInterface.prompt();
1445
- }
1446
- return;
1447
- }
1448
- case "/buy": {
1449
- // Only allow in token-specific chats
1450
- if (!tokenIdentifier) {
1451
- console.log(chalk.yellow("⚠️ /buy command only works in token-specific chats"));
1452
- if (readlineInterface)
1453
- readlineInterface.prompt();
1454
- return;
1455
- }
1456
- try {
1457
- const parts = trimmed.split(" ");
1458
- if (parts.length < 2) {
1459
- console.log(chalk.yellow("⚠️ Usage: /buy <amount> (e.g., /buy 0.05)"));
1460
- if (readlineInterface)
1461
- readlineInterface.prompt();
1462
- return;
1463
- }
1464
- const amount = parts[1];
1465
- console.log(chalk.yellow("💰 Buying tokens..."));
1466
- const isTestMode = client.getNetwork().includes("sepolia");
1467
- const result = await buyToken(client, tokenIdentifier, amount, isTestMode, true // silent mode
1468
- );
1469
- console.log(formatBuyResultCompact(result));
1470
- if (readlineInterface)
1471
- readlineInterface.prompt();
1472
- }
1473
- catch (error) {
1474
- const errorMsg = error instanceof Error ? error.message : String(error);
1475
- console.log(chalk.red(`❌ Buy failed: ${errorMsg}`));
1476
- if (readlineInterface)
1477
- readlineInterface.prompt();
1478
- }
1479
- return;
1480
- }
1481
- case "/sell": {
1482
- // Only allow in token-specific chats
1483
- if (!tokenIdentifier) {
1484
- console.log(chalk.yellow("⚠️ /sell command only works in token-specific chats"));
1485
- if (readlineInterface)
1486
- readlineInterface.prompt();
1487
- return;
1488
- }
1489
- try {
1490
- const parts = trimmed.split(" ");
1491
- if (parts.length < 2) {
1492
- console.log(chalk.yellow("⚠️ Usage: /sell <amount|all|percentage> (e.g., /sell 0.05, /sell all, /sell 50%)"));
1493
- if (readlineInterface)
1494
- readlineInterface.prompt();
1495
- return;
1496
- }
1497
- const amountStr = parts[1];
1498
- console.log(chalk.yellow("💸 Selling tokens..."));
1499
- // Get token info to check balance
1500
- if (!userAddress) {
1501
- throw new Error("User address not available");
1502
- }
1503
- const tokenInfo = await getTokenInfo(client, tokenIdentifier, userAddress, true // silent mode
1504
- );
1505
- if (!tokenInfo.userPosition || tokenInfo.userPosition.tokensOwned === "0") {
1506
- throw new Error("You do not own any of this token");
1507
- }
1508
- // Parse sell amount
1509
- const tokenAmount = parseTokenAmount(amountStr, tokenInfo.userPosition.tokensOwned);
1510
- const result = await sellToken(client, tokenIdentifier, tokenAmount, true // silent mode
1511
- );
1512
- console.log(formatSellResultCompact(result));
1513
- if (readlineInterface)
1514
- readlineInterface.prompt();
1515
- }
1516
- catch (error) {
1517
- const errorMsg = error instanceof Error ? error.message : String(error);
1518
- console.log(chalk.red(`❌ Sell failed: ${errorMsg}`));
1519
- if (readlineInterface)
1520
- readlineInterface.prompt();
1521
- }
1522
- return;
1523
- }
1524
- case "/help":
1525
- const helpText = tokenIdentifier
1526
- ? chalk.dim("Commands: /exit, /quit, /renew, /buy, /sell, /help")
1527
- : chalk.dim("Commands: /exit, /quit, /renew, /help");
1528
- console.log(helpText);
1529
- if (readlineInterface)
1530
- readlineInterface.prompt();
1531
- return;
1532
- default:
1533
- console.log(chalk.yellow(`Unknown command: ${cmd}. Type /help for commands.`));
1534
- if (readlineInterface)
1535
- readlineInterface.prompt();
1536
- return;
1537
- }
1538
- }
1539
- // Check if websocket is still connected
1144
+ // Double-check websocket is still connected after lease renewal
1145
+ // WebSocket.OPEN = 1
1540
1146
  if (!ws || ws.readyState !== 1) {
1541
- const stateMsg = ws
1542
- ? ws.readyState === 0 ? "connecting"
1543
- : ws.readyState === 2 ? "closing"
1544
- : ws.readyState === 3 ? "closed"
1545
- : "unknown"
1546
- : "not initialized";
1547
- console.log(chalk.red(`❌ WebSocket is not connected (state: ${stateMsg}). Please wait for connection or type /renew.`));
1548
- if (readlineInterface)
1549
- readlineInterface.prompt();
1550
- return;
1147
+ throw new Error("WebSocket connection lost. Please wait for reconnection.");
1551
1148
  }
1552
- // Check if lease is valid
1553
- if (!leaseInfo || leaseInfo.leaseExpiresAt.getTime() <= Date.now()) {
1554
- console.log(chalk.yellow("⏱️ Your lease has expired. Type /renew to continue chatting."));
1555
- if (readlineInterface)
1556
- readlineInterface.prompt();
1557
- return;
1558
- }
1559
- // Send message
1560
- isSending = true;
1561
- currentInput = trimmed;
1562
- try {
1563
- // Ensure lease is valid before sending
1564
- leaseInfo = await ensureLeaseValid(client, userAddress, leaseInfo, undefined, undefined);
1565
- // Double-check websocket is still connected
1566
- if (!ws || ws.readyState !== 1) {
1567
- throw new Error("WebSocket connection lost. Please wait for reconnection.");
1568
- }
1569
- const result = await sendChatMessage(client, trimmed, leaseInfo.leaseId, userAddress);
1570
- // Track this message
1571
- const tempMessageId = `pending-${Date.now()}-${Math.random()}`;
1572
- pendingMessages.set(result.messageId, tempMessageId);
1573
- pendingMessages.set(`text:${trimmed}`, tempMessageId);
1574
- // Message sent - will be displayed when received via WebSocket
1149
+ const result = await sendChatMessage(client, trimmed, leaseInfo.leaseId, userAddress);
1150
+ userAddress = result.author;
1151
+ // Track this message
1152
+ pendingMessages.set(result.messageId, tempMessageId);
1153
+ pendingMessages.set(`text:${trimmed}`, tempMessageId);
1154
+ // Keep pulsing until WebSocket confirms
1155
+ }
1156
+ catch (error) {
1157
+ // Stop pulsing
1158
+ if (pulseInterval) {
1159
+ clearInterval(pulseInterval);
1160
+ pulseIntervals.delete(tempMessageId);
1575
1161
  }
1576
- catch (error) {
1577
- const errorMsg = error instanceof Error ? error.message : String(error);
1578
- console.log(chalk.red(`❌ ${errorMsg}`));
1579
- isSending = false;
1580
- currentInput = "";
1162
+ const errorMsg = error instanceof Error ? error.message : String(error);
1163
+ if (messageLogBox) {
1164
+ messageLogBox.log(chalk.red(`❌ ${errorMsg}`));
1165
+ messageLogBox.setScrollPerc(100);
1581
1166
  }
1582
- if (readlineInterface)
1583
- readlineInterface.prompt();
1584
- });
1585
- // Handle Ctrl+C for readline
1586
- readlineInterface.on("SIGINT", () => {
1587
- isExiting = true;
1588
- isSending = false;
1589
- pulseIntervals.forEach((interval) => clearInterval(interval));
1590
- pulseIntervals.clear();
1591
- if (leaseCheckInterval)
1592
- clearInterval(leaseCheckInterval);
1593
- if (headerUpdateInterval)
1594
- clearInterval(headerUpdateInterval);
1595
- if (ws)
1596
- ws.close();
1597
- if (readlineInterface)
1598
- readlineInterface.close();
1599
- console.log();
1600
- printCat("sleeping");
1601
- console.log(chalk.cyan("Chat disconnected. Goodbye! 👋"));
1602
- process.exit(0);
1603
- });
1604
- }
1605
- // Handle Ctrl+C (for blessed mode)
1606
- if (useBlessed && screen) {
1607
- screen.key(["C-c"], () => {
1608
- isExiting = true;
1167
+ inputBox?.clearValue();
1168
+ inputBox?.focus();
1609
1169
  isSending = false;
1610
- pulseIntervals.forEach((interval) => clearInterval(interval));
1611
- pulseIntervals.clear();
1612
- if (leaseCheckInterval)
1613
- clearInterval(leaseCheckInterval);
1614
- if (headerUpdateInterval)
1615
- clearInterval(headerUpdateInterval);
1616
- if (ws)
1617
- ws.close();
1618
- screen?.destroy();
1619
- console.log();
1620
- printCat("sleeping");
1621
- console.log(chalk.cyan("Chat disconnected. Goodbye! 👋"));
1622
- process.exit(0);
1623
- });
1624
- // Focus input box and render (only for blessed)
1625
- if (inputBox) {
1626
- inputBox.focus();
1627
- screen.render();
1170
+ currentInput = "";
1171
+ screen?.render();
1628
1172
  }
1629
- }
1173
+ });
1174
+ // Handle Ctrl+C
1175
+ screen.key(["C-c"], () => {
1176
+ isExiting = true;
1177
+ isSending = false;
1178
+ pulseIntervals.forEach((interval) => clearInterval(interval));
1179
+ pulseIntervals.clear();
1180
+ if (leaseCheckInterval)
1181
+ clearInterval(leaseCheckInterval);
1182
+ if (headerUpdateInterval)
1183
+ clearInterval(headerUpdateInterval);
1184
+ if (ws)
1185
+ ws.close();
1186
+ screen?.destroy();
1187
+ console.log();
1188
+ printCat("sleeping");
1189
+ console.log(chalk.cyan("Chat disconnected. Goodbye! 👋"));
1190
+ process.exit(0);
1191
+ });
1192
+ // Focus input box and render
1193
+ inputBox.focus();
1194
+ screen.render();
1630
1195
  }
1631
1196
  else {
1632
1197
  // JSON mode: read messages from stdin and output JSON to stdout
@@ -1929,10 +1494,6 @@ export async function startChatStream(client, jsonMode = false, tokenIdentifier,
1929
1494
  if (screen) {
1930
1495
  screen.destroy();
1931
1496
  }
1932
- // Only close readline if not in JSON mode (readline should never exist in JSON mode)
1933
- if (rl && !jsonMode) {
1934
- rl.close();
1935
- }
1936
1497
  }
1937
1498
  catch (cleanupError) {
1938
1499
  // Ignore cleanup errors