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

## 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

2. You must have ChatGPT Plugin Developer available to you as well

## 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"
]
}
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
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.