opencandle 0.2.0 → 0.3.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.
Files changed (154) hide show
  1. package/README.md +110 -87
  2. package/dist/analysts/orchestrator.js +1 -2
  3. package/dist/analysts/orchestrator.js.map +1 -1
  4. package/dist/config.d.ts +25 -5
  5. package/dist/config.js +16 -8
  6. package/dist/config.js.map +1 -1
  7. package/dist/infra/cache.d.ts +4 -0
  8. package/dist/infra/cache.js +4 -0
  9. package/dist/infra/cache.js.map +1 -1
  10. package/dist/infra/rate-limiter.js +6 -0
  11. package/dist/infra/rate-limiter.js.map +1 -1
  12. package/dist/onboarding/connect.d.ts +23 -0
  13. package/dist/onboarding/connect.js +107 -0
  14. package/dist/onboarding/connect.js.map +1 -0
  15. package/dist/onboarding/credential-interceptor.d.ts +44 -0
  16. package/dist/onboarding/credential-interceptor.js +72 -0
  17. package/dist/onboarding/credential-interceptor.js.map +1 -0
  18. package/dist/onboarding/degradation-accumulator.d.ts +21 -0
  19. package/dist/onboarding/degradation-accumulator.js +55 -0
  20. package/dist/onboarding/degradation-accumulator.js.map +1 -0
  21. package/dist/onboarding/prompt-user.d.ts +23 -0
  22. package/dist/onboarding/prompt-user.js +61 -0
  23. package/dist/onboarding/prompt-user.js.map +1 -0
  24. package/dist/onboarding/providers.d.ts +109 -0
  25. package/dist/onboarding/providers.js +236 -0
  26. package/dist/onboarding/providers.js.map +1 -0
  27. package/dist/onboarding/state.d.ts +31 -2
  28. package/dist/onboarding/state.js +141 -13
  29. package/dist/onboarding/state.js.map +1 -1
  30. package/dist/onboarding/tool-helpers.d.ts +34 -0
  31. package/dist/onboarding/tool-helpers.js +80 -0
  32. package/dist/onboarding/tool-helpers.js.map +1 -0
  33. package/dist/onboarding/tool-tags.d.ts +37 -0
  34. package/dist/onboarding/tool-tags.js +149 -0
  35. package/dist/onboarding/tool-tags.js.map +1 -0
  36. package/dist/onboarding/validation.d.ts +19 -0
  37. package/dist/onboarding/validation.js +117 -0
  38. package/dist/onboarding/validation.js.map +1 -0
  39. package/dist/pi/opencandle-extension.js +303 -4
  40. package/dist/pi/opencandle-extension.js.map +1 -1
  41. package/dist/pi/setup.d.ts +0 -1
  42. package/dist/pi/setup.js +66 -119
  43. package/dist/pi/setup.js.map +1 -1
  44. package/dist/prompts/context-builder.js +2 -1
  45. package/dist/prompts/context-builder.js.map +1 -1
  46. package/dist/providers/alpha-vantage.js +20 -1
  47. package/dist/providers/alpha-vantage.js.map +1 -1
  48. package/dist/providers/exa-search.d.ts +39 -0
  49. package/dist/providers/exa-search.js +276 -0
  50. package/dist/providers/exa-search.js.map +1 -0
  51. package/dist/providers/finnhub.d.ts +17 -0
  52. package/dist/providers/finnhub.js +94 -0
  53. package/dist/providers/finnhub.js.map +1 -0
  54. package/dist/providers/fred.js +13 -1
  55. package/dist/providers/fred.js.map +1 -1
  56. package/dist/providers/index.d.ts +2 -0
  57. package/dist/providers/index.js +1 -0
  58. package/dist/providers/index.js.map +1 -1
  59. package/dist/providers/provider-credential-error.d.ts +8 -0
  60. package/dist/providers/provider-credential-error.js +22 -0
  61. package/dist/providers/provider-credential-error.js.map +1 -0
  62. package/dist/providers/reddit.d.ts +8 -0
  63. package/dist/providers/reddit.js +36 -9
  64. package/dist/providers/reddit.js.map +1 -1
  65. package/dist/providers/twitter.js +2 -8
  66. package/dist/providers/twitter.js.map +1 -1
  67. package/dist/providers/web-search.d.ts +17 -0
  68. package/dist/providers/web-search.js +224 -0
  69. package/dist/providers/web-search.js.map +1 -0
  70. package/dist/providers/wrap-provider.d.ts +7 -0
  71. package/dist/providers/wrap-provider.js +15 -0
  72. package/dist/providers/wrap-provider.js.map +1 -1
  73. package/dist/routing/classify-intent.js +22 -0
  74. package/dist/routing/classify-intent.js.map +1 -1
  75. package/dist/runtime/session-coordinator.d.ts +0 -1
  76. package/dist/runtime/session-coordinator.js.map +1 -1
  77. package/dist/sentiment/adapters/finnhub.d.ts +7 -0
  78. package/dist/sentiment/adapters/finnhub.js +39 -0
  79. package/dist/sentiment/adapters/finnhub.js.map +1 -0
  80. package/dist/sentiment/adapters/reddit.d.ts +11 -0
  81. package/dist/sentiment/adapters/reddit.js +54 -0
  82. package/dist/sentiment/adapters/reddit.js.map +1 -0
  83. package/dist/sentiment/adapters/twitter.d.ts +9 -0
  84. package/dist/sentiment/adapters/twitter.js +32 -0
  85. package/dist/sentiment/adapters/twitter.js.map +1 -0
  86. package/dist/sentiment/adapters/web.d.ts +9 -0
  87. package/dist/sentiment/adapters/web.js +40 -0
  88. package/dist/sentiment/adapters/web.js.map +1 -0
  89. package/dist/sentiment/index.d.ts +16 -0
  90. package/dist/sentiment/index.js +44 -0
  91. package/dist/sentiment/index.js.map +1 -0
  92. package/dist/sentiment/keywords.d.ts +2 -0
  93. package/dist/sentiment/keywords.js +9 -0
  94. package/dist/sentiment/keywords.js.map +1 -0
  95. package/dist/sentiment/pipeline.d.ts +9 -0
  96. package/dist/sentiment/pipeline.js +57 -0
  97. package/dist/sentiment/pipeline.js.map +1 -0
  98. package/dist/sentiment/scorer.d.ts +9 -0
  99. package/dist/sentiment/scorer.js +64 -0
  100. package/dist/sentiment/scorer.js.map +1 -0
  101. package/dist/sentiment/store.d.ts +24 -0
  102. package/dist/sentiment/store.js +177 -0
  103. package/dist/sentiment/store.js.map +1 -0
  104. package/dist/sentiment/trends.d.ts +13 -0
  105. package/dist/sentiment/trends.js +73 -0
  106. package/dist/sentiment/trends.js.map +1 -0
  107. package/dist/sentiment/types.d.ts +66 -0
  108. package/dist/sentiment/types.js +54 -0
  109. package/dist/sentiment/types.js.map +1 -0
  110. package/dist/system-prompt.js +9 -1
  111. package/dist/system-prompt.js.map +1 -1
  112. package/dist/tools/fundamentals/company-overview.d.ts +3 -1
  113. package/dist/tools/fundamentals/company-overview.js +27 -27
  114. package/dist/tools/fundamentals/company-overview.js.map +1 -1
  115. package/dist/tools/fundamentals/comps.js +45 -45
  116. package/dist/tools/fundamentals/comps.js.map +1 -1
  117. package/dist/tools/fundamentals/dcf.js +82 -82
  118. package/dist/tools/fundamentals/dcf.js.map +1 -1
  119. package/dist/tools/fundamentals/earnings.d.ts +3 -1
  120. package/dist/tools/fundamentals/earnings.js +25 -25
  121. package/dist/tools/fundamentals/earnings.js.map +1 -1
  122. package/dist/tools/fundamentals/financials.d.ts +3 -1
  123. package/dist/tools/fundamentals/financials.js +23 -23
  124. package/dist/tools/fundamentals/financials.js.map +1 -1
  125. package/dist/tools/index.js +8 -2
  126. package/dist/tools/index.js.map +1 -1
  127. package/dist/tools/interaction/ask-user.js +28 -64
  128. package/dist/tools/interaction/ask-user.js.map +1 -1
  129. package/dist/tools/macro/fred-data.d.ts +3 -1
  130. package/dist/tools/macro/fred-data.js +26 -26
  131. package/dist/tools/macro/fred-data.js.map +1 -1
  132. package/dist/tools/sentiment/reddit-sentiment.d.ts +3 -1
  133. package/dist/tools/sentiment/reddit-sentiment.js +107 -22
  134. package/dist/tools/sentiment/reddit-sentiment.js.map +1 -1
  135. package/dist/tools/sentiment/sentiment-summary.d.ts +7 -0
  136. package/dist/tools/sentiment/sentiment-summary.js +230 -0
  137. package/dist/tools/sentiment/sentiment-summary.js.map +1 -0
  138. package/dist/tools/sentiment/sentiment-trend.d.ts +22 -0
  139. package/dist/tools/sentiment/sentiment-trend.js +39 -0
  140. package/dist/tools/sentiment/sentiment-trend.js.map +1 -0
  141. package/dist/tools/sentiment/twitter-sentiment.js +17 -0
  142. package/dist/tools/sentiment/twitter-sentiment.js.map +1 -1
  143. package/dist/tools/sentiment/web-search.d.ts +11 -0
  144. package/dist/tools/sentiment/web-search.js +115 -0
  145. package/dist/tools/sentiment/web-search.js.map +1 -0
  146. package/dist/tools/sentiment/web-sentiment.d.ts +8 -0
  147. package/dist/tools/sentiment/web-sentiment.js +66 -0
  148. package/dist/tools/sentiment/web-sentiment.js.map +1 -0
  149. package/dist/types/index.d.ts +1 -1
  150. package/dist/types/sentiment.d.ts +21 -0
  151. package/package.json +19 -3
  152. package/dist/tools/sentiment/news-sentiment.d.ts +0 -7
  153. package/dist/tools/sentiment/news-sentiment.js +0 -55
  154. package/dist/tools/sentiment/news-sentiment.js.map +0 -1
