@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
package/src/proxy.ts
ADDED
@@ -0,0 +1,438 @@
|
|
1
|
+
import * as http from 'http';
|
2
|
+
import 'dotenv/config';
|
3
|
+
import chalk from 'chalk';
|
4
|
+
import axios from 'axios';
|
5
|
+
import * as fs from 'node:fs';
|
6
|
+
import * as path from 'node:path';
|
7
|
+
import { randomUUID } from 'node:crypto';
|
8
|
+
import { estimateCostUSD } from './pricing.js';
|
9
|
+
import { loadPolicy, currentMonthKey, computeMonthlySpend, sendBudgetAlert } from './policy.js';
|
10
|
+
import { analyzeCodeQuality, scoreEffectiveness, extractCodeFromResponse, CodeQualityMetrics } from './code-analysis.js';
|
11
|
+
import { hallucinationDetector, HallucinationDetection } from './hallucination-detector.js';
|
12
|
+
|
13
|
+
const PORT = 8788;
|
14
|
+
|
15
|
+
interface AIInteraction {
|
16
|
+
timestamp: string;
|
17
|
+
provider: string;
|
18
|
+
model: string;
|
19
|
+
promptTokens: number;
|
20
|
+
completionTokens: number;
|
21
|
+
totalTokens: number;
|
22
|
+
costUSD: number;
|
23
|
+
taskType?: string;
|
24
|
+
// New code quality analysis fields
|
25
|
+
userPrompt?: string;
|
26
|
+
aiResponse?: string;
|
27
|
+
extractedCode?: string;
|
28
|
+
codeQualityScore?: number;
|
29
|
+
codeQualityMetrics?: CodeQualityMetrics;
|
30
|
+
effectivenessScore?: number;
|
31
|
+
// Hallucination detection
|
32
|
+
hallucinationDetection?: HallucinationDetection;
|
33
|
+
}
|
34
|
+
|
35
|
+
// Helper to resolve dot notation paths
|
36
|
+
const getValueFromPath = (obj: unknown, path: string): number => {
|
37
|
+
if (!path || !obj) return 0;
|
38
|
+
try {
|
39
|
+
const result = path.split('.').reduce((res: unknown, prop: string) =>
|
40
|
+
res && typeof res === 'object' && prop in res ? (res as Record<string, unknown>)[prop] : undefined,
|
41
|
+
obj
|
42
|
+
);
|
43
|
+
return Number(result) || 0;
|
44
|
+
} catch {
|
45
|
+
return 0;
|
46
|
+
}
|
47
|
+
};
|
48
|
+
|
49
|
+
interface ProviderConfig {
|
50
|
+
providers: Array<{
|
51
|
+
name: string;
|
52
|
+
routePrefix: string;
|
53
|
+
targetUrl: string;
|
54
|
+
apiKeyEnvVar: string;
|
55
|
+
authHeader: string;
|
56
|
+
authScheme?: string;
|
57
|
+
tokenMapping: {
|
58
|
+
prompt: string;
|
59
|
+
completion: string;
|
60
|
+
total: string;
|
61
|
+
};
|
62
|
+
}>;
|
63
|
+
}
|
64
|
+
|
65
|
+
export const startProxyServer = async () => {
|
66
|
+
// --- Load Provider Config ---
|
67
|
+
let providerConfig: ProviderConfig;
|
68
|
+
const configPath = path.resolve(process.cwd(), 'toknxr.config.json');
|
69
|
+
if (!fs.existsSync(configPath)) {
|
70
|
+
console.error(chalk.red(`[Proxy] Error: 'toknxr.config.json' not found in the current directory.`));
|
71
|
+
console.error(chalk.yellow(`[Proxy] Please create one based on the 'ai-config-template.json' in the foundation pack.`));
|
72
|
+
process.exit(1);
|
73
|
+
}
|
74
|
+
try {
|
75
|
+
const configFile = fs.readFileSync(configPath, 'utf8');
|
76
|
+
providerConfig = JSON.parse(configFile);
|
77
|
+
console.log(chalk.green('[Proxy] Successfully loaded provider configuration.'));
|
78
|
+
} catch (error) {
|
79
|
+
console.error(chalk.red('[Proxy] Error parsing toknxr.config.json:', error));
|
80
|
+
process.exit(1);
|
81
|
+
}
|
82
|
+
// --------------------------
|
83
|
+
|
84
|
+
const server = http.createServer(async (req, res) => {
|
85
|
+
const requestId = randomUUID();
|
86
|
+
console.log(chalk.blue(`[Proxy] Received request: ${req.method} ${req.url}`));
|
87
|
+
|
88
|
+
// Health check endpoint
|
89
|
+
if (req.method === 'GET' && req.url === '/health') {
|
90
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
91
|
+
res.end(JSON.stringify({ status: 'ok' }));
|
92
|
+
return;
|
93
|
+
}
|
94
|
+
|
95
|
+
// Redirect root to dashboard for convenience
|
96
|
+
if (req.method === 'GET' && (req.url === '/' || req.url === '')) {
|
97
|
+
res.writeHead(302, { Location: '/dashboard' });
|
98
|
+
res.end();
|
99
|
+
return;
|
100
|
+
}
|
101
|
+
|
102
|
+
// Enhanced stats API for dashboard
|
103
|
+
if (req.method === 'GET' && req.url === '/api/stats') {
|
104
|
+
const logFilePath = path.resolve(process.cwd(), 'interactions.log');
|
105
|
+
const monthKey = currentMonthKey();
|
106
|
+
const sums = computeMonthlySpend(logFilePath, monthKey);
|
107
|
+
|
108
|
+
// Read recent interactions for the dashboard
|
109
|
+
interface RecentInteraction {
|
110
|
+
timestamp: string;
|
111
|
+
provider: string;
|
112
|
+
model: string;
|
113
|
+
cost: number;
|
114
|
+
taskType?: string;
|
115
|
+
qualityScore?: number;
|
116
|
+
effectivenessScore?: number;
|
117
|
+
}
|
118
|
+
|
119
|
+
let recentInteractions: RecentInteraction[] = [];
|
120
|
+
if (fs.existsSync(logFilePath)) {
|
121
|
+
try {
|
122
|
+
const fileContent = fs.readFileSync(logFilePath, 'utf8');
|
123
|
+
const lines = fileContent.trim().split('\n');
|
124
|
+
recentInteractions = lines.slice(-20) // Last 20 interactions
|
125
|
+
.map(line => JSON.parse(line))
|
126
|
+
.map((interaction: unknown) => ({
|
127
|
+
timestamp: (interaction as { timestamp: string }).timestamp,
|
128
|
+
provider: (interaction as { provider: string }).provider,
|
129
|
+
model: (interaction as { model: string }).model,
|
130
|
+
cost: (interaction as { costUSD: number }).costUSD || 0,
|
131
|
+
taskType: (interaction as { taskType?: string }).taskType,
|
132
|
+
qualityScore: (interaction as { codeQualityScore?: number }).codeQualityScore,
|
133
|
+
effectivenessScore: (interaction as { effectivenessScore?: number }).effectivenessScore
|
134
|
+
}))
|
135
|
+
.reverse(); // Most recent first
|
136
|
+
} catch (error) {
|
137
|
+
console.error('Error reading recent interactions:', error);
|
138
|
+
}
|
139
|
+
}
|
140
|
+
|
141
|
+
// Calculate waste rate based on quality scores
|
142
|
+
const codingInteractions = recentInteractions.filter(i => i.taskType === 'coding');
|
143
|
+
const wasteRate = codingInteractions.length > 0
|
144
|
+
? (codingInteractions.filter(i => (i.qualityScore || 0) < 70).length / codingInteractions.length) * 100
|
145
|
+
: 0;
|
146
|
+
|
147
|
+
// Count total interactions from recent interactions for this month
|
148
|
+
const monthInteractions = recentInteractions.filter(i => {
|
149
|
+
const interactionDate = new Date(i.timestamp);
|
150
|
+
const interactionMonth = currentMonthKey(interactionDate);
|
151
|
+
return interactionMonth === monthKey;
|
152
|
+
});
|
153
|
+
|
154
|
+
const enhancedStats = {
|
155
|
+
monthKey,
|
156
|
+
totals: sums,
|
157
|
+
recentInteractions,
|
158
|
+
wasteRate,
|
159
|
+
summary: {
|
160
|
+
totalCost: sums.total || 0,
|
161
|
+
totalInteractions: monthInteractions.length,
|
162
|
+
avgCostPerTask: monthInteractions.length ? (sums.total / monthInteractions.length) : 0,
|
163
|
+
wasteRate
|
164
|
+
}
|
165
|
+
};
|
166
|
+
|
167
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
168
|
+
res.end(JSON.stringify(enhancedStats));
|
169
|
+
return;
|
170
|
+
}
|
171
|
+
|
172
|
+
// Enhanced React Dashboard
|
173
|
+
if (req.method === 'GET' && req.url === '/dashboard') {
|
174
|
+
const html = `<!DOCTYPE html>
|
175
|
+
<html lang="en">
|
176
|
+
<head>
|
177
|
+
<meta charset="UTF-8">
|
178
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
179
|
+
<title>TokNXR Dashboard - AI Analytics</title>
|
180
|
+
<style>
|
181
|
+
/* Inline critical CSS for better loading experience */
|
182
|
+
body { margin: 0; font-family: system-ui, -apple-system, sans-serif; }
|
183
|
+
#dashboard-root { min-height: 100vh; }
|
184
|
+
.loading {
|
185
|
+
display: flex;
|
186
|
+
align-items: center;
|
187
|
+
justify-content: center;
|
188
|
+
height: 100vh;
|
189
|
+
font-size: 18px;
|
190
|
+
color: #64748b;
|
191
|
+
}
|
192
|
+
</style>
|
193
|
+
</head>
|
194
|
+
<body>
|
195
|
+
<div id="dashboard-root">
|
196
|
+
<div class="loading">Loading TokNXR Dashboard...</div>
|
197
|
+
</div>
|
198
|
+
<script>
|
199
|
+
// Inline the dashboard script
|
200
|
+
${fs.readFileSync(path.resolve(process.cwd(), 'src/dashboard.tsx'), 'utf8').replace(/export default function Dashboard/, 'function Dashboard').replace(/import React.*$/m, '').replace(/import { createRoot }.*$/m, '')}
|
201
|
+
</script>
|
202
|
+
</body>
|
203
|
+
</html>`;
|
204
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
205
|
+
res.end(html);
|
206
|
+
return;
|
207
|
+
}
|
208
|
+
|
209
|
+
const matchedProvider = providerConfig.providers.find((p: ProviderConfig['providers'][0]) => req.url?.startsWith(p.routePrefix));
|
210
|
+
|
211
|
+
if (req.method === 'POST' && matchedProvider) {
|
212
|
+
try {
|
213
|
+
console.log(chalk.gray(`[Proxy] Matched provider: ${matchedProvider.name} | requestId=${requestId}`));
|
214
|
+
const chunks: Buffer[] = [];
|
215
|
+
for await (const chunk of req) {
|
216
|
+
chunks.push(chunk);
|
217
|
+
}
|
218
|
+
const requestBody = Buffer.concat(chunks).toString();
|
219
|
+
const requestData = JSON.parse(requestBody);
|
220
|
+
|
221
|
+
// --- Hard budget enforcement (pre-flight) ---
|
222
|
+
const prePolicy = loadPolicy(process.cwd());
|
223
|
+
if (prePolicy) {
|
224
|
+
const preLogPath = path.resolve(process.cwd(), 'interactions.log');
|
225
|
+
const preMonth = currentMonthKey();
|
226
|
+
const preSums = computeMonthlySpend(preLogPath, preMonth);
|
227
|
+
let overCap = false;
|
228
|
+
const reasons: string[] = [];
|
229
|
+
if (prePolicy.monthlyUSD && preSums.total > prePolicy.monthlyUSD) {
|
230
|
+
overCap = true; reasons.push(`total>${prePolicy.monthlyUSD}`);
|
231
|
+
}
|
232
|
+
if (prePolicy.perProviderMonthlyUSD) {
|
233
|
+
const cap = prePolicy.perProviderMonthlyUSD[matchedProvider.name];
|
234
|
+
if (typeof cap === 'number') {
|
235
|
+
const spent = (preSums.byProvider[matchedProvider.name] || 0);
|
236
|
+
if (spent > cap) { overCap = true; reasons.push(`${matchedProvider.name}>${cap}`); }
|
237
|
+
}
|
238
|
+
}
|
239
|
+
if (overCap) {
|
240
|
+
console.log(chalk.red(`[Proxy] Hard budget enforcement: blocking request | reasons=${reasons.join(', ')} | requestId=${requestId}`));
|
241
|
+
res.writeHead(429, { 'Content-Type': 'application/json' });
|
242
|
+
res.end(JSON.stringify({ error: 'Budget exceeded', reasons, requestId }));
|
243
|
+
return;
|
244
|
+
}
|
245
|
+
}
|
246
|
+
|
247
|
+
// --- Dynamic Request Forwarding ---
|
248
|
+
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
249
|
+
if (matchedProvider.apiKeyEnvVar) {
|
250
|
+
const apiKey = process.env[matchedProvider.apiKeyEnvVar];
|
251
|
+
if (!apiKey) {
|
252
|
+
throw new Error(`${matchedProvider.apiKeyEnvVar} environment variable not set.`);
|
253
|
+
}
|
254
|
+
const authHeader = matchedProvider.authHeader || 'Authorization';
|
255
|
+
const authScheme = matchedProvider.authScheme ? `${matchedProvider.authScheme} ` : '';
|
256
|
+
headers[authHeader as string] = `${authScheme}${apiKey}`;
|
257
|
+
}
|
258
|
+
|
259
|
+
const targetUrl = matchedProvider.targetUrl.replace(/\/$/, '') + (req.url || '').substring(matchedProvider.routePrefix.length);
|
260
|
+
console.log(chalk.gray(`[Proxy] Forwarding request to ${targetUrl} | requestId=${requestId}`));
|
261
|
+
|
262
|
+
const apiResponse = await axios.post(targetUrl, requestData, {
|
263
|
+
headers,
|
264
|
+
timeout: 20000,
|
265
|
+
validateStatus: () => true,
|
266
|
+
});
|
267
|
+
if (apiResponse.status >= 500) {
|
268
|
+
// simple retry with exponential backoff
|
269
|
+
const backoffs = [300, 600, 1200];
|
270
|
+
for (const ms of backoffs) {
|
271
|
+
await new Promise(r => setTimeout(r, ms));
|
272
|
+
const retry = await axios.post(targetUrl, requestData, { headers, timeout: 20000, validateStatus: () => true });
|
273
|
+
if (retry.status < 500) {
|
274
|
+
apiResponse.status = retry.status;
|
275
|
+
(apiResponse as { data: unknown }).data = retry.data;
|
276
|
+
break;
|
277
|
+
}
|
278
|
+
}
|
279
|
+
}
|
280
|
+
if (apiResponse.status >= 400) {
|
281
|
+
throw new Error(`Upstream error ${apiResponse.status}`);
|
282
|
+
}
|
283
|
+
const responseData = apiResponse.data;
|
284
|
+
// --------------------------------
|
285
|
+
|
286
|
+
// --- Extract User Prompt and AI Response for Analysis ---
|
287
|
+
console.log(chalk.cyan(`[Proxy] Extracting request/response content for analysis... | requestId=${requestId}`));
|
288
|
+
|
289
|
+
// Extract user prompt from request (Gemini format)
|
290
|
+
let userPrompt = '';
|
291
|
+
if (requestData.contents && requestData.contents[0]?.parts?.[0]?.text) {
|
292
|
+
userPrompt = requestData.contents[0].parts[0].text;
|
293
|
+
}
|
294
|
+
|
295
|
+
// Extract AI response text (Gemini format)
|
296
|
+
let aiResponseText = '';
|
297
|
+
if (responseData.candidates && responseData.candidates[0]?.content?.parts?.[0]?.text) {
|
298
|
+
aiResponseText = responseData.candidates[0].content.parts[0].text;
|
299
|
+
}
|
300
|
+
|
301
|
+
// Check if this appears to be a coding request
|
302
|
+
const isCodeRequest = /code|function|script|program|algorithm|implement/i.test(userPrompt) ||
|
303
|
+
/```.*\n[\s\S]*```/.test(aiResponseText);
|
304
|
+
|
305
|
+
// --- Dynamic Data Extraction ---
|
306
|
+
console.log(chalk.cyan(`[Proxy] Extracting interaction data... | requestId=${requestId}`));
|
307
|
+
const mapping = matchedProvider.tokenMapping;
|
308
|
+
const promptTokens = getValueFromPath(responseData, mapping.prompt);
|
309
|
+
const completionTokens = getValueFromPath(responseData, mapping.completion);
|
310
|
+
const totalTokens = getValueFromPath(responseData, mapping.total) || promptTokens + completionTokens;
|
311
|
+
|
312
|
+
const interactionData: AIInteraction = {
|
313
|
+
timestamp: new Date().toISOString(),
|
314
|
+
provider: matchedProvider.name,
|
315
|
+
model: responseData.model || 'gemini-2.5-flash',
|
316
|
+
promptTokens: promptTokens,
|
317
|
+
completionTokens: completionTokens,
|
318
|
+
totalTokens: totalTokens,
|
319
|
+
costUSD: estimateCostUSD(responseData.model || 'gemini-2.5-flash', promptTokens, completionTokens),
|
320
|
+
taskType: isCodeRequest ? 'coding' : 'chat',
|
321
|
+
};
|
322
|
+
|
323
|
+
// --- Enhanced AI Analysis (for all requests) ---
|
324
|
+
if (userPrompt && aiResponseText) {
|
325
|
+
console.log(chalk.cyan(`[Proxy] Running AI analysis pipeline... | requestId=${requestId}`));
|
326
|
+
|
327
|
+
// Store original texts for analysis
|
328
|
+
interactionData.userPrompt = userPrompt;
|
329
|
+
interactionData.aiResponse = aiResponseText;
|
330
|
+
|
331
|
+
// Run hallucination detection on all interactions
|
332
|
+
const hallucinationDetection = hallucinationDetector.detectHallucination(
|
333
|
+
userPrompt,
|
334
|
+
aiResponseText
|
335
|
+
);
|
336
|
+
|
337
|
+
// Add hallucination data to interaction (will be serialized to JSON)
|
338
|
+
interactionData.hallucinationDetection = hallucinationDetection;
|
339
|
+
|
340
|
+
console.log(chalk.cyan(`[Proxy] Hallucination detection complete - Confidence: ${hallucinationDetection.confidence}%, Likely: ${hallucinationDetection.isLikelyHallucination} | requestId=${requestId}`));
|
341
|
+
|
342
|
+
// Code Quality Analysis (if this is a coding request)
|
343
|
+
if (isCodeRequest) {
|
344
|
+
console.log(chalk.cyan(`[Proxy] Running code quality analysis... | requestId=${requestId}`));
|
345
|
+
|
346
|
+
// Extract code from response
|
347
|
+
const extractedCodeResult = extractCodeFromResponse(aiResponseText);
|
348
|
+
if (extractedCodeResult) {
|
349
|
+
interactionData.extractedCode = extractedCodeResult.code;
|
350
|
+
|
351
|
+
// Analyze code quality
|
352
|
+
const qualityMetrics = analyzeCodeQuality(extractedCodeResult.code, extractedCodeResult.language);
|
353
|
+
interactionData.codeQualityMetrics = qualityMetrics;
|
354
|
+
|
355
|
+
// Calculate overall quality score (0-100)
|
356
|
+
let qualityScore = 50; // Base
|
357
|
+
if (qualityMetrics.syntaxValid) qualityScore += 20;
|
358
|
+
qualityScore += Math.round(qualityMetrics.estimatedReadability * 2); // 0-20
|
359
|
+
if (qualityMetrics.hasFunctions || qualityMetrics.hasClasses) qualityScore += 15;
|
360
|
+
if (qualityMetrics.potentialIssues.length === 0) qualityScore += 10;
|
361
|
+
if (qualityMetrics.linesOfCode > 20) qualityScore += 5; // Substantial implementation
|
362
|
+
interactionData.codeQualityScore = Math.min(100, qualityScore);
|
363
|
+
|
364
|
+
// Score effectiveness (how well the AI understood and fulfilled the request)
|
365
|
+
const effectiveness = scoreEffectiveness(userPrompt, aiResponseText, extractedCodeResult.code);
|
366
|
+
interactionData.effectivenessScore = effectiveness.overallEffectiveness;
|
367
|
+
|
368
|
+
console.log(chalk.green(`[Proxy] Code analysis complete - Quality: ${qualityScore}/100, Effectiveness: ${effectiveness.overallEffectiveness}/100 | requestId=${requestId}`));
|
369
|
+
}
|
370
|
+
}
|
371
|
+
}
|
372
|
+
// ---------------------------
|
373
|
+
|
374
|
+
// --- Local File Logging ---
|
375
|
+
console.log(chalk.cyan(`[Proxy] Logging interaction to local file... | requestId=${requestId}`));
|
376
|
+
const logFilePath = path.resolve(process.cwd(), 'interactions.log');
|
377
|
+
const logLine = JSON.stringify({ requestId, ...interactionData });
|
378
|
+
try {
|
379
|
+
const stats = fs.existsSync(logFilePath) ? fs.statSync(logFilePath) : null;
|
380
|
+
if (stats && stats.size > 5 * 1024 * 1024) {
|
381
|
+
const rotated = `${logFilePath.replace(/\.log$/, '')}.${Date.now()}.log`;
|
382
|
+
fs.renameSync(logFilePath, rotated);
|
383
|
+
console.log(chalk.gray(`[Proxy] Rotated log to ${rotated}`));
|
384
|
+
}
|
385
|
+
} catch {}
|
386
|
+
fs.appendFileSync(logFilePath, logLine + '\n');
|
387
|
+
console.log(chalk.green(`[Proxy] Interaction successfully logged to ${logFilePath} | requestId=${requestId}`));
|
388
|
+
|
389
|
+
// --- Budgets and Alerts ---
|
390
|
+
const policy = loadPolicy(process.cwd());
|
391
|
+
if (policy) {
|
392
|
+
const monthKey = currentMonthKey();
|
393
|
+
const sums = computeMonthlySpend(logFilePath, monthKey);
|
394
|
+
const breached: string[] = [];
|
395
|
+
if (policy.monthlyUSD && sums.total > policy.monthlyUSD) {
|
396
|
+
breached.push(`total>${policy.monthlyUSD}`);
|
397
|
+
}
|
398
|
+
if (policy.perProviderMonthlyUSD) {
|
399
|
+
for (const p in policy.perProviderMonthlyUSD) {
|
400
|
+
const cap = policy.perProviderMonthlyUSD[p];
|
401
|
+
const spent = sums.byProvider[p] || 0;
|
402
|
+
if (spent > cap) breached.push(`${p}>${cap}`);
|
403
|
+
}
|
404
|
+
}
|
405
|
+
if (breached.length && policy.webhookUrl) {
|
406
|
+
await sendBudgetAlert(policy.webhookUrl, {
|
407
|
+
requestId,
|
408
|
+
monthKey,
|
409
|
+
breaches: breached,
|
410
|
+
totals: sums,
|
411
|
+
});
|
412
|
+
console.log(chalk.red(`[Proxy] Budget breach detected (${breached.join(', ')}) | requestId=${requestId}`));
|
413
|
+
}
|
414
|
+
}
|
415
|
+
// --------------------------
|
416
|
+
|
417
|
+
res.writeHead(apiResponse.status, apiResponse.headers as Record<string, string>);
|
418
|
+
res.end(JSON.stringify(responseData));
|
419
|
+
|
420
|
+
console.log(chalk.magenta(`[Proxy] Request successfully proxied and data tracked. | requestId=${requestId}`));
|
421
|
+
|
422
|
+
} catch (error: unknown) {
|
423
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
424
|
+
console.error(chalk.red(`[Proxy] Error: ${errorMessage} | requestId=${requestId}`));
|
425
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
426
|
+
res.end(JSON.stringify({ error: 'Failed to proxy request', requestId }));
|
427
|
+
}
|
428
|
+
} else {
|
429
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
430
|
+
res.end('Not Found. No matching provider route in toknxr.config.json.');
|
431
|
+
}
|
432
|
+
});
|
433
|
+
|
434
|
+
server.listen(PORT, () => {
|
435
|
+
console.log(chalk.yellow(`[Proxy] Server listening on http://localhost:${PORT}`));
|
436
|
+
console.log(chalk.yellow('Loaded providers:', providerConfig.providers.map((p: ProviderConfig['providers'][0]) => p.name).join(', ')));
|
437
|
+
});
|
438
|
+
};
|
package/src/sync.ts
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
import fs from 'fs';
|
2
|
+
import path from 'path';
|
3
|
+
import { SupabaseClient } from '@supabase/supabase-js';
|
4
|
+
import { getToken } from './auth.js';
|
5
|
+
import { setAuthToken } from './config.js';
|
6
|
+
import chalk from 'chalk';
|
7
|
+
|
8
|
+
interface InteractionLog {
|
9
|
+
timestamp: string;
|
10
|
+
provider: string;
|
11
|
+
model: string;
|
12
|
+
promptTokens?: number;
|
13
|
+
completionTokens?: number;
|
14
|
+
totalTokens: number;
|
15
|
+
costUSD: number;
|
16
|
+
taskType?: string;
|
17
|
+
userPrompt?: string;
|
18
|
+
aiResponse?: string;
|
19
|
+
extractedCode?: string;
|
20
|
+
codeQualityScore?: number;
|
21
|
+
effectivenessScore?: number;
|
22
|
+
[key: string]: any;
|
23
|
+
}
|
24
|
+
|
25
|
+
export async function syncInteractions(supabase: SupabaseClient, options: { clear?: boolean }) {
|
26
|
+
console.log(chalk.blue('Syncing local analytics with the cloud dashboard...'));
|
27
|
+
|
28
|
+
// Get the stored token
|
29
|
+
const token = await getToken();
|
30
|
+
if (!token) {
|
31
|
+
console.error(chalk.red('No authentication token found. Please login first with `toknxr login`.'));
|
32
|
+
process.exit(1);
|
33
|
+
}
|
34
|
+
|
35
|
+
// Set auth in Supabase
|
36
|
+
setAuthToken(token);
|
37
|
+
|
38
|
+
const logFilePath = path.resolve(process.cwd(), 'interactions.log');
|
39
|
+
if (!fs.existsSync(logFilePath)) {
|
40
|
+
console.log(chalk.yellow('No interactions.log file found. Nothing to sync.'));
|
41
|
+
return;
|
42
|
+
}
|
43
|
+
|
44
|
+
const fileContent = fs.readFileSync(logFilePath, 'utf8');
|
45
|
+
const lines = fileContent.trim().split('\n');
|
46
|
+
if (lines.length === 0 || (lines.length === 1 && lines[0] === '')) {
|
47
|
+
console.log(chalk.yellow('Log file is empty. Nothing to sync.'));
|
48
|
+
return;
|
49
|
+
}
|
50
|
+
|
51
|
+
const logs: InteractionLog[] = [];
|
52
|
+
for (const line of lines) {
|
53
|
+
try {
|
54
|
+
const log = JSON.parse(line);
|
55
|
+
logs.push(log);
|
56
|
+
} catch (error) {
|
57
|
+
console.warn(`Skipping invalid log entry: ${line}`);
|
58
|
+
}
|
59
|
+
}
|
60
|
+
|
61
|
+
if (logs.length === 0) {
|
62
|
+
console.log('No valid interactions to sync.');
|
63
|
+
return;
|
64
|
+
}
|
65
|
+
|
66
|
+
// Map logs to interactions
|
67
|
+
const interactions = [];
|
68
|
+
for (const log of logs) {
|
69
|
+
// Find ai_service_id by provider and model
|
70
|
+
const { data: aiService, error: aiError } = await supabase
|
71
|
+
.from('ai_services')
|
72
|
+
.select('id')
|
73
|
+
.eq('provider', log.provider)
|
74
|
+
.eq('name', log.model)
|
75
|
+
.single();
|
76
|
+
|
77
|
+
if (aiError || !aiService) {
|
78
|
+
console.warn(`AI service not found for provider: ${log.provider}, model: ${log.model}`);
|
79
|
+
continue;
|
80
|
+
}
|
81
|
+
|
82
|
+
// Assume project_id - for now, use a default or query user's projects
|
83
|
+
// TODO: Add project selection or default project
|
84
|
+
const { data: projects, error: projError } = await supabase
|
85
|
+
.from('projects')
|
86
|
+
.select('id')
|
87
|
+
.limit(1);
|
88
|
+
|
89
|
+
if (projError || !projects || projects.length === 0) {
|
90
|
+
console.error('No projects found for user.');
|
91
|
+
continue;
|
92
|
+
}
|
93
|
+
|
94
|
+
const projectId = projects[0].id;
|
95
|
+
|
96
|
+
const interaction = {
|
97
|
+
project_id: projectId,
|
98
|
+
ai_service_id: aiService.id,
|
99
|
+
tokens_used: log.totalTokens,
|
100
|
+
cost_in_cents: Math.round(log.costUSD * 100),
|
101
|
+
timestamp: new Date(log.timestamp).toISOString(),
|
102
|
+
request_details: log.userPrompt || '',
|
103
|
+
response_details: log.aiResponse || log.extractedCode || '',
|
104
|
+
};
|
105
|
+
|
106
|
+
interactions.push(interaction);
|
107
|
+
}
|
108
|
+
|
109
|
+
if (interactions.length === 0) {
|
110
|
+
console.log('No interactions to insert.');
|
111
|
+
return;
|
112
|
+
}
|
113
|
+
|
114
|
+
// Insert interactions
|
115
|
+
const { error: insertError } = await supabase
|
116
|
+
.from('interactions')
|
117
|
+
.insert(interactions);
|
118
|
+
|
119
|
+
if (insertError) {
|
120
|
+
console.error('Error syncing interactions:', insertError);
|
121
|
+
} else {
|
122
|
+
console.log(`Successfully synced ${interactions.length} interactions.`);
|
123
|
+
}
|
124
|
+
|
125
|
+
if (options.clear) {
|
126
|
+
fs.writeFileSync(logFilePath, '');
|
127
|
+
console.log(chalk.gray('Local interactions.log has been cleared.'));
|
128
|
+
}
|
129
|
+
}
|
package/start.sh
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
echo "🚀 TokNXR - One Command AI Analytics Setup"
|
4
|
+
echo "============================================="
|
5
|
+
|
6
|
+
# Check if we're in the right directory
|
7
|
+
if [ ! -f "package.json" ] || [ ! -f "src/cli.ts" ]; then
|
8
|
+
echo "❌ Error: Please run this script from the toknxr-cli directory"
|
9
|
+
exit 1
|
10
|
+
fi
|
11
|
+
|
12
|
+
echo "📦 Setting up TokNXR with all providers..."
|
13
|
+
|
14
|
+
# Run the setup
|
15
|
+
npm run setup
|
16
|
+
|
17
|
+
# Create .env file with demo keys for immediate testing
|
18
|
+
if [ ! -f ".env" ]; then
|
19
|
+
echo "🔑 Creating .env file with demo configuration..."
|
20
|
+
cat > .env << 'EOF'
|
21
|
+
# TokNXR Environment Variables
|
22
|
+
# Add your real API keys here for full functionality
|
23
|
+
|
24
|
+
# Demo mode - Replace with your actual keys
|
25
|
+
GEMINI_API_KEY=demo_key_for_testing
|
26
|
+
OPENAI_API_KEY=demo_key_for_testing
|
27
|
+
ANTHROPIC_API_KEY=demo_key_for_testing
|
28
|
+
|
29
|
+
# Optional: Webhook for budget alerts (leave empty for now)
|
30
|
+
WEBHOOK_URL=
|
31
|
+
|
32
|
+
# Optional: Custom port (default: 8788)
|
33
|
+
# PORT=8788
|
34
|
+
EOF
|
35
|
+
echo "✅ Created .env file (please add your real API keys)"
|
36
|
+
else
|
37
|
+
echo "✅ .env file already exists"
|
38
|
+
fi
|
39
|
+
|
40
|
+
echo ""
|
41
|
+
echo "🎉 Setup complete! Starting TokNXR..."
|
42
|
+
echo ""
|
43
|
+
echo "📊 Dashboard will be available at: http://localhost:8788/dashboard"
|
44
|
+
echo "🔗 API endpoints:"
|
45
|
+
echo " • Gemini-Free: http://localhost:8788/gemini-free"
|
46
|
+
echo " • Gemini-Pro: http://localhost:8788/gemini"
|
47
|
+
echo " • OpenAI: http://localhost:8788/openai"
|
48
|
+
echo " • Claude: http://localhost:8788/anthropic"
|
49
|
+
echo " • Ollama: http://localhost:8788/ollama"
|
50
|
+
echo ""
|
51
|
+
echo "💡 To stop the server: Ctrl+C"
|
52
|
+
echo "🔄 To restart later: npm start"
|
53
|
+
echo ""
|
54
|
+
|
55
|
+
# Start the server
|
56
|
+
npm start
|