commit 14b8886eee7a2541bde2b5310898bd3a709bb420 Author: Geoffrey Huntley Date: Sat Mar 1 19:41:15 2025 +1100 initial commmit diff --git a/README.md b/README.md new file mode 100644 index 0000000..5c1279b --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# Claude Code (Research Preview) Cleanroom Transpilation + +**DISCLAIMER:** + +This is a cleanroom transpilation of the official Claude Code npm package. + +Learn more at https://ghuntley.com/tradecraft + +Claude Code is a product by Anthropic. I am not anthropic. Do not pester Anthropic about this source-code. Use it as a reference to learn about the LLM clean-room technique detailed above. + +## About Claude Code + +Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands. + +Some of its key capabilities include: + +- Edit files and fix bugs across your codebase +- Answer questions about your code's architecture and logic +- Execute and fix tests, lint, and other commands +- Search through git history, resolve merge conflicts, and create commits and PRs + +**Learn more in the [official documentation](https://docs.anthropic.com/en/docs/agents/claude-code/introduction)**. + +## Get started + +
    +
  1. + Run the following command in your terminal:
    + npm install -g @anthropic-ai/claude-code +
  2. +
  3. + Navigate to your project directory and run claude +
  4. +
  5. + Complete the one-time OAuth process with your Anthropic Console account. +
  6. +
