gloomberb 0.9.2 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/README.md +24 -2
  2. package/bin/gloomberb +1 -1
  3. package/package.json +1 -1
  4. package/src/app/runtime/desktop-menu.ts +5 -1
  5. package/src/app/runtime/ticker-refresh.ts +4 -1
  6. package/src/app.tsx +20 -8
  7. package/src/brokers/account-cache.ts +1 -1
  8. package/src/brokers/sync-broker-instance.test.ts +107 -0
  9. package/src/brokers/sync-broker-instance.ts +22 -10
  10. package/src/capabilities/registry.ts +1 -0
  11. package/src/capabilities/types.ts +23 -0
  12. package/src/cli/commands/api.ts +119 -0
  13. package/src/cli/commands/automation.ts +173 -0
  14. package/src/cli/commands/command-utils.ts +53 -0
  15. package/src/cli/commands/market.ts +527 -0
  16. package/src/cli/commands/overview.ts +222 -0
  17. package/src/cli/commands/search.ts +39 -0
  18. package/src/cli/commands/system.ts +553 -0
  19. package/src/cli/commands/ticker.ts +104 -12
  20. package/src/cli/context.ts +35 -2
  21. package/src/cli/desktop-pane-shot.ts +2 -1
  22. package/src/cli/entry.ts +51 -0
  23. package/src/cli/errors.ts +56 -5
  24. package/src/cli/index.ts +73 -11
  25. package/src/cli/options.test.ts +38 -0
  26. package/src/cli/options.ts +95 -0
  27. package/src/cli/pane-functions/index.ts +10 -1
  28. package/src/cli/pane-functions/screenshot.ts +13 -1
  29. package/src/cli/registry.test.ts +40 -0
  30. package/src/cli/registry.ts +46 -5
  31. package/src/cli/result.test.ts +82 -0
  32. package/src/cli/result.ts +149 -0
  33. package/src/cli/types.ts +11 -0
  34. package/src/cli.test.ts +11 -11
  35. package/src/components/chart/bar-chart-renderer.test.ts +108 -3
  36. package/src/components/chart/bar-chart-renderer.ts +226 -27
  37. package/src/components/chart/comparison-stock-chart/interactions.ts +7 -1
  38. package/src/components/chart/comparison-stock-chart/legend.tsx +4 -0
  39. package/src/components/chart/comparison-stock-chart/render-data.ts +37 -8
  40. package/src/components/chart/comparison-stock-chart/selection-runtime.ts +19 -2
  41. package/src/components/chart/comparison-stock-chart/sources.ts +8 -4
  42. package/src/components/chart/comparison-stock-chart/toolbar.tsx +13 -3
  43. package/src/components/chart/comparison-stock-chart/view.tsx +12 -2
  44. package/src/components/chart/core/pane-settings.test.ts +27 -0
  45. package/src/components/chart/core/pane-settings.ts +60 -1
  46. package/src/components/chart/core/pointer.ts +34 -6
  47. package/src/components/chart/price-axis-labels.tsx +3 -1
  48. package/src/components/chart/static/bar-chart-surface.test.tsx +97 -0
  49. package/src/components/chart/static/bar-chart-surface.tsx +151 -28
  50. package/src/components/chart/static/chart/axis-overlays.tsx +243 -0
  51. package/src/components/chart/static/chart/surface.test.tsx +92 -0
  52. package/src/components/chart/static/chart/surface.tsx +167 -24
  53. package/src/components/chart/stock-chart/index.test.ts +25 -0
  54. package/src/components/chart/stock-chart/resolved/runtime.ts +1 -1
  55. package/src/components/chart/stock-chart/settings.ts +7 -3
  56. package/src/components/chart/stock-chart/viewport/runtime.ts +5 -5
  57. package/src/components/command-bar/layout-items/current-layout.ts +5 -1
  58. package/src/components/command-bar/pane-templates/workflow-route.ts +10 -0
  59. package/src/components/command-bar/surface/pane.test.tsx +76 -0
  60. package/src/components/data-table/view.tsx +13 -2
  61. package/src/components/feed-data-table/stack-view.tsx +9 -1
  62. package/src/components/index.ts +1 -1
  63. package/src/components/input-search-bar.test.tsx +103 -0
  64. package/src/components/input-search-bar.tsx +16 -0
  65. package/src/components/layout/detached-pane-shell.tsx +6 -3
  66. package/src/components/layout/floating-pane.tsx +3 -2
  67. package/src/components/layout/pane/content.tsx +20 -8
  68. package/src/components/layout/pane/footer/registration.tsx +6 -2
  69. package/src/components/layout/pane/frame.tsx +4 -4
  70. package/src/components/layout/pane/header.tsx +40 -4
  71. package/src/components/layout/pane/index.tsx +3 -1
  72. package/src/components/layout/pane/sizing.test.ts +43 -0
  73. package/src/components/layout/pane/sizing.ts +13 -4
  74. package/src/components/layout/shell/active-drag.ts +5 -1
  75. package/src/components/layout/shell/cursor-occlusion.ts +196 -0
  76. package/src/components/layout/shell/drag/runtime.ts +28 -3
  77. package/src/components/layout/shell/fullscreen.ts +10 -0
  78. package/src/components/layout/shell/index.test.tsx +487 -4
  79. package/src/components/layout/shell/index.tsx +165 -4
  80. package/src/components/layout/shell/layout-state.test.ts +33 -0
  81. package/src/components/layout/shell/layout-state.ts +4 -9
  82. package/src/components/layout/shell/native/pointer-runtime.ts +10 -4
  83. package/src/components/layout/shell/pane/layers.tsx +34 -16
  84. package/src/components/layout/shell/pane/management-shortcuts.ts +19 -0
  85. package/src/components/layout/shell/shortcuts.ts +4 -0
  86. package/src/components/layout/shell/terminal-pointer-runtime.ts +29 -19
  87. package/src/components/layout/shell/visible-layout.ts +17 -0
  88. package/src/components/layout/shell/window-mode/index.ts +11 -3
  89. package/src/components/layout/shell/window-mode/overlays.tsx +8 -1
  90. package/src/components/layout/status-bar.test.tsx +111 -1
  91. package/src/components/layout/status-bar.tsx +37 -7
  92. package/src/components/layout/transient-layout.tsx +57 -0
  93. package/src/components/layout/window-edit/mode.test.ts +76 -5
  94. package/src/components/layout/window-edit/mode.ts +47 -2
  95. package/src/components/layout/window-edit/presentation.ts +2 -1
  96. package/src/components/onboarding/onboarding-steps.tsx +8 -28
  97. package/src/components/onboarding/onboarding-wizard.test.tsx +13 -12
  98. package/src/components/onboarding/onboarding-wizard.tsx +2 -4
  99. package/src/components/onboarding/portfolio-step/index.tsx +0 -12
  100. package/src/components/onboarding/portfolio-step/types.ts +1 -3
  101. package/src/components/onboarding/wizard-keyboard.ts +2 -5
  102. package/src/components/price-performance.tsx +9 -1
  103. package/src/components/table-view-shared.tsx +12 -4
  104. package/src/components/ui/data-table/opentui/index.tsx +25 -12
  105. package/src/components/ui/data-table/types.ts +4 -0
  106. package/src/components/ui/fields.tsx +10 -3
  107. package/src/components/ui/page-stack-view.tsx +6 -2
  108. package/src/components/ui/table-layout.test.ts +25 -0
  109. package/src/components/ui/table-layout.ts +22 -6
  110. package/src/components/ui/tabs.tsx +26 -10
  111. package/src/components/ui/ui.test.tsx +87 -0
  112. package/src/core/app-services.ts +7 -4
  113. package/src/core/state/app/layout-reducer.ts +2 -2
  114. package/src/data/config/store/index.ts +1 -2
  115. package/src/plugins/builtin/analytics/index.test.tsx +6 -2
  116. package/src/plugins/builtin/analytics/pane-model.ts +43 -8
  117. package/src/plugins/builtin/cloud-tweets/pane.tsx +2 -0
  118. package/src/plugins/builtin/cloud-tweets/search-bar.tsx +3 -0
  119. package/src/plugins/builtin/cloud-tweets/table.tsx +14 -2
  120. package/src/plugins/builtin/comparison-chart/index.test.tsx +130 -6
  121. package/src/plugins/builtin/kelly-sizer/constants.ts +3 -0
  122. package/src/plugins/builtin/kelly-sizer/fields.ts +214 -0
  123. package/src/plugins/builtin/kelly-sizer/index.test.tsx +197 -0
  124. package/src/plugins/builtin/kelly-sizer/index.tsx +47 -0
  125. package/src/plugins/builtin/kelly-sizer/model.test.ts +152 -0
  126. package/src/plugins/builtin/kelly-sizer/model.ts +580 -0
  127. package/src/plugins/builtin/kelly-sizer/pane.tsx +558 -0
  128. package/src/plugins/builtin/kelly-sizer/portfolio.ts +71 -0
  129. package/src/plugins/builtin/kelly-sizer/sections.tsx +142 -0
  130. package/src/plugins/builtin/kelly-sizer/state.ts +67 -0
  131. package/src/plugins/builtin/kelly-sizer/test-support.tsx +174 -0
  132. package/src/plugins/builtin/kelly-sizer/types.ts +156 -0
  133. package/src/plugins/builtin/kelly-sizer/view.test.tsx +77 -0
  134. package/src/plugins/builtin/kelly-sizer/view.tsx +375 -0
  135. package/src/plugins/builtin/layout-manager/index.tsx +5 -1
  136. package/src/plugins/builtin/news/wire/read-state.ts +9 -32
  137. package/src/plugins/builtin/options/view.test.tsx +141 -4
  138. package/src/plugins/builtin/options/view.tsx +76 -15
  139. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +26 -1
  140. package/src/plugins/builtin/portfolio-list/account-metrics.ts +14 -0
  141. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +23 -0
  142. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +19 -2
  143. package/src/plugins/builtin/portfolio-list/index.test.ts +23 -0
  144. package/src/plugins/builtin/portfolio-list/pane/index.tsx +3 -1
  145. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -7
  146. package/src/plugins/builtin/portfolio-list/summary/index.test.ts +59 -1
  147. package/src/plugins/builtin/portfolio-list/summary/index.tsx +36 -5
  148. package/src/plugins/builtin/shared/read-state.ts +43 -0
  149. package/src/plugins/builtin/substack/article-detail.test.tsx +10 -0
  150. package/src/plugins/builtin/substack/article-detail.tsx +39 -15
  151. package/src/plugins/builtin/substack/article-stack.tsx +6 -2
  152. package/src/plugins/builtin/substack/feed-tabs.tsx +12 -9
  153. package/src/plugins/builtin/substack/pane.tsx +8 -3
  154. package/src/plugins/builtin/substack/read-state.ts +55 -0
  155. package/src/plugins/builtin/thirteenf/pane.test.tsx +26 -0
  156. package/src/plugins/builtin/thirteenf/pane.tsx +12 -1
  157. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +149 -48
  158. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +25 -2
  159. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +5 -4
  160. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +16 -0
  161. package/src/plugins/builtin/ticker-detail/index.test.tsx +143 -0
  162. package/src/plugins/catalog-ui.ts +2 -0
  163. package/src/plugins/ibkr/broker-adapter.ts +19 -0
  164. package/src/plugins/ibkr/config.test.ts +31 -2
  165. package/src/plugins/ibkr/config.ts +9 -2
  166. package/src/plugins/ibkr/flex/index.test.ts +18 -9
  167. package/src/plugins/ibkr/flex/index.ts +30 -0
  168. package/src/plugins/ibkr/flex/statement-client.ts +68 -14
  169. package/src/plugins/ibkr/gateway/account-loaders.ts +1 -1
  170. package/src/plugins/ibkr/gateway/account-summary.ts +1 -0
  171. package/src/plugins/ibkr/gateway/service/index.test.ts +8 -0
  172. package/src/plugins/pane-manager/docking.ts +9 -2
  173. package/src/plugins/pane-manager/gridlock.ts +13 -5
  174. package/src/plugins/pane-manager/layout-state.ts +25 -0
  175. package/src/plugins/pane-manager/queries.ts +8 -2
  176. package/src/plugins/pane-manager.test.ts +33 -0
  177. package/src/plugins/pane-manager.ts +6 -1
  178. package/src/plugins/prediction-markets/chart.tsx +10 -4
  179. package/src/plugins/prediction-markets/controller/index.ts +2 -0
  180. package/src/plugins/prediction-markets/controller/keyboard.ts +34 -5
  181. package/src/plugins/prediction-markets/detail/book.tsx +141 -96
  182. package/src/plugins/prediction-markets/detail/overview.tsx +3 -0
  183. package/src/plugins/prediction-markets/detail/pane.tsx +71 -64
  184. package/src/plugins/prediction-markets/detail/shared.tsx +19 -0
  185. package/src/plugins/prediction-markets/detail/trades.tsx +64 -19
  186. package/src/plugins/prediction-markets/detail.test.tsx +50 -0
  187. package/src/plugins/prediction-markets/pane.test.tsx +75 -0
  188. package/src/plugins/prediction-markets/pane.tsx +71 -23
  189. package/src/plugins/prediction-markets/test-helpers.tsx +42 -23
  190. package/src/renderers/electrobun/bun/application-menu/click.test.ts +9 -0
  191. package/src/renderers/electrobun/bun/application-menu/click.ts +1 -0
  192. package/src/renderers/electrobun/bun/application-menu/index.test.ts +9 -0
  193. package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
  194. package/src/renderers/electrobun/bun/index.ts +13 -0
  195. package/src/renderers/electrobun/bun/window/focus.test.ts +35 -0
  196. package/src/renderers/electrobun/bun/window/focus.ts +5 -0
  197. package/src/renderers/electrobun/view/app-services.ts +6 -4
  198. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +13 -2
  199. package/src/renderers/electrobun/view/data-table/index.tsx +19 -4
  200. package/src/renderers/electrobun/view/data-table/row.tsx +30 -6
  201. package/src/renderers/electrobun/view/desktop/controls.tsx +11 -5
  202. package/src/renderers/electrobun/view/host/ascii-text.ts +50 -0
  203. package/src/renderers/electrobun/view/host/input.tsx +115 -18
  204. package/src/renderers/electrobun/view/host/tabs.tsx +7 -2
  205. package/src/renderers/electrobun/view/host/text.test.ts +22 -0
  206. package/src/renderers/electrobun/view/host/text.tsx +24 -20
  207. package/src/renderers/electrobun/view/styles.css +11 -0
  208. package/src/renderers/electrobun/view/ui-host.tsx +1 -1
  209. package/src/renderers/opentui/host.tsx +17 -1
  210. package/src/renderers/opentui/start.tsx +13 -5
  211. package/src/renderers/opentui/ui-host.tsx +57 -25
  212. package/src/sources/provider-router/batches.ts +16 -2
  213. package/src/sources/provider-router/financial-routes.ts +5 -0
  214. package/src/sources/provider-router/financials.ts +11 -23
  215. package/src/sources/provider-router/index.test.ts +189 -0
  216. package/src/sources/provider-router/primary.ts +3 -2
  217. package/src/sources/sec-edgar.test.ts +83 -0
  218. package/src/sources/sec-edgar.ts +231 -0
  219. package/src/sources/yahoo-finance.test.ts +51 -0
  220. package/src/sources/yahoo-finance.ts +49 -1
  221. package/src/state/app/context/index.test.tsx +5 -8
  222. package/src/state/app/input-capture.ts +12 -0
  223. package/src/types/broker.ts +1 -0
  224. package/src/types/config.ts +10 -0
  225. package/src/types/plugin.ts +28 -0
  226. package/src/types/trading.ts +2 -0
  227. package/src/ui/host.tsx +16 -0
  228. package/src/ui/index.tsx +2 -0
  229. package/src/utils/cli-output.ts +6 -0
  230. package/src/utils/financial-statements.ts +23 -0
  231. package/src/utils/search-focus-navigation.ts +24 -0
  232. package/src/version.ts +1 -1
  233. package/src/components/onboarding/portfolio-step/manual-name-panel.tsx +0 -62
