@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/.env +21 -0
- package/.env.example +21 -0
- package/README.md +238 -0
- package/interactions.log +8 -0
- package/lib/ai-analytics.js +296 -0
- package/lib/auth.js +73 -0
- package/lib/cli.js +382 -0
- package/lib/code-analysis.js +304 -0
- package/lib/code-review.js +319 -0
- package/lib/config.js +7 -0
- package/lib/dashboard.js +363 -0
- package/lib/hallucination-detector.js +272 -0
- package/lib/policy.js +49 -0
- package/lib/pricing.js +20 -0
- package/lib/proxy.js +359 -0
- package/lib/sync.js +95 -0
- package/package.json +38 -0
- package/src/ai-analytics.ts +418 -0
- package/src/auth.ts +80 -0
- package/src/cli.ts +447 -0
- package/src/code-analysis.ts +365 -0
- package/src/config.ts +10 -0
- package/src/dashboard.tsx +391 -0
- package/src/hallucination-detector.ts +368 -0
- package/src/policy.ts +55 -0
- package/src/pricing.ts +21 -0
- package/src/proxy.ts +438 -0
- package/src/sync.ts +129 -0
- package/start.sh +56 -0
- package/test-analysis.mjs +77 -0
- package/test-coding.mjs +27 -0
- package/test-generate-sample-data.js +118 -0
- package/test-proxy.mjs +25 -0
- package/toknxr.config.json +63 -0
- package/toknxr.policy.json +18 -0
- package/tsconfig.json +19 -0
@@ -0,0 +1,418 @@
|
|
1
|
+
import fs from 'fs';
|
2
|
+
import path from 'path';
|
3
|
+
import { HallucinationDetector, HallucinationDetection, HallucinationMetrics, BusinessImpactMetrics } from './hallucination-detector.js';
|
4
|
+
|
5
|
+
export interface AIAnalyticsData {
|
6
|
+
timestamp: string;
|
7
|
+
provider: string;
|
8
|
+
model: string;
|
9
|
+
userPrompt: string;
|
10
|
+
aiResponse: string;
|
11
|
+
taskType: string;
|
12
|
+
|
13
|
+
// Existing metrics
|
14
|
+
codeQualityScore?: number;
|
15
|
+
effectivenessScore?: number;
|
16
|
+
|
17
|
+
// New hallucination metrics
|
18
|
+
hallucinationDetection?: HallucinationDetection;
|
19
|
+
businessImpact?: BusinessImpactMetrics;
|
20
|
+
|
21
|
+
// Cost and usage
|
22
|
+
costUSD: number;
|
23
|
+
totalTokens: number;
|
24
|
+
}
|
25
|
+
|
26
|
+
export interface ProviderAnalytics {
|
27
|
+
totalInteractions: number;
|
28
|
+
hallucinationRate: number;
|
29
|
+
avgQualityScore: number;
|
30
|
+
avgEffectivenessScore: number;
|
31
|
+
businessImpact: BusinessImpactMetrics;
|
32
|
+
}
|
33
|
+
|
34
|
+
export interface AggregatedAIAnalytics {
|
35
|
+
totalInteractions: number;
|
36
|
+
hallucinationMetrics: HallucinationMetrics;
|
37
|
+
providerComparison: Record<string, ProviderAnalytics>;
|
38
|
+
trends: {
|
39
|
+
hallucinationRateOverTime: Array<{ date: string; rate: number }>;
|
40
|
+
qualityScoreOverTime: Array<{ date: string; score: number }>;
|
41
|
+
costEfficiencyOverTime: Array<{ date: string; efficiency: number }>;
|
42
|
+
};
|
43
|
+
recommendations: string[];
|
44
|
+
}
|
45
|
+
|
46
|
+
/**
|
47
|
+
* Enhanced AI Analytics with hallucination tracking
|
48
|
+
*/
|
49
|
+
export class AIAnalytics {
|
50
|
+
private detector: HallucinationDetector;
|
51
|
+
private logFilePath: string;
|
52
|
+
|
53
|
+
constructor(logFilePath?: string) {
|
54
|
+
this.detector = new HallucinationDetector();
|
55
|
+
this.logFilePath = logFilePath || path.resolve(process.cwd(), 'interactions.log');
|
56
|
+
}
|
57
|
+
|
58
|
+
/**
|
59
|
+
* Analyze a single interaction for hallucinations and business impact
|
60
|
+
*/
|
61
|
+
analyzeInteraction(
|
62
|
+
userPrompt: string,
|
63
|
+
aiResponse: string,
|
64
|
+
context: {
|
65
|
+
provider: string;
|
66
|
+
model: string;
|
67
|
+
taskType: string;
|
68
|
+
costUSD: number;
|
69
|
+
totalTokens: number;
|
70
|
+
codeQualityScore?: number;
|
71
|
+
effectivenessScore?: number;
|
72
|
+
}
|
73
|
+
): AIAnalyticsData {
|
74
|
+
// Run hallucination detection
|
75
|
+
const hallucinationDetection = this.detector.detectHallucination(
|
76
|
+
userPrompt,
|
77
|
+
aiResponse
|
78
|
+
);
|
79
|
+
|
80
|
+
// Calculate business impact if hallucination detected
|
81
|
+
let businessImpact: BusinessImpactMetrics | undefined;
|
82
|
+
if (hallucinationDetection.isLikelyHallucination) {
|
83
|
+
businessImpact = this.detector.calculateBusinessImpact(
|
84
|
+
hallucinationDetection.confidence,
|
85
|
+
1, // This interaction
|
86
|
+
context.costUSD
|
87
|
+
);
|
88
|
+
}
|
89
|
+
|
90
|
+
return {
|
91
|
+
timestamp: new Date().toISOString(),
|
92
|
+
provider: context.provider,
|
93
|
+
model: context.model,
|
94
|
+
userPrompt,
|
95
|
+
aiResponse,
|
96
|
+
taskType: context.taskType,
|
97
|
+
costUSD: context.costUSD,
|
98
|
+
totalTokens: context.totalTokens,
|
99
|
+
codeQualityScore: context.codeQualityScore,
|
100
|
+
effectivenessScore: context.effectivenessScore,
|
101
|
+
hallucinationDetection,
|
102
|
+
businessImpact
|
103
|
+
};
|
104
|
+
}
|
105
|
+
|
106
|
+
/**
|
107
|
+
* Generate comprehensive analytics from interaction logs
|
108
|
+
*/
|
109
|
+
generateAnalytics(): AggregatedAIAnalytics {
|
110
|
+
if (!fs.existsSync(this.logFilePath)) {
|
111
|
+
return this.getEmptyAnalytics();
|
112
|
+
}
|
113
|
+
|
114
|
+
const fileContent = fs.readFileSync(this.logFilePath, 'utf8');
|
115
|
+
const lines = fileContent.trim().split('\n');
|
116
|
+
const interactions: AIAnalyticsData[] = [];
|
117
|
+
|
118
|
+
// Parse all interactions
|
119
|
+
for (const line of lines) {
|
120
|
+
try {
|
121
|
+
const interaction = JSON.parse(line);
|
122
|
+
if (interaction.userPrompt && interaction.aiResponse) {
|
123
|
+
interactions.push(interaction as AIAnalyticsData);
|
124
|
+
}
|
125
|
+
} catch (error) {
|
126
|
+
// Skip malformed lines
|
127
|
+
continue;
|
128
|
+
}
|
129
|
+
}
|
130
|
+
|
131
|
+
if (interactions.length === 0) {
|
132
|
+
return this.getEmptyAnalytics();
|
133
|
+
}
|
134
|
+
|
135
|
+
return this.aggregateAnalytics(interactions);
|
136
|
+
}
|
137
|
+
|
138
|
+
/**
|
139
|
+
* Aggregate analytics from interaction data
|
140
|
+
*/
|
141
|
+
private aggregateAnalytics(interactions: AIAnalyticsData[]): AggregatedAIAnalytics {
|
142
|
+
const totalInteractions = interactions.length;
|
143
|
+
|
144
|
+
// Calculate hallucination metrics
|
145
|
+
const hallucinations = interactions.filter(i => i.hallucinationDetection?.isLikelyHallucination);
|
146
|
+
const hallucinationCount = hallucinations.length;
|
147
|
+
const hallucinationRate = (hallucinationCount / totalInteractions) * 100;
|
148
|
+
|
149
|
+
const avgConfidence = hallucinations.length > 0
|
150
|
+
? hallucinations.reduce((sum, h) => sum + (h.hallucinationDetection?.confidence || 0), 0) / hallucinations.length
|
151
|
+
: 0;
|
152
|
+
|
153
|
+
// Category breakdown
|
154
|
+
const byCategory: Record<string, number> = {};
|
155
|
+
hallucinations.forEach(h => {
|
156
|
+
h.hallucinationDetection?.evidence.forEach(evidence => {
|
157
|
+
byCategory[evidence.type] = (byCategory[evidence.type] || 0) + 1;
|
158
|
+
});
|
159
|
+
});
|
160
|
+
|
161
|
+
// Provider comparison
|
162
|
+
const providerStats: Record<string, AIAnalyticsData[]> = {};
|
163
|
+
interactions.forEach(interaction => {
|
164
|
+
if (!providerStats[interaction.provider]) {
|
165
|
+
providerStats[interaction.provider] = [];
|
166
|
+
}
|
167
|
+
providerStats[interaction.provider].push(interaction);
|
168
|
+
});
|
169
|
+
|
170
|
+
const providerComparison: Record<string, ProviderAnalytics> = {};
|
171
|
+
Object.entries(providerStats).forEach(([provider, providerInteractions]) => {
|
172
|
+
const providerHallucinations = providerInteractions.filter(i => i.hallucinationDetection?.isLikelyHallucination);
|
173
|
+
const providerHallucinationRate = (providerHallucinations.length / providerInteractions.length) * 100;
|
174
|
+
|
175
|
+
const avgQualityScore = providerInteractions.reduce((sum, i) => sum + (i.codeQualityScore || 0), 0) / providerInteractions.length;
|
176
|
+
const avgEffectivenessScore = providerInteractions.reduce((sum, i) => sum + (i.effectivenessScore || 0), 0) / providerInteractions.length;
|
177
|
+
|
178
|
+
// Calculate business impact for this provider
|
179
|
+
const totalCost = providerInteractions.reduce((sum, i) => sum + i.costUSD, 0);
|
180
|
+
const businessImpact = this.detector.calculateBusinessImpact(
|
181
|
+
providerHallucinationRate,
|
182
|
+
providerInteractions.length,
|
183
|
+
totalCost / providerInteractions.length
|
184
|
+
);
|
185
|
+
|
186
|
+
providerComparison[provider] = {
|
187
|
+
totalInteractions: providerInteractions.length,
|
188
|
+
hallucinationRate: Math.round(providerHallucinationRate * 10) / 10,
|
189
|
+
avgQualityScore: Math.round(avgQualityScore),
|
190
|
+
avgEffectivenessScore: Math.round(avgEffectivenessScore),
|
191
|
+
businessImpact
|
192
|
+
};
|
193
|
+
});
|
194
|
+
|
195
|
+
// Calculate overall business impact
|
196
|
+
const totalCost = interactions.reduce((sum, i) => sum + i.costUSD, 0);
|
197
|
+
const avgCostPerInteraction = totalCost / totalInteractions;
|
198
|
+
const overallBusinessImpact = this.detector.calculateBusinessImpact(
|
199
|
+
hallucinationRate,
|
200
|
+
totalInteractions,
|
201
|
+
avgCostPerInteraction
|
202
|
+
);
|
203
|
+
|
204
|
+
const hallucinationMetrics: HallucinationMetrics = {
|
205
|
+
totalAnalyses: totalInteractions,
|
206
|
+
hallucinationCount,
|
207
|
+
hallucinationRate: Math.round(hallucinationRate * 10) / 10,
|
208
|
+
avgConfidence: Math.round(avgConfidence),
|
209
|
+
byCategory,
|
210
|
+
byProvider: Object.fromEntries(
|
211
|
+
Object.entries(providerComparison).map(([provider, stats]) => [provider, stats.hallucinationRate])
|
212
|
+
),
|
213
|
+
businessImpact: overallBusinessImpact
|
214
|
+
};
|
215
|
+
|
216
|
+
// Generate trends (last 30 days)
|
217
|
+
const trends = this.generateTrends(interactions);
|
218
|
+
|
219
|
+
// Generate recommendations
|
220
|
+
const recommendations = this.generateRecommendations(hallucinationMetrics, providerComparison);
|
221
|
+
|
222
|
+
return {
|
223
|
+
totalInteractions,
|
224
|
+
hallucinationMetrics,
|
225
|
+
providerComparison,
|
226
|
+
trends,
|
227
|
+
recommendations
|
228
|
+
};
|
229
|
+
}
|
230
|
+
|
231
|
+
/**
|
232
|
+
* Generate trend data from interactions
|
233
|
+
*/
|
234
|
+
private generateTrends(interactions: AIAnalyticsData[]) {
|
235
|
+
const last30Days = new Date();
|
236
|
+
last30Days.setDate(last30Days.getDate() - 30);
|
237
|
+
|
238
|
+
const recentInteractions = interactions.filter(i => new Date(i.timestamp) >= last30Days);
|
239
|
+
|
240
|
+
// Group by day
|
241
|
+
const dailyGroups: Record<string, AIAnalyticsData[]> = {};
|
242
|
+
recentInteractions.forEach(interaction => {
|
243
|
+
const date = new Date(interaction.timestamp).toISOString().split('T')[0];
|
244
|
+
if (!dailyGroups[date]) {
|
245
|
+
dailyGroups[date] = [];
|
246
|
+
}
|
247
|
+
dailyGroups[date].push(interaction);
|
248
|
+
});
|
249
|
+
|
250
|
+
// Calculate daily metrics
|
251
|
+
const hallucinationRateOverTime = Object.entries(dailyGroups)
|
252
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
253
|
+
.map(([date, dayInteractions]) => {
|
254
|
+
const hallucinations = dayInteractions.filter(i => i.hallucinationDetection?.isLikelyHallucination);
|
255
|
+
const rate = dayInteractions.length > 0 ? (hallucinations.length / dayInteractions.length) * 100 : 0;
|
256
|
+
return { date, rate: Math.round(rate * 10) / 10 };
|
257
|
+
});
|
258
|
+
|
259
|
+
const qualityScoreOverTime = Object.entries(dailyGroups)
|
260
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
261
|
+
.map(([date, dayInteractions]) => {
|
262
|
+
const avgQuality = dayInteractions.reduce((sum, i) => sum + (i.codeQualityScore || 0), 0) / dayInteractions.length;
|
263
|
+
return { date, score: Math.round(avgQuality) };
|
264
|
+
});
|
265
|
+
|
266
|
+
const costEfficiencyOverTime = Object.entries(dailyGroups)
|
267
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
268
|
+
.map(([date, dayInteractions]) => {
|
269
|
+
const totalCost = dayInteractions.reduce((sum, i) => sum + i.costUSD, 0);
|
270
|
+
const avgQuality = dayInteractions.reduce((sum, i) => sum + (i.codeQualityScore || 0), 0) / dayInteractions.length;
|
271
|
+
const efficiency = totalCost > 0 ? (avgQuality / totalCost) * 1000 : 0; // Quality per dollar
|
272
|
+
return { date, efficiency: Math.round(efficiency * 10) / 10 };
|
273
|
+
});
|
274
|
+
|
275
|
+
return {
|
276
|
+
hallucinationRateOverTime,
|
277
|
+
qualityScoreOverTime,
|
278
|
+
costEfficiencyOverTime
|
279
|
+
};
|
280
|
+
}
|
281
|
+
|
282
|
+
/**
|
283
|
+
* Generate actionable recommendations
|
284
|
+
*/
|
285
|
+
private generateRecommendations(
|
286
|
+
metrics: HallucinationMetrics,
|
287
|
+
providerComparison: Record<string, ProviderAnalytics>
|
288
|
+
): string[] {
|
289
|
+
const recommendations: string[] = [];
|
290
|
+
|
291
|
+
// Hallucination rate recommendations
|
292
|
+
if (metrics.hallucinationRate > 20) {
|
293
|
+
recommendations.push(`🚨 CRITICAL: Hallucination rate is ${metrics.hallucinationRate}%. Consider reviewing AI-generated content more carefully.`);
|
294
|
+
} else if (metrics.hallucinationRate > 10) {
|
295
|
+
recommendations.push(`⚠️ WARNING: Hallucination rate is ${metrics.hallucinationRate}%. Monitor closely and verify important information.`);
|
296
|
+
} else if (metrics.hallucinationRate > 5) {
|
297
|
+
recommendations.push(`ℹ️ Hallucination rate is ${metrics.hallucinationRate}%. Generally acceptable but watch for patterns.`);
|
298
|
+
}
|
299
|
+
|
300
|
+
// Provider-specific recommendations
|
301
|
+
const worstProvider = Object.entries(providerComparison)
|
302
|
+
.sort(([,a], [,b]) => (b.hallucinationRate || 0) - (a.hallucinationRate || 0))[0];
|
303
|
+
|
304
|
+
if (worstProvider && (worstProvider[1].hallucinationRate || 0) > 15) {
|
305
|
+
recommendations.push(`🔄 Consider reducing usage of ${worstProvider[0]} (hallucination rate: ${worstProvider[1].hallucinationRate}%) or improve prompt quality.`);
|
306
|
+
}
|
307
|
+
|
308
|
+
// Business impact recommendations
|
309
|
+
if (metrics.businessImpact.estimatedDevTimeWasted > 5) {
|
310
|
+
recommendations.push(`⏱️ Hallucinations are wasting ~${metrics.businessImpact.estimatedDevTimeWasted} hours of development time. Consider AI response verification workflows.`);
|
311
|
+
}
|
312
|
+
|
313
|
+
if (metrics.businessImpact.roiImpact > 10) {
|
314
|
+
recommendations.push(`💰 Hallucinations are reducing ROI by ${metrics.businessImpact.roiImpact}%. Review AI usage strategy and prompt engineering.`);
|
315
|
+
}
|
316
|
+
|
317
|
+
// Quality improvement recommendations
|
318
|
+
const bestProvider = Object.entries(providerComparison)
|
319
|
+
.sort(([,a], [,b]) => (b.avgQualityScore || 0) - (a.avgQualityScore || 0))[0];
|
320
|
+
|
321
|
+
if (bestProvider && (bestProvider[1].avgQualityScore || 0) > 80) {
|
322
|
+
recommendations.push(`✅ ${bestProvider[0]} shows good performance (quality: ${bestProvider[1].avgQualityScore}/100). Consider using it more for critical tasks.`);
|
323
|
+
}
|
324
|
+
|
325
|
+
// Category-specific recommendations
|
326
|
+
if (metrics.byCategory.fabrication > metrics.byCategory.contradiction) {
|
327
|
+
recommendations.push(`🔧 High fabrication rate detected. Focus on improving technical prompt specificity and providing more context.`);
|
328
|
+
}
|
329
|
+
|
330
|
+
if (metrics.byCategory.contradiction > 3) {
|
331
|
+
recommendations.push(`⚖️ Multiple contradictions detected. Consider breaking complex requests into smaller, focused prompts.`);
|
332
|
+
}
|
333
|
+
|
334
|
+
if (recommendations.length === 0) {
|
335
|
+
recommendations.push('✨ AI hallucination metrics look good! Continue monitoring for any emerging patterns.');
|
336
|
+
}
|
337
|
+
|
338
|
+
return recommendations;
|
339
|
+
}
|
340
|
+
|
341
|
+
/**
|
342
|
+
* Get empty analytics for when no data is available
|
343
|
+
*/
|
344
|
+
private getEmptyAnalytics(): AggregatedAIAnalytics {
|
345
|
+
return {
|
346
|
+
totalInteractions: 0,
|
347
|
+
hallucinationMetrics: {
|
348
|
+
totalAnalyses: 0,
|
349
|
+
hallucinationCount: 0,
|
350
|
+
hallucinationRate: 0,
|
351
|
+
avgConfidence: 0,
|
352
|
+
byCategory: {},
|
353
|
+
byProvider: {},
|
354
|
+
businessImpact: {
|
355
|
+
estimatedDevTimeWasted: 0,
|
356
|
+
qualityDegradationScore: 0,
|
357
|
+
roiImpact: 0,
|
358
|
+
costOfHallucinations: 0
|
359
|
+
}
|
360
|
+
},
|
361
|
+
providerComparison: {},
|
362
|
+
trends: {
|
363
|
+
hallucinationRateOverTime: [],
|
364
|
+
qualityScoreOverTime: [],
|
365
|
+
costEfficiencyOverTime: []
|
366
|
+
},
|
367
|
+
recommendations: ['No data available yet. Start using AI through the proxy to see analytics.']
|
368
|
+
};
|
369
|
+
}
|
370
|
+
|
371
|
+
/**
|
372
|
+
* Export analytics to JSON file
|
373
|
+
*/
|
374
|
+
exportAnalytics(filePath?: string): void {
|
375
|
+
const analytics = this.generateAnalytics();
|
376
|
+
const exportPath = filePath || path.resolve(process.cwd(), 'ai-analytics.json');
|
377
|
+
|
378
|
+
fs.writeFileSync(exportPath, JSON.stringify(analytics, null, 2));
|
379
|
+
console.log(`AI analytics exported to ${exportPath}`);
|
380
|
+
}
|
381
|
+
|
382
|
+
/**
|
383
|
+
* Get real-time hallucination rate for a specific provider
|
384
|
+
*/
|
385
|
+
getProviderHallucinationRate(provider: string, hours: number = 24): number {
|
386
|
+
if (!fs.existsSync(this.logFilePath)) return 0;
|
387
|
+
|
388
|
+
const cutoffTime = new Date(Date.now() - hours * 60 * 60 * 1000);
|
389
|
+
const fileContent = fs.readFileSync(this.logFilePath, 'utf8');
|
390
|
+
const lines = fileContent.trim().split('\n');
|
391
|
+
|
392
|
+
let providerInteractions = 0;
|
393
|
+
let providerHallucinations = 0;
|
394
|
+
|
395
|
+
for (const line of lines) {
|
396
|
+
try {
|
397
|
+
const interaction = JSON.parse(line);
|
398
|
+
const interactionTime = new Date(interaction.timestamp);
|
399
|
+
|
400
|
+
if (interactionTime >= cutoffTime && interaction.provider === provider) {
|
401
|
+
providerInteractions++;
|
402
|
+
if (interaction.hallucinationDetection?.isLikelyHallucination) {
|
403
|
+
providerHallucinations++;
|
404
|
+
}
|
405
|
+
}
|
406
|
+
} catch {
|
407
|
+
continue;
|
408
|
+
}
|
409
|
+
}
|
410
|
+
|
411
|
+
return providerInteractions > 0 ? (providerHallucinations / providerInteractions) * 100 : 0;
|
412
|
+
}
|
413
|
+
}
|
414
|
+
|
415
|
+
/**
|
416
|
+
* Global AI analytics instance
|
417
|
+
*/
|
418
|
+
export const aiAnalytics = new AIAnalytics();
|
package/src/auth.ts
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
import * as http from 'http';
|
2
|
+
import open from 'open';
|
3
|
+
import chalk from 'chalk';
|
4
|
+
import * as keytar from 'keytar'; // Import keytar
|
5
|
+
|
6
|
+
const CLI_LOGIN_PORT = 8789;
|
7
|
+
const WEB_APP_URL = 'http://localhost:3000';
|
8
|
+
const SERVICE_NAME = 'toknxr-cli'; // A unique name for our service in the keychain
|
9
|
+
const ACCOUNT_NAME = 'default-user'; // A generic account name for the stored token
|
10
|
+
|
11
|
+
// Function to securely store the token
|
12
|
+
const storeToken = async (token: string) => {
|
13
|
+
await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, token);
|
14
|
+
console.log(chalk.green('Supabase JWT securely stored in system keychain.'));
|
15
|
+
};
|
16
|
+
|
17
|
+
// Function to retrieve the token
|
18
|
+
const getToken = async (): Promise<string | null> => {
|
19
|
+
return await keytar.getPassword(SERVICE_NAME, ACCOUNT_NAME);
|
20
|
+
};
|
21
|
+
|
22
|
+
export const login = async () => {
|
23
|
+
const server = new Promise<string>((resolve, reject) => {
|
24
|
+
const s = http.createServer(async (req, res) => {
|
25
|
+
// Handle CORS preflight requests
|
26
|
+
res.setHeader('Access-Control-Allow-Origin', WEB_APP_URL);
|
27
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
28
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
29
|
+
|
30
|
+
if (req.method === 'OPTIONS') {
|
31
|
+
res.writeHead(204);
|
32
|
+
res.end();
|
33
|
+
return;
|
34
|
+
}
|
35
|
+
|
36
|
+
if (req.method === 'POST' && req.url === '/token') {
|
37
|
+
const chunks: Buffer[] = [];
|
38
|
+
for await (const chunk of req) {
|
39
|
+
chunks.push(chunk);
|
40
|
+
}
|
41
|
+
const requestBody = Buffer.concat(chunks).toString();
|
42
|
+
const { token: supabaseJwt } = JSON.parse(requestBody); // Supabase JWT
|
43
|
+
|
44
|
+
if (supabaseJwt) {
|
45
|
+
console.log(chalk.green('CLI authentication successful!'));
|
46
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
47
|
+
res.end(JSON.stringify({ success: true }));
|
48
|
+
s.close();
|
49
|
+
resolve(supabaseJwt); // Resolve with the Supabase JWT
|
50
|
+
} else {
|
51
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
52
|
+
res.end(JSON.stringify({ error: 'No token provided' }));
|
53
|
+
s.close();
|
54
|
+
reject(new Error('No token provided'));
|
55
|
+
}
|
56
|
+
} else {
|
57
|
+
res.writeHead(404);
|
58
|
+
res.end();
|
59
|
+
}
|
60
|
+
});
|
61
|
+
|
62
|
+
s.listen(CLI_LOGIN_PORT, async () => {
|
63
|
+
const loginUrl = `${WEB_APP_URL}/cli-login?port=${CLI_LOGIN_PORT}`;
|
64
|
+
console.log(chalk.yellow('Your browser has been opened to complete the login process.'));
|
65
|
+
await open(loginUrl);
|
66
|
+
});
|
67
|
+
});
|
68
|
+
|
69
|
+
try {
|
70
|
+
const supabaseJwt = await server; // Get the Supabase JWT
|
71
|
+
await storeToken(supabaseJwt); // Store the Supabase JWT securely
|
72
|
+
console.log(chalk.cyan('Authentication complete. You can now use TokNxr CLI commands.'));
|
73
|
+
} catch (error: unknown) {
|
74
|
+
const message = error instanceof Error ? error.message : String(error);
|
75
|
+
console.error(chalk.red('Login failed:', message));
|
76
|
+
}
|
77
|
+
};
|
78
|
+
|
79
|
+
// Export getToken for other parts of the CLI to use
|
80
|
+
export { getToken };
|