@majkapp/majk-chat-cli 1.0.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.
Files changed (38) hide show
  1. package/README.md +746 -0
  2. package/dist/cli.d.ts +3 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +1001 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/interactive/interactive.d.ts +27 -0
  7. package/dist/interactive/interactive.d.ts.map +1 -0
  8. package/dist/interactive/interactive.js +361 -0
  9. package/dist/interactive/interactive.js.map +1 -0
  10. package/dist/tools/bash.tool.d.ts +11 -0
  11. package/dist/tools/bash.tool.d.ts.map +1 -0
  12. package/dist/tools/bash.tool.js +140 -0
  13. package/dist/tools/bash.tool.js.map +1 -0
  14. package/dist/tools/filesystem.tool.d.ts +20 -0
  15. package/dist/tools/filesystem.tool.d.ts.map +1 -0
  16. package/dist/tools/filesystem.tool.js +312 -0
  17. package/dist/tools/filesystem.tool.js.map +1 -0
  18. package/dist/utils/config.d.ts +8 -0
  19. package/dist/utils/config.d.ts.map +1 -0
  20. package/dist/utils/config.js +140 -0
  21. package/dist/utils/config.js.map +1 -0
  22. package/dist/utils/credentials.d.ts +45 -0
  23. package/dist/utils/credentials.d.ts.map +1 -0
  24. package/dist/utils/credentials.js +265 -0
  25. package/dist/utils/credentials.js.map +1 -0
  26. package/dist/utils/models.d.ts +10 -0
  27. package/dist/utils/models.d.ts.map +1 -0
  28. package/dist/utils/models.js +56 -0
  29. package/dist/utils/models.js.map +1 -0
  30. package/dist/utils/output.d.ts +133 -0
  31. package/dist/utils/output.d.ts.map +1 -0
  32. package/dist/utils/output.js +306 -0
  33. package/dist/utils/output.js.map +1 -0
  34. package/dist/utils/tool-filter.d.ts +35 -0
  35. package/dist/utils/tool-filter.d.ts.map +1 -0
  36. package/dist/utils/tool-filter.js +94 -0
  37. package/dist/utils/tool-filter.js.map +1 -0
  38. package/package.json +64 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1001 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ var __importDefault = (this && this.__importDefault) || function (mod) {
37
+ return (mod && mod.__esModule) ? mod : { "default": mod };
38
+ };
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ const commander_1 = require("commander");
41
+ const chalk_1 = __importDefault(require("chalk"));
42
+ const ora_1 = __importDefault(require("ora"));
43
+ const fs = __importStar(require("fs-extra"));
44
+ const path = __importStar(require("path"));
45
+ const yaml = __importStar(require("yaml"));
46
+ const dotenv_1 = __importDefault(require("dotenv"));
47
+ const majk_chat_core_1 = require("@majkapp/majk-chat-core");
48
+ const interactive_1 = require("./interactive/interactive");
49
+ const credentials_1 = require("./utils/credentials");
50
+ const config_1 = require("./utils/config");
51
+ const bash_tool_1 = require("./tools/bash.tool");
52
+ const filesystem_tool_1 = require("./tools/filesystem.tool");
53
+ const majk_chat_mcp_1 = require("@majkapp/majk-chat-mcp");
54
+ const majk_chat_basic_tools_1 = require("@majkapp/majk-chat-basic-tools");
55
+ const majk_chat_sessions_1 = require("@majkapp/majk-chat-sessions");
56
+ const models_1 = require("./utils/models");
57
+ const tool_filter_1 = require("./utils/tool-filter");
58
+ const output_1 = require("./utils/output");
59
+ // Load environment variables
60
+ dotenv_1.default.config();
61
+ const program = new commander_1.Command();
62
+ program
63
+ .name('majk-chat')
64
+ .description('CLI for multi-provider LLM chat interactions')
65
+ .version('1.0.0');
66
+ // Add credential options helper function
67
+ function addCredentialOptions(command) {
68
+ return command
69
+ // Generic credentials
70
+ .option('--api-key <key>', 'Generic API key (overrides environment)')
71
+ .option('--api-key-file <file>', 'Read API key from file')
72
+ .option('--env-file <file>', 'Load environment variables from file')
73
+ // OpenAI specific
74
+ .option('--openai-api-key <key>', 'OpenAI API key')
75
+ .option('--openai-org-id <id>', 'OpenAI organization ID')
76
+ // Anthropic specific
77
+ .option('--anthropic-api-key <key>', 'Anthropic API key')
78
+ // Azure OpenAI specific
79
+ .option('--azure-openai-api-key <key>', 'Azure OpenAI API key')
80
+ .option('--azure-openai-endpoint <url>', 'Azure OpenAI endpoint URL')
81
+ .option('--azure-openai-deployment <name>', 'Azure OpenAI deployment name')
82
+ // AWS Bedrock specific
83
+ .option('--aws-access-key-id <id>', 'AWS access key ID')
84
+ .option('--aws-secret-access-key <key>', 'AWS secret access key')
85
+ .option('--aws-session-token <token>', 'AWS session token')
86
+ .option('--aws-region <region>', 'AWS region');
87
+ }
88
+ // Chat command
89
+ const chatCommand = program
90
+ .command('chat')
91
+ .description('Send a chat message to an LLM provider')
92
+ .option('--provider <provider>', 'Provider to use (openai, anthropic, azure-openai, bedrock)')
93
+ .option('--model <model>', 'Model or alias (e.g., "sonnet", "opus", "claude-3-5-sonnet-20241022")')
94
+ .option('-M, --message <message>', 'Message to send')
95
+ .option('-p <value>', 'JSON messages array or prompt string')
96
+ .option('-f, --file <file>', 'Read message from file')
97
+ .option('--prompts <file>', 'File with one prompt per line (sequential execution)')
98
+ .option('-s, --system <system>', 'System message')
99
+ .option('-S, --system-file <file>', 'Read system message from file')
100
+ .option('--append-system-prompt <prompt>', 'Append to the default system prompt')
101
+ .option('-t, --temperature <temp>', 'Temperature (0-2)', parseFloat)
102
+ .option('--max-tokens <tokens>', 'Maximum tokens', parseInt)
103
+ .option('-j, --json', 'Output JSON response')
104
+ .option('--output-format <format>', 'Output format: "text" (default) or "stream-json"', 'text')
105
+ .option('--tools', 'Enable tool execution')
106
+ .option('--enable-core-tools', 'Enable majk-chat-basic-tools (bash, ls, read, etc.)')
107
+ .option('--tool-result-limit <limit>', 'Truncate tool results over this character limit', parseInt, 8000)
108
+ .option('--max-steps <steps>', 'Maximum tool execution steps', parseInt)
109
+ .option('-c, --config <file>', 'Configuration file')
110
+ .option('--mcp-config <value>', 'MCP configuration file path or JSON string')
111
+ .option('--mcp-servers <json>', 'MCP servers configuration as JSON string (deprecated, use --mcp-config)')
112
+ .option('--allowed-tools, --allowedTools <tools>', 'Comma/space-separated allowed tools (supports wildcards like "mcp__*")')
113
+ .option('--disallowed-tools, --disallowedTools <tools>', 'Comma/space-separated disallowed tools (supports wildcards)')
114
+ .option('--permission-prompt-tool <tool>', 'MCP tool to use for permission prompts')
115
+ .option('--save-session', 'Save conversation to a persistent session')
116
+ .option('--continue', 'Continue the most recent session in current directory')
117
+ .option('--session <uuid>', 'Continue a specific session by UUID')
118
+ .option('--session-title <title>', 'Set title for new session')
119
+ .option('--list-sessions', 'List all sessions for current directory')
120
+ .option('--delete-session <uuid>', 'Delete a specific session by UUID');
121
+ addCredentialOptions(chatCommand)
122
+ .action(async (options) => {
123
+ let cleanup = null;
124
+ try {
125
+ // Initialize output handler early so we can pass callbacks
126
+ const outputHandler = new output_1.OutputHandler(options.outputFormat);
127
+ const setupOptions = {
128
+ ...options,
129
+ outputFormat: options.outputFormat, // Pass output format to suppress logs
130
+ onToolCall: options.outputFormat === 'stream-json'
131
+ ? (toolCall) => outputHandler.outputToolCallMessage(toolCall)
132
+ : undefined,
133
+ onToolResult: options.outputFormat === 'stream-json'
134
+ ? (toolCall, result) => outputHandler.outputToolResult(toolCall.id, result)
135
+ : undefined
136
+ };
137
+ const setupResult = await setupChat(setupOptions);
138
+ const { chat, toolRegistry } = setupResult;
139
+ cleanup = setupResult.cleanup;
140
+ // Resolve model alias if provided
141
+ const model = options.model
142
+ ? (0, models_1.resolveModel)(options.model)
143
+ : (0, models_1.getDefaultModelForProvider)(options.provider);
144
+ // Get tool definitions from registry
145
+ const tools = toolRegistry.getDefinitions();
146
+ const toolNames = tools.map((t) => t.function.name);
147
+ // Update output handler with model and tool info
148
+ outputHandler.options.model = model;
149
+ outputHandler.options.tools = toolNames;
150
+ outputHandler.options.cwd = process.cwd();
151
+ // Initialize session manager and handle session operations
152
+ const currentWorkingDir = process.cwd();
153
+ const usesSessions = options.saveSession || options.continue || options.session || options.listSessions || options.deleteSession;
154
+ let sessionManager = null;
155
+ let existingMessages = [];
156
+ let sessionId = null;
157
+ if (usesSessions) {
158
+ sessionManager = new majk_chat_sessions_1.SessionManager();
159
+ // Handle session operations first
160
+ if (options.listSessions) {
161
+ await handleListSessions(sessionManager, currentWorkingDir, outputHandler);
162
+ await cleanup();
163
+ process.exit(0);
164
+ return;
165
+ }
166
+ if (options.deleteSession) {
167
+ await handleDeleteSession(sessionManager, currentWorkingDir, options.deleteSession, outputHandler);
168
+ await cleanup();
169
+ process.exit(0);
170
+ return;
171
+ }
172
+ // Load existing session if requested
173
+ if (options.continue) {
174
+ const sessionData = await sessionManager.continueLatestSession(currentWorkingDir);
175
+ if (sessionData) {
176
+ existingMessages = sessionData.messages;
177
+ sessionId = sessionData.metadata.id;
178
+ if (options.outputFormat !== 'stream-json') {
179
+ console.log(chalk_1.default.cyan(`Continuing session: ${sessionId}${sessionData.metadata.title ? ` (${sessionData.metadata.title})` : ''}`));
180
+ }
181
+ }
182
+ else if (options.outputFormat !== 'stream-json') {
183
+ console.log(chalk_1.default.yellow('No existing sessions found in this directory. Starting new session.'));
184
+ }
185
+ }
186
+ else if (options.session) {
187
+ const sessionData = await sessionManager.loadSession(options.session, currentWorkingDir);
188
+ if (sessionData) {
189
+ existingMessages = sessionData.messages;
190
+ sessionId = sessionData.metadata.id;
191
+ if (options.outputFormat !== 'stream-json') {
192
+ console.log(chalk_1.default.cyan(`Loaded session: ${sessionId}${sessionData.metadata.title ? ` (${sessionData.metadata.title})` : ''}`));
193
+ }
194
+ }
195
+ else {
196
+ outputHandler.outputError(`Session ${options.session} not found in current directory`);
197
+ await cleanup();
198
+ process.exit(1);
199
+ }
200
+ }
201
+ }
202
+ // Initialize session for stream-json
203
+ outputHandler.initializeSession(toolNames, [], model);
204
+ // Handle sequential prompts from file
205
+ if (options.prompts) {
206
+ if (sessionManager) {
207
+ await handleSequentialPromptsWithSessions(chat, toolRegistry, options, outputHandler, sessionManager, sessionId);
208
+ }
209
+ else {
210
+ await handleSequentialPrompts(chat, toolRegistry, options, outputHandler);
211
+ }
212
+ await cleanup();
213
+ process.exit(0);
214
+ return;
215
+ }
216
+ // Get messages based on input method
217
+ const newMessages = await buildMessages(options);
218
+ if (!newMessages || newMessages.length === 0) {
219
+ outputHandler.outputError('No message provided');
220
+ await cleanup();
221
+ process.exit(1);
222
+ }
223
+ // Combine existing messages with new messages, excluding duplicate system messages
224
+ const allMessages = [...existingMessages];
225
+ for (const newMsg of newMessages) {
226
+ if (newMsg.role === 'system' && allMessages.some(m => m.role === 'system')) {
227
+ continue; // Skip duplicate system message
228
+ }
229
+ allMessages.push(newMsg);
230
+ }
231
+ const spinner = outputHandler.startSpinner('Sending message...');
232
+ const request = {
233
+ model,
234
+ messages: allMessages,
235
+ temperature: options.temperature,
236
+ max_tokens: options.maxTokens,
237
+ // Always include tools if any are registered (from MCP or --tools flag)
238
+ tools: tools.length > 0 ? tools : undefined,
239
+ // Enable auto-execution if MCP is configured or --tools flag is set or --enable-core-tools is set
240
+ max_steps: (options.tools || options.enableCoreTools || options.mcpConfig || options.mcpServers)
241
+ ? (options.maxSteps || 5)
242
+ : 0
243
+ };
244
+ const chatSession = chat.createSession(options.provider);
245
+ const lastMessage = allMessages[allMessages.length - 1];
246
+ const messageContent = typeof lastMessage.content === 'string'
247
+ ? lastMessage.content
248
+ : JSON.stringify(lastMessage.content);
249
+ const apiStart = Date.now();
250
+ const response = await chatSession.send(messageContent, request);
251
+ const apiDuration = Date.now() - apiStart;
252
+ if (spinner)
253
+ spinner.succeed('Response received');
254
+ if (options.json) {
255
+ console.log(JSON.stringify(response, null, 2));
256
+ }
257
+ else {
258
+ const assistantMessage = response.choices[0].message;
259
+ // Output assistant message with tool calls
260
+ outputHandler.outputMessage('assistant', assistantMessage.content, response.usage, assistantMessage.tool_calls);
261
+ // Output tool results if any (for text format)
262
+ if (assistantMessage.tool_calls && options.outputFormat === 'text') {
263
+ for (const toolCall of assistantMessage.tool_calls) {
264
+ outputHandler.outputToolCall(toolCall.function.name, JSON.parse(toolCall.function.arguments));
265
+ }
266
+ }
267
+ outputHandler.outputComplete(response.usage, apiDuration);
268
+ }
269
+ // Save conversation to session (only if session tracking is enabled)
270
+ if (sessionManager) {
271
+ await saveConversationToSession(sessionManager, currentWorkingDir, sessionId, allMessages, response.choices[0].message, {
272
+ provider: options.provider,
273
+ model,
274
+ title: options.sessionTitle
275
+ });
276
+ }
277
+ // Clean up and exit
278
+ await cleanup();
279
+ // Force exit after a short delay to ensure cleanup completes
280
+ setTimeout(() => {
281
+ process.exit(0);
282
+ }, 100);
283
+ }
284
+ catch (error) {
285
+ const errorHandler = new output_1.OutputHandler(options.outputFormat);
286
+ errorHandler.outputError(error instanceof Error ? error : String(error));
287
+ if (cleanup) {
288
+ try {
289
+ await cleanup();
290
+ }
291
+ catch (cleanupError) {
292
+ if (options.outputFormat !== 'stream-json') {
293
+ console.error('Cleanup error:', cleanupError);
294
+ }
295
+ }
296
+ }
297
+ process.exit(1);
298
+ }
299
+ });
300
+ // Interactive mode command
301
+ const interactiveCommand = program
302
+ .command('interactive')
303
+ .alias('i')
304
+ .description('Start interactive chat mode')
305
+ .option('-p, --provider <provider>', 'Provider to use')
306
+ .option('-m, --model <model>', 'Model to use')
307
+ .option('-s, --system <system>', 'System message')
308
+ .option('-S, --system-file <file>', 'Read system message from file')
309
+ .option('--tools', 'Enable tool execution')
310
+ .option('--enable-core-tools', 'Enable majk-chat-basic-tools (bash, ls, read, etc.)')
311
+ .option('--tool-result-limit <limit>', 'Truncate tool results over this character limit', parseInt, 8000)
312
+ .option('--auto-execute', 'Auto-execute tools without confirmation')
313
+ .option('-c, --config <file>', 'Configuration file')
314
+ .option('--mcp-config <value>', 'MCP configuration file path or JSON string')
315
+ .option('--mcp-servers <json>', 'MCP servers configuration as JSON string (deprecated, use --mcp-config)')
316
+ .option('--allowed-tools <tools>', 'Comma-separated list of allowed MCP tools')
317
+ .option('--disallowed-tools <tools>', 'Comma-separated list of disallowed MCP tools')
318
+ .option('--permission-prompt-tool <tool>', 'MCP tool to use for permission prompts');
319
+ addCredentialOptions(interactiveCommand)
320
+ .action(async (options) => {
321
+ let cleanup = null;
322
+ try {
323
+ const setupResult = await setupChat(options);
324
+ const { chat, toolRegistry } = setupResult;
325
+ cleanup = setupResult.cleanup;
326
+ const systemMessage = await getSystemMessage(options);
327
+ // Check if we have tools available from MCP or --tools flag
328
+ const hasTools = toolRegistry.getDefinitions().length > 0;
329
+ const interactive = new interactive_1.InteractiveMode(chat, {
330
+ provider: options.provider,
331
+ model: options.model || getDefaultModel(options.provider),
332
+ systemMessage,
333
+ enableTools: options.tools || hasTools, // Enable if tools flag or MCP tools available
334
+ autoExecute: options.autoExecute
335
+ });
336
+ await interactive.start();
337
+ // Clean up when interactive mode exits
338
+ if (cleanup) {
339
+ await cleanup();
340
+ }
341
+ }
342
+ catch (error) {
343
+ console.error(chalk_1.default.red('Error:'), error instanceof Error ? error.message : error);
344
+ if (cleanup) {
345
+ try {
346
+ await cleanup();
347
+ }
348
+ catch (cleanupError) {
349
+ if (options.outputFormat !== 'stream-json') {
350
+ console.error('Cleanup error:', cleanupError);
351
+ }
352
+ }
353
+ }
354
+ process.exit(1);
355
+ }
356
+ });
357
+ // List models command
358
+ const modelsCommand = program
359
+ .command('models')
360
+ .description('List available models')
361
+ .option('-p, --provider <provider>', 'Filter by provider')
362
+ .option('-c, --config <file>', 'Configuration file');
363
+ addCredentialOptions(modelsCommand)
364
+ .action(async (options) => {
365
+ try {
366
+ const { chat } = await setupChat(options);
367
+ const spinner = (0, ora_1.default)('Fetching models...').start();
368
+ const providers = options.provider
369
+ ? [options.provider]
370
+ : chat.listProviders();
371
+ const allModels = [];
372
+ for (const provider of providers) {
373
+ try {
374
+ const providerAdapter = chat.getProvider(provider);
375
+ const response = await providerAdapter.listModels({
376
+ requestId: `models-${Date.now()}`
377
+ });
378
+ for (const model of response.data) {
379
+ allModels.push({
380
+ ...model,
381
+ provider
382
+ });
383
+ }
384
+ }
385
+ catch (error) {
386
+ console.warn(chalk_1.default.yellow(`Warning: Could not fetch models from ${provider}`));
387
+ }
388
+ }
389
+ spinner.succeed('Models fetched');
390
+ if (allModels.length === 0) {
391
+ console.log(chalk_1.default.yellow('No models found'));
392
+ return;
393
+ }
394
+ console.log(chalk_1.default.cyan('\nAvailable Models:'));
395
+ for (const model of allModels) {
396
+ console.log(` ${chalk_1.default.green(model.id)} (${model.provider})`);
397
+ if (model.capabilities) {
398
+ const caps = [];
399
+ if (model.capabilities.tools)
400
+ caps.push('tools');
401
+ if (model.capabilities.modalities?.includes('vision'))
402
+ caps.push('vision');
403
+ if (model.capabilities.json_mode)
404
+ caps.push('json');
405
+ if (caps.length > 0) {
406
+ console.log(` Capabilities: ${caps.join(', ')}`);
407
+ }
408
+ if (model.capabilities.max_output_tokens) {
409
+ console.log(` Max tokens: ${model.capabilities.max_output_tokens}`);
410
+ }
411
+ }
412
+ }
413
+ }
414
+ catch (error) {
415
+ console.error(chalk_1.default.red('Error:'), error instanceof Error ? error.message : error);
416
+ process.exit(1);
417
+ }
418
+ });
419
+ // Configure command
420
+ program
421
+ .command('config')
422
+ .description('Configure providers and credentials')
423
+ .option('--init', 'Initialize configuration')
424
+ .option('--set <key=value>', 'Set configuration value')
425
+ .option('--get <key>', 'Get configuration value')
426
+ .option('--list', 'List configuration')
427
+ .action(async (options) => {
428
+ const configPath = path.join(process.env.HOME || '~', '.majk-chat', 'config.yaml');
429
+ if (options.init) {
430
+ await initializeConfig(configPath);
431
+ console.log(chalk_1.default.green(`Configuration initialized at ${configPath}`));
432
+ }
433
+ else if (options.set) {
434
+ await setConfigValue(configPath, options.set);
435
+ }
436
+ else if (options.get) {
437
+ await getConfigValue(configPath, options.get);
438
+ }
439
+ else if (options.list) {
440
+ await listConfig(configPath);
441
+ }
442
+ else {
443
+ console.log(chalk_1.default.yellow('Please specify an action: --init, --set, --get, or --list'));
444
+ }
445
+ });
446
+ // Helper functions
447
+ // Extract credential options from CLI arguments
448
+ function extractCredentialOptions(options) {
449
+ return {
450
+ // Generic
451
+ apiKey: options.apiKey,
452
+ apiKeyFile: options.apiKeyFile,
453
+ envFile: options.envFile,
454
+ // OpenAI
455
+ openaiApiKey: options.openaiApiKey,
456
+ openaiOrgId: options.openaiOrgId,
457
+ // Anthropic
458
+ anthropicApiKey: options.anthropicApiKey,
459
+ // Azure OpenAI
460
+ azureOpenaiApiKey: options.azureOpenaiApiKey,
461
+ azureOpenaiEndpoint: options.azureOpenaiEndpoint,
462
+ azureOpenaiDeployment: options.azureOpenaiDeployment,
463
+ // AWS Bedrock
464
+ awsAccessKeyId: options.awsAccessKeyId,
465
+ awsSecretAccessKey: options.awsSecretAccessKey,
466
+ awsSessionToken: options.awsSessionToken,
467
+ awsRegion: options.awsRegion
468
+ };
469
+ }
470
+ async function setupChat(options) {
471
+ const builder = new majk_chat_core_1.MajkChatBuilder();
472
+ const credManager = new credentials_1.CredentialManager();
473
+ const credOptions = extractCredentialOptions(options);
474
+ // Load configuration
475
+ if (options.config) {
476
+ const config = await config_1.ConfigLoader.load(options.config);
477
+ await applyConfig(builder, config);
478
+ }
479
+ // Setup providers with credentials
480
+ const providers = await credManager.loadProviders(credOptions);
481
+ // Check if we have any providers or if a specific provider was requested
482
+ let hasProviders = false;
483
+ if (providers.openai || options.provider === 'openai') {
484
+ const creds = await credManager.getCredentials('openai', credOptions);
485
+ if (creds?.apiKey) {
486
+ builder.withOpenAI(creds);
487
+ hasProviders = true;
488
+ }
489
+ }
490
+ if (providers.anthropic || options.provider === 'anthropic') {
491
+ const creds = await credManager.getCredentials('anthropic', credOptions);
492
+ if (creds?.apiKey) {
493
+ builder.withAnthropic(creds);
494
+ hasProviders = true;
495
+ }
496
+ }
497
+ if (providers.azureOpenai || options.provider === 'azure-openai') {
498
+ const creds = await credManager.getCredentials('azure-openai', credOptions);
499
+ if (creds?.apiKey) {
500
+ builder.withAzureOpenAI(creds);
501
+ hasProviders = true;
502
+ }
503
+ }
504
+ if (providers.bedrock || options.provider === 'bedrock') {
505
+ const creds = await credManager.getCredentials('bedrock', credOptions);
506
+ if (creds?.accessKeyId) {
507
+ builder.withBedrock(creds);
508
+ hasProviders = true;
509
+ }
510
+ }
511
+ // Throw error if no valid credentials found
512
+ if (!hasProviders) {
513
+ throw new Error('No valid API credentials found. Please provide API keys via command line options, environment variables, or .env files. Use --help to see available credential options.');
514
+ }
515
+ // Add tools if enabled
516
+ if (options.tools) {
517
+ builder
518
+ .withTool(new bash_tool_1.BashTool())
519
+ .withTool(new filesystem_tool_1.FileSystemTool())
520
+ .withAutoToolExecution(options.maxSteps || 5);
521
+ }
522
+ // Add core tools if enabled
523
+ if (options.enableCoreTools) {
524
+ const toolRegistry = builder.getToolRegistry();
525
+ if (toolRegistry) {
526
+ (0, majk_chat_basic_tools_1.registerBasicTools)(toolRegistry);
527
+ // Apply tool filtering if specified
528
+ if (options.allowedTools || options.disallowedTools) {
529
+ applyToolFiltering(toolRegistry, options.allowedTools, options.disallowedTools);
530
+ }
531
+ // Setup tool result truncation and auto-registration
532
+ if (options.toolResultLimit) {
533
+ setupToolResultManagement(builder, options.toolResultLimit, options);
534
+ }
535
+ builder.withAutoToolExecution(options.maxSteps || 5);
536
+ }
537
+ }
538
+ let mcpPlugin = null;
539
+ // Setup MCP if configured - this will add tools to the registry
540
+ if (options.mcpConfig || options.mcpServers) {
541
+ mcpPlugin = await setupMCP(builder, options);
542
+ // Enable auto tool execution if MCP is configured
543
+ if (!options.tools) {
544
+ builder.withAutoToolExecution(options.maxSteps || 5);
545
+ }
546
+ }
547
+ // Set tool callbacks if provided
548
+ if (options.onToolCall || options.onToolResult) {
549
+ builder.withToolCallbacks(options.onToolCall, options.onToolResult);
550
+ }
551
+ const toolRegistry = builder.getToolRegistry();
552
+ return {
553
+ chat: builder.build(),
554
+ toolRegistry,
555
+ cleanup: async () => {
556
+ if (mcpPlugin) {
557
+ try {
558
+ await mcpPlugin.shutdown();
559
+ }
560
+ catch (error) {
561
+ if (options.outputFormat !== 'stream-json') {
562
+ console.error('Error during MCP cleanup:', error);
563
+ }
564
+ }
565
+ }
566
+ // Remove any stdin listeners that might be keeping process alive
567
+ process.stdin.removeAllListeners();
568
+ // Close stdin to prevent hanging
569
+ if (process.stdin.readable) {
570
+ process.stdin.destroy();
571
+ }
572
+ }
573
+ };
574
+ }
575
+ async function buildMessages(options) {
576
+ const messages = [];
577
+ // Add system message if provided
578
+ const systemMessage = await getSystemMessage(options);
579
+ if (systemMessage) {
580
+ messages.push({ role: 'system', content: systemMessage });
581
+ }
582
+ // Handle -p option (JSON array or prompt string)
583
+ if (options.p) {
584
+ const pValue = options.p.trim();
585
+ if (pValue.startsWith('[')) {
586
+ // Parse as JSON messages array
587
+ try {
588
+ const parsedMessages = JSON.parse(pValue);
589
+ if (Array.isArray(parsedMessages)) {
590
+ messages.push(...parsedMessages);
591
+ }
592
+ }
593
+ catch (e) {
594
+ throw new Error(`Invalid JSON in -p option: ${e}`);
595
+ }
596
+ }
597
+ else {
598
+ // Treat as a simple prompt string
599
+ messages.push({ role: 'user', content: pValue });
600
+ }
601
+ }
602
+ else {
603
+ // Use traditional message options
604
+ const message = await getMessage(options);
605
+ if (message) {
606
+ messages.push({ role: 'user', content: message });
607
+ }
608
+ }
609
+ return messages;
610
+ }
611
+ async function handleSequentialPrompts(chat, toolRegistry, options, outputHandler) {
612
+ const promptsFile = await fs.readFile(options.prompts, 'utf-8');
613
+ const prompts = promptsFile.split('\n').filter(line => line.trim().length > 0);
614
+ const model = options.model
615
+ ? (0, models_1.resolveModel)(options.model)
616
+ : (0, models_1.getDefaultModelForProvider)(options.provider);
617
+ const session = chat.createSession(options.provider);
618
+ const conversationMessages = [];
619
+ // Add system message if provided
620
+ const systemMessage = await getSystemMessage(options);
621
+ if (systemMessage) {
622
+ conversationMessages.push({ role: 'system', content: systemMessage });
623
+ }
624
+ // Get tool definitions from registry
625
+ const tools = toolRegistry.getDefinitions();
626
+ for (const prompt of prompts) {
627
+ outputHandler.outputMessage('user', prompt);
628
+ conversationMessages.push({ role: 'user', content: prompt });
629
+ const request = {
630
+ model,
631
+ messages: conversationMessages,
632
+ temperature: options.temperature,
633
+ max_tokens: options.maxTokens,
634
+ // Always include tools if any are registered
635
+ tools: tools.length > 0 ? tools : undefined,
636
+ // Enable auto-execution if MCP is configured or --tools flag is set
637
+ max_steps: (options.tools || options.mcpConfig || options.mcpServers)
638
+ ? (options.maxSteps || 5)
639
+ : 0
640
+ };
641
+ const spinner = outputHandler.startSpinner('Processing...');
642
+ const response = await session.send(prompt, request);
643
+ if (spinner)
644
+ spinner.stop();
645
+ const assistantMessage = response.choices[0].message;
646
+ conversationMessages.push(assistantMessage);
647
+ outputHandler.outputMessage('assistant', assistantMessage.content);
648
+ // Show tool calls if any
649
+ if (assistantMessage.tool_calls) {
650
+ for (const toolCall of assistantMessage.tool_calls) {
651
+ outputHandler.outputToolCall(toolCall.function.name, JSON.parse(toolCall.function.arguments));
652
+ }
653
+ }
654
+ }
655
+ outputHandler.outputComplete();
656
+ }
657
+ async function setupMCP(builder, options) {
658
+ const mcpOptions = {};
659
+ if (options.mcpConfig) {
660
+ // Check if it's a JSON string or a file path
661
+ const configValue = options.mcpConfig.trim();
662
+ if (configValue.startsWith('{')) {
663
+ // It's a JSON string
664
+ mcpOptions.configString = configValue;
665
+ }
666
+ else {
667
+ // It's a file path
668
+ mcpOptions.configPath = configValue;
669
+ }
670
+ }
671
+ // Support legacy --mcp-servers option
672
+ if (options.mcpServers) {
673
+ mcpOptions.configString = options.mcpServers;
674
+ }
675
+ // Parse tool filtering patterns
676
+ if (options.allowedTools || options.disallowedTools) {
677
+ // Note: We could use createToolFilter here for more advanced filtering
678
+ // but for now we pass patterns directly to MCP plugin
679
+ if (options.allowedTools) {
680
+ mcpOptions.allowedTools = options.allowedTools
681
+ .split(/[,\s]+/)
682
+ .map((t) => t.trim())
683
+ .filter((t) => t.length > 0);
684
+ }
685
+ if (options.disallowedTools) {
686
+ mcpOptions.disallowedTools = options.disallowedTools
687
+ .split(/[,\s]+/)
688
+ .map((t) => t.trim())
689
+ .filter((t) => t.length > 0);
690
+ }
691
+ }
692
+ if (options.permissionPromptTool) {
693
+ mcpOptions.permissionPromptTool = options.permissionPromptTool;
694
+ }
695
+ // Enable quiet mode for stream-json to suppress console output
696
+ mcpOptions.quiet = options.outputFormat === 'stream-json';
697
+ const mcpPlugin = new majk_chat_mcp_1.MCPPlugin(mcpOptions);
698
+ const registry = builder.getToolRegistry();
699
+ if (registry) {
700
+ try {
701
+ await mcpPlugin.initialize(registry);
702
+ if (options.outputFormat !== 'stream-json') {
703
+ console.log(chalk_1.default.cyan('MCP servers connected and tools registered'));
704
+ }
705
+ }
706
+ catch (error) {
707
+ if (options.outputFormat !== 'stream-json') {
708
+ console.error(chalk_1.default.red('Failed to initialize MCP:'), error instanceof Error ? error.message : error);
709
+ }
710
+ // Continue without MCP rather than failing completely
711
+ }
712
+ }
713
+ return mcpPlugin;
714
+ }
715
+ async function getMessage(options) {
716
+ if (options.message) {
717
+ return options.message;
718
+ }
719
+ if (options.file) {
720
+ const content = await fs.readFile(options.file, 'utf-8');
721
+ return content.trim();
722
+ }
723
+ // Only try to read from stdin if it's a TTY (interactive terminal)
724
+ // In tests and CI environments, just return null instead of hanging
725
+ if (!process.stdin.isTTY) {
726
+ return null;
727
+ }
728
+ return new Promise((resolve) => {
729
+ let data = '';
730
+ process.stdin.on('data', chunk => data += chunk);
731
+ process.stdin.on('end', () => resolve(data.trim()));
732
+ });
733
+ }
734
+ async function getSystemMessage(options) {
735
+ let systemMessage = '';
736
+ // Get base system message
737
+ if (options.system) {
738
+ systemMessage = options.system;
739
+ }
740
+ else if (options.systemFile) {
741
+ const content = await fs.readFile(options.systemFile, 'utf-8');
742
+ systemMessage = content.trim();
743
+ }
744
+ // Append additional system prompt if provided
745
+ if (options.appendSystemPrompt) {
746
+ if (systemMessage) {
747
+ systemMessage += '\n\n' + options.appendSystemPrompt;
748
+ }
749
+ else {
750
+ systemMessage = options.appendSystemPrompt;
751
+ }
752
+ }
753
+ return systemMessage || null;
754
+ }
755
+ function getDefaultModel(provider) {
756
+ return (0, models_1.getDefaultModelForProvider)(provider || 'openai');
757
+ }
758
+ async function applyConfig(builder, config) {
759
+ if (config.providers) {
760
+ for (const [provider, providerConfig] of Object.entries(config.providers)) {
761
+ switch (provider) {
762
+ case 'openai':
763
+ builder.withOpenAI(providerConfig);
764
+ break;
765
+ case 'anthropic':
766
+ builder.withAnthropic(providerConfig);
767
+ break;
768
+ case 'azure-openai':
769
+ builder.withAzureOpenAI(providerConfig);
770
+ break;
771
+ case 'bedrock':
772
+ builder.withBedrock(providerConfig);
773
+ break;
774
+ }
775
+ }
776
+ }
777
+ if (config.defaultProvider) {
778
+ builder.setDefaultProvider(config.defaultProvider);
779
+ }
780
+ }
781
+ async function initializeConfig(configPath) {
782
+ const defaultConfig = {
783
+ providers: {
784
+ openai: {
785
+ apiKey: '${OPENAI_API_KEY}'
786
+ },
787
+ anthropic: {
788
+ apiKey: '${ANTHROPIC_API_KEY}'
789
+ },
790
+ 'azure-openai': {
791
+ apiKey: '${AZURE_OPENAI_API_KEY}',
792
+ endpoint: '${AZURE_OPENAI_ENDPOINT}',
793
+ deploymentName: '${AZURE_OPENAI_DEPLOYMENT}'
794
+ },
795
+ bedrock: {
796
+ region: '${AWS_REGION}',
797
+ accessKeyId: '${AWS_ACCESS_KEY_ID}',
798
+ secretAccessKey: '${AWS_SECRET_ACCESS_KEY}'
799
+ }
800
+ },
801
+ defaultProvider: 'openai',
802
+ tools: {
803
+ autoExecute: false,
804
+ maxSteps: 5
805
+ }
806
+ };
807
+ await fs.ensureDir(path.dirname(configPath));
808
+ await fs.writeFile(configPath, yaml.stringify(defaultConfig), 'utf-8');
809
+ }
810
+ async function setConfigValue(configPath, keyValue) {
811
+ const [key, ...valueParts] = keyValue.split('=');
812
+ const value = valueParts.join('=');
813
+ let config = {};
814
+ if (await fs.pathExists(configPath)) {
815
+ const content = await fs.readFile(configPath, 'utf-8');
816
+ config = yaml.parse(content);
817
+ }
818
+ // Set nested value
819
+ const keys = key.split('.');
820
+ let current = config;
821
+ for (let i = 0; i < keys.length - 1; i++) {
822
+ if (!current[keys[i]]) {
823
+ current[keys[i]] = {};
824
+ }
825
+ current = current[keys[i]];
826
+ }
827
+ current[keys[keys.length - 1]] = value;
828
+ await fs.ensureDir(path.dirname(configPath));
829
+ await fs.writeFile(configPath, yaml.stringify(config), 'utf-8');
830
+ console.log(chalk_1.default.green(`Set ${key} = ${value}`));
831
+ }
832
+ async function getConfigValue(configPath, key) {
833
+ if (!await fs.pathExists(configPath)) {
834
+ console.log(chalk_1.default.yellow('Configuration file not found'));
835
+ return;
836
+ }
837
+ const content = await fs.readFile(configPath, 'utf-8');
838
+ const config = yaml.parse(content);
839
+ // Get nested value
840
+ const keys = key.split('.');
841
+ let current = config;
842
+ for (const k of keys) {
843
+ if (current && typeof current === 'object' && k in current) {
844
+ current = current[k];
845
+ }
846
+ else {
847
+ console.log(chalk_1.default.yellow(`Key not found: ${key}`));
848
+ return;
849
+ }
850
+ }
851
+ console.log(current);
852
+ }
853
+ async function listConfig(configPath) {
854
+ if (!await fs.pathExists(configPath)) {
855
+ console.log(chalk_1.default.yellow('Configuration file not found'));
856
+ return;
857
+ }
858
+ const content = await fs.readFile(configPath, 'utf-8');
859
+ console.log(content);
860
+ }
861
+ /**
862
+ * Apply tool filtering to remove disallowed tools from the registry
863
+ */
864
+ function applyToolFiltering(registry, allowedTools, disallowedTools) {
865
+ if (!allowedTools && !disallowedTools)
866
+ return;
867
+ const toolFilter = (0, tool_filter_1.createToolFilter)(allowedTools, disallowedTools);
868
+ const definitions = registry.getDefinitions();
869
+ // Remove filtered tools
870
+ for (const definition of definitions) {
871
+ const toolName = definition.function?.name;
872
+ if (toolName && !toolFilter(toolName)) {
873
+ // Note: This assumes the registry has a remove method
874
+ // If not available, we would need to implement it or use a different approach
875
+ if (registry.remove) {
876
+ registry.remove(toolName);
877
+ }
878
+ }
879
+ }
880
+ }
881
+ /**
882
+ * Setup tool result management with automatic ReadToolResult registration
883
+ */
884
+ function setupToolResultManagement(builder, resultLimit, options) {
885
+ let readToolResultRegistered = false;
886
+ const truncationConfig = {
887
+ maxLength: resultLimit,
888
+ previewLength: Math.floor(resultLimit * 0.6),
889
+ tailLength: Math.floor(resultLimit * 0.3),
890
+ storeResult: true
891
+ };
892
+ // Set up tool result callback to handle truncation
893
+ const originalOnToolResult = options.onToolResult;
894
+ const onToolResult = (toolName, result, context) => {
895
+ // Check if result should be truncated
896
+ if (majk_chat_basic_tools_1.ToolResultTruncator.shouldTruncate(result, truncationConfig)) {
897
+ // Set conversation ID from context if available
898
+ const conversationId = context?.metadata?.conversationId || context?.requestId || 'cli-session';
899
+ const configWithConversation = { ...truncationConfig, conversationId };
900
+ // Process the result with truncation
901
+ const truncatedResult = majk_chat_basic_tools_1.ToolResultTruncator.processToolResult(result, toolName, configWithConversation);
902
+ // Auto-register ReadToolResult tool on first truncation
903
+ if (!readToolResultRegistered && truncatedResult.truncated) {
904
+ const registry = builder.getToolRegistry();
905
+ if (registry && !registry.has('read_tool_result')) {
906
+ registry.register(new majk_chat_basic_tools_1.ReadToolResultTool());
907
+ readToolResultRegistered = true;
908
+ console.log(chalk_1.default.yellow('📋 Large tool result detected. ReadToolResult tool auto-registered.'));
909
+ console.log(chalk_1.default.gray(` Use read_tool_result with ID: ${truncatedResult.full_result_id}`));
910
+ }
911
+ }
912
+ // Call original callback with truncated result
913
+ if (originalOnToolResult) {
914
+ originalOnToolResult(toolName, truncatedResult, context);
915
+ }
916
+ return truncatedResult;
917
+ }
918
+ // Call original callback for non-truncated results
919
+ if (originalOnToolResult) {
920
+ originalOnToolResult(toolName, result, context);
921
+ }
922
+ return result;
923
+ };
924
+ // Update the options to include our callback
925
+ options.onToolResult = onToolResult;
926
+ }
927
+ // Session helper functions
928
+ async function handleListSessions(sessionManager, workingDirectory, outputHandler) {
929
+ try {
930
+ const sessions = await sessionManager.listSessions(workingDirectory);
931
+ if (sessions.length === 0) {
932
+ console.log(chalk_1.default.yellow('No sessions found in this directory.'));
933
+ return;
934
+ }
935
+ console.log(chalk_1.default.cyan('\nSessions in current directory:'));
936
+ for (const session of sessions) {
937
+ console.log(` ${chalk_1.default.green(session.id)}`);
938
+ if (session.title) {
939
+ console.log(` Title: ${session.title}`);
940
+ }
941
+ console.log(` Created: ${session.createdAt.toLocaleDateString()} ${session.createdAt.toLocaleTimeString()}`);
942
+ console.log(` Updated: ${session.updatedAt.toLocaleDateString()} ${session.updatedAt.toLocaleTimeString()}`);
943
+ console.log(` Messages: ${session.totalMessages}`);
944
+ if (session.lastMessage) {
945
+ console.log(` Last: ${session.lastMessage}`);
946
+ }
947
+ console.log();
948
+ }
949
+ }
950
+ catch (error) {
951
+ outputHandler.outputError(`Failed to list sessions: ${error instanceof Error ? error.message : error}`);
952
+ }
953
+ }
954
+ async function handleDeleteSession(sessionManager, workingDirectory, sessionId, outputHandler) {
955
+ try {
956
+ const deleted = await sessionManager.deleteSession(sessionId, workingDirectory);
957
+ if (deleted) {
958
+ console.log(chalk_1.default.green(`Session ${sessionId} deleted successfully.`));
959
+ }
960
+ else {
961
+ outputHandler.outputError(`Session ${sessionId} not found.`);
962
+ }
963
+ }
964
+ catch (error) {
965
+ outputHandler.outputError(`Failed to delete session: ${error instanceof Error ? error.message : error}`);
966
+ }
967
+ }
968
+ async function saveConversationToSession(sessionManager, workingDirectory, sessionId, allMessages, assistantMessage, options) {
969
+ try {
970
+ // Add the assistant's response to the conversation
971
+ const finalMessages = [...allMessages, assistantMessage];
972
+ if (sessionId) {
973
+ // Update existing session
974
+ await sessionManager.addMessage(assistantMessage);
975
+ }
976
+ else {
977
+ // Create new session
978
+ sessionId = await sessionManager.createSession(workingDirectory, {
979
+ title: options.title,
980
+ provider: options.provider,
981
+ model: options.model,
982
+ initialMessages: finalMessages
983
+ });
984
+ // Auto-generate title if not provided
985
+ if (!options.title) {
986
+ await sessionManager.generateSessionTitle();
987
+ }
988
+ }
989
+ }
990
+ catch (error) {
991
+ console.error(chalk_1.default.yellow(`Warning: Failed to save session: ${error instanceof Error ? error.message : error}`));
992
+ }
993
+ }
994
+ async function handleSequentialPromptsWithSessions(chat, toolRegistry, options, outputHandler, _sessionManager, _sessionId) {
995
+ // This is a modified version of handleSequentialPrompts that works with sessions
996
+ // For now, we'll delegate to the original function and add session support later
997
+ await handleSequentialPrompts(chat, toolRegistry, options, outputHandler);
998
+ }
999
+ // Parse and execute
1000
+ program.parse(process.argv);
1001
+ //# sourceMappingURL=cli.js.map