package/README.md CHANGED
@@ -60,6 +60,7 @@ Open command mode with `Ctrl+P`, then type a command. Press `` ` `` to open tick
60
60
  | `TOP` | Ranked market stories |
61
61
  | `MOST` | Market movers |
62
62
  | `PF` | Portfolios and watchlists |
63
+ | `KELLY AAPL` | Position sizing |
63
64
  | `HELP` | Full in-app shortcut list |
64
65
 
65
66
  ## What It Does
@@ -70,22 +71,43 @@ Open command mode with `Ctrl+P`, then type a command. Press `` ` `` to open tick
70
71
 
71
72
  ## CLI
72
73
 
73
- Running `gloomberb` with no arguments launches the terminal UI. Use `gloomberb help` to see the full command list.
74
+ Running `gloomberb` with no arguments launches the terminal UI. Normal commands run through a headless CLI path; use `gloomberb launch-ui` when a script should explicitly open the UI.
75
+
76
+ Human-readable output is the default. Automation can opt into structured output with `--json`, `--csv`, or `--ndjson`. JSON output favors the richest fetched model available and includes display-column metadata when a command has table columns; CSV and NDJSON use the command's tabular row view. Common global flags include `--limit`, `--refresh`, `--quiet`, `--no-color`, `--dry-run`, and `--yes`.
74
77
 