@@ -0,0 +1,115 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { searchWeb } from "../../providers/web-search.js";
3
+ import { hasCredential } from "../../onboarding/providers.js";
4
+ import { buildSoftDegradedTag } from "../../onboarding/tool-tags.js";
5
+ const params = Type.Object({
6
+ query: Type.String({ description: "Search query — ticker, topic, or question" }),
7
+ category: Type.Optional(Type.Union([Type.Literal("news"), Type.Literal("general")], {
8
+ description: 'Search category. "news" for recent articles, "general" for broader web. Default: "news"',
9
+ })),
10
+ freshness: Type.Optional(Type.Union([Type.Literal("hours"), Type.Literal("day"), Type.Literal("week"), Type.Literal("month")], { description: 'Time range filter. Default: "day"' })),
11
+ limit: Type.Optional(Type.Number({ description: "Number of results (1-20). Default: 10", minimum: 1, maximum: 20 })),
12
+ provider: Type.Optional(Type.Union([Type.Literal("exa"), Type.Literal("brave"), Type.Literal("ddg")], {
13
+ description: "Override search provider (skip cascade). Default: auto (Exa → Brave → DDG)",
14
+ })),
15
+ });
16
+ function escapeMd(text) {
17
+ return text.replace(/([[\]|])/g, "\\$1");
18
+ }
19
+ function safeUrl(url) {
20
+ if (url.startsWith("https://") || url.startsWith("http://"))
21
+ return url;
22
+ return `https://${url}`;
23
+ }
24
+ /**
25
+ * Build soft-degradation tags for search providers whose credentials are
26
+ * missing at call time. Returns an empty string when nothing is degraded, or
27
+ * a newline-terminated block of `[OPENCANDLE_SOFT_DEGRADED ...]` tags ready to
28
+ * prepend to the tool result content. The extension's `tool_result` handler
29
+ * records these into the per-turn degradation accumulator; the system prompt
30
+ * instructs the LLM to surface them in a `**Data gaps**` section.
31
+ *
32
+ * Emission rules:
33
+ * - Brave: if `hasCredential("brave") === false` AND the envelope's
34
+ * provider is not `"brave"`, the cascade fell back from Brave.
35
+ * - Exa: if `hasCredential("exa") === false` AND the envelope's provider
36
+ * is `"exa"`, the Exa provider used the keyless MCP path instead of the
37
+ * keyed API. Envelopes served by `ddg` are NOT tagged for Exa (Exa was
38
+ * tried first and failed for a reason unrelated to credentials).
39
+ */
40
+ function buildSoftDegradedPrefix(data) {
41
+ const tags = [];
42
+ if (!hasCredential("brave") && data.provider !== "brave") {
43
+ tags.push(buildSoftDegradedTag({
44
+ provider: "brave",
45
+ fallback: data.provider === "exa" ? "exa" : "ddg",
46
+ remediation: "run /connect search to enable Brave",
47
+ }));
48
+ }
49
+ if (!hasCredential("exa") && data.provider === "exa") {
50
+ tags.push(buildSoftDegradedTag({
51
+ provider: "exa",
52
+ fallback: "keyless-mcp",
53
+ remediation: "run /connect search to enable keyed Exa",
54
+ }));
55
+ }
56
+ return tags.length === 0 ? "" : `${tags.join("\n")}\n\n`;
57
+ }
58
+ export const webSearchTool = {
59
+ name: "search_web",
60
+ label: "Web Search",
61
+ description: "Search the web for financial news, earnings context, company events, regulatory developments, or general information. " +
62
+ "NOT for real-time prices, historical data, fundamentals, macro data, SEC filings, or social sentiment — those have dedicated tools.",
63
+ parameters: params,
64
+ async execute(toolCallId, args) {
65
+ const query = args.query?.trim();
66
+ if (!query) {
67
+ return {
68
+ content: [{ type: "text", text: "⚠ Cannot search with an empty query." }],
69
+ details: null,
70
+ };
71
+ }
72
+ const category = args.category ?? "news";
73
+ const freshness = args.freshness ?? "day";
74
+ const limit = Math.max(1, Math.min(args.limit ?? 10, 20));
75
+ const provider = args.provider;
76
+ const result = await searchWeb(query, { category, freshness, limit, provider });
77
+ if (result.status === "unavailable") {
78
+ return {
79
+ content: [{ type: "text", text: `⚠ Web search unavailable (${result.reason}).` }],
80
+ details: null,
81
+ };
82
+ }
83
+ const data = result.data;
84
+ if (data.resultCount === 0) {
85
+ const zeroPrefix = buildSoftDegradedPrefix(data);
86
+ return {
87
+ content: [
88
+ {
89
+ type: "text",
90
+ text: `${zeroPrefix}No results found for "${query}" (${category}, past ${freshness}).`,
91
+ },
92
+ ],
93
+ details: data,
94
+ };
95
+ }
96
+ const stalePrefix = result.stale
97
+ ? `⚠ Using cached data from ${result.timestamp}\n\n`
98
+ : "";
99
+ const softDegradedPrefix = buildSoftDegradedPrefix(data);
100
+ const header = `**Web Search** — ${data.resultCount} results for "${query}" (${category}, past ${freshness}, via ${data.provider})`;
101
+ const items = data.results.map((r) => {
102
+ const title = escapeMd(r.title);
103
+ const snippet = escapeMd(r.snippet);
104
+ const url = safeUrl(r.url);
105
+ const pub = r.published ? `Published: ${r.published}` : "Published: unknown";
106
+ return `• [${title}](${url}) — ${r.source}\n ${snippet}\n ${pub}`;
107
+ });
108
+ const text = `${softDegradedPrefix}${stalePrefix}${header}\n\n${items.join("\n\n")}`;
109
+ return {
110
+ content: [{ type: "text", text }],
111
+ details: data,
112
+ };
113
+ },
114
+ };
115
+ //# sourceMappingURL=web-search.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-search.js","sourceRoot":"","sources":["../../../src/tools/sentiment/web-search.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import type { AgentTool } from "@mariozechner/pi-agent-core";
2
+ declare const params: import("@sinclair/typebox").TObject<{
3
+ query: import("@sinclair/typebox").TString;
4
+ freshness: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"day">, import("@sinclair/typebox").TLiteral<"week">, import("@sinclair/typebox").TLiteral<"month">]>>;
5
+ limit: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
6
+ }>;
7
+ export declare const webSentimentTool: AgentTool<typeof params>;
8
+ export {};
@@ -0,0 +1,66 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { searchWeb } from "../../providers/web-search.js";
3
+ import { WebAdapter } from "../../sentiment/adapters/web.js";
4
+ import { getSentimentPipeline } from "../../sentiment/index.js";
5
+ const params = Type.Object({
6
+ query: Type.String({ description: "Ticker or topic to search for web/news sentiment" }),
7
+ freshness: Type.Optional(Type.Union([Type.Literal("day"), Type.Literal("week"), Type.Literal("month")], {
8
+ description: "Time window for results. Default: day",
9
+ })),
10
+ limit: Type.Optional(Type.Number({ description: "Max results. Default: 10, max: 20" })),
11
+ });
12
+ export const webSentimentTool = {
13
+ name: "get_web_sentiment",
14
+ label: "Web/News Sentiment",
15
+ description: "Analyze sentiment from web and news search results for a ticker or topic. Returns scored results with aggregate sentiment.",
16
+ parameters: params,
17
+ async execute(toolCallId, args) {
18
+ const freshness = args.freshness ?? "day";
19
+ const limit = Math.min(args.limit ?? 10, 20);
20
+ const providerResult = await searchWeb(args.query, { freshness, limit, category: "news" });
21
+ if (providerResult.status === "unavailable") {
22
+ return {
23
+ content: [{ type: "text", text: `⚠ Web sentiment unavailable for "${args.query}" (${providerResult.reason}).` }],
24
+ details: null,
25
+ };
26
+ }
27
+ const adapter = new WebAdapter();
28
+ const records = adapter.mapToRecords(providerResult.data, args.query);
29
+ const pipeline = getSentimentPipeline();
30
+ const result = await pipeline.processRecords(records, args.query);
31
+ const lines = [];
32
+ if (result.fresh.length === 0) {
33
+ lines.push(`No web results found for "${args.query}".`);
34
+ }
35
+ else {
36
+ const avgScore = result.fresh.reduce((s, r) => s + r.sentiment.score, 0) / result.fresh.length;
37
+ const label = sentimentLabel(avgScore);
38
+ lines.push(`**Web sentiment for "${args.query}"** — ${result.fresh.length} results (${label}, ${avgScore.toFixed(2)})`);
39
+ lines.push("");
40
+ for (const rec of result.fresh.slice(0, limit)) {
41
+ const indicator = rec.sentiment.score > 0 ? "🟢" : rec.sentiment.score < 0 ? "🔴" : "⚪";
42
+ lines.push(`${indicator} [${rec.title}](${rec.url}) — *${rec.author}*`);
43
+ lines.push(` ${rec.text.slice(0, 150)}`);
44
+ lines.push(` Score: ${rec.sentiment.score.toFixed(2)} | Confidence: ${rec.sentiment.confidence.toFixed(2)}`);
45
+ }
46
+ if (result.trend) {
47
+ lines.push("");
48
+ const t = result.trend[0];
49
+ lines.push(`Trend: ${t.sparkline} ${t.direction} (${t.count} records)`);
50
+ }
51
+ }
52
+ return { content: [{ type: "text", text: lines.join("\n") }], details: result };
53
+ },
54
+ };
55
+ function sentimentLabel(score) {
56
+ if (score > 0.3)
57
+ return "Bullish";
58
+ if (score < -0.3)
59
+ return "Bearish";
60
+ if (score > 0)
61
+ return "Leaning Bullish";
62
+ if (score < 0)
63
+ return "Leaning Bearish";
64
+ return "Neutral";
65
+ }
66
+ //# sourceMappingURL=web-sentiment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-sentiment.js","sourceRoot":"","sources":["../../../src/tools/sentiment/web-sentiment.ts"],"names":[],"mappings":"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"}
@@ -4,7 +4,7 @@ export type { FredObservation, FredSeries } from "./macro.js";
4
4
  export { FRED_SERIES } from "./macro.js";
