@gloom-sh/terminal 0.19.1 → 0.19.3

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 (554) hide show
  1. package/README.md +7 -1
  2. package/package.json +2 -1
  3. package/patches/electrobun@1.18.1.patch +108 -1
  4. package/src/account/upgrade-dialog.tsx +4 -23
  5. package/src/api-client/coalesce.ts +118 -0
  6. package/src/api-client/credit-boards.ts +13 -0
  7. package/src/api-client/crypto-etf-flows.ts +6 -0
  8. package/src/api-client/crypto-markets.ts +40 -0
  9. package/src/api-client/data.ts +201 -14
  10. package/src/api-client/equity-screener.ts +10 -0
  11. package/src/api-client/errors.ts +58 -0
  12. package/src/api-client/gpu.ts +29 -0
  13. package/src/api-client/index.ts +5 -2
  14. package/src/api-client/plan-access.ts +10 -1
  15. package/src/api-client/plan-rules.ts +35 -0
  16. package/src/api-client/research-activity.ts +1 -32
  17. package/src/api-client/types.ts +21 -1
  18. package/src/app/attention-notice.tsx +94 -0
  19. package/src/app/runtime/startup.ts +3 -1
  20. package/src/app.tsx +23 -1
  21. package/src/architecture/core-builtin-allowlist.json +2 -1
  22. package/src/capabilities/chart-series.ts +2 -1
  23. package/src/capabilities/factories.ts +1 -0
  24. package/src/cli/ascii-output.ts +42 -0
  25. package/src/cli/base-converter.ts +10 -2
  26. package/src/cli/command-options.ts +16 -1
  27. package/src/cli/commands/crypto-hints.ts +23 -4
  28. package/src/cli/commands/market.ts +101 -27
  29. package/src/cli/commands/news-search.ts +24 -5
  30. package/src/cli/commands/overview.ts +14 -10
  31. package/src/cli/commands/search.ts +84 -25
  32. package/src/cli/commands/system.ts +22 -30
  33. package/src/cli/commands/ticker.ts +49 -9
  34. package/src/cli/commands/version.ts +33 -0
  35. package/src/cli/context.ts +6 -9
  36. package/src/cli/data-dir.ts +7 -0
  37. package/src/cli/desktop-pane-shot-api-policy.ts +85 -0
  38. package/src/cli/desktop-pane-shot-browser.ts +320 -0
  39. package/src/cli/desktop-pane-shot-routes.ts +26 -1
  40. package/src/cli/desktop-pane-shot.ts +196 -256
  41. package/src/cli/early-command.ts +42 -0
  42. package/src/cli/earnings-calendar.ts +9 -0
  43. package/src/cli/entry.ts +19 -13
  44. package/src/cli/errors.ts +12 -2
  45. package/src/cli/glossary.ts +2 -1
  46. package/src/cli/help.ts +1 -1
  47. package/src/cli/history-export.ts +22 -2
  48. package/src/cli/history-rows.ts +189 -11
  49. package/src/cli/index.ts +18 -1
  50. package/src/cli/known-gaps.ts +51 -5
  51. package/src/cli/listing-arg.ts +63 -10
  52. package/src/cli/options.ts +1 -1
  53. package/src/cli/output-file.ts +60 -13
  54. package/src/cli/pane-functions/catalog.ts +14 -3
  55. package/src/cli/pane-functions/data.ts +1 -1
  56. package/src/cli/pane-functions/dom.ts +45 -0
  57. package/src/cli/pane-functions/freshness.ts +46 -9
  58. package/src/cli/pane-functions/function-help.ts +17 -9
  59. package/src/cli/pane-functions/headless.ts +22 -14
  60. package/src/cli/pane-functions/index.ts +6 -2
  61. package/src/cli/pane-functions/options.ts +6 -2
  62. package/src/cli/pane-functions/resolver.ts +27 -5
  63. package/src/cli/pane-functions/screenshot-assessment.ts +33 -0
  64. package/src/cli/pane-functions/screenshot-bridge.ts +94 -13
  65. package/src/cli/pane-functions/screenshot-http.ts +70 -0
  66. package/src/cli/pane-functions/screenshot-render.ts +19 -5
  67. package/src/cli/quote-answer.ts +49 -6
  68. package/src/cli/registry.ts +41 -13
  69. package/src/cli/result.ts +12 -6
  70. package/src/cli/ticker-resolution.ts +2 -1
  71. package/src/cli/types.ts +2 -0
  72. package/src/components/chart/composite/format.ts +3 -0
  73. package/src/components/chart-table/header.tsx +1 -1
  74. package/src/components/chart-table/layout.ts +28 -11
  75. package/src/components/command-bar/action-runtime.ts +4 -0
  76. package/src/components/command-bar/commands/direct/index.ts +18 -33
  77. package/src/components/command-bar/commands/direct/runtime.ts +5 -0
  78. package/src/components/command-bar/commands/registry.ts +10 -1
  79. package/src/components/command-bar/routes/root/command-items.ts +9 -4
  80. package/src/components/command-bar/routes/root/runtime.ts +51 -6
  81. package/src/components/command-bar/selection-runtime.ts +12 -29
  82. package/src/components/command-bar/surface/index.tsx +9 -3
  83. package/src/components/company-logo.tsx +2 -1
  84. package/src/components/layout/detached-pane-shell.tsx +2 -2
  85. package/src/components/layout/header.tsx +10 -5
  86. package/src/components/layout/market-summary.ts +25 -7
  87. package/src/components/layout/pane/footer/index.tsx +8 -2
  88. package/src/components/layout/pane/footer/model.ts +73 -6
  89. package/src/components/layout/pane/header.tsx +2 -2
  90. package/src/components/layout/pane/status-footer.ts +2 -1
  91. package/src/components/market-board/index.tsx +3 -2
  92. package/src/components/onboarding/desks-step.tsx +12 -4
  93. package/src/components/onboarding/first-run-workspace.ts +81 -4
  94. package/src/components/onboarding/onboarding-frame.tsx +4 -2
  95. package/src/components/onboarding/onboarding-keyboard.ts +8 -5
  96. package/src/components/onboarding/onboarding-wizard.tsx +60 -26
  97. package/src/components/onboarding/portfolio-step/positions-panel.tsx +7 -2
  98. package/src/components/onboarding/run-setup-again.ts +24 -0
  99. package/src/components/onboarding/use-onboarding-progress.ts +33 -15
  100. package/src/components/onboarding/use-onboarding-upgrade.ts +2 -6
  101. package/src/components/onboarding/wizard-model.ts +57 -3
  102. package/src/components/onboarding/wizard-positions.ts +1 -1
  103. package/src/components/ui/delayed-mark.tsx +56 -0
  104. package/src/components/ui/display.tsx +16 -5
  105. package/src/components/ui/external-link.tsx +3 -6
  106. package/src/components/ui/icon.tsx +16 -3
  107. package/src/components/ui/logo-mark.tsx +8 -5
  108. package/src/components/ui/stat-grid.tsx +263 -19
  109. package/src/core/app-services.ts +11 -2
  110. package/src/data/app-persistence.ts +9 -7
  111. package/src/data/config/store/node.ts +11 -5
  112. package/src/data/config/store/normalize.ts +16 -3
  113. package/src/data/plugin-cache.ts +15 -0
  114. package/src/data/plugin-state-store.ts +81 -24
  115. package/src/data/profile-permissions.ts +47 -0
  116. package/src/data/resource-store.ts +165 -90
  117. package/src/data/session-store.ts +31 -8
  118. package/src/data/sqlite/access.ts +324 -0
  119. package/src/data/sqlite/checkpointer.ts +285 -0
  120. package/src/data/sqlite/database.ts +80 -4
  121. package/src/data/sqlite/retry.ts +50 -9
  122. package/src/data/ticker-store.ts +57 -23
  123. package/src/docs/snapshot.json +191 -78
  124. package/src/i18n/es.ts +18 -2
  125. package/src/i18n/ja.ts +18 -2
  126. package/src/i18n/ko.ts +18 -2
  127. package/src/i18n/zh-cn.ts +18 -2
  128. package/src/i18n/zh-tw.ts +18 -2
  129. package/src/layout/desk-keys.ts +4 -1
  130. package/src/layout/desks.ts +38 -5
  131. package/src/layout-marketplace/gallery-desktop.tsx +45 -16
  132. package/src/market-data/batched-reads.ts +16 -0
  133. package/src/market-data/coordinator/auxiliary.ts +4 -2
  134. package/src/market-data/coordinator/index.ts +11 -2
  135. package/src/market-data/futures-board.ts +1 -1
  136. package/src/market-data/futures-contract.ts +3 -1
  137. package/src/market-data/market/index-level.ts +6 -4
  138. package/src/market-data/published-apac-sessions.ts +6 -0
  139. package/src/market-data/realized-volatility.ts +4 -2
  140. package/src/news/aggregator.ts +7 -4
  141. package/src/news/news-model.ts +11 -0
  142. package/src/news/story-id.ts +22 -0
  143. package/src/news/ticker-symbols.ts +17 -1
  144. package/src/notifications/app-notifier.ts +35 -14
  145. package/src/plugins/auto-update-node.ts +10 -6
  146. package/src/plugins/builtin/account-management/form-components.tsx +1 -1
  147. package/src/plugins/builtin/account-management/index.tsx +2 -3
  148. package/src/plugins/builtin/account-management/model.ts +1 -1
  149. package/src/plugins/builtin/alerts/cli-forms.ts +15 -1
  150. package/src/plugins/builtin/alerts/events-pane.tsx +7 -5
  151. package/src/plugins/builtin/alerts/events.ts +10 -2
  152. package/src/plugins/builtin/alerts/index.tsx +5 -5
  153. package/src/plugins/builtin/alerts/research-builder.ts +5 -1
  154. package/src/plugins/builtin/alerts/research-rules.ts +18 -0
  155. package/src/plugins/builtin/alerts/targets.ts +4 -1
  156. package/src/plugins/builtin/attention/headless.ts +1 -1
  157. package/src/plugins/builtin/attention/index.tsx +2 -4
  158. package/src/plugins/builtin/attention/listing.ts +1 -1
  159. package/src/plugins/builtin/attention/series.ts +1 -1
  160. package/src/plugins/builtin/awards/index.tsx +1 -2
  161. package/src/plugins/builtin/backtest/client.ts +1 -0
  162. package/src/plugins/builtin/backtest/index.tsx +1 -2
  163. package/src/plugins/builtin/backtest/rules.ts +3 -1
  164. package/src/plugins/builtin/backtest/window.ts +8 -7
  165. package/src/plugins/builtin/broker-manager/detail.tsx +16 -3
  166. package/src/plugins/builtin/broker-manager/index.tsx +1 -1
  167. package/src/plugins/builtin/buildout/cells.tsx +5 -0
  168. package/src/plugins/builtin/buildout/company/flow.tsx +367 -0
  169. package/src/plugins/builtin/buildout/company/id.ts +2 -0
  170. package/src/plugins/builtin/buildout/company/model.ts +247 -0
  171. package/src/plugins/builtin/buildout/company/pane.tsx +317 -0
  172. package/src/plugins/builtin/buildout/data-runtime.ts +31 -10
  173. package/src/plugins/builtin/buildout/facilities.ts +309 -0
  174. package/src/plugins/builtin/buildout/format.ts +9 -0
  175. package/src/plugins/builtin/buildout/index.tsx +51 -5
  176. package/src/plugins/builtin/buildout/model/index.ts +62 -10
  177. package/src/plugins/builtin/buildout/model/normalizers.ts +5 -0
  178. package/src/plugins/builtin/buildout/model/types.ts +11 -2
  179. package/src/plugins/builtin/buildout/pane/index.tsx +91 -20
  180. package/src/plugins/builtin/buildout/pane/status.tsx +1 -1
  181. package/src/plugins/builtin/buildout/pane/themes.tsx +110 -0
  182. package/src/plugins/builtin/buildout/table-model.ts +26 -2
  183. package/src/plugins/builtin/builtin-plugin-meta.ts +16 -0
  184. package/src/plugins/builtin/cds/headless.ts +10 -2
  185. package/src/plugins/builtin/cds/model.ts +7 -3
  186. package/src/plugins/builtin/central-bank-rates/index.tsx +1 -2
  187. package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -1
  188. package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
  189. package/src/plugins/builtin/changelog/index.tsx +3 -2
  190. package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
  191. package/src/plugins/builtin/chart-composer/headless-schema.ts +10 -0
  192. package/src/plugins/builtin/chart-composer/headless.ts +88 -21
  193. package/src/plugins/builtin/chart-composer/index.tsx +2 -3
  194. package/src/plugins/builtin/chart-composer/pane.tsx +30 -2
  195. package/src/plugins/builtin/chart-composer/series-expression.ts +3 -2
  196. package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
  197. package/src/plugins/builtin/chart-composer/studies.ts +1 -1
  198. package/src/plugins/builtin/chat/controller/index.ts +1 -1
  199. package/src/plugins/builtin/chat/controller/messages.ts +15 -4
  200. package/src/plugins/builtin/chat/ghost-user.ts +8 -0
  201. package/src/plugins/builtin/chat/index.tsx +5 -5
  202. package/src/plugins/builtin/clinical-trials/index.tsx +1 -2
  203. package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
  204. package/src/plugins/builtin/cloud/index.tsx +1 -2
  205. package/src/plugins/builtin/cloud/plugin.tsx +6 -4
  206. package/src/plugins/builtin/cloud/team/module.tsx +1 -1
  207. package/src/plugins/builtin/cloud/thesis/module.tsx +1 -1
  208. package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +24 -1
  209. package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +39 -15
  210. package/src/plugins/builtin/comment-letters/index.tsx +1 -2
  211. package/src/plugins/builtin/commodity-balances/client.ts +4 -1
  212. package/src/plugins/builtin/commodity-balances/index.tsx +3 -3
  213. package/src/plugins/builtin/commodity-balances/pane.tsx +2 -2
  214. package/src/plugins/builtin/congress-trades/aggregates.ts +13 -2
  215. package/src/plugins/builtin/congress-trades/detail.tsx +20 -4
  216. package/src/plugins/builtin/congress-trades/headless.ts +2 -0
  217. package/src/plugins/builtin/congress-trades/model.ts +92 -64
  218. package/src/plugins/builtin/congress-trades/table.tsx +14 -2
  219. package/src/plugins/builtin/connections/index.ts +2 -3
  220. package/src/plugins/builtin/cot/client.ts +28 -2
  221. package/src/plugins/builtin/cot/headless.ts +4 -1
  222. package/src/plugins/builtin/cot/index.tsx +5 -4
  223. package/src/plugins/builtin/cot/model.ts +58 -7
  224. package/src/plugins/builtin/cot/pane.tsx +6 -2
  225. package/src/plugins/builtin/cpi/client.ts +6 -5
  226. package/src/plugins/builtin/cpi/index.tsx +1 -2
  227. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  228. package/src/plugins/builtin/credit-boards/client.ts +7 -2
  229. package/src/plugins/builtin/credit-boards/headless.ts +25 -12
  230. package/src/plugins/builtin/credit-boards/model.ts +29 -0
  231. package/src/plugins/builtin/credit-boards/series.ts +3 -4
  232. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +9 -4
  233. package/src/plugins/builtin/credit-conditions/client.ts +22 -14
  234. package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
  235. package/src/plugins/builtin/crypto-board/client.ts +5 -5
  236. package/src/plugins/builtin/crypto-board/headless.ts +4 -3
  237. package/src/plugins/builtin/crypto-board/index.tsx +4 -2
  238. package/src/plugins/builtin/crypto-board/series.ts +6 -4
  239. package/src/plugins/builtin/crypto-etf-flows/client.ts +9 -2
  240. package/src/plugins/builtin/crypto-etf-flows/headless.ts +61 -6
  241. package/src/plugins/builtin/crypto-etf-flows/index.tsx +1 -2
  242. package/src/plugins/builtin/crypto-etf-flows/model.ts +14 -1
  243. package/src/plugins/builtin/crypto-etf-flows/pane.tsx +8 -5
  244. package/src/plugins/builtin/debt-maturities/headless.ts +4 -3
  245. package/src/plugins/builtin/debt-maturities/model.ts +17 -1
  246. package/src/plugins/builtin/debt-maturities/pane.tsx +18 -4
  247. package/src/plugins/builtin/debt-maturities/series.ts +3 -3
  248. package/src/plugins/builtin/debug/index.tsx +1 -1
  249. package/src/plugins/builtin/dividend-yield/client.ts +32 -2
  250. package/src/plugins/builtin/dividend-yield/headless.ts +58 -33
  251. package/src/plugins/builtin/dividend-yield/index.tsx +37 -2
  252. package/src/plugins/builtin/dividend-yield/pane.tsx +32 -13
  253. package/src/plugins/builtin/dividend-yield/ranking-reads.ts +73 -0
  254. package/src/plugins/builtin/dividend-yield/ranking-view.tsx +163 -0
  255. package/src/plugins/builtin/dividend-yield/ranking.ts +1 -1
  256. package/src/plugins/builtin/dividend-yield/view.ts +21 -0
  257. package/src/plugins/builtin/docs/index.tsx +2 -3
  258. package/src/plugins/builtin/doe/client.ts +6 -5
  259. package/src/plugins/builtin/doe/headless.ts +16 -3
  260. package/src/plugins/builtin/doe/index.tsx +5 -4
  261. package/src/plugins/builtin/doe/prices.ts +2 -3
  262. package/src/plugins/builtin/doe/series.ts +4 -4
  263. package/src/plugins/builtin/earnings/board.tsx +2 -1
  264. package/src/plugins/builtin/earnings/history.tsx +2 -1
  265. package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
  266. package/src/plugins/builtin/earnings-calls/data.ts +19 -11
  267. package/src/plugins/builtin/earnings-calls/pane.tsx +27 -14
  268. package/src/plugins/builtin/earnings-ripple/client.ts +32 -7
  269. package/src/plugins/builtin/earnings-ripple/index.tsx +1 -2
  270. package/src/plugins/builtin/econ/calendar-model.ts +29 -6
  271. package/src/plugins/builtin/econ/detail-view.tsx +16 -2
  272. package/src/plugins/builtin/econ-statistics/index.tsx +1 -2
  273. package/src/plugins/builtin/equity-screener/client.ts +1 -1
  274. package/src/plugins/builtin/equity-screener/headless.ts +1 -0
  275. package/src/plugins/builtin/equity-screener/ideas.ts +30 -1
  276. package/src/plugins/builtin/equity-screener/index.tsx +1 -2
  277. package/src/plugins/builtin/equity-screener/model.ts +28 -4
  278. package/src/plugins/builtin/equity-screener/pane.tsx +5 -0
  279. package/src/plugins/builtin/equity-screener/screen-check.ts +23 -1
  280. package/src/plugins/builtin/estimate-revisions/index.tsx +1 -2
  281. package/src/plugins/builtin/executives/data.ts +6 -1
  282. package/src/plugins/builtin/executives/pane.tsx +9 -21
  283. package/src/plugins/builtin/exposure/holdings.ts +35 -13
  284. package/src/plugins/builtin/exposure/index.tsx +1 -2
  285. package/src/plugins/builtin/fear-greed/cache.ts +4 -0
  286. package/src/plugins/builtin/fear-greed/cli.ts +44 -0
  287. package/src/plugins/builtin/fear-greed/index.tsx +5 -53
  288. package/src/plugins/builtin/fear-greed/pane.tsx +2 -1
  289. package/src/plugins/builtin/fear-greed/series.ts +6 -4
  290. package/src/plugins/builtin/fixed-income/index.tsx +1 -2
  291. package/src/plugins/builtin/futures/board-reads.ts +25 -0
  292. package/src/plugins/builtin/futures/coalesced-bump.ts +44 -0
  293. package/src/plugins/builtin/futures/index.tsx +15 -0
  294. package/src/plugins/builtin/futures/model.ts +14 -0
  295. package/src/plugins/builtin/futures/use-board-terms.ts +22 -15
  296. package/src/plugins/builtin/futures/use-front-returns.ts +20 -14
  297. package/src/plugins/builtin/futures-curve/headless.ts +6 -1
  298. package/src/plugins/builtin/futures-curve/index.tsx +1 -2
  299. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  300. package/src/plugins/builtin/gpu/board.tsx +51 -12
  301. package/src/plugins/builtin/gpu/client.ts +19 -3
  302. package/src/plugins/builtin/gpu/equities.tsx +5 -2
  303. package/src/plugins/builtin/gpu/headless.ts +14 -2
  304. package/src/plugins/builtin/gpu/index.tsx +1 -2
  305. package/src/plugins/builtin/gpu/model.ts +20 -2
  306. package/src/plugins/builtin/gpu/pane.tsx +1 -1
  307. package/src/plugins/builtin/gpu/series.ts +2 -3
  308. package/src/plugins/builtin/help/index.tsx +1 -1
  309. package/src/plugins/builtin/holders/beneficial-view.tsx +1 -2
  310. package/src/plugins/builtin/holders/index-holders.ts +24 -12
  311. package/src/plugins/builtin/holders/index.tsx +2 -3
  312. package/src/plugins/builtin/ipo-calendar/index.tsx +1 -2
  313. package/src/plugins/builtin/iv-history/index.tsx +1 -2
  314. package/src/plugins/builtin/jodi/client.ts +4 -1
  315. package/src/plugins/builtin/jodi/index.tsx +1 -2
  316. package/src/plugins/builtin/kelly-sizer/index.tsx +1 -2
  317. package/src/plugins/builtin/layout-manager/index.tsx +9 -8
  318. package/src/plugins/builtin/macro-day/pane.tsx +4 -2
  319. package/src/plugins/builtin/market-halts/index.tsx +1 -2
  320. package/src/plugins/builtin/market-heatmap/index.tsx +11 -2
  321. package/src/plugins/builtin/market-movers/client.ts +1 -0
  322. package/src/plugins/builtin/market-movers/index.tsx +16 -5
  323. package/src/plugins/builtin/market-movers/screener.ts +23 -6
  324. package/src/plugins/builtin/market-movers/session-body.tsx +6 -2
  325. package/src/plugins/builtin/market-valuation/index.tsx +1 -2
  326. package/src/plugins/builtin/members/index.tsx +1 -2
  327. package/src/plugins/builtin/money-markets/index.tsx +1 -2
  328. package/src/plugins/builtin/money-markets/pane.tsx +2 -1
  329. package/src/plugins/builtin/money-markets/series.ts +3 -5
  330. package/src/plugins/builtin/news/headless.ts +7 -0
  331. package/src/plugins/builtin/news/wire/index.tsx +1 -1
  332. package/src/plugins/builtin/news/wire/news/pop-out.tsx +9 -3
  333. package/src/plugins/builtin/notes/index.tsx +3 -6
  334. package/src/plugins/builtin/openfda/index.tsx +1 -2
  335. package/src/plugins/builtin/options/view.tsx +6 -5
  336. package/src/plugins/builtin/options-calculator/index.tsx +1 -2
  337. package/src/plugins/builtin/options-positioning/index.tsx +1 -2
  338. package/src/plugins/builtin/options-scenario/index.tsx +2 -4
  339. package/src/plugins/builtin/perps/index.tsx +1 -2
  340. package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -3
  341. package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -1
  342. package/src/plugins/builtin/plugin-module.ts +16 -2
  343. package/src/plugins/builtin/portfolio-list/cli/render.ts +53 -3
  344. package/src/plugins/builtin/portfolio-list/index.tsx +1 -2
  345. package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -0
  346. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -2
  347. package/src/plugins/builtin/power/gas-client.ts +2 -1
  348. package/src/plugins/builtin/power/index.tsx +1 -2
  349. package/src/plugins/builtin/power/prices-client.ts +3 -2
  350. package/src/plugins/builtin/rate-path/index.tsx +1 -2
  351. package/src/plugins/builtin/realized-vol/headless.ts +2 -0
  352. package/src/plugins/builtin/realized-vol/index.tsx +1 -2
  353. package/src/plugins/builtin/realized-vol/model.ts +16 -2
  354. package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
  355. package/src/plugins/builtin/relative-rotation/index.tsx +1 -2
  356. package/src/plugins/builtin/research/analyst-headless.ts +33 -6
  357. package/src/plugins/builtin/research/analyst-model.ts +37 -2
  358. package/src/plugins/builtin/research/analyst-pane.tsx +25 -5
  359. package/src/plugins/builtin/research/index.tsx +5 -8
  360. package/src/plugins/builtin/research-search/index.tsx +1 -2
  361. package/src/plugins/builtin/revenue-breakdown/headless.ts +22 -8
  362. package/src/plugins/builtin/revenue-breakdown/model.ts +23 -0
  363. package/src/plugins/builtin/reverse-dcf/pane.tsx +4 -2
  364. package/src/plugins/builtin/risk-factors/data.ts +8 -4
  365. package/src/plugins/builtin/risk-factors/index.tsx +3 -1
  366. package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
  367. package/src/plugins/builtin/roadmap/index.tsx +2 -3
  368. package/src/plugins/builtin/roadmap/pane.tsx +49 -2
  369. package/src/plugins/builtin/scanner/hilo-model.ts +18 -1
  370. package/src/plugins/builtin/scanner/hilo-pane.tsx +12 -2
  371. package/src/plugins/builtin/scanner/live-pane.tsx +7 -4
  372. package/src/plugins/builtin/scanner/lock-note.ts +26 -3
  373. package/src/plugins/builtin/sec/filing-content.ts +38 -6
  374. package/src/plugins/builtin/sec/headless.ts +5 -1
  375. package/src/plugins/builtin/sec/index.tsx +4 -2
  376. package/src/plugins/builtin/sec/model.ts +2 -0
  377. package/src/plugins/builtin/sectors/index.tsx +11 -5
  378. package/src/plugins/builtin/series-inventory.ts +2 -0
  379. package/src/plugins/builtin/shared/cloud-resource.ts +73 -8
  380. package/src/plugins/builtin/shared/cloud-upgrade.ts +24 -1
  381. package/src/plugins/builtin/shared/forming-bars.ts +29 -0
  382. package/src/plugins/builtin/shared/fred-points.ts +6 -1
  383. package/src/plugins/builtin/shared/fx-rate-freshness.ts +17 -0
  384. package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
  385. package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
  386. package/src/plugins/builtin/shared/series-warning.ts +13 -4
  387. package/src/plugins/builtin/shared/use-quote-board.ts +5 -0
  388. package/src/plugins/builtin/short-interest/publications-pane.tsx +2 -1
  389. package/src/plugins/builtin/short-volume/pane.tsx +2 -1
  390. package/src/plugins/builtin/short-volume/series.ts +2 -3
  391. package/src/plugins/builtin/social-mentions/client.ts +15 -10
  392. package/src/plugins/builtin/social-mentions/index.tsx +1 -2
  393. package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
  394. package/src/plugins/builtin/social-mentions/series.ts +2 -3
  395. package/src/plugins/builtin/supply-chain/client.ts +22 -1
  396. package/src/plugins/builtin/supply-chain/graph-client.ts +22 -1
  397. package/src/plugins/builtin/supply-chain/index.tsx +1 -2
  398. package/src/plugins/builtin/thirteenf/api.ts +127 -29
  399. package/src/plugins/builtin/thirteenf/data.ts +165 -35
  400. package/src/plugins/builtin/thirteenf/index.tsx +2 -4
  401. package/src/plugins/builtin/thirteenf/model.ts +2 -0
  402. package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
  403. package/src/plugins/builtin/thirteenf/pane.tsx +41 -16
  404. package/src/plugins/builtin/thirteenf/types.ts +14 -0
  405. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +22 -6
  406. package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
  407. package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
  408. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
  409. package/src/plugins/builtin/ticker-detail/headless.ts +70 -11
  410. package/src/plugins/builtin/ticker-detail/index.tsx +5 -10
  411. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +16 -6
  412. package/src/plugins/builtin/time-sales/index.tsx +1 -2
  413. package/src/plugins/builtin/time-sales/pane.tsx +3 -1
  414. package/src/plugins/builtin/trace-bonds/headless.ts +11 -2
  415. package/src/plugins/builtin/trace-bonds/model.ts +44 -10
  416. package/src/plugins/builtin/trace-bonds/pane.tsx +12 -7
  417. package/src/plugins/builtin/treasury-auctions/cache.ts +20 -8
  418. package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
  419. package/src/plugins/builtin/treasury-auctions/headless.ts +3 -1
  420. package/src/plugins/builtin/treasury-auctions/pane.tsx +10 -4
  421. package/src/plugins/builtin/vol-surface/client.ts +18 -3
  422. package/src/plugins/builtin/vol-surface/index.tsx +1 -2
  423. package/src/plugins/builtin/volatility/client.ts +43 -16
  424. package/src/plugins/builtin/volatility/headless.ts +5 -2
  425. package/src/plugins/builtin/volatility/index.tsx +1 -2
  426. package/src/plugins/builtin/volatility/model.ts +17 -6
  427. package/src/plugins/builtin/volatility/pane.tsx +6 -2
  428. package/src/plugins/builtin/volatility/series.ts +2 -1
  429. package/src/plugins/builtin/world-indices/footer.ts +6 -1
  430. package/src/plugins/builtin/world-venue-map/client.ts +10 -1
  431. package/src/plugins/builtin/world-venue-map/index.ts +6 -2
  432. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +48 -7
  433. package/src/plugins/builtin/world-venue-map/layers.ts +17 -0
  434. package/src/plugins/builtin/world-venue-map/local-layers.ts +141 -0
  435. package/src/plugins/builtin/world-venue-map/map-symbols.ts +18 -0
  436. package/src/plugins/builtin/world-venue-map/map.tsx +4 -2
  437. package/src/plugins/builtin/world-venue-map/scope.ts +63 -0
  438. package/src/plugins/builtin/yield-curve/client.ts +4 -6
  439. package/src/plugins/builtin/yield-curve/curves.ts +16 -2
  440. package/src/plugins/builtin/yield-curve/headless.ts +3 -3
  441. package/src/plugins/bundle.ts +52 -2
  442. package/src/plugins/catalog-browser.ts +2 -2
  443. package/src/plugins/catalog-cli.ts +20 -0
  444. package/src/plugins/host-link.ts +13 -1
  445. package/src/plugins/installer.ts +64 -42
  446. package/src/plugins/loader.ts +43 -11
  447. package/src/plugins/peer-imports.ts +201 -0
  448. package/src/plugins/peer-retry.ts +26 -29
  449. package/src/plugins/plugin-process.ts +128 -0
  450. package/src/plugins/redirection-guard.ts +45 -0
  451. package/src/remote/endpoint.ts +16 -3
  452. package/src/renderers/browser/main.tsx +3 -2
  453. package/src/renderers/cloudflare/http-proxy.ts +7 -123
  454. package/src/renderers/dom/desktop/icons.tsx +5 -2
  455. package/src/renderers/dom/host/image-surface.tsx +46 -21
  456. package/src/renderers/dom/styles.css +7 -0
  457. package/src/renderers/dom/toast-host.tsx +2 -0
  458. package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
  459. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
  460. package/src/renderers/electrobun/bun/desktop/initialization.ts +12 -3
  461. package/src/renderers/electrobun/bun/desktop/rpc-timing.ts +88 -0
  462. package/src/renderers/electrobun/bun/desktop/update.ts +20 -4
  463. package/src/renderers/electrobun/bun/desktop/view-ids.ts +8 -4
  464. package/src/renderers/electrobun/bun/external-plugins.ts +4 -0
  465. package/src/renderers/electrobun/bun/window/attention.ts +54 -0
  466. package/src/renderers/electrobun/bun/window/create.ts +6 -2
  467. package/src/renderers/electrobun/bun/window/native-timing.ts +48 -0
  468. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +82 -4
  469. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +13 -5
  470. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +3 -0
  471. package/src/renderers/electrobun/view/main.tsx +7 -2
  472. package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
  473. package/src/renderers/electrobun/view/ui-host.tsx +9 -2
  474. package/src/renderers/opentui/browser-open.ts +10 -4
  475. package/src/renderers/opentui/image/surface.tsx +1 -1
  476. package/src/renderers/opentui/start.tsx +12 -6
  477. package/src/renderers/opentui/toast-host.tsx +2 -1
  478. package/src/series/registry.ts +18 -0
  479. package/src/series/resolve.ts +2 -1
  480. package/src/series/standalone.ts +10 -1
  481. package/src/series/types.ts +12 -0
  482. package/src/series/validate.ts +11 -2
  483. package/src/sources/gloomberb-cloud/fred-series.ts +5 -3
  484. package/src/sources/gloomberb-cloud/index.ts +28 -5
  485. package/src/sources/gloomberb-cloud/news.ts +24 -3
  486. package/src/sources/gloomberb-cloud/normalizers.ts +17 -2
  487. package/src/sources/provider-errors.ts +26 -3
  488. package/src/sources/provider-router/history.ts +5 -0
  489. package/src/sources/provider-router/index.ts +13 -0
  490. package/src/sync/core-contributors.ts +32 -0
  491. package/src/telemetry/attention-counts.ts +64 -6
  492. package/src/telemetry/attention-setting.ts +33 -0
  493. package/src/telemetry/crash-reports-node.ts +3 -1
  494. package/src/telemetry/stall-clocks.ts +93 -0
  495. package/src/telemetry/stall-reports-node.ts +25 -9
  496. package/src/telemetry/stall-reports.ts +218 -13
  497. package/src/test-support/outage.ts +8 -0
  498. package/src/test-support/preload.ts +9 -0
  499. package/src/tickers/company-name-listing.ts +59 -29
  500. package/src/tickers/company-name.ts +84 -1
  501. package/src/tickers/listing-fate.ts +14 -3
  502. package/src/tickers/saved-instrument.ts +17 -0
  503. package/src/tickers/search/home-listing.ts +104 -8
  504. package/src/tickers/search/index.ts +5 -2
  505. package/src/tickers/search/types.ts +6 -1
  506. package/src/time-series/bar-aggregation.ts +7 -1
  507. package/src/time-series/base-series.ts +3 -3
  508. package/src/time-series/chart-bounds.ts +1 -1
  509. package/src/time-series/chart-data.ts +5 -2
  510. package/src/time-series/chart-seed.ts +1 -1
  511. package/src/time-series/coin-session-close.ts +211 -0
  512. package/src/time-series/live-bars.ts +13 -2
  513. package/src/time-series/live-quotes.ts +1 -1
  514. package/src/time-series/market.ts +5 -3
  515. package/src/time-series/official-close.ts +159 -0
  516. package/src/time-series/price-comparison.ts +75 -8
  517. package/src/time-series/resolve.ts +32 -4
  518. package/src/time-series/series-presentation.ts +1 -1
  519. package/src/time-series/studies/formula.ts +205 -0
  520. package/src/time-series/studies/indicators.ts +299 -0
  521. package/src/time-series/studies/leverage.ts +75 -0
  522. package/src/time-series/studies/pair.ts +138 -0
  523. package/src/time-series/studies/resolve.ts +333 -0
  524. package/src/time-series/studies/shared.ts +141 -0
  525. package/src/time-series/studies/units.ts +243 -0
  526. package/src/time-series/studies/vwap.ts +274 -0
  527. package/src/time-series/types.ts +7 -0
  528. package/src/types/config.ts +18 -2
  529. package/src/types/data-provider.ts +23 -0
  530. package/src/types/financials.ts +40 -3
  531. package/src/types/headless.ts +42 -3
  532. package/src/types/plugin.ts +32 -3
  533. package/src/ui/host.tsx +17 -1
  534. package/src/ui/toast.tsx +26 -1
  535. package/src/utils/ascii-text.ts +140 -0
  536. package/src/utils/cli-output.ts +91 -18
  537. package/src/utils/concurrency.ts +24 -0
  538. package/src/utils/exchanges.ts +6 -0
  539. package/src/utils/external-url.ts +20 -7
  540. package/src/utils/financial-statements.ts +73 -16
  541. package/src/utils/fundamentals.ts +17 -5
  542. package/src/utils/futures-generic.ts +4 -4
  543. package/src/utils/http-proxy-upstream.ts +124 -0
  544. package/src/utils/lease-liabilities.ts +54 -0
  545. package/src/utils/main-thread-stall.ts +122 -5
  546. package/src/utils/perf-marks.ts +58 -12
  547. package/src/utils/price-history-integrity.ts +47 -1
  548. package/src/utils/private-files.ts +141 -0
  549. package/src/utils/public-address.ts +99 -0
  550. package/src/account/upgrade-personalization.ts +0 -57
  551. package/src/account/upgrade-tickers.ts +0 -124
  552. package/src/api-client/upgrade-experiment.ts +0 -91
  553. package/src/plugins/builtin/cloud/browser.tsx +0 -3
  554. package/src/time-series/studies.ts +0 -1628
