gloomberb 0.19.0 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/package.json +1 -1
  4. package/patches/electrobun@1.18.1.patch +23 -1
  5. package/src/api-client/app-attention.ts +3 -0
  6. package/src/api-client/beneficial-owners.ts +3 -0
  7. package/src/api-client/company-kpis.ts +3 -0
  8. package/src/api-client/data.ts +2 -1
  9. package/src/api-client/hiring.ts +3 -0
  10. package/src/api-client/index.ts +4 -0
  11. package/src/api-client/ipo.ts +24 -1
  12. package/src/api-client/mna.ts +3 -0
  13. package/src/api-client/paths.ts +9 -1
  14. package/src/api-client/roadmap.ts +85 -0
  15. package/src/api-client/types.ts +12 -0
  16. package/src/architecture/core-builtin-allowlist.json +2 -2
  17. package/src/cli/commands/market.ts +35 -12
  18. package/src/cli/commands/news-search.ts +22 -0
  19. package/src/cli/commands/overview.ts +86 -20
  20. package/src/cli/commands/ticker.ts +32 -2
  21. package/src/cli/errors.ts +17 -2
  22. package/src/cli/glossary.ts +6 -0
  23. package/src/cli/index-roots.ts +0 -7
  24. package/src/cli/listing-arg.ts +57 -10
  25. package/src/cli/pane-functions/capabilities.ts +1 -0
  26. package/src/cli/pane-functions/discovery.ts +21 -1
  27. package/src/cli/pane-functions/freshness.ts +7 -1
  28. package/src/cli/pane-functions/function-help.ts +7 -0
  29. package/src/cli/pane-functions/index.ts +6 -3
  30. package/src/cli/quote-answer.ts +1 -1
  31. package/src/cli/saved-copy-persistence.ts +88 -0
  32. package/src/components/command-bar/list/model.ts +5 -0
  33. package/src/components/command-bar/routes/root/runtime.ts +6 -7
  34. package/src/components/command-bar/selection-runtime.ts +52 -3
  35. package/src/components/command-bar/surface/index.tsx +2 -0
  36. package/src/components/layout/pane/capture-stamp.ts +2 -2
  37. package/src/components/layout/pane/footer/index.tsx +22 -6
  38. package/src/components/layout/pane/footer/model.ts +83 -15
  39. package/src/components/layout/pane/status-footer.ts +35 -2
  40. package/src/components/layout/status-widget-space.ts +9 -0
  41. package/src/components/ticker/list-table-view.tsx +4 -20
  42. package/src/components/ui/display.tsx +18 -3
  43. package/src/components/ui/icon.tsx +4 -1
  44. package/src/data/plugin-cache.ts +5 -3
  45. package/src/data/saved-copies.ts +8 -0
  46. package/src/docs/snapshot.json +32 -5
  47. package/src/i18n/es.ts +2 -4
  48. package/src/i18n/ja.ts +2 -4
  49. package/src/i18n/ko.ts +2 -4
  50. package/src/i18n/zh-cn.ts +2 -4
  51. package/src/i18n/zh-tw.ts +2 -4
  52. package/src/market-data/depositary-pair.ts +25 -1
  53. package/src/market-data/quotes/metadata.ts +7 -0
  54. package/src/news/aggregator.ts +7 -1
  55. package/src/news/types.ts +6 -0
  56. package/src/plugins/builtin/alerts/calendar-events.ts +10 -4
  57. package/src/plugins/builtin/alerts/cli.ts +34 -11
  58. package/src/plugins/builtin/alerts/event-rows.ts +24 -3
  59. package/src/plugins/builtin/alerts/events-pane.tsx +15 -6
  60. package/src/plugins/builtin/alerts/headless.ts +10 -6
  61. package/src/plugins/builtin/cds/client.ts +142 -30
  62. package/src/plugins/builtin/cds/headless.ts +35 -8
  63. package/src/plugins/builtin/cds/model.ts +145 -18
  64. package/src/plugins/builtin/cds/pane.tsx +16 -5
  65. package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
  66. package/src/plugins/builtin/chart-composer/index.tsx +4 -5
  67. package/src/plugins/builtin/chat/controller/index.ts +5 -1
  68. package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
  69. package/src/plugins/builtin/chat/controller/state.ts +25 -2
  70. package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
  71. package/src/plugins/builtin/chat/controller/utils.ts +7 -2
  72. package/src/plugins/builtin/chat/status-preview.ts +79 -0
  73. package/src/plugins/builtin/chat/status-widget.tsx +147 -10
  74. package/src/plugins/builtin/chat/unread-badge.ts +51 -10
  75. package/src/plugins/builtin/clinical-trials/client.ts +49 -4
  76. package/src/plugins/builtin/clinical-trials/model.ts +86 -22
  77. package/src/plugins/builtin/clinical-trials/pane.tsx +46 -27
  78. package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
  79. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +87 -9
  80. package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
  81. package/src/plugins/builtin/company-attention/headless.ts +2 -1
  82. package/src/plugins/builtin/company-attention/pane.tsx +3 -0
  83. package/src/plugins/builtin/company-kpis/headless.ts +8 -3
  84. package/src/plugins/builtin/company-kpis/model.ts +73 -5
  85. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  86. package/src/plugins/builtin/composite-plugins.ts +2 -1
  87. package/src/plugins/builtin/credit-documents/headless.ts +3 -3
  88. package/src/plugins/builtin/doe/headless.ts +3 -2
  89. package/src/plugins/builtin/doe/pane.tsx +13 -3
  90. package/src/plugins/builtin/earnings-calls/data.ts +18 -6
  91. package/src/plugins/builtin/earnings-calls/headless.ts +13 -3
  92. package/src/plugins/builtin/earnings-calls/pane.tsx +7 -0
  93. package/src/plugins/builtin/econ/calendar-model.ts +48 -1
  94. package/src/plugins/builtin/econ/calendar-source.ts +4 -2
  95. package/src/plugins/builtin/econ/detail-view.tsx +2 -2
  96. package/src/plugins/builtin/econ/figures.ts +48 -0
  97. package/src/plugins/builtin/econ/index.tsx +14 -5
  98. package/src/plugins/builtin/econ/print-watch.ts +3 -1
  99. package/src/plugins/builtin/econ-statistics/client.ts +11 -0
  100. package/src/plugins/builtin/econ-statistics/headless.ts +10 -2
  101. package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
  102. package/src/plugins/builtin/econ-statistics/stats.ts +3 -0
  103. package/src/plugins/builtin/help/index.tsx +61 -59
  104. package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
  105. package/src/plugins/builtin/help/table-cells.tsx +13 -2
  106. package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
  107. package/src/plugins/builtin/holders/beneficial-view.tsx +3 -0
  108. package/src/plugins/builtin/holders/headless.ts +4 -2
  109. package/src/plugins/builtin/holders/pane.tsx +3 -0
  110. package/src/plugins/builtin/holders/report-rows.ts +4 -1
  111. package/src/plugins/builtin/insider/headless.ts +2 -2
  112. package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
  113. package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
  114. package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
  115. package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
  116. package/src/plugins/builtin/macro-day/headless.ts +12 -5
  117. package/src/plugins/builtin/macro-day/index.tsx +1 -1
  118. package/src/plugins/builtin/macro-day/model.ts +39 -5
  119. package/src/plugins/builtin/macro-day/pane.tsx +27 -19
  120. package/src/plugins/builtin/market-movers/client.ts +2 -0
  121. package/src/plugins/builtin/market-movers/headless.ts +3 -1
  122. package/src/plugins/builtin/market-movers/screener.ts +8 -4
  123. package/src/plugins/builtin/mna/headless.ts +2 -0
  124. package/src/plugins/builtin/mna/pane.tsx +3 -1
  125. package/src/plugins/builtin/news/headless.ts +3 -1
  126. package/src/plugins/builtin/news/index.tsx +3 -0
  127. package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -6
  128. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +0 -14
  129. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +2 -1
  130. package/src/plugins/builtin/portfolio-list/table.tsx +4 -6
  131. package/src/plugins/builtin/realized-vol/client.ts +13 -5
  132. package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
  133. package/src/plugins/builtin/realized-vol/headless.ts +8 -4
  134. package/src/plugins/builtin/realized-vol/model.ts +37 -6
  135. package/src/plugins/builtin/realized-vol/pane.tsx +9 -7
  136. package/src/plugins/builtin/research/event-model.ts +30 -4
  137. package/src/plugins/builtin/research/index.tsx +7 -3
  138. package/src/plugins/builtin/research/relative-valuation-headless.ts +8 -25
  139. package/src/plugins/builtin/research/relative-valuation-pane.tsx +52 -15
  140. package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
  141. package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
  142. package/src/plugins/builtin/risk-factors/data.ts +4 -2
  143. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  144. package/src/plugins/builtin/roadmap/index.tsx +28 -0
  145. package/src/plugins/builtin/roadmap/model.ts +67 -0
  146. package/src/plugins/builtin/roadmap/pane.tsx +244 -0
  147. package/src/plugins/builtin/scanner/live-headless.ts +21 -2
  148. package/src/plugins/builtin/sec/headless.ts +2 -1
  149. package/src/plugins/builtin/sec/index.tsx +3 -0
  150. package/src/plugins/builtin/shared/cloud-resource.ts +13 -5
  151. package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
  152. package/src/plugins/builtin/shared/report-freshness.ts +8 -0
  153. package/src/plugins/builtin/shared/research-cells.tsx +1 -1
  154. package/src/plugins/builtin/shared/series-cache.ts +4 -4
  155. package/src/plugins/builtin/thirteenf/api.ts +12 -1
  156. package/src/plugins/builtin/thirteenf/data.ts +7 -7
  157. package/src/plugins/builtin/thirteenf/headless.ts +31 -4
  158. package/src/plugins/builtin/ticker-detail/overview/model.ts +10 -2
  159. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
  160. package/src/plugins/builtin/ticker-detail/pane.tsx +11 -6
  161. package/src/plugins/builtin/treasury-auctions/headless.ts +21 -12
  162. package/src/plugins/builtin/treasury-auctions/pane.tsx +9 -3
  163. package/src/plugins/builtin/volatility/pane.tsx +5 -2
  164. package/src/plugins/builtin/yield-curve/client.ts +61 -1
  165. package/src/plugins/builtin/yield-curve/headless.ts +9 -6
  166. package/src/plugins/builtin/yield-curve/index.tsx +34 -10
  167. package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
  168. package/src/plugins/catalog-browser.ts +2 -1
  169. package/src/plugins/host-link.ts +1 -1
  170. package/src/portfolio/column-values.ts +11 -7
  171. package/src/renderers/dom/desktop/icons.tsx +7 -0
  172. package/src/renderers/electrobun/bun/desktop/backend.ts +9 -10
  173. package/src/renderers/electrobun/bun/desktop/host-requests.ts +8 -0
  174. package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
  175. package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
  176. package/src/renderers/electrobun/bun/desktop/windows-script.ts +92 -0
  177. package/src/renderers/electrobun/bun/index.ts +26 -2
  178. package/src/renderers/electrobun/bun/window/create.ts +43 -4
  179. package/src/renderers/electrobun/bun/window/events.ts +20 -0
  180. package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
  181. package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
  182. package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
  183. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  184. package/src/renderers/electrobun/view/news-source.ts +2 -0
  185. package/src/renderers/electrobun/view/ui-host.tsx +8 -0
  186. package/src/sources/gloomberb-cloud/fred-series.ts +3 -1
  187. package/src/sources/gloomberb-cloud/index.ts +42 -8
  188. package/src/tickers/company-name-listing.ts +53 -13
  189. package/src/tickers/company-name.ts +6 -1
  190. package/src/tickers/index-roots.ts +7 -0
  191. package/src/tickers/listing-fate.ts +2 -2
  192. package/src/tickers/search/index.ts +6 -2
  193. package/src/tickers/search/ranking.ts +5 -1
  194. package/src/tickers/search/upsert.ts +13 -0
  195. package/src/tickers/ticker-changes.ts +8 -4
  196. package/src/types/company-from.ts +40 -0
  197. package/src/types/data-provider.ts +7 -0
  198. package/src/types/financials.ts +27 -0
  199. package/src/types/headless.ts +6 -0
  200. package/src/utils/crypto-pair.ts +121 -7
  201. package/src/utils/declared-dividends.ts +1 -1
  202. package/src/utils/fundamentals.ts +36 -5
  203. package/src/version.ts +2 -2
