diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 21b34062..f08d064b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,6 +7,23 @@ on: jobs: yarn-lockfile-check: uses: salesforcecli/github-workflows/.github/workflows/lockFileCheck.yml@main + + # Detect which files have changed to determine what tests to run + changes: + runs-on: ubuntu-latest + outputs: + confidence-changed: ${{ steps.changes.outputs.confidence }} + steps: + - uses: actions/checkout@v4 + - uses: dorny/paths-filter@v2 + id: changes + with: + filters: | + confidence: + - 'confidence/**' + - 'test/confidence/**' + - 'src/tools/**' + # Since the Windows unit tests take much longer, we run the linux unit tests first and then run the windows unit tests in parallel with NUTs linux-unit-tests: needs: yarn-lockfile-check @@ -15,6 +32,31 @@ jobs: needs: linux-unit-tests uses: salesforcecli/github-workflows/.github/workflows/unitTestsWindows.yml@main + # Run the confidence tests after the unit tests + confidence-tests: + needs: [linux-unit-tests, changes] + runs-on: ubuntu-latest + if: ${{ needs.changes.outputs.confidence-changed == 'true'}} + env: + SF_MCP_CONFIDENCE_CONSUMER_KEY: ${{ secrets.SF_MCP_CONFIDENCE_CONSUMER_KEY }} + SF_MCP_CONFIDENCE_CONSUMER_SECRET: ${{ secrets.SF_MCP_CONFIDENCE_CONSUMER_SECRET }} + SF_MCP_CONFIDENCE_INSTANCE_URL: ${{ secrets.SF_MCP_CONFIDENCE_INSTANCE_URL }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + cache: yarn + - run: yarn install --frozen-lockfile + # Note: we cannot parallelize confidence tests since we don't have the rate limits to support it + # the test runner has rate limiting built-in to prevent hitting the API limits within that test run + - name: Run confidence tests + run: | + for file in test/confidence/*.yml; do + echo "Running confidence test for $file" + yarn test:confidence --file "$file" + done + # Uncomment to enable NUT testing in Github Actions # nuts: # needs: linux-unit-tests diff --git a/DEVELOPING.md b/DEVELOPING.md index 962cd107..f9f6362d 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -124,9 +124,59 @@ mcp-inspector --cli node bin/run.js --orgs DEFAULT_TARGET_ORG --method tools/lis Unit tests are run with `yarn test` and use the Mocha test framework. Tests are located in the `test` directory and are named with the pattern, `test/**/*.test.ts`. +### Confidence Tests + +Confidence tests validate that the MCP server tools are accurately invoked by various LLM models through the Salesforce LLM Gateway API. These tests ensure that natural language prompts correctly trigger the expected tools with appropriate parameters, maintaining the quality of the AI-powered tool selection. + +#### Running Confidence Tests Locally + +1. **Set up API access**: Follow this [documentation](https://developer.salesforce.com/docs/einstein/genai/guide/access-models-api-with-rest.html) to setup an External Client App that will give you access to the Models API. Once you have the consumer key and secret from the External Client App, you'll need to add these to environment variables: + + ```shell + export SF_MCP_CONFIDENCE_CONSUMER_KEY=your_client_id_here + export SF_MCP_CONFIDENCE_CONSUMER_SECRET=your_client_secret_here + export SF_MCP_CONFIDENCE_INSTANCE_URL=https://your_instance.salesforce.com + ``` + + These environment variables are used to generate a JWT token that will be used to authenticate with the Models API. + +2. **Run a confidence test**: + ```shell + yarn test:confidence --file test/confidence/sf-deploy-metadata.yml + ``` + +#### Test Structure + +Confidence tests are defined in YAML files located in `test/confidence/`. Each test file specifies: + +- **Models**: Which LLM models to test against. See the Agentforce Developer Guide for [available models](https://developer.salesforce.com/docs/einstein/genai/guide/supported-models.html). +- **Initial Context**: Background information provided to the model +- **Test Cases**: Natural language utterances with expected tool invocations and confidence thresholds + +The tests run multiple iterations (default: 5) to calculate confidence levels and ensure consistent tool selection across different model runs. This can be adjusted by passing the `--runs` flag when running the tests, like this: + +```shell +yarn test:confidence test/confidence/sf-deploy-metadata.yml --runs 2 +``` + +#### Understanding Test Results + +Tests measure two types of confidence: + +- **Tool Confidence**: Whether the correct tool was invoked +- **Parameter Confidence**: Whether the tool was called with the expected parameters + +Failed tests indicate that either: + +1. The model selected the wrong tool for a given prompt +2. The model selected the correct tool but with incorrect parameters +3. The confidence level fell below the specified threshold + +These failures help identify areas where tool descriptions or agent instructions need improvement. + ## Debugging -> [!NOTE] +> [!NOTE] > This section assumes you're using Visual Studio Code (VS Code). You can use the VS Code debugger with the MCP Inspector CLI to step through the code of your MCP tools: @@ -150,7 +200,7 @@ MCP_SERVER_REQUEST_TIMEOUT=120000 mcp-inspector --cli node --inspect-brk bin/run We suggest you set `MCP_SERVER_REQUEST_TIMEOUT` to 120000ms (2 minutes) to allow longer debugging sessions without having the MCP Inspector client timeout. For other configuration values see: https://github.com/modelcontextprotocol/inspector?tab=readme-ov-file#configuration -> [!IMPORTANT] +> [!IMPORTANT] > You must compile the local MCP server using `yarn compile` after every change in a TypeScript file, otherwise breakpoints in the TypeScript files might not match the running JavaScript code. ## Useful yarn Commands diff --git a/confidence/.eslintrc.cjs b/confidence/.eslintrc.cjs new file mode 100644 index 00000000..9b35591d --- /dev/null +++ b/confidence/.eslintrc.cjs @@ -0,0 +1,29 @@ +/* + * Copyright 2025, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module.exports = { + extends: '../.eslintrc.cjs', + parserOptions: { + project: [ + './tsconfig.json', + './test/tsconfig.json', + './confidence/tsconfig.json', // Add this line + ], + }, + rules: { + 'import/no-extraneous-dependencies': ['error', { devDependencies: true }], + }, +}; diff --git a/confidence/bin/dev.js b/confidence/bin/dev.js new file mode 100755 index 00000000..30fd6c8d --- /dev/null +++ b/confidence/bin/dev.js @@ -0,0 +1,23 @@ +#!/usr/bin/env -S node --loader ts-node/esm --disable-warning=ExperimentalWarning + +import { dirname } from 'node:path'; +import { execute } from '@oclif/core'; + +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // Disable TLS verification for local testing +await execute({ + development: true, + dir: import.meta.url, + loadOptions: { + root: dirname(import.meta.dirname), + pjson: { + name: 'mcp-test', + version: '1.0.0', + oclif: { + bin: 'mcp-test', + dirname: 'mcp-test', + commands: './lib/commands', + topicSeparator: ' ', + }, + }, + }, +}); diff --git a/confidence/bin/run.js b/confidence/bin/run.js new file mode 100755 index 00000000..4c7af587 --- /dev/null +++ b/confidence/bin/run.js @@ -0,0 +1,22 @@ +#!/usr/bin/env node + +import { dirname } from 'node:path'; +import { execute } from '@oclif/core'; + +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // Disable TLS verification for local testing +await execute({ + dir: import.meta.url, + loadOptions: { + root: dirname(import.meta.dirname), + pjson: { + name: 'mcp-test', + version: '1.0.0', + oclif: { + bin: 'mcp-test', + dirname: 'mcp-test', + commands: './lib/commands', + topicSeparator: ' ', + }, + }, + }, +}); diff --git a/confidence/src/commands/confidence-test.ts b/confidence/src/commands/confidence-test.ts new file mode 100644 index 00000000..39735e0b --- /dev/null +++ b/confidence/src/commands/confidence-test.ts @@ -0,0 +1,483 @@ +/* + * Copyright 2025, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { printTable } from '@oclif/table'; +import { stdout, colorize } from '@oclif/core/ux'; +import { Command, Flags } from '@oclif/core'; +import { z } from 'zod'; +import { makeGatewayRequests } from '../utils/gateway.js'; +import { getToolsList, InvocableTool } from '../utils/tools.js'; +import { TABLE_STYLE } from '../utils/table.js'; +import { readYamlFile } from '../utils/yaml.js'; +import { Model } from '../utils/models.js'; +import { mintJWT } from '../utils/jwt.js'; + +const Spec = z.object({ + models: z.array(z.custom()), + 'initial-context': z.array(z.string()).optional(), + tests: z.array( + z.object({ + utterances: z.union([z.string(), z.array(z.string())]), + 'expected-tool': z.string(), + 'expected-parameters': z.record(z.string(), z.string()).optional(), + 'expected-tool-confidence': z.number(), + 'expected-parameter-confidence': z.number().optional(), + 'allowed-tools': z.array(z.string()).optional(), + skip: z.boolean().optional(), + only: z.boolean().optional(), + }) + ), +}); + +type Spec = z.infer; + +type TestCase = { + readable: string; + utterances: string[]; + expectedTool: string; + expectedParameters?: Record; + expectedToolConfidence: number; + expectedParameterConfidence: number; + allowedTools: string[]; +}; + +const castToArray = (value: T | T[]): T[] => (Array.isArray(value) ? value : [value]); + +const groupBy = (array: T[], key: (item: T) => K): Record => + array.reduce>((result, item) => { + const groupKey = key(item); + if (!result[groupKey]) { + return { ...result, [groupKey]: [item] }; + } + return { ...result, [groupKey]: [...result[groupKey], item] }; + // eslint-disable-next-line + }, {} as Record); + +const makeReadableParameters = (param: Record): string => + Object.entries(param) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([key, value]) => ` - ${key}: ${value}`) + .join('\n'); + +const countRunsThatPassParameterMatching = ( + testSpec: TestCase, + runs: Array<{ model: string; invocations: Array<{ tool: string; parameters: Record }> }> +): number => + runs.filter((run) => + Object.entries(testSpec.expectedParameters ?? {}).every(([key, value]) => + run.invocations.some( + (inv) => + inv.tool === testSpec.expectedTool && inv.parameters[key] && new RegExp(value).test(inv.parameters[key]) + ) + ) + ).length; + +const countRunsThatPassToolMatching = ( + testSpec: TestCase, + runs: Array<{ model: string; invocations: Array<{ tool: string; parameters: Record }> }> +): number => + runs.filter( + ({ invocations }) => + invocations.some((inv) => inv.tool === testSpec.expectedTool) && + invocations.every((inv) => testSpec.allowedTools.includes(inv.tool)) + ).length; + +const filterFailingTests = ( + passFailMap: Map, + testIndex: Map, + type: 'tools' | 'parameters' +): TestCase[] => + Array.from(passFailMap.entries()) + .filter(([, result]) => !result[type]) + .map(([key]) => testIndex.get(key)) + .filter((test) => test !== undefined); + +async function compareModelOutputs( + jwtToken: string, + utterances: string | string[], + spec: Spec, + tools: InvocableTool[] +): Promise<{ + tableData: Array<{ model: Model; chat: string; tools: string }>; + invocations: Record }>>; +}> { + const models = spec.models; + const responses = await Promise.all( + models.map((model) => makeGatewayRequests(jwtToken, castToArray(utterances), model, tools, spec['initial-context'])) + ); + + const invocations = responses.reduce }>>>( + (acc, response) => { + const toolInvocations = response.responses.flatMap((r) => { + const toolInvocation = r.generation_details?.generations[0].tool_invocations?.[0]; + if (!toolInvocation) return []; + + const parameters: Record = toolInvocation.function.arguments + ? (JSON.parse(toolInvocation.function.arguments) as Record) + : {}; + + return [ + { + tool: toolInvocation.function.name, + parameters, + }, + ]; + }); + return { ...acc, [response.model]: toolInvocations }; + }, + {} + ); + + const tableData = responses.map((response) => ({ + model: response.model, + chat: response.messages.map((m) => `${colorize('bold', m.role)}: ${m.content}`).join('\n\n'), + tools: response.responses + .map((r, index) => { + const toolInvocation = r.generation_details?.generations[0].tool_invocations?.[0]; + if (!toolInvocation) { + return `Generation ${index + 1}: No tool invoked`; + } + + const toolArgs = JSON.parse(toolInvocation.function.arguments ?? '{}') as Record; + const argsString = makeReadableParameters(toolArgs); + + return `Generation ${index + 1}: ${colorize('bold', toolInvocation.function.name)}${ + argsString ? `\n${argsString}` : '' + }`; + }) + .join('\n\n'), + })); + return { invocations, tableData }; +} + +export default class ConfidenceTest extends Command { + public static summary = 'Test the MCP server against the LLM Gateway API'; + public static description = `Tests that the MCP server tools are accurately invoked by various LLM models. + +Configuration: +- Uses a YAML file to specify models and test cases +- Requires SF_MCP_CONFIDENCE_CONSUMER_KEY environment variable +- Requires SF_MCP_CONFIDENCE_CONSUMER_SECRET environment variable +- Requires SF_MCP_CONFIDENCE_INSTANCE_URL environment variable + +At runtime, the SF_MCP_CONFIDENCE_CONSUMER_KEY and SF_MCP_CONFIDENCE_CONSUMER_SECRET are used to generate a JWT token from a External Client App in the production org (SF_MCP_CONFIDENCE_INSTANCE_URL). +This token is then used to authenticate requests to the LLM Gateway API. + +YAML File Format: +The YAML file should contain: +- models: Array of model identifiers to test against +- initial-context: Optional array of strings to set the initial context for the conversation +- tests: Array of test objects with the following properties: + - utterances: String or array of strings for test utterances (supports multi-turn conversations) + - expected-tool: String identifying the expected tool to be invoked + - expected-parameters: Optional object with expected parameter key-value pairs + - expected-tool-confidence: Number representing the minimum confidence level (0-100) + - expected-parameter-confidence: Optional number for parameter confidence (defaults to expected-tool-confidence) + - allowed-tools: Optional array of tool names that are acceptable in addition to the expected tool + +Example YAML structure: +models: + - llmgateway__OpenAIGPT35Turbo_01_25 + - llmgateway__OpenAIGPT4OmniMini +tests: + - utterances: "What's my salesforce username?" + expected-tool: sf-org-display + expected-tool-confidence: 80 + - utterances: ["I am a Salesforce developer", "Deploy my project"] + expected-tool: sf-deploy-metadata + expected-parameters: + source-dir: "force-app" + expected-tool-confidence: 90 + allowed-tools: + - sf-list-all-orgs + - utterances: + - I am a Salesforce developer. + - Deploy my project + expected-tool: sf-deploy-metadata + expected-tool-confidence: 85 + +For available models, see: +https://developer.salesforce.com/docs/einstein/genai/guide/supported-models.html`; + + public static flags = { + file: Flags.file({ + summary: 'The YAML file to use for the response', + description: 'Must contain array of models and test cases', + required: true, + exists: true, + char: 'f', + }), + help: Flags.help({ + description: 'Show help', + char: 'h', + }), + runs: Flags.integer({ + summary: 'Number of runs to use for confidence level', + description: 'If specified, will run the tool multiple times to determine confidence level', + default: 5, + char: 'r', + }), + concise: Flags.boolean({ + summary: 'Suppress detailed output for each test run', + description: 'If true, will print only the final results of each test run', + default: false, + char: 'c', + }), + }; + + public async run(): Promise { + const { flags } = await this.parse(ConfidenceTest); + + const jwtToken = await mintJWT(); + + const spec = Spec.safeParse(await readYamlFile(flags.file)); + if (!spec.success) { + this.error(`Invalid spec file: ${flags.file}\n${spec.error.message}`); + } + + const { tools: mcpTools, tokens } = await getToolsList(); + if (!flags.concise) { + stdout(); + printTable({ + title: 'Tools List', + data: tokens, + columns: [ + 'tool', + { key: 'tokensGPT4oMini', name: 'GPT 4o Mini' }, + { key: 'tokensO3Mini', name: 'O3 Mini' }, + { key: 'tokensGPT4', name: 'GPT 4' }, + ], + titleOptions: { + color: 'yellowBright', + }, + ...TABLE_STYLE, + }); + stdout(); + } + + const filteredTests = spec.data.tests.some((test) => test.only) + ? [spec.data.tests.find((test) => test.only)!] + : spec.data.tests.filter((test) => !test.skip); + + // Generate unique keys for each test case to track runs + // This allows us to group runs by test case and display results clearly + const testIndex = new Map(); + + // Map to store test results by testCaseKey + // Each entry will contain an array of runs for that test case + // This allows us to aggregate results and print them after all runs are complete + const testResultsByTestCaseKey = new Map< + string, + Array<{ + idx: number; + testCaseKey: string; + invocations: Record }>>; + tableData: Array<{ model: Model; chat: string; tools: string }>; + }> + >(); + + // Map to track pass/fail status by testCaseKey + const passFailMap = new Map(); + + const maybePrintTestResults = (testCaseKey: string): void => { + const testRuns = (testResultsByTestCaseKey.get(testCaseKey) ?? []).sort((a, b) => a.idx - b.idx); + if (testRuns.length < flags.runs) { + return; // Not enough runs yet to print results + } + + const testSpec = testIndex.get(testCaseKey); + if (!testSpec) { + stdout(colorize('red', `No test spec found for utterance key: ${testCaseKey}`)); + return; + } + + stdout(colorize('bold', ' ─── Results for Test Case ───')); + stdout(testSpec.readable); + + if (!flags.concise) { + for (const run of testRuns) { + printTable({ + title: `Run #${run.idx + 1}`, + data: run.tableData, + columns: [ + { key: 'model', width: '30%' }, + { key: 'chat', width: '40%' }, + { key: 'tools', width: '30%', name: 'Tool Invocations' }, + ], + width: process.stdout.columns, + ...TABLE_STYLE, + }); + } + } + + const runsByModel = groupBy( + testRuns.flatMap((result) => + Object.entries(result.invocations).map(([model, invocations]) => ({ + model, + invocations, + })) + ), + (r) => r.model + ); + + printTable({ + title: 'Tool Invocations', + data: Object.entries(runsByModel).map(([model, runs]) => { + const actualToolCount = countRunsThatPassToolMatching(testSpec, runs); + const totalRuns = runs.length; + const confidence = Math.round((actualToolCount / totalRuns) * 100); + + if (confidence < testSpec.expectedToolConfidence) { + passFailMap.set(testCaseKey, { + ...(passFailMap.get(testCaseKey) ?? { tools: true, parameters: true }), + tools: false, + }); + } + + return { + model, + expectedTool: testSpec.expectedTool, + actualTools: runs + .map((r, idx) => `Run ${idx + 1}: ${r.invocations.flatMap((inv) => inv.tool).join(', ')}`) + .join('\n'), + count: `${actualToolCount}/${totalRuns}`, + actualConfidence: `${confidence}%`, + expectedConfidence: `${testSpec.expectedToolConfidence}%`, + status: confidence >= testSpec.expectedToolConfidence ? colorize('green', 'PASS') : colorize('red', 'FAIL'), + }; + }), + columns: [ + { key: 'model', name: 'Model', width: '30%' }, + { key: 'expectedTool', name: 'Expected Tool Invocation', width: '15%' }, + { key: 'actualTools', name: 'Actual Tool Invocations', width: '25%' }, + { key: 'count', name: 'Count', width: '7%' }, + { key: 'expectedConfidence', name: 'Expected Confidence', width: '8%' }, + { key: 'actualConfidence', name: 'Actual Confidence', width: '8%' }, + { key: 'status', name: 'Status', width: '7%' }, + ], + ...TABLE_STYLE, + width: process.stdout.columns, + }); + + if (testSpec.expectedParameters) { + printTable({ + title: 'Parameter Matching', + data: Object.entries(runsByModel).map(([model, runs]) => { + const runsThatMatchParameters = countRunsThatPassParameterMatching(testSpec, runs); + const totalRuns = runs.length; + const confidence = Math.round((runsThatMatchParameters / totalRuns) * 100); + + if (confidence < testSpec.expectedParameterConfidence) { + passFailMap.set(testCaseKey, { + ...(passFailMap.get(testCaseKey) ?? { tools: true, parameters: true }), + parameters: false, + }); + } + + return { + model, + count: `${runsThatMatchParameters}/${totalRuns}`, + expectedParameters: makeReadableParameters(testSpec.expectedParameters ?? {}), + actualParameters: runs + .map( + (r, idx) => + `Run ${idx + 1}:\n${makeReadableParameters( + r.invocations.find((inv) => inv.tool === testSpec.expectedTool)?.parameters ?? {} + )}` + ) + .join('\n'), + actualConfidence: `${confidence}%`, + expectedConfidence: `${testSpec.expectedParameterConfidence}%`, + status: + confidence >= testSpec.expectedParameterConfidence + ? colorize('green', 'PASS') + : colorize('red', 'FAIL'), + }; + }), + columns: [ + { key: 'model', name: 'Model', width: '30%' }, + { key: 'expectedParameters', name: 'Expected Parameters', width: '15%' }, + { key: 'actualParameters', name: 'Actual Parameters', width: '25%' }, + { key: 'count', name: 'Count', width: '7%' }, + { key: 'expectedConfidence', name: 'Expected Confidence', width: '8%' }, + { key: 'actualConfidence', name: 'Actual Confidence', width: '8%' }, + { key: 'status', name: 'Status', width: '7%' }, + ], + ...TABLE_STYLE, + width: process.stdout.columns, + }); + } + }; + + await Promise.all( + filteredTests.flatMap((test) => { + const testCaseKey = Math.random().toString(36).substring(2, 15); + testIndex.set(testCaseKey, { + readable: `${colorize('yellowBright', 'Utterance')}:\n - ${castToArray(test.utterances).join('\n - ')}`, + utterances: castToArray(test.utterances), + expectedTool: test['expected-tool'], + expectedParameters: test['expected-parameters'], + expectedToolConfidence: test['expected-tool-confidence'], + expectedParameterConfidence: test['expected-parameter-confidence'] ?? test['expected-tool-confidence'], + allowedTools: [test['expected-tool'], ...(test['allowed-tools'] ?? [])], + }); + passFailMap.set(testCaseKey, { + tools: true, + parameters: true, + }); + return Array.from({ length: flags.runs }, (_, idx) => + compareModelOutputs(jwtToken, test.utterances, spec.data, mcpTools).then(({ invocations, tableData }) => { + testResultsByTestCaseKey.set(testCaseKey, [ + ...(testResultsByTestCaseKey.get(testCaseKey) ?? []), + { + idx, + testCaseKey, + invocations, + tableData, + }, + ]); + maybePrintTestResults(testCaseKey); + }) + ); + }) + ); + + const failingToolTests = filterFailingTests(passFailMap, testIndex, 'tools'); + const failingParameterTests = filterFailingTests(passFailMap, testIndex, 'parameters'); + + if (failingToolTests.length > 0) { + stdout(); + stdout(colorize('red', 'Failed Tool Invocations')); + stdout('The following test cases did not meet the tool invocation confidence level:'); + failingToolTests.forEach((test) => stdout(test?.readable ?? 'Unknown Test Case')); + stdout(); + } + + if (failingParameterTests.length > 0) { + stdout(); + stdout(colorize('red', 'Failed Parameter Matching')); + stdout('The following test cases did not meet the parameter matching confidence level:'); + failingParameterTests.forEach((test) => stdout(test?.readable ?? 'Unknown Test Case')); + stdout(); + } + + if (failingToolTests.length === 0 && failingParameterTests.length === 0) { + stdout(colorize('green', 'All tests passed!')); + } else { + this.exit(1); + } + } +} diff --git a/confidence/src/index.ts b/confidence/src/index.ts new file mode 100644 index 00000000..320514bb --- /dev/null +++ b/confidence/src/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2025, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export {}; diff --git a/confidence/src/utils/gateway.ts b/confidence/src/utils/gateway.ts new file mode 100644 index 00000000..08929d0a --- /dev/null +++ b/confidence/src/utils/gateway.ts @@ -0,0 +1,142 @@ +/* + * Copyright 2025, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Model } from './models.js'; +import { InvocableTool } from './tools.js'; +import { RateLimiter } from './rate-limiter.js'; + +type GatewayResponse = { + generation_details?: { + generations: Array<{ + content: string; + role: string; + tool_invocations?: Array<{ + id: string; + function: { + name: string; + arguments: string; + }; + }>; + }>; + }; +}; + +const createRequestHeaders = (jwtToken: string): Record => ({ + Authorization: `Bearer ${jwtToken}`, + 'Content-Type': 'application/json', + 'x-sfdc-app-context': 'EinsteinGPT', + 'x-client-feature-id': 'ai-platform-models-connected-app', +}); + +const createRequestBody = ( + model: Model, + tools: InvocableTool[], + messages: Array<{ role: string; content: string }> +): string => + JSON.stringify({ + model, + tools, + tool_config: { + mode: 'auto', + }, + messages, + generation_settings: { + max_tokens: 500, + temperature: 0.5, + parameters: {}, + }, + }); + +// See https://developer.salesforce.com/docs/einstein/genai/guide/models-api-rate-limits.html +const rateLimiter = new RateLimiter(500, 60_000); + +const makeSingleGatewayRequest = async ( + jwtToken: string, + model: Model, + tools: InvocableTool[], + messages: Array<{ role: string; content: string }> +): Promise => { + const response = await rateLimiter.enqueue(async () => + fetch('https://api.salesforce.com/ai/gpt/v1/chat/generations', { + method: 'POST', + headers: createRequestHeaders(jwtToken), + body: createRequestBody(model, tools, messages), + }) + ); + + if (!response.ok) { + // eslint-disable-next-line no-console + console.error(`Error making request to LLM Gateway API: ${response.status} ${response.statusText}`); + // eslint-disable-next-line no-console + console.error('Response body:', await response.text()); + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const responseData = await response.json(); + return responseData as GatewayResponse; +}; + +/** + * Makes requests to the LLM Gateway API for multiple utterances using the specified model and tools. + * + * @param {string} jwtToken - JWT token for authentication with the Models API + * @param {string[]} utterances - Array of utterances to send to the API + * @param {Model} model - The model identifier to use for generation + * @param {InvocableTool[]} tools - Array of tools that can be invoked by the model + * @param {string[]} [initialContext] - Optional initial context messages to prepend to the conversation + * @returns {Promise<{model: Model, messages: Array<{role: string, content: string}>, responses: GatewayResponse[]}>} Object containing the model used, conversation messages, and API responses + * @throws {Error} If any API request fails or returns an error + * + * @see {@link https://git.soma.salesforce.com/pages/tech-enablement/einstein/docs/gateway/function-calling/} Function Calling Documentation + */ +export const makeGatewayRequests = async ( + jwtToken: string, + utterances: string[], + model: Model, + tools: InvocableTool[], + initialContext?: string[] +): Promise<{ model: Model; messages: Array<{ role: string; content: string }>; responses: GatewayResponse[] }> => { + const messages: Array<{ role: string; content: string }> = []; + const responses: GatewayResponse[] = []; + + const allUtterances = initialContext ? [...initialContext, ...utterances] : utterances; + + for (const utterance of allUtterances) { + // Add the current utterance to messages + messages.push({ + role: 'user', + content: utterance, + }); + + // eslint-disable-next-line no-await-in-loop + const responseData = await makeSingleGatewayRequest(jwtToken, model, tools, messages); + responses.push(responseData); + + // Add the assistant's response to messages for the next iteration + if (responseData.generation_details?.generations[0]?.content) { + messages.push({ + role: responseData.generation_details.generations[0].role, + content: responseData.generation_details.generations[0].content, + }); + } + } + + return { + responses, + model, + messages, + }; +}; diff --git a/confidence/src/utils/jwt.ts b/confidence/src/utils/jwt.ts new file mode 100644 index 00000000..7f9132aa --- /dev/null +++ b/confidence/src/utils/jwt.ts @@ -0,0 +1,50 @@ +/* + * Copyright 2025, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const mintJWT = async (): Promise => { + const consumerKey = process.env.SF_MCP_CONFIDENCE_CONSUMER_KEY; + const consumerSecret = process.env.SF_MCP_CONFIDENCE_CONSUMER_SECRET; + const instanceUrl = process.env.SF_MCP_CONFIDENCE_INSTANCE_URL; + + if (!consumerKey || !consumerSecret || !instanceUrl) { + throw new Error( + 'Missing required environment variables: SF_MCP_CONFIDENCE_CONSUMER_KEY, SF_MCP_CONFIDENCE_CONSUMER_SECRET, or SF_MCP_CONFIDENCE_INSTANCE_URL' + ); + } + + const response = await fetch(`${instanceUrl}/services/oauth2/token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: new URLSearchParams({ + grant_type: 'client_credentials', + client_id: consumerKey, + client_secret: consumerSecret, + }), + }); + + if (!response.ok) { + throw new Error(`Failed to mint JWT: ${response.statusText}`); + } + + const data = (await response.json()) as { access_token?: string }; + if (!data.access_token) { + throw new Error('Failed to retrieve access token from response'); + } + + return data.access_token; +}; diff --git a/confidence/src/utils/models.ts b/confidence/src/utils/models.ts new file mode 100644 index 00000000..c1cf4de0 --- /dev/null +++ b/confidence/src/utils/models.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2025, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// https://developer.salesforce.com/docs/einstein/genai/guide/supported-models.html +export const MODELS = [ + 'sfdc_ai__DefaultBedrockAnthropicClaude37Sonnet', + 'sfdc_ai__DefaultOpenAIGPT35Turbo', + 'sfdc_ai__DefaultGPT41Mini', + 'sfdc_ai__DefaultBedrockAnthropicClaude4Sonnet', + 'sfdc_ai__DefaultOpenAIGPT4OmniMini', + 'sfdc_ai__DefaultVertexAIGeminiPro25', +] as const; + +export type Model = (typeof MODELS)[number]; diff --git a/confidence/src/utils/rate-limiter.ts b/confidence/src/utils/rate-limiter.ts new file mode 100644 index 00000000..c713f8e8 --- /dev/null +++ b/confidence/src/utils/rate-limiter.ts @@ -0,0 +1,488 @@ +/* + * Copyright 2025, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* eslint-disable no-await-in-loop */ + +import makeDebug from 'debug'; + +const debug = makeDebug('confidence:rate-limiter'); + +type QueuedRequest = { + execute: () => Promise; + resolve: (value: T | PromiseLike) => void; + reject: (error: unknown) => void; +}; + +type RetryConfig = { + maxRetries: number; + baseDelayMs: number; + maxDelayMs: number; + retryOn: number[]; +}; + +type RateLimitStatus = { + queueLength: number; + requestsInWindow: number; + maxRequests: number; + canExecute: boolean; + nextAvailableSlot?: number; + isProcessing: boolean; + completed: number; + failed: number; + burstModeActive: boolean; + utilizationRatio: number; + timeUntilWindowReset: number; + adaptiveMaxRequests: number; + backoffMultiplier: number; + retryStats: { + totalRetries: number; + retriesByStatus: Record; + }; +}; + +class RateLimitError extends Error { + public constructor(message: string, public readonly status?: number, public readonly retryAfter?: number) { + super(message); + this.name = 'RateLimitError'; + } +} + +/** + * A rate limiter that controls the frequency of requests using a sliding window approach with adaptive burst control and intelligent retry logic. + * + * This class implements a queue-based rate limiter that ensures no more than a specified + * number of requests are executed within a given time window. It features intelligent burst + * detection that allows rapid execution of small batches while maintaining rate limit compliance + * for larger workloads. + * + * Key Features: + * - Sliding window rate limiting with adaptive capacity adjustment + * - Intelligent burst control for small request batches + * - Exponential backoff retry logic with jitter for resilience + * - Respect for Retry-After headers when present + * - Adaptive rate adjustment based on 429 responses + * - Comprehensive monitoring and debugging information + * - Graceful degradation and recovery mechanisms + * + * @example + * ```typescript + * // Create a rate limiter that allows 10 requests per minute + * const rateLimiter = new RateLimiter(10, 60_000); + * + * // Small batches execute immediately in burst mode + * const results = await Promise.all([ + * rateLimiter.enqueue(() => fetch('/api/data1')), + * rateLimiter.enqueue(() => fetch('/api/data2')), + * rateLimiter.enqueue(() => fetch('/api/data3')) + * ]); + * + * // Check current status including burst mode and retry information + * const status = rateLimiter.getStatus(); + * console.log(`Burst mode active: ${status.burstModeActive}`); + * console.log(`Adaptive capacity: ${status.adaptiveMaxRequests}/${status.maxRequests}`); + * console.log(`Retry stats: ${status.retryStats.totalRetries} total retries`); + * ``` + */ +export class RateLimiter { + private static completed: number = 0; + private static failed: number = 0; + + private readonly requestTimestamps: number[] = []; + private readonly queue: Array> = []; + private isProcessing = false; + + /** + * Utilization threshold below which burst mode is allowed. + * When current window utilization is below this ratio, requests can execute immediately. + */ + private readonly burstUtilizationThreshold = 0.5; + + /** + * Total work threshold (current + queued requests) for burst mode. + * Burst mode is only allowed when predicted total utilization is below this ratio. + */ + private readonly burstQueueThreshold = 0.75; + + /** + * Minimum delay between requests during controlled (non-burst) execution. + * Provides a baseline spacing to prevent overwhelming the target service. + */ + private readonly minDelayMs = 50; + + /** + * Configuration for retry logic when handling rate limit errors. + */ + private readonly retryConfig: RetryConfig = { + maxRetries: 3, + baseDelayMs: 1000, + maxDelayMs: 60_000, + retryOn: [429, 503, 502, 504], + }; + + /** + * Adaptive rate limiting state. + */ + private adaptiveMaxRequests: number; + private readonly originalMaxRequests: number; + private backoffMultiplier = 1.0; + + /** + * Retry statistics for monitoring. + */ + private retryStats = { + totalRetries: 0, + retriesByStatus: {} as Record, + }; + + public constructor(private readonly maxRequests = 40, private readonly windowMs = 60_000) { + this.adaptiveMaxRequests = maxRequests; + this.originalMaxRequests = maxRequests; + } + + /** + * Utility function to sleep for a given number of milliseconds + */ + private static sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + /** + * Determines if an error is retryable + */ + private static isRetryableError(error: unknown): boolean { + if (error instanceof Error) { + const message = error.message.toLowerCase(); + return ( + message.includes('429') || + message.includes('rate limit') || + message.includes('econnreset') || + message.includes('timeout') + ); + } + return false; + } + + /** + * Enqueues a request to be executed when rate limit allows + * + * @param requestFn Function that returns a promise for the actual request + * @returns Promise that resolves when the request is executed + */ + public async enqueue(requestFn: () => Promise): Promise { + debug('Enqueuing request: %O', this.getStatus()); + return new Promise((resolve, reject) => { + this.queue.push({ + execute: requestFn, + resolve, + reject, + }); + + // Start processing if not already running + if (!this.isProcessing) { + void this.processQueue(); + } + }); + } + + /** + * Gets current queue status for monitoring/debugging + */ + public getStatus(): RateLimitStatus { + const now = Date.now(); + this.cleanupOldTimestamps(now); + + return { + queueLength: this.queue.length, + requestsInWindow: this.requestTimestamps.length, + maxRequests: this.maxRequests, + canExecute: this.canExecuteRequest(), + nextAvailableSlot: this.requestTimestamps.length > 0 ? this.requestTimestamps[0] + this.windowMs : undefined, + isProcessing: this.isProcessing, + completed: RateLimiter.completed, + failed: RateLimiter.failed, + burstModeActive: this.shouldAllowBurst(), + utilizationRatio: this.requestTimestamps.length / this.adaptiveMaxRequests, + timeUntilWindowReset: this.getTimeUntilWindowReset(), + adaptiveMaxRequests: this.adaptiveMaxRequests, + backoffMultiplier: this.backoffMultiplier, + retryStats: { ...this.retryStats }, + }; + } + + /** + * Executes a single request with retry logic and adaptive rate limiting + */ + private async executeRequest(request: QueuedRequest): Promise { + let lastError: Error | null = null; + + for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) { + try { + const result = await request.execute(); + + // Check for rate limit response + if (!result.ok && this.retryConfig.retryOn.includes(result.status)) { + // Immediately adjust rate limit on first 429 to prevent more + if (result.status === 429) { + this.adjustRateLimit(true); + } + + if (attempt === this.retryConfig.maxRetries) { + this.recordRetryFailure(result.status); + throw new RateLimitError(`Rate limit exceeded after ${this.retryConfig.maxRetries} retries`, result.status); + } + + this.recordRetryAttempt(result.status); + const delay = this.calculateRetryDelay(attempt, result); + debug(`Rate limit hit (${result.status}), retrying in ${delay}ms (attempt ${attempt + 1})`); + + await RateLimiter.sleep(delay); + continue; + } + + // Success - record and resolve + RateLimiter.completed++; + this.adjustRateLimit(false); + request.resolve(result); + return; + } catch (error) { + lastError = error as Error; + debug(`Error executing request: ${lastError.message}. %O`, { + attempt, + status: lastError instanceof RateLimitError ? lastError.status : undefined, + retryAfter: lastError instanceof RateLimitError ? lastError.retryAfter : undefined, + }); + debug('Full error details: %O', lastError); + + if (attempt < this.retryConfig.maxRetries && RateLimiter.isRetryableError(error)) { + this.recordRetryAttempt(); + const delay = this.calculateRetryDelay(attempt); + debug(`Retryable error, retrying in ${delay}ms (attempt ${attempt + 1}): ${lastError.message}`); + + await RateLimiter.sleep(delay); + continue; + } + break; + } + } + + // All retries exhausted + RateLimiter.failed++; + request.reject(lastError ?? new Error('Max retries exceeded')); + } + + /** + * Processes the queue, executing requests when rate limit allows + */ + private async processQueue(): Promise { + this.isProcessing = true; + + while (this.queue.length > 0) { + const now = Date.now(); + + // Remove timestamps outside the current window + this.cleanupOldTimestamps(now); + + if (this.canExecuteRequest()) { + debug('Executing request: %O', this.getStatus()); + // Execute the next request without waiting for it to complete + const request = this.queue.shift()!; + this.recordRequest(now); + + // Execute the request asynchronously - don't await + void this.executeRequest(request); + + // Use adaptive delay instead of fixed delay + const delay = this.calculateAdaptiveDelay(); + if (delay > 0) { + await RateLimiter.sleep(delay); + } + } else { + // Wait until we can make the next request + await RateLimiter.sleep(this.calculateDelay(now)); + } + } + + this.isProcessing = false; + } + + /** + * Determines if burst mode should be allowed based on current utilization + */ + private shouldAllowBurst(): boolean { + const utilizationRatio = this.requestTimestamps.length / this.adaptiveMaxRequests; + const queueRatio = this.queue.length / this.adaptiveMaxRequests; + const totalWorkRatio = utilizationRatio + queueRatio; + + // Be more conservative with large queues - reduce burst threshold + const adjustedBurstThreshold = + this.queue.length > this.adaptiveMaxRequests * 0.25 + ? this.burstUtilizationThreshold * 0.5 + : this.burstUtilizationThreshold; + + // Allow bursts when: + // 1. Current utilization is below the burst threshold + // 2. Total work (current + queued) is below the queue threshold + // 3. We're not in backoff mode + return ( + utilizationRatio < adjustedBurstThreshold && + totalWorkRatio < this.burstQueueThreshold && + this.backoffMultiplier >= 0.9 + ); + } + + /** + * Calculates adaptive delay based on current utilization and queue state + */ + private calculateAdaptiveDelay(): number { + // Allow immediate execution during burst conditions + if (this.shouldAllowBurst()) { + return 0; + } + + const utilizationRatio = this.requestTimestamps.length / this.adaptiveMaxRequests; + const remainingCapacity = this.adaptiveMaxRequests - this.requestTimestamps.length; + const queueLength = this.queue.length; + + // At high utilization (>90%), use much more conservative delays + if (utilizationRatio > 0.9) { + const baseDelay = Math.ceil(this.windowMs / this.adaptiveMaxRequests); + // Use exponential scaling at high utilization to prevent 429s + const aggressiveScaling = Math.pow(utilizationRatio, 3) * 5; + return Math.max(baseDelay * aggressiveScaling, 1000); // Minimum 1 second at high utilization + } + + // If we have enough capacity for all queued requests, use minimal spacing + if (remainingCapacity >= queueLength) { + return this.minDelayMs; + } + + // Calculate base delay and scale it based on utilization + const baseDelay = Math.ceil(this.windowMs / this.adaptiveMaxRequests); + const scalingFactor = Math.min(utilizationRatio * 2, 1); + + return Math.max(this.minDelayMs, baseDelay * scalingFactor); + } + + /** + * Calculates retry delay with exponential backoff + */ + private calculateRetryDelay(attempt: number, response?: Response): number { + if (response?.status === 429) { + const remainingRequests = this.adaptiveMaxRequests - this.requestTimestamps.length; + if (remainingRequests <= 0) { + return this.getTimeUntilWindowReset(); // Wait until the window resets + } + } + + // Exponential backoff with jitter + const exponentialDelay = this.retryConfig.baseDelayMs * Math.pow(2, attempt); + const jitter = Math.random() * 0.3 * exponentialDelay; // 30% jitter + return Math.min(exponentialDelay + jitter, this.retryConfig.maxDelayMs); + } + /** + * Records a retry attempt for statistics + */ + private recordRetryAttempt(status?: number): void { + this.retryStats.totalRetries++; + if (status) { + this.retryStats.retriesByStatus[status] = (this.retryStats.retriesByStatus[status] || 0) + 1; + } + } + + /** + * Records a final retry failure + */ + private recordRetryFailure(status: number): void { + debug(`Final retry failure with status ${status} after ${this.retryConfig.maxRetries} attempts`); + } + + /** + * Adjusts rate limit based on success/failure + */ + private adjustRateLimit(hit429: boolean): void { + if (hit429) { + // Reduce rate by 25% when we hit rate limits + this.backoffMultiplier = Math.max(0.25, this.backoffMultiplier * 0.75); + this.adaptiveMaxRequests = Math.floor(this.originalMaxRequests * this.backoffMultiplier); + debug(`Rate limit hit, reducing to ${this.adaptiveMaxRequests} requests per window`); + } else { + // Gradually recover rate limit (2% increase per successful request) + this.backoffMultiplier = Math.min(1.0, this.backoffMultiplier * 1.02); + this.adaptiveMaxRequests = Math.floor(this.originalMaxRequests * this.backoffMultiplier); + } + } + + /** + * Gets the time until the current rate limit window resets + */ + private getTimeUntilWindowReset(): number { + if (this.requestTimestamps.length === 0) { + return 0; + } + const oldestRequest = this.requestTimestamps[0]; + return Math.max(0, oldestRequest + this.windowMs - Date.now()); + } + + /** + * Checks if a request can be executed based on current adaptive rate limit + */ + private canExecuteRequest(): boolean { + const utilizationRatio = this.requestTimestamps.length / this.adaptiveMaxRequests; + + // Be more conservative at high utilization to prevent 429s + if (utilizationRatio > 0.9) { + // Only allow if we have significant capacity remaining + return this.requestTimestamps.length <= Math.floor(this.adaptiveMaxRequests * 0.85); + } + + return this.requestTimestamps.length < this.adaptiveMaxRequests; + } + + /** + * Records the timestamp of a request + */ + private recordRequest(timestamp: number): void { + this.requestTimestamps.push(timestamp); + } + + /** + * Removes timestamps that are outside the current window + */ + private cleanupOldTimestamps(now: number): void { + const cutoff = now - this.windowMs; + while (this.requestTimestamps.length > 0 && this.requestTimestamps[0] < cutoff) { + this.requestTimestamps.shift(); + } + } + + /** + * Calculates how long to wait before the next request can be made + */ + private calculateDelay(now: number): number { + if (this.requestTimestamps.length === 0) { + return 0; + } + + // If we're at the adaptive limit, wait until the oldest request expires + if (this.requestTimestamps.length >= this.adaptiveMaxRequests) { + const oldestRequest = this.requestTimestamps[0]; + const timeUntilExpiry = oldestRequest + this.windowMs - now; + return Math.max(0, timeUntilExpiry + 100); // Add 100ms buffer + } + + return 0; + } +} diff --git a/confidence/src/utils/table.ts b/confidence/src/utils/table.ts new file mode 100644 index 00000000..784a47ae --- /dev/null +++ b/confidence/src/utils/table.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2025, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TableOptions } from '@oclif/table'; + +export const TABLE_STYLE = { + headerOptions: { + formatter: 'capitalCase', + color: 'cyanBright', + }, + borderColor: 'gray', + overflow: 'wrap', +} satisfies Partial>>; diff --git a/confidence/src/utils/tools.ts b/confidence/src/utils/tools.ts new file mode 100644 index 00000000..9b09167c --- /dev/null +++ b/confidence/src/utils/tools.ts @@ -0,0 +1,96 @@ +/* + * Copyright 2025, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { spawn } from 'node:child_process'; +import { Tool } from '@modelcontextprotocol/sdk/types.js'; +import { colorize } from '@oclif/core/ux'; +import { encode as encodeGPT4oMini } from 'gpt-tokenizer/model/gpt-4o-mini'; +import { encode as encodeO3Mini } from 'gpt-tokenizer/model/o3-mini'; +import { encode as encodeGPT4 } from 'gpt-tokenizer/model/gpt-4'; + +export type InvocableTool = { + name: string; + function: { + name: string; + description: string | undefined; + parameters: Tool['inputSchema']; + annotations: Tool['annotations']; + }; +}; + +export const getToolsList = async (): Promise<{ + tools: InvocableTool[]; + tokens: Array<{ tool: string; tokensGPT4oMini: number; tokensO3Mini: number; tokensGPT4: number }>; +}> => { + const toolsList: string = await new Promise((resolve, reject) => { + const child = spawn('npx', [ + '@modelcontextprotocol/inspector', + '--cli', + 'node', + 'bin/run.js', + '--orgs', + 'DEFAULT_TARGET_ORG', + '--method', + 'tools/list', + ]); + + let output = ''; + + child.stdout.on('data', (data: Buffer) => { + output += data.toString(); + }); + + child.stderr.on('data', (data: Buffer) => { + reject(new Error(data.toString())); + }); + + child.on('close', (code: number | null) => { + if (code === 0) { + resolve(output); + } else { + reject(new Error(`Process exited with code ${code ?? 'unknown'}`)); + } + }); + }); + + const parsedToolsList = JSON.parse(toolsList) as { tools: Tool[] }; + + const tokens = parsedToolsList.tools?.map((tool) => ({ + tool: tool.name, + tokensGPT4oMini: encodeGPT4oMini(JSON.stringify(tool)).length, + tokensO3Mini: encodeO3Mini(JSON.stringify(tool)).length, + tokensGPT4: encodeGPT4(JSON.stringify(tool)).length, + })); + tokens.push({ + tool: colorize('bold', 'TOTAL'), + tokensGPT4oMini: tokens.reduce((acc, tool) => acc + tool.tokensGPT4oMini, 0), + tokensO3Mini: tokens.reduce((acc, tool) => acc + tool.tokensO3Mini, 0), + tokensGPT4: tokens.reduce((acc, tool) => acc + tool.tokensGPT4, 0), + }); + + return { + tools: (parsedToolsList.tools ?? []).map((tool) => ({ + name: tool.name, + function: { + name: tool.name, + description: tool.description, + parameters: tool.inputSchema, + annotations: tool.annotations, + }, + })), + tokens, + }; +}; diff --git a/confidence/src/utils/yaml.ts b/confidence/src/utils/yaml.ts new file mode 100644 index 00000000..586c3281 --- /dev/null +++ b/confidence/src/utils/yaml.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2025, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'node:fs/promises'; +import yaml from 'yaml'; + +export async function readYamlFile(filePath: string): Promise { + try { + const fileContent = await fs.readFile(filePath, 'utf8'); + return yaml.parse(fileContent) as T; + } catch (error) { + throw new Error( + `Failed to read or parse YAML file at ${filePath}: ${error instanceof Error ? error.message : String(error)}` + ); + } +} diff --git a/confidence/tsconfig.json b/confidence/tsconfig.json new file mode 100644 index 00000000..f6681780 --- /dev/null +++ b/confidence/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "./lib", + "rootDir": "./src", + "skipLibCheck": true + }, + "include": ["./src/**/*.ts"] +} diff --git a/package.json b/package.json index 0460ac9d..f9dd1278 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "prepare": "sf-install", "start": "yarn build && npm link && mcp-inspector sf-mcp-server", "test": "wireit", - "test:only": "wireit" + "test:only": "wireit", + "test:confidence": "yarn compile && tsc -p confidence/ --pretty --incremental && confidence/bin/run.js confidence-test" }, "repository": "salesforcecli/mcp", "bugs": { @@ -57,15 +58,20 @@ }, "devDependencies": { "@modelcontextprotocol/inspector": "^0.15.0", + "@oclif/table": "^0.4.11", "@salesforce/cli-plugins-testkit": "^5.3.39", "@salesforce/dev-scripts": "11.0.2", + "@types/debug": "^4.1.12", "@types/node": "^22.16.5", + "debug": "^4.4.1", "eslint-config-salesforce-license": "^1.0.1", "eslint-plugin-sf-plugin": "^1.20.26", + "gpt-tokenizer": "^3.0.1", "oclif": "^4.21.0", "ts-node": "^10.9.2", "ts-patch": "^3.3.0", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "yaml": "^2.8.0" }, "publishConfig": { "access": "public" diff --git a/test/confidence/sf-deploy-metadata.yml b/test/confidence/sf-deploy-metadata.yml new file mode 100644 index 00000000..716fbd80 --- /dev/null +++ b/test/confidence/sf-deploy-metadata.yml @@ -0,0 +1,199 @@ +models: + - sfdc_ai__DefaultBedrockAnthropicClaude4Sonnet + +initial-context: + - 'My current OS is macos. I am working in a workspace with the following folders: /Users/sf-dev/dreamhouse-lwc + My org alias is dreamhouse. + This is the structure of /Users/sf-dev/dreamhouse-lwc: + package.xml + force-app/main/default/applications + force-app/main/default/aura + force-app/main/default/aura/pageTemplate_2_7_3 + force-app/main/default/classes + force-app/main/default/contentassets + force-app/main/default/cspTrustedSites + force-app/main/default/flexipages + force-app/main/default/flows + force-app/main/default/layouts + force-app/main/default/lwc + force-app/main/default/messageChannels + force-app/main/default/objects + force-app/main/default/permissionsets + force-app/main/default/prompts + force-app/main/default/remoteSiteSettings + force-app/main/default/staticresources + force-app/main/default/tabs' + +tests: + # Deploy specific source directory (Lightning Web Components) + - utterances: + - Deploy the Lightning Web Components in force-app/main/default/lwc to the dreamhouse org. + expected-tool: sf-deploy-metadata + expected-parameters: + sourceDir: force-app/main/default/lwc + directory: /Users/sf-dev/dreamhouse-lwc + usernameOrAlias: dreamhouse + expected-tool-confidence: 50 + expected-parameter-confidence: 50 + allowed-tools: + - sf-list-all-orgs + + # Deploy multiple source directories + - utterances: + - Deploy the classes and lwc folders to my dreamhouse org. + expected-tool: sf-deploy-metadata + expected-parameters: + sourceDir: force-app/main/default/classes,force-app/main/default/lwc + directory: /Users/sf-dev/dreamhouse-lwc + usernameOrAlias: dreamhouse + expected-tool-confidence: 50 + expected-parameter-confidence: 50 + allowed-tools: + - sf-list-all-orgs + + # Deploy using manifest file + - utterances: + - Deploy the components specified in my package.xml manifest to dreamhouse. + expected-tool: sf-deploy-metadata + expected-parameters: + manifest: /Users/sf-dev/dreamhouse-lwc/package.xml + directory: /Users/sf-dev/dreamhouse-lwc + usernameOrAlias: dreamhouse + expected-tool-confidence: 50 + expected-parameter-confidence: 50 + allowed-tools: + - sf-list-all-orgs + + # Deploy all local changes (no sourceDir or manifest specified) + - utterances: + - Deploy my changes to the dreamhouse org. + expected-tool: sf-deploy-metadata + expected-parameters: + directory: /Users/sf-dev/dreamhouse-lwc + usernameOrAlias: dreamhouse + expected-tool-confidence: 50 + expected-parameter-confidence: 50 + allowed-tools: + - sf-list-all-orgs + + # Deploy with no tests run + - utterances: + - Deploy my changes to the dreamhouse org without running any tests. + expected-tool: sf-deploy-metadata + expected-parameters: + directory: /Users/sf-dev/dreamhouse-lwc + usernameOrAlias: dreamhouse + apexTestLevel: NoTestRun + expected-tool-confidence: 50 + expected-parameter-confidence: 50 + allowed-tools: + - sf-list-all-orgs + + # Deploy with local tests + - utterances: + - Deploy force-app/main/default/classes to the dreamhouse org and run all local tests. + expected-tool: sf-deploy-metadata + expected-parameters: + sourceDir: force-app/main/default/classes + directory: /Users/sf-dev/dreamhouse-lwc + usernameOrAlias: dreamhouse + apexTestLevel: RunLocalTests + expected-tool-confidence: 50 + expected-parameter-confidence: 50 + allowed-tools: + - sf-list-all-orgs + + # Deploy with all org tests + - utterances: + - Deploy the apex classes and run all tests in the dreamhouse org including managed packages. + expected-tool: sf-deploy-metadata + expected-parameters: + sourceDir: force-app/main/default/classes + directory: /Users/sf-dev/dreamhouse-lwc + usernameOrAlias: dreamhouse + apexTestLevel: RunAllTestsInOrg + expected-tool-confidence: 50 + expected-parameter-confidence: 50 + allowed-tools: + - sf-list-all-orgs + + # Deploy with specific apex tests + - utterances: + - Deploy my classes to the dreamhouse org and run the PropertyControllerTest and BrokerControllerTest apex tests. + expected-tool: sf-deploy-metadata + expected-parameters: + sourceDir: force-app/main/default/classes + directory: /Users/sf-dev/dreamhouse-lwc + usernameOrAlias: dreamhouse + apexTests: PropertyControllerTest,BrokerControllerTest + expected-tool-confidence: 50 + expected-parameter-confidence: 50 + allowed-tools: + - sf-list-all-orgs + + # Deploy with single apex test + - utterances: + - Deploy the PropertyController class and run PropertyControllerTest to the dreamhouse org. + expected-tool: sf-deploy-metadata + expected-parameters: + sourceDir: force-app/main/default/classes + directory: /Users/sf-dev/dreamhouse-lwc + usernameOrAlias: dreamhouse + apexTests: PropertyControllerTest + expected-tool-confidence: 50 + expected-parameter-confidence: 50 + allowed-tools: + - sf-list-all-orgs + + # Deploy specific file type mentioned + - utterances: + - Deploy the flows to the dreamhouse org. + expected-tool: sf-deploy-metadata + expected-parameters: + sourceDir: force-app/main/default/flows + directory: /Users/sf-dev/dreamhouse-lwc + usernameOrAlias: dreamhouse + expected-tool-confidence: 50 + expected-parameter-confidence: 50 + allowed-tools: + - sf-list-all-orgs + + # Deploy with multiple metadata types + - utterances: + - Deploy the objects, classes, and tabs to the dreamhouse org. + expected-tool: sf-deploy-metadata + expected-parameters: + sourceDir: force-app/main/default/objects,force-app/main/default/classes,force-app/main/default/tabs + directory: /Users/sf-dev/dreamhouse-lwc + usernameOrAlias: dreamhouse + expected-tool-confidence: 50 + expected-parameter-confidence: 50 + allowed-tools: + - sf-list-all-orgs + + # Deploy with complex folder structure + - utterances: + - Deploy the PropertyController and PropertyTrigger from the classes folder to the dreamhouse org and run PropertyControllerTest, PropertyTriggerTest, and PropertyUtilTest. + expected-tool: sf-deploy-metadata + expected-parameters: + sourceDir: force-app/main/default/classes + directory: /Users/sf-dev/dreamhouse-lwc + usernameOrAlias: dreamhouse + apexTests: PropertyControllerTest,PropertyTriggerTest,PropertyUtilTest + expected-tool-confidence: 50 + expected-parameter-confidence: 50 + allowed-tools: + - sf-list-all-orgs + + # Deploy with relative path specification + - utterances: + - Deploy everything in the force-app directory to the dreamhouse org. + expected-tool: sf-deploy-metadata + expected-parameters: + sourceDir: force-app + directory: /Users/sf-dev/dreamhouse-lwc + usernameOrAlias: dreamhouse + expected-tool-confidence: 50 + expected-parameter-confidence: 50 + allowed-tools: + - sf-list-all-orgs diff --git a/yarn.lock b/yarn.lock index 4e46bf00..6e63ccef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,6 +7,14 @@ resolved "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz" integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== +"@alcalzone/ansi-tokenize@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.1.3.tgz#9f89839561325a8e9a0c32360b8d17e48489993f" + integrity sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw== + dependencies: + ansi-styles "^6.2.1" + is-fullwidth-code-point "^4.0.0" + "@ampproject/remapping@^2.2.0": version "2.3.0" resolved "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz" @@ -1949,6 +1957,21 @@ lodash "^4.17.21" registry-auth-token "^5.1.0" +"@oclif/table@^0.4.11": + version "0.4.11" + resolved "https://registry.yarnpkg.com/@oclif/table/-/table-0.4.11.tgz#5c6ebcc85554678924099e659ade5b60c0a59bd9" + integrity sha512-HKvX4YqabHYrt3juFOldmMedLiHRLBoxO3JnhVQxOCdq7Jr5HP7GM6nrHww4mNSA+Jrc7WhbmSu2GenrHnTOvQ== + dependencies: + "@types/react" "^18.3.12" + change-case "^5.4.4" + cli-truncate "^4.0.0" + ink "5.0.1" + natural-orderby "^3.0.2" + object-hash "^3.0.0" + react "^18.3.1" + strip-ansi "^7.1.0" + wrap-ansi "^9.0.0" + "@opentelemetry/api-logs@0.200.0": version "0.200.0" resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.200.0.tgz#f9015fd844920c13968715b3cdccf5a4d4ff907e" @@ -3310,6 +3333,13 @@ resolved "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz" integrity sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ== +"@types/debug@^4.1.12": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" + integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== + dependencies: + "@types/ms" "*" + "@types/glob@~7.2.0": version "7.2.0" resolved "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz" @@ -3388,6 +3418,11 @@ resolved "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz" integrity sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q== +"@types/ms@*": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78" + integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== + "@types/mute-stream@^0.0.4": version "0.0.4" resolved "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz" @@ -3426,6 +3461,19 @@ resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz" integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== +"@types/prop-types@*": + version "15.7.15" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.15.tgz#e6e5a86d602beaca71ce5163fadf5f95d70931c7" + integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw== + +"@types/react@^18.3.12": + version "18.3.23" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.23.tgz#86ae6f6b95a48c418fecdaccc8069e0fbb63696a" + integrity sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w== + dependencies: + "@types/prop-types" "*" + csstype "^3.0.2" + "@types/responselike@^1.0.0": version "1.0.3" resolved "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz" @@ -3718,6 +3766,13 @@ ansi-escapes@^4.3.2: dependencies: type-fest "^0.21.3" +ansi-escapes@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-7.0.0.tgz#00fc19f491bbb18e1d481b97868204f92109bfe7" + integrity sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw== + dependencies: + environment "^1.0.0" + ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" @@ -3735,7 +3790,7 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -ansi-styles@^6.1.0: +ansi-styles@^6.0.0, ansi-styles@^6.1.0, ansi-styles@^6.2.1: version "6.2.1" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== @@ -3961,6 +4016,11 @@ atomic-sleep@^1.0.0: resolved "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz" integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== +auto-bind@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/auto-bind/-/auto-bind-5.0.1.tgz#50d8e63ea5a1dddcb5e5e36451c1a8266ffbb2ae" + integrity sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg== + available-typed-arrays@^1.0.7: version "1.0.7" resolved "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz" @@ -4297,6 +4357,11 @@ chalk@^5.0.0: resolved "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz" integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== +chalk@^5.3.0: + version "5.4.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.4.1.tgz#1b48bf0963ec158dce2aacf69c093ae2dd2092d8" + integrity sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w== + change-case@^4, change-case@^4.1.2: version "4.1.2" resolved "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz" @@ -4315,6 +4380,11 @@ change-case@^4, change-case@^4.1.2: snake-case "^3.0.4" tslib "^2.0.3" +change-case@^5.4.4: + version "5.4.4" + resolved "https://registry.yarnpkg.com/change-case/-/change-case-5.4.4.tgz#0d52b507d8fb8f204343432381d1a6d7bff97a02" + integrity sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w== + character-entities-html4@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz" @@ -4408,11 +4478,31 @@ clean-stack@^3.0.1: dependencies: escape-string-regexp "4.0.0" +cli-boxes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-3.0.0.tgz#71a10c716feeba005e4504f36329ef0b17cf3145" + integrity sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g== + +cli-cursor@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-4.0.0.tgz#3cecfe3734bf4fe02a8361cbdc0f6fe28c6a57ea" + integrity sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg== + dependencies: + restore-cursor "^4.0.0" + cli-spinners@^2.9.2: version "2.9.2" resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz" integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== +cli-truncate@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-4.0.0.tgz#6cc28a2924fee9e25ce91e973db56c7066e6172a" + integrity sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA== + dependencies: + slice-ansi "^5.0.0" + string-width "^7.0.0" + cli-width@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz" @@ -4476,6 +4566,13 @@ cmdk@^1.0.4: "@radix-ui/react-id" "^1.1.0" "@radix-ui/react-primitive" "^2.0.2" +code-excerpt@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/code-excerpt/-/code-excerpt-4.0.0.tgz#2de7d46e98514385cb01f7b3b741320115f4c95e" + integrity sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA== + dependencies: + convert-to-spaces "^2.0.1" + color-convert@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" @@ -4640,6 +4737,11 @@ convert-source-map@^2.0.0: resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== +convert-to-spaces@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz#61a6c98f8aa626c16b296b862a91412a33bceb6b" + integrity sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ== + cookie-signature@^1.2.1: version "1.2.2" resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz" @@ -4711,6 +4813,11 @@ csprng@*: dependencies: sequin "*" +csstype@^3.0.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + csv-parse@^5.5.2: version "5.6.0" resolved "https://registry.npmjs.org/csv-parse/-/csv-parse-5.6.0.tgz" @@ -5039,6 +5146,11 @@ emoji-regex-xs@^1.0.0: resolved "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz" integrity sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg== +emoji-regex@^10.3.0: + version "10.4.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.4.0.tgz#03553afea80b3975749cfcb36f776ca268e413d4" + integrity sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" @@ -5073,6 +5185,11 @@ entities@^4.2.0, entities@^4.4.0, entities@^4.5.0: resolved "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== +environment@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/environment/-/environment-1.1.0.tgz#8e86c66b180f363c7ab311787e0259665f45a9f1" + integrity sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q== + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz" @@ -5200,6 +5317,11 @@ escape-string-regexp@^1.0.5: resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + escodegen@^1.8.1: version "1.14.3" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" @@ -5899,6 +6021,11 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5: resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== +get-east-asian-width@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz#21b4071ee58ed04ee0db653371b55b4299875389" + integrity sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ== + get-func-name@^2.0.1, get-func-name@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz" @@ -6153,6 +6280,11 @@ got@^13: p-cancelable "^3.0.0" responselike "^3.0.0" +gpt-tokenizer@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/gpt-tokenizer/-/gpt-tokenizer-3.0.1.tgz#19fa42314d15b69a1e82d3898336b5ba1f4f2c86" + integrity sha512-5jdaspBq/w4sWw322SvQj1Fku+CN4OAfYZeeEg8U7CWtxBz+zkxZ3h0YOHD43ee+nZYZ5Ud70HRN0ANcdIj4qg== + graceful-fs@4.2.10: version "4.2.10" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz" @@ -6458,6 +6590,11 @@ indent-string@^4.0.0: resolved "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== +indent-string@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-5.0.0.tgz#4fd2980fccaf8622d14c64d694f4cf33c81951a5" + integrity sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" @@ -6481,6 +6618,36 @@ ini@^4.1.3: resolved "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz" integrity sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg== +ink@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ink/-/ink-5.0.1.tgz#f2ef9796a3911830c3995dedd227ec84ae27de4b" + integrity sha512-ae4AW/t8jlkj/6Ou21H2av0wxTk8vrGzXv+v2v7j4in+bl1M5XRMVbfNghzhBokV++FjF8RBDJvYo+ttR9YVRg== + dependencies: + "@alcalzone/ansi-tokenize" "^0.1.3" + ansi-escapes "^7.0.0" + ansi-styles "^6.2.1" + auto-bind "^5.0.1" + chalk "^5.3.0" + cli-boxes "^3.0.0" + cli-cursor "^4.0.0" + cli-truncate "^4.0.0" + code-excerpt "^4.0.0" + indent-string "^5.0.0" + is-in-ci "^0.1.0" + lodash "^4.17.21" + patch-console "^2.0.0" + react-reconciler "^0.29.0" + scheduler "^0.23.0" + signal-exit "^3.0.7" + slice-ansi "^7.1.0" + stack-utils "^2.0.6" + string-width "^7.0.0" + type-fest "^4.8.3" + widest-line "^5.0.0" + wrap-ansi "^9.0.0" + ws "^8.15.0" + yoga-wasm-web "~0.3.3" + internal-slot@^1.0.7: version "1.0.7" resolved "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz" @@ -6594,6 +6761,18 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== +is-fullwidth-code-point@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz#fae3167c729e7463f8461ce512b080a49268aa88" + integrity sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ== + +is-fullwidth-code-point@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz#9609efced7c2f97da7b60145ef481c787c7ba704" + integrity sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA== + dependencies: + get-east-asian-width "^1.0.0" + is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" @@ -6601,6 +6780,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-in-ci@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-in-ci/-/is-in-ci-0.1.0.tgz#5e07d6a02ec3a8292d3f590973357efa3fceb0d3" + integrity sha512-d9PXLEY0v1iJ64xLiQMJ51J128EYHAaOR4yZqQi8aHGfw6KgifM3/Viw1oZZ1GCVmb3gBuyhLyHj0HgR2DhSXQ== + is-inside-container@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz" @@ -7691,6 +7875,11 @@ natural-compare@^1.4.0: resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= +natural-orderby@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/natural-orderby/-/natural-orderby-3.0.2.tgz#1b874d685fbd68beab2c6e7d14f298e03d631ec3" + integrity sha512-x7ZdOwBxZCEm9MM7+eQCjkrNLrW3rkBKNHVr78zbtqnMGVNlnDi6C/eUEYgxHNrcbu0ymvjzcwIL/6H1iHri9g== + negotiator@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz" @@ -7879,6 +8068,11 @@ object-assign@^4: resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== +object-hash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" + integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== + object-inspect@^1.13.1, object-inspect@^1.13.3: version "1.13.4" resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz" @@ -8196,6 +8390,11 @@ pascal-case@^3.1.2: no-case "^3.0.4" tslib "^2.0.3" +patch-console@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/patch-console/-/patch-console-2.0.0.tgz#9023f4665840e66f40e9ce774f904a63167433bb" + integrity sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA== + path-browserify@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz" @@ -8648,6 +8847,14 @@ react-dom@^18.3.1: loose-envify "^1.1.0" scheduler "^0.23.2" +react-reconciler@^0.29.0: + version "0.29.2" + resolved "https://registry.yarnpkg.com/react-reconciler/-/react-reconciler-0.29.2.tgz#8ecfafca63549a4f4f3e4c1e049dd5ad9ac3a54f" + integrity sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.2" + react-remove-scroll-bar@^2.3.7: version "2.3.8" resolved "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz" @@ -8905,6 +9112,14 @@ responselike@^3.0.0: dependencies: lowercase-keys "^3.0.0" +restore-cursor@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-4.0.0.tgz#519560a4318975096def6e609d44100edaa4ccb9" + integrity sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + retry@0.13.1: version "0.13.1" resolved "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz" @@ -9013,7 +9228,7 @@ sax@>=0.6.0: resolved "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz" integrity sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA== -scheduler@^0.23.2: +scheduler@^0.23.0, scheduler@^0.23.2: version "0.23.2" resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz" integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== @@ -9287,7 +9502,7 @@ side-channel@^1.0.4, side-channel@^1.1.0: side-channel-map "^1.0.1" side-channel-weakmap "^1.0.2" -signal-exit@^3.0.2, signal-exit@^3.0.3: +signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== @@ -9347,6 +9562,22 @@ slash@^3.0.0: resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +slice-ansi@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-5.0.0.tgz#b73063c57aa96f9cd881654b15294d95d285c42a" + integrity sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ== + dependencies: + ansi-styles "^6.0.0" + is-fullwidth-code-point "^4.0.0" + +slice-ansi@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-7.1.0.tgz#cd6b4655e298a8d1bdeb04250a433094b347b9a9" + integrity sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg== + dependencies: + ansi-styles "^6.2.1" + is-fullwidth-code-point "^5.0.0" + smart-buffer@^4.2.0: version "4.2.0" resolved "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz" @@ -9507,6 +9738,13 @@ stack-chain@^1.3.7: resolved "https://registry.yarnpkg.com/stack-chain/-/stack-chain-1.3.7.tgz#d192c9ff4ea6a22c94c4dd459171e3f00cea1285" integrity sha512-D8cWtWVdIe/jBA7v5p5Hwl5yOSOrmZPWDPe2KxQ5UAGD+nxbxU0lKXA4h85Ta6+qgdKVL3vUxsbIZjc1kBG7ug== +stack-utils@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" + integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== + dependencies: + escape-string-regexp "^2.0.0" + static-eval@2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/static-eval/-/static-eval-2.0.2.tgz#2d1759306b1befa688938454c546b7871f806a42" @@ -9537,6 +9775,15 @@ string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" +string-width@^7.0.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-7.2.0.tgz#b5bb8e2165ce275d4d43476dd2700ad9091db6dc" + integrity sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ== + dependencies: + emoji-regex "^10.3.0" + get-east-asian-width "^1.0.0" + strip-ansi "^7.1.0" + string.prototype.trim@^1.2.8: version "1.2.8" resolved "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz" @@ -9593,7 +9840,7 @@ stringify-entities@^4.0.0: dependencies: ansi-regex "^5.0.1" -strip-ansi@^7.0.1: +strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz" integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== @@ -9926,6 +10173,11 @@ type-fest@^0.8.0, type-fest@^0.8.1: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-fest@^4.8.3: + version "4.41.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.41.0.tgz#6ae1c8e5731273c2bf1f58ad39cbae2c91a46c58" + integrity sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA== + type-is@^2.0.0, type-is@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz" @@ -10286,6 +10538,13 @@ widest-line@^3.1.0: dependencies: string-width "^4.0.0" +widest-line@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-5.0.0.tgz#b74826a1e480783345f0cd9061b49753c9da70d0" + integrity sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA== + dependencies: + string-width "^7.0.0" + wireit@^0.14.12: version "0.14.12" resolved "https://registry.npmjs.org/wireit/-/wireit-0.14.12.tgz" @@ -10339,6 +10598,15 @@ wrap-ansi@^8.1.0: string-width "^5.0.1" strip-ansi "^7.0.1" +wrap-ansi@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-9.0.0.tgz#1a3dc8b70d85eeb8398ddfb1e4a02cd186e58b3e" + integrity sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q== + dependencies: + ansi-styles "^6.2.1" + string-width "^7.0.0" + strip-ansi "^7.1.0" + wrappy@1: version "1.0.2" resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" @@ -10354,6 +10622,11 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" +ws@^8.15.0: + version "8.18.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.2.tgz#42738b2be57ced85f46154320aabb51ab003705a" + integrity sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ== + ws@^8.18.0: version "8.18.1" resolved "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz" @@ -10407,6 +10680,11 @@ yaml@^2.5.1, yaml@^2.7.1: resolved "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz" integrity sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ== +yaml@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.0.tgz#15f8c9866211bdc2d3781a0890e44d4fa1a5fff6" + integrity sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ== + yargs-parser@^18.1.2: version "18.1.3" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz" @@ -10493,6 +10771,11 @@ yoctocolors-cjs@^2.1.2: resolved "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz" integrity sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA== +yoga-wasm-web@~0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz#eb8e9fcb18e5e651994732f19a220cb885d932ba" + integrity sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA== + zod-to-json-schema@^3.24.1: version "3.24.5" resolved "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz"