@goldensheepai/toknxr-cli 0.2.0 → 0.3.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/README.md +270 -9
- package/lib/audit-logger.js +500 -0
- package/lib/cli.js +1850 -129
- package/lib/cli.test.js +49 -0
- package/lib/code-analysis.js +349 -4
- package/lib/dashboard.js +4 -17
- package/lib/fixtures/canary-interaction.js +18 -0
- package/lib/plugin-system.js +266 -0
- package/lib/sync.js +27 -5
- package/lib/ui.js +129 -0
- package/lib/utils.js +117 -0
- package/package.json +51 -18
- package/.env +0 -21
- package/.env.example +0 -21
- package/interactions.log +0 -8
- package/src/ai-analytics.ts +0 -418
- package/src/auth.ts +0 -80
- package/src/cli.ts +0 -447
- package/src/code-analysis.ts +0 -365
- package/src/config.ts +0 -10
- package/src/dashboard.tsx +0 -391
- package/src/hallucination-detector.ts +0 -368
- package/src/policy.ts +0 -55
- package/src/pricing.ts +0 -21
- package/src/proxy.ts +0 -438
- package/src/sync.ts +0 -129
- package/start.sh +0 -56
- package/test-analysis.mjs +0 -77
- package/test-coding.mjs +0 -27
- package/test-generate-sample-data.js +0 -118
- package/test-proxy.mjs +0 -25
- package/toknxr.config.json +0 -63
- package/toknxr.policy.json +0 -18
- package/tsconfig.json +0 -19
@@ -0,0 +1,266 @@
|
|
1
|
+
/**
|
2
|
+
* Plugin System for TokNxr Code Analysis
|
3
|
+
* Allows extensible analysis capabilities for custom analyzers
|
4
|
+
*/
|
5
|
+
/**
|
6
|
+
* Plugin registry and management
|
7
|
+
*/
|
8
|
+
export class PluginManager {
|
9
|
+
constructor() {
|
10
|
+
this.plugins = new Map();
|
11
|
+
}
|
12
|
+
/**
|
13
|
+
* Register a new plugin
|
14
|
+
*/
|
15
|
+
register(plugin) {
|
16
|
+
this.plugins.set(plugin.name, plugin);
|
17
|
+
}
|
18
|
+
/**
|
19
|
+
* Unregister a plugin
|
20
|
+
*/
|
21
|
+
unregister(name) {
|
22
|
+
return this.plugins.delete(name);
|
23
|
+
}
|
24
|
+
/**
|
25
|
+
* Get all registered plugins
|
26
|
+
*/
|
27
|
+
getPlugins() {
|
28
|
+
return Array.from(this.plugins.values());
|
29
|
+
}
|
30
|
+
/**
|
31
|
+
* Get plugins that support a specific language
|
32
|
+
*/
|
33
|
+
getPluginsForLanguage(language) {
|
34
|
+
return Array.from(this.plugins.values())
|
35
|
+
.filter(plugin => plugin.supportedLanguages.includes(language));
|
36
|
+
}
|
37
|
+
/**
|
38
|
+
* Analyze code using all applicable plugins
|
39
|
+
*/
|
40
|
+
analyzeWithPlugins(code, baseMetrics, plugins) {
|
41
|
+
const applicablePlugins = plugins || this.getPluginsForLanguage(baseMetrics.language || 'unknown');
|
42
|
+
// Apply plugin analyses
|
43
|
+
for (const plugin of applicablePlugins) {
|
44
|
+
if (plugin.analyzeQuality && plugin.validateSyntax && plugin.calculateReadability) {
|
45
|
+
try {
|
46
|
+
const pluginResults = plugin.analyzeQuality(code, baseMetrics.language || 'unknown');
|
47
|
+
// Merge plugin results
|
48
|
+
if (pluginResults.pluginMetrics) {
|
49
|
+
baseMetrics.pluginMetrics = {
|
50
|
+
...baseMetrics.pluginMetrics,
|
51
|
+
[plugin.name]: pluginResults.pluginMetrics
|
52
|
+
};
|
53
|
+
}
|
54
|
+
// Override base metrics if plugin provides better validation
|
55
|
+
if (pluginResults.syntaxValid !== undefined) {
|
56
|
+
const pluginValid = plugin.validateSyntax(code, baseMetrics.language || 'unknown');
|
57
|
+
if (!pluginValid && baseMetrics.syntaxValid) {
|
58
|
+
// Plugin detected issues that base validation missed
|
59
|
+
baseMetrics.syntaxValid = false;
|
60
|
+
baseMetrics.potentialIssues.push(`${plugin.name}: syntax validation failed`);
|
61
|
+
}
|
62
|
+
}
|
63
|
+
// Enhance readability if plugin provides better calculation
|
64
|
+
if (pluginResults.estimatedReadability !== undefined) {
|
65
|
+
const pluginReadability = plugin.calculateReadability(code, baseMetrics.language || 'unknown');
|
66
|
+
// Use the better (higher) readability score
|
67
|
+
if (pluginReadability > baseMetrics.estimatedReadability) {
|
68
|
+
baseMetrics.estimatedReadability = pluginReadability;
|
69
|
+
}
|
70
|
+
}
|
71
|
+
// Add potential issues from plugin
|
72
|
+
if (pluginResults.potentialIssues && pluginResults.potentialIssues.length > 0) {
|
73
|
+
baseMetrics.potentialIssues.push(...pluginResults.potentialIssues.map(issue => `${plugin.name}: ${issue}`));
|
74
|
+
}
|
75
|
+
}
|
76
|
+
catch (error) {
|
77
|
+
console.warn(`Plugin ${plugin.name} failed during analysis:`, error);
|
78
|
+
}
|
79
|
+
}
|
80
|
+
}
|
81
|
+
return baseMetrics;
|
82
|
+
}
|
83
|
+
/**
|
84
|
+
* Score effectiveness using plugins
|
85
|
+
*/
|
86
|
+
scoreEffectivenessWithPlugins(userPrompt, aiResponse, baseScore, extractedCode, plugins) {
|
87
|
+
const code = extractedCode || aiResponse;
|
88
|
+
const codeMetrics = { language: this.detectLanguageFromCode(code) };
|
89
|
+
const applicablePlugins = plugins || this.getPluginsForLanguage(codeMetrics.language || 'unknown');
|
90
|
+
// Apply plugin effectiveness analysis
|
91
|
+
for (const plugin of applicablePlugins) {
|
92
|
+
if (plugin.analyzeEffectiveness) {
|
93
|
+
try {
|
94
|
+
const pluginResults = plugin.analyzeEffectiveness(userPrompt, aiResponse, extractedCode);
|
95
|
+
// Apply hallucination detection
|
96
|
+
if (plugin.detectHallucinations) {
|
97
|
+
const hallucinations = plugin.detectHallucinations(code, codeMetrics.language || 'unknown');
|
98
|
+
if (hallucinations.length > 0) {
|
99
|
+
const avgConfidence = hallucinations.reduce((sum, h) => sum + h.confidence, 0) / hallucinations.length;
|
100
|
+
baseScore.hallucinationRisk = (baseScore.hallucinationRisk || 0) + (avgConfidence * 0.3);
|
101
|
+
}
|
102
|
+
}
|
103
|
+
// Merge other effectiveness metrics
|
104
|
+
Object.assign(baseScore, pluginResults);
|
105
|
+
}
|
106
|
+
catch (error) {
|
107
|
+
console.warn(`Plugin ${plugin.name} failed during effectiveness scoring:`, error);
|
108
|
+
}
|
109
|
+
}
|
110
|
+
}
|
111
|
+
return baseScore;
|
112
|
+
}
|
113
|
+
/**
|
114
|
+
* Detect language from code (helper method)
|
115
|
+
*/
|
116
|
+
detectLanguageFromCode(code) {
|
117
|
+
if (/(?:import|export|function|const|let|var)\s+/.test(code)) {
|
118
|
+
return code.includes('interface') || code.includes(': string') ? 'typescript' : 'javascript';
|
119
|
+
}
|
120
|
+
if (/package\s+\w+|func\s+\w+\(/.test(code))
|
121
|
+
return 'go';
|
122
|
+
if (/fn\s+\w+|use\s+std::/.test(code))
|
123
|
+
return 'rust';
|
124
|
+
if (/public\s+class|import\s+java\./.test(code))
|
125
|
+
return 'java';
|
126
|
+
if (/#include\s+<|std::|cout\s+<<|int\s+main\(/.test(code))
|
127
|
+
return 'cpp';
|
128
|
+
return undefined;
|
129
|
+
}
|
130
|
+
}
|
131
|
+
/**
|
132
|
+
* Built-in security analyzer plugin
|
133
|
+
*/
|
134
|
+
export const SecurityAnalyzerPlugin = {
|
135
|
+
name: 'security-analyzer',
|
136
|
+
version: '1.0.0',
|
137
|
+
description: 'Analyzes code for security vulnerabilities',
|
138
|
+
supportedLanguages: ['javascript', 'typescript', 'python', 'go', 'rust', 'java', 'cpp'],
|
139
|
+
analyzeQuality(code, language) {
|
140
|
+
const issues = [];
|
141
|
+
let securityScore = 100;
|
142
|
+
// Common security issues across languages
|
143
|
+
if (code.includes('eval(') || code.includes('Function(')) {
|
144
|
+
issues.push('Use of eval() or Function() constructor - potential code injection');
|
145
|
+
securityScore -= 30;
|
146
|
+
}
|
147
|
+
if (code.includes('innerHTML') || code.includes('outerHTML')) {
|
148
|
+
issues.push('Direct HTML manipulation - potential XSS vulnerability');
|
149
|
+
securityScore -= 20;
|
150
|
+
}
|
151
|
+
if (/(password|secret|key).*=\s*["'][^"']*["']/.test(code)) {
|
152
|
+
issues.push('Hardcoded credentials detected');
|
153
|
+
securityScore -= 40;
|
154
|
+
}
|
155
|
+
// Language-specific checks
|
156
|
+
switch (language) {
|
157
|
+
case 'javascript':
|
158
|
+
case 'typescript':
|
159
|
+
if (code.includes('document.cookie')) {
|
160
|
+
issues.push('Direct cookie manipulation without secure flags');
|
161
|
+
securityScore -= 15;
|
162
|
+
}
|
163
|
+
break;
|
164
|
+
case 'python':
|
165
|
+
if (code.includes('exec(') || code.includes('eval(')) {
|
166
|
+
issues.push('Use of exec() or eval() in Python - code injection risk');
|
167
|
+
securityScore -= 30;
|
168
|
+
}
|
169
|
+
break;
|
170
|
+
}
|
171
|
+
return {
|
172
|
+
potentialIssues: issues,
|
173
|
+
securityScore: Math.max(0, securityScore)
|
174
|
+
};
|
175
|
+
},
|
176
|
+
detectHallucinations(code, language) {
|
177
|
+
const detections = [];
|
178
|
+
// Check for obviously incorrect API usage patterns
|
179
|
+
if (language === 'javascript' && code.includes('fs.readFile') && !code.includes('import fs')) {
|
180
|
+
detections.push({
|
181
|
+
confidence: 85,
|
182
|
+
type: 'missing_import',
|
183
|
+
message: 'Using fs.readFile without importing fs module'
|
184
|
+
});
|
185
|
+
}
|
186
|
+
if (language === 'python' && code.includes('import requests') && code.includes('requests.get') && !code.includes('response.raise_for_status()')) {
|
187
|
+
detections.push({
|
188
|
+
confidence: 60,
|
189
|
+
type: 'missing_error_handling',
|
190
|
+
message: 'HTTP request without proper error handling'
|
191
|
+
});
|
192
|
+
}
|
193
|
+
return detections;
|
194
|
+
}
|
195
|
+
};
|
196
|
+
/**
|
197
|
+
* Performance analyzer plugin
|
198
|
+
*/
|
199
|
+
export const PerformanceAnalyzerPlugin = {
|
200
|
+
name: 'performance-analyzer',
|
201
|
+
version: '1.0.0',
|
202
|
+
description: 'Analyzes code for performance bottlenecks',
|
203
|
+
supportedLanguages: ['javascript', 'typescript', 'python', 'go', 'rust', 'java', 'cpp'],
|
204
|
+
analyzeQuality(code, language) {
|
205
|
+
const issues = [];
|
206
|
+
let performanceScore = 100;
|
207
|
+
// Detect performance anti-patterns
|
208
|
+
if (/(for.*for|while.*while)/.test(code)) {
|
209
|
+
issues.push('Nested loops detected - potential O(n²) complexity');
|
210
|
+
performanceScore -= 15;
|
211
|
+
}
|
212
|
+
// Memory leaks in JavaScript
|
213
|
+
if (/(setInterval|setTimeout)/.test(code) && !code.includes('clearInterval') && !code.includes('clearTimeout')) {
|
214
|
+
issues.push('Timer without cleanup - potential memory leak');
|
215
|
+
performanceScore -= 20;
|
216
|
+
}
|
217
|
+
// Inefficient string concatenation
|
218
|
+
if (/(\w+\s*\+\s*\w+.*){3,}/.test(code) && !code.includes('join(')) {
|
219
|
+
issues.push('Inefficient string concatenation - consider using array join');
|
220
|
+
performanceScore -= 10;
|
221
|
+
}
|
222
|
+
return {
|
223
|
+
potentialIssues: issues,
|
224
|
+
performanceScore: Math.max(0, performanceScore)
|
225
|
+
};
|
226
|
+
}
|
227
|
+
};
|
228
|
+
/**
|
229
|
+
* Framework detector plugin
|
230
|
+
*/
|
231
|
+
export const FrameworkDetectorPlugin = {
|
232
|
+
name: 'framework-detector',
|
233
|
+
version: '1.0.0',
|
234
|
+
description: 'Detects and analyzes framework-specific code',
|
235
|
+
supportedLanguages: ['javascript', 'typescript', 'python'],
|
236
|
+
analyzeQuality(code, language) {
|
237
|
+
let framework;
|
238
|
+
if (code.includes('import React') || code.includes('from "react"')) {
|
239
|
+
framework = 'react';
|
240
|
+
}
|
241
|
+
else if (code.includes('import Vue') || code.includes('from "vue"')) {
|
242
|
+
framework = 'vue';
|
243
|
+
}
|
244
|
+
else if (code.includes('import { Component }') && code.includes('@Component')) {
|
245
|
+
framework = 'angular';
|
246
|
+
}
|
247
|
+
else if (code.includes('from flask import') || code.includes('import flask')) {
|
248
|
+
framework = 'flask';
|
249
|
+
}
|
250
|
+
else if (code.includes('from django') || code.includes('import django')) {
|
251
|
+
framework = 'django';
|
252
|
+
}
|
253
|
+
else if (code.includes('from fastapi import') || code.includes('import fastapi')) {
|
254
|
+
framework = 'fastapi';
|
255
|
+
}
|
256
|
+
return {
|
257
|
+
framework
|
258
|
+
};
|
259
|
+
}
|
260
|
+
};
|
261
|
+
// Export default plugin manager instance
|
262
|
+
export const pluginManager = new PluginManager();
|
263
|
+
// Register built-in plugins
|
264
|
+
pluginManager.register(SecurityAnalyzerPlugin);
|
265
|
+
pluginManager.register(PerformanceAnalyzerPlugin);
|
266
|
+
pluginManager.register(FrameworkDetectorPlugin);
|
package/lib/sync.js
CHANGED
@@ -13,9 +13,16 @@ export async function syncInteractions(supabase, options) {
|
|
13
13
|
}
|
14
14
|
// Set auth in Supabase
|
15
15
|
setAuthToken(token);
|
16
|
-
|
16
|
+
// Resolve interactions.log robustly across run contexts (repo root vs toknxr-cli)
|
17
|
+
const candidatePaths = [
|
18
|
+
path.resolve(process.cwd(), 'interactions.log'),
|
19
|
+
path.resolve(process.cwd(), 'toknxr-cli', 'interactions.log'),
|
20
|
+
// When executed from built JS in lib/, __dirname points to lib; go up one
|
21
|
+
path.resolve(__dirname, '../interactions.log'),
|
22
|
+
];
|
23
|
+
const logFilePath = candidatePaths.find(p => fs.existsSync(p)) || candidatePaths[0];
|
17
24
|
if (!fs.existsSync(logFilePath)) {
|
18
|
-
console.log(chalk.yellow('No interactions.log file found
|
25
|
+
console.log(chalk.yellow('No interactions.log file found in current directory or toknxr-cli/. Nothing to sync.'));
|
19
26
|
return;
|
20
27
|
}
|
21
28
|
const fileContent = fs.readFileSync(logFilePath, 'utf8');
|
@@ -30,7 +37,7 @@ export async function syncInteractions(supabase, options) {
|
|
30
37
|
const log = JSON.parse(line);
|
31
38
|
logs.push(log);
|
32
39
|
}
|
33
|
-
catch
|
40
|
+
catch {
|
34
41
|
console.warn(`Skipping invalid log entry: ${line}`);
|
35
42
|
}
|
36
43
|
}
|
@@ -71,6 +78,16 @@ export async function syncInteractions(supabase, options) {
|
|
71
78
|
timestamp: new Date(log.timestamp).toISOString(),
|
72
79
|
request_details: log.userPrompt || '',
|
73
80
|
response_details: log.aiResponse || log.extractedCode || '',
|
81
|
+
// Rich metrics from CLI analysis
|
82
|
+
code_quality_score: log.codeQualityScore,
|
83
|
+
effectiveness_score: log.effectivenessScore,
|
84
|
+
language: log.codeQualityMetrics?.language,
|
85
|
+
syntax_valid: log.codeQualityMetrics?.syntaxValid,
|
86
|
+
readability_score: log.codeQualityMetrics?.estimatedReadability,
|
87
|
+
hallucination_detected: log.hallucinationDetection?.isLikelyHallucination,
|
88
|
+
hallucination_confidence: log.hallucinationDetection?.confidence,
|
89
|
+
prompt_clarity_match: log.hallucinationDetection?.promptClarityMatch,
|
90
|
+
task_type: log.taskType,
|
74
91
|
};
|
75
92
|
interactions.push(interaction);
|
76
93
|
}
|
@@ -89,7 +106,12 @@ export async function syncInteractions(supabase, options) {
|
|
89
106
|
console.log(`Successfully synced ${interactions.length} interactions.`);
|
90
107
|
}
|
91
108
|
if (options.clear) {
|
92
|
-
|
93
|
-
|
109
|
+
try {
|
110
|
+
fs.writeFileSync(logFilePath, '');
|
111
|
+
console.log(chalk.gray('Local interactions.log has been cleared.'));
|
112
|
+
}
|
113
|
+
catch {
|
114
|
+
console.log(chalk.yellow('Could not clear interactions.log. Proceeding without clearing.'));
|
115
|
+
}
|
94
116
|
}
|
95
117
|
}
|
package/lib/ui.js
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
import chalk from 'chalk';
|
2
|
+
import inquirer from 'inquirer';
|
3
|
+
export const createStatsOverview = (cost, requests, waste, hallucinations) => `
|
4
|
+
${chalk.bold.blue('📊 TokNXR Analytics Overview')}
|
5
|
+
${chalk.gray('------------------------------------')}
|
6
|
+
${chalk.cyan('Total Cost:')} ${chalk.green(`$${cost.toFixed(2)}`)}
|
7
|
+
${chalk.cyan('Total Requests:')} ${chalk.green(requests)}
|
8
|
+
${chalk.cyan('Estimated Waste:')} ${chalk.yellow(`${waste.toFixed(2)}%`)}
|
9
|
+
${chalk.cyan('Hallucination Rate:')} ${chalk.red(`${hallucinations.toFixed(2)}%`)}
|
10
|
+
`;
|
11
|
+
export const createProviderTable = async (stats) => {
|
12
|
+
let table = `${chalk.bold.blue('🤖 Provider Performance')}\n`;
|
13
|
+
table += `${chalk.gray('----------------------------------------------------------------')}\n`;
|
14
|
+
table += `${chalk.bold('Provider')} | ${chalk.bold('Tokens')} | ${chalk.bold('Cost')} | ${chalk.bold('Quality')} | ${chalk.bold('Effectiveness')}\n`;
|
15
|
+
table += `${chalk.gray('----------------------------------------------------------------')}\n`;
|
16
|
+
for (const provider in stats) {
|
17
|
+
const { totalTokens, costUSD, avgQualityScore, avgEffectivenessScore } = stats[provider];
|
18
|
+
table += `${provider.padEnd(15)} | ${totalTokens.toLocaleString().padEnd(10)} | ${`$${costUSD.toFixed(2)}`.padEnd(8)} | ${`${avgQualityScore}/100`.padEnd(10)} | ${`${avgEffectivenessScore}/100`}\n`;
|
19
|
+
}
|
20
|
+
return table;
|
21
|
+
};
|
22
|
+
export const createQualityBreakdown = (interactions) => {
|
23
|
+
const qualityScores = interactions.map(i => i.codeQualityScore || 0);
|
24
|
+
const averageQuality = qualityScores.reduce((a, b) => a + b, 0) / qualityScores.length;
|
25
|
+
return `
|
26
|
+
${chalk.bold.magenta('🔍 Code Quality Breakdown')}
|
27
|
+
${chalk.gray('------------------------------------')}
|
28
|
+
${chalk.cyan('Average Quality Score:')} ${chalk.green(`${averageQuality.toFixed(2)}/100`)}
|
29
|
+
`;
|
30
|
+
};
|
31
|
+
export const createOperationProgress = (title, steps) => {
|
32
|
+
let currentStep = 0;
|
33
|
+
const spinner = {
|
34
|
+
updateProgress: (step) => {
|
35
|
+
currentStep = step;
|
36
|
+
console.log(`${chalk.blue(`[${currentStep + 1}/${steps.length}]`)} ${steps[currentStep]}`);
|
37
|
+
},
|
38
|
+
succeed: (message) => {
|
39
|
+
console.log(chalk.green(message));
|
40
|
+
},
|
41
|
+
fail: (message) => {
|
42
|
+
console.log(chalk.red(message));
|
43
|
+
},
|
44
|
+
};
|
45
|
+
console.log(chalk.bold.blue(title));
|
46
|
+
return spinner;
|
47
|
+
};
|
48
|
+
export const createInteractiveMenu = async (options) => {
|
49
|
+
const { choice } = await inquirer.prompt([
|
50
|
+
{
|
51
|
+
type: 'list',
|
52
|
+
name: 'choice',
|
53
|
+
message: 'Select an operation:',
|
54
|
+
choices: options,
|
55
|
+
},
|
56
|
+
]);
|
57
|
+
return choice;
|
58
|
+
};
|
59
|
+
export const createBox = (title, content, options) => {
|
60
|
+
const { borderColor, titleColor } = options;
|
61
|
+
const color = chalk[borderColor] || chalk.gray;
|
62
|
+
const titleColored = chalk[titleColor] || chalk.white;
|
63
|
+
let box = color('┌' + '─'.repeat(title.length + 2) + '┐\n');
|
64
|
+
box += color('│ ') + titleColored(title) + color(' │\n');
|
65
|
+
box += color('├' + '─'.repeat(title.length + 2) + '┤\n');
|
66
|
+
content.forEach(line => {
|
67
|
+
box +=
|
68
|
+
color('│ ') +
|
69
|
+
line +
|
70
|
+
color(' '.repeat(Math.max(0, title.length - line.length + 2))) +
|
71
|
+
color(' │\n');
|
72
|
+
});
|
73
|
+
box += color('└' + '─'.repeat(title.length + 2) + '┘');
|
74
|
+
return box;
|
75
|
+
};
|
76
|
+
export const createCostChart = (data) => {
|
77
|
+
// Basic chart for demonstration
|
78
|
+
let chart = `${chalk.bold.blue('Cost Trend (Last 7 Days)')}\n`;
|
79
|
+
const max = Math.max(...data);
|
80
|
+
data.forEach(value => {
|
81
|
+
const bar = '█'.repeat(Math.round((value / max) * 20));
|
82
|
+
chart += `${chalk.green(bar)}\n`;
|
83
|
+
});
|
84
|
+
return chart;
|
85
|
+
};
|
86
|
+
export const createPaginatedDisplay = (data, pageSize, currentPage, render, title) => {
|
87
|
+
let display = title ? `${chalk.bold.blue(title)}\n` : '';
|
88
|
+
data.forEach((item, index) => {
|
89
|
+
display += render(item, (currentPage - 1) * pageSize + index);
|
90
|
+
});
|
91
|
+
return display;
|
92
|
+
};
|
93
|
+
export const createFilterInterface = (currentFilters) => {
|
94
|
+
return new Promise(resolve => {
|
95
|
+
console.log('Filtering interface is not implemented in this mock.');
|
96
|
+
resolve(currentFilters);
|
97
|
+
});
|
98
|
+
};
|
99
|
+
export const createSearchInterface = (fields) => {
|
100
|
+
return new Promise(resolve => {
|
101
|
+
console.log('Search interface is not implemented in this mock.');
|
102
|
+
resolve({ query: 'test', fields });
|
103
|
+
});
|
104
|
+
};
|
105
|
+
export class InteractiveDataExplorer {
|
106
|
+
constructor(data) {
|
107
|
+
this.data = data;
|
108
|
+
}
|
109
|
+
setPageSize(_size) { } // Mock implementation
|
110
|
+
setPage(_page) { } // Mock implementation
|
111
|
+
getCurrentPageData() {
|
112
|
+
return this.data;
|
113
|
+
}
|
114
|
+
getPaginationInfo() {
|
115
|
+
return { totalItems: this.data.length, totalPages: 1 };
|
116
|
+
}
|
117
|
+
}
|
118
|
+
export class CliStateManager {
|
119
|
+
static getPreferences() {
|
120
|
+
return {};
|
121
|
+
}
|
122
|
+
static updateSessionBudget(_amount, _provider) { } // Mock implementation
|
123
|
+
static loadState() {
|
124
|
+
return { budgets: {} };
|
125
|
+
}
|
126
|
+
}
|
127
|
+
export const filterAndSearchInteractions = (interactions, _filters, _search) => {
|
128
|
+
return interactions;
|
129
|
+
};
|
package/lib/utils.js
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
import axios from 'axios';
|
2
|
+
import * as fs from 'node:fs';
|
3
|
+
// Helper to resolve dot notation paths (copied from proxy.ts for consistency)
|
4
|
+
const getValueFromPath = (obj, path) => {
|
5
|
+
if (!path || !obj)
|
6
|
+
return 0;
|
7
|
+
try {
|
8
|
+
const result = path.split('.').reduce((res, prop) => res && typeof res === 'object' && prop in res ? res[prop] : undefined, obj);
|
9
|
+
return Number(result) || 0;
|
10
|
+
}
|
11
|
+
catch {
|
12
|
+
return 0;
|
13
|
+
}
|
14
|
+
};
|
15
|
+
// Minimal payload for testing various providers
|
16
|
+
const TEST_PAYLOADS = {
|
17
|
+
'gemini': {
|
18
|
+
contents: [{ parts: [{ text: "Hello" }] }]
|
19
|
+
},
|
20
|
+
'openai': {
|
21
|
+
model: "gpt-3.5-turbo",
|
22
|
+
messages: [{ role: "user", content: "Hello" }]
|
23
|
+
},
|
24
|
+
'anthropic': {
|
25
|
+
model: "claude-3-opus-20240229",
|
26
|
+
messages: [{ role: "user", content: "Hello" }],
|
27
|
+
max_tokens: 10
|
28
|
+
},
|
29
|
+
'ollama': {
|
30
|
+
model: "llama3",
|
31
|
+
prompt: "Hello",
|
32
|
+
stream: false
|
33
|
+
}
|
34
|
+
};
|
35
|
+
export async function testConnection(provider, apiKey) {
|
36
|
+
const proxyUrl = `http://localhost:8788${provider.routePrefix}`;
|
37
|
+
const testPayload = TEST_PAYLOADS[provider.name.toLowerCase().split('-')[0]] || TEST_PAYLOADS['gemini']; // Default to gemini payload
|
38
|
+
if (!testPayload) {
|
39
|
+
return { ok: false, message: `No test payload defined for provider: ${provider.name}` };
|
40
|
+
}
|
41
|
+
try {
|
42
|
+
const headers = { 'Content-Type': 'application/json' };
|
43
|
+
// The proxy handles adding the actual API key to the upstream request
|
44
|
+
// We just need to ensure the proxy itself is reachable and responds
|
45
|
+
// For the test, we don't need to pass the API key here, as the proxy will add it.
|
46
|
+
// However, if the proxy itself fails due to missing key, that's what we want to catch.
|
47
|
+
const res = await axios.post(proxyUrl, testPayload, {
|
48
|
+
headers,
|
49
|
+
timeout: 5000, // 5 second timeout
|
50
|
+
validateStatus: () => true, // Don't throw on non-2xx status codes
|
51
|
+
});
|
52
|
+
if (res.status === 200) {
|
53
|
+
// Basic check for a valid response structure
|
54
|
+
if (res.data && (res.data.candidates || res.data.choices || res.data.response)) {
|
55
|
+
return { ok: true };
|
56
|
+
}
|
57
|
+
else {
|
58
|
+
return { ok: false, message: `Unexpected response format from ${provider.name}` };
|
59
|
+
}
|
60
|
+
}
|
61
|
+
else if (res.status === 401 || res.status === 403) {
|
62
|
+
return { ok: false, message: `Authentication failed (Status: ${res.status}). Check API key.` };
|
63
|
+
}
|
64
|
+
else if (res.status === 429) {
|
65
|
+
return { ok: false, message: `Rate limit exceeded (Status: ${res.status}).` };
|
66
|
+
}
|
67
|
+
else if (res.status === 500 && res.data && res.data.error) {
|
68
|
+
// Catch specific proxy errors, e.g., "API key not set"
|
69
|
+
return { ok: false, message: `Proxy error: ${res.data.error}` };
|
70
|
+
}
|
71
|
+
else {
|
72
|
+
return { ok: false, message: `Proxy returned status ${res.status}: ${JSON.stringify(res.data)}` };
|
73
|
+
}
|
74
|
+
}
|
75
|
+
catch (error) {
|
76
|
+
if (axios.isAxiosError(error)) {
|
77
|
+
if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
|
78
|
+
return { ok: false, message: `Proxy not running or unreachable at ${proxyUrl}.` };
|
79
|
+
}
|
80
|
+
else if (error.response) {
|
81
|
+
return { ok: false, message: `Proxy returned status ${error.response.status}: ${JSON.stringify(error.response.data)}` };
|
82
|
+
}
|
83
|
+
return { ok: false, message: `Network error: ${error.message}` };
|
84
|
+
}
|
85
|
+
return { ok: false, message: `Unknown error during connection test: ${error.message}` };
|
86
|
+
}
|
87
|
+
}
|
88
|
+
export function generateSampleInteraction(providerName, logFilePath) {
|
89
|
+
const sampleInteraction = {
|
90
|
+
requestId: `sample-${Date.now()}`,
|
91
|
+
timestamp: new Date().toISOString(),
|
92
|
+
provider: providerName,
|
93
|
+
model: `${providerName}-test-model`,
|
94
|
+
promptTokens: 10,
|
95
|
+
completionTokens: 20,
|
96
|
+
totalTokens: 30,
|
97
|
+
costUSD: 0.0005,
|
98
|
+
taskType: 'chat',
|
99
|
+
userPrompt: 'Generate a sample interaction for testing.',
|
100
|
+
aiResponse: 'This is a sample AI response generated by the doctor command.',
|
101
|
+
codeQualityScore: 85,
|
102
|
+
effectivenessScore: 90,
|
103
|
+
hallucinationDetection: {
|
104
|
+
isLikelyHallucination: false,
|
105
|
+
confidence: 5,
|
106
|
+
severity: 'low',
|
107
|
+
issues: []
|
108
|
+
}
|
109
|
+
};
|
110
|
+
try {
|
111
|
+
fs.appendFileSync(logFilePath, JSON.stringify(sampleInteraction) + '\n');
|
112
|
+
return { ok: true, message: `Generated sample interaction for ${providerName}.` };
|
113
|
+
}
|
114
|
+
catch (error) {
|
115
|
+
return { ok: false, message: `Failed to write sample interaction: ${error.message}` };
|
116
|
+
}
|
117
|
+
}
|
package/package.json
CHANGED
@@ -1,38 +1,71 @@
|
|
1
1
|
{
|
2
2
|
"name": "@goldensheepai/toknxr-cli",
|
3
|
-
"version": "0.
|
4
|
-
"
|
3
|
+
"version": "0.3.0",
|
4
|
+
"description": "CLI-powered AI effectiveness & code quality analysis tool - 100% Local",
|
5
|
+
"license": "MIT",
|
5
6
|
"type": "module",
|
7
|
+
"private": false,
|
6
8
|
"bin": {
|
7
9
|
"toknxr": "lib/cli.js"
|
8
10
|
},
|
11
|
+
"main": "lib/cli.js",
|
12
|
+
"files": [
|
13
|
+
"lib/**/*",
|
14
|
+
"package.json",
|
15
|
+
"README.md",
|
16
|
+
"LICENSE"
|
17
|
+
],
|
18
|
+
"publishConfig": {
|
19
|
+
"access": "public",
|
20
|
+
"registry": "https://registry.npmjs.org/"
|
21
|
+
},
|
22
|
+
"keywords": [
|
23
|
+
"ai",
|
24
|
+
"cli",
|
25
|
+
"code-quality",
|
26
|
+
"analytics",
|
27
|
+
"token-tracking",
|
28
|
+
"developer-tools",
|
29
|
+
"local-first",
|
30
|
+
"proxy",
|
31
|
+
"gemini",
|
32
|
+
"openai",
|
33
|
+
"anthropic"
|
34
|
+
],
|
9
35
|
"scripts": {
|
10
|
-
"start": "tsx src/cli.ts start",
|
11
|
-
"cli": "tsx src/cli.ts",
|
12
36
|
"build": "tsc",
|
13
|
-
"
|
37
|
+
"dev": "tsx watch src/cli.ts",
|
38
|
+
"lint": "echo 'Lint CLI code with root ESLint config'",
|
39
|
+
"typecheck": "tsc --noEmit",
|
40
|
+
"cli": "tsx src/cli.ts",
|
41
|
+
"start": "tsx src/cli.ts start",
|
42
|
+
"test": "tsx --test --test-reporter=verbose src/**/*.test.ts",
|
43
|
+
"test:watch": "tsx --test --watch src/**/*.test.ts"
|
14
44
|
},
|
15
45
|
"dependencies": {
|
16
|
-
"@supabase/supabase-js": "^2.47.1",
|
17
|
-
"commander": "^14.0.1",
|
18
|
-
"chalk": "^5.6.2",
|
19
46
|
"axios": "^1.12.2",
|
20
|
-
"
|
21
|
-
"
|
22
|
-
"ora": "^9.0.0",
|
23
|
-
"cli-spinners": "^3.3.0",
|
47
|
+
"boxen": "^7.1.1",
|
48
|
+
"chalk": "^5.6.2",
|
24
49
|
"cli-cursor": "^5.0.0",
|
25
|
-
"
|
50
|
+
"cli-progress": "^3.12.0",
|
51
|
+
"cli-spinners": "^3.3.0",
|
52
|
+
"cli-table3": "^0.6.4",
|
53
|
+
"commander": "^14.0.1",
|
54
|
+
"dotenv": "^16.4.5",
|
55
|
+
"inquirer": "^12.0.0",
|
26
56
|
"is-interactive": "^2.0.0",
|
27
|
-
"
|
28
|
-
"
|
57
|
+
"onetime": "^7.0.0",
|
58
|
+
"open": "^10.2.0",
|
59
|
+
"ora": "^9.0.0"
|
29
60
|
},
|
30
61
|
"devDependencies": {
|
31
|
-
"@types/node": "^24.6.2",
|
32
62
|
"@types/chalk": "^2.2.4",
|
63
|
+
"@types/node": "^24.6.2",
|
33
64
|
"@types/open": "^6.2.1",
|
34
|
-
"
|
65
|
+
"@vitest/ui": "^3.2.4",
|
66
|
+
"esbuild": "^0.25.10",
|
35
67
|
"tsx": "^4.20.6",
|
36
|
-
"
|
68
|
+
"typescript": "^5.9.3",
|
69
|
+
"vitest": "^3.2.4"
|
37
70
|
}
|
38
71
|
}
|
package/.env
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
# TokNXR Environment Variables
|
2
|
-
# Copy this file to .env and fill in your actual API keys
|
3
|
-
|
4
|
-
# Google AI API Key (used for both Gemini-Pro and Gemini-Free)
|
5
|
-
GEMINI_API_KEY=AIzaSyCtrQ8e5H66vbrAozWlWUmCcObJmrI2ovg
|
6
|
-
|
7
|
-
# OpenAI API Key
|
8
|
-
OPENAI_API_KEY=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
|
9
|
-
|
10
|
-
# Anthropic Claude API Key
|
11
|
-
ANTHROPIC_API_KEY=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
|
12
|
-
|
13
|
-
# Supabase Configuration
|
14
|
-
SUPABASE_URL=https://your-project.supabase.co
|
15
|
-
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
|
16
|
-
# Optional: Webhook URL for budget alerts
|
17
|
-
# WEBHOOK_URL=https://your-webhook-url.com/alerts
|
18
|
-
SUPABASE_URL=https://pkdytotoptkknghtsomn.supabase.co
|
19
|
-
SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBrZHl0b3RvcHRra25naHRzb21uIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTk3MjI1OTgsImV4cCI6MjA3NTI5ODU5OH0.Y3RVJvx7w-eGD4Nv2aVv8jnkUKhmA2vpkBs7_rFzEoQ
|
20
|
-
# Optional: Custom port for the proxy server (default: 8788)
|
21
|
-
# PORT=8788
|