diff --git a/.cspell.json b/.cspell.json index 213394b..b201567 100644 --- a/.cspell.json +++ b/.cspell.json @@ -4,7 +4,25 @@ "ignorePaths": ["**/*.json", "**/*.css", "node_modules", "**/*.log", "./src/adapters/supabase/**/**.ts"], "useGitignore": true, "language": "en", - "words": ["Nektos", "dataurl", "devpool", "outdir", "servedir", "Supabase", "SUPABASE", "typebox", "ubiquibot", "Smee"], + "words": [ + "mswjs", + "Nektos", + "dataurl", + "devpool", + "outdir", + "servedir", + "Supabase", + "SUPABASE", + "typebox", + "ubiquibot", + "Smee", + "sonarjs", + "knip", + "mischeck", + "convo", + "ubqbot", + "behaviour" + ], "dictionaries": ["typescript", "node", "software-terms"], "import": ["@cspell/dict-typescript/cspell-ext.json", "@cspell/dict-node/cspell-ext.json", "@cspell/dict-software-terms"], "ignoreRegExpList": ["[0-9a-fA-F]{6}"] diff --git a/.dev.vars.example b/.dev.vars.example index e49d79a..b9e5cff 100644 --- a/.dev.vars.example +++ b/.dev.vars.example @@ -1 +1 @@ -MY_SECRET="MY_SECRET" +OPENAI_API_KEY="MY_SECRET" diff --git a/.env.example b/.env.example deleted file mode 100644 index e49d79a..0000000 --- a/.env.example +++ /dev/null @@ -1 +0,0 @@ -MY_SECRET="MY_SECRET" diff --git a/.github/workflows/compute.yml b/.github/workflows/compute.yml deleted file mode 100644 index 3d204b1..0000000 --- a/.github/workflows/compute.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: "the name of the plugin" - -on: - workflow_dispatch: - inputs: - stateId: - description: "State Id" - eventName: - description: "Event Name" - eventPayload: - description: "Event Payload" - settings: - description: "Settings" - authToken: - description: "Auth Token" - ref: - description: "Ref" - -jobs: - compute: - name: "plugin name" - runs-on: ubuntu-latest - permissions: write-all - env: - SUPABASE_URL: ${{ secrets.SUPABASE_URL }} - SUPABASE_KEY: ${{ secrets.SUPABASE_KEY }} - - steps: - - uses: actions/checkout@v4 - - - name: setup node - uses: actions/setup-node@v4 - with: - node-version: "20.10.0" - - - name: install dependencies - run: yarn - - - name: execute directive - run: npx tsx ./src/main.ts - id: plugin-name - env: - SUPABASE_URL: ${{ secrets.SUPABASE_URL }} - SUPABASE_KEY: ${{ secrets.SUPABASE_KEY }} diff --git a/.github/workflows/worker-delete.yml b/.github/workflows/worker-delete.yml new file mode 100644 index 0000000..f715a20 --- /dev/null +++ b/.github/workflows/worker-delete.yml @@ -0,0 +1,44 @@ +name: Delete Deployment + +on: + delete: + +jobs: + delete: + runs-on: ubuntu-latest + name: Delete Deployment + steps: + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "20.10.0" + + - name: Enable corepack + run: corepack enable + + - uses: actions/checkout@v4 + + - name: Get Deleted Branch Name + id: get_branch + run: | + branch_name=$(echo '${{ github.event.ref }}' | sed 's#refs/heads/##' | sed 's#[^a-zA-Z0-9]#-#g') + echo "branch_name=$branch_name" >> $GITHUB_ENV + - name: Retrieve and Construct Full Worker Name + id: construct_worker_name + run: | + base_name=$(grep '^name = ' wrangler.toml | head -n 1 | sed 's/^name = "\(.*\)"$/\1/') + full_worker_name="${base_name}-${{ env.branch_name }}" + # Make sure that it doesnt exceed 63 characters or it will break RFC 1035 + full_worker_name=$(echo "${full_worker_name}" | cut -c 1-63) + echo "full_worker_name=$full_worker_name" >> $GITHUB_ENV + - name: Delete Deployment with Wrangler + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + command: delete --name ${{ env.full_worker_name }} + + - name: Output Deletion Result + run: | + echo "### Deployment URL" >> $GITHUB_STEP_SUMMARY + echo 'Deployment `${{ env.full_worker_name }}` has been deleted.' >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/worker-deploy.yml b/.github/workflows/worker-deploy.yml new file mode 100644 index 0000000..cba478c --- /dev/null +++ b/.github/workflows/worker-deploy.yml @@ -0,0 +1,48 @@ +name: Deploy Worker + +on: + push: + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + name: Deploy + steps: + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: "20.10.0" + + - name: Enable corepack + run: corepack enable + + - uses: actions/checkout@v4 + + - name: Update wrangler.toml Name Field + run: | + branch_name=$(echo '${{ github.event.ref }}' | sed 's#refs/heads/##' | sed 's#[^a-zA-Z0-9]#-#g') + # Extract base name from wrangler.toml + base_name=$(grep '^name = ' wrangler.toml | head -n 1 | sed 's/^name = "\(.*\)"$/\1/') + # Concatenate branch name with base name + new_name="${base_name}-${branch_name}" + # Truncate the new name to 63 characters for RFC 1035 + new_name=$(echo "$new_name" | cut -c 1-63) + # Update the wrangler.toml file + sed -i '0,/^name = .*/{s/^name = .*/name = "'"$new_name"'"/}' wrangler.toml + echo "Updated wrangler.toml name to: $new_name" + - name: Deploy with Wrangler + id: wrangler_deploy + uses: cloudflare/wrangler-action@v3 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + secrets: | + OPENAI_API_KEY + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + + - name: Write Deployment URL to Summary + run: | + echo "### Deployment URL" >> $GITHUB_STEP_SUMMARY + echo "${{ steps.wrangler_deploy.outputs.deployment-url }}" >> $GITHUB_STEP_SUMMARY diff --git a/README.md b/README.md index 4dcb9f1..e74704e 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,41 @@ -# `@ubiquibot/plugin-template` +# `@ubiquity-os/command-ask` -## Prerequisites +This is a highly context aware GitHub organization integrated bot that uses the OpenAI GPT-4o model to provide highly relevant answers to questions and queries in GitHub issues and pull requests. -- A good understanding of how the [kernel](https://github.com/ubiquity/ubiquibot-kernel) works and how to interact with it. -- A basic understanding of the Ubiquibot configuration and how to define your plugin's settings. +## Usage -## Getting Started +In any repository where your Ubiquity OS app is installed, both issues and pull requests alike, you simply mention `@UbiquityOS` with your question or query and using the latest OpenAi GPT-4o model, the bot will provide you with a highly relevant answer. -1. Create a new repository using this template. -2. Clone the repository to your local machine. -3. Install the dependencies preferably using `yarn` or `bun`. +## How it works -## Creating a new plugin +With it's huge context window, we are able to feed the entire conversational history to the model which we obtain by recursively fetching any referenced issues or pull requests from the chat history. This allows the model to have a very deep understanding of the current scope and provide highly relevant answers. -- If your plugin is to be used as a slash command which should have faster response times as opposed to longer running GitHub action tasks, you should use the `worker` type. +As it receives everything from discussions to pull request diffs and review comments, it is a highly versatile and capable bot that can assist in a wide range of scenarios. -1. Ensure you understand and have setup the [kernel](https://github.com/ubiquity/ubiquibot-kernel). -2. Update [compute.yml](./.github/workflows/compute.yml) with your plugin's name and update the `id`. -3. Update [context.ts](./src/types/context.ts) with the events that your plugin will fire on. -4. Update [plugin-inputs.ts](./src/types/plugin-inputs.ts) to match the `with:` settings in your org or repo level configuration. +## Installation -- Your plugin config should look similar to this: +`ubiquibot-config.yml`: ```yml -- plugin: /:compute.yml@development - name: plugin-name - id: plugin-name-command - description: "Plugin description" # small description of what the plugin does - command: "" # if you are creating a plugin with a slash command - example: "" # how to invoke the slash command - with: # these are the example settings, the kernel passes these to the plugin. - disabledCommands: [] - timers: - reviewDelayTolerance: 86000 - taskStaleTimeoutDuration: 2580000 - miscellaneous: - maxConcurrentTasks: 3 - labels: - time: [] - priority: [] +plugins: + - uses: + - plugin: http://localhost:4000 + with: + model: "" + openAiBaseUrl: "" ``` -###### At this stage, your plugin will fire on your defined events with the required settings passed in from the kernel. You can now start writing your plugin's logic. +`.dev.vars` (for local testing): -5. Start building your plugin by adding your logic to the [plugin.ts](./src/plugin.ts) file. +```sh +# OpenAI API key +OPENAI_API_KEY=your-api-key +UBIQUITY_OS_APP_SLUG="UbiquityOS" -## Testing a plugin - -### Worker Plugins - -- `yarn/bun worker` - to run the worker locally. -- To trigger the worker, `POST` requests to http://localhost:4000/ with an event payload similar to: - -```ts -await fetch("http://localhost:4000/", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - stateId: "", - eventName: "", - eventPayload: "", - settings: "", - ref: "", - authToken: "", - }), -}); ``` -A full example can be found [here](https://github.com/ubiquibot/assistive-pricing/blob/623ea3f950f04842f2d003bda3fc7b7684e41378/tests/http/request.http). - -### Action Plugins - -- Ensure the kernel is running and listening for events. -- Fire an event in/to the repo where the kernel is installed. This can be done in a number of ways, the easiest being via the GitHub UI or using the GitHub API, such as posting a comment, opening an issue, etc in the org/repo where the kernel is installed. -- The kernel will process the event and dispatch it using the settings defined in your `.ubiquibot-config.yml`. -- The `compute.yml` workflow will run and execute your plugin's logic. -- You can view the logs in the Actions tab of your repo. +## Testing -[Nektos Act](https://github.com/nektos/act) - a tool for running GitHub Actions locally. - -## More information - -- [Full Ubiquibot Configuration](https://github.com/ubiquity/ubiquibot/blob/0fde7551585499b1e0618ec8ea5e826f11271c9c/src/types/configuration-types.ts#L62) - helpful for defining your plugin's settings as they are strongly typed and will be validated by the kernel. -- [Ubiquibot V1](https://github.com/ubiquity/ubiquibot) - helpful for porting V1 functionality to V2, helper/utility functions, types, etc. Everything is based on the V1 codebase but with a more modular approach. When using V1 code, keep in mind that most all code will need refactored to work with the new V2 architecture. - -## Examples - -- [Start/Stop Slash Command](https://github.com/ubq-testing/start-stop-module) - simple -- [Assistive Pricing Plugin](https://github.com/ubiquibot/assistive-pricing) - complex -- [Conversation Rewards](https://github.com/ubiquibot/conversation-rewards) - really complex +```sh +yarn test +``` diff --git a/eslint.config.mjs b/eslint.config.mjs index e53d263..c714515 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -9,7 +9,7 @@ export default tsEslint.config({ "@typescript-eslint": tsEslint.plugin, "check-file": checkFile, }, - ignores: [".github/knip.ts"], + ignores: [".github/knip.ts", ".wrangler/**/*.ts", ".wrangler/**/*.js"], extends: [eslint.configs.recommended, ...tsEslint.configs.recommended, sonarjs.configs.recommended], languageOptions: { parser: tsEslint.parser, diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..5d6ce58 --- /dev/null +++ b/manifest.json @@ -0,0 +1,5 @@ +{ + "name": "command-ask", + "description": "A highly context aware organization integrated chatbot", + "ubiquity:listeners": ["issue_comment.created"] +} diff --git a/package.json b/package.json index cbe5fb0..dd8fb89 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { - "name": "plugin-template", + "name": "@ubiquity-os/command-ask", "version": "1.0.0", - "description": "Ubiquibot plugin template repository with TypeScript support.", - "author": "Ubiquity DAO", + "description": "A highly context aware organization integrated chatbot", + "author": "Ubiquity OS", "license": "MIT", "main": "src/worker.ts", "engines": { @@ -27,13 +27,12 @@ "open-source" ], "dependencies": { - "@actions/core": "1.10.1", - "@actions/github": "6.0.0", "@octokit/rest": "20.1.1", "@octokit/webhooks": "13.2.7", "@sinclair/typebox": "0.32.33", - "@supabase/supabase-js": "2.43.5", - "dotenv": "16.4.5", + "@ubiquity-dao/ubiquibot-logger": "^1.3.0", + "dotenv": "^16.4.5", + "openai": "^4.63.0", "typebox-validators": "0.3.5" }, "devDependencies": { @@ -45,6 +44,7 @@ "@eslint/js": "9.5.0", "@jest/globals": "29.7.0", "@mswjs/data": "0.16.1", + "@types/jest": "^29.5.12", "@types/node": "20.14.5", "cspell": "8.9.0", "eslint": "9.5.0", @@ -61,7 +61,6 @@ "npm-run-all": "4.1.5", "prettier": "3.3.2", "ts-jest": "29.1.5", - "tsx": "4.15.6", "typescript": "5.4.5", "typescript-eslint": "7.13.1", "wrangler": "3.60.3" @@ -80,4 +79,4 @@ "@commitlint/config-conventional" ] } -} +} \ No newline at end of file diff --git a/src/adapters/index.ts b/src/adapters/index.ts deleted file mode 100644 index 23fb4b3..0000000 --- a/src/adapters/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { SupabaseClient } from "@supabase/supabase-js"; -import { Context } from "../types/context"; -import { Access } from "./supabase/helpers/access"; -import { User } from "./supabase/helpers/user"; -import { Label } from "./supabase/helpers/label"; -import { Super } from "./supabase/helpers/supabase"; - -export function createAdapters(supabaseClient: SupabaseClient, context: Context) { - return { - supabase: { - access: new Access(supabaseClient, context), - user: new User(supabaseClient, context), - label: new Label(supabaseClient, context), - super: new Super(supabaseClient, context), - }, - }; -} diff --git a/src/adapters/supabase/helpers/access.ts b/src/adapters/supabase/helpers/access.ts deleted file mode 100644 index dc32281..0000000 --- a/src/adapters/supabase/helpers/access.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { SupabaseClient } from "@supabase/supabase-js"; -import { Super } from "./supabase"; -import { Context } from "../../../types/context"; - -export class Access extends Super { - constructor(supabase: SupabaseClient, context: Context) { - super(supabase, context); - } - - public async getAccess(userId: number, repositoryId: number) { - const { data, error } = await this.supabase - .from("access") - .select("*") - .filter("user_id", "eq", userId) - .filter("repository_id", "eq", repositoryId) - .limit(1) - .maybeSingle(); - - if (error) { - this.context.logger.fatal(error.message, error); - throw new Error(error.message); - } - return data; - } - - public async setAccess(userId: number, repositoryId: number, labels: string[]) { - if (!labels.length) { - return this.clearAccess(userId, repositoryId); - } - const { data, error } = await this.supabase - .from("access") - .upsert({ - user_id: userId, - repository_id: repositoryId, - labels: labels, - }) - .select() - .maybeSingle(); - - if (error) throw new Error(error.message); - return data; - } - - public async clearAccess(userId: number, repositoryId: number): Promise { - const { data, error } = await this.supabase.from("access").delete().filter("user_id", "eq", userId).filter("repository_id", "eq", repositoryId); - if (error) throw new Error(error.message); - return data; - } -} diff --git a/src/adapters/supabase/helpers/label.ts b/src/adapters/supabase/helpers/label.ts deleted file mode 100644 index 77e0288..0000000 --- a/src/adapters/supabase/helpers/label.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { SupabaseClient } from "@supabase/supabase-js"; -import { Super } from "./supabase"; -import { Context } from "../../../types/context"; - -export class Label extends Super { - constructor(supabase: SupabaseClient, context: Context) { - super(supabase, context); - } - - async saveLabelChange({ - previousLabel, - currentLabel, - authorized, - userId, - repositoryId, - }: { - previousLabel: string; - currentLabel: string; - authorized: boolean; - userId: number; - repositoryId: number; - }) { - const { data, error } = await this.supabase - .from("labels") - .insert({ - label_from: previousLabel, - label_to: currentLabel, - authorized: authorized, - user_id: userId, - repository_id: repositoryId, - }) - .select() - .single(); - - if (error) throw new Error(error.message); - return data; - } - - async getLabelChanges(repositoryNodeId: string) { - const { data, error } = await this.supabase.from("labels").select("*").eq("repository_id", repositoryNodeId).eq("authorized", false); - - if (error) throw new Error(error.message); - return data; - } - - async approveLabelChange(id: number): Promise { - const { data, error } = await this.supabase.from("labels").update({ authorized: true }).eq("id", id); - - if (error) throw new Error(error.message); - return data; - } -} diff --git a/src/adapters/supabase/helpers/supabase.ts b/src/adapters/supabase/helpers/supabase.ts deleted file mode 100644 index 7a13b85..0000000 --- a/src/adapters/supabase/helpers/supabase.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { SupabaseClient } from "@supabase/supabase-js"; -import { Context } from "../../../types/context"; - -export class Super { - protected supabase: SupabaseClient; - protected context: Context; - - constructor(supabase: SupabaseClient, context: Context) { - this.supabase = supabase; - this.context = context; - } -} diff --git a/src/adapters/supabase/helpers/user.ts b/src/adapters/supabase/helpers/user.ts deleted file mode 100644 index fa8b687..0000000 --- a/src/adapters/supabase/helpers/user.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { SupabaseClient } from "@supabase/supabase-js"; -import { Super } from "./supabase"; -import { Context } from "../../../types/context"; - -type Wallet = { - address: string; -}; - -export class User extends Super { - user_id: string | undefined; - comment_id: string | undefined; - issue_id: string | undefined; - repository_id: string | undefined; - node_id: string | undefined; - node_type: string | undefined; - - constructor(supabase: SupabaseClient, context: Context) { - super(supabase, context); - } - - async getUserById(userId: number, issueNumber: number) { - const { data, error } = await this.supabase.from("users").select("*").eq("id", userId).single(); - if (error) { - console.error(FAILED_TO_GET_USER, { userId, error, issueNumber }); - return null; - } - - console.info(SUCCESSFULLY_FETCHED_USER, { userId, issueNumber, ...data }); - return data; - } - - async getWalletByUserId(userId: number, issueNumber: number) { - const { data, error }: { data: { wallets: Wallet } | null; error: unknown } = await this.supabase - .from("users") - .select("wallets(*)") - .eq("id", userId) - .single(); - if ((error && !data) || !data?.wallets?.address) { - console.error("No wallet address found", { userId, issueNumber }, true); - throw new Error("No wallet address found"); - } - - console.info("Successfully fetched wallet", { userId, address: data.wallets?.address }); - return data.wallets?.address; - } - - public async getMultiplier(userId: number, repositoryId: number) { - const locationData = await this.getLocationsFromRepo(repositoryId); - if (locationData && locationData.length > 0) { - const accessData = await this._getAccessData(locationData, userId); - if (accessData) { - return { - value: accessData.multiplier || null, - reason: accessData.multiplier_reason || null, - }; - } - } - return null; - } - - private async _getAccessData(locationData: { id: number }[], userId: number) { - const locationIdsInCurrentRepository = locationData.map((location) => location.id); - - const { data: accessData, error: accessError } = await this.supabase - .from("access") - .select("multiplier, multiplier_reason") - .in("location_id", locationIdsInCurrentRepository) - .eq("user_id", userId) - .order("id", { ascending: false }) // get the latest one - .maybeSingle(); - if (accessError) throw console.error("Error getting access data", accessError); - return accessData; - } - - public async getLocationsFromRepo(repositoryId: number) { - const { data: locationData, error } = await this.supabase.from("locations").select("id").eq("repository_id", repositoryId); - - if (error) throw console.error("Error getting location data", new Error(error.message)); - return locationData; - } -} - -const FAILED_TO_GET_USER = "Failed to get user"; -const SUCCESSFULLY_FETCHED_USER = "Successfully fetched user"; diff --git a/src/handlers/add-comment.ts b/src/handlers/add-comment.ts new file mode 100644 index 0000000..56068f9 --- /dev/null +++ b/src/handlers/add-comment.ts @@ -0,0 +1,17 @@ +import { Context } from "../types/context"; + +export async function addCommentToIssue(context: Context, message: string) { + const { payload } = context; + const issueNumber = payload.issue.number; + + try { + await context.octokit.issues.createComment({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + issue_number: issueNumber, + body: message, + }); + } catch (e: unknown) { + context.logger.error("Adding a comment failed!", { e }); + } +} diff --git a/src/handlers/ask-gpt.ts b/src/handlers/ask-gpt.ts new file mode 100644 index 0000000..fbf9209 --- /dev/null +++ b/src/handlers/ask-gpt.ts @@ -0,0 +1,47 @@ +import OpenAI from "openai"; +import { Context } from "../types"; +import { createChatHistory, formatChatHistory } from "../helpers/format-chat-history"; +import { recursivelyFetchLinkedIssues } from "../helpers/issue-fetching"; + +export async function askQuestion(context: Context, question: string) { + if (!question) { + throw context.logger.error(`No question provided`); + } + + const { specAndBodies, streamlinedComments } = await recursivelyFetchLinkedIssues({ context }); + + const formattedChat = await formatChatHistory(context, streamlinedComments, specAndBodies); + return await askGpt(context, formattedChat); +} + +export async function askGpt(context: Context, formattedChat: string) { + const { + logger, + env: { OPENAI_API_KEY }, + config: { model, openAiBaseUrl }, + } = context; + + const openAi = new OpenAI({ + apiKey: OPENAI_API_KEY, + ...(openAiBaseUrl && { baseURL: openAiBaseUrl }), + }); + + const chat = createChatHistory(formattedChat); + + logger.info(`Sending chat to OpenAI`, { chat }); + + const res: OpenAI.Chat.Completions.ChatCompletion = await openAi.chat.completions.create({ + messages: createChatHistory(formattedChat), + model: model ?? "o1-mini", + }); + + const answer = res.choices[0].message.content; + + const tokenUsage = { + output: res.usage?.completion_tokens, + input: res.usage?.prompt_tokens, + total: res.usage?.total_tokens, + }; + + return { answer, tokenUsage }; +} diff --git a/src/handlers/comments.ts b/src/handlers/comments.ts new file mode 100644 index 0000000..392f8da --- /dev/null +++ b/src/handlers/comments.ts @@ -0,0 +1,98 @@ +import { splitKey } from "../helpers/issue"; +import { IssueComments, LinkedIssues, ReviewComments } from "../types/github"; +import { StreamlinedComment } from "../types/gpt"; + +export async function getAllStreamlinedComments(linkedIssues: LinkedIssues[]) { + const streamlinedComments: Record = {}; + + for (const issue of linkedIssues) { + const linkedIssueComments = issue.comments; + if (!linkedIssueComments) continue; + + if (linkedIssueComments.length > 0) { + const linkedStreamlinedComments = streamlineComments(linkedIssueComments); + + if (linkedStreamlinedComments) { + for (const [key, value] of Object.entries(linkedStreamlinedComments)) { + if (!streamlinedComments[key]) { + streamlinedComments[key] = value; + continue; + } + + const previous = streamlinedComments[key] || []; + streamlinedComments[key] = [...previous, ...value]; + } + } + } + } + + return streamlinedComments; +} + +export function createKey(issueUrl: string, issue?: number) { + const urlParts = issueUrl.split("/"); + + let key = ""; + + if (urlParts.length === 7) { + const [, , , issueOrg, issueRepo, , issueNumber] = urlParts; + key = `${issueOrg}/${issueRepo}/${issueNumber}`; + } + + if (urlParts.length === 5) { + const [, , issueOrg, issueRepo] = urlParts; + key = `${issueOrg}/${issueRepo}/${issue}`; + } + + if (urlParts.length === 8) { + const [, , , issueOrg, issueRepo, , , issueNumber] = urlParts; + key = `${issueOrg}/${issueRepo}/${issueNumber || issue}`; + } + + if (urlParts.length === 3) { + const [issueOrg, issueRepo, issueNumber] = urlParts; + key = `${issueOrg}/${issueRepo}/${issueNumber || issue}`; + } + + if (!key) { + throw new Error("Invalid issue url"); + } + + if (key.includes("#")) { + key = key.split("#")[0]; + } + + return key; +} + +export function streamlineComments(comments: IssueComments | ReviewComments) { + const streamlined: Record = {}; + + for (const comment of comments) { + const user = comment.user; + if (user && user.type === "Bot") { + continue; + } + + const url = comment.html_url; + const body = comment.body; + const key = createKey(url); + const [owner, repo] = splitKey(key); + + if (!streamlined[key]) { + streamlined[key] = []; + } + + if (user && body) { + streamlined[key].push({ + user: user.login, + body, + id: comment.id, + org: owner, + repo, + issueUrl: url, + }); + } + } + return streamlined; +} diff --git a/src/helpers/format-chat-history.ts b/src/helpers/format-chat-history.ts new file mode 100644 index 0000000..5d369fe --- /dev/null +++ b/src/helpers/format-chat-history.ts @@ -0,0 +1,161 @@ +import { ChatCompletionMessageParam } from "openai/resources"; +import { Context } from "../types"; +import { StreamlinedComment, StreamlinedComments } from "../types/gpt"; +import { createKey, streamlineComments } from "../handlers/comments"; +import { fetchPullRequestDiff, fetchIssue, fetchIssueComments } from "./issue-fetching"; +import { splitKey } from "./issue"; + +export async function formatChatHistory(context: Context, streamlined: Record, specAndBodies: Record) { + const convoKeys = Object.keys(streamlined); + const specAndBodyKeys = Object.keys(specAndBodies); + const chatHistory: string[] = []; + const currentIssueKey = createKey(context.payload.issue.html_url); + const keys: string[] = Array.from(new Set([...convoKeys, ...specAndBodyKeys, currentIssueKey])); + + for (const key of keys) { + const isCurrentIssue = key === currentIssueKey; + const block = await createContextBlockSection(context, key, streamlined, specAndBodies, isCurrentIssue); + chatHistory.push(block); + } + + return Array.from(new Set(chatHistory)).join(""); +} + +function getCorrectHeaderString(isPull: string | null, issueNumber: number, isCurrentIssue: boolean, isBody: boolean) { + const strings = { + pull: { + linked: `Linked Pull #${issueNumber} Request`, + current: `Current Pull #${issueNumber} Request`, + }, + issue: { + linked: `Linked Issue #${issueNumber} Specification`, + current: `Current Issue #${issueNumber} Specification`, + }, + convo: { + linked: `Linked Issue #${issueNumber} Conversation`, + current: `Current Issue #${issueNumber} Conversation`, + }, + }; + + let header = ""; + + if (isPull) { + header = isCurrentIssue ? strings.pull.current : strings.pull.linked; + } else { + header = isCurrentIssue ? strings.issue.current : strings.issue.linked; + } + + if (isBody) { + header = isCurrentIssue ? strings.convo.current : strings.convo.linked; + } + + return header; +} + +async function createContextBlockSection( + context: Context, + key: string, + streamlined: Record, + specAndBodies: Record, + isCurrentIssue: boolean +) { + let comments = streamlined[key]; + + if (!comments || comments.length === 0) { + const [owner, repo, number] = splitKey(key); + const { comments: comments_ } = await fetchIssueComments({ + context, + owner, + repo, + issueNum: parseInt(number), + }); + + comments = streamlineComments(comments_)[key]; + } + + const [org, repo, issueNum] = key.split("/"); + + const issueNumber = parseInt(issueNum); + const prDiff = await fetchPullRequestDiff(context, org, repo, issueNumber); + + if (!issueNumber || isNaN(issueNumber)) { + throw context.logger.error("Issue number is not valid"); + } + + const specHeader = getCorrectHeaderString(prDiff, issueNumber, isCurrentIssue, false); + + let specOrBody = specAndBodies[key]; + if (!specOrBody) { + specOrBody = + ( + await fetchIssue({ + context, + owner: org, + repo, + issueNum: issueNumber, + }) + )?.body || "No specification or body available"; + } + const specOrBodyBlock = [createHeader(specHeader, key), createSpecOrBody(specOrBody), createFooter(specHeader)]; + + const header = getCorrectHeaderString(prDiff, issueNumber, isCurrentIssue, true); + const repoString = `${org}/${repo} #${issueNumber}`; + + const block = [specOrBodyBlock.join(""), createHeader(header, repoString), createComment({ issueNumber, repo, org, comments }), createFooter(header)]; + + if (!prDiff) { + return block.join(""); + } + + const diffBlock = [createHeader("Linked Pull Request Code Diff", repoString), prDiff, createFooter("Linked Pull Request Code Diff")]; + + return block.concat(diffBlock).join(""); +} + +function createHeader(content: string, repoString: string) { + return `=== ${content} === ${repoString} ===\n\n`; +} + +function createFooter(content: string) { + return `=== End ${content} ===\n\n`; +} + +function createComment(comment: StreamlinedComments) { + if (!comment.comments) { + return ""; + } + const comments = []; + + // filter dupes + comment.comments = comment.comments?.filter((c, i, a) => a.findIndex((cc) => cc.id === c.id) === i); + + for (const c of comment.comments) { + comments.push(`${c.id} ${c.user}: ${c.body}\n`); + } + return comments.join(""); +} + +function createSpecOrBody(specOrBody: string) { + return `${specOrBody}\n`; +} + +export function createChatHistory(formattedChat: string) { + const chatHistory: ChatCompletionMessageParam[] = []; + + const systemMessage: ChatCompletionMessageParam = { + role: "system", + content: `You are a GitHub integrated chatbot tasked with assisting in research and discussion on GitHub issues and pull requests. +Using the provided context, address the question being asked providing a clear and concise answer with no follow-up statements. +The LAST comment in 'Issue Conversation' is the most recent one, focus on it as that is the question being asked. +Use GitHub flavoured markdown in your response making effective use of lists, code blocks and other supported GitHub md features.`, + }; + + const userMessage: ChatCompletionMessageParam = { + role: "user", + content: formattedChat, + }; + + chatHistory.push(systemMessage, userMessage); + + return chatHistory; +} diff --git a/src/helpers/issue-fetching.ts b/src/helpers/issue-fetching.ts new file mode 100644 index 0000000..fc1653f --- /dev/null +++ b/src/helpers/issue-fetching.ts @@ -0,0 +1,176 @@ +import { createKey, getAllStreamlinedComments } from "../handlers/comments"; +import { Context } from "../types"; +import { FetchParams, Issue, IssueComments, LinkedIssues, ReviewComments } from "../types/github"; +import { StreamlinedComment } from "../types/gpt"; +import { dedupeStreamlinedComments, idIssueFromComment, mergeStreamlinedComments, splitKey } from "./issue"; +import { handleIssue, handleSpec, handleSpecAndBodyKeys, throttlePromises } from "./issue-handling"; + +export async function recursivelyFetchLinkedIssues(params: FetchParams) { + const { linkedIssues, seen, specAndBodies, streamlinedComments } = await fetchLinkedIssues(params); + + const fetchPromises = linkedIssues.map(async (linkedIssue) => await mergeCommentsAndFetchSpec(params, linkedIssue, streamlinedComments, specAndBodies, seen)); + await throttlePromises(fetchPromises, 10); + + const linkedIssuesKeys = linkedIssues.map((issue) => createKey(`${issue.owner}/${issue.repo}/${issue.issueNumber}`)); + const specAndBodyKeys = Array.from(new Set([...Object.keys(specAndBodies), ...Object.keys(streamlinedComments), ...linkedIssuesKeys])); + + await handleSpecAndBodyKeys(specAndBodyKeys, params, dedupeStreamlinedComments(streamlinedComments), seen); + return { linkedIssues, specAndBodies, streamlinedComments }; +} + +export async function fetchLinkedIssues(params: FetchParams) { + const { comments, issue } = await fetchIssueComments(params); + if (!issue) { + return { streamlinedComments: {}, linkedIssues: [], specAndBodies: {}, seen: new Set() }; + } + const issueKey = createKey(issue.html_url); + const [owner, repo, issueNumber] = splitKey(issueKey); + const linkedIssues: LinkedIssues[] = [{ body: issue.body || "", comments, issueNumber: parseInt(issueNumber), owner, repo, url: issue.html_url }]; + const specAndBodies: Record = {}; + const seen = new Set(); + + // add the spec body as a comment + comments.push({ + body: issue.body || "", + // @ts-expect-error - github types undefined + user: issue.user, + id: issue.id, + html_url: issue.html_url, + }); + + for (const comment of comments) { + const foundIssues = idIssueFromComment(comment.body, params); + if (foundIssues) { + for (const linkedIssue of foundIssues) { + const linkedKey = createKey(linkedIssue.url, linkedIssue.issueNumber); + if (seen.has(linkedKey)) { + continue; + } + seen.add(linkedKey); + const { issueNumber, owner, repo } = linkedIssue; + + const { comments: fetchedComments, issue: fetchedIssue } = await fetchIssueComments({ + context: params.context, + issueNum: issueNumber, + owner, + repo, + }); + + specAndBodies[linkedKey] = fetchedIssue?.body || ""; + linkedIssue.body = fetchedIssue?.body || ""; + linkedIssue.comments = fetchedComments; + linkedIssues.push(linkedIssue); + } + } + } + + return { streamlinedComments: await getAllStreamlinedComments(linkedIssues), linkedIssues, specAndBodies, seen }; +} + +export async function mergeCommentsAndFetchSpec( + params: FetchParams, + linkedIssue: LinkedIssues, + streamlinedComments: Record, + specOrBodies: Record, + seen: Set +) { + if (linkedIssue.comments) { + const streamed = await getAllStreamlinedComments([linkedIssue]); + const merged = mergeStreamlinedComments(streamlinedComments, streamed); + streamlinedComments = { ...streamlinedComments, ...merged }; + } + + if (linkedIssue.body) { + await handleSpec(params, linkedIssue.body, specOrBodies, createKey(linkedIssue.url, linkedIssue.issueNumber), seen, streamlinedComments); + } +} + +export async function fetchPullRequestDiff(context: Context, org: string, repo: string, issue: number) { + const { octokit } = context; + + try { + const diff = await octokit.pulls.get({ + owner: org, + repo, + pull_number: issue, + mediaType: { + format: "diff", + }, + }); + return diff.data as unknown as string; + } catch (e) { + return null; + } +} + +export async function fetchIssue(params: FetchParams) { + const { octokit, payload, logger } = params.context; + const { issueNum, owner, repo } = params; + + try { + return await octokit.rest.issues + .get({ + owner: owner || payload.repository.owner.login, + repo: repo || payload.repository.name, + issue_number: issueNum || payload.issue.number, + }) + .then(({ data }) => data as Issue); + } catch (e) { + logger.error(`Error fetching issue `, { + e, + owner: owner || payload.repository.owner.login, + repo: repo || payload.repository.name, + issue_number: issueNum || payload.issue.number, + }); + return null; + } +} + +export async function fetchIssueComments(params: FetchParams) { + const { octokit, payload, logger } = params.context; + const { issueNum, owner, repo } = params; + + const issue = await fetchIssue(params); + + let comments: IssueComments | ReviewComments = []; + + try { + if (issue?.pull_request) { + comments = await octokit.paginate(octokit.pulls.listReviewComments, { + owner: owner || payload.repository.owner.login, + repo: repo || payload.repository.name, + pull_number: issueNum || payload.issue.number, + }); + } else { + comments = await octokit.paginate(octokit.issues.listComments, { + owner: owner || payload.repository.owner.login, + repo: repo || payload.repository.name, + issue_number: issueNum || payload.issue.number, + }); + } + } catch (e) { + logger.error(`Error fetching comments `, { + e, + owner: owner || payload.repository.owner.login, + repo: repo || payload.repository.name, + issue_number: issueNum || payload.issue.number, + }); + comments = []; + } + + return { + issue, + comments: comments.filter((comment) => comment.user?.type !== "Bot") as IssueComments | ReviewComments, + }; +} + +export async function fetchAndHandleIssue( + key: string, + params: FetchParams, + streamlinedComments: Record, + seen: Set +): Promise { + const [owner, repo, issueNumber] = splitKey(key); + await handleIssue({ ...params, owner, repo, issueNum: parseInt(issueNumber) }, streamlinedComments, seen); + return streamlinedComments[key] || []; +} diff --git a/src/helpers/issue-handling.ts b/src/helpers/issue-handling.ts new file mode 100644 index 0000000..f10d998 --- /dev/null +++ b/src/helpers/issue-handling.ts @@ -0,0 +1,106 @@ +import { createKey } from "../handlers/comments"; +import { FetchParams } from "../types/github"; +import { StreamlinedComment } from "../types/gpt"; +import { idIssueFromComment, mergeStreamlinedComments, splitKey } from "./issue"; +import { fetchLinkedIssues, fetchIssue, fetchAndHandleIssue, mergeCommentsAndFetchSpec } from "./issue-fetching"; + +export async function handleIssue(params: FetchParams, streamlinedComments: Record, alreadySeen: Set) { + if (alreadySeen.has(createKey(`${params.owner}/${params.repo}/${params.issueNum}`))) { + return; + } + const { linkedIssues, seen, specAndBodies, streamlinedComments: streamlined } = await fetchLinkedIssues(params); + const fetchPromises = linkedIssues.map(async (linkedIssue) => await mergeCommentsAndFetchSpec(params, linkedIssue, streamlinedComments, specAndBodies, seen)); + await throttlePromises(fetchPromises, 10); + return mergeStreamlinedComments(streamlinedComments, streamlined); +} + +export async function handleSpec( + params: FetchParams, + specOrBody: string, + specAndBodies: Record, + key: string, + seen: Set, + streamlinedComments: Record +) { + specAndBodies[key] = specOrBody; + const otherReferences = idIssueFromComment(specOrBody, params); + + if (otherReferences) { + for (const ref of otherReferences) { + const anotherKey = createKey(ref.url, ref.issueNumber); + if (seen.has(anotherKey)) { + return; + } + seen.add(anotherKey); + const issue = await fetchIssue({ + ...params, + owner: ref.owner, + repo: ref.repo, + issueNum: ref.issueNumber, + }); + if (issue?.body) { + specAndBodies[anotherKey] = issue.body; + } + const [owner, repo, issueNum] = splitKey(anotherKey); + if (!streamlinedComments[anotherKey]) { + await handleIssue({ ...params, owner, repo, issueNum: parseInt(issueNum) }, streamlinedComments, seen); + await handleSpec({ ...params, owner, repo, issueNum: parseInt(issueNum) }, issue?.body || "", specAndBodies, anotherKey, seen, streamlinedComments); + } + } + } + + return specAndBodies; +} + +export async function handleComment( + params: FetchParams, + comment: StreamlinedComment, + streamlinedComments: Record, + seen: Set +) { + const otherReferences = idIssueFromComment(comment.body, params); + + if (otherReferences) { + for (const ref of otherReferences) { + const key = createKey(ref.url); + const [refOwner, refRepo, refIssueNumber] = splitKey(key); + + if (!streamlinedComments[key]) { + await handleIssue({ ...params, owner: refOwner, repo: refRepo, issueNum: parseInt(refIssueNumber) }, streamlinedComments, seen); + } + } + } +} + +export async function handleSpecAndBodyKeys(keys: string[], params: FetchParams, streamlinedComments: Record, seen: Set) { + const commentProcessingPromises = keys.map(async (key) => { + let comments = streamlinedComments[key]; + if (!comments || comments.length === 0) { + comments = await fetchAndHandleIssue(key, params, streamlinedComments, seen); + } + + for (const comment of comments) { + await handleComment(params, comment, streamlinedComments, seen); + } + }); + + await throttlePromises(commentProcessingPromises, 10); +} + +export async function throttlePromises(promises: Promise[], limit: number) { + const executing: Promise[] = []; + + for (const promise of promises) { + const p = promise.then(() => { + void executing.splice(executing.indexOf(p), 1); + }); + + executing.push(p); + + if (executing.length >= limit) { + await Promise.race(executing); + } + } + + await Promise.all(executing); +} diff --git a/src/helpers/issue.ts b/src/helpers/issue.ts new file mode 100644 index 0000000..e0ec8ca --- /dev/null +++ b/src/helpers/issue.ts @@ -0,0 +1,71 @@ +import { createKey } from "../handlers/comments"; +import { FetchParams, LinkedIssues } from "../types/github"; +import { StreamlinedComment } from "../types/gpt"; + +export function dedupeStreamlinedComments(streamlinedComments: Record) { + for (const key of Object.keys(streamlinedComments)) { + streamlinedComments[key] = streamlinedComments[key].filter( + (comment: StreamlinedComment, index: number, self: StreamlinedComment[]) => index === self.findIndex((t: StreamlinedComment) => t.body === comment.body) + ); + } + + return streamlinedComments; +} + +export function mergeStreamlinedComments(existingComments: Record, newComments: Record) { + if (!existingComments) { + existingComments = {}; + } + for (const [key, value] of Object.entries(newComments)) { + if (!existingComments[key]) { + existingComments[key] = []; + } + + const previous = existingComments[key] || []; + existingComments[key] = [...previous, ...value]; + } + + return existingComments; +} + +export function splitKey(key: string): [string, string, string] { + const parts = key.split("/"); + return [parts[0], parts[1], parts[2]]; +} + +export function idIssueFromComment(comment?: string | null, params?: FetchParams): LinkedIssues[] | null { + const urlMatch = comment?.match(/https:\/\/(?:www\.)?github.com\/([^/]+)\/([^/]+)\/(pull|issue|issues)\/(\d+)/g); + const response: LinkedIssues[] = []; + + if (urlMatch && urlMatch.length > 0) { + urlMatch.forEach((url) => { + response.push(createLinkedIssueOrPr(url)); + }); + } + /** + * These can only reference issues within the same repository + * so params works here + */ + const hashMatch = comment?.match(/#(\d+)/g); + if (hashMatch && hashMatch.length > 0) { + hashMatch.forEach((hash) => { + const issueNumber = hash.replace("#", ""); + const owner = params?.context.payload.repository?.owner?.login || ""; + const repo = params?.context.payload.repository?.name || ""; + response.push({ owner, repo, issueNumber: parseInt(issueNumber), url: `https://github.com/${owner}/${repo}/issues/${issueNumber}` }); + }); + } + return response; +} + +function createLinkedIssueOrPr(url: string): LinkedIssues { + const key = createKey(url); + const [owner, repo, issueNumber] = splitKey(key); + + return { + owner, + repo, + issueNumber: parseInt(issueNumber), + url, + }; +} diff --git a/src/main.ts b/src/main.ts deleted file mode 100644 index b46765e..0000000 --- a/src/main.ts +++ /dev/null @@ -1,55 +0,0 @@ -import * as core from "@actions/core"; -import * as github from "@actions/github"; -import { Octokit } from "@octokit/rest"; -import { Value } from "@sinclair/typebox/value"; -import { envSchema, pluginSettingsSchema, PluginInputs, pluginSettingsValidator } from "./types"; -import { plugin } from "./plugin"; - -/** - * How a GitHub action executes the plugin. - */ -export async function run() { - const payload = github.context.payload.inputs; - - const env = Value.Decode(envSchema, payload.env); - const settings = Value.Decode(pluginSettingsSchema, Value.Default(pluginSettingsSchema, JSON.parse(payload.settings))); - - if (!pluginSettingsValidator.test(settings)) { - throw new Error("Invalid settings provided"); - } - - const inputs: PluginInputs = { - stateId: payload.stateId, - eventName: payload.eventName, - eventPayload: JSON.parse(payload.eventPayload), - settings, - authToken: payload.authToken, - ref: payload.ref, - }; - - await plugin(inputs, env); - - return returnDataToKernel(inputs.authToken, inputs.stateId, {}); -} - -async function returnDataToKernel(repoToken: string, stateId: string, output: object) { - const octokit = new Octokit({ auth: repoToken }); - await octokit.repos.createDispatchEvent({ - owner: github.context.repo.owner, - repo: github.context.repo.repo, - event_type: "return_data_to_ubiquibot_kernel", - client_payload: { - state_id: stateId, - output: JSON.stringify(output), - }, - }); -} - -run() - .then((result) => { - core.setOutput("result", result); - }) - .catch((error) => { - console.error(error); - core.setFailed(error); - }); diff --git a/src/plugin.ts b/src/plugin.ts index c790042..f89c894 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,15 +1,13 @@ import { Octokit } from "@octokit/rest"; -import { createClient } from "@supabase/supabase-js"; -import { createAdapters } from "./adapters"; -import { Env, PluginInputs } from "./types"; +import { PluginInputs } from "./types"; import { Context } from "./types"; +import { askQuestion } from "./handlers/ask-gpt"; +import { addCommentToIssue } from "./handlers/add-comment"; +import { LogReturn, Logs } from "@ubiquity-dao/ubiquibot-logger"; +import { Env } from "./types/env"; -/** - * How a worker executes the plugin. - */ export async function plugin(inputs: PluginInputs, env: Env) { const octokit = new Octokit({ auth: inputs.authToken }); - const supabase = createClient(env.SUPABASE_URL, env.SUPABASE_KEY); const context: Context = { eventName: inputs.eventName, @@ -17,31 +15,66 @@ export async function plugin(inputs: PluginInputs, env: Env) { config: inputs.settings, octokit, env, - logger: { - debug(message: unknown, ...optionalParams: unknown[]) { - console.debug(message, ...optionalParams); - }, - info(message: unknown, ...optionalParams: unknown[]) { - console.log(message, ...optionalParams); - }, - warn(message: unknown, ...optionalParams: unknown[]) { - console.warn(message, ...optionalParams); - }, - error(message: unknown, ...optionalParams: unknown[]) { - console.error(message, ...optionalParams); - }, - fatal(message: unknown, ...optionalParams: unknown[]) { - console.error(message, ...optionalParams); - }, - }, - adapters: {} as ReturnType, + logger: new Logs("debug"), }; - context.adapters = createAdapters(supabase, context); + return runPlugin(context); +} + +export async function runPlugin(context: Context) { + const { + logger, + env: { UBIQUITY_OS_APP_SLUG }, + } = context; + const question = context.payload.comment.body; + + const slugRegex = new RegExp(`@${UBIQUITY_OS_APP_SLUG} `, "gi"); + + if (!question.match(slugRegex)) { + logger.info("Comment does not mention the app. Skipping."); + return; + } + + if (context.payload.comment.user?.type === "Bot") { + logger.info("Comment is from a bot. Skipping."); + return; + } + + if (question.replace(slugRegex, "").trim().length === 0) { + logger.info("Comment is empty. Skipping."); + return; + } + + logger.info(`Asking question: ${question}`); + let commentToPost = ""; + + try { + const response = await askQuestion(context, question); + const { answer, tokenUsage } = response; + + if (!answer) { + throw logger.error(`No answer from OpenAI`); + } - if (context.eventName === "issue_comment.created") { - // do something - } else { - context.logger.error(`Unsupported event: ${context.eventName}`); + logger.info(`Answer: ${answer}`, { tokenUsage }); + + const tokens = `\n\n`; + + commentToPost = answer + tokens; + } catch (err) { + let errorMessage; + if (err instanceof LogReturn) { + errorMessage = err; + } else if (err instanceof Error) { + errorMessage = context.logger.error(err.message, { error: err, stack: err.stack }); + } else { + errorMessage = context.logger.error("An error occurred", { err }); + } + commentToPost = `${errorMessage?.logMessage.diff}\n`; } + + await addCommentToIssue(context, commentToPost); +} +function sanitizeMetadata(obj: LogReturn["metadata"]): string { + return JSON.stringify(obj, null, 2).replace(//g, ">").replace(/--/g, "--"); } diff --git a/src/types/context.ts b/src/types/context.ts index 45a0266..d5f7113 100644 --- a/src/types/context.ts +++ b/src/types/context.ts @@ -1,10 +1,10 @@ import { Octokit } from "@octokit/rest"; import { EmitterWebhookEvent as WebhookEvent, EmitterWebhookEventName as WebhookEventName } from "@octokit/webhooks"; -import { createAdapters } from "../adapters"; -import { Env } from "./env"; import { PluginSettings } from "./plugin-inputs"; +import { Logs } from "@ubiquity-dao/ubiquibot-logger"; +import { Env } from "./env"; -export type SupportedEventsU = "issue_comment.created"; // Add more events here +export type SupportedEventsU = "issue_comment.created"; export type SupportedEvents = { [K in SupportedEventsU]: K extends WebhookEventName ? WebhookEvent : never; @@ -14,14 +14,7 @@ export interface Context; - adapters: ReturnType; config: PluginSettings; + logger: Logs; env: Env; - logger: { - fatal: (message: unknown, ...optionalParams: unknown[]) => void; - error: (message: unknown, ...optionalParams: unknown[]) => void; - warn: (message: unknown, ...optionalParams: unknown[]) => void; - info: (message: unknown, ...optionalParams: unknown[]) => void; - debug: (message: unknown, ...optionalParams: unknown[]) => void; - }; } diff --git a/src/types/env.ts b/src/types/env.ts index 512e64e..e8a5f7b 100644 --- a/src/types/env.ts +++ b/src/types/env.ts @@ -1,11 +1,19 @@ import { Type as T } from "@sinclair/typebox"; import { StaticDecode } from "@sinclair/typebox"; -import "dotenv/config"; import { StandardValidator } from "typebox-validators"; +import dotenv from "dotenv"; +dotenv.config(); +/** + * Define sensitive environment variables here. + * + * These are fed into the worker/workflow as `env` and are + * taken from either `dev.vars` or repository secrets. + * They are used with `process.env` but are type-safe. + */ export const envSchema = T.Object({ - SUPABASE_URL: T.String(), - SUPABASE_KEY: T.String(), + OPENAI_API_KEY: T.String(), + UBIQUITY_OS_APP_SLUG: T.String(), }); export const envValidator = new StandardValidator(envSchema); diff --git a/src/types/github.ts b/src/types/github.ts new file mode 100644 index 0000000..eef93c0 --- /dev/null +++ b/src/types/github.ts @@ -0,0 +1,22 @@ +import { RestEndpointMethodTypes } from "@octokit/rest"; +import { Context } from "./context"; + +export type Issue = RestEndpointMethodTypes["issues"]["get"]["response"]["data"]; +export type IssueComments = RestEndpointMethodTypes["issues"]["listComments"]["response"]["data"]; +export type ReviewComments = RestEndpointMethodTypes["pulls"]["listReviewComments"]["response"]["data"]; + +export type FetchParams = { + context: Context; + issueNum?: number; + owner?: string; + repo?: string; +}; + +export type LinkedIssues = { + issueNumber: number; + repo: string; + owner: string; + url: string; + comments?: IssueComments | ReviewComments | null | undefined; + body?: string; +}; diff --git a/src/types/gpt.ts b/src/types/gpt.ts new file mode 100644 index 0000000..5bfaa19 --- /dev/null +++ b/src/types/gpt.ts @@ -0,0 +1,19 @@ +export type StreamlinedComment = { + id: number; + user?: string; + body?: string; + org: string; + repo: string; + issueUrl: string; + specOrBody?: { + html: string; + text: string; + }; +}; + +export type StreamlinedComments = { + issueNumber: number; + repo: string; + org: string; + comments: StreamlinedComment[]; +}; diff --git a/src/types/index.ts b/src/types/index.ts index 6ca5c88..4bcbbe7 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,3 +1,2 @@ export * from "./context"; -export * from "./env"; export * from "./plugin-inputs"; diff --git a/src/types/plugin-inputs.ts b/src/types/plugin-inputs.ts index 00d0a52..8b9e071 100644 --- a/src/types/plugin-inputs.ts +++ b/src/types/plugin-inputs.ts @@ -18,7 +18,12 @@ export interface PluginInputs; diff --git a/src/worker.ts b/src/worker.ts index 3048b5d..b713c77 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -1,10 +1,20 @@ import { Value } from "@sinclair/typebox/value"; +import { pluginSettingsSchema, pluginSettingsValidator } from "./types"; +import { Env, envValidator } from "./types/env"; +import manifest from "../manifest.json"; import { plugin } from "./plugin"; -import { Env, envValidator, pluginSettingsSchema, pluginSettingsValidator } from "./types"; export default { async fetch(request: Request, env: Env): Promise { try { + if (request.method === "GET") { + const url = new URL(request.url); + if (url.pathname === "/manifest.json") { + return new Response(JSON.stringify(manifest), { + headers: { "content-type": "application/json" }, + }); + } + } if (request.method !== "POST") { return new Response(JSON.stringify({ error: `Only POST requests are supported.` }), { status: 405, @@ -18,10 +28,9 @@ export default { headers: { "content-type": "application/json" }, }); } - const webhookPayload = await request.json(); const settings = Value.Decode(pluginSettingsSchema, Value.Default(pluginSettingsSchema, webhookPayload.settings)); - + const decodedEnv = Value.Decode(envValidator.schema, Value.Default(envValidator.schema, env)); if (!pluginSettingsValidator.test(settings)) { const errors: string[] = []; for (const error of pluginSettingsValidator.errors(settings)) { @@ -33,9 +42,9 @@ export default { headers: { "content-type": "application/json" }, }); } - if (!envValidator.test(env)) { + if (!envValidator.test(decodedEnv)) { const errors: string[] = []; - for (const error of envValidator.errors(env)) { + for (const error of envValidator.errors(decodedEnv)) { console.error(error); errors.push(`${error.path}: ${error.message}`); } @@ -46,7 +55,7 @@ export default { } webhookPayload.settings = settings; - await plugin(webhookPayload, env); + await plugin(webhookPayload, decodedEnv); return new Response(JSON.stringify("OK"), { status: 200, headers: { "content-type": "application/json" } }); } catch (error) { return handleUncaughtError(error); diff --git a/tests/__mocks__/db.ts b/tests/__mocks__/db.ts index 7df690c..9f25606 100644 --- a/tests/__mocks__/db.ts +++ b/tests/__mocks__/db.ts @@ -1,5 +1,5 @@ // cSpell:disable -import { factory, primaryKey } from "@mswjs/data"; +import { factory, nullable, primaryKey } from "@mswjs/data"; /** * Creates an object that can be used as a db to persist data within tests @@ -7,6 +7,110 @@ import { factory, primaryKey } from "@mswjs/data"; export const db = factory({ users: { id: primaryKey(Number), + login: String, + }, + issue: { + id: primaryKey(Number), + assignees: Array, + html_url: String, + repository_url: String, + state: String, + owner: String, + repo: String, + labels: Array, + author_association: String, + body: nullable(String), + closed_at: nullable(Date), + created_at: nullable(Date), + comments: Number, + comments_url: String, + events_url: String, + labels_url: String, + locked: Boolean, + node_id: String, + title: String, + number: Number, + updated_at: Date, + url: String, + user: nullable(Object), + milestone: nullable(Object), + assignee: nullable({ + avatar_url: String, + email: nullable(String), + events_url: String, + followers_url: String, + following_url: String, + gists_url: String, + gravatar_id: nullable(String), + html_url: String, + id: Number, + login: String, + name: nullable(String), + node_id: String, + organizations_url: String, + received_events_url: String, + repos_url: String, + site_admin: Boolean, + starred_at: String, + starred_url: String, + subscriptions_url: String, + type: String, + url: String, + }), + }, + repo: { + id: primaryKey(Number), + html_url: String, name: String, + url: String, + owner: { + login: String, + id: Number, + }, + issues: Array, + }, + pull: { + id: primaryKey(Number), + html_url: String, + number: Number, + state: String, + title: String, + user: Object, + body: nullable(String), + repo: String, + owner: String, + author: Object, + assignees: Array, + requested_reviewers: Array, + requested_teams: Array, + labels: Array, + draft: Boolean, + created_at: Date, + updated_at: Date, + closed_at: nullable(Date), + merged_at: nullable(Date), + merge_commit_sha: nullable(String), + assignee: nullable(Object), + milestone: nullable(Object), + head: Object, + base: Object, + _links: Object, + author_association: String, + }, + comments: { + id: primaryKey(Number), + node_id: String, + url: String, + issue_url: nullable(String), + pull_request_url: nullable(String), + body: nullable(String), + html_url: String, + user: { + login: String, + type: String, + }, + issue_number: Number, + owner: String, + repo: String, }, }); diff --git a/tests/__mocks__/handlers.ts b/tests/__mocks__/handlers.ts index 0d31c3c..20503d9 100644 --- a/tests/__mocks__/handlers.ts +++ b/tests/__mocks__/handlers.ts @@ -1,11 +1,88 @@ import { http, HttpResponse } from "msw"; import { db } from "./db"; +import issueTemplate from "./issue-template"; /** * Intercepts the routes and returns a custom payload */ export const handlers = [ - http.get("https://api.ubiquity.com/users", () => { - return HttpResponse.json(db.users.getAll()); + http.post("https://api.openai.com/v1/chat/completions", () => { + const answer = `This is a mock answer for the chat`; + + return HttpResponse.json({ + usage: { + completion_tokens: 150, + prompt_tokens: 1000, + total_tokens: 1150, + }, + choices: [ + { + message: { + content: answer, + }, + }, + ], + }); + }), + // GET https://api.github.com/repos/ubiquity/test-repo/issues/1 + http.get("https://api.github.com/repos/:owner/:repo/issues/:issue_number", ({ params: { owner, repo, issue_number: issueNumber } }) => + HttpResponse.json( + db.issue.findFirst({ where: { owner: { equals: owner as string }, repo: { equals: repo as string }, number: { equals: Number(issueNumber) } } }) + ) + ), + + // get repo + http.get("https://api.github.com/repos/:owner/:repo", ({ params: { owner, repo } }: { params: { owner: string; repo: string } }) => { + const item = db.repo.findFirst({ where: { name: { equals: repo }, owner: { login: { equals: owner } } } }); + if (!item) { + return new HttpResponse(null, { status: 404 }); + } + return HttpResponse.json(item); }), + // get issue + http.get("https://api.github.com/repos/:owner/:repo/issues", ({ params: { owner, repo } }: { params: { owner: string; repo: string } }) => + HttpResponse.json(db.issue.findMany({ where: { owner: { equals: owner }, repo: { equals: repo } } })) + ), + // create issue + http.post("https://api.github.com/repos/:owner/:repo/issues", () => { + const id = db.issue.count() + 1; + const newItem = { ...issueTemplate, id }; + db.issue.create(newItem); + return HttpResponse.json(newItem); + }), + // get repo issues + http.get("https://api.github.com/orgs/:org/repos", ({ params: { org } }: { params: { org: string } }) => + HttpResponse.json(db.repo.findMany({ where: { owner: { login: { equals: org } } } })) + ), + // add comment to issue + http.post("https://api.github.com/repos/:owner/:repo/issues/:issue_number/comments", ({ params: { owner, repo, issue_number: issueNumber } }) => + HttpResponse.json({ owner, repo, issueNumber }) + ), + // list pull requests + http.get("https://api.github.com/repos/:owner/:repo/pulls", ({ params: { owner, repo } }: { params: { owner: string; repo: string } }) => + HttpResponse.json(db.pull.findMany({ where: { owner: { equals: owner }, repo: { equals: repo } } })) + ), + // update a pull request + http.patch("https://api.github.com/repos/:owner/:repo/pulls/:pull_number", ({ params: { owner, repo, pull_number: pullNumber } }) => + HttpResponse.json({ owner, repo, pull_number: pullNumber }) + ), + + // list issue comments + http.get("https://api.github.com/repos/:owner/:repo/issues/:issue_number/comments", ({ params: { owner, repo, issue_number: issueNumber } }) => + HttpResponse.json( + db.comments.findMany({ where: { owner: { equals: owner as string }, repo: { equals: repo as string }, issue_number: { equals: Number(issueNumber) } } }) + ) + ), + //list review comments + http.get("https://api.github.com/repos/:owner/:repo/pulls/:pull_number/comments", ({ params: { owner, repo, pull_number: pullNumber } }) => + HttpResponse.json( + db.comments.findMany({ where: { owner: { equals: owner as string }, repo: { equals: repo as string }, issue_number: { equals: Number(pullNumber) } } }) + ) + ), + // octokit.pulls.get + http.get("https://api.github.com/repos/:owner/:repo/pulls/:pull_number", ({ params: { owner, repo, pull_number: pullNumber } }) => + HttpResponse.json( + db.pull.findFirst({ where: { owner: { equals: owner as string }, repo: { equals: repo as string }, number: { equals: Number(pullNumber) } } }) + ) + ), ]; diff --git a/tests/__mocks__/issue-template.ts b/tests/__mocks__/issue-template.ts new file mode 100644 index 0000000..d8f682c --- /dev/null +++ b/tests/__mocks__/issue-template.ts @@ -0,0 +1,55 @@ +export default { + assignee: { + login: "", + avatar_url: "", + email: "undefined", + events_url: "", + followers_url: "", + following_url: "", + gists_url: "", + gravatar_id: null, + html_url: "", + id: 1, + name: "undefined", + node_id: "", + organizations_url: "", + received_events_url: "", + repos_url: "", + site_admin: false, + starred_at: "", + starred_url: "", + subscriptions_url: "", + type: "", + url: "", + }, + author_association: "NONE", + closed_at: null, + comments: 0, + comments_url: "", + created_at: new Date().toISOString(), + events_url: "", + html_url: "https://github.com/ubiquity/test-repo/issues/1", + id: 1, + labels_url: "", + locked: false, + milestone: null, + node_id: "1", + owner: "ubiquity", + number: 1, + repository_url: "https://github.com/ubiquity/test-repo", + state: "open", + title: "issue", + updated_at: "", + url: "https://api.github.com/repos/ubiquity/test-repo/issues/1", + user: null, + repo: "test-repo", + labels: [ + { + name: "Price: 200 USD", + }, + { + name: "Time: 1h", + }, + ], + body: "This is a demo spec for a demo task just perfect for testing.", +}; diff --git a/tests/__mocks__/repo-template.ts b/tests/__mocks__/repo-template.ts new file mode 100644 index 0000000..7bf7be7 --- /dev/null +++ b/tests/__mocks__/repo-template.ts @@ -0,0 +1,11 @@ +export default { + id: 1, + html_url: "", + url: "https://api.github.com/repos/ubiquity/test-repo", + name: "test-repo", + owner: { + login: "ubiquity", + id: 1, + }, + issues: [], +}; diff --git a/tests/__mocks__/users-get.json b/tests/__mocks__/users-get.json index 59f0200..8681c7b 100644 --- a/tests/__mocks__/users-get.json +++ b/tests/__mocks__/users-get.json @@ -1,10 +1,10 @@ [ { "id": 1, - "name": "user1" + "login": "ubiquity" }, { "id": 2, - "name": "user2" + "login": "user2" } ] diff --git a/tests/main.test.ts b/tests/main.test.ts index 7967004..1b2069c 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -2,21 +2,309 @@ import { db } from "./__mocks__/db"; import { server } from "./__mocks__/node"; import usersGet from "./__mocks__/users-get.json"; import { expect, describe, beforeAll, beforeEach, afterAll, afterEach, it } from "@jest/globals"; +import { Logs } from "@ubiquity-dao/ubiquibot-logger"; +import { Context, SupportedEventsU } from "../src/types"; +import { drop } from "@mswjs/data"; +import issueTemplate from "./__mocks__/issue-template"; +import repoTemplate from "./__mocks__/repo-template"; +import { askQuestion } from "../src/handlers/ask-gpt"; +import { runPlugin } from "../src/plugin"; +import { TransformDecodeCheckError, Value } from "@sinclair/typebox/value"; +import { envSchema } from "../src/types/env"; -beforeAll(() => server.listen()); -afterEach(() => server.resetHandlers()); +const TEST_QUESTION = "what is pi?"; +const TEST_SLASH_COMMAND = "@UbiquityOS what is pi?"; +const LOG_CALLER = "_Logs."; + +const systemMsg = `You are a GitHub integrated chatbot tasked with assisting in research and discussion on GitHub issues and pull requests. +Using the provided context, address the question being asked providing a clear and concise answer with no follow-up statements. +The LAST comment in 'Issue Conversation' is the most recent one, focus on it as that is the question being asked. +Use GitHub flavoured markdown in your response making effective use of lists, code blocks and other supported GitHub md features.`; + +type Comment = { + id: number; + user: { + login: string; + type: string; + }; + body: string; + url: string; + html_url: string; + owner: string; + repo: string; + issue_number: number; + issue_url?: string; + pull_request_url?: string; +}; + +const octokit = jest.requireActual("@octokit/rest"); +jest.requireActual("openai"); + +beforeAll(() => { + server.listen(); +}); +afterEach(() => { + drop(db); + server.resetHandlers(); +}); afterAll(() => server.close()); -describe("User tests", () => { - beforeEach(() => { - for (const item of usersGet) { - db.users.create(item); - } +// TESTS + +describe("Ask plugin tests", () => { + beforeEach(async () => { + await setupTests(); }); - it("Should fetch all the users", async () => { - const res = await fetch("https://api.ubiquity.com/users"); - const data = await res.json(); - expect(data).toMatchObject(usersGet); + it("should ask GPT a question", async () => { + const ctx = createContext(TEST_SLASH_COMMAND); + createComments([transformCommentTemplate(1, 1, TEST_QUESTION, "ubiquity", "test-repo", true)]); + const res = await askQuestion(ctx, TEST_QUESTION); + + expect(res).toBeDefined(); + + expect(res?.answer).toBe("This is a mock answer for the chat"); + }); + + it("should not ask GPT a question if comment is from a bot", async () => { + const ctx = createContext(TEST_SLASH_COMMAND); + const infoSpy = jest.spyOn(ctx.logger, "info"); + + createComments([transformCommentTemplate(1, 1, TEST_QUESTION, "ubiquity", "test-repo", true)]); + if (!ctx.payload.comment.user) return; + ctx.payload.comment.user.type = "Bot"; + await runPlugin(ctx); + + expect(infoSpy).toHaveBeenCalledWith("Comment is from a bot. Skipping."); + }); + + it("should not ask GPT a question if comment does not start with /gpt", async () => { + const ctx = createContext(TEST_QUESTION); + const infoSpy = jest.spyOn(ctx.logger, "info"); + + createComments([transformCommentTemplate(1, 1, TEST_QUESTION, "ubiquity", "test-repo", true)]); + await runPlugin(ctx); + + expect(infoSpy).toHaveBeenCalledWith("Comment does not mention the app. Skipping."); + }); + + it("should not ask GPT a question if no question is provided", async () => { + const ctx = createContext(`@UbiquityOS `); + const infoSpy = jest.spyOn(ctx.logger, "info"); + + createComments([transformCommentTemplate(1, 1, TEST_QUESTION, "ubiquity", "test-repo", true)]); + await runPlugin(ctx); + + expect(infoSpy).toHaveBeenCalledWith("Comment is empty. Skipping."); + }); + it("Should throw if OPENAI_API_KEY is not defined", () => { + const settings = {}; + expect(() => Value.Decode(envSchema, settings)).toThrow(TransformDecodeCheckError); + }); + + it("should construct the chat history correctly", async () => { + const ctx = createContext(TEST_SLASH_COMMAND); + const infoSpy = jest.spyOn(ctx.logger, "info"); + createComments([transformCommentTemplate(1, 1, TEST_QUESTION, "ubiquity", "test-repo", true)]); + await runPlugin(ctx); + + expect(infoSpy).toHaveBeenCalledTimes(3); + + const prompt = `=== Current Issue #1 Specification === ubiquity/test-repo/1 === + +This is a demo spec for a demo task just perfect for testing. +=== End Current Issue #1 Specification === + +=== Current Issue #1 Conversation === ubiquity/test-repo #1 === + +1 ubiquity: what is pi? +=== End Current Issue #1 Conversation ===\n +`; + + expect(infoSpy).toHaveBeenNthCalledWith(1, "Asking question: @UbiquityOS what is pi?"); + expect(infoSpy).toHaveBeenNthCalledWith(2, "Sending chat to OpenAI", { + caller: LOG_CALLER, + chat: [ + { + role: "system", + content: systemMsg, + }, + { + role: "user", + content: prompt, + }, + ], + }); + + expect(infoSpy).toHaveBeenNthCalledWith(3, "Answer: This is a mock answer for the chat", { + caller: LOG_CALLER, + tokenUsage: { + input: 1000, + output: 150, + total: 1150, + }, + }); + }); + + it("should collect the linked issues correctly", async () => { + const ctx = createContext(TEST_SLASH_COMMAND); + const infoSpy = jest.spyOn(ctx.logger, "info"); + createComments([ + transformCommentTemplate(1, 1, "More context here #2", "ubiquity", "test-repo", true), + transformCommentTemplate(2, 1, TEST_QUESTION, "ubiquity", "test-repo", true), + transformCommentTemplate(3, 2, "More context here #3", "ubiquity", "test-repo", true), + transformCommentTemplate(4, 3, "Just a comment", "ubiquity", "test-repo", true), + ]); + + await runPlugin(ctx); + + expect(infoSpy).toHaveBeenCalledTimes(3); + + expect(infoSpy).toHaveBeenNthCalledWith(1, "Asking question: @UbiquityOS what is pi?"); + + const prompt = `=== Current Issue #1 Specification === ubiquity/test-repo/1 === + +This is a demo spec for a demo task just perfect for testing. +=== End Current Issue #1 Specification === + +=== Current Issue #1 Conversation === ubiquity/test-repo #1 === + +1 ubiquity: More context here #2 +2 ubiquity: what is pi? +=== End Current Issue #1 Conversation === + +=== Linked Issue #2 Specification === ubiquity/test-repo/2 === + +Related to issue #3 +=== End Linked Issue #2 Specification === + +=== Linked Issue #2 Conversation === ubiquity/test-repo #2 === + +3 ubiquity: More context here #3 +=== End Linked Issue #2 Conversation === + +=== Linked Issue #3 Specification === ubiquity/test-repo/3 === + +Just another issue +=== End Linked Issue #3 Specification === + +=== Linked Issue #3 Conversation === ubiquity/test-repo #3 === + +4 ubiquity: Just a comment +=== End Linked Issue #3 Conversation ===\n +`; + + expect(infoSpy).toHaveBeenNthCalledWith(2, "Sending chat to OpenAI", { + caller: LOG_CALLER, + chat: [ + { + role: "system", + content: systemMsg, + }, + { + role: "user", + content: prompt, + }, + ], + }); }); }); + +// HELPERS + +function transformCommentTemplate(commentId: number, issueNumber: number, body: string, owner: string, repo: string, isIssue = true) { + const COMMENT_TEMPLATE = { + id: 1, + user: { + login: "ubiquity", + type: "User", + }, + body: TEST_QUESTION, + url: "https://api.github.com/repos/ubiquity/test-repo/issues/comments/1", + html_url: "https://www.github.com/ubiquity/test-repo/issues/1", + owner: "ubiquity", + repo: "test-repo", + issue_number: 1, + }; + + const comment: Comment = { + id: commentId, + user: { + login: COMMENT_TEMPLATE.user.login, + type: "User", + }, + body: body, + url: COMMENT_TEMPLATE.url.replace("1", issueNumber.toString()), + html_url: COMMENT_TEMPLATE.html_url.replace("1", issueNumber.toString()), + owner: owner, + repo: repo, + issue_number: issueNumber, + }; + + if (isIssue) { + comment.issue_url = COMMENT_TEMPLATE.html_url.replace("1", issueNumber.toString()); + } else { + comment.pull_request_url = COMMENT_TEMPLATE.html_url.replace("1", issueNumber.toString()); + } + + return comment; +} + +async function setupTests() { + for (const item of usersGet) { + db.users.create(item); + } + + db.repo.create({ + ...repoTemplate, + }); + + db.issue.create({ + ...issueTemplate, + }); + + db.issue.create({ + ...issueTemplate, + id: 2, + number: 2, + body: "Related to issue #3", + }); + + db.issue.create({ + ...issueTemplate, + id: 3, + number: 3, + body: "Just another issue", + }); +} + +function createComments(comments: Comment[]) { + for (const comment of comments) { + db.comments.create({ + ...comment, + }); + } +} + +function createContext(body = TEST_SLASH_COMMAND) { + const user = db.users.findFirst({ where: { id: { equals: 1 } } }); + return { + payload: { + issue: db.issue.findFirst({ where: { id: { equals: 1 } } }) as unknown as Context["payload"]["issue"], + sender: user, + repository: db.repo.findFirst({ where: { id: { equals: 1 } } }) as unknown as Context["payload"]["repository"], + comment: { body, user: user } as unknown as Context["payload"]["comment"], + action: "created" as string, + installation: { id: 1 } as unknown as Context["payload"]["installation"], + organization: { login: "ubiquity" } as unknown as Context["payload"]["organization"], + }, + logger: new Logs("debug"), + config: {}, + env: { + UBIQUITY_OS_APP_SLUG: "UbiquityOS", + OPENAI_API_KEY: "test", + }, + octokit: new octokit.Octokit(), + eventName: "issue_comment.created" as SupportedEventsU, + } as unknown as Context; +} diff --git a/wrangler.toml b/wrangler.toml index 5a0953a..f780a61 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -1,4 +1,4 @@ -name = "your-plugin-name" +name = "command-ask" main = "src/worker.ts" compatibility_date = "2024-05-23" node_compat = true diff --git a/yarn.lock b/yarn.lock index d4049db..18a112c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,32 +2,6 @@ # yarn lockfile v1 -"@actions/core@1.10.1": - version "1.10.1" - resolved "https://registry.yarnpkg.com/@actions/core/-/core-1.10.1.tgz#61108e7ac40acae95ee36da074fa5850ca4ced8a" - integrity sha512-3lBR9EDAY+iYIpTnTIXmWcNbX3T2kCkAEQGIQx4NVQ0575nk2k3GRZDTPQG+vVtS2izSLmINlxXf0uLtnrTP+g== - dependencies: - "@actions/http-client" "^2.0.1" - uuid "^8.3.2" - -"@actions/github@6.0.0": - version "6.0.0" - resolved "https://registry.yarnpkg.com/@actions/github/-/github-6.0.0.tgz#65883433f9d81521b782a64cc1fd45eef2191ea7" - integrity sha512-alScpSVnYmjNEXboZjarjukQEzgCRmjMv6Xj47fsdnqGS73bjJNDpiiXmp8jr0UZLdUB6d9jW63IcmddUP+l0g== - dependencies: - "@actions/http-client" "^2.2.0" - "@octokit/core" "^5.0.1" - "@octokit/plugin-paginate-rest" "^9.0.0" - "@octokit/plugin-rest-endpoint-methods" "^10.0.0" - -"@actions/http-client@^2.0.1", "@actions/http-client@^2.2.0": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@actions/http-client/-/http-client-2.2.1.tgz#ed3fe7a5a6d317ac1d39886b0bb999ded229bb38" - integrity sha512-KhC/cZsq7f8I4LfZSJKgCvEwfkE8o1538VoBeoGzokVLLnbFDEAdFD3UhoMklxo2un9NJVBdANOresx7vTHlHw== - dependencies: - tunnel "^0.0.6" - undici "^5.25.4" - "@ampproject/remapping@^2.2.0": version "2.3.0" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" @@ -946,231 +920,116 @@ escape-string-regexp "^4.0.0" rollup-plugin-node-polyfills "^0.2.1" -"@esbuild/aix-ppc64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" - integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ== - "@esbuild/android-arm64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz#bafb75234a5d3d1b690e7c2956a599345e84a2fd" integrity sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA== -"@esbuild/android-arm64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052" - integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A== - "@esbuild/android-arm@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.17.19.tgz#5898f7832c2298bc7d0ab53701c57beb74d78b4d" integrity sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A== -"@esbuild/android-arm@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28" - integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg== - "@esbuild/android-x64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.19.tgz#658368ef92067866d95fb268719f98f363d13ae1" integrity sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww== -"@esbuild/android-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e" - integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA== - "@esbuild/darwin-arm64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz#584c34c5991b95d4d48d333300b1a4e2ff7be276" integrity sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg== -"@esbuild/darwin-arm64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a" - integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== - "@esbuild/darwin-x64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz#7751d236dfe6ce136cce343dce69f52d76b7f6cb" integrity sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw== -"@esbuild/darwin-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22" - integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw== - "@esbuild/freebsd-arm64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz#cacd171665dd1d500f45c167d50c6b7e539d5fd2" integrity sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ== -"@esbuild/freebsd-arm64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e" - integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g== - "@esbuild/freebsd-x64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz#0769456eee2a08b8d925d7c00b79e861cb3162e4" integrity sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ== -"@esbuild/freebsd-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261" - integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ== - "@esbuild/linux-arm64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz#38e162ecb723862c6be1c27d6389f48960b68edb" integrity sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg== -"@esbuild/linux-arm64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b" - integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q== - "@esbuild/linux-arm@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz#1a2cd399c50040184a805174a6d89097d9d1559a" integrity sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA== -"@esbuild/linux-arm@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9" - integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA== - "@esbuild/linux-ia32@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz#e28c25266b036ce1cabca3c30155222841dc035a" integrity sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ== -"@esbuild/linux-ia32@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2" - integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg== - "@esbuild/linux-loong64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz#0f887b8bb3f90658d1a0117283e55dbd4c9dcf72" integrity sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ== -"@esbuild/linux-loong64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df" - integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg== - "@esbuild/linux-mips64el@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz#f5d2a0b8047ea9a5d9f592a178ea054053a70289" integrity sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A== -"@esbuild/linux-mips64el@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe" - integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg== - "@esbuild/linux-ppc64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz#876590e3acbd9fa7f57a2c7d86f83717dbbac8c7" integrity sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg== -"@esbuild/linux-ppc64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4" - integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w== - "@esbuild/linux-riscv64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz#7f49373df463cd9f41dc34f9b2262d771688bf09" integrity sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA== -"@esbuild/linux-riscv64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc" - integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA== - "@esbuild/linux-s390x@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz#e2afd1afcaf63afe2c7d9ceacd28ec57c77f8829" integrity sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q== -"@esbuild/linux-s390x@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de" - integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A== - "@esbuild/linux-x64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz#8a0e9738b1635f0c53389e515ae83826dec22aa4" integrity sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw== -"@esbuild/linux-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0" - integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== - "@esbuild/netbsd-x64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz#c29fb2453c6b7ddef9a35e2c18b37bda1ae5c462" integrity sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q== -"@esbuild/netbsd-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047" - integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg== - "@esbuild/openbsd-x64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz#95e75a391403cb10297280d524d66ce04c920691" integrity sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g== -"@esbuild/openbsd-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70" - integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow== - "@esbuild/sunos-x64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz#722eaf057b83c2575937d3ffe5aeb16540da7273" integrity sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg== -"@esbuild/sunos-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b" - integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg== - "@esbuild/win32-arm64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz#9aa9dc074399288bdcdd283443e9aeb6b9552b6f" integrity sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag== -"@esbuild/win32-arm64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d" - integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A== - "@esbuild/win32-ia32@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz#95ad43c62ad62485e210f6299c7b2571e48d2b03" integrity sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw== -"@esbuild/win32-ia32@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b" - integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA== - "@esbuild/win32-x64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz#8cfaf2ff603e9aabb910e9c0558c26cf32744061" integrity sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA== -"@esbuild/win32-x64@0.21.5": - version "0.21.5" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" - integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== - "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -1602,7 +1461,7 @@ resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-4.0.0.tgz#40d203ea827b9f17f42a29c6afb93b7745ef80c7" integrity sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA== -"@octokit/core@^5.0.1", "@octokit/core@^5.0.2": +"@octokit/core@^5.0.2": version "5.2.0" resolved "https://registry.yarnpkg.com/@octokit/core/-/core-5.2.0.tgz#ddbeaefc6b44a39834e1bb2e58a49a117672a7ea" integrity sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg== @@ -1632,11 +1491,6 @@ "@octokit/types" "^13.0.0" universal-user-agent "^6.0.0" -"@octokit/openapi-types@^20.0.0": - version "20.0.0" - resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-20.0.0.tgz#9ec2daa0090eeb865ee147636e0c00f73790c6e5" - integrity sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA== - "@octokit/openapi-types@^22.2.0": version "22.2.0" resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-22.2.0.tgz#75aa7dcd440821d99def6a60b5f014207ae4968e" @@ -1654,13 +1508,6 @@ dependencies: "@octokit/types" "^13.5.0" -"@octokit/plugin-paginate-rest@^9.0.0": - version "9.2.1" - resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.1.tgz#2e2a2f0f52c9a4b1da1a3aa17dabe3c459b9e401" - integrity sha512-wfGhE/TAkXZRLjksFXuDZdmGnJQHvtU/joFQdweXUgzo1XwvBCD4o4+75NtFfjfLK5IwLf9vHTfSiU3sLRYpRw== - dependencies: - "@octokit/types" "^12.6.0" - "@octokit/plugin-request-log@^4.0.0": version "4.0.1" resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-4.0.1.tgz#98a3ca96e0b107380664708111864cb96551f958" @@ -1673,13 +1520,6 @@ dependencies: "@octokit/types" "^13.5.0" -"@octokit/plugin-rest-endpoint-methods@^10.0.0": - version "10.4.1" - resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz#41ba478a558b9f554793075b2e20cd2ef973be17" - integrity sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg== - dependencies: - "@octokit/types" "^12.6.0" - "@octokit/request-error@^5.1.0": version "5.1.0" resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-5.1.0.tgz#ee4138538d08c81a60be3f320cd71063064a3b30" @@ -1716,13 +1556,6 @@ "@octokit/plugin-request-log" "^4.0.0" "@octokit/plugin-rest-endpoint-methods" "13.2.2" -"@octokit/types@^12.6.0": - version "12.6.0" - resolved "https://registry.yarnpkg.com/@octokit/types/-/types-12.6.0.tgz#8100fb9eeedfe083aae66473bd97b15b62aedcb2" - integrity sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw== - dependencies: - "@octokit/openapi-types" "^20.0.0" - "@octokit/types@^13.0.0", "@octokit/types@^13.1.0", "@octokit/types@^13.5.0": version "13.5.0" resolved "https://registry.yarnpkg.com/@octokit/types/-/types-13.5.0.tgz#4796e56b7b267ebc7c921dcec262b3d5bfb18883" @@ -1801,63 +1634,6 @@ ignore "^5.1.8" p-map "^4.0.0" -"@supabase/auth-js@2.64.2": - version "2.64.2" - resolved "https://registry.yarnpkg.com/@supabase/auth-js/-/auth-js-2.64.2.tgz#fe6828ed2c9844bf2e71b27f88ddfb635f24d1c1" - integrity sha512-s+lkHEdGiczDrzXJ1YWt2y3bxRi+qIUnXcgkpLSrId7yjBeaXBFygNjTaoZLG02KNcYwbuZ9qkEIqmj2hF7svw== - dependencies: - "@supabase/node-fetch" "^2.6.14" - -"@supabase/functions-js@2.4.1": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@supabase/functions-js/-/functions-js-2.4.1.tgz#373e75f8d3453bacd71fb64f88d7a341d7b53ad7" - integrity sha512-8sZ2ibwHlf+WkHDUZJUXqqmPvWQ3UHN0W30behOJngVh/qHHekhJLCFbh0AjkE9/FqqXtf9eoVvmYgfCLk5tNA== - dependencies: - "@supabase/node-fetch" "^2.6.14" - -"@supabase/node-fetch@2.6.15", "@supabase/node-fetch@^2.6.14": - version "2.6.15" - resolved "https://registry.yarnpkg.com/@supabase/node-fetch/-/node-fetch-2.6.15.tgz#731271430e276983191930816303c44159e7226c" - integrity sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ== - dependencies: - whatwg-url "^5.0.0" - -"@supabase/postgrest-js@1.15.5": - version "1.15.5" - resolved "https://registry.yarnpkg.com/@supabase/postgrest-js/-/postgrest-js-1.15.5.tgz#7fa7744cb0991328bb1a7757861e435a5477f358" - integrity sha512-YR4TiitTE2hizT7mB99Cl3V9i00RAY5sUxS2/NuWWzkreM7OeYlP2OqnqVwwb4z6ILn+j8x9e/igJDepFhjswQ== - dependencies: - "@supabase/node-fetch" "^2.6.14" - -"@supabase/realtime-js@2.9.5": - version "2.9.5" - resolved "https://registry.yarnpkg.com/@supabase/realtime-js/-/realtime-js-2.9.5.tgz#22b7de952a7f37868ffc25d32d19f03f27bfcb40" - integrity sha512-TEHlGwNGGmKPdeMtca1lFTYCedrhTAv3nZVoSjrKQ+wkMmaERuCe57zkC5KSWFzLYkb5FVHW8Hrr+PX1DDwplQ== - dependencies: - "@supabase/node-fetch" "^2.6.14" - "@types/phoenix" "^1.5.4" - "@types/ws" "^8.5.10" - ws "^8.14.2" - -"@supabase/storage-js@2.6.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@supabase/storage-js/-/storage-js-2.6.0.tgz#0fa5e04db760ed7f78e4394844a6d409e537adc5" - integrity sha512-REAxr7myf+3utMkI2oOmZ6sdplMZZ71/2NEIEMBZHL9Fkmm3/JnaOZVSRqvG4LStYj2v5WhCruCzuMn6oD/Drw== - dependencies: - "@supabase/node-fetch" "^2.6.14" - -"@supabase/supabase-js@2.43.5": - version "2.43.5" - resolved "https://registry.yarnpkg.com/@supabase/supabase-js/-/supabase-js-2.43.5.tgz#e4d5f9e5e21ef4226e0cb013c7e51fb3c5262581" - integrity sha512-Y4GukjZWW6ouohMaPlYz8tSz9ykf9jY7w9/RhqKuScmla3Xiklce8eLr8TYAtA+oQYCWxo3RgS3B6O4rd/72FA== - dependencies: - "@supabase/auth-js" "2.64.2" - "@supabase/functions-js" "2.4.1" - "@supabase/node-fetch" "2.6.15" - "@supabase/postgrest-js" "1.15.5" - "@supabase/realtime-js" "2.9.5" - "@supabase/storage-js" "2.6.0" - "@types/babel__core@^7.1.14": version "7.20.5" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" @@ -1929,6 +1705,14 @@ dependencies: "@types/istanbul-lib-report" "*" +"@types/jest@^29.5.12": + version "29.5.12" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.12.tgz#7f7dc6eb4cf246d2474ed78744b05d06ce025544" + integrity sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw== + dependencies: + expect "^29.0.0" + pretty-format "^29.0.0" + "@types/lodash@^4.14.172": version "4.17.4" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.4.tgz#0303b64958ee070059e3a7184048a55159fe20b7" @@ -1946,6 +1730,14 @@ dependencies: "@types/node" "*" +"@types/node-fetch@^2.6.4": + version "2.6.11" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.11.tgz#9b39b78665dae0e82a08f02f4967d62c66f95d24" + integrity sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g== + dependencies: + "@types/node" "*" + form-data "^4.0.0" + "@types/node-forge@^1.3.0": version "1.3.11" resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da" @@ -1967,10 +1759,12 @@ dependencies: undici-types "~5.26.4" -"@types/phoenix@^1.5.4": - version "1.6.4" - resolved "https://registry.yarnpkg.com/@types/phoenix/-/phoenix-1.6.4.tgz#cceac93a827555473ad38057d1df7d06eef1ed71" - integrity sha512-B34A7uot1Cv0XtaHRYDATltAdKx0BvVKNgYNqE4WjtPUa4VQJM7kxeXcVKaH+KS+kCmZ+6w+QaUdcljiheiBJA== +"@types/node@^18.11.18": + version "18.19.39" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.39.tgz#c316340a5b4adca3aee9dcbf05de385978590593" + integrity sha512-nPwTRDKUctxw3di5b4TfT3I0sWDiWoPQCZjXhvdkINntwr8lcoVCKsTgnXeRubKIlfnV+eN/HYk6Jb40tbcEAQ== + dependencies: + undici-types "~5.26.4" "@types/pluralize@^0.0.29": version "0.0.29" @@ -1997,13 +1791,6 @@ resolved "https://registry.yarnpkg.com/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz#18b97a972f94f60a679fd5c796d96421b9abb9fd" integrity sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g== -"@types/ws@^8.5.10": - version "8.5.10" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.10.tgz#4acfb517970853fa6574a3a6886791d04a396787" - integrity sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A== - dependencies: - "@types/node" "*" - "@types/yargs-parser@*": version "21.0.3" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" @@ -2097,6 +1884,11 @@ "@typescript-eslint/types" "7.13.1" eslint-visitor-keys "^3.4.3" +"@ubiquity-dao/ubiquibot-logger@^1.3.0": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@ubiquity-dao/ubiquibot-logger/-/ubiquibot-logger-1.3.1.tgz#c3f45d70014dcc2551442c28101046e1c8ea6886" + integrity sha512-kDLnVP87Y3yZV6NnqIEDAOz+92IW0nIcccML2lUn93uZ5ada78vfdTPtwPJo8tkXl1Z9qMKAqqHkwBMp1Ksnag== + JSONStream@^1.3.5: version "1.3.5" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" @@ -2105,6 +1897,13 @@ JSONStream@^1.3.5: jsonparse "^1.2.0" through ">=2.2.7 <3" +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -2125,6 +1924,13 @@ acorn@^8.8.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== +agentkeepalive@^4.2.1: + version "4.5.0" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.5.0.tgz#2673ad1389b3c418c5a20c5d7364f93ca04be923" + integrity sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew== + dependencies: + humanize-ms "^1.2.1" + aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" @@ -2288,6 +2094,11 @@ async-lock@^1.4.1: resolved "https://registry.yarnpkg.com/async-lock/-/async-lock-1.4.1.tgz#56b8718915a9b68b10fce2f2a9a3dddf765ef53f" integrity sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ== +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + available-typed-arrays@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" @@ -2637,6 +2448,13 @@ colorette@^2.0.20: resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + commander@^12.1.0, commander@~12.1.0: version "12.1.0" resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" @@ -2998,6 +2816,11 @@ defu@^6.1.4: resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.4.tgz#4e0c9cf9ff68fe5f3d7f2765cc1a012dfdcb0479" integrity sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg== +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + deprecation@^2.0.0: version "2.3.1" resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" @@ -3032,7 +2855,7 @@ dot-prop@^5.1.0: dependencies: is-obj "^2.0.0" -dotenv@16.4.5: +dotenv@^16.4.5: version "16.4.5" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== @@ -3200,35 +3023,6 @@ esbuild@0.17.19: "@esbuild/win32-ia32" "0.17.19" "@esbuild/win32-x64" "0.17.19" -esbuild@~0.21.4: - version "0.21.5" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" - integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw== - optionalDependencies: - "@esbuild/aix-ppc64" "0.21.5" - "@esbuild/android-arm" "0.21.5" - "@esbuild/android-arm64" "0.21.5" - "@esbuild/android-x64" "0.21.5" - "@esbuild/darwin-arm64" "0.21.5" - "@esbuild/darwin-x64" "0.21.5" - "@esbuild/freebsd-arm64" "0.21.5" - "@esbuild/freebsd-x64" "0.21.5" - "@esbuild/linux-arm" "0.21.5" - "@esbuild/linux-arm64" "0.21.5" - "@esbuild/linux-ia32" "0.21.5" - "@esbuild/linux-loong64" "0.21.5" - "@esbuild/linux-mips64el" "0.21.5" - "@esbuild/linux-ppc64" "0.21.5" - "@esbuild/linux-riscv64" "0.21.5" - "@esbuild/linux-s390x" "0.21.5" - "@esbuild/linux-x64" "0.21.5" - "@esbuild/netbsd-x64" "0.21.5" - "@esbuild/openbsd-x64" "0.21.5" - "@esbuild/sunos-x64" "0.21.5" - "@esbuild/win32-arm64" "0.21.5" - "@esbuild/win32-ia32" "0.21.5" - "@esbuild/win32-x64" "0.21.5" - escalade@^3.1.1, escalade@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" @@ -3381,6 +3175,11 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + eventemitter3@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" @@ -3426,7 +3225,7 @@ exit@^0.1.2: resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== -expect@^29.7.0: +expect@^29.0.0, expect@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/expect/-/expect-29.7.0.tgz#578874590dcb3214514084c08115d8aee61e11bc" integrity sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw== @@ -3556,12 +3355,34 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +form-data-encoder@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-1.7.2.tgz#1f1ae3dccf58ed4690b86d87e4f57c654fbab040" + integrity sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A== + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +formdata-node@^4.3.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/formdata-node/-/formdata-node-4.4.1.tgz#23f6a5cb9cb55315912cbec4ff7b0f59bbd191e2" + integrity sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ== + dependencies: + node-domexception "1.0.0" + web-streams-polyfill "4.0.0-beta.3" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@^2.3.2, fsevents@~2.3.2, fsevents@~2.3.3: +fsevents@^2.3.2, fsevents@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== @@ -3654,13 +3475,6 @@ get-symbol-description@^1.0.2: es-errors "^1.3.0" get-intrinsic "^1.2.4" -get-tsconfig@^4.7.5: - version "4.7.5" - resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.7.5.tgz#5e012498579e9a6947511ed0cd403272c7acbbaf" - integrity sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw== - dependencies: - resolve-pkg-maps "^1.0.0" - git-raw-commits@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/git-raw-commits/-/git-raw-commits-4.0.0.tgz#b212fd2bff9726d27c1283a1157e829490593285" @@ -3841,6 +3655,13 @@ human-signals@^5.0.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== +humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" + integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== + dependencies: + ms "^2.0.0" + husky@9.0.11: version "9.0.11" resolved "https://registry.yarnpkg.com/husky/-/husky-9.0.11.tgz#fc91df4c756050de41b3e478b2158b87c1e79af9" @@ -4928,6 +4749,18 @@ micromatch@^4.0.4, micromatch@^4.0.5, micromatch@^4.0.7, micromatch@~4.0.7: braces "^3.0.3" picomatch "^2.3.1" +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + mime@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" @@ -5002,6 +4835,11 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +ms@^2.0.0: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + msw@^2.0.8: version "2.3.1" resolved "https://registry.yarnpkg.com/msw/-/msw-2.3.1.tgz#bfc73e256ffc2c74ec4381b604abb258df35f32b" @@ -5050,11 +4888,23 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +node-domexception@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" + integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== + node-fetch-native@^1.6.4: version "1.6.4" resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.6.4.tgz#679fc8fd8111266d47d7e72c379f1bed9acff06e" integrity sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ== +node-fetch@^2.6.7: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + node-forge@^1: version "1.3.1" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" @@ -5165,6 +5015,19 @@ onetime@^6.0.0: dependencies: mimic-fn "^4.0.0" +openai@^4.63.0: + version "4.63.0" + resolved "https://registry.yarnpkg.com/openai/-/openai-4.63.0.tgz#cabe7223788157c96c818317cc361386807157f7" + integrity sha512-Y9V4KODbmrOpqiOmCDVnPfMxMqKLOx8Hwcdn/r8mePq4yv7FSXGnxCs8/jZKO7zCB/IVPWihpJXwJNAIOEiZ2g== + dependencies: + "@types/node" "^18.11.18" + "@types/node-fetch" "^2.6.4" + abort-controller "^3.0.0" + agentkeepalive "^4.2.1" + form-data-encoder "1.7.2" + formdata-node "^4.3.2" + node-fetch "^2.6.7" + optionator@^0.9.3: version "0.9.4" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" @@ -5409,7 +5272,7 @@ prettier@3.3.2: resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.2.tgz#03ff86dc7c835f2d2559ee76876a3914cec4a90a" integrity sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA== -pretty-format@^29.7.0: +pretty-format@^29.0.0, pretty-format@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== @@ -5530,11 +5393,6 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== -resolve-pkg-maps@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" - integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== - resolve.exports@^2.0.0, resolve.exports@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" @@ -6124,21 +5982,6 @@ tslib@^2.2.0, tslib@^2.6.2: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== -tsx@4.15.6: - version "4.15.6" - resolved "https://registry.yarnpkg.com/tsx/-/tsx-4.15.6.tgz#4522ed093f7fa54f031a7a999274e8b35dbf3165" - integrity sha512-is0VQQlfNZRHEuSSTKA6m4xw74IU4AizmuB6lAYLRt9XtuyeQnyJYexhNZOPCB59SqC4JzmSzPnHGBXxf3k0hA== - dependencies: - esbuild "~0.21.4" - get-tsconfig "^4.7.5" - optionalDependencies: - fsevents "~2.3.3" - -tunnel@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" - integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== - type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" @@ -6244,7 +6087,7 @@ undici-types@~5.26.4: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -undici@^5.25.4, undici@^5.28.2: +undici@^5.28.2: version "5.28.4" resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.4.tgz#6b280408edb6a1a604a9b20340f45b422e373068" integrity sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g== @@ -6351,6 +6194,11 @@ wcwidth@^1.0.1: dependencies: defaults "^1.0.3" +web-streams-polyfill@4.0.0-beta.3: + version "4.0.0-beta.3" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz#2898486b74f5156095e473efe989dcf185047a38" + integrity sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug== + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" @@ -6479,7 +6327,7 @@ write-file-atomic@^4.0.2: imurmurhash "^0.1.4" signal-exit "^3.0.7" -ws@^8.11.0, ws@^8.14.2: +ws@^8.11.0: version "8.17.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.0.tgz#d145d18eca2ed25aaf791a183903f7be5e295fea" integrity sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==