dexterm-cli 0.1.2 → 0.1.4

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 (327) hide show
  1. package/dist/packages/agents/src/bus.js +37 -1
  2. package/dist/packages/agents/src/follow.js +105 -0
  3. package/dist/packages/agents/src/index.js +5 -0
  4. package/dist/packages/agents/src/spectator-server.js +153 -0
  5. package/dist/packages/agents/src/spectator.js +212 -0
  6. package/dist/packages/cli/assets/claude/analyst-system.md +7 -1
  7. package/dist/packages/cli/src/agent-cmd.js +71 -7
  8. package/dist/packages/cli/src/agent-spectator.js +37 -0
  9. package/dist/packages/cli/src/alerts/format.js +25 -0
  10. package/dist/packages/cli/src/app-help-gen.js +365 -0
  11. package/dist/packages/cli/src/docs-kb-gen.js +228 -0
  12. package/dist/packages/cli/src/doctor.js +28 -12
  13. package/dist/packages/cli/src/follow-cmd.js +107 -1
  14. package/dist/packages/cli/src/help-cmd.js +48 -0
  15. package/dist/packages/cli/src/index.js +1 -1
  16. package/dist/packages/cli/src/kb-skill-cmd.js +3 -0
  17. package/dist/packages/cli/src/larp-detected.js +209 -0
  18. package/dist/packages/cli/src/live/index.js +64 -3
  19. package/dist/packages/cli/src/live/screens/token-live.js +24 -0
  20. package/dist/packages/cli/src/live/signals.js +109 -0
  21. package/dist/packages/cli/src/live/token.js +38 -1
  22. package/dist/packages/cli/src/live/trade.js +24 -1
  23. package/dist/packages/cli/src/main.js +495 -11
  24. package/dist/packages/cli/src/mcp-cmd.js +2 -7
  25. package/dist/packages/cli/src/mcp-follow.js +225 -0
  26. package/dist/packages/cli/src/pnl-app.js +221 -0
  27. package/dist/packages/cli/src/pnl-card.js +615 -0
  28. package/dist/packages/cli/src/record-cmd.js +455 -0
  29. package/dist/packages/cli/src/rules.js +50 -2
  30. package/dist/packages/cli/src/screens/follows.js +91 -2
  31. package/dist/packages/cli/src/screens/overlay.js +3 -0
  32. package/dist/packages/cli/src/screens/positions.js +1 -1
  33. package/dist/packages/cli/src/screens/rules.js +1 -1
  34. package/dist/packages/cli/src/spectate-cmd.js +115 -0
  35. package/dist/packages/cli/src/state.js +1 -1
  36. package/dist/packages/cli/src/version.js +1 -1
  37. package/dist/packages/cli/src/voice-cmd.js +86 -11
  38. package/dist/packages/cli/src/voice-hud.js +1 -1
  39. package/dist/packages/cli/src/x-cmd.js +11 -5
  40. package/dist/packages/custom/src/rules.js +2 -1
  41. package/dist/packages/data/src/analyst-actions.js +130 -3
  42. package/dist/packages/data/src/fast-lane.js +16 -2
  43. package/dist/packages/data/src/index.js +1 -0
  44. package/dist/packages/data/src/intel/live-signals.js +185 -0
  45. package/dist/packages/data/src/trade/copy/index.js +1 -1
  46. package/dist/packages/data/src/trade/copy/stats.js +36 -2
  47. package/dist/packages/data/src/trade/copy/tracker.js +3 -2
  48. package/dist/packages/mcp/assets/kb/app-help/app-help-command.md +35 -0
  49. package/dist/packages/mcp/assets/kb/app-help/ask-the-analyst.md +43 -0
  50. package/dist/packages/mcp/assets/kb/app-help/backtest.md +33 -0
  51. package/dist/packages/mcp/assets/kb/app-help/bubble-map.md +35 -0
  52. package/dist/packages/mcp/assets/kb/app-help/calm-mode.md +31 -0
  53. package/dist/packages/mcp/assets/kb/app-help/change-voice-speed.md +31 -0
  54. package/dist/packages/mcp/assets/kb/app-help/change-voice.md +30 -0
  55. package/dist/packages/mcp/assets/kb/app-help/chart-popup-selftest.md +32 -0
  56. package/dist/packages/mcp/assets/kb/app-help/coin-images.md +29 -0
  57. package/dist/packages/mcp/assets/kb/app-help/configure-rpc-key.md +28 -0
  58. package/dist/packages/mcp/assets/kb/app-help/connect-an-agent-registry.md +35 -0
  59. package/dist/packages/mcp/assets/kb/app-help/connect-an-agent.md +39 -0
  60. package/dist/packages/mcp/assets/kb/app-help/custom-keymap.md +32 -0
  61. package/dist/packages/mcp/assets/kb/app-help/edit-kb-or-skill.md +36 -0
  62. package/dist/packages/mcp/assets/kb/app-help/edit-rules.md +34 -0
  63. package/dist/packages/mcp/assets/kb/app-help/export-a-view.md +36 -0
  64. package/dist/packages/mcp/assets/kb/app-help/follow-a-wallet.md +33 -0
  65. package/dist/packages/mcp/assets/kb/app-help/getting-started.md +37 -0
  66. package/dist/packages/mcp/assets/kb/app-help/kol-lists.md +34 -0
  67. package/dist/packages/mcp/assets/kb/app-help/larp-check.md +36 -0
  68. package/dist/packages/mcp/assets/kb/app-help/link-a-wallet.md +41 -0
  69. package/dist/packages/mcp/assets/kb/app-help/login-with-x.md +33 -0
  70. package/dist/packages/mcp/assets/kb/app-help/narrative-board.md +32 -0
  71. package/dist/packages/mcp/assets/kb/app-help/open-a-coin.md +43 -0
  72. package/dist/packages/mcp/assets/kb/app-help/open-chart-popup.md +33 -0
  73. package/dist/packages/mcp/assets/kb/app-help/order-plans.md +33 -0
  74. package/dist/packages/mcp/assets/kb/app-help/paper-trade.md +36 -0
  75. package/dist/packages/mcp/assets/kb/app-help/price-or-marketcap.md +38 -0
  76. package/dist/packages/mcp/assets/kb/app-help/record-headless.md +34 -0
  77. package/dist/packages/mcp/assets/kb/app-help/recorder-history.md +39 -0
  78. package/dist/packages/mcp/assets/kb/app-help/rug-check.md +39 -0
  79. package/dist/packages/mcp/assets/kb/app-help/run-doctor.md +32 -0
  80. package/dist/packages/mcp/assets/kb/app-help/scan-mode.md +37 -0
  81. package/dist/packages/mcp/assets/kb/app-help/screen-analyst.md +181 -0
  82. package/dist/packages/mcp/assets/kb/app-help/screen-kol.md +190 -0
  83. package/dist/packages/mcp/assets/kb/app-help/screen-newpairs.md +181 -0
  84. package/dist/packages/mcp/assets/kb/app-help/screen-positions.md +180 -0
  85. package/dist/packages/mcp/assets/kb/app-help/screen-screener.md +199 -0
  86. package/dist/packages/mcp/assets/kb/app-help/screen-token.md +240 -0
  87. package/dist/packages/mcp/assets/kb/app-help/screen-trending.md +192 -0
  88. package/dist/packages/mcp/assets/kb/app-help/screen-watchlist.md +181 -0
  89. package/dist/packages/mcp/assets/kb/app-help/set-a-filter.md +36 -0
  90. package/dist/packages/mcp/assets/kb/app-help/setup-neural-voice.md +36 -0
  91. package/dist/packages/mcp/assets/kb/app-help/share-packs.md +34 -0
  92. package/dist/packages/mcp/assets/kb/app-help/sort-the-screener.md +34 -0
  93. package/dist/packages/mcp/assets/kb/app-help/spectate-an-agent.md +39 -0
  94. package/dist/packages/mcp/assets/kb/app-help/standing-mandates.md +33 -0
  95. package/dist/packages/mcp/assets/kb/app-help/switch-theme.md +42 -0
  96. package/dist/packages/mcp/assets/kb/app-help/turn-on-alerts.md +37 -0
  97. package/dist/packages/mcp/assets/kb/app-help/update-dexterm.md +30 -0
  98. package/dist/packages/mcp/assets/kb/app-help/use-presets.md +42 -0
  99. package/dist/packages/mcp/assets/kb/app-help/use-voice-mode.md +36 -0
  100. package/dist/packages/mcp/assets/kb/app-help/watch-a-coin.md +34 -0
  101. package/dist/packages/mcp/assets/kb/app-help/watch-headless.md +35 -0
  102. package/dist/packages/mcp/assets/kb/app-help/what-dexterm-shows-agents.md +31 -0
  103. package/dist/packages/mcp/assets/kb/app-help/workspaces.md +38 -0
  104. package/dist/packages/mcp/assets/kb/docs/docs-agents-capability-tiers-enforced-server-side.md +38 -0
  105. package/dist/packages/mcp/assets/kb/docs/docs-agents-install-in-one-command.md +49 -0
  106. package/dist/packages/mcp/assets/kb/docs/docs-agents-per-agent-mandates.md +35 -0
  107. package/dist/packages/mcp/assets/kb/docs/docs-agents-recipes.md +43 -0
  108. package/dist/packages/mcp/assets/kb/docs/docs-agents-red-team-coverage.md +30 -0
  109. package/dist/packages/mcp/assets/kb/docs/docs-agents-two-ways-to-connect.md +37 -0
  110. package/dist/packages/mcp/assets/kb/docs/docs-agents-untrusted-by-default.md +36 -0
  111. package/dist/packages/mcp/assets/kb/docs/docs-agents-watch-an-agent-work-spectator-view.md +35 -0
  112. package/dist/packages/mcp/assets/kb/docs/docs-agents.md +26 -0
  113. package/dist/packages/mcp/assets/kb/docs/docs-analyst-answer-style.md +35 -0
  114. package/dist/packages/mcp/assets/kb/docs/docs-analyst-if-claude-is-missing.md +25 -0
  115. package/dist/packages/mcp/assets/kb/docs/docs-analyst-it-knows-the-app.md +33 -0
  116. package/dist/packages/mcp/assets/kb/docs/docs-analyst-mcp-tools.md +33 -0
  117. package/dist/packages/mcp/assets/kb/docs/docs-analyst-plain-language-filters.md +25 -0
  118. package/dist/packages/mcp/assets/kb/docs/docs-analyst-skills.md +31 -0
  119. package/dist/packages/mcp/assets/kb/docs/docs-analyst-starting-a-run.md +33 -0
  120. package/dist/packages/mcp/assets/kb/docs/docs-analyst-what-it-can-change-three-tiers.md +33 -0
  121. package/dist/packages/mcp/assets/kb/docs/docs-analyst-what-it-sees.md +26 -0
  122. package/dist/packages/mcp/assets/kb/docs/docs-analyst-what-the-analyst-can-never-do.md +31 -0
  123. package/dist/packages/mcp/assets/kb/docs/docs-analyst.md +23 -0
  124. package/dist/packages/mcp/assets/kb/docs/docs-beginners-1-start-dexterm.md +35 -0
  125. package/dist/packages/mcp/assets/kb/docs/docs-beginners-2-open-a-real-coin-by-contract-address.md +43 -0
  126. package/dist/packages/mcp/assets/kb/docs/docs-beginners-3-read-the-safety-panel.md +40 -0
  127. package/dist/packages/mcp/assets/kb/docs/docs-beginners-4-read-the-holder-panel.md +35 -0
  128. package/dist/packages/mcp/assets/kb/docs/docs-beginners-5-optionally-ask-the-analyst.md +39 -0
  129. package/dist/packages/mcp/assets/kb/docs/docs-beginners-6-export-or-share-what-you-see.md +34 -0
  130. package/dist/packages/mcp/assets/kb/docs/docs-beginners-7-quit-safely.md +34 -0
  131. package/dist/packages/mcp/assets/kb/docs/docs-beginners-before-any-experimental-trading.md +33 -0
  132. package/dist/packages/mcp/assets/kb/docs/docs-beginners-check-the-installation-with-doctor.md +38 -0
  133. package/dist/packages/mcp/assets/kb/docs/docs-beginners-choose-a-setup-path.md +30 -0
  134. package/dist/packages/mcp/assets/kb/docs/docs-beginners-common-mistakes-and-recovery.md +30 -0
  135. package/dist/packages/mcp/assets/kb/docs/docs-beginners-copy-and-paste-without-surprises.md +39 -0
  136. package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-data-says-unavailable-stale-or-rate-limited.md +35 -0
  137. package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-dexterm-says-command-not-found.md +36 -0
  138. package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-optional-help-is-unavailable.md +33 -0
  139. package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-paste-does-nothing-or-pastes-twice.md +35 -0
  140. package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-the-screen-looks-broken.md +33 -0
  141. package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-you-typed-the-wrong-thing.md +33 -0
  142. package/dist/packages/mcp/assets/kb/docs/docs-beginners-install-node-js-first.md +36 -0
  143. package/dist/packages/mcp/assets/kb/docs/docs-beginners-linux.md +30 -0
  144. package/dist/packages/mcp/assets/kb/docs/docs-beginners-macos-homebrew.md +46 -0
  145. package/dist/packages/mcp/assets/kb/docs/docs-beginners-macos-or-linux-npm.md +39 -0
  146. package/dist/packages/mcp/assets/kb/docs/docs-beginners-macos.md +30 -0
  147. package/dist/packages/mcp/assets/kb/docs/docs-beginners-open-the-terminal.md +29 -0
  148. package/dist/packages/mcp/assets/kb/docs/docs-beginners-reopen-dexterm-later.md +36 -0
  149. package/dist/packages/mcp/assets/kb/docs/docs-beginners-ten-minute-first-win.md +30 -0
  150. package/dist/packages/mcp/assets/kb/docs/docs-beginners-terminal-shell-prompt-command-output-and-path.md +40 -0
  151. package/dist/packages/mcp/assets/kb/docs/docs-beginners-where-to-go-next.md +32 -0
  152. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-expectations.md +29 -0
  153. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-powershell-experimental.md +39 -0
  154. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows.md +30 -0
  155. package/dist/packages/mcp/assets/kb/docs/docs-beginners.md +29 -0
  156. package/dist/packages/mcp/assets/kb/docs/docs-cli-dexterm-help.md +31 -0
  157. package/dist/packages/mcp/assets/kb/docs/docs-cli-filter-grammar.md +37 -0
  158. package/dist/packages/mcp/assets/kb/docs/docs-cli-flags.md +27 -0
  159. package/dist/packages/mcp/assets/kb/docs/docs-cli-invocations.md +43 -0
  160. package/dist/packages/mcp/assets/kb/docs/docs-cli-presets.md +26 -0
  161. package/dist/packages/mcp/assets/kb/docs/docs-cli-scan-commands.md +26 -0
  162. package/dist/packages/mcp/assets/kb/docs/docs-cli-sort-keys.md +27 -0
  163. package/dist/packages/mcp/assets/kb/docs/docs-cli-subcommands.md +26 -0
  164. package/dist/packages/mcp/assets/kb/docs/docs-cli.md +25 -0
  165. package/dist/packages/mcp/assets/kb/docs/docs-configuration-environment-variables.md +45 -0
  166. package/dist/packages/mcp/assets/kb/docs/docs-configuration-files-on-disk.md +41 -0
  167. package/dist/packages/mcp/assets/kb/docs/docs-configuration-headless-recorder-dexterm-record.md +44 -0
  168. package/dist/packages/mcp/assets/kb/docs/docs-configuration-history-recorder.md +33 -0
  169. package/dist/packages/mcp/assets/kb/docs/docs-configuration-packs.md +38 -0
  170. package/dist/packages/mcp/assets/kb/docs/docs-configuration-price-display-mode.md +35 -0
  171. package/dist/packages/mcp/assets/kb/docs/docs-configuration-push-alerts-and-dexterm-watch.md +45 -0
  172. package/dist/packages/mcp/assets/kb/docs/docs-configuration-rules-engine.md +36 -0
  173. package/dist/packages/mcp/assets/kb/docs/docs-configuration-the-analyst-s-launch.md +27 -0
  174. package/dist/packages/mcp/assets/kb/docs/docs-configuration-the-optional-free-rpc-key.md +42 -0
  175. package/dist/packages/mcp/assets/kb/docs/docs-configuration-themes-and-keymaps.md +29 -0
  176. package/dist/packages/mcp/assets/kb/docs/docs-configuration-themes-and-look.md +27 -0
  177. package/dist/packages/mcp/assets/kb/docs/docs-configuration-widgets.md +31 -0
  178. package/dist/packages/mcp/assets/kb/docs/docs-configuration-workspaces.md +30 -0
  179. package/dist/packages/mcp/assets/kb/docs/docs-configuration.md +25 -0
  180. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-stage-1-follow-a-wallet-see-what-it-does.md +51 -0
  181. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-stage-2-the-one-key-mirror-ticket-a-proposal.md +56 -0
  182. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-stage-3-capped-auto-mirror-default-off-less.md +47 -0
  183. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-what-is-proven-what-is-not.md +36 -0
  184. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading.md +31 -0
  185. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-cache.md +32 -0
  186. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-health-registry.md +29 -0
  187. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-rate-limits-and-pacing.md +32 -0
  188. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-request-budget-meter.md +30 -0
  189. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-sources.md +47 -0
  190. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-the-helius-credit-meter.md +30 -0
  191. package/dist/packages/mcp/assets/kb/docs/docs-data-sources.md +27 -0
  192. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-dexterm-doctor.md +33 -0
  193. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-first-run.md +34 -0
  194. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +71 -0
  195. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-next.md +32 -0
  196. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +35 -0
  197. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-running-the-tests.md +34 -0
  198. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-supported-platforms.md +44 -0
  199. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-updating.md +28 -0
  200. package/dist/packages/mcp/assets/kb/docs/docs-getting-started.md +27 -0
  201. package/dist/packages/mcp/assets/kb/docs/docs-keyboard-global-keys.md +26 -0
  202. package/dist/packages/mcp/assets/kb/docs/docs-keyboard-keys-handled-by-the-loop.md +29 -0
  203. package/dist/packages/mcp/assets/kb/docs/docs-keyboard-per-screen-keys.md +26 -0
  204. package/dist/packages/mcp/assets/kb/docs/docs-keyboard-what-shows.md +27 -0
  205. package/dist/packages/mcp/assets/kb/docs/docs-keyboard.md +26 -0
  206. package/dist/packages/mcp/assets/kb/docs/docs-larp-measured-accuracy.md +55 -0
  207. package/dist/packages/mcp/assets/kb/docs/docs-larp-open-link-keys.md +26 -0
  208. package/dist/packages/mcp/assets/kb/docs/docs-larp-paste-a-coin-on-the-web.md +35 -0
  209. package/dist/packages/mcp/assets/kb/docs/docs-larp-the-verdict.md +29 -0
  210. package/dist/packages/mcp/assets/kb/docs/docs-larp-timestamped-follow-up-drafts.md +35 -0
  211. package/dist/packages/mcp/assets/kb/docs/docs-larp-what-it-looks-at.md +33 -0
  212. package/dist/packages/mcp/assets/kb/docs/docs-larp.md +24 -0
  213. package/dist/packages/mcp/assets/kb/docs/docs-login-with-x-degraded-states-you-will-see.md +36 -0
  214. package/dist/packages/mcp/assets/kb/docs/docs-login-with-x-free-vs-paid-x-api-tiers-your-bill-your-limits.md +37 -0
  215. package/dist/packages/mcp/assets/kb/docs/docs-login-with-x-kol-lists-and-the-per-handle-filter.md +28 -0
  216. package/dist/packages/mcp/assets/kb/docs/docs-login-with-x-tests.md +25 -0
  217. package/dist/packages/mcp/assets/kb/docs/docs-login-with-x-what-is-read-and-when.md +35 -0
  218. package/dist/packages/mcp/assets/kb/docs/docs-login-with-x-what-you-set-up-once.md +40 -0
  219. package/dist/packages/mcp/assets/kb/docs/docs-login-with-x-where-the-tokens-live.md +27 -0
  220. package/dist/packages/mcp/assets/kb/docs/docs-login-with-x.md +26 -0
  221. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-a-live-ticket.md +42 -0
  222. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-before-you-press-size-vs-caps.md +34 -0
  223. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-devnet-signer-mainnet-token.md +35 -0
  224. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-jupiter-rate-limits.md +33 -0
  225. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-link-a-wallet.md +32 -0
  226. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-live-trading-devnet-first.md +30 -0
  227. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-mainnet.md +31 -0
  228. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-order-plans-and-standing-mandates.md +44 -0
  229. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-panic.md +31 -0
  230. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-pnl-cards.md +37 -0
  231. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-live-fire-path-devnet-the-fast-wallet.md +41 -0
  232. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-signer-process.md +34 -0
  233. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-ticket.md +31 -0
  234. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-wallet.md +34 -0
  235. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-trade-moments.md +32 -0
  236. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-what-the-live-path-keeps.md +37 -0
  237. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-your-first-live-trade.md +41 -0
  238. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading.md +32 -0
  239. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-bubble-map-popup.md +27 -0
  240. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-build-the-native-window.md +33 -0
  241. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-config-dexterm-popup-json.md +38 -0
  242. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-docking-macos.md +27 -0
  243. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-keys-in-the-popup.md +30 -0
  244. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-market-cap-axis.md +28 -0
  245. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-security-model.md +33 -0
  246. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-token-and-feeds-popups.md +38 -0
  247. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-what-it-shows.md +30 -0
  248. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart.md +27 -0
  249. package/dist/packages/mcp/assets/kb/docs/docs-scan-cost-control.md +28 -0
  250. package/dist/packages/mcp/assets/kb/docs/docs-scan-keys-flag-command.md +34 -0
  251. package/dist/packages/mcp/assets/kb/docs/docs-scan-safety-four-layers.md +32 -0
  252. package/dist/packages/mcp/assets/kb/docs/docs-scan-states.md +27 -0
  253. package/dist/packages/mcp/assets/kb/docs/docs-scan-the-read.md +25 -0
  254. package/dist/packages/mcp/assets/kb/docs/docs-scan-triggers-and-score.md +37 -0
  255. package/dist/packages/mcp/assets/kb/docs/docs-scan.md +24 -0
  256. package/dist/packages/mcp/assets/kb/docs/docs-scorecard-frozen-canary.md +30 -0
  257. package/dist/packages/mcp/assets/kb/docs/docs-scorecard-frozen-task-1401-evaluation.md +35 -0
  258. package/dist/packages/mcp/assets/kb/docs/docs-scorecard-latest-skill-care-run.md +33 -0
  259. package/dist/packages/mcp/assets/kb/docs/docs-scorecard-links-only-larp-baseline-where-it-is-weak.md +35 -0
  260. package/dist/packages/mcp/assets/kb/docs/docs-scorecard-not-measured.md +29 -0
  261. package/dist/packages/mcp/assets/kb/docs/docs-scorecard-rug-verdict-rules.md +33 -0
  262. package/dist/packages/mcp/assets/kb/docs/docs-scorecard-source-and-rebuild.md +31 -0
  263. package/dist/packages/mcp/assets/kb/docs/docs-scorecard.md +27 -0
  264. package/dist/packages/mcp/assets/kb/docs/docs-screens-1-screener.md +35 -0
  265. package/dist/packages/mcp/assets/kb/docs/docs-screens-2-new-pairs.md +28 -0
  266. package/dist/packages/mcp/assets/kb/docs/docs-screens-3-watchlist.md +27 -0
  267. package/dist/packages/mcp/assets/kb/docs/docs-screens-4-analyst-tab.md +28 -0
  268. package/dist/packages/mcp/assets/kb/docs/docs-screens-bubble-map.md +32 -0
  269. package/dist/packages/mcp/assets/kb/docs/docs-screens-coin-images.md +32 -0
  270. package/dist/packages/mcp/assets/kb/docs/docs-screens-live-holder-and-rug-signals.md +51 -0
  271. package/dist/packages/mcp/assets/kb/docs/docs-screens-order-plans.md +26 -0
  272. package/dist/packages/mcp/assets/kb/docs/docs-screens-paper-ticket-and-positions.md +29 -0
  273. package/dist/packages/mcp/assets/kb/docs/docs-screens-the-look.md +29 -0
  274. package/dist/packages/mcp/assets/kb/docs/docs-screens-token-screen.md +36 -0
  275. package/dist/packages/mcp/assets/kb/docs/docs-screens-trending-social-and-kol-lists.md +34 -0
  276. package/dist/packages/mcp/assets/kb/docs/docs-screens.md +26 -0
  277. package/dist/packages/mcp/assets/kb/docs/docs-spectator-1-the-agent-activity-window-follow-mode.md +56 -0
  278. package/dist/packages/mcp/assets/kb/docs/docs-spectator-2-the-read-only-stream-spectate.md +42 -0
  279. package/dist/packages/mcp/assets/kb/docs/docs-spectator-3-save-and-replay-record-file-dexterm-spectate.md +41 -0
  280. package/dist/packages/mcp/assets/kb/docs/docs-spectator-follow-over-mcp-in-the-running-terminal.md +39 -0
  281. package/dist/packages/mcp/assets/kb/docs/docs-spectator-limits.md +36 -0
  282. package/dist/packages/mcp/assets/kb/docs/docs-spectator-quick-start.md +37 -0
  283. package/dist/packages/mcp/assets/kb/docs/docs-spectator-security-model.md +35 -0
  284. package/dist/packages/mcp/assets/kb/docs/docs-spectator-see-also.md +29 -0
  285. package/dist/packages/mcp/assets/kb/docs/docs-spectator-streaming-it.md +32 -0
  286. package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-three-surfaces.md +37 -0
  287. package/dist/packages/mcp/assets/kb/docs/docs-spectator-what-is-redacted.md +35 -0
  288. package/dist/packages/mcp/assets/kb/docs/docs-spectator.md +29 -0
  289. package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-common-questions.md +33 -0
  290. package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-doctor-lines.md +36 -0
  291. package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-in-app-messages.md +42 -0
  292. package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-reporting-a-problem.md +25 -0
  293. package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting.md +23 -0
  294. package/dist/packages/mcp/assets/kb/docs/docs-voice-doctor.md +26 -0
  295. package/dist/packages/mcp/assets/kb/docs/docs-voice-it-clarifies-it-never-guesses.md +36 -0
  296. package/dist/packages/mcp/assets/kb/docs/docs-voice-latency.md +38 -0
  297. package/dist/packages/mcp/assets/kb/docs/docs-voice-linux-and-windows.md +32 -0
  298. package/dist/packages/mcp/assets/kb/docs/docs-voice-privacy.md +26 -0
  299. package/dist/packages/mcp/assets/kb/docs/docs-voice-still-unverified.md +27 -0
  300. package/dist/packages/mcp/assets/kb/docs/docs-voice-the-voice-neural-by-default.md +48 -0
  301. package/dist/packages/mcp/assets/kb/docs/docs-voice-what-it-can-never-do.md +27 -0
  302. package/dist/packages/mcp/assets/kb/docs/docs-voice-what-you-can-say.md +38 -0
  303. package/dist/packages/mcp/assets/kb/docs/docs-voice.md +25 -0
  304. package/dist/packages/mcp/src/app-help.js +258 -0
  305. package/dist/packages/mcp/src/firewall.js +2 -2
  306. package/dist/packages/mcp/src/index.js +5 -1
  307. package/dist/packages/mcp/src/server.js +129 -4
  308. package/dist/packages/mcp/src/spectator-sink.js +144 -0
  309. package/dist/packages/packs/src/index.js +2 -0
  310. package/dist/packages/packs/src/skill-care.js +206 -0
  311. package/dist/packages/popup/src/pages/spectator.css +45 -0
  312. package/dist/packages/popup/src/pages/spectator.html +25 -0
  313. package/dist/packages/popup/src/pages/spectator.js +74 -0
  314. package/dist/packages/popup/src/selftest.js +19 -0
  315. package/dist/packages/popup/src/server.js +7 -1
  316. package/dist/packages/tui/src/keymap.js +10 -0
  317. package/dist/packages/voice/src/controller.js +42 -1
  318. package/dist/packages/voice/src/doctor.js +25 -8
  319. package/dist/packages/voice/src/engines.js +8 -1
  320. package/dist/packages/voice/src/index.js +3 -1
  321. package/dist/packages/voice/src/speakable.js +111 -0
  322. package/dist/packages/voice/src/stt/whisper.js +4 -2
  323. package/dist/packages/voice/src/tts/piper-setup.js +229 -0
  324. package/dist/packages/voice/src/tts/piper.js +309 -66
  325. package/dist/packages/voice/src/tts/say.js +3 -2
  326. package/dist/packages/voice/src/tts/voices.js +8 -5
  327. package/package.json +6 -5
