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.
- package/README.md +24 -2
- package/bin/gloomberb +1 -1
- package/package.json +1 -1
- package/src/app/runtime/desktop-menu.ts +5 -1
- package/src/app/runtime/ticker-refresh.ts +4 -1
- package/src/app.tsx +20 -8
- package/src/brokers/account-cache.ts +1 -1
- package/src/brokers/sync-broker-instance.test.ts +107 -0
- package/src/brokers/sync-broker-instance.ts +22 -10
- package/src/capabilities/registry.ts +1 -0
- package/src/capabilities/types.ts +23 -0
- package/src/cli/commands/api.ts +119 -0
- package/src/cli/commands/automation.ts +173 -0
- package/src/cli/commands/command-utils.ts +53 -0
- package/src/cli/commands/market.ts +527 -0
- package/src/cli/commands/overview.ts +222 -0
- package/src/cli/commands/search.ts +39 -0
- package/src/cli/commands/system.ts +553 -0
- package/src/cli/commands/ticker.ts +104 -12
- package/src/cli/context.ts +35 -2
- package/src/cli/desktop-pane-shot.ts +2 -1
- package/src/cli/entry.ts +51 -0
- package/src/cli/errors.ts +56 -5
- package/src/cli/index.ts +73 -11
- package/src/cli/options.test.ts +38 -0
- package/src/cli/options.ts +95 -0
- package/src/cli/pane-functions/index.ts +10 -1
- package/src/cli/pane-functions/screenshot.ts +13 -1
- package/src/cli/registry.test.ts +40 -0
- package/src/cli/registry.ts +46 -5
- package/src/cli/result.test.ts +82 -0
- package/src/cli/result.ts +149 -0
- package/src/cli/types.ts +11 -0
- package/src/cli.test.ts +11 -11
- package/src/components/chart/bar-chart-renderer.test.ts +108 -3
- package/src/components/chart/bar-chart-renderer.ts +226 -27
- package/src/components/chart/comparison-stock-chart/interactions.ts +7 -1
- package/src/components/chart/comparison-stock-chart/legend.tsx +4 -0
- package/src/components/chart/comparison-stock-chart/render-data.ts +37 -8
- package/src/components/chart/comparison-stock-chart/selection-runtime.ts +19 -2
- package/src/components/chart/comparison-stock-chart/sources.ts +8 -4
- package/src/components/chart/comparison-stock-chart/toolbar.tsx +13 -3
- package/src/components/chart/comparison-stock-chart/view.tsx +12 -2
- package/src/components/chart/core/pane-settings.test.ts +27 -0
- package/src/components/chart/core/pane-settings.ts +60 -1
- package/src/components/chart/core/pointer.ts +34 -6
- package/src/components/chart/price-axis-labels.tsx +3 -1
- package/src/components/chart/static/bar-chart-surface.test.tsx +97 -0
- package/src/components/chart/static/bar-chart-surface.tsx +151 -28
- package/src/components/chart/static/chart/axis-overlays.tsx +243 -0
- package/src/components/chart/static/chart/surface.test.tsx +92 -0
- package/src/components/chart/static/chart/surface.tsx +167 -24
- package/src/components/chart/stock-chart/index.test.ts +25 -0
- package/src/components/chart/stock-chart/resolved/runtime.ts +1 -1
- package/src/components/chart/stock-chart/settings.ts +7 -3
- package/src/components/chart/stock-chart/viewport/runtime.ts +5 -5
- package/src/components/command-bar/layout-items/current-layout.ts +5 -1
- package/src/components/command-bar/pane-templates/workflow-route.ts +10 -0
- package/src/components/command-bar/surface/pane.test.tsx +76 -0
- package/src/components/data-table/view.tsx +13 -2
- package/src/components/feed-data-table/stack-view.tsx +9 -1
- package/src/components/index.ts +1 -1
- package/src/components/input-search-bar.test.tsx +103 -0
- package/src/components/input-search-bar.tsx +16 -0
- package/src/components/layout/detached-pane-shell.tsx +6 -3
- package/src/components/layout/floating-pane.tsx +3 -2
- package/src/components/layout/pane/content.tsx +20 -8
- package/src/components/layout/pane/footer/registration.tsx +6 -2
- package/src/components/layout/pane/frame.tsx +4 -4
- package/src/components/layout/pane/header.tsx +40 -4
- package/src/components/layout/pane/index.tsx +3 -1
- package/src/components/layout/pane/sizing.test.ts +43 -0
- package/src/components/layout/pane/sizing.ts +13 -4
- package/src/components/layout/shell/active-drag.ts +5 -1
- package/src/components/layout/shell/cursor-occlusion.ts +196 -0
- package/src/components/layout/shell/drag/runtime.ts +28 -3
- package/src/components/layout/shell/fullscreen.ts +10 -0
- package/src/components/layout/shell/index.test.tsx +487 -4
- package/src/components/layout/shell/index.tsx +165 -4
- package/src/components/layout/shell/layout-state.test.ts +33 -0
- package/src/components/layout/shell/layout-state.ts +4 -9
- package/src/components/layout/shell/native/pointer-runtime.ts +10 -4
- package/src/components/layout/shell/pane/layers.tsx +34 -16
- package/src/components/layout/shell/pane/management-shortcuts.ts +19 -0
- package/src/components/layout/shell/shortcuts.ts +4 -0
- package/src/components/layout/shell/terminal-pointer-runtime.ts +29 -19
- package/src/components/layout/shell/visible-layout.ts +17 -0
- package/src/components/layout/shell/window-mode/index.ts +11 -3
- package/src/components/layout/shell/window-mode/overlays.tsx +8 -1
- package/src/components/layout/status-bar.test.tsx +111 -1
- package/src/components/layout/status-bar.tsx +37 -7
- package/src/components/layout/transient-layout.tsx +57 -0
- package/src/components/layout/window-edit/mode.test.ts +76 -5
- package/src/components/layout/window-edit/mode.ts +47 -2
- package/src/components/layout/window-edit/presentation.ts +2 -1
- package/src/components/onboarding/onboarding-steps.tsx +8 -28
- package/src/components/onboarding/onboarding-wizard.test.tsx +13 -12
- package/src/components/onboarding/onboarding-wizard.tsx +2 -4
- package/src/components/onboarding/portfolio-step/index.tsx +0 -12
- package/src/components/onboarding/portfolio-step/types.ts +1 -3
- package/src/components/onboarding/wizard-keyboard.ts +2 -5
- package/src/components/price-performance.tsx +9 -1
- package/src/components/table-view-shared.tsx +12 -4
- package/src/components/ui/data-table/opentui/index.tsx +25 -12
- package/src/components/ui/data-table/types.ts +4 -0
- package/src/components/ui/fields.tsx +10 -3
- package/src/components/ui/page-stack-view.tsx +6 -2
- package/src/components/ui/table-layout.test.ts +25 -0
- package/src/components/ui/table-layout.ts +22 -6
- package/src/components/ui/tabs.tsx +26 -10
- package/src/components/ui/ui.test.tsx +87 -0
- package/src/core/app-services.ts +7 -4
- package/src/core/state/app/layout-reducer.ts +2 -2
- package/src/data/config/store/index.ts +1 -2
- package/src/plugins/builtin/analytics/index.test.tsx +6 -2
- package/src/plugins/builtin/analytics/pane-model.ts +43 -8
- package/src/plugins/builtin/cloud-tweets/pane.tsx +2 -0
- package/src/plugins/builtin/cloud-tweets/search-bar.tsx +3 -0
- package/src/plugins/builtin/cloud-tweets/table.tsx +14 -2
- package/src/plugins/builtin/comparison-chart/index.test.tsx +130 -6
- package/src/plugins/builtin/kelly-sizer/constants.ts +3 -0
- package/src/plugins/builtin/kelly-sizer/fields.ts +214 -0
- package/src/plugins/builtin/kelly-sizer/index.test.tsx +197 -0
- package/src/plugins/builtin/kelly-sizer/index.tsx +47 -0
- package/src/plugins/builtin/kelly-sizer/model.test.ts +152 -0
- package/src/plugins/builtin/kelly-sizer/model.ts +580 -0
- package/src/plugins/builtin/kelly-sizer/pane.tsx +558 -0
- package/src/plugins/builtin/kelly-sizer/portfolio.ts +71 -0
- package/src/plugins/builtin/kelly-sizer/sections.tsx +142 -0
- package/src/plugins/builtin/kelly-sizer/state.ts +67 -0
- package/src/plugins/builtin/kelly-sizer/test-support.tsx +174 -0
- package/src/plugins/builtin/kelly-sizer/types.ts +156 -0
- package/src/plugins/builtin/kelly-sizer/view.test.tsx +77 -0
- package/src/plugins/builtin/kelly-sizer/view.tsx +375 -0
- package/src/plugins/builtin/layout-manager/index.tsx +5 -1
- package/src/plugins/builtin/news/wire/read-state.ts +9 -32
- package/src/plugins/builtin/options/view.test.tsx +141 -4
- package/src/plugins/builtin/options/view.tsx +76 -15
- package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +26 -1
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +14 -0
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +23 -0
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +19 -2
- package/src/plugins/builtin/portfolio-list/index.test.ts +23 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +3 -1
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -7
- package/src/plugins/builtin/portfolio-list/summary/index.test.ts +59 -1
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +36 -5
- package/src/plugins/builtin/shared/read-state.ts +43 -0
- package/src/plugins/builtin/substack/article-detail.test.tsx +10 -0
- package/src/plugins/builtin/substack/article-detail.tsx +39 -15
- package/src/plugins/builtin/substack/article-stack.tsx +6 -2
- package/src/plugins/builtin/substack/feed-tabs.tsx +12 -9
- package/src/plugins/builtin/substack/pane.tsx +8 -3
- package/src/plugins/builtin/substack/read-state.ts +55 -0
- package/src/plugins/builtin/thirteenf/pane.test.tsx +26 -0
- package/src/plugins/builtin/thirteenf/pane.tsx +12 -1
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +149 -48
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +25 -2
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +5 -4
- package/src/plugins/builtin/ticker-detail/data-panes.test.ts +16 -0
- package/src/plugins/builtin/ticker-detail/index.test.tsx +143 -0
- package/src/plugins/catalog-ui.ts +2 -0
- package/src/plugins/ibkr/broker-adapter.ts +19 -0
- package/src/plugins/ibkr/config.test.ts +31 -2
- package/src/plugins/ibkr/config.ts +9 -2
- package/src/plugins/ibkr/flex/index.test.ts +18 -9
- package/src/plugins/ibkr/flex/index.ts +30 -0
- package/src/plugins/ibkr/flex/statement-client.ts +68 -14
- package/src/plugins/ibkr/gateway/account-loaders.ts +1 -1
- package/src/plugins/ibkr/gateway/account-summary.ts +1 -0
- package/src/plugins/ibkr/gateway/service/index.test.ts +8 -0
- package/src/plugins/pane-manager/docking.ts +9 -2
- package/src/plugins/pane-manager/gridlock.ts +13 -5
- package/src/plugins/pane-manager/layout-state.ts +25 -0
- package/src/plugins/pane-manager/queries.ts +8 -2
- package/src/plugins/pane-manager.test.ts +33 -0
- package/src/plugins/pane-manager.ts +6 -1
- package/src/plugins/prediction-markets/chart.tsx +10 -4
- package/src/plugins/prediction-markets/controller/index.ts +2 -0
- package/src/plugins/prediction-markets/controller/keyboard.ts +34 -5
- package/src/plugins/prediction-markets/detail/book.tsx +141 -96
- package/src/plugins/prediction-markets/detail/overview.tsx +3 -0
- package/src/plugins/prediction-markets/detail/pane.tsx +71 -64
- package/src/plugins/prediction-markets/detail/shared.tsx +19 -0
- package/src/plugins/prediction-markets/detail/trades.tsx +64 -19
- package/src/plugins/prediction-markets/detail.test.tsx +50 -0
- package/src/plugins/prediction-markets/pane.test.tsx +75 -0
- package/src/plugins/prediction-markets/pane.tsx +71 -23
- package/src/plugins/prediction-markets/test-helpers.tsx +42 -23
- package/src/renderers/electrobun/bun/application-menu/click.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu/click.ts +1 -0
- package/src/renderers/electrobun/bun/application-menu/index.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
- package/src/renderers/electrobun/bun/index.ts +13 -0
- package/src/renderers/electrobun/bun/window/focus.test.ts +35 -0
- package/src/renderers/electrobun/bun/window/focus.ts +5 -0
- package/src/renderers/electrobun/view/app-services.ts +6 -4
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +13 -2
- package/src/renderers/electrobun/view/data-table/index.tsx +19 -4
- package/src/renderers/electrobun/view/data-table/row.tsx +30 -6
- package/src/renderers/electrobun/view/desktop/controls.tsx +11 -5
- package/src/renderers/electrobun/view/host/ascii-text.ts +50 -0
- package/src/renderers/electrobun/view/host/input.tsx +115 -18
- package/src/renderers/electrobun/view/host/tabs.tsx +7 -2
- package/src/renderers/electrobun/view/host/text.test.ts +22 -0
- package/src/renderers/electrobun/view/host/text.tsx +24 -20
- package/src/renderers/electrobun/view/styles.css +11 -0
- package/src/renderers/electrobun/view/ui-host.tsx +1 -1
- package/src/renderers/opentui/host.tsx +17 -1
- package/src/renderers/opentui/start.tsx +13 -5
- package/src/renderers/opentui/ui-host.tsx +57 -25
- package/src/sources/provider-router/batches.ts +16 -2
- package/src/sources/provider-router/financial-routes.ts +5 -0
- package/src/sources/provider-router/financials.ts +11 -23
- package/src/sources/provider-router/index.test.ts +189 -0
- package/src/sources/provider-router/primary.ts +3 -2
- package/src/sources/sec-edgar.test.ts +83 -0
- package/src/sources/sec-edgar.ts +231 -0
- package/src/sources/yahoo-finance.test.ts +51 -0
- package/src/sources/yahoo-finance.ts +49 -1
- package/src/state/app/context/index.test.tsx +5 -8
- package/src/state/app/input-capture.ts +12 -0
- package/src/types/broker.ts +1 -0
- package/src/types/config.ts +10 -0
- package/src/types/plugin.ts +28 -0
- package/src/types/trading.ts +2 -0
- package/src/ui/host.tsx +16 -0
- package/src/ui/index.tsx +2 -0
- package/src/utils/cli-output.ts +6 -0
- package/src/utils/financial-statements.ts +23 -0
- package/src/utils/search-focus-navigation.ts +24 -0
- package/src/version.ts +1 -1
- 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.
|
|
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
|
|
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
package/package.json
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
|
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
|
-
<
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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;
|
|
@@ -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
|
+
];
|