├── .gitignore ├── logo.png ├── .vscode ├── extensions.json └── settings.json ├── assets └── thumbnail.png ├── nodemon.json ├── .dockerignore ├── src ├── constants.ts ├── plugin │ ├── environment.ts │ ├── commandController.ts │ └── fileController.ts ├── sessions │ ├── sessionsController.ts │ ├── process.ts │ ├── filesystemController.ts │ ├── processesController.ts │ └── session.ts ├── index.ts └── generated │ └── routes.ts ├── tsoa.json ├── .well-known └── ai-plugin.json ├── Dockerfile ├── scripts ├── build.js └── formatSpec.js ├── LICENSE ├── .devcontainer └── devcontainer.json ├── package.json ├── DEV.md ├── tsconfig.json ├── .github └── workflows │ └── deploy.yml ├── openapi.yaml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | node_modules 3 | lib 4 | tsconfig.tsbuildinfo 5 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2b-dev/llm-code-interpreter/HEAD/logo.png -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint" 4 | ] 5 | } -------------------------------------------------------------------------------- /assets/thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/e2b-dev/llm-code-interpreter/HEAD/assets/thumbnail.png -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": [ 3 | "src" 4 | ], 5 | "ignore": [ 6 | "src/generated/**/*" 7 | ], 8 | "ext": "ts", 9 | "exec": "npm run generate && npx tsx src/index.ts" 10 | } -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | DEV.md 3 | .devcontainer 4 | .github 5 | LICENSE 6 | nodemon.json 7 | openapi.yaml 8 | tsconfig.tsbuildinfo 9 | README.md 10 | .gitignore 11 | .venv 12 | lib 13 | node_modules 14 | .git 15 | npm-debug.log -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const defaultPort = 3000 2 | 3 | export const openAIConversationIDHeader = 'openai-conversation-id' 4 | export const openAIUserIDHeader = 'openai-ephemeral-user-id' 5 | 6 | export const textPlainMIME = 'text/plain; charset=UTF-8' 7 | 8 | export const maxSessionLength = 600 // 600 seconds = 10 minutes 9 | -------------------------------------------------------------------------------- /tsoa.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryFile": "src/index.ts", 3 | "noImplicitAdditionalProperties": "throw-on-extras", 4 | "controllerPathGlobs": [ 5 | "src/plugin/*Controller.ts" 6 | ], 7 | "ignore": [ 8 | "src/generated" 9 | ], 10 | "spec": { 11 | "outputDirectory": "./", 12 | "specVersion": 3, 13 | "spec": { 14 | "servers": [ 15 | { 16 | "url": "http://localhost:3000", 17 | "description": "Local development" 18 | } 19 | ] 20 | }, 21 | "yaml": true, 22 | "specFileBaseName": "openapi" 23 | }, 24 | "routes": { 25 | "routesDir": "src/generated" 26 | } 27 | } -------------------------------------------------------------------------------- /src/plugin/environment.ts: -------------------------------------------------------------------------------- 1 | import { 2 | components as devbookAPIComponents, 3 | } from '@devbookhq/sdk' 4 | 5 | /** 6 | * Type of environment to use. 7 | * This is used to determine which languages are installed by default. 8 | * 9 | * @format env 10 | */ 11 | export type Environment = devbookAPIComponents['schemas']['Template'] 12 | 13 | export const defaultEnvironment: Environment = 'Nodejs' 14 | 15 | export function getUserSessionID(conversationID: string | undefined, env: Environment) { 16 | if (!conversationID) { 17 | throw new Error('conversationID in header is required') 18 | } 19 | return `${conversationID}-${env}` 20 | } 21 | -------------------------------------------------------------------------------- /.well-known/ai-plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema_version": "v1", 3 | "name_for_human": "E2B code interpreter", 4 | "name_for_model": "e2bcodeintepreter", 5 | "description_for_human": "Sandboxed cloud environment where you can run any process, use filesystem and access the internet", 6 | "description_for_model": "Plugin for writing and reading files and running processes with access to the internet in a cloud environment.", 7 | "auth": { 8 | "type": "none" 9 | }, 10 | "api": { 11 | "type": "openapi", 12 | "url": "http://localhost:3000/openapi.yaml" 13 | }, 14 | "logo_url": "https://raw.githubusercontent.com/e2b-dev/chatgpt-plugin/main/logo.png", 15 | "contact_email": "hello@e2b.dev", 16 | "legal_info_url": "http://example.com/legal" 17 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Install modules 2 | FROM node:lts-slim as modules 3 | 4 | WORKDIR /app 5 | 6 | COPY package.json package-lock.json ./ 7 | RUN npm ci --omit=dev 8 | 9 | # Transpile Typescript 10 | FROM node:lts-slim as build 11 | 12 | WORKDIR /app 13 | 14 | COPY package.json package-lock.json ./ 15 | RUN npm ci 16 | 17 | COPY tsconfig.json . 18 | COPY tsoa.json . 19 | COPY scripts/ ./scripts/ 20 | COPY src ./src 21 | RUN npm run build 22 | 23 | # Export image 24 | FROM node:lts-slim 25 | 26 | WORKDIR /app 27 | 28 | RUN mkdir -p .well-known 29 | COPY .well-known/ai-plugin.json ./.well-known/ai-plugin.json 30 | 31 | COPY --from=modules ./app . 32 | COPY --from=build ./app/lib ./lib 33 | COPY --from=build ./app/openapi.yaml . 34 | 35 | EXPOSE 3000 36 | 37 | CMD ["node", "lib/index.js"] 38 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | 3 | const esbuild = require('esbuild') 4 | 5 | const makeAllPackagesExternalPlugin = { 6 | name: 'make-all-packages-external', 7 | setup(build) { 8 | const filter = /^[^./]|^\.[^./]|^\.\.[^/]/ // Must not start with "/" or "./" or "../" 9 | build.onResolve({ filter }, (args) => ({ 10 | path: args.path, 11 | external: true, 12 | })) 13 | }, 14 | } 15 | 16 | esbuild 17 | .build({ 18 | bundle: true, 19 | minify: true, 20 | tsconfig: 'tsconfig.json', 21 | platform: 'node', 22 | target: 'node18', 23 | sourcemap: 'both', 24 | plugins: [makeAllPackagesExternalPlugin], 25 | outdir: 'lib', 26 | entryPoints: ['src/index.ts'], 27 | }) 28 | .catch(() => process.exit(1)) 29 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "**/.git": true, 4 | "**/.svn": true, 5 | "**/.hg": true, 6 | "**/CVS": true, 7 | "**/.DS_Store": true, 8 | "lib/**": true, 9 | ".next/**": true, 10 | ".vscode/**": true, 11 | ".git/**": true, 12 | "node_modules/**": true, 13 | "package-lock.json": true, 14 | "yarn.lock": true 15 | }, 16 | "files.watcherExclude": { 17 | "**/.git": true, 18 | "**/.svn": true, 19 | "**/.hg": true, 20 | "**/CVS": true, 21 | "**/.DS_Store": true, 22 | "lib/**": true, 23 | ".next/**": true, 24 | ".vscode/**": true, 25 | ".git/**": true, 26 | "node_modules/**": true, 27 | "package-lock.json": true, 28 | "yarn.lock": true 29 | }, 30 | "typescript.tsdk": "node_modules/typescript/lib", 31 | "editor.quickSuggestions": { 32 | "strings": true 33 | }, 34 | "editor.formatOnSave": false, 35 | "editor.codeActionsOnSave": { 36 | "source.fixAll.eslint": true 37 | }, 38 | "eslint.validate": ["javascript", "typescript"], 39 | "css.validate": false 40 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 e2b 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/sessions/sessionsController.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Delete, 4 | Path, 5 | Get, 6 | Post, 7 | BodyProp, 8 | Route, 9 | } from 'tsoa' 10 | import { OpenedPort as OpenPort } from '@devbookhq/sdk' 11 | 12 | import { CachedSession } from './session' 13 | 14 | interface SessionResponse { 15 | id: string 16 | ports: OpenPort[] 17 | } 18 | 19 | @Route('sessions') 20 | export class SessionsController extends Controller { 21 | @Post() 22 | public async createSessions( 23 | @BodyProp() envID: string, 24 | ): Promise { 25 | const cachedSession = await new CachedSession(envID).init() 26 | 27 | return { 28 | id: cachedSession.id!, 29 | ports: cachedSession.ports, 30 | } 31 | } 32 | 33 | @Delete('{sessionID}') 34 | public async deleteSession( 35 | @Path() sessionID: string, 36 | ): Promise { 37 | await CachedSession 38 | .findSession(sessionID) 39 | .delete() 40 | } 41 | 42 | @Get('{sessionID}') 43 | public async getSession( 44 | @Path() sessionID: string, 45 | ): Promise { 46 | const cachedSession = CachedSession.findSession(sessionID) 47 | 48 | return { 49 | id: cachedSession.id!, 50 | ports: cachedSession.ports, 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/ubuntu 3 | { 4 | "name": "Ubuntu", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/base:jammy", 7 | "features": { 8 | "ghcr.io/devcontainers/features/docker-in-docker:2": {}, 9 | "ghcr.io/devcontainers/features/github-cli:1": {}, 10 | "ghcr.io/devcontainers/features/node:1": {}, 11 | "ghcr.io/devcontainers-contrib/features/actionlint:1": {}, 12 | "ghcr.io/devcontainers-contrib/features/actions-runner:1": {} 13 | } 14 | 15 | // Features to add to the dev container. More info: https://containers.dev/features. 16 | // "features": {}, 17 | 18 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 19 | // "forwardPorts": [], 20 | 21 | // Use 'postCreateCommand' to run commands after the container is created. 22 | // "postCreateCommand": "uname -a", 23 | 24 | // Configure tool-specific properties. 25 | // "customizations": {}, 26 | 27 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 28 | // "remoteUser": "root" 29 | } 30 | -------------------------------------------------------------------------------- /src/sessions/process.ts: -------------------------------------------------------------------------------- 1 | import { 2 | OutStderrResponse, 3 | OutStdoutResponse, 4 | ProcessManager, 5 | createSessionProcess, 6 | } from '@devbookhq/sdk' 7 | 8 | export interface RunProcessParams extends Pick[0], 'cmd' | 'envVars' | 'rootdir'> { } 9 | 10 | export class CachedProcess { 11 | readonly stdout: OutStdoutResponse[] = [] 12 | readonly stderr: OutStderrResponse[] = [] 13 | 14 | private started = false 15 | 16 | finished = false 17 | process?: Awaited> 18 | 19 | get response() { 20 | return { 21 | stdout: this.stdout, 22 | stderr: this.stderr, 23 | finished: this.finished, 24 | processID: this.process?.processID!, 25 | } 26 | } 27 | 28 | constructor(private readonly manager: ProcessManager) { } 29 | 30 | async start(params: RunProcessParams) { 31 | if (this.started) throw new Error('Process already started') 32 | this.started = true 33 | 34 | const process = await createSessionProcess({ 35 | manager: this.manager, 36 | onStderr: o => this.stderr.push(o), 37 | onStdout: o => this.stdout.push(o), 38 | ...params, 39 | }) 40 | 41 | process.exited.finally(() => { 42 | this.finished = true 43 | }) 44 | 45 | this.process = process 46 | return process 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@e2b/chatgpt-plugin", 3 | "version": "0.1.0", 4 | "description": "e2b ChatGPT Plugin", 5 | "homepage": "https://e2b.dev", 6 | "license": "SEE LICENSE IN LICENSE", 7 | "author": { 8 | "name": "FoundryLabs, Inc.", 9 | "email": "hello@e2b.dev", 10 | "url": "https://e2b.dev" 11 | }, 12 | "bugs": "https://github.com/e2b-dev/chatgpt-plugin/issues", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/e2b-dev/chatgpt-plugin/tree/main" 16 | }, 17 | "dependencies": { 18 | "@devbookhq/sdk": "^2.6.82", 19 | "cors": "^2.8.5", 20 | "express": "^4.18.2", 21 | "morgan": "^1.10.0", 22 | "node-cache": "^5.1.2", 23 | "tsoa": "^5.1.1" 24 | }, 25 | "devDependencies": { 26 | "@types/cors": "^2.8.13", 27 | "@types/express": "^4.17.17", 28 | "@types/morgan": "^1.9.4", 29 | "@types/node": "^20.4.5", 30 | "esbuild": "^0.18.17", 31 | "js-yaml": "^4.1.0", 32 | "nodemon": "^3.0.1", 33 | "tsx": "^3.12.7", 34 | "typescript": "^5.1.6" 35 | }, 36 | "main": "lib/index.js", 37 | "scripts": { 38 | "dev": "nodemon", 39 | "build": "npm run generate && tsc --noEmit && node ./scripts/build.js", 40 | "start": "node lib/index.js", 41 | "generate": "tsoa spec-and-routes && node ./scripts/formatSpec.js", 42 | "docker:build": "docker build -t e2b/chatgpt-plugin-api .", 43 | "docker:start": "docker run --init -p 3000:3000 e2b/chatgpt-plugin-api" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/plugin/commandController.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Post, 4 | Header, 5 | BodyProp, 6 | Route, 7 | Query, 8 | } from 'tsoa' 9 | 10 | import { CachedSession } from '../sessions/session' 11 | import { openAIConversationIDHeader } from '../constants' 12 | import { Environment, defaultEnvironment, getUserSessionID } from './environment' 13 | 14 | /** 15 | * 16 | */ 17 | interface CommandResponse { 18 | /** 19 | * Standard error output from the command 20 | */ 21 | stderr: string 22 | /** 23 | * Standard output from the command 24 | */ 25 | stdout: string 26 | } 27 | 28 | @Route('commands') 29 | export class CommandController extends Controller { 30 | /** 31 | * @summary Run a command in a shell 32 | * 33 | * @param env Environment to run the command in 34 | * @param command Command to run 35 | * @param workDir Working directory to run the command in 36 | * @returns JSON containing the standard output and error output of the command 37 | */ 38 | @Post() 39 | public async runCommand( 40 | @BodyProp() command: string, 41 | @BodyProp() workDir: string, 42 | @Query() env: Environment = defaultEnvironment, 43 | @Header(openAIConversationIDHeader) conversationID?: string, 44 | ): Promise { 45 | const sessionID = getUserSessionID(conversationID, env) 46 | const session = await CachedSession.findOrStartSession({ sessionID, envID: env }) 47 | 48 | const cachedProcess = await session.startProcess({ 49 | cmd: command, 50 | rootdir: workDir, 51 | }) 52 | 53 | await cachedProcess.process?.exited 54 | 55 | return { 56 | stderr: cachedProcess.response.stderr.map(({ line }) => line).join('\n'), 57 | stdout: cachedProcess.response.stdout.map(({ line }) => line).join('\n'), 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /DEV.md: -------------------------------------------------------------------------------- 1 | # Dev 2 | ## Issues 3 | - Work on plugin manifest and OpenAPI spec descriptions 4 | - Change deployment URL 5 | - Add prettier 6 | - Add eslint 7 | - Add handling of binary files upload/read/write (right now the read/write is handling utf-8 data) - https://tsoa-community.github.io/docs/file-upload.html 8 | - Expose more environment methods and parameters 9 | - Expose URL for the running environment 10 | - Fix logo hosting (not GH) 11 | - Fix legal page url 12 | - Add spectral for linting tsoa generated docs (https://stoplight.io/p/docs/gh/stoplightio/spectral) 13 | - Add examples to API doc 14 | - Add lint action 15 | - Enable GH analysis 16 | - Add vale 17 | - Add issue/PR templates 18 | - Improve README.md 19 | - Add support for function calling OpenAI API 20 | - Add url of the localhost server to the spec only in dev env 21 | - Post request confirmation flow? 22 | - Prepare for "install unverified plugin" 23 | - Publish plugin 24 | - Remove text body parsers? (chatGPT doesn't play well with plain text body) 25 | - Make issues from user issues 26 | - Can we document the response without creating return types? 27 | - ChatGPT tries to use read file even for listing directory content 28 | - Handle in which env are the operations executed because sometimes ChatGPT will switch to another env between operations - maybe create a single env that has all the dependencies? 29 | - Enable diff edits to save context 30 | - ChatGPT sometimes insists that it doesn't have capacity for editing files (on GitHub) - maybe we should rename "code interpreter" to something less obvious that it can edit downloaded repo 31 | - ChatGPT seems to think that when using Python env it can run python with the RunCommand operation directly (is is not using `python print(...)` but `print(...)`). It may think it is in the Python REPL. 32 | 33 | > we can change the API for the plugins whenever we want - we don't need to think about backward compatibility that much because the API is understood again everytime the plugin is used. 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "es2020" 5 | ], 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "module": "es2020", 9 | "moduleResolution": "node", 10 | "target": "es2020", 11 | "allowJs": true, /* Allow javascript files to be compiled. */ 12 | "checkJs": true, /* Report errors in .js files. */ 13 | "outDir": "lib", /* Redirect output structure to the directory. */ 14 | "rootDir": "src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 15 | "strict": true, /* Enable all strict type-checking options. */ 16 | "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */ 17 | "strictNullChecks": true, /* Enable strict null checks. */ 18 | "strictFunctionTypes": true, /* Enable strict checking of function types. */ 19 | "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 20 | "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 21 | "allowSyntheticDefaultImports": true, 22 | "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 23 | "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 24 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 25 | "skipLibCheck": true, 26 | // "downlevelIteration": true, 27 | "resolveJsonModule": true, /* Include modules imported with '.json' extension */ 28 | "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ 29 | "incremental": true, 30 | "noUnusedLocals": true, 31 | "noUnusedParameters": true, 32 | "noImplicitReturns": true, 33 | "noFallthroughCasesInSwitch": true, 34 | }, 35 | "exclude": [ 36 | "scripts", 37 | "src/generated", 38 | "lib", 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy ChatGPT Plugin API to Google Compute Engine 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | paths-ignore: 7 | - '.devcontainer/**' 8 | - '.vscode/**' 9 | - 'README.md' 10 | - 'LICENSE' 11 | - 'nodemon.json' 12 | - 'openapi.json' 13 | - 'DEV.md' 14 | branches: 15 | - main 16 | 17 | concurrency: 18 | group: ${{ github.workflow }}-${{ github.ref }} 19 | cancel-in-progress: true 20 | 21 | permissions: 22 | id-token: write 23 | contents: read 24 | 25 | env: 26 | IMAGE_TAG: ${{ vars.IMAGE_TAG }} 27 | GCE_INSTANCE: ${{ vars.GCE_INSTANCE }} 28 | GCE_INSTANCE_ZONE: ${{ vars.GCE_INSTANCE_ZONE }} 29 | 30 | # TODO: Update - https://stackoverflow.com/questions/68244641/how-to-circumvent-compute-engines-downtime-during-container-deployments 31 | jobs: 32 | deploy: 33 | name: Build and deploy 34 | runs-on: ubuntu-20.04 35 | steps: 36 | - name: Checkout repository 37 | uses: actions/checkout@v3 38 | 39 | - name: Setup Service Account 40 | uses: google-github-actions/auth@v1 41 | with: 42 | workload_identity_provider: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} 43 | service_account: ${{ secrets.SERVICE_ACCOUNT_EMAIL }} 44 | 45 | - name: Configure Docker 46 | run: gcloud --quiet auth configure-docker us-central1-docker.pkg.dev 47 | 48 | - name: Set up Docker Buildx 49 | uses: docker/setup-buildx-action@v2 50 | 51 | - name: Build and Push 52 | uses: docker/build-push-action@v3 53 | with: 54 | push: true 55 | tags: ${{ env.IMAGE_TAG }} 56 | cache-from: type=gha,scope=e2b-chatgpt-plugin-api 57 | cache-to: type=gha,scope=e2b-chatgpt-plugin-api 58 | 59 | - name: Delete old docker images from the instance 60 | run: |- 61 | gcloud compute instances add-metadata "$GCE_INSTANCE" \ 62 | --zone "$GCE_INSTANCE_ZONE" \ 63 | --metadata startup-script="#! /bin/bash 64 | docker image prune -af" 65 | 66 | - name: Update the server container 67 | run: |- 68 | gcloud compute instances update-container "$GCE_INSTANCE" \ 69 | --zone "$GCE_INSTANCE_ZONE" \ 70 | --container-env "PORT=80" \ 71 | --container-image "$IMAGE_TAG" 72 | -------------------------------------------------------------------------------- /src/sessions/filesystemController.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Path, 4 | Get, 5 | Delete, 6 | Put, 7 | Query, 8 | Route, 9 | BodyProp, 10 | } from 'tsoa' 11 | import { FileInfo as EntryInfo } from '@devbookhq/sdk' 12 | import { dirname } from 'path' 13 | 14 | import { CachedSession } from './session' 15 | 16 | interface ListFilesystemDirResponse { 17 | entries: EntryInfo[] 18 | } 19 | 20 | interface ReadFilesystemFileResponse { 21 | content: string 22 | } 23 | 24 | @Route('sessions') 25 | export class FilesystemController extends Controller { 26 | @Get('{sessionID}/filesystem/dir') 27 | public async listFilesystemDir( 28 | @Path() sessionID: string, 29 | @Query() path: string, 30 | ): Promise { 31 | const entries = await CachedSession 32 | .findSession(sessionID) 33 | .session 34 | .filesystem! 35 | .list(path) 36 | 37 | return { 38 | entries, 39 | } 40 | } 41 | 42 | @Put('{sessionID}/filesystem/dir') 43 | public async makeFilesystemDir( 44 | @Path() sessionID: string, 45 | @Query() path: string, 46 | ): Promise { 47 | await CachedSession 48 | .findSession(sessionID) 49 | .session 50 | .filesystem 51 | ?.makeDir(path) 52 | } 53 | 54 | @Delete('{sessionID}/filesystem') 55 | public async deleteFilesystemEntry( 56 | @Path() sessionID: string, 57 | @Query() path: string, 58 | ) { 59 | await CachedSession 60 | .findSession(sessionID) 61 | .session 62 | .filesystem! 63 | .remove(path) 64 | } 65 | 66 | @Get('{sessionID}/filesystem/file') 67 | public async readFilesystemFile( 68 | @Path() sessionID: string, 69 | @Query() path: string, 70 | ): Promise { 71 | const content = await CachedSession 72 | .findSession(sessionID) 73 | .session 74 | .filesystem! 75 | .read(path) 76 | 77 | return { 78 | content, 79 | } 80 | } 81 | 82 | @Put('{sessionID}/filesystem/file') 83 | public async writeFilesystemFile( 84 | @Path() sessionID: string, 85 | @Query() path: string, 86 | @BodyProp() content: string, 87 | ) { 88 | const dir = dirname(path) 89 | 90 | const session = CachedSession 91 | .findSession(sessionID) 92 | .session 93 | 94 | await session.filesystem!.makeDir(dir) 95 | await session.filesystem!.write(path, content) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /scripts/formatSpec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | 3 | const yaml = require('js-yaml') 4 | const fs = require('fs'); 5 | 6 | const spec = yaml.load(fs.readFileSync('openapi.yaml', 'utf8')) 7 | 8 | function isObjEmpty (obj) { 9 | return Object.keys(obj).length === 0; 10 | } 11 | 12 | const components = spec['components'] 13 | 14 | if (components) { 15 | Object.keys(components).forEach((key) => { 16 | // Delete empty component schemas 17 | if (isObjEmpty(components[key])) { 18 | delete components[key] 19 | } 20 | }) 21 | 22 | // Delete components if empty 23 | if (isObjEmpty(components)) { 24 | delete spec['components'] 25 | } 26 | } 27 | 28 | const paths = spec['paths'] 29 | 30 | if (paths) { 31 | Object.keys(paths).forEach((key) => { 32 | // Delete empty paths 33 | if (isObjEmpty(paths[key])) { 34 | delete paths[key] 35 | } 36 | 37 | 38 | Object.keys(paths[key]).forEach((method) => { 39 | // Delete empty methods 40 | if (isObjEmpty(paths[key][method])) { 41 | delete paths[key][method] 42 | } 43 | 44 | // Delete security if empty 45 | if (paths[key][method]['security']) { 46 | if (paths[key][method]['security'].length === 0) { 47 | delete paths[key][method]['security'] 48 | } 49 | } 50 | 51 | // Delete openai-conversation-id header if exists - this should not be in spec for the plugin, it is automatically added by openai 52 | if (paths[key][method]['parameters']) { 53 | const conversationIDParameterIdx = paths[key][method]['parameters'].findIndex((parameter) => parameter.name === 'openai-conversation-id' && parameter.in === 'header') 54 | if (conversationIDParameterIdx !== -1) { 55 | paths[key][method]['parameters'].splice(conversationIDParameterIdx, 1) 56 | } 57 | } 58 | }) 59 | }) 60 | } 61 | 62 | const info = spec['info'] 63 | if (info) { 64 | // Delete old info - it is generated from package.json with excessive fields 65 | delete spec['info'] 66 | } 67 | 68 | // Add new info 69 | spec['info'] = { 70 | title: 'E2B Code Interpreter', 71 | description: 'A plugin that allows writting and reading files and running processes in a cloud environment.', 72 | version: 'v1', 73 | } 74 | 75 | fs.writeFileSync('openapi.yaml', yaml.dump(spec, { sortKeys: true, indent: 2, condenseFlow: true, noArrayIndent: true, noCompatMode: true, lineWidth: -1 })) 76 | -------------------------------------------------------------------------------- /src/plugin/fileController.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Put, 5 | Header, 6 | Query, 7 | Route, 8 | BodyProp, 9 | TsoaResponse, 10 | Res, 11 | Produces, 12 | } from 'tsoa' 13 | import { dirname } from 'path' 14 | 15 | import { CachedSession } from '../sessions/session' 16 | import { openAIConversationIDHeader, textPlainMIME } from '../constants' 17 | import { Environment, defaultEnvironment, getUserSessionID } from './environment' 18 | 19 | @Route('files') 20 | export class FileController extends Controller { 21 | /** 22 | * @summary Read the contents of a file at the given path 23 | * 24 | * @param env Environment where to read the file from 25 | * @param path Path to the file to read 26 | * @param notFoundResponse Response to send if the file is not found 27 | * @returns Contents of the file as a string 28 | */ 29 | @Get() 30 | @Produces(textPlainMIME) 31 | public async readFile( 32 | @Query() env: Environment = defaultEnvironment, 33 | @Query() path: string, 34 | @Res() notFoundResponse: TsoaResponse<404, { reason: string }>, 35 | @Header(openAIConversationIDHeader) conversationID?: string, 36 | ): Promise { 37 | const sessionID = getUserSessionID(conversationID, env) 38 | const session = await CachedSession.findOrStartSession({ sessionID, envID: env }) 39 | 40 | // Even though we're returning a string and using @Produces, we need to set the content type manually. 41 | this.setHeader('Content-Type', textPlainMIME) 42 | 43 | try { 44 | return await session 45 | .session 46 | .filesystem! 47 | .read(path) 48 | } catch (err) { 49 | console.error(err) 50 | return notFoundResponse(404, { 51 | reason: `File on path "${path}" not found`, 52 | }) 53 | } 54 | } 55 | 56 | /** 57 | * @summary Write content to a file at the given path 58 | * 59 | * @param env Environment where to write the file 60 | * @param path Path to the file to write 61 | * @param content Content to write to the file 62 | */ 63 | @Put() 64 | public async writeFile( 65 | @Query() env: Environment = defaultEnvironment, 66 | @Query() path: string, 67 | @BodyProp() content: string, 68 | @Header(openAIConversationIDHeader) conversationID?: string, 69 | ) { 70 | const sessionID = getUserSessionID(conversationID, env) 71 | const session = await CachedSession.findOrStartSession({ sessionID, envID: env }) 72 | 73 | const dir = dirname(path) 74 | await session.session.filesystem!.makeDir(dir) 75 | await session.session.filesystem!.write(path, content) 76 | } 77 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import express, { 2 | urlencoded, 3 | text, 4 | json, 5 | Response as ExResponse, 6 | Request as ExRequest, 7 | NextFunction, 8 | } from 'express' 9 | import { ValidateError } from 'tsoa' 10 | import morgan from 'morgan' 11 | import cors from 'cors' 12 | import path from 'path' 13 | import { readFileSync } from 'fs' 14 | 15 | import { RegisterRoutes } from './generated/routes' 16 | import { defaultPort } from './constants' 17 | 18 | const apiKey = process.env.E2B_API_KEY 19 | if (!apiKey) throw new Error('E2B_API_KEY is not set. Please visit https://e2b.dev/docs?reason=sdk-missing-api-key to get your API key.') 20 | 21 | export const app = express() 22 | 23 | app.use( 24 | cors(), 25 | urlencoded({ 26 | extended: true, 27 | }), 28 | morgan('tiny'), 29 | text(), 30 | json(), 31 | ) 32 | 33 | app.get('/health', (_, res) => { 34 | res.status(200).send('OK') 35 | }) 36 | 37 | function loadStaticFile(relativePath: string) { 38 | return readFileSync(path.join(__dirname, relativePath), 'utf-8') 39 | } 40 | 41 | const pluginManifest = loadStaticFile('../.well-known/ai-plugin.json') 42 | const pluginAPISpec = loadStaticFile('../openapi.yaml') 43 | 44 | app.get( 45 | '/.well-known/ai-plugin.json', 46 | (_, res) => { 47 | res 48 | .setHeader('Content-Type', 'text/json') 49 | .status(200) 50 | .send(pluginManifest) 51 | } 52 | ) 53 | 54 | app.get( 55 | '/openapi.yaml', 56 | (_, res) => { 57 | res 58 | .setHeader('Content-Type', 'text/yaml') 59 | .status(200) 60 | .send(pluginAPISpec) 61 | } 62 | ) 63 | 64 | RegisterRoutes(app) 65 | 66 | app.use( 67 | function notFoundHandler(_req, res: ExResponse) { 68 | res.status(500).send({ 69 | message: 'Not Found', 70 | }) 71 | }, 72 | function errorHandler( 73 | err: unknown, 74 | req: ExRequest, 75 | res: ExResponse, 76 | next: NextFunction 77 | ): ExResponse | void { 78 | if (err instanceof ValidateError) { 79 | console.warn(`Caught Validation Error for ${req.path}:`, err.fields, err) 80 | return res.status(422).json({ 81 | message: 'Validation Failed', 82 | details: err?.fields, 83 | }) 84 | } 85 | if (err instanceof Error) { 86 | console.warn(`Caught Internal Error for ${req.path}:`, err) 87 | return res.status(500).json({ 88 | message: 'Internal Server Error', 89 | }) 90 | } 91 | next() 92 | }, 93 | ) 94 | 95 | const port = process.env.PORT || defaultPort 96 | 97 | app.listen(port, () => 98 | console.log(`Listening at http://localhost:${port}`) 99 | ) 100 | -------------------------------------------------------------------------------- /src/sessions/processesController.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Path, 5 | Get, 6 | Post, 7 | Route, 8 | Delete, 9 | Query, 10 | BodyProp, 11 | } from 'tsoa' 12 | import { 13 | ProcessManager, 14 | OutStderrResponse, 15 | OutStdoutResponse, 16 | } from '@devbookhq/sdk' 17 | 18 | import { CachedSession } from './session' 19 | 20 | interface StartProcessParams extends Pick[0], 'cmd' | 'envVars' | 'rootdir'> { } 21 | 22 | interface ProcessResponse { 23 | stderr: OutStderrResponse[] 24 | stdout: OutStdoutResponse[] 25 | processID: string 26 | finished: boolean 27 | } 28 | 29 | @Route('sessions') 30 | export class ProcessController extends Controller { 31 | /** 32 | * 33 | * @param sessionID 34 | * @param wait if true the request will wait until the process ends and then return the `stdout`, `stderr` and `processID`. 35 | * @param requestBody 36 | * @returns `processID` and all `stdout` and `stderr` that the process outputted until now. 37 | */ 38 | @Post('{sessionID}/processes') 39 | public async startProcess( 40 | @Path() sessionID: string, 41 | @Body() requestBody: StartProcessParams, 42 | @Query() wait?: boolean, 43 | ): Promise { 44 | const cachedProcess = await CachedSession 45 | .findSession(sessionID) 46 | .startProcess(requestBody) 47 | 48 | if (wait) { 49 | await cachedProcess.process?.exited 50 | } 51 | return cachedProcess.response 52 | } 53 | 54 | @Delete('{sessionID}/processes/{processID}') 55 | public async stopProcess( 56 | @Path() sessionID: string, 57 | @Path() processID: string, 58 | @Query() results?: boolean 59 | ): Promise { 60 | const cachedProcess = await CachedSession 61 | .findSession(sessionID) 62 | .stopProcess(processID) 63 | 64 | return cachedProcess && results 65 | ? cachedProcess.response 66 | : undefined 67 | } 68 | 69 | @Post('{sessionID}/processes/{processID}/stdin') 70 | public async writeProcessStdin( 71 | @Path() sessionID: string, 72 | @Path() processID: string, 73 | @BodyProp() stdin: string, 74 | ): Promise { 75 | await CachedSession 76 | .findSession(sessionID) 77 | .findProcess(processID) 78 | ?.process 79 | ?.sendStdin(stdin) 80 | } 81 | 82 | /** 83 | * 84 | * @param sessionID 85 | * @param processID 86 | * @param wait if true the request will wait until the process ends and then return the `stdout`, `stderr` and `processID`. 87 | * @returns `processID` and all `stdout` and `stderr` that the process outputted until now. 88 | */ 89 | @Get('{sessionID}/processes/{processID}') 90 | public async getProcess( 91 | @Path() sessionID: string, 92 | @Path() processID: string, 93 | @Query() wait?: boolean, 94 | ): Promise { 95 | const cachedProcess = CachedSession 96 | .findSession(sessionID) 97 | .findProcess(processID) 98 | 99 | if (wait) { 100 | await cachedProcess?.process?.exited 101 | } 102 | 103 | return cachedProcess?.response! 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/sessions/session.ts: -------------------------------------------------------------------------------- 1 | import { OpenedPort as OpenPort, Session } from '@devbookhq/sdk' 2 | import NodeCache from 'node-cache' 3 | 4 | import { CachedProcess, RunProcessParams } from './process' 5 | import { maxSessionLength } from '../constants' 6 | 7 | export const sessionCache = new NodeCache({ 8 | stdTTL: maxSessionLength, 9 | checkperiod: 10, 10 | useClones: false, 11 | deleteOnExpire: true, 12 | }) 13 | 14 | sessionCache.on('expired', async function (_, cached: CachedSession) { 15 | try { 16 | await cached.delete() 17 | } catch (err) { 18 | console.error(err) 19 | } 20 | }) 21 | 22 | export class CachedSession { 23 | private readonly cachedProcesses: CachedProcess[] = [] 24 | 25 | private closed = false 26 | 27 | ports: OpenPort[] = [] 28 | 29 | id?: string 30 | cacheID?: string 31 | session: Session 32 | 33 | /** 34 | * You must call `.init()` to start the session. 35 | * 36 | * @param envID 37 | */ 38 | constructor(envID: string) { 39 | this.session = new Session({ 40 | id: envID, 41 | apiKey: process.env.E2B_API_KEY, 42 | onClose: () => { 43 | this.delete() 44 | }, 45 | codeSnippet: { 46 | onScanPorts: (ports) => { 47 | // We need to remap the ports because there is a lot of hidden properties 48 | // that breaks the generated API between client and server. 49 | this.ports = ports.map(p => ({ 50 | Ip: p.Ip, 51 | Port: p.Port, 52 | State: p.State, 53 | })) 54 | }, 55 | }, 56 | }) 57 | } 58 | 59 | /** 60 | * @param customCacheID If you want to use a custom cache ID instead of the default one 61 | */ 62 | async init(customCacheID?: string) { 63 | await this.session.open() 64 | 65 | const url = this.session.getHostname() 66 | if (!url) throw new Error('Cannot start session') 67 | console.log('Opened session', url) 68 | 69 | const [id] = url.split('.') 70 | this.id = id 71 | this.cacheID = customCacheID || id 72 | sessionCache.set(this.cacheID, this) 73 | 74 | // Temporary hack to fix the clock drift issue for Alpine Linux 75 | this.session.filesystem?.write('/etc/chrony/chrony.conf', `initstepslew 0.5 pool.ntp.org 76 | makestep 0.5 -1`).then(() => { 77 | this.startProcess({ 78 | cmd: `rc-service chronyd restart`, 79 | }) 80 | }) 81 | 82 | return this 83 | } 84 | 85 | async delete() { 86 | if (!this.cacheID) return 87 | if (this.closed) return 88 | this.closed = true 89 | 90 | await this.session.close() 91 | sessionCache.del(this.cacheID) 92 | } 93 | 94 | async stopProcess(processID: string) { 95 | const cachedProcess = this.findProcess(processID) 96 | if (!cachedProcess) return 97 | 98 | await cachedProcess.process?.kill() 99 | const idx = this.cachedProcesses.findIndex(p => p.process?.processID === processID) 100 | if (idx !== -1) { 101 | this.cachedProcesses.splice(idx, 1) 102 | } 103 | 104 | return cachedProcess 105 | } 106 | 107 | findProcess(processID: string) { 108 | return this.cachedProcesses.find(p => p.process?.processID === processID) 109 | } 110 | 111 | async startProcess(params: RunProcessParams) { 112 | if (!this.session.process) throw new Error('Session is not open') 113 | 114 | const cachedProcess = new CachedProcess(this.session.process) 115 | await cachedProcess.start(params) 116 | this.cachedProcesses.push(cachedProcess) 117 | 118 | return cachedProcess 119 | } 120 | 121 | static async findOrStartSession({ sessionID, envID }: { sessionID: string, envID: string }) { 122 | try { 123 | return CachedSession.findSession(sessionID) 124 | } catch { 125 | const cachedSession = new CachedSession(envID) 126 | await cachedSession.init(sessionID) 127 | return cachedSession 128 | } 129 | } 130 | 131 | static findSession(id: string) { 132 | const cachedSession = sessionCache.get(id) as CachedSession 133 | if (!cachedSession) throw new Error('Session does not exist') 134 | return cachedSession 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /openapi.yaml: -------------------------------------------------------------------------------- 1 | components: 2 | schemas: 3 | CommandResponse: 4 | additionalProperties: false 5 | properties: 6 | stderr: 7 | description: Standard error output from the command 8 | type: string 9 | stdout: 10 | description: Standard output from the command 11 | type: string 12 | required: 13 | - stderr 14 | - stdout 15 | type: object 16 | Environment: 17 | description: |- 18 | Type of environment to use. 19 | This is used to determine which languages are installed by default. 20 | enum: 21 | - Nodejs 22 | - Go 23 | - Bash 24 | - Rust 25 | - Python3 26 | - PHP 27 | - Java 28 | - Perl 29 | - DotNET 30 | format: env 31 | type: string 32 | info: 33 | description: A plugin that allows writting and reading files and running processes in a cloud environment. 34 | title: E2B Code Interpreter 35 | version: v1 36 | openapi: 3.0.0 37 | paths: 38 | /commands: 39 | post: 40 | operationId: RunCommand 41 | parameters: 42 | - description: Environment to run the command in 43 | in: query 44 | name: env 45 | required: false 46 | schema: 47 | $ref: '#/components/schemas/Environment' 48 | requestBody: 49 | content: 50 | application/json: 51 | schema: 52 | properties: 53 | command: 54 | description: Command to run 55 | type: string 56 | workDir: 57 | description: Working directory to run the command in 58 | type: string 59 | required: 60 | - command 61 | - workDir 62 | type: object 63 | required: true 64 | responses: 65 | '200': 66 | content: 67 | application/json: 68 | schema: 69 | $ref: '#/components/schemas/CommandResponse' 70 | description: JSON containing the standard output and error output of the command 71 | summary: Run a command in a shell 72 | /files: 73 | get: 74 | operationId: ReadFile 75 | parameters: 76 | - description: Environment where to read the file from 77 | in: query 78 | name: env 79 | required: false 80 | schema: 81 | $ref: '#/components/schemas/Environment' 82 | - description: Path to the file to read 83 | in: query 84 | name: path 85 | required: true 86 | schema: 87 | type: string 88 | responses: 89 | '200': 90 | content: 91 | text/plain; charset=UTF-8: 92 | schema: 93 | type: string 94 | description: Contents of the file as a string 95 | '404': 96 | content: 97 | application/json: 98 | schema: 99 | properties: 100 | reason: 101 | type: string 102 | required: 103 | - reason 104 | type: object 105 | description: Response to send if the file is not found 106 | summary: Read the contents of a file at the given path 107 | put: 108 | operationId: WriteFile 109 | parameters: 110 | - description: Environment where to write the file 111 | in: query 112 | name: env 113 | required: false 114 | schema: 115 | $ref: '#/components/schemas/Environment' 116 | - description: Path to the file to write 117 | in: query 118 | name: path 119 | required: true 120 | schema: 121 | type: string 122 | requestBody: 123 | content: 124 | application/json: 125 | schema: 126 | properties: 127 | content: 128 | description: Content to write to the file 129 | type: string 130 | required: 131 | - content 132 | type: object 133 | required: true 134 | responses: 135 | '204': 136 | description: No content 137 | summary: Write content to a file at the given path 138 | servers: 139 | - description: Local development 140 | url: http://localhost:3000 141 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