@@ -16,6 +16,7 @@ import {
16
16
  candidateExchange,
17
17
  LISTING_SEARCH_LIMIT,
18
18
  listingDistance,
19
+ primaryCompanyLine,
19
20
  searchTickerCandidates,
20
21
  type TickerSearchCandidate,
21
22
  } from "../../tickers/search";
@@ -23,7 +24,7 @@ import { initMarketData, withMarketData } from "../context";
23
24
  import { listingChangeLine, loadListingFate } from "../../tickers/listing-fate";
24
25
  import { formatMarketPriceWithCurrency, quoteFormatOptions } from "../../market-data/market/format";
25
26
  import { formerIssuerLine, startTickerChangeLookup, type TickerChangeLookup } from "../../tickers/ticker-changes";
26
- import { fail, failRetryable } from "../errors";
27
+ import { fail, failRetryable, failWithError, isCliFailure, unreachableError } from "../errors";
27
28
  import { isProviderBusy, SEARCH_BUSY_CODE, SEARCH_BUSY_MESSAGE } from "../../sources/provider-errors";
28
29
  import { isNetworkFailure } from "../../utils/network-failure";
29
30
  import type { MarketContext } from "../types";
@@ -34,8 +35,8 @@ import { readBrokerIndexCode } from "../listing-arg";
34
35
  import { knownGapMessage, commandPointer } from "../known-gaps";