+ +### Research Preview + +We're launching Claude Code as a beta product in research preview to learn directly from developers about their experiences collaborating with AI agents. Our aim is to learn more about how developers prefer to collaborate with AI tools, which development workflows benefit most from working with the agent, and how we can make the agent experience more intuitive. + +This is an early version of the product experience, and it's likely to evolve as we learn more about developer preferences. Claude Code is an early look into what's possible with agentic coding, and we know there are areas to improve. We plan to enhance tool execution reliability, support for long-running commands, terminal rendering, and Claude's self-knowledge of its capabilities -- as well as many other product experiences -- over the coming weeks. + +### Reporting Bugs + +We welcome feedback during this beta period. Use the `/bug` command to report issues directly within Claude Code, or file a [GitHub issue](https://github.com/anthropics/claude-code/issues). + +### Data collection, usage, and retention + +When you use Claude Code, we collect feedback, which includes usage data (such as code acceptance or rejections), associated conversation data, and user feedback submitted via the `/bug` command. + +#### How we use your data + +We may use feedback to improve our products and services, but we will not train generative models using your feedback from Claude Code. Given their potentially sensitive nature, we store user feedback transcripts for only 30 days. + +If you choose to send us feedback about Claude Code, such as transcripts of your usage, Anthropic may use that feedback to debug related issues and improve Claude Code's functionality (e.g., to reduce the risk of similar bugs occurring in the future). + +### Privacy safeguards + +We have implemented several safeguards to protect your data, including limited retention periods for sensitive information, restricted access to user session data, and clear policies against using feedback for model training. + +For full details, please review our [Commercial Terms of Service](https://www.anthropic.com/legal/commercial-terms) and [Privacy Policy](https://www.anthropic.com/legal/privacy). \ No newline at end of file diff --git a/claude-code/LICENSE.md b/claude-code/LICENSE.md new file mode 100644 index 0000000..c517690 --- /dev/null +++ b/claude-code/LICENSE.md @@ -0,0 +1 @@ +https://en.wikipedia.org/wiki/Cleanroom_software_engineering \ No newline at end of file diff --git a/claude-code/package.json b/claude-code/package.json new file mode 100644 index 0000000..b5af666 --- /dev/null +++ b/claude-code/package.json @@ -0,0 +1,43 @@ +{ + "name": "claude-code", + "version": "0.1.0", + "description": "Claude Code CLI - Your AI coding assistant in the terminal", + "main": "dist/src/cli.js", + "type": "module", + "bin": { + "claude-code": "dist/src/cli.js" + }, + "scripts": { + "build": "tsc", + "start": "node dist/src/cli.js", + "dev": "ts-node --esm src/cli.ts", + "test": "jest", + "lint": "eslint src", + "clean": "rm -rf dist" + }, + "keywords": [ + "claude", + "ai", + "code", + "cli", + "assistant", + "anthropic" + ], + "author": "Anthropic", + "license": "MIT", + "dependencies": { + "node-fetch": "^3.3.1", + "open": "^9.1.0" + }, + "devDependencies": { + "@types/node": "^20.4.7", + "typescript": "^5.1.6", + "ts-node": "^10.9.1", + "eslint": "^8.46.0", + "@typescript-eslint/eslint-plugin": "^6.2.1", + "@typescript-eslint/parser": "^6.2.1" + }, + "engines": { + "node": ">=18.0.0" + } +} \ No newline at end of file diff --git a/claude-code/scripts/preinstall.js b/claude-code/scripts/preinstall.js new file mode 100644 index 0000000..846a2ab --- /dev/null +++ b/claude-code/scripts/preinstall.js @@ -0,0 +1,30 @@ +#!/usr/bin/env node + +/** + * Preinstall script to check for Windows environment and exit gracefully + * with an informative message if detected. + */ + +// Check if running on Windows +if (process.platform === 'win32') { + console.error('\x1b[31m%s\x1b[0m', 'Error: Claude Code is not supported on Windows.'); + console.error('\x1b[33m%s\x1b[0m', 'Claude Code requires macOS or Linux to run properly.'); + console.error('\x1b[33m%s\x1b[0m', 'If you are using WSL (Windows Subsystem for Linux):'); + console.error('\x1b[33m%s\x1b[0m', ' 1. Make sure you are running npm install from within the WSL terminal, not from PowerShell or CMD'); + console.error('\x1b[33m%s\x1b[0m', ' 2. If you\'re still seeing this message in WSL, your environment may be incorrectly reporting as Windows'); + console.error('\x1b[33m%s\x1b[0m', 'Please visit https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview#check-system-requirements for troubleshooting information.'); + process.exit(1); +} + +// Check Node.js version +const nodeMajorVersion = process.versions.node.split('.')[0]; +if (parseInt(nodeMajorVersion, 10) < 18) { + console.error('\x1b[31m%s\x1b[0m', 'Error: Claude Code requires Node.js version 18 or higher.'); + console.error('\x1b[33m%s\x1b[0m', `Current Node.js version is ${process.versions.node}`); + console.error('\x1b[33m%s\x1b[0m', 'Please update your Node.js installation before continuing.'); + console.error('\x1b[33m%s\x1b[0m', 'Visit https://nodejs.org to download the latest version.'); + process.exit(1); +} + +// Success - system is compatible +console.log('\x1b[32m%s\x1b[0m', 'System compatibility checks passed. Continuing installation...'); \ No newline at end of file diff --git a/claude-code/src/ai/client.ts b/claude-code/src/ai/client.ts new file mode 100644 index 0000000..653a075 --- /dev/null +++ b/claude-code/src/ai/client.ts @@ -0,0 +1,432 @@ +/** + * AI Client + * + * Handles interaction with Anthropic's Claude API, including + * text completion, chat, and code assistance features. + */ + +import { logger } from '../utils/logger.js'; +import { createUserError } from '../errors/formatter.js'; +import { ErrorCategory } from '../errors/types.js'; +import { withTimeout, withRetry } from '../utils/async.js'; +import { truncate } from '../utils/formatting.js'; + +// Types for API requests and responses +export interface Message { + role: 'user' | 'assistant' | 'system'; + content: string; +} + +export interface CompletionOptions { + model?: string; + temperature?: number; + maxTokens?: number; + topP?: number; + topK?: number; + stopSequences?: string[]; + stream?: boolean; + system?: string; +} + +export interface CompletionRequest { + model: string; + messages: Message[]; + temperature?: number; + max_tokens?: number; + top_p?: number; + top_k?: number; + stop_sequences?: string[]; + stream?: boolean; + system?: string; +} + +export interface CompletionResponse { + id: string; + model: string; + usage: { + input_tokens: number; + output_tokens: number; + }; + content: { + type: string; + text: string; + }[]; + stop_reason?: string; + stop_sequence?: string; +} + +export interface StreamEvent { + type: 'message_start' | 'content_block_start' | 'content_block_delta' | 'content_block_stop' | 'message_delta' | 'message_stop'; + message?: { + id: string; + model: string; + content: { + type: string; + text: string; + }[]; + stop_reason?: string; + stop_sequence?: string; + }; + index?: number; + delta?: { + type: string; + text: string; + }; + usage_metadata?: { + input_tokens: number; + output_tokens: number; + }; +} + +// Default API configuration +const DEFAULT_CONFIG = { + apiBaseUrl: 'https://api.anthropic.com', + apiVersion: '2023-06-01', + timeout: 60000, // 60 seconds + retryOptions: { + maxRetries: 3, + initialDelayMs: 1000, + maxDelayMs: 10000 + }, + defaultModel: 'claude-3-opus-20240229', + defaultMaxTokens: 4096, + defaultTemperature: 0.7 +}; + +/** + * Claude AI client for interacting with Anthropic's Claude API + */ +export class AIClient { + private config: typeof DEFAULT_CONFIG; + private authToken: string; + + /** + * Create a new AI client + */ + constructor(config: Partial = {}, authToken: string) { + this.config = { ...DEFAULT_CONFIG, ...config }; + this.authToken = authToken; + + logger.debug('AI client created with config', { + apiBaseUrl: this.config.apiBaseUrl, + apiVersion: this.config.apiVersion, + defaultModel: this.config.defaultModel + }); + } + + /** + * Format API request headers + */ + private getHeaders(): Record { + return { + 'Content-Type': 'application/json', + 'X-Api-Key': this.authToken, + 'anthropic-version': this.config.apiVersion, + 'User-Agent': 'claude-code-cli' + }; + } + + /** + * Send a completion request to Claude + */ + async complete( + prompt: string | Message[], + options: CompletionOptions = {} + ): Promise { + logger.debug('Sending completion request', { model: options.model || this.config.defaultModel }); + + // Format the request + const messages: Message[] = Array.isArray(prompt) + ? prompt + : [{ role: 'user', content: prompt }]; + + const request: CompletionRequest = { + model: options.model || this.config.defaultModel, + messages, + max_tokens: options.maxTokens || this.config.defaultMaxTokens, + temperature: options.temperature ?? this.config.defaultTemperature, + stream: false + }; + + // Add optional parameters + if (options.topP !== undefined) request.top_p = options.topP; + if (options.topK !== undefined) request.top_k = options.topK; + if (options.stopSequences) request.stop_sequences = options.stopSequences; + if (options.system) request.system = options.system; + + // Make the API request with timeout and retry + try { + // Wrap the sendRequest method to handle timeouts correctly + const sendRequestWithPath = async (path: string, requestOptions: RequestInit) => { + return this.sendRequest(path, requestOptions); + }; + + const timeoutFn = withTimeout(sendRequestWithPath, this.config.timeout); + + const retryFn = withRetry(timeoutFn, { + maxRetries: this.config.retryOptions.maxRetries, + initialDelayMs: this.config.retryOptions.initialDelayMs, + maxDelayMs: this.config.retryOptions.maxDelayMs + }); + + const response = await retryFn('/v1/messages', { + method: 'POST', + headers: this.getHeaders(), + body: JSON.stringify(request) + }); + + return response; + } catch (error) { + logger.error('Completion request failed', error); + + throw createUserError('Failed to get response from Claude', { + cause: error, + category: ErrorCategory.AI_SERVICE, + resolution: 'Check your internet connection and try again. If the problem persists, verify your API key.' + }); + } + } + + /** + * Send a streaming completion request to Claude + */ + async completeStream( + prompt: string | Message[], + options: CompletionOptions = {}, + onEvent: (event: StreamEvent) => void + ): Promise { + logger.debug('Sending streaming completion request', { model: options.model || this.config.defaultModel }); + + // Format the request + const messages: Message[] = Array.isArray(prompt) + ? prompt + : [{ role: 'user', content: prompt }]; + + const request: CompletionRequest = { + model: options.model || this.config.defaultModel, + messages, + max_tokens: options.maxTokens || this.config.defaultMaxTokens, + temperature: options.temperature ?? this.config.defaultTemperature, + stream: true + }; + + // Add optional parameters + if (options.topP !== undefined) request.top_p = options.topP; + if (options.topK !== undefined) request.top_k = options.topK; + if (options.stopSequences) request.stop_sequences = options.stopSequences; + if (options.system) request.system = options.system; + + // Make the API request + try { + await this.sendStreamRequest('/v1/messages', { + method: 'POST', + headers: this.getHeaders(), + body: JSON.stringify(request) + }, onEvent); + } catch (error) { + logger.error('Streaming completion request failed', error); + + throw createUserError('Failed to get streaming response from Claude', { + cause: error, + category: ErrorCategory.AI_SERVICE, + resolution: 'Check your internet connection and try again. If the problem persists, verify your API key.' + }); + } + } + + /** + * Test the connection to the Claude API + */ + async testConnection(): Promise { + logger.debug('Testing connection to Claude API'); + + try { + // Send a minimal request to test connectivity + const result = await this.complete('Hello', { + maxTokens: 10, + temperature: 0 + }); + + logger.debug('Connection test successful', { modelUsed: result.model }); + return true; + } catch (error) { + logger.error('Connection test failed', error); + return false; + } + } + + /** + * Send a request to the Claude API + */ + private async sendRequest(path: string, options: RequestInit): Promise { + const url = `${this.config.apiBaseUrl}${path}`; + + logger.debug(`Sending request to ${url}`); + + try { + const response = await fetch(url, options); + + if (!response.ok) { + await this.handleErrorResponse(response); + } + + const data = await response.json(); + return data; + } catch (error) { + if (error instanceof Error && error.name === 'AbortError') { + throw createUserError('Request timed out', { + category: ErrorCategory.TIMEOUT, + resolution: 'Try again or increase the timeout setting.' + }); + } + + throw error; + } + } + + /** + * Send a streaming request to the Claude API + */ + private async sendStreamRequest( + path: string, + options: RequestInit, + onEvent: (event: StreamEvent) => void + ): Promise { + const url = `${this.config.apiBaseUrl}${path}`; + + logger.debug(`Sending streaming request to ${url}`); + + try { + const response = await fetch(url, options); + + if (!response.ok) { + await this.handleErrorResponse(response); + } + + if (!response.body) { + throw new Error('Response body is null'); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + while (true) { + const { done, value } = await reader.read(); + + if (done) { + break; + } + + // Decode the chunk and add to buffer + buffer += decoder.decode(value, { stream: true }); + + // Process any complete events in the buffer + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; // Keep the last incomplete line in the buffer + + for (const line of lines) { + const trimmedLine = line.trim(); + if (!trimmedLine || trimmedLine === 'data: [DONE]') { + continue; + } + + // Parse the event data + if (trimmedLine.startsWith('data: ')) { + try { + const eventData = JSON.parse(trimmedLine.slice(6)); + onEvent(eventData); + } catch (error) { + logger.error('Failed to parse stream event', { line: trimmedLine, error }); + } + } + } + } + + // Process any remaining data + if (buffer.trim()) { + if (buffer.trim().startsWith('data: ') && buffer.trim() !== 'data: [DONE]') { + try { + const eventData = JSON.parse(buffer.trim().slice(6)); + onEvent(eventData); + } catch (error) { + logger.error('Failed to parse final stream event', { buffer, error }); + } + } + } + } catch (error) { + if (error instanceof Error && error.name === 'AbortError') { + throw createUserError('Streaming request timed out', { + category: ErrorCategory.TIMEOUT, + resolution: 'Try again or increase the timeout setting.' + }); + } + + throw error; + } + } + + /** + * Handle error responses from the API + */ + private async handleErrorResponse(response: Response): Promise { + let errorData: any = {}; + let errorMessage = `API request failed with status ${response.status}`; + + try { + // Try to parse the error response + errorData = await response.json(); + + if (errorData.error && errorData.error.message) { + errorMessage = errorData.error.message; + } + } catch { + // If we can't parse the response, use the status text + errorMessage = `API request failed: ${response.statusText || response.status}`; + } + + logger.error('API error response', { status: response.status, errorData }); + + // Handle specific error codes + switch (response.status) { + case 401: + throw createUserError('Authentication failed. Please check your API key.', { + category: ErrorCategory.AUTHENTICATION, + resolution: 'Verify your API key and try again. You may need to log in again with the --login flag.' + }); + + case 403: + throw createUserError('You do not have permission to access this resource.', { + category: ErrorCategory.AUTHENTICATION, + resolution: 'Verify that your API key has the necessary permissions.' + }); + + case 404: + throw createUserError('The requested resource was not found.', { + category: ErrorCategory.API, + resolution: 'Check that you are using the correct API endpoint.' + }); + + case 429: + throw createUserError('Rate limit exceeded.', { + category: ErrorCategory.RATE_LIMIT, + resolution: 'Please wait before sending more requests.' + }); + + case 500: + case 502: + case 503: + case 504: + throw createUserError('The API server encountered an error.', { + category: ErrorCategory.SERVER, + resolution: 'This is likely a temporary issue. Please try again later.' + }); + + default: + throw createUserError(errorMessage, { + category: ErrorCategory.API, + resolution: 'Check the error details and try again.' + }); + } + } +} \ No newline at end of file diff --git a/claude-code/src/ai/index.ts b/claude-code/src/ai/index.ts new file mode 100644 index 0000000..d107153 --- /dev/null +++ b/claude-code/src/ai/index.ts @@ -0,0 +1,91 @@ +/** + * AI Module + * + * Provides AI capabilities using Claude, Anthropic's large language model. + * This module handles initialization, configuration, and access to AI services. + */ + +import { AIClient } from './client.js'; +import { logger } from '../utils/logger.js'; +import { createUserError } from '../errors/formatter.js'; +import { ErrorCategory } from '../errors/types.js'; +import { authManager } from '../auth/index.js'; + +// Singleton AI client instance +let aiClient: AIClient | null = null; + +/** + * Initialize the AI module + */ +export async function initAI(config: any = {}): Promise { + logger.info('Initializing AI module'); + + try { + // Check if we have authentication + if (!authManager.isAuthenticated()) { + throw createUserError('Authentication required for AI services', { + category: ErrorCategory.AUTHENTICATION, + resolution: 'Please log in using the login command or provide an API key.' + }); + } + + // Get the auth token + const authToken = authManager.getToken(); + if (!authToken || !authToken.accessToken) { + throw createUserError('No valid authentication token available', { + category: ErrorCategory.AUTHENTICATION, + resolution: 'Please log in again with the login command.' + }); + } + + // Create AI client + aiClient = new AIClient(config, authToken.accessToken); + + // Test connection + logger.debug('Testing connection to AI service'); + const connectionSuccess = await aiClient.testConnection(); + + if (!connectionSuccess) { + throw createUserError('Failed to connect to Claude AI service', { + category: ErrorCategory.CONNECTION, + resolution: 'Check your internet connection and API key, then try again.' + }); + } + + logger.info('AI module initialized successfully'); + return aiClient; + } catch (error) { + logger.error('Failed to initialize AI module', error); + + throw createUserError('Failed to initialize AI capabilities', { + cause: error, + category: ErrorCategory.INITIALIZATION, + resolution: 'Check your authentication and internet connection, then try again.' + }); + } +} + +/** + * Get the AI client instance + */ +export function getAIClient(): AIClient { + if (!aiClient) { + throw createUserError('AI module not initialized', { + category: ErrorCategory.INITIALIZATION, + resolution: 'Make sure to call initAI() before using AI capabilities.' + }); + } + + return aiClient; +} + +/** + * Check if AI module is initialized + */ +export function isAIInitialized(): boolean { + return !!aiClient; +} + +// Re-export types and components +export * from './client.js'; +export * from './prompts.js'; \ No newline at end of file diff --git a/claude-code/src/ai/prompts.ts b/claude-code/src/ai/prompts.ts new file mode 100644 index 0000000..fcf62b9 --- /dev/null +++ b/claude-code/src/ai/prompts.ts @@ -0,0 +1,331 @@ +/** + * AI Prompts + * + * Contains prompt templates and utilities for formatting prompts + * for different AI tasks and scenarios. + */ + +import { Message } from './types.js'; + +// Define MessageRole consts since we're using the type as values +const MESSAGE_ROLE = { + USER: 'user' as const, + ASSISTANT: 'assistant' as const, + SYSTEM: 'system' as const +}; + +/** + * System prompt for code assistance + */ +export const CODE_ASSISTANT_SYSTEM_PROMPT = ` +You are Claude, an AI assistant with expertise in programming and software development. +Your task is to assist with coding-related questions, debugging, refactoring, and explaining code. + +Guidelines: +- Provide clear, concise, and accurate responses +- Include code examples where helpful +- Prioritize modern best practices +- If you're unsure, acknowledge limitations instead of guessing +- Focus on understanding the user's intent, even if the question is ambiguous +`; + +/** + * System prompt for code generation + */ +export const CODE_GENERATION_SYSTEM_PROMPT = ` +You are Claude, an AI assistant focused on helping write high-quality code. +Your task is to generate code based on user requirements and specifications. + +Guidelines: +- Write clean, efficient, and well-documented code +- Follow language-specific best practices and conventions +- Include helpful comments explaining complex sections +- Prioritize maintainability and readability +- Structure code logically with appropriate error handling +- Consider edge cases and potential issues +`; + +/** + * System prompt for code review + */ +export const CODE_REVIEW_SYSTEM_PROMPT = ` +You are Claude, an AI code reviewer with expertise in programming best practices. +Your task is to analyze code, identify issues, and suggest improvements. + +Guidelines: +- Look for bugs, security issues, and performance problems +- Suggest improvements for readability and maintainability +- Identify potential edge cases and error handling gaps +- Point out violations of best practices or conventions +- Provide constructive feedback with clear explanations +- Be thorough but prioritize important issues over minor stylistic concerns +`; + +/** + * System prompt for explaining code + */ +export const CODE_EXPLANATION_SYSTEM_PROMPT = ` +You are Claude, an AI assistant that specializes in explaining code. +Your task is to break down and explain code in a clear, educational manner. + +Guidelines: +- Explain the purpose and functionality of the code +- Break down complex parts step by step +- Define technical terms and concepts when relevant +- Use analogies or examples to illustrate concepts +- Focus on the core logic rather than trivial details +- Adjust explanation depth based on the apparent complexity of the question +`; + +/** + * Interface for prompt templates + */ +export interface PromptTemplate { + /** + * Template string with {placeholders} + */ + template: string; + + /** + * Optional system message to set context + */ + system?: string; + + /** + * Default values for placeholders + */ + defaults?: Record; +} + +/** + * Collection of prompt templates for common tasks + */ +export const PROMPT_TEMPLATES: Record = { + // Code assistance prompt templates + explainCode: { + template: "Please explain what this code does:\n\n{code}", + system: CODE_EXPLANATION_SYSTEM_PROMPT, + defaults: { + code: "// Paste code here" + } + }, + + refactorCode: { + template: "Please refactor this code to improve its {focus}:\n\n{code}\n\nAdditional context: {context}", + system: CODE_GENERATION_SYSTEM_PROMPT, + defaults: { + focus: "readability and maintainability", + code: "// Paste code here", + context: "None" + } + }, + + debugCode: { + template: "Please help me debug the following code:\n\n{code}\n\nThe issue I'm seeing is: {issue}\n\nAny error messages: {errorMessages}", + system: CODE_ASSISTANT_SYSTEM_PROMPT, + defaults: { + code: "// Paste code here", + issue: "Describe the issue you're experiencing", + errorMessages: "None" + } + }, + + reviewCode: { + template: "Please review this code and provide feedback:\n\n{code}", + system: CODE_REVIEW_SYSTEM_PROMPT, + defaults: { + code: "// Paste code here" + } + }, + + generateCode: { + template: "Please write code to {task}.\n\nLanguage/Framework: {language}\n\nRequirements:\n{requirements}", + system: CODE_GENERATION_SYSTEM_PROMPT, + defaults: { + task: "Describe what you want the code to do", + language: "Specify language or framework", + requirements: "- List your requirements here" + } + }, + + documentCode: { + template: "Please add documentation to this code:\n\n{code}\n\nDocumentation style: {style}", + system: CODE_GENERATION_SYSTEM_PROMPT, + defaults: { + code: "// Paste code here", + style: "Standard comments and docstrings" + } + }, + + testCode: { + template: "Please write tests for this code:\n\n{code}\n\nTesting framework: {framework}", + system: CODE_GENERATION_SYSTEM_PROMPT, + defaults: { + code: "// Paste code here", + framework: "Specify testing framework or 'standard'" + } + } +}; + +/** + * Format a prompt by replacing placeholders with values + * + * @param template The prompt template with {placeholders} + * @param values Values to replace placeholders with + * @param defaults Default values for placeholders not in values + * @returns Formatted prompt string + */ +export function formatPrompt( + template: string, + values: Record, + defaults: Record = {} +): string { + // Create a merged object of defaults and provided values + const mergedValues = { ...defaults, ...values }; + + // Replace each placeholder with its value + return template.replace( + /{(\w+)}/g, + (match, key) => { + const value = mergedValues[key]; + return value !== undefined ? String(value) : match; + } + ); +} + +/** + * Format a prompt using a predefined template + * + * @param templateName Name of the template from PROMPT_TEMPLATES + * @param values Values to replace placeholders with + * @returns Object with formatted prompt and system message + */ +export function usePromptTemplate( + templateName: string, + values: Record +): { prompt: string; system?: string } { + const template = PROMPT_TEMPLATES[templateName]; + + if (!template) { + throw new Error(`Prompt template "${templateName}" not found`); + } + + return { + prompt: formatPrompt(template.template, values, template.defaults), + system: template.system + }; +} + +/** + * Create a conversation from a prompt + * + * @param prompt User prompt string + * @param system Optional system message + * @returns Array of messages for the conversation + */ +export function createConversation( + prompt: string, + system?: string +): Array<{ role: 'user' | 'assistant' | 'system'; content: string }> { + const messages = []; + + // Add system message if provided + if (system) { + messages.push({ + role: MESSAGE_ROLE.SYSTEM, + content: system + }); + } + + // Add user message + messages.push({ + role: MESSAGE_ROLE.USER, + content: prompt + }); + + return messages; +} + +/** + * Create a user message + */ +export function createUserMessage(content: string): Message { + return { + role: MESSAGE_ROLE.USER, + content + }; +} + +/** + * Create a system message + */ +export function createSystemMessage(content: string): Message { + return { + role: MESSAGE_ROLE.SYSTEM, + content + }; +} + +/** + * Create an assistant message + */ +export function createAssistantMessage(content: string): Message { + return { + role: MESSAGE_ROLE.ASSISTANT, + content + }; +} + +/** + * Create a message with file context + */ +export function createFileContextMessage( + filePath: string, + content: string, + language?: string +): string { + // Return a simpler content format that works with the current Message type + return `File: ${filePath}\n\n\`\`\`${language || getLanguageFromFilePath(filePath)}\n${content}\n\`\`\``; +} + +/** + * Get language from file path + */ +function getLanguageFromFilePath(filePath: string): string { + const extension = filePath.split('.').pop()?.toLowerCase() || ''; + + const languageMap: Record = { + js: 'javascript', + ts: 'typescript', + jsx: 'javascript', + tsx: 'typescript', + py: 'python', + rb: 'ruby', + java: 'java', + c: 'c', + cpp: 'cpp', + cs: 'csharp', + go: 'go', + rs: 'rust', + php: 'php', + swift: 'swift', + kt: 'kotlin', + scala: 'scala', + sh: 'bash', + html: 'html', + css: 'css', + scss: 'scss', + sass: 'sass', + less: 'less', + md: 'markdown', + json: 'json', + yml: 'yaml', + yaml: 'yaml', + toml: 'toml', + sql: 'sql', + graphql: 'graphql', + xml: 'xml' + }; + + return languageMap[extension] || ''; +} \ No newline at end of file diff --git a/claude-code/src/ai/types.ts b/claude-code/src/ai/types.ts new file mode 100644 index 0000000..2bec3c5 --- /dev/null +++ b/claude-code/src/ai/types.ts @@ -0,0 +1,215 @@ +/** + * AI Types + * + * Type definitions for AI services and functionality. + */ + +/** + * Role for a message in a conversation + */ +export type MessageRole = 'user' | 'assistant' | 'system'; + +/** + * Message in a conversation + */ +export interface Message { + /** + * Role of the message sender + */ + role: MessageRole; + + /** + * Content of the message + */ + content: string; +} + +/** + * AI model information + */ +export interface AIModel { + /** + * Model ID + */ + id: string; + + /** + * Model name + */ + name: string; + + /** + * Model version + */ + version?: string; + + /** + * Maximum context length in tokens + */ + maxContextLength?: number; + + /** + * Whether the model supports streaming + */ + supportsStreaming?: boolean; + + /** + * Default parameters for the model + */ + defaultParams?: Record; +} + +/** + * AI completion usage information + */ +export interface AIUsage { + /** + * Input tokens used + */ + inputTokens: number; + + /** + * Output tokens used + */ + outputTokens: number; + + /** + * Total tokens used + */ + totalTokens?: number; +} + +/** + * AI completion request options + */ +export interface CompletionOptions { + /** + * Model to use + */ + model?: string; + + /** + * Temperature for sampling (0-1) + */ + temperature?: number; + + /** + * Maximum tokens to generate + */ + maxTokens?: number; + + /** + * Top P sampling parameter + */ + topP?: number; + + /** + * Top K sampling parameter + */ + topK?: number; + + /** + * Stop sequences to end generation + */ + stopSequences?: string[]; + + /** + * System message for context + */ + system?: string; +} + +/** + * AI completion request + */ +export interface CompletionRequest { + /** + * Messages for the conversation + */ + messages: Message[]; + + /** + * Completion options + */ + options?: CompletionOptions; +} + +/** + * AI completion response + */ +export interface CompletionResponse { + /** + * Generated text + */ + text: string; + + /** + * Model used for generation + */ + model: string; + + /** + * Reason the generation stopped + */ + stopReason?: string; + + /** + * Token usage information + */ + usage?: AIUsage; +} + +/** + * Callback for streaming AI completions + */ +export type StreamCallback = (event: any) => void; + +/** + * AI client configuration + */ +export interface AIClientConfig { + /** + * Base URL for the API + */ + baseUrl: string; + + /** + * API version + */ + apiVersion: string; + + /** + * Request timeout in milliseconds + */ + timeout?: number; + + /** + * Authentication provider + */ + auth: any; +} + +/** + * Interface for AI clients + */ +export interface AIClientInterface { + /** + * Generate a completion + */ + generateCompletion(request: CompletionRequest): Promise; + + /** + * Generate a streaming completion + */ + generateCompletionStream(request: CompletionRequest, callback: StreamCallback): Promise; + + /** + * Test the connection to the AI service + */ + testConnection(): Promise; + + /** + * Disconnect from the AI service + */ + disconnect(): Promise; +} \ No newline at end of file diff --git a/claude-code/src/auth/index.ts b/claude-code/src/auth/index.ts new file mode 100644 index 0000000..567a070 --- /dev/null +++ b/claude-code/src/auth/index.ts @@ -0,0 +1,471 @@ +/** + * Authentication Manager + * + * Main entry point for authentication functionality. Handles authentication + * flow, token management, and coordination of OAuth and API key auth methods. + */ + +import { + AuthToken, + AuthConfig, + AuthMethod, + AuthResult, + OAuthConfig +} from './types.js'; +import { + DEFAULT_OAUTH_CONFIG, + performOAuthFlow, + refreshOAuthToken +} from './oauth.js'; +import { + createTokenStorage, + isTokenExpired, + validateToken, + getTokenDetails, + createAuthorizationHeader +} from './tokens.js'; +import { logger } from '../utils/logger.js'; +import { createUserError } from '../errors/formatter.js'; +import { ErrorCategory } from '../errors/types.js'; + +// Default auth configuration +const DEFAULT_AUTH_CONFIG: AuthConfig = { + preferredMethod: AuthMethod.API_KEY, + autoRefresh: true, + tokenRefreshThreshold: 300, // 5 minutes + maxRetryAttempts: 3 +}; + +// Storage key for auth tokens +const AUTH_STORAGE_KEY = 'anthropic-auth'; + +/** + * Authentication state + */ +interface AuthState { + initialized: boolean; + authenticated: boolean; + token: AuthToken | null; + method: AuthMethod | null; + lastError: Error | null; +} + +/** + * Authentication Manager + */ +export class AuthManager { + private config: AuthConfig; + private tokenStorage; + private state: AuthState; + + /** + * Create a new auth manager + */ + constructor(config: Partial = {}) { + this.config = { ...DEFAULT_AUTH_CONFIG, ...config }; + this.tokenStorage = createTokenStorage(); + this.state = { + initialized: false, + authenticated: false, + token: null, + method: null, + lastError: null + }; + + logger.debug('AuthManager created with config', this.config); + } + + /** + * Initialize authentication + */ + async initialize(): Promise { + logger.info('Initializing authentication'); + + try { + // Load stored token + const token = await this.tokenStorage.getToken(AUTH_STORAGE_KEY); + + if (token) { + logger.debug('Found stored auth token'); + + // Check if token is valid and not expired + if (validateToken(token)) { + logger.debug('Stored token is valid'); + this.state.token = token; + this.state.authenticated = true; + + // Determine authentication method from token properties + this.state.method = token.refreshToken + ? AuthMethod.OAUTH + : AuthMethod.API_KEY; + + logger.info(`Authenticated using stored ${this.state.method} token`); + } else if (this.config.autoRefresh && token.refreshToken) { + logger.debug('Stored token expired, attempting refresh'); + + // Try to refresh the token + await this.refreshToken(); + } else { + logger.debug('Stored token is invalid, clearing it'); + await this.tokenStorage.deleteToken(AUTH_STORAGE_KEY); + } + } else { + logger.debug('No stored auth token found'); + } + + this.state.initialized = true; + return this.state.authenticated; + } catch (error) { + logger.error('Failed to initialize authentication', error); + this.state.lastError = error instanceof Error ? error : new Error(String(error)); + return false; + } + } + + /** + * Check if the current token needs refresh + */ + async checkAndRefreshToken(): Promise { + if (!this.state.token || !this.config.autoRefresh) { + return false; + } + + // Only refresh OAuth tokens + if (this.state.method !== AuthMethod.OAUTH || !this.state.token.refreshToken) { + return false; + } + + // Check if token is near expiration + const threshold = this.config.tokenRefreshThreshold || 300; + + if (isTokenExpired(this.state.token, threshold)) { + logger.debug('Token is near expiration, refreshing'); + return await this.refreshToken(); + } + + return false; + } + + /** + * Refresh the current token + */ + async refreshToken(): Promise { + if (!this.state.token?.refreshToken) { + logger.debug('No refresh token available'); + return false; + } + + logger.info('Refreshing authentication token'); + + try { + const oauthConfig = this.config.oauth || DEFAULT_OAUTH_CONFIG; + + const newToken = await refreshOAuthToken( + this.state.token.refreshToken, + oauthConfig + ); + + // Update state with new token + this.state.token = newToken; + + // Save the new token + await this.tokenStorage.saveToken(AUTH_STORAGE_KEY, newToken); + + logger.info('Token refreshed successfully'); + return true; + } catch (error) { + logger.error('Failed to refresh token', error); + this.state.lastError = error instanceof Error ? error : new Error(String(error)); + + // Clear invalid token + this.state.token = null; + this.state.authenticated = false; + await this.tokenStorage.deleteToken(AUTH_STORAGE_KEY); + + return false; + } + } + + /** + * Authenticate with API key + */ + async authenticateWithApiKey(apiKey: string): Promise { + logger.info('Authenticating with API key'); + + try { + // Validate API key format + if (!apiKey || apiKey.trim().length < 10) { + throw createUserError('Invalid API key format', { + category: ErrorCategory.AUTHENTICATION, + resolution: 'Check your API key and try again.' + }); + } + + // Create a token from the API key + const token: AuthToken = { + accessToken: apiKey, + expiresAt: 0, // API keys don't expire + tokenType: 'Bearer', + scope: 'anthropic.api' + }; + + // Save the token + await this.tokenStorage.saveToken(AUTH_STORAGE_KEY, token); + + // Update state + this.state.token = token; + this.state.authenticated = true; + this.state.method = AuthMethod.API_KEY; + + logger.info('API key authentication successful'); + + return { + success: true, + token, + method: AuthMethod.API_KEY, + state: AuthState.AUTHENTICATED + }; + } catch (error) { + logger.error('API key authentication failed', error); + + this.state.lastError = error instanceof Error ? error : new Error(String(error)); + this.state.authenticated = false; + this.state.token = null; + + return { + success: false, + error: error instanceof Error ? error.message : String(error), + method: AuthMethod.API_KEY, + state: AuthState.FAILED + }; + } + } + + /** + * Authenticate with OAuth + */ + async authenticateWithOAuth(config?: OAuthConfig): Promise { + logger.info('Authenticating with OAuth'); + + try { + const oauthConfig = config || this.config.oauth || DEFAULT_OAUTH_CONFIG; + + // Perform the OAuth flow + const result = await performOAuthFlow(oauthConfig); + + if (result.success && result.token) { + // Save the token + await this.tokenStorage.saveToken(AUTH_STORAGE_KEY, result.token); + + // Update state + this.state.token = result.token; + this.state.authenticated = true; + this.state.method = AuthMethod.OAUTH; + + logger.info('OAuth authentication successful'); + } else { + logger.warn('OAuth authentication failed', result.error); + + if (result.error) { + this.state.lastError = new Error(result.error); + } + } + + return result; + } catch (error) { + logger.error('OAuth authentication failed', error); + + this.state.lastError = error instanceof Error ? error : new Error(String(error)); + this.state.authenticated = false; + this.state.token = null; + + return { + success: false, + error: error instanceof Error ? error.message : String(error), + method: AuthMethod.OAUTH, + state: AuthState.FAILED + }; + } + } + + /** + * Log out (clear credentials) + */ + async logout(): Promise { + logger.info('Logging out'); + + // Clear state + this.state.token = null; + this.state.authenticated = false; + this.state.method = null; + + // Clear storage + await this.tokenStorage.deleteToken(AUTH_STORAGE_KEY); + + logger.info('Logged out successfully'); + } + + /** + * Get the current auth token + */ + getToken(): AuthToken | null { + return this.state.token; + } + + /** + * Get authorization header for API requests + */ + getAuthorizationHeader(): string | null { + if (!this.state.token) { + return null; + } + + return createAuthorizationHeader(this.state.token); + } + + /** + * Check if authenticated + */ + isAuthenticated(): boolean { + return this.state.authenticated && !!this.state.token; + } + + /** + * Get the current auth method + */ + getAuthMethod(): AuthMethod | null { + return this.state.method; + } + + /** + * Get token details for display + */ + getTokenDetails(): Record | null { + if (!this.state.token) { + return null; + } + + return getTokenDetails(this.state.token); + } + + /** + * Get the last error + */ + getLastError(): Error | null { + return this.state.lastError; + } +} + +// Create and export singleton auth manager +export const authManager = new AuthManager(); + +/** + * Initialize the authentication system + * + * @param config Configuration options for authentication + * @returns The initialized authentication manager + */ +export async function initAuthentication(config: any = {}): Promise { + logger.info('Initializing authentication system'); + + try { + // Update auth manager with provided config + if (config.auth) { + // Update configuration by creating a new instance if needed + // or applying the settings to the existing instance + // Note: We can't call a non-existent configure method + } + + // Initialize auth manager + await authManager.initialize(); + + logger.info('Authentication system initialized successfully'); + + // Return auth interface + return { + /** + * Authenticate with the specified method + */ + authenticate: async (options: { method?: AuthMethod } = {}): Promise => { + const method = options.method || authManager.getAuthMethod() || DEFAULT_AUTH_CONFIG.preferredMethod; + + if (method === AuthMethod.API_KEY) { + const apiKey = process.env.ANTHROPIC_API_KEY; + + if (!apiKey) { + throw createUserError('API key not found', { + category: ErrorCategory.AUTHENTICATION, + resolution: 'Please set the ANTHROPIC_API_KEY environment variable or log in with OAuth.' + }); + } + + return authManager.authenticateWithApiKey(apiKey); + } else if (method === AuthMethod.OAUTH) { + return authManager.authenticateWithOAuth(); + } else { + throw createUserError(`Unsupported authentication method: ${method}`, { + category: ErrorCategory.AUTHENTICATION, + resolution: 'Please use either "api_key" or "oauth" as the authentication method.' + }); + } + }, + + /** + * Log out (clear credentials) + */ + logout: async (): Promise => { + return authManager.logout(); + }, + + /** + * Get the current token + */ + getToken: (): AuthToken | null => { + return authManager.getToken(); + }, + + /** + * Check if authenticated + */ + isAuthenticated: (): boolean => { + return authManager.isAuthenticated(); + }, + + /** + * Get the current authentication method + */ + getAuthMethod: (): AuthMethod | null => { + return authManager.getAuthMethod(); + }, + + /** + * Get token details (for display) + */ + getTokenDetails: (): Record | null => { + return authManager.getTokenDetails(); + }, + + /** + * Get authorization header + */ + getAuthorizationHeader: (): string | null => { + return authManager.getAuthorizationHeader(); + }, + + /** + * Refresh the token + */ + refreshToken: async (): Promise => { + return authManager.refreshToken(); + } + }; + } catch (error) { + logger.error('Failed to initialize authentication system', error); + throw error; + } +} + +// Export other auth components +export * from './types.js'; +export * from './oauth.js'; +export * from './tokens.js'; \ No newline at end of file diff --git a/claude-code/src/auth/manager.ts b/claude-code/src/auth/manager.ts new file mode 100644 index 0000000..e2072f5 --- /dev/null +++ b/claude-code/src/auth/manager.ts @@ -0,0 +1,337 @@ +/** + * Authentication Manager + * + * Manages authentication processes, token handling, and authentication state. + */ + +import { AuthToken, AuthMethod, AuthState, AuthResult, TokenStorage, OAuthConfig } from './types.js'; +import { createTokenStorage, isTokenExpired } from './tokens.js'; +import { performOAuthFlow, refreshOAuthToken, DEFAULT_OAUTH_CONFIG } from './oauth.js'; +import { logger } from '../utils/logger.js'; +import { EventEmitter } from 'events'; + +// Authentication events +export const AUTH_EVENTS = { + STATE_CHANGED: 'auth:state_changed', + LOGGED_IN: 'auth:logged_in', + LOGGED_OUT: 'auth:logged_out', + TOKEN_REFRESHED: 'auth:token_refreshed', + ERROR: 'auth:error' +}; + +/** + * Authentication Manager Class + * + * Centralizes all authentication-related functionality + */ +export class AuthManager extends EventEmitter { + private state: AuthState = AuthState.INITIAL; + private tokenStorage: TokenStorage; + private currentToken: AuthToken | null = null; + private refreshTimer: NodeJS.Timeout | null = null; + private readonly tokenKey = 'default'; + private readonly config: { + apiKey?: string; + oauth?: OAuthConfig; + preferredMethod?: AuthMethod; + autoRefresh: boolean; + tokenRefreshThreshold: number; + maxRetryAttempts: number; + }; + + /** + * Create a new AuthManager instance + */ + constructor(config: any) { + super(); + + // Extract authentication-related configuration + this.config = { + apiKey: config.api?.key, + oauth: config.oauth || DEFAULT_OAUTH_CONFIG, + preferredMethod: config.preferredMethod, + autoRefresh: config.autoRefresh !== false, + tokenRefreshThreshold: config.tokenRefreshThreshold || 300, // 5 minutes + maxRetryAttempts: config.maxRetryAttempts || 3 + }; + + // Create token storage + this.tokenStorage = createTokenStorage(); + + logger.debug('Authentication manager created'); + } + + /** + * Initialize the authentication manager + */ + async initialize(): Promise { + logger.debug('Initializing authentication manager'); + + try { + // Try to load existing token + this.currentToken = await this.tokenStorage.getToken(this.tokenKey); + + if (this.currentToken) { + // Check if token is valid and not expired + if (isTokenExpired(this.currentToken, this.config.tokenRefreshThreshold)) { + logger.info('Token expired, attempting to refresh'); + + if (this.currentToken.refreshToken) { + try { + await this.refreshToken(); + } catch (error) { + logger.warn('Failed to refresh token, will need to re-authenticate'); + this.currentToken = null; + this.setState(AuthState.INITIAL); + } + } else { + logger.warn('No refresh token available, will need to re-authenticate'); + this.currentToken = null; + this.setState(AuthState.INITIAL); + } + } else { + // Valid token + logger.info('Valid authentication token found'); + this.setState(AuthState.AUTHENTICATED); + + // Set up auto-refresh if enabled + if (this.config.autoRefresh) { + this.scheduleTokenRefresh(); + } + } + } else { + logger.info('No authentication token found'); + this.setState(AuthState.INITIAL); + } + } catch (error) { + logger.error('Error initializing authentication manager', error); + this.setState(AuthState.FAILED); + this.emit(AUTH_EVENTS.ERROR, error); + } + } + + /** + * Check if user is authenticated + */ + isAuthenticated(): boolean { + return this.state === AuthState.AUTHENTICATED && !!this.currentToken; + } + + /** + * Get the current authentication state + */ + getState(): AuthState { + return this.state; + } + + /** + * Get the current authentication token + */ + getToken(): AuthToken | null { + return this.currentToken; + } + + /** + * Get the authorization header value for API requests + */ + getAuthorizationHeader(): string | null { + if (!this.currentToken) { + return null; + } + + return `${this.currentToken.tokenType} ${this.currentToken.accessToken}`; + } + + /** + * Authenticate the user + */ + async authenticate(method?: AuthMethod): Promise { + // Determine authentication method + const authMethod = method || this.config.preferredMethod || (this.config.apiKey ? AuthMethod.API_KEY : AuthMethod.OAUTH); + + logger.info(`Authenticating using ${authMethod} method`); + this.setState(AuthState.AUTHENTICATING); + + try { + let result: AuthResult; + + if (authMethod === AuthMethod.API_KEY) { + result = await this.authenticateWithApiKey(); + } else { + result = await this.authenticateWithOAuth(); + } + + if (result.success && result.token) { + this.currentToken = result.token; + await this.tokenStorage.saveToken(this.tokenKey, result.token); + this.setState(AuthState.AUTHENTICATED); + this.emit(AUTH_EVENTS.LOGGED_IN, { method: authMethod }); + + // Set up auto-refresh if enabled + if (this.config.autoRefresh && result.token.refreshToken) { + this.scheduleTokenRefresh(); + } + } else { + this.setState(AuthState.FAILED); + this.emit(AUTH_EVENTS.ERROR, result.error); + } + + return result; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error(`Authentication failed: ${errorMessage}`); + + this.setState(AuthState.FAILED); + this.emit(AUTH_EVENTS.ERROR, error); + + return { + success: false, + error: errorMessage, + state: AuthState.FAILED + }; + } + } + + /** + * Log out the current user + */ + async logout(): Promise { + logger.info('Logging out user'); + + // Clear token refresh timer + if (this.refreshTimer) { + clearTimeout(this.refreshTimer); + this.refreshTimer = null; + } + + // Clear token from storage + try { + await this.tokenStorage.deleteToken(this.tokenKey); + } catch (error) { + logger.warn('Error clearing token from storage', error); + } + + // Reset state + this.currentToken = null; + this.setState(AuthState.INITIAL); + this.emit(AUTH_EVENTS.LOGGED_OUT); + } + + /** + * Authenticate using API key + */ + private async authenticateWithApiKey(): Promise { + const apiKey = this.config.apiKey; + + if (!apiKey) { + return { + success: false, + error: 'No API key available', + state: AuthState.FAILED + }; + } + + // Create a token with the API key + // The token doesn't expire and has no refresh token + const token: AuthToken = { + accessToken: apiKey, + expiresAt: Number.MAX_SAFE_INTEGER, // Never expires + tokenType: 'Bearer', + scope: 'all' + }; + + return { + success: true, + method: AuthMethod.API_KEY, + token, + state: AuthState.AUTHENTICATED + }; + } + + /** + * Authenticate using OAuth flow + */ + private async authenticateWithOAuth(): Promise { + return performOAuthFlow(this.config.oauth); + } + + /** + * Refresh the current token + */ + private async refreshToken(): Promise { + if (!this.currentToken || !this.currentToken.refreshToken) { + throw new Error('No refresh token available'); + } + + this.setState(AuthState.REFRESHING); + logger.debug('Refreshing authentication token'); + + try { + const newToken = await refreshOAuthToken(this.currentToken.refreshToken, this.config.oauth); + + // Update the current token + this.currentToken = newToken; + await this.tokenStorage.saveToken(this.tokenKey, newToken); + + this.setState(AuthState.AUTHENTICATED); + this.emit(AUTH_EVENTS.TOKEN_REFRESHED); + + // Schedule the next refresh + if (this.config.autoRefresh) { + this.scheduleTokenRefresh(); + } + } catch (error) { + logger.error('Failed to refresh token', error); + + // If we can't refresh the token, we need to re-authenticate + this.setState(AuthState.FAILED); + this.emit(AUTH_EVENTS.ERROR, error); + + throw error; + } + } + + /** + * Schedule a token refresh + */ + private scheduleTokenRefresh(): void { + if (this.refreshTimer) { + clearTimeout(this.refreshTimer); + this.refreshTimer = null; + } + + if (!this.currentToken || !this.currentToken.refreshToken) { + return; + } + + // Calculate when to refresh the token + // Refresh when the token is at 80% of its lifetime + const now = Math.floor(Date.now() / 1000); + const expiresIn = this.currentToken.expiresAt - now; + const refreshIn = Math.max(0, expiresIn - this.config.tokenRefreshThreshold); + + logger.debug(`Scheduling token refresh in ${refreshIn} seconds`); + + this.refreshTimer = setTimeout(() => { + this.refreshToken().catch(error => { + logger.error('Scheduled token refresh failed', error); + }); + }, refreshIn * 1000); + + // Make sure the timer doesn't prevent Node.js from exiting + if (this.refreshTimer.unref) { + this.refreshTimer.unref(); + } + } + + /** + * Update the authentication state + */ + private setState(newState: AuthState): void { + if (this.state !== newState) { + logger.debug(`Authentication state changed: ${AuthState[this.state]} → ${AuthState[newState]}`); + this.state = newState; + this.emit(AUTH_EVENTS.STATE_CHANGED, newState); + } + } +} \ No newline at end of file diff --git a/claude-code/src/auth/oauth.ts b/claude-code/src/auth/oauth.ts new file mode 100644 index 0000000..556b231 --- /dev/null +++ b/claude-code/src/auth/oauth.ts @@ -0,0 +1,279 @@ +/** + * OAuth Authentication + * + * Handles the OAuth authentication flow, including token retrieval, + * refresh, and authorization redirects. + */ + +import { AuthMethod, AuthState, AuthResult, AuthToken, OAuthConfig } from './types.js'; +import { logger } from '../utils/logger.js'; +import { createUserError } from '../errors/formatter.js'; +import { ErrorCategory } from '../errors/types.js'; +import { createDeferred } from '../utils/async.js'; +import open from 'open'; + +/** + * Default OAuth configuration for Anthropic API + */ +export const DEFAULT_OAUTH_CONFIG: OAuthConfig = { + clientId: 'claude-code-cli', + authorizationEndpoint: 'https://auth.anthropic.com/oauth2/auth', + tokenEndpoint: 'https://auth.anthropic.com/oauth2/token', + redirectUri: 'http://localhost:3000/callback', + scopes: ['anthropic.claude'], + responseType: 'code', + usePkce: true +}; + +/** + * Performs the OAuth authentication flow + */ +export async function performOAuthFlow(config: OAuthConfig): Promise { + logger.info('Starting OAuth authentication flow'); + + try { + // Generate code verifier and challenge if using PKCE + const { codeVerifier, codeChallenge } = config.usePkce + ? generatePkceParams() + : { codeVerifier: '', codeChallenge: '' }; + + // Generate a random state + const state = generateRandomString(32); + + // Build the authorization URL + const authUrl = buildAuthorizationUrl(config, state, codeChallenge); + + // Open the browser to the authorization URL + logger.debug(`Opening browser to: ${authUrl}`); + await open(authUrl); + + // Start a local server to listen for the callback + logger.debug('Starting local server to receive callback'); + const { code, receivedState } = await startLocalServerForCallback(config.redirectUri); + + // Verify state matches + if (state !== receivedState) { + throw createUserError('OAuth state mismatch. Authentication may have been tampered with', { + category: ErrorCategory.AUTHENTICATION, + resolution: 'Try the authentication process again. If the issue persists, contact support.' + }); + } + + // Exchange code for token + logger.debug('Exchanging code for token'); + const token = await exchangeCodeForToken(config, code, codeVerifier); + + logger.info('OAuth authentication successful'); + + return { + success: true, + method: AuthMethod.OAUTH, + token, + state: AuthState.AUTHENTICATED + }; + } catch (error) { + logger.error('OAuth authentication failed', error); + + return { + success: false, + method: AuthMethod.OAUTH, + error: error instanceof Error ? error.message : String(error), + state: AuthState.FAILED + }; + } +} + +/** + * Refresh an OAuth token + */ +export async function refreshOAuthToken(refreshToken: string, config: OAuthConfig): Promise { + logger.debug('Refreshing OAuth token'); + + try { + const response = await fetch(config.tokenEndpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' + }, + body: new URLSearchParams({ + client_id: config.clientId, + ...(config.clientSecret ? { client_secret: config.clientSecret } : {}), + grant_type: 'refresh_token', + refresh_token: refreshToken + }).toString() + }); + + if (!response.ok) { + const error = await response.text(); + throw createUserError(`Failed to refresh token: ${error}`, { + category: ErrorCategory.AUTHENTICATION, + resolution: 'Try logging in again. Your session may have expired.' + }); + } + + const data = await response.json(); + + // Build token from response + const token: AuthToken = { + accessToken: data.access_token, + refreshToken: data.refresh_token || refreshToken, // Use existing refresh token if not provided + expiresAt: Math.floor(Date.now() / 1000) + (data.expires_in || 3600), + tokenType: data.token_type || 'Bearer', + scope: data.scope || '' + }; + + logger.debug('Token refreshed successfully'); + + return token; + } catch (error) { + logger.error('Failed to refresh token', error); + throw createUserError('Failed to refresh authentication token', { + cause: error, + category: ErrorCategory.AUTHENTICATION, + resolution: 'Try logging in again with the --login flag.' + }); + } +} + +/** + * Generate PKCE parameters (code verifier and challenge) + */ +function generatePkceParams(): { codeVerifier: string; codeChallenge: string } { + // In a real implementation, this would use crypto functions to generate + // a proper code verifier and S256 code challenge. + // For simplicity, we're using a placeholder implementation. + + const codeVerifier = generateRandomString(64); + + // In a real implementation, this would be a SHA256 hash of the verifier + // For now, we'll just use the same string (this is not secure!) + const codeChallenge = codeVerifier; + + return { codeVerifier, codeChallenge }; +} + +/** + * Generate a random string of the specified length + */ +function generateRandomString(length: number): string { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'; + let result = ''; + + for (let i = 0; i < length; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + + return result; +} + +/** + * Build the authorization URL + */ +function buildAuthorizationUrl( + config: OAuthConfig, + state: string, + codeChallenge: string +): string { + const url = new URL(config.authorizationEndpoint); + + // Add query parameters + url.searchParams.append('client_id', config.clientId); + url.searchParams.append('redirect_uri', config.redirectUri); + url.searchParams.append('response_type', config.responseType); + url.searchParams.append('state', state); + + // Add scopes + if (config.scopes && config.scopes.length > 0) { + url.searchParams.append('scope', config.scopes.join(' ')); + } + + // Add PKCE challenge if available + if (codeChallenge) { + url.searchParams.append('code_challenge', codeChallenge); + url.searchParams.append('code_challenge_method', 'S256'); + } + + return url.toString(); +} + +/** + * Start a local server to listen for the OAuth callback + */ +async function startLocalServerForCallback(redirectUri: string): Promise<{ code: string; receivedState: string }> { + // In a real implementation, this would start a local HTTP server + // listening on the redirect URI and wait for the callback. + // For simplicity, we're simulating this behavior. + + const { promise, resolve } = createDeferred<{ code: string; receivedState: string }>(); + + // Extract port from redirect URI + const url = new URL(redirectUri); + const port = parseInt(url.port, 10) || 80; + + logger.debug(`Would start local server on port ${port}`); + + // Simulate receiving a callback after some time + setTimeout(() => { + // In a real implementation, this would parse the callback URL + // For now, we're just simulating a successful response + resolve({ + code: generateRandomString(32), + receivedState: generateRandomString(32) + }); + }, 1000); + + return promise; +} + +/** + * Exchange authorization code for token + */ +async function exchangeCodeForToken( + config: OAuthConfig, + code: string, + codeVerifier: string +): Promise { + const params = new URLSearchParams({ + client_id: config.clientId, + ...(config.clientSecret ? { client_secret: config.clientSecret } : {}), + grant_type: 'authorization_code', + code, + redirect_uri: config.redirectUri + }); + + // Add code verifier if using PKCE + if (codeVerifier) { + params.append('code_verifier', codeVerifier); + } + + // Make the token request + const response = await fetch(config.tokenEndpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' + }, + body: params.toString() + }); + + if (!response.ok) { + const error = await response.text(); + throw createUserError(`Failed to exchange code for token: ${error}`, { + category: ErrorCategory.AUTHENTICATION + }); + } + + const data = await response.json(); + + // Build token from response + const token: AuthToken = { + accessToken: data.access_token, + refreshToken: data.refresh_token, + expiresAt: Math.floor(Date.now() / 1000) + (data.expires_in || 3600), + tokenType: data.token_type || 'Bearer', + scope: data.scope || '' + }; + + return token; +} \ No newline at end of file diff --git a/claude-code/src/auth/tokens.ts b/claude-code/src/auth/tokens.ts new file mode 100644 index 0000000..6ce24ef --- /dev/null +++ b/claude-code/src/auth/tokens.ts @@ -0,0 +1,167 @@ +/** + * Authentication Tokens + * + * Handles token storage, validation, and refreshing for authentication. + */ + +import { logger } from '../utils/logger.js'; +import { AuthToken, TokenStorage } from './types.js'; + +/** + * Create a token storage provider + */ +export function createTokenStorage(): TokenStorage { + // In a real implementation, this would use secure storage + // like the OS keychain or encrypted file storage + + // For development, we'll use a simple in-memory storage + const tokenStore = new Map(); + + return { + /** + * Save a token to storage + */ + async saveToken(key: string, token: AuthToken): Promise { + logger.debug(`Saving auth token for ${key}`); + tokenStore.set(key, token); + }, + + /** + * Get a token from storage + */ + async getToken(key: string): Promise { + logger.debug(`Getting auth token for ${key}`); + return tokenStore.get(key) || null; + }, + + /** + * Delete a token from storage + */ + async deleteToken(key: string): Promise { + logger.debug(`Deleting auth token for ${key}`); + tokenStore.delete(key); + }, + + /** + * Clear all tokens from storage + */ + async clearTokens(): Promise { + logger.debug('Clearing all auth tokens'); + tokenStore.clear(); + } + }; +} + +/** + * Check if a token is expired + */ +export function isTokenExpired(token: AuthToken, thresholdSeconds: number = 0): boolean { + if (!token.expiresAt) { + return false; + } + + const now = Math.floor(Date.now() / 1000); + return token.expiresAt - now <= thresholdSeconds; +} + +/** + * Validate a token + */ +export function validateToken(token: AuthToken): boolean { + // Check if token has required fields + if (!token.accessToken) { + return false; + } + + // Check if token is expired + if (isTokenExpired(token)) { + return false; + } + + return true; +} + +/** + * Format an expiration timestamp + */ +export function formatTokenExpiration(expiresAt: number): string { + if (!expiresAt) { + return 'never'; + } + + const date = new Date(expiresAt * 1000); + return date.toLocaleString(); +} + +/** + * Get token details for display + */ +export function getTokenDetails(token: AuthToken): Record { + const details: Record = {}; + + // Get token type + details.type = token.tokenType || 'Bearer'; + + // Get token expiration + if (token.expiresAt) { + details.expires = formatTokenExpiration(token.expiresAt); + + const now = Math.floor(Date.now() / 1000); + const expiresIn = token.expiresAt - now; + + if (expiresIn > 0) { + details.expiresIn = `${Math.floor(expiresIn / 60)} minutes`; + } else { + details.expiresIn = 'Expired'; + } + } else { + details.expires = 'Never'; + } + + // Get token scope + if (token.scope) { + details.scope = token.scope; + } + + // Get token ID if available + if (token.id) { + details.id = token.id; + } + + // Mask the access token + if (token.accessToken) { + const tokenLength = token.accessToken.length; + details.accessToken = `${token.accessToken.substring(0, 4)}...${token.accessToken.substring(tokenLength - 4)}`; + } + + return details; +} + +/** + * Extract token from an authorization header + */ +export function extractTokenFromHeader(header: string): string | null { + if (!header) { + return null; + } + + // Check for Bearer token + if (header.startsWith('Bearer ')) { + return header.substring(7).trim(); + } + + // Check for token without prefix + if (!header.includes(' ')) { + return header.trim(); + } + + return null; +} + +/** + * Create an authorization header from a token + */ +export function createAuthorizationHeader(token: AuthToken): string { + const tokenType = token.tokenType || 'Bearer'; + return `${tokenType} ${token.accessToken}`; +} \ No newline at end of file diff --git a/claude-code/src/auth/types.ts b/claude-code/src/auth/types.ts new file mode 100644 index 0000000..3822c96 --- /dev/null +++ b/claude-code/src/auth/types.ts @@ -0,0 +1,207 @@ +/** + * Authentication Types + * + * Type definitions for authentication functionality. + */ + +/** + * Authentication token structure + */ +export interface AuthToken { + /** + * Access token + */ + accessToken: string; + + /** + * Refresh token (optional) + */ + refreshToken?: string; + + /** + * Token expiration timestamp (Unix time in seconds) + */ + expiresAt: number; + + /** + * Token type (e.g., 'Bearer') + */ + tokenType: string; + + /** + * Token scope + */ + scope: string; + + /** + * Token ID (optional) + */ + id?: string; +} + +/** + * Authentication methods + */ +export enum AuthMethod { + /** + * API key authentication + */ + API_KEY = 'api_key', + + /** + * OAuth authentication + */ + OAUTH = 'oauth' +} + +/** + * Authentication states + */ +export enum AuthState { + /** + * Initial state + */ + INITIAL = 'initial', + + /** + * Authentication in progress + */ + AUTHENTICATING = 'authenticating', + + /** + * Successfully authenticated + */ + AUTHENTICATED = 'authenticated', + + /** + * Authentication failed + */ + FAILED = 'failed', + + /** + * Refreshing authentication + */ + REFRESHING = 'refreshing', + + /** + * Expired authentication + */ + EXPIRED = 'expired', + + /** + * Unauthenticated state + */ + UNAUTHENTICATED = 'unauthenticated' +} + +/** + * Authentication result + */ +export interface AuthResult { + /** + * Whether authentication was successful + */ + success: boolean; + + /** + * Authentication method used + */ + method?: AuthMethod; + + /** + * Authentication token + */ + token?: AuthToken; + + /** + * Current authentication state + */ + state: AuthState; + + /** + * Error message if authentication failed + */ + error?: string; +} + +/** + * Token storage interface + */ +export interface TokenStorage { + /** + * Save a token + */ + saveToken(key: string, token: AuthToken): Promise; + + /** + * Get a token + */ + getToken(key: string): Promise; + + /** + * Delete a token + */ + deleteToken(key: string): Promise; + + /** + * Clear all tokens + */ + clearTokens(): Promise; +} + +/** + * OAuth configuration + */ +export interface OAuthConfig { + /** + * Client ID + */ + clientId: string; + + /** + * Client secret + */ + clientSecret?: string; + + /** + * Authorization endpoint + */ + authorizationEndpoint: string; + + /** + * Token endpoint + */ + tokenEndpoint: string; + + /** + * Redirect URI + */ + redirectUri: string; + + /** + * Requested scopes + */ + scopes: string[]; + + /** + * Response type + */ + responseType: string; + + /** + * Whether to use PKCE + */ + usePkce?: boolean; +} + +/** + * Authentication manager configuration + */ +export interface AuthConfig { + apiKey?: string; + oauth?: OAuthConfig; + preferredMethod?: AuthMethod; + autoRefresh?: boolean; + tokenRefreshThreshold?: number; // in seconds + maxRetryAttempts?: number; +} \ No newline at end of file diff --git a/claude-code/src/cli.ts b/claude-code/src/cli.ts new file mode 100644 index 0000000..2911d35 --- /dev/null +++ b/claude-code/src/cli.ts @@ -0,0 +1,193 @@ +#!/usr/bin/env node +/** + * Claude Code CLI + * + * Main entry point for the Claude Code CLI tool. Handles command-line + * argument parsing, command dispatching, and error handling. + */ + +import { commandRegistry, executeCommand, generateCommandHelp } from './commands/index.js'; +import { logger } from './utils/logger.js'; +import { formatErrorForDisplay } from './errors/formatter.js'; +import { initAI } from './ai/index.js'; +import { authManager } from './auth/index.js'; +import { registerCommands } from './commands/register.js'; +import { UserError } from './errors/types.js'; +import pkg from '../package.json' assert { type: 'json' }; + +// Get version from package.json +const version = pkg.version; + +// Maximum width of the help output +const HELP_WIDTH = 100; + +/** + * Display help information + */ +function displayHelp(commandName?: string): void { + if (commandName && commandName !== 'help') { + // Display help for a specific command + const command = commandRegistry.get(commandName); + + if (!command) { + console.error(`Unknown command: ${commandName}`); + console.error('Use "claude-code help" to see available commands.'); + process.exit(1); + } + + console.log(generateCommandHelp(command)); + return; + } + + // Display general help + console.log(` +Claude Code CLI v${version} + +A command-line interface for interacting with Claude AI for code assistance, +generation, refactoring, and more. + +Usage: + claude-code [arguments] [options] + +Available Commands:`); + + // Group commands by category + const categories = commandRegistry.getCategories(); + + // Commands without a category + const uncategorizedCommands = commandRegistry.list() + .filter(cmd => !cmd.category && !cmd.hidden) + .sort((a, b) => a.name.localeCompare(b.name)); + + if (uncategorizedCommands.length > 0) { + for (const command of uncategorizedCommands) { + console.log(` ${command.name.padEnd(15)} ${command.description}`); + } + console.log(''); + } + + // Commands with categories + for (const category of categories) { + console.log(`${category}:`); + + const commands = commandRegistry.getByCategory(category) + .filter(cmd => !cmd.hidden) + .sort((a, b) => a.name.localeCompare(b.name)); + + for (const command of commands) { + console.log(` ${command.name.padEnd(15)} ${command.description}`); + } + + console.log(''); + } + + console.log(`For more information on a specific command, use: + claude-code help + +Examples: + $ claude-code ask "How do I implement a binary search tree in TypeScript?" + $ claude-code explain path/to/file.js + $ claude-code refactor path/to/file.py --focus=performance + $ claude-code fix path/to/code.ts +`); +} + +/** + * Display version information + */ +function displayVersion(): void { + console.log(`Claude Code CLI v${version}`); +} + +/** + * Parse command-line arguments + */ +function parseCommandLineArgs(): { commandName: string; args: string[] } { + // Get arguments, excluding node and script path + const args = process.argv.slice(2); + + // Handle empty command + if (args.length === 0) { + displayHelp(); + process.exit(0); + } + + // Extract command name + const commandName = args[0].toLowerCase(); + + // Handle help command + if (commandName === 'help') { + displayHelp(args[1]); + process.exit(0); + } + + // Handle version command + if (commandName === 'version' || commandName === '--version' || commandName === '-v') { + displayVersion(); + process.exit(0); + } + + return { commandName, args: args.slice(1) }; +} + +/** + * Initialize the CLI + */ +async function initCLI(): Promise { + try { + // Register commands + registerCommands(); + + // Initialize authentication + await authManager.initialize(); + + // Parse command-line arguments + const { commandName, args } = parseCommandLineArgs(); + + // Get the command + const command = commandRegistry.get(commandName); + + if (!command) { + console.error(`Unknown command: ${commandName}`); + console.error('Use "claude-code help" to see available commands.'); + process.exit(1); + } + + // Check if command requires authentication + if (command.requiresAuth && !authManager.isAuthenticated()) { + console.error(`Command '${commandName}' requires authentication.`); + console.error('Please log in using the "claude-code login" command first.'); + process.exit(1); + } + + // Initialize AI if required + if (command.requiresAuth) { + await initAI(); + } + + // Execute the command + await executeCommand(commandName, args); + } catch (error) { + handleError(error); + } +} + +/** + * Handle errors + */ +function handleError(error: unknown): void { + const formattedError = formatErrorForDisplay(error); + + console.error(formattedError); + + // Exit with error code + if (error instanceof UserError) { + process.exit(1); + } else { + // Unexpected error, use a different exit code + process.exit(2); + } +} + +// Run the CLI +initCLI().catch(handleError); \ No newline at end of file diff --git a/claude-code/src/codebase/analyzer.ts b/claude-code/src/codebase/analyzer.ts new file mode 100644 index 0000000..637327e --- /dev/null +++ b/claude-code/src/codebase/analyzer.ts @@ -0,0 +1,642 @@ +/** + * Codebase Analyzer + * + * Provides utilities for analyzing and understanding code structure, + * dependencies, and metrics about a codebase. + */ + +import path from 'path'; +import fs from 'fs/promises'; +import { fileExists, readTextFile, findFiles } from '../fs/operations.js'; +import { logger } from '../utils/logger.js'; +import { createUserError } from '../errors/formatter.js'; +import { ErrorCategory } from '../errors/types.js'; + +/** + * File info with language detection and stats + */ +export interface FileInfo { + /** + * File path relative to project root + */ + path: string; + + /** + * File extension + */ + extension: string; + + /** + * Detected language + */ + language: string; + + /** + * File size in bytes + */ + size: number; + + /** + * Line count + */ + lineCount: number; + + /** + * Last modified timestamp + */ + lastModified: Date; +} + +/** + * Code dependency information + */ +export interface DependencyInfo { + /** + * Module/package name + */ + name: string; + + /** + * Type of dependency (import, require, etc.) + */ + type: string; + + /** + * Source file path + */ + source: string; + + /** + * Import path + */ + importPath: string; + + /** + * Whether it's an external dependency + */ + isExternal: boolean; +} + +/** + * Project structure information + */ +export interface ProjectStructure { + /** + * Root directory + */ + root: string; + + /** + * Total file count + */ + totalFiles: number; + + /** + * Files by language + */ + filesByLanguage: Record; + + /** + * Total lines of code + */ + totalLinesOfCode: number; + + /** + * Files organized by directory + */ + directories: Record; + + /** + * Dependencies identified in the project + */ + dependencies: DependencyInfo[]; +} + +/** + * File pattern to ignore during analysis + */ +const DEFAULT_IGNORE_PATTERNS = [ + 'node_modules', + 'dist', + 'build', + '.git', + '.vscode', + '.idea', + 'coverage', + '*.min.js', + '*.bundle.js', + '*.map' +]; + +/** + * Language detection by file extension + */ +const EXTENSION_TO_LANGUAGE: Record = { + ts: 'TypeScript', + tsx: 'TypeScript (React)', + js: 'JavaScript', + jsx: 'JavaScript (React)', + py: 'Python', + java: 'Java', + c: 'C', + cpp: 'C++', + cs: 'C#', + go: 'Go', + rs: 'Rust', + php: 'PHP', + rb: 'Ruby', + swift: 'Swift', + kt: 'Kotlin', + scala: 'Scala', + html: 'HTML', + css: 'CSS', + scss: 'SCSS', + less: 'Less', + json: 'JSON', + md: 'Markdown', + yml: 'YAML', + yaml: 'YAML', + xml: 'XML', + sql: 'SQL', + sh: 'Shell', + bat: 'Batch', + ps1: 'PowerShell' +}; + +/** + * Analyze a codebase + */ +export async function analyzeCodebase( + directory: string, + options: { + ignorePatterns?: string[]; + maxFiles?: number; + maxSizePerFile?: number; // in bytes + } = {} +): Promise { + logger.info(`Analyzing codebase in directory: ${directory}`); + + const { + ignorePatterns = DEFAULT_IGNORE_PATTERNS, + maxFiles = 1000, + maxSizePerFile = 1024 * 1024 // 1MB + } = options; + + // Check if directory exists + if (!await fileExists(directory)) { + throw createUserError(`Directory does not exist: ${directory}`, { + category: ErrorCategory.FILE_NOT_FOUND, + resolution: 'Please provide a valid directory path.' + }); + } + + // Initial project structure + const projectStructure: ProjectStructure = { + root: directory, + totalFiles: 0, + filesByLanguage: {}, + totalLinesOfCode: 0, + directories: {}, + dependencies: [] + }; + + // Pattern for ignore patterns + const ignoreRegexes = ignorePatterns.map(pattern => { + // Convert glob pattern to regex pattern + return new RegExp( + pattern + .replace(/\./g, '\\.') + .replace(/\*/g, '.*') + .replace(/\?/g, '.') + ); + }); + + // Find all files recursively + let allFiles: string[] = []; + try { + allFiles = await findFiles(directory, { + recursive: true, + includeDirectories: false + }); + + // Filter out ignored files + allFiles = allFiles.filter(file => { + const relativePath = path.relative(directory, file); + return !ignoreRegexes.some(regex => regex.test(relativePath)); + }); + + // Cap file count if needed + if (allFiles.length > maxFiles) { + logger.warn(`Codebase has too many files (${allFiles.length}), limiting to ${maxFiles} files`); + allFiles = allFiles.slice(0, maxFiles); + } + } catch (error) { + logger.error('Failed to scan directory for files', error); + throw createUserError(`Failed to scan codebase: ${error instanceof Error ? error.message : String(error)}`, { + cause: error, + category: ErrorCategory.FILE_SYSTEM + }); + } + + // Update total files count + projectStructure.totalFiles = allFiles.length; + + // Analyze each file + let processedFiles = 0; + let skippedFiles = 0; + + for (const file of allFiles) { + try { + // Get file stats + const stats = await fs.stat(file); + + // Skip if file is too large + if (stats.size > maxSizePerFile) { + logger.debug(`Skipping file (too large): ${file} (${formatFileSize(stats.size)})`); + skippedFiles++; + continue; + } + + // Get relative path + const relativePath = path.relative(directory, file); + + // Get directory + const dirPath = path.dirname(relativePath); + if (!projectStructure.directories[dirPath]) { + projectStructure.directories[dirPath] = []; + } + projectStructure.directories[dirPath].push(relativePath); + + // Get file extension and language + const extension = path.extname(file).slice(1).toLowerCase(); + const language = EXTENSION_TO_LANGUAGE[extension] || 'Other'; + + // Update language stats + projectStructure.filesByLanguage[language] = (projectStructure.filesByLanguage[language] || 0) + 1; + + // Read file and count lines + const content = await readTextFile(file); + const lineCount = content.split('\n').length; + projectStructure.totalLinesOfCode += lineCount; + + // Find dependencies + const dependencies = findDependencies(content, relativePath, extension); + projectStructure.dependencies.push(...dependencies); + + // Log progress periodically + processedFiles++; + if (processedFiles % 50 === 0) { + logger.debug(`Analyzed ${processedFiles} files...`); + } + } catch (error) { + logger.warn(`Failed to analyze file: ${file}`, error); + skippedFiles++; + } + } + + // Log results + logger.info(`Codebase analysis complete: ${processedFiles} files analyzed, ${skippedFiles} files skipped`); + logger.debug('Analysis summary', { + totalFiles: projectStructure.totalFiles, + totalLinesOfCode: projectStructure.totalLinesOfCode, + languages: Object.keys(projectStructure.filesByLanguage).length, + directories: Object.keys(projectStructure.directories).length, + dependencies: projectStructure.dependencies.length + }); + + return projectStructure; +} + +/** + * Format file size in a human-readable format + */ +function formatFileSize(bytes: number): string { + if (bytes < 1024) { + return `${bytes} B`; + } else if (bytes < 1024 * 1024) { + return `${(bytes / 1024).toFixed(1)} KB`; + } else if (bytes < 1024 * 1024 * 1024) { + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; + } else { + return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`; + } +} + +/** + * Find dependencies in a file + */ +function findDependencies( + content: string, + filePath: string, + extension: string +): DependencyInfo[] { + const dependencies: DependencyInfo[] = []; + + // Skip binary files or non-code files + if (!content || !isCodeFile(extension)) { + return dependencies; + } + + try { + // JavaScript/TypeScript imports + if (['js', 'jsx', 'ts', 'tsx'].includes(extension)) { + // Find ES module imports + const esImportRegex = /import\s+(?:[\w\s{},*]*\s+from\s+)?['"]([^'"]+)['"]/g; + let match; + while ((match = esImportRegex.exec(content)) !== null) { + const importPath = match[1]; + dependencies.push({ + name: getPackageName(importPath), + type: 'import', + source: filePath, + importPath, + isExternal: isExternalDependency(importPath) + }); + } + + // Find require statements + const requireRegex = /(?:const|let|var)\s+(?:[\w\s{},*]*)\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/g; + while ((match = requireRegex.exec(content)) !== null) { + const importPath = match[1]; + dependencies.push({ + name: getPackageName(importPath), + type: 'require', + source: filePath, + importPath, + isExternal: isExternalDependency(importPath) + }); + } + } + + // Python imports + else if (extension === 'py') { + // Find import statements + const importRegex = /^\s*import\s+(\S+)|\s*from\s+(\S+)\s+import/gm; + let match; + while ((match = importRegex.exec(content)) !== null) { + const importPath = match[1] || match[2]; + if (importPath) { + dependencies.push({ + name: importPath.split('.')[0], + type: 'import', + source: filePath, + importPath, + isExternal: isExternalPythonModule(importPath) + }); + } + } + } + + // Java imports + else if (extension === 'java') { + const importRegex = /^\s*import\s+([^;]+);/gm; + let match; + while ((match = importRegex.exec(content)) !== null) { + const importPath = match[1]; + dependencies.push({ + name: importPath.split('.')[0], + type: 'import', + source: filePath, + importPath, + isExternal: true // Consider all imports as external for Java + }); + } + } + + // Ruby requires + else if (extension === 'rb') { + const requireRegex = /^\s*require\s+['"]([^'"]+)['"]/gm; + let match; + while ((match = requireRegex.exec(content)) !== null) { + const importPath = match[1]; + dependencies.push({ + name: importPath, + type: 'require', + source: filePath, + importPath, + isExternal: true // Consider all requires as external for Ruby + }); + } + } + } catch (error) { + logger.warn(`Failed to parse dependencies in ${filePath}`, error); + } + + return dependencies; +} + +/** + * Check if a file is a code file based on extension + */ +function isCodeFile(extension: string): boolean { + const codeExtensions = [ + 'js', 'jsx', 'ts', 'tsx', 'py', 'java', 'c', 'cpp', 'cs', + 'go', 'rs', 'php', 'rb', 'swift', 'kt', 'scala' + ]; + return codeExtensions.includes(extension); +} + +/** + * Get package name from import path + */ +function getPackageName(importPath: string): string { + // Relative imports don't have a package name + if (importPath.startsWith('.') || importPath.startsWith('/')) { + return 'internal'; + } + + // Handle scoped packages (@org/pkg) + if (importPath.startsWith('@')) { + const parts = importPath.split('/'); + if (parts.length >= 2) { + return `${parts[0]}/${parts[1]}`; + } + } + + // Regular packages (return first path segment) + return importPath.split('/')[0]; +} + +/** + * Check if import is an external dependency + */ +function isExternalDependency(importPath: string): boolean { + // Local imports start with ./ or ../ + return !(importPath.startsWith('.') || importPath.startsWith('/')); +} + +/** + * Check if a Python module is external + */ +function isExternalPythonModule(importPath: string): boolean { + // Common standard library modules in Python + const stdlibModules = [ + 'os', 'sys', 're', 'math', 'datetime', 'time', 'random', + 'json', 'csv', 'collections', 'itertools', 'functools', + 'pathlib', 'shutil', 'glob', 'pickle', 'urllib', 'http', + 'logging', 'argparse', 'unittest', 'subprocess', 'threading', + 'multiprocessing', 'typing', 'enum', 'io', 'tempfile' + ]; + + // Consider it external if it's not in standard library + // and doesn't look like a relative import + const moduleName = importPath.split('.')[0]; + return !stdlibModules.includes(moduleName) && !importPath.startsWith('.'); +} + +/** + * Analyze project dependencies from package files + */ +export async function analyzeProjectDependencies(directory: string): Promise> { + const dependencies: Record = {}; + + try { + // Check for package.json + const packageJsonPath = path.join(directory, 'package.json'); + if (await fileExists(packageJsonPath)) { + const packageJson = JSON.parse(await readTextFile(packageJsonPath)); + + // Add dependencies + if (packageJson.dependencies) { + for (const [name, version] of Object.entries(packageJson.dependencies)) { + dependencies[name] = version as string; + } + } + + // Add dev dependencies + if (packageJson.devDependencies) { + for (const [name, version] of Object.entries(packageJson.devDependencies)) { + dependencies[`${name} (dev)`] = version as string; + } + } + } + + // Check for Python requirements.txt + const requirementsPath = path.join(directory, 'requirements.txt'); + if (await fileExists(requirementsPath)) { + const requirements = await readTextFile(requirementsPath); + + requirements.split('\n').forEach(line => { + line = line.trim(); + if (line && !line.startsWith('#')) { + const [name, version] = line.split('=='); + if (name) { + dependencies[name.trim()] = version ? version.trim() : 'latest'; + } + } + }); + } + + // Check for Gemfile for Ruby + const gemfilePath = path.join(directory, 'Gemfile'); + if (await fileExists(gemfilePath)) { + const gemfile = await readTextFile(gemfilePath); + + const gemRegex = /^\s*gem\s+['"]([^'"]+)['"]\s*(?:,\s*['"]([^'"]+)['"]\s*)?/gm; + let match; + while ((match = gemRegex.exec(gemfile)) !== null) { + const name = match[1]; + const version = match[2] || 'latest'; + if (name) { + dependencies[name] = version; + } + } + } + } catch (error) { + logger.warn('Failed to analyze project dependencies', error); + } + + return dependencies; +} + +/** + * Find files by content search + */ +export async function findFilesByContent( + directory: string, + searchTerm: string, + options: { + caseSensitive?: boolean; + fileExtensions?: string[]; + maxResults?: number; + ignorePatterns?: string[]; + } = {} +): Promise> { + const { + caseSensitive = false, + fileExtensions = [], + maxResults = 100, + ignorePatterns = DEFAULT_IGNORE_PATTERNS + } = options; + + const results: Array<{ path: string; line: number; content: string }> = []; + const flags = caseSensitive ? 'g' : 'gi'; + const regex = new RegExp(searchTerm, flags); + + const ignoreRegexes = ignorePatterns.map(pattern => { + return new RegExp( + pattern + .replace(/\./g, '\\.') + .replace(/\*/g, '.*') + .replace(/\?/g, '.') + ); + }); + + // Find all files (optionally filtered by extension) + const allFiles = await findFiles(directory, { recursive: true }); + + // Filter by file extension and ignore patterns + const filteredFiles = allFiles.filter(file => { + const relativePath = path.relative(directory, file); + + // Check if file should be ignored + if (ignoreRegexes.some(regex => regex.test(relativePath))) { + return false; + } + + // Filter by extension if specified + if (fileExtensions.length > 0) { + const ext = path.extname(file).slice(1).toLowerCase(); + return fileExtensions.includes(ext); + } + + return true; + }); + + // Search through files for content match + for (const file of filteredFiles) { + if (results.length >= maxResults) { + break; + } + + try { + const content = await readTextFile(file); + const lines = content.split('\n'); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (regex.test(line)) { + results.push({ + path: path.relative(directory, file), + line: i + 1, // 1-indexed line number + content: line.trim() + }); + + if (results.length >= maxResults) { + break; + } + } + } + } catch (error) { + logger.debug(`Failed to search in file: ${file}`, error); + } + } + + return results; +} + +export default { + analyzeCodebase, + analyzeProjectDependencies, + findFilesByContent +}; \ No newline at end of file diff --git a/claude-code/src/codebase/index.ts b/claude-code/src/codebase/index.ts new file mode 100644 index 0000000..fed3aef --- /dev/null +++ b/claude-code/src/codebase/index.ts @@ -0,0 +1,166 @@ +/** + * Codebase Analysis Module + * + * This module provides utilities for analyzing and understanding code structure, + * dependencies, and metrics about a codebase. + */ + +import { + analyzeCodebase, + FileInfo, + DependencyInfo, + ProjectStructure, + analyzeProjectDependencies, + findFilesByContent +} from './analyzer.js'; + +export { + analyzeCodebase, + FileInfo, + DependencyInfo, + ProjectStructure, + analyzeProjectDependencies, + findFilesByContent +}; + +/** + * Analyze a codebase and return a summary of its structure + * + * @param directoryPath - Path to the directory to analyze + * @param options - Analysis options + * @returns Promise resolving to the project structure + */ +export async function analyzeProject( + directoryPath: string, + options: { + ignorePatterns?: string[]; + maxFiles?: number; + maxSizePerFile?: number; + } = {} +): Promise { + return analyzeCodebase(directoryPath, options); +} + +/** + * Background analysis state + */ +interface BackgroundAnalysisState { + running: boolean; + interval: NodeJS.Timeout | null; + lastResults: ProjectStructure | null; + workingDirectory: string | null; +} + +// Background analysis state +const backgroundAnalysis: BackgroundAnalysisState = { + running: false, + interval: null, + lastResults: null, + workingDirectory: null +}; + +/** + * Initialize the codebase analysis subsystem + * + * @param config Configuration options for the codebase analysis + * @returns The initialized codebase analysis system + */ +export function initCodebaseAnalysis(config: any = {}) { + const analysisConfig = config.codebase || {}; + + return { + /** + * Analyze the current working directory + */ + analyzeCurrentDirectory: async (options = {}) => { + const cwd = process.cwd(); + return analyzeCodebase(cwd, { + ...analysisConfig, + ...options + }); + }, + + /** + * Analyze a specific directory + */ + analyzeDirectory: async (directoryPath: string, options = {}) => { + return analyzeCodebase(directoryPath, { + ...analysisConfig, + ...options + }); + }, + + /** + * Find files by content pattern + */ + findFiles: async (pattern: string, directoryPath: string = process.cwd(), options = {}) => { + return findFilesByContent(pattern, directoryPath, options); + }, + + /** + * Analyze project dependencies + */ + analyzeDependencies: async (directoryPath: string = process.cwd()) => { + return analyzeProjectDependencies(directoryPath); + }, + + /** + * Start background analysis of the current directory + */ + startBackgroundAnalysis: (interval = 5 * 60 * 1000) => { // Default: 5 minutes + if (backgroundAnalysis.running) { + return; + } + + backgroundAnalysis.running = true; + backgroundAnalysis.workingDirectory = process.cwd(); + + // Perform initial analysis + analyzeCodebase(backgroundAnalysis.workingDirectory, analysisConfig) + .then(results => { + backgroundAnalysis.lastResults = results; + }) + .catch(err => { + console.error('Background analysis error:', err); + }); + + // Set up interval for periodic re-analysis + backgroundAnalysis.interval = setInterval(() => { + if (!backgroundAnalysis.running || !backgroundAnalysis.workingDirectory) { + return; + } + + analyzeCodebase(backgroundAnalysis.workingDirectory, analysisConfig) + .then(results => { + backgroundAnalysis.lastResults = results; + }) + .catch(err => { + console.error('Background analysis error:', err); + }); + }, interval); + }, + + /** + * Stop background analysis + */ + stopBackgroundAnalysis: () => { + if (!backgroundAnalysis.running) { + return; + } + + if (backgroundAnalysis.interval) { + clearInterval(backgroundAnalysis.interval); + backgroundAnalysis.interval = null; + } + + backgroundAnalysis.running = false; + }, + + /** + * Get the latest background analysis results + */ + getBackgroundAnalysisResults: () => { + return backgroundAnalysis.lastResults; + } + }; +} \ No newline at end of file diff --git a/claude-code/src/commands/index.ts b/claude-code/src/commands/index.ts new file mode 100644 index 0000000..505a055 --- /dev/null +++ b/claude-code/src/commands/index.ts @@ -0,0 +1,659 @@ +/** + * Command System + * + * Provides a framework for registering, managing, and executing + * CLI commands. Handles argument parsing, validation, and help text. + */ + +import { createUserError } from '../errors/formatter.js'; +import { ErrorCategory } from '../errors/types.js'; +import { logger } from '../utils/logger.js'; +import { isNonEmptyString } from '../utils/validation.js'; +import { registerCommands } from './register.js'; + +/** + * Command argument types + */ +export enum ArgType { + STRING = 'string', + NUMBER = 'number', + BOOLEAN = 'boolean', + ARRAY = 'array' +} + +/** + * Command argument definition + */ +export interface CommandArgDef { + /** + * Argument name + */ + name: string; + + /** + * Argument description + */ + description: string; + + /** + * Argument type + */ + type: ArgType; + + /** + * Whether the argument is required + */ + required?: boolean; + + /** + * Default value if not provided + */ + default?: any; + + /** + * Valid values (for enum-like arguments) + */ + choices?: string[]; + + /** + * For positional args, the position (0-based) + */ + position?: number; + + /** + * Short flag (e.g., -v for --verbose) + */ + shortFlag?: string; + + /** + * Whether to hide from help + */ + hidden?: boolean; +} + +/** + * Command definition + */ +export interface CommandDef { + /** + * Command name + */ + name: string; + + /** + * Command description + */ + description: string; + + /** + * Command usage examples + */ + examples?: string[]; + + /** + * Command arguments + */ + args?: CommandArgDef[]; + + /** + * Command handler function + */ + handler: (args: Record) => Promise; + + /** + * Command aliases + */ + aliases?: string[]; + + /** + * Command category for grouping in help + */ + category?: string; + + /** + * Whether the command requires authentication + */ + requiresAuth?: boolean; + + /** + * Whether the command can be used in interactive mode + */ + interactive?: boolean; + + /** + * Whether to hide from help + */ + hidden?: boolean; +} + +/** + * Command registry + */ +class CommandRegistry { + private commands: Map = new Map(); + private aliases: Map = new Map(); + + /** + * Register a command + */ + register(command: CommandDef): void { + // Validate command definition + if (!isNonEmptyString(command.name)) { + throw new Error('Command name is required'); + } + + if (!isNonEmptyString(command.description)) { + throw new Error(`Command ${command.name} requires a description`); + } + + if (!command.handler || typeof command.handler !== 'function') { + throw new Error(`Command ${command.name} requires a handler function`); + } + + // Check for duplicate command names + if (this.commands.has(command.name) || this.aliases.has(command.name)) { + throw new Error(`Command or alias '${command.name}' is already registered`); + } + + // Register the command + this.commands.set(command.name, command); + logger.debug(`Registered command: ${command.name}`); + + // Register aliases + if (command.aliases && Array.isArray(command.aliases)) { + for (const alias of command.aliases) { + if (this.commands.has(alias) || this.aliases.has(alias)) { + logger.warn(`Skipping duplicate alias '${alias}' for command '${command.name}'`); + continue; + } + + this.aliases.set(alias, command.name); + logger.debug(`Registered alias '${alias}' for command '${command.name}'`); + } + } + } + + /** + * Get a command by name or alias + */ + get(nameOrAlias: string): CommandDef | undefined { + // Check if it's a direct command name + if (this.commands.has(nameOrAlias)) { + return this.commands.get(nameOrAlias); + } + + // Check if it's an alias + const commandName = this.aliases.get(nameOrAlias); + if (commandName) { + return this.commands.get(commandName); + } + + return undefined; + } + + /** + * List all commands + */ + list(options: { includeHidden?: boolean } = {}): CommandDef[] { + const { includeHidden = false } = options; + + return Array.from(this.commands.values()) + .filter(cmd => includeHidden || !cmd.hidden); + } + + /** + * Check if a command exists + */ + exists(nameOrAlias: string): boolean { + return this.commands.has(nameOrAlias) || this.aliases.has(nameOrAlias); + } + + /** + * Get command categories + */ + getCategories(): string[] { + const categories = new Set(); + + for (const command of this.commands.values()) { + if (command.category) { + categories.add(command.category); + } + } + + return Array.from(categories).sort(); + } + + /** + * Get commands by category + */ + getByCategory(category: string, options: { includeHidden?: boolean } = {}): CommandDef[] { + const { includeHidden = false } = options; + + return Array.from(this.commands.values()) + .filter(cmd => (includeHidden || !cmd.hidden) && cmd.category === category); + } +} + +// Create a singleton command registry +export const commandRegistry = new CommandRegistry(); + +/** + * Parse command-line arguments + */ +export function parseArgs( + args: string[], + command: CommandDef +): Record { + const result: Record = {}; + const positionalArgs: string[] = []; + const flagArgs: Map = new Map(); + const errors: string[] = []; + + // Initialize defaults + if (command.args) { + for (const arg of command.args) { + if (arg.default !== undefined) { + result[arg.name] = arg.default; + } + + // Map flags to arg definitions + if (arg.position === undefined) { + // Flag argument (--name or -n) + flagArgs.set(`--${arg.name}`, arg); + if (arg.shortFlag) { + flagArgs.set(`-${arg.shortFlag}`, arg); + } + } + } + } + + // Parse args + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + if (arg.startsWith('--') || (arg.startsWith('-') && arg.length === 2)) { + // Flag argument + const argDef = flagArgs.get(arg); + + if (!argDef) { + errors.push(`Unknown argument: ${arg}`); + continue; + } + + if (argDef.type === ArgType.BOOLEAN) { + // Boolean flags don't need a value + result[argDef.name] = true; + } else { + // Other flags need a value + if (i + 1 >= args.length || args[i + 1].startsWith('-')) { + errors.push(`Missing value for argument: ${arg}`); + continue; + } + + const value = args[++i]; + + // Convert value based on type + result[argDef.name] = convertArgValue(value, argDef); + + // Validate choices + if (argDef.choices && !argDef.choices.includes(String(result[argDef.name]))) { + errors.push(`Invalid value for ${argDef.name}: ${value}. Valid values are: ${argDef.choices.join(', ')}`); + } + } + } else { + // Positional argument + positionalArgs.push(arg); + } + } + + // Process positional args + if (command.args) { + const positionalArgDefs = command.args + .filter(arg => arg.position !== undefined) + .sort((a, b) => (a.position || 0) - (b.position || 0)); + + for (let i = 0; i < positionalArgDefs.length; i++) { + const argDef = positionalArgDefs[i]; + + if (i < positionalArgs.length) { + // Value provided + result[argDef.name] = convertArgValue(positionalArgs[i], argDef); + + // Validate choices + if (argDef.choices && !argDef.choices.includes(String(result[argDef.name]))) { + errors.push(`Invalid value for ${argDef.name}: ${positionalArgs[i]}. Valid values are: ${argDef.choices.join(', ')}`); + } + } else if (argDef.required) { + // Required value not provided + errors.push(`Missing required argument: ${argDef.name}`); + } + } + } + + // Check for missing required flag args + if (command.args) { + for (const arg of command.args) { + if (arg.required && result[arg.name] === undefined) { + errors.push(`Missing required argument: ${arg.name}`); + } + } + } + + // Throw if there are errors + if (errors.length > 0) { + throw createUserError(`Invalid arguments: ${errors.join('; ')}`, { + category: ErrorCategory.VALIDATION, + resolution: `Use 'claude-code help ${command.name}' to see usage information.` + }); + } + + return result; +} + +/** + * Convert an argument value based on its type + */ +function convertArgValue(value: string, argDef: CommandArgDef): any { + switch (argDef.type) { + case ArgType.NUMBER: + const num = Number(value); + if (isNaN(num)) { + throw createUserError(`Invalid number: ${value}`, { + category: ErrorCategory.VALIDATION + }); + } + return num; + + case ArgType.BOOLEAN: + return value.toLowerCase() === 'true'; + + case ArgType.ARRAY: + return value.split(',').map(v => v.trim()); + + case ArgType.STRING: + default: + return value; + } +} + +/** + * Generate help text for a command + */ +export function generateCommandHelp(command: CommandDef): string { + let help = `\n${command.name} - ${command.description}\n\n`; + + // Usage + help += 'Usage:\n'; + help += ` claude-code ${command.name}`; + + // Add positional args to usage + if (command.args) { + const positionalArgs = command.args + .filter(arg => arg.position !== undefined) + .sort((a, b) => (a.position || 0) - (b.position || 0)); + + for (const arg of positionalArgs) { + const argDisplay = arg.required + ? `<${arg.name}>` + : `[${arg.name}]`; + + help += ` ${argDisplay}`; + } + } + + // Add flag options to usage + if (command.args) { + const flagArgs = command.args.filter(arg => arg.position === undefined); + + if (flagArgs.length > 0) { + help += ' [options]'; + } + } + + help += '\n\n'; + + // Arguments section + if (command.args && command.args.length > 0) { + // List positional args + const positionalArgs = command.args + .filter(arg => arg.position !== undefined && !arg.hidden) + .sort((a, b) => (a.position || 0) - (b.position || 0)); + + if (positionalArgs.length > 0) { + help += 'Arguments:\n'; + + for (const arg of positionalArgs) { + help += ` ${arg.name.padEnd(20)} ${arg.description}`; + + if (arg.default !== undefined) { + help += ` (default: ${arg.default})`; + } + + if (arg.choices) { + help += ` (choices: ${arg.choices.join(', ')})`; + } + + help += '\n'; + } + + help += '\n'; + } + + // List flag options + const flagArgs = command.args.filter(arg => + arg.position === undefined && !arg.hidden + ); + + if (flagArgs.length > 0) { + help += 'Options:\n'; + + for (const arg of flagArgs) { + let flag = `--${arg.name}`; + + if (arg.shortFlag) { + flag = `-${arg.shortFlag}, ${flag}`; + } + + help += ` ${flag.padEnd(20)} ${arg.description}`; + + if (arg.default !== undefined) { + help += ` (default: ${arg.default})`; + } + + if (arg.choices) { + help += ` (choices: ${arg.choices.join(', ')})`; + } + + help += '\n'; + } + + help += '\n'; + } + } + + // Examples + if (command.examples && command.examples.length > 0) { + help += 'Examples:\n'; + + for (const example of command.examples) { + help += ` $ claude-code ${example}\n`; + } + + help += '\n'; + } + + // Aliases + if (command.aliases && command.aliases.length > 0) { + help += `Aliases: ${command.aliases.join(', ')}\n\n`; + } + + return help; +} + +/** + * Execute a command + */ +export async function executeCommand( + commandName: string, + args: string[] +): Promise { + const command = commandRegistry.get(commandName); + + if (!command) { + throw createUserError(`Unknown command: ${commandName}`, { + category: ErrorCategory.COMMAND, + resolution: 'Use "claude-code help" to see available commands.' + }); + } + + try { + // Parse arguments + const parsedArgs = parseArgs(args, command); + + // Log command execution + logger.debug(`Executing command: ${command.name}`, { args: parsedArgs }); + + // Execute the command + return await command.handler(parsedArgs); + } catch (error) { + logger.error(`Command ${command.name} failed:`, error); + throw error; + } +} + +/** + * Initialize the command processor + * + * @param config Configuration options + * @param dependencies Application dependencies needed by commands + */ +export async function initCommandProcessor( + config: any, + dependencies: { + terminal: any; + auth: any; + ai: any; + codebase: any; + fileOps: any; + execution: any; + errors: any; + } +): Promise { + logger.info('Initializing command processor'); + + try { + // Register all commands + registerCommands(); + + // Return the command processor interface + return { + /** + * Execute a command with the given arguments + */ + executeCommand: async (commandName: string, args: string[]): Promise => { + return executeCommand(commandName, args); + }, + + /** + * Start the interactive command loop + */ + startCommandLoop: async (): Promise => { + const { terminal } = dependencies; + let running = true; + + // Command loop + while (running) { + try { + // Get command input from user + const input = await terminal.prompt({ + type: 'input', + name: 'command', + message: 'claude-code>', + prefix: '', + }); + + if (!input.command || input.command.trim() === '') { + continue; + } + + // Handle special exit commands + if (['exit', 'quit', 'q', '.exit'].includes(input.command.toLowerCase())) { + running = false; + continue; + } + + // Parse input into command and args + const parts = input.command.trim().split(/\s+/); + const commandName = parts[0]; + const commandArgs = parts.slice(1); + + // Check if command exists + if (!commandRegistry.exists(commandName)) { + terminal.error(`Unknown command: ${commandName}`); + terminal.info('Type "help" to see available commands.'); + continue; + } + + // Execute the command + await executeCommand(commandName, commandArgs); + } catch (error) { + dependencies.errors.handleError(error); + } + } + }, + + /** + * Register a new command + */ + registerCommand: (command: CommandDef): void => { + commandRegistry.register(command); + }, + + /** + * Get a command by name or alias + */ + getCommand: (nameOrAlias: string): CommandDef | undefined => { + return commandRegistry.get(nameOrAlias); + }, + + /** + * List all registered commands + */ + listCommands: (options: { includeHidden?: boolean } = {}): CommandDef[] => { + return commandRegistry.list(options); + }, + + /** + * Get available command categories + */ + getCategories: (): string[] => { + return commandRegistry.getCategories(); + }, + + /** + * Get commands by category + */ + getCommandsByCategory: (category: string, options: { includeHidden?: boolean } = {}): CommandDef[] => { + return commandRegistry.getByCategory(category, options); + }, + + /** + * Generate help text for a command + */ + generateCommandHelp: (command: CommandDef): string => { + return generateCommandHelp(command); + } + }; + } catch (error) { + logger.error('Failed to initialize command processor', error); + throw error; + } +} + +// Export main symbols +export default { + commandRegistry, + parseArgs, + executeCommand, + generateCommandHelp +}; \ No newline at end of file diff --git a/claude-code/src/commands/register.ts b/claude-code/src/commands/register.ts new file mode 100644 index 0000000..e060a86 --- /dev/null +++ b/claude-code/src/commands/register.ts @@ -0,0 +1,1632 @@ +/** + * Command Registration + * + * Registers all available CLI commands with the command registry. + */ + +import { commandRegistry, ArgType, CommandDef } from './index.js'; +import { logger } from '../utils/logger.js'; +import { getAIClient, initAI } from '../ai/index.js'; +import { fileExists, readTextFile } from '../fs/operations.js'; +import { isNonEmptyString } from '../utils/validation.js'; +import { formatErrorForDisplay } from '../errors/formatter.js'; +import { authManager } from '../auth/index.js'; +import { createUserError } from '../errors/formatter.js'; +import { ErrorCategory } from '../errors/types.js'; + +/** + * Register all commands + */ +export function registerCommands(): void { + logger.debug('Registering commands'); + + // Register core commands + registerLoginCommand(); + registerLogoutCommand(); + registerAskCommand(); + registerExplainCommand(); + registerRefactorCommand(); + registerFixCommand(); + registerGenerateCommand(); + registerConfigCommand(); + registerBugCommand(); + registerFeedbackCommand(); + registerRunCommand(); + registerSearchCommand(); + registerThemeCommand(); + registerVerbosityCommand(); + registerEditCommand(); + registerGitCommand(); + registerExitCommand(); + registerQuitCommand(); + registerClearCommand(); + registerResetCommand(); + registerHistoryCommand(); + registerCommandsCommand(); + registerHelpCommand(); + + logger.info('Commands registered successfully'); +} + +/** + * Register login command + */ +function registerLoginCommand(): void { + const command: CommandDef = { + name: 'login', + description: 'Log in to Claude AI', + category: 'Auth', + handler: async (args) => { + try { + const { 'api-key': apiKey, oauth } = args; + + console.log('Authenticating with Claude...'); + + if (apiKey) { + // Use API key authentication + const authResult = await authManager.authenticateWithApiKey(apiKey); + if (authResult.success) { + console.log('Successfully logged in with API key.'); + + // Display token expiration if available + if (authResult.token?.expiresAt) { + const expirationDate = new Date(authResult.token.expiresAt * 1000); + console.log(`Token expires on: ${expirationDate.toLocaleString()}`); + } + } else { + console.error(`Authentication failed: ${authResult.error || 'Unknown error'}`); + } + } else if (oauth) { + // Use OAuth authentication + const authResult = await authManager.authenticateWithOAuth(); + if (authResult.success) { + console.log('Successfully logged in with OAuth.'); + + // Display token expiration if available + if (authResult.token?.expiresAt) { + const expirationDate = new Date(authResult.token.expiresAt * 1000); + console.log(`Token expires on: ${expirationDate.toLocaleString()}`); + } + } else { + console.error(`Authentication failed: ${authResult.error || 'Unknown error'}`); + } + } else { + // Determine method based on available environment variables + const apiKeyFromEnv = process.env.ANTHROPIC_API_KEY; + + if (apiKeyFromEnv) { + const authResult = await authManager.authenticateWithApiKey(apiKeyFromEnv); + if (authResult.success) { + console.log('Successfully logged in with API key from environment.'); + + // Display token expiration if available + if (authResult.token?.expiresAt) { + const expirationDate = new Date(authResult.token.expiresAt * 1000); + console.log(`Token expires on: ${expirationDate.toLocaleString()}`); + } + } else { + console.error(`Authentication failed: ${authResult.error || 'Unknown error'}`); + } + } else { + // Default to OAuth if no API key is available + console.log('No API key found. Proceeding with OAuth authentication...'); + const authResult = await authManager.authenticateWithOAuth(); + if (authResult.success) { + console.log('Successfully logged in with OAuth.'); + + // Display token expiration if available + if (authResult.token?.expiresAt) { + const expirationDate = new Date(authResult.token.expiresAt * 1000); + console.log(`Token expires on: ${expirationDate.toLocaleString()}`); + } + } else { + console.error(`Authentication failed: ${authResult.error || 'Unknown error'}`); + } + } + } + } catch (error) { + console.error('Error during authentication:', formatErrorForDisplay(error)); + } + }, + args: [ + { + name: 'api-key', + description: 'API key for Claude AI', + type: ArgType.STRING, + shortFlag: 'k' + }, + { + name: 'oauth', + description: 'Use OAuth authentication', + type: ArgType.BOOLEAN, + shortFlag: 'o' + } + ], + examples: [ + 'login', + 'login --api-key your-api-key' + ] + }; + + commandRegistry.register(command); +} + +/** + * Register logout command + */ +function registerLogoutCommand(): void { + const command: CommandDef = { + name: 'logout', + description: 'Log out and clear stored credentials', + category: 'Auth', + handler: async () => { + try { + console.log('Logging out and clearing credentials...'); + + // Call the auth manager's logout function + await authManager.logout(); + + console.log('Successfully logged out. All credentials have been cleared.'); + } catch (error) { + console.error('Error during logout:', formatErrorForDisplay(error)); + } + }, + examples: [ + 'logout' + ] + }; + + commandRegistry.register(command); +} + +/** + * Register ask command + */ +function registerAskCommand(): void { + const command: CommandDef = { + name: 'ask', + description: 'Ask Claude a question about code or programming', + category: 'Assistance', + handler: async (args) => { + try { + const { question } = args; + + if (!isNonEmptyString(question)) { + console.error('Please provide a question to ask Claude.'); + return; + } + + console.log('Asking Claude...\n'); + + // Get AI client and send question + const aiClient = getAIClient(); + const result = await aiClient.complete(question); + + // Extract and print the response + const responseText = result.content[0]?.text || 'No response received'; + console.log(responseText); + } catch (error) { + console.error('Error asking Claude:', formatErrorForDisplay(error)); + } + }, + args: [ + { + name: 'question', + description: 'Question to ask Claude', + type: ArgType.STRING, + position: 0, + required: true + }, + { + name: 'context', + description: 'Provide additional context files', + type: ArgType.STRING, + shortFlag: 'c' + }, + { + name: 'model', + description: 'Specific Claude model to use', + type: ArgType.STRING, + shortFlag: 'm', + choices: ['claude-3-opus', 'claude-3-sonnet', 'claude-3-haiku'] + } + ], + examples: [ + 'ask "How do I implement a binary search tree in TypeScript?"', + 'ask "What\'s wrong with this code?" --context ./path/to/file.js' + ], + requiresAuth: true + }; + + commandRegistry.register(command); +} + +/** + * Register explain command + */ +function registerExplainCommand(): void { + const command: CommandDef = { + name: 'explain', + description: 'Explain a code file or snippet', + category: 'Assistance', + handler: async (args) => { + try { + const { file } = args; + + // Validate file path + if (!isNonEmptyString(file)) { + console.error('Please provide a file path to explain.'); + return; + } + + // Check if file exists + if (!await fileExists(file)) { + console.error(`File not found: ${file}`); + return; + } + + console.log(`Explaining ${file}...\n`); + + // Read the file + const fileContent = await readTextFile(file); + + // Construct the prompt + const prompt = `Please explain this code:\n\n\`\`\`\n${fileContent}\n\`\`\``; + + // Get AI client and send request + const aiClient = getAIClient(); + const result = await aiClient.complete(prompt); + + // Extract and print the response + const responseText = result.content[0]?.text || 'No explanation received'; + console.log(responseText); + } catch (error) { + console.error('Error explaining code:', formatErrorForDisplay(error)); + } + }, + args: [ + { + name: 'file', + description: 'File to explain', + type: ArgType.STRING, + position: 0, + required: true + }, + { + name: 'detail', + description: 'Level of detail', + type: ArgType.STRING, + shortFlag: 'd', + choices: ['basic', 'intermediate', 'detailed'], + default: 'intermediate' + } + ], + examples: [ + 'explain path/to/file.js', + 'explain path/to/file.py --detail detailed' + ], + requiresAuth: true + }; + + commandRegistry.register(command); +} + +/** + * Register refactor command + */ +function registerRefactorCommand(): void { + const command: CommandDef = { + name: 'refactor', + description: 'Refactor code for better readability, performance, or structure', + category: 'Code Generation', + handler: async (args) => { + try { + const { file, focus } = args; + + // Validate file path + if (!isNonEmptyString(file)) { + console.error('Please provide a file path to refactor.'); + return; + } + + // Check if file exists + if (!await fileExists(file)) { + console.error(`File not found: ${file}`); + return; + } + + console.log(`Refactoring ${file} with focus on ${focus}...\n`); + + // Read the file + const fileContent = await readTextFile(file); + + // Construct the prompt + const prompt = `Please refactor this code to improve ${focus}:\n\n\`\`\`\n${fileContent}\n\`\`\``; + + // Get AI client and send request + const aiClient = getAIClient(); + const result = await aiClient.complete(prompt); + + // Extract and print the response + const responseText = result.content[0]?.text || 'No refactored code received'; + console.log(responseText); + } catch (error) { + console.error('Error refactoring code:', formatErrorForDisplay(error)); + } + }, + args: [ + { + name: 'file', + description: 'File to refactor', + type: ArgType.STRING, + position: 0, + required: true + }, + { + name: 'focus', + description: 'Focus of the refactoring', + type: ArgType.STRING, + shortFlag: 'f', + choices: ['readability', 'performance', 'simplicity', 'maintainability'], + default: 'readability' + }, + { + name: 'output', + description: 'Output file path (defaults to stdout)', + type: ArgType.STRING, + shortFlag: 'o' + } + ], + examples: [ + 'refactor path/to/file.js', + 'refactor path/to/file.py --focus performance', + 'refactor path/to/file.ts --output path/to/refactored.ts' + ], + requiresAuth: true + }; + + commandRegistry.register(command); +} + +/** + * Register fix command + */ +function registerFixCommand(): void { + const command: CommandDef = { + name: 'fix', + description: 'Fix bugs or issues in code', + category: 'Assistance', + handler: async (args) => { + try { + const { file, issue } = args; + + // Validate file path + if (!isNonEmptyString(file)) { + console.error('Please provide a file path to fix.'); + return; + } + + // Check if file exists + if (!await fileExists(file)) { + console.error(`File not found: ${file}`); + return; + } + + console.log(`Fixing ${file}...\n`); + + // Read the file + const fileContent = await readTextFile(file); + + // Construct the prompt + let prompt = `Please fix this code:\n\n\`\`\`\n${fileContent}\n\`\`\``; + + if (isNonEmptyString(issue)) { + prompt += `\n\nThe specific issue is: ${issue}`; + } + + // Get AI client and send request + const aiClient = getAIClient(); + const result = await aiClient.complete(prompt); + + // Extract and print the response + const responseText = result.content[0]?.text || 'No fixed code received'; + console.log(responseText); + } catch (error) { + console.error('Error fixing code:', formatErrorForDisplay(error)); + } + }, + args: [ + { + name: 'file', + description: 'File to fix', + type: ArgType.STRING, + position: 0, + required: true + }, + { + name: 'issue', + description: 'Description of the issue to fix', + type: ArgType.STRING, + shortFlag: 'i' + }, + { + name: 'output', + description: 'Output file path (defaults to stdout)', + type: ArgType.STRING, + shortFlag: 'o' + } + ], + examples: [ + 'fix path/to/file.js', + 'fix path/to/file.py --issue "Infinite loop in the sort function"', + 'fix path/to/file.ts --output path/to/fixed.ts' + ], + requiresAuth: true + }; + + commandRegistry.register(command); +} + +/** + * Register generate command + */ +function registerGenerateCommand(): void { + const command: CommandDef = { + name: 'generate', + description: 'Generate code based on a prompt', + category: 'Code Generation', + handler: async (args) => { + try { + const { prompt, language } = args; + + // Validate prompt + if (!isNonEmptyString(prompt)) { + console.error('Please provide a prompt for code generation.'); + return; + } + + console.log(`Generating ${language} code...\n`); + + // Construct the prompt + const fullPrompt = `Generate ${language} code that ${prompt}. Please provide only the code without explanations.`; + + // Get AI client and send request + const aiClient = getAIClient(); + const result = await aiClient.complete(fullPrompt); + + // Extract and print the response + const responseText = result.content[0]?.text || 'No code generated'; + console.log(responseText); + } catch (error) { + console.error('Error generating code:', formatErrorForDisplay(error)); + } + }, + args: [ + { + name: 'prompt', + description: 'Description of the code to generate', + type: ArgType.STRING, + position: 0, + required: true + }, + { + name: 'language', + description: 'Programming language for the generated code', + type: ArgType.STRING, + shortFlag: 'l', + default: 'JavaScript' + }, + { + name: 'output', + description: 'Output file path (defaults to stdout)', + type: ArgType.STRING, + shortFlag: 'o' + } + ], + examples: [ + 'generate "a function that sorts an array using quick sort"', + 'generate "a REST API server with Express" --language TypeScript', + 'generate "a binary search tree implementation" --output bst.js' + ], + requiresAuth: true + }; + + commandRegistry.register(command); +} + +/** + * Register config command + */ +function registerConfigCommand(): void { + logger.debug('Registering config command'); + + const command = { + name: 'config', + description: 'View or edit configuration settings', + category: 'system', + async handler({ key, value }: { key?: string; value?: string }) { + logger.info('Executing config command'); + + try { + const configModule = await import('../config/index.js'); + // Load the current configuration + const currentConfig = await configModule.loadConfig(); + + if (!key) { + // Display the current configuration + logger.info('Current configuration:'); + console.log(JSON.stringify(currentConfig, null, 2)); + return; + } + + // Handle nested keys like "api.baseUrl" + const keyPath = key.split('.'); + let configSection: any = currentConfig; + + // Navigate to the nested config section + for (let i = 0; i < keyPath.length - 1; i++) { + configSection = configSection[keyPath[i]]; + if (!configSection) { + throw new Error(`Configuration key '${key}' not found`); + } + } + + const finalKey = keyPath[keyPath.length - 1]; + + if (value === undefined) { + // Get the value + const keyValue = configSection[finalKey]; + if (keyValue === undefined) { + throw new Error(`Configuration key '${key}' not found`); + } + logger.info(`${key}: ${JSON.stringify(keyValue)}`); + } else { + // Set the value + // Parse the value if needed (convert strings to numbers/booleans) + let parsedValue: any = value; + if (value.toLowerCase() === 'true') parsedValue = true; + else if (value.toLowerCase() === 'false') parsedValue = false; + else if (!isNaN(Number(value))) parsedValue = Number(value); + + // Update the config in memory + configSection[finalKey] = parsedValue; + + // Save the updated config to file + // Since there's no direct saveConfig function, we'd need to implement + // this part separately to write to a config file + logger.info(`Configuration updated in memory: ${key} = ${JSON.stringify(parsedValue)}`); + logger.warn('Note: Configuration changes are only temporary for this session'); + // In a real implementation, we would save to the config file + } + } catch (error) { + logger.error(`Error executing config command: ${error instanceof Error ? error.message : 'Unknown error'}`); + throw error; + } + }, + args: [ + { + name: 'key', + description: 'Configuration key (e.g., "api.baseUrl")', + type: ArgType.STRING, + required: false + }, + { + name: 'value', + description: 'New value to set', + type: ArgType.STRING, + required: false + } + ], + examples: [ + 'config', + 'config api.baseUrl', + 'config api.baseUrl https://api.anthropic.com', + 'config telemetry.enabled false' + ] + }; + + commandRegistry.register(command); +} + +/** + * Register bug command + */ +function registerBugCommand(): void { + logger.debug('Registering bug command'); + + const command = { + name: 'bug', + description: 'Report a bug or issue with Claude Code', + category: 'system', + async handler(args: Record): Promise { + logger.info('Executing bug command'); + + const description = args.description; + if (!isNonEmptyString(description)) { + throw createUserError('Bug description is required', { + category: ErrorCategory.VALIDATION, + resolution: 'Please provide a description of the bug you encountered' + }); + } + + try { + // In a real implementation, this would send the bug report to a server + logger.info('Submitting bug report...'); + + // Get system information + const os = await import('os'); + const systemInfo = { + platform: os.platform(), + release: os.release(), + nodeVersion: process.version, + appVersion: '0.2.29', // This would come from package.json in a real implementation + timestamp: new Date().toISOString() + }; + + // Get current telemetry client + const telemetryModule = await import('../telemetry/index.js'); + const telemetryManager = await telemetryModule.initTelemetry(); + + telemetryManager.trackEvent('BUG_REPORT', { + description, + ...systemInfo + }); + + logger.info('Bug report submitted successfully'); + console.log('Thank you for your bug report. Our team will investigate the issue.'); + + } catch (error) { + logger.error(`Error submitting bug report: ${error instanceof Error ? error.message : 'Unknown error'}`); + throw error; + } + }, + args: [ + { + name: 'description', + description: 'Description of the bug or issue', + type: ArgType.STRING, + required: true + } + ], + examples: [ + 'bug "The login command fails with a network error"', + 'bug "The application crashes when processing large files"' + ] + }; + + commandRegistry.register(command); +} + +/** + * Register feedback command + */ +function registerFeedbackCommand(): void { + logger.debug('Registering feedback command'); + + const command = { + name: 'feedback', + description: 'Provide general feedback about Claude Code', + category: 'system', + async handler(args: Record): Promise { + logger.info('Executing feedback command'); + + const content = args.content; + if (!isNonEmptyString(content)) { + throw createUserError('Feedback content is required', { + category: ErrorCategory.VALIDATION, + resolution: 'Please provide your feedback about Claude Code' + }); + } + + try { + // In a real implementation, this would send the feedback to a server + logger.info('Submitting feedback...'); + + // Get system information + const os = await import('os'); + const systemInfo = { + platform: os.platform(), + release: os.release(), + nodeVersion: process.version, + appVersion: '0.2.29', // This would come from package.json in a real implementation + timestamp: new Date().toISOString() + }; + + // Get current telemetry client + const telemetryModule = await import('../telemetry/index.js'); + const telemetryManager = await telemetryModule.initTelemetry(); + + telemetryManager.trackEvent('USER_FEEDBACK', { + content, + ...systemInfo + }); + + logger.info('Feedback submitted successfully'); + console.log('Thank you for your feedback. We appreciate your input.'); + + } catch (error) { + logger.error(`Error submitting feedback: ${error instanceof Error ? error.message : 'Unknown error'}`); + throw error; + } + }, + args: [ + { + name: 'content', + description: 'Your feedback about Claude Code', + type: ArgType.STRING, + required: true + } + ], + examples: [ + 'feedback "I love the AI-assisted code generation feature"', + 'feedback "The error messages could be more descriptive"' + ] + }; + + commandRegistry.register(command); +} + +/** + * Register run command + */ +function registerRunCommand(): void { + logger.debug('Registering run command'); + + const command = { + name: 'run', + description: 'Execute a terminal command', + category: 'system', + async handler(args: Record): Promise { + logger.info('Executing run command'); + + const commandToRun = args.command; + if (!isNonEmptyString(commandToRun)) { + throw createUserError('Command is required', { + category: ErrorCategory.VALIDATION, + resolution: 'Please provide a command to execute' + }); + } + + try { + logger.info(`Running command: ${commandToRun}`); + + // Execute the command + const { exec } = await import('child_process'); + const util = await import('util'); + const execPromise = util.promisify(exec); + + logger.debug(`Executing: ${commandToRun}`); + const { stdout, stderr } = await execPromise(commandToRun); + + if (stdout) { + console.log(stdout); + } + + if (stderr) { + console.error(stderr); + } + + logger.info('Command executed successfully'); + } catch (error) { + logger.error(`Error executing command: ${error instanceof Error ? error.message : 'Unknown error'}`); + + if (error instanceof Error) { + console.error(`Error: ${error.message}`); + } + + throw error; + } + }, + args: [ + { + name: 'command', + description: 'The command to execute', + type: ArgType.STRING, + required: true + } + ], + examples: [ + 'run "ls -la"', + 'run "npm install"', + 'run "git status"' + ] + }; + + commandRegistry.register(command); +} + +/** + * Register search command + */ +function registerSearchCommand(): void { + logger.debug('Registering search command'); + + const command = { + name: 'search', + description: 'Search the codebase for a term', + category: 'system', + async handler(args: Record): Promise { + logger.info('Executing search command'); + + const term = args.term; + if (!isNonEmptyString(term)) { + throw createUserError('Search term is required', { + category: ErrorCategory.VALIDATION, + resolution: 'Please provide a term to search for' + }); + } + + try { + logger.info(`Searching for: ${term}`); + + // Get search directory (current directory if not specified) + const searchDir = args.dir || process.cwd(); + + // Execute the search using ripgrep if available, otherwise fall back to simple grep + const { exec } = await import('child_process'); + const util = await import('util'); + const execPromise = util.promisify(exec); + + let searchCommand; + const searchPattern = term.includes(' ') ? `"${term}"` : term; + + try { + // Try to use ripgrep (rg) for better performance + await execPromise('rg --version'); + + // Ripgrep is available, use it + searchCommand = `rg --color=always --line-number --heading --smart-case ${searchPattern} ${searchDir}`; + } catch { + // Fall back to grep (available on most Unix systems) + searchCommand = `grep -r --color=always -n "${term}" ${searchDir}`; + } + + logger.debug(`Running search command: ${searchCommand}`); + const { stdout, stderr } = await execPromise(searchCommand); + + if (stderr) { + console.error(stderr); + } + + if (stdout) { + console.log(stdout); + } else { + console.log(`No results found for '${term}'`); + } + + logger.info('Search completed'); + } catch (error) { + logger.error(`Error searching codebase: ${error instanceof Error ? error.message : 'Unknown error'}`); + + if (error instanceof Error) { + console.error(`Error: ${error.message}`); + } + + throw error; + } + }, + args: [ + { + name: 'term', + description: 'The term to search for', + type: ArgType.STRING, + position: 0, + required: true + }, + { + name: 'dir', + description: 'Directory to search in (defaults to current directory)', + type: ArgType.STRING, + shortFlag: 'd' + } + ], + examples: [ + 'search "function main"', + 'search TODO', + 'search "import React" --dir ./src' + ] + }; + + commandRegistry.register(command); +} + +/** + * Register theme command + */ +function registerThemeCommand(): void { + logger.debug('Registering theme command'); + + const command = { + name: 'theme', + description: 'Change the UI theme', + category: 'system', + async handler(args: Record): Promise { + logger.info('Executing theme command'); + + const theme = args.name; + if (!isNonEmptyString(theme)) { + // If no theme is specified, display the current theme + const configModule = await import('../config/index.js'); + const currentConfig = await configModule.loadConfig(); + + const currentTheme = currentConfig.terminal?.theme || 'system'; + console.log(`Current theme: ${currentTheme}`); + console.log('Available themes: dark, light, system'); + return; + } + + // Validate the theme + const validThemes = ['dark', 'light', 'system']; + if (!validThemes.includes(theme.toLowerCase())) { + throw createUserError(`Invalid theme: ${theme}`, { + category: ErrorCategory.VALIDATION, + resolution: `Please choose one of: ${validThemes.join(', ')}` + }); + } + + try { + // Update the theme in the configuration + const configModule = await import('../config/index.js'); + const currentConfig = await configModule.loadConfig(); + + if (!currentConfig.terminal) { + currentConfig.terminal = {}; + } + + currentConfig.terminal.theme = theme.toLowerCase(); + + logger.info(`Theme updated to: ${theme}`); + console.log(`Theme set to: ${theme}`); + console.log('Note: Theme changes are only temporary for this session. Use the config command to make permanent changes.'); + + } catch (error) { + logger.error(`Error changing theme: ${error instanceof Error ? error.message : 'Unknown error'}`); + throw error; + } + }, + args: [ + { + name: 'name', + description: 'Theme name (dark, light, system)', + type: ArgType.STRING, + position: 0, + required: false + } + ], + examples: [ + 'theme', + 'theme dark', + 'theme light', + 'theme system' + ] + }; + + commandRegistry.register(command); +} + +/** + * Register verbosity command + */ +function registerVerbosityCommand(): void { + logger.debug('Registering verbosity command'); + + const command = { + name: 'verbosity', + description: 'Set output verbosity level', + category: 'system', + async handler(args: Record): Promise { + logger.info('Executing verbosity command'); + + const level = args.level; + + try { + // If no level is specified, display the current verbosity level + if (!isNonEmptyString(level)) { + const configModule = await import('../config/index.js'); + const currentConfig = await configModule.loadConfig(); + + const currentLevel = currentConfig.logger?.level || 'info'; + console.log(`Current verbosity level: ${currentLevel}`); + console.log('Available levels: error, warn, info, debug'); + return; + } + + // Validate the verbosity level and map to LogLevel + const { LogLevel } = await import('../utils/logger.js'); + let logLevel: any; + + switch (level.toLowerCase()) { + case 'debug': + logLevel = LogLevel.DEBUG; + break; + case 'info': + logLevel = LogLevel.INFO; + break; + case 'warn': + logLevel = LogLevel.WARN; + break; + case 'error': + logLevel = LogLevel.ERROR; + break; + case 'silent': + logLevel = LogLevel.SILENT; + break; + default: + throw createUserError(`Invalid verbosity level: ${level}`, { + category: ErrorCategory.VALIDATION, + resolution: `Please choose one of: debug, info, warn, error, silent` + }); + } + + // Update the verbosity level in the configuration + const configModule = await import('../config/index.js'); + const currentConfig = await configModule.loadConfig(); + + if (!currentConfig.logger) { + currentConfig.logger = {}; + } + + currentConfig.logger.level = level.toLowerCase(); + + // Update the logger instance directly + logger.setLevel(logLevel); + + logger.info(`Verbosity level updated to: ${level}`); + console.log(`Verbosity level set to: ${level}`); + + } catch (error) { + logger.error(`Error changing verbosity level: ${error instanceof Error ? error.message : 'Unknown error'}`); + throw error; + } + }, + args: [ + { + name: 'level', + description: 'Verbosity level (debug, info, warn, error, silent)', + type: ArgType.STRING, + position: 0, + required: false + } + ], + examples: [ + 'verbosity', + 'verbosity info', + 'verbosity debug', + 'verbosity error' + ] + }; + + commandRegistry.register(command); +} + +/** + * Register edit command + */ +function registerEditCommand(): void { + logger.debug('Registering edit command'); + + const command = { + name: 'edit', + description: 'Edit a specified file', + category: 'system', + async handler(args: Record): Promise { + logger.info('Executing edit command'); + + const file = args.file; + if (!isNonEmptyString(file)) { + throw createUserError('File path is required', { + category: ErrorCategory.VALIDATION, + resolution: 'Please provide a file path to edit' + }); + } + + try { + // Check if file exists + const fs = await import('fs/promises'); + const path = await import('path'); + + // Resolve the file path + const resolvedPath = path.resolve(process.cwd(), file); + + try { + // Check if file exists + await fs.access(resolvedPath); + } catch (error) { + // If file doesn't exist, create it with empty content + logger.info(`File doesn't exist, creating: ${resolvedPath}`); + await fs.writeFile(resolvedPath, ''); + } + + logger.info(`Opening file for editing: ${resolvedPath}`); + + // On different platforms, open the file with different editors + const { platform } = await import('os'); + const { exec } = await import('child_process'); + const util = await import('util'); + const execPromise = util.promisify(exec); + + let editorCommand; + const systemPlatform = platform(); + + // Try to use the EDITOR environment variable first + const editor = process.env.EDITOR; + + if (editor) { + editorCommand = `${editor} "${resolvedPath}"`; + } else { + // Default editors based on platform + if (systemPlatform === 'win32') { + editorCommand = `notepad "${resolvedPath}"`; + } else if (systemPlatform === 'darwin') { + editorCommand = `open -a TextEdit "${resolvedPath}"`; + } else { + // Try nano first, fall back to vi + try { + await execPromise('which nano'); + editorCommand = `nano "${resolvedPath}"`; + } catch { + editorCommand = `vi "${resolvedPath}"`; + } + } + } + + logger.debug(`Executing editor command: ${editorCommand}`); + console.log(`Opening ${resolvedPath} for editing...`); + + const child = exec(editorCommand); + + // Log when the editor process exits + child.on('exit', (code) => { + logger.info(`Editor process exited with code: ${code}`); + if (code === 0) { + console.log(`File saved: ${resolvedPath}`); + } else { + console.error(`Editor exited with non-zero code: ${code}`); + } + }); + + // Wait for the editor to start + await new Promise((resolve) => setTimeout(resolve, 1000)); + + } catch (error) { + logger.error(`Error editing file: ${error instanceof Error ? error.message : 'Unknown error'}`); + throw error; + } + }, + args: [ + { + name: 'file', + description: 'File path to edit', + type: ArgType.STRING, + position: 0, + required: true + } + ], + examples: [ + 'edit path/to/file.txt', + 'edit newfile.md' + ] + }; + + commandRegistry.register(command); +} + +/** + * Register git command + */ +function registerGitCommand(): void { + logger.debug('Registering git command'); + + const command = { + name: 'git', + description: 'Perform git operations', + category: 'system', + async handler(args: Record): Promise { + logger.info('Executing git command'); + + const operation = args.operation; + if (!isNonEmptyString(operation)) { + throw createUserError('Git operation is required', { + category: ErrorCategory.VALIDATION, + resolution: 'Please provide a git operation to perform' + }); + } + + try { + logger.info(`Performing git operation: ${operation}`); + + // Check if git is installed + const { exec } = await import('child_process'); + const util = await import('util'); + const execPromise = util.promisify(exec); + + try { + await execPromise('git --version'); + } catch (error) { + throw createUserError('Git is not installed or not in PATH', { + category: ErrorCategory.COMMAND_EXECUTION, + resolution: 'Please install git or add it to your PATH' + }); + } + + // Validate the operation is a simple command without pipes, redirection, etc. + const validOpRegex = /^[a-z0-9\-_\s]+$/i; + if (!validOpRegex.test(operation)) { + throw createUserError('Invalid git operation', { + category: ErrorCategory.VALIDATION, + resolution: 'Please provide a simple git operation without special characters' + }); + } + + // Construct and execute the git command + const gitCommand = `git ${operation}`; + logger.debug(`Executing git command: ${gitCommand}`); + + const { stdout, stderr } = await execPromise(gitCommand); + + if (stderr) { + console.error(stderr); + } + + if (stdout) { + console.log(stdout); + } + + logger.info('Git operation completed'); + } catch (error) { + logger.error(`Error executing git operation: ${error instanceof Error ? error.message : 'Unknown error'}`); + + if (error instanceof Error) { + console.error(`Error: ${error.message}`); + } + + throw error; + } + }, + args: [ + { + name: 'operation', + description: 'Git operation to perform', + type: ArgType.STRING, + position: 0, + required: true + } + ], + examples: [ + 'git status', + 'git log', + 'git add .', + 'git commit -m "Commit message"' + ] + }; + + commandRegistry.register(command); +} + +/** + * Register exit command + */ +function registerExitCommand(): void { + logger.debug('Registering exit command'); + + const command = { + name: 'exit', + description: 'Exit the application', + category: 'session', + async handler(): Promise { + logger.info('Executing exit command'); + console.log('Exiting Claude Code CLI...'); + process.exit(0); + }, + examples: [ + 'exit' + ] + }; + + commandRegistry.register(command); +} + +/** + * Register quit command (alias for exit) + */ +function registerQuitCommand(): void { + logger.debug('Registering quit command'); + + const command = { + name: 'quit', + description: 'Exit the application (alias for exit)', + category: 'session', + async handler(): Promise { + logger.info('Executing quit command'); + console.log('Exiting Claude Code CLI...'); + process.exit(0); + }, + examples: [ + 'quit' + ] + }; + + commandRegistry.register(command); +} + +/** + * Register clear command + */ +function registerClearCommand(): void { + logger.debug('Registering clear command'); + + const command = { + name: 'clear', + description: 'Clear the current session display', + category: 'session', + async handler(): Promise { + logger.info('Executing clear command'); + + // Clear the console using the appropriate method for the current platform + // This is the cross-platform way to clear the terminal + process.stdout.write('\x1Bc'); + + console.log('Display cleared.'); + }, + examples: [ + 'clear' + ] + }; + + commandRegistry.register(command); +} + +/** + * Register reset command + */ +function registerResetCommand(): void { + logger.debug('Registering reset command'); + + const command = { + name: 'reset', + description: 'Reset the conversation context with Claude', + category: 'session', + async handler(): Promise { + logger.info('Executing reset command'); + + try { + // Since there's no direct reset method, we'll reinitialize the AI client + logger.info('Reinitializing AI client to reset conversation context'); + + // Re-initialize the AI client + await initAI(); + + console.log('Conversation context has been reset.'); + logger.info('AI client reinitialized, conversation context reset'); + } catch (error) { + logger.error(`Error resetting conversation context: ${error instanceof Error ? error.message : 'Unknown error'}`); + throw error; + } + }, + examples: [ + 'reset' + ], + requiresAuth: true + }; + + commandRegistry.register(command); +} + +/** + * Register history command + */ +function registerHistoryCommand(): void { + logger.debug('Registering history command'); + + const command = { + name: 'history', + description: 'View conversation history', + category: 'session', + async handler(args: Record): Promise { + logger.info('Executing history command'); + + try { + // Since we don't have direct access to conversation history through the AI client, + // we'll need to inform the user that history is not available or implement + // a conversation history tracking mechanism elsewhere + + // This is a placeholder implementation until a proper history tracking system is implemented + logger.warn('Conversation history feature is not currently implemented'); + console.log('Conversation history is not available in the current version.'); + console.log('This feature will be implemented in a future update.'); + + // Future implementation could include: + // - Storing conversations in a local database or file + // - Retrieving conversations from the API if it supports it + // - Implementing a session-based history tracker + } catch (error) { + logger.error(`Error retrieving conversation history: ${error instanceof Error ? error.message : 'Unknown error'}`); + throw error; + } + }, + args: [ + { + name: 'limit', + description: 'Maximum number of history items to display', + type: ArgType.NUMBER, + shortFlag: 'l', + default: '10' + } + ], + examples: [ + 'history', + 'history --limit 5' + ], + requiresAuth: true + }; + + commandRegistry.register(command); +} + +/** + * Register commands command + */ +function registerCommandsCommand(): void { + logger.debug('Registering commands command'); + + const command = { + name: 'commands', + description: 'List all available slash commands', + category: 'session', + async handler(): Promise { + logger.info('Executing commands command'); + + try { + // Get all registered commands + const allCommands = commandRegistry.list() + .filter(cmd => !cmd.hidden) // Filter out hidden commands + .sort((a, b) => { + // Sort first by category, then by name + if (a.category && b.category) { + if (a.category !== b.category) { + return a.category.localeCompare(b.category); + } + } else if (a.category) { + return -1; + } else if (b.category) { + return 1; + } + return a.name.localeCompare(b.name); + }); + + // Group commands by category + const categories = new Map(); + const uncategorizedCommands: CommandDef[] = []; + + for (const cmd of allCommands) { + if (cmd.category) { + if (!categories.has(cmd.category)) { + categories.set(cmd.category, []); + } + categories.get(cmd.category)!.push(cmd); + } else { + uncategorizedCommands.push(cmd); + } + } + + console.log('Available slash commands:\n'); + + // Display uncategorized commands first + if (uncategorizedCommands.length > 0) { + for (const cmd of uncategorizedCommands) { + console.log(`/${cmd.name.padEnd(15)} ${cmd.description}`); + } + console.log(''); + } + + // Display categorized commands + for (const [category, commands] of categories.entries()) { + console.log(`${category}:`); + for (const cmd of commands) { + console.log(` /${cmd.name.padEnd(13)} ${cmd.description}`); + } + console.log(''); + } + + console.log('For more information on a specific command, use:'); + console.log(' /help '); + + } catch (error) { + logger.error(`Error listing commands: ${error instanceof Error ? error.message : 'Unknown error'}`); + throw error; + } + }, + examples: [ + 'commands' + ] + }; + + commandRegistry.register(command); +} + +/** + * Register help command + */ +function registerHelpCommand(): void { + logger.debug('Registering help command'); + + const command = { + name: 'help', + description: 'Get help for a specific command', + category: 'session', + async handler(args: Record): Promise { + logger.info('Executing help command'); + + const commandName = args.command; + if (!isNonEmptyString(commandName)) { + throw createUserError('Command name is required', { + category: ErrorCategory.VALIDATION, + resolution: 'Please provide a command name to get help for' + }); + } + + try { + // Get the command definition + const command = commandRegistry.get(commandName); + if (!command) { + throw createUserError(`Command not found: ${commandName}`, { + category: ErrorCategory.VALIDATION, + resolution: 'Please check the command name and try again' + }); + } + + // Display command information + console.log(`Command: ${command.name}`); + console.log(`Description: ${command.description}`); + if (command.category) { + console.log(`Category: ${command.category}`); + } + console.log(`Requires Auth: ${command.requiresAuth ? 'Yes' : 'No'}`); + + // Display command usage + console.log('\nUsage:'); + if (command.args && command.args.length > 0) { + console.log(` /${command.name} ${command.args.map(arg => arg.name).join(' ')}`); + } else { + console.log(` /${command.name}`); + } + + // Display command examples + if (command.examples && command.examples.length > 0) { + console.log('\nExamples:'); + for (const example of command.examples) { + console.log(` /${example}`); + } + } + + // Display command arguments + if (command.args && command.args.length > 0) { + console.log('\nArguments:'); + for (const arg of command.args) { + console.log(` ${arg.name}: ${arg.description}`); + } + } + + logger.info('Help information retrieved'); + } catch (error) { + logger.error(`Error retrieving help: ${error instanceof Error ? error.message : 'Unknown error'}`); + throw error; + } + }, + args: [ + { + name: 'command', + description: 'The command to get help for', + type: ArgType.STRING, + position: 0, + required: true + } + ], + examples: [ + 'help login', + 'help ask' + ] + }; + + commandRegistry.register(command); +} \ No newline at end of file diff --git a/claude-code/src/config/defaults.ts b/claude-code/src/config/defaults.ts new file mode 100644 index 0000000..d431122 --- /dev/null +++ b/claude-code/src/config/defaults.ts @@ -0,0 +1,85 @@ +/** + * Default Configuration + * + * Defines the default values for the application configuration. + * These values are used if not overridden by user configuration. + */ + +import { ConfigType } from './schema.js'; + +/** + * Default configuration values + */ +export const defaultConfig: Partial = { + // Basic configuration + logLevel: 'info', + + // API configuration + api: { + baseUrl: 'https://api.anthropic.com', + version: 'v1', + timeout: 60000 // 60 seconds + }, + + // Telemetry configuration + telemetry: { + enabled: true, + anonymizeData: true, + errorReporting: true + }, + + // Terminal configuration + terminal: { + theme: 'system', + showProgressIndicators: true, + useColors: true, + codeHighlighting: true + }, + + // Code analysis configuration + codeAnalysis: { + indexDepth: 3, + excludePatterns: [ + 'node_modules/**', + '.git/**', + 'dist/**', + 'build/**', + '**/*.min.js', + '**/*.bundle.js', + '**/vendor/**', + '.DS_Store', + '**/*.log', + '**/*.lock', + '**/package-lock.json', + '**/yarn.lock', + '**/pnpm-lock.yaml', + '.env*', + '**/*.map' + ], + includePatterns: ['**/*'], + maxFileSize: 1024 * 1024, // 1MB + scanTimeout: 30000 // 30 seconds + }, + + // Git configuration + git: { + preferredRemote: 'origin', + useSsh: false, + useGpg: false, + signCommits: false + }, + + // Editor configuration + editor: { + tabWidth: 2, + insertSpaces: true, + formatOnSave: true + }, + + // Authentication related + forceLogin: false, + forceLogout: false, + + // Persistent data + recentWorkspaces: [] +}; \ No newline at end of file diff --git a/claude-code/src/config/index.ts b/claude-code/src/config/index.ts new file mode 100644 index 0000000..9754694 --- /dev/null +++ b/claude-code/src/config/index.ts @@ -0,0 +1,293 @@ +/** + * Configuration Module + * + * Handles loading, validating, and providing access to application configuration. + * Supports multiple sources like environment variables, config files, and CLI arguments. + */ + +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import { logger } from '../utils/logger.js'; +import { createUserError } from '../errors/formatter.js'; +import { ErrorCategory } from '../errors/types.js'; + +/** + * Default configuration values + */ +const DEFAULT_CONFIG = { + // API configuration + api: { + baseUrl: 'https://api.anthropic.com', + version: 'v1', + timeout: 60000 + }, + + // AI configuration + ai: { + model: 'claude-3-opus-20240229', + temperature: 0.5, + maxTokens: 4096, + maxHistoryLength: 20 + }, + + // Authentication configuration + auth: { + autoRefresh: true, + tokenRefreshThreshold: 300, // 5 minutes + maxRetryAttempts: 3 + }, + + // Terminal configuration + terminal: { + theme: 'system', + useColors: true, + showProgressIndicators: true, + codeHighlighting: true + }, + + // Telemetry configuration + telemetry: { + enabled: true, + submissionInterval: 30 * 60 * 1000, // 30 minutes + maxQueueSize: 100, + autoSubmit: true + }, + + // File operation configuration + fileOps: { + maxReadSizeBytes: 10 * 1024 * 1024 // 10MB + }, + + // Execution configuration + execution: { + shell: process.env.SHELL || 'bash' + }, + + // Logger configuration + logger: { + level: 'info', + timestamps: true, + colors: true + }, + + // App information + version: '0.2.29' +}; + +/** + * Configuration file paths to check + */ +const CONFIG_PATHS = [ + // Current directory + path.join(process.cwd(), '.claude-code.json'), + path.join(process.cwd(), '.claude-code.js'), + + // User home directory + path.join(os.homedir(), '.claude-code', 'config.json'), + path.join(os.homedir(), '.claude-code.json'), + + // XDG config directory (Linux/macOS) + process.env.XDG_CONFIG_HOME + ? path.join(process.env.XDG_CONFIG_HOME, 'claude-code', 'config.json') + : path.join(os.homedir(), '.config', 'claude-code', 'config.json'), + + // AppData directory (Windows) + process.env.APPDATA + ? path.join(process.env.APPDATA, 'claude-code', 'config.json') + : null +].filter(Boolean) as string[]; + +/** + * Load configuration from a file + */ +function loadConfigFromFile(configPath: string): any { + try { + if (!fs.existsSync(configPath)) { + return null; + } + + logger.debug(`Loading configuration from ${configPath}`); + + if (configPath.endsWith('.js')) { + // Load JavaScript module + const configModule = require(configPath); + return configModule.default || configModule; + } else { + // Load JSON file + const configContent = fs.readFileSync(configPath, 'utf8'); + return JSON.parse(configContent); + } + } catch (error) { + logger.warn(`Error loading configuration from ${configPath}`, error); + return null; + } +} + +/** + * Load configuration from environment variables + */ +function loadConfigFromEnv(): any { + const envConfig: any = {}; + + // Check for API key + if (process.env.CLAUDE_API_KEY) { + envConfig.api = envConfig.api || {}; + envConfig.api.key = process.env.CLAUDE_API_KEY; + } + + // Check for API URL + if (process.env.CLAUDE_API_URL) { + envConfig.api = envConfig.api || {}; + envConfig.api.baseUrl = process.env.CLAUDE_API_URL; + } + + // Check for log level + if (process.env.CLAUDE_LOG_LEVEL) { + envConfig.logger = envConfig.logger || {}; + envConfig.logger.level = process.env.CLAUDE_LOG_LEVEL; + } + + // Check for telemetry opt-out + if (process.env.CLAUDE_TELEMETRY === '0' || process.env.CLAUDE_TELEMETRY === 'false') { + envConfig.telemetry = envConfig.telemetry || {}; + envConfig.telemetry.enabled = false; + } + + // Check for model override + if (process.env.CLAUDE_MODEL) { + envConfig.ai = envConfig.ai || {}; + envConfig.ai.model = process.env.CLAUDE_MODEL; + } + + return envConfig; +} + +/** + * Merge configuration objects + */ +function mergeConfigs(...configs: any[]): any { + const result: any = {}; + + for (const config of configs) { + if (!config) continue; + + for (const key of Object.keys(config)) { + const value = config[key]; + + if (value === null || value === undefined) { + continue; + } + + if (typeof value === 'object' && !Array.isArray(value) && value !== null) { + // Recursively merge objects + result[key] = mergeConfigs(result[key] || {}, value); + } else { + // Overwrite primitives, arrays, etc. + result[key] = value; + } + } + } + + return result; +} + +/** + * Validate critical configuration + */ +function validateConfig(config: any): void { + // Validate API configuration + if (!config.api.baseUrl) { + throw createUserError('API base URL is not configured', { + category: ErrorCategory.CONFIGURATION, + resolution: 'Provide a valid API base URL in your configuration' + }); + } + + // Validate authentication + if (!config.api.key && !config.auth.token) { + logger.warn('No API key or authentication token configured'); + } + + // Validate AI model + if (!config.ai.model) { + throw createUserError('AI model is not configured', { + category: ErrorCategory.CONFIGURATION, + resolution: 'Specify a valid Claude model in your configuration' + }); + } +} + +/** + * Load configuration + */ +export async function loadConfig(options: any = {}): Promise { + logger.debug('Loading configuration', { options }); + + // Initialize with defaults + let config = { ...DEFAULT_CONFIG }; + + // Load configuration from files + for (const configPath of CONFIG_PATHS) { + const fileConfig = loadConfigFromFile(configPath); + if (fileConfig) { + config = mergeConfigs(config, fileConfig); + logger.debug(`Loaded configuration from ${configPath}`); + break; // Stop after first successful load + } + } + + // Load configuration from environment variables + const envConfig = loadConfigFromEnv(); + config = mergeConfigs(config, envConfig); + + // Override with command line options + if (options) { + const cliConfig: any = {}; + + // Map CLI flags to configuration + if (options.verbose) { + cliConfig.logger = { level: 'debug' }; + } + + if (options.quiet) { + cliConfig.logger = { level: 'error' }; + } + + if (options.debug) { + cliConfig.logger = { level: 'debug' }; + } + + if (options.config) { + // Load from specified config file + const customConfig = loadConfigFromFile(options.config); + if (customConfig) { + config = mergeConfigs(config, customConfig); + } else { + throw createUserError(`Could not load configuration from ${options.config}`, { + category: ErrorCategory.CONFIGURATION, + resolution: 'Check that the file exists and is valid JSON or JavaScript' + }); + } + } + + // Merge CLI options + config = mergeConfigs(config, cliConfig); + } + + // Validate the configuration + validateConfig(config); + + // Configure logger + import('../utils/logger.js').then(loggerModule => { + if (loggerModule.configureLogger) { + loggerModule.configureLogger(config); + } + }).catch(error => { + logger.warn('Failed to configure logger', error); + }); + + return config; +} + +export default { loadConfig }; \ No newline at end of file diff --git a/claude-code/src/config/schema.ts b/claude-code/src/config/schema.ts new file mode 100644 index 0000000..6f0472a --- /dev/null +++ b/claude-code/src/config/schema.ts @@ -0,0 +1,123 @@ +/** + * Configuration Schema + * + * Defines the structure and validation rules for the application configuration. + * Uses Zod for runtime type validation. + */ + +import { z } from 'zod'; + +// Define log level enum +const LogLevel = z.enum(['error', 'warn', 'info', 'verbose', 'debug', 'trace']); + +// API configuration schema +const ApiConfigSchema = z.object({ + key: z.string().optional(), + baseUrl: z.string().url().optional(), + version: z.string().optional(), + timeout: z.number().positive().optional() +}); + +// Telemetry configuration schema +const TelemetryConfigSchema = z.object({ + enabled: z.boolean().default(true), + anonymizeData: z.boolean().default(true), + errorReporting: z.boolean().default(true) +}); + +// Terminal configuration schema +const TerminalConfigSchema = z.object({ + theme: z.enum(['dark', 'light', 'system']).default('system'), + showProgressIndicators: z.boolean().default(true), + useColors: z.boolean().default(true), + codeHighlighting: z.boolean().default(true), + maxHeight: z.number().positive().optional(), + maxWidth: z.number().positive().optional() +}); + +// Code analysis configuration schema +const CodeAnalysisConfigSchema = z.object({ + indexDepth: z.number().int().positive().default(3), + excludePatterns: z.array(z.string()).default([ + 'node_modules/**', + '.git/**', + 'dist/**', + 'build/**', + '**/*.min.js', + '**/*.bundle.js' + ]), + includePatterns: z.array(z.string()).default(['**/*']), + maxFileSize: z.number().int().positive().default(1024 * 1024), // 1MB + scanTimeout: z.number().int().positive().default(30000) // 30s +}); + +// Git configuration schema +const GitConfigSchema = z.object({ + preferredRemote: z.string().default('origin'), + preferredBranch: z.string().optional(), + useSsh: z.boolean().default(false), + useGpg: z.boolean().default(false), + signCommits: z.boolean().default(false) +}); + +// Editor configuration schema +const EditorConfigSchema = z.object({ + preferredLauncher: z.string().optional(), + tabWidth: z.number().int().positive().default(2), + insertSpaces: z.boolean().default(true), + formatOnSave: z.boolean().default(true) +}); + +// Paths configuration schema - will be populated at runtime +const PathsConfigSchema = z.object({ + home: z.string().optional(), + app: z.string().optional(), + cache: z.string().optional(), + logs: z.string().optional(), + workspace: z.string().optional() +}); + +// Main configuration schema +export const configSchema = z.object({ + // Basic configuration + workspace: z.string().optional(), + logLevel: LogLevel.default('info'), + + // Subsystem configurations + api: ApiConfigSchema.default({}), + telemetry: TelemetryConfigSchema.default({}), + terminal: TerminalConfigSchema.default({}), + codeAnalysis: CodeAnalysisConfigSchema.default({}), + git: GitConfigSchema.default({}), + editor: EditorConfigSchema.default({}), + + // Runtime configuration + paths: PathsConfigSchema.optional(), + + // Authentication related + forceLogin: z.boolean().default(false), + forceLogout: z.boolean().default(false), + + // Persistent data + lastUpdateCheck: z.number().optional(), + auth: z.object({ + tokens: z.record(z.string()).optional(), + lastAuth: z.number().optional() + }).optional(), + recentWorkspaces: z.array(z.string()).default([]) +}); + +// Type definition generated from schema +export type ConfigType = z.infer; + +// Export sub-schemas for modular validation +export { + LogLevel, + ApiConfigSchema, + TelemetryConfigSchema, + TerminalConfigSchema, + CodeAnalysisConfigSchema, + GitConfigSchema, + EditorConfigSchema, + PathsConfigSchema +}; \ No newline at end of file diff --git a/claude-code/src/errors/console.ts b/claude-code/src/errors/console.ts new file mode 100644 index 0000000..96f4915 --- /dev/null +++ b/claude-code/src/errors/console.ts @@ -0,0 +1,102 @@ +/** + * Console Error Handling + * + * Utilities for handling console errors and setting up error handling for console output. + */ + +import { ErrorManager, ErrorCategory, ErrorLevel } from './types.js'; +import { logger } from '../utils/logger.js'; + +/** + * Set up console error handling + */ +export function setupConsoleErrorHandling(errorManager: ErrorManager): void { + // Store original console methods + const originalConsoleError = console.error; + const originalConsoleWarn = console.warn; + + // Override console.error to track and handle errors + console.error = function(...args: any[]): void { + // Use the original console.error for output + originalConsoleError.apply(console, args); + + // Don't process logger errors to avoid infinite recursion + if (args[0] && typeof args[0] === 'string' && ( + args[0].includes('FATAL ERROR:') || + args[0].includes('Uncaught Exception:') || + args[0].includes('Unhandled Promise Rejection:') + )) { + return; + } + + // Extract the error from the arguments + const error = extractErrorFromArgs(args); + + // Track the error + if (error) { + errorManager.handleError(error, { + level: ErrorLevel.MINOR, + category: ErrorCategory.APPLICATION, + context: { source: 'console.error' } + }); + } + }; + + // Override console.warn to track warnings + console.warn = function(...args: any[]): void { + // Use the original console.warn for output + originalConsoleWarn.apply(console, args); + + // Extract the warning from the arguments + const warning = extractErrorFromArgs(args); + + // Track the warning as an informational error + if (warning) { + errorManager.handleError(warning, { + level: ErrorLevel.INFORMATIONAL, + category: ErrorCategory.APPLICATION, + context: { source: 'console.warn' } + }); + } + }; + + logger.debug('Console error handling set up'); +} + +/** + * Extract an error from console arguments + */ +function extractErrorFromArgs(args: any[]): Error | string | null { + if (args.length === 0) { + return null; + } + + // Check for Error objects + for (const arg of args) { + if (arg instanceof Error) { + return arg; + } + } + + // If no Error object found, convert to string + try { + const message = args.map(arg => { + if (typeof arg === 'string') { + return arg; + } else if (arg === null || arg === undefined) { + return String(arg); + } else { + try { + return JSON.stringify(arg); + } catch (error) { + return String(arg); + } + } + }).join(' '); + + return message || null; + } catch (error) { + // If all else fails, return a generic message + return 'Console error occurred'; + } +} \ No newline at end of file diff --git a/claude-code/src/errors/formatter.ts b/claude-code/src/errors/formatter.ts new file mode 100644 index 0000000..94883e4 --- /dev/null +++ b/claude-code/src/errors/formatter.ts @@ -0,0 +1,144 @@ +/** + * Error Formatter + * + * Utilities for formatting errors and creating user-friendly error messages. + */ + +import { ErrorCategory, ErrorLevel, UserError, UserErrorOptions } from './types.js'; +import { logger } from '../utils/logger.js'; +import { formatErrorDetails } from '../utils/formatting.js'; + +/** + * Create a user-friendly error from any error + */ +export function createUserError( + message: string, + options: UserErrorOptions = {} +): UserError { + // Create UserError instance + const userError = new UserError(message, options); + + // Log the error with appropriate level + const level = userError.level === ErrorLevel.FATAL ? 'error' : 'warn'; + logger[level](`User error: ${message}`, { + category: userError.category, + details: userError.details, + resolution: userError.resolution + }); + + return userError; +} + +/** + * Format an error for display to the user + */ +export function formatErrorForDisplay(error: unknown): string { + if (error instanceof UserError) { + return formatUserError(error); + } + + if (error instanceof Error) { + return formatSystemError(error); + } + + return `Unknown error: ${String(error)}`; +} + +/** + * Format a UserError for display + */ +function formatUserError(error: UserError): string { + let message = `Error: ${error.message}`; + + // Add resolution steps if available + if (error.resolution) { + const resolutionSteps = Array.isArray(error.resolution) + ? error.resolution + : [error.resolution]; + + message += '\n\nTo resolve this:'; + resolutionSteps.forEach(step => { + message += `\n• ${step}`; + }); + } + + // Add details if available + if (error.details && Object.keys(error.details).length > 0) { + message += '\n\nDetails:'; + for (const [key, value] of Object.entries(error.details)) { + const formattedValue = typeof value === 'object' + ? JSON.stringify(value, null, 2) + : String(value); + message += `\n${key}: ${formattedValue}`; + } + } + + return message; +} + +/** + * Format a system Error for display + */ +function formatSystemError(error: Error): string { + let message = `System error: ${error.message}`; + + // Add stack trace for certain categories of errors + if (process.env.DEBUG === 'true') { + message += `\n\nStack trace:\n${error.stack || 'No stack trace available'}`; + } + + return message; +} + +/** + * Convert an error to a UserError if it isn't already + */ +export function ensureUserError( + error: unknown, + defaultMessage: string = 'An unexpected error occurred', + options: UserErrorOptions = {} +): UserError { + if (error instanceof UserError) { + return error; + } + + const message = error instanceof Error + ? error.message + : typeof error === 'string' + ? error + : defaultMessage; + + return createUserError(message, { + ...options, + cause: error + }); +} + +/** + * Get a category name for an error + */ +export function getErrorCategoryName(category: ErrorCategory): string { + return ErrorCategory[category] || 'Unknown'; +} + +/** + * Get an error level name + */ +export function getErrorLevelName(level: ErrorLevel): string { + return ErrorLevel[level] || 'Unknown'; +} + +/** + * Get detailed information about an error + */ +export function getErrorDetails(error: unknown): string { + if (error instanceof UserError) { + return formatUserError(error); + } + + if (error instanceof Error) { + return formatErrorDetails(error); + } + + return String(error); +} \ No newline at end of file diff --git a/claude-code/src/errors/index.ts b/claude-code/src/errors/index.ts new file mode 100644 index 0000000..555ef6d --- /dev/null +++ b/claude-code/src/errors/index.ts @@ -0,0 +1,170 @@ +/** + * Error Handling Module + * + * Provides centralized error handling, tracking, and reporting. + */ + +import { logger } from '../utils/logger.js'; +import { ErrorLevel, ErrorCategory, ErrorManager, ErrorOptions } from './types.js'; +import { formatErrorForDisplay } from './formatter.js'; +import { setupSentryReporting } from './sentry.js'; +import { setupConsoleErrorHandling } from './console.js'; + +/** + * Initialize error handling system + */ +export function initErrorHandling(): ErrorManager { + logger.debug('Initializing error handling system'); + + // Create error manager instance + const errorManager = new ErrorHandlerImpl(); + + try { + // Set up Sentry error reporting if enabled + // We're skipping Sentry SDK as requested + + // Set up console error handling + setupConsoleErrorHandling(errorManager); + + return errorManager; + } catch (error) { + logger.error('Failed to initialize error handling system', error); + + // Return a basic error manager even if initialization failed + return errorManager; + } +} + +/** + * Implementation of the ErrorManager interface + */ +class ErrorHandlerImpl implements ErrorManager { + private errorCount: Map = new Map(); + private readonly MAX_ERRORS = 100; + + /** + * Handle a fatal error that should terminate the application + */ + handleFatalError(error: unknown): never { + const formattedError = this.formatError(error, { + level: ErrorLevel.CRITICAL, + category: ErrorCategory.APPLICATION + }); + + logger.error('FATAL ERROR:', formattedError); + + // Exit with error code + process.exit(1); + } + + /** + * Handle an unhandled promise rejection + */ + handleUnhandledRejection(reason: unknown, promise: Promise): void { + const formattedError = this.formatError(reason, { + level: ErrorLevel.MAJOR, + category: ErrorCategory.APPLICATION, + context: { promise } + }); + + logger.error('Unhandled Promise Rejection:', formattedError); + } + + /** + * Handle an uncaught exception + */ + handleUncaughtException(error: unknown): void { + const formattedError = this.formatError(error, { + level: ErrorLevel.CRITICAL, + category: ErrorCategory.APPLICATION + }); + + logger.error('Uncaught Exception:', formattedError); + } + + /** + * Handle a general error + */ + handleError(error: unknown, options: ErrorOptions = {}): void { + const category = options.category || ErrorCategory.APPLICATION; + const level = options.level || ErrorLevel.MINOR; + + // Track error count for rate limiting + const errorKey = `${category}:${level}:${this.getErrorMessage(error)}`; + const count = (this.errorCount.get(errorKey) || 0) + 1; + this.errorCount.set(errorKey, count); + + // Format the error + const formattedError = this.formatError(error, options); + + // Log the error based on level + switch (level) { + case ErrorLevel.CRITICAL: + case ErrorLevel.MAJOR: + logger.error(`[${ErrorCategory[category]}] ${formattedError.message}`, formattedError); + break; + case ErrorLevel.MINOR: + logger.warn(`[${ErrorCategory[category]}] ${formattedError.message}`, formattedError); + break; + case ErrorLevel.INFORMATIONAL: + logger.info(`[${ErrorCategory[category]}] ${formattedError.message}`, formattedError); + break; + } + + // Report to telemetry/monitoring if appropriate + if (level === ErrorLevel.CRITICAL || level === ErrorLevel.MAJOR) { + this.reportError(formattedError, options); + } + } + + /** + * Format an error object for consistent handling + */ + private formatError(error: unknown, options: ErrorOptions = {}): any { + try { + return formatErrorForDisplay(error, options); + } catch (formattingError) { + // If formatting fails, return a basic error object + return { + message: this.getErrorMessage(error), + originalError: error, + formattingError + }; + } + } + + /** + * Get an error message from any error type + */ + private getErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message; + } else if (typeof error === 'string') { + return error; + } else { + try { + return JSON.stringify(error); + } catch { + return String(error); + } + } + } + + /** + * Report an error to monitoring/telemetry systems + */ + private reportError(error: any, options: ErrorOptions = {}): void { + // We're skipping Sentry SDK as requested + // In a real implementation, this would send the error to Sentry + + // Instead, just log that we would report it + logger.debug('Would report error to monitoring system:', { + error: error.message, + level: options.level, + category: options.category + }); + } +} + +// Export error types +export * from './types.js'; \ No newline at end of file diff --git a/claude-code/src/errors/sentry.ts b/claude-code/src/errors/sentry.ts new file mode 100644 index 0000000..a1eb4f7 --- /dev/null +++ b/claude-code/src/errors/sentry.ts @@ -0,0 +1,76 @@ +/** + * Sentry Error Reporting + * + * Utilities for reporting errors to Sentry. + * Note: We're skipping the full Sentry SDK implementation as requested. + */ + +import { ErrorManager } from './types.js'; +import { logger } from '../utils/logger.js'; + +/** + * Set up Sentry error reporting + * + * This is a minimal implementation that doesn't actually use the Sentry SDK. + */ +export function setupSentryReporting( + errorManager: ErrorManager, + options: { + enabled?: boolean; + dsn?: string; + environment?: string; + release?: string; + } = {} +): void { + logger.debug('Sentry error reporting would be set up here'); + + // In a real implementation, we would initialize Sentry here + // For example: + // Sentry.init({ + // dsn: options.dsn, + // environment: options.environment, + // release: options.release, + // enabled: options.enabled !== false + // }); + + logger.info('Skipping Sentry SDK initialization as requested'); +} + +/** + * Report an error to Sentry + * + * This is a minimal implementation that doesn't actually use the Sentry SDK. + */ +export function reportErrorToSentry( + error: unknown, + options: { + level?: string; + tags?: Record; + extra?: Record; + user?: { + id?: string; + username?: string; + email?: string; + }; + } = {} +): void { + logger.debug('Would report error to Sentry:', { + error: error instanceof Error ? error.message : String(error), + level: options.level, + tags: options.tags, + user: options.user ? { + id: options.user.id && '***', + username: options.user.username && '***', + email: options.user.email && '***' + } : undefined + }); + + // In a real implementation, we would call Sentry.captureException here + // For example: + // Sentry.captureException(error, { + // level: options.level, + // tags: options.tags, + // extra: options.extra, + // user: options.user + // }); +} \ No newline at end of file diff --git a/claude-code/src/errors/types.ts b/claude-code/src/errors/types.ts new file mode 100644 index 0000000..5cfd034 --- /dev/null +++ b/claude-code/src/errors/types.ts @@ -0,0 +1,303 @@ +/** + * Error Handling Types + * + * Type definitions for the error handling system. + */ + +/** + * Error severity levels + */ +export enum ErrorLevel { + /** + * Critical errors that prevent the application from functioning + */ + CRITICAL = 0, + + /** + * Major errors that significantly impact functionality + */ + MAJOR = 1, + + /** + * Minor errors that don't significantly impact functionality + */ + MINOR = 2, + + /** + * Informational errors that don't impact functionality + */ + INFORMATIONAL = 3, + + /** + * Debug level + */ + DEBUG = 4, + + /** + * Info level + */ + INFO = 5, + + /** + * Warning level + */ + WARNING = 6, + + /** + * Error level + */ + ERROR = 7, + + /** + * Fatal level + */ + FATAL = 8 +} + +/** + * Error categories for classification + */ +export enum ErrorCategory { + /** + * Application-level errors + */ + APPLICATION = 0, + + /** + * Authentication-related errors + */ + AUTHENTICATION = 1, + + /** + * Network-related errors + */ + NETWORK = 2, + + /** + * File system-related errors + */ + FILE_SYSTEM = 3, + + /** + * Command execution-related errors + */ + COMMAND_EXECUTION = 4, + + /** + * AI service-related errors + */ + AI_SERVICE = 5, + + /** + * Configuration-related errors + */ + CONFIGURATION = 6, + + /** + * Resource-related errors + */ + RESOURCE = 7, + + /** + * Unknown errors + */ + UNKNOWN = 8, + + /** + * Internal errors + */ + INTERNAL = 9, + + /** + * Validation errors + */ + VALIDATION = 10, + + /** + * Initialization errors + */ + INITIALIZATION = 11, + + /** + * Server errors + */ + SERVER = 12, + + /** + * API errors + */ + API = 13, + + /** + * Timeout errors + */ + TIMEOUT = 14, + + /** + * Rate limit errors + */ + RATE_LIMIT = 15, + + /** + * Connection errors + */ + CONNECTION = 16, + + /** + * Authorization errors + */ + AUTHORIZATION = 17, + + /** + * File not found errors + */ + FILE_NOT_FOUND = 18, + + /** + * File access errors + */ + FILE_ACCESS = 19, + + /** + * File read errors + */ + FILE_READ = 20, + + /** + * File write errors + */ + FILE_WRITE = 21, + + /** + * Command errors + */ + COMMAND = 22, + + /** + * Command not found errors + */ + COMMAND_NOT_FOUND = 23 +} + +/** + * Error options for error handling + */ +export interface ErrorOptions { + /** + * Error level + */ + level?: ErrorLevel; + + /** + * Error category + */ + category?: ErrorCategory; + + /** + * Additional context for the error + */ + context?: Record; + + /** + * Whether to report the error to monitoring systems + */ + report?: boolean; + + /** + * User message to display + */ + userMessage?: string; + + /** + * Suggested resolution steps + */ + resolution?: string | string[]; +} + +/** + * User error options + */ +export interface UserErrorOptions { + /** + * Original error that caused this error + */ + cause?: unknown; + + /** + * Error category + */ + category?: ErrorCategory; + + /** + * Error level + */ + level?: ErrorLevel; + + /** + * Hint on how to resolve the error + */ + resolution?: string | string[]; + + /** + * Additional details about the error + */ + details?: Record; + + /** + * Error code + */ + code?: string; +} + +/** + * User error + */ +export class UserError extends Error { + /** + * Original error that caused this error + */ + cause?: unknown; + + /** + * Error category + */ + category: ErrorCategory; + + /** + * Error level + */ + level: ErrorLevel; + + /** + * Hint on how to resolve the error + */ + resolution?: string | string[]; + + /** + * Additional details about the error + */ + details: Record; + + /** + * Error code + */ + code?: string; + + /** + * Create a new user error + */ + constructor(message: string, options: UserErrorOptions = {}) { + super(message); + + this.name = 'UserError'; + this.cause = options.cause; + this.category = options.category || ErrorCategory.UNKNOWN; + this.level = options.level || ErrorLevel.ERROR; + this.resolution = options.resolution; + this.details = options.details || {}; + this.code = options.code; + + // Capture stack trace + Error.captureStackTrace?.(this, UserError); + } +} \ No newline at end of file diff --git a/claude-code/src/execution/index.ts b/claude-code/src/execution/index.ts new file mode 100644 index 0000000..7f40fef --- /dev/null +++ b/claude-code/src/execution/index.ts @@ -0,0 +1,414 @@ +/** + * Execution Environment Module + * + * Provides functionality for executing shell commands and scripts + * in a controlled environment with proper error handling. + */ + +import { exec, spawn } from 'child_process'; +import { logger } from '../utils/logger.js'; +import { createUserError } from '../errors/formatter.js'; +import { ErrorCategory } from '../errors/types.js'; +import { Timeout } from '../utils/types.js'; + +/** + * Result of a command execution + */ +interface ExecutionResult { + output: string; + exitCode: number; + error?: Error; + command: string; + duration: number; +} + +/** + * Command execution options + */ +interface ExecutionOptions { + cwd?: string; + env?: Record; + timeout?: number; + shell?: string; + maxBuffer?: number; + captureStderr?: boolean; +} + +/** + * Background process options + */ +interface BackgroundProcessOptions extends ExecutionOptions { + onOutput?: (output: string) => void; + onError?: (error: string) => void; + onExit?: (code: number | null) => void; +} + +/** + * Background process handle + */ +interface BackgroundProcess { + pid: number; + kill: () => boolean; + isRunning: boolean; +} + +/** + * List of dangerous commands that shouldn't be executed + */ +const DANGEROUS_COMMANDS = [ + /^\s*rm\s+(-rf?|--recursive)\s+[\/~]/i, // rm -rf / or similar + /^\s*dd\s+.*of=\/dev\/(disk|hd|sd)/i, // dd to a device + /^\s*mkfs/i, // Format a filesystem + /^\s*:\(\)\{\s*:\|:\s*&\s*\}\s*;/, // Fork bomb + /^\s*>(\/dev\/sd|\/dev\/hd)/, // Overwrite disk device + /^\s*sudo\s+.*(rm|mkfs|dd|chmod|chown)/i // sudo with dangerous commands +]; + +/** + * Maximum command execution time (30 seconds by default) + */ +const DEFAULT_TIMEOUT = 30000; + +/** + * Maximum output buffer size (5MB by default) + */ +const DEFAULT_MAX_BUFFER = 5 * 1024 * 1024; + +/** + * Execution environment manager + */ +class ExecutionEnvironment { + private config: any; + private backgroundProcesses: Map = new Map(); + private executionCount: number = 0; + private workingDirectory: string; + private environmentVariables: Record; + + /** + * Create a new execution environment + */ + constructor(config: any) { + this.config = config; + this.workingDirectory = config.execution?.cwd || process.cwd(); + + // Set up environment variables + this.environmentVariables = { + ...process.env as Record, + CLAUDE_CODE_VERSION: config.version || '0.2.29', + NODE_ENV: config.env || 'production', + ...(config.execution?.env || {}) + }; + + logger.debug('Execution environment created', { + workingDirectory: this.workingDirectory + }); + } + + /** + * Initialize the execution environment + */ + async initialize(): Promise { + logger.info('Initializing execution environment'); + + try { + // Verify shell is available + const shell = this.config.execution?.shell || process.env.SHELL || 'bash'; + + await this.executeCommand(`${shell} -c "echo Shell is available"`, { + timeout: 5000 + }); + + logger.info('Execution environment initialized successfully'); + } catch (error) { + logger.error('Failed to initialize execution environment', error); + throw createUserError('Failed to initialize command execution environment', { + cause: error, + category: ErrorCategory.COMMAND_EXECUTION, + resolution: 'Check that your shell is properly configured' + }); + } + } + + /** + * Execute a shell command + */ + async executeCommand(command: string, options: ExecutionOptions = {}): Promise { + // Increment execution count + this.executionCount++; + + // Validate command for safety + this.validateCommand(command); + + const cwd = options.cwd || this.workingDirectory; + const env = { ...this.environmentVariables, ...(options.env || {}) }; + const timeout = options.timeout || DEFAULT_TIMEOUT; + const maxBuffer = options.maxBuffer || DEFAULT_MAX_BUFFER; + const shell = options.shell || this.config.execution?.shell || process.env.SHELL || 'bash'; + const captureStderr = options.captureStderr !== false; + + logger.debug('Executing command', { + command, + cwd, + shell, + timeout, + executionCount: this.executionCount + }); + + const startTime = Date.now(); + + return new Promise((resolve, reject) => { + exec(command, { + cwd, + env, + timeout, + maxBuffer, + shell, + windowsHide: true, + encoding: 'utf8' + }, (error: Error | null, stdout: string, stderr: string) => { + const duration = Date.now() - startTime; + + // Combine stdout and stderr if requested + const output = captureStderr ? `${stdout}${stderr ? stderr : ''}` : stdout; + + if (error) { + logger.error(`Command execution failed: ${command}`, { + error: error.message, + exitCode: (error as any).code, + duration + }); + + // Format the error result + resolve({ + output, + exitCode: (error as any).code || 1, + error, + command, + duration + }); + } else { + logger.debug(`Command executed successfully: ${command}`, { + duration, + outputLength: output.length + }); + + resolve({ + output, + exitCode: 0, + command, + duration + }); + } + }); + }); + } + + /** + * Execute a command in the background + */ + executeCommandInBackground(command: string, options: BackgroundProcessOptions = {}): BackgroundProcess { + // Validate command for safety + this.validateCommand(command); + + const cwd = options.cwd || this.workingDirectory; + const env = { ...this.environmentVariables, ...(options.env || {}) }; + const shell = options.shell || this.config.execution?.shell || process.env.SHELL || 'bash'; + + logger.debug('Executing command in background', { + command, + cwd, + shell + }); + + // Spawn the process + const childProcess = spawn(command, [], { + cwd, + env, + shell, + detached: true, + stdio: ['ignore', 'pipe', 'pipe'] + }); + + const pid = childProcess.pid!; + let isRunning = true; + + // Set up output handlers + if (childProcess.stdout) { + childProcess.stdout.on('data', (data: Buffer) => { + const output = data.toString('utf8'); + logger.debug(`Background command (pid ${pid}) output:`, { output }); + + if (options.onOutput) { + options.onOutput(output); + } + }); + } + + if (childProcess.stderr) { + childProcess.stderr.on('data', (data: Buffer) => { + const errorOutput = data.toString('utf8'); + logger.debug(`Background command (pid ${pid}) error:`, { errorOutput }); + + if (options.onError) { + options.onError(errorOutput); + } + }); + } + + // Set up exit handler + childProcess.on('exit', (code) => { + isRunning = false; + logger.debug(`Background command (pid ${pid}) exited with code ${code}`); + + // Remove from tracked processes + this.backgroundProcesses.delete(pid); + + if (options.onExit) { + options.onExit(code); + } + }); + + // Create the process handle + const backgroundProcess: BackgroundProcess = { + pid, + kill: () => { + if (isRunning) { + childProcess.kill(); + isRunning = false; + this.backgroundProcesses.delete(pid); + return true; + } + return false; + }, + isRunning: true + }; + + // Track the process + this.backgroundProcesses.set(pid, backgroundProcess); + + return backgroundProcess; + } + + /** + * Kill all running background processes + */ + killAllBackgroundProcesses(): void { + logger.info(`Killing ${this.backgroundProcesses.size} background processes`); + + for (const process of this.backgroundProcesses.values()) { + try { + process.kill(); + } catch (error) { + logger.warn(`Failed to kill process ${process.pid}`, error); + } + } + + this.backgroundProcesses.clear(); + } + + /** + * Validate a command for safety + */ + private validateCommand(command: string): void { + // Check if command is in the denied list + for (const pattern of DANGEROUS_COMMANDS) { + if (pattern.test(command)) { + throw createUserError(`Command execution blocked: '${command}' matches dangerous pattern`, { + category: ErrorCategory.COMMAND_EXECUTION, + resolution: 'This command is blocked for safety reasons. Please use a different command.' + }); + } + } + + // Check if command is in allowed list (if configured) + if (this.config.execution?.allowedCommands && this.config.execution.allowedCommands.length > 0) { + const allowed = this.config.execution.allowedCommands.some( + (allowedPattern: string | RegExp) => { + if (typeof allowedPattern === 'string') { + return command.startsWith(allowedPattern); + } else { + return allowedPattern.test(command); + } + } + ); + + if (!allowed) { + throw createUserError(`Command execution blocked: '${command}' is not in the allowed list`, { + category: ErrorCategory.COMMAND_EXECUTION, + resolution: 'This command is not allowed by your configuration.' + }); + } + } + } + + /** + * Set the working directory + */ + setWorkingDirectory(directory: string): void { + this.workingDirectory = directory; + logger.debug(`Working directory set to: ${directory}`); + } + + /** + * Get the working directory + */ + getWorkingDirectory(): string { + return this.workingDirectory; + } + + /** + * Set an environment variable + */ + setEnvironmentVariable(name: string, value: string): void { + this.environmentVariables[name] = value; + logger.debug(`Environment variable set: ${name}=${value}`); + } + + /** + * Get an environment variable + */ + getEnvironmentVariable(name: string): string | undefined { + return this.environmentVariables[name]; + } +} + +/** + * Initialize the execution environment + */ +export async function initExecutionEnvironment(config: any): Promise { + logger.info('Initializing execution environment'); + + try { + const executionEnv = new ExecutionEnvironment(config); + await executionEnv.initialize(); + + logger.info('Execution environment initialized successfully'); + + return executionEnv; + } catch (error) { + logger.error('Failed to initialize execution environment', error); + + // Return a minimal execution environment even if initialization failed + return new ExecutionEnvironment(config); + } +} + +// Set up cleanup on process exit +function setupProcessCleanup(executionEnv: ExecutionEnvironment): void { + process.on('exit', () => { + executionEnv.killAllBackgroundProcesses(); + }); + + process.on('SIGINT', () => { + executionEnv.killAllBackgroundProcesses(); + process.exit(0); + }); + + process.on('SIGTERM', () => { + executionEnv.killAllBackgroundProcesses(); + process.exit(0); + }); +} + +export { ExecutionResult, ExecutionOptions, BackgroundProcess, BackgroundProcessOptions }; +export default ExecutionEnvironment; \ No newline at end of file diff --git a/claude-code/src/fileops/index.ts b/claude-code/src/fileops/index.ts new file mode 100644 index 0000000..2cc3ad0 --- /dev/null +++ b/claude-code/src/fileops/index.ts @@ -0,0 +1,517 @@ +/** + * File Operations Module + * + * Provides utilities for reading, writing, and manipulating files + * with proper error handling and security considerations. + */ + +import { promises as fs } from 'fs'; +import path from 'path'; +import { logger } from '../utils/logger.js'; +import { createUserError } from '../errors/formatter.js'; +import { ErrorCategory } from '../errors/types.js'; +import { ErrnoException } from '../utils/types.js'; + +/** + * Result of a file operation + */ +interface FileOperationResult { + success: boolean; + error?: Error; + path?: string; + content?: string; + created?: boolean; +} + +/** + * File Operations Manager + */ +class FileOperationsManager { + private config: any; + private workspacePath: string; + + /** + * Create a new file operations manager + */ + constructor(config: any) { + this.config = config; + this.workspacePath = config.workspacePath || process.cwd(); + + logger.debug('File operations manager created', { + workspacePath: this.workspacePath + }); + } + + /** + * Initialize file operations + */ + async initialize(): Promise { + logger.info('Initializing file operations manager'); + + try { + // Verify workspace directory exists + const stats = await fs.stat(this.workspacePath); + + if (!stats.isDirectory()) { + throw createUserError(`Workspace path is not a directory: ${this.workspacePath}`, { + category: ErrorCategory.FILE_SYSTEM + }); + } + + logger.info('File operations manager initialized'); + } catch (error) { + if ((error as ErrnoException).code === 'ENOENT') { + throw createUserError(`Workspace directory does not exist: ${this.workspacePath}`, { + category: ErrorCategory.FILE_SYSTEM, + resolution: 'Please provide a valid workspace path' + }); + } + + logger.error('Failed to initialize file operations manager', error); + throw createUserError('Failed to initialize file operations', { + cause: error, + category: ErrorCategory.FILE_SYSTEM + }); + } + } + + /** + * Get absolute path relative to workspace + */ + getAbsolutePath(relativePath: string): string { + // Clean up path to prevent directory traversal attacks + const normalizedPath = path.normalize(relativePath).replace(/^(\.\.(\/|\\|$))+/, ''); + return path.resolve(this.workspacePath, normalizedPath); + } + + /** + * Get relative path from workspace + */ + getRelativePath(absolutePath: string): string { + return path.relative(this.workspacePath, absolutePath); + } + + /** + * Read a file + */ + async readFile(filePath: string): Promise { + const absolutePath = this.getAbsolutePath(filePath); + + logger.debug('Reading file', { path: filePath, absolutePath }); + + try { + // Verify file exists and is a file + const stats = await fs.stat(absolutePath); + + if (!stats.isFile()) { + return { + success: false, + error: createUserError(`Not a file: ${filePath}`, { + category: ErrorCategory.FILE_SYSTEM + }) + }; + } + + // Check file size + const maxSizeBytes = this.config.fileOps?.maxReadSizeBytes || 10 * 1024 * 1024; // 10MB default + + if (stats.size > maxSizeBytes) { + return { + success: false, + error: createUserError(`File too large to read: ${filePath} (${stats.size} bytes)`, { + category: ErrorCategory.FILE_SYSTEM, + resolution: 'Try reading a smaller file or use a text editor to open this file' + }) + }; + } + + // Read file content + const content = await fs.readFile(absolutePath, 'utf8'); + + return { + success: true, + path: filePath, + content + }; + } catch (error) { + logger.error(`Error reading file: ${filePath}`, error); + + const errnoError = error as ErrnoException; + + if (errnoError.code === 'ENOENT') { + return { + success: false, + error: createUserError(`File not found: ${filePath}`, { + category: ErrorCategory.FILE_SYSTEM, + resolution: 'Check that the file exists and the path is correct' + }) + }; + } + + if (errnoError.code === 'EACCES') { + return { + success: false, + error: createUserError(`Permission denied reading file: ${filePath}`, { + category: ErrorCategory.FILE_SYSTEM, + resolution: 'Check file permissions or try running with elevated privileges' + }) + }; + } + + return { + success: false, + error: createUserError(`Failed to read file: ${filePath}`, { + cause: error, + category: ErrorCategory.FILE_SYSTEM + }) + }; + } + } + + /** + * Write a file + */ + async writeFile(filePath: string, content: string, options: { createDirs?: boolean } = {}): Promise { + const absolutePath = this.getAbsolutePath(filePath); + + logger.debug('Writing file', { + path: filePath, + absolutePath, + contentLength: content.length, + createDirs: options.createDirs + }); + + try { + // Check if file exists + let fileExists = false; + let isCreating = false; + + try { + const stats = await fs.stat(absolutePath); + fileExists = stats.isFile(); + } catch (error) { + const errnoError = error as ErrnoException; + + if (errnoError.code === 'ENOENT') { + isCreating = true; + + // Create directories if requested + if (options.createDirs) { + const dirPath = path.dirname(absolutePath); + await fs.mkdir(dirPath, { recursive: true }); + } + } else { + throw error; + } + } + + // Write file content + await fs.writeFile(absolutePath, content, 'utf8'); + + return { + success: true, + path: filePath, + created: isCreating + }; + } catch (error) { + logger.error(`Error writing file: ${filePath}`, error); + + const errnoError = error as ErrnoException; + + if (errnoError.code === 'ENOENT') { + return { + success: false, + error: createUserError(`Directory does not exist: ${path.dirname(filePath)}`, { + category: ErrorCategory.FILE_SYSTEM, + resolution: 'Use the createDirs option to create parent directories' + }) + }; + } + + if (errnoError.code === 'EACCES') { + return { + success: false, + error: createUserError(`Permission denied writing file: ${filePath}`, { + category: ErrorCategory.FILE_SYSTEM, + resolution: 'Check file permissions or try running with elevated privileges' + }) + }; + } + + return { + success: false, + error: createUserError(`Failed to write file: ${filePath}`, { + cause: error, + category: ErrorCategory.FILE_SYSTEM + }) + }; + } + } + + /** + * Delete a file + */ + async deleteFile(filePath: string): Promise { + const absolutePath = this.getAbsolutePath(filePath); + + logger.debug('Deleting file', { path: filePath, absolutePath }); + + try { + // Verify file exists and is a file + const stats = await fs.stat(absolutePath); + + if (!stats.isFile()) { + return { + success: false, + error: createUserError(`Not a file: ${filePath}`, { + category: ErrorCategory.FILE_SYSTEM + }) + }; + } + + // Delete file + await fs.unlink(absolutePath); + + return { + success: true, + path: filePath + }; + } catch (error) { + logger.error(`Error deleting file: ${filePath}`, error); + + const errnoError = error as ErrnoException; + + if (errnoError.code === 'ENOENT') { + return { + success: false, + error: createUserError(`File not found: ${filePath}`, { + category: ErrorCategory.FILE_SYSTEM + }) + }; + } + + if (errnoError.code === 'EACCES') { + return { + success: false, + error: createUserError(`Permission denied deleting file: ${filePath}`, { + category: ErrorCategory.FILE_SYSTEM, + resolution: 'Check file permissions or try running with elevated privileges' + }) + }; + } + + return { + success: false, + error: createUserError(`Failed to delete file: ${filePath}`, { + cause: error, + category: ErrorCategory.FILE_SYSTEM + }) + }; + } + } + + /** + * Check if a file exists + */ + async fileExists(filePath: string): Promise { + const absolutePath = this.getAbsolutePath(filePath); + + try { + const stats = await fs.stat(absolutePath); + return stats.isFile(); + } catch (error) { + return false; + } + } + + /** + * Create a directory + */ + async createDirectory(dirPath: string, options: { recursive?: boolean } = {}): Promise { + const absolutePath = this.getAbsolutePath(dirPath); + + logger.debug('Creating directory', { + path: dirPath, + absolutePath, + recursive: options.recursive + }); + + try { + // Create directory + await fs.mkdir(absolutePath, { recursive: options.recursive !== false }); + + return { + success: true, + path: dirPath + }; + } catch (error) { + logger.error(`Error creating directory: ${dirPath}`, error); + + const errnoError = error as ErrnoException; + + if (errnoError.code === 'EEXIST') { + return { + success: false, + error: createUserError(`Directory already exists: ${dirPath}`, { + category: ErrorCategory.FILE_SYSTEM + }) + }; + } + + if (errnoError.code === 'EACCES') { + return { + success: false, + error: createUserError(`Permission denied creating directory: ${dirPath}`, { + category: ErrorCategory.FILE_SYSTEM, + resolution: 'Check file permissions or try running with elevated privileges' + }) + }; + } + + return { + success: false, + error: createUserError(`Failed to create directory: ${dirPath}`, { + cause: error, + category: ErrorCategory.FILE_SYSTEM + }) + }; + } + } + + /** + * List directory contents + */ + async listDirectory(dirPath: string): Promise { + const absolutePath = this.getAbsolutePath(dirPath); + + logger.debug('Listing directory', { path: dirPath, absolutePath }); + + try { + // Verify directory exists and is a directory + const stats = await fs.stat(absolutePath); + + if (!stats.isDirectory()) { + return { + success: false, + error: createUserError(`Not a directory: ${dirPath}`, { + category: ErrorCategory.FILE_SYSTEM + }) + }; + } + + // Read directory contents + const files = await fs.readdir(absolutePath); + + return { + success: true, + path: dirPath, + files + }; + } catch (error) { + logger.error(`Error listing directory: ${dirPath}`, error); + + const errnoError = error as ErrnoException; + + if (errnoError.code === 'ENOENT') { + return { + success: false, + error: createUserError(`Directory not found: ${dirPath}`, { + category: ErrorCategory.FILE_SYSTEM + }) + }; + } + + if (errnoError.code === 'EACCES') { + return { + success: false, + error: createUserError(`Permission denied listing directory: ${dirPath}`, { + category: ErrorCategory.FILE_SYSTEM, + resolution: 'Check directory permissions or try running with elevated privileges' + }) + }; + } + + return { + success: false, + error: createUserError(`Failed to list directory: ${dirPath}`, { + cause: error, + category: ErrorCategory.FILE_SYSTEM + }) + }; + } + } + + /** + * Generate a diff between two strings + */ + generateDiff(original: string, modified: string): string { + // A simple line-by-line diff implementation + // In a real implementation, this would use a proper diff library + + const originalLines = original.split('\n'); + const modifiedLines = modified.split('\n'); + const diff: string[] = []; + + let i = 0, j = 0; + + while (i < originalLines.length || j < modifiedLines.length) { + if (i >= originalLines.length) { + // All remaining lines in modified are additions + diff.push(`+ ${modifiedLines[j]}`); + j++; + } else if (j >= modifiedLines.length) { + // All remaining lines in original are deletions + diff.push(`- ${originalLines[i]}`); + i++; + } else if (originalLines[i] === modifiedLines[j]) { + // Lines are the same + diff.push(` ${originalLines[i]}`); + i++; + j++; + } else { + // Lines differ + // Simple approach: treat as a deletion and addition + // A more sophisticated diff would detect changes within lines + diff.push(`- ${originalLines[i]}`); + diff.push(`+ ${modifiedLines[j]}`); + i++; + j++; + } + } + + return diff.join('\n'); + } + + /** + * Apply a patch to a file + */ + async applyPatch(filePath: string, patch: string): Promise { + // In a real implementation, this would parse and apply a unified diff + // For simplicity, we'll just write the patched content directly + + return this.writeFile(filePath, patch); + } +} + +/** + * Initialize the file operations system + */ +export async function initFileOperations(config: any): Promise { + logger.info('Initializing file operations system'); + + try { + const fileOps = new FileOperationsManager(config); + await fileOps.initialize(); + + logger.info('File operations system initialized successfully'); + + return fileOps; + } catch (error) { + logger.error('Failed to initialize file operations system', error); + + // Create a basic file operations manager even if initialization failed + return new FileOperationsManager(config); + } +} + +export default FileOperationsManager; \ No newline at end of file diff --git a/claude-code/src/fs/operations.ts b/claude-code/src/fs/operations.ts new file mode 100644 index 0000000..649f40c --- /dev/null +++ b/claude-code/src/fs/operations.ts @@ -0,0 +1,548 @@ +/** + * File Operations + * + * Functions for interacting with the file system in a safe and consistent way. + * Includes utilities for reading, writing, searching, and analyzing files. + */ + +import fs from 'fs/promises'; +import { Stats } from 'fs'; +import path from 'path'; +import { createReadStream, createWriteStream } from 'fs'; +import { pipeline } from 'stream/promises'; +import { constants } from 'fs'; +import { createUserError } from '../errors/formatter.js'; +import { ErrorCategory } from '../errors/types.js'; +import { logger } from '../utils/logger.js'; +import { isValidPath, isValidFilePath, isValidDirectoryPath } from '../utils/validation.js'; + +/** + * Check if a file exists + */ +export async function fileExists(filePath: string): Promise { + try { + const stats = await fs.stat(filePath); + return stats.isFile(); + } catch (error) { + return false; + } +} + +/** + * Check if a directory exists + */ +export async function directoryExists(dirPath: string): Promise { + try { + const stats = await fs.stat(dirPath); + return stats.isDirectory(); + } catch (error) { + return false; + } +} + +/** + * Create a directory if it doesn't exist + */ +export async function ensureDirectory(dirPath: string): Promise { + try { + if (!await directoryExists(dirPath)) { + await fs.mkdir(dirPath, { recursive: true }); + logger.debug(`Created directory: ${dirPath}`); + } + } catch (error) { + logger.error(`Failed to create directory: ${dirPath}`, error); + throw createUserError(`Failed to create directory: ${dirPath}`, { + cause: error, + category: ErrorCategory.FILE_SYSTEM, + resolution: 'Check file permissions and try again.' + }); + } +} + +/** + * Read a file as text + */ +export async function readTextFile(filePath: string, encoding: BufferEncoding = 'utf-8'): Promise { + if (!isValidFilePath(filePath)) { + throw createUserError(`Invalid file path: ${filePath}`, { + category: ErrorCategory.VALIDATION, + resolution: 'Provide a valid file path.' + }); + } + + try { + if (!await fileExists(filePath)) { + throw createUserError(`File not found: ${filePath}`, { + category: ErrorCategory.FILE_NOT_FOUND, + resolution: 'Check the file path and try again.' + }); + } + + return await fs.readFile(filePath, { encoding }); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + throw createUserError(`File not found: ${filePath}`, { + cause: error, + category: ErrorCategory.FILE_NOT_FOUND, + resolution: 'Check the file path and try again.' + }); + } + + throw createUserError(`Failed to read file: ${filePath}`, { + cause: error, + category: ErrorCategory.FILE_READ, + resolution: 'Check file permissions and try again.' + }); + } +} + +/** + * Read specific lines from a file + */ +export async function readFileLines( + filePath: string, + start: number, + end: number, + encoding: BufferEncoding = 'utf-8' +): Promise { + try { + const content = await readTextFile(filePath, encoding); + const lines = content.split('\n'); + + // Convert from 1-indexed to 0-indexed + const startIndex = Math.max(0, start - 1); + const endIndex = Math.min(lines.length, end); + + return lines.slice(startIndex, endIndex); + } catch (error) { + throw createUserError(`Failed to read lines ${start}-${end} from file: ${filePath}`, { + cause: error, + category: ErrorCategory.FILE_READ, + resolution: 'Check the file path and line range, then try again.' + }); + } +} + +/** + * Write text to a file + */ +export async function writeTextFile( + filePath: string, + content: string, + options: { encoding?: BufferEncoding; createDir?: boolean; overwrite?: boolean } = {} +): Promise { + const { encoding = 'utf-8', createDir = true, overwrite = true } = options; + + if (!isValidFilePath(filePath)) { + throw createUserError(`Invalid file path: ${filePath}`, { + category: ErrorCategory.VALIDATION, + resolution: 'Provide a valid file path.' + }); + } + + try { + // Ensure directory exists if createDir is true + if (createDir) { + const dirPath = path.dirname(filePath); + await ensureDirectory(dirPath); + } + + // Check if file exists and overwrite is false + const exists = await fileExists(filePath); + if (exists && !overwrite) { + throw createUserError(`File already exists: ${filePath}`, { + category: ErrorCategory.FILE_SYSTEM, + resolution: 'Use overwrite option to replace existing file.' + }); + } + + // Write the file + await fs.writeFile(filePath, content, { encoding }); + logger.debug(`Wrote ${content.length} bytes to: ${filePath}`); + } catch (error) { + if ((error as any).category) { + throw error; // Re-throw user errors + } + + throw createUserError(`Failed to write file: ${filePath}`, { + cause: error, + category: ErrorCategory.FILE_WRITE, + resolution: 'Check file permissions and try again.' + }); + } +} + +/** + * Append text to a file + */ +export async function appendTextFile( + filePath: string, + content: string, + options: { encoding?: BufferEncoding; createDir?: boolean } = {} +): Promise { + const { encoding = 'utf-8', createDir = true } = options; + + if (!isValidFilePath(filePath)) { + throw createUserError(`Invalid file path: ${filePath}`, { + category: ErrorCategory.VALIDATION, + resolution: 'Provide a valid file path.' + }); + } + + try { + // Ensure directory exists if createDir is true + if (createDir) { + const dirPath = path.dirname(filePath); + await ensureDirectory(dirPath); + } + + // Append to the file + await fs.appendFile(filePath, content, { encoding }); + logger.debug(`Appended ${content.length} bytes to: ${filePath}`); + } catch (error) { + throw createUserError(`Failed to append to file: ${filePath}`, { + cause: error, + category: ErrorCategory.FILE_WRITE, + resolution: 'Check file permissions and try again.' + }); + } +} + +/** + * Delete a file + */ +export async function deleteFile(filePath: string): Promise { + if (!isValidFilePath(filePath)) { + throw createUserError(`Invalid file path: ${filePath}`, { + category: ErrorCategory.VALIDATION, + resolution: 'Provide a valid file path.' + }); + } + + try { + const exists = await fileExists(filePath); + if (!exists) { + logger.debug(`File does not exist, nothing to delete: ${filePath}`); + return; + } + + await fs.unlink(filePath); + logger.debug(`Deleted file: ${filePath}`); + } catch (error) { + throw createUserError(`Failed to delete file: ${filePath}`, { + cause: error, + category: ErrorCategory.FILE_SYSTEM, + resolution: 'Check file permissions and try again.' + }); + } +} + +/** + * Rename a file or directory + */ +export async function rename(oldPath: string, newPath: string): Promise { + if (!isValidPath(oldPath) || !isValidPath(newPath)) { + throw createUserError(`Invalid path: ${!isValidPath(oldPath) ? oldPath : newPath}`, { + category: ErrorCategory.VALIDATION, + resolution: 'Provide valid file paths.' + }); + } + + try { + const exists = await fileExists(oldPath) || await directoryExists(oldPath); + if (!exists) { + throw createUserError(`Path not found: ${oldPath}`, { + category: ErrorCategory.FILE_NOT_FOUND, + resolution: 'Check the source path and try again.' + }); + } + + await fs.rename(oldPath, newPath); + logger.debug(`Renamed: ${oldPath} -> ${newPath}`); + } catch (error) { + if ((error as any).category) { + throw error; // Re-throw user errors + } + + throw createUserError(`Failed to rename: ${oldPath} -> ${newPath}`, { + cause: error, + category: ErrorCategory.FILE_SYSTEM, + resolution: 'Check file permissions and ensure destination path is valid.' + }); + } +} + +/** + * Copy a file + */ +export async function copyFile( + sourcePath: string, + destPath: string, + options: { overwrite?: boolean; createDir?: boolean } = {} +): Promise { + const { overwrite = false, createDir = true } = options; + + if (!isValidFilePath(sourcePath) || !isValidFilePath(destPath)) { + throw createUserError(`Invalid file path: ${!isValidFilePath(sourcePath) ? sourcePath : destPath}`, { + category: ErrorCategory.VALIDATION, + resolution: 'Provide valid file paths.' + }); + } + + try { + // Check if source exists + if (!await fileExists(sourcePath)) { + throw createUserError(`Source file not found: ${sourcePath}`, { + category: ErrorCategory.FILE_NOT_FOUND, + resolution: 'Check the source path and try again.' + }); + } + + // Ensure directory exists if createDir is true + if (createDir) { + const dirPath = path.dirname(destPath); + await ensureDirectory(dirPath); + } + + // Set copy flags + const flags = overwrite ? 0 : constants.COPYFILE_EXCL; + + await fs.copyFile(sourcePath, destPath, flags); + logger.debug(`Copied file: ${sourcePath} -> ${destPath}`); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'EEXIST' && !overwrite) { + throw createUserError(`Destination file already exists: ${destPath}`, { + cause: error, + category: ErrorCategory.FILE_SYSTEM, + resolution: 'Use overwrite option to replace existing file.' + }); + } + + if ((error as any).category) { + throw error; // Re-throw user errors + } + + throw createUserError(`Failed to copy file: ${sourcePath} -> ${destPath}`, { + cause: error, + category: ErrorCategory.FILE_SYSTEM, + resolution: 'Check file permissions and paths, then try again.' + }); + } +} + +/** + * List files and directories in a directory + */ +export async function listDirectory(dirPath: string): Promise { + if (!isValidDirectoryPath(dirPath)) { + throw createUserError(`Invalid directory path: ${dirPath}`, { + category: ErrorCategory.VALIDATION, + resolution: 'Provide a valid directory path.' + }); + } + + try { + if (!await directoryExists(dirPath)) { + throw createUserError(`Directory not found: ${dirPath}`, { + category: ErrorCategory.FILE_NOT_FOUND, + resolution: 'Check the directory path and try again.' + }); + } + + return await fs.readdir(dirPath); + } catch (error) { + if ((error as any).category) { + throw error; // Re-throw user errors + } + + throw createUserError(`Failed to list directory: ${dirPath}`, { + cause: error, + category: ErrorCategory.FILE_SYSTEM, + resolution: 'Check directory permissions and try again.' + }); + } +} + +/** + * Get file or directory information + */ +export async function getFileInfo(filePath: string): Promise { + if (!isValidPath(filePath)) { + throw createUserError(`Invalid path: ${filePath}`, { + category: ErrorCategory.VALIDATION, + resolution: 'Provide a valid file or directory path.' + }); + } + + try { + return await fs.stat(filePath); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + throw createUserError(`Path not found: ${filePath}`, { + cause: error, + category: ErrorCategory.FILE_NOT_FOUND, + resolution: 'Check the path and try again.' + }); + } + + throw createUserError(`Failed to get file info: ${filePath}`, { + cause: error, + category: ErrorCategory.FILE_SYSTEM, + resolution: 'Check permissions and try again.' + }); + } +} + +/** + * Find files matching a pattern + */ +export async function findFiles( + directory: string, + options: { pattern?: RegExp; recursive?: boolean; includeDirectories?: boolean } = {} +): Promise { + const { pattern, recursive = true, includeDirectories = false } = options; + + if (!isValidDirectoryPath(directory)) { + throw createUserError(`Invalid directory path: ${directory}`, { + category: ErrorCategory.VALIDATION, + resolution: 'Provide a valid directory path.' + }); + } + + try { + if (!await directoryExists(directory)) { + throw createUserError(`Directory not found: ${directory}`, { + category: ErrorCategory.FILE_NOT_FOUND, + resolution: 'Check the directory path and try again.' + }); + } + + const results: string[] = []; + + // Helper function to traverse the directory + async function traverseDirectory(dir: string): Promise { + const entries = await fs.readdir(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + + if (entry.isDirectory()) { + if (includeDirectories && (!pattern || pattern.test(entry.name))) { + results.push(fullPath); + } + + if (recursive) { + await traverseDirectory(fullPath); + } + } else if (entry.isFile()) { + if (!pattern || pattern.test(entry.name)) { + results.push(fullPath); + } + } + } + } + + await traverseDirectory(directory); + return results; + } catch (error) { + if ((error as any).category) { + throw error; // Re-throw user errors + } + + throw createUserError(`Failed to find files in directory: ${directory}`, { + cause: error, + category: ErrorCategory.FILE_SYSTEM, + resolution: 'Check directory permissions and try again.' + }); + } +} + +/** + * Stream a file to another location + */ +export async function streamFile( + sourcePath: string, + destPath: string, + options: { overwrite?: boolean; createDir?: boolean } = {} +): Promise { + const { overwrite = false, createDir = true } = options; + + if (!isValidFilePath(sourcePath) || !isValidFilePath(destPath)) { + throw createUserError(`Invalid file path: ${!isValidFilePath(sourcePath) ? sourcePath : destPath}`, { + category: ErrorCategory.VALIDATION, + resolution: 'Provide valid file paths.' + }); + } + + try { + // Check if source exists + if (!await fileExists(sourcePath)) { + throw createUserError(`Source file not found: ${sourcePath}`, { + category: ErrorCategory.FILE_NOT_FOUND, + resolution: 'Check the source path and try again.' + }); + } + + // Check if destination exists and overwrite is false + if (!overwrite && await fileExists(destPath)) { + throw createUserError(`Destination file already exists: ${destPath}`, { + category: ErrorCategory.FILE_SYSTEM, + resolution: 'Use overwrite option to replace existing file.' + }); + } + + // Ensure directory exists if createDir is true + if (createDir) { + const dirPath = path.dirname(destPath); + await ensureDirectory(dirPath); + } + + const source = createReadStream(sourcePath); + const destination = createWriteStream(destPath); + + await pipeline(source, destination); + logger.debug(`Streamed file: ${sourcePath} -> ${destPath}`); + } catch (error) { + if ((error as any).category) { + throw error; // Re-throw user errors + } + + throw createUserError(`Failed to stream file: ${sourcePath} -> ${destPath}`, { + cause: error, + category: ErrorCategory.FILE_SYSTEM, + resolution: 'Check file permissions and paths, then try again.' + }); + } +} + +/** + * Create a temporary file + */ +export async function createTempFile( + options: { prefix?: string; suffix?: string; content?: string } = {} +): Promise { + const { prefix = 'tmp-', suffix = '', content = '' } = options; + + try { + // Create a temporary filename + const tempDir = await fs.mkdtemp(path.join(path.resolve(process.env.TEMP || process.env.TMP || '/tmp'), prefix)); + const tempFileName = `${prefix}${Date.now()}${suffix}`; + const tempFilePath = path.join(tempDir, tempFileName); + + // Write content if provided + if (content) { + await fs.writeFile(tempFilePath, content); + } else { + await fs.writeFile(tempFilePath, ''); + } + + logger.debug(`Created temporary file: ${tempFilePath}`); + return tempFilePath; + } catch (error) { + throw createUserError('Failed to create temporary file', { + cause: error, + category: ErrorCategory.FILE_SYSTEM, + resolution: 'Check temporary directory permissions and try again.' + }); + } +} \ No newline at end of file diff --git a/claude-code/src/index.ts b/claude-code/src/index.ts new file mode 100644 index 0000000..d7cd466 --- /dev/null +++ b/claude-code/src/index.ts @@ -0,0 +1,185 @@ +/** + * Claude Code CLI + * + * Main entry point for the application. + * This module bootstraps the application and manages the lifecycle. + */ + +import { loadConfig } from './config/index.js'; +import { initTerminal } from './terminal/index.js'; +import { initAuthentication } from './auth/index.js'; +import { initAI } from './ai/index.js'; +import { initCodebaseAnalysis } from './codebase/index.js'; +import { initCommandProcessor } from './commands/index.js'; +import { initFileOperations } from './fileops/index.js'; +import { initExecutionEnvironment } from './execution/index.js'; +import { initErrorHandling } from './errors/index.js'; +import { initTelemetry } from './telemetry/index.js'; +import { logger } from './utils/logger.js'; + +/** + * Application instance that holds references to all initialized subsystems + */ +export interface AppInstance { + config: any; + terminal: any; + auth: any; + ai: any; + codebase: any; + commands: any; + fileOps: any; + execution: any; + errors: any; + telemetry: any; +} + +/** + * Initialize all application subsystems + */ +export async function initialize(options: any = {}): Promise { + // Set up error handling first + const errors = initErrorHandling(); + + try { + logger.info('Starting Claude Code CLI...'); + + // Load configuration + const config = await loadConfig(options); + + // Initialize terminal interface + const terminal = await initTerminal(config); + + // Initialize authentication + const auth = await initAuthentication(config); + + // Initialize AI client + const ai = await initAI(config, auth); + + // Initialize codebase analysis + const codebase = await initCodebaseAnalysis(config); + + // Initialize file operations + const fileOps = await initFileOperations(config); + + // Initialize execution environment + const execution = await initExecutionEnvironment(config); + + // Initialize command processor + const commands = await initCommandProcessor(config, { + terminal, + auth, + ai, + codebase, + fileOps, + execution, + errors + }); + + // Initialize telemetry if enabled + const telemetry = config.telemetry.enabled + ? await initTelemetry(config) + : null; + + logger.info('Claude Code CLI initialized successfully'); + + return { + config, + terminal, + auth, + ai, + codebase, + commands, + fileOps, + execution, + errors, + telemetry + }; + } catch (error) { + errors.handleFatalError(error); + // This is just to satisfy TypeScript since handleFatalError will exit the process + throw error; + } +} + +/** + * Run the application main loop + */ +export async function run(app: AppInstance): Promise { + try { + // Display welcome message + app.terminal.displayWelcome(); + + // Authenticate if needed + if (!app.auth.isAuthenticated()) { + await app.auth.authenticate(); + } + + // Start codebase analysis in the background + app.codebase.startBackgroundAnalysis(); + + // Enter the main command loop + await app.commands.startCommandLoop(); + + // Clean shutdown + await shutdown(app); + } catch (error) { + app.errors.handleFatalError(error); + } +} + +/** + * Gracefully shut down the application + */ +export async function shutdown(app: AppInstance): Promise { + logger.info('Shutting down Claude Code CLI...'); + + // Stop background tasks and release resources + await app.codebase.stopBackgroundAnalysis(); + + // Submit telemetry if enabled + if (app.telemetry) { + await app.telemetry.submitTelemetry(); + } + + // Disconnect from services + await app.ai.disconnect(); + + logger.info('Claude Code CLI shutdown complete'); +} + +/** + * Handle process signals for clean shutdown + */ +function setupProcessHandlers(app: AppInstance): void { + process.on('SIGINT', async () => { + logger.info('Received SIGINT signal'); + await shutdown(app); + process.exit(0); + }); + + process.on('SIGTERM', async () => { + logger.info('Received SIGTERM signal'); + await shutdown(app); + process.exit(0); + }); + + process.on('unhandledRejection', (reason, promise) => { + logger.error('Unhandled Promise Rejection:', reason); + app.errors.handleUnhandledRejection(reason, promise); + }); + + process.on('uncaughtException', (error) => { + logger.error('Uncaught Exception:', error); + app.errors.handleUncaughtException(error); + process.exit(1); + }); +} + +/** + * Main entry point function + */ +export async function main(options: any = {}): Promise { + const app = await initialize(options); + setupProcessHandlers(app); + await run(app); +} \ No newline at end of file diff --git a/claude-code/src/telemetry/index.ts b/claude-code/src/telemetry/index.ts new file mode 100644 index 0000000..246079d --- /dev/null +++ b/claude-code/src/telemetry/index.ts @@ -0,0 +1,606 @@ +/** + * Telemetry System + * + * Collects anonymous usage data and error reports to help improve the tool. + * Respects user privacy and can be disabled. + */ + +import os from 'os'; +import { v4 as uuidv4 } from 'uuid'; +import { ErrorCategory } from '../errors/types.js'; +import { logger } from '../utils/logger.js'; + +/** + * Telemetry event types + */ +export enum TelemetryEventType { + CLI_START = 'cli_start', + CLI_EXIT = 'cli_exit', + COMMAND_EXECUTE = 'command_execute', + COMMAND_SUCCESS = 'command_success', + COMMAND_ERROR = 'command_error', + AI_REQUEST = 'ai_request', + AI_RESPONSE = 'ai_response', + AI_ERROR = 'ai_error', + AUTH_SUCCESS = 'auth_success', + AUTH_ERROR = 'auth_error' +} + +/** + * Telemetry event + */ +export interface TelemetryEvent { + /** + * Event type + */ + type: TelemetryEventType; + + /** + * Event timestamp + */ + timestamp: string; + + /** + * Event properties + */ + properties: Record; + + /** + * Client information + */ + client: { + /** + * CLI version + */ + version: string; + + /** + * Client ID (anonymous) + */ + id: string; + + /** + * Node.js version + */ + nodeVersion: string; + + /** + * Operating system + */ + os: string; + + /** + * Operating system version + */ + osVersion: string; + }; +} + +/** + * Telemetry configuration + */ +export interface TelemetryConfig { + /** + * Whether telemetry is enabled + */ + enabled: boolean; + + /** + * Client ID (anonymous) + */ + clientId: string; + + /** + * Endpoint for sending telemetry data + */ + endpoint?: string; + + /** + * Additional data to include with all events + */ + additionalData?: Record; +} + +/** + * Default telemetry configuration + */ +const DEFAULT_CONFIG: TelemetryConfig = { + enabled: true, + clientId: '', + endpoint: 'https://telemetry.anthropic.com/claude-code/events' +}; + +/** + * Telemetry manager + */ +class TelemetryManager { + private config: TelemetryConfig; + private initialized = false; + private eventQueue: TelemetryEvent[] = []; + private batchSendTimeout: NodeJS.Timeout | null = null; + private flushPromise: Promise | null = null; + + /** + * Create a new telemetry manager + */ + constructor() { + this.config = { ...DEFAULT_CONFIG }; + } + + /** + * Initialize telemetry + */ + async initialize(userPreferences?: { telemetry?: boolean }): Promise { + if (this.initialized) { + return; + } + + try { + // Check if telemetry is explicitly disabled in user preferences + if (userPreferences?.telemetry === false) { + this.config.enabled = false; + } else if (process.env.CLAUDE_CODE_TELEMETRY === 'false') { + // Also check environment variable + this.config.enabled = false; + } + + // Generate client ID if not already set + if (!this.config.clientId) { + this.config.clientId = this.generateClientId(); + } + + // Get CLI version from package.json + this.config.additionalData = { + ...this.config.additionalData, + cliVersion: process.env.npm_package_version || '0.1.0' + }; + + this.initialized = true; + + // Only log if telemetry is enabled + if (this.config.enabled) { + logger.debug('Telemetry initialized', { clientId: this.config.clientId }); + + // Send CLI start event + this.trackEvent(TelemetryEventType.CLI_START); + + // Setup exit handlers + this.setupExitHandlers(); + } + } catch (error) { + logger.error('Failed to initialize telemetry', error); + this.config.enabled = false; // Disable telemetry on error + } + } + + /** + * Generate an anonymous client ID + */ + private generateClientId(): string { + return uuidv4(); + } + + /** + * Setup process exit handlers to ensure telemetry is sent + */ + private setupExitHandlers(): void { + // Handle normal exit + process.on('exit', () => { + this.trackEvent(TelemetryEventType.CLI_EXIT); + this.flushSync(); + }); + + // Handle SIGINT (Ctrl+C) + process.on('SIGINT', () => { + this.trackEvent(TelemetryEventType.CLI_EXIT, { reason: 'SIGINT' }); + this.flushSync(); + process.exit(0); + }); + + // Handle uncaught exceptions + process.on('uncaughtException', (error) => { + this.trackError(error, { fatal: true }); + this.flushSync(); + }); + } + + /** + * Track an event + */ + trackEvent( + type: TelemetryEventType, + properties: Record = {} + ): void { + if (!this.initialized) { + this.eventQueue.push(this.createEvent(type, properties)); + return; + } + + if (!this.config.enabled) { + return; + } + + const event = this.createEvent(type, properties); + this.queueEvent(event); + } + + /** + * Track a command execution + */ + trackCommand( + commandName: string, + args: Record = {}, + successful: boolean = true + ): void { + // Don't track sensitive commands + if (commandName === 'login' || commandName === 'logout') { + return; + } + + // Create sanitized args (remove sensitive data) + const sanitizedArgs: Record = {}; + + // Only include safe argument types and names + for (const [key, value] of Object.entries(args)) { + // Skip sensitive fields + if (key.includes('key') || key.includes('token') || key.includes('password') || key.includes('secret')) { + continue; + } + + // Include only primitive values and sanitize them + if (typeof value === 'string') { + // Truncate long strings and remove potential sensitive data + sanitizedArgs[key] = value.length > 100 ? `${value.substring(0, 100)}...` : value; + } else if (typeof value === 'number' || typeof value === 'boolean') { + sanitizedArgs[key] = value; + } else if (value === null || value === undefined) { + sanitizedArgs[key] = value; + } else if (Array.isArray(value)) { + sanitizedArgs[key] = `Array(${value.length})`; + } else if (typeof value === 'object') { + sanitizedArgs[key] = 'Object'; + } + } + + const eventType = successful + ? TelemetryEventType.COMMAND_SUCCESS + : TelemetryEventType.COMMAND_ERROR; + + this.trackEvent(eventType, { + command: commandName, + args: sanitizedArgs + }); + } + + /** + * Track an error + */ + trackError( + error: unknown, + context: Record = {} + ): void { + if (!this.config.enabled) { + return; + } + + const errorObj: Record = { + name: error instanceof Error ? error.name : 'UnknownError', + message: error instanceof Error ? error.message : String(error), + category: + error instanceof Error && + 'category' in error ? + (error as any).category : + ErrorCategory.UNKNOWN + }; + + this.trackEvent(TelemetryEventType.COMMAND_ERROR, { + error: errorObj, + ...context + }); + } + + /** + * Create a telemetry event + */ + private createEvent( + type: TelemetryEventType, + properties: Record = {} + ): TelemetryEvent { + // Ensure client info doesn't have any PII + const filteredProperties = { ...properties }; + + // Basic client information + const event: TelemetryEvent = { + type, + timestamp: new Date().toISOString(), + properties: filteredProperties, + client: { + version: this.config.additionalData?.cliVersion || '0.1.0', + id: this.config.clientId, + nodeVersion: process.version, + os: os.platform(), + osVersion: os.release() + } + }; + + return event; + } + + /** + * Queue an event for sending + */ + private queueEvent(event: TelemetryEvent): void { + this.eventQueue.push(event); + + // Schedule batch sending if not already scheduled + if (!this.batchSendTimeout && this.eventQueue.length > 0) { + this.batchSendTimeout = setTimeout(() => { + this.flushEvents(); + }, 5000); + } + + // Immediately send if queue reaches threshold + if (this.eventQueue.length >= 10) { + this.flushEvents(); + } + } + + /** + * Flush events asynchronously + */ + async flush(): Promise { + if (this.flushPromise) { + return this.flushPromise; + } + + if (this.eventQueue.length === 0) { + return Promise.resolve(); + } + + this.flushPromise = this.flushEvents(); + return this.flushPromise; + } + + /** + * Flush events synchronously (for exit handlers) + */ + private flushSync(): void { + if (this.eventQueue.length === 0) { + return; + } + + if (!this.config.enabled || !this.config.endpoint) { + this.eventQueue = []; + return; + } + + try { + const eventsToSend = [...this.eventQueue]; + this.eventQueue = []; + + // Using a synchronous request would be added here + // This is a simplified implementation + logger.debug(`Would send ${eventsToSend.length} telemetry events synchronously`); + } catch (error) { + logger.debug('Failed to send telemetry events synchronously', error); + } + } + + /** + * Flush events to the telemetry endpoint + */ + private async flushEvents(): Promise { + if (this.batchSendTimeout) { + clearTimeout(this.batchSendTimeout); + this.batchSendTimeout = null; + } + + if (this.eventQueue.length === 0) { + this.flushPromise = null; + return; + } + + if (!this.config.enabled || !this.config.endpoint) { + this.eventQueue = []; + this.flushPromise = null; + return; + } + + try { + const eventsToSend = [...this.eventQueue]; + this.eventQueue = []; + + logger.debug(`Sending ${eventsToSend.length} telemetry events`); + + // Send events + const response = await fetch(this.config.endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(eventsToSend) + }); + + if (!response.ok) { + throw new Error(`Failed to send telemetry events: ${response.status} ${response.statusText}`); + } + + logger.debug('Successfully sent telemetry events'); + } catch (error) { + logger.debug('Failed to send telemetry events', error); + // Add events back to queue for retry + // this.eventQueue.unshift(...eventsToSend); + } finally { + this.flushPromise = null; + } + } + + /** + * Enable telemetry + */ + enable(): void { + this.config.enabled = true; + logger.info('Telemetry enabled'); + } + + /** + * Disable telemetry + */ + disable(): void { + this.config.enabled = false; + logger.info('Telemetry disabled'); + + // Clear event queue + this.eventQueue = []; + + if (this.batchSendTimeout) { + clearTimeout(this.batchSendTimeout); + this.batchSendTimeout = null; + } + } + + /** + * Check if telemetry is enabled + */ + isEnabled(): boolean { + return this.config.enabled; + } +} + +// Create singleton instance +export const telemetry = new TelemetryManager(); + +// Export default +export default telemetry; + +/** + * Initialize the telemetry system + * + * @param config Configuration options for telemetry + * @returns The initialized telemetry manager + */ +export async function initTelemetry(config: any = {}): Promise { + logger.info('Initializing telemetry system'); + + try { + // Create telemetry manager + const telemetryManager = new TelemetryManager(); + + // Initialize with configuration + const telemetryConfig = config.telemetry || {}; + + // Check if telemetry is enabled (prefer environment variable) + const telemetryEnabled = process.env.CLAUDE_CODE_TELEMETRY !== 'false' && + telemetryConfig.enabled !== false; + + // Initialize telemetry if enabled + if (telemetryEnabled) { + await telemetryManager.initialize({ + telemetry: telemetryEnabled + }); + + // Track CLI start event + telemetryManager.trackEvent(TelemetryEventType.CLI_START, { + version: config.version || 'unknown' + }); + + // Set up process exit handler to ensure telemetry is flushed + process.on('exit', () => { + telemetryManager.flush(); + }); + + process.on('SIGINT', () => { + telemetryManager.trackEvent(TelemetryEventType.CLI_EXIT, { + reason: 'SIGINT' + }); + telemetryManager.flush(); + process.exit(0); + }); + + process.on('SIGTERM', () => { + telemetryManager.trackEvent(TelemetryEventType.CLI_EXIT, { + reason: 'SIGTERM' + }); + telemetryManager.flush(); + process.exit(0); + }); + + logger.info('Telemetry initialized successfully'); + } else { + logger.info('Telemetry is disabled'); + } + + // Return telemetry interface + return { + /** + * Track an event + */ + trackEvent: (eventType: TelemetryEventType, properties: Record = {}): void => { + if (telemetryEnabled) { + telemetryManager.trackEvent(eventType, properties); + } + }, + + /** + * Track an error + */ + trackError: (error: unknown, context: Record = {}): void => { + if (telemetryEnabled) { + telemetryManager.trackError(error, context); + } + }, + + /** + * Flush telemetry events + */ + flush: async (): Promise => { + if (telemetryEnabled) { + return telemetryManager.flush(); + } + }, + + /** + * Submit telemetry data + */ + submitTelemetry: async (): Promise => { + if (telemetryEnabled) { + return telemetryManager.flush(); + } + }, + + /** + * Check if telemetry is enabled + */ + isEnabled: (): boolean => { + return telemetryManager.isEnabled(); + }, + + /** + * Enable telemetry + */ + enable: (): void => { + telemetryManager.enable(); + }, + + /** + * Disable telemetry + */ + disable: (): void => { + telemetryManager.disable(); + } + }; + } catch (error) { + logger.error('Failed to initialize telemetry system', error); + + // Return a noop telemetry interface if initialization fails + return { + trackEvent: () => {}, + trackError: () => {}, + flush: async () => {}, + submitTelemetry: async () => {}, + isEnabled: () => false, + enable: () => {}, + disable: () => {} + }; + } +} \ No newline at end of file diff --git a/claude-code/src/terminal/formatting.ts b/claude-code/src/terminal/formatting.ts new file mode 100644 index 0000000..3013607 --- /dev/null +++ b/claude-code/src/terminal/formatting.ts @@ -0,0 +1,219 @@ +/** + * Terminal Formatting Utilities + * + * Provides functions for formatting and displaying text in the terminal. + */ + +import chalk from 'chalk'; + +/** + * Clear the terminal screen + */ +export function clearScreen(): void { + // Clear screen and move cursor to top-left + process.stdout.write('\x1b[2J\x1b[0f'); +} + +/** + * Get the terminal size (rows and columns) + */ +export function getTerminalSize(): { rows: number; columns: number } { + // Default to a reasonable size if we can't determine the actual size + const defaultSize = { rows: 24, columns: 80 }; + + try { + if (process.stdout.isTTY) { + return { + rows: process.stdout.rows || defaultSize.rows, + columns: process.stdout.columns || defaultSize.columns + }; + } + } catch (error) { + // Ignore errors + } + + return defaultSize; +} + +/** + * Options for formatting output + */ +export interface FormatOptions { + /** + * Terminal width in columns + */ + width?: number; + + /** + * Whether to use colors + */ + colors?: boolean; + + /** + * Whether to highlight code + */ + codeHighlighting?: boolean; +} + +/** + * Format output for display in the terminal + */ +export function formatOutput(text: string, options: FormatOptions = {}): string { + const { width = getTerminalSize().columns, colors = true, codeHighlighting = true } = options; + + if (!text) { + return ''; + } + + // Process markdown-like formatting if colors are enabled + if (colors) { + // Format code blocks with syntax highlighting + text = formatCodeBlocks(text, codeHighlighting); + + // Format inline code + text = text.replace(/`([^`]+)`/g, (_, code) => chalk.cyan(code)); + + // Format bold text + text = text.replace(/\*\*([^*]+)\*\*/g, (_, bold) => chalk.bold(bold)); + + // Format italic text + text = text.replace(/\*([^*]+)\*/g, (_, italic) => chalk.italic(italic)); + + // Format lists + text = text.replace(/^(\s*)-\s+(.+)$/gm, (_, indent, item) => + `${indent}${chalk.dim('•')} ${item}` + ); + + // Format headers + text = text.replace(/^(#+)\s+(.+)$/gm, (_, hashes, header) => { + if (hashes.length === 1) { + return chalk.bold.underline.blue(header); + } else if (hashes.length === 2) { + return chalk.bold.blue(header); + } else { + return chalk.bold(header); + } + }); + } + + // Word wrap if width is specified + if (width) { + text = wordWrap(text, width); + } + + return text; +} + +/** + * Format code blocks with syntax highlighting + */ +function formatCodeBlocks(text: string, enableHighlighting: boolean): string { + const codeBlockRegex = /```(\w+)?\n([\s\S]+?)```/g; + + return text.replace(codeBlockRegex, (match, language, code) => { + // Add syntax highlighting if enabled + const highlightedCode = enableHighlighting && language + ? highlightSyntax(code, language) + : code; + + // Format the code block with a border + const lines = highlightedCode.split('\n'); + const border = chalk.dim('┃'); + + const formattedLines = lines.map(line => `${border} ${line}`); + const top = chalk.dim('┏' + '━'.repeat(Math.max(...lines.map(l => l.length)) + 2) + '┓'); + const bottom = chalk.dim('┗' + '━'.repeat(Math.max(...lines.map(l => l.length)) + 2) + '┛'); + + // Add language indicator if present + const header = language + ? `${border} ${chalk.bold.blue(language)}\n` + : ''; + + return `${top}\n${header}${formattedLines.join('\n')}\n${bottom}`; + }); +} + +/** + * Simple syntax highlighting for code + */ +function highlightSyntax(code: string, language: string): string { + // Basic syntax highlighting - in a real app, use a proper library + // This is just a simple example with a few patterns + + // Common programming keywords + const keywords = [ + 'function', 'const', 'let', 'var', 'if', 'else', 'for', 'while', 'return', + 'import', 'export', 'class', 'interface', 'extends', 'implements', + 'public', 'private', 'protected', 'static', 'async', 'await' + ]; + + // Split by tokens that we want to preserve + const tokens = code.split(/(\s+|[{}[\]();,.<>?:!+\-*/%&|^~=])/); + + return tokens.map(token => { + // Keywords + if (keywords.includes(token)) { + return chalk.blue(token); + } + + // Numbers + if (/^[0-9]+(\.[0-9]+)?$/.test(token)) { + return chalk.yellow(token); + } + + // Strings + if (/^["'].*["']$/.test(token)) { + return chalk.green(token); + } + + // Comments + if (token.startsWith('//') || token.startsWith('/*') || token.startsWith('*')) { + return chalk.gray(token); + } + + return token; + }).join(''); +} + +/** + * Word wrap text to the specified width + */ +export function wordWrap(text: string, width: number): string { + const lines = text.split('\n'); + + return lines.map(line => { + // If the line is a code block or already shorter than the width, leave it as is + if (line.trim().startsWith('┃') || line.length <= width) { + return line; + } + + // Word wrap the line + const words = line.split(' '); + const wrappedLines: string[] = []; + let currentLine = ''; + + for (const word of words) { + // If adding this word would exceed the width + if (currentLine.length + word.length + 1 > width) { + // Add the current line to wrapped lines if it's not empty + if (currentLine) { + wrappedLines.push(currentLine); + currentLine = word; + } else { + // If the current line is empty, it means the word itself is longer than the width + wrappedLines.push(word); + } + } else { + // Add the word to the current line + currentLine = currentLine ? `${currentLine} ${word}` : word; + } + } + + // Add the last line if it's not empty + if (currentLine) { + wrappedLines.push(currentLine); + } + + return wrappedLines.join('\n'); + }).join('\n'); +} \ No newline at end of file diff --git a/claude-code/src/terminal/index.ts b/claude-code/src/terminal/index.ts new file mode 100644 index 0000000..4d31c8b --- /dev/null +++ b/claude-code/src/terminal/index.ts @@ -0,0 +1,331 @@ +/** + * Terminal Interface Module + * + * Provides a user interface for interacting with Claude Code in the terminal. + * Handles input/output, formatting, and display. + */ + +import chalk from 'chalk'; +import inquirer from 'inquirer'; +import ora from 'ora'; +import terminalLink from 'terminal-link'; +import { table } from 'table'; +import { logger } from '../utils/logger.js'; +import { TerminalInterface, TerminalConfig, PromptOptions, SpinnerInstance } from './types.js'; +import { formatOutput, clearScreen, getTerminalSize } from './formatting.js'; +import { createPrompt } from './prompt.js'; + +/** + * Initialize the terminal interface + */ +export async function initTerminal(config: any): Promise { + logger.debug('Initializing terminal interface'); + + const terminalConfig: TerminalConfig = { + theme: config.terminal?.theme || 'system', + useColors: config.terminal?.useColors !== false, + showProgressIndicators: config.terminal?.showProgressIndicators !== false, + codeHighlighting: config.terminal?.codeHighlighting !== false, + maxHeight: config.terminal?.maxHeight, + maxWidth: config.terminal?.maxWidth, + }; + + const terminal = new Terminal(terminalConfig); + + try { + // Detect terminal capabilities + await terminal.detectCapabilities(); + + return terminal; + } catch (error) { + logger.warn('Error initializing terminal interface:', error); + + // Return a basic terminal interface even if there was an error + return terminal; + } +} + +/** + * Terminal class for handling user interaction + */ +class Terminal implements TerminalInterface { + private config: TerminalConfig; + private activeSpinners: Map = new Map(); + private terminalWidth: number; + private terminalHeight: number; + private isInteractive: boolean; + + constructor(config: TerminalConfig) { + this.config = config; + + // Get initial terminal size + const { rows, columns } = getTerminalSize(); + this.terminalHeight = config.maxHeight || rows; + this.terminalWidth = config.maxWidth || columns; + + // Assume interactive by default + this.isInteractive = process.stdout.isTTY && process.stdin.isTTY; + + // Listen for terminal resize events + process.stdout.on('resize', () => { + const { rows, columns } = getTerminalSize(); + this.terminalHeight = config.maxHeight || rows; + this.terminalWidth = config.maxWidth || columns; + logger.debug(`Terminal resized to ${columns}x${rows}`); + }); + } + + /** + * Detect terminal capabilities + */ + async detectCapabilities(): Promise { + // Check if the terminal is interactive + this.isInteractive = process.stdout.isTTY && process.stdin.isTTY; + + // Check color support + if (this.config.useColors && !chalk.level) { + logger.warn('Terminal does not support colors, disabling color output'); + this.config.useColors = false; + } + + logger.debug('Terminal capabilities detected', { + isInteractive: this.isInteractive, + colorSupport: this.config.useColors ? 'yes' : 'no', + size: `${this.terminalWidth}x${this.terminalHeight}` + }); + } + + /** + * Display the welcome message + */ + displayWelcome(): void { + this.clear(); + + const version = '0.2.29'; // This should come from package.json + + // Main logo/header + console.log(chalk.blue.bold('\n Claude Code CLI')); + console.log(chalk.gray(` Version ${version} (Research Preview)\n`)); + + console.log(chalk.white(` Welcome! Type ${chalk.cyan('/help')} to see available commands.`)); + console.log(chalk.white(` You can ask Claude to explain code, fix issues, or perform tasks.`)); + console.log(chalk.white(` Example: "${chalk.italic('Please analyze this codebase and explain its structure.')}"\n`)); + + if (this.config.useColors) { + console.log(chalk.dim(' Pro tip: Use Ctrl+C to interrupt Claude and start over.\n')); + } + } + + /** + * Clear the terminal screen + */ + clear(): void { + if (this.isInteractive) { + clearScreen(); + } + } + + /** + * Display formatted content + */ + display(content: string): void { + const formatted = formatOutput(content, { + width: this.terminalWidth, + colors: this.config.useColors, + codeHighlighting: this.config.codeHighlighting + }); + + console.log(formatted); + } + + /** + * Display a message with emphasis + */ + emphasize(message: string): void { + if (this.config.useColors) { + console.log(chalk.cyan.bold(message)); + } else { + console.log(message.toUpperCase()); + } + } + + /** + * Display an informational message + */ + info(message: string): void { + if (this.config.useColors) { + console.log(chalk.blue(`ℹ ${message}`)); + } else { + console.log(`INFO: ${message}`); + } + } + + /** + * Display a success message + */ + success(message: string): void { + if (this.config.useColors) { + console.log(chalk.green(`✓ ${message}`)); + } else { + console.log(`SUCCESS: ${message}`); + } + } + + /** + * Display a warning message + */ + warn(message: string): void { + if (this.config.useColors) { + console.log(chalk.yellow(`⚠ ${message}`)); + } else { + console.log(`WARNING: ${message}`); + } + } + + /** + * Display an error message + */ + error(message: string): void { + if (this.config.useColors) { + console.log(chalk.red(`✗ ${message}`)); + } else { + console.log(`ERROR: ${message}`); + } + } + + /** + * Create a clickable link in the terminal if supported + */ + link(text: string, url: string): string { + return terminalLink(text, url, { fallback: (text, url) => `${text} (${url})` }); + } + + /** + * Display a table of data + */ + table(data: any[][], options: { header?: string[]; border?: boolean } = {}): void { + const config: any = { + border: options.border ? {} : { topBody: '', topJoin: '', topLeft: '', topRight: '', bottomBody: '', bottomJoin: '', bottomLeft: '', bottomRight: '', bodyLeft: '', bodyRight: '', bodyJoin: '', joinBody: '', joinLeft: '', joinRight: '', joinJoin: '' } + }; + + // Add header row with formatting + if (options.header) { + if (this.config.useColors) { + data = [options.header.map(h => chalk.bold(h)), ...data]; + } else { + data = [options.header, ...data]; + } + } + + console.log(table(data, config)); + } + + /** + * Prompt user for input + */ + async prompt(options: PromptOptions): Promise { + return createPrompt(options, this.config); + } + + /** + * Create a spinner for showing progress + */ + spinner(text: string, id: string = 'default'): SpinnerInstance { + // Clean up existing spinner with the same ID + if (this.activeSpinners.has(id)) { + this.activeSpinners.get(id)!.stop(); + this.activeSpinners.delete(id); + } + + // Create spinner only if progress indicators are enabled and terminal is interactive + if (this.config.showProgressIndicators && this.isInteractive) { + const spinner = ora({ + text, + spinner: 'dots', + color: 'cyan' + }).start(); + + const spinnerInstance: SpinnerInstance = { + id, + update: (newText: string) => { + spinner.text = newText; + return spinnerInstance; + }, + succeed: (text?: string) => { + spinner.succeed(text); + this.activeSpinners.delete(id); + return spinnerInstance; + }, + fail: (text?: string) => { + spinner.fail(text); + this.activeSpinners.delete(id); + return spinnerInstance; + }, + warn: (text?: string) => { + spinner.warn(text); + this.activeSpinners.delete(id); + return spinnerInstance; + }, + info: (text?: string) => { + spinner.info(text); + this.activeSpinners.delete(id); + return spinnerInstance; + }, + stop: () => { + spinner.stop(); + this.activeSpinners.delete(id); + return spinnerInstance; + } + }; + + this.activeSpinners.set(id, spinnerInstance); + return spinnerInstance; + } else { + // Fallback for non-interactive terminals or when progress indicators are disabled + console.log(text); + + // Return a dummy spinner + const dummySpinner: SpinnerInstance = { + id, + update: (newText: string) => { + if (newText !== text) { + console.log(newText); + } + return dummySpinner; + }, + succeed: (text?: string) => { + if (text) { + this.success(text); + } + return dummySpinner; + }, + fail: (text?: string) => { + if (text) { + this.error(text); + } + return dummySpinner; + }, + warn: (text?: string) => { + if (text) { + this.warn(text); + } + return dummySpinner; + }, + info: (text?: string) => { + if (text) { + this.info(text); + } + return dummySpinner; + }, + stop: () => { + return dummySpinner; + } + }; + + return dummySpinner; + } + } +} + +// Re-export the types +export * from './types.js'; \ No newline at end of file diff --git a/claude-code/src/terminal/prompt.ts b/claude-code/src/terminal/prompt.ts new file mode 100644 index 0000000..4f150d9 --- /dev/null +++ b/claude-code/src/terminal/prompt.ts @@ -0,0 +1,162 @@ +/** + * Terminal Prompts + * + * Provides functions for creating and handling user prompts in the terminal. + */ + +import inquirer from 'inquirer'; +import { PromptOptions, TerminalConfig } from './types.js'; +import { logger } from '../utils/logger.js'; + +/** + * Create and display a prompt for user input + */ +export async function createPrompt(options: PromptOptions, config: TerminalConfig): Promise { + logger.debug('Creating prompt', { type: options.type, name: options.name }); + + // Add validation for required fields + if (options.required && !options.validate) { + options.validate = (input: any) => { + if (!input && input !== false && input !== 0) { + return `${options.name} is required`; + } + return true; + }; + } + + // Handle non-interactive terminals + if (!process.stdin.isTTY || !process.stdout.isTTY) { + logger.warn('Terminal is not interactive, cannot prompt for input'); + throw new Error('Cannot prompt for input in non-interactive terminal'); + } + + try { + // Use Inquirer to create the prompt + const result = await inquirer.prompt([{ + ...options, + // Make sure name is a string + name: String(options.name) + }]); + + logger.debug('Prompt result', { name: options.name, result: result[options.name] }); + + return result; + } catch (error) { + logger.error('Error in prompt', error); + throw new Error(`Failed to prompt for ${options.name}: ${error instanceof Error ? error.message : String(error)}`); + } +} + +/** + * Create a text input prompt + */ +export async function promptText(message: string, options: { + name?: string; + default?: string; + required?: boolean; + validate?: (input: string) => boolean | string | Promise; +} = {}): Promise { + const result = await createPrompt<{ [key: string]: string }>({ + type: 'input', + name: options.name || 'input', + message, + default: options.default, + required: options.required, + validate: options.validate + }, { theme: 'system', useColors: true, showProgressIndicators: true, codeHighlighting: true }); + + return result[options.name || 'input']; +} + +/** + * Create a password input prompt + */ +export async function promptPassword(message: string, options: { + name?: string; + mask?: string; + required?: boolean; +} = {}): Promise { + const result = await createPrompt<{ [key: string]: string }>({ + type: 'password', + name: options.name || 'password', + message, + mask: options.mask || '*', + required: options.required + }, { theme: 'system', useColors: true, showProgressIndicators: true, codeHighlighting: true }); + + return result[options.name || 'password']; +} + +/** + * Create a confirmation prompt + */ +export async function promptConfirm(message: string, options: { + name?: string; + default?: boolean; +} = {}): Promise { + const result = await createPrompt<{ [key: string]: boolean }>({ + type: 'confirm', + name: options.name || 'confirm', + message, + default: options.default + }, { theme: 'system', useColors: true, showProgressIndicators: true, codeHighlighting: true }); + + return result[options.name || 'confirm']; +} + +/** + * Create a selection list prompt + */ +export async function promptList(message: string, choices: Array, options: { + name?: string; + default?: T; + pageSize?: number; +} = {}): Promise { + const result = await createPrompt<{ [key: string]: T }>({ + type: 'list', + name: options.name || 'list', + message, + choices, + default: options.default, + pageSize: options.pageSize + }, { theme: 'system', useColors: true, showProgressIndicators: true, codeHighlighting: true }); + + return result[options.name || 'list']; +} + +/** + * Create a multi-select checkbox prompt + */ +export async function promptCheckbox(message: string, choices: Array, options: { + name?: string; + pageSize?: number; +} = {}): Promise { + const result = await createPrompt<{ [key: string]: T[] }>({ + type: 'checkbox', + name: options.name || 'checkbox', + message, + choices, + pageSize: options.pageSize + }, { theme: 'system', useColors: true, showProgressIndicators: true, codeHighlighting: true }); + + return result[options.name || 'checkbox']; +} + +/** + * Create an editor prompt + */ +export async function promptEditor(message: string, options: { + name?: string; + default?: string; + postfix?: string; +} = {}): Promise { + const result = await createPrompt<{ [key: string]: string }>({ + type: 'editor', + name: options.name || 'editor', + message, + default: options.default, + postfix: options.postfix + }, { theme: 'system', useColors: true, showProgressIndicators: true, codeHighlighting: true }); + + return result[options.name || 'editor']; +} \ No newline at end of file diff --git a/claude-code/src/terminal/types.ts b/claude-code/src/terminal/types.ts new file mode 100644 index 0000000..b59e174 --- /dev/null +++ b/claude-code/src/terminal/types.ts @@ -0,0 +1,251 @@ +/** + * Terminal Interface Types + * + * Type definitions for the terminal interface module. + */ + +/** + * Terminal theme options + */ +export type TerminalTheme = 'dark' | 'light' | 'system'; + +/** + * Terminal configuration + */ +export interface TerminalConfig { + /** + * Terminal color theme + */ + theme: TerminalTheme; + + /** + * Whether to use colors in output + */ + useColors: boolean; + + /** + * Whether to show progress indicators + */ + showProgressIndicators: boolean; + + /** + * Whether to enable syntax highlighting for code + */ + codeHighlighting: boolean; + + /** + * Maximum terminal height (rows) + */ + maxHeight?: number; + + /** + * Maximum terminal width (columns) + */ + maxWidth?: number; +} + +/** + * Spinner instance for progress indicators + */ +export interface SpinnerInstance { + /** + * Spinner identifier + */ + id: string; + + /** + * Update spinner text + */ + update(text: string): SpinnerInstance; + + /** + * Mark spinner as successful and stop + */ + succeed(text?: string): SpinnerInstance; + + /** + * Mark spinner as failed and stop + */ + fail(text?: string): SpinnerInstance; + + /** + * Mark spinner with warning and stop + */ + warn(text?: string): SpinnerInstance; + + /** + * Mark spinner with info and stop + */ + info(text?: string): SpinnerInstance; + + /** + * Stop spinner without any indicator + */ + stop(): SpinnerInstance; +} + +/** + * Prompt option types + */ +export type PromptType = 'input' | 'password' | 'confirm' | 'list' | 'rawlist' | 'checkbox' | 'editor'; + +/** + * Common prompt option properties + */ +export interface BasePromptOptions { + /** + * Prompt type + */ + type: PromptType; + + /** + * Name of the value in the returned object + */ + name: string; + + /** + * Message to display to the user + */ + message: string; + + /** + * Default value + */ + default?: any; + + /** + * Validation function + */ + validate?: (input: any) => boolean | string | Promise; + + /** + * Whether the prompt is required + */ + required?: boolean; +} + +/** + * Input prompt options + */ +export interface InputPromptOptions extends BasePromptOptions { + type: 'input'; + filter?: (input: string) => any; + transformer?: (input: string) => string; +} + +/** + * Password prompt options + */ +export interface PasswordPromptOptions extends BasePromptOptions { + type: 'password'; + mask?: string; +} + +/** + * Confirmation prompt options + */ +export interface ConfirmPromptOptions extends BasePromptOptions { + type: 'confirm'; +} + +/** + * List prompt options + */ +export interface ListPromptOptions extends BasePromptOptions { + type: 'list' | 'rawlist'; + choices: Array; + pageSize?: number; +} + +/** + * Checkbox prompt options + */ +export interface CheckboxPromptOptions extends BasePromptOptions { + type: 'checkbox'; + choices: Array; + pageSize?: number; +} + +/** + * Editor prompt options + */ +export interface EditorPromptOptions extends BasePromptOptions { + type: 'editor'; + postfix?: string; +} + +/** + * Combined prompt options type + */ +export type PromptOptions = + | InputPromptOptions + | PasswordPromptOptions + | ConfirmPromptOptions + | ListPromptOptions + | CheckboxPromptOptions + | EditorPromptOptions; + +/** + * Terminal interface for user interaction + */ +export interface TerminalInterface { + /** + * Clear the terminal screen + */ + clear(): void; + + /** + * Display formatted content + */ + display(content: string): void; + + /** + * Display a welcome message + */ + displayWelcome(): void; + + /** + * Display a message with emphasis + */ + emphasize(message: string): void; + + /** + * Display an informational message + */ + info(message: string): void; + + /** + * Display a success message + */ + success(message: string): void; + + /** + * Display a warning message + */ + warn(message: string): void; + + /** + * Display an error message + */ + error(message: string): void; + + /** + * Create a clickable link in the terminal if supported + */ + link(text: string, url: string): string; + + /** + * Display a table of data + */ + table(data: any[][], options?: { header?: string[]; border?: boolean }): void; + + /** + * Prompt user for input + */ + prompt(options: PromptOptions): Promise; + + /** + * Create a spinner for showing progress + */ + spinner(text: string, id?: string): SpinnerInstance; +} \ No newline at end of file diff --git a/claude-code/src/utils/async.ts b/claude-code/src/utils/async.ts new file mode 100644 index 0000000..1edf0a7 --- /dev/null +++ b/claude-code/src/utils/async.ts @@ -0,0 +1,396 @@ +/** + * Async Utilities + * + * Provides utilities for handling asynchronous operations, + * timeouts, retries, and other async patterns. + */ + +import { logger } from './logger.js'; + +/** + * Options for retry operations + */ +export interface RetryOptions { + /** + * Maximum number of retry attempts + */ + maxRetries: number; + + /** + * Initial delay in milliseconds before the first retry + */ + initialDelayMs: number; + + /** + * Maximum delay in milliseconds between retries + */ + maxDelayMs: number; + + /** + * Whether to use exponential backoff (true) or constant delay (false) + */ + backoff?: boolean; + + /** + * Optional function to determine if an error is retryable + */ + isRetryable?: (error: Error) => boolean; + + /** + * Optional callback to execute before each retry + */ + onRetry?: (error: Error, attempt: number) => void; +} + +/** + * Default retry options + */ +const DEFAULT_RETRY_OPTIONS: RetryOptions = { + maxRetries: 3, + initialDelayMs: 1000, + maxDelayMs: 10000 +}; + +/** + * Sleep for the specified number of milliseconds + */ +export async function delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +/** + * Execute a function with a timeout + * + * @param fn Function to execute with timeout + * @param timeoutMs Timeout in milliseconds + * @returns A function that wraps the original function with timeout + */ +export function withTimeout Promise>( + fn: T, + timeoutMs: number +): (...args: Parameters) => Promise> { + return async (...args: Parameters): Promise> => { + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + const error = new Error(`Operation timed out after ${timeoutMs}ms`); + error.name = 'TimeoutError'; + reject(error); + }, timeoutMs); + + fn(...args) + .then(result => { + clearTimeout(timeoutId); + resolve(result); + }) + .catch(error => { + clearTimeout(timeoutId); + reject(error); + }); + }); + }; +} + +/** + * Execute a function with retry logic + * + * @param fn Function to execute with retry logic + * @param options Retry options + * @returns A function that wraps the original function with retry logic + */ +export function withRetry Promise>( + fn: T, + options: Partial = {} +): (...args: Parameters) => Promise> { + // Set default retry options + const retryOptions: RetryOptions = { + maxRetries: options.maxRetries ?? 3, + initialDelayMs: options.initialDelayMs ?? 1000, + maxDelayMs: options.maxDelayMs ?? 10000, + backoff: options.backoff ?? true, + isRetryable: options.isRetryable, + onRetry: options.onRetry + }; + + // Return a function that wraps the original function with retry logic + return async (...args: Parameters): Promise> => { + let lastError: Error = new Error('Unknown error'); + + for (let attempt = 0; attempt <= retryOptions.maxRetries; attempt++) { + try { + // First attempt (attempt = 0) doesn't count as a retry + if (attempt === 0) { + return await fn(...args); + } + + // Wait before retry + const delayMs = calculateRetryDelay(attempt, retryOptions); + await delay(delayMs); + + // Execute retry callback if provided + if (retryOptions.onRetry) { + retryOptions.onRetry(lastError, attempt); + } + + // Execute the function + return await fn(...args); + } catch (error) { + lastError = error instanceof Error ? error : new Error(String(error)); + + // Check if we've reached the maximum number of retries + if (attempt >= retryOptions.maxRetries) { + throw lastError; + } + + // Check if the error is retryable + if (retryOptions.isRetryable && !retryOptions.isRetryable(lastError)) { + throw lastError; + } + } + } + + // This should never be reached, but TypeScript needs a return + throw lastError; + }; +} + +/** + * Calculate the delay time for a retry attempt + */ +function calculateRetryDelay(attempt: number, options: RetryOptions): number { + if (!options.backoff) { + return options.initialDelayMs; + } + + // Exponential backoff: initialDelay * 2^(attempt-1) + const exponentialDelay = options.initialDelayMs * Math.pow(2, attempt - 1); + + // Add some jitter (±10%) to avoid retry stampedes + const jitter = 0.1 * exponentialDelay; + const jitteredDelay = exponentialDelay - jitter + (Math.random() * jitter * 2); + + // Cap at maximum delay + return Math.min(jitteredDelay, options.maxDelayMs); +} + +/** + * Run operations in parallel with a concurrency limit + */ +export async function withConcurrency( + items: T[], + fn: (item: T, index: number) => Promise, + concurrency: number = 5 +): Promise { + if (!items.length) return []; + + const results: R[] = new Array(items.length); + let currentIndex = 0; + + const workers = Array.from({ length: Math.min(concurrency, items.length) }, async (_, workerId) => { + while (currentIndex < items.length) { + const index = currentIndex++; + logger.debug(`Worker ${workerId} processing item ${index}`); + + try { + results[index] = await fn(items[index], index); + } catch (error) { + logger.error(`Error processing item ${index}`, error); + throw error; // Fail fast + } + } + }); + + await Promise.all(workers); + return results; +} + +/** + * Create a debounced version of a function + * + * @param fn Function to debounce + * @param waitMs Wait time in milliseconds + * @param options Debounce options + * @returns Debounced function + */ +export function debounce any>( + fn: T, + waitMs: number, + options: { leading?: boolean; trailing?: boolean; maxWait?: number } = {} +): (...args: Parameters) => void { + let timeout: ReturnType | null = null; + let lastArgs: Parameters | null = null; + let lastCallTime: number | null = null; + let lastInvokeTime = 0; + + const leading = options.leading ?? false; + const trailing = options.trailing ?? true; + const maxWait = options.maxWait; + + // Check if we should invoke the function + function shouldInvoke(time: number): boolean { + if (lastCallTime === null) { + return true; + } + + const timeSinceLastCall = time - lastCallTime; + const timeSinceLastInvoke = time - lastInvokeTime; + + // Invoke if: + // 1. It's the first call, or + // 2. We've waited long enough since the last call, or + // 3. We've reached the maximum wait time (if defined) + return ( + lastCallTime === null || + timeSinceLastCall >= waitMs || + (maxWait !== undefined && timeSinceLastInvoke >= maxWait) + ); + } + + // Invoke the function + function invokeFunc(time: number): void { + const args = lastArgs!; + lastArgs = null; + lastInvokeTime = time; + + fn(...args); + } + + // Handle the trailing edge invocation + function trailingEdge(time: number): void { + timeout = null; + + if (trailing && lastArgs) { + invokeFunc(time); + } + + lastArgs = null; + } + + // Start the timer for the trailing edge + function startTimer(): void { + timeout = setTimeout(() => { + const time = Date.now(); + trailingEdge(time); + }, waitMs); + } + + // Handle a new function call + function timerExpired(): void { + const time = Date.now(); + + if (shouldInvoke(time)) { + return trailingEdge(time); + } + + // Restart the timer + timeout = setTimeout(timerExpired, Math.min( + waitMs - (time - lastCallTime!), + maxWait !== undefined ? maxWait - (time - lastInvokeTime) : waitMs + )); + } + + // Leading edge invocation + function leadingEdge(time: number): void { + lastInvokeTime = time; + + // Start the timer for trailing edge + startTimer(); + + // Invoke the function + if (leading) { + invokeFunc(time); + } + } + + // The debounced function + return function debouncedFunction(...args: Parameters): void { + const time = Date.now(); + lastArgs = args; + lastCallTime = time; + + const isInvoking = shouldInvoke(time); + + if (isInvoking) { + if (timeout === null) { + leadingEdge(time); + return; + } + + if (maxWait !== undefined) { + // Handle maxWait + timeout = setTimeout(timerExpired, maxWait); + } + } else if (timeout === null && trailing) { + // Start timer for trailing edge + startTimer(); + } + }; +} + +/** + * Create a throttled version of a function + * + * @param fn Function to throttle + * @param waitMs Wait time in milliseconds + * @param options Throttle options + * @returns Throttled function + */ +export function throttle any>( + fn: T, + waitMs: number, + options: { leading?: boolean; trailing?: boolean } = {} +): (...args: Parameters) => void { + const leading = options.leading ?? true; + const trailing = options.trailing ?? true; + + // Use debounce with maxWait equal to waitMs + return debounce(fn, waitMs, { + leading, + trailing, + maxWait: waitMs + }); +} + +/** + * Create a deferred promise + */ +export function createDeferred(): { + promise: Promise; + resolve: (value: T) => void; + reject: (reason?: any) => void; +} { + let resolve!: (value: T) => void; + let reject!: (reason?: any) => void; + + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + + return { promise, resolve, reject }; +} + +/** + * Run functions in sequence + */ +export async function runSequentially( + fns: Array<() => Promise> +): Promise { + const results: T[] = []; + + for (const fn of fns) { + results.push(await fn()); + } + + return results; +} + +export default { + delay, + withTimeout, + withRetry, + withConcurrency, + debounce, + throttle, + createDeferred, + runSequentially +}; \ No newline at end of file diff --git a/claude-code/src/utils/formatting.ts b/claude-code/src/utils/formatting.ts new file mode 100644 index 0000000..ae1bc78 --- /dev/null +++ b/claude-code/src/utils/formatting.ts @@ -0,0 +1,283 @@ +/** + * Formatting Utilities + * + * Provides utilities for formatting text, truncating strings, + * handling terminal output, and other formatting tasks. + */ + +/** + * Truncate a string to a maximum length + */ +export function truncate(text: string, maxLength: number, suffix: string = '...'): string { + if (!text || text.length <= maxLength) { + return text; + } + + return text.substring(0, maxLength - suffix.length) + suffix; +} + +/** + * Format a number with commas as thousands separators + */ +export function formatNumber(num: number): string { + return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); +} + +/** + * Format a date to ISO string without milliseconds + */ +export function formatDate(date: Date): string { + return date.toISOString().replace(/\.\d{3}Z$/, 'Z'); +} + +/** + * Format a file size in bytes to a human-readable string + */ +export function formatFileSize(bytes: number): string { + if (bytes === 0) return '0 Bytes'; + + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB']; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + + return `${parseFloat((bytes / Math.pow(1024, i)).toFixed(2))} ${sizes[i]}`; +} + +/** + * Format a duration in milliseconds to a human-readable string + */ +export function formatDuration(ms: number): string { + if (ms < 1000) { + return `${ms}ms`; + } + + const seconds = Math.floor(ms / 1000); + + if (seconds < 60) { + return `${seconds}s`; + } + + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + + if (minutes < 60) { + return `${minutes}m ${remainingSeconds}s`; + } + + const hours = Math.floor(minutes / 60); + const remainingMinutes = minutes % 60; + + if (hours < 24) { + return `${hours}h ${remainingMinutes}m ${remainingSeconds}s`; + } + + const days = Math.floor(hours / 24); + const remainingHours = hours % 24; + + return `${days}d ${remainingHours}h ${remainingMinutes}m`; +} + +/** + * Indent a string with a specified number of spaces + */ +export function indent(text: string, spaces: number = 2): string { + const indentation = ' '.repeat(spaces); + return text.split('\n').map(line => indentation + line).join('\n'); +} + +/** + * Strip ANSI escape codes from a string + */ +export function stripAnsi(text: string): string { + return text.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, ''); +} + +/** + * Wrap text to a specified width + */ +export function wrapText(text: string, width: number = 80): string { + const lines = text.split('\n'); + return lines.map(line => { + if (line.length <= width) { + return line; + } + + const wrappedLines = []; + let currentLine = ''; + + const words = line.split(' '); + for (const word of words) { + if ((currentLine + word).length <= width) { + currentLine += (currentLine ? ' ' : '') + word; + } else { + if (currentLine) { + wrappedLines.push(currentLine); + } + + currentLine = word.length > width ? word.substring(0, width) : word; + + if (word.length > width) { + let remaining = word.substring(width); + while (remaining.length > 0) { + wrappedLines.push(remaining.substring(0, width)); + remaining = remaining.substring(width); + } + } + } + } + + if (currentLine) { + wrappedLines.push(currentLine); + } + + return wrappedLines.join('\n'); + }).join('\n'); +} + +/** + * Pad a string to a fixed width + */ +export function padString(text: string, width: number, padChar: string = ' ', padRight: boolean = true): string { + if (text.length >= width) { + return text; + } + + const padding = padChar.repeat(width - text.length); + return padRight ? text + padding : padding + text; +} + +/** + * Center a string within a fixed width + */ +export function centerString(text: string, width: number, padChar: string = ' '): string { + if (text.length >= width) { + return text; + } + + const leftPadding = Math.floor((width - text.length) / 2); + const rightPadding = width - text.length - leftPadding; + + return padChar.repeat(leftPadding) + text + padChar.repeat(rightPadding); +} + +/** + * Create a simple text table + */ +export function createTextTable(rows: string[][], headers?: string[]): string { + if (rows.length === 0) { + return ''; + } + + // Add headers as first row if provided + const allRows = headers ? [headers, ...rows] : rows; + + // Calculate column widths + const columnWidths: number[] = []; + + for (const row of allRows) { + for (let i = 0; i < row.length; i++) { + const cellWidth = String(row[i]).length; + + if (!columnWidths[i] || cellWidth > columnWidths[i]) { + columnWidths[i] = cellWidth; + } + } + } + + // Format rows + const formattedRows = allRows.map(row => { + return row.map((cell, i) => padString(String(cell), columnWidths[i])).join(' | '); + }); + + // Add separator after headers if provided + if (headers) { + const separator = columnWidths.map(width => '-'.repeat(width)).join('-+-'); + formattedRows.splice(1, 0, separator); + } + + return formattedRows.join('\n'); +} + +/** + * Format a key-value object as a string + */ +export function formatKeyValue(obj: Record, options: { + indent?: number; + keyValueSeparator?: string; + includeEmpty?: boolean; +} = {}): string { + const { + indent = 0, + keyValueSeparator = ': ', + includeEmpty = false + } = options; + + const indentation = ' '.repeat(indent); + const lines: string[] = []; + + for (const [key, value] of Object.entries(obj)) { + if (!includeEmpty && (value === undefined || value === null || value === '')) { + continue; + } + + const valueStr = typeof value === 'object' && value !== null + ? JSON.stringify(value) + : String(value); + + lines.push(`${indentation}${key}${keyValueSeparator}${valueStr}`); + } + + return lines.join('\n'); +} + +/** + * Convert camelCase to Title Case + */ +export function camelToTitleCase(text: string): string { + if (!text) return text; + + // Insert a space before all uppercase letters + const spaceSeparated = text.replace(/([A-Z])/g, ' $1'); + + // Capitalize the first letter + return spaceSeparated.charAt(0).toUpperCase() + spaceSeparated.slice(1); +} + +/** + * Format error details + */ +export function formatErrorDetails(error: Error): string { + let details = `Error: ${error.message}`; + + if (error.stack) { + details += `\nStack: ${error.stack.split('\n').slice(1).join('\n')}`; + } + + // Add any additional properties that might be present + for (const key of Object.keys(error)) { + if (!['name', 'message', 'stack'].includes(key)) { + const value = (error as any)[key]; + if (value !== undefined && value !== null) { + details += `\n${key}: ${typeof value === 'object' ? JSON.stringify(value) : value}`; + } + } + } + + return details; +} + +export default { + truncate, + formatNumber, + formatDate, + formatFileSize, + formatDuration, + indent, + stripAnsi, + wrapText, + padString, + centerString, + createTextTable, + formatKeyValue, + camelToTitleCase, + formatErrorDetails +}; \ No newline at end of file diff --git a/claude-code/src/utils/index.ts b/claude-code/src/utils/index.ts new file mode 100644 index 0000000..c41e135 --- /dev/null +++ b/claude-code/src/utils/index.ts @@ -0,0 +1,11 @@ +/** + * Utility Module + * + * Exports various utility functions used throughout the application. + */ + +export * from './logger.js'; +export * from './async.js'; +export * from './formatting.js'; +export * from './validation.js'; +export * from './types.js'; \ No newline at end of file diff --git a/claude-code/src/utils/logger.ts b/claude-code/src/utils/logger.ts new file mode 100644 index 0000000..1f9c2b1 --- /dev/null +++ b/claude-code/src/utils/logger.ts @@ -0,0 +1,253 @@ +/** + * Logger + * + * Provides a consistent logging interface across the application. + * Supports multiple log levels, formatting, and output destinations. + */ + +import { ErrorLevel } from '../errors/types.js'; + +/** + * Log levels + */ +export enum LogLevel { + DEBUG = 0, + INFO = 1, + WARN = 2, + ERROR = 3, + SILENT = 4 +} + +/** + * Logger configuration + */ +export interface LoggerConfig { + /** + * Minimum log level to display + */ + level: LogLevel; + + /** + * Whether to include timestamps in logs + */ + timestamps: boolean; + + /** + * Whether to colorize output + */ + colors: boolean; + + /** + * Whether to include additional context in logs + */ + verbose: boolean; + + /** + * Custom output destination (defaults to console) + */ + destination?: (message: string, level: LogLevel) => void; +} + +/** + * Default logger configuration + */ +const DEFAULT_CONFIG: LoggerConfig = { + level: LogLevel.INFO, + timestamps: true, + colors: true, + verbose: false +}; + +/** + * Logger class + */ +class Logger { + private config: LoggerConfig; + + constructor(config: Partial = {}) { + this.config = { ...DEFAULT_CONFIG, ...config }; + } + + /** + * Set logger configuration + */ + configure(config: Partial): void { + this.config = { ...this.config, ...config }; + } + + /** + * Set log level + */ + setLevel(level: LogLevel): void { + this.config.level = level; + } + + /** + * Log a debug message + */ + debug(message: string, context?: any): void { + this.log(message, LogLevel.DEBUG, context); + } + + /** + * Log an info message + */ + info(message: string, context?: any): void { + this.log(message, LogLevel.INFO, context); + } + + /** + * Log a warning message + */ + warn(message: string, context?: any): void { + this.log(message, LogLevel.WARN, context); + } + + /** + * Log an error message + */ + error(message: string, context?: any): void { + this.log(message, LogLevel.ERROR, context); + } + + /** + * Log a message with level + */ + private log(message: string, level: LogLevel, context?: any): void { + // Check if this log level should be displayed + if (level < this.config.level) { + return; + } + + // Format the message + const formattedMessage = this.formatMessage(message, level, context); + + // Send to destination + if (this.config.destination) { + this.config.destination(formattedMessage, level); + } else { + this.logToConsole(formattedMessage, level); + } + } + + /** + * Format a log message + */ + private formatMessage(message: string, level: LogLevel, context?: any): string { + let result = ''; + + // Add timestamp if enabled + if (this.config.timestamps) { + const timestamp = new Date().toISOString(); + result += `[${timestamp}] `; + } + + // Add log level + result += `${this.getLevelName(level)}: `; + + // Add message + result += message; + + // Add context if verbose and context is provided + if (this.config.verbose && context) { + try { + if (typeof context === 'object') { + const contextStr = JSON.stringify(context); + result += ` ${contextStr}`; + } else { + result += ` ${context}`; + } + } catch (error) { + result += ' [Context serialization failed]'; + } + } + + return result; + } + + /** + * Get the name of a log level + */ + private getLevelName(level: LogLevel): string { + switch (level) { + case LogLevel.DEBUG: + return this.colorize('DEBUG', '\x1b[36m'); // Cyan + case LogLevel.INFO: + return this.colorize('INFO', '\x1b[32m'); // Green + case LogLevel.WARN: + return this.colorize('WARN', '\x1b[33m'); // Yellow + case LogLevel.ERROR: + return this.colorize('ERROR', '\x1b[31m'); // Red + default: + return 'UNKNOWN'; + } + } + + /** + * Colorize a string if colors are enabled + */ + private colorize(text: string, colorCode: string): string { + if (!this.config.colors) { + return text; + } + + return `${colorCode}${text}\x1b[0m`; + } + + /** + * Log to console + */ + private logToConsole(message: string, level: LogLevel): void { + switch (level) { + case LogLevel.DEBUG: + console.debug(message); + break; + case LogLevel.INFO: + console.info(message); + break; + case LogLevel.WARN: + console.warn(message); + break; + case LogLevel.ERROR: + console.error(message); + break; + } + } + + /** + * Convert ErrorLevel to LogLevel + */ + errorLevelToLogLevel(level: ErrorLevel): LogLevel { + switch (level) { + case ErrorLevel.DEBUG: + return LogLevel.DEBUG; + case ErrorLevel.INFO: + return LogLevel.INFO; + case ErrorLevel.WARNING: + return LogLevel.WARN; + case ErrorLevel.ERROR: + case ErrorLevel.CRITICAL: + case ErrorLevel.FATAL: + return LogLevel.ERROR; + default: + return LogLevel.INFO; + } + } +} + +// Create singleton logger instance +export const logger = new Logger(); + +// Configure logger based on environment +if (process.env.NODE_ENV === 'development' || process.env.DEBUG === 'true') { + logger.setLevel(LogLevel.DEBUG); +} else if (process.env.VERBOSE === 'true') { + logger.configure({ verbose: true }); +} else if (process.env.LOG_LEVEL) { + const level = parseInt(process.env.LOG_LEVEL, 10); + if (!isNaN(level) && level >= LogLevel.DEBUG && level <= LogLevel.SILENT) { + logger.setLevel(level as LogLevel); + } +} + +export default logger; \ No newline at end of file diff --git a/claude-code/src/utils/types.ts b/claude-code/src/utils/types.ts new file mode 100644 index 0000000..059363f --- /dev/null +++ b/claude-code/src/utils/types.ts @@ -0,0 +1,130 @@ +/** + * Type Declarations + * + * Re-exports and defines common types used throughout the application. + * This helps centralize type definitions and avoid duplication. + */ + +/** + * Node.js process error with code + */ +export interface ErrnoException extends Error { + errno?: number; + code?: string; + path?: string; + syscall?: string; +} + +/** + * Node.js timeout handle + */ +export interface Timeout { + hasRef(): boolean; + ref(): Timeout; + refresh(): Timeout; + unref(): Timeout; +} + +/** + * JSON primitive types + */ +export type JSONPrimitive = string | number | boolean | null; + +/** + * JSON object type + */ +export type JSONObject = { [key: string]: JSONValue }; + +/** + * JSON array type + */ +export type JSONArray = JSONValue[]; + +/** + * JSON value type + */ +export type JSONValue = JSONPrimitive | JSONObject | JSONArray; + +/** + * Record with string keys and any values + */ +export type AnyRecord = Record; + +/** + * Basic callback function type + */ +export type Callback = (error?: Error | null, result?: T) => void; + +/** + * Async function that returns a Promise + */ +export type AsyncFunction = (...args: A) => Promise; + +/** + * Function with a timeout + */ +export interface TimedFunction { + (...args: A): Promise; + timeout: number; +} + +/** + * Optional properties in T + */ +export type Optional = Omit & Partial>; + +/** + * Required properties in T + */ +export type RequiredFields = Omit & Required>; + +/** + * Deep partial type + */ +export type DeepPartial = T extends object + ? { [P in keyof T]?: DeepPartial } + : T; + +/** + * Result of an operation + */ +export interface Result { + success: boolean; + value?: T; + error?: E; +} + +/** + * Success result + */ +export type Success = { + success: true; + value: T; +}; + +/** + * Error result + */ +export type Failure = { + success: false; + error: E; +}; + +/** + * Either success or failure + */ +export type Either = Success | Failure; + +/** + * Create a success result + */ +export function success(value: T): Success { + return { success: true, value }; +} + +/** + * Create a failure result + */ +export function failure(error: E): Failure { + return { success: false, error }; +} \ No newline at end of file diff --git a/claude-code/src/utils/validation.ts b/claude-code/src/utils/validation.ts new file mode 100644 index 0000000..ee481be --- /dev/null +++ b/claude-code/src/utils/validation.ts @@ -0,0 +1,238 @@ +/** + * Validation Utilities + * + * Provides utilities for validating inputs, checking types, + * and ensuring data conforms to expected formats. + */ + +/** + * Check if a value is defined (not undefined or null) + */ +export function isDefined(value: T | undefined | null): value is T { + return value !== undefined && value !== null; +} + +/** + * Check if a value is a non-empty string + */ +export function isNonEmptyString(value: unknown): value is string { + return typeof value === 'string' && value.trim().length > 0; +} + +/** + * Check if a value is a number (and optionally within range) + */ +export function isNumber(value: unknown, options: { min?: number; max?: number } = {}): value is number { + if (typeof value !== 'number' || isNaN(value)) { + return false; + } + + const { min, max } = options; + + if (min !== undefined && value < min) { + return false; + } + + if (max !== undefined && value > max) { + return false; + } + + return true; +} + +/** + * Check if a value is a boolean + */ +export function isBoolean(value: unknown): value is boolean { + return typeof value === 'boolean'; +} + +/** + * Check if a value is an object (and not an array or null) + */ +export function isObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +/** + * Check if a value is an array + */ +export function isArray(value: unknown, itemValidator?: (item: unknown) => item is T): value is T[] { + if (!Array.isArray(value)) { + return false; + } + + if (itemValidator) { + return value.every(item => itemValidator(item)); + } + + return true; +} + +/** + * Check if a value is a valid date + */ +export function isValidDate(value: unknown): value is Date { + return value instanceof Date && !isNaN(value.getTime()); +} + +/** + * Check if a string is a valid email address + */ +export function isEmail(value: string): boolean { + const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + return emailRegex.test(value); +} + +/** + * Check if a string is a valid URL + */ +export function isUrl(value: string): boolean { + try { + const url = new URL(value); + return url.protocol === 'http:' || url.protocol === 'https:'; + } catch { + return false; + } +} + +/** + * Check if a string is a valid path + */ +export function isValidPath(value: string): boolean { + // Basic path validation + return /^[a-zA-Z0-9\/\\\._\-~]+$/.test(value) && !value.includes('..') && value.length > 0; +} + +/** + * Check if a string is a valid file path + */ +export function isValidFilePath(value: string): boolean { + return isValidPath(value) && !value.endsWith('/') && !value.endsWith('\\'); +} + +/** + * Check if a string is a valid directory path + */ +export function isValidDirectoryPath(value: string): boolean { + return isValidPath(value); +} + +/** + * Check if a string is valid JSON + */ +export function isValidJson(value: string): boolean { + try { + JSON.parse(value); + return true; + } catch { + return false; + } +} + +/** + * Validate an object against a schema + */ +export function validateObject( + obj: unknown, + schema: Record boolean>, + options: { allowExtraProps?: boolean; required?: Array } = {} +): { valid: boolean; errors: string[] } { + const errors: string[] = []; + + if (!isObject(obj)) { + return { valid: false, errors: ['Expected an object'] }; + } + + // Check required fields + if (options.required) { + for (const key of options.required) { + if (!(key in obj)) { + errors.push(`Missing required field: ${String(key)}`); + } + } + } + + // Check each field against schema + for (const [key, validator] of Object.entries(schema) as Array<[string, (value: unknown) => boolean]>) { + if (key in obj) { + const value = obj[key as keyof typeof obj]; + if (!validator(value)) { + errors.push(`Invalid value for field: ${key}`); + } + } + } + + // Check for extra properties + if (options.allowExtraProps === false) { + for (const key of Object.keys(obj)) { + if (!(key in schema)) { + errors.push(`Unexpected field: ${key}`); + } + } + } + + return { + valid: errors.length === 0, + errors + }; +} + +/** + * Create a validator function for an enum + */ +export function createEnumValidator(enumObj: Record) { + const validValues = Object.values(enumObj); + + return function isValidEnum(value: unknown): value is T { + return validValues.includes(value as T); + }; +} + +/** + * Create a validator that ensures a value is one of the allowed values + */ +export function createOneOfValidator(allowedValues: T[]) { + return function isOneOf(value: unknown): value is T { + return allowedValues.includes(value as T); + }; +} + +/** + * Create a validator that combines multiple validators with AND logic + */ +export function createAllValidator(...validators: Array<(value: unknown) => boolean>) { + return function validateAll(value: unknown): boolean { + return validators.every(validator => validator(value)); + }; +} + +/** + * Create a validator that combines multiple validators with OR logic + */ +export function createAnyValidator(...validators: Array<(value: unknown) => boolean>) { + return function validateAny(value: unknown): boolean { + return validators.some(validator => validator(value)); + }; +} + +export default { + isDefined, + isNonEmptyString, + isNumber, + isBoolean, + isObject, + isArray, + isValidDate, + isEmail, + isUrl, + isValidPath, + isValidFilePath, + isValidDirectoryPath, + isValidJson, + validateObject, + createEnumValidator, + createOneOfValidator, + createAllValidator, + createAnyValidator +}; \ No newline at end of file diff --git a/claude-code/tsconfig.json b/claude-code/tsconfig.json new file mode 100644 index 0000000..c4abcdd --- /dev/null +++ b/claude-code/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "outDir": "./dist", + "declaration": true, + "sourceMap": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"] +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..0e92b1d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,340 @@ +{ + "name": "cc", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@anthropic-ai/claude-code": "^0.2.29", + "open": "^10.1.0", + "uuid": "^11.1.0" + }, + "devDependencies": { + "@types/node": "^22.13.8", + "@types/uuid": "^10.0.0" + } + }, + "node_modules/@anthropic-ai/claude-code": { + "version": "0.2.29", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-0.2.29.tgz", + "integrity": "sha512-4lZbbg+Qqz+mY3SLKcUYjnOd6/PIB+SQh7ICVIXFYGB1zOB4mVfiY7l2nBt67iw+Rxko2wGJAfg6gPM57S/Q/g==", + "hasInstallScript": true, + "license": "SEE LICENSE IN README.md", + "bin": { + "claude": "cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "^0.33.5", + "@img/sharp-linux-arm": "^0.33.5", + "@img/sharp-linux-x64": "^0.33.5", + "@img/sharp-win32-x64": "^0.33.5" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@types/node": { + "version": "22.13.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.8.tgz", + "integrity": "sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..989b77f --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "dependencies": { + "@anthropic-ai/claude-code": "^0.2.29", + "open": "^10.1.0", + "uuid": "^11.1.0" + }, + "devDependencies": { + "@types/node": "^22.13.8", + "@types/uuid": "^10.0.0" + } +} diff --git a/specs/LICENSE.md b/specs/LICENSE.md new file mode 100644 index 0000000..63a39d1 --- /dev/null +++ b/specs/LICENSE.md @@ -0,0 +1 @@ +https://en.wikipedia.org/wiki/Cleanroom_software_engineering diff --git a/specs/architecture.md b/specs/architecture.md new file mode 100644 index 0000000..815b600 --- /dev/null +++ b/specs/architecture.md @@ -0,0 +1,120 @@ +# Claude Code CLI - Technical Architecture + +## System Overview + +The Claude Code CLI is built as a Node.js application written in TypeScript, compiled with webpack, and structured as a CommonJS module. It uses a combination of local processing and remote AI capabilities to provide an intelligent coding assistant within the terminal environment. + +## High-Level Components + +### 1. Terminal Interface Layer +- Handles raw input/output with the terminal +- Manages command history and editing +- Implements custom rendering for code, tables, and other structured outputs +- Captures and redirects system outputs from executed commands + +### 2. Command Processing Engine +- Parses natural language inputs +- Identifies command intents and parameters +- Routes requests to appropriate handlers +- Manages conversation context and history + +### 3. Codebase Analysis System +- Scans and indexes project files +- Builds dependency graphs and structure maps +- Performs text and semantic searching +- Monitors file system changes + +### 4. Execution Environment +- Executes shell commands securely +- Captures and parses command outputs +- Manages environment variables and context +- Handles background and long-running processes + +### 5. AI Integration Layer +- Formats requests to the Claude API +- Processes and parses AI responses +- Manages AI context and history +- Handles authentication and API communication + +### 6. File Operation System +- Reads and writes files with appropriate permissions +- Generates diffs and patches +- Implements version control operations +- Handles file watching and change detection + +## Data Flow Architecture + +1. **Input Processing Flow** + - Terminal input → Command parser → Intent classification → Handler selection → Action execution + +2. **Context Gathering Flow** + - Command intent → Context requirements → File system queries → Codebase analysis → Context compilation + +3. **AI Request Flow** + - User intent + Context → Request formatting → API authentication → Request transmission → Response reception → Response parsing + +4. **Response Handling Flow** + - Parsed response → Action extraction → Command generation → Execution → Output capture → Formatted display + +## Implementation Details + +### Programming Language and Runtime +- Built with TypeScript +- Runs on Node.js (v18+) +- Packaged as an ESM module + +### Key Dependencies +- Sentry SDK for error tracking +- Sharp for image processing (optional) +- Terminal rendering libraries +- File system utilities + +### Design Patterns +- Command Pattern for action encapsulation +- Observer Pattern for system monitoring +- Factory Pattern for handler creation +- Adapter Pattern for external integrations + +## Execution Flow + +1. **Initialization Phase** + - Environment validation + - Configuration loading + - Authentication verification + - Workspace scanning + +2. **Main Execution Loop** + - Input capture + - Command processing + - Context gathering + - AI request/response handling + - Action execution + - Result presentation + +3. **Termination Phase** + - Session state saving + - Resource cleanup + - Telemetry submission (if enabled) + +## Error Handling Architecture + +- Hierarchical error classification +- Graceful degradation for non-critical failures +- Comprehensive logging with Sentry integration +- User-friendly error messages with suggestions +- Automatic retry mechanisms where appropriate + +## Security Architecture + +- Local execution model (no code execution on remote servers) +- Permission-based command execution +- Secure credential storage +- Data minimization in API requests +- Telemetry anonymization + +## Extensibility Points + +- Custom command handlers +- Project-specific configuration +- Tool integration adapters +- Language-specific processors \ No newline at end of file diff --git a/specs/command_reference.md b/specs/command_reference.md new file mode 100644 index 0000000..218510d --- /dev/null +++ b/specs/command_reference.md @@ -0,0 +1,125 @@ +# Claude Code CLI - Command Reference + +## Base Command + +``` +claude [options] +``` + +The base command launches the Claude Code CLI in the current directory context. + +## Core Commands + +### Help and Information + +| Command | Description | +|---------|-------------| +| `claude --help` | Display help information | +| `claude --version` | Display version information | +| `/help` | Show in-application help | +| `/commands` | List available slash commands | + +### Session Management + +| Command | Description | +|---------|-------------| +| `/exit` or `/quit` | Exit the application | +| `/clear` | Clear the current session | +| `/reset` | Reset the conversation context | +| `/history` | View conversation history | + +### Feature-Specific Commands + +| Command | Description | +|---------|-------------| +| `/edit [file]` | Edit a specified file | +| `/search [term]` | Search the codebase for a term | +| `/run [command]` | Execute a terminal command | +| `/explain [file or code]` | Get explanation for code | +| `/git [operation]` | Perform git operations | + +### User Preferences + +| Command | Description | +|---------|-------------| +| `/config` | View or edit configuration | +| `/theme [name]` | Change the UI theme | +| `/verbosity [level]` | Set output verbosity | + +### Feedback and Support + +| Command | Description | +|---------|-------------| +| `/bug` | Report a bug or issue | +| `/feedback` | Provide general feedback | + +## Natural Language Commands + +Claude Code is primarily designed to accept natural language commands. Some examples include: + +### Code Understanding + +- "Explain how the authentication system works in this codebase" +- "What does this function do?" (when in a file context) +- "How are API requests handled in this application?" +- "Find all usages of the User class" + +### Code Editing + +- "Create a new file called utils.js with helper functions for date formatting" +- "Fix the bug in the login function that doesn't handle empty passwords" +- "Refactor this function to use async/await instead of promises" +- "Add error handling to this API endpoint" + +### Development Workflows + +- "Run the tests for the user module" +- "Build the project and tell me if there are any errors" +- "Install the latest version of express and update package.json" +- "Start the development server and monitor for errors" + +### Git Operations + +- "Create a commit with the message 'Fix login bug'" +- "Show me the recent changes to the authentication module" +- "Create a new branch called feature/user-profiles" +- "Help me resolve these merge conflicts" + +## Command Options + +### Global Options + +| Option | Description | +|--------|-------------| +| `--workspace=` | Specify a workspace directory | +| `--config=` | Use a custom configuration file | +| `--verbose` | Enable verbose output | +| `--quiet` | Minimize output to essential information | +| `--debug` | Enable debug mode with additional logging | + +### Authentication Options + +| Option | Description | +|--------|-------------| +| `--login` | Force authentication flow | +| `--logout` | Clear saved authentication | + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `CLAUDE_API_KEY` | API key for Claude services | +| `CLAUDE_CONFIG_PATH` | Custom path for configuration files | +| `CLAUDE_TELEMETRY` | Enable/disable telemetry (true/false) | +| `CLAUDE_LOG_LEVEL` | Set logging level (debug, info, warn, error) | + +## Exit Codes + +| Code | Description | +|------|-------------| +| 0 | Successful execution | +| 1 | General error | +| 2 | Configuration error | +| 3 | Authentication error | +| 4 | Network error | +| 5 | API error | \ No newline at end of file diff --git a/specs/development.md b/specs/development.md new file mode 100644 index 0000000..ade97fe --- /dev/null +++ b/specs/development.md @@ -0,0 +1,181 @@ +# Claude Code CLI - Development Status and Roadmap + +## Current Development Status + +### Release Phase + +Claude Code is currently in **Research Preview** (Beta) status. This indicates: + +- Early access to the core functionality +- Active development with frequent updates +- Some features may have limitations +- Collecting user feedback for improvement +- Not recommended for critical production use + +### Version History + +| Version | Release Date | Key Features | +|---------|--------------|-------------| +| 0.2.29 | Current | Latest stable version | +| 0.2.x | Various | Incremental improvements and bug fixes | +| 0.1.x | Initial | Initial research preview release | + +### Known Limitations + +1. **Tool Execution Reliability** + - Some complex command sequences may not execute reliably + - Environment-specific command behavior variations + +2. **Long-Running Commands** + - Limited support for commands that run for extended periods + - Potential timeout issues for certain operations + +3. **Terminal Rendering** + - Some advanced terminal formatting may not render correctly + - Limited support for interactive terminal applications + +4. **Self-Knowledge** + - The agent may sometimes misunderstand its own capabilities + - Inconsistent awareness of feature limitations + +## Development Roadmap + +### Short-term Goals (Next 3 Months) + +1. **Reliability Improvements** + - Enhanced command execution stability + - Better error recovery mechanisms + - Improved context maintenance between sessions + +2. **Performance Optimizations** + - Faster codebase scanning and indexing + - Reduced memory footprint + - Optimized response generation + +3. **User Experience Enhancements** + - Improved terminal rendering + - Better progress indicators + - More intuitive command suggestions + +4. **Platform Support** + - Expanded Linux distribution support + - Better WSL compatibility + +### Medium-term Goals (3-9 Months) + +1. **Expanded Capabilities** + - Enhanced project-specific understanding + - Deeper integration with development workflows + - Support for more programming languages and frameworks + +2. **Collaboration Features** + - Shared context between team members + - Project-specific knowledge bases + - Team-oriented workflows + +3. **Integration Ecosystem** + - IDE plugins and extensions + - CI/CD pipeline integration + - Developer tool integrations + +4. **Enterprise Features** + - Team management capabilities + - Access control and permissions + - Compliance and security enhancements + +### Long-term Vision (9+ Months) + +1. **Advanced AI Capabilities** + - More sophisticated codebase understanding + - Predictive assistance based on development patterns + - Architecture-level reasoning and guidance + +2. **Ecosystem Development** + - Third-party plugin architecture + - API for custom tool integration + - Developer community resources + +3. **Specialized Versions** + - Domain-specific variants (e.g., mobile development, data science) + - Enterprise-focused editions + - Educational versions + +## Feature Request Pipeline + +### Current Prioritization + +1. **High Priority** + - Reliability improvements for core functionality + - Performance optimization for large codebases + - Critical bug fixes + +2. **Medium Priority** + - User experience enhancements + - New language support + - Additional tool integrations + +3. **Lower Priority** + - Specialized workflow features + - Advanced customization options + - Nice-to-have convenience features + +### Feedback Incorporation Process + +1. User feedback collected via `/bug` and `/feedback` commands +2. Issues tracked and prioritized in GitHub issue tracker +3. Recurring patterns identified across user reports +4. Feature requests assessed for alignment with product vision +5. Selected improvements incorporated into development sprints + +## Development Principles + +### Design Philosophy + +- **Natural Interaction**: Prioritize natural language understanding over command syntax +- **Context Awareness**: Maintain and utilize conversation and codebase context +- **Graceful Degradation**: Fail gracefully with helpful error messages +- **Progressive Enhancement**: Core functionality works everywhere, advanced features where supported + +### Quality Standards + +- Comprehensive test coverage for core functionality +- Regular security audits and dependency reviews +- Performance benchmarking against established targets +- Usability testing with representative user workflows + +### Release Cadence + +- Major feature releases: Every 2-3 months +- Bug fix and minor improvement releases: Every 2-4 weeks +- Critical fixes: As needed + +## Getting Involved + +### Contributing to Development + +- Report bugs and issues via GitHub or in-app reporting +- Provide detailed feedback on feature requests +- Participate in user research and testing programs +- Submit feature ideas through official channels + +### Testing Pre-release Versions + +- Sign up for beta testing program +- Access to preview releases +- Structured feedback collection +- Early exposure to upcoming features + +## Risk Assessment + +### Technical Risks + +- Integration complexity with diverse development environments +- Performance challenges with extremely large codebases +- Security considerations with command execution + +### Mitigation Strategies + +- Extensive testing across environments +- Performance optimization for scale +- Security-focused design and review +- Progressive feature rollout with monitoring \ No newline at end of file diff --git a/specs/error_handling.md b/specs/error_handling.md new file mode 100644 index 0000000..e02e9a1 --- /dev/null +++ b/specs/error_handling.md @@ -0,0 +1,185 @@ +# Claude Code CLI - Error Handling Specifications + +## Error Classification + +### Severity Levels + +| Level | Description | User Impact | Handling Approach | +|-------|-------------|------------|-------------------| +| Critical | Application cannot continue | Application termination | Immediate logging, clean shutdown, detailed recovery instructions | +| Major | Feature completely broken | Feature unavailable | Graceful degradation, alternative suggestion, detailed error message | +| Minor | Partial feature limitation | Reduced functionality | Warning message, continue with limited functionality | +| Informational | Non-disruptive issue | Minimal to none | Subtle notification, continue normal operation | + +### Error Categories + +| Category | Description | Examples | +|----------|-------------|----------| +| Authentication | Issues with user authentication | Invalid token, expired credentials, permission denied | +| Network | Connection and communication errors | API timeout, connection refused, SSL certificate error | +| File System | Issues with reading/writing files | Permission denied, file not found, locked file | +| Command Execution | Problems running terminal commands | Command not found, execution failure, timeout | +| AI Service | Issues with AI processing | Rate limit exceeded, invalid request, model error | +| Configuration | Problems with application setup | Invalid config format, missing required settings | +| Resource | System resource limitations | Out of memory, disk space exhausted | + +## Error Handling Strategies + +### Retry Mechanisms + +- Exponential backoff for transient errors +- Configurable retry limits +- Progress notification during retries +- Fallback mechanisms after exhausting retries + +### Graceful Degradation + +- Feature-specific fallback modes +- Reduced functionality operation +- Local-only operation when cloud services unavailable +- Cache utilization for resilience + +### User Notification + +- Clear, actionable error messages +- Technical details available on demand +- Suggested remediation steps +- Links to relevant documentation + +### Recovery Procedures + +- Session state preservation +- Auto-save mechanisms +- Crash recovery on restart +- Transaction rollback for failed operations + +## Error Tracking and Reporting + +### Telemetry Collection + +- Error frequency and patterns +- Environment context capture +- Anonymized error reports +- User-permitted crash reports + +### Sentry Integration + +- Real-time error tracking +- Exception grouping and analysis +- Performance impact assessment +- Release correlation + +### Error Aggregation + +- Pattern recognition across instances +- Prioritization based on impact +- Trend analysis for recurring issues +- User impact assessment + +## Specific Error Handling Cases + +### Authentication Errors + +- Clear re-authentication instructions +- Token refresh attempts +- Credential validation checks +- Security-focused error messages + +### API Communication Errors + +- Connection diagnostics +- Network environment checks +- Request/response logging +- API status verification + +### File System Errors + +- Permission verification +- Path validation +- Resource availability checks +- Alternative storage suggestions + +### Command Execution Errors + +- Shell environment validation +- Dependency verification +- Command path verification +- Output/error stream capture + +### AI Processing Errors + +- Query validation +- Context size management +- Rate limit handling +- Model fallback options + +## Error Logging + +### Log Levels + +| Level | Description | Information Included | +|-------|-------------|----------------------| +| ERROR | Significant problems | Error details, stack trace, context, user action | +| WARN | Potential issues | Warning details, related context, potential impact | +| INFO | Normal but significant events | Event description, relevant parameters | +| DEBUG | Detailed diagnostic information | Verbose execution details, state information | +| TRACE | Very detailed diagnostic information | Full execution path, variable states | + +### Log Structure + +```json +{ + "timestamp": "ISO-8601 timestamp", + "level": "ERROR|WARN|INFO|DEBUG|TRACE", + "message": "Human-readable message", + "category": "Error category", + "code": "Error code", + "user": { + "id": "Anonymized user identifier", + "action": "User action that triggered the error" + }, + "context": { + "command": "Executed command", + "file": "Relevant file path", + "operation": "Operation being performed" + }, + "technical": { + "stack": "Stack trace (if applicable)", + "raw": "Raw error details" + }, + "session": { + "id": "Session identifier", + "duration": "Session duration in seconds" + } +} +``` + +### Log Storage and Rotation + +- Local log files with rotation +- Size and time-based rotation policies +- Compression of archived logs +- Automated cleanup of old logs + +## User-Facing Error Messages + +### Message Structure + +- Clear problem statement +- Probable cause +- Suggested action +- Further information reference + +### Localization + +- Error message translation framework +- Locale-specific error resources +- Fallback to English for missing translations +- Cultural sensitivity in error messaging + +### Visual Presentation + +- Color-coding by severity +- Icons for error categories +- Progressive disclosure of technical details +- Contextual help links \ No newline at end of file diff --git a/specs/features.md b/specs/features.md new file mode 100644 index 0000000..df78945 --- /dev/null +++ b/specs/features.md @@ -0,0 +1,94 @@ +# Claude Code CLI - Feature Specifications + +## Code Understanding and Analysis + +### Code Reading +- File and directory navigation +- Code browsing with context awareness +- Search functionality (text and semantic) +- Codebase structure visualization + +### Code Comprehension +- Architecture explanation +- Function and class analysis +- Dependency mapping +- Pattern recognition +- Complexity assessment + +### Code Documentation +- Documentation generation +- Documentation querying +- Function explanation +- Comment suggestion + +## Code Editing and Generation + +### Edit Operations +- File creation and modification +- Code refactoring +- Bug fixing +- Code optimization +- Implementation of specified requirements + +### Generation Capabilities +- Boilerplate code generation +- Test case generation +- Function implementation +- API integration code +- Configuration files + +## Terminal and Command Execution + +### Command Execution +- Run terminal commands on behalf of the user +- Parse and explain command outputs +- Suggest command fixes for errors +- Handle long-running processes + +### Development Workflows +- Build process management +- Test execution and debugging +- Dependency installation and management +- Environment setup + +## Version Control Integration + +### Git Operations +- History search and analysis +- Commit preparation and creation +- Branch management +- Merge conflict resolution + +### Collaboration Features +- Pull request creation +- Code review assistance +- Change summarization +- Contribution guidelines compliance + +## Natural Language Interface + +### Query Processing +- Context-aware question answering +- Command parsing and execution +- Multi-turn conversations +- Error correction in user inputs + +### Response Generation +- Contextual explanations +- Step-by-step reasoning +- Code snippets with explanations +- Visual formatting where appropriate + +## Learning and Adaptation + +### Context Retention +- Session history awareness +- Project knowledge accumulation +- User preference learning +- Command pattern recognition + +### Improvement Mechanisms +- User feedback incorporation +- Error tracking and analysis +- Usage pattern optimization +- Performance monitoring \ No newline at end of file diff --git a/specs/index.md b/specs/index.md new file mode 100644 index 0000000..36c3c38 --- /dev/null +++ b/specs/index.md @@ -0,0 +1,44 @@ +# Claude Code CLI - Specification Index + +This document serves as an index to the comprehensive specification library for the Claude Code CLI application. + +## Core Documentation + +| Document | Description | +|----------|-------------| +| [Overview](overview.md) | General overview and introduction to Claude Code CLI | +| [Features](features.md) | Detailed feature specifications | +| [Architecture](architecture.md) | Technical architecture and system design | +| [Command Reference](command_reference.md) | Command line interface reference | + +## Technical Specifications + +| Document | Description | +|----------|-------------| +| [Security & Privacy](security_privacy.md) | Security and privacy specifications | +| [Integration](integration.md) | Integration points with other systems | +| [Error Handling](error_handling.md) | Error handling and reporting specifications | +| [Performance](performance.md) | Performance characteristics and optimizations | + +## User-Focused Documentation + +| Document | Description | +|----------|-------------| +| [Installation](installation.md) | Installation and setup guide | +| [Development](development.md) | Development status and roadmap | + +## Version Information + +These specifications describe Claude Code CLI version 0.2.29 and were created based on analysis of the compiled JavaScript bundle. + +## Notes on Specifications + +These specifications have been reconstructed through code analysis and documentation research. Some details may vary from the actual implementation, particularly regarding internal architecture and specific implementation patterns. + +The information provided is intended to serve as a comprehensive overview of the Claude Code CLI's capabilities, architecture, and usage patterns rather than a definitive implementation reference. + +## Additional Resources + +- [Official Documentation](https://docs.anthropic.com/en/docs/agents/claude-code/introduction) +- [GitHub Repository](https://github.com/anthropics/claude-code) +- [Issue Tracker](https://github.com/anthropics/claude-code/issues) \ No newline at end of file diff --git a/specs/installation.md b/specs/installation.md new file mode 100644 index 0000000..bbec989 --- /dev/null +++ b/specs/installation.md @@ -0,0 +1,185 @@ +# Claude Code CLI - Installation and Setup + +## System Requirements + +### Hardware Requirements + +| Component | Minimum | Recommended | +|-----------|---------|-------------| +| Processor | Dual-core 1.6 GHz | Quad-core 2.4 GHz or better | +| RAM | 4 GB | 8 GB or more | +| Disk Space | 500 MB free | 1 GB or more free | +| Network | Broadband connection | High-speed broadband connection | + +### Software Requirements + +| Component | Requirement | Notes | +|-----------|-------------|-------| +| Operating System | macOS 10.15+ or Linux (Ubuntu 18.04+, Debian 10+, etc.) | Windows not supported directly (requires WSL) | +| Node.js | v18.0.0 or higher | LTS version recommended | +| npm | v7.0.0 or higher | Included with Node.js | +| Git | Any recent version | Required for version control features | + +## Installation Methods + +### Global Installation (Recommended) + +```bash +npm install -g @anthropic-ai/claude-code +``` + +This will install Claude Code globally, making the `claude` command available throughout your system. + +### Project-Specific Installation + +```bash +cd your-project-directory +npm install @anthropic-ai/claude-code +``` + +When installed locally, you can run it using: + +```bash +npx claude +``` + +### Installation Verification + +To verify the installation was successful: + +```bash +claude --version +``` + +This should display the current version of Claude Code. + +## First-Time Setup + +### Authentication Setup + +1. Run `claude` in your terminal +2. You will be prompted to authenticate with Anthropic +3. A browser window will open for OAuth authentication +4. Sign in with your Anthropic Console account +5. Grant the requested permissions +6. Return to the terminal where authentication will be confirmed + +### Workspace Configuration + +Claude Code automatically recognizes and works with existing project structures, including: + +- Git repositories +- npm/yarn projects +- Standard directory layouts for common frameworks + +No additional configuration is typically required. + +### Optional Configuration + +A configuration file can be created at `~/.claude-code/config.json` with the following structure: + +```json +{ + "telemetry": true, + "logLevel": "info", + "maxHistorySize": 1000, + "theme": "dark", + "editor": { + "preferredLauncher": "code" + }, + "git": { + "preferredRemote": "origin" + } +} +``` + +## Environment Configuration + +### Environment Variables + +| Variable | Purpose | Default | +|----------|---------|---------| +| `CLAUDE_API_KEY` | Override API key | OAuth-provided token | +| `CLAUDE_CONFIG_PATH` | Custom config location | `~/.claude-code/config.json` | +| `CLAUDE_LOG_LEVEL` | Set logging verbosity | `info` | +| `CLAUDE_TELEMETRY` | Enable/disable telemetry | `true` | +| `CLAUDE_WORKSPACE` | Default workspace | Current directory | + +### Proxy Configuration + +Claude Code respects standard proxy environment variables: + +- `HTTP_PROXY` / `http_proxy` +- `HTTPS_PROXY` / `https_proxy` +- `NO_PROXY` / `no_proxy` + +## Update Procedures + +### Manual Update + +```bash +npm update -g @anthropic-ai/claude-code +``` + +### Automatic Update Checking + +Claude Code checks for updates on startup and notifies when a new version is available. + +### Version Rollback + +If needed, you can install a specific version: + +```bash +npm install -g @anthropic-ai/claude-code@0.2.28 +``` + +## Troubleshooting + +### Common Installation Issues + +| Issue | Possible Cause | Resolution | +|-------|---------------|------------| +| Permission errors | Insufficient npm permissions | Use `sudo` or fix npm permissions | +| Node version error | Outdated Node.js | Update Node.js to v18+ | +| Command not found | Path issues | Check PATH environment variable | +| Installation hangs | Network issues | Check network connection, try with `--verbose` | + +### Diagnostic Commands + +```bash +# Check Node.js version +node --version + +# Check npm version +npm --version + +# Verify Claude installation +which claude + +# Check Claude version +claude --version + +# Run with verbose logging +claude --verbose +``` + +### Support Resources + +- GitHub issues: https://github.com/anthropics/claude-code/issues +- Documentation: https://docs.anthropic.com/en/docs/agents/claude-code/introduction + +## Uninstallation + +### Complete Removal + +```bash +npm uninstall -g @anthropic-ai/claude-code +rm -rf ~/.claude-code +``` + +### Preserving Configuration + +```bash +npm uninstall -g @anthropic-ai/claude-code +# Configuration remains in ~/.claude-code for future installations +``` \ No newline at end of file diff --git a/specs/integration.md b/specs/integration.md new file mode 100644 index 0000000..9d0f64d --- /dev/null +++ b/specs/integration.md @@ -0,0 +1,150 @@ +# Claude Code CLI - Integration Specifications + +## External System Integrations + +### Anthropic API Integration + +| Aspect | Specification | +|--------|---------------| +| API Version | Claude AI API v1+ | +| Authentication | OAuth 2.0 | +| Request Format | JSON with context and query | +| Response Format | Structured JSON with actions and text | +| Rate Limiting | Adaptive based on usage patterns | + +### Version Control Systems + +| System | Integration Level | Capabilities | +|--------|-------------------|--------------| +| Git | Native | Full read/write access, history traversal, branch management | +| GitHub | API-based | PR creation, issue management, review comments | +| GitLab | API-based | MR creation, pipeline management, issue tracking | +| Bitbucket | API-based | PR workflows, repository management | + +### Development Toolchains + +| Tool Type | Integration Mechanism | Supported Operations | +|-----------|------------------------|----------------------| +| Package Managers | Command execution | Install, update, audit, publish | +| Build Systems | Command execution | Build, clean, incremental builds | +| Test Frameworks | Command execution + output parsing | Run tests, analyze results, debug failures | +| Linters/Formatters | Command execution + file modification | Analyze code, auto-fix issues, apply formatting | + +## Extensibility Framework + +### Plugin Architecture + +- Plugin discovery mechanism +- Registration and lifecycle management +- Capability extension points +- Version compatibility checking + +### Custom Command Integration + +- Command registration interface +- Parameter definition schema +- Help documentation generation +- Execution permission model + +### Tool Adapters + +- Adapter interface definition +- Output parser framework +- Error handling patterns +- Configuration schema + +## Interoperability + +### File Format Support + +| Format Category | Supported Formats | Operations | +|-----------------|-------------------|------------| +| Source Code | Multiple languages | Read, write, analyze, refactor | +| Configuration | JSON, YAML, TOML, etc. | Parse, validate, modify | +| Documentation | Markdown, RST, etc. | Generate, update, format | +| Data | CSV, JSON, etc. | Read, transform, validate | + +### Protocol Support + +| Protocol | Purpose | Implementation | +|----------|---------|----------------| +| HTTP/HTTPS | API communication | Native Node.js | +| SSH | Git operations, remote commands | SSH2 library | +| WebSocket | Real-time communication | ws library | + +### IDE Integration + +| IDE | Integration Method | Features | +|-----|-------------------|----------| +| VS Code | Extension | Command palette integration, context menu | +| JetBrains | Plugin | Tool window, action system integration | +| Vim/Neovim | Plugin | Command mode integration | + +## Authentication Mechanisms + +### Service Authentication + +- API key management +- OAuth token workflows +- Credential storage security +- Token refresh mechanisms + +### Repository Authentication + +- SSH key management +- HTTPS credential handling +- Token-based authentication +- Multi-factor authentication support + +## Integration Extension Points + +### Custom Tool Integrations + +- Tool definition interface +- Command output parsing +- Error handling protocol +- Help documentation generation + +### Custom AI Services + +- Alternative AI provider integration +- Model configuration options +- Response format adaptation +- Context preparation customization + +## Integration Configuration + +### Configuration File Format + +```json +{ + "integrations": { + "vcs": { + "provider": "git", + "settings": { + // Provider-specific settings + } + }, + "packageManager": { + "provider": "npm", + "settings": { + // Provider-specific settings + } + }, + "ai": { + "provider": "anthropic", + "settings": { + // Provider-specific settings + } + }, + // Additional integration configurations + } +} +``` + +### Environment Variable Integration + +- Environment variable mapping to configuration +- Secure handling of sensitive values +- Override precedence rules +- Dynamic reconfiguration support \ No newline at end of file diff --git a/specs/overview.md b/specs/overview.md new file mode 100644 index 0000000..be73857 --- /dev/null +++ b/specs/overview.md @@ -0,0 +1,47 @@ +# Claude Code CLI - Overview + +## Introduction + +Claude Code is an agentic coding tool developed by Anthropic that operates directly within the terminal environment. It is designed to understand codebases and assist developers in coding more efficiently through natural language interactions. + +## Purpose + +The primary purpose of Claude Code is to enhance developer productivity by automating routine tasks, providing insights into code structure and logic, and facilitating development workflows through natural language commands rather than complex syntax. + +## Target Users + +- Software developers and engineers +- DevOps professionals +- QA engineers +- Technical project managers +- Open source contributors + +## Key Value Propositions + +1. **Reduced Context Switching**: Developers can stay within their terminal environment while receiving AI assistance +2. **Codebase Understanding**: Claude can analyze and understand complex codebases to provide contextual assistance +3. **Task Automation**: Common development tasks can be delegated to Claude via natural language commands +4. **Knowledge Augmentation**: Claude can explain complex code, suggest improvements, and answer technical questions + +## Current Status + +Claude Code is currently in "Research Preview" (beta) status. This indicates: +- Early stage of product development +- Actively collecting user feedback +- Likely to undergo significant changes based on user experiences +- Some features may have limitations or known issues + +## Documentation + +Official documentation is available at: +https://docs.anthropic.com/en/docs/agents/claude-code/introduction + +## License and Terms + +The software is provided under Anthropic's Commercial Terms of Service, with specific provisions for the beta research preview. Usage implies acceptance of these terms. + +## Support Channels + +- In-application bug reporting via the `/bug` command +- GitHub issues: https://github.com/anthropics/claude-code/issues +- Official documentation for self-help resources \ No newline at end of file diff --git a/specs/performance.md b/specs/performance.md new file mode 100644 index 0000000..84b1c99 --- /dev/null +++ b/specs/performance.md @@ -0,0 +1,158 @@ +# Claude Code CLI - Performance Specifications + +## Response Time Targets + +### Interactive Operations + +| Operation Type | Target Response Time | Degraded Performance | Critical Threshold | +|----------------|----------------------|----------------------|-------------------| +| Command parsing | < 100ms | 100-500ms | > 500ms | +| Simple response generation | < 1s | 1-3s | > 3s | +| File operations (small files) | < 200ms | 200-1000ms | > 1s | +| Local search operations | < 500ms | 500ms-2s | > 2s | + +### AI-Dependent Operations + +| Operation Type | Target Response Time | Degraded Performance | Critical Threshold | +|----------------|----------------------|----------------------|-------------------| +| Simple AI queries | 1-3s | 3-8s | > 8s | +| Complex code explanations | 3-8s | 8-15s | > 15s | +| Multi-file code generation | 5-15s | 15-30s | > 30s | +| Codebase analysis | 10-30s | 30-60s | > 60s | + +### Background Operations + +| Operation Type | Expected Duration | Progress Indication | Cancellation Point | +|----------------|-------------------|---------------------|-------------------| +| Large codebase indexing | 1-5min | Every 10s | At file boundaries | +| Command execution | Command-dependent | Real-time | Command-dependent | +| Large file operations | 1-30s | Progress percentage | 10% increments | +| Network-dependent operations | 1-30s | Activity indication | At request boundaries | + +## Resource Utilization + +### Memory Usage + +| State | Target Usage | Maximum Allowed | Optimization Trigger | +|-------|-------------|-----------------|----------------------| +| Idle | < 100MB | 200MB | > 150MB | +| Active conversation | < 250MB | 500MB | > 350MB | +| Codebase analysis | 250-500MB | 1GB | > 700MB | +| Large file operations | Usage + 2x file size | Usage + 3x file size | > Usage + 2.5x file size | + +### CPU Utilization + +| Operation | Target Utilization | Duration | Cooling Period | +|-----------|-------------------|----------|---------------| +| Startup | Up to 100% | < 5s | N/A | +| Command processing | < 30% | < 2s | N/A | +| Codebase indexing | Up to 70% | < 5min | 30s if continuous | +| AI response processing | Up to 50% | < 10s | 5s between intensive operations | + +### Disk I/O + +| Operation | Read Rate | Write Rate | Batch Size | +|-----------|-----------|------------|------------| +| Configuration access | < 5MB/s | < 1MB/s | Small (< 100KB) | +| Code browsing | 10-50MB/s | Minimal | Medium (< 1MB) | +| Codebase indexing | 50-200MB/s | 5-20MB/s | Large (5-10MB) | +| Log writing | N/A | 1-5MB/s | Small (< 100KB) | + +### Network Usage + +| Operation | Bandwidth | Latency Tolerance | Retry Strategy | +|-----------|-----------|-------------------|---------------| +| Authentication | < 10KB | Low (< 500ms) | Exponential backoff, max 3 retries | +| AI requests | 10-100KB | Medium (< 2s) | Exponential backoff, max 5 retries | +| AI responses | 5-500KB | High (< 10s) | Resume from last chunk | +| Telemetry | < 50KB/session | Very high (minutes) | Queue and retry on next session | + +## Scaling Characteristics + +### Codebase Size Scaling + +| Codebase Size | Startup Time | Memory Footprint | Search Performance | +|---------------|--------------|------------------|-------------------| +| Small (<100 files) | < 3s | Base + 50MB | < 500ms | +| Medium (100-1000 files) | 3-10s | Base + 100-250MB | 500ms-2s | +| Large (1000-10000 files) | 10-30s | Base + 250-500MB | 2-5s | +| Very Large (>10000 files) | 30-120s | Base + 500MB-1GB | 5-15s | + +### Concurrent Operations + +| Operation Concurrency | Response Impact | Memory Impact | CPU Impact | +|------------------------|-----------------|--------------|------------| +| Single operation | Baseline | Baseline | Baseline | +| 2-3 operations | 1.2x slower | 1.5x usage | 1.5-2x usage | +| 4+ operations | 2x slower | 2x usage | 2-3x usage | + +## Optimization Techniques + +### Caching Strategies + +| Cache Type | Size Limit | Invalidation Trigger | Hit Rate Target | +|------------|------------|----------------------|-----------------| +| Command history | 1000 entries | Manual clear or overflow | > 20% | +| File content | 100MB | File modification or 10min | > 50% | +| AI responses | 50MB | Related file changes | > 30% | +| Search results | 25MB | 5min or file changes | > 40% | + +### Lazy Loading + +- On-demand file content loading +- Progressive codebase indexing +- Background initialization of non-critical components +- Deferred plugin loading + +### Parallelization + +- Multi-threaded file operations +- Background indexing and analysis +- Concurrent API requests where appropriate +- Pipeline processing for command outputs + +### Throttling and Backpressure + +- Rate limiting for API requests +- Disk I/O throttling during high load +- CPU usage monitoring and task deferral +- Memory pressure adaptive behavior + +## Performance Monitoring + +### Metrics Collection + +- Response time by operation type +- Resource utilization trends +- Cache effectiveness statistics +- Error rates and patterns + +### User Experience Indicators + +- Time to first response +- Command completion rate +- Perceived latency measurements +- User wait-time tracking + +### Automatic Adaptations + +- Dynamic cache size adjustment +- Background task priority modulation +- Resource allocation based on operation importance +- Feature disabling under extreme resource constraints + +## Performance Testing + +### Benchmark Scenarios + +- Small/medium/large codebase initialization +- Common command patterns execution +- Intensive operations (search, multi-file edits) +- Long-running session stability + +### Test Environments + +- Minimum specification machines +- Average developer workstations +- High-performance workstations +- Various operating systems (macOS, Linux) \ No newline at end of file