opencandle 0.5.0 → 0.7.0
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 +170 -186
- package/dist/analysts/contracts.d.ts +1 -3
- package/dist/analysts/contracts.js +1 -11
- package/dist/analysts/contracts.js.map +1 -1
- package/dist/analysts/orchestrator.d.ts +1 -3
- package/dist/analysts/orchestrator.js +1 -26
- package/dist/analysts/orchestrator.js.map +1 -1
- package/dist/cli.js +66 -7
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +13 -3
- package/dist/config.js +25 -5
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/infra/cache.d.ts +8 -11
- package/dist/infra/cache.js +17 -15
- package/dist/infra/cache.js.map +1 -1
- package/dist/infra/http-client.d.ts +4 -1
- package/dist/infra/http-client.js +59 -6
- package/dist/infra/http-client.js.map +1 -1
- package/dist/infra/index.d.ts +2 -3
- package/dist/infra/index.js +2 -3
- package/dist/infra/index.js.map +1 -1
- package/dist/infra/native-dependencies.js +2 -2
- package/dist/infra/native-dependencies.js.map +1 -1
- package/dist/infra/node-version.js.map +1 -1
- package/dist/infra/opencandle-paths.d.ts +0 -3
- package/dist/infra/opencandle-paths.js +4 -11
- package/dist/infra/opencandle-paths.js.map +1 -1
- package/dist/infra/rate-limiter.js +12 -9
- package/dist/infra/rate-limiter.js.map +1 -1
- package/dist/market-state/alert-conditions.d.ts +34 -0
- package/dist/market-state/alert-conditions.js +23 -0
- package/dist/market-state/alert-conditions.js.map +1 -0
- package/dist/market-state/alert-runner.d.ts +55 -0
- package/dist/market-state/alert-runner.js +634 -0
- package/dist/market-state/alert-runner.js.map +1 -0
- package/dist/market-state/daily-report.d.ts +26 -0
- package/dist/market-state/daily-report.js +179 -0
- package/dist/market-state/daily-report.js.map +1 -0
- package/dist/market-state/local-automation-service.d.ts +25 -0
- package/dist/market-state/local-automation-service.js +119 -0
- package/dist/market-state/local-automation-service.js.map +1 -0
- package/dist/market-state/notification-delivery.d.ts +14 -0
- package/dist/market-state/notification-delivery.js +139 -0
- package/dist/market-state/notification-delivery.js.map +1 -0
- package/dist/market-state/resolve-for-mutation.d.ts +10 -0
- package/dist/market-state/resolve-for-mutation.js +15 -0
- package/dist/market-state/resolve-for-mutation.js.map +1 -0
- package/dist/market-state/resolve.d.ts +14 -0
- package/dist/market-state/resolve.js +89 -0
- package/dist/market-state/resolve.js.map +1 -0
- package/dist/market-state/service.d.ts +527 -0
- package/dist/market-state/service.js +1099 -0
- package/dist/market-state/service.js.map +1 -0
- package/dist/memory/index.d.ts +7 -7
- package/dist/memory/index.js +6 -6
- package/dist/memory/index.js.map +1 -1
- package/dist/memory/manager.js +11 -11
- package/dist/memory/manager.js.map +1 -1
- package/dist/memory/retrieval.js +7 -4
- package/dist/memory/retrieval.js.map +1 -1
- package/dist/memory/sqlite.js +385 -3
- package/dist/memory/sqlite.js.map +1 -1
- package/dist/memory/storage.js +1 -2
- package/dist/memory/storage.js.map +1 -1
- package/dist/memory/tool-defaults.js +64 -28
- package/dist/memory/tool-defaults.js.map +1 -1
- package/dist/memory/types.js.map +1 -1
- package/dist/monitor.d.ts +2 -0
- package/dist/monitor.js +104 -0
- package/dist/monitor.js.map +1 -0
- package/dist/onboarding/connect.d.ts +2 -2
- package/dist/onboarding/connect.js +13 -8
- package/dist/onboarding/connect.js.map +1 -1
- package/dist/onboarding/credential-interceptor.js +1 -1
- package/dist/onboarding/credential-interceptor.js.map +1 -1
- package/dist/onboarding/degradation-accumulator.js +1 -3
- package/dist/onboarding/degradation-accumulator.js.map +1 -1
- package/dist/onboarding/provider-status.d.ts +48 -0
- package/dist/onboarding/provider-status.js +285 -0
- package/dist/onboarding/provider-status.js.map +1 -0
- package/dist/onboarding/providers.d.ts +85 -8
- package/dist/onboarding/providers.js +83 -18
- package/dist/onboarding/providers.js.map +1 -1
- package/dist/onboarding/state.d.ts +1 -0
- package/dist/onboarding/state.js +5 -0
- package/dist/onboarding/state.js.map +1 -1
- package/dist/onboarding/tool-helpers.js +1 -1
- package/dist/onboarding/tool-helpers.js.map +1 -1
- package/dist/onboarding/tool-tags.d.ts +12 -1
- package/dist/onboarding/tool-tags.js +37 -5
- package/dist/onboarding/tool-tags.js.map +1 -1
- package/dist/onboarding/validation.d.ts +2 -2
- package/dist/onboarding/validation.js +1 -1
- package/dist/onboarding/validation.js.map +1 -1
- package/dist/pi/opencandle-extension.d.ts +8 -0
- package/dist/pi/opencandle-extension.js +502 -42
- package/dist/pi/opencandle-extension.js.map +1 -1
- package/dist/pi/session.d.ts +1 -1
- package/dist/pi/session.js +3 -1
- package/dist/pi/session.js.map +1 -1
- package/dist/pi/setup.js +8 -3
- package/dist/pi/setup.js.map +1 -1
- package/dist/pi/tool-adapter.d.ts +4 -1
- package/dist/pi/tool-adapter.js +10 -6
- package/dist/pi/tool-adapter.js.map +1 -1
- package/dist/prompts/context-builder.d.ts +1 -1
- package/dist/prompts/context-builder.js +20 -7
- package/dist/prompts/context-builder.js.map +1 -1
- package/dist/prompts/policy-cards.d.ts +1 -1
- package/dist/prompts/policy-cards.js +2 -2
- package/dist/prompts/policy-cards.js.map +1 -1
- package/dist/prompts/sections.d.ts +1 -1
- package/dist/prompts/symbol-preflight.d.ts +20 -0
- package/dist/prompts/symbol-preflight.js +49 -0
- package/dist/prompts/symbol-preflight.js.map +1 -0
- package/dist/prompts/workflow-prompts.d.ts +1 -1
- package/dist/prompts/workflow-prompts.js +54 -16
- package/dist/prompts/workflow-prompts.js.map +1 -1
- package/dist/providers/alpha-vantage.d.ts +1 -1
- package/dist/providers/alpha-vantage.js +26 -7
- package/dist/providers/alpha-vantage.js.map +1 -1
- package/dist/providers/coingecko.js +1 -1
- package/dist/providers/coingecko.js.map +1 -1
- package/dist/providers/errors.d.ts +5 -0
- package/dist/providers/errors.js +11 -0
- package/dist/providers/errors.js.map +1 -0
- package/dist/providers/exa-search.d.ts +2 -2
- package/dist/providers/exa-search.js +19 -11
- package/dist/providers/exa-search.js.map +1 -1
- package/dist/providers/external-tool-error.d.ts +10 -0
- package/dist/providers/external-tool-error.js +21 -0
- package/dist/providers/external-tool-error.js.map +1 -0
- package/dist/providers/fear-greed.js +1 -1
- package/dist/providers/fear-greed.js.map +1 -1
- package/dist/providers/finnhub.js +3 -5
- package/dist/providers/finnhub.js.map +1 -1
- package/dist/providers/fred.js +2 -2
- package/dist/providers/fred.js.map +1 -1
- package/dist/providers/index.d.ts +7 -6
- package/dist/providers/index.js +6 -5
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/reddit-cli.d.ts +36 -0
- package/dist/providers/reddit-cli.js +201 -0
- package/dist/providers/reddit-cli.js.map +1 -0
- package/dist/providers/reddit.d.ts +1 -1
- package/dist/providers/reddit.js +9 -37
- package/dist/providers/reddit.js.map +1 -1
- package/dist/providers/sec-edgar.d.ts +1 -0
- package/dist/providers/sec-edgar.js +12 -4
- package/dist/providers/sec-edgar.js.map +1 -1
- package/dist/providers/tradingview.d.ts +47 -0
- package/dist/providers/tradingview.js +275 -0
- package/dist/providers/tradingview.js.map +1 -0
- package/dist/providers/twitter-cli.d.ts +40 -0
- package/dist/providers/twitter-cli.js +153 -0
- package/dist/providers/twitter-cli.js.map +1 -0
- package/dist/providers/twitter.d.ts +0 -8
- package/dist/providers/twitter.js +8 -60
- package/dist/providers/twitter.js.map +1 -1
- package/dist/providers/web-search.js +26 -12
- package/dist/providers/web-search.js.map +1 -1
- package/dist/providers/with-fallback.js +4 -2
- package/dist/providers/with-fallback.js.map +1 -1
- package/dist/providers/wrap-provider.d.ts +2 -3
- package/dist/providers/wrap-provider.js +44 -8
- package/dist/providers/wrap-provider.js.map +1 -1
- package/dist/providers/yahoo-finance.d.ts +1 -1
- package/dist/providers/yahoo-finance.js +153 -48
- package/dist/providers/yahoo-finance.js.map +1 -1
- package/dist/routing/classify-intent.d.ts +6 -0
- package/dist/routing/classify-intent.js +78 -7
- package/dist/routing/classify-intent.js.map +1 -1
- package/dist/routing/defaults.d.ts +1 -1
- package/dist/routing/entity-extractor.d.ts +1 -0
- package/dist/routing/entity-extractor.js +234 -29
- package/dist/routing/entity-extractor.js.map +1 -1
- package/dist/routing/fund-symbols.d.ts +2 -0
- package/dist/routing/fund-symbols.js +55 -0
- package/dist/routing/fund-symbols.js.map +1 -0
- package/dist/routing/horizon.d.ts +1 -0
- package/dist/routing/horizon.js +10 -0
- package/dist/routing/horizon.js.map +1 -0
- package/dist/routing/index.d.ts +10 -10
- package/dist/routing/index.js +6 -6
- package/dist/routing/index.js.map +1 -1
- package/dist/routing/planning.d.ts +2 -2
- package/dist/routing/planning.js +65 -34
- package/dist/routing/planning.js.map +1 -1
- package/dist/routing/route-manifest.d.ts +2 -2
- package/dist/routing/route-manifest.js +25 -4
- package/dist/routing/route-manifest.js.map +1 -1
- package/dist/routing/router-llm-client.js.map +1 -1
- package/dist/routing/router-prompt.js +7 -9
- package/dist/routing/router-prompt.js.map +1 -1
- package/dist/routing/router-types.d.ts +1 -0
- package/dist/routing/router.js +137 -22
- package/dist/routing/router.js.map +1 -1
- package/dist/routing/slot-resolver.d.ts +1 -1
- package/dist/routing/slot-resolver.js +2 -4
- package/dist/routing/slot-resolver.js.map +1 -1
- package/dist/routing/symbol-disambiguator.d.ts +11 -0
- package/dist/routing/symbol-disambiguator.js +52 -0
- package/dist/routing/symbol-disambiguator.js.map +1 -0
- package/dist/routing/turn-context.d.ts +1 -1
- package/dist/routing/turn-context.js +1 -1
- package/dist/routing/turn-context.js.map +1 -1
- package/dist/routing/types.d.ts +2 -0
- package/dist/runtime/answer-contracts.d.ts +1 -1
- package/dist/runtime/answer-contracts.js +48 -9
- package/dist/runtime/answer-contracts.js.map +1 -1
- package/dist/runtime/artifact-contracts.js.map +1 -1
- package/dist/runtime/planning-evidence.js +47 -26
- package/dist/runtime/planning-evidence.js.map +1 -1
- package/dist/runtime/prompt-step.d.ts +1 -9
- package/dist/runtime/prompt-step.js +0 -10
- package/dist/runtime/prompt-step.js.map +1 -1
- package/dist/runtime/run-context.d.ts +5 -2
- package/dist/runtime/run-context.js +8 -1
- package/dist/runtime/run-context.js.map +1 -1
- package/dist/runtime/session-coordinator.d.ts +13 -5
- package/dist/runtime/session-coordinator.js +160 -20
- package/dist/runtime/session-coordinator.js.map +1 -1
- package/dist/runtime/session-title.d.ts +14 -0
- package/dist/runtime/session-title.js +50 -0
- package/dist/runtime/session-title.js.map +1 -0
- package/dist/runtime/tool-defaults-wrapper.js +7 -5
- package/dist/runtime/tool-defaults-wrapper.js.map +1 -1
- package/dist/runtime/validation.js.map +1 -1
- package/dist/runtime/workflow-events.js.map +1 -1
- package/dist/runtime/workflow-runner.d.ts +3 -3
- package/dist/runtime/workflow-runner.js +1 -1
- package/dist/runtime/workflow-runner.js.map +1 -1
- package/dist/sentiment/adapters/finnhub.d.ts +1 -1
- package/dist/sentiment/adapters/finnhub.js +6 -1
- package/dist/sentiment/adapters/finnhub.js.map +1 -1
- package/dist/sentiment/adapters/reddit.d.ts +2 -2
- package/dist/sentiment/adapters/twitter.d.ts +1 -1
- package/dist/sentiment/adapters/web.d.ts +1 -1
- package/dist/sentiment/index.d.ts +10 -11
- package/dist/sentiment/index.js +10 -20
- package/dist/sentiment/index.js.map +1 -1
- package/dist/sentiment/insights.d.ts +17 -0
- package/dist/sentiment/insights.js +206 -0
- package/dist/sentiment/insights.js.map +1 -0
- package/dist/sentiment/keywords.js +26 -4
- package/dist/sentiment/keywords.js.map +1 -1
- package/dist/sentiment/pipeline.d.ts +2 -2
- package/dist/sentiment/pipeline.js +14 -2
- package/dist/sentiment/pipeline.js.map +1 -1
- package/dist/sentiment/scorer.d.ts +2 -0
- package/dist/sentiment/scorer.js +11 -2
- package/dist/sentiment/scorer.js.map +1 -1
- package/dist/sentiment/store.d.ts +1 -1
- package/dist/sentiment/store.js +1 -1
- package/dist/sentiment/store.js.map +1 -1
- package/dist/sentiment/trends.d.ts +1 -1
- package/dist/sentiment/trends.js.map +1 -1
- package/dist/sentiment/types.d.ts +2 -0
- package/dist/sentiment/types.js.map +1 -1
- package/dist/system-prompt.js +6 -9
- package/dist/system-prompt.js.map +1 -1
- package/dist/tool-kit.d.ts +7 -7
- package/dist/tool-kit.js +4 -4
- package/dist/tool-kit.js.map +1 -1
- package/dist/tools/fundamentals/company-overview.js +11 -6
- package/dist/tools/fundamentals/company-overview.js.map +1 -1
- package/dist/tools/fundamentals/comps.js +18 -9
- package/dist/tools/fundamentals/comps.js.map +1 -1
- package/dist/tools/fundamentals/dcf.js +23 -11
- package/dist/tools/fundamentals/dcf.js.map +1 -1
- package/dist/tools/fundamentals/earnings.js +8 -3
- package/dist/tools/fundamentals/earnings.js.map +1 -1
- package/dist/tools/fundamentals/financials.js +8 -3
- package/dist/tools/fundamentals/financials.js.map +1 -1
- package/dist/tools/fundamentals/sec-filings.js +21 -6
- package/dist/tools/fundamentals/sec-filings.js.map +1 -1
- package/dist/tools/index.d.ts +27 -20
- package/dist/tools/index.js +55 -43
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/interaction/ask-user.js +15 -3
- package/dist/tools/interaction/ask-user.js.map +1 -1
- package/dist/tools/macro/fear-greed.js.map +1 -1
- package/dist/tools/macro/fred-data.d.ts +1 -1
- package/dist/tools/macro/fred-data.js +17 -6
- package/dist/tools/macro/fred-data.js.map +1 -1
- package/dist/tools/market/crypto-history.js +3 -1
- package/dist/tools/market/crypto-history.js.map +1 -1
- package/dist/tools/market/crypto-price.js +3 -1
- package/dist/tools/market/crypto-price.js.map +1 -1
- package/dist/tools/market/screen-stocks.d.ts +18 -0
- package/dist/tools/market/screen-stocks.js +252 -0
- package/dist/tools/market/screen-stocks.js.map +1 -0
- package/dist/tools/market/search-ticker.js +160 -8
- package/dist/tools/market/search-ticker.js.map +1 -1
- package/dist/tools/market/stock-history.d.ts +2 -2
- package/dist/tools/market/stock-history.js +26 -7
- package/dist/tools/market/stock-history.js.map +1 -1
- package/dist/tools/market/stock-quote.js +5 -3
- package/dist/tools/market/stock-quote.js.map +1 -1
- package/dist/tools/options/greeks.js +1 -1
- package/dist/tools/options/greeks.js.map +1 -1
- package/dist/tools/options/option-chain.js +19 -6
- package/dist/tools/options/option-chain.js.map +1 -1
- package/dist/tools/portfolio/alerts.d.ts +15 -0
- package/dist/tools/portfolio/alerts.js +357 -0
- package/dist/tools/portfolio/alerts.js.map +1 -0
- package/dist/tools/portfolio/correlation.d.ts +1 -1
- package/dist/tools/portfolio/correlation.js +33 -13
- package/dist/tools/portfolio/correlation.js.map +1 -1
- package/dist/tools/portfolio/daily-report.d.ts +8 -0
- package/dist/tools/portfolio/daily-report.js +83 -0
- package/dist/tools/portfolio/daily-report.js.map +1 -0
- package/dist/tools/portfolio/holdings-overlap.js +10 -3
- package/dist/tools/portfolio/holdings-overlap.js.map +1 -1
- package/dist/tools/portfolio/notifications.d.ts +7 -0
- package/dist/tools/portfolio/notifications.js +43 -0
- package/dist/tools/portfolio/notifications.js.map +1 -0
- package/dist/tools/portfolio/predictions.d.ts +12 -6
- package/dist/tools/portfolio/predictions.js +337 -87
- package/dist/tools/portfolio/predictions.js.map +1 -1
- package/dist/tools/portfolio/risk-analysis.d.ts +1 -1
- package/dist/tools/portfolio/risk-analysis.js +45 -6
- package/dist/tools/portfolio/risk-analysis.js.map +1 -1
- package/dist/tools/portfolio/tracker.d.ts +4 -3
- package/dist/tools/portfolio/tracker.js +246 -101
- package/dist/tools/portfolio/tracker.js.map +1 -1
- package/dist/tools/portfolio/watchlist.d.ts +6 -4
- package/dist/tools/portfolio/watchlist.js +208 -108
- package/dist/tools/portfolio/watchlist.js.map +1 -1
- package/dist/tools/sentiment/insight-format.d.ts +2 -0
- package/dist/tools/sentiment/insight-format.js +36 -0
- package/dist/tools/sentiment/insight-format.js.map +1 -0
- package/dist/tools/sentiment/query-match.d.ts +3 -0
- package/dist/tools/sentiment/query-match.js +113 -0
- package/dist/tools/sentiment/query-match.js.map +1 -0
- package/dist/tools/sentiment/reddit-sentiment.d.ts +12 -1
- package/dist/tools/sentiment/reddit-sentiment.js +266 -107
- package/dist/tools/sentiment/reddit-sentiment.js.map +1 -1
- package/dist/tools/sentiment/sentiment-summary.d.ts +9 -1
- package/dist/tools/sentiment/sentiment-summary.js +223 -205
- package/dist/tools/sentiment/sentiment-summary.js.map +1 -1
- package/dist/tools/sentiment/sentiment-trend.d.ts +1 -1
- package/dist/tools/sentiment/sentiment-trend.js +12 -2
- package/dist/tools/sentiment/sentiment-trend.js.map +1 -1
- package/dist/tools/sentiment/twitter-sentiment.d.ts +11 -1
- package/dist/tools/sentiment/twitter-sentiment.js +188 -58
- package/dist/tools/sentiment/twitter-sentiment.js.map +1 -1
- package/dist/tools/sentiment/untrusted-text.d.ts +2 -0
- package/dist/tools/sentiment/untrusted-text.js +17 -0
- package/dist/tools/sentiment/untrusted-text.js.map +1 -0
- package/dist/tools/sentiment/web-search.js +9 -13
- package/dist/tools/sentiment/web-search.js.map +1 -1
- package/dist/tools/sentiment/web-sentiment.js +19 -3
- package/dist/tools/sentiment/web-sentiment.js.map +1 -1
- package/dist/tools/technical/backtest.d.ts +1 -1
- package/dist/tools/technical/backtest.js +27 -20
- package/dist/tools/technical/backtest.js.map +1 -1
- package/dist/tools/technical/indicators.js +23 -5
- package/dist/tools/technical/indicators.js.map +1 -1
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.js.map +1 -1
- package/dist/types/market.d.ts +1 -0
- package/dist/types/portfolio.d.ts +14 -4
- package/dist/types/sentiment.d.ts +52 -0
- package/dist/workflows/compare-assets.d.ts +0 -3
- package/dist/workflows/compare-assets.js +20 -11
- package/dist/workflows/compare-assets.js.map +1 -1
- package/dist/workflows/index.d.ts +3 -4
- package/dist/workflows/index.js +3 -3
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/options-screener.d.ts +0 -3
- package/dist/workflows/options-screener.js +4 -11
- package/dist/workflows/options-screener.js.map +1 -1
- package/dist/workflows/portfolio-builder.d.ts +0 -3
- package/dist/workflows/portfolio-builder.js +0 -8
- package/dist/workflows/portfolio-builder.js.map +1 -1
- package/gui/server/ask-user-bridge.ts +1 -1
- package/gui/server/automation-heartbeat.ts +97 -0
- package/gui/server/background-quotes.ts +97 -1
- package/gui/server/chat-event-adapter.ts +32 -10
- package/gui/server/chat-run-session.ts +16 -0
- package/gui/server/invoke-tool.ts +160 -3
- package/gui/server/live-chat-event-adapter.ts +21 -6
- package/gui/server/market-state-api.ts +315 -0
- package/gui/server/model-setup.ts +156 -2
- package/gui/server/private-api-access.ts +62 -0
- package/gui/server/projector.ts +18 -9
- package/gui/server/prompt-observation.ts +4 -7
- package/gui/server/quote-snapshot-store.ts +50 -0
- package/gui/server/server.ts +218 -451
- package/gui/server/session-actions.ts +186 -1
- package/gui/server/shutdown.ts +47 -0
- package/gui/server/tool-invoke-ack.ts +49 -0
- package/gui/server/tool-metadata.ts +101 -24
- package/gui/server/websocket.ts +13 -3
- package/gui/server/writer-lock.ts +6 -2
- package/gui/server/ws-hub.ts +311 -0
- package/gui/shared/chat-events.ts +16 -1
- package/gui/shared/event-reducer.ts +24 -6
- package/gui/web/dist/assets/CatalogOverlay-CgeY5Pkp.js +1 -0
- package/gui/web/dist/assets/index-C6W_2eAn.js +69 -0
- package/gui/web/dist/assets/index-hwbx24a5.css +1 -0
- package/gui/web/dist/index.html +2 -2
- package/package.json +9 -6
- package/src/analysts/contracts.ts +10 -23
- package/src/analysts/orchestrator.ts +8 -43
- package/src/cli.ts +76 -12
- package/src/config.ts +44 -9
- package/src/index.ts +1 -1
- package/src/infra/cache.ts +41 -30
- package/src/infra/http-client.ts +72 -6
- package/src/infra/index.ts +6 -10
- package/src/infra/native-dependencies.ts +8 -3
- package/src/infra/node-version.ts +3 -1
- package/src/infra/opencandle-paths.ts +3 -14
- package/src/infra/rate-limiter.ts +22 -19
- package/src/market-state/alert-conditions.ts +82 -0
- package/src/market-state/alert-runner.ts +863 -0
- package/src/market-state/daily-report.ts +247 -0
- package/src/market-state/local-automation-service.ts +162 -0
- package/src/market-state/notification-delivery.ts +158 -0
- package/src/market-state/resolve-for-mutation.ts +24 -0
- package/src/market-state/resolve.ts +112 -0
- package/src/market-state/service.ts +2344 -0
- package/src/memory/index.ts +7 -7
- package/src/memory/manager.ts +14 -16
- package/src/memory/retrieval.ts +8 -7
- package/src/memory/sqlite.ts +407 -6
- package/src/memory/storage.ts +5 -15
- package/src/memory/tool-defaults.ts +60 -39
- package/src/memory/types.ts +3 -3
- package/src/monitor.ts +121 -0
- package/src/onboarding/connect.ts +24 -31
- package/src/onboarding/credential-interceptor.ts +3 -15
- package/src/onboarding/degradation-accumulator.ts +1 -3
- package/src/onboarding/provider-status.ts +410 -0
- package/src/onboarding/providers.ts +144 -45
- package/src/onboarding/state.ts +13 -15
- package/src/onboarding/tool-helpers.ts +2 -9
- package/src/onboarding/tool-tags.ts +51 -8
- package/src/onboarding/validation.ts +16 -22
- package/src/pi/opencandle-extension.ts +643 -101
- package/src/pi/session.ts +7 -5
- package/src/pi/setup.ts +61 -43
- package/src/pi/tool-adapter.ts +19 -6
- package/src/prompts/context-builder.ts +24 -13
- package/src/prompts/policy-cards.ts +3 -3
- package/src/prompts/sections.ts +1 -1
- package/src/prompts/symbol-preflight.ts +80 -0
- package/src/prompts/workflow-prompts.ts +77 -28
- package/src/providers/alpha-vantage.ts +58 -39
- package/src/providers/coingecko.ts +2 -5
- package/src/providers/errors.ts +9 -0
- package/src/providers/exa-search.ts +24 -22
- package/src/providers/external-tool-error.ts +20 -0
- package/src/providers/fear-greed.ts +1 -1
- package/src/providers/finnhub.ts +7 -6
- package/src/providers/fred.ts +3 -3
- package/src/providers/index.ts +14 -6
- package/src/providers/reddit-cli.ts +317 -0
- package/src/providers/reddit.ts +14 -59
- package/src/providers/sec-edgar.ts +20 -6
- package/src/providers/tradingview.ts +399 -0
- package/src/providers/twitter-cli.ts +233 -0
- package/src/providers/twitter.ts +8 -79
- package/src/providers/web-search.ts +30 -20
- package/src/providers/with-fallback.ts +8 -7
- package/src/providers/wrap-provider.ts +49 -10
- package/src/providers/yahoo-finance.ts +204 -66
- package/src/routing/classify-intent.ts +101 -10
- package/src/routing/defaults.ts +1 -1
- package/src/routing/entity-extractor.ts +287 -38
- package/src/routing/fund-symbols.ts +58 -0
- package/src/routing/horizon.ts +7 -0
- package/src/routing/index.ts +48 -48
- package/src/routing/planning.ts +145 -53
- package/src/routing/route-manifest.ts +37 -15
- package/src/routing/router-llm-client.ts +4 -4
- package/src/routing/router-prompt.ts +15 -19
- package/src/routing/router-types.ts +2 -5
- package/src/routing/router.ts +251 -53
- package/src/routing/slot-resolver.ts +34 -11
- package/src/routing/symbol-disambiguator.ts +72 -0
- package/src/routing/turn-context.ts +6 -9
- package/src/routing/types.ts +2 -0
- package/src/runtime/answer-contracts.ts +105 -45
- package/src/runtime/artifact-contracts.ts +2 -1
- package/src/runtime/planning-evidence.ts +157 -66
- package/src/runtime/prompt-step.ts +1 -16
- package/src/runtime/run-context.ts +12 -2
- package/src/runtime/session-coordinator.ts +238 -63
- package/src/runtime/session-title.ts +60 -0
- package/src/runtime/tool-defaults-wrapper.ts +13 -5
- package/src/runtime/validation.ts +1 -4
- package/src/runtime/workflow-events.ts +7 -7
- package/src/runtime/workflow-runner.ts +5 -11
- package/src/sentiment/adapters/finnhub.ts +7 -2
- package/src/sentiment/adapters/reddit.ts +2 -2
- package/src/sentiment/adapters/twitter.ts +1 -1
- package/src/sentiment/adapters/web.ts +1 -1
- package/src/sentiment/index.ts +17 -26
- package/src/sentiment/insights.ts +269 -0
- package/src/sentiment/keywords.ts +26 -4
- package/src/sentiment/pipeline.ts +28 -5
- package/src/sentiment/scorer.ts +13 -2
- package/src/sentiment/store.ts +2 -2
- package/src/sentiment/trends.ts +9 -3
- package/src/sentiment/types.ts +8 -4
- package/src/system-prompt.ts +6 -9
- package/src/tool-kit.ts +10 -9
- package/src/tools/fundamentals/company-overview.ts +19 -9
- package/src/tools/fundamentals/comps.ts +68 -55
- package/src/tools/fundamentals/dcf.ts +145 -95
- package/src/tools/fundamentals/earnings.ts +16 -6
- package/src/tools/fundamentals/financials.ts +16 -7
- package/src/tools/fundamentals/sec-filings.ts +37 -16
- package/src/tools/index.ts +56 -43
- package/src/tools/interaction/ask-user.ts +22 -10
- package/src/tools/macro/fear-greed.ts +1 -1
- package/src/tools/macro/fred-data.ts +58 -46
- package/src/tools/market/crypto-history.ts +8 -3
- package/src/tools/market/crypto-price.ts +6 -6
- package/src/tools/market/screen-stocks.ts +279 -0
- package/src/tools/market/search-ticker.ts +218 -17
- package/src/tools/market/stock-history.ts +37 -12
- package/src/tools/market/stock-quote.ts +10 -7
- package/src/tools/options/greeks.ts +5 -5
- package/src/tools/options/option-chain.ts +41 -17
- package/src/tools/portfolio/alerts.ts +457 -0
- package/src/tools/portfolio/correlation.ts +47 -20
- package/src/tools/portfolio/daily-report.ts +101 -0
- package/src/tools/portfolio/holdings-overlap.ts +31 -15
- package/src/tools/portfolio/notifications.ts +45 -0
- package/src/tools/portfolio/predictions.ts +406 -106
- package/src/tools/portfolio/risk-analysis.ts +46 -7
- package/src/tools/portfolio/tracker.ts +270 -109
- package/src/tools/portfolio/watchlist.ts +250 -121
- package/src/tools/sentiment/insight-format.ts +50 -0
- package/src/tools/sentiment/query-match.ts +117 -0
- package/src/tools/sentiment/reddit-sentiment.ts +360 -121
- package/src/tools/sentiment/sentiment-summary.ts +302 -235
- package/src/tools/sentiment/sentiment-trend.ts +24 -7
- package/src/tools/sentiment/twitter-sentiment.ts +264 -73
- package/src/tools/sentiment/untrusted-text.ts +21 -0
- package/src/tools/sentiment/web-search.ts +21 -18
- package/src/tools/sentiment/web-sentiment.ts +30 -10
- package/src/tools/technical/backtest.ts +32 -22
- package/src/tools/technical/indicators.ts +39 -14
- package/src/types/index.ts +8 -3
- package/src/types/market.ts +1 -0
- package/src/types/portfolio.ts +14 -4
- package/src/types/sentiment.ts +61 -2
- package/src/workflows/compare-assets.ts +33 -21
- package/src/workflows/index.ts +3 -4
- package/src/workflows/options-screener.ts +27 -29
- package/src/workflows/portfolio-builder.ts +34 -27
- package/dist/infra/browser.d.ts +0 -35
- package/dist/infra/browser.js +0 -103
- package/dist/infra/browser.js.map +0 -1
- package/dist/tools/interaction/twitter-login.d.ts +0 -8
- package/dist/tools/interaction/twitter-login.js +0 -77
- package/dist/tools/interaction/twitter-login.js.map +0 -1
- package/dist/workflows/types.d.ts +0 -4
- package/dist/workflows/types.js +0 -2
- package/dist/workflows/types.js.map +0 -1
- package/gui/web/dist/assets/CatalogOverlay-Bmp6Knu7.js +0 -1
- package/gui/web/dist/assets/index-Bxt9QpLX.css +0 -1
- package/gui/web/dist/assets/index-CZ9DHZYy.js +0 -67
- package/src/infra/browser.ts +0 -111
- package/src/tools/interaction/twitter-login.ts +0 -93
- package/src/workflows/types.ts +0 -4
package/gui/server/server.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
1
2
|
import { createReadStream, existsSync } from "node:fs";
|
|
2
3
|
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
|
3
4
|
import { extname, join, resolve } from "node:path";
|
|
@@ -6,45 +7,52 @@ import {
|
|
|
6
7
|
AuthStorage,
|
|
7
8
|
createAgentSessionRuntime,
|
|
8
9
|
createAgentSessionServices,
|
|
9
|
-
type AgentSession,
|
|
10
10
|
getAgentDir,
|
|
11
11
|
ModelRegistry,
|
|
12
12
|
SessionManager,
|
|
13
13
|
SettingsManager,
|
|
14
14
|
} from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { createOpenCandleSession } from "../../src/index.js";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
16
|
+
import { probeProviderStatus } from "../../src/onboarding/provider-status.js";
|
|
17
|
+
import type { ChatEvent } from "../shared/chat-events.js";
|
|
18
|
+
import { createAskUserBridge } from "./ask-user-bridge.js";
|
|
18
19
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
import { invokeToolFromUi } from "./invoke-tool.js";
|
|
28
|
-
import { buildCatalog, setToolEnabled } from "./tool-metadata.js";
|
|
29
|
-
import { deleteSessionFile, renameSessionFile } from "./session-actions.js";
|
|
30
|
-
import { acquireWriterLock, refreshWriterLock, releaseWriterLock } from "./writer-lock.js";
|
|
20
|
+
createLocalAutomationHeartbeat,
|
|
21
|
+
normalizeAutomationHeartbeatMs,
|
|
22
|
+
} from "./automation-heartbeat.js";
|
|
23
|
+
import {
|
|
24
|
+
type BackgroundQuotePoller,
|
|
25
|
+
BackgroundQuoteRefreshes,
|
|
26
|
+
createBackgroundQuotePoller,
|
|
27
|
+
} from "./background-quotes.js";
|
|
31
28
|
import { sessionEntriesToChatEvents } from "./chat-event-adapter.js";
|
|
29
|
+
import { chatRunSessionConflict } from "./chat-run-session.js";
|
|
30
|
+
import { createInitialGuiSessionManager } from "./gui-session-manager.js";
|
|
31
|
+
import { createToolInvokeController } from "./invoke-tool.js";
|
|
32
32
|
import { createLiveChatEventAdapter } from "./live-chat-event-adapter.js";
|
|
33
|
-
import { waitForNewEntryId, waitForSessionTurnSettlement } from "./session-entry-wait.js";
|
|
34
33
|
import {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
} from "./
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
43
|
-
import
|
|
34
|
+
buildMarketStateQuoteSnapshot,
|
|
35
|
+
buildMarketStateSnapshot,
|
|
36
|
+
searchInstrumentCandidates,
|
|
37
|
+
} from "./market-state-api.js";
|
|
38
|
+
import { buildModelSetupState, createModelSetupController } from "./model-setup.js";
|
|
39
|
+
import { isTrustedPrivateApiRequest, privateApiCookieHeader } from "./private-api-access.js";
|
|
40
|
+
import { createPromptObservation, observePromptEvent } from "./prompt-observation.js";
|
|
41
|
+
import { QuoteSnapshotStore } from "./quote-snapshot-store.js";
|
|
42
|
+
import { createSessionActionsController, promptAndSettle } from "./session-actions.js";
|
|
43
|
+
import { waitForNewEntryId } from "./session-entry-wait.js";
|
|
44
|
+
import { createGracefulShutdown } from "./shutdown.js";
|
|
45
|
+
import { acquireWriterLock, refreshWriterLock, releaseWriterLock } from "./writer-lock.js";
|
|
46
|
+
import { createWsHub, type WsHub } from "./ws-hub.js";
|
|
44
47
|
|
|
45
48
|
const cwd = process.cwd();
|
|
46
49
|
const host = process.env.OPENCANDLE_GUI_HOST ?? "127.0.0.1";
|
|
47
50
|
const port = Number(process.env.OPENCANDLE_GUI_PORT ?? 14567);
|
|
51
|
+
const automationHeartbeatMs = normalizeAutomationHeartbeatMs(
|
|
52
|
+
process.env.OPENCANDLE_AUTOMATION_HEARTBEAT_MS,
|
|
53
|
+
);
|
|
54
|
+
const allowRemotePrivateApi = process.env.OPENCANDLE_GUI_ALLOW_REMOTE_PRIVATE_API === "1";
|
|
55
|
+
const privateApiSessionToken = randomBytes(32).toString("base64url");
|
|
48
56
|
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
49
57
|
const webDist = resolve(__dirname, "../web/dist");
|
|
50
58
|
|
|
@@ -56,8 +64,10 @@ const initialSessionManager = createInitialGuiSessionManager(cwd);
|
|
|
56
64
|
let sessionManager = initialSessionManager;
|
|
57
65
|
const sessionDir = sessionManager.getSessionDir();
|
|
58
66
|
const lockResult = await acquireWriterLock(sessionDir, "gui");
|
|
67
|
+
let wsHub: WsHub;
|
|
68
|
+
let quotePoller: BackgroundQuotePoller;
|
|
59
69
|
const askUserBridge = createAskUserBridge({
|
|
60
|
-
broadcast,
|
|
70
|
+
broadcast: (message) => wsHub.broadcast(message),
|
|
61
71
|
getSessionId: () => sessionManager.getSessionId(),
|
|
62
72
|
});
|
|
63
73
|
const runtime = await createAgentSessionRuntime(
|
|
@@ -83,18 +93,71 @@ const runtime = await createAgentSessionRuntime(
|
|
|
83
93
|
{ cwd, agentDir, sessionManager },
|
|
84
94
|
);
|
|
85
95
|
let session = runtime.session;
|
|
86
|
-
const clients = new Set<WsClient>();
|
|
87
96
|
const heartbeat = setInterval(() => refreshWriterLock(sessionDir), 5000);
|
|
88
97
|
const backgroundQuoteRefreshes = new BackgroundQuoteRefreshes();
|
|
89
|
-
|
|
90
|
-
|
|
98
|
+
const quoteSnapshotStore = new QuoteSnapshotStore(() => buildMarketStateQuoteSnapshot());
|
|
99
|
+
quotePoller = createBackgroundQuotePoller({
|
|
100
|
+
getClientCount: () => wsHub.getClientCount(),
|
|
101
|
+
getSessionManager: () => sessionManager,
|
|
102
|
+
refreshes: backgroundQuoteRefreshes,
|
|
103
|
+
broadcastState: () => wsHub.broadcastState(),
|
|
104
|
+
});
|
|
105
|
+
const localAutomationHeartbeat = createLocalAutomationHeartbeat({
|
|
106
|
+
role: lockResult.role,
|
|
107
|
+
getSessionId: () => sessionManager.getSessionId(),
|
|
108
|
+
intervalMs: automationHeartbeatMs,
|
|
109
|
+
});
|
|
110
|
+
const modelSetupController = createModelSetupController({
|
|
111
|
+
role: lockResult.role,
|
|
112
|
+
getSession: () => session,
|
|
113
|
+
getSessionManager: () => sessionManager,
|
|
114
|
+
broadcastState: () => wsHub.broadcastState(),
|
|
115
|
+
});
|
|
116
|
+
const toolInvokeController = createToolInvokeController({
|
|
117
|
+
role: lockResult.role,
|
|
118
|
+
getSessionManager: () => sessionManager,
|
|
119
|
+
broadcastState: () => wsHub.broadcastState(),
|
|
120
|
+
onMarketStateChanged: () => quoteSnapshotStore.invalidate(),
|
|
121
|
+
askUserHandler: askUserBridge.ask,
|
|
122
|
+
});
|
|
123
|
+
const sessionActionsController = createSessionActionsController({
|
|
124
|
+
role: lockResult.role,
|
|
125
|
+
cwd,
|
|
126
|
+
sessionDir,
|
|
127
|
+
getSession: () => session,
|
|
128
|
+
getSessionManager: () => sessionManager,
|
|
129
|
+
getModelSetupState: () => modelSetupController.buildCurrentModelSetupState(),
|
|
130
|
+
askUserBridge,
|
|
131
|
+
runtime,
|
|
132
|
+
sendBoot: (client) => wsHub.sendBoot(client),
|
|
133
|
+
broadcastState: () => wsHub.broadcastState(),
|
|
134
|
+
broadcastSessions: () => wsHub.broadcastSessions(),
|
|
135
|
+
});
|
|
136
|
+
wsHub = createWsHub({
|
|
137
|
+
role: lockResult.role,
|
|
138
|
+
lock: lockResult.lock,
|
|
139
|
+
cwd,
|
|
140
|
+
sessionDir,
|
|
141
|
+
getSession: () => session,
|
|
142
|
+
getSessionManager: () => sessionManager,
|
|
143
|
+
backgroundQuoteRefreshes,
|
|
144
|
+
askUserBridge,
|
|
145
|
+
modelSetupController,
|
|
146
|
+
toolInvokeController,
|
|
147
|
+
sessionActionsController,
|
|
148
|
+
onClientCountChanged: () => quotePoller.updatePoller(),
|
|
149
|
+
isTrustedRequest: (req) =>
|
|
150
|
+
isTrustedPrivateApiRequest(req.headers, privateApiSessionToken, req.socket.remoteAddress, {
|
|
151
|
+
allowRemote: allowRemotePrivateApi,
|
|
152
|
+
}),
|
|
153
|
+
});
|
|
91
154
|
|
|
92
|
-
let unsubscribeSession = subscribeToSessionEvents();
|
|
155
|
+
let unsubscribeSession = wsHub.subscribeToSessionEvents();
|
|
93
156
|
runtime.setRebindSession(async (nextSession) => {
|
|
94
157
|
unsubscribeSession();
|
|
95
158
|
session = nextSession;
|
|
96
159
|
sessionManager = nextSession.sessionManager;
|
|
97
|
-
unsubscribeSession = subscribeToSessionEvents();
|
|
160
|
+
unsubscribeSession = wsHub.subscribeToSessionEvents();
|
|
98
161
|
});
|
|
99
162
|
|
|
100
163
|
const server = createServer((req, res) => {
|
|
@@ -109,19 +172,26 @@ async function handleHttpRequest(req: IncomingMessage, res: ServerResponse): Pro
|
|
|
109
172
|
}
|
|
110
173
|
|
|
111
174
|
if (url.pathname === "/api/bootstrap" && req.method === "GET") {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
175
|
+
if (!allowTrustedGuiRequest(req, res, "Bootstrap API")) return;
|
|
176
|
+
writeJson(res, await wsHub.buildBootstrapPayload());
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (url.pathname === "/api/session/new" && req.method === "POST") {
|
|
181
|
+
if (!allowTrustedGuiRequest(req, res, "Session API")) return;
|
|
182
|
+
if (lockResult.role !== "writer") {
|
|
183
|
+
writeJson(res, { error: "Read-only follower mode" }, 409);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
await sessionActionsController.handleNewSession();
|
|
187
|
+
wsHub.broadcastState();
|
|
188
|
+
wsHub.broadcastSessions();
|
|
189
|
+
writeJson(res, await wsHub.buildBootstrapPayload());
|
|
121
190
|
return;
|
|
122
191
|
}
|
|
123
192
|
|
|
124
193
|
if (url.pathname === "/api/sessions" && req.method === "GET") {
|
|
194
|
+
if (!allowTrustedGuiRequest(req, res, "Session API")) return;
|
|
125
195
|
writeJson(res, {
|
|
126
196
|
currentSessionId: sessionManager.getSessionId(),
|
|
127
197
|
role: lockResult.role,
|
|
@@ -131,15 +201,51 @@ async function handleHttpRequest(req: IncomingMessage, res: ServerResponse): Pro
|
|
|
131
201
|
}
|
|
132
202
|
|
|
133
203
|
if (url.pathname === "/api/session/events" && req.method === "GET") {
|
|
204
|
+
if (!allowTrustedGuiRequest(req, res, "Session API")) return;
|
|
134
205
|
writeJson(res, {
|
|
135
206
|
sessionId: sessionManager.getSessionId(),
|
|
136
207
|
role: lockResult.role,
|
|
137
|
-
events: currentChatEvents(),
|
|
208
|
+
events: wsHub.currentChatEvents(),
|
|
138
209
|
});
|
|
139
210
|
return;
|
|
140
211
|
}
|
|
141
212
|
|
|
213
|
+
if (url.pathname === "/api/market-state" && req.method === "GET") {
|
|
214
|
+
if (!allowTrustedGuiRequest(req, res, "Market-state API")) return;
|
|
215
|
+
writeJson(res, buildMarketStateSnapshot());
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (url.pathname === "/api/market-state/quotes" && req.method === "GET") {
|
|
220
|
+
if (!allowTrustedGuiRequest(req, res, "Market-state API")) return;
|
|
221
|
+
writeJson(res, await quoteSnapshotStore.get());
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (url.pathname === "/api/instruments/search" && req.method === "GET") {
|
|
226
|
+
if (!allowTrustedGuiRequest(req, res, "Market-state API")) return;
|
|
227
|
+
writeJson(res, await searchInstrumentCandidates(url.searchParams.get("q") ?? ""));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (url.pathname === "/api/diagnostics/twitter-cli" && req.method === "GET") {
|
|
232
|
+
if (!allowTrustedGuiRequest(req, res, "Diagnostics API")) return;
|
|
233
|
+
const mode = url.searchParams.get("mode") === "session" ? "session" : "install";
|
|
234
|
+
const force = url.searchParams.get("force") === "1";
|
|
235
|
+
writeJson(res, await probeProviderStatus("twitter", { mode, force }));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (url.pathname === "/api/diagnostics/reddit-cli" && req.method === "GET") {
|
|
240
|
+
if (!allowTrustedGuiRequest(req, res, "Diagnostics API")) return;
|
|
241
|
+
const mode = url.searchParams.get("mode") === "session" ? "session" : "install";
|
|
242
|
+
const force = url.searchParams.get("force") === "1";
|
|
243
|
+
writeJson(res, await probeProviderStatus("reddit", { mode, force }));
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
142
247
|
if (url.pathname === "/api/chat/run" && req.method === "POST") {
|
|
248
|
+
if (!allowTrustedGuiRequest(req, res, "Chat run API")) return;
|
|
143
249
|
await handleSseChatRun(req, res);
|
|
144
250
|
return;
|
|
145
251
|
}
|
|
@@ -149,7 +255,7 @@ async function handleHttpRequest(req: IncomingMessage, res: ServerResponse): Pro
|
|
|
149
255
|
if (!path.startsWith(webDist) || !existsSync(path)) {
|
|
150
256
|
const fallback = resolve(join(webDist, "index.html"));
|
|
151
257
|
if (!extname(requested) && fallback.startsWith(webDist) && existsSync(fallback)) {
|
|
152
|
-
res.writeHead(200,
|
|
258
|
+
res.writeHead(200, privateGuiHeaders("text/html; charset=utf-8"));
|
|
153
259
|
createReadStream(fallback).pipe(res);
|
|
154
260
|
return;
|
|
155
261
|
}
|
|
@@ -157,298 +263,43 @@ async function handleHttpRequest(req: IncomingMessage, res: ServerResponse): Pro
|
|
|
157
263
|
return;
|
|
158
264
|
}
|
|
159
265
|
|
|
160
|
-
|
|
266
|
+
const type = contentType(path);
|
|
267
|
+
res.writeHead(200, privateGuiHeaders(type));
|
|
161
268
|
createReadStream(path).pipe(res);
|
|
162
269
|
}
|
|
163
270
|
|
|
164
|
-
server.on("upgrade", (req, socket) =>
|
|
165
|
-
if (req.url !== "/ws") {
|
|
166
|
-
socket.destroy();
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const client = acceptWebSocket(req, socket);
|
|
171
|
-
clients.add(client);
|
|
172
|
-
client.onClose(() => {
|
|
173
|
-
clients.delete(client);
|
|
174
|
-
updatePoller();
|
|
175
|
-
});
|
|
176
|
-
client.onMessage((message) => void handleClientMessage(client, message));
|
|
177
|
-
sendBoot(client);
|
|
178
|
-
updatePoller();
|
|
179
|
-
});
|
|
271
|
+
server.on("upgrade", (req, socket) => wsHub.handleUpgrade(req, socket));
|
|
180
272
|
|
|
181
273
|
server.listen(port, host, () => {
|
|
182
274
|
console.log(`OpenCandle GUI listening on http://${host}:${port}`);
|
|
183
275
|
if (host === "0.0.0.0") {
|
|
184
276
|
console.log(`OpenCandle GUI is accepting LAN/Tailscale connections on port ${port}`);
|
|
185
277
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
process.on("SIGINT", shutdown);
|
|
190
|
-
process.on("SIGTERM", shutdown);
|
|
191
|
-
|
|
192
|
-
async function handleClientMessage(client: WsClient, message: unknown): Promise<void> {
|
|
193
|
-
const data = asRecord(message);
|
|
194
|
-
try {
|
|
195
|
-
switch (data.type) {
|
|
196
|
-
case "chat.prompt":
|
|
197
|
-
await handlePrompt(String(data.prompt ?? ""));
|
|
198
|
-
break;
|
|
199
|
-
case "ask_user.answer":
|
|
200
|
-
await handleAskUserAnswer(String(data.id ?? ""), data.answer);
|
|
201
|
-
break;
|
|
202
|
-
case "ask_user.cancel":
|
|
203
|
-
await handleAskUserCancel(String(data.id ?? ""));
|
|
204
|
-
break;
|
|
205
|
-
case "tool.invoke":
|
|
206
|
-
await handleToolInvoke(String(data.toolName ?? ""), asRecord(data.args));
|
|
207
|
-
break;
|
|
208
|
-
case "tool.enabled":
|
|
209
|
-
if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
|
|
210
|
-
setToolEnabled(String(data.toolName), Boolean(data.enabled));
|
|
211
|
-
broadcast({ type: "catalog", catalog: buildCatalog(), restartRequired: true });
|
|
212
|
-
break;
|
|
213
|
-
case "catalog.refresh":
|
|
214
|
-
client.send({ type: "catalog", catalog: buildCatalog() });
|
|
215
|
-
break;
|
|
216
|
-
case "model.setup.refresh":
|
|
217
|
-
session.modelRegistry.refresh();
|
|
218
|
-
broadcastModelSetup();
|
|
219
|
-
break;
|
|
220
|
-
case "model.setup.save_api_key":
|
|
221
|
-
await handleSaveModelApiKey(String(data.provider ?? ""), String(data.apiKey ?? ""));
|
|
222
|
-
broadcastModelSetup();
|
|
223
|
-
break;
|
|
224
|
-
case "model.setup.select_model":
|
|
225
|
-
await handleSelectModel(String(data.provider ?? ""), String(data.modelId ?? ""));
|
|
226
|
-
broadcastModelSetup();
|
|
227
|
-
break;
|
|
228
|
-
case "provider.save_api_key":
|
|
229
|
-
await handleSaveProviderApiKey(String(data.providerId ?? ""), String(data.apiKey ?? ""));
|
|
230
|
-
broadcast({ type: "catalog", catalog: buildCatalog() });
|
|
231
|
-
break;
|
|
232
|
-
case "session.new":
|
|
233
|
-
if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
|
|
234
|
-
await handleNewSession();
|
|
235
|
-
sendBoot(client);
|
|
236
|
-
broadcastState();
|
|
237
|
-
broadcastSessions();
|
|
238
|
-
break;
|
|
239
|
-
case "session.open":
|
|
240
|
-
if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
|
|
241
|
-
await handleOpenSession(String(data.path ?? ""));
|
|
242
|
-
sendBoot(client);
|
|
243
|
-
broadcastState();
|
|
244
|
-
broadcastSessions();
|
|
245
|
-
break;
|
|
246
|
-
case "session.rename":
|
|
247
|
-
if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
|
|
248
|
-
await handleRenameSession(String(data.path ?? ""), String(data.name ?? ""));
|
|
249
|
-
broadcastState();
|
|
250
|
-
broadcastSessions();
|
|
251
|
-
break;
|
|
252
|
-
case "session.delete":
|
|
253
|
-
if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
|
|
254
|
-
await handleDeleteSession(client, String(data.path ?? ""));
|
|
255
|
-
break;
|
|
256
|
-
}
|
|
257
|
-
} catch (error) {
|
|
258
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
259
|
-
client.send({
|
|
260
|
-
type: "error",
|
|
261
|
-
message,
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
async function handlePrompt(prompt: string): Promise<void> {
|
|
267
|
-
if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
|
|
268
|
-
|
|
269
|
-
const modelSetup = buildCurrentModelSetupState();
|
|
270
|
-
const trimmedPrompt = prompt.trim();
|
|
271
|
-
if (!trimmedPrompt.startsWith("/") && modelSetup.requirement !== "ready") {
|
|
272
|
-
sessionManager.appendMessage({ role: "user", content: prompt, timestamp: Date.now() });
|
|
273
|
-
broadcastState();
|
|
274
|
-
const message =
|
|
275
|
-
modelSetup.requirement === "select_model"
|
|
276
|
-
? "Choose an available model before chat can run. OpenCandle found configured credentials but no active model."
|
|
277
|
-
: "Connect an AI model before chat can run. Paste a Google Gemini, OpenAI, or Anthropic API key in the setup panel.";
|
|
278
|
-
sessionManager.appendCustomMessageEntry(
|
|
279
|
-
"opencandle-model-setup",
|
|
280
|
-
message,
|
|
281
|
-
true,
|
|
282
|
-
{ source: "gui", requirement: modelSetup.requirement },
|
|
283
|
-
);
|
|
284
|
-
broadcastState();
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const beforeIds = new Set(sessionManager.getEntries().map((entry) => entry.id));
|
|
289
|
-
await promptAndSettle(session, prompt, beforeIds);
|
|
290
|
-
broadcastState();
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
async function handleAskUserAnswer(id: string, value: unknown): Promise<void> {
|
|
294
|
-
if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
|
|
295
|
-
const answer = String(value ?? "").trim();
|
|
296
|
-
if (!answer) throw new Error("Answer cannot be empty");
|
|
297
|
-
if (!askUserBridge.answer(id, answer)) throw new Error("Unknown or resolved question");
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
async function handleAskUserCancel(id: string): Promise<void> {
|
|
301
|
-
if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
|
|
302
|
-
if (!askUserBridge.cancel(id)) throw new Error("Unknown or resolved question");
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
async function handleNewSession(): Promise<void> {
|
|
306
|
-
const result = await runtime.newSession();
|
|
307
|
-
if (result.cancelled) throw new Error("Session switch cancelled");
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
async function handleOpenSession(path: string): Promise<void> {
|
|
311
|
-
const sessions = await SessionManager.list(cwd, sessionDir);
|
|
312
|
-
const match = sessions.find((candidate) => candidate.path === path);
|
|
313
|
-
if (!match) throw new Error("Unknown saved session");
|
|
314
|
-
const result = await runtime.switchSession(match.path);
|
|
315
|
-
if (result.cancelled) throw new Error("Session switch cancelled");
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
async function handleRenameSession(path: string, name: string): Promise<void> {
|
|
319
|
-
const nextName = name.trim();
|
|
320
|
-
if (!nextName) throw new Error("Session name cannot be empty");
|
|
321
|
-
if (sessionManager.getSessionFile() === path) {
|
|
322
|
-
sessionManager.appendSessionInfo(nextName);
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
await renameSessionFile(cwd, sessionDir, path, nextName);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
async function handleDeleteSession(client: WsClient, path: string): Promise<void> {
|
|
329
|
-
const deletingCurrent = sessionManager.getSessionFile() === path;
|
|
330
|
-
await deleteSessionFile(cwd, sessionDir, path);
|
|
331
|
-
if (deletingCurrent) {
|
|
332
|
-
await handleNewSession();
|
|
333
|
-
sendBoot(client);
|
|
334
|
-
broadcastState();
|
|
335
|
-
}
|
|
336
|
-
broadcastSessions();
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
async function handleSaveModelApiKey(providerId: string, apiKey: string): Promise<void> {
|
|
340
|
-
if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
|
|
341
|
-
|
|
342
|
-
const provider = modelSetupProviders.find((candidate) => candidate.id === providerId);
|
|
343
|
-
if (!provider) throw new Error(`Unknown model provider: ${providerId}`);
|
|
344
|
-
|
|
345
|
-
const trimmed = apiKey.trim();
|
|
346
|
-
if (!trimmed) throw new Error(`Paste a ${provider.label} API key first.`);
|
|
347
|
-
|
|
348
|
-
session.modelRegistry.authStorage.set(provider.id, { type: "api_key", key: trimmed });
|
|
349
|
-
session.modelRegistry.refresh();
|
|
350
|
-
|
|
351
|
-
const model = findPreferredModel(session.modelRegistry, provider);
|
|
352
|
-
if (!model) {
|
|
353
|
-
throw new Error(`Saved the ${provider.label} key, but no ${provider.label} models are available yet.`);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
await session.setModel(model);
|
|
357
|
-
await session.settingsManager.flush();
|
|
358
|
-
sessionManager.appendCustomMessageEntry(
|
|
359
|
-
"opencandle-model-setup",
|
|
360
|
-
`Connected ${provider.label} and selected ${model.provider}/${model.id}.`,
|
|
361
|
-
true,
|
|
362
|
-
{ source: "gui", provider: provider.id, model: `${model.provider}/${model.id}` },
|
|
363
|
-
);
|
|
364
|
-
broadcastState();
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
async function handleSaveProviderApiKey(providerId: string, apiKey: string): Promise<void> {
|
|
368
|
-
if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
|
|
369
|
-
|
|
370
|
-
const descriptor = PROVIDERS.find((candidate) => candidate.id === providerId);
|
|
371
|
-
if (!descriptor) throw new Error(`Unknown provider: ${providerId}`);
|
|
372
|
-
|
|
373
|
-
if (getCredentialSource(descriptor.id) === "env") {
|
|
374
|
-
throw new Error(
|
|
375
|
-
`${descriptor.displayName} is set via the ${descriptor.envVar} environment variable. Unset it to override here.`,
|
|
376
|
-
);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
const trimmed = apiKey.trim();
|
|
380
|
-
if (!trimmed) throw new Error(`Paste a ${descriptor.displayName} API key first.`);
|
|
381
|
-
|
|
382
|
-
const validation = await validateCredential(descriptor.id as ProviderId, trimmed);
|
|
383
|
-
if (validation.status === "invalid") {
|
|
384
|
-
const statusHint = validation.httpStatus !== undefined ? ` (HTTP ${validation.httpStatus})` : "";
|
|
385
|
-
const messageHint = validation.message ? ` — ${validation.message}` : "";
|
|
386
|
-
throw new Error(
|
|
387
|
-
`${descriptor.displayName} rejected the key${statusHint}${messageHint}. The existing configuration was not changed.`,
|
|
278
|
+
if (allowRemotePrivateApi) {
|
|
279
|
+
console.log(
|
|
280
|
+
"OpenCandle GUI private market-state API accepts cookie-authenticated remote requests.",
|
|
388
281
|
);
|
|
389
282
|
}
|
|
283
|
+
console.log(`Writer role: ${lockResult.role}`);
|
|
284
|
+
localAutomationHeartbeat.start();
|
|
285
|
+
});
|
|
390
286
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
broadcastState();
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
async function handleSelectModel(provider: string, modelId: string): Promise<void> {
|
|
408
|
-
if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
|
|
409
|
-
session.modelRegistry.refresh();
|
|
410
|
-
const model = session.modelRegistry.find(provider, modelId);
|
|
411
|
-
if (!model) throw new Error(`Unknown model: ${provider}/${modelId}`);
|
|
412
|
-
await session.setModel(model);
|
|
413
|
-
await session.settingsManager.flush();
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
async function handleToolInvoke(toolName: string, args: Record<string, unknown>): Promise<void> {
|
|
417
|
-
if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
|
|
418
|
-
const tool = getAllTools().find((candidate) => candidate.name === toolName);
|
|
419
|
-
if (!tool) throw new Error(`Unknown tool: ${toolName}`);
|
|
420
|
-
await invokeToolFromUi(sessionManager, tool, args, "ui");
|
|
421
|
-
broadcastState();
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
function sendBoot(client: WsClient): void {
|
|
425
|
-
const snapshot = buildStateSnapshot();
|
|
426
|
-
client.send({
|
|
427
|
-
type: "boot",
|
|
428
|
-
role: lockResult.role,
|
|
429
|
-
lock: lockResult.lock,
|
|
430
|
-
sessionId: sessionManager.getSessionId(),
|
|
431
|
-
catalog: buildCatalog(),
|
|
432
|
-
modelSetup: buildCurrentModelSetupState(),
|
|
433
|
-
askUserPrompts: askUserBridge.getPrompts(),
|
|
434
|
-
});
|
|
435
|
-
client.send({
|
|
436
|
-
type: "state.snapshot",
|
|
437
|
-
...snapshot,
|
|
438
|
-
});
|
|
439
|
-
void SessionManager.list(cwd, sessionDir).then((sessions) => client.send({ type: "sessions", sessions }));
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function broadcastModelSetup(): void {
|
|
443
|
-
broadcast({ type: "model.setup", modelSetup: buildCurrentModelSetupState() });
|
|
444
|
-
}
|
|
287
|
+
const shutdown = createGracefulShutdown({
|
|
288
|
+
server,
|
|
289
|
+
cleanup: async () => {
|
|
290
|
+
clearInterval(heartbeat);
|
|
291
|
+
quotePoller.stop();
|
|
292
|
+
localAutomationHeartbeat.stop();
|
|
293
|
+
wsHub.closeClients();
|
|
294
|
+
unsubscribeSession();
|
|
295
|
+
releaseWriterLock(sessionDir);
|
|
296
|
+
await runtime.dispose();
|
|
297
|
+
},
|
|
298
|
+
exit: (code) => process.exit(code),
|
|
299
|
+
});
|
|
445
300
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
type: "state.snapshot",
|
|
449
|
-
...buildStateSnapshot(),
|
|
450
|
-
});
|
|
451
|
-
}
|
|
301
|
+
process.once("SIGINT", shutdown);
|
|
302
|
+
process.once("SIGTERM", shutdown);
|
|
452
303
|
|
|
453
304
|
async function handleSseChatRun(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
454
305
|
if (lockResult.role !== "writer") {
|
|
@@ -463,6 +314,15 @@ async function handleSseChatRun(req: IncomingMessage, res: ServerResponse): Prom
|
|
|
463
314
|
return;
|
|
464
315
|
}
|
|
465
316
|
|
|
317
|
+
const sessionConflict = chatRunSessionConflict(
|
|
318
|
+
asRecord(body).sessionId,
|
|
319
|
+
sessionManager.getSessionId(),
|
|
320
|
+
);
|
|
321
|
+
if (sessionConflict) {
|
|
322
|
+
writeJson(res, sessionConflict, 409);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
466
326
|
res.writeHead(200, {
|
|
467
327
|
"content-type": "text/event-stream; charset=utf-8",
|
|
468
328
|
"cache-control": "no-cache, no-transform",
|
|
@@ -474,6 +334,11 @@ async function handleSseChatRun(req: IncomingMessage, res: ServerResponse): Prom
|
|
|
474
334
|
const runSession = session;
|
|
475
335
|
const runSessionManager = sessionManager;
|
|
476
336
|
const sessionId = runSessionManager.getSessionId();
|
|
337
|
+
// Name new sessions by the user's words before any workflow transform
|
|
338
|
+
// replaces the turn text, so the sidebar shows what the user actually asked.
|
|
339
|
+
if (!prompt.startsWith("/") && !runSessionManager.getSessionName()) {
|
|
340
|
+
runSessionManager.appendSessionInfo(prompt.length > 80 ? `${prompt.slice(0, 77)}...` : prompt);
|
|
341
|
+
}
|
|
477
342
|
const beforeEntries = runSessionManager.getEntries();
|
|
478
343
|
const beforeCount = beforeEntries.length;
|
|
479
344
|
const beforeIds = new Set(beforeEntries.map((entry) => entry.id));
|
|
@@ -485,6 +350,7 @@ async function handleSseChatRun(req: IncomingMessage, res: ServerResponse): Prom
|
|
|
485
350
|
sessionId,
|
|
486
351
|
startSeq: seq,
|
|
487
352
|
emit: (event) => writeSse(res, event),
|
|
353
|
+
originalPrompt: prompt,
|
|
488
354
|
});
|
|
489
355
|
const observation = createPromptObservation();
|
|
490
356
|
const unsubscribeLive = runSession.subscribe((event) => {
|
|
@@ -500,16 +366,14 @@ async function handleSseChatRun(req: IncomingMessage, res: ServerResponse): Prom
|
|
|
500
366
|
modelSetup.requirement === "select_model"
|
|
501
367
|
? "Choose an available model before chat can run. OpenCandle found configured credentials but no active model."
|
|
502
368
|
: "Connect an AI model before chat can run. Paste a Google Gemini, OpenAI, or Anthropic API key in the setup panel.";
|
|
503
|
-
runSessionManager.appendCustomMessageEntry(
|
|
504
|
-
"
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
);
|
|
509
|
-
broadcastState();
|
|
369
|
+
runSessionManager.appendCustomMessageEntry("opencandle-model-setup", message, true, {
|
|
370
|
+
source: "gui",
|
|
371
|
+
requirement: modelSetup.requirement,
|
|
372
|
+
});
|
|
373
|
+
wsHub.broadcastState();
|
|
510
374
|
} else {
|
|
511
375
|
await promptAndSettle(runSession, prompt, beforeIds, observation);
|
|
512
|
-
broadcastState();
|
|
376
|
+
wsHub.broadcastState();
|
|
513
377
|
}
|
|
514
378
|
seq = liveAdapter.nextSeq();
|
|
515
379
|
if (seq === liveStartSeq) {
|
|
@@ -517,7 +381,8 @@ async function handleSseChatRun(req: IncomingMessage, res: ServerResponse): Prom
|
|
|
517
381
|
() => runSessionManager.getEntries().map((entry) => entry.id),
|
|
518
382
|
beforeIds,
|
|
519
383
|
);
|
|
520
|
-
const newEntries = runSessionManager
|
|
384
|
+
const newEntries = runSessionManager
|
|
385
|
+
.getEntries()
|
|
521
386
|
.slice(beforeCount)
|
|
522
387
|
.filter((entry) => !beforeIds.has(entry.id));
|
|
523
388
|
const events = sessionEntriesToChatEvents(newEntries, {
|
|
@@ -541,122 +406,33 @@ async function handleSseChatRun(req: IncomingMessage, res: ServerResponse): Prom
|
|
|
541
406
|
}
|
|
542
407
|
}
|
|
543
408
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
beforeIds: Set<string>,
|
|
548
|
-
observation?: PromptObservation,
|
|
549
|
-
): Promise<void> {
|
|
550
|
-
await runSession.prompt(prompt);
|
|
551
|
-
await waitForSessionTurnSettlement(() => ({
|
|
552
|
-
isStreaming: runSession.isStreaming,
|
|
553
|
-
pendingMessageCount: runSession.pendingMessageCount,
|
|
554
|
-
}));
|
|
555
|
-
await waitForNewEntryId(() => runSession.sessionManager.getEntries().map((entry) => entry.id), beforeIds);
|
|
556
|
-
await replayObservedWorkflowPromptIfNeeded(runSession, prompt, observation);
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
async function replayObservedWorkflowPromptIfNeeded(
|
|
560
|
-
runSession: AgentSession,
|
|
561
|
-
originalPrompt: string,
|
|
562
|
-
observation?: PromptObservation,
|
|
563
|
-
): Promise<void> {
|
|
564
|
-
if (!observation) return;
|
|
565
|
-
const replayPrompt = selectReplayPrompt(observation, originalPrompt);
|
|
566
|
-
if (!replayPrompt) return;
|
|
567
|
-
|
|
568
|
-
await runSession.prompt(replayPrompt, {
|
|
569
|
-
expandPromptTemplates: false,
|
|
570
|
-
source: "extension",
|
|
571
|
-
});
|
|
572
|
-
await waitForSessionTurnSettlement(() => ({
|
|
573
|
-
isStreaming: runSession.isStreaming,
|
|
574
|
-
pendingMessageCount: runSession.pendingMessageCount,
|
|
575
|
-
}));
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
function buildStateSnapshot() {
|
|
579
|
-
const sessionId = sessionManager.getSessionId();
|
|
580
|
-
const entries = sessionManager.getEntries();
|
|
581
|
-
return {
|
|
582
|
-
sessionId,
|
|
583
|
-
state: projectDashboard(backgroundQuoteRefreshes.withEntries(entries), sessionId),
|
|
584
|
-
entries,
|
|
585
|
-
events: currentChatEvents(entries),
|
|
586
|
-
};
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
function currentChatEvents(entries = sessionManager.getEntries()): ChatEvent[] {
|
|
590
|
-
return sessionEntriesToChatEvents(entries, {
|
|
591
|
-
sessionId: sessionManager.getSessionId(),
|
|
592
|
-
title: sessionManager.getSessionName(),
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
function broadcastSessions(): void {
|
|
597
|
-
void SessionManager.list(cwd, sessionDir).then((sessions) => broadcast({ type: "sessions", sessions }));
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
function buildCurrentModelSetupState() {
|
|
601
|
-
return buildModelSetupState(session.modelRegistry, session.model);
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
function broadcast(message: unknown): void {
|
|
605
|
-
for (const client of clients) client.send(message);
|
|
409
|
+
function writeJson(res: ServerResponse, value: unknown, status = 200): void {
|
|
410
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
411
|
+
res.end(JSON.stringify(value));
|
|
606
412
|
}
|
|
607
413
|
|
|
608
|
-
function
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
414
|
+
function allowTrustedGuiRequest(req: IncomingMessage, res: ServerResponse, label: string): boolean {
|
|
415
|
+
if (
|
|
416
|
+
isTrustedPrivateApiRequest(req.headers, privateApiSessionToken, req.socket.remoteAddress, {
|
|
417
|
+
allowRemote: allowRemotePrivateApi,
|
|
418
|
+
})
|
|
419
|
+
)
|
|
420
|
+
return true;
|
|
421
|
+
res.writeHead(403, { "content-type": "application/json" });
|
|
422
|
+
res.end(
|
|
423
|
+
JSON.stringify({
|
|
424
|
+
error: `${label} is only available to trusted GUI browser sessions.`,
|
|
425
|
+
}),
|
|
426
|
+
);
|
|
427
|
+
return false;
|
|
613
428
|
}
|
|
614
429
|
|
|
615
|
-
function
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
if (clients.size === 0 && poller) {
|
|
620
|
-
clearInterval(poller);
|
|
621
|
-
poller = null;
|
|
430
|
+
function privateGuiHeaders(contentTypeValue: string): Record<string, string> {
|
|
431
|
+
const headers: Record<string, string> = { "content-type": contentTypeValue };
|
|
432
|
+
if (contentTypeValue.startsWith("text/html")) {
|
|
433
|
+
headers["set-cookie"] = privateApiCookieHeader(privateApiSessionToken);
|
|
622
434
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
async function pollVisibleQuotes(): Promise<void> {
|
|
626
|
-
if (quotePollInFlight) return;
|
|
627
|
-
quotePollInFlight = true;
|
|
628
|
-
try {
|
|
629
|
-
const state = projectDashboard(sessionManager.getEntries(), sessionManager.getSessionId());
|
|
630
|
-
const tool = getAllTools().find((candidate) => candidate.name === "get_stock_quote");
|
|
631
|
-
if (!tool) return;
|
|
632
|
-
for (const row of state.watchlist.filter((item) => item.pinned || item.quote)) {
|
|
633
|
-
const result = await invokeToolFromUi(
|
|
634
|
-
sessionManager,
|
|
635
|
-
tool,
|
|
636
|
-
{ symbol: row.symbol },
|
|
637
|
-
"background",
|
|
638
|
-
{ recordTranscript: false },
|
|
639
|
-
);
|
|
640
|
-
backgroundQuoteRefreshes.upsert({
|
|
641
|
-
symbol: row.symbol,
|
|
642
|
-
toolName: tool.name,
|
|
643
|
-
args: { symbol: row.symbol },
|
|
644
|
-
value: result.result.details,
|
|
645
|
-
content: result.result.content,
|
|
646
|
-
isError: result.isError,
|
|
647
|
-
});
|
|
648
|
-
}
|
|
649
|
-
broadcastState();
|
|
650
|
-
} catch (error) {
|
|
651
|
-
console.warn(`Background quote refresh failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
652
|
-
} finally {
|
|
653
|
-
quotePollInFlight = false;
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
function writeJson(res: ServerResponse, value: unknown): void {
|
|
658
|
-
res.writeHead(200, { "content-type": "application/json" });
|
|
659
|
-
res.end(JSON.stringify(value));
|
|
435
|
+
return headers;
|
|
660
436
|
}
|
|
661
437
|
|
|
662
438
|
function writeSse(res: ServerResponse, event: ChatEvent): void {
|
|
@@ -689,15 +465,6 @@ function contentType(path: string): string {
|
|
|
689
465
|
|
|
690
466
|
function asRecord(value: unknown): Record<string, unknown> {
|
|
691
467
|
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
692
|
-
? value as Record<string, unknown>
|
|
468
|
+
? (value as Record<string, unknown>)
|
|
693
469
|
: {};
|
|
694
470
|
}
|
|
695
|
-
|
|
696
|
-
function shutdown(): void {
|
|
697
|
-
clearInterval(heartbeat);
|
|
698
|
-
if (poller) clearInterval(poller);
|
|
699
|
-
releaseWriterLock(sessionDir);
|
|
700
|
-
void runtime.dispose().finally(() => {
|
|
701
|
-
server.close(() => process.exit(0));
|
|
702
|
-
});
|
|
703
|
-
}
|