gloomberb 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (310) hide show
  1. package/README.md +20 -19
  2. package/bin/gloomberb +3 -0
  3. package/package.json +5 -4
  4. package/patches/@opentui%2Fcore@0.1.90.patch +356 -0
  5. package/src/app.tsx +1330 -0
  6. package/src/brokers/sync-broker-instance.test.ts +100 -0
  7. package/src/brokers/sync-broker-instance.ts +341 -0
  8. package/src/cli/commands/plugins.ts +173 -0
  9. package/src/cli/commands/portfolio.ts +210 -0
  10. package/src/cli/commands/search.ts +120 -0
  11. package/src/cli/commands/ticker.ts +375 -0
  12. package/src/cli/commands/watchlist.ts +210 -0
  13. package/src/cli/context.ts +62 -0
  14. package/src/cli/errors.ts +13 -0
  15. package/src/cli/helpers.ts +85 -0
  16. package/src/cli/index.ts +143 -0
  17. package/src/cli/ticker-resolution.ts +39 -0
  18. package/src/cli/types.ts +15 -0
  19. package/src/cli.test.ts +361 -0
  20. package/src/components/chart/chart-data.ts +239 -0
  21. package/src/components/chart/chart-renderer.ts +1199 -0
  22. package/src/components/chart/chart-types.ts +82 -0
  23. package/src/components/chart/chart-viewport.ts +21 -0
  24. package/src/components/chart/chart.test.ts +439 -0
  25. package/src/components/chart/comparison-chart-data.test.ts +109 -0
  26. package/src/components/chart/comparison-chart-data.ts +316 -0
  27. package/src/components/chart/comparison-chart-renderer.test.ts +84 -0
  28. package/src/components/chart/comparison-chart-renderer.ts +354 -0
  29. package/src/components/chart/comparison-stock-chart.tsx +1486 -0
  30. package/src/components/chart/cursor-motion.ts +48 -0
  31. package/src/components/chart/native/chart-rasterizer.ts +732 -0
  32. package/src/components/chart/native/kitty-adapter.ts +12 -0
  33. package/src/components/chart/native/kitty-manager.ts +142 -0
  34. package/src/components/chart/native/kitty-protocol.ts +110 -0
  35. package/src/components/chart/native/kitty-support.ts +71 -0
  36. package/src/components/chart/native/kitty.test.ts +620 -0
  37. package/src/components/chart/native/renderer-selection.ts +24 -0
  38. package/src/components/chart/native/surface-manager.ts +225 -0
  39. package/src/components/chart/native/surface-sync.ts +34 -0
  40. package/src/components/chart/stock-chart-renderer-switch.test.tsx +165 -0
  41. package/src/components/chart/stock-chart.test.ts +107 -0
  42. package/src/components/chart/stock-chart.tsx +1556 -0
  43. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +137 -0
  44. package/src/components/command-bar/command-bar.test.tsx +1524 -0
  45. package/src/components/command-bar/command-bar.tsx +4823 -0
  46. package/src/components/command-bar/command-registry.ts +261 -0
  47. package/src/components/command-bar/helpers.test.ts +224 -0
  48. package/src/components/command-bar/helpers.ts +371 -0
  49. package/src/components/command-bar/root-shortcuts.ts +158 -0
  50. package/src/components/command-bar/view-model.test.ts +152 -0
  51. package/src/components/command-bar/view-model.ts +185 -0
  52. package/src/components/command-bar/workflow-ops.test.ts +70 -0
  53. package/src/components/command-bar/workflow-ops.ts +433 -0
  54. package/src/components/command-bar/workflow-types.ts +127 -0
  55. package/src/components/detail-feed-view.test.tsx +105 -0
  56. package/src/components/detail-feed-view.tsx +295 -0
  57. package/src/components/index.ts +32 -0
  58. package/src/components/layout/floating-pane.tsx +49 -0
  59. package/src/components/layout/header.tsx +129 -0
  60. package/src/components/layout/pane-header.tsx +44 -0
  61. package/src/components/layout/pane.tsx +49 -0
  62. package/src/components/layout/pixel-mouse-mode.test.ts +122 -0
  63. package/src/components/layout/pixel-mouse.test.ts +40 -0
  64. package/src/components/layout/shell.test.tsx +625 -0
  65. package/src/components/layout/shell.tsx +1200 -0
  66. package/src/components/layout/status-bar.test.tsx +173 -0
  67. package/src/components/layout/status-bar.tsx +138 -0
  68. package/src/components/markdown-editor.tsx +134 -0
  69. package/src/components/onboarding/onboarding-wizard.test.tsx +184 -0
  70. package/src/components/onboarding/onboarding-wizard.tsx +1365 -0
  71. package/src/components/pane-settings-dialog.tsx +468 -0
  72. package/src/components/pane-template-wizard.test.tsx +52 -0
  73. package/src/components/pane-template-wizard.tsx +199 -0
  74. package/src/components/price-selector-dialog.tsx +195 -0
  75. package/src/components/spinner.tsx +2 -0
  76. package/src/components/tab-bar.tsx +17 -0
  77. package/src/components/ticker-badge-text.test.tsx +160 -0
  78. package/src/components/ticker-badge-text.tsx +58 -0
  79. package/src/components/ticker-badge.tsx +88 -0
  80. package/src/components/ticker-list-table.tsx +188 -0
  81. package/src/components/toggle-list.tsx +74 -0
  82. package/src/components/ui/button.tsx +77 -0
  83. package/src/components/ui/data-table.tsx +210 -0
  84. package/src/components/ui/external-link.tsx +22 -0
  85. package/src/components/ui/fields.tsx +187 -0
  86. package/src/components/ui/frame.tsx +73 -0
  87. package/src/components/ui/index.ts +53 -0
  88. package/src/components/ui/list-view.tsx +166 -0
  89. package/src/components/ui/loading.tsx +70 -0
  90. package/src/components/ui/status.tsx +83 -0
  91. package/src/components/ui/tabs.tsx +47 -0
  92. package/src/components/ui/toggle.tsx +164 -0
  93. package/src/components/ui/ui.test.tsx +134 -0
  94. package/src/data/app-persistence.ts +25 -0
  95. package/src/data/config-store.test.ts +310 -0
  96. package/src/data/config-store.ts +469 -0
  97. package/src/data/plugin-state-store.ts +102 -0
  98. package/src/data/resource-store.ts +323 -0
  99. package/src/data/session-store.ts +52 -0
  100. package/src/data/sqlite-cache.test.ts +103 -0
  101. package/src/data/sqlite-database.ts +62 -0
  102. package/src/data/sqlite-json.ts +19 -0
  103. package/src/data/ticker-repository.ts +45 -0
  104. package/src/data/ticker-store.ts +46 -0
  105. package/src/index.tsx +67 -0
  106. package/src/market-data/coordinator.test.ts +369 -0
  107. package/src/market-data/coordinator.ts +580 -0
  108. package/src/market-data/hooks.tsx +234 -0
  109. package/src/market-data/query-store.ts +28 -0
  110. package/src/market-data/request-types.test.ts +64 -0
  111. package/src/market-data/request-types.ts +70 -0
  112. package/src/market-data/result-types.ts +56 -0
  113. package/src/market-data/selectors.ts +166 -0
  114. package/src/market-data/trace.ts +7 -0
  115. package/src/pane-settings.test.ts +71 -0
  116. package/src/pane-settings.ts +73 -0
  117. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +338 -0
  118. package/src/plugins/builtin/ai/gloomberb-cli.ts +70 -0
  119. package/src/plugins/builtin/ai/index.test.tsx +139 -0
  120. package/src/plugins/builtin/ai/index.tsx +86 -0
  121. package/src/plugins/builtin/ai/providers.ts +99 -0
  122. package/src/plugins/builtin/ai/runner.ts +82 -0
  123. package/src/plugins/builtin/ai/screener-contract.test.ts +117 -0
  124. package/src/plugins/builtin/ai/screener-contract.ts +174 -0
  125. package/src/plugins/builtin/ai/screener-pane.test.tsx +365 -0
  126. package/src/plugins/builtin/ai/screener-pane.tsx +1161 -0
  127. package/src/plugins/builtin/ai/settings.ts +79 -0
  128. package/src/plugins/builtin/ai/ticker-context.ts +64 -0
  129. package/src/plugins/builtin/ai/utils.ts +71 -0
  130. package/src/plugins/builtin/ask-ai.test.tsx +258 -0
  131. package/src/plugins/builtin/ask-ai.tsx +9 -0
  132. package/src/plugins/builtin/chat-controller.test.ts +217 -0
  133. package/src/plugins/builtin/chat-controller.ts +340 -0
  134. package/src/plugins/builtin/chat.test.tsx +451 -0
  135. package/src/plugins/builtin/chat.tsx +620 -0
  136. package/src/plugins/builtin/comparison-chart.test.tsx +277 -0
  137. package/src/plugins/builtin/comparison-chart.tsx +169 -0
  138. package/src/plugins/builtin/data-providers.ts +16 -0
  139. package/src/plugins/builtin/debug.tsx +351 -0
  140. package/src/plugins/builtin/help.test.tsx +110 -0
  141. package/src/plugins/builtin/help.tsx +281 -0
  142. package/src/plugins/builtin/layout-manager.tsx +264 -0
  143. package/src/plugins/builtin/manual-entry.tsx +25 -0
  144. package/src/plugins/builtin/news.tsx +143 -0
  145. package/src/plugins/builtin/notes-files.ts +58 -0
  146. package/src/plugins/builtin/notes.tsx +396 -0
  147. package/src/plugins/builtin/options-availability.test.tsx +179 -0
  148. package/src/plugins/builtin/options-availability.ts +158 -0
  149. package/src/plugins/builtin/options.tsx +364 -0
  150. package/src/plugins/builtin/portfolio-list/header.tsx +227 -0
  151. package/src/plugins/builtin/portfolio-list/index.tsx +99 -0
  152. package/src/plugins/builtin/portfolio-list/metrics.ts +436 -0
  153. package/src/plugins/builtin/portfolio-list/pane.tsx +533 -0
  154. package/src/plugins/builtin/portfolio-list/settings.ts +280 -0
  155. package/src/plugins/builtin/portfolio-list/summary.tsx +270 -0
  156. package/src/plugins/builtin/portfolio-list/table.tsx +64 -0
  157. package/src/plugins/builtin/portfolio-list.test.ts +74 -0
  158. package/src/plugins/builtin/portfolio-list.test.tsx +1164 -0
  159. package/src/plugins/builtin/portfolio-metrics.test.ts +153 -0
  160. package/src/plugins/builtin/quote-monitor.test.tsx +114 -0
  161. package/src/plugins/builtin/sec.tsx +250 -0
  162. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +43 -0
  163. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +390 -0
  164. package/src/plugins/builtin/ticker-detail/index.tsx +80 -0
  165. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +242 -0
  166. package/src/plugins/builtin/ticker-detail/pane.tsx +183 -0
  167. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +83 -0
  168. package/src/plugins/builtin/ticker-detail/settings.ts +132 -0
  169. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +193 -0
  170. package/src/plugins/builtin/ticker-detail.test.tsx +578 -0
  171. package/src/plugins/builtin/yahoo.ts +18 -0
  172. package/src/plugins/event-bus.ts +37 -0
  173. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +35 -0
  174. package/src/plugins/ibkr/account-cache.test.ts +101 -0
  175. package/src/plugins/ibkr/account-cache.ts +137 -0
  176. package/src/plugins/ibkr/broker-adapter.ts +168 -0
  177. package/src/plugins/ibkr/config.test.ts +68 -0
  178. package/src/plugins/ibkr/config.ts +262 -0
  179. package/src/plugins/ibkr/dialogs.tsx +83 -0
  180. package/src/plugins/ibkr/flex.test.ts +107 -0
  181. package/src/plugins/ibkr/flex.ts +267 -0
  182. package/src/plugins/ibkr/fundamental-parser.ts +149 -0
  183. package/src/plugins/ibkr/gateway-helpers.ts +28 -0
  184. package/src/plugins/ibkr/gateway-selection.ts +51 -0
  185. package/src/plugins/ibkr/gateway-service.test.ts +450 -0
  186. package/src/plugins/ibkr/gateway-service.ts +2228 -0
  187. package/src/plugins/ibkr/index.test.tsx +183 -0
  188. package/src/plugins/ibkr/index.tsx +153 -0
  189. package/src/plugins/ibkr/instance-selection.test.ts +121 -0
  190. package/src/plugins/ibkr/instance-selection.ts +36 -0
  191. package/src/plugins/ibkr/price-normalization.test.ts +38 -0
  192. package/src/plugins/ibkr/price-normalization.ts +59 -0
  193. package/src/plugins/ibkr/trade-badge.tsx +27 -0
  194. package/src/plugins/ibkr/trade-tab.test.tsx +225 -0
  195. package/src/plugins/ibkr/trade-tab.tsx +1050 -0
  196. package/src/plugins/ibkr/trade-utils.test.ts +175 -0
  197. package/src/plugins/ibkr/trade-utils.ts +149 -0
  198. package/src/plugins/ibkr/trading-pane.tsx +438 -0
  199. package/src/plugins/ibkr/trading-state.test.ts +109 -0
  200. package/src/plugins/ibkr/trading-state.ts +339 -0
  201. package/src/plugins/loader.ts +71 -0
  202. package/src/plugins/pane-manager.test.ts +103 -0
  203. package/src/plugins/pane-manager.ts +974 -0
  204. package/src/plugins/plugin-persistence.ts +85 -0
  205. package/src/plugins/plugin-runtime.test.tsx +185 -0
  206. package/src/plugins/plugin-runtime.tsx +175 -0
  207. package/src/plugins/prediction-markets/cache.ts +114 -0
  208. package/src/plugins/prediction-markets/categories.ts +223 -0
  209. package/src/plugins/prediction-markets/chart.tsx +512 -0
  210. package/src/plugins/prediction-markets/columns.ts +133 -0
  211. package/src/plugins/prediction-markets/controller-data.ts +614 -0
  212. package/src/plugins/prediction-markets/controller.ts +725 -0
  213. package/src/plugins/prediction-markets/detail/book.tsx +118 -0
  214. package/src/plugins/prediction-markets/detail/outcomes.tsx +102 -0
  215. package/src/plugins/prediction-markets/detail/overview.tsx +73 -0
  216. package/src/plugins/prediction-markets/detail/pane.tsx +308 -0
  217. package/src/plugins/prediction-markets/detail/rules.tsx +16 -0
  218. package/src/plugins/prediction-markets/detail/shared.tsx +29 -0
  219. package/src/plugins/prediction-markets/detail/trades.tsx +30 -0
  220. package/src/plugins/prediction-markets/detail.test.tsx +47 -0
  221. package/src/plugins/prediction-markets/index.tsx +102 -0
  222. package/src/plugins/prediction-markets/keyboard.ts +64 -0
  223. package/src/plugins/prediction-markets/launch.test.ts +126 -0
  224. package/src/plugins/prediction-markets/launch.ts +240 -0
  225. package/src/plugins/prediction-markets/metrics.ts +311 -0
  226. package/src/plugins/prediction-markets/navigation.ts +90 -0
  227. package/src/plugins/prediction-markets/outcome-order.ts +19 -0
  228. package/src/plugins/prediction-markets/pane.test.tsx +425 -0
  229. package/src/plugins/prediction-markets/pane.tsx +294 -0
  230. package/src/plugins/prediction-markets/plugin.test.tsx +570 -0
  231. package/src/plugins/prediction-markets/rows.ts +229 -0
  232. package/src/plugins/prediction-markets/services/fetch.ts +97 -0
  233. package/src/plugins/prediction-markets/services/kalshi-adapter.ts +555 -0
  234. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +660 -0
  235. package/src/plugins/prediction-markets/services/polymarket-ws.ts +108 -0
  236. package/src/plugins/prediction-markets/settings.ts +164 -0
  237. package/src/plugins/prediction-markets/status.ts +24 -0
  238. package/src/plugins/prediction-markets/table.tsx +84 -0
  239. package/src/plugins/prediction-markets/test-helpers.tsx +636 -0
  240. package/src/plugins/prediction-markets/types.ts +186 -0
  241. package/src/plugins/registry.ts +633 -0
  242. package/src/sources/exchange-rates.ts +18 -0
  243. package/src/sources/gloomberb-cloud.test.ts +199 -0
  244. package/src/sources/gloomberb-cloud.ts +326 -0
  245. package/src/sources/provider-errors.ts +19 -0
  246. package/src/sources/provider-router.test.ts +1476 -0
  247. package/src/sources/provider-router.ts +1405 -0
  248. package/src/sources/sec-edgar.test.ts +286 -0
  249. package/src/sources/sec-edgar.ts +526 -0
  250. package/src/sources/yahoo-finance.test.ts +14 -0
  251. package/src/sources/yahoo-finance.ts +1017 -0
  252. package/src/state/app-activity.test.ts +41 -0
  253. package/src/state/app-activity.ts +106 -0
  254. package/src/state/app-bootstrap.test.ts +471 -0
  255. package/src/state/app-bootstrap.ts +308 -0
  256. package/src/state/app-context.test.ts +406 -0
  257. package/src/state/app-context.test.tsx +132 -0
  258. package/src/state/app-context.tsx +969 -0
  259. package/src/state/selectors.ts +67 -0
  260. package/src/state/session-persistence.test.ts +128 -0
  261. package/src/state/session-persistence.ts +161 -0
  262. package/src/state/ticker-refresh-queue.test.ts +46 -0
  263. package/src/state/ticker-refresh-queue.ts +40 -0
  264. package/src/state/use-inline-tickers.ts +174 -0
  265. package/src/state/use-quote-streaming.test.ts +37 -0
  266. package/src/state/use-quote-streaming.test.tsx +70 -0
  267. package/src/state/use-quote-streaming.ts +80 -0
  268. package/src/state/use-session-snapshot.ts +84 -0
  269. package/src/test-support/data-provider.ts +22 -0
  270. package/src/theme/color-utils.ts +67 -0
  271. package/src/theme/colors.ts +128 -0
  272. package/src/theme/themes.test.ts +45 -0
  273. package/src/theme/themes.ts +491 -0
  274. package/src/types/broker.ts +105 -0
  275. package/src/types/config.ts +434 -0
  276. package/src/types/data-provider.ts +81 -0
  277. package/src/types/financials.ts +142 -0
  278. package/src/types/instrument.ts +29 -0
  279. package/src/types/persistence.ts +21 -0
  280. package/src/types/plugin.ts +354 -0
  281. package/src/types/ticker.ts +58 -0
  282. package/src/types/trading.ts +85 -0
  283. package/src/updater.test.ts +162 -0
  284. package/src/updater.ts +247 -0
  285. package/src/utils/api-client.ts +666 -0
  286. package/src/utils/ascii-chart.ts +172 -0
  287. package/src/utils/broker-instances.ts +47 -0
  288. package/src/utils/cli-output.ts +83 -0
  289. package/src/utils/currency-units.test.ts +49 -0
  290. package/src/utils/currency-units.ts +82 -0
  291. package/src/utils/debug-log.ts +133 -0
  292. package/src/utils/exchange-rate-map.ts +8 -0
  293. package/src/utils/format.ts +158 -0
  294. package/src/utils/fuzzy-search.ts +34 -0
  295. package/src/utils/market-status.ts +95 -0
  296. package/src/utils/options.ts +86 -0
  297. package/src/utils/price-history.ts +39 -0
  298. package/src/utils/quote-time.test.ts +22 -0
  299. package/src/utils/quote-time.ts +50 -0
  300. package/src/utils/sec.test.ts +55 -0
  301. package/src/utils/sec.ts +45 -0
  302. package/src/utils/ticker-list.test.ts +22 -0
  303. package/src/utils/ticker-list.ts +35 -0
  304. package/src/utils/ticker-metadata.ts +51 -0
  305. package/src/utils/ticker-search.test.ts +227 -0
  306. package/src/utils/ticker-search.ts +764 -0
  307. package/src/utils/ticker-tokenizer.test.ts +48 -0
  308. package/src/utils/ticker-tokenizer.ts +72 -0
  309. package/src/version.ts +1 -0
  310. package/install.mjs +0 -68
