@majkapp/majk-chat-cli 1.0.15 → 1.0.20

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/dist/cli.js CHANGED
@@ -49,8 +49,6 @@ const interactive_1 = require("./interactive/interactive");
49
49
  const credentials_1 = require("./utils/credentials");
50
50
  const profile_config_1 = require("./utils/profile-config");
51
51
  const config_1 = require("./utils/config");
52
- const bash_tool_1 = require("./tools/bash.tool");
53
- const filesystem_tool_1 = require("./tools/filesystem.tool");
54
52
  const majk_chat_mcp_1 = require("@majkapp/majk-chat-mcp");
55
53
  const majk_chat_basic_tools_1 = require("@majkapp/majk-chat-basic-tools");
56
54
  const majk_chat_document_tools_1 = require("@majkapp/majk-chat-document-tools");
@@ -58,13 +56,14 @@ const majk_chat_sessions_1 = require("@majkapp/majk-chat-sessions");
58
56
  const models_1 = require("./utils/models");
59
57
  const tool_filter_1 = require("./utils/tool-filter");
60
58
  const output_1 = require("./utils/output");
59
+ const executors_1 = require("./executors");
61
60
  // Load environment variables
62
61
  dotenv_1.default.config();
63
62
  const program = new commander_1.Command();
64
63
  program
65
64
  .name('majk-chat')
66
65
  .description('CLI for multi-provider LLM chat interactions')
67
- .version('1.0.15');
66
+ .version('1.0.20');
68
67
  // Add credential options helper function
