gloomberb 0.12.0 → 0.13.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 (219) hide show
  1. package/README.md +2 -0
  2. package/package.json +1 -2
  3. package/src/api-client/auth.ts +2 -1
  4. package/src/api-client/index.test.ts +34 -0
  5. package/src/api-client/index.ts +10 -2
  6. package/src/api-client/request.ts +4 -1
  7. package/src/api-client/research-activity.ts +61 -0
  8. package/src/api-client/types.ts +2 -1
  9. package/src/cli/commands/plugins.ts +4 -3
  10. package/src/cli/desktop-pane-shot.ts +79 -10
  11. package/src/cli/pane-functions/capabilities.ts +76 -23
  12. package/src/cli/pane-functions/cloud-session.test.ts +54 -0
  13. package/src/cli/pane-functions/cloud-session.ts +38 -0
  14. package/src/cli/pane-functions/dom.test.ts +41 -0
  15. package/src/cli/pane-functions/dom.ts +199 -0
  16. package/src/cli/pane-functions/index.test.ts +13 -2
  17. package/src/cli/pane-functions/index.ts +9 -5
  18. package/src/cli/pane-functions/intraday-shot.test.ts +307 -0
  19. package/src/cli/pane-functions/intraday-shot.ts +334 -0
  20. package/src/cli/pane-functions/report.test.ts +29 -1
  21. package/src/cli/pane-functions/report.ts +68 -7
  22. package/src/cli/pane-functions/screenshot.ts +195 -42
  23. package/src/cli/restore-plugins.ts +10 -3
  24. package/src/components/chart/composite/composite-chart.test.tsx +68 -1
  25. package/src/components/chart/composite/composite-chart.tsx +87 -21
  26. package/src/components/chart/composite/rasterizer.ts +33 -0
  27. package/src/components/chart/composite/renderers.test.ts +51 -4
  28. package/src/components/chart/composite/scene.test.ts +128 -2
  29. package/src/components/chart/composite/scene.ts +92 -16
  30. package/src/components/chart/composite/text-renderer.ts +13 -0
  31. package/src/components/chart/composite/time-scale.ts +30 -10
  32. package/src/components/chart/composite/types.ts +26 -0
  33. package/src/components/command-bar/assist/runtime.ts +38 -30
  34. package/src/components/command-bar/surface/assist.test.tsx +63 -9
  35. package/src/components/command-bar/surface/test-harness.tsx +23 -4
  36. package/src/components/onboarding/account-step/chooser-panel.tsx +2 -2
  37. package/src/components/onboarding/account-step/qr-panel.tsx +2 -10
  38. package/src/components/onboarding/onboarding-wizard.test.tsx +38 -12
  39. package/src/components/onboarding/onboarding-wizard.tsx +49 -14
  40. package/src/data/config/store/index.test.ts +23 -0
  41. package/src/data/config/store/node.ts +7 -1
  42. package/src/data/config/store/normalize.ts +4 -0
  43. package/src/news/types.ts +1 -0
  44. package/src/plugins/builtin/cds/client.test.ts +20 -0
  45. package/src/plugins/builtin/cds/client.ts +13 -3
  46. package/src/plugins/builtin/cds/headless.test.ts +30 -0
  47. package/src/plugins/builtin/cds/headless.ts +113 -0
  48. package/src/plugins/builtin/cds/index.tsx +4 -0
  49. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -1
  50. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  51. package/src/plugins/builtin/chat/controller/channels.ts +2 -1
  52. package/src/plugins/builtin/chat/controller/index.ts +13 -5
  53. package/src/plugins/builtin/chat/controller/realtime.ts +24 -0
  54. package/src/plugins/builtin/chat/controller/session-runtime.ts +76 -8
  55. package/src/plugins/builtin/chat/controller/state.ts +1 -0
  56. package/src/plugins/builtin/chat/controller.test.ts +260 -4
  57. package/src/plugins/builtin/cloud/askg/executor.test.ts +160 -0
  58. package/src/plugins/builtin/cloud/askg/executor.ts +429 -0
  59. package/src/plugins/builtin/cloud/askg/manifest.test.ts +153 -0
  60. package/src/plugins/builtin/cloud/askg/manifest.ts +314 -0
  61. package/src/plugins/builtin/cloud/askg/protocol.test.ts +190 -0
  62. package/src/plugins/builtin/cloud/askg/protocol.ts +286 -0
  63. package/src/plugins/builtin/cloud/askg/undo.test.ts +44 -0
  64. package/src/plugins/builtin/cloud/askg/undo.ts +277 -0
  65. package/src/plugins/builtin/cloud/askg/verify.ts +130 -0
  66. package/src/plugins/builtin/cloud/auth-dialog.tsx +7 -1
  67. package/src/plugins/builtin/cloud/device-signin-dialog.tsx +21 -8
  68. package/src/plugins/builtin/cloud/plugin.tsx +2 -0
  69. package/src/plugins/builtin/cloud/verification-panel.tsx +56 -0
  70. package/src/plugins/builtin/congress-trades/client.ts +11 -0
  71. package/src/plugins/builtin/congress-trades/detail.tsx +3 -3
  72. package/src/plugins/builtin/congress-trades/headless.test.ts +98 -0
  73. package/src/plugins/builtin/congress-trades/headless.ts +145 -0
  74. package/src/plugins/builtin/congress-trades/pane.tsx +3 -3
  75. package/src/plugins/builtin/credit-conditions/client.ts +1 -1
  76. package/src/plugins/builtin/credit-conditions/headless.test.ts +31 -0
  77. package/src/plugins/builtin/credit-conditions/headless.ts +55 -0
  78. package/src/plugins/builtin/credit-conditions/index.tsx +4 -0
  79. package/src/plugins/builtin/dividend-yield/headless.test.ts +88 -0
  80. package/src/plugins/builtin/dividend-yield/headless.ts +135 -0
  81. package/src/plugins/builtin/dividend-yield/index.tsx +12 -8
  82. package/src/plugins/builtin/dividend-yield/model.ts +4 -22
  83. package/src/plugins/builtin/dividend-yield/view.ts +17 -0
  84. package/src/plugins/builtin/earnings/headless.test.ts +82 -0
  85. package/src/plugins/builtin/earnings/headless.ts +162 -0
  86. package/src/plugins/builtin/earnings/index.tsx +2 -0
  87. package/src/plugins/builtin/earnings-calls/data.ts +29 -8
  88. package/src/plugins/builtin/earnings-calls/headless.test.ts +192 -0
  89. package/src/plugins/builtin/earnings-calls/headless.ts +300 -0
  90. package/src/plugins/builtin/earnings-calls/index.tsx +19 -9
  91. package/src/plugins/builtin/earnings-calls/model.ts +238 -0
  92. package/src/plugins/builtin/earnings-calls/pane.tsx +4 -0
  93. package/src/plugins/builtin/earnings-calls/transcript-view.tsx +13 -11
  94. package/src/plugins/builtin/fear-greed/headless.test.ts +53 -0
  95. package/src/plugins/builtin/fear-greed/headless.ts +84 -0
  96. package/src/plugins/builtin/fear-greed/index.tsx +4 -0
  97. package/src/plugins/builtin/holders/client.ts +29 -0
  98. package/src/plugins/builtin/holders/headless.test.ts +58 -0
  99. package/src/plugins/builtin/holders/headless.ts +162 -0
  100. package/src/plugins/builtin/holders/index.tsx +14 -8
  101. package/src/plugins/builtin/holders/pane.tsx +7 -1
  102. package/src/plugins/builtin/insider/client.ts +50 -0
  103. package/src/plugins/builtin/insider/headless.test.ts +122 -0
  104. package/src/plugins/builtin/insider/headless.ts +117 -0
  105. package/src/plugins/builtin/insider/index.tsx +21 -50
  106. package/src/plugins/builtin/insider/insider-data.ts +1 -1
  107. package/src/plugins/builtin/insider/model.ts +72 -0
  108. package/src/plugins/builtin/ipo-calendar/client.ts +15 -0
  109. package/src/plugins/builtin/ipo-calendar/headless.test.ts +89 -0
  110. package/src/plugins/builtin/ipo-calendar/headless.ts +127 -0
  111. package/src/plugins/builtin/ipo-calendar/index.tsx +2 -0
  112. package/src/plugins/builtin/ipo-calendar/model.ts +3 -15
  113. package/src/plugins/builtin/market-movers/client.ts +76 -0
  114. package/src/plugins/builtin/market-movers/headless.test.ts +34 -0
  115. package/src/plugins/builtin/market-movers/headless.ts +73 -0
  116. package/src/plugins/builtin/market-movers/index.tsx +11 -51
  117. package/src/plugins/builtin/news/headless.test.ts +138 -0
  118. package/src/plugins/builtin/news/headless.ts +190 -0
  119. package/src/plugins/builtin/news/index.tsx +13 -9
  120. package/src/plugins/builtin/news/wire/index.tsx +2 -1
  121. package/src/plugins/builtin/research/analyst-headless.test.ts +60 -0
  122. package/src/plugins/builtin/research/analyst-headless.ts +172 -0
  123. package/src/plugins/builtin/research/analyst-model.ts +219 -0
  124. package/src/plugins/builtin/research/analyst-pane.tsx +31 -221
  125. package/src/plugins/builtin/research/client.ts +12 -0
  126. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -237
  127. package/src/plugins/builtin/research/event-model.ts +245 -0
  128. package/src/plugins/builtin/research/events-headless.test.ts +32 -0
  129. package/src/plugins/builtin/research/events-headless.ts +87 -0
  130. package/src/plugins/builtin/research/headless.test.ts +93 -0
  131. package/src/plugins/builtin/research/headless.ts +161 -0
  132. package/src/plugins/builtin/research/index.tsx +32 -18
  133. package/src/plugins/builtin/research-search/pane.tsx +4 -0
  134. package/src/plugins/builtin/scanner/headless.test.ts +59 -0
  135. package/src/plugins/builtin/scanner/headless.ts +125 -0
  136. package/src/plugins/builtin/scanner/index.ts +4 -0
  137. package/src/plugins/builtin/sec/client.ts +16 -0
  138. package/src/plugins/builtin/sec/headless.test.ts +72 -0
  139. package/src/plugins/builtin/sec/headless.ts +69 -0
  140. package/src/plugins/builtin/sec/index.tsx +20 -75
  141. package/src/plugins/builtin/sec/model.ts +89 -0
  142. package/src/plugins/builtin/sectors/client.ts +54 -0
  143. package/src/plugins/builtin/sectors/headless.test.ts +28 -0
  144. package/src/plugins/builtin/sectors/headless.ts +80 -0
  145. package/src/plugins/builtin/sectors/index.tsx +6 -51
  146. package/src/plugins/builtin/shared/cloud-upgrade.ts +8 -4
  147. package/src/plugins/builtin/short-interest/client.ts +5 -2
  148. package/src/plugins/builtin/short-interest/headless.test.ts +96 -0
  149. package/src/plugins/builtin/short-interest/headless.ts +111 -0
  150. package/src/plugins/builtin/short-interest/index.tsx +15 -9
  151. package/src/plugins/builtin/thirteenf/data.ts +13 -11
  152. package/src/plugins/builtin/thirteenf/headless.test.ts +112 -0
  153. package/src/plugins/builtin/thirteenf/headless.ts +286 -0
  154. package/src/plugins/builtin/thirteenf/index.tsx +4 -0
  155. package/src/plugins/builtin/thirteenf/model.ts +13 -0
  156. package/src/plugins/builtin/thirteenf/table.ts +3 -15
  157. package/src/plugins/builtin/ticker-detail/pane.tsx +14 -0
  158. package/src/plugins/builtin/treasury-auctions/headless.test.ts +34 -0
  159. package/src/plugins/builtin/treasury-auctions/headless.ts +95 -0
  160. package/src/plugins/builtin/treasury-auctions/index.tsx +4 -0
  161. package/src/plugins/builtin/volatility/client.ts +16 -3
  162. package/src/plugins/builtin/volatility/headless.test.ts +35 -0
  163. package/src/plugins/builtin/volatility/headless.ts +75 -0
  164. package/src/plugins/builtin/volatility/index.tsx +4 -0
  165. package/src/plugins/builtin/world-indices/client.ts +43 -0
  166. package/src/plugins/builtin/world-indices/headless.test.ts +32 -0
  167. package/src/plugins/builtin/world-indices/headless.ts +109 -0
  168. package/src/plugins/builtin/world-indices/index.tsx +4 -0
  169. package/src/plugins/builtin/yield-curve/headless.test.ts +24 -0
  170. package/src/plugins/builtin/yield-curve/headless.ts +67 -0
  171. package/src/plugins/builtin/yield-curve/index.tsx +6 -8
  172. package/src/plugins/builtin/yield-curve/treasury-data.ts +16 -6
  173. package/src/plugins/bundle.test.ts +58 -1
  174. package/src/plugins/bundle.ts +4 -2
  175. package/src/plugins/catalog-backend.test.ts +26 -0
  176. package/src/plugins/catalog-backend.ts +9 -2
  177. package/src/plugins/catalog-browser.test.ts +0 -13
  178. package/src/plugins/catalog-browser.ts +4 -0
  179. package/src/plugins/catalog-manifest.test.ts +15 -0
  180. package/src/plugins/catalog-manifest.ts +12 -5
  181. package/src/plugins/catalog-ui.ts +0 -6
  182. package/src/plugins/host-contract.ts +9 -0
  183. package/src/plugins/host-modules.ts +12 -0
  184. package/src/plugins/loader.ts +63 -15
  185. package/src/plugins/prediction-markets/headless.test.ts +119 -0
  186. package/src/plugins/prediction-markets/headless.ts +255 -0
  187. package/src/plugins/prediction-markets/index.tsx +2 -0
  188. package/src/plugins/seed.ts +3 -0
  189. package/src/public/broker.ts +6 -0
  190. package/src/remote/schema.test.ts +65 -0
  191. package/src/remote/schema.ts +322 -39
  192. package/src/remote/types.ts +49 -2
  193. package/src/renderers/browser/app-services.ts +3 -0
  194. package/src/renderers/browser/config-host.ts +24 -8
  195. package/src/renderers/browser/deeplink-bridge.test.ts +11 -0
  196. package/src/renderers/browser/deeplink-bridge.ts +15 -3
  197. package/src/renderers/browser/main.tsx +3 -1
  198. package/src/renderers/browser/research-entry.test.ts +8 -0
  199. package/src/renderers/browser/research-entry.ts +7 -0
  200. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +2 -2
  201. package/src/renderers/electrobun/bun/desktop/initialization.ts +10 -3
  202. package/src/renderers/electrobun/bun/external-plugins.ts +20 -4
  203. package/src/renderers/electrobun/view/build-assets.ts +0 -2
  204. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +63 -10
  205. package/src/sources/gloomberb-cloud/news.ts +1 -0
  206. package/src/sync/core-contributors.test.ts +111 -0
  207. package/src/sync/core-contributors.ts +155 -1
  208. package/src/time-series/resolve.test.ts +118 -1
  209. package/src/time-series/resolve.ts +14 -16
  210. package/src/types/config.ts +2 -0
  211. package/src/plugins/broker-sync/broker-sync.test.ts +0 -188
  212. package/src/plugins/broker-sync/native-loader.ts +0 -10
  213. package/src/plugins/broker-sync/normalize.ts +0 -308
  214. package/src/plugins/broker-sync/public.ts +0 -122
  215. package/src/plugins/broker-sync/robinhood-native.ts +0 -367
  216. package/src/plugins/broker-sync/robinhood.ts +0 -114
  217. package/src/plugins/broker-sync/simplefin-native.ts +0 -201
  218. package/src/plugins/broker-sync/simplefin.ts +0 -70
  219. package/src/renderers/electrobun/view/native-stubs/broker-sync-native-loader.ts +0 -7
