Full Code of kesor/chatgpt-code-plugin for AI

main d51c6f81dca0 cached
16 files
40.9 KB
10.2k tokens
20 symbols
1 requests
Download .txt
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": [ "<node_internals>/**" ],
      "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": [ "<node_internals>/**" ],
      "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": [
      "<rootDir>/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<CommandResult>(
    (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<boolean> {
  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<FileRef[]> {
  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<FunctionData | undefined> {
  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<Object>} 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"
  ]
}
Download .txt
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
Download .txt
SYMBOL INDEX (20 symbols across 5 files)

FILE: src/cmd-runner.ts
  constant ALLOWED_COMMANDS (line 3) | const ALLOWED_COMMANDS = [
  type CommandResult (line 14) | type CommandResult = {
  function runCommand (line 25) | function runCommand (command: string, base_path: string, strict = true) {

FILE: src/file-utils.ts
  function isDirectory (line 5) | async function isDirectory(filePath: string): Promise<boolean> {
  function getFileList (line 14) | async function getFileList(directory = __dirname, originalRoot = directo...

FILE: src/function-utils.ts
  type FileRef (line 6) | interface FileRef {
  type FunctionRef (line 11) | interface FunctionRef {
  function minimize (line 18) | function minimize(body: string): string {
  function findFunctionsInFile (line 26) | function findFunctionsInFile(ast: AST<{range:true,loc:true}>) {
  function getFunctionList (line 71) | async function getFunctionList(directory: string = __dirname, fileName?:...
  type FunctionData (line 102) | type FunctionData = {
  function extractFunctionRange (line 113) | async function extractFunctionRange(ast: AST<{loc:true,range:true}>, fun...
  function getFunctionData (line 121) | async function getFunctionData(functionName:string, fileName: string): P...

FILE: src/index.ts
  constant PORT (line 16) | const PORT = +(process.env.PORT ?? 3000)
  constant HOST (line 17) | const HOST = process.env.HOST ?? '127.0.0.1'
  constant TIMEOUT (line 18) | const TIMEOUT = '15000ms' // https://expressjs.com/en/resources/middlewa...
  constant BASE_PATH (line 19) | const BASE_PATH = process.env.BASE_PATH ?? path.resolve(__dirname, '..')
  constant ALLOW_OVERWRITE (line 20) | const ALLOW_OVERWRITE = process.env.ALLOW_OVERWRITE ?? false
  constant PKG_MANAGER (line 21) | const PKG_MANAGER = process.env.PKG_MANAGER ?? 'yarn'

FILE: src/logger.ts
  constant ALLOWED_CHARACTERS (line 3) | const ALLOWED_CHARACTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr...
Condensed preview — 16 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (44K chars).
[
  {
    "path": ".github/dependabot.yml",
    "chars": 502,
    "preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
  },
  {
    "path": ".github/workflows/codeql.yml",
    "chars": 3090,
    "preview": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# Y"
  },
  {
    "path": ".gitignore",
    "chars": 36,
    "preview": "dist/\nnode_modules/\n*.log\ncoverage/\n"
  },
  {
    "path": ".vscode/launch.json",
    "chars": 680,
    "preview": "{\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"name\": \"Debug Server\",\n      \"type\": \"node\",\n      \"request\":"
  },
  {
    "path": ".vscode/tasks.json",
    "chars": 186,
    "preview": "{\n  \"version\": \"2.0.0\",\n  \"tasks\": [\n    {\n      \"type\": \"npm\",\n      \"script\": \"build\",\n      \"group\": \"build\",\n      \""
  },
  {
    "path": "LICENSE",
    "chars": 1070,
    "preview": "MIT License\n\nCopyright (c) 2023 Evgeny Zislis\n\nPermission is hereby granted, free of charge, to any person obtaining a c"
  },
  {
    "path": "README.md",
    "chars": 2193,
    "preview": "# Code ChatGPT Plugin\n\nCode ChatGPT Plugin is a TypeScript Code Analyzer that provides a set of utilities for analyzing "
  },
  {
    "path": "package.json",
    "chars": 1350,
    "preview": "{\n  \"name\": \"chatgpt-code-plugin\",\n  \"version\": \"0.0.1\",\n  \"scripts\": {\n    \"start\": \"cd $INIT_CWD/dist && node index.js"
  },
  {
    "path": "src/cmd-runner.ts",
    "chars": 1267,
    "preview": "import { spawn } from 'child_process';\n\nconst ALLOWED_COMMANDS = [\n  'yarn test',\n  'yarn run coverage',\n  'yarn install"
  },
  {
    "path": "src/error-handler.ts",
    "chars": 1638,
    "preview": "import express from 'express'\nimport { check, validationResult } from 'express-validator'\nimport { logger } from './logg"
  },
  {
    "path": "src/file-utils.ts",
    "chars": 2101,
    "preview": "import fs from 'fs';\nimport ignore from 'ignore';\nimport path from 'path';\n\nexport async function isDirectory(filePath: "
  },
  {
    "path": "src/function-utils.ts",
    "chars": 4522,
    "preview": "import { AST, AST_NODE_TYPES, parse } from '@typescript-eslint/typescript-estree';\nimport fs from 'fs';\nimport path from"
  },
  {
    "path": "src/index.ts",
    "chars": 12751,
    "preview": "import compression from 'compression'\nimport timeout from 'connect-timeout'\nimport cors from 'cors'\nimport express from "
  },
  {
    "path": "src/logger.ts",
    "chars": 1222,
    "preview": "import winston from 'winston';\n\nconst ALLOWED_CHARACTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567"
  },
  {
    "path": "src/openapi.yaml",
    "chars": 8800,
    "preview": "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"
  },
  {
    "path": "tsconfig.json",
    "chars": 516,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"esnext\",\n    \"lib\": [\n      \"DOM\",\n      \"DOM.Iterable\",\n      \"ESNext\"\n\n    ],\n"
  }
]

About this extraction

This page contains the full source code of the kesor/chatgpt-code-plugin GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 16 files (40.9 KB), approximately 10.2k tokens, and a symbol index with 20 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!