gloomberb 0.17.0 → 0.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/package.json +1 -1
  2. package/src/{plugins/builtin/cloud → account}/auth-dialog.tsx +7 -7
  3. package/src/api-client/data.ts +5 -1
  4. package/src/api-client/doe.ts +19 -0
  5. package/src/api-client/earnings.ts +4 -2
  6. package/src/api-client/index.ts +1 -0
  7. package/src/app/global-shortcuts.ts +10 -0
  8. package/src/app/keybindings/actions.ts +16 -0
  9. package/src/app/runtime/desktop-deeplink.ts +1 -1
  10. package/src/app/runtime/update.ts +16 -0
  11. package/src/app.tsx +1 -1
  12. package/src/architecture/core-builtin-allowlist.json +4 -9
  13. package/src/architecture/dom-cell-metrics-allowlist.json +100 -26
  14. package/src/cli/commands/market.ts +87 -17
  15. package/src/cli/commands/options-chain.ts +93 -23
  16. package/src/cli/commands/options-leaps.ts +462 -63
  17. package/src/cli/commands/search.ts +10 -7
  18. package/src/cli/commands/ticker.ts +19 -10
  19. package/src/cli/glossary.ts +1 -1
  20. package/src/cli/index.ts +14 -6
  21. package/src/cli/known-gaps.ts +22 -0
  22. package/src/cli/pane-functions/catalog.ts +45 -10
  23. package/src/cli/pane-functions/dom.ts +36 -10
  24. package/src/cli/pane-functions/function-help.ts +4 -2
  25. package/src/cli/pane-functions/headless.ts +12 -1
  26. package/src/cli/pane-functions/index.ts +44 -3
  27. package/src/cli/pane-functions/lock-note.ts +56 -0
  28. package/src/cli/pane-functions/report-notices.ts +15 -0
  29. package/src/cli/symbol-args.ts +19 -0
  30. package/src/components/command-bar/list/view.tsx +2 -1
  31. package/src/components/command-bar/workflow/builtin.ts +2 -2
  32. package/src/components/command-bar/workflow/collection-actions.ts +1 -1
  33. package/src/components/feedback-dialog.tsx +1 -1
  34. package/src/components/index.ts +3 -2
  35. package/src/components/layout/header.tsx +58 -14
  36. package/src/components/layout/shell/chrome.ts +10 -4
  37. package/src/components/layout/shell/index.tsx +1 -1
  38. package/src/components/layout/status-bar-groups.ts +1 -1
  39. package/src/components/layout/status-bar.tsx +2 -2
  40. package/src/components/markdown-text.tsx +14 -2
  41. package/src/components/reader/article-reader.tsx +1 -1
  42. package/src/components/reader/detail-reader.tsx +10 -7
  43. package/src/components/reader/reader-text.tsx +32 -17
  44. package/src/components/ui/column-grid.tsx +206 -0
  45. package/src/components/ui/display.tsx +40 -3
  46. package/src/components/ui/fill.ts +8 -0
  47. package/src/components/ui/index.ts +2 -0
  48. package/src/layout-marketplace/gallery.tsx +2 -2
  49. package/src/layout-marketplace/linked-sync.ts +1 -1
  50. package/src/market-data/market/freshness.ts +3 -0
  51. package/src/market-data/published-apac-sessions.ts +12 -2
  52. package/src/market-data/quotes/screener-live-quotes.ts +31 -7
  53. package/src/plugins/builtin/account-management/form-components.tsx +4 -7
  54. package/src/plugins/builtin/account-management/password-dialog.tsx +3 -2
  55. package/src/plugins/builtin/backtest/pane.tsx +9 -3
  56. package/src/plugins/builtin/buildout/detail/company.tsx +4 -3
  57. package/src/plugins/builtin/buildout/detail/ui.tsx +9 -12
  58. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  59. package/src/plugins/builtin/chart-composer/presets.ts +33 -1
  60. package/src/plugins/builtin/chat/channel-labels.ts +2 -2
  61. package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -1
  62. package/src/plugins/builtin/chat/content/index.tsx +2 -2
  63. package/src/plugins/builtin/chat/conversations.ts +1 -1
  64. package/src/plugins/builtin/chat/index.tsx +2 -2
  65. package/src/plugins/builtin/chat/share/flow.tsx +1 -1
  66. package/src/plugins/builtin/chat/sidebar-rows.ts +1 -1
  67. package/src/plugins/builtin/chat/sidebar.tsx +2 -2
  68. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +2 -2
  69. package/src/plugins/builtin/cloud/askg/pane.tsx +2 -1
  70. package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +3 -2
  71. package/src/plugins/builtin/cloud/auth-actions.tsx +1 -1
  72. package/src/plugins/builtin/cloud/auth-commands.ts +1 -1
  73. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +1 -1
  74. package/src/plugins/builtin/cloud/team/acm-tab.tsx +3 -3
  75. package/src/plugins/builtin/cloud/team/capability.ts +2 -2
  76. package/src/plugins/builtin/cloud/team/command.ts +4 -4
  77. package/src/plugins/builtin/cloud/team/module.tsx +3 -3
  78. package/src/plugins/builtin/cloud/team/pane-model.ts +2 -2
  79. package/src/plugins/builtin/cloud/team/pane-sections.tsx +1 -1
  80. package/src/plugins/builtin/cloud/team/pane-ui.tsx +1 -1
  81. package/src/plugins/builtin/cloud/team/pane.tsx +3 -3
  82. package/src/plugins/builtin/cloud/team/status-widget.tsx +3 -3
  83. package/src/plugins/builtin/cloud/team/team-state-host.ts +1 -1
  84. package/src/plugins/builtin/cloud/team/views.ts +2 -2
  85. package/src/plugins/builtin/cloud/thesis/board-pane.tsx +1 -1
  86. package/src/plugins/builtin/cloud/thesis/detail.tsx +12 -10
  87. package/src/plugins/builtin/cloud/thesis/ticker-tab.tsx +1 -1
  88. package/src/plugins/builtin/cloud/upgrade-command.ts +1 -1
  89. package/src/plugins/builtin/congress-trades/detail.tsx +8 -4
  90. package/src/plugins/builtin/connections/pane.tsx +16 -5
  91. package/src/plugins/builtin/cpi/pane.tsx +5 -4
  92. package/src/plugins/builtin/custom-view/pane.tsx +2 -2
  93. package/src/plugins/builtin/dividend-yield/headless.ts +8 -2
  94. package/src/plugins/builtin/dividend-yield/pane.tsx +4 -2
  95. package/src/plugins/builtin/docs/blocks.tsx +13 -2
  96. package/src/plugins/builtin/docs/pane.tsx +6 -5
  97. package/src/plugins/builtin/doe/index.tsx +11 -4
  98. package/src/plugins/builtin/doe/pane.tsx +5 -4
  99. package/src/plugins/builtin/doe/prices.ts +84 -0
  100. package/src/plugins/builtin/doe/series.ts +4 -1
  101. package/src/plugins/builtin/earnings/board-model.ts +18 -1
  102. package/src/plugins/builtin/earnings/board-table.ts +18 -3
  103. package/src/plugins/builtin/earnings/headless.ts +13 -2
  104. package/src/plugins/builtin/earnings/history-model.ts +1 -1
  105. package/src/plugins/builtin/earnings/history-table.ts +2 -2
  106. package/src/plugins/builtin/earnings-calls/headless.ts +7 -21
  107. package/src/plugins/builtin/earnings-calls/index.tsx +2 -0
  108. package/src/plugins/builtin/earnings-calls/lock-note.ts +17 -0
  109. package/src/plugins/builtin/equity-screener/headless.ts +59 -1
  110. package/src/plugins/builtin/equity-screener/live.ts +82 -31
  111. package/src/plugins/builtin/equity-screener/pane.tsx +60 -23
  112. package/src/plugins/builtin/estimate-revisions/client.ts +1 -1
  113. package/src/plugins/builtin/estimate-revisions/headless.ts +9 -2
  114. package/src/plugins/builtin/estimate-revisions/index.tsx +3 -0
  115. package/src/plugins/builtin/estimate-revisions/lock-note.ts +23 -0
  116. package/src/plugins/builtin/executives/index.tsx +18 -0
  117. package/src/plugins/builtin/exposure/charts.tsx +4 -3
  118. package/src/plugins/builtin/filing-events/index.tsx +8 -10
  119. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  120. package/src/plugins/builtin/gpu/equities.tsx +9 -6
  121. package/src/plugins/builtin/help/feedback-tab.tsx +1 -1
  122. package/src/plugins/builtin/iv-history/format.ts +3 -0
  123. package/src/plugins/builtin/iv-history/headless.ts +3 -1
  124. package/src/plugins/builtin/iv-history/index.tsx +3 -1
  125. package/src/plugins/builtin/iv-history/model.ts +3 -1
  126. package/src/plugins/builtin/iv-history/screen-pane.tsx +3 -1
  127. package/src/plugins/builtin/iv-history/universe.ts +1 -1
  128. package/src/plugins/builtin/jobs/headless.ts +6 -20
  129. package/src/plugins/builtin/jobs/index.tsx +2 -0
  130. package/src/plugins/builtin/jobs/lock-note.ts +16 -0
  131. package/src/plugins/builtin/notes/index.tsx +1 -1
  132. package/src/plugins/builtin/notes/owner.tsx +2 -2
  133. package/src/plugins/builtin/options/carry.ts +36 -0
  134. package/src/plugins/builtin/options/index.tsx +2 -1
  135. package/src/plugins/builtin/options/model-spot.ts +94 -0
  136. package/src/plugins/builtin/options/open-interest-as-of.ts +74 -0
  137. package/src/plugins/builtin/options/view.tsx +69 -11
  138. package/src/plugins/builtin/options-calculator/pane.tsx +56 -1
  139. package/src/plugins/builtin/options-calculator/premium-history.ts +34 -0
  140. package/src/plugins/builtin/options-scenario/client.ts +33 -9
  141. package/src/plugins/builtin/options-scenario/csp.ts +168 -0
  142. package/src/plugins/builtin/options-scenario/evidence.ts +3 -0
  143. package/src/plugins/builtin/options-scenario/headless.ts +24 -8
  144. package/src/plugins/builtin/options-scenario/index.tsx +3 -1
  145. package/src/plugins/builtin/options-scenario/pane.tsx +32 -6
  146. package/src/plugins/builtin/portfolio-list/mutations.ts +1 -1
  147. package/src/plugins/builtin/portfolio-list/pane/remove-ticker.ts +1 -1
  148. package/src/plugins/builtin/research/corporate-actions-pane.tsx +4 -2
  149. package/src/plugins/builtin/research/event-model.ts +7 -4
  150. package/src/plugins/builtin/risk-factors/index.tsx +18 -0
  151. package/src/plugins/builtin/risk-factors/pane.tsx +2 -1
  152. package/src/plugins/builtin/scanner/headless.ts +9 -2
  153. package/src/plugins/builtin/scanner/index.ts +3 -0
  154. package/src/plugins/builtin/scanner/lock-note.ts +17 -0
  155. package/src/plugins/builtin/series-inventory.ts +2 -1
  156. package/src/plugins/builtin/shared/cloud-denied.ts +39 -8
  157. package/src/plugins/builtin/shared/earnings-report.ts +10 -4
  158. package/src/plugins/builtin/shared/pro-lock-note.ts +58 -0
  159. package/src/plugins/builtin/shared/series-list-detail.tsx +10 -5
  160. package/src/plugins/builtin/social-mentions/client.ts +3 -3
  161. package/src/plugins/builtin/social-mentions/headless.ts +6 -2
  162. package/src/plugins/builtin/social-mentions/index.tsx +2 -1
  163. package/src/plugins/builtin/social-mentions/lock-note.ts +15 -0
  164. package/src/plugins/builtin/supply-chain/flow.tsx +15 -13
  165. package/src/plugins/builtin/supply-chain/graph.tsx +6 -4
  166. package/src/plugins/builtin/supply-chain/pane.tsx +8 -5
  167. package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +14 -1
  168. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +9 -7
  169. package/src/plugins/builtin/ticker-detail/headless.ts +4 -1
  170. package/src/plugins/builtin/ticker-detail/overview/components.tsx +105 -104
  171. package/src/plugins/builtin/ticker-detail/overview/model.ts +9 -2
  172. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +34 -8
  173. package/src/plugins/builtin/vol-surface/pane.tsx +6 -3
  174. package/src/public/__snapshots__/public-api.test.ts.snap +2 -0
  175. package/src/renderers/dom/data-table/index.tsx +48 -15
  176. package/src/renderers/dom/data-table/row.tsx +1 -1
  177. package/src/renderers/dom/desktop/popover.tsx +6 -15
  178. package/src/renderers/dom/host/input.tsx +27 -3
  179. package/src/renderers/dom/host/surface-3d.tsx +6 -4
  180. package/src/renderers/dom/styles.css +26 -6
  181. package/src/renderers/electrobun/view/app-services.ts +2 -22
  182. package/src/renderers/electrobun/view/news-source.ts +42 -0
  183. package/src/series/validate.ts +5 -0
  184. package/src/shares/dialog.tsx +2 -1
  185. package/src/sources/gloomberb-cloud/normalizers.ts +2 -0
  186. package/src/sources/provider-router/financials.ts +5 -0
  187. package/src/{plugins/builtin/cloud/team → team}/collections-sync.ts +6 -6
  188. package/src/{plugins/builtin/cloud/team → team}/collections.ts +3 -3
  189. package/src/{plugins/builtin/cloud/team → team}/model.ts +2 -2
  190. package/src/{plugins/builtin/cloud/team → team}/pane-request.ts +1 -1
  191. package/src/{plugins/builtin/cloud/team → team}/store.ts +3 -3
  192. package/src/types/data-provider.ts +3 -0
  193. package/src/types/financials.ts +28 -0
  194. package/src/types/plugin.ts +17 -4
  195. package/src/utils/decimal-comma.ts +18 -0
  196. package/src/utils/exchanges.ts +7 -0
  197. package/src/utils/financial-statements.ts +25 -0
  198. package/src/utils/forward-dividend.ts +39 -9
  199. package/src/utils/fundamentals.ts +41 -1
  200. package/src/utils/option-expiry.ts +16 -0
  201. package/src/utils/sec.ts +2 -1
