prab-cli 1.2.1 → 1.2.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.
- package/dist/index.js +181 -2
- package/dist/lib/chat-handler.js +39 -5
- package/dist/lib/crypto/analyzer.js +275 -0
- package/dist/lib/crypto/chart-visual.js +548 -0
- package/dist/lib/crypto/data-fetcher.js +166 -0
- package/dist/lib/crypto/index.js +57 -0
- package/dist/lib/crypto/indicators.js +390 -0
- package/dist/lib/crypto/market-analyzer.js +497 -0
- package/dist/lib/crypto/market-scanner.js +569 -0
- package/dist/lib/crypto/news-fetcher.js +394 -0
- package/dist/lib/crypto/signal-generator.js +625 -0
- package/dist/lib/crypto/smc-analyzer.js +450 -0
- package/dist/lib/crypto/smc-indicators.js +512 -0
- package/dist/lib/crypto/whale-tracker.js +508 -0
- package/dist/lib/slash-commands.js +36 -0
- package/dist/lib/ui.js +45 -1
- package/package.json +1 -1
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Crypto News Fetcher
|
|
4
|
+
* Fetches latest cryptocurrency news from free APIs
|
|
5
|
+
*/
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
+
};
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.displayNews = displayNews;
|
|
11
|
+
exports.fetchCryptoNews = fetchCryptoNews;
|
|
12
|
+
exports.runCryptoNews = runCryptoNews;
|
|
13
|
+
/* global fetch */
|
|
14
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
15
|
+
const ora_1 = __importDefault(require("ora"));
|
|
16
|
+
// ============================================
|
|
17
|
+
// NEWS SOURCES
|
|
18
|
+
// ============================================
|
|
19
|
+
/**
|
|
20
|
+
* Fetch news from CryptoPanic (free public API)
|
|
21
|
+
*/
|
|
22
|
+
async function fetchCryptoPanicNews(filter) {
|
|
23
|
+
try {
|
|
24
|
+
// CryptoPanic free public feed
|
|
25
|
+
let url = "https://cryptopanic.com/api/free/v1/posts/?public=true";
|
|
26
|
+
if (filter) {
|
|
27
|
+
url += `¤cies=${filter.toUpperCase()}`;
|
|
28
|
+
}
|
|
29
|
+
const response = await fetch(url, {
|
|
30
|
+
headers: {
|
|
31
|
+
Accept: "application/json",
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
const data = await response.json();
|
|
38
|
+
if (!data.results || !Array.isArray(data.results)) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
return data.results.slice(0, 15).map((item) => ({
|
|
42
|
+
title: item.title || "No title",
|
|
43
|
+
description: item.title || "", // CryptoPanic doesn't always have description
|
|
44
|
+
url: item.url || "",
|
|
45
|
+
source: item.source?.title || "CryptoPanic",
|
|
46
|
+
publishedAt: item.published_at || new Date().toISOString(),
|
|
47
|
+
sentiment: item.votes ? determineSentiment(item.votes) : "neutral",
|
|
48
|
+
coins: item.currencies?.map((c) => c.code) || [],
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Fetch news from CoinGecko status updates (free, no API key)
|
|
57
|
+
*/
|
|
58
|
+
async function fetchCoinGeckoNews() {
|
|
59
|
+
try {
|
|
60
|
+
const response = await fetch("https://api.coingecko.com/api/v3/news", {
|
|
61
|
+
headers: {
|
|
62
|
+
Accept: "application/json",
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
const data = await response.json();
|
|
69
|
+
if (!data.data || !Array.isArray(data.data)) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
return data.data.slice(0, 10).map((item) => ({
|
|
73
|
+
title: item.title || "No title",
|
|
74
|
+
description: item.description || "",
|
|
75
|
+
url: item.url || "",
|
|
76
|
+
source: item.news_site || "CoinGecko",
|
|
77
|
+
publishedAt: item.updated_at || new Date().toISOString(),
|
|
78
|
+
sentiment: "neutral",
|
|
79
|
+
coins: [],
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Fetch from alternative free news API
|
|
88
|
+
*/
|
|
89
|
+
async function fetchAlternativeNews(query = "cryptocurrency") {
|
|
90
|
+
try {
|
|
91
|
+
// Using a free RSS-to-JSON service for crypto news
|
|
92
|
+
const feeds = [
|
|
93
|
+
`https://api.rss2json.com/v1/api.json?rss_url=https://cointelegraph.com/rss`,
|
|
94
|
+
`https://api.rss2json.com/v1/api.json?rss_url=https://coindesk.com/arc/outboundfeeds/rss/`,
|
|
95
|
+
];
|
|
96
|
+
const results = [];
|
|
97
|
+
for (const feedUrl of feeds) {
|
|
98
|
+
try {
|
|
99
|
+
const response = await fetch(feedUrl);
|
|
100
|
+
if (response.ok) {
|
|
101
|
+
const data = await response.json();
|
|
102
|
+
if (data.items && Array.isArray(data.items)) {
|
|
103
|
+
const newsItems = data.items.slice(0, 5).map((item) => ({
|
|
104
|
+
title: item.title || "No title",
|
|
105
|
+
description: stripHtml(item.description || ""),
|
|
106
|
+
url: item.link || "",
|
|
107
|
+
source: data.feed?.title || "Crypto News",
|
|
108
|
+
publishedAt: item.pubDate || new Date().toISOString(),
|
|
109
|
+
sentiment: "neutral",
|
|
110
|
+
coins: extractCoins(item.title + " " + item.description),
|
|
111
|
+
}));
|
|
112
|
+
results.push(...newsItems);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Skip failed feed
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return results;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// ============================================
|
|
127
|
+
// HELPER FUNCTIONS
|
|
128
|
+
// ============================================
|
|
129
|
+
function determineSentiment(votes) {
|
|
130
|
+
if (!votes)
|
|
131
|
+
return "neutral";
|
|
132
|
+
const positive = (votes.positive || 0) + (votes.liked || 0);
|
|
133
|
+
const negative = (votes.negative || 0) + (votes.disliked || 0);
|
|
134
|
+
if (positive > negative + 2)
|
|
135
|
+
return "positive";
|
|
136
|
+
if (negative > positive + 2)
|
|
137
|
+
return "negative";
|
|
138
|
+
return "neutral";
|
|
139
|
+
}
|
|
140
|
+
function stripHtml(html) {
|
|
141
|
+
return html
|
|
142
|
+
.replace(/<[^>]*>/g, "")
|
|
143
|
+
.replace(/ /g, " ")
|
|
144
|
+
.replace(/&/g, "&")
|
|
145
|
+
.replace(/</g, "<")
|
|
146
|
+
.replace(/>/g, ">")
|
|
147
|
+
.replace(/"/g, '"')
|
|
148
|
+
.replace(/'/g, "'")
|
|
149
|
+
.trim()
|
|
150
|
+
.slice(0, 200);
|
|
151
|
+
}
|
|
152
|
+
function extractCoins(text) {
|
|
153
|
+
const coins = [];
|
|
154
|
+
const coinPatterns = [
|
|
155
|
+
/\bBTC\b/gi,
|
|
156
|
+
/\bBitcoin\b/gi,
|
|
157
|
+
/\bETH\b/gi,
|
|
158
|
+
/\bEthereum\b/gi,
|
|
159
|
+
/\bSOL\b/gi,
|
|
160
|
+
/\bSolana\b/gi,
|
|
161
|
+
/\bXRP\b/gi,
|
|
162
|
+
/\bRipple\b/gi,
|
|
163
|
+
/\bADA\b/gi,
|
|
164
|
+
/\bCardano\b/gi,
|
|
165
|
+
/\bDOGE\b/gi,
|
|
166
|
+
/\bDogecoin\b/gi,
|
|
167
|
+
/\bBNB\b/gi,
|
|
168
|
+
/\bAVAX\b/gi,
|
|
169
|
+
/\bAvalanche\b/gi,
|
|
170
|
+
/\bDOT\b/gi,
|
|
171
|
+
/\bPolkadot\b/gi,
|
|
172
|
+
/\bLINK\b/gi,
|
|
173
|
+
/\bChainlink\b/gi,
|
|
174
|
+
/\bMATIC\b/gi,
|
|
175
|
+
/\bPolygon\b/gi,
|
|
176
|
+
/\bSHIB\b/gi,
|
|
177
|
+
/\bPEPE\b/gi,
|
|
178
|
+
];
|
|
179
|
+
const normalizeMap = {
|
|
180
|
+
bitcoin: "BTC",
|
|
181
|
+
btc: "BTC",
|
|
182
|
+
ethereum: "ETH",
|
|
183
|
+
eth: "ETH",
|
|
184
|
+
solana: "SOL",
|
|
185
|
+
sol: "SOL",
|
|
186
|
+
ripple: "XRP",
|
|
187
|
+
xrp: "XRP",
|
|
188
|
+
cardano: "ADA",
|
|
189
|
+
ada: "ADA",
|
|
190
|
+
dogecoin: "DOGE",
|
|
191
|
+
doge: "DOGE",
|
|
192
|
+
bnb: "BNB",
|
|
193
|
+
avalanche: "AVAX",
|
|
194
|
+
avax: "AVAX",
|
|
195
|
+
polkadot: "DOT",
|
|
196
|
+
dot: "DOT",
|
|
197
|
+
chainlink: "LINK",
|
|
198
|
+
link: "LINK",
|
|
199
|
+
polygon: "MATIC",
|
|
200
|
+
matic: "MATIC",
|
|
201
|
+
shib: "SHIB",
|
|
202
|
+
pepe: "PEPE",
|
|
203
|
+
};
|
|
204
|
+
for (const pattern of coinPatterns) {
|
|
205
|
+
const matches = text.match(pattern);
|
|
206
|
+
if (matches) {
|
|
207
|
+
for (const match of matches) {
|
|
208
|
+
const normalized = normalizeMap[match.toLowerCase()] || match.toUpperCase();
|
|
209
|
+
if (!coins.includes(normalized)) {
|
|
210
|
+
coins.push(normalized);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return coins;
|
|
216
|
+
}
|
|
217
|
+
function formatTimeAgo(dateStr) {
|
|
218
|
+
const date = new Date(dateStr);
|
|
219
|
+
const now = new Date();
|
|
220
|
+
const diffMs = now.getTime() - date.getTime();
|
|
221
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
222
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
223
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
224
|
+
if (diffMins < 1)
|
|
225
|
+
return "just now";
|
|
226
|
+
if (diffMins < 60)
|
|
227
|
+
return `${diffMins}m ago`;
|
|
228
|
+
if (diffHours < 24)
|
|
229
|
+
return `${diffHours}h ago`;
|
|
230
|
+
if (diffDays < 7)
|
|
231
|
+
return `${diffDays}d ago`;
|
|
232
|
+
return date.toLocaleDateString();
|
|
233
|
+
}
|
|
234
|
+
function getSentimentIcon(sentiment) {
|
|
235
|
+
switch (sentiment) {
|
|
236
|
+
case "positive":
|
|
237
|
+
return chalk_1.default.green("▲");
|
|
238
|
+
case "negative":
|
|
239
|
+
return chalk_1.default.red("▼");
|
|
240
|
+
default:
|
|
241
|
+
return chalk_1.default.gray("●");
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function getSentimentColor(sentiment) {
|
|
245
|
+
switch (sentiment) {
|
|
246
|
+
case "positive":
|
|
247
|
+
return chalk_1.default.green;
|
|
248
|
+
case "negative":
|
|
249
|
+
return chalk_1.default.red;
|
|
250
|
+
default:
|
|
251
|
+
return chalk_1.default.gray;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// ============================================
|
|
255
|
+
// DISPLAY FUNCTION
|
|
256
|
+
// ============================================
|
|
257
|
+
function displayNews(result, filter) {
|
|
258
|
+
console.log("");
|
|
259
|
+
console.log(chalk_1.default.bold.cyan(" ╔═══════════════════════════════════════════════════════════════════╗"));
|
|
260
|
+
console.log(chalk_1.default.bold.cyan(" ║ 📰 LATEST CRYPTO NEWS & UPDATES ║"));
|
|
261
|
+
console.log(chalk_1.default.bold.cyan(" ╚═══════════════════════════════════════════════════════════════════╝"));
|
|
262
|
+
console.log("");
|
|
263
|
+
if (filter) {
|
|
264
|
+
console.log(chalk_1.default.bgYellow.black(` 🔍 Filtered: ${filter.toUpperCase()} `));
|
|
265
|
+
console.log("");
|
|
266
|
+
}
|
|
267
|
+
console.log(chalk_1.default.gray(` 📊 ${result.news.length} news items | ${new Date(result.timestamp).toLocaleString()}`));
|
|
268
|
+
console.log("");
|
|
269
|
+
if (result.news.length === 0) {
|
|
270
|
+
console.log(chalk_1.default.yellow(" ⚠️ No news found. Try again later or with a different filter."));
|
|
271
|
+
console.log("");
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
for (let i = 0; i < result.news.length; i++) {
|
|
275
|
+
const news = result.news[i];
|
|
276
|
+
const timeAgo = formatTimeAgo(news.publishedAt);
|
|
277
|
+
const sentimentIcon = getSentimentIcon(news.sentiment || "neutral");
|
|
278
|
+
const sentimentColor = getSentimentColor(news.sentiment || "neutral");
|
|
279
|
+
// News box header
|
|
280
|
+
console.log(chalk_1.default.cyan(` ┌${"─".repeat(70)}┐`));
|
|
281
|
+
// News number and sentiment
|
|
282
|
+
const numBadge = chalk_1.default.bgCyan.black(` ${(i + 1).toString().padStart(2)} `);
|
|
283
|
+
console.log(` │ ${numBadge} ${sentimentIcon} ${chalk_1.default.bold.white(truncateText(news.title, 58))}`);
|
|
284
|
+
// Second line of title if needed
|
|
285
|
+
if (news.title.length > 58) {
|
|
286
|
+
console.log(` │ ${chalk_1.default.white(news.title.slice(55, 120))}`);
|
|
287
|
+
}
|
|
288
|
+
console.log(chalk_1.default.cyan(` ├${"─".repeat(70)}┤`));
|
|
289
|
+
// Key highlights
|
|
290
|
+
console.log(` │ ${chalk_1.default.yellow("⏰")} ${chalk_1.default.dim("Time:")} ${chalk_1.default.white(timeAgo)} ${chalk_1.default.dim("•")} ${chalk_1.default.gray(news.source)}`);
|
|
291
|
+
// Related coins with badges
|
|
292
|
+
if (news.coins && news.coins.length > 0) {
|
|
293
|
+
const coinBadges = news.coins.map((c) => chalk_1.default.bgYellow.black(` ${c} `)).join(" ");
|
|
294
|
+
console.log(` │ ${chalk_1.default.yellow("🪙")} ${chalk_1.default.dim("Coins:")} ${coinBadges}`);
|
|
295
|
+
}
|
|
296
|
+
// Sentiment indicator
|
|
297
|
+
const sentimentText = news.sentiment === "positive"
|
|
298
|
+
? "Bullish"
|
|
299
|
+
: news.sentiment === "negative"
|
|
300
|
+
? "Bearish"
|
|
301
|
+
: "Neutral";
|
|
302
|
+
console.log(` │ ${chalk_1.default.yellow("📈")} ${chalk_1.default.dim("Sentiment:")} ${sentimentColor(sentimentText)}`);
|
|
303
|
+
// Description with bullet point
|
|
304
|
+
if (news.description && news.description.length > 20) {
|
|
305
|
+
console.log(` │`);
|
|
306
|
+
console.log(` │ ${chalk_1.default.cyan("►")} ${chalk_1.default.gray(truncateText(news.description, 65))}`);
|
|
307
|
+
}
|
|
308
|
+
// Full clickable URL
|
|
309
|
+
console.log(` │`);
|
|
310
|
+
console.log(` │ ${chalk_1.default.green("🔗")} ${chalk_1.default.blue.underline(news.url)}`);
|
|
311
|
+
console.log(chalk_1.default.cyan(` └${"─".repeat(70)}┘`));
|
|
312
|
+
console.log("");
|
|
313
|
+
}
|
|
314
|
+
// Sentiment summary box
|
|
315
|
+
const positive = result.news.filter((n) => n.sentiment === "positive").length;
|
|
316
|
+
const negative = result.news.filter((n) => n.sentiment === "negative").length;
|
|
317
|
+
const neutral = result.news.length - positive - negative;
|
|
318
|
+
console.log(chalk_1.default.gray(" ┌─────────────────────────────────────────────────────────────────────┐"));
|
|
319
|
+
console.log(chalk_1.default.gray(" │ 📊 SENTIMENT SUMMARY │"));
|
|
320
|
+
console.log(chalk_1.default.gray(" ├─────────────────────────────────────────────────────────────────────┤"));
|
|
321
|
+
console.log(chalk_1.default.gray(" │ ") +
|
|
322
|
+
chalk_1.default.green(`▲ ${positive} Bullish`) +
|
|
323
|
+
" " +
|
|
324
|
+
chalk_1.default.red(`▼ ${negative} Bearish`) +
|
|
325
|
+
" " +
|
|
326
|
+
chalk_1.default.gray(`● ${neutral} Neutral`) +
|
|
327
|
+
chalk_1.default.gray(" │"));
|
|
328
|
+
console.log(chalk_1.default.gray(" └─────────────────────────────────────────────────────────────────────┘"));
|
|
329
|
+
console.log("");
|
|
330
|
+
console.log(chalk_1.default.dim.italic(" 💡 Tip: Click on links to read the full article"));
|
|
331
|
+
console.log("");
|
|
332
|
+
}
|
|
333
|
+
function truncateText(text, maxLength) {
|
|
334
|
+
if (text.length <= maxLength)
|
|
335
|
+
return text;
|
|
336
|
+
return text.slice(0, maxLength - 3) + "...";
|
|
337
|
+
}
|
|
338
|
+
// ============================================
|
|
339
|
+
// MAIN FUNCTION
|
|
340
|
+
// ============================================
|
|
341
|
+
async function fetchCryptoNews(filter) {
|
|
342
|
+
const spinner = (0, ora_1.default)("Fetching latest crypto news...").start();
|
|
343
|
+
try {
|
|
344
|
+
// Fetch from multiple sources in parallel
|
|
345
|
+
const [cryptoPanicNews, alternativeNews] = await Promise.all([
|
|
346
|
+
fetchCryptoPanicNews(filter),
|
|
347
|
+
fetchAlternativeNews(),
|
|
348
|
+
]);
|
|
349
|
+
// Combine and deduplicate news
|
|
350
|
+
const allNews = [...cryptoPanicNews, ...alternativeNews];
|
|
351
|
+
// Remove duplicates based on similar titles
|
|
352
|
+
const uniqueNews = [];
|
|
353
|
+
const seenTitles = new Set();
|
|
354
|
+
for (const news of allNews) {
|
|
355
|
+
const normalizedTitle = news.title.toLowerCase().slice(0, 50);
|
|
356
|
+
if (!seenTitles.has(normalizedTitle)) {
|
|
357
|
+
seenTitles.add(normalizedTitle);
|
|
358
|
+
uniqueNews.push(news);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// Sort by date (newest first)
|
|
362
|
+
uniqueNews.sort((a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime());
|
|
363
|
+
// Filter by coin if specified
|
|
364
|
+
let filteredNews = uniqueNews;
|
|
365
|
+
if (filter) {
|
|
366
|
+
const filterUpper = filter.toUpperCase();
|
|
367
|
+
filteredNews = uniqueNews.filter((news) => news.coins?.includes(filterUpper) ||
|
|
368
|
+
news.title.toUpperCase().includes(filterUpper) ||
|
|
369
|
+
news.description?.toUpperCase().includes(filterUpper));
|
|
370
|
+
}
|
|
371
|
+
spinner.succeed(`Fetched ${filteredNews.length} news items`);
|
|
372
|
+
const result = {
|
|
373
|
+
timestamp: Date.now(),
|
|
374
|
+
news: filteredNews.slice(0, 20),
|
|
375
|
+
totalCount: filteredNews.length,
|
|
376
|
+
};
|
|
377
|
+
return result;
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
spinner.fail("Failed to fetch news");
|
|
381
|
+
return {
|
|
382
|
+
timestamp: Date.now(),
|
|
383
|
+
news: [],
|
|
384
|
+
totalCount: 0,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Run news fetcher and display results
|
|
390
|
+
*/
|
|
391
|
+
async function runCryptoNews(filter) {
|
|
392
|
+
const result = await fetchCryptoNews(filter);
|
|
393
|
+
displayNews(result, filter);
|
|
394
|
+
}
|