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.
@@ -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 += `&currencies=${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(/&nbsp;/g, " ")
144
+ .replace(/&amp;/g, "&")
145
+ .replace(/&lt;/g, "<")
146
+ .replace(/&gt;/g, ">")
147
+ .replace(/&quot;/g, '"')
148
+ .replace(/&#39;/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
+ }