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,111 @@
|
|
|
1
|
+
// Tool-boundary helpers for credentialed providers.
|
|
2
|
+
//
|
|
3
|
+
// `withCredentialCheck` is the single-entry helper that OpenCandle tools use
|
|
4
|
+
// to wrap their provider calls. It centralizes:
|
|
5
|
+
// 1. The upfront "credential missing" check via the registry.
|
|
6
|
+
// 2. The catch-and-convert logic for `ProviderCredentialError` thrown from
|
|
7
|
+
// providers on 401/403 (stale credential after HTTP call).
|
|
8
|
+
// 3. The tagged `[OPENCANDLE_CREDENTIAL_REQUIRED ...]` content emission that
|
|
9
|
+
// the Pi `tool_result` extension hook intercepts.
|
|
10
|
+
//
|
|
11
|
+
// Non-credential errors (network timeouts, parse errors, etc.) are re-thrown
|
|
12
|
+
// unchanged so existing error handling in `wrapProvider` continues to work.
|
|
13
|
+
|
|
14
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
15
|
+
import { ProviderCredentialError } from "../providers/provider-credential-error.js";
|
|
16
|
+
import { getProvider, hasCredential, type ProviderId } from "./providers.js";
|
|
17
|
+
import {
|
|
18
|
+
buildCredentialRequiredTag,
|
|
19
|
+
type CredentialRequiredReason,
|
|
20
|
+
} from "./tool-tags.js";
|
|
21
|
+
|
|
22
|
+
// Metadata carried on `details` for UI / test assertion. This is NOT the
|
|
23
|
+
// LLM-facing contract — that lives in `content` as the tagged line. Pi
|
|
24
|
+
// provider adapters serialize `content`, not `details`, into the model's
|
|
25
|
+
// conversation.
|
|
26
|
+
export interface CredentialRequiredDetails {
|
|
27
|
+
credentialRequired: {
|
|
28
|
+
provider: ProviderId;
|
|
29
|
+
reason: CredentialRequiredReason;
|
|
30
|
+
httpStatus?: number;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function buildCredentialRequiredResult(
|
|
35
|
+
provider: ProviderId,
|
|
36
|
+
reason: CredentialRequiredReason,
|
|
37
|
+
httpStatus?: number,
|
|
38
|
+
): AgentToolResult<CredentialRequiredDetails> {
|
|
39
|
+
const descriptor = getProvider(provider);
|
|
40
|
+
const tag = buildCredentialRequiredTag({
|
|
41
|
+
provider,
|
|
42
|
+
reason,
|
|
43
|
+
unlocks: descriptor.unlocks,
|
|
44
|
+
fallback: descriptor.fallbackDescription,
|
|
45
|
+
httpStatus,
|
|
46
|
+
});
|
|
47
|
+
// The second line is natural language describing what's missing. Users won't
|
|
48
|
+
// see it directly — the model sees it and uses the information to synthesize
|
|
49
|
+
// a gap note in its final answer. The tagged first line is the machine-
|
|
50
|
+
// readable signal the `tool_result` interception handler keys off of.
|
|
51
|
+
const narrative =
|
|
52
|
+
reason === "missing"
|
|
53
|
+
? `${descriptor.displayName} was not fetched for this request because no API key is configured. It unlocks ${descriptor.unlocks.join(", ")}.`
|
|
54
|
+
: `${descriptor.displayName} rejected the configured API key (HTTP ${httpStatus ?? "auth error"}). It may be expired or revoked.`;
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
content: [{ type: "text", text: `${tag}\n\n${narrative}` }],
|
|
58
|
+
details: {
|
|
59
|
+
credentialRequired: {
|
|
60
|
+
provider,
|
|
61
|
+
reason,
|
|
62
|
+
...(httpStatus !== undefined ? { httpStatus } : {}),
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Wrap a tool's provider-calling logic with a credential-check layer.
|
|
70
|
+
*
|
|
71
|
+
* Usage:
|
|
72
|
+
* ```
|
|
73
|
+
* async execute(_, args) {
|
|
74
|
+
* return withCredentialCheck("alpha_vantage", async () => {
|
|
75
|
+
* const apiKey = getConfig().alphaVantageApiKey!;
|
|
76
|
+
* const result = await wrapProvider("alphavantage", () => getFinancials(args.symbol, apiKey));
|
|
77
|
+
* // ... format success result ...
|
|
78
|
+
* return { content: [...], details: [...] };
|
|
79
|
+
* });
|
|
80
|
+
* }
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* On missing credential (upfront): `fn` is NOT called. A tagged tool result
|
|
84
|
+
* is returned directly.
|
|
85
|
+
*
|
|
86
|
+
* On `ProviderCredentialError` thrown during `fn`: the error is caught and a
|
|
87
|
+
* tagged tool result is returned.
|
|
88
|
+
*
|
|
89
|
+
* Any other thrown value (plain Error, string, etc.) is re-thrown unchanged.
|
|
90
|
+
*/
|
|
91
|
+
export async function withCredentialCheck<T>(
|
|
92
|
+
providerId: ProviderId,
|
|
93
|
+
fn: () => Promise<AgentToolResult<T>>,
|
|
94
|
+
): Promise<AgentToolResult<T> | AgentToolResult<CredentialRequiredDetails>> {
|
|
95
|
+
if (!hasCredential(providerId)) {
|
|
96
|
+
return buildCredentialRequiredResult(providerId, "missing");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
return await fn();
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (error instanceof ProviderCredentialError) {
|
|
103
|
+
return buildCredentialRequiredResult(
|
|
104
|
+
error.provider,
|
|
105
|
+
error.reason,
|
|
106
|
+
error.httpStatus,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// Tool-result text tags used by OpenCandle's conversational-provider-setup
|
|
2
|
+
// flow. These tagged lines are the LLM-facing contract — Pi provider adapters
|
|
3
|
+
// serialize the `content` field of tool results into the model's conversation,
|
|
4
|
+
// so any signal we want the model to reason over must live in text, not
|
|
5
|
+
// `details`.
|
|
6
|
+
//
|
|
7
|
+
// Tag format (single line, parseable):
|
|
8
|
+
// [OPENCANDLE_<KIND> field=value field2=value ... ]
|
|
9
|
+
//
|
|
10
|
+
// Values may be:
|
|
11
|
+
// - bare: provider=alpha_vantage reason=missing silenced=true httpStatus=401
|
|
12
|
+
// - quoted (for spaces or commas): remediation="run /connect economy"
|
|
13
|
+
// - lists (comma-separated inside quotes): unlocks="fundamentals, DCF"
|
|
14
|
+
// - null/absent: fallback=none
|
|
15
|
+
//
|
|
16
|
+
// Parser is tolerant of unknown fields and extra whitespace.
|
|
17
|
+
|
|
18
|
+
import type { ProviderId } from "./providers.js";
|
|
19
|
+
|
|
20
|
+
// -----------------------------------------------------------------------------
|
|
21
|
+
// Public types
|
|
22
|
+
// -----------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
export type CredentialRequiredReason = "missing" | "stale";
|
|
25
|
+
|
|
26
|
+
export interface CredentialRequiredTagFields {
|
|
27
|
+
provider: ProviderId;
|
|
28
|
+
reason: CredentialRequiredReason;
|
|
29
|
+
unlocks: readonly string[];
|
|
30
|
+
fallback: string | null;
|
|
31
|
+
httpStatus?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface SoftDegradedTagFields {
|
|
35
|
+
provider: ProviderId;
|
|
36
|
+
fallback: string;
|
|
37
|
+
remediation: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SkippedTagFields {
|
|
41
|
+
provider: ProviderId;
|
|
42
|
+
reason: "credential_not_provided";
|
|
43
|
+
remediation: string;
|
|
44
|
+
silenced?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ConnectedTagFields {
|
|
48
|
+
provider: ProviderId;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type ParsedTag =
|
|
52
|
+
| ({ kind: "credential_required" } & CredentialRequiredTagFields)
|
|
53
|
+
| ({ kind: "soft_degraded" } & SoftDegradedTagFields)
|
|
54
|
+
| ({ kind: "skipped" } & SkippedTagFields)
|
|
55
|
+
| ({ kind: "connected" } & ConnectedTagFields);
|
|
56
|
+
|
|
57
|
+
// -----------------------------------------------------------------------------
|
|
58
|
+
// Builders
|
|
59
|
+
// -----------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
function quote(value: string): string {
|
|
62
|
+
return `"${value.replace(/"/g, '\\"')}"`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function formatField(key: string, value: string | number | boolean): string {
|
|
66
|
+
return `${key}=${value}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function buildCredentialRequiredTag(fields: CredentialRequiredTagFields): string {
|
|
70
|
+
const parts: string[] = [
|
|
71
|
+
"[OPENCANDLE_CREDENTIAL_REQUIRED",
|
|
72
|
+
formatField("provider", fields.provider),
|
|
73
|
+
formatField("reason", fields.reason),
|
|
74
|
+
`unlocks=${quote(fields.unlocks.join(", "))}`,
|
|
75
|
+
formatField("fallback", fields.fallback ?? "none"),
|
|
76
|
+
];
|
|
77
|
+
if (fields.httpStatus !== undefined) {
|
|
78
|
+
parts.push(formatField("httpStatus", fields.httpStatus));
|
|
79
|
+
}
|
|
80
|
+
return `${parts.join(" ")}]`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function buildSoftDegradedTag(fields: SoftDegradedTagFields): string {
|
|
84
|
+
return [
|
|
85
|
+
"[OPENCANDLE_SOFT_DEGRADED",
|
|
86
|
+
formatField("provider", fields.provider),
|
|
87
|
+
formatField("fallback", fields.fallback),
|
|
88
|
+
`remediation=${quote(fields.remediation)}`,
|
|
89
|
+
].join(" ") + "]";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function buildSkippedTag(fields: SkippedTagFields): string {
|
|
93
|
+
const parts: string[] = [
|
|
94
|
+
"[OPENCANDLE_SKIPPED",
|
|
95
|
+
formatField("provider", fields.provider),
|
|
96
|
+
formatField("reason", fields.reason),
|
|
97
|
+
`remediation=${quote(fields.remediation)}`,
|
|
98
|
+
];
|
|
99
|
+
if (fields.silenced) {
|
|
100
|
+
parts.push(formatField("silenced", "true"));
|
|
101
|
+
}
|
|
102
|
+
return `${parts.join(" ")}]`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function buildConnectedTag(fields: ConnectedTagFields): string {
|
|
106
|
+
return `[OPENCANDLE_CONNECTED provider=${fields.provider}]`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// -----------------------------------------------------------------------------
|
|
110
|
+
// Parser
|
|
111
|
+
// -----------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
const TAG_LINE_REGEX =
|
|
114
|
+
/\[OPENCANDLE_(CREDENTIAL_REQUIRED|SOFT_DEGRADED|SKIPPED|CONNECTED)([^\]]*)\]/;
|
|
115
|
+
|
|
116
|
+
function parseFields(raw: string): Record<string, string> {
|
|
117
|
+
// Split on `key=value` pairs, respecting quoted values.
|
|
118
|
+
const fields: Record<string, string> = {};
|
|
119
|
+
const pattern = /(\w+)=("((?:[^"\\]|\\.)*)"|([^\s\]]+))/g;
|
|
120
|
+
let match: RegExpExecArray | null;
|
|
121
|
+
while ((match = pattern.exec(raw)) !== null) {
|
|
122
|
+
const key = match[1];
|
|
123
|
+
const value = match[3] !== undefined ? match[3].replace(/\\"/g, '"') : match[4];
|
|
124
|
+
fields[key] = value;
|
|
125
|
+
}
|
|
126
|
+
return fields;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function parseToolTag(text: string): ParsedTag | undefined {
|
|
130
|
+
const match = TAG_LINE_REGEX.exec(text);
|
|
131
|
+
if (!match) return undefined;
|
|
132
|
+
|
|
133
|
+
const kind = match[1];
|
|
134
|
+
const rest = match[2];
|
|
135
|
+
const fields = parseFields(rest);
|
|
136
|
+
|
|
137
|
+
switch (kind) {
|
|
138
|
+
case "CREDENTIAL_REQUIRED": {
|
|
139
|
+
const provider = fields.provider;
|
|
140
|
+
const reason = fields.reason;
|
|
141
|
+
const unlocksRaw = fields.unlocks ?? "";
|
|
142
|
+
const fallbackRaw = fields.fallback;
|
|
143
|
+
if (!provider || (reason !== "missing" && reason !== "stale")) {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
const unlocks = unlocksRaw
|
|
147
|
+
.split(",")
|
|
148
|
+
.map((s) => s.trim())
|
|
149
|
+
.filter((s) => s.length > 0);
|
|
150
|
+
const fallback =
|
|
151
|
+
fallbackRaw === undefined || fallbackRaw === "none" ? null : fallbackRaw;
|
|
152
|
+
const parsed: ParsedTag = {
|
|
153
|
+
kind: "credential_required",
|
|
154
|
+
provider: provider as ProviderId,
|
|
155
|
+
reason: reason as CredentialRequiredReason,
|
|
156
|
+
unlocks,
|
|
157
|
+
fallback,
|
|
158
|
+
};
|
|
159
|
+
if (fields.httpStatus !== undefined) {
|
|
160
|
+
const n = Number(fields.httpStatus);
|
|
161
|
+
if (!Number.isNaN(n)) parsed.httpStatus = n;
|
|
162
|
+
}
|
|
163
|
+
return parsed;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
case "SOFT_DEGRADED": {
|
|
167
|
+
const { provider, fallback, remediation } = fields;
|
|
168
|
+
if (!provider || !fallback || !remediation) return undefined;
|
|
169
|
+
return {
|
|
170
|
+
kind: "soft_degraded",
|
|
171
|
+
provider: provider as ProviderId,
|
|
172
|
+
fallback,
|
|
173
|
+
remediation,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
case "SKIPPED": {
|
|
178
|
+
const { provider, reason, remediation } = fields;
|
|
179
|
+
if (!provider || reason !== "credential_not_provided" || !remediation) {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
const parsed: ParsedTag = {
|
|
183
|
+
kind: "skipped",
|
|
184
|
+
provider: provider as ProviderId,
|
|
185
|
+
reason: "credential_not_provided",
|
|
186
|
+
remediation,
|
|
187
|
+
};
|
|
188
|
+
if (fields.silenced === "true") parsed.silenced = true;
|
|
189
|
+
return parsed;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
case "CONNECTED": {
|
|
193
|
+
const { provider } = fields;
|
|
194
|
+
if (!provider) return undefined;
|
|
195
|
+
return { kind: "connected", provider: provider as ProviderId };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
default:
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Pre-persist credential validation for OpenCandle data providers.
|
|
2
|
+
//
|
|
3
|
+
// Each `validateCredential` call makes a single cheap request to the
|
|
4
|
+
// provider's canonical API with the pasted key and classifies the response
|
|
5
|
+
// into one of three states:
|
|
6
|
+
//
|
|
7
|
+
// - `valid` — provider accepted the credential (persist it)
|
|
8
|
+
// - `invalid` — provider rejected the credential (do NOT persist;
|
|
9
|
+
// re-prompt or return a classified failure)
|
|
10
|
+
// - `transient` — network/timeout/server error (persist anyway; the next
|
|
11
|
+
// tool call will surface any real issue and re-open the
|
|
12
|
+
// prompt via the credential-required tag)
|
|
13
|
+
//
|
|
14
|
+
// The endpoint chosen for each provider is the cheapest one available that
|
|
15
|
+
// also exercises the auth header/query param — we don't pay for meaningful
|
|
16
|
+
// data, just "did the server like the key". Alpha Vantage is a special case:
|
|
17
|
+
// it returns HTTP 200 even for bad keys, with an `Error Message` or
|
|
18
|
+
// `Information` field in the JSON body, so the body must be inspected.
|
|
19
|
+
//
|
|
20
|
+
// All validators share a 5-second timeout via `AbortSignal.timeout` so the
|
|
21
|
+
// connect flow cannot hang if a provider is unreachable.
|
|
22
|
+
|
|
23
|
+
import type { ProviderId } from "./providers.js";
|
|
24
|
+
|
|
25
|
+
const VALIDATION_TIMEOUT_MS = 5_000;
|
|
26
|
+
|
|
27
|
+
export type ValidationResult =
|
|
28
|
+
| { status: "valid" }
|
|
29
|
+
| { status: "invalid"; httpStatus?: number; message?: string }
|
|
30
|
+
| { status: "transient"; reason: string };
|
|
31
|
+
|
|
32
|
+
type BodyClassifier = (body: string) => ValidationResult | undefined;
|
|
33
|
+
|
|
34
|
+
async function validateWithFetch(
|
|
35
|
+
url: string,
|
|
36
|
+
init: RequestInit,
|
|
37
|
+
classifyBody?: BodyClassifier,
|
|
38
|
+
): Promise<ValidationResult> {
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetch(url, {
|
|
41
|
+
...init,
|
|
42
|
+
signal: AbortSignal.timeout(VALIDATION_TIMEOUT_MS),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Hard auth failures — the key is definitively bad.
|
|
46
|
+
if (response.status === 401 || response.status === 403) {
|
|
47
|
+
return { status: "invalid", httpStatus: response.status };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// FRED returns 400 with an `error_message` for bad keys.
|
|
51
|
+
if (response.status === 400) {
|
|
52
|
+
return { status: "invalid", httpStatus: 400 };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 5xx and other non-2xx — treat as transient. Don't block the user on a
|
|
56
|
+
// provider outage; the next real tool call will surface a fresh error.
|
|
57
|
+
if (!response.ok) {
|
|
58
|
+
return { status: "transient", reason: `HTTP ${response.status}` };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Body-level classification for providers that return 200 with an error
|
|
62
|
+
// shape (Alpha Vantage).
|
|
63
|
+
if (classifyBody) {
|
|
64
|
+
const body = await response.text();
|
|
65
|
+
const classified = classifyBody(body);
|
|
66
|
+
if (classified) return classified;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { status: "valid" };
|
|
70
|
+
} catch (err) {
|
|
71
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
72
|
+
return { status: "transient", reason };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Alpha Vantage returns HTTP 200 with an error shape in the JSON body when
|
|
78
|
+
* the key is bad. Two fields to watch for:
|
|
79
|
+
* - `Error Message`: indicates a malformed query OR an invalid key
|
|
80
|
+
* - `Information`: used for rate-limit messages AND for "Invalid API key"
|
|
81
|
+
*
|
|
82
|
+
* The rate-limit form of `Information` is NOT an invalid-key signal, so we
|
|
83
|
+
* only match when the string mentions "invalid api".
|
|
84
|
+
*/
|
|
85
|
+
function classifyAlphaVantageBody(body: string): ValidationResult | undefined {
|
|
86
|
+
try {
|
|
87
|
+
const parsed = JSON.parse(body) as Record<string, unknown>;
|
|
88
|
+
const errorMessage = parsed["Error Message"];
|
|
89
|
+
if (typeof errorMessage === "string" && errorMessage.length > 0) {
|
|
90
|
+
return { status: "invalid", message: errorMessage };
|
|
91
|
+
}
|
|
92
|
+
const information = parsed["Information"];
|
|
93
|
+
if (typeof information === "string" && /invalid api/i.test(information)) {
|
|
94
|
+
return { status: "invalid", message: information };
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
// Non-JSON body — fall through and trust the HTTP status.
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Validate a pasted credential against the provider's API. Returns a
|
|
104
|
+
* ValidationResult describing whether the key should be persisted.
|
|
105
|
+
*
|
|
106
|
+
* This function is side-effect-free beyond the network call: it does not
|
|
107
|
+
* touch the config file, onboarding state, or any in-memory cache.
|
|
108
|
+
*/
|
|
109
|
+
export async function validateCredential(
|
|
110
|
+
providerId: ProviderId,
|
|
111
|
+
key: string,
|
|
112
|
+
): Promise<ValidationResult> {
|
|
113
|
+
switch (providerId) {
|
|
114
|
+
case "alpha_vantage":
|
|
115
|
+
return validateWithFetch(
|
|
116
|
+
`https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=IBM&apikey=${encodeURIComponent(key)}`,
|
|
117
|
+
{ method: "GET" },
|
|
118
|
+
classifyAlphaVantageBody,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
case "fred":
|
|
122
|
+
return validateWithFetch(
|
|
123
|
+
`https://api.stlouisfed.org/fred/series?series_id=GDP&api_key=${encodeURIComponent(key)}&file_type=json`,
|
|
124
|
+
{ method: "GET" },
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
case "finnhub":
|
|
128
|
+
return validateWithFetch(
|
|
129
|
+
`https://finnhub.io/api/v1/quote?symbol=AAPL&token=${encodeURIComponent(key)}`,
|
|
130
|
+
{ method: "GET" },
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
case "brave":
|
|
134
|
+
return validateWithFetch(
|
|
135
|
+
"https://api.search.brave.com/res/v1/web/search?q=test&count=1",
|
|
136
|
+
{
|
|
137
|
+
method: "GET",
|
|
138
|
+
headers: {
|
|
139
|
+
"X-Subscription-Token": key,
|
|
140
|
+
Accept: "application/json",
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
case "exa":
|
|
146
|
+
return validateWithFetch(
|
|
147
|
+
"https://api.exa.ai/search",
|
|
148
|
+
{
|
|
149
|
+
method: "POST",
|
|
150
|
+
headers: {
|
|
151
|
+
"Content-Type": "application/json",
|
|
152
|
+
"x-api-key": key,
|
|
153
|
+
},
|
|
154
|
+
body: JSON.stringify({ query: "test", numResults: 1 }),
|
|
155
|
+
},
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|