Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions packages/cli/src/commands/$default.refactored.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import * as fs from 'fs/promises';

import { userPrompt } from 'mycoder-agent';

import { getContainer } from '../di/container.js';
import { SharedOptions } from '../options.js';
import { CommandService } from '../services/command.service.js';
import { LoggerService } from '../services/logger.service.js';
import { ServiceFactory } from '../services/service.factory.js';

import type { CommandModule, Argv } from 'yargs';

interface DefaultArgs extends SharedOptions {
prompt?: string;
}

/**
* Executes a prompt with the given configuration
* This function is exported to be reused by custom commands
*/
export async function executePrompt(
prompt: string,
_argv: DefaultArgs, // Prefix with underscore to indicate it's intentionally unused
): Promise<void> {
const container = getContainer();

// Get the command service from the container
const commandService = container.get<CommandService>('commandService');

// Execute the command
await commandService.execute(prompt);
}

export const command: CommandModule<SharedOptions, DefaultArgs> = {
command: '* [prompt]',
describe: 'Execute a prompt or start interactive mode',
builder: (yargs: Argv<object>): Argv<DefaultArgs> => {
return yargs.positional('prompt', {
type: 'string',
description: 'The prompt to execute',
}) as Argv<DefaultArgs>;
},
handler: async (argv) => {
// Initialize services
const serviceFactory = new ServiceFactory();
await serviceFactory.initializeServices(argv);

const container = getContainer();
const loggerService = container.get<LoggerService>('loggerService');
const logger = loggerService.getDefaultLogger();

// Determine the prompt source
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');
}

// Execute the prompt
await executePrompt(prompt, argv);
},
};
79 changes: 79 additions & 0 deletions packages/cli/src/di/container.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* Simple dependency injection container
* Manages service instances and dependencies
*/
export class Container {
private services: Map<string, unknown> = new Map();

/**
* Register a service instance with the container
* @param name Service name/key
* @param instance Service instance
*/
register<T>(name: string, instance: T): void {
this.services.set(name, instance);
}

/**
* Get a service instance by name
* @param name Service name/key
* @returns Service instance
* @throws Error if service is not registered
*/
get<T>(name: string): T {
if (!this.services.has(name)) {
throw new Error(`Service '${name}' not registered in container`);
}
return this.services.get(name) as T;
}

/**
* Check if a service is registered
* @param name Service name/key
* @returns True if service is registered
*/
has(name: string): boolean {
return this.services.has(name);
}

/**
* Remove a service from the container
* @param name Service name/key
* @returns True if service was removed
*/
remove(name: string): boolean {
return this.services.delete(name);
}

/**
* Clear all registered services
*/
clear(): void {
this.services.clear();
}
}

// Global container instance
let globalContainer: Container | null = null;

/**
* Get the global container instance
* Creates a new container if none exists
*/
export function getContainer(): Container {
if (!globalContainer) {
globalContainer = new Container();
}
return globalContainer;
}

/**
* Reset the global container
* Useful for testing
*/
export function resetContainer(): void {
if (globalContainer) {
globalContainer.clear();
}
globalContainer = null;
}
194 changes: 194 additions & 0 deletions packages/cli/src/services/command.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import chalk from 'chalk';
import {
toolAgent,
Logger,
getTools,
getProviderApiKeyError,
providerConfig,
LogLevel,
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 { GitHubService } from './github.service.js';
import { VersionService } from './version.service.js';

import type { Config } from '../settings/config.js';

/**
* Service for executing commands
* Handles the core logic of executing prompts with the AI agent
*/
export class CommandService {
private logger: Logger;
private config: Config;
private versionService: VersionService;
private githubService: GitHubService;

/**
* Create a new CommandService
* @param logger Logger instance
* @param config Application configuration
* @param versionService Version service for update checks
* @param githubService GitHub service for GitHub mode validation
*/
constructor(
logger: Logger,
config: Config,
versionService: VersionService,
githubService: GitHubService,
) {
this.logger = logger;
this.config = config;
this.versionService = versionService;
this.githubService = githubService;
}

/**
* Execute a prompt with the AI agent
* @param prompt The prompt to execute
* @returns Promise that resolves when execution is complete
*/
async execute(prompt: string): Promise<void> {
const packageInfo = this.versionService.getPackageInfo();

this.logger.info(
`MyCoder v${packageInfo.version} - AI-powered coding assistant`,
);

// Check for updates if enabled
await this.versionService.checkForUpdates();

// Validate GitHub mode if enabled
if (this.config.githubMode) {
this.config.githubMode = await this.githubService.validateGitHubMode();
}

const tokenTracker = new TokenTracker(
'Root',
undefined,
this.config.tokenUsage ? LogLevel.info : LogLevel.debug,
);
// Use command line option if provided, otherwise use config value
tokenTracker.tokenCache = this.config.tokenCache;

const backgroundTools = new BackgroundTools('mainAgent');

try {
await this.executeWithAgent(prompt, tokenTracker, backgroundTools);
} catch (error) {
this.logger.error(
'An error occurred:',
errorToString(error),
error instanceof Error ? error.stack : '',
);
// Capture the error with Sentry
captureException(error);
} finally {
await backgroundTools.cleanup();
}

this.logger.log(
tokenTracker.logLevel,
chalk.blueBright(`[Token Usage Total] ${tokenTracker.toString()}`),
);
}

/**
* Execute the prompt with the AI agent
* @param prompt The prompt to execute
* @param tokenTracker Token usage tracker
* @param backgroundTools Background tools manager
*/
private async executeWithAgent(
prompt: string,
tokenTracker: TokenTracker,
backgroundTools: BackgroundTools,
): Promise<void> {
// Early API key check based on model provider
const providerSettings =
providerConfig[this.config.provider as keyof typeof providerConfig];

if (!providerSettings) {
// Unknown provider
this.logger.info(`Unknown provider: ${this.config.provider}`);
throw new Error(`Unknown provider: ${this.config.provider}`);
}

const { keyName } = providerSettings;
let apiKey: string | undefined = undefined;
if (keyName) {
// Then fall back to environment variable
apiKey = process.env[keyName];
if (!apiKey) {
this.logger.error(getProviderApiKeyError(this.config.provider));
throw new Error(`${this.config.provider} API key not found`);
}
}

this.logger.info(`LLM: ${this.config.provider}/${this.config.model}`);
if (this.config.baseUrl) {
// For Ollama, we check if the base URL is set
this.logger.info(`Using base url: ${this.config.baseUrl}`);
}
console.log();

// 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: this.config.userPrompt,
mcpConfig: this.config.mcp,
});

// Error handling
process.on('SIGINT', () => {
this.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: this.logger,
headless: this.config.headless,
userSession: this.config.userSession,
pageFilter: this.config.pageFilter,
workingDirectory: '.',
tokenTracker,
githubMode: this.config.githubMode,
customPrompt: this.config.customPrompt,
tokenCache: this.config.tokenCache,
userPrompt: this.config.userPrompt,
provider: this.config.provider as ModelProvider,
baseUrl: this.config.baseUrl,
model: this.config.model,
maxTokens: this.config.maxTokens,
temperature: this.config.temperature,
backgroundTools,
apiKey,
});

const output =
typeof result.result === 'string'
? result.result
: JSON.stringify(result.result, null, 2);
this.logger.info('\\n=== Result ===\\n', output);
}
}
Loading
Loading