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
package/src/config.ts
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { ensureParentDir, getConfigPath } from "./infra/opencandle-paths.js";
|
|
3
|
+
import type { PlanningBehaviorMode, TaskFamily } from "./routing/planning.js";
|
|
4
|
+
|
|
5
|
+
export interface SentimentConfig {
|
|
6
|
+
retentionDays: number;
|
|
7
|
+
defaultSubreddits: string[];
|
|
8
|
+
commentsPerPost: number;
|
|
9
|
+
divergenceThreshold: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type RouterMode = "rules" | "llm";
|
|
13
|
+
export type ToolScopeMode = "observe" | "enforce";
|
|
14
|
+
export type PlanningMigrationStatuses = Partial<Record<TaskFamily, PlanningBehaviorMode>>;
|
|
15
|
+
|
|
16
|
+
export interface Config {
|
|
17
|
+
alphaVantageApiKey?: string;
|
|
18
|
+
fredApiKey?: string;
|
|
19
|
+
braveApiKey?: string;
|
|
20
|
+
exaApiKey?: string;
|
|
21
|
+
finnhubApiKey?: string;
|
|
22
|
+
/** Enable adversarial bull/bear debate in comprehensive analysis. Default: true. */
|
|
23
|
+
debate?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Intent-router mode. `"llm"` (default) runs the LLM router ahead of prompt
|
|
26
|
+
* assembly. `"rules"` is the explicit legacy rule-router rollback path
|
|
27
|
+
* (`classifyIntent` + `extractPreferences`). Controlled by
|
|
28
|
+
* `OPENCANDLE_ROUTER_MODE`.
|
|
29
|
+
*/
|
|
30
|
+
routerMode: RouterMode;
|
|
31
|
+
/**
|
|
32
|
+
* Route-selected tool scope mode. `"observe"` (default) records selected
|
|
33
|
+
* bundles and active-tool candidates. `"enforce"` applies Pi active tools
|
|
34
|
+
* for the turn via `pi.setActiveTools`.
|
|
35
|
+
*/
|
|
36
|
+
toolScopeMode: ToolScopeMode;
|
|
37
|
+
/**
|
|
38
|
+
* Per-task planning behavior rollback/activation overrides. Controlled by
|
|
39
|
+
* `OPENCANDLE_PLANNING_MIGRATION_STATUSES`, e.g.
|
|
40
|
+
* `asset_compare=dual_run,single_asset_decision=observe_only`.
|
|
41
|
+
*/
|
|
42
|
+
planningMigrationStatuses?: PlanningMigrationStatuses;
|
|
43
|
+
sentiment?: SentimentConfig;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface OpenCandleFileConfig {
|
|
47
|
+
providers?: {
|
|
48
|
+
alphaVantage?: {
|
|
49
|
+
apiKey?: string;
|
|
50
|
+
};
|
|
51
|
+
fred?: {
|
|
52
|
+
apiKey?: string;
|
|
53
|
+
};
|
|
54
|
+
brave?: {
|
|
55
|
+
apiKey?: string;
|
|
56
|
+
};
|
|
57
|
+
exa?: {
|
|
58
|
+
apiKey?: string;
|
|
59
|
+
};
|
|
60
|
+
finnhub?: {
|
|
61
|
+
apiKey?: string;
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
/** Enable adversarial bull/bear debate in comprehensive analysis. Default: true. */
|
|
65
|
+
debate?: boolean;
|
|
66
|
+
sentiment?: {
|
|
67
|
+
retentionDays?: number;
|
|
68
|
+
defaultSubreddits?: string[];
|
|
69
|
+
commentsPerPost?: number;
|
|
70
|
+
divergenceThreshold?: number;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function loadEnv(path = ".env"): void {
|
|
75
|
+
let content: string;
|
|
76
|
+
try {
|
|
77
|
+
content = readFileSync(path, "utf-8");
|
|
78
|
+
} catch {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
for (const line of content.split("\n")) {
|
|
82
|
+
const trimmed = line.trim();
|
|
83
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
84
|
+
const eqIndex = trimmed.indexOf("=");
|
|
85
|
+
if (eqIndex === -1) continue;
|
|
86
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
87
|
+
const value = trimmed.slice(eqIndex + 1).trim();
|
|
88
|
+
if (key && value) {
|
|
89
|
+
process.env[key] = value;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let cachedConfig: Config | null = null;
|
|
95
|
+
|
|
96
|
+
const SENTIMENT_DEFAULTS: SentimentConfig = {
|
|
97
|
+
retentionDays: 30,
|
|
98
|
+
defaultSubreddits: ["wallstreetbets", "stocks", "investing", "options"],
|
|
99
|
+
commentsPerPost: 5,
|
|
100
|
+
divergenceThreshold: 0.4,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const PLANNING_TASK_FAMILIES = [
|
|
104
|
+
"single_asset_decision",
|
|
105
|
+
"asset_compare",
|
|
106
|
+
"portfolio_build",
|
|
107
|
+
"portfolio_review",
|
|
108
|
+
"macro_allocation_review",
|
|
109
|
+
"options_strategy",
|
|
110
|
+
"current_event_explanation",
|
|
111
|
+
"ticker_disambiguation",
|
|
112
|
+
"filing_thesis_review",
|
|
113
|
+
"sentiment_snapshot",
|
|
114
|
+
"concept_explainer",
|
|
115
|
+
"retail_finance_tradeoff",
|
|
116
|
+
"stateful_tracking_update",
|
|
117
|
+
"backtest_review",
|
|
118
|
+
"general_fallback",
|
|
119
|
+
] as const satisfies readonly TaskFamily[];
|
|
120
|
+
|
|
121
|
+
const PLANNING_BEHAVIOR_MODES = [
|
|
122
|
+
"observe_only",
|
|
123
|
+
"dual_run",
|
|
124
|
+
"replacement_active",
|
|
125
|
+
] as const satisfies readonly PlanningBehaviorMode[];
|
|
126
|
+
|
|
127
|
+
function resolveRouterMode(): RouterMode {
|
|
128
|
+
const raw = process.env.OPENCANDLE_ROUTER_MODE;
|
|
129
|
+
if (raw === undefined || raw === "") return "llm";
|
|
130
|
+
if (raw === "rules" || raw === "llm") return raw;
|
|
131
|
+
throw new Error(
|
|
132
|
+
`Invalid OPENCANDLE_ROUTER_MODE="${raw}". Allowed values: "llm" (default) or "rules".`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function resolveToolScopeMode(): ToolScopeMode {
|
|
137
|
+
const raw = process.env.OPENCANDLE_TOOL_SCOPE_MODE;
|
|
138
|
+
if (raw === undefined || raw === "") return "observe";
|
|
139
|
+
if (raw === "observe" || raw === "enforce") return raw;
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Invalid OPENCANDLE_TOOL_SCOPE_MODE="${raw}". Allowed values: "observe" (default) or "enforce".`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function resolvePlanningMigrationStatuses(): PlanningMigrationStatuses | undefined {
|
|
146
|
+
const raw = process.env.OPENCANDLE_PLANNING_MIGRATION_STATUSES;
|
|
147
|
+
if (raw === undefined || raw.trim() === "") return undefined;
|
|
148
|
+
|
|
149
|
+
const statuses: PlanningMigrationStatuses = {};
|
|
150
|
+
for (const entry of raw.split(",")) {
|
|
151
|
+
const trimmed = entry.trim();
|
|
152
|
+
if (!trimmed) continue;
|
|
153
|
+
|
|
154
|
+
const parts = trimmed.split("=");
|
|
155
|
+
const taskFamily = parts[0]?.trim();
|
|
156
|
+
const behaviorMode = parts[1]?.trim();
|
|
157
|
+
if (
|
|
158
|
+
parts.length !== 2 ||
|
|
159
|
+
!isPlanningTaskFamily(taskFamily) ||
|
|
160
|
+
!isPlanningBehaviorMode(behaviorMode)
|
|
161
|
+
) {
|
|
162
|
+
throw new Error(`Invalid OPENCANDLE_PLANNING_MIGRATION_STATUSES entry "${trimmed}".`);
|
|
163
|
+
}
|
|
164
|
+
statuses[taskFamily] = behaviorMode;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return Object.keys(statuses).length > 0 ? statuses : undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function isPlanningTaskFamily(value: string | undefined): value is TaskFamily {
|
|
171
|
+
return PLANNING_TASK_FAMILIES.includes(value as TaskFamily);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function isPlanningBehaviorMode(value: string | undefined): value is PlanningBehaviorMode {
|
|
175
|
+
return PLANNING_BEHAVIOR_MODES.includes(value as PlanningBehaviorMode);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function resolveConfig(fileConfig: OpenCandleFileConfig): Config {
|
|
179
|
+
const debateEnv = process.env.OPENCANDLE_DEBATE;
|
|
180
|
+
const fileSentiment = fileConfig.sentiment;
|
|
181
|
+
return {
|
|
182
|
+
alphaVantageApiKey:
|
|
183
|
+
process.env.ALPHA_VANTAGE_API_KEY ?? fileConfig.providers?.alphaVantage?.apiKey,
|
|
184
|
+
fredApiKey: process.env.FRED_API_KEY ?? fileConfig.providers?.fred?.apiKey,
|
|
185
|
+
braveApiKey: process.env.BRAVE_API_KEY ?? fileConfig.providers?.brave?.apiKey,
|
|
186
|
+
exaApiKey: process.env.EXA_API_KEY ?? fileConfig.providers?.exa?.apiKey,
|
|
187
|
+
finnhubApiKey: process.env.FINNHUB_API_KEY ?? fileConfig.providers?.finnhub?.apiKey,
|
|
188
|
+
debate: debateEnv !== undefined ? debateEnv !== "false" && debateEnv !== "0" : fileConfig.debate ?? true,
|
|
189
|
+
routerMode: resolveRouterMode(),
|
|
190
|
+
toolScopeMode: resolveToolScopeMode(),
|
|
191
|
+
planningMigrationStatuses: resolvePlanningMigrationStatuses(),
|
|
192
|
+
sentiment: {
|
|
193
|
+
retentionDays: fileSentiment?.retentionDays ?? SENTIMENT_DEFAULTS.retentionDays,
|
|
194
|
+
defaultSubreddits: fileSentiment?.defaultSubreddits ?? SENTIMENT_DEFAULTS.defaultSubreddits,
|
|
195
|
+
commentsPerPost: fileSentiment?.commentsPerPost ?? SENTIMENT_DEFAULTS.commentsPerPost,
|
|
196
|
+
divergenceThreshold: fileSentiment?.divergenceThreshold ?? SENTIMENT_DEFAULTS.divergenceThreshold,
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function loadFileConfig(path = getConfigPath()): OpenCandleFileConfig {
|
|
202
|
+
if (!existsSync(path)) {
|
|
203
|
+
return {};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let content: string;
|
|
207
|
+
try {
|
|
208
|
+
content = readFileSync(path, "utf-8");
|
|
209
|
+
} catch (error) {
|
|
210
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
211
|
+
throw new Error(`Unable to read OpenCandle config at ${path}: ${message}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const parsed = JSON.parse(content) as OpenCandleFileConfig;
|
|
216
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
217
|
+
} catch (error) {
|
|
218
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
219
|
+
throw new Error(`Invalid OpenCandle config at ${path}: ${message}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function saveFileConfig(config: OpenCandleFileConfig, path = getConfigPath()): void {
|
|
224
|
+
ensureParentDir(path);
|
|
225
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function loadConfig(): Config {
|
|
229
|
+
loadEnv();
|
|
230
|
+
cachedConfig = resolveConfig(loadFileConfig());
|
|
231
|
+
|
|
232
|
+
return cachedConfig;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function getConfig(): Config {
|
|
236
|
+
if (!cachedConfig) {
|
|
237
|
+
return loadConfig();
|
|
238
|
+
}
|
|
239
|
+
return cachedConfig;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Test-only: clear the memoized config so the next `getConfig()` re-reads env. */
|
|
243
|
+
export function resetConfigCache(): void {
|
|
244
|
+
cachedConfig = null;
|
|
245
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createOpenCandleSession, type CreateOpenCandleSessionOptions } from "./pi/session.js";
|
|
2
|
+
export { default as openCandleExtension } from "./pi/opencandle-extension.js";
|
|
3
|
+
export { agentToolToPiTool, getOpenCandleToolDefinitions } from "./pi/tool-adapter.js";
|
|
4
|
+
export { registerTools } from "./tool-kit.js";
|
|
5
|
+
export { getAllTools } from "./tools/index.js";
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared stealth browser infrastructure using Camoufox (anti-detection Firefox).
|
|
3
|
+
* Provides a singleton browser instance that any tool can use for scraping.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* import { StealthBrowser } from "../infra/browser.js";
|
|
7
|
+
* const data = await StealthBrowser.fetchJson<MyType>(url);
|
|
8
|
+
* const result = await StealthBrowser.evaluate(url, () => document.title);
|
|
9
|
+
*/
|
|
10
|
+
import "./node-version.js";
|
|
11
|
+
import { Camoufox } from "camoufox-js";
|
|
12
|
+
import type { Browser, Page } from "playwright-core";
|
|
13
|
+
|
|
14
|
+
let browser: Browser | null = null;
|
|
15
|
+
let page: Page | null = null;
|
|
16
|
+
let launching: Promise<void> | null = null;
|
|
17
|
+
let pageQueue: Promise<void> = Promise.resolve();
|
|
18
|
+
|
|
19
|
+
async function ensureBrowser(): Promise<Page> {
|
|
20
|
+
if (page && browser?.isConnected()) return page;
|
|
21
|
+
|
|
22
|
+
// Prevent concurrent launches
|
|
23
|
+
if (launching) {
|
|
24
|
+
await launching;
|
|
25
|
+
if (page && browser?.isConnected()) return page;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
launching = (async () => {
|
|
29
|
+
const b = await Camoufox({ headless: true });
|
|
30
|
+
browser = b;
|
|
31
|
+
page = await b.newPage();
|
|
32
|
+
})();
|
|
33
|
+
|
|
34
|
+
await launching;
|
|
35
|
+
launching = null;
|
|
36
|
+
return page!;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function withPage<T>(fn: (p: Page) => Promise<T>): Promise<T> {
|
|
40
|
+
let resolve!: () => void;
|
|
41
|
+
const next = new Promise<void>((r) => { resolve = r; });
|
|
42
|
+
const prev = pageQueue;
|
|
43
|
+
pageQueue = next;
|
|
44
|
+
await prev;
|
|
45
|
+
try {
|
|
46
|
+
const p = await ensureBrowser();
|
|
47
|
+
return await fn(p);
|
|
48
|
+
} finally {
|
|
49
|
+
resolve();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const StealthBrowser = {
|
|
54
|
+
/**
|
|
55
|
+
* Navigate to a URL, run a JS function in the page context, and return the result.
|
|
56
|
+
*/
|
|
57
|
+
async evaluate<T>(url: string, fn: () => T | Promise<T>): Promise<T> {
|
|
58
|
+
return withPage(async (p) => {
|
|
59
|
+
await p.goto(url, { waitUntil: "domcontentloaded", timeout: 15000 });
|
|
60
|
+
return p.evaluate(fn);
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Fetch JSON from a URL using the browser's session (cookies, TLS fingerprint).
|
|
66
|
+
* Useful for APIs that block Node.js fetch but allow real browsers.
|
|
67
|
+
*/
|
|
68
|
+
async fetchJson<T>(url: string): Promise<T> {
|
|
69
|
+
return withPage(async (p) => {
|
|
70
|
+
const result = await p.evaluate(async (fetchUrl: string) => {
|
|
71
|
+
const res = await fetch(fetchUrl, { credentials: "include" });
|
|
72
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
73
|
+
return res.json();
|
|
74
|
+
}, url);
|
|
75
|
+
return result as T;
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Run a custom async function in the browser page context.
|
|
81
|
+
* The page must already be on a relevant domain for cookies to work.
|
|
82
|
+
*/
|
|
83
|
+
async run<T>(fn: (page: Page) => Promise<T>): Promise<T> {
|
|
84
|
+
return withPage(async (p) => fn(p));
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Navigate to a URL and establish session cookies for that domain.
|
|
89
|
+
*/
|
|
90
|
+
async initSession(url: string): Promise<void> {
|
|
91
|
+
return withPage(async (p) => {
|
|
92
|
+
await p.goto(url, { waitUntil: "domcontentloaded", timeout: 15000 });
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Close the browser. It will be re-launched on next use.
|
|
98
|
+
*/
|
|
99
|
+
async close(): Promise<void> {
|
|
100
|
+
if (browser) {
|
|
101
|
+
await browser.close().catch(() => {});
|
|
102
|
+
browser = null;
|
|
103
|
+
page = null;
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Clean up on process exit
|
|
109
|
+
process.on("exit", () => {
|
|
110
|
+
browser?.close().catch(() => {});
|
|
111
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
interface CacheEntry<T> {
|
|
2
|
+
value: T;
|
|
3
|
+
expiresAt: number;
|
|
4
|
+
cachedAt: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface StaleResult<T> {
|
|
8
|
+
value: T;
|
|
9
|
+
stale: true;
|
|
10
|
+
cachedAt: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class Cache {
|
|
14
|
+
private store = new Map<string, CacheEntry<unknown>>();
|
|
15
|
+
private lastStaleHit = false;
|
|
16
|
+
private lastStaleCachedAt = 0;
|
|
17
|
+
|
|
18
|
+
get<T>(key: string): T | undefined {
|
|
19
|
+
const entry = this.store.get(key);
|
|
20
|
+
if (!entry) return undefined;
|
|
21
|
+
if (Date.now() > entry.expiresAt) return undefined;
|
|
22
|
+
return entry.value as T;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Return an expired entry if it exists and is within the stale limit.
|
|
27
|
+
* Unlike get(), this does not require the entry to be within its TTL.
|
|
28
|
+
* Entries beyond the stale limit are deleted.
|
|
29
|
+
*/
|
|
30
|
+
getStale<T>(key: string, staleLimitMs: number): StaleResult<T> | undefined {
|
|
31
|
+
const entry = this.store.get(key);
|
|
32
|
+
if (!entry) return undefined;
|
|
33
|
+
|
|
34
|
+
if (Date.now() > entry.cachedAt + staleLimitMs) {
|
|
35
|
+
this.store.delete(key);
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.lastStaleHit = true;
|
|
40
|
+
this.lastStaleCachedAt = entry.cachedAt;
|
|
41
|
+
return { value: entry.value as T, stale: true, cachedAt: entry.cachedAt };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Consume the stale flag set by the last getStale() hit.
|
|
46
|
+
* Returns { stale: true, cachedAt } if the last getStale() found data,
|
|
47
|
+
* then resets the flag. Used by wrapProvider to propagate stale metadata.
|
|
48
|
+
*/
|
|
49
|
+
consumeStaleFlag(): { stale: boolean; cachedAt: number } {
|
|
50
|
+
const result = { stale: this.lastStaleHit, cachedAt: this.lastStaleCachedAt };
|
|
51
|
+
this.lastStaleHit = false;
|
|
52
|
+
this.lastStaleCachedAt = 0;
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
set<T>(key: string, value: T, ttlMs: number): void {
|
|
57
|
+
this.store.set(key, { value, expiresAt: Date.now() + ttlMs, cachedAt: Date.now() });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
invalidate(pattern: string): void {
|
|
61
|
+
for (const key of this.store.keys()) {
|
|
62
|
+
if (key.includes(pattern)) {
|
|
63
|
+
this.store.delete(key);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
clear(): void {
|
|
69
|
+
this.store.clear();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get size(): number {
|
|
73
|
+
return this.store.size;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Shared cache instance
|
|
78
|
+
export const cache = new Cache();
|
|
79
|
+
|
|
80
|
+
// Default TTLs
|
|
81
|
+
export const TTL = {
|
|
82
|
+
QUOTE: 60_000, // 1 minute
|
|
83
|
+
HISTORY: 3_600_000, // 1 hour
|
|
84
|
+
FUNDAMENTALS: 86_400_000, // 24 hours
|
|
85
|
+
MACRO: 3_600_000, // 1 hour
|
|
86
|
+
SENTIMENT: 300_000, // 5 minutes
|
|
87
|
+
OPTIONS_CHAIN: 120_000, // 2 minutes
|
|
88
|
+
CRUMB: 900_000, // 15 minutes
|
|
89
|
+
WEB_SEARCH: 300_000, // 5 minutes
|
|
90
|
+
FINNHUB_NEWS: 300_000, // 5 minutes
|
|
91
|
+
} as const;
|
|
92
|
+
|
|
93
|
+
// Stale limits — how long past TTL expiry a cached value is still useful as fallback
|
|
94
|
+
export const STALE_LIMIT = {
|
|
95
|
+
QUOTE: 15 * 60_000, // 15 minutes
|
|
96
|
+
HISTORY: 24 * 3_600_000, // 24 hours
|
|
97
|
+
FUNDAMENTALS: 7 * 86_400_000, // 7 days
|
|
98
|
+
MACRO: 24 * 3_600_000, // 24 hours
|
|
99
|
+
SENTIMENT: 3_600_000, // 1 hour
|
|
100
|
+
OPTIONS_CHAIN: 30 * 60_000, // 30 minutes
|
|
101
|
+
WEB_SEARCH: 3_600_000, // 1 hour
|
|
102
|
+
FINNHUB_NEWS: 3_600_000, // 1 hour
|
|
103
|
+
} as const;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export interface HttpClientOptions {
|
|
2
|
+
timeoutMs?: number;
|
|
3
|
+
maxRetries?: number;
|
|
4
|
+
retryDelayMs?: number;
|
|
5
|
+
headers?: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const DEFAULT_OPTIONS: Required<HttpClientOptions> = {
|
|
9
|
+
timeoutMs: 10_000,
|
|
10
|
+
maxRetries: 2,
|
|
11
|
+
retryDelayMs: 1_000,
|
|
12
|
+
headers: {},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export class HttpError extends Error {
|
|
16
|
+
constructor(
|
|
17
|
+
public readonly status: number,
|
|
18
|
+
public readonly statusText: string,
|
|
19
|
+
public readonly body: string,
|
|
20
|
+
) {
|
|
21
|
+
super(`HTTP ${status} ${statusText}`);
|
|
22
|
+
this.name = "HttpError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function httpGet<T>(
|
|
27
|
+
url: string,
|
|
28
|
+
options: HttpClientOptions = {},
|
|
29
|
+
): Promise<T> {
|
|
30
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
31
|
+
let lastError: Error | undefined;
|
|
32
|
+
|
|
33
|
+
for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
|
|
34
|
+
if (attempt > 0) {
|
|
35
|
+
await sleep(opts.retryDelayMs * attempt);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const controller = new AbortController();
|
|
39
|
+
const timeout = setTimeout(() => controller.abort(), opts.timeoutMs);
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const response = await fetch(url, {
|
|
43
|
+
signal: controller.signal,
|
|
44
|
+
headers: opts.headers,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
const body = await response.text().catch(() => "");
|
|
49
|
+
throw new HttpError(response.status, response.statusText, body);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (await response.json()) as T;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
lastError = error as Error;
|
|
55
|
+
if (error instanceof HttpError && error.status >= 400 && error.status < 500) {
|
|
56
|
+
throw error; // Don't retry client errors
|
|
57
|
+
}
|
|
58
|
+
} finally {
|
|
59
|
+
clearTimeout(timeout);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
throw lastError!;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function sleep(ms: number): Promise<void> {
|
|
67
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
68
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { Cache, cache, TTL } from "./cache.js";
|
|
2
|
+
export { RateLimiter, rateLimiter } from "./rate-limiter.js";
|
|
3
|
+
export { httpGet, HttpError, type HttpClientOptions } from "./http-client.js";
|
|
4
|
+
export { StealthBrowser } from "./browser.js";
|
|
5
|
+
export {
|
|
6
|
+
getOpenCandleHomeDir,
|
|
7
|
+
ensureOpenCandleHomeDir,
|
|
8
|
+
resolveOpenCandlePath,
|
|
9
|
+
ensureParentDir,
|
|
10
|
+
getWatchlistPath,
|
|
11
|
+
getPortfolioPath,
|
|
12
|
+
getPredictionsPath,
|
|
13
|
+
getConfigPath,
|
|
14
|
+
getOnboardingPath,
|
|
15
|
+
getStateDbPath,
|
|
16
|
+
getLogsDir,
|
|
17
|
+
getBrowserProfileDir,
|
|
18
|
+
} from "./opencandle-paths.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function getNativeDependencyErrorMessage(error: unknown, dependencyName: string): string | null {
|
|
2
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3
|
+
if (
|
|
4
|
+
!message.includes("NODE_MODULE_VERSION") &&
|
|
5
|
+
!message.includes("was compiled against a different Node.js version")
|
|
6
|
+
) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return `${dependencyName} native binding was built for a different Node ABI than the active Node ${process.versions.node}. ` +
|
|
11
|
+
`Run \`npm rebuild ${dependencyName}\` or reinstall dependencies under the active Node with \`npm install\`.`;
|
|
12
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const SUPPORTED_NODE_RANGE = "20.19+, 22.12+, or 24.x-26.x";
|
|
2
|
+
|
|
3
|
+
function isSupportedNodeVersion(version: string): boolean {
|
|
4
|
+
const [majorRaw, minorRaw] = version.split(".");
|
|
5
|
+
const major = Number(majorRaw);
|
|
6
|
+
const minor = Number(minorRaw);
|
|
7
|
+
|
|
8
|
+
if (major === 20) return minor >= 19;
|
|
9
|
+
if (major === 22) return minor >= 12;
|
|
10
|
+
return major >= 24 && major < 27;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getUnsupportedNodeVersionMessage(version: string = process.versions.node): string | null {
|
|
14
|
+
if (isSupportedNodeVersion(version)) return null;
|
|
15
|
+
|
|
16
|
+
return `OpenCandle supports Node ${SUPPORTED_NODE_RANGE}. Current Node is ${version}. Use Node ${SUPPORTED_NODE_RANGE}; the repo default is Node 22.22.0 via \`nvm use\`. After switching Node versions, reinstall dependencies under the active Node with \`npm install\` or rebuild native modules with \`npm rebuild better-sqlite3\`.`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function assertSupportedNodeVersion(version?: string): void {
|
|
20
|
+
const message = getUnsupportedNodeVersionMessage(version);
|
|
21
|
+
if (message) throw new Error(message);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
assertSupportedNodeVersion();
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export function openInBrowser(url: string): Promise<void> {
|
|
4
|
+
return new Promise<void>((resolve, reject) => {
|
|
5
|
+
let command: string;
|
|
6
|
+
let args: string[];
|
|
7
|
+
|
|
8
|
+
if (process.platform === "darwin") {
|
|
9
|
+
command = "open";
|
|
10
|
+
args = [url];
|
|
11
|
+
} else if (process.platform === "win32") {
|
|
12
|
+
command = "cmd";
|
|
13
|
+
args = ["/c", "start", "", url];
|
|
14
|
+
} else {
|
|
15
|
+
command = "xdg-open";
|
|
16
|
+
args = [url];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const child = execFile(command, args, { timeout: 5000 }, (error) => {
|
|
20
|
+
if (error) {
|
|
21
|
+
reject(error);
|
|
22
|
+
} else {
|
|
23
|
+
resolve();
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
child.unref();
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
const OPENCANDLE_HOME_ENV = "OPENCANDLE_HOME";
|
|
6
|
+
|
|
7
|
+
function ensureDir(path: string): void {
|
|
8
|
+
if (!existsSync(path)) {
|
|
9
|
+
mkdirSync(path, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getOpenCandleHomeDir(): string {
|
|
14
|
+
const override = process.env[OPENCANDLE_HOME_ENV];
|
|
15
|
+
return override ? resolve(override) : join(homedir(), ".opencandle");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ensureOpenCandleHomeDir(): string {
|
|
19
|
+
const home = getOpenCandleHomeDir();
|
|
20
|
+
ensureDir(home);
|
|
21
|
+
return home;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveOpenCandlePath(...segments: string[]): string {
|
|
25
|
+
return join(getOpenCandleHomeDir(), ...segments);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function ensureParentDir(path: string): string {
|
|
29
|
+
const parent = dirname(path);
|
|
30
|
+
ensureDir(parent);
|
|
31
|
+
return parent;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getWatchlistPath(): string {
|
|
35
|
+
return resolveOpenCandlePath("watchlist.json");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getPortfolioPath(): string {
|
|
39
|
+
return resolveOpenCandlePath("portfolio.json");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getPredictionsPath(): string {
|
|
43
|
+
return resolveOpenCandlePath("predictions.json");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getConfigPath(): string {
|
|
47
|
+
return resolveOpenCandlePath("config.json");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getOnboardingPath(): string {
|
|
51
|
+
return resolveOpenCandlePath("onboarding.json");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getStateDbPath(): string {
|
|
55
|
+
return resolveOpenCandlePath("state.db");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getLogsDir(): string {
|
|
59
|
+
return resolveOpenCandlePath("logs");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getBrowserProfileDir(): string {
|
|
63
|
+
return resolveOpenCandlePath("browser-profile");
|
|
64
|
+
}
|