75
78
  | Command | Use |
76
79
  |---------|-----|
77
80
  | `gloomberb` | Launch the terminal UI |
81
+ | `gloomberb launch-ui` | Explicitly launch the terminal UI |
78
82
  | `gloomberb help` | Show all CLI commands |
79
- | `gloomberb search <query>` | Search tickers and company names |
83
+ | `gloomberb api list|get|invoke|subscribe` | Inspect and call plugin capabilities directly |
84
+ | `gloomberb quote <symbols>` | Fetch current quotes |
85
+ | `gloomberb search <query>` / `provider-search <query>` | Search tickers and provider symbols |
80
86
  | `gloomberb ticker <symbol>` | Show quote, ownership, and financials |
87
+ | `gloomberb history|financials|fundamentals|options <symbol>` | Fetch research data |
88
+ | `gloomberb news|filings|holders|insider|13f|analyst|events|valuation <symbol>` | Fetch company research feeds |
89
+ | `gloomberb movers|indices|sectors|fx|fear-greed|earnings` | Fetch market overview data |
90
+ | `gloomberb econ|fred|yield-curve` | Fetch macro data |
91
+ | `gloomberb compare|correlation|relationship <symbols>` | Compare securities |
81
92
  | `gloomberb portfolio [action]` | Manage manual portfolios |