5
5
  export type { Greeks, OptionContract, OptionsChain } from "./options.js";
6
6
  export type { Position, PortfolioSummary, RiskMetrics, TechnicalIndicators } from "./portfolio.js";
7
- export type { FearGreedData, RedditSentimentResult } from "./sentiment.js";
7
+ export type { FearGreedData, RedditSentimentResult, WebSearchResult, WebSearchEnvelope } from "./sentiment.js";
8
8
  /**
9
9
  * Handler for `ask_user` tool invocations in non-UI contexts (e.g. test harness).
10
10
  * When provided to `createOpenCandleSession`, the ask-user tool calls this handler
@@ -7,6 +7,7 @@ export interface FearGreedData {
7
7
  monthAgo: number | null;
8
8
  }
9
9
  export interface TwitterTweet {
10
+ id: string;
10
11
  text: string;
11
12
  author: string;
12
13
  likes: number;
@@ -26,11 +27,31 @@ export interface TwitterSentimentResult {
26
27
  topMentions: string[];
27
28
  fetchedAt: string;
28
29
  }
30
+ export interface WebSearchResult {
31
+ title: string;
32
+ url: string;
33
+ snippet: string;
34
+ /** Domain extracted from url (e.g., "reuters.com") */
35
+ source: string;
36
+ /** ISO 8601 timestamp, null if unknown */
37
+ published: string | null;
38
+ category: "news" | "general";
39
+ }
40
+ export interface WebSearchEnvelope {
41
+ query: string;
42
+ results: WebSearchResult[];
43
+ resultCount: number;
44
+ fetchedAt: string;
45
+ provider: "ddg" | "brave" | "exa";
46
+ }
29
47
  export interface RedditSentimentResult {
30
48
  subreddit: string;
31
49
  postCount: number;
32
50
  posts: Array<{
51
+ id: string;
33
52
  title: string;
53
+ selftext: string;
54
+ author: string;
34
55
  score: number;
35
56
  comments: number;
36
57
  url: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencandle",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Financial trading & investing agent",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/Kahtaf/OpenCandle#readme",
@@ -43,9 +43,20 @@
43
43
  ]
44
44
  },
