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