diff --git a/package.json b/package.json index cf9066d577..989e41ca71 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "!packages/_example/*" ], "resolutions": { + "@forestadmin/ai-proxy": "1.0.0", "express": "^4.21.1", "tar": "^6.2.1", "form-data": "^4.0.4", diff --git a/packages/agent/package.json b/packages/agent/package.json index f728572dd5..4b24220ff7 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -14,6 +14,7 @@ "dependencies": { "@fast-csv/format": "^4.3.5", "@fastify/express": "^1.1.0", + "@forestadmin/ai-proxy": "1.0.0", "@forestadmin/datasource-customizer": "1.67.1", "@forestadmin/datasource-toolkit": "1.50.0", "@forestadmin/forestadmin-client": "1.36.14", diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts index bccf70428f..f4d08cd895 100644 --- a/packages/agent/src/agent.ts +++ b/packages/agent/src/agent.ts @@ -20,7 +20,7 @@ import FrameworkMounter from './framework-mounter'; import makeRoutes from './routes'; import makeServices, { ForestAdminHttpDriverServices } from './services'; import CustomizationService from './services/model-customizations/customization'; -import { AgentOptions, AgentOptionsWithDefaults } from './types'; +import { AgentOptions, AgentOptionsWithDefaults, AiConfiguration } from './types'; import SchemaGenerator from './utils/forest-schema/generator'; import OptionsValidator from './utils/options-validator'; @@ -40,6 +40,7 @@ export default class Agent extends FrameworkMounter protected nocodeCustomizer: DataSourceCustomizer; protected customizationService: CustomizationService; protected schemaGenerator: SchemaGenerator; + protected aiConfiguration: AiConfiguration | null = null; /** * Create a new Agent Builder. @@ -190,8 +191,30 @@ export default class Agent extends FrameworkMounter return this; } + /** + * Add AI configuration to the agent. + * This enables AI-powered features through the /forest/_internal/ai-proxy/* endpoints. + * + * @param configuration AI client configuration + * @example + * agent.addAI({ + * provider: 'openai', + * apiKey: process.env.OPENAI_API_KEY, + * model: 'gpt-4' + * }); + */ + addAI(configuration: AiConfiguration): this { + if (this.aiConfiguration) { + throw new Error('addAI() can only be called once'); + } + + this.aiConfiguration = configuration; + + return this; + } + protected getRoutes(dataSource: DataSource, services: ForestAdminHttpDriverServices) { - return makeRoutes(dataSource, this.options, services); + return makeRoutes(dataSource, this.options, services, this.aiConfiguration); } /** @@ -261,7 +284,12 @@ export default class Agent extends FrameworkMounter // Either load the schema from the file system or build it let schema: Pick; - const { meta } = SchemaGenerator.buildMetadata(this.customizationService.buildFeatures()); + // Get the AI provider name if configured (e.g., 'openai') + const aiProvider = this.aiConfiguration?.provider ?? null; + const { meta } = SchemaGenerator.buildMetadata( + this.customizationService.buildFeatures(), + aiProvider, + ); // When using experimental no-code features even in production we need to build a new schema if (!experimental?.webhookCustomActions && isProduction) { diff --git a/packages/agent/src/routes/ai/ai-proxy.ts b/packages/agent/src/routes/ai/ai-proxy.ts new file mode 100644 index 0000000000..0935ba2b26 --- /dev/null +++ b/packages/agent/src/routes/ai/ai-proxy.ts @@ -0,0 +1,38 @@ +import { Router as AiProxyRouter } from '@forestadmin/ai-proxy'; +import KoaRouter from '@koa/router'; +import { Context } from 'koa'; + +import { ForestAdminHttpDriverServices } from '../../services'; +import { AgentOptionsWithDefaults, AiConfiguration, HttpCode, RouteType } from '../../types'; +import BaseRoute from '../base-route'; + +export default class AiProxyRoute extends BaseRoute { + readonly type = RouteType.PrivateRoute; + private readonly aiProxyRouter: AiProxyRouter; + + constructor( + services: ForestAdminHttpDriverServices, + options: AgentOptionsWithDefaults, + aiConfiguration: AiConfiguration, + ) { + super(services, options); + this.aiProxyRouter = new AiProxyRouter({ + aiConfiguration, + logger: this.options.logger, + }); + } + + setupRoutes(router: KoaRouter): void { + router.post('/_internal/ai-proxy/:route', this.handleAiProxy.bind(this)); + } + + private async handleAiProxy(context: Context): Promise { + context.response.body = await this.aiProxyRouter.route({ + route: context.params.route, + body: context.request.body, + query: context.query, + mcpConfigs: await this.options.forestAdminClient.mcpServerConfigService.getConfiguration(), + }); + context.response.status = HttpCode.Ok; + } +} diff --git a/packages/agent/src/routes/index.ts b/packages/agent/src/routes/index.ts index b528f8ddea..d49295a57c 100644 --- a/packages/agent/src/routes/index.ts +++ b/packages/agent/src/routes/index.ts @@ -11,6 +11,7 @@ import Get from './access/get'; import List from './access/list'; import ListRelated from './access/list-related'; import NativeQueryDatasource from './access/native-query-datasource'; +import AiProxyRoute from './ai/ai-proxy'; import BaseRoute from './base-route'; import Capabilities from './capabilities'; import ActionRoute from './modification/action/action'; @@ -28,7 +29,7 @@ import ErrorHandling from './system/error-handling'; import HealthCheck from './system/healthcheck'; import Logger from './system/logger'; import { ForestAdminHttpDriverServices as Services } from '../services'; -import { AgentOptionsWithDefaults as Options } from '../types'; +import { AiConfiguration, AgentOptionsWithDefaults as Options } from '../types'; export const ROOT_ROUTES_CTOR = [ Authentication, @@ -164,10 +165,21 @@ function getActionRoutes( return routes; } +function getAiRoutes( + options: Options, + services: Services, + aiConfiguration?: AiConfiguration, +): BaseRoute[] { + if (!aiConfiguration) return []; + + return [new AiProxyRoute(services, options, aiConfiguration)]; +} + export default function makeRoutes( dataSource: DataSource, options: Options, services: Services, + aiConfiguration?: AiConfiguration, ): BaseRoute[] { const routes = [ ...getRootRoutes(options, services), @@ -177,6 +189,7 @@ export default function makeRoutes( ...getApiChartRoutes(dataSource, options, services), ...getRelatedRoutes(dataSource, options, services), ...getActionRoutes(dataSource, options, services), + ...getAiRoutes(options, services, aiConfiguration), ]; // Ensure routes and middlewares are loaded in the right order. diff --git a/packages/agent/src/types.ts b/packages/agent/src/types.ts index d9a24316c6..b7dfc28cd4 100644 --- a/packages/agent/src/types.ts +++ b/packages/agent/src/types.ts @@ -1,7 +1,11 @@ +import type { AiConfiguration, AiProvider } from '@forestadmin/ai-proxy'; + import { CompositeId, Logger, LoggerLevel } from '@forestadmin/datasource-toolkit'; import { ForestAdminClient } from '@forestadmin/forestadmin-client'; import { IncomingMessage, ServerResponse } from 'http'; +export type { AiConfiguration, AiProvider }; + /** Options to configure behavior of an agent's forestadmin driver */ export type AgentOptions = { authSecret: string; diff --git a/packages/agent/src/utils/forest-schema/generator.ts b/packages/agent/src/utils/forest-schema/generator.ts index 85ded5e5dd..3900c92ef1 100644 --- a/packages/agent/src/utils/forest-schema/generator.ts +++ b/packages/agent/src/utils/forest-schema/generator.ts @@ -2,7 +2,7 @@ import { DataSource } from '@forestadmin/datasource-toolkit'; import { ForestSchema } from '@forestadmin/forestadmin-client'; import SchemaGeneratorCollection from './generator-collection'; -import { AgentOptionsWithDefaults } from '../../types'; +import { AgentOptionsWithDefaults, AiProvider } from '../../types'; export default class SchemaGenerator { private readonly schemaGeneratorCollection: SchemaGeneratorCollection; @@ -21,7 +21,10 @@ export default class SchemaGenerator { }; } - static buildMetadata(features: Record | null): Pick { + static buildMetadata( + features: Record | null, + aiLlm: AiProvider | null = null, + ): Pick { const { version } = require('../../../package.json'); // eslint-disable-line @typescript-eslint/no-var-requires,global-require return { @@ -29,6 +32,7 @@ export default class SchemaGenerator { liana: 'forest-nodejs-agent', liana_version: version, liana_features: features, + ai_llms: aiLlm ? [{ provider: aiLlm }] : null, stack: { engine: 'nodejs', engine_version: process.versions && process.versions.node, diff --git a/packages/agent/test/agent.test.ts b/packages/agent/test/agent.test.ts index b7b9df38f9..fc4061c111 100644 --- a/packages/agent/test/agent.test.ts +++ b/packages/agent/test/agent.test.ts @@ -127,6 +127,7 @@ describe('Agent', () => { liana: 'forest-nodejs-agent', liana_version: expect.stringMatching(/\d+\.\d+\.\d+.*/), liana_features: null, + ai_llms: null, stack: expect.anything(), }, }); @@ -155,6 +156,7 @@ describe('Agent', () => { liana_features: { 'webhook-custom-actions': expect.stringMatching(/\d+\.\d+\.\d+.*/), }, + ai_llms: null, stack: expect.anything(), }, }); @@ -329,4 +331,73 @@ describe('Agent', () => { }); }); }); + + describe('addAI', () => { + const options = factories.forestAdminHttpDriverOptions.build({ + isProduction: false, + forestAdminClient: factories.forestAdminClient.build({ postSchema: mockPostSchema }), + }); + + test('should store the AI configuration', () => { + const agent = new Agent(options); + const result = agent.addAI({ + provider: 'openai', + apiKey: 'test-key', + model: 'gpt-4', + }); + + expect(result).toBe(agent); + }); + + test('should throw an error when called more than once', () => { + const agent = new Agent(options); + + agent.addAI({ + provider: 'openai', + apiKey: 'test-key', + model: 'gpt-4', + }); + + expect(() => + agent.addAI({ + provider: 'openai', + apiKey: 'another-key', + model: 'gpt-4-turbo', + }), + ).toThrow('addAI() can only be called once'); + }); + + test('should include ai_llms in schema meta when AI is configured', async () => { + const agent = new Agent(options); + agent.addAI({ + provider: 'openai', + apiKey: 'test-key', + model: 'gpt-4', + }); + + await agent.start(); + + expect(mockPostSchema).toHaveBeenCalledWith( + expect.objectContaining({ + meta: expect.objectContaining({ + ai_llms: [{ provider: 'openai' }], + }), + }), + ); + }); + + test('should not include ai_llms in schema meta when AI is not configured', async () => { + const agent = new Agent(options); + + await agent.start(); + + expect(mockPostSchema).toHaveBeenCalledWith( + expect.objectContaining({ + meta: expect.objectContaining({ + ai_llms: null, + }), + }), + ); + }); + }); }); diff --git a/packages/agent/test/routes/ai/ai-proxy.test.ts b/packages/agent/test/routes/ai/ai-proxy.test.ts new file mode 100644 index 0000000000..ad36c7238f --- /dev/null +++ b/packages/agent/test/routes/ai/ai-proxy.test.ts @@ -0,0 +1,90 @@ +import { createMockContext } from '@shopify/jest-koa-mocks'; + +import AiProxyRoute from '../../../src/routes/ai/ai-proxy'; +import { RouteType } from '../../../src/types'; +import * as factories from '../../__factories__'; + +jest.mock('@forestadmin/ai-proxy', () => ({ + Router: jest.fn().mockImplementation(() => ({ + route: jest.fn().mockResolvedValue({ result: 'success' }), + })), +})); + +describe('AiProxyRoute', () => { + const services = factories.forestAdminHttpDriverServices.build(); + const router = factories.router.mockAllMethods().build(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('constructor', () => { + it('should create an AiProxyRouter with the configuration', () => { + const { Router } = jest.requireMock('@forestadmin/ai-proxy'); + const options = factories.forestAdminHttpDriverOptions.build(); + const aiConfiguration = { provider: 'openai' as const, apiKey: 'test-key', model: 'gpt-4' }; + + // eslint-disable-next-line no-new + new AiProxyRoute(services, options, aiConfiguration); + + expect(Router).toHaveBeenCalledWith({ + aiConfiguration, + logger: options.logger, + }); + }); + }); + + describe('type', () => { + it('should be a private route', () => { + const options = factories.forestAdminHttpDriverOptions.build(); + const aiConfiguration = { provider: 'openai' as const, apiKey: 'test-key', model: 'gpt-4' }; + + const route = new AiProxyRoute(services, options, aiConfiguration); + + expect(route.type).toBe(RouteType.PrivateRoute); + }); + }); + + describe('setupRoutes', () => { + it('should register POST /_internal/ai-proxy/:route', () => { + const options = factories.forestAdminHttpDriverOptions.build(); + const aiConfiguration = { provider: 'openai' as const, apiKey: 'test-key', model: 'gpt-4' }; + + const route = new AiProxyRoute(services, options, aiConfiguration); + route.setupRoutes(router); + + expect(router.post).toHaveBeenCalledWith('/_internal/ai-proxy/:route', expect.any(Function)); + }); + }); + + describe('handleAiProxy', () => { + it('should route requests through the AI proxy router', async () => { + const mcpServerConfigService = { + getConfiguration: jest.fn().mockResolvedValue({ mcpServers: [] }), + }; + const options = factories.forestAdminHttpDriverOptions.build({ + forestAdminClient: { + mcpServerConfigService, + } as never, + }); + const aiConfiguration = { provider: 'openai' as const, apiKey: 'test-key', model: 'gpt-4' }; + + const route = new AiProxyRoute(services, options, aiConfiguration); + + const context = createMockContext({ + requestBody: { message: 'hello' }, + customProperties: { + params: { route: 'chat' }, + query: { stream: 'true' }, + }, + }); + + // Call the handler - access private method for testing + // eslint-disable-next-line @typescript-eslint/dot-notation + await route['handleAiProxy'](context); + + expect(context.response.body).toEqual({ result: 'success' }); + expect(context.response.status).toBe(200); + }); + }); +}); diff --git a/packages/agent/test/routes/index.test.ts b/packages/agent/test/routes/index.test.ts index 2afaaa7ceb..65040ce6dc 100644 --- a/packages/agent/test/routes/index.test.ts +++ b/packages/agent/test/routes/index.test.ts @@ -34,6 +34,13 @@ import Logger from '../../src/routes/system/logger'; import { RouteType } from '../../src/types'; import * as factories from '../__factories__'; +// Mock the ai-proxy module to avoid langchain module resolution issues in tests +jest.mock('@forestadmin/ai-proxy', () => ({ + Router: jest.fn().mockImplementation(() => ({ + route: jest.fn(), + })), +})); + describe('Route index', () => { it('should declare all the routes', () => { expect(ROOT_ROUTES_CTOR).toEqual([ @@ -298,5 +305,30 @@ describe('Route index', () => { expect(lqRoute).toBeTruthy(); }); }); + + describe('with AI configuration', () => { + test('should instantiate AI proxy route when aiConfiguration is provided', async () => { + const dataSource = factories.dataSource.buildWithCollection( + factories.collection.build({ name: 'books' }), + ); + const aiConfiguration = { provider: 'openai' as const, apiKey: 'test-key', model: 'gpt-4' }; + + const routes = makeRoutes( + dataSource, + factories.forestAdminHttpDriverOptions.build(), + factories.forestAdminHttpDriverServices.build(), + aiConfiguration, + ); + + // Should have one more route than without AI configuration + const routesWithoutAi = makeRoutes( + dataSource, + factories.forestAdminHttpDriverOptions.build(), + factories.forestAdminHttpDriverServices.build(), + ); + + expect(routes.length).toEqual(routesWithoutAi.length + 1); + }); + }); }); }); diff --git a/packages/agent/test/utils/forest-schema/generator.test.ts b/packages/agent/test/utils/forest-schema/generator.test.ts index e9a8f11d40..63d19324cc 100644 --- a/packages/agent/test/utils/forest-schema/generator.test.ts +++ b/packages/agent/test/utils/forest-schema/generator.test.ts @@ -40,6 +40,7 @@ describe('SchemaGenerator', () => { liana: 'forest-nodejs-agent', liana_version: expect.any(String), liana_features: null, + ai_llms: null, stack: { engine: 'nodejs', engine_version: expect.any(String), @@ -62,6 +63,7 @@ describe('SchemaGenerator', () => { 'webhook-custom-actions': '1.0.0', 'awesome-feature': '3.0.0', }, + ai_llms: null, stack: { engine: 'nodejs', engine_version: expect.any(String), diff --git a/packages/ai-proxy/src/provider-dispatcher.ts b/packages/ai-proxy/src/provider-dispatcher.ts index 53a63432df..4d13c70db7 100644 --- a/packages/ai-proxy/src/provider-dispatcher.ts +++ b/packages/ai-proxy/src/provider-dispatcher.ts @@ -9,7 +9,8 @@ import { AINotConfiguredError, OpenAIUnprocessableError } from './errors'; export type OpenAiConfiguration = ClientOptions & { provider: 'openai'; - model: ChatCompletionCreateParamsNonStreaming['model']; + // Allow string to support custom models or new model versions without updating the package + model: ChatCompletionCreateParamsNonStreaming['model'] | string; }; export type AiConfiguration = OpenAiConfiguration; diff --git a/packages/forestadmin-client/package.json b/packages/forestadmin-client/package.json index d2ce68f5a6..5c30ed6fa1 100644 --- a/packages/forestadmin-client/package.json +++ b/packages/forestadmin-client/package.json @@ -31,7 +31,7 @@ "test": "jest" }, "devDependencies": { - "@forestadmin/ai-proxy": "0.1.0", + "@forestadmin/ai-proxy": "1.0.0", "@forestadmin/datasource-toolkit": "1.50.0", "@types/json-api-serializer": "^2.6.3", "@types/jsonwebtoken": "^9.0.1", diff --git a/yarn.lock b/yarn.lock index afaf34dfb5..488c8de1cb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1794,56 +1794,6 @@ path-to-regexp "^6.3.0" reusify "^1.0.4" -"@forestadmin/agent@1.65.1": - version "1.65.1" - resolved "https://registry.yarnpkg.com/@forestadmin/agent/-/agent-1.65.1.tgz#64f37ac6a85eeef1f585964e1c861773306a9d7d" - integrity sha512-B5FuDnF77Q9Z11cRdssRWkp3RQB6UoFyhrBtZbq/veWbXd4Dg3TlUdcSOI4ilM4cbLDXX7m6jmD27HPrSwttEw== - dependencies: - "@fast-csv/format" "^4.3.5" - "@fastify/express" "^1.1.0" - "@forestadmin/datasource-customizer" "1.67.1" - "@forestadmin/datasource-toolkit" "1.50.0" - "@forestadmin/forestadmin-client" "1.36.14" - "@koa/bodyparser" "^6.0.0" - "@koa/cors" "^5.0.0" - "@koa/router" "^13.1.0" - "@types/koa__router" "^12.0.4" - forest-ip-utils "^1.0.1" - json-api-serializer "^2.6.6" - json-stringify-pretty-compact "^3.0.0" - jsonwebtoken "^9.0.0" - koa "^2.16.1" - koa-jwt "^4.0.4" - luxon "^3.2.1" - object-hash "^3.0.0" - superagent "^10.2.3" - uuid "11.0.2" - -"@forestadmin/agent@1.66.0": - version "1.66.0" - resolved "https://registry.yarnpkg.com/@forestadmin/agent/-/agent-1.66.0.tgz#94c3143ced404e15288c45588be6739338051ad8" - integrity sha512-WqfDZzvAozelYaQPMl1BWIZTgT7oO6FdrZqipezpyc+wUMrWzsJ3akOwH2L8OLgIkWTnf4die/nzg7Q6XLc8BQ== - dependencies: - "@fast-csv/format" "^4.3.5" - "@fastify/express" "^1.1.0" - "@forestadmin/datasource-customizer" "1.67.1" - "@forestadmin/datasource-toolkit" "1.50.0" - "@forestadmin/forestadmin-client" "1.36.14" - "@koa/bodyparser" "^6.0.0" - "@koa/cors" "^5.0.0" - "@koa/router" "^13.1.0" - "@types/koa__router" "^12.0.4" - forest-ip-utils "^1.0.1" - json-api-serializer "^2.6.6" - json-stringify-pretty-compact "^3.0.0" - jsonwebtoken "^9.0.0" - koa "^3.0.1" - koa-jwt "^4.0.4" - luxon "^3.2.1" - object-hash "^3.0.0" - superagent "^10.2.3" - uuid "11.0.2" - "@forestadmin/context@1.37.1": version "1.37.1" resolved "https://registry.yarnpkg.com/@forestadmin/context/-/context-1.37.1.tgz#301486c456061d43cb653b3e8be60644edb3f71a" @@ -11465,35 +11415,6 @@ koa@^2.13.4: type-is "^1.6.16" vary "^1.1.2" -koa@^2.16.1: - version "2.16.3" - resolved "https://registry.yarnpkg.com/koa/-/koa-2.16.3.tgz#dd3a250472862cf7a3ef6e25bf325cc9db620ab5" - integrity sha512-zPPuIt+ku1iCpFBRwseMcPYQ1cJL8l60rSmKeOuGfOXyE6YnTBmf2aEFNL2HQGrD0cPcLO/t+v9RTgC+fwEh/g== - dependencies: - accepts "^1.3.5" - cache-content-type "^1.0.0" - content-disposition "~0.5.2" - content-type "^1.0.4" - cookies "~0.9.0" - debug "^4.3.2" - delegates "^1.0.0" - depd "^2.0.0" - destroy "^1.0.4" - encodeurl "^1.0.2" - escape-html "^1.0.3" - fresh "~0.5.2" - http-assert "^1.3.0" - http-errors "^1.6.3" - is-generator-function "^1.0.7" - koa-compose "^4.1.0" - koa-convert "^2.0.0" - on-finished "^2.3.0" - only "~0.0.2" - parseurl "^1.3.2" - statuses "^1.5.0" - type-is "^1.6.16" - vary "^1.1.2" - koa@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/koa/-/koa-3.0.1.tgz#b211a0f350d1cc6185047671f8ef7e019c16351d" @@ -16210,16 +16131,7 @@ string-similarity@^4.0.1: resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-4.0.4.tgz#42d01ab0b34660ea8a018da8f56a3309bb8b2a5b" integrity sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -16295,7 +16207,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -16323,13 +16235,6 @@ strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -17663,7 +17568,7 @@ wordwrap@>=0.0.2, wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -17681,15 +17586,6 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"