DEPRECATED - visit Code Interpreter SDK instead

3 |

Code Interpreter on steroids for ChatGPT (by e2b)

4 |

5 | 6 | 7 | 8 | 9 | 10 | 11 |

12 | 13 | 14 |

This plugin is powered by E2B's AI Playgrounds

15 | 16 |

17 | Click on the image to watch the demo 18 |

19 | 20 | [![Video](./assets/thumbnail.png)](https://www.youtube.com/watch?v=kJuJnsatU2s?utm_source=github) 21 | 22 | [E2B](https://e2b.dev/) plugin for ChatGPT is like **code interpreter on steroids**. 23 | 24 | We give your ChatGPT instance access to a full cloud environment that's sandboxed. That means: 25 | - Access to Linux OS 26 | - Install programs 27 | - Using filesystem (create, list, and delete files and dirs) 28 | - Run processes 29 | - Sandboxed - you can run any code 30 | - Access to the internet 31 | 32 | These cloud instances are meant to be used for agents. Like a sandboxed playgrounds, where the agent can do whatever it wants. 33 | 34 | 👉 **This plugin is powered by the E2B API. If you'd like an early access, [join our Discord](https://discord.gg/U7KEcGErtQ) and send us a message!** 👈 35 | 36 | ## ❓ What can I do with this plugin? 37 | This plugin exposes 3 simple commands (see the [OpenAPI file](https://github.com/e2b-dev/chatgpt-plugin/blob/main/openapi.yaml)): 38 | - `RunCommand` 39 | - Runs any shell command 40 | - `ReadFile` 41 | - Reads file on path 42 | - `WriteFile` 43 | - Writes content to a file on a path 44 | 45 | These simple 3 primitives enable a whole set of possibilities. 46 | 47 | Here is a few ideas what you can do with these commands: 48 | - Run **any** language, not just Python. Currently supported out of the box: 49 | - Nodejs 50 | - Go 51 | - Bash 52 | - Rust 53 | - Python3 54 | - PHP 55 | - Java 56 | - Perl 57 | - .NET 58 | 59 | Please open an issue if you want us to support another language 60 | 61 | - Install headless chrome (go wild!) 62 | - Run databases 63 | - Start servers 64 | - Run terminal commands 65 | - Create long running processes 66 | - Deploy websites 67 | - Install programs via terminal 68 | 69 | ## 💻 Installation 70 | There are two ways: 71 | 1. Wait for OpenAI to approve our plugin in their store 72 | 2. Have developer access to ChatGPT plugins and install the plugin by following the instructions below for how to [run plugin locally](#how-to-run-plugin-locally) 73 | 74 | ### How to run plugin locally 75 | To install the required packages for this plugin, run the following command: 76 | 77 | ```bash 78 | npm install 79 | ``` 80 | 81 | To run the plugin, you will need **API Key**. Click [here](https://e2b.dev/docs?reason=sdk-missing-api-key) to get your API key. 82 | 83 | Then enter the following command: 84 | 85 | ```bash 86 | E2B_API_KEY=*** npm run dev 87 | ``` 88 | 89 | Once the local server is running: 90 | 91 | 1. Navigate to https://chat.openai.com. 92 | 2. In the Model drop down, select "Plugins" (note, if you don't see it there, you don't have access yet). 93 | 3. Select "Plugin store" 94 | 4. Select "Develop your own plugin" 95 | 5. Enter in localhost:3000 since this is the URL the server is running on locally, then select "Find manifest file". 96 | 97 | ## 🤖 Usage examples 98 | > Install youtube-dl and use it to download this video https://www.youtube.com/watch?v=jNQXAC9IVRw 99 | 100 | > Start HTTP server on port 3000 101 | 102 | > Clone this repo "https://github.com/e2b-dev/chatgpt-plugin", fix any typos in readme push it 103 | 104 | ## 📂 How to upload & download files 105 | The official ChatGPT Code Interpreter supports uploading and downloading files. While the e2b code interpreter doesn't support this functionality natively (yet), you can "hack" around it just by using the `curl` or `wget` command and a service such as the S3 bucket. 106 | 107 | ### Uploading your files to plugin 108 | 1. Get S3 bucket (or any alternative) 109 | 2. Upload your files there and make them public 110 | 3. Tell ChatGPT to download that files using curl 111 | 112 | ### Downloading files from plugin 113 | 1. Tell ChaGPT to upload its files to S3 bucket using curl 114 | 115 | ## What is e2b? 116 | [E2B](https://www.e2b.dev/) is the company behind this plugin. We're building an operating system for AI agents. A set of low-level APIs for building agents (debugging, auth, monitor, and more) together with sandboxed cloud environments for the agents where the agents can roam freely without barriers 🐎. 117 | 118 | 119 | ## Development 120 | Install dependencies: 121 | ```bash 122 | npm install 123 | ``` 124 | 125 | Then start reloading server by running: 126 | ```bash 127 | npm run dev 128 | ``` 129 | 130 | ### API routes 131 | We are using [tsoa](https://github.com/lukeautry/tsoa) to generate [OpenAPI spec](./openapi.yaml) and to generate server route boilerplate. It uses TypeScript decorators to describe the API. 132 | 133 | Edit the Controllers in [`src/plugin`](./src/plugin/) to modify the API exposed to the plugin. 134 | 135 | ### Documentation 136 | The documentation of API in the OpenAPI spec is generated from the JSDoc comments in the Controllers. See [tsoa docs](https://tsoa-community.github.io/docs/descriptions.html) for more info. 137 | 138 | The info section inside of OpenAPI spec is injected in the [script that reformats the generated spec](./scripts/formatSpec.js) so if you want to change it, you need to change it there not by changing the `openapi.yaml` file directly. 139 | 140 | ### Manifest 141 | You may also want to modify the [ChatGPT plugin manifest](./.well-known/ai-plugin.json) to change metadata about the plugin. 142 | -------------------------------------------------------------------------------- /src/generated/routes.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 4 | import { Controller, ValidationService, FieldErrors, ValidateError, TsoaRoute, HttpStatusCodeLiteral, TsoaResponse, fetchMiddlewares } from '@tsoa/runtime'; 5 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 6 | import { CommandController } from './../plugin/commandController'; 7 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 8 | import { FileController } from './../plugin/fileController'; 9 | import type { RequestHandler, Router } from 'express'; 10 | 11 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 12 | 13 | const models: TsoaRoute.Models = { 14 | "CommandResponse": { 15 | "dataType": "refObject", 16 | "properties": { 17 | "stderr": {"dataType":"string","required":true}, 18 | "stdout": {"dataType":"string","required":true}, 19 | }, 20 | "additionalProperties": false, 21 | }, 22 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 23 | "Environment": { 24 | "dataType": "refAlias", 25 | "type": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["Nodejs"]},{"dataType":"enum","enums":["Go"]},{"dataType":"enum","enums":["Bash"]},{"dataType":"enum","enums":["Rust"]},{"dataType":"enum","enums":["Python3"]},{"dataType":"enum","enums":["PHP"]},{"dataType":"enum","enums":["Java"]},{"dataType":"enum","enums":["Perl"]},{"dataType":"enum","enums":["DotNET"]}],"validators":{}}, 26 | }, 27 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 28 | }; 29 | const validationService = new ValidationService(models); 30 | 31 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 32 | 33 | export function RegisterRoutes(app: Router) { 34 | // ########################################################################################################### 35 | // NOTE: If you do not see routes for all of your controllers in this file, then you might not have informed tsoa of where to look 36 | // Please look into the "controllerPathGlobs" config option described in the readme: https://github.com/lukeautry/tsoa 37 | // ########################################################################################################### 38 | app.post('/commands', 39 | ...(fetchMiddlewares(CommandController)), 40 | ...(fetchMiddlewares(CommandController.prototype.runCommand)), 41 | 42 | function CommandController_runCommand(request: any, response: any, next: any) { 43 | const args = { 44 | command: {"in":"body-prop","name":"command","required":true,"dataType":"string"}, 45 | workDir: {"in":"body-prop","name":"workDir","required":true,"dataType":"string"}, 46 | env: {"default":"Nodejs","in":"query","name":"env","ref":"Environment"}, 47 | conversationID: {"in":"header","name":"openai-conversation-id","dataType":"string"}, 48 | }; 49 | 50 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 51 | 52 | let validatedArgs: any[] = []; 53 | try { 54 | validatedArgs = getValidatedArgs(args, request, response); 55 | 56 | const controller = new CommandController(); 57 | 58 | 59 | const promise = controller.runCommand.apply(controller, validatedArgs as any); 60 | promiseHandler(controller, promise, response, undefined, next); 61 | } catch (err) { 62 | return next(err); 63 | } 64 | }); 65 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 66 | app.get('/files', 67 | ...(fetchMiddlewares(FileController)), 68 | ...(fetchMiddlewares(FileController.prototype.readFile)), 69 | 70 | function FileController_readFile(request: any, response: any, next: any) { 71 | const args = { 72 | env: {"default":"Nodejs","in":"query","name":"env","ref":"Environment"}, 73 | path: {"in":"query","name":"path","required":true,"dataType":"string"}, 74 | notFoundResponse: {"in":"res","name":"404","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"reason":{"dataType":"string","required":true}}}, 75 | conversationID: {"in":"header","name":"openai-conversation-id","dataType":"string"}, 76 | }; 77 | 78 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 79 | 80 | let validatedArgs: any[] = []; 81 | try { 82 | validatedArgs = getValidatedArgs(args, request, response); 83 | 84 | const controller = new FileController(); 85 | 86 | 87 | const promise = controller.readFile.apply(controller, validatedArgs as any); 88 | promiseHandler(controller, promise, response, undefined, next); 89 | } catch (err) { 90 | return next(err); 91 | } 92 | }); 93 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 94 | app.put('/files', 95 | ...(fetchMiddlewares(FileController)), 96 | ...(fetchMiddlewares(FileController.prototype.writeFile)), 97 | 98 | function FileController_writeFile(request: any, response: any, next: any) { 99 | const args = { 100 | env: {"default":"Nodejs","in":"query","name":"env","ref":"Environment"}, 101 | path: {"in":"query","name":"path","required":true,"dataType":"string"}, 102 | content: {"in":"body-prop","name":"content","required":true,"dataType":"string"}, 103 | conversationID: {"in":"header","name":"openai-conversation-id","dataType":"string"}, 104 | }; 105 | 106 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 107 | 108 | let validatedArgs: any[] = []; 109 | try { 110 | validatedArgs = getValidatedArgs(args, request, response); 111 | 112 | const controller = new FileController(); 113 | 114 | 115 | const promise = controller.writeFile.apply(controller, validatedArgs as any); 116 | promiseHandler(controller, promise, response, undefined, next); 117 | } catch (err) { 118 | return next(err); 119 | } 120 | }); 121 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 122 | 123 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 124 | 125 | 126 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 127 | 128 | function isController(object: any): object is Controller { 129 | return 'getHeaders' in object && 'getStatus' in object && 'setStatus' in object; 130 | } 131 | 132 | function promiseHandler(controllerObj: any, promise: any, response: any, successStatus: any, next: any) { 133 | return Promise.resolve(promise) 134 | .then((data: any) => { 135 | let statusCode = successStatus; 136 | let headers; 137 | if (isController(controllerObj)) { 138 | headers = controllerObj.getHeaders(); 139 | statusCode = controllerObj.getStatus() || statusCode; 140 | } 141 | 142 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 143 | 144 | returnHandler(response, statusCode, data, headers) 145 | }) 146 | .catch((error: any) => next(error)); 147 | } 148 | 149 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 150 | 151 | function returnHandler(response: any, statusCode?: number, data?: any, headers: any = {}) { 152 | if (response.headersSent) { 153 | return; 154 | } 155 | Object.keys(headers).forEach((name: string) => { 156 | response.set(name, headers[name]); 157 | }); 158 | if (data && typeof data.pipe === 'function' && data.readable && typeof data._read === 'function') { 159 | response.status(statusCode || 200) 160 | data.pipe(response); 161 | } else if (data !== null && data !== undefined) { 162 | response.status(statusCode || 200).json(data); 163 | } else { 164 | response.status(statusCode || 204).end(); 165 | } 166 | } 167 | 168 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 169 | 170 | function responder(response: any): TsoaResponse { 171 | return function(status, data, headers) { 172 | returnHandler(response, status, data, headers); 173 | }; 174 | }; 175 | 176 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 177 | 178 | function getValidatedArgs(args: any, request: any, response: any): any[] { 179 | const fieldErrors: FieldErrors = {}; 180 | const values = Object.keys(args).map((key) => { 181 | const name = args[key].name; 182 | switch (args[key].in) { 183 | case 'request': 184 | return request; 185 | case 'query': 186 | return validationService.ValidateParam(args[key], request.query[name], name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); 187 | case 'queries': 188 | return validationService.ValidateParam(args[key], request.query, name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); 189 | case 'path': 190 | return validationService.ValidateParam(args[key], request.params[name], name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); 191 | case 'header': 192 | return validationService.ValidateParam(args[key], request.header(name), name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); 193 | case 'body': 194 | return validationService.ValidateParam(args[key], request.body, name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); 195 | case 'body-prop': 196 | return validationService.ValidateParam(args[key], request.body[name], name, fieldErrors, 'body.', {"noImplicitAdditionalProperties":"throw-on-extras"}); 197 | case 'formData': 198 | if (args[key].dataType === 'file') { 199 | return validationService.ValidateParam(args[key], request.file, name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); 200 | } else if (args[key].dataType === 'array' && args[key].array.dataType === 'file') { 201 | return validationService.ValidateParam(args[key], request.files, name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); 202 | } else { 203 | return validationService.ValidateParam(args[key], request.body[name], name, fieldErrors, undefined, {"noImplicitAdditionalProperties":"throw-on-extras"}); 204 | } 205 | case 'res': 206 | return responder(response); 207 | } 208 | }); 209 | 210 | if (Object.keys(fieldErrors).length > 0) { 211 | throw new ValidateError(fieldErrors, ''); 212 | } 213 | return values; 214 | } 215 | 216 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 217 | } 218 | 219 | // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa 220 | --------------------------------------------------------------------------------