dexterm-cli 0.0.0-stage → 0.1.2

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 (377) hide show
  1. package/LICENSE +661 -0
  2. package/dist/packages/agents/src/audit.js +122 -0
  3. package/dist/packages/agents/src/bus.js +394 -0
  4. package/dist/packages/agents/src/executor.js +202 -0
  5. package/dist/packages/agents/src/gate.js +179 -0
  6. package/dist/packages/agents/src/identity.js +218 -0
  7. package/dist/packages/agents/src/index.js +15 -0
  8. package/dist/packages/agents/src/proposals.js +153 -0
  9. package/dist/packages/agents/src/schema.js +87 -0
  10. package/dist/packages/agents/src/sdk.js +140 -0
  11. package/dist/packages/agents/src/tiers.js +119 -0
  12. package/dist/packages/agents/src/tools.js +72 -0
  13. package/dist/packages/agents/src/ws.js +158 -0
  14. package/dist/packages/cli/assets/claude/analyst-system.md +76 -0
  15. package/dist/packages/cli/assets/claude/plugin/.claude-plugin/plugin.json +5 -0
  16. package/dist/packages/cli/assets/claude/plugin/skills/REGISTRY.json +16 -0
  17. package/dist/packages/cli/assets/claude/plugin/skills/larp-check/SKILL.md +75 -0
  18. package/dist/packages/cli/assets/claude/plugin/skills/rug-check/SKILL.md +74 -0
  19. package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +63 -0
  20. package/dist/packages/cli/assets/claude/plugin/skills/trench-scan/SKILL.md +44 -0
  21. package/dist/packages/cli/assets/claude/schemas/larp.schema.json +117 -0
  22. package/dist/packages/cli/assets/claude/schemas/trench-scan.schema.json +71 -0
  23. package/dist/packages/cli/assets/claude/schemas/verdict.schema.json +115 -0
  24. package/dist/packages/cli/bin/dexterm.mjs +11 -0
  25. package/dist/packages/cli/src/agent-cmd.js +336 -0
  26. package/dist/packages/cli/src/agent-roster.js +26 -0
  27. package/dist/packages/cli/src/alerts/adapters/desktop.js +40 -0
  28. package/dist/packages/cli/src/alerts/adapters/discord.js +36 -0
  29. package/dist/packages/cli/src/alerts/adapters/telegram.js +43 -0
  30. package/dist/packages/cli/src/alerts/adapters/toast.js +16 -0
  31. package/dist/packages/cli/src/alerts/creds.js +83 -0
  32. package/dist/packages/cli/src/alerts/format.js +41 -0
  33. package/dist/packages/cli/src/alerts/index.js +10 -0
  34. package/dist/packages/cli/src/alerts/notifier.js +53 -0
  35. package/dist/packages/cli/src/alerts/route.js +19 -0
  36. package/dist/packages/cli/src/alerts/types.js +1 -0
  37. package/dist/packages/cli/src/alerts-cmd.js +212 -0
  38. package/dist/packages/cli/src/analyst/backend.js +526 -0
  39. package/dist/packages/cli/src/analyst/results.js +38 -0
  40. package/dist/packages/cli/src/analyst/session.js +319 -0
  41. package/dist/packages/cli/src/audit.js +51 -0
  42. package/dist/packages/cli/src/backtest-cmd.js +208 -0
  43. package/dist/packages/cli/src/boot-state.js +62 -0
  44. package/dist/packages/cli/src/coin-target.js +2 -0
  45. package/dist/packages/cli/src/custom.js +261 -0
  46. package/dist/packages/cli/src/display.js +191 -0
  47. package/dist/packages/cli/src/doctor.js +375 -0
  48. package/dist/packages/cli/src/export.js +235 -0
  49. package/dist/packages/cli/src/feed.js +634 -0
  50. package/dist/packages/cli/src/fixtures.js +275 -0
  51. package/dist/packages/cli/src/follow-cmd.js +221 -0
  52. package/dist/packages/cli/src/images-config.js +59 -0
  53. package/dist/packages/cli/src/index.js +31 -0
  54. package/dist/packages/cli/src/kb-skill-cmd.js +240 -0
  55. package/dist/packages/cli/src/larp-record.js +21 -0
  56. package/dist/packages/cli/src/live/copy.js +80 -0
  57. package/dist/packages/cli/src/live/flow.js +233 -0
  58. package/dist/packages/cli/src/live/index.js +511 -0
  59. package/dist/packages/cli/src/live/intel.js +227 -0
  60. package/dist/packages/cli/src/live/newpairs.js +334 -0
  61. package/dist/packages/cli/src/live/screens/newpairs-live.js +219 -0
  62. package/dist/packages/cli/src/live/screens/token-live.js +459 -0
  63. package/dist/packages/cli/src/live/screens/watchlist-live.js +149 -0
  64. package/dist/packages/cli/src/live/token.js +585 -0
  65. package/dist/packages/cli/src/live/trade.js +929 -0
  66. package/dist/packages/cli/src/live/watchlist.js +178 -0
  67. package/dist/packages/cli/src/main.js +5785 -0
  68. package/dist/packages/cli/src/mcp-cmd.js +326 -0
  69. package/dist/packages/cli/src/narrative-cmd.js +76 -0
  70. package/dist/packages/cli/src/pack-cmd.js +323 -0
  71. package/dist/packages/cli/src/plan-cmd.js +278 -0
  72. package/dist/packages/cli/src/popup.js +810 -0
  73. package/dist/packages/cli/src/rules-cmd.js +131 -0
  74. package/dist/packages/cli/src/rules.js +358 -0
  75. package/dist/packages/cli/src/scan/command.js +57 -0
  76. package/dist/packages/cli/src/scan/engine.js +419 -0
  77. package/dist/packages/cli/src/scan/observe.js +64 -0
  78. package/dist/packages/cli/src/scan/ui.js +98 -0
  79. package/dist/packages/cli/src/screener/filter.js +241 -0
  80. package/dist/packages/cli/src/screener/presets.js +80 -0
  81. package/dist/packages/cli/src/screener/rows.js +146 -0
  82. package/dist/packages/cli/src/screener/rug.js +50 -0
  83. package/dist/packages/cli/src/screener/sort.js +52 -0
  84. package/dist/packages/cli/src/screener/view.js +183 -0
  85. package/dist/packages/cli/src/screens/analyst-session.js +343 -0
  86. package/dist/packages/cli/src/screens/analyst.js +326 -0
  87. package/dist/packages/cli/src/screens/boot.js +74 -0
  88. package/dist/packages/cli/src/screens/bubbles.js +255 -0
  89. package/dist/packages/cli/src/screens/coin.js +33 -0
  90. package/dist/packages/cli/src/screens/follows.js +201 -0
  91. package/dist/packages/cli/src/screens/kol.js +90 -0
  92. package/dist/packages/cli/src/screens/narrative.js +90 -0
  93. package/dist/packages/cli/src/screens/newpairs.js +150 -0
  94. package/dist/packages/cli/src/screens/overlay.js +257 -0
  95. package/dist/packages/cli/src/screens/plans.js +126 -0
  96. package/dist/packages/cli/src/screens/positions.js +176 -0
  97. package/dist/packages/cli/src/screens/rules.js +170 -0
  98. package/dist/packages/cli/src/screens/screener.js +459 -0
  99. package/dist/packages/cli/src/screens/social-panel.js +107 -0
  100. package/dist/packages/cli/src/screens/ticket.js +342 -0
  101. package/dist/packages/cli/src/screens/token.js +312 -0
  102. package/dist/packages/cli/src/screens/trending.js +181 -0
  103. package/dist/packages/cli/src/screens/watchlist.js +104 -0
  104. package/dist/packages/cli/src/skill-eval-build.js +109 -0
  105. package/dist/packages/cli/src/social/demo.js +96 -0
  106. package/dist/packages/cli/src/social/view.js +250 -0
  107. package/dist/packages/cli/src/state.js +463 -0
  108. package/dist/packages/cli/src/theme-cmd.js +126 -0
  109. package/dist/packages/cli/src/trade.js +319 -0
  110. package/dist/packages/cli/src/update.js +294 -0
  111. package/dist/packages/cli/src/version.js +3 -0
  112. package/dist/packages/cli/src/voice-cmd.js +119 -0
  113. package/dist/packages/cli/src/voice-hud.js +42 -0
  114. package/dist/packages/cli/src/wallet-cmd.js +440 -0
  115. package/dist/packages/cli/src/wallet-command.js +31 -0
  116. package/dist/packages/cli/src/watch-cmd.js +190 -0
  117. package/dist/packages/cli/src/widgets.js +208 -0
  118. package/dist/packages/cli/src/workspace-cmd.js +68 -0
  119. package/dist/packages/cli/src/x-cmd.js +154 -0
  120. package/dist/packages/custom/src/config.js +213 -0
  121. package/dist/packages/custom/src/index.js +10 -0
  122. package/dist/packages/custom/src/keymap.js +107 -0
  123. package/dist/packages/custom/src/layout.js +235 -0
  124. package/dist/packages/custom/src/packs.js +633 -0
  125. package/dist/packages/custom/src/rules.js +704 -0
  126. package/dist/packages/custom/src/schema.js +538 -0
  127. package/dist/packages/custom/src/themes.js +176 -0
  128. package/dist/packages/custom/src/widget-html.js +222 -0
  129. package/dist/packages/custom/src/workspace.js +141 -0
  130. package/dist/packages/data/src/analyst-actions.js +133 -0
  131. package/dist/packages/data/src/budget.js +88 -0
  132. package/dist/packages/data/src/cache.js +142 -0
  133. package/dist/packages/data/src/candles.js +168 -0
  134. package/dist/packages/data/src/clock.js +64 -0
  135. package/dist/packages/data/src/coin-target.js +82 -0
  136. package/dist/packages/data/src/credits.js +137 -0
  137. package/dist/packages/data/src/detail.js +97 -0
  138. package/dist/packages/data/src/fast-lane.js +321 -0
  139. package/dist/packages/data/src/fixtures.js +135 -0
  140. package/dist/packages/data/src/health.js +245 -0
  141. package/dist/packages/data/src/http.js +277 -0
  142. package/dist/packages/data/src/images/cache.js +141 -0
  143. package/dist/packages/data/src/images/decode-worker.js +28 -0
  144. package/dist/packages/data/src/images/decode.js +81 -0
  145. package/dist/packages/data/src/images/fetch.js +162 -0
  146. package/dist/packages/data/src/images/gif.js +214 -0
  147. package/dist/packages/data/src/images/index.js +10 -0
  148. package/dist/packages/data/src/images/jpeg.js +544 -0
  149. package/dist/packages/data/src/images/png.js +310 -0
  150. package/dist/packages/data/src/images/service.js +334 -0
  151. package/dist/packages/data/src/images/types.js +27 -0
  152. package/dist/packages/data/src/index.js +211 -0
  153. package/dist/packages/data/src/intel/helius-history.js +70 -0
  154. package/dist/packages/data/src/intel/holder-class.js +125 -0
  155. package/dist/packages/data/src/intel/rules.js +421 -0
  156. package/dist/packages/data/src/intel/scan.js +170 -0
  157. package/dist/packages/data/src/larp/evidence.js +288 -0
  158. package/dist/packages/data/src/larp/link-baseline.js +196 -0
  159. package/dist/packages/data/src/larp/links.js +137 -0
  160. package/dist/packages/data/src/larp/robots.js +90 -0
  161. package/dist/packages/data/src/larp/safe-fetch.js +389 -0
  162. package/dist/packages/data/src/limiter.js +341 -0
  163. package/dist/packages/data/src/model.js +12 -0
  164. package/dist/packages/data/src/paper.js +331 -0
  165. package/dist/packages/data/src/rpc-config.js +126 -0
  166. package/dist/packages/data/src/rpc-pool.js +166 -0
  167. package/dist/packages/data/src/social/chain.js +83 -0
  168. package/dist/packages/data/src/social/dedupe.js +205 -0
  169. package/dist/packages/data/src/social/feed.js +462 -0
  170. package/dist/packages/data/src/social/firewall.js +110 -0
  171. package/dist/packages/data/src/social/index.js +11 -0
  172. package/dist/packages/data/src/social/kol.js +269 -0
  173. package/dist/packages/data/src/social/model.js +11 -0
  174. package/dist/packages/data/src/social/rank.js +184 -0
  175. package/dist/packages/data/src/social/rss.js +113 -0
  176. package/dist/packages/data/src/social/summary.js +104 -0
  177. package/dist/packages/data/src/social/venues.js +186 -0
  178. package/dist/packages/data/src/sources/dexscreener.js +184 -0
  179. package/dist/packages/data/src/sources/geckoterminal.js +133 -0
  180. package/dist/packages/data/src/sources/goplus.js +161 -0
  181. package/dist/packages/data/src/sources/jupiter-swap.js +236 -0
  182. package/dist/packages/data/src/sources/jupiter.js +198 -0
  183. package/dist/packages/data/src/sources/pda.js +113 -0
  184. package/dist/packages/data/src/sources/pump-curve.js +58 -0
  185. package/dist/packages/data/src/sources/pumpportal.js +252 -0
  186. package/dist/packages/data/src/sources/rugcheck.js +71 -0
  187. package/dist/packages/data/src/sources/solana-rpc.js +299 -0
  188. package/dist/packages/data/src/sources/solana-ws.js +287 -0
  189. package/dist/packages/data/src/store.js +289 -0
  190. package/dist/packages/data/src/trade/bridge.js +464 -0
  191. package/dist/packages/data/src/trade/caps.js +323 -0
  192. package/dist/packages/data/src/trade/copy/auto.js +385 -0
  193. package/dist/packages/data/src/trade/copy/follows.js +103 -0
  194. package/dist/packages/data/src/trade/copy/index.js +9 -0
  195. package/dist/packages/data/src/trade/copy/mirror.js +91 -0
  196. package/dist/packages/data/src/trade/copy/stats.js +120 -0
  197. package/dist/packages/data/src/trade/copy/tracker.js +158 -0
  198. package/dist/packages/data/src/trade/copy/trades.js +87 -0
  199. package/dist/packages/data/src/trade/copy/types.js +6 -0
  200. package/dist/packages/data/src/trade/executor.js +933 -0
  201. package/dist/packages/data/src/trade/kill-switch.js +47 -0
  202. package/dist/packages/data/src/trade/link-store.js +52 -0
  203. package/dist/packages/data/src/trade/plans/confirm.js +157 -0
  204. package/dist/packages/data/src/trade/plans/engine.js +784 -0
  205. package/dist/packages/data/src/trade/plans/executor-live.js +220 -0
  206. package/dist/packages/data/src/trade/plans/executor-paper.js +44 -0
  207. package/dist/packages/data/src/trade/plans/executor.js +11 -0
  208. package/dist/packages/data/src/trade/plans/index.js +12 -0
  209. package/dist/packages/data/src/trade/plans/journal.js +77 -0
  210. package/dist/packages/data/src/trade/plans/mandate.js +129 -0
  211. package/dist/packages/data/src/trade/plans/plan.js +179 -0
  212. package/dist/packages/data/src/trade/plans/service.js +168 -0
  213. package/dist/packages/data/src/trade/plans/types.js +6 -0
  214. package/dist/packages/data/src/trade/rpc-exec.js +229 -0
  215. package/dist/packages/data/src/trade/signer/client.js +197 -0
  216. package/dist/packages/data/src/trade/signer/fast-wallet.js +154 -0
  217. package/dist/packages/data/src/trade/signer/host.js +97 -0
  218. package/dist/packages/data/src/trade/signer/main.js +121 -0
  219. package/dist/packages/data/src/trade/signer/probe-builder.js +85 -0
  220. package/dist/packages/data/src/trade/signer/protocol.js +92 -0
  221. package/dist/packages/data/src/trade/signer/service.js +400 -0
  222. package/dist/packages/data/src/trade/signer/sol-usd-feed.js +98 -0
  223. package/dist/packages/data/src/trade/sol-usd.js +86 -0
  224. package/dist/packages/data/src/trade/swap-build.js +108 -0
  225. package/dist/packages/data/src/trade/token-delta.js +201 -0
  226. package/dist/packages/data/src/trade/tx-decode.js +632 -0
  227. package/dist/packages/data/src/util.js +24 -0
  228. package/dist/packages/data/src/watchlist.js +176 -0
  229. package/dist/packages/data/src/x/api.js +256 -0
  230. package/dist/packages/data/src/x/budget.js +66 -0
  231. package/dist/packages/data/src/x/config.js +49 -0
  232. package/dist/packages/data/src/x/index.js +10 -0
  233. package/dist/packages/data/src/x/mock-server.js +217 -0
  234. package/dist/packages/data/src/x/oauth.js +242 -0
  235. package/dist/packages/data/src/x/pkce.js +21 -0
  236. package/dist/packages/data/src/x/source.js +285 -0
  237. package/dist/packages/data/src/x/token-store.js +134 -0
  238. package/dist/packages/data/src/x/transport.js +71 -0
  239. package/dist/packages/data/vendor/solana-web3/LICENSE +20 -0
  240. package/dist/packages/data/vendor/solana-web3/NOTICE.md +13 -0
  241. package/dist/packages/data/vendor/solana-web3/index.iife.min.js +20 -0
  242. package/dist/packages/mcp/assets/cases.json +1194 -0
  243. package/dist/packages/mcp/assets/kb/entries/cabal-and-coordinated-launches.md +28 -0
  244. package/dist/packages/mcp/assets/kb/entries/common-rug-patterns.md +40 -0
  245. package/dist/packages/mcp/assets/kb/entries/dbc-mechanics.md +30 -0
  246. package/dist/packages/mcp/assets/kb/entries/dev-wallet-behaviour.md +33 -0
  247. package/dist/packages/mcp/assets/kb/entries/glossary.md +35 -0
  248. package/dist/packages/mcp/assets/kb/entries/holder-concentration.md +30 -0
  249. package/dist/packages/mcp/assets/kb/entries/jito-bundles.md +29 -0
  250. package/dist/packages/mcp/assets/kb/entries/launchlab-curve.md +31 -0
  251. package/dist/packages/mcp/assets/kb/entries/lp-burn-vs-lock.md +31 -0
  252. package/dist/packages/mcp/assets/kb/entries/metadata-and-copycats.md +29 -0
  253. package/dist/packages/mcp/assets/kb/entries/mint-freeze-authority.md +31 -0
  254. package/dist/packages/mcp/assets/kb/entries/narrative-lifecycle.md +32 -0
  255. package/dist/packages/mcp/assets/kb/entries/pump-bonding-curve.md +33 -0
  256. package/dist/packages/mcp/assets/kb/entries/pumpswap-migration.md +29 -0
  257. package/dist/packages/mcp/assets/kb/entries/sniping-and-snipers.md +29 -0
  258. package/dist/packages/mcp/assets/kb/entries/token2022-extensions-risk.md +32 -0
  259. package/dist/packages/mcp/assets/kb/entries/wash-trading-fake-volume.md +33 -0
  260. package/dist/packages/mcp/assets/skills/rug-check.md +52 -0
  261. package/dist/packages/mcp/assets/skills/token-dossier.md +57 -0
  262. package/dist/packages/mcp/assets/skills/trench-scan.md +40 -0
  263. package/dist/packages/mcp/bin/dexterm-mcp.mjs +16 -0
  264. package/dist/packages/mcp/src/dossier.js +78 -0
  265. package/dist/packages/mcp/src/firewall.js +21 -0
  266. package/dist/packages/mcp/src/index.js +22 -0
  267. package/dist/packages/mcp/src/server.js +936 -0
  268. package/dist/packages/mcp/src/user-content.js +434 -0
  269. package/dist/packages/packs/src/backtest.js +183 -0
  270. package/dist/packages/packs/src/digest.js +95 -0
  271. package/dist/packages/packs/src/firewall.js +57 -0
  272. package/dist/packages/packs/src/index.js +20 -0
  273. package/dist/packages/packs/src/larp-eval.js +100 -0
  274. package/dist/packages/packs/src/narrative.js +309 -0
  275. package/dist/packages/packs/src/reason.js +135 -0
  276. package/dist/packages/packs/src/rug-model.js +137 -0
  277. package/dist/packages/packs/src/schema.js +54 -0
  278. package/dist/packages/packs/src/skill-backtest.js +300 -0
  279. package/dist/packages/packs/src/token-dossier.js +174 -0
  280. package/dist/packages/packs/src/verdicts.js +17 -0
  281. package/dist/packages/packs/src/wallet-dossier.js +105 -0
  282. package/dist/packages/popup/src/config.js +71 -0
  283. package/dist/packages/popup/src/dock.js +271 -0
  284. package/dist/packages/popup/src/host.js +210 -0
  285. package/dist/packages/popup/src/index.js +12 -0
  286. package/dist/packages/popup/src/loopback.js +53 -0
  287. package/dist/packages/popup/src/open.js +183 -0
  288. package/dist/packages/popup/src/pages/bubbles-lib.js +160 -0
  289. package/dist/packages/popup/src/pages/bubbles.css +23 -0
  290. package/dist/packages/popup/src/pages/bubbles.html +36 -0
  291. package/dist/packages/popup/src/pages/bubbles.js +214 -0
  292. package/dist/packages/popup/src/pages/chart-lib.js +262 -0
  293. package/dist/packages/popup/src/pages/chart.css +50 -0
  294. package/dist/packages/popup/src/pages/chart.html +56 -0
  295. package/dist/packages/popup/src/pages/chart.js +554 -0
  296. package/dist/packages/popup/src/pages/common.css +34 -0
  297. package/dist/packages/popup/src/pages/common.js +187 -0
  298. package/dist/packages/popup/src/pages/feeds.html +20 -0
  299. package/dist/packages/popup/src/pages/feeds.js +77 -0
  300. package/dist/packages/popup/src/pages/narrative.html +20 -0
  301. package/dist/packages/popup/src/pages/narrative.js +61 -0
  302. package/dist/packages/popup/src/pages/plans.css +30 -0
  303. package/dist/packages/popup/src/pages/plans.html +26 -0
  304. package/dist/packages/popup/src/pages/plans.js +82 -0
  305. package/dist/packages/popup/src/pages/roster.css +41 -0
  306. package/dist/packages/popup/src/pages/roster.html +25 -0
  307. package/dist/packages/popup/src/pages/roster.js +75 -0
  308. package/dist/packages/popup/src/pages/token.html +20 -0
  309. package/dist/packages/popup/src/pages/token.js +112 -0
  310. package/dist/packages/popup/src/pages/widget-sdk.js +224 -0
  311. package/dist/packages/popup/src/pages/widget.css +5 -0
  312. package/dist/packages/popup/src/pages/widget.html +21 -0
  313. package/dist/packages/popup/src/pages/widget.js +85 -0
  314. package/dist/packages/popup/src/protocol.js +31 -0
  315. package/dist/packages/popup/src/selftest.js +156 -0
  316. package/dist/packages/popup/src/server.js +582 -0
  317. package/dist/packages/popup/src/sinkproxy.js +167 -0
  318. package/dist/packages/popup/src/widget-html.js +231 -0
  319. package/dist/packages/popup/vendor/lightweight-charts/LICENSE +201 -0
  320. package/dist/packages/popup/vendor/lightweight-charts/NOTICE +2 -0
  321. package/dist/packages/popup/vendor/lightweight-charts/README.md +30 -0
  322. package/dist/packages/popup/vendor/lightweight-charts/VERSION +1 -0
  323. package/dist/packages/popup/vendor/lightweight-charts/lightweight-charts.standalone.production.js +7 -0
  324. package/dist/packages/recorder/bin/dexterm-recorder.mjs +11 -0
  325. package/dist/packages/recorder/src/cli.js +113 -0
  326. package/dist/packages/recorder/src/export.js +71 -0
  327. package/dist/packages/recorder/src/index.js +20 -0
  328. package/dist/packages/recorder/src/labels.js +151 -0
  329. package/dist/packages/recorder/src/open.js +29 -0
  330. package/dist/packages/recorder/src/recorder.js +553 -0
  331. package/dist/packages/recorder/src/schedule.js +43 -0
  332. package/dist/packages/recorder/src/schema.js +135 -0
  333. package/dist/packages/recorder/src/snapshot.js +191 -0
  334. package/dist/packages/recorder/src/sweep.js +52 -0
  335. package/dist/packages/recorder/src/verdicts.js +52 -0
  336. package/dist/packages/tui/src/bubbles.js +415 -0
  337. package/dist/packages/tui/src/canvas.js +370 -0
  338. package/dist/packages/tui/src/chrome.js +85 -0
  339. package/dist/packages/tui/src/color.js +133 -0
  340. package/dist/packages/tui/src/flush.js +62 -0
  341. package/dist/packages/tui/src/fmt.js +188 -0
  342. package/dist/packages/tui/src/fx-trade.js +278 -0
  343. package/dist/packages/tui/src/fx.js +527 -0
  344. package/dist/packages/tui/src/images/blocks.js +141 -0
  345. package/dist/packages/tui/src/images/coin.js +22 -0
  346. package/dist/packages/tui/src/images/detect.js +140 -0
  347. package/dist/packages/tui/src/images/index.js +7 -0
  348. package/dist/packages/tui/src/images/layer.js +146 -0
  349. package/dist/packages/tui/src/images/pixels.js +157 -0
  350. package/dist/packages/tui/src/images/protocols.js +87 -0
  351. package/dist/packages/tui/src/index.js +18 -0
  352. package/dist/packages/tui/src/keymap.js +357 -0
  353. package/dist/packages/tui/src/layout.js +11 -0
  354. package/dist/packages/tui/src/raster-font.js +195 -0
  355. package/dist/packages/tui/src/raster.js +213 -0
  356. package/dist/packages/tui/src/sound.js +177 -0
  357. package/dist/packages/tui/src/theme.js +182 -0
  358. package/dist/packages/tui/src/widgets.js +598 -0
  359. package/dist/packages/voice/src/controller.js +407 -0
  360. package/dist/packages/voice/src/doctor.js +131 -0
  361. package/dist/packages/voice/src/download.js +184 -0
  362. package/dist/packages/voice/src/engines.js +84 -0
  363. package/dist/packages/voice/src/index.js +20 -0
  364. package/dist/packages/voice/src/router.js +502 -0
  365. package/dist/packages/voice/src/stt/apple-helper.swift.js +103 -0
  366. package/dist/packages/voice/src/stt/apple.js +179 -0
  367. package/dist/packages/voice/src/stt/fake.js +34 -0
  368. package/dist/packages/voice/src/stt/whisper.js +112 -0
  369. package/dist/packages/voice/src/summary.js +44 -0
  370. package/dist/packages/voice/src/tts/espeak.js +131 -0
  371. package/dist/packages/voice/src/tts/fake.js +30 -0
  372. package/dist/packages/voice/src/tts/piper.js +206 -0
  373. package/dist/packages/voice/src/tts/say.js +132 -0
  374. package/dist/packages/voice/src/tts/voices.js +206 -0
  375. package/dist/packages/voice/src/types.js +3 -0
  376. package/package.json +38 -4
  377. package/README.md +0 -3
