httpcat-cli 0.3.0 → 0.3.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 (322) hide show
  1. package/.github/workflows/ci.yml +3 -0
  2. package/.github/workflows/rc-publish.yml +6 -0
  3. package/.github/workflows/release.yml +102 -0
  4. package/.github/workflows/sync-version.yml +31 -2
  5. package/README.md +1408 -109
  6. package/additions.txt +3 -0
  7. package/bun.lock +260 -25
  8. package/dist/agent/autonomous-trader.d.ts.map +1 -0
  9. package/dist/agent/autonomous-trader.js +362 -0
  10. package/dist/agent/autonomous-trader.js.map +1 -0
  11. package/dist/agent/ax-agent.d.ts.map +1 -1
  12. package/dist/agent/ax-agent.js +356 -18
  13. package/dist/agent/ax-agent.js.map +1 -1
  14. package/dist/agent/event-client.d.ts.map +1 -0
  15. package/dist/agent/event-client.js +82 -0
  16. package/dist/agent/event-client.js.map +1 -0
  17. package/dist/agent/log-stream.d.ts.map +1 -0
  18. package/dist/agent/log-stream.js +95 -0
  19. package/dist/agent/log-stream.js.map +1 -0
  20. package/dist/agent/memory/conversation-session.d.ts.map +1 -0
  21. package/dist/agent/memory/conversation-session.js +232 -0
  22. package/dist/agent/memory/conversation-session.js.map +1 -0
  23. package/dist/agent/memory/conversation-store.d.ts.map +1 -0
  24. package/dist/agent/memory/conversation-store.js +214 -0
  25. package/dist/agent/memory/conversation-store.js.map +1 -0
  26. package/dist/agent/memory/database-schema.d.ts.map +1 -0
  27. package/dist/agent/memory/database-schema.js +355 -0
  28. package/dist/agent/memory/database-schema.js.map +1 -0
  29. package/dist/agent/memory/decision-tracker.d.ts.map +1 -0
  30. package/dist/agent/memory/decision-tracker.js +274 -0
  31. package/dist/agent/memory/decision-tracker.js.map +1 -0
  32. package/dist/agent/memory/memory-manager.d.ts.map +1 -0
  33. package/dist/agent/memory/memory-manager.js +187 -0
  34. package/dist/agent/memory/memory-manager.js.map +1 -0
  35. package/dist/agent/memory/types.d.ts.map +1 -0
  36. package/dist/agent/memory/types.js +5 -0
  37. package/dist/agent/memory/types.js.map +1 -0
  38. package/dist/agent/message-formatter.d.ts.map +1 -0
  39. package/dist/agent/message-formatter.js +76 -0
  40. package/dist/agent/message-formatter.js.map +1 -0
  41. package/dist/agent/position-db.d.ts.map +1 -0
  42. package/dist/agent/position-db.js +154 -0
  43. package/dist/agent/position-db.js.map +1 -0
  44. package/dist/agent/simple-chat-ui-static.d.ts.map +1 -0
  45. package/dist/agent/simple-chat-ui-static.js +129 -0
  46. package/dist/agent/simple-chat-ui-static.js.map +1 -0
  47. package/dist/agent/simple-chat-ui.d.ts.map +1 -0
  48. package/dist/agent/simple-chat-ui.js +90 -0
  49. package/dist/agent/simple-chat-ui.js.map +1 -0
  50. package/dist/agent/tools.d.ts.map +1 -1
  51. package/dist/agent/tools.js +297 -4
  52. package/dist/agent/tools.js.map +1 -1
  53. package/dist/agent/ui.d.ts.map +1 -0
  54. package/dist/agent/ui.js +84 -0
  55. package/dist/agent/ui.js.map +1 -0
  56. package/dist/agent/unified-runtime.d.ts.map +1 -0
  57. package/dist/agent/unified-runtime.js +397 -0
  58. package/dist/agent/unified-runtime.js.map +1 -0
  59. package/dist/client.d.ts.map +1 -1
  60. package/dist/client.js +272 -21
  61. package/dist/client.js.map +1 -1
  62. package/dist/commands/account.d.ts.map +1 -1
  63. package/dist/commands/account.js +187 -33
  64. package/dist/commands/account.js.map +1 -1
  65. package/dist/commands/agent.d.ts.map +1 -0
  66. package/dist/commands/agent.js +125 -0
  67. package/dist/commands/agent.js.map +1 -0
  68. package/dist/commands/approve.d.ts.map +1 -0
  69. package/dist/commands/approve.js +505 -0
  70. package/dist/commands/approve.js.map +1 -0
  71. package/dist/commands/automation.d.ts.map +1 -0
  72. package/dist/commands/automation.js +346 -0
  73. package/dist/commands/automation.js.map +1 -0
  74. package/dist/commands/balances.d.ts.map +1 -1
  75. package/dist/commands/balances.js +226 -73
  76. package/dist/commands/balances.js.map +1 -1
  77. package/dist/commands/buy.d.ts.map +1 -1
  78. package/dist/commands/buy.js +149 -146
  79. package/dist/commands/buy.js.map +1 -1
  80. package/dist/commands/call.d.ts.map +1 -0
  81. package/dist/commands/call.js +51 -0
  82. package/dist/commands/call.js.map +1 -0
  83. package/dist/commands/cex.d.ts.map +1 -0
  84. package/dist/commands/cex.js +958 -0
  85. package/dist/commands/cex.js.map +1 -0
  86. package/dist/commands/chat.d.ts.map +1 -1
  87. package/dist/commands/chat.js +169 -411
  88. package/dist/commands/chat.js.map +1 -1
  89. package/dist/commands/claim.d.ts.map +1 -1
  90. package/dist/commands/claim.js +313 -29
  91. package/dist/commands/claim.js.map +1 -1
  92. package/dist/commands/create.d.ts.map +1 -1
  93. package/dist/commands/create.js +151 -43
  94. package/dist/commands/create.js.map +1 -1
  95. package/dist/commands/gasless-swap.d.ts.map +1 -0
  96. package/dist/commands/gasless-swap.js +232 -0
  97. package/dist/commands/gasless-swap.js.map +1 -0
  98. package/dist/commands/health.d.ts.map +1 -1
  99. package/dist/commands/health.js +63 -7
  100. package/dist/commands/health.js.map +1 -1
  101. package/dist/commands/info.d.ts.map +1 -1
  102. package/dist/commands/info.js +131 -47
  103. package/dist/commands/info.js.map +1 -1
  104. package/dist/commands/launchpad.d.ts.map +1 -0
  105. package/dist/commands/launchpad.js +708 -0
  106. package/dist/commands/launchpad.js.map +1 -0
  107. package/dist/commands/list.d.ts.map +1 -1
  108. package/dist/commands/list.js +57 -23
  109. package/dist/commands/list.js.map +1 -1
  110. package/dist/commands/market.d.ts.map +1 -0
  111. package/dist/commands/market.js +960 -0
  112. package/dist/commands/market.js.map +1 -0
  113. package/dist/commands/mcp-install.d.ts.map +1 -0
  114. package/dist/commands/mcp-install.js +387 -0
  115. package/dist/commands/mcp-install.js.map +1 -0
  116. package/dist/commands/opps.d.ts.map +1 -0
  117. package/dist/commands/opps.js +409 -0
  118. package/dist/commands/opps.js.map +1 -0
  119. package/dist/commands/perps.d.ts.map +1 -0
  120. package/dist/commands/perps.js +248 -0
  121. package/dist/commands/perps.js.map +1 -0
  122. package/dist/commands/portfolio.d.ts.map +1 -0
  123. package/dist/commands/portfolio.js +679 -0
  124. package/dist/commands/portfolio.js.map +1 -0
  125. package/dist/commands/positions.d.ts.map +1 -1
  126. package/dist/commands/positions.js +76 -47
  127. package/dist/commands/positions.js.map +1 -1
  128. package/dist/commands/predict.d.ts.map +1 -0
  129. package/dist/commands/predict.js +280 -0
  130. package/dist/commands/predict.js.map +1 -0
  131. package/dist/commands/predictions.d.ts.map +1 -0
  132. package/dist/commands/predictions.js +486 -0
  133. package/dist/commands/predictions.js.map +1 -0
  134. package/dist/commands/risk.d.ts.map +1 -0
  135. package/dist/commands/risk.js +225 -0
  136. package/dist/commands/risk.js.map +1 -0
  137. package/dist/commands/security.d.ts.map +1 -0
  138. package/dist/commands/security.js +244 -0
  139. package/dist/commands/security.js.map +1 -0
  140. package/dist/commands/sell.d.ts.map +1 -1
  141. package/dist/commands/sell.js +67 -34
  142. package/dist/commands/sell.js.map +1 -1
  143. package/dist/commands/send.d.ts.map +1 -0
  144. package/dist/commands/send.js +733 -0
  145. package/dist/commands/send.js.map +1 -0
  146. package/dist/commands/sign.d.ts.map +1 -0
  147. package/dist/commands/sign.js +1048 -0
  148. package/dist/commands/sign.js.map +1 -0
  149. package/dist/commands/swap.d.ts.map +1 -0
  150. package/dist/commands/swap.js +744 -0
  151. package/dist/commands/swap.js.map +1 -0
  152. package/dist/commands/system.d.ts.map +1 -0
  153. package/dist/commands/system.js +417 -0
  154. package/dist/commands/system.js.map +1 -0
  155. package/dist/commands/tools/index.d.ts.map +1 -0
  156. package/dist/commands/tools/index.js +2040 -0
  157. package/dist/commands/tools/index.js.map +1 -0
  158. package/dist/commands/trade.d.ts.map +1 -0
  159. package/dist/commands/trade.js +237 -0
  160. package/dist/commands/trade.js.map +1 -0
  161. package/dist/commands/transactions.d.ts.map +1 -1
  162. package/dist/commands/transactions.js +29 -17
  163. package/dist/commands/transactions.js.map +1 -1
  164. package/dist/commands/update.d.ts.map +1 -0
  165. package/dist/commands/update.js +429 -0
  166. package/dist/commands/update.js.map +1 -0
  167. package/dist/config.d.ts.map +1 -1
  168. package/dist/config.js +351 -40
  169. package/dist/config.js.map +1 -1
  170. package/dist/index.js +4524 -924
  171. package/dist/index.js.map +1 -1
  172. package/dist/interactive/art.d.ts.map +1 -1
  173. package/dist/interactive/art.js +33 -1
  174. package/dist/interactive/art.js.map +1 -1
  175. package/dist/interactive/shell.d.ts.map +1 -1
  176. package/dist/interactive/shell.js +467 -2652
  177. package/dist/interactive/shell.js.map +1 -1
  178. package/dist/mcp/context.d.ts.map +1 -0
  179. package/dist/mcp/context.js +211 -0
  180. package/dist/mcp/context.js.map +1 -0
  181. package/dist/mcp/onboarding.d.ts.map +1 -0
  182. package/dist/mcp/onboarding.js +266 -0
  183. package/dist/mcp/onboarding.js.map +1 -0
  184. package/dist/mcp/resources.d.ts.map +1 -0
  185. package/dist/mcp/resources.js +222 -0
  186. package/dist/mcp/resources.js.map +1 -0
  187. package/dist/mcp/server.d.ts.map +1 -1
  188. package/dist/mcp/server.js +51 -1
  189. package/dist/mcp/server.js.map +1 -1
  190. package/dist/mcp/tools.d.ts.map +1 -1
  191. package/dist/mcp/tools.js +4119 -169
  192. package/dist/mcp/tools.js.map +1 -1
  193. package/dist/mcp/types.d.ts.map +1 -1
  194. package/dist/types/agent-info.d.ts.map +1 -0
  195. package/dist/types/agent-info.js +11 -0
  196. package/dist/types/agent-info.js.map +1 -0
  197. package/dist/ui/components/ScrollableList.d.ts.map +1 -0
  198. package/dist/ui/components/ScrollableList.js +72 -0
  199. package/dist/ui/components/ScrollableList.js.map +1 -0
  200. package/dist/ui/components/ThemeProvider.d.ts.map +1 -0
  201. package/dist/ui/components/ThemeProvider.js +87 -0
  202. package/dist/ui/components/ThemeProvider.js.map +1 -0
  203. package/dist/ui/components/ThemedBox.d.ts.map +1 -0
  204. package/dist/ui/components/ThemedBox.js +24 -0
  205. package/dist/ui/components/ThemedBox.js.map +1 -0
  206. package/dist/ui/components/agent/ChatHeader.d.ts.map +1 -0
  207. package/dist/ui/components/agent/ChatHeader.js +39 -0
  208. package/dist/ui/components/agent/ChatHeader.js.map +1 -0
  209. package/dist/ui/components/agent/Header.d.ts.map +1 -0
  210. package/dist/ui/components/agent/Header.js +14 -0
  211. package/dist/ui/components/agent/Header.js.map +1 -0
  212. package/dist/ui/components/agent/Input.d.ts.map +1 -0
  213. package/dist/ui/components/agent/Input.js +23 -0
  214. package/dist/ui/components/agent/Input.js.map +1 -0
  215. package/dist/ui/components/agent/Output.d.ts.map +1 -0
  216. package/dist/ui/components/agent/Output.js +23 -0
  217. package/dist/ui/components/agent/Output.js.map +1 -0
  218. package/dist/ui/components/chat/TokenChatUI.d.ts.map +1 -0
  219. package/dist/ui/components/chat/TokenChatUI.js +133 -0
  220. package/dist/ui/components/chat/TokenChatUI.js.map +1 -0
  221. package/dist/ui/components/shell/ShellHeader.d.ts.map +1 -0
  222. package/dist/ui/components/shell/ShellHeader.js +31 -0
  223. package/dist/ui/components/shell/ShellHeader.js.map +1 -0
  224. package/dist/ui/components/shell/ShellInput.d.ts.map +1 -0
  225. package/dist/ui/components/shell/ShellInput.js +151 -0
  226. package/dist/ui/components/shell/ShellInput.js.map +1 -0
  227. package/dist/ui/components/shell/ShellOutput.d.ts.map +1 -0
  228. package/dist/ui/components/shell/ShellOutput.js +8 -0
  229. package/dist/ui/components/shell/ShellOutput.js.map +1 -0
  230. package/dist/ui/hooks/useChatWebSocket.d.ts.map +1 -0
  231. package/dist/ui/hooks/useChatWebSocket.js +76 -0
  232. package/dist/ui/hooks/useChatWebSocket.js.map +1 -0
  233. package/dist/ui/hooks/useCommandHistory.d.ts.map +1 -0
  234. package/dist/ui/hooks/useCommandHistory.js +70 -0
  235. package/dist/ui/hooks/useCommandHistory.js.map +1 -0
  236. package/dist/ui/hooks/useDebounce.d.ts.map +1 -0
  237. package/dist/ui/hooks/useDebounce.js +17 -0
  238. package/dist/ui/hooks/useDebounce.js.map +1 -0
  239. package/dist/ui/hooks/useLogStream.d.ts.map +1 -0
  240. package/dist/ui/hooks/useLogStream.js +20 -0
  241. package/dist/ui/hooks/useLogStream.js.map +1 -0
  242. package/dist/ui/hooks/useVirtualScroll.d.ts.map +1 -0
  243. package/dist/ui/hooks/useVirtualScroll.js +70 -0
  244. package/dist/ui/hooks/useVirtualScroll.js.map +1 -0
  245. package/dist/utils/admin.d.ts.map +1 -0
  246. package/dist/utils/admin.js +144 -0
  247. package/dist/utils/admin.js.map +1 -0
  248. package/dist/utils/autoSetup.d.ts.map +1 -0
  249. package/dist/utils/autoSetup.js +252 -0
  250. package/dist/utils/autoSetup.js.map +1 -0
  251. package/dist/utils/build-constants.d.ts.map +1 -0
  252. package/dist/utils/build-constants.js +10 -0
  253. package/dist/utils/build-constants.js.map +1 -0
  254. package/dist/utils/constants.d.ts.map +1 -1
  255. package/dist/utils/errors.d.ts.map +1 -1
  256. package/dist/utils/errors.js +10 -1
  257. package/dist/utils/errors.js.map +1 -1
  258. package/dist/utils/formatting.d.ts.map +1 -1
  259. package/dist/utils/formatting.js +46 -9
  260. package/dist/utils/formatting.js.map +1 -1
  261. package/dist/utils/llm-cli-config.d.ts.map +1 -0
  262. package/dist/utils/llm-cli-config.js +963 -0
  263. package/dist/utils/llm-cli-config.js.map +1 -0
  264. package/dist/utils/llm-cli-detector.d.ts.map +1 -0
  265. package/dist/utils/llm-cli-detector.js +202 -0
  266. package/dist/utils/llm-cli-detector.js.map +1 -0
  267. package/dist/utils/loading.d.ts.map +1 -1
  268. package/dist/utils/loading.js +25 -3
  269. package/dist/utils/loading.js.map +1 -1
  270. package/dist/utils/maintenance.d.ts.map +1 -0
  271. package/dist/utils/maintenance.js +17 -0
  272. package/dist/utils/maintenance.js.map +1 -0
  273. package/dist/utils/mcp-config.d.ts.map +1 -0
  274. package/dist/utils/mcp-config.js +77 -0
  275. package/dist/utils/mcp-config.js.map +1 -0
  276. package/dist/utils/privateKeyPrompt.d.ts.map +1 -1
  277. package/dist/utils/privateKeyPrompt.js +308 -129
  278. package/dist/utils/privateKeyPrompt.js.map +1 -1
  279. package/dist/utils/process-cleanup.d.ts.map +1 -0
  280. package/dist/utils/process-cleanup.js +136 -0
  281. package/dist/utils/process-cleanup.js.map +1 -0
  282. package/dist/utils/retry.d.ts.map +1 -0
  283. package/dist/utils/retry.js +56 -0
  284. package/dist/utils/retry.js.map +1 -0
  285. package/dist/utils/rpc-helpers.d.ts.map +1 -0
  286. package/dist/utils/rpc-helpers.js +70 -0
  287. package/dist/utils/rpc-helpers.js.map +1 -0
  288. package/dist/utils/rpc-transport.d.ts.map +1 -0
  289. package/dist/utils/rpc-transport.js +87 -0
  290. package/dist/utils/rpc-transport.js.map +1 -0
  291. package/dist/utils/shell-setup.d.ts.map +1 -0
  292. package/dist/utils/shell-setup.js +531 -0
  293. package/dist/utils/shell-setup.js.map +1 -0
  294. package/dist/utils/status.d.ts.map +1 -1
  295. package/dist/utils/status.js +34 -5
  296. package/dist/utils/status.js.map +1 -1
  297. package/dist/utils/token-resolver.d.ts.map +1 -1
  298. package/dist/utils/token-resolver.js +51 -8
  299. package/dist/utils/token-resolver.js.map +1 -1
  300. package/dist/utils/x402-caller.d.ts.map +1 -0
  301. package/dist/utils/x402-caller.js +17 -0
  302. package/dist/utils/x402-caller.js.map +1 -0
  303. package/docs/README.md +28 -0
  304. package/docs/agent/README.md +18 -0
  305. package/docs/api/README.md +41 -0
  306. package/docs/cli/README.md +42 -0
  307. package/docs/guides/README.md +26 -0
  308. package/docs/implementation/README.md +18 -0
  309. package/docs/planning/README.md +19 -0
  310. package/docs/testing/README.md +15 -0
  311. package/docs/ux/README.md +16 -0
  312. package/issues.txt +2 -0
  313. package/package.json +24 -9
  314. package/scripts/cat-spin.sh +417 -0
  315. package/scripts/deprecate-rc-versions.js +58 -0
  316. package/scripts/inject-build-constants.js +43 -0
  317. package/scripts/monitor-foobar.js +117 -0
  318. package/swap.logs +61 -0
  319. package/swapping.txt +108 -0
  320. package/test.txt +12 -0
  321. package/tests/fixtures/test-data.json +16 -0
  322. package/Screenshot 2025-12-21 at 8.56.02/342/200/257PM.png +0 -0
