diff --git a/packages/cli/package.json b/packages/cli/package.json index 44bf2f086a..e0ecc82eb7 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -114,6 +114,7 @@ "minimatch": "^5.1.2", "moo": "^0.5.1", "open": "^8.2.1", + "openai": "^4.19.0", "openapi-diff": "^0.23.6", "openapi-types": "^12.1.3", "ora": "^5.4.1", diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 49a4b4c32e..01508ee077 100755 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -36,6 +36,7 @@ const CompareCommand = require('./cmds/compare/compare'); const CompareReportCommand = require('./cmds/compare-report/compareReport'); const InventoryCommand = require('./cmds/inventory/inventory'); const InventoryReportCommand = require('./cmds/inventory-report/inventoryReport'); +const Ask = require('./cmds/ask/ask'); import UploadCommand from './cmds/upload'; import { default as sqlErrorLog } from './lib/sqlErrorLog'; @@ -192,6 +193,7 @@ yargs(process.argv.slice(2)) .command(CompareReportCommand) .command(InventoryCommand) .command(InventoryReportCommand) + .command(Ask) .option('verbose', { alias: 'v', type: 'boolean', diff --git a/packages/cli/src/cmds/ask/ask.ts b/packages/cli/src/cmds/ask/ask.ts new file mode 100644 index 0000000000..00750c2f80 --- /dev/null +++ b/packages/cli/src/cmds/ask/ask.ts @@ -0,0 +1,349 @@ +import { warn } from 'console'; +import OpenAI from 'openai'; +import lunr from 'lunr'; +import { ChatCompletionMessageParam } from 'openai/resources'; +import { readFile } from 'fs/promises'; +import { AppMapFilter, CodeObject, Event, Metadata, buildAppMap } from '@appland/models'; +import { Action, Specification, buildDiagram, nodeName } from '@appland/sequence-diagram'; + +import { handleWorkingDirectory } from '../../lib/handleWorkingDirectory'; +import { locateAppMapDir } from '../../lib/locateAppMapDir'; +import { exists, verbose } from '../../utils'; +import FindAppMaps, { SearchResult as FindAppMapSearchResult } from '../../fulltext/FindAppMaps'; +import FindEvents, { SearchResult as FindEventSearchResult } from '../../fulltext/FindEvents'; + +export const command = 'ask '; +export const describe = + 'Ask a plain text question and get a filtered and configured AppMap as a response'; + +export const builder = (args) => { + args.positional('question', { + describe: 'plain text question about the code base', + }); + args.option('max-diagram-matches', { + describe: 'maximum number of diagram matches to return', + type: 'number', + default: 5, + }); + args.option('max-code-object-matches', { + describe: 'maximum number of code objects matches to return for each diagram', + type: 'number', + default: 5, + }); + args.option('directory', { + describe: 'program working directory', + type: 'string', + alias: 'd', + }); + return args.strict(); +}; + +function buildOpenAI(): OpenAI { + const OPENAI_API_KEY = process.env.OPENAI_API_KEY; + if (!OPENAI_API_KEY) { + throw new Error('OPENAI_API_KEY environment variable must be set'); + } + return new OpenAI({ apiKey: OPENAI_API_KEY }); +} + +type SerializedCodeObject = { + name: string; + type: string; + labels: string[]; + children: SerializedCodeObject[]; + static?: boolean; + sourceLocation?: string; +}; + +type ActionInfo = { + elapsed?: number; + eventId: number; + location?: string; +}; + +type SearchDiagramResult = { + diagramId: string; +}; + +type DiagramDetailsParam = { + search: string; + diagramIds: string[]; +}; + +type LookupSourceCodeParam = { + locations: string[]; +}; + +type LookupSourceCodeResult = Record; + +type EventInfo = { + name: string; + fqid?: string; + sourceLocation?: string; + elapsed?: number; + eventIds?: number[]; +}; + +type DiagramDetailsResult = { + summary: string; + metadata: Metadata; + keyEvents: FindEventSearchResult[]; +}; + +const isCamelized = (str: string): boolean => { + if (str.length < 3) return false; + + const testStr = str.slice(1); + return /[a-z][A-Z]/.test(testStr); +}; + +const splitCamelized = (str: string): string => { + if (!isCamelized(str)) return str; + + const result = new Array(); + let last = 0; + for (let i = 1; i < str.length; i++) { + const pc = str[i - 1]; + const c = str[i]; + const isUpper = c >= 'A' && c <= 'Z'; + if (isUpper) { + result.push(str.slice(last, i)); + last = i; + } + } + result.push(str.slice(last)); + return result.join(' '); +}; + +export const handler = async (argv: any) => { + verbose(argv.verbose); + handleWorkingDirectory(argv.directory); + const { question, maxCodeObjectMatches, maxDiagramMatches } = argv; + const appmapDir = await locateAppMapDir(argv.appmapDir); + + const findAppMaps = new FindAppMaps(appmapDir); + await findAppMaps.initialize(); + + function showPlan(paramStr: string) { + let params: any; + try { + params = JSON.parse(paramStr) as { plan: string }; + } catch (e) { + warn(`Failed to parse plan: ${paramStr}: ${e}`); + return; + } + warn(`AI Plan: ${params.plan}`); + } + + function fetchDiagrams(): FindAppMapSearchResult[] { + warn(`Fetching diagrams`); + return findAppMaps.search(question, { maxResults: maxDiagramMatches }); + } + + const diagramDetailsResults = new Array(); + + async function getDiagramDetails(paramStr: string): Promise { + const params = JSON.parse(paramStr) as DiagramDetailsParam; + const { diagramIds } = params; + warn(`Getting details for diagram ${diagramIds}, retrieved by "${question}"`); + const result = new Array(); + for (const diagramId of diagramIds) { + warn(`Loading AppMap ${diagramId} and pruning to 1MB`); + + const index = new FindEvents(diagramId); + index.maxSize = 1024 * 1024; + await index.initialize(); + const searchResults = index.search(question, { maxResults: maxCodeObjectMatches }); + diagramDetailsResults.push(...searchResults); + + const diagramText = new Array(); + for (const event of index.appmap.rootEvents()) { + const actionInfo: ActionInfo = { eventId: event.id }; + if (event.elapsedTime) actionInfo.elapsed = event.elapsedTime; + if (event.codeObject.location) actionInfo.location = event.codeObject.location; + const actionInfoStr = Object.keys(actionInfo) + .sort() + .map((key) => { + const value = actionInfo[key]; + return `${key}=${value}`; + }) + .join(','); + diagramText.push( + `${event.codeObject.id}${actionInfoStr !== '' ? ` (${actionInfoStr})` : ''}` + ); + } + + const metadata = index.appmap.metadata; + delete metadata['git']; + delete (metadata as any)['client']; + // TODO: Do we want the AI to read the source code of the test case? + delete metadata['source_location']; + result.push({ metadata, summary: diagramText.join('\n'), keyEvents: searchResults }); + } + + return result; + } + + async function lookupSourceCode( + locationStr: string + ): Promise { + const params = JSON.parse(locationStr) as LookupSourceCodeParam; + + const languageRegexMap: Record = { + '.rb': new RegExp(`def\\s+\\w+.*?\\n(.*?\\n)*?^end\\b`, 'gm'), + '.java': new RegExp( + `(?:public|private|protected)?\\s+(?:static\\s+)?(?:final\\s+)?(?:synchronized\\s+)?(?:abstract\\s+)?(?:native\\s+)?(?:strictfp\\s+)?(?:transient\\s+)?(?:volatile\\s+)?(?:\\w+\\s+)*\\w+\\s+\\w+\\s*\\([^)]*\\)\\s*(?:throws\\s+\\w+(?:,\\s*\\w+)*)?\\s*\\{(?:[^{}]*\\{[^{}]*\\})*[^{}]*\\}`, + 'gm' + ), + '.py': new RegExp(`def\\s+\\w+.*?:\\n(.*?\\n)*?`, 'gm'), + '.js': new RegExp( + `(?:async\\s+)?function\\s+\\w+\\s*\\([^)]*\\)\\s*\\{(?:[^{}]*\\{[^{}]*\\})*[^{}]*\\}`, + 'gm' + ), + }; + + const result: LookupSourceCodeResult = {}; + for (const location of params.locations) { + const [path, lineno] = location.split(':'); + + if (await exists(path)) { + const fileContent = await readFile(path, 'utf-8'); + let functionContent: string | undefined; + if (lineno) { + const extension = path.substring(path.lastIndexOf('.')); + const regex = languageRegexMap[extension]; + + if (regex) { + const match = regex.exec(fileContent); + if (match) { + const lines = match[0].split('\n'); + const startLine = parseInt(lineno, 10); + const endLine = startLine + lines.length - 1; + if (startLine <= endLine) { + functionContent = lines.slice(startLine - 1, endLine).join('\n'); + } + } + } + } else { + functionContent = fileContent; + } + if (functionContent) result[location] = functionContent; + } + } + return result; + } + + const systemMessages: ChatCompletionMessageParam[] = [ + 'You are an assistant that answers questions about the design and architecture of code.', + 'You answer these questions by accessing a knowledge base of sequence diagrams.', + 'Each sequence diagram conists of a series of events, such as function calls, HTTP server requests, SQL queries, etc.', + 'Before each function call, call "showPlan" function with a Markdown document that describes your strategy for answering the question.', + `Begin by calling the "fetchDiagrams" function to obtain the diagrams that are most relevant to the user's question.`, + 'Next, use the "getDiagramDetails" function get details about the events that occur with in the matching diagrams.', + 'Enhance your answer by using "lookupSourceCode" function to get the source code for the most relevant functions.', + 'Finally, respond with a Markdown document that summarizes the diagrams and answers the question.', + 'Never emit phrases like "note that the actual behavior may vary between different applications"', + ].map((msg) => ({ + content: msg, + role: 'system', + })); + + const userMessage: ChatCompletionMessageParam = { + content: question, + role: 'user', + }; + + const messages = [...systemMessages, userMessage]; + + const openai = buildOpenAI(); + const runFunctions = openai.beta.chat.completions.runFunctions({ + model: 'gpt-4', + messages, + function_call: 'auto', + functions: [ + { + function: showPlan, + description: 'Print the plan for answering the question', + parameters: { + type: 'object', + properties: { + plan: { + type: 'string', + description: 'The plan in Markdown format', + }, + }, + required: ['plan'], + }, + }, + { + function: fetchDiagrams, + description: `Obtain sequence diagrams that are relevant to the user's question. The response is a list of diagram ids.`, + parameters: { + type: 'object', + properties: {}, + }, + }, + { + function: getDiagramDetails, + description: `Get details about diagrams, including their name, code language, frameworks, source location, exceptions raised.`, + parameters: { + type: 'object', + properties: { + diagramIds: { + type: 'array', + description: 'Array of diagram ids', + items: { + type: 'string', + }, + }, + }, + required: ['search', 'diagramIds'], + }, + }, + { + function: lookupSourceCode, + description: `Get the source code for a specific function.`, + parameters: { + type: 'object', + properties: { + locations: { + type: 'array', + description: `An array of source code locations in the format [:]. Line number can be omitted if it's not known.`, + items: { + type: 'string', + }, + }, + }, + required: ['locations'], + }, + }, + ], + }); + + runFunctions.on('functionCall', (data) => { + warn(JSON.stringify(data, null, 2)); + }); + runFunctions.on('finalFunctionCall', (data) => { + warn(JSON.stringify(data, null, 2)); + }); + runFunctions.on('functionCallResult', (data) => { + if (verbose()) warn(JSON.stringify(data)); + }); + runFunctions.on('finalFunctionCallResult', (data) => { + if (verbose()) warn(JSON.stringify(data)); + }); + + const response = await runFunctions.finalContent(); + if (!response) { + warn(`No response from OpenAI`); + return; + } + console.log(response); + console.log(''); + console.log('The best matching sequence diagram events are:'); + console.log(''); + diagramDetailsResults.sort((a, b) => b.score - a.score); + for (const event of diagramDetailsResults) { + console.log(` ${event.fqid} (${event.score})`); + } +}; diff --git a/packages/cli/src/functionStats.js b/packages/cli/src/functionStats.js index 299dcf49c4..a2745b54d1 100644 --- a/packages/cli/src/functionStats.js +++ b/packages/cli/src/functionStats.js @@ -41,6 +41,7 @@ class FunctionStats { const trigram = (/** @type {Trigram} */ t) => [t.callerId, t.codeObjectId, t.calleeId].join(' ->\n'); return { + appmaps: this.appMapNames, returnValues: this.returnValues, httpServerRequests: this.httpServerRequests, sqlQueries: this.sqlQueries, @@ -58,6 +59,10 @@ class FunctionStats { return [...new Set(this.eventMatches.map((e) => e.appmap))].sort(); } + get appmaps() { + return this.appMapNames; + } + get returnValues() { return [...new Set(this.eventMatches.map((e) => e.event.returnValue).map(formatValue))].sort(); } diff --git a/packages/cli/src/inspect/context.ts b/packages/cli/src/inspect/context.ts index 2a0e1e2ae8..11577fec5d 100644 --- a/packages/cli/src/inspect/context.ts +++ b/packages/cli/src/inspect/context.ts @@ -3,7 +3,7 @@ import EventEmitter from 'events'; import { default as FunctionStatsImpl } from '../functionStats'; import FindCodeObjects from '../search/findCodeObjects'; import FindEvents from '../search/findEvents'; -import { FunctionStats, Filter, CodeObjectMatch } from '../search/types'; +import { FunctionStats, Filter, CodeObjectMatch, EventMatch } from '../search/types'; export default class Context extends EventEmitter { public filters: Filter[] = []; @@ -38,12 +38,12 @@ export default class Context extends EventEmitter { this.emit('stop'); } - async buildStats() { + async buildStats(): Promise { assert(this.codeObjectMatches, `codeObjectMatches is not yet computed`); this.emit('start', this.codeObjectMatches.length); - const result: any[] = []; + const result: EventMatch[] = []; await Promise.all( this.codeObjectMatches.map(async (codeObjectMatch) => { const findEvents = new FindEvents(codeObjectMatch.appmap, codeObjectMatch.codeObject); @@ -57,6 +57,7 @@ export default class Context extends EventEmitter { this.emit('collate'); this.stats = new FunctionStatsImpl(result); + return this.stats; } save() { diff --git a/packages/cli/src/search/types.d.ts b/packages/cli/src/search/types.d.ts index d70414183d..0fb2cbe818 100644 --- a/packages/cli/src/search/types.d.ts +++ b/packages/cli/src/search/types.d.ts @@ -95,6 +95,7 @@ export interface Filter { } export interface FunctionStats { + appmaps: string[]; eventMatches: EventMatch[]; returnValues: string[]; httpServerRequests: string[]; diff --git a/yarn.lock b/yarn.lock index d2a8665e1a..72f99bc241 100644 --- a/yarn.lock +++ b/yarn.lock @@ -185,6 +185,7 @@ __metadata: moo: ^0.5.1 node-fetch: 2.6.7 open: ^8.2.1 + openai: ^4.19.0 openapi-diff: ^0.23.6 openapi-types: ^12.1.3 ora: ^5.4.1 @@ -8372,6 +8373,16 @@ __metadata: languageName: node linkType: hard +"@types/node-fetch@npm:^2.6.4": + version: 2.6.9 + resolution: "@types/node-fetch@npm:2.6.9" + dependencies: + "@types/node": "*" + form-data: ^4.0.0 + checksum: 212269aff4b251477c13c33cee6cea23e4fd630be6c0bfa3714968cce7efd7055b52f2f82aab3394596d8c758335cc802e7c5fa3f775e7f2a472fa914c90dc15 + languageName: node + linkType: hard + "@types/node@npm:*, @types/node@npm:^17.0.2": version: 17.0.17 resolution: "@types/node@npm:17.0.17" @@ -8407,6 +8418,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^18.11.18": + version: 18.18.9 + resolution: "@types/node@npm:18.18.9" + dependencies: + undici-types: ~5.26.4 + checksum: 629ce20357586144031cb43d601617eef45e59460dea6b1e123f708e6885664f44d54f65e5b72b2614af5b8613f3652ced832649c0b991accbc6a85139befa51 + languageName: node + linkType: hard + "@types/normalize-package-data@npm:^2.4.0": version: 2.4.1 resolution: "@types/normalize-package-data@npm:2.4.1" @@ -10250,6 +10270,15 @@ __metadata: languageName: node linkType: hard +"abort-controller@npm:^3.0.0": + version: 3.0.0 + resolution: "abort-controller@npm:3.0.0" + dependencies: + event-target-shim: ^5.0.0 + checksum: 170bdba9b47b7e65906a28c8ce4f38a7a369d78e2271706f020849c1bfe0ee2067d4261df8bbb66eb84f79208fd5b710df759d64191db58cfba7ce8ef9c54b75 + languageName: node + linkType: hard + "accepts@npm:~1.3.4, accepts@npm:~1.3.5, accepts@npm:~1.3.7": version: 1.3.8 resolution: "accepts@npm:1.3.8" @@ -11963,6 +11992,13 @@ __metadata: languageName: node linkType: hard +"base-64@npm:^0.1.0": + version: 0.1.0 + resolution: "base-64@npm:0.1.0" + checksum: 5a42938f82372ab5392cbacc85a5a78115cbbd9dbef9f7540fa47d78763a3a8bd7d598475f0d92341f66285afd377509851a9bb5c67bbecb89686e9255d5b3eb + languageName: node + linkType: hard + "base64-js@npm:^1.0.2, base64-js@npm:^1.3.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" @@ -13025,6 +13061,13 @@ __metadata: languageName: node linkType: hard +"charenc@npm:0.0.2": + version: 0.0.2 + resolution: "charenc@npm:0.0.2" + checksum: 81dcadbe57e861d527faf6dd3855dc857395a1c4d6781f4847288ab23cffb7b3ee80d57c15bba7252ffe3e5e8019db767757ee7975663ad2ca0939bb8fcaf2e5 + languageName: node + linkType: hard + "check-more-types@npm:2.24.0, check-more-types@npm:^2.24.0": version: 2.24.0 resolution: "check-more-types@npm:2.24.0" @@ -14276,6 +14319,13 @@ __metadata: languageName: node linkType: hard +"crypt@npm:0.0.2": + version: 0.0.2 + resolution: "crypt@npm:0.0.2" + checksum: baf4c7bbe05df656ec230018af8cf7dbe8c14b36b98726939cef008d473f6fe7a4fad906cfea4062c93af516f1550a3f43ceb4d6615329612c6511378ed9fe34 + languageName: node + linkType: hard + "crypto-browserify@npm:^3.11.0": version: 3.12.0 resolution: "crypto-browserify@npm:3.12.0" @@ -15666,6 +15716,16 @@ __metadata: languageName: node linkType: hard +"digest-fetch@npm:^1.3.0": + version: 1.3.0 + resolution: "digest-fetch@npm:1.3.0" + dependencies: + base-64: ^0.1.0 + md5: ^2.3.0 + checksum: 8ebdb4b9ef02b1ac0da532d25c7d08388f2552813dfadabfe7c4630e944bb4a48093b997fc926440a10e1ccf4912f2ce9adcf2d6687b0518dab8480e08f22f9d + languageName: node + linkType: hard + "dir-glob@npm:^2.0.0, dir-glob@npm:^2.2.2": version: 2.2.2 resolution: "dir-glob@npm:2.2.2" @@ -17383,6 +17443,13 @@ __metadata: languageName: node linkType: hard +"event-target-shim@npm:^5.0.0": + version: 5.0.1 + resolution: "event-target-shim@npm:5.0.1" + checksum: 1ffe3bb22a6d51bdeb6bf6f7cf97d2ff4a74b017ad12284cc9e6a279e727dc30a5de6bb613e5596ff4dc3e517841339ad09a7eec44266eccb1aa201a30448166 + languageName: node + linkType: hard + "eventemitter2@npm:6.4.7": version: 6.4.7 resolution: "eventemitter2@npm:6.4.7" @@ -18261,6 +18328,13 @@ __metadata: languageName: node linkType: hard +"form-data-encoder@npm:1.7.2": + version: 1.7.2 + resolution: "form-data-encoder@npm:1.7.2" + checksum: aeebd87a1cb009e13cbb5e4e4008e6202ed5f6551eb6d9582ba8a062005178907b90f4887899d3c993de879159b6c0c940af8196725b428b4248cec5af3acf5f + languageName: node + linkType: hard + "form-data@npm:*, form-data@npm:^4.0.0": version: 4.0.0 resolution: "form-data@npm:4.0.0" @@ -18301,6 +18375,16 @@ __metadata: languageName: node linkType: hard +"formdata-node@npm:^4.3.2": + version: 4.4.1 + resolution: "formdata-node@npm:4.4.1" + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + checksum: d91d4f667cfed74827fc281594102c0dabddd03c9f8b426fc97123eedbf73f5060ee43205d89284d6854e2fc5827e030cd352ef68b93beda8decc2d72128c576 + languageName: node + linkType: hard + "forwarded@npm:0.2.0": version: 0.2.0 resolution: "forwarded@npm:0.2.0" @@ -20482,7 +20566,7 @@ __metadata: languageName: node linkType: hard -"is-buffer@npm:^1.1.5": +"is-buffer@npm:^1.1.5, is-buffer@npm:~1.1.6": version: 1.1.6 resolution: "is-buffer@npm:1.1.6" checksum: 4a186d995d8bbf9153b4bd9ff9fd04ae75068fe695d29025d25e592d9488911eeece84eefbd8fa41b8ddcc0711058a71d4c466dcf6f1f6e1d83830052d8ca707 @@ -25094,6 +25178,17 @@ __metadata: languageName: node linkType: hard +"md5@npm:^2.3.0": + version: 2.3.0 + resolution: "md5@npm:2.3.0" + dependencies: + charenc: 0.0.2 + crypt: 0.0.2 + is-buffer: ~1.1.6 + checksum: a63cacf4018dc9dee08c36e6f924a64ced735b37826116c905717c41cebeb41a522f7a526ba6ad578f9c80f02cb365033ccd67fe186ffbcc1a1faeb75daa9b6e + languageName: node + linkType: hard + "mdast-squeeze-paragraphs@npm:^4.0.0": version: 4.0.0 resolution: "mdast-squeeze-paragraphs@npm:4.0.0" @@ -26101,6 +26196,13 @@ __metadata: languageName: node linkType: hard +"node-domexception@npm:1.0.0": + version: 1.0.0 + resolution: "node-domexception@npm:1.0.0" + checksum: ee1d37dd2a4eb26a8a92cd6b64dfc29caec72bff5e1ed9aba80c294f57a31ba4895a60fd48347cf17dd6e766da0ae87d75657dfd1f384ebfa60462c2283f5c7f + languageName: node + linkType: hard + "node-emoji@npm:^1.11.0": version: 1.11.0 resolution: "node-emoji@npm:1.11.0" @@ -27054,6 +27156,25 @@ __metadata: languageName: node linkType: hard +"openai@npm:^4.19.0": + version: 4.19.0 + resolution: "openai@npm:4.19.0" + dependencies: + "@types/node": ^18.11.18 + "@types/node-fetch": ^2.6.4 + abort-controller: ^3.0.0 + agentkeepalive: ^4.2.1 + digest-fetch: ^1.3.0 + form-data-encoder: 1.7.2 + formdata-node: ^4.3.2 + node-fetch: ^2.6.7 + web-streams-polyfill: ^3.2.1 + bin: + openai: bin/cli + checksum: 906c79b73a236504d2ca30a104c988d30833c37ccdffe68755665b285116d56baf03a00027587e9390e6e87894c2a6783f105826a2f66a67fb948300efac14f1 + languageName: node + linkType: hard + "openapi-diff@npm:^0.23.6": version: 0.23.6 resolution: "openapi-diff@npm:0.23.6" @@ -34170,6 +34291,13 @@ typescript@~4.4.3: languageName: node linkType: hard +"undici-types@npm:~5.26.4": + version: 5.26.5 + resolution: "undici-types@npm:5.26.5" + checksum: 3192ef6f3fd5df652f2dc1cd782b49d6ff14dc98e5dced492aa8a8c65425227da5da6aafe22523c67f035a272c599bb89cfe803c1db6311e44bed3042fc25487 + languageName: node + linkType: hard + "unfetch@npm:^4.2.0": version: 4.2.0 resolution: "unfetch@npm:4.2.0" @@ -35193,6 +35321,20 @@ typescript@~4.4.3: languageName: node linkType: hard +"web-streams-polyfill@npm:4.0.0-beta.3": + version: 4.0.0-beta.3 + resolution: "web-streams-polyfill@npm:4.0.0-beta.3" + checksum: dfec1fbf52b9140e4183a941e380487b6c3d5d3838dd1259be81506c1c9f2abfcf5aeb670aeeecfd9dff4271a6d8fef931b193c7bedfb42542a3b05ff36c0d16 + languageName: node + linkType: hard + +"web-streams-polyfill@npm:^3.2.1": + version: 3.2.1 + resolution: "web-streams-polyfill@npm:3.2.1" + checksum: b119c78574b6d65935e35098c2afdcd752b84268e18746606af149e3c424e15621b6f1ff0b42b2676dc012fc4f0d313f964b41a4b5031e525faa03997457da02 + languageName: node + linkType: hard + "webidl-conversions@npm:^3.0.0": version: 3.0.1 resolution: "webidl-conversions@npm:3.0.1"