@@ -0,0 +1,90 @@
1
+ // Conservative robots policy for analyst page reads, never a bypass for blocked pages.
2
+ import { safeFetch, SsrfError } from "./safe-fetch.js";
3
+ // Iterative wildcard matching avoids regex backtracking on attacker-owned robots.
4
+ function pathMatches(pattern, target) {
5
+ const unreserved = (s) => s.replace(/%([a-f0-9]{2})/gi, (m, hex) => {
6
+ const ch = String.fromCharCode(parseInt(hex, 16));
7
+ return /[a-z0-9._~-]/i.test(ch) ? ch : m.toUpperCase();
8
+ });
9
+ pattern = unreserved(pattern);
10
+ target = unreserved(target);
11
+ const exact = pattern.endsWith('$');
12
+ const p = (exact ? pattern.slice(0, -1) : pattern + '*');
13
+ let i = 0, j = 0, star = -1, retry = 0;
14
+ while (j < target.length) {
15
+ if (p[i] === '*') {
16
+ star = i++;
17
+ retry = j;
18
+ }
19
+ else if (p[i] === target[j]) {
20
+ i++;
21
+ j++;
22
+ }
23
+ else if (star >= 0) {
24
+ i = star + 1;
25
+ j = ++retry;
26
+ }
27
+ else
28
+ return false;
29
+ }
30
+ while (p[i] === '*')
31
+ i++;
32
+ return i === p.length;
33
+ }
34
+ /** RFC-style product groups and longest-path allow/disallow matching. */
35
+ export function robotsAllows(text, url) {
36
+ const groups = [];
37
+ let group = { agents: [], rules: [] };
38
+ for (const line of text.split(/\r?\n/)) {
39
+ const m = /^\s*([\w-]+)\s*:\s*(.*?)\s*$/.exec(line.split('#')[0]);
40
+ if (!m)
41
+ continue;
42
+ const key = m[1].toLowerCase(), value = m[2];
43
+ if (key === 'user-agent') {
44
+ if (group.rules.length) {
45
+ groups.push(group);
46
+ group = { agents: [], rules: [] };
47
+ }
48
+ group.agents.push(value.toLowerCase());
49
+ }
50
+ else if (group.agents.length && (key === 'allow' || key === 'disallow') && value) {
51
+ group.rules.push({ allow: key === 'allow', path: value });
52
+ }
53
+ }
54
+ groups.push(group);
55
+ const specific = groups.filter((g) => g.agents.includes('dexterm'));
56
+ const selected = specific.length ? specific : groups.filter((g) => g.agents.includes('*'));
57
+ let best = -1, allowed = true;
58
+ for (const rule of selected.flatMap((g) => g.rules)) {
59
+ if (pathMatches(rule.path, url.pathname + url.search)) {
60
+ const length = rule.path.replaceAll('*', '').length;
61
+ if (length > best || (length === best && rule.allow)) {
62
+ best = length;
63
+ allowed = rule.allow;
64
+ }
65
+ }
66
+ }
67
+ return allowed;
68
+ }
69
+ export async function fetchPage(url, options = {}) {
70
+ // Per-operation cache only: no persistent untrusted state, and every origin
71
+ // reached by a redirect gets its own robots check through the SSRF transport.
72
+ const robots = new Map();
73
+ return safeFetch(url, { ...options, beforeRequest: async (target) => {
74
+ let result = robots.get(target.origin);
75
+ if (!result) {
76
+ result = await safeFetch(new URL('/robots.txt', target).href, { ...options, beforeRequest: undefined, maxBytes: 64 * 1024, timeoutMs: 3000 });
77
+ robots.set(target.origin, result);
78
+ }
79
+ if ([404, 410].includes(result.status) && !result.blocked)
80
+ return;
81
+ if (!result.ok || result.blocked || result.truncated)
82
+ throw new SsrfError('robots unavailable; page not fetched');
83
+ // A long requested crawl delay cannot fit this interactive tool's budget.
84
+ // Decline the page rather than quietly violate it (even for another group).
85
+ if ([...result.body.matchAll(/^\s*crawl-delay\s*:\s*([\d.]+)/gim)].some((m) => Number(m[1]) > 1))
86
+ throw new SsrfError('robots crawl-delay exceeds interactive budget; page not fetched');
87
+ if (!robotsAllows(result.body, target))
88
+ throw new SsrfError('robots disallow; page not fetched');
89
+ } });
90
+ }
@@ -0,0 +1,389 @@
1
+ // LARP detector — SSRF-safe, read-only HTTP GET for untrusted coin sites (task-1315).
2
+ // Rules (card constraints): http/https only; GET only; no cookies, no auth, no
3
+ // credentials; resolve every host and refuse private/loopback/link-local/CGNAT/
4
+ // multicast/reserved IP ranges (IPv4 + IPv6, including IPv4-mapped and NAT64);
5
+ // follow redirects MANUALLY, re-validating each Location so a public URL cannot
6
+ // bounce into a private one; hard size and time caps across the whole redirect
7
+ // chain. DNS and fetch are injectable so the policy is unit-testable offline.
8
+ //
9
+ // The native transport validates DNS in its socket lookup callback and connects
10
+ // to that exact address. It never performs a second, unvalidated DNS resolution.
11
+ import { lookup as dnsLookup } from 'node:dns';
12
+ import { promisify } from 'node:util';
13
+ import { request as httpRequest } from 'node:http';
14
+ import { request as httpsRequest } from 'node:https';
15
+ import { isIP } from 'node:net';
16
+ import { Readable } from 'node:stream';
17
+ const lookupAll = promisify(dnsLookup);
18
+ export class SsrfError extends Error {
19
+ reason;
20
+ constructor(reason) { super(reason); this.name = 'SsrfError'; this.reason = reason; }
21
+ }
22
+ const DEFAULTS = { maxBytes: 512 * 1024, timeoutMs: 8000, maxRedirects: 4 };
23
+ // Textual content types we will decode. The `[\w.-]+\+(json|xml)` arm covers RFC 6839 structured
24
+ // suffixes — notably `application/rdap+json`, which every authoritative RDAP server returns (check_domain
25
+ // got an empty body without it) — plus the general `…+xml` family.
26
+ const TEXTUAL = /^(text\/|application\/(json|xml|xhtml|rss|atom|javascript|ld\+json|[\w.-]+\+(json|xml))|image\/svg)/i;
27
+ /** Parse an IPv4 dotted-quad to its 4 octets, or null if it is not one. */
28
+ function ipv4Octets(ip) {
29
+ const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(ip);
30
+ if (!m)
31
+ return null;
32
+ const o = [Number(m[1]), Number(m[2]), Number(m[3]), Number(m[4])];
33
+ return o.every((x) => x >= 0 && x <= 255) ? o : null;
34
+ }
35
+ /** Return a reason string if the IPv4 address is in a blocked range, else null. */
36
+ function blockedIpv4(ip) {
37
+ const o = ipv4Octets(ip);
38
+ if (!o)
39
+ return null;
40
+ const [a, b] = o;
41
+ if (a === 0)
42
+ return 'this-network (0.0.0.0/8)';
43
+ if (a === 10)
44
+ return 'private (10.0.0.0/8)';
45
+ if (a === 127)
46
+ return 'loopback (127.0.0.0/8)';
47
+ if (a === 100 && b >= 64 && b <= 127)
48
+ return 'CGNAT (100.64.0.0/10)';
49
+ if (a === 169 && b === 254)
50
+ return 'link-local (169.254.0.0/16)';
51
+ if (a === 172 && b >= 16 && b <= 31)
52
+ return 'private (172.16.0.0/12)';
53
+ if (a === 192 && b === 168)
54
+ return 'private (192.168.0.0/16)';
55
+ if (a === 192 && b === 0 && o[2] === 0)
56
+ return 'IETF protocol (192.0.0.0/24)';
57
+ if (a === 192 && b === 0 && o[2] === 2)
58
+ return 'TEST-NET-1 (192.0.2.0/24)';
59
+ if (a === 198 && (b === 18 || b === 19))
60
+ return 'benchmarking (198.18.0.0/15)';
61
+ if (a === 198 && b === 51 && o[2] === 100)
62
+ return 'TEST-NET-2 (198.51.100.0/24)';
63
+ if (a === 203 && b === 0 && o[2] === 113)
64
+ return 'TEST-NET-3 (203.0.113.0/24)';
65
+ if (a >= 224 && a <= 239)
66
+ return 'multicast (224.0.0.0/4)';
67
+ if (a >= 240)
68
+ return 'reserved (240.0.0.0/4)';
69
+ return null;
70
+ }
71
+ /** Expand an IPv6 address to its 8 16-bit groups, or null if unparseable. */
72
+ function ipv6Groups(ip) {
73
+ let s = ip.toLowerCase();
74
+ // IPv4-mapped / translated tail (e.g. ::ffff:127.0.0.1) → fold the v4 into two groups.
75
+ const v4 = /:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.exec(s);
76
+ if (v4) {
77
+ const o = ipv4Octets(v4[1]);
78
+ if (!o)
79
+ return null;
80
+ s = s.slice(0, v4.index + 1) + ((o[0] << 8) | o[1]).toString(16) + ':' + ((o[2] << 8) | o[3]).toString(16);
81
+ }
82
+ if (!s.includes(':'))
83
+ return null;
84
+ const halves = s.split('::');
85
+ if (halves.length > 2)
86
+ return null;
87
+ const head = halves[0] ? halves[0].split(':') : [];
88
+ const tail = halves.length === 2 ? (halves[1] ? halves[1].split(':') : []) : null;
89
+ let groups;
90
+ if (tail === null) {
91
+ groups = head;
92
+ }
93
+ else {
94
+ const fill = 8 - head.length - tail.length;
95
+ if (fill < 0)
96
+ return null;
97
+ groups = [...head, ...Array(fill).fill('0'), ...tail];
98
+ }
99
+ if (groups.length !== 8)
100
+ return null;
101
+ const nums = groups.map((g) => (g === '' ? NaN : parseInt(g, 16)));
102
+ return nums.every((n) => Number.isInteger(n) && n >= 0 && n <= 0xffff) ? nums : null;
103
+ }
104
+ /** Return a reason string if the IPv6 address is in a blocked range, else null. */
105
+ function blockedIpv6(ip) {
106
+ const g = ipv6Groups(ip);
107
+ if (!g)
108
+ return null;
109
+ const isZero = (n, upto) => g.slice(0, upto).every((x) => x === 0);
110
+ if (g.every((x) => x === 0))
111
+ return 'unspecified (::)';
112
+ if (isZero(0, 7) && g[7] === 1)
113
+ return 'loopback (::1)';
114
+ // ::ffff:0:0/96 IPv4-mapped — the embedded v4 was already folded; treat mapped space as the v4 check's job
115
+ if (isZero(0, 5) && g[5] === 0xffff)
116
+ return 'IPv4-mapped (::ffff:0:0/96)';
117
+ if ((g[0] & 0xfe00) === 0xfc00)
118
+ return 'unique-local (fc00::/7)';
119
+ if ((g[0] & 0xffc0) === 0xfe80)
120
+ return 'link-local (fe80::/10)';
121
+ if ((g[0] & 0xff00) === 0xff00)
122
+ return 'multicast (ff00::/8)';
123
+ if (g[0] === 0x2001 && g[1] === 0x0db8)
124
+ return 'documentation (2001:db8::/32)';
125
+ if (g[0] === 0x0064 && g[1] === 0xff9b && g[2] === 0 && g[3] === 0 && g[4] === 0 && g[5] === 0)
126
+ return 'NAT64 (64:ff9b::/96)';
127
+ if (g[0] === 0x0100 && g[1] === 0 && g[2] === 0 && g[3] === 0)
128
+ return 'discard-only (100::/64)';
129
+ return null;
130
+ }
131
+ /** Reason string if this literal/resolved IP must be refused, else null. */
132
+ export function blockedIp(ip) {
133
+ // Node strips zone ids (fe80::1%eth0); drop any just in case.
134
+ const clean = ip.replace(/%.*$/, '');
135
+ if (!isIP(clean))
136
+ return 'invalid IP address';
137
+ if (isIP(clean) === 6) {
138
+ const g = ipv6Groups(clean);
139
+ // Only ordinary global unicast. Refuse transition/embedded IPv4 ranges,
140
+ // including NAT64 local-use, IPv4-compatible, Teredo and 6to4.
141
+ if ((g[0] & 0xe000) !== 0x2000)
142
+ return blockedIpv6(clean) ?? 'non-global IPv6';
143
+ if (g[0] === 0x2002 || (g[0] === 0x2001 && g[1] < 0x0200))
144
+ return 'IPv6 special/transition range';
145
+ }
146
+ return clean.includes(':') ? blockedIpv6(clean) : blockedIpv4(clean);
147
+ }
148
+ /** Validated addresses handed directly to a socket: usable as a regression seam. */
149
+ export async function publicAddresses(host, resolve) {
150
+ const ips = isIP(host) ? [host] : await resolve(host);
151
+ if (!ips.length)
152
+ throw new SsrfError('no DNS addresses');
153
+ for (const ip of ips) {
154
+ const reason = blockedIp(ip);
155
+ if (reason)
156
+ throw new SsrfError(`blocked DNS address: ${reason}`);
157
+ }
158
+ return ips;
159
+ }
160
+ const nextRequest = new Map();
161
+ function pinnedFetch(resolve) {
162
+ return (async (input, init) => {
163
+ const url = new URL(String(input));
164
+ const at = Math.max(Date.now(), nextRequest.get(url.origin) ?? 0);
165
+ if (nextRequest.size > 128)
166
+ nextRequest.clear();
167
+ nextRequest.set(url.origin, at + 1000);
168
+ if (at > Date.now())
169
+ await new Promise((yes, no) => {
170
+ const finish = () => { init?.signal?.removeEventListener('abort', abort); yes(); };
171
+ const timer = setTimeout(finish, at - Date.now());
172
+ const abort = () => { clearTimeout(timer); init?.signal?.removeEventListener('abort', abort); no(init?.signal?.reason); };
173
+ init?.signal?.addEventListener('abort', abort, { once: true });
174
+ if (init?.signal?.aborted)
175
+ abort();
176
+ });
177
+ return new Promise((yes, no) => {
178
+ const request = url.protocol === 'https:' ? httpsRequest : httpRequest;
179
+ const req = request(url, {
180
+ method: 'GET', agent: false, signal: init?.signal ?? undefined,
181
+ headers: Object.fromEntries(new Headers(init?.headers)),
182
+ // Host header, SNI and TLS verification still use the original hostname.
183
+ lookup(host, options, cb) {
184
+ void publicAddresses(host, resolve).then((ips) => {
185
+ if (options.all)
186
+ cb(null, ips.map((address) => ({ address, family: isIP(address) })));
187
+ else
188
+ cb(null, ips[0], isIP(ips[0]));
189
+ }, (err) => cb(err, '', 4));
190
+ },
191
+ }, (res) => {
192
+ const headers = new Headers();
193
+ for (const [key, value] of Object.entries(res.headers)) {
194
+ if (value !== undefined)
195
+ headers.set(key, Array.isArray(value) ? value.join(', ') : value);
196
+ }
197
+ const status = res.statusCode ?? 502;
198
+ if ([204, 205, 304].includes(status)) {
199
+ res.destroy();
200
+ yes(new Response(null, { status, headers }));
201
+ }
202
+ else
203
+ yes(new Response(Readable.toWeb(res), { status, headers }));
204
+ });
205
+ req.on('error', no);
206
+ req.end();
207
+ });
208
+ });
209
+ }
210
+ async function bounded(promise, signal) {
211
+ signal.throwIfAborted();
212
+ return new Promise((yes, no) => {
213
+ const abort = () => no(signal.reason);
214
+ signal.addEventListener('abort', abort, { once: true });
215
+ promise.then(yes, no).finally(() => signal.removeEventListener('abort', abort));
216
+ });
217
+ }
218
+ /** Default resolver: every A/AAAA for the host. */
219
+ async function defaultResolve(host) {
220
+ const res = await lookupAll(host, { all: true });
221
+ return res.map((r) => r.address);
222
+ }
223
+ /**
224
+ * Validate scheme/shape and resolve the host, refusing any blocked IP. Throws SsrfError on refusal.
225
+ * Returns the parsed URL (host already lower-cased by WHATWG, numeric IPv4 forms normalised).
226
+ */
227
+ export async function assertPublicUrl(raw, resolve) {
228
+ let u;
229
+ try {
230
+ u = new URL(raw);
231
+ }
232
+ catch {
233
+ throw new SsrfError(`unparseable url`);
234
+ }
235
+ if (u.protocol !== 'http:' && u.protocol !== 'https:')
236
+ throw new SsrfError(`scheme not allowed: ${u.protocol}`);
237
+ if (u.username || u.password)
238
+ throw new SsrfError('credentials in url not allowed');
239
+ const host = u.hostname.replace(/^\[|\]$/g, ''); // strip IPv6 brackets
240
+ if (!host)
241
+ throw new SsrfError('empty host');
242
+ // If the host is itself an IP literal, check it directly (WHATWG already normalised 0x7f/decimal forms).
243
+ const literal = isIP(host) ? blockedIp(host) : null;
244
+ if (literal)
245
+ throw new SsrfError(`blocked address ${host}: ${literal}`);
246
+ // Otherwise resolve and refuse if ANY resolved address is blocked.
247
+ if (ipv4Octets(host) === null && !host.includes(':')) {
248
+ let ips;
249
+ try {
250
+ ips = await resolve(host);
251
+ }
252
+ catch (e) {
253
+ throw new SsrfError(`dns failed for ${host}: ${e.message}`);
254
+ }
255
+ if (ips.length === 0)
256
+ throw new SsrfError(`no addresses for ${host}`);
257
+ for (const ip of ips) {
258
+ const bad = blockedIp(ip);
259
+ if (bad)
260
+ throw new SsrfError(`${host} resolves to blocked ${ip}: ${bad}`);
261
+ }
262
+ }
263
+ return u;
264
+ }
265
+ function headerValue(res, name) {
266
+ return res.headers.get(name);
267
+ }
268
+ /** Read a web ReadableStream up to maxBytes, truncating and cancelling the rest. */
269
+ async function readCapped(body, maxBytes) {
270
+ if (!body)
271
+ return { text: '', bytes: 0, truncated: false };
272
+ const reader = body.getReader();
273
+ const chunks = [];
274
+ let total = 0;
275
+ let truncated = false;
276
+ try {
277
+ for (;;) {
278
+ const { done, value } = await reader.read();
279
+ if (done)
280
+ break;
281
+ if (!value)
282
+ continue;
283
+ if (total + value.length > maxBytes) {
284
+ chunks.push(value.subarray(0, maxBytes - total));
285
+ total = maxBytes;
286
+ truncated = true;
287
+ break;
288
+ }
289
+ chunks.push(value);
290
+ total += value.length;
291
+ }
292
+ }
293
+ finally {
294
+ try {
295
+ await reader.cancel();
296
+ }
297
+ catch { /* already closed */ }
298
+ }
299
+ const buf = Buffer.concat(chunks.map((c) => Buffer.from(c)), total);
300
+ return { text: buf.toString('utf8'), bytes: total, truncated };
301
+ }
302
+ /**
303
+ * SSRF-safe GET with manual redirect following. Never throws for network/policy failures —
304
+ * returns { ok:false, blocked } so the caller (and the analyst) can report it as a missing-data fact.
305
+ */
306
+ export async function safeFetch(rawUrl, opts = {}) {
307
+ const maxBytes = opts.maxBytes ?? DEFAULTS.maxBytes;
308
+ const timeoutMs = opts.timeoutMs ?? DEFAULTS.timeoutMs;
309
+ const maxRedirects = opts.maxRedirects ?? DEFAULTS.maxRedirects;
310
+ const resolve = opts.resolve ?? defaultResolve;
311
+ const doFetch = opts.fetchImpl ?? pinnedFetch(resolve);
312
+ const now = opts.now ?? (() => Date.now());
313
+ const ua = opts.userAgent ?? 'dexterm/0.0.1 (+local terminal; keyless read-only larp-check)';
314
+ const started = now();
315
+ const deadline = started + timeoutMs;
316
+ const signal = AbortSignal.timeout(timeoutMs);
317
+ const redirects = [];
318
+ const fail = (blocked, status = 0, finalUrl = rawUrl) => ({ ok: false, status, finalUrl, contentType: null, body: '', bytes: 0, truncated: false, redirects, blocked, elapsedMs: now() - started });
319
+ let current = rawUrl;
320
+ for (let hop = 0; hop <= maxRedirects; hop++) {
321
+ let u;
322
+ try {
323
+ u = await bounded(assertPublicUrl(current, resolve), signal);
324
+ if (opts.beforeRequest)
325
+ await bounded(opts.beforeRequest(u), signal);
326
+ }
327
+ catch (e) {
328
+ return fail(e.reason ?? String(e));
329
+ }
330
+ const remaining = deadline - now();
331
+ if (remaining <= 0)
332
+ return fail('timeout before request', 0, current);
333
+ let res;
334
+ try {
335
+ res = await bounded(doFetch(u.toString(), {
336
+ method: 'GET',
337
+ redirect: 'manual',
338
+ signal,
339
+ headers: { 'user-agent': ua, 'accept-encoding': 'identity', accept: 'text/html,application/xhtml+xml,application/json;q=0.9,*/*;q=0.1' },
340
+ // never attach or store cookies / credentials.
341
+ credentials: 'omit',
342
+ }), signal);
343
+ }
344
+ catch (e) {
345
+ const msg = e.name === 'TimeoutError' ? 'request timed out' : e.message;
346
+ return fail(`fetch failed: ${msg}`, 0, current);
347
+ }
348
+ // Manual redirect: 3xx with a Location → re-validate and continue.
349
+ if (res.status >= 300 && res.status < 400) {
350
+ const loc = headerValue(res, 'location');
351
+ try {
352
+ await res.body?.cancel();
353
+ }
354
+ catch { /* ignore */ }
355
+ if (!loc)
356
+ return fail(`${res.status} redirect with no location`, res.status, current);
357
+ let nextUrl;
358
+ try {
359
+ nextUrl = new URL(loc, u).toString();
360
+ }
361
+ catch {
362
+ return fail(`bad redirect location`, res.status, current);
363
+ }
364
+ if (hop >= maxRedirects)
365
+ return fail(`too many redirects (> ${maxRedirects})`, res.status, current);
366
+ redirects.push(u.toString());
367
+ current = nextUrl;
368
+ continue;
369
+ }
370
+ const contentType = headerValue(res, 'content-type');
371
+ const textual = contentType ? TEXTUAL.test(contentType) : true; // no CT → attempt as text
372
+ if (!textual) {
373
+ try {
374
+ await res.body?.cancel();
375
+ }
376
+ catch { /* ignore */ }
377
+ return { ok: res.ok, status: res.status, finalUrl: u.toString(), contentType, body: '', bytes: 0, truncated: false, redirects, blocked: null, elapsedMs: now() - started };
378
+ }
379
+ let read;
380
+ try {
381
+ read = await bounded(readCapped(res.body, maxBytes), signal);
382
+ }
383
+ catch (e) {
384
+ return fail(`read failed: ${e.message}`, res.status, u.toString());
385
+ }
386
+ return { ok: res.ok, status: res.status, finalUrl: u.toString(), contentType, body: read.text, bytes: read.bytes, truncated: read.truncated, redirects, blocked: null, elapsedMs: now() - started };
387
+ }
388
+ return fail(`too many redirects (> ${maxRedirects})`);
389
+ }