├── .gitignore ├── package.json ├── README.md ├── changelog.ts └── pnpm-lock.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Automated-release-note", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@linear/sdk": "^8.0.0", 14 | "openai": "^4.11.0" 15 | }, 16 | "devDependencies": { 17 | "@types/node": "^20.8.0", 18 | "prettier": "3.0.3", 19 | "ts-node": "^10.9.1", 20 | "tslib": "^2.6.2", 21 | "typescript": "^5.2.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Automated release notes 2 | 3 | 🚀 10x your release note process 4 | 5 | ✌️ Went from spending **~2h** to do nice release notes for the whole company -> to **15 mins** 6 | 7 | ![09f698e165f545fbb10f6c8fda98a174](https://github.com/Collective-work/Automated-release-notes/assets/10058703/ebd557ec-ab79-480e-ba75-965bf7a8a7e9) 8 | 9 | ## Motivation and our process at Collective 10 | 11 | Disclaimer: This is not a ready to go package, that can be used out of the box. 12 | 13 | But it's simple enough it should give you ideas about how to generated release notes (it's one simple and readable script, 14 | if you don't like the code, blame ChatGPT - I used it for most of it to go fast). 15 | 16 | The release notes is generated from tickets in a done column (you can of course adapt it to your logic, I'm aware the 17 | processes are very different from one company to another) 18 | 19 | Here is how our process works overall: 20 | 21 | 1. We use linear 22 | 2. Every week, tickets with PRs merged go in a column on our engineering board `'Deployed (this week)'` 23 | 3. At the end of every week, I look at all those tickets and create a changelog I push to the team (this used to take 24 | me hours, mainly the reason I decided to automate it) 25 | 4. I push the update on slack, adding some ping here and there and some additional picture for context - people react 26 | and are generally happy that we progress 😉 27 | 28 | ## What this does 29 | 30 | This is an opinionated script of course. I did not make the effort to abstract it. The goal is more to show all we can 31 | do with AI and release notes 🤩 32 | 33 | 1. The script fetch all the cards in the column `'Deployed (this week)'` 34 | 2. It passes them through GPT-3 with a prompt TLDR; it asks it to summarise in 1 line more or less the ticket and categorise it 35 | 3. We create the final release note, with our 4 main categories (to better categorise what was shipped), these are: 36 | 37 | - **App** - for new feature and application interaction 38 | - **Admin** - for new admin features, as we are a pretty ops heavy company 39 | - **Bug** - for bugs that we fixed 40 | - **Misc** - for anything unrelated to the 3 classes above 41 | 42 | 4. I copy/paste the generated result in slack, modify 2-3 things (of course some things are not in the right place) and 43 | add pictures and ping the right people so they see the feature release (hard for GPT to know this 😜) 44 | 5. Hit send and collect emojis on slack (but I'm an imposter of course, the engineers did all the hard work) 45 | 46 | ## Make it work 47 | 48 | - Requires `node` and `pnpm` (or `npm`) 49 | - Install dependencies (`linear sdk` and `openai` at the moment + dev dependencies for `typescript`) by doing 50 | 51 | ``` 52 | pnpm install 53 | ``` 54 | 55 | or if you want to go fast 56 | 57 | ``` 58 | // dev deps for typescript - see here for ts-node https://github.com/TypeStrong/ts-node 59 | npm install -D typescript tslib @types/node ts-node 60 | 61 | // script dependencies 62 | npm install @linear/sdk openai 63 | ``` 64 | 65 | ## Usage 66 | 67 | ``` 68 | LINEAR_API_KEY=key OPEN_AI_API_KEY=key ts-node changelog.ts 69 | ``` 70 | 71 | ## The final result 72 | 73 | The result: 74 | 75 | ``` 76 | Final release note: 77 | 78 | *App*: 79 | [Setting] Allow collectives to change the roles of their members - E-3300 80 | [Opportunity] Allow collectives to answer questions to better respond to a project opportunit - E-3301 81 | 82 | *Admin*: 83 | [Forest Admin] Add a button to check if IBAN is valid - E-3302 84 | [Email] Send emails by the push of a button to users that need to fill a KYC - E-3303 85 | 86 | *Bug*: 87 | [UI] Fix side panel not closing on Safari - E-3304 88 | 89 | *Misc*: 90 | [CI] Fix CI issues related to Datadog - E-3305 91 | ``` 92 | -------------------------------------------------------------------------------- /changelog.ts: -------------------------------------------------------------------------------- 1 | import { LinearClient, Issue } from "@linear/sdk"; 2 | import OpenAI from "openai"; 3 | 4 | const BATCH_SIZE = 15; 5 | 6 | const LINEAR_TEAM = "Engineering"; 7 | const LINEAR_DONE_COLUMN = "Deployed (this week)"; 8 | 9 | const APP_CLASS = "App"; 10 | const ADMIN_CLASS = "Admin"; 11 | const BUG_CLASS = "Bug"; 12 | const MISC_CLASS = "Misc"; 13 | 14 | // Define the order of the classes 15 | const CLASS_ORDER = [APP_CLASS, ADMIN_CLASS, BUG_CLASS, MISC_CLASS]; 16 | 17 | // Define returned type by GPT 18 | type SummarisedTicket = { 19 | identifier: string; 20 | url: string; 21 | summary: string; 22 | category: string; 23 | class: string; 24 | }; 25 | 26 | const linearClient = new LinearClient({ 27 | apiKey: process.env["LINEAR_API_KEY"], 28 | }); 29 | 30 | async function summarise(tickets: string[]): Promise { 31 | const openai = new OpenAI({ 32 | apiKey: process.env["OPEN_AI_API_KEY"], 33 | }); 34 | 35 | const prompt = ` 36 | You are a product manager with a great technical background for a tech startup company making a release log of the tickets shipped by the company. 37 | 38 | The tickets are structured the following way: 39 | - An identifier, to recognise the ticket 40 | - A title, explaining the main goal of the ticket (it's helps to understand the ticket general idea) 41 | - A description, explaining all the details and how to achieve the goal (it tells what the ticket does) 42 | - Some labels, to explain what the ticket is about (e.g. bug for a bug fix, product for a product feature, forest admin for an admin feature) 43 | - a priority, which tells how urgent the ticket was 44 | - an estimate, which tells how much work the ticket represented (estimate is between 1 and 7, 1 being small ticket and 7 being huge) - a ticket with high priority and high estimate is usually an key feature for the company 45 | - the person responsible for it (it's a name) 46 | - a url, to link to the ticket 47 | 48 | Each feature is either one of the 4 classes below, and cannot be anything else: 49 | 1) ${APP_CLASS} - it means this is a product feature that impacted the core of our application (label is often product, but not always, and high estimate and high priority are often product features) 50 | 2) ${ADMIN_CLASS} - it means it touched forest admin or something related to admin tasks, it's rarely a product task (label is often "forest admin" but not always) - some examples of things that are admin include "the Shortlister", "forest admin workspaces" 51 | 3) ${BUG_CLASS} - it means it was a bug fix (generally tagged with a label "bug") 52 | 4) ${MISC_CLASS} - anything that does not fit clearly in one of those 3 categories above 53 | 54 | Now given all the context your have, summarise a ticket in the json structure below 55 | 56 | Json object should look something like this (it's json, and words between {{}} should be replace with result along with the {{}}, so for example {{ticket url}} should give https://linear.app/collective-work/issue/E-3307/test): 57 | 58 | { 59 | "identifier": "{{ticket identifier}}", 60 | "url": "{{ticket url}}", 61 | "summary": "{{ticket summary}}", 62 | "category": "{{ticket category}}", 63 | "class": "{{ticket class}}" 64 | } 65 | 66 | where 67 | - {{ticket identifier}} is just the identifier 68 | - {{ticket url}} is just the url 69 | - {{ticket summary}} is a quick summary of what the ticket solved or created - it must be a proper natural english sentence at the imperative mood (like a git commit message) and should not contain ay weird structure like [] characters at the beginning (example of not tolarated "[Shortlister] Show not onboarded collectives", it should be instead "Show not onboarded collectives on the shortlister" or similar) 70 | - {{ticket category}} is an ideally max 2 words (3 tolerated if hard to describe) describing the category/area of the ticket (for example, if the ticket is around impoving the CI, then it should be "CI", if it around Datadog test, it should be "Datadog", if it's about the Shortlister, then it should be "Shortlister") 71 | - {{ticket class}} is the class the ticket belong to (class definition is the one above, and can ONLY be one of [${APP_CLASS}, ${ADMIN_CLASS}, ${BUG_CLASS}, ${MISC_CLASS}], nothing else!) 72 | 73 | here is an example of a ticket json object: 74 | 75 | { 76 | "identifier": "E-3306", 77 | "url": "https://linear.app/collective-work/issue/E-3306/update-prisma-to-v5", 78 | "summary": "Updated the database to it's new major version", 79 | "category": "Database", 80 | "class": "${MISC_CLASS}" 81 | } 82 | 83 | As there are multiple tickets, the result should JUST a json array of objects as above, directly parsable. 84 | You should always return a json array, even if the array contains only one element - this is really important. 85 | 86 | Here are the tickets: 87 | `; 88 | 89 | let returnedContent: string = ""; 90 | 91 | try { 92 | const result = await openai.chat.completions.create({ 93 | messages: [ 94 | { role: "system", content: prompt }, 95 | { role: "user", content: tickets.join(`\n---\n`) }, 96 | ], 97 | model: "gpt-3.5-turbo-16k", 98 | max_tokens: 2000, 99 | }); 100 | 101 | returnedContent = result.choices?.[0]?.message?.content || ""; 102 | } catch (error) { 103 | console.error("Error querying OpenAI API:", error); 104 | } 105 | 106 | try { 107 | let parsedContent = JSON.parse(returnedContent); 108 | 109 | // when there is only one ticket, sometimes GPT returns a single item 110 | // this is to fix the problem if it occurs 111 | if (!Array.isArray(parsedContent)) { 112 | parsedContent = [parsedContent]; 113 | } 114 | 115 | return parsedContent as SummarisedTicket[]; 116 | } catch (error) { 117 | console.error("Error parsing OpenAI API response:", error); 118 | return []; 119 | } 120 | } 121 | 122 | async function getAllFinishedTickets() { 123 | let tickets: Issue[] = []; 124 | 125 | try { 126 | // Assuming there exists a method `issues` that fetches all issues. 127 | const allIssues = await linearClient.issues({ 128 | filter: { 129 | and: [ 130 | { 131 | team: { name: { eq: LINEAR_TEAM } }, 132 | state: { name: { eq: LINEAR_DONE_COLUMN } }, 133 | }, 134 | ], 135 | }, 136 | // should be enough to not paginate (250 is Linear's limit) 137 | first: 250, 138 | }); 139 | 140 | const totalTicketCount = allIssues.nodes.length; 141 | 142 | if (allIssues && totalTicketCount) { 143 | console.log(`Found ${totalTicketCount} tickets...`); 144 | tickets = allIssues.nodes; 145 | } else { 146 | console.log("No issues found"); 147 | } 148 | } catch (error) { 149 | console.error("Error fetching issues", error); 150 | } 151 | 152 | return tickets; 153 | } 154 | 155 | async function summariseTickets(tickets: Issue[]): Promise { 156 | let summarisedTickets: SummarisedTicket[] = []; 157 | const totalTicketCount = tickets.length; 158 | 159 | console.log(`Summarising ${totalTicketCount} tickets...`); 160 | 161 | for (let i = 0; i < totalTicketCount; i += BATCH_SIZE) { 162 | // we summarise in batches, to circumvent the token limit of GPT 163 | // who cannot do all of this summarising all at once 164 | const batch = tickets.slice(i, i + BATCH_SIZE).map(async (ticket) => { 165 | const assignee = await ticket.assignee; 166 | const labels = await ticket.labels(); 167 | 168 | // ticket representation in natural language 169 | return ` 170 | - Ticket identifier: ${ticket.identifier} 171 | - Ticket title: ${ticket.title} 172 | - Ticket priority: ${ticket.priorityLabel} 173 | - Ticket esimate: ${ticket.estimate} 174 | - Person responsable for the ticket: ${assignee?.name || ""} 175 | - Labels of the ticket: ${labels.nodes 176 | .map((label) => label.name) 177 | .join(", ")} 178 | - Url of the ticket: ${ticket.url} 179 | - Ticket description: ${ticket.description} 180 | `; 181 | }); 182 | 183 | const ticketBatch: string[] = await Promise.all(batch); 184 | 185 | console.log( 186 | `Summarising ${ticketBatch.length} tickets - batch: [${i}, ${ 187 | i + BATCH_SIZE 188 | }] out of ${totalTicketCount}...`, 189 | ); 190 | 191 | const summarisedTicketBatch = await summarise(ticketBatch); 192 | 193 | console.log("Summarised results:"); 194 | console.log(`${JSON.stringify(summarisedTicketBatch)}`); 195 | 196 | summarisedTickets = [...summarisedTickets, ...summarisedTicketBatch]; 197 | } 198 | 199 | return summarisedTickets; 200 | } 201 | 202 | function formatReleaseNote(tickets: SummarisedTicket[]) { 203 | // Step 1: Group by class 204 | let groupedTickets: Record = tickets.reduce( 205 | (group, ticket: SummarisedTicket) => { 206 | let className: string = ticket.class; 207 | 208 | if (!group[className]) { 209 | group[className] = []; 210 | } 211 | 212 | group[className].push(ticket); 213 | 214 | return group; 215 | }, 216 | {} as Record, 217 | ); 218 | 219 | // Step 2: Form the string 220 | let resultString = ""; 221 | 222 | CLASS_ORDER.forEach((className) => { 223 | resultString += `*${className}*:\n`; 224 | 225 | if (groupedTickets[className]) { 226 | groupedTickets[className].forEach((ticket: SummarisedTicket) => { 227 | resultString += `[${ticket.category}] ${ticket.summary} - [${ticket.identifier}](${ticket.url})\n`; 228 | }); 229 | } 230 | 231 | console.log( 232 | `Found ${ 233 | (groupedTickets[className] || []).length 234 | } for class ${className}`, 235 | ); 236 | 237 | resultString += "\n"; 238 | }); 239 | 240 | // Step 3: Handle the miss labeled 241 | resultString += `*Other*:\n`; 242 | 243 | tickets.forEach(ticket => { 244 | if (CLASS_ORDER.includes(ticket.class)) { 245 | return 246 | } 247 | 248 | resultString += `[${ticket.category}] ${ticket.summary} - [${ticket.identifier}](${ticket.url})\n`; 249 | }) 250 | 251 | resultString += "\n"; 252 | 253 | return resultString; 254 | } 255 | 256 | async function generateReleaseNote() { 257 | // 1. Get the linear tickets 258 | const finishedTickets: Issue[] = await getAllFinishedTickets(); 259 | 260 | // 2. Summarise the ticket thanks to GPT using a special format 261 | const summarisedTickets: SummarisedTicket[] = 262 | await summariseTickets(finishedTickets); 263 | 264 | // 3. Build the release note string from all the summarised ticket data 265 | const releaseNote: string = formatReleaseNote(summarisedTickets); 266 | 267 | // Log the result 268 | console.log("\nFinal release note:\n"); 269 | console.log(releaseNote); 270 | } 271 | 272 | generateReleaseNote(); 273 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: 5.4 2 | 3 | specifiers: 4 | "@linear/sdk": ^8.0.0 5 | "@types/node": ^20.8.0 6 | openai: ^4.11.0 7 | prettier: 3.0.3 8 | ts-node: ^10.9.1 9 | tslib: ^2.6.2 10 | typescript: ^5.2.2 11 | 12 | dependencies: 13 | "@linear/sdk": 8.0.0 14 | openai: 4.11.0 15 | 16 | devDependencies: 17 | "@types/node": 20.8.0 18 | prettier: 3.0.3 19 | ts-node: 10.9.1_r6idwkry7t7s7ss6rqn36gsmbe 20 | tslib: 2.6.2 21 | typescript: 5.2.2 22 | 23 | packages: 24 | /@cspotcode/source-map-support/0.8.1: 25 | resolution: 26 | { 27 | integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==, 28 | } 29 | engines: { node: ">=12" } 30 | dependencies: 31 | "@jridgewell/trace-mapping": 0.3.9 32 | dev: true 33 | 34 | /@graphql-typed-document-node/core/3.2.0_graphql@15.8.0: 35 | resolution: 36 | { 37 | integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==, 38 | } 39 | peerDependencies: 40 | graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 41 | dependencies: 42 | graphql: 15.8.0 43 | dev: false 44 | 45 | /@jridgewell/resolve-uri/3.1.1: 46 | resolution: 47 | { 48 | integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==, 49 | } 50 | engines: { node: ">=6.0.0" } 51 | dev: true 52 | 53 | /@jridgewell/sourcemap-codec/1.4.15: 54 | resolution: 55 | { 56 | integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==, 57 | } 58 | dev: true 59 | 60 | /@jridgewell/trace-mapping/0.3.9: 61 | resolution: 62 | { 63 | integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==, 64 | } 65 | dependencies: 66 | "@jridgewell/resolve-uri": 3.1.1 67 | "@jridgewell/sourcemap-codec": 1.4.15 68 | dev: true 69 | 70 | /@linear/sdk/8.0.0: 71 | resolution: 72 | { 73 | integrity: sha512-crNMJuQVIrouUuRrmmWCZgReQ44rthFPpFOOzqJxHmZreS3xhGF3x2JHVfxz3m17vQwum2LBCGsMBEESyiwm5A==, 74 | } 75 | engines: { node: ">=12.x", yarn: 1.x } 76 | dependencies: 77 | "@graphql-typed-document-node/core": 3.2.0_graphql@15.8.0 78 | graphql: 15.8.0 79 | isomorphic-unfetch: 3.1.0 80 | transitivePeerDependencies: 81 | - encoding 82 | dev: false 83 | 84 | /@tsconfig/node10/1.0.9: 85 | resolution: 86 | { 87 | integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==, 88 | } 89 | dev: true 90 | 91 | /@tsconfig/node12/1.0.11: 92 | resolution: 93 | { 94 | integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==, 95 | } 96 | dev: true 97 | 98 | /@tsconfig/node14/1.0.3: 99 | resolution: 100 | { 101 | integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==, 102 | } 103 | dev: true 104 | 105 | /@tsconfig/node16/1.0.4: 106 | resolution: 107 | { 108 | integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==, 109 | } 110 | dev: true 111 | 112 | /@types/node-fetch/2.6.6: 113 | resolution: 114 | { 115 | integrity: sha512-95X8guJYhfqiuVVhRFxVQcf4hW/2bCuoPwDasMf/531STFoNoWTT7YDnWdXHEZKqAGUigmpG31r2FE70LwnzJw==, 116 | } 117 | dependencies: 118 | "@types/node": 20.8.0 119 | form-data: 4.0.0 120 | dev: false 121 | 122 | /@types/node/18.18.1: 123 | resolution: 124 | { 125 | integrity: sha512-3G42sxmm0fF2+Vtb9TJQpnjmP+uKlWvFa8KoEGquh4gqRmoUG/N0ufuhikw6HEsdG2G2oIKhog1GCTfz9v5NdQ==, 126 | } 127 | dev: false 128 | 129 | /@types/node/20.8.0: 130 | resolution: 131 | { 132 | integrity: sha512-LzcWltT83s1bthcvjBmiBvGJiiUe84NWRHkw+ZV6Fr41z2FbIzvc815dk2nQ3RAKMuN2fkenM/z3Xv2QzEpYxQ==, 133 | } 134 | 135 | /abort-controller/3.0.0: 136 | resolution: 137 | { 138 | integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==, 139 | } 140 | engines: { node: ">=6.5" } 141 | dependencies: 142 | event-target-shim: 5.0.1 143 | dev: false 144 | 145 | /acorn-walk/8.2.0: 146 | resolution: 147 | { 148 | integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==, 149 | } 150 | engines: { node: ">=0.4.0" } 151 | dev: true 152 | 153 | /acorn/8.10.0: 154 | resolution: 155 | { 156 | integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==, 157 | } 158 | engines: { node: ">=0.4.0" } 159 | hasBin: true 160 | dev: true 161 | 162 | /agentkeepalive/4.5.0: 163 | resolution: 164 | { 165 | integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==, 166 | } 167 | engines: { node: ">= 8.0.0" } 168 | dependencies: 169 | humanize-ms: 1.2.1 170 | dev: false 171 | 172 | /arg/4.1.3: 173 | resolution: 174 | { 175 | integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==, 176 | } 177 | dev: true 178 | 179 | /asynckit/0.4.0: 180 | resolution: 181 | { 182 | integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==, 183 | } 184 | dev: false 185 | 186 | /base-64/0.1.0: 187 | resolution: 188 | { 189 | integrity: sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==, 190 | } 191 | dev: false 192 | 193 | /charenc/0.0.2: 194 | resolution: 195 | { 196 | integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==, 197 | } 198 | dev: false 199 | 200 | /combined-stream/1.0.8: 201 | resolution: 202 | { 203 | integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==, 204 | } 205 | engines: { node: ">= 0.8" } 206 | dependencies: 207 | delayed-stream: 1.0.0 208 | dev: false 209 | 210 | /create-require/1.1.1: 211 | resolution: 212 | { 213 | integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==, 214 | } 215 | dev: true 216 | 217 | /crypt/0.0.2: 218 | resolution: 219 | { 220 | integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==, 221 | } 222 | dev: false 223 | 224 | /delayed-stream/1.0.0: 225 | resolution: 226 | { 227 | integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==, 228 | } 229 | engines: { node: ">=0.4.0" } 230 | dev: false 231 | 232 | /diff/4.0.2: 233 | resolution: 234 | { 235 | integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==, 236 | } 237 | engines: { node: ">=0.3.1" } 238 | dev: true 239 | 240 | /digest-fetch/1.3.0: 241 | resolution: 242 | { 243 | integrity: sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==, 244 | } 245 | dependencies: 246 | base-64: 0.1.0 247 | md5: 2.3.0 248 | dev: false 249 | 250 | /event-target-shim/5.0.1: 251 | resolution: 252 | { 253 | integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==, 254 | } 255 | engines: { node: ">=6" } 256 | dev: false 257 | 258 | /form-data-encoder/1.7.2: 259 | resolution: 260 | { 261 | integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==, 262 | } 263 | dev: false 264 | 265 | /form-data/4.0.0: 266 | resolution: 267 | { 268 | integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==, 269 | } 270 | engines: { node: ">= 6" } 271 | dependencies: 272 | asynckit: 0.4.0 273 | combined-stream: 1.0.8 274 | mime-types: 2.1.35 275 | dev: false 276 | 277 | /formdata-node/4.4.1: 278 | resolution: 279 | { 280 | integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==, 281 | } 282 | engines: { node: ">= 12.20" } 283 | dependencies: 284 | node-domexception: 1.0.0 285 | web-streams-polyfill: 4.0.0-beta.3 286 | dev: false 287 | 288 | /graphql/15.8.0: 289 | resolution: 290 | { 291 | integrity: sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==, 292 | } 293 | engines: { node: ">= 10.x" } 294 | dev: false 295 | 296 | /humanize-ms/1.2.1: 297 | resolution: 298 | { 299 | integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==, 300 | } 301 | dependencies: 302 | ms: 2.1.3 303 | dev: false 304 | 305 | /is-buffer/1.1.6: 306 | resolution: 307 | { 308 | integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==, 309 | } 310 | dev: false 311 | 312 | /isomorphic-unfetch/3.1.0: 313 | resolution: 314 | { 315 | integrity: sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==, 316 | } 317 | dependencies: 318 | node-fetch: 2.7.0 319 | unfetch: 4.2.0 320 | transitivePeerDependencies: 321 | - encoding 322 | dev: false 323 | 324 | /make-error/1.3.6: 325 | resolution: 326 | { 327 | integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==, 328 | } 329 | dev: true 330 | 331 | /md5/2.3.0: 332 | resolution: 333 | { 334 | integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==, 335 | } 336 | dependencies: 337 | charenc: 0.0.2 338 | crypt: 0.0.2 339 | is-buffer: 1.1.6 340 | dev: false 341 | 342 | /mime-db/1.52.0: 343 | resolution: 344 | { 345 | integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==, 346 | } 347 | engines: { node: ">= 0.6" } 348 | dev: false 349 | 350 | /mime-types/2.1.35: 351 | resolution: 352 | { 353 | integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==, 354 | } 355 | engines: { node: ">= 0.6" } 356 | dependencies: 357 | mime-db: 1.52.0 358 | dev: false 359 | 360 | /ms/2.1.3: 361 | resolution: 362 | { 363 | integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==, 364 | } 365 | dev: false 366 | 367 | /node-domexception/1.0.0: 368 | resolution: 369 | { 370 | integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==, 371 | } 372 | engines: { node: ">=10.5.0" } 373 | dev: false 374 | 375 | /node-fetch/2.7.0: 376 | resolution: 377 | { 378 | integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==, 379 | } 380 | engines: { node: 4.x || >=6.0.0 } 381 | peerDependencies: 382 | encoding: ^0.1.0 383 | peerDependenciesMeta: 384 | encoding: 385 | optional: true 386 | dependencies: 387 | whatwg-url: 5.0.0 388 | dev: false 389 | 390 | /openai/4.11.0: 391 | resolution: 392 | { 393 | integrity: sha512-zU/MJxZTijL0Ym6CKoQPbnmHDsGZlH9g5zorPszdc41OyLxlhnlrorBcGzmGS9qpnjGGNncJR1hfg/mXq0OONw==, 394 | } 395 | hasBin: true 396 | dependencies: 397 | "@types/node": 18.18.1 398 | "@types/node-fetch": 2.6.6 399 | abort-controller: 3.0.0 400 | agentkeepalive: 4.5.0 401 | digest-fetch: 1.3.0 402 | form-data-encoder: 1.7.2 403 | formdata-node: 4.4.1 404 | node-fetch: 2.7.0 405 | transitivePeerDependencies: 406 | - encoding 407 | dev: false 408 | 409 | /prettier/3.0.3: 410 | resolution: 411 | { 412 | integrity: sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==, 413 | } 414 | engines: { node: ">=14" } 415 | hasBin: true 416 | dev: true 417 | 418 | /tr46/0.0.3: 419 | resolution: 420 | { 421 | integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==, 422 | } 423 | dev: false 424 | 425 | /ts-node/10.9.1_r6idwkry7t7s7ss6rqn36gsmbe: 426 | resolution: 427 | { 428 | integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==, 429 | } 430 | hasBin: true 431 | peerDependencies: 432 | "@swc/core": ">=1.2.50" 433 | "@swc/wasm": ">=1.2.50" 434 | "@types/node": "*" 435 | typescript: ">=2.7" 436 | peerDependenciesMeta: 437 | "@swc/core": 438 | optional: true 439 | "@swc/wasm": 440 | optional: true 441 | dependencies: 442 | "@cspotcode/source-map-support": 0.8.1 443 | "@tsconfig/node10": 1.0.9 444 | "@tsconfig/node12": 1.0.11 445 | "@tsconfig/node14": 1.0.3 446 | "@tsconfig/node16": 1.0.4 447 | "@types/node": 20.8.0 448 | acorn: 8.10.0 449 | acorn-walk: 8.2.0 450 | arg: 4.1.3 451 | create-require: 1.1.1 452 | diff: 4.0.2 453 | make-error: 1.3.6 454 | typescript: 5.2.2 455 | v8-compile-cache-lib: 3.0.1 456 | yn: 3.1.1 457 | dev: true 458 | 459 | /tslib/2.6.2: 460 | resolution: 461 | { 462 | integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==, 463 | } 464 | dev: true 465 | 466 | /typescript/5.2.2: 467 | resolution: 468 | { 469 | integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==, 470 | } 471 | engines: { node: ">=14.17" } 472 | hasBin: true 473 | dev: true 474 | 475 | /unfetch/4.2.0: 476 | resolution: 477 | { 478 | integrity: sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==, 479 | } 480 | dev: false 481 | 482 | /v8-compile-cache-lib/3.0.1: 483 | resolution: 484 | { 485 | integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==, 486 | } 487 | dev: true 488 | 489 | /web-streams-polyfill/4.0.0-beta.3: 490 | resolution: 491 | { 492 | integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==, 493 | } 494 | engines: { node: ">= 14" } 495 | dev: false 496 | 497 | /webidl-conversions/3.0.1: 498 | resolution: 499 | { 500 | integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==, 501 | } 502 | dev: false 503 | 504 | /whatwg-url/5.0.0: 505 | resolution: 506 | { 507 | integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==, 508 | } 509 | dependencies: 510 | tr46: 0.0.3 511 | webidl-conversions: 3.0.1 512 | dev: false 513 | 514 | /yn/3.1.1: 515 | resolution: 516 | { 517 | integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==, 518 | } 519 | engines: { node: ">=6" } 520 | dev: true 521 | --------------------------------------------------------------------------------