opencandle 0.6.0 → 0.7.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 +10 -3
- package/dist/cli.js +36 -0
- 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/infra/index.d.ts +0 -1
- package/dist/infra/index.js +0 -1
- package/dist/infra/index.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 +48 -0
- package/dist/onboarding/provider-status.js +285 -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/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 +1 -1
- package/dist/prompts/policy-cards.js.map +1 -1
- 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 +36 -0
- package/dist/providers/reddit-cli.js +201 -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 +40 -0
- package/dist/providers/twitter-cli.js +153 -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/planning.d.ts +1 -1
- package/dist/routing/planning.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/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 +18 -0
- package/gui/server/tool-metadata.ts +80 -16
- package/gui/server/ws-hub.ts +19 -0
- package/gui/web/dist/assets/CatalogOverlay-CgeY5Pkp.js +1 -0
- package/gui/web/dist/assets/index-C6W_2eAn.js +69 -0
- package/gui/web/dist/assets/{index-2KZtKBmu.css → index-hwbx24a5.css} +1 -1
- package/gui/web/dist/index.html +2 -2
- package/package.json +5 -6
- package/src/cli.ts +41 -0
- package/src/config.ts +27 -0
- package/src/infra/index.ts +0 -1
- package/src/onboarding/connect.ts +20 -4
- package/src/onboarding/provider-status.ts +410 -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/tool-adapter.ts +14 -4
- package/src/prompts/context-builder.ts +1 -1
- package/src/prompts/policy-cards.ts +1 -1
- package/src/providers/external-tool-error.ts +20 -0
- package/src/providers/reddit-cli.ts +317 -0
- package/src/providers/reddit.ts +7 -63
- package/src/providers/twitter-cli.ts +233 -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/planning.ts +1 -0
- 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
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { Type } from "@sinclair/typebox";
|
|
4
|
+
import { promptUser } from "../../onboarding/prompt-user.js";
|
|
5
|
+
import { getProvider } from "../../onboarding/providers.js";
|
|
6
|
+
import {
|
|
7
|
+
loadOnboardingState,
|
|
8
|
+
markProviderNeverAsk,
|
|
9
|
+
saveOnboardingState,
|
|
10
|
+
} from "../../onboarding/state.js";
|
|
11
|
+
import { buildExternalToolRequiredTag, buildSkippedTag } from "../../onboarding/tool-tags.js";
|
|
3
12
|
import { getTwitterSentiment } from "../../providers/twitter.js";
|
|
4
13
|
import { wrapProvider } from "../../providers/wrap-provider.js";
|
|
5
14
|
import { TwitterAdapter } from "../../sentiment/adapters/twitter.js";
|
|
6
15
|
import { getSentimentPipeline } from "../../sentiment/index.js";
|
|
16
|
+
import type { AskUserHandler } from "../../types/index.js";
|
|
7
17
|
import type { TwitterSentimentResult } from "../../types/sentiment.js";
|
|
18
|
+
import { formatInsightSection } from "./insight-format.js";
|
|
8
19
|
import { renderUntrustedText, untrustedContentHeader } from "./untrusted-text.js";
|
|
9
20
|
|
|
10
21
|
const params = Type.Object({
|
|
@@ -15,89 +26,262 @@ const params = Type.Object({
|
|
|
15
26
|
hours: Type.Optional(Type.Number({ description: "Lookback window in hours. Default: 24" })),
|
|
16
27
|
});
|
|
17
28
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"Fetch recent tweets for a stock ticker or search query and compute engagement-weighted sentiment. Returns tweet data, sentiment score, and co-mentioned tickers. Requires a Twitter session via trigger_twitter_login.",
|
|
23
|
-
parameters: params,
|
|
24
|
-
async execute(_toolCallId, args) {
|
|
25
|
-
const limit = Math.min(args.limit ?? 50, 200);
|
|
26
|
-
const hours = args.hours ?? 24;
|
|
27
|
-
|
|
28
|
-
const providerResult = await wrapProvider("twitter", () =>
|
|
29
|
-
getTwitterSentiment(args.query, limit, hours),
|
|
30
|
-
);
|
|
29
|
+
const INSTALL_CONTINUE_LABEL = "Continue after installing twitter-cli";
|
|
30
|
+
const SESSION_CONTINUE_LABEL = "Continue after refreshing X/Twitter session";
|
|
31
|
+
const SKIP_ONCE_LABEL = "Skip X/Twitter once";
|
|
32
|
+
const ALWAYS_SKIP_LABEL = "Always skip X/Twitter";
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
providerResult.reason.includes("session expired");
|
|
36
|
-
const text = isLoginIssue
|
|
37
|
-
? `⚠ Twitter sentiment unavailable: ${providerResult.reason}\n[LOGIN_NEEDED] Use ask_user to confirm, then call trigger_twitter_login. After success, retry this tool.`
|
|
38
|
-
: `⚠ Twitter sentiment unavailable (${providerResult.reason}).`;
|
|
39
|
-
return {
|
|
40
|
-
content: [{ type: "text", text }],
|
|
41
|
-
details: null as any,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
34
|
+
interface TwitterSentimentToolOptions {
|
|
35
|
+
askUserHandler?: AskUserHandler;
|
|
36
|
+
}
|
|
44
37
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
result.sentimentScore > 0.3
|
|
49
|
-
? "Bullish"
|
|
50
|
-
: result.sentimentScore < -0.3
|
|
51
|
-
? "Bearish"
|
|
52
|
-
: result.sentimentScore > 0
|
|
53
|
-
? "Leaning Bullish"
|
|
54
|
-
: result.sentimentScore < 0
|
|
55
|
-
? "Leaning Bearish"
|
|
56
|
-
: "Neutral";
|
|
57
|
-
|
|
58
|
-
const lines = [
|
|
59
|
-
`**Twitter: ${result.query}** — ${result.tweetCount} tweets (last ${hours}h, ${result.fetchedAt})`,
|
|
60
|
-
`Sentiment: ${result.sentimentScore.toFixed(2)} (${sentimentLabel}) | Bullish: ${result.bullishCount} | Bearish: ${result.bearishCount}`,
|
|
61
|
-
];
|
|
62
|
-
|
|
63
|
-
if (result.topMentions.length > 0) {
|
|
64
|
-
lines.push(`Co-mentions: ${result.topMentions.map((t) => `$${t}`).join(", ")}`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
lines.push("");
|
|
68
|
-
lines.push(untrustedContentHeader("tweets"));
|
|
69
|
-
lines.push("| Author | Tweet | ❤️ | 🔁 | 💬 |");
|
|
70
|
-
lines.push("|--------|-------|----|----|----|");
|
|
71
|
-
const top = result.tweets.slice(0, 15);
|
|
72
|
-
for (const tweet of top) {
|
|
73
|
-
const text = renderUntrustedText(tweet.text, 100);
|
|
74
|
-
lines.push(
|
|
75
|
-
`| @${tweet.author} | ${text} | ${tweet.likes} | ${tweet.retweets} | ${tweet.replies} |`,
|
|
76
|
-
);
|
|
77
|
-
}
|
|
38
|
+
interface ExtensionContextWithAskUserHandler extends Partial<ExtensionContext> {
|
|
39
|
+
askUserHandler?: AskUserHandler;
|
|
40
|
+
}
|
|
78
41
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
lines.push(`⚠ Stale data (cached at ${providerResult.timestamp})`);
|
|
82
|
-
}
|
|
42
|
+
type TwitterUnavailableKind = "install" | "session" | "other";
|
|
43
|
+
type TwitterToolDetails = TwitterSentimentResult | null;
|
|
83
44
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
45
|
+
export function createTwitterSentimentTool(
|
|
46
|
+
options: TwitterSentimentToolOptions = {},
|
|
47
|
+
): AgentTool<typeof params, TwitterToolDetails> {
|
|
48
|
+
return {
|
|
49
|
+
name: "get_twitter_sentiment",
|
|
50
|
+
label: "Twitter Sentiment",
|
|
51
|
+
description:
|
|
52
|
+
"Fetch recent tweets for a stock ticker or search query and compute engagement-weighted sentiment. Returns tweet data, sentiment score, and co-mentioned tickers. Requires twitter-cli and a browser X/Twitter session.",
|
|
53
|
+
parameters: params,
|
|
54
|
+
async execute(_toolCallId, args, _signal, _onUpdate, ctx?: ExtensionContext) {
|
|
55
|
+
const limit = Math.min(args.limit ?? 50, 200);
|
|
56
|
+
const hours = args.hours ?? 24;
|
|
57
|
+
const descriptor = getProvider("twitter");
|
|
58
|
+
const state = loadOnboardingState();
|
|
59
|
+
if (state.providers.twitter?.status === "never_ask") {
|
|
60
|
+
return skippedResult(
|
|
61
|
+
"You previously asked not to be reminded about X/Twitter.",
|
|
62
|
+
true,
|
|
63
|
+
"run opencandle doctor --enable twitter to re-enable X/Twitter sentiment (silenced)",
|
|
95
64
|
);
|
|
96
65
|
}
|
|
97
|
-
|
|
98
|
-
|
|
66
|
+
|
|
67
|
+
const fetchTwitterSentiment = () =>
|
|
68
|
+
wrapProvider("twitter", () => getTwitterSentiment(args.query, limit, hours));
|
|
69
|
+
const providerResult = await fetchTwitterSentiment();
|
|
70
|
+
|
|
71
|
+
if (providerResult.status === "unavailable") {
|
|
72
|
+
const reason = providerResult.reason;
|
|
73
|
+
const unavailableKind = classifyUnavailableReason(reason);
|
|
74
|
+
const askUserHandler =
|
|
75
|
+
options.askUserHandler ??
|
|
76
|
+
(ctx as ExtensionContextWithAskUserHandler | undefined)?.askUserHandler;
|
|
77
|
+
const canAsk = askUserHandler != null || ctx?.hasUI === true;
|
|
78
|
+
|
|
79
|
+
if (unavailableKind !== "other" && canAsk) {
|
|
80
|
+
const promptResult = await promptUser(
|
|
81
|
+
(ctx ?? { hasUI: false }) as ExtensionContext,
|
|
82
|
+
{
|
|
83
|
+
question: setupQuestion(unavailableKind, reason),
|
|
84
|
+
questionType: "select",
|
|
85
|
+
options: [
|
|
86
|
+
unavailableKind === "install" ? INSTALL_CONTINUE_LABEL : SESSION_CONTINUE_LABEL,
|
|
87
|
+
SKIP_ONCE_LABEL,
|
|
88
|
+
ALWAYS_SKIP_LABEL,
|
|
89
|
+
],
|
|
90
|
+
reason:
|
|
91
|
+
"X/Twitter sentiment needs the twitter-cli command and a browser session before it can fetch tweets.",
|
|
92
|
+
},
|
|
93
|
+
askUserHandler,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
if (!promptResult.cancelled && promptResult.answer?.startsWith("Continue")) {
|
|
97
|
+
const retried = await fetchTwitterSentiment();
|
|
98
|
+
if (retried.status === "ok") {
|
|
99
|
+
return formatTwitterSentimentResult(retried.data, args.query, hours, {
|
|
100
|
+
stale: retried.stale,
|
|
101
|
+
timestamp: retried.timestamp,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return passiveUnavailableResult(
|
|
105
|
+
retried.reason,
|
|
106
|
+
classifyUnavailableReason(retried.reason),
|
|
107
|
+
{ interceptable: false },
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!promptResult.cancelled && promptResult.answer === ALWAYS_SKIP_LABEL) {
|
|
112
|
+
saveOnboardingState(markProviderNeverAsk(state, "twitter"));
|
|
113
|
+
return skippedResult(
|
|
114
|
+
`User chose to always skip ${descriptor.displayName} data. Do not ask about X/Twitter setup again unless the user explicitly reconnects it.`,
|
|
115
|
+
true,
|
|
116
|
+
"run opencandle doctor --enable twitter to re-enable X/Twitter sentiment (silenced)",
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return skippedResult(
|
|
121
|
+
`User chose to skip ${descriptor.displayName} data for this request. Do not ask about X/Twitter setup again in this turn.`,
|
|
122
|
+
false,
|
|
123
|
+
"user chose to skip X/Twitter for this request",
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return passiveUnavailableResult(reason, unavailableKind);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return formatTwitterSentimentResult(providerResult.data, args.query, hours, {
|
|
131
|
+
stale: providerResult.stale,
|
|
132
|
+
timestamp: providerResult.timestamp,
|
|
133
|
+
});
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const twitterSentimentTool = createTwitterSentimentTool();
|
|
139
|
+
|
|
140
|
+
function classifyUnavailableReason(reason: string): TwitterUnavailableKind {
|
|
141
|
+
if (reason.includes("not installed") || reason.includes("uv tool install")) return "install";
|
|
142
|
+
if (/no twitter cookies|no cookies|401|unauthorized|expired|session/i.test(reason)) {
|
|
143
|
+
return "session";
|
|
144
|
+
}
|
|
145
|
+
return "other";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function setupQuestion(kind: Exclude<TwitterUnavailableKind, "other">, reason: string): string {
|
|
149
|
+
if (kind === "install") {
|
|
150
|
+
return `X/Twitter sentiment requires twitter-cli. Install it with \`uv tool install twitter-cli\`, then choose whether to continue or skip X/Twitter. Current status: ${reason}`;
|
|
151
|
+
}
|
|
152
|
+
return `X/Twitter sentiment needs an active browser session. Log into or refresh x.com in a supported browser, then choose whether to continue or skip X/Twitter. Current status: ${reason}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function passiveUnavailableResult(
|
|
156
|
+
reason: string,
|
|
157
|
+
kind: TwitterUnavailableKind,
|
|
158
|
+
options: { interceptable?: boolean } = {},
|
|
159
|
+
) {
|
|
160
|
+
if ((options.interceptable ?? true) && (kind === "install" || kind === "session")) {
|
|
161
|
+
const tag = buildExternalToolRequiredTag({
|
|
162
|
+
provider: "twitter",
|
|
163
|
+
reason:
|
|
164
|
+
kind === "install"
|
|
165
|
+
? "not_installed"
|
|
166
|
+
: /401|unauthorized|expired/i.test(reason)
|
|
167
|
+
? "session_stale"
|
|
168
|
+
: "session_missing",
|
|
169
|
+
installCmd: "uv tool install twitter-cli",
|
|
170
|
+
loginCmd: "log into x.com in a supported browser",
|
|
171
|
+
fallback: "reddit-web-news",
|
|
172
|
+
});
|
|
173
|
+
const guidance =
|
|
174
|
+
kind === "install"
|
|
175
|
+
? "Twitter sentiment requires twitter-cli. Install it with `uv tool install twitter-cli`, then choose whether to continue or skip X/Twitter."
|
|
176
|
+
: "Twitter sentiment requires an active X/Twitter browser session. Log into or refresh x.com in a supported browser, then retry after confirmation.";
|
|
177
|
+
return {
|
|
178
|
+
content: [
|
|
179
|
+
{
|
|
180
|
+
type: "text" as const,
|
|
181
|
+
text: `⚠ Twitter sentiment unavailable: ${reason}\n${tag}\n${guidance}`,
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
details: null,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const text = `⚠ Twitter sentiment unavailable (${reason}).`;
|
|
188
|
+
return {
|
|
189
|
+
content: [{ type: "text" as const, text }],
|
|
190
|
+
details: null,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function skippedResult(message: string, silenced: boolean, remediation: string) {
|
|
195
|
+
return {
|
|
196
|
+
content: [
|
|
197
|
+
{
|
|
198
|
+
type: "text" as const,
|
|
199
|
+
text: `${buildSkippedTag({
|
|
200
|
+
provider: "twitter",
|
|
201
|
+
reason: "credential_not_provided",
|
|
202
|
+
remediation,
|
|
203
|
+
silenced,
|
|
204
|
+
})}\n\n${message}`,
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
details: null,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function formatTwitterSentimentResult(
|
|
212
|
+
result: TwitterSentimentResult,
|
|
213
|
+
query: string,
|
|
214
|
+
hours: number,
|
|
215
|
+
metadata: { stale?: boolean; timestamp: string },
|
|
216
|
+
) {
|
|
217
|
+
const sentimentLabel =
|
|
218
|
+
result.sentimentScore > 0.3
|
|
219
|
+
? "Bullish"
|
|
220
|
+
: result.sentimentScore < -0.3
|
|
221
|
+
? "Bearish"
|
|
222
|
+
: result.sentimentScore > 0
|
|
223
|
+
? "Leaning Bullish"
|
|
224
|
+
: result.sentimentScore < 0
|
|
225
|
+
? "Leaning Bearish"
|
|
226
|
+
: "Neutral";
|
|
227
|
+
|
|
228
|
+
const lines = [
|
|
229
|
+
`**Twitter: ${result.query}** — ${result.tweetCount} tweets (last ${hours}h, ${result.fetchedAt})`,
|
|
230
|
+
`Sentiment: ${result.sentimentScore.toFixed(2)} (${sentimentLabel}) | Bullish: ${result.bullishCount} | Bearish: ${result.bearishCount}`,
|
|
231
|
+
];
|
|
232
|
+
|
|
233
|
+
if (result.topMentions.length > 0) {
|
|
234
|
+
lines.push(`Co-mentions: ${result.topMentions.map((t) => `$${t}`).join(", ")}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
lines.push("");
|
|
238
|
+
lines.push(untrustedContentHeader("tweets"));
|
|
239
|
+
lines.push("| Author | Tweet | ❤️ | 🔁 | 💬 |");
|
|
240
|
+
lines.push("|--------|-------|----|----|----|");
|
|
241
|
+
const top = result.tweets.slice(0, 15);
|
|
242
|
+
for (const tweet of top) {
|
|
243
|
+
const text = renderUntrustedText(tweet.text, 100);
|
|
244
|
+
lines.push(
|
|
245
|
+
`| @${tweet.author} | ${text} | ${tweet.likes} | ${tweet.retweets} | ${tweet.replies} |`,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (metadata.stale) {
|
|
250
|
+
lines.push("");
|
|
251
|
+
lines.push(`⚠ Stale data (cached at ${metadata.timestamp})`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
let details: TwitterSentimentResult = result;
|
|
255
|
+
|
|
256
|
+
// Index in sentiment store and append trend context
|
|
257
|
+
try {
|
|
258
|
+
const adapter = new TwitterAdapter();
|
|
259
|
+
const records = adapter.mapToRecords(result, query);
|
|
260
|
+
const pipeline = getSentimentPipeline();
|
|
261
|
+
const pipelineResult = await pipeline.processRecords(records, query);
|
|
262
|
+
if (pipelineResult.insight) {
|
|
263
|
+
const insight = metadata.stale
|
|
264
|
+
? {
|
|
265
|
+
...pipelineResult.insight,
|
|
266
|
+
caveats: [
|
|
267
|
+
...pipelineResult.insight.caveats,
|
|
268
|
+
`Stale data cached at ${metadata.timestamp}.`,
|
|
269
|
+
],
|
|
270
|
+
}
|
|
271
|
+
: pipelineResult.insight;
|
|
272
|
+
details = { ...result, insight };
|
|
273
|
+
lines.push(...formatInsightSection(insight));
|
|
274
|
+
}
|
|
275
|
+
if (pipelineResult.trend && pipelineResult.trend.length > 0) {
|
|
276
|
+
const t = pipelineResult.trend[0];
|
|
277
|
+
lines.push("");
|
|
278
|
+
lines.push(
|
|
279
|
+
`Trend: ${t.sparkline} ${t.direction} (${t.delta >= 0 ? "+" : ""}${t.delta.toFixed(2)}, ${t.count} records)`,
|
|
280
|
+
);
|
|
99
281
|
}
|
|
282
|
+
} catch {
|
|
283
|
+
// Sentiment indexing is best-effort — don't fail the tool
|
|
284
|
+
}
|
|
100
285
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
};
|
|
286
|
+
return { content: [{ type: "text" as const, text: lines.join("\n") }], details };
|
|
287
|
+
}
|
|
@@ -3,6 +3,7 @@ import { Type } from "@sinclair/typebox";
|
|
|
3
3
|
import { searchWeb } from "../../providers/web-search.js";
|
|
4
4
|
import { WebAdapter } from "../../sentiment/adapters/web.js";
|
|
5
5
|
import { getSentimentPipeline } from "../../sentiment/index.js";
|
|
6
|
+
import { formatInsightSection } from "./insight-format.js";
|
|
6
7
|
import { renderUntrustedText, untrustedContentHeader } from "./untrusted-text.js";
|
|
7
8
|
|
|
8
9
|
const params = Type.Object({
|
|
@@ -54,6 +55,9 @@ export const webSentimentTool: AgentTool<typeof params> = {
|
|
|
54
55
|
lines.push(
|
|
55
56
|
`**Web sentiment for "${args.query}"** — ${result.fresh.length} results (${label}, ${avgScore.toFixed(2)})`,
|
|
56
57
|
);
|
|
58
|
+
if (result.insight) {
|
|
59
|
+
lines.push(...formatInsightSection(result.insight));
|
|
60
|
+
}
|
|
57
61
|
lines.push("");
|
|
58
62
|
lines.push(untrustedContentHeader("web sentiment results"));
|
|
59
63
|
|
package/src/types/sentiment.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { SentinelRecord } from "../sentiment/types.js";
|
|
2
|
+
|
|
1
3
|
export interface FearGreedData {
|
|
2
4
|
value: number;
|
|
3
5
|
label: string; // "Extreme Fear" | "Fear" | "Neutral" | "Greed" | "Extreme Greed"
|
|
@@ -19,6 +21,60 @@ export interface TwitterTweet {
|
|
|
19
21
|
created: string;
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
export type SentimentInsightMethod = "deterministic-keyword-v1" | "llm";
|
|
25
|
+
export type SentimentConfidenceLevel = "low" | "medium" | "high";
|
|
26
|
+
|
|
27
|
+
export interface SentimentInsightConfidence {
|
|
28
|
+
level: SentimentConfidenceLevel;
|
|
29
|
+
score: number; // 0.0 to 1.0
|
|
30
|
+
reasons: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface SentimentInsightDriver {
|
|
34
|
+
label: string;
|
|
35
|
+
count: number;
|
|
36
|
+
polarity: "positive" | "negative" | "mixed";
|
|
37
|
+
terms: string[];
|
|
38
|
+
sourceIds: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SentimentRepresentativeItem {
|
|
42
|
+
source: "twitter" | "reddit" | "web" | "finnhub" | "aggregate";
|
|
43
|
+
sourceId: string;
|
|
44
|
+
title: string | null;
|
|
45
|
+
excerpt: string;
|
|
46
|
+
url: string | null;
|
|
47
|
+
author: string | null;
|
|
48
|
+
publishedAt: string | null;
|
|
49
|
+
engagement: number | null;
|
|
50
|
+
score: number;
|
|
51
|
+
matchedTerms: string[];
|
|
52
|
+
metadata?: Record<string, unknown>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface SentimentSourceCoverage {
|
|
56
|
+
sources: string[];
|
|
57
|
+
counts: Record<string, number>;
|
|
58
|
+
missingSources?: string[];
|
|
59
|
+
notes?: string[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface SentimentInsight {
|
|
63
|
+
label: string;
|
|
64
|
+
score: number;
|
|
65
|
+
sampleSize: number;
|
|
66
|
+
scoredSampleSize: number;
|
|
67
|
+
confidence: SentimentInsightConfidence;
|
|
68
|
+
positiveDrivers: SentimentInsightDriver[];
|
|
69
|
+
negativeDrivers: SentimentInsightDriver[];
|
|
70
|
+
mixedDrivers: SentimentInsightDriver[];
|
|
71
|
+
notableClaims: string[];
|
|
72
|
+
representativeItems: SentimentRepresentativeItem[];
|
|
73
|
+
sourceCoverage: SentimentSourceCoverage;
|
|
74
|
+
caveats: string[];
|
|
75
|
+
method: SentimentInsightMethod;
|
|
76
|
+
}
|
|
77
|
+
|
|
22
78
|
export interface TwitterSentimentResult {
|
|
23
79
|
query: string;
|
|
24
80
|
tweetCount: number;
|
|
@@ -28,6 +84,7 @@ export interface TwitterSentimentResult {
|
|
|
28
84
|
bearishCount: number;
|
|
29
85
|
topMentions: string[];
|
|
30
86
|
fetchedAt: string;
|
|
87
|
+
insight?: SentimentInsight;
|
|
31
88
|
}
|
|
32
89
|
|
|
33
90
|
export interface WebSearchResult {
|
|
@@ -67,4 +124,6 @@ export interface RedditSentimentResult {
|
|
|
67
124
|
bullishCount: number;
|
|
68
125
|
bearishCount: number;
|
|
69
126
|
fetchedAt: string;
|
|
127
|
+
insight?: SentimentInsight;
|
|
128
|
+
records?: SentinelRecord[];
|
|
70
129
|
}
|
package/dist/infra/browser.d.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared stealth browser infrastructure using Camoufox (anti-detection Firefox).
|
|
3
|
-
* Provides a singleton browser instance that any tool can use for scraping.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* import { StealthBrowser } from "../infra/browser.js";
|
|
7
|
-
* const data = await StealthBrowser.fetchJson<MyType>(url);
|
|
8
|
-
* const result = await StealthBrowser.evaluate(url, () => document.title);
|
|
9
|
-
*/
|
|
10
|
-
import "./node-version.js";
|
|
11
|
-
import type { Page } from "playwright-core";
|
|
12
|
-
export declare const StealthBrowser: {
|
|
13
|
-
/**
|
|
14
|
-
* Navigate to a URL, run a JS function in the page context, and return the result.
|
|
15
|
-
*/
|
|
16
|
-
evaluate<T>(url: string, fn: () => T | Promise<T>): Promise<T>;
|
|
17
|
-
/**
|
|
18
|
-
* Fetch JSON from a URL using the browser's session (cookies, TLS fingerprint).
|
|
19
|
-
* Useful for APIs that block Node.js fetch but allow real browsers.
|
|
20
|
-
*/
|
|
21
|
-
fetchJson<T>(url: string): Promise<T>;
|
|
22
|
-
/**
|
|
23
|
-
* Run a custom async function in the browser page context.
|
|
24
|
-
* The page must already be on a relevant domain for cookies to work.
|
|
25
|
-
*/
|
|
26
|
-
run<T>(fn: (page: Page) => Promise<T>): Promise<T>;
|
|
27
|
-
/**
|
|
28
|
-
* Navigate to a URL and establish session cookies for that domain.
|
|
29
|
-
*/
|
|
30
|
-
initSession(url: string): Promise<void>;
|
|
31
|
-
/**
|
|
32
|
-
* Close the browser. It will be re-launched on next use.
|
|
33
|
-
*/
|
|
34
|
-
close(): Promise<void>;
|
|
35
|
-
};
|
package/dist/infra/browser.js
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared stealth browser infrastructure using Camoufox (anti-detection Firefox).
|
|
3
|
-
* Provides a singleton browser instance that any tool can use for scraping.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* import { StealthBrowser } from "../infra/browser.js";
|
|
7
|
-
* const data = await StealthBrowser.fetchJson<MyType>(url);
|
|
8
|
-
* const result = await StealthBrowser.evaluate(url, () => document.title);
|
|
9
|
-
*/
|
|
10
|
-
import "./node-version.js";
|
|
11
|
-
import { Camoufox } from "camoufox-js";
|
|
12
|
-
let browser = null;
|
|
13
|
-
let page = null;
|
|
14
|
-
let launching = null;
|
|
15
|
-
let pageQueue = Promise.resolve();
|
|
16
|
-
async function ensureBrowser() {
|
|
17
|
-
if (page && browser?.isConnected())
|
|
18
|
-
return page;
|
|
19
|
-
// Prevent concurrent launches
|
|
20
|
-
if (launching) {
|
|
21
|
-
await launching;
|
|
22
|
-
if (page && browser?.isConnected())
|
|
23
|
-
return page;
|
|
24
|
-
}
|
|
25
|
-
launching = (async () => {
|
|
26
|
-
const b = await Camoufox({ headless: true });
|
|
27
|
-
browser = b;
|
|
28
|
-
page = await b.newPage();
|
|
29
|
-
})();
|
|
30
|
-
await launching;
|
|
31
|
-
launching = null;
|
|
32
|
-
return page;
|
|
33
|
-
}
|
|
34
|
-
async function withPage(fn) {
|
|
35
|
-
let resolve;
|
|
36
|
-
const next = new Promise((r) => {
|
|
37
|
-
resolve = r;
|
|
38
|
-
});
|
|
39
|
-
const prev = pageQueue;
|
|
40
|
-
pageQueue = next;
|
|
41
|
-
await prev;
|
|
42
|
-
try {
|
|
43
|
-
const p = await ensureBrowser();
|
|
44
|
-
return await fn(p);
|
|
45
|
-
}
|
|
46
|
-
finally {
|
|
47
|
-
resolve();
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
export const StealthBrowser = {
|
|
51
|
-
/**
|
|
52
|
-
* Navigate to a URL, run a JS function in the page context, and return the result.
|
|
53
|
-
*/
|
|
54
|
-
async evaluate(url, fn) {
|
|
55
|
-
return withPage(async (p) => {
|
|
56
|
-
await p.goto(url, { waitUntil: "domcontentloaded", timeout: 15000 });
|
|
57
|
-
return p.evaluate(fn);
|
|
58
|
-
});
|
|
59
|
-
},
|
|
60
|
-
/**
|
|
61
|
-
* Fetch JSON from a URL using the browser's session (cookies, TLS fingerprint).
|
|
62
|
-
* Useful for APIs that block Node.js fetch but allow real browsers.
|
|
63
|
-
*/
|
|
64
|
-
async fetchJson(url) {
|
|
65
|
-
return withPage(async (p) => {
|
|
66
|
-
const result = await p.evaluate(async (fetchUrl) => {
|
|
67
|
-
const res = await fetch(fetchUrl, { credentials: "include" });
|
|
68
|
-
if (!res.ok)
|
|
69
|
-
throw new Error(`HTTP ${res.status}`);
|
|
70
|
-
return res.json();
|
|
71
|
-
}, url);
|
|
72
|
-
return result;
|
|
73
|
-
});
|
|
74
|
-
},
|
|
75
|
-
/**
|
|
76
|
-
* Run a custom async function in the browser page context.
|
|
77
|
-
* The page must already be on a relevant domain for cookies to work.
|
|
78
|
-
*/
|
|
79
|
-
async run(fn) {
|
|
80
|
-
return withPage(async (p) => fn(p));
|
|
81
|
-
},
|
|
82
|
-
/**
|
|
83
|
-
* Navigate to a URL and establish session cookies for that domain.
|
|
84
|
-
*/
|
|
85
|
-
async initSession(url) {
|
|
86
|
-
return withPage(async (p) => {
|
|
87
|
-
await p.goto(url, { waitUntil: "domcontentloaded", timeout: 15000 });
|
|
88
|
-
});
|
|
89
|
-
},
|
|
90
|
-
/**
|
|
91
|
-
* Close the browser. It will be re-launched on next use.
|
|
92
|
-
*/
|
|
93
|
-
async close() {
|
|
94
|
-
if (browser) {
|
|
95
|
-
await browser.close().catch(() => { });
|
|
96
|
-
browser = null;
|
|
97
|
-
page = null;
|
|
98
|
-
}
|
|
99
|
-
},
|
|
100
|
-
};
|
|
101
|
-
// Clean up on process exit
|
|
102
|
-
process.on("exit", () => {
|
|
103
|
-
browser?.close().catch(() => { });
|
|
104
|
-
});
|
|
105
|
-
//# sourceMappingURL=browser.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"browser.js","sourceRoot":"","sources":["../../src/infra/browser.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
interface TwitterLoginResult {
|
|
3
|
-
success: boolean;
|
|
4
|
-
message: string;
|
|
5
|
-
}
|
|
6
|
-
export declare function runTwitterLogin(notify: (msg: string) => void): Promise<TwitterLoginResult>;
|
|
7
|
-
export declare function registerTwitterLoginTool(pi: ExtensionAPI): void;
|
|
8
|
-
export {};
|