@@ -1,2702 +1,517 @@
1
- import chalk from "chalk";
2
- // @ts-ignore - neo-blessed doesn't have types, but @types/blessed provides compatible types
3
- import blessed from "neo-blessed";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Interactive Shell - Ink version
4
+ *
5
+ * This is a React/Ink replacement for the neo-blessed shell.
6
+ * Key improvements:
7
+ * - No manual rendering needed (React handles it)
8
+ * - Cleaner state management
9
+ * - Better separation of concerns
10
+ * - Command history with hooks
11
+ * - Theme toggling with F1
12
+ */
13
+ import { useState, useEffect } from "react";
14
+ import { render, Box, useInput, useApp, useStdout } from "ink";
15
+ import { ThemeProvider, useTheme, } from "../ui/components/ThemeProvider.js";
16
+ import { ChatHeader } from "../ui/components/agent/ChatHeader.js";
17
+ import { ShellOutput } from "../ui/components/shell/ShellOutput.js";
18
+ import { ShellInput } from "../ui/components/shell/ShellInput.js";
4
19
  import { HttpcatClient } from "../client.js";
20
+ import chalk from "chalk";
5
21
  import { config } from "../config.js";
6
- import { printCat } from "./art.js";
7
- import { validateAmount } from "../utils/validation.js";
8
- // Import commands
9
- import { createToken, processPhotoUrl, isFilePath, } from "../commands/create.js";
10
- import { buyToken, TEST_AMOUNTS, PROD_AMOUNTS, } from "../commands/buy.js";
11
- import { sellToken, parseTokenAmount } from "../commands/sell.js";
12
- import { getTokenInfo } from "../commands/info.js";
13
- import { listTokens } from "../commands/list.js";
14
- import { formatCurrency, formatTokenAmount, formatAddress, } from "../utils/formatting.js";
15
- import { getPositions } from "../commands/positions.js";
16
22
  import { privateKeyToAccount } from "viem/accounts";