35
36
  import { instrumentTypeLabel } from "../../tickers/instrument-kind";
36
37
  import { canonicalExchange, isUsListingExchange } from "../../utils/exchanges";
37
- import { companyNameExtraWords, groupByCompany, sameCompany } from "../../tickers/company-name";
38
- import { namedByService } from "../../tickers/company-name-listing";
38
+ import { companyLines, companyNameExtraWords, groupByCompany, namedByService, sameCompany } from "../../tickers/company-name";
39
+ import { isLowQualityTickerName } from "../../tickers/search/result";
39
40
  import { findCatalogSeries, renderSeriesMatches, type SeriesMatch } from "../series-search";
40
41
  import { centralBankAskedAbout, centralBankCiting, type CentralBank } from "../../utils/central-banks";
41
42
  import { createPaneCatalog } from "../pane-functions/discovery";
@@ -133,8 +134,8 @@ export async function searchCandidatesForCli({
133
134
  const quote = BARE_SYMBOL.test(symbol) && candidates.some((candidate) => candidate.symbol.toUpperCase() === symbol)
134
135
  ? await (quoted ?? loadSearchedQuote(symbol, savedExchange, dataProvider))
135
136
  : null;
136
- return withQuotedNames(symbol, withUsLineOfPrimary(withHomeListingPrimary(trimmedQuery, withPrimaryListingFirst(symbol, candidates, quote))), quote)
137
- .slice(0, totalLimit);
137
+ const ordered = withQuotedNames(symbol, withUsLineOfPrimary(withHomeListingPrimary(trimmedQuery, withPrimaryListingFirst(symbol, candidates, quote))), quote);
138
+ return withOtherCompaniesApart(ordered).slice(0, totalLimit);
138
139
  } catch (error) {
139
140
  // Not "No matches found": a script or a reader would take that for a name nothing lists.
140
141
  if (isProviderBusy(error)) failRetryable(SEARCH_BUSY_MESSAGE, SEARCH_BUSY_CODE);
@@ -283,17 +284,17 @@ function withHomeListingPrimary(query: string, candidates: TickerSearchCandidate
283
284
  || primary.symbol.toUpperCase() === query.toUpperCase()) return candidates;
284
285
  const company = resolveSearchName(primary);
285
286
  const marked = !!primary.result && namedByService(query, primary.result);
286
- const lines = candidates.filter((candidate) => candidate.kind === "search" && candidate.category !== "Funds & Derivatives"
287
- && (candidate.instrumentClass === "equity" || candidate.instrumentClass === "other") && sameCompany(resolveSearchName(candidate), company)
288
- && (!marked || companyNameExtraWords(resolveSearchName(candidate), company) != null));
289
- const score = (candidate: TickerSearchCandidate) => candidate.result?.popularity ?? candidate.popularity ?? null;
290
- const [home] = [...lines].sort((a, b) => listingDistance(a) - listingDistance(b) || (score(b) ?? -1) - (score(a) ?? -1) || candidates.indexOf(a) - candidates.indexOf(b));
291
- if (!home || home === primary) return candidates;
292
- const closer = listingDistance(home) < listingDistance(primary);
293
- const [homeScore, primaryScore] = [score(home), score(primary)];
294
- const moreSearched = !marked && listingDistance(home) === listingDistance(primary) && homeScore != null
295
- && (primaryScore == null || homeScore >= primaryScore * 2);
296
- if (!closer && !moreSearched) return candidates;
287
+ // Never a line of another business its name is in (`companyLines`): Siemens Energy AG is not Siemens AG's.
288
+ const lines = companyLines(primary, candidates.filter((candidate) => candidate === primary || (candidate.kind === "search"
289
+ && candidate.category !== "Funds & Derivatives" && (candidate.instrumentClass === "equity" || candidate.instrumentClass === "other"))),
290
+ (candidate) => ({
291
+ name: resolveSearchName(candidate), exchange: candidateExchange(candidate),
292
+ receipt: listingDistance(candidate) === 2, home: candidate.result?.homeListing === true,
293
+ })).filter((candidate) => candidate === primary || !marked || companyNameExtraWords(resolveSearchName(candidate), company) != null);
294
+ const home = primaryCompanyLine(primary, lines, (candidate) => ({
295
+ distance: listingDistance(candidate), score: candidate.result?.popularity ?? candidate.popularity ?? null,
296
+ }), { named: marked });
297
+ if (home === primary) return candidates;
297
298
  const rest = candidates.filter((candidate) => candidate !== home);
298
299
  const at = rest.indexOf(primary);
299
300
  return [...rest.slice(0, at), { ...home, category: "Primary Listing" as const }, { ...primary, category: "Other Listings" as const }, ...rest.slice(at + 1)];
@@ -346,6 +347,35 @@ function withQuotedNames(symbol: string, candidates: TickerSearchCandidate[], qu
346
347
  });
347
348
  }