@@ -23,7 +23,7 @@ import { fail } from "../errors";
23
23
  import type { MarketContext } from "../types";
24
24
  import type { CliCommandContext, GloomPlugin } from "../../types/plugin";
25
25
  import { indexRootFor, indexRootTryLine } from "../index-roots";
26
- import { knownGapMessage } from "../known-gaps";
26
+ import { knownGapMessage, settingsPointer } from "../known-gaps";
27
27
  import { instrumentTypeLabel } from "../../tickers/instrument-kind";
28
28
  import { canonicalExchange, isUsListingExchange } from "../../utils/exchanges";
29
29
  import { groupByCompany } from "../../tickers/company-name";
@@ -325,9 +325,9 @@ function catalogHint(query: string): string {
325
325
 
326
326
  /**
327
327
  * The notes beside the results: the index a bare root names, data the query
328
- * asks for that no source carries yet, the central bank it names, and for an
329
- * empty search the catalog, unless a known gap, the bank or the functions
330
- * listed already said where to look instead.
328
+ * asks for that no source carries yet, the central bank it names, the setting
329
+ * it names ("light theme"), and for an empty search the catalog, unless one of
330
+ * those or the functions listed already said where to look instead.
331
331
  */
332
332
  function searchNotes(
333
333
  query: string,
@@ -337,9 +337,10 @@ function searchNotes(
337
337
  ): string[] {
338
338
  const gap = knownGapMessage(query);
339
339
  const bank = searchedCentralBank(query);
340
+ const setting = settingsPointer(query);
340
341
  const empty = candidates.length === 0 && !fate.stopped;
341
- return [...fate.notes, indexRootNote(query, candidates), gap, bank ? centralBankLine(bank) : null,
342
- empty && !gap && !bank && functions.length === 0 ? catalogHint(query) : null]
342
+ return [...fate.notes, indexRootNote(query, candidates), gap, bank ? centralBankLine(bank) : null, setting,
343
+ empty && !gap && !bank && !setting && functions.length === 0 ? catalogHint(query) : null]
343
344
  .filter((note): note is string => !!note);
344
345
  }
345
346
 
@@ -379,6 +380,8 @@ export function buildSearchReport({
379
380
  if (gap) lines.push("", gap);
380
381
  const bank = searchedCentralBank(query);
381
382
  if (bank) lines.push("", centralBankLine(bank));
383
+ const setting = settingsPointer(query);
384
+ if (setting) lines.push("", setting);
382
385
  const charted = renderSeriesMatches(query, series, SEARCH_SERIES_LIMIT, cliTerminalWidth() ?? 100, "catalog");
383
386
  // A query that names the data ("carbon", "day-ahead") lists its series first; words that happen to
384
387
  // be in a series name ("apple") list them after the tickers.
@@ -390,7 +393,7 @@ export function buildSearchReport({
390
393
  if (candidates.length === 0) {
391
394
  if (fate.stopped) return lines.join("\n");
392
395
  if (functions.length > 0) lines.push("", renderFunctionMatches(functions));
393
- else if (!gap && !bank && !charted) lines.push(cliStyles.muted("No matches found."), cliStyles.muted(catalogHint(query)));
396
+ else if (!gap && !bank && !setting && !charted) lines.push(cliStyles.muted("No matches found."), cliStyles.muted(catalogHint(query)));
394
397
  return lines.join("\n");
395
398
  }
396
399
 
@@ -1,6 +1,6 @@
1
1
  import { formatShortDate, parseDisplayDate } from "../../utils/datetime-format";
2
2
  import { formatReportedMoney } from "../../utils/reported-money";
3
- import { convertedPriceEarningsNote, epsConversionNote, fundamentalsCurrency, reportedEnterpriseValue, ttmConversionNote } from "../../utils/fundamentals";
3
+ import { convertedPriceEarningsNote, enterpriseValueLabel, enterpriseValueReconciliationNote, epsConversionNote, fundamentalsCurrency, reportedEnterpriseValue, ttmConversionNote } from "../../utils/fundamentals";
4
4
  import { latestFinancialPeriod } from "../../utils/latest-financial-period";
5
5
  import { exportedFundamentals, formatPriceEarnings, priceEarningsOnEarnings } from "../../utils/price-earnings";
6
6
  import { describeFundamentalMarketCap, selectMarketCapitalization } from "../../utils/market-capitalization";
@@ -336,11 +336,18 @@ function reportedMoney(value: number | undefined, currency: string | undefined,
336
336
  return { text: formatReportedMoney(value, currency, perShare), value, unit: currency?.trim() || "ccy?" };
337
337
  }
338
338
 
339
+ /** The EV line: labelled "(provider)" when it is more than 5% from market cap + debt - cash, "unavailable" when withdrawn. */
340
+ function enterpriseValueFigure(figure: MetricFigure, fundamentals: TickerFinancials["fundamentals"]): MetricFigure {
341
+ const label = enterpriseValueLabel(fundamentals);
342
+ if (label === "unavailable") return { text: "unavailable", value: null, unit: figure.unit };
343
+ return label === "provider" && figure.text !== "—" ? { ...figure, text: `${figure.text} (provider)` } : figure;
344
+ }
345
+
339
346
  function percentFigure(fraction: number | null | undefined, format: (value: number) => string): MetricFigure {
340
347
  return fraction != null ? { text: format(fraction), value: fraction * 100, unit: "%" } : { text: "—" };
341
348
  }
342
349
 
343
- /** A forward yield says what it annualizes and when, as DVD does; any other yield is the figure alone. */
350
+ /** A forward yield says what it annualizes and when, as DVD does (a declared distribution by its ex-date); any other yield is the figure alone. */
344
351
  function forwardYieldFigure(fundamentals: TickerFinancials["fundamentals"]): MetricFigure {
345
352
  const figure = percentFigure(fundamentals?.dividendYield, formatFractionPercentCell);
346
353
  return fundamentals?.dividendYieldBasis === "forward" && fundamentals.dividendYield != null
@@ -366,7 +373,7 @@ function fundamentalsMetrics(
366
373
  const receipt = fundamentals?.sharesOutstanding != null && sharesOutstandingInReceipts(quote, fundamentals, profile?.description);
367
374
  return [
368
375
  { label: "Market Cap", ...marketCap },
369
- { label: "Enterprise Value", ...enterpriseValue },
376
+ { label: "Enterprise Value", ...enterpriseValueFigure(enterpriseValue, fundamentals) },
370
377
  // A multiple over a loss is N/M, even beside a positive one the source served from an older period.
371
378
  { label: "P/E (TTM)", ...multipleFigure(priceEarningsOnEarnings(fundamentals?.trailingPE, fundamentals?.eps)) },
372
379
  { label: "Forward P/E", ...multipleFigure(priceEarningsOnEarnings(fundamentals?.forwardPE, fundamentals?.forwardEps)) },
@@ -404,10 +411,11 @@ function metricLines(metrics: readonly FundamentalsMetric[]): Array<[string, str
404
411
  const TRAILING_MONEY_METRICS = new Set(["EPS (TTM)", "Revenue (TTM)", "Net Income (TTM)", "Operating Cash Flow (TTM)", "Free Cash Flow (TTM)"]);
405
412
 
406
413
  /**
407
- * The unit notes under shown fundamentals: what a converted TTM block was converted from, an EPS in another
408
- * currency than the amounts beside it, and what a P/E on such an EPS divides.
414
+ * The notes under shown fundamentals: how a labelled or withdrawn EV compares with market cap + debt - cash, what a
415
+ * converted TTM block was converted from, an EPS in another currency than the amounts beside it, and what a P/E on
416
+ * such an EPS divides.
409
417
  */
410
- function trailingConversionNotes(
418
+ function fundamentalsNotes(
411
419
  fundamentals: TickerFinancials["fundamentals"],
412
420
  metrics: readonly FundamentalsMetric[],
413
421
  quoteCurrency: string | undefined,
@@ -418,7 +426,8 @@ function trailingConversionNotes(
418
426
  const priceEarnings = metrics.some((metric) => metric.label === "P/E (TTM)" && typeof metric.value === "number")
419
427
  ? convertedPriceEarningsNote(fundamentals, quoteCurrency)
420
428
  : undefined;
421
- return [conversion, eps, priceEarnings].filter((note): note is string => !!note);
429
+ const enterpriseValue = shown("Enterprise Value") ? enterpriseValueReconciliationNote(fundamentals) : undefined;
430
+ return [enterpriseValue, conversion, eps, priceEarnings].filter((note): note is string => !!note);
422
431
  }
423
432
 
424
433
  const VALUATION_METRICS = new Set(["Market Cap", "Enterprise Value", "P/E (TTM)", "Forward P/E", "PEG", "EPS (TTM)"]);
@@ -523,7 +532,7 @@ export function renderFundamentalsReport(
523
532
  const fund = fundFactsNote(symbol, financials.exchange || quote?.exchangeName, financials);
524
533
  if (lines.length === before) lines.push("", cliStyles.muted([`No ${view} reported for ${financials.symbol}.`, fund].filter(Boolean).join(" ")));
525
534
  else if (fund) lines.push(cliStyles.muted(fund));
526
- for (const note of trailingConversionNotes(financials.fundamentals, metrics, quote?.currency)) lines.push(cliStyles.muted(note));
535
+ for (const note of fundamentalsNotes(financials.fundamentals, metrics, quote?.currency)) lines.push(cliStyles.muted(note));
527
536
  if (view === "fundamentals") appendTextSection(lines, "Description", profile?.description);
528
537
  return lines.join("\n");
529
538
  }
@@ -563,7 +572,7 @@ export function fundamentalsReportTables(
563
572
  notes: [
564
573
  ...metrics.length === 0
565
574
  ? [`No ${view} reported for ${financials.symbol}.`]
566
- : trailingConversionNotes(financials.fundamentals, metrics, financials.quote?.currency),
575
+ : fundamentalsNotes(financials.fundamentals, metrics, financials.quote?.currency),
567
576
  ...fund ? [fund] : [],
568
577
  ],
569
578
  }),
@@ -726,7 +735,7 @@ export async function buildTickerReport({
726
735
  const metrics = fundamentalsMetrics(quote, priced.fundamentals, profile, { text: marketCapText }, priceReturns, fundamentalsCurrency(financials), { text: enterpriseValueText });
727
736
  const beforeFundamentals = lines.length;
728
737
  appendMetricSection(lines, "Fundamentals", metricLines(metrics));
729
- for (const note of trailingConversionNotes(fundamentals, metrics, quote?.currency)) lines.push(cliStyles.muted(note));
738
+ for (const note of fundamentalsNotes(fundamentals, metrics, quote?.currency)) lines.push(cliStyles.muted(note));
730
739
  // Where a fund's facts would go: under its fundamentals, or in their place.
731
740
  const fund = fundFactsNote(quote?.symbol ?? symbol,
732
741
  quote?.tradingExchangeName || listingExchange || quote?.listingExchangeName || quote?.exchangeName || tickerFile?.metadata.exchange, financials, tickerFile);
@@ -351,7 +351,7 @@ export const GLOSSARY: readonly GlossaryEntry[] = [
351
351
  term: "TTM", aliases: ["trailing twelve months", "trailing 12 months", "trailing four quarters", "ltm", "last twelve months"],
352
352
  functions: ["FA"],
353
353
  definition: "Trailing twelve months: the four latest quarters added up, a year that ends with the latest quarter rather than the fiscal year. "
354
- + "FA's quarterly leverage ratios read their flows over it, and show < 4 quarters when fewer are on file.",
354
+ + "FA's quarterly leverage and valuation ratios (P/E, EV / EBITDA, P/S, FCF yield) read their flows over it, and show < 4 quarters when fewer are on file.",
355
355
  },
356
356
  // Funds (ETF). Named in what ETF says it cannot show yet.
357
357
  {
package/src/cli/index.ts CHANGED
@@ -37,7 +37,7 @@ import { createSystemCliCommands } from "./commands/system";
37
37
  import { brokerCliCommand, ibkrCliCommand } from "./commands/broker";
38
38
  import { listPlugins, updatePlugins } from "./commands/plugins";
39
39
  import { installListedPlugin, removePlugin } from "../plugins/installer";
40
- import { runPaneCatalog, runPaneFunction, runPaneScreenshot } from "./pane-functions";
40
+ import { functionCodeCommand, runPaneCatalog, runPaneFunction, runPaneScreenshot } from "./pane-functions";
41
41
  import { catalogFredSeriesIds } from "./series-search";
42
42
 
43
43
  const { width: SHOT_WIDTH, height: SHOT_HEIGHT } = SHOT_SIZE_LIMITS;
@@ -60,11 +60,11 @@ function createCoreCliCommands(
60
60
  usage: ["help [command]", "<command> --help"],
61
61
  examples: ["help", "help quote", "quote --help"],
62
62
  },
63
- execute: (args, ctx) => {
63
+ execute: async (args, ctx) => {
64
64
  const topic = args[0];
65
65
  if (topic) {
66
66
  const command = lookupCommand(topic);
67
- if (!command) failUnknownCommand(topic, allCommands());
67
+ if (!command) failUnknownCommand(topic, allCommands(), await functionCodeCommand(ctx, topic, []));
68
68
  printCommandHelp(command, ctx);
69
69
  return;
70
70
  }
@@ -306,7 +306,9 @@ function printCommandHelp(command: CliCommandDef, ctx: CliCommandContext): void
306
306
  ctx.printResult({ data: { ...describeCliCommand(command), sections: command.help?.sections ?? [] } });
307
307
  }
308
308
 
309
- function failUnknownCommand(token: string, commands: CliCommandDef[]): never {
309
+ /** `function` is what the word is when it is a function's code, and the command that runs it. */
310
+ function failUnknownCommand(token: string, commands: CliCommandDef[], fn: { code: string; command: string } | null = null): never {
311
+ if (fn) fail(`${fn.code} is a function: ${fn.command}`);
310
312
  const suggestion = suggestCliCommand(
311
313
  normalizeCliCommandToken(token),
312
314
  commands.flatMap((command) => [command.name, ...(command.aliases ?? [])]),
@@ -397,14 +399,20 @@ export async function dispatchCli(args: string[], options: DispatchCliOptions =
397
399
  /** Explains why dispatch found no command in `args`, suggesting the closest one. */
398
400
  export async function failUnknownCliCommand(args: string[], options: DispatchCliOptions = {}): Promise<never> {
399
401
  let token = args[0] ?? "";
402
+ let parsed: ReturnType<typeof parseCliGlobalArgs> | null = null;
400
403
  try {
401
404
  // Global flags may come first, so name the token dispatch actually looked up.
402
- token = parseCliGlobalArgs(args).args[0] ?? token;
405
+ parsed = parseCliGlobalArgs(args);
406
+ token = parsed.args[0] ?? token;
403
407
  } catch {
404
408
  // dispatchCli already reported flags that do not parse.
405
409
  }
406
410
  const registry = await createRegistry(options);
407
- return failUnknownCommand(token, listedCommands(registry).map((entry) => entry.command));
411
+ // A function's code typed as a command is pointed at `fn`, with the arguments the reader gave.
412
+ const fn = token && parsed
413
+ ? await functionCodeCommand(createCliCommandContext("core", registry, parsed.options, parsed.args.slice(1)), token, parsed.args.slice(1))
414
+ : null;
415
+ return failUnknownCommand(token, listedCommands(registry).map((entry) => entry.command), fn);
408
416
  }
409
417
 
410
418
  export async function runCli(args: string[], options: DispatchCliOptions = {}): Promise<boolean> {
@@ -82,6 +82,28 @@ export function topicPointer(query: string): { message: string; codes: string[]
82
82
  return { message: `${pointer.topic}: not in the terminal yet; closest: ${closest}`, codes: pointer.closest.map(([code]) => code) };
83
83
  }
84
84
 
85
+ /**
86
+ * Settings people look for by name that are a `config` command, not a function or a ticker: the line `search` and
87
+ * `catalog` print beside whatever else the query finds. A query asks for one when every word is a word of that
88
+ * setting and one of them names it: "light theme", "dark", "colour-blind", but not "ai theme" (THEM's basket) or
89
+ * "dark pool".
90
+ */
91
+ const SETTING_POINTERS: ReadonlyArray<{ names: readonly string[]; words: readonly string[]; line: string }> = [
92
+ {
93
+ names: ["theme", "themes", "light", "dark", "colorblind", "colourblind", "blind"],
94
+ words: ["mode", "color", "colour", "colors", "colours", "high", "contrast", "change", "switch", "set", "setting", "settings"],
95
+ line: "Themes: gloomberb config themes",
96
+ },
97
+ ];
98
+
99
+ /** The `config` command for a query that names a setting ("light theme"), else null. */
100
+ export function settingsPointer(query: string): string | null {
101
+ const asked = words(query);
102
+ if (asked.length === 0) return null;
103
+ return SETTING_POINTERS.find(({ names, words: other }) => asked.some((word) => names.includes(word))
104
+ && asked.every((word) => names.includes(word) || other.includes(word)))?.line ?? null;
105
+ }
106
+
85
107
  /** Letters and digits only, lower case: `EU-ETS`, `eu ets` and `EUETS` are one lookup. */
86
108
  function lookupKey(text: string): string {
87
109
  return text.toLowerCase().replace(/[^a-z0-9]/g, "");
@@ -115,6 +115,43 @@ export function buildPaneFunctionLookup(registry: PaneFunctionCatalog): Map<stri
115
115
  return lookup;
116
116
  }
117
117
 
118
+ /** What a function takes after its code, as the catalog writes it: `<ticker>`, `<fund, ticker, or CIK>`, or nothing. */
119
+ function argumentHint({ argKind, argPlaceholder }: Pick<PaneCatalogEntry, "argKind" | "argPlaceholder">): string {
120
+ if (argPlaceholder) return `<${argPlaceholder}>`;
121
+ return argKind && argKind !== "none" ? `<${argKind}>` : "";
122
+ }
123
+
124
+ /** A command-line token that is the code or an alias of a function, with what the function takes. */
125
+ export interface FunctionCode {
126
+ argument: string;
127
+ }
128
+
129
+ /**
130
+ * Whether `token` is the code or an alias of a function (`OVDV`, `EEO`), in any
131
+ * case: the function `fn` opens for it, not one it merely matches by a keyword
132
+ * or a label.
133
+ */
134
+ export function findFunctionCode(registry: PaneFunctionCatalog, token: string): FunctionCode | null {
135
+ const wanted = normalizeLookupToken(token);
136
+ const found = wanted ? buildPaneFunctionLookup(registry).get(wanted) : undefined;
137
+ if (!found || !("paneId" in found && "description" in found)) return null;
138
+ const pane = registry.panes.get(found.paneId);
139
+ const headless = pane ? getHeadlessPaneDefinition(found, pane) : undefined;
140
+ const codes = [
141
+ found.shortcut?.prefix,
142
+ ...(found.shortcut?.aliases ?? []),
143
+ headless?.discovery?.id,
144
+ ...(headless?.discovery?.aliases ?? []),
145
+ ];
146
+ if (!codes.some((code) => code && normalizeLookupToken(code) === wanted)) return null;
147
+ return {
148
+ argument: argumentHint({
149
+ argKind: headless?.argument.kind ?? found.shortcut?.argKind,
150
+ argPlaceholder: headless?.argument.placeholder ?? found.shortcut?.argPlaceholder,
151
+ }),
152
+ };
153
+ }
154
+
118
155
  function sampleArgForTemplate(template: PaneTemplateDef): string {
119
156
  switch (template.shortcut?.argKind) {
120
157
  case "ticker":
@@ -438,11 +475,6 @@ function readinessCell(readiness: PaneFunctionReadiness): string {
438
475
  return cliStyles.muted(label);
439
476
  }
440
477
 
441
- function catalogArgument(entry: PaneCatalogEntry): string {
442
- if (entry.argPlaceholder) return `<${entry.argPlaceholder}>`;
443
- return entry.argKind && entry.argKind !== "none" ? `<${entry.argKind}>` : "";
444
- }
445
-
446
478
  // Choices longer than this move from the flag into its description, where they can wrap.
447
479
  const MAX_INLINE_CHOICES = 24;
448
480
 
@@ -473,7 +505,7 @@ export function limitationSentence(text: string): string {
473
505
  function renderCatalogEntry(entry: PaneCatalogEntry): string {
474
506
  const width = Math.min(cliTerminalWidth() ?? CATALOG_TEXT_WIDTH, CATALOG_TEXT_WIDTH);
475
507
  const capability = entry.capability;
476
- const argument = catalogArgument(entry);
508
+ const argument = argumentHint(entry);
477
509
  const invocation = `${entry.token}${argument ? ` ${argument}` : ""}`;
478
510
  const lines = [
479
511
  `${cliStyles.accent(entry.token)} ${cliStyles.bold(entry.label)}`,
@@ -543,25 +575,28 @@ export function renderPaneCatalogReport(
543
575
  glossary: GlossaryLookup = { exact: [], partial: [] },
544
576
  gap: string | null = null,
545
577
  series: readonly SeriesMatch[] = [],
578
+ /** The `config` command a query names a setting of ("light theme"), printed last, beside the functions. */
579
+ setting: string | null = null,
546
580
  ): string {
547
581
  const query = args.query.trim().toLowerCase();
548
582
  const exact = query
549
583
  ? entries.find((entry) => entry.token.toLowerCase() === query)
550
584
  ?? entries.find((entry) => entry.aliases.some((alias) => alias.toLowerCase() === query))
551
585
  : undefined;
552
- if (exact) return renderCatalogEntry(exact);
586
+ const withSetting = (text: string) => [text, setting ?? ""].filter(Boolean).join("\n\n");
587
+ if (exact) return withSetting(renderCatalogEntry(exact));
553
588
 
554
589
  const width = Math.min(cliTerminalWidth() ?? CATALOG_TEXT_WIDTH, CATALOG_TEXT_WIDTH);
555
590
  const terms = renderCatalogGlossary(glossary, width);
556
591
  const charted = renderSeriesMatches(args.query.trim(), series, Number.isFinite(args.limit) ? CATALOG_SERIES_LIMIT : series.length, width);
557
- const withGap = (text: string) => [gap ? wrapText(gap, width).join("\n") : "", text].filter(Boolean).join("\n\n");
592
+ const withGap = (text: string) => withSetting([gap ? wrapText(gap, width).join("\n") : "", text].filter(Boolean).join("\n\n"));
558
593
  const withTerms = (text: string) => withGap([text, charted, terms].filter(Boolean).join("\n\n"));
559
594
  if (args.query && entries.length === 1) return withTerms(renderCatalogEntry(entries[0]!));
560
595
 
561
596
  const note = (text: string) => wrapText(text, width).map((line) => cliStyles.muted(line)).join("\n");
562
597
  if (entries.length === 0) {
563
598
  if (charted) return withGap([charted, terms].filter(Boolean).join("\n\n"));
564
- if (glossary.exact.length > 0 || gap) return withGap(terms);
599
+ if (glossary.exact.length > 0 || gap || setting) return withGap(terms);
565
600
  return withTerms(note(args.query
566
601
  ? `No functions match "${args.query}". Run gloomberb catalog to browse them all.`
567
602
  : "No functions are available."));
@@ -586,7 +621,7 @@ export function renderPaneCatalogReport(
586
621
  shown.map((entry) => [
587
622
  cliStyles.command(entry.token),
588
623
  entry.label,
589
- catalogArgument(entry),
624
+ argumentHint(entry),
590
625
  readinessCell(entry.capability.reportReadiness),
591
626
  entry.description,
592
627
  ]),
@@ -1,34 +1,39 @@
1
1
  import type { DesktopPaneShotRenderedRow } from "../desktop-pane-shot";
2
- import { cliStyles, cliTerminalWidth, renderSection, renderTable, wrapText } from "../../utils/cli-output";
2
+ import { cliStyles, renderSection, renderTable } from "../../utils/cli-output";
3
3
  import { formatUtcTime } from "../../utils/utc-time";
4
4
  import type { MarketContext } from "../types";
5
5
  import type { PaneFunctionReport } from "./report";
6
6
  import type { ResolvedPaneFunction } from "./resolver";
7
7
  import { collectShotSymbols } from "./data";
8
+ import { lockNoteLines } from "./lock-note";
9
+ import { lockNoteReason } from "../../plugins/builtin/shared/pro-lock-note";
8
10
  import { renderDesktopShot, type PaneScreenshotResult } from "./screenshot";
9
11
  import { deriveRenderedFreshness, formatFreshnessLine, type ReportFreshness } from "./freshness";
10
12
  import { exportTextTable, reportFooterLines, type CliReportTables } from "../report-tables";
11
- import { renderedReportFreshness, renderedReportNotices, renderedReportObservations } from "./report-notices";
13
+ import { renderedReportFailure, renderedReportFreshness, renderedReportNotices, renderedReportObservations } from "./report-notices";
12
14
 
13
15
  const DOM_REPORT_WIDTH = 1280;
14
16
  const DOM_REPORT_HEIGHT = 720;
15
17
  const DOM_LIMITATION =
16
18
  "Only values visible in the rendered pane are returned; clipped or off-screen data may be omitted.";
17
- /** The widest a lock note wraps to, as the catalog's text. */
18
- const LOCK_NOTE_WIDTH = 100;
19
19
  const DOM_EVERY_ROW =
20
20
  "Every row of the pane's table is returned, including rows scrolled out of view.";
21
21
 
22
22
  /** The sign-in wall's buttons, which a rendered view reads as words: `Log in ↵ Sign up free`. */
23
23
  const SIGN_IN_WALL_BUTTONS = /\s*\b(?:Log in|Sign up free|Resend Verification Email)(?:\s*\u21B5)?(?=\s|$)/g;
24
24
 
25
+ /** Whether the wall asks for the account's email to be verified, not for a sign-in. */
26
+ function asksToVerifyEmail(visibleText: string): boolean {
27
+ return /\bVerify your email\b/i.test(visibleText);
28
+ }
29
+
25
30
  /**
26
31
  * A pane behind the sign-in wall, as a report reads it: the wall's headline
27
32
  * without its buttons, and what it needs in place of them.
28
33
  */
29
34
  function signInWallText(visibleText: string): string {
30
35
  const headline = visibleText.replace(SIGN_IN_WALL_BUTTONS, "").trim();
31
- const needs = /\bVerify your email\b/i.test(visibleText)
36
+ const needs = asksToVerifyEmail(visibleText)
32
37
  ? "Requires a verified email on your Gloom account; resend the verification email in the app."
33
38
  : "Requires a free Gloom sign-in: log in or sign up in the app.";
34
39
  return headline ? `${headline} ${needs}` : needs;
@@ -195,14 +200,20 @@ export function buildDomPaneReportFromRender(
195
200
  // A wall in place of the whole pane: nothing was read, so the report says
196
201
  // what is locked and how to reach it, and cites no source.
197
202
  const gate = !hasStructuredRows ? result.render.accessGate ?? null : null;
198
- const lockNote = gate ? resolved.pane.reportLockNote?.({ gate, symbol: result.symbols[0] ?? null }) ?? null : null;
203
+ const lockNote = gate
204
+ ? resolved.pane.reportLockNote?.({
205
+ gate,
206
+ symbol: result.symbols[0] ?? null,
207
+ token: resolved.token,
208
+ ...gate === "sign-in" && asksToVerifyEmail(result.render.visibleText ?? "") ? { verifyEmail: true } : {},
209
+ }) ?? null
210
+ : null;
199
211
  const locked = gate === "sign-in" || lockNote ? gate : null;
200
- // JSON and CSV say why the report is empty in the note's words, on one line.
201
- const lockReason = lockNote?.replace(/\s*\n\s*/g, " ") ?? null;
212
+ const lockReason = lockNote ? lockNoteReason(lockNote) : null;
202
213
  if (hasStructuredRows) {
203
214
  textLines.push(...renderDomTables(rows));
204
215
  } else if (lockNote) {
205
- textLines.push(...wrapText(lockNote, Math.min(cliTerminalWidth() ?? LOCK_NOTE_WIDTH, LOCK_NOTE_WIDTH)));
216
+ textLines.push(...lockNoteLines(lockNote));
206
217
  } else if (result.render.visibleText) {
207
218
  textLines.push(gate === "sign-in" ? signInWallText(result.render.visibleText) : result.render.visibleText);
208
219
  } else {
@@ -321,13 +332,21 @@ function failedDomReport(
321
332
  };
322
333
  }
323
334
 
335
+ /** A report the rendered pane says it cannot give; the command fails with the pane's words and advice line. */
336
+ class RenderedReportFailure extends Error {
337
+ constructor(message: string, readonly details?: string) {
338
+ super(message);
339
+ }
340
+ }
341
+
324
342
  export async function buildDomFunctionReport(
325
343
  resolved: ResolvedPaneFunction,
326
344
  context: MarketContext,
327
345
  rawArg: string,
328
346
  ): Promise<PaneFunctionReport> {
347
+ let result: PaneScreenshotResult;
329
348
  try {
330
- const result = await renderDesktopShot({
349
+ result = await renderDesktopShot({
331
350
  resolved,
332
351
  context,
333
352
  rawArg,
@@ -340,6 +359,13 @@ export async function buildDomFunctionReport(
340
359
  options: {},
341
360
  captureImage: false,
342
361
  });
362
+ } catch (error) {
363
+ return failedDomReport(resolved, rawArg, error);
364
+ }
365
+ // No header and no partial view: the error alone, and a failed exit, as the plain command gives.
366
+ const failure = renderedReportFailure(result.render.semanticUi ?? []);
367
+ if (failure) throw new RenderedReportFailure(failure.message, failure.details);
368
+ try {
343
369
  return buildDomPaneReportFromRender(resolved, result);
344
370
  } catch (error) {
345
371
  return failedDomReport(resolved, rawArg, error);
@@ -264,8 +264,10 @@ export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
264
264
  },
265
265
  OSA: {
266
266
  summary: "Build a multi-leg options position and value it at any spot, date and vol shift: payoff, P&L grid, breakevens and Greeks. European pricing, so no early exercise. "
267
- + "With a NAV and a budget in bps it sizes the position as a hedge: contracts, premium, notional protected and coverage of an equity sleeve.",
267
+ + "With a NAV and a budget in bps it sizes the position as a hedge: contracts, premium, notional protected and coverage of an equity sleeve. "
268
+ + "--strategy csp sells a cash-secured put: cash secured, premium, yield on cash and annualised, distance out of the money, break-even, days to expiry and an earnings flag.",
268
269
  usage: ["OSA AAPL", "gloomberb fn OSA AAPL --strategy vertical --expiration 2027-01-15",
270
+ "gloomberb fn OSA KO --strategy csp --strike 85 --expiration 2026-11-20",
269
271
  "gloomberb fn OSA SPY --strategy put --strike 700 --expiration 2027-01-15 --nav 100m --budget-bps 50 --sleeve 100m"],
270
272
  keys: [key("a", "dd leg"), key("c", "hain"), key("d", "ate"), key("v", "ol shift")],
271
273
  data: OPTIONS,
@@ -393,7 +395,7 @@ export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
393
395
  bloomberg: ["ERN"],
394
396
  },
395
397
  DVD: {
396
- summary: "Cash distributions, trailing yield, the forward yield fundamentals show (latest distribution annualized), and payout growth. Future payments are not forecast.",
398
+ summary: "Cash distributions, trailing yield, the forward yield fundamentals show (the latest distribution annualized, or the declared one once announced), and payout growth. Future payments are not forecast.",
397
399
  usage: ["DVD KO"],
398
400
  keys: [STEP],
399
401
  data: same("Daily"),
@@ -40,6 +40,7 @@ import { isRecord } from "../../utils/guards";
40
40
  import { formatUtcTime, localTimeSuffix } from "../../utils/utc-time";
41
41
  import { deriveHeadlessFreshness, formatFreshnessLine, staleRowsText, type ReportFreshness } from "./freshness";
42
42
  import { accessGateStatus, errorsAccessGate, isProLock } from "./access-gate";
43
+ import { lockedHeadlessReport } from "./lock-note";
43
44
 
44
45
  interface SerializableHeadlessColumn {
45
46
  key: string;
@@ -721,7 +722,17 @@ export async function buildHeadlessFunctionReport(
721
722
  context: MarketContext,
722
723
  rawArgument: string,
723
724
  ): Promise<PaneFunctionReport> {
724
- const loaded = await loadResolvedHeadlessPaneModel(resolved, context, rawArgument);
725
+ let loaded: LoadedHeadlessPaneModel;
726
+ try {
727
+ loaded = await loadResolvedHeadlessPaneModel(resolved, context, rawArgument);
728
+ } catch (error) {
729
+ // A Pro pane the server refused reports what its wall covers, as its rendered view does.
730
+ const locked = lockedHeadlessReport(resolved, error, () => (
731
+ buildHeadlessPaneLoadArgs(resolved.headless!, resolved.token, rawArgument, resolved.options).symbols
732
+ ));
733
+ if (locked) return locked;
734
+ throw error;
735
+ }
725
736
  const { complete, unavailableSymbols, rowCount, symbols } = reportCoverage(loaded);
726
737
  const serialized = serializeHeadlessPaneResult(loaded.definition, loaded.result);
727
738
  const freshness = deriveHeadlessFreshness(loaded.definition, loaded.result);
@@ -5,6 +5,7 @@ import { withMarketData } from "../context";
5
5
  import { cliStyles, renderStats, type CliStatEntry } from "../../utils/cli-output";
6
6
  import {
7
7
  filterPaneCatalogEntries,
8
+ findFunctionCode,
8
9
  renderPaneCatalogReport,
9
10
  selectPaneCatalogMatches,
10
11
  type PaneFunctionCatalog,
@@ -25,7 +26,7 @@ import {
25
26
  import { incompleteReportGateMessage } from "./access-gate";
26
27
  import { withPersistedCloudSession } from "./cloud-session";
27
28
  import { ListingArgError, listingReadNotes, loadForListing, quotedListingChoiceNotes } from "../listing-arg";
28
- import { knownGapMessage, topicPointer } from "../known-gaps";
29
+ import { knownGapMessage, settingsPointer, topicPointer } from "../known-gaps";
29
30
  import { selectReportTables } from "../report-tables";
30
31
  import { cliOutputWritten } from "../result";
31
32
  import { CLI_EXIT_CODES } from "../exit-codes";
@@ -63,6 +64,44 @@ async function withPaneRuntime<T>(
63
64
  });
64
65
  }
65
66
 
67
+ /** A word that can be a function code: letters and digits, with the dots and dashes some codes carry. */
68
+ const FUNCTION_CODE_WORD = /^[\w.$-]{1,24}$/;
69
+
70
+ /** An argument as a shell reads it back: bare when it is one word, in quotes when it is not. */
71
+ function commandWord(argument: string): string {
72
+ return /^[\w.:,=%+@/-]+$/.test(argument) ? argument : JSON.stringify(argument);
73
+ }
74
+
75
+ /**
76
+ * What to run when a command name is a function's code (`gloomberb OVDV KO`,
77
+ * `gloomberb help OVDV`): `fn` with the reader's own arguments, or the
78
+ * function's argument hint when they gave none. Null when the word is no
79
+ * function code or the catalog cannot be read, and the command stays unknown.
80
+ */
81
+ export async function functionCodeCommand(
82
+ ctx: CliCommandContext,
83
+ token: string,
84
+ args: readonly string[],
85
+ ): Promise<{ code: string; command: string } | null> {
86
+ const word = token.trim();
87
+ if (!FUNCTION_CODE_WORD.test(word)) return null;
88
+ try {
89
+ const found = await withMarketData(ctx, async (market) => {
90
+ const registry = await createPaneCatalog(market, ctx.plugins);
91
+ try {
92
+ return findFunctionCode(registry, word);
93
+ } finally {
94
+ registry.destroy();
95
+ }
96
+ });
97
+ if (!found) return null;
98
+ const code = word.replace(/^\$/, "").toUpperCase();
99
+ return { code, command: ["gloomberb fn", code, args.length > 0 ? args.map(commandWord).join(" ") : found.argument].filter(Boolean).join(" ") };
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+
66
105
  export async function runPaneFunction(args: string[], ctx: CliCommandContext) {
67
106
  await runPaneCliCommand(ctx, async () => {
68
107
  await withPaneRuntime(ctx, args, async ({ parsed, context, resolved }) => {
@@ -277,6 +316,8 @@ export async function runPaneCatalog(args: string[], ctx: CliCommandContext) {
277
316
  const glossary = lookupGlossary(parsed.query);
278
317
  // Data no source carries yet is said once: in the text, else as the envelope's warning.
279
318
  const gap = knownGapMessage(parsed.query);
319
+ // A setting is a command, not a function: "light theme" names `config themes` beside THEM.
320
+ const setting = settingsPointer(parsed.query);
280
321
  // The data catalog series the query names, which `fn G` charts.
281
322
  const series = findCatalogSeries(ctx.plugins, parsed.query);
282
323
  const listed = pointer
@@ -285,9 +326,9 @@ export async function runPaneCatalog(args: string[], ctx: CliCommandContext) {
285
326
  ctx.printResult({
286
327
  data: listed,
287
328
  ...series.length ? { metadata: { series } } : {},
288
- ...gap && ctx.cliOptions.format !== "text" ? { warnings: [gap] } : {},
329
+ ...(gap || setting) && ctx.cliOptions.format !== "text" ? { warnings: [gap, setting].filter((note): note is string => !!note) } : {},
289
330
  }, {
290
- text: () => renderPaneCatalogReport(filtered, effectiveParsed, glossary, gap, series),
331
+ text: () => renderPaneCatalogReport(filtered, effectiveParsed, glossary, gap, series, setting),
291
332
  });
292
333
  } finally {
293
334
  registry.destroy();
@@ -0,0 +1,56 @@
1
+ import { refusedLock } from "../../plugins/builtin/shared/cloud-denied";
2
+ import { lockNoteReason } from "../../plugins/builtin/shared/pro-lock-note";
3
+ import { cliTerminalWidth, wrapText } from "../../utils/cli-output";
4
+ import { reportFooterLines } from "../report-tables";
5
+ import type { PaneFunctionReport } from "./report";
6
+ import type { ResolvedPaneFunction } from "./resolver";
7
+
8
+ /** The widest a lock note wraps to, as the catalog's text. */
9
+ const LOCK_NOTE_WIDTH = 100;
10
+
11
+ /** The closing line of a headless report whose pane was walled whole, as a rendered view's says it. */
12
+ const LOCKED_VIEW_NOTE = "The pane is locked, so no values were read.";
13
+
14
+ /** A pane's `reportLockNote`, wrapped for the text report. */
15
+ export function lockNoteLines(note: string): string[] {
16
+ return wrapText(note, Math.min(cliTerminalWidth() ?? LOCK_NOTE_WIDTH, LOCK_NOTE_WIDTH));
17
+ }
18
+
19
+ /**
20
+ * What a headless report prints when the server refused its read behind a
21
+ * wall, as the rendered view of the same pane prints that wall: the pane's
22
+ * `reportLockNote`, no source line and `locked` in JSON. The refusal may be
23
+ * the error itself or the cause of the one a headless read wrote for it
24
+ * (`explainRefusal`). Null when the error is no such refusal or the pane has
25
+ * no note, and the error stands.
26
+ */
27
+ export function lockedHeadlessReport(
28
+ resolved: ResolvedPaneFunction,
29
+ error: unknown,
30
+ /** The symbols the report was asked for, read once the error is known to be a refusal. */
31
+ symbolsOf: () => string[],
32
+ ): PaneFunctionReport | null {
33
+ const refused = refusedLock(error);
34
+ if (!refused || !resolved.pane.reportLockNote) return null;
35
+ const gate = refused.gate;
36
+ const symbols = symbolsOf();
37
+ const note = resolved.pane.reportLockNote({ ...refused, symbol: symbols[0] ?? null, token: resolved.token });
38
+ const reason = lockNoteReason(note);
39
+ return {
40
+ data: {
41
+ kind: resolved.headless?.shape ?? "rendered-view",
42
+ target: resolved.token,
43
+ capabilityId: resolved.capability.id,
44
+ symbols,
45
+ options: resolved.options,
46
+ rowCount: 0,
47
+ empty: true,
48
+ complete: false,
49
+ unavailableSymbols: symbols,
50
+ reason,
51
+ locked: gate,
52
+ },
53
+ text: [resolved.label, "", ...lockNoteLines(note), "", LOCKED_VIEW_NOTE].join("\n"),
54
+ tables: { tables: [], footer: reportFooterLines({ incomplete: reason, notes: [LOCKED_VIEW_NOTE] }) },
55
+ };
56
+ }