@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.
- package/README.md +746 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1001 -0
- package/dist/cli.js.map +1 -0
- package/dist/interactive/interactive.d.ts +27 -0
- package/dist/interactive/interactive.d.ts.map +1 -0
- package/dist/interactive/interactive.js +361 -0
- package/dist/interactive/interactive.js.map +1 -0
- package/dist/tools/bash.tool.d.ts +11 -0
- package/dist/tools/bash.tool.d.ts.map +1 -0
- package/dist/tools/bash.tool.js +140 -0
- package/dist/tools/bash.tool.js.map +1 -0
- package/dist/tools/filesystem.tool.d.ts +20 -0
- package/dist/tools/filesystem.tool.d.ts.map +1 -0
- package/dist/tools/filesystem.tool.js +312 -0
- package/dist/tools/filesystem.tool.js.map +1 -0
- package/dist/utils/config.d.ts +8 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +140 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/credentials.d.ts +45 -0
- package/dist/utils/credentials.d.ts.map +1 -0
- package/dist/utils/credentials.js +265 -0
- package/dist/utils/credentials.js.map +1 -0
- package/dist/utils/models.d.ts +10 -0
- package/dist/utils/models.d.ts.map +1 -0
- package/dist/utils/models.js +56 -0
- package/dist/utils/models.js.map +1 -0
- package/dist/utils/output.d.ts +133 -0
- package/dist/utils/output.d.ts.map +1 -0
- package/dist/utils/output.js +306 -0
- package/dist/utils/output.js.map +1 -0
- package/dist/utils/tool-filter.d.ts +35 -0
- package/dist/utils/tool-filter.d.ts.map +1 -0
- package/dist/utils/tool-filter.js +94 -0
- package/dist/utils/tool-filter.js.map +1 -0
- 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
|