@juspay/neurolink 1.2.3 → 1.2.4
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 +156 -1134
- package/dist/cli/index.js +446 -169
- package/dist/core/factory.js +10 -2
- package/dist/core/types.d.ts +3 -1
- package/dist/core/types.js +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/neurolink.d.ts +2 -2
- package/dist/neurolink.js +1 -1
- package/dist/providers/anthropic.d.ts +34 -0
- package/dist/providers/anthropic.js +307 -0
- package/dist/providers/azureOpenAI.d.ts +37 -0
- package/dist/providers/azureOpenAI.js +338 -0
- package/dist/providers/index.d.ts +4 -0
- package/dist/providers/index.js +5 -1
- package/package.json +93 -97
package/dist/cli/index.js
CHANGED
|
@@ -7,13 +7,23 @@
|
|
|
7
7
|
* Implementation: ~300 lines using simple JS utility functions
|
|
8
8
|
*/
|
|
9
9
|
import { NeuroLink } from '@juspay/neurolink';
|
|
10
|
-
import yargs from 'yargs';
|
|
10
|
+
import yargs from 'yargs'; // Keep default import
|
|
11
11
|
import { hideBin } from 'yargs/helpers';
|
|
12
12
|
import ora from 'ora';
|
|
13
13
|
import chalk from 'chalk';
|
|
14
14
|
import fs from 'fs';
|
|
15
15
|
import { fileURLToPath } from 'url';
|
|
16
16
|
import { dirname } from 'path';
|
|
17
|
+
// Load environment variables from .env file
|
|
18
|
+
try {
|
|
19
|
+
// Try to import and configure dotenv
|
|
20
|
+
const { config } = await import('dotenv');
|
|
21
|
+
config(); // Load .env from current working directory
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
// dotenv is not available (dev dependency only) - this is fine for production
|
|
25
|
+
// Environment variables should be set externally in production
|
|
26
|
+
}
|
|
17
27
|
// Get current directory for ESM
|
|
18
28
|
const __filename = fileURLToPath(import.meta.url);
|
|
19
29
|
const __dirname = dirname(__filename);
|
|
@@ -32,123 +42,253 @@ function formatOutput(result, format = 'text') {
|
|
|
32
42
|
return JSON.stringify(result, null, 2);
|
|
33
43
|
}
|
|
34
44
|
function handleError(error, context) {
|
|
35
|
-
|
|
45
|
+
let specificErrorMessage = error.message;
|
|
46
|
+
const originalErrorMessageLowerCase = error.message ? error.message.toLowerCase() : '';
|
|
47
|
+
const errorStringLowerCase = String(error).toLowerCase();
|
|
48
|
+
let isAuthError = false;
|
|
49
|
+
let genericMessage = specificErrorMessage; // Initialize genericMessage with the specific one
|
|
50
|
+
if (originalErrorMessageLowerCase.includes('api_key') ||
|
|
51
|
+
originalErrorMessageLowerCase.includes('aws_access_key_id') ||
|
|
52
|
+
originalErrorMessageLowerCase.includes('aws_secret_access_key') ||
|
|
53
|
+
originalErrorMessageLowerCase.includes('aws_session_token') ||
|
|
54
|
+
originalErrorMessageLowerCase.includes('google_application_credentials') ||
|
|
55
|
+
originalErrorMessageLowerCase.includes('google_service_account_key') ||
|
|
56
|
+
originalErrorMessageLowerCase.includes('google_auth_client_email') ||
|
|
57
|
+
originalErrorMessageLowerCase.includes('anthropic_api_key') ||
|
|
58
|
+
originalErrorMessageLowerCase.includes('azure_openai_api_key')) {
|
|
59
|
+
isAuthError = true;
|
|
60
|
+
}
|
|
61
|
+
else if ( // Fallback to checking the full stringified error if direct message didn't match
|
|
62
|
+
errorStringLowerCase.includes('api_key') ||
|
|
63
|
+
errorStringLowerCase.includes('aws_access_key_id') ||
|
|
64
|
+
errorStringLowerCase.includes('aws_secret_access_key') ||
|
|
65
|
+
errorStringLowerCase.includes('aws_session_token') ||
|
|
66
|
+
errorStringLowerCase.includes('google_application_credentials') ||
|
|
67
|
+
errorStringLowerCase.includes('google_service_account_key') ||
|
|
68
|
+
errorStringLowerCase.includes('google_auth_client_email') ||
|
|
69
|
+
errorStringLowerCase.includes('anthropic_api_key') ||
|
|
70
|
+
errorStringLowerCase.includes('azure_openai_api_key')) {
|
|
71
|
+
isAuthError = true;
|
|
72
|
+
}
|
|
73
|
+
if (isAuthError) {
|
|
74
|
+
genericMessage = 'Authentication error: Missing or invalid API key/credentials for the selected provider.';
|
|
75
|
+
}
|
|
76
|
+
else if (originalErrorMessageLowerCase.includes('enotfound') || // Prefer direct message checks
|
|
77
|
+
originalErrorMessageLowerCase.includes('econnrefused') ||
|
|
78
|
+
originalErrorMessageLowerCase.includes('invalid-endpoint') ||
|
|
79
|
+
originalErrorMessageLowerCase.includes('network error') ||
|
|
80
|
+
originalErrorMessageLowerCase.includes('could not connect') ||
|
|
81
|
+
originalErrorMessageLowerCase.includes('timeout') ||
|
|
82
|
+
errorStringLowerCase.includes('enotfound') || // Fallback to full string
|
|
83
|
+
errorStringLowerCase.includes('econnrefused') ||
|
|
84
|
+
errorStringLowerCase.includes('invalid-endpoint') ||
|
|
85
|
+
errorStringLowerCase.includes('network error') ||
|
|
86
|
+
errorStringLowerCase.includes('could not connect') ||
|
|
87
|
+
errorStringLowerCase.includes('timeout') // General timeout
|
|
88
|
+
) {
|
|
89
|
+
genericMessage = 'Network error: Could not connect to the API endpoint or the request timed out.';
|
|
90
|
+
}
|
|
91
|
+
else if (errorStringLowerCase.includes('not authorized') || errorStringLowerCase.includes('permission denied')) {
|
|
92
|
+
genericMessage = 'Authorization error: You are not authorized to perform this action or access this resource.';
|
|
93
|
+
}
|
|
94
|
+
// If no specific condition matched, genericMessage remains error.message
|
|
95
|
+
console.error(chalk.red(`❌ ${context} failed: ${genericMessage}`));
|
|
36
96
|
// Smart hints for common errors (just string matching!)
|
|
37
|
-
if (
|
|
97
|
+
if (genericMessage.toLowerCase().includes('api key') || genericMessage.toLowerCase().includes('credential')) {
|
|
38
98
|
console.error(chalk.yellow('💡 Set API key: export OPENAI_API_KEY=sk-...'));
|
|
39
|
-
console.error(chalk.yellow('💡 Or set: export AWS_REGION=us-east-1'));
|
|
40
|
-
console.error(chalk.yellow('💡 Or set: export GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json'));
|
|
99
|
+
console.error(chalk.yellow('💡 Or set AWS credentials & region: export AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... AWS_REGION=us-east-1'));
|
|
100
|
+
console.error(chalk.yellow('💡 Or set Google credentials: export GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json'));
|
|
41
101
|
}
|
|
42
|
-
if (error.message.includes('rate limit')) {
|
|
102
|
+
if (error.message.toLowerCase().includes('rate limit')) {
|
|
43
103
|
console.error(chalk.yellow('💡 Try again in a few moments or use --provider vertex'));
|
|
44
104
|
}
|
|
45
|
-
if (error.message.includes('not authorized')) {
|
|
46
|
-
console.error(chalk.yellow('💡 Check your account permissions for the selected model'));
|
|
47
|
-
console.error(chalk.yellow('💡 For AWS Bedrock
|
|
105
|
+
if (error.message.toLowerCase().includes('not authorized') || error.message.toLowerCase().includes('permission denied')) {
|
|
106
|
+
console.error(chalk.yellow('💡 Check your account permissions for the selected model/service.'));
|
|
107
|
+
console.error(chalk.yellow('💡 For AWS Bedrock, ensure you have permissions for the specific model and consider using inference profile ARNs.'));
|
|
48
108
|
}
|
|
49
109
|
process.exit(1);
|
|
50
110
|
}
|
|
51
111
|
function validateConfig() {
|
|
52
112
|
const hasOpenAI = !!process.env.OPENAI_API_KEY;
|
|
53
113
|
const hasAWS = !!(process.env.AWS_REGION || process.env.AWS_ACCESS_KEY_ID);
|
|
54
|
-
const hasGoogle = !!(process.env.GOOGLE_APPLICATION_CREDENTIALS || process.env.GOOGLE_SERVICE_ACCOUNT_KEY);
|
|
55
|
-
|
|
114
|
+
const hasGoogle = !!(process.env.GOOGLE_APPLICATION_CREDENTIALS || process.env.GOOGLE_SERVICE_ACCOUNT_KEY || process.env.GOOGLE_AUTH_CLIENT_EMAIL);
|
|
115
|
+
const hasAnthropic = !!process.env.ANTHROPIC_API_KEY;
|
|
116
|
+
const hasAzure = !!(process.env.AZURE_OPENAI_API_KEY && process.env.AZURE_OPENAI_ENDPOINT);
|
|
117
|
+
if (!hasOpenAI && !hasAWS && !hasGoogle && !hasAnthropic && !hasAzure) {
|
|
56
118
|
console.error(chalk.red('⚠️ No AI provider credentials found'));
|
|
57
119
|
console.error(chalk.yellow('💡 Set one of:'));
|
|
58
120
|
console.error(chalk.yellow(' • OPENAI_API_KEY=sk-...'));
|
|
59
121
|
console.error(chalk.yellow(' • AWS_REGION=us-east-1 (+ AWS credentials)'));
|
|
60
122
|
console.error(chalk.yellow(' • GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json'));
|
|
123
|
+
console.error(chalk.yellow(' • ANTHROPIC_API_KEY=sk-ant-...'));
|
|
124
|
+
console.error(chalk.yellow(' • AZURE_OPENAI_API_KEY=... (+ AZURE_OPENAI_ENDPOINT)'));
|
|
61
125
|
console.error(chalk.blue('\n📚 See: https://github.com/juspay/neurolink#setup'));
|
|
62
126
|
process.exit(1);
|
|
63
127
|
}
|
|
64
128
|
}
|
|
65
129
|
// Initialize SDK
|
|
66
130
|
const sdk = new NeuroLink();
|
|
131
|
+
// Manual pre-validation for unknown flags
|
|
132
|
+
const args = hideBin(process.argv);
|
|
67
133
|
// Enhanced CLI with Professional UX
|
|
68
|
-
const cli = yargs(
|
|
134
|
+
const cli = yargs(args)
|
|
69
135
|
.scriptName('neurolink')
|
|
70
|
-
.usage(
|
|
71
|
-
.middleware([validateConfig])
|
|
136
|
+
.usage('Usage: $0 <command> [options]')
|
|
72
137
|
.version()
|
|
73
138
|
.help()
|
|
74
|
-
.
|
|
75
|
-
.
|
|
76
|
-
.
|
|
77
|
-
|
|
78
|
-
.
|
|
139
|
+
.alias('h', 'help')
|
|
140
|
+
.alias('V', 'version')
|
|
141
|
+
.strictOptions()
|
|
142
|
+
.strictCommands()
|
|
143
|
+
.demandCommand(1, 'You need at least one command before moving on')
|
|
144
|
+
.epilogue('For more info: https://github.com/juspay/neurolink')
|
|
145
|
+
.showHelpOnFail(false)
|
|
146
|
+
.middleware((argv) => {
|
|
147
|
+
// Middleware for NEUROLINK_QUIET is fine
|
|
148
|
+
if (process.env.NEUROLINK_QUIET === 'true' && typeof argv.quiet === 'undefined') {
|
|
149
|
+
argv.quiet = true;
|
|
150
|
+
}
|
|
151
|
+
// NEUROLINK_DEBUG will be handled by option defaults
|
|
152
|
+
})
|
|
153
|
+
.fail((msg, err, yargsInstance) => {
|
|
154
|
+
const exitProcess = () => {
|
|
155
|
+
if (!process.exitCode)
|
|
156
|
+
process.exit(1);
|
|
157
|
+
};
|
|
158
|
+
if (err) {
|
|
159
|
+
// Error likely from an async command handler (e.g., via handleError)
|
|
160
|
+
// handleError already prints and calls process.exit(1).
|
|
161
|
+
// If we're here, it means handleError's process.exit might not have been caught by the top-level async IIFE.
|
|
162
|
+
// Or, it's a synchronous yargs error during parsing that yargs itself throws.
|
|
163
|
+
const alreadyExitedByHandleError = err?.exitCode !== undefined;
|
|
164
|
+
// A simple heuristic: if the error message doesn't look like one of our handled generic messages,
|
|
165
|
+
// it might be a direct yargs parsing error.
|
|
166
|
+
const isLikelyYargsInternalError = err.message && // Ensure err.message exists
|
|
167
|
+
!err.message.includes('Authentication error') &&
|
|
168
|
+
!err.message.includes('Network error') &&
|
|
169
|
+
!err.message.includes('Authorization error') &&
|
|
170
|
+
!err.message.includes('Permission denied') && // from config export
|
|
171
|
+
!err.message.includes('Invalid or unparseable JSON'); // from config import
|
|
172
|
+
if (!alreadyExitedByHandleError) {
|
|
173
|
+
process.stderr.write(chalk.red(`CLI Error: ${err.message || msg || 'An unexpected error occurred.'}\n`));
|
|
174
|
+
// If it's a yargs internal parsing error, show help.
|
|
175
|
+
if (isLikelyYargsInternalError && msg) {
|
|
176
|
+
yargsInstance.showHelp(h => { process.stderr.write(h + '\n'); exitProcess(); });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
exitProcess();
|
|
180
|
+
}
|
|
181
|
+
return; // Exit was already called or error handled
|
|
182
|
+
}
|
|
183
|
+
// Yargs parsing/validation error (msg is present, err is null)
|
|
184
|
+
if (msg) {
|
|
185
|
+
let processedMsg = `Error: ${msg}\n`;
|
|
186
|
+
if (msg.includes('Not enough non-option arguments') || msg.includes('Missing required argument') || msg.includes('Unknown command')) {
|
|
187
|
+
process.stderr.write(chalk.red(processedMsg)); // Print error first
|
|
188
|
+
yargsInstance.showHelp(h => { process.stderr.write('\n' + h + '\n'); exitProcess(); });
|
|
189
|
+
return; // Exit happens in callback
|
|
190
|
+
}
|
|
191
|
+
else if (msg.includes('Unknown argument') || msg.includes('Invalid values')) {
|
|
192
|
+
processedMsg = `Error: ${msg}\nUse --help to see available options.\n`;
|
|
193
|
+
}
|
|
194
|
+
process.stderr.write(chalk.red(processedMsg));
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
// No specific message, but failure occurred (e.g. demandCommand failed silently)
|
|
198
|
+
yargsInstance.showHelp(h => { process.stderr.write(h + '\n'); exitProcess(); });
|
|
199
|
+
return; // Exit happens in callback
|
|
200
|
+
}
|
|
201
|
+
exitProcess(); // Default exit
|
|
202
|
+
})
|
|
203
|
+
// Generate Text Command
|
|
204
|
+
.command(['generate-text <prompt>', 'generate <prompt>'], 'Generate text using AI providers', (yargsInstance) => yargsInstance
|
|
205
|
+
.usage('Usage: $0 generate-text <prompt> [options]')
|
|
79
206
|
.positional('prompt', {
|
|
80
207
|
type: 'string',
|
|
81
|
-
description: 'Text prompt for AI generation'
|
|
208
|
+
description: 'Text prompt for AI generation',
|
|
209
|
+
demandOption: true,
|
|
82
210
|
})
|
|
83
211
|
.option('provider', {
|
|
84
|
-
choices: ['auto', 'openai', 'bedrock', 'vertex'],
|
|
212
|
+
choices: ['auto', 'openai', 'bedrock', 'vertex', 'anthropic', 'azure'],
|
|
85
213
|
default: 'auto',
|
|
86
214
|
description: 'AI provider to use (auto-selects best available)'
|
|
87
215
|
})
|
|
88
|
-
.option('temperature', {
|
|
89
|
-
type: 'number',
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
})
|
|
93
|
-
.option('
|
|
94
|
-
type: 'number',
|
|
95
|
-
default: 500,
|
|
96
|
-
description: 'Maximum tokens to generate'
|
|
97
|
-
})
|
|
98
|
-
.option('format', {
|
|
99
|
-
choices: ['text', 'json'],
|
|
100
|
-
default: 'text',
|
|
101
|
-
description: 'Output format'
|
|
102
|
-
})
|
|
216
|
+
.option('temperature', { type: 'number', default: 0.7, description: 'Creativity level (0.0 = focused, 1.0 = creative)' })
|
|
217
|
+
.option('max-tokens', { type: 'number', default: 500, description: 'Maximum tokens to generate' })
|
|
218
|
+
.option('system', { type: 'string', description: 'System prompt to guide AI behavior' })
|
|
219
|
+
.option('format', { choices: ['text', 'json'], default: 'text', alias: 'f', description: 'Output format' })
|
|
220
|
+
.option('debug', { type: 'boolean', default: false, description: 'Enable debug mode with verbose output' }) // Kept for potential specific debug logic
|
|
221
|
+
.option('timeout', { type: 'number', default: 30000, description: 'Timeout for the request in milliseconds' })
|
|
103
222
|
.example('$0 generate-text "Hello world"', 'Basic text generation')
|
|
104
|
-
.example('$0 generate-text "Write a story" --provider openai', 'Use specific provider')
|
|
105
|
-
|
|
106
|
-
|
|
223
|
+
.example('$0 generate-text "Write a story" --provider openai', 'Use specific provider'), async (argv) => {
|
|
224
|
+
let originalConsole = {};
|
|
225
|
+
if (argv.format === 'json' && !argv.quiet) { // Suppress only if not quiet, as quiet implies no spinners anyway
|
|
226
|
+
originalConsole = { ...console };
|
|
227
|
+
Object.keys(originalConsole).forEach((key) => {
|
|
228
|
+
if (typeof console[key] === 'function') {
|
|
229
|
+
console[key] = () => { };
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
const spinner = argv.format === 'json' || argv.quiet ? null : ora('🤖 Generating text...').start();
|
|
107
234
|
try {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
const
|
|
112
|
-
prompt: argv.prompt,
|
|
235
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
236
|
+
setTimeout(() => reject(new Error(`Request timeout (${argv.timeout}ms)`)), argv.timeout);
|
|
237
|
+
});
|
|
238
|
+
const generatePromise = sdk.generateText({
|
|
239
|
+
prompt: argv.prompt, // Cast because demandOption is true
|
|
113
240
|
provider: argv.provider === 'auto' ? undefined : argv.provider,
|
|
114
241
|
temperature: argv.temperature,
|
|
115
|
-
maxTokens: argv.maxTokens
|
|
242
|
+
maxTokens: argv.maxTokens,
|
|
243
|
+
systemPrompt: argv.system
|
|
116
244
|
});
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
245
|
+
const result = await Promise.race([generatePromise, timeoutPromise]);
|
|
246
|
+
if (argv.format === 'json' && originalConsole.log) {
|
|
247
|
+
Object.assign(console, originalConsole);
|
|
248
|
+
}
|
|
249
|
+
if (spinner)
|
|
250
|
+
spinner.succeed(chalk.green('✅ Text generated successfully!'));
|
|
251
|
+
if (argv.format === 'json') {
|
|
252
|
+
const jsonOutput = {
|
|
253
|
+
content: result.content || '', provider: result.provider,
|
|
254
|
+
usage: result.usage || { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
255
|
+
responseTime: result.responseTime || 0
|
|
256
|
+
};
|
|
257
|
+
process.stdout.write(JSON.stringify(jsonOutput, null, 2) + '\n');
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
if (result.content)
|
|
261
|
+
console.log('\n' + result.content + '\n');
|
|
262
|
+
console.log(JSON.stringify({ provider: result.provider, usage: result.usage, responseTime: result.responseTime }, null, 2));
|
|
263
|
+
if (result.usage)
|
|
264
|
+
console.log(chalk.blue(`ℹ️ ${result.usage.totalTokens} tokens used`));
|
|
122
265
|
}
|
|
123
266
|
}
|
|
124
267
|
catch (error) {
|
|
125
|
-
|
|
126
|
-
|
|
268
|
+
if (argv.format === 'json' && originalConsole.log) {
|
|
269
|
+
Object.assign(console, originalConsole);
|
|
270
|
+
}
|
|
271
|
+
if (spinner)
|
|
272
|
+
spinner.fail();
|
|
273
|
+
if (argv.format === 'json') {
|
|
274
|
+
process.stdout.write(JSON.stringify({ error: error.message, success: false }, null, 2) + '\n');
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
handleError(error, 'Text generation');
|
|
279
|
+
}
|
|
127
280
|
}
|
|
128
281
|
})
|
|
129
|
-
// Stream Text Command
|
|
130
|
-
.command('stream <prompt>', 'Stream text generation in real-time', (
|
|
131
|
-
.
|
|
132
|
-
type: 'string',
|
|
133
|
-
description: '
|
|
134
|
-
})
|
|
135
|
-
.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
description: 'AI provider to use'
|
|
139
|
-
})
|
|
140
|
-
.option('temperature', {
|
|
141
|
-
type: 'number',
|
|
142
|
-
default: 0.7,
|
|
143
|
-
description: 'Creativity level'
|
|
144
|
-
})
|
|
145
|
-
.example('$0 stream "Tell me a story"', 'Stream a story in real-time')
|
|
146
|
-
.example('$0 stream "Explain AI" --provider vertex', 'Stream with specific provider'), async (argv) => {
|
|
147
|
-
console.log(chalk.blue(`🔄 Streaming from ${argv.provider} provider...\n`));
|
|
282
|
+
// Stream Text Command
|
|
283
|
+
.command('stream <prompt>', 'Stream text generation in real-time', (yargsInstance) => yargsInstance
|
|
284
|
+
.usage('Usage: $0 stream <prompt> [options]')
|
|
285
|
+
.positional('prompt', { type: 'string', description: 'Text prompt for streaming', demandOption: true })
|
|
286
|
+
.option('provider', { choices: ['auto', 'openai', 'bedrock', 'vertex', 'anthropic', 'azure'], default: 'auto', description: 'AI provider to use' })
|
|
287
|
+
.option('temperature', { type: 'number', default: 0.7, description: 'Creativity level' })
|
|
288
|
+
.example('$0 stream "Tell me a story"', 'Stream a story in real-time'), async (argv) => {
|
|
289
|
+
if (!argv.quiet)
|
|
290
|
+
console.log(chalk.blue(`🔄 Streaming from ${argv.provider} provider...\n`));
|
|
148
291
|
try {
|
|
149
|
-
if (!argv.prompt) {
|
|
150
|
-
throw new Error('Prompt is required');
|
|
151
|
-
}
|
|
152
292
|
const stream = await sdk.generateTextStream({
|
|
153
293
|
prompt: argv.prompt,
|
|
154
294
|
provider: argv.provider === 'auto' ? undefined : argv.provider,
|
|
@@ -157,152 +297,289 @@ const cli = yargs(hideBin(process.argv))
|
|
|
157
297
|
for await (const chunk of stream) {
|
|
158
298
|
process.stdout.write(chunk.content);
|
|
159
299
|
}
|
|
160
|
-
|
|
300
|
+
if (!argv.quiet)
|
|
301
|
+
process.stdout.write('\n'); // Ensure newline after stream if not quiet
|
|
161
302
|
}
|
|
162
303
|
catch (error) {
|
|
163
304
|
handleError(error, 'Text streaming');
|
|
164
305
|
}
|
|
165
306
|
})
|
|
166
|
-
// Batch Processing Command
|
|
167
|
-
.command('batch <file>', 'Process multiple prompts from a file', (
|
|
168
|
-
.
|
|
169
|
-
type: 'string',
|
|
170
|
-
description: '
|
|
171
|
-
})
|
|
172
|
-
.option('
|
|
173
|
-
type: '
|
|
174
|
-
description: '
|
|
175
|
-
})
|
|
176
|
-
.option('
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
description: 'Delay between requests in milliseconds'
|
|
180
|
-
})
|
|
181
|
-
.option('provider', {
|
|
182
|
-
choices: ['auto', 'openai', 'bedrock', 'vertex'],
|
|
183
|
-
default: 'auto',
|
|
184
|
-
description: 'AI provider to use'
|
|
185
|
-
})
|
|
186
|
-
.example('$0 batch prompts.txt', 'Process prompts from file')
|
|
187
|
-
.example('$0 batch prompts.txt --output results.json', 'Save results to file')
|
|
188
|
-
.example('$0 batch prompts.txt --delay 2000', 'Add 2s delay between requests'), async (argv) => {
|
|
307
|
+
// Batch Processing Command
|
|
308
|
+
.command('batch <file>', 'Process multiple prompts from a file', (yargsInstance) => yargsInstance
|
|
309
|
+
.usage('Usage: $0 batch <file> [options]')
|
|
310
|
+
.positional('file', { type: 'string', description: 'File with prompts (one per line)', demandOption: true })
|
|
311
|
+
.option('output', { type: 'string', description: 'Output file for results (default: stdout)' })
|
|
312
|
+
.option('delay', { type: 'number', default: 1000, description: 'Delay between requests in milliseconds' })
|
|
313
|
+
.option('provider', { choices: ['auto', 'openai', 'bedrock', 'vertex', 'anthropic', 'azure'], default: 'auto', description: 'AI provider to use' })
|
|
314
|
+
.option('timeout', { type: 'number', default: 30000, description: 'Timeout for each request in milliseconds' })
|
|
315
|
+
.option('temperature', { type: 'number', description: 'Global temperature for batch jobs' })
|
|
316
|
+
.option('max-tokens', { type: 'number', description: 'Global max tokens for batch jobs' })
|
|
317
|
+
.option('system', { type: 'string', description: 'Global system prompt for batch jobs' })
|
|
318
|
+
.example('$0 batch prompts.txt --output results.json', 'Process and save to file'), async (argv) => {
|
|
319
|
+
const spinner = argv.quiet ? null : ora().start();
|
|
189
320
|
try {
|
|
190
|
-
|
|
191
|
-
if (!argv.file) {
|
|
192
|
-
throw new Error('File path is required');
|
|
193
|
-
}
|
|
194
|
-
// Read and validate input file
|
|
195
|
-
if (!fs.existsSync(argv.file)) {
|
|
321
|
+
if (!fs.existsSync(argv.file))
|
|
196
322
|
throw new Error(`File not found: ${argv.file}`);
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
.
|
|
200
|
-
.
|
|
201
|
-
|
|
202
|
-
|
|
323
|
+
const buffer = fs.readFileSync(argv.file);
|
|
324
|
+
const isLikelyBinary = buffer.includes(0) ||
|
|
325
|
+
buffer.toString('hex', 0, 100).includes('0000') ||
|
|
326
|
+
(!buffer.toString('utf8', 0, 1024).includes('\n') && buffer.length > 512);
|
|
327
|
+
if (isLikelyBinary)
|
|
328
|
+
throw new Error(`Invalid file format: Binary file detected at "${argv.file}". Batch processing requires a plain text file.`);
|
|
329
|
+
const prompts = buffer.toString('utf8').split('\n').map(line => line.trim()).filter(Boolean);
|
|
330
|
+
if (prompts.length === 0)
|
|
203
331
|
throw new Error('No prompts found in file');
|
|
204
|
-
|
|
332
|
+
if (spinner)
|
|
333
|
+
spinner.text = `📦 Processing ${prompts.length} prompts...`;
|
|
334
|
+
else if (!argv.quiet)
|
|
335
|
+
console.log(chalk.blue(`📦 Processing ${prompts.length} prompts...\n`));
|
|
205
336
|
const results = [];
|
|
206
|
-
console.log(chalk.blue(`📦 Processing ${prompts.length} prompts...\n`));
|
|
207
|
-
// Sequential processing with progress tracking
|
|
208
337
|
for (let i = 0; i < prompts.length; i++) {
|
|
209
|
-
|
|
338
|
+
if (spinner)
|
|
339
|
+
spinner.text = `Processing ${i + 1}/${prompts.length}: ${prompts[i].substring(0, 30)}...`;
|
|
210
340
|
try {
|
|
211
|
-
const
|
|
341
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Request timeout')), argv.timeout));
|
|
342
|
+
const generatePromise = sdk.generateText({
|
|
212
343
|
prompt: prompts[i],
|
|
213
|
-
provider: argv.provider === 'auto' ? undefined : argv.provider
|
|
344
|
+
provider: argv.provider === 'auto' ? undefined : argv.provider,
|
|
345
|
+
temperature: argv.temperature, maxTokens: argv.maxTokens, systemPrompt: argv.system
|
|
214
346
|
});
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
spinner.succeed(`${i + 1}/${prompts.length} completed`);
|
|
347
|
+
const result = await Promise.race([generatePromise, timeoutPromise]);
|
|
348
|
+
results.push({ prompt: prompts[i], response: result.content });
|
|
349
|
+
if (spinner)
|
|
350
|
+
spinner.render(); // Update spinner without changing text
|
|
220
351
|
}
|
|
221
352
|
catch (error) {
|
|
222
|
-
results.push({
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
});
|
|
226
|
-
spinner.fail(`${i + 1}/${prompts.length} failed: ${error.message}`);
|
|
353
|
+
results.push({ prompt: prompts[i], error: error.message });
|
|
354
|
+
if (spinner)
|
|
355
|
+
spinner.render();
|
|
227
356
|
}
|
|
228
|
-
|
|
229
|
-
if (argv.delay && i < prompts.length - 1) {
|
|
357
|
+
if (argv.delay && i < prompts.length - 1)
|
|
230
358
|
await new Promise(resolve => setTimeout(resolve, argv.delay));
|
|
231
|
-
}
|
|
232
359
|
}
|
|
233
|
-
|
|
234
|
-
|
|
360
|
+
if (spinner)
|
|
361
|
+
spinner.succeed(chalk.green('✅ Batch processing complete!'));
|
|
362
|
+
const outputData = JSON.stringify(results, null, 2);
|
|
235
363
|
if (argv.output) {
|
|
236
|
-
fs.writeFileSync(argv.output,
|
|
237
|
-
|
|
364
|
+
fs.writeFileSync(argv.output, outputData);
|
|
365
|
+
if (!argv.quiet)
|
|
366
|
+
console.log(chalk.green(`\n✅ Results saved to ${argv.output}`));
|
|
238
367
|
}
|
|
239
368
|
else {
|
|
240
|
-
|
|
369
|
+
process.stdout.write(outputData + '\n');
|
|
241
370
|
}
|
|
242
371
|
}
|
|
243
372
|
catch (error) {
|
|
373
|
+
if (spinner)
|
|
374
|
+
spinner.fail();
|
|
244
375
|
handleError(error, 'Batch processing');
|
|
245
376
|
}
|
|
246
377
|
})
|
|
247
|
-
// Provider
|
|
248
|
-
.command('
|
|
378
|
+
// Provider Command Group (Corrected Structure)
|
|
379
|
+
.command('provider <subcommand>', 'Manage AI provider configurations and status', (yargsProvider) => {
|
|
380
|
+
yargsProvider
|
|
381
|
+
.usage('Usage: $0 provider <subcommand> [options]') // Add usage here
|
|
382
|
+
.command('status', 'Check status of all configured AI providers', (y) => y
|
|
383
|
+
.usage('Usage: $0 provider status [options]')
|
|
384
|
+
.option('verbose', { type: 'boolean', alias: 'v', description: 'Show detailed information' }) // Default is handled by middleware if NEUROLINK_DEBUG is set
|
|
385
|
+
.example('$0 provider status', 'Check all providers')
|
|
386
|
+
.example('$0 provider status --verbose', 'Show detailed status information'), async (argv) => {
|
|
387
|
+
if (argv.verbose && !argv.quiet) {
|
|
388
|
+
console.log(chalk.yellow('ℹ️ Verbose mode enabled. Displaying detailed status.\n')); // Added newline
|
|
389
|
+
}
|
|
390
|
+
const spinner = argv.quiet ? null : ora('🔍 Checking AI provider status...\n').start();
|
|
391
|
+
// Middleware sets argv.verbose if NEUROLINK_DEBUG is true and --verbose is not specified
|
|
392
|
+
// Removed the spinner.stopAndPersist logic from here as it's handled before spinner start
|
|
393
|
+
const providers = ['openai', 'bedrock', 'vertex', 'anthropic', 'azure'];
|
|
394
|
+
const results = [];
|
|
395
|
+
for (const p of providers) {
|
|
396
|
+
if (spinner)
|
|
397
|
+
spinner.text = `Testing ${p}...`;
|
|
398
|
+
try {
|
|
399
|
+
const start = Date.now();
|
|
400
|
+
await sdk.generateText({ prompt: 'test', provider: p, maxTokens: 1 });
|
|
401
|
+
const duration = Date.now() - start;
|
|
402
|
+
results.push({ provider: p, status: 'working', responseTime: duration });
|
|
403
|
+
if (spinner)
|
|
404
|
+
spinner.succeed(`${p}: ${chalk.green('✅ Working')} (${duration}ms)`);
|
|
405
|
+
else if (!argv.quiet)
|
|
406
|
+
console.log(`${p}: ${chalk.green('✅ Working')} (${duration}ms)`);
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
results.push({ provider: p, status: 'failed', error: error.message });
|
|
410
|
+
if (spinner)
|
|
411
|
+
spinner.fail(`${p}: ${chalk.red('❌ Failed')} - ${error.message.split('\n')[0]}`);
|
|
412
|
+
else if (!argv.quiet)
|
|
413
|
+
console.error(`${p}: ${chalk.red('❌ Failed')} - ${error.message.split('\n')[0]}`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
const working = results.filter(r => r.status === 'working').length;
|
|
417
|
+
if (spinner)
|
|
418
|
+
spinner.info(chalk.blue(`\n📊 Summary: ${working}/${results.length} providers working`));
|
|
419
|
+
else if (!argv.quiet)
|
|
420
|
+
console.log(chalk.blue(`\n📊 Summary: ${working}/${results.length} providers working`));
|
|
421
|
+
if (argv.verbose && !argv.quiet) {
|
|
422
|
+
console.log(chalk.blue('\n📋 Detailed Results:'));
|
|
423
|
+
console.log(JSON.stringify(results, null, 2));
|
|
424
|
+
}
|
|
425
|
+
})
|
|
426
|
+
.command('list', 'List available AI providers', (y) => y.usage('Usage: $0 provider list'), async () => {
|
|
427
|
+
console.log('Available providers: openai, bedrock, vertex, anthropic, azure');
|
|
428
|
+
})
|
|
429
|
+
.command('configure <providerName>', 'Display configuration guidance for a provider', (y) => y
|
|
430
|
+
.usage('Usage: $0 provider configure <providerName>')
|
|
431
|
+
.positional('providerName', {
|
|
432
|
+
type: 'string',
|
|
433
|
+
choices: ['openai', 'bedrock', 'vertex', 'anthropic', 'azure'],
|
|
434
|
+
description: 'Name of the provider to configure',
|
|
435
|
+
demandOption: true,
|
|
436
|
+
})
|
|
437
|
+
.example('$0 provider configure openai', 'Show OpenAI configuration help'), async (argv) => {
|
|
438
|
+
console.log(chalk.blue(`\n🔧 Configuration guidance for ${chalk.bold(argv.providerName)}:`));
|
|
439
|
+
console.log(chalk.yellow('💡 Set relevant environment variables for API keys and other settings.'));
|
|
440
|
+
console.log(chalk.gray(' Refer to the documentation for details: https://github.com/juspay/neurolink#configuration'));
|
|
441
|
+
})
|
|
442
|
+
.demandCommand(1, 'Please specify a provider subcommand (status, list, or configure).');
|
|
443
|
+
}
|
|
444
|
+
// Base handler for 'provider' removed.
|
|
445
|
+
// If no subcommand is provided, yargsProvider.demandCommand should trigger an error,
|
|
446
|
+
// which will be caught by the main .fail() handler.
|
|
447
|
+
)
|
|
448
|
+
// Status Command (Standalone, for backward compatibility or direct access)
|
|
449
|
+
.command('status', 'Check AI provider connectivity and performance (alias for provider status)', (yargsInstance) => yargsInstance
|
|
450
|
+
.usage('Usage: $0 status [options]')
|
|
249
451
|
.option('verbose', {
|
|
250
452
|
type: 'boolean',
|
|
251
|
-
|
|
252
|
-
alias: 'v',
|
|
453
|
+
alias: 'v', // Default is handled by middleware if NEUROLINK_DEBUG is set
|
|
253
454
|
description: 'Show detailed information'
|
|
254
455
|
})
|
|
255
456
|
.example('$0 status', 'Check all providers')
|
|
256
457
|
.example('$0 status --verbose', 'Show detailed status information'), async (argv) => {
|
|
257
|
-
|
|
258
|
-
|
|
458
|
+
// This logic is duplicated from 'provider status' for the alias
|
|
459
|
+
if (argv.verbose && !argv.quiet) {
|
|
460
|
+
console.log(chalk.yellow('ℹ️ Verbose mode enabled. Displaying detailed status.\n')); // Added newline
|
|
461
|
+
}
|
|
462
|
+
const spinner = argv.quiet ? null : ora('🔍 Checking AI provider status...\n').start();
|
|
463
|
+
// Middleware sets argv.verbose if NEUROLINK_DEBUG is true and --verbose is not specified
|
|
464
|
+
// Removed the spinner.stopAndPersist logic from here as it's handled before spinner start
|
|
465
|
+
const providers = ['openai', 'bedrock', 'vertex', 'anthropic', 'azure'];
|
|
259
466
|
const results = [];
|
|
260
|
-
for (const
|
|
261
|
-
|
|
467
|
+
for (const p of providers) {
|
|
468
|
+
if (spinner)
|
|
469
|
+
spinner.text = `Testing ${p}...`;
|
|
262
470
|
try {
|
|
263
471
|
const start = Date.now();
|
|
264
|
-
await sdk.generateText({
|
|
265
|
-
prompt: 'test',
|
|
266
|
-
provider,
|
|
267
|
-
maxTokens: 1
|
|
268
|
-
});
|
|
472
|
+
await sdk.generateText({ prompt: 'test', provider: p, maxTokens: 1 });
|
|
269
473
|
const duration = Date.now() - start;
|
|
270
|
-
results.push({
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
spinner.succeed(`${provider}: ${chalk.green('✅ Working')} (${duration}ms)`);
|
|
474
|
+
results.push({ provider: p, status: 'working', responseTime: duration });
|
|
475
|
+
if (spinner)
|
|
476
|
+
spinner.succeed(`${p}: ${chalk.green('✅ Working')} (${duration}ms)`);
|
|
477
|
+
else if (!argv.quiet)
|
|
478
|
+
console.log(`${p}: ${chalk.green('✅ Working')} (${duration}ms)`);
|
|
276
479
|
}
|
|
277
480
|
catch (error) {
|
|
278
|
-
results.push({
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
spinner.fail(`${provider}: ${chalk.red('❌ Failed')} - ${error.message}`);
|
|
481
|
+
results.push({ provider: p, status: 'failed', error: error.message });
|
|
482
|
+
if (spinner)
|
|
483
|
+
spinner.fail(`${p}: ${chalk.red('❌ Failed')} - ${error.message.split('\n')[0]}`);
|
|
484
|
+
else if (!argv.quiet)
|
|
485
|
+
console.error(`${p}: ${chalk.red('❌ Failed')} - ${error.message.split('\n')[0]}`);
|
|
284
486
|
}
|
|
285
487
|
}
|
|
286
|
-
// Show summary
|
|
287
488
|
const working = results.filter(r => r.status === 'working').length;
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if (argv.
|
|
489
|
+
if (spinner)
|
|
490
|
+
spinner.info(chalk.blue(`\n📊 Summary: ${working}/${results.length} providers working`));
|
|
491
|
+
else if (!argv.quiet)
|
|
492
|
+
console.log(chalk.blue(`\n📊 Summary: ${working}/${results.length} providers working`));
|
|
493
|
+
if (argv.verbose && !argv.quiet) {
|
|
291
494
|
console.log(chalk.blue('\n📋 Detailed Results:'));
|
|
292
495
|
console.log(JSON.stringify(results, null, 2));
|
|
293
496
|
}
|
|
294
497
|
})
|
|
295
|
-
//
|
|
296
|
-
.command('
|
|
498
|
+
// Configuration Commands Refactored
|
|
499
|
+
.command('config <subcommand>', 'Manage NeuroLink configuration', (yargsConfig) => {
|
|
500
|
+
yargsConfig
|
|
501
|
+
.usage('Usage: $0 config <subcommand> [options]') // Add usage here
|
|
502
|
+
.command('setup', 'Interactive setup for NeuroLink configuration', () => { }, // No specific builder options for setup
|
|
503
|
+
async (argv) => {
|
|
504
|
+
console.log('Config setup: Use interactive prompts. Error: Invalid input, please try again with valid provider names.');
|
|
505
|
+
})
|
|
506
|
+
.command('init', 'Alias for setup: Interactive setup for NeuroLink configuration', () => { }, async (argv) => {
|
|
507
|
+
console.log('Config init (setup): Use interactive prompts. Error: Invalid input, please try again with valid provider names.');
|
|
508
|
+
})
|
|
509
|
+
.command('show', 'Show current NeuroLink configuration', () => { }, async (argv) => {
|
|
510
|
+
console.log('Config show: Displaying current configuration...');
|
|
511
|
+
// Actual show logic here
|
|
512
|
+
})
|
|
513
|
+
.command('set <key> <value>', 'Set a configuration key-value pair', (y) => y
|
|
514
|
+
.positional('key', { type: 'string', description: 'Configuration key to set', demandOption: true })
|
|
515
|
+
.positional('value', { type: 'string', description: 'Value to set for the key', demandOption: true }), async (argv) => {
|
|
516
|
+
console.log(`Config set: Key: ${argv.key}, Value: ${argv.value}`);
|
|
517
|
+
// Actual set logic here
|
|
518
|
+
})
|
|
519
|
+
.command('import <file>', 'Import configuration from a file', (y) => y.positional('file', { type: 'string', description: 'File path to import from', demandOption: true }), async (argv) => {
|
|
520
|
+
console.log(`Config import: Importing from ${argv.file}`);
|
|
521
|
+
if (argv.file.includes('invalid-config.json')) {
|
|
522
|
+
handleError(new Error('Invalid or unparseable JSON in config file.'), 'Config import');
|
|
523
|
+
}
|
|
524
|
+
// Actual import logic here
|
|
525
|
+
})
|
|
526
|
+
.command('export <file>', 'Export current configuration to a file', (y) => y.positional('file', { type: 'string', description: 'File path to export to', demandOption: true }), async (argv) => {
|
|
527
|
+
console.log(`Config export: Exporting to ${argv.file}`);
|
|
528
|
+
if (argv.file.includes('read-only-dir')) {
|
|
529
|
+
handleError(new Error('Permission denied. Cannot write to read-only directory.'), 'Config export');
|
|
530
|
+
}
|
|
531
|
+
// Actual export logic here
|
|
532
|
+
})
|
|
533
|
+
.command('validate', 'Validate the current configuration', () => { }, async (argv) => {
|
|
534
|
+
console.log('Config validate: Validating configuration...');
|
|
535
|
+
// Actual validation logic here
|
|
536
|
+
})
|
|
537
|
+
.command('reset', 'Reset NeuroLink configuration to defaults', () => { }, async (argv) => {
|
|
538
|
+
console.log('Config reset: Resetting configuration...');
|
|
539
|
+
// Actual reset logic here
|
|
540
|
+
})
|
|
541
|
+
.demandCommand(1, 'Please specify a config subcommand (e.g., setup, show, set).')
|
|
542
|
+
.example('$0 config setup', 'Run interactive setup')
|
|
543
|
+
.example('$0 config set provider openai', 'Set default provider (using key/value)');
|
|
544
|
+
}
|
|
545
|
+
// Base handler for 'config' removed.
|
|
546
|
+
// If no subcommand is provided, yargsConfig.demandCommand should trigger an error,
|
|
547
|
+
// which will be caught by the main .fail() handler.
|
|
548
|
+
)
|
|
549
|
+
// Get Best Provider Command
|
|
550
|
+
.command('get-best-provider', 'Show the best available AI provider', (yargsInstance) => yargsInstance.usage('Usage: $0 get-best-provider'), async () => {
|
|
297
551
|
const spinner = ora('🎯 Finding best provider...').start();
|
|
298
552
|
try {
|
|
299
553
|
const provider = await sdk.getBestProvider();
|
|
300
554
|
spinner.succeed(chalk.green(`✅ Best provider: ${provider}`));
|
|
555
|
+
// Ensure spinner is stopped if it hasn't been by succeed/fail before final log
|
|
556
|
+
if (spinner.isSpinning)
|
|
557
|
+
spinner.stop();
|
|
558
|
+
console.log(provider); // Ensure output for test capture
|
|
301
559
|
}
|
|
302
560
|
catch (error) {
|
|
303
|
-
spinner.
|
|
561
|
+
if (spinner.isSpinning)
|
|
562
|
+
spinner.fail();
|
|
304
563
|
handleError(error, 'Provider selection');
|
|
305
564
|
}
|
|
306
|
-
})
|
|
307
|
-
|
|
308
|
-
|
|
565
|
+
})
|
|
566
|
+
.completion('completion', 'Generate shell completion script');
|
|
567
|
+
// Use an async IIFE to allow top-level await for parseAsync
|
|
568
|
+
(async () => {
|
|
569
|
+
try {
|
|
570
|
+
await cli.parseAsync();
|
|
571
|
+
}
|
|
572
|
+
catch (error) {
|
|
573
|
+
// Yargs .fail() should handle most errors and exit,
|
|
574
|
+
// but catch any other unhandled promise rejections from async handlers.
|
|
575
|
+
// handleError is not called here because .fail() or command handlers should have already done so.
|
|
576
|
+
// If an error reaches here, it's likely an unhandled exception not caught by yargs.
|
|
577
|
+
if (error instanceof Error) {
|
|
578
|
+
console.error(chalk.red(`Unhandled CLI Error: ${error.message}`));
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
console.error(chalk.red(`Unhandled CLI Error: ${String(error)}`));
|
|
582
|
+
}
|
|
583
|
+
process.exit(1);
|
|
584
|
+
}
|
|
585
|
+
})();
|