diff --git a/.cspell.json b/.cspell.json index 83a383f..43040b8 100644 --- a/.cspell.json +++ b/.cspell.json @@ -22,7 +22,12 @@ "knip", "mischeck", "commentbody", - "issuebody" + "issuebody", + "voyageai", + "vectordump", + "payloadobject", + "markdownit", + "plpgsql" ], "dictionaries": ["typescript", "node", "software-terms"], "import": ["@cspell/dict-typescript/cspell-ext.json", "@cspell/dict-node/cspell-ext.json", "@cspell/dict-software-terms"], diff --git a/.dev.vars.example b/.dev.vars.example index 6ceaeff..2fc8cf8 100644 --- a/.dev.vars.example +++ b/.dev.vars.example @@ -1,3 +1,3 @@ SUPABASE_URL="" SUPABASE_KEY="" -OPENAI_API_KEY="" \ No newline at end of file +VOYAGEAI_API_KEY="" \ No newline at end of file diff --git a/.env.example b/.env.example index 0272714..6e8b18f 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,3 @@ SUPABASE_URL= SUPABASE_KEY= -OPENAI_API_KEY= \ No newline at end of file +VOYAGEAI_API_KEY= \ No newline at end of file diff --git a/.github/workflows/compute.yml b/.github/workflows/compute.yml index f83e80c..8cb2597 100644 --- a/.github/workflows/compute.yml +++ b/.github/workflows/compute.yml @@ -24,7 +24,7 @@ jobs: env: SUPABASE_URL: ${{ secrets.SUPABASE_URL }} SUPABASE_KEY: ${{ secrets.SUPABASE_KEY }} - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + VOYAGEAI_API_KEY: ${{secrets.VOYAGEAI_API_KEY}} steps: - uses: actions/checkout@v4 @@ -43,4 +43,4 @@ jobs: env: SUPABASE_URL: ${{ secrets.SUPABASE_URL }} SUPABASE_KEY: ${{ secrets.SUPABASE_KEY }} - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + VOYAGEAI_API_KEY: ${{secrets.VOYAGEAI_API_KEY}} diff --git a/.github/workflows/worker-delete.yml b/.github/workflows/worker-delete.yml new file mode 100644 index 0000000..89d3e23 --- /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 | 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..deafdc0 --- /dev/null +++ b/.github/workflows/worker-deploy.yml @@ -0,0 +1,52 @@ +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 | 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 "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: | + SUPABASE_URL + SUPABASE_KEY + VOYAGEAI_API_KEY + env: + SUPABASE_URL: ${{ secrets.SUPABASE_URL }} + SUPABASE_KEY: ${{ secrets.SUPABASE_KEY }} + VOYAGEAI_API_KEY: ${{ secrets.VOYAGEAI_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/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..bf0b82f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,29 @@ +# Changelog + +## 1.0.0 (2024-09-01) + + +### Features + +* modified tests ([97e267f](https://github.com/ubiquibot/issue-comment-embeddings/commit/97e267f801ce4e6bd29bbe967de3df4fc3b1942a)) + + +### Bug Fixes + +* added config.yml ([c0f784b](https://github.com/ubiquibot/issue-comment-embeddings/commit/c0f784b20e59c2c4714805331c7ae9034fd73f73)) +* added config.yml ([221d34d](https://github.com/ubiquibot/issue-comment-embeddings/commit/221d34d801af6ebd764028be4a5c6200a18b776e)) +* added config.yml ([d12c522](https://github.com/ubiquibot/issue-comment-embeddings/commit/d12c522291db36dcf6aea72e5759e1a055185d8f)) +* cspell fix ([736bea6](https://github.com/ubiquibot/issue-comment-embeddings/commit/736bea6172444fdf783ffff729879d8278ff82f3)) +* fixed tests missing supabase files ([0e870ac](https://github.com/ubiquibot/issue-comment-embeddings/commit/0e870ac50eb68249edf5fc4e46fd509425dd7bbb)) +* github workflow, types package.json, env examples ([16786d7](https://github.com/ubiquibot/issue-comment-embeddings/commit/16786d76ee7a598c885f15af1baeadcf6a471b2c)) +* issue_comments linting added issue_comments:edited, created and deleted ([9c0de23](https://github.com/ubiquibot/issue-comment-embeddings/commit/9c0de237048ce30bf4254960c443bf3938037dce)) +* knip workflow ([f325310](https://github.com/ubiquibot/issue-comment-embeddings/commit/f3253109c290c9fce6d14e6a2e1e328133ac6f81)) +* manifest.json, compute.yml ([21409d5](https://github.com/ubiquibot/issue-comment-embeddings/commit/21409d530c3aad6ff2676fc813314e5b29c1a533)) +* package.json ([806c6c0](https://github.com/ubiquibot/issue-comment-embeddings/commit/806c6c0b393a9b87741a6341fa65bc5b3d22cb15)) +* plugin name ([d91b991](https://github.com/ubiquibot/issue-comment-embeddings/commit/d91b991d717b7fb0b73359ca29ae6de08a1074b9)) +* readme.md ([9c5fbfe](https://github.com/ubiquibot/issue-comment-embeddings/commit/9c5fbfe9ca46eb842779468c85d329b9f941fb82)) +* readme.md ([2fec447](https://github.com/ubiquibot/issue-comment-embeddings/commit/2fec44786526e7c10faaa2c13c4349e1232cf5bd)) +* remove config.yml and wrangler.toml namespace entries ([127cc22](https://github.com/ubiquibot/issue-comment-embeddings/commit/127cc225903c3fe3ca934e8407df4eb9c27e378c)) +* removed config.yml changed name ([744e08c](https://github.com/ubiquibot/issue-comment-embeddings/commit/744e08cebac310ae81c3c102f5f3a9473e6e4b9e)) +* test and linting ([a4ee41e](https://github.com/ubiquibot/issue-comment-embeddings/commit/a4ee41e6fca8723ce2fddc96b1171c89cfe7d5b7)) +* wrangler name ([f890071](https://github.com/ubiquibot/issue-comment-embeddings/commit/f890071c01c5bb1d611a5b7aa07cba84f4546251)) diff --git a/README.md b/README.md index 0ee0bbe..9661b9c 100644 --- a/README.md +++ b/README.md @@ -7,15 +7,19 @@ This is a plugin for [Ubiquibot](https://github.com/ubiquity/ubiquibot-kernel). To set up the `.dev.vars` file, you will need to provide the following variables: - `SUPABASE_URL`: The URL for your Supabase instance. - `SUPABASE_KEY`: The key for your Supabase instance. -- `OPENAI_API_KEY`: The API key for OpenAI. +- `VOYAGEAI_API_KEY`: The API key for Voyage. ## Usage -- Add the following to your `.ubiquibot.config.yml` file with the appropriate URL: -```javascript - -plugin: http://127.0.0.1:4000 - runsOn: [ "issue_comment.created", "issue_comment.edited", "issue_comment.deleted" ] +- Add the following to your `.ubiquibot-config.yml` file with the appropriate URL: +```yaml +- plugin: ubiquity-os-marketplace/generate-vector-embeddings + with: + matchThreshold: 0.95 + warningThreshold: 0.75 + jobMatchingThreshold: 0.75 ``` + ## Testing Locally - Run `yarn install` to install the dependencies. - Run `yarn worker` to start the server. @@ -52,4 +56,4 @@ To set up the `.dev.vars` file, you will need to provide the following variables - Replace the placeholders with the appropriate values. ## Testing -- Run `yarn test` to run the tests. \ No newline at end of file +- Run `yarn test` to run the tests. diff --git a/eslint.config.mjs b/eslint.config.mjs index f43237a..f594657 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", "src/types/database.ts"], + ignores: [".github/knip.ts", "src/types/database.ts", "src/adapters/utils/markdown-to-plaintext.ts"], extends: [eslint.configs.recommended, ...tsEslint.configs.recommended, sonarjs.configs.recommended], languageOptions: { parser: tsEslint.parser, diff --git a/manifest.json b/manifest.json index 0e83faf..39df377 100644 --- a/manifest.json +++ b/manifest.json @@ -1,5 +1,5 @@ { - "name": "@ubiquity-os/comment-vector-embeddings", - "description": "Issue comment plugin for Ubiquibot. It enables the storage, updating, and deletion of issue comment embeddings.", - "ubiquity:listeners": ["issue_comment.created", "issue_comment.edited", "issue_comment.deleted"] + "name": "Generate vector embeddings", + "description": "Enables the storage, updating, and deletion of issue comment embeddings.", + "ubiquity:listeners": ["issue_comment.created", "issue_comment.edited", "issue_comment.deleted", "issues.opened", "issues.edited", "issues.deleted", "issues.labeled"] } diff --git a/package.json b/package.json index bb43997..54ad601 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "knip-ci": "knip --no-exit-code --reporter json --config .github/knip.ts", "prepare": "husky install", "test": "jest --setupFiles dotenv/config --coverage", - "worker": "wrangler dev --env dev --port 4000", + "worker": "wrangler dev --env dev --port 5000", "supabase:generate:local": "supabase gen types typescript --local > src/types/database.ts", "supabase:generate:remote": "cross-env-shell \"supabase gen types typescript --project-id $SUPABASE_PROJECT_ID --schema public > src/types/database.ts\"" }, @@ -35,10 +35,13 @@ "@octokit/webhooks": "13.2.7", "@sinclair/typebox": "0.32.33", "@supabase/supabase-js": "^2.45.2", + "@types/markdown-it": "^14.1.2", "@ubiquity-dao/ubiquibot-logger": "^1.3.0", "dotenv": "16.4.5", - "openai": "^4.56.0", - "typebox-validators": "0.3.5" + "markdown-it": "^14.1.0", + "markdown-it-plain-text": "^0.3.0", + "typebox-validators": "0.3.5", + "voyageai": "^0.0.1-5" }, "devDependencies": { "@commitlint/cli": "19.3.0", @@ -71,7 +74,7 @@ "tsx": "4.15.6", "typescript": "5.4.5", "typescript-eslint": "7.13.1", - "wrangler": "3.62.0" + "wrangler": "3.78.12" }, "lint-staged": { "*.ts": [ diff --git a/src/adapters/index.ts b/src/adapters/index.ts index e5f0346..ca31d27 100644 --- a/src/adapters/index.ts +++ b/src/adapters/index.ts @@ -2,19 +2,21 @@ import { SupabaseClient } from "@supabase/supabase-js"; import { Context } from "../types"; import { Comment } from "./supabase/helpers/comment"; import { SuperSupabase } from "./supabase/helpers/supabase"; -import { SuperOpenAi } from "./openai/helpers/openai"; -import OpenAI from "openai"; -import { Embedding } from "./openai/helpers/embedding"; +import { Embedding as VoyageEmbedding } from "./voyage/helpers/embedding"; +import { SuperVoyage } from "./voyage/helpers/voyage"; +import { VoyageAIClient } from "voyageai"; +import { Issues } from "./supabase/helpers/issues"; -export function createAdapters(supabaseClient: SupabaseClient, openai: OpenAI, context: Context) { +export function createAdapters(supabaseClient: SupabaseClient, voyage: VoyageAIClient, context: Context) { return { supabase: { comment: new Comment(supabaseClient, context), + issue: new Issues(supabaseClient, context), super: new SuperSupabase(supabaseClient, context), }, - openai: { - embedding: new Embedding(openai, context), - super: new SuperOpenAi(openai, context), + voyage: { + embedding: new VoyageEmbedding(voyage, context), + super: new SuperVoyage(voyage, context), }, }; } diff --git a/src/adapters/openai/helpers/embedding.ts b/src/adapters/openai/helpers/embedding.ts deleted file mode 100644 index 6b912dd..0000000 --- a/src/adapters/openai/helpers/embedding.ts +++ /dev/null @@ -1,25 +0,0 @@ -import OpenAI from "openai"; -import { Context } from "../../../types"; -import { SuperOpenAi } from "./openai"; -const VECTOR_SIZE = 3072; - -export class Embedding extends SuperOpenAi { - protected context: Context; - - constructor(client: OpenAI, context: Context) { - super(client, context); - this.context = context; - } - - async createEmbedding(text: string): Promise { - const params: OpenAI.EmbeddingCreateParams = { - model: "text-embedding-3-large", - input: text, - dimensions: VECTOR_SIZE, - }; - const response = await this.client.embeddings.create({ - ...params, - }); - return response.data[0]?.embedding; - } -} diff --git a/src/adapters/openai/helpers/openai.ts b/src/adapters/openai/helpers/openai.ts deleted file mode 100644 index 108a3d2..0000000 --- a/src/adapters/openai/helpers/openai.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { OpenAI } from "openai"; -import { Context } from "../../../types/context"; - -export class SuperOpenAi { - protected client: OpenAI; - protected context: Context; - - constructor(client: OpenAI, context: Context) { - this.client = client; - this.context = context; - } -} diff --git a/src/adapters/supabase/helpers/comment.ts b/src/adapters/supabase/helpers/comment.ts index 7530c10..3fa08b2 100644 --- a/src/adapters/supabase/helpers/comment.ts +++ b/src/adapters/supabase/helpers/comment.ts @@ -1,10 +1,14 @@ import { SupabaseClient } from "@supabase/supabase-js"; import { SuperSupabase } from "./supabase"; import { Context } from "../../../types/context"; +import { markdownToPlainText } from "../../utils/markdown-to-plaintext"; export interface CommentType { - id: number; - body: string; + id: string; + markdown?: string; + author_id: number; + created_at: string; + modified_at: string; embedding: number[]; } @@ -13,9 +17,16 @@ export class Comment extends SuperSupabase { super(supabase, context); } - async createComment(commentBody: string, commentId: number, issueBody: string) { + async createComment( + markdown: string | null, + commentNodeId: string, + authorId: number, + payload: Record | null, + isPrivate: boolean, + issueId: string + ) { //First Check if the comment already exists - const { data, error } = await this.supabase.from("issue_comments").select("*").eq("id", commentId); + const { data, error } = await this.supabase.from("issue_comments").select("*").eq("id", commentNodeId); if (error) { this.context.logger.error("Error creating comment", error); return; @@ -25,10 +36,16 @@ export class Comment extends SuperSupabase { return; } else { //Create the embedding for this comment - const embedding = await this.context.adapters.openai.embedding.createEmbedding(commentBody); + const embedding = await this.context.adapters.voyage.embedding.createEmbedding(markdown); + let plaintext: string | null = markdownToPlainText(markdown || ""); + if (isPrivate) { + markdown = null as string | null; + payload = null as Record | null; + plaintext = null as string | null; + } const { error } = await this.supabase .from("issue_comments") - .insert([{ id: commentId, commentbody: commentBody, issuebody: issueBody, embedding: embedding }]); + .insert([{ id: commentNodeId, markdown, plaintext, author_id: authorId, payload, embedding: embedding, issue_id: issueId }]); if (error) { this.context.logger.error("Error creating comment", error); return; @@ -37,25 +54,34 @@ export class Comment extends SuperSupabase { this.context.logger.info("Comment created successfully"); } - async updateComment(commentBody: string, commentId: number) { + async updateComment(markdown: string | null, commentNodeId: string, payload: Record | null, isPrivate: boolean) { //Create the embedding for this comment - const embedding = Array.from(await this.context.adapters.openai.embedding.createEmbedding(commentBody)); - const { error } = await this.supabase.from("issue_comments").update({ commentbody: commentBody, embedding: embedding }).eq("id", commentId); + const embedding = Array.from(await this.context.adapters.voyage.embedding.createEmbedding(markdown)); + let plaintext: string | null = markdownToPlainText(markdown || ""); + if (isPrivate) { + markdown = null as string | null; + payload = null as Record | null; + plaintext = null as string | null; + } + const { error } = await this.supabase + .from("issue_comments") + .update({ markdown, plaintext, embedding: embedding, payload, modified_at: new Date() }) + .eq("id", commentNodeId); if (error) { this.context.logger.error("Error updating comment", error); } } - async getComment(commentId: number): Promise { - const { data, error } = await this.supabase.from("issue_comments").select("*").eq("id", commentId); + async getComment(commentNodeId: string): Promise { + const { data, error } = await this.supabase.from("issue_comments").select("*").eq("id", commentNodeId); if (error) { this.context.logger.error("Error getting comment", error); } return data; } - async deleteComment(commentId: number) { - const { error } = await this.supabase.from("issue_comments").delete().eq("id", commentId); + async deleteComment(commentNodeId: string) { + const { error } = await this.supabase.from("issue_comments").delete().eq("id", commentNodeId); if (error) { this.context.logger.error("Error deleting comment", error); } diff --git a/src/adapters/supabase/helpers/issues.ts b/src/adapters/supabase/helpers/issues.ts new file mode 100644 index 0000000..6bfef09 --- /dev/null +++ b/src/adapters/supabase/helpers/issues.ts @@ -0,0 +1,113 @@ +import { SupabaseClient } from "@supabase/supabase-js"; +import { SuperSupabase } from "./supabase"; +import { Context } from "../../../types/context"; +import { markdownToPlainText } from "../../utils/markdown-to-plaintext"; + +export interface IssueSimilaritySearchResult { + issue_id: string; + issue_plaintext: string; + similarity: number; +} + +export interface IssueType { + id: string; + markdown?: string; + plaintext?: string; + payload?: Record; + author_id: number; + created_at: string; + modified_at: string; + embedding: number[]; +} + +export class Issues extends SuperSupabase { + constructor(supabase: SupabaseClient, context: Context) { + super(supabase, context); + } + + async createIssue(issueNodeId: string, payload: Record | null, isPrivate: boolean, markdown: string | null, authorId: number) { + //First Check if the issue already exists + const { data, error } = await this.supabase.from("issues").select("*").eq("id", issueNodeId); + if (error) { + this.context.logger.error("Error creating issue", error); + return; + } + if (data && data.length > 0) { + this.context.logger.info("Issue already exists"); + return; + } else { + const embedding = await this.context.adapters.voyage.embedding.createEmbedding(markdown); + let plaintext: string | null = markdownToPlainText(markdown || ""); + if (isPrivate) { + payload = null; + markdown = null; + plaintext = null; + } + const { error } = await this.supabase.from("issues").insert([{ id: issueNodeId, payload, markdown, plaintext, author_id: authorId, embedding }]); + if (error) { + this.context.logger.error("Error creating issue", error); + return; + } + } + this.context.logger.info("Issue created successfully"); + } + + async updateIssue(markdown: string | null, issueNodeId: string, payload: Record | null, isPrivate: boolean) { + //Create the embedding for this comment + const embedding = Array.from(await this.context.adapters.voyage.embedding.createEmbedding(markdown)); + let plaintext: string | null = markdownToPlainText(markdown || ""); + if (isPrivate) { + markdown = null as string | null; + payload = null as Record | null; + plaintext = null as string | null; + } + const { error } = await this.supabase + .from("issues") + .update({ markdown, plaintext, embedding: embedding, payload, modified_at: new Date() }) + .eq("id", issueNodeId); + if (error) { + this.context.logger.error("Error updating comment", error); + } + } + + async deleteIssue(issueNodeId: string) { + const { error } = await this.supabase.from("issues").delete().eq("id", issueNodeId); + if (error) { + this.context.logger.error("Error deleting comment", error); + } + } + + async getIssue(issueNodeId: string): Promise { + const { data, error } = await this.supabase + .from("issues") // Provide the second type argument + .select("*") + .eq("id", issueNodeId) + .returns(); + if (error) { + this.context.logger.error("Error getting issue", error); + return null; + } + return data; + } + + async findSimilarIssues(markdown: string, threshold: number, currentId: string): Promise { + const embedding = await this.context.adapters.voyage.embedding.createEmbedding(markdown); + const { data, error } = await this.supabase.rpc("find_similar_issues", { + current_id: currentId, + query_embedding: embedding, + threshold: threshold, + }); + if (error) { + this.context.logger.error("Error finding similar issues", error); + return []; + } + return data; + } + + async updatePayload(issueNodeId: string, payload: Record) { + const { error } = await this.supabase.from("issues").update({ payload }).eq("id", issueNodeId); + if (error) { + this.context.logger.error("Error updating issue payload", error); + } + } +} diff --git a/src/adapters/utils/markdown-to-plaintext.ts b/src/adapters/utils/markdown-to-plaintext.ts new file mode 100644 index 0000000..93e5787 --- /dev/null +++ b/src/adapters/utils/markdown-to-plaintext.ts @@ -0,0 +1,18 @@ +import markdownit from "markdown-it"; +import plainTextPlugin from "markdown-it-plain-text"; + +/** + * Converts a Markdown string to plain text. + * @param markdown + * @returns + */ +export function markdownToPlainText(markdown: string | null): string | null { + if (!markdown) { + return markdown; + } + const md = markdownit(); + md.use(plainTextPlugin); + md.render(markdown); + //Package markdown-it-plain-text does not have types + return (md as any).plainText; +} diff --git a/src/adapters/voyage/helpers/embedding.ts b/src/adapters/voyage/helpers/embedding.ts new file mode 100644 index 0000000..575543e --- /dev/null +++ b/src/adapters/voyage/helpers/embedding.ts @@ -0,0 +1,25 @@ +import { VoyageAIClient } from "voyageai"; +import { Context } from "../../../types"; +import { SuperVoyage } from "./voyage"; +const VECTOR_SIZE = 1024; + +export class Embedding extends SuperVoyage { + protected context: Context; + + constructor(client: VoyageAIClient, context: Context) { + super(client, context); + this.context = context; + } + + async createEmbedding(text: string | null): Promise { + if (text === null) { + return new Array(VECTOR_SIZE).fill(0); + } else { + const response = await this.client.embed({ + input: text, + model: "voyage-large-2-instruct", + }); + return (response.data && response.data[0]?.embedding) || []; + } + } +} diff --git a/src/adapters/voyage/helpers/voyage.ts b/src/adapters/voyage/helpers/voyage.ts new file mode 100644 index 0000000..c08c0af --- /dev/null +++ b/src/adapters/voyage/helpers/voyage.ts @@ -0,0 +1,12 @@ +import { VoyageAIClient } from "voyageai"; +import { Context } from "../../../types/context"; + +export class SuperVoyage { + protected client: VoyageAIClient; + protected context: Context; + + constructor(client: VoyageAIClient, context: Context) { + this.client = client; + this.context = context; + } +} diff --git a/src/handlers/add-comments.ts b/src/handlers/add-comments.ts index 35bd387..54745a1 100644 --- a/src/handlers/add-comments.ts +++ b/src/handlers/add-comments.ts @@ -1,25 +1,23 @@ import { Context } from "../types"; +import { CommentPayload } from "../types/payload"; export async function addComments(context: Context) { const { logger, - payload, adapters: { supabase }, } = context; + const { payload } = context as { payload: CommentPayload }; + const markdown = payload.comment.body; + const authorId = payload.comment.user?.id || -1; + const nodeId = payload.comment.node_id; + const isPrivate = payload.repository.private; + const issueId = payload.issue.node_id; - const sender = payload.comment.user?.login; - const repo = payload.repository.name; - const issueNumber = payload.issue.number; - const issueBody = payload.issue.body || ""; - const owner = payload.repository.owner.login; - const body = payload.comment.body; - - // Log the payload - logger.info(`Executing addComments:`, { sender, repo, issueNumber, owner }); - - // Add the comment to the database try { - await supabase.comment.createComment(body, payload.comment.id, issueBody); + if (!markdown) { + throw new Error("Comment body is empty"); + } + await supabase.comment.createComment(markdown, nodeId, authorId, payload, isPrivate, issueId); } catch (error) { if (error instanceof Error) { logger.error(`Error creating comment:`, { error: error, stack: error.stack }); @@ -31,5 +29,5 @@ export async function addComments(context: Context) { } logger.ok(`Successfully created comment!`); - logger.verbose(`Exiting addComments`); + logger.debug(`Exiting addComments`); } diff --git a/src/handlers/add-issue.ts b/src/handlers/add-issue.ts new file mode 100644 index 0000000..969a5c2 --- /dev/null +++ b/src/handlers/add-issue.ts @@ -0,0 +1,32 @@ +import { Context } from "../types"; +import { IssuePayload } from "../types/payload"; + +export async function addIssue(context: Context) { + const { + logger, + adapters: { supabase }, + } = context; + const { payload } = context as { payload: IssuePayload }; + const markdown = payload.issue.body + " " + payload.issue.title || null; + const authorId = payload.issue.user?.id || -1; + const nodeId = payload.issue.node_id; + const isPrivate = payload.repository.private; + + try { + if (!markdown) { + throw new Error("Issue body is empty"); + } + await supabase.issue.createIssue(nodeId, payload, isPrivate, markdown, authorId); + } catch (error) { + if (error instanceof Error) { + logger.error(`Error creating issue:`, { error: error, stack: error.stack }); + throw error; + } else { + logger.error(`Error creating issue:`, { err: error, error: new Error() }); + throw error; + } + } + + logger.ok(`Successfully created issue!`); + logger.debug(`Exiting addIssue`); +} diff --git a/src/handlers/delete-comments.ts b/src/handlers/delete-comments.ts index 7e9842f..8c9e394 100644 --- a/src/handlers/delete-comments.ts +++ b/src/handlers/delete-comments.ts @@ -1,23 +1,16 @@ import { Context } from "../types"; +import { CommentPayload } from "../types/payload"; export async function deleteComment(context: Context) { const { logger, - payload, adapters: { supabase }, } = context; + const { payload } = context as { payload: CommentPayload }; + const nodeId = payload.comment.node_id; - const sender = payload.comment.user?.login; - const repo = payload.repository.name; - const issueNumber = payload.issue.number; - const owner = payload.repository.owner.login; - - // Log the payload - logger.debug(`Executing deleteComment:`, { sender, repo, issueNumber, owner }); - - // Add the comment to the database try { - await supabase.comment.deleteComment(payload.comment.id); + await supabase.comment.deleteComment(nodeId); } catch (error) { if (error instanceof Error) { logger.error(`Error deleting comment:`, { error: error, stack: error.stack }); @@ -29,5 +22,5 @@ export async function deleteComment(context: Context) { } logger.ok(`Successfully deleted comment!`); - logger.verbose(`Exiting deleteComments`); + logger.debug(`Exiting deleteComments`); } diff --git a/src/handlers/delete-issue.ts b/src/handlers/delete-issue.ts new file mode 100644 index 0000000..b392c69 --- /dev/null +++ b/src/handlers/delete-issue.ts @@ -0,0 +1,26 @@ +import { Context } from "../types"; +import { IssuePayload } from "../types/payload"; + +export async function deleteIssues(context: Context) { + const { + logger, + adapters: { supabase }, + } = context; + const { payload } = context as { payload: IssuePayload }; + const nodeId = payload.issue.node_id; + + try { + await supabase.issue.deleteIssue(nodeId); + } catch (error) { + if (error instanceof Error) { + logger.error(`Error deleting issue:`, { error: error, stack: error.stack }); + throw error; + } else { + logger.error(`Error deleting issue:`, { err: error, error: new Error() }); + throw error; + } + } + + logger.ok(`Successfully deleted issue!`); + logger.debug(`Exiting deleteIssue`); +} diff --git a/src/handlers/issue-deduplication.ts b/src/handlers/issue-deduplication.ts new file mode 100644 index 0000000..6174e4f --- /dev/null +++ b/src/handlers/issue-deduplication.ts @@ -0,0 +1,110 @@ +import { IssueSimilaritySearchResult } from "../adapters/supabase/helpers/issues"; +import { Context } from "../types"; +import { IssuePayload } from "../types/payload"; + +export interface IssueGraphqlResponse { + node: { + title: string; + url: string; + }; + similarity: string; +} + +/** + * Check if an issue is similar to any existing issues in the database + * @param context + * @returns true if the issue is similar to an existing issue, false otherwise + */ +export async function issueChecker(context: Context): Promise { + const { + logger, + adapters: { supabase }, + octokit, + } = context; + const { payload } = context as { payload: IssuePayload }; + const issue = payload.issue; + const issueContent = issue.body + issue.title; + + // Fetch all similar issues based on settings.warningThreshold + const similarIssues = await supabase.issue.findSimilarIssues(issueContent, context.config.warningThreshold, issue.node_id); + if (similarIssues && similarIssues.length > 0) { + const matchIssues = similarIssues.filter((issue) => issue.similarity >= context.config.matchThreshold); + + // Handle issues that match the MATCH_THRESHOLD (Very Similar) + if (matchIssues.length > 0) { + logger.info(`Similar issue which matches more than ${context.config.matchThreshold} already exists`); + await octokit.issues.update({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + issue_number: issue.number, + state: "closed", + state_reason: "not_planned", + }); + } + + // Handle issues that match the settings.warningThreshold but not the MATCH_THRESHOLD + if (similarIssues.length > 0) { + logger.info(`Similar issue which matches more than ${context.config.warningThreshold} already exists`); + await handleSimilarIssuesComment(context, payload, issue.number, similarIssues); + return true; + } + } + + return false; +} + +/** + * Handle commenting on an issue with similar issues information + * @param context + * @param payload + * @param issueNumber + * @param similarIssues + */ +async function handleSimilarIssuesComment(context: Context, payload: IssuePayload, issueNumber: number, similarIssues: IssueSimilaritySearchResult[]) { + const issueList: IssueGraphqlResponse[] = await Promise.all( + similarIssues.map(async (issue: IssueSimilaritySearchResult) => { + const issueUrl: IssueGraphqlResponse = await context.octokit.graphql( + `query($issueNodeId: ID!) { + node(id: $issueNodeId) { + ... on Issue { + title + url + } + } + }`, + { issueNodeId: issue.issue_id } + ); + issueUrl.similarity = (issue.similarity * 100).toFixed(2); + return issueUrl; + }) + ); + + const commentBody = issueList.map((issue) => `- [${issue.node.title}](${issue.node.url}) Similarity: ${issue.similarity}`).join("\n"); + const body = `This issue seems to be similar to the following issue(s):\n\n${commentBody}`; + + const existingComments = await context.octokit.issues.listComments({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + issue_number: issueNumber, + }); + + const existingComment = existingComments.data.find( + (comment) => comment.body && comment.body.includes("This issue seems to be similar to the following issue(s)") + ); + + if (existingComment) { + await context.octokit.issues.updateComment({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + comment_id: existingComment.id, + body: body, + }); + } else { + await context.octokit.issues.createComment({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + issue_number: issueNumber, + body: body, + }); + } +} diff --git a/src/handlers/issue-matching.ts b/src/handlers/issue-matching.ts new file mode 100644 index 0000000..cc1d060 --- /dev/null +++ b/src/handlers/issue-matching.ts @@ -0,0 +1,150 @@ +import { Context } from "../types"; +import { IssuePayload } from "../types/payload"; + +export interface IssueGraphqlResponse { + node: { + title: string; + url: string; + state: string; + stateReason: string; + closed: boolean; + repository: { + owner: { + login: string; + }; + name: string; + }; + assignees: { + nodes: Array<{ + login: string; + url: string; + }>; + }; + }; + similarity: number; +} + +const commentBuilder = (matchResultArray: Map>): string => { + const commentLines: string[] = [">[!NOTE]", ">The following contributors may be suitable for this task:"]; + matchResultArray.forEach((issues, assignee) => { + commentLines.push(`>### [${assignee}](https://www.github.com/${assignee})`); + issues.forEach((issue) => { + commentLines.push(issue); + }); + }); + return commentLines.join("\n"); +}; + +export async function issueMatching(context: Context) { + const { + logger, + adapters: { supabase }, + octokit, + } = context; + const { payload } = context as { payload: IssuePayload }; + const issue = payload.issue; + const issueContent = issue.body + issue.title; + const commentStart = ">The following contributors may be suitable for this task:"; + + // On Adding the labels to the issue, the bot should + // create a new comment with users who completed task most similar to the issue + // if the comment already exists, it should update the comment with the new users + const matchResultArray: Map> = new Map(); + const similarIssues = await supabase.issue.findSimilarIssues(issueContent, context.config.jobMatchingThreshold, issue.node_id); + if (similarIssues && similarIssues.length > 0) { + // Find the most similar issue and the users who completed the task + similarIssues.sort((a, b) => b.similarity - a.similarity); + const fetchPromises = similarIssues.map(async (issue) => { + const issueObject: IssueGraphqlResponse = await context.octokit.graphql( + `query ($issueNodeId: ID!) { + node(id: $issueNodeId) { + ... on Issue { + title + url + state + repository{ + name + owner { + login + } + } + stateReason + closed + assignees(first: 10) { + nodes { + login + url + } + } + } + } + }`, + { issueNodeId: issue.issue_id } + ); + issueObject.similarity = issue.similarity; + return issueObject; + }); + + const issueList = await Promise.all(fetchPromises); + issueList.forEach((issue) => { + if (issue.node.closed && issue.node.stateReason === "COMPLETED" && issue.node.assignees.nodes.length > 0) { + const assignees = issue.node.assignees.nodes; + assignees.forEach((assignee) => { + const similarityPercentage = Math.round(issue.similarity * 100); + const issueLink = issue.node.url.replace(/https?:\/\/github.com/, "https://www.github.com"); + if (matchResultArray.has(assignee.login)) { + matchResultArray + .get(assignee.login) + ?.push( + `> \`${similarityPercentage}% Match\` [${issue.node.repository.owner.login}/${issue.node.repository.name}#${issue.node.url.split("/").pop()}](${issueLink})` + ); + } else { + matchResultArray.set(assignee.login, [ + `> \`${similarityPercentage}% Match\` [${issue.node.repository.owner.login}/${issue.node.repository.name}#${issue.node.url.split("/").pop()}](${issueLink})`, + ]); + } + }); + } + }); + // Fetch if any previous comment exists + const listIssues = await octokit.issues.listComments({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + issue_number: issue.number, + }); + //Check if the comment already exists + const existingComment = listIssues.data.find((comment) => comment.body && comment.body.includes(">[!NOTE]" + "\n" + commentStart)); + //Check if matchResultArray is empty + if (matchResultArray && matchResultArray.size === 0) { + if (existingComment) { + // If the comment already exists, delete it + await octokit.issues.deleteComment({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + comment_id: existingComment.id, + }); + } + logger.debug("No similar issues found"); + return; + } + const comment = commentBuilder(matchResultArray); + if (existingComment) { + await context.octokit.issues.updateComment({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + comment_id: existingComment.id, + body: comment, + }); + } else { + await context.octokit.issues.createComment({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + issue_number: payload.issue.number, + body: comment, + }); + } + } + + logger.ok(`Successfully created issue comment!`); + logger.debug(`Exiting issueMatching handler`); +} diff --git a/src/handlers/update-comments.ts b/src/handlers/update-comments.ts index b0c3142..b1b9d18 100644 --- a/src/handlers/update-comments.ts +++ b/src/handlers/update-comments.ts @@ -1,24 +1,21 @@ import { Context } from "../types"; +import { CommentPayload } from "../types/payload"; export async function updateComment(context: Context) { const { logger, - payload, adapters: { supabase }, } = context; - - const sender = payload.comment.user?.login; - const repo = payload.repository.name; - const issueNumber = payload.issue.number; - const owner = payload.repository.owner.login; - const body = payload.comment.body; - - // Log the payload - logger.debug(`Executing updateComment:`, { sender, repo, issueNumber, owner }); - + const { payload } = context as { payload: CommentPayload }; + const nodeId = payload.comment.node_id; + const isPrivate = payload.repository.private; + const markdown = payload.comment.body || null; // Fetch the previous comment and update it in the db try { - await supabase.comment.updateComment(body, payload.comment.id); + if (!markdown) { + throw new Error("Comment body is empty"); + } + await supabase.comment.updateComment(markdown, nodeId, payload, isPrivate); } catch (error) { if (error instanceof Error) { logger.error(`Error updating comment:`, { error: error, stack: error.stack }); @@ -30,5 +27,5 @@ export async function updateComment(context: Context) { } logger.ok(`Successfully updated comment!`); - logger.verbose(`Exiting updateComment`); + logger.debug(`Exiting updateComment`); } diff --git a/src/handlers/update-issue.ts b/src/handlers/update-issue.ts new file mode 100644 index 0000000..763b2ba --- /dev/null +++ b/src/handlers/update-issue.ts @@ -0,0 +1,32 @@ +import { Context } from "../types"; +import { IssuePayload } from "../types/payload"; + +export async function updateIssue(context: Context) { + const { + logger, + adapters: { supabase }, + } = context; + const { payload } = context as { payload: IssuePayload }; + const payloadObject = payload; + const nodeId = payload.issue.node_id; + const isPrivate = payload.repository.private; + const markdown = payload.issue.body + " " + payload.issue.title || null; + // Fetch the previous issue and update it in the db + try { + if (!markdown) { + throw new Error("Issue body is empty"); + } + await supabase.issue.updateIssue(markdown, nodeId, payloadObject, isPrivate); + } catch (error) { + if (error instanceof Error) { + logger.error(`Error updating issue:`, { error: error, stack: error.stack }); + throw error; + } else { + logger.error(`Error updating issue:`, { err: error, error: new Error() }); + throw error; + } + } + + logger.ok(`Successfully updated issue!`); + logger.debug(`Exiting updateIssue`); +} diff --git a/src/main.ts b/src/main.ts index 34f3e2f..23a3e74 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,7 +10,7 @@ import { plugin } from "./plugin"; */ export async function run() { const payload = github.context.payload.inputs; - const env = Value.Decode(envSchema, payload.env); + const env = Value.Decode(envSchema, process.env); const settings = Value.Decode(pluginSettingsSchema, Value.Default(pluginSettingsSchema, JSON.parse(payload.settings))); if (!pluginSettingsValidator.test(settings)) { diff --git a/src/plugin.ts b/src/plugin.ts index 4197ff6..197948b 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,7 +1,7 @@ import { Octokit } from "@octokit/rest"; import { Env, PluginInputs } from "./types"; import { Context } from "./types"; -import { isIssueCommentEvent } from "./types/typeguards"; +import { isIssueCommentEvent, isIssueEvent } from "./types/typeguards"; import { LogLevel, Logs } from "@ubiquity-dao/ubiquibot-logger"; import { Database } from "./types/database"; import { createAdapters } from "./adapters"; @@ -9,7 +9,12 @@ import { createClient } from "@supabase/supabase-js"; import { addComments } from "./handlers/add-comments"; import { updateComment } from "./handlers/update-comments"; import { deleteComment } from "./handlers/delete-comments"; -import OpenAI from "openai"; +import { VoyageAIClient } from "voyageai"; +import { deleteIssues } from "./handlers/delete-issue"; +import { addIssue } from "./handlers/add-issue"; +import { updateIssue } from "./handlers/update-issue"; +import { issueChecker } from "./handlers/issue-deduplication"; +import { issueMatching } from "./handlers/issue-matching"; /** * The main plugin function. Split for easier testing. @@ -25,6 +30,21 @@ export async function runPlugin(context: Context) { case "issue_comment.edited": return await updateComment(context); } + } else if (isIssueEvent(context)) { + switch (eventName) { + case "issues.opened": + await issueChecker(context); + await addIssue(context); + return await issueMatching(context); + case "issues.edited": + await issueChecker(context); + await updateIssue(context); + return await issueMatching(context); + case "issues.deleted": + return await deleteIssues(context); + } + } else if (eventName == "issues.labeled") { + return await issueMatching(context); } else { logger.error(`Unsupported event: ${eventName}`); } @@ -37,8 +57,8 @@ export async function runPlugin(context: Context) { 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 openaiClient = new OpenAI({ - apiKey: env.OPENAI_API_KEY, + const voyageClient = new VoyageAIClient({ + apiKey: env.VOYAGEAI_API_KEY, }); const context: Context = { eventName: inputs.eventName, @@ -49,6 +69,6 @@ export async function plugin(inputs: PluginInputs, env: Env) { logger: new Logs("info" as LogLevel), adapters: {} as ReturnType, }; - context.adapters = createAdapters(supabase, openaiClient, context); - return runPlugin(context); + context.adapters = createAdapters(supabase, voyageClient, context); + return await runPlugin(context); } diff --git a/src/types/context.ts b/src/types/context.ts index d7e3cff..b11ac2f 100644 --- a/src/types/context.ts +++ b/src/types/context.ts @@ -10,7 +10,14 @@ import { createAdapters } from "../adapters"; * * ubiquity:listeners: ["issue_comment.created", ...] */ -export type SupportedEventsU = "issue_comment.created" | "issue_comment.deleted" | "issue_comment.edited"; +export type SupportedEventsU = + | "issue_comment.created" + | "issue_comment.deleted" + | "issue_comment.edited" + | "issues.opened" + | "issues.edited" + | "issues.deleted" + | "issues.labeled"; export type SupportedEvents = { [K in SupportedEventsU]: K extends WebhookEventName ? WebhookEvent : never; diff --git a/src/types/database.ts b/src/types/database.ts index 02b8d2d..e7e193e 100644 --- a/src/types/database.ts +++ b/src/types/database.ts @@ -5,22 +5,81 @@ export type Database = { Tables: { issue_comments: { Row: { - commentbody: string; + author_id: string; + created_at: string; embedding: string; - id: number; - issuebody: string | null; + id: string; + issue_id: string | null; + markdown: string | null; + modified_at: string; + payloadobject: Json | null; + plaintext: string | null; + type: string; }; Insert: { - commentbody: string; + author_id: string; + created_at?: string; embedding: string; - id: number; - issuebody?: string | null; + id: string; + issue_id?: string | null; + markdown?: string | null; + modified_at?: string; + payloadobject?: Json | null; + plaintext?: string | null; + type: string; + }; + Update: { + author_id?: string; + created_at?: string; + embedding?: string; + id?: string; + issue_id?: string | null; + markdown?: string | null; + modified_at?: string; + payloadobject?: Json | null; + plaintext?: string | null; + type?: string; + }; + Relationships: [ + { + foreignKeyName: "issue_comments_issue_id_fkey"; + columns: ["issue_id"]; + isOneToOne: false; + referencedRelation: "issues"; + referencedColumns: ["id"]; + }, + ]; + }; + issues: { + Row: { + created_at: string; + embedding: string; + id: string; + markdown: string | null; + modified_at: string; + payload: Json | null; + plaintext: string | null; + type: string; + }; + Insert: { + created_at?: string; + embedding: string; + id: string; + markdown?: string | null; + modified_at?: string; + payload?: Json | null; + plaintext?: string | null; + type?: string; }; Update: { - commentbody?: string; + created_at?: string; embedding?: string; - id?: number; - issuebody?: string | null; + id?: string; + markdown?: string | null; + modified_at?: string; + payload?: Json | null; + plaintext?: string | null; + type?: string; }; Relationships: []; }; @@ -42,6 +101,17 @@ export type Database = { }; Returns: unknown; }; + find_similar_issues: { + Args: { + query_embedding: string; + threshold: number; + }; + Returns: { + id: number; + issue: string; + similarity: number; + }[]; + }; halfvec_avg: { Args: { "": number[]; diff --git a/src/types/env.ts b/src/types/env.ts index 6f47748..4c6cc1c 100644 --- a/src/types/env.ts +++ b/src/types/env.ts @@ -13,7 +13,7 @@ import { StandardValidator } from "typebox-validators"; export const envSchema = T.Object({ SUPABASE_URL: T.String(), SUPABASE_KEY: T.String(), - OPENAI_API_KEY: T.String(), + VOYAGEAI_API_KEY: T.String(), }); export const envValidator = new StandardValidator(envSchema); diff --git a/src/types/payload.ts b/src/types/payload.ts new file mode 100644 index 0000000..395fa09 --- /dev/null +++ b/src/types/payload.ts @@ -0,0 +1,3 @@ +import { EmitterWebhookEvent as WebhookEvent } from "@octokit/webhooks"; +export type CommentPayload = WebhookEvent<"issue_comment">["payload"]; +export type IssuePayload = WebhookEvent<"issues">["payload"]; diff --git a/src/types/plugin-inputs.ts b/src/types/plugin-inputs.ts index 77a40f9..226beee 100644 --- a/src/types/plugin-inputs.ts +++ b/src/types/plugin-inputs.ts @@ -18,7 +18,14 @@ export interface PluginInputs { return context.eventName === "issue_comment.created" || context.eventName === "issue_comment.deleted" || context.eventName === "issue_comment.edited"; } + +/** + * Restricts the scope of `context` to the `issues.opened`, `issues.edited`, and `issues.deleted` payloads. + * + * @param context The context object. + */ +export function isIssueEvent(context: Context): context is Context<"issues.opened" | "issues.edited" | "issues.deleted"> { + return context.eventName === "issues.opened" || context.eventName === "issues.edited" || context.eventName === "issues.deleted"; +} diff --git a/supabase/migrations/20240908231044_issue_comments.sql b/supabase/migrations/20240908231044_issue_comments.sql new file mode 100644 index 0000000..51afb87 --- /dev/null +++ b/supabase/migrations/20240908231044_issue_comments.sql @@ -0,0 +1,15 @@ +-- Create the extension if it doesn't exist +CREATE EXTENSION IF NOT EXISTS vector; + +-- Drop the old issue_comments table +DROP TABLE IF EXISTS issue_comments; + +-- Create the issue_comments table +CREATE TABLE IF NOT EXISTS issue_comments ( + id VARCHAR PRIMARY KEY, + created_at TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + modified_at TIMESTAMP WITH TIME ZONE NOT NULL, + author_id VARCHAR NOT NULL, + plaintext TEXT NOT NULL, + embedding VECTOR(3072) NOT NULL +); \ No newline at end of file diff --git a/supabase/migrations/20240908235818_issue_comments.sql b/supabase/migrations/20240908235818_issue_comments.sql new file mode 100644 index 0000000..11eb77a --- /dev/null +++ b/supabase/migrations/20240908235818_issue_comments.sql @@ -0,0 +1,2 @@ +ALTER TABLE issue_comments +ALTER COLUMN modified_at SET DEFAULT current_timestamp; \ No newline at end of file diff --git a/supabase/migrations/20240909072603_issue_comments.sql b/supabase/migrations/20240909072603_issue_comments.sql new file mode 100644 index 0000000..93de8ff --- /dev/null +++ b/supabase/migrations/20240909072603_issue_comments.sql @@ -0,0 +1,2 @@ +ALTER TABLE issue_comments +ALTER COLUMN plaintext DROP NOT NULL; \ No newline at end of file diff --git a/supabase/migrations/20240911023641_issue_comments.sql b/supabase/migrations/20240911023641_issue_comments.sql new file mode 100644 index 0000000..4ef319b --- /dev/null +++ b/supabase/migrations/20240911023641_issue_comments.sql @@ -0,0 +1,2 @@ +ALTER TABLE issue_comments +ADD COLUMN commentObject jsonb; \ No newline at end of file diff --git a/supabase/migrations/20240911104515_issue_comments.sql b/supabase/migrations/20240911104515_issue_comments.sql new file mode 100644 index 0000000..b0b2904 --- /dev/null +++ b/supabase/migrations/20240911104515_issue_comments.sql @@ -0,0 +1,5 @@ +ALTER TABLE issue_comments +DROP COLUMN embedding; + +ALTER TABLE issue_comments +ADD COLUMN embedding Vector(1024) NOT NULL; \ No newline at end of file diff --git a/supabase/migrations/20240911233824_issue_comments.sql b/supabase/migrations/20240911233824_issue_comments.sql new file mode 100644 index 0000000..c6ef4eb --- /dev/null +++ b/supabase/migrations/20240911233824_issue_comments.sql @@ -0,0 +1,8 @@ +ALTER TABLE issue_comments +RENAME COLUMN commentObject TO payloadObject; + +ALTER TABLE issue_comments +ADD COLUMN type Text NOT NULL; + +ALTER TABLE issue_comments +RENAME TO vectorDump; diff --git a/supabase/migrations/20240912225853_issue_comments.sql b/supabase/migrations/20240912225853_issue_comments.sql new file mode 100644 index 0000000..0d71cab --- /dev/null +++ b/supabase/migrations/20240912225853_issue_comments.sql @@ -0,0 +1,23 @@ +ALTER TABLE vectordump +RENAME TO issue_comments; + +CREATE TABLE IF NOT EXISTS issues ( + id VARCHAR primary key, + plaintext text, + embedding Vector(1024) not null, + payload jsonb, + author_id VARCHAR not null, + created_at timestamptz not null default now(), + modified_at timestamptz not null default now() +); + +ALTER TABLE issue_comments +ADD COLUMN issue_id VARCHAR +REFERENCES issues(id) +ON DELETE CASCADE; + +ALTER TABLE issue_comments +DROP COLUMN type; + +ALTER TABLE issue_comments +RENAME COLUMN payloadobject TO payload; \ No newline at end of file diff --git a/supabase/migrations/20240913070225_issue_comments.sql b/supabase/migrations/20240913070225_issue_comments.sql new file mode 100644 index 0000000..09daf65 --- /dev/null +++ b/supabase/migrations/20240913070225_issue_comments.sql @@ -0,0 +1,5 @@ +ALTER TABLE issue_comments +ADD COLUMN markdown TEXT; + +ALTER TABLE issues +ADD COLUMN markdown TEXT; \ No newline at end of file diff --git a/supabase/migrations/20240913145445_issue_comments.sql b/supabase/migrations/20240913145445_issue_comments.sql new file mode 100644 index 0000000..1753ab1 --- /dev/null +++ b/supabase/migrations/20240913145445_issue_comments.sql @@ -0,0 +1,13 @@ +CREATE OR REPLACE FUNCTION find_similar_issues(current_id VARCHAR, query_embedding vector(1024), threshold float8) +RETURNS TABLE(issue_id VARCHAR, issue_plaintext TEXT, similarity float8) AS $$ +BEGIN + RETURN QUERY + SELECT id AS issue_id, + plaintext AS issue_plaintext, + 1 - (embedding <=> query_embedding) AS similarity + FROM issues + WHERE id <> current_id + AND 1 - (embedding <=> query_embedding) >= threshold + ORDER BY similarity DESC; +END; +$$ LANGUAGE plpgsql; diff --git a/tests/__mocks__/adapter.ts b/tests/__mocks__/adapter.ts index 4c7a797..d1f634c 100644 --- a/tests/__mocks__/adapter.ts +++ b/tests/__mocks__/adapter.ts @@ -1,54 +1,71 @@ import { Context } from "../../src/types"; import { Comment } from "../../src/adapters/supabase/helpers/comment"; -import { Embedding } from "../../src/adapters/openai/helpers/embedding"; import { STRINGS } from "./strings"; export interface CommentMock { - id: number; - commentbody: string; - issuebody: string; + id: string; + plaintext: string | null; + author_id: number; + payload?: Record | null; + type?: string; + issue_id?: string; embedding: number[]; } export function createMockAdapters(context: Context) { - const commentMap: Map = new Map(); + const commentMap: Map = new Map(); return { supabase: { comment: { - createComment: jest.fn(async (commentBody: string, commentId: number, issueBody: string) => { - if (commentMap.has(commentId)) { - throw new Error("Comment already exists"); + createComment: jest.fn( + async ( + plaintext: string | null, + commentNodeId: string, + authorId: number, + payload: Record | null, + isPrivate: boolean, + issueId: string + ) => { + if (commentMap.has(commentNodeId)) { + throw new Error("Comment already exists"); + } + const embedding = await context.adapters.voyage.embedding.createEmbedding(plaintext); + if (isPrivate) { + plaintext = null; + } + commentMap.set(commentNodeId, { id: commentNodeId, plaintext, author_id: authorId, embedding, issue_id: issueId }); } - const embedding = await context.adapters.openai.embedding.createEmbedding(commentBody); - commentMap.set(commentId, { id: commentId, commentbody: commentBody, issuebody: issueBody, embedding }); - }), - updateComment: jest.fn(async (commentBody: string, commentId: number) => { - if (!commentMap.has(commentId)) { + ), + updateComment: jest.fn(async (plaintext: string | null, commentNodeId: string, payload: Record | null, isPrivate: boolean) => { + if (!commentMap.has(commentNodeId)) { throw new Error(STRINGS.COMMENT_DOES_NOT_EXIST); } - const originalComment = commentMap.get(commentId); + const originalComment = commentMap.get(commentNodeId); if (!originalComment) { throw new Error(STRINGS.COMMENT_DOES_NOT_EXIST); } - const { id, issuebody } = originalComment; - const embedding = await context.adapters.openai.embedding.createEmbedding(commentBody); - commentMap.set(commentId, { id, commentbody: commentBody, issuebody, embedding }); + const { id, author_id } = originalComment; + const embedding = await context.adapters.voyage.embedding.createEmbedding(plaintext); + if (isPrivate) { + plaintext = null; + } + commentMap.set(commentNodeId, { id, plaintext, author_id, embedding }); }), - deleteComment: jest.fn(async (commentId: number) => { - if (!commentMap.has(commentId)) { + deleteComment: jest.fn(async (commentNodeId: string) => { + if (!commentMap.has(commentNodeId)) { throw new Error(STRINGS.COMMENT_DOES_NOT_EXIST); } - commentMap.delete(commentId); + commentMap.delete(commentNodeId); }), - getComment: jest.fn(async (commentId: number) => { - if (!commentMap.has(commentId)) { + getComment: jest.fn(async (commentNodeId: string) => { + if (!commentMap.has(commentNodeId)) { throw new Error(STRINGS.COMMENT_DOES_NOT_EXIST); } - return commentMap.get(commentId); + return commentMap.get(commentNodeId); }), } as unknown as Comment, }, - openai: { + voyage: { embedding: { createEmbedding: jest.fn(async (text: string) => { if (text && text.length > 0) { @@ -56,7 +73,7 @@ export function createMockAdapters(context: Context) { } return new Array(3072).fill(0); }), - } as unknown as Embedding, + } as unknown as number[], }, }; } diff --git a/tests/__mocks__/db.ts b/tests/__mocks__/db.ts index f7bd2b6..1681106 100644 --- a/tests/__mocks__/db.ts +++ b/tests/__mocks__/db.ts @@ -7,36 +7,82 @@ import { factory, nullable, primaryKey } from "@mswjs/data"; export const db = factory({ users: { id: primaryKey(Number), - name: String, login: String, + avatar_url: nullable(String), // Add any additional fields based on the schema }, - issue: { + repo: { id: primaryKey(Number), - assignees: Array, + name: String, + full_name: String, // Assuming full_name is needed for repo + private: Boolean, + owner: { + login: String, + id: Number, + avatar_url: nullable(String), + }, html_url: String, - repository_url: String, - state: String, + description: nullable(String), + fork: Boolean, + url: String, + forks_url: String, + keys_url: String, + collaborators_url: String, + teams_url: String, + hooks_url: String, + issue_events_url: String, + events_url: String, + assignees_url: String, + branches_url: String, + tags_url: String, + blobs_url: String, + git_tags_url: String, + git_refs_url: String, + trees_url: String, + statuses_url: String, + languages_url: String, + stargazers_url: String, + contributors_url: String, + subscribers_url: String, + subscription_url: String, + commits_url: String, + git_commits_url: String, + comments_url: String, + issue_comment_url: String, + contents_url: String, + compare_url: String, + merges_url: String, + archive_url: String, + downloads_url: String, + issues_url: String, + pulls_url: String, + milestones_url: String, + notifications_url: String, + labels_url: String, + releases_url: String, + deployments_url: String, + }, + issue: { + id: primaryKey(Number), + number: Number, + title: String, + body: String, + user: { + login: String, + id: Number, + }, owner: String, repo: String, - labels: Array, author_association: String, - body: nullable(String), - closed_at: nullable(Date), - created_at: nullable(Date), + created_at: Date, + updated_at: Date, comments: Number, - comments_url: String, - events_url: String, - labels_url: String, + labels: Array, + state: 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, + login: String, + id: Number, + avatar_url: nullable(String), email: nullable(String), events_url: String, followers_url: String, @@ -44,8 +90,6 @@ export const db = factory({ gists_url: String, gravatar_id: nullable(String), html_url: String, - id: Number, - login: String, name: nullable(String), node_id: String, organizations_url: String, @@ -58,27 +102,42 @@ export const db = factory({ type: String, url: String, }), - }, - repo: { - id: primaryKey(Number), - html_url: String, - name: String, - labels: Array, - owner: { - login: String, - id: Number, + milestone: nullable({ + title: String, + description: nullable(String), + due_on: nullable(Date), + number: Number, + state: String, + }), + reactions: { + url: String, + total_count: Number, + "+1": Number, + "-1": Number, + laugh: Number, + hooray: Number, + confused: Number, + heart: Number, + rocket: Number, + eyes: Number, }, - issues: Array, + timeline_url: String, + performed_via_github_app: nullable(Object), + state_reason: nullable(String), }, issueComments: { id: primaryKey(Number), body: String, created_at: Date, updated_at: Date, + node_id: String, issue_number: Number, user: { login: String, id: Number, }, + author_association: String, + html_url: String, + issue_url: String, }, }); diff --git a/tests/__mocks__/handlers.ts b/tests/__mocks__/handlers.ts index f1c494a..0019673 100644 --- a/tests/__mocks__/handlers.ts +++ b/tests/__mocks__/handlers.ts @@ -1,6 +1,5 @@ import { http, HttpResponse } from "msw"; import { db } from "./db"; -import issueTemplate from "./issue-template"; /** * Intercepts the routes and returns a custom payload */ @@ -31,13 +30,6 @@ export const handlers = [ } return HttpResponse.json(item); }), - // 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); - }), // create comment http.post("https://api.github.com/repos/:owner/:repo/issues/:issue_number/comments", async ({ params: { issue_number: issueNumber }, request }) => { const { body } = await getValue(request.body); diff --git a/tests/__mocks__/helpers.ts b/tests/__mocks__/helpers.ts index b4c5b14..236ac8e 100644 --- a/tests/__mocks__/helpers.ts +++ b/tests/__mocks__/helpers.ts @@ -1,5 +1,4 @@ import { db } from "./db"; -import issueTemplate from "./issue-template"; import { STRINGS } from "./strings"; import usersGet from "./users-get.json"; @@ -12,34 +11,93 @@ import usersGet from "./users-get.json"; * Here is where you create issues, commits, pull requests, etc. */ export async function setupTests() { + // Insert users for (const item of usersGet) { - db.users.create(item); + db.users.create({ + login: item.login, + id: item.id, + }); } + // Insert repository db.repo.create({ id: 1, name: STRINGS.TEST_REPO, + full_name: `${STRINGS.USER_1}/${STRINGS.TEST_REPO}`, + private: false, owner: { login: STRINGS.USER_1, id: 1, + avatar_url: "", }, - issues: [], }); + // Insert issues db.issue.create({ - ...issueTemplate, + id: 1, + number: 1, + title: "First Issue", + body: "This is the body of the first issue.", + user: { + login: STRINGS.USER_1, + id: 1, + }, + author_association: "OWNER", + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + comments: 0, + labels: [], + state: "open", + locked: false, + reactions: { + url: "", + total_count: 0, + "+1": 0, + "-1": 0, + laugh: 0, + hooray: 0, + confused: 0, + heart: 0, + rocket: 0, + eyes: 0, + }, + timeline_url: "", }); db.issue.create({ - ...issueTemplate, id: 2, number: 2, + title: "Second Issue", + body: "This is the body of the second issue.", + user: { + login: STRINGS.USER_1, + id: 1, + }, + author_association: "OWNER", + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + comments: 0, labels: [], + state: "open", + locked: false, + reactions: { + url: "", + total_count: 0, + "+1": 0, + "-1": 0, + laugh: 0, + hooray: 0, + confused: 0, + heart: 0, + rocket: 0, + eyes: 0, + }, + timeline_url: "", }); } -export function createComment(comment: string, commentId: number) { - const isComment = db.issueComments.findFirst({ +export function createComment(comment: string, commentId: number, nodeId: string) { + const existingComment = db.issueComments.findFirst({ where: { id: { equals: commentId, @@ -47,7 +105,7 @@ export function createComment(comment: string, commentId: number) { }, }); - if (isComment) { + if (existingComment) { db.issueComments.update({ where: { id: { @@ -56,6 +114,7 @@ export function createComment(comment: string, commentId: number) { }, data: { body: comment, + updated_at: new Date().toISOString(), }, }); } else { @@ -63,10 +122,14 @@ export function createComment(comment: string, commentId: number) { id: commentId, body: comment, issue_number: 1, + node_id: nodeId, user: { login: STRINGS.USER_1, id: 1, }, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + author_association: "OWNER", }); } } diff --git a/tests/main.test.ts b/tests/main.test.ts index c78ec3d..bc4ed19 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -4,7 +4,7 @@ import { drop } from "@mswjs/data"; import { db } from "./__mocks__/db"; import { server } from "./__mocks__/node"; import { expect, describe, beforeAll, beforeEach, afterAll, afterEach, it } from "@jest/globals"; -import { Context } from "../src/types/context"; +import { Context, SupportedEvents } from "../src/types/context"; import { Octokit } from "@octokit/rest"; import { STRINGS } from "./__mocks__/strings"; import { createComment, setupTests } from "./__mocks__/helpers"; @@ -18,7 +18,6 @@ import { CommentMock, createMockAdapters } from "./__mocks__/adapter"; dotenv.config(); jest.requireActual("@octokit/rest"); jest.requireActual("@supabase/supabase-js"); -jest.requireActual("openai"); const octokit = new Octokit(); beforeAll(() => { @@ -41,51 +40,51 @@ describe("Plugin tests", () => { const response = await worker.fetch(new Request("http://localhost/manifest.json"), { SUPABASE_KEY: "test", SUPABASE_URL: "test", - OPENAI_API_KEY: "test", + VOYAGEAI_API_KEY: "test", }); const content = await response.json(); expect(content).toEqual(manifest); }); it("When a comment is created it should add it to the database", async () => { - const { context } = createContext(); + const { context } = createContext(STRINGS.HELLO_WORLD, 1, 1, 1, 1, "sasasCreate"); await runPlugin(context); const supabase = context.adapters.supabase; + const commentObject = null; try { - const issueBody = context.payload.issue.body || ""; - await supabase.comment.createComment(STRINGS.HELLO_WORLD, 1, issueBody); + await supabase.comment.createComment(STRINGS.HELLO_WORLD, "sasasCreate", 1, commentObject, false, "sasasCreateIssue"); throw new Error("Expected method to reject."); } catch (error) { if (error instanceof Error) { expect(error.message).toBe("Comment already exists"); } } - const comment = (await supabase.comment.getComment(1)) as unknown as CommentMock; + const comment = (await supabase.comment.getComment("sasasCreate")) as unknown as CommentMock; expect(comment).toBeDefined(); - expect(comment?.commentbody).toBeDefined(); - expect(comment?.commentbody).toBe(STRINGS.HELLO_WORLD); + expect(comment?.plaintext).toBeDefined(); + expect(comment?.plaintext).toBe(STRINGS.HELLO_WORLD); }); it("When a comment is updated it should update the database", async () => { - const { context } = createContext("Updated Message", 1, 1, 1, 1, "issue_comment.edited"); + const { context } = createContext("Updated Message", 1, 1, 1, 1, "sasasUpdate", "issue_comment.edited"); const supabase = context.adapters.supabase; - const issueBody = context.payload.issue.body || ""; - await supabase.comment.createComment(STRINGS.HELLO_WORLD, 1, issueBody); + const commentObject = null; + await supabase.comment.createComment(STRINGS.HELLO_WORLD, "sasasUpdate", 1, commentObject, false, "sasasUpdateIssue"); await runPlugin(context); - const comment = (await supabase.comment.getComment(1)) as unknown as CommentMock; + const comment = (await supabase.comment.getComment("sasasUpdate")) as unknown as CommentMock; expect(comment).toBeDefined(); - expect(comment?.commentbody).toBeDefined(); - expect(comment?.commentbody).toBe("Updated Message"); + expect(comment?.plaintext).toBeDefined(); + expect(comment?.plaintext).toBe("Updated Message"); }); it("When a comment is deleted it should delete it from the database", async () => { - const { context } = createContext("Text Message", 1, 1, 1, 1, "issue_comment.deleted"); + const { context } = createContext("Text Message", 1, 1, 1, 1, "sasasDelete", "issue_comment.deleted"); const supabase = context.adapters.supabase; - const issueBody = context.payload.issue.body || ""; - await supabase.comment.createComment(STRINGS.HELLO_WORLD, 1, issueBody); + const commentObject = null; + await supabase.comment.createComment(STRINGS.HELLO_WORLD, "sasasDelete", 1, commentObject, false, "sasasDeleteIssue"); await runPlugin(context); try { - await supabase.comment.getComment(1); + await supabase.comment.getComment("sasasDelete"); throw new Error("Expected method to reject."); } catch (error) { if (error instanceof Error) { @@ -109,14 +108,17 @@ function createContext( payloadSenderId: number = 1, commentId: number = 1, issueOne: number = 1, + nodeId: string = "sasas", eventName: Context["eventName"] = "issue_comment.created" ) { const repo = db.repo.findFirst({ where: { id: { equals: repoId } } }) as unknown as Context["payload"]["repository"]; const sender = db.users.findFirst({ where: { id: { equals: payloadSenderId } } }) as unknown as Context["payload"]["sender"]; const issue1 = db.issue.findFirst({ where: { id: { equals: issueOne } } }) as unknown as Context["payload"]["issue"]; - createComment(commentBody, commentId); // create it first then pull it from the DB and feed it to _createContext - const comment = db.issueComments.findFirst({ where: { id: { equals: commentId } } }) as unknown as Context["payload"]["comment"]; + createComment(commentBody, commentId, nodeId); // create it first then pull it from the DB and feed it to _createContext + const comment = db.issueComments.findFirst({ + where: { id: { equals: commentId } }, + }) as unknown as unknown as SupportedEvents["issue_comment.created"]["payload"]["comment"]; const context = createContextInner(repo, sender, issue1, comment, eventName); context.adapters = createMockAdapters(context) as unknown as Context["adapters"]; @@ -147,7 +149,7 @@ function createContextInner( repo: Context["payload"]["repository"], sender: Context["payload"]["sender"], issue: Context["payload"]["issue"], - comment: Context["payload"]["comment"], + comment: SupportedEvents["issue_comment.created"]["payload"]["comment"], eventName: Context["eventName"] = "issue_comment.created" ): Context { return { @@ -161,7 +163,11 @@ function createContextInner( installation: { id: 1 } as Context["payload"]["installation"], organization: { login: STRINGS.USER_1 } as Context["payload"]["organization"], } as Context["payload"], - config: {}, + config: { + warningThreshold: 0.75, + matchThreshold: 0.95, + jobMatchingThreshold: 0.95, + }, adapters: {} as Context["adapters"], logger: new Logs("debug"), env: {} as Env, diff --git a/yarn.lock b/yarn.lock index 78b6afa..519f6f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -351,30 +351,38 @@ dependencies: mime "^3.0.0" -"@cloudflare/workerd-darwin-64@1.20240620.1": - version "1.20240620.1" - resolved "https://registry.yarnpkg.com/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20240620.1.tgz#322bc6350f9649d4a649b62a86165fa12bbdc088" - integrity sha512-YWeS2aE8jAzDefuus/3GmZcFGu3Ef94uCAoxsQuaEXNsiGM9NeAhPpKC1BJAlcv168U/Q1J+6hckcGtipf6ZcQ== - -"@cloudflare/workerd-darwin-arm64@1.20240620.1": - version "1.20240620.1" - resolved "https://registry.yarnpkg.com/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20240620.1.tgz#25cf37caf77837b8cd40e796f44aad095c39f322" - integrity sha512-3rdND+EHpmCrwYX6hvxIBSBJ0f40tRNxond1Vfw7GiR1MJVi3gragiBx75UDFHCxfRw3J0GZ1qVlkRce2/Xbsg== - -"@cloudflare/workerd-linux-64@1.20240620.1": - version "1.20240620.1" - resolved "https://registry.yarnpkg.com/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20240620.1.tgz#4138144341d7c02709c518599ae65cb070d0eb7b" - integrity sha512-tURcTrXGeSbYqeM5ISVcofY20StKbVIcdxjJvNYNZ+qmSV9Fvn+zr7rRE+q64pEloVZfhsEPAlUCnFso5VV4XQ== - -"@cloudflare/workerd-linux-arm64@1.20240620.1": - version "1.20240620.1" - resolved "https://registry.yarnpkg.com/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20240620.1.tgz#35eb047c7146075d41ce4ee1af87550de892b95d" - integrity sha512-TThvkwNxaZFKhHZnNjOGqIYCOk05DDWgO+wYMuXg15ymN/KZPnCicRAkuyqiM+R1Fgc4kwe/pehjP8pbmcf6sg== - -"@cloudflare/workerd-windows-64@1.20240620.1": - version "1.20240620.1" - resolved "https://registry.yarnpkg.com/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20240620.1.tgz#f4c6cb81a474aaf019dcf000bb31e46d3538c3f2" - integrity sha512-Y/BA9Yj0r7Al1HK3nDHcfISgFllw6NR3XMMPChev57vrVT9C9D4erBL3sUBfofHU+2U9L+ShLsl6obBpe3vvUw== +"@cloudflare/workerd-darwin-64@1.20240925.0": + version "1.20240925.0" + resolved "https://registry.yarnpkg.com/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20240925.0.tgz#f78fe394f73540594609d8e05a2da7feb46c76c0" + integrity sha512-KdLnSXuzB65CbqZPm+qYzk+zkQ1tUNPaaRGYVd/jPYAxwwtfTUQdQ+ahDPwVVs2tmQELKy7ZjQjf2apqSWUfjw== + +"@cloudflare/workerd-darwin-arm64@1.20240925.0": + version "1.20240925.0" + resolved "https://registry.yarnpkg.com/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20240925.0.tgz#f03b17177744ad898bb12610d15cc0a9744abfe6" + integrity sha512-MiQ6uUmCXjsXgWNV+Ock2tp2/tYqNJGzjuaH6jFioeRF+//mz7Tv7J7EczOL4zq+TH8QFOh0/PUsLyazIWVGng== + +"@cloudflare/workerd-linux-64@1.20240925.0": + version "1.20240925.0" + resolved "https://registry.yarnpkg.com/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20240925.0.tgz#fe0366b804b957acf5012d889e94163bab806a57" + integrity sha512-Rjix8jsJMfsInmq3Hm3fmiRQ+rwzuWRPV1pg/OWhMSfNP7Qp2RCU+RGkhgeR9Z5eNAje0Sn2BMrFq4RvF9/yRA== + +"@cloudflare/workerd-linux-arm64@1.20240925.0": + version "1.20240925.0" + resolved "https://registry.yarnpkg.com/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20240925.0.tgz#fcf82de06def420972c661a6021c87683cd8fbdc" + integrity sha512-VYIPeMHQRtbwQoIjUwS/zULlywPxyDvo46XkTpIW5MScEChfqHvAYviQ7TzYGx6Q+gmZmN+DUB2KOMx+MEpCxA== + +"@cloudflare/workerd-windows-64@1.20240925.0": + version "1.20240925.0" + resolved "https://registry.yarnpkg.com/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20240925.0.tgz#0a5c82b95b03a94591cc8a1830f28d2e41ff7685" + integrity sha512-C8peGvaU5R51bIySi1VbyfRgwNSSRknqoFSnSbSBI3uTN3THTB3UnmRKy7GXJDmyjgXuT9Pcs1IgaWNubLtNtw== + +"@cloudflare/workers-shared@0.5.4": + version "0.5.4" + resolved "https://registry.yarnpkg.com/@cloudflare/workers-shared/-/workers-shared-0.5.4.tgz#bbf8f03b79a6bc0169ad66a6015ebe579d36753a" + integrity sha512-PNL/0TjKRdUHa1kwgVdqUNJVZ9ez4kacsi8omz+gv859EvJmsVuGiMAClY2YfJnC9LVKhKCcjqmFgKNXG9/IXA== + dependencies: + mime "^3.0.0" + zod "^3.22.3" "@commitlint/cli@19.3.0": version "19.3.0" @@ -1961,16 +1969,34 @@ expect "^29.0.0" pretty-format "^29.0.0" +"@types/linkify-it@^5": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76" + integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q== + "@types/lodash@^4.14.172": version "4.17.4" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.4.tgz#0303b64958ee070059e3a7184048a55159fe20b7" integrity sha512-wYCP26ZLxaT3R39kiN2+HcJ4kTd3U1waI/cY7ivWYqFP6pW3ZNpvi6Wd6PHZx7T/t8z0vlkXMg3QYLa7DZ/IJQ== +"@types/markdown-it@^14.1.2": + version "14.1.2" + resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61" + integrity sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog== + dependencies: + "@types/linkify-it" "^5" + "@types/mdurl" "^2" + "@types/md5@^2.3.0": version "2.3.5" resolved "https://registry.yarnpkg.com/@types/md5/-/md5-2.3.5.tgz#481cef0a896e3a5dcbfc5a8a8b02c05958af48a5" integrity sha512-/i42wjYNgE6wf0j2bcTX6kuowmdL/6PE4IVitMpm2eYKBUuYCprdcWVK+xEF0gcV6ufMCRhtxmReGfc6hIK7Jw== +"@types/mdurl@^2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-2.0.0.tgz#d43878b5b20222682163ae6f897b20447233bdfd" + integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg== + "@types/mute-stream@^0.0.4": version "0.0.4" resolved "https://registry.yarnpkg.com/@types/mute-stream/-/mute-stream-0.0.4.tgz#77208e56a08767af6c5e1237be8888e2f255c478" @@ -1978,14 +2004,6 @@ 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" @@ -2007,13 +2025,6 @@ dependencies: undici-types "~5.26.4" -"@types/node@^18.11.18": - version "18.19.46" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.46.tgz#51801396c01153e0626e36f43386e83bc768b072" - integrity sha512-vnRgMS7W6cKa1/0G3/DTtQYpVrZ8c0Xm6UkLaVFrb9jtcVC3okokW09Ki1Qdrj9ISokszD69nY4WDLRlvHlhAA== - dependencies: - undici-types "~5.26.4" - "@types/phoenix@^1.5.4": version "1.6.5" resolved "https://registry.yarnpkg.com/@types/phoenix/-/phoenix-1.6.5.tgz#5654e14ec7ad25334a157a20015996b6d7d2075e" @@ -2191,13 +2202,6 @@ agent-base@^7.0.2: dependencies: debug "^4.3.4" -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" @@ -2443,6 +2447,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + before-after-hook@^2.2.0: version "2.2.3" resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.3.tgz#c51e809c81a4e354084422b9b26bad88249c517c" @@ -2519,6 +2528,14 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" @@ -2783,11 +2800,6 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -consola@^3.2.3: - version "3.2.3" - resolved "https://registry.yarnpkg.com/consola/-/consola-3.2.3.tgz#0741857aa88cfa0d6fd53f1cff0375136e98502f" - integrity sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ== - conventional-changelog-angular@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz#5eec8edbff15aa9b1680a8dcfbd53e2d7eb2ba7a" @@ -3056,11 +3068,6 @@ date-fns@^2.21.1: dependencies: "@babel/runtime" "^7.21.0" -date-fns@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.6.0.tgz#f20ca4fe94f8b754951b24240676e8618c0206bf" - integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww== - debug@4: version "4.3.6" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" @@ -3210,6 +3217,11 @@ emoji-regex@^9.2.2: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== +entities@^4.4.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + env-paths@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" @@ -3535,6 +3547,11 @@ eventemitter3@^5.0.1: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== +events@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + execa@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" @@ -3721,11 +3738,6 @@ foreground-child@^3.1.0: cross-spawn "^7.0.0" signal-exit "^4.0.1" -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" @@ -3735,13 +3747,10 @@ form-data@^4.0.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" +formdata-node@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/formdata-node/-/formdata-node-6.0.3.tgz#48f8e2206ae2befded82af621ef015f08168dc6d" + integrity sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg== formdata-polyfill@^4.0.10: version "4.0.10" @@ -4055,13 +4064,6 @@ 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" @@ -4072,6 +4074,11 @@ identity-function@^1.0.0: resolved "https://registry.yarnpkg.com/identity-function/-/identity-function-1.0.0.tgz#bea1159f0985239be3ca348edf40ce2f0dd2c21d" integrity sha512-kNrgUK0qI+9qLTBidsH85HjDLpZfrrS0ElquKKe/fJFdB3D7VeKdXXEvOPDUHSHOzdZKCAAaQIWWyp0l2yq6pw== +ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + ignore@^5.1.4, ignore@^5.1.8, ignore@^5.2.0, ignore@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" @@ -4821,6 +4828,11 @@ jiti@^1.21.0: resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.6.tgz#6c7f7398dd4b3142767f9a168af2f317a428d268" integrity sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w== +js-base64@3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.2.tgz#816d11d81a8aff241603d19ce5761e13e41d7745" + integrity sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ== + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -4946,6 +4958,13 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== +linkify-it@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421" + integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ== + dependencies: + uc.micro "^2.0.0" + lint-staged@15.2.7: version "15.2.7" resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.2.7.tgz#97867e29ed632820c0fb90be06cd9ed384025649" @@ -5126,6 +5145,23 @@ map-obj@^2.0.0: resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-2.0.0.tgz#a65cd29087a92598b8791257a523e021222ac1f9" integrity sha512-TzQSV2DiMYgoF5RycneKVUzIa9bQsj/B3tTgsE3dOGqlzHnGIDaC7XBE7grnA+8kZPnfqSGFe95VHc2oc0VFUQ== +markdown-it-plain-text@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/markdown-it-plain-text/-/markdown-it-plain-text-0.3.0.tgz#374abfcc1c95ae3d7a5e09365558d43184e1e7db" + integrity sha512-JT5x7oXZaTY6lwxsnxaIAT4hm5Q2Mi8dZoCaCNg3s7JXmDxY84D90OtV0US436UvN4zOUHoac4ExceTogydOLw== + +markdown-it@^14.1.0: + version "14.1.0" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.1.0.tgz#3c3c5992883c633db4714ccb4d7b5935d98b7d45" + integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg== + dependencies: + argparse "^2.0.1" + entities "^4.4.0" + linkify-it "^5.0.0" + mdurl "^2.0.0" + punycode.js "^2.3.1" + uc.micro "^2.1.0" + md5@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f" @@ -5135,6 +5171,11 @@ md5@^2.3.0: crypt "0.0.2" is-buffer "~1.1.6" +mdurl@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0" + integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w== + memorystream@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/memorystream/-/memorystream-0.3.1.tgz#86d7090b30ce455d63fbae12dda51a47ddcaf9b2" @@ -5195,10 +5236,10 @@ mimic-response@^3.1.0: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== -miniflare@3.20240620.0: - version "3.20240620.0" - resolved "https://registry.yarnpkg.com/miniflare/-/miniflare-3.20240620.0.tgz#18e0b0755e7755d6830c67937646049fdcb8aa1a" - integrity sha512-NBMzqUE2mMlh/hIdt6U5MP+aFhEjKDq3l8CAajXAQa1WkndJdciWvzB2mfLETwoVFhMl/lphaVzyEN2AgwJpbQ== +miniflare@3.20240925.0: + version "3.20240925.0" + resolved "https://registry.yarnpkg.com/miniflare/-/miniflare-3.20240925.0.tgz#a291998dedf90bfb4bcfdad033ba030851ff9171" + integrity sha512-2LmQbKHf0n6ertUKhT+Iltixi53giqDH7P71+wCir3OnGyXIODqYwOECx1mSDNhYThpxM2dav8UdPn6SQiMoXw== dependencies: "@cspotcode/source-map-support" "0.8.1" acorn "^8.8.0" @@ -5208,8 +5249,8 @@ miniflare@3.20240620.0: glob-to-regexp "^0.4.1" stoppable "^1.1.0" undici "^5.28.4" - workerd "1.20240620.1" - ws "^8.14.2" + workerd "1.20240925.0" + ws "^8.17.1" youch "^3.2.2" zod "^3.22.3" @@ -5267,11 +5308,6 @@ 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" @@ -5320,17 +5356,12 @@ 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, node-domexception@^1.0.0: +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: +node-fetch@2.7.0: 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== @@ -5440,6 +5471,11 @@ object.assign@^4.1.5: has-symbols "^1.0.3" object-keys "^1.1.1" +ohash@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/ohash/-/ohash-1.1.4.tgz#ae8d83014ab81157d2c285abf7792e2995fadd72" + integrity sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g== + once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -5461,19 +5497,6 @@ onetime@^6.0.0: dependencies: mimic-fn "^4.0.0" -openai@^4.56.0: - version "4.56.0" - resolved "https://registry.yarnpkg.com/openai/-/openai-4.56.0.tgz#07d3982544cabd5781127288a8dfcceb7319a4cf" - integrity sha512-zcag97+3bG890MNNa0DQD9dGmmTWL8unJdNkulZzWRXrl+QeD+YkBI4H58rJcwErxqGK6a0jVPZ4ReJjhDGcmw== - 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" @@ -5640,6 +5663,11 @@ path-to-regexp@^6.2.0: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.2.tgz#324377a83e5049cbecadc5554d6a63a9a4866b36" integrity sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw== +path-to-regexp@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.3.0.tgz#2b6a26a337737a8e1416f9272ed0766b1c0389f4" + integrity sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ== + path-type@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" @@ -5752,6 +5780,11 @@ printable-characters@^1.0.42: resolved "https://registry.yarnpkg.com/printable-characters/-/printable-characters-1.0.42.tgz#3f18e977a9bd8eb37fcc4ff5659d7be90868b3d8" integrity sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ== +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + prompts@^2.0.1: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" @@ -5760,6 +5793,11 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" +punycode.js@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7" + integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA== + punycode@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" @@ -5770,6 +5808,13 @@ pure-rand@^6.0.0: resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2" integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== +qs@6.11.2: + version "6.11.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" + integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== + dependencies: + side-channel "^1.0.4" + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -5803,6 +5848,17 @@ readable-stream@^3.4.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readable-stream@^4.5.2: + version "4.5.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.5.2.tgz#9e7fc4c45099baeed934bff6eb97ba6cf2729e09" + integrity sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g== + dependencies: + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + string_decoder "^1.3.0" + readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -6288,7 +6344,7 @@ string.prototype.trimstart@^1.0.8: define-properties "^1.2.1" es-object-atoms "^1.0.0" -string_decoder@^1.1.1: +string_decoder@^1.1.1, string_decoder@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== @@ -6605,10 +6661,15 @@ typescript@5.4.5: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== -ufo@^1.5.3: - version "1.5.3" - resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.3.tgz#3325bd3c977b6c6cd3160bf4ff52989adc9d3344" - integrity sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw== +uc.micro@^2.0.0, uc.micro@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee" + integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A== + +ufo@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.4.tgz#16d6949674ca0c9e0fbbae1fa20a71d7b1ded754" + integrity sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ== unbox-primitive@^1.0.2: version "1.0.2" @@ -6632,17 +6693,15 @@ undici@^5.25.4, undici@^5.28.4: dependencies: "@fastify/busboy" "^2.0.0" -"unenv@npm:unenv-nightly@1.10.0-1717606461.a117952": - version "1.10.0-1717606461.a117952" - resolved "https://registry.yarnpkg.com/unenv-nightly/-/unenv-nightly-1.10.0-1717606461.a117952.tgz#ff0b97e1e159f84be747271e1d55263b4b3eae7e" - integrity sha512-u3TfBX02WzbHTpaEfWEKwDijDSFAHcgXkayUZ+MVDrjhLFvgAJzFGTSTmwlEhwWi2exyRQey23ah9wELMM6etg== +"unenv@npm:unenv-nightly@2.0.0-20240919-125358-9a64854": + version "2.0.0-20240919-125358-9a64854" + resolved "https://registry.yarnpkg.com/unenv-nightly/-/unenv-nightly-2.0.0-20240919-125358-9a64854.tgz#13f6812c7b12b9521ea05c6d49259d136e093acd" + integrity sha512-XjsgUTrTHR7iw+k/SRTNjh6EQgwpC9voygnoCJo5kh4hKqsSDHUW84MhL9EsHTNfLctvVBHaSw8e2k3R2fKXsQ== dependencies: - consola "^3.2.3" defu "^6.1.4" - mime "^3.0.0" - node-fetch-native "^1.6.4" + ohash "^1.1.4" pathe "^1.1.2" - ufo "^1.5.3" + ufo "^1.5.4" unescape-js@^1.0.5: version "1.1.4" @@ -6676,6 +6735,11 @@ uri-js@^4.2.2, uri-js@^4.4.1: dependencies: punycode "^2.1.0" +url-join@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7" + integrity sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA== + util-deprecate@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -6708,6 +6772,19 @@ vlq@^0.2.1: resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26" integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow== +voyageai@^0.0.1-5: + version "0.0.1-5" + resolved "https://registry.yarnpkg.com/voyageai/-/voyageai-0.0.1-5.tgz#e0457d991784900c16e4cdf095654f195d62fdf2" + integrity sha512-IuXSXM3l9J3NIq+MLHXacG/yhswpEgWIu9eBqoFqMRnFiDx00dLL62OWg6WqVSipddZLwFeWH1Kaj56x5eqhOQ== + dependencies: + form-data "^4.0.0" + formdata-node "^6.0.3" + js-base64 "3.7.2" + node-fetch "2.7.0" + qs "6.11.2" + readable-stream "^4.5.2" + url-join "4.0.1" + vscode-languageserver-textdocument@^1.0.11: version "1.0.11" resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz#0822a000e7d4dc083312580d7575fe9e3ba2e2bf" @@ -6732,11 +6809,6 @@ 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== - web-streams-polyfill@^3.0.3: version "3.3.3" resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" @@ -6796,37 +6868,38 @@ word-wrap@^1.2.5: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -workerd@1.20240620.1: - version "1.20240620.1" - resolved "https://registry.yarnpkg.com/workerd/-/workerd-1.20240620.1.tgz#1898fc94de8e53289b2eb261d5592474c4bc9f03" - integrity sha512-Qoq+RrFNk4pvEO+kpJVn8uJ5TRE9YJx5jX5pC5LjdKlw1XeD8EdXt5k0TbByvWunZ4qgYIcF9lnVxhcDFo203g== +workerd@1.20240925.0: + version "1.20240925.0" + resolved "https://registry.yarnpkg.com/workerd/-/workerd-1.20240925.0.tgz#0a2602eabfa7e1d01d89ff2b600ed359be9b515d" + integrity sha512-/Jj6+yLwfieZGEt3Kx4+5MoufuC3g/8iFaIh4MPBNGJOGYmdSKXvgCqz09m2+tVCYnysRfbq2zcbVxJRBfOCqQ== optionalDependencies: - "@cloudflare/workerd-darwin-64" "1.20240620.1" - "@cloudflare/workerd-darwin-arm64" "1.20240620.1" - "@cloudflare/workerd-linux-64" "1.20240620.1" - "@cloudflare/workerd-linux-arm64" "1.20240620.1" - "@cloudflare/workerd-windows-64" "1.20240620.1" + "@cloudflare/workerd-darwin-64" "1.20240925.0" + "@cloudflare/workerd-darwin-arm64" "1.20240925.0" + "@cloudflare/workerd-linux-64" "1.20240925.0" + "@cloudflare/workerd-linux-arm64" "1.20240925.0" + "@cloudflare/workerd-windows-64" "1.20240925.0" -wrangler@3.62.0: - version "3.62.0" - resolved "https://registry.yarnpkg.com/wrangler/-/wrangler-3.62.0.tgz#2b98e47734af2240e8c3136b1f6eaa90e363e1ed" - integrity sha512-TM1Bd8+GzxFw/JzwsC3i/Oss4LTWvIEWXXo1vZhx+7PHcsxdbnQGBBwPurHNJDSu2Pw22+2pCZiUGKexmgJksw== +wrangler@3.78.12: + version "3.78.12" + resolved "https://registry.yarnpkg.com/wrangler/-/wrangler-3.78.12.tgz#c3d7b605856b904ab7cce54afc223286b8454935" + integrity sha512-a/xk/N04IvOGk9J+BLkiFg42GDyPS+0BiJimbrHsbX+CDr8Iqq3HNMEyQld+6zbmq01u/gmc8S7GKVR9vDx4+g== dependencies: "@cloudflare/kv-asset-handler" "0.3.4" + "@cloudflare/workers-shared" "0.5.4" "@esbuild-plugins/node-globals-polyfill" "^0.2.3" "@esbuild-plugins/node-modules-polyfill" "^0.2.2" blake3-wasm "^2.1.5" chokidar "^3.5.3" - date-fns "^3.6.0" esbuild "0.17.19" - miniflare "3.20240620.0" + miniflare "3.20240925.0" nanoid "^3.3.3" - path-to-regexp "^6.2.0" + path-to-regexp "^6.3.0" resolve "^1.22.8" resolve.exports "^2.0.2" selfsigned "^2.0.1" source-map "^0.6.1" - unenv "npm:unenv-nightly@1.10.0-1717606461.a117952" + unenv "npm:unenv-nightly@2.0.0-20240919-125358-9a64854" + workerd "1.20240925.0" xxhash-wasm "^1.0.1" optionalDependencies: fsevents "~2.3.2" @@ -6897,7 +6970,7 @@ write-file-atomic@^5.0.0: imurmurhash "^0.1.4" signal-exit "^4.0.1" -ws@^8.14.2: +ws@^8.14.2, ws@^8.17.1: version "8.18.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==