@jellyos/agent 0.1.4 → 0.1.5

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 (94) hide show
  1. package/README.npm.md +212 -0
  2. package/bin/jellyos-mcp +26 -0
  3. package/dist/api/ExtensionAPI.d.ts +6 -0
  4. package/dist/cli.js +114 -48
  5. package/dist/index.d.ts +15 -2
  6. package/dist/index.js +13 -3
  7. package/dist/mcp/entry.d.ts +2 -0
  8. package/dist/mcp/entry.js +71 -0
  9. package/dist/mcp/server.d.ts +31 -0
  10. package/dist/mcp/server.js +128 -0
  11. package/dist/models/ModelRegistry.d.ts +12 -1
  12. package/dist/models/ModelRegistry.js +105 -9
  13. package/dist/runner/AgentRunner.d.ts +19 -2
  14. package/dist/runner/AgentRunner.js +247 -17
  15. package/dist/runner/ModelClient.d.ts +10 -1
  16. package/dist/runner/ModelClient.js +79 -6
  17. package/dist/runner/SwarmRouter.d.ts +6 -6
  18. package/dist/runner/SwarmRouter.js +73 -24
  19. package/dist/runner/ToolDispatcher.d.ts +10 -0
  20. package/dist/runner/ToolDispatcher.js +106 -2
  21. package/dist/scheduler/AgentScheduler.d.ts +118 -0
  22. package/dist/scheduler/AgentScheduler.js +253 -0
  23. package/dist/session/ContextStore.d.ts +96 -0
  24. package/dist/session/ContextStore.js +207 -0
  25. package/dist/session/GoalManager.d.ts +101 -0
  26. package/dist/session/GoalManager.js +167 -0
  27. package/dist/session/MemoryStore.d.ts +48 -0
  28. package/dist/session/MemoryStore.js +166 -0
  29. package/dist/session/SessionManager.d.ts +45 -4
  30. package/dist/session/SessionManager.js +151 -8
  31. package/dist/telemetry/Tracer.d.ts +48 -0
  32. package/dist/telemetry/Tracer.js +102 -0
  33. package/dist/tests/ContextStore.test.d.ts +2 -0
  34. package/dist/tests/ContextStore.test.js +74 -0
  35. package/dist/tests/ModelRegistry.test.d.ts +2 -0
  36. package/dist/tests/ModelRegistry.test.js +69 -0
  37. package/dist/tests/SessionManager.test.d.ts +2 -0
  38. package/dist/tests/SessionManager.test.js +108 -0
  39. package/dist/tests/TechnicalAnalysis.test.d.ts +2 -0
  40. package/dist/tests/TechnicalAnalysis.test.js +109 -0
  41. package/dist/tools/MarketSentiment.d.ts +166 -0
  42. package/dist/tools/MarketSentiment.js +209 -0
  43. package/dist/tools/NewsSentiment.js +40 -13
  44. package/dist/tools/PriceFeed.d.ts +2 -0
  45. package/dist/tools/PriceFeed.js +79 -27
  46. package/dist/tools/TechnicalAnalysis.d.ts +37 -0
  47. package/dist/tools/TechnicalAnalysis.js +85 -0
  48. package/dist/tui/App.d.ts +2 -2
  49. package/dist/tui/App.js +280 -117
  50. package/dist/tui/REPL.d.ts +2 -1
  51. package/dist/tui/REPL.js +11 -6
  52. package/package.json +9 -4
  53. package/dist/api/ExtensionAPI.d.ts.map +0 -1
  54. package/dist/api/ExtensionAPI.js.map +0 -1
  55. package/dist/api/Registry.d.ts.map +0 -1
  56. package/dist/api/Registry.js.map +0 -1
  57. package/dist/cli.d.ts.map +0 -1
  58. package/dist/cli.js.map +0 -1
  59. package/dist/index.d.ts.map +0 -1
  60. package/dist/index.js.map +0 -1
  61. package/dist/loader.d.ts.map +0 -1
  62. package/dist/loader.js.map +0 -1
  63. package/dist/models/CostTracker.d.ts.map +0 -1
  64. package/dist/models/CostTracker.js.map +0 -1
  65. package/dist/models/ModelRegistry.d.ts.map +0 -1
  66. package/dist/models/ModelRegistry.js.map +0 -1
  67. package/dist/models/index.d.ts.map +0 -1
  68. package/dist/models/index.js.map +0 -1
  69. package/dist/runner/AgentRunner.d.ts.map +0 -1
  70. package/dist/runner/AgentRunner.js.map +0 -1
  71. package/dist/runner/ModelClient.d.ts.map +0 -1
  72. package/dist/runner/ModelClient.js.map +0 -1
  73. package/dist/runner/SwarmRouter.d.ts.map +0 -1
  74. package/dist/runner/SwarmRouter.js.map +0 -1
  75. package/dist/runner/ToolDispatcher.d.ts.map +0 -1
  76. package/dist/runner/ToolDispatcher.js.map +0 -1
  77. package/dist/session/SessionManager.d.ts.map +0 -1
  78. package/dist/session/SessionManager.js.map +0 -1
  79. package/dist/tools/NewsSentiment.d.ts.map +0 -1
  80. package/dist/tools/NewsSentiment.js.map +0 -1
  81. package/dist/tools/PriceFeed.d.ts.map +0 -1
  82. package/dist/tools/PriceFeed.js.map +0 -1
  83. package/dist/tools/TechnicalAnalysis.d.ts.map +0 -1
  84. package/dist/tools/TechnicalAnalysis.js.map +0 -1
  85. package/dist/tools/index.d.ts.map +0 -1
  86. package/dist/tools/index.js.map +0 -1
  87. package/dist/tui/App.d.ts.map +0 -1
  88. package/dist/tui/App.js.map +0 -1
  89. package/dist/tui/REPL.d.ts.map +0 -1
  90. package/dist/tui/REPL.js.map +0 -1
  91. package/dist/tui/StatusBar.d.ts.map +0 -1
  92. package/dist/tui/StatusBar.js.map +0 -1
  93. package/dist/tui/theme.d.ts.map +0 -1
  94. package/dist/tui/theme.js.map +0 -1
