@albalink/agent 1.0.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 (67) hide show
  1. package/README.md +91 -0
  2. package/bin/albacli +31 -0
  3. package/dist/api/ExtensionAPI.d.ts +103 -0
  4. package/dist/api/ExtensionAPI.js +15 -0
  5. package/dist/api/Registry.d.ts +54 -0
  6. package/dist/api/Registry.js +103 -0
  7. package/dist/cli.d.ts +6 -0
  8. package/dist/cli.js +230 -0
  9. package/dist/index.d.ts +41 -0
  10. package/dist/index.js +41 -0
  11. package/dist/loader.d.ts +16 -0
  12. package/dist/loader.js +89 -0
  13. package/dist/mcp/entry.d.ts +2 -0
  14. package/dist/mcp/entry.js +71 -0
  15. package/dist/mcp/server.d.ts +31 -0
  16. package/dist/mcp/server.js +128 -0
  17. package/dist/models/CostTracker.d.ts +67 -0
  18. package/dist/models/CostTracker.js +151 -0
  19. package/dist/models/ModelRegistry.d.ts +159 -0
  20. package/dist/models/ModelRegistry.js +500 -0
  21. package/dist/models/index.d.ts +5 -0
  22. package/dist/models/index.js +3 -0
  23. package/dist/runner/AgentRunner.d.ts +88 -0
  24. package/dist/runner/AgentRunner.js +473 -0
  25. package/dist/runner/ModelClient.d.ts +97 -0
  26. package/dist/runner/ModelClient.js +350 -0
  27. package/dist/runner/SwarmRouter.d.ts +64 -0
  28. package/dist/runner/SwarmRouter.js +216 -0
  29. package/dist/runner/ToolDispatcher.d.ts +29 -0
  30. package/dist/runner/ToolDispatcher.js +168 -0
  31. package/dist/scheduler/AgentScheduler.d.ts +119 -0
  32. package/dist/scheduler/AgentScheduler.js +263 -0
  33. package/dist/server-entry.d.ts +11 -0
  34. package/dist/server-entry.js +15 -0
  35. package/dist/session/ContextStore.d.ts +96 -0
  36. package/dist/session/ContextStore.js +207 -0
  37. package/dist/session/GoalManager.d.ts +101 -0
  38. package/dist/session/GoalManager.js +167 -0
  39. package/dist/session/MemoryStore.d.ts +48 -0
  40. package/dist/session/MemoryStore.js +167 -0
  41. package/dist/session/SessionManager.d.ts +64 -0
  42. package/dist/session/SessionManager.js +193 -0
  43. package/dist/telemetry/Tracer.d.ts +48 -0
  44. package/dist/telemetry/Tracer.js +102 -0
  45. package/dist/tools/MarketSentiment.d.ts +166 -0
  46. package/dist/tools/MarketSentiment.js +209 -0
  47. package/dist/tools/NewsSentiment.d.ts +67 -0
  48. package/dist/tools/NewsSentiment.js +226 -0
  49. package/dist/tools/PriceFeed.d.ts +105 -0
  50. package/dist/tools/PriceFeed.js +282 -0
  51. package/dist/tools/TechnicalAnalysis.d.ts +110 -0
  52. package/dist/tools/TechnicalAnalysis.js +357 -0
  53. package/dist/tools/index.d.ts +7 -0
  54. package/dist/tools/index.js +4 -0
  55. package/dist/tui/App.d.ts +17 -0
  56. package/dist/tui/App.js +225 -0
  57. package/dist/tui/ModelSelector.d.ts +12 -0
  58. package/dist/tui/ModelSelector.js +87 -0
  59. package/dist/tui/REPL.d.ts +24 -0
  60. package/dist/tui/REPL.js +51 -0
  61. package/dist/tui/StatusBar.d.ts +14 -0
  62. package/dist/tui/StatusBar.js +14 -0
  63. package/dist/tui/theme.d.ts +30 -0
  64. package/dist/tui/theme.js +41 -0
  65. package/dist/util/safeLog.d.ts +3 -0
  66. package/dist/util/safeLog.js +21 -0
  67. package/package.json +67 -0
