diff --git a/packages/agent/src/core/executeToolCall.ts b/packages/agent/src/core/executeToolCall.ts index 0de1cdf..462ddfa 100644 --- a/packages/agent/src/core/executeToolCall.ts +++ b/packages/agent/src/core/executeToolCall.ts @@ -39,7 +39,25 @@ export const executeToolCall = async ( } // TODO: validate JSON schema for input - const output = await tool.execute(toolCall.input, toolContext); + let output; + try { + output = await tool.execute(toolCall.input, toolContext); + } catch (err) { + if (err instanceof Error) { + logger.error(err.message); + return JSON.stringify({ + error: true, + message: err.message, + stack: err.stack, + }); + } else { + logger.error(err); + return JSON.stringify({ + error: true, + message: err, + }); + } + } // for each result log it and its name if (tool.logReturns) { diff --git a/packages/agent/src/core/toolAgent.test.ts b/packages/agent/src/core/toolAgent.test.ts index 16fd17f..292906a 100644 --- a/packages/agent/src/core/toolAgent.test.ts +++ b/packages/agent/src/core/toolAgent.test.ts @@ -14,6 +14,7 @@ const toolContext: ToolContext = { userSession: false, pageFilter: 'simple', tokenTracker: new TokenTracker(), + githubMode: false, }; // Mock tool for testing @@ -98,16 +99,21 @@ describe('toolAgent', () => { }); it('should handle tool execution errors', async () => { - await expect( - executeToolCall( - { - id: '1', - name: 'errorTool', - input: {}, - }, - [errorTool], - toolContext, - ), - ).rejects.toThrow('Deliberate failure'); + const result = await executeToolCall( + { + id: '1', + name: 'errorTool', + input: {}, + }, + [errorTool], + toolContext, + ); + + // Parse the result as JSON + const parsedResult = JSON.parse(result); + + // Check that it contains the expected error properties + expect(parsedResult.error).toBe(true); + expect(parsedResult.message).toContain('Deliberate failure'); }); }); diff --git a/packages/agent/src/core/toolAgent/config.ts b/packages/agent/src/core/toolAgent/config.ts index 6bb7785..e66737c 100644 --- a/packages/agent/src/core/toolAgent/config.ts +++ b/packages/agent/src/core/toolAgent/config.ts @@ -30,6 +30,8 @@ export function getModel(provider: ModelProvider, modelName: string) { } } +import { ToolContext } from '../types'; + /** * Default configuration for the tool agent */ @@ -44,9 +46,7 @@ export const DEFAULT_CONFIG = { /** * Gets the default system prompt with contextual information about the environment */ -export function getDefaultSystemPrompt(options?: { - githubMode?: boolean; -}): string { +export function getDefaultSystemPrompt(toolContext: ToolContext): string { // Gather context with error handling const getCommandOutput = (command: string, label: string): string => { try { @@ -61,7 +61,7 @@ export function getDefaultSystemPrompt(options?: { files: getCommandOutput('ls -la', 'file listing'), system: getCommandOutput('uname -a', 'system information'), datetime: new Date().toString(), - githubMode: options?.githubMode || false, + githubMode: toolContext.githubMode, }; const githubModeInstructions = context.githubMode @@ -110,7 +110,7 @@ export function getDefaultSystemPrompt(options?: { '3. Update documentation as needed', '4. Consider adding documentation if you encountered setup/understanding challenges', '', - 'Feel free to use Google and Bing via the browser tools to search for information or for ideas when you get stuck.', + 'Feel free to use AI friendly search engines via the browser tools to search for information or for ideas when you get stuck.', '', 'When you run into issues or unexpected results, take a step back and read the project documentation and configuration files and look at other source files in the project for examples of what works.', '', diff --git a/packages/agent/src/core/toolAgent/index.ts b/packages/agent/src/core/toolAgent/index.ts index 6076672..1d5b3dd 100644 --- a/packages/agent/src/core/toolAgent/index.ts +++ b/packages/agent/src/core/toolAgent/index.ts @@ -43,7 +43,7 @@ export const toolAgent = async ( logger.debug('User message:', initialPrompt); // Get the system prompt once at the start - const systemPrompt = config.getSystemPrompt(); + const systemPrompt = config.getSystemPrompt(context); for (let i = 0; i < config.maxIterations; i++) { logger.verbose( diff --git a/packages/agent/src/core/toolAgent/toolExecutor.ts b/packages/agent/src/core/toolAgent/toolExecutor.ts index 56fa15c..52c0f64 100644 --- a/packages/agent/src/core/toolAgent/toolExecutor.ts +++ b/packages/agent/src/core/toolAgent/toolExecutor.ts @@ -49,11 +49,18 @@ export async function executeTools( ...context, tokenTracker: new TokenTracker(call.name, context.tokenTracker), }); - } catch (error: any) { - toolResult = JSON.stringify({ - errorMessage: error.message, - errorType: error.constructor.name, - }); + } catch (errorStr: any) { + if (errorStr instanceof Error) { + if (errorStr.stack) { + context.logger.error(`Tool error stack trace: ${errorStr.stack}`); + } + toolResult = JSON.stringify(errorStr); + } else { + toolResult = JSON.stringify({ + errorMessage: errorStr.message, + errorType: errorStr.name, + }); + } } return { diff --git a/packages/agent/src/core/types.ts b/packages/agent/src/core/types.ts index 1bdab20..4e66a80 100644 --- a/packages/agent/src/core/types.ts +++ b/packages/agent/src/core/types.ts @@ -16,6 +16,7 @@ export type ToolContext = { userSession: boolean; pageFilter: pageFilter; tokenTracker: TokenTracker; + githubMode: boolean; }; export type Tool, TReturn = any> = { diff --git a/packages/agent/src/tools/interaction/subAgent.ts b/packages/agent/src/tools/interaction/subAgent.ts index d808e0f..25e80b3 100644 --- a/packages/agent/src/tools/interaction/subAgent.ts +++ b/packages/agent/src/tools/interaction/subAgent.ts @@ -1,9 +1,10 @@ import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; +import { getDefaultSystemPrompt } from '../../core/toolAgent/index.js'; import { getModel } from '../../core/toolAgent/config.js'; import { toolAgent } from '../../core/toolAgent.js'; -import { Tool } from '../../core/types.js'; +import { Tool, ToolContext } from '../../core/types.js'; import { getTools } from '../getTools.js'; const parameterSchema = z.object({ @@ -53,8 +54,9 @@ const subAgentConfig = { model: getModel('anthropic', 'claude-3-7-sonnet-20250219'), maxTokens: 4096, temperature: 0.7, - getSystemPrompt: () => { + getSystemPrompt: (context: ToolContext) => { return [ + getDefaultSystemPrompt(context), 'You are a focused AI sub-agent handling a specific task.', 'You have access to the same tools as the main agent but should focus only on your assigned task.', 'When complete, call the sequenceComplete tool with your results.', diff --git a/packages/agent/src/tools/io/textEditor.test.ts b/packages/agent/src/tools/io/textEditor.test.ts index c218e6f..4b7f974 100644 --- a/packages/agent/src/tools/io/textEditor.test.ts +++ b/packages/agent/src/tools/io/textEditor.test.ts @@ -19,6 +19,7 @@ const toolContext: ToolContext = { userSession: false, pageFilter: 'simple', tokenTracker: new TokenTracker(), + githubMode: false, }; describe('textEditor', () => { @@ -259,18 +260,16 @@ describe('textEditor', () => { const testPath = join(testDir, `${randomUUID()}.txt`); // Try to view a non-existent file - const result = await textEditorTool.execute( - { - command: 'view', - path: testPath, - description: 'test', - }, - toolContext, - ); - - // Verify return value - expect(result.success).toBe(false); - expect(result.message).toContain('not found'); + await expect(async () => { + await textEditorTool.execute( + { + command: 'view', + path: testPath, + description: 'test', + }, + toolContext, + ); + }).rejects.toThrow(/not found/); }); it('should handle errors for duplicate string replacements', async () => { @@ -291,19 +290,17 @@ describe('textEditor', () => { ); // Try to replace text with multiple occurrences - const result = await textEditorTool.execute( - { - command: 'str_replace', - path: testPath, - old_str: oldStr, - new_str: newStr, - description: 'test', - }, - toolContext, - ); - - // Verify return value - expect(result.success).toBe(false); - expect(result.message).toContain('Found 2 occurrences'); + await expect(async () => { + await textEditorTool.execute( + { + command: 'str_replace', + path: testPath, + old_str: oldStr, + new_str: newStr, + description: 'test', + }, + toolContext, + ); + }).rejects.toThrow(/Found 2 occurrences/); }); }); diff --git a/packages/agent/src/tools/io/textEditor.ts b/packages/agent/src/tools/io/textEditor.ts index c0a1d89..ce487ea 100644 --- a/packages/agent/src/tools/io/textEditor.ts +++ b/packages/agent/src/tools/io/textEditor.ts @@ -97,245 +97,203 @@ export const textEditorTool: Tool = { ? path.join(context.workingDirectory, normalizedPath) : path.resolve(normalizedPath); - try { - switch (command) { - case 'view': { - // Check if path is a directory - const stats = await fs.stat(absolutePath).catch(() => null); - if (!stats) { - return { - success: false, - message: `File or directory not found: ${filePath}`, - }; - } - - if (stats.isDirectory()) { - // List directory contents up to 2 levels deep - try { - const output = execSync( - `find "${absolutePath}" -type f -not -path "*/\\.*" -maxdepth 2 | sort`, - { encoding: 'utf8' }, - ); - return { - success: true, - message: `Directory listing for ${filePath}:`, - content: output, - }; - } catch (error) { - return { - success: false, - message: `Error listing directory: ${error}`, - }; - } - } else { - // Read file content - const content = await fs.readFile(absolutePath, 'utf8'); - const lines = content.split('\n'); - - // Apply view range if specified - let displayContent = content; - if (view_range && view_range.length === 2) { - const [start, end] = view_range; - const startLine = Math.max(1, start || 1) - 1; // Convert to 0-indexed - const endLine = end === -1 ? lines.length : end; - displayContent = lines.slice(startLine, endLine).join('\n'); - } - - // Add line numbers - const startLineNum = - view_range && view_range.length === 2 ? view_range[0] : 1; - const numberedContent = displayContent - .split('\n') - .map((line, i) => `${(startLineNum || 1) + i}: ${line}`) - .join('\n'); - - // Truncate if too large - if (numberedContent.length > OUTPUT_LIMIT) { - const truncatedContent = numberedContent.substring( - 0, - OUTPUT_LIMIT, - ); - return { - success: true, - message: `File content (truncated):`, - content: `${truncatedContent}\n`, - }; - } + switch (command) { + case 'view': { + // Check if path is a directory + const stats = await fs.stat(absolutePath).catch(() => null); + if (!stats) { + throw new Error(`File or directory not found: ${filePath}`); + } + if (stats.isDirectory()) { + // List directory contents up to 2 levels deep + try { + const output = execSync( + `find "${absolutePath}" -type f -not -path "*/\\.*" -maxdepth 2 | sort`, + { encoding: 'utf8' }, + ); return { success: true, - message: `File content:`, - content: numberedContent, + message: `Directory listing for ${filePath}:`, + content: output, }; + } catch (error) { + throw new Error(`Error listing directory: ${error}`); } - } + } else { + // Read file content + const content = await fs.readFile(absolutePath, 'utf8'); + const lines = content.split('\n'); - case 'create': { - // Check if file already exists - if (fsSync.existsSync(absolutePath)) { - return { - success: false, - message: `File already exists: ${filePath}. Use str_replace to modify it.`, - }; + // Apply view range if specified + let displayContent = content; + if (view_range && view_range.length === 2) { + const [start, end] = view_range; + const startLine = Math.max(1, start || 1) - 1; // Convert to 0-indexed + const endLine = end === -1 ? lines.length : end; + displayContent = lines.slice(startLine, endLine).join('\n'); } - if (!file_text) { + // Add line numbers + const startLineNum = + view_range && view_range.length === 2 ? view_range[0] : 1; + const numberedContent = displayContent + .split('\n') + .map((line, i) => `${(startLineNum || 1) + i}: ${line}`) + .join('\n'); + + // Truncate if too large + if (numberedContent.length > OUTPUT_LIMIT) { + const truncatedContent = numberedContent.substring(0, OUTPUT_LIMIT); return { - success: false, - message: 'file_text parameter is required for create command', + success: true, + message: `File content (truncated):`, + content: `${truncatedContent}\n`, }; } - // Create parent directories if they don't exist - await fs.mkdir(path.dirname(absolutePath), { recursive: true }); - - // Create the file - await fs.writeFile(absolutePath, file_text, 'utf8'); - - // Store initial state for undo - fileStateHistory[absolutePath] = [file_text]; - return { success: true, - message: `File created: ${filePath}`, + message: `File content:`, + content: numberedContent, }; } + } - case 'str_replace': { - if (!old_str) { - return { - success: false, - message: 'old_str parameter is required for str_replace command', - }; - } + case 'create': { + // Check if file already exists + if (fsSync.existsSync(absolutePath)) { + throw new Error( + `File already exists: ${filePath}. Use str_replace to modify it.`, + ); + } - // Ensure the file exists - if (!fsSync.existsSync(absolutePath)) { - return { - success: false, - message: `File not found: ${filePath}`, - }; - } + if (!file_text) { + throw new Error('file_text parameter is required for create command'); + } - // Read the current content - const content = await fs.readFile(absolutePath, 'utf8'); + // Create parent directories if they don't exist + await fs.mkdir(path.dirname(absolutePath), { recursive: true }); - // Check if old_str exists uniquely in the file - const occurrences = content.split(old_str).length - 1; - if (occurrences === 0) { - return { - success: false, - message: `The specified old_str was not found in the file`, - }; - } - if (occurrences > 1) { - return { - success: false, - message: `Found ${occurrences} occurrences of old_str, expected exactly 1`, - }; - } + // Create the file + await fs.writeFile(absolutePath, file_text, 'utf8'); - // Save current state for undo - if (!fileStateHistory[absolutePath]) { - fileStateHistory[absolutePath] = []; - } - fileStateHistory[absolutePath].push(content); + // Store initial state for undo + fileStateHistory[absolutePath] = [file_text]; - // Replace the content - const updatedContent = content.replace(old_str, new_str || ''); - await fs.writeFile(absolutePath, updatedContent, 'utf8'); + return { + success: true, + message: `File created: ${filePath}`, + }; + } - return { - success: true, - message: `Successfully replaced text in ${filePath}`, - }; + case 'str_replace': { + if (!old_str) { + throw new Error( + 'old_str parameter is required for str_replace command', + ); } - case 'insert': { - if (insert_line === undefined) { - return { - success: false, - message: 'insert_line parameter is required for insert command', - }; - } + // Ensure the file exists + if (!fsSync.existsSync(absolutePath)) { + throw new Error(`File not found: ${filePath}`); + } - if (!new_str) { - return { - success: false, - message: 'new_str parameter is required for insert command', - }; - } + // Read the current content + const content = await fs.readFile(absolutePath, 'utf8'); - // Ensure the file exists - if (!fsSync.existsSync(absolutePath)) { - return { - success: false, - message: `File not found: ${filePath}`, - }; - } + // Check if old_str exists uniquely in the file + const occurrences = content.split(old_str).length - 1; + if (occurrences === 0) { + throw new Error(`The specified old_str was not found in the file`); + } + if (occurrences > 1) { + throw new Error( + `Found ${occurrences} occurrences of old_str, expected exactly 1`, + ); + } - // Read the current content - const content = await fs.readFile(absolutePath, 'utf8'); - const lines = content.split('\n'); + // Save current state for undo + if (!fileStateHistory[absolutePath]) { + fileStateHistory[absolutePath] = []; + } + fileStateHistory[absolutePath].push(content); - // Validate line number - if (insert_line < 0 || insert_line > lines.length) { - return { - success: false, - message: `Invalid line number: ${insert_line}. File has ${lines.length} lines.`, - }; - } + // Replace the content + const updatedContent = content.replace(old_str, new_str || ''); + await fs.writeFile(absolutePath, updatedContent, 'utf8'); - // Save current state for undo - if (!fileStateHistory[absolutePath]) { - fileStateHistory[absolutePath] = []; - } - fileStateHistory[absolutePath].push(content); + return { + success: true, + message: `Successfully replaced text in ${filePath}`, + }; + } - // Insert the new content after the specified line - lines.splice(insert_line, 0, new_str); - const updatedContent = lines.join('\n'); - await fs.writeFile(absolutePath, updatedContent, 'utf8'); + case 'insert': { + if (insert_line === undefined) { + throw new Error( + 'insert_line parameter is required for insert command', + ); + } - return { - success: true, - message: `Successfully inserted text after line ${insert_line} in ${filePath}`, - }; + if (!new_str) { + throw new Error('new_str parameter is required for insert command'); } - case 'undo_edit': { - // Check if we have history for this file - if ( - !fileStateHistory[absolutePath] || - fileStateHistory[absolutePath].length === 0 - ) { - return { - success: false, - message: `No edit history found for ${filePath}`, - }; - } + // Ensure the file exists + if (!fsSync.existsSync(absolutePath)) { + throw new Error(`File not found: ${filePath}`); + } - // Get the previous state - const previousState = fileStateHistory[absolutePath].pop(); - await fs.writeFile(absolutePath, previousState as string, 'utf8'); + // Read the current content + const content = await fs.readFile(absolutePath, 'utf8'); + const lines = content.split('\n'); - return { - success: true, - message: `Successfully reverted last edit to ${filePath}`, - }; + // Validate line number + if (insert_line < 0 || insert_line > lines.length) { + throw new Error( + `Invalid line number: ${insert_line}. File has ${lines.length} lines.`, + ); } - default: - return { - success: false, - message: `Unknown command: ${command}`, - }; + // Save current state for undo + if (!fileStateHistory[absolutePath]) { + fileStateHistory[absolutePath] = []; + } + fileStateHistory[absolutePath].push(content); + + // Insert the new content after the specified line + lines.splice(insert_line, 0, new_str); + const updatedContent = lines.join('\n'); + await fs.writeFile(absolutePath, updatedContent, 'utf8'); + + return { + success: true, + message: `Successfully inserted text after line ${insert_line} in ${filePath}`, + }; } - } catch (error: any) { - return { - success: false, - message: `Error: ${error.message}`, - }; + + case 'undo_edit': { + // Check if we have history for this file + if ( + !fileStateHistory[absolutePath] || + fileStateHistory[absolutePath].length === 0 + ) { + throw new Error(`No edit history found for ${filePath}`); + } + + // Get the previous state + const previousState = fileStateHistory[absolutePath].pop(); + await fs.writeFile(absolutePath, previousState as string, 'utf8'); + + return { + success: true, + message: `Successfully reverted last edit to ${filePath}`, + }; + } + + default: + throw new Error(`Unknown command: ${command}`); } }, logParameters: (input, { logger }) => { diff --git a/packages/agent/src/tools/system/respawn.test.ts b/packages/agent/src/tools/system/respawn.test.ts index 0d80cfd..ed968b4 100644 --- a/packages/agent/src/tools/system/respawn.test.ts +++ b/packages/agent/src/tools/system/respawn.test.ts @@ -13,6 +13,7 @@ const toolContext: ToolContext = { userSession: false, pageFilter: 'simple', tokenTracker: new TokenTracker(), + githubMode: false, }; describe('respawnTool', () => { it('should have correct name and description', () => { diff --git a/packages/agent/src/tools/system/shellExecute.test.ts b/packages/agent/src/tools/system/shellExecute.test.ts index 5670f3e..16715a7 100644 --- a/packages/agent/src/tools/system/shellExecute.test.ts +++ b/packages/agent/src/tools/system/shellExecute.test.ts @@ -13,6 +13,7 @@ const toolContext: ToolContext = { userSession: false, pageFilter: 'simple', tokenTracker: new TokenTracker(), + githubMode: false, }; describe('shellExecute', () => { diff --git a/packages/agent/src/tools/system/shellMessage.test.ts b/packages/agent/src/tools/system/shellMessage.test.ts index 4f89457..918508b 100644 --- a/packages/agent/src/tools/system/shellMessage.test.ts +++ b/packages/agent/src/tools/system/shellMessage.test.ts @@ -15,6 +15,7 @@ const toolContext: ToolContext = { userSession: false, pageFilter: 'simple', tokenTracker: new TokenTracker(), + githubMode: false, }; // Helper function to get instanceId from shellStart result diff --git a/packages/agent/src/tools/system/shellStart.test.ts b/packages/agent/src/tools/system/shellStart.test.ts index 88a0764..7984296 100644 --- a/packages/agent/src/tools/system/shellStart.test.ts +++ b/packages/agent/src/tools/system/shellStart.test.ts @@ -14,6 +14,7 @@ const toolContext: ToolContext = { userSession: false, pageFilter: 'simple', tokenTracker: new TokenTracker(), + githubMode: false, }; describe('shellStartTool', () => { beforeEach(() => { diff --git a/packages/agent/src/tools/system/sleep.test.ts b/packages/agent/src/tools/system/sleep.test.ts index 725fe68..8243769 100644 --- a/packages/agent/src/tools/system/sleep.test.ts +++ b/packages/agent/src/tools/system/sleep.test.ts @@ -13,6 +13,7 @@ const toolContext: ToolContext = { userSession: false, pageFilter: 'simple', tokenTracker: new TokenTracker(), + githubMode: false, }; describe('sleep tool', () => { diff --git a/packages/cli/README.md b/packages/cli/README.md index 251bd41..7a5b8d4 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -76,6 +76,26 @@ mycoder config get githubMode mycoder config set githubMode true ``` +### Available Configuration Options + +- `githubMode`: Enable GitHub mode for working with issues and PRs (default: `false`) +- `headless`: Run browser in headless mode with no UI showing (default: `true`) +- `userSession`: Use user's existing browser session instead of sandboxed session (default: `false`) +- `pageFilter`: Method to process webpage content: 'simple', 'none', or 'readability' (default: `none`) + +Example: + +```bash +# Set browser to show UI +mycoder config set headless false + +# Use existing browser session +mycoder config set userSession true + +# Use readability for webpage processing +mycoder config set pageFilter readability +``` + ## Environment Variables - `ANTHROPIC_API_KEY`: Your Anthropic API key (required) diff --git a/packages/cli/src/commands/$default.ts b/packages/cli/src/commands/$default.ts index 1113ace..cdfaab2 100644 --- a/packages/cli/src/commands/$default.ts +++ b/packages/cli/src/commands/$default.ts @@ -160,6 +160,7 @@ export const command: CommandModule = { ); process.exit(0); }); + const config = await getConfig(); // Create a config with the selected model const agentConfig = { @@ -172,11 +173,12 @@ export const command: CommandModule = { const result = await toolAgent(prompt, tools, agentConfig, { logger, - headless: argv.headless ?? true, - userSession: argv.userSession ?? false, - pageFilter: argv.pageFilter ?? 'none', + headless: argv.headless ?? config.headless, + userSession: argv.userSession ?? config.userSession, + pageFilter: argv.pageFilter ?? config.pageFilter, workingDirectory: '.', tokenTracker, + githubMode: config.githubMode, }); const output = diff --git a/packages/cli/src/commands/.ts b/packages/cli/src/commands/.ts deleted file mode 100644 index 165667d..0000000 --- a/packages/cli/src/commands/.ts +++ /dev/null @@ -1,187 +0,0 @@ -import * as fs from 'fs/promises'; -import { createInterface } from 'readline/promises'; - -import chalk from 'chalk'; -import { - toolAgent, - Logger, - getTools, - getAnthropicApiKeyError, - userPrompt, - LogLevel, - subAgentTool, - errorToString, -} from 'mycoder-agent'; -import { TokenTracker } from 'mycoder-agent/dist/core/tokens.js'; - -import { SharedOptions } from '../options.js'; -import { initSentry, captureException } from '../sentry/index.js'; -import { getConfig } from '../settings/config.js'; -import { hasUserConsented, saveUserConsent } from '../settings/settings.js'; -import { nameToLogIndex } from '../utils/nameToLogIndex.js'; -import { checkForUpdates, getPackageInfo } from '../utils/versionCheck.js'; - -import type { CommandModule, Argv } from 'yargs'; - -interface DefaultArgs extends SharedOptions { - prompt?: string; -} - -export const command: CommandModule = { - command: '* [prompt]', - describe: 'Execute a prompt or start interactive mode', - builder: (yargs: Argv): Argv => { - return yargs.positional('prompt', { - type: 'string', - description: 'The prompt to execute', - }) as Argv; - }, - handler: async (argv) => { - // Initialize Sentry with custom DSN if provided - if (argv.sentryDsn) { - initSentry(argv.sentryDsn); - } - - const logger = new Logger({ - name: 'Default', - logLevel: nameToLogIndex(argv.logLevel), - customPrefix: subAgentTool.logPrefix, - }); - - const packageInfo = getPackageInfo(); - - logger.info( - `MyCoder v${packageInfo.version} - AI-powered coding assistant`, - ); - - await checkForUpdates(logger); - - if (!hasUserConsented()) { - const readline = createInterface({ - input: process.stdin, - output: process.stdout, - }); - - logger.warn( - 'This tool can do anything on your command line that you ask it to.', - 'It can delete files, install software, and even send data to remote servers.', - 'It is a powerful tool that should be used with caution.', - 'Do you consent to using this tool at your own risk? (y/N)', - ); - - const answer = (await readline.question('> ')).trim().toLowerCase(); - readline.close(); - - if (answer === 'y' || answer === 'yes') { - saveUserConsent(); - } else { - logger.info('User did not consent. Exiting.'); - throw new Error('User did not consent'); - } - } - - const tokenTracker = new TokenTracker( - 'Root', - undefined, - argv.tokenUsage ? LogLevel.info : LogLevel.debug, - ); - - try { - // Early API key check - if (!process.env.ANTHROPIC_API_KEY) { - logger.error(getAnthropicApiKeyError()); - throw new Error('Anthropic API key not found'); - } - - let prompt: string | undefined; - - // If promptFile is specified, read from file - if (argv.file) { - prompt = await fs.readFile(argv.file, 'utf-8'); - } - - // If interactive mode - if (argv.interactive) { - prompt = await userPrompt( - "Type your request below or 'help' for usage information. Use Ctrl+C to exit.", - ); - } else if (!prompt) { - // Use command line prompt if provided - prompt = argv.prompt; - } - - if (!prompt) { - logger.error( - 'No prompt provided. Either specify a prompt, use --promptFile, or run in --interactive mode.', - ); - throw new Error('No prompt provided'); - } - - // Add the standard suffix to all prompts - prompt += [ - 'Please ask for clarifications if required or if the tasks is confusing.', - "If you need more context, don't be scared to create a sub-agent to investigate and generate report back, this can save a lot of time and prevent obvious mistakes.", - 'Once the task is complete ask the user, via the userPrompt tool if the results are acceptable or if changes are needed or if there are additional follow on tasks.', - ].join('\n'); - - const tools = getTools(); - - // Error handling - process.on('SIGINT', () => { - logger.log( - tokenTracker.logLevel, - chalk.blueBright(`[Token Usage Total] ${tokenTracker.toString()}`), - ); - process.exit(0); - }); - - // Get configuration - const config = getConfig(); - - // Create a custom config with GitHub mode - const customConfig = { - ...undefined, // Use default config - getSystemPrompt: () => { - // Import the function dynamically to avoid circular dependencies - const { - getDefaultSystemPrompt, - } = require('mycoder-agent/dist/core/toolAgent/config.js'); - return getDefaultSystemPrompt({ githubMode: config.githubMode }); - }, - }; - - // If GitHub mode is enabled, log it - if (config.githubMode) { - logger.info('GitHub mode is enabled'); - } - - const result = await toolAgent(prompt, tools, customConfig, { - logger, - headless: argv.headless ?? true, - userSession: argv.userSession ?? false, - pageFilter: argv.pageFilter ?? 'none', - workingDirectory: '.', - tokenTracker, - }); - - const output = - typeof result.result === 'string' - ? result.result - : JSON.stringify(result.result, null, 2); - logger.info('\n=== Result ===\n', output); - } catch (error) { - logger.error( - 'An error occurred:', - errorToString(error), - error instanceof Error ? error.stack : '', - ); - // Capture the error with Sentry - captureException(error); - } - - logger.log( - tokenTracker.logLevel, - chalk.blueBright(`[Token Usage Total] ${tokenTracker.toString()}`), - ); - }, -}; diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 187b1bf..7a61e28 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -6,6 +6,7 @@ import yargs, { CommandModule } from 'yargs'; import { hideBin } from 'yargs/helpers'; import { command as defaultCommand } from './commands/$default.js'; +import { command as configCommand } from './commands/config.js'; import { command as testSentryCommand } from './commands/test-sentry.js'; import { command as toolsCommand } from './commands/tools.js'; import { sharedOptions } from './options.js'; @@ -35,6 +36,7 @@ const main = async () => { defaultCommand, testSentryCommand, toolsCommand, + configCommand, ] as CommandModule[]) .strict() .showHelpOnFail(true) diff --git a/packages/cli/src/settings/config.ts b/packages/cli/src/settings/config.ts index e950125..de00d2f 100644 --- a/packages/cli/src/settings/config.ts +++ b/packages/cli/src/settings/config.ts @@ -9,6 +9,9 @@ const configFile = path.join(getSettingsDir(), 'config.json'); const defaultConfig = { // Add default configuration values here githubMode: false, + headless: true, + userSession: false, + pageFilter: 'none' as 'simple' | 'none' | 'readability', modelProvider: 'anthropic', modelName: 'claude-3-7-sonnet-20250219', }; diff --git a/packages/cli/tests/settings/config.test.ts b/packages/cli/tests/settings/config.test.ts index f079271..71ff3da 100644 --- a/packages/cli/tests/settings/config.test.ts +++ b/packages/cli/tests/settings/config.test.ts @@ -38,6 +38,9 @@ describe('Config', () => { expect(config).toEqual({ githubMode: false, + headless: true, + userSession: false, + pageFilter: 'none', modelProvider: 'anthropic', modelName: 'claude-3-7-sonnet-20250219', }); @@ -66,6 +69,9 @@ describe('Config', () => { expect(config).toEqual({ githubMode: false, + headless: true, + userSession: false, + pageFilter: 'none', modelProvider: 'anthropic', modelName: 'claude-3-7-sonnet-20250219', });