@@ -0,0 +1,615 @@
1
+ // dexterm — PNL CARDS (task-1407). One key (`H` on the positions, token and followed-wallets screens) turns a coin's
2
+ // RECORDED fills into a shareable result card: a 1200×675 (X card, 16:9) and a 1080×1080 (1:1) PNG plus a plain-text
3
+ // version, saved to ~/.dexterm/exports. Share to X = save + copy the image to the OS clipboard + open X's compose
4
+ // intent in the user's own browser with neutral text; the user pastes the image and posts. dexterm never posts,
5
+ // never holds X credentials and makes no network call here.
6
+ //
7
+ // Honesty rules, enforced by construction:
8
+ // - The stamp (PAPER / LIVE / TRACKED WALLET) comes from `CardInput.source` and is drawn by the renderer in three
9
+ // places (the stamp block, the frame colour, the stamp strip). No option turns it off; an unknown source throws.
10
+ // - Numbers come only from fills (the paper ledger, the signer's live ledger, a tracked wallet's decoded history).
11
+ // There is no API that takes a PnL, a price or a size; every figure is computed here from those fills, with
12
+ // the time window, the fee basis, what was excluded and the data ages printed on the card.
13
+ // - Wallet addresses (and LIVE tx signatures, which point at the wallet) are hidden unless `showWallet`; size is a
14
+ // band unless `exactSize`; no balance, no key, no file path is ever drawn.
15
+ // - Footer: "not financial advice" and the dexterm URL. No performance claims beyond this one position's fills.
16
+ //
17
+ // Rendering reuses the task-1401 exporter stack: a cell Canvas rasterized by @dexterm/tui (the 5×9 bitmap font) and
18
+ // encoded by @dexterm/data. Big type (the stamp, the ticker, the PnL %) is the same font rasterized at a larger
19
+ // scale and pasted into reserved cell rects, auto-fitted so it can never clip.
20
+ import { mkdirSync, writeFileSync } from 'node:fs';
21
+ import { join } from 'node:path';
22
+ import { spawn } from 'node:child_process';
23
+ import { Canvas, THEMES, rasterizeCanvas, fmtPriceBare, mix } from '../../tui/src/index.js';
24
+ import { encodePng, lamportsToSol } from '../../data/src/index.js';
25
+ import { exportsDir, fmtAge } from "./export.js";
26
+ import { cleanText } from "./screener/rows.js";
27
+ /** The URL on every card footer and in the default share text. One place, so it can change in one place. */
28
+ export const DEXTERM_URL = 'dexterm.xyz';
29
+ /** Refusal to build a card, with the reason the UI shows. */
30
+ export class CardError extends Error {
31
+ constructor(message) { super(message); this.name = 'CardError'; }
32
+ }
33
+ // ------------------------------------------------------------------------------------------------ ledger adapters
34
+ /** PAPER: the paper wallet's TradeRecords for one mint (the positions screen's history). */
35
+ export function paperCardInput(records, o) {
36
+ const rs = records.filter((r) => r.mint === o.mint);
37
+ return {
38
+ source: 'paper', mint: o.mint, symbol: o.symbol ?? rs[0]?.symbol ?? '?', name: o.name ?? null, decimals: rs[0]?.decimals ?? null,
39
+ fills: rs.map((r) => ({ t: r.t, side: r.side, solDelta: lamportsToSol(r.solDelta), tokenDelta: r.tokenDelta, feeSol: lamportsToSol(r.feeLamports), sig: null })),
40
+ ledger: 'dexterm paper ledger', wallet: null, mark: o.mark ?? null,
41
+ excluded: [], fees: 'simulated network fee included', image: o.image ?? null,
42
+ };
43
+ }
44
+ /** LIVE: the signer's ledger rows (confirmed on-chain fills, swaps only) for one mint. */
45
+ export function liveCardInput(rows, o) {
46
+ const rs = rows.filter((r) => r.mint === o.mint && r.kind === 'swap');
47
+ const nets = new Set(rs.map((r) => r.network));
48
+ const network = nets.size > 1 ? 'mixed' : rs[0]?.network === 'devnet' ? 'devnet' : 'mainnet';
49
+ return {
50
+ source: 'live', network, mint: o.mint, symbol: o.symbol ?? rs[0]?.symbol ?? '?', name: o.name ?? null, decimals: rs[0]?.decimals ?? null,
51
+ fills: rs.map((r) => ({ t: r.t, side: r.side === 'sell' ? 'sell' : 'buy', solDelta: lamportsToSol(BigInt(r.solDelta)), tokenDelta: BigInt(r.tokenDelta), feeSol: lamportsToSol(BigInt(r.feeLamports)), sig: r.signature })),
52
+ ledger: 'signer live ledger', wallet: o.wallet ?? null, mark: o.mark ?? null,
53
+ excluded: network === 'mixed' ? ['devnet and mainnet fills are both in the ledger for this coin'] : [], fees: 'network + priority fees included', image: o.image ?? null,
54
+ };
55
+ }
56
+ /** TRACKED: a followed wallet's decoded swaps for one mint (read-only public history). */
57
+ export function trackedCardInput(trades, o) {
58
+ const all = trades.filter((t) => t.mint === o.mint && t.wallet === o.wallet);
59
+ const kept = [];
60
+ let noSol = 0, transfers = 0, lp = 0, airdrop = 0, timeUnknown = 0;
61
+ for (const t of all) {
62
+ if (t.side === 'transfer') {
63
+ transfers++;
64
+ continue;
65
+ }
66
+ if (t.note === 'lp-like') {
67
+ lp++;
68
+ continue;
69
+ }
70
+ if (t.note === 'airdrop') {
71
+ airdrop++;
72
+ continue;
73
+ }
74
+ if (t.solDelta === null || t.note === 'token-swap') {
75
+ noSol++;
76
+ continue;
77
+ }
78
+ if (!t.tKnown)
79
+ timeUnknown++;
80
+ kept.push({ t: t.t, side: t.side, solDelta: t.solDelta, tokenDelta: BigInt(t.tokenDelta), feeSol: null, sig: t.sig });
81
+ }
82
+ const excluded = [];
83
+ const n = (k, w) => `${k} ${w}${k === 1 ? '' : 's'}`;
84
+ if (noSol)
85
+ excluded.push(`${n(noSol, 'token-to-token swap')} (no SOL leg)`);
86
+ if (transfers)
87
+ excluded.push(n(transfers, 'transfer'));
88
+ if (airdrop)
89
+ excluded.push(`${n(airdrop, 'airdrop')} (no cost)`);
90
+ if (lp)
91
+ excluded.push(`${n(lp, 'LP-like move')}`);
92
+ if (timeUnknown)
93
+ excluded.push(`${n(timeUnknown, 'fill')} without a block time (fetch time used)`);
94
+ if (o.complete === false)
95
+ excluded.push('wallet history older than the fetched pages');
96
+ return {
97
+ source: 'tracked', mint: o.mint, symbol: o.symbol ?? o.mint.slice(0, 4), name: o.name ?? null, decimals: o.decimals ?? null,
98
+ fills: kept, ledger: 'tracked wallet history', wallet: o.wallet, mark: o.mark ?? null, historyAt: o.historyAt ?? null,
99
+ excluded, fees: 'network fees + tips included (balance deltas)', image: o.image ?? null,
100
+ };
101
+ }
102
+ /** Cost-basis maths over the fills (average cost, the paper and live ledgers' own rule). Throws CardError when a card would mislead. */
103
+ export function computeCard(input, now) {
104
+ if (input.source !== 'paper' && input.source !== 'live' && input.source !== 'tracked')
105
+ throw new CardError('a PnL card needs a source stamp: paper, live or tracked');
106
+ const fills = [...input.fills].sort((a, b) => a.t - b.t);
107
+ if (!fills.length)
108
+ throw new CardError(`no recorded fills for $${cleanText(input.symbol, 12)} in the ${input.ledger}`);
109
+ if (!fills.some((f) => f.side === 'buy'))
110
+ throw new CardError('no recorded buy for this coin, so there is no cost basis to measure against');
111
+ let held = 0n, peak = 0n, cost = 0, costIn = 0, proceeds = 0, realized = 0, boughtRaw = 0n, soldRaw = 0n, buys = 0, sells = 0, unmatched = 0;
112
+ let fees = 0, feesKnown = false, firstBuy = Infinity, lastSell = -Infinity;
113
+ for (const f of fills) {
114
+ if (f.feeSol !== null) {
115
+ fees += f.feeSol;
116
+ feesKnown = true;
117
+ }
118
+ if (f.side === 'buy') {
119
+ if (f.tokenDelta <= 0n || f.solDelta >= 0)
120
+ continue; // not a priced buy
121
+ buys++;
122
+ firstBuy = Math.min(firstBuy, f.t);
123
+ held += f.tokenDelta;
124
+ boughtRaw += f.tokenDelta;
125
+ if (held > peak)
126
+ peak = held;
127
+ cost += -f.solDelta;
128
+ costIn += -f.solDelta;
129
+ }
130
+ else {
131
+ const sold = -f.tokenDelta;
132
+ if (sold <= 0n || f.solDelta <= 0)
133
+ continue;
134
+ sells++;
135
+ lastSell = Math.max(lastSell, f.t);
136
+ // tokens sold beyond what the recorded buys hold have no cost basis: only the matched share counts
137
+ const matched = sold > held ? held : sold;
138
+ if (matched < sold)
139
+ unmatched++;
140
+ if (matched <= 0n)
141
+ continue;
142
+ const share = Number(matched) / Number(sold);
143
+ const removed = held > 0n ? cost * (Number(matched) / Number(held)) : 0;
144
+ const got = f.solDelta * share;
145
+ proceeds += got;
146
+ realized += got - removed;
147
+ cost -= removed;
148
+ held -= matched;
149
+ soldRaw += matched;
150
+ }
151
+ }
152
+ if (!buys)
153
+ throw new CardError('no priced buy for this coin, so there is no cost basis to measure against');
154
+ if (unmatched)
155
+ input = { ...input, excluded: [...input.excluded, `${unmatched} sell${unmatched === 1 ? '' : 's'} beyond the recorded buys (only the matched part counts)`] };
156
+ const dust = peak > 0n && held * 100n <= peak;
157
+ const open = held > 0n && !dust;
158
+ if (held > 0n && dust)
159
+ input = { ...input, excluded: [...input.excluded, 'dust under 1 % of the peak holding'] };
160
+ let unrealized = null;
161
+ if (open) {
162
+ const m = input.mark;
163
+ if (!m || !Number.isFinite(m.priceSol) || m.priceSol <= 0)
164
+ throw new CardError('the position is still open and has no price mark yet (positions mark every 10 s); try again in a moment');
165
+ if (input.decimals === null)
166
+ throw new CardError('the position is still open and the token decimals are unknown, so it cannot be marked');
167
+ const value = (Number(held) / 10 ** input.decimals) * m.priceSol;
168
+ unrealized = value - cost;
169
+ }
170
+ const pnl = realized + (unrealized ?? 0);
171
+ const whole = (raw) => (input.decimals === null ? null : Number(raw) / 10 ** input.decimals);
172
+ const boughtW = whole(boughtRaw), soldW = whole(soldRaw);
173
+ return {
174
+ input, buys, sells, firstT: fills[0].t, lastT: fills[fills.length - 1].t, open,
175
+ costInSol: costIn, proceedsSol: proceeds, realizedSol: realized, unrealizedSol: unrealized, pnlSol: pnl, pnlPct: costIn > 0 ? pnl / costIn : 0,
176
+ kind: !open ? 'realized' : sells ? 'realized + unrealized' : 'unrealized',
177
+ entryAvg: boughtW ? costIn / boughtW : null, exitAvg: soldW ? proceeds / soldW : null,
178
+ holdMs: Math.max(0, (open ? now : lastSell === -Infinity ? now : lastSell) - (firstBuy === Infinity ? fills[0].t : firstBuy)),
179
+ feesSol: feesKnown ? fees : null, now,
180
+ };
181
+ }
182
+ // ------------------------------------------------------------------------------------------------ formatting
183
+ /** Size bands (SOL spent on buys). The default card shows the band, never the amount. */
184
+ export const SIZE_BANDS = [0.1, 0.5, 1, 5, 10, 50, 100];
185
+ export function sizeBand(sol) {
186
+ if (sol < SIZE_BANDS[0])
187
+ return `under ${SIZE_BANDS[0]} SOL`;
188
+ for (let i = 1; i < SIZE_BANDS.length; i++)
189
+ if (sol < SIZE_BANDS[i])
190
+ return `${SIZE_BANDS[i - 1]}-${SIZE_BANDS[i]} SOL`;
191
+ return `${SIZE_BANDS[SIZE_BANDS.length - 1]}+ SOL`;
192
+ }
193
+ /** `+28.4%` / `-7.1%` / `+1,284%`. */
194
+ export function fmtPnlPct(frac) {
195
+ const p = frac * 100, a = Math.abs(p);
196
+ const body = a >= 1000 ? Math.round(a).toLocaleString('en-US') : a.toFixed(1);
197
+ return `${p >= 0 ? '+' : '-'}${body}%`;
198
+ }
199
+ /** `38s` / `45m` / `3h 12m` / `9d 4h`. */
200
+ export function fmtHold(ms) {
201
+ const s = Math.max(0, Math.round(ms / 1000));
202
+ if (s < 60)
203
+ return `${s}s`;
204
+ const m = Math.floor(s / 60);
205
+ if (m < 60)
206
+ return `${m}m`;
207
+ const h = Math.floor(m / 60);
208
+ if (h < 24)
209
+ return `${h}h ${m % 60}m`;
210
+ return `${Math.floor(h / 24)}d ${h % 24}h`;
211
+ }
212
+ /** Local `2026-10-16 01:23`. */
213
+ export function fmtWhen(t) {
214
+ const d = new Date(t);
215
+ const p = (n) => String(n).padStart(2, '0');
216
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
217
+ }
218
+ const signedSol = (n) => `${n >= 0 ? '+' : '-'}${Math.abs(n).toFixed(4)} SOL`;
219
+ const short = (a) => (a.length > 10 ? `${a.slice(0, 4)}…${a.slice(-4)}` : a);
220
+ /** The stamp of a source. Exhaustive: anything else throws, so a card can never be drawn without one. */
221
+ export function stampOf(s, t) {
222
+ const src = s.input.source;
223
+ if (src === 'paper')
224
+ return { big: 'PAPER', line: 'SIMULATED TRADE · NO REAL FUNDS', strip: 'PAPER · SIMULATED', color: t.warn, note: 'PAPER: simulated in dexterm\'s paper wallet against live quotes; no real funds moved.' };
225
+ if (src === 'live') {
226
+ const net = s.input.network;
227
+ const sigs = s.input.fills.filter((f) => f.sig).length;
228
+ return {
229
+ big: 'LIVE', line: net === 'devnet' ? 'ON-CHAIN · DEVNET TEST FUNDS' : net === 'mixed' ? 'ON-CHAIN · DEVNET + MAINNET' : 'ON-CHAIN · WALLET-SIGNED', strip: net === 'devnet' ? 'LIVE · DEVNET' : 'LIVE · WALLET-SIGNED', color: t.up,
230
+ note: `LIVE: ${sigs} wallet-signed tx${sigs === 1 ? '' : 's'} confirmed on ${net === 'devnet' ? 'devnet (test funds, not real money)' : net === 'mixed' ? 'devnet and mainnet' : 'Solana mainnet'}, verifiable on-chain.`,
231
+ };
232
+ }
233
+ if (src === 'tracked')
234
+ return { big: 'TRACKED', line: 'WALLET · READ-ONLY OBSERVATION', strip: 'TRACKED WALLET · READ-ONLY', color: t.accent, note: 'TRACKED: a read-only view of a public wallet\'s decoded history. Not a recommendation.' };
235
+ throw new CardError('a PnL card needs a source stamp: paper, live or tracked');
236
+ }
237
+ /**
238
+ * The provenance lines (window, fees, exclusions, data ages; the wallet and LIVE tx signatures only when shown).
239
+ * `card` keeps signatures short to fit the PNG; `text` prints them in full so they can be checked on an explorer.
240
+ */
241
+ export function provenance(s, o, mode = 'text') {
242
+ const i = s.input;
243
+ const n = i.fills.length;
244
+ const L = [];
245
+ L.push(`window: ${fmtWhen(s.firstT)} to ${s.open ? `now (${fmtWhen(s.now)})` : fmtWhen(s.lastT)} local · ${n} recorded fill${n === 1 ? '' : 's'} · ${i.ledger}`);
246
+ L.push(`fees: ${i.fees}${s.feesSol !== null && o.exactSize ? ` (${s.feesSol.toFixed(6)} SOL)` : ''} · excluded: ${i.excluded.length ? i.excluded.join('; ') : 'nothing'}`);
247
+ const ages = [`last fill ${fmtAge((s.now - s.lastT) / 1000)} ago`];
248
+ if (i.historyAt)
249
+ ages.push(`history fetched ${fmtAge((s.now - i.historyAt) / 1000)} ago`);
250
+ ages.push(s.open && i.mark ? `mark ${fmtAge((s.now - i.mark.at) / 1000)} old (${i.mark.via})` : 'no mark needed (closed)');
251
+ if (mode === 'text')
252
+ ages.push(`card made ${fmtWhen(s.now)}`);
253
+ L.push(`data ages: ${ages.join(' · ')}`);
254
+ if (o.showWallet && i.wallet)
255
+ L.push(`wallet: ${i.wallet}`);
256
+ if (i.source === 'live' && o.showWallet) {
257
+ const sigs = i.fills.map((f) => f.sig).filter((x) => !!x);
258
+ const fmt = (x) => (mode === 'card' ? `${x.slice(0, 8)}…${x.slice(-6)}` : x);
259
+ const k = mode === 'card' ? 3 : sigs.length;
260
+ if (sigs.length)
261
+ L.push(`tx: ${sigs.slice(0, k).map(fmt).join(' ')}${sigs.length > k ? ` +${sigs.length - k} more` : ''}`);
262
+ }
263
+ return L;
264
+ }
265
+ // ------------------------------------------------------------------------------------------------ the card canvas
266
+ /** Pixel geometry per aspect: cols × rows cells of cellW × cellH px = exactly the X card / square sizes. */
267
+ export const CARD_GEOMETRY = {
268
+ wide: { w: 1200, h: 675, cols: 100, rows: 27, cellW: 12, cellH: 25 },
269
+ square: { w: 1080, h: 1080, cols: 90, rows: 45, cellW: 12, cellH: 24 },
270
+ };
271
+ /** Deterministic hash for the background code sprinkle (same card → same pixels). */
272
+ function hash(a, b, c) {
273
+ let h = (a * 374761393 + b * 668265263 + c * 2147483647) | 0;
274
+ h = Math.imul(h ^ (h >>> 13), 1274126177);
275
+ return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
276
+ }
277
+ export function renderCard(s, o = {}) {
278
+ const aspect = o.aspect ?? 'wide';
279
+ const g = CARD_GEOMETRY[aspect];
280
+ const t = THEMES.cyberspace;
281
+ const stamp = stampOf(s, t);
282
+ const c = new Canvas(g.cols, g.rows, t);
283
+ const big = [];
284
+ const clipped = [];
285
+ const W = g.cols, H = g.rows;
286
+ const panel = t.panel;
287
+ const put = (x, y, str, st, maxW) => {
288
+ if ([...str].length > maxW)
289
+ clipped.push(str);
290
+ return c.text(x, y, str, { fg: st.fg, bg: st.bg ?? panel, b: st.b }, maxW);
291
+ };
292
+ const putR = (xRight, y, str, st) => {
293
+ const len = [...str].length;
294
+ put(xRight - len + 1, y, str, st, len);
295
+ };
296
+ const wrap = (x, y, w, maxLines, lines, fg) => {
297
+ const out = [];
298
+ for (const line of lines) {
299
+ let cur = '';
300
+ for (const word of line.split(' ')) {
301
+ const next = cur ? `${cur} ${word}` : word;
302
+ if ([...next].length > w && cur) {
303
+ out.push(cur);
304
+ cur = ` ${word}`;
305
+ }
306
+ else
307
+ cur = next;
308
+ }
309
+ out.push(cur);
310
+ }
311
+ if (out.length > maxLines)
312
+ clipped.push(...out.slice(maxLines));
313
+ out.slice(0, maxLines).forEach((l, k) => put(x, y + k, l, { fg }, w));
314
+ return y + Math.min(out.length, maxLines);
315
+ };
316
+ c.fill(0, 0, W, H, ' ', { bg: t.bg });
317
+ c.fill(1, 2, W - 2, H - 4, ' ', { bg: panel });
318
+ // header band (outside the frame) and footer band
319
+ c.fill(0, 0, W, 1, ' ', { bg: t.headBg });
320
+ let hx = put(2, 0, '◆ dexterm', { fg: t.borderFocus, bg: t.headBg, b: true }, 12);
321
+ put(hx + 1, 0, '· PnL card', { fg: t.muted, bg: t.headBg }, 12);
322
+ putR(W - 3, 0, fmtWhen(s.now), { fg: t.dim, bg: t.headBg });
323
+ c.fill(0, H - 1, W, 1, ' ', { bg: t.headBg });
324
+ put(2, H - 1, aspect === 'wide' ? 'not financial advice · trading in dexterm is experimental · from recorded fills' : 'not financial advice · trading in dexterm is experimental', { fg: t.muted, bg: t.headBg }, W - 16);
325
+ putR(W - 3, H - 1, DEXTERM_URL, { fg: t.borderFocus, bg: t.headBg, b: true });
326
+ // frame in the stamp colour (rows 1 .. H-2)
327
+ const fc = mix(t.bg, stamp.color, 0.6);
328
+ for (let x = 0; x < W; x++) {
329
+ c.put(x, 1, '━', { fg: fc, bg: t.bg });
330
+ c.put(x, H - 2, '━', { fg: fc, bg: t.bg });
331
+ }
332
+ for (let y = 1; y < H - 1; y++) {
333
+ c.put(0, y, '┃', { fg: fc, bg: t.bg });
334
+ c.put(W - 1, y, '┃', { fg: fc, bg: t.bg });
335
+ }
336
+ c.put(0, 1, '┏', { fg: fc, bg: t.bg });
337
+ c.put(W - 1, 1, '┓', { fg: fc, bg: t.bg });
338
+ c.put(0, H - 2, '┗', { fg: fc, bg: t.bg });
339
+ c.put(W - 1, H - 2, '┛', { fg: fc, bg: t.bg });
340
+ const up = s.pnlSol >= 0;
341
+ const pnlColor = up ? t.up : t.down;
342
+ const sym = `$${cleanText(s.input.symbol, 12) || '?'}`;
343
+ const name = cleanText(s.input.name ?? '', 40);
344
+ const walletLine = s.input.wallet
345
+ ? (o.showWallet ? `wallet ${short(s.input.wallet)}` : 'wallet hidden')
346
+ : s.input.source === 'paper' ? 'paper wallet (simulated)' : 'wallet hidden';
347
+ const idLine = `mint ${short(s.input.mint)} · ${walletLine}`;
348
+ const label = `${up ? '▲' : '▼'} ${s.kind.toUpperCase()} PnL`;
349
+ const sub = o.exactSize
350
+ ? `${signedSol(s.pnlSol)} on ${s.costInSol.toFixed(4)} SOL in${s.unrealizedSol !== null ? ` (realized ${signedSol(s.realizedSol)})` : ''}`
351
+ : 'return on SOL spent · net of fees · size shown as a band';
352
+ const dec = s.input.decimals !== null;
353
+ const px = (p) => (p === null ? (dec ? '—' : 'n/a (decimals unknown)') : `${fmtPriceBare(p)} SOL`);
354
+ const stats = [
355
+ ['ENTRY avg', px(s.entryAvg)],
356
+ s.open ? ['MARK', px(s.input.mark?.priceSol ?? null)] : ['EXIT avg', px(s.exitAvg)],
357
+ ['HOLD', `${fmtHold(s.holdMs)}${s.open ? ' (open)' : ''}`],
358
+ ['SIZE', o.exactSize ? `${s.costInSol.toFixed(4)} SOL in` : `${sizeBand(s.costInSol)} (band)`],
359
+ ['FILLS', `${s.buys} buy${s.buys === 1 ? '' : 's'} · ${s.sells} sell${s.sells === 1 ? '' : 's'}`],
360
+ ['OPENED', fmtWhen(s.firstT)],
361
+ ['CLOSED', s.open ? 'still open' : fmtWhen(s.lastT)],
362
+ ];
363
+ const prov = [...provenance(s, o, 'card'), stamp.note];
364
+ const coinTile = (x, y, w, h) => {
365
+ if (s.input.image && s.input.image.w > 0 && s.input.image.h > 0) {
366
+ c.fill(x, y, w, h, ' ', { bg: panel });
367
+ c.images.push({ x, y, w, h, key: 'pnl-coin', img: s.input.image });
368
+ }
369
+ else {
370
+ const tile = mix(panel, t.accent, 0.16);
371
+ c.fill(x, y, w, h, ' ', { bg: tile });
372
+ big.push({ x, y, w, h, text: (cleanText(s.input.symbol, 2) || '?').slice(0, 1).toUpperCase(), fg: t.accent, bg: tile, align: 'center' });
373
+ }
374
+ };
375
+ const stampBlock = (x, y, w, h) => {
376
+ c.fill(x, y, w, h, ' ', { bg: stamp.color });
377
+ big.push({ x: x + 2, y, w: w - 4, h: h - 1, text: stamp.big, fg: '#000000', bg: stamp.color, align: 'center' });
378
+ const len = [...stamp.line].length;
379
+ put(x + Math.max(0, Math.floor((w - len) / 2)), y + h - 1, stamp.line, { fg: '#000000', bg: stamp.color, b: true }, w);
380
+ };
381
+ const strip = (y) => {
382
+ const bg = mix(panel, stamp.color, 0.2);
383
+ c.fill(1, y, W - 2, 1, ' ', { bg });
384
+ const unit = `◈ ${stamp.strip} `;
385
+ let x = 2;
386
+ while (x + [...unit].length <= W - 2) {
387
+ c.text(x, y, unit, { fg: stamp.color, bg, b: true });
388
+ x += [...unit].length;
389
+ }
390
+ };
391
+ const statRows = (x, y, rows, w) => {
392
+ rows.forEach(([k, v], i) => { put(x, y + i, k.padEnd(11), { fg: t.dim }, 11); put(x + 11, y + i, v, { fg: t.text, b: true }, w - 11); });
393
+ };
394
+ let stampRect;
395
+ if (aspect === 'wide') {
396
+ stampRect = { x: 62, y: 3, w: 35, h: 5 };
397
+ stampBlock(stampRect.x, stampRect.y, stampRect.w, stampRect.h);
398
+ coinTile(3, 3, 8, 4);
399
+ c.fill(13, 3, 47, 2, ' ', { bg: panel });
400
+ big.push({ x: 13, y: 3, w: 47, h: 2, text: sym, fg: t.text, bg: panel, align: 'left' });
401
+ put(13, 5, name || ' ', { fg: t.muted }, 47);
402
+ put(13, 6, idLine, { fg: t.dim }, 47);
403
+ c.hline(2, 8, W - 4, { fg: t.border, bg: panel });
404
+ put(3, 9, label, { fg: pnlColor, b: true }, 56);
405
+ c.fill(3, 10, 56, 6, ' ', { bg: panel });
406
+ big.push({ x: 3, y: 10, w: 56, h: 6, text: fmtPnlPct(s.pnlPct), fg: pnlColor, bg: panel, align: 'left' });
407
+ put(3, 16, sub, { fg: t.muted }, 56);
408
+ statRows(62, 9, stats, 35);
409
+ c.hline(2, 17, W - 4, { fg: t.border, bg: panel });
410
+ wrap(3, 18, W - 6, 6, prov, t.muted);
411
+ strip(H - 3);
412
+ }
413
+ else {
414
+ stampRect = { x: 3, y: 3, w: W - 6, h: 6 };
415
+ stampBlock(stampRect.x, stampRect.y, stampRect.w, stampRect.h);
416
+ coinTile(3, 11, 8, 4);
417
+ c.fill(13, 11, W - 16, 2, ' ', { bg: panel });
418
+ big.push({ x: 13, y: 11, w: W - 16, h: 2, text: sym, fg: t.text, bg: panel, align: 'left' });
419
+ put(13, 13, name || ' ', { fg: t.muted }, W - 16);
420
+ put(13, 14, idLine, { fg: t.dim }, W - 16);
421
+ c.hline(2, 16, W - 4, { fg: t.border, bg: panel });
422
+ put(3, 17, label, { fg: pnlColor, b: true }, W - 6);
423
+ c.fill(3, 18, W - 6, 7, ' ', { bg: panel });
424
+ big.push({ x: 3, y: 18, w: W - 6, h: 7, text: fmtPnlPct(s.pnlPct), fg: pnlColor, bg: panel, align: 'center' });
425
+ put(3 + Math.max(0, Math.floor((W - 6 - [...sub].length) / 2)), 25, sub, { fg: t.muted }, W - 6);
426
+ c.hline(2, 27, W - 4, { fg: t.border, bg: panel });
427
+ statRows(3, 28, stats.slice(0, 4), 41);
428
+ statRows(46, 28, stats.slice(4), 41);
429
+ c.hline(2, 33, W - 4, { fg: t.border, bg: panel });
430
+ wrap(3, 34, W - 6, 7, prov, t.muted);
431
+ strip(H - 3);
432
+ }
433
+ // texture last: a sparse, dim code sprinkle (the cyberspace look) only in cells with blank neighbours, never in a
434
+ // big-text rect, so it can never touch or be read as part of a label or a number
435
+ const inBig = (x, y) => big.some((b) => x >= b.x - 1 && x < b.x + b.w + 1 && y >= b.y && y < b.y + b.h);
436
+ const blank = (x, y) => x < 1 || x >= W - 1 || c.ch[c.idx(x, y)] === ' ';
437
+ const code = `${s.input.mint}0123456789abcdef`;
438
+ const fx = mix(panel, t.rain[1], 0.55);
439
+ for (let y = 2; y < H - 2; y++)
440
+ for (let x = 1; x < W - 1; x++) {
441
+ if (hash(x, y, 1407) >= 0.045 || c.bg[c.idx(x, y)] !== panel || inBig(x, y))
442
+ continue;
443
+ let clear = true;
444
+ for (let dx = -2; dx <= 2 && clear; dx++)
445
+ if (!blank(x + dx, y))
446
+ clear = false;
447
+ if (clear)
448
+ c.put(x, y, code[Math.floor(hash(y, x, 7) * code.length)], { fg: fx, bg: panel, soft: true });
449
+ }
450
+ return { aspect, canvas: c, big, clipped, stampRect, stamp };
451
+ }
452
+ /** The largest integer font scale at which `text` fits a w×h px box (glyph cell 6s × 10s); 0 = does not fit. */
453
+ export function fitScale(text, wPx, hPx) {
454
+ const n = Math.max(1, [...text].length);
455
+ return Math.max(0, Math.min(Math.floor(wPx / (6 * n)), Math.floor(hPx / 10)));
456
+ }
457
+ /** Rasterize a card render to exact-size RGBA: the cell canvas, then each big text pasted at its fitted scale. */
458
+ export function cardPixels(r) {
459
+ const g = CARD_GEOMETRY[r.aspect];
460
+ const base = rasterizeCanvas(r.canvas, { cellW: g.cellW, cellH: g.cellH, scale: 2 });
461
+ const scales = [];
462
+ for (const b of r.big) {
463
+ const rw = b.w * g.cellW, rh = b.h * g.cellH;
464
+ const s = fitScale(b.text, rw, rh);
465
+ scales.push(s);
466
+ if (s < 1) {
467
+ r.clipped.push(b.text);
468
+ continue;
469
+ }
470
+ const n = [...b.text].length;
471
+ const mini = new Canvas(n, 1, r.canvas.theme);
472
+ mini.fill(0, 0, n, 1, ' ', { bg: b.bg });
473
+ mini.text(0, 0, b.text, { fg: b.fg, bg: b.bg, b: true });
474
+ const img = rasterizeCanvas(mini, { cellW: 6 * s, cellH: 10 * s, scale: s });
475
+ const ox = b.x * g.cellW + (b.align === 'center' ? Math.floor((rw - img.w) / 2) : 0);
476
+ const oy = b.y * g.cellH + Math.floor((rh - img.h) / 2);
477
+ for (let y = 0; y < img.h; y++) {
478
+ const dy = oy + y;
479
+ if (dy < 0 || dy >= base.h)
480
+ continue;
481
+ const src = y * img.w * 4, dst = (dy * base.w + ox) * 4;
482
+ base.rgba.set(img.rgba.subarray(src, src + Math.min(img.w, base.w - ox) * 4), dst);
483
+ }
484
+ }
485
+ return { w: base.w, h: base.h, rgba: base.rgba, scales };
486
+ }
487
+ export function cardPng(s, o = {}) {
488
+ const px = cardPixels(renderCard(s, o));
489
+ return encodePng({ w: px.w, h: px.h, rgba: px.rgba });
490
+ }
491
+ /** The terminal preview: the card canvas with the big texts written at cell size (bold), centred in their rects. */
492
+ export function previewCanvas(r) {
493
+ const c = new Canvas(r.canvas.w, r.canvas.h, r.canvas.theme);
494
+ c.copyFrom(r.canvas);
495
+ c.images.length = 0;
496
+ for (const b of r.big) {
497
+ const len = [...b.text].length;
498
+ const x = b.align === 'center' ? b.x + Math.max(0, Math.floor((b.w - len) / 2)) : b.x;
499
+ c.text(x, b.y + Math.floor((b.h - 1) / 2), b.text, { fg: b.fg, bg: b.bg, b: true }, b.w);
500
+ }
501
+ return c;
502
+ }
503
+ // ------------------------------------------------------------------------------------------------ text + share text
504
+ /** "Copy as text": the same facts as the PNG, plain. */
505
+ export function cardText(s, o = {}) {
506
+ const st = stampOf(s, THEMES.cyberspace);
507
+ const L = [];
508
+ L.push(`dexterm PnL card · ${st.big}${s.input.source === 'tracked' ? ' WALLET' : ''} (${st.line.toLowerCase()})`);
509
+ L.push(`$${cleanText(s.input.symbol, 12)}${s.input.name ? ` · ${cleanText(s.input.name, 40)}` : ''} · mint ${short(s.input.mint)}${s.input.wallet ? ` · ${o.showWallet ? `wallet ${s.input.wallet}` : 'wallet hidden'}` : ''}`);
510
+ L.push(`${s.kind} PnL ${fmtPnlPct(s.pnlPct)} (net of fees)${o.exactSize ? ` · ${signedSol(s.pnlSol)} on ${s.costInSol.toFixed(4)} SOL in` : ` · size ${sizeBand(s.costInSol)} (band)`}`);
511
+ const dec = s.input.decimals !== null;
512
+ const p = (v) => (v === null ? (dec ? '—' : 'n/a') : `${fmtPriceBare(v)} SOL`);
513
+ L.push(`entry avg ${p(s.entryAvg)} · ${s.open ? `mark ${p(s.input.mark?.priceSol ?? null)}` : `exit avg ${p(s.exitAvg)}`} · hold ${fmtHold(s.holdMs)}${s.open ? ' (open)' : ''} · ${s.buys} buys · ${s.sells} sells`);
514
+ L.push(...provenance(s, o));
515
+ L.push(st.note + ' Trading in dexterm is experimental.');
516
+ L.push(`not financial advice · ${DEXTERM_URL}`);
517
+ return L.join('\n') + '\n';
518
+ }
519
+ /**
520
+ * The default X compose text. Neutral by rule (dexterm/marketing/GUARDRAILS.md): no coin, ticker or address, no
521
+ * price, PnL or return talk, no performance claim, and the stamp in words. The user can edit it in X before posting.
522
+ */
523
+ export function shareText(source) {
524
+ const what = source === 'paper' ? 'PAPER: simulated, no real funds.' : source === 'live' ? 'LIVE: from my own wallet-signed transactions.' : 'TRACKED WALLET: a read-only view of a public wallet.';
525
+ return `Made this card in dexterm, a terminal for Solana. ${what} Not financial advice. https://${DEXTERM_URL}`;
526
+ }
527
+ /** X's compose intent for the user's own browser. Only the text parameter; nothing is posted by dexterm. */
528
+ export function xIntentUrl(text) {
529
+ return `https://x.com/intent/post?text=${encodeURIComponent(text)}`;
530
+ }
531
+ /** The OS command that puts a PNG file on the clipboard, or null where dexterm has none (then the file is the way). */
532
+ export function imageClipboardCommand(platform, env, path) {
533
+ if (platform === 'darwin') {
534
+ // the path is an argv item, never interpolated into the AppleScript source
535
+ return { cmd: 'osascript', args: ['-e', 'on run argv', '-e', 'set the clipboard to (read (POSIX file (item 1 of argv)) as «class PNGf»)', '-e', 'end run', path] };
536
+ }
537
+ if (platform === 'linux') {
538
+ if (env.WAYLAND_DISPLAY)
539
+ return { cmd: 'wl-copy', args: ['--type', 'image/png'], stdinFile: path };
540
+ if (env.DISPLAY)
541
+ return { cmd: 'xclip', args: ['-selection', 'clipboard', '-t', 'image/png', '-i', path] };
542
+ }
543
+ return null;
544
+ }
545
+ /** Default `run`: spawn without a shell, 5 s cap, true on exit 0. */
546
+ export function runLocal(cmd, args, stdinFile) {
547
+ return new Promise((resolve) => {
548
+ let settled = false;
549
+ const done = (ok) => { if (!settled) {
550
+ settled = true;
551
+ resolve(ok);
552
+ } };
553
+ try {
554
+ const p = spawn(cmd, args, { stdio: [stdinFile ? 'pipe' : 'ignore', 'ignore', 'ignore'] });
555
+ const timer = setTimeout(() => { try {
556
+ p.kill();
557
+ }
558
+ catch { /* gone */ } done(false); }, 5000);
559
+ timer.unref?.();
560
+ p.on('error', () => { clearTimeout(timer); done(false); });
561
+ p.on('exit', (code) => { clearTimeout(timer); done(code === 0); });
562
+ if (stdinFile && p.stdin) {
563
+ p.stdin.on('error', () => { });
564
+ import('node:fs').then(({ createReadStream }) => createReadStream(stdinFile).on('error', () => done(false)).pipe(p.stdin));
565
+ }
566
+ }
567
+ catch {
568
+ done(false);
569
+ }
570
+ });
571
+ }
572
+ const slug = (s) => s.replace(/[^A-Za-z0-9_-]+/g, '').slice(0, 16) || 'coin';
573
+ function stampOfNow(now) {
574
+ const d = new Date(now);
575
+ const p = (n) => String(n).padStart(2, '0');
576
+ return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
577
+ }
578
+ /** Write both PNGs and the text card to ~/.dexterm/exports (dir 0700, files 0600). Local files only. */
579
+ export function saveCard(s, o, opts = {}) {
580
+ const dir = opts.dir ?? exportsDir(opts.env);
581
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
582
+ const base = join(dir, `dexterm-pnl-${s.input.source}-${slug(s.input.symbol)}-${stampOfNow(opts.now ?? s.now)}`);
583
+ const files = { wide: `${base}-16x9.png`, square: `${base}-1x1.png`, txt: `${base}.txt` };
584
+ writeFileSync(files.wide, cardPng(s, { ...o, aspect: 'wide' }), { mode: 0o600 });
585
+ writeFileSync(files.square, cardPng(s, { ...o, aspect: 'square' }), { mode: 0o600 });
586
+ writeFileSync(files.txt, cardText(s, o), { mode: 0o600 });
587
+ return files;
588
+ }
589
+ /**
590
+ * The three share actions. `save` writes the files. `text` also copies the text card. `x` also copies the 16:9 image
591
+ * to the clipboard (where the OS allows) and opens X's compose intent with the neutral text in the user's browser.
592
+ * Nothing is posted, uploaded or sent by dexterm; the user attaches the image and posts it themselves.
593
+ */
594
+ export async function shareCard(s, o, action, deps) {
595
+ const files = saveCard(s, o, { dir: deps.dir, env: deps.env, now: deps.now });
596
+ const home = deps.env.HOME;
597
+ const shown = (p) => (home && p.startsWith(home) ? `~${p.slice(home.length)}` : p);
598
+ if (action === 'save')
599
+ return { files, clipboard: null, opened: null, message: `saved ${shown(files.wide)} (+1:1, +.txt) · local only, nothing uploaded` };
600
+ if (action === 'text') {
601
+ deps.copyText(cardText(s, o));
602
+ return { files, clipboard: 'text', opened: null, message: `card text copied · saved ${shown(files.wide)} · nothing uploaded` };
603
+ }
604
+ const cmd = imageClipboardCommand(deps.platform, deps.env, files.wide);
605
+ let clipboard = 'unsupported';
606
+ if (cmd)
607
+ clipboard = (await deps.run(cmd.cmd, cmd.args, cmd.stdinFile)) ? 'image' : 'failed';
608
+ const url = xIntentUrl(shareText(s.input.source));
609
+ const opened = deps.openUrl(url) ? url : null;
610
+ const how = clipboard === 'image' ? 'image copied: paste it into the post' : `attach ${shown(files.wide)}`;
611
+ return {
612
+ files, clipboard, opened,
613
+ message: opened ? `X compose opened in your browser · ${how} · you post it, dexterm posts nothing` : `could not open the browser · ${how} · compose: ${url}`,
614
+ };
615
+ }