45
45
  "keywords": [
46
- "pi-package",
47
46
  "opencandle",
48
- "finance"
47
+ "finance",
48
+ "stock",
49
+ "investing",
50
+ "trading",
51
+ "options",
52
+ "market-data",
53
+ "portfolio",
54
+ "ai-agent",
55
+ "terminal",
56
+ "financial-analysis",
57
+ "sentiment",
58
+ "technical-analysis",
59
+ "pi-package"
49
60
  ],
50
61
  "files": [
51
62
  "dist"
@@ -58,6 +69,10 @@
58
69
  "test:watch": "vitest",
59
70
  "test:e2e": "tsx tests/e2e/tools.test.ts",
60
71
  "test:e2e:cli": "tsx tests/e2e/cli.test.ts",
72
+ "test:e2e:credential-prompt": "tsx tests/e2e/credential-prompt.test.ts",
73
+ "test:e2e:credential-snooze": "tsx tests/e2e/credential-snooze.test.ts",
74
+ "test:e2e:credential-soft-fallback": "tsx tests/e2e/credential-soft-fallback.test.ts",
75
+ "test:e2e:credential-per-workflow-cap": "tsx tests/e2e/credential-per-workflow-cap.test.ts",
61
76
  "test:e2e:providers": "tsx tests/e2e/providers.test.ts",
62
77
  "test:evals": "vitest run --config vitest.config.evals.ts",
63
78
  "test:evals:usually": "EVAL_TIER=usually vitest run --config vitest.config.evals.ts",
@@ -78,6 +93,7 @@
78
93
  "@the-convocation/twitter-scraper": "^0.22.3",
79
94
  "better-sqlite3": "^12.8.0",
80
95
  "camoufox-js": "^0.9.3",
96
+ "duck-duck-scrape": "^2.2.7",
81
97
  "playwright-core": "^1.58.2"
82
98
  },
