opencandle 0.3.0 → 0.5.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/LICENSE +1 -1
- package/README.md +106 -14
- package/assets/logo.svg +187 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +40 -3
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +25 -0
- package/dist/config.js +72 -0
- package/dist/config.js.map +1 -1
- package/dist/infra/browser.d.ts +11 -3
- package/dist/infra/browser.js +2 -1
- package/dist/infra/browser.js.map +1 -1
- package/dist/infra/native-dependencies.d.ts +1 -0
- package/dist/infra/native-dependencies.js +10 -0
- package/dist/infra/native-dependencies.js.map +1 -0
- package/dist/infra/node-version.d.ts +2 -0
- package/dist/infra/node-version.js +23 -0
- package/dist/infra/node-version.js.map +1 -0
- package/dist/infra/rate-limiter.d.ts +4 -0
- package/dist/infra/rate-limiter.js +5 -1
- package/dist/infra/rate-limiter.js.map +1 -1
- package/dist/memory/index.d.ts +2 -0
- package/dist/memory/index.js +1 -0
- package/dist/memory/index.js.map +1 -1
- package/dist/memory/manager.d.ts +9 -0
- package/dist/memory/manager.js +28 -11
- package/dist/memory/manager.js.map +1 -1
- package/dist/memory/sqlite.js +42 -4
- package/dist/memory/sqlite.js.map +1 -1
- package/dist/memory/storage.d.ts +7 -0
- package/dist/memory/storage.js +3 -3
- package/dist/memory/storage.js.map +1 -1
- package/dist/memory/tool-defaults.d.ts +8 -0
- package/dist/memory/tool-defaults.js +59 -0
- package/dist/memory/tool-defaults.js.map +1 -0
- package/dist/memory/types.js +4 -0
- package/dist/memory/types.js.map +1 -1
- package/dist/onboarding/connect.d.ts +13 -1
- package/dist/onboarding/connect.js +21 -10
- package/dist/onboarding/connect.js.map +1 -1
- package/dist/onboarding/prompt-user.d.ts +1 -1
- package/dist/onboarding/providers.d.ts +7 -0
- package/dist/onboarding/providers.js +6 -3
- package/dist/onboarding/providers.js.map +1 -1
- package/dist/onboarding/tool-helpers.d.ts +1 -1
- package/dist/pi/opencandle-extension.d.ts +7 -1
- package/dist/pi/opencandle-extension.js +391 -21
- package/dist/pi/opencandle-extension.js.map +1 -1
- package/dist/pi/session-storage.d.ts +2 -0
- package/dist/pi/session-storage.js +5 -0
- package/dist/pi/session-storage.js.map +1 -0
- package/dist/pi/session.d.ts +4 -1
- package/dist/pi/session.js +25 -3
- package/dist/pi/session.js.map +1 -1
- package/dist/pi/setup.d.ts +1 -1
- package/dist/pi/setup.js +11 -1
- package/dist/pi/setup.js.map +1 -1
- package/dist/pi/tool-adapter.d.ts +2 -2
- package/dist/pi/tool-adapter.js +14 -1
- package/dist/pi/tool-adapter.js.map +1 -1
- package/dist/prompts/context-builder.d.ts +40 -3
- package/dist/prompts/context-builder.js +140 -19
- package/dist/prompts/context-builder.js.map +1 -1
- package/dist/prompts/disclaimer.d.ts +6 -0
- package/dist/prompts/disclaimer.js +9 -0
- package/dist/prompts/disclaimer.js.map +1 -0
- package/dist/prompts/policy-cards.d.ts +13 -0
- package/dist/prompts/policy-cards.js +197 -0
- package/dist/prompts/policy-cards.js.map +1 -0
- package/dist/prompts/sections.js +3 -3
- package/dist/prompts/sections.js.map +1 -1
- package/dist/prompts/workflow-prompts.d.ts +8 -0
- package/dist/prompts/workflow-prompts.js +208 -22
- package/dist/prompts/workflow-prompts.js.map +1 -1
- package/dist/providers/alpha-vantage.js +23 -1
- package/dist/providers/alpha-vantage.js.map +1 -1
- package/dist/providers/sec-edgar.d.ts +8 -1
- package/dist/providers/sec-edgar.js +172 -5
- package/dist/providers/sec-edgar.js.map +1 -1
- package/dist/providers/yahoo-finance.d.ts +2 -0
- package/dist/providers/yahoo-finance.js +203 -35
- package/dist/providers/yahoo-finance.js.map +1 -1
- package/dist/routing/classify-intent.d.ts +3 -0
- package/dist/routing/classify-intent.js +82 -3
- package/dist/routing/classify-intent.js.map +1 -1
- package/dist/routing/defaults.js +4 -4
- package/dist/routing/defaults.js.map +1 -1
- package/dist/routing/entity-extractor.d.ts +1 -0
- package/dist/routing/entity-extractor.js +158 -12
- package/dist/routing/entity-extractor.js.map +1 -1
- package/dist/routing/index.d.ts +10 -0
- package/dist/routing/index.js +7 -0
- package/dist/routing/index.js.map +1 -1
- package/dist/routing/legacy-rule-router.d.ts +9 -0
- package/dist/routing/legacy-rule-router.js +12 -0
- package/dist/routing/legacy-rule-router.js.map +1 -0
- package/dist/routing/planning.d.ts +54 -0
- package/dist/routing/planning.js +531 -0
- package/dist/routing/planning.js.map +1 -0
- package/dist/routing/route-manifest.d.ts +35 -0
- package/dist/routing/route-manifest.js +221 -0
- package/dist/routing/route-manifest.js.map +1 -0
- package/dist/routing/router-llm-client.d.ts +11 -0
- package/dist/routing/router-llm-client.js +42 -0
- package/dist/routing/router-llm-client.js.map +1 -0
- package/dist/routing/router-prompt.d.ts +2 -0
- package/dist/routing/router-prompt.js +141 -0
- package/dist/routing/router-prompt.js.map +1 -0
- package/dist/routing/router-types.d.ts +71 -0
- package/dist/routing/router-types.js +2 -0
- package/dist/routing/router-types.js.map +1 -0
- package/dist/routing/router.d.ts +11 -0
- package/dist/routing/router.js +638 -0
- package/dist/routing/router.js.map +1 -0
- package/dist/routing/slot-resolver.js +46 -6
- package/dist/routing/slot-resolver.js.map +1 -1
- package/dist/routing/turn-context.d.ts +44 -0
- package/dist/routing/turn-context.js +45 -0
- package/dist/routing/turn-context.js.map +1 -0
- package/dist/routing/types.d.ts +13 -1
- package/dist/runtime/answer-contracts.d.ts +82 -0
- package/dist/runtime/answer-contracts.js +414 -0
- package/dist/runtime/answer-contracts.js.map +1 -0
- package/dist/runtime/artifact-contracts.d.ts +14 -0
- package/dist/runtime/artifact-contracts.js +57 -0
- package/dist/runtime/artifact-contracts.js.map +1 -0
- package/dist/runtime/planning-evidence.d.ts +99 -0
- package/dist/runtime/planning-evidence.js +445 -0
- package/dist/runtime/planning-evidence.js.map +1 -0
- package/dist/runtime/session-coordinator.d.ts +81 -3
- package/dist/runtime/session-coordinator.js +201 -17
- package/dist/runtime/session-coordinator.js.map +1 -1
- package/dist/runtime/tool-defaults-wrapper.d.ts +3 -0
- package/dist/runtime/tool-defaults-wrapper.js +25 -0
- package/dist/runtime/tool-defaults-wrapper.js.map +1 -0
- package/dist/sentiment/store.js +5 -0
- package/dist/sentiment/store.js.map +1 -1
- package/dist/system-prompt.js +23 -12
- package/dist/system-prompt.js.map +1 -1
- package/dist/tool-kit.d.ts +4 -4
- package/dist/tools/fundamentals/company-overview.d.ts +1 -1
- package/dist/tools/fundamentals/company-overview.js +1 -1
- package/dist/tools/fundamentals/company-overview.js.map +1 -1
- package/dist/tools/fundamentals/comps.d.ts +1 -1
- package/dist/tools/fundamentals/comps.js +1 -1
- package/dist/tools/fundamentals/comps.js.map +1 -1
- package/dist/tools/fundamentals/dcf.d.ts +1 -1
- package/dist/tools/fundamentals/dcf.js +1 -1
- package/dist/tools/fundamentals/dcf.js.map +1 -1
- package/dist/tools/fundamentals/earnings.d.ts +1 -1
- package/dist/tools/fundamentals/earnings.js +1 -1
- package/dist/tools/fundamentals/earnings.js.map +1 -1
- package/dist/tools/fundamentals/financials.d.ts +1 -1
- package/dist/tools/fundamentals/financials.js +1 -1
- package/dist/tools/fundamentals/financials.js.map +1 -1
- package/dist/tools/fundamentals/sec-filings.d.ts +2 -1
- package/dist/tools/fundamentals/sec-filings.js +19 -2
- package/dist/tools/fundamentals/sec-filings.js.map +1 -1
- package/dist/tools/index.d.ts +29 -1
- package/dist/tools/index.js +30 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/interaction/ask-user.d.ts +1 -1
- package/dist/tools/interaction/twitter-login.d.ts +1 -1
- package/dist/tools/macro/fear-greed.d.ts +1 -1
- package/dist/tools/macro/fear-greed.js +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 +29 -5
- package/dist/tools/macro/fred-data.js.map +1 -1
- package/dist/tools/market/crypto-history.d.ts +1 -1
- package/dist/tools/market/crypto-history.js +18 -2
- package/dist/tools/market/crypto-history.js.map +1 -1
- package/dist/tools/market/crypto-price.d.ts +1 -1
- package/dist/tools/market/crypto-price.js +1 -1
- package/dist/tools/market/crypto-price.js.map +1 -1
- package/dist/tools/market/search-ticker.d.ts +1 -1
- package/dist/tools/market/search-ticker.js +1 -1
- package/dist/tools/market/search-ticker.js.map +1 -1
- package/dist/tools/market/stock-history.d.ts +1 -1
- package/dist/tools/market/stock-history.js +1 -1
- package/dist/tools/market/stock-history.js.map +1 -1
- package/dist/tools/market/stock-quote.d.ts +1 -1
- package/dist/tools/market/stock-quote.js +1 -1
- package/dist/tools/market/stock-quote.js.map +1 -1
- package/dist/tools/options/greeks.js +0 -1
- package/dist/tools/options/greeks.js.map +1 -1
- package/dist/tools/options/option-chain.d.ts +1 -1
- package/dist/tools/options/option-chain.js +13 -5
- package/dist/tools/options/option-chain.js.map +1 -1
- package/dist/tools/portfolio/correlation.d.ts +1 -1
- package/dist/tools/portfolio/correlation.js +1 -1
- package/dist/tools/portfolio/correlation.js.map +1 -1
- package/dist/tools/portfolio/holdings-overlap.d.ts +8 -0
- package/dist/tools/portfolio/holdings-overlap.js +105 -0
- package/dist/tools/portfolio/holdings-overlap.js.map +1 -0
- package/dist/tools/portfolio/predictions.d.ts +1 -1
- package/dist/tools/portfolio/predictions.js +1 -1
- 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 +1 -1
- package/dist/tools/portfolio/risk-analysis.js.map +1 -1
- package/dist/tools/portfolio/tracker.d.ts +1 -1
- package/dist/tools/portfolio/tracker.js +1 -1
- package/dist/tools/portfolio/tracker.js.map +1 -1
- package/dist/tools/portfolio/watchlist.d.ts +1 -1
- package/dist/tools/portfolio/watchlist.js +12 -4
- package/dist/tools/portfolio/watchlist.js.map +1 -1
- package/dist/tools/sentiment/reddit-sentiment.d.ts +1 -1
- package/dist/tools/sentiment/reddit-sentiment.js +1 -1
- package/dist/tools/sentiment/reddit-sentiment.js.map +1 -1
- package/dist/tools/sentiment/sentiment-summary.d.ts +1 -1
- package/dist/tools/sentiment/sentiment-summary.js +57 -2
- package/dist/tools/sentiment/sentiment-summary.js.map +1 -1
- package/dist/tools/sentiment/sentiment-trend.d.ts +1 -1
- package/dist/tools/sentiment/twitter-sentiment.d.ts +1 -1
- package/dist/tools/sentiment/twitter-sentiment.js +1 -1
- package/dist/tools/sentiment/twitter-sentiment.js.map +1 -1
- package/dist/tools/sentiment/web-search.d.ts +1 -1
- package/dist/tools/sentiment/web-search.js +32 -3
- package/dist/tools/sentiment/web-search.js.map +1 -1
- package/dist/tools/sentiment/web-sentiment.d.ts +1 -1
- package/dist/tools/sentiment/web-sentiment.js +1 -1
- package/dist/tools/sentiment/web-sentiment.js.map +1 -1
- package/dist/tools/technical/backtest.d.ts +3 -3
- package/dist/tools/technical/backtest.js +41 -27
- package/dist/tools/technical/backtest.js.map +1 -1
- package/dist/tools/technical/indicators.d.ts +1 -1
- package/dist/tools/technical/indicators.js +8 -4
- package/dist/tools/technical/indicators.js.map +1 -1
- package/dist/types/options.d.ts +10 -0
- package/dist/types/portfolio.d.ts +27 -0
- package/dist/workflows/compare-assets.js +38 -2
- package/dist/workflows/compare-assets.js.map +1 -1
- package/dist/workflows/options-screener.js +94 -8
- package/dist/workflows/options-screener.js.map +1 -1
- package/dist/workflows/portfolio-builder.js +9 -5
- package/dist/workflows/portfolio-builder.js.map +1 -1
- package/gui/server/ask-user-bridge.ts +82 -0
- package/gui/server/background-quotes.ts +31 -0
- package/gui/server/chat-event-adapter.ts +142 -0
- package/gui/server/gui-session-manager.ts +5 -0
- package/gui/server/invoke-tool.ts +89 -0
- package/gui/server/live-chat-event-adapter.ts +181 -0
- package/gui/server/model-setup.ts +100 -0
- package/gui/server/package.json +5 -0
- package/gui/server/projector.ts +254 -0
- package/gui/server/prompt-observation.ts +61 -0
- package/gui/server/server.ts +703 -0
- package/gui/server/session-actions.ts +31 -0
- package/gui/server/session-entry-wait.ts +81 -0
- package/gui/server/tool-metadata.ts +88 -0
- package/gui/server/websocket.ts +128 -0
- package/gui/server/writer-lock.ts +118 -0
- package/gui/shared/chat-events.ts +118 -0
- package/gui/shared/event-reducer.ts +186 -0
- package/gui/web/dist/assets/CatalogOverlay-Bmp6Knu7.js +1 -0
- package/gui/web/dist/assets/index-Bxt9QpLX.css +1 -0
- package/gui/web/dist/assets/index-CZ9DHZYy.js +67 -0
- package/gui/web/dist/assets/logo-CWpt6Y2a.svg +187 -0
- package/gui/web/dist/index.html +17 -0
- package/package.json +50 -18
- package/src/analysts/contracts.ts +189 -0
- package/src/analysts/orchestrator.ts +300 -0
- package/src/cli.ts +206 -0
- package/src/config.ts +245 -0
- package/src/index.ts +5 -0
- package/src/infra/browser.ts +111 -0
- package/src/infra/cache.ts +103 -0
- package/src/infra/http-client.ts +68 -0
- package/src/infra/index.ts +18 -0
- package/src/infra/native-dependencies.ts +12 -0
- package/src/infra/node-version.ts +24 -0
- package/src/infra/open-url.ts +28 -0
- package/src/infra/opencandle-paths.ts +64 -0
- package/src/infra/rate-limiter.ts +73 -0
- package/src/memory/index.ts +10 -0
- package/src/memory/manager.ts +192 -0
- package/src/memory/preference-extractor.ts +106 -0
- package/src/memory/retrieval.ts +70 -0
- package/src/memory/sqlite.ts +172 -0
- package/src/memory/storage.ts +205 -0
- package/src/memory/tool-defaults.ts +87 -0
- package/src/memory/types.ts +71 -0
- package/src/onboarding/connect.ts +184 -0
- package/src/onboarding/credential-interceptor.ts +134 -0
- package/src/onboarding/degradation-accumulator.ts +79 -0
- package/src/onboarding/prompt-user.ts +85 -0
- package/src/onboarding/providers.ts +315 -0
- package/src/onboarding/state.ts +218 -0
- package/src/onboarding/tool-helpers.ts +111 -0
- package/src/onboarding/tool-tags.ts +201 -0
- package/src/onboarding/validation.ts +158 -0
- package/src/pi/opencandle-extension.ts +955 -0
- package/src/pi/session-storage.ts +5 -0
- package/src/pi/session.ts +81 -0
- package/src/pi/setup.ts +381 -0
- package/src/pi/tool-adapter.ts +36 -0
- package/src/prompts/context-builder.ts +315 -0
- package/src/prompts/disclaimer.ts +9 -0
- package/src/prompts/policy-cards.ts +220 -0
- package/src/prompts/sections.ts +46 -0
- package/src/prompts/workflow-prompts.ts +433 -0
- package/src/providers/alpha-vantage.ts +315 -0
- package/src/providers/coingecko.ts +96 -0
- package/src/providers/exa-search.ts +373 -0
- package/src/providers/fear-greed.ts +45 -0
- package/src/providers/finnhub.ts +124 -0
- package/src/providers/fred.ts +83 -0
- package/src/providers/index.ts +9 -0
- package/src/providers/provider-credential-error.ts +23 -0
- package/src/providers/reddit.ts +151 -0
- package/src/providers/sec-edgar.ts +312 -0
- package/src/providers/twitter.ts +173 -0
- package/src/providers/web-search.ts +293 -0
- package/src/providers/with-fallback.ts +41 -0
- package/src/providers/wrap-provider.ts +64 -0
- package/src/providers/yahoo-finance.ts +534 -0
- package/src/routing/classify-intent.ts +285 -0
- package/src/routing/defaults.ts +29 -0
- package/src/routing/entity-extractor.ts +291 -0
- package/src/routing/index.ts +70 -0
- package/src/routing/legacy-rule-router.ts +13 -0
- package/src/routing/planning.ts +732 -0
- package/src/routing/route-manifest.ts +287 -0
- package/src/routing/router-llm-client.ts +51 -0
- package/src/routing/router-prompt.ts +163 -0
- package/src/routing/router-types.ts +87 -0
- package/src/routing/router.ts +712 -0
- package/src/routing/slot-resolver.ts +190 -0
- package/src/routing/turn-context.ts +111 -0
- package/src/routing/types.ts +75 -0
- package/src/runtime/answer-contracts.ts +633 -0
- package/src/runtime/artifact-contracts.ts +76 -0
- package/src/runtime/evidence.ts +77 -0
- package/src/runtime/planning-evidence.ts +591 -0
- package/src/runtime/prompt-step.ts +75 -0
- package/src/runtime/provider-tracker.ts +40 -0
- package/src/runtime/run-context.ts +22 -0
- package/src/runtime/session-coordinator.ts +472 -0
- package/src/runtime/tool-defaults-wrapper.ts +35 -0
- package/src/runtime/validation.ts +214 -0
- package/src/runtime/workflow-events.ts +75 -0
- package/src/runtime/workflow-runner.ts +188 -0
- package/src/runtime/workflow-types.ts +102 -0
- package/src/sentiment/adapters/finnhub.ts +44 -0
- package/src/sentiment/adapters/reddit.ts +65 -0
- package/src/sentiment/adapters/twitter.ts +36 -0
- package/src/sentiment/adapters/web.ts +44 -0
- package/src/sentiment/index.ts +58 -0
- package/src/sentiment/keywords.ts +9 -0
- package/src/sentiment/pipeline.ts +68 -0
- package/src/sentiment/scorer.ts +78 -0
- package/src/sentiment/store.ts +260 -0
- package/src/sentiment/trends.ts +90 -0
- package/src/sentiment/types.ts +108 -0
- package/src/system-prompt.ts +118 -0
- package/src/tool-kit.ts +68 -0
- package/src/tools/AGENTS.md +36 -0
- package/src/tools/fundamentals/company-overview.ts +54 -0
- package/src/tools/fundamentals/comps.ts +156 -0
- package/src/tools/fundamentals/dcf.ts +267 -0
- package/src/tools/fundamentals/earnings.ts +47 -0
- package/src/tools/fundamentals/financials.ts +54 -0
- package/src/tools/fundamentals/sec-filings.ts +84 -0
- package/src/tools/index.ts +91 -0
- package/src/tools/interaction/ask-user.ts +81 -0
- package/src/tools/interaction/twitter-login.ts +93 -0
- package/src/tools/macro/fear-greed.ts +41 -0
- package/src/tools/macro/fred-data.ts +80 -0
- package/src/tools/market/crypto-history.ts +67 -0
- package/src/tools/market/crypto-price.ts +53 -0
- package/src/tools/market/search-ticker.ts +53 -0
- package/src/tools/market/stock-history.ts +79 -0
- package/src/tools/market/stock-quote.ts +64 -0
- package/src/tools/options/greeks.ts +81 -0
- package/src/tools/options/option-chain.ts +96 -0
- package/src/tools/portfolio/correlation.ts +162 -0
- package/src/tools/portfolio/holdings-overlap.ts +123 -0
- package/src/tools/portfolio/predictions.ts +253 -0
- package/src/tools/portfolio/risk-analysis.ts +134 -0
- package/src/tools/portfolio/tracker.ts +147 -0
- package/src/tools/portfolio/watchlist.ts +159 -0
- package/src/tools/sentiment/reddit-sentiment.ts +164 -0
- package/src/tools/sentiment/sentiment-summary.ts +316 -0
- package/src/tools/sentiment/sentiment-trend.ts +58 -0
- package/src/tools/sentiment/twitter-sentiment.ts +96 -0
- package/src/tools/sentiment/web-search.ts +183 -0
- package/src/tools/sentiment/web-sentiment.ts +76 -0
- package/src/tools/technical/backtest.ts +267 -0
- package/src/tools/technical/indicators.ts +256 -0
- package/src/types/fundamentals.ts +46 -0
- package/src/types/index.ts +20 -0
- package/src/types/macro.ts +27 -0
- package/src/types/market.ts +43 -0
- package/src/types/options.ts +52 -0
- package/src/types/portfolio.ts +73 -0
- package/src/types/sentiment.ts +70 -0
- package/src/workflows/compare-assets.ts +75 -0
- package/src/workflows/index.ts +4 -0
- package/src/workflows/options-screener.ts +127 -0
- package/src/workflows/portfolio-builder.ts +56 -0
- package/src/workflows/types.ts +4 -0
- package/dist/runtime/index.d.ts +0 -16
- package/dist/runtime/index.js +0 -10
- package/dist/runtime/index.js.map +0 -1
- package/dist/runtime/provider-ids.d.ts +0 -14
- package/dist/runtime/provider-ids.js +0 -14
- package/dist/runtime/provider-ids.js.map +0 -1
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
import { createReadStream, existsSync } from "node:fs";
|
|
2
|
+
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
|
3
|
+
import { extname, join, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import {
|
|
6
|
+
AuthStorage,
|
|
7
|
+
createAgentSessionRuntime,
|
|
8
|
+
createAgentSessionServices,
|
|
9
|
+
type AgentSession,
|
|
10
|
+
getAgentDir,
|
|
11
|
+
ModelRegistry,
|
|
12
|
+
SessionManager,
|
|
13
|
+
SettingsManager,
|
|
14
|
+
} from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { createOpenCandleSession } from "../../src/index.js";
|
|
16
|
+
import { getAllTools } from "../../src/tools/index.js";
|
|
17
|
+
import { persistProviderCredential } from "../../src/onboarding/connect.js";
|
|
18
|
+
import {
|
|
19
|
+
getCredentialSource,
|
|
20
|
+
PROVIDERS,
|
|
21
|
+
type ProviderId,
|
|
22
|
+
} from "../../src/onboarding/providers.js";
|
|
23
|
+
import { validateCredential } from "../../src/onboarding/validation.js";
|
|
24
|
+
import { buildModelSetupState, findPreferredModel, modelSetupProviders } from "./model-setup.js";
|
|
25
|
+
import { projectDashboard } from "./projector.js";
|
|
26
|
+
import { acceptWebSocket, type WsClient } from "./websocket.js";
|
|
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";
|
|
31
|
+
import { sessionEntriesToChatEvents } from "./chat-event-adapter.js";
|
|
32
|
+
import { createLiveChatEventAdapter } from "./live-chat-event-adapter.js";
|
|
33
|
+
import { waitForNewEntryId, waitForSessionTurnSettlement } from "./session-entry-wait.js";
|
|
34
|
+
import {
|
|
35
|
+
createPromptObservation,
|
|
36
|
+
observePromptEvent,
|
|
37
|
+
selectReplayPrompt,
|
|
38
|
+
type PromptObservation,
|
|
39
|
+
} from "./prompt-observation.js";
|
|
40
|
+
import { BackgroundQuoteRefreshes } from "./background-quotes.js";
|
|
41
|
+
import { createAskUserBridge } from "./ask-user-bridge.js";
|
|
42
|
+
import { createInitialGuiSessionManager } from "./gui-session-manager.js";
|
|
43
|
+
import type { ChatEvent } from "../shared/chat-events.js";
|
|
44
|
+
|
|
45
|
+
const cwd = process.cwd();
|
|
46
|
+
const host = process.env.OPENCANDLE_GUI_HOST ?? "127.0.0.1";
|
|
47
|
+
const port = Number(process.env.OPENCANDLE_GUI_PORT ?? 14567);
|
|
48
|
+
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
49
|
+
const webDist = resolve(__dirname, "../web/dist");
|
|
50
|
+
|
|
51
|
+
const agentDir = getAgentDir();
|
|
52
|
+
const authStorage = AuthStorage.create();
|
|
53
|
+
const modelRegistry = ModelRegistry.create(authStorage);
|
|
54
|
+
const settingsManager = SettingsManager.create(cwd, agentDir);
|
|
55
|
+
const initialSessionManager = createInitialGuiSessionManager(cwd);
|
|
56
|
+
let sessionManager = initialSessionManager;
|
|
57
|
+
const sessionDir = sessionManager.getSessionDir();
|
|
58
|
+
const lockResult = await acquireWriterLock(sessionDir, "gui");
|
|
59
|
+
const askUserBridge = createAskUserBridge({
|
|
60
|
+
broadcast,
|
|
61
|
+
getSessionId: () => sessionManager.getSessionId(),
|
|
62
|
+
});
|
|
63
|
+
const runtime = await createAgentSessionRuntime(
|
|
64
|
+
async (opts) => {
|
|
65
|
+
const services = await createAgentSessionServices({
|
|
66
|
+
cwd: opts.cwd,
|
|
67
|
+
agentDir: opts.agentDir,
|
|
68
|
+
authStorage,
|
|
69
|
+
settingsManager,
|
|
70
|
+
modelRegistry,
|
|
71
|
+
});
|
|
72
|
+
const result = await createOpenCandleSession({
|
|
73
|
+
cwd: opts.cwd,
|
|
74
|
+
agentDir: opts.agentDir,
|
|
75
|
+
authStorage,
|
|
76
|
+
modelRegistry,
|
|
77
|
+
settingsManager,
|
|
78
|
+
sessionManager: opts.sessionManager,
|
|
79
|
+
askUserHandler: askUserBridge.ask,
|
|
80
|
+
});
|
|
81
|
+
return { ...result, services, diagnostics: services.diagnostics };
|
|
82
|
+
},
|
|
83
|
+
{ cwd, agentDir, sessionManager },
|
|
84
|
+
);
|
|
85
|
+
let session = runtime.session;
|
|
86
|
+
const clients = new Set<WsClient>();
|
|
87
|
+
const heartbeat = setInterval(() => refreshWriterLock(sessionDir), 5000);
|
|
88
|
+
const backgroundQuoteRefreshes = new BackgroundQuoteRefreshes();
|
|
89
|
+
let poller: NodeJS.Timeout | null = null;
|
|
90
|
+
let quotePollInFlight = false;
|
|
91
|
+
|
|
92
|
+
let unsubscribeSession = subscribeToSessionEvents();
|
|
93
|
+
runtime.setRebindSession(async (nextSession) => {
|
|
94
|
+
unsubscribeSession();
|
|
95
|
+
session = nextSession;
|
|
96
|
+
sessionManager = nextSession.sessionManager;
|
|
97
|
+
unsubscribeSession = subscribeToSessionEvents();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const server = createServer((req, res) => {
|
|
101
|
+
void handleHttpRequest(req, res);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
async function handleHttpRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
105
|
+
const url = new URL(req.url ?? "/", `http://${host}:${port}`);
|
|
106
|
+
if (url.pathname === "/health") {
|
|
107
|
+
writeJson(res, { ok: true, role: lockResult.role });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (url.pathname === "/api/bootstrap" && req.method === "GET") {
|
|
112
|
+
writeJson(res, {
|
|
113
|
+
role: lockResult.role,
|
|
114
|
+
sessionId: sessionManager.getSessionId(),
|
|
115
|
+
catalog: buildCatalog(),
|
|
116
|
+
modelSetup: buildCurrentModelSetupState(),
|
|
117
|
+
askUserPrompts: askUserBridge.getPrompts(),
|
|
118
|
+
sessions: await SessionManager.list(cwd, sessionDir),
|
|
119
|
+
snapshot: buildStateSnapshot(),
|
|
120
|
+
});
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (url.pathname === "/api/sessions" && req.method === "GET") {
|
|
125
|
+
writeJson(res, {
|
|
126
|
+
currentSessionId: sessionManager.getSessionId(),
|
|
127
|
+
role: lockResult.role,
|
|
128
|
+
sessions: await SessionManager.list(cwd, sessionDir),
|
|
129
|
+
});
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (url.pathname === "/api/session/events" && req.method === "GET") {
|
|
134
|
+
writeJson(res, {
|
|
135
|
+
sessionId: sessionManager.getSessionId(),
|
|
136
|
+
role: lockResult.role,
|
|
137
|
+
events: currentChatEvents(),
|
|
138
|
+
});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (url.pathname === "/api/chat/run" && req.method === "POST") {
|
|
143
|
+
await handleSseChatRun(req, res);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const requested = url.pathname === "/" ? "/index.html" : url.pathname;
|
|
148
|
+
const path = resolve(join(webDist, requested));
|
|
149
|
+
if (!path.startsWith(webDist) || !existsSync(path)) {
|
|
150
|
+
const fallback = resolve(join(webDist, "index.html"));
|
|
151
|
+
if (!extname(requested) && fallback.startsWith(webDist) && existsSync(fallback)) {
|
|
152
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
153
|
+
createReadStream(fallback).pipe(res);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
res.writeHead(404).end("Not found");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
res.writeHead(200, { "content-type": contentType(path) });
|
|
161
|
+
createReadStream(path).pipe(res);
|
|
162
|
+
}
|
|
163
|
+
|
|
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
|
+
});
|
|
180
|
+
|
|
181
|
+
server.listen(port, host, () => {
|
|
182
|
+
console.log(`OpenCandle GUI listening on http://${host}:${port}`);
|
|
183
|
+
if (host === "0.0.0.0") {
|
|
184
|
+
console.log(`OpenCandle GUI is accepting LAN/Tailscale connections on port ${port}`);
|
|
185
|
+
}
|
|
186
|
+
console.log(`Writer role: ${lockResult.role}`);
|
|
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.`,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
persistProviderCredential(descriptor.id as ProviderId, trimmed);
|
|
392
|
+
|
|
393
|
+
const verifiedNote =
|
|
394
|
+
validation.status === "transient"
|
|
395
|
+
? `Saved ${descriptor.displayName} key but couldn't verify it (${validation.reason}). The next request will surface any issue.`
|
|
396
|
+
: `Connected ${descriptor.displayName}. Key saved to ~/.opencandle/config.json.`;
|
|
397
|
+
|
|
398
|
+
sessionManager.appendCustomMessageEntry(
|
|
399
|
+
"opencandle-provider-setup",
|
|
400
|
+
verifiedNote,
|
|
401
|
+
true,
|
|
402
|
+
{ source: "gui", provider: descriptor.id, status: validation.status },
|
|
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
|
+
}
|
|
445
|
+
|
|
446
|
+
function broadcastState(): void {
|
|
447
|
+
broadcast({
|
|
448
|
+
type: "state.snapshot",
|
|
449
|
+
...buildStateSnapshot(),
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function handleSseChatRun(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
454
|
+
if (lockResult.role !== "writer") {
|
|
455
|
+
writeJson(res, { error: "Read-only follower mode" }, 409);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const body = await readJsonBody(req);
|
|
460
|
+
const prompt = String(asRecord(body).prompt ?? "").trim();
|
|
461
|
+
if (!prompt) {
|
|
462
|
+
writeJson(res, { error: "prompt is required" }, 400);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
res.writeHead(200, {
|
|
467
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
468
|
+
"cache-control": "no-cache, no-transform",
|
|
469
|
+
connection: "keep-alive",
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
let seq = 1;
|
|
473
|
+
const runId = `gui-run-${Date.now()}`;
|
|
474
|
+
const runSession = session;
|
|
475
|
+
const runSessionManager = sessionManager;
|
|
476
|
+
const sessionId = runSessionManager.getSessionId();
|
|
477
|
+
const beforeEntries = runSessionManager.getEntries();
|
|
478
|
+
const beforeCount = beforeEntries.length;
|
|
479
|
+
const beforeIds = new Set(beforeEntries.map((entry) => entry.id));
|
|
480
|
+
writeSse(res, { type: "run.started", runId, sessionId, seq: seq++ });
|
|
481
|
+
res.flushHeaders?.();
|
|
482
|
+
const liveStartSeq = seq;
|
|
483
|
+
const liveAdapter = createLiveChatEventAdapter({
|
|
484
|
+
runId,
|
|
485
|
+
sessionId,
|
|
486
|
+
startSeq: seq,
|
|
487
|
+
emit: (event) => writeSse(res, event),
|
|
488
|
+
});
|
|
489
|
+
const observation = createPromptObservation();
|
|
490
|
+
const unsubscribeLive = runSession.subscribe((event) => {
|
|
491
|
+
liveAdapter.handle(event);
|
|
492
|
+
observePromptEvent(observation, event);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
try {
|
|
496
|
+
const modelSetup = buildModelSetupState(runSession.modelRegistry, runSession.model);
|
|
497
|
+
if (!prompt.startsWith("/") && modelSetup.requirement !== "ready") {
|
|
498
|
+
runSessionManager.appendMessage({ role: "user", content: prompt, timestamp: Date.now() });
|
|
499
|
+
const message =
|
|
500
|
+
modelSetup.requirement === "select_model"
|
|
501
|
+
? "Choose an available model before chat can run. OpenCandle found configured credentials but no active model."
|
|
502
|
+
: "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
|
+
"opencandle-model-setup",
|
|
505
|
+
message,
|
|
506
|
+
true,
|
|
507
|
+
{ source: "gui", requirement: modelSetup.requirement },
|
|
508
|
+
);
|
|
509
|
+
broadcastState();
|
|
510
|
+
} else {
|
|
511
|
+
await promptAndSettle(runSession, prompt, beforeIds, observation);
|
|
512
|
+
broadcastState();
|
|
513
|
+
}
|
|
514
|
+
seq = liveAdapter.nextSeq();
|
|
515
|
+
if (seq === liveStartSeq) {
|
|
516
|
+
await waitForNewEntryId(
|
|
517
|
+
() => runSessionManager.getEntries().map((entry) => entry.id),
|
|
518
|
+
beforeIds,
|
|
519
|
+
);
|
|
520
|
+
const newEntries = runSessionManager.getEntries()
|
|
521
|
+
.slice(beforeCount)
|
|
522
|
+
.filter((entry) => !beforeIds.has(entry.id));
|
|
523
|
+
const events = sessionEntriesToChatEvents(newEntries, {
|
|
524
|
+
sessionId,
|
|
525
|
+
updatedAt: new Date().toISOString(),
|
|
526
|
+
startSeq: seq,
|
|
527
|
+
});
|
|
528
|
+
for (const event of events) {
|
|
529
|
+
writeSse(res, event);
|
|
530
|
+
seq = event.seq + 1;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
writeSse(res, { type: "run.completed", runId, seq });
|
|
534
|
+
} catch (error) {
|
|
535
|
+
seq = liveAdapter.nextSeq();
|
|
536
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
537
|
+
writeSse(res, { type: "run.failed", runId, error: { message }, seq });
|
|
538
|
+
} finally {
|
|
539
|
+
unsubscribeLive();
|
|
540
|
+
res.end();
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
async function promptAndSettle(
|
|
545
|
+
runSession: AgentSession,
|
|
546
|
+
prompt: string,
|
|
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);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function subscribeToSessionEvents(): () => void {
|
|
609
|
+
return session.subscribe((event) => {
|
|
610
|
+
broadcast({ type: "session.event", event });
|
|
611
|
+
broadcastState();
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function updatePoller(): void {
|
|
616
|
+
if (clients.size > 0 && !poller) {
|
|
617
|
+
poller = setInterval(() => void pollVisibleQuotes(), 30000);
|
|
618
|
+
}
|
|
619
|
+
if (clients.size === 0 && poller) {
|
|
620
|
+
clearInterval(poller);
|
|
621
|
+
poller = null;
|
|
622
|
+
}
|
|
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));
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function writeSse(res: ServerResponse, event: ChatEvent): void {
|
|
663
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
async function readJsonBody(req: IncomingMessage): Promise<unknown> {
|
|
667
|
+
const chunks: Buffer[] = [];
|
|
668
|
+
for await (const chunk of req) {
|
|
669
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
670
|
+
}
|
|
671
|
+
if (chunks.length === 0) return {};
|
|
672
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function contentType(path: string): string {
|
|
676
|
+
switch (extname(path)) {
|
|
677
|
+
case ".html":
|
|
678
|
+
return "text/html; charset=utf-8";
|
|
679
|
+
case ".css":
|
|
680
|
+
return "text/css; charset=utf-8";
|
|
681
|
+
case ".js":
|
|
682
|
+
return "text/javascript; charset=utf-8";
|
|
683
|
+
case ".svg":
|
|
684
|
+
return "image/svg+xml";
|
|
685
|
+
default:
|
|
686
|
+
return "application/octet-stream";
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
691
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
692
|
+
? value as Record<string, unknown>
|
|
693
|
+
: {};
|
|
694
|
+
}
|
|
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
|
+
}
|