[
  {
    "path": ".eslintrc.json",
    "content": "{\n  \"parser\": \"@typescript-eslint/parser\",\n  \"extends\": [\"plugin:@typescript-eslint/recommended\"],\n  \"plugins\": [\"@typescript-eslint\"],\n  \"rules\": {}\n}\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: []\n"
  },
  {
    "path": ".github/workflows/npm-publish.yml",
    "content": "# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created\n# For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages\n\nname: Node.js Package\n\non:\n  release:\n    types: [created]\n  workflow_dispatch: # Allows you to run this workflow manually from the Actions tab\njobs:\n  publish-npm:\n    runs-on: ubuntu-latest\n    environment:\n      name: ohmyenv\n      url: https://github.com\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-node@v3\n        with:\n          node-version: 18\n          registry-url: https://registry.npmjs.org/\n      - run: npm ci\n      - run: npm run build\n      - run: npm publish\n        env:\n          NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}\n"
  },
  {
    "path": ".gitignore",
    "content": "/.idea/\n/dist/\n/node_modules/\n/.env\n/auto-copilot-cli.iml\n/config.json\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"singleQuote\": true,\n  \"trailingComma\": \"all\",\n  \"tabWidth\": 2,\n  \"printWidth\": 120,\n  \"semi\": true\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 Saryev Rustam\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "[![npm](https://img.shields.io/npm/v/auto-copilot-cli)](https://www.npmjs.com/package/auto-copilot-cli)\n[![Node.js Package](https://github.com/rsaryev/auto-copilot-cli/actions/workflows/npm-publish.yml/badge.svg)](https://github.com/rsaryev/auto-copilot-cli/actions/workflows/npm-publish.yml)\n[![MIT License](https://img.shields.io/badge/license-MIT-blue)](https://github.com/transitive-bullshit/chatgpt-api/blob/main/license)\n[![auto-copilot-cli npm downloads](https://img.shields.io/npm/dt/auto-copilot-cli)](https://www.npmjs.com/package/auto-copilot-cli)\n\n<p align=\"center\">\n  <img src=\"https://github.com/rsaryev/auto-copilot-cli/assets/70219513/8deb1865-6ec6-4dc8-a631-344627dabb83\" width=\"800\" alt=\"chat\">\n</p>\n\n## Description\n\n`auto-copilot-cli` is a versatile tool that offers several functionalities, including:\n\n- AI chat help you quickly find and improve codebase and answer questions about codebase\n- Code review\n- Pre-commit for generating commit messages\n- Code refactoring and linting structure of a folder or a file\n- Test generation\n- Shell command generation and execution\n- Natural language to SQL translation\n\n\n## Setup\n\n1. Install `auto-copilot-cli` globally:\n    ```bash\n   # using npm\n    npm install -g auto-copilot-cli\n   \n   # using install script\n   curl -s https://raw.githubusercontent.com/rsaryev/auto-copilot-cli/main/deployment/deploy.bash | bash\n    ```\n2. Get an API key from [OpenAI](https://platform.openai.com/account/api-keys).\n3. Refer to the [CLI usage](https://github.com/rsaryev/auto-copilot-cli/tree/main/docs) guide to learn how to use\n   the tool.\n\n### Commands\n\n- `code-chat <path>` - AI chat with codebase [usage](https://github.com/rsaryev/auto-copilot-cli/blob/main/docs/code-chat.md)\n    - Options:\n        - `-p, --prompt <prompt>` - Prompt for AI\n- `code-review` - Perform code review [usage](https://github.com/rsaryev/auto-copilot-cli/blob/main/docs/code-review.md) - Perform code review\n- `test <file>` - Generate test [usage](https://github.com/rsaryev/auto-copilot-cli/blob/main/docs/test.md)\n    - Options:\n        - `-p, --prompt <prompt>` - Prompt for AI\n        - `-o, --output <file>` - Output file\n- `refactor <file>` - Refactor code [usage](https://github.com/rsaryev/auto-copilot-cli/blob/main/docs/refactor.md)\n    - Options:\n        - `-p, --prompt <prompt>` - Prompt for AI\n        - `-o, --output <file>` - Output file\n- `sql-translator <query>` - Translate natural language to SQL [usage](https://github.com/rsaryev/auto-copilot-cli/blob/main/docs/sql-translator.md)\n    - Options:\n        - `-o, --output <output>` - Output sql file\n        - `-s, --schema-path <schemaPath>` - Path to schema file (sql, prisma, any format)\n- `chat <chat>` - Chat with AI [usage](https://github.com/rsaryev/auto-copilot-cli/blob/main/docs/chat.md)\n    - Options:\n        - `-p, --prompt <prompt>` - Prompt for AI\n- `shell <goal>` - Generate and execute a shell command [usage](https://github.com/rsaryev/auto-copilot-cli/blob/main/docs/shell.md)\n- `pre-commit` - Analyze git diff and generate a commit message [usage](https://github.com/rsaryev/auto-copilot-cli/blob/main/docs/pre-commit.md)\n    - Options:\n        - `-y, --yes` - Skip confirmation\n- `config <key> <value>` - Set configuration [usage](https://github.com/rsaryev/auto-copilot-cli/blob/main/docs/config.md)\n- `get-config` - Print configuration\n\n### Options\n\n- `-h, --help` - Display help for command\n- `-V, --version` - Output the version number\n\n\n## Contributing\n\nContributions are always welcome!\n"
  },
  {
    "path": "cli.js",
    "content": "#!/usr/bin/env node\nrequire('./dist/index.js');\n"
  },
  {
    "path": "deployment/deploy.bash",
    "content": "#!/bin/bash\n# Install Node.js version 18.16.0 if not already installed\nif ! node -v | grep -q \"^v18\\.\"; then\n  echo \"🚀 Node.js is not installed. Installing Node.js...\"\n  curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash\n  source ~/.nvm/nvm.sh\n  nvm install v18.16.0\nfi\n\nif ! npm list -g | grep -q \"auto-copilot-cli\"; then\n  echo \"🚀 Node.js is installed. Installing auto-copilot-cli...\"\n  npm install -g auto-copilot-cli\nfi\n\necho \"🚀 auto-copilot-cli is installed. Running copilot --help\"\n\n"
  },
  {
    "path": "docs/chat.md",
    "content": "# Chat with AI\n\n<p align=\"center\">\n  <img src=\"https://github.com/rsaryev/auto-copilot-cli/assets/70219513/85666309-ab3b-421f-8cbe-7c4efd7f5693\" width=\"800\" alt=\"chat\">\n</p>\n\n## Description\n\nChat with AI\n\n## Usage\n\n```bash\n# Chat with AI\n$ copilot chat \"How are you?\"\n\n# Chat with AI with prompt\n$ copilot chat \"How many types in typescript are there?\" -p \"Software Engineering\"\n```"
  },
  {
    "path": "docs/code-chat.md",
    "content": "# AI chat with codebase\n\n<p align=\"center\">\n  <img src=\"https://github.com/rsaryev/auto-copilot-cli/assets/70219513/8deb1865-6ec6-4dc8-a631-344627dabb83\" width=\"800\" alt=\"chat\">\n</p>\n\n## Description\n\nIn the chat, you can ask questions about the codebase.\nAI will answer your questions, and if necessary, it will offer code improvements.\nThis is very convenient when you want to quickly find something in the codebase, but don't want to waste time searching.\nIt is also convenient when you want to improve a specific function, you can ask \"How can I improve the function {function name}?\" and AI will suggest improvements.\nCodebase is analyzed using openai.\n\n## Usage\n\ncode-chat works only with files of popular programming languages and additionally with .txt files. All other files will be ignored.\n\n```bash\n$ copilot code-chat ./src\n```"
  },
  {
    "path": "docs/code-review.md",
    "content": "# Code review\n\n<p align=\"center\">\n    <img src=\"https://github.com/rsaryev/auto-copilot-cli/assets/70219513/d7abc8d7-9f5e-441c-8662-fe657ee07922\" width=\"800\" alt=\"code-review\">\n</p>\n\n## Description\n\nPerform code review and suggest improvements\n\n## Usage\n\nNeed to be in a git repository\nIf you want some files not to be checked, then add to .gitignore\n```bash\n# Code review\n$ copilot code-review\n```"
  },
  {
    "path": "docs/config.md",
    "content": "# Set Config\n\n## Description\n\nSet configuration\n\n## Usage\n\n```bash\n# Set openai api key\n$ copilot config OPENAI_API_KEY <api_key>\n\n# Set openai base url Default: https://api.openai.com/v1\n$ copilot config OPEN_AI_BASE_URL <base_url>\n\n# Set openai model Default: gpt-3.5-turbo\n$ copilot config MODEL <model>\n\n# Set config commit with description Default: no\ncopilot config INCLUDE_COMMIT_DESCRIPTION yes\n\n# Set config commit without description Default: no\ncopilot config INCLUDE_COMMIT_DESCRIPTION no\n\n# Set config package manager Default: brew\n# For determine which package manager to recommend in generated shell scripts\ncopilot config PACKAGE_MANAGER brew\n```\n\nGet Config\n```bash\n# Print config\n$ copilot get-config\n```"
  },
  {
    "path": "docs/pre-commit.md",
    "content": "# Pre-commit\n\n<p align=\"center\">\n  <img src=\"https://github.com/rsaryev/auto-copilot-cli/assets/70219513/805175ca-2d23-4468-9e11-8e3e1c1174cb\" width=\"800\" alt=\"Pre-commit\">\n</p>\n\n## Description\n\nAnalyze git diff and generate a commit message\n\n## Usage\n\nNeed to be in a git repository\nIf you want some files not to be checked, then add to .gitignore\n```bash\n# Analyzes git diff and generates a commit message\n$ copilot pre-commit\n\n# Analyzes git diff and generates a commit message with skip confirmation\n$ copilot pre-commit -y\n```"
  },
  {
    "path": "docs/refactor.md",
    "content": "# Refactor code\n\n<p align=\"center\">\n  <img src=\"https://github.com/rsaryev/auto-copilot-cli/assets/70219513/2c7da6ed-d74a-4aa3-a6d0-33031cc492c0\" width=\"800\" alt=\"refactor\">\n</p>\n\n## Description\n\nRefactor code\n\n## Usage\n\n```bash\n# Refactor code\n$ copilot refactor ./server.js\n\n# Refactor code with prompt\n$ copilot refactor ./server.js -p \"use typescript\"\n\n# Refactor code with prompt and output\n$ copilot refactor ./server.js -p \"use typescript\" -o ./server.ts\n```"
  },
  {
    "path": "docs/shell.md",
    "content": "# Generate and execute a shell command\n\n<p align=\"center\">\n  <img src=\"https://github.com/rsaryev/auto-copilot-cli/assets/70219513/4e2233cf-84ab-49b2-9d7a-1580d8d9cdd1\" width=\"800\" alt=\"shell\">\n</p>\n\n## Description\n\nGenerate and execute a shell command\n\n## Usage\n\n```bash\n# Convert all mov files to gif\n$ copilot shell \"convert all mov files to gif\"\n\n# Rename all files in the current directory to lowercase\n$ copilot shell \"rename files in the current directory to lowercase\"\n\n# Convert all images in the current directory to size 100x100\n$ copilot shell \"convert all images in the current directory to size 100x100\"\n\n# Create a file with implementation of binary search\n$ copilot shell \"create a js file with implementation of binary search\"\n\n# Create a simple web server in Node.js using Koajs\n$ copilot shell \"create a simple web server in Node.js using Koajs\"\n\n# Start PostgreSQL in Docker\n$ copilot shell \"start PostgreSQL in Docker\"\n```"
  },
  {
    "path": "docs/sql-translator.md",
    "content": "# SQL Translator\n\n<p align=\"center\">\n  <img src=\"https://github.com/rsaryev/auto-copilot-cli/assets/70219513/aa3c88d0-d747-48be-8406-7dbdab11061e\" width=\"800\" alt=\"sql-translator\">\n</p>\n\n## Description\n\nTranslate natural language to SQL\n\n## Usage\n\n```bash\n# Translate natural language to SQL\n$ copilot sql-translator \"get all last posts of users\"\n\n# Translate natural language to SQL with output\n$ copilot sql-translator \"get all last posts of users\"\n\n# Translate natural language to SQL with output and sql \n$ copilot sql-translator \"get all last posts of users\" -s ./schema.sql\n\n# Translate natural language to SQL with output and prisma schema\n$ copilot sql-translator \"get all last posts of users\" -s ./schema.prisma\n\n```"
  },
  {
    "path": "docs/test.md",
    "content": "# Generate test\n\n<p align=\"center\">\n  <img src=\"https://github.com/rsaryev/auto-copilot-cli/assets/70219513/e405d17f-598c-457e-9827-1f7d8117e2b7\" width=\"800\" alt=\"sql-translator\">\n</p>\n\n## Description\n\nGenerate test\n\n## Usage\n\n```bash\n# Generate test\n$ copilot test ./server.js\n\n# Generate test with prompt\n$ copilot test ./server.js -p \"use jest framework\"\n\n# Generate test with prompt and output\n$ copilot test ./server.js -p \"use jest framework\" -o ./server.test.js\n```"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"auto-copilot-cli\",\n  \"version\": \"1.1.2\",\n  \"main\": \"index.js\",\n  \"description\": \"This CLI tool uses the ChatGPT language model to create commands. This allows you to create a list of tasks and perform them sequentially, optimizing your workflow and increasing the efficiency of repetitive actions..\",\n  \"scripts\": {\n    \"build\": \"tsc\",\n    \"lint\": \"eslint . --ext .ts\",\n    \"lint:fix\": \"eslint . --ext .ts --fix\",\n    \"pretty\": \"prettier --write .\"\n  },\n  \"bin\": {\n    \"auto-copilot-cli\": \"cli.js\",\n    \"copilot\": \"cli.js\"\n  },\n  \"files\": [\n    \"dist\",\n    \"cli.js\",\n    \"README.md\",\n    \"config.json\",\n    \"demo\"\n  ],\n  \"author\": \"Rustam Saryev\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"@types/cli-table\": \"^0.3.1\",\n    \"@types/inquirer\": \"^9.0.3\",\n    \"@types/node\": \"^18.16.3\",\n    \"@typescript-eslint/eslint-plugin\": \"^5.59.2\",\n    \"@typescript-eslint/parser\": \"^5.59.2\",\n    \"eslint\": \"^8.39.0\",\n    \"eslint-plugin-import\": \"^2.27.5\",\n    \"prettier\": \"^2.8.8\",\n    \"ts-node\": \"^10.9.1\",\n    \"typescript\": \"^5.0.4\"\n  },\n  \"dependencies\": {\n    \"@dqbd/tiktoken\": \"^1.0.7\",\n    \"ai-validator\": \"^1.0.76\",\n    \"ajv\": \"^8.12.0\",\n    \"axios\": \"^1.4.0\",\n    \"chalk\": \"^4.1.2\",\n    \"cli-table\": \"^0.3.11\",\n    \"commander\": \"^10.0.1\",\n    \"directory-tree\": \"^3.5.1\",\n    \"hnswlib-node\": \"^1.4.2\",\n    \"inquirer\": \"^8.2.5\",\n    \"langchain\": \"^0.0.94\",\n    \"openai\": \"^3.2.1\",\n    \"ora\": \"^5.4.1\",\n    \"semver\": \"^7.5.0\",\n    \"simple-git\": \"^3.18.0\",\n    \"typeorm\": \"^0.3.15\",\n    \"winston\": \"^3.8.2\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/rsaryev/auto-copilot-cli.git\"\n  },\n  \"keywords\": [\n    \"copilot\",\n    \"cli\",\n    \"chatgpt\",\n    \"openai\"\n  ],\n  \"bugs\": {\n    \"url\": \"https://github.com/rsaryev/auto-copilot-cli/issues\"\n  },\n  \"homepage\": \"https://github.com/rsaryev/auto-copilot-cli#readme\"\n}\n"
  },
  {
    "path": "src/commands/chat.ts",
    "content": "import {Command} from '../types';\nimport {LLMChat} from '../llm';\nimport * as readline from 'readline';\nimport {inputAsk} from '../utils';\n\nexport class ChatCommand extends Command {\n    async execute(\n        message: string,\n        options: {\n            prompt?: string;\n        },\n    ): Promise<void> {\n        const {prompt} = options;\n\n        let input = '';\n        while (input !== 'exit') {\n            input = await inputAsk();\n            await LLMChat.chat({\n                config: this.config,\n                input,\n                prompt,\n                handleLLMStart: () => {\n                    readline.cursorTo(process.stdout, 0);\n                    process.stdout.write('🤖 ');\n                },\n                handleLLMEnd: () => {\n                    process.stdout.write('\\n');\n                },\n                handleLLMError: () => {\n                    process.stdout.write('\\n');\n                },\n                handleLLMNewToken: (token: string) => {\n                    process.stdout.write(token);\n                },\n            });\n        }\n    }\n}"
  },
  {
    "path": "src/commands/code-chat-command.ts",
    "content": "import {Command} from '../types';\nimport {LLMCodeChat} from '../llm';\nimport * as readline from 'readline';\n\nexport class CodeChatCommand extends Command {\n    async execute(\n        directory: string,\n        options: {\n            prompt?: string;\n        },\n    ): Promise<void> {\n        await LLMCodeChat.chat({\n            config: this.config,\n            directory,\n            input: '',\n            prompt: options.prompt,\n            handleLLMStart: () => {\n                readline.cursorTo(process.stdout, 0);\n                process.stdout.write('🤖 ');\n            },\n            handleLLMEnd: () => {\n                process.stdout.write('\\n');\n            },\n            handleLLMError: () => {\n                process.stdout.write('\\n');\n            },\n            handleLLMNewToken: (token: string) => {\n                process.stdout.write(token);\n            },\n        });\n    }\n}\n"
  },
  {
    "path": "src/commands/code-review.ts",
    "content": "import {Command} from '../types';\nimport {LLMCodeReview} from '../llm';\nimport chalk from 'chalk';\nimport path from 'path';\nimport fs from 'fs';\nimport {gitDiffFiles} from '../utils/git';\n\nexport class CodeReviewCommand extends Command {\n    async execute(message: string, options: { yes?: string }): Promise<void> {\n        const diffFiles = await gitDiffFiles();\n        if (diffFiles.length === 0) {\n            console.log(`${chalk.red('✘')} No files to review, use git add to add files to review`);\n            return;\n        }\n\n        console.log(`${chalk.green('✔')} Found ${diffFiles.length} files to review`);\n\n        const logPath = path.resolve(process.cwd(), 'review.log');\n        const writeStream = fs.createWriteStream(logPath, {flags: 'a'});\n\n        console.log(`${chalk.green('✔')} ${chalk.yellow('Writing review log to')} ${logPath}\\n`);\n\n        for (const file of diffFiles) {\n            console.log(`${chalk.green('✔')} ${chalk.yellow('Reviewing')} ${file}`);\n\n            const filePath = path.resolve(process.cwd(), file);\n            const content = fs.readFileSync(filePath, 'utf-8');\n\n            if (content === '') {\n                console.log(`${chalk.red('✘')} ${chalk.yellow('Skip empty file')}`);\n                continue;\n            }\n\n            if (content.length > 10000) {\n                console.log(`${chalk.red('✘')} ${chalk.yellow('Skip large file')}`);\n                continue;\n            }\n\n            await LLMCodeReview.codeReview({\n                config: this.config,\n                content,\n                filePath,\n                handleLLMStart: async () => {\n                    process.stdout.write('\\n');\n                },\n                handleLLMEnd: async () => {\n                    process.stdout.write('\\n');\n                },\n                handleLLMError: async () => {\n                    process.stdout.write('\\n');\n                },\n                handleLLMNewToken: async (token: string) => {\n                    process.stdout.write(token);\n                    writeStream.write(token);\n                },\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "src/commands/generate-tests.ts",
    "content": "import {Command} from '../types';\nimport fs from 'fs';\nimport chalk from 'chalk';\nimport {askOpenEditor, askTest, inputTest} from '../utils';\nimport {exec} from 'child_process';\nimport ora from 'ora';\nimport {LLMCode} from '../llm';\n\nexport class TestCommand extends Command {\n    async execute(\n        path: string,\n        {\n            prompt,\n            output,\n        }: {\n            prompt?: string;\n            output?: string;\n        },\n    ): Promise<void> {\n        if (!fs.existsSync(path)) {\n            console.error(`${chalk.red('✘')} no such file or directory: ${path}`);\n            return;\n        }\n\n        const fileType = path.split('.').pop();\n        if (!fileType) {\n            console.error(`${chalk.red('✘')} invalid file type: ${path}`);\n            return;\n        }\n\n        output = output || path.replace(`.${fileType}`, `.test.${fileType}`);\n        const questionOpenCode = await askOpenEditor();\n        if (questionOpenCode) {\n            exec(`${this.config.EDITOR || 'code'} ${output}`);\n        }\n\n        const spinner = ora('Generating tests');\n        const content = fs.readFileSync(path, 'utf-8');\n        const handleLLMStart = () => spinner.start();\n        const handleLLMEnd = () => spinner.succeed('Successfully generated tests');\n        const handleLLMError = () => spinner.fail();\n\n        await LLMCode.generateTest({\n            config: this.config,\n            content,\n            prompt: prompt,\n            output: output,\n            handleLLMStart,\n            handleLLMEnd,\n            handleLLMError,\n        });\n\n        const answer = await askTest();\n        if (answer) {\n            prompt = await inputTest();\n            await this.execute(output, {\n                prompt,\n                output,\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "src/commands/pre-commit.ts",
    "content": "import {Command} from '../types';\nimport {LLMPreCommit} from '../llm';\nimport {exec} from 'child_process';\nimport {promisify} from 'util';\nimport {askCommit, askRetryCommit} from '../utils';\nimport ora from 'ora';\nimport {gitDiffCommand} from '../utils/git';\n\nexport class PreCommitCommand extends Command {\n    async execute(\n        message: string,\n        options: {\n            yes?: string;\n        },\n    ): Promise<void> {\n        const spinner = ora('Analyzing').start();\n        try {\n            const {config} = this;\n\n            const diff = await gitDiffCommand();\n            if (!diff) {\n                spinner.succeed('No diff found using git add');\n                return;\n            }\n\n            const {title, messages} = await LLMPreCommit.preCommit({config, diff});\n            spinner.stop();\n\n            const commitBullets = messages?.map((message) => `- ${message}`).join('\\n') ?? '';\n            const fullCommitMessage = `\"${title}${commitBullets ? `\\n\\n${commitBullets}` : ''}\"`;\n            const shouldCommit = options.yes ? true : await askCommit(fullCommitMessage);\n\n            if (shouldCommit) {\n                spinner.text = 'Committing';\n                await promisify(exec)(`git commit -m ${fullCommitMessage}`);\n                spinner.succeed('Successfully committed');\n            } else {\n                const shouldRetry = await askRetryCommit();\n                if (shouldRetry) await this.execute(message, options);\n            }\n        } catch (error) {\n            spinner.fail('Failed to commit');\n            throw error;\n        }\n    }\n}\n"
  },
  {
    "path": "src/commands/refactor.ts",
    "content": "import {Command} from '../types';\nimport fs from 'fs';\nimport chalk from 'chalk';\nimport {askOpenEditor, askRetryRefactor, inputRefactor} from '../utils';\nimport {exec} from 'child_process';\nimport ora from 'ora';\nimport {LLMCode} from '../llm';\n\nexport class RefactorCommand extends Command {\n    public async execute(\n        filePath: string,\n        {\n            prompt,\n            output,\n        }: {\n            prompt?: string;\n            output?: string;\n        },\n    ): Promise<void> {\n        if (!fs.existsSync(filePath)) {\n            console.error(`${chalk.red('✘')} no such file or directory: ${filePath}`);\n            return;\n        }\n\n        const fileType = filePath.split('.').pop();\n        if (!fileType) {\n            console.error(`${chalk.red('✘')} invalid file type: ${filePath}`);\n            return;\n        }\n\n        output = output || filePath.replace(`.${fileType}`, `.refactored.${fileType}`);\n\n        const questionOpenCode = await askOpenEditor();\n        if (questionOpenCode) {\n            exec(`${this.config.EDITOR || 'code'} ${output}`);\n        }\n\n        const spinner = ora('Refactoring');\n        const content = fs.readFileSync(filePath, 'utf-8');\n\n        const handleLLMStart = () => spinner.start();\n        const handleLLMEnd = () => spinner.succeed('Successfully refactored');\n        const handleLLMError = () => spinner.fail();\n\n        await LLMCode.refactor({\n            config: this.config,\n            content,\n            prompt: prompt,\n            output: output,\n            handleLLMStart,\n            handleLLMEnd,\n            handleLLMError,\n        });\n\n        const answer = await askRetryRefactor();\n        if (answer) {\n            const input = await inputRefactor();\n            await this.execute(output, {\n                prompt: input,\n                output,\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "src/commands/shell.ts",
    "content": "import {Command} from '../types';\nimport path from 'path';\nimport fs from 'fs';\nimport chalk from 'chalk';\nimport {randomUUID} from 'crypto';\nimport {executeCommand, executeShell, exFunction} from '../utils/helpers';\nimport {askExecute, askOpenEditor} from '../utils';\nimport {LLMGenerateShell} from '../llm';\nimport os from 'os';\n\nexport class ShellCommand extends Command {\n    async execute(goal: string): Promise<void> {\n        const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'auto_copilot_cli'));\n        const pathToSaveShellScript = path.join(tempDir, `./${randomUUID()}.sh`);\n        const shellScript = await exFunction(\n            LLMGenerateShell.generateShell.bind(null, this.config, goal),\n            'Pending',\n            'Done',\n        );\n        fs.writeFileSync(pathToSaveShellScript, shellScript.shellScript);\n        console.log(`${shellScript.isDangerous ? chalk.red('✘') : chalk.green('✔')} Safe | ${shellScript.description}`);\n\n        const questionOpenScript = await askOpenEditor();\n\n        if (questionOpenScript) {\n            const command = `${this.config.EDITOR || 'code'} ${pathToSaveShellScript}`;\n            await executeCommand(command);\n        }\n        const isApproved = await askExecute();\n        if (isApproved) {\n            const shellScriptModified = fs.readFileSync(pathToSaveShellScript, 'utf-8');\n            await executeShell(shellScriptModified.toString());\n        }\n    }\n}\n"
  },
  {
    "path": "src/commands/sql-translator.ts",
    "content": "import {Command} from '../types';\nimport fs from 'fs';\nimport chalk from 'chalk';\nimport {askOpenEditor, askRetryRefactor, inputRefactor} from '../utils';\nimport {exec} from 'child_process';\nimport ora from 'ora';\nimport {LLMCode} from '../llm';\n\nexport class SqlTranslatorCommand extends Command {\n    public async execute(\n        query: string,\n        {\n            output,\n            schemaPath,\n        }: {\n            output?: string;\n            schemaPath?: string;\n        },\n    ): Promise<void> {\n        if (schemaPath && !fs.existsSync(schemaPath)) {\n            console.error(`${chalk.red('✘')} no such file or directory: ${schemaPath}`);\n            return;\n        }\n\n        output = output || 'output.sql';\n        const questionOpenCode = await askOpenEditor();\n        if (questionOpenCode) {\n            exec(`${this.config.EDITOR || 'code'} ${output}`);\n        }\n\n        const spinner = ora('Translating');\n        const schema = schemaPath ? fs.readFileSync(schemaPath, 'utf-8') : '';\n        const handleLLMStart = () => spinner.start();\n        const handleLLMEnd = () => spinner.succeed('Successfully translated');\n        const handleLLMError = () => spinner.fail();\n        await LLMCode.translateSql({\n            config: this.config,\n            content: schema,\n            prompt: query,\n            output,\n            handleLLMStart,\n            handleLLMEnd,\n            handleLLMError,\n        });\n\n        const answer = await askRetryRefactor();\n        if (answer) {\n            const input = await inputRefactor();\n            await this.execute(input, {\n                schemaPath,\n                output,\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "src/config/config.ts",
    "content": "import * as fs from 'fs';\nimport * as path from 'path';\nimport {IConfig} from '../types';\n\nconst configPath = path.join(__dirname, '../../config.json');\n\nconst defaultConfig: IConfig = {\n    OPENAI_API_KEY: 'sk-xxx',\n    TEMPERATURE: 0,\n    MODEL: 'gpt-3.5-turbo-0613',\n    EDITOR: 'code',\n    OPEN_AI_BASE_URL: 'https://api.openai.com/v1',\n    INCLUDE_COMMIT_DESCRIPTION: 'no',\n    PACKAGE_MANAGER: 'brew',\n};\n\nexport function setConfig(config: IConfig): void {\n    fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');\n}\n\n// Set a specific configuration property by key\nexport function setConfigByKey<K extends keyof IConfig>(key: K, value: IConfig[K]): void {\n    const config = getConfig();\n    config[key] = value;\n    setConfig(config);\n}\n\nexport function getConfig(): IConfig {\n    if (!fs.existsSync(configPath)) {\n        setConfig(defaultConfig);\n    }\n    const existingConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));\n    if (!existingConfig.OPENAI_API_KEY) {\n        existingConfig.OPENAI_API_KEY = defaultConfig.OPENAI_API_KEY;\n        setConfig(existingConfig);\n    }\n    return {...defaultConfig, ...existingConfig};\n}\n"
  },
  {
    "path": "src/index.ts",
    "content": "import { Command } from 'commander';\n// @ts-ignore\nimport { version } from '../package.json';\nimport { getConfig, setConfig, setConfigByKey } from './config/config';\nimport { TestCommand } from './commands/generate-tests';\nimport { RefactorCommand } from './commands/refactor';\nimport { ChatCommand } from './commands/chat';\nimport { ShellCommand } from './commands/shell';\nimport { IConfig } from './types';\nimport axios, { AxiosError } from 'axios';\nimport { askOpenAIKey } from './utils';\nimport chalk from 'chalk';\nimport { PreCommitCommand } from './commands/pre-commit';\nimport { checkNodeVersion } from './utils/helpers';\nimport { checkUpdate } from './utils/update';\nimport { SqlTranslatorCommand } from './commands/sql-translator';\nimport { CodeReviewCommand } from './commands/code-review';\nimport { checkGitExists } from './utils/git';\nimport { CodeChatCommand } from './commands/code-chat-command';\n\nconst program: Command = new Command()\n  .name('auto-copilot-cli')\n  .description('Auto Copilot CLI')\n  .version(version)\n  .alias('copilot');\n\ntype IOption = {\n  name: string;\n  description: string;\n  required: boolean;\n};\ntype ICommand = {\n  name: string;\n  description: string;\n  args: string;\n  options: IOption[];\n  action: (...args: any[]) => Promise<void>;\n};\n\nconst testCommand: ICommand = {\n  name: 'test',\n  description: 'Generate test',\n  args: '<file>',\n  options: [\n    {\n      name: '-p, --prompt <prompt>',\n      description: 'Prompt for AI',\n      required: false,\n    },\n    {\n      name: '-o, --output <output>',\n      description: 'Output file',\n      required: false,\n    },\n  ],\n  action: async (file: string, options: { prompt?: string; output?: string }): Promise<void> => {\n    const config: IConfig = getConfig();\n    const testCommand: TestCommand = new TestCommand(config);\n    await testCommand.execute(file, options);\n  },\n};\n\nconst refactorCommand: ICommand = {\n  name: 'refactor',\n  description: 'Refactor code',\n  args: '<file>',\n  options: [\n    {\n      name: '-p, --prompt <prompt>',\n      description: 'Prompt for AI',\n      required: false,\n    },\n    {\n      name: '-o, --output <output>',\n      description: 'Output file',\n      required: false,\n    },\n  ],\n  action: async (file: string, options: { prompt?: string; output?: string }): Promise<void> => {\n    const config: IConfig = getConfig();\n    const refactorCommand: RefactorCommand = new RefactorCommand(config);\n    await refactorCommand.execute(file, options);\n  },\n};\n\nconst sqlTranslatorCommand: ICommand = {\n  name: 'sql-translator',\n  description: 'Translate natural language to SQL',\n  args: '<query>',\n  options: [\n    {\n      name: '-o, --output <output>',\n      description: 'Output sql file',\n      required: false,\n    },\n    {\n      name: '-s, --schema-path <schemaPath>',\n      description: 'Path to schema file (sql, prisma, any format)',\n      required: false,\n    },\n  ],\n  action: async (query: string, options: { schemaPath?: string; output?: string }): Promise<void> => {\n    const config: IConfig = getConfig();\n    const sqlCommand: SqlTranslatorCommand = new SqlTranslatorCommand(config);\n    await sqlCommand.execute(query, options);\n  },\n};\n\nconst chatCommand: ICommand = {\n  name: 'chat',\n  description: 'Chat with AI',\n  args: '',\n  options: [\n    {\n      name: '-p, --prompt <prompt>',\n      description: 'Prompt for AI',\n      required: false,\n    },\n  ],\n  action: async (message: string, options: { prompt?: string }): Promise<void> => {\n    const config: IConfig = getConfig();\n    const chatCommand: ChatCommand = new ChatCommand(config);\n    await chatCommand.execute(message, options);\n  },\n};\n\nconst shellCommand: ICommand = {\n  name: 'shell',\n  description: 'Generate and execute a shell command',\n  args: '<goal>',\n  options: [],\n  action: async (goal: string): Promise<void> => {\n    const config: IConfig = getConfig();\n    const shellCommand: ShellCommand = new ShellCommand(config);\n    await shellCommand.execute(goal);\n  },\n};\n\nconst configCommand: ICommand = {\n  name: 'config',\n  description: 'Set config',\n  args: '<key> <value>',\n  options: [],\n  action: async (key: keyof IConfig, value: string): Promise<void> => setConfigByKey(key, value),\n};\n\nconst getConfigCommand: ICommand = {\n  name: 'get-config',\n  description: 'Print config',\n  args: '',\n  options: [],\n  action: async (): Promise<void> => {\n    const config: any = getConfig();\n    console.table(Object.keys(config).map((key: string) => ({ key, value: config[key] })));\n  },\n};\n\nconst preCommitCommand: ICommand = {\n  name: 'pre-commit',\n  description: 'Pre commit hook',\n  args: '',\n  options: [\n    {\n      name: '-y, --yes',\n      description: 'Skip confirmation',\n      required: false,\n    },\n  ],\n  action: async (options: { yes?: string }): Promise<void> => {\n    await checkGitExists();\n    const config: IConfig = getConfig();\n    const preCommitCommand: PreCommitCommand = new PreCommitCommand(config);\n    await preCommitCommand.execute('', options);\n  },\n};\n\nconst codeReviewCommand: ICommand = {\n  name: 'code-review',\n  description: 'Code review',\n  args: '',\n  options: [\n    {\n      name: '-y, --yes',\n      description: 'Skip confirmation',\n      required: false,\n    },\n  ],\n  action: async (options: { yes?: string }): Promise<void> => {\n    await checkGitExists();\n    const config: IConfig = getConfig();\n    const codeReviewCommand = new CodeReviewCommand(config);\n    await codeReviewCommand.execute('', options);\n  },\n};\n\nconst codeChatCommand: ICommand = {\n  name: 'code-chat',\n  description: 'Chat with AI about code',\n  args: '<directory>',\n  options: [\n    {\n      name: '-p, --prompt <prompt>',\n      description: 'Prompt for AI',\n      required: false,\n    },\n  ],\n  action: async (directory: string, options: { prompt?: string }): Promise<void> => {\n    const config: IConfig = getConfig();\n    const codeChatCommand: CodeChatCommand = new CodeChatCommand(config);\n    await codeChatCommand.execute(directory, options);\n  },\n};\n\nconst commands: ICommand[] = [\n  testCommand,\n  refactorCommand,\n  chatCommand,\n  shellCommand,\n  configCommand,\n  getConfigCommand,\n  preCommitCommand,\n  sqlTranslatorCommand,\n  codeReviewCommand,\n  codeChatCommand,\n];\n\nasync function main() {\n  checkNodeVersion();\n  await checkUpdate();\n  commands.forEach(({ name, description, args, options, action }) => {\n    const command: Command = new Command(name).description(description);\n    if (args) {\n      command.arguments(args);\n    }\n    options.forEach(({ name, description, required }) => {\n      command.option(name, description, required);\n    });\n\n    const handler = async (...args: any[]): Promise<void> => {\n      const config: IConfig = getConfig();\n      try {\n        await action(...args);\n      } catch (error: any) {\n        if (axios.isAxiosError(error)) {\n          if ((error as AxiosError).response?.status === 401) {\n            config.OPENAI_API_KEY = await askOpenAIKey();\n            setConfig(config);\n            return handler(...args);\n          } else if ((error as AxiosError).response?.status === 429) {\n            console.log(`${chalk.red('✘')} ${chalk.yellow('You have reached your OpenAI API usage limit')}`);\n            return;\n          } else if ((error as AxiosError).response?.status === 500) {\n            console.log(`${chalk.red('✘')} ${chalk.yellow('OpenAI API is down')}`);\n            return;\n          }\n        }\n        console.log(`${chalk.red('✘')} ${error.response?.data?.error?.message || error.message}`);\n      }\n    };\n\n    command.action(handler);\n    program.addCommand(command);\n  });\n\n  program.parse(process.argv);\n}\n\nmain();\n"
  },
  {
    "path": "src/llm/index.ts",
    "content": "import {PromptTemplate} from 'langchain/prompts';\nimport {z} from 'zod';\nimport * as os from 'os';\nimport {IChatParams, IConfig, IRefactorParams, ShellScriptResponse} from '../types';\nimport {OpenAI, OpenAIChat} from 'langchain/llms/openai';\nimport fs from 'fs';\nimport {throwLLMParseError} from '../utils/error';\nimport {ChatCompletionRequestMessage} from 'openai';\nimport path from 'path';\nimport {AiValidator} from 'ai-validator';\nimport {calculateCost, getPackageManagerByOs} from '../utils/helpers';\nimport {OpenAIEmbeddings} from 'langchain/embeddings/openai';\nimport {RecursiveCharacterTextSplitter} from 'langchain/text_splitter';\nimport {TextLoader} from 'langchain/document_loaders/fs/text';\nimport {customAsk, inputAsk} from '../utils';\nimport ora from 'ora';\nimport {DirectoryLoader} from 'langchain/document_loaders/fs/directory';\nimport {extensionsList} from '../utils/language-extensions';\nimport * as process from 'process';\nimport {HNSWLib} from 'langchain/vectorstores/hnswlib';\n\nexport class LLMCommand {\n    protected llm: OpenAI;\n    protected config: IConfig;\n\n    constructor(config: IConfig, maxTokens: number, streaming: boolean, temperature = 0) {\n        this.config = config;\n        this.llm = new OpenAI(\n            {\n                modelName: config.MODEL,\n                maxTokens,\n                temperature,\n                openAIApiKey: config.OPENAI_API_KEY,\n                streaming,\n            },\n            {\n                basePath: config.OPEN_AI_BASE_URL,\n            },\n        );\n    }\n}\n\nexport class LLMGenerateShell extends LLMCommand {\n    constructor(config: IConfig) {\n        super(config, 1024, false);\n    }\n\n    static async generateShell(config: IConfig, prompt: string): Promise<ShellScriptResponse> {\n        return new LLMGenerateShell(config).generateShell(prompt);\n    }\n\n    async generateShell(prompt: string): Promise<ShellScriptResponse> {\n        const packageManager = this.config.PACKAGE_MANAGER || getPackageManagerByOs();\n        const schema = z.object({\n            shellScript: z.string().describe(`shell script with comments`),\n            isDangerous: z.boolean().describe(`if the shell is very dangerous, it will be marked as dangerous`),\n            description: z.string().describe(`short description`),\n        });\n        const validator = AiValidator.input`\nGoal: Write the best shell script based on the prompt: \\`${prompt}\\`\n\nConstraints for the shell script:\n- should be compatible with the ${os.platform()}.\n- Should work without user intervention and should not require keyboard input.\n- Every step should be printed to the console so that the user can understand what is happening.\n- Check the installed packages and install the missing packages if necessary.\n- If you need to create a file use operator \"Here Document\" (<<) to create a multiline string:\n\\`\\`\\`\ncat << EOF > file.txt\n{{content}}\nEOF\n\\`\\`\\`\n- Use package manager ${packageManager}\n \nRecommendations:\n- Use best practices\n- Use the best tools for the job\n- Use the best practices for writing shell scripts\n\n${schema}\nThe current time and date is ${new Date().toLocaleString()}\nThe current working directory is ${process.cwd()}\nThe current os platform is ${os.platform()}\n`;\n        const response = await this.llm.call(validator.prompt());\n        try {\n            return validator.parse(response);\n        } catch (error) {\n            return throwLLMParseError();\n        }\n    }\n}\n\nexport class LLMCode extends LLMCommand {\n    constructor(config: IConfig) {\n        super(config, 2056, true);\n    }\n\n    static async refactor(\n        params: IRefactorParams & {\n            config: IConfig;\n        },\n    ): Promise<void> {\n        return new LLMCode(params.config).refactor(params);\n    }\n\n    static async generateTest(\n        params: IRefactorParams & {\n            config: IConfig;\n        },\n    ): Promise<void> {\n        return new LLMCode(params.config).generateTest(params);\n    }\n\n    static async translateSql(\n        params: IRefactorParams & {\n            config: IConfig;\n        },\n    ): Promise<void> {\n        return new LLMCode(params.config).translateSql(params);\n    }\n\n    async translateSql(params: IRefactorParams): Promise<void> {\n        const promptTemplate = new PromptTemplate({\n            template: `\nGoal: Based on the following prompt translate the natural language to sql.\nConstraints:\n- The sql should be formatted according to the standard for that sql language.\n\nRecommendations:\n- Use the best practices for writing sql.\n\nOutput format:\n- Should be only sql, otherwise the answer will be rejected.\n\n\nThe prompt: {prompt}\nThe schema: {schema}\n      `,\n            inputVariables: ['output', 'prompt', 'schema'],\n        });\n\n        const writeStream = fs.createWriteStream(params.output);\n        const input = await promptTemplate.format({\n            prompt: params.prompt,\n            output: params.output,\n            schema: params.content.trim(),\n        });\n\n        await this.llm.call(input, undefined, [\n            {\n                handleLLMStart: params.handleLLMStart,\n                handleLLMNewToken(token: string) {\n                    writeStream.write(token);\n                },\n                handleLLMEnd() {\n                    params.handleLLMEnd();\n                    writeStream.end();\n                },\n                handleLLMError(e): Promise<void> | void {\n                    params.handleLLMError(e);\n                    writeStream.end();\n                },\n            },\n        ]);\n    }\n\n    async generateTest({\n                           content,\n                           output,\n                           prompt,\n                           handleLLMStart,\n                           handleLLMEnd,\n                           handleLLMError,\n                       }: IRefactorParams): Promise<void> {\n        const promptTemplate = new PromptTemplate({\n            template: `\nGoal: Generate tests for the following code as much as possible.\nConstraints:\n- The code should be formatted according to the standard for that programming language.\n\nRecommendations:\n- Use the best testing framework for the programming language.\n\nOutput format:\n- Should be only tests code, otherwise the answer will be rejected.\n\n\n${prompt ? `Prompt for generating tests: \\`\\`\\`${prompt}\\`\\`\\`` : ''}\nThe content: {content}\n      `,\n            inputVariables: ['content', 'date', 'output'],\n        });\n        const input = await promptTemplate.format({\n            content,\n            date: new Date().toISOString(),\n            prompt,\n            output,\n        });\n\n        const writeStream = fs.createWriteStream(output);\n\n        await this.llm.call(input, undefined, [\n            {\n                handleLLMStart,\n                handleLLMNewToken(token: string) {\n                    writeStream.write(token);\n                },\n                handleLLMEnd() {\n                    handleLLMEnd();\n                    writeStream.end();\n                },\n                handleLLMError(e): Promise<void> | void {\n                    handleLLMError(e);\n                    writeStream.end();\n                },\n            },\n        ]);\n    }\n\n    async refactor({\n                       content,\n                       output,\n                       prompt,\n                       handleLLMStart,\n                       handleLLMEnd,\n                       handleLLMError,\n                   }: IRefactorParams): Promise<void> {\n        const promptTemplate = new PromptTemplate({\n            template: `Refactor and fix the following content\nConstraints:\n- If this is code in a programming language, it must be formatted according to the standard for that programming language and run without errors.\n\nRecommendations:\n- Use best practices for the content.\n\nAnswer format:\n- Return only refactored valid content, otherwise the answer will be rejected. \n\n${prompt ? `Prompt for refactoring: \\`\\`\\`${prompt}\\`\\`\\`` : ''}\nThe content: {content}\n      `,\n            inputVariables: ['content', 'date'],\n        });\n\n        const input = await promptTemplate.format({\n            content,\n            date: new Date().toISOString(),\n            prompt,\n        });\n\n        const writeStream = fs.createWriteStream(output);\n\n        await this.llm.call(input, undefined, [\n            {\n                handleLLMStart,\n                handleLLMNewToken(token: string) {\n                    writeStream.write(token);\n                },\n                handleLLMEnd() {\n                    handleLLMEnd();\n                    writeStream.end();\n                },\n                handleLLMError(e): Promise<void> | void {\n                    handleLLMError(e);\n                    writeStream.end();\n                },\n            },\n        ]);\n    }\n}\n\nexport class LLMChat extends LLMCommand {\n    static messages: ChatCompletionRequestMessage[] = [];\n    private llmChat: OpenAIChat;\n\n    constructor(config: IConfig) {\n        super(config, 1024, false);\n        this.llmChat = new OpenAIChat({\n            prefixMessages: LLMChat.messages,\n            modelName: config.MODEL,\n            maxTokens: 256,\n            temperature: 0,\n            openAIApiKey: config.OPENAI_API_KEY,\n            streaming: true,\n        });\n    }\n\n    static async chat(\n        params: IChatParams & {\n            config: IConfig;\n        },\n    ): Promise<void> {\n        return new LLMChat(params.config).chat(params);\n    }\n\n    async chat({\n                   input,\n                   prompt,\n                   handleLLMNewToken,\n                   handleLLMStart,\n                   handleLLMEnd,\n                   handleLLMError,\n               }: IChatParams): Promise<void> {\n        const messages = LLMChat.messages;\n        if (input === '') {\n            LLMChat.messages = [];\n            handleLLMStart();\n            handleLLMNewToken('Chat history cleared');\n            return handleLLMEnd();\n        }\n        if (messages.length === 0) {\n            messages.push({\n                role: 'system',\n                content: prompt || 'You are a helpful assistant that answers in language understandable to humans.',\n            });\n        }\n\n        const answer = await this.llmChat.call(input, undefined, [\n            {\n                handleLLMNewToken,\n                handleLLMStart,\n                handleLLMEnd,\n                handleLLMError,\n            },\n        ]);\n        messages.push({\n            role: 'user',\n            content: input,\n        });\n\n        messages.push({\n            role: 'assistant',\n            content: answer,\n        });\n    }\n}\n\nexport class LLMPreCommit extends LLMCommand {\n    constructor(config: IConfig) {\n        super(config, 256, false, 0.7);\n    }\n\n    static async preCommit(params: { config: IConfig; diff: string }): Promise<{\n        title: string;\n        messages?: string[];\n    }> {\n        return new LLMPreCommit(params.config).preCommit(params.diff);\n    }\n\n    async preCommit(diff: string): Promise<{\n        title: string;\n        messages?: string[];\n    }> {\n        const schemaWithMessages = z.object({\n            title: z.string().describe('The title of short description of the changes'),\n            messages: z.array(z.string()).describe('paragraphs describing the changes'),\n        });\n\n        const schemaWithOptionalMessages = z.object({\n            title: z.string().describe('The title of the commit'),\n        });\n\n        const schema = this.config.INCLUDE_COMMIT_DESCRIPTION === 'yes' ? schemaWithMessages : schemaWithOptionalMessages;\n        const validator = AiValidator.input`You are reviewing the git diff and writing a git commit.\nConstraints:\n- Use format Conventional Commits.\n\n${schema}\nThe git diff: \n\\`\\`\\`${diff}\\`\\`\\`\n      `;\n        const response = await this.llm.call(validator.prompt());\n        try {\n            return await validator.parse(response.replace(/\\\\n/g, '\\n'));\n        } catch (error) {\n            return throwLLMParseError();\n        }\n    }\n}\n\nexport class LLMCodeReview extends LLMCommand {\n    constructor(config: IConfig) {\n        super(config, 256, true, 0);\n    }\n\n    static async codeReview(params: {\n        config: IConfig;\n        content: string;\n        filePath: string;\n        handleLLMNewToken: (token: string) => Promise<void>;\n        handleLLMStart: () => Promise<void>;\n        handleLLMEnd: () => Promise<void>;\n        handleLLMError: (error: Error) => Promise<void>;\n    }): Promise<string> {\n        return new LLMCodeReview(params.config).codeReview({\n            content: params.content,\n            filePath: params.filePath,\n            handleLLMNewToken: params.handleLLMNewToken,\n            handleLLMStart: params.handleLLMStart,\n            handleLLMEnd: params.handleLLMEnd,\n            handleLLMError: params.handleLLMError,\n        });\n    }\n\n    async codeReview(params: {\n        content: string;\n        filePath: string;\n        handleLLMNewToken: (token: string) => Promise<void>;\n        handleLLMStart: () => Promise<void>;\n        handleLLMEnd: () => Promise<void>;\n        handleLLMError: (error: Error) => Promise<void>;\n    }): Promise<string> {\n        const fullFilePath = path.resolve(process.cwd(), params.filePath);\n        const promptTemplate = new PromptTemplate({\n            template: `You are an automatic assistant who helps with Code Review.\nThe goal is to improve the quality of the code and ensure the effective operation of the application in terms of security, scalability, and ease of maintenance.\nDuring the analysis, you should pay attention to the use of best programming practices, code optimization, security, and compliance with coding standards.\n\nConstraints:\n- Always specify where exactly in the code \"In ${fullFilePath}:line:column\" and what exactly \"Need to fix like this\".\n- Do not suggest fixes that do not improve the code or fix errors.\n- Be concise and accurate.\n\nAnswer only valid, otherwise the answer will be rejected.\n\\`\\`\\`\n🤖 ${fullFilePath}:{{line}}:{{column}} \n💡 {{suggestion}}\n\\`\\`\\`,\n\n\\`\\`\\`{code}\\`\\`\\``,\n            inputVariables: ['code'],\n        });\n\n        const codeWithLineNumbers = params.content\n            .split('\\n')\n            .map((line, index) => `/*${index + 1}*/ ${line}`)\n            .join('\\n')\n            .trim();\n        const input = await promptTemplate.format({\n            code: codeWithLineNumbers,\n        });\n\n        const response = await this.llm.call(input, undefined, [\n            {\n                handleLLMNewToken: params.handleLLMNewToken,\n                handleLLMStart: params.handleLLMStart,\n                handleLLMEnd: params.handleLLMEnd,\n                handleLLMError: params.handleLLMError,\n            },\n        ]);\n        return response;\n    }\n}\n\nexport class LLMCodeChat extends LLMCommand {\n    private vectorStore: any;\n\n    constructor(config: IConfig) {\n        super(config, 1024, true);\n    }\n\n    static async chat({\n                          config,\n                          directory,\n                          ...params\n                      }: IChatParams & { config: IConfig; directory: string }): Promise<void> {\n        const llmCodeChat = new LLMCodeChat(config);\n        await llmCodeChat.getOrCreateVectorStore(directory);\n        return llmCodeChat.chat(params);\n    }\n\n    async chat(params: IChatParams): Promise<void> {\n        const messages: ChatCompletionRequestMessage[] = [\n            {\n                role: 'system',\n                content:\n                    params.prompt ||\n                    `You are given from the vector store the most relevant code that you can use to solve the user request. \nTry to answer user questions briefly and clearly.`,\n            },\n        ];\n\n        while (true) {\n            const input = await inputAsk();\n            const relevantCode = await this.vectorStore.asRetriever(4).getRelevantDocuments(input);\n            if (relevantCode.length === 0) {\n                console.log(\"🤖 Sorry, I don't found any code for your question.\");\n                return this.chat(params);\n            }\n\n            const llmChat = new OpenAIChat({\n                prefixMessages: messages.concat({\n                    role: 'user',\n                    content: relevantCode\n                        .map((doc) => doc.pageContent)\n                        .join('\\n')\n                        .replace(/\\n/g, ' ')\n                        .trim(),\n                }),\n                modelName: this.config.MODEL,\n                temperature: 0,\n                openAIApiKey: this.config.OPENAI_API_KEY,\n                streaming: true,\n            });\n\n            await llmChat.call(input, undefined, [\n                {\n                    handleLLMNewToken: params.handleLLMNewToken,\n                    handleLLMStart: params.handleLLMStart,\n                    handleLLMEnd: params.handleLLMEnd,\n                    handleLLMError: params.handleLLMError,\n                },\n            ]);\n            relevantCode.forEach((doc) => {\n                console.log(`📄 ${doc.metadata.source}:`);\n            });\n        }\n    }\n\n    private async getOrCreateVectorStore(directory: string): Promise<any> {\n        const vectorStorePath = path.resolve(directory, 'vector-store');\n        if (fs.existsSync(vectorStorePath)) {\n            const store = await HNSWLib.load(\n                vectorStorePath,\n                new OpenAIEmbeddings({\n                    openAIApiKey: this.config.OPENAI_API_KEY,\n                }),\n            );\n            const input = await customAsk(`Found existing vector store. Do you want to use it? (y/n) `);\n            if (input) {\n                this.vectorStore = store;\n                return;\n            }\n        }\n        const loader = new DirectoryLoader(\n            directory,\n            extensionsList.reduce((acc, ext) => {\n                acc[ext] = (path) => new TextLoader(path);\n                return acc;\n            }, {}),\n        );\n\n        const rawDocs = await loader.load();\n        const textSplitter = new RecursiveCharacterTextSplitter({chunkSize: 500, chunkOverlap: 50});\n        const docs = await textSplitter.splitDocuments(rawDocs);\n        const cost = await calculateCost(\n            this.config.MODEL,\n            docs.map((doc) => doc.pageContent),\n        );\n\n        const input = await customAsk(\n            `🤖 Creating a vector store for ${rawDocs.length} documents will cost ~$${cost.toFixed(\n                5,\n            )}. Do you want to continue? (y/n) `,\n        );\n        if (!input) {\n            console.log('🤖 Bye!');\n            process.exit(0);\n        }\n        const spinner = ora('Loading vector store...').start();\n        this.vectorStore = await HNSWLib.fromDocuments(\n            docs,\n            new OpenAIEmbeddings({\n                openAIApiKey: this.config.OPENAI_API_KEY,\n            }),\n        );\n        await this.vectorStore.save(vectorStorePath);\n        spinner.succeed(`Created vector store with ${rawDocs.length} documents`);\n    }\n}\n"
  },
  {
    "path": "src/types.ts",
    "content": "export interface IConfig {\n    OPENAI_API_KEY: string;\n    TEMPERATURE: number;\n    MODEL: string;\n    EDITOR: string;\n    OPEN_AI_BASE_URL: string;\n    INCLUDE_COMMIT_DESCRIPTION: string;\n    PACKAGE_MANAGER: string;\n}\n\nexport interface ShellScriptResponse {\n    shellScript: string;\n    isDangerous: boolean;\n    description: string;\n}\n\nexport interface IRefactorParams {\n    content: string;\n    output: string;\n    prompt?: string;\n    handleLLMStart: () => void;\n    handleLLMEnd: () => void;\n    handleLLMError: (e: any) => void;\n}\n\nexport interface IChatParams {\n    input: string;\n    prompt?: string;\n    handleLLMNewToken: (token: string) => void;\n    handleLLMStart: () => void;\n    handleLLMEnd: () => void;\n    handleLLMError: (e: Error) => void;\n}\n\nexport abstract class Command {\n    protected readonly config: IConfig;\n\n    constructor(config: IConfig) {\n        this.config = config;\n    }\n\n    abstract execute(args: string, options: Record<any, any>): Promise<void>;\n}\n\nexport interface IAnalyseParams {\n    errorOutput: string;\n    command: string;\n    handleLLMNewToken: (token: string) => void;\n    handleLLMStart?: () => void;\n    handleLLMEnd?: () => void;\n    handleLLMError?: (e: Error) => void;\n}\n"
  },
  {
    "path": "src/utils/error.ts",
    "content": "class BaseError extends Error {\n    constructor(message: string) {\n        super(message);\n    }\n}\n\nexport class LLMError extends BaseError {\n    constructor(message: string) {\n        super(message);\n    }\n}\n\nexport const throwLLMParseError = (): never => {\n    throw new LLMError('Failed to parse the response from the LLM');\n};\n"
  },
  {
    "path": "src/utils/git.ts",
    "content": "import simpleGit from 'simple-git';\nimport chalk from 'chalk';\nimport { excludePackagesFiles, extensions } from './language-extensions';\nconst git = simpleGit();\n\nexport async function checkGitExists() {\n  const gitExists = await git.checkIsRepo();\n  if (!gitExists) {\n    console.error(`${chalk.red('✘')} need to be in a git repository`);\n    process.exit(1);\n  }\n}\n\nexport async function getGitIgnoreFiles() {\n  const gitIgnoreFiles = await git.raw(['ls-files', '--others', '--exclude-standard', '-i', '--directory', '--cached']);\n  return gitIgnoreFiles.split('\\n').filter(Boolean);\n}\n\nexport async function gitDiffCommand() {\n  const exts = Array.from(extensions.values(), (ext) => `*${ext}`);\n  const excludeFiles = Array.from(excludePackagesFiles.values());\n  const gitIgnoreFiles = await getGitIgnoreFiles();\n\n  return git.diff([\n    '--cached',\n    '--diff-filter=ACMRT',\n    '--',\n    ...exts,\n    ...excludeFiles.map((file) => `:(exclude)${file}`),\n    ...gitIgnoreFiles.map((file) => `:(exclude)${file}`),\n  ]);\n}\n\nexport async function gitDiffFiles() {\n  const exts = Array.from(extensions.values(), (ext) => `*${ext}`);\n  const excludeFiles = Array.from(excludePackagesFiles.values());\n  const gitIgnoreFiles = await getGitIgnoreFiles();\n\n  const raw = await git.diff([\n    '--name-only',\n    '--cached',\n    '--diff-filter=ACMRT',\n    '--',\n    ...exts,\n    ...excludeFiles.map((file) => `:(exclude)${file}`),\n    ...gitIgnoreFiles.map((file) => `:(exclude)${file}`),\n  ]);\n\n  return raw.split('\\n').filter(Boolean);\n}\n\nexport async function gitFiles(): Promise<string[]> {\n  const exts = Array.from(extensions.values(), (ext) => `*${ext}`);\n  const excludeFiles = Array.from(excludePackagesFiles.values());\n  const gitIgnoreFiles = await getGitIgnoreFiles();\n\n  const raw = await git.raw([\n    'ls-files',\n    '--cached',\n    '--others',\n    '--exclude-standard',\n    '--',\n    ...exts,\n    ...excludeFiles.map((file) => `:(exclude)${file}`),\n    ...gitIgnoreFiles.map((file) => `:(exclude)${file}`),\n  ]);\n\n  return raw.split('\\n').filter(Boolean);\n}"
  },
  {
    "path": "src/utils/helpers.ts",
    "content": "import ora from 'ora';\nimport {exec, spawn} from 'child_process';\nimport chalk from 'chalk';\nimport {Tiktoken} from '@dqbd/tiktoken/lite';\nimport {load} from '@dqbd/tiktoken/load';\nimport registry from '@dqbd/tiktoken/registry.json';\nimport models from '@dqbd/tiktoken/model_to_encoding.json';\n\nexport const exFunction = async <T>(fn: () => Promise<T>, message: string, successMessage: string): Promise<T> => {\n    const spinner = ora(message).start();\n    try {\n        const result = await fn();\n        spinner.succeed(successMessage);\n        return result;\n    } catch (error) {\n        spinner.fail();\n        throw error;\n    }\n};\n\nexport function executeCommand(command: string) {\n    return new Promise((resolve) => {\n        const child = exec(command);\n        child.stdout?.on('data', (data) => {\n            process.stdout.write(data);\n        });\n\n        child.stderr?.on('data', (data) => {\n            process.stderr.write(data);\n        });\n\n        child.on('close', (code) => {\n            resolve(code);\n        });\n    });\n}\n\nexport function executeShell(command: string) {\n    return new Promise((resolve) => {\n        const child = spawn(command, [], {shell: true});\n        child.stdout.pipe(process.stdout);\n        child.stderr.pipe(process.stderr);\n        child.stdin.pipe(process.stdin);\n        process.stdin.pipe(child.stdin);\n\n        child.on('close', (code) => {\n            resolve(code);\n        });\n    });\n}\n\nexport function checkNodeVersion() {\n    const nodeVersion = process.versions.node.split('.')[0];\n    if (Number(nodeVersion) < 18) {\n        console.log(`${chalk.red('✘')} Please update your node version to 18 or above\\nCurrent version: ${nodeVersion}`);\n        process.exit(1);\n    }\n}\n\nexport function getPackageManagerByOs() {\n    const os = process.platform;\n    const packageManager: Record<string, string> = {\n        linux: 'apt-get',\n        darwin: 'brew',\n        win32: 'choco',\n    };\n    return packageManager[os] || 'apt-get';\n}\n\nexport async function calculateCost(modelName: string, docs: string[]) {\n    const spinner = ora('Calculating cost').start();\n    try {\n        const {bpe_ranks, special_tokens, pat_str} = await load(registry[models[modelName]]);\n        const encoder = new Tiktoken(bpe_ranks, special_tokens, pat_str);\n        const tokenCount = encoder.encode(JSON.stringify(docs)).length;\n        const cost = (tokenCount / 1000) * 0.0005;\n        encoder.free();\n        return cost;\n    } finally {\n        spinner.stop();\n    }\n}\n"
  },
  {
    "path": "src/utils/index.ts",
    "content": "export * from './inquirer';\n"
  },
  {
    "path": "src/utils/inquirer.ts",
    "content": "import inquirer from 'inquirer';\nimport chalk from 'chalk';\n\nexport const askExecute = async (): Promise<boolean> => {\n    const {execute} = await inquirer.prompt<{ execute: 'Yes' | 'No' }>([\n        {\n            type: 'list',\n            name: 'execute',\n            message: `🚀 Execute?`,\n            choices: ['Yes', 'No'],\n        },\n    ]);\n    return execute === 'Yes';\n};\n\nexport const askOpenEditor = async (): Promise<boolean> => {\n    const {openEditor} = await inquirer.prompt<{ openEditor: 'Yes' | 'No' }>([\n        {\n            type: 'list',\n            name: 'openEditor',\n            message: `💻 Open in editor?`,\n            choices: ['Yes', 'No'],\n        },\n    ]);\n    return openEditor === 'Yes';\n};\n\nexport const askGoal = async (): Promise<string> => {\n    const {goal} = await inquirer.prompt<{ goal: string }>([\n        {\n            type: 'input',\n            name: 'goal',\n            message: '🎯 Input your goal:',\n        },\n    ]);\n    return goal;\n};\n\nexport const askOpenAIKey = async (): Promise<string> => {\n    const {openAIKey} = await inquirer.prompt<{ openAIKey: string }>([\n        {\n            type: 'input',\n            name: 'openAIKey',\n            message: '🔑 Enter your OpenAI API key. You can get your API key from https://beta.openai.com/account/api-keys:',\n        },\n    ]);\n    return openAIKey;\n};\n\nexport const askRetryRefactor = async (): Promise<boolean> => {\n    const {refactor} = await inquirer.prompt<{ refactor: 'Yes' | 'No' }>([\n        {\n            type: 'list',\n            name: 'refactor',\n            message: `🔁 Retry refactor?`,\n            choices: ['Yes', 'No'],\n        },\n    ]);\n    return refactor === 'Yes';\n};\n\nexport const inputRefactor = async (): Promise<string> => {\n    const {refactor} = await inquirer.prompt<{ refactor: string }>([\n        {\n            type: 'input',\n            name: 'refactor',\n            message: '🎯 Input your refactor plan:',\n        },\n    ]);\n    return refactor;\n};\n\nexport const askTest = async (): Promise<boolean> => {\n    const {test} = await inquirer.prompt<{ test: 'Yes' | 'No' }>([\n        {\n            type: 'list',\n            name: 'test',\n            message: `🔁 Retry generate tests?`,\n            choices: ['Yes', 'No'],\n        },\n    ]);\n    return test === 'Yes';\n};\n\nexport const inputTest = async (): Promise<string> => {\n    const {input} = await inquirer.prompt<{ input: string }>([\n        {\n            type: 'input',\n            name: 'input',\n            message: '🎯 Input your test plan:',\n        },\n    ]);\n    return input;\n};\n\nexport const customAsk = async (message: string): Promise<boolean> => {\n    const {ask} = await inquirer.prompt<{ ask: 'Yes' | 'No' }>([\n        {\n            type: 'list',\n            name: 'ask',\n            message,\n            choices: ['Yes', 'No'],\n        },\n    ]);\n    return ask === 'Yes';\n};\nexport const inputAsk = async (): Promise<string> => {\n    const {ask} = await inquirer.prompt<{ ask: string }>([\n        {\n            type: 'input',\n            name: 'ask',\n            message: '👉',\n        },\n    ]);\n    return ask;\n};\n\nexport const askCommit = async (commit: string): Promise<boolean> => {\n    const {ask} = await inquirer.prompt<{ ask: 'Yes' | 'No' }>([\n        {\n            type: 'list',\n            name: 'ask',\n            message: `Do you want to commit with the following message?\n${chalk.green(commit)}\n      `,\n            choices: ['Yes', 'No'],\n        },\n    ]);\n    return ask === 'Yes';\n};\n\nexport const askRetryCommit = async (): Promise<boolean> => {\n    const {ask} = await inquirer.prompt<{ ask: 'Yes' | 'No' }>([\n        {\n            type: 'list',\n            name: 'ask',\n            message: `Do you want to retry generating commit message?`,\n            choices: ['Yes', 'No'],\n        },\n    ]);\n    return ask === 'Yes';\n};\n"
  },
  {
    "path": "src/utils/language-extensions.ts",
    "content": "import path from 'path';\n\nexport const programmingLanguageExtensions = {\n    Text: ['.txt'],\n    JavaScript: ['.js', '.mjs'],\n    TypeScript: ['.ts', '.tsx'],\n    CSS: ['.css', '.scss', '.less'],\n    HTML: ['.html', '.htm'],\n    JSON: ['.json'],\n    Python: ['.py'],\n    Java: ['.java'],\n    C: ['.c'],\n    'C++': ['.cpp'],\n    'C#': ['.cs'],\n    Go: ['.go'],\n    PHP: ['.php'],\n    Ruby: ['.rb'],\n    Rust: ['.rs'],\n    Swift: ['.swift'],\n    Kotlin: ['.kt'],\n    Scala: ['.scala'],\n    'Objective-C': ['.m', '.h'],\n    Shell: ['.sh'],\n    Perl: ['.pl', '.pm'],\n    Lua: ['.lua'],\n    SQL: ['.sql'],\n};\n\nexport const excludePackagesFilesList = {\n    JavaScript: ['package-lock.json', 'yarn.lock'],\n    Python: ['requirements.txt'],\n    Java: ['pom.xml'],\n    Go: ['go.mod'],\n    PHP: ['composer.json'],\n    Ruby: ['Gemfile'],\n    Rust: ['Cargo.toml'],\n    Swift: ['Package.swift'],\n    Kotlin: ['build.gradle'],\n    Scala: ['build.sbt'],\n    'Objective-C': ['Podfile'],\n    Shell: ['package.json'],\n    Perl: ['cpanfile'],\n    Lua: ['rockspec'],\n};\n\nexport const extensionsList = Object.values(programmingLanguageExtensions).flat();\nexport const extensions = new Set<string>(extensionsList);\nexport const excludePackagesFiles = new Set<string>(Object.values(excludePackagesFilesList).flat());\n\nexport const filterFilesByExtensions = (files: string[]): string[] => {\n    return files.filter((file) => extensions.has(path.extname(file)));\n};\n"
  },
  {
    "path": "src/utils/update.ts",
    "content": "import ora from 'ora';\nimport {exec} from 'child_process';\nimport semver from 'semver';\nimport chalk from 'chalk';\n// @ts-ignore\nimport {name, version} from '../../package.json';\nimport {promisify} from 'util';\n\nexport async function checkUpdate() {\n    const spinner = ora('Checking for updates').start();\n    const execPromise = promisify(exec);\n    const {stdout} = await execPromise('npm view auto-copilot-cli version');\n    const latestVersion = semver.clean(stdout);\n    if (!latestVersion) {\n        spinner.fail(chalk.yellow('Could not check for updates'));\n        return;\n    }\n    if (semver.gt(latestVersion, version)) {\n        spinner.fail(\n            chalk.yellow(`Please update ${name} to the latest version: ${chalk.blue('npm i -g auto-copilot-cli')}`),\n        );\n    } else {\n        spinner.stop();\n    }\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es2021\",\n    \"module\": \"commonjs\",\n    \"outDir\": \"./dist\",\n    \"rootDir\": \"./src\",\n    \"baseUrl\": \"./src\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"resolveJsonModule\": true,\n    \"noImplicitAny\": false,\n    \"skipLibCheck\": true\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\"node_modules\", \"**/*.spec.ts\"]\n}\n"
  }
]