package/README.md CHANGED
@@ -50,7 +50,7 @@ The script downloads the prebuilt release from GitHub and checks its SHA-256 aga
50
50
  With [Bun](https://bun.sh), on any supported OS:
51
51
 
52
52
  ```bash
53
- bun install -g gloomberb
53
+ bun install -g @gloom-sh/terminal
54
54
  ```
55
55
 
56
56
  Then run `gloom`. The command was called `gloomberb` before 0.18.4, and that name still works. For chart graphics, use a Kitty-compatible terminal such as Ghostty, Kitty, or WezTerm. Windows Terminal draws charts as text; the desktop app draws them in full.
package/README.zh-CN.md CHANGED
@@ -62,7 +62,7 @@ curl -fsSL gloom.sh/install | bash
62
62
  也可在 macOS、Linux 或 Windows x64 上通过 [Bun](https://bun.sh) 安装:
63
63
 
64
64
  ```bash
65
- bun install -g gloomberb
65
+ bun install -g @gloom-sh/terminal
66
66
  ```
67
67
 
68
68
  运行 `gloom` 即可启动。0.18.4 之前命令名为 `gloomberb`,该名称仍可使用。要显示图形,请使用兼容 Kitty 图形协议的终端,例如 Ghostty、Kitty 或 WezTerm。直接下载、安装位置与更新方式详见[安装指南(英文)](docs/installation.md)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gloomberb",
3
- "version": "0.19.0",
3
+ "version": "0.19.1",
4
4
  "packageManager": "bun@1.3.11",
5
5
  "description": "Market research and portfolio tracker for the terminal",
6
6
  "module": "src/public/index.ts",
@@ -1,3 +1,16 @@
1
+ diff --git a/dist/api/browser/index.ts b/dist/api/browser/index.ts
2
+ index 3e309b18fdbcec84a4a56f97df4debe7fb8246f8..03420222c1d9ba897835c5e7376c7153ede294fc 100644
3
+ --- a/dist/api/browser/index.ts
4
+ +++ b/dist/api/browser/index.ts
5
+ @@ -50,7 +50,7 @@ class Electroview<T extends RPCWithTransport> {
6
+ // Note: Using ws:// for localhost is intentional - all RPC messages are
7
+ // encrypted with per-webview AES-GCM keys, making TLS redundant
8
+ const socket = new WebSocket(
9
+ - `ws://localhost:${RPC_SOCKET_PORT}/socket?webviewId=${WEBVIEW_ID}`,
10
+ + `ws://127.0.0.1:${RPC_SOCKET_PORT}/socket?webviewId=${WEBVIEW_ID}`,
11
+ );
12
+
13
+ this.bunSocket = socket;
1
14
  diff --git a/dist/api/bun/core/BrowserWindow.ts b/dist/api/bun/core/BrowserWindow.ts
2
15
  index c26c2c977ee1fc25a8dc144b85bac8626a45f572..8920a2905380228c23cba66c20d9bfd19ee13e02 100644
3
16
  --- a/dist/api/bun/core/BrowserWindow.ts
@@ -54,7 +67,7 @@ index c26c2c977ee1fc25a8dc144b85bac8626a45f572..8920a2905380228c23cba66c20d9bfd1
54
67
  width: this.frame.width,
55
68
  height: this.frame.height,
56
69
  diff --git a/dist/api/bun/core/Socket.ts b/dist/api/bun/core/Socket.ts
57
- index cdaa2bca0fabe577cb667056aa6f8f84211189c2..3839e5232742c2467180c344f51a30f1ff3c797f 100644
70
+ index cdaa2bca0fabe577cb667056aa6f8f84211189c2..e9cdd82c934f6da58055dc8343ee3b953ec1ee81 100644
58
71
  --- a/dist/api/bun/core/Socket.ts
59
72
  +++ b/dist/api/bun/core/Socket.ts
60
73
  @@ -3,13 +3,8 @@ import { BrowserView } from "./BrowserView";
@@ -73,6 +86,15 @@ index cdaa2bca0fabe577cb667056aa6f8f84211189c2..3839e5232742c2467180c344f51a30f1
73
86
  }
74
87
 
75
88
  // Encrypt function
89
+ @@ -65,6 +60,8 @@ const startRPCServer = () => {
90
+ while (port <= endPort) {
91
+ try {
92
+ server = Bun.serve<{ webviewId: number }>({
93
+ + // Only this machine's webviews connect.
94
+ + hostname: "127.0.0.1",
95
+ port,
96
+ fetch(req: Request, server: Server<{ webviewId: number }>) {
97
+ const url = new URL(req.url);
76
98
  diff --git a/dist/api/bun/core/Updater.ts b/dist/api/bun/core/Updater.ts
77
99
  index 39daf6d341780af8e30c2c50327162863f5977ec..4f3d2bef8e4b41a13a5a522c0af8dd700879a5db 100644
78
100
  --- a/dist/api/bun/core/Updater.ts
@@ -1,3 +1,4 @@
1
+ import type { CompanyFrom } from "../types/company-from"
1
2
  type AppStore = "app-store" | "google-play"
2
3
  export type AppChart = "free" | "paid" | "grossing" | "unranked"
3
4
 
@@ -97,6 +98,8 @@ interface AppSourceState {
97
98
  export interface AppAttentionPayload {
98
99
  generatedAt: string
99
100
  requestedSymbol?: string
101
+ /** The company's home line the data is read from, when not the listing asked (a receipt, a foreign line). */
102
+ companyFrom?: CompanyFrom
100
103
  page?: { offset: number; limit: number; total: number; nextOffset: number | null }
101
104
  symbol: string | null
102
105
  access: "pro" | "preview"
@@ -1,3 +1,4 @@
1
+ import type { CompanyFrom } from "../types/company-from";
1
2
  /**
2
3
  * Schedule 13D and 13G beneficial ownership reports about one issuer, as
3
4
  * `GET /cloud/sec/beneficial-owners` returns them. Percentages are percent
@@ -71,6 +72,8 @@ export interface BeneficialOwnersCoverage {
71
72
 
72
73
  export interface BeneficialOwnersPayload {
73
74
  ticker: string;
75
+ /** The company's home line the owners are read from, when not the listing asked (a foreign line). */
76
+ companyFrom?: CompanyFrom;
74
77
  cik: string;
75
78
  companyName: string;
76
79
  /** When the server assembled the list, an ISO time. */
@@ -1,3 +1,4 @@
1
+ import type { CompanyFrom } from "../types/company-from"
1
2
  import { listingAbroad, type IssuerListingParams } from "./paths";
2
3
 
3
4
  type QuoteMatchMode = "exact" | "whitespace" | "nfkc_whitespace";
@@ -132,6 +133,8 @@ export interface KpiSeries {
132
133
  }
133
134
  export interface KpisPayload {
134
135
  symbol: string
136
+ /** The company's home line the data is read from, when not the listing asked (a receipt, a foreign line). */
137
+ companyFrom?: CompanyFrom
135
138
  status: "available" | "unavailable"
136
139
  asOf: string | null
137
140
  coverage: KpiCoverage
@@ -1284,8 +1284,9 @@ export class CloudDataApi {
1284
1284
  query: string,
1285
1285
  limit?: number,
1286
1286
  topics?: readonly string[],
1287
+ options: { closest?: boolean; tickers?: readonly string[] } = {},
1287
1288
  ): Promise<CloudNewsListResponse> {
1288
- return this.request<CloudNewsListResponse>(cloudNewsSearchPath(query, limit, topics));
1289
+ return this.request<CloudNewsListResponse>(cloudNewsSearchPath(query, limit, topics, options));
1289
1290
  }
1290
1291
 
1291
1292
  async getCloudNewsStory(storyId: string): Promise<CloudNewsPayload> {
@@ -1,3 +1,4 @@
1
+ import type { CompanyFrom } from "../types/company-from"
1
2
  /** HIRING is an observed requisition series, not headcount or job creation. */
2
3
  interface HiringCompany {
3
4
  entityId: string
@@ -76,6 +77,8 @@ export interface HiringSummary extends HiringCompany {
76
77
  export interface HiringPayload extends HiringSummary {
77
78
  generatedAt: string
78
79
  requestedSymbol?: string
80
+ /** The company's home line the data is read from, when not the listing asked (a receipt, a foreign line). */
81
+ companyFrom?: CompanyFrom
79
82
  proRequired: true
80
83
  preview: boolean
81
84
  locked: { history: number; evidence: number; peers: number; signals: number; mix: number }
@@ -24,6 +24,7 @@ import { CloudApiRequestTransport, putWithRevision } from "./request";
24
24
  import { CloudApiSocket } from "./socket";
25
25
  import { CloudCollectionsApi } from "./collections";
26
26
  import { CloudFeedbackApi } from "./feedback";
27
+ import { CloudRoadmapApi } from "./roadmap";
27
28
  import { CloudNotesApi } from "./notes";
28
29
  import { CloudTeamsApi } from "./teams";
29
30
  import { CloudTelemetryApi } from "./telemetry";
@@ -154,6 +155,7 @@ class GloomApiClient {
154
155
  private readonly data: CloudDataApi = new CloudDataApi((path, options) => this.request(path, options));
155
156
  private readonly notes: CloudNotesApi = new CloudNotesApi((path, options) => this.request(path, options));
156
157
  private readonly feedback: CloudFeedbackApi = new CloudFeedbackApi((path, options) => this.request(path, options));
158
+ private readonly roadmap: CloudRoadmapApi = new CloudRoadmapApi((path, options) => this.request(path, options));
157
159
  private readonly telemetry: CloudTelemetryApi = new CloudTelemetryApi((path, options) => this.request(path, options));
158
160
  private readonly theses: CloudThesesApi = new CloudThesesApi((path, options) => this.request(path, options));
159
161
  private readonly collections: CloudCollectionsApi = new CloudCollectionsApi((path, options) => this.request(path, options));
@@ -874,6 +876,8 @@ class GloomApiClient {
874
876
  putCloudNote = this.notes.putNote.bind(this.notes);
875
877
  submitFeedback = this.feedback.submitFeedback.bind(this.feedback);
876
878
  listFeedback = this.feedback.listFeedback.bind(this.feedback);
879
+ getRoadmap = this.roadmap.getRoadmap.bind(this.roadmap);
880
+ setRoadmapVote = this.roadmap.setRoadmapVote.bind(this.roadmap);
877
881
  reportCrashErrors = this.telemetry.reportCrashErrors.bind(this.telemetry);
878
882
  reportUsageCounts = this.telemetry.reportUsageCounts.bind(this.telemetry);
879
883
  reportAttentionCounts = this.telemetry.reportAttentionCounts.bind(this.telemetry);
@@ -28,6 +28,24 @@ interface IpoFirstDay {
28
28
  returnPct: number | null;
29
29
  }
30
30
 
31
+ /**
32
+ * When a US IPO's lock-up is estimated to end: the final prospectus's date
33
+ * plus the period it states, or the first session plus the usual 180 days
34
+ * until the prospectus is read. An estimate: underwriters can release
35
+ * shares early, and some holders agree to other periods.
36
+ */
37
+ export interface IpoLockup {
38
+ /** YYYY-MM-DD. */
39
+ endsOn: string;
40
+ days: number;
41
+ /** `prospectus`: the period the final prospectus states; `typical`: 180 days, when it states none of a lock-up's length or was not read. */
42
+ basis: "prospectus" | "typical";
43
+ /** The date the final prospectus is dated, when it was read. */
44
+ prospectusDate: string | null;
45
+ /** The SEC document the period was read from. */
46
+ prospectusUrl: string | null;
47
+ }
48
+
31
49
  export interface IpoDeal {
32
50
  /** Assigned when the deal is first seen and never changed. */
33
51
  id: string;
@@ -66,6 +84,8 @@ export interface IpoDeal {
66
84
  listingType: IpoListingType;
67
85
  filedDate: string | null;
68
86
  firstDay: IpoFirstDay | null;
87
+ /** A priced or listed US IPO's estimated lock-up end; absent elsewhere, and from a server without the estimate. */
88
+ lockup?: IpoLockup | null;
69
89
  updatedAt: string;
70
90
  }
71
91
 
@@ -86,11 +106,14 @@ export interface IpoCalendarPayload {
86
106
 
87
107
  /**
88
108
  * Narrows or moves the window. Without any, the server sends its default:
89
- * 90 days back to 180 ahead, filed and withdrawn deals left out, 500 at most.
109
+ * a year back (90 days on a server before that) to 180 days ahead, filed
110
+ * and withdrawn deals left out.
90
111
  */
91
112
  export interface IpoCalendarParams {
92
113
  region?: IpoRegion;
93
114
  status?: IpoStatus;
115
+ /** Company (in either script) or symbol the deals' names contain. */
116
+ q?: string;
94
117
  /** First listing date, YYYY-MM-DD. */
95
118
  from?: string;
96
119
  /** Last listing date, YYYY-MM-DD. */
@@ -1,3 +1,4 @@
1
+ import type { CompanyFrom } from "../types/company-from";
1
2
  /**
2
3
  * Mergers and acquisitions read from news, press releases and SEC filings.
3
4
  * A deal is one target and one buyer; a target with competing bidders has a
@@ -84,6 +85,8 @@ export interface MnaDealsParams {
84
85
 
85
86
  export interface MnaDealsPayload {
86
87
  deals: MnaDeal[];
88
+ /** The company's home line the data is read from, when not the listing asked (a receipt, a foreign line). */
89
+ companyFrom?: CompanyFrom;
87
90
  hasMore: boolean;
88
91
  nextOffset: number;
89
92
  /** "delayed" hides deals first reported within `delayDays` from a free account. */
@@ -621,10 +621,18 @@ export function cloudNewsPath(params: CloudNewsParams = {}): string {
621
621
 
622
622
  /** Stories whose headline, summary or publisher matches the text, best match first; the service takes at most 100. */
623
623
  /** `topics` are NI codes the stories must carry: `query=gold&topics=metals` is gold's market news. */
624
- export function cloudNewsSearchPath(query: string, limit?: number, topics?: readonly string[]): string {
624
+ /**
625
+ * `closest`: when no story has every word, the stories with the most of them, marked `match: "closest"`.
626
+ * `tickers`: only the stories filed under them (`HAWK:NYSE`).
627
+ */
628
+ export function cloudNewsSearchPath(
629
+ query: string, limit?: number, topics?: readonly string[], options: { closest?: boolean; tickers?: readonly string[] } = {},
630
+ ): string {
625
631
  const search = new URLSearchParams({ query });
626
632
  if (limit != null) search.set("limit", String(limit));
627
633
  if (topics?.length) search.set("topics", topics.join(","));
634
+ if (options.tickers?.length) search.set("tickers", options.tickers.join(","));
635
+ if (options.closest) search.set("closest", "1");
628
636
  return appendQuery("/news/search", search);
629
637
  }
630
638
 
@@ -0,0 +1,85 @@
1
+ import type { CloudApiRequest } from "./request";
2
+
3
+ /**
4
+ * The public roadmap: what is being built, what is planned and what shipped,
5
+ * with the votes Pro members put on the first two. Anyone reads it; a signed-in
6
+ * Pro member votes once per item and can take the vote back.
7
+ */
8
+ const ROADMAP_STATUSES = ["building", "planned", "shipped"] as const;
9
+ export type RoadmapStatus = (typeof ROADMAP_STATUSES)[number];
10
+
11
+ export interface RoadmapItem {
12
+ id: string;
13
+ title: string;
14
+ /** One line on what it is. */
15
+ blurb: string;
16
+ status: RoadmapStatus;
17
+ /** ISO time it shipped; null until then. */
18
+ shippedAt: string | null;
19
+ votes: number;
20
+ /** Whether this account voted for it; false signed out. */
21
+ voted: boolean;
22
+ }
23
+
24
+ export interface RoadmapPayload {
25
+ items: RoadmapItem[];
26
+ /** The service's word on whether this session may vote (a verified Pro account). */
27
+ canVote: boolean;
28
+ }
29
+
30
+ export interface RoadmapVote {
31
+ id: string;
32
+ votes: number;
33
+ voted: boolean;
34
+ }
35
+
36
+ function isRecord(value: unknown): value is Record<string, unknown> {
37
+ return typeof value === "object" && value !== null && !Array.isArray(value);
38
+ }
39
+
40
+ function parseItem(value: unknown): RoadmapItem | null {
41
+ if (!isRecord(value)) return null;
42
+ const { id, title, blurb, status, shippedAt, votes, voted } = value;
43
+ if (typeof id !== "string" || !id || typeof title !== "string" || !title.trim()) return null;
44
+ if (!(ROADMAP_STATUSES as readonly unknown[]).includes(status)) return null;
45
+ return {
46
+ id,
47
+ title: title.trim(),
48
+ blurb: typeof blurb === "string" ? blurb.trim() : "",
49
+ status: status as RoadmapStatus,
50
+ shippedAt: typeof shippedAt === "string" && Number.isFinite(Date.parse(shippedAt)) ? shippedAt : null,
51
+ votes: typeof votes === "number" && Number.isFinite(votes) ? Math.max(0, Math.round(votes)) : 0,
52
+ voted: voted === true,
53
+ };
54
+ }
55
+
56
+ /** Items it cannot read (an unknown status from a newer service) are left out, not the whole list. */
57
+ function parseRoadmapPayload(value: unknown): RoadmapPayload {
58
+ if (!isRecord(value) || !Array.isArray(value.items)) throw new Error("The roadmap service returned invalid data.");
59
+ return {
60
+ items: value.items.map(parseItem).filter((item): item is RoadmapItem => item !== null),
61
+ canVote: value.canVote === true,
62
+ };
63
+ }
64
+
65
+ function parseVote(value: unknown): RoadmapVote {
66
+ if (!isRecord(value) || typeof value.id !== "string" || typeof value.votes !== "number" || typeof value.voted !== "boolean") {
67
+ throw new Error("The roadmap service returned invalid data.");
68
+ }
69
+ return { id: value.id, votes: value.votes, voted: value.voted };
70
+ }
71
+
72
+ export class CloudRoadmapApi {
73
+ constructor(private readonly request: CloudApiRequest) {}
74
+
75
+ async getRoadmap(signal?: AbortSignal): Promise<RoadmapPayload> {
76
+ return parseRoadmapPayload(await this.request<unknown>("/roadmap", { signal }));
77
+ }
78
+
79
+ /** Votes, or takes the vote back; the answer is the item's count after it. */
80
+ async setRoadmapVote(id: string, voted: boolean): Promise<RoadmapVote> {
81
+ return parseVote(await this.request<unknown>(`/roadmap/${encodeURIComponent(id)}/vote`, {
82
+ method: voted ? "POST" : "DELETE",
83
+ }));
84
+ }
85
+ }
@@ -1,3 +1,4 @@
1
+ import type { CompanyFrom } from "../types/company-from";
1
2
  import type {
2
3
  AnalystResearchData,
3
4
  CorporateActionsData,
@@ -1060,6 +1061,8 @@ export interface CloudEarningsCallListPayload {
1060
1061
  pending?: boolean;
1061
1062
  /** Set when the requested symbol is not one the SEC knows. */
1062
1063
  unknownTicker?: boolean;
1064
+ /** The company's home line the calls are read from, when not the listing asked (a receipt, a foreign line). */
1065
+ companyFrom?: CompanyFrom;
1063
1066
  }
1064
1067
 
1065
1068
  /** One row of a proxy statement's Summary Compensation Table, in dollars. */
@@ -1456,6 +1459,8 @@ export interface CloudSecFilingsResponse {
1456
1459
  filings: CloudSecFilingPayload[];
1457
1460
  hasMore: boolean;
1458
1461
  nextOffset: number;
1462
+ /** The company's home line the filings are read from, when not the listing asked (a foreign line). */
1463
+ companyFrom?: CompanyFrom;
1459
1464
  }
1460
1465
 
1461
1466
  export interface CloudSecDocumentsResponse {
@@ -1558,6 +1563,13 @@ export interface CloudNewsListResponse {
1558
1563
  nextCursor: string | null;
1559
1564
  /** Which news the service answered with, when it says: the live wire, or the one held back for a free account. */
1560
1565
  access?: "realtime" | "delayed";
1566
+ /**
1567
+ * A search asked for its closest stories: `all` when stories have every
1568
+ * word, `closest` when none does and these have the most of them.
1569
+ */
1570
+ match?: "all" | "closest";
1571
+ /** A receipt's or foreign line's feed is its company's: the home line it reads, when not the listing asked. */
1572
+ companyFrom?: CompanyFrom;
1561
1573
  }
1562
1574
 
1563
1575
  interface CloudTweetUserPayload {
@@ -9,7 +9,7 @@
9
9
  "src/cli/commands/options-chain.ts": ["options","options-calculator"],
10
10
  "src/cli/commands/options-leaps.ts": ["iv-history","options","vol-surface"],
11
11
  "src/cli/commands/overview.ts": ["correlation","econ","market-movers","sectors","world-indices"],
12
- "src/cli/commands/ticker.ts": ["notes"],
12
+ "src/cli/commands/ticker.ts": ["ipo-calendar","notes"],
13
13
  "src/cli/desktop-pane-shot.ts": ["market-valuation","time-sales"],
14
14
  "src/cli/pane-functions/data.ts": ["chart-composer","correlation","seasonality","ticker-detail"],
15
15
  "src/cli/pane-functions/financial-ratio-shot.ts": ["ticker-detail"],
@@ -27,7 +27,7 @@
27
27
  "src/components/layout/shell/index.tsx": ["cloud"],
28
28
  "src/components/onboarding/use-onboarding-upgrade.ts": ["chat"],
29
29
  "src/layout/desks.ts": ["chart-composer"],
30
- "src/plugins/catalog-browser.ts": ["alerts","analytics","attention","awards","backtest","bond-calculator","catalysts","cds","central-bank-rates","changelog","chart-composer","clinical-trials","cloud","comment-letters","commodities-energy","commodity-balances","company-attention","company-kpis","connections","correlation","cot","cpi","credit-boards","credit-conditions","credit-documents","crypto-board","crypto-etf-flows","debt-maturities","dividend-yield","docs","doe","earnings","earnings-calls","earnings-ripple","econ","econ-statistics","equity-screener","estimate-revisions","executives","exposure","fear-greed","filing-events","fixed-income","futures","futures-curve","fx-matrix","gpu","help","holders","insider","ipo-calendar","iv-history","jobs","jodi","kelly-sizer","layout-manager","macro-day","market-halts","market-heatmap","market-valuation","members","mna","money-markets","news","openfda","options","options-calculator","options-positioning","options-scenario","pe-band","perps","portfolio-list","power","primary-dealers","rate-path","realized-vol","relative-rotation","research","research-search","revenue-breakdown","reverse-dcf","risk-factors","scanner","seasonality","sec","sectors","short-interest","short-volume","social-mentions","supply-chain","thirteenf","ticker-detail","time-sales","trace-bonds","treasury-auctions","treasury-daily","vol-surface","volatility","world-indices","world-venue-map","yield-curve"],
30
+ "src/plugins/catalog-browser.ts": ["alerts","analytics","attention","awards","backtest","bond-calculator","catalysts","cds","central-bank-rates","changelog","chart-composer","clinical-trials","cloud","comment-letters","commodities-energy","commodity-balances","company-attention","company-kpis","connections","correlation","cot","cpi","credit-boards","credit-conditions","credit-documents","crypto-board","crypto-etf-flows","debt-maturities","dividend-yield","docs","doe","earnings","earnings-calls","earnings-ripple","econ","econ-statistics","equity-screener","estimate-revisions","executives","exposure","fear-greed","filing-events","fixed-income","futures","futures-curve","fx-matrix","gpu","help","holders","insider","ipo-calendar","iv-history","jobs","jodi","kelly-sizer","layout-manager","macro-day","market-halts","market-heatmap","market-valuation","members","mna","money-markets","news","openfda","options","options-calculator","options-positioning","options-scenario","pe-band","perps","portfolio-list","power","primary-dealers","rate-path","realized-vol","relative-rotation","research","research-search","revenue-breakdown","reverse-dcf","risk-factors","roadmap","scanner","seasonality","sec","sectors","short-interest","short-volume","social-mentions","supply-chain","thirteenf","ticker-detail","time-sales","trace-bonds","treasury-auctions","treasury-daily","vol-surface","volatility","world-indices","world-venue-map","yield-curve"],
31
31
  "src/plugins/catalog-ui.ts": ["alerts","clinical-trials","cloud","comment-letters","custom-view","fear-greed","ipo-calendar","market-halts","market-heatmap","members","news","notes","openfda","research-search"],
32
32
  "src/plugins/catalog.ts": ["debug"],
33
33
  "src/remote/controller.ts": ["custom-view"],
@@ -3,6 +3,7 @@ import type { CliCommandDef } from "../../types/plugin";
3
3
  import { TIME_RANGES, type TimeRange } from "../../time-series/range";
4
4
  import { yearToDateStartIndex } from "../../market-data/market/year-to-date";
5
5
  import type { EarningsEvent, QuoteBatchResult, QuoteSubscriptionTarget, SecFilingItem } from "../../types/data-provider";
6
+ import { companyDataNotes, type CompanyFrom } from "../../types/company-from";
6
7
  import type { NewsArticle, NewsFeed, NewsQuery } from "../../news/types";
7
8
  import type {
8
9
  AnalystResearchData,
@@ -165,6 +166,7 @@ import { bareFuturesCode, futuresBoardContract } from "../../market-data/futures
165
166
  import { commodityNamedIn, commoditySearchName } from "../../market-data/commodity-names";
166
167
  import { newsEmptyMessage } from "./news-hints";
167
168
  import {
169
+ closestStoriesReason,
168
170
  commodityNewsRoute,
169
171
  commonWordCandidate,
170
172
  METALS_NEWS_TOPIC,
@@ -179,6 +181,7 @@ import {
179
181
  noMetalStoriesMessage,
180
182
  noStoriesMessage,
181
183
  quotedSearchCommand,
184
+ searchClosestNewsStories,
182
185
  searchNewsStories,
183
186
  wordMeansMore,
184
187
  } from "./news-search";
@@ -189,7 +192,7 @@ import { exportedFundamentals } from "../../utils/price-earnings";
189
192
  import { fundamentalsFreshness, fxPairFreshness, quotesFreshness, rowsFreshness, valuationFreshness } from "../freshness";
190
193
  import { fxRateNotes } from "../../plugins/builtin/shared/fx-rate-freshness";
191
194
  import { quoteBarNotes } from "../../market-data/quotes/intraday-bar";
192
- import { depositaryPairText, isReceiptSide, listingDepositaryPair } from "../../market-data/depositary-pair";
195
+ import { cedearRatioText, depositaryPairText, isReceiptSide, listingCedearRatio, listingDepositaryPair } from "../../market-data/depositary-pair";
193
196
  import { isPesoDollarPair, loadPesoDollarRates, pesoDollarRateLines, pesoDollarRatesJson } from "../peso-dollar-rates";
194
197
  import { atRegularSessionPrice, regularSessionValuationPrice } from "../../market-data/quotes/regular-session-valuation";
195
198
  import { viewMarketCapitalization } from "../../utils/market-capitalization";
@@ -199,6 +202,7 @@ import {
199
202
  cloudNewsFreshness,
200
203
  cloudRealtimeAccess,
201
204
  REPORTED_DATA,
205
+ SEC_FILING_ROWS,
202
206
  SEC_FILINGS,
203
207
  } from "../../plugins/builtin/shared/report-freshness";
204
208
  import {
@@ -242,7 +246,7 @@ import {
242
246
  type ListingIdentity,
243
247
  } from "../listing-arg";
244
248
  import { bareListingNote, listingFateNotes, noLongerTradingNote, tradingListing } from "../../tickers/listing-fate";
245
- import { isYieldIndexSymbol } from "../index-roots";
249
+ import { isYieldIndexSymbol } from "../../tickers/index-roots";
246
250
  import {
247
251
  analyzeTaxEquivalent, distributionBasis, loadTreasuryTenors, parseTaxRates, TAX_RATE_HELP_OPTIONS, taxEquivalentJson, type TaxRates,
248
252
  } from "../../market-data/tax-equivalent";
@@ -913,7 +917,7 @@ function corporateActionRows(data: CorporateActionsData) {
913
917
  ...declaredDividends(data).map((event) => ({
914
918
  type: "dividend",
915
919
  date: event.exDate,
916
- detail: `declared ${currency ? formatDistributionAmount(event.amount, currency) : cleanDecimal(event.amount)}`
920
+ detail: `declared ${event.special ? "special " : ""}${currency ? formatDistributionAmount(event.amount, currency) : cleanDecimal(event.amount)}`
917
921
  + (event.payDate ? `, payable ${event.payDate}` : ""),
918
922
  })),
919
923
  ...data.splits.map((event) => ({
@@ -1105,8 +1109,10 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
1105
1109
  const quiet = ended ? null : noPrintsTodayText(row.quote, now);
1106
1110
  return [ended ?? (quiet ? `${label}: ${quiet}` : []), quoteBarNotes(label, row.quote, now)].flat();
1107
1111
  }),
1108
- // A receipt and its home line each say the ratio, as `ticker` does.
1112
+ // A receipt and its home line each say the ratio, as `ticker` does; a CEDEAR names itself in its own.
1109
1113
  ...data.flatMap((row) => {
1114
+ const cedear = listingCedearRatio({ quote: row.quote });
1115
+ if (cedear) return [`${cedearRatioText(cedear)}.`];
1110
1116
  const pair = listingDepositaryPair({ quote: row.quote });
1111
1117
  return pair ? [`${row.target.symbol}: ${depositaryPairText(pair, isReceiptSide({ quote: row.quote }))}.`] : [];
1112
1118
  }),
@@ -1932,26 +1938,29 @@ const NEWS_COLUMNS = [
1932
1938
  async function printNewsSearch(
1933
1939
  text: string,
1934
1940
  articles: NewsArticle[],
1935
- { reason, empty, commodity = true, failure, ticker = null }: {
1941
+ { reason, empty, commodity = true, failure, ticker = null, closest = false }: {
1936
1942
  reason?: string;
1937
1943
  empty?: string;
1938
1944
  commodity?: boolean;
1939
1945
  failure?: string;
1940
1946
  /** The listing a coin pair's search stands in for. */
1941
1947
  ticker?: string | null;
1948
+ /** No story has every word: these are the ones with the most. */
1949
+ closest?: boolean;
1942
1950
  },
1943
1951
  ctx: Parameters<CliCommandDef["execute"]>[1],
1944
1952
  ) {
1945
1953
  const searched = failure === undefined;
1946
1954
  const named = searched && commodity && articles.length === 0 && commodityNamedIn(text) !== null;
1947
1955
  const route = named ? await commodityNewsRoute(text) : null;
1956
+ const closestReason = closest && articles.length > 0 ? closestStoriesReason(text) : null;
1948
1957
  ctx.printResult({
1949
1958
  data: articles,
1950
1959
  ...(searched ? {} : { warnings: [`Searching stories for "${text}" failed: ${failure}`] }),
1951
- metadata: { ticker, search: text, feed: null, ...(route ? { futuresNews: route.command } : {}) },
1960
+ metadata: { ticker, search: text, feed: null, ...(closestReason ? { match: "closest" } : {}), ...(route ? { futuresNews: route.command } : {}) },
1952
1961
  freshness: rowsFreshness(newsRows(articles), cloudNewsFreshness(await cloudRealtimeAccess())),
1953
1962
  }, {
1954
- heading: newsSearchHeading(text, reason),
1963
+ heading: newsSearchHeading(text, [reason, closestReason].filter(Boolean).join(" · ") || undefined),
1955
1964
  rows: newsRows,
1956
1965
  columns: NEWS_COLUMNS,
1957
1966
  empty: named ? noStoriesMessage(text, route) : empty ?? (searched ? noStoriesMessage(text, null) : "No stories to show."),
@@ -2013,7 +2022,8 @@ async function runNews(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
2013
2022
  if (exchangeOption || feed) ctx.fail(`${exchangeOption ? "--exchange" : "--feed"} goes with a symbol, not with text to search stories for.`, "Usage: gloom news <query>");
2014
2023
  const problem = newsSearchTextProblem(text);
2015
2024
  if (problem) ctx.fail(problem);
2016
- await printNewsSearch(text, await searchNewsStories(text, limit), {}, ctx);
2025
+ const answer = await searchClosestNewsStories(text, limit);
2026
+ await printNewsSearch(text, answer.found, { closest: answer.closest }, ctx);
2017
2027
  return;
2018
2028
  }
2019
2029
  // A metal's future or name is its market's news, not a company's: GOLD in capitals stays Gold.com's.
@@ -2193,7 +2203,7 @@ async function runFilings(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
2193
2203
  ...listingMetadata(resolved), ...(form ? { form } : {}),
2194
2204
  ...(page ? { announcementsPage: page.url } : {}),
2195
2205
  },
2196
- freshness: rowsFreshness(filingRows(filings), { ...SEC_FILINGS, observedKey: "filingDate" }),
2206
+ freshness: rowsFreshness(filingRows(filings), SEC_FILING_ROWS),
2197
2207
  ...(choice.length > 0 ? { warnings: choice } : {}),
2198
2208
  }, {
2199
2209
  heading: listingHeading(resolved),
@@ -2205,7 +2215,7 @@ async function runFilings(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
2205
2215
  empty: [form ? `No ${form} filings found for ${listingTitle(resolved)}.`
2206
2216
  : identity ? noSecReportsNotice(resolved)
2207
2217
  : `No SEC filings found for ${listingTitle(resolved)}.`, pageLine].filter(Boolean).join("\n"),
2208
- ...(pageLine ? { footnote: pageLine, exportNotes: [pageLine] } : {}),
2218
+ ...filingsFootnote(pageLine, filings[0]?.companyFrom),
2209
2219
  });
2210
2220
  } catch (error) {
2211
2221
  if (!(error instanceof SecRegistrantMismatchError)) throw error;
@@ -2223,6 +2233,16 @@ async function runFilings(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
2223
2233
  });
2224
2234
  }
2225
2235
 
2236
+ /**
2237
+ * The lines `filings` prints under its rows and CSV writes as notes: an ASX
2238
+ * listing's announcements page, and whose filings a foreign line shows when
2239
+ * they are its company's home line's.
2240
+ */
2241
+ function filingsFootnote(pageLine: string | null | undefined, companyFrom: CompanyFrom | undefined): { footnote?: string; exportNotes?: string[] } {
2242
+ const lines = [...(pageLine ? [pageLine] : []), ...companyDataNotes(companyFrom)];
2243
+ return lines.length ? { footnote: lines.join("\n"), exportNotes: lines } : {};
2244
+ }
2245
+
2226
2246
  /**
2227
2247
  * `filings` for a company on ASX that files nothing with the SEC: its latest
2228
2248
  * ASX announcements from Gloom Cloud, newest first, under the line saying it
@@ -2393,7 +2413,8 @@ async function printBeneficialOwners(
2393
2413
  heading: `${listingHeading(identity)}${cliStyles.muted(beneficialListFacts(payload, form, rows.length, history).map((fact) => ` · ${fact}`).join(""))}`,
2394
2414
  // Text draws each column as the HDS report does; CSV and JSON keep the raw values.
2395
2415
  textColumns: BENEFICIAL_REPORT_COLUMNS.map((column) => ({ ...column, format: column.textFormat ?? column.format })),
2396
- ...(legend ? { footnote: legend } : {}),
2416
+ // A foreign line's owners read from its company's home line say whose.
2417
+ ...(legend || payload.companyFrom ? { footnote: [legend, ...companyDataNotes(payload.companyFrom)].filter(Boolean).join("\n") } : {}),
2397
2418
  columns: [
2398
2419
  ...BENEFICIAL_REPORT_COLUMNS.map(({ key, header, align }) => ({ key, header, ...(align ? { align } : {}) })),
2399
2420
  { key: "otherPersons", header: "Other persons" },
@@ -2494,7 +2515,7 @@ async function runHolders(
2494
2515
  const columns = holderReportColumns(shownRows, valueCurrency);
2495
2516
  // Dated as HDS dates it: by the list's as-of; an insider report of Form 4 filings alone as INS dates it, by the newest.
2496
2517
  const freshness = form4Only
2497
- ? rowsFreshness(form4?.newestFiled ? [{ filingDate: form4.newestFiled }] : [], { ...SEC_FILINGS, basis: "Form 4 filings", observedKey: "filingDate" })
2518
+ ? rowsFreshness(form4?.newestFiled ? [{ filingDate: form4.newestFiled }] : [], { ...SEC_FILING_ROWS, basis: "Form 4 filings" })
2498
2519
  : rowsFreshness(shownRows, { ...SEC_FILINGS, basis: "13F filings" }, { asOf: data.asOf ?? null });
2499
2520
  // Named as fn HDS takes it, so the closing notes point to the same listing.
2500
2521
  const requested = exchangeOption && !raw.includes(":") ? `${raw}:${exchangeOption.toUpperCase()}` : raw;
@@ -2564,6 +2585,8 @@ async function runHolders(
2564
2585
  insiderCommand ? insiderSummary(data, listing, identity, form4) : "",
2565
2586
  ].filter(Boolean).join("\n"),
2566
2587
  empty: `No holders reported for ${listingTitle(identity)}.`,
2588
+ // Holders read from the company's home line say whose; the SEC index's are the listing's own.
2589
+ ...(!indexed && data.companyFrom ? { footnote: companyDataNotes(data.companyFrom).join("\n") } : {}),
2567
2590
  });
2568
2591
  });
2569
2592
  }
@@ -82,6 +82,28 @@ export async function searchNewsStories(text: string, limit: number, topics?: re
82
82
  return response.items.map((item) => mapCloudNewsArticle(item));
83
83
  }
84
84
 
85
+ /** A search's stories, and whether they are its closest ones because no story has every word. */
86
+ export interface NewsSearchAnswer {
87
+ found: NewsArticle[];
88
+ closest: boolean;
89
+ }
90
+
91
+ /**
92
+ * The stories matching the text; when no story has every word of a search of
93
+ * several, the ones with the most of them, most words first. Spellings of
94
+ * one word are one word to the service: lockup, lock-up and lock up;
95
+ * spinoff and spin-off; expiry and expiration.
96
+ */
97
+ export async function searchClosestNewsStories(text: string, limit: number): Promise<NewsSearchAnswer> {
98
+ const response = await apiClient.searchCloudNews(text, limit, undefined, { closest: true });
99
+ return { found: response.items.map((item) => mapCloudNewsArticle(item)), closest: response.match === "closest" };
100
+ }
101
+
102
+ /** What the heading says of a search no story has every word of. */
103
+ export function closestStoriesReason(text: string): string {
104
+ return `no story has every word of "${text}"; the closest, most words first`;
105
+ }
106
+
85
107
  /** The NI code of the metal markets: prices, futures, bullion and the trusts that hold it, not a miner's releases. */
86
108
  export const METALS_NEWS_TOPIC = "metals";
87
109