diff --git a/migration-plan.md b/migration-plan.md new file mode 100644 index 0000000..460edb5 --- /dev/null +++ b/migration-plan.md @@ -0,0 +1,314 @@ +# Migration Plan: Yargs to Citty + +## Overview + +This document outlines the plan to migrate the CLI package from yargs to citty. Citty is a modern, lightweight CLI builder that offers several advantages over yargs, including better TypeScript support, a more elegant API, and a more modular approach. + +## Current Implementation Analysis + +### CLI Structure + +The current CLI implementation uses yargs with the following components: + +- `index.ts`: Main entry point that sets up yargs and registers commands +- `commands/*.ts`: Individual command implementations +- `options.ts`: Shared options definition +- Custom command loading from config + +### Command Pattern + +Commands are implemented as yargs command modules with: + +- `command`: Command definition string +- `describe`: Command description +- `builder`: Function to define command-specific options +- `handler`: Function to execute the command + +### Shared Options + +Shared options are defined in `options.ts` and used across commands. + +## Citty Implementation Plan + +### 1. Dependencies Update + +- Add citty as a dependency +- Keep yargs temporarily during the migration + +### 2. Command Structure Refactoring + +- Create a new directory structure for citty commands +- Implement the main command definition using citty's `defineCommand` +- Implement subcommands using citty's nested command structure + +### 3. Migration Steps + +#### Step 1: Create Base Command Structure + +```typescript +// src/cli.ts +import { defineCommand, runMain } from 'citty'; +import { sharedArgs } from './args'; + +const main = defineCommand({ + meta: { + name: 'mycoder', + version: '1.3.1', + description: + 'A command line tool using agent that can do arbitrary tasks, including coding tasks', + }, + args: sharedArgs, + subCommands: { + // Will be populated with commands + }, +}); + +export default main; +``` + +#### Step 2: Convert Shared Options + +```typescript +// src/args.ts +import type { CommandArgs } from 'citty'; + +export const sharedArgs: CommandArgs = { + logLevel: { + type: 'string', + description: 'Set minimum logging level', + options: ['debug', 'verbose', 'info', 'warn', 'error'], + }, + profile: { + type: 'boolean', + description: 'Enable performance profiling of CLI startup', + }, + provider: { + type: 'string', + description: 'AI model provider to use', + options: ['anthropic', 'ollama', 'openai'], + }, + model: { + type: 'string', + description: 'AI model name to use', + }, + maxTokens: { + type: 'number', + description: 'Maximum number of tokens to generate', + }, + temperature: { + type: 'number', + description: 'Temperature for text generation (0.0-1.0)', + }, + interactive: { + type: 'boolean', + alias: 'i', + description: 'Run in interactive mode, asking for prompts', + default: false, + }, + file: { + type: 'string', + alias: 'f', + description: 'Read prompt from a file', + }, + tokenUsage: { + type: 'boolean', + description: 'Output token usage at info log level', + }, + headless: { + type: 'boolean', + description: 'Use browser in headless mode with no UI showing', + }, + userSession: { + type: 'boolean', + description: + "Use user's existing browser session instead of sandboxed session", + }, + pageFilter: { + type: 'string', + description: 'Method to process webpage content', + options: ['simple', 'none', 'readability'], + }, + tokenCache: { + type: 'boolean', + description: 'Enable token caching for LLM API calls', + }, + userPrompt: { + type: 'boolean', + description: 'Alias for userPrompt: enable or disable the userPrompt tool', + }, + githubMode: { + type: 'boolean', + description: + 'Enable GitHub mode for working with issues and PRs (requires git and gh CLI tools)', + default: true, + }, + upgradeCheck: { + type: 'boolean', + description: 'Disable version upgrade check (for automated/remote usage)', + }, + ollamaBaseUrl: { + type: 'string', + description: 'Base URL for Ollama API (default: http://localhost:11434)', + }, +}; +``` + +#### Step 3: Convert Default Command + +```typescript +// src/commands/default.ts +import { defineCommand } from 'citty'; +import { sharedArgs } from '../args'; +import { executePrompt } from '../utils/execute-prompt'; +import { loadConfig, getConfigFromArgv } from '../settings/config'; +import * as fs from 'fs/promises'; +import { userPrompt } from 'mycoder-agent'; + +export const defaultCommand = defineCommand({ + meta: { + name: 'default', + description: 'Execute a prompt or start interactive mode', + }, + args: { + ...sharedArgs, + prompt: { + type: 'positional', + description: 'The prompt to execute', + }, + }, + async run({ args }) { + // Get configuration for model provider and name + const config = await loadConfig(getConfigFromArgv(args)); + + let prompt: string | undefined; + + // If file is specified, read from file + if (args.file) { + prompt = await fs.readFile(args.file, 'utf-8'); + } + + // If interactive mode + if (args.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 = args.prompt; + } + + if (!prompt) { + throw new Error( + 'No prompt provided. Either specify a prompt, use --file, or run in --interactive mode.', + ); + } + + // Execute the prompt + await executePrompt(prompt, config); + }, +}); +``` + +#### Step 4: Convert Other Commands + +Convert each command in the `commands/` directory to use citty's `defineCommand` function. + +#### Step 5: Implement Custom Command Loading + +```typescript +// src/commands/custom.ts +import { defineCommand, CommandDef } from 'citty'; +import { loadConfig } from '../settings/config'; +import { executePrompt } from '../utils/execute-prompt'; + +export async function getCustomCommands(): Promise> { + const config = await loadConfig(); + + if (!config.commands) { + return {}; + } + + const commands: Record = {}; + + for (const [name, commandConfig] of Object.entries(config.commands)) { + const args: Record = {}; + + // Convert args to citty format + (commandConfig.args || []).forEach((arg) => { + args[arg.name] = { + type: 'string', + description: arg.description, + default: arg.default, + required: arg.required, + }; + }); + + commands[name] = defineCommand({ + meta: { + name, + description: commandConfig.description || `Custom command: ${name}`, + }, + args, + async run({ args }) { + // Load config + const config = await loadConfig(); + + // Execute the command + const prompt = await commandConfig.execute(args); + + // Execute the prompt using the default command handler + await executePrompt(prompt, config); + }, + }); + } + + return commands; +} +``` + +#### Step 6: Update Entry Point + +```typescript +// src/index.ts +import { runMain } from 'citty'; +import main from './cli'; + +// Start the CLI +runMain(main); +``` + +#### Step 7: Update bin/cli.js + +```javascript +#!/usr/bin/env node +import '../dist/index.js'; +``` + +### 4. Testing Strategy + +- Implement unit tests for each converted command +- Test command execution with various arguments +- Test help output and argument parsing +- Test custom command loading + +### 5. Incremental Migration Approach + +1. Implement citty commands alongside existing yargs commands +2. Add a feature flag to switch between implementations +3. Test thoroughly with both implementations +4. Switch to citty implementation by default +5. Remove yargs implementation when stable + +## Benefits of Migration + +- Better TypeScript support and type safety +- More modular and composable command structure +- Improved performance due to lighter dependencies +- Better maintainability with modern API design +- Enhanced developer experience + +## Potential Challenges + +- Ensuring backward compatibility for all command options +- Handling custom command loading logic +- Managing the transition period with both implementations diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 0307488..1dd90e4 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,10 +1,9 @@ # [mycoder-agent-v1.3.1](https://github.com/drivecore/mycoder/compare/mycoder-agent-v1.3.0...mycoder-agent-v1.3.1) (2025-03-13) - ### Bug Fixes -* redo ollama llm provider using ollama sdk ([586fe82](https://github.com/drivecore/mycoder/commit/586fe827d048aa6c13675ba838bd50309b3980e2)) -* update Ollama provider to use official npm package API correctly ([738a84a](https://github.com/drivecore/mycoder/commit/738a84aff560076e4ad24129f5dc9bf09d304ffa)) +- redo ollama llm provider using ollama sdk ([586fe82](https://github.com/drivecore/mycoder/commit/586fe827d048aa6c13675ba838bd50309b3980e2)) +- update Ollama provider to use official npm package API correctly ([738a84a](https://github.com/drivecore/mycoder/commit/738a84aff560076e4ad24129f5dc9bf09d304ffa)) # [mycoder-agent-v1.3.0](https://github.com/drivecore/mycoder/compare/mycoder-agent-v1.2.0...mycoder-agent-v1.3.0) (2025-03-12) diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index f23be76..59d220c 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,9 +1,8 @@ # [mycoder-v1.3.1](https://github.com/drivecore/mycoder/compare/mycoder-v1.3.0...mycoder-v1.3.1) (2025-03-13) - ### Bug Fixes -* redo ollama llm provider using ollama sdk ([586fe82](https://github.com/drivecore/mycoder/commit/586fe827d048aa6c13675ba838bd50309b3980e2)) +- redo ollama llm provider using ollama sdk ([586fe82](https://github.com/drivecore/mycoder/commit/586fe827d048aa6c13675ba838bd50309b3980e2)) # [mycoder-v1.3.0](https://github.com/drivecore/mycoder/compare/mycoder-v1.2.0...mycoder-v1.3.0) (2025-03-12) diff --git a/packages/cli/package.json b/packages/cli/package.json index ca46f03..52802e3 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -20,6 +20,7 @@ }, "scripts": { "start": "node --no-deprecation bin/cli.js", + "start:citty": "USE_CITTY=true node --no-deprecation bin/cli.js", "typecheck": "tsc --noEmit", "build": "tsc", "clean": "rimraf dist", @@ -47,6 +48,7 @@ "dependencies": { "@sentry/node": "^9.3.0", "chalk": "^5", + "citty": "^0.1.6", "cosmiconfig": "^9.0.0", "deepmerge": "^4.3.1", "dotenv": "^16", diff --git a/packages/cli/src/citty/args.ts b/packages/cli/src/citty/args.ts new file mode 100644 index 0000000..343b7e0 --- /dev/null +++ b/packages/cli/src/citty/args.ts @@ -0,0 +1,108 @@ +import type { ArgumentsType } from 'citty'; + +export const sharedArgs: ArgumentsType = { + logLevel: { + type: 'string', + description: 'Set minimum logging level', + options: ['debug', 'verbose', 'info', 'warn', 'error'], + }, + profile: { + type: 'boolean', + description: 'Enable performance profiling of CLI startup', + }, + provider: { + type: 'string', + description: 'AI model provider to use', + options: ['anthropic', 'ollama', 'openai'], + }, + model: { + type: 'string', + description: 'AI model name to use', + }, + maxTokens: { + type: 'number', + description: 'Maximum number of tokens to generate', + }, + temperature: { + type: 'number', + description: 'Temperature for text generation (0.0-1.0)', + }, + interactive: { + type: 'boolean', + alias: 'i', + description: 'Run in interactive mode, asking for prompts', + default: false, + }, + file: { + type: 'string', + alias: 'f', + description: 'Read prompt from a file', + }, + tokenUsage: { + type: 'boolean', + description: 'Output token usage at info log level', + }, + headless: { + type: 'boolean', + description: 'Use browser in headless mode with no UI showing', + }, + userSession: { + type: 'boolean', + description: + "Use user's existing browser session instead of sandboxed session", + }, + pageFilter: { + type: 'string', + description: 'Method to process webpage content', + options: ['simple', 'none', 'readability'], + }, + tokenCache: { + type: 'boolean', + description: 'Enable token caching for LLM API calls', + }, + userPrompt: { + type: 'boolean', + description: 'Alias for userPrompt: enable or disable the userPrompt tool', + }, + githubMode: { + type: 'boolean', + description: + 'Enable GitHub mode for working with issues and PRs (requires git and gh CLI tools)', + default: true, + }, + upgradeCheck: { + type: 'boolean', + description: 'Disable version upgrade check (for automated/remote usage)', + }, + ollamaBaseUrl: { + type: 'string', + description: 'Base URL for Ollama API (default: http://localhost:11434)', + }, +}; + +// Type definition for CLI args +export interface SharedArgs { + logLevel?: string; + interactive?: boolean; + file?: string; + tokenUsage?: boolean; + headless?: boolean; + userSession?: boolean; + pageFilter?: 'simple' | 'none' | 'readability'; + sentryDsn?: string; + provider?: string; + model?: string; + maxTokens?: number; + temperature?: number; + profile?: boolean; + tokenCache?: boolean; + userPrompt?: boolean; + githubMode?: boolean; + upgradeCheck?: boolean; + ollamaBaseUrl?: string; +} + +// Type for default command with prompt +export interface DefaultArgs extends SharedArgs { + prompt?: string; +} diff --git a/packages/cli/src/citty/cli.ts b/packages/cli/src/citty/cli.ts new file mode 100644 index 0000000..36b3219 --- /dev/null +++ b/packages/cli/src/citty/cli.ts @@ -0,0 +1,43 @@ +import { createRequire } from 'module'; + +import { defineCommand } from 'citty'; + +import { sharedArgs } from './args.js'; +import { getCustomCommands } from './commands/custom.js'; +import { defaultCommand } from './commands/default.js'; +import { testProfileCommand } from './commands/test-profile.js'; +import { testSentryCommand } from './commands/test-sentry.js'; +import { toolsCommand } from './commands/tools.js'; + +import type { PackageJson } from 'type-fest'; + +const require = createRequire(import.meta.url); +const packageInfo = require('../../package.json') as PackageJson; + +/** + * Create the main CLI command with all subcommands + */ +export async function createMainCommand() { + // Load custom commands from config + const customCommands = await getCustomCommands(); + + // Create the main command + const main = defineCommand({ + meta: { + name: packageInfo.name!, + version: packageInfo.version!, + description: packageInfo.description!, + }, + args: sharedArgs, + subCommands: { + 'test-sentry': testSentryCommand, + 'test-profile': testProfileCommand, + tools: toolsCommand, + ...customCommands, + }, + // Default command implementation + run: defaultCommand.run, + }); + + return main; +} diff --git a/packages/cli/src/citty/commands/custom.ts b/packages/cli/src/citty/commands/custom.ts new file mode 100644 index 0000000..8e47e92 --- /dev/null +++ b/packages/cli/src/citty/commands/custom.ts @@ -0,0 +1,63 @@ +import { defineCommand, CommandDef } from 'citty'; +import { loadConfig } from '../../settings/config.js'; +import { executePrompt } from '../utils/execute-prompt.js'; +import { sharedArgs } from '../args.js'; + +/** + * Gets custom commands defined in the config file + * @returns Record of command name to command definition + */ +export async function getCustomCommands(): Promise> { + const config = await loadConfig(); + + if (!config.commands) { + return {}; + } + + const commands: Record = {}; + + for (const [name, commandConfig] of Object.entries(config.commands)) { + const commandArgs: Record = { + ...sharedArgs, + }; + + // Convert args to citty format + (commandConfig.args || []).forEach((arg) => { + commandArgs[arg.name] = { + type: 'string', + description: arg.description, + default: arg.default, + required: arg.required, + }; + }); + + commands[name] = defineCommand({ + meta: { + name, + description: commandConfig.description || `Custom command: ${name}`, + }, + args: commandArgs, + async run({ args }) { + // Load config + const config = await loadConfig(); + + // Extract args from command line + const commandArgs = (commandConfig.args || []).reduce( + (acc, arg) => { + acc[arg.name] = args[arg.name] as string || ''; + return acc; + }, + {} as Record, + ); + + // Execute the command + const prompt = await commandConfig.execute(commandArgs); + + // Execute the prompt using the default command handler + await executePrompt(prompt, config); + }, + }); + } + + return commands; +} \ No newline at end of file diff --git a/packages/cli/src/citty/commands/default.ts b/packages/cli/src/citty/commands/default.ts new file mode 100644 index 0000000..222234a --- /dev/null +++ b/packages/cli/src/citty/commands/default.ts @@ -0,0 +1,64 @@ +import * as fs from 'fs/promises'; + +import { defineCommand } from 'citty'; +import { userPrompt, Logger, subAgentTool } from 'mycoder-agent'; + +import { getConfigFromArgv, loadConfig } from '../../settings/config.js'; +import { nameToLogIndex } from '../../utils/nameToLogIndex.js'; +import { sharedArgs, DefaultArgs } from '../args.js'; +import { executePrompt } from '../utils/execute-prompt.js'; + +export const defaultCommand = defineCommand({ + meta: { + name: 'default', + description: 'Execute a prompt or start interactive mode', + }, + args: { + ...sharedArgs, + prompt: { + type: 'positional', + description: 'The prompt to execute', + }, + }, + async run({ args }) { + // Get configuration for model provider and name + const typedArgs = args as unknown as DefaultArgs; + const config = await loadConfig(getConfigFromArgv({ + ...typedArgs, + logLevel: typedArgs.logLevel || 'info' + })); + + let prompt: string | undefined; + + // If file is specified, read from file + if (typedArgs.file) { + prompt = await fs.readFile(typedArgs.file, 'utf-8'); + } + + // If interactive mode + if (typedArgs.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 = typedArgs.prompt || ''; + } + + if (!prompt) { + const logger = new Logger({ + name: 'Default', + logLevel: nameToLogIndex(config.logLevel), + customPrefix: subAgentTool.logPrefix, + }); + + logger.error( + 'No prompt provided. Either specify a prompt, use --file, or run in --interactive mode.', + ); + throw new Error('No prompt provided'); + } + + // Execute the prompt + await executePrompt(prompt, config); + }, +}); \ No newline at end of file diff --git a/packages/cli/src/citty/commands/test-profile.ts b/packages/cli/src/citty/commands/test-profile.ts new file mode 100644 index 0000000..72edd2f --- /dev/null +++ b/packages/cli/src/citty/commands/test-profile.ts @@ -0,0 +1,42 @@ +import { defineCommand } from 'citty'; + +import { + enableProfiling, + mark, + reportTimings, +} from '../../utils/performance.js'; +import { sharedArgs } from '../args.js'; + +export const testProfileCommand = defineCommand({ + meta: { + name: 'test-profile', + description: 'Test performance profiling', + }, + args: { + ...sharedArgs, + }, + async run() { + console.log('Testing performance profiling...'); + + // Enable profiling + enableProfiling(true); + + // Create some test marks + mark('Start test'); + + // Simulate some work + await new Promise((resolve) => setTimeout(resolve, 100)); + mark('After 100ms'); + + await new Promise((resolve) => setTimeout(resolve, 200)); + mark('After 300ms'); + + await new Promise((resolve) => setTimeout(resolve, 300)); + mark('After 600ms'); + + // Report timings + await reportTimings(); + + console.log('Performance profiling test complete.'); + }, +}); diff --git a/packages/cli/src/citty/commands/test-sentry.ts b/packages/cli/src/citty/commands/test-sentry.ts new file mode 100644 index 0000000..f77b876 --- /dev/null +++ b/packages/cli/src/citty/commands/test-sentry.ts @@ -0,0 +1,30 @@ +import { defineCommand } from 'citty'; + +import { captureException, initSentry } from '../../sentry/index.js'; +import { sharedArgs } from '../args.js'; + +export const testSentryCommand = defineCommand({ + meta: { + name: 'test-sentry', + description: 'Test Sentry error reporting', + }, + args: { + ...sharedArgs, + }, + run() { + console.log('Testing Sentry error reporting...'); + + // Initialize Sentry if not already initialized + initSentry(); + + // Create a test error + const testError = new Error('This is a test error from the CLI'); + + // Capture the error with Sentry + captureException(testError); + + console.log( + 'Test error sent to Sentry. Please check your Sentry dashboard.', + ); + }, +}); diff --git a/packages/cli/src/citty/commands/tools.ts b/packages/cli/src/citty/commands/tools.ts new file mode 100644 index 0000000..3a9ed7d --- /dev/null +++ b/packages/cli/src/citty/commands/tools.ts @@ -0,0 +1,96 @@ +import { defineCommand } from 'citty'; +import { getTools } from 'mycoder-agent'; + +import { sharedArgs } from '../args.js'; + +import type { JsonSchema7Type } from 'zod-to-json-schema'; + +function formatSchema(schema: { + properties?: Record; + required?: string[]; +}) { + let output = ''; + + if (schema.properties) { + for (const [paramName, param] of Object.entries(schema.properties)) { + const required = schema.required?.includes(paramName) + ? '' + : ' (optional)'; + const description = (param as any).description || ''; + output += `${paramName}${required}: ${description}\n`; + + if ((param as any).type) { + output += ` Type: ${(param as any).type}\n`; + } + if ((param as any).maxLength) { + output += ` Max Length: ${(param as any).maxLength}\n`; + } + if ((param as any).additionalProperties) { + output += ` Additional Properties: ${JSON.stringify((param as any).additionalProperties)}\n`; + } + } + } + + return output; +} + +export const toolsCommand = defineCommand({ + meta: { + name: 'tools', + description: 'List all available tools and their capabilities', + }, + args: { + ...sharedArgs, + }, + run() { + try { + const tools = getTools(); + + console.log('Available Tools:\n'); + + for (const tool of tools) { + // Tool name and description + console.log(`${tool.name}`); + console.log('-'.repeat(tool.name.length)); + console.log(`Description: ${tool.description}\n`); + + // Parameters section + console.log('Parameters:'); + // Use parametersJsonSchema if available, otherwise convert from ZodSchema + const parametersSchema = + (tool as any).parametersJsonSchema || tool.parameters; + console.log( + formatSchema( + parametersSchema as { + properties?: Record; + required?: string[]; + }, + ), + ); + + // Returns section + console.log('Returns:'); + if (tool.returns) { + // Use returnsJsonSchema if available, otherwise convert from ZodSchema + const returnsSchema = (tool as any).returnsJsonSchema || tool.returns; + console.log( + formatSchema( + returnsSchema as { + properties?: Record; + required?: string[]; + }, + ), + ); + } else { + console.log(' Type: any'); + console.log(' Description: Tool execution result or error\n'); + } + + console.log(); // Add spacing between tools + } + } catch (error) { + console.error('Error listing tools:', error); + process.exit(1); + } + }, +}); diff --git a/packages/cli/src/citty/index.ts b/packages/cli/src/citty/index.ts new file mode 100644 index 0000000..ec5e913 --- /dev/null +++ b/packages/cli/src/citty/index.ts @@ -0,0 +1,61 @@ +import { runMain } from 'citty'; +import * as dotenv from 'dotenv'; +import sourceMapSupport from 'source-map-support'; + +import { initSentry, captureException } from '../sentry/index.js'; +import { cleanupResources, setupForceExit } from '../utils/cleanup.js'; +import { mark, reportTimings } from '../utils/performance.js'; + +import { createMainCommand } from './cli.js'; + +// Install source map support for better error stack traces +sourceMapSupport.install(); + +/** + * Main entry point for the CLI when using citty + */ +export async function main() { + mark('Main function start'); + + // Load environment variables from .env file + dotenv.config(); + mark('After dotenv config'); + + // Only initialize Sentry if needed + if ( + process.env.NODE_ENV !== 'development' || + process.env.ENABLE_SENTRY === 'true' + ) { + initSentry(); + mark('After Sentry init'); + } + + // Create the main command with all subcommands + const mainCommand = await createMainCommand(); + mark('After command setup'); + + // Run the main command + await runMain(mainCommand); +} + +// Run the main function with error handling +export async function runCittyMain() { + await main() + .catch(async (error) => { + console.error(error); + // Capture the error with Sentry + captureException(error); + process.exit(1); + }) + .finally(async () => { + // Report timings if profiling is enabled + await reportTimings(); + + // Clean up all resources before exit + await cleanupResources(); + + // Setup a force exit as a failsafe + // This ensures the process will exit even if there are lingering handles + setupForceExit(5000); + }); +} diff --git a/packages/cli/src/citty/utils/execute-prompt.ts b/packages/cli/src/citty/utils/execute-prompt.ts new file mode 100644 index 0000000..e8acf1f --- /dev/null +++ b/packages/cli/src/citty/utils/execute-prompt.ts @@ -0,0 +1,195 @@ +import chalk from 'chalk'; +import { + toolAgent, + Logger, + getTools, + getProviderApiKeyError, + providerConfig, + LogLevel, + subAgentTool, + errorToString, + DEFAULT_CONFIG, + AgentConfig, + ModelProvider, + BackgroundTools, +} from 'mycoder-agent'; +import { TokenTracker } from 'mycoder-agent/dist/core/tokens.js'; + +import { captureException } from '../../sentry/index.js'; +import { Config } from '../../settings/config.js'; +import { checkGitCli } from '../../utils/gitCliCheck.js'; +import { nameToLogIndex } from '../../utils/nameToLogIndex.js'; +import { checkForUpdates, getPackageInfo } from '../../utils/versionCheck.js'; + +/** + * Executes a prompt with the given configuration + * This function is exported to be reused by custom commands + */ +export async function executePrompt( + prompt: string, + config: Config, +): Promise { + const packageInfo = getPackageInfo(); + + const logger = new Logger({ + name: 'Default', + logLevel: nameToLogIndex(config.logLevel), + customPrefix: subAgentTool.logPrefix, + }); + + logger.info(`MyCoder v${packageInfo.version} - AI-powered coding assistant`); + + // Skip version check if upgradeCheck is false + if (config.upgradeCheck !== false) { + await checkForUpdates(logger); + } + + // Check for git and gh CLI tools if GitHub mode is enabled + if (config.githubMode) { + logger.debug( + 'GitHub mode is enabled, checking for git and gh CLI tools...', + ); + const gitCliCheck = await checkGitCli(logger); + + if (gitCliCheck.errors.length > 0) { + logger.warn( + 'GitHub mode is enabled but there are issues with git/gh CLI tools:', + ); + gitCliCheck.errors.forEach((error) => logger.warn(`- ${error}`)); + + if (!gitCliCheck.gitAvailable || !gitCliCheck.ghAvailable) { + logger.warn( + 'GitHub mode requires git and gh CLI tools to be installed.', + ); + logger.warn( + 'Please install the missing tools or disable GitHub mode with --githubMode false', + ); + // Disable GitHub mode if git or gh CLI is not available + logger.info('Disabling GitHub mode due to missing CLI tools.'); + config.githubMode = false; + } else if (!gitCliCheck.ghAuthenticated) { + logger.warn( + 'GitHub CLI is not authenticated. Please run "gh auth login" to authenticate.', + ); + // Disable GitHub mode if gh CLI is not authenticated + logger.info('Disabling GitHub mode due to unauthenticated GitHub CLI.'); + config.githubMode = false; + } + } else { + logger.info( + 'GitHub mode is enabled and all required CLI tools are available.', + ); + } + } + + const tokenTracker = new TokenTracker( + 'Root', + undefined, + config.tokenUsage ? LogLevel.info : LogLevel.debug, + ); + // Use command line option if provided, otherwise use config value + tokenTracker.tokenCache = config.tokenCache; + + const backgroundTools = new BackgroundTools('mainAgent'); + + try { + // Early API key check based on model provider + const providerSettings = + providerConfig[config.provider as keyof typeof providerConfig]; + + if (providerSettings) { + const { keyName } = providerSettings; + + // First check if the API key is in the config + const configApiKey = config[keyName as keyof typeof config] as string; + // Then fall back to environment variable + const envApiKey = process.env[keyName]; + // Use config key if available, otherwise use env key + const apiKey = configApiKey || envApiKey; + + if (!apiKey) { + logger.error(getProviderApiKeyError(config.provider)); + throw new Error(`${config.provider} API key not found`); + } + + // If we're using a key from config, set it as an environment variable + // This ensures it's available to the provider libraries + if (configApiKey && !envApiKey) { + process.env[keyName] = configApiKey; + logger.info(`Using ${keyName} from configuration`); + } + } else if (config.provider === 'ollama') { + // For Ollama, we check if the base URL is set + logger.info(`Using Ollama with base URL: ${config.ollamaBaseUrl}`); + } else { + // Unknown provider + logger.info(`Unknown provider: ${config.provider}`); + throw new Error(`Unknown provider: ${config.provider}`); + } + + // 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({ + userPrompt: config.userPrompt, + mcpConfig: config.mcp, + }); + + // Error handling + process.on('SIGINT', () => { + logger.log( + tokenTracker.logLevel, + chalk.blueBright(`[Token Usage Total] ${tokenTracker.toString()}`), + ); + process.exit(0); + }); + + // Create a config for the agent + const agentConfig: AgentConfig = { + ...DEFAULT_CONFIG, + }; + + const result = await toolAgent(prompt, tools, agentConfig, { + logger, + headless: config.headless, + userSession: config.userSession, + pageFilter: config.pageFilter, + workingDirectory: '.', + tokenTracker, + githubMode: config.githubMode, + customPrompt: config.customPrompt, + tokenCache: config.tokenCache, + userPrompt: config.userPrompt, + provider: config.provider as ModelProvider, + model: config.model, + maxTokens: config.maxTokens, + temperature: config.temperature, + backgroundTools, + }); + + 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); + } finally { + await backgroundTools.cleanup(); + } + + 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 14b8952..51c1d8e 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,99 +1,28 @@ -import { createRequire } from 'module'; - -import * as dotenv from 'dotenv'; -import sourceMapSupport from 'source-map-support'; -import yargs, { ArgumentsCamelCase, CommandModule } from 'yargs'; -import { hideBin } from 'yargs/helpers'; - -import { command as defaultCommand } from './commands/$default.js'; -import { getCustomCommands } from './commands/custom.js'; -import { command as testProfileCommand } from './commands/test-profile.js'; -import { command as testSentryCommand } from './commands/test-sentry.js'; -import { command as toolsCommand } from './commands/tools.js'; -import { SharedOptions, sharedOptions } from './options.js'; -import { initSentry, captureException } from './sentry/index.js'; -import { getConfigFromArgv, loadConfig } from './settings/config.js'; -import { cleanupResources, setupForceExit } from './utils/cleanup.js'; -import { enableProfiling, mark, reportTimings } from './utils/performance.js'; - -mark('After imports'); - -import type { PackageJson } from 'type-fest'; - -// Add global declaration for our patched toolAgent - -mark('Before sourceMapSupport install'); -sourceMapSupport.install(); -mark('After sourceMapSupport install'); - -const main = async () => { - mark('Main function start'); - - dotenv.config(); - mark('After dotenv config'); - - // Only initialize Sentry if needed - if ( - process.env.NODE_ENV !== 'development' || - process.env.ENABLE_SENTRY === 'true' - ) { - initSentry(); - mark('After Sentry init'); - } - - mark('Before package.json load'); - const require = createRequire(import.meta.url); - const packageInfo = require('../package.json') as PackageJson; - mark('After package.json load'); - - // Set up yargs with the new CLI interface - mark('Before yargs setup'); - - // Load custom commands from config - const customCommands = await getCustomCommands(); - - const argv = await yargs(hideBin(process.argv)) - .scriptName(packageInfo.name!) - .version(packageInfo.version!) - .options(sharedOptions) - .alias('h', 'help') - .alias('V', 'version') - .command([ - defaultCommand, - testSentryCommand, - testProfileCommand, - toolsCommand, - ...customCommands, // Add custom commands - ] as CommandModule[]) - .strict() - .showHelpOnFail(true) - .help().argv; - - // Get config to check for profile setting - const config = await loadConfig( - getConfigFromArgv(argv as ArgumentsCamelCase), - ); - - // Enable profiling if --profile flag is set or if enabled in config - enableProfiling(config.profile); - mark('After yargs setup'); -}; - -await main() - .catch(async (error) => { - console.error(error); - // Capture the error with Sentry - captureException(error); +import { mark } from './utils/performance.js'; +mark('Before imports'); + +// Check if we should use citty or yargs +const useCitty = process.env.USE_CITTY === 'true'; + +// Function to handle async imports and run the appropriate implementation +async function run() { + try { + if (useCitty) { + // Use citty implementation + const { runCittyMain } = await import('./citty/index.js'); + await runCittyMain(); + } else { + // Use original yargs implementation + const { runYargsMain } = await import('./yargs-main.js'); + await runYargsMain(); + } + } catch (error) { + console.error('Failed to run CLI:', error); process.exit(1); - }) - .finally(async () => { - // Report timings if profiling is enabled - await reportTimings(); + } +} - // Clean up all resources before exit - await cleanupResources(); +// Run the CLI +run(); - // Setup a force exit as a failsafe - // This ensures the process will exit even if there are lingering handles - setupForceExit(5000); - }); +mark('After imports'); diff --git a/packages/cli/src/yargs-main.ts b/packages/cli/src/yargs-main.ts new file mode 100644 index 0000000..f9a1375 --- /dev/null +++ b/packages/cli/src/yargs-main.ts @@ -0,0 +1,99 @@ +import { createRequire } from 'module'; + +import * as dotenv from 'dotenv'; +import sourceMapSupport from 'source-map-support'; +import yargs, { ArgumentsCamelCase, CommandModule } from 'yargs'; +import { hideBin } from 'yargs/helpers'; + +import { command as defaultCommand } from './commands/$default.js'; +import { getCustomCommands } from './commands/custom.js'; +import { command as testProfileCommand } from './commands/test-profile.js'; +import { command as testSentryCommand } from './commands/test-sentry.js'; +import { command as toolsCommand } from './commands/tools.js'; +import { SharedOptions, sharedOptions } from './options.js'; +import { initSentry, captureException } from './sentry/index.js'; +import { getConfigFromArgv, loadConfig } from './settings/config.js'; +import { cleanupResources, setupForceExit } from './utils/cleanup.js'; +import { enableProfiling, mark, reportTimings } from './utils/performance.js'; + +import type { PackageJson } from 'type-fest'; + +// Add global declaration for our patched toolAgent + +mark('Before sourceMapSupport install'); +sourceMapSupport.install(); +mark('After sourceMapSupport install'); + +export const main = async () => { + mark('Main function start'); + + dotenv.config(); + mark('After dotenv config'); + + // Only initialize Sentry if needed + if ( + process.env.NODE_ENV !== 'development' || + process.env.ENABLE_SENTRY === 'true' + ) { + initSentry(); + mark('After Sentry init'); + } + + mark('Before package.json load'); + const require = createRequire(import.meta.url); + const packageInfo = require('../package.json') as PackageJson; + mark('After package.json load'); + + // Set up yargs with the new CLI interface + mark('Before yargs setup'); + + // Load custom commands from config + const customCommands = await getCustomCommands(); + + const argv = await yargs(hideBin(process.argv)) + .scriptName(packageInfo.name!) + .version(packageInfo.version!) + .options(sharedOptions) + .alias('h', 'help') + .alias('V', 'version') + .command([ + defaultCommand, + testSentryCommand, + testProfileCommand, + toolsCommand, + ...customCommands, // Add custom commands + ] as CommandModule[]) + .strict() + .showHelpOnFail(true) + .help().argv; + + // Get config to check for profile setting + const config = await loadConfig( + getConfigFromArgv(argv as ArgumentsCamelCase), + ); + + // Enable profiling if --profile flag is set or if enabled in config + enableProfiling(config.profile); + mark('After yargs setup'); +}; + +export async function runYargsMain() { + await main() + .catch(async (error) => { + console.error(error); + // Capture the error with Sentry + captureException(error); + process.exit(1); + }) + .finally(async () => { + // Report timings if profiling is enabled + await reportTimings(); + + // Clean up all resources before exit + await cleanupResources(); + + // Setup a force exit as a failsafe + // This ensures the process will exit even if there are lingering handles + setupForceExit(5000); + }); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94fa961..59b97b5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -157,6 +157,9 @@ importers: chalk: specifier: ^5 version: 5.4.1 + citty: + specifier: ^0.1.6 + version: 0.1.6 cosmiconfig: specifier: ^9.0.0 version: 9.0.0(typescript@5.8.2) @@ -3119,6 +3122,9 @@ packages: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + cjs-module-lexer@1.4.3: resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} @@ -12402,6 +12408,10 @@ snapshots: ci-info@3.9.0: {} + citty@0.1.6: + dependencies: + consola: 3.4.0 + cjs-module-lexer@1.4.3: {} clean-css@5.3.3: