@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/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