348
349
 
350
+ /** A symbol without a venue's suffix or code: DK for DK.BA or DK:BYMA. */
351
+ const baseSymbol = (symbol: string) => symbol.toUpperCase().replace(/[:.][A-Z]{1,4}$/, "");
352
+
353
+ /**
354
+ * Other Listings are the primary listing's company's own lines alone: by its
355
+ * name, a name it had (Cassava Sciences for Filana's FLNA), or, for a line
356
+ * search names no company for, its symbol on another venue. Another company's
357
+ * line, or a nameless line under another symbol (DKC1O on BYMA under
358
+ * `search DK`), is an Other Match, listed after them.
359
+ */
360
+ function withOtherCompaniesApart(candidates: TickerSearchCandidate[]): TickerSearchCandidate[] {
361
+ // A line search names no company for: no name, or only its symbol (DKC1O).
362
+ const nameless = (candidate: TickerSearchCandidate) =>
363
+ isLowQualityTickerName(candidate.result ? candidate.result.name ?? "" : resolveSearchName(candidate), candidate.symbol);
364
+ const primary = candidates.find((candidate) => candidate.category === "Primary Listing");
365
+ // A primary line search names no company for gives nothing to tell its company's lines by.
366
+ if (!primary || nameless(primary)) return candidates;
367
+ const names = [resolveSearchName(primary), primary.result?.formerName, ...primary.nameAliases ?? []];
368
+ const ownLine = (candidate: TickerSearchCandidate) => nameless(candidate)
369
+ ? baseSymbol(candidate.symbol) === baseSymbol(primary.symbol)
370
+ : names.some((company) => sameCompany(resolveSearchName(candidate), company));
371
+ const others = candidates.filter((candidate) => candidate.category === "Other Listings" && !ownLine(candidate));
372
+ if (others.length === 0) return candidates;
373
+ const kept = candidates.filter((candidate) => !others.includes(candidate));
374
+ const at = kept.findLastIndex((candidate) => candidate === primary || candidate.category === "Other Listings") + 1;
375
+ const apart = others.map((candidate) => ({ ...candidate, category: "Other Matches" as const }));
376
+ return [...kept.slice(0, at), ...apart, ...kept.slice(at)];
377
+ }
378
+
349
379
  /** "Saved", or the broker a row came from; never the data service's internal id. */
