@jellyos/agent 0.1.4 → 0.1.6

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 (90) hide show
  1. package/README.npm.md +212 -0
  2. package/bin/jellyos-mcp +26 -0
  3. package/dist/api/ExtensionAPI.d.ts +11 -0
  4. package/dist/cli.js +127 -49
  5. package/dist/index.d.ts +15 -2
  6. package/dist/index.js +13 -3
  7. package/dist/loader.d.ts +2 -9
  8. package/dist/loader.js +2 -1
  9. package/dist/mcp/entry.d.ts +2 -0
  10. package/dist/mcp/entry.js +71 -0
  11. package/dist/mcp/server.d.ts +31 -0
  12. package/dist/mcp/server.js +128 -0
  13. package/dist/models/ModelRegistry.d.ts +12 -1
  14. package/dist/models/ModelRegistry.js +105 -9
  15. package/dist/runner/AgentRunner.d.ts +19 -2
  16. package/dist/runner/AgentRunner.js +247 -17
  17. package/dist/runner/ModelClient.d.ts +10 -1
  18. package/dist/runner/ModelClient.js +79 -6
  19. package/dist/runner/SwarmRouter.d.ts +6 -6
  20. package/dist/runner/SwarmRouter.js +73 -24
  21. package/dist/runner/ToolDispatcher.d.ts +10 -0
  22. package/dist/runner/ToolDispatcher.js +106 -2
  23. package/dist/scheduler/AgentScheduler.d.ts +118 -0
  24. package/dist/scheduler/AgentScheduler.js +253 -0
  25. package/dist/session/ContextStore.d.ts +96 -0
  26. package/dist/session/ContextStore.js +207 -0
  27. package/dist/session/GoalManager.d.ts +101 -0
  28. package/dist/session/GoalManager.js +167 -0
  29. package/dist/session/MemoryStore.d.ts +48 -0
  30. package/dist/session/MemoryStore.js +166 -0
  31. package/dist/session/SessionManager.d.ts +45 -4
  32. package/dist/session/SessionManager.js +151 -8
  33. package/dist/telemetry/Tracer.d.ts +48 -0
  34. package/dist/telemetry/Tracer.js +102 -0
  35. package/dist/tools/MarketSentiment.d.ts +166 -0
  36. package/dist/tools/MarketSentiment.js +209 -0
  37. package/dist/tools/NewsSentiment.js +40 -13
  38. package/dist/tools/PriceFeed.d.ts +2 -0
  39. package/dist/tools/PriceFeed.js +79 -27
  40. package/dist/tools/TechnicalAnalysis.d.ts +37 -0
  41. package/dist/tools/TechnicalAnalysis.js +85 -0
  42. package/dist/tui/App.d.ts +4 -3
  43. package/dist/tui/App.js +346 -119
  44. package/dist/tui/ModelSelector.d.ts +22 -0
  45. package/dist/tui/ModelSelector.js +86 -0
  46. package/dist/tui/REPL.d.ts +2 -1
  47. package/dist/tui/REPL.js +11 -6
  48. package/package.json +10 -6
  49. package/dist/api/ExtensionAPI.d.ts.map +0 -1
  50. package/dist/api/ExtensionAPI.js.map +0 -1
  51. package/dist/api/Registry.d.ts.map +0 -1
  52. package/dist/api/Registry.js.map +0 -1
  53. package/dist/cli.d.ts.map +0 -1
  54. package/dist/cli.js.map +0 -1
  55. package/dist/index.d.ts.map +0 -1
  56. package/dist/index.js.map +0 -1
  57. package/dist/loader.d.ts.map +0 -1
  58. package/dist/loader.js.map +0 -1
  59. package/dist/models/CostTracker.d.ts.map +0 -1
  60. package/dist/models/CostTracker.js.map +0 -1
  61. package/dist/models/ModelRegistry.d.ts.map +0 -1
  62. package/dist/models/ModelRegistry.js.map +0 -1
  63. package/dist/models/index.d.ts.map +0 -1
  64. package/dist/models/index.js.map +0 -1
  65. package/dist/runner/AgentRunner.d.ts.map +0 -1
  66. package/dist/runner/AgentRunner.js.map +0 -1
  67. package/dist/runner/ModelClient.d.ts.map +0 -1
  68. package/dist/runner/ModelClient.js.map +0 -1
  69. package/dist/runner/SwarmRouter.d.ts.map +0 -1
  70. package/dist/runner/SwarmRouter.js.map +0 -1
  71. package/dist/runner/ToolDispatcher.d.ts.map +0 -1
  72. package/dist/runner/ToolDispatcher.js.map +0 -1
  73. package/dist/session/SessionManager.d.ts.map +0 -1
  74. package/dist/session/SessionManager.js.map +0 -1
  75. package/dist/tools/NewsSentiment.d.ts.map +0 -1
  76. package/dist/tools/NewsSentiment.js.map +0 -1
  77. package/dist/tools/PriceFeed.d.ts.map +0 -1
  78. package/dist/tools/PriceFeed.js.map +0 -1
  79. package/dist/tools/TechnicalAnalysis.d.ts.map +0 -1
  80. package/dist/tools/TechnicalAnalysis.js.map +0 -1
  81. package/dist/tools/index.d.ts.map +0 -1
  82. package/dist/tools/index.js.map +0 -1
  83. package/dist/tui/App.d.ts.map +0 -1
  84. package/dist/tui/App.js.map +0 -1
  85. package/dist/tui/REPL.d.ts.map +0 -1
  86. package/dist/tui/REPL.js.map +0 -1
  87. package/dist/tui/StatusBar.d.ts.map +0 -1
  88. package/dist/tui/StatusBar.js.map +0 -1
  89. package/dist/tui/theme.d.ts.map +0 -1
  90. package/dist/tui/theme.js.map +0 -1