@@ -0,0 +1,67 @@
1
+ import type {
2
+ HeadlessPaneDefinition,
3
+ HeadlessPaneLoadArgs,
4
+ } from "../../../types/plugin";
5
+ import {
6
+ curveAsOf,
7
+ isInverted,
8
+ loadYieldCurve,
9
+ parseYieldPoints,
10
+ spreadBasisPoints,
11
+ type YieldCurveLoader,
12
+ type YieldPoint,
13
+ } from "./treasury-data";
14
+
15
+ const COLUMNS = [
16
+ { key: "maturity", header: "Maturity" },
17
+ {
18
+ key: "maturityYears",
19
+ header: "Years",
20
+ align: "right" as const,
21
+ format: (value: unknown) => Number(value).toFixed(2).replace(/\.00$/, ""),
22
+ },
23
+ {
24
+ key: "yield",
25
+ header: "Yield",
26
+ align: "right" as const,
27
+ format: (value: unknown) => value == null ? "-" : `${Number(value).toFixed(2)}%`,
28
+ },
29
+ { key: "asOf", header: "As of" },
30
+ ];
31
+
32
+ export interface YieldCurveHeadlessDependencies {
33
+ load(args: HeadlessPaneLoadArgs, loader: YieldCurveLoader): Promise<YieldPoint[]>;
34
+ }
35
+
36
+ const defaultDependencies: YieldCurveHeadlessDependencies = {
37
+ load: (_args, loader) => loadYieldCurve(loader),
38
+ };
39
+
40
+ export function createYieldCurveHeadless(
41
+ dependencies: YieldCurveHeadlessDependencies = defaultDependencies,
42
+ ): HeadlessPaneDefinition<"rows"> {
43
+ return {
44
+ shape: "rows",
45
+ argument: { kind: "none" },
46
+ options: [],
47
+ columns: COLUMNS,
48
+ describe: "US Treasury Yield Curve",
49
+ async load(args, ctx) {
50
+ const points = await dependencies.load(
51
+ args,
52
+ () => ctx.apiClient.getCloudYieldCurve(),
53
+ );
54
+ const rows = parseYieldPoints(points);
55
+ return {
56
+ rows: rows.map((point) => ({ ...point })),
57
+ metadata: {
58
+ asOf: curveAsOf(points),
59
+ inverted: isInverted(points),
60
+ spread2Y10YBasisPoints: spreadBasisPoints(points),
61
+ },
62
+ };
63
+ },
64
+ };
65
+ }
66
+
67
+ export const yieldCurveHeadless = createYieldCurveHeadless();
@@ -12,11 +12,15 @@ import {
12
12
  loadYieldCurve,
13
13
  parseYieldPoints,
14
14
  isInverted,
15
+ spreadBasisPoints,
15
16
  TREASURY_MATURITIES,
16
17
  type YieldPoint,
17
18
  } from "./treasury-data";