350
380
  function searchSourceLabel(candidate: TickerSearchCandidate): string {
351
381
  return candidate.kind === "ticker" ? "Saved" : candidate.result?.brokerLabel || "";
@@ -498,18 +528,19 @@ export function buildSearchReport({
498
528
  /** What the query was searched as: "Read VIX.X as ^VIX (Cboe Volatility Index)." */
499
529
  readAs?: string;
500
530
  }): string {
531
+ // Every note is prose: it wraps to a narrow --width.
501
532
  const lines = [renderSection(`Search: ${query}`)];
502
- if (readAs) lines.push("", readAs);
533
+ if (readAs) lines.push("", wrapToTerminal(readAs));
503
534
  const notes = [...fate.notes, ...formerIssuerNotes(candidates, fate)];
504
- if (notes.length > 0) lines.push("", ...notes);
535
+ if (notes.length > 0) lines.push("", ...notes.map((note) => wrapToTerminal(note)));
505
536
  const gap = knownGapMessage(query);
506
537
  const indexNote = gap ? null : indexRootNote(query, candidates);
507
- if (indexNote) lines.push("", indexNote);
508
- if (gap) lines.push("", gap);
538
+ if (indexNote) lines.push("", wrapToTerminal(indexNote));
539
+ if (gap) lines.push("", wrapToTerminal(gap));
509
540
  const bank = searchedCentralBank(query);
510
541
  if (bank) lines.push("", wrapToTerminal(centralBankLine(bank)));
511
542
  const setting = commandPointer(query);
512
- if (setting) lines.push("", setting);
543
+ if (setting) lines.push("", wrapToTerminal(setting));
513
544
  const charted = renderSeriesMatches(query, series, SEARCH_SERIES_LIMIT, cliTerminalWidth() ?? 100, "catalog");
514
545
  // A query that names the data ("carbon", "day-ahead") lists its series first; words that happen to
515
546
  // be in a series name ("apple") list them after the tickers.
@@ -523,9 +554,9 @@ export function buildSearchReport({
523
554
  if (functions.length > 0) lines.push("", renderFunctionMatches(functions));
524
555
  else if (!gap && !bank && !setting && !charted) {
525
556
  return [...lines, ...hasNonLatinLetters(query) ? [cliStyles.muted(OTHER_SCRIPT_NO_MATCH)]
526
- : [cliStyles.muted("No matches found."), cliStyles.muted(catalogHint(query))]].join("\n");
557
+ : [cliStyles.muted("No matches found."), cliStyles.muted(wrapToTerminal(catalogHint(query)))]].join("\n");
527
558
  }
528
- if (pointToCatalog) lines.push("", cliStyles.muted(catalogHint(query)));
559
+ if (pointToCatalog) lines.push("", cliStyles.muted(wrapToTerminal(catalogHint(query))));
529
560
  return lines.join("\n");
530
561
  }
531
562
 
@@ -558,11 +589,39 @@ export function buildSearchReport({
558
589
  ));
559
590
  }
560
591
  if (charted && !seriesFirst) lines.push("", charted);
561
- if (pointToCatalog) lines.push("", cliStyles.muted(catalogHint(query)));
592
+ if (pointToCatalog) lines.push("", cliStyles.muted(wrapToTerminal(catalogHint(query))));
562
593
 
563
594
  return lines.join("\n");
564
595
  }