82
93
  | `gloomberb watchlist [action]` | Manage watchlists |
94
+ | `gloomberb notes|alerts [action]` | Manage local notes and alerts |
95
+ | `gloomberb broker|ibkr [action]` | Inspect broker integrations; trading actions require explicit account/profile and `--yes` |
96
+ | `gloomberb ai providers|ask|screen` | Use configured AI providers and screeners |
97
+ | `gloomberb rss fetch <url>` | Fetch an RSS feed |
98
+ | `gloomberb buildout|congress|substack|x-feed|tweets` | Access cloud and social data sources when an existing session is available |
99
+ | `gloomberb provider status` | Inspect enabled data providers |
100
+ | `gloomberb config|cache|plugin|layout|pane|debug|doctor|version|changelog` | Inspect and manage local app state |
101
+ | `gloomberb fn [...]` | Run a pane-backed report command |
102
+ | `gloomberb shot [...]` | Capture a pane-backed screenshot |
83
103
  | `gloomberb predictions [...]` | Launch Prediction Markets |
84
104
  | `gloomberb plugins` | List installed plugins |
85
105
  | `gloomberb install <user/repo>` | Install a plugin from GitHub |
86
106
  | `gloomberb remove <name>` | Remove an installed plugin |
87
107
  | `gloomberb update [name]` | Update plugins |
88
108
 
109
+ Commands that need a signed-in cloud session may return `auth_required`; sign-in, account management, and chat workflows are still handled in the app UI for now.
110
+
89
111
  ## Plugins
90
112
 
91
113
  Everything from the portfolio list to broker integrations is a plugin. Plugins can add panes, tabs, columns, command bar commands, CLI commands, status bar widgets, and data providers.
package/bin/gloomberb CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- import "../src/index.tsx"
3
+ import "../src/cli/entry.ts"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gloomberb",
3
- "version": "0.9.2",
3
+ "version": "0.9.4",
4
4
  "description": "Market research and portfolio tracker for the terminal",
5
5
  "module": "src/index.tsx",
6
6
  "type": "module",
