[
  {
    "path": ".github/dependabot.yml",
    "content": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where the package manifests are located.\n# Please see the documentation for all configuration options:\n# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates\n\nversion: 2\nupdates:\n  - package-ecosystem: \"npm\" # See documentation for possible values\n    directory: \"/\" # Location of package manifests\n    schedule:\n      interval: \"weekly\"\n"
  },
  {
    "path": ".github/workflows/codeql.yml",
    "content": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# You may wish to alter this file to override the set of languages analyzed,\n# or to provide custom queries or build logic.\n#\n# ******** NOTE ********\n# We have attempted to detect the languages in your repository. Please check\n# the `language` matrix defined below to confirm you have the correct set of\n# supported CodeQL languages.\n#\nname: \"CodeQL\"\n\non:\n  push:\n    branches: [ \"main\" ]\n  pull_request:\n    # The branches below must be a subset of the branches above\n    branches: [ \"main\" ]\n  schedule:\n    - cron: '37 4 * * 0'\n\njobs:\n  analyze:\n    name: Analyze\n    runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}\n    timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}\n    permissions:\n      actions: read\n      contents: read\n      security-events: write\n\n    strategy:\n      fail-fast: false\n      matrix:\n        language: [ 'javascript' ]\n        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift' ]\n        # Use only 'java' to analyze code written in Java, Kotlin or both\n        # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both\n        # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support\n\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@v3\n\n    # Initializes the CodeQL tools for scanning.\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@v2\n      with:\n        languages: ${{ matrix.language }}\n        # If you wish to specify custom queries, you can do so here or in a config file.\n        # By default, queries listed here will override any specified in a config file.\n        # Prefix the list here with \"+\" to use these queries and those in the config file.\n\n        # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs\n        # queries: security-extended,security-and-quality\n\n\n    # Autobuild attempts to build any compiled languages  (C/C++, C#, Go, or Java).\n    # If this step fails, then you should remove it and run the build manually (see below)\n    - name: Autobuild\n      uses: github/codeql-action/autobuild@v2\n\n    # ℹ️ Command-line programs to run using the OS shell.\n    # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun\n\n    #   If the Autobuild fails above, remove it and uncomment the following three lines.\n    #   modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.\n\n    # - run: |\n    #     echo \"Run, Build Application using script\"\n    #     ./location_of_script_within_repo/buildscript.sh\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@v2\n      with:\n        category: \"/language:${{matrix.language}}\"\n"
  },
  {
    "path": ".gitignore",
    "content": "dist/\nnode_modules/\n*.log\ncoverage/\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"name\": \"Debug Server\",\n      \"type\": \"node\",\n      \"request\": \"launch\",\n      \"skipFiles\": [ \"<node_internals>/**\" ],\n      \"env\": {\n        \"BASE_PATH\": \"${workspaceFolder}\"\n      },\n      \"program\": \"${workspaceFolder}/dist/index.js\",\n      \"cwd\": \"${workspaceFolder}/dist\",\n      \"preLaunchTask\": \"yarn: build\",\n      \"outFiles\": [ \"${workspaceFolder}/dist/**/*.js\" ]\n    },\n    {\n      \"name\": \"Debug Unit Tests\",\n      \"type\": \"node\",\n      \"request\": \"launch\",\n      \"skipFiles\": [ \"<node_internals>/**\" ],\n      \"cwd\": \"${workspaceFolder}\",\n      \"program\": \"${workspaceFolder}/node_modules/.bin/jest\"\n    }\n  ]\n}\n"
  },
  {
    "path": ".vscode/tasks.json",
    "content": "{\n  \"version\": \"2.0.0\",\n  \"tasks\": [\n    {\n      \"type\": \"npm\",\n      \"script\": \"build\",\n      \"group\": \"build\",\n      \"problemMatcher\": \"$tsc\",\n      \"label\": \"yarn: build\"\n    }\n  ]\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 Evgeny Zislis\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": "# Code ChatGPT Plugin\n\nCode ChatGPT Plugin is a TypeScript Code Analyzer that provides a set of utilities for analyzing TypeScript code. It can fetch a list of all TypeScript files in a project, find all functions in a file, and even get the content of a specific function. It's a great tool for developers who want to understand a TypeScript codebase, and it's also useful for automated tools that need to analyze or manipulate TypeScript code.\n\n## Features\n\n- Fetch a list of all TypeScript files in a project\n- Find all functions in a TypeScript file\n- Get the content of a specific function in a TypeScript file\n\n## Example Usage\n\n![Example Usage in ChatGPT](example.png)\n\n## Installation\n\n1. Clone the repository: `git clone https://github.com/kesor/chatgpt-code-plugin.git`\n2. Navigate to the project directory: `cd chatgpt-code-plugin`\n3. Install the dependencies: `npm install`\n4. Build project: `npm run build`\n5. Start the server: `BASE_PATH=/home/myuser/src/awesome-project  npm start`\n6. Add the API into ChatGPT Plus plugins' \"Developer your own plugin\" interface (`http://localhost:3000`)\n\n### Prerequisites\n\n1. You must have ChatGPT Plugins available to you\n\n    ![ChatGPT Plugins Beta](prereq-plugins.png)\n\n2. You must have ChatGPT Plugin Developer available to you as well\n\n    ![ChatGPT Plugins Developer](prereq-plugin-dev.png)\n\n## Usage\n\nOnce the server is running, you, or ChatGPT, can use the following endpoints:\n\n- `GET /files`: Fetch a list of all TypeScript files in the project\n- `GET /files/:fileName`: Get the content of a specific file\n- `GET /functions`: Fetch a list of all functions in the project\n- `GET /files/:fileName/functions`: Find all functions in a specific file\n- `GET /files/:fileName/functions/:functionName`: Get the content of a specific function in a file\n\n## Contributing\n\nWe welcome contributions from the community!\n\n### How to Contribute\n\n1. Fork the repository\n2. Create a new branch for each feature or bugfix\n3. Write your code\n4. Write tests for your code\n5. Run the tests and make sure they pass\n6. Submit a pull request\n\n## License\n\nThis project is licensed under the MIT License. See the [LICENSE](LICENSE) file for more information.\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"chatgpt-code-plugin\",\n  \"version\": \"0.0.1\",\n  \"scripts\": {\n    \"start\": \"cd $INIT_CWD/dist && node index.js\",\n    \"start:logs\": \"cd $INIT_CWD/dist && { node index.js & } && tail -F combined.log error.log\",\n    \"lint\": \"eslint $INIT_CWD/src\",\n    \"test\": \"cd $INIT_CWD ; jest --coverage\",\n    \"build\": \"npm run clean && npm run compile && cp -r src/*.yaml src/public dist/\",\n    \"compile\": \"tsc -b $INIT_CWD -v --listEmittedFiles\",\n    \"clean\": \"rm -rf $INIT_CWD/dist\"\n  },\n  \"license\": \"SEE LICENSE IN 'LICENSE'\",\n  \"devDependencies\": {\n    \"@types/compression\": \"^1.7.2\",\n    \"@types/connect-timeout\": \"^0.0.37\",\n    \"@types/cors\": \"^2\",\n    \"@types/express\": \"^4.17.17\",\n    \"@types/jest\": \"^29.5.1\",\n    \"@types/morgan\": \"^1\",\n    \"@types/supertest\": \"^2.0.12\",\n    \"jest\": \"^29.5.0\",\n    \"supertest\": \"^6.3.3\",\n    \"ts-jest\": \"^29.1.0\",\n    \"typescript\": \">=3.3.1 <5.2.0\"\n  },\n  \"jest\": {\n    \"preset\": \"ts-jest\",\n    \"testEnvironment\": \"node\",\n    \"testMatch\": [\n      \"<rootDir>/test/**/*.spec.ts\"\n    ]\n  },\n  \"dependencies\": {\n    \"@typescript-eslint/typescript-estree\": \"^5.60.0\",\n    \"compression\": \"^1.8.1\",\n    \"connect-timeout\": \"^1.9.1\",\n    \"cors\": \"^2.8.5\",\n    \"express\": \"^4.22.1\",\n    \"express-validator\": \"^7.0.1\",\n    \"fuzzy\": \"^0.1.3\",\n    \"ignore\": \"^5.2.4\",\n    \"morgan\": \"^1.10.1\",\n    \"winston\": \"^3.9.0\"\n  }\n}\n"
  },
  {
    "path": "src/cmd-runner.ts",
    "content": "import { spawn } from 'child_process';\n\nconst ALLOWED_COMMANDS = [\n  'yarn test',\n  'yarn run coverage',\n  'yarn install',\n  'yarn coverage',\n  'npm test',\n  'npm run test',\n  'npm run coverage',\n  'npm install',\n]\n\nexport type CommandResult = {\n  exitCode: number|null\n  stdout: string\n  stderr: string\n}\n\n/**\n * Executes a command and returns stdout, stderr and exit code.\n *\n * @param {string} command - The command to execute\n */\nexport function runCommand (command: string, base_path: string, strict = true) {\n  if (!command)\n    throw new Error('Command is required')\n\n  if (strict && !ALLOWED_COMMANDS.includes(command.trim()))\n    throw new Error(`Allowed commands are strictly ${ALLOWED_COMMANDS.join(',')}. This command is not allowed.`)\n\n  return new Promise<CommandResult>(\n    (resolve, reject) => {\n      const childProcess = spawn(command, {\n        cwd: base_path,\n        shell: true,\n      })\n\n      let stdout = ''\n      let stderr = ''\n\n      childProcess.stdout.on('data', data => { stdout += data; })\n      childProcess.stderr.on('data', data => { stderr += data; })\n\n      childProcess.on('close', exitCode => resolve({\n        exitCode,\n        stdout,\n        stderr\n      }))\n\n      childProcess.on('error', error => reject(error))\n    })\n}\n"
  },
  {
    "path": "src/error-handler.ts",
    "content": "import express from 'express'\nimport { check, validationResult } from 'express-validator'\nimport { logger } from './logger'\n\nexport const validateFileName =\n  check('0').isString().withMessage('File name should be a string')\n\nexport const validateFunctionName =\n  check('functionName').isString().withMessage('Function name should be a string')\n\nexport const validateDependencyOperation =\n  check('operation').isString().isIn(['add','remove','update']).withMessage('Invalid operation. Must be one of \"add\", \"remove\", \"update\".')\n\nexport const validatePackageName =\n  check('package').isString().withMessage('Package name is required.')\n\n// Parameter validation function\nexport const validateParams: express.RequestHandler = (req, res, next) => {\n  const errors = validationResult(req)\n  if (!errors.isEmpty())\n    return next({\n      error: new Error('Validation failed'),\n      validationErrors: errors.array()\n    })\n\n  return\n}\n\nexport const handleErrors: express.ErrorRequestHandler = (err, req, res, next) => {\n  logger.error(err)\n  if (err.code === 'EEXIST')\n    return res.status(400).json({ error: 'File already exists' })\n  if (err.code === 'ENOENT')\n    return res.status(404).send({ error: 'File not found' })\n  if (err.code === 'EACCES')\n    return res.status(403).send({ error: 'Permission denied' })\n  if (err.code === 'ETIMEDOUT')\n    return res.status(500).send({ error: `Response timed out after ${err.timeout}ms`})\n  if ('error' in err && 'validationErrors' in err)\n    return res.status(400).json({ error: err.error.message, details: err.validationErrors })\n  res.status(500).json({ error: 'Internal server error' })\n}\n"
  },
  {
    "path": "src/file-utils.ts",
    "content": "import fs from 'fs';\nimport ignore from 'ignore';\nimport path from 'path';\n\nexport async function isDirectory(filePath: string): Promise<boolean> {\n  try {\n    const stat = await fs.promises.stat(filePath)\n    return stat.isDirectory()\n  } catch (err) {\n    return false\n  }\n}\n\nexport async function getFileList(directory = __dirname, originalRoot = directory, ig = ignore()) {\n  const fileList: string[] = [];\n  const files = await fs.promises.readdir(directory);\n\n  if (directory === originalRoot) {\n    // always ignore .git folder and node_modules/ folders\n    ig.add(['.git/**', 'node_modules/**']);\n\n    // Check if there's a .gitignore file in the current directory\n    // If .gitignore exists, add its rules to the ignore filter\n    const gitignorePath = path.join(directory, '.gitignore');\n    if (fs.existsSync(gitignorePath)) {\n      const gitignoreContent = await fs.promises.readFile(gitignorePath, 'utf8');\n      ig.add(\n        gitignoreContent\n          .split(/\\n|\\r/)\n          .filter(line => !line.startsWith('#'))\n          .map(line => line.startsWith('/') ? line.slice(1) : line)\n      );\n    }\n  }\n\n  for (const file of files) {\n    const fullPath = path.join(directory, file);\n    const fullRelativePath = path.relative(originalRoot, fullPath);\n\n    // Skip if the file or directory is ignored\n    if (ig.ignores(fullRelativePath) || ig.ignores(fullRelativePath.endsWith('/') ? fullRelativePath : fullRelativePath + '/'))\n      continue;\n\n    const stat = await fs.promises.stat(fullPath);\n\n    // If the file is a directory, recurse into it\n    if (stat.isDirectory()) {\n      fileList.push(...await getFileList(fullPath, originalRoot, ig));\n    } else if (stat.isFile()) {\n      if (fullPath.endsWith('.gitignore')) {\n        const gitignoreContent = fs.readFileSync(fullPath, 'utf8')\n        ig.add(\n          gitignoreContent.split(/\\n|\\r/)\n          .filter(line => !line.startsWith('#'))\n          .map(line => path.dirname(fullPath) + '/' + (line.startsWith('/') ? line.slice(1) : line))\n        )\n      }\n      fileList.push(fullPath);\n    }\n  }\n\n  return fileList;\n}\n"
  },
  {
    "path": "src/function-utils.ts",
    "content": "import { AST, AST_NODE_TYPES, parse } from '@typescript-eslint/typescript-estree';\nimport fs from 'fs';\nimport path from 'path';\nimport { getFileList } from './file-utils';\n\ninterface FileRef {\n  fileName: string\n  functions?: FunctionRef[]\n}\n\ninterface FunctionRef {\n  functionName: string,\n  startByte: number,\n  endByte: number\n}\n\n// Minimizes a given function body by only keeping the first and last lines\nexport function minimize(body: string): string {\n  const bodyLines = body.split(/(\\n|\\r)/);\n  const MIN_LINES = 2;\n  if (bodyLines.length <= MIN_LINES)\n    return body;\n  return bodyLines[0] + '\\n// ...\\n' + bodyLines[bodyLines.length - 1];\n}\n\nfunction findFunctionsInFile(ast: AST<{range:true,loc:true}>) {\n  // Initialize an empty array to hold the functions\n  const functions: {name: string, start: number, end: number}[] = [];\n\n  // Traverse the AST and find all functions\n  for (const functionNode of ast.body) {\n\n    if (AST_NODE_TYPES.FunctionDeclaration === functionNode.type)\n      functions.push({\n        name: functionNode.id?.name || 'anonymous',\n        start: functionNode.range[0],\n        end: functionNode.range[1],\n      });\n\n    if (AST_NODE_TYPES.VariableDeclaration === functionNode.type)\n      for (const declarator of functionNode.declarations)\n        if ((AST_NODE_TYPES.FunctionExpression === declarator.init?.type ||\n            AST_NODE_TYPES.ArrowFunctionExpression === declarator.init?.type) &&\n            AST_NODE_TYPES.Identifier === declarator.id.type)\n          functions.push({\n            name: declarator.id.name,\n            start: functionNode.range[0],\n            end: functionNode.range[1],\n          });\n\n    if (AST_NODE_TYPES.ClassDeclaration === functionNode.type)\n      for (const method of functionNode.body.body)\n        if (method.type === AST_NODE_TYPES.MethodDefinition && AST_NODE_TYPES.Identifier === method.key.type)\n          functions.push({\n            name: method.key.name,\n            start: method.range[0],\n            end: method.range[1],\n          });\n\n    if (AST_NODE_TYPES.ExportNamedDeclaration === functionNode.type)\n      if (functionNode.declaration && functionNode.declaration.type === 'FunctionDeclaration')\n        functions.push({\n          name: functionNode.declaration.id?.name || 'anonymous',\n          start: functionNode.range[0],\n          end: functionNode.range[1],\n        });\n  }\n  return functions;\n}\n\nexport async function getFunctionList(directory: string = __dirname, fileName?: string): Promise<FileRef[]> {\n  const functionList: FileRef[] = [];\n  const files = await getFileList(directory)\n\n  const actualFileName = fileName ? path.join(directory, fileName) : undefined\n  for (const file of files) {\n    const fh = await fs.promises.open(file, 'r')\n    const stat = await fh.stat()\n    if (\n       (actualFileName && actualFileName !== file) // when filename is specified, only use that file\n    || (!file.endsWith('.ts') && !file.endsWith('.js')) // must be a js/ts file\n    || (!stat.isFile()) // must be a file\n    )\n      continue\n    const content = await fh.readFile('utf8')\n    fh.close()\n    const ast = parse(content, { range: true, loc: true })\n    functionList.push({\n      fileName: file,\n      functions: findFunctionsInFile(ast)\n        .map(func => ({\n          functionName: func.name,\n          startByte: func.start,\n          endByte: func.end\n        }))\n      })\n  }\n\n  return functionList\n}\n\nexport type FunctionData = {\n  fileName: string,\n  functionName: string\n  content: {\n    minimal: string,\n    full: string\n  },\n  startByte: number,\n  endByte: number\n}\n\nasync function extractFunctionRange(ast: AST<{loc:true,range:true}>, functionName: string): Promise<{start:number,end:number}|undefined> {\n  const functions = findFunctionsInFile(ast);\n  const func = functions.find(func => func.name === functionName);\n  if (func) {\n    return { start: func.start, end: func.end };\n  }\n}\n\nexport async function getFunctionData(functionName:string, fileName: string): Promise<FunctionData | undefined> {\n  const fileContent = await fs.promises.readFile(fileName, 'utf-8')\n  const ast = parse(fileContent, { loc: true, range: true })\n  const range = await extractFunctionRange(ast, functionName)\n  if (range) {\n    const functionContent = fileContent.substring(range.start, range.end)\n    return {\n      fileName,\n      functionName,\n      content: {\n        minimal: minimize(functionContent),\n        full: functionContent\n      },\n      startByte: range.start,\n      endByte: range.end\n    }\n  }\n}\n"
  },
  {
    "path": "src/index.ts",
    "content": "import compression from 'compression'\nimport timeout from 'connect-timeout'\nimport cors from 'cors'\nimport express from 'express'\nimport fs from 'fs'\nimport type http from 'http'\nimport morgan from 'morgan'\nimport path from 'path'\nimport { runCommand } from './cmd-runner'\nimport { handleErrors, validateDependencyOperation, validateFileName, validateFunctionName, validatePackageName, validateParams } from './error-handler'\nimport { getFileList, isDirectory } from './file-utils'\nimport { getFunctionData, getFunctionList } from './function-utils'\nimport { logger } from './logger'\n\n// Define constants for server configuration\nconst PORT = +(process.env.PORT ?? 3000)\nconst HOST = process.env.HOST ?? '127.0.0.1'\nconst TIMEOUT = '15000ms' // https://expressjs.com/en/resources/middleware/timeout.html\nconst BASE_PATH = process.env.BASE_PATH ?? path.resolve(__dirname, '..')\nconst ALLOW_OVERWRITE = process.env.ALLOW_OVERWRITE ?? false\nconst PKG_MANAGER = process.env.PKG_MANAGER ?? 'yarn'\n\n/**\n * Handle requests to /.well-known/ai-plugin.json\n * Provides the description and URLs for this plugin.\n * doc: https://platform.openai.com/docs/plugins/getting-started/plugin-manifest\n *\n * @param {express.Request} req - The HTTP request object.\n * @param {express.Response} res - The HTTP response object.\n */\nconst aiPluginJson = async (req: express.Request, res: express.Response) => {\n  res.json({\n    \"schema_version\": \"v1\",\n    \"name_for_human\": \"Code Plugin\",\n    \"name_for_model\": \"code\",\n    \"description_for_human\": \"Plugin for reading and writing TypeScript code. You can fetch full and minimal versions of functions and files.\",\n    \"description_for_model\": \"Reading and writing files with code. Fetch full and minimal versions files and functions, created new files, run test commands.\",\n    \"auth\": { \"type\": \"none\" },\n    \"api\": {\n      \"type\": \"openapi\",\n      \"url\": `http://localhost:${PORT}/openapi.yaml`\n    },\n    \"logo_url\": `http://localhost:${PORT}/logo.png`,\n    \"contact_email\": \"support@example.com\",\n    \"legal_info_url\": \"http://www.example.com/legal\"\n  })\n  res.end()\n}\n\nconst openApiYaml = (req: express.Request, res: express.Response) => {\n  const openApiFilePath = path.join(__dirname, 'openapi.yaml')\n  fs.readFile(openApiFilePath, 'utf8', (err, data) => {\n    if (err)\n      return res.status(500).send('An error occured while reading openapi.yaml')\n    const updatedYaml = data.replace(/localhost:3000/g, `localhost:${PORT}`)\n    res.send(updatedYaml)\n  })\n}\n\n/**\n * Resolves the file path for a given file name.\n *\n * @param {string} fileName - The name of the file.\n * @returns {string} The resolved file path.\n */\nconst resolveFilePath = (fileName: string) => {\n  return path.join(BASE_PATH, decodeURIComponent(fileName))\n}\n\n/**\n * Reads the content of a file.\n *\n * @param {express.Request} req - The HTTP request object.\n * @param {boolean} content - Whether to read the file content.\n * @returns {Promise<Object>} An object containing the file name, file path, and file content.\n */\nconst readFileContent = async (req: express.Request, content = true) => {\n  const fileName = decodeURIComponent(req.params[0])\n  const filePath = resolveFilePath(fileName)\n  return {\n    fileName,\n    filePath,\n    fileContent: content ? await fs.promises.readFile(filePath, 'utf8') : undefined\n  }\n}\n\n/**\n * Handles GET requests to /files.\n * Fetches the list of files under BASE_PATH and sends it in the response.\n *\n * @param {express.Request} req - The HTTP request object.\n * @param {express.Response} res - The HTTP response object.\n * @param {express.NextFunction} next - The next middleware function.\n */\nconst getFiles: express.RequestHandler = async (req, res, next) => {\n  logger.info('getFiles')\n  try {\n    const files = await getFileList(BASE_PATH)\n    res.send(files.map(fileName => encodeURIComponent(path.relative(BASE_PATH, fileName))))\n  } catch (err) {\n    next(err)\n  }\n}\n\nconst postNewFile: express.RequestHandler = async (req, res, next) => {\n  validateParams(req, res, next)\n  const fileName = req.params[0]\n  const { content } = req.body\n  logger.info(`Creating a new file named ${fileName}`)\n  if (!content)\n    return res.status(400).json({ error: 'Missing file content.' })\n  const filePath = path.join(BASE_PATH, fileName)\n  try {\n    await fs.promises.mkdir(path.dirname(filePath), { recursive: true })\n    const fh = await fs.promises.open(filePath, ALLOW_OVERWRITE ? 'w' : 'wx')\n    await fh.writeFile(content)\n    await fh.close()\n    logger.info(`Successfully created new file ${fileName}`)\n    res.status(201).json({ message: 'File created successfully' })\n  } catch (err) {\n    next(err)\n  }\n}\n\n/**\n * Handles GET requests to /files/:fileName.\n * Responds with file content or an error\n *\n * @param {express.Request} req - The HTTP request object.\n * @param {express.Response} res - The HTTP response object.\n * @param {express.NextFunction} next - The next middleware function.\n */\nconst getFileOrFolderContent: express.RequestHandler = async (req, res, next) => {\n  validateParams(req, res, next)\n  const { fileName, filePath } = await readFileContent(req, false)\n\n  if (await isDirectory(filePath)) {\n    logger.info(`Listing files in directory ${filePath}`)\n    try {\n      const files = await getFileList(filePath)\n      res.send(files.map(fileName => encodeURIComponent(path.relative(BASE_PATH, fileName))))\n    } catch (err) {\n      next(err)\n    }\n    return\n  }\n\n  try {\n    logger.info(`Reading file content file ${fileName}`)\n    const { fileContent } = await readFileContent(req)\n    if (!fileContent)\n      return next({ error: 'No content found in file', fileName })\n    const startByte = +(req.query['startByte'] ?? 0)\n    const endByte = +(req.query['endByte'] ?? fileContent.length - 1)\n    res.json({\n      fileName,\n      content: fileContent.substring(startByte, endByte),\n      startByte,\n      endByte\n    })\n  } catch (err) {\n    next(err)\n  }\n}\n\n/**\n * Handles GET requests to /functions.\n * Responds with a list of functions from all project .ts files\n *\n * @param {express.Request} req - The HTTP request object.\n * @param {express.Response} res - The HTTP response object.\n * @param {express.NextFunction} next - The next middleware function.\n */\nconst getAllFunctions: express.RequestHandler = async (req, res, next) => {\n  logger.info('getAllFunctions')\n  try {\n    res.send(\n      (await getFunctionList(BASE_PATH))\n        .map(obj => ({ ...obj, fileName: path.relative(BASE_PATH, obj.fileName) }))\n    )\n  } catch (err) {\n    next(err)\n  }\n}\n\n/**\n * Handles GET requests to /files/:fileName/functions.\n * Responds with a list of functions from the specified .ts file\n *\n * @param {express.Request} req - The HTTP request object.\n * @param {express.Response} res - The HTTP response object.\n * @param {express.NextFunction} next - The next middleware function.\n */\nconst getFunctionsInFile: express.RequestHandler = async (req, res, next) => {\n  validateParams(req, res, next)\n  try {\n    logger.info(`Reading file content file ${req.params[0]}`)\n    const { fileName } = await readFileContent(req, false)\n    res.send(\n      (await getFunctionList(BASE_PATH, fileName))\n        .map(obj => ({ ...obj, fileName: path.relative(BASE_PATH, obj.fileName) }))\n    )\n  } catch (err) {\n    next(err)\n  }\n}\n\n/**\n * Handles GET requests to /files/:fileName/functions/:functionName.\n * Responds with the content of the named function in a specific file.\n *\n * @param {express.Request} req - The HTTP request object.\n * @param {express.Response} res - The HTTP response object.\n * @param {express.NextFunction} next - The next middleware function.\n */\nconst getFunctionContent: express.RequestHandler = async (req, res, next) => {\n  validateParams(req, res, next)\n  try {\n    const { functionName } = req.params\n    logger.info(`Reading file content file ${req.params[0]} to inspect function ${functionName}`)\n    const { filePath } = await readFileContent(req, false)\n    const functionCode = await getFunctionData(functionName, filePath)\n    if (!functionCode)\n      return res.status(404).json({ error: 'Function not found' })\n    res.json(functionCode)\n  } catch(err) {\n    next(err)\n  }\n}\n\n/**\n * Handles POST requests to /run-command.\n * Executes a command and streams the output.\n *\n * @param {express.Request} req - The HTTP request object.\n * @param {express.Response} res - The HTTP response object.\n * @param {express.NextFunction} next - The next middleware function.\n */\nconst runCmd: express.RequestHandler = async (req, res, next) => {\n  const { command } = req.body;\n\n  if (!command)\n    return res.status(400).json({ error: 'Command is required' });\n\n  try {\n    const { exitCode, stdout, stderr } = await runCommand(command, BASE_PATH)\n    res.json({ exitCode, stdout, stderr })\n  } catch (error) {\n    next(error);\n  }\n}\n\nconst getDependencies: express.RequestHandler = async (req, res, next) => {\n  // Ignore PKG_MANAGER here because:\n  //   github.com/yarnpkg/yarn/issues/3569\n  const command = `npm list --json --depth=0 --omit dev`\n  try {\n    const { exitCode, stdout, stderr } = await runCommand(command, BASE_PATH, false)\n    res.json({ exitCode, stdout, stderr })\n  } catch (error) {\n    next(error)\n  }\n}\n\nconst postDependencies: express.RequestHandler = async (req, res, next) => {\n  const { operation, packageName, version } = req.body;\n  let op, command = ''\n  switch (operation) {\n    case 'list':\n      command = `${PKG_MANAGER} list`\n      break\n    case 'add':\n      op = PKG_MANAGER === 'yarn' ? 'add' : 'install'\n      command = `${PKG_MANAGER} ${op} ${packageName}${version ? `@${version}` : ''}`\n      break\n    case 'remove':\n      op = PKG_MANAGER === 'yarn' ? 'remove' : 'uninstall'\n      command = `${PKG_MANAGER} ${op} ${packageName}`\n      break\n    case 'update':\n      op = PKG_MANAGER === 'yarn' ? 'upgrade' : 'update'\n      command = `${PKG_MANAGER} ${op} ${packageName}${version ? `@${version}` : ''}`\n      break\n  }\n  try {\n    const { exitCode, stdout, stderr } = await runCommand(command, BASE_PATH, false)\n    res.json({ exitCode, stdout, stderr })\n  } catch (error) {\n    next(error)\n  }\n}\n\n/**\n * Sets extra CORS headers.\n * Middleware that adds headers required by OpenAI plugins to each response.\n *\n * @param {express.Request} req - The HTTP request object.\n * @param {express.Response} res - The HTTP response object.\n * @param {express.NextFunction} next - The next middleware function.\n */\nconst extraCors: express.RequestHandler = async (req, res, next) => {\n  res.setHeader(\"Access-Control-Allow-Private-Network\", \"true\")\n  res.setHeader(\"Access-Control-Allow-Headers\", \"Content-Type, Authorization\")\n  next()\n}\n\nconst app = express()\n  .disable('x-powered-by')\n  .use( timeout(TIMEOUT) )\n  .use( compression() )\n  .use( express.json({ strict: true }) )\n  .use( extraCors )\n  .use( cors({\n    credentials: true,\n    origin: [ `http://localhost:${PORT}`, 'https://chat.openai.com' ],\n  }) )\n  .use( morgan('dev') )\n  .use( express.static('public') )\n  .all( '/.well-known/ai-plugin.json', aiPluginJson )\n  .get( '/openapi.yaml', openApiYaml )\n  .get( '/files', [ timeout(TIMEOUT) ], getFiles )\n  .post( '/files/*', [ timeout(TIMEOUT), validateFileName ], postNewFile )\n  .get( '/functions', [ timeout(TIMEOUT) ], getAllFunctions )\n  .get( '/files/*/functions/:functionName', [ timeout(TIMEOUT), validateFileName, validateFunctionName ], getFunctionContent )\n  .get( '/files/*/functions', [ timeout(TIMEOUT), validateFileName ], getFunctionsInFile )\n  .get( '/files/*', [ timeout(TIMEOUT), validateFileName ], getFileOrFolderContent )\n  .post( '/run-command', [ timeout(TIMEOUT) ], runCmd )\n  .get( '/dependencies', [ timeout(TIMEOUT) ], getDependencies )\n  .post( '/dependencies', [ timeout(TIMEOUT), validateDependencyOperation, validatePackageName ], postDependencies )\n  .use( handleErrors )\n\nlet server: http.Server\n\nif (require.main === module) {\n  server = app.listen( PORT, HOST, () => {\n    console.error(`HTTP Server listening on ${HOST}:${PORT}`)\n  })\n}\n\nprocess.on('SIGTERM', () => {\n  logger.info('SIGTERM signal received: closing HTTP server')\n  server.close(() => {\n    logger.info('HTTP server closed')\n  })\n})\n\nprocess.on('SIGINT', () => {\n  logger.info('SIGINT signal received: closing HTTP server')\n  server.close(() => {\n    logger.info('HTTP server closed')\n  })\n})\n\nprocess.on('uncaughtException', (err) => {\n  logger.error(`Uncaught exception: ${err}`)\n  server.close(() => {\n    logger.info('HTTP server closed')\n  })\n  process.exit(1)\n})\n\nprocess.on('unhandledRejection', (reason, promise) => {\n  logger.error(`Unhandled promise rejection: ${JSON.stringify({ promise, reason })}`)\n  server.close(() => {\n    logger.info('HTTP server closed')\n  })\n  process.exit(1)\n})\n\nexport { app }\n"
  },
  {
    "path": "src/logger.ts",
    "content": "import winston from 'winston';\n\nconst ALLOWED_CHARACTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 .,!?'\n\n/**\n * Sanitizes a log message.\n *\n * This function takes a string as input and returns a new string where each character\n * is either included as is (if it's in the whitelist of allowed characters) or replaced\n * with its hexadecimal Unicode value (if it's not in the whitelist).\n *\n * @param {string} message - The log message to sanitize.\n * @returns {string} The sanitized log message.\n */\nconst sanitizeMessage = (message: string): string =>\n  message.split('').map(char =>\n    ALLOWED_CHARACTERS.includes(char) ? char : '\\\\x' + char.charCodeAt(0).toString(16)\n  ).join('')\n\n\nconst sanitizeFormat = winston.format(\n  info => (info.message ? { ...info, message: sanitizeMessage(info.message) } : info)\n)\n\nexport const logger = winston.createLogger({\n  level: 'info',\n  format: winston.format.combine(\n    sanitizeFormat(),\n    winston.format.json(),\n  ),\n  transports: [\n    new winston.transports.File({ filename: 'error.log', level: 'error', handleExceptions: true, handleRejections: true }),\n    new winston.transports.File({ filename: 'combined.log' }),\n  ],\n  exitOnError: false\n});\n"
  },
  {
    "path": "src/openapi.yaml",
    "content": "openapi: 3.0.0\ninfo:\n  title: Code Parser API\n  version: 1.0.0\nservers:\n  - url: http://localhost:3000\n\npaths:\n\n  /files:\n    get:\n      operationId: getFiles\n      summary: Get the list of files in this project\n      responses:\n        '200':\n          description: Successful\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  type: string\n        '500':\n          description: Internal server error\n\n  /functions:\n    get:\n      operationId: getFunctions\n      summary: Get the list of all functions in all files in this project\n      responses:\n        '200':\n          description: Successful\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  type: object\n                  properties:\n                    fileName:\n                      type: string\n                    functions:\n                      type: array\n                      items:\n                        type: object\n                        properties:\n                          functionName:\n                            type: string\n                          startByte:\n                            type: number\n                          endByte:\n                            type: number\n        '500':\n          description: Internal server error\n\n  /files/{fileName}:\n    get:\n      operationId: getFileOrFolderContent\n      summary: Get content or range of bytes from a specific file in this project, when specified filename is a directory it will list the files in the directory\n      parameters:\n        - name: fileName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: startByte\n          in: query\n          required: false\n          schema:\n            type: number\n        - name: endByte\n          in: query\n          required: false\n          schema:\n            type: number\n      responses:\n        '200':\n          description: Successful\n          content:\n            application/json:\n              schema:\n                type: object\n                properties:\n                  fileName:\n                    type: string\n                  content:\n                    type: string\n        '400':\n          description: Bad request (missing or incorrect parameters)\n        '404':\n          description: Not found (file not found)\n        '500':\n          description: Internal server error\n\n    post:\n      operationId: postNewFile\n      summary: Create a new file in the project with specified content\n      parameters:\n        - in: path\n          name: fileName\n          required: true\n          schema:\n            type: string\n          description: The name of the file to create\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              type: object\n              properties:\n                content:\n                  type: string\n              required:\n                - content\n            example:\n              content: \"Hello, world!\"\n      responses:\n        '201':\n          description: File created successfully\n          content:\n            application/json:\n              schema:\n                type: object\n                properties:\n                  message:\n                    type: string\n        '404':\n          description: Bad request\n        '500':\n          description: Internal server error\n\n  /files/{fileName}/functions:\n    get:\n      operationId: getFunctionsInFile\n      parameters:\n      - name: fileName\n        in: path\n        required: true\n        schema:\n          type: string\n      summary: Get the list of functions in a specified file in this project\n      responses:\n        '200':\n          description: 'Successful'\n          content:\n            application/json:\n              schema:\n                type: object\n                properties:\n                  fileName:\n                    type: string\n                  functions:\n                    type: array\n                    items:\n                      type: object\n                      properties:\n                        functionName:\n                          type: string\n                        startByte:\n                          type: number\n                        endByte:\n                          type: number\n        '400':\n          description: Bad request (missing or incorrect parameters)\n        '404':\n          description: Not found (file not found)\n        '500':\n          description: Internal server error\n\n  /files/{fileName}/functions/{functionName}:\n    get:\n      operationId: getFunctionContent\n      summary: Get the content of a specific function in this project\n      parameters:\n        - name: fileName\n          in: path\n          required: true\n          schema:\n            type: string\n        - name: functionName\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        '200':\n          description: 'Successful'\n          content:\n            application/json:\n              schema:\n                type: object\n                properties:\n                  fileName:\n                    type: string\n                  functionName:\n                    type: string\n                  content:\n                    type: object\n                    properties:\n                      minimal:\n                        description: minimal content for this function\n                        type: string\n                      full:\n                        description: full content of function\n                        type: string\n                  startByte:\n                    description: first byte of function location in the file\n                    type: number\n                  endByte:\n                    description: last byte of function location in the file\n                    type: number\n\n  /run-command:\n    post:\n      operationId: runCommand\n      summary: Run a command and stream the output\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              type: object\n              properties:\n                command:\n                  type: string\n              required:\n                - command\n            example:\n              command: \"npm test\"\n      responses:\n        '200':\n          description: Successful\n          content:\n            application/json:\n              schema:\n                type: object\n                properties:\n                  exitCode:\n                    type: number\n                  stdout:\n                    type: string\n                  stderr:\n                    type: string\n        '400':\n          description: Bad request (missing or incorrect parameters)\n        '500':\n          description: Internal server error\n\n  /dependencies:\n    get:\n      operationId: getDependencies\n      summary: List project dependencies\n      responses:\n        '200':\n          description: Successful execution of yarn or npm command.\n          content:\n            application/json:\n              schema:\n                type: object\n                properties:\n                  exitCode:\n                    type: number\n                  stdout:\n                    type: string\n                  stderr:\n                    type: string\n        '400':\n          description: Bad request (missing or incorrect parameters)\n        '500':\n          description: Internal server error\n    post:\n      operationId: manageDependencies\n      summary: Add, remove or update project dependencies\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              type: object\n              properties:\n                operation:\n                  type: string\n                packageName:\n                  type: string\n                version:\n                  type: string\n              required:\n                - operation\n                - packageName\n            example:\n              operation: \"add\"\n              packageName: \"@types/node\"\n              version: \"^20.3\"\n      responses:\n        '200':\n          description: Successful execution of yarn or npm command.\n          content:\n            application/json:\n              schema:\n                type: object\n                properties:\n                  exitCode:\n                    type: number\n                  stdout:\n                    type: string\n                  stderr:\n                    type: string\n        '400':\n          description: Bad request (missing or incorrect parameters)\n        '500':\n          description: Internal server error\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"esnext\",\n    \"lib\": [\n      \"DOM\",\n      \"DOM.Iterable\",\n      \"ESNext\"\n\n    ],\n    \"skipLibCheck\": true,\n    \"module\": \"CommonJS\",\n    \"moduleResolution\": \"node\",\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"noUnusedLocals\": false,\n    \"strict\": true,\n    \"outDir\": \"dist\",\n    \"sourceMap\": true,\n    \"allowJs\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true\n  },\n  \"include\": [\n    \"src\"\n  ],\n  \"exclude\": [\n    \"dist\"\n  ]\n}\n"
  }
]