@@ -0,0 +1,209 @@
1
+ /**
2
+ * MarketSentiment — free, no-key market data tools.
3
+ *
4
+ * #20: Fear & Greed Index (alternative.me)
5
+ * #19: BTC Mempool stats (mempool.space — free, no key)
6
+ * #19: ETH / EVM Gas prices (blocknative public — free)
7
+ * #19: Solana TPS (public RPC — no key)
8
+ * #19: DeFi TVL by chain (DeFiLlama — free, no key)
9
+ * #20: Binance perp funding rates (public endpoint — no key)
10
+ */
11
+ import { Type } from "@sinclair/typebox";
12
+ // ── Fear & Greed Index ────────────────────────────────────────────────────────
13
+ export const fearGreedParams = Type.Object({
14
+ days: Type.Optional(Type.Number({ description: "Number of historical days to return (default: 7, max: 30)" })),
15
+ });
16
+ export async function getFearGreedTool(_id, params) {
17
+ const days = Math.min(params.days ?? 7, 30);
18
+ try {
19
+ const res = await fetch(`https://api.alternative.me/fng/?limit=${days}`, { signal: AbortSignal.timeout(8_000), headers: { "User-Agent": "MIO/1.0" } });
20
+ if (!res.ok)
21
+ throw new Error(`HTTP ${res.status}`);
22
+ const data = await res.json();
23
+ const items = data.data ?? [];
24
+ const current = items[0];
25
+ if (!current)
26
+ throw new Error("No data returned");
27
+ const val = parseInt(current.value);
28
+ const emoji = val >= 75 ? "😱 Extreme Greed" : val >= 55 ? "😀 Greed"
29
+ : val >= 45 ? "😐 Neutral" : val >= 25 ? "😨 Fear"
30
+ : "😱 Extreme Fear";
31
+ const trend = items.slice(0, 7).map(d => parseInt(d.value));
32
+ const avg7 = Math.round(trend.reduce((a, b) => a + b, 0) / trend.length);
33
+ const direction = trend.length > 1
34
+ ? (trend[0] > trend[trend.length - 1] ? "↑ rising" : trend[0] < trend[trend.length - 1] ? "↓ falling" : "→ flat")
35
+ : "";
36
+ const historyLine = items.slice(0, Math.min(days, 7))
37
+ .map(d => `${d.value}(${d.value_classification.slice(0, 4)})`)
38
+ .join(" → ");
39
+ const text = [
40
+ `Fear & Greed Index: ${val} — ${emoji}`,
41
+ `7-day avg: ${avg7} Trend: ${direction}`,
42
+ `History: ${historyLine}`,
43
+ ].join("\n");
44
+ return {
45
+ content: [{ type: "text", text }],
46
+ details: { current: val, classification: current.value_classification, avg7, trend, direction },
47
+ };
48
+ }
49
+ catch (e) {
50
+ const msg = e instanceof Error ? e.message : String(e);
51
+ return { content: [{ type: "text", text: `Fear & Greed fetch failed: ${msg}` }], details: {} };
52
+ }
53
+ }
54
+ // ── Binance Perp Funding Rates ────────────────────────────────────────────────
55
+ export const fundingRatesParams = Type.Object({
56
+ symbol: Type.Optional(Type.String({ description: "Symbol e.g. BTC, ETH (default: BTC)" })),
57
+ limit: Type.Optional(Type.Number({ description: "Number of historical funding periods (default: 8)" })),
58
+ });
59
+ export async function getFundingRatesTool(_id, params) {
60
+ const sym = (params.symbol?.toUpperCase() ?? "BTC").replace(/USDT$/, "") + "USDT";
61
+ const limit = Math.min(params.limit ?? 8, 100);
62
+ try {
63
+ const res = await fetch(`https://fapi.binance.com/fapi/v1/fundingRate?symbol=${sym}&limit=${limit}`, { signal: AbortSignal.timeout(8_000), headers: { "User-Agent": "MIO/1.0" } });
64
+ if (!res.ok)
65
+ throw new Error(`HTTP ${res.status}`);
66
+ const data = await res.json();
67
+ if (!data.length)
68
+ throw new Error("No data");
69
+ const current = data[data.length - 1];
70
+ const rate = parseFloat(current.fundingRate);
71
+ const annRate = rate * 3 * 365 * 100; // 3x daily → annual %
72
+ const sentiment = rate > 0.001 ? "Longs paying shorts (bullish bias)" :
73
+ rate < -0.001 ? "Shorts paying longs (bearish bias)" : "Neutral";
74
+ const history = data.map(d => (parseFloat(d.fundingRate) * 100).toFixed(4) + "%").join(", ");
75
+ const text = [
76
+ `${sym} Funding Rate: ${(rate * 100).toFixed(4)}% per 8h`,
77
+ `Annualized: ${annRate.toFixed(1)}%`,
78
+ `Sentiment: ${sentiment}`,
79
+ `History (${data.length} periods): ${history}`,
80
+ ].join("\n");
81
+ return {
82
+ content: [{ type: "text", text }],
83
+ details: { symbol: sym, currentRate: rate, annualizedPct: annRate, sentiment, history: data },
84
+ };
85
+ }
86
+ catch (e) {
87
+ const msg = e instanceof Error ? e.message : String(e);
88
+ return { content: [{ type: "text", text: `Funding rate fetch failed for ${sym}: ${msg}` }], details: {} };
89
+ }
90
+ }
91
+ // ── BTC Mempool Stats ─────────────────────────────────────────────────────────
92
+ export const btcMempoolParams = Type.Object({});
93
+ export async function getBtcMempoolTool() {
94
+ try {
95
+ const [statsRes, feesRes] = await Promise.all([
96
+ fetch("https://mempool.space/api/mempool", { signal: AbortSignal.timeout(8_000), headers: { "User-Agent": "MIO/1.0" } }),
97
+ fetch("https://mempool.space/api/v1/fees/recommended", { signal: AbortSignal.timeout(8_000), headers: { "User-Agent": "MIO/1.0" } }),
98
+ ]);
99
+ if (!statsRes.ok || !feesRes.ok)
100
+ throw new Error("mempool.space API error");
101
+ const stats = await statsRes.json();
102
+ const fees = await feesRes.json();
103
+ const congestion = stats.count > 100_000 ? "🔴 HIGH" : stats.count > 50_000 ? "🟡 MEDIUM" : "🟢 LOW";
104
+ const text = [
105
+ `BTC Mempool`,
106
+ `Pending txs: ${stats.count.toLocaleString()} Size: ${(stats.vsize / 1_000_000).toFixed(1)} MB`,
107
+ `Congestion: ${congestion}`,
108
+ `Fee rates (sat/vB):`,
109
+ ` Next block: ${fees.fastestFee}`,
110
+ ` ~30 min: ${fees.halfHourFee}`,
111
+ ` ~1 hour: ${fees.hourFee}`,
112
+ ` Economy: ${fees.economyFee}`,
113
+ ].join("\n");
114
+ return {
115
+ content: [{ type: "text", text }],
116
+ details: { pendingTxs: stats.count, vsizeMB: stats.vsize / 1_000_000, fees },
117
+ };
118
+ }
119
+ catch (e) {
120
+ const msg = e instanceof Error ? e.message : String(e);
121
+ return { content: [{ type: "text", text: `BTC mempool fetch failed: ${msg}` }], details: {} };
122
+ }
123
+ }
124
+ // ── DeFi TVL ─────────────────────────────────────────────────────────────────
125
+ export const defiTvlParams = Type.Object({
126
+ chain: Type.Optional(Type.String({ description: "Chain name e.g. ethereum, bsc, solana, arbitrum (omit for all chains)" })),
127
+ });
128
+ export async function getDefiTvlTool(_id, params) {
129
+ try {
130
+ const url = params.chain
131
+ ? `https://api.llama.fi/v2/historicalChainTvl/${encodeURIComponent(params.chain)}`
132
+ : "https://api.llama.fi/v2/chains";
133
+ const res = await fetch(url, { signal: AbortSignal.timeout(10_000), headers: { "User-Agent": "MIO/1.0" } });
134
+ if (!res.ok)
135
+ throw new Error(`HTTP ${res.status}`);
136
+ const data = await res.json();
137
+ if (params.chain) {
138
+ // Historical TVL for one chain
139
+ const arr = data;
140
+ if (!arr.length)
141
+ throw new Error("No data for chain");
142
+ const latest = arr[arr.length - 1];
143
+ const prev7 = arr[Math.max(0, arr.length - 8)];
144
+ const chg7 = prev7 ? ((latest.tvl - prev7.tvl) / prev7.tvl * 100).toFixed(1) : null;
145
+ const text = [
146
+ `DeFi TVL — ${params.chain}`,
147
+ `Current TVL: $${(latest.tvl / 1e9).toFixed(2)}B`,
148
+ chg7 ? `7-day change: ${parseFloat(chg7) >= 0 ? "+" : ""}${chg7}%` : "",
149
+ `As of: ${new Date(latest.date * 1000).toLocaleDateString()}`,
150
+ ].filter(Boolean).join("\n");
151
+ return { content: [{ type: "text", text }], details: { chain: params.chain, tvl: latest.tvl, change7d: chg7 } };
152
+ }
153
+ else {
154
+ // All chains ranked by TVL
155
+ const chains = data
156
+ .filter(c => c.tvl > 100_000_000)
157
+ .sort((a, b) => b.tvl - a.tvl)
158
+ .slice(0, 15);
159
+ const totalTvl = chains.reduce((s, c) => s + c.tvl, 0);
160
+ const rows = chains.map((c, i) => ` ${String(i + 1).padStart(2)}. ${c.name.padEnd(16)} $${(c.tvl / 1e9).toFixed(2)}B`);
161
+ const text = [
162
+ `DeFi TVL by Chain (top 15)`,
163
+ `Total tracked: $${(totalTvl / 1e9).toFixed(1)}B`,
164
+ ...rows,
165
+ ].join("\n");
166
+ return { content: [{ type: "text", text }], details: { chains, totalTvl } };
167
+ }
168
+ }
169
+ catch (e) {
170
+ const msg = e instanceof Error ? e.message : String(e);
171
+ return { content: [{ type: "text", text: `DeFi TVL fetch failed: ${msg}` }], details: {} };
172
+ }
173
+ }
174
+ // ── Solana Network Stats ──────────────────────────────────────────────────────
175
+ export const solanaStatsParams = Type.Object({});
176
+ export async function getSolanaStatsTool() {
177
+ try {
178
+ const res = await fetch("https://api.mainnet-beta.solana.com", {
179
+ method: "POST",
180
+ headers: { "Content-Type": "application/json", "User-Agent": "MIO/1.0" },
181
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "getRecentPerformanceSamples", params: [5] }),
182
+ signal: AbortSignal.timeout(10_000),
183
+ });
184
+ if (!res.ok)
185
+ throw new Error(`HTTP ${res.status}`);
186
+ const body = await res.json();
187
+ const samples = body.result ?? [];
188
+ if (!samples.length)
189
+ throw new Error("No samples");
190
+ const tpsArr = samples.map(s => Math.round(s.numTransactions / s.samplePeriodSecs));
191
+ const avgTps = Math.round(tpsArr.reduce((a, b) => a + b, 0) / tpsArr.length);
192
+ const maxTps = Math.max(...tpsArr);
193
+ const latestSlot = samples[0]?.slot ?? 0;
194
+ const health = avgTps > 3000 ? "🟢 Healthy" : avgTps > 1500 ? "🟡 Moderate" : "🔴 Degraded";
195
+ const text = [
196
+ `Solana Network Stats`,
197
+ `TPS (avg 5 samples): ${avgTps.toLocaleString()}`,
198
+ `TPS (peak sample): ${maxTps.toLocaleString()}`,
199
+ `Latest slot: ${latestSlot.toLocaleString()}`,
200
+ `Network health: ${health}`,
201
+ ].join("\n");
202
+ return { content: [{ type: "text", text }], details: { avgTps, maxTps, latestSlot, samples: tpsArr } };
203
+ }
204
+ catch (e) {
205
+ const msg = e instanceof Error ? e.message : String(e);
206
+ return { content: [{ type: "text", text: `Solana stats fetch failed: ${msg}` }], details: {} };
207
+ }
208
+ }
209
+ //# sourceMappingURL=MarketSentiment.js.map
@@ -0,0 +1,67 @@
1
+ /**
2
+ * NewsSentiment — fetches RSS feeds, runs headline sentiment scoring
3
+ * with a cheap model, and surfaces trending narratives.
4
+ */
5
+ import { type Static } from "@sinclair/typebox";
6
+ export interface NewsItem {
7
+ title: string;
8
+ source: string;
9
+ url: string;
10
+ published: number;
11
+ sentiment?: number;
12
+ }
13
+ export interface SentimentReport {
14
+ items: NewsItem[];
15
+ avgSentiment: number;
16
+ positive: number;
17
+ negative: number;
18
+ neutral: number;
19
+ updatedAt: number;
20
+ topKeywords: string[];
21
+ }
22
+ export declare function scoreSentiment(text: string): number;
23
+ export declare class NewsFeed {
24
+ private lastReport;
25
+ private pollInterval;
26
+ private timer?;
27
+ constructor(pollIntervalMs?: number);
28
+ start(): void;
29
+ stop(): void;
30
+ fetch(): Promise<void>;
31
+ getLatest(): SentimentReport | null;
32
+ summary(): string;
33
+ /** Compact sentiment badge for status bar. */
34
+ statusBadge(): string;
35
+ }
36
+ export declare const newsFeed: NewsFeed;
37
+ export declare const getNewsParams: import("@sinclair/typebox").TObject<{
38
+ limit: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
39
+ }>;
40
+ export declare function getNewsTool(_id: string, params: Static<typeof getNewsParams>): Promise<{
41
+ content: {
42
+ type: "text";
43
+ text: string;
44
+ }[];
45
+ details: {
46
+ avgSentiment?: undefined;
47
+ positive?: undefined;
48
+ negative?: undefined;
49
+ neutral?: undefined;
50
+ articleCount?: undefined;
51
+ topKeywords?: undefined;
52
+ };
53
+ } | {
54
+ content: {
55
+ type: "text";
56
+ text: string;
57
+ }[];
58
+ details: {
59
+ avgSentiment: number;
60
+ positive: number;
61
+ negative: number;
62
+ neutral: number;
63
+ articleCount: number;
64
+ topKeywords: string[];
65
+ };
66
+ }>;
67
+ //# sourceMappingURL=NewsSentiment.d.ts.map
@@ -0,0 +1,226 @@
1
+ /**
2
+ * NewsSentiment — fetches RSS feeds, runs headline sentiment scoring
3
+ * with a cheap model, and surfaces trending narratives.
4
+ */
5
+ import { Type } from "@sinclair/typebox";
6
+ // ── RSS sources ───────────────────────────────────────────────────────────────
7
+ const FEEDS = [
8
+ { name: "CoinDesk", url: "https://www.coindesk.com/arc/outboundfeeds/rss/" },
9
+ { name: "CoinTelegraph", url: "https://cointelegraph.com/rss" },
10
+ { name: "The Block", url: "https://www.theblock.co/rss" },
11
+ ];
12
+ // ── Simple keyword-based sentiment (no model required) ────────────────────────
13
+ const BULLISH_WORDS = [
14
+ "surge", "soar", "rally", "bull", "breakout", "moon", "pump", "record high",
15
+ "all-time high", "ath", "accumulate", "institutional", "adoption", "approval",
16
+ "etf approved", "launch", "partnership", "upgrade", "mainnet", "listing",
17
+ "airdrop", "halving", "buyback", "burn", "yield", "staking",
18
+ ];
19
+ const BEARISH_WORDS = [
20
+ "crash", "plunge", "dump", "bear", "downturn", "correction", "liquidate",
21
+ "hack", "exploit", "rug", "scam", "sec", "lawsuit", "regulation", "ban",
22
+ "crackdown", "fine", "penalty", "delist", "suspend", "halt", "freeze",
23
+ "insolvent", "bankrupt", "depeg", "exploit",
24
+ ];
25
+ export function scoreSentiment(text) {
26
+ const lower = text.toLowerCase();
27
+ let score = 0;
28
+ let hits = 0;
29
+ for (const w of BULLISH_WORDS) {
30
+ if (lower.includes(w)) {
31
+ score++;
32
+ hits++;
33
+ }
34
+ }
35
+ for (const w of BEARISH_WORDS) {
36
+ if (lower.includes(w)) {
37
+ score--;
38
+ hits++;
39
+ }
40
+ }
41
+ return hits === 0 ? 0 : Math.max(-1, Math.min(1, score / hits));
42
+ }
43
+ // ── Fetch & parse ────────────────────────────────────────────────────────────
44
+ /**
45
+ * Proper RSS item extractor — handles CDATA, encoded entities, attributes.
46
+ * No regex on the whole document; scans block by block.
47
+ */
48
+ function extractTag(block, tag) {
49
+ // Try CDATA form first: <tag><![CDATA[...]]></tag>
50
+ const cdataRe = new RegExp(`<${tag}[^>]*><!\\[CDATA\\[([\\s\\S]*?)\\]\\]>`, "i");
51
+ const cdataM = block.match(cdataRe);
52
+ if (cdataM?.[1])
53
+ return cdataM[1].trim();
54
+ // Plain text form: <tag ...>content</tag>
55
+ const plainRe = new RegExp(`<${tag}[^>]*>([^<]*)`, "i");
56
+ const plainM = block.match(plainRe);
57
+ if (plainM?.[1]) {
58
+ return plainM[1]
59
+ .replace(/&amp;/g, "&")
60
+ .replace(/&lt;/g, "<")
61
+ .replace(/&gt;/g, ">")
62
+ .replace(/&quot;/g, '"')
63
+ .replace(/&#39;/g, "'")
64
+ .trim();
65
+ }
66
+ return "";
67
+ }
68
+ async function fetchRSS(url, source) {
69
+ try {
70
+ const res = await fetch(url, {
71
+ signal: AbortSignal.timeout(10_000),
72
+ headers: { "User-Agent": "MIO/1.0", "Accept": "application/rss+xml, application/xml, text/xml, */*" },
73
+ });
74
+ if (!res.ok)
75
+ return [];
76
+ const xml = await res.text();
77
+ // Find all <item>...</item> blocks (also handles <entry> for Atom feeds)
78
+ const TAG_RE = /<(?:item|entry)[^>]*>([\s\S]*?)<\/(?:item|entry)>/gi;
79
+ const items = [];
80
+ let match;
81
+ while ((match = TAG_RE.exec(xml)) !== null) {
82
+ const block = match[1] ?? "";
83
+ const title = extractTag(block, "title");
84
+ const link = extractTag(block, "link") || extractTag(block, "guid");
85
+ const pubDate = extractTag(block, "pubDate") || extractTag(block, "updated") || extractTag(block, "published");
86
+ if (!title)
87
+ continue;
88
+ items.push({
89
+ title,
90
+ source,
91
+ url: link,
92
+ published: pubDate ? (new Date(pubDate).getTime() || Date.now()) : Date.now(),
93
+ sentiment: scoreSentiment(title),
94
+ });
95
+ if (items.length >= 25)
96
+ break; // cap per feed
97
+ }
98
+ return items;
99
+ }
100
+ catch {
101
+ return [];
102
+ }
103
+ }
104
+ // ── Keyword extraction ────────────────────────────────────────────────────────
105
+ function extractKeywords(items, topN = 10) {
106
+ const wordFreq = new Map();
107
+ const stopWords = new Set(["the", "a", "an", "in", "on", "at", "to", "for", "of", "and", "is", "are", "was", "were", "will", "has", "have", "it", "its", "with", "from", "by", "as", "be", "that", "this", "but", "or", "not", "can", "may"]);
108
+ for (const item of items) {
109
+ const words = item.title.toLowerCase().replace(/[^a-z0-9 ]/g, "").split(/\s+/);
110
+ for (const w of words) {
111
+ if (w.length < 3 || stopWords.has(w))
112
+ continue;
113
+ wordFreq.set(w, (wordFreq.get(w) ?? 0) + 1);
114
+ }
115
+ }
116
+ return [...wordFreq.entries()]
117
+ .sort((a, b) => b[1] - a[1])
118
+ .slice(0, topN)
119
+ .map(([w]) => w);
120
+ }
121
+ // ── NewsFeed class ────────────────────────────────────────────────────────────
122
+ export class NewsFeed {
123
+ lastReport = null;
124
+ pollInterval;
125
+ timer;
126
+ constructor(pollIntervalMs = 600_000) {
127
+ this.pollInterval = pollIntervalMs;
128
+ }
129
+ start() {
130
+ this.fetch();
131
+ this.timer = setInterval(() => this.fetch(), this.pollInterval);
132
+ }
133
+ stop() {
134
+ if (this.timer) {
135
+ clearInterval(this.timer);
136
+ this.timer = undefined;
137
+ }
138
+ }
139
+ async fetch() {
140
+ const allItems = [];
141
+ for (const feed of FEEDS) {
142
+ const items = await fetchRSS(feed.url, feed.name);
143
+ allItems.push(...items);
144
+ }
145
+ allItems.sort((a, b) => b.published - a.published);
146
+ const scored = allItems.filter(i => i.sentiment !== undefined && i.sentiment !== 0);
147
+ const avgSent = scored.length > 0 ? scored.reduce((s, i) => s + (i.sentiment ?? 0), 0) / scored.length : 0;
148
+ const positive = allItems.filter(i => (i.sentiment ?? 0) > 0.1).length;
149
+ const negative = allItems.filter(i => (i.sentiment ?? 0) < -0.1).length;
150
+ const neutral = allItems.length - positive - negative;
151
+ const topKeywords = extractKeywords(allItems);
152
+ this.lastReport = {
153
+ items: allItems,
154
+ avgSentiment: Math.round(avgSent * 1000) / 1000,
155
+ positive,
156
+ negative,
157
+ neutral,
158
+ updatedAt: Date.now(),
159
+ topKeywords,
160
+ };
161
+ }
162
+ getLatest() {
163
+ return this.lastReport;
164
+ }
165
+ summary() {
166
+ if (!this.lastReport)
167
+ return "No news data available.";
168
+ const r = this.lastReport;
169
+ const score = r.avgSentiment;
170
+ const mood = score > 0.2 ? "🟢 Bullish" : score < -0.2 ? "🔴 Bearish" : "🟡 Neutral";
171
+ return [
172
+ `News Sentiment: ${mood} (score: ${score >= 0 ? "+" : ""}${score.toFixed(3)})`,
173
+ ` ${r.positive} positive · ${r.negative} negative · ${r.neutral} neutral (${r.items.length} articles)`,
174
+ ` Trending: ${r.topKeywords.slice(0, 8).join(", ")}`,
175
+ ` Latest:`,
176
+ ...r.items.slice(0, 5).map(i => {
177
+ const s = (i.sentiment ?? 0) >= 0.1 ? "🟢" : (i.sentiment ?? 0) <= -0.1 ? "🔴" : " ";
178
+ return ` ${s} [${i.source}] ${i.title.slice(0, 80)}`;
179
+ }),
180
+ ].join("\n");
181
+ }
182
+ /** Compact sentiment badge for status bar. */
183
+ statusBadge() {
184
+ if (!this.lastReport)
185
+ return "📰 ?";
186
+ const s = this.lastReport.avgSentiment;
187
+ const emoji = s > 0.3 ? "🟢" : s > 0 ? "🟡" : "🔴";
188
+ return `📰 ${emoji} ${(s * 100).toFixed(0)}%`;
189
+ }
190
+ }
191
+ // ── Singleton ─────────────────────────────────────────────────────────────────
192
+ export const newsFeed = new NewsFeed();
193
+ // ── Tool: get_news ────────────────────────────────────────────────────────────
194
+ export const getNewsParams = Type.Object({
195
+ limit: Type.Optional(Type.Number({ default: 10, description: "Number of articles to show" })),
196
+ });
197
+ export async function getNewsTool(_id, params) {
198
+ const report = newsFeed.getLatest();
199
+ if (!report) {
200
+ return {
201
+ content: [{ type: "text", text: "News data not yet available. Please wait for the first fetch." }],
202
+ details: {},
203
+ };
204
+ }
205
+ const items = report.items.slice(0, params.limit ?? 10)
206
+ .map(i => {
207
+ const s = (i.sentiment ?? 0) >= 0.1 ? "+" : (i.sentiment ?? 0) <= -0.1 ? "-" : " ";
208
+ return `${s} [${i.source}] ${i.title}`;
209
+ })
210
+ .join("\n");
211
+ return {
212
+ content: [{
213
+ type: "text",
214
+ text: `News Sentiment: ${report.avgSentiment >= 0 ? "+" : ""}${(report.avgSentiment * 100).toFixed(0)}% sentiment · ${report.positive}p/${report.negative}n/${report.neutral}·\nTrending: ${report.topKeywords.join(", ")}\n\n${items}`,
215
+ }],
216
+ details: {
217
+ avgSentiment: report.avgSentiment,
218
+ positive: report.positive,
219
+ negative: report.negative,
220
+ neutral: report.neutral,
221
+ articleCount: report.items.length,
222
+ topKeywords: report.topKeywords,
223
+ },
224
+ };
225
+ }
226
+ //# sourceMappingURL=NewsSentiment.js.map
@@ -0,0 +1,105 @@
1
+ /**
2
+ * PriceFeed — real-time price aggregation from CoinGecko and Binance.
3
+ *
4
+ * Provides:
5
+ * - Background polling with configurable intervals
6
+ * - Multi-source aggregation (prioritises Binance for precision, CG for breadth)
7
+ * - 24h change, volume, and market cap tracking
8
+ * - In-memory price cache for tool lookups
9
+ * - Exposed as tools for the AI to query
10
+ */
11
+ import { type Static } from "@sinclair/typebox";
12
+ import { EventEmitter } from "node:events";
13
+ export interface PriceTick {
14
+ symbol: string;
15
+ price: number;
16
+ change24h: number;
17
+ volume24h: number;
18
+ marketCap: number;
19
+ high24h: number;
20
+ low24h: number;
21
+ source: "coingecko" | "binance";
22
+ timestamp: number;
23
+ }
24
+ export interface CoinGeckoCoin {
25
+ id: string;
26
+ symbol: string;
27
+ name: string;
28
+ current_price: number;
29
+ price_change_percentage_24h: number;
30
+ market_cap: number;
31
+ total_volume: number;
32
+ high_24h: number;
33
+ low_24h: number;
34
+ }
35
+ export declare class PriceFeed extends EventEmitter {
36
+ private prices;
37
+ private pollInterval;
38
+ private timer?;
39
+ private tracking;
40
+ constructor(pollIntervalMs?: number);
41
+ start(): void;
42
+ stop(): void;
43
+ /** Add symbols to track. IDs are auto-resolved from the symbol map. */
44
+ track(...symbols: string[]): void;
45
+ fetchAll(): Promise<void>;
46
+ private fetchBinance;
47
+ private fetchCoinGecko;
48
+ get(symbol: string): PriceTick | undefined;
49
+ getMultiple(symbols: string[]): PriceTick[];
50
+ getAll(): PriceTick[];
51
+ getTopMovers(limit?: number): PriceTick[];
52
+ formatPrice(tick: PriceTick): string;
53
+ /** Compact ticker line for the status bar. */
54
+ tickerLine(maxSymbols?: number): string;
55
+ }
56
+ export declare const priceFeed: PriceFeed;
57
+ export declare const getPricesParams: import("@sinclair/typebox").TObject<{
58
+ symbols: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>;
59
+ }>;
60
+ export declare function getPricesTool(_id: string, params: Static<typeof getPricesParams>): Promise<{
61
+ content: {
62
+ type: "text";
63
+ text: string;
64
+ }[];
65
+ details: Record<string, unknown>;
66
+ }>;
67
+ export declare const topMoversParams: import("@sinclair/typebox").TObject<{
68
+ limit: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
69
+ }>;
70
+ export declare function topMoversTool(_id: string, params: Static<typeof topMoversParams>): Promise<{
71
+ content: {
72
+ type: "text";
73
+ text: string;
74
+ }[];
75
+ details: {
76
+ count: number;
77
+ };
78
+ }>;
79
+ export declare const marketOverviewParams: import("@sinclair/typebox").TObject<{}>;
80
+ export declare function marketOverviewTool(): Promise<{
81
+ content: {
82
+ type: "text";
83
+ text: string;
84
+ }[];
85
+ details: {
86
+ totalMarketCap?: undefined;
87
+ avgChange24h?: undefined;
88
+ gainers?: undefined;
89
+ losers?: undefined;
90
+ assets?: undefined;
91
+ };
92
+ } | {
93
+ content: {
94
+ type: "text";
95
+ text: string;
96
+ }[];
97
+ details: {
98
+ totalMarketCap: number;
99
+ avgChange24h: number;
100
+ gainers: number;
101
+ losers: number;
102
+ assets: number;
103
+ };
104
+ }>;
105
+ //# sourceMappingURL=PriceFeed.d.ts.map