18
19
  import { usePaneStatusFooter } from "../shared/pane-footer";
19
20
  import { useAutoRefresh, useUpdatedAgo } from "../shared/auto-refresh";
21
+ import { yieldCurveHeadless } from "./headless";
22
+
23
+ export { yieldCurveHeadless } from "./headless";
20
24
 
21
25
  function formatYield(y: number | null): string {
22
26
  if (y == null) return "—";
@@ -27,13 +31,6 @@ function formatYieldAxis(value: number): string {
27
31
  return `${value.toFixed(2)}%`;
28
32
  }
29
33
 
30
- function spreadBp(points: YieldPoint[]): number | null {
31
- const y2 = points.find((p) => p.maturity === "2Y")?.yield;
32
- const y10 = points.find((p) => p.maturity === "10Y")?.yield;
33
- if (y2 == null || y10 == null) return null;
34
- return Math.round((y10 - y2) * 100);
35
- }
36
-
37
34
  function YieldCurvePane({ focused, width, height }: PaneProps) {
38
35
  const [points, setPoints] = useState<YieldPoint[]>([]);
39
36
  const [loading, setLoading] = useState(false);
@@ -75,7 +72,7 @@ function YieldCurvePane({ focused, width, height }: PaneProps) {
75
72
  });
76
73
 
77
74
  const inverted = isInverted(points);
78
- const bp = spreadBp(points);
75
+ const bp = spreadBasisPoints(points);
79
76
  // Treasury series are daily closes, so which session the curve represents is
80
77
  // status the user needs; "updated Xm ago" only says when we last fetched it.
81
78
  const asOf = curveAsOf(points);
@@ -191,5 +188,6 @@ export const yieldCurveModule: PluginModule = {
191
188
  description: "US Treasury yield curve charted from FRED data.",
192
189
  keywords: ["yield", "curve", "treasury", "bonds", "rates", "gc", "interest"],
193
190
  shortcut: { prefix: "GC" },
191
+ headless: yieldCurveHeadless,
194
192
  }],