83
99
  "peerDependencies": {
@@ -1,7 +0,0 @@
1
- import type { AgentTool } from "@mariozechner/pi-agent-core";
2
- import type { RedditSentimentResult } from "../../types/sentiment.js";
3
- declare const params: import("@sinclair/typebox").TObject<{
4
- topic: import("@sinclair/typebox").TString;
5
- }>;
6
- export declare const newsSentimentTool: AgentTool<typeof params, RedditSentimentResult>;
7
- export {};
@@ -1,55 +0,0 @@
1
- import { Type } from "@sinclair/typebox";
2
- import { getSubredditPosts } from "../../providers/reddit.js";
3
- import { wrapProvider } from "../../providers/wrap-provider.js";
4
- const params = Type.Object({
5
- topic: Type.String({
6
- description: "Topic or ticker to search for news sentiment (e.g. AAPL, bitcoin, inflation)",
7
- }),
8
- });
9
- export const newsSentimentTool = {
10
- name: "get_reddit_discussions",
11
- label: "Reddit Financial Discussions",
12
- description: "Search financial Reddit communities (r/stocks, r/investing) for recent discussions about a topic. Useful for gauging retail sentiment, but not a substitute for news or institutional analysis.",
13
- parameters: params,
14
- async execute(toolCallId, args) {
15
- // Use r/stocks and r/investing as news proxies
16
- const subreddits = ["stocks", "investing"];
17
- const allPosts = [];
18
- const allMentions = [];
19
- for (const sub of subreddits) {
20
- const providerResult = await wrapProvider("reddit", () => getSubredditPosts(sub, 25));
21
- if (providerResult.status === "unavailable")
22
- continue;
23
- const result = providerResult.data;
24
- const relevant = result.posts.filter((p) => p.title.toLowerCase().includes(args.topic.toLowerCase()));
25
- allPosts.push(...relevant);
26
- allMentions.push(...result.topMentions);
27
- }
28
- const uniqueMentions = [...new Set(allMentions)];
29
- const text = [
30
- `**Reddit Discussions for "${args.topic}"** — ${allPosts.length} relevant posts found`,
31
- uniqueMentions.length > 0
32
- ? `Related tickers: ${uniqueMentions.map((t) => `$${t}`).join(", ")}`
33
- : "",
34
- "",
35
- ...allPosts.slice(0, 10).map((p) => ` ⬆${p.score} 💬${p.comments} — ${p.title.slice(0, 100)}`),
36
- allPosts.length === 0 ? "No relevant discussions found. Try broader search terms." : "",
37
- ]
38
- .filter(Boolean)
39
- .join("\n");
40
- return {
41
- content: [{ type: "text", text }],
42
- details: {
43
- subreddit: "stocks+investing",
44
- postCount: allPosts.length,
45
- posts: allPosts,
46
- topMentions: uniqueMentions,
47
- sentimentScore: 0,
48
- bullishCount: 0,
49
- bearishCount: 0,
50
- fetchedAt: new Date().toISOString(),
51
- },
52
- };
53
- },
54
- };
55
- //# sourceMappingURL=news-sentiment.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"news-sentiment.js","sourceRoot":"","sources":["../../../src/tools/sentiment/news-sentiment.ts"],"names":[],"mappings":"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"}