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,323 @@
1
+ import type { Database } from "bun:sqlite";
2
+ import type { CachePolicy, PersistedResourceValue } from "../types/persistence";
3
+ import type { JsonValue } from "./sqlite-json";
4
+ import { safeParseJson, serializeJson } from "./sqlite-json";
5
+
6
+ const RESOURCE_CACHE_SOFT_ROW_LIMIT = 25_000;
7
+ const RESOURCE_CACHE_SOFT_SIZE_LIMIT = 100 * 1024 * 1024;
8
+ const DEFAULT_RESOURCE_SCHEMA_VERSION = 1;
9
+ const EMPTY_VARIANT_KEY = "";
10
+ const EMPTY_SOURCE_KEY = "";
11
+
12
+ interface ResourceCacheRow {
13
+ namespace: string;
14
+ kind: string;
15
+ entity_key: string;
16
+ variant_key: string;
17
+ source_key: string;
18
+ schema_version: number;
19
+ payload: string;
20
+ provenance: string | null;
21
+ fetched_at: number;
22
+ stale_at: number;
23
+ expires_at: number;
24
+ last_accessed_at: number;
25
+ size_bytes: number;
26
+ }
27
+
28
+ export interface ResourceCacheKey {
29
+ namespace: string;
30
+ kind: string;
31
+ entityKey: string;
32
+ variantKey?: string;
33
+ sourceKey?: string;
34
+ }
35
+
36
+ export interface CachedResourceRecord<T = unknown> extends PersistedResourceValue<T> {
37
+ namespace: string;
38
+ kind: string;
39
+ entityKey: string;
40
+ variantKey: string;
41
+ sourceKey: string;
42
+ provenance: JsonValue | null;
43
+ lastAccessedAt: number;
44
+ sizeBytes: number;
45
+ }
46
+
47
+ export interface SetResourceOptions {
48
+ schemaVersion?: number;
49
+ cachePolicy: CachePolicy;
50
+ provenance?: JsonValue | null;
51
+ fetchedAt?: number;
52
+ }
53
+
54
+ export interface GetResourceOptions {
55
+ schemaVersion?: number;
56
+ allowExpired?: boolean;
57
+ touch?: boolean;
58
+ }
59
+
60
+ export interface ListResourceOptions extends GetResourceOptions {
61
+ variantKeys?: string[];
62
+ sourceKeys?: string[];
63
+ }
64
+
65
+ function normalizeVariantKey(value: string | undefined): string {
66
+ return value ?? EMPTY_VARIANT_KEY;
67
+ }
68
+
69
+ function normalizeSourceKey(value: string | undefined): string {
70
+ return value ?? EMPTY_SOURCE_KEY;
71
+ }
72
+
73
+ function isExpired(expiresAt: number): boolean {
74
+ return expiresAt < Date.now();
75
+ }
76
+
77
+ function isStale(staleAt: number): boolean {
78
+ return staleAt < Date.now();
79
+ }
80
+
81
+ function sortRows(rows: ResourceCacheRow[]): ResourceCacheRow[] {
82
+ return [...rows].sort((a, b) => {
83
+ if (a.expires_at !== b.expires_at) return b.expires_at - a.expires_at;
84
+ if (a.stale_at !== b.stale_at) return b.stale_at - a.stale_at;
85
+ if (a.fetched_at !== b.fetched_at) return b.fetched_at - a.fetched_at;
86
+ return b.last_accessed_at - a.last_accessed_at;
87
+ });
88
+ }
89
+
90
+ export class ResourceStore {
91
+ constructor(private readonly db: Database) {}
92
+
93
+ private toCachedResource<T>(row: ResourceCacheRow): CachedResourceRecord<T> | null {
94
+ const value = safeParseJson<T>(row.payload);
95
+ if (value == null) return null;
96
+ return {
97
+ namespace: row.namespace,
98
+ kind: row.kind,
99
+ entityKey: row.entity_key,
100
+ variantKey: row.variant_key,
101
+ sourceKey: row.source_key,
102
+ value,
103
+ schemaVersion: row.schema_version,
104
+ provenance: safeParseJson<JsonValue>(row.provenance),
105
+ fetchedAt: row.fetched_at,
106
+ staleAt: row.stale_at,
107
+ expiresAt: row.expires_at,
108
+ lastAccessedAt: row.last_accessed_at,
109
+ sizeBytes: row.size_bytes,
110
+ stale: isStale(row.stale_at),
111
+ expired: isExpired(row.expires_at),
112
+ };
113
+ }
114
+
115
+ private deleteExact(key: ResourceCacheKey): void {
116
+ this.db
117
+ .query(
118
+ `DELETE FROM resource_cache
119
+ WHERE namespace = ? AND kind = ? AND entity_key = ? AND variant_key = ? AND source_key = ?`,
120
+ )
121
+ .run(
122
+ key.namespace,
123
+ key.kind,
124
+ key.entityKey,
125
+ normalizeVariantKey(key.variantKey),
126
+ normalizeSourceKey(key.sourceKey),
127
+ );
128
+ }
129
+
130
+ private touch(key: ResourceCacheKey): void {
131
+ this.db
132
+ .query(
133
+ `UPDATE resource_cache
134
+ SET last_accessed_at = ?
135
+ WHERE namespace = ? AND kind = ? AND entity_key = ? AND variant_key = ? AND source_key = ?`,
136
+ )
137
+ .run(
138
+ Date.now(),
139
+ key.namespace,
140
+ key.kind,
141
+ key.entityKey,
142
+ normalizeVariantKey(key.variantKey),
143
+ normalizeSourceKey(key.sourceKey),
144
+ );
145
+ }
146
+
147
+ private pruneIfNeeded(): void {
148
+ const stats = this.db
149
+ .query<{ count: number; total_size: number }, []>(
150
+ "SELECT COUNT(*) as count, COALESCE(SUM(size_bytes), 0) as total_size FROM resource_cache",
151
+ )
152
+ .get();
153
+ const count = stats?.count ?? 0;
154
+ const totalSize = stats?.total_size ?? 0;
155
+ if (count <= RESOURCE_CACHE_SOFT_ROW_LIMIT && totalSize <= RESOURCE_CACHE_SOFT_SIZE_LIMIT) return;
156
+
157
+ this.db.query("DELETE FROM resource_cache WHERE expires_at < ?").run(Date.now());
158
+
159
+ const remaining = this.db
160
+ .query<{ count: number; total_size: number }, []>(
161
+ "SELECT COUNT(*) as count, COALESCE(SUM(size_bytes), 0) as total_size FROM resource_cache",
162
+ )
163
+ .get();
164
+ const remainingCount = remaining?.count ?? 0;
165
+ const remainingSize = remaining?.total_size ?? 0;
166
+ if (remainingCount <= RESOURCE_CACHE_SOFT_ROW_LIMIT && remainingSize <= RESOURCE_CACHE_SOFT_SIZE_LIMIT) return;
167
+
168
+ const rowsToDelete = this.db
169
+ .query<{ namespace: string; kind: string; entity_key: string; variant_key: string; source_key: string }, [number]>(
170
+ `SELECT namespace, kind, entity_key, variant_key, source_key
171
+ FROM resource_cache
172
+ ORDER BY last_accessed_at ASC, fetched_at ASC
173
+ LIMIT ?`,
174
+ )
175
+ .all(Math.max(Math.ceil((remainingCount - RESOURCE_CACHE_SOFT_ROW_LIMIT) + 250), 250));
176
+
177
+ if (rowsToDelete.length === 0) return;
178
+ const remove = this.db.query(
179
+ `DELETE FROM resource_cache
180
+ WHERE namespace = ? AND kind = ? AND entity_key = ? AND variant_key = ? AND source_key = ?`,
181
+ );
182
+ const tx = this.db.transaction(() => {
183
+ for (const row of rowsToDelete) {
184
+ remove.run(row.namespace, row.kind, row.entity_key, row.variant_key, row.source_key);
185
+ }
186
+ });
187
+ tx();
188
+ }
189
+
190
+ get<T>(key: ResourceCacheKey, options: GetResourceOptions = {}): CachedResourceRecord<T> | null {
191
+ const row = this.db
192
+ .query<ResourceCacheRow, [string, string, string, string, string]>(
193
+ `SELECT namespace, kind, entity_key, variant_key, source_key, schema_version, payload, provenance,
194
+ fetched_at, stale_at, expires_at, last_accessed_at, size_bytes
195
+ FROM resource_cache
196
+ WHERE namespace = ? AND kind = ? AND entity_key = ? AND variant_key = ? AND source_key = ?`,
197
+ )
198
+ .get(
199
+ key.namespace,
200
+ key.kind,
201
+ key.entityKey,
202
+ normalizeVariantKey(key.variantKey),
203
+ normalizeSourceKey(key.sourceKey),
204
+ );
205
+
206
+ if (!row) return null;
207
+ if (!options.allowExpired && isExpired(row.expires_at)) return null;
208
+ if (options.schemaVersion != null && row.schema_version !== options.schemaVersion) {
209
+ this.deleteExact(key);
210
+ return null;
211
+ }
212
+
213
+ const record = this.toCachedResource<T>(row);
214
+ if (!record) {
215
+ this.deleteExact(key);
216
+ return null;
217
+ }
218
+
219
+ if (options.touch !== false) {
220
+ this.touch(key);
221
+ record.lastAccessedAt = Date.now();
222
+ }
223
+ return record;
224
+ }
225
+
226
+ list<T>(
227
+ key: Pick<ResourceCacheKey, "namespace" | "kind" | "entityKey">,
228
+ options: ListResourceOptions = {},
229
+ ): CachedResourceRecord<T>[] {
230
+ const rows = this.db
231
+ .query<ResourceCacheRow, [string, string, string]>(
232
+ `SELECT namespace, kind, entity_key, variant_key, source_key, schema_version, payload, provenance,
233
+ fetched_at, stale_at, expires_at, last_accessed_at, size_bytes
234
+ FROM resource_cache
235
+ WHERE namespace = ? AND kind = ? AND entity_key = ?`,
236
+ )
237
+ .all(key.namespace, key.kind, key.entityKey);
238
+
239
+ const allowedVariantKeys = options.variantKeys ? new Set(options.variantKeys.map(normalizeVariantKey)) : null;
240
+ const allowedSourceKeys = options.sourceKeys ? new Set(options.sourceKeys.map(normalizeSourceKey)) : null;
241
+
242
+ const records: CachedResourceRecord<T>[] = [];
243
+ for (const row of sortRows(rows)) {
244
+ if (!options.allowExpired && isExpired(row.expires_at)) continue;
245
+ if (allowedVariantKeys && !allowedVariantKeys.has(row.variant_key)) continue;
246
+ if (allowedSourceKeys && !allowedSourceKeys.has(row.source_key)) continue;
247
+ if (options.schemaVersion != null && row.schema_version !== options.schemaVersion) {
248
+ this.deleteExact({
249
+ namespace: row.namespace,
250
+ kind: row.kind,
251
+ entityKey: row.entity_key,
252
+ variantKey: row.variant_key,
253
+ sourceKey: row.source_key,
254
+ });
255
+ continue;
256
+ }
257
+ const record = this.toCachedResource<T>(row);
258
+ if (!record) {
259
+ this.deleteExact({
260
+ namespace: row.namespace,
261
+ kind: row.kind,
262
+ entityKey: row.entity_key,
263
+ variantKey: row.variant_key,
264
+ sourceKey: row.source_key,
265
+ });
266
+ continue;
267
+ }
268
+ records.push(record);
269
+ }
270
+ return records;
271
+ }
272
+
273
+ set<T>(key: ResourceCacheKey, value: T, options: SetResourceOptions): CachedResourceRecord<T> {
274
+ const now = options.fetchedAt ?? Date.now();
275
+ const payload = serializeJson(value);
276
+ const provenance = options.provenance == null ? null : serializeJson(options.provenance);
277
+ const rowKey = {
278
+ namespace: key.namespace,
279
+ kind: key.kind,
280
+ entityKey: key.entityKey,
281
+ variantKey: normalizeVariantKey(key.variantKey),
282
+ sourceKey: normalizeSourceKey(key.sourceKey),
283
+ };
284
+
285
+ this.db
286
+ .query(
287
+ `INSERT OR REPLACE INTO resource_cache (
288
+ namespace, kind, entity_key, variant_key, source_key, schema_version,
289
+ payload, provenance, fetched_at, stale_at, expires_at, last_accessed_at, size_bytes
290
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
291
+ )
292
+ .run(
293
+ rowKey.namespace,
294
+ rowKey.kind,
295
+ rowKey.entityKey,
296
+ rowKey.variantKey,
297
+ rowKey.sourceKey,
298
+ options.schemaVersion ?? DEFAULT_RESOURCE_SCHEMA_VERSION,
299
+ payload,
300
+ provenance,
301
+ now,
302
+ now + options.cachePolicy.staleMs,
303
+ now + options.cachePolicy.expireMs,
304
+ now,
305
+ Buffer.byteLength(payload, "utf8"),
306
+ );
307
+
308
+ this.pruneIfNeeded();
309
+ return this.get<T>(rowKey, { allowExpired: true, touch: false })!;
310
+ }
311
+
312
+ delete(key: ResourceCacheKey): void {
313
+ this.deleteExact(key);
314
+ }
315
+
316
+ clear(namespace?: string): void {
317
+ if (!namespace) {
318
+ this.db.query("DELETE FROM resource_cache").run();
319
+ return;
320
+ }
321
+ this.db.query("DELETE FROM resource_cache WHERE namespace = ?").run(namespace);
322
+ }
323
+ }
@@ -0,0 +1,52 @@
1
+ import type { Database } from "bun:sqlite";
2
+ import { safeParseJson, serializeJson } from "./sqlite-json";
3
+
4
+ export const DEFAULT_SESSION_SCHEMA_VERSION = 1;
5
+
6
+ export interface SessionSnapshotRecord<T = unknown> {
7
+ sessionId: string;
8
+ value: T;
9
+ schemaVersion: number;
10
+ updatedAt: number;
11
+ }
12
+
13
+ export class SessionStore {
14
+ constructor(private readonly db: Database) {}
15
+
16
+ get<T>(sessionId = "app", schemaVersion = DEFAULT_SESSION_SCHEMA_VERSION): SessionSnapshotRecord<T> | null {
17
+ const row = this.db
18
+ .query<{ schema_version: number; value: string; updated_at: number }, [string]>(
19
+ "SELECT schema_version, value, updated_at FROM session_snapshots WHERE session_id = ?",
20
+ )
21
+ .get(sessionId);
22
+ if (!row) return null;
23
+ if (row.schema_version !== schemaVersion) {
24
+ this.delete(sessionId);
25
+ return null;
26
+ }
27
+ const value = safeParseJson<T>(row.value);
28
+ if (value == null) {
29
+ this.delete(sessionId);
30
+ return null;
31
+ }
32
+ return {
33
+ sessionId,
34
+ value,
35
+ schemaVersion: row.schema_version,
36
+ updatedAt: row.updated_at,
37
+ };
38
+ }
39
+
40
+ set(sessionId: string, value: unknown, schemaVersion = DEFAULT_SESSION_SCHEMA_VERSION): void {
41
+ this.db
42
+ .query(
43
+ `INSERT OR REPLACE INTO session_snapshots (session_id, schema_version, value, updated_at)
44
+ VALUES (?, ?, ?, ?)`,
45
+ )
46
+ .run(sessionId, schemaVersion, serializeJson(value), Date.now());
47
+ }
48
+
49
+ delete(sessionId: string): void {
50
+ this.db.query("DELETE FROM session_snapshots WHERE session_id = ?").run(sessionId);
51
+ }
52
+ }
@@ -0,0 +1,103 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { existsSync, rmSync } from "fs";
3
+ import { join } from "path";
4
+ import { tmpdir } from "os";
5
+ import { AppPersistence } from "./app-persistence";
6
+
7
+ const tempPaths: string[] = [];
8
+
9
+ function createTempDbPath(name: string): string {
10
+ const path = join(tmpdir(), `gloomberb-${name}-${Date.now()}-${Math.random().toString(36).slice(2)}.db`);
11
+ tempPaths.push(path);
12
+ return path;
13
+ }
14
+
15
+ afterEach(() => {
16
+ for (const path of tempPaths.splice(0)) {
17
+ if (existsSync(path)) rmSync(path, { force: true });
18
+ }
19
+ });
20
+
21
+ describe("AppPersistence", () => {
22
+ test("stores and returns cached resources", () => {
23
+ const dbPath = createTempDbPath("resource-cache");
24
+ const persistence = new AppPersistence(dbPath);
25
+ persistence.resources.set({
26
+ namespace: "market",
27
+ kind: "quote",
28
+ entityKey: "AAPL",
29
+ sourceKey: "provider:yahoo",
30
+ }, {
31
+ price: 123,
32
+ }, {
33
+ cachePolicy: { staleMs: 60_000, expireMs: 120_000 },
34
+ schemaVersion: 1,
35
+ });
36
+
37
+ expect(persistence.resources.get<{ price: number }>({
38
+ namespace: "market",
39
+ kind: "quote",
40
+ entityKey: "AAPL",
41
+ sourceKey: "provider:yahoo",
42
+ }, { allowExpired: true })?.value.price).toBe(123);
43
+ persistence.close();
44
+ });
45
+
46
+ test("invalidates plugin state when schema versions differ", () => {
47
+ const dbPath = createTempDbPath("plugin-version");
48
+ const persistence = new AppPersistence(dbPath);
49
+ persistence.pluginState.set("ai", "conversation", { messages: ["hello"] }, 2);
50
+
51
+ expect(persistence.pluginState.get("ai", "conversation", 1)).toBeNull();
52
+ expect(persistence.pluginState.get("ai", "conversation", 2)).toBeNull();
53
+ persistence.close();
54
+ });
55
+
56
+ test("returns a stable plugin state snapshot until the row changes", () => {
57
+ const dbPath = createTempDbPath("plugin-snapshot-cache");
58
+ const persistence = new AppPersistence(dbPath);
59
+ persistence.pluginState.set("ai", "resume:screener-pane:test", {
60
+ tabs: [{ id: "1", prompt: "durable compounders" }],
61
+ }, 1);
62
+
63
+ const first = persistence.pluginState.get<{ tabs: Array<{ id: string; prompt: string }> }>("ai", "resume:screener-pane:test", 1);
64
+ const second = persistence.pluginState.get<{ tabs: Array<{ id: string; prompt: string }> }>("ai", "resume:screener-pane:test", 1);
65
+
66
+ expect(first).not.toBeNull();
67
+ expect(second).not.toBeNull();
68
+ expect(second).toBe(first);
69
+
70
+ persistence.pluginState.set("ai", "resume:screener-pane:test", {
71
+ tabs: [{ id: "2", prompt: "new leaders" }],
72
+ }, 1);
73
+ const third = persistence.pluginState.get<{ tabs: Array<{ id: string; prompt: string }> }>("ai", "resume:screener-pane:test", 1);
74
+
75
+ expect(third).not.toBeNull();
76
+ expect(third).not.toBe(first);
77
+ expect(third?.value.tabs[0]?.id).toBe("2");
78
+ persistence.close();
79
+ });
80
+
81
+ test("returns stable references for unchanged plugin state snapshots", () => {
82
+ const dbPath = createTempDbPath("plugin-stable-snapshot");
83
+ const persistence = new AppPersistence(dbPath);
84
+ persistence.pluginState.set("prediction-markets", "resume:watchlist:v1", ["kalshi:ABC"], 1);
85
+
86
+ const first = persistence.pluginState.get<string[]>("prediction-markets", "resume:watchlist:v1", 1);
87
+ const second = persistence.pluginState.get<string[]>("prediction-markets", "resume:watchlist:v1", 1);
88
+
89
+ expect(first).not.toBeNull();
90
+ expect(second).not.toBeNull();
91
+ expect(second?.value).toBe(first?.value);
92
+ persistence.close();
93
+ });
94
+
95
+ test("stores and returns session snapshots", () => {
96
+ const dbPath = createTempDbPath("session-snapshot");
97
+ const persistence = new AppPersistence(dbPath);
98
+ persistence.sessions.set("app", { focusedPaneId: "ticker-detail:main" }, 1);
99
+
100
+ expect(persistence.sessions.get<{ focusedPaneId: string }>("app", 1)?.value.focusedPaneId).toBe("ticker-detail:main");
101
+ persistence.close();
102
+ });
103
+ });
@@ -0,0 +1,62 @@
1
+ import { Database } from "bun:sqlite";
2
+
3
+ const SCHEMA = `
4
+ CREATE TABLE IF NOT EXISTS tickers (
5
+ symbol TEXT PRIMARY KEY,
6
+ metadata TEXT NOT NULL,
7
+ updated_at INTEGER NOT NULL
8
+ );
9
+
10
+ CREATE TABLE IF NOT EXISTS resource_cache (
11
+ namespace TEXT NOT NULL,
12
+ kind TEXT NOT NULL,
13
+ entity_key TEXT NOT NULL,
14
+ variant_key TEXT NOT NULL DEFAULT '',
15
+ source_key TEXT NOT NULL DEFAULT '',
16
+ schema_version INTEGER NOT NULL,
17
+ payload TEXT NOT NULL,
18
+ provenance TEXT,
19
+ fetched_at INTEGER NOT NULL,
20
+ stale_at INTEGER NOT NULL,
21
+ expires_at INTEGER NOT NULL,
22
+ last_accessed_at INTEGER NOT NULL,
23
+ size_bytes INTEGER NOT NULL,
24
+ PRIMARY KEY (namespace, kind, entity_key, variant_key, source_key)
25
+ );
26
+
27
+ CREATE INDEX IF NOT EXISTS idx_resource_cache_lookup
28
+ ON resource_cache (namespace, kind, entity_key, expires_at, stale_at);
29
+
30
+ CREATE INDEX IF NOT EXISTS idx_resource_cache_lru
31
+ ON resource_cache (expires_at, last_accessed_at);
32
+
33
+ CREATE TABLE IF NOT EXISTS plugin_state (
34
+ plugin_id TEXT NOT NULL,
35
+ key TEXT NOT NULL,
36
+ schema_version INTEGER NOT NULL,
37
+ value TEXT NOT NULL,
38
+ updated_at INTEGER NOT NULL,
39
+ PRIMARY KEY (plugin_id, key)
40
+ );
41
+
42
+ CREATE TABLE IF NOT EXISTS session_snapshots (
43
+ session_id TEXT PRIMARY KEY,
44
+ schema_version INTEGER NOT NULL,
45
+ value TEXT NOT NULL,
46
+ updated_at INTEGER NOT NULL
47
+ );
48
+ `;
49
+
50
+ export class SqliteDatabase {
51
+ readonly connection: Database;
52
+
53
+ constructor(dbPath: string) {
54
+ this.connection = new Database(dbPath);
55
+ this.connection.exec("PRAGMA journal_mode=WAL");
56
+ this.connection.exec(SCHEMA);
57
+ }
58
+
59
+ close(): void {
60
+ this.connection.close();
61
+ }
62
+ }
@@ -0,0 +1,19 @@
1
+ export type JsonPrimitive = string | number | boolean | null;
2
+ export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
3
+
4
+ export function serializeJson(value: unknown): string {
5
+ return JSON.stringify(value);
6
+ }
7
+
8
+ export function parseJson<T>(value: string): T {
9
+ return JSON.parse(value) as T;
10
+ }
11
+
12
+ export function safeParseJson<T>(value: string | null): T | null {
13
+ if (!value) return null;
14
+ try {
15
+ return parseJson<T>(value);
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
@@ -0,0 +1,45 @@
1
+ import type { TickerRecord, TickerMetadata } from "../types/ticker";
2
+ import type { TickerStore } from "./ticker-store";
3
+ import { hydrateTickerMetadata } from "../utils/ticker-metadata";
4
+
5
+ function parseTickerMetadata(json: string): TickerMetadata {
6
+ return hydrateTickerMetadata(JSON.parse(json) as Record<string, unknown>);
7
+ }
8
+
9
+ export class TickerRepository {
10
+ constructor(private readonly tickers: TickerStore) {}
11
+
12
+ async loadAllTickers(): Promise<TickerRecord[]> {
13
+ return this.tickers.getAll().flatMap((row) => {
14
+ try {
15
+ return [{ metadata: parseTickerMetadata(row.metadata) }];
16
+ } catch {
17
+ return [];
18
+ }
19
+ });
20
+ }
21
+
22
+ async loadTicker(symbol: string): Promise<TickerRecord | null> {
23
+ const json = this.tickers.get(symbol);
24
+ if (!json) return null;
25
+ try {
26
+ return { metadata: parseTickerMetadata(json) };
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+
32
+ async saveTicker(ticker: TickerRecord): Promise<void> {
33
+ this.tickers.save(ticker.metadata.ticker, ticker.metadata);
34
+ }
35
+
36
+ async createTicker(metadata: TickerMetadata): Promise<TickerRecord> {
37
+ const ticker: TickerRecord = { metadata };
38
+ await this.saveTicker(ticker);
39
+ return ticker;
40
+ }
41
+
42
+ async deleteTicker(symbol: string): Promise<void> {
43
+ this.tickers.delete(symbol);
44
+ }
45
+ }
@@ -0,0 +1,46 @@
1
+ import type { Database } from "bun:sqlite";
2
+ import { serializeJson } from "./sqlite-json";
3
+
4
+ export interface StoredTickerRecord {
5
+ symbol: string;
6
+ metadata: string;
7
+ }
8
+
9
+ export class TickerStore {
10
+ constructor(private readonly db: Database) {}
11
+
12
+ getAll(): StoredTickerRecord[] {
13
+ return this.db
14
+ .query<StoredTickerRecord, []>(
15
+ "SELECT symbol, metadata FROM tickers ORDER BY symbol",
16
+ )
17
+ .all();
18
+ }
19
+
20
+ get(symbol: string): string | null {
21
+ const row = this.db
22
+ .query<{ metadata: string }, [string]>(
23
+ "SELECT metadata FROM tickers WHERE symbol = ?",
24
+ )
25
+ .get(symbol);
26
+ return row?.metadata ?? null;
27
+ }
28
+
29
+ save(symbol: string, metadata: unknown): void {
30
+ this.db
31
+ .query(
32
+ `INSERT OR REPLACE INTO tickers (symbol, metadata, updated_at)
33
+ VALUES (?, ?, ?)`,
34
+ )
35
+ .run(symbol, serializeJson(metadata), Date.now());
36
+ }
37
+
38
+ delete(symbol: string): void {
39
+ this.db.query("DELETE FROM tickers WHERE symbol = ?").run(symbol);
40
+ }
41
+
42
+ count(): number {
43
+ const row = this.db.query<{ count: number }, []>("SELECT COUNT(*) as count FROM tickers").get();
44
+ return row?.count ?? 0;
45
+ }
46
+ }