@goldensheepai/toknxr-cli 0.2.2 → 0.4.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/lib/cli.js +899 -216
- package/lib/commands/hallucination-commands.js +453 -0
- package/lib/enhanced-hallucination-detector.js +622 -0
- package/lib/execution-based-detector.js +538 -0
- package/lib/execution-sandbox.js +602 -0
- package/lib/hallucination-database-service.js +447 -0
- package/lib/hallucination-patterns.js +490 -0
- package/lib/types/database-types.js +5 -0
- package/lib/types/hallucination-types.js +74 -0
- package/lib/types/index.js +8 -0
- package/lib/ui.js +84 -20
- package/lib/utils.js +117 -0
- package/package.json +1 -1
- package/lib/auth.js +0 -73
- package/lib/cli.test.js +0 -49
- package/lib/code-review.js +0 -319
- package/lib/config.js +0 -7
- package/lib/sync.js +0 -117
package/lib/ui.js
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
import chalk from 'chalk';
|
2
|
-
import
|
2
|
+
import inquirer from 'inquirer';
|
3
3
|
export const createStatsOverview = (cost, requests, waste, hallucinations) => `
|
4
4
|
${chalk.bold.blue('📊 TokNXR Analytics Overview')}
|
5
5
|
${chalk.gray('------------------------------------')}
|
@@ -45,19 +45,16 @@ export const createOperationProgress = (title, steps) => {
|
|
45
45
|
console.log(chalk.bold.blue(title));
|
46
46
|
return spinner;
|
47
47
|
};
|
48
|
-
export const createInteractiveMenu = (options) => {
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
}
|
56
|
-
|
57
|
-
|
58
|
-
resolve(answer);
|
59
|
-
});
|
60
|
-
});
|
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;
|
61
58
|
};
|
62
59
|
export const createBox = (title, content, options) => {
|
63
60
|
const { borderColor, titleColor } = options;
|
@@ -99,11 +96,20 @@ export const createFilterInterface = (currentFilters) => {
|
|
99
96
|
resolve(currentFilters);
|
100
97
|
});
|
101
98
|
};
|
102
|
-
export const createSearchInterface = (fields) => {
|
103
|
-
|
104
|
-
|
105
|
-
|
99
|
+
export const createSearchInterface = async (fields) => {
|
100
|
+
const { selectedFields } = await inquirer.prompt({
|
101
|
+
type: 'checkbox',
|
102
|
+
name: 'selectedFields',
|
103
|
+
message: 'Which fields would you like to search in?',
|
104
|
+
choices: fields.map(field => ({ name: field, value: field, checked: true })),
|
105
|
+
validate: (answer) => {
|
106
|
+
if (!answer || answer.length < 1) {
|
107
|
+
return 'You must choose at least one field.';
|
108
|
+
}
|
109
|
+
return true;
|
110
|
+
}
|
106
111
|
});
|
112
|
+
return { query: '', fields: selectedFields };
|
107
113
|
};
|
108
114
|
export class InteractiveDataExplorer {
|
109
115
|
constructor(data) {
|
@@ -127,6 +133,64 @@ export class CliStateManager {
|
|
127
133
|
return { budgets: {} };
|
128
134
|
}
|
129
135
|
}
|
130
|
-
export const filterAndSearchInteractions = (interactions, _filters,
|
131
|
-
|
136
|
+
export const filterAndSearchInteractions = (interactions, _filters, search) => {
|
137
|
+
if (!search.query || search.query.trim().length === 0) {
|
138
|
+
return interactions;
|
139
|
+
}
|
140
|
+
const query = search.query.toLowerCase();
|
141
|
+
return interactions.filter(interaction => {
|
142
|
+
return search.fields.some(field => {
|
143
|
+
const value = interaction[field];
|
144
|
+
if (typeof value === 'string') {
|
145
|
+
return value.toLowerCase().includes(query);
|
146
|
+
}
|
147
|
+
return false;
|
148
|
+
});
|
149
|
+
});
|
150
|
+
};
|
151
|
+
// Helper functions for search results
|
152
|
+
export const calcRelevanceScore = (interaction, query, fields) => {
|
153
|
+
const queryLower = query.toLowerCase();
|
154
|
+
let totalScore = 0;
|
155
|
+
let matchingFields = 0;
|
156
|
+
fields.forEach(field => {
|
157
|
+
const value = interaction[field];
|
158
|
+
if (typeof value === 'string') {
|
159
|
+
const valueLower = value.toLowerCase();
|
160
|
+
if (valueLower.includes(queryLower)) {
|
161
|
+
matchingFields++;
|
162
|
+
// Exact match gets highest score
|
163
|
+
if (valueLower === queryLower) {
|
164
|
+
totalScore += 1.0;
|
165
|
+
}
|
166
|
+
else if (valueLower.startsWith(queryLower)) {
|
167
|
+
totalScore += 0.8;
|
168
|
+
}
|
169
|
+
else if (valueLower.endsWith(queryLower)) {
|
170
|
+
totalScore += 0.7;
|
171
|
+
}
|
172
|
+
else {
|
173
|
+
// Partial match - score based on how much of the string matches
|
174
|
+
const matchRatio = queryLower.length / valueLower.length;
|
175
|
+
totalScore += 0.3 + (matchRatio * 0.3);
|
176
|
+
}
|
177
|
+
}
|
178
|
+
}
|
179
|
+
});
|
180
|
+
// Calculate final score
|
181
|
+
if (matchingFields === 0)
|
182
|
+
return 0;
|
183
|
+
const baseScore = totalScore / matchingFields;
|
184
|
+
const multiFieldBonus = matchingFields > 1 ? 0.2 : 0;
|
185
|
+
// Ensure exact matches get high scores
|
186
|
+
const finalScore = Math.min(1.0, baseScore + multiFieldBonus);
|
187
|
+
// Debug: log the score calculation for troubleshooting
|
188
|
+
// console.log(`Debug: query="${query}", baseScore=${baseScore}, finalScore=${finalScore}`);
|
189
|
+
return finalScore;
|
190
|
+
};
|
191
|
+
export const highlightMatch = (text, query) => {
|
192
|
+
if (!text || !query)
|
193
|
+
return text;
|
194
|
+
const regex = new RegExp(`(${query})`, 'gi');
|
195
|
+
return text.replace(regex, chalk.yellow.bold('$1'));
|
132
196
|
};
|
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
package/lib/auth.js
DELETED
@@ -1,73 +0,0 @@
|
|
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
|
-
const CLI_LOGIN_PORT = 8789;
|
6
|
-
const WEB_APP_URL = 'http://localhost:3000';
|
7
|
-
const SERVICE_NAME = 'toknxr-cli'; // A unique name for our service in the keychain
|
8
|
-
const ACCOUNT_NAME = 'default-user'; // A generic account name for the stored token
|
9
|
-
// Function to securely store the token
|
10
|
-
const storeToken = async (token) => {
|
11
|
-
await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, token);
|
12
|
-
console.log(chalk.green('Supabase JWT securely stored in system keychain.'));
|
13
|
-
};
|
14
|
-
// Function to retrieve the token
|
15
|
-
const getToken = async () => {
|
16
|
-
return await keytar.getPassword(SERVICE_NAME, ACCOUNT_NAME);
|
17
|
-
};
|
18
|
-
export const login = async () => {
|
19
|
-
const server = new Promise((resolve, reject) => {
|
20
|
-
const s = http.createServer(async (req, res) => {
|
21
|
-
// Handle CORS preflight requests
|
22
|
-
res.setHeader('Access-Control-Allow-Origin', WEB_APP_URL);
|
23
|
-
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
24
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
25
|
-
if (req.method === 'OPTIONS') {
|
26
|
-
res.writeHead(204);
|
27
|
-
res.end();
|
28
|
-
return;
|
29
|
-
}
|
30
|
-
if (req.method === 'POST' && req.url === '/token') {
|
31
|
-
const chunks = [];
|
32
|
-
for await (const chunk of req) {
|
33
|
-
chunks.push(chunk);
|
34
|
-
}
|
35
|
-
const requestBody = Buffer.concat(chunks).toString();
|
36
|
-
const { token: supabaseJwt } = JSON.parse(requestBody); // Supabase JWT
|
37
|
-
if (supabaseJwt) {
|
38
|
-
console.log(chalk.green('CLI authentication successful!'));
|
39
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
40
|
-
res.end(JSON.stringify({ success: true }));
|
41
|
-
s.close();
|
42
|
-
resolve(supabaseJwt); // Resolve with the Supabase JWT
|
43
|
-
}
|
44
|
-
else {
|
45
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
46
|
-
res.end(JSON.stringify({ error: 'No token provided' }));
|
47
|
-
s.close();
|
48
|
-
reject(new Error('No token provided'));
|
49
|
-
}
|
50
|
-
}
|
51
|
-
else {
|
52
|
-
res.writeHead(404);
|
53
|
-
res.end();
|
54
|
-
}
|
55
|
-
});
|
56
|
-
s.listen(CLI_LOGIN_PORT, async () => {
|
57
|
-
const loginUrl = `${WEB_APP_URL}/cli-login?port=${CLI_LOGIN_PORT}`;
|
58
|
-
console.log(chalk.yellow('Your browser has been opened to complete the login process.'));
|
59
|
-
await open(loginUrl);
|
60
|
-
});
|
61
|
-
});
|
62
|
-
try {
|
63
|
-
const supabaseJwt = await server; // Get the Supabase JWT
|
64
|
-
await storeToken(supabaseJwt); // Store the Supabase JWT securely
|
65
|
-
console.log(chalk.cyan('Authentication complete. You can now use TokNxr CLI commands.'));
|
66
|
-
}
|
67
|
-
catch (error) {
|
68
|
-
const message = error instanceof Error ? error.message : String(error);
|
69
|
-
console.error(chalk.red('Login failed:', message));
|
70
|
-
}
|
71
|
-
};
|
72
|
-
// Export getToken for other parts of the CLI to use
|
73
|
-
export { getToken };
|
package/lib/cli.test.js
DELETED
@@ -1,49 +0,0 @@
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
2
|
-
import { spawn } from 'node:child_process';
|
3
|
-
import path from 'node:path';
|
4
|
-
const CLI = path.resolve(process.cwd(), 'toknxr-cli', 'lib', 'cli.js');
|
5
|
-
function runCli(args, env = {}, timeoutMs = 8000) {
|
6
|
-
return new Promise(resolve => {
|
7
|
-
const child = spawn('node', [CLI, ...args], {
|
8
|
-
env: {
|
9
|
-
...process.env,
|
10
|
-
SUPABASE_URL: process.env.SUPABASE_URL || 'http://localhost',
|
11
|
-
SUPABASE_KEY: process.env.SUPABASE_KEY || 'dummy',
|
12
|
-
...env,
|
13
|
-
},
|
14
|
-
});
|
15
|
-
let stdout = '';
|
16
|
-
let stderr = '';
|
17
|
-
const timer = setTimeout(() => {
|
18
|
-
child.kill('SIGKILL');
|
19
|
-
}, timeoutMs);
|
20
|
-
child.stdout.on('data', d => (stdout += String(d)));
|
21
|
-
child.stderr.on('data', d => (stderr += String(d)));
|
22
|
-
child.on('close', code => {
|
23
|
-
clearTimeout(timer);
|
24
|
-
resolve({ code, stdout, stderr });
|
25
|
-
});
|
26
|
-
});
|
27
|
-
}
|
28
|
-
describe('TokNXR CLI smoke tests', () => {
|
29
|
-
it('--help renders', async () => {
|
30
|
-
const res = await runCli(['--help']);
|
31
|
-
expect(res.code).toBe(0);
|
32
|
-
expect(res.stdout).toContain('Usage: toknxr');
|
33
|
-
});
|
34
|
-
it('budget --view runs', async () => {
|
35
|
-
const res = await runCli(['budget', '--view']);
|
36
|
-
expect(res.code).toBe(0);
|
37
|
-
expect(res.stdout).toContain('Budget Configuration');
|
38
|
-
});
|
39
|
-
it('audit:view --help renders', async () => {
|
40
|
-
const res = await runCli(['audit:view', '--help']);
|
41
|
-
expect(res.code).toBe(0);
|
42
|
-
expect(res.stdout).toContain('--to <date>');
|
43
|
-
});
|
44
|
-
it('audit:export --help renders', async () => {
|
45
|
-
const res = await runCli(['audit:export', '--help']);
|
46
|
-
expect(res.code).toBe(0);
|
47
|
-
expect(res.stdout).toContain('--to <date>');
|
48
|
-
});
|
49
|
-
});
|