@@ -54,7 +54,11 @@ export function useDesktopApplicationMenuRuntime({
54
54
  break;
55
55
  case "layout-gridlock": {
56
56
  const { width, height } = pluginRegistry.getTermSizeFn();
57
- pluginRegistry.updateLayoutFn(gridlockAllPanes(stateRef.current.config.layout, { x: 0, y: 0, width, height }));
57
+ pluginRegistry.updateLayoutFn(gridlockAllPanes(
58
+ stateRef.current.config.layout,
59
+ { x: 0, y: 0, width, height },
60
+ pluginRegistry.panes,
61
+ ));
58
62
  notifyGridlockComplete(pluginRegistry.notify.bind(pluginRegistry), () => {
59
63
  dispatch({ type: "UNDO_LAYOUT" });
60
64
  });
@@ -164,7 +164,10 @@ export function useTickerRefreshRuntime({
164
164
  dispatch({ type: "SET_REFRESHING", symbol, refreshing: true });
165
165
  }
166
166
  try {
167
- const entries = await marketData.loadSnapshotsBatch(instrumentEntries.map((entry) => entry.instrument));
167
+ const entries = await marketData.loadSnapshotsBatch(
168
+ instrumentEntries.map((entry) => entry.instrument),
169
+ { forceRefresh: true },
170
+ );
168
171
  entries.forEach((entry, index) => {
169
172
  const ticker = instrumentEntries[index]?.ticker;
170
173
  const data = entry.data ?? entry.lastGoodData;
package/src/app.tsx CHANGED
@@ -14,6 +14,7 @@ import { Header } from "./components/layout/header";
14
14
  import { StatusBar } from "./components/layout/status-bar";
15
15
  import { Shell } from "./components/layout/shell";
16
16
  import { DetachedPaneShell } from "./components/layout/detached-pane-shell";
17
+ import { TransientLayoutProvider } from "./components/layout/transient-layout";
17
18
  import { CommandBar } from "./components/command-bar/surface";
18
19
  import { OnboardingWizard } from "./components/onboarding/onboarding-wizard";
19
20
  import { useDialog } from "./ui/dialog";
@@ -59,7 +60,16 @@ interface AppInnerProps {
59
60
  function ThemedAppRoot({ children }: { children: ReactNode }) {
60
61
  const themeColors = useThemeColors();
61
62
  return (
62
- <Box flexDirection="column" flexGrow={1} backgroundColor={themeColors.bg}>
63
+ <Box
64
+ flexDirection="column"
65
+ flexGrow={1}
66
+ flexShrink={1}
67
+ flexBasis={0}
68
+ minWidth={0}
69
+ minHeight={0}
70
+ overflow="hidden"
71
+ backgroundColor={themeColors.bg}
72
+ >
63
73
  {children}
64
74
  </Box>
65
75
  );
@@ -293,13 +303,15 @@ function AppInner({
293
303
  <ContextMenuProvider pluginRegistry={pluginRegistry}>
294
304
  <ThemedAppRoot>
295
305
  <Header />
296
- <Shell
297
- pluginRegistry={pluginRegistry}
298
- desktopWindowBridge={desktopWindowBridge}
299
- desktopDockPreview={desktopDockPreview}
300
- commandBarNativeOccluder={commandBarNativeOccluder}
301
- />
302
- <StatusBar />
306
+ <TransientLayoutProvider>
307
+ <Shell
308
+ pluginRegistry={pluginRegistry}
309
+ desktopWindowBridge={desktopWindowBridge}
310
+ desktopDockPreview={desktopDockPreview}
311
+ commandBarNativeOccluder={commandBarNativeOccluder}
312
+ />
313
+ <StatusBar />
314
+ </TransientLayoutProvider>
303
315
  {state.commandBarOpen && (
304
316
  <CommandBar
305
317
  dataProvider={dataProvider}
@@ -124,7 +124,7 @@ export function loadPersistedBrokerAccountMap(
124
124
 
125
125
  for (const instance of brokerInstances) {
126
126
  const broker = brokers.get(instance.brokerType);
127
- if (!broker?.listAccounts) continue;
127
+ if (!broker?.listAccounts && !broker?.importPortfolioSnapshot) continue;
128
128
  const accounts = loadPersistedBrokerAccounts(resources, instance, broker);
129
129
  if (!accounts || accounts.length === 0) continue;
130
130
  accountMap[instance.id] = accounts;
@@ -2,6 +2,8 @@ import { describe, expect, test } from "bun:test";
2
2
  import { createDefaultConfig, type BrokerInstanceConfig } from "../types/config";
3
3
  import type { BrokerAdapter } from "../types/broker";
4
4
  import type { TickerRecord } from "../types/ticker";
5
+ import { AppPersistence } from "../data/app-persistence";
6
+ import { loadPersistedBrokerAccounts, persistBrokerAccounts } from "./account-cache";
5
7
  import {
6
8
  restoreBrokerPortfoliosFromTickerPositions,
7
9
  syncBrokerInstance,
@@ -164,6 +166,111 @@ describe("syncBrokerInstance", () => {
164
166
  ]);
165
167
  });
166
168
 
169
+ test("imports account and position data from one broker portfolio snapshot", async () => {
170
+ const instance = createBrokerInstance();
171
+ const config = {
172
+ ...createDefaultConfig("/tmp/gloomberb-sync-broker-snapshot"),
173
+ portfolios: [],
174
+ brokerInstances: [instance],
175
+ };
176
+ const tickerRepository = createTickerRepository();
177
+ const persistence = new AppPersistence(":memory:");
178
+ let listAccountsCalled = false;
179
+ let importPositionsCalled = false;
180
+ const broker: BrokerAdapter = {
181
+ ...createDemoBroker(),
182
+ listAccounts: async () => {
183
+ listAccountsCalled = true;
184
+ return [];
185
+ },
186
+ importPositions: async () => {
187
+ importPositionsCalled = true;
188
+ return [];
189
+ },
190
+ importPortfolioSnapshot: async () => ({
191
+ accounts: [{
192
+ accountId: "ACC-1",
193
+ name: "Snapshot Account",
194
+ currency: "USD",
195
+ netLiquidation: 125_000,
196
+ grossPositionValue: 175_000,
197
+ totalCashValue: -50_000,
198
+ }],
199
+ positions: [{
200
+ ticker: "AAPL",
201
+ exchange: "NASDAQ",
202
+ shares: 12,
203
+ avgCost: 180,
204
+ currency: "USD",
205
+ accountId: "ACC-1",
206
+ name: "Apple Inc.",
207
+ assetCategory: "STK",
208
+ }],
209
+ }),
210
+ };
211
+
212
+ try {
213
+ const result = await syncBrokerInstance({
214
+ config,
215
+ instanceId: "demo-broker",
216
+ brokers: new Map([["demo", broker]]),
217
+ tickerRepository: tickerRepository as any,
218
+ resources: persistence.resources,
219
+ });
220
+
221
+ expect(listAccountsCalled).toBe(false);
222
+ expect(importPositionsCalled).toBe(false);
223
+ expect(result.brokerAccounts[0]?.netLiquidation).toBe(125_000);
224
+ expect(result.positions).toHaveLength(1);
225
+ expect(loadPersistedBrokerAccounts(persistence.resources, instance, broker)).toEqual(result.brokerAccounts);
226
+ } finally {
227
+ persistence.close();
228
+ }
229
+ });
230
+
231
+ test("preserves the last account snapshot when a broker portfolio snapshot fails", async () => {
232
+ const instance = createBrokerInstance();
233
+ const config = {
234
+ ...createDefaultConfig("/tmp/gloomberb-sync-broker-snapshot-failure"),
235
+ portfolios: [],
236
+ brokerInstances: [instance],
237
+ };
238
+ const tickerRepository = createTickerRepository();
239
+ const persistence = new AppPersistence(":memory:");
240
+ const broker: BrokerAdapter = {
241
+ ...createDemoBroker(),
242
+ importPortfolioSnapshot: async () => {
243
+ throw new Error("account snapshot unavailable");
244
+ },
245
+ };
246
+
247
+ try {
248
+ persistBrokerAccounts(persistence.resources, instance, broker, [{
249
+ accountId: "ACC-1",
250
+ name: "Stale Account",
251
+ currency: "USD",
252
+ netLiquidation: 99_000,
253
+ }]);
254
+
255
+ await expect(syncBrokerInstance({
256
+ config,
257
+ instanceId: "demo-broker",
258
+ brokers: new Map([["demo", broker]]),
259
+ tickerRepository: tickerRepository as any,
260
+ resources: persistence.resources,
261
+ })).rejects.toThrow("account snapshot unavailable");
262
+
263
+ expect(loadPersistedBrokerAccounts(persistence.resources, instance, broker)).toEqual([{
264
+ accountId: "ACC-1",
265
+ name: "Stale Account",
266
+ currency: "USD",
267
+ netLiquidation: 99_000,
268
+ }]);
269
+ } finally {
270
+ persistence.close();
271
+ }
272
+ });
273
+
167
274
  test("preserves broker portfolios across sequential profile syncs", async () => {
168
275
  const config = {
169
276
  ...createDefaultConfig("/tmp/gloomberb-sync-broker-instances"),
@@ -131,17 +131,30 @@ export async function syncBrokerInstance({
131
131
  const tickers = await loadTickerMap(tickerRepository, existingTickers);
132
132
 
133
133
  let brokerAccounts: BrokerAccount[] = [];
134
- if (broker.listAccounts) {
135
- try {
136
- brokerAccounts = await broker.listAccounts(instance);
137
- if (resources) {
138
- try {
139
- persistBrokerAccounts(resources, instance, broker, brokerAccounts);
140
- } catch {}
134
+ let positions: BrokerPosition[];
135
+ if (broker.importPortfolioSnapshot) {
136
+ const snapshot = await broker.importPortfolioSnapshot(instance);
137
+ brokerAccounts = snapshot.accounts;
138
+ positions = snapshot.positions;
139
+ if (resources) {
140
+ try {
141
+ persistBrokerAccounts(resources, instance, broker, brokerAccounts);
142
+ } catch {}
143
+ }
144
+ } else {
145
+ if (broker.listAccounts) {
146
+ try {
147
+ brokerAccounts = await broker.listAccounts(instance);
148
+ if (resources) {
149
+ try {
150
+ persistBrokerAccounts(resources, instance, broker, brokerAccounts);
151
+ } catch {}
152
+ }
153
+ } catch (error) {
154
+ throw error;
141
155
  }
142
- } catch {
143
- brokerAccounts = [];
144
156
  }
157
+ positions = await broker.importPositions(instance);
145
158
  }
146
159
 
147
160
  const accountMetadata = new Map(
@@ -154,7 +167,6 @@ export async function syncBrokerInstance({
154
167
  ]),
155
168
  );
156
169
 
157
- const positions = await broker.importPositions(instance);
158
170
  const syncedAt = Date.now();
159
171
 
160
172
  let nextConfig = config;
@@ -58,6 +58,7 @@ export class CapabilityRegistry {
58
58
  id,
59
59
  kind: operation.kind,
60
60
  rendererSafe: operation.rendererSafe === true,
61
+ ...operation.cli,
61
62
  }));
62
63
  return {
63
64
  id: capability.id,
@@ -3,6 +3,7 @@ import type { AssetDataProvider } from "../types/data-provider";
3
3
  import type { NewsDataProvider } from "../types/capability-route-source";
4
4
 
5
5
  type CapabilityOperationKind = "read" | "query" | "action" | "stream";
6
+ type CapabilitySideEffectLevel = "none" | "local-write" | "network-write" | "external-trade" | "external-side-effect";
6
7
 
7
8
  type CapabilityKind =
8
9
  | "asset-data"
@@ -20,9 +21,22 @@ interface CapabilityHandlerContext {
20
21
 
21
22
  type CapabilityStreamEmit<T = unknown> = (event: T) => void;
22
23
 
24
+ export interface CapabilityOperationCliManifest {
25
+ summary?: string;
26
+ inputShape?: string;
27
+ outputShape?: string;
28
+ examples?: string[];
29
+ sideEffectLevel?: CapabilitySideEffectLevel;
30
+ requirements?: string[];
31
+ batch?: boolean;
32
+ formats?: Array<"text" | "json" | "csv" | "ndjson">;
33
+ safety?: string[];
34
+ }
35
+
23
36
  export interface CapabilityOperation<I = unknown, O = unknown, E = unknown> {
24
37
  kind: CapabilityOperationKind;
25
38
  rendererSafe?: boolean;
39
+ cli?: CapabilityOperationCliManifest;
26
40
  input?: CapabilitySchema<I>;
27
41
  output?: CapabilitySchema<O>;
28
42
  cachePolicy?: CachePolicyMap[keyof CachePolicyMap];
@@ -55,6 +69,15 @@ export interface CapabilityOperationManifest {
55
69
  id: string;
56
70
  kind: CapabilityOperationKind;
57
71
  rendererSafe: boolean;
72
+ summary?: string;
73
+ inputShape?: string;
74
+ outputShape?: string;
75
+ examples?: string[];
76
+ sideEffectLevel?: CapabilitySideEffectLevel;
77
+ requirements?: string[];
78
+ batch?: boolean;
79
+ formats?: Array<"text" | "json" | "csv" | "ndjson">;
80
+ safety?: string[];
58
81
  }
59
82
 
60
83
  export interface CapabilityManifest {
@@ -0,0 +1,119 @@
1
+ import type { CliCommandDef } from "../../types/plugin";
2
+ import { parseJsonPayload, requireArg, takeOption } from "./command-utils";
3
+
4
+ function splitOperationTarget(target: string): { capabilityId: string; operationId: string } {
5
+ const separator = target.lastIndexOf(".");
6
+ if (separator <= 0 || separator === target.length - 1) {
7
+ throw new Error("Use <capability-id>.<operation-id>.");
8
+ }
9
+ return {
10
+ capabilityId: target.slice(0, separator),
11
+ operationId: target.slice(separator + 1),
12
+ };
13
+ }
14
+
15
+ function parseOperationTarget(target: string, ctx: Parameters<CliCommandDef["execute"]>[1]) {
16
+ try {
17
+ return splitOperationTarget(target);
18
+ } catch (error) {
19
+ ctx.fail("Invalid capability operation target.", error instanceof Error ? error.message : String(error));
20
+ }
21
+ }
22
+
23
+ export const apiCliCommand: CliCommandDef = {
24
+ name: "api",
25
+ description: "List, inspect, invoke, or subscribe to plugin capabilities",
26
+ help: {
27
+ usage: [
28
+ "api list [--kind kind]",
29
+ "api get <capability-id>",
30
+ "api invoke <capability.operation> [json-payload]",
31
+ "api subscribe <capability.operation> [json-payload] [--limit n]",
32
+ ],
33
+ },
34
+ execute: async (rawArgs, ctx) => {
35
+ const action = rawArgs[0] ?? "list";
36
+ const args = rawArgs.slice(1);
37
+ const services = await ctx.initServices();
38
+
39
+ try {
40
+ if (action === "list") {
41
+ const kind = takeOption(args, "--kind");
42
+ const manifests = services.services.pluginRegistry.capabilities
43
+ .manifests()
44
+ .filter((manifest) => !kind || manifest.kind === kind);
45
+ ctx.printResult({ data: manifests }, {
46
+ rows: (data) => data.map((manifest) => ({
47
+ id: manifest.id,
48
+ kind: manifest.kind,
49
+ name: manifest.name,
50
+ operations: manifest.operations.map((operation) => operation.id).join(", "),
51
+ })),
52
+ columns: [
53
+ { key: "id", header: "Capability" },
54
+ { key: "kind", header: "Kind" },
55
+ { key: "name", header: "Name" },
56
+ { key: "operations", header: "Operations" },
57
+ ],
58
+ });
59
+ return;
60
+ }
61
+
62
+ if (action === "get") {
63
+ const capabilityId = requireArg(args[0], "Usage: gloomberb api get <capability-id>", ctx);
64
+ const manifest = services.services.pluginRegistry.capabilities
65
+ .manifests()
66
+ .find((entry) => entry.id === capabilityId);
67
+ if (!manifest) ctx.fail(`Capability "${capabilityId}" is not available.`);
68
+ ctx.printResult({ data: manifest });
69
+ return;
70
+ }
71
+
72
+ if (action === "invoke") {
73
+ const target = requireArg(args[0], "Usage: gloomberb api invoke <capability.operation> [json-payload]", ctx);
74
+ const operationTarget = parseOperationTarget(target, ctx);
75
+ const payload = parseJsonPayload(args[1], ctx);
76
+ const data = await services.services.pluginRegistry.capabilities.invoke(
77
+ operationTarget.capabilityId,
78
+ operationTarget.operationId,
79
+ payload,
80
+ );
81
+ ctx.printResult({ data });
82
+ return;
83
+ }
84
+
85
+ if (action === "subscribe") {
86
+ const target = requireArg(args[0], "Usage: gloomberb api subscribe <capability.operation> [json-payload] [--limit n]", ctx);
87
+ const operationTarget = parseOperationTarget(target, ctx);
88
+ const limit = ctx.cliOptions.limit ?? 10;
89
+ const payload = parseJsonPayload(args[1], ctx);
90
+ let count = 0;
91
+ let subscriptionId: string | null = null;
92
+ await new Promise<void>(async (resolve, reject) => {
93
+ try {
94
+ subscriptionId = await services.services.pluginRegistry.capabilities.subscribe(
95
+ operationTarget.capabilityId,
96
+ operationTarget.operationId,
97
+ payload,
98
+ (event) => {
99
+ count += 1;
100
+ ctx.printResult({ data: event });
101
+ if (count >= limit && subscriptionId) {
102
+ services.services.pluginRegistry.capabilities.unsubscribe(subscriptionId);
103
+ resolve();
104
+ }
105
+ },
106
+ );
107
+ } catch (error) {
108
+ reject(error);
109
+ }
110
+ });
111
+ return;
112
+ }
113
+
114
+ ctx.fail("Usage: gloomberb api list|get|invoke|subscribe");
115
+ } finally {
116
+ services.destroy();
117
+ }
118
+ },
119
+ };
@@ -0,0 +1,173 @@
1
+ import { detectProviders, getAiProvider } from "../../plugins/builtin/ai/providers";
2
+ import { runAiPrompt } from "../../plugins/builtin/ai/runner";
3
+ import { createRssNewsCapability } from "../../plugins/builtin/news/wire/rss/source";
4
+ import type { CliCommandDef } from "../../types/plugin";
5
+ import { requireArg, takeOption } from "./command-utils";
6
+
7
+ function deferredResult(command: string, code: "auth_required" | "not_implemented", message: string) {
8
+ return {
9
+ command,
10
+ status: code,
11
+ code,
12
+ message,
13
+ };
14
+ }
15
+
16
+ export const brokerCliCommand: CliCommandDef = {
17
+ name: "broker",
18
+ aliases: ["brokers"],
19
+ description: "Inspect broker profiles and guarded broker operations",
20
+ help: { usage: ["broker list", "broker status", "broker add|edit|remove|connect|sync"] },
21
+ execute: async (args, ctx) => {
22
+ const action = args[0] ?? "list";
23
+ const services = await ctx.initServices();
24
+ try {
25
+ if (action === "list" || action === "status") {
26
+ const rows = services.config.brokerInstances.map((instance) => ({
27
+ id: instance.id,
28
+ type: instance.brokerType,
29
+ label: instance.label,
30
+ enabled: instance.enabled !== false,
31
+ connectionMode: instance.connectionMode ?? "",
32
+ lastSyncedAt: instance.lastSyncedAt ? new Date(instance.lastSyncedAt).toISOString() : "",
33
+ }));
34
+ ctx.printResult({ data: rows });
35
+ return;
36
+ }
37
+
38
+ ctx.printResult({
39
+ data: deferredResult("broker", "not_implemented", "Broker mutation and connection commands need shared non-UI broker service extraction before they can safely run headless."),
40
+ });
41
+ } finally {
42
+ services.destroy();
43
+ }
44
+ },
45
+ };
46
+
47
+ export const ibkrCliCommand: CliCommandDef = {
48
+ name: "ibkr",
49
+ description: "Guarded IBKR account, order preview, and order action commands",
50
+ help: { usage: ["ibkr accounts|positions|orders|preview|place|cancel"] },
51
+ execute: async (args, ctx) => {
52
+ const action = args[0] ?? "status";
53
+ const profile = takeOption(args, "--profile");
54
+ const account = takeOption(args, "--account");
55
+ if ((action === "place" || action === "cancel") && (!profile || !account || !ctx.cliOptions.yes)) {
56
+ ctx.fail(`ibkr ${action} requires --profile, --account, and --yes.`);
57
+ }
58
+ const services = await ctx.initServices();
59
+ try {
60
+ const ibkrProfiles = services.config.brokerInstances.filter((instance) => instance.brokerType === "ibkr");
61
+ if (action === "accounts" || action === "status") {
62
+ ctx.printResult({ data: ibkrProfiles.map((instance) => ({
63
+ id: instance.id,
64
+ label: instance.label,
65
+ enabled: instance.enabled !== false,
66
+ connectionMode: instance.connectionMode ?? "",
67
+ lastSyncedAt: instance.lastSyncedAt ? new Date(instance.lastSyncedAt).toISOString() : "",
68
+ })) });
69
+ return;
70
+ }
71
+ ctx.printResult({
72
+ data: {
73
+ ...deferredResult("ibkr", "not_implemented", "IBKR trading commands are registered with safety gates, but headless order/account service extraction is still required."),
74
+ action,
75
+ profile: profile ?? null,
76
+ account: account ?? null,
77
+ dryRun: ctx.cliOptions.dryRun,
78
+ confirmed: ctx.cliOptions.yes,
79
+ },
80
+ });
81
+ } finally {
82
+ services.destroy();
83
+ }
84
+ },
85
+ };
86
+
87
+ export const aiCliCommand: CliCommandDef = {
88
+ name: "ai",
89
+ description: "Inspect AI providers and run guarded headless AI prompts",
90
+ help: { usage: ["ai providers", "ai ask [--provider id] <prompt>", "ai screen list|show|delete|refresh|export"] },
91
+ execute: async (args, ctx) => {
92
+ const action = args[0] ?? "providers";
93
+ if (action === "providers") {
94
+ ctx.printResult({ data: detectProviders().map((provider) => ({
95
+ id: provider.id,
96
+ name: provider.name,
97
+ command: provider.command,
98
+ available: provider.available,
99
+ })) });
100
+ return;
101
+ }
102
+ if (action === "ask") {
103
+ const rawArgs = args.slice(1);
104
+ const providerId = takeOption(rawArgs, "--provider") ?? detectProviders().find((provider) => provider.available)?.id;
105
+ const provider = getAiProvider(providerId);
106
+ if (!provider) ctx.fail("Unknown AI provider.");
107
+ const selectedProvider = provider;
108
+ if (!selectedProvider.available) ctx.fail(`${selectedProvider.name} is not installed or not available in PATH.`);
109
+ const prompt = rawArgs.join(" ").trim();
110
+ if (!prompt) ctx.fail("Usage: gloomberb ai ask [--provider id] <prompt>");
111
+ const controller = runAiPrompt({ provider: selectedProvider, prompt, cwd: process.cwd() });
112
+ const text = await controller.done;
113
+ ctx.printResult({ data: { provider: selectedProvider.id, text } });
114
+ return;
115
+ }
116
+ if (action === "screen") {
117
+ ctx.printResult({
118
+ data: deferredResult("ai screen", "not_implemented", "AI screener tabs are still pane-state backed; expose them through a shared non-UI screener store before headless mutation."),
119
+ });
120
+ return;
121
+ }
122
+ ctx.fail("Usage: gloomberb ai providers|ask|screen");
123
+ },
124
+ };
125
+
126
+ export const rssCliCommand: CliCommandDef = {
127
+ name: "rss",
128
+ description: "Fetch an RSS feed as news rows",
129
+ help: { usage: ["rss fetch <url> [--name label]"] },
130
+ execute: async (args, ctx) => {
131
+ const action = args[0] ?? "fetch";
132
+ if (action !== "fetch") ctx.fail("Usage: gloomberb rss fetch <url> [--name label]");
133
+ const rawArgs = args.slice(1);
134
+ const name = takeOption(rawArgs, "--name") ?? "RSS";
135
+ const url = requireArg(rawArgs[0], "Usage: gloomberb rss fetch <url> [--name label]", ctx);
136
+ const capability = createRssNewsCapability([{
137
+ id: "cli-feed",
138
+ url,
139
+ name,
140
+ category: "cli",
141
+ authority: 50,
142
+ enabled: true,
143
+ }]);
144
+ const articles = await capability.provider.fetchNews({ feed: "latest", limit: ctx.cliOptions.limit ?? 20 });
145
+ ctx.printResult({ data: articles.map((article) => ({
146
+ title: article.title,
147
+ source: article.source,
148
+ publishedAt: article.publishedAt.toISOString(),
149
+ url: article.url,
150
+ summary: article.summary ?? "",
151
+ })) });
152
+ },
153
+ };
154
+
155
+ function cloudCommand(name: string, description: string): CliCommandDef {
156
+ return {
157
+ name,
158
+ description,
159
+ execute: (_args, ctx) => {
160
+ ctx.printResult({
161
+ data: deferredResult(name, "auth_required", "This command needs an existing cloud/session token. New auth/account/chat CLI workflows are deferred in this pass."),
162
+ });
163
+ },
164
+ };
165
+ }
166
+
167
+ export const cloudCliCommands: CliCommandDef[] = [
168
+ cloudCommand("buildout", "Fetch Buildout company data through an existing cloud session"),
169
+ cloudCommand("congress", "Fetch congressional trade data through an existing cloud session"),
170
+ cloudCommand("substack", "Fetch Substack reader data through an existing session"),
171
+ cloudCommand("x-feed", "Fetch X/Twitter feed data through an existing cloud session"),
172
+ cloudCommand("tweets", "Alias for x-feed"),
173
+ ];