@goldensheepai/toknxr-cli 0.2.0

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/src/cli.ts ADDED
@@ -0,0 +1,447 @@
1
+ import 'dotenv/config';
2
+ import { Command } from 'commander';
3
+ import chalk from 'chalk';
4
+ import { startProxyServer } from './proxy.js';
5
+ import { login } from './auth.js';
6
+ import * as fs from 'node:fs';
7
+ import * as path from 'node:path';
8
+ import { createClient, SupabaseClient } from '@supabase/supabase-js';
9
+ import open from 'open';
10
+ import { syncInteractions } from './sync.js';
11
+
12
+ // Define a type for the interaction object
13
+ interface Interaction {
14
+ provider: string;
15
+ totalTokens: number;
16
+ promptTokens: number;
17
+ completionTokens: number;
18
+ costUSD: number;
19
+ taskType?: string;
20
+ codeQualityScore?: number;
21
+ effectivenessScore?: number;
22
+ codeQualityMetrics?: {
23
+ language?: string;
24
+ potentialIssues?: string[];
25
+ };
26
+ userPrompt?: string;
27
+ model?: string;
28
+ requestId?: string;
29
+ timestamp?: string;
30
+ }
31
+
32
+
33
+ // Gracefully handle broken pipe (e.g., piping output to `head`)
34
+ process.stdout.on('error', (err: any) => {
35
+ if (err && err.code === 'EPIPE') process.exit(0);
36
+ });
37
+ process.stderr.on('error', (err: any) => {
38
+ if (err && err.code === 'EPIPE') process.exit(0);
39
+ });
40
+
41
+ const program = new Command();
42
+
43
+ // ASCII Art Welcome Screen with gradient colors
44
+ const asciiArt = `
45
+ ${chalk.blue(' ████████╗')}${chalk.hex('#6B5BED')(' ██████╗ ')}${chalk.hex('#9B5BED')(' ██╗ ██╗')}${chalk.hex('#CB5BED')(' ███╗ ██╗')}${chalk.hex('#ED5B9B')(' ██╗ ██╗')}${chalk.hex('#ED5B6B')(' ██████╗ ')}
46
+ ${chalk.blue(' ╚══██╔══╝')}${chalk.hex('#6B5BED')(' ██╔═══██╗')}${chalk.hex('#9B5BED')(' ██║ ██╔╝')}${chalk.hex('#CB5BED')(' ████╗ ██║')}${chalk.hex('#ED5B9B')(' ╚██╗██╔╝')}${chalk.hex('#ED5B6B')(' ██╔══██╗')}
47
+ ${chalk.blue(' ██║ ')}${chalk.hex('#6B5BED')(' ██║ ██║')}${chalk.hex('#9B5BED')(' █████╔╝ ')}${chalk.hex('#CB5BED')(' ██╔██╗ ██║')}${chalk.hex('#ED5B9B')(' ╚███╔╝ ')}${chalk.hex('#ED5B6B')(' ██████╔╝')}
48
+ ${chalk.blue(' ██║ ')}${chalk.hex('#6B5BED')(' ██║ ██║')}${chalk.hex('#9B5BED')(' ██╔═██╗ ')}${chalk.hex('#CB5BED')(' ██║╚██╗██║')}${chalk.hex('#ED5B9B')(' ██╔██╗ ')}${chalk.hex('#ED5B6B')(' ██╔══██╗')}
49
+ ${chalk.blue(' ██║ ')}${chalk.hex('#6B5BED')(' ╚██████╔╝')}${chalk.hex('#9B5BED')(' ██║ ██╗')}${chalk.hex('#CB5BED')(' ██║ ╚████║')}${chalk.hex('#ED5B9B')(' ██╔╝ ██╗')}${chalk.hex('#ED5B6B')(' ██║ ██║')}
50
+ ${chalk.blue(' ╚═╝ ')}${chalk.hex('#6B5BED')(' ╚═════╝ ')}${chalk.hex('#9B5BED')(' ╚═╝ ╚═╝')}${chalk.hex('#CB5BED')(' ╚═╝ ╚═══╝')}${chalk.hex('#ED5B9B')(' ╚═╝ ╚═╝')}${chalk.hex('#ED5B6B')(' ╚═╝ ╚═╝')}
51
+
52
+ ${chalk.cyan('Tips for getting started:')}
53
+ ${chalk.white('1. Start tracking:')} ${chalk.yellow('toknxr start')} ${chalk.gray('- Launch the proxy server')}
54
+ ${chalk.white('2. View analytics:')} ${chalk.yellow('toknxr stats')} ${chalk.gray('- See token usage and code quality')}
55
+ ${chalk.white('3. Deep dive:')} ${chalk.yellow('toknxr code-analysis')} ${chalk.gray('- Detailed quality insights')}
56
+ ${chalk.white('4. Code review:')} ${chalk.yellow('toknxr review')} ${chalk.gray('- Review AI-generated code')}
57
+ ${chalk.white('5. Login:')} ${chalk.yellow('toknxr login')} ${chalk.gray('- Authenticate with your account')}
58
+ ${chalk.white('6. Set limits:')} ${chalk.yellow('toknxr policy:init')} ${chalk.gray('- Configure spending policies')}
59
+ ${chalk.white('7. Need help?')} ${chalk.yellow('toknxr --help')} ${chalk.gray('- View all commands')}
60
+
61
+ ${chalk.gray('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')}
62
+
63
+ ${chalk.hex('#FFD700')('🐑 Powered by Golden Sheep AI')}
64
+
65
+ `;
66
+
67
+ console.log(asciiArt);
68
+
69
+ // --- Supabase Client ---
70
+ const supabaseUrl = process.env.SUPABASE_URL || '';
71
+ const supabaseKey = process.env.SUPABASE_KEY || '';
72
+
73
+ if (!supabaseUrl || !supabaseKey) {
74
+ console.error(chalk.red('Error: Supabase URL or Key not found in environment variables.'));
75
+ process.exit(1);
76
+ }
77
+
78
+ const supabase: SupabaseClient = createClient(supabaseUrl, supabaseKey);
79
+
80
+ program
81
+ .name('toknxr')
82
+ .description('AI Effectiveness & Code Quality Analysis CLI')
83
+ .version('0.1.0');
84
+
85
+ program
86
+ .command('start')
87
+ .description('Start the TokNxr proxy server to monitor AI interactions.')
88
+ .action(() => {
89
+ console.log(chalk.green('Starting TokNxr proxy server...'));
90
+ startProxyServer();
91
+ });
92
+
93
+ program
94
+ .command('sync')
95
+ .description('Sync local interaction logs to the Supabase dashboard.')
96
+ .option('--clear', 'Clear the log file after a successful sync.')
97
+ .action(async (options) => {
98
+ await syncInteractions(supabase, options);
99
+ });
100
+
101
+ program
102
+ .command('stats')
103
+ .description('Display token usage statistics from the local log.')
104
+ .action(() => {
105
+ const logFilePath = path.resolve(process.cwd(), 'interactions.log');
106
+ if (!fs.existsSync(logFilePath)) {
107
+ console.log(chalk.yellow('No interactions logged yet. Use the `start` command to begin tracking.'));
108
+ return;
109
+ }
110
+
111
+ const fileContent = fs.readFileSync(logFilePath, 'utf8');
112
+ const lines = fileContent.trim().split('\n');
113
+ const interactions: Interaction[] = lines.map(line => {
114
+ try {
115
+ return JSON.parse(line);
116
+ } catch (error) {
117
+ console.warn(`Skipping invalid log entry: ${line}`);
118
+ return null;
119
+ }
120
+ }).filter((interaction): interaction is Interaction => interaction !== null);
121
+
122
+ const stats: Record<string, {
123
+ totalTokens: number;
124
+ promptTokens: number;
125
+ completionTokens: number;
126
+ requestCount: number;
127
+ costUSD: number;
128
+ codingCount: number;
129
+ avgQualityScore: number;
130
+ avgEffectivenessScore: number;
131
+ qualitySum: number;
132
+ effectivenessSum: number;
133
+ }> = interactions.reduce((acc, interaction) => {
134
+ if (!acc[interaction.provider]) {
135
+ acc[interaction.provider] = {
136
+ totalTokens: 0, promptTokens: 0, completionTokens: 0, requestCount: 0, costUSD: 0,
137
+ codingCount: 0, avgQualityScore: 0, avgEffectivenessScore: 0, qualitySum: 0, effectivenessSum: 0
138
+ };
139
+ }
140
+ acc[interaction.provider].totalTokens += interaction.totalTokens;
141
+ acc[interaction.provider].promptTokens += interaction.promptTokens;
142
+ acc[interaction.provider].completionTokens += interaction.completionTokens;
143
+ acc[interaction.provider].requestCount += 1;
144
+ acc[interaction.provider].costUSD += interaction.costUSD || 0;
145
+
146
+ if (interaction.taskType === 'coding') {
147
+ acc[interaction.provider].codingCount += 1;
148
+ if (interaction.codeQualityScore !== undefined) {
149
+ acc[interaction.provider].qualitySum += interaction.codeQualityScore;
150
+ }
151
+ if (interaction.effectivenessScore !== undefined) {
152
+ acc[interaction.provider].effectivenessSum += interaction.effectivenessScore;
153
+ }
154
+ }
155
+ return acc;
156
+ }, {} as Record<string, any>);
157
+
158
+ // Calculate averages
159
+ for (const provider in stats) {
160
+ const p = stats[provider];
161
+ if (p.codingCount > 0) {
162
+ p.avgQualityScore = Math.round(p.qualitySum / p.codingCount);
163
+ p.avgEffectivenessScore = Math.round(p.effectivenessSum / p.codingCount);
164
+ }
165
+ }
166
+
167
+ const grandTotals = Object.values(stats).reduce((acc, s) => {
168
+ acc.totalTokens += s.totalTokens;
169
+ acc.promptTokens += s.promptTokens;
170
+ acc.completionTokens += s.completionTokens;
171
+ acc.requestCount += s.requestCount;
172
+ acc.costUSD += s.costUSD;
173
+ acc.codingCount += s.codingCount;
174
+ acc.qualitySum += s.qualitySum;
175
+ acc.effectivenessSum += s.effectivenessSum;
176
+ return acc;
177
+ }, { totalTokens: 0, promptTokens: 0, completionTokens: 0, requestCount: 0, costUSD: 0, codingCount: 0, qualitySum: 0, effectivenessSum: 0 });
178
+
179
+ // Calculate grand averages
180
+ const codingTotal = grandTotals.codingCount;
181
+ const avgQuality = codingTotal > 0 ? Math.round(grandTotals.qualitySum / codingTotal) : 0;
182
+ const avgEffectiveness = codingTotal > 0 ? Math.round(grandTotals.effectivenessSum / codingTotal) : 0;
183
+
184
+ console.log(chalk.bold.underline('Token Usage Statistics'));
185
+ for (const provider in stats) {
186
+ console.log(chalk.bold(`\nProvider: ${provider}`));
187
+ console.log(` Total Requests: ${stats[provider].requestCount}`);
188
+ console.log(chalk.cyan(` Total Tokens: ${stats[provider].totalTokens}`));
189
+ console.log(` - Prompt Tokens: ${stats[provider].promptTokens}`);
190
+ console.log(` - Completion Tokens: ${stats[provider].completionTokens}`);
191
+ console.log(chalk.green(` Cost (USD): $${(stats[provider].costUSD).toFixed(4)}`));
192
+
193
+ if (stats[provider].codingCount > 0) {
194
+ console.log(chalk.blue(` Code Quality: ${stats[provider].avgQualityScore}/100 (avg)`));
195
+ console.log(chalk.magenta(` Effectiveness: ${stats[provider].avgEffectivenessScore}/100 (avg, ${stats[provider].codingCount} coding requests)`));
196
+ }
197
+ }
198
+
199
+ console.log(chalk.bold(`\nGrand Totals`));
200
+ console.log(` Requests: ${grandTotals.requestCount}`);
201
+ console.log(chalk.cyan(` Tokens: ${grandTotals.totalTokens}`));
202
+ console.log(` - Prompt: ${grandTotals.promptTokens}`);
203
+ console.log(` - Completion: ${grandTotals.completionTokens}`);
204
+ console.log(chalk.green(` Cost (USD): $${(grandTotals.costUSD).toFixed(4)}`));
205
+
206
+ if (codingTotal > 0) {
207
+ console.log(`\n${chalk.bold('Code Quality Insights:')}`);
208
+ console.log(chalk.blue(` Coding Requests: ${codingTotal}`));
209
+ console.log(chalk.blue(` Avg Code Quality: ${avgQuality}/100`));
210
+ console.log(chalk.magenta(` Avg Effectiveness: ${avgEffectiveness}/100`));
211
+
212
+ if (avgQuality < 70) {
213
+ console.log(chalk.red(' ⚠️ Low code quality - consider reviewing AI-generated code more carefully'));
214
+ }
215
+ if (avgEffectiveness < 70) {
216
+ console.log(chalk.red(' ⚠️ Low effectiveness - prompts may need improvement or different AI model'));
217
+ }
218
+ }
219
+ });
220
+
221
+ program
222
+ .command('init')
223
+ .description('Scaffold .env and toknxr.config.json in the current directory')
224
+ .action(() => {
225
+ const envPath = path.resolve(process.cwd(), '.env');
226
+ if (!fs.existsSync(envPath)) {
227
+ fs.writeFileSync(envPath, 'GEMINI_API_KEY=\n');
228
+ console.log(chalk.green(`Created ${envPath}`));
229
+ } else {
230
+ console.log(chalk.yellow(`Skipped ${envPath} (exists)`));
231
+ }
232
+
233
+ const configPath = path.resolve(process.cwd(), 'toknxr.config.json');
234
+ if (!fs.existsSync(configPath)) {
235
+ const config = {
236
+ providers: [
237
+ {
238
+ name: 'Gemini-Pro',
239
+ routePrefix: '/gemini',
240
+ targetUrl: 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent',
241
+ apiKeyEnvVar: 'GEMINI_API_KEY',
242
+ authHeader: 'x-goog-api-key',
243
+ tokenMapping: {
244
+ prompt: 'usageMetadata.promptTokenCount',
245
+ completion: 'usageMetadata.candidatesTokenCount',
246
+ total: 'usageMetadata.totalTokenCount'
247
+ }
248
+ }
249
+ ]
250
+ };
251
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
252
+ console.log(chalk.green(`Created ${configPath}`));
253
+ } else {
254
+ console.log(chalk.yellow(`Skipped ${configPath} (exists)`));
255
+ }
256
+
257
+ const policyPath = path.resolve(process.cwd(), 'toknxr.policy.json');
258
+ if (!fs.existsSync(policyPath)) {
259
+ const policy = {
260
+ version: '1',
261
+ monthlyUSD: 50,
262
+ perProviderMonthlyUSD: { 'Gemini-Pro': 30 },
263
+ webhookUrl: ''
264
+ };
265
+ fs.writeFileSync(policyPath, JSON.stringify(policy, null, 2));
266
+ console.log(chalk.green(`Created ${policyPath}`));
267
+ } else {
268
+ console.log(chalk.yellow(`Skipped ${policyPath} (exists)`));
269
+ }
270
+ });
271
+
272
+
273
+
274
+
275
+
276
+ program
277
+ .command('tail')
278
+ .description('Follow interactions.log and pretty-print new lines')
279
+ .action(() => {
280
+ const logFilePath = path.resolve(process.cwd(), 'interactions.log');
281
+ if (!fs.existsSync(logFilePath)) {
282
+ console.log(chalk.yellow('No interactions.log found. Start the proxy first.'));
283
+ return;
284
+ }
285
+ console.log(chalk.gray(`Tailing ${logFilePath}... (Ctrl+C to stop)`));
286
+ fs.watchFile(logFilePath, { interval: 500 }, () => {
287
+ const content = fs.readFileSync(logFilePath, 'utf8').trim();
288
+ const lines = content.split('\n');
289
+ const last = lines[lines.length - 1];
290
+ try {
291
+ const j: Interaction = JSON.parse(last);
292
+ console.log(`${chalk.bold(j.provider)} ${chalk.gray(j.timestamp)} id=${j.requestId} model=${j.model} tokens=${j.totalTokens} cost=$${(j.costUSD||0).toFixed(4)}`);
293
+ } catch {
294
+ console.log(last);
295
+ }
296
+ });
297
+ });
298
+
299
+ program
300
+ .command('dashboard')
301
+ .description('Open the minimal dashboard served by the proxy (/dashboard)')
302
+ .action(async () => {
303
+ const url = 'http://localhost:3000/dashboard'; // Assuming Next.js app serves the dashboard
304
+ console.log(chalk.gray(`Opening ${url}...`));
305
+ await open(url);
306
+ });
307
+
308
+ program
309
+ .command('policy:init')
310
+ .description('Scaffold toknxr.policy.json from the foundation starter pack if missing')
311
+ .action(() => {
312
+ const dest = path.resolve(process.cwd(), 'toknxr.policy.json');
313
+ if (fs.existsSync(dest)) {
314
+ console.log(chalk.yellow(`Skipped ${dest} (exists)`));
315
+ return;
316
+ }
317
+ // Fallback scaffold using sensible defaults if starter pack path is unavailable
318
+ const fallback = {
319
+ version: '1',
320
+ monthlyUSD: 50,
321
+ perProviderMonthlyUSD: { 'Gemini-Pro': 30 },
322
+ webhookUrl: ''
323
+ };
324
+ fs.writeFileSync(dest, JSON.stringify(fallback, null, 2));
325
+ console.log(chalk.green(`Created ${dest}`));
326
+ });
327
+
328
+ program
329
+ .command('code-analysis')
330
+ .description('Show detailed code quality analysis from coding interactions')
331
+ .action(() => {
332
+ const logFilePath = path.resolve(process.cwd(), 'interactions.log');
333
+ if (!fs.existsSync(logFilePath)) {
334
+ console.log(chalk.yellow('No interactions logged yet. Use the `start` command to begin tracking.'));
335
+ return;
336
+ }
337
+
338
+ const fileContent = fs.readFileSync(logFilePath, 'utf8');
339
+ const lines = fileContent.trim().split('\n');
340
+ const interactions: Interaction[] = lines.map(line => {
341
+ try {
342
+ return JSON.parse(line);
343
+ } catch (error) {
344
+ console.warn(`Skipping invalid log entry: ${line}`);
345
+ return null;
346
+ }
347
+ }).filter((interaction): interaction is Interaction => interaction !== null);
348
+
349
+ if (interactions.length === 0) {
350
+ console.log(chalk.yellow('No coding interactions found. Code analysis requires coding requests to the proxy.'));
351
+ return;
352
+ }
353
+
354
+ console.log(chalk.bold.underline('AI Code Quality Analysis'));
355
+
356
+ // Language distribution
357
+ const langStats = interactions.reduce((acc: Record<string, number>, i: Interaction) => {
358
+ const lang = i.codeQualityMetrics?.language || 'unknown';
359
+ if (!acc[lang]) acc[lang] = 0;
360
+ acc[lang]++;
361
+ return acc;
362
+ }, {});
363
+
364
+ console.log(chalk.bold('\nLanguage Distribution:'));
365
+ for (const [lang, count] of Object.entries(langStats)) {
366
+ console.log(` ${lang}: ${count} requests`);
367
+ }
368
+
369
+ // Quality score distribution
370
+ const qualityRanges = { excellent: 0, good: 0, fair: 0, poor: 0 };
371
+ const effectivenessRanges = { excellent: 0, good: 0, fair: 0, poor: 0 };
372
+
373
+ interactions.forEach((i: Interaction) => {
374
+ const q = i.codeQualityScore || 0;
375
+ const e = i.effectivenessScore || 0;
376
+
377
+ if (q >= 90) qualityRanges.excellent++;
378
+ else if (q >= 75) qualityRanges.good++;
379
+ else if (q >= 60) qualityRanges.fair++;
380
+ else qualityRanges.poor++;
381
+
382
+ if (e >= 90) effectivenessRanges.excellent++;
383
+ else if (e >= 75) effectivenessRanges.good++;
384
+ else if (e >= 60) effectivenessRanges.fair++;
385
+ else effectivenessRanges.poor++;
386
+ });
387
+
388
+ console.log(chalk.bold('\nCode Quality Scores:'));
389
+ console.log(chalk.green(` Excellent (90-100): ${qualityRanges.excellent}`));
390
+ console.log(chalk.blue(` Good (75-89): ${qualityRanges.good}`));
391
+ console.log(chalk.yellow(` Fair (60-74): ${qualityRanges.fair}`));
392
+ console.log(chalk.red(` Poor (0-59): ${qualityRanges.poor}`));
393
+
394
+ console.log(chalk.bold('\nEffectiveness Scores (Prompt ↔ Result):'));
395
+ console.log(chalk.green(` Excellent (90-100): ${effectivenessRanges.excellent}`));
396
+ console.log(chalk.blue(` Good (75-89): ${effectivenessRanges.good}`));
397
+ console.log(chalk.yellow(` Fair (60-74): ${effectivenessRanges.fair}`));
398
+ console.log(chalk.red(` Poor (0-59): ${effectivenessRanges.poor}`));
399
+
400
+ // Recent examples with low scores
401
+ const lowQuality = interactions.filter((i: Interaction) => (i.codeQualityScore || 0) < 70).slice(-3);
402
+ if (lowQuality.length > 0) {
403
+ console.log(chalk.bold('\n🔍 Recent Low-Quality Code Examples:'));
404
+ lowQuality.forEach((i: Interaction, idx: number) => {
405
+ console.log(`\n${idx + 1}. Quality: ${i.codeQualityScore}/100${i.effectivenessScore ? ` | Effectiveness: ${i.effectivenessScore}/100` : ''}`);
406
+ console.log(` Provider: ${i.provider} | Model: ${i.model}`);
407
+ if (i.userPrompt) {
408
+ const prompt = i.userPrompt.substring(0, 100);
409
+ console.log(` Prompt: ${prompt}${i.userPrompt.length > 100 ? '...' : ''}`);
410
+ }
411
+ if (i.codeQualityMetrics && i.codeQualityMetrics.potentialIssues && i.codeQualityMetrics.potentialIssues.length > 0) {
412
+ console.log(` Issues: ${i.codeQualityMetrics.potentialIssues.join(', ')}`);
413
+ }
414
+ });
415
+ }
416
+
417
+ // Improvement suggestions
418
+ const avgQuality = interactions.reduce((sum: number, i: Interaction) => sum + (i.codeQualityScore || 0), 0) / interactions.length;
419
+ const avgEffectiveness = interactions.reduce((sum: number, i: Interaction) => sum + (i.effectivenessScore || 0), 0) / interactions.length;
420
+
421
+ console.log(chalk.bold('\n💡 Improvement Suggestions:'));
422
+ if (avgQuality < 70) {
423
+ console.log(' • Consider reviewing AI-generated code more carefully before use');
424
+ console.log(' • Try more specific, detailed prompts for complex tasks');
425
+ }
426
+ if (avgEffectiveness < 70) {
427
+ console.log(' • Improve prompt clarity - be more specific about requirements');
428
+ console.log(' • Consider using different AI models for different types of tasks');
429
+ console.log(' • Break complex requests into smaller, focused prompts');
430
+ }
431
+ if (avgQuality >= 80 && avgEffectiveness >= 80) {
432
+ console.log(' • Great! Your AI coding setup is working well');
433
+ console.log(' • Consider establishing code review processes for edge cases');
434
+ }
435
+
436
+ console.log(`\n${chalk.gray('Total coding interactions analyzed: ' + interactions.length)}`);
437
+ });
438
+
439
+ program
440
+ .command('login')
441
+ .description('Authenticate with your TokNxr account')
442
+ .action(async () => {
443
+ console.log(chalk.blue('Starting CLI authentication process...'));
444
+ await login();
445
+ });
446
+
447
+ program.parse(process.argv);