69
68
  function addCredentialOptions(command) {
70
69
  return command
@@ -92,13 +91,15 @@ function addCredentialOptions(command) {
92
91
  }
93
92
  // Chat command
94
93
  const chatCommand = program
95
- .command('chat')
94
+ .command('chat [message...]')
96
95
  .description('Send a chat message to an LLM provider')
96
+ .option('--agent <type>', 'Executor to use: "majk" (default) or "claude-code". Can also be set via MAJK_AGENT environment variable')
97
97
  .option('--provider <provider>', 'Provider to use (openai, anthropic, azure-openai, bedrock)')
98
98
  .option('--model <model>', 'Model or alias (e.g., "sonnet", "opus", "claude-3-5-sonnet-20241022")')
99
99
  .option('-M, --message <message>', 'Message to send')
100
100
  .option('-p <value>', 'JSON messages array or prompt string')
101
101
  .option('-f, --file <file>', 'Read message from file')
102
+ .option('--image <paths...>', 'One or more image file paths to include with the message (PNG, JPEG, GIF, WebP)')
102
103
  .option('--prompts <file>', 'File with one prompt per line (sequential execution)')
103
104
  .option('-s, --system <system>', 'System message')
104
105
  .option('-S, --system-file <file>', 'Read system message from file')
@@ -107,7 +108,7 @@ const chatCommand = program
107
108
  .option('--max-tokens <tokens>', 'Maximum tokens', parseInt)
108
109
  .option('-j, --json', 'Output JSON response')
109
110
  .option('--output-format <format>', 'Output format: "text" (default) or "stream-json"', 'text')
110
- .option('--tools', 'Enable tool execution')
111
+ .option('--tools', 'Enable core tools (bash, read, write, etc.) - equivalent to --enable-core-tools')
111
112
  .option('--enable-core-tools', 'Enable majk-chat-basic-tools (bash, ls, read, etc.)')
112
113
  .option('--enable-read-only-tools', 'Enable read-only majk-chat-basic-tools (ls, read, grep, web_fetch, etc.)')
113
114
  .option('--disable-tools', 'Disable all tools including read-only tools (overrides defaults)')
@@ -129,13 +130,23 @@ const chatCommand = program
129
130
  .option('--list-sessions', 'List all sessions for current directory')
130
131
  .option('--delete-session <uuid>', 'Delete a specific session by UUID')
131
132
  .option('--dry-run', 'Enable dry run mode - plan operations without executing them')
132
- .option('--dry-run-review', 'Enable dry run mode with interactive review and execution of planned operations');
133
+ .option('--dry-run-review', 'Enable dry run mode with interactive review and execution of planned operations')
134
+ .option('--save-prompts <file>', 'Save all prompts to JSONL file for analysis and debugging');
133
135
  addCredentialOptions(chatCommand)
134
- .action(async (options) => {
136
+ .action(async (messageArgs, options) => {
135
137
  let cleanup = null;
136
138
  // Initialize output handler early so we can pass callbacks
137
139
  const outputHandler = new output_1.OutputHandler(options.outputFormat);
140
+ // Handle positional message arguments
141
+ if (messageArgs && messageArgs.length > 0 && !options.message && !options.file && !options.p) {
142
+ options.message = messageArgs.join(' ');
143
+ }
138
144
  try {
145
+ const agentType = options.agent || process.env.MAJK_AGENT || 'majk';
146
+ if (agentType === 'claude-code' || agentType === 'claude') {
147
+ await handleClaudeCodeExecutor(options, outputHandler);
148
+ return;
149
+ }
139
150
  const setupOptions = {
140
151
  ...options,
141
152
  outputFormat: options.outputFormat, // Pass output format to suppress logs
@@ -254,11 +265,11 @@ addCredentialOptions(chatCommand)
254
265
  messages: allMessages,
255
266
  temperature: options.temperature,
256
267
  max_tokens: options.maxTokens,
257
- // Always include tools if any are registered (from MCP or --tools flag)
268
+ // Always include tools if any are registered (from MCP or basic tools)
258
269
  tools: tools.length > 0 ? tools : undefined,
259
270
  // Enable auto-execution if tools are enabled (default unless --disable-tools) or MCP configured
260
271
  max_steps: (options.tools || !options.disableTools || options.mcpConfig || options.mcpServers)
261
- ? (options.maxSteps || 5)
272
+ ? (options.maxSteps || (!options.disableTools && !options.tools && !options.mcpConfig && !options.mcpServers ? 500 : 5))
262
273
  : 0,
263
274
  // Request transcript when saving sessions to capture complete tool execution history
264
275
  return_transcript: usesSessions
@@ -351,7 +362,7 @@ const interactiveCommand = program
351
362
  .option('-m, --model <model>', 'Model to use')
352
363
  .option('-s, --system <system>', 'System message')
353
364
  .option('-S, --system-file <file>', 'Read system message from file')
354
- .option('--tools', 'Enable tool execution')
365
+ .option('--tools', 'Enable core tools (bash, read, write, etc.) - equivalent to --enable-core-tools')
355
366
  .option('--enable-core-tools', 'Enable majk-chat-basic-tools (bash, ls, read, etc.)')
356
367
  .option('--enable-read-only-tools', 'Enable read-only majk-chat-basic-tools (ls, read, grep, web_fetch, etc.)')
357
368
  .option('--disable-tools', 'Disable all tools including read-only tools (overrides defaults)')
@@ -374,7 +385,7 @@ addCredentialOptions(interactiveCommand)
374
385
  const { chat, toolRegistry } = setupResult;
375
386
  cleanup = setupResult.cleanup;
376
387
  const systemMessage = await getSystemMessage(options);
377
- // Check if we have tools available from MCP or --tools flag
388
+ // Check if we have tools available from MCP or basic tools
378
389
  const hasTools = toolRegistry.getDefinitions().length > 0;
379
390
  const interactive = new interactive_1.InteractiveMode(chat, {
380
391
  provider: options.provider,
@@ -698,6 +709,74 @@ program
698
709
  }
699
710
  });
700
711
  // Helper functions
712
+ async function handleClaudeCodeExecutor(options, outputHandler) {
713
+ try {
714
+ await executors_1.ExecutorFactory.validateExecutorChoice('claude-code');
715
+ const messages = await buildMessages(options);
716
+ if (!messages || messages.length === 0) {
717
+ outputHandler.outputError('No message provided');
718
+ process.exit(1);
719
+ }
720
+ const credOptions = extractCredentialOptions(options);
721
+ const executorOptions = {
722
+ provider: options.provider,
723
+ model: options.model,
724
+ messages,
725
+ temperature: options.temperature,
726
+ maxTokens: options.maxTokens,
727
+ maxSteps: options.maxSteps,
728
+ systemPrompt: options.system || options.appendSystemPrompt,
729
+ outputFormat: options.outputFormat || 'text',
730
+ tools: options.tools,
731
+ enableCoreTools: options.enableCoreTools || options.tools,
732
+ enableReadOnlyTools: options.enableReadOnlyTools,
733
+ disableTools: options.disableTools,
734
+ planningMode: options.planningMode,
735
+ mcpConfig: options.mcpConfig,
736
+ mcpServers: options.mcpServers,
737
+ allowedTools: options.allowedTools,
738
+ disallowedTools: options.disallowedTools,
739
+ permissionPromptTool: options.permissionPromptTool,
740
+ bedrockApiKey: credOptions.bedrockApiKey,
741
+ awsAccessKeyId: credOptions.awsAccessKeyId,
742
+ awsSecretAccessKey: credOptions.awsSecretAccessKey,
743
+ awsSessionToken: credOptions.awsSessionToken,
744
+ awsRegion: credOptions.awsRegion,
745
+ anthropicApiKey: credOptions.anthropicApiKey,
746
+ openaiApiKey: credOptions.openaiApiKey,
747
+ cwd: process.cwd(),
748
+ onToolCall: options.outputFormat === 'stream-json'
749
+ ? (toolCall) => outputHandler.outputToolCallMessage(toolCall)
750
+ : undefined,
751
+ onToolResult: options.outputFormat === 'stream-json'
752
+ ? (toolCall, result) => outputHandler.outputToolResult(toolCall.id, result)
753
+ : undefined
754
+ };
755
+ const executor = executors_1.ExecutorFactory.create('claude-code');
756
+ const spinner = outputHandler.startSpinner('Sending message...');
757
+ const apiStart = Date.now();
758
+ const result = await executor.execute(executorOptions);
759
+ const apiDuration = Date.now() - apiStart;
760
+ if (spinner)
761
+ spinner.succeed('Response received');
762
+ if (options.json) {
763
+ console.log(JSON.stringify(result.response, null, 2));
764
+ }
765
+ else {
766
+ const assistantMessage = result.response.choices[0].message;
767
+ const content = typeof assistantMessage.content === 'string'
768
+ ? assistantMessage.content
769
+ : JSON.stringify(assistantMessage.content);
770
+ outputHandler.outputMessage('assistant', content, result.response.usage, assistantMessage.tool_calls);
771
+ outputHandler.outputComplete(result.response.usage, apiDuration);
772
+ }
773
+ process.exit(0);
774
+ }
775
+ catch (error) {
776
+ outputHandler.outputError(error instanceof Error ? error.message : String(error));
777
+ process.exit(1);
778
+ }
779
+ }
701
780
  // Extract credential options from CLI arguments
702
781
  function extractCredentialOptions(options) {
703
782
  return {
@@ -790,38 +869,16 @@ async function setupChat(options) {
790
869
  if (!hasValidCredentials) {
791
870
  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.');
792
871
  }
793
- // Add tools if enabled
794
- if (options.tools) {
795
- builder
796
- .withTool(new bash_tool_1.BashTool())
797
- .withTool(new filesystem_tool_1.FileSystemTool())
798
- .withAutoToolExecution(options.maxSteps || 5);
799
- // Override default context management if user specified custom tool result limit
800
- if (options.toolResultLimit && !isNaN(options.toolResultLimit) && options.toolResultLimit > 0) {
801
- builder.withContextManagement({
802
- enableTruncation: true,
803
- maxLength: options.toolResultLimit,
804
- conversationId: options.requestId || 'cli-session'
805
- });
806
- }
807
- }
808
872
  // Add tools - now enabled by default unless explicitly disabled
809
873
  const shouldEnableTools = !options.disableTools; // Default to true unless --disable-tools is used
810
874
  const enableReadOnlyTools = shouldEnableTools && (options.enableReadOnlyTools !== false); // Default to true
811
- const enableCoreTools = options.enableCoreTools;
875
+ const enableCoreTools = options.enableCoreTools || options.tools; // --tools now enables core tools
812
876
  const enablePlanningMode = options.planningMode;
813
877
  if (enableCoreTools || enableReadOnlyTools || enablePlanningMode) {
814
878
  const toolRegistry = builder.getToolRegistry();
815
879
  if (toolRegistry) {
816
- // Configure basic tools with planning mode settings
880
+ // Configure basic tools
817
881
  const basicToolsConfig = {};
818
- if (enablePlanningMode) {
819
- basicToolsConfig.planningMode = {
820
- enabled: true,
821
- reminderInterval: options.todoReminderInterval || 3,
822
- autoReminders: !options.disableAutoReminders
823
- };
824
- }
825
882
  // Register appropriate tool set based on flags
826
883
  if (enableCoreTools || enablePlanningMode) {
827
884
  // Register all tools (including write/execute tools)
@@ -835,29 +892,54 @@ async function setupChat(options) {
835
892
  // Register document tools (they are inherently read-only)
836
893
  (0, majk_chat_document_tools_1.registerDocumentTools)(toolRegistry);
837
894
  }
838
- // Setup planning mode reminders if enabled
839
- if (options.planningMode) {
840
- const instanceId = options.requestId || 'cli-session';
841
- (0, majk_chat_basic_tools_1.setupPlanningModeReminders)(instanceId, basicToolsConfig);
842
- }
843
895
  // Apply tool filtering if specified
844
896
  if (options.allowedTools || options.disallowedTools) {
845
897
  applyToolFiltering(toolRegistry, options.allowedTools, options.disallowedTools);
846
898
  }
847
- // Setup tool result truncation and auto-registration
899
+ // Setup context management with truncation using ContextManagementExtension
848
900
  // Handle the case where toolResultLimit might be null or NaN from commander parsing
849
901
  let toolResultLimit = 2000; // Much smaller default to handle large files within context limits
850
902
  if (options.toolResultLimit && !isNaN(options.toolResultLimit) && options.toolResultLimit > 0) {
851
903
  toolResultLimit = options.toolResultLimit;
852
- // Override default context management if user specified custom tool result limit
853
- builder.withContextManagement({
854
- enableTruncation: true,
855
- maxLength: toolResultLimit,
856
- conversationId: options.requestId || 'cli-session'
904
+ }
905
+ // Always setup context management with the extension (replaces legacy setupToolResultManagement)
906
+ builder.withContextManagement({
907
+ enableTruncation: true,
908
+ maxLength: toolResultLimit,
909
+ conversationId: options.requestId || 'cli-session'
910
+ });
911
+ // Setup prompt logging if requested
912
+ if (options.savePrompts) {
913
+ builder.withExtension(new majk_chat_core_1.PromptLoggingExtension({
914
+ filePath: options.savePrompts,
915
+ includeContext: true,
916
+ includeTools: true,
917
+ includeMessages: true,
918
+ append: true
919
+ }));
920
+ console.log(chalk_1.default.cyan(`📝 Prompt logging enabled: ${options.savePrompts}`));
921
+ }
922
+ // Setup planning mode extension if requested
923
+ if (enablePlanningMode) {
924
+ builder.withPlanningMode({
925
+ enabled: true,
926
+ autoActivation: {
927
+ enabled: true,
928
+ complexityThreshold: 70
929
+ },
930
+ guidance: {
931
+ reminderInterval: options.todoReminderInterval || 3,
932
+ enableProgressTracking: !options.disableAutoReminders
933
+ },
934
+ session: {
935
+ conversationId: options.requestId || 'cli-session'
936
+ }
857
937
  });
938
+ console.log(chalk_1.default.cyan(`🎯 Planning mode enabled: Research → Plan → Execute workflow`));
858
939
  }
859
- setupToolResultManagement(builder, toolResultLimit, options);
860
- builder.withAutoToolExecution(options.maxSteps || 5);
940
+ // Set default steps based on tool type - 500 for read-only tools by default, 5 for other scenarios
941
+ const defaultSteps = enableReadOnlyTools && !enableCoreTools && !enablePlanningMode ? 500 : 5;
942
+ builder.withAutoToolExecution(options.maxSteps || defaultSteps);
861
943
  }
862
944
  }
863
945
  let mcpPlugin = null;
@@ -867,14 +949,6 @@ async function setupChat(options) {
867
949
  // Enable auto tool execution if MCP is configured
868
950
  if (!options.tools) {
869
951
  builder.withAutoToolExecution(options.maxSteps || 5);
870
- // Override default context management if user specified custom tool result limit
871
- if (options.toolResultLimit && !isNaN(options.toolResultLimit) && options.toolResultLimit > 0) {
872
- builder.withContextManagement({
873
- enableTruncation: true,
874
- maxLength: options.toolResultLimit,
875
- conversationId: options.requestId || 'cli-session'
876
- });
877
- }
878
952
  }
879
953
  }
880
954
  // Set tool callbacks if provided
@@ -921,6 +995,31 @@ async function setupChat(options) {
921
995
  }
922
996
  };
923
997
  }
998
+ async function loadImageFromPath(filePath) {
999
+ const resolvedPath = path.resolve(filePath.replace(/^~/, process.env.HOME || '~'));
1000
+ const ext = path.extname(resolvedPath).toLowerCase();
1001
+ const mediaTypeMap = {
1002
+ '.png': 'image/png',
1003
+ '.jpg': 'image/jpeg',
1004
+ '.jpeg': 'image/jpeg',
1005
+ '.gif': 'image/gif',
1006
+ '.webp': 'image/webp'
1007
+ };
1008
+ const media_type = mediaTypeMap[ext];
1009
+ if (!media_type) {
1010
+ throw new Error(`Unsupported image format: ${ext}. Supported: .png, .jpg, .jpeg, .gif, .webp`);
1011
+ }
1012
+ const buffer = await fs.readFile(resolvedPath);
1013
+ const base64Data = buffer.toString('base64');
1014
+ return {
1015
+ type: 'image',
1016
+ source: {
1017
+ type: 'base64',
1018
+ media_type,
1019
+ data: base64Data
1020
+ }
1021
+ };
1022
+ }
924
1023
  async function buildMessages(options) {
925
1024
  const messages = [];
926
1025
  // Add system message if provided
@@ -952,7 +1051,31 @@ async function buildMessages(options) {
952
1051
  // Use traditional message options
953
1052
  const message = await getMessage(options);
954
1053
  if (message) {
955
- messages.push({ role: 'user', content: message });
1054
+ // Check if images are provided
1055
+ if (options.image && Array.isArray(options.image) && options.image.length > 0) {
1056
+ // Build multi-modal content with text + images
1057
+ const contentBlocks = [];
1058
+ // Add text block first
1059
+ contentBlocks.push({
1060
+ type: 'text',
1061
+ text: message
1062
+ });
1063
+ // Add image blocks
1064
+ for (const imagePath of options.image) {
1065
+ try {
1066
+ const imageBlock = await loadImageFromPath(imagePath);
1067
+ contentBlocks.push(imageBlock);
1068
+ }
1069
+ catch (error) {
1070
+ throw new Error(`Failed to load image ${imagePath}: ${error instanceof Error ? error.message : error}`);
1071
+ }
1072
+ }
1073
+ messages.push({ role: 'user', content: contentBlocks });
1074
+ }
1075
+ else {
1076
+ // Simple text message
1077
+ messages.push({ role: 'user', content: message });
1078
+ }
956
1079
  }
957
1080
  }
958
1081
  return messages;
@@ -1227,52 +1350,6 @@ function applyToolFiltering(registry, allowedTools, disallowedTools) {
1227
1350
  }
1228
1351
  }
1229
1352
  }
1230
- /**
1231
- * Setup tool result management with automatic ReadToolResult registration
1232
- */
1233
- function setupToolResultManagement(builder, resultLimit, options) {
1234
- let readToolResultRegistered = false;
1235
- const truncationConfig = {
1236
- maxLength: resultLimit,
1237
- previewLength: Math.floor(resultLimit * 0.6),
1238
- tailLength: Math.floor(resultLimit * 0.3),
1239
- storeResult: true
1240
- };
1241
- // Set up tool result callback to handle truncation
1242
- const originalOnToolResult = options.onToolResult;
1243
- const onToolResult = (toolName, result, context) => {
1244
- // Check if result should be truncated
1245
- if (majk_chat_basic_tools_1.ToolResultTruncator.shouldTruncate(result, truncationConfig)) {
1246
- // Set conversation ID from context if available
1247
- const conversationId = context?.metadata?.conversationId || context?.requestId || 'cli-session';
1248
- const configWithConversation = { ...truncationConfig, conversationId };
1249
- // Process the result with truncation
1250
- const truncatedResult = majk_chat_basic_tools_1.ToolResultTruncator.processToolResult(result, toolName, configWithConversation);
1251
- // Auto-register ReadToolResult tool on first truncation
1252
- if (!readToolResultRegistered && truncatedResult.truncated) {
1253
- const registry = builder.getToolRegistry();
1254
- if (registry && !registry.has('read_tool_result')) {
1255
- registry.register(new majk_chat_basic_tools_1.ReadToolResultTool());
1256
- readToolResultRegistered = true;
1257
- console.log(chalk_1.default.yellow('📋 Large tool result detected. ReadToolResult tool auto-registered.'));
1258
- console.log(chalk_1.default.gray(` Use read_tool_result with ID: ${truncatedResult.full_result_id}`));
1259
- }
1260
- }
1261
- // Call original callback with truncated result
1262
- if (originalOnToolResult) {
1263
- originalOnToolResult(toolName, truncatedResult, context);
1264
- }
1265
- return truncatedResult;
1266
- }
1267
- // Call original callback for non-truncated results
1268
- if (originalOnToolResult) {
1269
- originalOnToolResult(toolName, result, context);
1270
- }
1271
- return result;
1272
- };
1273
- // Update the options to include our callback
1274
- options.onToolResult = onToolResult;
1275
- }
1276
1353
  // Session helper functions
1277
1354
  async function handleListSessions(sessionManager, workingDirectory, outputHandler) {
1278
1355
  try {