@@ -0,0 +1,85 @@
1
+ import {
2
+ formatCompact,
3
+ formatCurrency,
4
+ formatNumber,
5
+ formatPercentRaw,
6
+ } from "../utils/format";
7
+ import type { AppConfig } from "../types/config";
8
+ import type { Watchlist, TickerRecord } from "../types/ticker";
9
+
10
+ export function slugifyName(name: string, fallbackPrefix: string): string {
11
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || `${fallbackPrefix}-${Date.now()}`;
12
+ }
13
+
14
+ export function formatSignedCurrency(value: number, currency: string): string {
15
+ return value > 0 ? `+${formatCurrency(value, currency)}` : formatCurrency(value, currency);
16
+ }
17
+
18
+ export function formatSignedPercentRaw(value: number): string {
19
+ return formatPercentRaw(value);
20
+ }
21
+
22
+ export function formatTimestamp(timestamp: number | undefined): string {
23
+ if (!timestamp) return "—";
24
+ return new Date(timestamp).toLocaleString("en-US", {
25
+ month: "short",
26
+ day: "numeric",
27
+ year: "numeric",
28
+ hour: "numeric",
29
+ minute: "2-digit",
30
+ });
31
+ }
32
+
33
+ export function formatNullableCompact(value: number | undefined): string {
34
+ return value == null ? "—" : formatCompact(value);
35
+ }
36
+
37
+ export function formatStatementValue(value: number | undefined, kind: "compact" | "eps" = "compact"): string {
38
+ if (value == null) return "—";
39
+ return kind === "eps" ? formatNumber(value, 2) : formatCompact(value);
40
+ }
41
+
42
+ export function formatBidAsk(
43
+ bid: number | undefined,
44
+ ask: number | undefined,
45
+ bidSize: number | undefined,
46
+ askSize: number | undefined,
47
+ currency: string,
48
+ ): string {
49
+ if (bid == null && ask == null) return "—";
50
+ const bidText = bid != null
51
+ ? `${formatCurrency(bid, currency)}${bidSize != null ? ` x ${formatNumber(bidSize, 0)}` : ""}`
52
+ : "—";
53
+ const askText = ask != null
54
+ ? `${formatCurrency(ask, currency)}${askSize != null ? ` x ${formatNumber(askSize, 0)}` : ""}`
55
+ : "—";
56
+ return `${bidText} / ${askText}`;
57
+ }
58
+
59
+ export function formatWatchlistNames(config: AppConfig, watchlistIds: string[]): string[] {
60
+ return watchlistIds
61
+ .map((id) => config.watchlists.find((watchlist) => watchlist.id === id)?.name)
62
+ .filter((name): name is string => !!name);
63
+ }
64
+
65
+ export function formatPortfolioNames(config: AppConfig, portfolioIds: string[]): string[] {
66
+ return portfolioIds
67
+ .map((id) => config.portfolios.find((portfolio) => portfolio.id === id)?.name)
68
+ .filter((name): name is string => !!name);
69
+ }
70
+
71
+ export function countCollectionTickers(
72
+ tickers: TickerRecord[],
73
+ field: "portfolios" | "watchlists",
74
+ id: string,
75
+ ): number {
76
+ return tickers.filter((ticker) => ticker.metadata[field].includes(id)).length;
77
+ }
78
+
79
+ export function findWatchlist(config: AppConfig, rawName: string): Watchlist | null {
80
+ const normalized = rawName.trim().toLowerCase();
81
+ if (!normalized) return null;
82
+ return config.watchlists.find((watchlist) =>
83
+ watchlist.id.toLowerCase() === normalized || watchlist.name.toLowerCase() === normalized
84
+ ) ?? null;
85
+ }
@@ -0,0 +1,143 @@
1
+ import { VERSION } from "../version";
2
+ import {
3
+ cliStyles,
4
+ renderSection,
5
+ renderTable,
6
+ } from "../utils/cli-output";
7
+ import { fail } from "./errors";
8
+ import { portfolio } from "./commands/portfolio";
9
+ import { watchlist } from "./commands/watchlist";
10
+ import { search, searchCandidatesForCli, buildSearchReport } from "./commands/search";
11
+ import { ticker, buildTickerReport } from "./commands/ticker";
12
+ import {
13
+ installPlugin,
14
+ listPlugins,
15
+ removePlugin,
16
+ updatePlugins,
17
+ } from "./commands/plugins";
18
+
19
+ function help() {
20
+ console.log(`${cliStyles.bold(`gloomberb v${VERSION}`)}\n${cliStyles.muted("Bloomberg-style portfolio tracker for the terminal")}`);
21
+ console.log("");
22
+ console.log(renderSection("Usage"));
23
+ console.log("gloomberb [command]");
24
+ console.log("");
25
+ console.log(renderSection("Commands"));
26
+ console.log(renderTable(
27
+ [
28
+ { header: "Command" },
29
+ { header: "Description" },
30
+ ],
31
+ [
32
+ ["(no command)", "Launch the terminal UI"],
33
+ ["predictions [...]", "Launch the UI with Prediction Markets focused"],
34
+ ["help", "Show this help message"],
35
+ ["portfolio [name]", "List collections or show a portfolio/watchlist"],
36
+ ["watchlist [action]", "List, create, delete, add, or remove watchlists"],
37
+ ["search <query>", "Search tickers and company names"],
38
+ ["ticker <symbol>", "Show quote, ownership, and detailed financials"],
39
+ ["install <user/repo>", "Install a plugin from GitHub"],
40
+ ["remove <name>", "Remove an installed plugin"],
41
+ ["update [name]", "Update plugins"],
42
+ ["plugins", "List installed plugins"],
43
+ ],
44
+ ));
45
+ console.log("");
46
+ console.log(renderSection("Prediction Launch"));
47
+ console.log(renderTable(
48
+ [
49
+ { header: "Input" },
50
+ { header: "Example" },
51
+ ],
52
+ [
53
+ ["gloomberb predictions [venue] [category] [browse-tab] [search...]", "gloomberb predictions world"],
54
+ ["venue", "all | polymarket | kalshi"],
55
+ ["category", "all | politics | world | macro | crypto | science | sports | entertainment | climate | social"],
56
+ ["browse-tab", "top | ending | new | watchlist"],
57
+ ],
58
+ ));
59
+ console.log("");
60
+ console.log(renderSection("Watchlist Actions"));
61
+ console.log(renderTable(
62
+ [
63
+ { header: "Action" },
64
+ { header: "Example" },
65
+ ],
66
+ [
67
+ ["list", "gloomberb watchlist list"],
68
+ ["show", "gloomberb watchlist show Growth"],
69
+ ["create", "gloomberb watchlist create Growth"],
70
+ ["delete", "gloomberb watchlist delete Growth"],
71
+ ["add", "gloomberb watchlist add Growth NVDA"],
72
+ ["remove", "gloomberb watchlist remove Growth NVDA"],
73
+ ],
74
+ ));
75
+ }
76
+
77
+ export { buildSearchReport, buildTickerReport, searchCandidatesForCli };
78
+
79
+ export async function runCli(args: string[]): Promise<boolean> {
80
+ const command = args[0];
81
+
82
+ switch (command) {
83
+ case "help":
84
+ case "--help":
85
+ case "-h": {
86
+ help();
87
+ return true;
88
+ }
89
+ case "portfolio": {
90
+ await portfolio(args.slice(1).join(" ") || undefined);
91
+ return true;
92
+ }
93
+ case "watchlist":
94
+ case "watchlists": {
95
+ await watchlist(args.slice(1));
96
+ return true;
97
+ }
98
+ case "search": {
99
+ const query = args.slice(1).join(" ");
100
+ if (!query) {
101
+ fail("Usage: gloomberb search <query>");
102
+ }
103
+ await search(query);
104
+ return true;
105
+ }
106
+ case "ticker": {
107
+ const symbol = args[1];
108
+ if (!symbol) {
109
+ fail("Usage: gloomberb ticker <symbol>");
110
+ }
111
+ await ticker(symbol);
112
+ return true;
113
+ }
114
+ case "install": {
115
+ const ref = args[1];
116
+ if (!ref) {
117
+ fail("Usage: gloomberb install <github-user/repo>");
118
+ }
119
+ await installPlugin(ref);
120
+ return true;
121
+ }
122
+ case "remove":
123
+ case "uninstall": {
124
+ const name = args[1];
125
+ if (!name) {
126
+ fail("Usage: gloomberb remove <plugin-name>");
127
+ }
128
+ await removePlugin(name);
129
+ return true;
130
+ }
131
+ case "update": {
132
+ await updatePlugins(args[1]);
133
+ return true;
134
+ }
135
+ case "plugins":
136
+ case "list": {
137
+ listPlugins();
138
+ return true;
139
+ }
140
+ default:
141
+ return false;
142
+ }
143
+ }
@@ -0,0 +1,39 @@
1
+ import type { DataProvider } from "../types/data-provider";
2
+ import type { TickerRecord } from "../types/ticker";
3
+ import type { TickerRepository } from "../data/ticker-repository";
4
+ import { resolveTickerSearch, upsertTickerFromSearchResult } from "../utils/ticker-search";
5
+
6
+ export async function resolveTickerForCli(
7
+ symbol: string,
8
+ store: TickerRepository,
9
+ dataProvider: DataProvider,
10
+ ): Promise<TickerRecord> {
11
+ const normalized = symbol.trim().toUpperCase();
12
+ if (!normalized) {
13
+ throw new Error("Ticker symbol is required.");
14
+ }
15
+
16
+ const localTicker = await store.loadTicker(normalized);
17
+ if (localTicker) return localTicker;
18
+
19
+ const localTickers = new Map(
20
+ (await store.loadAllTickers()).map((ticker) => [ticker.metadata.ticker.toUpperCase(), ticker] as const),
21
+ );
22
+ const resolved = await resolveTickerSearch({
23
+ query: normalized,
24
+ activeTicker: null,
25
+ tickers: localTickers,
26
+ dataProvider,
27
+ });
28
+
29
+ if (!resolved) {
30
+ throw new Error(`No ticker match found for "${normalized}".`);
31
+ }
32
+
33
+ if (resolved.kind === "local") {
34
+ return resolved.ticker;
35
+ }
36
+
37
+ const { ticker } = await upsertTickerFromSearchResult(store, resolved.result);
38
+ return ticker;
39
+ }
@@ -0,0 +1,15 @@
1
+ import type { AppConfig } from "../types/config";
2
+ import { AppPersistence } from "../data/app-persistence";
3
+ import { TickerRepository } from "../data/ticker-repository";
4
+ import { ProviderRouter } from "../sources/provider-router";
5
+
6
+ export type ConfigContext = {
7
+ config: AppConfig;
8
+ persistence: AppPersistence;
9
+ store: TickerRepository;
10
+ dataDir: string;
11
+ };
12
+
13
+ export type MarketContext = ConfigContext & {
14
+ dataProvider: ProviderRouter;
15
+ };
@@ -0,0 +1,361 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { mkdtemp, mkdir, rm, writeFile } from "fs/promises";
3
+ import { join } from "path";
4
+ import { tmpdir } from "os";
5
+ import { AppPersistence } from "./data/app-persistence";
6
+ import { loadConfig, saveConfig } from "./data/config-store";
7
+ import {
8
+ buildSearchReport,
9
+ buildTickerReport,
10
+ runCli,
11
+ searchCandidatesForCli,
12
+ } from "./cli/index";
13
+ import { createDefaultConfig } from "./types/config";
14
+ import { TickerRepository } from "./data/ticker-repository";
15
+ import type { TickerFinancials } from "./types/financials";
16
+ import type { NewsItem, SecFilingItem } from "./types/data-provider";
17
+ import type { TickerRecord } from "./types/ticker";
18
+ import { createTestDataProvider } from "./test-support/data-provider";
19
+
20
+ const tempDirs: string[] = [];
21
+ const originalHome = process.env.HOME;
22
+
23
+ afterEach(async () => {
24
+ process.env.HOME = originalHome;
25
+ await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
26
+ });
27
+
28
+ async function createTempDir(prefix: string): Promise<string> {
29
+ const dir = await mkdtemp(join(tmpdir(), prefix));
30
+ tempDirs.push(dir);
31
+ return dir;
32
+ }
33
+
34
+ async function createCliFixture({
35
+ watchlists,
36
+ tickers = [],
37
+ }: {
38
+ watchlists: Array<{ id: string; name: string }>;
39
+ tickers?: TickerRecord[];
40
+ }) {
41
+ const homeDir = await createTempDir("gloomberb-cli-home-");
42
+ const dataDir = await createTempDir("gloomberb-cli-data-");
43
+ process.env.HOME = homeDir;
44
+
45
+ await mkdir(join(homeDir, ".gloomberb"), { recursive: true });
46
+ await writeFile(join(homeDir, ".gloomberb", "config.json"), JSON.stringify({ dataDir }), "utf-8");
47
+
48
+ const config = createDefaultConfig(dataDir);
49
+ config.watchlists = watchlists;
50
+ await saveConfig(config);
51
+
52
+ const persistence = new AppPersistence(join(dataDir, ".gloomberb-cache.db"));
53
+ const store = new TickerRepository(persistence.tickers);
54
+ for (const ticker of tickers) {
55
+ await store.saveTicker(ticker);
56
+ }
57
+ persistence.close();
58
+
59
+ return { dataDir };
60
+ }
61
+
62
+ async function captureConsole<T>(fn: () => Promise<T> | T): Promise<{ result: T; stdout: string; stderr: string }> {
63
+ const logs: string[] = [];
64
+ const errors: string[] = [];
65
+ const originalLog = console.log;
66
+ const originalError = console.error;
67
+
68
+ console.log = (...args: unknown[]) => {
69
+ logs.push(args.map(String).join(" "));
70
+ };
71
+ console.error = (...args: unknown[]) => {
72
+ errors.push(args.map(String).join(" "));
73
+ };
74
+
75
+ try {
76
+ const result = await fn();
77
+ return {
78
+ result,
79
+ stdout: logs.join("\n"),
80
+ stderr: errors.join("\n"),
81
+ };
82
+ } finally {
83
+ console.log = originalLog;
84
+ console.error = originalError;
85
+ }
86
+ }
87
+
88
+ function makeTicker(overrides: Partial<TickerRecord["metadata"]> = {}): TickerRecord {
89
+ return {
90
+ metadata: {
91
+ ticker: "NVDA",
92
+ exchange: "NASDAQ",
93
+ currency: "USD",
94
+ name: "NVIDIA Corporation",
95
+ portfolios: [],
96
+ watchlists: [],
97
+ positions: [],
98
+ custom: {},
99
+ tags: [],
100
+ ...overrides,
101
+ },
102
+ };
103
+ }
104
+
105
+ describe("CLI watchlist commands", () => {
106
+ test("help lists the prediction markets launcher", async () => {
107
+ const { result, stdout } = await captureConsole(() => runCli(["help"]));
108
+
109
+ expect(result).toBe(true);
110
+ expect(stdout).toContain("predictions [...]");
111
+ expect(stdout).toContain("Prediction Launch");
112
+ expect(stdout).toContain("gloomberb predictions world");
113
+ });
114
+
115
+ test("creates a watchlist and persists the generated id", async () => {
116
+ const { dataDir } = await createCliFixture({ watchlists: [] });
117
+
118
+ const { stdout } = await captureConsole(() => runCli(["watchlist", "create", "Growth Radar"]));
119
+ const config = await loadConfig(dataDir);
120
+
121
+ expect(config.watchlists).toEqual([{ id: "growth-radar", name: "Growth Radar" }]);
122
+ expect(stdout).toContain('Created watchlist "Growth Radar".');
123
+ expect(stdout).toContain("growth-radar");
124
+ });
125
+
126
+ test("adds a local ticker to a watchlist and cleans memberships on delete", async () => {
127
+ const { dataDir } = await createCliFixture({
128
+ watchlists: [{ id: "growth", name: "Growth" }],
129
+ tickers: [makeTicker()],
130
+ });
131
+
132
+ const addResult = await captureConsole(() => runCli(["watchlist", "add", "Growth", "NVDA"]));
133
+ expect(addResult.stdout).toContain('Added NVDA to "Growth".');
134
+
135
+ let persistence = new AppPersistence(join(dataDir, ".gloomberb-cache.db"));
136
+ let store = new TickerRepository(persistence.tickers);
137
+ let savedTicker = await store.loadTicker("NVDA");
138
+ persistence.close();
139
+
140
+ expect(savedTicker?.metadata.watchlists).toEqual(["growth"]);
141
+
142
+ const deleteResult = await captureConsole(() => runCli(["watchlist", "delete", "Growth"]));
143
+ expect(deleteResult.stdout).toContain('Deleted watchlist "Growth".');
144
+ expect(deleteResult.stdout).toContain("Cleaned Tickers");
145
+
146
+ const config = await loadConfig(dataDir);
147
+ expect(config.watchlists).toEqual([]);
148
+
149
+ persistence = new AppPersistence(join(dataDir, ".gloomberb-cache.db"));
150
+ store = new TickerRepository(persistence.tickers);
151
+ savedTicker = await store.loadTicker("NVDA");
152
+ persistence.close();
153
+
154
+ expect(savedTicker?.metadata.watchlists).toEqual([]);
155
+ });
156
+ });
157
+
158
+ describe("buildTickerReport", () => {
159
+ test("includes deeper fundamentals and recent statement sections", async () => {
160
+ const config = createDefaultConfig("/tmp/gloomberb-cli-report");
161
+ config.watchlists = [{ id: "growth", name: "Growth" }];
162
+
163
+ const ticker = makeTicker({
164
+ portfolios: ["main"],
165
+ watchlists: ["growth"],
166
+ positions: [{
167
+ portfolio: "main",
168
+ shares: 10,
169
+ avgCost: 400,
170
+ broker: "manual",
171
+ }],
172
+ assetCategory: "STK",
173
+ sector: "Technology",
174
+ industry: "Semiconductors",
175
+ });
176
+
177
+ const financials: TickerFinancials = {
178
+ quote: {
179
+ symbol: "NVDA",
180
+ price: 912.34,
181
+ currency: "USD",
182
+ change: 12.5,
183
+ changePercent: 1.39,
184
+ lastUpdated: Date.UTC(2026, 3, 1, 14, 30),
185
+ marketCap: 2_240_000_000_000,
186
+ volume: 48_000_000,
187
+ name: "NVIDIA Corporation",
188
+ exchangeName: "NMS",
189
+ marketState: "REGULAR",
190
+ bid: 912.3,
191
+ ask: 912.4,
192
+ bidSize: 100,
193
+ askSize: 200,
194
+ open: 905,
195
+ high: 918,
196
+ low: 901,
197
+ high52w: 980,
198
+ low52w: 500,
199
+ dataSource: "live",
200
+ },
201
+ fundamentals: {
202
+ trailingPE: 35.2,
203
+ forwardPE: 29.8,
204
+ pegRatio: 1.4,
205
+ enterpriseValue: 2_300_000_000_000,
206
+ operatingCashFlow: 82_000_000_000,
207
+ freeCashFlow: 67_000_000_000,
208
+ dividendYield: 0.001,
209
+ revenue: 128_000_000_000,
210
+ netIncome: 73_000_000_000,
211
+ eps: 2.97,
212
+ operatingMargin: 0.58,
213
+ profitMargin: 0.57,
214
+ revenueGrowth: 0.42,
215
+ return1Y: 0.88,
216
+ return3Y: 2.45,
217
+ lastQuarterGrowth: 0.61,
218
+ sharesOutstanding: 2_450_000_000,
219
+ },
220
+ profile: {
221
+ description: "Designs GPUs and accelerated computing platforms.",
222
+ sector: "Technology",
223
+ industry: "Semiconductors",
224
+ },
225
+ annualStatements: [{
226
+ date: "2025-12-31",
227
+ totalRevenue: 128_000_000_000,
228
+ grossProfit: 97_000_000_000,
229
+ operatingIncome: 76_000_000_000,
230
+ netIncome: 73_000_000_000,
231
+ ebitda: 79_000_000_000,
232
+ operatingCashFlow: 82_000_000_000,
233
+ freeCashFlow: 67_000_000_000,
234
+ cashAndCashEquivalents: 40_000_000_000,
235
+ totalAssets: 110_000_000_000,
236
+ totalLiabilities: 36_000_000_000,
237
+ totalDebt: 9_000_000_000,
238
+ totalEquity: 74_000_000_000,
239
+ eps: 11.84,
240
+ dilutedShares: 2_450_000_000,
241
+ }],
242
+ quarterlyStatements: [{
243
+ date: "2026-03-31",
244
+ totalRevenue: 38_000_000_000,
245
+ grossProfit: 28_000_000_000,
246
+ operatingIncome: 22_000_000_000,
247
+ netIncome: 21_000_000_000,
248
+ ebitda: 23_000_000_000,
249
+ operatingCashFlow: 24_000_000_000,
250
+ freeCashFlow: 19_000_000_000,
251
+ cashAndCashEquivalents: 42_000_000_000,
252
+ totalAssets: 118_000_000_000,
253
+ totalLiabilities: 38_000_000_000,
254
+ totalDebt: 9_000_000_000,
255
+ totalEquity: 80_000_000_000,
256
+ eps: 3.4,
257
+ dilutedShares: 2_455_000_000,
258
+ }],
259
+ priceHistory: [],
260
+ };
261
+
262
+ const recentNews: NewsItem[] = [{
263
+ title: "NVIDIA unveils next platform",
264
+ source: "Example News",
265
+ url: "https://example.com/nvda-platform",
266
+ publishedAt: new Date(Date.UTC(2026, 3, 1, 15, 45)),
267
+ summary: "Analysts expect the launch to expand datacenter demand.",
268
+ }];
269
+
270
+ const recentSecFilings: SecFilingItem[] = [{
271
+ accessionNumber: "0000000000-26-000001",
272
+ form: "8-K",
273
+ filingDate: new Date(Date.UTC(2026, 2, 31)),
274
+ cik: "0001045810",
275
+ filingUrl: "https://www.sec.gov/Archives/example-8k",
276
+ primaryDocument: "nvda-8k.htm",
277
+ primaryDocDescription: "Current report announcing a product launch",
278
+ items: "2.02, 7.01",
279
+ }];
280
+
281
+ const report = await buildTickerReport({
282
+ symbol: "NVDA",
283
+ tickerFile: ticker,
284
+ financials,
285
+ config,
286
+ toBase: async (value) => value,
287
+ notes: "Conviction remains high.\nWatch gross margin guidance.",
288
+ recentNews,
289
+ recentSecFilings,
290
+ });
291
+
292
+ expect(report).toContain("Fundamentals");
293
+ expect(report).toContain("(+1.39%)");
294
+ expect(report).not.toContain("++1.39%");
295
+ expect(report).toContain("Operating Cash Flow");
296
+ expect(report).toContain("Latest Annual (2025-12-31)");
297
+ expect(report).toContain("Latest Quarter (2026-03-31)");
298
+ expect(report).toContain("Watchlists Growth");
299
+ expect(report).toContain("Designs GPUs and accelerated computing platforms.");
300
+ expect(report).toContain("Notes");
301
+ expect(report).toContain("Conviction remains high.");
302
+ expect(report).toContain("Recent News");
303
+ expect(report).toContain("NVIDIA unveils next platform");
304
+ expect(report).toContain("Example News");
305
+ expect(report).toContain("Recent SEC Filings");
306
+ expect(report).toContain("8-K | Mar 31, 2026");
307
+ expect(report).toContain("Current report announcing a product launch");
308
+ });
309
+ });
310
+
311
+ describe("CLI search helpers", () => {
312
+ test("merges local ticker matches with provider company search results", async () => {
313
+ const candidates = await searchCandidatesForCli({
314
+ query: "micro",
315
+ tickers: [
316
+ makeTicker({
317
+ ticker: "MSFT",
318
+ name: "Microsoft Corporation",
319
+ exchange: "NASDAQ",
320
+ assetCategory: "STK",
321
+ }),
322
+ ],
323
+ dataProvider: createTestDataProvider({
324
+ search: async () => [{
325
+ providerId: "yahoo",
326
+ symbol: "MSTR",
327
+ name: "MicroStrategy Incorporated",
328
+ exchange: "NASDAQ",
329
+ type: "EQUITY",
330
+ currency: "USD",
331
+ }],
332
+ }),
333
+ });
334
+
335
+ expect(candidates.some((candidate) => candidate.label === "MSFT")).toBe(true);
336
+ expect(candidates.some((candidate) => candidate.label === "MSTR")).toBe(true);
337
+
338
+ const report = buildSearchReport({
339
+ query: "micro",
340
+ candidates,
341
+ });
342
+
343
+ expect(report).toContain("Search: micro");
344
+ expect(report).toContain("MSFT");
345
+ expect(report).toContain("Microsoft Corporation");
346
+ expect(report).toContain("MSTR");
347
+ expect(report).toContain("MicroStrategy Incorporated");
348
+ expect(report).toContain("Saved");
349
+ expect(report).toContain("yahoo");
350
+ });
351
+
352
+ test("renders an empty state when no search results match", () => {
353
+ const report = buildSearchReport({
354
+ query: "zzzz",
355
+ candidates: [],
356
+ });
357
+
358
+ expect(report).toContain("Search: zzzz");
359
+ expect(report).toContain("No matches found.");
360
+ });
361
+ });