opencandle 0.7.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 +34 -21
- 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 +13 -229
- package/dist/cli.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/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/provider-status.d.ts +3 -5
- package/dist/onboarding/provider-status.js +5 -34
- package/dist/onboarding/provider-status.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/prompts/policy-cards.js +8 -4
- 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/reddit-cli.d.ts +2 -6
- package/dist/providers/reddit-cli.js +4 -33
- package/dist/providers/reddit-cli.js.map +1 -1
- package/dist/providers/twitter-cli.d.ts +2 -6
- package/dist/providers/twitter-cli.js +4 -33
- package/dist/providers/twitter-cli.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.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/gui/server/http-routes.ts +419 -0
- package/gui/server/server.ts +29 -292
- 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-hwbx24a5.css → index-UzZUg3dx.css} +1 -1
- package/gui/web/dist/index.html +2 -2
- package/package.json +13 -9
- package/src/cli-main.ts +233 -0
- package/src/cli.ts +14 -267
- 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/native-dependencies.ts +67 -0
- package/src/infra/node-version.ts +2 -5
- package/src/monitor.ts +3 -1
- package/src/onboarding/provider-status.ts +10 -38
- package/src/pi/session.ts +2 -1
- package/src/prompts/policy-cards.ts +12 -4
- package/src/prompts/workflow-prompts.ts +13 -6
- package/src/providers/external-tool-command.ts +164 -0
- package/src/providers/reddit-cli.ts +10 -41
- package/src/providers/twitter-cli.ts +10 -41
- package/src/routing/classify-intent.ts +11 -6
- package/src/routing/planning.ts +15 -10
- package/src/routing/router.ts +11 -6
- package/gui/web/dist/assets/CatalogOverlay-CgeY5Pkp.js +0 -1
- package/gui/web/dist/assets/index-C6W_2eAn.js +0 -69
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
const SUPPORTED_NODE_RANGE = "
|
|
1
|
+
const SUPPORTED_NODE_RANGE = "22.19+ or 24.x-26.x";
|
|
2
2
|
|
|
3
3
|
function isSupportedNodeVersion(version: string): boolean {
|
|
4
4
|
const [majorRaw, minorRaw] = version.split(".");
|
|
5
5
|
const major = Number(majorRaw);
|
|
6
6
|
const minor = Number(minorRaw);
|
|
7
7
|
|
|
8
|
-
if (major ===
|
|
9
|
-
if (major === 22) return minor >= 12;
|
|
8
|
+
if (major === 22) return minor >= 19;
|
|
10
9
|
return major >= 24 && major < 27;
|
|
11
10
|
}
|
|
12
11
|
|
|
@@ -22,5 +21,3 @@ export function assertSupportedNodeVersion(version?: string): void {
|
|
|
22
21
|
const message = getUnsupportedNodeVersionMessage(version);
|
|
23
22
|
if (message) throw new Error(message);
|
|
24
23
|
}
|
|
25
|
-
|
|
26
|
-
assertSupportedNodeVersion();
|
package/src/monitor.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import "./infra/node-version.js";
|
|
3
2
|
import { parseArgs } from "node:util";
|
|
4
3
|
import { loadEnv } from "./config.js";
|
|
4
|
+
import { assertSupportedNodeVersion } from "./infra/node-version.js";
|
|
5
5
|
import { runLocalAutomationHeartbeat } from "./market-state/local-automation-service.js";
|
|
6
6
|
import { MarketStateService } from "./market-state/service.js";
|
|
7
7
|
import { initDefaultDatabase } from "./memory/sqlite.js";
|
|
8
8
|
|
|
9
|
+
assertSupportedNodeVersion();
|
|
10
|
+
|
|
9
11
|
const DEFAULT_MONITOR_INTERVAL_MS = 60_000;
|
|
10
12
|
const MIN_MONITOR_INTERVAL_MS = 5_000;
|
|
11
13
|
const MONITOR_OWNER_ID = `monitor:${process.pid}`;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type ExternalToolCommandResult,
|
|
3
|
+
runExternalToolCommand,
|
|
4
|
+
} from "../providers/external-tool-command.js";
|
|
2
5
|
import type { ProviderId } from "./providers.js";
|
|
3
6
|
import {
|
|
4
7
|
getCredentialSource,
|
|
@@ -14,11 +17,7 @@ const COMMAND_TIMEOUT_MS = 5_000;
|
|
|
14
17
|
const PUBLIC_HTTP_TIMEOUT_MS = 3_000;
|
|
15
18
|
const MAX_OUTPUT_CHARS = 32_000;
|
|
16
19
|
|
|
17
|
-
export
|
|
18
|
-
readonly code: number | null;
|
|
19
|
-
readonly stdout: string;
|
|
20
|
-
readonly stderr: string;
|
|
21
|
-
}
|
|
20
|
+
export type CommandResult = ExternalToolCommandResult;
|
|
22
21
|
|
|
23
22
|
export type CommandRunner = (
|
|
24
23
|
command: string,
|
|
@@ -69,6 +68,7 @@ export type ProviderStatus =
|
|
|
69
68
|
export interface ProbeProviderStatusOptions {
|
|
70
69
|
readonly mode?: "install" | "session";
|
|
71
70
|
readonly force?: boolean;
|
|
71
|
+
readonly respectSkipped?: boolean;
|
|
72
72
|
readonly commandRunner?: CommandRunner;
|
|
73
73
|
readonly fetchImpl?: typeof fetch;
|
|
74
74
|
readonly now?: Date;
|
|
@@ -97,7 +97,7 @@ export async function probeProviderStatus(
|
|
|
97
97
|
isExternalToolProvider(provider) &&
|
|
98
98
|
loadOnboardingState().providers[providerId]?.status === "never_ask";
|
|
99
99
|
|
|
100
|
-
if (skippedByPreference && !options.force) {
|
|
100
|
+
if (skippedByPreference && (!options.force || options.respectSkipped)) {
|
|
101
101
|
return {
|
|
102
102
|
providerId,
|
|
103
103
|
kind: "external-tool",
|
|
@@ -376,35 +376,7 @@ export function redactSensitiveOutput(input: string): string {
|
|
|
376
376
|
}
|
|
377
377
|
|
|
378
378
|
export const runCommand: CommandRunner = (command, args, options) =>
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
let stderr = "";
|
|
383
|
-
let settled = false;
|
|
384
|
-
|
|
385
|
-
const timeout = setTimeout(() => {
|
|
386
|
-
if (settled) return;
|
|
387
|
-
settled = true;
|
|
388
|
-
child.kill("SIGTERM");
|
|
389
|
-
reject(new Error(`${command} timed out after ${options?.timeoutMs ?? COMMAND_TIMEOUT_MS}ms`));
|
|
390
|
-
}, options?.timeoutMs ?? COMMAND_TIMEOUT_MS);
|
|
391
|
-
|
|
392
|
-
child.stdout.on("data", (chunk: Buffer) => {
|
|
393
|
-
stdout = (stdout + chunk.toString("utf8")).slice(0, MAX_OUTPUT_CHARS);
|
|
394
|
-
});
|
|
395
|
-
child.stderr.on("data", (chunk: Buffer) => {
|
|
396
|
-
stderr = (stderr + chunk.toString("utf8")).slice(0, MAX_OUTPUT_CHARS);
|
|
397
|
-
});
|
|
398
|
-
child.on("error", (err) => {
|
|
399
|
-
if (settled) return;
|
|
400
|
-
settled = true;
|
|
401
|
-
clearTimeout(timeout);
|
|
402
|
-
reject(err);
|
|
403
|
-
});
|
|
404
|
-
child.on("close", (code) => {
|
|
405
|
-
if (settled) return;
|
|
406
|
-
settled = true;
|
|
407
|
-
clearTimeout(timeout);
|
|
408
|
-
resolve({ code, stdout, stderr });
|
|
409
|
-
});
|
|
379
|
+
runExternalToolCommand(command, args, {
|
|
380
|
+
timeoutMs: options?.timeoutMs ?? COMMAND_TIMEOUT_MS,
|
|
381
|
+
maxOutputChars: MAX_OUTPUT_CHARS,
|
|
410
382
|
});
|
package/src/pi/session.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import "../infra/node-version.js";
|
|
2
1
|
import {
|
|
3
2
|
type AuthStorage,
|
|
4
3
|
type CreateAgentSessionResult,
|
|
@@ -10,6 +9,7 @@ import {
|
|
|
10
9
|
type SettingsManager,
|
|
11
10
|
} from "@earendil-works/pi-coding-agent";
|
|
12
11
|
import { loadEnv } from "../config.js";
|
|
12
|
+
import { assertSupportedNodeVersion } from "../infra/node-version.js";
|
|
13
13
|
import type { AskUserHandler } from "../types/index.js";
|
|
14
14
|
import openCandleExtension from "./opencandle-extension.js";
|
|
15
15
|
|
|
@@ -28,6 +28,7 @@ export interface CreateOpenCandleSessionOptions {
|
|
|
28
28
|
export async function createOpenCandleSession(
|
|
29
29
|
options: CreateOpenCandleSessionOptions = {},
|
|
30
30
|
): Promise<CreateAgentSessionResult> {
|
|
31
|
+
assertSupportedNodeVersion();
|
|
31
32
|
loadEnv();
|
|
32
33
|
|
|
33
34
|
const cwd = options.cwd ?? process.cwd();
|
|
@@ -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",
|
|
@@ -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
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type ExternalToolCommandResult, runExternalToolCommand } from "./external-tool-command.js";
|
|
2
2
|
import { ExternalToolError, ExternalToolNotInstalled } from "./external-tool-error.js";
|
|
3
3
|
import type { RedditComment } from "./reddit.js";
|
|
4
4
|
|
|
@@ -62,13 +62,10 @@ interface RdtListing<T> {
|
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
readonly
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
type RdtCommandRunner = (command: string, args: readonly string[]) => Promise<CommandResult>;
|
|
65
|
+
type RdtCommandRunner = (
|
|
66
|
+
command: string,
|
|
67
|
+
args: readonly string[],
|
|
68
|
+
) => Promise<ExternalToolCommandResult>;
|
|
72
69
|
|
|
73
70
|
let commandRunner: RdtCommandRunner = runCommand;
|
|
74
71
|
|
|
@@ -147,7 +144,7 @@ export async function readRedditPost(
|
|
|
147
144
|
}
|
|
148
145
|
|
|
149
146
|
async function runRdt<T>(args: readonly string[]): Promise<T> {
|
|
150
|
-
let result:
|
|
147
|
+
let result: ExternalToolCommandResult;
|
|
151
148
|
try {
|
|
152
149
|
result = await commandRunner(RDT_BINARY, args);
|
|
153
150
|
} catch (err) {
|
|
@@ -281,37 +278,9 @@ export function redactSensitiveOutput(input: string): string {
|
|
|
281
278
|
);
|
|
282
279
|
}
|
|
283
280
|
|
|
284
|
-
function runCommand(command: string, args: readonly string[]): Promise<
|
|
285
|
-
return
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
let stderr = "";
|
|
289
|
-
let settled = false;
|
|
290
|
-
|
|
291
|
-
const timeout = setTimeout(() => {
|
|
292
|
-
if (settled) return;
|
|
293
|
-
settled = true;
|
|
294
|
-
child.kill("SIGTERM");
|
|
295
|
-
reject(new Error(`${command} timed out after ${COMMAND_TIMEOUT_MS}ms`));
|
|
296
|
-
}, COMMAND_TIMEOUT_MS);
|
|
297
|
-
|
|
298
|
-
child.stdout.on("data", (chunk: Buffer) => {
|
|
299
|
-
stdout = (stdout + chunk.toString("utf8")).slice(0, MAX_OUTPUT_CHARS);
|
|
300
|
-
});
|
|
301
|
-
child.stderr.on("data", (chunk: Buffer) => {
|
|
302
|
-
stderr = (stderr + chunk.toString("utf8")).slice(0, MAX_OUTPUT_CHARS);
|
|
303
|
-
});
|
|
304
|
-
child.on("error", (err) => {
|
|
305
|
-
if (settled) return;
|
|
306
|
-
settled = true;
|
|
307
|
-
clearTimeout(timeout);
|
|
308
|
-
reject(err);
|
|
309
|
-
});
|
|
310
|
-
child.on("close", (code) => {
|
|
311
|
-
if (settled) return;
|
|
312
|
-
settled = true;
|
|
313
|
-
clearTimeout(timeout);
|
|
314
|
-
resolve({ code, stdout, stderr });
|
|
315
|
-
});
|
|
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,
|
|
316
285
|
});
|
|
317
286
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import type { TwitterTweet } from "../types/sentiment.js";
|
|
2
|
+
import { type ExternalToolCommandResult, runExternalToolCommand } from "./external-tool-command.js";
|
|
3
3
|
import { ExternalToolError, ExternalToolNotInstalled } from "./external-tool-error.js";
|
|
4
4
|
|
|
5
5
|
const TWITTER_CLI_BINARY = "twitter";
|
|
@@ -47,13 +47,10 @@ interface TwitterCliEnvelope<T> {
|
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
readonly
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
type TwitterCliCommandRunner = (command: string, args: readonly string[]) => Promise<CommandResult>;
|
|
50
|
+
type TwitterCliCommandRunner = (
|
|
51
|
+
command: string,
|
|
52
|
+
args: readonly string[],
|
|
53
|
+
) => Promise<ExternalToolCommandResult>;
|
|
57
54
|
|
|
58
55
|
let commandRunner: TwitterCliCommandRunner = runCommand;
|
|
59
56
|
|
|
@@ -90,7 +87,7 @@ export async function searchTweets(query: string, max = 20): Promise<TwitterTwee
|
|
|
90
87
|
}
|
|
91
88
|
|
|
92
89
|
async function runTwitterCli<T>(args: readonly string[]): Promise<T> {
|
|
93
|
-
let result:
|
|
90
|
+
let result: ExternalToolCommandResult;
|
|
94
91
|
try {
|
|
95
92
|
result = await commandRunner(TWITTER_CLI_BINARY, args);
|
|
96
93
|
} catch (err) {
|
|
@@ -197,37 +194,9 @@ export function redactSensitiveOutput(input: string): string {
|
|
|
197
194
|
.replace(new RegExp(`\\b(${xCookieNames})=([^;\\s,)]+)`, "gi"), "$1=[redacted]");
|
|
198
195
|
}
|
|
199
196
|
|
|
200
|
-
function runCommand(command: string, args: readonly string[]): Promise<
|
|
201
|
-
return
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
let stderr = "";
|
|
205
|
-
let settled = false;
|
|
206
|
-
|
|
207
|
-
const timeout = setTimeout(() => {
|
|
208
|
-
if (settled) return;
|
|
209
|
-
settled = true;
|
|
210
|
-
child.kill("SIGTERM");
|
|
211
|
-
reject(new Error(`${command} timed out after ${COMMAND_TIMEOUT_MS}ms`));
|
|
212
|
-
}, COMMAND_TIMEOUT_MS);
|
|
213
|
-
|
|
214
|
-
child.stdout.on("data", (chunk: Buffer) => {
|
|
215
|
-
stdout = (stdout + chunk.toString("utf8")).slice(0, MAX_OUTPUT_CHARS);
|
|
216
|
-
});
|
|
217
|
-
child.stderr.on("data", (chunk: Buffer) => {
|
|
218
|
-
stderr = (stderr + chunk.toString("utf8")).slice(0, MAX_OUTPUT_CHARS);
|
|
219
|
-
});
|
|
220
|
-
child.on("error", (err) => {
|
|
221
|
-
if (settled) return;
|
|
222
|
-
settled = true;
|
|
223
|
-
clearTimeout(timeout);
|
|
224
|
-
reject(err);
|
|
225
|
-
});
|
|
226
|
-
child.on("close", (code) => {
|
|
227
|
-
if (settled) return;
|
|
228
|
-
settled = true;
|
|
229
|
-
clearTimeout(timeout);
|
|
230
|
-
resolve({ code, stdout, stderr });
|
|
231
|
-
});
|
|
197
|
+
function runCommand(command: string, args: readonly string[]): Promise<ExternalToolCommandResult> {
|
|
198
|
+
return runExternalToolCommand(command, args, {
|
|
199
|
+
timeoutMs: COMMAND_TIMEOUT_MS,
|
|
200
|
+
maxOutputChars: MAX_OUTPUT_CHARS,
|
|
232
201
|
});
|
|
233
202
|
}
|
|
@@ -285,17 +285,22 @@ export function classifyIntent(input: string): ClassificationResult {
|
|
|
285
285
|
function isPortfolioEvaluationRequest(input: string): boolean {
|
|
286
286
|
const lower = input.toLowerCase();
|
|
287
287
|
const hasEvaluationIntent =
|
|
288
|
-
/\b(?:evaluat(?:e|ion)|review|assess|analy[sz]e|prospects?|risks?|opportunities?|mitigat(?:e|ion)|
|
|
289
|
-
lower,
|
|
290
|
-
);
|
|
291
|
-
const hasPortfolioObject =
|
|
292
|
-
/\b(?:portfolio|allocation|asset\s+allocation|60\/40|equity|fixed\s+income|bonds?)\b/.test(
|
|
288
|
+
/\b(?:evaluat(?:e|ion)|review|assess|analy[sz]e|prospects?|risks?|risky|opportunities?|mitigat(?:e|ion)|adjust(?:ment)?|change|rebalance|diversify|concentration|overweight|underweight|expos(?:ed|ure)|target\s+bands?|drift|worried|crash|protect|protection|missing\s+out\s+on\s+growth)\b/.test(
|
|
293
289
|
lower,
|
|
294
290
|
);
|
|
291
|
+
const hasExplicitPortfolioSubject =
|
|
292
|
+
/\b(?:portfolio|allocation|asset\s+allocation|60\/40|holdings?|positions?)\b/.test(lower);
|
|
293
|
+
const hasOwnedAssetMix =
|
|
294
|
+
/\b(?:my|current|have|holding|hold|own|invested|positions?|\d+(?:\.\d+)?%)\b/.test(lower) &&
|
|
295
|
+
/\b(?:equity|fixed\s+income|bonds?|cash|stocks?|etfs?|funds?)\b/.test(lower);
|
|
295
296
|
const hasConstructionIntent =
|
|
296
297
|
/\b(?:build|create|construct|put\s+together|invest|allocate)\b/.test(lower) &&
|
|
297
298
|
/\$\s*\d|\b\d+(?:\.\d+)?\s*k\b|\bbudget\b|\bcapital\b/.test(lower);
|
|
298
|
-
return
|
|
299
|
+
return (
|
|
300
|
+
hasEvaluationIntent &&
|
|
301
|
+
(hasExplicitPortfolioSubject || hasOwnedAssetMix) &&
|
|
302
|
+
!hasConstructionIntent
|
|
303
|
+
);
|
|
299
304
|
}
|
|
300
305
|
|
|
301
306
|
function isStatefulTrackingRequest(input: string): boolean {
|