565
596
 
597
+ /**
598
+ * What a search says from its own text, whatever the ticker lookup does: the
599
+ * data no source carries and its proxies ("baltic dry"), the central bank the
600
+ * query names, the setting or report it names.
601
+ */
602
+ function localSearchNotes(query: string): string[] {
603
+ const bank = searchedCentralBank(query);
604
+ return [knownGapMessage(query), bank ? centralBankLine(bank) : null, commandPointer(query)].filter((note): note is string => !!note);
605
+ }
606
+
607
+ /**
608
+ * A ticker lookup that was busy or never reached the data service fails only
609
+ * the ticker part of a search: the notes from local text print first (text) or
610
+ * ride in the failure's details (JSON), then the failure says that no tickers
611
+ * were looked up. A search with no such note fails as before.
612
+ */
613
+ function failTickerLookup(error: unknown, query: string, printNotes: boolean): never {
614
+ const notes = localSearchNotes(query);
615
+ const busy = isCliFailure(error) && error.code === SEARCH_BUSY_CODE;
616
+ if (notes.length === 0 || !(busy || isNetworkFailure(error))) throw error;
617
+ if (printNotes) console.log([renderSection(`Search: ${query}`), "", ...notes.map((note) => wrapToTerminal(note))].join("\n"));
618
+ const failure = busy ? { code: SEARCH_BUSY_CODE, message: TICKER_SEARCH_BUSY_MESSAGE, retryable: true } : unreachableError();
619
+ return failWithError(printNotes ? failure : { ...failure, details: [failure.details, ...notes].filter(Boolean).join("\n") });
620
+ }
621
+
622
+ /** The busy line after a search's own notes printed: only the ticker lookup is missing. */
623
+ const TICKER_SEARCH_BUSY_MESSAGE = "Ticker search is busy, try again in a moment.";
624
+
566
625
  /** The functions a search lists when it found no ticker, unless a known gap or a central bank already says where to look. */
567
626
  function searchedFunctions(
568
627
  query: string,
@@ -613,7 +672,7 @@ export async function search(query: string, dependencies: SearchCommandDependenc
613
672
  tickers,
614
673
  dataProvider,
615
674
  ...quote ? { quote } : {},
616
- });
675
+ }).catch((error: unknown) => failTickerLookup(error, trimmedQuery, !dependencies.printResult));
617
676
  const fate = quote && changes ? await searchFate(symbol, candidates, await quote, dataProvider, changes) : NO_SEARCH_FATE;
618
677
 
619
678
  const series = findCatalogSeries(dependencies.plugins ?? [], trimmedQuery, "names");
@@ -1,6 +1,6 @@
1
1
  import { join } from "path";
2
2
  import { logFailure } from "../../utils/log-failure";
3
- import { VERSION } from "../../version";
3
+ import { versionCommand } from "./version";
4
4
  import { saveConfig } from "../../data/config/store";
5
5
  import { builtinPluginGroupMembers } from "../../plugins/ownership";
6
6
  import type { AppConfig, TelemetryConfig } from "../../types/config";
@@ -9,7 +9,7 @@ import { readBaseCurrency } from "../../utils/base-currency";
9
9
  import { NIGHTLY_CHANNEL_SUMMARY, parseUpdateChannel, withUpdateChannel } from "../../updater/channel";
10
10
  import type { CliCommandDef } from "../../types/plugin";
11
11
  import type { BuiltinCliCommandDef } from "../command-options";
12
- import { resolveCliDataDir, withCliServices, withConfigData } from "../context";
12
+ import { withCliServices, withConfigData } from "../context";
13
13
  import { CLI_COMMAND_GROUPS } from "../help";
14
14
  import { dryRunNote, formatBytes, formatStatusCell } from "../helpers";
15
15
  import { cliStyles, cliTerminalWidth, renderSection, renderTable, wrapText } from "../../utils/cli-output";
@@ -97,6 +97,21 @@ function describeTimezone(value: unknown): string {
97
97
  return typeof value === "string" && value ? value : "not set, times print in UTC alone";
98
98
  }
99
99
 
