opencandle 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -23
- package/dist/cli-main.d.ts +1 -0
- package/dist/cli-main.js +200 -0
- package/dist/cli-main.js.map +1 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +14 -194
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +10 -0
- package/dist/config.js +13 -0
- package/dist/config.js.map +1 -1
- package/dist/doctor/cli-command.d.ts +1 -0
- package/dist/doctor/cli-command.js +69 -0
- package/dist/doctor/cli-command.js.map +1 -0
- package/dist/doctor/render.d.ts +2 -0
- package/dist/doctor/render.js +32 -0
- package/dist/doctor/render.js.map +1 -0
- package/dist/doctor/report.d.ts +63 -0
- package/dist/doctor/report.js +500 -0
- package/dist/doctor/report.js.map +1 -0
- package/dist/infra/index.d.ts +0 -1
- package/dist/infra/index.js +0 -1
- package/dist/infra/index.js.map +1 -1
- package/dist/infra/native-dependencies.d.ts +8 -0
- package/dist/infra/native-dependencies.js +49 -0
- package/dist/infra/native-dependencies.js.map +1 -1
- package/dist/infra/node-version.js +2 -5
- package/dist/infra/node-version.js.map +1 -1
- package/dist/monitor.d.ts +1 -1
- package/dist/monitor.js +2 -1
- package/dist/monitor.js.map +1 -1
- package/dist/onboarding/connect.d.ts +2 -2
- package/dist/onboarding/connect.js +10 -3
- package/dist/onboarding/connect.js.map +1 -1
- package/dist/onboarding/provider-status.d.ts +46 -0
- package/dist/onboarding/provider-status.js +256 -0
- package/dist/onboarding/provider-status.js.map +1 -0
- package/dist/onboarding/providers.d.ts +85 -8
- package/dist/onboarding/providers.js +87 -9
- package/dist/onboarding/providers.js.map +1 -1
- package/dist/onboarding/state.d.ts +1 -0
- package/dist/onboarding/state.js +5 -0
- package/dist/onboarding/state.js.map +1 -1
- package/dist/onboarding/tool-tags.d.ts +12 -1
- package/dist/onboarding/tool-tags.js +31 -1
- package/dist/onboarding/tool-tags.js.map +1 -1
- package/dist/onboarding/validation.d.ts +2 -2
- package/dist/onboarding/validation.js.map +1 -1
- package/dist/pi/opencandle-extension.js +91 -15
- package/dist/pi/opencandle-extension.js.map +1 -1
- package/dist/pi/session.d.ts +0 -1
- package/dist/pi/session.js +2 -1
- package/dist/pi/session.js.map +1 -1
- package/dist/pi/tool-adapter.d.ts +4 -1
- package/dist/pi/tool-adapter.js +5 -4
- package/dist/pi/tool-adapter.js.map +1 -1
- package/dist/prompts/context-builder.js +1 -1
- package/dist/prompts/policy-cards.js +9 -5
- package/dist/prompts/policy-cards.js.map +1 -1
- package/dist/prompts/workflow-prompts.js +10 -6
- package/dist/prompts/workflow-prompts.js.map +1 -1
- package/dist/providers/external-tool-command.d.ts +17 -0
- package/dist/providers/external-tool-command.js +114 -0
- package/dist/providers/external-tool-command.js.map +1 -0
- package/dist/providers/external-tool-error.d.ts +10 -0
- package/dist/providers/external-tool-error.js +21 -0
- package/dist/providers/external-tool-error.js.map +1 -0
- package/dist/providers/reddit-cli.d.ts +32 -0
- package/dist/providers/reddit-cli.js +172 -0
- package/dist/providers/reddit-cli.js.map +1 -0
- package/dist/providers/reddit.d.ts +1 -1
- package/dist/providers/reddit.js +7 -35
- package/dist/providers/reddit.js.map +1 -1
- package/dist/providers/twitter-cli.d.ts +36 -0
- package/dist/providers/twitter-cli.js +124 -0
- package/dist/providers/twitter-cli.js.map +1 -0
- package/dist/providers/twitter.d.ts +0 -8
- package/dist/providers/twitter.js +4 -54
- package/dist/providers/twitter.js.map +1 -1
- package/dist/providers/wrap-provider.js +30 -0
- package/dist/providers/wrap-provider.js.map +1 -1
- package/dist/providers/yahoo-finance.js +53 -32
- package/dist/providers/yahoo-finance.js.map +1 -1
- package/dist/routing/classify-intent.js +7 -3
- package/dist/routing/classify-intent.js.map +1 -1
- package/dist/routing/planning.d.ts +1 -1
- package/dist/routing/planning.js +9 -6
- package/dist/routing/planning.js.map +1 -1
- package/dist/routing/router.js +7 -3
- package/dist/routing/router.js.map +1 -1
- package/dist/runtime/answer-contracts.d.ts +1 -1
- package/dist/runtime/answer-contracts.js +12 -1
- package/dist/runtime/answer-contracts.js.map +1 -1
- package/dist/runtime/tool-defaults-wrapper.js +6 -2
- package/dist/runtime/tool-defaults-wrapper.js.map +1 -1
- package/dist/sentiment/index.d.ts +1 -0
- package/dist/sentiment/index.js +1 -0
- package/dist/sentiment/index.js.map +1 -1
- package/dist/sentiment/insights.d.ts +17 -0
- package/dist/sentiment/insights.js +206 -0
- package/dist/sentiment/insights.js.map +1 -0
- package/dist/sentiment/pipeline.js +13 -1
- package/dist/sentiment/pipeline.js.map +1 -1
- package/dist/sentiment/scorer.d.ts +2 -0
- package/dist/sentiment/scorer.js +10 -1
- package/dist/sentiment/scorer.js.map +1 -1
- package/dist/sentiment/types.d.ts +2 -0
- package/dist/sentiment/types.js.map +1 -1
- package/dist/system-prompt.js +3 -7
- package/dist/system-prompt.js.map +1 -1
- package/dist/tools/index.d.ts +5 -2
- package/dist/tools/index.js +8 -8
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/sentiment/insight-format.d.ts +2 -0
- package/dist/tools/sentiment/insight-format.js +36 -0
- package/dist/tools/sentiment/insight-format.js.map +1 -0
- package/dist/tools/sentiment/query-match.d.ts +3 -0
- package/dist/tools/sentiment/query-match.js +113 -0
- package/dist/tools/sentiment/query-match.js.map +1 -0
- package/dist/tools/sentiment/reddit-sentiment.d.ts +12 -1
- package/dist/tools/sentiment/reddit-sentiment.js +263 -117
- package/dist/tools/sentiment/reddit-sentiment.js.map +1 -1
- package/dist/tools/sentiment/sentiment-summary.d.ts +9 -1
- package/dist/tools/sentiment/sentiment-summary.js +217 -201
- package/dist/tools/sentiment/sentiment-summary.js.map +1 -1
- package/dist/tools/sentiment/twitter-sentiment.d.ts +11 -1
- package/dist/tools/sentiment/twitter-sentiment.js +187 -64
- package/dist/tools/sentiment/twitter-sentiment.js.map +1 -1
- package/dist/tools/sentiment/web-sentiment.js +4 -0
- package/dist/tools/sentiment/web-sentiment.js.map +1 -1
- package/dist/types/sentiment.d.ts +52 -0
- package/gui/server/http-routes.ts +419 -0
- package/gui/server/invoke-tool.ts +17 -3
- package/gui/server/model-setup.ts +10 -3
- package/gui/server/projector.ts +6 -2
- package/gui/server/server.ts +30 -275
- package/gui/server/tool-metadata.ts +80 -16
- package/gui/server/ws-hub.ts +19 -0
- package/gui/web/dist/assets/CatalogOverlay-CCVKwBUB.js +1 -0
- package/gui/web/dist/assets/index-Dm4Aom2_.js +69 -0
- package/gui/web/dist/assets/{index-2KZtKBmu.css → index-UzZUg3dx.css} +1 -1
- package/gui/web/dist/index.html +2 -2
- package/package.json +17 -14
- package/src/cli-main.ts +233 -0
- package/src/cli.ts +14 -226
- package/src/config.ts +27 -0
- package/src/doctor/cli-command.ts +83 -0
- package/src/doctor/render.ts +37 -0
- package/src/doctor/report.ts +638 -0
- package/src/infra/index.ts +0 -1
- package/src/infra/native-dependencies.ts +67 -0
- package/src/infra/node-version.ts +2 -5
- package/src/monitor.ts +3 -1
- package/src/onboarding/connect.ts +20 -4
- package/src/onboarding/provider-status.ts +382 -0
- package/src/onboarding/providers.ts +148 -18
- package/src/onboarding/state.ts +9 -0
- package/src/onboarding/tool-tags.ts +45 -2
- package/src/onboarding/validation.ts +2 -2
- package/src/pi/opencandle-extension.ts +115 -17
- package/src/pi/session.ts +2 -1
- package/src/pi/tool-adapter.ts +14 -4
- package/src/prompts/context-builder.ts +1 -1
- package/src/prompts/policy-cards.ts +13 -5
- package/src/prompts/workflow-prompts.ts +13 -6
- package/src/providers/external-tool-command.ts +164 -0
- package/src/providers/external-tool-error.ts +20 -0
- package/src/providers/reddit-cli.ts +286 -0
- package/src/providers/reddit.ts +7 -63
- package/src/providers/twitter-cli.ts +202 -0
- package/src/providers/twitter.ts +4 -73
- package/src/providers/wrap-provider.ts +34 -0
- package/src/providers/yahoo-finance.ts +65 -32
- package/src/routing/classify-intent.ts +11 -6
- package/src/routing/planning.ts +16 -10
- package/src/routing/router.ts +11 -6
- package/src/runtime/answer-contracts.ts +23 -2
- package/src/runtime/tool-defaults-wrapper.ts +12 -2
- package/src/sentiment/index.ts +1 -0
- package/src/sentiment/insights.ts +269 -0
- package/src/sentiment/pipeline.ts +13 -1
- package/src/sentiment/scorer.ts +12 -1
- package/src/sentiment/types.ts +3 -0
- package/src/system-prompt.ts +3 -7
- package/src/tools/index.ts +9 -8
- package/src/tools/sentiment/insight-format.ts +50 -0
- package/src/tools/sentiment/query-match.ts +117 -0
- package/src/tools/sentiment/reddit-sentiment.ts +354 -141
- package/src/tools/sentiment/sentiment-summary.ts +283 -237
- package/src/tools/sentiment/twitter-sentiment.ts +262 -78
- package/src/tools/sentiment/web-sentiment.ts +4 -0
- package/src/types/sentiment.ts +59 -0
- package/dist/infra/browser.d.ts +0 -35
- package/dist/infra/browser.js +0 -105
- package/dist/infra/browser.js.map +0 -1
- package/dist/tools/interaction/twitter-login.d.ts +0 -8
- package/dist/tools/interaction/twitter-login.js +0 -87
- package/dist/tools/interaction/twitter-login.js.map +0 -1
- package/gui/web/dist/assets/CatalogOverlay-eJ2cBk33.js +0 -1
- package/gui/web/dist/assets/index-CveNgtDg.js +0 -69
- package/src/infra/browser.ts +0 -113
- package/src/tools/interaction/twitter-login.ts +0 -105
|
@@ -37,6 +37,14 @@ export interface PolicyCard {
|
|
|
37
37
|
content: string;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
const HIGH_RISK_SPECULATION_SECTION_LIST =
|
|
41
|
+
"Why the win story is misleading, Main risks, Why limited capital changes the answer, If you insist, and Better uses for the money";
|
|
42
|
+
const VALUATION_METRIC_EDUCATION_SECTION_LIST =
|
|
43
|
+
"Bottom line, then a one-sentence Core mental model, then Practical workflow, Where it misleads, Cross-checks, and Quick checklist";
|
|
44
|
+
const VALUATION_METRIC_START = "Bottom line and a one-sentence Core mental model";
|
|
45
|
+
const OPTIONS_CURRENT_FACTS_GUARDRAIL =
|
|
46
|
+
"Do not name strikes, expirations, premiums, Greeks, or implied volatility as current facts without fetched evidence.";
|
|
47
|
+
|
|
40
48
|
const POLICY_CARDS: Record<PromptPolicyCardId, PolicyCard> = {
|
|
41
49
|
ticker_disambiguation: {
|
|
42
50
|
id: "ticker_disambiguation",
|
|
@@ -68,7 +76,7 @@ For "today", "right now", "this morning", "after close", or "why did it move" pr
|
|
|
68
76
|
status: "implemented",
|
|
69
77
|
capabilityGapIds: ["sentiment_sample_depth"],
|
|
70
78
|
content: `## Sentiment Snapshot Policy
|
|
71
|
-
For sentiment-only prompts, include the direction and strength of the sentiment signal, the score scale when available, missing sources, why missing sources matter for the user's question, source-coverage risk, low sample counts, and how those gaps downgrade confidence. For ticker-specific sentiment prompts, compare sentiment with fetched price action and state whether sentiment diverges from price action. Treat sentiment as supporting evidence, not a standalone buy/sell verdict. Disclose sparse source coverage, unavailable Twitter/X sessions, provider gaps, or low sample depth instead of implying full-market sentiment coverage.`,
|
|
79
|
+
For sentiment-only prompts, include the direction and strength of the sentiment signal, the score scale when available, the key positive and negative drivers behind the signal, representative source evidence, missing sources, why missing sources matter for the user's question, source-coverage risk, low sample counts, and how those gaps downgrade confidence. For ticker-specific sentiment prompts, compare sentiment with fetched price action and state whether sentiment diverges from price action; if quote data is unavailable, say price-action divergence could not be evaluated. Treat sentiment as supporting evidence, not a standalone buy/sell verdict. Disclose sparse source coverage, unavailable Twitter/X sessions, provider gaps, or low sample depth instead of implying full-market sentiment coverage.`,
|
|
72
80
|
},
|
|
73
81
|
filing_thesis_review: {
|
|
74
82
|
id: "filing_thesis_review",
|
|
@@ -141,7 +149,7 @@ For watchlist, portfolio tracking, alert management, daily-report, prediction re
|
|
|
141
149
|
status: "implemented",
|
|
142
150
|
capabilityGapIds: ["brokerage_comparison", "cash_yield_products", "fund_tax_efficiency"],
|
|
143
151
|
content: `## Retail Finance Tradeoff Policy
|
|
144
|
-
For brokerage, account, cash-parking, mortgage-vs-investing, robo-advisor, debt payoff, high-risk speculation, or retail financial-product prompts, use the retail tradeoff answer shape. Do not punt just because no dedicated live-data provider exists. Answer from durable public finance knowledge, disclose unavailable provider coverage, and label provider-site facts or current yield facts as facts the user should verify instead of fabricating them. For brokerage, robo-advisor, and product comparisons, start with a direct comparison table, cover fees, expense ratios, advisory fees, cash drag, cash sweep yields, fractional shares, fund minimums, tax-loss-harvesting support, transfer/account fees, mutual-fund versus ETF availability, support quality, recurring investment ease, ETF tax efficiency, and asset-location caveats for taxable accounts; explicitly tell the user to verify current fees, minimums, promotions, and cash allocations on provider sites. For cash parking, compare liquidity, FDIC/SIPC/Treasury risk, rate risk, taxes, minimums, access timing, and default 6-12 month cash hierarchy without inventing live yields. Debt payoff prompts should lead with avalanche when rates differ materially, compare snowball only as a behavioral alternative, show approximate monthly interest cost, mention emergency-fund floor, balance-transfer/refinance options, minimum payments, prepayment penalties, and the missing extra-payment amount needed for a precise timeline. High-risk speculation prompts such as penny stocks should answer suitability directly, especially with limited capital; lead with downside, survivorship bias, opportunity cost, liquidity/manipulation/dilution risk, and safer alternatives such as diversified ETFs or fractional shares. Do not use Practical workflow, Cross-checks, or Quick checklist for high-risk speculation; use direct sections such as
|
|
152
|
+
For brokerage, account, cash-parking, mortgage-vs-investing, robo-advisor, debt payoff, high-risk speculation, or retail financial-product prompts, use the retail tradeoff answer shape. Do not punt just because no dedicated live-data provider exists. Answer from durable public finance knowledge, disclose unavailable provider coverage, and label provider-site facts or current yield facts as facts the user should verify instead of fabricating them. For brokerage, robo-advisor, and product comparisons, start with a direct comparison table, cover fees, expense ratios, advisory fees, cash drag, cash sweep yields, fractional shares, fund minimums, tax-loss-harvesting support, transfer/account fees, mutual-fund versus ETF availability, support quality, recurring investment ease, ETF tax efficiency, and asset-location caveats for taxable accounts; explicitly tell the user to verify current fees, minimums, promotions, and cash allocations on provider sites. For cash parking, compare liquidity, FDIC/SIPC/Treasury risk, rate risk, taxes, minimums, access timing, and default 6-12 month cash hierarchy without inventing live yields. Debt payoff prompts should lead with avalanche when rates differ materially, compare snowball only as a behavioral alternative, show approximate monthly interest cost, mention emergency-fund floor, balance-transfer/refinance options, minimum payments, prepayment penalties, and the missing extra-payment amount needed for a precise timeline. High-risk speculation prompts such as penny stocks should answer suitability directly, especially with limited capital; lead with downside, survivorship bias, opportunity cost, liquidity/manipulation/dilution risk, and safer alternatives such as diversified ETFs or fractional shares. Do not use Practical workflow, Cross-checks, or Quick checklist for high-risk speculation; use direct sections such as ${HIGH_RISK_SPECULATION_SECTION_LIST}. If the user insists, frame any workflow as harm reduction with a tiny play-money bucket, a 100% loss assumption, no averaging down, and a pre-set exit rule. For mortgage-vs-investing prompts that compare against index funds, fetch broad-market history when available, then compare the guaranteed after-tax return from the supplied debt rate against uncertain market returns, liquidity, emergency fund, taxes, risk tolerance, time horizon, hybrid payoff/investing splits, and the practical implications of the user's stated rate.`,
|
|
145
153
|
},
|
|
146
154
|
concept_explainer: {
|
|
147
155
|
id: "concept_explainer",
|
|
@@ -149,7 +157,7 @@ For brokerage, account, cash-parking, mortgage-vs-investing, robo-advisor, debt
|
|
|
149
157
|
status: "implemented",
|
|
150
158
|
capabilityGapIds: [],
|
|
151
159
|
content: `## Concept Explainer Policy
|
|
152
|
-
For conceptual or educational finance prompts, use a decision-framework shape instead of a stock-analysis shape. Do not fetch live data unless the user asks for current examples, named securities, or live comparisons. Do not mention OpenCandle tool names unless the user asks how to apply the concept with OpenCandle. Do not append Analyst View, Commitment, Reasoning Chain, Confidence Band, or Invalidation Level sections. Lead with Bottom line and a plain-language Core mental model, then add a simple numerical example early when the concept has mechanics. For pure definition or interpretation prompts, explain the concept directly and do not force the Practical workflow, Cross-checks, or Quick checklist shape unless the user asks how to apply it. Adapt the sections to the concept instead of forcing every topic into a valuation-metric template. Include common misconception coverage, typical ranges or rules of thumb when stable, and a concise practical takeaway. For high-risk speculation education, directly address suitability, survivorship bias, opportunity cost for limited capital, safer alternatives such as diversified ETFs or fractional shares, and a tiny play-money bucket with a 100% loss assumption if the user insists; do not use Practical workflow, Cross-checks, or Quick checklist, and use direct sections such as
|
|
160
|
+
For conceptual or educational finance prompts, use a decision-framework shape instead of a stock-analysis shape. Do not fetch live data unless the user asks for current examples, named securities, or live comparisons. Do not mention OpenCandle tool names unless the user asks how to apply the concept with OpenCandle. Do not append Analyst View, Commitment, Reasoning Chain, Confidence Band, or Invalidation Level sections. Lead with Bottom line and a plain-language Core mental model, then add a simple numerical example early when the concept has mechanics. For pure definition or interpretation prompts, explain the concept directly and do not force the Practical workflow, Cross-checks, or Quick checklist shape unless the user asks how to apply it. Adapt the sections to the concept instead of forcing every topic into a valuation-metric template. Include common misconception coverage, typical ranges or rules of thumb when stable, and a concise practical takeaway. For high-risk speculation education, directly address suitability, survivorship bias, opportunity cost for limited capital, safer alternatives such as diversified ETFs or fractional shares, and a tiny play-money bucket with a 100% loss assumption if the user insists; do not use Practical workflow, Cross-checks, or Quick checklist, and use direct sections such as ${HIGH_RISK_SPECULATION_SECTION_LIST}. For volatility or risk-indicator education, explain options-implied annualized volatility as magnitude, not direction; include the daily expected-move rule VIX / sqrt(252); include a small range table for low, normal, elevated, and crisis-like readings; Use direct Q&A headings such as Key things to keep straight, What a high reading means, What a low reading means, and Does a spike mean a crash is coming; state that spikes often accompany rather than reliably precede selloffs, that very high readings can become contrarian on average, and that no single indicator predicts crashes; frame the indicator as a thermometer, not a forecast; mention term structure as a fuller volatility cross-check; end with Practical takeaways. For inflation, cash, savings, or purchasing-power education, explain nominal returns versus real returns, how inflation affects cash, bonds, stocks, and real assets, common protection tools such as TIPS or shorter-duration bonds, and the tax/time-horizon tradeoffs. For options education, use a simple analogy before mechanics, define jargon immediately, and include an explicit Main risks section covering capped upside, assignment risk, premium decay or limited protection, tax consequences, and behavioral risk. For valuation-metric education, start with ${VALUATION_METRIC_EDUCATION_SECTION_LIST}. Frame metrics as screening tools or question generators, not verdicts; cover earnings-quality distortions, variants such as trailing, forward, normalized, or cyclically adjusted, and cross-checks such as cash flow or enterprise-value lenses.`,
|
|
153
161
|
},
|
|
154
162
|
concept_options_education: {
|
|
155
163
|
id: "concept_options_education",
|
|
@@ -157,7 +165,7 @@ For conceptual or educational finance prompts, use a decision-framework shape in
|
|
|
157
165
|
status: "implemented",
|
|
158
166
|
capabilityGapIds: [],
|
|
159
167
|
content: `## Options Education Policy
|
|
160
|
-
For no-symbol options education prompts, teach the concept without fetching live option chains unless the user asks for current tradable examples. Start with Bottom line and a simple analogy before mechanics, then define jargon immediately. Use distinct beginner-friendly headings or a simple table for How it works, What you give up, Main risks, and When it is not ideal. Explain the payoff shape, why the strategy exists, and the tradeoffs in plain language, especially for long-term holdings. Include a Main risks section covering capped upside, assignment risk, share-price downside that premium does not protect, tax consequences including possible wash-sale or holding-period complications, liquidity or bid/ask caveats, behavioral risk, and tax treatment complexity. For volatility strategies such as long strangles or straddles, include a hypothetical stock price, strike prices, premiums, total debit, and break-even points; explain IV crush, IV rank or percentile, Vega, Theta, and why long-volatility trades can have a low win rate despite large payoff potential. Add practical trade-management guidance: profit target, stop loss, time-based exit, position sizing, and paper trading for beginners. Include a strangle versus straddle comparison and a pre-trade checklist covering catalyst, implied move, current IV environment, liquidity, max loss, tax caveat, and exit plan. Treat premium decay as a mechanics point for sellers rather than overstating it as a seller risk.
|
|
168
|
+
For no-symbol options education prompts, teach the concept without fetching live option chains unless the user asks for current tradable examples. Start with Bottom line and a simple analogy before mechanics, then define jargon immediately. Use distinct beginner-friendly headings or a simple table for How it works, What you give up, Main risks, and When it is not ideal. Explain the payoff shape, why the strategy exists, and the tradeoffs in plain language, especially for long-term holdings. Include a Main risks section covering capped upside, assignment risk, share-price downside that premium does not protect, tax consequences including possible wash-sale or holding-period complications, liquidity or bid/ask caveats, behavioral risk, and tax treatment complexity. For volatility strategies such as long strangles or straddles, include a hypothetical stock price, strike prices, premiums, total debit, and break-even points; explain IV crush, IV rank or percentile, Vega, Theta, and why long-volatility trades can have a low win rate despite large payoff potential. Add practical trade-management guidance: profit target, stop loss, time-based exit, position sizing, and paper trading for beginners. Include a strangle versus straddle comparison and a pre-trade checklist covering catalyst, implied move, current IV environment, liquidity, max loss, tax caveat, and exit plan. Treat premium decay as a mechanics point for sellers rather than overstating it as a seller risk. ${OPTIONS_CURRENT_FACTS_GUARDRAIL}`,
|
|
161
169
|
},
|
|
162
170
|
concept_inflation_cash_education: {
|
|
163
171
|
id: "concept_inflation_cash_education",
|
|
@@ -173,7 +181,7 @@ For no-symbol inflation, cash, savings, or purchasing-power education, explain n
|
|
|
173
181
|
status: "implemented",
|
|
174
182
|
capabilityGapIds: [],
|
|
175
183
|
content: `## Valuation Metric Education Policy
|
|
176
|
-
For valuation-metric education, start with
|
|
184
|
+
For valuation-metric education, start with ${VALUATION_METRIC_START}, then ground the idea with a simple analogy and numerical example. Explain how to use the metric as a set of considerations rather than a rigid step-by-step verdict: what the business earns, what investors pay for those earnings, and what growth or risk expectations are embedded. Include rules of thumb carefully, noting that growth companies, mature value companies, cyclicals, banks, and different industries can deserve very different ranges. Then explain Practical workflow, Where it misleads, Cross-checks, and a Quick checklist in beginner-friendly language. Frame P/E, P/S, EV/EBITDA, trailing, forward, normalized, or cyclically adjusted metrics as screening tools and question generators, not verdicts. Cover earnings-quality distortions, cyclicality, balance-sheet differences, capital intensity, growth quality, margin durability, accounting noise, and cross-checks such as cash flow, enterprise-value lenses, historical ranges, and peer context. Do not add entry levels, confidence bands, or invalidation boilerplate for pure education prompts.`,
|
|
177
185
|
},
|
|
178
186
|
general_fallback: placeholder("general_fallback", "general_fallback", []),
|
|
179
187
|
};
|
|
@@ -56,6 +56,13 @@ const DISPLAY_NAMES: Record<string, string> = {
|
|
|
56
56
|
metrics: "metrics",
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
+
const ASSUMPTIONS_RESPONSE_INSTRUCTION =
|
|
60
|
+
"- Start with the assumptions block above exactly as written. Do not relabel source attribution anywhere else in your response.";
|
|
61
|
+
|
|
62
|
+
function currentDateLine(date = todayStr()): string {
|
|
63
|
+
return `Current date: ${date}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
59
66
|
/**
|
|
60
67
|
* Build a deterministic assumption disclosure block from resolution.sources.
|
|
61
68
|
* This is the single authoritative provenance representation.
|
|
@@ -172,7 +179,7 @@ export function buildPortfolioPrompt(resolution: SlotResolution<PortfolioSlots>)
|
|
|
172
179
|
4. Use analyze_risk on each candidate for volatility, Sharpe, and max drawdown.
|
|
173
180
|
5. Use analyze_correlation across all candidates to check diversification.`;
|
|
174
181
|
|
|
175
|
-
return
|
|
182
|
+
return `${currentDateLine()}
|
|
176
183
|
|
|
177
184
|
Build a draft portfolio under these parameters:
|
|
178
185
|
- Budget: ${formatBudget(s.budget)}
|
|
@@ -194,7 +201,7 @@ Portfolio construction guardrails:
|
|
|
194
201
|
${disclosureBlock}
|
|
195
202
|
|
|
196
203
|
Response format:
|
|
197
|
-
|
|
204
|
+
${ASSUMPTIONS_RESPONSE_INSTRUCTION}
|
|
198
205
|
- Then start the analysis with "Bottom line:" and directly say what portfolio you would build for the user.
|
|
199
206
|
- Commit to the draft: give concrete percentages for each position, not ranges, and not "consider allocating X-Y%".
|
|
200
207
|
- Present an allocation table: symbol, allocation %, dollar amount, current price used, estimated shares, role, and a one-line analyst rationale for each position (what the data showed and why it belongs in this portfolio).
|
|
@@ -315,7 +322,7 @@ Protective-put hedge guidance:
|
|
|
315
322
|
? `Explain why the top pick is ranked #1. For covered calls with a cost basis, include the effective assignment sale price (strike + premium collected) and compare it with the ${s.costBasis !== undefined ? formatBudget(s.costBasis) : "user's"} cost basis.`
|
|
316
323
|
: "Explain why the top pick is ranked #1.";
|
|
317
324
|
|
|
318
|
-
return
|
|
325
|
+
return `${currentDateLine(dateStr)}
|
|
319
326
|
Do NOT invent or assume a different current date.${expirationSection}
|
|
320
327
|
|
|
321
328
|
Screen and rank options contracts for ${s.symbol}:
|
|
@@ -349,7 +356,7 @@ ${rankingConstraints}
|
|
|
349
356
|
${disclosureBlock}
|
|
350
357
|
|
|
351
358
|
Response format:
|
|
352
|
-
|
|
359
|
+
${ASSUMPTIONS_RESPONSE_INSTRUCTION}
|
|
353
360
|
- ${isCoveredCallContext ? `Start with an Interpretation line: "Interpretation: Treating ${s.symbol} as the held ticker because you phrased it as an existing position. If you meant ${s.symbol} as memory exposure or another ticker, clarify before trading."` : isProtectivePutContext ? `Start with an Interpretation line: "Interpretation: Treating this as buying protective puts on an existing long ${s.symbol} share position."` : "State the interpretation only if the user's requested underlying is ambiguous."}
|
|
354
361
|
- Present top 3-5 ranked contracts in a table: strike, expiry, premium, delta, gamma, theta, vega, rho, IV, OI, bid-ask spread${isProtectivePutContext ? ", hedge floor, premium % of position" : ""}.
|
|
355
362
|
- ${topPickExplanation}
|
|
@@ -460,7 +467,7 @@ macro hedge decision guidance:
|
|
|
460
467
|
resolution.sources as Record<string, SlotSource | undefined>,
|
|
461
468
|
);
|
|
462
469
|
|
|
463
|
-
return
|
|
470
|
+
return `${currentDateLine()}
|
|
464
471
|
|
|
465
472
|
Compare these assets side by side: ${symbolList}${horizonLine}${budgetLine}
|
|
466
473
|
|
|
@@ -475,7 +482,7 @@ ${overlapGuidance}
|
|
|
475
482
|
${disclosureBlock}
|
|
476
483
|
|
|
477
484
|
Response format:
|
|
478
|
-
|
|
485
|
+
${ASSUMPTIONS_RESPONSE_INSTRUCTION}
|
|
479
486
|
${tableInstruction}
|
|
480
487
|
- Provide a summary verdict: which is most attractive and why.
|
|
481
488
|
- Note any caveats (different sectors, concentration, market cap disparity, unavailable fundamentals, unavailable forward-looking estimates, etc.).${horizonResponse}`;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { delimiter, isAbsolute, join, sep } from "node:path";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 20_000;
|
|
7
|
+
const DEFAULT_MAX_OUTPUT_CHARS = 2_000_000;
|
|
8
|
+
|
|
9
|
+
export interface ExternalToolCommandResult {
|
|
10
|
+
readonly code: number | null;
|
|
11
|
+
readonly stdout: string;
|
|
12
|
+
readonly stderr: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ExternalToolCommandOptions {
|
|
16
|
+
readonly timeoutMs?: number;
|
|
17
|
+
readonly maxOutputChars?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface CandidateOptions {
|
|
21
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
22
|
+
readonly homeDir?: string;
|
|
23
|
+
readonly platform?: NodeJS.Platform;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getExternalToolCommandCandidates(
|
|
27
|
+
command: string,
|
|
28
|
+
options: CandidateOptions = {},
|
|
29
|
+
): string[] {
|
|
30
|
+
if (isPathLikeCommand(command)) return [command];
|
|
31
|
+
|
|
32
|
+
const env = options.env ?? process.env;
|
|
33
|
+
const platform = options.platform ?? process.platform;
|
|
34
|
+
const home = options.homeDir ?? homedir();
|
|
35
|
+
const candidates = [command];
|
|
36
|
+
const binDirs = [
|
|
37
|
+
env.OPENCANDLE_EXTERNAL_TOOL_BIN_DIR,
|
|
38
|
+
env.UV_TOOL_BIN_DIR,
|
|
39
|
+
env.XDG_BIN_HOME,
|
|
40
|
+
home ? join(home, ".local", "bin") : undefined,
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
for (const dir of binDirs) {
|
|
44
|
+
if (!dir) continue;
|
|
45
|
+
for (const executable of commandNames(command, platform)) {
|
|
46
|
+
candidates.push(join(dir, executable));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return [...new Set(candidates)];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function runExternalToolCommand(
|
|
54
|
+
command: string,
|
|
55
|
+
args: readonly string[],
|
|
56
|
+
options: ExternalToolCommandOptions = {},
|
|
57
|
+
): Promise<ExternalToolCommandResult> {
|
|
58
|
+
const uvToolBinDirs = await resolveUvToolBinDirs(options);
|
|
59
|
+
const candidates = [
|
|
60
|
+
...getExternalToolCommandCandidates(command),
|
|
61
|
+
...uvToolBinDirs.flatMap((dir) =>
|
|
62
|
+
commandNames(command, process.platform).map((executable) => join(dir, executable)),
|
|
63
|
+
),
|
|
64
|
+
].filter((candidate, index, all) => all.indexOf(candidate) === index);
|
|
65
|
+
const existingCandidates = candidates.filter(
|
|
66
|
+
(candidate) => candidate === command || existsSync(candidate),
|
|
67
|
+
);
|
|
68
|
+
return runCandidate(
|
|
69
|
+
existingCandidates.length > 0 ? existingCandidates : [command],
|
|
70
|
+
args,
|
|
71
|
+
options,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function resolveUvToolBinDirs(
|
|
76
|
+
options: ExternalToolCommandOptions,
|
|
77
|
+
): Promise<readonly string[]> {
|
|
78
|
+
try {
|
|
79
|
+
const result = await spawnCommand("uv", ["tool", "dir", "--bin"], {
|
|
80
|
+
timeoutMs: Math.min(options.timeoutMs ?? DEFAULT_TIMEOUT_MS, 2_000),
|
|
81
|
+
maxOutputChars: 4_000,
|
|
82
|
+
});
|
|
83
|
+
if (result.code !== 0) return [];
|
|
84
|
+
const binDir = result.stdout.trim();
|
|
85
|
+
return binDir ? [binDir] : [];
|
|
86
|
+
} catch {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function runCandidate(
|
|
92
|
+
candidates: readonly string[],
|
|
93
|
+
args: readonly string[],
|
|
94
|
+
options: ExternalToolCommandOptions,
|
|
95
|
+
): Promise<ExternalToolCommandResult> {
|
|
96
|
+
const [command, ...fallbacks] = candidates;
|
|
97
|
+
return spawnCommand(command, args, options).catch((err: NodeJS.ErrnoException) => {
|
|
98
|
+
if (err.code === "ENOENT" && fallbacks.length > 0) {
|
|
99
|
+
return runCandidate(fallbacks, args, options);
|
|
100
|
+
}
|
|
101
|
+
throw err;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function spawnCommand(
|
|
106
|
+
command: string,
|
|
107
|
+
args: readonly string[],
|
|
108
|
+
options: ExternalToolCommandOptions,
|
|
109
|
+
): Promise<ExternalToolCommandResult> {
|
|
110
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
111
|
+
const maxOutputChars = options.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
|
|
112
|
+
|
|
113
|
+
return new Promise((resolve, reject) => {
|
|
114
|
+
const child = spawn(command, [...args], { stdio: ["ignore", "pipe", "pipe"] });
|
|
115
|
+
let stdout = "";
|
|
116
|
+
let stderr = "";
|
|
117
|
+
let settled = false;
|
|
118
|
+
|
|
119
|
+
const timeout = setTimeout(() => {
|
|
120
|
+
if (settled) return;
|
|
121
|
+
settled = true;
|
|
122
|
+
child.kill("SIGTERM");
|
|
123
|
+
reject(new Error(`${command} timed out after ${timeoutMs}ms`));
|
|
124
|
+
}, timeoutMs);
|
|
125
|
+
|
|
126
|
+
child.stdout.on("data", (chunk: Buffer) => {
|
|
127
|
+
stdout = (stdout + chunk.toString("utf8")).slice(0, maxOutputChars);
|
|
128
|
+
});
|
|
129
|
+
child.stderr.on("data", (chunk: Buffer) => {
|
|
130
|
+
stderr = (stderr + chunk.toString("utf8")).slice(0, maxOutputChars);
|
|
131
|
+
});
|
|
132
|
+
child.on("error", (err) => {
|
|
133
|
+
if (settled) return;
|
|
134
|
+
settled = true;
|
|
135
|
+
clearTimeout(timeout);
|
|
136
|
+
reject(err);
|
|
137
|
+
});
|
|
138
|
+
child.on("close", (code) => {
|
|
139
|
+
if (settled) return;
|
|
140
|
+
settled = true;
|
|
141
|
+
clearTimeout(timeout);
|
|
142
|
+
resolve({ code, stdout, stderr });
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function isPathLikeCommand(command: string): boolean {
|
|
148
|
+
return (
|
|
149
|
+
isAbsolute(command) || command.includes("/") || command.includes("\\") || command.includes(sep)
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function commandNames(command: string, platform: NodeJS.Platform): string[] {
|
|
154
|
+
if (platform !== "win32") return [command];
|
|
155
|
+
|
|
156
|
+
const names = [command];
|
|
157
|
+
const pathExt = process.env.PATHEXT?.split(delimiter) ?? [".EXE", ".CMD", ".BAT"];
|
|
158
|
+
for (const ext of pathExt) {
|
|
159
|
+
if (!command.toLowerCase().endsWith(ext.toLowerCase())) {
|
|
160
|
+
names.push(`${command}${ext.toLowerCase()}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return names;
|
|
164
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export class ExternalToolError extends Error {
|
|
2
|
+
constructor(
|
|
3
|
+
public readonly toolName: string,
|
|
4
|
+
message: string,
|
|
5
|
+
public readonly code?: string,
|
|
6
|
+
) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "ExternalToolError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class ExternalToolNotInstalled extends Error {
|
|
13
|
+
constructor(
|
|
14
|
+
public readonly toolName: string,
|
|
15
|
+
public readonly installCmd: string,
|
|
16
|
+
) {
|
|
17
|
+
super(`${toolName} is not installed. Install it with: ${installCmd}`);
|
|
18
|
+
this.name = "ExternalToolNotInstalled";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { type ExternalToolCommandResult, runExternalToolCommand } from "./external-tool-command.js";
|
|
2
|
+
import { ExternalToolError, ExternalToolNotInstalled } from "./external-tool-error.js";
|
|
3
|
+
import type { RedditComment } from "./reddit.js";
|
|
4
|
+
|
|
5
|
+
const RDT_BINARY = "rdt";
|
|
6
|
+
const RDT_INSTALL_CMD = "uv tool install rdt-cli";
|
|
7
|
+
const COMMAND_TIMEOUT_MS = 20_000;
|
|
8
|
+
const MAX_OUTPUT_CHARS = 2_000_000;
|
|
9
|
+
|
|
10
|
+
interface RdtEnvelope<T> {
|
|
11
|
+
readonly ok: boolean;
|
|
12
|
+
readonly schema_version: string;
|
|
13
|
+
readonly data?: T;
|
|
14
|
+
readonly error?: {
|
|
15
|
+
readonly code?: string;
|
|
16
|
+
readonly message?: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface RdtPost {
|
|
21
|
+
readonly id: string;
|
|
22
|
+
readonly title: string;
|
|
23
|
+
readonly selftext: string;
|
|
24
|
+
readonly author: string;
|
|
25
|
+
readonly score: number;
|
|
26
|
+
readonly comments: number;
|
|
27
|
+
readonly subreddit?: string;
|
|
28
|
+
readonly url: string;
|
|
29
|
+
readonly permalink?: string;
|
|
30
|
+
readonly created: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface RawRdtPost {
|
|
34
|
+
readonly id?: unknown;
|
|
35
|
+
readonly title?: unknown;
|
|
36
|
+
readonly selftext?: unknown;
|
|
37
|
+
readonly author?: unknown;
|
|
38
|
+
readonly score?: unknown;
|
|
39
|
+
readonly num_comments?: unknown;
|
|
40
|
+
readonly subreddit?: unknown;
|
|
41
|
+
readonly url?: unknown;
|
|
42
|
+
readonly permalink?: unknown;
|
|
43
|
+
readonly created_utc?: unknown;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface RawRdtComment {
|
|
47
|
+
readonly id?: unknown;
|
|
48
|
+
readonly body?: unknown;
|
|
49
|
+
readonly author?: unknown;
|
|
50
|
+
readonly score?: unknown;
|
|
51
|
+
readonly permalink?: unknown;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface RdtListingChild<T> {
|
|
55
|
+
readonly kind?: string;
|
|
56
|
+
readonly data?: T;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface RdtListing<T> {
|
|
60
|
+
readonly data?: {
|
|
61
|
+
readonly children?: Array<RdtListingChild<T>>;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type RdtCommandRunner = (
|
|
66
|
+
command: string,
|
|
67
|
+
args: readonly string[],
|
|
68
|
+
) => Promise<ExternalToolCommandResult>;
|
|
69
|
+
|
|
70
|
+
let commandRunner: RdtCommandRunner = runCommand;
|
|
71
|
+
|
|
72
|
+
export function setRdtCommandRunnerForTests(runner: RdtCommandRunner): void {
|
|
73
|
+
commandRunner = runner;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function resetRdtCommandRunnerForTests(): void {
|
|
77
|
+
commandRunner = runCommand;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function searchRedditPosts(
|
|
81
|
+
query: string,
|
|
82
|
+
opts: { subreddit?: string; limit: number },
|
|
83
|
+
): Promise<RdtPost[]> {
|
|
84
|
+
const args = ["search", query];
|
|
85
|
+
if (opts.subreddit) {
|
|
86
|
+
args.push("--subreddit", opts.subreddit);
|
|
87
|
+
}
|
|
88
|
+
args.push("--json", "--compact", "-n", String(opts.limit));
|
|
89
|
+
const data = await runRdt<RawRdtPost[]>(args);
|
|
90
|
+
if (!Array.isArray(data)) {
|
|
91
|
+
throw new ExternalToolError(RDT_BINARY, "rdt-cli returned invalid search data");
|
|
92
|
+
}
|
|
93
|
+
return data.map(adaptPost).filter((post) => post.id.length > 0);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function listSubredditPosts(
|
|
97
|
+
subreddit: string,
|
|
98
|
+
opts: { limit: number },
|
|
99
|
+
): Promise<RdtPost[]> {
|
|
100
|
+
const data = await runRdt<RawRdtPost[]>([
|
|
101
|
+
"sub",
|
|
102
|
+
subreddit,
|
|
103
|
+
"--json",
|
|
104
|
+
"--compact",
|
|
105
|
+
"-n",
|
|
106
|
+
String(opts.limit),
|
|
107
|
+
]);
|
|
108
|
+
if (!Array.isArray(data)) {
|
|
109
|
+
throw new ExternalToolError(RDT_BINARY, "rdt-cli returned invalid subreddit data");
|
|
110
|
+
}
|
|
111
|
+
return data.map((raw) => adaptPost({ ...raw, subreddit })).filter((post) => post.id.length > 0);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function readRedditPost(
|
|
115
|
+
postId: string,
|
|
116
|
+
opts: { limit: number },
|
|
117
|
+
): Promise<{ post: RdtPost; comments: RedditComment[] }> {
|
|
118
|
+
const data = await runRdt<Array<RdtListing<RawRdtPost | RawRdtComment>>>([
|
|
119
|
+
"read",
|
|
120
|
+
postId,
|
|
121
|
+
"--json",
|
|
122
|
+
"-n",
|
|
123
|
+
String(opts.limit),
|
|
124
|
+
]);
|
|
125
|
+
if (!Array.isArray(data)) {
|
|
126
|
+
throw new ExternalToolError(RDT_BINARY, "rdt-cli returned invalid read data");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const rawPost = data[0]?.data?.children?.find((child) => child.kind === "t3")?.data;
|
|
130
|
+
if (!rawPost) {
|
|
131
|
+
throw new ExternalToolError(RDT_BINARY, "rdt-cli read output did not include a post");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const post = adaptPost(rawPost as RawRdtPost);
|
|
135
|
+
const commentListing = data[1]?.data?.children ?? [];
|
|
136
|
+
const comments = commentListing
|
|
137
|
+
.filter((child): child is RdtListingChild<RawRdtComment> => child.kind === "t1")
|
|
138
|
+
.map((child) => adaptComment(child.data, post.permalink))
|
|
139
|
+
.filter((comment): comment is RedditComment => comment !== null)
|
|
140
|
+
.sort((a, b) => b.score - a.score)
|
|
141
|
+
.slice(0, opts.limit);
|
|
142
|
+
|
|
143
|
+
return { post, comments };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function runRdt<T>(args: readonly string[]): Promise<T> {
|
|
147
|
+
let result: ExternalToolCommandResult;
|
|
148
|
+
try {
|
|
149
|
+
result = await commandRunner(RDT_BINARY, args);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
const nodeError = err as NodeJS.ErrnoException;
|
|
152
|
+
if (nodeError.code === "ENOENT") {
|
|
153
|
+
throw new ExternalToolNotInstalled(RDT_BINARY, RDT_INSTALL_CMD);
|
|
154
|
+
}
|
|
155
|
+
throw new ExternalToolError(
|
|
156
|
+
RDT_BINARY,
|
|
157
|
+
redactSensitiveOutput(err instanceof Error ? err.message : String(err)),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (result.code !== 0) {
|
|
162
|
+
const envelopeError = parseCliErrorEnvelope(result.stdout);
|
|
163
|
+
if (envelopeError) {
|
|
164
|
+
throw new ExternalToolError(
|
|
165
|
+
RDT_BINARY,
|
|
166
|
+
redactSensitiveOutput(envelopeError.message),
|
|
167
|
+
envelopeError.code,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
throw new ExternalToolError(
|
|
171
|
+
RDT_BINARY,
|
|
172
|
+
redactSensitiveOutput(result.stderr.trim() || `rdt-cli exited with code ${result.code}`),
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let envelope: RdtEnvelope<T>;
|
|
177
|
+
try {
|
|
178
|
+
envelope = JSON.parse(result.stdout) as RdtEnvelope<T>;
|
|
179
|
+
} catch {
|
|
180
|
+
throw new ExternalToolError(
|
|
181
|
+
RDT_BINARY,
|
|
182
|
+
`rdt-cli returned non-JSON output: ${redactSensitiveOutput(result.stdout.slice(0, 200))}`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!envelope.ok) {
|
|
187
|
+
throw new ExternalToolError(
|
|
188
|
+
RDT_BINARY,
|
|
189
|
+
redactSensitiveOutput(envelope.error?.message ?? "rdt-cli returned an error"),
|
|
190
|
+
envelope.error?.code,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
if (envelope.data === undefined) {
|
|
194
|
+
throw new ExternalToolError(RDT_BINARY, "rdt-cli returned no data");
|
|
195
|
+
}
|
|
196
|
+
return envelope.data;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function parseCliErrorEnvelope(stdout: string): { code?: string; message: string } | null {
|
|
200
|
+
try {
|
|
201
|
+
const parsed = JSON.parse(stdout) as {
|
|
202
|
+
ok?: unknown;
|
|
203
|
+
error?: { code?: unknown; message?: unknown };
|
|
204
|
+
};
|
|
205
|
+
if (parsed.ok !== false || typeof parsed.error?.message !== "string") return null;
|
|
206
|
+
return {
|
|
207
|
+
code: typeof parsed.error.code === "string" ? parsed.error.code : undefined,
|
|
208
|
+
message: parsed.error.message,
|
|
209
|
+
};
|
|
210
|
+
} catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function adaptPost(raw: RawRdtPost): RdtPost {
|
|
216
|
+
const permalink = stringValue(raw.permalink);
|
|
217
|
+
const url = stringValue(raw.url) || (permalink ? `https://reddit.com${permalink}` : "");
|
|
218
|
+
return {
|
|
219
|
+
id: stringValue(raw.id),
|
|
220
|
+
title: stringValue(raw.title),
|
|
221
|
+
selftext: stringValue(raw.selftext),
|
|
222
|
+
author: stringValue(raw.author) || "unknown",
|
|
223
|
+
score: numberValue(raw.score),
|
|
224
|
+
comments: numberValue(raw.num_comments),
|
|
225
|
+
subreddit: stringValue(raw.subreddit) || undefined,
|
|
226
|
+
url,
|
|
227
|
+
permalink: permalink || undefined,
|
|
228
|
+
created: normalizeCreatedAt(raw.created_utc),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function adaptComment(
|
|
233
|
+
raw: RawRdtComment | undefined,
|
|
234
|
+
postPermalink?: string,
|
|
235
|
+
): RedditComment | null {
|
|
236
|
+
if (!raw || typeof raw.body !== "string" || raw.body.length === 0) return null;
|
|
237
|
+
const permalink = stringValue(raw.permalink) || postPermalink || "";
|
|
238
|
+
return {
|
|
239
|
+
id: stringValue(raw.id),
|
|
240
|
+
body: raw.body,
|
|
241
|
+
author: stringValue(raw.author) || "unknown",
|
|
242
|
+
score: numberValue(raw.score),
|
|
243
|
+
permalink: permalink ? `https://reddit.com${permalink}` : "",
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function normalizeCreatedAt(value: unknown): string {
|
|
248
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
249
|
+
const millis = value > 1_000_000_000_000 ? value : value * 1000;
|
|
250
|
+
return new Date(millis).toISOString();
|
|
251
|
+
}
|
|
252
|
+
if (typeof value === "string" && value.length > 0) {
|
|
253
|
+
const millis = Date.parse(value);
|
|
254
|
+
if (!Number.isNaN(millis)) return new Date(millis).toISOString();
|
|
255
|
+
}
|
|
256
|
+
return new Date(0).toISOString();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function stringValue(value: unknown): string {
|
|
260
|
+
return typeof value === "string" ? value : "";
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function numberValue(value: unknown): number {
|
|
264
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function redactSensitiveOutput(input: string): string {
|
|
268
|
+
return input
|
|
269
|
+
.slice(0, MAX_OUTPUT_CHARS)
|
|
270
|
+
.replace(
|
|
271
|
+
/\b([a-z0-9_]*(?:cookie|session|token)[a-z0-9_]*)\b\s*[:=]\s*[^;\s,)]+/gi,
|
|
272
|
+
"$1=[redacted]",
|
|
273
|
+
)
|
|
274
|
+
.replace(/\b([a-z0-9_]*(?:cookie|session|token)[a-z0-9_]*)=([^;\s,)]+)/gi, "$1=[redacted]")
|
|
275
|
+
.replace(
|
|
276
|
+
/(?:~|\/Users\/[^/\s]+|\/home\/[^/\s]+)?\/\.config\/rdt-cli\/credential\.json/g,
|
|
277
|
+
"[redacted-credential-path]",
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function runCommand(command: string, args: readonly string[]): Promise<ExternalToolCommandResult> {
|
|
282
|
+
return runExternalToolCommand(command, args, {
|
|
283
|
+
timeoutMs: COMMAND_TIMEOUT_MS,
|
|
284
|
+
maxOutputChars: MAX_OUTPUT_CHARS,
|
|
285
|
+
});
|
|
286
|
+
}
|