@@ -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
@@ -269,4 +269,89 @@ export const macdParams = Type.Object({
269
269
  slow: Type.Optional(Type.Number({ default: 26 })),
270
270
  signal: Type.Optional(Type.Number({ default: 9 })),
271
271
  });
272
+ // ── Tool: get_candles (#18 — fetch real OHLCV data from Binance) ─────────────
273
+ export const getCandlesParams = Type.Object({
274
+ symbol: Type.String({ description: "Crypto symbol e.g. BTC, ETH, SOL" }),
275
+ interval: Type.Optional(Type.String({ description: "Candle interval: 1m 5m 15m 1h 4h 1d (default: 1h)" })),
276
+ limit: Type.Optional(Type.Number({ description: "Number of candles 1-500 (default: 100)" })),
277
+ analyze: Type.Optional(Type.Boolean({ description: "Run full TA analysis on fetched candles (default: true)" })),
278
+ });
279
+ export async function getCandlesTool(_id, params) {
280
+ const symbol = params.symbol.toUpperCase().replace(/USDT$/, "") + "USDT";
281
+ const interval = params.interval ?? "1h";
282
+ const limit = Math.min(Math.max(params.limit ?? 100, 10), 500);
283
+ const analyze = params.analyze !== false;
284
+ const VALID_INTERVALS = new Set(["1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "8h", "12h", "1d", "3d", "1w", "1M"]);
285
+ if (!VALID_INTERVALS.has(interval)) {
286
+ return {
287
+ content: [{ type: "text", text: `Invalid interval "${interval}". Valid: ${[...VALID_INTERVALS].join(", ")}` }],
288
+ details: {},
289
+ };
290
+ }
291
+ try {
292
+ const res = await fetch(`https://api.binance.com/api/v3/klines?symbol=${symbol}&interval=${interval}&limit=${limit}`, { signal: AbortSignal.timeout(12_000), headers: { "User-Agent": "JellyOS/1.0" } });
293
+ if (!res.ok) {
294
+ return {
295
+ content: [{ type: "text", text: `Binance API error ${res.status} for ${symbol}. Check symbol.` }],
296
+ details: {},
297
+ };
298
+ }
299
+ const raw = await res.json();
300
+ const candles = raw.map(c => ({
301
+ timestamp: c[0],
302
+ open: parseFloat(c[1]),
303
+ high: parseFloat(c[2]),
304
+ low: parseFloat(c[3]),
305
+ close: parseFloat(c[4]),
306
+ volume: parseFloat(c[5]),
307
+ }));
308
+ const latest = candles[candles.length - 1];
309
+ const oldest = candles[0];
310
+ const chgPct = ((latest.close - oldest.close) / oldest.close * 100).toFixed(2);
311
+ const priceStr = latest.close < 1
312
+ ? `$${latest.close.toFixed(6)}`
313
+ : `$${latest.close.toLocaleString(undefined, { maximumFractionDigits: 2 })}`;
314
+ const lines = [
315
+ `${symbol} | ${interval} | ${candles.length} candles`,
316
+ `Close: ${priceStr} Period change: ${parseFloat(chgPct) >= 0 ? "+" : ""}${chgPct}%`,
317
+ `High: $${latest.high.toLocaleString()} Low: $${latest.low.toLocaleString()}`,
318
+ `Volume (last bar): ${latest.volume.toLocaleString()}`,
319
+ ];
320
+ if (analyze && candles.length >= 30) {
321
+ const results = fullAnalysis(candles);
322
+ const summary = results.find(r => r.indicator === "SUMMARY");
323
+ lines.push("", "── Technical Analysis ──");
324
+ for (const r of results) {
325
+ if (r.indicator === "SUMMARY")
326
+ continue;
327
+ const icon = r.signal === "bullish" ? "🟢" : r.signal === "bearish" ? "🔴" : "⚪";
328
+ const val = typeof r.value === "number" ? r.value.toFixed(2) : String(r.value);
329
+ const meta = r.metadata?.condition ? ` (${String(r.metadata.condition)})` : "";
330
+ lines.push(` ${icon} ${r.indicator.padEnd(16)} ${val}${meta}`);
331
+ }
332
+ if (summary) {
333
+ const icon = summary.signal === "bullish" ? "🟢" : summary.signal === "bearish" ? "🔴" : "⚪";
334
+ lines.push("", `${icon} OVERALL: ${String(summary.signal).toUpperCase()} — ${String(summary.metadata?.bullish_indicators ?? 0)} bull / ${String(summary.metadata?.bearish_indicators ?? 0)} bear signals`);
335
+ }
336
+ }
337
+ else if (analyze && candles.length < 30) {
338
+ lines.push(`(need 30+ candles for TA, got ${candles.length} — increase limit)`);
339
+ }
340
+ return {
341
+ content: [{ type: "text", text: lines.join("\n") }],
342
+ details: {
343
+ symbol, interval, candleCount: candles.length,
344
+ latest: { close: latest.close, high: latest.high, low: latest.low, volume: latest.volume },
345
+ priceChangePercent: parseFloat(chgPct),
346
+ },
347
+ };
348
+ }
349
+ catch (e) {
350
+ const msg = e instanceof Error ? e.message : String(e);
351
+ return {
352
+ content: [{ type: "text", text: `Failed to fetch candles for ${symbol}: ${msg}` }],
353
+ details: {},
354
+ };
355
+ }
356
+ }
272
357
  //# sourceMappingURL=TechnicalAnalysis.js.map
package/dist/tui/App.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * App — root Ink component.
3
- * Wires StatusBar + REPL + AgentRunner, registers built-in tools,
4
- * starts background feeds, and renders a multi-pane trading terminal.
3
+ * Multi-pane TUI with context bar, syntax-highlighted tool output,
4
+ * live side panel with ticker/prices, and command palette triggered via /palette.
5
5
  */
6
6
  import { Registry } from "../api/Registry.js";
7
7
  import type { ModelRegistry } from "../models/ModelRegistry.js";