100
+ /** What `config get telemetry.attention` prints while the setting is untouched. */
101
+ const ATTENTION_DEFAULT = "default";
102
+
103
+ /**
104
+ * Attention Counts as set, not as "on": unset is the default, which counts an
105
+ * account only once the app has shown it its notice, so it is not `true`.
106
+ */
107
+ function attentionSetting(telemetry: TelemetryConfig | undefined): boolean | typeof ATTENTION_DEFAULT {
108
+ return telemetry?.attention ?? ATTENTION_DEFAULT;
109
+ }
110
+
111
+ function describeAttentionSetting(value: unknown): string {
112
+ return value === ATTENTION_DEFAULT ? "default: on once the app has shown its notice" : describeConfigValue(value);
113
+ }
114
+
100
115
  function describeConfigValue(value: unknown): string {
101
116
  if (value == null) return "nothing";
102
117
  if (Array.isArray(value)) return value.length > 0 ? value.join(", ") : "nothing";
@@ -174,32 +189,6 @@ function resourceCacheStats(
174
189
  }
175
190
 
176
191
  export function createSystemCliCommands(): CliCommandDef[] {
177
- const versionCommand: CliCommandDef = {
178
- name: "version",
179
- aliases: ["--version", "-v"],
180
- description: "Print the version, runtime, and data folder",
181
- help: { group: CLI_COMMAND_GROUPS.app, usage: ["version"] },
182
- execute: async (_args, ctx) => {
183
- // Where the data lives, or will once a command or the app first runs:
184
- // printing the version never creates the folder.
185
- let dataDir: string | null = null;
186
- try {
187
- dataDir = await resolveCliDataDir();
188
- } catch (error) {
189
- logFailure("cli-system", "Resolve data directory failed")(error);
190
- dataDir = null;
191
- }
192
- ctx.printResult({
193
- data: [{
194
- version: VERSION,
195
- bun: typeof Bun === "undefined" ? null : Bun.version,
196
- platform: process.platform,
197
- arch: process.arch,
198
- dataDir,
199
- }],
200
- }, { layout: "record" });
201
- },
202
- };
203
192
 
204
193
  const doctorCommand: CliCommandDef = {
205
194
  name: "doctor",
@@ -259,6 +248,7 @@ export function createSystemCliCommands(): CliCommandDef[] {
259
248
  "config set theme <id>",
260
249
  "config set telemetry.crashReports false",
261
250
  "config set telemetry.usage false",
251
+ "config set telemetry.attention false",
262
252
  "config set starPrompt.enabled false",
263
253
  "config set timezone <Region/City>|none",
264
254
  "config set updates.channel stable|nightly",
@@ -333,7 +323,7 @@ export function createSystemCliCommands(): CliCommandDef[] {
333
323
  valueFlashingEnabled: context.config.valueFlashingEnabled,
334
324
  "telemetry.crashReports": context.config.telemetry?.crashReports !== false,
335
325
  "telemetry.usage": context.config.telemetry?.usage !== false,
336
- "telemetry.attention": context.config.telemetry?.attention === true,
326
+ "telemetry.attention": attentionSetting(context.config.telemetry),
337
327
  [STAR_PROMPT_CONFIG_KEY]: context.config.starPrompt?.enabled !== false,
338
328
  [TIMEZONE_CONFIG_KEY]: context.config.timezone ?? null,
339
329
  [UPDATE_CHANNEL_CONFIG_KEY]: context.config.updates?.channel ?? "stable",
@@ -348,6 +338,7 @@ export function createSystemCliCommands(): CliCommandDef[] {
348
338
  ...(key === "theme" ? { format: (value: unknown) => describeThemeId(String(value)) } : {}),
349
339
  ...(key === TIMEZONE_CONFIG_KEY ? { format: describeTimezone } : {}),
350
340
  ...(key === "baseCurrency" ? { format: describeBaseCurrency } : {}),
341
+ ...(key === "telemetry.attention" ? { format: describeAttentionSetting } : {}),
351
342
  })),
352
343
  });
353
344
  return;
@@ -367,7 +358,8 @@ export function createSystemCliCommands(): CliCommandDef[] {
367
358
  ctx.printResult({ data: { key, value: safeConfig[key] } }, {
368
359
  text: (data) => key === KEYBINDINGS_CONFIG_KEY ? renderKeybindings(data.value)
369
360
  : key === TIMEZONE_CONFIG_KEY ? describeTimezone(data.value)
370
- : key === "baseCurrency" ? describeBaseCurrency(data.value) : describeConfigValue(data.value),
361
+ : key === "baseCurrency" ? describeBaseCurrency(data.value)
362
+ : key === "telemetry.attention" ? describeAttentionSetting(data.value) : describeConfigValue(data.value),
371
363
  });
372
364
  return;
373
365
  }
@@ -72,6 +72,8 @@ import {
72
72
  quoteChangeText,
73
73
  } from "../helpers";
74
74
  import { NotesFiles } from "../../plugins/builtin/notes/files";
75
+ import { getFormDescription } from "../../plugins/builtin/sec/model";
76
+ import { rowSummary, rowSummaryTarget } from "../../plugins/builtin/sec/row-summaries";
75
77
  import { isKnownNonUsListing, secFilingItemCodes, US_FUND_FACTS_ONLY } from "../../utils/sec";
76
78
  import { currentReportItemsLabel, hiddenFilingsNote, loadLatestFilings, type HiddenFilings } from "../../utils/sec-filings";
77
79
  import { canonicalExchange, exchangeLabel, isKnownExchangeCode, isUsListingExchange } from "../../utils/exchanges";
@@ -80,6 +82,7 @@ import {
80
82
  resolveCliListing, type CliListing, type ListingDataRequest,
81
83
  } from "../listing-arg";
82
84
  import { movedAwayChange } from "../../tickers/ticker-changes";
85
+ import { listingChangeLine } from "../../tickers/listing-fate";
83
86
  import { companyMemberships, loadCompanyMemberships, type CompanyMemberships, type ListMembership } from "../issuer-membership";
84
87
  import { isNotATickerMessage } from "../not-a-ticker";
85
88
  import { sharesOutstandingInReceipts } from "../../utils/depositary-receipt";
@@ -305,6 +308,34 @@ function normalizeComparable(value: string): string {
305
308
  return value.toUpperCase().replace(/\bFORM\b/g, "").replace(/[^A-Z0-9]+/g, "");
306
309
  }
307
310
 
311
+ /** A recent filing with what an insider form's own XML says: who filed it and what it reports (`rowSummary`). */
312
+ type RecentFiling = SecFilingItem & { summary?: string };
313
+
314
+ /**
315
+ * The recent filings, each insider form (3, 4, 5, 144) with its filer and what it reports, read from the
316
+ * filing as fn SEC's rows read it: "Fred Thiel: sale of 50K shares". A filing that cannot be read keeps its
317
+ * form's meaning alone.
318
+ */
319
+ async function withFilingSummaries(
320
+ filings: readonly SecFilingItem[],
321
+ dataProvider: { getSecFilingContent?(filing: SecFilingItem): Promise<string | null> },
322
+ ): Promise<RecentFiling[]> {
323
+ if (!dataProvider.getSecFilingContent) return [...filings];
324
+ return Promise.all(filings.map(async (filing) => {
325
+ const target = rowSummaryTarget(filing);
326
+ if (!target) return filing;
327
+ const content = await Promise.resolve().then(() => dataProvider.getSecFilingContent!(target))
328
+ .catch(logFailure("cli.commands-ticker", "Read an insider filing failed", null));
329
+ const summary = rowSummary(filing.form, content)?.title;
330
+ return summary ? { ...filing, summary } : filing;
331
+ }));
332
+ }
333
+
334
+ /** What the form is: the filing's own description, else the form's meaning ("Insider Transaction"); none beside an 8-K's items. */
335
+ function filingMeaning(filing: SecFilingItem): string {
336
+ return getFilingDescription(filing) ?? (currentReportItemsLabel(filing.items) ? "" : getFormDescription(filing.form));
337
+ }
338
+
308
339
  function getFilingDescription(filing: SecFilingItem): string | undefined {
309
340
  const description = filing.primaryDocDescription?.trim();
310
341
  if (!description) return undefined;
@@ -1117,7 +1148,7 @@ export async function buildTickerReport({
1117
1148
  /** "CAR = Avis Budget Group, NASDAQ (different companies on XETRA, ASX)", for a bare symbol several companies list. */
1118
1149
  listingNote?: string;
1119
1150
  recentNews?: NewsArticle[];
1120
- recentSecFilings?: SecFilingItem[];
1151
+ recentSecFilings?: RecentFiling[];
1121
1152
  /** What `recentSecFilings` left out. */
1122
1153
  hiddenSecFilings?: HiddenFilings;
1123
1154
  }): Promise<string> {
@@ -1226,6 +1257,8 @@ export async function buildTickerReport({
1226
1257
  )
1227
1258
  : "—";
1228
1259
  const volume = reportedQuoteVolume(quote);
1260
+ // A coin's volume is traded value in its quote currency (BTC-USD: US dollars), not a count of coins.
1261
+ const volumeCurrency = quote.currency && isRoundTheClockCoin(quote.symbol ?? symbol, quote.exchangeName) ? ` ${quote.currency}` : "";
1229
1262
  // A futures price is its latest trade; the exchange settlement is its own row, dated by its session.
1230
1263
  appendMetricSection(lines, "Quote", [
1231
1264
  // A price in cents says what it is per: 314.95¢/lb.
@@ -1247,7 +1280,7 @@ export async function buildTickerReport({
1247
1280
  : "—"],
1248
1281
  ["Bid / Ask", bidAskText(quote, quoteOptions)],
1249
1282
  // An index's and a perpetual's 0 means none is counted, not no trades: no Volume row.
1250
- ["Volume", volume != null ? formatNumber(volume, 0) : "—"],
1283
+ ["Volume", volume != null ? `${formatNumber(volume, 0)}${volumeCurrency}` : "—"],
1251
1284
  ["Updated", formatTimestamp(quoteHeadlineTime(quote))],
1252
1285
  ]);
1253
1286
 
@@ -1338,9 +1371,10 @@ export async function buildTickerReport({
1338
1371
  meta: [
1339
1372
  // The items by number and name, so a results 8-K reads apart from another 8-K filed the same day.
1340
1373
  filingItemsMeta(filing),
1341
- getFilingDescription(filing) ?? "",
1342
- filing.primaryDocument ? `Primary Document ${filing.primaryDocument}` : "",
1374
+ // What the form is, and below it who filed an insider form and what it reports: the file name said neither.
1375
+ filingMeaning(filing),
1343
1376
  ],
1377
+ body: filing.summary,
1344
1378
  link: filing.filingUrl,
1345
1379
  })), hiddenFilingsNote(hiddenSecFilings));
1346
1380
 
@@ -1384,7 +1418,7 @@ function buildTickerStructuredData({
1384
1418
  config: AppConfig;
1385
1419
  notes: string;
1386
1420
  recentNews: NewsArticle[];
1387
- recentSecFilings: SecFilingItem[];
1421
+ recentSecFilings: RecentFiling[];
1388
1422
  hiddenSecFilings: HiddenFilings;
1389
1423
  }) {
1390
1424
  const quote = financials.quote;
@@ -1472,6 +1506,8 @@ function buildTickerStructuredData({
1472
1506
  filingUrl: filing.filingUrl,
1473
1507
  primaryDocumentUrl: filing.primaryDocumentUrl ?? "",
1474
1508
  description: getFilingDescription(filing) ?? "",
1509
+ // An insider form's filer and what it reports, as the text report shows it.
1510
+ summary: filing.summary ?? "",
1475
1511
  items: secFilingItemCodes(filing.items) ?? "",
1476
1512
  // The items by number and name, as the text report shows them.
1477
1513
  itemsLabel: currentReportItemsLabel(filing.items) ?? "",
@@ -1524,7 +1560,7 @@ export async function ticker(symbol: string, dependencies: TickerCommandDependen
1524
1560
  // So does what became of the symbol, read from the research's quote as `history` reads it: a
1525
1561
  // company that moved (HFC became DINO), with nothing new listed there, has no profile of its own.
1526
1562
  const researchQuote = Promise.withResolvers<Quote | null>();
1527
- const fate = listingFateLoader(listing, { dataProvider }, researchQuote.promise);
1563
+ const fate = listingFateLoader(listing, { dataProvider }, researchQuote.promise, { successor: true });
1528
1564
  const request: ListingDataRequest = { command: "ticker", noun: "research", fate };
1529
1565
  /** Nothing to show: a company that moved is under its new symbol. */
1530
1566
  const failIfMovedAway = async () => {
@@ -1568,8 +1604,8 @@ export async function ticker(symbol: string, dependencies: TickerCommandDependen
1568
1604
  const quote = resolvedFinancials.quote;
1569
1605
  // The quote is the old line's (stopped, or named for the company that left): the profile and figures are another security's.
1570
1606
  researchQuote.resolve(quote ?? null);
1571
- const { moved } = await fate();
1572
- if (moved) failMovedSymbol(moved, request);
1607
+ const researchFate = await fate();
1608
+ if (researchFate.moved) failMovedSymbol(researchFate.moved, request);
1573
1609
 
1574
1610
  const notesFiles = new NotesFiles(dataDir);
1575
1611
  const listingName = listingIdentity(listing, quote).name;
@@ -1630,14 +1666,18 @@ export async function ticker(symbol: string, dependencies: TickerCommandDependen
1630
1666
  (currency) => dataProvider.getExchangeRate(currency));
1631
1667
  const notes = notesResult.status === "fulfilled" ? notesResult.value : "";
1632
1668
  const recentNews = newsResult.status === "fulfilled" ? newsResult.value : [];
1633
- const { filings: recentSecFilings, hidden: hiddenSecFilings } = secFilingsResult.status === "fulfilled"
1669
+ const { filings: secFilings, hidden: hiddenSecFilings } = secFilingsResult.status === "fulfilled"
1634
1670
  ? secFilingsResult.value : { filings: [], hidden: NOTHING_HIDDEN };
1671
+ const recentSecFilings = await withFilingSummaries(secFilings, dataProvider);
1635
1672
 
1636
1673
  // The quote is the feed in this report; without one, the fundamentals date it.
1637
1674
  const freshness = quote ? quotesFreshness([quote]) : fundamentalsFreshness(resolvedFinancials);
1638
1675
  const quoteNote = quote ? undefined : await quoteUnavailableNote(dataProvider, requestSymbol, exchange);
1639
1676
  // A bare coin ticker read as another listing says where the coin is, as `quote` does: from the quote, else from the saved listing.
1677
+ const successor = listingChangeLine(researchFate);
1640
1678
  const choiceNotes = [
1679
+ // What became of the company that had the symbol (GOGL), as `quote` says it, then the listing it was read as.
1680
+ ...successor ? [successor] : [],
1641
1681
  ...await listingChoiceNotes(listing, { store, dataProvider }, quote, venues),
1642
1682
  ...quote || !tickerFile ? [] : coinListingNotes(listing, { name: tickerFile.metadata.name, instrumentType: tickerFile.metadata.assetCategory }),
1643
1683
  ];
@@ -0,0 +1,33 @@
1
+ import { VERSION } from "../../version";
2
+ import { logFailure } from "../../utils/log-failure";
3
+ import type { CliCommandContext, CliCommandDef } from "../../types/plugin";
4
+ import { resolveCliDataDir } from "../data-dir";
5
+ import { CLI_COMMAND_GROUPS } from "../help";
6
+
7
+ /** Shared by the early entry point and the command registry. */
8
+ export async function printVersion(printResult: CliCommandContext["printResult"]): Promise<void> {
9
+ // Reading the version never creates the data folder.
10
+ let dataDir: string | null = null;
11
+ try {
12
+ dataDir = await resolveCliDataDir();
13
+ } catch (error) {
14
+ logFailure("cli-system", "Resolve data directory failed")(error);
15
+ }
16
+ printResult({
17
+ data: [{
18
+ version: VERSION,
19
+ bun: typeof Bun === "undefined" ? null : Bun.version,
20
+ platform: process.platform,
21
+ arch: process.arch,
22
+ dataDir,
23
+ }],
24
+ }, { layout: "record" });
25
+ }
26
+
27
+ export const versionCommand: CliCommandDef = {
28
+ name: "version",
29
+ aliases: ["--version", "-v"],
30
+ description: "Print the version, runtime, and data folder",
31
+ help: { group: CLI_COMMAND_GROUPS.app, usage: ["version"] },
32
+ execute: (_args, ctx) => printVersion(ctx.printResult),
33
+ };
@@ -9,8 +9,7 @@ import { createAppServices } from "../core/app-services";
9
9
  import type { PluginCapability } from "../capabilities";
10
10
  import type { AppConfig } from "../types/config";
11
11
  import type { GloomPlugin } from "../types/plugin";
12
- import { getLoadablePlugins } from "../plugins/catalog";
13
- import { standaloneSeriesCatalog, standaloneSeriesInvoker } from "../series/standalone";
12
+ import { standaloneSeriesCatalog, standaloneSeriesInvoker, standaloneSeriesNamed } from "../series/standalone";
14
13
  import type { PluginRegistry } from "../plugins/registry";
15
14
  import type { LoadedExternalPlugin } from "../plugins/loader";
16
15
  import { apiClient } from "../api-client";
@@ -73,11 +72,6 @@ function isUnreadableConfig(configFile: string): boolean {
73
72
  }
74
73
  }
75
74
 
76
- /** The data folder this profile uses, whether or not anything has created it yet. Never writes. */
77
- export async function resolveCliDataDir(): Promise<string> {
78
- return await getDataDir() ?? getGloomberbDirs().data;
79
- }
80
-
81
75
  /**
82
76
  * The data folder and config a command works with. The first command on a new
83
77
  * profile creates them with the same `initDataDir` a first launch of the
@@ -120,7 +114,7 @@ function applyPersistedCloudSession(context: Pick<MarketContext, "persistence">)
120
114
  export async function initMarketData(options: CliContextOptions = {}): Promise<MarketContext> {
121
115
  const context = await initConfigData();
122
116
  applyPersistedCloudSession(context);
123
- const plugins = options.plugins ?? getLoadablePlugins();
117
+ const plugins = options.plugins ?? (require("../plugins/catalog") as typeof import("../plugins/catalog")).getLoadablePlugins();
124
118
  const capabilities = resolveCliCapabilities(context.config, plugins);
125
119
  const dataProvider = new AssetDataRouter(null, [], context.persistence.resources);
126
120
  const registryAdapter: Pick<PluginRegistry, "brokers" | "getEnabledCapabilities"> = {
@@ -132,15 +126,18 @@ export async function initMarketData(options: CliContextOptions = {}): Promise<M
132
126
  dataProvider.setBackgroundRevalidation(false);
133
127
  const seriesCapabilities = standaloneSeriesInvoker(plugins, context.dataDir, context.config.disabledPlugins);
134
128
  const findCatalogSeries = standaloneSeriesCatalog(plugins, context.config.disabledPlugins);
135
- return { ...context, dataProvider, seriesCapabilities, findCatalogSeries, statementSources: cloudStatementReportSources };
129
+ const findCatalogSeriesNamed = standaloneSeriesNamed(plugins, context.config.disabledPlugins);
130
+ return { ...context, dataProvider, seriesCapabilities, findCatalogSeries, findCatalogSeriesNamed, statementSources: cloudStatementReportSources };
136
131
  }
137
132
 
138
133
  export async function initCliServices(options: CliServicesOptions = {}) {
134
+ const { getLoadablePlugins } = require("../plugins/catalog") as typeof import("../plugins/catalog");
139
135
  const { dataDir, config, created } = await openCliDataDir();
140
136
  const services = createAppServices({
141
137
  config,
142
138
  plugins: getLoadablePlugins(options.externalPlugins ?? []),
143
139
  externalPlugins: options.externalPlugins ?? [],
140
+ interactive: false,
144
141
  });
145
142
  services.providerRouter.setConfigAccessor(() => config);
146
143
  services.providerRouter.setBackgroundRevalidation(false);
@@ -0,0 +1,7 @@
1
+ import { getDataDir } from "../data/config/store";
2
+ import { getGloomberbDirs } from "../data/config/home";
3
+
4
+ /** The data folder this profile uses, whether or not anything has created it yet. Never writes. */
5
+ export async function resolveCliDataDir(): Promise<string> {
6
+ return await getDataDir() ?? getGloomberbDirs().data;
7
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Which Gloom Cloud requests the pane screenshot page may send through its
3
+ * local proxy, which adds the user's session (desktop-pane-shot.ts). The page
4
+ * only draws panes, so it gets the reads panes make and nothing that acts on
5
+ * the account: no sign-in or sign-out, billing, keys, chat, sync or sharing,
6
+ * and no write of any kind.
7
+ */
8
+
9
+ /** The API families panes read their data from. */
10
+ const READ_FAMILIES = /^\/(?:cloud|market|news|public|research)(?:\/|$)/;
11
+ /** Auth routes inside a read family. */
12
+ const AUTH_IN_FAMILY = /^\/cloud\/auth(?:\/|$)/;
13
+ /**
14
+ * Reads outside those families that panes make, none of whose answers holds a
15
+ * credential, token or key: the session and plan an access gate reads (the
16
+ * session cut to its user, see `isShotSessionPath`), plan prices, the service
17
+ * probe, the roadmap, the layout gallery (marketplace and team layouts),
18
+ * theses, and a signed-in broker's catalog, connection status, holdings,
19
+ * orders and fills.
20
+ */
21
+ const READ_PATHS = [
22
+ /^\/auth\/get-session$/,
23
+ /^\/account\/cloud$/,
24
+ /^\/pricing$/,
25
+ /^\/health$/,
26
+ /^\/roadmap$/,
27
+ /^\/layouts(?:\/[^/]+)?$/,
28
+ /^\/teams\/[^/]+\/layouts$/,
29
+ /^\/theses(?:\/[^/]+)?$/,
30
+ /^\/brokers\/connectors$/,
31
+ /^\/brokers\/[a-z][a-z0-9-]{0,31}(?:\/(?:snapshot|orders|executions))?$/,
32
+ ];
33
+ /** Reads sent as a POST because their query is a list: batches, scans and analyses. */
34
+ const READ_POSTS = new Set([
35
+ "/cloud/exposure/analyze",
36
+ "/cloud/perps/equities",
37
+ "/market/financials/batch",
38
+ "/market/history/batch",
39
+ "/market/organization-tickers",
40
+ "/market/quotes/batch",
41
+ "/market/scanner/live",
42
+ "/market/search/batch",
43
+ "/market/ticker-changes/batch",
44
+ "/research/equity-diagnostic",
45
+ ]);
46
+
47
+ /**
48
+ * The path as a server could read it: decoded and lower-cased, so an escaped
49
+ * letter or another case cannot step past a check. Null for one that steps up
50
+ * a folder or holds a backslash once decoded, which is refused rather than
51
+ * trusted to reach the server as the path it was checked as. An encoded slash
52
+ * inside a segment (a symbol such as `EUR/USD`) is fine.
53
+ */
54
+ function readablePath(path: string): string | null {
55
+ if (!path.startsWith("/") || path.includes("//")) return null;
56
+ let decoded: string;
57
+ try {
58
+ decoded = decodeURIComponent(path);
59
+ } catch {
60
+ // Intentional ignore: a path that does not decode is refused outright, never sent on.
61
+ return null;
62
+ }
63
+ if (decoded.includes("\\") || decoded.split("/").some((segment) => segment === "." || segment === "..")) return null;
64
+ return decoded.toLowerCase();
65
+ }
66
+
67
+ /**
68
+ * Whether the page may send `method` to the API path `path` (the part after
69
+ * the API's base, query excluded). A batch read sent as POST must be named
70
+ * exactly, with nothing escaped.
71
+ */
72
+ export function isShotApiRequestAllowed(method: string, path: string): boolean {
73
+ const readable = readablePath(path);
74
+ if (readable === null) return false;
75
+ const verb = method.toUpperCase();
76
+ if (verb === "POST") return READ_POSTS.has(path);
77
+ if (verb !== "GET" && verb !== "HEAD") return false;
78
+ if (READ_PATHS.some((pattern) => pattern.test(readable))) return true;
79
+ return READ_FAMILIES.test(readable) && !AUTH_IN_FAMILY.test(readable);
80
+ }
81
+
82
+ /** The session read, whose answer also carries the session's token: the proxy hands the page its user alone. */
83
+ export function isShotSessionPath(path: string): boolean {
84
+ return readablePath(path) === "/auth/get-session";
85
+ }