17
- import { checkHealth } from "../commands/health.js";
18
- import { joinChat, sendChatMessage, renewLease, normalizeWebSocketUrl, } from "../commands/chat.js";
19
- import WebSocket from "ws";
20
- import { checkBalance } from "../commands/balances.js";
21
- import { viewFees, claimFees } from "../commands/claim.js";
22
- import { getTransactions } from "../commands/transactions.js";
23
- import { getAccountInfo, switchAccount, addAccount, } from "../commands/account.js";
24
- import { HttpcatError } from "../client.js";
25
- import { createHttpcatAgent, chatWithAgent } from "../agent/ax-agent.js";
26
- import { createLLM } from "../agent/llm-factory.js";
27
- // Detect terminal background color
28
- function detectTerminalBackground() {
29
- // Check COLORFGBG (format: "foreground;background")
30
- // Background values: 0-7 are dark, 8-15 are light
31
- const colorfgbg = process.env.COLORFGBG;
32
- if (colorfgbg) {
33
- const parts = colorfgbg.split(";");
34
- if (parts.length >= 2) {
35
- const bg = parseInt(parts[1], 10);
36
- if (!isNaN(bg)) {
37
- // 0-7 are dark backgrounds, 8-15 are light
38
- return bg < 8 ? "dark" : "light";
39
- }
40
- }
41
- }
42
- // Check TERM_PROGRAM for common terminals
43
- const termProgram = process.env.TERM_PROGRAM?.toLowerCase();
44
- if (termProgram) {
45
- // These terminals often have dark backgrounds by default
46
- if (["iterm2", "vscode", "hyper", "alacritty", "kitty"].includes(termProgram)) {
47
- return "dark";
48
- }
49
- // These often have light backgrounds
50
- if (["apple_terminal"].includes(termProgram)) {
51
- return "light";
52
- }
53
- }
54
- // Check for common dark terminal indicators
55
- const term = process.env.TERM?.toLowerCase() || "";
56
- if (term.includes("256") || term.includes("xterm")) {
57
- // Most modern terminals default to dark
58
- return "dark";
59
- }
60
- // Default to dark (safer assumption for modern terminals)
61
- return "dark";
62
- }
63
- export async function startInteractiveShell(client, autoChatToken) {
64
- // Auto-detect terminal background and set default theme
65
- const detectedBg = detectTerminalBackground();
66
- let currentTheme = detectedBg === "dark" ? "dark" : "win95";
67
- // Helper function to get theme-appropriate cyan/blue color for blessed tags
68
- // For dark theme, use lighter colors (light-cyan-fg) for better visibility on black
69
- const getCyanColor = (theme) => theme === "dark" ? "light-cyan-fg" : "cyan-fg";
70
- const getBlueColor = (theme) => theme === "dark" ? "light-blue-fg" : "blue-fg";
71
- // Create blessed screen with optimized settings
72
- const screen = blessed.screen({
73
- smartCSR: true,
74
- title: "httpcat Interactive Shell",
75
- fullUnicode: true, // Support double-width/surrogate/combining chars (emojis)
76
- fastCSR: false, // Disable fast CSR to prevent rendering issues
77
- cursor: {
78
- artificial: true,
79
- shape: "line",
80
- blink: true,
81
- color: "green",
82
- },
83
- // Force Unicode support for emojis
84
- forceUnicode: true,
85
- });
86
- const network = client.getNetwork();
87
- // Theme colors - no backgrounds, just borders
88
- const getThemeColors = (theme) => {
89
- switch (theme) {
90
- case "light":
91
- return {
92
- bg: "default", // Transparent/default
93
- fg: "black",
94
- border: "black",
95
- inputBg: "default",
96
- inputFg: "black", // Explicit black for visibility
97
- inputFocusBg: "default",
98
- inputFocusFg: "black",
99
- };
100
- case "win95":
101
- return {
102
- bg: "default",
103
- fg: "black",
104
- border: "black",
105
- inputBg: "default",
106
- inputFg: "black", // Explicit black for visibility
107
- inputFocusBg: "default",
108
- inputFocusFg: "black",
109
- };
110
- default: // dark
111
- return {
112
- bg: "default",
113
- fg: "green",
114
- border: "green",
115
- inputBg: "default",
116
- inputFg: "green", // Explicit green for visibility
117
- inputFocusBg: "default",
118
- inputFocusFg: "green",
119
- };
120
- }
121
- };
122
- let themeColors = getThemeColors(currentTheme);
123
- // Create header box with thick borders, transparent background - more compact
124
- const headerBox = blessed.box({
125
- top: 0,
126
- left: 0,
127
- width: "100%",
128
- height: 6, // Reduced to save vertical space
129
- content: "",
130
- tags: true,
131
- style: {
132
- fg: themeColors.fg,
133
- bg: "default", // Transparent
134
- bold: false,
135
- border: {
136
- fg: themeColors.border,
137
- bold: true,
138
- },
139
- },
140
- padding: {
141
- left: 1,
142
- right: 1,
143
- top: 0,
144
- bottom: 0,
145
- },
146
- border: {
147
- type: "line",
148
- fg: themeColors.border,
149
- ch: "═", // Double line for thicker border
150
- },
151
- });
152
- // Cat face variants for animation
153
- const catFaces = [
154
- { name: "Sleepy", face: "[=^ -.- ^=]" },
155
- { name: "Smug", face: "[=^‿^=]" },
156
- { name: "Unhinged", face: "[=^◉_◉^=]" },
157
- { name: "Judgy", face: "[=^ಠ‿ಠ^=]" },
158
- { name: "Cute", face: "[=^。^=]" },
159
- { name: "Menacing", face: "[=^>_<^=]" },
160
- { name: "Loaf Mode", face: "[=^___^=]" },
161
- { name: "Cosmic", face: "[=^✧_✧^=]" },
162
- ];
163
- let currentCatIndex = 0;
164
- let catAnimationInterval = null;
165
- // Helper function to build welcome content with account info - more compact
166
- const buildWelcomeContent = async (theme, catFace) => {
167
- const welcomeLines = [];
168
- const colorTag = theme === "dark" ? "green-fg" : "black-fg";
169
- const cyanColor = getCyanColor(theme);
170
- // Use provided cat face or current one
171
- const displayCatFace = catFace || catFaces[currentCatIndex].face;
172
- // Cat face with breathing room, compact info below
173
- welcomeLines.push(`{${colorTag}}${displayCatFace}{/${colorTag}}`);
174
- welcomeLines.push(`{green-fg}🐱 Welcome to httpcat!{/green-fg} | {green-fg}🌐 {${cyanColor}}${network}{/${cyanColor}}{/green-fg}`);
175
- // Get account info
176
- let accountInfo = null;
177
- try {
178
- const accounts = config.getAllAccounts();
179
- const activeIndex = config.getActiveAccountIndex();
180
- const account = accounts.find((acc) => acc.index === activeIndex);
181
- if (account) {
182
- // Get balance info
23
+ import { checkMaintenanceMode } from "../utils/maintenance.js";
24
+ function Shell({ client, autoChatToken, onTransition }) {
25
+ const { exit } = useApp();
26
+ const { theme, setTheme } = useTheme();
27
+ const { stdout } = useStdout();
28
+ const [outputLines, setOutputLines] = useState([]);
29
+ const [isFirstCommand, setIsFirstCommand] = useState(true);
30
+ // Header state
31
+ const [network, setNetwork] = useState("Unknown");
32
+ const [account, setAccount] = useState();
33
+ const [ethBalance, setEthBalance] = useState("0.00 ETH");
34
+ const [usdcBalance, setUsdcBalance] = useState("$0.00");
35
+ // Initialize network and account info
36
+ useEffect(() => {
37
+ const networkName = client.getNetwork();
38
+ setNetwork(networkName);
39
+ const privateKey = config.getPrivateKey();
40
+ if (privateKey) {
41
+ const accountInfo = privateKeyToAccount(privateKey);
42
+ setAccount(accountInfo.address);
43
+ // Fetch balances in background
44
+ (async () => {
183
45
  try {
184
- const privateKey = config.getAccountPrivateKey(activeIndex);
185
- const balance = await checkBalance(privateKey, true); // silent mode
186
- accountInfo = {
187
- account,
188
- balance,
189
- };
46
+ const { checkBalance } = await import("../commands/balances.js");
47
+ const balanceInfo = await checkBalance(privateKey, true);
48
+ setEthBalance(`${parseFloat(balanceInfo.ethFormatted).toFixed(4)} ETH`);
49
+ setUsdcBalance(`$${parseFloat(balanceInfo.usdcFormatted).toFixed(2)}`);
190
50
  }
191
51
  catch (error) {
192
- // If balance check fails, just show account info without balance
193
- accountInfo = { account };
52
+ // Silently fail
194
53
  }
195
- }
196
- }
197
- catch (error) {
198
- // If account info fails, continue without it
54
+ })();
199
55
  }
200
- // Compact account info - combined on fewer lines
201
- if (accountInfo) {
202
- const { account, balance } = accountInfo;
203
- const accountType = account.type === "custom" ? "Custom" : "Seed-Derived";
204
- const accountLabel = account.label ? ` (${account.label})` : "";
205
- if (balance) {
206
- const ethDisplay = balance.ethFormatted || balance.ethBalance || "0 ETH";
207
- const usdcDisplay = balance.usdcFormatted || balance.usdcBalance || "$0.00";
208
- // Combine account and balance on one line to save space
209
- welcomeLines.push(`{${cyanColor}}👤 Account #{green-fg}${account.index}{/green-fg} | {green-fg}${accountType}${accountLabel}{/green-fg} | 💰 {yellow-fg}${ethDisplay}{/yellow-fg} | {green-fg}${usdcDisplay}{/green-fg}{/${cyanColor}}`);
56
+ // Check for maintenance mode on initialization (with timeout to prevent hanging)
57
+ (async () => {
58
+ try {
59
+ // Add timeout to prevent hanging if API is slow
60
+ const timeoutPromise = new Promise((resolve) => {
61
+ setTimeout(() => resolve(false), 2000); // 2 second timeout
62
+ });
63
+ const checkPromise = checkMaintenanceMode(client);
64
+ const isMaintenance = await Promise.race([checkPromise, timeoutPromise]);
65
+ if (isMaintenance) {
66
+ log(chalk.yellow("⚠️ API is in maintenance mode"));
67
+ log(chalk.dim(" Most commands will be unavailable. Check status with: health"));
68
+ log("");
69
+ }
210
70
  }
211
- else {
212
- welcomeLines.push(`{${cyanColor}}👤 Account #{green-fg}${account.index}{/green-fg} | {green-fg}${accountType}${accountLabel}{/green-fg}{/${cyanColor}}`);
71
+ catch (error) {
72
+ // Silently fail - don't block shell startup
213
73
  }
214
- }
215
- return welcomeLines.join("\n");
216
- };
217
- // Set initial header content
218
- buildWelcomeContent(currentTheme, catFaces[currentCatIndex].face).then((content) => {
219
- headerBox.setContent(content);
220
- screen.render();
221
- });
222
- // Start cat face animation (cycle through every minute)
223
- const startCatAnimation = () => {
224
- if (catAnimationInterval) {
225
- clearInterval(catAnimationInterval);
226
- }
227
- catAnimationInterval = setInterval(() => {
228
- currentCatIndex = (currentCatIndex + 1) % catFaces.length;
229
- buildWelcomeContent(currentTheme, catFaces[currentCatIndex].face).then((content) => {
230
- headerBox.setContent(content);
231
- screen.render();
232
- });
233
- }, 60000); // Change every minute (60 seconds)
234
- };
235
- // Start animation
236
- startCatAnimation();
237
- // Create output log box (scrollable) with thick borders, transparent background
238
- const outputBox = blessed.log({
239
- top: 6, // Adjusted to match new header height
240
- left: 0,
241
- width: "100%",
242
- bottom: 4, // Leave space for input box at bottom
243
- tags: true,
244
- scrollable: true,
245
- alwaysScroll: true,
246
- scrollbar: {
247
- ch: " ",
248
- inverse: currentTheme !== "dark",
249
- },
250
- style: {
251
- fg: themeColors.fg,
252
- bg: "default", // Transparent
253
- border: {
254
- fg: themeColors.border,
255
- bold: true,
256
- },
257
- },
258
- padding: {
259
- left: 0,
260
- right: 1,
261
- },
262
- mouse: true, // Enable mouse scrolling
263
- border: {
264
- type: "line",
265
- fg: themeColors.border,
266
- ch: "═", // Double line for thicker border
267
- },
268
- });
269
- // Create prompt label with bold font (appears larger) - positioned inside input box
270
- const promptLabel = blessed.text({
271
- bottom: 1,
272
- left: 2,
273
- width: 8, // Exactly "httpcat>" (8 characters)
274
- height: 1,
275
- content: "",
276
- tags: true,
277
- style: {
278
- fg: themeColors.fg,
279
- bg: "default", // Transparent
280
- bold: true,
281
- },
282
- });
283
- // Helper to update prompt label content
284
- const updatePromptLabel = (theme) => {
285
- const colorTag = theme === "dark" ? "green-fg" : "black-fg";
286
- promptLabel.content = `{${colorTag}}{bold}httpcat>{/bold}{/${colorTag}}`;
287
- };
288
- updatePromptLabel(currentTheme);
289
- // Create input box with visible cursor and stylish border
290
- const inputBox = blessed.textbox({
291
- bottom: 0,
292
- left: 0,
293
- width: "100%",
294
- height: 3,
295
- inputOnFocus: true,
296
- keys: true,
297
- vi: false, // Disabled to prevent double input issues in agent mode
298
- secret: false,
299
- tags: true,
300
- alwaysScroll: false,
301
- scrollable: false,
302
- padding: {
303
- left: 10, // Space for "httpcat>" prompt (8 chars) + 2 for spacing
304
- right: 1,
305
- top: 0,
306
- bottom: 0,
307
- },
308
- cursor: {
309
- artificial: true,
310
- shape: "block", // Block cursor is more visible than line
311
- blink: true,
312
- color: currentTheme === "dark" ? "green" : "black",
313
- },
314
- style: {
315
- fg: themeColors.fg,
316
- bg: "default",
317
- border: {
318
- fg: themeColors.border,
319
- bold: true,
320
- },
321
- focus: {
322
- fg: themeColors.fg,
323
- bg: "default",
324
- border: {
325
- fg: themeColors.border,
326
- bold: true,
327
- },
328
- },
329
- },
330
- border: {
331
- type: "line",
332
- fg: themeColors.border,
333
- ch: "─", // Single line border
334
- },
335
- });
336
- // Helper to update theme
337
- const updateTheme = (newTheme) => {
338
- currentTheme = newTheme;
339
- themeColors = getThemeColors(currentTheme);
340
- // Update screen cursor color
341
- screen.cursor.color =
342
- currentTheme === "dark" ? "green" : "black";
343
- // Update header content with new theme colors (keep current cat face)
344
- buildWelcomeContent(currentTheme, catFaces[currentCatIndex].face).then((content) => {
345
- headerBox.setContent(content);
346
- screen.render();
347
- });
348
- // Update all widget styles
349
- headerBox.style.fg = themeColors.fg;
350
- headerBox.style.bg = "default"; // Transparent
351
- headerBox.border = { type: "line", fg: themeColors.border, ch: "═" };
352
- outputBox.style.fg = themeColors.fg;
353
- outputBox.style.bg = "default"; // Transparent
354
- outputBox.scrollbar.inverse = currentTheme !== "dark";
355
- outputBox.border = { type: "line", fg: themeColors.border, ch: "═" };
356
- updatePromptLabel(currentTheme);
357
- promptLabel.style.fg = themeColors.fg;
358
- promptLabel.style.bg = "default"; // Transparent
359
- promptLabel.style.bold = true;
360
- // Update input box cursor, style, and border
361
- inputBox.style.fg = themeColors.fg;
362
- inputBox.style.bg = "default";
363
- inputBox.style.focus.fg = themeColors.fg;
364
- inputBox.style.focus.bg = "default";
365
- inputBox.style.border.fg = themeColors.border;
366
- inputBox.style.focus.border.fg = themeColors.border;
367
- inputBox.border = { type: "line", fg: themeColors.border, ch: "─" };
368
- if (inputBox.cursor) {
369
- inputBox.cursor.color =
370
- currentTheme === "dark" ? "green" : "black";
371
- }
372
- screen.cursor.color =
373
- currentTheme === "dark" ? "green" : "black";
374
- screen.render();
375
- };
376
- // Helper to log output (define before use)
74
+ })();
75
+ }, [client]);
76
+ // Add a line to output
377
77
  const log = (text) => {
378
- outputBox.log(text);
379
- outputBox.setScrollPerc(100);
380
- screen.render();
78
+ setOutputLines((prev) => [...prev, text]);
381
79
  };
382
- // Helper to log multiple lines
383
80
  const logLines = (lines) => {
384
- lines.forEach((line) => outputBox.log(line));
385
- outputBox.setScrollPerc(100);
386
- screen.render();
81
+ setOutputLines((prev) => [...prev, ...lines]);
387
82
  };
388
- // Helper to log multiple lines with smooth scrolling (for tables/lists)
389
- const logLinesSmooth = async (lines, scrollToTop = false) => {
390
- // Disable auto-scroll temporarily to batch output (prevents rapid scrolling)
391
- const originalAlwaysScroll = outputBox.alwaysScroll;
392
- outputBox.alwaysScroll = false;
393
- // Get line count before adding new lines
394
- const linesBefore = outputBox.lines?.length || 0;
395
- // Add all lines at once (no rendering between lines = smooth, no rapid scrolling)
396
- lines.forEach((line) => outputBox.log(line));
397
- // Calculate scroll position after adding lines
398
- const linesAfter = outputBox.lines?.length || 0;
399
- const visibleHeight = outputBox.height || 20;
400
- if (scrollToTop && linesAfter > visibleHeight) {
401
- // Scroll to show the beginning of the new content
402
- // Calculate which line the new content starts at
403
- const newContentStartLine = linesBefore;
404
- // Calculate percentage to show that line near the top
405
- // We want to show the new content starting from the top of visible area
406
- const totalScrollable = Math.max(1, linesAfter - visibleHeight);
407
- const scrollPerc = Math.max(0, Math.min(100, (newContentStartLine / totalScrollable) * 100));
408
- try {
409
- outputBox.setScrollPerc(scrollPerc);
410
- }
411
- catch (e) {
412
- // Fallback if method doesn't exist
413
- outputBox.setScrollPerc(0);
414
- }
83
+ // Helper to redirect console.log output to shell output during command execution
84
+ const withConsoleRedirect = async (fn) => {
85
+ const originalLog = console.log;
86
+ const redirectOutput = (...args) => {
87
+ const message = args
88
+ .map((arg) => (typeof arg === "string" ? arg : JSON.stringify(arg)))
89
+ .join(" ");
90
+ // Split multi-line output into separate log entries
91
+ const lines = message.split("\n");
92
+ lines.forEach((line) => {
93
+ if (line) {
94
+ log(line);
95
+ }
96
+ });
97
+ };
98
+ console.log = redirectOutput;
99
+ try {
100
+ return await fn();
415
101
  }
416
- else {
417
- // Scroll to bottom
418
- outputBox.setScrollPerc(100);
102
+ finally {
103
+ console.log = originalLog;
419
104
  }
420
- // Re-enable auto-scroll
421
- outputBox.alwaysScroll = originalAlwaysScroll;
422
- screen.render();
423
105
  };
424
- // Wrap toggleTheme to also log
425
- const toggleThemeWithLog = () => {
426
- const themes = ["win95", "dark", "light"];
427
- const currentIndex = themes.indexOf(currentTheme);
428
- const nextTheme = themes[(currentIndex + 1) % themes.length];
429
- updateTheme(nextTheme);
430
- log(chalk.blue(`Theme switched to: ${nextTheme}`));
431
- };
432
- // Append all widgets in correct z-order (last appended is on top)
433
- screen.append(headerBox);
434
- screen.append(outputBox);
435
- screen.append(inputBox);
436
- screen.append(promptLabel); // Prompt label on top so it's always visible
437
- // Handle F1 for theme toggle
438
- screen.key(["f1"], () => {
439
- toggleThemeWithLog();
440
- });
441
- // Store toggle function for command handler
442
- screen.toggleTheme = toggleThemeWithLog;
443
- screen.updateTheme = updateTheme;
444
- // Command history
445
- const commandHistory = [];
446
- let historyIndex = -1; // -1 means not navigating history (showing current input)
447
- let currentInputBeforeHistory = ""; // Store current input when starting to navigate history
448
- // Handle up arrow - navigate to previous command in history
449
- inputBox.key(["up"], () => {
450
- if (commandHistory.length === 0) {
451
- return;
452
- }
453
- // If we're not currently navigating history, save the current input
454
- if (historyIndex === -1) {
455
- currentInputBeforeHistory = inputBox.getValue();
456
- historyIndex = commandHistory.length - 1; // Start at the most recent command
457
- }
458
- else {
459
- // Move to previous command (earlier in history)
460
- if (historyIndex > 0) {
461
- historyIndex--;
462
- }
463
- }
464
- // Set the input to the command at current history index
465
- inputBox.setValue(commandHistory[historyIndex]);
466
- screen.render();
467
- });
468
- // Handle down arrow - navigate to next command in history (or back to current input)
469
- inputBox.key(["down"], () => {
470
- if (commandHistory.length === 0 || historyIndex === -1) {
471
- return;
472
- }
473
- // Move to next command (more recent in history)
474
- if (historyIndex < commandHistory.length - 1) {
475
- historyIndex++;
476
- inputBox.setValue(commandHistory[historyIndex]);
477
- }
478
- else {
479
- // We're at the most recent command, go back to the input that was there before
480
- historyIndex = -1;
481
- inputBox.setValue(currentInputBeforeHistory);
482
- currentInputBeforeHistory = "";
483
- }
484
- screen.render();
485
- });
486
- // Handle left arrow - move cursor left within input
487
- // Only intercept if we're not navigating history (blessed handles cursor movement automatically with vi: true)
488
- inputBox.key(["left"], () => {
489
- // Reset history navigation when user starts editing with arrow keys
490
- if (historyIndex !== -1) {
491
- historyIndex = -1;
492
- currentInputBeforeHistory = "";
493
- }
494
- // Blessed will handle cursor movement automatically with vi: true
495
- screen.render();
496
- });
497
- // Handle right arrow - move cursor right within input
498
- inputBox.key(["right"], () => {
499
- // Reset history navigation when user starts editing with arrow keys
500
- if (historyIndex !== -1) {
501
- historyIndex = -1;
502
- currentInputBeforeHistory = "";
503
- }
504
- // Blessed will handle cursor movement automatically with vi: true
505
- screen.render();
506
- });
507
- // Handle Home key - move cursor to beginning of line
508
- inputBox.key(["home"], () => {
509
- if (historyIndex === -1) {
510
- // Blessed handles this automatically with vi: true
511
- screen.render();
512
- }
513
- });
514
- // Handle End key - move cursor to end of line
515
- inputBox.key(["end"], () => {
516
- if (historyIndex === -1) {
517
- // Blessed handles this automatically with vi: true
518
- screen.render();
106
+ // Welcome message
107
+ useEffect(() => {
108
+ log(chalk.green("╔════════════════════════════════════════════╗"));
109
+ log(chalk.green("║ httpcat Interactive Shell v0.3.0 ║"));
110
+ log(chalk.green("╚════════════════════════════════════════════╝"));
111
+ log("");
112
+ log(chalk.cyan("Welcome to the httpcat CLI!"));
113
+ log("Type " + chalk.yellow("help") + " for available commands");
114
+ log("Press " + chalk.yellow("F1") + " to toggle theme");
115
+ log("Press " + chalk.yellow("Ctrl+C") + " to exit");
116
+ log("");
117
+ }, []);
118
+ // Handle theme toggle and exit
119
+ useInput((input, key) => {
120
+ // F1 key for theme toggle (check via input)
121
+ if (input === "\x1bOP" || input === "q") {
122
+ // F1 sends ESC[OP sequence, or use 'q' as alternative
123
+ const themes = ["dark", "light", "win95"];
124
+ const currentIndex = themes.indexOf(theme);
125
+ const nextTheme = themes[(currentIndex + 1) % themes.length];
126
+ setTheme(nextTheme);
127
+ log(chalk.cyan(`Theme changed to: ${nextTheme}`));
128
+ }
129
+ else if (key.ctrl && input === "c") {
130
+ exit();
519
131
  }
520
132
  });
521
- // Handle input submission
522
- inputBox.on("submit", async (value) => {
523
- const trimmed = value.trim();
524
- inputBox.clearValue();
525
- screen.render(); // Clear input immediately
526
- // Reset history navigation
527
- historyIndex = -1;
528
- currentInputBeforeHistory = "";
529
- if (!trimmed) {
530
- inputBox.focus();
531
- screen.render();
532
- return;
533
- }
534
- // Add to history (avoid duplicates - don't add if same as last command)
535
- if (commandHistory.length === 0 ||
536
- commandHistory[commandHistory.length - 1] !== trimmed) {
537
- commandHistory.push(trimmed);
538
- // Limit history size to prevent memory issues (keep last 100 commands)
539
- if (commandHistory.length > 100) {
540
- commandHistory.shift();
541
- }
542
- }
543
- // Log the command with prompt
544
- log(`{green-fg}httpcat>{/green-fg} ${trimmed}`);
545
- const [command, ...args] = trimmed.split(/\s+/);
133
+ // Handle command execution
134
+ const handleCommand = async (command) => {
135
+ // Clear welcome text on first command
136
+ if (isFirstCommand) {
137
+ setOutputLines([]);
138
+ setIsFirstCommand(false);
139
+ }
140
+ log(chalk.gray(`> ${command}`));
141
+ let parts = command.trim().split(/\s+/);
142
+ // Strip "httpcat" prefix if user includes it
143
+ if (parts[0].toLowerCase() === "httpcat" && parts.length > 1) {
144
+ parts = parts.slice(1);
145
+ }
146
+ const cmd = parts[0].toLowerCase();
147
+ const args = parts.slice(1);
546
148
  try {
547
- await handleCommand(client, command.toLowerCase(), args, log, logLines, logLinesSmooth, screen, inputBox, currentTheme, buildWelcomeContent, headerBox, catFaces, currentCatIndex);
548
- }
549
- catch (error) {
550
- log(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
551
- }
552
- log(""); // Empty line after command output
553
- inputBox.focus();
554
- screen.render();
555
- });
556
- // Handle escape to clear input
557
- inputBox.key(["escape"], () => {
558
- inputBox.clearValue();
559
- screen.render();
560
- });
561
- // Handle Ctrl+C - two-stage: first clears input if text exists, second quits
562
- const handleCtrlC = () => {
563
- const currentValue = inputBox.getValue();
564
- // If there's text in the input, clear it instead of quitting
565
- if (currentValue && currentValue.trim().length > 0) {
566
- inputBox.clearValue();
567
- screen.render();
568
- return;
569
- }
570
- // No text in input, so quit
571
- if (catAnimationInterval) {
572
- clearInterval(catAnimationInterval);
573
- catAnimationInterval = null;
574
- }
575
- screen.destroy();
576
- printCat("sleeping");
577
- console.log(chalk.cyan("Goodbye! 👋"));
578
- process.exit(0);
579
- };
580
- // Handle Ctrl+C on screen level
581
- screen.key(["C-c"], handleCtrlC);
582
- // Handle Ctrl+C on input box level
583
- inputBox.key(["C-c"], handleCtrlC);
584
- // Focus input and render
585
- inputBox.focus();
586
- screen.render();
587
- // Show welcome message with key commands on load
588
- displayWelcomeMessage(log, logLines, outputBox, screen, currentTheme);
589
- // Auto-enter chat mode if token identifier provided
590
- if (autoChatToken !== undefined) {
591
- // Store original submit handler
592
- const originalHandlers = inputBox.listeners("submit");
593
- const originalSubmitHandler = originalHandlers[0];
594
- // Remove original handler temporarily
595
- inputBox.removeAllListeners("submit");
596
- // Enter chat mode automatically
597
- await startChatInShell(client, autoChatToken, log, logLines, screen, inputBox, originalSubmitHandler, headerBox, buildWelcomeContent, currentTheme, catFaces, currentCatIndex);
598
- }
599
- }
600
- async function handleCommand(client, command, args, log, logLines, logLinesSmooth, screen, inputBox, currentTheme, buildWelcomeContent, headerBox, catFaces, currentCatIndex) {
601
- switch (command) {
602
- case "help":
603
- // Get outputBox from screen children (it's the second child after headerBox)
604
- const outputBox = screen.children.find((child) => child.type === "log");
605
- displayHelp(log, logLines, outputBox, screen, currentTheme);
606
- break;
607
- case "create": {
608
- if (args.length < 2) {
609
- log(chalk.red("Usage: create <name> <symbol> [--photo URL|path] [--website URL]"));
610
- return;
611
- }
612
- const [name, symbol] = args;
613
- let photoUrl = extractFlag(args, "--photo");
614
- const websiteUrl = extractFlag(args, "--website");
615
- // Process photo if it's a file path (show loading state)
616
- if (photoUrl && isFilePath(photoUrl)) {
617
- log(chalk.blue("Uploading image..."));
618
- screen.render();
619
- try {
620
- photoUrl = processPhotoUrl(photoUrl);
621
- log(chalk.green("✓ Image uploaded"));
622
- screen.render();
623
- }
624
- catch (error) {
625
- log(chalk.red(`Failed to upload image: ${error instanceof Error ? error.message : String(error)}`));
626
- screen.render();
627
- return;
628
- }
629
- }
630
- log(chalk.blue("Creating token..."));
631
- screen.render();
632
- const result = await createToken(client, {
633
- name,
634
- symbol,
635
- photoUrl,
636
- websiteUrl,
637
- });
638
- displayCreateResultToLog(result, log, logLines);
639
- break;
640
- }
641
- case "buy": {
642
- if (args.length < 2) {
643
- log(chalk.red("Usage: buy <address|name|symbol> <amount> [--repeat <count>] [--delay <ms>]"));
644
- log(chalk.dim(" amount: 0.05, 0.10, or 0.20 (test mode) or 50, 100, 200 (production)"));
645
- log(chalk.dim(' Examples: buy 0x1234... 0.20, buy "My Token" 0.10, buy MTK 0.05 --repeat 10'));
646
- return;
647
- }
648
- const [identifier, amountInput] = args;
649
- const network = client.getNetwork();
650
- const isTestMode = network === "eip155:84532" || network === "eip155:11155111" || network.includes("sepolia");
651
- const validAmounts = isTestMode ? TEST_AMOUNTS : PROD_AMOUNTS;
652
- // Parse flags
653
- const repeatCount = extractFlag(args, "--repeat")
654
- ? parseInt(extractFlag(args, "--repeat") || "1", 10)
655
- : undefined;
656
- const delayMs = extractFlag(args, "--delay")
657
- ? parseInt(extractFlag(args, "--delay") || "0", 10)
658
- : 0;
659
- // Try to validate and normalize the amount
660
- let amount;
661
- try {
662
- amount = validateAmount(amountInput, validAmounts, isTestMode);
663
- }
664
- catch (error) {
665
- // Validation failed - show error and return
666
- log(chalk.yellow(`Invalid amount: ${amountInput}`));
667
- log(chalk.dim(`Valid amounts: ${validAmounts.join(", ")}`));
668
- return;
669
- }
670
- const privateKey = config.getPrivateKey();
671
- // Handle repeat mode
672
- if (repeatCount && repeatCount > 0) {
673
- const results = [];
674
- let totalSpent = 0;
675
- let stoppedEarly = false;
676
- let stopReason = "";
677
- for (let i = 1; i <= repeatCount; i++) {
678
- try {
679
- // Retry logic: try up to 10 times with exponential backoff on 402 errors
680
- // Client is recreated on each attempt to ensure fresh signature for new nonce
681
- let result = null;
682
- let lastError = null;
683
- const maxRetries = 10;
684
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
685
- try {
686
- if (attempt === 1) {
687
- log(chalk.blue(`Buy ${i}/${repeatCount}...`));
688
- }
689
- else {
690
- log(chalk.yellow(` Retrying buy ${i}/${repeatCount} (attempt ${attempt}/${maxRetries})...`));
691
- }
692
- screen.render();
693
- // Recreate client inside the operation to ensure fresh signature
694
- // for each attempt (including retries). This fixes the issue where x402-fetch
695
- // generates a new nonce but reuses the same signature, causing subsequent buys to fail
696
- result = await (async () => {
697
- // Recreate client for each attempt to ensure fresh signature for new nonce
698
- const client = await HttpcatClient.create(privateKey);
699
- return buyToken(client, identifier, amount, isTestMode, true, // silent=true to avoid console.log interference
700
- privateKey);
701
- })();
702
- // Success! Break out of retry loop
703
- break;
704
- }
705
- catch (error) {
706
- lastError = error;
707
- // Only retry on 402 errors (payment required)
708
- const is402Error = error?.message?.includes("402") ||
709
- error?.status === 402 ||
710
- (typeof error === "string" && error.includes("402"));
711
- if (is402Error && attempt < maxRetries) {
712
- // Exponential backoff: 1s, 2s, 4s, 8s, 16s, 32s, 64s, 128s, 256s, 512s (capped at 10s)
713
- const backoffMs = Math.min(Math.pow(2, attempt - 1) * 1000, 10000);
714
- log(chalk.yellow(` ⚠️ Payment required (attempt ${attempt}/${maxRetries}), retrying in ${backoffMs / 1000}s...`));
715
- screen.render();
716
- await new Promise((resolve) => setTimeout(resolve, backoffMs));
717
- continue; // Retry
718
- }
719
- else {
720
- // Not a 402 error, or max retries reached - throw the error
721
- throw error;
722
- }
723
- }
149
+ switch (cmd) {
150
+ case "help":
151
+ logLines([
152
+ "",
153
+ chalk.bold.cyan("Available Commands:"),
154
+ "",
155
+ chalk.bold.yellow("Token Operations:"),
156
+ chalk.yellow(" buy <token> <amt>") + " - Buy tokens",
157
+ chalk.yellow(" sell <token> <amt>") + " - Sell tokens",
158
+ chalk.yellow(" swap <in> <out> <amt>") + " - Swap tokens",
159
+ chalk.yellow(" create") +
160
+ " - Create a new token (use CLI)",
161
+ "",
162
+ chalk.bold.yellow("Token Information:"),
163
+ chalk.yellow(" info <token>") + " - Get token information",
164
+ chalk.yellow(" list") + " - List all tokens",
165
+ "",
166
+ chalk.bold.yellow("Portfolio:"),
167
+ chalk.yellow(" positions") + " - View your positions",
168
+ chalk.yellow(" balances") + " - Check wallet balances",
169
+ chalk.yellow(" transactions") +
170
+ " - View recent transactions",
171
+ "",
172
+ chalk.bold.yellow("Perps (Perpetual Futures):"),
173
+ chalk.yellow(" perps markets") +
174
+ " - Browse available markets (use CLI)",
175
+ chalk.yellow(" perps onboard") +
176
+ " - One-time venue setup (use CLI)",
177
+ chalk.yellow(" perps deposit") +
178
+ " - Deposit USDC to venues (use CLI)",
179
+ chalk.yellow(" perps trade") +
180
+ " - Open/close positions (use CLI)",
181
+ chalk.yellow(" perps positions") +
182
+ " - View open positions (use CLI)",
183
+ "",
184
+ chalk.bold.yellow("Predictions (Betting Markets):"),
185
+ chalk.yellow(" predictions markets") +
186
+ " - Browse markets (use CLI)",
187
+ chalk.yellow(" predictions search") +
188
+ " - Search markets (use CLI)",
189
+ chalk.yellow(" predictions bet") + " - Place a bet (use CLI)",
190
+ chalk.yellow(" predictions detect-arbs") +
191
+ " - Find arbitrage (use CLI)",
192
+ chalk.yellow(" predictions detect-insiders") +
193
+ " - Find traders (use CLI)",
194
+ "",
195
+ chalk.bold.yellow("Social & AI:"),
196
+ chalk.yellow(" chat") + " - Join general chat",
197
+ chalk.yellow(" chat <token>") + " - Join token chat",
198
+ "",
199
+ chalk.bold.yellow("Signing:"),
200
+ chalk.yellow(" sign") +
201
+ " - Enter interactive signing mode",
202
+ chalk.yellow(" sign message <text>") + " - Sign a message",
203
+ chalk.yellow(" sign file <path>") + " - Sign a file",
204
+ "",
205
+ chalk.bold.yellow("System:"),
206
+ chalk.yellow(" account") + " - View account info",
207
+ chalk.yellow(" health") + " - Check API health",
208
+ chalk.yellow(" clear") + " - Clear screen",
209
+ chalk.yellow(" exit") + " - Exit shell",
210
+ chalk.yellow(" help") + " - Show this help",
211
+ "",
212
+ chalk.bold.cyan("Keyboard Shortcuts:"),
213
+ "",
214
+ chalk.yellow(" /") + " + type - Autocomplete commands",
215
+ chalk.yellow(" Tab / Enter") + " - Select autocomplete",
216
+ chalk.yellow(" F1") + " - Toggle theme",
217
+ chalk.yellow(" Ctrl+C") + " - Exit",
218
+ chalk.yellow(" ↑/↓") +
219
+ " - Navigate command history",
220
+ "",
221
+ ]);
222
+ break;
223
+ case "clear":
224
+ setOutputLines([]);
225
+ break;
226
+ case "exit":
227
+ case "quit":
228
+ exit();
229
+ break;
230
+ case "sign":
231
+ {
232
+ // Handle sign command - delegate to sign command's interactive mode
233
+ log(chalk.cyan("Entering interactive signing mode..."));
234
+ const { createSignCommand } = await import("../commands/sign.js");
235
+ const signCmd = createSignCommand();
236
+ // Execute sign command without subcommand to trigger interactive mode
237
+ const signArgs = args.length > 0 ? ["sign", ...args] : ["sign"];
238
+ // Note: This will trigger the interactive mode defined in sign.ts
239
+ log(chalk.yellow("Note: Use 'httpcat sign' in CLI for full signing features"));
240
+ log(chalk.gray("Available: sign message <text>, sign file <path>"));
241
+ if (args.length >= 2 && args[0] === "message") {
242
+ // Simple message signing
243
+ const message = args.slice(1).join(" ");
244
+ log(chalk.cyan(`Signing message: "${message}"...`));
245
+ // Would need to import and call sign functions directly
246
+ log(chalk.yellow('Use CLI: httpcat sign message "' + message + '"'));
247
+ }
248
+ else if (args.length >= 2 && args[0] === "file") {
249
+ const filePath = args[1];
250
+ log(chalk.cyan(`Signing file: ${filePath}...`));
251
+ log(chalk.yellow('Use CLI: httpcat sign file "' + filePath + '"'));
724
252
  }
725
- // If we exhausted retries, throw the last error
726
- if (!result) {
727
- if (lastError) {
728
- throw lastError;
729
- }
730
- else {
731
- throw new Error(`Failed to complete buy ${i}/${repeatCount} after ${maxRetries} attempts`);
732
- }
253
+ }
254
+ break;
255
+ case "health":
256
+ // Import and execute health command
257
+ log(chalk.cyan("Checking API health..."));
258
+ const { checkHealth, displayHealthStatus } = await import("../commands/health.js");
259
+ const healthData = await checkHealth(client);
260
+ await withConsoleRedirect(() => displayHealthStatus(healthData));
261
+ break;
262
+ case "balances":
263
+ {
264
+ log(chalk.cyan("Fetching balances..."));
265
+ const { checkBalance } = await import("../commands/balances.js");
266
+ const privateKey = config.getPrivateKey();
267
+ if (privateKey) {
268
+ await withConsoleRedirect(() => checkBalance(privateKey));
269
+ }
270
+ else {
271
+ log(chalk.red("Error: Private key not configured"));
733
272
  }
734
- results.push(result);
735
- totalSpent += parseFloat(result.amountSpent);
736
- // Display compact result
737
- displayBuyResultToLog(result, log, logLines);
738
- // Check if token graduated
739
- if (result.graduationReached) {
740
- stoppedEarly = true;
741
- stopReason = "Token graduated";
742
- log(chalk.green("🎓 Token has graduated! Stopping buy loop."));
273
+ }
274
+ break;
275
+ case "list":
276
+ {
277
+ log(chalk.cyan("Fetching token list..."));
278
+ const { listTokens, displayTokenList } = await import("../commands/list.js");
279
+ const result = await listTokens(client, 1, 20, "mcap");
280
+ await withConsoleRedirect(() => displayTokenList(result, true));
281
+ }
282
+ break;
283
+ case "info":
284
+ if (args.length === 0) {
285
+ log(chalk.red("Error: Please provide a token address or symbol"));
286
+ log(chalk.gray("Usage: info <token>"));
287
+ break;
288
+ }
289
+ log(chalk.cyan(`Fetching info for ${args[0]}...`));
290
+ const { getTokenInfo, displayTokenInfo } = await import("../commands/info.js");
291
+ const info = await getTokenInfo(client, args[0], undefined, true); // silent=true
292
+ await withConsoleRedirect(() => displayTokenInfo(info, undefined, undefined, true, client));
293
+ break;
294
+ case "account":
295
+ log(chalk.cyan("Account information:"));
296
+ const { getAccountInfo, displayAccountInfo } = await import("../commands/account.js");
297
+ const accountData = await getAccountInfo();
298
+ await withConsoleRedirect(() => displayAccountInfo(accountData));
299
+ break;
300
+ case "buy":
301
+ {
302
+ if (args.length < 2) {
303
+ log(chalk.red("Error: Please provide token and amount"));
304
+ log(chalk.gray("Usage: buy <token> <amount>"));
743
305
  break;
744
306
  }
745
- // Wait for transaction confirmations if present
746
- // This ensures both the buy transaction and payment transaction are confirmed
747
- // before the next buy, preventing nonce/signature conflicts
748
- if (i < repeatCount) {
749
- const { createPublicClient, http } = await import("viem");
750
- const { baseSepolia } = await import("viem/chains");
751
- const publicClient = createPublicClient({
752
- chain: baseSepolia,
753
- transport: http(config.getRpcUrl()),
754
- });
755
- // Wait for payment transaction first (if present)
756
- // This is critical to ensure the payment nonce is consumed before next request
757
- if (result.paymentTxHash) {
758
- log(chalk.dim(` Waiting for payment transaction confirmation...`));
759
- screen.render();
760
- try {
761
- await publicClient.waitForTransactionReceipt({
762
- hash: result.paymentTxHash,
763
- });
764
- log(chalk.dim(` ✅ Payment transaction confirmed`));
765
- screen.render();
766
- }
767
- catch (txError) {
768
- log(chalk.yellow(` ⚠️ Could not confirm payment transaction, proceeding...`));
769
- screen.render();
770
- }
771
- }
772
- // Wait for buy transaction (if present)
773
- if (result.txHash) {
774
- log(chalk.dim(` Waiting for buy transaction confirmation...`));
775
- screen.render();
776
- try {
777
- await publicClient.waitForTransactionReceipt({
778
- hash: result.txHash,
779
- });
780
- log(chalk.dim(` ✅ Buy transaction confirmed`));
781
- screen.render();
782
- }
783
- catch (txError) {
784
- log(chalk.yellow(` ⚠️ Could not confirm buy transaction, proceeding...`));
785
- screen.render();
786
- }
787
- }
307
+ log(chalk.cyan(`Buying ${args[1]} USDC of ${args[0]}...`));
308
+ const { buyToken, displayBuyResult } = await import("../commands/buy.js");
309
+ const privateKey = config.getPrivateKey();
310
+ if (privateKey) {
311
+ // Create fresh client to ensure x402 payment headers are valid
312
+ const freshClient = await HttpcatClient.create(privateKey);
313
+ const network = freshClient.getNetwork();
314
+ const isTestMode = network === "eip155:84532" ||
315
+ network === "eip155:11155111" ||
316
+ network.includes("sepolia");
317
+ const result = await buyToken(freshClient, args[0], args[1], isTestMode, true, // silent=true to suppress status lines
318
+ privateKey);
319
+ await withConsoleRedirect(() => displayBuyResult(result, true));
788
320
  }
789
- // Apply delay between iterations (except after the last one)
790
- // For bonding curve buys, we need a minimum delay to allow backend
791
- // to process the transaction and update balance state
792
- if (i < repeatCount) {
793
- const MIN_DELAY_MS = 2000; // 2 seconds minimum for backend processing
794
- const totalDelay = Math.max(delayMs, MIN_DELAY_MS);
795
- if (totalDelay > 0) {
796
- if (totalDelay === MIN_DELAY_MS && delayMs === 0) {
797
- log(chalk.dim(` Waiting ${totalDelay / 1000}s for backend to process...`));
798
- screen.render();
799
- }
800
- await new Promise((resolve) => setTimeout(resolve, totalDelay));
801
- }
321
+ else {
322
+ log(chalk.red("Error: Private key not configured"));
802
323
  }
803
324
  }
804
- catch (error) {
805
- // Handle insufficient funds (402) gracefully
806
- if (error instanceof HttpcatError && error.status === 402) {
807
- stoppedEarly = true;
808
- stopReason = "Insufficient funds";
809
- log(chalk.yellow("💡 Insufficient funds. Stopping buy loop."));
810
- log("");
811
- // Show current balance to help diagnose the issue
812
- try {
813
- const balance = await checkBalance(privateKey, true);
814
- log(chalk.cyan("💰 Current Wallet Balances:"));
815
- log(chalk.dim(` ETH: ${balance.ethFormatted}`));
816
- log(chalk.dim(` USDC: ${balance.usdcFormatted}`));
817
- if (balance.cat402Formatted) {
818
- log(chalk.dim(` CAT: ${balance.cat402Formatted}`));
819
- }
820
- log("");
821
- // Provide guidance based on what might be insufficient
822
- const usdcBalance = parseFloat(balance.usdcFormatted.replace("$", "").replace(",", ""));
823
- const ethBalance = parseFloat(balance.ethFormatted.replace(" ETH", "").replace(",", ""));
824
- const buyAmount = parseFloat(amount);
825
- log(chalk.yellow("💡 Possible issues:"));
826
- if (usdcBalance < buyAmount) {
827
- log(chalk.dim(` • Insufficient USDC for purchase: Need $${buyAmount.toFixed(6)}, have $${usdcBalance.toFixed(6)}`));
828
- }
829
- if (usdcBalance < 0.01) {
830
- log(chalk.dim(` • Low USDC for API payments: x402 protocol requires USDC for API calls`));
831
- }
832
- if (ethBalance < 0.001) {
833
- log(chalk.dim(` • Low ETH for gas: Need ETH to pay for transaction fees`));
834
- }
835
- log("");
836
- log(chalk.dim(" Check your balance with: balances"));
837
- }
838
- catch (balanceError) {
839
- // If we can't fetch balance, just show generic message
840
- log(chalk.dim(" Check your balance with: balances"));
841
- }
325
+ break;
326
+ case "sell":
327
+ {
328
+ if (args.length < 2) {
329
+ log(chalk.red("Error: Please provide token and amount"));
330
+ log(chalk.gray("Usage: sell <token> <amount>"));
842
331
  break;
843
332
  }
844
- // For other errors, re-throw to be handled by outer catch
845
- throw error;
333
+ log(chalk.cyan(`Selling ${args[1]} tokens of ${args[0]}...`));
334
+ const { sellToken, displaySellResult } = await import("../commands/sell.js");
335
+ const privateKey = config.getPrivateKey();
336
+ if (privateKey) {
337
+ // Create fresh client to ensure x402 payment headers are valid
338
+ const freshClient = await HttpcatClient.create(privateKey);
339
+ const result = await sellToken(freshClient, args[0], args[1], true, // silent=true to suppress status lines
340
+ privateKey);
341
+ await withConsoleRedirect(() => displaySellResult(result, true));
342
+ }
343
+ else {
344
+ log(chalk.red("Error: Private key not configured"));
345
+ }
846
346
  }
847
- }
848
- // Show final summary
849
- if (results.length > 0) {
850
- const lastResult = results[results.length - 1];
851
- const graduationStatus = lastResult.graduationReached
852
- ? " GRADUATED!"
853
- : `${(lastResult.graduationProgress || 0).toFixed(2)}%`;
854
- log("");
855
- log(chalk.cyan.bold("📊 Repeat Buy Summary"));
856
- log(chalk.dim("─".repeat(50)));
857
- log(`Total buys completed: ${chalk.bold(results.length.toString())}${repeatCount ? `/${repeatCount}` : ""}`);
858
- log(`Total amount spent: ${chalk.bold(`$${totalSpent.toFixed(2)}`)}`);
859
- log(`Final token price: ${chalk.bold(lastResult.newPrice)}`);
860
- log(`Final graduation: ${chalk.bold(graduationStatus)}`);
861
- if (stoppedEarly) {
862
- log(`Stopped early: ${chalk.yellow(stopReason)}`);
347
+ break;
348
+ case "create":
349
+ {
350
+ if (args.length < 2) {
351
+ log(chalk.red("Usage: create <name> <symbol>"));
352
+ log(chalk.gray('Example: create "My Token" "MTK"'));
353
+ break;
354
+ }
355
+ const name = args[0];
356
+ const symbol = args[1];
357
+ log(chalk.cyan(`Creating token ${name} (${symbol})...`));
358
+ const { createToken, displayCreateResult, processPhotoUrl } = await import("../commands/create.js");
359
+ const { randomUUID } = await import("crypto");
360
+ // Create fresh client for each create command (to avoid x402 header expiry)
361
+ const privateKey = config.getPrivateKey();
362
+ if (!privateKey) {
363
+ log(chalk.red("Error: Private key not configured"));
364
+ break;
365
+ }
366
+ const createClient = await HttpcatClient.create(privateKey);
367
+ // Generate default robohash URL
368
+ const uuid = randomUUID();
369
+ const photoUrl = `https://robohash.org/${uuid}?set=set4`;
370
+ const result = await withConsoleRedirect(() => createToken(createClient, {
371
+ name: name.trim(),
372
+ symbol: symbol.trim(),
373
+ photoUrl,
374
+ }));
375
+ await withConsoleRedirect(() => displayCreateResult(result, true));
863
376
  }
864
- }
865
- }
866
- else {
867
- // Normal single buy execution
868
- log(chalk.blue("Buying tokens..."));
869
- screen.render();
870
- const result = await buyToken(client, identifier, amount, isTestMode, true, // silent=true to avoid console.log interference
871
- privateKey);
872
- displayBuyResultToLog(result, log, logLines);
873
- }
874
- break;
875
- }
876
- case "sell": {
877
- if (args.length < 2) {
878
- log(chalk.red("Usage: sell <address|name|symbol> <amount|percentage|all>"));
879
- log(chalk.dim(' Examples: sell 0x1234... 1000, sell "My Token" 50%, sell MTK all'));
880
- return;
881
- }
882
- const [identifier, amountInput] = args;
883
- // Get user address from private key for position checking
884
- const privateKey = config.getPrivateKey();
885
- const account = privateKeyToAccount(privateKey);
886
- const userAddress = account.address;
887
- log(chalk.blue("Checking token info..."));
888
- screen.render();
889
- const info = await getTokenInfo(client, identifier, userAddress, true); // silent=true to avoid console.log interference
890
- if (!info.userPosition || info.userPosition.tokensOwned === "0") {
891
- log(chalk.yellow("You do not own any of this token."));
892
- return;
893
- }
894
- const tokenAmount = parseTokenAmount(amountInput, info.userPosition.tokensOwned);
895
- log(chalk.blue("Selling tokens..."));
896
- screen.render();
897
- const result = await sellToken(client, identifier, tokenAmount, true, privateKey); // silent=true to avoid console.log interference
898
- displaySellResultToLog(result, log, logLines);
899
- break;
900
- }
901
- case "info": {
902
- if (args.length < 1) {
903
- log(chalk.red("Usage: info <address|name|symbol>"));
904
- log(chalk.dim(' Examples: info 0x1234..., info "My Token", info MTK'));
905
- return;
906
- }
907
- const [identifier] = args;
908
- // Get user address from private key for balance checking
909
- const privateKey = config.getPrivateKey();
910
- const account = privateKeyToAccount(privateKey);
911
- const userAddress = account.address;
912
- log(chalk.blue("Fetching token info..."));
913
- screen.render();
914
- const info = await getTokenInfo(client, identifier, userAddress, true); // silent=true to avoid console.log interference
915
- displayTokenInfoToLog(info, log, logLines);
916
- break;
917
- }
918
- case "list": {
919
- const page = parseInt(extractFlag(args, "--page") || "1");
920
- const limit = parseInt(extractFlag(args, "--limit") || "20");
921
- const sortBy = (extractFlag(args, "--sort") || "mcap");
922
- log(chalk.blue("Fetching token list..."));
923
- screen.render();
924
- const result = await listTokens(client, page, limit, sortBy);
925
- displayTokenListToLog(result, log, logLines, logLinesSmooth);
926
- break;
927
- }
928
- case "positions": {
929
- const privateKey = config.getPrivateKey();
930
- const account = privateKeyToAccount(privateKey);
931
- const userAddress = account.address;
932
- // Parse flags
933
- const activeOnly = args.includes("--active");
934
- const graduatedOnly = args.includes("--graduated");
935
- log(chalk.blue("Fetching positions..."));
936
- screen.render();
937
- const result = await getPositions(client, userAddress);
938
- // Filter positions if flags are set
939
- let filteredResult = result;
940
- if (activeOnly || graduatedOnly) {
941
- filteredResult = {
942
- ...result,
943
- positions: result.positions.filter((p) => activeOnly
944
- ? p.token.status !== "graduated"
945
- : p.token.status === "graduated"),
946
- };
947
- filteredResult.total = filteredResult.positions.length;
948
- }
949
- displayPositionsToLog(filteredResult, log, logLines);
950
- break;
951
- }
952
- case "health": {
953
- log(chalk.dim("Checking health..."));
954
- screen.render();
955
- const health = await checkHealth(client);
956
- displayHealthStatusToLog(health, log, logLines);
957
- break;
958
- }
959
- case "balances": {
960
- try {
961
- const privateKey = config.getPrivateKey();
962
- log(chalk.blue("Checking balance..."));
963
- screen.render();
964
- const balance = await checkBalance(privateKey, true); // silent mode
965
- displayBalanceToLog(balance, log, logLines);
966
- }
967
- catch (error) {
968
- log(chalk.blue("Checking balance..."));
969
- screen.render();
970
- const balance = await checkBalance(undefined, true); // silent mode
971
- displayBalanceToLog(balance, log, logLines);
972
- }
973
- break;
974
- }
975
- case "config": {
976
- if (args[0] === "--show") {
977
- displayConfigToLog(log, logLines);
978
- }
979
- else if (args[0] === "--set" && args[1]) {
980
- const [key, value] = args[1].split("=");
981
- config.set(key, value);
982
- log(chalk.green(`${key} set to ${value}`));
983
- }
984
- else if (args[0] === "--reset") {
985
- log(chalk.yellow("Config reset requires exiting the shell. Use 'httpcat config' from command line."));
986
- }
987
- else {
988
- log(chalk.red("Usage: config [--show|--set key=value|--reset]"));
989
- }
990
- break;
991
- }
992
- case "network": {
993
- log(chalk.yellow("Current network: ") + chalk.green(config.get("network")));
994
- log(chalk.blue("Note: Only base-sepolia (testnet) is currently supported"));
995
- break;
996
- }
997
- case "chat": {
998
- const tokenIdentifier = args.length > 0 && !args[0].startsWith("--") ? args[0] : undefined;
999
- log(chalk.yellow("💬 Entering chat mode..."));
1000
- log(chalk.dim("Type /exit to return to shell, or /help for chat commands"));
1001
- log("");
1002
- // Start chat mode within the shell
1003
- // Store original submit handler
1004
- const originalHandlers = inputBox.listeners("submit");
1005
- const originalSubmitHandler = originalHandlers[0];
1006
- // Remove original handler temporarily
1007
- inputBox.removeAllListeners("submit");
1008
- await startChatInShell(client, tokenIdentifier, log, logLines, screen, inputBox, originalSubmitHandler, headerBox, buildWelcomeContent, currentTheme, catFaces, currentCatIndex);
1009
- break;
1010
- }
1011
- case "exit":
1012
- case "quit":
1013
- screen.destroy();
1014
- printCat("sleeping");
1015
- console.log(chalk.cyan("Goodbye! 👋"));
1016
- process.exit(0);
1017
- break;
1018
- case "clear":
1019
- // Clear the output box
1020
- screen.children[1].setContent("");
1021
- screen.render();
1022
- // Show playful message with ASCII cat
1023
- const catArt = currentTheme === "dark"
1024
- ? `{green-fg} /\\_/\\{/green-fg}
1025
- {green-fg} ( ^.^ ){/green-fg}
1026
- {green-fg} > ^ <{/green-fg}`
1027
- : `{black-fg} /\\_/\\{/black-fg}
1028
- {black-fg} ( ^.^ ){/black-fg}
1029
- {black-fg} > ^ <{/black-fg}`;
1030
- log("");
1031
- log(catArt);
1032
- log(chalk.green.bold("Terminal cleared!"));
1033
- log("");
1034
- break;
1035
- case "claim": {
1036
- if (args.length < 1) {
1037
- log(chalk.red("Usage: claim <address|name|symbol> [--execute] [--address <address>]"));
1038
- log(chalk.dim(' Examples: claim MTK, claim "My Token" --execute'));
1039
- return;
1040
- }
1041
- const [identifier] = args;
1042
- const execute = args.includes("--execute");
1043
- const callerAddress = extractFlag(args, "--address");
1044
- if (execute) {
1045
- const privateKey = config.getPrivateKey();
1046
- const account = privateKeyToAccount(privateKey);
1047
- const address = callerAddress || account.address;
1048
- log(chalk.blue("Claiming fees..."));
1049
- screen.render();
1050
- const result = await claimFees(client, identifier, address, true); // silent=true to avoid console.log interference
1051
- displayClaimResultToLog(result, log, logLines);
1052
- }
1053
- else {
1054
- log(chalk.blue("Fetching fee information..."));
1055
- screen.render();
1056
- const result = await viewFees(client, identifier, true); // silent=true to avoid console.log interference
1057
- displayFeesToLog(result, log, logLines);
1058
- }
1059
- break;
1060
- }
1061
- case "transactions": {
1062
- const privateKey = config.getPrivateKey();
1063
- const account = privateKeyToAccount(privateKey);
1064
- const userAddress = account.address;
1065
- const input = {};
1066
- if (extractFlag(args, "--user")) {
1067
- input.userAddress = extractFlag(args, "--user");
1068
- }
1069
- else {
1070
- input.userAddress = userAddress;
1071
- }
1072
- if (extractFlag(args, "--token")) {
1073
- input.tokenId = extractFlag(args, "--token");
1074
- }
1075
- if (extractFlag(args, "--type")) {
1076
- const type = extractFlag(args, "--type");
1077
- if (!["buy", "sell", "airdrop"].includes(type || "")) {
1078
- log(chalk.red("Invalid type. Must be: buy, sell, or airdrop"));
1079
- return;
1080
- }
1081
- input.type = type;
1082
- }
1083
- if (extractFlag(args, "--limit")) {
1084
- input.limit = parseInt(extractFlag(args, "--limit") || "50", 10);
1085
- }
1086
- else {
1087
- input.limit = 50;
1088
- }
1089
- if (extractFlag(args, "--offset")) {
1090
- input.offset = parseInt(extractFlag(args, "--offset") || "0", 10);
1091
- }
1092
- else {
1093
- input.offset = 0;
1094
- }
1095
- log(chalk.blue("Fetching transactions..."));
1096
- screen.render();
1097
- const result = await getTransactions(client, input);
1098
- displayTransactionsToLog(result, log, logLines);
1099
- break;
1100
- }
1101
- case "account": {
1102
- if (args.length === 0) {
1103
- // Show account info
1104
- log(chalk.blue("Fetching account information..."));
1105
- screen.render();
1106
- const result = await getAccountInfo();
1107
- displayAccountInfoToLog(result, log, logLines);
1108
- }
1109
- else if (args[0] === "list") {
1110
- // For list, we need to capture console output or recreate the display
1111
- const accounts = config.getAllAccounts();
1112
- const activeIndex = config.getActiveAccountIndex();
1113
- log("");
1114
- log(chalk.green.bold("📋 All Accounts"));
1115
- log("");
1116
- if (accounts.length === 0) {
1117
- log(chalk.yellow("No accounts configured."));
1118
- log(chalk.blue('Run "httpcat config" to set up your wallet.'));
1119
- return;
1120
- }
1121
- for (const account of accounts) {
1122
- const isActive = account.index === activeIndex;
1123
- const status = isActive
1124
- ? chalk.green("● Active")
1125
- : chalk.blue("○ Inactive");
1126
- const type = account.type === "custom" ? "Custom" : "Seed-Derived";
1127
- const address = account.address.slice(0, 6) + "..." + account.address.slice(-4);
1128
- log(` ${chalk.green(account.index.toString().padEnd(5))} ${chalk.blue(type.padEnd(12))} ${chalk.green(address.padEnd(15))} ${status}`);
1129
- }
1130
- log("");
1131
- log(chalk.blue(`Active account: ${chalk.green(activeIndex.toString())}`));
1132
- }
1133
- else if (args[0] === "switch") {
1134
- if (args.length < 2) {
1135
- log(chalk.red("Usage: account switch <index>"));
1136
- return;
1137
- }
1138
- const index = parseInt(args[1], 10);
1139
- if (isNaN(index)) {
1140
- log(chalk.red("Invalid account index"));
1141
- return;
1142
- }
1143
- switchAccount(index);
1144
- const accounts = config.getAllAccounts();
1145
- const account = accounts.find((acc) => acc.index === index);
1146
- log(chalk.green(`✅ Switched to account ${index}`));
1147
- if (account) {
1148
- log(chalk.blue(` Address: ${account.address.slice(0, 6)}...${account.address.slice(-4)}`));
1149
- }
1150
- // Refresh header with new account info
1151
- buildWelcomeContent(currentTheme, catFaces[currentCatIndex].face).then((content) => {
1152
- headerBox.setContent(content);
1153
- screen.render();
1154
- });
1155
- }
1156
- else if (args[0] === "add") {
1157
- log(chalk.blue("Adding new account..."));
1158
- screen.render();
1159
- await addAccount();
1160
- log(chalk.green("✅ Account added"));
1161
- // Refresh header with new account info
1162
- buildWelcomeContent(currentTheme, catFaces[currentCatIndex].face).then((content) => {
1163
- headerBox.setContent(content);
1164
- screen.render();
1165
- });
1166
- }
1167
- else {
1168
- log(chalk.red("Usage: account [list|switch <index>|add]"));
1169
- }
1170
- break;
1171
- }
1172
- case "env": {
1173
- if (args.length === 0 || args[0] === "show") {
1174
- const current = config.getCurrentEnvironment();
1175
- if (!current) {
1176
- log(chalk.yellow("No environment selected"));
1177
- return;
1178
- }
1179
- const env = config.getEnvironmentConfig(current);
1180
- if (!env) {
1181
- log(chalk.yellow(`Environment "${current}" not found`));
1182
- return;
1183
- }
1184
- log(chalk.cyan.bold(`Current Environment: ${chalk.green(current)}`));
1185
- log(chalk.dim(`Agent URL: ${env.agentUrl}`));
1186
- log(chalk.dim(`Network: ${env.network}`));
1187
- }
1188
- else if (args[0] === "list") {
1189
- const envs = config.getEnvironments();
1190
- const current = config.getCurrentEnvironment();
1191
- log(chalk.cyan.bold("Available Environments:"));
1192
- log("");
1193
- for (const [name, env] of Object.entries(envs)) {
1194
- const isCurrent = name === current;
1195
- const prefix = isCurrent ? chalk.green("→ ") : " ";
1196
- const nameDisplay = isCurrent
1197
- ? chalk.green.bold(name)
1198
- : chalk.bold(name);
1199
- log(`${prefix}${nameDisplay}`);
1200
- log(chalk.dim(` Agent URL: ${env.agentUrl}`));
1201
- log(chalk.dim(` Network: ${env.network}`));
1202
- log("");
1203
- }
1204
- if (current) {
1205
- log(chalk.dim(`Current environment: ${chalk.green(current)}`));
1206
- }
1207
- }
1208
- else if (args[0] === "use") {
1209
- if (args.length < 2) {
1210
- log(chalk.red("Usage: env use <name>"));
1211
- return;
1212
- }
1213
- config.setEnvironment(args[1]);
1214
- const env = config.getEnvironmentConfig(args[1]);
1215
- log(chalk.green(`✅ Switched to environment: ${args[1]}`));
1216
- if (env) {
1217
- log(chalk.dim(` Agent URL: ${env.agentUrl}`));
1218
- log(chalk.dim(` Network: ${env.network}`));
1219
- }
1220
- }
1221
- else if (args[0] === "add") {
1222
- if (args.length < 3) {
1223
- log(chalk.red("Usage: env add <name> <agentUrl> [--network <network>]"));
1224
- return;
1225
- }
1226
- const network = extractFlag(args, "--network") || "eip155:84532";
1227
- config.addEnvironment(args[1], args[2], network);
1228
- log(chalk.green(`✅ Added environment: ${args[1]}`));
1229
- log(chalk.dim(` Agent URL: ${args[2]}`));
1230
- log(chalk.dim(` Network: ${network}`));
1231
- }
1232
- else if (args[0] === "update") {
1233
- if (args.length < 3) {
1234
- log(chalk.red("Usage: env update <name> <agentUrl> [--network <network>]"));
1235
- return;
1236
- }
1237
- const network = extractFlag(args, "--network") || "eip155:84532";
1238
- config.updateEnvironment(args[1], args[2], network);
1239
- log(chalk.green(`✅ Updated environment: ${args[1]}`));
1240
- log(chalk.dim(` Agent URL: ${args[2]}`));
1241
- log(chalk.dim(` Network: ${network}`));
1242
- }
1243
- else {
1244
- log(chalk.red("Usage: env [list|use <name>|show|add <name> <url>|update <name> <url>]"));
1245
- }
1246
- break;
1247
- }
1248
- case "agent":
1249
- case "ai":
1250
- case "cat": {
1251
- log("");
1252
- log(chalk.yellow("⚠️ Agent command is only available in CLI mode"));
1253
- log(chalk.dim("Run 'httpcat cat' from the command line to start agent mode."));
1254
- log("");
1255
- break;
1256
- }
1257
- default:
1258
- log(chalk.red(`Unknown command: ${command}`));
1259
- log(chalk.dim('Type "help" for available commands'));
1260
- }
1261
- }
1262
- function displayWelcomeMessage(log, logLines, outputBox, screen, theme = "dark") {
1263
- // Helper to get theme-appropriate colors
1264
- const getCyanColor = (t) => t === "dark" ? "light-cyan-fg" : "cyan-fg";
1265
- const getBlueColor = (t) => t === "dark" ? "light-blue-fg" : "blue-fg";
1266
- const cyanColor = getCyanColor(theme);
1267
- const blueColor = getBlueColor(theme);
1268
- // If we have outputBox, use blessed tags instead of chalk to avoid question marks
1269
- // Otherwise fall back to the log function with chalk
1270
- if (outputBox && screen) {
1271
- const welcomeLines = [
1272
- "",
1273
- `{bold}{${cyanColor}}🐱 Welcome to httpcat!{/${cyanColor}}{/bold}`,
1274
- "",
1275
- "{yellow-fg}✨ Here are some commands to get you started:{/yellow-fg}",
1276
- "",
1277
- "{green-fg} 📋 list{/green-fg} - Browse all available tokens",
1278
- "{green-fg} ℹ️ info <token>{/green-fg} - Get detailed info about a token",
1279
- "{green-fg} 💰 buy <token> <amount>{/green-fg} - Buy tokens (e.g., buy MTK 0.10)",
1280
- "{green-fg} 💼 positions{/green-fg} - View your portfolio",
1281
- "{green-fg} 💵 balances{/green-fg} - Check your wallet balance",
1282
- "",
1283
- `{${blueColor}}💡 Type {bold}help{/bold} to see all available commands!{/${blueColor}}`,
1284
- "",
1285
- ];
1286
- // Log all welcome lines at once
1287
- welcomeLines.forEach((line) => outputBox.log(line));
1288
- // Scroll to top so welcome message is visible
1289
- outputBox.setScrollPerc(0);
1290
- screen.render();
1291
- }
1292
- else {
1293
- // Fallback to regular log function if outputBox not available
1294
- log("");
1295
- log(chalk.cyan.bold("🐱 Welcome to httpcat!"));
1296
- log("");
1297
- log(chalk.yellow("✨ Here are some commands to get you started:"));
1298
- log("");
1299
- log(chalk.green(" 📋 list") +
1300
- chalk.dim(" - Browse all available tokens"));
1301
- log(chalk.green(" ℹ️ info <token>") +
1302
- chalk.dim(" - Get detailed info about a token"));
1303
- log(chalk.green(" 💰 buy <token> <amount>") +
1304
- chalk.dim(" - Buy tokens (e.g., buy MTK 0.10)"));
1305
- log(chalk.green(" 💼 positions") +
1306
- chalk.dim(" - View your portfolio"));
1307
- log(chalk.green(" 💵 balances") +
1308
- chalk.dim(" - Check your wallet balance"));
1309
- log("");
1310
- log(chalk.blue("💡 Type ") +
1311
- chalk.bold("help") +
1312
- chalk.blue(" to see all available commands!"));
1313
- log("");
1314
- }
1315
- }
1316
- function displayHelp(log, logLines, outputBox, screen, theme = "dark") {
1317
- // Helper to get theme-appropriate colors
1318
- const getCyanColor = (t) => t === "dark" ? "light-cyan-fg" : "cyan-fg";
1319
- const getBlueColor = (t) => t === "dark" ? "light-blue-fg" : "blue-fg";
1320
- const cyanColor = getCyanColor(theme);
1321
- const blueColor = getBlueColor(theme);
1322
- // If we have outputBox, use blessed tags instead of chalk to avoid question marks
1323
- // Otherwise fall back to the log function
1324
- if (outputBox && screen) {
1325
- // Get the line number where help will start (before logging)
1326
- const linesBeforeHelp = outputBox.lines?.length || 0;
1327
- // Collect all help lines using blessed tags
1328
- const helpLines = [
1329
- "",
1330
- `{${cyanColor}}{bold}Available Commands:{/bold}{/${cyanColor}}`,
1331
- "",
1332
- // Token Operations
1333
- `{${cyanColor}}{bold}Token Operations:{/bold}{/${cyanColor}}`,
1334
- "{green-fg} create <name> <symbol>{/green-fg}",
1335
- "{green-fg} buy <id> <amount> [--repeat N] [--delay MS]{/green-fg}",
1336
- "{green-fg} sell <id> <amount|all>{/green-fg}",
1337
- "{green-fg} claim <id> [--execute] [--address ADDR]{/green-fg}",
1338
- "",
1339
- // Token Information
1340
- `{${cyanColor}}{bold}Token Information:{/bold}{/${cyanColor}}`,
1341
- "{green-fg} info <id>{/green-fg}",
1342
- "{green-fg} list [--sort mcap|created|name] [--page N] [--limit N]{/green-fg}",
1343
- "",
1344
- // Portfolio
1345
- `{${cyanColor}}{bold}Portfolio:{/bold}{/${cyanColor}}`,
1346
- "{green-fg} positions [--active|--graduated]{/green-fg}",
1347
- "{green-fg} transactions [--user ADDR] [--token ID] [--type TYPE] [--limit N] [--offset N]{/green-fg}",
1348
- "{green-fg} balances{/green-fg}",
1349
- "",
1350
- // Account Management
1351
- `{${cyanColor}}{bold}Account Management:{/bold}{/${cyanColor}}`,
1352
- "{green-fg} account [list|switch <index>|add]{/green-fg}",
1353
- "{green-fg} env [list|use <name>|show|add|update]{/green-fg}",
1354
- "",
1355
- // Social
1356
- `{${cyanColor}}{bold}Social:{/bold}{/${cyanColor}}`,
1357
- "{green-fg} chat [token] [--input-format FORMAT]{/green-fg}",
1358
- "",
1359
- // Cat (agent and cat are synonyms)
1360
- `{${cyanColor}}{bold}Cat (or 'agent'):{/bold}{/${cyanColor}}`,
1361
- "{green-fg} agent <query>{/green-fg} - Ask the cat to do something (or use 'cat')",
1362
- "{green-fg} agent --chat{/green-fg} - Enter interactive chat mode (or 'cat --chat')",
1363
- "{green-fg} agent --setup{/green-fg} - Configure API key/provider (or 'cat --setup')",
1364
- "",
1365
- // System
1366
- `{${cyanColor}}{bold}System:{/bold}{/${cyanColor}}`,
1367
- "{green-fg} health{/green-fg}",
1368
- "{green-fg} config [--show|--set|--reset]{/green-fg}",
1369
- "{green-fg} network{/green-fg}",
1370
- "",
1371
- // Shell Commands
1372
- `{${cyanColor}}{bold}Shell Commands:{/bold}{/${cyanColor}}`,
1373
- "{green-fg} clear{/green-fg}",
1374
- "{green-fg} help{/green-fg}",
1375
- "{green-fg} exit{/green-fg}",
1376
- "",
1377
- `{${blueColor}}Tip: Run any command without arguments to see detailed help{/${blueColor}}`,
1378
- ];
1379
- // Log all help lines at once
1380
- helpLines.forEach((line) => outputBox.log(line));
1381
- // Scroll to show "Available Commands" at the top of the visible area
1382
- // Get total lines after logging
1383
- const totalLines = outputBox.lines?.length || 0;
1384
- const visibleHeight = outputBox.height || 20;
1385
- if (totalLines > visibleHeight && linesBeforeHelp >= 0) {
1386
- // Calculate which line "Available Commands" is on (after the empty line)
1387
- const availableCommandsLine = linesBeforeHelp + 1;
1388
- // Calculate scroll percentage to show that line at the top
1389
- // Percentage = (line number / total lines) * 100
1390
- // But we want the line to be at the top, so we need to account for visible height
1391
- const scrollPerc = Math.max(0, Math.min(100, ((availableCommandsLine - 1) /
1392
- Math.max(1, totalLines - visibleHeight)) *
1393
- 100));
1394
- outputBox.setScrollPerc(scrollPerc);
1395
- }
1396
- else {
1397
- // If all content fits in visible area or no previous content, scroll to top
1398
- outputBox.setScrollPerc(0);
1399
- }
1400
- screen.render();
1401
- }
1402
- else {
1403
- // Fallback to regular log function if outputBox not available
1404
- log("");
1405
- log(chalk.cyan.bold("Available Commands:"));
1406
- log("");
1407
- // Token Operations
1408
- log(chalk.bold(chalk.cyan("Token Operations:")));
1409
- log(chalk.green(" create <name> <symbol>"));
1410
- log(chalk.green(" buy <id> <amount> [--repeat N] [--delay MS]"));
1411
- log(chalk.green(" sell <id> <amount|all>"));
1412
- log(chalk.green(" claim <id> [--execute] [--address ADDR]"));
1413
- log("");
1414
- // Token Information
1415
- log(chalk.bold(chalk.cyan("Token Information:")));
1416
- log(chalk.green(" info <id>"));
1417
- log(chalk.green(" list [--sort mcap|created|name] [--page N] [--limit N]"));
1418
- log("");
1419
- // Portfolio
1420
- log(chalk.bold(chalk.cyan("Portfolio:")));
1421
- log(chalk.green(" positions [--active|--graduated]"));
1422
- log(chalk.green(" transactions [--user ADDR] [--token ID] [--type TYPE] [--limit N] [--offset N]"));
1423
- log(chalk.green(" balances"));
1424
- log("");
1425
- // Account Management
1426
- log(chalk.bold(chalk.cyan("Account Management:")));
1427
- log(chalk.green(" account [list|switch <index>|add]"));
1428
- log(chalk.green(" env [list|use <name>|show|add|update]"));
1429
- log("");
1430
- // Social
1431
- log(chalk.bold(chalk.cyan("Social:")));
1432
- log(chalk.green(" chat [token] [--input-format FORMAT]"));
1433
- log("");
1434
- // Cat (agent and cat are synonyms)
1435
- log(chalk.bold(chalk.cyan("Cat (or 'agent'):")));
1436
- log(chalk.green(" agent <query>") +
1437
- chalk.dim(" - Ask the cat to do something (or use 'cat')"));
1438
- log(chalk.green(" agent --chat") +
1439
- chalk.dim(" - Enter interactive chat mode (or 'cat --chat')"));
1440
- log(chalk.green(" agent --setup") +
1441
- chalk.dim(" - Configure API key/provider (or 'cat --setup')"));
1442
- log("");
1443
- // System
1444
- log(chalk.bold(chalk.cyan("System:")));
1445
- log(chalk.green(" health"));
1446
- log(chalk.green(" config [--show|--set|--reset]"));
1447
- log(chalk.green(" network"));
1448
- log("");
1449
- // Shell Commands
1450
- log(chalk.bold(chalk.cyan("Shell Commands:")));
1451
- log(chalk.green(" clear"));
1452
- log(chalk.green(" help"));
1453
- log(chalk.green(" exit"));
1454
- log("");
1455
- log(chalk.blue("Tip: Run any command without arguments to see detailed help"));
1456
- }
1457
- }
1458
- function displayConfigToLog(log, logLines) {
1459
- const cfg = config.getAll();
1460
- log("");
1461
- log(chalk.green.bold("Current Configuration:"));
1462
- log("");
1463
- log(chalk.blue("Network: ") + chalk.green(cfg.network));
1464
- log(chalk.blue("Agent URL: ") + chalk.green(cfg.agentUrl));
1465
- log(chalk.blue("Facilitator: ") + chalk.green(cfg.facilitatorUrl));
1466
- log(chalk.blue("Max Payment: ") + chalk.green("$" + cfg.defaultMaxPayment));
1467
- log(chalk.blue("ASCII Art: ") +
1468
- chalk.green(cfg.preferences.enableAsciiArt ? "enabled" : "disabled"));
1469
- log(chalk.blue("Private Key: ") +
1470
- chalk.blue(cfg.privateKey ? "configured" : "not configured"));
1471
- log("");
1472
- log(chalk.blue(`Config file: ${config.getConfigPath()}`));
1473
- }
1474
- // Simplified display functions that log to blessed output
1475
- function displayCreateResultToLog(result, log, logLines) {
1476
- log(chalk.green("Token created successfully!"));
1477
- log(chalk.blue("Name: ") + chalk.green(result.name));
1478
- log(chalk.blue("Symbol: ") + chalk.green(result.symbol));
1479
- if (result.tokenAddress) {
1480
- log(chalk.blue("Address: ") + chalk.green(result.tokenAddress));
1481
- }
1482
- }
1483
- function displayBuyResultToLog(result, log, logLines) {
1484
- log(chalk.green("Purchase successful!"));
1485
- log(chalk.blue("Tokens received: ") + chalk.green(result.tokensReceived));
1486
- log(chalk.blue("Amount spent: ") + chalk.yellow(result.amountSpent));
1487
- log(chalk.blue("New price: ") + chalk.green(result.newPrice));
1488
- log(chalk.blue("Graduation: ") +
1489
- chalk.magenta(result.graduationProgress.toFixed(2) + "%"));
1490
- }
1491
- function displaySellResultToLog(result, log, logLines) {
1492
- log(chalk.green("Sale successful!"));
1493
- log(chalk.blue("Tokens sold: ") +
1494
- chalk.green(formatTokenAmount(result.tokensSold || "0")));
1495
- log(chalk.blue("USDC received: ") +
1496
- chalk.yellow(formatCurrency(result.usdcReceived || "0", 6)));
1497
- if (result.fee) {
1498
- log(chalk.blue("Fee (1%): ") + chalk.yellow(formatCurrency(result.fee, 6)));
1499
- }
1500
- if (result.newPrice) {
1501
- log(chalk.blue("New price: ") + chalk.green(formatCurrency(result.newPrice)));
1502
- }
1503
- if (result.newMcap) {
1504
- log(chalk.blue("New market cap: ") +
1505
- chalk.yellow(formatCurrency(result.newMcap)));
1506
- }
1507
- if (result.graduationProgress !== undefined) {
1508
- log(chalk.blue("Graduation: ") +
1509
- chalk.magenta(result.graduationProgress.toFixed(2) + "%"));
1510
- }
1511
- if (result.txHash) {
1512
- log(chalk.blue("Transaction: ") + chalk.green(result.txHash));
1513
- }
1514
- else if (result.usdcTransferTx) {
1515
- log(chalk.blue("Transaction: ") + chalk.green(result.usdcTransferTx));
1516
- }
1517
- // Graduation progress bar
1518
- if (result.graduationProgress !== undefined) {
1519
- const barLength = 20;
1520
- const filled = Math.floor((result.graduationProgress / 100) * barLength);
1521
- const empty = barLength - filled;
1522
- const bar = "🐱" + "=".repeat(filled) + ">" + " ".repeat(empty);
1523
- log(chalk.yellow(`[${bar}] ${chalk.bold(result.graduationProgress.toFixed(2) + "%")} to moon!`));
1524
- }
1525
- // Display price impact warning for Uniswap swaps
1526
- if (result.source === "uniswap_v2" && result.priceImpact !== undefined) {
1527
- if (result.priceImpact < 0.5) {
1528
- log(chalk.green(`Price Impact: ${result.priceImpact.toFixed(2)}%`));
1529
- }
1530
- else if (result.priceImpact < 1) {
1531
- log(chalk.yellow(`Price Impact: ${result.priceImpact.toFixed(2)}%`));
1532
- }
1533
- else if (result.priceImpact < 3) {
1534
- log(chalk.red(`⚠️ HIGH Price Impact: ${result.priceImpact.toFixed(2)}%`));
1535
- }
1536
- else {
1537
- log(chalk.red(`🚨 VERY HIGH Price Impact: ${result.priceImpact.toFixed(2)}%`));
1538
- }
1539
- }
1540
- // Display route info for Uniswap swaps
1541
- if (result.source === "uniswap_v2" && result.route) {
1542
- const feePercent = result.routeFee
1543
- ? (result.routeFee / 10000).toFixed(2)
1544
- : "N/A";
1545
- log(chalk.cyan(`Route: ${result.route} (${feePercent}% fee)`));
1546
- }
1547
- }
1548
- function displayTokenInfoToLog(info, log, logLines) {
1549
- log("");
1550
- // Token header box
1551
- const priceDisplay = info.status === "graduated"
1552
- ? chalk.dim("--")
1553
- : info.price
1554
- ? formatCurrency(info.price)
1555
- : chalk.dim("N/A");
1556
- const mcapDisplay = info.status === "graduated"
1557
- ? chalk.dim("--")
1558
- : info.mcap
1559
- ? formatCurrency(info.mcap)
1560
- : chalk.dim("N/A");
1561
- // Create box-like display
1562
- log(chalk.green.bold(`📊 ${info.name} (${info.symbol})`));
1563
- log(chalk.dim("─".repeat(60)));
1564
- log(chalk.blue("Address: ") + chalk.green(formatAddress(info.address || "")));
1565
- log(chalk.blue("Status: ") + chalk.green(info.status || "unknown"));
1566
- log(chalk.blue("Price: ") + chalk.green(priceDisplay));
1567
- log(chalk.blue("Market Cap: ") + chalk.yellow(mcapDisplay));
1568
- log(chalk.blue("Total Supply: ") +
1569
- chalk.green(formatTokenAmount(info.totalSupply || "0")));
1570
- log(chalk.blue("Created: ") +
1571
- chalk.green(new Date(info.createdAt || Date.now()).toLocaleString()));
1572
- log("");
1573
- // Graduation progress
1574
- if (info.graduationProgress !== undefined) {
1575
- const barLength = 20;
1576
- const filled = Math.floor((info.graduationProgress / 100) * barLength);
1577
- const empty = barLength - filled;
1578
- const bar = "🐱" + "=".repeat(filled) + ">" + " ".repeat(empty);
1579
- log(chalk.yellow(`[${bar}] ${chalk.bold(info.graduationProgress.toFixed(2) + "%")} to moon!`));
1580
- log("");
1581
- }
1582
- // Position display
1583
- if (info.userPosition && info.userPosition.tokensOwned !== "0") {
1584
- log(chalk.green.bold("💼 Your Position"));
1585
- log(chalk.dim("─".repeat(60)));
1586
- log(chalk.blue("Tokens Owned: ") +
1587
- chalk.green(formatTokenAmount(info.userPosition.tokensOwned)));
1588
- log(chalk.blue("Invested: ") +
1589
- chalk.green(formatCurrency(info.userPosition.usdcInvested || "0")));
1590
- if (info.userPosition.currentValue) {
1591
- log(chalk.blue("Current Value: ") +
1592
- chalk.green(formatCurrency(info.userPosition.currentValue)));
1593
- }
1594
- if (info.userPosition.pnl) {
1595
- const pnl = parseFloat(info.userPosition.pnl);
1596
- const pnlColor = pnl >= 0 ? chalk.green : chalk.red;
1597
- const pnlSign = pnl >= 0 ? "+" : "";
1598
- const pnlIcon = pnl >= 0 ? "🟢" : "🔴";
1599
- log(chalk.blue("P&L: ") +
1600
- `${pnlIcon} ${pnlColor(pnlSign + formatCurrency(info.userPosition.pnl))}`);
1601
- }
1602
- log("");
1603
- }
1604
- }
1605
- function displayTokenListToLog(result, log, logLines, logLinesSmooth) {
1606
- // Collect all lines first for smooth scrolling
1607
- const allLines = [];
1608
- allLines.push("");
1609
- allLines.push(chalk.green.bold(`Tokens (${result.total || result.tokens.length} total):`));
1610
- if (result.page && result.pages) {
1611
- allLines.push(chalk.dim(`Page ${result.page} of ${result.pages}`));
1612
- }
1613
- allLines.push("");
1614
- if (result.tokens.length === 0) {
1615
- allLines.push(chalk.yellow("No tokens found."));
1616
- // Use smooth scrolling if available, otherwise regular logLines
1617
- if (logLinesSmooth) {
1618
- logLinesSmooth(allLines, false);
1619
- }
1620
- else {
1621
- logLines(allLines);
1622
- }
1623
- return;
1624
- }
1625
- // Show header
1626
- allLines.push(` ${chalk.cyan.bold("Name".padEnd(20))} ${chalk.cyan.bold("Symbol".padEnd(10))} ${chalk.cyan.bold("Market Cap".padEnd(15))} ${chalk.cyan.bold("Price".padEnd(12))} ${chalk.cyan.bold("Graduation".padEnd(12))} ${chalk.cyan.bold("Status")}`);
1627
- allLines.push(chalk.dim(" " + "─".repeat(90)));
1628
- // Show tokens
1629
- for (const token of result.tokens) {
1630
- const graduationIcon = token.status === "graduated"
1631
- ? "[G]"
1632
- : token.graduationProgress >= 90
1633
- ? "[H]"
1634
- : token.graduationProgress >= 50
1635
- ? "[M]"
1636
- : "[L]";
1637
- const mcapDisplay = token.status === "graduated"
1638
- ? chalk.dim("--")
1639
- : formatCurrency(token.mcap || "0");
1640
- const priceDisplay = token.status === "graduated"
1641
- ? chalk.dim("--")
1642
- : formatCurrency(token.price || "0");
1643
- const graduationDisplay = `${graduationIcon} ${(token.graduationProgress || 0).toFixed(1)}%`;
1644
- const statusDisplay = token.status === "graduated"
1645
- ? chalk.green("✓ Graduated")
1646
- : chalk.yellow("Active");
1647
- allLines.push(` ${chalk.bold(token.name.padEnd(20))} ${chalk.green(token.symbol.padEnd(10))} ${mcapDisplay.padEnd(15)} ${priceDisplay.padEnd(12)} ${graduationDisplay.padEnd(12)} ${statusDisplay}`);
1648
- }
1649
- allLines.push("");
1650
- if (result.page && result.pages && result.page < result.pages) {
1651
- allLines.push(chalk.dim(`Use --page ${result.page + 1} to see more tokens`));
1652
- allLines.push("");
1653
- }
1654
- // Show example commands
1655
- if (result.tokens.length > 0) {
1656
- const firstToken = result.tokens[0];
1657
- allLines.push(chalk.dim("Example commands:"));
1658
- allLines.push(chalk.dim(` buy ${firstToken.symbol}`));
1659
- allLines.push(chalk.dim(` sell ${firstToken.symbol}`));
1660
- allLines.push(chalk.dim(` info ${firstToken.symbol}`));
1661
- allLines.push("");
1662
- }
1663
- // Use smooth scrolling if available (scrolls to show the list header), otherwise regular logLines
1664
- if (logLinesSmooth) {
1665
- logLinesSmooth(allLines, true); // scrollToTop=true to show the list header
1666
- }
1667
- else {
1668
- logLines(allLines);
1669
- }
1670
- }
1671
- function displayPositionsToLog(result, log, logLines) {
1672
- if (!result.positions || result.positions.length === 0) {
1673
- log(chalk.yellow("No positions found."));
1674
- return;
1675
- }
1676
- log(chalk.green.bold(`Your Positions (${result.positions.length}):`));
1677
- log("");
1678
- for (const pos of result.positions) {
1679
- const tokenName = pos.token?.name || "???";
1680
- const tokenSymbol = pos.token?.symbol || "???";
1681
- const tokensDisplay = formatTokenAmount(pos.tokensOwned || "0");
1682
- const valueDisplay = pos.currentValue
1683
- ? formatCurrency(pos.currentValue)
1684
- : "N/A";
1685
- log(` ${chalk.green.bold(`${tokenName} (${tokenSymbol})`)} - ${chalk.green(`${tokensDisplay} tokens`)} @ ${chalk.yellow(valueDisplay)}`);
1686
- }
1687
- }
1688
- function displayHealthStatusToLog(health, log, logLines) {
1689
- const statusColor = health.status === "ok" || health.status === "healthy"
1690
- ? chalk.green
1691
- : chalk.red;
1692
- log(statusColor(`Agent Status: ${health.status}`));
1693
- if (health.version) {
1694
- log(chalk.blue("Version: ") + chalk.green(health.version));
1695
- }
1696
- if (health.uptime) {
1697
- log(chalk.blue("Uptime: ") + chalk.green(health.uptime));
1698
- }
1699
- }
1700
- function displayBalanceToLog(balance, log, logLines) {
1701
- log("");
1702
- log(chalk.green.bold("Wallet Balance:"));
1703
- log(chalk.blue("Address: ") + chalk.green(balance.address));
1704
- log(chalk.blue("ETH: ") +
1705
- chalk.yellow(balance.ethFormatted || balance.ethBalance));
1706
- log(chalk.blue("USDC: ") +
1707
- chalk.green(balance.usdcFormatted || balance.usdcBalance));
1708
- if (balance.cat402Formatted) {
1709
- log(chalk.blue("CAT: ") + chalk.cyan(balance.cat402Formatted));
1710
- }
1711
- log("");
1712
- // Show warnings if balances are low
1713
- const ethNum = Number(balance.ethFormatted || balance.ethBalance);
1714
- const usdcNum = Number((balance.usdcFormatted || balance.usdcBalance)
1715
- .replace("$", "")
1716
- .replace(",", ""));
1717
- if (ethNum < 0.001 && usdcNum < 1) {
1718
- log(chalk.yellow("⚠️ Low balances detected!"));
1719
- log(chalk.dim(" You need Base Sepolia ETH for gas fees"));
1720
- log(chalk.dim(" You need Base Sepolia USDC for trading"));
1721
- log("");
1722
- }
1723
- else if (ethNum < 0.001) {
1724
- log(chalk.yellow("⚠️ Low ETH balance - you may not have enough for gas fees"));
1725
- log("");
1726
- }
1727
- else if (usdcNum < 1) {
1728
- log(chalk.yellow("⚠️ Low USDC balance - you may not have enough for trades"));
1729
- log("");
1730
- }
1731
- }
1732
- function extractFlag(args, flag) {
1733
- const index = args.findIndex((arg) => arg === flag);
1734
- if (index >= 0 && index < args.length - 1) {
1735
- return args[index + 1];
1736
- }
1737
- return undefined;
1738
- }
1739
- function displayFeesToLog(fees, log, logLines) {
1740
- const hasFeesToken = BigInt(fees.feeToken || "0") > 0n;
1741
- const hasFeesPaired = BigInt(fees.feePaired || "0") > 0n;
1742
- const hasFees = hasFeesToken || hasFeesPaired;
1743
- log("");
1744
- log(chalk.green.bold(`💰 Accumulated Fees: ${fees.tokenName} (${fees.tokenSymbol})`));
1745
- log(chalk.blue("━".repeat(50)));
1746
- log(chalk.blue("Contract: ") + chalk.green(fees.tokenAddress));
1747
- log(chalk.blue("LP Status: ") +
1748
- chalk.green(fees.isLocked ? "🔒 Locked" : "❌ Not Locked"));
1749
- if (typeof fees.v4TickLower === "number" &&
1750
- typeof fees.v4TickUpper === "number" &&
1751
- typeof fees.v4TickCurrent === "number" &&
1752
- typeof fees.v4InRange === "boolean") {
1753
- log(chalk.blue("V4 Range: ") +
1754
- chalk.green(`[${fees.v4TickLower}, ${fees.v4TickUpper}) | Current: ${fees.v4TickCurrent} | In Range: ${fees.v4InRange ? "✅" : "❌"}`));
1755
- }
1756
- log("");
1757
- log(chalk.green("Total Fees:"));
1758
- log(chalk.blue(" Tokens: ") + chalk.green(hasFeesToken ? fees.feeToken : "0"));
1759
- log(chalk.blue(" USDC: ") +
1760
- chalk.green(hasFeesPaired ? fees.feePaired : "$0.00"));
1761
- log("");
1762
- log(chalk.green("Creator Share (80%):"));
1763
- log(chalk.blue(" Tokens: ") + chalk.green(fees.creatorToken || "0"));
1764
- log(chalk.blue(" USDC: ") + chalk.green(fees.creatorPaired || "$0.00"));
1765
- log("");
1766
- log(chalk.green("Platform Share (20%):"));
1767
- log(chalk.blue(" Tokens: ") + chalk.green(fees.platformToken || "0"));
1768
- log(chalk.blue(" USDC: ") + chalk.green(fees.platformPaired || "$0.00"));
1769
- log(chalk.blue("━".repeat(50)));
1770
- if (!hasFees) {
1771
- if (fees.v4InRange === false) {
1772
- log(chalk.yellow("\n⚠️ No fees accrued: position is out of range"));
1773
- }
1774
- else {
1775
- log(chalk.yellow("\n⚠️ No fees accumulated yet. Trade volume needed."));
1776
- }
1777
- }
1778
- else {
1779
- log(chalk.blue("\n💡 Run with --execute to claim fees"));
1780
- }
1781
- }
1782
- function displayClaimResultToLog(result, log, logLines) {
1783
- log(chalk.green("✅ Fees claimed successfully!"));
1784
- log(chalk.blue("Token: ") + chalk.green(result.tokenAddress));
1785
- log(chalk.blue("Transaction: ") + chalk.green(result.txHash));
1786
- log(chalk.blue("Claimed:"));
1787
- log(chalk.blue(" Tokens: ") + chalk.green(result.creatorToken || "0"));
1788
- log(chalk.blue(" USDC: ") + chalk.green(result.creatorPaired || "$0.00"));
1789
- }
1790
- function displayTransactionsToLog(result, log, logLines) {
1791
- log(chalk.green.bold(`📜 Transactions (${result.total} total)`));
1792
- log("");
1793
- if (result.transactions.length === 0) {
1794
- log(chalk.yellow("No transactions found."));
1795
- return;
1796
- }
1797
- for (let i = 0; i < result.transactions.length; i++) {
1798
- const tx = result.transactions[i];
1799
- const typeIcon = tx.type === "buy" ? "🟢" : tx.type === "sell" ? "🔴" : "🎁";
1800
- const statusIcon = tx.status === "success" ? "✅" : tx.status === "pending" ? "⏳" : "❌";
1801
- log(chalk.green.bold(`${i + 1}. ${typeIcon} ${tx.type.toUpperCase()} ${statusIcon} ${tx.status.toUpperCase()}`));
1802
- if (tx.token) {
1803
- log(chalk.blue(` Token: ${tx.token.name} (${tx.token.symbol})`));
1804
- }
1805
- log(chalk.blue(` Amount: ${tx.amount}`));
1806
- log(chalk.blue(` Fee: ${tx.fee}`));
1807
- if (tx.txHash) {
1808
- log(chalk.blue(` TX: ${tx.txHash}`));
1809
- }
1810
- log(chalk.blue(` Date: ${new Date(tx.createdAt).toLocaleString()}`));
1811
- log("");
1812
- }
1813
- if (result.hasMore) {
1814
- log(chalk.blue(`Showing ${result.offset + 1}-${result.offset + result.transactions.length} of ${result.total}`));
1815
- log(chalk.blue(`Use --offset ${result.offset + result.limit} to see more`));
1816
- }
1817
- }
1818
- function displayAccountInfoToLog(data, log, logLines) {
1819
- const { account, balance } = data;
1820
- log("");
1821
- log(chalk.green.bold("👤 Account Information"));
1822
- log(chalk.blue("=".repeat(50)));
1823
- log("");
1824
- log(chalk.blue("Account Index: ") + chalk.green(account.index.toString()));
1825
- log(chalk.blue("Type: ") +
1826
- chalk.green(account.type === "custom" ? "Custom" : "Seed-Derived"));
1827
- log(chalk.blue("Address: ") + chalk.green(account.address));
1828
- if (account.label) {
1829
- log(chalk.blue("Label: ") + chalk.green(account.label));
1830
- }
1831
- log("");
1832
- log(chalk.green.bold("💰 Balances"));
1833
- log(chalk.blue("ETH: ") + chalk.yellow(balance.ethBalance));
1834
- log(chalk.blue("USDC: ") + chalk.green(balance.usdcBalance));
1835
- if (balance.cat402Formatted) {
1836
- log(chalk.blue("CAT: ") + chalk.green(balance.cat402Formatted));
1837
- }
1838
- log("");
1839
- if (data.positions && data.positions.positions.length > 0) {
1840
- log(chalk.green.bold("💼 Positions"));
1841
- log("");
1842
- displayPositionsToLog(data.positions, log, logLines);
1843
- }
1844
- }
1845
- // Helper function to format time remaining for lease
1846
- function formatTimeRemaining(expiresAt) {
1847
- const now = new Date();
1848
- const diffMs = expiresAt.getTime() - now.getTime();
1849
- if (diffMs <= 0) {
1850
- return "expired";
1851
- }
1852
- const minutes = Math.floor(diffMs / 60000);
1853
- const seconds = Math.floor((diffMs % 60000) / 1000);
1854
- if (minutes > 0) {
1855
- return `${minutes}m ${seconds}s`;
1856
- }
1857
- return `${seconds}s`;
1858
- }
1859
- // Helper function to build chat header content
1860
- async function buildChatHeaderContent(theme, tokenInfo, tokenIdentifier, isConnected, leaseInfo, getCyanColor) {
1861
- const colorTag = theme === "dark" ? "green-fg" : "black-fg";
1862
- const cyanColor = getCyanColor(theme);
1863
- const lines = [];
1864
- // Title line
1865
- if (tokenInfo) {
1866
- lines.push(`{${colorTag}}💬 httpcat Chat: {${cyanColor}}${tokenInfo.name} (${tokenInfo.symbol}){/${cyanColor}}{/${colorTag}}`);
1867
- }
1868
- else if (tokenIdentifier) {
1869
- // Fallback to identifier if token lookup failed
1870
- const displayId = tokenIdentifier.length > 20
1871
- ? `${tokenIdentifier.slice(0, 10)}...${tokenIdentifier.slice(-6)}`
1872
- : tokenIdentifier;
1873
- lines.push(`{${colorTag}}💬 httpcat Chat: {${cyanColor}}${displayId}{/${cyanColor}}{/${colorTag}}`);
1874
- }
1875
- else {
1876
- lines.push(`{${colorTag}}💬 httpcat Chat: General{/${colorTag}}`);
1877
- }
1878
- lines.push(`{${colorTag}}${"═".repeat(60)}{/${colorTag}}`);
1879
- lines.push("");
1880
- // Connection status
1881
- if (isConnected) {
1882
- lines.push(`{green-fg}✅ Connected to chat stream{/green-fg}`);
1883
- lines.push("");
1884
- }
1885
- // Lease info
1886
- if (leaseInfo) {
1887
- const timeRemaining = formatTimeRemaining(leaseInfo.leaseExpiresAt);
1888
- const isExpired = leaseInfo.leaseExpiresAt.getTime() <= Date.now();
1889
- const timePrefix = isExpired ? "⚠️ " : "⏱️ ";
1890
- lines.push(`{yellow-fg}${timePrefix}Lease expires in: ${timeRemaining}{/yellow-fg}`);
1891
- }
1892
- lines.push("");
1893
- lines.push(`{${cyanColor}}💡 Type your message and press Enter to send{/${cyanColor}}`);
1894
- lines.push(`{${cyanColor}}💡 Type /exit to return to shell{/${cyanColor}}`);
1895
- lines.push(`{${cyanColor}}💡 Type /renew to renew your lease{/${cyanColor}}`);
1896
- if (tokenIdentifier) {
1897
- lines.push(`{${cyanColor}}💡 Type /buy <amount> to buy tokens{/${cyanColor}}`);
1898
- lines.push(`{${cyanColor}}💡 Type /sell <amount> to sell tokens{/${cyanColor}}`);
1899
- }
1900
- return lines.join("\n");
1901
- }
1902
- // Simplified chat mode that works within the shell
1903
- async function startChatInShell(client, tokenIdentifier, log, logLines, screen, inputBox, originalSubmitHandler, headerBox, buildWelcomeContent, currentTheme, catFaces, currentCatIndex) {
1904
- const privateKey = config.getPrivateKey();
1905
- const account = privateKeyToAccount(privateKey);
1906
- const userAddress = account.address;
1907
- let leaseInfo = null;
1908
- let ws = null;
1909
- let isInChatMode = true;
1910
- let isProcessing = false; // Flag to prevent duplicate processing
1911
- const displayedMessageIds = new Set();
1912
- // Helper to format chat messages
1913
- const formatChatMessage = (msg, isOwn = false) => {
1914
- const date = new Date(msg.timestamp);
1915
- const now = new Date();
1916
- const diffMs = now.getTime() - date.getTime();
1917
- const diffSec = Math.floor(diffMs / 1000);
1918
- const diffMin = Math.floor(diffSec / 60);
1919
- let timeStr = "just now";
1920
- if (diffSec >= 60) {
1921
- timeStr =
1922
- diffMin < 60
1923
- ? `${diffMin}m ago`
1924
- : date.toLocaleTimeString("en-US", {
1925
- hour: "2-digit",
1926
- minute: "2-digit",
1927
- });
1928
- }
1929
- const authorColor = isOwn ? chalk.green : chalk.cyan;
1930
- const authorShort = msg.authorShort || formatAddress(msg.author, 6);
1931
- return `${chalk.dim(`[${timeStr}]`)} ${authorColor(authorShort)}: ${msg.message}`;
1932
- };
1933
- // Get token information if it's a token chat
1934
- let tokenInfo = null;
1935
- if (tokenIdentifier) {
1936
- try {
1937
- const info = await getTokenInfo(client, tokenIdentifier, userAddress, true);
1938
- tokenInfo = { name: info.name, symbol: info.symbol };
1939
- }
1940
- catch (error) {
1941
- // Token lookup failed - will use identifier in header
1942
- tokenInfo = null;
1943
- }
1944
- }
1945
- // Helper to get cyan color based on theme
1946
- const getCyanColor = (theme) => theme === "dark" ? "light-cyan-fg" : "cyan-fg";
1947
- // Helper to update header
1948
- const updateChatHeader = async (isConnected = false) => {
1949
- const content = await buildChatHeaderContent(currentTheme, tokenInfo, tokenIdentifier, isConnected, leaseInfo, getCyanColor);
1950
- headerBox.setContent(content);
1951
- screen.render();
1952
- };
1953
- // Update header to show chat mode
1954
- await updateChatHeader(false);
1955
- // Set up header update interval for lease countdown
1956
- let headerUpdateInterval = null;
1957
- try {
1958
- // Join chat
1959
- log(chalk.blue("Joining chat..."));
1960
- screen.render();
1961
- const joinResult = await joinChat(client, userAddress, tokenIdentifier, true);
1962
- leaseInfo = {
1963
- leaseId: joinResult.leaseId,
1964
- leaseExpiresAt: new Date(joinResult.leaseExpiresAt),
1965
- };
1966
- const tokenAddress = joinResult.tokenAddress; // Store token address from response
1967
- // Update header with lease info
1968
- await updateChatHeader(false);
1969
- // Display last messages
1970
- if (joinResult.lastMessages.length > 0) {
1971
- log(chalk.dim("─".repeat(60)));
1972
- log(chalk.cyan.bold("Recent messages:"));
1973
- for (const msg of joinResult.lastMessages) {
1974
- displayedMessageIds.add(msg.messageId);
1975
- const isOwn = msg.author.toLowerCase() === userAddress.toLowerCase();
1976
- log(formatChatMessage(msg, isOwn));
1977
- }
1978
- log(chalk.dim("─".repeat(60)));
1979
- }
1980
- // Connect to WebSocket
1981
- const agentUrl = client.getAgentUrl();
1982
- const normalizedWsUrl = normalizeWebSocketUrl(joinResult.wsUrl, agentUrl);
1983
- const wsUrlObj = new URL(normalizedWsUrl);
1984
- wsUrlObj.searchParams.set("leaseId", joinResult.leaseId);
1985
- await new Promise((resolve) => setTimeout(resolve, 500));
1986
- ws = new WebSocket(wsUrlObj.toString());
1987
- ws.on("open", () => {
1988
- log(chalk.green("✅ Connected to chat stream"));
1989
- updateChatHeader(true);
1990
- screen.render();
1991
- });
1992
- ws.on("message", (data) => {
1993
- try {
1994
- const event = JSON.parse(data.toString());
1995
- if (event.type === "message" && event.data) {
1996
- const msg = {
1997
- ...event.data,
1998
- authorShort: formatAddress(event.data.author, 6),
1999
- };
2000
- if (!displayedMessageIds.has(msg.messageId)) {
2001
- displayedMessageIds.add(msg.messageId);
2002
- const isOwn = msg.author.toLowerCase() === userAddress.toLowerCase();
2003
- log(formatChatMessage(msg, isOwn));
2004
- screen.render();
377
+ break;
378
+ case "swap":
379
+ {
380
+ if (args.length < 3) {
381
+ log(chalk.red("Error: Please provide tokenIn, tokenOut, and amount"));
382
+ log(chalk.gray("Usage: swap <tokenIn> <tokenOut> <amount>"));
383
+ break;
384
+ }
385
+ log(chalk.cyan(`Swapping ${args[2]} of ${args[0]} for ${args[1]}...`));
386
+ const { swap, displaySwapResult } = await import("../commands/swap.js");
387
+ const privateKey = config.getPrivateKey();
388
+ if (privateKey) {
389
+ // Create fresh client to ensure x402 payment headers are valid
390
+ const freshClient = await HttpcatClient.create(privateKey);
391
+ const result = await swap(freshClient, args[0], args[1], args[2], 50, true, // silent=true to suppress status lines
392
+ privateKey);
393
+ await withConsoleRedirect(() => displaySwapResult(result));
394
+ }
395
+ else {
396
+ log(chalk.red("Error: Private key not configured"));
397
+ }
2005
398
  }
2006
- }
2007
- else if (event.type === "lease_expired") {
2008
- log(chalk.yellow("⏱️ Your lease has expired. Type /renew to continue."));
2009
- leaseInfo = null;
2010
- updateChatHeader(true);
2011
- screen.render();
2012
- }
2013
- }
2014
- catch (error) {
2015
- // Ignore parse errors
2016
- }
2017
- });
2018
- ws.on("error", (error) => {
2019
- log(chalk.red(`❌ WebSocket error: ${error.message}`));
2020
- screen.render();
2021
- });
2022
- ws.on("close", () => {
2023
- if (isInChatMode) {
2024
- log(chalk.yellow("⚠️ WebSocket connection closed"));
2025
- updateChatHeader(false);
2026
- screen.render();
2027
- }
2028
- });
2029
- // Start header update interval
2030
- headerUpdateInterval = setInterval(async () => {
2031
- if (leaseInfo && isInChatMode) {
2032
- await updateChatHeader(true);
2033
- }
2034
- }, 1000);
2035
- // Wait for connection
2036
- await new Promise((resolve, reject) => {
2037
- const timeout = setTimeout(() => reject(new Error("Connection timeout")), 10000);
2038
- ws.once("open", () => {
2039
- clearTimeout(timeout);
2040
- resolve();
2041
- });
2042
- ws.once("error", (err) => {
2043
- clearTimeout(timeout);
2044
- reject(err);
2045
- });
2046
- });
2047
- // Set up chat input handler
2048
- const chatInputHandler = async (value) => {
2049
- const trimmed = value.trim();
2050
- // Clear input immediately
2051
- inputBox.clearValue();
2052
- screen.render();
2053
- if (!trimmed) {
2054
- isProcessing = false;
2055
- inputBox.focus();
2056
- screen.render();
2057
- return;
2058
- }
2059
- // Handle commands
2060
- if (trimmed.startsWith("/")) {
2061
- const [cmd, ...cmdArgs] = trimmed.split(" ");
2062
- switch (cmd) {
2063
- case "/exit":
2064
- case "/quit":
2065
- isInChatMode = false;
2066
- if (headerUpdateInterval) {
2067
- clearInterval(headerUpdateInterval);
2068
- headerUpdateInterval = null;
399
+ break;
400
+ case "positions":
401
+ {
402
+ log(chalk.cyan("Fetching your positions..."));
403
+ const { getPositions, displayPositions } = await import("../commands/positions.js");
404
+ const privateKey = config.getPrivateKey();
405
+ if (privateKey) {
406
+ const account = privateKeyToAccount(privateKey);
407
+ const positions = await getPositions(client, account.address);
408
+ await withConsoleRedirect(() => displayPositions(positions, "all", true));
2069
409
  }
2070
- if (ws) {
2071
- ws.close();
2072
- ws = null;
410
+ else {
411
+ log(chalk.red("Error: Private key not configured"));
2073
412
  }
2074
- log(chalk.yellow("Exited chat mode. Back to shell."));
2075
- log("");
2076
- // Restore original header
2077
- const originalContent = await buildWelcomeContent(currentTheme, catFaces[currentCatIndex].face);
2078
- headerBox.setContent(originalContent);
2079
- // Restore original input handler
2080
- inputBox.removeAllListeners("submit");
2081
- inputBox.on("submit", originalSubmitHandler);
2082
- isProcessing = false;
2083
- inputBox.focus();
2084
- screen.render();
2085
- return;
2086
- case "/renew":
2087
- try {
2088
- log(chalk.blue("Renewing lease..."));
2089
- screen.render();
2090
- const renewal = await renewLease(client, userAddress, leaseInfo?.leaseId);
2091
- leaseInfo = {
2092
- leaseId: renewal.leaseId,
2093
- leaseExpiresAt: new Date(renewal.leaseExpiresAt),
2094
- };
2095
- await updateChatHeader(true);
2096
- log(chalk.green("✅ Lease renewed!"));
2097
- screen.render();
413
+ }
414
+ break;
415
+ case "transactions":
416
+ case "txns":
417
+ {
418
+ log(chalk.cyan("Fetching recent transactions..."));
419
+ const { getTransactions, displayTransactions } = await import("../commands/transactions.js");
420
+ const privateKey = config.getPrivateKey();
421
+ if (privateKey) {
422
+ const account = privateKeyToAccount(privateKey);
423
+ const txns = await getTransactions(client, {
424
+ userAddress: account.address,
425
+ });
426
+ await withConsoleRedirect(() => displayTransactions(txns));
2098
427
  }
2099
- catch (error) {
2100
- log(chalk.red(`❌ Renew failed: ${error instanceof Error ? error.message : String(error)}`));
2101
- screen.render();
428
+ else {
429
+ log(chalk.red("Error: Private key not configured"));
2102
430
  }
2103
- isProcessing = false;
2104
- inputBox.focus();
2105
- screen.render();
2106
- return;
2107
- case "/help":
2108
- log(chalk.cyan("Chat commands: /exit, /quit, /renew, /help"));
2109
- screen.render();
2110
- isProcessing = false;
2111
- inputBox.focus();
2112
- screen.render();
2113
- return;
2114
- default:
2115
- log(chalk.yellow(`Unknown command: ${cmd}. Type /help for commands.`));
2116
- screen.render();
2117
- isProcessing = false;
2118
- inputBox.focus();
2119
- screen.render();
2120
- return;
2121
- }
2122
- }
2123
- // Send message
2124
- if (!leaseInfo || leaseInfo.leaseExpiresAt.getTime() <= Date.now()) {
2125
- log(chalk.yellow("⏱️ Your lease has expired. Type /renew to continue."));
2126
- screen.render();
2127
- isProcessing = false;
2128
- inputBox.focus();
2129
- screen.render();
2130
- return;
2131
- }
2132
- try {
2133
- // Send message - it will appear via WebSocket when received
2134
- await sendChatMessage(client, trimmed, leaseInfo.leaseId, userAddress, tokenAddress);
2135
- // Re-attach handler for next message
2136
- inputBox.removeAllListeners("submit");
2137
- inputBox.once("submit", chatInputHandler);
2138
- isProcessing = false;
2139
- inputBox.focus();
2140
- screen.render();
2141
- }
2142
- catch (error) {
2143
- log(chalk.red(`❌ Send failed: ${error instanceof Error ? error.message : String(error)}`));
2144
- screen.render();
2145
- // Re-attach handler for next message
2146
- inputBox.removeAllListeners("submit");
2147
- inputBox.once("submit", chatInputHandler);
2148
- isProcessing = false;
2149
- inputBox.focus();
2150
- screen.render();
2151
- }
2152
- };
2153
- // Temporarily replace input handler
2154
- // Remove all listeners first to ensure clean state
2155
- inputBox.removeAllListeners("submit");
2156
- // Clear any existing input value to prevent state issues
2157
- inputBox.clearValue();
2158
- // Add the chat handler - only one handler should be active
2159
- inputBox.once("submit", chatInputHandler);
2160
- // Ensure input box has focus and is ready
2161
- inputBox.focus();
2162
- screen.render();
2163
- }
2164
- catch (error) {
2165
- log(chalk.red(`❌ Chat error: ${error instanceof Error ? error.message : String(error)}`));
2166
- if (headerUpdateInterval) {
2167
- clearInterval(headerUpdateInterval);
2168
- headerUpdateInterval = null;
2169
- }
2170
- if (ws)
2171
- ws.close();
2172
- // Restore original header on error
2173
- const originalContent = await buildWelcomeContent(currentTheme, catFaces[currentCatIndex].face);
2174
- headerBox.setContent(originalContent);
2175
- // Restore original handler on error
2176
- inputBox.removeAllListeners("submit");
2177
- inputBox.on("submit", originalSubmitHandler);
2178
- screen.render();
2179
- inputBox.focus();
2180
- screen.render();
2181
- }
2182
- }
2183
- /**
2184
- * Start agent interactive mode (standalone, called from CLI)
2185
- * This looks identical to the regular shell but starts in agent chat mode
2186
- */
2187
- export async function startAgentInteractiveMode(client) {
2188
- // Auto-detect terminal background and set default theme
2189
- const detectedBg = detectTerminalBackground();
2190
- let currentTheme = detectedBg === "dark" ? "dark" : "win95";
2191
- // Helper function to get theme-appropriate cyan/blue color for blessed tags
2192
- // For dark theme, use lighter colors (light-cyan-fg) for better visibility on black
2193
- const getCyanColor = (theme) => theme === "dark" ? "light-cyan-fg" : "cyan-fg";
2194
- const getBlueColor = (theme) => theme === "dark" ? "light-blue-fg" : "blue-fg";
2195
- // Create blessed screen with optimized settings
2196
- const screen = blessed.screen({
2197
- smartCSR: true,
2198
- title: "httpcat Agent Mode",
2199
- fullUnicode: true, // Support double-width/surrogate/combining chars (emojis)
2200
- fastCSR: false, // Disable fast CSR to prevent rendering issues
2201
- cursor: {
2202
- artificial: true,
2203
- shape: "line",
2204
- blink: true,
2205
- color: "green",
2206
- },
2207
- // Force Unicode support for emojis
2208
- forceUnicode: true,
2209
- });
2210
- const network = client.getNetwork();
2211
- // Theme colors - no backgrounds, just borders
2212
- const getThemeColors = (theme) => {
2213
- switch (theme) {
2214
- case "light":
2215
- return {
2216
- bg: "default", // Transparent/default
2217
- fg: "black",
2218
- border: "black",
2219
- inputBg: "default",
2220
- inputFg: "black", // Explicit black for visibility
2221
- inputFocusBg: "default",
2222
- inputFocusFg: "black",
2223
- };
2224
- case "win95":
2225
- return {
2226
- bg: "default",
2227
- fg: "black",
2228
- border: "black",
2229
- inputBg: "default",
2230
- inputFg: "black", // Explicit black for visibility
2231
- inputFocusBg: "default",
2232
- inputFocusFg: "black",
2233
- };
2234
- default: // dark
2235
- return {
2236
- bg: "default",
2237
- fg: "green",
2238
- border: "green",
2239
- inputBg: "default",
2240
- inputFg: "green", // Explicit green for visibility
2241
- inputFocusBg: "default",
2242
- inputFocusFg: "green",
2243
- };
2244
- }
2245
- };
2246
- let themeColors = getThemeColors(currentTheme);
2247
- // Create header box with thick borders, transparent background - more compact
2248
- const headerBox = blessed.box({
2249
- top: 0,
2250
- left: 0,
2251
- width: "100%",
2252
- height: 6, // Reduced to save vertical space
2253
- content: "",
2254
- tags: true,
2255
- style: {
2256
- fg: themeColors.fg,
2257
- bg: "default", // Transparent
2258
- bold: false,
2259
- border: {
2260
- fg: themeColors.border,
2261
- bold: true,
2262
- },
2263
- },
2264
- padding: {
2265
- left: 1,
2266
- right: 1,
2267
- top: 0,
2268
- bottom: 0,
2269
- },
2270
- border: {
2271
- type: "line",
2272
- fg: themeColors.border,
2273
- ch: "═", // Double line for thicker border
2274
- },
2275
- });
2276
- // Cat face variants for animation
2277
- const catFaces = [
2278
- { name: "Sleepy", face: "[=^ -.- ^=]" },
2279
- { name: "Smug", face: "[=^‿^=]" },
2280
- { name: "Unhinged", face: "[=^◉_◉^=]" },
2281
- { name: "Judgy", face: "[=^ಠ‿ಠ^=]" },
2282
- { name: "Cute", face: "[=^。^=]" },
2283
- { name: "Menacing", face: "[=^>_<^=]" },
2284
- { name: "Loaf Mode", face: "[=^___^=]" },
2285
- { name: "Cosmic", face: "[=^✧_✧^=]" },
2286
- ];
2287
- let currentCatIndex = 0;
2288
- let catAnimationInterval = null;
2289
- // Helper function to build welcome content with account info - more compact
2290
- const buildWelcomeContent = async (theme, catFace) => {
2291
- const welcomeLines = [];
2292
- const colorTag = theme === "dark" ? "green-fg" : "black-fg";
2293
- const cyanColor = getCyanColor(theme);
2294
- // Use provided cat face or current one
2295
- const displayCatFace = catFace || catFaces[currentCatIndex].face;
2296
- // Cat face with breathing room, compact info below
2297
- welcomeLines.push(`{${colorTag}}${displayCatFace}{/${colorTag}}`);
2298
- welcomeLines.push(`{green-fg}🐱 Welcome to httpcat!{/green-fg} | {green-fg}🌐 {${cyanColor}}${network}{/${cyanColor}}{/green-fg}`);
2299
- // Get account info
2300
- let accountInfo = null;
2301
- try {
2302
- const accounts = config.getAllAccounts();
2303
- const activeIndex = config.getActiveAccountIndex();
2304
- const account = accounts.find((acc) => acc.index === activeIndex);
2305
- if (account) {
2306
- // Get balance info
2307
- try {
2308
- const privateKey = config.getAccountPrivateKey(activeIndex);
2309
- const balance = await checkBalance(privateKey, true); // silent mode
2310
- accountInfo = {
2311
- account,
2312
- balance,
2313
- };
2314
- }
2315
- catch (error) {
2316
- // If balance check fails, just show account info without balance
2317
- accountInfo = { account };
2318
- }
431
+ }
432
+ break;
433
+ case "chat":
434
+ {
435
+ const tokenIdentifier = args[0];
436
+ if (tokenIdentifier) {
437
+ log(chalk.cyan(`Joining chat for token: ${tokenIdentifier}...`));
438
+ }
439
+ else {
440
+ log(chalk.cyan("Joining general chat..."));
441
+ }
442
+ // Trigger transition to chat
443
+ if (onTransition) {
444
+ onTransition({ action: "chat", token: tokenIdentifier });
445
+ }
446
+ exit(); // Exit shell to trigger the transition
447
+ }
448
+ break;
449
+ default:
450
+ log(chalk.red(`Unknown command: ${cmd}`));
451
+ log(chalk.gray('Type "help" for available commands'));
2319
452
  }
2320
453
  }
2321
454
  catch (error) {
2322
- // If account info fails, continue without it
2323
- }
2324
- // Compact account info - combined on fewer lines
2325
- if (accountInfo) {
2326
- const { account, balance } = accountInfo;
2327
- const accountType = account.type === "custom" ? "Custom" : "Seed-Derived";
2328
- const accountLabel = account.label ? ` (${account.label})` : "";
2329
- if (balance) {
2330
- const ethDisplay = balance.ethFormatted || balance.ethBalance || "0 ETH";
2331
- const usdcDisplay = balance.usdcFormatted || balance.usdcBalance || "$0.00";
2332
- // Combine account and balance on one line to save space
2333
- welcomeLines.push(`{${cyanColor}}👤 Account #{green-fg}${account.index}{/green-fg} | {green-fg}${accountType}${accountLabel}{/green-fg} | 💰 {yellow-fg}${ethDisplay}{/yellow-fg} | {green-fg}${usdcDisplay}{/green-fg}{/${cyanColor}}`);
2334
- }
2335
- else {
2336
- welcomeLines.push(`{${cyanColor}}👤 Account #{green-fg}${account.index}{/green-fg} | {green-fg}${accountType}${accountLabel}{/green-fg}{/${cyanColor}}`);
2337
- }
2338
- }
2339
- return welcomeLines.join("\n");
2340
- };
2341
- // Set initial header content
2342
- buildWelcomeContent(currentTheme, catFaces[currentCatIndex].face).then((content) => {
2343
- headerBox.setContent(content);
2344
- screen.render();
2345
- });
2346
- // Start cat face animation (cycle through every minute)
2347
- const startCatAnimation = () => {
2348
- if (catAnimationInterval) {
2349
- clearInterval(catAnimationInterval);
2350
- }
2351
- catAnimationInterval = setInterval(() => {
2352
- currentCatIndex = (currentCatIndex + 1) % catFaces.length;
2353
- buildWelcomeContent(currentTheme, catFaces[currentCatIndex].face).then((content) => {
2354
- headerBox.setContent(content);
2355
- screen.render();
2356
- });
2357
- }, 60000); // Change every minute (60 seconds)
2358
- };
2359
- // Start animation
2360
- startCatAnimation();
2361
- // Create output log box (scrollable) with thick borders, transparent background
2362
- const outputBox = blessed.log({
2363
- top: 6, // Adjusted to match new header height
2364
- left: 0,
2365
- width: "100%",
2366
- bottom: 4, // Leave space for input box at bottom
2367
- tags: true,
2368
- scrollable: true,
2369
- alwaysScroll: true,
2370
- scrollbar: {
2371
- ch: " ",
2372
- inverse: currentTheme !== "dark",
2373
- },
2374
- style: {
2375
- fg: themeColors.fg,
2376
- bg: "default", // Transparent
2377
- border: {
2378
- fg: themeColors.border,
2379
- bold: true,
2380
- },
2381
- },
2382
- padding: {
2383
- left: 0,
2384
- right: 1,
2385
- },
2386
- mouse: true, // Enable mouse scrolling
2387
- border: {
2388
- type: "line",
2389
- fg: themeColors.border,
2390
- ch: "═", // Double line for thicker border
2391
- },
2392
- });
2393
- // Create prompt label with bold font (appears larger) - positioned inside input box
2394
- const promptLabel = blessed.text({
2395
- bottom: 1,
2396
- left: 2,
2397
- width: 8, // Exactly "httpcat>" (8 characters)
2398
- height: 1,
2399
- content: "",
2400
- tags: true,
2401
- style: {
2402
- fg: themeColors.fg,
2403
- bg: "default", // Transparent
2404
- bold: true,
2405
- },
2406
- });
2407
- // Helper to update prompt label content
2408
- const updatePromptLabel = (theme) => {
2409
- const colorTag = theme === "dark" ? "green-fg" : "black-fg";
2410
- promptLabel.content = `{${colorTag}}{bold}httpcat>{/bold}{/${colorTag}}`;
2411
- };
2412
- updatePromptLabel(currentTheme);
2413
- // Create input box with visible cursor and stylish border
2414
- const inputBox = blessed.textbox({
2415
- bottom: 0,
2416
- left: 0,
2417
- width: "100%",
2418
- height: 3,
2419
- inputOnFocus: true,
2420
- keys: true,
2421
- vi: false, // Disabled to prevent double input issues in agent mode
2422
- secret: false,
2423
- tags: true,
2424
- alwaysScroll: false,
2425
- scrollable: false,
2426
- padding: {
2427
- left: 10, // Space for "httpcat>" prompt (8 chars) + 2 for spacing
2428
- right: 1,
2429
- top: 0,
2430
- bottom: 0,
2431
- },
2432
- cursor: {
2433
- artificial: true,
2434
- shape: "block", // Block cursor is more visible than line
2435
- blink: true,
2436
- color: currentTheme === "dark" ? "green" : "black",
2437
- },
2438
- style: {
2439
- fg: themeColors.fg,
2440
- bg: "default",
2441
- border: {
2442
- fg: themeColors.border,
2443
- bold: true,
2444
- },
2445
- focus: {
2446
- fg: themeColors.fg,
2447
- bg: "default",
2448
- border: {
2449
- fg: themeColors.border,
2450
- bold: true,
2451
- },
2452
- },
2453
- },
2454
- border: {
2455
- type: "line",
2456
- fg: themeColors.border,
2457
- ch: "─", // Single line border
2458
- },
2459
- });
2460
- // Helper to update theme
2461
- const updateTheme = (newTheme) => {
2462
- currentTheme = newTheme;
2463
- themeColors = getThemeColors(currentTheme);
2464
- // Update screen cursor color
2465
- screen.cursor.color =
2466
- currentTheme === "dark" ? "green" : "black";
2467
- // Update header content with new theme colors (keep current cat face)
2468
- buildWelcomeContent(currentTheme, catFaces[currentCatIndex].face).then((content) => {
2469
- headerBox.setContent(content);
2470
- screen.render();
2471
- });
2472
- // Update all widget styles
2473
- headerBox.style.fg = themeColors.fg;
2474
- headerBox.style.bg = "default"; // Transparent
2475
- headerBox.border = { type: "line", fg: themeColors.border, ch: "═" };
2476
- outputBox.style.fg = themeColors.fg;
2477
- outputBox.style.bg = "default"; // Transparent
2478
- outputBox.scrollbar.inverse = currentTheme !== "dark";
2479
- outputBox.border = { type: "line", fg: themeColors.border, ch: "═" };
2480
- updatePromptLabel(currentTheme);
2481
- promptLabel.style.fg = themeColors.fg;
2482
- promptLabel.style.bg = "default"; // Transparent
2483
- promptLabel.style.bold = true;
2484
- // Update input box cursor, style, and border
2485
- inputBox.style.fg = themeColors.fg;
2486
- inputBox.style.bg = "default";
2487
- inputBox.style.focus.fg = themeColors.fg;
2488
- inputBox.style.focus.bg = "default";
2489
- inputBox.style.border.fg = themeColors.border;
2490
- inputBox.style.focus.border.fg = themeColors.border;
2491
- inputBox.border = { type: "line", fg: themeColors.border, ch: "─" };
2492
- if (inputBox.cursor) {
2493
- inputBox.cursor.color =
2494
- currentTheme === "dark" ? "green" : "black";
2495
- }
2496
- screen.cursor.color =
2497
- currentTheme === "dark" ? "green" : "black";
2498
- screen.render();
2499
- };
2500
- // Helper to log output (define before use)
2501
- const log = (text) => {
2502
- outputBox.log(text);
2503
- outputBox.setScrollPerc(100);
2504
- screen.render();
2505
- };
2506
- // Helper to log multiple lines
2507
- const logLines = (lines) => {
2508
- lines.forEach((line) => outputBox.log(line));
2509
- outputBox.setScrollPerc(100);
2510
- screen.render();
2511
- };
2512
- // Wrap toggleTheme to also log
2513
- const toggleThemeWithLog = () => {
2514
- const themes = ["win95", "dark", "light"];
2515
- const currentIndex = themes.indexOf(currentTheme);
2516
- const nextTheme = themes[(currentIndex + 1) % themes.length];
2517
- updateTheme(nextTheme);
2518
- log(chalk.blue(`Theme switched to: ${nextTheme}`));
2519
- };
2520
- // Append all widgets in correct z-order (last appended is on top)
2521
- screen.append(headerBox);
2522
- screen.append(outputBox);
2523
- screen.append(inputBox);
2524
- screen.append(promptLabel); // Prompt label on top so it's always visible
2525
- // Handle F1 for theme toggle
2526
- screen.key(["f1"], () => {
2527
- toggleThemeWithLog();
2528
- });
2529
- // Store toggle function for command handler
2530
- screen.toggleTheme = toggleThemeWithLog;
2531
- screen.updateTheme = updateTheme;
2532
- // Handle Ctrl+C - two-stage: first clears input if text exists, second quits
2533
- const handleCtrlC = () => {
2534
- const currentValue = inputBox.getValue();
2535
- // If there's text in the input, clear it instead of quitting
2536
- if (currentValue && currentValue.trim().length > 0) {
2537
- inputBox.clearValue();
2538
- screen.render();
2539
- return;
2540
- }
2541
- // No text in input, so quit
2542
- if (catAnimationInterval) {
2543
- clearInterval(catAnimationInterval);
2544
- catAnimationInterval = null;
455
+ log(chalk.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
2545
456
  }
2546
- screen.destroy();
2547
- printCat("sleeping");
2548
- console.log(chalk.cyan("Goodbye! 👋"));
2549
- process.exit(0);
457
+ log("");
2550
458
  };
2551
- // Handle Ctrl+C on screen level
2552
- screen.key(["C-c"], handleCtrlC);
2553
- // Handle Ctrl+C on input box level
2554
- inputBox.key(["C-c"], handleCtrlC);
2555
- // Focus input and render
2556
- inputBox.focus();
2557
- screen.render();
2558
- // Show welcome message with key commands on load
2559
- displayWelcomeMessage(log, logLines, outputBox, screen, currentTheme);
2560
- // Start agent chat mode
2561
- await startAgentChatMode(client, log, logLines, screen, inputBox, headerBox, buildWelcomeContent, currentTheme, catFaces, currentCatIndex);
459
+ // Calculate terminal height and component heights
460
+ // Subtract 3: 1 for top status bar, 1 for bottom status bar, 1 for safety margin
461
+ const terminalHeight = Math.max(20, (stdout?.rows || 24) - 3);
462
+ // Component heights (including borders):
463
+ // - ChatHeader: height={6} + 2 border lines = 8 total
464
+ // - ShellInput: height={3} includes border = 3 total
465
+ // - ShellOutput: flexGrow fills remaining, but needs viewport height for scrolling
466
+ const headerTotalHeight = 8;
467
+ const inputTotalHeight = 3;
468
+ // Output viewport height = terminal - header - input - output border
469
+ const outputContentHeight = Math.max(8, // minimum reasonable height
470
+ terminalHeight - headerTotalHeight - inputTotalHeight - 2 // -2 for output's top+bottom border
471
+ );
472
+ return (_jsxs(Box, { flexDirection: "column", height: terminalHeight, width: "100%", overflow: "hidden", children: [_jsx(Box, { flexShrink: 0, width: "100%", children: _jsx(ChatHeader, { network: network, account: account, ethBalance: ethBalance, usdcBalance: usdcBalance, agentStatus: "Interactive Shell" }) }), _jsx(ShellOutput, { lines: outputLines, viewportHeight: outputContentHeight }), _jsx(Box, { flexShrink: 0, width: "100%", children: _jsx(ShellInput, { onSubmit: handleCommand }) })] }));
2562
473
  }
2563
474
  /**
2564
- * Start agent chat mode (similar to startChatInShell)
475
+ * Start the interactive shell
476
+ * Backwards-compatible with the blessed version
2565
477
  */
2566
- async function startAgentChatMode(client, log, logLines, screen, inputBox, headerBox, buildWelcomeContent, currentTheme, catFaces, currentCatIndex) {
2567
- let isProcessing = false;
2568
- // Get agent config
2569
- const agentConfig = config.getAIAgentConfig();
2570
- if (!agentConfig) {
2571
- throw new Error("Cat not configured");
2572
- }
2573
- const apiKey = config.getAIAgentApiKey();
2574
- if (!apiKey) {
2575
- throw new Error("Failed to get API key");
2576
- }
2577
- // Create LLM and agent
2578
- const llm = createLLM(agentConfig, apiKey);
2579
- const agent = createHttpcatAgent(client, llm);
2580
- // Helper to update header
2581
- const updateAgentHeader = async () => {
2582
- const colorTag = currentTheme === "dark" ? "green-fg" : "black-fg";
2583
- const cyanColor = currentTheme === "dark" ? "light-cyan-fg" : "cyan-fg";
2584
- const lines = [];
2585
- lines.push(`{${colorTag}}🐱 Cat Chat Mode{/${colorTag}}`);
2586
- lines.push(`{${colorTag}}${"═".repeat(60)}{/${colorTag}}`);
2587
- lines.push("");
2588
- lines.push(`{${cyanColor}}💡 Ask me to buy tokens, check balances, list tokens, and more!{/${cyanColor}}`);
2589
- lines.push(`{${cyanColor}}💡 Type /exit to return to shell{/${cyanColor}}`);
2590
- lines.push("");
2591
- const content = lines.join("\n");
2592
- headerBox.setContent(content);
2593
- screen.render();
2594
- };
2595
- // Update header to show agent mode
2596
- await updateAgentHeader();
2597
- // Store original submit handler
2598
- const originalHandlers = inputBox.listeners("submit");
2599
- const originalSubmitHandler = originalHandlers[0];
2600
- // Set up agent input handler (similar to chat mode)
2601
- const agentInputHandler = async (value) => {
2602
- // Guard against double processing
2603
- if (isProcessing) {
2604
- return;
2605
- }
2606
- isProcessing = true;
2607
- const trimmed = value.trim();
2608
- // Clear input immediately
2609
- inputBox.clearValue();
2610
- screen.render();
2611
- if (!trimmed) {
2612
- isProcessing = false;
2613
- inputBox.focus();
2614
- screen.render();
2615
- return;
2616
- }
2617
- // Handle commands
2618
- if (trimmed.startsWith("/")) {
2619
- const [cmd] = trimmed.split(" ");
2620
- switch (cmd) {
2621
- case "/exit":
2622
- case "/quit":
2623
- screen.destroy();
2624
- printCat("sleeping");
2625
- console.log(chalk.cyan("Goodbye! 👋"));
2626
- process.exit(0);
2627
- return;
2628
- case "/help":
2629
- log(chalk.cyan("Cat commands: /exit, /quit, /help"));
2630
- isProcessing = false;
2631
- inputBox.focus();
2632
- screen.render();
2633
- return;
2634
- default:
2635
- log(chalk.yellow(`Unknown command: ${cmd}. Type /help for commands.`));
2636
- isProcessing = false;
2637
- inputBox.focus();
2638
- screen.render();
2639
- return;
2640
- }
2641
- }
2642
- // Send to agent
478
+ export async function startInteractiveShell(client, autoChatToken) {
479
+ // Clear screen and reset cursor to top-left
480
+ process.stdout.write("\x1Bc"); // Full reset (clears screen and scrollback)
481
+ const transitionRef = { current: null };
482
+ const { waitUntilExit } = render(_jsx(ThemeProvider, { children: _jsx(Shell, { client: client, autoChatToken: autoChatToken, onTransition: (transition) => {
483
+ transitionRef.current = transition;
484
+ } }) }));
485
+ await waitUntilExit();
486
+ // If there's a pending transition, handle it
487
+ const transition = transitionRef.current;
488
+ if (transition && transition.action === "chat") {
2643
489
  try {
2644
- // Get account address for session ID
2645
- const privateKey = config.getPrivateKey();
2646
- const account = privateKeyToAccount(privateKey);
2647
- const sessionId = account.address;
2648
- log(chalk.blue("🐱 Cat thinking..."));
2649
- screen.render();
2650
- const response = await chatWithAgent(agent, llm, trimmed, sessionId);
2651
- log(chalk.green("🐱 Cat:"));
2652
- log(response);
2653
- log("");
490
+ const { startChatStream } = await import("../commands/chat.js");
491
+ // Give the terminal a moment to clear after shell exit
492
+ await new Promise((resolve) => setTimeout(resolve, 100));
493
+ await startChatStream(client, false, // jsonMode
494
+ transition.token || undefined, "text", true // returnToShell
495
+ );
496
+ // Small delay before restarting shell
497
+ await new Promise((resolve) => setTimeout(resolve, 100));
498
+ // Restart shell after chat exits
499
+ await startInteractiveShell(client);
2654
500
  }
2655
501
  catch (error) {
2656
- const errorMsg = error.message || String(error);
2657
- const errorStack = error.stack || "";
2658
- // Log full error details for debugging
2659
- if (process.env.HTTPCAT_DEBUG) {
2660
- log(chalk.dim(`Debug: Full error: ${JSON.stringify(error, null, 2)}`));
2661
- if (errorStack) {
2662
- log(chalk.dim(`Debug: Stack trace: ${errorStack}`));
2663
- }
2664
- }
2665
- log(chalk.red(`❌ Agent error: ${errorMsg}`));
2666
- // Check for authentication errors
2667
- const isAuthError = errorMsg.includes("Authentication failed") ||
2668
- errorMsg.includes("API key") ||
2669
- errorMsg.includes("401") ||
2670
- errorMsg.includes("403") ||
2671
- errorMsg.includes("Unauthorized") ||
2672
- errorMsg.includes("Invalid API key") ||
2673
- errorMsg.includes("authentication") ||
2674
- errorMsg.includes("Invalid API Key") ||
2675
- errorStack.includes("401") ||
2676
- errorStack.includes("403");
2677
- if (isAuthError) {
2678
- log("");
2679
- log(chalk.yellow("🔑 Authentication Error Detected"));
2680
- log(chalk.dim("Your API key may be invalid, expired, or incorrectly configured."));
2681
- log(chalk.dim("Exit cat mode and run 'agent' (or 'cat') again to re-run the setup wizard."));
2682
- }
2683
- log("");
502
+ console.error("Chat error:", error);
503
+ // Restart shell even if chat fails
504
+ await startInteractiveShell(client);
2684
505
  }
2685
- isProcessing = false;
2686
- inputBox.focus();
2687
- screen.render();
2688
- };
2689
- // Remove original handler and add agent handler
2690
- inputBox.removeAllListeners("submit");
2691
- inputBox.on("submit", agentInputHandler);
2692
- // Clear and focus input
2693
- inputBox.clearValue();
2694
- inputBox.focus();
2695
- screen.render();
2696
- // Show welcome message
2697
- log(chalk.green("🐱 Cat chat mode activated!"));
2698
- log(chalk.dim("Ask me anything about tokens, trading, or your portfolio."));
2699
- log(chalk.dim("Type /exit to return to shell."));
2700
- log("");
506
+ }
507
+ }
508
+ /**
509
+ * Start agent interactive mode
510
+ * Legacy function for backwards compatibility
511
+ */
512
+ export async function startAgentInteractiveMode(client) {
513
+ // For now, just start the regular interactive shell
514
+ // This can be enhanced later with agent-specific features
515
+ await startInteractiveShell(client);
2701
516
  }
2702
517
  //# sourceMappingURL=shell.js.map