@@ -0,0 +1,166 @@
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 Static } from "@sinclair/typebox";
12
+ export declare const fearGreedParams: import("@sinclair/typebox").TObject<{
13
+ days: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
14
+ }>;
15
+ export declare function getFearGreedTool(_id: string, params: Static<typeof fearGreedParams>): Promise<{
16
+ content: {
17
+ type: "text";
18
+ text: string;
19
+ }[];
20
+ details: {
21
+ current: number;
22
+ classification: string;
23
+ avg7: number;
24
+ trend: number[];
25
+ direction: string;
26
+ };
27
+ } | {
28
+ content: {
29
+ type: "text";
30
+ text: string;
31
+ }[];
32
+ details: {
33
+ current?: undefined;
34
+ classification?: undefined;
35
+ avg7?: undefined;
36
+ trend?: undefined;
37
+ direction?: undefined;
38
+ };
39
+ }>;
40
+ export declare const fundingRatesParams: import("@sinclair/typebox").TObject<{
41
+ symbol: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
42
+ limit: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
43
+ }>;
44
+ export declare function getFundingRatesTool(_id: string, params: Static<typeof fundingRatesParams>): Promise<{
45
+ content: {
46
+ type: "text";
47
+ text: string;
48
+ }[];
49
+ details: {
50
+ symbol: string;
51
+ currentRate: number;
52
+ annualizedPct: number;
53
+ sentiment: string;
54
+ history: {
55
+ fundingRate: string;
56
+ fundingTime: number;
57
+ }[];
58
+ };
59
+ } | {
60
+ content: {
61
+ type: "text";
62
+ text: string;
63
+ }[];
64
+ details: {
65
+ symbol?: undefined;
66
+ currentRate?: undefined;
67
+ annualizedPct?: undefined;
68
+ sentiment?: undefined;
69
+ history?: undefined;
70
+ };
71
+ }>;
72
+ export declare const btcMempoolParams: import("@sinclair/typebox").TObject<{}>;
73
+ export declare function getBtcMempoolTool(): Promise<{
74
+ content: {
75
+ type: "text";
76
+ text: string;
77
+ }[];
78
+ details: {
79
+ pendingTxs: number;
80
+ vsizeMB: number;
81
+ fees: {
82
+ fastestFee: number;
83
+ halfHourFee: number;
84
+ hourFee: number;
85
+ economyFee: number;
86
+ };
87
+ };
88
+ } | {
89
+ content: {
90
+ type: "text";
91
+ text: string;
92
+ }[];
93
+ details: {
94
+ pendingTxs?: undefined;
95
+ vsizeMB?: undefined;
96
+ fees?: undefined;
97
+ };
98
+ }>;
99
+ export declare const defiTvlParams: import("@sinclair/typebox").TObject<{
100
+ chain: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
101
+ }>;
102
+ export declare function getDefiTvlTool(_id: string, params: Static<typeof defiTvlParams>): Promise<{
103
+ content: {
104
+ type: "text";
105
+ text: string;
106
+ }[];
107
+ details: {
108
+ chain: string;
109
+ tvl: number;
110
+ change7d: string | null;
111
+ chains?: undefined;
112
+ totalTvl?: undefined;
113
+ };
114
+ } | {
115
+ content: {
116
+ type: "text";
117
+ text: string;
118
+ }[];
119
+ details: {
120
+ chains: {
121
+ name: string;
122
+ tvl: number;
123
+ }[];
124
+ totalTvl: number;
125
+ chain?: undefined;
126
+ tvl?: undefined;
127
+ change7d?: undefined;
128
+ };
129
+ } | {
130
+ content: {
131
+ type: "text";
132
+ text: string;
133
+ }[];
134
+ details: {
135
+ chain?: undefined;
136
+ tvl?: undefined;
137
+ change7d?: undefined;
138
+ chains?: undefined;
139
+ totalTvl?: undefined;
140
+ };
141
+ }>;
142
+ export declare const solanaStatsParams: import("@sinclair/typebox").TObject<{}>;
143
+ export declare function getSolanaStatsTool(): Promise<{
144
+ content: {
145
+ type: "text";
146
+ text: string;
147
+ }[];
148
+ details: {
149
+ avgTps: number;
150
+ maxTps: number;
151
+ latestSlot: number;
152
+ samples: number[];
153
+ };
154
+ } | {
155
+ content: {
156
+ type: "text";
157
+ text: string;
158
+ }[];
159
+ details: {
160
+ avgTps?: undefined;
161
+ maxTps?: undefined;
162
+ latestSlot?: undefined;
163
+ samples?: undefined;
164
+ };
165
+ }>;
166
+ //# sourceMappingURL=MarketSentiment.d.ts.map
@@ -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": "JellyOS/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": "JellyOS/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": "JellyOS/1.0" } }),
97
+ fetch("https://mempool.space/api/v1/fees/recommended", { signal: AbortSignal.timeout(8_000), headers: { "User-Agent": "JellyOS/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": "JellyOS/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": "JellyOS/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
@@ -41,34 +41,61 @@ export function scoreSentiment(text) {
41
41
  return hits === 0 ? 0 : Math.max(-1, Math.min(1, score / hits));
42
42
  }
43
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
+ }
44
68
  async function fetchRSS(url, source) {
45
69
  try {
46
70
  const res = await fetch(url, {
47
71
  signal: AbortSignal.timeout(10_000),
48
- headers: { "User-Agent": "JellyOS/1.0", "Accept": "application/rss+xml, application/xml, text/xml" },
72
+ headers: { "User-Agent": "JellyOS/1.0", "Accept": "application/rss+xml, application/xml, text/xml, */*" },
49
73
  });
50
74
  if (!res.ok)
51
75
  return [];
52
- const text = await res.text();
53
- // Parse RSS items with regex (no XML parser dependency)
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;
54
79
  const items = [];
55
- const itemMatches = text.match(/<item[\s\S]*?<\/item>/gi) ?? [];
56
- for (const item of itemMatches) {
57
- const titleMatch = item.match(/<title[^>]*><!\[CDATA\[(.*?)\]\]><\/title>|<title[^>]*>(.*?)<\/title>/i);
58
- const linkMatch = item.match(/<link[^>]*>(.*?)<\/link>/i);
59
- const dateMatch = item.match(/<pubDate[^>]*>(.*?)<\/pubDate>/i);
60
- const title = titleMatch?.[1] ?? titleMatch?.[2] ?? "";
61
- if (!title.trim())
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)
62
87
  continue;
63
88
  items.push({
64
89
  title,
65
90
  source,
66
- url: linkMatch?.[1] ?? "",
67
- published: dateMatch ? new Date(dateMatch[1]).getTime() : Date.now(),
91
+ url: link,
92
+ published: pubDate ? (new Date(pubDate).getTime() || Date.now()) : Date.now(),
68
93
  sentiment: scoreSentiment(title),
69
94
  });
95
+ if (items.length >= 25)
96
+ break; // cap per feed
70
97
  }
71
- return items.slice(0, 20);
98
+ return items;
72
99
  }
73
100
  catch {
74
101
  return [];
@@ -43,6 +43,8 @@ export declare class PriceFeed extends EventEmitter {
43
43
  /** Add symbols to track. IDs are auto-resolved from the symbol map. */
44
44
  track(...symbols: string[]): void;
45
45
  fetchAll(): Promise<void>;
46
+ private fetchBinance;
47
+ private fetchCoinGecko;
46
48
  get(symbol: string): PriceTick | undefined;
47
49
  getMultiple(symbols: string[]): PriceTick[];
48
50
  getAll(): PriceTick[];
@@ -81,37 +81,89 @@ export class PriceFeed extends EventEmitter {
81
81
  }
82
82
  // ── Fetch ────────────────────────────────────────────────────────────────
83
83
  async fetchAll() {
84
+ // #17: Fan-out to Binance (primary) + CoinGecko (secondary)
85
+ // Binance: no rate limits on public endpoints, real-time order book prices
86
+ // CoinGecko: covers long-tail tokens Binance doesn't list, includes market cap
87
+ const [binanceResult, cgResult] = await Promise.allSettled([
88
+ this.fetchBinance(),
89
+ this.fetchCoinGecko(),
90
+ ]);
91
+ let updated = false;
92
+ // Binance wins for price precision on listed pairs
93
+ if (binanceResult.status === "fulfilled") {
94
+ for (const tick of binanceResult.value) {
95
+ this.prices.set(tick.symbol.toLowerCase(), tick);
96
+ updated = true;
97
+ }
98
+ }
99
+ // CoinGecko fills gaps (market cap data, unlisted tokens)
100
+ if (cgResult.status === "fulfilled") {
101
+ for (const tick of cgResult.value) {
102
+ const key = tick.symbol.toLowerCase();
103
+ const existing = this.prices.get(key);
104
+ if (!existing) {
105
+ // Token not on Binance — add from CoinGecko
106
+ this.prices.set(key, tick);
107
+ updated = true;
108
+ }
109
+ else if (tick.marketCap > 0) {
110
+ // Enrich Binance tick with market cap from CoinGecko
111
+ existing.marketCap = tick.marketCap;
112
+ }
113
+ }
114
+ }
115
+ if (updated)
116
+ this.emit("prices", [...this.prices.values()]);
117
+ }
118
+ async fetchBinance() {
119
+ // Build list of USDT pairs for tracked symbols
120
+ const pairs = [...this.tracking]
121
+ .map(s => s.toUpperCase() + "USDT")
122
+ .filter(s => !s.startsWith("USDT") && !s.startsWith("DAI")); // skip stablecoins
123
+ if (!pairs.length)
124
+ return [];
125
+ const url = `https://api.binance.com/api/v3/ticker/24hr?symbols=${encodeURIComponent(JSON.stringify(pairs))}`;
126
+ const res = await fetch(url, {
127
+ signal: AbortSignal.timeout(8_000),
128
+ headers: { "User-Agent": "JellyOS/1.0" },
129
+ });
130
+ if (!res.ok)
131
+ throw new Error(`Binance ticker HTTP ${res.status}`);
132
+ const data = await res.json();
133
+ return data.map(d => ({
134
+ symbol: d.symbol.replace(/USDT$/, ""),
135
+ price: parseFloat(d.lastPrice),
136
+ change24h: parseFloat(d.priceChangePercent),
137
+ volume24h: parseFloat(d.quoteVolume),
138
+ marketCap: 0, // Binance doesn't provide market cap — CoinGecko fills this
139
+ high24h: parseFloat(d.highPrice),
140
+ low24h: parseFloat(d.lowPrice),
141
+ source: "binance",
142
+ timestamp: Date.now(),
143
+ }));
144
+ }
145
+ async fetchCoinGecko() {
84
146
  const ids = [...this.tracking]
85
147
  .map(s => SYMBOL_TO_ID[s])
86
- .filter(Boolean)
148
+ .filter((id) => Boolean(id))
87
149
  .join(",");
88
150
  if (!ids)
89
- return;
90
- try {
91
- const res = await fetch(`https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=${ids}&order=market_cap_desc&sparkline=false`, { signal: AbortSignal.timeout(10_000), headers: { "User-Agent": "JellyOS/1.0" } });
92
- if (!res.ok)
93
- return;
94
- const data = (await res.json());
95
- for (const coin of data) {
96
- const symbol = coin.symbol.toUpperCase();
97
- const tick = {
98
- symbol,
99
- price: coin.current_price,
100
- change24h: coin.price_change_percentage_24h ?? 0,
101
- volume24h: coin.total_volume ?? 0,
102
- marketCap: coin.market_cap ?? 0,
103
- high24h: coin.high_24h ?? 0,
104
- low24h: coin.low_24h ?? 0,
105
- source: "coingecko",
106
- timestamp: Date.now(),
107
- };
108
- this.prices.set(symbol.toLowerCase(), tick);
109
- }
110
- this.emit("prices", [...this.prices.values()]);
111
- }
112
- catch {
113
- // Silent — will retry next interval
114
- }
151
+ return [];
152
+ const res = await fetch(`https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=${ids}&order=market_cap_desc&sparkline=false`, { signal: AbortSignal.timeout(10_000), headers: { "User-Agent": "JellyOS/1.0" } });
153
+ if (!res.ok)
154
+ throw new Error(`CoinGecko HTTP ${res.status}`);
155
+ const data = await res.json();
156
+ return data.map(coin => ({
157
+ symbol: coin.symbol.toUpperCase(),
158
+ price: coin.current_price,
159
+ change24h: coin.price_change_percentage_24h ?? 0,
160
+ volume24h: coin.total_volume ?? 0,
161
+ marketCap: coin.market_cap ?? 0,
162
+ high24h: coin.high_24h ?? 0,
163
+ low24h: coin.low_24h ?? 0,
164
+ source: "coingecko",
165
+ timestamp: Date.now(),
166
+ }));
115
167
  }
116
168
  // ── Query ─────────────────────────────────────────────────────────────────
117
169
  get(symbol) {
@@ -5,6 +5,7 @@
5
5
  * All calculations are pure math on price arrays — no external API needed.
6
6
  * Designed to be exposed as tools the AI can call autonomously.
7
7
  */
8
+ import { type Static } from "@sinclair/typebox";
8
9
  export interface OHLCV {
9
10
  timestamp: number;
10
11
  open: number;
@@ -70,4 +71,40 @@ export declare const macdParams: import("@sinclair/typebox").TObject<{
70
71
  slow: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
71
72
  signal: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
72
73
  }>;
74
+ export declare const getCandlesParams: import("@sinclair/typebox").TObject<{
75
+ symbol: import("@sinclair/typebox").TString;
76
+ interval: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
77
+ limit: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
78
+ analyze: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
79
+ }>;
80
+ export declare function getCandlesTool(_id: string, params: Static<typeof getCandlesParams>): Promise<{
81
+ content: {
82
+ type: "text";
83
+ text: string;
84
+ }[];
85
+ details: {
86
+ symbol?: undefined;
87
+ interval?: undefined;
88
+ candleCount?: undefined;
89
+ latest?: undefined;
90
+ priceChangePercent?: undefined;
91
+ };
92
+ } | {
93
+ content: {
94
+ type: "text";
95
+ text: string;
96
+ }[];
97
+ details: {
98
+ symbol: string;
99
+ interval: string;
100
+ candleCount: number;
101
+ latest: {
102
+ close: number;
103
+ high: number;
104
+ low: number;
105
+ volume: number;
106
+ };
107
+ priceChangePercent: number;
108
+ };
109
+ }>;
73
110
  //# sourceMappingURL=TechnicalAnalysis.d.ts.map