prab-cli 1.2.4 → 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
+ }
@@ -20,13 +20,17 @@ const analyzer_1 = require("./analyzer");
20
20
  const market_analyzer_1 = require("./market-analyzer");
21
21
  const config_1 = require("../config");
22
22
  const groq_provider_1 = require("../models/groq-provider");
23
+ const ui_1 = require("../ui");
23
24
  /**
24
25
  * Generate AI reasoning for the trading signal
25
26
  */
26
27
  async function generateAIReasoning(symbol, signal, price, priceChange24h) {
27
28
  const apiKey = (0, config_1.getApiKey)();
28
29
  if (!apiKey) {
29
- return "AI reasoning unavailable (no API key configured)";
30
+ return {
31
+ text: "AI reasoning unavailable (no API key configured)",
32
+ tokens: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
33
+ };
30
34
  }
31
35
  const modelConfig = (0, config_1.getModelConfig)();
32
36
  const provider = new groq_provider_1.GroqProvider(modelConfig.modelId, 0.7);
@@ -50,6 +54,7 @@ Key Observations:
50
54
  ${signal.reasoning.map((r) => `- ${r}`).join("\n")}
51
55
 
52
56
  Provide a concise trading insight (2-3 sentences) explaining the signal and any cautions. Be direct and actionable.`;
57
+ const tokens = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
53
58
  try {
54
59
  const stream = provider.streamChat([{ role: "user", content: prompt }], [] // No tools needed
55
60
  );
@@ -58,11 +63,35 @@ Provide a concise trading insight (2-3 sentences) explaining the signal and any
58
63
  if (chunk.content && typeof chunk.content === "string") {
59
64
  response += chunk.content;
60
65
  }
66
+ // Capture token usage (check multiple formats)
67
+ if (chunk.usage_metadata) {
68
+ tokens.promptTokens =
69
+ chunk.usage_metadata.input_tokens || chunk.usage_metadata.prompt_tokens || 0;
70
+ tokens.completionTokens =
71
+ chunk.usage_metadata.output_tokens || chunk.usage_metadata.completion_tokens || 0;
72
+ tokens.totalTokens =
73
+ chunk.usage_metadata.total_tokens || tokens.promptTokens + tokens.completionTokens;
74
+ }
75
+ if (chunk.response_metadata?.usage) {
76
+ const usage = chunk.response_metadata.usage;
77
+ tokens.promptTokens = usage.prompt_tokens || usage.input_tokens || 0;
78
+ tokens.completionTokens = usage.completion_tokens || usage.output_tokens || 0;
79
+ tokens.totalTokens = usage.total_tokens || tokens.promptTokens + tokens.completionTokens;
80
+ }
61
81
  }
62
- return response.trim();
82
+ // Estimate tokens if not provided by API (rough estimate: ~4 chars per token)
83
+ if (tokens.totalTokens === 0 && response.length > 0) {
84
+ tokens.promptTokens = Math.ceil(prompt.length / 4);
85
+ tokens.completionTokens = Math.ceil(response.length / 4);
86
+ tokens.totalTokens = tokens.promptTokens + tokens.completionTokens;
87
+ }
88
+ return { text: response.trim(), tokens };
63
89
  }
64
90
  catch (error) {
65
- return `AI reasoning unavailable: ${error.message}`;
91
+ return {
92
+ text: `AI reasoning unavailable: ${error.message}`,
93
+ tokens,
94
+ };
66
95
  }
67
96
  }
68
97
  /**
@@ -78,10 +107,13 @@ async function generateTradingSignal(symbol, interval = "1h", includeAI = true)
78
107
  const signal = (0, analyzer_1.generateSignal)(data);
79
108
  spinner.text = "Generating trading signal...";
80
109
  let aiReasoning;
110
+ let tokenUsage;
81
111
  // Generate AI reasoning if requested
82
112
  if (includeAI) {
83
113
  spinner.text = "Getting AI insights...";
84
- aiReasoning = await generateAIReasoning(data.symbol, signal, data.currentPrice, data.priceChangePercent24h);
114
+ const aiResult = await generateAIReasoning(data.symbol, signal, data.currentPrice, data.priceChangePercent24h);
115
+ aiReasoning = aiResult.text;
116
+ tokenUsage = aiResult.tokens;
85
117
  }
86
118
  spinner.succeed(`Analysis complete for ${data.symbol}`);
87
119
  return {
@@ -91,6 +123,7 @@ async function generateTradingSignal(symbol, interval = "1h", includeAI = true)
91
123
  aiReasoning,
92
124
  price: data.currentPrice,
93
125
  priceChange24h: data.priceChangePercent24h,
126
+ tokenUsage,
94
127
  };
95
128
  }
96
129
  catch (error) {
@@ -212,6 +245,10 @@ function displaySignal(result) {
212
245
  }
213
246
  // Footer
214
247
  console.log(chalk_1.default.cyan("\u{2514}" + "\u{2500}".repeat(45) + "\u{2518}"));
248
+ // Token usage
249
+ if (result.tokenUsage && result.tokenUsage.totalTokens > 0) {
250
+ (0, ui_1.showTokenUsageCompact)(result.tokenUsage.promptTokens, result.tokenUsage.completionTokens, result.tokenUsage.totalTokens);
251
+ }
215
252
  // Disclaimer
216
253
  console.log("");
217
254
  console.log(chalk_1.default.gray.italic(" \u{26A0}\u{FE0F} This is not financial advice. Always do your own research."));
@@ -240,7 +277,10 @@ async function fullSignal(symbol, interval = "1h") {
240
277
  async function generateComprehensiveAIAnalysis(analysis) {
241
278
  const apiKey = (0, config_1.getApiKey)();
242
279
  if (!apiKey) {
243
- return "AI analysis unavailable (no API key configured)";
280
+ return {
281
+ text: "AI analysis unavailable (no API key configured)",
282
+ tokens: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
283
+ };
244
284
  }
245
285
  const modelConfig = (0, config_1.getModelConfig)();
246
286
  const provider = new groq_provider_1.GroqProvider(modelConfig.modelId, 0.7);
@@ -307,6 +347,7 @@ Provide a detailed trading analysis (4-6 sentences) that:
307
347
  3. Mentions specific price levels to watch
308
348
  4. Highlights any risks or cautions
309
349
  Be specific with prices and actionable advice.`;
350
+ const tokens = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
310
351
  try {
311
352
  const stream = provider.streamChat([{ role: "user", content: prompt }], []);
312
353
  let response = "";
@@ -314,11 +355,32 @@ Be specific with prices and actionable advice.`;
314
355
  if (chunk.content && typeof chunk.content === "string") {
315
356
  response += chunk.content;
316
357
  }
358
+ // Capture token usage (check multiple formats)
359
+ if (chunk.usage_metadata) {
360
+ tokens.promptTokens =
361
+ chunk.usage_metadata.input_tokens || chunk.usage_metadata.prompt_tokens || 0;
362
+ tokens.completionTokens =
363
+ chunk.usage_metadata.output_tokens || chunk.usage_metadata.completion_tokens || 0;
364
+ tokens.totalTokens =
365
+ chunk.usage_metadata.total_tokens || tokens.promptTokens + tokens.completionTokens;
366
+ }
367
+ if (chunk.response_metadata?.usage) {
368
+ const usage = chunk.response_metadata.usage;
369
+ tokens.promptTokens = usage.prompt_tokens || usage.input_tokens || 0;
370
+ tokens.completionTokens = usage.completion_tokens || usage.output_tokens || 0;
371
+ tokens.totalTokens = usage.total_tokens || tokens.promptTokens + tokens.completionTokens;
372
+ }
373
+ }
374
+ // Estimate tokens if not provided by API (rough estimate: ~4 chars per token)
375
+ if (tokens.totalTokens === 0 && response.length > 0) {
376
+ tokens.promptTokens = Math.ceil(prompt.length / 4);
377
+ tokens.completionTokens = Math.ceil(response.length / 4);
378
+ tokens.totalTokens = tokens.promptTokens + tokens.completionTokens;
317
379
  }
318
- return response.trim();
380
+ return { text: response.trim(), tokens };
319
381
  }
320
382
  catch (error) {
321
- return `AI analysis unavailable: ${error.message}`;
383
+ return { text: `AI analysis unavailable: ${error.message}`, tokens };
322
384
  }
323
385
  }
324
386
  /**
@@ -357,7 +419,7 @@ function wordWrap(text, maxWidth) {
357
419
  /**
358
420
  * Display comprehensive analysis in terminal
359
421
  */
360
- function displayComprehensiveAnalysis(analysis, aiAnalysis) {
422
+ function displayComprehensiveAnalysis(analysis, aiAnalysis, tokenUsage) {
361
423
  const boxWidth = 55;
362
424
  const contentWidth = boxWidth - 4;
363
425
  const border = {
@@ -535,6 +597,10 @@ function displayComprehensiveAnalysis(analysis, aiAnalysis) {
535
597
  }
536
598
  // Footer
537
599
  console.log(border.bot);
600
+ // Token usage
601
+ if (tokenUsage && tokenUsage.totalTokens > 0) {
602
+ (0, ui_1.showTokenUsageCompact)(tokenUsage.promptTokens, tokenUsage.completionTokens, tokenUsage.totalTokens);
603
+ }
538
604
  console.log("");
539
605
  console.log(chalk_1.default.gray.italic(" \u{26A0}\u{FE0F} This is not financial advice. Always do your own research."));
540
606
  console.log("");
@@ -548,9 +614,9 @@ async function comprehensiveAnalysis(symbol) {
548
614
  spinner.text = "Fetching 1H, 4H, and 1D data...";
549
615
  const analysis = await (0, market_analyzer_1.analyzeMarket)(symbol);
550
616
  spinner.text = "Generating AI insights...";
551
- const aiAnalysis = await generateComprehensiveAIAnalysis(analysis);
617
+ const aiResult = await generateComprehensiveAIAnalysis(analysis);
552
618
  spinner.succeed(`Comprehensive analysis complete for ${analysis.symbol}`);
553
- displayComprehensiveAnalysis(analysis, aiAnalysis);
619
+ displayComprehensiveAnalysis(analysis, aiResult.text, aiResult.tokens);
554
620
  }
555
621
  catch (error) {
556
622
  spinner.fail(`Failed to analyze ${symbol}`);
@@ -15,6 +15,7 @@ const data_fetcher_1 = require("./data-fetcher");
15
15
  const smc_indicators_1 = require("./smc-indicators");
16
16
  const config_1 = require("../config");
17
17
  const groq_provider_1 = require("../models/groq-provider");
18
+ const ui_1 = require("../ui");
18
19
  const chart_visual_1 = require("./chart-visual");
19
20
  // ============================================
20
21
  // TRADE SETUP GENERATION
@@ -121,8 +122,12 @@ function generateSMCTradeSetup(smc, currentPrice) {
121
122
  // ============================================
122
123
  async function generateSMCAIAnalysis(analysis) {
123
124
  const apiKey = (0, config_1.getApiKey)();
124
- if (!apiKey)
125
- return "AI analysis unavailable (no API key configured)";
125
+ if (!apiKey) {
126
+ return {
127
+ text: "AI analysis unavailable (no API key configured)",
128
+ tokens: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
129
+ };
130
+ }
126
131
  const modelConfig = (0, config_1.getModelConfig)();
127
132
  const provider = new groq_provider_1.GroqProvider(modelConfig.modelId, 0.7);
128
133
  provider.initialize(apiKey, modelConfig.modelId);
@@ -173,6 +178,7 @@ Provide SMC-focused analysis explaining:
173
178
  3. Best entry strategy (order block, FVG, or liquidity sweep)
174
179
  4. Key levels to watch and when to enter
175
180
  Be specific with prices and SMC terminology.`;
181
+ const tokens = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
176
182
  try {
177
183
  const stream = provider.streamChat([{ role: "user", content: prompt }], []);
178
184
  let response = "";
@@ -180,11 +186,32 @@ Be specific with prices and SMC terminology.`;
180
186
  if (chunk.content && typeof chunk.content === "string") {
181
187
  response += chunk.content;
182
188
  }
189
+ // Capture token usage (check multiple formats)
190
+ if (chunk.usage_metadata) {
191
+ tokens.promptTokens =
192
+ chunk.usage_metadata.input_tokens || chunk.usage_metadata.prompt_tokens || 0;
193
+ tokens.completionTokens =
194
+ chunk.usage_metadata.output_tokens || chunk.usage_metadata.completion_tokens || 0;
195
+ tokens.totalTokens =
196
+ chunk.usage_metadata.total_tokens || tokens.promptTokens + tokens.completionTokens;
197
+ }
198
+ if (chunk.response_metadata?.usage) {
199
+ const usage = chunk.response_metadata.usage;
200
+ tokens.promptTokens = usage.prompt_tokens || usage.input_tokens || 0;
201
+ tokens.completionTokens = usage.completion_tokens || usage.output_tokens || 0;
202
+ tokens.totalTokens = usage.total_tokens || tokens.promptTokens + tokens.completionTokens;
203
+ }
183
204
  }
184
- return response.trim();
205
+ // Estimate tokens if not provided by API (rough estimate: ~4 chars per token)
206
+ if (tokens.totalTokens === 0 && response.length > 0) {
207
+ tokens.promptTokens = Math.ceil(prompt.length / 4);
208
+ tokens.completionTokens = Math.ceil(response.length / 4);
209
+ tokens.totalTokens = tokens.promptTokens + tokens.completionTokens;
210
+ }
211
+ return { text: response.trim(), tokens };
185
212
  }
186
213
  catch (error) {
187
- return `AI analysis unavailable: ${error.message}`;
214
+ return { text: `AI analysis unavailable: ${error.message}`, tokens };
188
215
  }
189
216
  }
190
217
  // ============================================
@@ -216,7 +243,7 @@ function wordWrap(text, maxWidth) {
216
243
  lines.push(currentLine.trim());
217
244
  return lines;
218
245
  }
219
- function displaySMCAnalysis(analysis, aiAnalysis) {
246
+ function displaySMCAnalysis(analysis, aiAnalysis, tokenUsage) {
220
247
  const boxWidth = 58;
221
248
  const contentWidth = boxWidth - 4;
222
249
  const border = {
@@ -380,6 +407,11 @@ function displaySMCAnalysis(analysis, aiAnalysis) {
380
407
  console.log("");
381
408
  const smcChart = (0, chart_visual_1.createSMCVisualChart)(currentPrice, smc, tradeSetup);
382
409
  smcChart.forEach((l) => console.log(l));
410
+ // Token usage
411
+ if (tokenUsage && tokenUsage.totalTokens > 0) {
412
+ console.log("");
413
+ (0, ui_1.showTokenUsageCompact)(tokenUsage.promptTokens, tokenUsage.completionTokens, tokenUsage.totalTokens);
414
+ }
383
415
  console.log("");
384
416
  console.log(chalk_1.default.gray.italic(" \u{26A0}\u{FE0F} This is not financial advice. Always do your own research."));
385
417
  console.log("");
@@ -407,9 +439,9 @@ async function runSMCAnalysis(symbol) {
407
439
  candles: data.candles,
408
440
  };
409
441
  spinner.text = "Getting AI insights...";
410
- const aiAnalysis = await generateSMCAIAnalysis(analysis);
442
+ const aiResult = await generateSMCAIAnalysis(analysis);
411
443
  spinner.succeed(`SMC analysis complete for ${data.symbol}`);
412
- displaySMCAnalysis(analysis, aiAnalysis);
444
+ displaySMCAnalysis(analysis, aiResult.text, aiResult.tokens);
413
445
  }
414
446
  catch (error) {
415
447
  spinner.fail(`Failed to analyze ${symbol}`);