@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/ui.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import readline from 'readline';
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
- return new Promise(resolve => {
50
- console.log(chalk.bold.blue('MENU'));
51
- options.forEach(option => console.log(option.name));
52
- const rl = readline.createInterface({
53
- input: process.stdin,
54
- output: process.stdout,
55
- });
56
- rl.question('Choose an option: ', answer => {
57
- rl.close();
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
- return new Promise(resolve => {
104
- console.log('Search interface is not implemented in this mock.');
105
- resolve({ query: 'test', fields });
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, _search) => {
131
- return interactions;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goldensheepai/toknxr-cli",
3
- "version": "0.2.2",
3
+ "version": "0.4.0",
4
4
  "description": "CLI-powered AI effectiveness & code quality analysis tool - 100% Local",
5
5
  "license": "MIT",
6
6
  "type": "module",
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
- });