195
193
  };
@@ -20,8 +20,12 @@ export const TREASURY_MATURITIES: Array<{ maturity: string; years: number; serie
20
20
  { maturity: "30Y", years: 30, seriesId: "DGS30" },
21
21
  ];
22
22
 
23
- export async function loadYieldCurve(): Promise<YieldPoint[]> {
24
- return apiClient.getCloudYieldCurve();
23
+ export type YieldCurveLoader = () => Promise<YieldPoint[]>;
24
+
25
+ export async function loadYieldCurve(
26
+ loader: YieldCurveLoader = () => apiClient.getCloudYieldCurve(),
27
+ ): Promise<YieldPoint[]> {
28
+ return loader();
25
29
  }
26
30
 
27
31
  export function parseYieldPoints(points: YieldPoint[]): YieldPoint[] {
@@ -45,8 +49,14 @@ export function curveAsOf(points: readonly YieldPoint[]): string | null {
45
49
  return latest;
46
50
  }
47
51
 
48
- export function isInverted(points: YieldPoint[]): boolean {
49
- const y2 = points.find((p) => p.maturity === "2Y")?.yield;
50
- const y10 = points.find((p) => p.maturity === "10Y")?.yield;
51
- return y2 != null && y10 != null && y2 > y10;
52
+ export function spreadBasisPoints(points: readonly YieldPoint[]): number | null {
53
+ const y2 = points.find((point) => point.maturity === "2Y")?.yield;
54
+ const y10 = points.find((point) => point.maturity === "10Y")?.yield;
55
+ if (y2 == null || y10 == null) return null;
56
+ return Math.round((y10 - y2) * 100);
57
+ }
58
+
59
+ export function isInverted(points: readonly YieldPoint[]): boolean {
60
+ const spread = spreadBasisPoints(points);
61
+ return spread != null && spread < 0;
52
62
  }
@@ -22,7 +22,13 @@ function scratchPlugin(source: string): string {
22
22
  }
23
23
 
24
24
  const fakeExports = async (specifier: string) => (
25
- specifier === "gloomberb/ui" ? ["Box", "Text"] : specifier === "react" ? ["useState"] : []
25
+ specifier === "gloomberb/ui"
26
+ ? ["Box", "Text"]
27
+ : specifier === "react"
28
+ ? ["useState"]
29
+ : specifier === "gloomberb/broker"
30
+ ? ["PRESERVED_PASSWORD_HINT", "getBrokerRemoteClient"]
31
+ : []
26
32
  );
27
33
 
28
34
  describe("buildSharedModuleSource", () => {
@@ -97,6 +103,57 @@ describe("bundleExternalPlugin", () => {
97
103
  }
98
104
  });
99
105
 
106
+ test("shares gloomberb/broker rather than bundling the host's copy", async () => {
107
+ // That module owns the remote broker client the desktop view installs at
108
+ // startup. A plugin carrying its own copy reads an empty one and reports
109
+ // that the broker host is unavailable, which looks like a broken broker
110
+ // rather than a bundling mistake.
111
+ const dir = scratchPlugin(`
112
+ import { PRESERVED_PASSWORD_HINT } from "gloomberb/broker";
113
+ export default { id: "scratch", name: "Scratch", version: "1.0.0", hint: PRESERVED_PASSWORD_HINT };
114
+ `);
115
+ try {
116
+ const result = await bundleExternalPlugin(dir, join(dir, "out"), { exportNamesFor: fakeExports });
117
+ const code = await Bun.file(result.outputPath).text();
118
+
119
+ expect(result.shared).toEqual(["gloomberb/broker"]);
120
+ expect(code).toContain(PLUGIN_HOST_GLOBAL);
121
+ } finally {
122
+ rmSync(dir, { recursive: true, force: true });
123
+ }
124
+ });
125
+
126
+ test("compiles the browser entry so a plugin with a native half still loads", async () => {
127
+ // Bun's browser target rejects `node:*` imports even behind a dynamic
128
+ // import, so without this a broker that opens a socket or resolves DNS
129
+ // could not ship to the desktop view at all.
130
+ const dir = mkdtempSync(join(tmpdir(), "gloom-bundle-browser-"));
131
+ writeFileSync(
132
+ join(dir, "package.json"),
133
+ JSON.stringify({ name: "scratch", main: "index.ts", browser: "index.browser.ts" }),
134
+ );
135
+ writeFileSync(join(dir, "native.ts"), `
136
+ import { Socket } from "node:net";
137
+ export const connect = () => new Socket();
138
+ `);
139
+ writeFileSync(join(dir, "index.ts"), `
140
+ export default { id: "scratch", name: "Scratch", version: "1.0.0", load: () => import("./native") };
141
+ `);
142
+ writeFileSync(join(dir, "index.browser.ts"), `
143
+ export default { id: "scratch", name: "Scratch", version: "1.0.0" };
144
+ `);
145
+ const out = join(dir, "out");
146
+ try {
147
+ const result = await bundleExternalPlugin(dir, out, { exportNamesFor: fakeExports });
148
+ const code = await Bun.file(result.outputPath).text();
149
+
150
+ expect(code).toContain("Scratch");
151
+ expect(code).not.toContain("node:net");
152
+ } finally {
153
+ rmSync(dir, { recursive: true, force: true });
154
+ }
155
+ });
156
+
100
157
  test("rejects a directory with no entry file", async () => {
101
158
  const dir = mkdtempSync(join(tmpdir(), "gloom-bundle-empty-"));
102
159
  mkdirSync(join(dir, "src"), { recursive: true });
@@ -1,7 +1,7 @@
1
1
  import { mkdir } from "fs/promises";
2
2
  import { join } from "path";
3
3
 
4
- import { resolvePluginEntry } from "./loader";
4
+ import { resolvePluginBrowserEntry } from "./loader";
5
5
  import { PLUGIN_HOST_GLOBAL, SHARED_SPECIFIERS } from "./host-contract";
6
6
 
7
7
  /**
@@ -98,7 +98,9 @@ export async function bundleExternalPlugin(
98
98
  outDir: string,
99
99
  options: { exportNamesFor?: (specifier: string) => Promise<readonly string[]> } = {},
100
100
  ): Promise<BundlePluginResult> {
101
- const entry = await resolvePluginEntry(pluginDir);
101
+ // The browser entry when the plugin ships one, so a plugin with a native
102
+ // half can still present its UI and metadata in the view.
103
+ const entry = await resolvePluginBrowserEntry(pluginDir);
102
104
  if (!entry) throw new Error(`No plugin entry file in ${pluginDir}`);
103
105
 
104
106
  await mkdir(outDir, { recursive: true });
@@ -18,4 +18,30 @@ describe("desktop backend plugin catalog", () => {
18
18
  expect(plugin?.slots).toBeUndefined();
19
19
  }
20
20
  });
21
+
22
+ test("includes compatible external plugins in the native desktop backend", () => {
23
+ const externalPlugin = {
24
+ id: "external-broker",
25
+ name: "External broker",
26
+ version: "1.0.0",
27
+ targets: ["cli", "tui", "desktop"] as const,
28
+ };
29
+ const backendPlugins = getDesktopBackendPlugins([
30
+ { plugin: externalPlugin, path: "/plugins/external-broker" },
31
+ {
32
+ plugin: { id: "broken", name: "Broken", version: "1.0.0" },
33
+ path: "/plugins/broken",
34
+ error: "load failed",
35
+ },
36
+ {
37
+ plugin: { id: "web-only", name: "Web only", version: "1.0.0" },
38
+ path: "/plugins/web-only",
39
+ unsupportedTarget: "desktop",
40
+ },
41
+ ]);
42
+
43
+ expect(backendPlugins).toContain(externalPlugin);
44
+ expect(backendPlugins.some((plugin) => plugin.id === "broken")).toBe(false);
45
+ expect(backendPlugins.some((plugin) => plugin.id === "web-only")).toBe(false);
46
+ });
21
47
  });
@@ -1,12 +1,19 @@
1
1
  import type { GloomPlugin } from "../types/plugin";
2
2
  import { tickerResearchBackendPlugin } from "./builtin/ticker-research-backend-plugin";
3
3
  import { getLoadablePlugins } from "./catalog";
4
+ import { loadExternalPlugins, type LoadedExternalPlugin } from "./loader";
4
5
  import { predictionMarketsBackendPlugin } from "./prediction-markets/backend-plugin";
5
6
 
6
- export function getDesktopBackendPlugins(): GloomPlugin[] {
7
- return getLoadablePlugins().map((plugin) => {
7
+ export function getDesktopBackendPlugins(
8
+ externalPlugins: LoadedExternalPlugin[] = [],
9
+ ): GloomPlugin[] {
10
+ return getLoadablePlugins(externalPlugins).map((plugin) => {
8
11
  if (plugin.id === "ticker-research") return tickerResearchBackendPlugin;
9
12
  if (plugin.id === "prediction-markets") return predictionMarketsBackendPlugin;
10
13
  return plugin;
11
14
  });
12
15
  }
16
+
17
+ export async function loadDesktopBackendPlugins(): Promise<GloomPlugin[]> {
18
+ return getDesktopBackendPlugins(await loadExternalPlugins("desktop"));
19
+ }
@@ -7,19 +7,6 @@ const paneIds = browserBuiltinPlugins.flatMap((plugin) => plugin.panes?.map((pan
7
7
  const templateIds = new Set(browserBuiltinPlugins.flatMap((plugin) => plugin.paneTemplates?.map((template) => template.id) ?? []));
8
8
 
9
9
  describe("browser plugin catalog", () => {
10
- test("contains the reviewed cloud, local, market, and research plugins", () => {
11
- expect(ids).toEqual([
12
- "gloomberb-cloud",
13
- "portfolio",
14
- "ticker-research",
15
- "application",
16
- "news",
17
- "market-overview",
18
- "macro",
19
- "alerts",
20
- ]);
21
- });
22
-
23
10
  test("excludes native, filesystem, local AI, debug, updater, and external plugins", () => {
24
11
  for (const forbidden of [
25
12
  "broker",
@@ -4,6 +4,8 @@ import {
4
4
  } from "../data/fred-series";
5
5
  import type { GloomPlugin } from "../types/plugin";
6
6
  import { portfolioAnalyticsModule } from "./builtin/analytics";
7
+ import { earningsCallsModule } from "./builtin/earnings-calls";
8
+ import { researchSearchPlugin } from "./builtin/research-search";
7
9
  import { alertsPlugin } from "./builtin/alerts";
8
10
  import { browserGloomberbCloudPlugin } from "./builtin/cloud/browser";
9
11
  import { changelogModule } from "./builtin/changelog";
@@ -68,6 +70,7 @@ const browserTickerResearchPlugin = composeBuiltinPlugin({
68
70
  optionsModule,
69
71
  optionsCalculatorModule,
70
72
  researchModule,
73
+ earningsCallsModule,
71
74
  ],
72
75
  });
73
76
 
@@ -141,6 +144,7 @@ export const browserBuiltinPlugins: readonly GloomPlugin[] = [
141
144
  browserMarketOverviewPlugin,
142
145
  browserMacroPlugin,
143
146
  alertsPlugin,
147
+ researchSearchPlugin,
144
148
  ];
145
149
 
146
150
  export function getBrowserBuiltinPlugins(): readonly GloomPlugin[] {
@@ -34,6 +34,21 @@ describe("built-in plugin manifest", () => {
34
34
  expect(featured.map((plugin) => plugin.id)).toEqual(["gloomberb-cloud"]);
35
35
  });
36
36
 
37
+ test("every declared icon exists in the repo", async () => {
38
+ // The directory resolves these against this repo's HEAD, so a wrong path
39
+ // is a broken image in the plugin grid rather than a missing file here.
40
+ const icons = buildBuiltinManifest()
41
+ .plugins.map((plugin) => plugin.icon)
42
+ .filter((icon): icon is string => !!icon);
43
+
44
+ expect(icons.length).toBeGreaterThan(0);
45
+
46
+ for (const icon of icons) {
47
+ const file = Bun.file(new URL(`../../${icon}`, import.meta.url));
48
+ expect(await file.exists()).toBe(true);
49
+ }
50
+ });
51
+
37
52
  test("describes what each plugin contributes, which is what the directory renders", () => {
38
53
  const cloud = buildBuiltinManifest().plugins.find((plugin) => plugin.id === "gloomberb-cloud");
39
54
 
@@ -18,6 +18,8 @@ export interface BuiltinPluginManifestEntry {
18
18
  categories: string[];
19
19
  toggleable: boolean;
20
20
  featured?: true;
21
+ /** Path to artwork in this repo, resolved by the directory against HEAD. */
22
+ icon?: string;
21
23
  contributes: {
22
24
  panes: string[];
23
25
  capabilities: string[];
@@ -30,8 +32,15 @@ export interface BuiltinPluginManifestEntry {
30
32
  * is reported by `generate-plugin-manifest.ts` rather than silently defaulted,
31
33
  * so a new built-in cannot slip into the directory uncategorised.
32
34
  */
33
- const EDITORIAL: Record<string, { categories: string[]; featured?: true }> = {
34
- "gloomberb-cloud": { categories: ["data", "cloud"], featured: true },
35
+ const EDITORIAL: Record<
36
+ string,
37
+ { categories: string[]; featured?: true; icon?: string }
38
+ > = {
39
+ "gloomberb-cloud": {
40
+ categories: ["data", "cloud"],
41
+ featured: true,
42
+ icon: "plugin-icons/gloomberb-cloud.svg",
43
+ },
35
44
  ai: { categories: ["ai"] },
36
45
  alerts: { categories: ["alerts"] },
37
46
  application: { categories: ["core"] },
@@ -44,10 +53,7 @@ const EDITORIAL: Record<string, { categories: string[]; featured?: true }> = {
44
53
  polls: { categories: ["data"] },
45
54
  portfolio: { categories: ["portfolio"] },
46
55
  "prediction-markets": { categories: ["markets"] },
47
- public: { categories: ["broker"] },
48
56
  "research-search": { categories: ["research", "news"] },
49
- robinhood: { categories: ["broker"] },
50
- simplefin: { categories: ["broker"] },
51
57
  "ticker-research": { categories: ["research"] },
52
58
  yahoo: { categories: ["data"] },
53
59
  };
@@ -74,6 +80,7 @@ export function buildBuiltinManifest(): BuiltinManifest {
74
80
  categories: editorial?.categories ?? [],
75
81
  toggleable: plugin.toggleable === true,
76
82
  ...(editorial?.featured ? { featured: true as const } : {}),
83
+ ...(editorial?.icon ? { icon: editorial.icon } : {}),
77
84
  contributes: {
78
85
  panes: (plugin.panes ?? []).map((pane) => pane.id).sort(),
79
86
  capabilities: (plugin.capabilities ?? [])
@@ -4,9 +4,6 @@ import { newsPlugin } from "./builtin/news";
4
4
  import { notesPlugin } from "./builtin/notes";
5
5
  import { aiPlugin } from "./builtin/ai";
6
6
  import { gloomberbCloudPlugin } from "./builtin/cloud";
7
- import { publicPlugin } from "./broker-sync/public";
8
- import { robinhoodPlugin } from "./broker-sync/robinhood";
9
- import { simpleFinPlugin } from "./broker-sync/simplefin";
10
7
  import { predictionMarketsPlugin } from "./prediction-markets";
11
8
  import { pollsPlugin } from "./builtin/polls";
12
9
  import { alertsPlugin } from "./builtin/alerts";
@@ -25,9 +22,6 @@ export const uiBuiltinPlugins: GloomPlugin[] = [
25
22
  portfolioPlugin,
26
23
  tickerResearchPlugin,
27
24
  brokerPlugin,
28
- publicPlugin,
29
- robinhoodPlugin,
30
- simpleFinPlugin,
31
25
  applicationPlugin,
32
26
  newsPlugin,
33
27
  notesPlugin,
@@ -32,6 +32,15 @@ export const SHARED_SPECIFIERS = [
32
32
  "gloomberb/capabilities",
33
33
  "gloomberb/utils",
34
34
  "gloomberb/react",
35
+ // Modules below hold state or reach the host's services, so a bundled copy
36
+ // is worse than a missing one. `gloomberb/broker` is the clearest case: it
37
+ // owns the remote broker client the desktop view sets at startup, and a
38
+ // plugin carrying its own copy reads an empty one and reports that the
39
+ // broker host is unavailable.
40
+ "gloomberb/broker",
41
+ "gloomberb/dialog",
42
+ "gloomberb/market-data",
43
+ "gloomberb/time-series",
35
44
  ] as const;
36
45
 
37
46
  export type SharedSpecifier = (typeof SHARED_SPECIFIERS)[number];
@@ -24,6 +24,10 @@ export async function installPluginHostModules(): Promise<void> {
24
24
  capabilities,
25
25
  utils,
26
26
  pluginReact,
27
+ broker,
28
+ dialog,
29
+ marketData,
30
+ timeSeries,
27
31
  ] = await Promise.all([
28
32
  import("react"),
29
33
  import("react/jsx-runtime"),
@@ -37,6 +41,10 @@ export async function installPluginHostModules(): Promise<void> {
37
41
  import("../capabilities"),
38
42
  import("../public/utils"),
39
43
  import("../public/react"),
44
+ import("../public/broker"),
45
+ import("../ui/dialog"),
46
+ import("../public/market-data"),
47
+ import("../public/time-series"),
40
48
  ]);
41
49
 
42
50
  const registry: Record<string, unknown> = {
@@ -51,6 +59,10 @@ export async function installPluginHostModules(): Promise<void> {
51
59
  "gloomberb/capabilities": capabilities,
52
60
  "gloomberb/utils": utils,
53
61
  "gloomberb/react": pluginReact,
62
+ "gloomberb/broker": broker,
63
+ "gloomberb/dialog": dialog,
64
+ "gloomberb/market-data": marketData,
65
+ "gloomberb/time-series": timeSeries,
54
66
  };
55
67
 
56
68
  for (const specifier of SHARED_SPECIFIERS) {
@@ -9,6 +9,11 @@ import { linkHostPackages } from "./host-link";
9
9
  const loaderLog = debugLog.createLogger("plugin-loader");
10
10
 
11
11
  const PLUGINS_DIR = join(process.env.HOME || homedir(), ".gloomberb", "plugins");
12
+ /**
13
+ * Host-owned scratch space for plugins, kept beside the plugins folder rather
14
+ * than inside it so nothing the host writes can be mistaken for an install.
15
+ */
16
+ const PLUGIN_CACHE_DIR = join(process.env.HOME || homedir(), ".gloomberb", "plugin-cache");
12
17
 
13
18
  export interface LoadedExternalPlugin {
14
19
  plugin: GloomPlugin;
@@ -22,27 +27,70 @@ export function getPluginsDir(): string {
22
27
  return PLUGINS_DIR;
23
28
  }
24
29
 
25
- /** Resolves a plugin directory's entry file the way `bun install` would. */
26
- export async function resolvePluginEntry(pluginDir: string): Promise<string | null> {
30
+ export function getPluginCacheDir(): string {
31
+ return PLUGIN_CACHE_DIR;
32
+ }
33
+
34
+ /**
35
+ * Whether a directory inside the plugins folder is a plugin at all.
36
+ *
37
+ * Dot-directories are bookkeeping, not plugins: the desktop bundle cache used
38
+ * to be written to `plugins/.cache`, and it showed up in `gloomberb plugins`
39
+ * as an installed plugin and in `gloomberb update` as a repo to pull. The
40
+ * cache has moved out, but old installs still have that directory, so this
41
+ * stays as the single rule every reader shares.
42
+ */
43
+ export function isPluginDirectory(name: string): boolean {
44
+ return !name.startsWith(".");
45
+ }
46
+
47
+ async function readPluginPackageField(pluginDir: string, field: string): Promise<string | null> {
27
48
  const pkgPath = join(pluginDir, "package.json");
28
- if (existsSync(pkgPath)) {
29
- try {
30
- const pkg = JSON.parse(await Bun.file(pkgPath).text());
31
- if (pkg.main) {
32
- const main = join(pluginDir, pkg.main);
33
- if (existsSync(main)) return main;
34
- }
35
- } catch {
36
- // Malformed package.json falls through to the index candidates.
37
- }
49
+ if (!existsSync(pkgPath)) return null;
50
+ try {
51
+ const pkg = JSON.parse(await Bun.file(pkgPath).text());
52
+ const value = pkg[field];
53
+ if (typeof value !== "string" || !value) return null;
54
+ const resolved = join(pluginDir, value);
55
+ return existsSync(resolved) ? resolved : null;
56
+ } catch {
57
+ // Malformed package.json falls through to the index candidates.
58
+ return null;
38
59
  }
39
- for (const candidate of ["index.ts", "index.tsx", "index.js"]) {
40
- const path = join(pluginDir, candidate);
60
+ }
61
+
62
+ function resolveIndexCandidate(pluginDir: string, prefix: string): string | null {
63
+ for (const extension of ["ts", "tsx", "js"]) {
64
+ const path = join(pluginDir, `${prefix}.${extension}`);
41
65
  if (existsSync(path)) return path;
42
66
  }
43
67
  return null;
44
68
  }
45
69
 
70
+ /** Resolves a plugin directory's entry file the way `bun install` would. */
71
+ export async function resolvePluginEntry(pluginDir: string): Promise<string | null> {
72
+ return await readPluginPackageField(pluginDir, "main")
73
+ ?? resolveIndexCandidate(pluginDir, "index");
74
+ }
75
+
76
+ /**
77
+ * Resolves the entry to compile for a browser renderer, preferring the standard
78
+ * `browser` field and falling back to the native entry.
79
+ *
80
+ * A plugin that touches sockets, DNS, or the filesystem cannot be compiled for
81
+ * the desktop view: Bun's browser target rejects `node:*` imports even behind a
82
+ * dynamic import. The app solves this for its own build with private alias
83
+ * rules that swap in stubs, which a plugin in its own repository cannot reach.
84
+ * The `browser` field is that same escape hatch, published: ship a
85
+ * renderer-safe module with the same plugin metadata and let the native half
86
+ * run in the Bun process, where the desktop calls it over RPC anyway.
87
+ */
88
+ export async function resolvePluginBrowserEntry(pluginDir: string): Promise<string | null> {
89
+ return await readPluginPackageField(pluginDir, "browser")
90
+ ?? resolveIndexCandidate(pluginDir, "index.browser")
91
+ ?? await resolvePluginEntry(pluginDir);
92
+ }
93
+
46
94
  export function pluginSupportsTarget(plugin: GloomPlugin, target: PluginTarget): boolean {
47
95
  // No declaration means "everywhere"; the registry fills this in for listed plugins.
48
96
  return !plugin.targets || plugin.targets.length === 0 || plugin.targets.includes(target);
@@ -55,7 +103,7 @@ export async function loadExternalPlugins(target: PluginTarget = "cli"): Promise
55
103
  const entries = await readdir(PLUGINS_DIR, { withFileTypes: true });
56
104
 
57
105
  for (const entry of entries) {
58
- if (!entry.isDirectory()) continue;
106
+ if (!entry.isDirectory() || !isPluginDirectory(entry.name)) continue;
59
107
  const pluginDir = join(PLUGINS_DIR, entry.name);
60
108
 
61
109
  const entryFile = await resolvePluginEntry(pluginDir);